多态是通过绑定(标识符名称—>函数代码)实现的。

1、运行时多态/绑定 (晚绑定、动态多态性)

  • 虚函数
  • 指针
  • 引用

运行时多态与虚函数有关。基类的对象调用其虚函数,会调用到最新重载的子类的该函数。派生类的对象可以认为是基类的对象,但基类的对象不是其派生类的对象。因此,C++允许一个基类对象的指针指向其派生类对象,但不允许一个派生类对象指向其基类对象。在调用虚函数的过程中指针和引用会起到一定的作用。

2、编译时多态/绑定 (早绑定、静态多态性)

  • 函数重载

一、运算符重载

运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。

  • C++ 几乎可以重载全部的运算符,而且只能够重载C++中已经有的。
  • 不能重载的运算符:..*::?:
    • 重载之后运算符的优先级和结合性都不会改变。
  • 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。例如:
    • 使复数类的对象可以用“+”运算符实现加法;
    • 是时钟类对象可以用“++”运算符实现时间增加1秒。
  • new 和 delete 也是运算符 也可以重载
  • 不能重载基本类型的运算

重载的两种方式

  • 重载为类的非静态成员函数;
  • 重载为非成员函数。

1.1双目运算符重载为成员函数

  1. 返回类型 operator 运算符(形参)
  2. {
  3. ......
  4. }
  5. 参数个数=原操作数个数-1 (后置++、--除外)

双目运算符重载规则

  • 如果要重载 操作符B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型。
    • 如果oprd不是A类对象,那么就要用非成员函数实现重载
  • 经重载后,表达式 oprd1 B oprd2 相当于 oprd1.operatorB(oprd2)

1.2单目运算符重载为成员函数

  1. //前置运算符
  2. 返回类型 operator 运算符()
  3. {
  4. ......
  5. }
  6. //后置运算符
  7. 返回类型 operator 运算符(int
  8. {
  9. ......
  10. }

前置单目运算符重载规则

  • 如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。
  • 经重载后,表达式 U oprd 相当于 oprd.operator U()

后置单目运算符 ++和—重载规则

  • 如果要重载 或—为类成员函数,使之能够实现表达式 oprd 或 oprd— ,其中 oprd 为A类对象,则 ++或— 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
  • 经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
  1. //例8-2重载前置++和后置++为时钟类成员函数
  2. Clock & Clock::operator ++ () {
  3. second++;
  4. if (second >= 60) {
  5. second -= 60; minute++;
  6. if (minute >= 60) {
  7. minute -= 60; hour = (hour + 1) % 24;
  8. }
  9. }
  10. return *this;
  11. }
  12. Clock Clock::operator ++ (int) {
  13. //注意形参表中的整型参数
  14. Clock old = *this;
  15. ++(*this); //调用前置“++”运算符
  16. return old;
  17. }

1.3 运算符重载为非成员函数

成员函数情况下,左操作数必须是类的对象。如果左操作数不是(或者是别人设计好的我们没法重载),那就要用非成员函数重载。

类外普通全局函数

运算符重载为非成员函数的规则

  • 函数的形参代表依自左至右次序排列的各操作数。
  • 重载为非成员函数时
    • 参数个数=原操作数个数(后置++、—除外)
    • 至少应该有一个自定义类型的参数。
  • 后置单目运算符 ++和—的重载函数,形参列表中要增加一个int,但不必写形参名。
  • 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。
  • 必须至少有一个参数是自定义类型,否则就是修改了基本类型运算,是非法的

运算符重载为非成员函数的规则

  • 双目运算符 B重载后,

表达式oprd1 B oprd2等同于operator B(oprd1,oprd2 )

  • 前置单目运算符 B重载后,

表达式 B oprd等同于operator B(oprd )

  • 后置单目运算符 ++和—重载后,

表达式 oprd B等同于operator B(oprd,0 )

例8-3 重载Complex的加减法和“<<”运算符为非成员函数

• 将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数类的常引用。 • 将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用,用以支持下面形式的输出:

  1. cout << a << b;

该输出调用的是:

  1. operator << (operator << (cout, a), b);

源代码:

  1. class{
  2. friend Complex operator+(const Complex &c1, const Complex &c2);
  3. friend Complex operator-(const Complex &c1, const Complex &c2);
  4. friend ostream & operator<<(ostream &out, const Complex &c);
  5. };
  6. Complex operator+(const Complex &c1, const Complex &c2){
  7. return Complex(c1.real+c2.real, c1.imag+c2.imag);
  8. }
  9. Complex operator-(const Complex &c1, const Complex &c2){
  10. return Complex(c1.real-c2.real, c1.imag-c2.imag);
  11. }
  12. ostream & operator<<(ostream &out, const Complex &c){
  13. out << "(" << c.real << ", " << c.imag << ")";
  14. return out;
  15. }

二、虚函数

在静态绑定时候,编译器在编译时就要知道某个指针调用的函数具体是那一段代码。为了实现动态绑定,让这个函数根据传入的指针不同调用继承族谱中不同类的同名函数方法。我们就把基类中的此函数声明为虚函数。这样就会进行运行时绑定。等到运行时传入不同对象的指针,调用对象重载的同名函数。

所以虚函数本质上是为了解决继承族谱中同名函数重载问题的。

初识虚函数

  • 用virtual关键字说明的函数
  • 虚函数是实现运行时多态性基础,C++中的虚函数是动态绑定的函数
  • 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
  • 一般成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数

一般虚函数成员

  • 虚函数的声明
    virtual 函数类型 函数名(形参表);
  • 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
  • 在派生类中可以对基类中的成员函数进行覆盖。
  • 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。

virtual 关键字

  • 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
    • 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
    • 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
  • 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
  • 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
  • 总结:一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。

对比第7章例子 :
类的继承与派生

  1. #include <iostream>
  2. using namespace std;
  3. class Base1 { //基类Base1定义
  4. public:
  5. virtual void display()const;
  6. };
  7. class Base2: public Base1 { //公有派生类Base2定义
  8. public:
  9. virtual void display()const;
  10. };
  11. class Derived: public Base2 { //公有派生类Derived定义
  12. public:
  13. virtual void display()const;
  14. };
  15. void Base1::display() const {
  16. cout << "Base1::display()" << endl;
  17. }
  18. void Base2::display() const {
  19. cout << "Derived::display()" << endl;
  20. }
  21. void Derived::display() const {
  22. cout << "Base2::display()" << endl;
  23. }
  1. void fun(Base1 *ptr) { //参数为指向基类对象的指针
  2. ptr->display(); //"对象指针->成员名"
  3. }
  4. int main() { //主函数
  5. Base1 base1; //声明Base1类对象
  6. Base2 base2; //声明Base2类对象
  7. Derived derived; //声明Derived类对象
  8. fun(&base1); //用Base1对象的指针调用fun函数
  9. fun(&base2); //用Base2对象的指针调用fun函数
  10. fun(&derived); //用Derived对象的指针调用fun函数
  11. return 0;
  12. }

虚析构函数

为什么需要虚析构函数? - 可能通过基类指针删除派生类对象; - 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual ~Base();
  6. };
  7. class Derived: public Base{
  8. public:
  9. Derived();
  10. virtual ~Derived();
  11. int *p;
  12. };
  13. Base::~Base() {
  14. cout<< "Base destructor" << endl;
  15. }
  16. Derived::Derived(){ p = new int(22);}
  17. Derived::~Derived(){
  18. cout<<"Derived destructor"<<endl;
  19. delete p;
  20. }
  21. void func( Base *b){
  22. delete b;
  23. }
  24. int main(){
  25. Derived *bb = new Derived();
  26. func(bb);
  27. return 0;
  28. }
  29. Derived destructor
  30. Base destructor

