先看一个简单的派生类
#include <iostream> using namespace std; class A { public: void func() { cout << "这是基类的func函数" << endl; } }; class B : public A { public: void func() { cout << "这是派生类里的func函数" << endl; } }; //派生类中重写基类函数,基类函数将被覆盖,要用::访问 int main() { A a; B b; a.func(); b.func(); b.A::func(); return 0; }
正如注释所说,派生类重写基类类方法,基类的累方法将会被覆盖
int main() { A * pa = new A; A * pb = new B; pa->func(); pb->func(); return 0; }
若将主函数中写成这样呢?
那应该是这样
为什么我创建的派生类用确实用基类的函数呢?为什么基类指针可以指向派生类呢?那么什么又是多态呢?
首先,C++中:将派生类指针或者引用转换为基类指针或者引用称为“向上强制转换”,这使得共有继承不需要进行显示类型的转换。派生类继承了基类中所有的数据,因此,对基类中所有的操作,都适用于派生类,而不必担心任何问题;相反的过程——将基类指针或者引用强制转换为派生类指针或者引用——称为向下强制转换。如果不使用显示类型转换,这种转换是不允许的,派生类可以新增数据成员,因此这些成员和数据不能用于基类
简单的来说,可以用基类的指针或者引用指向派生类,而不可以用派生类的指针或者引用指向基类
———————————————————————————————————————————
如果没有使用多态这种技术,编译器会根据指针或者引用的类型;来调用类方法;
如果使用了多态这种技术,编译器将会按照指针或者引用指向的对象来选择类方法;
因为两个指针都是基类的指针,所以调用基类的类方法
———————————————————————————————————————————
使用多态技术
在基类的类方法前面加上一个virtual关键字,表示这个函数是虚函数,在运行的时候确定该函数的地址,而不是编译阶段,这用到了动态联编的技术,我们放在下面说。
现在,我们看看使用多态后的程序
#include <iostream> using namespace std; class A { public: virtual void func() { cout << "这是基类的func函数" << endl; } }; class B : public A { public: //with C++ 11 or use override // void func() override //并且,这里的virtual可以不写,但最好还是用virtual或者override 修饰以下 virtual void func() { cout << "这是派生类里的func函数" << endl; } }; //派生类中重写基类函数,基类函数将被覆盖,要用::访问 int main() { A * pa = new A; A * pb = new B; pa->func(); pb->func(); return 0; }
因为使用virtual后,编译器将根据引用或者指针指向的对象选择类方法,其中一个指针指向基类,调用基类的类方法,另一个指针指向了派生类,将调用派生类的类方法,虽然他是基类的指针。
注意:不能用指向派生类的基类指针调用派生类中的成员和数据,但是可以调用基类的
虚函数的更多用法
不单单是正常的类方法可能用到虚函数,析构函数也会用到多态技术,称为虚析构函数
在使用虚析构函数之前我们先了解以下拥有继承关系的两个类中基类和子类的构造顺序
class A; class B :public A;
类似下面的,构造函数中和析构函数中我们给出了提示
A() { cout << "基类中的构造函数" << endl; }
创建一个B对象,最后的显示
可以看到,基类先构造,后析构,派生类后构造,先析构。
在基类中声明一个虚析构函数。这样做只为了确保释放派生对象时,按正确的顺序调用析构函数。
A * pb = new B; delete pb; //use ~A() or ~()B?
这时候就需要用虚析构函数来解决
#include <iostream> using namespace std; class A { public: int *pa; A() { pa = new int(10); cout << "基类中的构造函数" << endl; } ~A() { delete pa; cout << "基类中的析构函数" << endl; } }; class B : public A { public: int *pb; B() { pb = new int(25); cout << "派生类中的构造函数" << endl; } ~B() { delete pb; cout << "派生类中的析构函数" << endl; } }; void test() { A *pb = new B; delete pb; } int main() { test(); return 0; }
这样不行,用的是基类中的析构函数,pb没有被释放干净
#include <iostream> using namespace std; class A { public: int *pa; A() { pa = new int(10); cout << "基类中的构造函数" << endl; } virtual ~A() { delete pa; cout << "基类中的析构函数" << endl; } }; class B : public A { public: int *pb; B() { pb = new int(25); cout << "派生类中的构造函数" << endl; } ~B() { delete pb; cout << "派生类中的析构函数" << endl; } }; void test() { A *pb = new B; delete pb; } int main() { test(); return 0; }
这样就ok了
总结多态使用方法,基类中用virtual关键字声明,派生类中重写基类类方法,编译器会在运行的时候确定函数的地址,而不是编译的时候。
那么编译器是怎么做到的呢?
动态联编与虚函数表
《C++》Primer Plus 对于动态联编有着这样的解释
程序调用函数时,将使用哪个可执行代码块呢?编译器将负责这个问题。将源代码中的函数调用解释为执行特定函数的代码块被称为函数名联编。...... 在编译过程中进行的联编被称为静态联编,又称早期联编。然而,虚函数使这项工作变得困难。使用哪一个函数在编译时期是不确定的,因为编译器不知道用户选择了那种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法代码,这被称为动态联编,又叫做晚期联编。
虚函数的工作原理
C++规定了虚函数的行为,但将实现交给了编译器的作者,通常情况下
给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针(即指针数组指针)。这种数组称为虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象惊醒声明的虚函数的地址。派生类对象将包含一个指向指向独立地址表的指针。如果派生类进行了重定义,该虚函数表将保存新函数的地址,否则将保存原来函数的地址,如果派生类定义了新的虚函数,则该函数的地址也被添加到vtbl中。
class A { public: virtual void func1(); //address 1000 virtual void func2(); //address 1008 }; class B :public A { public: void func1();//address 2000 };
这里用类代替了对象,实际上是每个对象中都有这样的一张表,用类代表以下可以抽象出通性
两条经验规则:1.如果重定义基类中的类方法,应确保与原来的原型完全相同,如果返回的是基类引用或者指针,则可以修改为指向派生类的指着或者引用。这种特性被称为返回类型白协变,因为允许返回类型随类 类型的变化而变化
2.如果基类声明被重载了,则应在派生类中重定义基类中所有的重载版本
抽象基类
abstract base class 简称ABC
纯虚函数:在虚函数声明的基础上加上一个 = 0即可变为纯虚函数。
只要有纯虚函数的累就被称为纯虚基类,这个类不能实例化对象。
纯虚函数在类声明中不可定义函数,但是在实现方法文件中可以定义(即.cpp文件)
#include <iostream> using namespace std; class Base { protected: int a; int b; int c; public: Base(int _a, int _b, int _c) { a = _a; b = _b; c = _c; } virtual void show() = 0; }; class A :protected Base { private: int d; public: A(int _a=1, int _b=2, int _c=3,int _d=4) : Base(_a, _b, _c), d(_d) {} void show() override { cout << a << " " << b << " " << c << " " << d << endl; } }; int main() { //Base base; //error:Variable type 'Base' is an abstract class 变量类型'Base'是一个抽象类 A a; // OK a.show(); return 0; }
其中,保护权限下的数据不同于私有属性下,在派生类中可以直接访问基类中的保护数据。
对于外部来说,保护权限和私有权限是一样的,而对于内部来说,保护权限和私有权限的区别就在这里。
C++中的理念,包含纯虚函数的类只能用作基类,不能实例化对象,所以称纯虚基类,也可以说成抽象基类
ps:没有对象怎么办? new 一个! 笑死! 抽象的,new 不出来!!!!
ABC的代码很好写,但重要的是理解贯通ABC理念!
在处理继承问题上,ABC更具系统性,更加规范。设计ABC之前,首先应开发一个模型——指出变成问题所需的类以及他们之间的互相关系。
可将ABC看作是一种必须实现的接口。ABC要求派生类必须覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。这使得编写的代码更具有统一性,更加方便管理。