c++-多态

多态介绍

买车票时,学生是半价,军人要优先......,在同一个售票窗口不同的人会享受到不同折扣,这是多态的一个现实例子。对于同一操作,有时候我们需要根据对象类型的不同产生不同行为,这就是多态。多态又分为静态多态(静态绑定)动态多态(动态多态)

  1. 静态多态典型的例子就是函数重载,参数列表不同调用到的重载函数也不同,结果也就不同。之所以叫静态多态,是因为在编译期间根据实参就已经确定了实际要调用的函数的地址。
  2. 动态多态是指在程序运行中程序才会确定要调用的函数地址。本文讨论多态类型的就是动态多态。

多态的实现

首先看一下多态,如下图,不同的对象作为实参调用同一个函数,表现出了不同的行为:

class person { public: virtual void sales() { cout << "正常买票" << endl; } private: }; class student :public person { public: virtual void sales() { cout << "买学生票,打5折" << endl; } private: }; class solider :public person { public: virtual void sales() { cout << "买军人票,打1折" << endl; } private: }; void func(person& x) { x.sales(); } int main() { solider x; student y; person z; func(x); func(y); func(z); }

接下来是实现多态的步骤:

  1. 在父类中找到想要呈现出多态的函数(图中是sales函数),用virtual修饰。被virtual修饰的函数叫做虚函数
  2. 在各个子类中重写父类的虚函数,要求函数名,返回值,参数必须相同子类中的virtual可加可不加,就算不加,子类重写的函数也是虚函数,但是父类中的virtual一定要加,表示这个函数将被重写。
  3. 用父类的引用或者指针接收不同子类对象(func函数的形参就是父类的引用)
  4. 使用父类的引用或者指针调用父类虚函数,这样根据引用对象的不同,调用到的函数实现也不同。

使用多态的注意事项

1.多态条件下在子类中重写函数后,重写的是函数的实现,可以看成重写后的函数的函数名,参数,返回值都用的是父类的。来看一个例子:

class person { public: virtual void sales(int x = 6) { cout << x << endl; } private: }; class student : public person { public: virtual void sales(int x = 5) { cout << x << endl; } private: }; int main() { student st; person& p = st; p.sales();//打印的是6而不是5,因为函数签名仍是用父类的,而父类的默认参数是6 }

2.如果虚函数声明和定义分离,在类外定义时不要加virtual,virtual只用于声明。

3. 没有虚静态成员函数

4.关于多态的原理,我相信看完这篇文章读者会深刻理解:

揭秘C++对象模型:虚表与内存布局-CSDN博客https://blog.csdn.net/2302_80221042/article/details/149544880?spm=1001.2014.3001.5501


虚函数重写的两个例外

协变(基类返回值与子类返回值不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,同样构成重写,这是c++对虚函数重写规则的例外放宽。下面是一个例子:
class Base { public: virtual Base* func() { cout << "virtual Base* func()" << endl; return this; } }; class A : public Base { public: virtual A* func() { cout << "virtual A* func()" << endl; return this; } }; int main() { A a; Base* p = &a; p->func(); }

结果是:

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,就与基类的析构函数构成重写,虽然基类与派生类析构函数名不相同。这看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。下面是一个例子:
class Base { public: virtual ~Base() { cout << " virtual ~Base()" << endl; } }; class A : public Base { public: virtual ~A() { cout << "virtual ~A()" << endl; } }; int main() { Base* p = new A; delete p; }

结果是:

上述过程就是,delete p调用的是A的析构函数,而A的析构函数调用结束后会默认调用Base的析构函数(这是继承体系中,析构函数的特性)。

在实现多态时最好把析构函数也声明为虚函数,因为如果析构函数不声明为虚函数,可能会导致内存泄漏(子类析构函数没有被调用):

class Base { public: ~Base() { cout << " ~Base()" << endl; } }; class A : public Base { public: ~A() { cout << " ~A()" << endl; } }; int main() { Base* p = new A; delete p;//由于没有把析构函数声明为虚函数,delet调用的只是Base类的析构函数 //,如果A类中有需要释放的资源,就会导致内存泄漏 }

结果为:


C++11 中的 override 和 final

final在父类中修饰虚函数,表示该虚函数不能再被重写:

class Base { public: virtual void func()final {} };

final也可以修饰整个类,表示该类不能被继承:

class Base final { public: virtual void func() {} };

override检查被修饰函数是否是基类某个虚函数的重写,如果不是则在编译的时候报错:

class Base { public: virtual void func() {} }; class A : public Base { public: virtual void func()override { } };

抽象类(接口类)

在虚函数的后面写上" = 0" ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写了所有纯虚函数之后,派生类才能实例化出对象

举个例子:

class Base//抽象类 { public: virtual void func() = 0;//纯虚函数 virtual void func2(){}; void func3() { //..... } }; class A : public Base { public: virtual void func()override { } }; int main() { Base base;//报错,抽象类不能实例化 A a;//正确,因为A类重写了Base中的所有纯虚虚函数 }

为什么会有抽象类:想象一下基类是人类,派生出中国人,小日子,英国佬等子类,说这个人是中国人很合理,但是总不能说这个人是人类吧,也就是说,有时候我们不需要甚至不允许用父类创建对象,因此就有了抽象类。