【C++面试高频】构造函数、析构函数、拷贝构造和对象生命周期总结

一、什么是对象生命周期?

对象生命周期可以简单理解为:

复制代码
对象从创建开始,
经过初始化、使用,
最终被销毁和释放资源的整个过程。

在 C++ 中,一个对象通常会经历下面几个阶段:

复制代码
1. 分配内存
2. 调用构造函数,完成初始化
3. 对象被使用
4. 调用析构函数,释放资源
5. 对象占用的内存被回收

例如:

复制代码
#include <iostream>
using namespace std;

class Student {
public:
    Student() {
        cout << "Student 构造函数" << endl;
    }

    ~Student() {
        cout << "Student 析构函数" << endl;
    }
};

int main() {
    Student stu;

    cout << "正在使用 stu 对象" << endl;

    return 0;
}

输出结果大致是:

复制代码
Student 构造函数
正在使用 stu 对象
Student 析构函数

这里的 stu 是局部对象。

当程序执行到:

复制代码
Student stu;

时,会调用构造函数。

main() 函数结束时,stu 离开作用域,会自动调用析构函数。


二、构造函数

构造函数是在对象创建时自动调用的特殊成员函数,主要作用是初始化对象成员。

构造函数特点:

复制代码
1. 函数名和类名相同。
2. 没有返回值类型。
3. 对象创建时自动调用。
4. 一个类可以有多个构造函数。

三、默认构造函数和带参数构造函数

1. 默认构造函数

默认构造函数指不需要传入参数就可以调用的构造函数。

复制代码
#include <iostream>
using namespace std;

class Student {
public:
    Student() {
        cout << "调用默认构造函数" << endl;
    }
};

int main() {
    // 创建对象时自动调用默认构造函数
    Student stu;

    return 0;
}

这里:

复制代码
Student stu;

会自动调用:

复制代码
Student();

2. 带参数构造函数

带参数构造函数可以在创建对象时传入初始数据。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int age;

public:
    // 带参数构造函数
    Student(const string& n, int a) {
        name = n;
        age = a;

        cout << "调用带参数构造函数" << endl;
    }

    void print() const {
        cout << "姓名:" << name
             << ",年龄:" << age << endl;
    }
};

int main() {
    // 创建对象时传入姓名和年龄
    Student stu("Tom", 20);

    stu.print();

    return 0;
}

输出结果:

复制代码
调用带参数构造函数
姓名:Tom,年龄:20

四、构造函数初始化列表

构造函数除了可以在函数体中给成员变量赋值,也可以使用初始化列表。

例如:

复制代码
#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int age;

public:
    // 使用初始化列表初始化成员变量
    Student(const string& n, int a)
        : name(n), age(a) {
        cout << "调用构造函数" << endl;
    }

    void print() const {
        cout << "姓名:" << name
             << ",年龄:" << age << endl;
    }
};

int main() {
    Student stu("Jack", 22);

    stu.print();

    return 0;
}

其中:

复制代码
: name(n), age(a)

就是构造函数初始化列表。

它表示对象创建时,直接使用 n 初始化 name,使用 a 初始化 age


1. 为什么推荐使用初始化列表?

初始化列表的优点主要有:

复制代码
1. 写法更清晰。
2. 对某些成员变量必须使用初始化列表。
3. 通常比先默认构造、再赋值更高效。

例如下面这些成员通常必须通过初始化列表初始化:

复制代码
const 成员变量
引用成员变量
没有默认构造函数的成员对象

示例:

复制代码
#include <iostream>
using namespace std;

class Test {
private:
    const int value;
    int& ref;

public:
    // const 成员和引用成员必须在初始化列表中初始化
    Test(int v, int& r)
        : value(v), ref(r) {
    }

    void print() const {
        cout << "value = " << value << endl;
        cout << "ref = " << ref << endl;
    }
};

int main() {
    int num = 100;

    Test t(10, num);

    t.print();

    return 0;
}

2. 成员变量初始化顺序要注意什么?

成员变量的实际初始化顺序,不是由初始化列表的书写顺序决定的,而是由成员变量在类中声明的顺序决定的。

例如:

复制代码
#include <iostream>
using namespace std;

class Test {
private:
    int a;
    int b;

public:
    // 虽然这里写的是 b(a),a(10)
    // 但实际初始化顺序仍然是先 a,再 b
    Test()
        : b(a), a(10) {
    }

    void print() const {
        cout << "a = " << a << endl;
        cout << "b = " << b << endl;
    }
};

int main() {
    Test t;

    t.print();

    return 0;
}

上面的写法不推荐,因为 b(a) 执行时,a 还没有完成初始化,容易出现问题。

