Linux/C++系统编程 day14

纯虚函数

  • 形式

    class 类名
    {
    public:
    	virtual 返回类型 函数名(参数列表)=0;
    };
    
  • 作用

    • 设置纯虚函数的意义,就是让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

抽象类

  • 一个类可以包含多个纯虚函数。只要类中含有(声明)一个纯虚函数,该类便为抽象类,用来作为接口

  • 一个抽象类只能作为基类来派生新类,不能创建抽象类的对象,但是可以定义这种指针。

  • 这说明只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类才不再是抽象类,可以继承实现

  • 对一个类来说,如果只定义了protected型的构造函数而没有提供public构造函数,也是抽象类,抽象类不能创建对象,但是可以在其派生类中访问

  • 能否创建指针?不行

虚析构函数

  • 基类指针指向派生类对象,析构的时候,也可以使用dynamic_cast<derived *>(pbase);
  • 析构函数可以设置为虚函数,设置虚析构函数目的就是防止内存泄漏,每个虚构函数只负责清理自己类的数据成员,如果有基类指针指向派生类对象的时候,会造成派生类的对象成员析构不完全
  • 将基类的析构函数设置为虚函数,派生类的析构函数自动成为虚函数,目的:防止内存泄漏
  • 原理:根据虚函数可以被重写这个特性,基类的析构函数设置为虚函数后,那么派生类的析构函数就会自动重写基类的析构函数。虽然他们的函数名不相同,但是编译器对析构函数的名称做了特殊的处理,编译后析构函数的名称统一为destructor。之所以可以这样做,是因为在每个类里面,析构函数是独一无二的,不能重载,所以可以这么设计
  • 如果基类的析构函数声明为虚的,派生类的析构函数也将自动成为虚析构函数,无论派生类析构函数声明中是否加virtual关键字。
  • 如果主动将析构函数写为destructor会报错吗?会,不符合析构函数的形式,可能会被认为是一个普通成员函数但没有返回值

面向对象的设计原则

  • 开闭原则:对扩展开放,对修改关闭
  • 通过纯虚函数+继承实现多态

隐藏、覆盖、重载

  • 末尾加上override,基类是虚函数,派生类也是虚函数,避免派生类中忘记重写虚函数(写到非虚函数那里)
class A
{
public:
	virtual
	void func(int val = 1) { cout << "A->" << val << endl;}
	virtual void test() { func(); }
private:
	long _a;
};
class B
: public A
{
public:
	virtual
	void func(int val = 10) { cout << "B->" << val << endl; }
private:
	long _b;
};
int main(void)
{
	B b;
	A * p1 = (A*)&b;
	B * p2 = &b;
	p1->func();
	p2->func();
	return 0;
}

//Output
B->1
B->10
  • 示例说明:b建立的是B类的对象
    • p1是指向b的基类指针,调用func函数的时候,实际调用的是吸收的基类的func函数
    • p2是指向b的派生类指针,调用func函数的时候,实际调用的是重写的派生类的func函数

Linux/C++系统编程 day14

  • 重载:发生在同一个作用域中,函数名称相同但参数类型,个数、顺序不同,编译时多态
  • 覆盖:发生在派生类与基类中,同名虚函数,参数也完全相同,运行时多态
  • 隐藏:发生在派生类与基类中,某些情况下,派生类中的函数屏蔽了基类中的同名函数,关键字不一定有、参数不一定相同。(静态时多态)

Linux/C++系统编程 day14

虚函数内存布局

虚函数表存在性

  • 虚表是存在的,位于只读段
  • 对于单继承虚表只有一张
class Base
{
public:
    Base(long data1): _data1(data1) {}
    virtual
        void func1() { cout << "Base::func1()" << endl; }
    virtual
        void func2() { cout << "Base::func2()" << endl; }
    virtual
        void func3() { cout << "Base::func3()" << endl; }
private:
    long _data1;
};
class Derived
: public Base
{
public:
    Derived(long data1, long data2)
        : _data1(data1), _data2(data2)
        {}
    virtual void func1() { cout << "Derived::func1()" << endl; }
    virtual void func2() { cout << "Derived::func2()" << endl; }
private:
    long _data2;
};
void test()
{
    Derived derived(10, 20);
    //地址和指针可以等价,双重指是针指向指针的指针
    long ** pVtable = (long **)&derived;
    //(long *)*(long *)&derived;
    typedef void(* Function)();//函数指针
    //Function f=(Function)*((long *)*(long *)&derived);
    for(int idx = 0; idx < 3; ++idx) {
        Function f = (Function)pVtable[0][idx];
        f();
    }
}

