C++:多态 详解

C++:多态 详解多态的概念:通俗来说,就是多种形态,。举个例子:比如,当买票时,是全价买票;买票时,是半价买票;买票时是优先买票。再举个栗子:最近为了,支付宝年底经常会做诱人的的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛….。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额=;……………………………

目录

一.多态的概念

二.多态的定义及实现

1.重写/覆盖 的要求

2.多态两个要求:

3.多态的切片示意图

4.多态演示:

买票场景下的多态 完整代码

5.虚函数重写的例外:

协变(父类与子类虚函数返回值类型不同)

6.接口继承和实现继承

多态的坑题目(考接口继承)

7.析构函数的重写-析构函数名统一会被处理成destructor()

8.C++11 override 和 final

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

10.抽象类

三.多态的原理

1.虚函数介绍

2.虚函数表

3.虚表存储

(1)虚函数重写/覆盖 语法与原理层解释

(2)虚表存储解释

多态调用和普通调用底层解释(编译时多态/运行时多态)

(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?

(4)静态和动态的多态(了解)

(5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到

(6)同一类型对象,共用一个虚表

(7)虚表存在常量区/代码段

4.多继承,虚表的存储(一个子类继承两个父亲时)

四.虚函数使用规则

4. inline函数可以是虚函数吗?


一.多态的概念

多态的概念:通俗来说,就是多种形态,
具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态
举个例子:比如
买票这个行为
,当
普通人
买票时,是全价买票;
学生
买票时,是半价买票;
军人
买票时是优先买票。
再举个栗子:
最近为了
争夺在线支付市场
,支付宝年底经常会做诱人的
扫红包

支付

给奖励金
活动。那么大家想想为什么有人扫的红包又大又新鲜
8
块、
10


,而有人扫的红包都是
1
毛,
5

….
。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额
=
random()%99
;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
去使用支付宝,那么就你扫码金额
= random()%1
;总结一下:
同样是扫码动作,不同的用户扫
得到的不一样的红包,这也是一种多态行为。
ps
:支付宝红包问题纯属瞎编,大家仅供娱乐。
总结:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如
Student
继承了 Person。
Person
对象买票全价,
Student
对象买票半价。

二.多态的定义及实现

1.重写/覆盖 的要求

重写/覆盖:
子类中有一个跟父类完全相同的虚函数,子类的虚函数重写了基类的虚函数

即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)

2.多态两个要求:

 1、被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
 2、父类指针或者引用去调用虚函数。

3.多态的切片示意图

(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数

C++:多态 详解

(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func

 C++:多态 详解


class A
{
public:
	virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
	virtual void test(){ func(); }
};

class B : public A
{
public:
	void func(int val = 0){ std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	B*p1 = new B;
	//p1->test();	这个是多态调用,下有讲解 二->6
	p1->func();	//普通调用

	A*p2 = new B;
	p2->func();	//多态调用

	return 0;
}

4.多态演示:

class Person {
public:
	Person(const char* name)
		:_name(name)
	{}

	// 虚函数
	virtual void BuyTicket()
    { 
        cout << _name << "Person:买票-全价 100¥" << endl;
    }

protected:
	string _name;
	//int _id;
};

class Student : public Person {
public:
	Student(const char* name)
		:Person(name)
	{}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket() 
    { 
        cout << _name << " Student:买票-半价 50 ¥" << endl; 
    }
};

void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

int main()
{
        string name;
		cin >> name;
		Student s(name.c_str());
		Pay(s);
}

C++:多态 详解

买票场景下的多态 完整代码

普通人
买票时,是全价买票;
学生
买票时,是半价买票;
军人 买票时是优先买票。
class Person {
public:
	Person(const char* name)
		:_name(name)
	{}

	// 虚函数
	virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }

protected:
	string _name;
	//int _id;
};

class Student : public Person {
public:
	Student(const char* name)
		:Person(name)
	{}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
};

class Soldier : public Person {
public:
	Soldier(const char* name)
		:Person(name)
	{}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
};

// 多态两个要求:
// 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
// 2、父类指针或者引用去调用虚函数。

//void Pay(Person* ptr)
//{
//	ptr->BuyTicket();
//}

void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

// 不能构成多态
//void Pay(Person ptr)
//{
//	ptr.BuyTicket();
//}

int main()
{
	int option = 0;
	cout << "=======================================" << endl;
	do 
	{
		cout << "请选择身份:";
		cout << "1、普通人 2、学生 3、军人" << endl;
		cin >> option;
		cout << "请输入名字:";
		string name;
		cin >> name;
		switch (option)
		{
		case 1:
		{
				  Person p(name.c_str());
				  Pay(p);
				  break;
		}
		case 2:
		{
				  Student s(name.c_str());
				  Pay(s);
				  break;
		}
		case 3:
		{
				  Soldier s(name.c_str());
				  Pay(s);
				  break;
		}
		default:
			cout << "输入错误,请重新输入" << endl;
			break;
		}
		cout << "=======================================" << endl;
	} while (option != -1);

	return 0;
}
解释 2、父类指针或者引用去调用虚函数,传值调用不构成多态。
用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参
void Pay(Person* ptr)     //指针调用可以
{
	ptr->BuyTicket();
}

void Pay(Person& ptr)    //引用调用可以
{
	ptr.BuyTicket();
}

// 不能构成多态
//void Pay(Person ptr)    //传值调用不可以
//{
//	ptr.BuyTicket();
//}

5.虚函数重写的例外:

协变(父类与子类虚函数返回值类型不同)

子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。

虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。

子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f()  ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )        ps:我们自己写的时候子类虚函数也写上virtual

class A{};
class B : public A {};

// 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
// 
class Person {
public:
	virtual A* f() { 
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	// 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
	// 重写父类虚函数实现
	// ps:我们自己写的时候子类虚函数也写上virtual
	// B& f() { 
	virtual B* f() {
		cout << "virtual B* Student::f()" << endl;
		return nullptr; 
	}
};
int main()
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	return 0;
}

C++:多态 详解

6.接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现

多态的坑题目(考接口继承)

子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f()  ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )        ps:我们自己写的时候子类虚函数也写上virtual

C++:多态 详解

p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1

7.析构函数的重写-析构函数名统一会被处理成destructor()

只有派生类
Student
的析构函数重写了
Person
的析构函数,下面的
delete
对象调用析构函
数,才能构成多态,才能保证
p1

p2
指向的对象正确的调用析构函数。
函数名处理成destructor()
才能满足多态:
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,
都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;
 return 0; 
}

 2.注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
