Home | Projects | Notes > C++ Programming > Constructors & Destructors
Special member method
Invoked during object creation
Useful for initialization (to a stable state)
Has same name as the class
No return type is specified
Can be overloaded
Player
class
xxxxxxxxxx
121class Player
2{
3private:
4 std::string name;
5 int health;
6 int xp;
7public:
8 // Overloaded constructors
9 Player();
10 Player(std::string name);
11 Player(std::string name, int health, int xp);
12};
Account
class
xxxxxxxxxx
121class Account
2{
3private:
4 std::string name;
5 double balance;
6public:
7 // Constructors
8 Account();
9 Account(std::string name, double balance);
10 Account(std::string name);
11 Account(double balance);
12};
Special member method
Has same name as the class except that it is proceeded with a tilde (~
)
Invoked automatically when an object is destroyed (When an object goes out of its scope or a pointer pointing to an object gets deleted)
It is a great place to release memory, close files and free up any other resources.
No return type and no parameters
Only 1 destructor is allowed per class - CANNOT be overloaded!
Doesn't make sense to overload a destructor since it is called automatically by C++.
Useful to release memory and other resources
Player
class
xxxxxxxxxx
141class Player
2{
3private:
4 std::string name;
5 int health;
6 int xp;
7public:
8 // Overloaded constructors
9 Player();
10 Player(std::string name);
11 Player(std::string name, int health, int xp);
12 // Destructor
13 ~Player();
14};
xxxxxxxxxx
91{
2 // The following 3 objects are all local and get created on stack memory
3 Player jack;
4 Player sunny {"Sunny", 100, 4};
5 Player yena {"Yena"};
6} // 3 destructors called
7
8Player *kj = new Player("KJ", 1000, 0);
9delete enemy; // Destructor called
Account
class
xxxxxxxxxx
141class Account
2{
3private:
4 std::string name;
5 double balance;
6public:
7 // Constructors
8 Account();
9 Account(std::string name, double balance);
10 Account(std::string name);
11 Account(double balance);
12 // Destructor
13 ~Account();
14};
A constructor that does not expect any arguments (Also, called the no-args constructor)
If you do NOT provide any constructors at all for a class, C++ will generate a default constructor that does nothing.
Since the compiler generated default constructor does nothing, the class member attributes could contain garbage value because they haven't been initialized.
Called when you instantiate a new object with no arguments
It's a best practice to provide a user-defined constructor that ensures objects are instantiated in a known, stable (safe) state.
Once the user provides a user-defined constructor for a class, C++ will not generate the no-args default constructor automatically. If you still need it, then you must explicitly define it yourself.
Player
class
xxxxxxxxxx
21Player jack; // Creates an object on the stack
2Palyer *sunny = new Player; // Creates an object on the heap
Account
class
With default constructor
xxxxxxxxxx
151class Account
2{
3private:
4 std::string name;
5 double balance;
6public:
7 // Default constructor
8 Account()
9 {
10 name = "None";
11 balance = 0.0;
12 }
13 bool withdraw(double amount);
14 bool deposit(double amount);
15};
xxxxxxxxxx
51Account jack_account;
2Account yena_account;
3
4Account *sunny_account = new Account;
5delete sunny_account;
With a user-defined constructor (No default constructor)
xxxxxxxxxx
151class Account
2{
3private:
4 std::string name;
5 double balance;
6public:
7 // No default constructor
8 Account(std::string name_val, double bal)
9 {
10 name = name_val;
11 balance bal;
12 }
13 bool withdraw(double amount);
14 bool deposit(double amount);
15};
xxxxxxxxxx
71Account jack_account; // Error
2Account yena_account; // Error
3
4Account *sunny_account = new Account; // Error
5delete sunny_account;
6
7Account kj_account {"KJ", 15000.0}; // OK
Errors because the default constructor is no longer automatically generated by the compiler.
Classes can have as many constructors as necessary
Each must have a unique signature
Default constructor is no longer compiler-generated once another constructor is declared
It is important that we initialize our objects to some state that we know. We don't want garbage data in any of the attributes after they are created.
Player
class
xxxxxxxxxx
331class player
2{
3private:
4 std::string name;
5 int health;
6 int xp;
7public:
8 // Overloaded constructors
9 Player();
10 Player(std::string name_val);
11 Player(std::string name_val, int health_val, int xp_val);
12};
13
14Player::Player()
15{
16 name = "None";
17 health = 0;
18 xp = 0;
19}
20
21Player::Player(std::string name_val)
22{
23 name = name_val;
24 health = 0;
25 xp = 0;
26}
27
28Player::Player(std::string name_val, int health_val, int xp_val)
29{
30 name = name_val;
31 health = health_val;
32 xp = xp_val;
33}
L16: For the record, by the time that we get to this statement, the
name
string object has already been constructed and initialized to an empty string. So we're really just assigning a new string to it, in this case, to an existing object. This is, in reality, different from initialization.
See "Constructor initialization lists" section.
xxxxxxxxxx
61Player empty; // None, 0, 0
2Player hero("Hero"); // Hero, 0, 0
3Player villain("Villain"); // Villain, 0, 0
4Player frank {"Frank", 100, 4}; // Frank, 100, 4
5Player *enemy = new Player("Enemy", 1000, 0); // Enemy, 1000, 0
6delete enemy;
So far, all data member values have been set in the constructor body. While this works, it technically isn't initialization because by the time the constructor body is executed these member attributes have already been created. So, we're really just assigning values to already created attributes.
Constructor initialization lists help initialize the member data values to specified values BEFORE the constructor body executes. (True initialization!)
Constructor initialization lists
Are more efficient
Immediately follows the parameter list
Initializes the data members as the object is created
Order of initialization is the order of their declarations in the class
The order specified in the initialization list does not matter!
Player
class
xxxxxxxxxx
241class player
2{
3private:
4 std::string name;
5 int health;
6 int xp;
7public:
8 // Overloaded constructors
9 Player();
10 Player(std::string name_val);
11 Player(std::string name_val, int health_val, int xp_val);
12};
13
14Player::Player()
15 : name{"None"}, health{0}, sp{0} // Initialization list
16{ }
17
18Player::Player::Player(std::string name_val)()
19 : name{name_val}, health{0}, sp{0} // Initialization list
20{ }
21
22Player::Player(std::string name_val, int health_val, int xp_val)
23 : name{name_val}, health{health_val}, xp{xp_val}; // Initialization list
24{ }
Using constructor initialization lists, we can be sure that our data members have been initialized to our own values before any code in the constructor body is executed.
True initialization!
Often the code for constructors is very similar. Many times only the initialization values of the data members is what chages.
Duplicated code can lead to errors, so we want to minimize the amount of duplicated code in our programs.
C++ allows delegating constructors
Code for one constructor can call another in the initialization list
Avoids duplicating code
Player
class
xxxxxxxxxx
241class player
2{
3private:
4 std::string name;
5 int health;
6 int xp;
7public:
8 // Overloaded constructors
9 Player();
10 Player(std::string name_val);
11 Player(std::string name_val, int health_val, int xp_val);
12};
13
14Player::Player(std::string name_val, int health_val, int xp_val)
15 : name{name_val}, health{health_val}, xp{xp_val}; // Initialization list
16{ }
17
18Player::Player()
19 : Player{"None", 0, 0} // Delegating constructor
20{ }
21
22Player::Player::Player(std::string name_val)()
23 : Player{name_val, 0, 0} // Delegating constructor
24{ }
In this case, the no-args constructor is the delegating constructor, since it delegates object initialization to another constructor.
Can often simplify our code and reduce the number of overloaded constructors by providing default constructor parameters.
Same rules apply as we learned with non-member functions
Player
class
We can simply have one constructor that does everything that we did with the three constructors in the previous example.
xxxxxxxxxx
211class player
2{
3private:
4 std::string name;
5 int health;
6 int xp;
7public:
8 // Constructor with default parameter values
9 Player(std::string name_val = "None",
10 int health_val = 0,
11 int xp_val = 0);
12};
13
14Player::Player(std::string name_val, int health_val, int xp_val)
15 : name{name_val}, health{health_val}, xp{xp_val}
16{ }
17
18Player empty; // None, 0, 0
19Player jack{"Jack"}; // Jack, 0, 0
20Player yena{"Yena", 100, 55}; // Yena, 100, 55
21Player hero{"Hero", 100}; // Hero, 100, 0
Notice that you don't supply the default parameters here. You can simply use an initializer list to initialize the class attributes, and the compiler will take care of providing the default values as needed.
This is very handy and results in less code to write, which means less code to test, which means less code that could have errors.
Since the single constructor is doing the job of several constructors, we have to be careful not to declare ambiguous constructors. For example, what would happen if you declared a no-args constructor? The compiler will generate an error since it doesn't know which one to call if no arguments were provided when creating an object.
When objects are copied C++ must create a new object from an existing object, and it uses "copy constructor" to do this. (Copy constructors are called whenever the compiler needs to make a copy of an object.)
When is a copy of an object made?
Passing object by value as a parameter
Returning an object from a function by value
Constructing one object based on another of the same class
C++ must have a way of accomplishing this so it provides a compiler-defined copy constructor if you don't. (If you don't provide your own way of copying objects by value then the compiler provides a default way of copying objects.)
Copies the values of each data member to the new object
If you do not provide a user-defined copy constructor, C++ will generate a default copy constructor that will do member-wise copy.
Perfectly fine in many cases, but beware if you have a pointer data member,the pointer will be copied not what it is pointing to.
Shallow copy vs. Deep copy
Best practices
Provide a copy constructor when your class has raw pointer members
Provide the copy constructor with a const reference parameter
Use STL classes as member attributes as they already provide copy constructors
Avoid using raw pointer data members if possible or use smart pointers.
Copy constructor signature
xxxxxxxxxx
41Type::Type(const Type &source);
2
3Player::Player(const Player &source);
4Account::Account(const Account &source);
Should pass the source object by constant reference!
Why pass by reference?
If we pass it in by value, then the passed object will get copied along the way, which is the whole point of the copy constructor. We would defeat the purpose and end up with never ending recursive calls. Why constant?
Since we are copying the source without modifying it.
Pass object by value
xxxxxxxxxx
101Player hero{"Hero", 100, 20};
2
3void display_player(Player p)
4{
5 // p is a COPY of hero in this example (copy constructor does the copy)
6 // Use p
7 // Destructor for p will be called
8}
9
10display_player(hero);
Return object by value
xxxxxxxxxx
91Player enemy;
2
3Player create_super_enemy()
4{
5 Player an_enemy{"Super Enemy", 100, 100};
6 return an_enemy; // A COPY of an_enemy is returned (copy constructor does the copy)
7}
8
9enemy = create_super_enemy();
Here, a copy of
an_enemy
object is made by the copy constructor.
Construct one object based on another
xxxxxxxxxx
21Player hero{"Hero", 100, 100};
2Player another_hero{hero}; // A COPY of hero is made using the copy constructor
Declaring and implementing a copy constructor
xxxxxxxxxx
81// Declaration
2Type::Type(const Type &source);
3
4// Implementation
5Type::Type(const Type &source)
6{
7 // Code or initialization list to copy the object
8}
xxxxxxxxxx
121// Declaration
2Player::Player(const Player &source);
3Account::Account(const Account &source);
4
5// Implementation
6Player::Player(const Player &source)
7 : name{source.name}, health{source.health}, xp{source.xp}
8{ }
9
10Account::Account(const Account &source)
11 : name{source.name}, balance{source.balance}
12{ }
One of C++11's most pervasive features was probably move semantics. And, to really understand move semantics, it's important to be able to tell whether an expression is an l-value or an r-value.
Rule of thumb:
L-value
When you can refer to an object by name or you can follow a pointer to get to an object, then that object is addressable and it's an l-value.
R-value
Everything else
In context of move semantics r-values refer to temporary objects that are created by the compiler, and objects returned from methods.
Sometimes when we execute code the compiler creates unnamed temporary values:
xxxxxxxxxx
21int total{0};
2total = 100 + 200;
100 + 200
is evaluated and300
stored in an unnamedtemp
value (This value is not addressable so it's an r-value.)The
300
(r-value) is then stored in the variabletotal
(l-value).Then the
temp
value is discarded
The same happens with objects as well. However, with objects there can be a great amount of overhead if copy constructors are called over and over again making copies of these temporary objects. And, when we have raw pointers and we have to do deep copies then the overhead is even greater.
This is where move semantics and the move constructor comes into picture.
Sometimes copy constructors are called many times automatically due to the copy semantics of C++.
Copy constructors doing deep copying can have a significant performance bottleneck.
Deep copy must be implemented if a class contains a raw poiner.
This is computationally expensive since we have to allocate space for the copy and then copy the data over.
C++11 introduced move semantics and the move constructor (Extremely efficient!)
Move constructor moves an object rather than copy it.
Move constructors are optional but recommended when you have a raw pointer.
If you don't provide them, then the copy constructors will be called.
Copy elision - C++ may optimize copying away completely (RVO - Return Value Optimization)
You may not even see the copy constructors being called. If you experience this, it's probably due to something called copy elision.
Copy elision is a compiler optimization technique that eliminates unnecessary copying. Compilers are really smart with their optimizations now. And one of the common techniques is called "Return Value Optimization". That's when the compiler generates code that doesn't create a copy every return value from a function making the code much more efficient.
Instead of making a deep copy of the resource
It simply moves the resource on the heap
Simply copies the address of the resource from source to the current object
And, nulls out the pointer in the source pointer.
Very efficient since not doing a copy at all.
In the context of move semantics, think of r-value references as references to temporary objects.
Used in moving semantics and perfect forwarding
Move semantics is all about r-value references
Used by move constructor and move assignment operator to efficiently move an object rather than copy it.
R-value reference operator (&&
)
This is unlike the l-value reference operator (&
)
R-value references
xxxxxxxxxx
51// Syntax
2Type::Type(Type &&source);
3
4Player::Player(Player &&source);
5Move::Move(Move &&source);
Difference from copy constructors?
There's no
const
qualifier for the parameter source because we need to modify it in order to null out its pointer.Parameter is an r-value reference
xxxxxxxxxx
91int x{100};
2
3int &l_ref = x; // L-value reference (since 'x' is an l-value, addressable, got a name)
4l_ref = 10; // Change x to 10
5
6int &&r_ref = 200; // R-value reference
7r_ref = 300; // Change r_ref to 300 (just changed the temporary variable value!)
8
9int &&x_ref = x; // Compiler error (Cannot assign an l-value to an r-value reference)
R-value reference parameters
xxxxxxxxxx
81int x{100}; // x is an l-value
2
3void func(int &&num); // B
4
5func(200); // Calls B (200 is an r-value)
6func(x); // Error (x is an l-value)
7 // - Cannot bind r-value reference of type 'int&&' to
8 // an l-value of type 'int'
L-value reference parameters
xxxxxxxxxx
81int x{100}; // x is an l-value
2
3void func(int &num); // A
4
5func(x); // Calls A (x is an l-value)
6func(200); // Error (200 is an r-value)
7 // - Cannot bind non-const l-value reference of type 'int&' to
8 // an r-value of type 'int'
L-value and r-value reference parameters
xxxxxxxxxx
71int x{100}; // x is an l-value
2
3void func(int &num); // A
4void func(int &&num); // B
5
6func(x); // Calls A (x is an l-value)
7func(200); // Calls B (200 is an r-value)
Move
class WITHOUT move constructor
xxxxxxxxxx
181class Move
2{
3private:
4 int *data;
5public:
6 void set_data_value(int d) { *data = d; }
7 int get_data_value() { return *data }
8 Move(int d); // Constructor
9 Move(const Move &source); // Copy constructor
10 ~Move(); // Destructor
11};
12
13// Copy constructor (Deep copy)
14Move::Move(const Move &source)
15{
16 data = new int;
17 *data = *source.data;
18}
The move constructor 'steals' the data and nulls out the source pointer.
Inefficient copying (Copy constructors will be called to copy the temps)
xxxxxxxxxx
41vector<Move> vec;
2
3vec.push_back(Move{10});
4vec.push_back(Move{20});
Move{10}
andMove{20}
create temporary objects which are unnamed and r-values. The compiler is going to use the copy constructor to make copies of these.Sample output:
xxxxxxxxxx
111Constructor for: 10
2Constructor for: 10
3Copy constructor (deep copy) for: 10
4Destructor freeing data for: 10
5Constructor for: 20
6Constructor for: 20
7Copy constructor (deep copy) for: 20
8Constructor for: 10
9Copy constructor (deep copy) for: 10
10Destructor freeing data for: 10
11Destructor freeing data for: 20
Copy constructor does several deep copies, which is very inefficient.
Move
class WITH move constructor
xxxxxxxxxx
261class Move
2{
3private:
4 int *data;
5public:
6 void set_data_value(int d) { *data = d; }
7 int get_data_value() { return *data }
8 Move(int d); // Constructor
9 Move(const Move &source); // Copy constructor
10 Move(Move &&source); // Move constructor
11 ~Move(); // Destructor
12};
13
14// Copy constructor (Deep copy)
15Move::Move(const Move &source)
16{
17 data = new int;
18 *data = *source.data;
19}
20
21// Move constructor
22Move::Move(Move &&source)
23 : data{source.data}
24{
25 source.data = nullptr; // Null out source's pointer
26}
Efficient copying (Move constructors will be called for the temp r-values)
xxxxxxxxxx
41vector<Move> vec;
2
3vec.push_back(Move{10});
4vec.push_back(Move{20});
xxxxxxxxxx
101Constructor for: 10
2Move constructor (moving resource): 10
3Destructor freeing data for nullptr
4Constructor for: 20
5Move constructor (moving resource): 20
6Move constructor (moving resource): 10
7Destructor freeing data for nullptr
8Destructor freeing data for nullptr
9Destructor freeing data for: 10
10Destructor freeing data for: 20
No copy constructor is called at all. Instead, move constructor is being called.
Also, note that the destructors are called for
nullptr
. That's destroying the object that we just moved and set its data pointer tonullptr
.