Knowledge Dump

Classes

In this article, classes, their different types and properties in C++ (version 17) are covered. Classes are objects that encapsulate other objects and functions, their so-called members. A class can be declared by using the class,struct or union keywords.

Contents

class / struct
While very closely related, a class usually is used for bigger, closed objects, while structs are for less complex and more open ones. The only actual difference is in the default setting for their contents: The members of a struct are set as public by default, while those of a class are private, unless specified otherwise. In the following sections, we will simply proceed to use the keyword class, even though struct could be used similarly.
Example for class declaration:
class classname {
	int member1;				//Members are private by default.
	char member2;
	int foo() const { return member1; }	//Semicolons after function definitions optional.
	//etc.
};						//Very important: Semicolon after class definition!
Objects of class "classname" can be declared with classname object;. The members can then be accessed, by adding a . after the object's identifier, e.g. object.member1 = 1; or object.foo();. The const tag signifies that calling this member function doesn't affect the object itself. It is required, to be able to call this function with a const class object.
Pointers can also be defined as usual: classname *p = &object;. To access members with it, we can either dereference it with *, e.g. (*p).foo();, or use the special member access operator ->, e.g. p->foo();. It should be noted, that (*p).foo() is different from *p.foo(), which is evaluated as *(p.foo()) and makes no sense in our context.

public / protected / private
The members of a class can be declared as public, protected or private. These keywords are called access specifiers and affect the accessibility of members outside of the class and for derived subclasses (see inheritance).
  • public: Members are directly accessible everywhere, where the object is visible.
  • protected: Members are only accessible to members of the same class or derived classes.
  • private: Members are only accessible to members of the same class.
Note that private/protected members are not just accessible to the member functions of the object itself, but also to all member functions of other objects of the same class. Example:
class class1 {			//Class is named class1.
public:
	void foo(class1 &obj, int a) const { obj.value1 = a; }	//Changes protected value1 member from any class1 object.
	void bar(class1 &obj, int b) const { obj.value2 = b; }	//Class1 member functions can access any member of class1 objects.
	int value0;		//public value
protected:
	int value1;		//protected value
private:
	int value2;		//private value
};

class class2 : public class1 {	//Derived class, which simply contains every member class1 has.
	void foo2(class2 &obj, int a) const { obj.value1 = a; }		//Members of derived classes can access protected members.
	//void bar2(class2 &obj, int b) const { obj.value2 = b; }	//Members of derived classes can't access private members.
};

int main()
{
	class1 object1;
	object1.value0 = 0;		//Public members can be directly accessed and manipulated.
	//object1.value1 = 0;		//Direct access doesn't work for protected/private values.

	return 0;
}
A notable exception to the above restrictions are functions marked as friends, or member functions from friend classes, which can access not just the public members, but also the protected and private ones. Note: The friend tag for classes doesn't work for mutual access, unless both tag the other class with the friend keyword. Example:
//Declare classes.
class class1;
class class2;

class class1 {
	//Declare function foo and class class2 as friends of class1.
	friend void foo(class1 &obj);
	friend class2;		//Important: class2 needs to be declared already.
public:
	int value0;
protected:
	int value1;
private:
	int value2;
};

class class2 {			//class2 is friend of class1. class1 is not friend of class2.
public:
	void bar(class1 &obj);	//Member function takes a class1 object and can access all its values.
private:
	int value;		//class1 members can't access this value.
};


//Define class2 member function bar. It can access all values of class1, since it's a friend class.
void class2::bar(class1 &obj) {
	obj.value0 = obj.value1 = obj.value2 = 1;	//Set all value members from a class1 object to 1.
}

//Define friend function. Note: It's not a member of class1.
void foo(class1 &obj) {
	obj.value0 = obj.value1 = obj.value2 = 0;	//friend function can access any value, even private ones.
}

