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.
class
usually is used for bigger, closed objects, while struct
s 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.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.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
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.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. }
classname(){};
or classname() = default;
to the class member list.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; };
~
. 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; };
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.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.&&
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; }
= 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. };
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).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.#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 123When 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){} };
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 |
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.
#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 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.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. };
=
in the "Other Constructors" section. However, there are plenty more operators, which can be overloaded: +
, -
, *
, /
, %
, ^
, +=
, -=
, *=
, /=
, %=
, ^=
, <
, >
, <=
, >=
, ==
, !=
, !
, &&
, ||
, ()
, []
, <<
, >>
, <<=
, >>=
, ++
, --
, &
, |
, &=
, |=
, ~
, ->
, ->*
, new
, new[]
, delete
, delete[]
and even ,
.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
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>
. template <>
tag, which are replaced by non-generic ones after the class name, similarly to an object declaration. #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
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.struct
/class
, union classes can't make use of class inheritance.