正确写法应该和成员声明顺序一致:

复制代码
Test()
    : a(10), b(a) {
}

面试时可以这样回答:

成员变量的初始化顺序由它们在类中声明的顺序决定,而不是初始化列表中的书写顺序。为了避免成员使用未初始化数据,初始化列表的顺序最好和成员声明顺序保持一致。


五、析构函数

析构函数是在对象销毁时自动调用的特殊成员函数,主要作用是释放对象占用的资源。

析构函数特点:

复制代码
1. 函数名是在类名前加 ~。
2. 没有返回值。
3. 没有参数。
4. 一个类只能有一个析构函数。
5. 对象销毁时自动调用。

1. 析构函数基本示例

复制代码
#include <iostream>
using namespace std;

class Student {
public:
    Student() {
        cout << "Student 构造函数" << endl;
    }

    ~Student() {
        cout << "Student 析构函数" << endl;
    }
};

int main() {
    {
        Student stu;

        cout << "stu 正在作用域内使用" << endl;
    }

    // stu 离开花括号作用域后,自动调用析构函数
    cout << "stu 已经销毁" << endl;

    return 0;
}

输出结果大致为:

复制代码
Student 构造函数
stu 正在作用域内使用
Student 析构函数
stu 已经销毁

这说明:局部对象离开作用域时,会自动调用析构函数。


六、栈对象和堆对象的生命周期

C++ 中常见对象创建方式有两种:

复制代码
栈上创建对象
堆上创建对象

1. 栈对象

栈对象一般是普通局部对象。

复制代码
#include <iostream>
using namespace std;

class Test {
public:
    Test() {
        cout << "Test 构造函数" << endl;
    }

    ~Test() {
        cout << "Test 析构函数" << endl;
    }
};

void func() {
    // t 是栈对象
    Test t;

    cout << "func 函数中正在使用 t" << endl;

    // func 结束后,t 自动析构
}

int main() {
    func();

    return 0;
}

这里的 t 是栈对象。

特点是:

复制代码
创建时自动调用构造函数。
离开作用域时自动调用析构函数。
不需要手动 delete。

2. 堆对象

堆对象通常通过 new 创建。

复制代码
#include <iostream>
using namespace std;

class Test {
public:
    Test() {
        cout << "Test 构造函数" << endl;
    }

    ~Test() {
        cout << "Test 析构函数" << endl;
    }
};

int main() {
    // 在堆区创建对象
    Test* p = new Test();

    cout << "正在使用堆对象" << endl;

    // 手动释放堆对象
    delete p;

    // 避免悬空指针
    p = nullptr;

    return 0;
}

特点是:

复制代码
new 时调用构造函数。
delete 时调用析构函数。
如果忘记 delete,可能造成内存泄漏。

面试时可以这样回答:

栈对象由系统自动管理,离开作用域时会自动调用析构函数。堆对象通过 new 创建,需要通过 delete 手动释放,delete 时会调用析构函数。如果只 new 不 delete,就可能产生内存泄漏。


七、拷贝构造函数

拷贝构造函数用于:使用一个已经存在的对象来创建一个新对象。

常见形式:

复制代码
ClassName(const ClassName& other);

1. 拷贝构造函数示例

复制代码
#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int age;

public:
    // 普通构造函数
    Student(const string& n, int a)
        : name(n), age(a) {
        cout << "普通构造函数" << endl;
    }

    // 拷贝构造函数
    Student(const Student& other)
        : name(other.name), age(other.age) {
        cout << "拷贝构造函数" << endl;
    }

    void print() const {
        cout << "姓名:" << name
             << ",年龄:" << age << endl;
    }
};

int main() {
    Student s1("Tom", 20);

    // 使用 s1 创建 s2,调用拷贝构造函数
    Student s2(s1);

    s2.print();

    return 0;
}

输出结果:

复制代码
普通构造函数
拷贝构造函数
姓名:Tom,年龄:20

2. 拷贝构造函数常见调用时机

拷贝构造函数常见调用时机包括:

复制代码
1. 用一个对象创建新对象。
2. 按值传递对象参数。
3. 函数按值返回对象时,可能调用拷贝构造或移动构造。
情况一:用已有对象创建新对象
复制代码
Student s1("Tom", 20);

// 调用拷贝构造函数
Student s2(s1);
情况二:按值传参
复制代码
#include <iostream>
using namespace std;

class Test {
public:
    Test() {
        cout << "普通构造函数" << endl;
    }

    Test(const Test& other) {
        cout << "拷贝构造函数" << endl;
    }
};

void show(Test t) {
    cout << "进入 show 函数" << endl;
}

