Một struct truyền vào hàm sẽ bị sao chép. Với một điểm gồm hai số int thì không sao, nhưng với một bản ghi 200 byte thì lãng phí, và còn khiến việc sửa đổi là không thể, vì hàm chỉ thấy bản sao của riêng nó. Trỏ tới struct giải quyết cả hai vấn đề cùng lúc, và đây cũng là cách duy nhất để xây dựng các cấu trúc có thể lớn dần: danh sách, cây, đồ thị.
Lấy con trỏ tới một struct
Không cần gì đặc biệt. & cho bạn địa chỉ, và kiểu dữ liệu là "con trỏ tới struct":
Cả ba dòng đều in ra 3. Hai dòng cuối là cùng một thao tác được viết theo hai cách.
Vì sao (*ptr).x cần dấu ngoặc
Bạn có thể thử bỏ ngoặc và viết *ptr.x. Khi đó code được biên dịch thành một thứ hoàn toàn khác, hoặc thường gặp hơn là không biên dịch được, vì toán tử (operator) thành viên . có độ ưu tiên cao hơn toán tử giải tham chiếu *:
*ptr.x // parses as *(ptr.x) -- wrong
(*ptr).x // dereference first, then take the member -- correct
ptr->x // the same thing, said shortly
ptr.x đòi một thành viên của con trỏ, mà con trỏ thì không có thành viên nào. Thông báo lỗi của trình biên dịch ("request for member 'x' in something not a structure or union") thực ra là một bug về độ ưu tiên đội lốt.
Vì dạng có ngoặc vừa xấu vừa dễ viết sai, C cung cấp ->. Trong thực tế bạn gần như sẽ không bao giờ thấy (*p).x trong code thật; cứ dùng p->x và quên dạng dài đi, ngoại trừ khi cần giải thích mũi tên có nghĩa là gì.
Truyền struct bằng con trỏ
Tham số con trỏ đưa cho hàm chính struct thật của nơi gọi, nên các thay đổi được giữ lại:
Hai thói quen nên hình thành ở đây:
- Đánh dấu con trỏ chỉ đọc là
const.const struct Point *pcam kết hàm sẽ không thay đổi thứ màptrỏ tới. Nếu sau này ai đó thêmp->x = 0vào bên trong, việc biên dịch sẽ thất bại thay vì bug lọt ra ngoài. Nó cũng thể hiện rõ ý định ngay tại chỗ gọi. - Truyền con trỏ với struct lớn, kể cả khi chỉ đọc. Một struct có buffer tên 64 byte tốn 64 byte sao chép mỗi lần gọi theo giá trị; một con trỏ chỉ tốn 8.
Struct nhỏ vẫn có thể truyền theo giá trị: struct Point với 8 byte sao chép rẻ như một con trỏ, và bản truyền theo giá trị không thể là NULL, loại bỏ hẳn một kiểu lỗi.
Con trỏ vào mảng struct
Phép toán con trỏ trên mảng struct hoạt động giống hệt như trên mảng int: p + 1 tiến lên đúng một struct, tính cả phần đệm.
Tên mảng suy biến thành con trỏ tới phần tử đầu tiên, nên tại chỗ gọi staff đã là một struct Employee *. Đó cũng là lý do hàm cần nhận n riêng: sizeof bên trong hàm sẽ đo con trỏ chứ không đo mảng.
Cấp phát struct trên heap
Struct trên stack biến mất khi kết thúc phạm vi (scope) của nó. Để tạo một struct sống lâu hơn hàm đã tạo ra nó, hoặc để tạo bao nhiêu struct tùy theo đầu vào, hãy cấp phát bằng malloc:
Bốn chi tiết trong hàm nhỏ đó đều quan trọng:
sizeof *e, không phảisizeof(struct Employee). Nó có nghĩa là "kích thước của thứ màetrỏ tới", nên nếu kiểu dữ liệu thay đổi thì phần cấp phát tự động theo kịp. Hai thứ không thể lệch nhau.- Kiểm tra
NULL.malloctrả vềNULLkhi không đáp ứng được yêu cầu. Ghie->idqua con trỏ null là một lỗi segmentation fault. - Khởi tạo mọi thành viên.
mallockhông xóa vùng nhớ về 0; struct nhận về chứa đầy giá trị rác. (calloc(1, sizeof *e)sẽ đặt về 0 cho bạn.) - Phải có người gọi
free. Trả về một con trỏ đã cấp phát nghĩa là chuyển trách nhiệm đó cho nơi gọi. Hãy ghi rõ trong chú thích: quy tắc sở hữu không rõ ràng chính là khởi đầu của rò rỉ bộ nhớ.
Thành quả: một nút danh sách liên kết
Đây là điều struct không làm được nếu thiếu con trỏ. Một struct không thể chứa chính nó, vì như vậy nó sẽ lớn vô hạn, nhưng nó có thể chứa một con trỏ tới struct cùng loại, và chỉ một mẹo đó đủ để xây dựng mọi cấu trúc dữ liệu liên kết trong C.
Chú ý rằng struct Node *next; tham chiếu tới struct Node khi struct Node vẫn đang được khai báo. Điều này hợp lệ chính vì con trỏ có kích thước xác định bất kể nó trỏ tới đâu, nên trình biên dịch chưa cần định nghĩa đầy đủ. Đây cũng là chỗ duy nhất bạn bắt buộc phải dùng tag struct Node ngay cả khi đã viết typedef: tên typedef chưa tồn tại bên trong chính định nghĩa của nó.
Vòng lặp free_list lưu lại head->next trước khi gọi free(head). Đọc head->next sau khi free là lỗi use-after-free: vùng nhớ đó không còn là của bạn, và giá trị bạn đọc được là bất cứ thứ gì bộ cấp phát đã ghi vào.
Lỗi thường gặp
p->xtrên con trỏ chưa khởi tạo hoặcNULL. Nguyên nhân crash phổ biến nhất trong code dùng nhiều struct. Hãy khởi tạo con trỏ bằngNULLvà kiểm tra trước khi giải tham chiếu.- Trả về con trỏ tới struct cục bộ.
struct Point *bad(void) { struct Point p = {1,2}; return &p; }trả về địa chỉ của vùng nhớ không còn tồn tại ngay khi hàm trả về. Hãy trả về struct theo giá trị, hoặc cấp phát nó. - Free hai lần, hoặc quên free. Mỗi
mallocđi cặp với đúng mộtfree. Sau khi free, hãy gán con trỏ bằngNULLđể một lần vô tình dùng lại sau đó crash rõ ràng thay vì âm thầm làm hỏng bộ nhớ. - Sao chép struct có chứa con trỏ. Phép gán struct sao chép giá trị con trỏ chứ không sao chép thứ nó trỏ tới. Lúc này cả hai struct cùng tham chiếu một buffer, và struct nào free nó trước sẽ khiến struct kia thành con trỏ treo.
Câu hỏi thường gặp
Toán tử -> trong C dùng để làm gì?
p->x truy cập thành viên x thông qua con trỏ p. Nó tương đương hoàn toàn với (*p).x: giải tham chiếu con trỏ rồi lấy thành viên. Toán tử mũi tên tồn tại vì con trỏ tới struct phổ biến đến mức dạng viết dài làm code rối mắt.
Vì sao *p.x không dùng được với con trỏ struct?
Vì . có độ ưu tiên cao hơn *. *p.x được hiểu là *(p.x): lấy thành viên x của p (vốn là con trỏ chứ không phải struct) rồi giải tham chiếu nó. Bạn cần viết (*p).x, hoặc đơn giản là p->x.
Nên truyền struct theo giá trị hay bằng con trỏ trong C?
Truyền bằng con trỏ khi hàm cần sửa struct của nơi gọi, hoặc khi struct lớn và việc sao chép gây lãng phí. Truyền theo giá trị với các struct nhỏ chỉ đọc, khi bản sao rẻ và code đơn giản hơn. Đánh dấu tham số con trỏ chỉ đọc là const struct T * để trình biên dịch bắt buộc giữ lời hứa đó.
Làm sao cấp phát struct bằng malloc trong C?
struct Node *n = malloc(sizeof *n);. Dạng sizeof *n hỏi kích thước của thứ mà n trỏ tới, nên vẫn đúng nếu sau này bạn đổi kiểu. Kiểm tra kết quả với NULL, khởi tạo mọi thành viên, và gọi free(n) khi dùng xong.