Tham số
Sau bài này bạn sẽ làm được
- Phân biệt tham số hình thức với đối số thực tế
- Giải thích vì sao tham số mảng luôn là con trỏ
- Luôn truyền kèm độ dài khi nhận mảng
- Biết khi nào nên gom nhiều tham số vào một struct
Tham số là kênh dữ liệu duy nhất đi vào một hàm, nếu bạn tuân thủ nguyên tắc tránh biến toàn cục. Thiết kế tham số tốt thì hàm dễ gọi đúng, thiết kế tồi thì người gọi sẽ nhầm và trình biên dịch không cứu được.
#Tham số và đối số
int cong(int a, int b) /* a và b là tham số hình thức */
{
return a + b;
}
int x = 3;
int kq = cong(x, 4 + 5); /* x và 4+5 là đối số thực tế */Điểm dễ bị bỏ qua: a và b là biến cục bộ đầy đủ của hàm cong. Bạn được phép gán lại chúng, và làm vậy không ảnh hưởng gì tới bên ngoài. Bài 8.7 giải thích cặn kẽ vì sao.
int lam_tron_khong_am(int x)
{
if (x < 0)
x = 0; /* sửa tham số thoải mái, đây là bản sao riêng */
return x;
}#Trình biên dịch kiểm tra gì
Khi có nguyên mẫu, trình biên dịch kiểm tra hai thứ tại mỗi lời gọi: số lượng đối số và khả năng chuyển kiểu. Nó không kiểm tra được ý nghĩa.
int cong(int a, int b);
cong(1); /* lỗi: too few arguments to function 'cong' */
cong(1, 2, 3); /* lỗi: too many arguments to function 'cong' */
cong(1.5, 2.7); /* biên dịch được, ép thành cong(1, 2), kết quả 3 */
cong("a", "b"); /* lỗi: passing 'char *' to parameter of type 'int' */Điều trình biên dịch không bao giờ bắt được
double lai_kep(double von, double lai_suat, int nam);
/* Gọi nhầm thứ tự hai tham số double. Biên dịch sạch, kết quả sai hoàn toàn. */
double kq = lai_kep(0.07, 1000000, 10);Cả hai tham số đều là double nên không có gì để kiểm tra. Đây chính là lý do nguyên tắc bốn tới năm tham số ở Bài 8.1 tồn tại: càng nhiều tham số cùng kiểu, xác suất gọi nhầm càng cao, và không có công cụ nào cứu bạn.
#Truyền mảng luôn là con trỏ
Đây là một trong những điểm gây nhầm lẫn nhất của C. Ba cách viết sau đây giống hệt nhau với trình biên dịch:
void f(int arr[10]) { } /* con số 10 bị bỏ qua hoàn toàn */
void g(int arr[]) { } /* giống hệt f */
void h(int *arr) { } /* giống hệt, và đây là sự thật */Trong danh sách tham số, kiểu mảng luôn bị chuyển thành kiểu con trỏ tới phần tử đầu. Hệ quả trực tiếp và rất hay bị vấp:
#include <stdio.h>
void thu(int a[])
{
printf("trong ham: %zu\n", sizeof a); /* 8, kích thước một con trỏ */
}
int main(void)
{
int a[10];
printf("trong main: %zu\n", sizeof a); /* 40, mười số int */
thu(a);
return 0;
}trong main: 40 trong ham: 8
void in_mang(int a[])
{
size_t n = sizeof a / sizeof a[0]; /* luôn ra 2 */
for (size_t i = 0; i < n; ++i)
printf("%d ", a[i]);
}void in_mang(const int *a, size_t n)
{
for (size_t i = 0; i < n; ++i)
printf("%d ", a[i]);
}
int a[10] = {0};
in_mang(a, sizeof a / sizeof a[0]); /* tính ở nơi mảng còn là mảng */Cách viết diễn đạt ý định rõ hơn
/* C99 cho phép ghi độ dài trước rồi dùng nó trong khai báo mảng.
Trình biên dịch vẫn coi đây là con trỏ, nhưng người đọc hiểu ngay ý bạn,
và một số công cụ phân tích tĩnh dùng được thông tin này. */
void in_mang(size_t n, const int a[n]);
/* Từ khóa static trong ngoặc vuông là lời hứa: mảng có ít nhất n phần tử.
Trình biên dịch được phép dựa vào lời hứa đó để tối ưu. */
void xu_ly(int a[static 3]);#const trong tham số
Khi hàm nhận con trỏ nhưng không có ý định sửa dữ liệu, hãy ghi const. Đây là thói quen rẻ tiền mà lợi ích rất lớn.
size_t do_dai(const char *s); /* hứa không sửa chuỗi */
void in_mang(const int *a, size_t n); /* hứa không sửa mảng */
void sap_xep(int *a, size_t n); /* không có const vì có sửa thật */| Lợi ích của const | Giải thích |
|---|---|
| Là tài liệu chính xác | Nhìn nguyên mẫu là biết hàm có sửa dữ liệu của bạn hay không |
| Trình biên dịch kiểm tra hộ | Lỡ viết a[0] = 1 trong hàm nhận const là lỗi biên dịch ngay |
| Nhận được dữ liệu chỉ đọc | Truyền hằng chuỗi vào tham số char * vẫn hợp lệ, nhưng hàm ghi vào nó mới là hành vi không xác định. Có const thì trình biên dịch chặn ngay lúc biên dịch |
| Mở đường tối ưu | Trình biên dịch biết vùng nhớ không đổi trong suốt hàm |
void sua(char *s) { s[0] = 'A'; }
void khong_sua(const char *s) { printf("%s\n", s); }
sua("xin chao"); /* biên dịch có thể qua nhưng chạy thì sập */
khong_sua("xin chao"); /* an toàn */Hằng chuỗi nằm ở vùng .rodata, tức vùng chỉ đọc của tiến trình. Ghi vào đó làm hệ điều hành chấm dứt chương trình. Bật -Wwrite-strings để trình biên dịch cảnh báo ngay tại chỗ gọi.
#Khi nào gom vào struct
Ba dấu hiệu cho biết đã tới lúc gom tham số lại:
- Danh sách tham số vượt quá năm, đặc biệt khi nhiều tham số cùng kiểu.
- Cùng một nhóm tham số xuất hiện lặp lại ở nhiều hàm khác nhau. Nhóm đó đang đòi được đặt tên.
- Bạn thấy mình phải viết chú thích ngay tại chỗ gọi để nhớ tham số nào là tham số nào.
void tao_sv(char *ten, int tuoi, float d1, float d2,
float d3, char *lop, char *mssv);
tao_sv("Nam", 20, 8.5f, 7.0f, 9.0f, "CNTT01", "20200001");
/* Ba số float liền nhau. Đổi chỗ hai cái là sai điểm mà không ai biết. */typedef struct {
char ten[64];
char lop[16];
char mssv[16];
int tuoi;
float diem[3];
} SinhVien;
void tao_sv(SinhVien *sv);
SinhVien sv = {
.ten = "Nam",
.lop = "CNTT01",
.mssv = "20200001",
.tuoi = 20,
.diem = { 8.5f, 7.0f, 9.0f },
};
tao_sv(&sv);Tự làm thử
- Viết
void in_mang(const int *a, size_t n)và gọi nó với một mảng, một mảng con bắt đầu từ giữa, và độ dài bằng 0. - In
sizeofcủa cùng một mảng ở trongmainvà ở trong hàm nhận nó. Giải thích chênh lệch. - Viết hàm nhận
char *rồi gọi nó với một hằng chuỗi và thử ghi vào. Quan sát chương trình sập, sau đó thêmconstđể lỗi bị bắt lúc biên dịch. - Lấy một hàm bảy tham số bất kỳ và viết lại bằng struct với khởi tạo có tên. So sánh chỗ gọi của hai phiên bản.
- Bật
-Wconversionrồi gọi một hàm nhậnintbằng giá trịdouble. Đọc cảnh báo và giải thích giá trị thật hàm nhận được.
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
- Tham số hình thức là biến cục bộ của hàm, sửa nó không ảnh hưởng bên ngoài.
- Trình biên dịch kiểm tra số lượng và kiểu đối số, nhưng không bao giờ bắt được lỗi đổi chỗ hai tham số cùng kiểu.
- Trong danh sách tham số, mảng luôn biến thành con trỏ, nên
sizeofbên trong hàm chỉ cho kích thước con trỏ. Luôn truyền kèm độ dài. - Ghi
constcho mọi con trỏ mà hàm không sửa. Nó vừa là tài liệu, vừa được trình biên dịch kiểm tra. - Nhiều hơn năm tham số, hoặc một nhóm tham số lặp lại ở nhiều hàm, là dấu hiệu nên gom vào struct.