C++ 多态

目录

    • 多态的构成条件
    • 经典题目
    • override 和 final
    • 重载、覆盖(重写)、隐藏(重定义)的对比
    • 抽象类
    • 虚函数表和多态原理
    • 虚函数和虚表存在哪?
    • 多继承中的虚表
    • 菱形继承和菱形虚拟继承中的多态
    • 常见问题

多态的概念:通俗来说,就是多种形态
比如学生买学生票,成人买成人票

多态的构成条件

多态的条件:
在继承中
1、虚函数重写
2、父类的指针或者引用去调用虚函数

虚函数:被virtual修饰的类成员函数称为虚函数
虚函数重写
父子继承关系的两个虚函数,三同(函数名/参数/返回)
virtual只能修饰成员
三同(函数名/参数/返回)的例外:协变->返回值可以不同,但是必须是父子类关系的指针或者引用(可以是别的类的)
派生类重写的虚函数函数可以不加virtual(建议都加上)

class Person {
public:
	virtual A* BuyTicket() 
	{ 
		cout << "Person买票-全价" << endl;
		return nullptr;
	}

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

class Student : public Person {
public:
	virtual B* BuyTicket()
	{ 
		cout << "Student买票-半价" << endl;
		return nullptr;
	}

	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p = new Person;

	// 析构是虚函数,才能正确调用析构函数
	// p->destrutor() + operator delete(p)
	delete p;

	p = new Student;

	// p->destrutor() + operator delete(p)
	delete p;

	return 0;
}

可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

在这里插入图片描述

所以当出现new场景时 析构是虚函数,才能正确调用析构函数

C++ 多态_第1张图片

经典题目

C++ 多态_第2张图片

继承下来不改变函数原型,继承的意思是可以复用,不会把test在B中生成一份。所以是A* this,B去调用那么就切片,A*指向的是B中A那部分
我们认为func参数是相同的,参数相同看的是类型相同(和个数、顺序相同),多态调用指向的是子类,那么调用子类(B类)的func

虚函数的重写继承的是父类的接口声明,重写的是实现,意味着val缺省值用的是父类的接口val=1,所以答案是B->1

如果修改一下

   int main(int argc ,char* argv[])
   {
       B*p = new B;
       p->func();
       return 0;
   }

结果是B->0,因为不是父类的指针或者引用去调用虚函数,所以这是普通调用

override 和 final

final 修饰类,不能被继承
final 修饰虚函数,不能被重写,不是虚函数不能用final修饰

class Car
{
public:
 	virtual void Drive() final {}
};

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
 	virtual void Drive(){}//不加virtual就报错
};
class Benz :public Car {
public:
 	virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

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

C++ 多态_第3张图片

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

间接强制去派生类去重写

class Car
{
public:
	// 纯虚函数
	// 1、间接强制去派生类去重写
	// 2、抽象类-不能实例化出对象
	virtual void Drive() = 0;

	void func()
	{
		cout << "void func()" << endl;
	}
};

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

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

void func(Car* ptr)
{
	ptr->Drive();
}

int main()
{
	func(new Benz);
	func(new BMW);

	return 0;
}

虚函数表和多态原理

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

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

	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}

	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
	char _ch;
};

int main()
{
	cout << sizeof(Base) << endl;//记得算上虚函数表指针
	Base b;

	return 0;
}

C++ 多态_第4张图片
C++ 多态_第5张图片

ptr只需要在对象的头4个字节取虚函数表指针,在虚函数表里拿到函数的地址
ptr指向谁,就在谁的虚函数表里找虚函数,再调用。
C++ 多态_第6张图片

为什么一定要指针或引用才能实现多态?

C++ 多态_第7张图片
如果子类不重写虚函数,父类和子类对象的虚表是否一样?
不一样,虽然可以一样,但编译器不想,共用一张虚表节省不了多少空间,一会一样一会不一样容易混乱

如果是同类型的多个对象的虚表呢?
是一样的

虚函数和虚表存在哪?

虚函数和普通函数一样,都存在代码段,同时把虚函数地址存了一份到虚函数表

虚函数表存在代码段(常量区)
C++ 多态_第8张图片

vs 32位下取对象的头4个字节就可以得到虚函数表指针,虚函数表末尾有0
用 * ((int*)&b1)拿到前4个字节
在这里插入图片描述

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

void func()
{
	cout << "void func()" << endl;
}

int main()
{
	Base b1;
	Base b2;

	static int a = 0;
	int b = 0;
	int* p1 = new int;
	const char* p2 = "hello world";
	printf("静态区:%p\n", &a);
	printf("栈:%p\n", &b);
	printf("堆:%p\n", p1);
	printf("代码段:%p\n", p2);
	printf("虚表:%p\n", *((int*)&b1));
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", func);

	return 0;
}

函数名是函数地址,属于类域要加上,成员函数比较特殊,想取地址需要加上&

C++ 多态_第9张图片

虚函数地址一定被放入虚函数表吗?

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
	void func5() { cout << "Derive::func5" << endl; }
private:
	int b;
};

class X :public Derive {
public:
	virtual void func3() { cout << "X::func3" << endl; }
};

