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

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

首先,C++ 构造函数可以是虚函数吗?

语法上来说,答案是不行的。类对象的创建依赖于类构造函数的执行,在构造函数执行之前虚函数指针还是空的,虚函数指针需要被初始化才能使用,所以构造函数不能为虚函数。笔者之前有篇文章对此也做过相关刨析,有兴趣可关注我然后搜索阅读《刨析一下C++构造析构函数能不能声明为虚函数的背后机理?》。

那么为什么要说 C++ 构造函数也可以是虚函数?

这要回到需求上来分析,假设正在设计一个画板,我们可以在上面添加编辑各种图形,比如方框、三角形、圆形等。

编辑自然包括复制粘贴等操作,复制的时候面对的是对象,由于类的多态特性,对象可能基于派生类实例化,访问是通过基类指针变量,所以不一定会知道当前对象的具体类型。那么如何拷贝对象?

常规拷贝对象

如果我们知道目标对象的类型,那么拷贝对象的常规做法是直接调用类的拷贝构造函数(copy contrutor),看例子

c 复制代码
#include <iostream>
using namespace std;

class Implementation
{
public:
    Implementation()
    {
        cout << "default constructor" << endl;
    }

    Implementation(const Implementation &other)
    {
        cout << "copy constructor" << endl;
    }

    Implementation& operator= (const Implementation &other)
    {
        cout << "operator=" << endl;
        return *this;
    }
};

int main()
{
    Implementation x;
    Implementation y = x;
    return 0;
}

欸,不是要演示拷贝构造函数的调用吗,为什么上面的 main 函数里用的是等号 = 表达式?

注意:创建并初始化对象时调用的等号 = 不会调用赋值操作符,虽然类中已实现了赋值操作符,由下面的输出结果来看,实际上是调用了拷贝构造函数。如何区分什么情况下等号 = 表达式才会调用赋值操作符的实现?请记住,拷贝构造函数用于初始化未存在的对象,赋值操作符用于替换已存在的对象的状态,两者区别的关键因素是对象是否已存在。而上面的例子中,由于等号 = 表达式是初始化对象,所以该对象是未存在的,理所当然就是调用了拷贝构造函数。

output:

go 复制代码
default constructor
copy constructor

动态克隆

如果我们面对对象时,正如开头的画板中,复制一个已存在的具体图形,但是不能确定其具体类型,又应该如何拷贝这些对象呢?

下面创建一些图形,基础图形特征用基类 BaseShape 表示,各种具体图形用 BaseShape 的派生类表示:

arduino 复制代码
class BaseShape
{
    // ...
};

class Square : public BaseShape
{
    // ...
};

class Rectangle : public BaseShape
{
    // ...
};

int main()
{
    BaseShape *s1 = new Square();
    BaseShape *s2 = new Rectangle();
    // ...

   return 0;
}

在上面这个例子中,创建具体的图形对象,指针分别存放在基类指针变量 s1 和 s2 中。

在后续的使用中,仅仅依靠基类对象指针,并且不清楚对象的创建类型,于是无法直接使用创建类型对应的拷贝构造函数复制对象。

但是接口仍然能被基类指针调用,是否可以通过对象能直接调用的接口,赋予接口一定的魔法,利用类的多态特性实现动态拷贝?

下面给基类 BaseShape 添加个接口(没有函数体实现的纯虚函数),为了凸显接口的意图---拷贝对象,特意命名为 Clone(),并在派生类中给出实现:

kotlin 复制代码
class BaseShape
{
public:
   // ...
   virtual BaseShape *Clone() = 0;
};

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

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

在派生类中的接口被重写时,可以直接调用各自的拷贝构造函数,使得复制对象又变得如此简单了,避免了语法上的限制。

如果你细心的话,会发现派生类对接口 Clone() 重写后返回值的类型与基类的声明不同。基类中返回接口 Clone() 的返回值是指向基类对象的指针,而各个派生类中接口 Clone() 重写后返回值分别是指向派生类对象的指针。这在 c++ 代码中是合法的,被称呼为协差(Covariance)。

所谓协差(Covariance),就是基类虚函数的返回值为指向对象的指针时,派生类重写该虚函数并且返回值同样为指向对象的指针,前后两个返回的指针指向的对象类型可以不同,但是要求后一个类型(派生类)指针可转换为前一个类型(基类)的指针,也就是向上转换(Upcasting)。

从上面的代码可见,接口 Clone() 做的事情和拷贝构造函数一样,都是拷贝对象,但接口 Clone() 属于虚函数,于是,这个接口 Clone() 也被称呼为 虚拷贝构造函数

触类旁通:虚构造函数

既然 虚拷贝构造函数 能实现,那么 虚构造函数 也应该可以实现。

虚函数里的 ,指的是动态调用。虚函数在各个派生类中被重写,编译器无法确认哪个被重写的版本会被何时何地调用,只有在运行时,依据对象内部的虚函数指针指向来调用对应的版本,动态的特性说的就是这么一回事。

那么可以基于输入参数选择性调用构造函数,不也是运行时才能做的确定吗?所以 虚构造函数 在目的意义上不要求必须是虚函数。

按照代码惯例,一般派生类的对象指针都用基类指针变量保存,所以,可以在基类中定义一个静态成员函数,输入参数决定构造哪个派生类的对象:

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

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

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

上面代码中的静态成员函数 Create() 就是我们心心念的 虚构造函数 了,内部通过参数选择地调用操作符 new 实例化对应的派生类对象,然后返回对象指针,返回的值仍然通过向上转换(Upcasting)为基类指针类型。

虽然 C++ 语法上不允许类拷贝构造函数或者构造函数声明为虚函数,但这不妨碍我们对目标追求的方法变通。

上面仅仅是经典的解决方案,还有现代版 C++ 的做法。欲知后事如何,不妨关注我!

相关推荐
jyan_敬言20 分钟前
【Linux】Linux命令与操作详解(一)文件管理(文件命令)、用户与用户组管理(创建、删除用户/组)
linux·运维·服务器·c语言·开发语言·汇编·c++
笑非不退34 分钟前
C++ 异步编程 并发编程技术
开发语言·c++
T0uken1 小时前
【QT Qucik】C++交互:接收QML信号
c++·qt·交互
爱写代码的刚子1 小时前
C++知识总结
java·开发语言·c++
s_little_monster2 小时前
【QT】QT入门
数据库·c++·经验分享·笔记·qt·学习·mfc
Yingye Zhu(HPXXZYY)2 小时前
洛谷 P11045 [蓝桥杯 2024 省 Java B] 最优分组
c++·蓝桥杯
三玖诶2 小时前
第一弹:C++ 的基本知识概述
开发语言·c++
木向3 小时前
leetcode42:接雨水
开发语言·c++·算法·leetcode
sukalot3 小时前
windows C++-创建基于代理的应用程序(下)
c++