Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 12.618 phút đọc

Mô phỏng truyền tham chiếu

Sau bài này bạn sẽ làm được

  • Cài ba mẫu tham số ra thông dụng
  • Viết hàm trả về nhiều giá trị bằng con trỏ
  • Kiểm tra con trỏ rỗng ở đầu hàm một cách nhất quán
  • Sửa được con trỏ của người gọi bằng con trỏ tới con trỏ

Nhiều ngôn ngữ có cú pháp riêng cho việc hàm sửa biến của người gọi. C không có. Nó chỉ có một cơ chế duy nhất, là truyền địa chỉ, và bài này gom lại mọi mẫu dùng cơ chế đó mà bạn sẽ gặp trong mã thật.

#C không có truyền tham chiếu

/* C++ có tham chiếu, viết như thế này */
void tang(int &x) { x += 1; }      /* LỖI CÚ PHÁP TRONG C */

tang(n);                            /* trông như truyền giá trị nhưng sửa được n */

/* C chỉ có cách này */
void tang(int *p) { *p += 1; }

tang(&n);                           /* dấu và ở chỗ gọi nói rõ ý định */
Tham chiếu của C++Con trỏ của C
Chỗ gọi có dấu hiệu gìKhông, trông như truyền giá trịCó dấu và, nhìn là biết hàm sẽ sửa
Có thể rỗng khôngKhôngCó, nên phải kiểm tra
Trỏ đi chỗ khác được khôngKhông, gắn vĩnh viễn
Cần dấu sao khi dùngKhông

#Ba mẫu tham số ra

Mẫu 1: sửa một giá trị có sẵn

mau-1.c
void nhan_doi(int *p)
{
    if (p == NULL) return;

    *p *= 2;
}

void hoan_doi(int *a, int *b)
{
    if (a == NULL || b == NULL || a == b) return;

    int t = *a;

    *a = *b;
    *b = t;
}

void chuan_hoa(double *x, double *y)
{
    double d = sqrt(*x * *x + *y * *y);

    if (d == 0.0) return;

    *x /= d;
    *y /= d;
}

Mẫu 2: ghi kết quả ra, kèm mã lỗi

mau-2.c
/* Trả về 0 nếu thành công, khác 0 nếu thất bại.
   Kết quả thật đi ra qua con trỏ. */
int chia(int a, int b, int *thuong)
{
    if (thuong == NULL) return -1;
    if (b == 0)         return -2;

    *thuong = a / b;

    return 0;
}

int kq;

switch (chia(17, 5, &kq)) {
    case  0: printf("%d\n", kq);                     break;
    case -1: fprintf(stderr, "Con tro rong\n");      break;
    case -2: fprintf(stderr, "Chia cho 0\n");        break;
}

Đây là mẫu của gần như mọi hàm hệ thống. Bạn đã gặp nó ở scanf từ Bài 3.3: giá trị trả về là số mục đọc được, còn dữ liệu thật đi ra qua các con trỏ.

Mẫu 3: ghi vào bộ đệm người gọi cấp

mau-3.c
/* Người gọi cấp bộ đệm và nói rõ nó rộng bao nhiêu.
   Trả về độ dài đáng lẽ cần, giống quy ước của snprintf. */
size_t doi_sang_chuoi(int n, char *ra, size_t co)
{
    int len = snprintf(ra, co, "%d", n);

    return len < 0 ? 0 : (size_t)len;
}

char bo_dem[16];

if (doi_sang_chuoi(12345, bo_dem, sizeof bo_dem) >= sizeof bo_dem)
    fprintf(stderr, "Bo dem qua nho\n");

#Trả về nhiều giá trị

nhieu.c
/* Chia có dư: trả về cả thương lẫn số dư. */
int chia_du(int a, int b, int *thuong, int *du)
{
    if (b == 0) return -1;

    if (thuong != NULL) *thuong = a / b;
    if (du     != NULL) *du     = a % b;

    return 0;
}

int t, d;

chia_du(17, 5, &t, &d);       /* lấy cả hai */
chia_du(17, 5, &t, NULL);     /* chỉ cần thương */
chia_du(17, 5, NULL, &d);     /* chỉ cần số dư */

So sánh với cách trả về struct

Ba tham số ra khi kết quả nhỏ
void phan_tich(int n, int *chan, int *le, int *tong);

int c, l, t;

phan_tich(100, &c, &l, &t);
Trả về struct
typedef struct {
    int chan, le, tong;
} KetQua;

KetQua phan_tich(int n);

KetQua r = phan_tich(100);

printf("%d %d %d\n", r.chan, r.le, r.tong);
Chọn cách nàoKhi nào
Tham số raKết quả lớn, hoặc cần phân biệt thành công với thất bại, hoặc người gọi muốn tái dùng bộ đệm
Trả về structKết quả nhỏ, dưới khoảng mười sáu byte, và luôn thành công

