前言
本博客中的代码和解释都是在VS2019下的x86程序中进行的,涉及的指针都是 4 字节,如果要其他平台下测试,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等。
文章目录
- 前言
- 一、多态的概念
- 二、多态的定义及实现
- 2.1 构成多态的两个必要条件
- 2.2 什么虚函数?
- 2.3 什么是虚函数重写?
- 2.4 多态调用的例子
- 2.5 虚函数重写的三个例外
- 第一:派生类虚函数不加 virtual 关键字
- 第二:协变(基类与派生类虚函数返回值类型不同)
- 第三:析构函数的重写(基类与派生类析构函数的名字不同)
- 2.6 重载、覆盖(重写)、隐藏(重定义)的对比
- 2.7 C++11 override 和 final
- 三、抽像类
- 3.1 接口继承与实现继承
- 四、探究多态下的对象模型及认识虚表
- 4.1 虚函数指针与虚函数表
- 4.2 虚函数与虚函数表的存储位置
- 4.3 虚函数指针初始化和虚表生成时间
- 4.4 动态多态的原理
- 五、单继承和多继承关系的虚函数表
- 5.1 单继承中的虚函数表
- 5.2多继承中的虚函数表
- 六、多态相关的一些问题
一、多态的概念
多态是面向对象编程中一个重要特性,它允许以一致的方式来使用不同的对象得到不同的结果,或者说,某一个动作被不同的对象完成会得到不同的结果,这两种说法都是一样的。
在C++中,多态性有两种主要形式:编译时多态性(静态多态性)和运行时多态性(动态多态性)。
- 静态多态性:在程序编译阶段实现,表现为函数重载,通过传递不同的实参调用相应的同名函数获取不同的结果。
- 动态多态性:基于继承实现,指在程序运行阶段,根据具体拿到的类型确定程序的具体行为,调用具体的函数。
后面的内容都是关于动态多态,为了方便,接下来的内容的“多态”都默认指动态多态
二、多态的定义及实现
2.1 构成多态的两个必要条件
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数完成重写。
(注意:只有虚函数才有重写这个概念)
2.2 什么虚函数?
虚函数:即被关键字 virtual
修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.3 什么是虚函数重写?
虚函数的重写,又叫做虚函数的覆盖,当派生类中实现一个跟基类完全相同的虚函数,这时候称 “派生类的虚函数重写了基类的虚函数”。
派生类虚函数与基类虚函数的完全相同要求满足以下三同:① 返回值类型相同、② 函数名相同、③ 参数列表相同。
2.4 多态调用的例子
以下是一个多态调用的例子:
首先,左边 Func 函数中,people 是基类的引用,派生类 Student 完成了对基类 Person 的 BuyTicket() 的重写,满足多态调用。
其次,people 引用基类对象调用基类的 BuyTicket() ,引用派生类对象调用派生类重写的 BuyTicket() 。
2.5 虚函数重写的三个例外
C++中有三个形式上不满足函数重写的语法规定,但依旧是虚函数重写的特殊情况。
第一:派生类虚函数不加 virtual 关键字
上面那个例子中 ,Student 类中的虚函数像下面这样写也是可以编译通过的,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,但是该种写法不是很规范,建议基类和派生类都加上 virtual
,以提高可读性。
class Student : public Person {
public:
void BuyTicket() {
cout << "买票-半价" << endl;
}
};
第二:协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,派生类虚函数与基类虚函数的返回值类型可以不同,但要求基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,即返回值构成继承关系,这种做法称之为 “ 协变 ”。
以下代码为一个协变的例子:
// A、B构成继承关系
class A {};
class B : public A {};
class Person {
public:
// Person 返回 基类A 的 指针
virtual A* f() {
cout << "A* f()" << endl;
return new A;
}
};
class Student : public Person {
public:
// Student 返回 派生类B 的 指针
virtual B* f() {
cout << "B* f()" << endl;
return new B;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
p1->f();
p2->f();
return 0;
}
假设A、B不构成继承关系,就会引发报错
// 去掉继承关系
class A {}
class B {}
在VS2019中,编译器对协变进行了强制检查,如果没有强制检查,会发生什么?
首先,基类和派生类的f()
函数由于返回值类型不同不构成重写,不构成重写就满足多态调用,所以和普通的函数调用没有区别,普通函数调用取决于对象或者指针或者引用的类型。
其次,由于Person和Student是继承关系,f()
构成隐藏关系,由于编译器的赋值兼容转换机制且指针p1
和p2
的类型都是Person*
,两个指针会去调用Person的f()
,而不会去调用Student类的f()
。
而下面讲的第三个例外不实现成重写也会导致这个问题。
第三:析构函数的重写(基类与派生类析构函数的名字不同)
一个继承体系中,派生类和基类的析构函数都会被编译器特殊处理成 destructor(),所以基类和派生类的析构函数会构成隐藏关系,在派生类调用基类析构函数需要指定类域显式调用,现在可以解释为什么要做这种特殊处理了,是为了重写。
学了动态多态之后,函数调用可以分成两种:
- 普通调用,取决于指针或者引用或者对象的类型。
- 多态调用,取决于指针或者引用指向的对象。
下面这份代码中由于两个析构函数没有满足虚函数重写,无法进行多态调用,指针p2
仅对一个Student对象中的Person部分进行析构,Student对象内部的资源没有完全回收,这会导致内存泄漏问题。
// 析构隐藏
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,
// 才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,只要基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual
关键字,都与基类的析构函数构成重写。
// 析构重写
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,
// 才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
2.6 重载、覆盖(重写)、隐藏(重定义)的对比
2.7 C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
- final,修饰虚函数时,表示该虚函数不能再被重写;修饰一个类时,表示该类不能被继承。
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
三、抽像类
在虚函数的后面写上 =0
,则这个函数为纯虚函数。
class Car
{
public:
// Drive是纯虚函数
virtual void Drive() = 0;
};
包含纯虚函数的类被称之为抽象类(也叫接口类),抽象类不能被实例化对象。
抽象类定义了一个类可能发出的动作的原型,但既没有实现,也没有任何状态信息,引入抽象类的原因在于很多时候基类本身实例化不合情理的,例如车类作为一个基类可以派生出奔驰、宝马等子类,但是车类本身实例化是没有意义的。
这时候就可以将车类定义成抽象类,由于抽象类只能提供原型而无法被实例化,因此派生类必须提供接口的实现,派生类亦无法被实例化,纯虚函数规范了派生类必须重写。
class Car
{
public:
virtual void Drive() = 0;
};
// 奔驰类
class Benz :public Car
{
public:
// 完成重写
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
// 宝马类
class BMW :public Car
{
public:
// 不完成重写
};
int main()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
3.1 接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
下面这道题就体现了接口继承
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* p = new B;
p->test();
return 0;
}
A:A->0
B:B->1
C:A->1
D:B->0
E:编译出错 F:以上都不正确
答案是B,解析如下:
A、B是继承关系,A中有两个虚函数(func 和 test),B中有一个虚函数(func),func接口构成重写。
在 main 函数中:B* 的指针变量 p 指向一个B对象,p-> 告诉编译器要到 B 的类域中找 test 的定义,同时把 p 传给 this,换言之,this 指向的B对象。
编译器在 B 类中找不到 test,然后由于继承关系存在,到 A 类中去找,找到并且继承到了使用权,所以,会调用到 A 类中的 test 接口。
A 类的 test 接口调用了 func 函数,函数是通过 this 指针来调用的(this->func();),此时在 A 的类域中,this 的类型显然是 A*。
类型为A* 的 this 指针指向一个 B 对象,且 func 满足虚函数重写,会去调用 B 中的 func()。
虚函数的重写是接口继承,virtual void func(int val = 1),这时候 val 的是 1,所以答案是 B->1。
四、探究多态下的对象模型及认识虚表
4.1 虚函数指针与虚函数表
下面代码中,sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
按道理说,对象只存储成员变量,预期大小应该是 4 字节,可通过运行结果可以发现,Base对象的大小是 8 字节(看前言),因此,当一个类包含虚函数时,类对象模型肯定发生了改变。
接下来实例化出 Base 类的两个对象,然后通过监视窗口观察 Base 类的对象结构发现:
- Base类对象中除了_b成员,还多一个__vfptr指针放在对象的最前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),__vfptr指向一个叫做 vftable 的数组,数组里有两个元素,但监视窗口只显示了第一个元素,它是 Base::Func 的函数指针。
- Base 类实例化出的两个对象的 __vfptr 的内容都是一样的。
当一个类中包含虚函数成员,类对象模型如下:
- 对象内部除了自己定义的成员变量外,编译器自动添加了一个指针成员,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),该指针指向的是一个数组,被称为虚函数表,虚函数表也简称虚表,虚表里面存放的是虚函数的地址。
- 一个类的实例化出多个对象时,它们共享该类的虚表。
了解什么是虚表指针和虚表之后,Base的派生类对象模型又是怎样的呢?接着往下分析。
为了更好地测试,针对上面的代码改造成单继承但无虚函数重写的场景,查看派生类对象模型
- 我们增加一个派生类Derive去继承Base
- Base再增加一个虚函数Func2和一个普通函数Func3
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:
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过对监视窗口的观察可以看到:
- d 对象由两部分构成,一部分是基类继承下来的成员,另一部分是自己的成员。
- 派生类对象 d 中也有一个虚表指针,虚表指针存在基类部分的首个位置。
- 基类b对象和派生类d对象虚表指针是不一样的,可是虚表的内容是一样的,也就是说派生类对象会拷贝一份基类的虚表给自己。
- Func3 也继承下来了,但是不是它虚函数,所以不会放进虚表。
针对上面的代码的Derive中重写Func1改造成单继承且有虚函数重写的场景,再查看派生类对象模型
// Base 类不变
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
// main 函数不变
通过对监视窗口的观察可以看到:
- 派生类对 Func1 完成重写之后,派生类对象 d 的虚表发生部分变换,原本 Base::Func1 地址被重写后的 Derive::Func1 的地址覆盖,这就是为什么虚函数的重写也叫作覆盖,重写是语法的叫法,覆盖是原理层的叫法。
针对上面的代码的Derive中增加虚函数 Func4再查看派生类对象模型
// Base 类不变
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
// 增加虚函数 Func4
virtual void Func4()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
// main 函数不变
通过监视窗口 + 内存窗口的观察验证发现:
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
总结一下派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中。
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
4.2 虚函数与虚函数表的存储位置
这里还有一个很容易混淆的问题:
虚函数存在哪的?虚表存在哪的?
答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。
上面的回答的错的。
首先,虚表存的是虚函数指针,不是虚函数本身,虚函数和普通函数虽然在语法上一样的,但在编译器看来它们都是函数,经过编译之后都会生成地址和指令,指令存储在代码段的,地址存到了虚表中。
其次,对象中存的不是虚表,存的是虚表指针,虚表指针是对象的成员,如果对象在栈上的,虚表指针就在栈上,如果对象是new出来的,虚表指针就在堆上。
既然不确定虚表的存储位置,那样可以对比法来验证一下。
int main()
{
Base b;
Derive d;
int i = 0;
static int j = 0;
int* p1 = new int;
const char* p2 = "xxxxxxxxxxxxxxxxx";
Base* p3 = &b;
Derive* p4 = &d;
printf("栈:%p\n", &i);
printf("堆:%p\n", p1);
printf("静态区:%p\n", &j);
printf("常量区:%p\n", p2);
// vfptr在对象的第一个位置,x86下指针是4字节,类型强转(int*)p3获得vfptr
// 对vfptr解引用能够找到虚表第一个虚函数的地址
// 对比分析虚函数地址和哪个区的地址接近就在哪个区
printf("Base虚表首元素:%p\n", *(int*)p3);
printf("Derive虚表首元素:%p\n", *(int*)p4);
return 0;
}
测试结果发现,虚表上的函数指针和常量区(代码段)的地址是最接近,由此可以认为在VS下虚表是存储在常量区(代码段)。
Linux 发行版 CentOS 7.6 下的g++编译器的测试结果如下:
测试结果同样是发现虚表实在代码端上的。
4.3 虚函数指针初始化和虚表生成时间
先来一波猜测:
- 对象内部的虚函数指针成员是编译器自己加上去的,虚函数指针的初始化应当交由编译器在对象构造时进行的。
- 类与对象的语法部分规定:对象的成员变量的初始化必须经过初始化列表,如果虚函数指针是在调用构造函数期间初始化的,就能够说明虚函数指针在初始化列表完成初始化的。
- 在VS平台下,虚函数指针在对象模型的首位,假如虚函数指针的初始化时间比一个对象中任意一个成员还早就说明它是第一个被初始化。
- 在 C++ 中,虚函数转换成地址和指令是程序在编译期间完成的,对象的构造函数是在运行时期间被调用的,如果虚函数指针在初始化列表被初始化,说明虚表在虚函数指针被初始化之前就已经生成好了。
为 Base 类添加构造函数后验证结果如下:
- 虚表在编译阶段生成。
- 虚函数指针在运行阶段由编译器调用构造函数通过初始化列表初始化。
- 虚函数指针在VS的类对像模型中是第一个被初始化的。
4.4 动态多态的原理
多态调用通过基类的指针或者引用,指向基类调用基类的虚函数,指向派生类调用派生类的虚函数,通过对上面虚表的了解之后,不用说肯定是通过虚表来完成的,但具体的过程是怎么样的呢?
下面就用这份代码例子来做一个深入的研究:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
- 观察下图的红色箭头我们看到,
p
是指向mike
对象时,p->BuyTicket
在mike
的虚表中找到虚函数是Person::BuyTicket
。 - 观察下图的蓝色箭头我们看到,
p
是指向johnson
对象时,p->BuyTicket
在johson
的虚表中找到虚函数是Student::BuyTicket
。 - 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是基类的指针或引用调用虚函数,这是为什么?
第一:基类的指针或者引用指向派生类对象时,编译器会发生赋值兼容转换操作,将派生类对象中基类部分切割给基类的指针或者引用,然后基类的指针和引用可以把这些派生类对象当成基类对象来使用。
第二:由于继承的缘故,派生类的虚表指针是在基类部分的成员中的,切割之后基类的指针或者引用依旧能够使用派生类的虚表。
第三:如果不完成虚函数覆盖,派生类的虚表和基类的虚表是一样的,只有派生类完成了虚函数覆盖,虚表上的函数指针才会发生改变,基类指针或者引用才能调用到派生类重写的虚函数,否则只能调用到基类的虚函数。
- 为什么说动态多态是在运行时阶段实现的?
编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。
只有在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
五、单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
在前面 4.1 探究派生类对象模型中,通过下面三种情况基本了解清楚了:
- 单继承,派生类无虚函数覆盖
- 单继承,派生类有虚函数覆盖,但无自己的虚函数
- 单继承,派生类有虚函数覆盖,有自己的虚函数
这里不进行过多的赘述,不过可以将基类和派生类的虚表打印出来进行一个验证:
取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr
- 先取b的地址,强转成一个int*的指针
- 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
- 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
- 虚表指针传递给PrintVTable进行打印虚表
- 需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
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; }
private:
int b;
};
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()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
5.2多继承中的虚函数表
用下面这份代码来探究一下,多继承中派生类对象模型以及虚表结构:
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()
{
Derive d;
cout << " 对象空间的大小: " << sizeof(d) << endl << endl;
Base1* ptr1 = &d;
Base2* ptr2 = &d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)ptr1);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)ptr1 + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
第一:sizeof(d) 的大小是多少?
对象 d 由三部分成员构成,① Base1 部分的虚表指针及其成员,这里有 8 字节;② Base2 部分的虚表指针及其成员,这里有 8 字节;③ Derive 自己的成员变量,这里有 4 字节,结果应该是 20 字节。
第二:赋值兼容转换的过程是怎样,或者说,ptr1
和 ptr2
是否相等?
答案是不相等。
- 监视窗口中,&d 和 ptr1 的值是一样的,但是意义不一样,虽然 &d 和 ptr1 都是指向 对象 d 这块空间的起始位置,但是指针的类型限制了指针解引用能够访问多大的空间,&d 的类型是 Derive* 解引用可以访问 20 个字节,ptr1 的类型是 Base1* 解引用只能够访问 8 个字节。
- ptr2 在切片过程中会发生偏移,编译器会找到 Base2 部分的开始,然后将地址交给 ptr2。
第三:对象 d 中有两张虚表,Base1 的虚函数指针放在 Base1 部分的虚表,Base2 的虚函数指针放在 Base3 部分的虚表,但是 Derive 中有一个 Func3() 既不属于 Base1 也不属于 Base2,它该放到哪张虚表里?
有两种可能性:①两张虚表都有 Func3 的函数指针,② Base1部分的虚表里有 Func3 的函数指针
经过测试验证:在VS平台下,多继承体系总派生类的虚函数放在第一个声明的基类当中。
六、多态相关的一些问题
- inline 函数能否是虚函数?
inline 函数会在编译阶段原地展开,直接转换为指令,剩下的建立栈帧带来的消耗,但是这样的做法导致 inline 函数没有函数指针,按道理来说,inline 函数无法称为虚函数。
但是 inline 只是对编译器的一个建议,加不加 inline 是否生效取决于编译器。
如果 inline 虚函数 满足多态调用,编译器就会忽略 inline 属性;
如果 inline 虚函数不满足多态调用, inline 虚函数依旧可以在原地展开。
class Base
{
public:
inline 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; }
private:
int _d = 2;
};
int main()
{
// inline 虚函数满足多态调用
Base* p = new Derive;
p->Func1();
// inline 虚函数不满足多态调用
Base b;
b.Func1();
return 0;
}
- 静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用
类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?
可以,并且建议虚构函数都定义成虚函数,具体看虚函数重写的第三个例外。
- 对象访问普通函数快还是虚函数更快?
首先如果是普通调用,结果是一样快的。
如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。