一、什么是对象生命周期对象生命周期可以简单理解为对象从创建开始 经过初始化、使用 最终被销毁和释放资源的整个过程。在 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年龄202. 拷贝构造函数常见调用时机拷贝构造函数常见调用时机包括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