文章目录
继承:
重载、重写、同名隐藏对比:
#include<iostream>
using namespace std;
class Base
{
public:
virtual int foo(int x)
{
return x * 10;
}
int foo(char x[14])
{
return sizeof(x)+10;
}
};
class Derived : public Base
{
int foo(int x)
{
return x * 20;
}
virtual int foo(char x[10])
{
return sizeof(x)+20;
}
};
int main()
{
Derived stDerived;
Base *pstBase = &stDerived;
char x[10];
printf("%d\n", pstBase->foo(100) + pstBase->foo(x));
return 0;
}
pstBase->foo(100)调用基类的foo但是同名隐藏,所以调用的是派生类的foo,结果为2000
pstBase->foo(x),调用基类的foo,指针在32位操作系统下大小为4,所以输出2014
virtual int foo(int x),基类的虚函数
int foo(char x[14]),基类虚函数的重载
virtual int foo(char x[10]),派生类自己定义的虚函数,只在派生类的子类中有用,干扰选项
派生类的默认成员函数:
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员(因为继承自对方,初始化方法在对方手中)
注意:派生类默认生成无参的构造函数,初始化列表自动调用基类构造函数(无参)
如果基类没有默认的构造函数,则必须在初始化列表显式调用
2.派生类对象初始化先调用基类构造在调用派生类构造(初始化列表会调用基类构造)
3.派生类对象析构先调用自身的析构函数在调用基类的析构函数
Q:实现一个不能被继承的类
A:基类的构造函数给成私有,使提供创建对象的实例方法在类外调不了,或者使用C++11中的final关键字
菱形继承和菱形虚拟化继承:
单继承:class A,class B:public A,class C:public B
多继承:class A,class B,class C:public A,public B
菱形继承:class A,class B:public A,class C:public A,class D:publicB,public C
菱形继承问题:
如果class A,class B,class C都有_b变量,这时候会出现访问不明确,class D中有多份_b;数据冗余和二义性
解决方法:
使用作用域限定符使访问明确化,或者使用虚拟继承
虚拟继承的内存模型:
派生类模型倒立(基类在下,派生类在上)
编译器为派生类生成默认构造函数,作用是在构造函数中将对象的前四个字节初始化好
前四个字节是一个地址,指向虚基表,虚基表中存放的是偏移量(相对于自己的偏移量,基类部分对于对象起始位置的偏移量)
1.取前四个字节内容
2.取偏移量
3.给基类赋值
Q:虚拟继承如何解决数据冗余和二义性?
A:通过访问偏移量表格
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive:public Base1,public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1=&d;
Base2* p2=&d;
Derive* p3=&d;
return 0;
}
//A. p1==p2==p3
//B. p1<p2<p3
//C. p1==p3!=p2
//D. p1!=p2!=p3
正确选项:C
p3指向派生起始位置,base1的起始位置就是派生类的其实位置,p2指向的是base2的位置
多态:
构成动态的两个条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
两个条件缺一不可,否则都只调用基类的虚函数
虚函数的重写:
派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名、参数列表,完全相同)
协变:
基类虚函数返回基类对象的引用或指针,派生类虚函数返回派生类对象的引用或指针,虽然返回值类型不同,但是也是重写
class A
{
};
class B:public A
{
};
class Person
{
virtual A* f()
{
return new A;
}
};
class Student:public Person
{
virtual B* f()
{
return new B;
}
};
析构函数的重写:
虽然基类与派生类析构函数名字不同,但是可以理解为编译器对析构函数的名称做了特殊处理,编译后的析构函数名称统一变为destructor
一般将基类虚函数设置为public,基类虚函数可以和派生类虚函数访问权限不一致
将基类中的析构函数一般给成虚函数
多态的原理:
虚函数表:
class Base1
{
public:
virtual void func1()
{
cout<<"func1()"<<endl;
}
private:
int _b=1;
};
Base的大小是8个字节!
因为基类中有虚函数,除了_b成员还会有一个vfptr的对象(虚函数指针),这个指针指向一个表(虚表),虚表中存放着虚函数入口地址的地址
虚函数的覆盖:
如果我们对派生类中的虚函数完成了重写,我们会发现派生类的虚函数表中存放的是重写的Derived:TestFunc2(),所以虚函数的重写也叫做覆盖
虚函数表的本质是一个存虚函数指针的指针数组,数组最后面放了一个nullptr,也就是00 00 00 00
派生类的虚表生成:
1.先将基类的虚表拷贝一份
2.如果派生类重写了基类某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
3.派生类自己新增加的虚函数按照声明次序增加到派生类虚表的最后
注意:拷贝来的内容不变,新定义的在代码中哪怕在最上面,但是虚表中的位置也在拷贝基类的下面
代码中函数声明次序为5 1 2 3 4 ,虚表中为1 2 3 5 4
虚表的打印:
派生类自己定义的虚函数不会显示在vtfptr中,这时候我们需要自己打印虚函数表
1.先拿到派生类对象地址,&b
2.取前四个字节,先转为int * 型,( int * )&b
3.此时解引用,得到一个int型的数字,数字大小刚好是虚函数地址的数字
4.转换为虚函数指针类型,void ( * )();把这种类型typedef为PVTF
5.虚表地址为 *(PVTF * ) *( int * )&b
多态的调用:
满足多态条件时:
1.从前四个字节取虚表地址
2.虚函数传参
3.找虚函数地址
4.调用
未满足多态条件:直接调用
TestVirtual(Base b),如果不是传指针或者传引用,采用传值的方式,b就是拷贝构造基类的临时对象,直接调用基类的函数
动态绑定与静态绑定:
静态绑定又被称作前期绑定,在程序编译期间就确定程序行为,如:函数重载
动态绑定又称后期绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也被称为动态多态
多态的常见问题:
Q:inline函数可以是虚函数吗?
A:不可以,内联函数在编译时展开,进行代码替换,所以代码已经唯一,不会从虚表中找
Q:静态成员可以是虚函数吗?
A:不可以,virtual只能修饰类的成员函数,友元不属于类的成员,连继承都不行
virtual不可以和static一起使用,static修饰过的静态成员函数连对象都没有,this指针也不可能有
Q:构造函数可以是虚函数吗?
A:不可以,因为虚表指针是在构造函数初始化列表阶段才初始化的,构造函数不能用const修饰,也不能用virtual修饰
Q:析构函数可以是虚函数吗?
A:可以,虽然名称不一样但是构成重写,并且最好把基类的析构函数定义成虚函数
Q:对象访问普通函数快还是虚函数快?
A:如果是普通对象,是一样快,但是如果是指针对象或者引用对象,访问普通函数快,因为是直接调用,构成多态的话运行时
调用虚函数需要到虚函数表中查找
Q:虚函数存在哪里?虚函数表存在哪里?
A:虚函数和普通函数一样,都是存放在代码段里的,虚表存放的虚函数指针,不是虚函数
经过验证,虚表在VS下也是存放在代码段的