1.引子
在类的继承当中曾经出现过这样一种情况:B、C继承自A,D继承自B和C。
之前提到过,这种情况下,关于类A当中的内容,会被复制成两份给到D,当进行访问的时候,需要指定C或者B,才能够定位到A当中的变量是来自哪里。就像下面这样。
代码表示:
class A { public: A(int a) : m_A(a) {} int m_A; }; class B : public A { public: B(int a) : A(a) {} }; class C : public A { public: C(int a) : A(a) {} }; class D : public B, public C { public: D(int a = 1) : B(a),C(a) {} }; int main() { D d; //std::cout << "size of D.m_A is " << d.m_A << endl; d.m_A++; std::cout << "size of D.B::m_A is " << d.B::m_A << endl; d.B::m_A++; // 输出1 std::cout << "size of D.C::m_A is " << d.C::m_A << endl; d.C::m_A++; // 因为与B中继承的A不是同一参数,因此++无效,输出1 std::cout << "size of D.A::m_A is " << d.A::m_A << endl; //依据类的构造顺序,决定直接访问A时指定的位置,这里B类先构造,所以默认是指向B当中的A,++后输出2 }
需要注意的是,可以直接使用d.A::m_A来访问子类当中的成员,这是依据类的构造顺序,决定直接访问A时指定的位置,这里B类先构造,所以默认是指向B当中的A。
2.虚继承
通常在上面的情况下,我们是不希望从A当中得到两个衍生变量的,只想得到其中的一个。那么虚继承就是用来解决这个问题的。
虚继承的写法是在常规继承的优先级前面,加上virtual关键字。
class B : virtual public A
B和C分别虚继承A,就可以使得后面所有同时继承B、C的子类当中,只会保留一份A的内容。
class A { public: A(int a) : m_A(a) {} int m_A; }; class B : virtual public A { public: B(int a) : A(a) {} }; class C : virtual public A { public: C(int a) : A(a) {} }; class D : public B, public C { public: D(int a = 1) : B(a),C(a),A(a) {} //注意,这里需要对A进行额外构造 }; int main() { D d; std::cout << "size of D.m_A is " << d.m_A << endl; d.m_A++; // 输出1 std::cout << "size of D.B::m_A is " << d.B::m_A << endl; d.B::m_A++; // 输出2 std::cout << "size of D.C::m_A is " << d.C::m_A << endl; d.C::m_A++; // 输出3 std::cout << "size of D.A::m_A is " << d.A::m_A << endl; // 输出4 }
因此在上面这个例子当中,每次的++都是对同一变量的操作,因此会分别输出1、2、3、4.
注意:一旦使用了虚继承,那么必须在后面的子类当中,都对虚继承的基类进行构造。
3.虚函数
virtual用以修饰继承,就是虚继承。如果用来修饰函数,那么就是虚函数,它的基本格式如下。
class test{ virtual void Function() };
虽然都使用了virtual关键字,但它们解决的根本问题并不一样。
虚函数最主要的作用是C++多态性的表现,即同一事件(函数),发生在不同对象(类)当中,会呈现不同的特性。和函数重载类似,只不过多态性是在不同类当中的相同函数,而实现的方法,则是使用类指针。
3.1.类指针
如果在基类和派生类当中,都存在一个名称和参数完全相同的函数,在派生类当中就会确实地存在两个相同的函数。派生类直接调用就是自己的函数,指定基类就可以调用基类的函数,然而这样一种用法并不方便,于是我们会使用类的指针。
通常,对于引用和指针来说,必须要求参数类型完全一样。
int a; double* p_d = &a; //错误,不允许不同类型的转换 char& c = a; //错误,不允许不同类型的转换
对于类来说,基类和派生类的指针和引用,是可以互相转换的。
将派生类引用或指针转换为基类引用或指针被称为向上转换。
class Base{ }; class Drived : public Base{ }; Drived d; Base * p_b = &d; Base & r_b = d;
将基类引用或指针转换为派生类引用或指针被称为向下转换。但是向下转换必须使用显式的转换。
class Base{ }; class Drived : public Base{ }; Base d; Drived * p_d = (Drived*)&b; Drived & r_d = (Drived&)b;
那么相互转换的意义在哪里呢?向下转换意义不是很大,但通过向上转换,即基类指针指向派生类对象,可以调用让基类指针对象调用派生类当中的函数,这个过程也被叫动态绑定。
3.2.静态绑定和动态绑定
程序执行时,调用哪段函数块的过程,被称为函数的绑定。
在C语言当中比较简单,全部都会在编译时完成,这就被称为静态绑定。
对于C++来说,因为有重载和虚函数的存在,想要找到对应的函数块就没那么简单,而有时候则会在程序运行时,根据场景选择不同的函数块,这就被称为动态绑定。
class Base { public: virtual void Fun1(void) { std::cout << "Base::Fun1..." << endl; } void Fun2(void) { std::cout << "Base::Fun2..." << endl; } }; class Derived :public Base { public: void Fun1(void) { std::cout << "Derived::Fun1..." << endl; } void Fun2(void) { std::cout << "Derived::Fun2..." << endl; } }; int main() { Base* p_b; Derived d; p_b = &d; p_b->Fun1(); // 基类指针指向派生类,虚函数调用派生类 p_b->Fun2(); // 基类指针指向派生类,函数调用基类 return 0; }
在上面的例子当中,Fun1在基类当中是虚函数,Fun2不是,因此在使用基类指针指向派生类对象方法的调用中,调用的是派生类的Fun1和基类的Fun2.
3.3.纯虚函数和抽象类
不是所有时候,基类的函数都需要实现。
例如这种情况,对于不同的图形,定义一个基类shape,其中包含了一个函数draw用以绘制图形。然而在不确定图形是什么的情况下,draw没有任何意义。即一个函数在基类当中没有意义,但是在派生类当中存在意义,那么这样的基类函数就可以被定义为纯虚函数。
标准写法是,在函数后面写上'= 0 ',并且不给出函数实现:
class test{ virtual void Fun() = 0; };
则上面这个例子用代码可以这样实现:
class Shape { public: virtual void Draw(void) = 0; }; class Circle :public Shape { public: void Draw(void) { std::cout << "Draw::Circle..." << endl; } }; class Square :public Shape { public: void Draw(void) { std::cout << "Draw::square..." << endl; } }; int main() { Shape* p_s; //Shape s; //纯虚函数不允许直接作为对象 Circle c; Square sq; p_s = &c; p_s->Draw(); //绘制圆形 p_s = &sq; p_s->Draw(); //绘制正方形 return 0; }
4.虚函数想要解决的问题
费了这么大劲了,虚函数到底是想要干什么?主要目的是为了代码后续的维护性。
在C语言当中,库文件都是依据编译好的,使用时需要包含这些库文件后,再将这些文件添加到程序内容当中。我们修改的部分是程序的框架,而里面的模块即库文件是写好的。
虚函数带来这样一种用法,我们可以先将程序的框架搭好,然后再填充库文件或配置文件,这样更加有利于模块化的开发过程,每个小组或成员只需要负责自己模块的那部分内容。
例如:使用Process类作为基类,run虚函数作为方法,在主函数当中读取配置文件,遍历调度表当中的类模块,并执行它们。这样只需要更改配置文件和各个模块,就可以实现程序的更改。
5.虚函数的注意事项
5.1.构造函数不能是虚函数
尽管声明的是基类指针,但是我们首先必须要有一个派生类的对象,这个对象在构造的时候,会自动调用基类的构造函数,因此将基类的构造函数声明为虚函数没有任何意义。
5.2.析构函数必须是虚函数
除非当前类不用作基类,否则析构函数尽量声明为虚函数。在删除基类指针时,如果基类的虚构函数不是虚函数,那么就无法调用到派生类的析构函数,可能会造成内存风险。
5.3.友元不能是虚函数
因为友元函数不是类成员,所以友元函数不能是虚函数。
5.4.不能重定义
如果基类的虚函数,在派生类当中没有重新定义,那么还是会使用基类当中的该函数。