是不是看错了,C++ 构造函数也可以是虚函数?(下)

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/iteWy7TyO...

前面讲的经典解决方案里,无论是 虚拷贝构造函数 还是 虚构造函数 返回值都是对象指针,这要求接口调用方必须管理好返回的内存空间并且在不再需要时承担释放的责任。这无疑放飞了内存指针这只老虎,放虎归山后患无穷,随时都有内存泄漏的风险。

现代版 C++ 针对内存管理有着非常好的解决手段,比如智能指针,从 C++ 11 开始标准库提供了几种智能指针的实现,一般推荐使用 std::unique_ptr,适用于内存只有一个管理者的简单场景,除非有共享使用的情景。

为了契合内存安全的理念,现代 C++ 提倡使用智能指针替代裸指针,我们称其为现代化,裸指针就是 C 语言中的普通指针。

智能指针除了解决内存管理的问题,同时也是现代 C++ 为了提高性能而完善移动语义的重要应用例子,在笔者之前的文章中也有提到,感兴趣可点击阅读《现代 C++ 性能飞跃之:移动语义》。

欢迎关注我,让我有动力给你介绍更多更详细内容。

现代化版本

针对内存安全的需求,以上文的接口类 BaseShape 及其派生为例,虚拷贝构造函数虚构造函数 都改为现代版的风格实现,返回值变成 std::unique_ptr 类型

arduino 复制代码
class BaseShape
{
public:
    // ...
    static std::unique_ptr<BaseShape> Create(int id);
    virtual std::unique_ptr<BaseShape> Clone() = 0;
};

class Square : public BaseShape
{
public:
   // ...
	  std::unique_ptr<BaseShape> Clone() {
      return std::make_unique<Square>(*this);
	  }
};

class Rectangle : public BaseShape
{
public:
   // ...
	  std::unique_ptr<BaseShape> Clone() {
      return std::make_unique<Rectangle>(*this);
	  }
};

std::unique_ptr<BaseShape> BaseShape::Create(int id)
{
    switch (id) {
      case 1:
         return std::make_unique<Square>();
      case 2:
         return std::make_unique<Rectangle>();

      default:
         std::cout << "unkown id" << endl;
         return std::make_unique<Square>();
    }
}

从上面的代码中有没有发现,接口在派生类中被重写时,返回的类型和基类是一样的,都是 std::unique_ptr<BaseShape>

可见协变(covariance)已经不适用,由于返回的不是指针,而是模板类的实例对象,智能指针都属于模板类。

如果选择返回智能指针对象的指针呢?反而会和现代 C++ 提倡使用智能指针替代裸指针的内存安全理念相驳,开历史倒车是不行的。

细心的你还发现,接口 Clone() 的实现在返回智能指针对象时,实际创建的智能指针底层指针类型有所不同。如果是将派生类指针的智能指针返回给基类指针的智能指针,这是合法的,属于隐式转换。如果是将基类指针的智能指针返回给派生类指针的智能指针,就需要使用到 std::dynamic_pointer_cast 显式转换。

注意:std::make_unique 在 C++ 14 才被引入,而 std::unique_ptr 在 C++ 11 就已引入。如果你还在使用 C++ 11 版的编译器,恐怕需要自己补充实现函数模板 std::make_unique,网上也可以找到下面这块代码

arduino 复制代码
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

多继承

回头看看前面的所有例子,你会发现接口都是单类继承,也就是一个派生类只基于一个基类派生。如果派生自多个基类,虚拷贝构造函数虚构造函数 的实现有什么差异?

先来看看经典解决方案:

arduino 复制代码
class BaseShape
{
public:
   // ...
   static BaseShape *Create(int id);
   virtual BaseShape *Clone() = 0;
};

class BaseColor
{
public:
   // ...
   static BaseColor *Create(int id);
   virtual BaseColor *Clone() = 0;
};

class Square : public BaseShape,
               public BaseColor
{
public:
   // ...
   Square* Clone() {
      return new Square(*this);
   }
};

class Rectangle : public BaseShape,
                  public BaseColor
{
public:
   // ...
   Rectangle* Clone() {
      return new Rectangle(*this);
   }
};

BaseShape* BaseShape::Create(int id)
{
   switch (id) {
      case 1:
         return new Square;
      case 2:
         return new Rectangle;

      default:
         std::cout << "unkown id" << endl;
         return nullptr;
   }
}

BaseColor* BaseColor::Create(int id)
{
   switch (id) {
      case 1:
         return new Square;
      case 2:
         return new Rectangle;

      default:
         std::cout << "unkown id" << endl;
         return nullptr;
   }
}

同样基于协变(Covariance)的原因,虚拷贝构造函数 的派生类实现不依赖接口(也就是基类的定义)。从上面代码来看,这种接口返回裸指针的经典解决方案,虚拷贝构造函数虚构造函数 的实现在多继承或者单继承时基本一致,也就是通用性非常好。