Bài 8.6 đã lập bảng đầy đủ về hai cách này. Điều mới ở đây là bạn đã hiểu vì sao tham số ra hoạt động: nó chỉ là truyền địa chỉ.

#Sửa cả con trỏ của người gọi

Đôi khi hàm cần đổi chính biến con trỏ của người gọi, không phải dữ liệu nó trỏ tới. Khi đó bạn cần thêm một tầng.

Một tầng, không đủ
void cap_phat(int *p, size_t n)
{
    p = malloc(n * sizeof *p);      /* chỉ đổi bản sao trong hàm */
}

int *a = NULL;

cap_phat(a, 10);
a[0] = 1;              /* a vẫn là NULL, chương trình sập */
Hai tầng
int cap_phat(int **pp, size_t n)
{
    if (pp == NULL) return -1;

    int *tam = malloc(n * sizeof *tam);

    if (tam == NULL) return -1;

    *pp = tam;                      /* ghi vào biến con trỏ của người gọi */

    return 0;
}

int *a = NULL;

if (cap_phat(&a, 10) == 0) {
    a[0] = 1;
    free(a);
}

Mẫu giải phóng an toàn

giai-phong.c
/* Giải phóng rồi đặt NULL luôn cho người gọi. */
void giai_phong(int **pp)
{
    if (pp == NULL || *pp == NULL) return;

    free(*pp);
    *pp = NULL;
}

int *a = malloc(40);

giai_phong(&a);
/* a giờ là NULL, không còn là con trỏ treo */

#Kiểm tra tham số nhất quán

Hàm nhận con trỏ phải quyết định rõ nó xử lý con trỏ rỗng thế nào, và quyết định đó phải nhất quán trong cả dự án.

Chiến lượcCài thế nàoPhù hợp với
Chấp nhận và bỏ quaif (p == NULL) return;Hàm không có gì để báo cáo, ví dụ hàm in
Chấp nhận và báo lỗiif (p == NULL) return -1;Hàm đã có quy ước mã lỗi sẵn
Coi là lỗi lập trìnhassert(p != NULL);Hàm nội bộ, nơi con trỏ rỗng là bằng chứng có lỗi ở nơi khác
Không kiểm tra, ghi rõ trong tài liệuKhông có gìHàm nóng, gọi hàng triệu lần, giống thư viện chuẩn
nhat-quan.c
#include <assert.h>

static int noi_bo_xu_ly(int *p, size_t n);

/* Hàm công khai: phòng thủ, chấp nhận NULL và báo lỗi. */
int api_xu_ly(int *p, size_t n)
{
    if (p == NULL) return -1;
    if (n == 0)    return 0;

    return noi_bo_xu_ly(p, n);
}

/* Hàm nội bộ: hợp đồng, dùng assert để bắt lỗi lúc phát triển. */
static int noi_bo_xu_ly(int *p, size_t n)
{
    assert(p != NULL);
    assert(n > 0);

    for (size_t i = 0; i < n; ++i)
        p[i] *= 2;

    return 0;
}

Tự làm thử

  1. Cài hoan_doi đầy đủ với kiểm tra NULL và kiểm tra hai con trỏ trùng nhau.
  2. Viết hàm chia_du cho phép truyền NULL cho tham số ra không cần, thử cả ba cách gọi.
  3. Cài hàm cấp phát nhận int * rồi int **, chứng minh chỉ cách thứ hai hoạt động.
  4. Viết hàm giai_phong hai tầng, dùng nó rồi kiểm tra con trỏ đã thành NULL.
  5. Cài mô hình hai lớp với assert ở lớp trong, chạy có và không có -DNDEBUG và so sánh hành vi.
  6. Viết hàm nhận bộ đệm và kích thước, ghi kết quả vào đó, trả về độ dài đáng lẽ cần. Thử với bộ đệm quá nhỏ.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • C không có truyền tham chiếu. Cơ chế duy nhất là truyền địa chỉ, và dấu và ở chỗ gọi là tín hiệu rõ ràng.
  • Ba mẫu tham số ra: sửa giá trị có sẵn, ghi kết quả kèm mã lỗi, và ghi vào bộ đệm người gọi cấp.
  • Cho phép truyền NULL cho tham số ra không cần thiết, dùng mẫu if (p != NULL) *p = ....
  • Muốn hàm sửa được thứ gì thì truyền địa chỉ của thứ đó. Sửa int * thì cần int **.
  • Chọn một chiến lược xử lý con trỏ rỗng và giữ nhất quán trong cả dự án. Mô hình hai lớp phòng thủ ngoài và hợp đồng trong là lựa chọn tốt.