Constructors and Destructors
Since classes are assembled objects, declaring and initializing them is not as simple as for fundamental data types. Thus, in order to create a class object, special functions are used, called constructors. To remove them at the end of their lifetime, destructors are called.
Basic Constructors
Constructors have to be declared inside the class description, simply by writing the classes name, followed by the input arguments required to build the class. A constructor without any arguments is also called default constructor. As most functions, constructors can be overloaded.
When the compiler receives no constructors (including no copy/move constructors) for a class, it creates a default constructor by itself. If other constructors with input arguments are present, such a constructor can also be created manually, by adding classname(){}; or classname() = default; to the class member list.
Example:
class class1 {
public:
	class1() {}		//Trivial default constructor. Alternative: class1() = default;
	class1(int a) { value = a; }
	class1(short a) { value = a; }
	int value;
};
When a constructor initializes members by simply setting them as the input arguments, an alternative syntax can be used. Example:
class class1 {
public:
	class1(char c) : value1(c) {}	//Alternative initialization syntax. Only value1 initialized as char c.
	class1(int a) : value0(a), value1('a'), value2((short)0) {}	//Full initialization with one input and default values.
	class1(int a, char b, short c) : value0(a), value1(b), value2(c) {}	//Full initialization, three input arguments.
private:
	int value0;
	char value1;
	short value2;
};
Destructors
In order to remove class objects, the destructor member function is called. Since this is an essential task, every class must have a destructor. When none is declared, a default destructor is used, which simply removes all members. This is not sufficient in many cases though, particularly with members pointing to dynamically allocated data. When the default destructor is called for such an object, the pointer member is removed, but the underlying allocated memory remains. It becomes inaccessible if no backup pointer exists, leading to a memory leak.
Destructors can be declared similarly to default constructors, except that there is a prefix tilde sign ~. Example:
class class1 {
public:
	class1() : p(new int) {}	//Default constructor sets p to point to a dynamically assigned integer.
	~class1() { delete p; }		//Custom destructor also frees dynamically allocated memory.
private:
	int *p;
};
Other Constructors
Besides using normal constructors to create objects from scratch, there are other ways to create class objects: By copying an existing one, or moving the contents of a temporary object to the new one.
The copy-process can either be initiated by a copy constructor, or a copy assignment. Copy constructors usually take the form class1(class1 &obj);, while copy assignments are initiated by overloading the assignment operator = (see operator overloads), for example class1& operator= (const class1 &obj);. Note, that the copy constructor creates a new object, opposed to the assignment operator, which changes an already existing one.
When these constructors are not defined (and also no move constructors or move assignment operator), the compiler implicitly creates the default versions, which simply copy the member values from one object to the other. Obviously, this may lead to problems. For example, when some members are pointers, the copied object's members will point to the same destination, which is possibly undesired – particularly, when dynamic memory management is involved. Example solving this issue:
class class1 {
public:
	class1() : p(new int) {}	//Default constructor sets p to point to a dynamically allocated integer. Uninitialized.
	~class1() { delete p; }		//Destructor.

	//Copy constructor dynamically allocates int and copies integer value.
	class1(const class1 &obj) : p(new int) { *p = *(obj.p); }
	//Assignment operator dereferences pointers and copies integer value. *this returns the object itself.
	class1& operator=(const class1 &obj) { *p = *(obj.p); return *this; }
private:
	int *p;
};

int main()
{
	class1 obj1;
	class1 obj2(obj1);		//Copy constructor called.
	class1 obj3 = obj2;		//Copy constructor called.
	class1 obj4;
	obj4 = obj3;			//Copy assignment.

	return 0;
}
Since the assignment operator is supposed to return the object that is calling it, the dereferenced this pointer is returned. When used inside member functions of a class, it points to the class object itself.

Some objects, like e.g. returned values from functions or type-casts, are temporary by nature. When the original object can be discarded after the construction/assignment process, ownership over its memory blocks can simply be transferred to the other object, which is more efficient than normal copying. For this purpose, move constructors and move assignments are used, which are declared similarly to their copy counterparts, except that the input is an rvalue reference, i.e. two ampersands && instead of one and the arguments aren't declared const. Example:
#include <iostream>

