Skip to content

Latest commit

 

History

History
593 lines (491 loc) · 15.3 KB

l_14.md

File metadata and controls

593 lines (491 loc) · 15.3 KB

Lesson14 类:动态(继承)多态 Dynamic Polymorphism



1. 多态的基本概念

概念引入

class Base {
 public:
   void function() {
     std::cout << "Base" << std::endl;
   }
};

class Derived : public Base {
 public:
   void function() {
     std::cout << "Derived" << std::endl;
   }
};

int main() {
    Derived derived;
    Base* base = &derived;
    base->function();
}

当我们执行上面的代码时,程序会优先调用基类中的function

output

Base

所以我们得出第一条结论:基类指针(或引用)只能调用基类的成员函数,不能调用派生类的成员

当我们在基类成员函数前添加virtual关键字时,该函数变为虚函数;此时我们再让基类指针指向派生类对象

class Base {
  public:
   virtual void function() {
      std::cout << "Base" << std::endl;
   }
};

class Derived : public Base {
  public:
   void function() {
      std::cout << "Derived" << std::endl;
   }
};

int main() {
   Derived derived;
   Base* base = &derived;
   base->function();
}

此时,程序会调用派生类的function output

Derived

所以我们得出第二条结论,基类指针指向派生类对象时,可以调用派生类中与基类虚函数特征相同的成员函数

当更多派生类继承基类时,我们用同样的方法处理基类

class Base {
  public:
   virtual void function() {
      std::cout << "Base" << std::endl;
   }
};

class Derived1 : public Base {
  public:
   void function() {
      std::cout << "Derived1" << std::endl;
   }
};

class Derived2 : public Base {
  public:
   void function() {
      std::cout << "Derived2" << std::endl;
   }
};

class Derived3 : public Base {
  public:
   void function() {
      std::cout << "Derived3" << std::endl;
   }
};

int main() {
   Derived1 derived1;
   Base* base1 = &derived1;
   base1->function();

   Derived2 derived2;
   Base* base2 = &derived2;
   base2->function();

   Derived3 derived3;
   Base& base3 = derived3;
   base3.function();
}

output

Derived1
Derived2
Derived3

我们发现,base 同样都为基类指针或引用,而通过该指针或引用调用同一个虚函数,在运行时根据指针指向或引用表现出多种形式

这样的现象,我们称为动态(继承)多态

综上,我们可以总结出多态形成的条件

  • 派生类重写 (overwrite/override) 基类虚函数
  • 基类指针(或引用)指向子类对象

多态的注意点

  1. 只需要在基类的函数声明中加上virtual关键字,函数定义时不能加
  2. 在派生类中重写虚函数时,函数特征要相同(返回类型,函数名,参数表)
  3. 当在基类中写了虚函数时,如果派生类没有重写该函数,那么将使用基类的虚函数
  4. 在派生类中重写了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符
  5. 如果要在派生类中重新写基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。

2. 多态的应用场景

下面,我们来通过一个案例深刻体验以下多态的优点

task:实现一个计算器类

使用一般写法

#include <iostream>

class Calculator {
  public:
   int getResult(std::string oper);
   void inputNum(int num1, int num2);

   int _num1;
   int _num2;
};

int Calculator::getResult(std::string oper) {
   int result;
   if (oper == "+") {
      result = _num1 + _num2;
   } else if (oper == "-") {
      result = _num1 - _num2;
   } else if (oper == "*") {
      result = _num1 * _num2;
   }
   return result;
}

void Calculator::inputNum(int num1, int num2) {
   _num1 = num1;
   _num2 = num2;
}

void test() {
   Calculator c;
   c.inputNum(10, 10);

   std::cout << c._num1 << "+" << c._num2 << "=" << c.getResult("+") << std::endl;
   std::cout << c._num1 << "-" << c._num2 << "=" << c.getResult("-") << std::endl;
   std::cout << c._num1 << "*" << c._num2 << "=" << c.getResult("*") << std::endl;
}

int main() {
   test();
}

output

10+10=20
10-10=0
10*10=100

写上述代码时我们不难发现,如果我们想要拓展新功能,需要修改源码 而真实开发中,我们提倡开闭原则,即对开放拓展,关闭修改


使用多态写法

#include <iostream>

class AbstractCalculator {
  public:
   virtual int getResult() { return 0; }

   void inputNum(int num1, int num2) {
      _num1 = num1;
      _num2 = num2;
   }

   int _num1;
   int _num2;
};