int main() {
    Test t;

    // 按值传参,可能触发拷贝构造
    show(t);

    return 0;
}

因此,如果对象比较大,函数参数通常建议写成:

复制代码
const Test& t

这样可以避免不必要的对象拷贝。


3. 返回对象时一定会调用拷贝构造吗?

不一定。

例如:

复制代码
#include <iostream>
using namespace std;

class Test {
public:
    Test() {
        cout << "普通构造函数" << endl;
    }

    Test(const Test& other) {
        cout << "拷贝构造函数" << endl;
    }
};

Test createTest() {
    Test t;

    return t;
}

int main() {
    Test result = createTest();

    return 0;
}

从概念上说,函数返回对象可能涉及拷贝构造或移动构造。

但是现代 C++ 编译器通常会进行返回值优化,也就是 RVO 或 NRVO,直接在目标位置构造对象,从而省略不必要的拷贝。

所以实际运行时,你可能看不到拷贝构造函数输出。

面试时可以这样回答:

函数按值返回对象时,从语义上可能涉及拷贝构造或移动构造,但现代编译器通常会进行返回值优化,减少甚至省略对象拷贝。


八、构造函数和析构函数调用顺序

1. 同一作用域中局部对象的构造和析构顺序

对象构造顺序是按照定义顺序进行。

对象析构顺序与构造顺序相反。

复制代码
#include <iostream>
using namespace std;

class Test {
private:
    string name;

public:
    Test(const string& n)
        : name(n) {
        cout << name << " 构造" << endl;
    }

    ~Test() {
        cout << name << " 析构" << endl;
    }
};

int main() {
    Test t1("t1");
    Test t2("t2");
    Test t3("t3");

    return 0;
}

输出结果:

复制代码
t1 构造
t2 构造
t3 构造
t3 析构
t2 析构
t1 析构

可以简单记忆:

复制代码
构造:先创建的先构造。
析构:后创建的先析构。

2. 类成员对象的构造和析构顺序

如果一个类中包含其他类对象作为成员,那么创建外部对象时,会先构造成员对象,再执行当前类构造函数体。

销毁时则相反:先执行当前类析构函数体,再析构成员对象。

复制代码
#include <iostream>
using namespace std;

class Engine {
public:
    Engine() {
        cout << "Engine 构造" << endl;
    }

    ~Engine() {
        cout << "Engine 析构" << endl;
    }
};

class Car {
private:
    // Engine 是 Car 的成员对象
    Engine engine;

public:
    Car() {
        cout << "Car 构造" << endl;
    }

    ~Car() {
        cout << "Car 析构" << endl;
    }
};

int main() {
    Car car;

    return 0;
}

输出结果:

复制代码
Engine 构造
Car 构造
Car 析构
Engine 析构

可以这样记忆:

复制代码
成员对象构造:先成员,后当前类。
成员对象析构:先当前类,后成员。

3. 父类和子类的构造、析构顺序

当子类继承父类时,创建子类对象需要先构造父类部分,再构造子类部分。

销毁子类对象时,先析构子类部分,再析构父类部分。

复制代码
#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base 构造" << endl;
    }

    ~Base() {
        cout << "Base 析构" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived 构造" << endl;
    }

    ~Derived() {
        cout << "Derived 析构" << endl;
    }
};

int main() {
    Derived d;

    return 0;
}

输出结果:

复制代码
Base 构造
Derived 构造
Derived 析构
Base 析构

简单记忆:

复制代码
构造:先父后子。
析构:先子后父。

如果基类可能通过父类指针删除子类对象,基类析构函数应声明为 virtual,避免只调用父类析构函数。


九、静态对象和全局对象的生命周期

1. 静态局部对象

静态局部对象只会初始化一次,生命周期直到程序结束。

复制代码
#include <iostream>
using namespace std;

class Test {
public:
    Test() {
        cout << "Test 构造" << endl;
    }

    ~Test() {
        cout << "Test 析构" << endl;
    }
};

void func() {
    // 第一次执行 func 时创建
    // 之后再次进入 func,不会重复构造
    static Test t;

    cout << "执行 func" << endl;
}

int main() {
    func();
    func();

    return 0;
}

输出结果大致为:

复制代码
Test 构造
执行 func
执行 func
Test 析构

这里的 t 在程序结束时才析构。


2. 全局对象

定义在所有函数外部的对象叫全局对象。

复制代码
#include <iostream>
using namespace std;

class Test {
public:
    Test() {
        cout << "全局对象构造" << endl;
    }

    ~Test() {
        cout << "全局对象析构" << endl;
    }
};

