Con trỏ và hàm
Sau bài này bạn sẽ làm được
- Viết hàm nhận con trỏ để sửa dữ liệu của người gọi
- Biết khi nào trả về con trỏ là an toàn
- Nhận ra con trỏ treo do trả về địa chỉ biến cục bộ
- Đặt quy ước rõ ràng về ai sở hữu vùng nhớ
Con trỏ mở ra hai khả năng mà hàm C bình thường không có: sửa được dữ liệu của người gọi, và làm việc với dữ liệu lớn mà không phải sao chép. Đổi lại, nó mang tới một câu hỏi mới mà bạn phải trả lời cho mọi hàm: khi hàm kết thúc, vùng nhớ đó còn sống không.
#Truyền con trỏ vào hàm
#include <stdio.h>
void tang(int *p)
{
if (p == NULL) return;
*p += 1;
}
int main(void)
{
int n = 41;
tang(&n);
printf("%d\n", n); /* 42 */
return 0;
}Bài 8.7 đã nói kỹ: C luôn truyền theo giá trị, kể cả ở đây. Bản thân con trỏ p là bản sao của &n. Thứ được chia sẻ là ô nhớ mà nó trỏ tới, không phải bản thân con trỏ.
void thu(int *p)
{
p = NULL; /* chỉ đổi bản sao trong hàm, q của người gọi không đổi */
/* Nếu bỏ dòng trên và viết *p = 1; thì mới sửa được dữ liệu của người gọi.
Nhưng viết *p = 1; NGAY SAU dòng trên là giải tham chiếu con trỏ rỗng. */
}
int n = 5;
int *q = &n;
thu(q);
printf("%p\n", (void *)q); /* vẫn là địa chỉ của n */#Tránh sao chép dữ liệu lớn
typedef struct {
double m[100][100]; /* 80 000 byte */
} MaTran;
double tong_1(MaTran m) /* sao chép 80 KB mỗi lời gọi */
{
double s = 0.0;
for (int i = 0; i < 100; ++i)
for (int j = 0; j < 100; ++j)
s += m.m[i][j];
return s;
}
double tong_2(const MaTran *m) /* sao chép 8 byte */
{
double s = 0.0;
for (int i = 0; i < 100; ++i)
for (int j = 0; j < 100; ++j)
s += m->m[i][j];
return s;
}truyen theo gia tri : 2.418 s truyen con tro : 1.032 s ti le : 2.3 lan
| Kích thước dữ liệu | Nên truyền thế nào |
|---|---|
| Tới khoảng 16 byte | Theo giá trị, quy ước gọi hàm đặt gọn vào thanh ghi |
| Lớn hơn, hàm không sửa | const con trỏ |
| Lớn hơn, hàm có sửa | Con trỏ không const |
| Cần bản sao độc lập | Theo giá trị, dù lớn |
#Trả về con trỏ
Trả về con trỏ hữu ích nhưng đặt ra câu hỏi vòng đời. Có bốn nguồn gốc cho con trỏ trả về, và chỉ một trong bốn là sai.
| Con trỏ trỏ tới | An toàn | Ví dụ |
|---|---|---|
| Biến cục bộ của hàm | KHÔNG | return &x; với int x cục bộ |
| Vùng vừa cấp phát động | Có, nhưng người gọi phải giải phóng | return malloc(n); |
| Dữ liệu mà người gọi truyền vào | Có | return strchr(s, c); |
| Biến static hoặc toàn cục | Có, nhưng chỉ có một bản dùng chung | return bo_dem_static; |
int *tao_so(void)
{
int x = 42;
return &x; /* x chết ngay khi hàm kết thúc */
}
int *p = tao_so();
printf("%d\n", *p); /* có thể in 42, có thể in rác, tùy lần chạy *//* Cách 1: người gọi cấp chỗ, hàm chỉ ghi vào */
void tao_so(int *ra)
{
*ra = 42;
}
int n;
tao_so(&n);
/* Cách 2: hàm cấp phát, người gọi giải phóng */
int *tao_so(void)
{
int *p = malloc(sizeof *p);
if (p != NULL) *p = 42;
return p;
}
int *p = tao_so();
if (p != NULL) {
printf("%d\n", *p);
free(p);
}
/* Cách 3: trả về con trỏ vào dữ liệu người gọi đã có */
char *tim(char *s, char c)
{
for (; *s != '\0'; ++s)
if (*s == c) return s;
return NULL;
}sai.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
Trường hợp mảng static là ngoại lệ
/* Mảng static sống suốt chương trình nên trả về địa chỉ của nó là hợp lệ. */
const char *thu_trong_tuan(int n)
{
static const char *ten[] = {
"Chu nhat", "Thu hai", "Thu ba", "Thu tu",
"Thu nam", "Thu sau", "Thu bay",
};
return (n >= 0 && n < 7) ? ten[n] : NULL;
}Nhưng nhớ cái bẫy ở Bài 8.9: nếu hàm ghi vào bộ đệm static rồi trả về nó, hai lời gọi liên tiếp sẽ giẫm lên nhau. Chỉ dùng cách này khi dữ liệu là hằng.
#Quy tắc vàng về vòng đời
| Đối tượng | Sống từ khi nào tới khi nào |
|---|---|
| Biến cục bộ | Từ khi vào khối tới khi ra khỏi khối |
| Tham số hàm | Từ khi vào hàm tới khi hàm kết thúc |
| Biến static hoặc toàn cục | Suốt chương trình |
| Hằng chuỗi | Suốt chương trình |
| Vùng cấp phát động | Từ malloc tới free, do bạn quyết định |
int *p;
{
int x = 42;
p = &x;
} /* x chết ở đây, p thành con trỏ treo */
printf("%d\n", *p); /* hành vi không xác định */
/* Cùng vấn đề với vòng lặp */
int *q;
for (int i = 0; i < 3; ++i) {
int tam = i;
q = &tam; /* tam mới mỗi lượt, và chết mỗi lượt */
}
printf("%d\n", *q); /* hành vi không xác định */#Ai sở hữu vùng nhớ
| Kiểu hàm | Ai giải phóng | Ví dụ trong thư viện chuẩn |
|---|---|---|
| Hàm trả về vùng vừa cấp | Người gọi | malloc, strdup, fopen |
| Hàm trả về con trỏ vào dữ liệu có sẵn | Không ai, vì không cấp gì | strchr, strstr, strtok |
| Hàm ghi vào bộ đệm người gọi cấp | Người gọi, nhưng bộ đệm của họ | snprintf, fgets, memcpy |
| Hàm trả về con trỏ tới dữ liệu tĩnh | Không ai, và không được giải phóng | localtime, getenv |
Ghi quy ước vào tên và vào tài liệu
/* Trả về vùng nhớ mới. Người gọi PHẢI gọi free.
Trả về NULL nếu không đủ bộ nhớ. */
char *tao_ban_sao(const char *s);
/* Ghi vào bộ đệm do người gọi cấp. Không cấp phát gì.
Trả về độ dài đáng lẽ cần, giống snprintf. */
size_t dinh_dang(char *ra, size_t co, int n);
/* Trả về con trỏ vào chính chuỗi đầu vào. Không được giải phóng.
Chỉ hợp lệ khi chuỗi đầu vào còn sống. */
const char *tim_tu(const char *s, const char *tu);Tự làm thử
- Viết hàm nhận con trỏ và sửa giá trị của người gọi, thêm kiểm tra
NULLở đầu hàm. - Viết hàm gán
NULLcho tham số con trỏ bên trong, chứng minh con trỏ của người gọi không đổi. - Đo thời gian truyền một struct 80 kilobyte theo giá trị và theo con trỏ, mỗi cách một trăm nghìn lần.
- Cài chương trình trả về địa chỉ biến cục bộ, in giá trị trước và sau khi gọi một hàm khác, ghi lại kết quả.
- Viết ba phiên bản hàm tạo chuỗi theo ba quy ước sở hữu trong bảng, so sánh mã ở chỗ gọi.
- Gọi
freelên con trỏ trả về từstrchrvà chạy dưới valgrind, đọc thông báo.
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
- Truyền con trỏ cho phép hàm sửa dữ liệu của người gọi, nhưng bản thân con trỏ vẫn là bản sao.
- Dữ liệu lớn hơn khoảng mười sáu byte nên truyền bằng con trỏ, kèm
constnếu hàm không sửa. - Không bao giờ trả về địa chỉ biến cục bộ. Lỗi này thường cho kết quả đúng ở lần thử đầu tiên.
- Con trỏ chỉ hợp lệ trong khoảng thời gian đối tượng nó trỏ tới còn sống.
- Mỗi hàm trả về con trỏ phải ghi rõ ai sở hữu vùng nhớ đó và ai có trách nhiệm giải phóng.