C++ 构造函数完全指南:从入门到进阶 C 构造函数完全指南从入门到进阶构造函数是 C 类设计中最重要的部分之一。它控制着对象的诞生方式直接影响代码的安全性、性能和可维护性。很多人觉得构造函数无非就是初始化变量但 C 的构造函数体系远比这复杂——从默认构造、拷贝构造、移动构造到初始化列表、委托构造、explicit 关键字每一个知识点都是面试和工程实践的重点。1. 构造函数是什么为什么需要它构造函数是在对象创建时自动调用的特殊成员函数负责初始化对象的状态。classWidget{intid;std::string name;public:Widget(inti,conststd::stringn){idi;// 这是赋值不是初始化namen;// 这也是赋值}};特点函数名与类名相同没有返回类型连void都没有可以重载多个构造函数参数不同对象创建时自动调用不能手动调用2. 初始化列表赋值的坑与正道看下面这段代码的问题classWidget{constintid;// const 成员std::stringname;// 引用成员std::string desc;public:Widget(inti,std::stringn,conststd::stringd){idi;// 错误const 成员不能赋值namen;// 错误引用成员不能赋值descd;// 正确但低效先默认初始化再赋值}};初始化列表就是为解决这个问题而生的Widget(inti,std::stringn,conststd::stringd):id(i),name(n),desc(d){// 构造函数体可以为空或者做其他工作}为什么初始化列表更好const 成员和引用成员必须用初始化列表效率更高成员在初始化列表中直接构造在函数体内是默认构造后再赋值多一次操作成员初始化顺序只取决于声明顺序与初始化列表顺序无关classExample{inta;intb;public:Example(intx):b(x),a(b){}// 危险a 先初始化按声明顺序但此时 b 还未初始化a 的值未定义};最佳实践能用初始化列表就用初始化列表且顺序与成员声明顺序一致。3. 默认构造函数没有参数的那个默认构造函数是不传参数就能调用的构造函数。classWidget{public:Widget(){/* ... */}// 默认构造函数};什么时候编译器会帮你生成只有在你没有定义任何构造函数时编译器才会自动生成一个合成的默认构造函数。classA{intx;// 默认构造x 的值是未定义的内置类型不初始化};classB{intx0;// 类内初始值默认构造时 x 为 0};classC{B b;// 包含类类型成员默认构造会调用 B 的默认构造};规则如果类有内置类型成员且没有类内初始值合成的默认构造函数不初始化它们值未定义如果类包含类类型成员会调用它们的默认构造函数一旦你定义了任何构造函数编译器就不再生成默认构造函数classWidget{public:Widget(intx){}// 自定义构造函数};Widget w;// 错误没有默认构造函数了解决用 default显式要求编译器生成classWidget{public:Widget()default;// 让编译器生成Widget(intx){}};4. 析构函数对象的清理工classWidget{int*data;public:Widget():data(newint[100]){}~Widget(){delete[]data;}// 析构函数};特点函数名是~类名没有参数不能重载一个类只有一个析构函数对象生命周期结束时自动调用如果类作为基类析构函数应该是虚的classBase{public:virtual~Base()default;// 基类必须有虚析构};Base*pnewDerived();deletep;// 如果析构不虚Derived 的析构不会被调用5. 拷贝构造函数与拷贝赋值对象的克隆5.1 拷贝构造函数classWidget{std::string name;public:Widget(constWidgetother):name(other.name){std::coutCopy constructed\n;}};Widget w1;Widgetw2(w1);// 调用拷贝构造Widget w3w1;// 也是拷贝构造不是赋值调用时机用一个对象初始化另一个对象函数传值传参时复制函数返回值可能被 RVO/NRVO 优化掉5.2 拷贝赋值运算符classWidget{std::string name;public:Widgetoperator(constWidgetother){nameother.name;// 注意对象已经存在这里是赋值return*this;}};Widget w1,w2;w2w1;// 调用拷贝赋值两个对象都已存在区别拷贝构造是从无到有拷贝赋值是覆盖已有。5.3 浅拷贝与深拷贝// 浅拷贝危险的classShallow{int*data;public:Shallow(intv):data(newint(v)){}// 默认拷贝构造只复制指针值不复制内存// 析构时会 double free};// 深拷贝正确的classDeep{int*data;public:Deep(intv):data(newint(v)){}Deep(constDeepother):data(newint(*other.data)){}// 分配新内存~Deep(){deletedata;}};当你管理堆内存时必须自己写深拷贝的拷贝构造和拷贝赋值。6. 移动构造函数与移动赋值C11性能的大杀器移动语义让转移所有权成为可能避免了不必要的深拷贝。classBuffer{char*data;size_t size;public:// 移动构造Buffer(Bufferother)noexcept:data(other.data),size(other.size){other.datanullptr;// 把原对象置空other.size0;}// 移动赋值Bufferoperator(Bufferother)noexcept{if(this!other){delete[]data;// 释放自己的旧资源dataother.data;// 接管对方资源sizeother.size;other.datanullptr;// 置空原对象other.size0;}return*this;}~Buffer(){delete[]data;}};为什么用noexcept移动操作标记noexcept后标准库容器如std::vector才能在扩容时安全使用移动而非拷贝大幅提升性能。7. 三/五法则资源管理的黄金准则三法则C98如果需要自定义析构函数、拷贝构造、拷贝赋值中的任何一个大概率三个都需要。五法则C11 扩展加上移动构造和移动赋值。classResource{int*data;public:// 五件套Resource():data(newint(0)){}~Resource(){deletedata;}Resource(constResourceother):data(newint(*other.data)){}Resourceoperator(constResourceother){/* 深拷贝 */return*this;}Resource(Resourceother)noexcept:data(other.data){other.datanullptr;}Resourceoperator(Resourceother)noexcept{/* 移动交换 */return*this;}};零法则如果类的所有成员都正确管理自己的资源使用std::string、std::vector、智能指针等你不需要写任何特殊成员函数编译器生成的默认版本就是正确的。classGood{std::string name;std::vectorintdata;std::unique_ptrConfigconfig;// 不需要写析构、拷贝、移动编译器生成的都是对的};8. explicit 关键字防止隐式转换的坑classMyString{public:MyString(intn){}// 可以用 int 构造};voidprint(constMyStrings){/* ... */}print(42);// 这也能编译42 隐式转换成 MyString加explicit阻止隐式转换classMyString{public:explicitMyString(intn){}};print(MyString(42));// 必须显式构造// print(42); // 编译错误准则除非有明确的理由支持隐式转换单参数构造函数都应该加explicit。9. 委托构造函数C11一个构造函数可以调用同类中另一个构造函数减少重复代码classWidget{inta,b,c;public:Widget():Widget(0,0,0){}// 委托给三参数版本Widget(intx):Widget(x,x,x){}Widget(intx,inty,intz):a(x),b(y),c(z){// 真正的初始化逻辑只写一次}};注意一旦使用了委托构造初始化列表中就不能再有其他成员初始化了。10. 继承体系中的构造函数10.1 派生类构造时发生了什么classBase{public:Base(intx){std::coutBase(x)\n;}};classDerived:publicBase{public:Derived(intx,inty):Base(x){// 显式调用基类构造std::coutDerived(x,y)\n;}};Derivedd(1,2);// 输出Base(1)// Derived(1,2)构造顺序基类 - 成员按声明顺序- 派生类构造体。10.2 继承构造函数C11classBase{public:Base(intx){}Base(intx,doubley){}};classDerived:publicBase{public:usingBase::Base;// 继承基类的所有构造函数};11. 面试常考清单11.1 初始化列表和构造函数体内赋值的区别答案要点初始化列表是真正的初始化函数体内是赋值const 成员和引用成员必须用初始化列表初始化列表效率更高类类型成员少一次默认构造初始化顺序只取决于成员声明顺序11.2 什么情况下编译器不会自动生成默认构造函数答案要点当你自定义了任何构造函数时编译器不再生成默认构造函数。11.3 拷贝构造和拷贝赋值的区别答案要点拷贝构造是用一个对象初始化另一个新对象拷贝赋值是把一个对象的值赋给另一个已存在的对象。11.4 什么是深拷贝为什么需要它答案要点当类管理堆内存等资源时浅拷贝只复制指针值导致两个对象指向同一内存析构时 double free。深拷贝会分配新内存并复制数据。11.5 移动构造相比拷贝构造的优势是什么答案要点移动构造直接偷走临时对象的资源把指针挪过来避免了深拷贝的开销。对于堆内存、大容器性能提升巨大。11.6 explicit 关键字的作用答案要点阻止单参数构造函数的隐式类型转换要求必须显式调用避免意外的隐式转换和临时对象。11.7 什么是三/五法则和零法则答案要点三法则如果定义了析构/拷贝构造/拷贝赋值之一大概率三个都需要五法则C11 加上移动构造和移动赋值零法则如果成员都正确管理资源用 RAII 类型不写任何特殊成员函数11.8 为什么基类析构函数必须是虚的答案要点通过基类指针删除派生类对象时如果析构函数不虚只会调用基类析构而不会调用派生类析构导致资源泄漏。12. 实践清单一个设计良好的类其构造函数应该使用初始化列表初始化所有成员单参数构造加 explicit除非有明确理由遵循三/五法则或零法则移动操作加 noexcept基类析构加 virtual能用 default就让编译器生成构造函数是对象生命周期的起点设计好构造函数就为类的安全性和性能打下了坚实的基础。