Summary of inheritance and polymorphism in C + +

1, The essence and principle of inheritance


Summary:

  1. External objects can only be accessed by public members, but protected and private members cannot be accessed directly
  2. In the inheritance structure, the derived class can inherit the private member of the base class, but it cannot be accessed directly
  3. The difference between protected and private: the members defined in the base class want to be accessed by the derived class but do not want to be accessed externally. The related members in the base class are defined as protected. If neither the derived class nor the external class is accessed, the related members in the base class are defined as peivate
  4. Use class to define derived classes. The default inheritance method is private; Use struct to define derived classes. The default inheritance method is public

2, Construction process of derived classes

How do derived classes initialize member variables inherited from the base class?

  1. Derived classes can inherit all members (including methods and variables), except constructors and destructors. Initialize by calling the corresponding constructor of the base class
  2. The constructor and destructor of the derived class are responsible for initializing and cleaning up the derived class part; The initialization and cleanup of members inherited from the base class by the derived class are the responsibility of the constructor and destructor of the base class.

The process of constructing and destructing derived class objects

  1. The derived class calls the constructor of the base class to initialize the members inherited from the base class
  2. The derived class calls its own constructor to initialize its own unique members
  3. When the object scope expires, call the destructor of the derived class to release the external resources (heap memory, files) that may be occupied by the members of the derived class
  4. Call the destructor of the base class to release the external resources (heap memory, files) that may be occupied by the members of the derived class who inherit from the base class in the memory of the derived class
class Base {
public:
	Base(int data) :ma(data) {
		cout << "Base()" << endl;
	}
	~Base() {
		cout << "~Base()" << endl;
	}

protected:
	int ma;
};

class Derive :public Base {
public:
	// Incorrect writing method: Derive(int data):ma(data), mb(data)
	Derive(int data) :Base(data), mb(data){
		cout << "Derive()" << endl;
	}

	~Derive() {
		cout << "~Derive()" << endl;
	}

private:
		int mb;
};

3, Overload, hide

Overload: overloaded functions must be in the same scope, have the same function name and different parameter lists
Hide (SCOPE): in the inheritance structure, the member with the same name of the derived class hides the member with the same name of the base class

#include<iostream>

using namespace std;

class Base {
public:
	Base(int data) :ma(data) {
		cout << "Base()" << endl;
	}

	void show() {
		cout << "Base::show()" << endl;
	}

	void show(int) {
		cout << "Base::show(int)" << endl;
	}

	~Base() {
		cout << "~Base()" << endl;
	}

protected:
	int ma;
};

class Derive :public Base {
public:
	Derive(int data) :Base(data), mb(data){
		cout << "Derive()" << endl;
	}

	void show() {
		cout << "Derive::show()" << endl;
	}

	~Derive() {
		cout << "~Derive()" << endl;
	}

private:
		int mb;
};

int main() {

#if 0
	Derive d(20);
	d.show();
	// d.show(10);//Derive::show: function does not accept 1 parameter (hidden)
	d.Base::show(10);
#endif

	Base base(10);
	Derive derive(20);
	
	base = derive;

	// error
	derive = base;

	// Because it is a pointer to Base, the pointer only points to the space of Base size, and can only access the methods and members of Base unless forced
	Base* ptr_b = &derive;
	
	// error, because it is a pointer to Derive, which points to a space of Derive size,
	// However, the actual accessible space is only as large as Base, and the remaining space belongs to illegal access
	Derive* ptr_d = &base;
	return 0;
}

Summary: the type of pointer determines the ability of the pointer, but the actual accessible area is determined by the object pointed to. If the capability of the pointer is greater than the space actually allowed to be accessed (derive * ptr_d = & base), it is illegal access (directly compiled). If the capability of the pointer is less than the space actually pointed to (base * ptr_b = & derive;), Only data within the range pointed to by the pointer can be accessed.

4, Static binding and dynamic binding

Static binding (during compilation)

	Base base(10);
	Derive derive(20);
	Base* ptr_b = &derive;

	//Base::show(), static (compile time) binding (function call), call Base::show (0D71037h)  
	ptr_b->show();    
	//Base::show(int), static (compile time) binding (function call), call Base::show (0D712FDh)
	ptr_b->show(10); 

	// 4
	cout << sizeof(Base) << endl;  
	//8
	cout << sizeof(Derive) << endl;
	//class Base*
	cout << typeid(ptr_b).name() << endl;
	//class Base
	cout << typeid(*ptr_b).name() << endl;

