Bài 2.515 phút đọc
Hằng số
Sau bài này bạn sẽ làm được
- So sánh define, const và enum trên sáu tiêu chí
- Đọc được const với con trỏ theo chiều phải sang trái
- Tránh ba cạm bẫy kinh điển của macro
- Chọn đúng cách tạo hằng cho từng tình huống
C có ba cách tạo hằng số, và chúng khác nhau về bản chất chứ không chỉ về cú pháp. Một cái do preprocessor xử lý, hai cái còn lại do trình biên dịch. Chọn sai cách sẽ dẫn tới lỗi khó hiểu.
#Ba cách tạo hằng
#define MAX_A 100 /* macro, do preprocessor thay thế */
const int MAX_B = 100; /* biến chỉ đọc, do trình biên dịch quản lý */
enum { MAX_C = 100 }; /* hằng số nguyên thật sự */| Tiêu chí | #define | const | enum |
|---|---|---|---|
| Ai xử lý | Preprocessor | Trình biên dịch | Trình biên dịch |
| Có kiểu dữ liệu | Không | Có | Có, là int |
| Trình gỡ lỗi thấy tên | Không | Có | Có |
| Chiếm bộ nhớ | Không | Có thể | Không |
| Dùng làm kích thước mảng | Được | Không | Được |
| Dùng trong nhãn case | Được | Không | Được |
| Phạm vi | Tới hết file | Theo khối | Theo khối |
| Dùng cho số thực | Được | Được | Không |
#define MAX_A 100
const int MAX_B = 100;
enum { MAX_C = 100 };
int a[MAX_A]; /* hợp lệ */
int b[MAX_B]; /* trong C89 là lỗi.
Trong C99 tạo mảng độ dài thay đổi, KHÔNG phải mảng cố định */
int c[MAX_C]; /* hợp lệ */
switch (x) {
case MAX_A: break; /* hợp lệ */
case MAX_B: break; /* LỖI: không phải hằng số nguyên */
case MAX_C: break; /* hợp lệ */
}#define và ba cạm bẫy
Preprocessor chỉ thay chữ, không hiểu C. Ba cạm bẫy dưới đây đều bắt nguồn từ sự thật đó.
Cạm bẫy 1: thiếu ngoặc
Không nên
#define BINH_PHUONG(x) x * x
BINH_PHUONG(2 + 3)
/* Preprocessor thay chữ, thành:
2 + 3 * 2 + 3 = 11
Không phải 25. */Nên
#define BINH_PHUONG(x) ((x) * (x))
BINH_PHUONG(2 + 3)
/* Thành:
((2 + 3) * (2 + 3)) = 25 */Quy tắc: bọc ngoặc cho từng tham số và bọc ngoặc cho cả biểu thức. Thiếu lớp ngoài thì 2 * BINH_PHUONG(3) vẫn sai.
Cạm bẫy 2: đánh giá nhiều lần
#define BINH_PHUONG(x) ((x) * (x))
int i = 3;
int kq = BINH_PHUONG(i++);
/* Thành: ((i++) * (i++))
i tăng HAI lần, và đây còn là hành vi không xác định. */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int m = MAX(tinh_toan_nang(), 10);
/* tinh_toan_nang() bị gọi HAI lần khi nó lớn hơn 10. */Cạm bẫy 3: nhiều câu lệnh không bọc
Không nên
#define HOAN_DOI(a, b) t = a; a = b; b = t;
if (x > y)
HOAN_DOI(x, y);
/* Bung ra thành:
if (x > y)
t = x;
x = y; <- LUÔN chạy
y = t; <- LUÔN chạy */Nên
#define HOAN_DOI(a, b) \
do { \
int t = (a); \
(a) = (b); \
(b) = t; \
} while (0)
if (x > y)
HOAN_DOI(x, y); /* an toàn, và vẫn nuốt được dấu chấm phẩy */#const và cách đọc
Quy tắc đọc const
Đọc từ phải sang trái, bắt đầu từ tên biến. Cái gì đứng ngay bên trái
const thì cái đó là hằng.const int x = 5; /* x là int hằng */
int const x = 5; /* giống hệt dòng trên */
const char *p; /* p là con trỏ tới char HẰNG */
char const *p; /* giống hệt dòng trên */
char *const p = buf; /* p là con trỏ HẰNG tới char */
const char *const p = buf;/* cả con trỏ lẫn nội dung đều hằng */| Khai báo | Sửa được *p? | Sửa được p? |
|---|---|---|
| const char *p | Không | Được |
| char *const p | Được | Không |
| const char *const p | Không | Không |
int x = 10, y = 20;
const int *p1 = &x;
*p1 = 5; /* LỖI: không sửa được giá trị */
p1 = &y; /* được: đổi con trỏ sang chỗ khác */
int *const p2 = &x;
*p2 = 5; /* được: sửa được giá trị */
p2 = &y; /* LỖI: không đổi được con trỏ */const trong tham số hàm là một hợp đồng
Đây là công dụng quan trọng nhất của const. Nó nói với người gọi rằng hàm sẽ không sửa dữ liệu của họ, và trình biên dịch sẽ ép buộc lời hứa đó.
size_t do_dai(const char *s); /* tôi chỉ đọc chuỗi của bạn */
void in_mang(const int *a, size_t n); /* tôi chỉ đọc mảng */
void sap_xep(int *a, size_t n); /* không có const: tôi SẼ sửa */Lợi ích cụ thể:
- Người đọc biết ngay hàm có sửa dữ liệu hay không, không cần đọc thân hàm
- Trình biên dịch báo lỗi nếu bạn lỡ tay sửa
- Truyền được dữ liệu
constvào. Không cóconsttrong tham số thì không truyền được - Trình biên dịch tối ưu tốt hơn vì biết dữ liệu không đổi
#enum
enum tạo ra một nhóm hằng số nguyên có tên. Nó phù hợp nhất khi các hằng có quan hệ với nhau.
enum TrangThai { CHO, DANG_CHAY, TAM_DUNG, KET_THUC };
/* Tự đánh số từ 0: CHO=0, DANG_CHAY=1, TAM_DUNG=2, KET_THUC=3 */
enum Mau { DO = 1, XANH = 5, LUC };
/* Đặt giá trị thủ công. LUC tiếp tục từ 5 nên bằng 6. */
enum HttpCode { OK = 200, NOT_FOUND = 404, SERVER_ERROR = 500 };
typedef enum { CHO, DANG_CHAY, TAM_DUNG, KET_THUC } TrangThai;
TrangThai tt = DANG_CHAY; /* gọn hơn, không phải viết enum mỗi lần */#Chọn cách nào
| Tình huống | Nên dùng | Vì sao |
|---|---|---|
| Kích thước mảng cố định | #define | const không dùng làm kích thước mảng trong C được |
| Nhóm hằng liên quan | enum | Có tên, tự đánh số, dùng được trong case |
| Hằng số thực | #define | enum chỉ chứa được số nguyên |
| Tham số hàm chỉ đọc | const | Đây là hợp đồng với người gọi |
| Biến cục bộ không đổi | const | Có kiểu, có phạm vi, trình gỡ lỗi thấy được |
| Bật tắt tính năng lúc build | #define | Chỉ preprocessor mới làm được việc này |
Tự làm thử
- Viết chương trình tính chu vi và diện tích hình tròn, một bản dùng
#define PI, một bản dùngconst double PI. So sánh assembly sinh ra bằnggcc -S -O2. - Tìm ba lỗi trong
#define ABS(x) x < 0 ? -x : x, thử vớiABS(3 - 5)và2 * ABS(-3), rồi sửa lại. - Định nghĩa
enumcho bảy ngày trong tuần, viết hàm nhận enum và trả về tên tiếng Việt. Dùng mẹo đếm phần tử. - Viết ba khai báo con trỏ với
constở ba vị trí khác nhau, rồi thử sửa cả con trỏ lẫn giá trị để xem cái nào báo lỗi. - Viết macro
MAXrồi chứng minh nó gọi hàm hai lần, bằng cách truyền vào một hàm có in ra màn hì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
- define do preprocessor thay chữ, const là biến chỉ đọc, enum là hằng số nguyên thật.
- Trong C,
constkhông dùng làm kích thước mảng hay nhãncaseđược. C++ thì được. - Macro có ba cạm bẫy: thiếu ngoặc, đánh giá nhiều lần, và nhiều câu lệnh không bọc.
- Macro nhiều câu lệnh phải bọc trong
do { } while (0). - Đọc const từ phải sang trái, bắt đầu từ tên biến.
- const trong tham số hàm là hợp đồng cho người gọi biết hàm không sửa dữ liệu.