//虚函数的地址一定会被放进类的虚函数表吗?是的
int main()
{
	Base b;
	Derive d;
	X x;

	Derive* p = &d;
	p->func3();

	p = &x;
	p->func3();

	return 0;
}

监视窗口是被处理过的,可能会出现看不到虚函数表中虚函数地址的情况,在内存看会更真实
C++ 多态_第10张图片
怀疑后面的两个地址是虚函数地址,但监视窗口看不了,那么我们打印一下虚表
C++ 多态_第11张图片

// 打印虚表
typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		f();
	   //(*f)();
	}
	printf("\n");
}

int main()
{
	void (*f1)();
	VFUNC f2;

	cout << sizeof(long long) << endl;

	Base b;
	PrintVFT((VFUNC*)(*((long long*)&b)));

	Derive d;
	X x;

	// PrintVFT((VFUNC*)&d);
	PrintVFT((VFUNC*)(*((long long*)&d)));

	PrintVFT((VFUNC*)(*((long long*)&x)));

	return 0;
}

整形和浮点型是能强转的,都表示数据大小,指针之间也可以,整形和指针也可以,这些数据类型之间有关联可以强转。如果没有关联,像string和int,就不能强转了。
强转会产生中间变量,基类和派生类之间有赋值兼容规则,不会产生中间变量,不是强转。

多继承中的虚表

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() 
	{ 
		cout << "Derive::func1" << endl;
	}

	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

int main()
{
	Derive d;
	PrintVFT((VFUNC*)(*(int*)&d));

	PrintVFT((VFUNC*)(*(int*)((char*)&d+sizeof(Base1))));
	Base2* ptr = &d;
	PrintVFT((VFUNC*)(*(int*)ptr));

	Derive d;
	Base1* p1 = &d;
	p1->func1();

	Base2* p2 = &d;
	p2->func1();

	return 0;
}

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
C++ 多态_第12张图片
拓展
发现都是调用func1,但是地址不一样

因为这个地址不是真实的地址,在真实地址外面包裹了几层,需要jmp去找
C++ 多态_第13张图片

C++ 多态_第14张图片

this要能访问整个对象,那么在对象的开始会比较合适,p1去调用func1的时候,p1的位置和Derive的地址是重叠的,值不需要修改,只要改一下类型即可。
如果是p2去调用func1,在真正调用之前得把ecx减回开头位置,因为调用的func1是Derive的,this也是Derive的
为了保证p2调用的this指针位置正确,所以ecx会减

菱形继承和菱形虚拟继承中的多态

class A
{
public:
	virtual void func1() 
	{ 
		cout << "A::func1" << endl;
	}
public:
	int _a;
};

class B : public A
//class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}

public:
	int _b;
};

class C : public A
//class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}

public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1" << endl;
	}

	virtual void func2()
	{
		cout << "D::func2" << endl;
	}
public:
	int _d = 1;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

菱形继承也是多继承,d有两张虚表,d继承B,B有一张虚表,d继承了C,C有一张虚表。

如果是菱形虚拟继承,d的func2本来是要放到B里的,但B自己没有独立的虚表,B和C共享了A,A有一张虚表,所以d自己开了一张虚表放func2,d有两张虚表。

菱形虚拟继承下:
如果B、C单独再增加虚函数,虚表还会变多

class A
{
public:
	virtual void func1() 
	{ 
		cout << "A::func1" << endl;
	}
public:
	int _a;
};

//class B : public A
class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}

	virtual void func3()
	{
		cout << "B::func3" << endl;
	}

public:
	int _b;
};

//class C : public A
class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}

	virtual void func5()
	{
		cout << "C::func5" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1" << endl;
	}

	virtual void func2()
	{
		cout << "D::func2" << endl;
	}
public:
	int _d = 1;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

A有一张虚表,B和C不敢往里放,因为这是共享的虚表,B有虚函数,B就再搞一张虚表,C也搞一张。D增加的放到B独立的虚表中,D有三张虚表。
C++ 多态_第15张图片
全f是-1,fc是-4
虚基表中一个是距离虚基类的,一个是距离虚函数表的

常见问题

  1. 什么是多态?
    静态的多态:函数重载
    动态的多态:1.父类的指针或引用 2.虚函数完成重写
    指向谁就调用谁的虚函数,实现多种形态
  2. inline函数可以是虚函数吗?
    可以,普通调用,inline起作用。多态调用,inline不起作用
  3. 静态成员可以是虚函数吗?
    不可以,编译报错,static成员没有this指针,它可以指定类域调用,无法构成多态,没有意义
  4. 构造函数可以是虚函数吗?
    不可以,编译报错,对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。虚函数多态调用,要到虚表中找,可是虚表指针还没初始化
  5. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    最好是虚函数。
    父类指针 = new 子类对象 ;
    delete 父类指针
    这种场景只有析构是虚函数构成重写,构成多态时,才能正确调用子类的析构函数
  6. 对象访问普通函数快还是虚函数更快?
    普通调用,一样快的。
    多态调用,要慢一些,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
    如果是虚函数,但不构成多态调用,虚函数和普通函数调用是一样快的
  7. 虚函数表是在什么阶段生成的,存在哪的?
    虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)。虚函数表指针构造时,才初始化给对象

你可能感兴趣的:(C++进阶,c++)