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

发布时间:2026/10/20 3:00:45

C++ 构造函数和析构函数示例详解 前言构造函数和析构函数是 C 对象生命周期的两个端点。构造函数负责把一片原始内存变成有效对象析构函数负责在对象消失前释放它持有的资源。理解这两端才能理解 RAII 为什么是 C 资源管理的基石。常见误解有几个。第一以为构造函数「分配对象内存」——内存是new、栈帧或容器分配的构造函数只是在这片内存上建立对象。第二以为成员按初始化列表的书写顺序构造——实际顺序由成员声明顺序决定初始化列表顺序写反了编译器不会报错却可能读到未初始化的值。第三以为构造函数抛异常时析构函数会被调用——不会只有已经构造完成的基类和成员会被销毁。本文从构造顺序讲起接着讲析构的调用时机与异常约束再梳理特殊成员函数的生成规则最后给出一个完整的 RAII 类型示例。示例以 C17 为基准代码未经编译或运行验证。一、构造初始化列表与成员顺序一个对象构造时执行顺序是固定的基类子对象按继承列表顺序构造非静态数据成员按类中声明顺序构造执行构造函数体。初始化列表只是给前两步提供参数它不改变顺序。看下面这个例子// 适用于 C17class Config {public:Config(int limit, int step): step_(step), limit_(limit) {} // 书写顺序与声明顺序相反private:int limit_; // 先构造int step_; // 后构造};这里两个成员都是int谁先谁后看不出差别。但如果step_的初始化依赖limit_顺序就致命了// 适用于 C17class Bad {public:Bad(int n) : limit_(n), step_(limit_ * 2) {} // 错误limit_ 尚未初始化private:int step_; // 声明在前先初始化int limit_;};step_先初始化此时limit_还是未初始化的值读取它是未定义行为。编译器通常能用-Wreorder或类似警告提示初始化列表顺序与声明顺序不一致应当把警告打开并清零。初始化列表也不是唯一手段。C11 起可以使用默认成员初始化器default member initializer在声明处给出默认值如果构造函数没有覆盖就使用默认值。C11 还引入了委托构造delegating constructor一个构造函数可以调用同类另一个构造函数避免重复代码。// 适用于 C17#include string#include utilityclass 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成员和引用成员必须在初始化列表中初始化不能在函数体里赋值。成员初始化可以用圆括号C11 起也可以用花括号花括号会拒绝窄化转换例如double到int。初始化列表中的表达式在进入构造函数体之前求值此时对象尚未完全构造不要在里面调用可能依赖完整对象的虚函数。二、析构逆序销毁与异常边界析构顺序与构造严格相反先执行析构函数体再按声明逆序销毁成员最后销毁基类子对象。这保证了「后构造的先销毁」依赖关系不会颠倒。析构函数在以下情况被调用自动存储期对象离开作用域delete一个对象临时对象所在完整表达式结束容器销毁时销毁其元素异常传播时销毁已构造的局部对象栈展开。构造函数抛出异常时析构函数不会被调用因为对象从未完整构造。但已经构造完成的成员和基类子对象会被销毁这就是为什么资源应当交给成员对象管理而不是在构造函数体里手动申请再指望析构函数兜底。析构函数有两个重要约束第一多态基类的析构函数必须是virtual。通过基类指针delete派生类对象时如果基类析构不是虚函数行为是未定义行为通常表现为派生类析构不执行、资源泄漏。第二析构函数不应抛出异常。C11 起析构函数默认是noexcept(true)除非成员或基类的析构可能抛出。异常从析构函数逸出会触发std::terminate。如果某个清理动作可能失败应当提供单独的close()或flush()接口让调用方显式处理错误而不是把错误留在析构里。// 适用于 C17class 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::vectorunsigned char或std::unique_ptrunsigned char[]。// 适用于 C17#include algorithm#include cstddef#include utilityclass 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::vectorunsigned char这五个函数一个都不用写// 适用于 C17#include vectorclass Buffer2 {public:explicit Buffer2(std::size_t size) : data_(size) {}std::size_t size() const { return data_.size(); }private:std::vectorunsigned char data_;};拷贝、移动、析构全部由std::vector正确实现这就是规则零带来的简洁性。常见坑点❌ 初始化列表顺序与成员声明顺序不一致且成员之间存在依赖。✅ 按声明顺序书写初始化列表打开-Wreorder类警告并清零。❌ 在构造函数体里给const成员或引用成员赋值。✅ 这两类成员必须在初始化列表中初始化函数体里赋值无法通过编译。❌ 以为构造函数抛异常时析构函数会执行从而在析构里释放构造中途申请的资源。✅ 用成员对象管理资源已构造的成员会自动销毁未构造完成的对象不会调用析构。❌ 多态基类析构不写virtual通过基类指针delete。✅ 基类析构声明为virtual派生类用override标注。❌ 在析构函数里抛出异常。✅ 析构默认noexcept异常逸出会std::terminate把可能失败的清理放到显式接口。❌ 手写拷贝构造只复制指针导致两个对象持有同一块内存。✅ 深拷贝或使用std::vector/std::unique_ptr等 RAII 成员实现规则零。❌ 移动构造不把源对象置为可析构状态源对象析构时重复释放。✅ 转移指针后把源指针置空、大小置零被移动对象保持「有效但未指定」。❌ 声明了析构函数却仍指望编译器生成移动构造。✅ 用户声明析构会抑制移动操作的隐式生成需要移动就显式 default或手写。总结阶段顺序 / 规则关键提醒构造基类 → 成员按声明顺序→ 函数体初始化列表顺序不决定构造顺序析构函数体 → 成员逆序 → 基类多态基类必须虚析构拷贝深拷贝资源注意自赋值与异常安全移动转移资源并置空源对象标noexcept不读被移动对象规则零用 RAII 成员不手写特殊成员现代 C 的首选构造函数建立不变式析构函数释放资源两者之间的所有权属转移都要说清楚。把资源交给成员对象、把特殊成员函数交给编译器绝大多数生命周期问题根本不会出现。
延伸阅读

