Truyền theo giá trị
Sau bài này bạn sẽ làm được
- Giải thích vì sao C luôn truyền theo giá trị, không có ngoại lệ
- Viết hàm swap đúng bằng con trỏ
- Giải thích vì sao mảng trông như được truyền theo tham chiếu
- Biết khi nào nên truyền struct bằng con trỏ thay vì bằng giá trị
C có đúng một cách truyền tham số, và bài này nói hết về nó trong một câu: hàm luôn nhận bản sao. Mọi thứ trông như ngoại lệ đều chỉ là hệ quả của việc bản sao đó đang chứa một địa chỉ.
#Quy tắc duy nhất
#include <stdio.h>
void tang(int x)
{
x = x + 1;
printf("trong ham: %d\n", x);
}
int main(void)
{
int a = 5;
tang(a);
printf("trong main: %d\n", a);
return 0;
}trong ham: 6 trong main: 5
Muốn hàm sửa được biến của người gọi, bạn phải đưa cho nó địa chỉ của biến. Toán tử & lấy địa chỉ, toán tử * đi tới nội dung ở địa chỉ đó. Chương 12 dạy con trỏ đầy đủ, bài này chỉ dùng đúng hai toán tử ấy.
void tang2(int *x)
{
*x = *x + 1; /* ghi vào ô nhớ mà x đang trỏ tới */
}
int b = 5;
tang2(&b); /* truyền địa chỉ của b */
printf("%d\n", b); /* 6 */#Hàm hoán đổi kinh điển
void swap_sai(int a, int b)
{
int t = a;
a = b;
b = t;
}
int x = 1, y = 2;
swap_sai(x, y);
printf("%d %d\n", x, y); /* 1 2, không đổi gì */void swap_dung(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
int x = 1, y = 2;
swap_dung(&x, &y);
printf("%d %d\n", x, y); /* 2 1 */Hàm bên trái vẫn chạy, vẫn hoán đổi thật, chỉ là nó hoán đổi hai bản sao rồi vứt cả hai đi khi hàm kết thúc. Đây là bài kiểm tra kinh điển để xem người học đã hiểu truyền theo giá trị hay chưa.
#Mảng: ngoại lệ chỉ có vẻ
#include <stdio.h>
void sua_mang(int a[], size_t n)
{
if (n > 0)
a[0] = 999;
}
int main(void)
{
int arr[3] = { 1, 2, 3 };
sua_mang(arr, 3);
printf("%d\n", arr[0]); /* 999, đã đổi thật */
return 0;
}Trông như C truyền mảng theo tham chiếu. Không phải. Chuyện xảy ra là phân rã mảng: khi tên mảng xuất hiện trong hầu hết các biểu thức, nó tự động biến thành con trỏ tới phần tử đầu tiên. Nên lời gọi sua_mang(arr, 3) thực chất là sua_mang(&arr[0], 3). Bạn đang truyền một con trỏ theo giá trị, đúng quy tắc duy nhất.
| Biểu thức | Kiểu thật | Ghi chú |
|---|---|---|
| arr | int * | Phân rã thành con trỏ tới arr[0] |
| &arr[0] | int * | Hoàn toàn tương đương với dòng trên |
| &arr | int (*)[3] | Con trỏ tới cả mảng, kiểu khác hẳn |
| sizeof arr | size_t | Không phân rã, cho ra 12 chứ không phải 8 |
Muốn hàm không sửa được mảng thì làm sao
void chi_doc(const int *a, size_t n)
{
for (size_t i = 0; i < n; ++i)
printf("%d ", a[i]);
/* a[0] = 1; <- lỗi biên dịch: assignment of read-only location */
}Đây là lý do thực dụng nhất để dùng const: nó khôi phục lại phần bảo vệ mà cơ chế phân rã mảng đã lấy đi.
#Truyền struct
Struct là ngoại lệ thú vị: nó không phân rã. Truyền struct theo giá trị sẽ sao chép toàn bộ nội dung, kể cả mảng nằm bên trong nó.
typedef struct {
int a[1000]; /* 4000 byte */
} To;
void f(To t) /* sao chép 4000 byte mỗi lời gọi */
{
t.a[0] = 1; /* chỉ sửa bản sao */
}
void g(const To *t) /* sao chép 8 byte, và hứa không sửa */
{
printf("%d\n", t->a[0]);
}
void h(To *t) /* sao chép 8 byte, có sửa thật */
{
t->a[0] = 1;
}| Kích thước struct | Nên truyền thế nào | Vì sao |
|---|---|---|
| Tới khoảng 16 byte | Theo giá trị | Quy ước gọi hàm đặt gọn vào thanh ghi, không tốn gì thêm |
| Lớn hơn 16 byte, hàm không sửa | const con trỏ | Chỉ sao chép 8 byte, và trình biên dịch kiểm tra hộ |
| Lớn hơn 16 byte, hàm có sửa | Con trỏ không const | Sửa trực tiếp lên bản gốc |
| Cần bản sao độc lập để nghịch | Theo giá trị, dù lớn | Đôi khi bản sao chính là thứ bạn muốn |
#Bảng tra: truyền cái gì thì thế nào
| Truyền vào | Hàm nhận được | Hàm sửa được bản gốc |
|---|---|---|
| int a | bản sao của giá trị | Không |
| int *p, gọi bằng &a | bản sao của địa chỉ | Có, qua *p |
| mảng int arr[10] | bản sao con trỏ tới arr[0] | Có, qua arr[i] |
| struct S s | bản sao toàn bộ struct | Không |
| struct S *ps | bản sao của địa chỉ | Có, qua ps->truong |
| const int *p | bản sao của địa chỉ | Không, trình biên dịch chặn |
| char *s cho chuỗi | bản sao con trỏ tới ký tự đầu | Có, nếu chuỗi ghi được |
Tự làm thử
- Viết cả
swap_saivàswap_dung, in giá trị trước và sau mỗi lần gọi, giải thích từng dòng kết quả. - Viết hàm nhận con trỏ rồi gán
NULLcho chính con trỏ đó bên trong hàm. In địa chỉ ở cả hai nơi để chứng minh con trỏ cũng là bản sao. - Viết
void tang_het(int *a, size_t n)cộng 1 vào mọi phần tử, vàlong long tong(const int *a, size_t n). Giải thích vì sao một hàm cóconstcòn hàm kia thì không. - Tạo struct chứa mảng bốn nghìn byte, viết hai hàm nhận nó theo giá trị và theo con trỏ, gọi mỗi hàm mười triệu lần và đo bằng
clock()như Bài 7.5. - Viết hàm nhận struct có con trỏ bên trong, sửa vùng nhớ qua bản sao, rồi kiểm tra bản gốc có bị đổi không. Giải thích kết quả.
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 chỉ có truyền theo giá trị. Hàm luôn nhận bản sao của đối số.
- Muốn hàm sửa được biến của người gọi thì truyền địa chỉ. Bản thân con trỏ vẫn là bản sao, chỉ ô nhớ nó trỏ tới mới được chia sẻ.
- Mảng không phải ngoại lệ. Tên mảng phân rã thành con trỏ tới phần tử đầu trước khi được truyền.
- Struct không phân rã. Struct 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. - Sao chép struct là sao chép nông: con trỏ bên trong được chép nguyên giá trị, hai bản cùng trỏ một chỗ.