虚表与动态绑定

虚表

每个多态类有一个虚表(virtual table)(指向虚函数入口地址的指针序列)
每个对象有一个指向当前类的虚表的指针(虚指针vptr),在64位机器上,指针占8个字节。所以即使是抽象类,也占空间。

动态绑定的实现

构造函数中为对象的虚指针赋值
通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
通过该入口地址调用虚函数

1235400306867703808.png

三、抽象类

规范整个类家族的统一对外接口

(类似protected访问权限中的函数,可以规范统一家族内部的接口

纯虚函数

纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:

virtual 函数类型 函数名(参数表) = 0;

  • 带有纯虚函数的类称为抽象类

抽象类

带有纯虚函数的类称为抽象类:

class 类名 { virtual 类型 函数名(参数表)=0; //其他成员…… }

抽象类作用

  • 抽象类为抽象和设计的目的而声明
  • 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
  • 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。

注意

  • 抽象类只能作为基类来使用。
  • 不能定义抽象类的对象。
    • 如果抽象类的派生类仍然没有定义纯虚函数,那么派生类也是抽象类,不能定义对象。

四、override与final

C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。 - 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明override的虚拟函数,有相同的函数签名(signature);若不存在,则会回报错误。

override

  • 基类声明虚函数,继承类声明一个函数覆盖该虚函数
  • 覆盖要求: 函数签名(signatture)完全一致
    • 函数签名包括:函数名 参数列表 const

final

C++11提供的final,用来避免类被继承,或是基类的函数被改写 例:

  1. struct Base1 final { };
  2. struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
  3. struct Base2 { virtual void f() final; };
  4. struct Derived2 : Base2 { void f(); // 编译错误:Base2::f 为final,不允许被覆盖 };

注意:override和final都不是关键字,只在特定位置起作用。也就是说可以作为一般性的标识符,但是不建议这么使用。

  1. #include "stdafx.h"
  2. #include <iostream>
  3. using namespace std;
  4. class Parent
  5. {
  6. public:
  7. virtual void Func();
  8. void Func_B();
  9. virtual void Func_C() final{ }
  10. };
  11. void Parent::Func()
  12. {
  13. cout<<"call the function of Parent"<<endl;
  14. }
  15. class Child : public Parent
  16. {
  17. public:
  18. void Func() override;//基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字
  19. /*
  20. void Func_A() override;
  21. 父类中没有此方法,添加override编译会报如下错错误:
  22. error C3668: “Child::Func_A”: 包含重写说明符“override”的方法没有重写任何基类方法
  23. */
  24. /*
  25. void Func_B() override { }
  26. Func_B在父类中不是虚函数,添加override编译会报如下错错误:
  27. error C3668: “Child::Func_B”: 包含重写说明符“override”的方法没有重写任何基类方法
  28. */
  29. /*
  30. void Func_C() override { }
  31. Func_C在父类中被final修饰,禁止在派生类中被重写
  32. error: Func_C在基类中声明禁止重写
  33. */
  34. };
  35. void Child::Func()
  36. {
  37. cout<<"implement the function of Parent"<<endl;
  38. }
  39. int _tmain(int argc, _TCHAR* argv[])
  40. {
  41. Parent objParent;
  42. Child objChild;
  43. return 0;
  44. }