Introduction
To understand what is the need of object oriented programming, first we need to understand what are the limitations of structural/procedural oriented programming.
- Functions need to know about the structure of the data. If the structure of the data changes, many functions must be changed.
- Suppose we have a program with hundreds of functions and many of those functions accept data with a specific data type as argument.
- If the structure or format for that data structure being passed around changes, then this requires changes in all of the functions written to support the new format of the data.
- This can have a domino effect in the program and the amount of effort needed to change and then test all of the updates would be huge.
- As the programs get larger, they become more:
- Difficult to understand since the number of connections in the program becomes very hard to trace.
- Difficult to maintain.
- Difficult to extend.
- Difficult to debug.
- Difficult to reuse.
- Fragile and easier to break.
Object oriented programming is a paradigm designed parallel to the real world. In structure oriented programming we would mainly focus on the data and the operations performed on the data. Whereas in object oriented programming, the main focus is on the entities to which the data is related to.
Object oriented programming is driven by classes and objects at its rudimentary level. Object oriented programming is all about modelling the software being written in terms of classes and objects. These classes and objects model real world entities in the problem domain being solved. A class is used to define a user defined data type and can be considered as a blueprint for maintaining the data.
The fact that the objects contain data and operations that work on that data is called encapsulation. And it’s an extension of the abstract data type in computer science.Now the data and the operations are together in the class where they belong and not spread across many functions, each passing and receiving data. Encapsulation is another mechanism used by object oriented programming to help us deal with complexity.
We can take the idea of encapsulation even further with information hiding. Object oriented programming allows us to hide implementation-specific logic in a class so that it’s available only within the class. This is a super powerful concept since it allows us to provide a public interface to the class and hide everything else. Now we know that the user of the class can’t mess with the implementation-specific code since they never knew about it in the first place! This makes the code easier to maintain, debug and extend.
Object-oriented programming is also all about reusability. Reusability is defined in terms of using something from one program in another program. Since classes are pretty much encapsulated units of data and operations, reusing them in other applications is much easier and since the class has already been tested, we get that benefit as well. This leads to faster development and better quality software.
Inheritance enables us to generate a new class from an existing class by adding or modifying only the elements required to create the new class. For instance, let’s assume we have an account class that represents a basic bank account, which includes a balance and regular withdrawal and deposit methods. Now, suppose we require a special trust account class with the following business logic: the account type is restricted to three withdrawals per year, and each withdrawal cannot exceed 10% of the current account balance. We could insert this logic into the existing account class and implement an enumeration or another discriminant to differentiate between the account types and execute the appropriate if-else logic. This approach is feasible, but it may become complex when several account variations exist, such as a money market account, a demat account, etc. Each account variation has its unique business logic, not just for withdrawal, but also for deposit. Consequently, the original account class could become an unwieldy and unmanageable behemoth.
In this case, we can create a new class from the existing account class and add the appropriate behaviour in the new class without modifying the original class at all. This approach enables reusability and the creation of polymorphic classes. Since all the derived classes that we produce are accounts, this approach is beneficial.
Differences between C and C++
C | C++ |
Was developed by Dennis Ritchie between the years 1969 and 1973 at AT&T Bell Labs. | Was developed by Bjarne Stroustrup in 1979. |
Strictly structure-oriented / procedural language. | Supports both structure oriented as well as an object-oriented language. |
C is a subset of C++. | C++ is a superset of C. |
Contains 32 keywords. | C++14 contains 90 keywords. |
Function and operator overloading is not supported in C. | Function and operator overloading is supported by C++. |
Functions in C are not defined inside structures. | Functions can be used inside a structure in C++. |
Namespace features are not present. | Namespace is used by C++, which avoids name collisions. |
Reference variables are not supported. | Reference variables are supported. |
Virtual and friend functions are not supported. | Virtual and friend functions are supported. |
Does not support inheritance. | C++ supports inheritance. |
Instead of focusing on data, C focuses on method or process. | C++ focuses on data instead of focusing on method or procedure. |
C provides malloc(), calloc(), realloc() functions for Dynamic memory allocation, and free() for memory deallocation. | C++ provides ‘new’ operator for memory allocation and ‘delete’ operator for memory deallocation. |
Direct support for exception handling is not supported. | Exception handling is supported. |
cin() and cout() are not supported; instead scanf() and printf() functions are used for input/output in C. | Almost all functions in C are supported in C++. |
C structures don’t have access modifiers. | C ++ structures have access modifiers. |
Differences between object oriented and structure oriented programming
Parameter | Object Oriented Programming | Procedural Programming |
Definition | Uses classes and objects to create models based on the real world environment. It makes it easier to maintain and modify existing code as new objects are created inheriting characteristics from existing ones. | Follows a step-by-step approach to break down a task into a collection of variables and routines (or subroutines) through a sequence of instructions. |
Approach | The concept of objects and classes is introduced and hence the program is divided into small chunks called objects which are instances of classes. | The main program is divided into small parts based on the functions. |
Access modifiers | In OOPs access modifiers are introduced namely as Private, Public, and Protected. | No such modifiers are introduced in procedural programming. |
Security | Due to abstraction in OOPs data hiding is possible and hence it is more secure than POP. | Procedural programming is less secure as compared to OOPs. |
Complexity | Due to modularity, are less complex and hence new data objects can be created easily from existing objects making programs easy to modify | There is no simple process to add data in procedural programming, at least not without revising the whole program. |
Program division | OOP divides a program into small parts and these parts are referred to as objects. | Procedural programming divides a program into small programs and each small program is referred to as a function. |
Importance | OOP gives importance to data rather than functions or procedures. | Procedural programming does not give importance to data. In POP, functions along with sequence of actions are followed. |
Inheritance | OOP provides inheritance in three modes i.e. protected, private, and public | Procedural programming does not provide any inheritance. |
Examples | C++, C#, Java, Python, etc. are examples of OOP languages. | C, BASIC, COBOL, Pascal, etc. are examples of POP languages. |
Advantages of object oriented programming:
- Improved software-development productivity: Object-oriented programming is modular, as it provides separation of duties in object-based program development. It is also extensible, as objects can be extended to include new attributes and behaviours. Objects can also be reused within and across applications. Because of these three factors – modularity, extensibility, and reusability – object-oriented programming provides improved software-development productivity over traditional procedure-based programming techniques.
- Improved software maintainability: For the reasons mentioned above, object oriented software is also easier to maintain. Since the design is modular, part of the system can be updated in case of issues without a need to make large-scale changes.
- Faster development: Reuse enables faster development. Object-oriented programming languages come with rich libraries of objects, and code developed during projects is also reusable in future projects.
- Lower cost of development: The reuse of software also lowers the cost of development. Typically, more effort is put into the object-oriented analysis and design, which lowers the overall cost of development.
- Higher-quality software: Faster development of software and lower cost of development allows more time and resources to be used in the verification of the software. Although quality is dependent upon the experience of the teams, object-oriented programming tends to result in higher-quality software.
Limitations of object oriented programming:
- Not a panacea
- Object oriented programming won’t make bad code better. It’s most likely to make it worse.
- Not suitable for all types of problems. There are problems that lend themselves well to functional-programming style, logic-programming style, or procedure-based programming style, and applying object-oriented programming in those situations will not result in efficient programs.
- Not everything decomposes to a class.
- Learning curve
- Usually a steeper learning curve. The thought process involved in object-oriented programming may not be natural for some people, and it can take time to get used to it. It is complex to create programs based on interaction of objects. Some of the key programming techniques, such as inheritance and polymorphism, can be challenging to comprehend initially.
- Many object oriented languages, many variations of object oriented concepts.
- Design
- Usually more up-front design is necessary to create good models and hierarchies.
- Programs can be
- Larger in size
- Slower: Object-oriented programs are typically slower than procedure-based programs, as they typically require more instructions to be executed.
- More complex
Classes and objects
Classes
- Classes are blueprints from which objects are created. Classes are user defined types.
- One of the goals in object oriented programming is to make the user defined types feel like they are part of the programming language. So when we create our classes, we want to be able to use them, just like we use basic data types such as integers, doubles.
- Classes have attributes that are data. And also, classes have functions, which are called methods.
- Classes can hide data and methods that are only used internally by the class. This is done using the private and public access modifiers.
- The goal of a class is to provide a well-defined public interface that the user of the class can easily use to solve their problem.
Objects
- Objects are created from classes and represent a specific instance of the class they are created from. So if I have an account class, I can create Prajwal’s account object that’s a specific instance of an account that models Prajwal’s account information.
- We can have as many objects as we need. If we were to model a real banking application, we could have hundreds of thousands of account objects, each representing an individual instance of an account.
- Each object has its own identity, and each can use the methods defined in the class.
Examples
Let’s assume only for the sake of this example, that int is a class and not a primitive data type.
int max_value;
int min_value;
The above statement tells us about the valid values max_value and min_value can hold. And it also tells us the operations we can perform on those variables such as addition, subtraction etc. As we have assumed int as a class and hence max_value and min_value are objects of this class. Notice that the max_value and min_value are the instances of int. They each have a value and an identity, perhaps a location in the memory.
Let’s consider the following two examples:
Account prajwal_account;
Account chandra_account;
Let’s assume that we’ve already written the Account class. Notice the syntax and how similar it is to the syntax of declaration of integer variables above. Account is a user-defined type. Hence, prajwal_account and chandra_account are the instances/objects of the Account class.
We have been using objects for a long time now. In the following example, marks is an object of class vector and address is an object of class string. vector and string classes provided by the standard C++ library.
std::vector<int> marks;
std::string address;
Class declaration
In C++, you can declare a class using the following syntax:
class ClassName {
// member variables and functions
// access specifier (public, private, protected) for member functions and variables
public:
// public member functions and variables
private:
// private member functions and variables
protected:
// protected member functions and variables
};
The keyword class is used to indicate that a new class is being declared. ClassName is the name of the class you are defining, and the body of the class follows inside the curly braces {}.
You can declare member functions and variables inside the class, and they can be public, private, or protected. The access specifiers control the visibility of the member functions and variables. Public members are accessible from outside the class, private members are only accessible from within the class, and protected members are accessible from within the class and its derived classes.
Here’s an example of a class declaration in C++:
class MyClass {
public:
int public_variable;
void public_function();
private:
int private_variable;
void private_function();
protected:
int protected_variable;
void protected_function();
};
In this example, MyClass is the name of the class being declared. It has three types of member variables and functions, each with its own access specifier. The public variables and functions can be accessed from outside the class, the private variables and functions can only be accessed from within the class, and the protected variables and functions can be accessed from within the class and its derived classes. More on the access specifiers in upcoming sections.
Object creation
To create objects of a class in C++, you need to use the class name followed by the object name and parentheses. The parentheses can either be empty or contain arguments, depending on whether the class has a default constructor or not.
Here’s an example of how to create an object of the MyClass class that we declared earlier:
MyClass my_object; // creates an object named "my_object" of the "MyClass" class
In this example, we’re creating an object of the MyClass class using the default constructor, which doesn’t take any arguments. The object is named my_object.
The object creation syntax heavily depends on whether the corresponding class has an overloaded constructor or not. We shall deal with overloading and constructors in the upcoming sections.
Accessing class members
To access class members (variables or functions) via objects in C++, you use the dot operator (.) to separate the object name and the member name. The syntax for accessing a member variable or function is:
object_name.member_name; // to access a member variable
object_name.member_function(); // to call a member function
Here’s an example of how to access the member variables and functions of the MyClass class using an object:
class MyClass {
public:
int public_variable;
void public_function() {
cout << "Hello from public_function!" << endl;
}
private:
int private_variable;
void private_function() {
cout << "Hello from private_function!" << endl;
}
};
MyClass my_object;
my_object.public_variable = 42; // accessing a public member variable
my_object.public_function(); // calling a public member function
In this example, we’re creating an object named my_object of the MyClass class. We’re then accessing the public member variables public_variable and public_function using the dot operator (.). We set the value of the public_variable to 42, and we call the public_function, which outputs the message “Hello from public_function!” to the console.
Note that you can only access the public members of a class using an object. Private members can only be accessed from within the class itself, not from outside the class.
Class members access specifiers
In C++, access specifiers are keywords used to control the visibility and accessibility of class members (variables and functions) from outside the class. There are three access specifiers in C++: public, private, and protected.
Public Access Specifier:
Members declared as public are accessible from anywhere in the program, both inside and outside the class. Any function or object can access and modify public members of the class. Here’s an example of a class with a public member variable and function:
class MyClass {
public:
int public_variable;
void public_function() {
cout << "This is a public function." << endl;
}
};
In this example, public_variable and public_function are declared as public members of the MyClass class. These members can be accessed from any function or object in the program.
Private Access Specifier:
Members declared as private are only accessible from within the class itself. They cannot be accessed or modified from outside the class. This provides encapsulation, which is an important concept in object-oriented programming.
Here’s an example of a class with a private member variable and function:
class MyClass {
private:
int private_variable;
void private_function() {
cout << "This is a private function." << endl;
}
public:
void set_private_variable(int x) {
private_variable = x;
}
};
In this example, private_variable and private_function are declared as private members of the MyClass class. These members can only be accessed from within the class itself, but the set_private_variable function is declared as a public member, which allows external code to set the value of private_variable.
Protected Access Specifier:
Members declared as protected are accessible from within the class and its derived classes. They cannot be accessed from outside the class or its derived classes.
Here’s an example of a class with a protected member variable and function:
class MyClass {
protected:
int protected_variable;
void protected_function() {
cout << "This is a protected function." << endl;
}
public:
void set_protected_variable(int x) {
protected_variable = x;
}
};
class DerivedClass : public MyClass {
public:
void derived_function() {
protected_variable = 42; // accessing the protected member from a derived class
protected_function(); // accessing the protected function from a derived class
}
};
In this example, protected_variable and protected_function are declared as protected members of the MyClass class. These members can be accessed from within the class and its derived classes, but not from outside the class or its derived classes. The DerivedClass class is derived from MyClass and can access the protected members of MyClass.
To summarise, access specifiers in C++ provide control over the visibility and accessibility of class members. public members are accessible from anywhere in the program, private members are only accessible from within the class, and protected members are accessible from within the class and its derived classes.
Exercises on access specifiers
- Create a class called Rectangle with the following private member variables: length and width. Add public member functions to set and get the values of these member variables.
- Create a class called BankAccount with a private member variable called balance and a public member function called withdraw. The withdraw function should subtract the given amount from the balance variable and return true if the withdrawal was successful, or false if the withdrawal amount is greater than the balance.
- Create a class called Student with a private member variable called name and a public member function called printName. The printName function should print the name of the student to the console.
- Create a class called Animal with a protected member variable called age and a public member function called getAge. The getAge function should return the value of the age variable.
Implementing member methods
In C++, member methods are functions that are defined inside a class and are used to manipulate the class’s data members. They are sometimes referred to as member functions or methods.
To define a member method in C++, you must first declare it in the class definition using the following syntax:
class MyClass {
public:
void myMethod(); // function declaration
};
In this example, myMethod is declared as a public member function of the MyClass class. It does not have any parameters and returns void.
To implement this method, you must define it outside of the class definition using the following syntax:
void MyClass::myMethod() {
// implementation code here
}
Note that the method definition must include the class name followed by the scope resolution operator ::
and the method name. This tells the compiler that you are defining a member function of the class, not a standalone function.
Within the method implementation, you can access the class’s data members and call other member methods using this keyword. For example:
class MyClass {
private:
int myInt;
public:
void setInt(int value) {
this->myInt = value;
}
int getInt() {
return this->myInt;
}
};
In this example, the setInt and getInt methods are defined to set and retrieve the value of the private myInt member variable. The this keyword is used to refer to the current object, allowing you to access its data members and call its methods.
You can also pass parameters to member methods and return values from them, just like regular functions. For example:
class MyClass {
public:
int addNumbers(int a, int b) {
return a + b;
}
};
In this example, the addNumbers method takes two int parameters and returns their sum. It can be called on an object of the MyClass class like this:
MyClass myObject;
int result = myObject.addNumbers(5, 10);
This will create an instance of MyClass called myObject and call its addNumbers method with the parameters 5 and 10. The resulting sum 15 will be returned and assigned to the result variable.
Overall, member methods are a powerful feature of C++ classes that allow you to encapsulate data and behaviour within a single object-oriented construct. By declaring and implementing these methods properly, you can write robust, efficient, and reusable code that solves a wide variety of programming problems.
Best practice: code separation
In C++, it’s common to separate the class declaration from the implementation by putting the declaration in a header file (often with a .h or .hpp extension) and the method definitions in a separate implementation file (often with a .cpp extension). This makes the code more modular and easier to maintain.
Here’s an example of how to separate a class into separate files:
MyClass.h:
class MyClass {
private:
int myInt;
public:
MyClass();
void setInt(int value);
int getInt();
};
In this header file, we have declared the MyClass class and its member methods setInt, getInt, and a constructor. Constructors will be dealt in great detail in upcoming sections. For simplicity, consider constructor as any other method in a class. Note that we only provide method declarations, not the full method implementations.
MyClass.cpp:
#include "MyClass.h"
MyClass::MyClass() {
myInt = 0;
}
void MyClass::setInt(int value) {
myInt = value;
}
int MyClass::getInt() {
return myInt;
}
In this implementation file, we include the header file and provide the full implementations of the class methods declared in the header file. Note that we use the scope resolution operator :: to link the method definitions to the class.
main.cpp
#include <iostream>
#include "MyClass.h"
int main() {
MyClass myObject;
myObject.setInt(42);
std::cout << myObject.getInt() << std::endl;
return 0;
}
In the main driver file, we include the header file and use the MyClass class just like we would normally. Note that we don’t need to include the implementation file, because it’s already included in the header file.
To compile and run this program, we can use the following command:
g++ MyClass.cpp main.cpp -o myProgram
./myProgram
This will compile both source files and link them together to create an executable called myProgram. When we run it, it will create an instance of MyClass, set its integer value to 42, and print it out to the console.
Separating class declaration from implementation in this way is a common practice in C++ programming, as it helps to keep the code organised and maintainable. By using header files for class declarations and implementation files for method definitions, you can write clear and concise code that is easy to read, modify, and reuse.
Exercises
- Create a Rectangle class with private member variables width and height, and public methods setWidth, setWidth, getArea, and getPerimeter. Implement the methods to set and retrieve the values of the width and height, and calculate the area and perimeter of the rectangle.
- Create a Person class with private member variables name and age, and public methods setName, setAge, getName, and getAge. Implement the methods to set and retrieve the values of the name and age, and validate the input (e.g. age must be a positive integer).
- Create a BankAccount class with private member variables accountNumber, balance, and interestRate, and public methods deposit, withdraw, getBalance, and calculateInterest. Implement the methods to deposit and withdraw money from the account, retrieve the current balance, and calculate the interest earned based on the current balance and interest rate.
- Create a Student class with private member variables name, id, grades (an array of doubles), and public methods setName, setId, setGrades, getName, getId, getGrades, and getAverageGrade. Implement the methods to set and retrieve the values of the name and id, set the grades, retrieve the grades, and calculate the average grade.
- Create a Car class with private member variables make, model, year, speed, and public methods setMake, setModel, setYear, setSpeed, getMake, getModel, getYear, getSpeed, accelerate, and brake. Implement the methods to set and retrieve the values of the make, model, year, and speed, and increase or decrease the speed by a certain amount when accelerating or braking.
Constructors
In C++, constructors are special member functions that are called automatically when an object of a class is created. Constructors are responsible for initialising the member variables of the class to specific values, and they can also perform additional initialization tasks if needed. Constructors have the same name as the class and no return type, not even void.
Here’s an example to illustrate how constructors work:
class Rectangle {
public:
Rectangle(); // Constructor declaration
int getWidth() const;
int getHeight() const;
void setWidth(int width);
void setHeight(int height);
private:
int width_;
int height_;
};
Rectangle::Rectangle() {
width_ = 0;
height_ = 0;
}
int Rectangle::getWidth() const {
return width_;
}
int Rectangle::getHeight() const {
return height_;
}
void Rectangle::setWidth(int width) {
width_ = width;
}
void Rectangle::setHeight(int height) {
height_ = height;
}
In this example, we have defined a class called Rectangle with private member variables width_ and height_, and public methods getWidth, getHeight, setWidth, and setHeight to retrieve and modify these values. We have also declared a constructor for the class called Rectangle, which takes no parameters.
In the implementation of the constructor, we have initialised the width_ and height_ member variables to 0. This means that whenever an object of the Rectangle class is created, its width_ and height_ will be set to 0 by default.
Now, let’s say we want to create an object of the Rectangle class and set its width and height to specific values. We can do this as follows:
Rectangle rect; // Create a Rectangle object using the default constructor/ zero constructor
rect.setWidth(10); // Set the width to 10
rect.setHeight(20); // Set the height to 20
In this code, we have created an object of the Rectangle class called rect using the default constructor. We have then set its width_ and height_ member variables to 10 and 20, respectively, using the setWidth and setHeight methods.
Constructors can also take parameters, which can be used to initialise member variables to specific values. Here’s an example:
class Person {
public:
Person(const std::string& name, int age);
const std::string& getName() const;
int getAge() const;
private:
std::string name_;
int age_;
};
Person::Person(const std::string& name, int age)
: name_(name), age_(age) {
}
const std::string& Person::getName() const {
return name_;
}
int Person::getAge() const {
return age_;
}
In this example, we have defined a class called Person with private member variables name_ and age_, and public methods getName and getAge to retrieve these values. We have also declared a constructor for the class called Person, which takes two parameters: a const std::string& representing the person’s name, and an int representing the person’s age. Constructors that take parameters are called Parameterised constructors.
The following syntax shows how to pass arguments to constructor:
ClassName objName (arg1, arg2, …, argn); // Local object
ClassName *objName = new ClassName(arg1, arg2, …, argn); // Object pointer
Note that if we define any other constructor for the class, then the default constructor will not be automatically generated. In that case, if we want to create objects using the default constructor, we need to explicitly define it.
#include <iostream>
class A {
public:
int x; // data member
A(int a) { // parameterized constructor
std::cout << "Parameterized constructor called" << std::endl;
x = a; // initialize x with given value
}
};
int main() {
A a; // error: no matching function for call to 'A::A()'
return 0;
}
In this example, we have defined a parameterized constructor for class A. Since we have defined a constructor, the compiler will not generate the default constructor. Therefore, when we try to create an object of class A without any arguments, we get a compilation error.
Constructor overloading
In C++, constructor overloading is a technique that allows us to define multiple constructors with different parameters in a class. This means that we can create objects of the same class in different ways, depending on the parameters we provide when creating the object.
Constructor overloading is useful when we want to create objects of a class in different ways. For example, suppose we have a Person class that has two member variables – name and age. We might want to create a Person object with just a name, or with just an age, or with both name and age. We can achieve this by overloading the constructor of the Person class with different parameter combinations.
Here’s an example:
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
// Default constructor
Person() {
name = "Unknown";
age = 0;
std::cout << "Default constructor called" << std::endl;
}
// Constructor with name parameter
Person(std::string n) {
name = n;
age = 0;
std::cout << "Constructor with name parameter called" << std::endl;
}
// Constructor with age parameter
Person(int a) {
name = "Unknown";
age = a;
std::cout << "Constructor with age parameter called" << std::endl;
}
// Constructor with both name and age parameters
Person(std::string n, int a) {
name = n;
age = a;
std::cout << "Constructor with name and age parameters called" << std::endl;
}
};
int main() {
// Creating objects with different constructors
Person p1; // Default constructor called
Person p2("John"); // Constructor with name parameter called
Person p3(25); // Constructor with age parameter called
Person p4("Jane", 30); // Constructor with name and age parameters called
return 0;
}
In the above code, we have defined a Person class with four constructors. The first constructor is the default constructor, which initialises the name and age member variables to default values. The second constructor takes a name parameter and initialises the name member variable with the passed value, while the age member variable is set to a default value. The third constructor takes an age parameter and initialises the age member variable with the passed value, while the name member variable is set to a default value. The fourth constructor takes both name and age parameters and initialises the member variables with the passed values.
In the main() function, we create objects of the Person class using different constructors. We create an object p1 with the default constructor, an object p2 with the constructor that takes a name parameter, an object p3 with the constructor that takes an age parameter, and an object p4 with the constructor that takes both name and age parameters. Depending on the constructor used, the corresponding message is printed to the console.
Constructor overloading makes it easier and more flexible to create objects of a class, by providing different ways to initialise the member variables.
Destructors
In C++, a destructor is a member function of a class that is called when an object of that class is destroyed, typically when it goes out of scope or is explicitly deleted. A destructor has the same name as the class preceded by a tilde (~) symbol.
The purpose of a destructor is to perform cleanup tasks such as freeing dynamically allocated memory, closing files, or releasing other resources that the object has acquired during its lifetime.
Here is an example of a class with a destructor:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
MyClass obj; // create an object of MyClass
// obj goes out of scope here and its destructor is called
return 0;
}
In this example, the constructor of MyClass is called when an object of the class is created. The constructor simply outputs a message to the console. The destructor of MyClass is called automatically when the object goes out of scope at the end of the main function. The destructor also outputs a message to the console.
When a class has dynamically allocated memory or other resources, it is important to ensure that these resources are released properly. Here is an example of a class with a destructor that frees dynamically allocated memory:
#include <iostream>
class MyClass {
private:
int* ptr;
public:
MyClass() {
ptr = new int[10]; // allocate memory
}
~MyClass() {
delete[] ptr; // free memory
}
};
int main() {
MyClass obj; // create an object of MyClass
// obj goes out of scope here and its destructor is called
return 0;
}
In this example, the constructor of MyClass allocates memory using the new operator. The destructor of MyClass frees the memory using the delete[] operator. This ensures that the memory allocated by the constructor is released when the object is destroyed.
Note that in C++, a class can have only one destructor, and it cannot be overloaded with different parameter lists. The destructor has no return type, and its parameters cannot be modified.
Order of invocation of constructors and destructors
Objects containing other objects
In C++, the order in which constructors and destructors are called depends on the order in which objects are created and destroyed. The general rule is that the constructor of an object is called before the constructor of any objects that it contains, and the destructor of an object is called after the destructor of any objects that it contains.
Let’s consider an example to illustrate this:
#include <iostream>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
class B {
private:
A a;
public:
B() {
std::cout << "B constructor called" << std::endl;
}
~B() {
std::cout << "B destructor called" << std::endl;
}
};
int main() {
B b; // create an object of class B
return 0;
}
In this example, we have two classes, A and B, where B contains an object of class A. In the main function, we create an object of class B named b.
The output of this program will be:
A constructor called B constructor called B destructor called A destructor called |
As you can see, the constructor of A is called first because it is a member of B. Next, the constructor of B is called. When the program ends and B goes out of scope, the destructor of B is called first, followed by the destructor of A.
It is important to note that the order of destruction is the reverse of the order of construction. This is because the destruction of objects follows the opposite path of construction. In the example above, a was constructed before b, but a was destroyed after b.
Multiple objects of same class
When we create multiple objects of the same class, the constructor and destructor are invoked in the order of creation and destruction of the objects respectively.
Let’s consider an example to illustrate this:
#include <iostream>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
int main() {
A a1; // create first object of class A
A a2; // create second object of class A
A a3; // create third object of class A
return 0;
}
In this example, we have a class A with a constructor and destructor. In the main function, we create three objects of class A named a1, a2, and a3.
The output of this program will be:
A constructor called
A constructor called
A constructor called
A destructor called
A destructor called
A destructor called
As you can see, the constructor of A is called three times in the order of creation of the objects, and the destructor of A is called three times in the reverse order of creation of the objects.
This means that when the first object a1 is created, its constructor is called, followed by the constructors of a2 and a3 respectively. When the program ends and the objects go out of scope, the destructor of a3 is called first, followed by the destructors of a2 and a1 respectively.
So, the order of invocation of constructor and destructor of multiple objects of the same class is determined by the order of creation and destruction of the objects, respectively.
Exercises
- Write a C++ class Rectangle with two private data members length and width of type double. Implement a parameterized constructor that takes two arguments len and wid and initialises the data members with the given values. Also, implement a destructor that prints a message “Rectangle object destroyed”.
#include <iostream>
class Rectangle {
private:
double length;
double width;
public:
Rectangle(double len, double wid) {
length = len;
width = wid;
}
~Rectangle() {
std::cout << "Rectangle object destroyed" << std::endl;
}
};
int main() {
Rectangle r(5, 10);
return 0;
}
- Write a C++ class Person with two private data members name and age of type string and int respectively. Implement a default constructor that initialises the data members with default values “Unknown” and 0. Also, implement a parameterized constructor that takes two arguments n and a and initialises the data members with the given values. Also, implement a destructor that prints a message “Person object destroyed”.
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
Person() {
name = "Unknown";
age = 0;
}
Person(std::string n, int a) {
name = n;
age = a;
}
~Person() {
std::cout << "Person object destroyed" << std::endl;
}
};
int main() {
Person p1;
Person p2("John", 30);
return 0;
}
- Write a C++ class Circle with a private data member radius of type double. Implement a default constructor that initialises the radius to 0.0. Also, implement a parameterized constructor that takes one argument r and initialises the radius with the given value. Also, implement a function area that returns the area of the circle.
#include <iostream>
class Circle {
private:
double radius;
public:
Circle() {
radius = 0.0;
}
Circle(double r) {
radius = r;
}
double area() {
return 3.14 * radius * radius;
}
};
int main() {
Circle c1;
Circle c2(5);
std::cout << "Area of c1: " << c1.area() << std::endl;
std::cout << "Area of c2: " << c2.area() << std::endl;
return 0;
}
- Write a C++ class Student with two private data members name and roll of type string and int respectively. Implement a default constructor that initialises the data members with default values “Unknown” and 0. Also, implement a parameterized constructor that takes two arguments n and r and initialises the data members with the given values. Also, implement a function display that displays the name and roll number of the student.
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int roll;
public:
// Default constructor
Student() : name("Unknown"), roll(0) {}
// Parameterized constructor
Student(std::string n, int r) : name(n), roll(r) {}
// Display method
void display() {
std::cout << "Name: " << name << "\nRoll: " << roll << std::endl;
}
};
int main() {
// Create default student object
Student s1;
s1.display(); // Output: Name: Unknown, Roll: 0
// Create parameterized student object
Student s2("John Doe", 123);
s2.display(); // Output: Name: John Doe, Roll: 123
return 0;
}
Constructor initialiser lists
In C++, constructor initialization lists provide a way to initialise the member variables of a class before the constructor body is executed. Initialization lists use a special syntax to initialise the members of the class, and can be used to initialise both primitive and non-primitive data types.
Constructor initialization lists are used in the constructor’s definition and are placed after the constructor’s parameters and before the constructor’s body. Here’s an example of a constructor initialization list:
class Person {
private:
std::string name;
int age;
public:
// Constructor with initialization list
Person(std::string n, int a) : name(n), age(a) {}
};
In this example, the Person class has two member variables, name and age. The constructor takes two parameters, n and a, and initialises the name and age member variables using an initialization list.
The syntax of an initialization list is a colon : followed by a comma-separated list of member variable names, each one followed by its initial value in parentheses. For example, in the above Person class, the initialization list : name(n), age(a) initialises the name member variable with the value of the n parameter, and the age member variable with the value of the a parameter.
Initialization lists can also be used to initialise non-primitive data types such as arrays, pointers, and other classes. Here’s an example:
class Rectangle {
private:
int* dimensions;
public:
// Constructor with initialization list
Rectangle(int length, int width) : dimensions(new int[2]) {
dimensions[0] = length;
dimensions[1] = width;
}
// Destructor
~Rectangle() {
delete[] dimensions;
}
};
In this example, the Rectangle class has a member variable dimensions, which is a pointer to an integer array. The constructor initialises dimensions with a new integer array of size 2, and sets the first and second elements of the array to length and width, respectively. The destructor deallocates the memory allocated for the integer array.
Using constructor initialization lists is preferred over initialising member variables inside the constructor’s body, because it is more efficient and can prevent undefined behaviour when initialising const or reference member variables.
Delegating constructors
Delegating constructors are a feature introduced in C++11 that allow one constructor of a class to call another constructor of the same class to perform initialization tasks. This helps to avoid code duplication and simplify the code.
Delegating constructors are invoked using the colon (:) syntax and are placed before the constructor body. The syntax for delegating constructors is:
Classname(params) : Constructor_to_call(args)
{
// constructor body
}
Here, Classname(params) is the constructor being defined, and Constructor_to_call(args) is the constructor being delegated to. The constructor being delegated to must have already been defined in the class before the constructor being defined. The delegated constructor is called before the constructor being defined and initialises the members of the class.
Let’s take an example to understand delegating constructors in detail:
#include <iostream>
using namespace std;
class MyClass {
private:
int num1;
int num2;
public:
MyClass() : MyClass(0, 0) {
cout << "Default Constructor" << endl;
}
MyClass(int n) : MyClass(n, 0) {
cout << "Single Parameter Constructor" << endl;
}
MyClass(int n1, int n2) : num1(n1), num2(n2) {
cout << "Two Parameter Constructor" << endl;
}
void display() {
cout << "Number 1: " << num1 << endl;
cout << "Number 2: " << num2 << endl;
}
};
int main() {
MyClass obj1;
obj1.display();
MyClass obj2(5);
obj2.display();
MyClass obj3(10, 20);
obj3.display();
return 0;
}
In this example, we have defined three constructors for the MyClass class. The first constructor is the default constructor that initialises the data members with default values. The second constructor is a single-parameter constructor that takes an integer value and calls the two-parameter constructor with the given value and 0. The third constructor is a two-parameter constructor that initialises the data members with the given values.
Notice how the two-parameter constructor is being called in the delegating constructor syntax in both the default and single-parameter constructors. This helps to avoid code duplication and simplify the code.
Output:
Two Parameter Constructor
Default Constructor
Number 1: 0
Number 2: 0
Two Parameter Constructor
Single Parameter Constructor
Number 1: 5
Number 2: 0
Two Parameter Constructor
Number 1: 10
Number 2: 20
In the output, we can see that the constructors are being called in the correct order, and the correct values are being passed to the constructors. This is an example of how delegating constructors can simplify the code and avoid code duplication.
Default arguments in Constructors
Default arguments in constructors allow us to define parameters with default values. These default values are used when no argument is provided for that parameter. The syntax for defining default arguments in constructors is the same as in functions.
Consider the following example:
class Person {
private:
std::string name;
int age;
public:
Person(std::string name = "Unknown", int age = 0) {
this->name = name;
this->age = age;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
// Using default values
Person p1; // name = "Unknown", age = 0
// Providing values for all parameters
Person p2("John", 25); // name = "John", age = 25
// Providing value for one parameter, using default value for the other
Person p3("Alice"); // name = "Alice", age = 0
return 0;
}
In this example, the Person class has a constructor with two parameters: name and age. Both parameters have default values of “Unknown” and 0, respectively. This means that if no arguments are provided when creating a Person object, the object will be initialised with these default values.
The rules for default arguments in constructors are as follows:
- Default arguments must be specified in the declaration of the constructor in the class definition, not in the implementation.
- If a constructor has multiple parameters, default arguments can only be specified for the trailing parameters.
- Default arguments can be used in constructors that are also overloaded.
Here is an example that illustrates rules 2 and 3:
class Rectangle {
private:
int width;
int height;
public:
Rectangle(int w, int h = 0) {
width = w;
height = h;
}
Rectangle() : Rectangle(0) {}
void display() {
std::cout << "Width: " << width << ", Height: " << height << std::endl;
}
};
int main() {
// Using default values
Rectangle r1; // width = 0, height = 0
// Providing values for all parameters
Rectangle r2(10, 20); // width = 10, height = 20
// Providing value for one parameter, using default value for the other
Rectangle r3(5); // width = 5, height = 0
return 0;
}
In summary, default arguments in constructors provide a convenient way to initialise objects with default values, while still allowing for customization by providing arguments for the parameters.
Copy constructor
In C++, a copy constructor is a special constructor that is used to create a new object as a copy of an existing object of the same class. It is a member function of a class that has the same name as the class and takes a single argument of a const reference to the same class.
Syntax of declaring the copy constructor:
Type::Type(const Type &source);
The syntax for a copy constructor is:
ClassName(const ClassName &old_obj)
{
// Code to copy the old object to new object
}
Here, old_obj is the existing object that is being copied, and ClassName is the name of the class. The copy constructor takes a reference to old_obj and creates a new object with the same data as old_obj.
The copy constructor is called automatically when an object is created from an existing object, such as:
ClassName obj1; // Calling default constructor
ClassName obj2(obj1); // Calling copy constructor
In the above example, obj2 is created using the copy constructor and is initialised with the values of obj1.
Here is an example of a copy constructor in action:
#include <iostream>
using namespace std;
class MyClass {
int x, y;
public:
MyClass(int a, int b) {
x = a;
y = b;
}
MyClass(const MyClass &old_obj) {
x = old_obj.x;
y = old_obj.y;
}
void display() {
cout << "x = " << x << ", y = " << y << endl;
}
};
int main() {
MyClass obj1(10, 20);
MyClass obj2(obj1); // Copy constructor called
obj1.display(); // Output: x = 10, y = 20
obj2.display(); // Output: x = 10, y = 20
return 0;
}
In the above example, MyClass has a copy constructor that takes a reference to an existing object and initialises the new object with the same values. The display function is used to output the values of the data members.
When obj2 is created using obj1 as the argument, the copy constructor is called and a new object is created with the same values as obj1. The display function is then called on both objects to show that they have the same values.
It is important to note that if a copy constructor is not defined explicitly, the compiler generates a default copy constructor, which performs a shallow copy of the data members. This may not be sufficient for classes that contain dynamically allocated memory or have other complex data structures, which is why it is often necessary to define a copy constructor explicitly.
This is particularly important when an object contains dynamically allocated memory, such as arrays or pointers, as simply copying the memory address would result in both objects pointing to the same memory location.
To implement copying of dynamically allocated memory members in a copy constructor, we need to ensure that a deep copy is made. This means that we allocate new memory for the copy and copy the contents of the original memory into the new memory. Here is an example:
#include <iostream>
using namespace std;
class MyClass {
private:
size_t size;
public:
int* ptr;
// Constructor
MyClass(int s) {
size = s;
ptr = new int[size];
}
// Copy constructor
MyClass(const MyClass& obj) {
size = obj.size;
ptr = new int[size];
for (int i = 0; i < size; i++) {
ptr[i] = obj.ptr[i];
}
}
size_t getSize() { return size; }
int getPtr(size_t idx) { return ptr[idx]; }
// Destructor
~MyClass() {
delete[] ptr;
}
};
int main() {
MyClass obj1(5); // Create object with size 5
obj1.ptr[0] = 1;
obj1.ptr[1] = 2;
obj1.ptr[2] = 3;
obj1.ptr[3] = 4;
obj1.ptr[4] = 5;
MyClass obj2 = obj1; // Copy object
obj2.ptr[0] = 10; // Modify copy
obj2.ptr[1] = 20;
// Display original and copy
for (int i = 0; i < obj1.getSize(); i++) {
cout << obj1.getPtr(i) << " ";
}
cout << endl;
for (int i = 0; i < obj2.getSize(); i++) {
cout << obj2.getPtr(i) << " ";
}
cout << endl;
return 0;
}
Ouput:
1 2 3 4 5
10 20 3 4 5
Output if the copy constructor is commented out:
10 20 3 4 5
10 20 3 4 5
In this example, the MyClass class contains a dynamically allocated array ptr of size size. The copy constructor is defined to create a deep copy of the object by allocating a new array and copying the contents of the original array into the new array. The getSize() and getPtr() functions are defined to access the private member variables size and ptr, respectively.
In the main() function, we create an object obj1 with size 5 and assign values to its elements. We then create a copy of obj1 using the copy constructor, modify the copy, and display both objects. Since a deep copy is made, the modifications made to the copy do not affect the original object.
Copy constructors get called even when the objects are passed to the function or if the function is returning the object. This is because, when an object is communicated from or to the function, internally the object is being copied from one scope to the other, hence the copy constructor is called. Here’s an example that demonstrates the use of copy constructor when passing and returning objects from a function:
#include <iostream>
using namespace std;
class MyClass {
private:
int* arr;
int size;
public:
MyClass(int s) {
size = s;
arr = new int[size];
for(int i = 0; i < size; i++) {
arr[i] = i;
}
}
// Copy constructor
MyClass(const MyClass& obj) {
cout << "Copy constructor called" << endl;
size = obj.size;
arr = new int[size];
for(int i = 0; i < size; i++) {
arr[i] = obj.arr[i];
}
}
~MyClass() {
delete[] arr;
}
void print() {
for(int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
};
// Function that returns an object of MyClass
MyClass func(MyClass obj) {
obj.print();
cout << "Returning the object back to caller" << endl;
return obj;
}
int main() {
MyClass obj1(5);
// Pass obj1 to func
cout << "Passing the object to the function" << endl;
MyClass obj2 = func(obj1);
return 0;
}
Output:
Passing the object to the function
Copy constructor called
0 1 2 3 4
Returning the object back to caller
Copy constructor called
In this example, we have a MyClass that dynamically allocates an array of integers of size size in the constructor. We also have a copy constructor to deep copy the array. In the main function, we first create an object obj1 of MyClass with size 5 and print its contents using the print function.
Next, we pass obj1 to the func function. Since the parameter of func is an object of MyClass, a copy constructor is called to create a new object inside the function. The function then prints the contents of this new object and returns it.
Finally, we create another object obj2 and assign it the returned object of func. Since the copy constructor is called again to create obj2, the dynamically allocated memory is deep copied to the new object. We then print the contents of obj2.
In this way, we can see that the copy constructor is called both when passing and returning objects from a function.
A copy constructor can also have an initialiser list. Here is an example of the same:
class Car {
private:
string model;
int year;
public:
// Copy constructor with initializer list
Car(const Car& otherCar) : model{otherCar.model}, year{otherCar.year} {
cout << "Copy constructor called." << endl;
}
};
The above example basically pics up the attributes from the source object and assigns it to the attributes of the assigned object.
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 they already provide copy constructors.
- Avoid using raw pointer data members if possible.
Shallow copying with copy constructor
Shallow copying is a type of object copying where only the values of the data members are copied from the source object to the destination object. This means that if the data member is a pointer, then only the address of the memory location pointed to by the pointer is copied, not the contents of the memory location itself. As a result, both the source and destination objects will be pointing to the same memory location, which can lead to issues like memory leaks and data corruption.
When using the copy constructor for shallow copying, the default copy constructor provided by the compiler is sufficient. Here’s an example:
#include <iostream>
#include <cstring>
class Person {
public:
char* name;
int age;
Person(const char* n, int a) {
name = new char[strlen(n) + 1];
strcpy(name, n);
age = a;
}
~Person() {
delete[] name;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
Person p1("John", 30);
Person p2 = p1; // using default copy constructor
p1.display(); // output: Name: John, Age: 30
p2.display(); // output: Name: John, Age: 30
// changing name of p1 will also change name of p2
p1.name[0] = 'M';
p1.display(); // output: Name: Mon, Age: 30
p2.display(); // output: Name: Mon, Age: 30
return 0;
}
In this example, the Person class has a char* data member name, which is dynamically allocated using the new operator in the constructor. When the default copy constructor is used to copy p1 to p2, the pointer name is copied, but not the contents of the memory location it points to. This means that both p1 and p2 are pointing to the same memory location, and any changes made to name in one object will affect the other object as well. This can be seen when we change the first character of name in p1 to ‘M’, and then display both objects using the display() function. Both objects have the same name “Mon”, which is the modified name of p1. This is an example of shallow copying.
But there is a problem with shallow copy which might crash the program written. The problem is explained in the following example: Consider a class named Person which has a dynamic member name of type char*. The Person class has a constructor which allocates memory for name using new and a destructor which frees the allocated memory using delete. Here’s the class definition:
#include <cstring>
class Person {
private:
char* name;
public:
Person(const char* n) {
name = new char[strlen(n) + 1];
strcpy(name, n);
}
Person(const Person& p) {
name = p.name; // Shallow copy
}
~Person() {
delete[] name;
}
const char* getName() const {
return name;
}
};
In the Person copy constructor, we are performing a shallow copy of the name member. This means that both the original object and the new object will have the same value for the name pointer, and they will both point to the same memory location.
Now let’s write a function named printPerson which takes a Person object by value and prints its name:
void printPerson(Person p) {
std::cout << "Name: " << p.getName() << std::endl;
}
Notice that printPerson takes a Person object by value, which means that a copy of the original object will be made when the function is called.
Now let’s create a Person object and call printPerson with it:
int main() {
Person p1("Alice");
printPerson(p1);
return 0;
}
In this code, we are creating a Person object p1 with the name “Alice”, and then passing it to the printPerson function. Remember that printPerson takes a Person object by value, so a copy of p1 will be made. Since we are performing a shallow copy in the copy constructor, both the original object p1 and the copy created for the printPerson function will have the same name pointer, pointing to the same memory location.
Now let’s look at what happens when the printPerson function returns. When a function returns, all the variables created inside the function go out of scope and their destructors are called. This means that the destructor of the copy created for the printPerson function will be called, and it will free the memory pointed to by the name pointer. However, the original object p1 still has a name pointer pointing to that memory location, even though it has already been freed by the destructor of the copy. This is a problem, because the memory has already been freed and accessing it can cause undefined behaviour.
In summary, the problem with shallow copying is that if multiple objects have pointers to the same dynamically allocated memory, deleting the memory once can cause problems when the other objects try to access that memory.
Exercises
Exercise 1: Copy constructor for a class with dynamic memory allocation
Create a class named MyArray which has a dynamically allocated integer array as a member variable. Implement the following methods:
- A default constructor which initialises the array with a size of 0.
- A parameterized constructor which takes an integer value n and initialises the array with the given size.
- A copy constructor which creates a new instance of MyArray and copies the contents of the original array to the new array.
- A destructor which frees the memory allocated to the array.
Example usage:
MyArray a(5); // create an array of size 5
MyArray b = a; // create a copy of a
Exercise 2: Initializer list for a class with non-primitive data members
Create a class named Person which has two string member variables: name and address. Implement the following methods:
- A default constructor which initialises the name and address variables with empty strings.
- A parameterized constructor which takes two string values and initialises the name and address variables with the given values.
- A copy constructor which creates a new instance of Person and copies the name and address variables from the original instance.
- A destructor which does nothing.
Use an initializer list to initialise the member variables in the constructors.
Example usage:
Person p1("John", "123 Main St.");
Person p2 = p1; // create a copy of p1
Exercise 3: Delegating constructors
Create a class named Rectangle which has two member variables: length and width. Implement the following methods:
- A default constructor which initialises length and width to 0.
- A parameterized constructor which takes two integer values and initialises length and width with the given values.
- A constructor which takes a single integer value and delegates to the parameterized constructor, setting both length and width to the given value.
- A destructor which does nothing.
Example usage:
Rectangle r1(5, 10); // create a rectangle with length 5 and width 10
Rectangle r2(7); // create a square with length 7
Move constructors
Move semantics are one of the most powerful features introduced in C++ 11. It is quite complicated hence we shall try to dissect the topic one step at a time. First let’s revisit lvalues and rvalues. In C++, an expression refers to a piece of code that produces a value. There are two types of expressions in C++: lvalues and rvalues.
Lvalues and rvalues
An lvalue refers to an object that has a name, a memory address, and a value. An lvalue can be assigned a value or passed as an argument to a function. For example:
int x = 10; // 'x' is an lvalue
int y = x; // 'x' is an lvalue, 'y' is an lvalue
In the above example, x and y are lvalues because they are objects with names, memory addresses, and values.
On the other hand, an rvalue refers to an object that has a value but no name or memory address. An rvalue cannot be assigned a value or passed as an argument by non-const reference. For example:
int x = 10; // '10' is an rvalue
int y = x + 5; // '(x + 5)' is an rvalue, 'y' is an lvalue
In the above example, 10 and (x + 5) are rvalues because they are objects with values but no names or memory addresses. y is an lvalue because it is an object with a name, a memory address, and a value.
Lvalue references and rvalue references
An lvalue reference is a reference to an lvalue object. It is denoted by the & symbol. The main use case of lvalue references is to modify the object referred to by the reference. Here is an example of an lvalue reference:
int x = 5;
int& ref = x;
ref = 10; // changes the value of x to 10
In this example, ref is an lvalue reference to the int object x. When we assign 10 to ref, it modifies the value of x as well.
On the other hand, an rvalue reference is a reference to an rvalue object. It is denoted by the && symbol. The main use case of rvalue references is to enable move semantics in C++. Here is an example of an rvalue reference:
int&& rref = 5;
int x = 6;
int&& rref1 = x; // Error
In this example, rref is an rvalue reference to the int object 5.
Lvalues and rvalues as function parameters
A function parameter can be declared as either an lvalue reference or an rvalue reference. This allows functions to distinguish between lvalues and rvalues passed as arguments. Consider the following example:
#include <iostream>
void func(int& lref) {
std::cout << "lvalue reference: " << lref << std::endl;
}
void func(int&& rref) {
std::cout << "rvalue reference: " << rref << std::endl;
}
int main() {
int a = 5;
int& lref = a;
func(lref); // calls lvalue reference version of func
func(10); // calls rvalue reference version of func
return 0;
}
Output:
lvalue reference: 5
rvalue reference: 10
In this example, we have two versions of the func function – one that takes an lvalue reference parameter and one that takes an rvalue reference parameter. In the main function, we create an integer variable a and a reference lref to a. We then call func with lref as an argument, which will call the lvalue reference version of func. Finally, we call func with the integer literal 10 as an argument, which will call the rvalue reference version of func.
Move constructor
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. C++11 introduced move semantics and the move constructor. Move constructor moves an object rather than copying it.
In C++, a move constructor is a special constructor that allows efficient transfer of resources (such as memory) from a temporary object to a new object. It is used to implement move semantics, which improve the performance of certain operations by avoiding unnecessary copying of data.
When an object is moved, the resources owned by the object (such as memory allocated on the heap) are transferred to the new object, leaving the original object in a “valid but unspecified” state. This can be more efficient than copying the data, especially for large objects or when copying is expensive.
The syntax for a move constructor is similar to that of a copy constructor, but it uses an rvalue reference parameter (denoted by &&) instead of a const reference parameter:
class MyClass {
public:
// Move constructor
MyClass(MyClass&& other) {
// Transfer resources from 'other' to 'this'
// ...
}
};
Here’s an example of how a move constructor can be used to improve the performance of a function that returns a large object:
class LargeObject {
public:
// Constructor that allocates a large amount of memory
LargeObject(size_t size) {
data_ = new char[size];
}
// Move constructor
LargeObject(LargeObject&& other) {
data_ = other.data_;
other.data_ = nullptr;
}
// Destructor
~LargeObject() {
delete[] data_;
}
private:
char* data_;
};
// Function that returns a large object by value
LargeObject createLargeObject() {
LargeObject obj(1000000);
return obj; // Move-constructs a new object from 'obj'
}
int main() {
LargeObject obj = createLargeObject(); // Move-constructs 'obj' from temporary object
// ...
}
In this example, the createLargeObject() function returns a LargeObject by value. Without a move constructor, this would result in a copy of the LargeObject being made when the function returns, which could be expensive for large objects. With a move constructor, the new object can be constructed by transferring the memory owned by the temporary object returned by createLargeObject(), resulting in better performance.
Exercise 1: Implement a class with a move constructor
Write a C++ class Person that has a move constructor. The class should have the following private data members:
- name of type std::string
- age of type int
The class should have the following public member functions:
- a constructor that takes name and age as arguments and initialises the corresponding data members
- a move constructor that moves the name and age from the source object to the destination object
- a getName function that returns the name of the person
- an getAge function that returns the age of the person
Exercise 2: Implement a class with a move constructor that moves a dynamically allocated array
Write a C++ class IntArray that has a move constructor. The class should have the following private data members:
- size_ of type int, the size of the dynamically allocated array
- data_ of type int*, a pointer to the dynamically allocated array
The class should have the following public member functions:
- a constructor that takes size as an argument and dynamically allocates an array of size size
- a destructor that frees the dynamically allocated array
- a move constructor that moves the size and data from the source object to the destination object
- a getSize function that returns the size of the array
- an getElement function that takes an index i as argument and returns the i-th element of the array
The `this` pointer
In C++, the this pointer is a pointer to the current object. It is an implicit parameter that is passed to all non-static member functions of a class. The this pointer is used to refer to the calling object inside a member function. It is particularly useful when there are multiple objects of the same class, and the member function needs to know which object it is currently operating on. Here is an example of how the this pointer is used in a class called Person:
#include <iostream>
#include <string>
class Person {
public:
Person(std::string name, int age) {
this->name = name;
this->age = age;
}
void printInfo() {
std::cout << "Name: " << this->name << std::endl;
std::cout << "Age: " << this->age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
Person person1("John", 30);
Person person2("Jane", 25);
person1.printInfo();
person2.printInfo();
return 0;
}
In the above example, the this pointer is used to refer to the name and age members of the calling object inside the printInfo function. When person1.printInfo() is called, this will point to person1. Similarly, when person2.printInfo() is called, this will point to person2. Note that this is particularly of great importance when the function parameter and the member objects have the same name. In the above example, this can be seen in the constructor. The constructor has 2 parameters: name and age. The class Person has 2 member objects of the same name. The this pointer helps the compiler distinguish between the two.
The const keyword in context of classes
Consider a class named Person with two member variables: name which is a string and age which is unsigned short int. A constructor that initialises both of these attributes. The class contains a method named set_name() that accepts an argument: new_name that replaces the value of the current name attribute to that of the received new attribute.
class Person {
public:
std::string name;
int age;
Person(std::string name, int age) {
this->name = name;
this->age = age;
}
void set_name(std::string new_name) {
this->name = new_name;
}
};
The following statement creates a const object of Person:
const Person p {"Prajwal", 32};
If set_name() is called from p to change name, the compiler will throw an error. Obviously this is done to prevent accidental modification of member objects of p. Let’s consider another method to print_name() that prints the name of the caller object. Consider the following definition of the function print_name():
void print_name() {
std::cout << this->name << std::endl;
}
The above method would also make the compiler throw error.The reason is that there is a possibility that the object’s member values might get altered inside the method. The C++ compiler takes utmost care to prevent any accidental modification of the object’s member values. To solve this problem, we need to introduce const keyword to methods too which make them compatible to be called from const objects. The following is the syntax to do the same:
void print_name() const{
std::cout << this->name << std::endl;
}
Note that it is not enough to use const keyword in both object creation and in the function. We need to make sure that we are actually not changing/modifying the object values inside of the methods to prevent any errors. Also, a const method can be called by a non-const object but the converse is not possible as we just saw.
Exercises
Exercise 1: Circle class
Create a class Circle with a private data member radius of type double. Implement a public member function setRadius to set the radius of the circle and a public member function getArea to calculate and return the area of the circle. Make sure that the getArea function is a const function as it does not modify the state of the object.
Solution
class Circle {
private:
double radius;
public:
void setRadius(double r) {
radius = r;
}
double getArea() const {
return 3.14159 * radius * radius;
}
};
Exercise 2: Car class
Create a class Car with a private data member speed of type int. Implement a public member function drive that takes an int parameter time and increases the speed of the car by time. Implement a public member function getSpeed to return the current speed of the car. Make sure that the drive function is a const function as it does not modify the state of the object.
Solution
class Car {
private:
int speed;
public:
void drive(int time) {
speed += time;
}
int getSpeed() const {
return speed;
}
};
Exercise 3: Person class
Create a class Person with private data members name and age of type string and int respectively. Implement a public member function getName to return the name of the person and a public member function getAge to return the age of the person. Make sure that both the getName and getAge functions are const functions as they do not modify the state of the object.
Solution
class Person {
private:
string name;
int age;
public:
string getName() const {
return name;
}
int getAge() const {
return age;
}
};
Exercise 4: Rectangle
Create a class Rectangle with private data members length and width of type double. Implement a public member function setDimensions to set the dimensions of the rectangle and a public member function getPerimeter to calculate and return the perimeter of the rectangle. Make sure that the getPerimeter function is a const function as it does not modify the state of the object.
Solution
class Rectangle {
private:
double length;
double width;
public:
void setDimensions(double l, double w) {
length = l;
width = w;
}
double getPerimeter() const {
return 2 * (length + width);
}
};
Exercise 5: Book class
Create a class Book with private data members title and author of type string. Implement public member functions getTitle and getAuthor to return the title and author of the book respectively. Make sure that both the getTitle and getAuthor functions are const functions as they do not modify the state of the object.
Solution
class Book {
private:
string title;
string author;
public:
string getTitle() const {
return title;
}
string getAuthor() const {
return author;
}
};
static class members
A static member variable is a member variable that belongs to the class itself rather than to instances of the class. In other words, there is only one instance of a static member variable, regardless of how many objects of the class are created. A static member variable is declared using the static keyword inside the class definition, like this:
class MyClass {
public:
static int myStaticMember;
// ...
};
The static member variable can then be defined and initialised outside the class definition, like this:
int MyClass::myStaticMember = 0;
A static member function is a member function that is associated with the class rather than with individual objects of the class. Like a static member variable, a static member function is declared using the static keyword inside the class definition. A static member function can be called using the class name and the scope resolution operator, like this:
class MyClass {
public:
static void myStaticFunction();
// ...
};
void MyClass::myStaticFunction() {
// ...
}
int main() {
MyClass::myStaticFunction();
return 0;
}
The main advantage of using static class members is that they can be accessed without creating an instance of the class. This is particularly useful for defining constants that are associated with the class, or for implementing utility functions that operate on the class. By using a static member variable or function, you can avoid the overhead of creating unnecessary objects, and you can make the code more readable by indicating that the variable or function is associated with the class as a whole rather than with individual objects.
Exercises: static members
Create a class named “Person” with the following private members:
- A static integer member variable named “count”.
- A string member variable named “name”.
- An integer member variable named “age”.
- The “Person” class should have the following public member functions:
A constructor that takes two parameters: a string “name” and an integer “age”. This constructor should increment the “count” static member variable by 1 each time it is called.
- A destructor that decrements the “count” static member variable by 1 each time it is called.
- A function named “getCount” that returns the value of the “count” static member variable.
- A function named “getName” that returns the value of the “name” member variable.
- A function named “getAge” that returns the value of the “age” member variable.
Create a program that creates several “Person” objects with different names and ages, and then displays the total count of “Person” objects using the “getCount” member function. Finally, display the names and ages of all the “Person” objects using the “getName” and “getAge” member functions.
Example code:
#include <iostream>
#include <string>
class Person {
private:
static int count;
std::string name;
int age;
public:
Person(std::string n, int a) {
name = n;
age = a;
count++;
}
~Person() {
count--;
}
static int getCount() {
return count;
}
std::string getName() {
return name;
}
int getAge() {
return age;
}
};
int Person::count = 0;
int main() {
Person p1("Alice", 25);
Person p2("Bob", 30);
Person p3("Charlie", 35);
std::cout << "Total count of Person objects: " << Person::getCount() << std::endl;
std::cout << "Names and ages of all Person objects:" << std::endl;
std::cout << p1.getName() << " (" << p1.getAge() << ")" << std::endl;
std::cout << p2.getName() << " (" << p2.getAge() << ")" << std::endl;
std::cout << p3.getName() << " (" << p3.getAge() << ")" << std::endl;
return 0;
}
Structs vs Classes
Both structs and classes are used to define custom data types, but there are some differences between them.
- Default Access Specifier: The default access specifier for members of a struct is public, whereas for a class, it is private.
- Inheritance: By default, inheritance of a struct is public, whereas for a class, it is private.
- Member Functions: A struct can have member functions defined inside it or outside it, whereas for a class, member functions are typically defined outside of the class declaration using the scope resolution operator (::).
- Usage: Structs are often used for simple data structures, where the emphasis is on the data itself rather than on the behaviour of the data. On the other hand, classes are often used for more complex data structures that have both data and behaviour.
Here’s an example to demonstrate the differences between struct and class:
#include <iostream>
// struct example
struct Person {
std::string name;
int age;
};
// class example
class Car {
private:
std::string brand;
std::string model;
public:
void setBrand(std::string b) {
brand = b;
}
void setModel(std::string m) {
model = m;
}
void printInfo() {
std::cout << "Brand: " << brand << "\nModel: " << model << std::endl;
}
};
int main() {
// struct example
Person p1;
p1.name = "John";
p1.age = 25;
std::cout << "Name: " << p1.name << "\nAge: " << p1.age << std::endl;
// class example
Car c1;
c1.setBrand("Toyota");
c1.setModel("Camry");
c1.printInfo();
return 0;
}
In this example, we define a Person struct and a Car class. We create an object of each type and set their member variables using their respective member functions. We then print out the member variables using std::cout. This example demonstrates how the syntax for defining and using structs and classes is similar, but the differences between them lie in their default access specifiers, inheritance, and usage.
Friends of a class
A friend of a class is a function or another class that is given special permission to access the private and protected members of the class. It is declared within the class definition and defined outside of the class definition. There are two types of friends:
- Function Friend: A non-member function can be declared as a friend of a class.
- Class Friend: A class can also be declared as a friend of another class. This allows the friend class to access the private and protected members of the other class.
Here’s an example to illustrate the concept of friend classes:
#include <iostream>
// forward declaration of class B
class B;
// class A
class A {
private:
int x;
public:
A() {
x = 0;
}
void setX(int a) {
x = a;
}
// declaration of friend function
friend void sum(A obj1, B obj2);
};
// class B
class B {
private:
int y;
public:
B() {
y = 0;
}
void setY(int b) {
y = b;
}
// declaration of friend function
friend void sum(A obj1, B obj2);
};
// definition of friend function
void sum(A obj1, B obj2) {
std::cout << "Sum of x and y is " << obj1.x + obj2.y << std::endl;
}
int main() {
A obj1;
B obj2;
obj1.setX(10);
obj2.setY(20);
sum(obj1, obj2);
return 0;
}
In this example, class A and class B are two independent classes. The friend function sum is declared in both classes, allowing it to access the private data members of both classes. The sum function takes objects of class A and B as arguments and calculates the sum of their private data members x and y. The main function creates objects of class A and B, sets their private data members, and calls the sum function to print the sum of x and y.
In summary, friends of a class provide a way to share private and protected data between classes or functions. However, it should be used with caution as it can lead to a loss of encapsulation and increase coupling between classes.
Section exercise
Consider the following requirements for implementing a simple bank account management system:
- A BankAccount class needs to be defined that stores the following information:
- Account number (integer)
- Account holder name (string)
- Account type (string)
- Account balance (float)
- The BankAccount class should have the following member functions:
- Constructor that takes account number, account holder name, account type and account balance as arguments and initialises the member variables.
- Destructor that deallocates any dynamically allocated memory.
- Setter and getter functions for all member variables.
- Function to deposit money into the account.
- Function to withdraw money from the account.
- Function to display the account details.
- The BankAccount class should also have a static member variable that keeps track of the total number of accounts created.
- The BankAccount class should also have a friend function named transferMoney that transfers money from one bank account to another.
- The BankAccount class should also have a move constructor and a move assignment operator that transfers the ownership of dynamically allocated memory from one object to another.
- The BankAccount class should ensure that the account balance cannot be negative.
- The BankAccount class should also have a default constructor that sets all member variables to default values.
- The BankAccount class should use constructor initializer lists wherever possible.
- The BankAccount class should have a static member function that returns the total number of accounts created.
- The BankAccount class should have a const member function that displays the account details.
Implement the BankAccount class according to the above requirements and test it with a driver program that creates at least two BankAccount objects, transfers money between them, displays the account details, and displays the total number of accounts created. Use dynamic memory allocation wherever required.
Bonus: Implement the BankAccount class as a template class that can store account balance in different data types (float, double, int, etc.).