成都营销型网站建设及推广那家好,深圳龙华区大浪社区,项目概述,设计上海展会2021时间一 多态是什么#xff1f;
多态是面向对象三大特征中重要一项#xff0c;另外两项分别是封装与继承。
所谓多态#xff0c;指的是多种不同的形态#xff0c;也就是去完成某个具体的行为#xff0c;多个不同的对象去操作同一个函数时#xff0c;会产生不同的行为
多态是面向对象三大特征中重要一项另外两项分别是封装与继承。
所谓多态指的是多种不同的形态也就是去完成某个具体的行为多个不同的对象去操作同一个函数时会产生不同的行为进而出现不同的状态。
二 C 类中的普通成员函数
普通成员函数面临着两个问题 1. 无法实现多态行为 2. 派生类同名函数会覆盖基类的同名函数即使函数的参数不同也会导致覆盖
// base.h
#includeiostreamclass 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
#includebase.hint 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
#includeiostreamclass 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
#includeA.hint 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
#includeiostreamclass 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
#includeAA.hint 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
#includeiostreamclass 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
#includeAA.hint 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
#includeiostreamclass 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
#includeA.hint 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
#includea.hint 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 *)a2));printf(the addr of the fourth function pointer in class A vptr is: 0x%p\n, *((intptr_t *)*(intptr_t *)a3));pFunc pa_vfunc1 (pFunc)*((intptr_t *)*(intptr_t *)a2);pFunc pa_vfunc2 (pFunc)*((intptr_t *)*(intptr_t *)a3);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 *)b2));printf(the addr of the fourth function pointer in class B vptr is: 0x%p\n, *((intptr_t *)*(intptr_t *)b3));pFunc pb_vfunc1 (pFunc)*((intptr_t *)*(intptr_t *)b2);pFunc pb_vfunc2 (pFunc)*((intptr_t *)*(intptr_t *)b3);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 *)c2));printf(the addr of the fourth function pointer in class C vptr is: 0x%p\n, *((intptr_t *)*(intptr_t *)c3));pFunc pc_vfunc1 (pFunc)*((intptr_t *)*(intptr_t *)c2);pFunc pc_vfunc2 (pFunc)*((intptr_t *)*(intptr_t *)c3);pc_vfunc1();pc_vfunc2();return 0;
}输出 2.3 c 多态实现条件 C 多态的实现条件有三条 1. 指针 2. 指针向上转型 3. 虚函数 多态可以通过下面的例子来去实现
// main.cpp
#includeA.hint 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
#includeiostreamclass 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
#includebase.hint main()
{Base* d1 new Derive1;d1-func();delete d1;return 0;
} 输出