
Intro
const is a keyword shared by both C and C++. From the moment you wrote your first line of code, someone has probably been telling you: "if you want this variable to stay unchanged, use const." That is indeed part of what const means — but today, let's talk about its other semantics and the mechanics behind them.
Digging into const
The most common meaning of const is to declare that a variable will never change again. Note that this is not a declaration that the variable is a compile-time constant (that's the job of constexpr). Setting aside optimizations that the compiler and the CPU perform on their own, a const-declared variable is still evaluated at runtime. What it actually restricts is the developer: it prevents any attempt to assign to the variable after initialization.
On types
Once you extend const to pointers, an extra rule kicks in at the syntax level: the type modified by const is always the one right next to it, and const preferentially modifies the type on its left — unless there is nothing on the left, in which case it modifies the type on its right. Let's look at a few examples:
const int* ptr;
Here, const modifies int, meaning the int data the pointer points to cannot be modified. Put more loosely: the value obtained by dereferencing the address ptr cannot be changed.
int* const ptr;
In this example, const preferentially modifies the int* type on its left. In other words, it modifies the address itself, before any dereferencing: the value of the address ptr cannot be modified, but the int data obtained by dereferencing it can be. What it declares is that "where the pointer points" is itself immutable.
int const* ptr;
Now for a tricky one. This is a classic anti-pattern — nobody writes it this way nowadays — but according to the rule, const here preferentially modifies int on its left, so it's equivalent to const int*.
In compiler terminology: const applied to the variable itself is called top-level const, and const applied to the result of dereferencing the variable as an address is called low-level const. And a trivial bit of trivia: when a const-qualified type is deduced through templates or auto, the top-level qualification is usually stripped, while the low-level qualification is preserved.
Another thing C++ developers may care about: a const variable declared at global scope in a header file is guaranteed by default to be linked only once and will not trigger redefinition errors — no inline needed.
A special case: references
const int& r = x;
This is a pattern you'll see countless times in C++ programs. The primary purpose, of course, is to take a reference to x while guaranteeing it won't be modified. But it also has abilities that ordinary references lack: an r declared this way can bind to a temporary object / rvalue, and it will forcibly extend that object's lifetime to match its own. This is why function parameters are so often written as const int& — it lets the function also accept an rvalue argument.
Incidentally, using const alone without a reference is an anti-pattern in about 99% of cases: if you're not going to modify the object anyway, there's no reason to pay for an extra copy during construction. Many C++ developers reviewing beginner code zero in on exactly this — and they always find plenty.
In classes and objects
Many people know that declaring a member function with a trailing const prevents developers from modifying the object's own state inside that function (it's also frequently paired with noexcept, which lets the compiler confidently optimize away exception handling machinery). The actual mechanism is that it implicitly qualifies the this pointer: since every modification of internal state goes through this, all state changes are blocked at once. There is one exception (which logically isn't an exception at all!): mutable members. A const-qualified member function can modify mutable variables — but the very semantics of mutable are "this variable is logically unrelated to the object's state; changing it does not affect the object's state." It's commonly used for things like reference counts and caches.
Additionally, const is part of a function's declaration, so a const and a non-const version of the same function form a valid overload pair. The STL uses this trick extensively: const objects dispatch to the const overload, non-const objects to the non-const one.
const_cast
const_cast can strip away low-level const, but only on the condition that the referenced object itself has no top-level const. Dropping the jargon: if a variable declares itself as const, then no matter whether the pointer to it declares "you may not modify what I point to," you still cannot modify that variable through the pointer. It's like locking something inside two safes: even if you manage to crack the outer safe, there's still another one inside.
Misc
A C array declared with const makes every byte const and untouchable, and the array decays to const T*.
"Type nesting": if you alias int* as PTR (let me temporarily call this nesting), then in the type const PTR, the const actually chooses the PTR on its right — in other words, const modifies the PTR type itself, which is int*; in other words, const prevents the address itself from being changed; in yet other words, const PTR is equivalent to int* const!
Thread safety: the standard library, at least, assumes that member functions declared with a trailing const are thread-safe.
Finally...
Many of these mechanisms feel baffling the first time you encounter them, but eventually you'll find yourself thinking "well, what else could it be?" No matter how scattered or hard to chew C++'s knowledge points are, in the end they all come to feel natural.
Appendix
Original author: Zhouzhou