class AddCalculator : public AbstractCalculator {
  public:
   int getResult() { return _num1 + _num2; }
};

class SubCalculator : public AbstractCalculator {
  public:
   int getResult() { return _num1 - _num2; }
};

class MulCalculator : public AbstractCalculator {
  public:
   int getResult() { return _num1 * _num2; }
};

void test() {
   AbstractCalculator* cal = new AddCalculator;
   cal->inputNum(10, 10);
   std::cout << cal->_num1 << "+" << cal->_num2 << "=" << cal->getResult() << std::endl;
   delete cal;

   cal = new SubCalculator;
   cal->inputNum(10, 10);
   std::cout << cal->_num1 << "-" << cal->_num2 << "=" << cal->getResult() << std::endl;
   delete cal;

   cal = new MulCalculator;
   cal->inputNum(10, 10);
   std::cout << cal->_num1 << "*" << cal->_num2 << "=" << cal->getResult() << std::endl;
   delete cal;
}

int main() {
   test();
}

output

10+10=20
10-10=0
10*10=100

从上述代码中,我们可以看到:多态的写法虽然代码量更大,但是结构更清晰,功能拓展更方便,可读性强以及更优雅(我加的


3. C++对象模型 (object model)

多态的实现原理

多态到底是如何实现的呢,我们以下面的代码做演示

class Base {
  public:
   virtual void function1() {
      std::cout << "Base" << std::endl;
   }
   virtual void function2() {
      std::cout << "Base" << std::endl;
   }
   virtual void function3() {
      std::cout << "Base" << std::endl;
   }
};

class Derived : public Base {
  public:
   void function1() {
      std::cout << "Derived" << std::endl;
   }
   virtual void function2() {
      std::cout << "Derived" << std::endl;
   }
   virtual void function3() {
      std::cout << "Derived" << std::endl;
   }
};
  • 使用vs提供的工具查看基类和派生类的内存结构

基类

class _s__RTTIBaseClassDescriptor       size(36):
        +---
 0      | pTypeDescriptor
 8      | numContainedBases
12      | _PMD where
24      | attributes
28      | pClassDescriptor
        +---

class _s__RTTIBaseClassArray    size(1):
        +---
 0      | arrayOfBaseClassDescriptors
        +---

class Base      size(8):
        +---
 0      | {vfptr}
        +---

Base::$vftable@:
        | &Base_meta
        |  0
 0      | &Base::function1
 1      | &Base::function2
 2      | &Base::function3

派生类

class Derived  size(8):
        +---
 0      | +--- (base class Base)
 0      | | {vfptr}
        | +---
        +---

Derived1::$vftable@:
        | &Derived1_meta
        |  0
 0      | &Derived::function1
 1      | &Derived::function2
 2      | &Derived::function3

vfptr: virtual function pointer 虚函数(表)指针
由编译器在构造函数中被初始化,指针指向内存中的虚函数表

vftable: virtual function table 虚函数表
由编译器自动构建,表内记录每个虚函数的地址
派生类会继承基类的虚函数表

为什么一定是基类指针或引用指向派生类对象

Base* base1 = &derived;
Base base2 = derived;

上述代码中,仅 base1 会发生多态,而 base2 不会发生,这是为什么呢?

摘自《深度探索C++对象模型》
一个pointer或一个reference之所以支持多态,是因为 它们并不引发内存任何“与类型有关的内存委托操作” ; 会受到改变的,只有 它们所指向内存的大小和解释方式 而已。
编译器在 (1)初始化(2)指定 (assignment) 间做出了仲裁。编译器必须确保如果某个object含有一个或一个以上的vfptr,那些 vfptr的内容不会被base class object初始化或改变

对这段话的解读就是:

  • 指针和引用类型只是要求了基地址和这种指针所指对象的内存大小,与对象的类型无关,相当于把指向的内存解释成指针或引用的类型
  • 而把一个派生类对象直接赋值给基类对象,就牵扯到对象的类型问题,为了保证基类对象的vftable不被改变,基类的vfptr不会替换派生类的vfptr,即赋值后基类对象中的内容仍为基类本身,从而无法实现多态。

补充释义:
静态类型:声明的类型,编译期确定。如 *base1base2 的静态类型都是Base
动态类型:在内存中实际的类型。*base1 的动态类型是 Derived,发生动态绑定;而base2的动态类型仍是Base,发生类型转换。
动态绑定:即静态类型与动态类型不同,改变指针指向时实际改变的是内存的解释方式修改静态类型。

多态是如何发生的(虚机制)

  1. 继承时,派生类先调用基类构造函数,再调用自身构造函数,继承得到的vfptr被修改,指向派生类的vftable
  2. 当基类指针或引用指向派生类时,派生类的内存被指定 (assignment) 为基类,发生动态绑定;此时基类指针指向的内存中,vfptr指向派生类vftable
  3. 此时调用的函数即为派生类重写的虚函数

C++对象模型

现在,我们可以将C++对象模型完善了

class A{
 public:
   int _x;
   int _y;
   static int _s;

   void A_function();
   virtual void A_virtual_function();
   static void static_function();
};

virtual的单个对象A模型

类型记录(空类时占位) 成员变量(连续存储) 虚函数表指针 虚函数表 虚函数
type_info for A int A::_x
int A::_y
A_vfptr → > &A::A_virtual_function > A::A_virtual_function
静态成员变量(不连续存储)
static int A::_s
静态成员函数(不连续存储)
static void A::static_function
成员函数(不连续存储)
void A::A_function()

继承/多态的对象B模型

class B : public A {
 public:
   int B_a;
   void B_function();
   void virtual_function();
   virtual void B_virtual_function();
}
类型记录(空类时占位) 成员变量(连续存储) 虚函数表指针 虚函数表 虚函数
type_info for B int A::_x
int A::_y
int B::_a
B_vfptr → > &B::A_virtual_function
&B::B_virtual_function
>
>
B::A_virtual_function
B::B_virtual_function
静态成员变量(不连续存储)
static int A::_s
静态成员函数(不连续存储)
static void A::static_function
成员函数 (不连续存储)
void A::A_function()
void B::B_function()
  • 落灰书目推荐(我正在痛苦阅读中):《深度探索C++对象模型》

4. 纯虚函数和抽象类

纯虚函数

纯虚函数是一种特殊的虚函数,当不需要基类虚函数的实现时,可以将虚函数改为纯虚函数

语法

virtual ret_type function(param) = 0;

示例

class Base {
  public:
   virtual void function() = 0;
};

class Derived1 : public Base {
  public:
   void function() {
      std::cout << "Derived1" << std::endl;
   }
};

class Derived2 : public Base {
  public:
   void function() {
      std::cout << "Derived2" << std::endl;
   }
};

class Derived3 : public Base {
  public:
   void function() {
      std::cout << "Derived3" << std::endl;
   }
};

int main() {
   Derived1 derived1;
   Base* base1 = &derived1;
   base1->function();

   Derived2 derived2;
   Base* base2 = &derived2;
   base2->function();

   Derived3 derived3;
   Base& base3 = derived3;
   base3.function();
}

当类中出现纯虚函数时,该类变为抽象类,不能实例化对象,可以创建指针和引用
派生类必须重写抽象类的纯虚函数,否则也属于抽象类


虚析构和纯虚析构

  • 虚析构

发生多态时,如果需要通过基类指针析构派生类,由于析构函数无法继承,所以靠基类指针是做不到的

class Base {
  public:
    Base() {
      std::cout << "Base constructor" << std::endl;
    }
    virtual void function() = 0;
    ~Base() {
      std::cout << "Base destructor" << std::endl;
    }
};

class Derived : public Base {
  public:
   Derived() {
      std::cout << "Derived constructor" << std::endl;
      p = new int(10);
   }
   void function() {
      std::cout << "Derived" << *p << std::endl;
   }
   ~Derived() {
      std::cout << "Derived destructor" << std::endl;
      delete p;
      p = nullptr;
   }

   int* p;
};

int main() {
   Base* base = new Derived;
   base->function();
   delete base;
   base = nullptr;
}

output

Base constructor
Derived constructor
Derived10
Base destructor

基类指针在析构时不会调用派生类析构函数,导致派生类析构函数无法被执行,造成内存泄露

解决方法是将基类析构函数改为虚析构

virtual ~Base() {
  std::cout << "Base destructor" << std::endl;
}

output

Base constructor
Derived constructor
Derived10
Derived destructor
Base destructor
  • 纯虚析构
virtual ~Base() = 0

纯虚析构必须有定义

// 类外
Base::~Base() {
  std::cout << "Base destructor" << std::endl;
}

一个类声明了纯虚析构后,该类也变成抽象类,无法实例化对象;也就是说,纯虚析构相比于虚析构仅仅是用来将类变为抽象类的


edit Serein