1.概念:
一个东西可以在不同场景下变现出多种状态。
例如:* / & *:在两个操作数之间为乘,在指针变量前面为解引用 &:在变量前为取地址,在变量后为引用,在两个操作数之间为与运算符
2. 分类
(1) 静态多态(静态链编/静态绑定/前期绑定):程序编译期可以确定程序的行为
例如:函数重载。在编译期间就知道是应该调用哪一个函数
(2)动态多态(动态链编/动态绑定/后期绑定):在程序运行时来确定程序的行为
3.实现条件
(1)基类中必须有虚函数,派生类必须对基类中的虚函数进行重写
(2)必须用通过基类的指针或者引用来调用虚函数。
4.重写
(1)基类中必须包含虚函数,派生类中虚函数的原型必须与其基类中虚函数一致(返回值类型,函数名字,参数列表)
例外:1)协变:返回值类型可以不一样。但基类必须返回基类的引用或者指针,派生类必须返回派生类的引用或者指针。
2)析构:派生类与基类中析构函数的名字可以不同
(2)一个在基类,一个在派生类
(3)派生类和基类虚函数访问权限可以不同。但基类必须为public。
(4)派生类函数前virtual可加可不加,最好加上
5.重写与同名隐藏的对比
重写:(1)在不同的作用域
(2)基类必须为虚函数
(3)函数名相同、参数相同、返回值相同(除例外)
同名隐藏:(1)在不同的作用域
(2)只要基类与派生类中有相同名称的成员
(3)函数名相同,类型无要求
6.抽象类
概念:在成员函数(必须为虚函数)的形参列表后边写上=0,则成员函数为虚函数。包含虚函数的类叫抽象类(也加接口类),抽象类不可以实例化出对象。纯虚函数在派生类中重新定义了以后,派生类才能实例化出对象。
例如:小米手机有很多型号,每个型号的功能不同。那么就会采用这种方式。每个型号都是一个独立的类,去实现自己的功能,而抽象类提供一个接口去实现。
virtual void Driver() = 0; //纯虚函数
7.多态的原理
虚函数表:通过一块连续内存来存储虚函数的地址。
class Base
{
public:
virtual void TestFunc1()
{
cout << "Base::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "Base::TestFunc2()" << endl;
}
int _b;
};
class Derived : public Base
{
public:
int _d;
};
int main()
{
Base b;
b._b = 1;
return 0;
}
我们调试,打开内存,发现
我猜测虚函数表里面的是基类的两个函数。 因此进行验证:
typedef void(*PVTF)();
void print(Base& b)
{
PVTF *pVTF = (PVTF*)(*(int *)&b);
while (*pVTF)
{
(*pVTF)();
pVTF++;
}
}
int main()
{
Base b;
b._b = 1;
print(b);
return 0;
}
发现确实是这两个函数的地址
这里PVTF *pVTF = (PVTF*)(*(int *)&b); 我先取b的地址,但由于它是base类型,我只想拿到前四个字节,所以进行强转为int*在对其进行解引用,拿到前四个字节里面的内容。由于刚刚看到的是一个函数指针数组,因为我们对其强转为PVTF* .此时我们就会改变对前四个字节的看待方式,他此时就是成为了一个地址。并且指向一个函数。
那么我们在让它做+1,实际加的就是其类型(函数)的大小,知道他里面的内容变为0,此时运行,发现打印出了两个函数。那么证明之前的猜想没有错。
对于基类中的虚表:按照虚函数声明次序将其添加到虚表。
看完了基类,在里看看派生类
d._b = 1;
d._d = 2;
发现和基类的是一样的调用的函数是一样。是将基类的续表拷了一份。
那此时我们在派生类重写一个函数,看看会怎么样
class Derived : public Base
{
public:
virtual void TestFunc2()
{
cout << "Derived::TestFunc2()" << endl;
}
int _d;
};
发现打印的就是重写那个函数。
是将 重写的函数进行了替换。
那么假如派生类自己还有自己的函数呢?
class Derived : public Base
{
public:
virtual void TestFunc3()
{
cout << "Derived::TestFunc3()" << endl;
}
virtual void TestFunc2()
{
cout << "Derived::TestFunc2()" << endl;
}
virtual void TestFunc4()
{
cout << "Derived::TestFunc4()" << endl;
}
int _d;
};
派生类的虚表:
(1)先将基类中的虚表拷贝一份
(2)如果派生类重写了基类中的某个虚函数就会替换虚表中的相同偏移量位置的基类函数。
(3)将派生类新增的虚函数按期在类中声明顺序放在虚表后面
多态的原理:
通过基类的指针或者引用来调用虚函数
(1)在对象的前4个字节取虚表的地址
(2)传递对象的this指针(传参)
(3)在虚表找那个取被调用虚函数的地址
(4)调用虚函数
对于普通函数根据其声明的类型。即便将派生类对象传过来,也打印的是基类的func4(),因为此时虚表中没有普通函数的地址,它依赖于声明的类型,因为是base类,所以调用基类。
8.在探虚表&不同继承下带有虚函数的对象模型
1.单继承:在前面都就是用的单继承,这里不再赘述。
2.多继承:
class Base1
{
public:
virtual void func1()
{
cout << "Base1::func1" << endl;
}
virtual void func2()
{
cout << "Base1::func2" << endl;
}
int _b1;
};
class Base2
{
public:
virtual void func1()
{
cout << "Base2::func1" << endl;
}
virtual void func2()
{
cout << "Base2::func2" << endl;
}
int _b2;
};
class Derive:public Base1,public Base2
{
public:
virtual void func1() //func1进行重写
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
int _d;
};
计算其派生类的大小,发现是否有无func3()他们的大小都是20。那么究竟内部是如何来做得。
typedef void* (*FUNC)();
void Print(int* VTable)
{
cout << "虚表的地址" << VTable << endl;
for (int i = 0; i < VTable[i] != 0; i++)
{
printf("第%个虚函数的地址:0x%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d1;
cout << sizeof(d1) << endl; //20
int *VTable = (int *)(*(int *)&d1);
Print(VTable); //打印的是base1的函数
//base2虚函数在对象base1后面
cout << sizeof(Base1) / 4 << endl;
VTable = (int*)(*((int*)&d1 + sizeof(Base1) / 4));//为什么要/4 因为对指针+1是加上其所指类型的大小。看起来+2实则加上了8
Print(VTable);
我们来看看结果:对象模型还是基类在上,派生类在下。
通过结果我们发现:
多继承派生类自己新增加的虚函数不新维护一张虚表,而是 放在第一个继承类的虚函数表中。
3.菱形继承
class A
{
public:
virtual void fun1()
{
cout << "A::fun1" << endl;
}
virtual void fun2()
{
cout << "A::fun2" << endl;
}
virtual void fun3()
{
cout << "A::fun3" << endl;
}
int _a;
};
class A1:public A
{
public:
virtual void fun1() //将fun1进行重写
{
cout << "A1::fun1" << endl;
}
int _a1;
};
class A2:public A
{
public:
virtual void fun2() //将fun2进行重写
{
cout << "A2::fun2" << endl;
}
virtual void fun3() //将fun3进行重写
{
cout << "A2::fun3" << endl;
}
int _a2;
};
class B :public A1, public A2
{
public:
virtual void fun1() //将fun1进行重写
{
cout << "B::fun1" << endl;
}
virtual void fun2() //将fun2进行重写
{
cout << "B::fun2" << endl;
}
virtual void fun4()
{
cout << "B::fun4" << endl;
}
int _d;
};
typedef void* (*FUNC)();
void Print(int* VTable)
{
cout << "虚表的地址" << VTable << endl;
for (int i = 0; i < VTable[i] != 0; i++)
{
printf("第%个虚函数的地址:0x%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
int main()
{
B b;
cout << sizeof(b) << endl; //28
//b._a = 1;//二义性 不能确定到底是谁的_a
b.A1::_a = 1;
b._a1 = 2;
b._a2 = 3;
b._d = 4;
b.A2::_a = 5;
int * VTable = (int*)(*(int *)&b);
Print(VTable);//A1的虚表地址
//A2的虚表在A1对象后面
VTable = (int *)(*((int*)&b + sizeof(A1) / 4));
Print(VTable);
和我们的猜想是一样的。
结合前面提到的知识。可以总结:
对于菱形继承。派生类在下,基类在上。在单继承的时候。派生类会先将基类中的虚表拷一份,对于重写的函数,就将虚表中相同偏移量的地址改为派生类自己的。对于多继承,派生类自己不会维护一张虚表。直接在基类的虚表中做出修改,若有新增的函数,则放在第一个继承的父类虚表后面。在这里的菱形继承存在二义性。我们在访问基类A中的_a时不能直接访问。因为在继承的时候A1,A2分别拷贝了一份,因此存在两份_b。因此提出菱形虚拟继承。
菱形虚拟继承
class A
{
public:
virtual void fun1()
{
cout << "A::fun1" << endl;
}
int _a;
};
class A1:virtual public A
{
public:
virtual void fun1() //将fun1进行重写
{
cout << "A1::fun1" << endl;
}
virtual void fun2()
{
cout << "A1::fun2" << endl;
}
int _a1;
};
此时这个A1类的大小是16.
刚开始我没有加func2()
可以看到正如之前所说的,虚拟继承是派生类在上,基类在下。此时派生类不会复制基类的基类的虚表指针。那么对于虚拟继承他会多出来一个偏移量指针,指向基类的偏移量值。因此这里的两个地址,一个是基类的虚表指针。一个是偏移量指针。可以看到偏移量指针在这里偏移了08,那么正好是对于基类的偏移量。
加上func2()之后
看到多了一个地址,通过验证,发现是因为我们在派生类自己写了一个虚函数,因此它会给自己再有一个虚表指针。所以这里一共有三个地址。一个是派生类的虚表地址,一个是派生类的偏移量指针(一个是对于自己的偏移量-4,一个是但由于基类的偏移量08),一个是基类的虚表地址。
了解了单继承方式的包含虚函数的虚拟继承,再来看看菱形虚拟继承
class A
{
public:
virtual void fun1()
{
cout << "A::fun1" << endl;
}
virtual void fun2()
{
cout << "A::fun2" << endl;
}
int _a;
};
class A1 :virtual public A
{
public:
virtual void fun1() //将fun1进行重写
{
cout << "A1::fun1" << endl;
}
virtual void fun3() //自己写的函数,就会有虚表指针。若没写就不会有
{
cout << "A1::fun3" << endl;
}
int _a1;
};
class A2 :virtual public A
{
public:
virtual void fun2() //将fun2进行重写
{
cout << "A2::fun2" << endl;
}
virtual void fun4() //自己写的函数,就会有虚表指针。若没写就不会有
{
cout << "A2::fun4" << endl;
}
int _a2;
};
class B :public A1, public A2
{
public:
virtual void fun1() //将fun1进行重写
{
cout << "B::fun1" << endl;
}
virtual void fun2() //将fun2进行重写
{
cout << "B::fun2" << endl;
}
virtual void fun5() //自己的函数,若这里没写,A1,A2也没写。此时就没有派生类的虚表指针,若这里写了,A1,A2没写,此时A1会多一个虚表指针,
//因为多继承基类会将自己写的函数添加到第一个基类的虚表后面
{
cout << "B::fun5" << endl;
}
int _d;
}
此时先画一下他的对象模型
总结:
对于包含虚函数菱形虚拟继承,他是基类在下,派生类在上 。派生类的两个基类分别有两个偏移量指针,指向基类的偏移量。此时基类只存了一份。因为菱形虚拟继承不会拷贝虚表指针。因此在没有新增函数的时候,派生类的两个基类不会有虚表指针。一旦他们有自己的虚函数,才有有虚表指针。除非在B基类中有自己的虚函数,就会添加到第一个父类中,因此就算第一个父类么有增加新的虚函数,他还是会有虚表指针用来放基类的虚函数。
此外发现在A1/A2/B加了构造函数或者析构函数,就会多出4个字节,内存中看到为全0.此时很难得知者4个字节的作用。据猜测是起了分割基类的作用。
再来对多态进行回顾:
进行一些补充:
1.同一个类创建多个对象,都只是维护了一张虚表,所有对象共享同一张虚表。
2.基类和派生类是不同的虚表。
3.对于operator=虽然可以定义为虚函数,但是最好不要。因为存在以下几种赋值方式:
(1) 派生类=派生类(肯定没有问题)
(2) 派生类=基类:用基类给派生类赋值不安全。
(3) 基类=基类(肯定没有问题)
(4) 基类=派生类 :本来就可以,满足赋值兼容规则
4.哪些函数不可以定义为虚函数
(1)不能被继承的函数 (2)不能被重写的函数
1)普通函数:不是类的成员函数
2)友元函数:不是类的成员函数,没有this指针
3)构造函数:作用:创建对象&初始化工作(放初值包括虚表指针)。有虚函数就必须得有对象。此时构造函数才准备去创建对象,却将构造函数写为虚函数,此时虚函数就会取对象的前4个字节,可此时都没有对象。
4)内联成员函数:展开就不调用这个函数
5)静态成员变量:virtual不可以和static同时使用。不需要用对象来调用,也就没有this指针,无对象,找不到虚表
5.对于析构函数
建议:最好将基类的析构函数给成虚函数。
Base& b=new derive;
此时若不是虚函数,不能形成重写,就不会走虚表。只会在去声明的类型来释放。
若是虚函数,调用析构函数,形成多态(1.基类对象的指针2.重写)。就会去指向派生类对象,在派生类对象的前4个字节中取到虚表的地址。
之前对继承也进行了总结
那么对于c++中的三大特性:封装,继承,多态已经总结完了。