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

【c++面向对象编程】第3篇:类与对象(二):构造函数与析构函数

目录

一、一个让人头疼的问题

二、构造函数:对象出生时的“第一声啼哭”

1. 最基本的构造函数

2. 带参数的构造函数(重载)

3. 初始化列表:更高效的初始化方式

三、默认构造函数:那个“看不见”的函数

四、析构函数:对象临终前的“遗言”

五、完整的例子:动态字符串数组类

六、三个容易踩的坑

1. 构造函数里抛出异常

2. 析构函数里再抛异常

3. 忘记写析构函数导致内存泄露

七、这一篇的收获


一、一个让人头疼的问题

继续用上一讲的Book类:

cpp

Book b; b.setPrice(128.5); // 必须记得手动初始化 b.print(); // 如果忘了setPrice,price是未定义的垃圾值

C的结构体好歹可以用{0}初始化,但C++的类不行——因为类可能有复杂的内部逻辑,简单的内存置零可能破坏状态。

更麻烦的是,如果你的类需要分配动态内存(比如用new申请了一块空间),用完必须手动delete。一旦忘记,内存就泄露了。

构造函数和析构函数就是用来解决这些问题的。


二、构造函数:对象出生时的“第一声啼哭”

构造函数是一种特殊的成员函数

  • 名字和类名完全相同

  • 没有返回值(连void都不写)

  • 在创建对象时自动调用

  • 可以重载(多个构造函数,参数不同)

1. 最基本的构造函数

cpp

