← Lập trình C nâng cao

Bài 8 · Nâng cao · 24 phút· Cập nhật 11/06/2026

Struct & con trỏ struct

Biên soạn bởi Nguyễn Anh Tuấn

Struct và con trỏ struct trong C: toán tử ->, malloc một struct trên heap, và danh sách liên kết (linked list) - struct trỏ tới chính kiểu của nó.

struct gộp nhiều trường (field) thành một kiểu mới. Có một struct giá trị, ta truy cập trường bằng dấu chấm (.):

struct.c

#include <stdio.h>

struct Diem {
    int x;
    int y;
};

int main() {
    struct Diem p;
    p.x = 3;
    p.y = 4;
    printf("%d %d\n", p.x, p.y);  // 3 4
    return 0;
}

Kết quả khi chạy

3 4
  • struct Tên { kiểu trường1; kiểu trường2; ... };
  • Truy cập trường của một struct giá trị bằng dấu chấm: p.x.
  • struct cho phép tạo “kiểu dữ liệu của riêng bạn” từ các kiểu cơ bản.

Khi có một con trỏ tới struct, dùng mũi tên (->) để truy cập trường. pp->x chỉ là viết tắt của (*pp).x:

arrow.c

#include <stdio.h>

struct Diem { int x; int y; };

int main() {
    struct Diem p = {3, 4};
    struct Diem *pp = &p;     // con tro toi p

    printf("%d\n", pp->x);    // 3   = (*pp).x
    pp->y = 9;                // sua p qua con tro
    printf("%d\n", p.y);      // 9
    return 0;
}

Kết quả khi chạy

3
9
  • p.x - khi p là struct GIÁ TRỊ.
  • pp->x - khi pp là CON TRỎ struct.
  • pp->x ≡ (*pp).x (dereference rồi lấy trường).

Ghép với bài trước: ta có thể malloc một struct trên heap. Kết quả là một con trỏ struct, nên truy cập trường bằng ->:

malloc-struct.c

#include <stdio.h>
#include <stdlib.h>

struct Diem { int x; int y; };

int main() {
    struct Diem *p = malloc(sizeof(struct Diem));
    p->x = 5;
    p->y = 7;
    printf("%d %d\n", p->x, p->y);  // 5 7
    free(p);
    return 0;
}

Kết quả khi chạy

5 7

Điểm lợi hại nhất: một struct chứa con trỏ tới chính kiểu của nó (struct Node *next) - tạo thành chuỗi nút nối nhau. Bấm Bước tiếp để dựng rồi duyệt danh sách 10 → 20 → 30 → NULL:

Bước 1/12
danh-sach.c
Bộ nhớ (heap): các nút nối nhau bằng next
đầu 10
next
NULL
In ra: (chưa in gì)
Node *a = newNode(10); - Cấp phát một Node trên heap, val = 10, next = NULL.

linked-list.c

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int val;
    struct Node *next;     // tro toi nut ke (chinh kieu Node)
} Node;

int main() {
    Node *a = malloc(sizeof(Node)); a->val = 10;
    Node *b = malloc(sizeof(Node)); b->val = 20;
    a->next = b;
    b->next = NULL;        // cuoi danh sach

    for (Node *p = a; p != NULL; p = p->next)
        printf("%d ", p->val);   // 10 20

    free(b); free(a);          // tra lai heap
    return 0;
}

Kết quả khi chạy

10 20

Bốn lỗi struct/con-trỏ thường gặp:

  • Nhầm . với ->: dùng . trên con trỏ (p.x khi p là Node *) → lỗi biên dịch. Có con trỏ thì dùng ->.
  • struct Node next (không có *) → kích thước vô hạn; phải là struct Node *next.
  • Duyệt mà quên kiểm tra p != NULL → deref NULL ở cuối danh sách → sập.
  • Quên free từng nút (mỗi nút một malloc) → rò rỉ bộ nhớ.

Giải phóng danh sách cho đúng

