三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【C++ 面试真题】聊聊 C++ 的构造与析构

【C++ 面试真题】聊聊 C++ 的构造与析构

【C++ 面试真题】聊聊 C++ 的构造与析构

构造和析构是 C++ 面向对象篇的"开场必问"。背得出"构造初始化、析构清理"只是及格,真考你的是"多层继承下构造析构的执行顺序、基类析构为什么必须 virtual、构造函数里能不能调虚函数"——一道题能从语法问到内存布局。本文把构造析构的通用机制一次讲透——它是整个面向对象的地基。


一、先说结论:构造/析构 = 对象的"生与死"

❓ 构造函数和析构函数到底是干嘛的?

✅ 一句话:构造函数让对象"活过来"(成员拿到合法初值、申请资源),析构函数让对象"干净地走"(释放资源、归还系统)。

两者都是特殊成员函数——名字固定、没有返回值、你不调,编译器替你调

structFile{File(constchar*path);// 构造:打开文件~File();// 析构:关闭文件private:FILE*fp_;};
{Filef("a.txt");// 构造自动调用// ... 用 f}// 离开作用域,析构自动调用

💡 这就是RAII的根基:把资源绑定到对象生命周期,构造获取、析构释放,再也不用手动 close/free。智能指针、内存管理用的都是这个思想。


二、构造函数"家族"有哪些?

❓ 构造函数分几种?

✅ 主要四种,本篇聚焦前两种的通用机制,拷贝和移动各有专门考法,先不展开:

种类签名特征
默认构造无参T()
有参构造T(args)
拷贝构造T(const T&)
移动构造T(T&&)
structWidget{Widget();// 默认构造Widget(intn);// 有参构造Widget(constWidget&);// 拷贝构造Widget(Widget&&);// 移动构造};

⚠️注意:一旦你写了任何一个构造函数,编译器就不再为你合成默认构造了。想保留默认构造,得显式写Widget() = default;


三、构造与析构的执行顺序(高频考点)

❓ 多层继承、成员对象都有构造析构时,到底按什么顺序执行?

✅ 记住一条铁律:

  • 构造:基类先 → 成员对象次 → 自己的函数体最后;
  • 析构:完全相反,函数体先 → 成员次 → 基类最后。
structMember{Member(){cout<<"Member 构造\n";}~Member(){cout<<"Member 析构\n";}};structBase{Base(){cout<<"Base 构造\n";}~Base(){cout<<"Base 析构\n";}};structMid:Base{// 中间层Mid(){cout<<"Mid 构造\n";}~Mid(){cout<<"Mid 析构\n";}};structDer:Mid{Member m_;// 成员对象Der(){cout<<"Der 构造\n";}~Der(){cout<<"Der 析构\n";}};
{Der d;}// 输出顺序:// Base 构造// Mid 构造// Member 构造// Der 构造// Der 析构// Member 析构// Mid 析构// Base 析构

💡记忆窍门:构造像"穿衣"(内衣→外套,从里到外),析构像"脱衣"(外套→内衣,从外到里)。派生类永远裹在基类外面,所以构造最后到位、析构最先脱掉。


四、为什么基类析构要 virtual?(重头戏)

❓ 为什么总有人说"基类析构函数必须声明为 virtual"?

✅ 因为通过基类指针 delete 派生类对象时,只有基类析构是 virtual,才会触发派生类析构——否则只调基类析构,派生部分直接泄漏。

看这个经典翻车现场:

structBase{~Base(){}// ❌ 非 virtual};structDer:Base{int*p_;Der():p_(newint[100]){}~Der(){delete[]p_;}// 不会执行!};
Base*b=newDer;deleteb;// ❌ 只调 Base::~Base()// Der::~Der() 没被调用// 100 个 int 泄漏

加上virtual立刻修好:

structBase{virtual~Base(){}// ✅ 虚析构};// 现在 delete b 会先调 Der::~Der()// 再调 Base::~Base(),顺序正确

⚠️判断准则只要这个类打算被多态使用(即有人会用基类指针/引用指向派生对象),它的析构就必须 virtual。反过来,如果一个类不打算被继承(或只用具体类型操作),析构非 virtual 即可,省掉虚表开销。

💡加分点:C++ 核心准则(C.35)的建议更干脆——一个要有虚函数的类,析构基本上就该是 virtual。因为"有虚函数"本身就是"打算被多态使用"的信号。这条几乎可以当公式记。


五、构造/析构里能调虚函数吗?

❓ 在构造函数或析构函数里调用虚函数,会发生什么?

能编译通过,但行为和你想的"多态"不一样——构造/析构期间,虚调用会退化成静态调用(只调当前层自己的版本,不会进入派生类)。

structBase{Base(){init();}// ⚠ 调虚函数virtualvoidinit(){cout<<"Base::init";}};structDer:Base{voidinit()override{cout<<"Der::init";}};
Der d;// 你以为打印 Der::init// 其实打印 Base::init !

为什么?构造Der时,先执行Base的构造体——此时Der还没"成形",它的成员还没初始化。如果这时候真的去调Der::init,访问到的是一堆垃圾。所以语言规定:基类构造期间,对象的实际类型就是基类,虚函数只能落到基类版本。析构同理(拆除顺序里,派生部分已经没了)。

🎯最佳实践别在构造/析构里调虚函数,那是 bug 的温床。如果非要在构造期做"派生类相关的初始化",让派生类自己构造函数里调一次普通虚函数即可——那时对象已完整。

虚函数到底是怎么"知道"该调哪个版本的?答案是虚函数表(vtable)——这是虚函数那道题的核心,本文先点到为止。


六、= default= delete(C++11)

= default= delete是干嘛的?

✅ 两个 C++11 引入的"显式控制特殊成员函数"的语法:

structWidget{Widget()=default;// 显式要默认构造Widget(constWidget&)=delete;// 禁止拷贝};
  • = default:让编译器重新合成那个被你写别的构造函数"压制"掉的默认实现;
  • = delete明确禁用某个函数,任何人调用都编译报错。
Widget w1;// ✅Widgetw2(w1);// ❌ 编译报错:已删除Widget w3=w1;// ❌ 同上

💡典型场景:不可拷贝的类(如std::mutexstd::unique_ptr)就是把拷贝构造/拷贝赋值= delete。比 C++98 时代"声明成 private 且不实现"干净得多。


七、成员初始化列表:为什么要用?

❓ 为什么推荐用初始化列表,而不是在构造体里赋值?

✅ 因为初始化列表是"直接构造成员",而在构造体里x = 1是"先默认构造、再赋值"——对成员多了一次默认构造开销,而且有些类型(const、引用、无默认构造的类)只能初始化、不能赋值

structBad{intn;string s;Bad(intx,string str){n=x;// 先默认构造再赋值s=str;// 多一次临时对象}};structGood{intn;string s;Good(intx,string str):n(x),s(str){}// ✅ 直接构造};

⚠️初始化顺序陷阱成员按声明顺序初始化,跟列表里写的顺序无关。如果成员间有依赖,搞错声明顺序会用到没初始化的成员:

structTrap{inta_,b_;// 想让 b = a + 1Trap(intv):b_(v+1),a_(v){}// ⚠ 危险!// 实际顺序:a_ 先(=v),b_ 后(=v+1)// 这里碰巧对,但若写成 b_(a_+1)// a_ 还是垃圾值!};

🎯建议成员声明顺序和初始化列表顺序保持一致,开-Wreorder警告(GCC/Clang 默认开)帮你抓这种坑。


八、面试高频追问

❓ Q1:构造函数能是虚函数吗?

不能。虚函数靠虚表工作,而虚表指针是在构造期间才被填进去的——构造函数执行时对象还没"活",没有虚表可用。语言层面直接禁止 virtual 构造函数。

❓ Q2:析构函数能是纯虚函数吗?

(常用来定义抽象类),但必须给纯虚析构提供一份定义,否则链接报错——派生类析构时会调用基类析构,找不到实现就炸。

structBase{virtual~Base()=0;// 纯虚析构};Base::~Base(){}// ✅ 必须有定义

❓ Q3:编译器什么时候会合成默认构造函数?

✅ 仅当你需要它、又没自己写时(比如定义了对象却没给初值)。如果类里有成员本身没有默认构造,合成会失败。别把"会合成"当默认保证——需要就显式写。

❓ Q4:为什么析构函数执行后对象就"不能用了"?

✅ 析构一旦执行,对象的成员已被销毁、资源已归还,再访问就是未定义行为(use after free)。这也是为什么不要返回局部对象的引用/指针——对象早就没了。

❓ Q5:构造抛异常会怎样?

已构造的成员和基类子对象会被逆序析构,但对象自己的析构不会执行(它根本没构造完成)。所以"在构造里申请多个资源"很危险,推荐用智能指针成员托管——每个成员自己 RAII,异常时自动释放,这也是内存管理的核心思想。


九、总结速查表

考点一句话结论
构造顺序基类 → 成员 → 函数体
析构顺序函数体 → 成员 → 基类(相反)
多态基类析构必须 virtual,否则派生部分泄漏
构造期调虚函数退化为静态调用,别这么干
= default重新要编译器合成
= delete明确禁用某函数
初始化列表直接构造,省开销;顺序按声明
纯虚析构可以,但必须给定义

一句话回顾

构造是"基类→成员→自己",析构恰好反过来;只要类会被多态使用,基类析构就得 virtual——这两条记牢,构造析构这一关基本就过了。

如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们继续聊面向对象篇的拷贝构造与拷贝赋值——浅拷贝为什么会 double free、三五法则到底在说什么,敬请关注 👋

← 返回列表