Core concepts of OOP(Object-oriented programming): abstraction, encapsulation, inheritance and polymorphism.
Inheritance
Syntax
- Single inheritance: class derived-class: [access] base
- Multiple inheritance: class derived-class: [access] baseA, [access] baseB....
where [access] is one of public, protected and private.
Notice that:
(1). The derived class inherits ALL MEMBERS (except for the constructor and destructor) from the base class.
(2). Once the [access] is omitted, the inheritance will be private.
(3). In the instantilization of the derived class, the constructor of the base class should always be called before that of the derived class, the order is inverse in the case of destructor. (bc the objects are stored in the stack, FILO)
(4). If only a constructor with parameters is defined in the base class, the derived class must call it explicitly; in other cases, the derived class will call the non-parameter constructor of the base class implicitly before calling its own constructor.
(5). In multiple inheritances, the order of the derived class members created conforms with the order of inheritance of base classes. Also, the order in the member initialization list conforms with the order of the declarations of the variables.
Public, protected and private
Base | public | protected | private |
---|---|---|---|
[public] | public | protected | private |
[protected] | protected | protected | privare |
[private] | private | private | private |
Notice that:
The derived class has no direct access to The private members of the base class (should use the methods of the base class), but it has access to the protected/private members even if the inheritance is [protected]/[private].
Friend
-
Syntax: Friend relationship grants the same access as class members to a non-member function or another class (or part of its methods). The friend function (or the functions of a friend class) took the reference of the class as one argument, and access the private/protected members by using the dot (.) operator.
-
Friend function: use keyword friend in the declaration in class. The friend declaration can be placed anywhere without being affected by the [access] control keywords.
Friend functions can be defined inside class declarations. These functions are inline functions, and like member inline functions they behave as though they were defined immediately after all class members have been seen but before the class scope is closed (the end of the class declaration). -
Friend class: Two types of syntax: i) elaborated-class-specifier
friend class
, ii) simple-type-specifierfriend
(c++11).
A friend class declaration cannot define a new class (friend class X {};
is an error)
The simple-type-specifier allows declaring typedef names as friends while another one does not.
Unlike the elaborated-type-specifier form, the simple-type-specifier form cannot declare a new entity but must refer to an existing declaration.
-
Friend function: use keyword friend in the declaration in class. The friend declaration can be placed anywhere without being affected by the [access] control keywords.
-
Scope: When a local class declares an unqualified function or class as a friend, only functions and classes in the innermost non-class scope are looked up, not the global functions:
class F {}; int f(); int main() { extern int g(); class Local { // Local class in the main() function friend int f(); // Error, no such function declared in main() friend int g(); // OK, there is a declaration for g in main() friend class F; // friends a local F (defined later) friend class ::F; // friends the global F }; class F {}; // local F }
- Properties: i). one-way, ii). not transitive, and iii). not inherited.
Static members
The static members of a class are not associated with any objects of the class. Hence, to refer a static member m
of a class T
, we can use T::m
(if m
is public) or member access expression; and for a static member function, they have no this
pointer.
-
Static member functions
- Static member functions cannot be
virtual
,const
, orvolatile
. - A static member function can access only the names of static members, enumerators, and nested types of the class in which it is declared.
- The address of a static member function may be stored in a regular pointer to function, but not in a pointer to member function.
- Static member functions cannot be
-
Static data members
- Local classes (classes defined inside functions) and unnamed classes, including member classes of unnamed classes, cannot have static data members.
- (C++17) A static data member may be declared inline. An inline static data member can be defined in the class definition and may specify an initializer. It does not need an out-of-class definition.
-
Constant static members
- If a static data member of integral or enumeration type is declared const (and not volatile), it CAN be initialized with an initializer in which every expression is a constant expression, right inside the class definition.
- (C++11) If a static data member of LiteralType is declared constexpr, it MUST be initialized with an initializer in which every expression is a constant expression, right inside the class definition.
Copy constructors
As the default copy constructor (and assignment operator) only provides shallow copy, in many cases, we should define the copy constructor (and assignment operator) explicitly, following the Rule of Five:
Whenever you are writing either one of Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor or Move Assignment Operator you probably need to write the other four.
Shallow copy vs. deep copy
- Shallow copy: A shallow copy of an object copies all of the member field values. This works well if the fields are values, but may not be what you want for fields that point to dynamically allocated memory. The pointer will be copied. but the memory it points to will not be copied -- the field in both the original object and the copy will then point to the same dynamically allocated memory, which is not usually what you want.
-
Deep copy: A deep copy copies all fields, and makes copies of dynamically allocated memory pointed to by the fields.
class deepCopy { private: int i; int *pi; public: deepCopy(int i): i{i} {pi = &i;} deepCopy(const deepCopy& copy) // copy constructor :i{copy.i} {pi = &i;} void address(){cout<< pi <<endl;} }; class shallowCopy{ private: int i; int *pi; public: shallowCopy(int i): i{i} {pi = &i;} // copy constructor by default void address(){cout<< pi <<endl;} }; int main(){ deepCopy d(10); d.address(); //////////// deepCopy d1{d}; // different address d1.address(); /////////// shallowCopy s(1); s.address(); //////////// shallowCopy s1{s}; // same address s1.address(); /////////// return 0; }
Is-a & has-a
Is-a is a subsumption relationship between abstractions (e.g. types, classes), wherein one class A is a subclass of another class B (and so B is a superclass of A). Has-a (has_a or has a) is a composition relationship where one object (often called the constituted object, or part/constituent/member object) "belongs to" (is part or member of) another object (called the composite type), and behaves according to the rules of ownership. The is-a relationship is to be contrasted with the has-a (has_a or has a) relationship between types (classes).
-
Is-a: Subtyping enables a given type to be substituted for another type or abstraction. Subtyping is said to establish an is-a relationship between the subtype and some existing abstraction (either implicitly or explicitly, depending on language support). In C++, this relationship is usually implemented explicitly via [public] inheritance. With [public]* inheritance, the public methods of the base class become public methods of the derived class. In other words, the derived class inherits the base-class interface (the interface is still visible to outside and can use it).
* On the contrary, private inheritance does acquire the implementation, but does not acquire interface."Private inheritance means is-implemented-in-terms-of. If you let a class D privately inherit from a class B, you do so because you are interested in taking advantage of some of the features available in class B, not because there is any conceptual relationship between objects of types B and D. As such, private inheritance is purely an implementation technique. (That's why everything you inherit from a private base class becomes private in your class: it's all just implementation detail.)
....
private inheritance means that implementation only should be inherited; the interface should be ignored. If D privately inherits from B, it means that D objects are implemented in terms of B objects, nothing more. Private inheritance means nothing during software design, only during software implementation." -
Has-a: In the UML class diagram (instance-level), the has-a relationship refers to composition. Aggregation is a variant of the "has-a" association relationship. Their differences lie mainly in:
-
composition:
(1). is usally used to represent whole-part relationships, e.g. an engine is a part of a car.
(2). When the container is destroyed, the contents are also destroyed, e.g. a university and its departments. The implementation are various and depends on your requirements. e.g.:// COMPOSITION - with simple member variable class Foo { private: Bar bar; public: Foo(int baz) : bar(baz) {} }; // COMPOSITION - with unique_ptr class Foo { private: std::unique_ptr<Bar> bar; public: Foo(int baz) : bar(barFactory(baz)) {} };
-
aggregation:
(1). presents a relationship stronger than associated and similar to whole-part, but not a complete procession. e.g. a car model engine Eng-A is part of a car model Car1, it may be also part of a different car model Car2.
(2). When the container is destroyed, the contents are usually not destroyed, e.g. a professor has students, when the professor dies the students don't die along with him or her. This relationship can be implemented with the pointer, for example.
-
Determining the size of a class object
Notice that: Static data members and non-virtual functions are shared by all class object, and their sizes are not counted in the size of class objects.
There are many factors that decide the size of an object of a class in C++. These factors are:
(1). Size of all non-static data members (influenced by the compiler)
(2). Alignment
(3). The existence of virtual function(s)
(4). Mode of inheritance (virtual inheritance)
Alignment
Alignment requirements specify what address offsets can be assigned to what types. For built-in types, the alignment is correspondent to its size, e.g. an int
(four bytes) will be 4-byte aligned.
In a structure/class, by default, the storage of variables will be aligned optimally for reading and writing. Suppose that you have the structure as following (in X64):
struct S {
short a; //2B
int b; //4B
char c; 1B
int *d; //8B
};
without alignment, the size of this structure will be 2+1+4+1+8=15B. For an X64 machine which reads 8B each time, it requires splitting the first 8B into 2 part and combine it with part of the second 8B, which is time-consuming. The least time-consuming way is to align each variable in a structure according to the largest-size one. So the size of S
is 48.
To make a comprimise between size and time, by default #pragma pack(8)
is used, ensuring that members that are up to 8 bytes long get aligned on an address that's a multiple of their size. In this case the above example will be laid out in memory like:
0 1 2 3 4 5 6 7
|a|a|-|-|b|b|b|b|
|c|-|-|-|-|-|-|-|
|d|d|d|d|d|d|d|d| *:'-' means byte padding
and costs 24B in the memory. Notice that the total size will be an integer multiple of the lagest-size member, and the offset of a variable depends on the size of its previous variable. e.g. change the order of a
and b
in S
:
0 1 2 3 4 5 6 7
|b|b|b|b|a|a|c|-|
|d|d|d|d|d|d|d|d|
The existence of virtual functions *
*See the next section for the explanation about virtual function.
The existence of virtual function(s) will add 4 bytes of virtual function table pointer (which is shared by all the same class objects like member functions) in the class, which will be added to the size of the class. In this case, its derived class (not virtual inheritance) will inherit its virtual function thus it will have its own virtual table pointer, whether it overrides the virtual function or not.
class A{
public:
int data;
void virtual func();
class B1: public A{};
class B2: public A{};
class C: public B1, public B2{};
In the example above, size (no alignment) of A, B1, B2, B3 are respectively 12B, 12B, 12B, 24B.
Polymorphism
Binding and polymorphism
Static/dynamic binding (or early/late binding) are related to the concept of static/dynamic polymorphism. However, these two concepts only have a little overlap: there can be both static and dynamic bindings without polymorphism.
Binding is the link to map the function definition and function call. During compilation, every function is given a memory address, as soon as function calling is done, control of program execution moves to that memory address and get the function code stored at that location executed.
- Definition: If it’s already known before runtime, which function will be invoked or what value is allotted to a variable, then it is a static binding, and if it comes to know at the runtime then it is called dynamic binding.
- Pros & cons: Since all information needed to call a function is available before runtime, static binding results in faster execution. The major advantage of dynamic binding lies in its flexibility since a single function can handle different kinds of objects at runtime, which significantly reduces the size of the codebase and makes the source code more readable.
-
Situation d'usage: The only situations in which C++ programs bind function calls dynamically are:
- when a dynamic library is used, in which case the binding may be done by the Operating Systems loader before main() is called, or explicitly in code using dlsym (or similar OS-specific function), which returns a function pointer to use to call a function found in a dynamic library (.so, .dll, ...).
- in virtual dispatch, when a virtual member function is found at run-time, typically by following a pointer/reference from the data-object to a virtual dispatch table where the function pointer's recorded.
- when function pointers are used explicitly by the programmer.
In C++ polymorphism is mainly divided into two types:
-
Compile time polymorphism
- Function/operator overloading
- Template
-
Runtime polymorphism
- function overriding (but can sometimes be optimised)
In this section, we mainly focus on the polymorphism implemented with function overriding.
Overriding and hiding
Function overriding is the method that allows a subclass or child class to provide a specific implementation of a method that is already provided by one of its superclasses or parent classes.
Overriding
-
Overriding: The overriding requires the base class function and the derived class function has the same function name/type and ARGUMENT LIST. The base class function MUST have the keyword
virtual
. The derived class function may have the keywordoverride
to help avoid bugs by producing a compilation error when the intended override isn't technically an override. e.g.:class Base { virtual void foo() {} }; class Derived: Base { void foo() override {} };
- Overload: Overload should happen in the same scope, and the parameters list of the overridden function should differ from the original one's.
Hidding
Method hidding happens when there is a derived class function with the same name as the base function, and any of the following conditions is satisfied: 1) no virtual
in the base class function, 2) the parametor lists are different.
// hidding 1)
using namespace std;
class BaseClass{
public:
void Write() {cout<<"base"<<endl;}
};
class DerivedClass: public BaseClass {
public:
void Write() {cout<<"derived"<<endl;}
};
int main(){
DerivedClass d;
BaseClass &d_as_b = d;
d_as_b.Write(); //base
d.Write(); //derived
return 0;
}
// hidding 2)
using namespace std;
class BaseClass{
public:
void virtual Write() {cout<<"base"<<endl;}
};
class DerivedClass: public BaseClass {
public:
void Write(int a) {cout<<"derived"<<endl;}
};
int main(){
DerivedClass d;
BaseClass &d_as_b = d;
d_as_b.Write(); //base
d.Write(10); //derived
return 0;
}
// overriding
using namespace std;
class BaseClass{
public:
virtual ~BaseClass(){};
void virutal Write() {cout<<"base"<<endl;}
};
class DerivedClass: public BaseClass {
public:
void Write() {cout<<"derived"<<endl;}
};
int main(){
DerivedClass d;
BaseClass &d_as_b = d;
d_as_b.Write(); //derived
d.Write(); //derived
return 0;
}
// use 'override' to help to debug
using namespace std;
class BaseClass{
public:
virtual ~BaseClass(){};
void virutal Write(int a) {cout<<"base"<<endl;}
};
class DerivedClass: public BaseClass {
public:
void Write() override {cout<<"derived"<<endl;}
// ~~~ will shows bellow 'override'
};
// error: non-virtual member function marked 'override' hides virtual member function
Virtual destructor
Always make a virtual destructor in the base class if it will be manipulated polymorphically. If not, when you delete an instance of a derived class through a pointer to the base class, using the above class as an example:
BaseClass *p = new DerivedClass();
delete p;
There will be an undefined behaviour. If you want to prevent the deletion of an instance through a base class pointer, you can make the base class destructor protected and nonvirtual; by doing so, the compiler won't let you call delete
on a base class pointer.
Upcasting & downcasting
Upcasting and downcasting give a possibility to build complicated programs with a simple syntax. It can be achieved by using Polymorphism. C++ allows that a derived class pointer (or reference) to be treated as a base class pointer. This is upcasting. In this case, although we use a base class pointer to point to a derived class, the program will find the derived class method and data to be used at the runtime. Downcasting is an opposite process, which consists in converting base class pointer (or reference) to derived class pointer. e.g.:
We have a hierarchy of classes:
+-----------+ upcasting
| employee | /|\
+-----------+ |
| |
+---------------------+ |
| | |
+-----------+ +-----------+ |
| Manager | | clerk | \|/
+-----------+ +-----------+ downcasting
using namespace std;
class Employee:public Person
{
public:
Employee(string fName, string lName, double sal)
{
FirstName = fName;
LastName = lName;
salary = sal;
}
string FirstName;
string LastName;
double salary;
void show()
{
cout << "First Name: " << FirstName << " Last Name: " << LastName << " Salary: " << salary<< endl;
}
void addBonus(double bonus)
{
salary += bonus;
}
};
class Manager :public Employee
{
public:
Manager(string fName, string lName, double sal, double comm) :Employee(fName, lName, sal)
{
Commision = comm;
}
double Commision;
double getComm()
{
return Commision;
}
};
class Clerk :public Employee
{
public:
Clerk(string fName, string lName, double sal, Manager* man) :Employee(fName, lName, sal)
{
manager = man;
}
Manager* manager;
Manager* getManager()
{
return manager;
}
};
void printSalary(Employee* p, double bonus){
p -> salary = p -> salary + bonus;
cout << p -> salary;
}
int main(){
Manager m1("Steve", "Kent", 3000, 0.2);
Clerk c1("Kevin","Jones", 1000, &m1);
// upcasting, implicitly, no need of type cast
printSalary(&m1, 1000);
printSalary(&c1, 600);
// explicitly downcasting, type cast must be specified
Employee &em = m1;
// Manager &m2 = em; Error of type mismatch, although em is an reference of a Manager object
Manager &m2 = (Manager&)(em) //OK
Employee e1("Peter", "Green", 1400);
// Manager *m3 = &e1; Error of type mismatch, can cause unexpected results for lacking commission attribure
// Manager *m3 = dynamic_cast<Manager *>(&e1); //Error, 'Employee' is not polymorphism
return 0;
}
In order to use dynamic_cast
in downcasting, the base class neeed to be polymorphism, that is, it mast have virtual method(s). Hence, if we add
virtual void func(){};
in class Employee
, the downcasting with dynamic_cast
is valid. However, the pointer m3
will be a nullptr
. In this case, if we use reference instead of pointer, the program will throw an error: terminating with uncaught exception of type std::bad_cast: std::bad_cast.
Diamond problem
X
/ \
/ \
A B
\ /
\ /
C
Consider the following class hierarchy:
struct Animal {
virtual ~Animal(){};
int planet;
virtual void eat(){};
};
struct Mammal : Animal {
virtual void breathe(){};
};
struct WingedAnimal : Animal {
virtual void flap(){};
};
// A bat is a winged mammal
struct Bat : Mammal, WingedAnimal {
};
int main(){
Bat bat; // Error: member found by ambiguous name lookup
Animal &animal = bat; // Error: ambiguous conversion from derived class 'Bat' to base class 'Animal'
return 0;
}
As declared above, a call to bat.eat()
/bat.planet
is ambiguous because there are two Animal (indirect) base classes in Bat, so any Bat object has two different Animal base class subobjects. So an attempt to directly bind a reference to the Animal subobject of a Bat object would fail, since the binding is inherently ambiguous.
To disambiguate, one would have to explicitly convert bat to either base class subobject:
Bat b;
Animal &mammal = static_cast<Mammal&> (b);
Animal &winged = static_cast<WingedAnimal&> (b);
In order to call eat()
, the same disambiguation, or explicit qualification is needed: static_cast<Mammal&>(bat).eat()
or static_cast<WingedAnimal&>(bat).eat()
or alternatively bat.Mammal::eat()
and bat.WingedAnimal::eat()
.
However, what we really desire is that Bat
should have only one way of implementing eat()
and one planet
variable. The solution is to use the virtual base class, which ensures that an object of class Bat inherits only one subobject of class Animal.
struct Animal {
virtual ~Animal() {};
int planet;
virtual void eat(){};
};
// Two classes virtually inheriting Animal:
struct Mammal : virtual Animal {
virtual void breathe(){};
};
struct WingedAnimal : virtual Animal {
virtual void flap(){};
};
// A bat is still a winged mammal
struct Bat : Mammal, WingedAnimal {
};
The usage of virtual destructor is necessary bc the class Animal
is manipulated polymorphically*.
Memory layout
This section is based on msvc X86. We use /d1 reportAllClassLayout
to print the memory layout of classes.
Simple inheritance
class A {
public:
int a = 1;
void ma() {};
};
class B : public A {
public:
int b = 2;
void mb() {};
};
class C : public B, public A {
int c = 3;
};
1>class A size(4):
1> +---
1> 0 | a
1> +---
1>
1>class B size(8):
1> +---
1> 0 | +--- (base class A)
1> 0 | | a
1> | +---
1> 4 | b
1> +---
1>
1>class C size(16):
1> +---
1> 0 | +--- (base class B)
1> 0 | | +--- (base class A)
1> 0 | | | a
1> | | +---
1> 4 | | b
1> | +---
1> 8 | +--- (base class A)
1> 8 | | a
1> | +---
1>12 | c
1> +---
Vitual function & vftable
Based on the example above, we replace the original function in class A
with three virtual function ma
, knowing that there are in the same scope, it is function overload. In B
, two of them are override, so function overload still exists in B
.
class A {
public:
int a = 1;
virtual ~A() {};
void virtual ma() {};
void virtual ma(int i) {};
void virtual ma(int x, int y) {};
};
class B : public A {
public:
int b = 2;
void ma() override {}; // can have the keyword 'virtual'
void ma(int x, int y) override {};
void virtual mb() {};
};
1>class A size(8):
1> +---
1> 0 | {vfptr} <----- A pointer point to the virtual
1> 4 | a function table vftable A::$vftable@.
1> +--- The vftable stores virtual functions.
1>
1>A::$vftable@:
1> | &A_meta
1> | 0 <------------------- offset, as the ponter pointer to this
1> 0 | &A::dtor{} <---+ vftable occupies the first 4B, the
1> 1 | &A::ma | offset is 0 (offset to the top of the
1> 2 | &A::ma | memory of class A).
1> 3 | &A::ma |
1> +------- virtual destructor
1>A::{dtor} this adjustor: 0
1>A::ma this adjustor: 0
1>A::ma this adjustor: 0
1>A::ma this adjustor: 0
1>A::__delDtor this adjustor: 0
1>A::__vecDelDtor this adjustor: 0
1>
1>class B size(12):
1> +---
1> 0 | +--- (base class A)
1> 0 | | {vfptr}
1> 4 | | a
1> | +---
1> 8 | b
1> +---
1>
1>B::$vftable@:
1> | &B_meta
1> | 0
1> 0 | &B::{dtor} <----- class B has a virtual destructor without declaration
1> 1 | &B::ma <----- overrideen by class B
1> 2 | &A::ma <----- not overriden
1> 3 | &B::ma
1> 4 | &B::mb
1>
1>B::ma this adjustor: 0
1>B::ma this adjustor: 0
1>B::mb this adjustor: 0
1>B::{dtor} this adjustor: 0
1>B::__delDtor this adjustor: 0
1>B::__vecDelDtor this adjustor: 0
Virtual based inheritance & vbtable
Wrong implementation
First, consider the (wrong) case where the virtual based inheritance is not used:
class A {
public:
int a = 1;
virtual ~A() {};
void virtual ma() {};
};
class B : public A {
public:
int b = 2;
void virtual mb() {};
};
class C : public A {
public:
int c = 3;
void virtual mc() {};
};
class D : public B, public C {
public:
int d = 4;
void virtual md() {};
};
1>class A size(8):
1> +---
1> 0 | {vfptr}
1> 4 | a
1> +---
1>
1>A::$vftable@:
1> | &A_meta
1> | 0
1> 0 | &A::{dtor}
1> 1 | &A::ma
1>
1>A::{dtor} this adjustor: 0
1>A::ma this adjustor: 0
1>A::__delDtor this adjustor: 0
1>A::__vecDelDtor this adjustor: 0
1>
1>class B size(12):
1> +---
1> 0 | +--- (base class A)
1> 0 | | {vfptr}
1> 4 | | a
1> | +---
1> 8 | b
1> +---
1>
1>B::$vftable@:
1> | &B_meta
1> | 0
1> 0 | &B::{dtor}
1> 1 | &A::ma
1> 2 | &B::mb
1>
1>B::mb this adjustor: 0
1>B::{dtor} this adjustor: 0
1>B::__delDtor this adjustor: 0
1>B::__vecDelDtor this adjustor: 0
1>
1>class C size(12):
1> +---
1> 0 | +--- (base class A)
1> 0 | | {vfptr}
1> 4 | | a
1> | +---
1> 8 | c
1> +---
1>
1>C::$vftable@:
1> | &C_meta
1> | 0
1> 0 | &C::{dtor}
1> 1 | &A::ma
1> 2 | &C::mc
1>
1>C::mc this adjustor: 0
1>C::{dtor} this adjustor: 0
1>C::__delDtor this adjustor: 0
1>C::__vecDelDtor this adjustor: 0
1>
1>class D size(28):
1> +---
1> 0 | +--- (base class B)
1> 0 | | +--- (base class A)
1> 0 | | | {vfptr}
1> 4 | | | a
1> | | +---
1> 8 | | b
1> | +---
1>12 | +--- (base class C)
1>12 | | +--- (base class A)
1>12 | | | {vfptr}
1>16 | | | a
1> | | +---
1>20 | | c
1> | +---
1>24 | d
1> +---
1>
1>D::$vftable@B@:
1> | &D_meta
1> | 0
1> 0 | &D::{dtor}
1> 1 | &A::ma------------------------------+
1> 2 | &B::mb | This
1> 3 | &D::md | is
1> | where
1>D::$vftable@C@: | ambiguity
1> | -12 | comes
1> 0 | &thunk: this-=12; goto D::{dtor} | from
1> 1 | &A::ma -----------------------------+
1> 2 | &C::mc
1>
1>D::md this adjustor: 0
1>D::{dtor} this adjustor: 0
1>D::__delDtor this adjustor: 0
1>D::__vecDelDtor this adjustor: 0
A simple example of virtual base inheritance
Before the comparation with the correct one with virtual based class, let's see a simple virtual base class example:
class A {
public:
int a = 1;
virtual ~A() {};
void virtual ma() {};
};
class B {
public:
int b = 2;
virtual ~B() {};
void virtual mb() {};
};
class C : virtual public A, virtual public B {
public:
int c = 3;
void virtual mc() {};
};
1>class A size(8):
1> +---
1> 0 | {vfptr}
1> 4 | a
1> +---
1>
1>A::$vftable@:
1> | &A_meta
1> | 0
1> 0 | &A::{dtor}
1> 1 | &A::ma
1>
1>A::{dtor} this adjustor: 0
1>A::ma this adjustor: 0
1>A::__delDtor this adjustor: 0
1>A::__vecDelDtor this adjustor: 0
1>
1>class B size(8):
1> +---
1> 0 | {vfptr}
1> 4 | b
1> +---
1>
1>B::$vftable@:
1> | &B_meta
1> | 0
1> 0 | &B::{dtor}
1> 1 | &B::mb
1>
1>B::{dtor} this adjustor: 0
1>B::mb this adjustor: 0
1>B::__delDtor this adjustor: 0
1>B::__vecDelDtor this adjustor: 0
1>
1>class C size(28):
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr} <-------------- pointer to virtual base class table.
1> 8 | c It is used to determine the exact
1> +--- location of the virtual base classes;
1> +--- (virtual base A)
1>12 | {vfptr}
1>16 | a
1> +---
1> +--- (virtual base B)
1>20 | {vfptr}
1>24 | b
1> +---
1>
1>C::$vftable@C@:<------------------ virtual base class table.
1> | &C_meta
1> | 0
1> 0 | &C::mc
1>
1>C::$vbtable@: <------------------- virtual base class table.
1> 0 | -
1> 1 | 8 (Cd(C+4)A) <------------ offset of the vfptr of virtual base A from vbptr
1> 2 | 16 (Cd(C+4)B) <----------- offset of the vfptr of virtual base B from vbptr
1>
1>C::$vftable@A@:
1> | -12 <--------------------- offset of the vfptr of virtual base A from the top
1> 0 | &C::{dtor} ----------- the virtual destructor of C----+
1> 1 | &A::ma |
1> |
1>C::$vftable@B@: |
1> | -20 |
1> 0 | &thunk: this-=8; goto C::{dtor} ----------------------+
1> 1 | &B::mb
1>
1>C::mc this adjustor: 0
1>C::{dtor} this adjustor: 12
1>C::__delDtor this adjustor: 12
1>C::__vecDelDtor this adjustor: 12
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> A 12 4 4 0
1> B 20 4 8 0
The correct implementation
Now, Let's compare the wrong & correct implementation of the diamond problem:
class A {
public:
int a = 1;
virtual ~A() {};
void virtual ma() {};
};
class B : virtual public A {
public:
int b = 2;
void virtual mb() {};
};
class C : virtual public A {
public:
int c = 3;
void virtual mc() {};
};
class D : public B, public C {
public:
int d = 4;
void virtual md() {};
};
1>class A size(8):
1> +---
1> 0 | {vfptr}
1> 4 | a
1> +---
1>
1>A::$vftable@:
1> | &A_meta
1> | 0
1> 0 | &A::{dtor}
1> 1 | &A::ma
1>
1>A::{dtor} this adjustor: 0
1>A::ma this adjustor: 0
1>A::__delDtor this adjustor: 0
1>A::__vecDelDtor this adjustor: 0
1>
1>class B size(20):
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | b
1> +---
1> +--- (virtual base A)
1>12 | {vfptr}
1>16 | a
1> +---
1>
1>B::$vftable@B@:
1> | &B_meta
1> | 0
1> 0 | &B::mb
1>
1>B::$vbtable@:
1> 0 | -4
1> 1 | 8 (Bd(B+4)A)
1>
1>B::$vftable@A@:
1> | -12
1> 0 | &B::{dtor}
1> 1 | &A::ma
1>
1>B::mb this adjustor: 0
1>B::{dtor} this adjustor: 12
1>B::__delDtor this adjustor: 12
1>B::__vecDelDtor this adjustor: 12
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> A 12 4 4 0
1>
1>class C size(20):
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | c
1> +---
1> +--- (virtual base A)
1>12 | {vfptr}
1>16 | a
1> +---
1>
1>C::$vftable@C@:
1> | &C_meta
1> | 0
1> 0 | &C::mc
1>
1>C::$vbtable@:
1> 0 | -4
1> 1 | 8 (Cd(C+4)A)
1>
1>C::$vftable@A@:
1> | -12
1> 0 | &C::{dtor}
1> 1 | &A::ma
1>
1>C::mc this adjustor: 0
1>C::{dtor} this adjustor: 12
1>C::__delDtor this adjustor: 12
1>C::__vecDelDtor this adjustor: 12
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> A 12 4 4 0
1>
1>class D size(36):
1> +---
1> 0 | +--- (base class B)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | b
1> | +---
1>12 | +--- (base class C)
1>12 | | {vfptr}
1>16 | | {vbptr}
1>20 | | c
1> | +---
1>24 | d
1> +---
1> +--- (virtual base A)
1>28 | {vfptr}
1>32 | a
1> +---
1>
1>D::$vftable@B@:
1> | &D_meta
1> | 0
1> 0 | &B::mb
1> 1 | &D::md
1>
1>D::$vftable@C@:
1> | -12
1> 0 | &C::mc
1>
1>D::$vbtable@B@:
1> 0 | -4
1> 1 | 24 (Dd(B+4)A)
1>
1>D::$vbtable@C@:
1> 0 | -4
1> 1 | 12 (Dd(C+4)A)
1>
1>D::$vftable@A@: <-------- virtual base inheritance mechanism make an only vftable
1> | -28 of the virtual base class, thus we get rid of the ambiguity
1> 0 | &D::{dtor} <------ the virtual destructor of this class is always in the
1> 1 | &A::ma vftable of the virtual base class(es)
1>
1>D::md this adjustor: 0
1>D::{dtor} this adjustor: 28
1>D::__delDtor this adjustor: 28
1>D::__vecDelDtor this adjustor: 28
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> A 28 4 4 0
Use function pointers to 'print' vftable (not recommanded)
Take the class defined as follows for example (msvc, X86), by putting cout
of its own qualified (scope specified) function name (string) into the function bodies and using function pointers, we are able to 'print' vftable.
#include<iostream>
using namespace std;
class A {
public:
int a = 1;
// virtual ~A() {};
void virtual ma() {
cout << "A::void virtual ma()" << endl;
};
void virtual ma2() {
cout << "A::void virtual ma2()" << endl;
};
};
class B : public A {
public:
int b = 2;
void ma() override {
cout << "B::void virtual ma()" << endl;
};
void virtual mb() {
cout << "B::void virtual mb()" << endl;
};
};
typedef void(*vfptr)(); // create an alias name for the function
//pointer which will point to the virtual functions
void printClassBvf(B b) {
void* classB_address = &b; // &b is the address of b, the address of
// vftable is stored in the first 4B
int* classB_address_int = reinterpret_cast<int*>(classB_address);
// reinterpret_cast is a very special and dangerous
// type of casting operator, and is suggested to use
// it using data type. In X86, the size of pointer is
// 4B, so we convert it to int*
int vftable_address = *classB_address_int;
// the virtual table address in int
int* vftable_address_int = reinterpret_cast<int*> (vftable_address);
// convert the virtual table address in int*. The
// first 4B of it stores the address of the first
// virtual function,
int vf_address = *vftable_address_int; // virtual function address
vfptr vf_address_vfptr = reinterpret_cast<vfptr>(vf_address);
// convert the virtual function address to vfptr type
cout << "The vftable of class B is:" << endl;
while (vf_address_vfptr != NULL) {
vf_address_vfptr();
vftable_address_int++;
vf_address_vfptr = reinterpret_cast<vfptr>(*vftable_address_int);
}
}
int main() {
B b;
printClassBvf(b);
return 0;
}
Notice that this method is very limited and unsafe, the virtual destructor definition or overloaded virtual functions will not work by this method.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。