欢迎光临
我们一直在努力

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

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

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


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

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

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

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

struct File {
File(const char* path); // 构造:打开文件
~File(); // 析构:关闭文件
private:
FILE* fp_;
};

{
File f("a.txt"); // 构造自动调用
// … 用 f
} // 离开作用域,析构自动调用

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


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

❓ 构造函数分几种?

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

种类签名特征
默认构造 无参 T()
有参构造 T(args)
拷贝构造 T(const T&)
移动构造 T(T&&)

struct Widget {
Widget(); // 默认构造
Widget(int n); // 有参构造
Widget(const Widget&); // 拷贝构造
Widget(Widget&&); // 移动构造
};

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


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

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

✅ 记住一条铁律:

  • 构造:基类先 → 成员对象次 → 自己的函数体最后;
  • 析构:完全相反,函数体先 → 成员次 → 基类最后。

struct Member {
Member() { cout << "Member 构造\\n"; }
~Member() { cout << "Member 析构\\n"; }
};
struct Base {
Base() { cout << "Base 构造\\n"; }
~Base() { cout << "Base 析构\\n"; }
};
struct Mid : Base { // 中间层
Mid() { cout << "Mid 构造\\n"; }
~Mid() { cout << "Mid 析构\\n"; }
};
struct Der : 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,才会触发派生类析构——否则只调基类析构,派生部分直接泄漏。

看这个经典翻车现场:

struct Base {
~Base() { } // ❌ 非 virtual
};
struct Der : Base {
int* p_;
Der() : p_(new int[100]) {}
~Der() { delete[] p_; } // 不会执行!
};

Base* b = new Der;
delete b;
// ❌ 只调 Base::~Base()
// Der::~Der() 没被调用
// 100 个 int 泄漏

加上 virtual 立刻修好:

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

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

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


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

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

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

struct Base {
Base() { init(); } // ⚠ 调虚函数
virtual void init() {
cout << "Base::init";
}
};
struct Der : Base {
void init() 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 引入的"显式控制特殊成员函数"的语法:

struct Widget {
Widget() = default; // 显式要默认构造
Widget(const Widget&) = delete; // 禁止拷贝
};

  • = default:让编译器重新合成那个被你写别的构造函数"压制"掉的默认实现;
  • = delete:明确禁用某个函数,任何人调用都编译报错。

Widget w1; // ✅
Widget w2(w1); // ❌ 编译报错:已删除
Widget w3 = w1; // ❌ 同上

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


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

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

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

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

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

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

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


八、面试高频追问

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

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

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

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

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

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

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

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

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

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

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


九、总结速查表

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

一句话回顾

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

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

赞(0)
未经允许不得转载:171主机测评 » 【C++ 面试真题】聊聊 C++ 的构造与析构
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址