Vì sao cần hằng số
Hằng số (constant) là giá trị bạn hứa sẽ không bao giờ thay đổi sau khi đặt. Đánh dấu một thứ là const làm được hai việc cùng lúc: nói rõ ý định của bạn với bất kỳ ai đọc code, và để trình biên dịch bắt buộc tuân theo ý định đó. Mọi dòng cố sửa giá trị sẽ thành lỗi biên dịch thay vì một bug âm thầm lúc chạy.
Nếu từ khóa auto để trình biên dịch tự suy ra kiểu của biến, thì const giới hạn những gì bạn được làm với biến đó. Hai từ khóa kết hợp thoải mái: const auto limit = 100; là một int chỉ đọc.
Khai báo giá trị const
Đặt const trước kiểu dữ liệu. Biến const phải được khởi tạo ngay trên cùng dòng, vì sau đó không còn lúc nào bạn được phép gán cho nó nữa.
Bỏ chú thích ở dòng gán và chương trình sẽ không biên dịch được: trình biên dịch báo "assignment of read-only variable". Đó chính là mục đích: lỗi bị phát hiện trước khi chương trình kịp chạy.
Một thói quen của người mới mang từ C sang là #define MAX_USERS 100. Hãy tránh nó. Macro chỉ là phép thay thế văn bản mù quáng, không có kiểu và không tôn trọng phạm vi, nên không xem được trong debugger và sinh ra thông báo lỗi khó hiểu. Biến const (hoặc constexpr) được kiểm tra kiểu và có phạm vi như mọi biến khác.
const và constexpr
Cả hai từ khóa đều cho bạn một giá trị không thể đổi, nhưng chúng trả lời hai câu hỏi khác nhau. const nói "giá trị này không bao giờ đổi sau khi đã đặt". constexpr nói điều mạnh hơn: "giá trị này tính được lúc biên dịch", và mọi thứ constexpr đều tự động là const.
Quy tắc chung: dùng constexpr mỗi khi giá trị là một literal cố định hoặc một phép tính trình biên dịch làm được (kích thước mảng, độ dài buffer, nhãn switch, đối số template). Dùng const thường khi giá trị được quyết định lúc chạy nhưng không được đổi về sau, ví dụ bản sao const của một đối số hàm.
Từ C++20 còn có consteval, dùng cho những hàm bắt buộc chạy lúc biên dịch:
consteval int square(int x) { return x * x; }
constexpr int area = square(8); // computed during compilation
Hàm constexpr có thể chạy lúc biên dịch; hàm consteval thì luôn phải như vậy, nếu không sẽ là lỗi.
Con trỏ và const: đọc từ phải sang trái
Đây là chỗ const hay làm người học vấp, vì từ khóa có thể đứng ở cả hai phía của * và hai nghĩa hoàn toàn ngược nhau. Mẹo là đọc khai báo từ phải sang trái.
Đọc int* const p2 từ phải sang trái: "p2 là con trỏ const tới int". Đọc const int* p1 là "p1 là con trỏ tới const int". Đọc sai chỗ này, bạn sẽ mất khá nhiều thời gian bối rối trước một lỗi nói rằng bạn không được sửa thứ mà bạn tưởng là sửa được.
Một cái bẫy thực tế: đừng bao giờ lấy địa chỉ của một const rồi ép kiểu bỏ const đi để sửa đối tượng gốc. Làm vậy là undefined behavior nếu đối tượng gốc thật sự là const, và trình biên dịch được phép coi giá trị đó không bao giờ đổi, nên lệnh "ghi" của bạn có thể bị bỏ qua hoàn toàn.
Tham chiếu const làm tham số hàm
Cách dùng const phổ biến nhất hằng ngày là truyền đối tượng lớn bằng tham chiếu mà không sao chép. Tham số const& tránh được bản sao và hứa rằng hàm sẽ không sửa đối số của nơi gọi.
Truyền bằng const& là lựa chọn mặc định cho mọi tham số lớn hơn vài byte (chuỗi, vector, lớp do bạn tự viết). Lưu ý nó còn cho phép hàm nhận một giá trị tạm như "Grace". Tham chiếu không const thông thường không gắn được vào giá trị tạm, nên nếu bỏ const ở đây thì lời gọi thứ hai sẽ bị từ chối.
Hàm thành viên const
Khi viết một lớp, hãy đánh dấu mọi phương thức không sửa đối tượng bằng const ở cuối. Chính điều này giúp phương thức gọi được trên instance const và trên tham số const&. Thiếu nó, bạn không thể đọc chính đối tượng của mình qua một tham chiếu const.
Thói quen đánh dấu const cho phương thức chỉ đọc gọi là const correctness. Hãy làm đúng ngay từ đầu: thêm const cho một phương thức lẽ ra phải là const thì dễ, nhưng bổ sung const khắp một codebase lớn về sau thì rất vất vả, vì mọi nơi gọi qua tham chiếu const đều phụ thuộc vào nó.
Tiếp theo: toán tử
Giờ giá trị của bạn đã khóa được bằng const, bước tiếp theo là làm việc với chúng. Trang toán tử trình bày các toán tử số học, so sánh, logic và gán, kể cả những cái bẫy quanh phép chia số nguyên, độ ưu tiên toán tử, và cách const tương tác với các toán tử gán mà bạn không được dùng.
Câu hỏi thường gặp
const và constexpr trong C++ khác nhau thế nào?
const nghĩa là giá trị không thể thay đổi sau khi khởi tạo, nhưng giá trị đó có thể được tính lúc chương trình chạy. constexpr mạnh hơn: nó bảo đảm giá trị tính được ngay lúc biên dịch, nên dùng được ở những chỗ cần hằng số biên dịch (kích thước mảng, đối số template, nhãn switch). Mọi đối tượng constexpr cũng là const, nhưng không phải đối tượng const nào cũng là constexpr.
Khai báo hằng số trong C++ như thế nào?
Đặt const trước kiểu dữ liệu và gán giá trị: const int maxUsers = 100;. Biến const phải được khởi tạo ngay khi khai báo, vì sau đó bạn không bao giờ gán lại được. Với hằng số biên dịch, nên dùng constexpr int maxUsers = 100;. Tránh macro #define kiểu C cũ, vì macro không có kiểu và bỏ qua phạm vi.
Con trỏ const trong C++ nghĩa là gì?
Tùy vào vị trí của const. const int* p là con trỏ tới const: bạn có thể cho p trỏ sang chỗ khác, nhưng không sửa được *p. int* const p là con trỏ const: bạn sửa được *p, nhưng không cho p trỏ sang chỗ khác được. Hãy đọc khai báo từ phải sang trái: int* const là "con trỏ const tới int".