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ông | Không | Có, nên phải kiểm tra |
| Trỏ đi chỗ khác được không | Không, gắn vĩnh viễn | Có |
| Cần dấu sao khi dùng | Không | Có |
#Ba mẫu tham số ra
Mẫu 1: sửa một giá trị có sẵn
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
/* 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
/* 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ị
/* 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
void phan_tich(int n, int *chan, int *le, int *tong);
int c, l, t;
phan_tich(100, &c, &l, &t);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ào | Khi nào |
|---|---|
| Tham số ra | Kế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ề struct | Kế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.
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 */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
/* 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ược | Cài thế nào | Phù hợp với |
|---|---|---|
| Chấp nhận và bỏ qua | if (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ỗi | if (p == NULL) return -1; | Hàm đã có quy ước mã lỗi sẵn |
| Coi là lỗi lập trình | assert(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ệu | Không có gì | Hàm nóng, gọi hàng triệu lần, giống thư viện chuẩn |
#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ử
- Cài
hoan_doiđầy đủ với kiểm traNULLvà kiểm tra hai con trỏ trùng nhau. - Viết hàm
chia_ducho phép truyềnNULLcho tham số ra không cần, thử cả ba cách gọi. - Cài hàm cấp phát nhận
int *rồiint **, chứng minh chỉ cách thứ hai hoạt động. - Viết hàm
giai_phonghai tầng, dùng nó rồi kiểm tra con trỏ đã thànhNULL. - Cài mô hình hai lớp với
assertở lớp trong, chạy có và không có-DNDEBUGvà so sánh hành vi. - 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
NULLcho tham số ra không cần thiết, dùng mẫuif (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ầnint **. - 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.