C++对象的出生与消失:搞懂构造函数、析构函数、初始化列表与拷贝构造

发布时间:2026/9/28 4:29:05
C++对象的出生与消失:搞懂构造函数、析构函数、初始化列表与拷贝构造
文章目录1. 先清楚对象的生命周期2. 构造函数让对象一出生就能用2.1 基本写法2.2 什么是默认构造函数2.3 对象定义和函数声明的歧义3. 初始化列表成员真正的初始化位置3.1 函数体赋值与初始化列表的不同3.2 哪些成员必须使用初始化列表const 成员引用成员没有默认构造函数的成员对象3.3 初始化顺序由声明顺序决定3.4 类内缺省值和初始化列表4. 成员对象的构造和析构顺序5. 析构函数对象离开作用域时做清理5.1 基本写法5.2 什么时候可以使用默认析构6. 拷贝构造用一个已有对象创建新对象6.1 什么时候调用拷贝构造6.2 编译器生成的拷贝通常做什么7. 浅拷贝和深拷贝指针成员改变了问题8. 赋值运算符给已经存在的对象赋值9. 分清类和对象的四个关键10. 传值、返回值与拷贝省略10.1 void Use(Point point) —— 函数传值10.2 Point MakePoint() —— 函数按值返回对象11.小结接续对C类和对象的叙述这一篇专门解决类和对象学习中最容易混在一起的一组问题对象什么时候被初始化成员变量到底怎样初始化析构函数什么时候执行拷贝构造和赋值又有什么区别。1. 先清楚对象的生命周期定义一个局部对象时可以先用下面这条线索观察进入作用域 ↓ 分配对象空间 ↓ 调用构造函数初始化成员 ↓ 对象可正常使用 ↓ 离开作用域 ↓ 调用析构函数释放资源C 语言里常见的写法是先定义结构体再调用Init如果忘记调用结构体就可能处于未初始化状态。而C 则把“对象必须经过初始化”写进了构造函数机制中。构造函数不是普通的初始化工具而是对象建立时自动执行的特殊成员函数。2. 构造函数让对象一出生就能用2.1 基本写法classPoint{public:Point(){_x0;_y0;}Point(intx,inty){_xx;_yy;}voidPrint()const{std::cout(_x,_y)std::endl;}private:int_x;int_y;};构造函数的三个明显特点函数名与类名相同。无返回值。创建对象时由编译器自动调用。2.2 什么是默认构造函数“默认构造函数”不是只指编译器自动生成的那个函数无参构造函数、全缺省构造函数、编译器自动生成的构造函数都可以叫做默认构造函数。这三个函数有且只有一个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用时会存在歧义。只要一个构造函数可以不传实参调用它就属于默认构造函数classA{public:A(){}// 默认构造};classB{public:B(intvalue0){}// 也是默认构造};无参构造、全缺省构造和编译器生成的构造不能同时以相同形式存在否则会产生调用歧义。编译器无法判断以哪一种方式构造当我们自己写了一个不带默认值的带参构造classDate{public:Date(intyear,intmonth,intday){}};那么如果初始化对象时没有给对应的参数Date d; 就没有可调用的默认构造函数会编译失败。编译器不会因为你已经写了带参构造就自动额外补一个无参构造。所以建议如果自己实现了有参构造的构造函数时最好再补一个无参的默认构造这个默认构造函数体内可以为空只是作为没有传入参数时的补救措施。2.3 对象定义和函数声明的歧义Point p1;// 对象定义Pointp2();// 通常会被解释为函数声明不是对象定义当我们需要默认构造时直接写类名加对象名即可Point p1;不要在对象名后面加空括号。3. 初始化列表成员真正的初始化位置3.1 函数体赋值与初始化列表的不同下面的代码看起来像初始化classNumber{public:Number(intvalue){_valuevalue;}private:int_value;};执行流程进入构造函数函数体之前成员变量_value已经完成默认初始化int 类型默认随机值进入{}内部后才执行_value value这是赋值操作不属于初始化。真正的成员初始化位置是初始化列表classNumber{public:Number(intvalue):_value(value){}private:int_value;};初始化列表以冒号开始多个成员之间用逗号分隔。3.2 哪些成员必须使用初始化列表const 成员classConfig{public:Config(intvalue):_limit(value){}private:constint_limit;};const 对象必须在创建时确定初值之后不能再赋值。因此下面的写法不成立Config(intvalue){_limitvalue;// _limit 已经完成初始化不能再次赋值}引用成员classAlias{public:Alias(intvalue):_ref(value){}private:int_ref;};引用必须在定义时绑定对象不能先“空着”再在构造函数体中绑定。没有默认构造函数的成员对象classClock{public:Clock(inthour):_hour(hour){}private:int_hour;};classMeeting{public:Meeting(inthour):_clock(hour){}private:Clock _clock;};Meeting构造时必须告诉_clock用什么参数构造。如果Clock没有无参构造则编译器报错。3.3 初始化顺序由声明顺序决定classOrder{public:Order(intfirst,intsecond):_second(second),_first(first){}private:int_first;int_second;};虽然初始化列表先写_second实际仍然先初始化_first因为成员初始化顺序由它们在类中声明的顺序决定。为了避免误解推荐让初始化列表顺序与成员声明顺序保持一致。3.4 类内缺省值和初始化列表classCounter{public:Counter()default;// 编译器生成默认构造private:int_value0;int_step1;};类内写的int _value 0、int _step 1是成员的缺省初值。当初始化列表没有明确指定对应成员时它就可以作为默认来源如果初始化列表明确指定了值则使用初始化列表中的值。4. 成员对象的构造和析构顺序#includeiostreamclassPart{public:Part(intid):_id(id){std::coutPart construct _idstd::endl;}~Part(){std::coutPart destroy _idstd::endl;}private:int_id;};classMachine{public:Machine():_first(1),_second(2){std::coutMachine constructstd::endl;}~Machine(){std::coutMachine destroystd::endl;}private:Part _first;Part _second;};intmain(){Machine machine;return0;}运行示例可以看到运行Machine machine; 时构造的顺序是先_first再_second最后执行Machine构造函数体。销毁时顺序反过来先执行Machine析构函数体再销毁_second最后销毁_first。可以记成构造成员先构造外层对象后构造 析构外层对象先析构成员后析构这个规则在一个类包含另一个类对象时非常重要。外层类不需要手动调用成员对象的构造和析构函数编译器会按照声明顺序执行它们。5. 析构函数对象离开作用域时做清理5.1 基本写法析构函数名字是 ~类名没有参数没有返回值同时不能重载classMessage{public:Message(){std::coutconstructstd::endl;}~Message(){std::coutdestroystd::endl;}};intmain(){Message message;std::coutusingstd::endl;}执行流程局部对象message在定义时调用构造函数。当执行流离开main的作用域}对象生命周期结束自动调用析构。所以先打印using之后才触发析构打印destroy。析构函数的价值就在于把清理动作绑定到对象生命周期不再依赖调用者记住某个额外的Destroy函数。5.2 什么时候可以使用默认析构如果类只包含内置类型或者成员对象自身已经能正确清理资源通常可以使用编译器生成的析构函数classDate{private:int_year0;int_month0;int_day0;};成员都是内置基础类型没有堆上动态内存。编译器自动生成的默认析构就会依次销毁成员不需要再手动实现。如果类申请了动态资源就必须考虑自己释放classIntBuffer{public:IntBuffer(intsize):_data(newint[size]),_size(size){}~IntBuffer(){delete[]_data;}private:int*_datanullptr;int_size0;};如果此时只写了构造函数而没有析构函数对象销毁时申请的动态数组内存并不会自动释放从而形成资源泄漏。6. 拷贝构造用一个已有对象创建新对象如果一个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷贝构造函数也就是说拷贝构造是一个特殊的构造函数。拷贝构造特点拷贝构造是构造函数的一个重载。第一个参数必须是类类型对象的引用。6.1 什么时候调用拷贝构造classPoint{public:Point(intx0,inty0):_x(x),_y(y){}Point(constPointother):_x(other._x),_y(other._y){std::coutcopy constructstd::endl;}private:int_x;int_y;};intmain(){Pointp1(1,2);Pointp2(p1);// 拷贝构造Point p3p1;// 也是拷贝构造Point p4;p4p1;//不是拷贝构造是赋值}运行示例拷贝构造函数固定格式类名(const 类名)必须是const 引用不能传值传值会无限递归调用拷贝构造。拷贝构造的关键判断是左边的新对象还没有诞生需要根据右边已有对象初始化它。初始化对象还不存在用别的对象创建 → 拷贝构造赋值对象已经存在p2 p1;- 调用赋值重载不触发拷贝构造6.2 编译器生成的拷贝通常做什么对只包含 int 等内置类型的类编译器自动生成的拷贝构造也可以完成成员逐个复制classDate{private:int_year0;int_month0;int_day0;};编译器自动生成的拷贝构造执行逐成员拷贝浅拷贝内置类型int、double直接复制值成员对象则调用成员自己的拷贝构造。对于只有内置类型、没有申请堆资源的类默认拷贝构造函数够用但如果类里面有指针浅拷贝就会造成问题由于拷贝时是逐成员拷贝浅拷贝所以拷贝后两个对象指针都指向同一块堆内存在析构时就会对这同一块内存进行重复释放导致程序崩溃。7. 浅拷贝和深拷贝指针成员改变了问题classIntArray{public:IntArray(intsize):_data(newint[size]),_size(size){}~IntArray(){delete[]_data;}private:int*_data;int_size;};如果直接使用编译器生成的拷贝构造的话IntArraya(3);IntArray ba;运行报错Invalid address specified to RtlValidateHeap传递给堆校验函数的地址是非法的程序尝试delete一块已经释放过的堆内存。这就是因为a._data和b._data会保存同一个地址。这就是浅拷贝。两个对象销毁时都会执行delete[]第二次释放同一块资源就可能导致程序崩溃。深拷贝则需要重新申请一块资源再复制内容classIntArray{public:IntArray(intsize):_data(newint[size]),_size(size){for(inti0;i_size;i){_data[i]0;}}IntArray(constIntArrayother):_data(newint[other._size]),_size(other._size){for(inti0;i_size;i){_data[i]other._data[i];}}~IntArray(){delete[]_data;}private:int*_data;int_size;};intmain(){IntArraya(3);IntArray ba;return0;}此时两个对象拥有不同的数组修改一个对象不会影响另一个对象程序正常执行不崩溃。8. 赋值运算符给已经存在的对象赋值拷贝构造和赋值运算符可以放在一起比较写法左侧对象状态调用的机制Point b a;b 正在创建拷贝构造Point b(a);b 正在创建拷贝构造b a;b 已经存在赋值运算符带资源的类需要考虑对赋值运算符进行如下重载IntArrayIntArray::operator(constIntArrayother){if(this!other){int*newDatanewint[other._size];for(inti0;iother._size;i){newData[i]other._data[i];}delete[]_data;_datanewData;_sizeother._size;}return*this;}9. 分清类和对象的四个关键机制发生时机解决的问题构造函数新对象建立时让对象获得初始状态析构函数对象销毁时释放对象持有的资源拷贝构造用已有对象建立新对象初始化新对象的内容赋值运算符两个已有对象之间赋值替换左对象的内容10. 传值、返回值与拷贝省略10.1void Use(Point point)—— 函数传值voidUse(Point point){point.Print();}参数是值传递Point point调用时Point p(1,2); Use(p);会用实参p拷贝构造形参point。形参point是函数内部独立副本函数内修改 point 不会影响外面的 p。函数执行结束局部形参point会调用析构。优化写法void Use(const Point point)传 const 引用完全不触发拷贝构造是类对象传参首选。10.2Point MakePoint()—— 函数按值返回对象PointMakePoint(){Pointlocal(1,2);returnlocal;}local是函数内局部对象离开函数作用域就要销毁。语义上return 需要把local拷贝成临时对象带回调用处。调用示例Point res MakePoint();现代编译器会在不改变程序语义的前提下省略一些不必要的拷贝因此你在调试输出中不一定能看到表面分析时列出的每一次拷贝。11.小结构造函数决定对象怎样出生。初始化列表决定成员怎样被真正初始化。析构函数决定对象怎样结束生命周期。拷贝构造处理“新对象根据旧对象建立”。赋值运算符处理“两个已有对象交换内容”。