class class1 {
public:
	class1() : p(new int) {}	//Default constructor sets p, to point to a dynamically assigned integer.
	~class1() { delete p; }		//Destructor.

	//Move constructor. "Ownership" of pointer destination is transferred, original pointer is declared nullptr.
	class1(class1 &&obj) : p(obj.p) { std::cout << "move1"; obj.p = nullptr; }	//Prints "move1" when called.
	//Move assignment operator. Prints "move2" when called.
	class1& operator=(class1 &&obj) { std::cout << "move2"; p = obj.p; obj.p = nullptr; return *this; }
private:
	int *p;
};


int main()
{
	class1 obj((class1()));	//Could call move constructor, but usually return value optimization makes it obsolete.
				//Equivalent to  class1 obj = class1();
	obj = class1();		//Calls move assignment operator, prints "move2" to console.

	return 0;
}


Lastly, it should be mentioned that the "trivial" default constructors, assignment operators and destructors can either be manually implemented, by adding a = default; at the end of the respective declaration, or removed with a = delete; suffix. Example:
class class1 {	//Class that only contains default constructor and destructor.
public:
	class1() = default;	//Default constructor exists. (at least one constructor type required)
	~class1() = default;	//Default destructor, too. (destructor is obligatory)
	class1(const class1&) = delete;			//No copy constructor.
	class1(class1&&) = delete;			//No move constructor.
	class1& operator=(const class1&) = delete;	//No copy assignment operator.
	class1& operator=(class1&&) = delete;		//No move assignment operator.
};

Inheritance
Classes can pass down their members to derived classes through inheritance. As mentioned in the access specifier section, members declared in the derived class can access the members from the base class, which were declared either public or protected. Even though private members of the base class are also inherited to the derived class, they can only be accessed by members of the base class (or friend classes of the base).
The syntax to define a derived class is e.g. class class2 : public class1 {};, where class2 is a class derived from class1. When a derived class is to inherit the members of several base classes, one can write class class3 : public class1, public class2 {};, where class3 inherits the members of class1 and class2. The role of the public keyword will be explained later on.
To create an object of a derived class, first the default constructor (unless specified otherwise) of the base class is called and only then the constructor of the derived class. The destructor works similarly, except in the opposite order. Example:
#include <iostream>

class class1 {		
public:
	//Default constructor initializes a,b,c to 1,2,3 and prints message when called.
	class1() : a(1), b(2), c(3) { std::cout << "class1 default constructor called." << std::endl; }
	int a;
	void foo() const { std::cout << a << b << c << std::endl; }	//class1 member functions can access any member of it.
protected:
	int b;
private:
	int c;
};

//Derived class class2 contains all members of class1, besides constructors, destructor and assignment operators.
class class2 : public class1 {	//Constructors etc. are implicitly called by their class2 equivalent though.
public:		
	//Default constructor from class1 is implicitly called and initializes a,b,c.
	class2() { std::cout << "class2 default constructor called." << std::endl; };
	//class2 member functions can only access public/protected members inherited from class1.
	void bar() const { std::cout << a << b << std::endl; }
};


int main()
{
	class1 obj1;	//Calls class1 default constructor.
	obj1.foo();	//Prints initialized values of a,b,c. (123)
	class2 obj2;	//Calls class1 and class2 default constructors.
	obj2.bar();	//Prints initialized values of a,b. c is private and can't be accessed by class2 member functions.
	obj2.foo();	//class2 objects can use class1 members to access inherited private members though. Prints 123.

	return 0;
}
Output:
class1 default constructor called.
123
class1 default constructor called.
class2 default constructor called.
12
123
When the base class constructor is supposed to take parameters, instead of using the default constructor, the derived class constructor can pass on a directive, which parameters are to be used. Example:
class class1 {		
public:
	class1(int x, int y, int z) : a(x), b(y), c(z) {}	//class1 Constructor with parameters.
	int a;
protected:
	int b;
private:
	int c;
};