// 全局对象
Test globalTest;

int main() {
    cout << "进入 main 函数" << endl;

    return 0;
}

一般来说:

复制代码
程序启动时,全局对象会先构造。
程序结束时,全局对象会析构。

实际工程中,不建议过度依赖全局对象,因为多个全局对象之间的初始化顺序可能带来复杂问题。


十、构造函数和析构函数的常见注意点

1. 构造函数可以是虚函数吗?

构造函数不能是虚函数。

因为对象还在构造过程中,对象的完整类型和虚函数机制还没有完全建立,无法通过虚函数机制实现多态调用。

面试时可以这样回答:

构造函数不能是虚函数。因为虚函数依赖对象内部的虚函数表指针,而对象在构造阶段还没有完全构造完成,无法安全地通过虚函数机制调用构造函数。


2. 析构函数为什么可以是虚函数?

析构函数可以是虚函数。

如果一个类作为基类使用,并且可能通过基类指针删除派生类对象,那么基类析构函数应该写成虚函数。

复制代码
class Base {
public:
    virtual ~Base() {
    }
};

这样:

复制代码
Base* p = new Derived();
delete p;

才能先正确调用 Derived 的析构函数,再调用 Base 的析构函数。


3. 构造函数中能调用虚函数吗?

语法上可以调用,但通常不建议依赖多态行为。

因为在基类构造函数执行时,派生类部分还没有构造完成。此时调用虚函数,通常只会调用当前构造阶段对应类的版本,而不会表现出预期的派生类多态行为。

析构函数中调用虚函数也有类似问题。

因此,一般不建议在构造函数和析构函数中依赖虚函数实现多态逻辑。


十一、面试高频问题整理

1. 构造函数和析构函数分别有什么作用?

构造函数在对象创建时自动调用,主要用于初始化成员变量和申请资源。

析构函数在对象销毁时自动调用,主要用于释放对象占用的资源,例如动态内存、文件句柄、锁或网络连接等。


2. 构造函数可以重载吗?析构函数可以重载吗?

构造函数可以重载,因为一个类可以有多个不同参数列表的构造函数。

析构函数不能重载,因为一个类只能有一个析构函数,并且析构函数没有参数。


3. 拷贝构造函数什么时候调用?

常见情况包括:

复制代码
使用已有对象创建新对象。
对象按值传递给函数参数。
函数按值返回对象时,可能发生拷贝或移动。

不过现代编译器通常会进行返回值优化,减少不必要的拷贝。


4. 为什么拷贝构造函数参数通常写成 const 引用?

常见写法是:

复制代码
ClassName(const ClassName& other);

使用引用可以避免传参时再次复制对象。

使用 const 可以保证不会修改原对象,并且允许传入常量对象。


5. 栈对象和堆对象有什么区别?

栈对象通常是局部对象,离开作用域时会自动析构和释放。

堆对象通过 new 创建,需要通过 delete 手动释放。忘记 delete 可能导致内存泄漏。


6. 对象构造和析构顺序是什么?

局部对象按照定义顺序构造,按照相反顺序析构。

成员对象先构造,再执行当前类构造函数;析构时先执行当前类析构函数,再析构成员对象。

继承关系中,构造时先父类后子类;析构时先子类后父类。


7. 为什么基类析构函数通常要写成 virtual?

因为可能通过基类指针删除派生类对象。

如果基类析构函数不是虚函数,delete 时可能只调用基类析构函数,而派生类资源无法正确释放。


十二、总结

构造函数、析构函数和对象生命周期是 C++ 面试中的基础高频内容。

构造函数负责对象创建时的初始化,析构函数负责对象销毁时的资源释放。

局部对象一般创建在栈区,离开作用域后自动析构。通过 new 创建的堆对象需要手动 delete,否则可能造成内存泄漏。

拷贝构造函数用于用已有对象创建新对象。对象按值传参和按值返回时,也可能涉及拷贝构造或移动构造,但现代编译器通常会优化掉不必要的拷贝。

对象构造和析构顺序需要重点记忆:

复制代码
同一作用域:
构造按定义顺序,析构按相反顺序。

成员对象:
构造先成员后当前类,析构先当前类后成员。

继承关系:
构造先父后子,析构先子后父。

最后可以简单记忆:

复制代码
构造函数:对象出生时初始化。
析构函数:对象销毁时释放资源。
栈对象:离开作用域自动销毁。
堆对象:需要 delete 手动销毁。
拷贝构造:用旧对象创建新对象。
初始化列表:对象创建时直接初始化成员。
构造:先父后子、先成员后当前类。
析构:先子后父、先当前类后成员。

0voice · GitHub