What is the impact of adding virtual functions to a class?

  1. If a virtual function is defined in a class, the compiler generates a unique vftable virtual function table for this class type at the compilation stage. The contents stored in the virtual function table are RTTI pointer and virtual function address.
  2. When the program runs, each virtual function table will be loaded into memory rodata area.
  3. If a virtual function is defined in a class, when the program runs, a vfptr virtual function pointer will be stored at the beginning of the memory corresponding to the object corresponding to this class, pointing to the virtual function table vftable of the corresponding type. The vfptr of all objects defined by a type will point to the same virtual function table.
  4. The number of virtual functions in a class does not affect the memory occupied by the object (sharing a vfptr), but the size of the virtual function table vftable.
  5. If the return value, function name and parameter list of a method inherited from the derived class and the base class are the same, and the method of the base class is virtual, the method in the derived class is directly processed as a virtual function, that is, overwriting (note that only the method will be overwritten, and the data member will not be overwritten, but will store multiple, which can be accessed through the scope).

Dynamic binding (during runtime)

Instead of calling the method with class name and scope, it calls the address stored in the register, which can only be determined at run time

#include<iostream>
#include<typeinfo>

using namespace std;

class Base {
public:
	Base(int data):ma(data) {
		cout << "Base()" << endl;
	}

	virtual void show() {
		cout << "Base::show()" << endl;
	}

	virtual void show(int) {
		cout << "Base::show(int)" << endl;
	}

	~Base() {
		cout << "~Base()" << endl;
	}
	
protected:
	int ma;
};

class Derive :public Base {
public:
	Derive(int data) :Base(data), mb(data){
		cout << "Derive()" << endl;
	}

	void show() {
		cout << "Derive::show()" << endl;
	}

	~Derive() {
		cout << "~Derive()" << endl;
	}
	
private:
	int mb;
};

int main() {
	Base base(10);
	Derive derive(20);
	Base* ptr_b = &derive;

	/*
		View ptr_b is Base; View Base::show;
		1. If Base::show is an ordinary function, perform static binding and call Base::show 
		2. Base::show If it is a virtual function, it is bound dynamically (at run time) (function call, obtained from the virtual function table)
		mov eax, dword ptr[ptr_b]  ; According to the address of vfptr virtual function table
		mov ecx, dword ptr[eax]    ; Get the address of the virtual function from the virtual function table
		call ecx                   ; Call virtual function
	*/
	ptr_b->show();    //Derive::show()
	ptr_b->show(10); //Base::show(int)

	// 4 + 4(vfptr) = 8
	cout << sizeof(Base) << endl;  
	// 8 + 4(vfptr) = 12 
	cout << sizeof(Derive) << endl;
	//The pointer type is class Base * (it is directly determined during compilation and will not be changed)
	cout << typeid(ptr_b).name() << endl;
	/*
	First, PTR_ The type of B is Base, and then check whether Base has virtual functions
	1. No virtual function, * PTR_ B is the compile time (static) type, * ptr_b is class Base
	2. If there is a virtual function, * ptr_b is the type of runtime (dynamic), i.e. RTTI type
	*/
	cout << typeid(*ptr_b).name() << endl;
}

Since Derive inherits the virtual function from Base, RTTI (run time type identification) points to a pointer of RTTI type, which can be understood as an object name string. It originally stored & Base:: show() (inherited), but since the virtual function was rewritten in Derive, it overwrites & Base:: show() with & Derive:: show()

Override: if the method in the derived class and a method inherited from the base class have the same return value, function name and parameter list, and the method of the base class is virtual, the method in the derived class is directly processed as a virtual function. Then the original methods in the virtual function table of the derived class are overwritten.

Understanding virtual functions

Virtual functional dependency:

  1. Virtual functions can generate function addresses in vftable
  2. Object must exist (vfptr – > vftable – > virtual function address)

