当前位置: 首页 > news >正文

C++ 中的默认构造函数:非必要,不提供

《More Effective C++:35个改善编程与设计的有效方法》
读书笔记:非必要不提供default constructor

在这里插入图片描述

在 C++ 中,默认构造函数(即无需任何参数即可调用的构造函数)是对象“无中生有”的一种方式。它的核心作用是在没有外部信息输入时完成对象的初始化。但并非所有类都需要默认构造函数,“非必要不提供”才是更合理的设计原则。

一、默认构造函数的适用边界

默认构造函数的价值,在于“无外部信息时仍能合理初始化对象”。比如:

  • 数值类对象可初始化为 0 或无意义值;
  • 指针可初始化为 null;
  • 链表、哈希表等容器可初始化为空容器。

这些场景中,默认构造函数能确保对象处于“可用状态”。但更多场景下,对象的初始化依赖外部信息——比如模拟公司设备的类必须有唯一 ID,通信簿字段必须包含人名。这类对象若没有外部信息,根本无法完成“有意义的初始化”,此时强行提供默认构造函数反而会埋下隐患。

二、缺乏默认构造函数的“限制”与应对

若类不提供默认构造函数,使用时会面临一些限制,但这些限制并非无法解决,只是需要更谨慎的处理。

1. 数组初始化的挑战

C++ 中创建对象数组时,默认会调用元素的默认构造函数。因此,缺乏默认构造函数的类无法直接创建数组:

class EquipmentPiece {
public:EquipmentPiece(int IDNumber); // 必须传入ID,无默认构造函数
};EquipmentPiece pieces[10]; // 错误:无法调用构造函数

应对方法有三种:

  • 栈上显式初始化:仅适用于栈数组,通过初始化列表为每个元素传入参数:

    int ids[10] = {1,2,...,10};
    EquipmentPiece pieces[] = {EquipmentPiece(ids[0]),EquipmentPiece(ids[1]),...,EquipmentPiece(ids[9])
    };
    

    但此方法无法用于堆数组。

  • 指针数组:用指针数组替代对象数组,后续通过 new 为每个指针分配带参数的对象:

    using PEP = EquipmentPiece*;
    PEP pieces[10]; // 指针数组无需调用构造函数
    for (int i=0; i<10; i++) {pieces[i] = new EquipmentPiece(ids[i]);
    }
    

    缺点是需手动管理内存(避免泄漏),且额外占用指针的存储空间。

  • placement new:先分配原始内存,再通过 placement new 在指定内存上构造对象:

    // 分配足够存储10个对象的原始内存
    void* rawMem = operator new[](10 * sizeof(EquipmentPiece));
    EquipmentPiece* pieces = static_cast<EquipmentPiece*>(rawMem);// 逐个构造对象(需传入参数)
    for (int i=0; i<10; i++) {new (&pieces[i]) EquipmentPiece(ids[i]);
    }// 手动析构与释放内存(注意顺序)
    for (int i=9; i>=0; i--) {pieces[i].~EquipmentPiece();
    }
    operator delete[](rawMem);
    

    此方法节省内存,但实现复杂,维护成本高(需手动调用析构函数,且释放内存的方式特殊)。

2. 与模板容器的兼容性问题

部分模板容器(如早期设计的数组模板)会在内部创建元素类型的数组,此时要求元素类型必须有默认构造函数。例如:

template<class T>
class Array {
public:Array(int size) { data = new T[size]; } // 调用T的默认构造函数
private:T* data;
};

T 是缺乏默认构造函数的类(如 EquipmentPiece),模板实例化会失败。

不过,随着模板设计的成熟(如标准库 vector),许多现代模板已消除了对默认构造函数的依赖。这一问题的影响正在逐渐减弱。

3. 虚拟基类的协作成本

若类作为虚拟基类且缺乏默认构造函数,所有派生类(无论层级多深)都必须在构造函数中为其传递参数。这会增加派生类的设计负担,但本质上是“强制正确初始化”的合理约束。

三、强行添加默认构造函数的隐患

为了规避上述限制,有些开发者会给“本不需要默认构造函数”的类强行添加,比如给 EquipmentPiece 加一个带默认参数的构造函数:

class EquipmentPiece {
public:EquipmentPiece(int IDNumber = UNSPECIFIED); // 强行添加默认构造函数
private:static const int UNSPECIFIED = -1; // 无意义的“占位值”
};

这种做法看似解决了使用限制,实则埋下更深的问题:

1. 成员函数复杂化

强行添加的默认构造函数无法保证对象完全初始化(比如 IDNumber 可能为 UNSPECIFIED)。此时,类的所有成员函数都必须先检查“对象是否处于有效状态”,否则可能触发逻辑错误。例如,调用设备操作函数前需先验证 ID 有效性,这会让代码变得臃肿且易错。

2. 效率降低

额外的有效性检查会增加运行时开销(时间成本)和代码体积(空间成本)。若检查到无效状态,还需处理异常(如抛出异常、终止程序),进一步消耗资源。

3. 软件质量下降

“未完全初始化的对象”本质上是一种“不合法状态”。允许这种状态存在,会让类的行为变得不可预测,增加调试难度,最终降低软件的可靠性。

四、总结:坚守“非必要不提供”的原则

默认构造函数的核心价值是“在无外部信息时确保对象可合理初始化”。对于需要外部信息才能完成初始化的类(如依赖 ID 的设备、必须包含内容的通信簿),强行添加默认构造函数只会带来复杂性、低效率和不可靠性。

虽然缺乏默认构造函数会带来数组初始化、模板兼容等限制,但这些限制可以通过更严谨的代码(如指针数组、placement new)解决,且本质上是“确保对象正确初始化”的合理代价。

因此,设计类时应遵循:能通过默认构造函数完成合理初始化的类,才提供它;否则,坚决不提供。这是保证代码健壮性和效率的重要原则。

http://www.lryc.cn/news/595374.html

相关文章:

  • 苍穹外卖Day5
  • B树、B+树的区别及MySQL为何选择B+树
  • Git核心功能简要学习
  • GraphRAG快速入门和原理理解
  • 关于JVM
  • AXI接口学习
  • 上网行为管理-身份认证1
  • 剖析Sully.ai:革新医疗领域的AI助手功能启示
  • Hyperledger Fabric V2.5 生产环境部署及安装Java智能合约
  • 【OD机试】模拟数据序列号传输
  • 09_Spring Boot 整合 Freemarker 模板引擎的坑
  • 用简鹿视频格式转换器轻松制作 GIF 表情包教程
  • 牛客周赛 Round 101(题解的token计算, 76修地铁 ,76选数,76构造,qcjj寄快递,幂中幂plus)
  • 解决vscode中vue格式化后缩进太小的问题,并去除分号 - 设置Vetur tabSize从2到4,设置prettier取消分号semi
  • 元宇宙工厂漫游指南:VR可视化在设备巡检与远程运维中的沉浸式应用
  • zabbix企业级分布式监控
  • Java 实现 UDP 多发多收通信
  • C++unordered系列的map和set类(封装)
  • WAMP配置局域网https服务
  • C# 实现:动态规划解决 0/1 背包问题
  • Nacos 探活机制深度解析:临时 / 永久实例差异及与 Sentinel 的熔断协作
  • OpenAI API(1)补全Responses(Chat Completions)API和记忆Assistants API对比分析
  • Java 大视界 -- 基于 Java 的大数据分布式计算在地球物理勘探数据处理与地质结构建模中的应用(356)
  • 16 BTLO 蓝队靶场 Drill Down 解题记录
  • 前缀和题目:元素和小于等于阈值的正方形的最大边长
  • 计算机发展史:互联网时代的万物互联与全球变革
  • MySQL 17 如何正确地显示随机消息?
  • 【爬虫】06 - 自动化爬虫selenium
  • 元宇宙与游戏:虚实交融的数字文明新纪元
  • ni-app 对鸿蒙的支持现状