C++ Module 7 – Classes, objects and RAII
C++ classes, member functions and access control
Write C++ classes that keep their data private, protect an invariant through member functions, and use const so read-only objects stay read-only.
What you will learn
- Define classes with private data and public member functions
- Protect an invariant through the class interface
- Write const-correct member functions
- Decide whether a function belongs inside the class or outside it
Before you start
What you’ll learn · Before you start
What you will learn
- Define classes with private data and public member functions
- Protect an invariant through the class interface
- Write const-correct member functions
- Decide whether a function belongs inside the class or outside it
Before you start
On this page
One line anywhere in a program, wallet.paise = -500;, is enough to leave a struct holding a debt that no rule
allows, and nothing points to the line that did it. A class closes that hole. Its data members are private, so
only its own member function: A function declared inside a class and called on one object of it with a dot, as in asha.deposit(500). It may use the private members of every object of its class. can change them, and a few public member functions
form its interface, the only way the rest of the program can use an object. When each of those functions checks
its argument before it changes anything, a rule such as “the balance is never negative” holds for every object at
every moment: an invariant: A rule about an object's data that holds whenever none of its member functions is running, such as "the balance is never negative". A class keeps it by making the data private and checking every change in its member functions. you can prove from the class alone.
Below, a bank account that keeps its money in paise is built with Apple clang 21 and GCC 16. You will step through
its calls, see both compilers refuse code that reaches past the interface, mark the functions that only read as
const, and decide which functions belong inside the class at all.
A class lets only its own functions change its data
A struct with a public member lets any code write any value into it: with struct Wallet { long long paise; };, the
assignment of -500 compiles in every file that can see a Wallet. Here is the same account as a class:
// The balance is private, so the only way to change it is through member functions
// that keep the class's rule: the balance is never negative.
#include <format>
#include <print>
#include <string>
class BankAccount {
public:
// Adds money; refuses an amount that is zero or negative.
bool deposit(long long paise) {
if (paise <= 0) return false;
balance_ += paise;
return true;
}
// Takes money out; refuses an amount that would leave the balance below zero.
bool withdraw(long long paise) {
if (paise <= 0 || paise > balance_) return false;
balance_ -= paise;
return true;
}
// Moves money into another account.
bool transfer_to(BankAccount& other, long long paise) {
if (!withdraw(paise)) return false;
other.balance_ += paise; // allowed: access is granted per class, not per object
return true;
}
long long balance() const { return balance_; } // const: it only reads
private:
long long balance_ = 0; // paise (1 rupee = 100 paise); never below 0
};
// A free function: it needs nothing private, so it is not a member.
std::string in_rupees(long long paise) {
return std::format("{}.{:02}", paise / 100, paise % 100);
}
int main() {
BankAccount asha;
BankAccount dev;
std::println("deposit 50000: {}", asha.deposit(50'000));
std::println("withdraw 80000: {}", asha.withdraw(80'000));
std::println("deposit -100: {}", asha.deposit(-100));
std::println("transfer 12550: {}", asha.transfer_to(dev, 12'550));
std::println("asha {} rupees, dev {} rupees", in_rupees(asha.balance()), in_rupees(dev.balance()));
std::println("sizeof(BankAccount) = {}, sizeof(long long) = {}", sizeof(BankAccount), sizeof(long long));
} from session import run
# bank_account.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account && ./bank_account
g++-16 -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account-gcc && ./bank_account-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account && ./bank_account deposit 50000: true withdraw 80000: false deposit -100: false transfer 12550: true asha 374.50 rupees, dev 125.50 rupees sizeof(BankAccount) = 8, sizeof(long long) = 8 $ g++-16 -std=c++23 -Wall -Wextra bank_account.cpp -o bank_account-gcc && ./bank_account-gcc deposit 50000: true withdraw 80000: false deposit -100: false transfer 12550: true asha 374.50 rupees, dev 125.50 rupees sizeof(BankAccount) = 8, sizeof(long long) = 8
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bank_account_session.py
Every change to balance_ goes through a member function, and each one checks its argument before it touches the
data. Step through the four calls that main makes and watch each account’s balance after every one:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Two valid accounts. BankAccount asha; and BankAccount dev; create two accounts. The default member initialiser = 0 gives each balance_ the value 0, so both obey the rule from their first moment.
- deposit(50'000). asha.deposit(50'000) checks that the amount is above 0, then adds it. The call returns true, the first line of the output, and balance_ is 50,000 paise.
- withdraw(80'000). withdraw tests 80,000 > balance_ before it subtracts anything. The test is true, so the call returns false and balance_ stays 50,000: a refused call changes nothing.
- deposit(-100). -100 is not a deposit, so deposit returns false at its first line. Again nothing changed, and the balance still obeys the rule.
- The transfer's withdrawal. asha.transfer_to(dev, 12'550) first calls withdraw(12'550) on asha. That passes its check and leaves 37,450 paise.
- Into another object. Then it adds the amount to other.balance_, the private member of dev. That compiles, because access is granted to the code of BankAccount, not to one object.
- The rule held throughout. main prints asha 374.50 rupees, dev 125.50 rupees. After every call both balances were 0 or more, and no line outside the class could have written them.
The withdrawal of 80,000 paise is refused because the account holds only 50,000, and the deposit of -100 is refused
because it is not a deposit at all. Neither refusal changed anything, so the account still satisfies its rule after
every call, and you can read that off the class definition alone: nothing outside it can assign to balance_.
The last line of the output is worth a second look. A BankAccount is 8 bytes, exactly the size of the long long
inside it. Access control is a compile-time check of names. The object holds the same bytes as a struct with the same
member would, and calling asha.balance() costs the same as reading the member directly once the compiler inlines
the call.
Keep money in whole paise
Store the smallest unit as an integer. A double cannot hold most decimal fractions exactly: 0.1 is stored as the
nearest binary fraction, and sums of such values drift. Whole paise, the hundredths of a rupee, in a long long
are exact, and the same fix works in any currency.
Checkpoint
Check: Private data behind public functions
Access is checked by class, while compiling
The access specifier: One of the labels public:, private: and protected: in a class. Each applies to the members declared after it, up to the next label; members of a class start out private and those of a struct public. public: and private: apply to every member declared after
them, until the next label. A class may use each label as often as it likes and in any order; the common style puts
the public interface first, because that is what someone reading the class needs, and the private details last. A
third label, protected:, matters only for inheritance, which the next module covers.
Without any label, the members of a class are private and those of a struct are public, and base classes follow
the same default. That default is the only difference between the two keywords. Rule C.2 of the C++ Core Guidelines
builds the convention on it: use class when the members are tied together by an invariant, and struct when each
member can take any value on its own.
| Criterion | public | private |
|---|---|---|
| Own member functions | Yes | Yes |
| Same, other object | Yes | Yes |
| Any other code | Yes | No |
| Default in a class | No | Yes |
| Default in a struct | Yes | No |
A “No” in the third row is a compile error, as the next example shows. The second row, a member function working on
another object of the same class, explains the last call of the figure above: transfer_to adds to
other.balance_, the private member of a different account, and that compiles. Access is granted to the code of a
class, not to one object: any member function of BankAccount may use the private members of any BankAccount.
Code outside the class cannot. This file tries to set the balance from main:
// Code outside the class tries to set the balance directly.
class BankAccount {
public:
long long balance() const { return balance_; }
private:
long long balance_ = 0;
};
int main() {
BankAccount acct;
acct.balance_ = -500; // not allowed: balance_ is private
return acct.balance() < 0;
} from session import run
# Both compilers reject the assignment to a private member.
run("""
clang++ -std=c++23 -Wall -Wextra -c private_access.cpp
g++-16 -std=c++23 -Wall -Wextra -c private_access.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c private_access.cpp
private_access.cpp:12:10: error: 'balance_' is a private member of 'BankAccount'
12 | acct.balance_ = -500; // not allowed: balance_ is private
| ^
private_access.cpp:7:15: note: declared private here
7 | long long balance_ = 0;
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c private_access.cpp
private_access.cpp: In function 'int main()':
private_access.cpp:12:10: error: 'long long int BankAccount::balance_' is private within this context
12 | acct.balance_ = -500; // not allowed: balance_ is private
| ^~~~~~~~
private_access.cpp:7:15: note: declared private here
7 | long long balance_ = 0;
| ^~~~~~~~
private_access.cpp:12:10: note: field 'long long int BankAccount::balance_' can be accessed via 'long long int BankAccount::balance() const'
12 | acct.balance_ = -500; // not allowed: balance_ is private
| ^~~~~~~~
| balance()
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 private_access_session.py
Both errors name the member and point to the line where it was declared private. Clang says it “is a private
member”; GCC says it “is private within this context” and, in GCC 16, adds a hint that the field can be read through
the public balance() const function. Both compilers accept a switch, -fno-access-control, that turns the check
off; GCC’s manual describes it as a way to work around bugs in the compiler’s own access checking, so it is no way
into a class. To let main write the balance, you would have to change the class, and that is the protection.
Checkpoint
Check: Access is per class and checked while compiling
A member function runs on the object before the dot
A call such as asha.deposit(50'000) runs deposit for one particular object. Inside the function, the name
balance_ refers to that object’s member. The compiler makes this work by passing the function a hidden pointer to
the object, which the body can name as this: Inside an ordinary member function, a pointer to the object the function was called on: in a.add(4'000), this is &a. A member name such as total_ in the body means this->total_.. Writing this->balance_ and writing
balance_ mean the same thing.
// Inside a member function, this points to the object the function was called on.
#include <print>
class Basket {
public:
Basket& add(long long price_paise); // declared here, defined after the class
long long total() const { return total_; }
int items() const { return items_; }
bool is_object_at(const Basket* where) const { return this == where; }
private:
long long total_ = 0;
int items_ = 0;
};
// Basket:: names the class this member function belongs to.
Basket& Basket::add(long long price_paise) {
this->total_ += price_paise; // the same as total_ += price_paise
++items_;
return *this; // the object itself, so calls can be chained
}
int main() {
Basket a;
Basket b;
a.add(4'000).add(2'550).add(999);
std::println("a: {} items, {} paise", a.items(), a.total());
std::println("b: {} items, {} paise", b.items(), b.total());
std::println("a.is_object_at(&a): {}", a.is_object_at(&a));
std::println("b.is_object_at(&a): {}", b.is_object_at(&a));
} from session import run
# this_pointer.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer && ./this_pointer
g++-16 -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer-gcc && ./this_pointer-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer && ./this_pointer a: 3 items, 7549 paise b: 0 items, 0 paise a.is_object_at(&a): true b.is_object_at(&a): false $ g++-16 -std=c++23 -Wall -Wextra this_pointer.cpp -o this_pointer-gcc && ./this_pointer-gcc a: 3 items, 7549 paise b: 0 items, 0 paise a.is_object_at(&a): true b.is_object_at(&a): false
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 this_pointer_session.py
Step through the program and watch where this points in each call: first along the chain of calls on a, then in
the two calls of is_object_at:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- a.add(4'000). a.add(4'000) runs Basket::add with this set to &a, the address of the object before the dot. total_ += price_paise in the body means this->total_, so a holds 1 item and 4,000 paise.
- return *this. add ends with return *this;, which hands back a itself, by reference, not a copy. The next call of the chain therefore runs on a again.
- .add(2'550). The second call gets the same this, &a: total_ becomes 6,550 paise and items_ 2.
- .add(999). The third call adds 999: a holds 3 items and 7,549 paise, the first line of the output. b was never before a dot, so it still holds 0 items, the second line.
- a.is_object_at(&a). is_object_at compares this with the address it is given. Called on a with &a, this is &a, so the comparison is true, as the third line shows.
- b.is_object_at(&a). Called on b, this is &b. The argument is still &a, so the comparison is false, the last line: this always names the object before the dot, never an argument.
Three details of this file come up in every larger class:
addis declared inside the class and defined after it. The definition names the class,Basket::add, so the compiler knows which class the function belongs to and lets the body use the private members.return *this;returns the object itself, by reference, so the calls can be chained:a.add(4'000).add(2'550).add(999)adds three prices to the same basket.is_object_atcomparesthiswith an address and shows thatthisis the address of the object before the dot:a.is_object_at(&a)is true andb.is_object_at(&a)is false.
A member function defined inside the class body is implicitly inline (in code that does not use C++20 modules), so a
header can contain the whole class and be included by many .cpp files without the duplicate symbol error of the
lesson on compiling. A definition written after the class, as Basket::add is here, goes in a .cpp file, or is
marked inline when it stays in a header.
Checkpoint
Check: this and the object before the dot
A const member function promises not to change the object
balance() in bank_account.cpp is declared long long balance() const, a
const member function: A member function with const after its parameter list, as in long long balance() const. It may not change the object, and it is the only kind of member function a const object or a const reference can call.. The const after the parameter list is a promise that
the function does not change the object. Inside it, this points to a const object, so an assignment such as
balance_ = 0; in its body is a compile error.
The promise matters to the caller. A function that receives const BankAccount& acct may only call the member
functions of acct that are marked const, and so may a const BankAccount variable:
| Criterion | const member function | Any other |
|---|---|---|
| BankAccount acct | Yes | Yes |
| const BankAccount& ref | Yes | No: compile error |
| const BankAccount acct | Yes | No: compile error |
Forget the marker on a function that only reads, and a correct-looking call in the second row fails:
// print_statement promises not to change the account (it takes a const reference),
// then calls a member function that was not marked const.
#include <print>
class BankAccount {
public:
bool deposit(long long paise) {
if (paise <= 0) return false;
balance_ += paise;
return true;
}
long long balance() { return balance_; } // missing: const after ()
private:
long long balance_ = 0;
};
void print_statement(const BankAccount& acct) {
std::println("balance: {} paise", acct.balance());
}
int main() {
BankAccount acct;
acct.deposit(2'500);
print_statement(acct);
} from session import run
# A non-const member function called through a const reference.
run("""
clang++ -std=c++23 -Wall -Wextra -c const_member.cpp
g++-16 -std=c++23 -Wall -Wextra -c const_member.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c const_member.cpp
const_member.cpp:19:39: error: 'this' argument to member function 'balance' has type 'const BankAccount', but function is not marked const
19 | std::println("balance: {} paise", acct.balance());
| ^~~~
const_member.cpp:12:15: note: 'balance' declared here
12 | long long balance() { return balance_; } // missing: const after ()
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c const_member.cpp
const_member.cpp: In function 'void print_statement(const BankAccount&)':
const_member.cpp:19:51: error: passing 'const BankAccount' as 'this' argument discards qualifiers [-fpermissive]
19 | std::println("balance: {} paise", acct.balance());
| ~~~~~~~~~~~~^~
const_member.cpp:12:15: note: in call to 'long long int BankAccount::balance()'
12 | long long balance() { return balance_; } // missing: const after ()
| ^~~~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 const_member_session.py
Clang states the problem directly: the object has type const BankAccount, “but function is not marked const”.
GCC’s message, “passing ‘const BankAccount’ as ‘this’ argument discards qualifiers”, says the same in older terms:
calling balance() would turn a pointer to a const object into a plain pointer. The [-fpermissive] at the end of
GCC’s line names an option that would downgrade this error to a warning. GCC’s manual describes it as a way to let
some nonconforming code compile, and here it would only hide a real mistake.
Removing const from the caller
The error points at the call, so it is tempting to change print_statement(const BankAccount& acct) into
print_statement(BankAccount& acct). That compiles, but the function no longer promises to leave the account alone,
and callers that hold a const account can no longer use it. Fix the class instead: long long balance() const.
Rule Con.2 of the C++ Core Guidelines turns this into a habit: every member function is const unless calling it
changes what the object looks like from outside. Readers then see at a glance which calls can modify an object, and
const references can be passed around freely.
Checkpoint
Check: const member functions
Operations, not setters, keep the invariant
The public interface is the only way to the private data
Text description of the diagram
The diagram has three boxes: code outside the class (main and other functions), the public interface of BankAccount (deposit, withdraw, transfer_to and balance) and the private data (balance_ in paise, with the rule that it is never below 0).
- Code outside the class calls the public member functions.
- Each public function checks its argument first and changes the private balance only when the rule still holds afterwards.
- A dashed arrow from the outside code straight to the private data is labelled "acct.balance_ = -500; compile error": the compiler refuses any direct access, so the only way to the data is through the interface.
The diagram shows the shape every class in this module has: the rest of the program reaches the data only through the public functions, and each of those functions keeps the rule. Three habits keep that shape intact:
- Offer operations, not setters.
depositandwithdrawdescribe what users of an account do. A publicset_balance(long long)would accept -500 just as the struct did, and would reopen the hole the class closed. Rule C.131 of the Core Guidelines warns against getters and setters that do nothing but read or write a member. - Check first, then change.
withdrawtests its argument before it subtracts, so a refused call leaves the object exactly as it was. A function that changed one member and then gave up halfway would leave the invariant broken. - Start valid. The default member initialiser
= 0makes every new account valid from its first moment. The next lesson adds constructors, for classes whose objects need values from the caller before they can be valid.
When the members have no rule between them, as in a point with two coordinates, a struct with public members is the simpler and better choice.
Key idea
A class keeps its invariant when its data is private, every public function checks before it changes anything, and no function hands out a way to write the data unchecked.
Checkpoint
Check: An interface that keeps the invariant
A function that needs no private data stays outside the class
Not every function that works with an object should be a member. Rule C.4 of the Core Guidelines keeps the member
list for functions that cannot do their job without touching the private data. Sorting the functions of
bank_account.cpp, and a transfer built from the interface, by that test:
| Criterion | Uses balance_? | Belongs |
|---|---|---|
| deposit | Writes it | Member |
| withdraw | Writes it | Member |
| balance | Reads it | Member, const |
| in_rupees | No, only its argument | Free function |
| transfer, from withdraw and deposit | No, only the interface | Free function |
in_rupees turns any number of paise into text such as 374.50; it needs nothing private, so it is an ordinary
function outside the class. Every member function is a place where the invariant can be broken, so a class with fewer
members is easier to check.
transfer_to shows the same choice. As a member it may add to other.balance_ directly, which is how this lesson
demonstrated per-class access. It could just as well be a free function built from the public interface,
bool transfer(BankAccount& from, BankAccount& to, long long paise), which calls from.withdraw(paise) and then
to.deposit(paise). That version cannot break the rule at all, because the two functions it uses already keep it.
Checkpoint
Check: Member or free function
Practise: a race clock that keeps its invariant
RaceTime holds a time on a race clock, such as 3:05:09 for three hours, five minutes and nine seconds. Its
invariant is that minutes() and seconds() are always between 0 and 59. The starter code does not keep it yet:
C header · race_time.h
// Starter code for "Your turn": make RaceTime keep its invariant.
#pragma once
#include <compare>
#include <format>
#include <string>
// A time on a race clock, such as 3:05:09 (hours, minutes, seconds).
// The invariant: minutes() and seconds() are always between 0 and 59.
class RaceTime {
public:
// Adds a split time. Carries every 60 seconds into a minute and every 60 minutes
// into an hour. Returns false, and changes nothing, if any part is negative.
bool add(int hours, int minutes, int seconds) {
// TODO: check the arguments and keep the invariant.
hours_ += hours;
minutes_ += minutes;
seconds_ += seconds;
return true;
}
int hours() const { return hours_; }
int minutes() const { return minutes_; }
int seconds() const { return seconds_; }
// "3:05:09": minutes and seconds always take two digits.
std::string text() const {
// TODO: pad minutes and seconds to two digits.
return std::format("{}:{}:{}", hours_, minutes_, seconds_);
}
// Compares hours, then minutes, then seconds: right only while the invariant holds.
auto operator<=>(const RaceTime&) const = default;
private:
int hours_ = 0;
int minutes_ = 0;
int seconds_ = 0;
}; C++ · race_time_tests.cpp
// Sample tests for race_time.h. Build: clang++ -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests
#include <print>
#include "race_time.h"
int passed = 0;
int failed = 0;
void expect(bool ok, const char* what) {
if (ok) {
++passed;
} else {
++failed;
std::println("FAIL {}", what);
}
}
RaceTime make(int h, int m, int s) {
RaceTime t;
t.add(h, m, s);
return t;
}
int main() {
expect(RaceTime{}.text() == "0:00:00", "a new RaceTime reads 0:00:00");
expect(make(0, 0, 75).text() == "0:01:15", "75 seconds carry into 0:01:15");
expect(make(0, 125, 0).text() == "2:05:00", "125 minutes carry into 2:05:00");
RaceTime lap = make(1, 59, 59);
lap.add(0, 0, 1);
expect(lap.text() == "2:00:00", "one more second after 1:59:59 makes 2:00:00");
RaceTime run = make(3, 5, 9);
expect(!run.add(0, -1, 0), "add refuses a negative part");
expect(run.text() == "3:05:09", "a refused add changes nothing");
expect(make(0, 90, 0) == make(1, 30, 0), "90 minutes equal 1:30:00");
expect(make(2, 58, 119) < make(3, 0, 0), "2:59:59 is faster than 3:00:00");
expect(make(0, 0, 86'400).hours() == 24, "a whole day of seconds is 24 hours");
std::println("{} of {} checks passed", passed, passed + failed);
return failed == 0 ? 0 : 1;
} Before you change anything, the starter code fails most of the checks:
from session import run
# The sample tests against the starter code, before the TODOs are done.
run("""
clang++ -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests && ./race_time_tests
g++-16 -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests-gcc && ./race_time_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests && ./race_time_tests FAIL a new RaceTime reads 0:00:00 FAIL 75 seconds carry into 0:01:15 FAIL 125 minutes carry into 2:05:00 FAIL one more second after 1:59:59 makes 2:00:00 FAIL add refuses a negative part FAIL a refused add changes nothing FAIL 90 minutes equal 1:30:00 FAIL a whole day of seconds is 24 hours 1 of 9 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra race_time_tests.cpp -o race_time_tests-gcc && ./race_time_tests-gcc FAIL a new RaceTime reads 0:00:00 FAIL 75 seconds carry into 0:01:15 FAIL 125 minutes carry into 2:05:00 FAIL one more second after 1:59:59 makes 2:00:00 FAIL add refuses a negative part FAIL a refused add changes nothing FAIL 90 minutes equal 1:30:00 FAIL a whole day of seconds is 24 hours 1 of 9 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 race_time_session.py
Edit race_time.h until both compilers print 9 of 9 checks passed:
addrefuses a negative part: it returnsfalseand changes nothing.addcarries every 60 seconds into a minute and every 60 minutes into an hour. Converting the stored time and the split to a number of seconds, adding them, and converting back is the simplest way. Do the arithmetic inlong long, so that a large split cannot overflow anint.textpads minutes and seconds to two digits:std::format("{:02}", 5)gives05.
The class compares times with the defaulted operator<=> from the lesson on structs, member by member: hours, then
minutes, then seconds. That comparison is right only while the invariant holds. Without the carry, 90 minutes would be
stored as 0 hours and 90 minutes, and it would compare as smaller than 1:30:00 because its hours are smaller. Keeping
the invariant inside add is what makes the one-line comparison correct.
Then try to break the class from the outside: add t.seconds_ = 75; to the tests and build them again. The compiler
stops you, which is the protection this lesson is about.
Then test what stayed with you, from the private balance to the free transfer. The session opens on whichever of
the six ideas your answers have been weakest on so far, and each choice says what it gets right or misses:
Practice that adapts to you
Practise this lesson
13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What does const after a member function’s parameter list mean?
It promises that the function does not change the object it is called on. Inside such a function, this points to
a const object, so the compiler rejects any assignment to a data member, and any call to a member function that is
not itself const. The promise is what lets the function be called on const objects and through const references:
a function that takes const Account& can call balance() const but not withdraw(). Getting it wrong shows up at
the call site, as Clang’s “function is not marked const” or GCC’s “discards qualifiers”. A good answer adds that
const is part of the function’s signature, so a class may overload a member function on it, and that a member
declared mutable may still change inside a const function, which is meant for caches and counters that do not
change what the object represents.
Two follow-ups usually come next: which of two overloads that differ only in const a call picks, and how a const
function can still count its own calls. Decide both:
Checkpoint
Interview round: two follow-up questions
Two design questions start from this lesson: an account class whose public setter let a balance go negative, and a
const_cast proposed to make a call through a const reference compile:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
An Account class exposes setBalance(long long) and getBalance(), and a production bug left one balance negative. How would you redesign the class so that this cannot happen again, and how would you move hundreds of existing callers to the new interface safely?
-
Mid to senior
A function takes const Order& and fails to compile because it calls order.total(), which is not const. A teammate proposes const_cast to make it build. What do you do instead, and when is const_cast actually safe?
Key takeaways
- A class keeps its data members private and offers public member functions, so every change goes through code that checks it and the account’s rule holds after every call.
- Access is checked by the compiler, per class rather than per object, and costs nothing at run time: the account is
8 bytes, the size of its one member, and
structdiffers fromclassonly in its default access. - Inside a member function,
thispoints to the object before the dot, and returning*thisby reference lets calls be chained on the same object. - A
constmember function promises not to change the object, and const objects and const references can call only such functions, so every function that only reads should be markedconst. - Operations rather than setters, a check before every change and a valid start keep a class’s invariant.
- A function that needs no private data stays outside the class, where it cannot break the invariant.
Terms in this lesson
- access specifier
- One of the labels public:, private: and protected: in a class. Each applies to the members declared after it, up to the next label; members of a class start out private and those of a struct public.
- const member function
- A member function with const after its parameter list, as in long long balance() const. It may not change the object, and it is the only kind of member function a const object or a const reference can call.
- invariant
- A rule about an object's data that holds whenever none of its member functions is running, such as "the balance is never negative". A class keeps it by making the data private and checking every change in its member functions.
- member function
- A function declared inside a class and called on one object of it with a dot, as in asha.deposit(500). It may use the private members of every object of its class.
- this
- Inside an ordinary member function, a pointer to the object the function was called on: in a.add(4'000), this is &a. A member name such as total_ in the body means this->total_.
References
- Working Draft, Programming Languages: C++ (N4950): [class.access], [class.mfct], [expr.prim.this] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: C.2, use class if the class has an invariant (Standard C++ Foundation)
- C++ Core Guidelines: C.4, make a function a member only if it needs direct access to the representation (Standard C++ Foundation)
- C++ Core Guidelines: C.9, minimize exposure of members (Standard C++ Foundation)
- C++ Core Guidelines: C.131, avoid trivial getters and setters (Standard C++ Foundation)
- C++ Core Guidelines: Con.2, by default make member functions const (Standard C++ Foundation)
- Member Access Control (C++) (Microsoft)
- Options Controlling C++ Dialect (-fno-access-control) (GNU Project)
- Options to Request or Suppress Warnings (-fpermissive) (GNU Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress