C++ 构造函数和析构函数示例详解

前言


构造函数和析构函数是 C++ 对象生命周期的两个端点。构造函数负责把一片原始内存变成有效对象,析构函数负责在对象消失前释放它持有的资源。理解这两端,才能理解 RAII 为什么是 C++ 资源管理的基石。


常见误解有几个。第一,以为构造函数「分配对象内存」——内存是new、栈帧或容器分配的,构造函数只是在这片内存上建立对象。第二,以为成员按初始化列表的书写顺序构造——实际顺序由成员声明顺序决定,初始化列表顺序写反了编译器不会报错,却可能读到未初始化的值。第三,以为构造函数抛异常时析构函数会被调用——不会,只有已经构造完成的基类和成员会被销毁。


本文从构造顺序讲起,接着讲析构的调用时机与异常约束,再梳理特殊成员函数的生成规则,最后给出一个完整的 RAII 类型示例。示例以 C++17 为基准,代码未经编译或运行验证。


一、构造:初始化列表与成员顺序


一个对象构造时,执行顺序是固定的:



  1. 基类子对象按继承列表顺序构造;

  2. 非静态数据成员按类中声明顺序构造;

  3. 执行构造函数体。


初始化列表只是给前两步提供参数,它不改变顺序。看下面这个例子:


// 适用于 C++17
class Config {
public:
Config(int limit, int step)
: step_(step), limit_(limit) {} // 书写顺序与声明顺序相反

private:
int limit_; // 先构造
int step_; // 后构造
};

这里两个成员都是int,谁先谁后看不出差别。但如果step_的初始化依赖limit_,顺序就致命了:


// 适用于 C++17
class Bad {
public:
Bad(int n) : limit_(n), step_(limit_ * 2) {} // 错误:limit_ 尚未初始化
private:
int step_; // 声明在前,先初始化
int limit_;
};

step_先初始化,此时limit_还是未初始化的值,读取它是未定义行为。编译器通常能用-Wreorder或类似警告提示初始化列表顺序与声明顺序不一致,应当把警告打开并清零。


初始化列表也不是唯一手段。C++11 起可以使用默认成员初始化器(default member initializer),在声明处给出默认值;如果构造函数没有覆盖,就使用默认值。C++11 还引入了委托构造(delegating constructor),一个构造函数可以调用同类另一个构造函数,避免重复代码。


// 适用于 C++17
#include <string>
#include <utility>

class Connection {
public:
Connection() : Connection("127.0.0.1", 8080) {} // 委托构造
Connection(std::string host, int port)
: host_(std::move(host)), port_(port) {}
private:
std::string host_ = "localhost";
int port_ = 0;
};

explicit用在单参数构造函数上,阻止隐式转换。Connection c = "127.0.0.1";这种写法如果没有explicit会意外通过,加上之后必须显式构造。转换构造函数、单参数构造函数默认都应考虑explicit,除非你确实想要隐式转换。


构造函数的成员初始化还有几个边界:



  • const成员和引用成员必须在初始化列表中初始化,不能在函数体里赋值。

  • 成员初始化可以用圆括号,C++11 起也可以用花括号;花括号会拒绝窄化转换,例如double到int。

  • 初始化列表中的表达式在进入构造函数体之前求值,此时对象尚未完全构造,不要在里面调用可能依赖完整对象的虚函数。


二、析构:逆序销毁与异常边界


析构顺序与构造严格相反:先执行析构函数体,再按声明逆序销毁成员,最后销毁基类子对象。这保证了「后构造的先销毁」,依赖关系不会颠倒。


析构函数在以下情况被调用:



  • 自动存储期对象离开作用域;

  • delete一个对象;

  • 临时对象所在完整表达式结束;

  • 容器销毁时销毁其元素;

  • 异常传播时销毁已构造的局部对象(栈展开)。


构造函数抛出异常时,析构函数不会被调用,因为对象从未完整构造。但已经构造完成的成员和基类子对象会被销毁,这就是为什么资源应当交给成员对象管理,而不是在构造函数体里手动申请再指望析构函数兜底。


析构函数有两个重要约束:


第一,多态基类的析构函数必须是virtual。通过基类指针delete派生类对象时,如果基类析构不是虚函数,行为是未定义行为,通常表现为派生类析构不执行、资源泄漏。


第二,析构函数不应抛出异常。C++11 起,析构函数默认是noexcept(true),除非成员或基类的析构可能抛出。异常从析构函数逸出会触发std::terminate。如果某个清理动作可能失败,应当提供单独的close()或flush()接口,让调用方显式处理错误,而不是把错误留在析构里。


// 适用于 C++17
class Base {
public:
virtual ~Base() = default; // 多态基类必须有虚析构
virtual void run() = 0;
};

class Derived : public Base {
public:
~Derived() override = default;
void run() override {}
};

析构函数里也不要调用虚函数并期待派生类版本。析构进行到基类部分时,派生类部分已经销毁,虚调用只会派发到当前层次。


三、特殊成员函数:规则五与规则零


编译器会在需要时隐式生成六个特殊成员函数:默认构造、拷贝构造、拷贝赋值、移动构造、移动赋值、析构。它们的生成规则互相牵连,简化成一句话:只要用户声明了析构、拷贝或移动中的任何一个,就应当显式考虑其余几个,这就是「规则五」(rule of five)。




特殊成员何时需要手写不写的后果



默认构造有其它构造函数但仍需默认构造被其它构造函数抑制

析构持有需要释放的资源资源泄漏

拷贝构造需要深拷贝浅拷贝导致重复释放

拷贝赋值需要深拷贝且处理自赋值同上,且可能自赋值出错

移动构造需要转移资源所有权退化为拷贝,或根本不被生成

移动赋值同上同上



更推荐的做法是「规则零」(rule of zero):用std::string、std::vector、std::unique_ptr这类已经正确管理资源的成员,自己不写任何特殊成员函数,让编译器生成正确的版本。手写new/delete只应出现在实现底层容器或与 C 接口打交道的少数场合。


如果确实要手写,移动操作应标noexcept。标准容器在扩容时,只有当移动构造是noexcept(或不可拷贝)时才倾向移动元素;否则为了强异常保证可能选择拷贝。这不是绝对规则,但标上noexcept能让容器做出更有利的选择。


四、完整示例:一个遵守规则五的缓冲区


下面的Buffer用裸数组演示深拷贝与移动。它的目的是展示五个函数如何配合,真实项目应优先用std::vector<unsigned char>或std::unique_ptr<unsigned char[]>。


// 适用于 C++17
#include <algorithm>
#include <cstddef>
#include <utility>

class Buffer {
public:
explicit Buffer(std::size_t size)
: size_(size), data_(size ? new unsigned char[size]() : nullptr) {}

~Buffer() {
delete[] data_;
}

Buffer(const Buffer& other)
: size_(other.size_),
data_(other.size_ ? new unsigned char[other.size_] : nullptr) {
if (size_ != 0) {
std::copy(other.data_, other.data_ + size_, data_);
}
}

Buffer& operator=(const Buffer& other) {
if (this != &other) {
Buffer tmp(other); // 先完成可能抛异常的拷贝
swap(tmp); // 再交换,异常安全
}
return *this;
}

Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}

Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}

void swap(Buffer& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}

std::size_t size() const { return size_; }
unsigned char* data() { return data_; }
const unsigned char* data() const { return data_; }

private:
std::size_t size_;
unsigned char* data_;
};