更多相关文章

2026/10/20 3:00:45

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

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

2026/10/20 2:55:42

TensorRT实战手册:GPU推理优化与INT8量化完整指南

要说GPU推理优化,TensorRT是绕不过去的那一个。框架跑训练、TensorRT做上线推理,这个分工在工业级落地里已经成了默认组合。简单讲,TensorRT是一个专攻推理阶段的高性能优化引擎,它把训练好的模型拿到手,通过层融合、精…

2026/10/20 2:55:42

Python工业相机实时画面获取实战:SDK配置到稳定出图

最近有个项目要在 Python 里把一台工业机器人相机的实时画面快速调出来。听起来真不算难:SDK 一装,OpenCV 一显示,不就成了吗?但真上手才发现,从设备枚举到图像格式转换,每一环都藏着能把人绊倒半天的细节。…

2026/10/20 4:06:14

多感官学习Prompt实战:让大模型用多感官体验讲懂知识

“多感官学习”这个词,在教育圈和认知科学领域已经提了很多年,但真正把它落到Prompt里,让大模型按七种感官逻辑去“讲课”,而不是简单输出一段文字,这中间的门道远比我最初想象的深。我一开始以为,多感官Pr…

2026/10/20 4:06:14

Windows 10下LIBSVM安装使用与调参完整实战指南

第一次在Windows 10下完整走通LIBSVM的安装和使用实验,我用了整整一天时间,其中多半的精力都花在解决环境、路径和参数理解上。如果你已经习惯了点鼠标或者调高级API,第一次面对这两个冷冰冰的.exe命令时,大概也会有一瞬间的恍惚&…

2026/10/20 4:01:14

context-mode:一种以用户意图为中心的动态上下文交互范式

1. 项目概述:这不是一个功能,而是一种交互范式重构“context-mode”这个词最近在多个技术社区和产品讨论组里高频出现,但它既不是某个新发布的开源库,也不是某家大厂刚推出的API服务。我第一次在某跨平台系统的需求评审会上听到它…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* 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:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* 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:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

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

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