恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C++面向对象中篇:构造函数、深拷贝与this指针核心解析

  • 首页
  • 资讯中心
  • /
  • C++面向对象中篇:构造函数、深拷贝与this指针核心解析

相关资讯

文件大小与占用空间为何不同?深入解析文件系统簇分配与磁盘空间管理原理 2026/10/13 12:01:28
二叉树叶子结点个数统计:递归与非递归实现及边界处理 2026/10/13 11:56:28
I2C外挂EEPROM读写实战:从选型、时序到代码实现与避坑指南 2026/10/13 11:56:28

最新资讯

Premiere Pro剪辑全流程指南:从新手入门到高效出片
Farm 的 css.transformToScript:把 CSS 模块编译为 JS 注入脚本的原理与实战
Arduino驱动LCD1602无驱动板:从引脚接线到时序详解
FreeType字体渲染实战:从灰度位图到彩色像素的完整指南
鸿蒙Unity崩溃排查:用Sentry将堆栈还原到C#行号
MATLAB与Excel数据导入导出实战:readtable/writetable函数选型与避坑指南

今日推荐

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析
CAN总线错误处理机制深度解析:从位错误到Bus Off的硬件级调试

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

C++面向对象中篇:构造函数、深拷贝与this指针核心解析

发布时间:2026/10/13 12:01:28
C++面向对象中篇:构造函数、深拷贝与this指针核心解析 C 学到面向对象的中段很多人会出现一种“知识点都认识一写就翻车”的状态。类、对象、成员函数这些基本概念刚过了一遍一到构造函数、拷贝、指针成员就开始出问题。这一章面向对象-中解决的就是这个阶段最核心的几个问题对象的生命周期怎么控制、拷贝到底拷贝了什么、this 到底指谁、静态成员和普通成员到底差在哪。说白了前半程教你怎么定义类这一章教你类在实际内存里是怎么运转的。这份配套教材的内容我也反复带人过过今天以一个过来人的身份把这一章的核心逻辑和最容易踩坑的地方拆开讲一遍。适合刚学完类与对象基础、正卡在深拷贝和 this 指针上的读者也适合准备系统性梳理 C 面向对象知识点的开发者。1. 这一章到底解决什么问题1.1 为什么是“中篇”而不是直接讲继承把 C 的面向对象拆成上、中、下三篇来学对于基础不扎实、或者半路转码的学习者来说确实是最合适的节奏。上篇解决“类是什么”讲成员变量、成员函数、访问限定、对象怎么定义下篇解决“类怎么扩展”讲继承、多态、虚函数、抽象类中间这一篇就是用类写真实代码之前最关键的一道关卡——对象从哪来、怎么拷贝、怎么释放、怎么共享数据。很多人急着跳到下篇学多态结果连拷贝构造和析构都分不清写出来的类一跑就崩。中篇看着知识点零散其实全部围绕一个核心问题对象在内存里的生命周期。把这个想清楚了后面学继承多态才会顺否则虚表、基类指针这些东西越学越糊涂。1.2 本章知识地图这一章内容可以按四个组来梳理对应不同的使用场景知识组包含内容解决的核心问题生命周期组构造函数、析构函数、初始化列表对象创建和销毁时的规矩拷贝组拷贝构造、拷贝赋值运算符一个对象怎么复制给另一个对象身份与共享组this 指针、静态成员对象怎么知道自己是哪个对象、数据怎么共享组织与访问组友元、对象数组、对象指针对象怎么组织存放、权限边界放哪里这张表我建议记在笔记里。学的时候一个知识点一个知识点过学完回头再看这张表会发现自己能串起来了。1.3 学这一章之前需要什么基础类和对象的基本语法能自己写一个简单的 class知道 public、private 分别限制什么。new 和 delete 的基本用法清楚堆区、栈区的区别知道 new 出来的东西要手动 delete。引用的概念不用太深知道引用是起了个别名就行。数组的基本用法后面对象数组会用到。如果这些还生疏先回去补一补不然这一章的内容你会觉得每个字都认识但连起来完全不知道在说啥。2. 构造函数与析构函数对象的一生2.1 构造函数的重载与默认行为构造函数是对象创建时自动调用的特殊成员函数名字和类名相同没有返回值。它解决的核心问题是对象一出生就必须处于一个可用状态。比如一个日期类年、月、日都应该有初始值而不是随机的垃圾数据。class Date { public: Date() : m_year(2024), m_month(1), m_day(1) {} Date(int year, int month, int day) : m_year(year), m_month(month), m_day(day) {} void show() const { std::cout m_year - m_month - m_day std::endl; } private: int m_year; int m_month; int m_day; };一个关键知识点如果你一个构造函数都没写编译器会隐式生成一个默认构造函数但只要你定义了自己的构造函数编译器就不再帮你生成默认的了。很多人在这里翻车——写了一个带参构造结果在别处直接Date d;编译器报错“没有默认构造函数”一脸懵。所以需要无参构造时一定要自己显式写出来。2.2 初始化列表为什么推荐用它构造函数有两种写法一种在函数体里赋值一种用初始化列表。初学者觉得差不多但实际差很多。class Demo { public: Demo(int v) : m_val(v), m_ratio(v * 2) {} private: const int m_val; // 常量成员 int m_ratio; };先记住一条硬规则const 成员和引用成员必须在初始化列表里初始化不能在函数体里赋值。因为这两种成员一旦创建就不能再变而函数体里的赋值发生在对象构造完成之后那时候 const 已经定死了改不了。初始化列表还有一个特性成员按它们在类中的声明顺序初始化而不是按列表里写的顺序。所以最好让初始化列表的顺序和声明顺序保持一致不然一个小坑列表里先写后面的成员实际却是另一个先初始化如果初始化有依赖关系就会得到错误结果。这类问题编译器不报错全靠自己细心。2.3 析构函数的执行时机析构函数是对象销毁时自动调用的函数名字是类名前加~。它最典型的用途是释放资源new出来的内存、打开的文件、申请的锁。class FileHandler { public: FileHandler(const char* filename) { m_file fopen(filename, w); } ~FileHandler() { if (m_file) { fclose(m_file); } } private: FILE* m_file; };关键是搞清楚析构什么时候被调用栈上的局部对象作用域结束的时候。全局对象程序退出时。堆上new出来的对象手动delete的时候。临时对象所在表达式结束的瞬间。很多人会忽略临时对象。写Date d createDate();这种语句可能中间会创建临时对象临时对象在完成拷贝使命后马上析构。如果析构函数里有复杂逻辑这里容易埋性能问题调试时也会看到莫名的析构调用。注意析构函数里不要抛异常。析构本身是在对象回收的关键流程里如果抛异常且没被捕获程序会直接异常终止。资源释放失败时宁可记录日志也别往外出异常。2.4 拷贝构造函数什么时候被调用拷贝构造函数是这一章最容易混淆的概念它是用一个已有对象去初始化另一个新对象时调用的。以下几种写法都会触发拷贝构造Date d1(2024, 3, 15); Date d2 d1; // 这叫拷贝初始化触发拷贝构造 Date d3(d1); // 直接初始化同样触发拷贝构造 void printDate(Date d) { ... } // 按值传参形参是实参的拷贝 printDate(d1); // 触发拷贝构造 Date getDate() { Date d(2024, 3, 16); return d; } Date d4 getDate(); // 返回值拷贝触发拷贝构造特别注意Date d2;之后再d2 d1;不算拷贝构造那是赋值运算符的工作。区别本质在于拷贝构造发生在对象还没出生的时候赋值发生在对象已经存在的时候。这个区分后面深拷贝那里非常关键。如果你没有写拷贝构造函数编译器会生成一个默认的它做的是成员到成员的复制。对于只有 int、double 这类普通成员的类默认拷贝构造完全够用。但一旦类里有指针成员默认拷贝就出大问题了这就是下一节的内容。3. 深拷贝与浅拷贝最容易踩的坑3.1 浅拷贝为什么有问题先看一个经典的反面例子class IntArray { public: IntArray(int size) { m_size size; m_data new int[m_size]; } ~IntArray() { delete[] m_data; } private: int m_size; int* m_data; }; IntArray a(10); IntArray b a; // 调用了默认拷贝构造问题来了默认拷贝构造做的是浅拷贝也就是只复制成员的值。a.m_data和b.m_data存的是同一个地址两个指针指向同一块堆内存。程序结束时b先析构释放了这块内存然后a析构又 delete 一次。同一块内存释放两次就是著名的 double free程序直接崩溃。更隐蔽的问题是悬空指针一个对象先析构了另一个对象的指针还指向已经释放的内存后面一访问就是野指针行为错误可能隔了好远才爆发特别难排查。3.2 深拷贝的正确写法深拷贝的意思很简单拷贝的时候把指针指向的数据也重新拷贝一份让两个对象各自拥有独立的内存。改写之后的拷贝构造函数长这样class IntArray { public: IntArray(int size) : m_size(size), m_data(new int[size]) {} // 拷贝构造深拷贝 IntArray(const IntArray other) : m_size(other.m_size), m_data(new int[other.m_size]) { std::copy(other.m_data, other.m_data m_size, m_data); } ~IntArray() { delete[] m_data; } private: int m_size; int* m_data; };这样IntArray b a;之后b.m_data指向一块新分配的内存数据是复制过来的和a.m_data互不相干。两者的析构也都各自释放自己的内存相安无事。写深拷贝时有个细节构造函数里分配内存可能失败抛出异常。面试题里喜欢问这个但日常开发只要正确处理异常和资源释放即可。真正要死记的是只要类里有指针成员或资源句柄默认拷贝构造就绝对不能直接用。3.3 拷贝赋值运算符 operator既然有了拷贝构造赋值也得配套。赋值运算符处理的是两个都已存在的对象之间的赋值IntArray operator(const IntArray other) { if (this ! other) { // 第一步自赋值检查 delete[] m_data; // 第二步释放旧资源 m_size other.m_size; m_data new int[m_size]; // 第三步分配新资源 std::copy(other.m_data, other.m_data m_size, m_data); } return *this; // 第四步返回自身引用 }第一步的自赋值检查很多人漏。如果写成arr arr;没检查的话先把m_data释放了然后new int[m_size]的时候other.m_size也已经是个无效对象了直接出事。检查this ! other是标准做法。赋值运算符返回*this也很重要这是为了支持链式赋值a b c;以及和标准库容器风格保持一致。忘了返回值或者返回错了编译未必报错但行为就有问题。3.4 三/五法则一条需要背下来的规矩C 圈子里有个老规矩叫“三法则”——如果你写了析构函数、拷贝构造函数、拷贝赋值运算符中的任意一个那么另外两个也几乎必须一起实现。因为一旦需要显式管理资源说明浅拷贝不适用那么三个和资源相关的特殊函数都得同步维护。C11 之后扩展成“五法则”多了移动构造函数和移动赋值运算符。我建议初学阶段至少把三法则落实到位要么都不写让编译器默认处理要么三个都写做到真正的深拷贝。一个记忆技巧写一个带指针成员的类之前先问自己三个问题——析构要释放吗拷贝要深拷贝吗赋值要重新分配吗答案只要有一个是“要”三法则就全都要。4. this 指针与静态成员4.1 this 指针到底是什么很多初学者把 this 想得很玄其实它就是一个隐式参数。当你调用d.show()时编译器等价地把对象的地址传给成员函数函数内部通过 this 访问当前对象。换句话说this 指向“正在调用这个函数的那个对象”。看这个最简单的场景class Point { public: Point(int x, int y) { this-x x; // 左边是成员右边是参数 this-y y; } void moveTo(int newX, int newY) { x newX; y newY; } private: int x; int y; };在moveTo里直接写x其实编译器默认解析成了this-x。当你写了和成员同名的参数时就不得不显式用this-来区分。可以说this 让函数知道“我操作的是谁”。this 指针还有一个经典用途是链式调用class Counter { public: Counter add(int step) { m_count step; return *this; } int value() const { return m_count; } private: int m_count 0; }; Counter c; c.add(1).add(2).add(3); // 连续调用靠的是每次返回 *this4.2 const 成员函数与 mutable成员函数声明后面加一个 const比如int value() const表示这个函数承诺不会修改对象的成员变量。编译器会把这个函数里的 this 指针当作指向常量的指针来检查一旦函数里试图写成员直接编译报错。这个机制的价值是文档性的调用方看到 const 成员函数就知道调用它是安全的不会改变对象状态。反过来const 对象只能调用 const 成员函数普通成员函数都不能调。有时候你确实需要在 const 函数里改某些内部计数器、缓存这类不影响外部状态的变量C 提供了mutable关键字。但现实建议是尽量少用一旦用上就要在注释里说清楚为什么这些变量不受 const 约束否则后来接手的人会非常困惑。4.3 静态成员属于类的数据静态成员变量不属于任何具体对象它属于整个类所有对象共享同一份数据。典型场景是统计一共创建了多少个对象class Student { public: Student() { s_totalCount; } static int totalCount() { return s_totalCount; } private: static int s_totalCount; }; int Student::s_totalCount 0; // 类外定义必须写这里有个初学者必踩的坑静态成员变量必须在类外单独定义和初始化在类内只是声明。不写int Student::s_totalCount 0;这一行链接阶段直接报“未定义引用”。静态成员函数也有特点它没有 this 指针因此不能访问非静态成员。很自然因为它不属于某个对象哪来的“这个对象”呢想访问某个对象的非静态成员必须通过参数把对象传进去。静态成员和全局变量相比优势在于它被类的访问控制管住还有命名空间隔离不会污染全局。5. 友元、对象数组与对象指针5.1 友元权限的例外机制友元函数和友元类的作用是允许类外部的函数或类访问本类的私有成员。声明方式是在类内部写friend关键字。比如想写一个全局函数计算两点距离但不想对外暴露坐标成员class Point { friend int distanceSquared(const Point a, const Point b); private: int x; int y; }; int distanceSquared(const Point a, const Point b) { int dx a.x - b.x; // 友元函数可以直接访问私有成员 int dy a.y - b.y; return dx * dx dy * dy; }注意友元关系不受访问权限位置影响写在 public、private 下面效果一样。友元也不能继承A 是 B 的友元不代表 A 的对象能访问 B 派生类的私有成员。教材里一般都会提醒友元是方便但别滥用。它破坏了封装边界一旦类内部结构改变所有依赖友元的外部函数都会受影响。我的建议是优先把功能设计成公开成员函数、或者用公开接口间接操作私有数据只有在运算符重载等确实需要直接访问私有成员的情况下才用友元。5.2 对象数组与对象指针对象数组在构造上有自己的特殊性。数组元素是按顺序默认构造的所以如果数组元素类型没有默认构造函数直接声明数组会编译失败Date dates[5]; // 需要 Date 有默认构造函数解决思路有两个给 Date 写一个默认构造函数或者用初始化列表显式构造Date dates[] { Date(2024, 1, 1), Date(2024, 2, 1) };对象指针则要区分两种情况。一种是指向已有对象的指针比如Date* p d1;这只是记录地址不涉及新对象的创建另一种是用new创建堆对象比如Date* p new Date(2024, 3, 15);这时要记得配对使用new/delete或new[]/delete[]。数组形式分配的就用delete[]单个对象就用delete混着用是未定义行为可能崩也可能内存错乱。一个非常常见的错误是只 delete 不置空delete p; // p 变成悬空指针但它还保留着旧地址更好的习惯是删除后把指针置为nullptr或者干脆离开作用域让指针自然消失。5.3 堆上对象的生命周期管理堆上对象的生命周期完全由开发者控制这是一把双刃剑。好处是灵活可以在函数返回后继续存活坏处是漏释放就会内存泄漏。教材里一般会引出 RAII 的概念把资源绑定在对象生命周期上对象构造时拿资源析构时释放资源。前面 FileHandler 的例子就是 RAII 的雏形。实操中还有一条经验能用栈对象就用栈对象别一股脑 new。栈对象最晚在作用域结束时会自动析构代码路径再多、提前 return 多少次都不会漏资源。只有对象需要活得比所在作用域更长时才考虑堆对象。6. 常见问题与排查技巧实录6.1 高频翻车速查表症状原因定位与解决程序崩溃报 double free 或段错误浅拷贝两个指针指向同一块内存重复释放实现深拷贝的构造函数和赋值运算符遵循三法则编译报找不到默认构造函数定义了带参构造后没有默认构造显式补一个无参构造链接报未定义的静态成员静态成员没有在类外定义在源文件补一行定义如int Student::s_totalCount 0;拷贝后修改一个对象另一个也变了浅拷贝导致共享内存深拷贝让两个对象各自持有数据赋值给自身后数据丢失或崩溃赋值运算符缺少自赋值检查加if (this ! other)delete 一个数组对象崩了new[]和delete不配对数组用new[]就必须用delete[]出现了异常的析构调用次数临时对象在表达式结束后析构用引用或右值引用减少临时拷贝确认析构逻辑不依赖顺序6.2 一个完整的调试思路当年我排查过最折腾的一个 bug一个自定义数组类在某个场景下打印出来的数据全是随机值偶尔还崩溃。查了半小时最后发现是拷贝构造没写编译器用了浅拷贝两个对象共享内存其中一个析构释放后另一个还在用。那次之后我养成了一个习惯任何带指针成员的类写完第一件事就是把构造函数、拷贝构造、赋值、析构四个函数列出来缺哪个补哪个。另外一个特别实用的调试技巧是“生命周期日志”——在构造函数和析构函数的入口打印标记。跑一遍测试日志序列基本能告诉你对象多构造了一次还是少析构了一次比打断点慢慢看效率高得多。std::vector这类标准容器是排查浅拷贝问题的顺手工具。如果同一个地址在拷贝后被调用了两次delete很多内存检测工具都能直接抓到。系统有内存检测工具或 sanitizer 的话编译期加个选项就能把 double free 和越界访问原形毕露学这一章时强烈建议开着跑。6.3 我带人过这一章时的几个心得先画内存图再写代码。凡是涉及指针成员都把栈上的对象画出来把堆上的内存画出来两个对象共用还是各自独立一眼就清楚。每个知识点都配一个最小可运行程序。拷贝构造用一个类静态成员写一个计数器类友元就写一个全局函数。别写大项目越小越容易观察。学会看编译器的报错日志。C 的报错往往长而绕但关键信息在前面几行比如“没有合适的默认构造函数可用”“尝试引用已删除的函数”这类基本直接告诉了你答案。最后再分享一个混合的教学技巧把这章的类当作数学里的定义题来练。给你一个需求比如“一个类管理一个字符串缓冲区”要求你自己写出构造、拷贝、赋值、析构四件套。能写出来比看十遍书都有用。写的过程中才会真正体会三法则为什么存在this 为什么返回引用静态成员为什么要类外定义。这门课说到底就是要多动手把代码量堆上去道理自然就通了。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号