Which functions cannot be implemented as virtual functions?

  1. The constructor cannot be implemented as virtual. Only when the constructor is constructed can the object be generated and vfptr be available.
  2. The virtual function is called in the constructor, and no dynamic binding occurs. Any function called is static bound. (dynamic binding needs to call the method of the derived class, but the derived class object has not been constructed yet)
  3. Static calls to static member methods are object independent

virtual destructor

class Base {
public:
	Base(int data):ma(data) {
		cout << "Base()" << endl;
	}

	virtual void show() {
		cout << "Base::show()" << endl;
	}

	virtual void show(int) {
		cout << "Base::show(int)" << endl;
	}

	~Base() {
		cout << "~Base()" << endl;
	}
	
protected:
	int ma;
};

class Derive :public Base {
public:
	Derive(int data) :Base(data), mb(data), ptr(new int(data)){
		cout << "Derive()" << endl;
	}

	~Derive() {
		delete ptr;
		cout << "~Derive()" << endl;
	}
	
private:
	int mb;
	int* ptr;
};

int main() {
	Base* ptr_b = new Derive(10);
	ptr_b->show();
	delete ptr_b; 
}


The destructor of the derived class is not called, which leads to memory leakage in this code

delete ptr_b. description of execution process:
View ptr_b is Base *; Then check whether Base::~Base() is a normal function or a destructor
1. It is an ordinary function. The call to the destructor is static binding. The assembly code is: call Base::~Base
2. It is a virtual function, and the call to the destructor is a dynamic binding.
In the vftable of Derive, the destructor of the base class is replaced with & Derive:: ~ Derive (as long as the destructor in the base class is virtual, the destructor of the derived class is also virtual)

Code changes are as follows:

virtual ~Base(){
	cout << "~Base()" << endl;
}


When must the Base destructor be implemented as a virtual function?

Delete PTR when the pointer to the Base class points to a derived class object on the heap_ When Base calls the destructor, it must be dynamically bound because it must call the destructor of the derived class object. At this time, it is necessary to implement the destructor of Base into virtual. If it is a static binding, the destructor is called directly according to the type of the pointer, and the destructor of the derived class cannot be called.

Instance object, pointer and reference call virtual functions respectively

int main() {
	Base b;
	Derive d;
	// Through object access, it must be static binding
	b.show();
	d.show();

	// Dynamic binding (must be called by pointer), and the virtual function table of which object is pointed to is checked
	Base* ptr_b1 = &b;
	ptr_b1->show(); 
	
	Base* ptr_b2 = &d;
	ptr_b2->show();

	// Reference is also the same as pointer, call eax
	Base& r_b1 = b;
	r_b1.show();

	Base& r_b2 = d;
	r_b2.show();

	/*
		p The type of is Derive *, so check the show in Derive and find that it is virtual,
		So check the first 4 bytes of the object, and take out the address of the calling function from the virtual function table
	*/
	Derive* p = (Derive*)&b;
	p->show();
}

Interviewer: how do you understand polymorphism?

It is divided into static (compile time) polymorphism and dynamic (run time) polymorphism.

  1. The manifestations of static polymorphism include function overloading and class template.
  2. In the inheritance structure, the Base class pointer (Reference) points to the derived class object, and the override method (virtual function) with the same name is called through the pointer (Reference). The derived class method is called according to the override object of the derived object to which the pointer points

Interviewer: what are the benefits of inheritance?

  1. Code reuse can be done
  2. Provide a unified virtual function interface in the base class and let the derived class override to realize polymorphism

5, Abstract class

When we don't want a class to instantiate an object, we can write this class as an abstract class. Classes with pure virtual functions are called abstract classes. Abstract classes cannot instantiate objects, but can define pointers and reference variables. Pure virtual functions refer to those that have no function body and must be rewritten, such as virtual void fun() = 0

class Animal {
public:
	Animal(string name) :_name(name) {

	}
	// Pure virtual function, must be overridden
	virtual void bark() = 0;
protected:
	string _name;
};

class Cat : public Animal{
public:
	Cat(string name) :Animal(name) {

	}

	void bark() {
		cout << _name << " miao miao" << endl;
	}
private:

};

class Dog : public Animal {
public:
	Dog(string name) :Animal(name) {

	}

	void bark() {
		cout << _name << " wang wang" << endl;
	}
private:

};

