大纲
多态的原理
多态的实现原理
- 当类中声明了虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表是由编译器自动生成和维护的
- 虚函数(
virtual
)会被编译器放入虚函数表中 - 当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++ 编译器给父类对象、子类对象提前设置了
VPTR
虚函数表指针,因此 C++ 编译器不需要区分子类对象或者父类对象,只需要在 base
指针中,找 VPTR
指针即可) VPTR
虚函数表指针一般作为类对象的第一个成员
多态的实现原理图解
- a) 多态实现原理的图解 如图 所示
- b) 通过
VPTR
虚函数表指针调用重写函数的过程是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数,而普通成员函数是在编译时就确定了调用的函数 - c) 在效率上,虚函数的效率要低很多,因此出于效率考虑,没有必要将所有成员函数都声明为虚函数,即使 C++ 编译器允许这么做
- d) 由于有了虚函数表,C++ 编译器不再需要知道是子类对象还是父类对象,这往往会给我们造成一种假象:C++ 编译器能识别子类对象或者父类对象
证明 VPTR
指针的存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <iostream>
using namespace std;
class Parent1 { public: Parent1(int a) { this->a = a; }
void print() { cout << "I'm parent1" << endl; }
private: int a; };
class Parent2 { public: Parent2(int a) { this->a = a; }
virtual void print() { cout << "I'm parent2" << endl; }
private: int a; };
int main() { cout << "sizeof(Parent1): " << sizeof(Parent1) << endl; cout << "sizeof(Parent2): " << sizeof(Parent2) << endl; return 0; }
|
程序运行的输出结果如下:
1 2
| sizeof(Parent1): 4 sizeof(Parent2): 8
|
父类指针和子类指针的步长可能是不一样的
- a) 指针也只一种数据类型,对 C++ 类对象的指针执行
++
、--
运算符仍然是合法的 - b)
"多态是用父类的指针指向子类的对象"
和 "父类指针步长的自加(++)"
是两个完全不同的概念 - c) 当子类继承父类后,没有添加任何自己的成员变量和成员函数,那么此时父类指针和子类指针的步长才是一样的
- d) 指针运算是按照指针所指的类型进行的,父类指针和子类指针的步长可能是不一样的,不要用父类指针自加(
++
)、自减(--
)的方式来操作子类的对象数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #include <iostream> using namespace std;
class Parent { public: Parent(int a = 0) { this->a = a; }
virtual void print() { cout << "I'm parent" << endl; }
private: int a; };
class Child : public Parent { public:
Child(int b, int c) :Parent(0) { this->b = b; this->c = c; }
virtual void print() { cout << "I'm child" << endl; } private: int b; int c; };
int main() { Parent* parent = NULL; Child* child = NULL;
Child array[] = { Child(1, 2), Child(3,4), Child(5, 6) }; parent = array; child = array;
parent++; child++;
parent++; child++;
return 0; }
|
在父类的构造函数中调用虚函数,不能实现多态
子类的 VPTR
指针是分步完成初始化的,当执行父类的构造函数时,子类 的 VPTR
指针指向父类的虚函数表,当父类的构造函数执行完毕后,才会把子类的 VPTR
指针指向子类的虚函数表。因此,在父类的构造函数中调用虚函数,不能实现多态。
- a) 分析图解 如图 所示
- b) 对象在创建的时,由编译器对
VPTR
指针进行初始化 - c) 只有当对象的构造全部完成后,
VPTR
指针的指向才能最终确定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <iostream>
using namespace std;
class Parent {
public: Parent(int a) { this->a = a; print(); }
virtual void print() { cout << "I'm parent, a = " << a << endl; }
private: int a; };
class Child : public Parent { public:
Child(int a, int c) : Parent(a) { this->c = c; }
virtual void print() { cout << "I'm child, c = " << c << endl; }
private: int c; };
int main() { Child child(5, 8); return 0; }
|
程序运行的输出结果如下:
纯虚函数和抽象类
纯虚函数和抽象类的基本概念
基本概念:
- a) 纯虚函数是一个在基类中说明的虚函数,且在基类中没有被定义,要求任何派生类都定义自己的版本
- b) 纯虚函数为各派生类提供一个公共界面,可以实现接口的封装和设计、软件的模块功能划分
- c) 纯虚函数的声明形式:
virtual 类型 函数名 ( 参数表 ) = 0;
- d) 一个具有纯虚函数的基类称为抽象类
使用限制:
- a) 可以声明抽象类的指针和引用
- b) 抽象类不能创建对象(实例化)
- c) 抽象类不能作为函数的参数类型和返回值类型
![]()
![]()
纯虚函数和抽象类的应用案例
定义一个图形抽象类 Figure,并声明了负责计算图形面积的纯虚函数 getArea()
,然后再定义 Circle、Triangle、Squre 派生类,并各自实现了纯虚函数 getArea()
来计算不同图形的面积。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| #include <iostream>
using namespace std;
class Figure {
public: virtual double getArea() = 0; };
class Circle : public Figure {
public:
Circle(double r) { this->r = r; }
virtual double getArea() { double area = 3.14 * r * r; cout << "圆的面积: " << area << endl; return area; }
private: double r; };
class Triangle : public Figure {
public: Triangle(double a, double b) { this->a = a; this->b = b; }
virtual double getArea() { double area = a * b / 2; cout << "三角形的面积: " << area << endl; return area; }
private: double a; double b; };
class Square : public Figure {
public: Square(double a, double b) { this->a = a; this->b = b; }
virtual double getArea() { double area = a * b; cout << "四边形的面积: " << area << endl; return area; }
private: double a; double b; };
void printArea(Figure* base) { base->getArea(); }
int main() {
Triangle Triangle(20, 30); Circle circle(6.8); Square square(50, 60);
Figure* pBase = new Circle(5.3); pBase->getArea();
Figure& base = square; base.getArea();
printArea(&Triangle);
return 0; }
|
程序运行的输出结果如下:
1 2 3
| 圆的面积: 88.2026 四边形的面积: 3000 三角形的面积: 300
|
纯虚函数和抽象类在多继承中的应用案例
C++ 中没有 Java 中的接口概念,但可以使用抽象类和纯虚函数模拟 Java 中的接口(代码如下)。值得一提的是,C++ 中的接口类只有函数原型定义,没有任何数据的定义,同时继承多个接口类不会带来二义性和复杂性等问题。C++ 面向抽象类编程(Java 面向接口编程)是项目开发中重要技能之一。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| #include <iostream>
using namespace std;
class Interface1 {
public: virtual int add(int a, int b) = 0; virtual void print() = 0; };
class Interface2 {
public: virtual int mult(int a, int b) = 0; virtual void print() = 0; };
class Parent {
public: Parent() { this->a = 8; }
virtual ~Parent() {
}
virtual int getA() { return a; }
private: int a; };
class Child : public Parent, public Interface1, public Interface2 {
public: int add(int a, int b) { return a + b; }
int mult(int a, int b) { return a * b; }
void print() { cout << "Child::print() 函数被执行" << endl; }
};
int main() { Child child; child.print();
Parent* parent = &child; cout << "a = " << parent->getA() << endl;
Interface1* interface1 = &child; int result1 = interface1->add(2, 5); cout << "2 + 5 = " << result1 << endl;
Interface2* interface2 = &child; int result2 = interface2->mult(3, 6); cout << "3 * 6 = " << result2 << endl;
return 0; }
|
程序运行的输出结果如下:
1 2 3
| a = 8 2 + 5 = 7 3 * 6 = 18
|
纯虚函数和抽象类在多继承中的使用总结
C++ 中没有 Java 中的接口概念:
- 绝大多数面向对象语言都不支持多继承
- 绝大多数面向对象语言都支持接口的概念
- C++ 中没有 Java 中的接口概念,但可以使用抽象类和纯虚函数模拟 Java 中的接口
- C++ 中的接口类只有函数原型定义,没有任何数据的定义(代码如下)
★点击显示示例代码★
1 2 3 4 5 6 7
| class Interface { public: virtual void func1() = 0; virtual void func2(int i) = 0; virtual void func3(int i) = 0; };
|
工程上多继承的使用说明:
- a) 多继承已经被实际开发经验所抛弃
- b) 工程开发中真正意义上的多继承是几乎不被使用的
- c) 多继承带来的代码复杂性远多于其带来的便利
- d) 多继承对代码维护性上的影响是灾难性的
- e) 在设计方法上,任何多继承都可以使用单继承代替
- f) 在多继承中,使用虚继承不能完全解决二义性的问题
- 虚继承的使用与适用场景介绍
- 虚继承只适用于有共同基类(公共基类)的多继承场景(钻石菱形 ◇),如右图所示
- 对于
V
字形的多继承场景,虚继承是没办法解决二义性问题的,如右图所示
工程上继承多个接口类的使用说明:
- a) 继承多个接口类不会带来二义性和复杂性等问题
- b) 多继承可以通过精心设计的单继承和接口类来代替
- c) 接口类只是一个功能说明,而不是功能实现,子类需要根据功能说明定义功能实现