目录
多态的概念
多态换句话来说就是多种形态,具体点就是不同的对象去完成某一个行为时会产生不同的状态。比如买票这个行为,成年人去买是全价,未成年人去买是半价,而军人去买则是优先购票。
多态的实现
多态的构成条件
多态的两个条件:
1、虚函数的重写
2、父类对象的指针或者引用去调用虚函数 // 很重要
虚函数
被virtual修饰的类成员函数称为虚函数
class Person {
public:
virtual void BuyTicket() { // 注意virtual的位置
cout << "买票-全价" << endl;
}
};
注:虚函数的 virtual 和虚继承中的 virtual 是同一个关键字,但是它们之间没有任何关系,虚函数的 virtual 是为了实现多态,而虚继承的 virtual 是为了解决菱形继承的数据冗余和二义性
虚函数的重写
//基类 -- 普通人
class Person {
public:
//基类的虚函数
virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
//派生类 -- 学生
class Student : public Person {
public:
//派生类的虚函数重写了父类的虚函数
virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};
//构成多态:1.虚函数重写 2.基类的指针或引用去调用虚函数
void Func(Person& p)
// 参数类型也可以是Person* ,但不可以是Person, // 很重要
// 如果参数类型是student,不管是指针还是引用,父类实例化的对象是穿不进来的(看继承那章如何解决)
{
p.BuyTicket();
}
int main()
{
Person ps;
student st;
Func(ps); // 买票 - 全价
Func(st); // 买票 - 半价
return 0;
}
一些细节
子类可以不加virtual
,但是父类必须加virtual
(建议:父类子类虚函数都加上)
协变:返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用。
小总结
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
虚函数的重写就是在隐藏(名相同)的基础之上多了两大条件:virtual 和 三个相同
三个相同:
- 函数名相同
- 参数类型、数量相同
- 返回类型相同
具体细节:
1、子类虚函数可以不加virtual (建议:父类子类虚函数都加上)
2、协变:三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用
是否满足多态:
满足多态:跟调用对象的类型无关,跟指向对象有关,指向哪个对象调用的就是他的虚函数
不满足多态:跟调用对象的类型有关,类型是什么调用的就是谁的虚函数
析构函数与virtual
class Person2
{
public:
~Person2()
{
cout << "Person delete:" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
class Student2 : public Person2
{
public:
~Student2()
{
cout << "Student delete:" << _s << endl;
delete[] _s;
}
protected:
int* _s = new int[20];
};
int main()
{
Person2* ptr1 = new Person2;
Person2* ptr2 = new Student2; // 这里会出现一个问题
delete ptr1;
delete ptr2;
return 0;
}
上面代码运行后
因为ptr2 的类型是Person,所以只会调用Person的析构函数
会发现student的析构函数没有调用,_s没有释放,造成了内存泄露
所以我们需要多态,只能对析构函数重写
(基类与派生类析构函数的名字不同)
虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,
class Person2
{
public:
virtual ~Person2()
{
cout << "Person delete:" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
class Student2 : public Person2
{
public:
virtual ~Student2()//子类可以不用加virtual
{
cout << "Student delete:" << _s << endl;
delete[] _s;
}
protected:
int* _s = new int[20];
};
int main()
{
Person2* ptr1 = new Person2;
Person2* ptr2 = new Student2;
delete ptr1;
delete ptr2;
return 0;
}
运行后
student的_s 得以释放,解决了内存泄漏
通过多态的调用就可以防止内存泄漏的现象发生了。因此也可以看出析构名最终变成destructor
这样设计的原因。
因此可以看出:当实现父类的时候,析构不用考虑别的,直接加virtual
,没有什么坏处。
C++11 final和override
final
final:修饰虚函数,表示该虚函数不能重写
final 不能修饰非虚函数
final可以用于实现一个不能继承的类
另外一种方式是将基类的构造函数私有化,这样就不能继承了
override
override 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译错误
//基类
class Person {
public:
//基类的虚函数
virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
//派生类
class Student : public Person {
public:
//派生类完成了虚函数的重写,编译通过
virtual void BuyTicket()override
{
cout << "Student-买票-半价" << endl;
}
};
//派生类
class Soldier : public Person
{
public:
//派生类没有完成虚函数的重写,编译报错
virtual void BuyTicket(int n)override
{
cout << "Soldier-优先-买票" << endl;
}
};
重载、覆盖(重写)、隐藏(重定义)的对比
抽象类
概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
接口继承和实现继承
①实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
②接口继承:
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
两道题
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;
}
输出:
2、
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;
}
//问:p1, p2, p3的关系?
//A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
答案是 C
多态的原理
虚函数表
// sizeof(Base2)是多少?
class Base2
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b2;
cout << sizeof(b2) << endl;
return 0;
}
通过调试,发现b的成员除了_b变量,还有一个_vfptr,称为虚函数表指针(v代表virtual,f代表function)
所以b2 的大小是8
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址都要被放到虚函数表中,虚函数表也简称虚表。这个虚表指针,实际上是函数指针,
再看一下继承基类的派生类的虚表
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;
char _ch;
};
class Derive : public Base // 派生类
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b1;
Derive d1;
cout << sizeof(b1) << endl;
cout << sizeof(d1) << endl;
}
通过调试发现 继承下来的虚表在重新(覆盖)之后,就不是继承下来的地址,而是变成一个新的虚表。
所以 b1 :12 , d1:16
总结
注意:同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的
派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数(没有重写不会被覆盖) c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int* p1 = new int;
cout << "堆:" << p1 << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
static int b = 0;
cout << "静态区/数据段:" << &b << endl;
Base be;
cout << "虚表:" << (void*)*((int*)&be) << endl;
Base* ptr1 = &be;
int* ptr2 = (int*)ptr1;
printf("虚表:%p\n", *ptr2);
Derive de;
cout << "虚表:" << (void*)*((int*)&de) << endl;
return 0;
}
多态的原理就是:
(这里再用Person类、Student类举例子)
当基类指针Person*或引用Perosn&指向派生类对象,在调用重写的虚函数时,就会到指向的对象的类里面找虚表!从虚表中得到这个虚函数的地址,从而去调用这个函数!如果指向的对象是基类自己,那么就会到基类里面找到基类的虚表。
①为什么虚函数覆盖/重写:
因为要对派生类的虚表进行覆盖。在调用重写的函数的时候,如果指向的是派生类对象,那么就必须从这个派生类的虚表中拿到这个虚函数的地址。
②为什么要基类对象的指针或引用去调用虚函数:
首先,虚函数必须写在基类中。其次,基类指针或引用派生类对象的时候,在切片后,指向的是派生类对象中属于基类成员的那一部分,但总体来说依然是指向派生类的,当需要调用重写的虚函数的时候,就会去基类成员那一部分中找接口(如果有默认参数,会使用基类的,即派生类的默认参数不起效,看下面的例子),再去派生类中找定义。不是切片的话,就会自己调用自己的指定的虚函数。
class Person // final
{
public:
virtual Person& BuyTicket(int a = 1) // final
{
cout << "买票-全价" << a << endl;
return *this;
}
};
class student : public Person
{
public:
virtual student& BuyTicket (int a = 0) override
{
cout << "买票-半价" << a << endl;
return *this;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
student st;
Func(ps); // 买票 - 全价1
Func(st); // 买票 - 半价1
return 0;
}
动态绑定与静态绑定
①静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也就是说已经确定好要调用的函数的地址了。静态绑定也称为静态多态,比如函数重载。
②动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,即上面所说的,会先到虚表中找具体的函数的地址,再去调用。动态绑定也称为动态多态。
单继承和多继承关系的虚函数表
单继承中的虚函数表
class Base3 {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive3 :public Base3 {
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;
};
int main()
{
Base3 b3;
Derive3 d3;
return 0;
}
用代码打印虚函数的地址
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]);
vTable[i](); // 使用虚函数地址调用虚函数
}
cout << endl;
}
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
int main()
{
Base3 b;
Derive3 d;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
多继承中的虚函数表
class Base4 {
public:
virtual void func1() { cout << "Base4::func1" << endl; }
virtual void func2() { cout << "Base4::func2" << endl; }
private:
int b1;
};
class Base5 {
public:
virtual void func1() { cout << "Base5::func1" << endl; }
virtual void func2() { cout << "Base5::func2" << endl; }
private:
int b2;
};
class Derive4 : public Base4, public Base5 {
public:
virtual void func1() { cout << "Derive4::func1" << endl; }
virtual void func3() { cout << "Derive4::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]);
vTable[i](); // 使用虚函数地址调用虚函数
}
cout << endl;
}
int main()
{
Base4 b1;
Base5 b2;
PrintVTable((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容
PrintVTable((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容
Derive4 d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);//打印派生类对象d的第一个虚表地址及其内容
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base4)));
PrintVTable(vTableb2);//打印派生类对象d的第2个虚表地址及其内容
return 0;
}
在多继承关系当中,派生类的虚表生成过程如下:
- 分别继承各个基类的虚表内容到派生类的各个虚表当中
- 对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1
- 在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如 func3
总结
什么是多态?
多态分为静态多态和动态多态,静态多态是在编译时绑定,也就是函数重载。动态多态通过虚函数的重写之后,指针/引用指向或者引用父类,则调用父类,指向或者引用子类,则调用子类,本质是虚表动态绑定。
inline函数可以是虚函数吗?
理论上不行,但实操时可以,实操时编译器会忽略inline属性,这个函数就不是inline,因为虚函数要放在虚表中去。那具体一点,为什么理论不可行呢?如果按照理论不忽略inline的功能,我们知道inline是使函数在类中展开,即将代码保存在类中,但我们知道这样就与虚函数的功能相违背,因为虚函数是放在虚表中的,二者功能矛盾,就实例来说,在一个继承的虚函数也就是多态,调用这个函数是在虚表中找,那inline如果有用的话,该去那里找呢?所以说编译器会自动忽略inline属性。但同类的对象调用同类的inline就可以保留inline的属性,因为不是多态。因此总结一下:多态调用就没有inline属性,普通调用就可以保持inline属性。
静态成员可以是虚函数吗?
不可以。因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数的地址无法放在虚函数表里。
构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。
对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,即构造函数初始化列表中初始化(虚表指针),一般情况下存在代码段(常量区)的。
注意不要将虚函数列表和虚基表搞混
虚函数表是存放虚函数的地址,为了实现多态。虚基表(继承中的虚继承)是为了构造偏移量,在菱形虚拟继承中防止二义性。