16. Essential Class Operations#
In C++, classes are a fundamental building block of object-oriented programming. When creating a class, several essential operations exist that you should understand and implement correctly to ensure proper memory management and object behavior. These operations are collectively known as the “Rule of Five,” which refers to the following five special member functions:
Default Constructor
Destructor
Copy Constructor
Copy Assignment Operator
Move Constructor and Move Assignment Operator (since C++11)
If you define any of these special member functions in your class, it’s generally recommended to define all five of them to ensure consistent behavior and prevent potential resource leaks or undefined behavior.
16.1. Default Constructor#
The default constructor is a constructor that takes no arguments. It is automatically generated by the compiler if you don’t provide any other constructor. However, it’s a good practice to define it explicitly, especially if your class has non-static member variables that require initialization.
class MyClass {
public:
MyClass() {
// Initialize member variables here
}
// ...
};
16.2. Destructor#
The destructor is a special member function that is automatically called when an object is destroyed or goes out of scope. It is responsible for deallocating any dynamically allocated memory and performing any necessary cleanup operations.
class MyClass {
public:
~MyClass() {
// Deallocate dynamic memory
// Perform cleanup operations
}
// ...
};
16.3. Copy Constructor#
The copy constructor is a constructor that creates a new object by copying the contents of an existing object. It is automatically generated by the compiler if you don’t provide one, but it’s generally a good practice to define your own copy constructor, especially if your class has dynamically allocated memory or non-trivial member objects.
class MyClass {
public:
MyClass(const MyClass& other) {
// Copy member variables from other
// Deep copy if necessary
}
// ...
};
TODO: Explain Deep vs Shallow Copy again
16.4. Copy Assignment Operator#
The copy assignment operator (operator=) is a member function that copies the contents of one object to another existing object of the same class. Like the copy constructor, it is automatically generated by the compiler if you don’t provide one, but you should define your own copy assignment operator if your class has dynamically allocated memory or non-trivial member objects.
class MyClass {
public:
MyClass& operator=(const MyClass& other) {
// Copy member variables from other
// Handle self-assignment
// Deep copy if necessary
return *this;
}
// ...
};
16.5. Move Constructor and Move Assignment Operator (C++11)#
The move constructor is a constructor that creates a new object by transferring (moving) the resources from a temporary object (rvalue reference) to the newly created object. The move assignment operator (operator=) is a member function that transfers (moves) the resources from an rvalue reference object to the current object. These operations are more efficient than copying, as they avoid unnecessary memory allocations and deallocations. They were introduced in C++11 to support move semantics and improve performance.
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// Move member variables from other
// Handle transfer of ownership
}
MyClass& operator=(MyClass&& other) noexcept {
// Move member variables from other
// Handle self-assignment
// Handle transfer of ownership
return *this;
}
// ...
};
16.6. RAII#
RAII (Resource Acquisition Is Initialization) is a fundamental concept in C++ that ties resource management to the lifetime of objects. It ensures that resources (such as memory, file handles, or network connections) are acquired during object construction and released automatically when the object goes out of scope or is destroyed.
RAII is closely related to the Rule of Five. When you define any of the special member functions (constructor, destructor, copy constructor, copy assignment operator, or move constructor/assignment operator), you are effectively managing resources within your class. By adhering to the Rule of Five, you ensure that resources are properly acquired, managed, and released throughout the object’s lifetime, regardless of how the object is created, copied, moved, or destroyed.
The followig example of a bank account class shows both RAII and the rule of five.
1//filename: bank_account.cpp
2//complile: g++ -std=c++17 -o bank_account bank_account.cpp
3//execute: ./bank_account
4
5#include <iostream>
6#include <string>
7
8class BankAccount {
9public:
10 // Default constructor
11 BankAccount() : _balance(0.0), _accountNumber("") {
12 std::cout << "Default constructor called" << std::endl;
13 }
14
15 // Parameterized constructor
16 BankAccount(double initialBalance, const std::string& accountNumber)
17 : _balance(initialBalance), _accountNumber(accountNumber) {
18 std::cout << "Parameterized constructor called" << std::endl;
19 }
20
21 // Copy constructor
22 BankAccount(const BankAccount& other)
23 : _balance(other._balance), _accountNumber(other._accountNumber) {
24 std::cout << "Copy constructor called" << std::endl;
25 }
26
27 // Copy assignment operator
28 BankAccount& operator=(const BankAccount& other) {
29 std::cout << "Copy assignment operator called" << std::endl;
30 if (this != &other) {
31 _balance = other._balance;
32 _accountNumber = other._accountNumber;
33 }
34 return *this;
35 }
36
37 // Move constructor
38 BankAccount(BankAccount&& other) noexcept
39 : _balance(std::move(other._balance)), _accountNumber(std::move(other._accountNumber)) {
40 std::cout << "Move constructor called" << std::endl;
41 other._balance = 0.0;
42 other._accountNumber = "";
43 }
44
45 // Move assignment operator
46 BankAccount& operator=(BankAccount&& other) noexcept {
47 std::cout << "Move assignment operator called" << std::endl;
48 if (this != &other) {
49 _balance = std::move(other._balance);
50 _accountNumber = std::move(other._accountNumber);
51 other._balance = 0.0;
52 other._accountNumber = "";
53 }
54 return *this;
55 }
56
57 // Destructor
58 ~BankAccount() {
59 std::cout << "Destructor called for account: " << _accountNumber << std::endl;
60 // Release any resources acquired, if necessary
61 }
62
63 double getBalance() const { return _balance; }
64 std::string getAccountNumber() const { return _accountNumber; }
65
66private:
67 double _balance;
68 std::string _accountNumber;
69};
16.7. RAII and Scope#
The interaction between RAII and scope in C++ is crucial for managing resources efficiently and reliably.
Resource Acquisition: When an object is created, its constructor is called. This constructor can acquire the necessary resources, such as dynamically allocated memory or opening a file.
Object Lifetime: The object and the resources it holds remain valid until the end of its scope. This means that within the scope where the object is defined, you can safely use the acquired resources knowing they are available.
Scope Exit: When the object goes out of scope, either due to reaching the end of a block or by an explicit delete operation, its destructor is automatically called. The destructor releases the acquired resources, ensuring they are properly deallocated or closed - i.e., the resource cleanup occurs.
1//filename: file_handler.cpp
2//complile: g++ -std=c++17 -o file_handler file_handler.cpp
3//execute: ./file_handler
4//After execution (to view the created file): cat example.txt
5#include <iostream>
6#include <fstream>
7
8class FileHandler {
9private:
10 std::ofstream file; // File stream for resource management
11
12public:
13 FileHandler(const std::string& filename) : file(filename) {
14 if (!file.is_open()) {
15 throw std::runtime_error("Failed to open file");
16 }
17 std::cout << "File opened successfully!\n";
18 }
19
20 ~FileHandler() {
21 file.close(); // Resource cleanup
22 std::cout << "File closed\n";
23 }
24
25 void write(const std::string& data) {
26 file << data;
27 }
28};
29
30int main() {
31 try {
32 FileHandler handler("example.txt"); // Object created, file opened
33
34 handler.write("Hello, RAII!\n"); // Using the file resource
35
36 // File automatically closed when 'handler' goes out of scope at the end of main()
37 } catch (const std::exception& e) {
38 std::cerr << "Error: " << e.what() << std::endl;
39 return 1;
40 }
41
42 return 0;
43}
FileHandler class manages the file resource. When handler object is created in the main() function, the file is opened. The file resource is automatically closed when handler goes out of scope at the end of main() function, regardless of how the block is exited (normally or due to an exception).