C++进阶 —— 多态

目录

一,多态及虚函数

构成多态条件

虚函数

override和final(C++11)

重载、重写(覆盖)、隐藏(重定义)对比

二,抽象类

接口继承和实现继承

三,多态原理

动态绑定和静态绑定

四,单继承和多继承关系的虚函数表

单继承中虚函数表

多继承中虚函数表

菱形继承、菱形虚拟继承


一,多态及虚函数

  • 多态是不同继承关系的类对象,调用同一函数,产生了不同的行为;

构成多态条件

  • 必须通过基类指针或引用调用虚函数;
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

  • 即被virtual修饰的类成员函数,称为虚函数;

虚函数重写(覆盖)

  • 派生类中有一个跟基类完全相同的虚函数,称子类虚函数重写了基类的虚函数;
  • 派生类虚函数与基类虚函数的返回值类型函数名参数列表完全相同;
class Person
{
public:
	virtual void BuyTicket() {cout << "No discount!" << endl;}
};

class Stu :public Person
{
public:
	virtual void BuyTicket() {cout << "Half discount!" << endl;}
};

void Func(Person& person) {person.BuyTicket();}

int main()
{
	Person person;
	Stu stu;
	Func(person);
	Func(stu);
	return 0;
}

注:重写基类虚函数时,派生类的虚函数可不加virtual关键字,虽然也构成重写(因继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但这种写法不规范,不建议使用;

虚函数重写的两个例外

  • 协变 covariant,基类与派生类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用;
  • 析构函数的重写,基类与派生类析构函数的名字不同;如基类析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类析构函数构成重写;虽然函数名不同,违背重写规则,可理解为编译器对析构函数的名称做了特殊处理;
//协变
class A{};
class B:public A{};

class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "No discount!" << endl;
		return new A;
	}
};

class Stu :public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "Half discount!" << endl;
		return new B;
	}
};

void Func(Person& person) {person.BuyTicket();}

int main()
{
	Person person;
	Stu stu;
	Func(person);
	Func(stu);
	return 0;
}
//析构函数重载
class Person
{
public:
	virtual ~Person() {cout << "~Person()" << endl;}
};

class Stu :public Person
{
public:
	virtual ~Stu() {cout << "~Stu()" << endl;}
};

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Stu;
	delete p1;
	delete p2;
	return 0;
}

override和final(C++11)

        C++对函数重写的要求比较严格,如函数名字母次序写反即无法构成重载,而这种错误在编译期间无法识别,只有在程序运行时没有得到预期结果才会发现得不偿失;因此C++11提供了关键字override和final,可帮助检测是否重写了;

final

  • 修饰类,防止子类继承该类;
  • 修饰虚函数,表示该虚函数不能在被重写;
class Car final{};
//不可将final类作为基类
class Benz :public Car {};
class Car
{
public:
	virtual void Drive() final {cout << "Car" << endl;}
};

class Benz :public Car
{
public:
    //报错,无法重写final函数
	virtual void Drive() {cout << "Benz" << endl;}
};

override

  • 检查派生类虚函数是否重写了基类某个虚函数,如没有编译报错;
class Car
{
public:
	virtual void Drive() {cout << "Car" << endl;}
};

class Benz :public Car
{
public:
    //报错,没有重写基类成员
	virtual void Dri() override {cout << "Benz" << endl;}
};

重载、重写(覆盖)、隐藏(重定义)对比

重载

  • 两个函数在同一作用域;
  • 函数名相同和参数不同;
class fatherA
{
public:
	void print() { cout << "->" << " fatherA " << endl; }
	void print(int) { cout << "->" << " fatherA(int) " << endl; }
};

重写(覆盖)

  • 两个函数分别在基类和派生类的作用域;
  • 函数名、参数、返回值都必须相同(协变例外);
  • 两个函数都必须为虚函数;
  • 实现运行时多态性;
class fatherA
{
public:
	virtual void print() { cout << "->" << " fatherA " << endl; }
};
class childB :public fatherA
{
public:
	virtual void print() { cout << "->" << " childB " << endl; }
};

C++进阶 —— 多态_第1张图片

 隐藏(重定义):

  • 两个函数分别在基类和派生类作用域;
  • 函数名相同;
  • 两个基类和派生类的同名函数不构成重写就构成重定义;
class fatherA
{
public:
	void print() { cout << "->" << " fatherA " << endl; }
};
class childB :public fatherA
{
public:
	void print() { cout << "->" << " childB " << endl; }
};

  

二,抽象类

概念:

  • 在虚函数的后面添加上“= 0”,则该函数为纯虚函数
  • 包含纯虚函数的类,称为抽象类接口类,抽象类不能实例化对象;
  • 派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象;
  • 纯虚函数规范了派生类必须重写,另外纯虚函数体现了接口继承;

接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现;
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口;所以如不需实现多态,不要把函数定义成虚函数;
class Car
{
public:
	virtual void Drive() = 0; //纯虚函数
};

class Benz :public Car
{
public:
	virtual void Drive() {cout << "Benz" << endl;}
};

class BMW :public Car
{
public:
	virtual void Drive() {cout << "BMW" << endl;}
};

void fun(Car& car) {car.Drive();}

int main()
{
	//Car car; //抽象类不可实例化对象
	Benz benz;
	BMW bmw;
	fun(benz);
	fun(bmw);

	Car* pbenz = new Benz;
	Car* pbmw = new BMW;
	pbenz->Drive();
	pbmw->Drive();
	return 0;
}

