Embercode
✕
Pointers and Memory AddressesLesson 15 of 16

45 min read

const and final

Why this matters

Good code says what it will not do. When a function receives a list of scores and only reads it, a reader should not have to check every line to be sure the list stays the same. C++ lets you write that promise in the code with the word const, short for "constant". The compiler checks the promise. A broken promise shows up as a build error on your machine, not as a wrong total in a report weeks later.

Java has a similar word, final. Many Java programmers expect the two words to do the same job, and they do not. In Java, final locks the name, so the name cannot point at something else. In C++, const locks the way you reach an object through a particular name. It can lock a variable, a pointer, a reference or a member function, and each one protects a different part of the picture.

This lesson builds each form one step at a time, shows the error messages the compiler prints, and then compares C++ const with Java final. It assumes you know what a pointer is and what & and * do, which the earlier lessons in this module cover. It also uses std::vector, a list that grows on its own, from the standard library.

One object, more than one way in

Before the code, one idea is needed. An object can have more than one name, and each name can have its own rules. This is the key to every example in this lesson.

Picture a room with two doors. One door has a lock that only lets people in to look, not to move the furniture. The other door has no lock. Both doors open into the same room. Someone who uses the open door can move the furniture, and the person at the locked door will see the change when they look.

const works like the lock on one door. A name marked const can read the object but cannot change it. Another name, without const, may still change the same object. This is the most common surprise in C++, and the later examples show it directly.

A const variable

The simplest form is a variable that can never change after it is created.

int main() {
    const int limit = 10;
    limit = 20;
    return 0;
}

The compiler refuses to build it. With g++ 15.2.0 and -std=c++20 -Wall, the message is:

main.cpp:3:11: error: assignment of read-only variable ‘limit’

The message names the line (3), the column (11) and the variable. The word "read-only" means that the variable accepts a value once, when it is created, and never after. You must give a const variable a value when you declare it. A line such as const int limit; with no value does not build, because nothing could ever set it.

A const parameter that only reads

Functions are the most useful place for const. A parameter that is passed by reference avoids a copy, so the function can work on the caller's data without taking time to copy it. The risk is that the function could change the caller's data. Marking the parameter const removes that risk while keeping the fast pass-by-reference.

Here is a function that adds up scores. It reads them and returns a total. It never needs to change them.

#include <iostream>
#include <vector>
 
int sum(const std::vector<int>& values) {
    int total = 0;
    for (int v : values) {
        total += v;
    }
    return total;
}
 
int main() {
    std::vector<int> scores{90, 75, 60};
    std::cout << "sum = " << sum(scores) << '\n';
    return 0;
}

Output:

sum = 225

The & in const std::vector<int>& means the function gets the caller's vector, not a copy. The word const means the function's name for that vector is read-only. The loop reads each value, and the total is kept in a separate variable. Nothing in the function writes to values.

Now suppose a later edit adds a line that writes to the vector.

#include <vector>
 
void add_one(const std::vector<int>& v) {
    v.push_back(1);
}
 
int main() {
    std::vector<int> a;
    add_one(a);
    return 0;
}

The compiler reports:

main.cpp:4:16: error: passing ‘const std::vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]

The phrase "discards qualifiers" means the call would throw away the const promise. The word this refers to the object that push_back changes, here the vector v. The error stops the build before the program can run, which is the point of the promise.

Three forms of pointer

A pointer adds one more question. There are two things a pointer might keep read-only: the address it holds, and the value at that address. The keyword const can go before the * or after it, and the two positions mean different things. Read the declaration from right to left to be sure what it says.

Written asCan the address change?Can the value change through this name?
const int* pYesNo
int* const qNoYes
const int* const rNoNo

Here is one program that uses all three forms, with a second variable so the address changes can be seen.

#include <iostream>
 
int main() {
    int x = 1;
    int y = 2;
 
    // Pointer to const: the value is read-only through p, but p can move.
    const int* p = &x;
    std::cout << "*p = " << *p << '\n';
    p = &y;
    std::cout << "*p after move = " << *p << '\n';
 
    // Const pointer: q always points at x, but the value in x can change.
    int* const q = &x;
    *q = 42;
    std::cout << "x = " << x << '\n';
 
    // Const pointer to const: neither the address nor the value can change.
    const int* const r = &y;
    std::cout << "*r = " << *r << '\n';
    return 0;
}