int main() {
	Cat cat("cat");
	Cat dog("dog");
	Animal* animal1 = &cat;
	Animal* animal2 = &dog;
	animal1->bark();
	animal2->bark();
}

6, Summary of written test questions

Written test question 1

void fun(){
	Animal* p1 = new Cat("cat");
	Animal* p2 = new Dog("dog");
	// Strong to int*
	int* p11 = (int*)p1;
	int* p22 = (int*)p2;
	// In fact, the first four bytes of the object are exchanged, that is, vfptr
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;
	// Access the virtual function table according to the vfptr after the exchange
	p1->bark();
	p2->bark();
}

Written test question 2

#include<iostream>
#include<string>

using namespace std;

class Base {
public:
	virtual void show(int i = 10) {
		cout << "Base::show() i = "<< i << endl;
	}
};

class Derive :public Base {
public:
	void show(int i = 20) {
		cout << "Derive::show() i = "<< i << endl;
	}
};

int main() {
	Base* p = new Derive();
	p->show();
	delete p;
	return 0;
}


Analysis: the assembly instruction is determined during compilation, and the parameter stack and access rights are also determined during compilation. Since p is Base *, and the default value of the show method in Base is 10, the default value of the parameter 10 is directly stacked. After pressing the stack, although call eax is bound dynamically and the derived method is called, the parameters have been pressed, and the printed value is 10 determined during compilation

Written test question 3

#include<iostream>

using namespace std;

class Base {
public:
	virtual void show() {
		cout << "Base::show()"<< endl;
	}
};

class Derive :public Base {
private:
	// The show method has been changed to private
	void show() {
		cout << "Derive::show()"<< endl;
	}
};

int main() {
	Base* p = new Derive();
	p->show();// Normal dynamic binding, access Derive::show()
	delete p;
	return 0;
}

Analysis: the access permission is determined during compilation. During compilation, the Base pointer is used for access. During compilation, the compiler can only see that the show method in the Base class is public, so it is considered that this show method is accessible during compilation. When compiling a derived class, you can directly put the address of the show method of the derived class into vftable. When dynamically binding (call eax), you can normally take the rewritten show method address from vftable. Similarly, the following code cannot be directly compiled

class Base {
// Access rights are determined at compile time
private:
	virtual void show() {
		cout << "Base::show()"<< endl;
	}
};

class Derive :public Base {
public:
	void show() {
		cout << "Derive::show()"<< endl;
	}
};

int main() {
	Base* p = new Derive();
	p->show();
	delete p;
	return 0;
}

Written test question 4

#include<iostream>

using namespace std;

class Base {
public:
	Base() {
		/*
			push ebp        
			mov ebp, esp  ; Open stack frame
			sub esp, 4Ch
			rep stos      ; windows Can do memory initialization
			Finally, write & base:: vftable to vfptr
		*/
		cout << "Base::Base()" << endl;
		clear();
	}
	
	virtual void show() {
		cout << "Base::show()"<< endl;
	}

	void clear() {
		// Current object cleared 0
		memset(this, 0, sizeof(*this));
	}
};

class Derive :public Base {
public:
	Derive() {
		/*	
			Each function comes in with the following four lines of code
			push ebp
			mov ebp, esp  ; Open stack frame
			sub esp, 4Ch
			rep stos      ; windows Can do memory initialization

			Finally, write & derive:: vftable to vfptr; Write the address of vftable to vfptr here
		*/
		cout << "Derive::Derive()" << endl;
	}

	void show() {
		cout << "Derive::show()" << endl;
	}
};

void fun1() {
	Base* p1 = new Base();
	p1->show();   // Dynamic binding, error reporting
	delete p1;
}

void fun2() {
	Base* p2 = new Derive();
	p2->show();  // Dynamic binding, normal access to vftable
	delete p2;
}

int main() {
	fun1();
	fun2();
	return 0;
}

fun1 analysis:

fun2 analysis:

The derived class object needs to first call Base(), write & base:: vftable to vfptr, and the memory of the current object is cleared to 0. Then Derive() is called, and &Derive:: vftable is written to vfptr, and the derived class virtual function table can be accessed normally.

Keywords: C++

Added by hairulazami on Sat, 25 Dec 2021 20:14:41 +0200