Classes and objects
Lesson 14 used struct to keep a few things together. That's handy, but anyone can change the members directly. A bank account's balance can be set to a negative number just like that:
1#include <iostream>2#include <string>3 4struct Account {5 std::string owner;6 int balance;7};8 9int main() {10 Account acc{"Ana", 100};11 acc.balance = -5000;12 std::cout << acc.owner << "'s balance: " << acc.balance << '\n';13 return 0;14}
Ana's balance: -5000
The compiler doesn't stop it and the program runs to the end. This lesson is about class: hide the data, let the outside use it only through a few functions, and let the type itself keep its rules.
Member functions
A struct or class can hold functions as well as variables. A function written inside is a member function, and it can use the member variables of the same object directly:
1#include <iostream>2 3struct Counter {4 int count = 0;5 6 void add() {7 count++;8 }9 10 void show() {11 std::cout << "Now at " << count << '\n';12 }13};14 15int main() {16 Counter people;17 Counter cars;18 people.add();19 people.add();20 cars.add();21 people.show();22 cars.show();23 return 0;24}
Now at 2 Now at 1
int count = 0; gives the member a starting value. Counter is a type; people and cars are two objects of that type, each with its own count. You call a member function with . too: people.add() adds to the count of people and leaves cars alone. Press Step and watch each object's fields change on its own.
class and private
Change struct to class and split it with public: and private:. Anyone can use what's under public:; what's under private: can be used only by the class's own member functions.
1#include <iostream>2 3class Account {4public:5 void deposit(int amount) {6 if (amount > 0) {7 balance_ += amount;8 }9 }10 11 int balance() {12 return balance_;13 }14 15private:16 int balance_ = 0;17};18 19int main() {20 Account acc;21 acc.deposit(300);22 acc.deposit(-50);23 std::cout << "Balance: " << acc.balance() << '\n';24 return 0;25}
Balance: 300
The balance balance_ is private. The only way in from outside is deposit, and deposit refuses negative amounts, so the -50 never got in. This course puts an underscore after the name of a private member variable (balance_): you can tell members at a glance, and the function that reads it can be called balance() without a name clash.
Try to change it directly from outside and you get a compile error:
1#include <iostream>2 3class Account {4public:5 int balance() {6 return balance_;7 }8 9private:10 int balance_ = 0;11};12 13int main() {14 Account acc;15 acc.balance_ = -5000;16 std::cout << acc.balance() << '\n';17 return 0;18}
main.cpp: In function 'int main()':
main.cpp:15:9: error: 'int Account::balance_' is private within this context
15 | acc.balance_ = -5000;
| ^~~~~~~~
main.cpp:10:9: note: declared private here
10 | int balance_ = 0;
| ^~~~~~~~Look at the first error:: 'int Account::balance_' is private within this context. Below it, a note: from g++ points to the line where it was declared.
Why private?
Hiding the data isn't meant to make the class awkward to use. It puts the rules in one place. The account's rule is "the balance never goes negative", so the check for taking money out goes in withdraw, and nothing outside can get around it:
1#include <iostream>2 3class Account {4public:5 void deposit(int amount) {6 if (amount > 0) {7 balance_ += amount;8 }9 }10 11 bool withdraw(int amount) {12 if (amount <= 0 || amount > balance_) {13 return false;14 }15 balance_ -= amount;16 return true;17 }18 19 int balance() {20 return balance_;21 }22 23private:24 int balance_ = 0;25};26 27int main() {28 Account acc;29 acc.deposit(100);30 std::cout << acc.withdraw(30) << '\n';31 std::cout << acc.withdraw(500) << '\n';32 std::cout << "Balance: " << acc.balance() << '\n';33 return 0;34}
1 0 Balance: 70
Taking 30 works (it shows 1), 500 is too much (it shows 0), and the balance is 70. If a negative balance ever turns up, this class is the only place to look.
There's another gain: the outside knows only deposit, withdraw and balance. If one day you store the balance in cents, or keep a list of transactions, you change only the inside of the class, and the code that uses it doesn't change at all.
What does this show?
1#include <iostream>2 3class Light {4public:5 void toggle() {6 on_ = !on_;7 if (on_) {8 times_++;9 }10 }11 12 int times() {13 return times_;14 }15 16private:17 bool on_ = false;18 int times_ = 0;19};20 21int main() {22 Light lamp;23 lamp.toggle();24 lamp.toggle();25 lamp.toggle();26 std::cout << lamp.times() << '\n';27 return 0;28}
times_ goes up only when the light turns on. It starts off: the first toggle turns it on (1), the second turns it off, the third turns it on again (2).
const member functions
Lesson 10 said: pass big things you only read as a const reference. But pass an Account as const Account&, call balance(), and it doesn't compile:
1#include <iostream>2 3class Account {4public:5 int balance() {6 return balance_;7 }8 9private:10 int balance_ = 70;11};12 13void report(const Account& acc) {14 std::cout << "Balance: " << acc.balance() << '\n';15}16 17int main() {18 Account acc;19 report(acc);20 return 0;21}
main.cpp: In function 'void report(const Account&)':
main.cpp:14:44: error: passing 'const Account' as 'this' argument discards qualifiers [-fpermissive]
14 | std::cout << "Balance: " << acc.balance() << '\n';
| ~~~~~~~~~~~^~The error says passing 'const Account' as 'this' argument discards qualifiers: acc is const, but balance() never promised not to change the object, so the compiler won't call it. (this is a pointer to the object itself; more on it with pointers in lesson 19.)
The fix: put const after the parentheses of every member function that only reads:
1#include <iostream>2 3class Account {4public:5 int balance() const {6 return balance_;7 }8 9private:10 int balance_ = 70;11};12 13void report(const Account& acc) {14 std::cout << "Balance: " << acc.balance() << '\n';15}16 17int main() {18 Account acc;19 report(acc);20 return 0;21}
Balance: 70
int balance() const says: this function doesn't change the object. Change balance_ inside it and that's a compile error. Make it a habit: member functions that only read, show or compute get a const.
A temperature class
One complete example. A temperature can't go below absolute zero, −273.15 °C, and set_celsius keeps that rule. Fahrenheit isn't stored; it's worked out when asked for:
1#include <iostream>2 3class Temperature {4public:5 bool set_celsius(double c) {6 if (c < -273.15) {7 return false;8 }9 celsius_ = c;10 return true;11 }12 13 double celsius() const {14 return celsius_;15 }16 17 double fahrenheit() const {18 return celsius_ * 9 / 5 + 32;19 }20 21private:22 double celsius_ = 0;23};24 25int main() {26 Temperature t;27 t.set_celsius(25);28 t.set_celsius(-300);29 std::cout << t.celsius() << " °C = " << t.fahrenheit() << " °F\n";30 return 0;31}
25 °C = 77 °F
-300 was refused, so it's still 25 degrees. Right now every object starts at 0 degrees and needs a call to set_celsius to get a value; next lesson's constructors give an object its values the moment it's created.
struct vs class
In C++, struct and class are almost the same: a struct can have member functions and private: too. There is one difference: **when you don't write public: or private:**, members of a struct are public and members of a class are private.
What happens?
1#include <iostream>2 3class Point {4 int x = 3;5};6 7int main() {8 Point p;9 std::cout << p.x << '\n';10 return 0;11}
A class member with no public: above it is private, so p.x in main is a compile error: 'int Point::x' is private within this context. With struct instead, it shows 3.
The usual habit: when you just keep some data together and there are no rules to keep, use struct (like Item in lesson 14). When there are rules and the data should be hidden, use class, with the public: part first and the private: part after.
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it.
Fix it: reading needs const
✓This program doesn't compile. Change only the Account class so it shows Balance: 250.
Balance: 250
1#include <iostream>2 3class Account {4public:5 void deposit(int amount) {6 balance_ += amount;7 }8 9 int balance() {10 return balance_;11 }12 13private:14 int balance_ = 0;15};16 17void report(const Account& acc) {18 std::cout << "Balance: " << acc.balance() << '\n';19}20 21int main() {22 Account acc;23 acc.deposit(250);24 report(acc);25 return 0;26}
Step counter
✓Finish StepCounter: add(steps) adds the steps but ignores negative numbers; total() returns how many steps in all. The program should show 9000 steps today.
9000 steps today
1#include <iostream>2 3class StepCounter {4public:5 6private:7 int total_ = 0;8};9 10int main() {11 StepCounter today;12 today.add(3500);13 today.add(-200);14 today.add(5500);15 std::cout << today.total() << " steps today\n";16 return 0;17}
How long is the playlist?
✓Playlist keeps each song's length in seconds in a vector. Add a const member function total_minutes() that returns the total in whole minutes (integer division; leftover seconds don't count). The program should show 3 songs, 11 minutes.
3 songs, 11 minutes
1#include <iostream>2#include <vector>3 4class Playlist {5public:6 void add(int seconds) {7 songs_.push_back(seconds);8 }9 10 int count() const {11 return songs_.size();12 }13 14private:15 std::vector<int> songs_;16};17 18int main() {19 Playlist p;20 p.add(215);21 p.add(250);22 p.add(240);23 std::cout << p.count() << " songs, " << p.total_minutes() << " minutes\n";24 return 0;25}
What you learned
- A function written inside a
structorclassis a member function. Call it withobject.function(); it uses that object's member variables directly, and each object has its own copy of them. - Anyone can use what's under
public:; only the class's own member functions can use what's underprivate:. Using a private member from outside is a compile error (is private within this context). - Make the data
privateso it changes only through member functions; then the rules (no negative balance, nothing below absolute zero) live in one place. - Put
constafter the parentheses of member functions that only read; aconstobject orconstreference can call only those. structandclassdiffer only in default access:structmembers are public by default,classmembers private.- This course adds an underscore to private member variable names:
balance_.