Output:

*p = 1
*p after move = 2
x = 42
*r = 2

Trace the first part. p is a pointer to const, so *p reads the value at the address. The line p = &y changes which address p holds, and that is allowed. The value at the new address is 2.

The second part uses q, a const pointer. The line *q = 42 writes through the pointer, and the value of x becomes 42. That is allowed because the value is not const. The line q = &y would be refused, because the address is fixed.

The third part uses r, a const pointer to const. It can only be read.

Reading the declaration helps. In const int* p, the const sits before the *, so the value is the thing that is read-only. In int* const q, the const sits after the *, so the address is the thing that is fixed. When both appear, as in const int* const r, both are fixed.

Const member functions

A class can also promise that a method leaves the object alone. You write const after the parameter list of the method. The compiler then refuses any change to the object's data inside that method. The explicit keyword on the constructor stops a plain number such as 5 from turning into a Counter without being asked. The part after the colon, count_(start), sets the member before the constructor body runs.

#include <iostream>
 
class Counter {
public:
    explicit Counter(int start) : count_(start) {}
 
    int value() const {
        return count_;
    }
 
    void increment() {
        ++count_;
    }
 
private:
    int count_;
};
 
int main() {
    const Counter fixed(5);
    std::cout << "fixed value = " << fixed.value() << '\n';
 
    Counter normal(10);
    normal.increment();
    std::cout << "normal value = " << normal.value() << '\n';
    return 0;
}

Output:

fixed value = 5
normal value = 11

The object fixed is const, so the program may call only methods marked const. value() is marked const, so the call fixed.value() is allowed. increment() is not marked const, so fixed.increment() would be refused. The rule is simple: a const object can call only the const methods of its class.

Here is what happens when a const method tries to change the object.

class Counter {
public:
    int value() const {
        ++count_;
        return count_;
    }
 
private:
    int count_ = 0;
};
 
int main() {
    Counter c;
    return c.value();
}

The error is:

main.cpp:4:11: error: increment of member ‘Counter::count_’ in read-only object

Put the const on the method when the method only reads. The compiler then checks that the method keeps its promise.

A harder case: a class that hands out a read-only view

Real classes often hold a list and need to let other code read it, without letting that code change it. A const reference return solves this. The method returns a reference to the internal list, marked const, so the caller can look at the list but cannot add to it.

#include <iostream>
#include <vector>
 
class Team {
public:
    void add(int score) { scores_.push_back(score); }
 
    const std::vector<int>& scores() const { return scores_; }
 
    int best() const {
        int top = 0;
        for (int s : scores_) {
            if (s > top) top = s;
        }
        return top;
    }
 
private:
    std::vector<int> scores_;
};
 
int main() {
    Team t;
    t.add(70);
    t.add(95);
    std::cout << "best = " << t.best() << '\n';
    std::cout << "count = " << t.scores().size() << '\n';
    return 0;
}

Output:

best = 95
count = 2

The scores() method returns a reference to the real vector inside the team, so there is no copy. Because the reference is const, the caller cannot change the team's list. If the caller writes t.scores().push_back(100); in main, before the best line, the compiler reports the same "discards qualifiers" error as before:

main.cpp:26:25: error: passing ‘const std::vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]

The best() method is also const. It reads the scores and returns one number, so it is marked const too. A good class marks every method that does not change the object this way. Callers can then use a const version of the object without losing any feature they need.

Comparing with Java final

Java's final keyword is used in three places: on a local variable, on a field (a variable that belongs to an object), and on a method or a class. For this lesson the first two matter. The examples below were compiled and run with javac 25.0.4.1.

A Java final variable can be given a value once and never reassigned. That matches a C++ const variable. The difference appears with objects. In Java, final locks the name, not the object the name points at. The object behind the name can still change.

import java.util.ArrayList;
import java.util.List;
 
public class FinalDemo {
    public static void main(String[] args) {
        final List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        System.out.println("numbers = " + numbers);
    }
}

Output:

numbers = [1, 2]

The name numbers is final, so it can never point at a different list. The list itself is an ordinary ArrayList, and add changes it. The program is valid, and the list grows.

The C++ version of the same idea works differently. A const vector cannot be changed through its name at all, because push_back is not a const method.

#include <iostream>
#include <vector>
 
int main() {
    const std::vector<int> numbers{1, 2};
    std::cout << "size = " << numbers.size() << '\n';
    std::cout << "first = " << numbers[0] << '\n';
    return 0;
}

Output:

size = 2
first = 1

Reading works, because size and the index operator are const methods. Calling numbers.push_back(3) on this object is refused with the same "discards qualifiers" message shown earlier. Java final on a list gives you a list that can only be reached through one name and still grows. C++ const on a vector gives you a vector that does not grow through that name at all.

Fields show a second difference. In Java, a final field must be set once, in the constructor, and cannot be set again. In C++ the closest match is a const data member. The next program shows a class with one field that is set once and one field that can change.

public class FinalField {
    static class Box {
        final int size;
        int count;
 
        Box(int size) {
            this.size = size;
        }
    }
 
    public static void main(String[] args) {
        Box box = new Box(3);
        box.count = 7;
        System.out.println("count = " + box.count + ", size = " + box.size);
    }
}

Output:

count = 7, size = 3

The C++ version has the same shape.

#include <iostream>
 
class Box {
public:
    Box(int size) : size_(size) {}
    const int size_;
    int count_ = 0;
};
 
int main() {
    Box a(3);
    a.count_ = 7;
    std::cout << "count = " << a.count_ << ", size = " << a.size_ << '\n';
    return 0;
}

Output:

count = 7, size = 3

There is one more difference. A C++ class with a const member cannot be assigned to another object of the same class. Assignment would overwrite the const member, so the compiler refuses this program:

class Box {
public:
    Box(int size) : size_(size) {}
    const int size_;
};
 
int main() {
    Box a(3);
    Box b(4);
    a = b;
    return 0;
}

The compiler reports:

main.cpp:10:9: error: use of deleted function ‘Box& Box::operator=(const Box&)’
main.cpp:1:7: error: non-static const member ‘const int Box::size_’, cannot use default assignment operator

In Java, the statement a = b on two Box variables is allowed. It makes a point at the same object as b, and no data is copied. C++ copies the values into a, and a const member cannot be overwritten, so the copy fails. The lesson on aliasing covers the difference between a name that points at an object and a copy of it.

The C++ keyword final also exists, for classes and virtual methods, and it does a different job. This lesson uses the word final only for the Java comparison.

Common mistakes

Beginners make the same few mistakes with const. Each one below shows the real error or the real behaviour.

Mistake 1: thinking a const pointer protects the value

A common belief is that int* const q stops the value from changing. It does not. It stops the address from changing. The value can still be written through q, as the earlier example showed with *q = 42. The mistake shows up the other way round, when the programmer tries to move the pointer:

int main() {
    int x = 1;
    int y = 2;
    int* const q = &x;
    q = &y;
    return 0;
}

The compiler reports:

main.cpp:5:7: error: assignment of read-only variable ‘q’

If you want both fixed, write const int* const.

Mistake 2: thinking a pointer to const makes the original const

With const int* p = &x;, the pointer is read-only, but x itself is not. Writing x through its own name still works, and the pointer sees the change:

#include <iostream>
 
int main() {
    int x = 1;
    const int* p = &x;
    x = 5;
    std::cout << *p << '\n';
    return 0;
}

Output:

5

The const in const int* p only applies to writes made through p. The variable x is not const, so the line x = 5 is fine. If you need a value that can never change, declare the variable itself as const.

Mistake 3: passing a const object to a non-const parameter

A function that takes std::vector<int>& cannot accept a const std::vector<int>. The call is refused:

error: binding reference of type ‘std::vector<int>&’ to ‘const std::vector<int>’ discards qualifiers

Two fixes are possible. Make the parameter const std::vector<int>& when the function only reads. Or remove the const from the object when the function really needs to change it. Choose based on what the function does, not on what makes the error go away.

Mistake 4: forgetting const on a method that only reads

This version of Counter leaves the const off value():

class Counter {
public:
    explicit Counter(int start) : count_(start) {}
 
    int value() {
        return count_;
    }
 
private:
    int count_;
};
 
int main() {
    const Counter fixed(5);
    return fixed.value();
}

The compiler refuses the call on the const object:

main.cpp:15:23: error: passing ‘const Counter’ as ‘this’ argument discards qualifiers [-fpermissive]

The method is not allowed to be called on a const object, because the compiler cannot promise that value() leaves the object alone. If a method is called on a const object and fails, look at the method first. It is usually missing the const after its parameter list.

