1、虚函数
虚函数是类的一种特殊成员函数,主要是为实现C++的多态特性引入。
虚函数之所以“虚”是因为调用的虚函数不是在静态编译(静态编联)时确定,而是在运行时通过动态编联确定的。
多态核心理念即是通过基类访问派生的子类,通常情况是借助基类指针来访问派生类对象。
Note:
1)析构函数通常声明为虚函数,这样在有继承场合,可以做到基于对象类型动态调用正确对象类型的析构函数,完成相应的对象析构。
2)友元函数不是成员函数,只有成员函数才能是虚函数,所以友元函数不能是虚函数;但是友元函数可以通过调用成员虚函数达到虚拟化效果。
3)通过虚函数可以实现多态灵活,但是虚函数也有缺点如占用更多内存(虚表),运行效率低(需要查找虚表找到正确函数)。
4)通常普通函数(非成员函数)和类中不能继承(只属于本类)的函数(构造函数/static成员函数/inline成员函数/友元函数)不能声明为虚函数;不能声明为虚函数的函数共同点基本都是静态编联,而虚函数要靠动态编联机制才能起作用。
5)虚函数被继承后仍为虚函数。
2、纯虚函数
纯虚函数是一种特殊的虚函数,通常定义在基类中。纯虚函数在基类中定义方法是在函数声明末尾加“=0”,如 virtual func(int, int) = 0。
纯虚函数的“纯”体现为基类不需要实现它,其主要作用是为派生类定义函数接口框架,由派生类完成纯虚函数的实现。
一般地,我们不希望纯虚函数的构造函数暴露。所以我们把构造函数设为 protected
。
class test {
int property1;
int attribute2;
virtual string toString() = 0;
protected:
test() {}
test(int p1, int p2) {
property1 = p1;
attribute2 = p2;
}
};
1)纯虚函数被继承后为虚函数
2)重载函数虽同名,但其实是不同的函数,我们让其中一种成为虚函数或者纯虚函数,其它的同名函数(如果其它的都不加virtual)并不会成为虚函数或者纯虚函数。在子类中,若重载了继承过来的虚函数,那么新的重载形式(即你的在子类中定义而在父类中没有的同名函数)的函数不会是虚函数。
3、普通函数
普通函数是静态编译的,没有运行时多态,只会根据指针或引用的“字面值”类对象,调用自己的普通函数。
普通函数是父类为子类提供的“强制实现”。
因此,在继承关系中,子类不应该重写父类的普通函数,因为函数的调用至于类对象的字面值有关。
4、抽象类
如果一个类包含有纯虚函数,则该类称为抽象类。
抽象类一般只能是基类,其声明的纯虚函数由派生类实现。如果派生类没有重写(覆盖)抽象类的纯虚函数则派生类也是抽象类。
对象不能基于抽象类创建,必须基于派生出来的具体类创建对象,抽象类不能被实例化。
5、虚表
参考自:https://www.cnblogs.com/LUO77/p/5771237.html
多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
- 原始基类的虚函数表
下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)
- 单继承时的虚函数(无重写基类虚函数)
假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表。
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放。
- 单继承时的虚函数(重写基类虚函数)
现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。
- 多重继承时的虚函数(Derived ::public Base1,public Base2)
这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。
它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。
多重继承时指针的调整:
Derive b; Base1* ptr1 = &b; // 指向 b 的初始地址 Base2* ptr2 = &b; // 指向 b 的第二个子对象
因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。
因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。
当然,上述过程是由编译器完成的。
Base1* b1 = (Base1*)ptr2; b1->y(); // 输出 Base2::y() Base2* b2 = (Base2*)ptr1; b2->y(); // 输出 Base1::y()
其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()
调用到的是Base2::y()
,而 ptr1 本来就指向 Base1 子对象的起始地址(即 Derive对象的起始地址),所以b2->y()
调用到的是Base1::y()
。
- 虚继承时的虚函数表
虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbtable的指针vbptr。MyClassC的对象模型如图4所示。
虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如MyClassA的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClassA::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass::vfptr相对于MyClassA::vbptr的偏移量。(虚函数与虚继承深入探讨)
对象模型探讨:
1.没有继承情况,vptr存放在对象的开始位置,以下是Base1的内存布局
m_iData :100 |
2.单继承的情况下,对象只有一个vptr,它存放在对象的开始位置,派生类子对象在父类子对象的最后面,以下是D1的内存布局
B1:: m_iData : 100 |
B1::vptr : 4294800 |
B2::vptr : 4294776 |
D::m_iData :300 |
4. 虚拟继承情况下,虚父类子对象会放在派生类子对象之后,派生类子对象的第一个位置存放着一个vptr,虚拟子类子对象也会保存一个vptr,以下是VD1的内存布局
Unknown : 4294888 |
B1::vptr :4294864 |
VD1::vptr : 4294944 |
VD1::m_iData : 200 |
VD2::Unknown : 4294952 |
VD::m_iData : 500 |
B1::m_iData : 100 |
5. 棱形继承的情况下,非虚基类子对象在派生类子对象前面,并按照声明顺序排列,虚基类子对象在派生类子对象后面
VD1::Unknown : 4294968 |
VD2::vptr : 4 294932 |
VD2::m_iData : 300 |
B1::vptr : 4294920 |
B1::m_iData : 100 |