☰
# C++ 对象生命周期全攻略:构造/析构/拷贝构造/深浅拷贝/new 与 delete,笔试必考一次讲完
2026/10/14 19:39:24 网站建设 项目流程

摘要:本文围绕“对象从出生到销毁”这条主线,讲清构造函数、初始化列表、析构函数、拷贝构造的调用时机,并深挖浅拷贝双重释放的事故现场。核心结论:带指针成员的类必须自定义深拷贝或禁用拷贝;new/delete 与 malloc/free 的本质区别是是否调用构造/析构。示例在 g++(C++11 模式)下验证。

一、问题背景

C 语言里malloc一块内存、填几个字段就算“创建对象”;到了 C++,类里可能有const成员、引用成员、需要申请堆内存的指针成员,编译器不认识你的自定义逻辑,没法替你自动初始化。于是 C++ 提供了构造函数与析构函数:对象实例化时自动调用构造,生命周期结束时自动调用析构。

围绕这套机制,笔试面试题高度集中:

  • 空类sizeof为什么是 1?
  • 初始化列表和函数体里赋值有什么区别?
  • 析构函数能手动调用吗?调用几次?
  • 浅拷贝到底怎么把程序搞崩的?

二、构造函数:对象的“出生手续”

思路

函数名与类名相同、无返回值(连 void 都不能写)、支持重载。用户不写任何构造时编译器给一个默认无参构造;一旦写了任何构造函数,默认无参构造就不再提供(想保留要显式写出来)。