三,多态原理

class Car
{
public:
	virtual void fun() { cout << "fun()" << endl; }
protected:
	int _car;
};

int main()
{
	Car car;
	cout << sizeof(car) << endl; //结果为8
	return 0;
}

   

         上述案例结果为8,是因为除了成员变量_car,还有一个成员 _vfptr(在_car前面还是后面和平台有关),对象中的成员_vfptr为指针类型,叫做虚函数表指针;一个包含虚函数的类中,至少有一个虚函数表指针;虚函数表简称虚表,是存放该类所有虚函数地址的数组;

class Car
{
public:
	virtual void fun1() { cout << "fun1()" << endl; }
	virtual void fun2() { cout << "fun2()" << endl; }
	void fun3() { cout << "fun3()" << endl; }
protected:
	int _car;
};

class Benz :public Car
{
public:
	virtual void fun1() { cout << "Benz::fun1()" << endl; }
protected:
	int _benz;
};

int main()
{
	Car car;
	Benz benz;
	return 0;
}

C++进阶 —— 多态_第2张图片  

  • 派生类对象benz,也有一个虚表指针,是继承于父类的;
  • 基类对象car和派生类对象benz虚表是不一样的,发现fun1重写了,所以虚函数的重写也叫覆盖,即指虚表中虚函数的覆盖;重写是语法上的叫法,覆盖是原理上的叫法;
  • 虚函数fun2继承下来了,放进了虚表,fun3不是虚函数,就不会放进虚表;
  • 虚函数表,本质上是存放虚函数指针的指针数组,一般情况下这个数组最后面放一个nullptr;
  • 派生类虚表生成,首先将基类中的虚表内容拷贝一份到派生类虚表中,然后派生类重写基类中的虚函数,用派生类自己的虚函数覆盖基类中的虚函数,最后派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后;
  • 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,存在代码段,只是它的的指针又存到了虚表中,另外对象中存的不是虚表,存的是虚表指针;虚表存在代码段(vs平台);

满足多态以后的函数调用,是运行后到对象中去找的;

不满足多态的函数调用,是编译时就确认好的;

动态绑定和静态绑定

  • 静态绑定,又称前期绑定(早绑定),在程序编译期间确定了程序的行为,也称静态多态,如函数重载;
  • 多态绑定,又称后期绑定(晚绑定),在程序运行时,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称动态多态;

四,单继承和多继承关系的虚函数表

单继承中虚函数表

class Car
{
public:
	virtual void fun1() { cout << "fun1()" << endl; }
	virtual void fun2() { cout << "fun2()" << endl; }
protected:
	int _car;
};

class Benz :public Car
{
public:
	virtual void fun1() { cout << "Benz::fun1()" << endl; }
	virtual void fun3() { cout << "Benz::fun3()" << endl; }
	virtual void fun4() { cout << "Benz::fun4()" << endl; }
protected:
	int _benz;
};

typedef void(*VFPTR)(); //函数指针
void PrintVTable(VFPTR vtable[])
{
	cout << "虚表地址->" << vtable << endl;
	for (int i = 0; vtable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址: 0X%x->", i, vtable[i]);
		VFPTR f = vtable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Car car;
	Benz benz;
    
    //思路:取对象头4个字节,即是虚表指针,虚表是存虚函数指针的指针数组,最后为nullptr;
    //运行时可能会崩溃,是因为编译器对虚表处理不干净,编译器问题,在点击菜单栏生成-清理解决方案;
	VFPTR* vtable_car = (VFPTR*)(*(int*)&car);
	PrintVTable(vtable_car);
	VFPTR* vtable_benz = (VFPTR*)(*(int*)&benz);
	PrintVTable(vtable_benz);
	return 0;
}

C++进阶 —— 多态_第3张图片

C++进阶 —— 多态_第4张图片

C++进阶 —— 多态_第5张图片

多继承中虚函数表

  • 多继承派生类的未重写的虚函数,放在第一个继承基类的虚函数表中;
class Car1
{
public:
	virtual void fun1() { cout << "Car1::fun1()" << endl; }
	virtual void fun2() { cout << "Car1::fun2()" << endl; }
protected:
	int _car1;
};

class Car2
{
public:
	virtual void fun1() { cout << "Car2::fun1()" << endl; }
	virtual void fun2() { cout << "Car2::fun2()" << endl; }
protected:
	int _car2;
};

class Benz :public Car1, public Car2
{
public:
	virtual void fun1() { cout << "Benz::fun1()" << endl; }
	virtual void fun3() { cout << "Benz::fun3()" << endl; }
protected:
	int _benz;
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vtable[])
{
	cout << "虚表地址->" << vtable << endl;
	for (int i = 0; vtable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址: 0X%x->", i, vtable[i]);
		VFPTR f = vtable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Benz benz;

	VFPTR* vtable_benz_car1 = (VFPTR*)(*(int*)&benz);
	PrintVTable(vtable_benz_car1);
	VFPTR* vtable_benz_car2 = (VFPTR*)(*(int*)((char*)&benz+sizeof(Car1)));
	PrintVTable(vtable_benz_car2);
	return 0;
}

C++进阶 —— 多态_第6张图片

C++进阶 —— 多态_第7张图片

C++进阶 —— 多态_第8张图片

菱形继承、菱形虚拟继承

  • 实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面访问基类成员有一定的性能消耗;
  • C++虚函数表解析;
  • C++对象的内存布局;

你可能感兴趣的:(#,C++,编程语言,c++,开发语言,后端)