Sunday, March 27, 2016

Bài 10: Kiểu dữ liệu trong Go: Con trỏ

Một nhóm kiểu dữ liệu quan trọng trong Go là kiểu dữ liệu tham chiếu, là kiểu dữ liệu mà bản thân nó không chứa dữ liệu thật sự cần xử lý mà chứa con trỏ tham chiếu đến địa chỉ vùng nhớ lưu trữ dữ liệu. Vậy con trỏ là gì?


Con trỏ


Thông thường các biến lưu giữ các giá trị ứng với kiểu dữ liệu mà nó khai báo tại vùng nhớ gắn với nó. Ví dụ khi khai báo i := 10 thì chúng ta có biến i kiểu dữ liệu int chứa giá trị 10 tại vùng nhớ gắn với nó. Khi chúng ta gán lại i = 5 thì tại vùng nhớ của biến i lúc này sẽ lưu giá trị 5.

Vùng nhớ gắn với biến hay còn gọi là địa chỉ của biến, đó là địa chỉ đầu vùng nhớ của biến. Thông tin địa chỉ của biến có ý nghĩa nhất định nên như các ngôn ngữ khác Go cung cấp toán tử & lấy địa chỉ của một biến. Nên khi chúng ta gọi &i sẽ cho giá trị 0xc082008340.

Có được địa chỉ của biến i thì có thể truy xuất giá trị mà biến i lưu trữ và thậm chí có thể thay đổi giá trị này nên nhu cầu biến lưu giữ địa chỉ của một biến khác như biến i ở trên là có. Biến này Go gọi là biến con trỏ (pointer), tương tự như ở các ngôn ngữ khác.

Biến con trỏ một kiểu dữ liệu sẽ được khai báo như sau: var <tên biến con trỏ> *<tên kiểu dữ liệu>.
1    i := 10 
2    fmt.Println(i)     //  10 
3    fmt.Println(&i)    //   0xc082008340 
4    var p1 *int 
5    fmt.Println(p1)    //   nil 
6    p1 = &i 
7    p2 := &i 
8    fmt.Println(p1)    //  0xc082008340 
9    fmt.Println(p2)    //  0xc082008340 
10   fmt.Println(&p1)   //  0xc082024020 
- Dòng 1 khai báo biến i kiểu int có giá trị 10 như ta thấy kết quả in ra ở dòng 2.
- Dòng 3 in ra địa chỉ biến i.
- Dòng 4 & 5 khai báo biến kiểu con trỏ int p1 và in giá trị nó ra. Kết quả là nil. Đây là giá trị zero mặc định mà Go gán cho biến con trỏ, ý nghĩa là không trỏ đến đâu hết.
- Dòng 6 gán giá trị biến p1 là địa chỉ biến i.
- Dòng 7 là một kiểu khai báo nhanh biến con trỏ p2.
- Dòng 8 và 9 in giá trị 2 biến con trỏ p1 và p2 và kết quả như chúng ta thấy là bằng nhau vì cùng trỏ đến biến i.
- Dòng 10 in địa chỉ biến con trỏ p1. Có thể diễn giải là tại địa chỉ biến p1 có giá trị 0xc082024020 lưu địa chỉ của biến i có giá trị là 0xc082008340.

Để truy xuất giá trị mà một biến con trỏ trỏ đến chúng ta dùng toán tử *<biến con trỏ>. Ví dụ ở trên *p1 hay *p2 sẽ cho giá trị 10. *p1 tương đương biến i nên khi cần gán lại giá trị cho i, ta có thể dùng *p1 = 5. Lúc này giá trị biến i cũng sẽ là 5.

fmt.Println(*p1)  // 10 
*p1 = 5 
fmt.Println(i)    // 5 
Do giá trị của biến con trỏ luôn là địa chỉ của biến kiểu dữ liệu khác nên kích thước luôn nhỏ (tùy theo kiến trúc máy: 16 bit, 32 bit hay 64 bit), tiện cho việc khi dùng là tham số của hàm thay vì phải truyền giá trị cồng kềnh như biến giá trị kiểu mảng hay cấu trúc. Bên cạnh đó khi truyền kiểu con trỏ cho hàm, chúng ta dễ dàng thay đổi giá trị của biến mà con trỏ trỏ đến. Chúng ta sẽ bàn kỹ hơn ở phần về hàm.

Con trỏ có thể so sánh bằng (==) hoặc khác (!=). Thường dùng nhất khi kiểm tra coi 1 hàm có trả về lỗi hay không. Go không cung cấp toán tử ++ cho biến con trỏ như C/C++.

Hàm new


Trong ví dụ nêu trên, biến i còn được sử dụng nhưng trong một số tình huống chúng ta chỉ cần tạo ra biến con trỏ trỏ đến một biến kiểu dữ liệu nào đó rồi thao tác trên biến con trỏ mà không cần quan tâm đến biến chứa giá trị nữa. Lúc này việc tạo ra biến và đặt tên là không cần thiết. Để giải quyết vấn đề này, Go cung cấp hàm new. Nó sẽ giúp tạo ra một biến không tên của một kiểu dữ liệu nào đó qua cách khai báo new(<kiểu dữ liệu>), trả về con trỏ kiểu dữ liệu đó. Áp dụng vào ví dụ trên:
var p1 = new(int) 
fmt.Println(p1)    //  0xc082008340 
fmt.Println(*p1)   //  0 
*p1 = 10 
fmt.Println(*p1)   //  10 
- Dòng 1 khai báo biến con trỏ p1 và gán giá trị trả về từ hàm new. Lúc này ta có biến p1 là biến con trỏ kiểu int trỏ đến 1 biến không đặt tên.
- Dòng 2 và 3 in ra giá trị biến con trỏ lưu giữ và giá trị mà nó trỏ đến. Như thường lệ, Go khởi tạo mặc định giúp chúng ta nên ở dòng 3 cho giá trị 0.
- Dòng 4 gán giá trị cho biến mà con trỏ p1 trỏ đến và dòng 5 in giá trị của biến được trỏ đến đó ra màn hình.

Hàm new không được sử dụng nhiều và nó cũng không phải là từ khóa nên chúng ta có quyền đặt tên biến hay hàm trùng với nó. Tất nhiên, lúc này hàm new do Go cung cấp sẽ mất tác dụng.


Trong bài tới chúng ta sẽ tìm hiểu về 2 kiểu tham chiếu được dùng rất phổ biến trong Go là slice và map.



Tóm tắt:
- Con trỏ:
 + Là biến lưu giữ địa chỉ chứa dữ liệu thay vì chứa dữ liệu. Kích thước vùng nhớ luôn cố định do không phụ thuộc kiểu dữ liệu.
 + Khai báo: var <tên biến con trỏ> *<kiểu dữ liệu>
 + Toán tử: *p để lấy giá trị biến con trỏ p trỏ đến và &i để lấy địa chỉ vùng nhớ biến i.
 + Biến con trỏ có thể so sánh == hoặc !=.
- Hàm new:
 + Tạo vùng nhớ của một kiểu dữ liệu trả về địa chỉ vùng nhớ đó.
 + Sử dụng khi cần tạo biến con trỏ để thao tác không qua trung gian một biến dữ liệu.

No comments:

Post a Comment