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

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

Con trỏ & hàm

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

Con trỏ và hàm trong C: truyền địa chỉ (pass by pointer) để hàm sửa được biến gốc; viết hàm hoanVi (swap), out-parameter và bẫy dangling pointer.

khoá cơ bản ta thấy: hàm nhận bản sao của đối số, nên sửa tham số bên trong không đổi biến gốc. Thử viết hoanVi kiểu “ngây thơ” - nó thất bại:

swap-sai.c

#include <stdio.h>

void hoanViSai(int x, int y) {  // nhan BAN SAO
    int t = x; x = y; y = t;    // chi doi ban sao
}

int main() {
    int a = 3, b = 7;
    hoanViSai(a, b);
    printf("a=%d b=%d\n", a, b);  // van 3 va 7!
    return 0;
}

Kết quả khi chạy

a=3 b=7

Vấn đề

hoanViSai đổi x, y - là bản sao của a, b. Khi hàm kết thúc, bản sao biến mất, ab y nguyên. Hàm cần biết a, b nằm ở đâu.

Giải pháp: truyền địa chỉ của a và b (&a, &b) vào các tham số con trỏ. Bên trong hàm, *pa chính là a - ghi vào *pa là ghi thẳng vào a. Bấm Bước tiếp để xem swap diễn ra trong bộ nhớ:

Bước 1/8
con-tro.c
Địa chỉBiếnGiá trị
0x100 int a 3
int a = 3; - Cấp phát a tại địa chỉ 0x100 (4 byte), gán 3.

swap-dung.c

#include <stdio.h>

void hoanVi(int *pa, int *pb) {  // nhan DIA CHI
    int t = *pa;                 // giu gia tri cu cua a
    *pa = *pb;                   // a <- b
    *pb = t;                     // b <- a (cu)
}

int main() {
    int a = 3, b = 7;
    hoanVi(&a, &b);              // truyen dia chi cua a, b
    printf("a=%d b=%d\n", a, b);  // 7 va 3 - da hoan vi!
    return 0;
}

Kết quả khi chạy

a=7 b=3
  • Tham số con trỏ: void hoanVi(int *pa, int *pb).
  • Gọi với địa chỉ: hoanVi(&a, &b) - KHÔNG phải hoanVi(a, b).
  • Trong hàm, *pa là biến gốc a; ghi *pa = … đổi luôn a ở main.

Còn nhớ scanf("%d", &x) ở khoá cơ bản? Dấu & chính là “truyền địa chỉ để hàm ghi vào biến”. scanf cần biết x ở đâu để đặt giá trị đọc được vào đó:

scanf-la-con-tro.c

int tuoi;
scanf("%d", &tuoi);   // & : dua DIA CHI de scanf ghi ket qua vao tuoi
printf("%d", tuoi);   // khong & : chi can GIA TRI de in
  • scanf GHI vào biến ⇒ cần địa chỉ (&tuoi).
  • printf chỉ ĐỌC biến ⇒ truyền giá trị là đủ (không &).
  • Quên & trong scanf là lỗi kinh điển - scanf ghi vào địa chỉ rác.

return chỉ trả một giá trị. Muốn một hàm trả nhiều kết quả, truyền thêm các con trỏ làm “tham số ra” (out-parameter) để hàm ghi kết quả vào:

chia-lay-du.c

#include <stdio.h>

void chia(int tu, int mau, int *thuong, int *du) {
    *thuong = tu / mau;   // ghi thuong ra ngoai
    *du = tu % mau;       // ghi du ra ngoai
}

int main() {
    int q, r;
    chia(17, 5, &q, &r);
    printf("%d du %d\n", q, r);  // 3 du 2
    return 0;
}

Kết quả khi chạy

3 du 2

Lỗi nguy hiểm nhất khi trả con trỏ từ hàm: trỏ tới biến cục bộ đã chết.

dangling.c

int* taoSai() {
    int n = 5;
    return &n;     // SAI: n bien mat khi ham ket thuc
}                  // -> con tro "treo" (dangling), tro toi vung nho da thu hoi
// Trinh bien dich CANH BAO: "address of stack memory returned"

Ghi nhớ

