Key words: The Philosophy of C, The Philosophy of C++, Foundations of C++, object orientation in C++, class in C++, class vs object in C++, size of an object, inheritance and class hierarchies, compile time polymorphism, run time polymorphism, Early binding and late binding in C++, Virtual table and the virtual pointer in C++, Name mangling in C++, static member functions, static data members in C++, static data declaration vs initialization, The enum class of C++, operator overloading and friend functions
Topics at a glance:
- Introduction : The foundations of classical C++
- Classes – defining your own types and establishing behaviors
- Objects – manifestation of behaviors, achieving uniqueness
- How big an object really is ?
- Code Reuse – Inheritance, late binding and achieving polymorphism
- Name mangling and how to decrypt C++’s generated mangled naming convention
- Code to base class idiom
- Static classes, Static member functions, initializing, working and their intricacies
- Making friends, putting enums to use, introduction to operator overloading
C++ is a complex and a powerful programming language. Yes, it is complex, but once you start to understand the philosophy behind the features it provides, the ways it follows to achieve things, then you will begin to understand that C++ is much simple than you initially thought.
Let us understand the foundation on which C++ is built on., and it’s philosophy. We all know that C++ originated as a derivative of ‘C’ language. So to understand C++’s philosophy one must understand C’s philosophy first.
The Philosophy of C
The philosophy of ‘C’ programming language lies on how it abstracts various data types, and how it’s execution model works. For this, you need to understand how various data types such as int, char, float, user defined data types such as arrays, structs, unions are stored and manipulated/used in ‘C’. How the byte ordering such as little and big endian systems differ and how C approaches both these systems, (Chapters 1, 2, 3), how the stack is used in ‘C’ and how it is used in its execution model (i.e. stacks and functions, as I have explained in detail in Chapter 4), and should have an understanding of different sections of a C program such as data segment, text segment, and heap in general. Once you understand these then you will get a grasp of the Philosophy of ‘C’, the ideology its built on.
The Philosophy of C++
Philosophy of classical C++ is realized in three aspects.
Foundations of classical C++
1: It is essential that you understand C’s philosophy first, before venturing into C++. This is the first philosophical aspect of C++ – i.e. The philosophy of ‘C’.
2: C++ is not just ‘C’. C++ gives you more sophisticated abstraction mechanisms that helps a programmer to define his own data types, to establish their behavioral attributes; This is the foundation on which C++’s support for Object Oriented Programming is based on. OOP is the second philosophical aspect of C++.
3: The third philosophical aspect of C++ is its immense capability for supporting generic programming via templates.
In this chapter we will focus on the C++’s OOP aspect realized through the abstraction called class and it’s attributes.
Note: For explaining C++ concepts, semantics and idioms I have used C++11 or C++14 throughout my blog. I strongly recommend everyone who really want to use C++ to it’s fullest potential to adopt modern C++ i.e C++11, 14, 17, or 20.
Object orientation in C++
Classes and Objects in C++
C++ realizes OOP and its concepts such as encapsulation, inheritance, and various manifestations of polymorphisms through a different category of abstraction known as class. Without class there is no OOP in C++. Class helps one to define his own data type, and define a behavior to that data type. Instances of a class are known as objects. Object is a realization of a class with an attribute, that is, its uniqueness. Object instances are distinct from one another even though they belong to the same class (type). Class definition defines common behavior for all of its object instances. i.e. the methods/member functions defined in a class helps in implementing the behavior and object instances personify its class and exhibits uniqueness through the data it encapsulates. To explain how a class’s behavior is different from object’s uniqueness I will use the following example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | #include <iostream> #include <iomanip> using namespace std; class Integer_Pair { protected: int i_; int j_; public: // constructs an instance of Integer_Pair with provided values Integer_Pair(int i = 0, int j = 0): i_{i}, j_{j} { cout << "An Object of Integer_Pair is instantiated with pair(" << dec << i_ << ", " << j_ << ") " << endl; } int add() const { return i_ + j_; } int subtract() const { return i_ - j_; } int multiply() const { return i_ * j_; } double divide() const { return ( static_cast<double>(i_)/static_cast<double>(j_) ); } ~Integer_Pair() { cout << "Object - (" << dec << i_ << ", " << j_ << ") " << "has been destroyed!" << endl; } }; class Integer_Pair_2 : public Integer_Pair { // inheriting Base class constructor. // No need of explicit constructor for derived class Integer_Pair_2 public: using Integer_Pair::Integer_Pair; void print() { cout << "(" << dec << Integer_Pair::i_ << ", " << Integer_Pair::j_ << ")" << endl; } }; int main() { Integer_Pair ip_1(1, 2); Integer_Pair ip_2(5, 5); // Now to show you how unique ip_1 and ip_2 are cout << "ip_1's behavior : " << endl; cout << "ip1.add() gives " << ip_1.add() << endl; cout << "ip1.subtract() gives " << ip_1.subtract() << endl; cout << "ip1.multiply() gives " << ip_1.multiply() << endl; cout << "ip1.divide() gives " << ip_1.divide() << endl; cout << "\nip_2's behavior : " << endl; cout << "ip2.add() gives " << ip_2.add() << endl; cout << "ip2.subtract() gives " << ip_2.subtract() << endl; cout << "ip2.multiply() gives " << ip_2.multiply() << endl; cout << "ip2.divide() gives " << ip_2.divide() << endl; cout << "\nip_1 is stored @ " << hex << &ip_1 << endl; cout << "ip_2 is stored @ " << hex << &ip_2 << endl; // observe the trick to get the address of a class member function in C++ int (Integer_Pair::*ptr_to_add)() const = &Integer_Pair::add; int (Integer_Pair::*ptr_to_multiply)() const = &Integer_Pair::multiply; cout << "address of add and multiply are as : " << hex << (void*&)ptr_to_add << ", " << (void*&)ptr_to_multiply << endl; cout << "\nsizeof(ip_1): " << dec << sizeof(ip_1) << endl; cout << "sizeof(ip_2): " << dec << sizeof(ip_2) << endl; cout << endl; Integer_Pair_2 ip_a_1(4,3); ip_a_1.print(); cout << "sizeof(ip_a_1) : " << dec << sizeof(ip_a_1) << endl; int (Integer_Pair::*ptr_to_add_parent)() const = &Integer_Pair::add; int (Integer_Pair::*ptr_to_add_derived)() const = &Integer_Pair_2::add; cout << "\naddress of add in parent class Integer_Pair: " << hex << (void*&)ptr_to_add_parent << "\naddress of add in derived class Integer_Pair_2: " << hex << (void*&)ptr_to_add_derived << "\n" << endl; return 0; } |
A general advice: please restrain from using ‘endl’ whenever you want a new line in a text. Use ‘\n’ if possible, as ‘endl’ also flushes out the output buffer of the ostream. Use ‘endl’ at end of lines, of course.
Instance of class Integer_Pair can store two integers thus the name Integer_Pair. Behaviors for adding, subtracting, multiplying and dividing is defined by the class. That means any object instance of the class exhibits these behaviors.
Result:
An Object of Integer_Pair is instantiated with pair(1, 2)
An Object of Integer_Pair is instantiated with pair(5, 5)
ip_1's behavior :
ip1.add() gives 3
ip1.subtract() gives -1
ip1.multiply() gives 2
ip1.divide() gives 0.5
ip_2's behavior :
ip2.add() gives 10
ip2.subtract() gives 0
ip2.multiply() gives 25
ip2.divide() gives 1
ip_1 is stored @ 0x461cc48
ip_2 is stored @ 0x461cc40
address of add and multiply are as : 0x4020e0, 0x402118
sizeof(ip_1): 8
sizeof(ip_2): 8
An Object of Integer_Pair is instantiated with pair(4, 3)
(4, 3)
sizeof(ip_a_1) : 8
address of add in parent class Integer_Pair: 0x4020e0
address of add in derived class Integer_Pair_2: 0x4020e0
Object - (4, 3) has been destroyed!
Object - (5, 5) has been destroyed!
Object - (1, 2) has been destroyed!
Now, coming to an object’s uniqueness. We can see that result of the above program execution, shows that even though objects’s behavior is same, the results from object 1 (i.e. ip_1) is distinct from that of object 2 (i.e. ip_2).
Behavior simply defines what the class is going to execute, when you invoke the associated member function. Whereas, the exact manifestation of the behavior depends on the state of the object on which the member function is invoked. ‘ip_1.add()’ resulted in 3, which is the result of 1 + 2. That means object ip_1’s state is just defined by the values it stores, i_ and j_. Now you know why ip_2.add() resulted in 10, as state of ip_2 is defined by i_ at 5 and j_ at 5. When you invoke add() on ip_1 or ip_2 it executed addition of the pair of numbers that the two objects store. i.e. How the behavior manifests depend upon the objects state defined by the data that the objects store in memory.
Two things are here to note; a class defining a set of common behavior, and there are objects with unique states. So, how this is possible in C++ through class abstraction? Simple. Instances of objects are allocated in distinct memory locations. Common behavior which is achieved via member functions of a class are not replicated for each object but only one instance exists for all the objects of a class, and will be allocated to, what is called as the, text segment.
To illustrate this, the main() also prints the storage locations of ip_1, ip_2 and the member functions as well. We see that both ip_1 and ip_2 object sizes are same and is 8 bytes. These are used for storing i_ and j_. So, where are the member functions stored? They are stored in the text segment somewhere else. The point here to note is wherever they are stored, the member functions are not stored where objects are stored. This is the difference you need to understand.
Don’t divert your focus into the complex syntax I have used to get the address of member function. Just see that addresses of add and subtract are obtained without even using the objects. In C++, you cannot get member function address through an object instance. Because, object doesn’t store member function address in it’s memory. Remember object size in above code is just 8 bytes used for storing it’s data i_ and j_? For getting member function address, use the class name, in conjunction with C++ scope resolution operator :: along with some other tricks which I have depicted in the code above.
Inheritance and class hierarchies
Code reuse via inheritance:
We use inheritance mainly to extend or improvise or for adding extra features for an existing class. I will show you the exact meaning of code reuse in C++. In fact, C++ has taken the literal meaning of code reuse.
I have derived a class Integer_Pair_2 from Integer_Pair. Integer_Pair_2 introduce an extra member function print. Since it doesn’t have any data members of its own, Integer_Pair_2 doesn’t need an explicit constructor. So I am inheriting the constructor from base/parent class with the help of C++ 11 feature – ‘using’. NOTE: I have made access specifier for Interger_Pair parent class’s i_ and j_ to protected, so that derived class can access these members.
Please focus on the following lines of the above result,
An Object of Integer_Pair is inistantiated with pair(4, 3)
(4, 3)
sizeof(ip_a_1) : 8
address of add in parent class Integer_Pair: 0x4020e0
address of add in derived class Integer_Pair_2: 0x4020e0
Both classes use the exact same function implementation of add() stored at 0x4020E0.
That means, derived class doesn’t even create an unnecessary copy of member functions that it inherits from parent class. Also, look at the object size of ip_a_1. its still 8 bytes as Integer_Par_2 doesn’t add its own copies of data members in the hierarchical class system, object instances still use 8 bytes for storing i_ and j_.
Size of an object
Size of object is the size of data members declared in the class of that object. If the object is an instance of a hierarchical class system ( i.e. inherited class system), then, the size of object is equal to the combined size of all data members of all the classes it is derived from. Also, if the class is polymorphic, then you will have to consider the size of a pointer to virtual table as well. This v_table pointer, typically adds 4 more bytes in a 32-bit system in addition to the size of data members declared in the hierarchical class system.
Consider the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | #include <iostream> #include <iomanip> using namespace std; class Integer_Pair { protected: int i_; int j_; public: // constructs an instance of Integer_Pair with provided values Integer_Pair(int i = 0, int j = 0): i_{i}, j_{j} { cout << "An Object of Integer_Pair is inistantiated with pair(" << dec << i_ << ", " << j_ << ") " << endl; } int add() const { return i_ + j_; } int subtract() const { return i_ - j_; } int multiply() const { return i_ * j_; } virtual double divide() const { return ( static_cast<double>(i_)/static_cast<double>(j_) ); } ~Integer_Pair() { cout << "Object - (" << dec << i_ << ", " << j_ << ") " << "has been destroyed!" << endl; } }; class Swapped_Pair : public Integer_Pair { public: using Integer_Pair::Integer_Pair; //using Integer_Pair2::Integer_Pair; double divide() const override { return ( static_cast<double>(j_) \ /static_cast<double>(i_) ); } }; // ip_obejct is a reference to an object of the type Inetger_Pair void integer_pair_client(Integer_Pair &ip_object) { // add is not a virtual function so this invokes base class add() cout << "\nip_object.add() : " << dec << ip_object.add() << endl; // divide is polymorphic, so this invokes derived calss divide cout << "ip_object.divide() : " << ip_object.divide() << endl; return; } int main() { Integer_Pair ip_1(1, 2); Swapped_Pair ip_b_1(1,2); cout << "\nip_b_1.divide : " << ip_b_1.divide() << endl; cout << "ip_1.divide : " << ip_1.divide() << endl; cout << "\nsizeof(ip_1): " << dec << sizeof(ip_1) << endl; cout << "sizeof(ip_b_1): " << dec << sizeof(ip_b_1) << endl; //Code to the base class principle integer_pair_client(ip_b_1); cout << endl; return 0; } |
An Object of Integer_Pair is inistantiated with pair(1, 2)
An Object of Integer_Pair is inistantiated with pair(1, 2)
ip_b_1.divide : 2
ip_1.divide : 0.5
sizeof(ip_1): 12
sizeof(ip_b_1): 12
ip_object.add() : 3
ip_object.divide() : 2
Object - (1, 2) has been destroyed!
Object - (1, 2) has been destroyed!
If you want the result of j_ / i_ i.e. j_ divided by i_, instead of i_ divided by j_ that the parent class Integer_Pair implements, then instantiate an object of class Swapped_Pair and see how divide works. That means divide method of parent class is now overridden by the derived class.
That’s why, when divide is invoked on ip_b_1, which is an object of class Swapped_Pair, the result is 2, i.e. (2/1). But the same on ip_1 results in 0.5 i.e. 1/2.
The interesting thing to notice here is why the size of objects, both ip_1 and ip_b_1 is now 12 bytes and not 8 bytes. This is the result of a pointer to virtual table, a.ka. pointer to v_table or vptr that I mentioned above. But you may ask why compiler inserts one? For answering this, we need to understand, how the compiler correctly picks the right variant of function divide? The derived class variant or base class variant? Please continue the following sections to understand what’s happening under the hood.
Early binding and Late binding in C++
Have you ever wondered how a call for divide on ip_b_1 and ip_1 invokes the correct variant of divide, despite the similarity in their name, parameter types, and return type? This is achieved through a concept known as late binding in C++. In C and C++ all normal functions, when compiled generates unique symbols, eventually becoming part of symbol table. When compiler finds any reference to these functions in code, it can easily map that reference to a specific function directly identified by its unique symbol from the symbol table. This deduction is done completely by the compiler at the compilation stage. This is also known as static binding, where the compiler at compile time deduce and hence binds the actual function from symbol table to wherever a caller has made a reference to the function in the code. Once you generate the assembly code for a normal function call in ‘C’ or ‘C++’ you will understand this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <stdio.h> // normal function in C and C++ int increment(int arg) { // do some computation on arg // and return the result return (arg + 1); } int main() { int number = 4; // invoke the increment function number = increment(number); return 0; } |
The portion of assembly (relevant to this topic), generated for this by gcc ‘C’ compiler and g++ ‘C++’ compiler is shown below:
‘C’ compiler generated assembly:
_main:
; do the necessary stack frame settings
call _increment
_increment:
; do the necessary stack frame settings
; do the computation, store the result
; do the necessary stack reverse and return
‘C++’ compiler generated assembly
_main:
; do the necessary stack frame settings
call __Z9incrementi
__Z9incrementi:
; do the necessary stack frame settings
; do the computation, store the result
; do the necessary stack reverse and return
One thing is common here. The compiler knows which function to call. Let us see how it is achieved in C++.
Compile time polymorphism
You would have noticed the symbols i.e. the function name symbols generated by C and C++ compilers for the same source code differs. For C, its ‘_increment’ while in C++, its some mysterious ‘__Z9incrementi’.
Name mangling in C++
This mysterious symbol naming generation in C++ is known as name-mangling. First I will explain how to decrypt the name __Z9incrementi.
Name part |
meaning |
‘__Z’ |
Indicates that the name is managed by C++ compiler; avoids conflicts with other user defined symbols present throughout the code |
‘increment’ |
The name user defined in C++ source code |
‘i’ |
First argument is an integer therefore its ‘i’; For char it will become ‘c’, for void it will become ‘v’ and so on. |
‘9’ |
I have kept this towards the last because you can easily understand now what’s ‘9’. It’s just the string length of the user defined name ‘increment’. So that after parsing 9 characters of the symbol, the remaining characters will be treated as part of parameter identifiers, in this case 10th character is ‘i’ for integer parameter. |
Now, you understand how to decrypt the C++ name mangled symbols. The question is why is this necessary? C++ supports something known as function overloading. That meas, defining functions with exactly same name in the same scope, but with different behavior. It is easy to explain this with the help of an example of function overloading in C++.
Function Overloading in C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <iostream> using namespace std; int increment(int number) { return (number + 1); } int increment(int number, int count) { return (number + count); } char increment(char c) { return (c + static_cast<char>(1)); } int main() { int number = 4; // invoke the increment function number = increment(number); // does 4 + 1 cout << "Number is " << dec << number << endl; number = increment(number, 5); // does 5 + 5 cout << "Number is " << dec << number << endl; char c = 'A'; c = increment(c); // does 'A' + 1 cout << "Character is " << c << endl; return 0; } |
The above example, shows increment function overloaded for supporting integer and character, and one variant that takes one extra parameter to specify the amount by which the input should be incremented.
So, in the above code, how compiler will understand which function to call? By checking the number of parameters, and type of parameters in addition to the function’s user defined name itself. At least, some would have wondered why the return type is never considered in name mangling by C++? The answer is simple, return value is not necessary to be used by the caller/invoker? What if the caller simply invokes the function – increment() without taking in the returned value; also assume that all versions of overloaded functions uses the same number and type of parameters.
Let us see how name mangling can help C++ compiler to resolve the symbols in case of function overloading. These are the g++ generated mangled names for the above variants of the function increment:
Mangled name |
Function variant declared in C++ source |
__Z9incrementi |
int increment(int); |
__Z9incrementii |
int increment(int, int); |
__Z9incrementc |
char increment(char); |
You can see how unique symbols are generated for the three function variants. Name mangling, thus, avoids any confusion while deducing the specific variant of the function that the caller has invoked/referred. This kind of binding symbols, to the places where they are referred in code, by the compiler at compile time itself is called static binding or compile time binding or simply early binding as it happens at an early stage before loading and running the program.
Now let us focus on to the counterpart of early binding which is dynamic binding/late binding/run time binding.
For function overloading, which is a type of compile time polymorphism in C++, name mangling can help the compiler in deducing the exact variant.
Run time polymorphism
Now let us turn our attention to run time polymorphism supported by C++ using function overriding by derived classes? There, the function name, return type, parameter type and number of parameters are all exactly the same in base class and its variant re-defined in derived class. Remember the member function divide() of Integer_Pair and Integer_Pair_2?
This time, C++ cannot do early binding and it uses something known as run time/late binding. This is achieved in two phases. Phase one is done by the compiler itself, and phase 2 is manifested during run time as a result of phase 1.
Phase 1 of Late Binding (compiler stage):
Virtual table and the virtual pointer in C++
In C++ polymorphic member functions are getting called always via the v_table. Simply put, v_table is just an array of pointers. (There are other Run Time Type Identification (RTTI) information packed into v_table, but for the time being let us not deviate and stick to just the use of v_table as an array of pointers). v_tables are created per polymorphic class. During the construction of an object instance of a polymorphic class such as the one we have used here, the compiler, in addition to allocating memory for storing the object, also allocates additional memory for storing a pointer to the v_table. For objects of a base class, the addresses of base class v_table is added to this v_ptr. But in case of derived class object, this v_ptr gets filled first with the addresses of base class v_table and then it will get overwritten with the address of derived class v_table. Remember the order of constructor invocation. So, regardless of the number of polymorphic functions defined in the class, object size is incremented by an extra 4-bytes for storing this vptr.
That means v_table is not inside an object’s memory, but outside. And object just carries in its v_ptr an address to it’s class’s v_table.
Two things to note here:
- v_table is class specific and is always one v_table per one polymorphic class. (i.e. classes with virtual functions).
- v_ptr is per object and points to the actual class type of the object.
Phase 2 of Late Binding (run time stage):
Whenever the prog ram invokes any polymorphic function, a special code inserted by the compiler comes into action. This special code goes through the v_table accessed via the object’s vptr, which will in turn, point to the address of correct variant of the polymorphic function. i.e. Base class variant or derived class variant. The only hurdle here is when the polymorphic function is also an overloaded function. Therefore, at this stage, the special code also checks the correct function signature using the name mangling information and finally finds the correct function address from the v_table and invokes it through the address.
This procedure is a kind of a callback function. In a way, it is similar to how embedded programmers program interrupt handlers. Upon any interrupt, the interrupt source is conveyed by certain CPU registers, along with some additional information, which will be used, to find the correct interrupt handler. This involves callback function along with deducing the correct function to be called, similar to phase 2 of late binding in C++.
Steps in late binding for achieving run time polymorphism.
- Compiler creates a v_table per class (separately for base class and derived class(es) ) which can hold the polymorphic function addresses. For base class, this v_table will have base class function addresses and for derived class it will have derived class function addresses.
- During object creation, one v_ptr data member per object is added in the object’s memory, which will points to the class specific v_table. As base class constructor is run first, v_ptr will be initialized to base classes’ v_table address. When derived class constructor is run, this v_ptr will get overwritten with derived v_table address. Remember the order of invoking constructors for derived class objects. Base first and then derived.
- Throughout the code, wherever compiler sees any reference to any function (either polymorphic or non-polymorphic) , the function signature is deduced using name mangling information.
- Compiler finally adds special code for accessing the v_table through object’s v_ptr.
- This is where the compiler stage completes. Or let’s say, phase 1 of late binding is done here.
- Now, during run time, i.e. phase 2, the compiler inserted special code will come into action. Using the phase 1’s function signature, this special code access the classes‘ v_table through the object’s v_ptr, finally getting the address of the correct function variant and invokes it through this address from v_table.
- Voila 🙂 Run time polymorphism has been achieved!
The presence of C++ keyword virtual tells the compiler that the function is polymorphic and will be overridden by some derived class.
Keyword override is optional. But as a C++ programmer I always recommend to use this keyword when you declare polymorphic functions in derived classes. This helps, trust me. The reason I am not explaining here, at this point. I am saving this topic for a later discussion.
Code to base class principle
When you write client code, i.e. code that uses the objects of a particular class, it’s difficult to keep track of how many derived classes are there and will get added. In such situations we write our code aligning to the principle of coding to a base class object. That means our client code will work on the assumption that the object is of the type base class. But when the client code runs, the correct functions will get invoked with the help of vptr and v_table that I mentioned above. This kind of polymorphism is possible in C++ only via pointers and references.
Let us walk through a portion of the above code to show this in work:
1 2 3 4 5 6 7 8 9 10 | // ip_obejct is a reference to an object of the type Inetger_Pair void integer_pair_client(Integer_Pair &ip_object) { // add is not a virtual function so this invokes base class add() cout << "\nip_object.add() : " << dec << ip_object.add() << endl; // divide is polymorphic, so this invokes derived calss divide cout << "ip_object.divide() : " << ip_object.divide() << endl; return; } |
Focus to the following lines of the above result
ip_object.add() : 3
ip_object.divide() : 2
Client code function doesn’t even distinct between object instances of derived and base class. Invoking divide on ip_object calls the correct implementation of function through C++’s run time polymorphism I explained above. You understand, what and all happens under the hood now?
‘is a’ Relationship
Any derived class, also, is a base class in a class hierarchy. This is known as ‘is a’ relationship. That means Swapped_Pair object is also an Integer_Pair object.
NOTE 1: Always use references or pointers when using polymorphic objects, as an object of base class instance. Do not force type cast from derived object type to base object type, which will result in object trimming/truncation/slicing. Suppose derived class has declared extra data that are not declared in base class. Then what will happen if you typecast an object of that derived class to its base class? Then a bigger data type is getting type-casted to a smaller data type. If you know what will happen if you typecast int to a char or double to a float, you will understand what I am referring to. It will result in data loss.
NOTE 2: The other type of relationship is ‘has a’ which is related to composite class types, which I will explain later.
Static data members in C++ and static member functions
In simple terms, a static data member means there is only one copy of that data per class and no instances are further created per object. That also means static data members are not allocated within object memory, which implies, static data members of a class can be accessed by all of its objects, and all objects at any given point of time see the same static member. As static data members are common to any object instance or to the class itself, we can access such static data members either via object instance using ‘.’ (dot operator) or using the class name along with scope resolution operator ‘::’.
Static data declaration vs initialization
In C++, we need to note that, static data is just declared inside a class and is not instantiated. As you know when you just declare a class, and even if you define all member function bodies within the class itself, there is no data or object instantiation. So the question is when and how to instantiate a static data member. The obvious choice is to do this outside the class body.
See the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | #include <iostream> #include <string> using namespace std; class Student { protected: static int count; // static is just for delcaring count. static const float record_version; // or use constexpr instead const for in-class initialization string name_; int marks_; public: Student(const char *name, const int marks = 0): name_{name}, marks_{marks} { ++count; cout << "Student Record #" << dec << count << " for - " << name_ << " with marks " << marks_ << " is created!" << endl; } void set_marks(const int new_marks) { marks_ = new_marks; } void print_record(void) const // note the parameter list is marked as void { cout << "Student name - " << this->name_ // this pointer still does its job :) << " marks - " << dec << this->marks_ << endl; return; } // static member function static int get_current_count() { return count; } static float get_record_version() { return record_version; } // overloaded version of set_marks function static int set_marks(Student *this_student, const int marks) { this_student->marks_ = marks; cout << "Student - " << this_student->name_ << " marks is updated to " << this_student->marks_ << endl; } ~Student() { --count; } }; // important step to instantiate and initialize static data of class int Student::count = 0; // note: dont add static keyword again here const float Student::record_version = 1.2F; |
Here, can you see how static data member ‘count’ is instantiate outside the class. But there are certain scenarios where instantiation and initialization has to be done inside the class itself. The candidates are constant static data members. But the problem with initializing values inside a class is permitted in C++ only for integral constants such as char, bool, int etc. and not applicable for floats, doubles and other user defined types. So how this is done for non-integral static constants types then? Again the answer is to do that outside the class or if you want to do that inside the class then instead of static ‘const’ float make it static constexpr float. Hope you have noticed this in the code snippet above.
Now coming to static member functions, such functions like any other non-static member functions are not allocated in an object’s memory. But these functions have an important aspect.
Have you ever wondered how non-static member functions can access an object’s private/protected/public data? These functions are implemented only once and are not aware of how many objects are instantiated? But these non-static member functions do know where the object on which the function is currently invoked is stored. This is achieved by C++’s this pointer. C++ compiler automatically adds this pointer to non-static member functions. But for static member functions, this is not done. This is because static member functions don’t work with object instances and rather they ONLY work with class itself. So there is no this pointer supplied by the C++ compiler to these functions.
NOTE 1: It doesn’t matter even if you mark parameter list to a non-static member function explicitly as void. The compiler will remove this and supply a ‘this pointer’ as the first parameter to all non-static member functions of a class.
NOTE 2: None of the static member functions of a class will get access to ‘this pointer’ by the compiler. This doesn’t restrain a creative programmer in supplying one this pointer programmatically. The overloaded function set_marks declared as static shows how to do this elegantly.
1 2 3 4 5 6 7 8 9 10 11 | int main() { Student s1("Student_1", 95); s1.print_record(); // See the magic of using a static member function // to change student s1's marks Student::set_marks(&s1, 97); return 0; } |
The enum class of C++, operator overloading and friend functions: A use-case
For explaining this I have written a program illustrating a typical use case where a programmer can use the powerful C++ features such as enum class, friend functions and operator overloading.
The code extends Student base class to add data members such as standard, medium of instruction and affiliated syllabus for the curriculum.
Look at the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | class Student_Extended : public Student { public: enum class medium{english = 0, native = 1}; enum class syllabus{center = 0, international = 1}; private: int standard_; medium med_; syllabus syl_; static const float record_version; public: Student_Extended( const char *const name, const int standard = 1, // default value is 1 const medium med = Student_Extended::medium::english, // default medium english const syllabus syl = Student_Extended::syllabus::center ): // default syllabus center Student(name), // calling base class constructor standard_{standard}, med_{med}, syl_{syl}// members are initialized { cout << "Student Record created with the details: \n" << "standard - " << dec << standard_ << ", in medium - " << med_ << ", affiliated to " << syl_ << " syllabus" << endl; } static int get_current_count() { return Student::count; } static float get_own_record_version() { return Student_Extended::record_version; } static float get_student_record_version() { return Student::record_version; } // operator << has to be overloaded for enum medium and enum syllabus // need to use friend functions semantics here as << has to work on private class members friend ostream& operator<<(ostream& this_ostream, const Student_Extended::medium& this_medium); friend ostream& operator<<(ostream& this_ostream, const Student_Extended::syllabus& this_syllabus); }; const float Student_Extended::record_version = 5.7F; ostream& operator << (ostream& this_ostream, const Student_Extended::medium& this_medium) { switch(this_medium) { case (Student_Extended::medium::english): { this_ostream << " English"; break; } default: { this_ostream << " native"; break; } } return this_ostream; } ostream& operator << (ostream& this_ostream, const Student_Extended::syllabus& this_syllabus) { switch(this_syllabus) { case (Student_Extended::syllabus::center): { this_ostream << " Center"; break; } default: { this_ostream << " International"; break; } } return this_ostream; } |
Now, let’s see how code is using Student_Extended below:
1 2 3 4 5 6 7 8 9 10 11 | int main() { Student_Extended s2("Student_2"); // take other parameters as defaults cout << "Number of student record(s) created : " << s2.get_current_count() << endl; cout << "Student Record Version : " << Student::get_record_version() << endl; cout << "Student Extended Record Version : " << Student_Extended::get_own_record_version() << endl; cout << "Student Base Record Version : " << Student_Extended::get_student_record_version() << endl; return 0; } |
‘standard’ is just an ‘int’ not much merry about this. ‘medium’ and ‘syllabus’ are enum classes. To print these data properly and meaningfully we cannot use cout’s ‘<<’ put to output stream operator. We need to do two things here:
- Overload the operator ‘<<’ to work on enum classes medium and syllabus
- Make these overloaded operators friends to the class Student_Extended. This is required as overloaded operator function needs access to private/protected class data members. In the example I have written, ‘<<’ has to access med_ and syl, both of them are private data members of class Student_Extended.
NOTE: You may ask, why these overloaded operator functions can’t be made as class member functions? If they are member functions automatically the first parameter will be a reference to object instance (Remember the ‘this pointer’ ?). In the above example code, ‘<<’ operator is operating on operands of type medium and syllabus and not on type Student_Extended. Hope you understand the intricacies involved here.
Enjoyed the chapter? Let me know in the comments below. Thanks. 🙂
couldn’t express how good this is, great work!
Thank you dear reader. Keep on reading, let me know your valuable feedback 🙂