Default Assignment Operator and References in C++
Introduction
In C++, the default assignment operator is used to copy one object to another object of the same class. When a programmer does not define a custom assignment operator, the default assignment operator is generated by the compiler. However, the default assignment operator behaves differently when it comes to objects that contain references. This article will explore how the default assignment operator behaves with references and why it is important to define a custom assignment operator when working with objects that contain references.
The Default Assignment Operator
The default assignment operator is a special member function that is used to copy one object to another object of the same class. It is generated by the compiler if the programmer does not define a custom assignment operator. Here is an example of a class that contains a default assignment operator:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int val) : _val(val) {}
int getVal() { return _val; }
private:
int _val;
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1;
cout << obj1.getVal() << endl; // prints 10
cout << obj2.getVal() << endl; // prints 10
return 0;
}
Explanation:
- This program defines a MyClass class with a private member variable _val and a public member function getVal() that returns the value of _val.
- The class has a constructor that takes an integer argument and sets the value of _val to the value of the argument.
- In the main() function, two objects of the MyClass class, obj1 and obj2, are created using the constructor with the arguments 10 and 20, respectively.
- Then, the line obj2 = obj1; assigns the value of obj1 to obj2. This creates a copy of obj1 and assigns it to obj2, which means that obj2 now has the same value of _val as obj1.
- Finally, the program prints the value of _val for both objects using the getVal() function. Since obj2 was assigned the value of obj1, the value of _val for both objects is the same, which is 10, and this is what gets printed to the console.
Program Output:
References in C++
References are similar to pointers, but they are much safer to use because they cannot be null, and they do not need to be dereferenced. Instead, they refer to an existing object in memory. When an object contains a reference, the default assignment operator behaves differently than it would for a normal object.
Here is an example of a class that contains a reference:
#include <iostream>
using namespace std;
class BankAccount {
public:
BankAccount(int balance) : _balance(balance) {}
int getBalance() const { return _balance; }
private:
int _balance;
};
class Person {
public:
Person(const string& name, BankAccount& account) : _name(name), _account(account) {}
const string& getName() const { return _name; }
const BankAccount& getAccount() const { return _account; }
private:
string _name;
BankAccount& _account;
};
int main() {
BankAccount account(1000);
Person person("Alice", account);
cout << person.getName() << " has " << person.getAccount().getBalance() << " in their account." << endl;
return 0;
}
Explanation:
- This program defines two classes, BankAccount and Person, and creates an instance of each class in main().
- The BankAccount class represents a simple bank account with a balance, and the Person class represents a person with a name and a reference to a bank account.
- The BankAccount class has a single private member variable _balance, which stores the current balance of the account. The constructor of BankAccount takes an integer parameter balance, which is used to initialize the _balance variable. The getBalance() function returns the current balance of the account.
- The Person class has two private member variables: _name, which stores the name of the person, and _account, which is a reference to a BankAccount object.
- The constructor of Person takes a const string& parameter name, which is used to initialize the _name variable, and a BankAccount& parameter account, which is used to initialize the _account variable.
- The getName() function returns the name of the person, and the getAccount() function returns a constant reference to the bank account.
- In main(), the program creates a BankAccount object with a balance of 1000 using the BankAccount constructor.
- Then, it creates a Person object named "Alice" with a reference to the bank account using the Person constructor.
- Finally, it prints Alice's name and the balance in her account using the getName() and getAccount() functions.
Program Output:
Dangling References
This behaviour can be problematic in certain situations. For example, if we were to delete the integer that obj1 refers to, obj2 would still contain a reference to the deleted integer. This is known as a dangling reference, and it can cause undefined behaviour.
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int& ref) : _ref(ref) {}
int& getRef() { return _ref; }
private:
int& _ref;
};
int main() {
int* ptr = new int(10);
MyClass obj1(*ptr);
MyClass obj2(*ptr);
delete ptr;
cout << obj1.getRef() << endl; // prints garbage value
cout << obj2.getRef() << endl; // prints garbage value
return 0;
}
Explanation:
- This program creates a class named MyClass that takes a reference to an int as its constructor argument. The class also has a member function named getRef() that returns the reference to the integer passed in the constructor.
- In the main() function, a dynamic integer variable is created using the new keyword and initialized with the value 10. Then, two objects of MyClass are created, obj1 and obj2, each with a reference to the dynamically allocated integer pointed by ptr.
- After that, the program deletes the dynamically allocated integer using the delete keyword. This causes the memory that was previously allocated to ptr to be freed, and the integer value it pointed to is no longer valid.
- Then the program attempts to print the value of the integer to which each object is referring by calling the getRef() member function. However, since the integer to which the references refer to is no longer valid, the output would be garbage values.
Program:
Defining a Custom Assignment Operator:
To avoid problems with references, it is important to define a custom assignment operator when working with objects that contain references. This allows the programmer to explicitly copy the value of the reference rather than copying the reference itself.
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int& ref) : _ref(ref) {}
MyClass& operator=(const MyClass& other) {
if (this != &other) {
_ref = other._ref;
}
return *this;
}
int& getRef() { return _ref; }
private:
int& _ref;
};
int main() {
int num1 = 10;
int num2 = 20;
MyClass obj1(num1);
MyClass obj2(num2);
obj2 = obj1;
cout << obj1.getRef() << endl; // prints 10
cout << obj2.getRef() << endl; // prints 10
return 0;
}
Explanation:
- This program creates a class named MyClass that takes a reference to an int as its constructor argument. The class also has a member function named getRef() that returns the reference to the integer passed in the constructor.
- In the main() function, two int variables named num1 and num2 are created with the values 10 and 20, respectively.
- Two objects of MyClass are then created, obj1 with a reference to num1 and obj2 with a reference to num2.
- The interesting part of the program is the assignment statement obj2 = obj1;. This calls the assignment operator operator= of the MyClass.
- In this operator, the reference _ref of the left-hand side object (obj2) is set to the reference of the right-hand side object (obj1). Therefore, after the assignment, both objects now refer to num1.
- Finally, the program prints the value of the integer to which each object is referring by calling the getRef() member function. As both objects refer to num1 after the assignment statement, both print the value 10.
Program Output:
Conclusion
In conclusion, the default assignment operator behaves differently with references in C++. When a programmer does not define a custom assignment operator, the default assignment operator will copy the reference rather than the value of the reference.
This can lead to problems with dangling references and undefined behaviour. To avoid these problems, it is important to define a custom assignment operator that explicitly copies the value of the reference.
This allows the programmer to control the behaviour of the assignment operator and avoid issues with references.