class class2 : public class1 {
public:		
	//Default constructor for class2, which passes the fixed parameters 0,0,0 on to the class1 constructor. 
	class2() : class1(0, 0, 0) {}
	//Constructor with three parameters, which are passed on to the class1 constructor.
	class2(int x, int y, int z) : class1(x,y,z){}
};


Besides inheritance with the public keyword, which basically just adds the members of the base class to the derived class, using the same member access specifications as in the original, there is also protected and private inheritance. To declare this inheritance type, one simply uses the respective keyword, e.g. class class2 : protected class1 {}; for a derived class class2 with base class1. When no keyword is used, classes assume private and structs public inheritance. Depending on the keyword used, the members of the base class will have differing access specifications in the derived class:
Inheritance Type Base Class -> Derived Class
public inheritance public member -> public member
public inheritance protected member -> protected member
public inheritance private member -> not accessible private member
protected inheritance public member -> protected member
protected inheritance protected member -> protected member
protected inheritance private member -> not accessible private member
private inheritance public member -> private member
private inheritance protected member -> private member
private inheritance private member -> not accessible private member
The "not accessible" private members are only accessible to member functions of the base class, not members of the derived class. Only the "normal" private members can be accessed by the derived class member functions.

In practice, public inheritance is used the most often. It signifies an "is-a" relationships, e.g. for class square : public rectangle {};, class square is a rectangle. Private inheritance is less popular and describes an "is-implemented-in-terms-of" relationship, while protected inheritance is barely used at all.
Polymorphism
Class polymorphism describes the duality of an object of a derived class, as belonging to the derived class and the based class. For instance, derived class objects are not just compatible with pointers of the derived class, but also those of the base class. However, pointers of the base class can only call base class member functions, while derived class pointers can call both. If a member function is declared in both classes with the same function head, the derived class pointer will call the one from its own class. Example:
#include <iostream>

class class1 {
public:
	void foo() { std::cout << "Base class function called. " << std::endl; }
};

class class2 : public class1 {
public:
	void foo() { std::cout << "Derived class function called. " << std::endl; }	//Same function head as in base class.
};


int main()
{
	class2 obj;
	class1 *p1 = &obj;	//Pointer of base class to object of derived class.
	class2 *p2 = &obj;	//Pointer of derived class to object of derived class.

	p1->foo();		//Base class pointer calls base class foo(). Can only be used to call base class functions.
	p2->foo();		//Derived class pointer calls derived class foo().
				//Would've called base class foo(), if no derived alternative were present.
	return 0;
}
Output:
Base class function called.
Derived class function called.
If one wants the base class pointer to call the derived class member function in the example above, one could simply declare the function foo() to be virtual in the base class definition. It's a directive for the compiler to use late binding (also called dynamic binding), instead of early binding (also called static binding). With late binding, the choice, which member function is picked, is made at runtime – opposed to compilation time, as with early binding. When using pointers to call the member functions, early binding means that the pointer type decides, which function is called, while the type of the pointed-to object is decisive with late binding.
Virtual functions are declared, by adding the keyword as a prefix to the function head, e.g. virtual void foo() {...}. Pointers of the base class to an object of derived class will pick the virtual base class function only, if there is no alternative with the same function head in the derived class. Whether the derived class function is declared virtual or not, doesn't affect the outcome here.
Classes with one or more virtual functions, are also called polymorphic classes. Note: The special this pointer is of the same type as the class it is used in. Thus, it can even be used to call functions of the derived classes inside base class function definitions. Example:
#include <iostream>

class class1 {	//Polymorphic class.
public:
	virtual void bar() { std::cout << "Base class function called." << std::endl; }	//Base class bar().
	void foo() { this->bar(); }	//foo() only defined in base class! Calls virtual function bar().
					//Which bar() definition is used, depends on the object type.
};

class class2 : public class1 {
public:
	void bar() { std::cout << "Derived class function called. " << std::endl; }	//Derived class bar().
};