Phải lưu next trước khi free: while (p) { Node *t = p->next; free(p); p = t; }. Nếu free(p) trước rồi mới đọc p->next, bạn đã đọc vùng nhớ vừa giải phóng - mất đường tới phần còn lại của danh sách (use-after-free).

Câu hỏi thường gặp

Gói nhiều trường (field) có liên quan dưới MỘT kiểu/tên - ví dụ struct Diem { int x; int y; } gộp hai toạ độ. Thay vì truyền x, y rời rạc, ta truyền một Diem. struct là cách C xây “kiểu dữ liệu của riêng bạn”.

Ôn lại: Bài Struct & kiểu tự đặt tên - khoá C cơ bản →

Dùng dấu chấm (.) khi bạn có một struct GIÁ TRỊ: p.x. Dùng mũi tên (->) khi bạn có một CON TRỎ struct: pp->x. pp->x chỉ là viết tắt của (*pp).x - dereference rồi lấy trường. Có con trỏ thì gần như luôn dùng ->.

Nếu một Node CHỨA nguyên một Node bên trong thì kích thước sẽ vô hạn (Node chứa Node chứa Node…). Con trỏ chỉ rộng 8 byte và TRỎ tới nút kế - nên struct tự tham chiếu luôn dùng con trỏ.

Mảng: các phần tử liền khối, truy cập a[i] tức thì, nhưng kích thước cố định và chèn/xoá giữa chừng tốn kém. Danh sách liên kết: các nút rải rác nối bằng con trỏ next, thêm/xoá dễ, lớn nhỏ tuỳ ý - nhưng phải đi TUẦN TỰ (không có a[i]) và mỗi nút tốn thêm bộ nhớ cho con trỏ.

Bài Con trỏ & mảng →

Đánh dấu HẾT danh sách. Vòng duyệt chạy while (p != NULL) và dừng đúng lúc. Quên gán next cuối = NULL (để nó là rác) → vòng duyệt đi lố vào vùng nhớ bậy → sập.

Đi từng nút, nhưng phải LƯU p->next vào biến tạm TRƯỚC khi free(p) - vì sau free(p) ta không còn đọc được p->next nữa (mất đường tới phần còn lại). Mỗi nút malloc cần đúng một free.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Khi pp là một con trỏ struct, pp->x là viết tắt của biểu thức nào?

  1. 1

    Điều kiện dừng

    Trong mô phỏng ở Bước 4, ở bước cuối con trỏ p bằng gì khiến vòng while dừng?

    Hoàn thành khi: p = NULL (next của nút cuối cNULL) ⇒ while (p != NULL) sai ⇒ dừng. Đã in 10 20 30.

  2. 2

    Truy cập trường

    struct Diem p; p.x = 3; - truy cập trường y bằng cú pháp nào?

    Hoàn thành khi: p.y - dấu chấm vì p là struct giá trị (không phải con trỏ).

  3. 3

    Qua con trỏ

    struct Diem *pp = &p; thì pp->x tương đương cách viết nào dùng *?

    Hoàn thành khi: (*pp).x - pp->x chính là viết tắt của nó.

  4. 4

    Struct tự tham chiếu

    Vì sao Node phải khai báo struct Node *next mà không phải struct Node next?

    Hoàn thành khi: struct Node next sẽ làm kích thước vô hạn (đệ quy). Con trỏ (8 byte) chỉ TRỎ tới nút kế.

  5. 5

    Tổng danh sách

    Danh sách 10 → 20 → 30 → NULL. Viết vòng duyệt tính TỔNG các val. Kết quả?

    Hoàn thành khi: int s = 0; for (Node *p = a; p; p = p->next) s += p->val;s = 60.

  6. 6

    Giải phóng đúng

    Khi free cả danh sách, vì sao phải lưu p->next TRƯỚC khi free(p)?

    Hoàn thành khi: Sau free(p), p->next không còn đọc được (vùng đã giải phóng) → mất con trỏ tới phần còn lại của danh sách.