Chỉ trả về con trỏ tới bộ nhớ còn sống: tham số truyền vào, biến static, hoặc vùng malloc (bài sau). Và khi gọi hàm sửa biến, đừng quên &: hoanVi(&a, &b), không phải hoanVi(a, b).

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

Vì hàm nhận một BẢN SAO của đối số (Bài Hàm ở khoá cơ bản). Sửa bản sao không đụng bản gốc. Muốn sửa bản gốc, hàm phải biết nó NẰM Ở ĐÂU - tức là nhận địa chỉ của nó (con trỏ).

Bài Hàm - khoá C cơ bản →

Bạn truyền &a (địa chỉ của a) vào một tham số con trỏ int *pa. Bên trong hàm, *pa chính là a - ghi vào *pa là ghi thẳng vào a ở nơi gọi. Hàm không cần biết tên a, chỉ cần địa chỉ.

Không hẳn. C chỉ có truyền theo GIÁ TRỊ - nhưng ta truyền giá trị của một ĐỊA CHỈ (con trỏ), đạt hiệu quả tương đương. C++ có tham chiếu thật (int &x). Trong C, con trỏ là cách duy nhất để hàm sửa biến nơi gọi.

scanf phải GHI giá trị đọc được vào x → nó cần ĐỊA CHỈ của x để ghi vào. printf chỉ ĐỌC giá trị x để in → truyền giá trị là đủ. Đây chính là “truyền địa chỉ để hàm sửa biến” bạn đã dùng từ những bài đầu tiên, chỉ là chưa để ý!

return chỉ trả được một giá trị. Mẹo: truyền thêm các con trỏ làm “tham số ra” (out-parameter). Vd void chia(int tu, int mau, int *thuong, int *du) ghi kết quả qua *thuong*du. Rất phổ biến trong thư viện C.

Chỉ khi nó trỏ tới bộ nhớ CÒN SỐNG sau khi hàm kết thúc: một tham số truyền vào, biến static, hoặc vùng malloc (bài sau). TUYỆT ĐỐI không return &biến_cục_bộ - biến đó biến mất khi hàm thoát, con trỏ thành “treo” (dangling).

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

Vì sao hàm hoanViSai(int x, int y) không đổi được biến gốc ở nơi gọi?

  1. 1

    Theo dõi swap

    Dùng mô phỏng ở Bước 2, dừng ở bước “*pa = *pb;”. Lúc này a, b, t bằng bao nhiêu? Vì sao cần biến t?

    Hoàn thành khi: a=7, b=7, t=3. Cần t để GIỮ giá trị cũ của a trước khi ghi đè, nếu không sẽ mất nó (b cũng thành 7).

  2. 2

    Hàm tăng

    Viết void tang(int *p) làm tăng giá trị p trỏ tới lên 1. Gọi tang(&x) với x = 9 thì x thành mấy?

    Hoàn thành khi: void tang(int *p) { (*p)++; }x = 10. Nhớ (*p)++ chứ không phải *p++.

  3. 3

    Vì sao scanf có &?

    Giải thích vì sao scanf("%d", &tuoi)& nhưng printf("%d", tuoi) lại không.

    Hoàn thành khi: scanf GHI vào tuoi nên cần địa chỉ (&tuoi); printf chỉ ĐỌC tuoi nên truyền giá trị là đủ.

  4. 4

    Quên dấu &

    Khi gọi hoanVi(a, b) thay vì hoanVi(&a, &b) thì điều gì xảy ra?

    Hoàn thành khi: Sai kiểu: hàm cần int* nhưng nhận int → trình biên dịch báo lỗi. (Nếu ép kiểu liều, a, b bị hiểu là địa chỉ rác → sập.)

  5. 5

    Con trỏ treo

    Vì sao hàm sau bị lỗi? int* tao() { int n = 5; return &n; }

    Hoàn thành khi: n là biến cục bộ, biến mất khi tao() kết thúc. Con trỏ trả về trỏ tới vùng nhớ không còn hợp lệ (dangling) → dùng nó là hành vi không xác định.

  6. 6

    Trả về hai giá trị

    Viết void chia(int tu, int mau, int *thuong, int *du) để trả CẢ thương và dư qua con trỏ.

    Hoàn thành khi: *thuong = tu / mau; *du = tu % mau; - gọi: chia(17, 5, &q, &r);q=3, r=2.