classCar{string brand;floatweight;inthorsepower;public:Car():brand("红旗"),weight(1.5f),horsepower(200){}// 无参Car(string b,floatw,inth):brand(b),weight(w),horsepower(h){}// 带参重载};

初始化列表:不只是风格问题

Computer::Computer(string n,intp,doublec):name(n),price(p),cpu(c){// 函数体可为空:进入函数体前成员已初始化完毕}
  • 对 string 等自定义类型成员,函数体内赋值 =先默认构造再赋值(两次操作);初始化列表 =直接构造(一次操作),效率差异明显。对内置类型(int、float 等),两者差异不大。
  • const成员和引用成员只能用初始化列表,没有第二条路。
  • 列表书写顺序不影响执行顺序:实际初始化顺序永远按成员在类内的声明顺序,书写顺序与之不一致时(如: b(a_), a(1),b 依赖 a 就会用到未初始化值),是经典笔试题陷阱。

三、空类 sizeof 之谜

classEmpty{};cout<<sizeof(Empty);// 1,实测输出

C++ 规定每个对象必须有唯一地址,编译器能分配的最小单元是 1 字节,所以空类对象塞 1 字节的“占位符”。非虚成员函数和静态成员变量都不占对象内存——函数代码在代码区,静态成员在静态存储区。带虚函数的类则另当别论(虚表指针 4/8 字节,后面多态文章会讲)。

四、析构函数与“手动调用”实验

思路

~类名()、无参、不能重载,对象销毁时自动调用,用于释放资源。

容易忽略的坑:析构函数可以被手动调用

Carcc("demo",1.0f,100);cc.~Car();// 手动调用:对象生命周期提前结束!cc.info();// 未定义行为:对象已死,不能再使用// 作用域结束时编译器再次自动调用析构 → 重复析构

显式调用析构函数会提前结束对象生命周期,此后再次使用该对象是未定义行为。而对象离开作用域或delete时还会再次自动调用析构,造成重复析构。实践结论:除了 placement new 配合的极端场景,永远不要手动调析构。

五、拷贝构造:调用时机

函数原型固定:Student(const Student& other)——参数必须是本类常引用,写成传值会无穷递归(拷贝实参又要调拷贝构造)。

Students1("张三",20);Student s2=s1;// 时机1:用已有对象初始化新对象Students3(s1);// 时机2:直接构造voidfunc(Student s);func(s1);// 时机3:值传递实参Studentfunc2();Student r=func2();// 时机4:返回对象

无名对象(临时对象)补充:Car("BMW", 2.0f, 300);创建即用即销毁;C++17 起Car c = Car(...)的 prvalue 返回强制省略拷贝,直接构造到目标,不再多一次拷贝。

对于时机4,返回对象时编译器优先进行RVO/NRVO(返回值优化)。C++17 起,对于返回 prvalue(纯右值)的情况,拷贝省略是强制性的;对于返回具名局部变量的 NRVO,C++17 仍为可选优化,不同编译器可能表现不同。因此拷贝构造通常不会触发。

六、new/delete vs malloc/free:笔试题标准答案

对比项malloc / freenew / delete
性质C 标准库函数C++ 运算符(关键字)
调用构造/析构不调用自动调用(new 调构造,delete 调析构)
返回类型void*,需强转直接返回目标类型指针
类型检查/大小计算手动算字节数编译器按类型自动确定
失败行为返回 NULL默认抛std::bad_alloc
释放数组free(p)必须delete[] p,写成delete p是未定义行为
int*p=newint(10);// 分配并初始化Car*cp=newCar("Tesla",2.1f,500);// 自动调构造deletecp;// 自动调析构(实测输出:带参构造→析构各一次)int*arr=newint[10];delete[]arr;// 注意方括号

七、深拷贝 vs 浅拷贝:指针成员引发的“血案”

思路

不写拷贝构造时,编译器给默认的逐成员复制(浅拷贝)。成员里有裸指针时,两个对象的指针指向同一块堆内存:析构时被delete两次 → double free,程序崩溃;或者一方修改,另一方“见鬼”地跟着变。

事故现场 + 修复代码

#include<cstring>#include<iostream>usingnamespacestd;classMyString{char*data;public:MyString(constchar*s=""){data=newchar[strlen(s)+1];strcpy(data,s);}~MyString(){delete[]data;}constchar*c_str()const{returndata;}// 【浅拷贝(默认)的问题】:删掉下面这个函数,// MyString b = a; 后函数退出时 a、b 析构同一块内存 → 崩溃/双重释放// 方案:深拷贝——重新开内存、搬内容MyString(constMyString&other){data=newchar[strlen(other.data)+1];strcpy(data,other.data);cout<<"深拷贝: "<<data<<endl;}// 同时禁用拷贝赋值,避免赋值时浅拷贝MyString&operator=(constMyString&other)=delete;};intmain(){MyStringa("hello");MyString b=a;// 触发深拷贝构造cout<<a.c_str()<<" / "<<b.c_str()<<endl;// hello / hello,两块独立内存return0;// a、b 各自析构各自的内存,安全}

效果与代价

  • 深拷贝:多分配一块内存(空间代价),换来所有权清晰——谁申请谁释放。
  • 不想深拷贝的替代方案:把拷贝构造= delete;(C++11),从语法上禁止拷贝,适用于管理独占资源的类(文件句柄、连接)。同样也应禁用拷贝赋值运算符。

八、std::string:为什么别再拿 char* 当字符串

string s2="Hello";strings4("World");cout<<s2+s4;// 直接拼接 HelloWorlds2.append(" C++");// 追加s2.length();// 长度s2.clear();// 清空

string 内部封装了动态内存管理(自带深拷贝语义),+、=、==全部重载好,彻底告别strcpy越界和忘记free的经典事故。

九、对比分析:三种“初始化成员”的方式怎么选

方式执行时机适用场景代价
初始化列表对象进入构造函数体之前const/引用成员必用;非POD成员性能更优需注意声明顺序陷阱
函数体内赋值默认构造完成之后逻辑简单、需要计算/校验后再定值对自定义类型多一次默认构造+赋值
= delete拷贝编译期禁止独占资源类防误拷贝对象不可复制,传参用引用/指针

底层原因:初始化列表对应“用参数直接调用成员的构造函数”;函数体赋值是“先调成员的默认构造,再调operator=”。对 string 这种堆上持有字符缓冲的成员,前者一次分配,后者先分配再析构再分配,浪费明显。对内置类型则没有这种开销。

十、结论

  1. 对象生命周期 = 构造(含拷贝构造)→ 使用 → 析构,编译器全自动插入,new/delete 只是把“自动”从栈扩展到堆。
  2. 带指针成员的类三条路:深拷贝、= delete禁拷、换智能指针(下一篇 STL/模板后会见到 RAII 思想)。
  3. new[] 配 delete[],delete 单个配不带括号的——配对错误是未定义行为,常见表现为崩溃或堆损坏,务必严格遵守。

十一、实践建议

  • 亲手做一次“事故复现”:删掉深拷贝函数、打开调试器跑MyString b = a;,观察崩溃栈停在free/delete——比背十遍概念有用。
  • 写一个“个人理财管理系统”小项目练手(课堂大作业思路):账户类用 string + 动态数组记录流水,必须自定义拷贝构造处理指针成员,每天迭代一个新知识点。
  • 笔试题自测:对象数组Test arr[10];调几次构造?答:如果类有无参构造,调 10 次默认构造;如果类只有带参构造,则编译不过,需要用初始化列表逐个提供参数,例如Test arr[3] = {Test(1), Test(2), Test(3)};(可能涉及拷贝构造,但通常被优化)。

参考

  • malloc/free 与 new/delete 区别 - CSDN 博客
  • new/new[]/delete/delete[] 与 malloc/free 对比 - 知乎专栏
  • C++ 深拷贝和浅拷贝 - CSDN 博客

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询