派生类内存布局

  • 以下测试都是基于VS,X86环境(32bit)

    注意:虚基指针指向虚基类,虚函数指针指向虚表
    Linux与vs的唯一区别是,在Linux下虚函数指针与虚基指针合并了

    项目->(右键)属性->配置属性->C/C+±>命令行
    /d1 reportSingleClassLayoutXXX 或者/d1 reportAllClassLayout

- 测试一、虚继承与继承的区别
//	1. 多了一个虚基指针
//	2. 虚基类子对象位于派生类存储空间的最末尾(先存不变的后存共享的)

1.1、单个继承,不带虚函数
4/8
1>class B	size(8):
1>	+---
1> 0	| +--- (base class A)
1> 0	| | _ia
1>	| +---
1> 4	| _ib


1.2、单个虚继承,不带虚函数
4/12
1>class B	size(12):
1>	+---
1> 0	| {vbptr}
1> 4	| _ib
1>	+---
1>	+--- (virtual base A)
1> 8	| _ia
1>	+---
1>B::$vbtable@:
1> 0	| 0
1> 1	| 8 (Bd(B+0)A)


测试二:单个虚继承,带虚函数
//   1.如果派生类没有自己新增的虚函数,此时派生类对象不会产生虚函数指针
//   2.如果派生类拥有自己新增的虚函数,此时派生类对象就会产生自己本身的
//    虚函数指针(指向新增的虚函数),并且该虚函数指针位于派生类对象存储空间的开始位置
2.1、单个继承,带虚函数
8/12
1>class B	size(12):
1>	+---
1> 0	| +--- (base class A)
1> 0	| | {vfptr}
1> 4	| | _ia
1>	| +---
1> 8	| _ib
1>	+---
1>B::$vftable@:
1>	| &B_meta
1>	|  0
1> 0	| &B::f

2.2、单个继承,带虚函数(自己新增虚函数)
8/12
class B	size(12):
1>	+---
1> 0	| +--- (base class A)
1> 0	| | {vfptr}
1> 4	| | _ia
1>	| +---
1> 8	| _ib
1>	+---
1>B::$vftable@:
1>	| &B_meta
1>	|  0
1> 0	| &B::f
1> 1	| &B::fb2
总结:针对2.1、2.2,普通继承,派生类新增虚函数直接放在基类虚表中;且基类布局在前面


2.3、单个虚继承,带虚函数
8/16
1>class B	size(16):
1>	+---
1> 0	| {vbptr}    //有虚继承的时候就多一个虚基指针,虚基指针指向虚基表  
1> 4	| _ib        //有虚函数的时候就产生一个虚函数指针,虚函数指针指向虚函数表
1>	+--- 
1>	+--- (virtual base A)
1> 8	| {vfptr}
1>12	| _ia
1>	+---
1>B::$vbtable@:
1> 0	| 0
1> 1	| 8 (Bd(B+0)A)
1>B::$vftable@:
1>	| -8
1> 0	| &B::f

2.4、单个虚继承,带虚函数(自己新增虚函数)
8/20
1>class B	size(20):
1>	+---
1> 0	| {vfptr}
1> 4	| {vbptr}
1> 8	| _ib
1>	+---
1>	+--- (virtual base A)
1>12	| {vfptr}
1>16	| _ia
1>	+---
1>B::$vftable@B@:
1>	| &B_meta
1>	|  0
1> 0	| &B::fb2
1>B::$vbtable@:
1> 0	| -4
1> 1	| 8 (Bd(B+4)A)
1>B::$vftable@A@:
1>	| -12
1> 0	| &B::f
总结:2.3、2.4、虚继承多一个虚基指针,如果派生类新增虚函数,则放在最前面;且基类布局放在最后面


// 测试三:多重继承(带虚函数)
// 1、每个基类都有自己的虚函数表
// 2、派生类如果有自己新增的虚函数,会被加入到第一个虚函数表之中
// 3、内存布局中,其基类的布局按照基类被继承时的顺序进行排列
// 4、派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的虚函数的地址;
//     其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令