class Person {
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	// Person析构函数加了virtual,关系就变了
	// 重定义(隐藏)关系 -> 重写(覆盖)关系
	virtual ~Student()    //这里virtual加不加都行
	{
		cout << "~Student()" << endl;
		delete[] _name;
		cout << "delete:" << (void*)_name << endl;
	}

private:
	char* _name = new char[10]{ 'j','a','c','k' };
};

int main()
{
	// 对于普通对象是没有影响的
	//Person p;
	//Student s;

	// 期望delete ptr调用析构函数是一个多态调用
	// 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
	Person* ptr = new Person;
	delete ptr; // ptr->destructor() + operator delete(ptr)

	ptr = new Student;
	delete ptr;  // ptr->destructor() + operator delete(ptr)

	return 0;
}

8.C++11 override final

(1)final
:修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承

C++:多态 详解

C++:多态 详解

 (2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错

override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如 果重写成功,编译    通过,否则,编译失败,因此 override作用发生在编译时

override只能修饰子类的虚函数

override修饰子类成员函数虚函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写。(在子类里面是可以自己增加 成员函数的,如果这个成员函数不是虚函数,就不可以进行修饰)

 示例:如果父类没写virtual能检查出来并报错

C++:多态 详解

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

(只有重写要求原型相同,原型相同就是指 函数名/参数/返回值都相同)

函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。

重定义(也叫做隐藏)是指在继承体系中,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,此时子类的函数会屏蔽掉父类的那个同名函数。

重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。

C++:多态 详解

10.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW;
(1)子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
(2)父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似
另外纯虚函数更体现出了接口继承。
(3)纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数
C++:多态 详解

 抽象类  -- 在现实一般没有具体对应实体
 不能实例化出对象
 间接功能:要求子类需要重写,才能实例化出对象
class Car
{
public:
	virtual void Drive() = 0;
	//	// 实现没有价值,因为没有对象会调用他
	//	/*virtual void Drive() = 0
	//	{
	//		cout << " Drive()" << endl;
	//	}*/
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

.多态的原理

1.虚函数介绍

被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要

2.虚函数表

和菱形虚拟继承的虚基表不一样,那个存的是偏移量

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些
平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代
表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们
接着往下分析
C++:多态 详解

3.虚表存储

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

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

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

private:
	int _b = 1;
};

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

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	cout << sizeof(Derive) << endl;
	Derive d;

	Base* p = &b;
	p->Func1();
	p->Func3();
	
	p = &d;
	p->Func1();
	p->Func3();

//	/*Base& r1 = b;    引用也是多态调用
//	r1.Func1();
//	r1.Func3();
//
//	Base& r2 = d;
//	r2.Func1();
//	r2.Func3();*/
}

C++:多态 详解

(1)虚函数重写/覆盖 语法与原理层解释

–语法层的概念: 派生类对继承基类虚函数实现进行了重写
–原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数

(2)虚表存储解释

无论是子类还是父类中只要有虚函数都会多存一个指针,这个指针叫虚表指针,他指向一个指针数组,指针数组中存着各个虚函数的地址。

Func1是重写的函数,Base[0]中存的地址并非真正的Derive中Func1的地址,而是通过call这个地址,找到这个地址的内容,这个地址的内容指令又是jump到地址2,地址2存的才是真正的Derive中Func1的地址