Mistake 5: removing const with a cast

C++ offers ways to remove const from a pointer or reference. These are for very rare cases, such as an old library that takes a non-const pointer but does not change the data. Removing const to make the compiler quiet and then writing to the object is undefined behaviour for an object that really was created const. The program may appear to work, then fail in a different place. When the compiler reports a const error, change the design, not the cast.

Practice

Each exercise has its answer under it. Try each one on paper or in a file before you read the answer.

Exercise 1: which lines fail

Read this code. Which of the six marked lines fail to compile?

int a = 1, b = 2;
const int* p = &a;
int* const q = &a;
const int* const r = &a;
*p = 3;      // line A
p = &b;      // line B
*q = 4;      // line C
q = &b;      // line D
*r = 5;      // line E
r = &b;      // line F

Show the answer

Lines A, D, E and F fail. Line B is allowed, because p is a pointer to const and its address may change. Line C is allowed, because q is a const pointer and the value it points at is not const. With g++ 15.2.0, the four errors are "assignment of read-only location ‘* p’" (line A), "assignment of read-only variable ‘q’" (line D), "assignment of read-only location ‘(const int)r’" (line E) and "assignment of read-only variable ‘r’" (line F).

Exercise 2: make a function read-only

This function copies the whole vector just to read it, and it is slower than needed. Change it so that it does not copy and does not allow changes to the caller's vector.

int total(std::vector<int> values) {
    int sum = 0;
    for (int v : values) sum += v;
    return sum;
}

Show the answer

Change the parameter to a const reference:

int total(const std::vector<int>& values) {
    int sum = 0;
    for (int v : values) sum += v;
    return sum;
}

The & removes the copy, and the const stops any change to the caller's vector. Calls such as total({1, 2, 3}) still compile, because the compiler can build a temporary vector that binds to a const reference.

Exercise 3: predict the output

What does this program print?

#include <iostream>
 
int main() {
    int x = 1;
    const int* p = &x;
    x = 5;
    std::cout << *p << '\n';
    return 0;
}

Show the answer

It prints 5. The const in const int* p stops writes through p. It does not stop the owner of x from changing it.

Exercise 4: add const where it belongs

The class below has two methods that only read. Add const to each one where it belongs, so that a const Team object can call them.

class Team {
public:
    void add(int score) { scores_.push_back(score); }
    int count() { return static_cast<int>(scores_.size()); }
    int total() {
        int sum = 0;
        for (int s : scores_) sum += s;
        return sum;
    }
private:
    std::vector<int> scores_;
};

Show the answer

Add const after the parameter list of count and total:

int count() const { return static_cast<int>(scores_.size()); }
int total() const {
    int sum = 0;
    for (int s : scores_) sum += s;
    return sum;
}

add stays without const, because it changes scores_. A test program that adds 4 and 6 to a team, copies it into const Team t and calls t.count() and t.total() compiles and prints 2 10. Without the const on those two methods, the call on the const object fails with "passing ‘const Team’ as ‘this’ argument discards qualifiers [-fpermissive]".

Exercise 5: explain the Java difference

A Java programmer writes final List<String> names = new ArrayList<>(); and then calls names.add("Ana"). The code compiles. The same programmer then writes the C++ line const std::vector<std::string> names{}; and calls names.push_back("Ana"). Explain why the C++ line does not compile and what the Java final keyword locks.

Show the answer

Java final locks the name names, so the name can never point at a different list. The list object itself is an ordinary ArrayList, and add changes it. In C++, const on the vector locks the object through the name names. push_back is not a const method, so calling it on a const vector breaks the promise and the compiler refuses. To build a list that can be read but not extended, use the C++ const form and call only const methods on it. To build a list that can grow, do not mark it const.

Summary

  • const makes a name read-only. Another name for the same object may still change it.
  • A const reference parameter avoids a copy and promises the function will not change the caller's data.
  • In const int* p, the value is read-only through p. In int* const q, the address is fixed. In const int* const r, both are fixed.
  • A method marked const can be called on a const object. A method that changes the object must not be marked const.
  • Java final locks the name, and the object behind it can still change. A C++ const object cannot be changed through its name, so it blocks calls to methods that change it.
Check yourself: Stack and const (7 questions) →Practice this topic: coding problems for Pointers and Memory Addresses →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Stack overflow limits