3.1、普通多重继承,带虚函数,自己有新增虚函数
28
1>class Derived	size(28):
1>	+---
1> 0	| +--- (base class Base1)
1> 0	| | {vfptr}
1> 4	| | _iBase1
1>	| +---
1> 8	| +--- (base class Base2)
1> 8	| | {vfptr}
1>12	| | _iBase2
1>	| +---
1>16	| +--- (base class Base3)
1>16	| | {vfptr}
1>20	| | _iBase3
1>	| +---
1>24	| _iDerived
1>	+---
1>Derived::$vftable@Base1@:
1>	| &Derived_meta
1>	|  0
1> 0	| &Derived::f(虚函数的覆盖)
1> 1	| &Base1::g
1> 2	| &Base1::h
1> 3	| &Derived::g1(新的虚函数,直接放在基类之后,加快查找速度)
1>Derived::$vftable@Base2@:
1>	| -8
1> 0	| &thunk: this-=8; goto Derived::f   //虚函数表还可以存放跳转指令
1> 1	| &Base2::g
1> 2	| &Base2::h
1>Derived::$vftable@Base3@:
1>	| -16
1> 0	| &thunk: this-=16; goto Derived::f
1> 1	| &Base3::g
1> 2	| &Base3::h


3.2、虚拟多重继承,带虚函数,自己有新增虚函数(只有第一个是虚继承)
32
1>class Derived	size(32):
1>	+---
1> 0	| +--- (base class Base2)
1> 0	| | {vfptr}
1> 4	| | _iBase2
1>	| +---
1> 8	| +--- (base class Base3)
1> 8	| | {vfptr}
1>12	| | _iBase3
1>	| +---
1>16	| {vbptr}
1>20	| _iDerived
1>	+---
1>	+--- (virtual base Base1)
1>24	| {vfptr}
1>28	| _iBase1
1>	+---
1>Derived::$vftable@Base2@:
1>	| &Derived_meta
1>	|  0
1> 0	| &Derived::f
1> 1	| &Base2::g
1> 2	| &Base2::h
1> 3	| &Derived::g1
1>Derived::$vftable@Base3@:
1>	| -8
1> 0	| &thunk: this-=8; goto Derived::f
1> 1	| &Base3::g
1> 2	| &Base3::h
1>Derived::$vbtable@:
1> 0	| -16
1> 1	| 8 (Derivedd(Derived+16)Base1)
1>Derived::$vftable@Base1@:
1>	| -24
1> 0	| &thunk: this-=24; goto Derived::f
1> 1	| &Base1::g
1> 2	| &Base1::h


3.3、虚拟多重继承,带虚函数,自己有新增虚函数(三个都是虚继承)
36
1>class Derived	size(36):
1>	+---
1> 0	| {vfptr}   //以空间换时间
1> 4	| {vbptr}
1> 8	| _iDerived
1>	+---
1>	+--- (virtual base Base1)
1>12	| {vfptr}
1>16	| _iBase1
1>	+---
1>	+--- (virtual base Base2)
1>20	| {vfptr}
1>24	| _iBase2
1>	+---
1>	+--- (virtual base Base3)
1>28	| {vfptr}
1>32	| _iBase3
1>	+---
1>Derived::$vftable@Derived@:
1>	| &Derived_meta
1>	|  0
1> 0	| &Derived::g1
1>Derived::$vbtable@:
1> 0	| -4
1> 1	| 8 (Derivedd(Derived+4)Base1)
1> 2	| 16 (Derivedd(Derived+4)Base2)
1> 3	| 24 (Derivedd(Derived+4)Base3)
1>Derived::$vftable@Base1@:
1>	| -12
1> 0	| &Derived::f
1> 1	| &Base1::g
1> 2	| &Base1::h
1>Derived::$vftable@Base2@:
1>	| -20
1> 0	| &thunk: this-=8; goto Derived::f
1> 1	| &Base2::g
1> 2	| &Base2::h
1>Derived::$vftable@Base3@:
1>	| -28
1> 0	| &thunk: this-=16; goto Derived::f
1> 1	| &Base3::g
1> 2	| &Base3::h

菱形继承

// 测试四:菱形虚继承

