Exercises — const correctness — const variables, const pointers, const member functions
The one rule that solves almost everything on this page:

Level 1 — Recognition
Exercise 1.1
For each declaration, say what can change: the pointed-to value (*p), the pointer (p), both, or neither.
const int* p1;
int* const p2 = &a;
const int* const p3 = &a;
int* p4;Recall Solution
Read each right-to-left.
p1— "p1 is a pointer to a const int." Pointee locked, pointer free. Can changep1, not*p1.p2— "p2 is a const pointer to an int." Pointer locked, pointee free. Can change*p2, notp2.p3— "p3 is a const pointer to a const int." Neither.p4— plain pointer to int. Both.
Count of "can change *p" cases: p2, p4 → 2.
Count of "can change p" cases: p1, p4 → 2.
Exercise 1.2
Which of these lines compile? (Assume int a = 1, b = 2; above.)
const int x = 5; // A
const int y; // B
int z; // C
z = 4; // D
x = 9; // ERecall Solution
- A compiles — const, initialized. ✅
- B fails — a const must be initialized at declaration. ❌
- C compiles — plain int, no init required. ✅
- D compiles —
zis not const. ✅ - E fails — can't assign to a const. ❌
Compiling lines: A, C, D → 3 of 5.
Level 2 — Application
Exercise 2.1
Given int a = 10, b = 20;, mark each line OK or ERROR:
const int* p = &a;
*p = 99; // (1)
p = &b; // (2)
int* const q = &a;
*q = 99; // (3)
q = &b; // (4)Recall Solution
p is pointer to const int: pointee locked, pointer free.
- (1)
*p = 99;→ ERROR (writing through pointer-to-const). - (2)
p = &b;→ OK (repointing is allowed).
q is const pointer to int: pointer locked, pointee free.
- (3)
*q = 99;→ OK. - (4)
q = &b;→ ERROR (repointing a const pointer).
Errors: (1) and (4). OK: (2) and (3).
Exercise 2.2
Write the parameter type for a function print that reads a C-string but promises never to modify it, and can accept a caller's const char[]. Then say why the non-const version void print(char* s) fails a const caller.
Recall Solution
void print(const char* s); // pointer to const charThe pointee is const, so inside print you may read s[i] but not write it. Because the pointee type is const char, a caller may pass a const char[] — the const-ness matches.
void print(char* s) demands a pointer to mutable char. Passing const char[] would drop the caller's const promise, which the compiler forbids. So the const caller can't use it. Answer: const char*.
Level 3 — Analysis
Exercise 3.1
Consider:
class Circle {
double r;
public:
double area() const { return 3.14159 * r * r; }
void setR(double x) { r = x; }
};
const Circle c{}; // r defaults to 0.0
c.area(); // (1)
c.setR(5); // (2)Which calls compile? What type does this have inside area()? Compute the value returned by c.area().
Recall Solution
cis a const object, so itsthisisconst Circle*.- (1)
area()is a const member function → itsthisisconst Circle*, which matches. OK. - (2)
setRis non-const → itsthisisCircle*, cannot bind to a const object. ERROR. - Inside
area(),thishas typeconst Circle*(data members read-only). rwas value-initialized to0.0, soarea()returns .
Compiles: (1) only. Returned value: 0.0.
Exercise 3.2
Explain why this class needs two at overloads, and predict which one each call below selects.
class Buffer {
char data[100];
public:
char& at(int i) { return data[i]; }
const char& at(int i) const { return data[i]; }
};
Buffer mb;
const Buffer cb{};
mb.at(0) = 'X'; // (1)
char x = cb.at(0); // (2)
cb.at(0) = 'Y'; // (3)Recall Solution
Overload resolution uses the object's const-ness to pick the this type.
- (1)
mbis non-const → selectschar& at(int)→ returns a writable ref → assignment OK. - (2)
cbis const → selectsconst char& at(int) const→ returns read-only ref → reading is OK. - (3)
cbis const → selects the const overload returningconst char&; you can't assign to aconst char&. ERROR.
Two overloads exist so a const object gets a read-only view while a non-const object gets a mutable one — exactly how std::vector::operator[] works (see const operator[]).
OK: (1),(2). ERROR: (3).
Level 4 — Synthesis
Exercise 4.1
Make this class const-correct. magnitude() should be callable on a const Vec2, and it should cache its result so a second call doesn't recompute — without breaking const. List every change.
class Vec2 {
double x, y;
double cached; // last computed magnitude
bool valid; // is cache fresh?
public:
Vec2(double x_, double y_) : x(x_), y(y_), valid(false) {}
double magnitude() {
if (!valid) { cached = sqrt(x*x + y*y); valid = true; }
return cached;
}
};Recall Solution
magnitude() only logically reads the vector — the cache is an implementation detail, not part of the observable state. So mark it const, and make the two cache fields mutable so they may still be written inside a const method.
class Vec2 {
double x, y;
mutable double cached; // (change 1) mutable
mutable bool valid; // (change 2) mutable
public:
Vec2(double x_, double y_) : x(x_), y(y_), valid(false) {}
double magnitude() const { // (change 3) const method
if (!valid) { cached = sqrt(x*x + y*y); valid = true; }
return cached;
}
};Three changes: mark cached and valid mutable, add const after magnitude()'s parameter list.
Sanity check the number: Vec2(3,4).magnitude() returns , and calling it twice returns 5.0 both times (second call uses the cache).
Exercise 4.2
For Vec2 v(3,4); and const Vec2 cv(5,12);, state which calls compile and their numeric results (using the const-correct class above).
v.magnitude(); // (1)
cv.magnitude(); // (2)Recall Solution
- (1)
vnon-const →magnitude() conststill callable (a const method works on non-const objects too). Returns . OK. - (2)
cvconst →magnitude()is const, matches. Returns . OK.
Both compile. Results: 5.0 and 13.0. Note: a const method is the superset — callable on both const and non-const objects. That is why you mark read-only methods const up front.
Level 5 — Mastery
Exercise 5.1
Classify each snippet as well-defined, compile error, or undefined behavior (UB).
// A
const int k = 42;
int* p = const_cast<int*>(&k);
*p = 7; // ?
// B
int m = 42;
const int* cp = &m;
int* p = const_cast<int*>(cp);
*p = 7; // ?
// C
const int k = 42;
int& r = const_cast<int&>(k); // ?Recall Solution
- A —
kwas originally declaredconst. Casting away const and writing through the result is undefined behavior. It may "work", crash, or corrupt neighbouring data — never rely on it. - B — the underlying object
mis actually non-const;cpmerely pointed to it via a const view. Casting the const away and writing is well-defined:mbecomes7. - C — this line only forms the reference; the cast itself compiles. But it compiles (not an error). Writing
r = ...afterward would be UB becausekis truly const — as written (no write yet), the line is a legal but dangerous binding.
A = UB, B = well-defined (m becomes 7), C = compiles (writing through r would be UB).
Exercise 5.2
A struct holds a raw pointer. Show what a const object freezes and what it does not.
struct Holder {
int* ptr;
int value;
};
int backing = 10;
const Holder h{ &backing, 99 };
h.value = 1; // (1)
h.ptr = nullptr;// (2)
*h.ptr = 55; // (3)Recall Solution
On a const Holder, every member becomes const bitwise:
value→const int→ (1) ERROR (can't write a const member).ptr→int* const(const pointer, mutable pointee) → (2) ERROR (can't repoint a const pointer).*h.ptrwrites through tobacking, which is a separate, non-const object → (3) OK —backingbecomes55.
This is shallow constness in one picture: the pointer is frozen, the pointed-to data is not. (1) ERROR, (2) ERROR, (3) OK → backing == 55.
Score yourself
Recall Answer key (one line each)
Ex 1.1 :::: p1=change pointer, p2=change value, p3=neither, p4=both Ex 1.2 :::: A,C,D compile (3 of 5) Ex 2.1 :::: ERROR,OK,OK,ERROR Ex 2.2 :::: const char* Ex 3.1 :::: only area() compiles; this is const Circle*; returns 0.0 Ex 3.2 :::: (1) OK writable, (2) OK read, (3) ERROR Ex 4.1 :::: mutable cached, mutable valid, const on magnitude() Ex 4.2 :::: 5.0 and 13.0, both compile Ex 5.1 :::: A=UB, B=well-defined (m=7), C=compiles (later write is UB) Ex 5.2 :::: (1) ERROR, (2) ERROR, (3) OK, backing=55
Recall Feynman recap
const on a variable freezes the name. const left of * freezes the pointee; right of * freezes the pointer. const after a method freezes this so the method may only read. mutable unfreezes a chosen member for bookkeeping. Casting away a genuinely const object and writing = undefined behaviour. Nothing else to memorize — just read right-to-left.