多继承的情况下,如果换做现代版本呢?

由于 虚构造函数 是在基类中创建静态成员函数 Create(int id),不需要在派生类中重写,所以就算多继承的情况下也没什么变化,但每个基类 T 都需要实现各自的静态成员函数 std::unique_ptr<T> Create(int id),如上面代码所示。

至于 虚拷贝构造函数,基于前面的分析结论,协变(Covariance)不适用于其现代版本,也就是接口返回智能指针的做法在派生类的重写中会依赖基类接口定义,返回类型必须一样。

按照前面的思路,由于是多继承,假设再多一个基类 BaseColor,仍然在基类 T 中定义接口 Clone() 并在派生类中重写:

arduino 复制代码
class BaseShape
{
public:
   // ...
   virtual std::unique_ptr<BaseShape> Clone() = 0;
};

class BaseColor
{
public:
   // ...
   virtual std::unique_ptr<BaseColor> Clone() = 0;
};

class Square : public BaseShape,
               public BaseColor
{
public:
   // ...
   std::unique_ptr<BaseShape> Clone() {
      return std::make_unique<Square>(*this);
   }

   std::unique_ptr<BaseColor> Clone() {
      return std::make_unique<Square>(*this);
   }
};

class Rectangle : public BaseShape,
                  public BaseColor
{
public:
   // ...
   std::unique_ptr<BaseShape> Clone() {
      return std::make_unique<Rectangle>(*this);
   }

   std::unique_ptr<BaseColor> Clone() {
      return std::make_unique<Rectangle>(*this);
   }
};

上面这段代码是无法通过编译的,比如 std::unique_ptr<BaseShape> Square::Clone() 的返回类型与 std::unique_ptr<BaseColor> BaseColor::Clone() 的返回类型既不相同也不协变,也就是说无法绑定,所以编译器必然报错。

那么为了实现 虚拷贝构造函数 的现代化,应该怎么变通呢?

虚拷贝构造函数 在不同的基类中定义时取不同的接口名即可,如下

arduino 复制代码
class BaseShape
{
public:
   // ...
   virtual std::unique_ptr<BaseShape> BaseShapeClone() = 0;
};

class BaseColor
{
public:
   // ...
   virtual std::unique_ptr<BaseColor> BaseColorClone() = 0;
};

class Square : public BaseShape,
               public BaseColor
{
public:
   // ...
   std::unique_ptr<BaseShape> BaseShapeClone() {
      return std::make_unique<Square>(*this);
   }

   std::unique_ptr<BaseColor> BaseColorClone() {
      return std::make_unique<Square>(*this);
   }
};

class Rectangle : public BaseShape,
                  public BaseColor
{
public:
   // ...
   std::unique_ptr<BaseShape> BaseShapeClone() {
      return std::make_unique<Rectangle>(*this);
   }

   std::unique_ptr<BaseColor> BaseColorClone() {
      return std::make_unique<Rectangle>(*this);
   }
};

由于接口声明是在基类中,所以就算调用了不同的基类指针,仍然可以清楚应该调用哪个接口而不至于混肴。

c 复制代码
class Demo
{
public:
   Demo() {
      int input;
      cout << "Enter ID (1, 2): ";
      cin >> input;

      while ((input != 1) && (input != 2)) {
         cout << "Enter ID (1, 2 only): ";
         cin >> input;
      }

      // 通过 虚构造函数 创建对象
      pShape = BaseShape::Create(input);
      pColor = BaseColor::Create(input);
   }

   ~Demo() {}

   void Action() {
      // 通过 虚拷贝构造函数 动态拷贝对象本身
      auto pNewBase = pShape->BaseShapeClone();
      auto pNewColor = pColor->BaseColorClone();

      // do something
      // pNewBase->xxx
      // pNewColor->xxx
   }

private:
   std::unique_ptr<BaseShape> pShape;
   std::unique_ptr<BaseColor> pColor;
};

好了,看完上面的内容,如果你觉得有意思或者对你有帮助,请拉到文章底部点个赞和在看,这是我的动力,谢谢!

关于 虚拷贝构造函数虚构造函数 的概念就介绍到这里,相信你已经略懂一二,不信你来找我聊聊?往下拉,可扫码添加我为好友!

相关推荐
奶香臭豆腐31 分钟前
C++ —— 模板类具体化
开发语言·c++·学习
不想当程序猿_37 分钟前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
cdut_suye1 小时前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
捕鲸叉1 小时前
C++软件设计模式之外观(Facade)模式
c++·设计模式·外观模式
只做开心事2 小时前
C++之红黑树模拟实现
开发语言·c++
程序员老冯头3 小时前
第十五章 C++ 数组
开发语言·c++·算法
程序猿会指北4 小时前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发
无 证明8 小时前
new 分配空间;引用
数据结构·c++
别NULL13 小时前
机试题——疯长的草
数据结构·c++·算法