几个关键点:



  • 拷贝构造先按other.size_分配,再复制。size_为 0 时data_为nullptr,不做std::copy,避免对空指针做指针算术。

  • 拷贝赋值采用「拷贝并交换」:先构造临时对象,成功后再交换。如果分配失败,原对象保持不变,满足强异常保证。自赋值检查也保留了。

  • 移动构造把源对象的指针置空、大小置零,源对象进入「有效但未指定」状态,之后只能销毁或重新赋值。不要读被移动对象的内容。

  • 移动赋值先释放自己的资源,再接管对方的资源,同样处理了自赋值。

  • 所有移动操作标noexcept,这是容器愿意使用移动的前提。

  • 析构函数只做delete[],不抛异常。


如果改用std::vector<unsigned char>,这五个函数一个都不用写:


// 适用于 C++17
#include <vector>

class Buffer2 {
public:
explicit Buffer2(std::size_t size) : data_(size) {}
std::size_t size() const { return data_.size(); }
private:
std::vector<unsigned char> data_;
};

拷贝、移动、析构全部由std::vector正确实现,这就是规则零带来的简洁性。


常见坑点



  1. ❌ 初始化列表顺序与成员声明顺序不一致,且成员之间存在依赖。


✅ 按声明顺序书写初始化列表;打开-Wreorder类警告并清零。



  1. ❌ 在构造函数体里给const成员或引用成员赋值。


✅ 这两类成员必须在初始化列表中初始化,函数体里赋值无法通过编译。



  1. ❌ 以为构造函数抛异常时析构函数会执行,从而在析构里释放构造中途申请的资源。


✅ 用成员对象管理资源;已构造的成员会自动销毁,未构造完成的对象不会调用析构。



  1. ❌ 多态基类析构不写virtual,通过基类指针delete。


✅ 基类析构声明为virtual,派生类用override标注。



  1. ❌ 在析构函数里抛出异常。


✅ 析构默认noexcept,异常逸出会std::terminate;把可能失败的清理放到显式接口。



  1. ❌ 手写拷贝构造只复制指针,导致两个对象持有同一块内存。


✅ 深拷贝,或使用std::vector/std::unique_ptr等 RAII 成员实现规则零。



  1. ❌ 移动构造不把源对象置为可析构状态,源对象析构时重复释放。


✅ 转移指针后把源指针置空、大小置零;被移动对象保持「有效但未指定」。



  1. ❌ 声明了析构函数,却仍指望编译器生成移动构造。


✅ 用户声明析构会抑制移动操作的隐式生成;需要移动就显式= default或手写。


总结




阶段顺序 / 规则关键提醒



构造基类 → 成员(按声明顺序)→ 函数体初始化列表顺序不决定构造顺序

析构函数体 → 成员逆序 → 基类多态基类必须虚析构

拷贝深拷贝资源注意自赋值与异常安全

移动转移资源并置空源对象标noexcept,不读被移动对象

规则零用 RAII 成员,不手写特殊成员现代 C++ 的首选



构造函数建立不变式,析构函数释放资源,两者之间的所有权属转移都要说清楚。把资源交给成员对象、把特殊成员函数交给编译器,绝大多数生命周期问题根本不会出现。




相关新闻

Flink实时广告点击量统计:黑名单过滤与窗口聚合实战

Flink实时广告点击量统计:黑名单过滤与窗口聚合实战

1. 从一条广告点击日志说起&#xff1a;这个统计任务到底在算什么广告点击量统计这件事&#xff0c;听起来像是数据平台里最基础的一环&#xff0c;但真正落到实时计算场景里&#xff0c;坑远比想象中多。我最早接触这个需求的时候&#xff0c;第一反应是“不就是按广告位分组计…

2026/10/20 3:00:51 阅读更多 →
Java泛型协变逆变与通配符:PECS原则和上下边界详解

Java泛型协变逆变与通配符:PECS原则和上下边界详解

1. 从一个编译报错说起&#xff1a;为什么你的泛型代码总是不通过写了几年Java的人&#xff0c;几乎都遇到过这样一种情况&#xff1a;代码逻辑明明没问题&#xff0c;IDE却给你标了一堆红&#xff0c;编译器报的错还特别抽象&#xff0c;比如"incompatible types: Listca…

