Classes
Objects in C++ are data structures with state and member functions. We can think of a class
as a blueprint to define the structure and behaviour of an instantiated object.
Table of Contents
- Difference From Structs
- Defining a Class
- Initializing Class Members
- Constructors
- Member Initializers
- Default Constructors
- Overloaded Constructors
- Defining Member Functions
- Fetching and Modifying Class Members
- Public vs Private vs Protected Access
- Destructors
- RAII - Resource Acquisition is Initialization
- Static Members Variables and Functions
- Const Objects
- Const Member Functions
- This Pointer
- Header Class Definition and CPP Class Implementations
- Object Oriented Inheritance
- Simple Inheritance
- Polymorphism To Be Continued
- Further Reading
Difference From Structs
Let’s start by reiterating what was said at the end of the struct
section:
The only difference between a struct
and a class
is the default for member visibility:
struct
members are public by default.class
members are private by default.
💡 Best Practice:
Use struct
for data-only structures and class
for objects with data and behaviours.
Defining a Class
Let’s start to define a class to represent a calendar date:
class Date {
int year;
int month;
int day;
};
⚡ Warning:
Don’t forget the semicolon after the closing curly brace.
Initializing Class Members
Because the member variables are private we cannot (yet) use an initializer list to create an object of type Date
:
Date birthday{ 1977, 9, 27 }; // Error! No matching constructor.
We can, however, give the members default values:
class Date {
int year{ 1 };
int month{ 1 };
int day{ 1 };
};
// Later in the program:
Date birthday{}; // Ok: Internally day, month, year all set to 1.
🎵 Note:
Classes without constructors are provided zero-argument default constructors.
Constructors
In order to initialize private member variables when constructing an object we need one (or more) special functions called constructors.
Constructors are named after the class and have no return type:
class Date {
int year{ 1 };
int month{ 1 };
int day{ 1 };
public:
Date(int y, int m, int d)
{
year = y;
month = m;
day = d;
}
🎵 Note:
Class members are private by default. We use public:
to allow access to the constructor.
Member Initializers
A more modern way to define the constructor would be to use member initializers to initialize our three member variables.
class Date {
int year;
int month;
int day;
public:
Date(int y, int m, int d)
: year{y}, month{m}, day{d} // member initializers.
{
// Constructor body empty as member initializers did all the work.
}
};
// Later in the program:
Date birthday{ 1977, 9, 27 }; // Works now!
Default Constructors
With our new constructor in place our class will no longer have an implicit default constructor:
Date birthday{}; // Error! No matching constructor.
This can be fixed by requesting a default constructor:
class Date {
int year; // Uninitialized properties are set to 0.
int month;
int day;
public:
Date(int y, int m, int d)
: year{y}, month{m}, day{d}
{ }
Date() = default;
};
// Later in the program:
Date birthday{}; // Works again! But all properties default to zero if uninitialized.
Overloaded Constructors
Just like other functions, contructors can be overloaded:
class Date {
// Let's put back in the defaults through initialization.
int year{ 1 };
int month{ 1 };
int day{ 1 };
public:
Date(int y, int m, int d) // Three argument constructor.
: year{y}, month{m}, day{d}
{ }
Date(int y) // Year only, one argument constructor.
: year{y}
{ }
Date() = default;
};
// Later in the program:
Date defaultDate{}; // Internally: year = 1, month = 1, day = 1
Date JanFirstInTheFuture{ 3000 }; // Internally: year = 3000, month = 1, day = 1
Date SeptSecondInTheFuture{ 300, 9, 2 }; // Internally: year = 3000, month = 9, day = 2
Defining Member Functions
We can add other functions to the class that use the private member variables.
Here’s the Date
class again, but simplified to have only one constructor. A public
debugging function has been added:
class Date {
int year{ 1 };
int month{ 1 };
int day{ 1 };
public:
Date(int y, int m, int d)
: year{y}, month{m}, day{d}
{ }
void debugPrint() {
std::cout << "Y:" << year << " M:" << month << " D:" << day << "\n";
}
};
// Later in the program:
Date birthday{ 1977, 9, 27 };
birthday.debugPrint(); // Outputs: Y:1977 M:9 D:27
Fetching and Modifying Class Members
If you need to break encapsulation and provide getters and setters for private members variables, it’s common to prefix the member names with a lowercase m
or m_
:
class Date {
int mYear{1};
int mMonth{1};
int mDay{1};
public:
Date(int y, int m, int d)
: mYear{y}, mMonth{m}, mDay{d}
{ }
int year() { // Trivial Getter
return mYear;
}
void year(int y) { // Trivial Setter
mYear = y;
}
void debugPrint() {
std::cout << "Y:" << mYear << " M:" << mMonth << " D:" << mDay << "\n";
}
};
// Later in the code:
Date birthday{ 1, 9, 1977 };
std::cout << birthday.year() << "\n"; // Outputs: 1
birthday.year(1977);
birthday.debugPrint(); // Outputs: Y:1977 M:9 D:27
💡 Best Practice:
The C++ Code Guidelines warn against “trivial getters and setters”.
Public vs Private vs Protected Access
We’ve seen how the public:
access specifier can make member functions accessible from outside the class. There are actually three access specifiers we can use:
public:
- Members are accessible from outside the class.private:
- Members cannot be accessed from outside the class. (Default)protected:
- Members cannot be accessed from the outside, but can be accessed in inherited classes.
Destructors
Class destructors are called when an object is destroyed. This usually happens when a variable goes out of scope. Destructors are named after the class with a tilde ~
prefix. They take no arguments and have no return type.
Here’s a class with a constructor and destructor that announce their own execution:
#include <iostream>
#include <string>
class LoudMouth {
public:
LoudMouth(std::string n)
: name{n} {
std::cout << " 📣 Constructor: " << name << "\n";
}
~LoudMouth() {
std::cout << " 📣 Destructor: " << name << "\n";
}
private:
std::string name;
};
int main() {
std::cout << "Program main() begins.\n";
LoudMouth wally{ "Wally" };
if (1 == 1) { // Useless test to create a nested scope.
std::cout << " If scope begins.\n ";
LoudMouth daisy{ "Daisy" };
std::cout << " If scope ends.\n ";
}
std::cout << "Program main() ends.\n";
}
RAII - Resource Acquisition is Initialization
Constructors and destructors allow for the popular resource management technique RAII, where the life-cycles of resources (like memory, network sockets, open files, etc) are bound to the lifetime of specific objects.
Said another way, with RAII resources are obtained within class constructors and released in class destructors. This way, resource cleanup is automatically handled when objects go out of scope.
A better name for this technique is Scope-Bound Resource Management (SBRM), but RAII is the term everyone seems to use.
⏳ Wait For It:
We’ll see RAII in action when we get to the pointers and objects module.
Static Members Variables and Functions
Members and functions can be made to belong to the class (rather than to an instance of the class) using the static
keyword.
#include <iostream>
class Widget {
public:
Widget() {
mId = ++sWidgetsMade;
}
static int numberOfWidgetsMade() {
return sWidgetsMade;
}
int id() {
return mId;
}
private:
static int sWidgetsMade;
int mId;
};
// Non-const static members must be initialized outside the class.
int Widget::sWidgetsMade{ 0 };
int main() {
std::cout << "Current number of Widgets: " << Widget::numberOfWidgetsMade() << "\n";
Widget A;
Widget B;
std::cout << "Current number of Widgets: " << Widget::numberOfWidgetsMade() << "\n";
std::cout << "The id of widget A: " << A.id() << "\n";
std::cout << "The id of widget B: " << B.id() << "\n";
}
Const Objects
Recall that we can mark variables as const
to have the compiler ensure they are never modified once initialized.
Objects can also be made const
, which will ensure that none of their member variables can change. Let’s return to our example Date
class:
#include <iostream>
class Date {
int mYear;
int mMonth;
int mDay;
public:
Date(int y, int m, int d)
: mYear{y}, mMonth{m}, mDay{d}
{ }
int year() const { // Trivial Getter. Function marked as const.
return mYear;
}
void year(int y) { // Trivial Setter.
mYear = y;
}
void debugPrint() const { // Function marked as const.
std::cout << "Y:" << mYear << " M:" << mMonth << " D:" << mDay << "\n";
}
};
int main() {
Date mutableDate{ 2021, 01, 01 };
const Date immutableDate{ 2021, 12, 31 };
mutableDate.debugPrint();
immutableDate.debugPrint();
// Attempt to change member variables via setter:
mutableDate.year(400); // Okay.
immutableDate.year(400); // Compiler error.
}
🎵 Note:
Public member variables of a const
object also cannot be changed.
Const Member Functions
Class member functions that do not change the state of any member variables can also be marked as const
. When marking a function as const
the keyword comes after the function signature.
If you look at the Date
code in the previous section you’ll see that two of the member functions are marked as const
. When an object is marked as const
, we can only call const
member functions on that object. Try removing the const
from the debugPrint
function and you’ll see that the compiler will prevent you from calling this function on the immutableDate
object.
This Pointer
Like other object oriented languages, C++ objects have a hidden this
variable to provide an internal self-reference to the object.
You will rarely (if ever) need to make use of this
directly, although you might see legacy code using this
to disambiguate class member variables from method parameters of the same name:
class Widget {
public:
Widget(int id) {
this->id = id; // this->id is the member, while id is the parameter.
}
private:
int id;
}
🎵 Note:
The ->
operator is used to reference members via this
.
Header Class Definition and CPP Class Implementations
Up to this point we’ve been defining the class and its implementation within the main.cpp
but it’s more common to define a class within a .h
header file, with its implementation defined in a separate .cpp
file.
When defining methods in the .cpp
file we must prefix the method name with Classname::
where Classname
is the actual name of the class.
date.h
:
class Date {
int year;
int month;
int day;
public:
Date(int y, int m, int d);
void debugPrint();
};
date.cpp
:
#include <iostream>
#include "date.h"
// The implementation of the Date class methods:
Date::Date(int y, int m, int d)
: year{y}, month{m}, day{d}
{ }
void Date::debugPrint() {
std::cout << "Y:" << year << " M:" << month << " D:" << day << "\n";
}
main.cpp
:
#include <iostream>
#include "date.h"
int main() {
Date halloween{2021, 10, 31};
halloween.debugPrint();
}
🚀 Run and modify this Date class example on Compiler Explorer!
🎵 Note:
Some folks like to use .hpp
for C++ header files to distinguish them from C’s .h
files.
Object Oriented Inheritance
In object-oriented programming, we can create classes that inherit members from existing classes. In C++ we say that the derived class inherits from a base class. In other programming languages we might say that a child class (or sub class) inherits from a parent class (or super class).
Imagine a Character
class which is the base class of two derived classes, PlayerCharacter
and AICharacter
.
PlayerCharacter hero{"Daisy Glutton"};
AICharacter helper{"Wally Glutton"};
Inheritance describes an is-a relationship:
- The
hero
object is-aPlayerCharacter
and it also is-aCharacter
. - The
helper
object is-aAICharacter
and it also is-aCharacter
.
Through this relationship, the derived class inherits access to the public
and protected
members of the base class.
Simple Inheritance
Let’s do a quick overview of the basics of C++ inheritance with an example of a Student
class derived from a Person
class.
#include <iostream>
#include <string>
class Person {
public:
Person(std::string name, int age) // Person constructor
: mName{name}, mAge{age} // Member variable initializers
{ }
// Defined in the base class only but available to derived class.
void greetings() {
std::cout << "Hello, my name is " + mName + ".\n";
}
// Defined in both the base class and the derived class.
std::string debugMessage() {
return "name: " + mName + " age: " + std::to_string(mAge);
}
protected: // Protected members will be accessible to the derived class.
std::string mName;
private: // Private members will not be accessible to the derived class.
int mAge;
};
class Student : public Person { // Student inherits from Person
public:
Student(std::string name, int age, double gpa) // Student constructor
: Person{name, age}, // Base class constructor call
mGpa{gpa} // Member initializer
{ }
// Redefined function which uses base class version of function.
std::string debugMessage() {
return Person::debugMessage() + " GPA: " + std::to_string(mGpa);
}
void writeCode() { // Defined only in the derived class.
std::cout << mName << " sez: All this coding is making me thirsty!\n";
}
private: // Derived class private members.
double mGpa;
};
int main() {
Person coda{ "Coda Forth", 25 };
Student timbre{ "Timbre Dalfoor", 43, 3.9 };
// The coda object is-a Person, so we can call Person::greetings().
coda.greetings();
// timber is-a Student *and* a Person, so Person::greetings() works too.
timbre.greetings();
// The timber oject is-a Student, so we can call Student::writeCode().
timbre.writeCode();
// We cannot call Student::writeCode() on coda:
// coda.writeCode(); // Compiler Error: No member named 'writeCode`.
// Calling .debugMessage() on coda runs Person::debugMessage().
std::cout << coda.debugMessage() << "\n";
// Calling .debugMessage() on coda runs Student::debugMessage().
std::cout << timbre.debugMessage() << "\n";
}
💡 Best Practice:
See a version of this code with classes defined and implemented in .h
and .cpp
files.
Polymorphism To Be Continued
⏳ Wait For It:
Subtype polymorphism is covered in our pointers and polymorphism module.