上篇聊了类与对象的基本概念,今天深入一步——对象的生命周期。

构造函数和析构函数,是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深拷贝的坑

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