2026/10/20 3:00:51 阅读更多 →
大模型V4编程能力升级实测:工程级协作与避坑指南

大模型V4编程能力升级实测:工程级协作与避坑指南

1. 大模型版本的编程能力升级&#xff0c;开发者为什么不该只当吃瓜群众春节假期刷手机&#xff0c;群里突然被一个新版本刷屏——某国产大模型V4正式发布&#xff0c;主打编程能力。往年这时候&#xff0c;大家晒的都是年夜饭和旅行照&#xff0c;今年一个接一个转发的都是能力…

2026/10/20 3:00:51 阅读更多 →

最新新闻

Cursor 详细使用指南:TaoToken 统一 Key 接入与 AI 辅助编程调试实战

Cursor 详细使用指南:TaoToken 统一 Key 接入与 AI 辅助编程调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 3:49:13 阅读更多 →
别急着拿它给短剧配音:音色版权、多角色与长文本的三个隐性坑

别急着拿它给短剧配音:音色版权、多角色与长文本的三个隐性坑

别急着拿它给短剧配音&#xff1a;音色版权、多角色与长文本的三个隐性坑 【免费下载链接】drama-3 项目地址: https://ai.gitcode.com/hf_mirrors/fishaudio/drama-3 当一款开源 TTS 模型被社区评测顶到"开放权重语音竞技场"榜首&#xff0c;和 ElevenLabs、…

2026/10/20 3:49:13 阅读更多 →
电脑主机改造成服务器:硬件差异、稳定性与避坑指南

电脑主机改造成服务器:硬件差异、稳定性与避坑指南

先别急着回答“能”或者“不能”&#xff0c;我把话放在这里&#xff1a;普通电脑主机确实能改造成服务器&#xff0c;而且成本可以压得很低。但我见过太多人&#xff0c;把自己的旧电脑折腾成“服务器”之后&#xff0c;没跑两个月就放弃了&#xff0c;原因就四个字&#xff1…

2026/10/20 3:49:13 阅读更多 →
CFRP约束型钢再生混凝土短柱ABAQUS复现:参数设置与曲线调试全解析

CFRP约束型钢再生混凝土短柱ABAQUS复现:参数设置与曲线调试全解析

做CFRP约束型钢再生混凝土短柱的论文复现&#xff0c;最折磨人的往往不是建模本身&#xff0c;而是材料参数取值和相互作用设置。你从论文里抄参数&#xff0c;原封不动搬到ABAQUS里&#xff0c;结果曲线跟原文对不上&#xff0c;大概率不是模型搭错&#xff0c;是CDP损伤参数、…

2026/10/20 3:49:13 阅读更多 →
物理开路与奇点:文明权的终局博弈,一场底层逻辑推演

物理开路与奇点:文明权的终局博弈,一场底层逻辑推演

最近我一直在琢磨一个特别上头的问题&#xff1a;如果物理学出现本质层面的突破&#xff0c;会不会直接改写文明的命运&#xff1f;这个问题的民间版本&#xff0c;就是标题里那八个字——物理开路、奇点、文明权、终局博弈。先声明一句&#xff0c;我就是一个业余物理爱好者&a…

2026/10/20 3:49:13 阅读更多 →
电商客服与导购智能体合并设计:意图识别、工具调用与知识库实践

电商客服与导购智能体合并设计:意图识别、工具调用与知识库实践

做客服类智能体这几年&#xff0c;我的电脑里存过三版方案。第一版是规则加菜单按钮&#xff0c;用户点两下就烦了&#xff1b;第二版是纯大模型问答&#xff0c;什么都能聊&#xff0c;但一问到订单和售后就露馅&#xff1b;第三版才是真正跑通业务价值的——把电商客服导购智…

2026/10/20 3:48:13 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →