文章目录
- 一、多态的理解
- 二、多态的定义及实现
- 1.多态的构成条件
- 2.虚函数
- 3.虚函数的重写/覆盖
- 4. C++11 的 override 和 final
- 5.重载、重写/覆盖、隐藏/重定义
- 三、抽象类
- 四、多态的原理
- 1.虚函数表
- 2.多态的原理
- 3.动态绑定与静态绑定
- 五、单继承和多继承关系的虚函数表
- 1.单继承中的虚函数表
- 2.多继承中的虚函数表
- 3.菱形继承、菱形虚拟继承中的虚函数表
一、多态的理解
多态的概念,通俗来讲,就是多种形态。具体来讲,就是去完成某个行为,当不同的对象去完成时,会产生出不同的行为。
比如:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票。
// 静态的多态:函数重载,看起来调用同一个函数有不同的行为,编译时实现
// 动态的多态:一个父类的指针或引用去调用同一个函数,传递不同的对象,会调用不同的函数,运行时实现
// 本质:不同的对象去做同一件事情,行为不同
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
// 子类中满足三同(函数名、参数、返回值)的虚函数,叫做重写/覆盖
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket(); // 多态
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
运行结果是:
买票-全价
买票-半价
二、多态的定义及实现
1.多态的构成条件
那么在继承中要构成多态有两个条件:
- 必须通过基类的指针或引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
这两个条件缺一不可。
2.虚函数
虚函数:被virtual
修饰的类的非静态成员函数称为虚函数。
其它函数不能成为虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
3.虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数,称派生类的虚函数重写/覆盖了基类的虚函数。
注:完全相同,指函数名、参数、返回值完全相同。
派生类虚函数重写时,继承的是基类虚函数的接口,重写的是虚函数的实现!
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
// 子类中满足三同(函数名、参数、返回值)的虚函数,叫做重写/覆盖
virtual void BuyTicket() { cout << "买票-半价" << endl; }
// 注意:在重写基类虚函数时,若派生类的虚函数不加virtual关键字,也可以构成重写
// 因为基类的虚函数被继承下来了,在派生类依旧保持虚函数属性
// 若其访问限定符为非公有,也能调用,因为保持了基类虚函数的访问限定符
// 重写的本质是重写了内容
// 但是这种写法不是很规范,不建议这样使用
//void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
运行结果是:
买票-全价
买票-半价
验证构成多态的条件:
① 破坏条件1,没有通过基类的指针或引用调用虚函数:
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
// 若构成多态,传的是哪个类的对象,调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态,调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
运行结果是:
买票-全价
买票-全价
② 破坏条件2,不是虚函数:
class Person
{
public:
void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
void BuyTicket() { cout << "买票-半价" << endl; }
};
// 若构成多态,传的是哪个类的对象,调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态,调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
运行结果是:
买票-全价
买票-全价
③ 破坏条件2,不构成重写。
class Person
{
public:
virtual void BuyTicket(char ch) { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket(int i) { cout << "买票-半价" << endl; }
};
// 若构成多态,传的是哪个类的对象,调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态,调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person& p)
{
p.BuyTicket(10);
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
运行结果是:
买票-全价
买票-全价
虚函数重写的两个例外:
- 协变
派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,不过必须是基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。
测试代码1:
// 重写要求返回值相同有一个例外:协变 -- 返回值是父子关系的指针或引用
class Person
{
public:
virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person
{
public:
virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
测试代码2:
// 重写要求返回值相同有一个例外:协变 -- 返回值是父子关系的指针或引用
class A {};
class B : public A {};
class Person
{
public:
virtual A* f() { cout << "Person" << endl; return nullptr; }
};
class Student : public Person
{
public:
virtual B* f() { cout << "Student" << endl; return nullptr; }
};
void Func(Person& p)
{
p.f();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
- 析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual
关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名字不同。析构函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
// 若析构函数是虚函数,则构成重写
// 析构函数名被特殊处理了,处理成了destructor
class Person
{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
// 普通对象,析构函数是否是虚函数,是否完成重写,都正确调用了
//Person p;
//Student s;
// 动态申请的父子对象,如果给了父类指针管理
// 那么需要析构函数是虚函数,子类完成重写,构成多态
// 这样才能正确调用子类的析构函数
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
// 若父类的析构函数不是虚函数
// 那么delete p2时没有正确调用子类析构函数,导致内存泄漏
// 其它场景,析构函数是否为虚函数,都可以正确调用析构函数
// 比如上面的普通对象场景
return 0;
}
4. C++11 的 override 和 final
final
:
① 修饰类,表示该类不能被继承。
② 修饰虚函数,表示该虚函数不能被重写。
// C++11 final 修饰类,直接限制它不能被继承
class A final
{
protected:
int _a;
};
class B : public A // 编译报错
{
};
// C++11 final 修饰虚函数,限制它不能被子类中的虚函数重写
class C
{
public:
virtual void f() final
{
cout << "C::f()" << endl;
}
};
class D : public C
{
public:
virtual void f() // 编译报错
{
cout << "D::f()" << endl;
}
};
override
:检查派生类虚函数是否重写了基类某个虚函数。如果没有重写,则会编译报错。
// C++11 override 放在子类虚函数后面,检查它是否重写了父类的某个虚函数
// 如果没有重写,则会编译报错
// 比如想让父类的某个函数被子类重写,但是忘了加virtual关键字
// 给子类虚函数加了override,就会检查到没有重写父类的某个虚函数,就会编译报错
class Car
{
public:
void Drive()
{}
};
class Benz :public Car
{
public:
virtual void Drive() override // 编译报错
{
cout << "Benz-舒适" << endl;
}
};
5.重载、重写/覆盖、隐藏/重定义
三、抽象类
- 在虚函数的后面写上
= 0
,则这个函数为纯虚函数。 - 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承抽象类后由于包含纯虚函数,也是抽象类,也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。 - 纯虚函数更体现出了接口继承。
override
只是在语法上检查派生类虚函数是否重写了基类某个虚函数。
接口继承和实现继承:
① 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
② 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
// 抽象 -- 在现实世界中没有对应的实物
// 一个类型,如果一般在现实世界中,没有具体的对应实物,就定义成抽象类比较好
class Car // 抽象类
{
public:
// 纯虚函数一般只声明不实现(可以实现,但是没有价值)
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
//Car c; // 编译报错,因为抽象类不能实例化出对象
Car* p1 = new Benz;
p1->Drive();
Car* p2 = new BMW;
p2->Drive();
return 0;
}
四、多态的原理
1.虚函数表
先看下面的代码:
// 在32位平台下,sizeof(Base) == ?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
protected:
int _b = 1;
char _ch = 'A';
};
int main()
{
Base b;
cout << sizeof(Base) << endl;
return 0;
}
在 32 位平台下,sizeof(Base) == 12 。
原因:
虚函数表指针(即 vfptr ,v 代表 virtual ,f 代表 function),指向虚函数表。
一个含有虚函数的类,其对象至少有一个虚函数表指针(在当前平台放在对象的最前面,注意有些平台可能会放到对象的最后面,这个跟平台有关)。
所以,加入虚函数表指针后再按照内存对齐规则计算,在 32 位平台下,该类的大小为 12 。
- 一个虚函数表指针指向一张虚函数表(也简称为虚表),虚函数表里存放虚函数的地址,即虚函数表本质就是函数指针数组。
- 虚函数表在编译阶段生成,存放在常量区(常量区是 C/C++ 语言角度,在操作系统角度常量区和代码段都是代码区)。
用一段代码来进行验证:
// 还是使用上面的代码
int main()
{
int a = 0;
printf("栈:%p\n", &a);
int* p = (int*)malloc(4);
printf("堆:%p\n", p);
static int b = 0;
printf("数据段:%p\n", &b);
const char* str = "aaaaaa";
printf("常量区:%p\n", str);
printf("代码段:%p\n", &Base::Func1);
Base bs;
printf("虚函数表:%p\n", *((int*)&bs)); // 查看虚表指针内容(虚表的地址)
return 0;
}
我们可以看到,虚表存放的位置离常量区最近,实际上虚表存放在常量区。
为什么虚表会存放在常量区呢?因为虚表创建好后是不会被修改的。
- 虚函数的重写,也叫做覆盖。子类继承父类后,会先拷贝一份父类的虚表,然后再把子类重写的虚函数地址覆盖虚表中父类虚函数地址,形成子类的虚表。
因此,可以认为重写是语法层的概念,覆盖是原理层的概念。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
protected:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
protected:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
虚表里只存放虚函数的地址,非虚函数的地址不会被放进虚表。
2.多态的原理
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:
int _a = 1;
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
int _b = 2;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
解释多态的原理:
多态是如何实现的?
① 普通函数的调用都是在编译时直接决定地址的,而满足多态的虚函数的调用是运行时才能决定地址的:先通过父类的指针或引用找到对象的虚表指针,再通过虚表指针找到其指向的虚表,然后在虚表中找对应函数的地址。
② 若传递的是父类对象,父类的指针或引用指向的是父类对象,它找父类对象的虚表,调用的是父类的虚函数。
③ 若传递的是子类对象,会发生切片,父类的指针或引用指向的是子类对象中的父类对象,它找父类对象的虚表,由于子类的虚函数完成了覆盖(子类虚函数地址覆盖了父类虚函数地址),调用的是子类的虚函数。
④ 所以满足多态的虚函数的调用过程中,执行相同的指令,调用的是不同的虚函数。
多态的实现依赖于虚函数的重写,虚函数重写了以后,父子类对象的虚表中存的就是不同的虚函数地址,就能实现多态。
为什么多态的实现必须是基类的指针或引用呢?
还是上面的代码,通过对比对象切片和引用切片就可以理解了:
① 引用切片,r1 是子类对象中父类对象的别名(指针的话,就是指向子类对象中的父类对象),指针或引用切片都没有出现对子类对象中的父类对象进行拷贝的行为。实现多态时,调用的虚函数一定是子类对象虚表中的虚函数。
② 对象切片,p 是 Johnson 子类对象中父类对象的拷贝,但唯独 p 的虚表指针不是 Johnson 子类对象中虚表指针的拷贝,而且 p 的虚表指针也不可能是 Johnson 子类对象中虚表指针的拷贝,父类对象的虚表指针一定指向父类虚表,子类对象的虚表指针一定指向子类虚表。
③ 因此,多态的实现必须是基类的指针或引用。
同类型的对象,虚表指针是相等的,指向同一张虚表。
// 还是使用上面的代码
int main()
{
Person p1;
Person p2;
Student s1;
Student s2;
return 0;
}
普通函数和虚函数的存储位置是一样的,都在代码段。只是虚函数又把地址存到虚表中,方便实现多态。
- 普通函数的调用,在编译时就能决定它的地址。
- 满足多态的虚函数的调用,不是编译时就能决定它的地址的,而是运行时需要到对象的虚表中寻找,才能决定它的地址。
- 不满足多态的虚函数的调用,跟普通函数的调用一样,在编译时就能决定它的地址,不需要在运行时到对象的虚表中寻找。
总结:在编译时能直接决定调用函数的地址的,一定会在编译时决定。满足多态的虚函数调用,在编译时是不能直接决定调用虚函数的地址的,只能在运行时去对象虚表中找虚函数的地址,这样才能决定。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
void f() { cout << "f()" << endl; }
public:
int _a = 1;
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
int _b = 2;
};
void Func(Person& p)
{
p.BuyTicket(); // 满足多态,运行时决定函数地址
p.f(); // 编译时决定函数地址
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
Mike.BuyTicket(); // 编译时决定函数地址
Johnson.BuyTicket(); // 编译时决定函数地址
return 0;
}
3.动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
- 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
五、单继承和多继承关系的虚函数表
1.单继承中的虚函数表
我们都知道,派生类中的虚函数,无论是否重写,都会被放到虚表中:重写的虚函数会覆盖,自己新增加的虚函数会按其在派生类中的声明次序增加到虚表的最后。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
protected:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
protected:
int _d = 2;
};
int main()
{
Derive d;
Base* p1 = &d;
p1->Func1();
return 0;
}
由于监视窗口隐藏了派生类虚表中未重写的虚函数,所以需要通过内存窗口观察派生类对象的虚表才能看到:
虚表中存的地址,严格上来说,其实也不是真正意义上的虚函数地址,而是在虚函数地址基础上封装了一层的地址。
这里虚函数的调用满足多态,eax 中存放的是从虚表中取出来的地址,这个就是封装过的虚函数地址。
执行 call 指令后,会到那个地址(jmp 指令的地址)。接着执行 jmp 指令,才会跳到真正意义上的虚函数地址。
换言之,虚表中存的是 jmp 指令的地址,而不是真正的虚函数地址,实际调用时相当于多跳了一层。
我们也可以通过打印虚表的方式去查看虚表:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
protected:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
protected:
int _d = 2;
};
// 这里虚函数的类型都是void(*)(),方便我们打印虚表
// 将函数指针类型void(*)()重命名为VF_PTR,简化类型名称
typedef void(*VF_PTR)();
// 依次取虚表中的虚函数指针打印并调用
// 通过调用可以直观地看出存的是哪个虚函数
//void PrintVFTable(VF_PTR table[]) // 两种写法均可
void PrintVFTable(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; ++i) // VS在虚表的最后会放nullptr
{
printf("vft[%d]:%p -> ", i, table[i]);
table[i]();
}
cout << endl;
}
int main()
{
Base b;
// 为了拿到虚表指针的值,需要取b头上4/8(32位/64位)个字节的内容
// 先强转成能看到b头上4/8个字节的指针类型int*/long long*
// 解引用就能取到b头上4/8个字节的内容
// 指针类型int*/long long*解引用后就是int/long long
// 但这不是我们想要的类型,所以还需强转为VF_PTR*
// 因为这里的虚表就是一个存VF_PTR类型(函数指针类型)的数组
// 最后传参给PrintVFTable函数
//PrintVFTable((VF_PTR*)(*(int*)&b)); // 32位平台
//PrintVFTable((VF_PTR*)(*(long long*)&b)); // 64位平台
// 上面的这种方式不能根据32位和64位平台自适应,不是很好
// 其实最好的方式是先强转为二级指针
// 因为二级指针能看到b头上的一个指针这么多个字节
// 解引用就能取到b头上的一个指针大小的内容
// 又因为指针的大小在32/64位下是4/8个字节
// 所以这种方式在32位和64位平台下能自适应
// 虽说是二级指针都可以,但是建议用void**,免得引起别人疑惑
PrintVFTable((VF_PTR*)(*(void**)&b));
Derive d;
PrintVFTable((VF_PTR*)(*(void**)&d));
return 0;
}
运行结果:
其实这种方式有点非法,因为可以乱调用虚表中的虚函数。所以说虚表是有安全小隐患的。当然,我们在这里只是以这种方式去查看虚表而已,实际中并不会这样用。
2.多继承中的虚函数表
多继承中,派生类未重写的虚函数,会被放到第一个继承基类的虚表中。
派生类中基类继承顺序的声明,就是派生类实际继承基类的顺序。
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
protected:
int _b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
protected:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
protected:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
p1->func1();
Base2* p2 = &d;
p2->func1();
return 0;
}
通过内存窗口观察派生类对象的虚表:
在多继承中,子类重写了父类 Base1 和 Base2 的虚函数 func1() ,可是两个虚表中重写的 func1() 的地址不相同,这是因为虚表中存的是 jmp 指令的地址,而不是真正的虚函数地址,但最终都会跳到同一个真正的虚函数地址。这个可以通过调试反汇编来进行验证。
实际上,在多继承中,满足多态的情况下,相比于用第一个继承基类的指针或引用,用非第一个继承基类的指针或引用去调用派生类对象的虚函数时会多做一些准备工作,主要是修正 ecx 存放的值(this 指针),即指针的偏移。
通过打印虚表的方式去查看虚表:
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
protected:
int _b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
protected:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
protected:
int _d;
};
typedef void(*VF_PTR)();
// 依次取虚表中的虚函数指针打印并调用
// 通过调用可以直观地看出存的是哪个虚函数
void PrintVFTable(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; ++i)
{
printf("vft[%d]:%p -> ", i, table[i]);
table[i]();
}
cout << endl;
}
int main()
{
Base1 b1;
PrintVFTable((VF_PTR*)(*(void**)&b1));
Base2 b2;
PrintVFTable((VF_PTR*)(*(void**)&b2));
Derive d;
// 打印Derive从Base1继承的虚表
PrintVFTable((VF_PTR*)(*(void**)&d));
// 打印Derive从Base2继承的虚表
PrintVFTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
// 打印Derive从Base2继承的虚表的另一种方式
// 即利用切片时指针的自动偏移
// 跟上面的方式本质是一样的
//Base2* p = &d;
//PrintVFTable((VF_PTR*)(*(void**)p));
return 0;
}
运行结果:
3.菱形继承、菱形虚拟继承中的虚函数表
实际中我们不建议设计出菱形继承及菱形虚拟继承,它们再叠加上多态,就会非常复杂,所以我们就不看它们的虚表了,一般我们也不需要研究清楚,因为实际中很少用。