C++:多态 详解

多态调用和普通调用底层解释(编译时多态/运行时多态)

C++语言的多态性分为编译时的多态性和运行时的多态性

①运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。

②编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载模板实现。

-在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

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

	void Buy() { cout << "Person::Buy()" << endl; }
};

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

	void Buy() { cout << "Student::Buy()" << endl; }
};

void Func1(Person* p)
{
	跟对象有关,指向谁调用谁 -- 运行时确定函数地址
	p->BuyTicket();
	跟类型有关,p类型是谁,调用就是谁的虚函数  -- 编译时确定函数地址
	p->Buy();
}
int main()
{
	Person p;
	Student s;

	Func1(&p);
	Func1(&s);

	return 0;
}

 C++:多态 详解

 C++:多态 详解

重点总结:
多态调用:运行时决议– 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议– 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)

(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?

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

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

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

private:
	int _b = 1;
};

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

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	cout << sizeof(Derive) << endl;
	Derive d;


	// 父类赋值给子类对象,也可以切片。为什么实现不了多态?
	Base r1 = b;
	r1.Func1();
	r1.Func3();

	Base r2 = d;
	r2.Func1();
	r2.Func3();

	return 0;
}

C++:多态 详解C++:多态 详解

我们发现r2没有拷贝子类d的虚表,则r2虚表中存的还是父类的虚表,调用时还是调用父类的func1,而不是子类切片后的func1

C++:多态 详解

(4)静态和动态的多态(了解)

C++:多态 详解

(5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到

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

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

	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};

// 取内存值,打印并调用,确认是否是func4
//typedef void(*)() V_FUNC; // 不支持这种写法
typedef void(*V_FUNC)();    // 只能这样定义函数指针

// 打印虚表
//void PrintVFTable(V_FUNC a[])
void PrintVFTable(V_FUNC* a)
{
	printf("vfptr:%p\n", a);

	for (size_t i = 0; a[i] != nullptr; ++i)    //VS下的虚表以空指针结束
	{
		printf("[%d]:%p->", i, a[i]);    //打印虚表中的所有函数的地址
		V_FUNC f = a[i];                //调用函数中打印函数,可以知道是哪个func函数
		f();
	}
}

int c = 2;

int main()
{
	Base b;
	Derive d;
	PrintVFTable((V_FUNC*)(*((int*)&d)));    //下有解释
}

 PrintVFTable((V_FUNC*)(*((int*)&d))); 解释:

因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

监视窗口和内存窗口:

C++:多态 详解

C++:多态 详解

(6)同一类型对象,共用一个虚表

一个类型公共一个虚表,所有这个类型对象都存这个虚表指针

C++:多态 详解

(7)虚表存在常量区/代码段

不可能存在栈,栈区是建立栈帧,出作用域栈帧销毁, 虚表是一个永久的存在,排除栈。

不可能存在堆区,堆区是动态申请,最后动态释放

可以存在静态区或者常量区,最可能存在常量区。通过下面打印地址可见虚表存储地址离常量区地址最近 

C++:多态 详解

 C++:多态 详解

4.多继承,虚表的存储(一个子类继承两个父亲时)

大体的结论就是:func1是重写的函数,在子类的两个父类的虚表中存储的func1地址不相同,但是通过一系列的call这个地址,这个地址的内容又是jump到另一个指令,最终都会跳到子类重写的func1地址上

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;
};

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()
{
	printf("%p\n", &Derive::func1);

	Derive d;
	//PrintVTable((VFPTR*)(*(int*)&d));
	PrintVTable((VFPTR*)(*(int*)&d));    
	PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
}

 PrintVTable((VFPTR*)(*(int*)&d)); 

因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )

C++:多态 详解

C++:多态 详解

 C++:多态 详解

结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值
 

四.虚函数使用规则

1. 什么是多态?答:参考本节内容
2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本节内容
3. 多态的实现原理?答:参考本节内容
虚函数使用规则:

(1)虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上

(2)静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

(3)友元函数不属于成员函数,不能成为虚函数

(4)静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表就无法实现多态,因此不能设置为虚函数

(5)析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)

4. inline函数可以是虚函数吗?

答:可以,不过多态调用的时候编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的
虚函数表指针
是在
构造函数初始化列表阶段才初始化的
。虚函数的意义是多态,多态调用时到虚函数表中去找,构造函数之前还没初始化,如何去找?
7. 析构函数可以是虚函数吗?
什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。析构函数名统一会被处理成destructor()
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:
虚函数表是在编译阶段就生成的
,一般情况下存在代码段(常量区)的。(
虚函数表指针初始化是指把虚函数表的指针放到对象中去,但生成仍是在编译阶段
10. C++菱形继承的问题?虚继承的原理?
答:参考继承课件。注意这里不要把虚函数表和虚基
表搞混了。
11. 什么是抽象类?抽象类的作用?
答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽
象类体现出了接口继承关系。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/38970.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注