//虚基指针所指向的虚基表的内容:
//	1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
//	2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移
4.1、菱形普通继承(存储二义性)
48
class D	size(48):
1>	+---
1> 0	| +--- (base class B1)
1> 0	| | +--- (base class B)
1> 0	| | | {vfptr}
1> 4	| | | _ib
1> 8	| | | _cb  //1
1>  	| | | <alignment member> (size=3) //内存对齐
1>	| | +---
1>12	| | _ib1
1>16	| | _cb1
1>  	| | <alignment member> (size=3)
1>	| +---
1>20	| +--- (base class B2)
1>20	| | +--- (base class B)
1>20	| | | {vfptr}
1>24	| | | _ib
1>28	| | | _cb
1>  	| | | <alignment member> (size=3)
1>	| | +---
1>32	| | _ib2
1>36	| | _cb2
1>  	| | <alignment member> (size=3)
1>	| +---
1>40	| _id
1>44	| _cd
1>  	| <alignment member> (size=3)
1>	+---
1>D::$vftable@B1@:
1>	| &D_meta
1>	|  0
1> 0	| &D::f
1> 1	| &B::Bf
1> 2	| &D::f1
1> 3	| &B1::Bf1
1> 4	| &D::Df
1>D::$vftable@B2@:
1>	| -20
1> 0	| &thunk: this-=20; goto D::f
1> 1	| &B::Bf
1> 2	| &D::f2
1> 3	| &B2::Bf2

4.2、菱形虚拟继承
52
1>class D	size(52):
1>	+---
1> 0	| +--- (base class B1)
1> 0	| | {vfptr}
1> 4	| | {vbptr}
1> 8	| | _ib1
1>12	| | _cb1
1>  	| | <alignment member> (size=3)
1>	| +---
1>16	| +--- (base class B2)
1>16	| | {vfptr}
1>20	| | {vbptr}
1>24	| | _ib2
1>28	| | _cb2
1>  	| | <alignment member> (size=3)
1>	| +---
1>32	| _id
1>36	| _cd
1>  	| <alignment member> (size=3)
1>	+---
1>	+--- (virtual base B)
1>40	| {vfptr}
1>44	| _ib
1>48	| _cb
1>  	| <alignment member> (size=3)
1>	+---
1>D::$vftable@B1@:
1>	| &D_meta
1>	|  0
1> 0	| &D::f1
1> 1	| &B1::Bf1
1> 2	| &D::Df
1>D::$vftable@B2@:
1>	| -16
1> 0	| &D::f2
1> 1	| &B2::Bf2
1>D::$vbtable@B1@:
1> 0	| -4
1> 1	| 36 (Dd(B1+4)B)
1>D::$vbtable@B2@:
1> 0	| -4
1> 1	| 20 (Dd(B2+4)B)
1>D::$vftable@B@:
1>	| -40
1> 0	| &D::f
1> 1	| &B::Bf
  • 先后声明的类内存布局:

    • 派生类是否有自己的虚函数vfptr
  • 虚拟继承继承vbptr

    • 数据成员
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tXvLYEHo-1611035411442)(C:\Users\Admin\AppData\Roaming\Typora\typora-user-images\image-20210119093217355.png)]

多基派生的二义性(带虚函数)

  • Linux/C++系统编程 day14

  • 一个基类中是虚函数,另一个基类中不是虚函数,则继承两个基类之后视为虚函数,可通过再次派生一个类之后用基类指针指向派生类验证

  • 带虚函数的多基派生的派生类有多张虚表

虚拟继承

  • 虚的含义:存在、间接、共享
    • 虚函数本身是存在的
    • 虚函数通过一种间接运行时的机智才能激活
    • 基类会共享被派生类重定义的虚函数
  • 虚拟继承
    • 虚拟继承体系和虚基类是存在的
    • 访问虚基类成员时通过间接机制(虚基表存虚基指针偏移信息)来完成
    • 虚基类会在虚继承体系中被共享,而不会出现多份拷贝,在派生类中要初始化最底层的基类

虚拟继承

  • 虚的含义:存在、间接、共享
    • 虚函数本身是存在的
    • 虚函数通过一种间接运行时的机智才能激活
    • 基类会共享被派生类重定义的虚函数
  • 虚拟继承
    • 虚拟继承体系和虚基类是存在的
    • 访问虚基类成员时通过间接机制(虚基表存虚基指针偏移信息)来完成
    • 虚基类会在虚继承体系中被共享,而不会出现多份拷贝,在派生类中要初始化最底层的基类

效率分析

  • 虚拟继承越多指针越多越低
上一篇:Day14 合并两个有序链表


下一篇:机器学习day14 K均值算法