一 多态是什么?
多态是面向对象三大特征中重要一项,另外两项分别是封装与继承。
所谓多态,指的是多种不同的形态,也就是去完成某个具体的行为,多个不同的对象去操作同一个函数时,会产生不同的行为,进而出现不同的状态。
二 C++ 类中的普通成员函数
普通成员函数面临着两个问题:
1. 无法实现多态行为
2. 派生类同名函数会覆盖基类的同名函数,即使函数的参数不同也会导致覆盖
// base.h
#include<iostream>class Base
{
public:Base(){}~Base(){}void func1(){std::cout << "Base::func1()---" << std::endl;}void func2(){std::cout << "Base::func2()---" << std::endl;}
};class Derive1 : public Base
{
public:Derive1(){}~Derive1(){}void func1(){std::cout << "Derive1::func1()---" << std::endl;}void func2(int num){std::cout << "Derive1::func2(int num)---" << std::endl;}
};// main.cpp
#include"base.h"int main()
{// 多态三要素// 1. 指针 2. 向上转型 3. 调用虚函数// 1. 普通函数 是没有多态行为的// 2. 派生类的同名函数会覆盖基类同名函数,即使参数不同也会被覆盖Base* b1 = new Derive1;b1->func1(); // 调用的是 Base::func1() 函数delete b1;// b1->func2(2); // 编译不过,因为 Base::func2() 函数是不带参数的Derive1* d1 = new Derive1;// d1->func2(); // 编译不过,Derive1::func2(int num) 覆盖了同名函数 Base::func2() 函数,delete d1;return 0;
}
输出:
Base::func1()---
Derive1::func1()---
三 C++ 中的虚函数
1. virtual 关键字
c++ 中的 virtual 关键字来常有以下两种使用,
1.1 修饰函数
被 virtual 关键字修饰的类成员函数,被称为虚函数。虚函数分为两种:
1. 纯虚函数 (也叫做抽象函数)
含有纯虚函数的类是无法被实例化的,因为在编译器看来,纯虚函数并不是一个完整的函数,它没有具体的实现,若是在使用时调用它,编译器也不知道到底该怎么办
2. 非纯虚函数
若是类的虚函数都是非纯虚函数,那么这个类是可以实例化的
友元函数、构造函数与 static 函数 是不能被 virtual 关键字修饰的。
虚函数具有继承性,基类中 virtual关键字修饰的虚函数,派生类重写时,可以不再用 virtual 修饰,重写的基类虚函数会自动成为虚函数。
// A.h
#include<iostream>class A
{
public:A() { }virtual ~A() { }// 虚函数virtual void vfunc1(){std::cout << "A::vfunc1()---" << std::endl;}// 纯虚函数virtual void vfunc() = 0;private:int m_data1;
};class B : public A
{
public:B(){ }~B() { }void vfunc1(){std::cout << "B::vfunc1()---" << std::endl;}void vfunc(){std::cout << "B::vfunc()---" << std::endl;}
private:int m_data2;
};// main.cpp
#include"A.h"int main()
{// A a1; // 类中有纯虚函数,无法实例化,编译不过// A* a2 = new A; // 类中有纯虚函数,无法实例化,编译不过B b1; // B class 实现了 A class 的纯虚函数,可以实例化B* b2 = new B; // B class 实现了 A class 的纯虚函数,可以实例化delete b2;return 0;
}
1.2 修饰类继承
C++虚继承和虚基类详解 - 知乎 (zhihu.com)
1.2.1 public、protected 与 private 继承的区别
我们知道 类之间的继承有三种:public、protected 与 private
三者区别为:
- public 继承
- 基类中原 public 的成员 在派生类中仍然是 public
- 基类中原 protected 的成员 在派生类中仍然是 protected
- 基类中原 private 的成员 在派生类中仍然是 private
- protected
- 基类中原 public 的成员 在派生类中变为了 protected
- 基类中原 protected 的成员 在派生类中仍然是 protected
- 基类中原 private 的成员 在派生类中仍然是 private
- private
- 基类中原 public 的成员 在派生类中变为了 private
- 基类中原 protected 的成员 在派生类中仍然是 private
- 基类中原 private 的成员 在派生类中仍然是 private
class A
{};class B1 : public A
{};class B2 : protected A
{};class B3 : private A
{};
那么 virtual 继承是用于解决什么问题呢?下面让我们看以下的场景:
有这样一种继承关系,菱形继承:
// AA.h
#include<iostream>class AA
{
public:AA(int a = 0):m_a(a){std::cout << "AA constructor." << std::endl;}~AA(){std::cout << "AA destructor." << std::endl;}public:int m_a;
};class BB : public AA
{
public:BB(int a = 0):m_b(a){std::cout << "BB constructor." << std::endl;}~BB(){std::cout << "BB destructor." << std::endl;}public:int m_b;
};class CC : public AA
{
public:CC(int a = 0):m_c(a){std::cout << "CC constructor." << std::endl;}~CC(){std::cout << "CC destructor." << std::endl;}public:int m_c;
};class DD : public BB, public CC
{
public:DD(int a = 0):m_d(a){std::cout << "DD constructor." << std::endl;}~DD(){std::cout << "DD destructor." << std::endl;}void set_a(int a){// m_a = a; // 编译不通过,这里有歧义,可能是 BB::m_a ,也可能是 CC::m_a}void set_b(int b){m_b = b;}void set_c(int c){m_c = c;}void set_d(int d){m_d = d;}public:int m_d;
};// main.cpp
#include"AA.h"int main()
{D d;return 0;
}
输出:
通过以上代码与输出我们可以得出下面两点结论:
1. AA 类的构造函数与析构函数分别被执行了两次,分别是 DD 继承 BB 类时执行,DD继承 CC 类时执行。
2. m_a = a 这行代码是无法通过编译的,因为编译器无法判断 m_a 是从路径 AA -> BB -> DD 还是从路径 AA -> CC -> DD 得来的,产生了歧义。
那么是否有办法消除歧义呢?
答案是有,可以通过如下的代码来消除歧义
// 使用 BB 类路径的 m_a
void set_a(int a)
{BB::m_a = a;
}
// 使用 CC 类路径的 m_a
void set_a(int a)
{CC::m_a = a;
}
上面的多继承的方式主要有两点问题:
1. 派生类 DD 有两个 m_a 成员变量,分别来自于 AA -> BB -> DD 于 AA -> CC -> DD,这样会造成冗余与内存的浪费
2. 同名变量与函数会出现命名的冲突
1.2.2 虚继承
为了解决多继承这两点问题,c++引入了虚继承的概念
虚继承代码如下:
// AA.h
#include<iostream>class AA
{
public:AA(int a = 0):m_a(a){std::cout << "AA constructor." << std::endl;}~AA(){std::cout << "AA destructor." << std::endl;}public:int m_a;
};class BB : virtual public AA
{
public:BB(int a = 0):m_b(a){std::cout << "BB constructor." << std::endl;}~BB(){std::cout << "BB destructor." << std::endl;}public:int m_b;
};class CC : virtual public AA
{
public:CC(int a = 0):m_c(a){std::cout << "CC constructor." << std::endl;}~CC(){std::cout << "CC destructor." << std::endl;}public:int m_c;
};class DD : public BB, public CC
{
public:DD(int a = 0):m_d(a){std::cout << "DD constructor." << std::endl;}~DD(){std::cout << "DD destructor." << std::endl;}void set_a(int a){m_a = a; // 编译通过}void set_b(int b){m_b = b;}void set_c(int c){m_c = c;}void set_d(int d){m_d = d;}public:int m_d;
};// main.cpp
#include"AA.h"int main()
{DD dd;return 0;
}
输出:
通过输出可以看出,AA类的构造函数与析构函数只被执行了一次,这说明在虚继承时,不再存在两种路径 AA -> BB -> DD 与 AA -> CC -> DD。
上述的虚继承代码解决了菱形继承中 m_a 数据冗余的问题,所以 D 类中直接访问 m_a 就不再有歧义的问题了。
// 虚继承中访问 m_a 无冗余问题
void set_a(int a)
{m_a = a;
}
总结:
虚继承的目的是为了表明某个类的基类是可以被这个类的所有派生类共享,这个基类也被称作虚基类(Virtual Base Class),上述代码中的 class AA 就是 class BB 与 class CC 的虚基类。
不管虚基类在派生类中出现多少次,最终在派生类中都只有一份虚基类的成员变量与成员函数。
2. 虚函数指针与虚函数表
2.1 c++多态原理
c++ 实现多态的原理就是利用 虚函数表指针与虚函数表。
若是定义的类实现了一个或者多个虚函数,那么这个类会有一张对应的虚函数表,表中存放的是对应虚函数的指针。
如下图所示:
若是派生类 B 重写了基类 A 中的 虚函数 vfunc1,那么在 class B 的虚函数表中对应的 vfunc1 虚函数的地址就是派生类 B 重写的虚函数 B::vfunc1 地址;
若是 派生类 B 没有重写了基类 A 中的 虚函数 vfunc2,那么在 class B 的虚函数表中对应的 vfunc2 虚函数的地址就是基类 A 的虚函数 A::vfunc2 地址。
当新生成 class B或者 class C 对象时,会在对象内部自动生成一个指向虚函数表地址的虚函数表指针 vptr,如果我们用 sizeof(A) 发现其大小为 16 ,计算方式为 两个 int 类型的成员变量大小分别为 4 ,有一个默认的虚函数指针大小为 8 (这里是 64 位操作系统,如果是 32 位操作系统的话,指针大小为 4)所以 sizeof(B) = 4 * 2 + 8
// A.h
#include<iostream>class A
{
public:A() { }virtual ~A() { }virtual void vfunc1() override{std::cout << "A::vfunc1()---" << std::endl;}virtual void vfunc2(){std::cout << "A::vfunc2()---" << std::endl;}
private:int m_data1;int m_data2;
};class B : public A
{
public:B(){ }~B() { }void vfunc1() override{std::cout << "B::vfunc1()---" << std::endl;}
private:int m_data2;int m_data3;};class C : public B
{
public:C(){ }~C(){ }void vfunc1() override{std::cout << "C::vfunc1()---" << std::endl;}
private:int m_data1;int m_data4;
};// main.cpp
#include"A.h"int main()
{// 多态三要素// 1. 指针 2. 向上转型 3. 调用虚函数A* a = new A;A* b = new B;A* c = new C;a->vfunc1(); // A::vfunc1a->vfunc2(); // A::vfunc2b->vfunc1(); // B::vfunc1b->vfunc2(); // A::vfunc2c->vfunc1(); // C::vfunc1c->vfunc2(); // A::vfunc2return 0;
}
输出
通过输出可以看出,指针为 A* 的 B 类对象调用的 vfunc1 函数为 B 类中重写的 vfunc1 虚函数; 指针为 A* 的 C 类对象调用的 vfunc1 函数为 C 类中重写的 vfunc1 虚函数。
2.2 虚函数表分析
以下通过类的内存布局来看一下虚函数表是什么样的:
通过debug 方式,查看 A类、B类 与 C 类生成对象中虚函数表中的内存分配,操作系统是 windows 10 ,64位。
A类虚函数表 vptr 中的头两个元素存放的是 其析构函数 A::~A() 的地址,第三个元素是虚函数 A::vfunc1() 的地址,第四个元素 是虚函数 A::vfunc2() 的地址;
B类虚函数表 vptr 中的头两个元素存放的是 其析构函数 B::~B() 的地址,第三个元素是虚函数 B::vfunc1() 的地址,第四个元素 是虚函数 A::vfunc2() 的地址;
C类虚函数表 vptr 中的头两个元素存放的是 其析构函数 C::~C() 的地址,第三个元素是虚函数 C::vfunc1() 的地址,第四个元素 是虚函数 A::vfunc2() 的地址;
如何获取到类 Object 的虚函数表呢?对于有虚函数的对象 object 来说:
1. &object 代表对象 object 的起始地址
2. (intptr_t *)&object 代表获取对象起始地址的前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节),而这前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节)则是虚函数表的指针
3. *(intptr_t *)&object 则是取前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节)中的数据,也就是虚函数表 vptr 的地址
4. 取 虚函数表 vptr 地址的前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节),为虚函数表中第一个元素的地址 (intptr_t *) *(intptr_t *)&object,取出虚函数表中第一个元素 *((intptr_t *) *(intptr_t *)&object), 则取出虚函数表中第 n 个元素为 *((intptr_t *) *(intptr_t *)&object + n),所取元素即为 虚函数的地址
5. 定义一个函数指针:typedef void(*pFunc)(); 函数指针 pFunc 代表 形如 void print(); 的函数的指针类型。通过前面的 1 - 4 步,获取道虚函数的地址,将地址赋给函数指针 pFunc ,就可以通过 pFunc 调用对应的虚函数了。 深入浅出——理解c/c++函数指针 - 知乎 (zhihu.com)
我们可以把类对象的虚函数表中的虚函数指针获取出来,直接调用虚函数,代码如下所示:
// main.cpp
#include"a.h"int main()
{typedef void(*pFunc)() ;A a;printf("the addr of A::vfunc1 is 0x%p\n", &A::vfunc1);printf("the addr of the third function pointer in class A vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&a+2));printf("the addr of the fourth function pointer in class A vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&a+3));pFunc pa_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&a+2);pFunc pa_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&a+3);pa_vfunc1();pa_vfunc2();B b;printf("the addr of B::vfunc1 is 0x%p\n", &B::vfunc1);printf("the addr of the third function pointer in class B vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&b+2));printf("the addr of the fourth function pointer in class B vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&b+3));pFunc pb_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&b+2);pFunc pb_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&b+3);pb_vfunc1();pb_vfunc2();C c;printf("the addr of B::vfunc1 is 0x%p\n", &C::vfunc1);printf("the addr of the third function pointer in class C vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&c+2));printf("the addr of the fourth function pointer in class C vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&c+3));pFunc pc_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&c+2);pFunc pc_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&c+3);pc_vfunc1();pc_vfunc2();return 0;
}
输出:
2.3 c++ 多态实现条件
C++ 多态的实现条件有三条:
1. 指针
2. 指针向上转型
3. 虚函数
多态可以通过下面的例子来去实现:
// main.cpp
#include"A.h"int main()
{// 多态三要素:1. 指针 2. 向上转型 3. 调用虚函数A* a = new A; // 指针a->vfunc1();a->vfunc2();B b;a = &b; // 向上转型a->vfunc1(); // 调用虚函数a->vfunc2();C c;a = &c; // 向上转型a->vfunc1(); // 调用虚函数a->vfunc2();return 0;
}
输出:
四 常见问题
1. 虚函数指针属于类还是对象?
答:虚函数指针属于对象,每个含有虚函数的类对象都有一个默认的虚函数指针,通过虚函数指针可以获取到虚函数表,进而实现动态调用虚函数,实现多态行为
2. 虚函数表属于类还是对象?
答:虚函数表属于类,让我们站在设计者的角度思考一下,虚函数表本身是占有一定的内存空间的,而每个对象的虚函数表都是相同的,要是每个对象都有一个,那得耗费多少冗余内存啊,所有对象共用一份虚函数表即可。
3. 基类的析构函数为什么要加 virtual 关键字
答:若是基类的析构函数不加 virtual 关键字的话,则 delete 多态时的对象指针是,会出现只调用基类的析构函数,未调用派生类的析构函数,那么派生类的数据就可能未被释放掉,会出现内存泄漏的现象。
实验如下:
// base.h
#include<iostream>class Base
{
public:Base(){std::cout << "Base constructor." << std::endl;}~Base(){std::cout << "Base destructor." << std::endl;}virtual void func(){std::cout << "virtual Base::func()---" << std::endl;}};class Derive1 : public Base
{
public:Derive1(){std::cout << "Derive1 constructor." << std::endl;}virtual ~Derive1(){std::cout << "Derive1 constructor." << std::endl;}void func() override{std::cout << "virtual Derive1::func()---" << std::endl;}};// main.cpp
#include"base.h"int main()
{Base* d1 = new Derive1;d1->func();delete d1;return 0;
}
输出: