是不是看错了,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;
};

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

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

相关推荐
凌云行者34 分钟前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者38 分钟前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
~yY…s<#>1 小时前
【刷题17】最小栈、栈的压入弹出、逆波兰表达式
c语言·数据结构·c++·算法·leetcode
可均可可2 小时前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
白子寰2 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_012 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj2 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT2 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
-Even-2 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
我是谁??3 小时前
C/C++使用AddressSanitizer检测内存错误
c语言·c++