C++对象构造与析构顺序详解:从原理到RAII实战避坑指南

发布时间:2026/8/7 2:37:04
C++对象构造与析构顺序详解:从原理到RAII实战避坑指南 1. 项目概述为什么构造与析构顺序是C进阶的基石在C的世界里对象的生命周期管理是区分新手与资深开发者的关键分水岭。很多朋友在掌握了类、继承、多态这些基础概念后写起代码来依然会碰到一些“灵异事件”比如基类的成员变量访问异常或者在对象销毁时程序莫名其妙地崩溃。这些问题十有八九都跟对象的构造和析构顺序有关。今天我们就来彻底拆解这个在面试和实际项目中都绕不开的经典话题——C中对象的构造与析构顺序。简单来说构造顺序决定了对象从“无”到“有”时其各个部分基类子对象、成员变量、自身构造函数体是如何一步步搭建起来的而析构顺序则恰恰相反它规定了对象从“有”到“无”时如何安全、有序地拆除这个结构。这个顺序不是随机的而是由C语言标准严格定义的。理解它你就能预判代码的行为写出更健壮、更安全的程序不理解它你的代码就可能埋下难以察觉的定时炸弹。这个主题尤其适合那些已经熟悉C基本语法开始接触复杂类设计、继承体系和资源管理的开发者。无论是开发游戏引擎、高性能服务器还是嵌入式系统清晰的生命周期管理都是保障稳定性的前提。接下来我将结合具体的代码实例带你从原理到实践彻底掌握构造与析构的来龙去脉。2. 构造顺序的深度解析对象是如何“搭建”起来的对象的构造过程远比一句MyClass obj;要复杂。它是一场精心编排的“建筑仪式”遵循着严格的步骤。理解这个顺序是理解后续一切行为的基础。2.1 构造顺序的核心规则C中一个派生类对象的构造顺序是确定且不可更改的遵循以下自顶向下、由内而外的原则虚拟基类构造如果存在按照它们在继承关系图中深度优先、从左到右的顺序进行初始化。这是最先发生的步骤确保了虚基类子对象在整个继承体系中只被构造一次。直接基类构造按照它们在派生类定义中声明的顺序从左到右进行构造与它们在初始化列表中的顺序无关。非静态成员变量构造按照它们在类定义中声明的顺序进行构造同样与初始化列表中的顺序无关。派生类自身的构造函数体执行最后才执行派生类构造函数{}花括号内的代码。这个顺序可以形象地理解为先打好最底层、最共享的地基虚基类然后搭建每一层的承重结构直接基类接着砌墙和安装内部设施成员变量最后进行内部装修和布置构造函数体。注意这里有一个极其重要的陷阱。很多开发者误以为初始化列表:后面的顺序决定了构造顺序这是完全错误的。编译器只认类定义中的声明顺序。错误地依赖初始化列表顺序会导致一些成员变量在初始化时引用了尚未被初始化的其他成员引发未定义行为。2.2 单继承场景下的构造顺序实例让我们从一个相对简单的单继承例子开始直观地感受这个顺序。#include iostream class Base { public: Base() { std::cout Base constructor called. std::endl; } ~Base() { std::cout Base destructor called. std::endl; } }; class Member { public: Member() { std::cout Member constructor called. std::endl; } ~Member() { std::cout Member destructor called. std::endl; } }; class Derived : public Base { private: Member mem; // 成员对象 int* data; public: Derived() : data(new int(42)) { // 初始化列表但mem的构造顺序由声明决定 std::cout Derived constructor body called. std::endl; } ~Derived() { delete data; std::cout Derived destructor body called. std::endl; } }; int main() { std::cout Creating Derived object... std::endl; Derived d; std::cout \nDerived object about to go out of scope... std::endl; return 0; }运行这段代码输出结果将是Creating Derived object... Base constructor called. Member constructor called. Derived constructor body called. Derived object about to go out of scope... Derived destructor body called. Member destructor called. Base destructor called.输出分析构造顺序正如规则所述先构造基类Base然后构造成员对象Member最后执行Derived的构造函数体。data的初始化new int发生在初始化列表阶段这个阶段在构造函数体执行之前但晚于mem的构造。析构顺序与构造顺序完全相反。先执行Derived的析构函数体释放data内存然后析构成员mem最后析构基类Base。这个“后构造的先析构”的栈式LIFO行为是资源安全释放的关键。2.3 多继承与虚拟继承的复杂场景当引入多继承和虚拟继承时顺序会变得更加复杂但也更有规律。#include iostream class VirtualBase { public: VirtualBase() { std::cout VirtualBase constructor. std::endl; } ~VirtualBase() { std::cout VirtualBase destructor. std::endl; } }; class Base1 { public: Base1() { std::cout Base1 constructor. std::endl; } ~Base1() { std::cout Base1 destructor. std::endl; } }; class Base2 { public: Base2() { std::cout Base2 constructor. std::endl; } ~Base2() { std::cout Base2 destructor. std::endl; } }; class MemberA { public: MemberA() { std::cout MemberA constructor. std::endl; } ~MemberA() { std::cout MemberA destructor. std::endl; } }; class MemberB { public: MemberB() { std::cout MemberB constructor. std::endl; } ~MemberB() { std::cout MemberB destructor. std::endl; } }; // 多继承且Base1虚拟继承自VirtualBase class DerivedComplex : public Base1, public Base2, virtual public VirtualBase { private: MemberA memA; MemberB memB; public: DerivedComplex() { std::cout DerivedComplex constructor body. std::endl; } ~DerivedComplex() { std::cout DerivedComplex destructor body. std::endl; } }; int main() { std::cout Constructing DerivedComplex std::endl; DerivedComplex obj; std::cout \n Destroying DerivedComplex std::endl; return 0; }运行这段代码典型的输出顺序是 Constructing DerivedComplex VirtualBase constructor. Base1 constructor. Base2 constructor. MemberA constructor. MemberB constructor. DerivedComplex constructor body. Destroying DerivedComplex DerivedComplex destructor body. MemberB destructor. MemberA destructor. Base2 destructor. Base1 destructor. VirtualBase destructor.关键点解析虚拟基类优先无论VirtualBase在继承列表的哪个位置它总是最先被构造。这保证了在复杂的“菱形继承”中虚基类子对象只有一份。直接基类按声明顺序Base1和Base2按照class DerivedComplex : public Base1, public Base2, ...中的声明顺序构造。成员变量按声明顺序memA和memB按照它们在类DerivedComplex中定义的顺序构造。析构顺序严格逆序完美印证了“后构造者先析构”的原则。实操心得在设计复杂的类继承体系时我强烈建议在纸上画出类的继承关系图并标出成员变量。然后按照上述规则手动推导一遍构造顺序。这能帮你提前发现潜在的设计问题比如对未初始化基类成员的依赖。对于虚拟继承除非确有必要解决菱形继承带来的数据冗余问题否则应谨慎使用因为它会增加对象模型的理解和维护成本。3. 析构顺序的逆向对称性与资源管理如果说构造顺序是“搭积木”那么析构顺序就是“拆积木”而且必须是完全逆向的拆除。这个特性对于资源管理至关重要特别是当涉及动态内存、文件句柄、网络连接等需要显式释放的资源时。3.1 析构顺序的严格规则析构顺序是构造顺序的严格逆序执行派生类自身的析构函数体。按成员变量在类中声明顺序的逆序析构各个非静态成员变量。按直接基类在派生类中声明顺序的逆序调用它们的析构函数。按虚拟基类构造顺序的逆序调用它们的析构函数。这个逆序特性是由C的对象模型和栈展开机制保证的。它确保了当一个部分被销毁时它所依赖的其他部分比如基类提供的接口仍然有效。3.2 资源泄漏的经典陷阱与解决方案不理解析构顺序最容易导致资源泄漏。看一个反面教材#include iostream class ResourceHolder { public: int* resource; ResourceHolder(int val) : resource(new int(val)) { std::cout ResourceHolder acquired resource. std::endl; } ~ResourceHolder() { // 假设这里忘记释放资源了 std::cout ResourceHolder destructor called (LEAK!). std::endl; } }; class Logger { public: Logger() { std::cout Logger started. std::endl; } ~Logger() { std::cout Logger stopped. std::endl; } }; class BadClass : public Logger { private: ResourceHolder holder; public: BadClass() : holder(100) { std::cout BadClass constructor body. std::endl; } ~BadClass() { std::cout BadClass destructor body. std::endl; // 在这里holder已经被析构了它的resource已经无法被正确释放。 // 如果试图在这里访问holder.resource将是未定义行为。 } };在这个例子中ResourceHolder的析构函数没有释放new分配的内存导致内存泄漏。更糟糕的是由于析构顺序是~BadClass()先执行然后才是~ResourceHolder()所以你无法在~BadClass()中补救这个泄漏因为holder对象在那之后才被销毁。解决方案遵循RAII原则RAIIResource Acquisition Is Initialization是C资源管理的核心范式。其思想是将资源内存、文件、锁等的生命周期与一个对象的生命周期绑定。#include memory class GoodResourceHolder { private: std::unique_ptrint resource; // 使用智能指针 public: GoodResourceHolder(int val) : resource(std::make_uniqueint(val)) { std::cout GoodResourceHolder acquired resource. std::endl; } // 不需要显式定义析构函数unique_ptr会自动释放内存。 ~GoodResourceHolder() { std::cout GoodResourceHolder destructor called (Resource auto-freed). std::endl; } }; class GoodClass : public Logger { private: GoodResourceHolder holder; public: GoodClass() : holder(100) { std::cout GoodClass constructor body. std::endl; } // 同样不需要显式释放资源 ~GoodClass() { std::cout GoodClass destructor body. std::endl; } };使用std::unique_ptr后无论析构顺序如何当holder被析构时其unique_ptr成员也会被析构并自动释放其管理的内存。这彻底消除了因忘记释放或顺序问题导致泄漏的可能性。3.3 在构造函数和析构函数中调用虚函数这是一个高级但危险的角落。在构造函数和析构函数中对象的类型被认为是当前正在构造/析构的类而不是最终的派生类。因此虚函数机制不会按你预期的方式工作。#include iostream class Base { public: Base() { std::cout Base constructor. Calling virtual function... std::endl; doSomething(); // 危险 } virtual ~Base() { std::cout Base destructor. Calling virtual function... std::endl; doSomething(); // 同样危险 } virtual void doSomething() { std::cout Base::doSomething() std::endl; } }; class Derived : public Base { public: Derived() { std::cout Derived constructor. std::endl; } ~Derived() override { std::cout Derived destructor. std::endl; } void doSomething() override { std::cout Derived::doSomething() std::endl; } }; int main() { Derived d; return 0; }输出可能是Base constructor. Calling virtual function... Base::doSomething() // 注意这里调用的是Base的版本不是Derived的 Derived constructor. Derived destructor. Base destructor. Calling virtual function... Base::doSomething() // 析构时Derived部分已销毁所以也是Base的版本。重要警告在构造和析构函数中调用虚函数通常无法调用到派生类的重写版本。因为当基类构造函数运行时派生类部分尚未构造完成当基类析构函数运行时派生类部分已经被认为销毁了。这违反了虚函数的设计初衷容易导致错误。一个常见的替代模式是在构造函数中传递必要的状态信息给基类或者使用“两次初始化”模式在构造完成后调用一个独立的initialize()方法。4. 构造与析构顺序在实战中的应用与避坑指南理解了基本原理后我们来看看在实际项目中如何利用和规避构造析构顺序带来的影响。4.1 依赖注入与初始化顺序在设计框架或库时我们常常使用依赖注入。这时成员变量的构造顺序就至关重要。class Logger { /* ... */ }; class ConfigLoader { /* ... */ }; class DatabaseConnection { public: DatabaseConnection(const ConfigLoader config); // 需要配置来连接 }; class MyService { private: Logger logger; // 声明顺序1 ConfigLoader config; // 声明顺序2 DatabaseConnection db; // 声明顺序3 public: MyService() : db(config) // 初始化列表试图用config初始化db // logger和config会先于db按照声明顺序构造 { // 此时logger, config, db 都已构造完毕 } };在这个设计中由于logger和config的声明在db之前它们会先被构造。因此在db的初始化列表中config对象是已经构造好的可以安全地用于初始化db。这是正确的设计。如果把声明顺序搞错class MyService_BAD { private: DatabaseConnection db; // 声明顺序1 Logger logger; // 声明顺序2 ConfigLoader config; // 声明顺序3 public: MyService_BAD() : db(config) // 错误config尚未构造其值未定义。 { } };这会导致未定义行为因为db在config之前构造却试图使用config的值。避坑技巧在类中声明成员变量时有意识地按照它们的依赖关系进行排序。被依赖的对象如基础组件、配置声明在前依赖它们的对象声明在后。这样编译器自动生成的构造顺序就与你的依赖关系一致无需在初始化列表中费心调整实际上调整也没用。4.2 智能指针与成员管理当类中含有动态分配的资源或需要特殊管理的对象时使用智能指针可以简化析构顺序的管理但也要注意它们作为成员变量时的构造顺序。#include memory #include vector class Texture { /* ... */ }; class Shader { /* ... */ }; class GraphicsObject { private: // 声明顺序很重要 std::unique_ptrShader shader_; // 1. 着色器 std::vectorstd::unique_ptrTexture textures_; // 2. 纹理数组 // 假设OpenGL上下文或其它资源句柄 // unsigned int vao_; // 如果这是原生句柄需要手动管理 public: GraphicsObject(std::unique_ptrShader shader) : shader_(std::move(shader)) // shader_先构造 { // 在构造函数体中shader_已就绪可以用来初始化依赖它的东西 // 例如加载纹理可能需要着色器程序ID // loadTextures(shader_-getProgramId()); } ~GraphicsObject() { // 析构顺序textures_先析构释放所有Texture然后shader_析构。 // 这个顺序是合理的因为纹理可能依赖于着色器状态。 // 但如果依赖关系相反就需要调整声明顺序。 std::cout Cleaning up graphics object. std::endl; } // 禁用拷贝以简化 GraphicsObject(const GraphicsObject) delete; GraphicsObject operator(const GraphicsObject) delete; };在这个例子中shader_在textures_之前声明和构造。如果纹理的加载或析构依赖于着色器对象这在图形编程中很常见那么这个顺序就是正确的。如果顺序反了在textures_的析构函数中访问可能已被销毁的shader_就会出错。4.3 全局与静态对象的顺序问题除了类内部不同编译单元.cpp文件中的全局对象和静态对象的构造/析构顺序是未定义的。这被称为“Static Initialization Order Fiasco”。问题示例// FileA.cpp extern int globalValue; // 声明定义在FileB.cpp class A { public: A() { value globalValue * 2; } // 依赖globalValue int value; }; A a; // 全局对象其构造依赖于另一个文件中的globalValue // FileB.cpp int globalValue 100; // 定义如果编译器先初始化globalValue再构造a那么a.value就是200。如果顺序反过来globalValue可能是0未初始化a.value就是0导致错误。解决方案使用“构造时首次使用Construct On First Use”惯用法将全局对象包装在函数内通过函数返回引用来访问。// FileA.cpp int getGlobalValue() { static int globalValue 100; // C11保证线程安全的局部静态初始化 return globalValue; } class A { public: A() { value getGlobalValue() * 2; } // 安全 int value; }; A getA() { static A a; return a; }避免复杂的全局对象依赖尽量将初始化逻辑移到明确的初始化函数中在main函数开始后手动调用。使用单例模式需注意线程安全。对于静态成员变量其初始化顺序在同一个编译单元内是定义好的按定义顺序但在不同编译单元间同样存在未定义顺序的问题解决方法同上。5. 高级话题继承体系中的构造与析构细节5.1 构造函数初始化列表的威力与限制构造函数初始化列表是设置成员和基类初始状态的唯一场所。对于常量成员、引用成员以及没有默认构造函数的类类型成员必须在初始化列表中初始化。class RefAndConst { private: const int id_; // 常量成员 int ref_; // 引用成员 std::string name_; // 有默认构造但可以列表初始化 public: // 必须使用初始化列表 RefAndConst(int id, int externalInt, const std::string name) : id_(id) // 正确初始化常量 , ref_(externalInt) // 正确绑定引用 , name_(name) // 高效直接构造而非先默认构造再赋值 { // id_ id; // 错误常量不能在构造函数体内赋值 // ref_ externalInt; // 错误引用必须在初始化时绑定 // name_ name; // 低效先默认构造再赋值 } };初始化列表的执行顺序再次强调初始化列表中的书写顺序不影响实际的初始化顺序。实际的顺序只由类中成员的声明顺序决定。混淆这两者是常见的错误来源。好的编程习惯是让初始化列表的顺序与成员声明的顺序保持一致这可以提高代码的可读性和可维护性。5.2 析构函数设为虚函数的重要性当类被设计为基类并且可能通过基类指针来删除派生类对象时基类的析构函数必须声明为虚函数。class BaseNonVirtual { public: ~BaseNonVirtual() { std::cout BaseNonVirtual dtor\n; } }; class DerivedNonVirtual : public BaseNonVirtual { public: ~DerivedNonVirtual() { std::cout DerivedNonVirtual dtor\n; } }; class BaseVirtual { public: virtual ~BaseVirtual() { std::cout BaseVirtual dtor\n; } // 关键virtual }; class DerivedVirtual : public BaseVirtual { public: ~DerivedVirtual() override { std::cout DerivedVirtual dtor\n; } }; int main() { std::cout Case 1: Non-virtual destructor\n; BaseNonVirtual* p1 new DerivedNonVirtual(); delete p1; // 未定义行为~DerivedNonVirtual 不会被调用资源可能泄漏。 // 输出可能只有 BaseNonVirtual dtor std::cout \nCase 2: Virtual destructor\n; BaseVirtual* p2 new DerivedVirtual(); delete p2; // 正确先调用 ~DerivedVirtual()再调用 ~BaseVirtual() // 输出 DerivedVirtual dtor 然后 BaseVirtual dtor return 0; }如果基类析构函数非虚通过基类指针删除派生类对象是未定义行为。派生类的析构函数不会被调用其成员和资源也不会被正确释放。这是一个严重的内存泄漏和资源泄漏风险。经验法则如果一个类有任何虚函数它很可能需要被多态地使用那么它的析构函数也应该声明为虚函数。5.3 在构造/析构函数中处理异常在构造函数中抛出异常是通知对象构造失败的唯一方式。但一旦构造函数抛出异常已经构造完成的子对象基类和成员的析构函数会被自动调用这是一个“部分构造”对象的回滚机制。class Part { public: Part(int id) : id_(id) { std::cout Part id_ constructed.\n; } ~Part() { std::cout Part id_ destroyed.\n; } private: int id_; }; class DangerousObject { private: Part p1; Part p2; int* leakyResource; public: DangerousObject() : p1(1) , p2(2) , leakyResource(new int[100]) // 动态资源 { std::cout DangerousObject constructor body.\n; throw std::runtime_error(Something went wrong in constructor!); // 异常抛出 } ~DangerousObject() { delete[] leakyResource; std::cout DangerousObject destructor body.\n; } }; int main() { try { DangerousObject obj; // 构造失败 } catch (const std::exception e) { std::cout Caught: e.what() std::endl; } return 0; }输出可能如下Part 1 constructed. Part 2 constructed. DangerousObject constructor body. Part 2 destroyed. Part 1 destroyed. Caught: Something went wrong in constructor!关键观察p1和p2被成功构造。leakyResource成功分配在初始化列表阶段。构造函数体抛出异常。因为DangerousObject对象没有完全构造成功所以它的析构函数不会被调用这意味着leakyResource指向的内存泄漏了。然而已经构造完成的成员p2和p1会按照与构造顺序相反的顺序被析构。这是语言提供的保障。教训如果构造函数会抛出异常并且已经获取了需要手动管理的资源如原始指针、文件句柄等必须在抛出异常前清理这些资源或者使用RAII对象如智能指针来管理它们让它们的析构函数自动处理清理工作。在析构函数中抛出异常是极其危险的。如果栈正在因异常而展开即已经在处理一个异常此时析构函数又抛出另一个异常程序通常会直接调用std::terminate()终止。因此析构函数应该尽可能不抛出异常通常用noexcept修饰并在内部吞掉任何可能发生的异常。6. 综合案例一个简易资源管理器的生命周期模拟让我们设计一个模拟场景整合前面讨论的所有要点继承、成员组合、动态资源、虚析构函数以及构造/析构顺序的观察。#include iostream #include memory #include vector // 一个简单的“资源”类模拟需要管理的东西如文件、网络连接 class ManagedResource { int id_; public: explicit ManagedResource(int id) : id_(id) { std::cout [Resource id_ ] Acquired.\n; } ~ManagedResource() { std::cout [Resource id_ ] Released.\n; } void use() const { std::cout [Resource id_ ] In use.\n; } }; // 基类提供日志功能 class Loggable { public: Loggable(const std::string name) : name_(name) { std::cout Loggable name_ constructing.\n; } virtual ~Loggable() { // 虚析构因为这是多态基类 std::cout Loggable name_ destroying.\n; } virtual void log(const std::string msg) const { std::cout LOG [ name_ ]: msg std::endl; } private: std::string name_; }; // 派生类管理一组资源 class ResourceManager : public Loggable { // 成员声明顺序决定了构造/析构顺序 std::unique_ptrManagedResource primaryResource_; // 1. 智能指针成员 std::vectorstd::unique_ptrManagedResource secondaryResources_; // 2. 容器成员 // 原始资源句柄模拟不推荐在实际中使用 int* rawResourceArray_; size_t rawSize_; public: ResourceManager(const std::string name, int primaryId, std::vectorint secondaryIds) : Loggable(name) // 基类初始化 , primaryResource_(std::make_uniqueManagedResource(primaryId)) // 成员初始化 , rawResourceArray_(nullptr) , rawSize_(0) { // 构造函数体开始基类和primaryResource_已构造完毕 log(Constructor body started.); // 初始化 secondaryResources_ for (int id : secondaryIds) { secondaryResources_.push_back(std::make_uniqueManagedResource(id)); } log(Secondary resources created.); // 动态分配原始资源有风险 rawSize_ 5; rawResourceArray_ new int[rawSize_]{1, 2, 3, 4, 5}; log(Raw resource array allocated.); // 模拟一个可能失败的操作 if (secondaryIds.empty()) { // 如果失败需要清理已分配的资源 delete[] rawResourceArray_; // 手动清理 rawResourceArray_ nullptr; log(Cleaned up due to failure condition.); throw std::invalid_argument(Secondary IDs cannot be empty!); } log(Constructor completed successfully.); } ~ResourceManager() noexcept override { log(Destructor started.); // 必须手动释放原始资源 delete[] rawResourceArray_; rawResourceArray_ nullptr; rawSize_ 0; log(Raw resource array freed.); // secondaryResources_ 和 primaryResource_ 会被它们的 unique_ptr 自动释放 // 析构顺序先执行完这个函数体然后自动析构 secondaryResources_最后析构 primaryResource_ } void useResources() const { log(Using resources...); if (primaryResource_) primaryResource_-use(); for (const auto res : secondaryResources_) { res-use(); } } // 禁用拷贝 ResourceManager(const ResourceManager) delete; ResourceManager operator(const ResourceManager) delete; }; int main() { std::cout Scenario 1: Normal Construction and Destruction \n; { ResourceManager mgr(Manager1, 100, {101, 102, 103}); mgr.useResources(); std::cout --- End of scope for mgr ---\n; } // mgr 离开作用域自动析构 std::cout \n Scenario 2: Constructor Throws Exception \n; try { ResourceManager badMgr(Manager2, 200, {}); // 空列表构造函数会抛出 } catch (const std::exception e) { std::cout Exception caught: e.what() std::endl; } std::cout \n Scenario 3: Polymorphic Usage \n; Loggable* polyPtr new ResourceManager(Manager3, 300, {301}); polyPtr-log(Created via base pointer.); // 通过基类指针删除由于基类有虚析构函数这是安全的 delete polyPtr; std::cout \n Program End \n; return 0; }这个案例演示了构造顺序Loggable基类 -primaryResource_成员 -secondaryResources_和rawResourceArray_在构造函数体中初始化。析构顺序ResourceManager析构函数体释放原始数组- 自动析构secondaryResources_向量及其管理的ManagedResource- 自动析构primaryResource_- 析构Loggable基类。异常安全在构造函数失败的情况下已经构造的成员Loggable,primaryResource_会被自动析构但手动分配的rawResourceArray_必须在抛出异常前显式释放否则会泄漏。这凸显了使用RAII对象如unique_ptr管理资源的优越性。多态析构通过Loggable*指针删除ResourceManager对象由于基类有虚析构函数整个过程是安全的。通过这样一步步拆解和实例演示相信你对C中构造与析构顺序这个“静默的规则”有了更深刻的理解。掌握它不仅能帮你避免坑更能让你设计出生命周期清晰、资源管理安全的健壮类这是迈向高级C开发者的必经之路。在实际编码中养成“思考对象生命周期”的习惯多画图多写测试验证构造析构顺序你的代码质量会得到显著的提升。