int main()
{
	class1 obj1;		//Base class object.
	class2 obj2;		//Derived class object.
	class1 *p1 = &obj1;
	class1 *p2 = &obj2;	//Both pointers of base class type.

	p1->foo();		//Base class member function calls base class bar(), because obj1 is of base class type.
	p2->foo();		//Base class member function calls derived class bar(), because obj2 is of derived class type!

	return 0;
}
Output:
Base class function called.
Derived class function called.
Polymorphism allows a base class to be used as a common interface for multiple derived classes. Thus, when objects of the base class aren't used at all, the function definitions that will be replaced by the derived classes, can be omitted altogether. This can be done either by declaring the function virtual and leaving the body empty, or adding =0 after the declaration. In the latter case, the function is called purely virtual. Since purely virtual functions are left undefined, classes containing such functions can't create objects and only serve as an interface for derived classes. Hence, classes containing one or more purely virtual functions are called abstract base classes. Note: Derived classes have to overload all purely virtual functions – otherwise, the functions remain without definition and the derived class becomes an abstract base class, too. Example:
class class1 {			//Abstract base class. Can't create objects.
public:
	virtual void foo() = 0;	//Purely virtual function, hence no definition.
};

class class2 : public class1 {
public:
	void foo() {}	//foo() has to be defined here, since it is purely virtual in base class.
			//If definition was omitted, class2 would also be an abstract base class.
};

Operator Overloads
In order to be able to use operators on classes, they have to be overloaded – we have already seen how this is done for the assignment operator = in the "Other Constructors" section. However, there are plenty more operators, which can be overloaded: +, -, *, /, %, ^, +=, -=, *=, /=, %=, ^=, <, >, <=, >=, ==, !=, !, &&, ||, (), [], <<, >>, <<=, >>=, ++, --, &, |, &=, |=, ~, ->, ->*, new, new[], delete, delete[] and even ,.
The declaration and definition can either be written inside, or outside the class definition, with slightly varying syntax. Note that, when declared outside the class definition, the operator is not a member function of the class. Hence, it can't be called like a member function (e.g. object1.operator+(object2)) and might have to be declared a friend function, when access to private or protected members is required. Example:
#include <iostream>

class class1 {	//Class that stores two integers. + and - operators shall simply add/subtract the respective values.
public:
	class1(int y, int z) : a(y), b(z) {}	//Constructor.
	void foo() const { std::cout << "a: " << a << ", b: " << b << std::endl; }	//Prints private members.

	//Define + operator that adds the respective integer values and creates a new class1 object as return.
	//Operator is declared const, since the added class objects aren't changed.
	class1 operator+(const class1 &obj) const { return class1(a + obj.a, b + obj.b); }

	//Operator declared outside of class definition has to be marked as friend, since private value access is required.
	friend class1 operator-(const class1 &obj1, const class1 &obj2);
private:
	int a, b;
};

//Operators can also be declared outside of the class, in which case they're declared with two parameters.
class1 operator-(const class1 &obj1, const class1 &obj2) {
	return class1(obj1.a - obj2.a, obj1.b - obj2.b);
}


int main()
{
	class1 obj1(12, 5);
	class1 obj2(4, 6);
	class1 obj3 = obj1 + obj2;	//Right side equivalent to obj1.operator+(obj2)
	obj3.foo();			//Print result.
	class1 obj4 = obj1 - obj2;	//Can't be written as obj1.operator-(obj2), since it isn't a class member.
	obj4.foo();			//Print result.

	return 0;
}
Output:
a: 16, b: 11
a: 8, b: -1

Templates
Templates for classes work similarly to those for functions. For the class definition, the only change there is, is the added prefix template <class T>, where T is the template parameter, which can take any data type. A class class1 with template parameter T has the template class identifier class1<T>. To instantiate a class for some parameter and declare objects, this template parameter is replaced by the desired data type. For example, class1<int> obj; instantiates the class class1 with the template parameter T replaced by int and declares an object of this class. Example:
#include <iostream>

template <class T>	//Template with parameter T. class keyword equivalent to typename.
class class1 {
public:
	class1(T val) { value = val; }		//Assignment operator needs to be overloaded for type T.
	void foo();				//Defined outside class definition.
private:
	T value;
};

