构造函数与析构函数:面试官问“对象创建时发生了什么”,很多人答不上来
上篇聊了类与对象的基本概念,今天深入一步——对象的生命周期。
构造函数和析构函数,是C++里最容易被低估的两个概念。说它容易,是因为语法简单;说它容易被低估,是因为很多人用了很久,对底层的调用时机和顺序还是模模糊糊。
面试的时候,我特别喜欢问构造和析构相关的问题。不是考语法,而是考你对对象生命周期的理解。
构造函数:对象出生时做什么
构造函数的任务是在对象创建的时候,把成员变量初始化到合法的状态。
class Sensor {
double value_;
bool is_active_;
string name_;
public:
Sensor() : value_(0.0), is_active_(false), name_("unknown") {
cout << "Sensor created" << endl;
}
};
Sensor s; // 构造函数被调用
构造函数有几个要点:
第一,构造函数名和类名相同,没有返回类型。
第二,构造函数可以重载。不同的参数列表,对应不同的初始化方式。
class Sensor {
double value_;
string name_;
public:
Sensor() : value_(0.0), name_("unknown") {} // 默认构造
Sensor(double v) : value_(v), name_("sensor") {} // 一个参数
Sensor(double v, const string& n) : value_(v), name_(n) {} // 两个参数
};
Sensor s1; // 调用默认构造
Sensor s2(3.14); // 调用一个参数构造
Sensor s3(3.14, "lidar"); // 调用两个参数构造
第三,初始化列表比构造函数体内赋值更高效。对于像string这样的复杂类型,初始化列表直接构造,而体内赋值要先默认构造再赋值,多了一次操作。
在机器人开发里,传感器对象的初始化特别重要。如果构造函数里没把状态初始化好,后面读到的数据可能是垃圾值,导致整个系统出问题。
析构函数:对象死亡时做什么
析构函数的任务是在对象销毁的时候,做一些清理工作。比如释放动态分配的内存、关闭文件句柄、断开硬件连接。
class Sensor {
double* data_buffer_;
public:
Sensor() : data_buffer_(new double[1000]) {}
~Sensor() {
delete[] data_buffer_; // 释放内存
cout << "Sensor destroyed" << endl;
}
};
析构函数没有参数,不能重载,每个类只有一个析构函数。
在机器人开发里,析构函数特别关键。因为机器人系统里有很多硬件资源——传感器连接、串口、内存缓冲区。如果对象销毁时没有正确释放这些资源,就会出现资源泄漏。
我见过一个案例:一个团队写的机器人程序,跑着跑着内存越来越大,最后OOM崩了。排查了两天,发现是一个传感器对象在vector里被删除时,析构函数没有释放内部的缓冲区。
构造和析构的调用顺序
面试高频考点:对象的构造和析构顺序。
规则很简单:先构造的后析构,后构造的先析构。就像叠盘子,最后放上去的最先拿走。
class Sensor {
string name_;
public:
Sensor(const string& n) : name_(n) {
cout << name_ << " created" << endl;
}
~Sensor() {
cout << name_ << " destroyed" << endl;
}
};
void test() {
Sensor s1("lidar");
Sensor s2("camera");
Sensor s3("imu");
}
// 输出:
// lidar created
// camera created
// imu created
// imu destroyed
// camera destroyed
// lidar destroyed
局部对象的析构顺序和构造顺序相反。这个在面试里经常考。
还有个容易忽略的点:成员变量的构造顺序。成员变量按照声明顺序构造,不是按照初始化列表的顺序。
class Sensor {
string name_; // 先构造
double value_; // 后构造
public:
// 即使初始化列表里value_写在前面,name_还是先构造
Sensor() : value_(0.0), name_("sensor") {}
};
这个顺序问题在面试里是个陷阱题。很多人以为初始化列表的顺序就是构造顺序,其实不是。
默认构造函数
如果你没有定义任何构造函数,编译器会自动生成一个默认构造函数。但如果你定义了任何构造函数(哪怕是有参数的),编译器就不会再自动生成默认构造函数了。
class Sensor {
double value_;
public:
Sensor(double v) : value_(v) {} // 定义了有参构造
};
Sensor s; // 编译错误!没有默认构造函数
C++11之后,可以用= default显式要求编译器生成默认构造函数:
class Sensor {
double value_;
public:
Sensor() = default; // 显式要求默认构造
Sensor(double v) : value_(v) {}
};
在机器人开发里,很多框架要求类必须有默认构造函数,因为容器(比如vector)在扩容时需要默认构造。如果你忘了定义,编译就过不了。
委托构造和继承构造
C++11引入了两个实用的特性:
委托构造——一个构造函数调用另一个构造函数:
class Sensor {
double value_;
string name_;
public:
Sensor(double v, const string& n) : value_(v), name_(n) {}
Sensor() : Sensor(0.0, "unknown") {} // 委托给上面的构造函数
};
继承构造——子类继承父类的构造函数:
class BaseSensor {
public:
BaseSensor(double v) { /* ... */ }
};
class LidarSensor : public BaseSensor {
public:
using BaseSensor::BaseSensor; // 继承父类构造函数
};
LidarSensor ls(3.14); // 调用继承来的构造函数
这两个特性在实际项目里能减少不少重复代码。
面试中的构造析构综合题
面试的时候,面试官最爱考的是构造和析构的调用时机:
class Sensor {
public:
Sensor() { cout << "constructed" << endl; }
~Sensor() { cout << "destroyed" << endl; }
};
Sensor global_sensor; // 程序启动时构造,结束时析构
void test() {
static Sensor static_sensor; // 第一次调用时构造,程序结束时析构
Sensor local_sensor; // 每次调用都构造,函数结束时析构
}
这三种对象的构造和析构时机完全不同。全局对象在main之前构造,静态局部对象在第一次执行到声明语句时构造,局部对象每次进入作用域都构造。
如果面试官追问:"如果构造函数里抛出异常会怎样?"答案是:析构函数不会被调用。因为对象还没构造完成。这就是为什么构造函数里要慎用异常,尤其是涉及资源分配的时候。
给正在准备面试的你一点建议
构造函数和析构函数,面试考的不只是语法,更是你对资源管理的意识。
我面试的时候,看候选人写类,第一眼看构造函数——有没有正确初始化所有成员变量?第二眼看析构函数——有没有释放分配的资源?
如果这两点都做对了,说明这个人有基本的工程素养和代码规范意识。如果构造函数里什么都没初始化,或者析构函数里没释放资源,那我会觉得这个人写代码比较随意,在机器人这种对稳定性要求高的领域,这种习惯是很危险的,出了线上事故排查起来也非常麻烦,代价很大的,得不偿失。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第18篇 类与对象——用机器人传感器来理解封装 下一篇预告:第20篇 拷贝构造与赋值——浅拷贝vs深拷贝的坑
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)