class Book { private: string title; string author; double price; public: // 构造函数——名字和类一样,没返回值 Book() { title = "未知"; author = "未知"; price = 0.0; cout << "构造函数被调用了" << endl; } void print() { cout << title << ", " << author << ", " << price << endl; } }; int main() { Book b; // 自动调用构造函数,输出"构造函数被调用了" b.print(); // 输出:未知, 未知, 0 }

注意最后一行的Book b;——我们没有写任何“初始化”的代码,但对象已经有了合理的默认值。

2. 带参数的构造函数(重载)

一个类可以有多个构造函数,只要参数不同:

cpp

class Book { private: string title; string author; double price; public: // 默认构造函数 Book() { title = "未知"; author = "未知"; price = 0.0; } // 带三个参数的构造函数 Book(string t, string a, double p) { title = t; author = a; price = p; } // 只传书名的构造函数 Book(string t) { title = t; author = "佚名"; price = 0.0; } void print() { cout << "《" << title << "》 " << author << " ¥" << price << endl; } }; int main() { Book b1; // 调用默认构造函数 Book b2("C++ Primer", "Lippman", 128.5); // 调用3参数构造 Book b3("三体"); // 调用1参数构造 b1.print(); // 《未知》 未知 ¥0 b2.print(); // 《C++ Primer》 Lippman ¥128.5 b3.print(); // 《三体》 佚名 ¥0 }

3. 初始化列表:更高效的初始化方式

上面的构造函数在函数体里赋值,但还有更正统的写法——初始化列表

cpp

Book(string t, string a, double p) : title(t), author(a), price(p) { // 函数体可以为空,或者写其他逻辑 }

:后面的title(t)意思是“用参数t来初始化title成员变量”。

为什么要用初始化列表?

  • 对于stringvector等有构造函数的类型,初始化列表直接构造一次;在函数体里赋值会先默认构造再赋值,多一步开销

  • const成员变量和引用类型必须用初始化列表

  • 成员变量的初始化顺序按它们在类中声明的顺序,不是按初始化列表的顺序

cpp

class Demo { private: const int id; // const成员必须用初始化列表 int& ref; // 引用必须用初始化列表 string name; public: Demo(int i, int& r, string n) : id(i), ref(r), name(n) { // 这里id和ref已经初始化好了 } };

三、默认构造函数:那个“看不见”的函数

如果你写一个类,没有定义任何构造函数,编译器会悄悄给你生成一个默认构造函数(什么也不做)。

cpp

class Simple { int x; string s; // 编译器会生成一个 Simple() {} }; Simple obj; // 可以编译,但x是垃圾值,s是空字符串

但有一个陷阱:一旦你自己写了任何一个构造函数,编译器就不再生成默认构造函数。

cpp

class Book { public: Book(string t) { title = t; } // 写了构造函数 private: string title; }; Book b1("C++"); // ✅ 没问题 Book b2; // ❌ 编译错误!没有默认构造函数

如果你既想要自己的构造函数,又想要无参的版本,必须显式写出来

cpp

Book() {} // 或者 = default (C++11)

C++11提供了更简洁的写法:

cpp

Book() = default; // 让编译器生成默认版本

四、析构函数:对象临终前的“遗言”

析构函数是构造函数的“镜像”:

  • 名字是~类名

  • 没有参数,不能重载(一个类只有一个析构函数)

  • 没有返回值

  • 对象销毁时自动调用

cpp

class Book { private: string* pComment; // 指向动态分配的评论 public: Book(string comment) { pComment = new string(comment); cout << "构造:分配内存" << endl; } ~Book() { delete pComment; // 释放内存 cout << "析构:释放内存" << endl; } }; int main() { Book b("很好看的一本书"); // 函数结束时,b被销毁,析构函数自动调用 } // 输出: // 构造:分配内存 // 析构:释放内存

什么时候调用析构函数?

对象类型析构时机
局部对象(栈上)离开作用域时(如函数结束)
静态局部对象程序结束时
全局对象程序结束时
new创建的对象显式调用delete

五、完整的例子:动态字符串数组类

把构造和析构结合起来,写一个管理动态数组的类StringArray

cpp

#include <iostream> #include <string> using namespace std; class StringArray { private: string* data; // 指向动态数组的指针 int size; // 数组大小 public: // 构造函数:分配内存并初始化 StringArray(int n) : size(n) { data = new string[n]; cout << "分配了" << n << "个字符串的空间" << endl; } // 析构函数:释放内存 ~StringArray() { delete[] data; cout << "释放了" << size << "个字符串的空间" << endl; } // 设置元素 void set(int index, const string& s) { if (index >= 0 && index < size) { data[index] = s; } } // 获取元素 string get(int index) { if (index >= 0 && index < size) { return data[index]; } return ""; } // 打印所有 void print() { for (int i = 0; i < size; i++) { cout << "[" << i << "] = " << data[i] << endl; } } }; int main() { StringArray arr(3); // 构造:分配内存 arr.set(0, "Hello"); arr.set(1, "World"); arr.set(2, "C++"); arr.print(); // 函数结束,arr被销毁 → 析构函数自动调用,释放内存 return 0; }

运行结果:

text

分配了3个字符串的空间 [0] = Hello [1] = World [2] = C++ 释放了3个字符串的空间

注意:我们没有手动调用任何“释放”函数,析构函数自动帮我们做了。


六、三个容易踩的坑

1. 构造函数里抛出异常

如果在构造函数里抛异常,对象被认为“没有构造完成”,析构函数不会被调用。这意味着构造函数里已经分配的资源需要自己处理。

cpp

Book() { p = new int[100]; throw "error"; // 如果这里抛异常,~Book()不会被调用,内存泄露 }

解决方案:用智能指针(后面的章节会讲),或者在构造函数里用try-catch。

2. 析构函数里再抛异常

极其危险!如果析构函数在栈展开期间(处理另一个异常时)又抛出异常,程序会直接调用terminate()崩溃。

cpp

~Book() { delete p; throw "析构异常"; // ❌ 千万别这么写 }

规则:析构函数应该吞掉所有异常,只做清理,不抛出任何东西

3. 忘记写析构函数导致内存泄露

如果你的类里有指针成员,并且你在构造函数里用new分配了内存,必须写析构函数用delete释放,否则内存就泄露了。

cpp

class Leaky { int* p; public: Leaky() { p = new int[1000]; } // 没有析构函数 → 内存泄露! };

七、这一篇的收获

你现在应该理解:

  • 构造函数在对象创建时自动调用,适合做初始化

  • 析构函数在对象销毁时自动调用,适合做清理

  • 构造函数可以重载(多个版本)

  • 初始化的优先级:初始化列表 > 构造函数体内的赋值

  • 如果你管理了动态资源(new),必须提供析构函数去释放

💡 小作业:写一个IntArray类,构造函数接收大小n并分配n个int的空间,析构函数释放空间。提供set()get()方法。在main中创建对象,观察构造和析构的调用时机。


下一篇预告:第4篇《类与对象(三):拷贝构造函数与深浅拷贝问题》——当你把一个对象赋值给另一个对象时,背后发生了什么?默认的拷贝行为为什么可能导致程序崩溃?

http://www.jsqmd.com/news/794122/

相关文章:

  • 法律智能体构建指南:从LLM与RAG技术到合同审查实战
  • LeetCode 或运算题解
  • 从零到精通的EtherCAT DS402控制模式选择指南:轮廓位置、同步位置、速度模式到底怎么选?
  • 西安石油大学仪光实践协会4月活动机械蝴蝶台灯
  • AI原生用户体验设计:为什么92%的传统交互团队在SITS 2026评估中首轮淘汰?
  • PDF编程的艺术:从基础到实践
  • Blender 3MF插件:5分钟掌握3D打印文件格式转换的完整方案
  • AI智能体记忆系统实战:基于向量数据库构建持久化记忆库
  • python机器学习毕设方向帮助
  • ATE PCB组装:半导体测试中的精密工艺与挑战解析
  • 联发科2012年崛起:从功能机到智能机的转型与挑战
  • 智能体网格(Agent Mesh)架构解析:构建大规模异构智能体协同网络
  • 告别‘瞎跑’:智能车竞赛中线性CCD动态曝光与浮动中心算法的实战调参心得
  • 用Cursor+ChatGPT实现代码报错的自动分析与修复
  • 2012年Accellera标准演进:SystemC、UCIS与AMS如何重塑EDA设计流程
  • 无线充电技术:从手机标配到多场景应用的挑战与机遇
  • TTS听觉校对法:技术写作质量提升的工程实践指南
  • AI编程智能体评估平台CodingAgentExplorer:从原理到实践的系统评测指南
  • 【c++面向对象编程】第4篇:类与对象(三):拷贝构造函数与深浅拷贝问题
  • Java对接海康威视人脸考勤机实战:Spring Boot整合SDK获取刷卡流水记录
  • G.hn Prime家庭网络技术解析与应用实践
  • LeetCode 最大单词长度乘积题解
  • 从公共卫生演习到社会韧性构建:口罩日的系统设计与实施路径
  • ARM调试架构中DBGCLAIMSET寄存器详解与应用
  • LeetCode 二进制中1的个数题解
  • 终极视频修复指南:使用Untrunc快速恢复损坏的MP4、MOV、M4V文件
  • Obsidian Quiz Generator:用AI从笔记生成交互测验,打造学习闭环
  • 5分钟快速上手:Blender 3MF插件让你轻松实现3D打印模型转换
  • EDA工程师成长与验证技术演进:从算法到芯片的实践闭环
  • AI心智理论评估:VLM意图理解接近人类,但视角采样能力存在瓶颈