Bỏ qua điều hướng, tới nội dung chính
Học C
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í#defineconstenum
Ai xử lýPreprocessorTrình biên dịchTrình biên dịch
Có kiểu dữ liệuKhôngCó, là int
Trình gỡ lỗi thấy tênKhông
Chiếm bộ nhớKhôngCó thểKhông
Dùng làm kích thước mảngĐượcKhôngĐược
Dùng trong nhãn caseĐượcKhôngĐược
Phạm viTới hết fileTheo khốiTheo khối
Dùng cho số thựcĐượcĐượcKhô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áoSửa được *p?Sửa được p?
const char *pKhôngĐược
char *const pĐượcKhông
const char *const pKhôngKhô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 const vào. Không có const trong 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ốngNên dùngVì sao
Kích thước mảng cố định#defineconst không dùng làm kích thước mảng trong C được
Nhóm hằng liên quanenumCó tên, tự đánh số, dùng được trong case
Hằng số thực#defineenum chỉ chứa được số nguyên
Tham số hàm chỉ đọcconstĐây là hợp đồng với người gọi
Biến cục bộ không đổiconstCó 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#defineChỉ preprocessor mới làm được việc này

Tự làm thử

  1. 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ùng const double PI. So sánh assembly sinh ra bằng gcc -S -O2.
  2. Tìm ba lỗi trong #define ABS(x) x < 0 ? -x : x, thử với ABS(3 - 5)2 * ABS(-3), rồi sửa lại.
  3. Định nghĩa enum cho 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ử.
  4. 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.
  5. Viết macro MAX rồ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, const không dùng làm kích thước mảng hay nhãn case đượ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.