Class is a user-defined type. A class consists of members. The members are data members and member functions. A class can be described as data and some functionality on that data, wrapped into one. An instance of a class is called an object. To only declare a class name, we
write:To define an empty class, we add a class body marked by braces
{}:
To create an instance of the class, an object, we use:
Explanation
We defined a class called MyClass. Then we created an object o of type MyClass. It is said that o is an object, a class instance.
23.1 Data Member Fields
A class can have a set of some data in it. These are called
member fields. Let us add one
member field
to our class and make it of type
char:
Now our class has one data member field of type
char called
c. Let us now add two more fields of type
int and
double:
class MyClass
{
char c;
int x;
double d;
};
Now our class has three member fields, and each member field has its name.
23.2 Member Functions
Similarly, a class can store functions. These are called
member functions. They are mostly used to perform some operations on data fields. To declare a
member function
of type void called
dosomething(), we write:
class MyClass
{
void dosomething();
};
There are two ways to define this member function. The first is to define it inside the class:
class MyClass
{
void dosomething()
{
std::cout << "Hello World from a class.";
}
};
The second one is to define it
outside the class. In that case, we write the function type first, followed by a class name, followed by a scope resolution :: operator followed by a function name, list of parameters if any and a function body:
class MyClass
{
void dosomething();
};
void MyClass::dosomething()
{
std::cout << "Hello World from a class.";
}
Here we declared a member function inside the class and defined it outside the class.
We can have multiple members functions in a class. To define them inside a class, we would write:
class MyClass
{
void dosomething()
{
std::cout << "Hello World from a class.";
}
void dosomethingelse()
{
std::cout << "Hello Universe from a class.";
}
};
To declare members functions inside a class and define them outside the class, we would write:
class MyClass
{
void dosomething();
void dosomethingelse();
};
void MyClass::dosomething()
{
std::cout << "Hello World from a class.";
}
void MyClass::dosomethingelse()
{
std::cout << "Hello Universe from a class.";
}
Now we can create a simple class that has both a data member field and a
member function:
class MyClass
{
int x;
void printx()
{
std::cout << "The value of x is:" << x;
}
};
This class has one data field of type int called x, and it has a member function called printx().
This member function reads the value of x and prints it out. This example is an introduction to member access specifiers or class member visibility.
23.3 Access Specifiers
Wouldn’t it be convenient if there was a way we could disable access to member fields but allow access to member functions for our object and other entities accessing our class members? And that is what access specifiers are for. They specify access for class members. There are three
access specifiers/labels
: public, protected, and private:
class MyClass
{
public:
// everything in here
// has public access level
protected:
// everything in here
// has protected access level
private:
// everything in here
// has private access level
};
Default visibility/access specifier for a class is
private if none of the access specifiers is present:
class MyClass
{
// everything in here
// has private access by default
};
Another way to write a class is to write a
struct. A struct is also a
class in which members have
public access by default. So, a
struct is the same thing as a
class but with a
public access specifier by default:
struct MyStruct
{
// everything in here
// is public by default
};
For now, we will focus only on public and private access specifiers. Public access members are accessible anywhere. For example, they are accessible to other class members and to objects of our class. To access a class member from an object, we use the dot . operator.
Let’s define a class where all the members have public access. To define a class with public access specifier, we can write:
class MyClass
{
public:
int x;
void printx()
{
std::cout << "The value of x is:" << x;
}
};
Let us instantiate this class and use it in our main program:
class MyClass
{
public:
int x;
void printx()
{
std::cout << "The value of data member x is: " << x;
}
};
int main()
{
MyClass o;
o.x = 123; // x is accessible to object o
o.printx(); // printx() is accessible to object o
}
Our object o now has direct access to all member fields as they are all marked public. Member fields always have access to each other regardless of the access specifier. That is why the member function printx() can access the member field x and print or change its value.
Private access members are accessible only to other class members, not objects. Example with full commentary:
class MyClass
{
private:
int x; // x now has private access
public:
void printx()
{
std::cout << "The value of x is:" << x; // x is accessible to // printx()
}
};
int main()
{
MyClass o; // Create an object
o.x = 123; // Error, x has private access and is not accessible to // object o
o.printx(); // printx() is accessible from object o
}
Our object o now only has access to a member function printx() in the public section of the class. It cannot access members in the private section of the class.
If we want the class members to be accessible to our object, then we will put them inside the public: area. If we want the class members not to be accessible to our object, then we will put them into the private: area.
We want the data members to have private access and function members to have public access. This way, our object can access the member functions directly but not the member fields. There is another access specifier called protected: which we will talk about later in the book when we learn about inheritance.
23.4 Constructors
A constructor is a member function that has the same name as the class. To initialize an object of a class, we use constructors. Constructor's purpose is to initialize an object of a class. It constructs an object and can set values to data members. If a class has a constructor, all objects of that class will be initialized by a constructor call.
23.4.1 Default Constructor
A
constructor without parameters or with default parameters set is called a
default constructor. It is a constructor which can be called without arguments:
class MyClass
{
public:
MyClass()
{
std::cout << "Default constructor invoked." << '
';
}
};
int main()
{
MyClass o; // invoke a default constructor
}
Another example of a default constructor, the one with the default arguments:
class MyClass
{
public:
MyClass(int x = 123, int y = 456)
{
std::cout << "Default constructor invoked." << '
';
}
};
int main()
{
MyClass o; // invoke a default constructor
}
If a default constructor is not explicitly defined in the code, the compiler will generate a default constructor. But when we define a constructor of our own, the one that needs parameters, the default constructor gets removed and is not generated by a compiler.
Constructors are invoked when object initialization takes place. They can’t be invoked directly.
Constructors can have arbitrary parameters; in which case we can call them
user-provided constructors:
class MyClass
{
public:
int x, y;
MyClass(int xx, int yy)
{
x = xx;
y = yy;
}
};
int main()
{
MyClass o{ 1, 2 }; // invoke a user-provided constructor
std::cout << "User-provided constructor invoked." << '
';
std::cout << o.x << ' ' << o.y;
}
In this example, our class has two data fields of type int and a constructor. The constructor accepts two parameters and assigns them to data members. We invoke the constructor with by providing arguments in the initializer list with MyClass o{ 1, 2 };
Constructors do not have a return type, and their purposes are to initialize the object of its class.
23.4.2 Member Initialization
In our previous example, we used a constructor body and
assignments to assign value to each class
member. A better, more efficient way to initialize an object of a class is to use the constructor’s
member initializer list in the definition of the constructor:
class MyClass
{
public:
int x, y;
MyClass(int xx, int yy)
: x{ xx }, y{ yy } // member initializer list
{
int main()
{
MyClass o{ 1, 2 }; // invoke a user-defined constructor
std::cout << o.x << ' ' << o.y;
}
A member initializer list starts with a colon, followed by member names and their initializers, where each initialization expression is separated by a comma. This is the preferred way of initializing class data members.
23.4.3 Copy Constructor
When we initialize an object with another object of the same class, we invoke a copy constructor. If we do not supply our
copy constructor, the compiler generates a default copy constructor that performs the so-called shallow copy. Example:
class MyClass
{
private:
int x, y;
public:
MyClass(int xx, int yy) : x{ xx }, y{ yy }
{
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // default copy constructor invoked
}
In this example, we initialize the object o2 with the object o1 of the same type. This invokes the default copy constructor.
We can provide our own copy constructor. The copy constructor has a special parameter signature of
MyClass(const MyClass& rhs). Example of a user-defined copy constructor:
class MyClass
{
private:
int x, y;
public:
MyClass(int xx, int yy) : x{ xx }, y{ yy }
{
}
// user defined copy constructor
MyClass(const MyClass& rhs)
: x{ rhs.x }, y{ rhs.y } // initialize members with other object's // members
{
std::cout << "User defined copy constructor invoked.";
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // user defined copy constructor invoked
}
Here we defined our own copy constructor in which we explicitly initialized data members with other objects data members, and we print out a simple message in the console / standard output.
Please note that the default copy constructor does not
correctly copy members of some types, such as pointers, arrays, etc. In order to properly make copies, we need to define our own copy logic inside the copy constructor. This is referred to as a
deep copy.
For pointers, for example, we need both to create a pointer and assign a value to the object it points to in our user-defined copy constructor:
class MyClass
{
private:
int x;
int* p;
public:
MyClass(int xx, int pp)
: x{ xx }, p{ new int{pp} }
{
}
MyClass(const MyClass& rhs)
: x{ rhs.x }, p{ new int {*rhs.p} }
{
std::cout << "User defined copy constructor invoked.";
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // user defined copy constructor invoked
}
Here we have two constructors, one is a user-provided regular constructor, and the other is a user-defined copy constructor. The first constructor initializes an object and is invoked here: MyClass o1{ 1, 2 }; in our main function.
The second, the user-defined copy constructor is invoked here: MyClass o2 = o1; This constructor now properly copies the values from both int and int* member fields.
In this example, we have pointers as member fields. If we had left out the user-defined copy constructor, and relied on a default copy constructor only the int member field would be properly copied, the pointer would not. In this example, we rectified that.
In addition to copying, there is also a move semantic, where data is moved from one object to the other. This semantic is represented through a move constructor and a move assignment operator.
23.4.4 Copy Assignment
So far, we have used copy constructors to initialize one object with another object. We can also copy the values to an object after it has been initialized/created. We use a
copy assignment for that. Simply, when we initialize an object with another object using the = operator on the same line, then the copy operation uses the copy constructor:
MyClass copyto = copyfrom; // on the same line, uses a copy constructor
When an object is created on one line and then assigned to in the next line, it then uses the
copy assignment operator to copy the data from another object:
MyClass copyto;
copyto = copyfrom; // uses a copy assignment operator
A copy assignment operator is of the following signature:
MyClass& operator=(const MyClass& rhs)
To define a user-defined copy assignment operator inside a class we use:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
};
Notice that the overloaded = operators must return a dereferenced this pointer at the end. To define a user-defined copy assignment operator outside the class, we use:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs);
};
MyClass& MyClass::operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
Similarly, there is a move assignment operator, which we will discuss later in the book. More on operator overloading in the following chapters.
23.4.5 Move Constructor
In addition to copying, we can also move the data from one object to the other. We call it a move semantics. Move semantics is achieved through a move constructor and move assignment operator. The object from which the data was moved, is left in some valid but unspecified state. The move operation is efficient in terms of speed of execution, as we do not have to make copies.
Move constructor accepts something called rvalue reference as an argument.
Every expression can find itself on the left-hand side or the right-hand side of the assignment operator. The expressions that can be used on the left-hand side are called lvalues, such as variables, function calls, class members, etc. The expressions that can be used on the right-hand side of an assignment operator are called rvalues, such as literals, and other expressions.
Now the move semantics accepts a reference to that rvalue. The signature of an rvalue reference type is
T&&, with double reference symbols. So, the signature of a move constructor is:
To cast something to an rvalue reference, we use the
std::move function. This function casts the object to an rvalue reference. It does not move anything. An example where a move constructor is invoked:
int main()
{
MyClass o1;
MyClass o2 = std::move(o1);
std::cout << "Move constructor invoked.";
// or MyClass o2{std::move(o1)};
}
In this example, we define an object of type MyClass called o1. Then we initialize the second object o2 by moving everything from object o1 to o2. To do that, we need to cast the o2 to rvalue reference with std::move(o1). This, in turn, invokes the MyClass move constructor for o2.
If a user does not provide a move constructor, the compiler provides an implicitly generated default move constructor.
Let us specify our own, user-defined move constructor:
#include <iostream>
#include <string>
class MyClass
{
private:
int x;
std::string s;
public:
MyClass(int xx, std::string ss) // user provided constructor
: x{ xx }, s{ ss }
{}
MyClass(MyClass&& rhs) // move constructor
:
x{ std::move(rhs.x) }, s{ std::move(rhs.s) }
{
std::cout << "Move constructor invoked." << '
';
}
};
int main()
{
MyClass o1{ 1, "Some string value" };
MyClass o2 = std::move(o1);
}
This example defines a class with two data members and two constructors. The first constructor is some user-provided constructor used to initialize data members with provided arguments.
The second constructor is a user-defined move constructor accepting an rvalue reference parameter of type MyClass&& called rhs. This parameter will become our std::move(o1) argument/object. Then in the constructor initializer list, we also use the std::move function to move the data fields from o1 to o2.
23.4.6 Move Assignment
Move assignment operator is invoked when we declare an object and then try to assign an rvalue reference to it. This is done via the move assignment operator. The signature of the move assignment operator is: MyClass& operator=(MyClass&& otherobject).
To define a user-defined move assignment operator inside a class we use:
class MyClass
{
public:
MyClass& operator=(MyClass&& otherobject)
{
// implement the copy logic here
return *this;
}
};
As with any assignment operator overloading, we must return a dereferenced this pointer at the end. To define a move assignment operator outside the class, we use:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs);
};
MyClass& MyClass::operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
Move assignment operator example adapted from a move constructor example would be:
#include <iostream>
#include <string>
class MyClass
{
private:
int x;
std::string s;
public:
MyClass(int xx, std::string ss) // user provided constructor
: x{ xx }, s{ ss }
{}
MyClass& operator=(MyClass&& otherobject) // move assignment operator
{
x = std::move(otherobject.x);
s = std::move(otherobject.s);
return *this;
}
};
int main()
{
MyClass o1{ 123, "This is currently in object 1." };
MyClass o2{ 456, "This is currently in object 2." };
o2 = std::move(o1); // move assignment operator invoked
std::cout << "Move assignment operator used.";
}
Here we defined two objects called o1 and o2. Then we try to move the data from object o1 to o2 by assigning an rvalue reference (of object o1) using the std::move(o1) expression to object o2. This invokes the move assignment operator in our object o2. The move assignment operator implementation itself uses the std::move() function to cast each data member to an rvalue reference.
23.5 Operator Overloading
Objects of
classes can be used in expression as operands. For example, we can do the following:
myobject = otherobject;
myobject + otherobject;
myobject / otherobject;
myobject++;
++myobject;
Here objects of a class are used as operands. To do that, we need to overload the operators for complex types such as classes. It is said that we need to overload them to provide a meaningful operation on objects of a class. Some operators can be overloaded for classes; some cannot. We can overload the following operators:
Arithmetic operators, binary operators, boolean operators, unary operators, comparison operators, compound operators, function and subscript operators:
+ - * / % ^ & | ~ ! = < > == != <= >= += -= *= /= %= ^= &= |= << >> >>= <<= && || ++ -- , ->* -> () []
Each operator carries its signature and set of rules when
overloading for classes. Some operator overloads are implemented as member functions, some as none member functions. Let us overload a unary
prefix ++ operator for classes. It is of signature
MyClass& operator++():
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass()
: x{ 0 }, d{ 0.0 }
{
}
// prefix operator ++
MyClass& operator++()
{
++x;
++d;
std::cout << "Prefix operator ++ invoked." << '
';
return *this;
}
};
int main()
{
MyClass myobject;
// prefix operator
++myobject;
// the same as:
myobject.operator++();
}
In this example, when invoked in our class, the overloaded prefix increment ++ operator increments each of the member fields by one. We can also invoke an operator by calling a .operatoractual_operator_name(parameters_if_any); such as .operator++();
Often operators depend on each other and can be implemented in terms of other operators. To implement a
postfix operator ++, we will implement it in terms of a prefix operator:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass()
: x{ 0 }, d{ 0.0 }
{
}
// prefix operator ++
MyClass& operator++()
{
++x;
++d;
std::cout << "Prefix operator ++ invoked." << '
';
return *this;
}
// postfix operator ++
MyClass operator++(int)
{
MyClass tmp(*this); // create a copy
operator++(); // invoke the prefix operator overload
std::cout << "Postfix operator ++ invoked." << '
';
return tmp; // return old value
}
};
int main()
{
MyClass myobject;
// postfix operator
myobject++;
// is the same as if we had:
myobject.operator++(0);
}
Please do not worry too much about the somewhat inconsistent rules for operator overloading. Remember, each (set of) operator has its own rules for overloading.
Let us overload a binary operator
+=:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
MyClass& operator+=(const MyClass& rhs)
{
this->x += rhs.x;
this->d += rhs.d;
return *this;
}
};
int main()
{
MyClass myobject{ 1, 1.0 };
MyClass mysecondobject{ 2, 2.0 };
myobject += mysecondobject;
std::cout << "Used the overloaded += operator.";
}
Now, myobject member field x has a value of 3, and a member field d has a value of 3.0.
Let us implement arithmetic
+ operator in terms of
+= operator:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
MyClass& operator+=(const MyClass& rhs)
{
this->x += rhs.x;
this->d += rhs.d;
return *this;
}
friend MyClass operator+(MyClass lhs, const MyClass& rhs)
{
lhs += rhs;
return lhs;
}
};
int main()
{
MyClass myobject{ 1, 1.0 };
MyClass mysecondobject{ 2, 2.0 };
MyClass myresult = myobject + mysecondobject;
std::cout << "Used the overloaded + operator.";
}
Summary:
When we need to perform arithmetic, logic, and other operations on our objects of a class, we need to overload the appropriate operators. There are rules and signatures for overloading each operator. Some operators can be implemented in terms of other operators. For a complete list of rules of operator overloading rules, please refer to C++ reference at https://en.cppreference.com/w/cpp/language/operators.
23.6 Destructors
As we saw earlier, a constructor is a member function that gets invoked when the object is initialized. Similarly, a destructor is a member function that gets invoked when an object is destroyed. The name of the
destructor
is tilde ~ followed by a class name:
class MyClass
{
public:
MyClass() {} // constructor
~MyClass() {} // destructor
};
Destructor takes no parameters, and there is one destructor per class. Example:
class MyClass
{
public:
MyClass() {} // constructor
~MyClass()
{
std::cout << "Destructor invoked.";
} // destructor
};
int main()
{
MyClass o;
} // destructor invoked here, when o gets out of scope
Destructors are called when an object goes out of scope or when a pointer to an object is deleted. We should not call the destructor directly.
Destructors can be used to clean up the taken resources. Example:
class MyClass
{
private:
int* p;
public:
MyClass()
: p{ new int{123} }
{
std::cout << "Created a pointer in the constructor." << '
';
}
~MyClass()
{
delete p;
std::cout << "Deleted a pointer in the destructor." << '
';
}
};
int main()
{
MyClass o; // constructor invoked here
} // destructor invoked here
Here we allocate memory for a pointer in the constructor and deallocate the memory in the destructor. This style of resource allocation/deallocation is called RAII or Resource Acquisition is Initialization. Destructors should not be called directly.