template <class T>				//Required, just like with "normal" (non-member) template functions.
void class1<T>::foo() {				//class1<T>, since class1 is template class with parameter T.
	std::cout << value << std::endl;	//Insertion operator << needs to be overloaded for type T.
}


int main()
{
	class1<int> obj_int(123);		//Instantiates class1 for int and constructs object.
	class1<char> obj_char('f');		//Same with char.

	obj_int.foo(); obj_char.foo();		//Print values.

	return 0;
}
When several template parameters are used, one simply precedes the class definition with both, e.g. template<class T1, class T2>.
As with template functions, class templates can be specialized. Explicit template specialization describes, when a template class instance is manually created, with all generic parameters replaced by fixed ones. With partial template specialization, some parameters remain generic, while some are replaced by fixed ones. To specialize a class, it is redeclared without the generic parameter(s) in the preceding template <> tag, which are replaced by non-generic ones after the class name, similarly to an object declaration.
Note, that member functions have to be declared and defined for all those specialized class instances. Example:
#include <iostream>

template <class T1, class T2>
class class1 {
public:
	//Constructor prints message when object is created.
	class1() { std::cout << "Non-specialized class object created." << std::endl; }
private:
	T1 value1;
	T2 value2;
};

template <class T1>		//Partial template specialization. One generic parameter left.
class class1<T1, int> {		//Is called, when 2nd parameter is int and 1st is not a char (see below).
public:
	class1() { std::cout << "Partially specialized class object created." << std::endl; }
private:
	T1 value1;
	int value2;
};

template <>			//Explicit template specialization, no generic parameters left.
class class1<char, int> {	//Is called, when 1st parameter is char and 2nd is int.
public:
	class1() { std::cout << "Explicitly specialized class object created." << std::endl; }
private:
	char value1;
	int value2;
};

int main()
{
	class1<char, char> obj1;	//No specialization.
	class1<bool, int> obj2;		//Partial specialization used, because 2nd parameter is int and 1st not char.
	class1<int, bool> obj3;		//No specialization.
	class1<char, int> obj4;		//Full specialization.

	return 0;
}
Output:
Non-specialized class object created.
Partially specialized class object created.
Non-specialized class object created.
Explicitly specialized class object created.

union
While the union keyword also creates classes and has the same syntax, union classes differ a lot from those declared with struct and class. Most notably, all data members of a union share the same memory block. Due to this, only one of those members should be used at a time – while accessing other members might be possible and yield an outcome related to the actually initialized member, it is undefined behavior. Example:
#include <iostream>

struct struct1 {
public:
	int val1;
	long val2;
};

union union1 {
public:			//public by default, but explicitly stating it does no harm.
	int val1;
	long val2;
};


int main()
{
	struct1 obj1;
	union1 obj2;
	//struct members have differing memory addresses and class objects are bigger.
	std::cout << "struct object member addresses: " << &obj1.val1 << "   " << &obj1.val2 << std::endl;
	std::cout << "Size of struct1: " << sizeof(struct1) << " byte." << std::endl;
	//union members have identical memory addresses and the objects are as big as the biggest member type.
	std::cout << "union object member addresses: " << &obj2.val1 << "   " << &obj2.val2 << std::endl;
	std::cout << "Size of union1: " << sizeof(union1) << " byte." << std::endl;

	return 0;
}
Possible Output:
struct object member addresses: 0xffffcc10   0xffffcc18
Size of struct1: 16 byte.
union object member addresses: 0xffffcc08   0xffffcc08
Size of union1: 8 byte.
Just like with struct, members of classes declared with union are public by default. Considering the way they are implemented, unions can be used, when only one data member is required, whose type shall be changeable. Even though struct or class could be used for a similar result, they use up more memory, due to the unused members. Whether that is relevant depends on the setting.
Since unions are classes, they can also contain member functions. However, unlike those created with struct/class, union classes can't make use of class inheritance.