文章目录一、六大默认成员函数总览二、构造函数对象的出厂设置2.1 构造函数到底干了什么2.2 特性2.3 编译器默认生成的构造函数做了什么三、析构函数给对象办后事3.1 析构函数不是销毁对象本身3.2 什么时候必须写析构四、拷贝构造函数对象的克隆术4.1 什么场景触发拷贝构造4.2 为什么参数必须是引用4.3 浅拷贝的致命问题4.4 深拷贝的正确实现五、赋值运算符重载对象间的数据转移5.1 赋值 vs 拷贝构造5.2 赋值运算符重载的基本格式5.3 深拷贝的赋值运算符5.4 前置和后置的重载5.5 流插入和流提取的重载六、const成员函数总结C的类有六个默认成员函数你写了编译器按你的来你不写编译器自己生成一套。这六个里前四个构造、析构、拷贝构造、赋值重载是重点——它们分别对应对象的「出生」「死亡」「克隆」「赋值」四个生命周期节点。搞懂了这四个类的设计能力直接上一个台阶。一、六大默认成员函数总览classDate{public:// 1. 构造函数Date();// 2. 析构函数~Date();// 3. 拷贝构造函数Date(constDated);// 4. 赋值运算符重载Dateoperator(constDated);// 5. 取地址运算符重载普通Date*operator();// 6. 取地址运算符重载constconstDate*operator()const;};五个关键原则你不写编译器自动生成。但编译器生成的版本行为固定不一定满足需求。一旦你显式定义了一个编译器就不再生成那个版本。前四个是重点后两个取地址重载绝大多数情况下直接用编译器的就行不需要自己写。二、构造函数对象的出厂设置2.1 构造函数到底干了什么构造函数的名字容易让人误解——它不开空间。局部对象在栈帧创建时空间已经开辟好了构造函数做的事是初始化这片空间。它的本质是替代以前C里写的Init()函数。区别在于Init()需要你手动调构造函数自动调。这是质的飞跃——你再也不会忘记初始化一个对象了。2.2 特性classDate{public:// 1. 无参构造函数Date(){_year1;_month1;_day1;}// 2. 带参构造函数Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}// 3. 全缺省构造函数和无参构造不能同时存在调用会引起歧义// Date(int year 1, int month 1, int day 1)// {// _year year;// _month month;// _day day;// }voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;// 调用无参构造函数默认构造Dated2(2025,1,1);// 调用带参构造函数// 坑下面这句不是定义一个对象是声明一个函数// Date d3(); // 编译器会当成函数声明返回Date参数为void// 正确写法——无参构造调用时不加括号Date d3;// 这才是调用默认构造return0;}六条铁律函数名与类名相同无返回值不需要写void这是语法规定。对象实例化时自动调用。可以重载。你不写编译器自动生成一个无参的默认构造你一旦写了编译器就不再生成。默认构造函数指的是「不传实参就能调用的构造函数」包括三种形式无参构造函数、全缺省构造函数、编译器自动生成的构造函数。这三种有且只能存在一个。Date d1;和Date d1()不一样——后者是函数声明。2.3 编译器默认生成的构造函数做了什么很多人以为编译器什么都不做。实际上它做了只是选择性地做// 内置类型classDate{public:// 不写构造编译器生成默认构造private:int_year;// 内置类型编译器不做初始化值是随机的int_month;int_day;};// 自定义类型classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);_capacityn;_top0;}private:int*_a;size_t _capacity;size_t _top;};classMyQueue{public:// 没写构造编译器生成默认构造// 这个默认构造会调用 Stack 的默认构造完成 pushst 和 popst 的初始化private:Stack pushst;Stack popst;};intmain(){MyQueue mq;// OK编译器生成的默认构造调了 Stack 的构造return0;}结论一句话内置类型成员编译器默认生成的不一定会初始化值是未定义的。自定义类型成员编译器会调用该成员自己的默认构造函数。如果这个成员没有默认构造函数编译报错。这就是为什么MyQueue不写构造也能正常工作——它的两个Stack成员各有自己的默认构造编译器默认生成的构造函数会去调用它们。三、析构函数给对象办后事3.1 析构函数不是销毁对象本身局部对象的栈空间是栈帧回收时自动归还的析构函数做的是对象内部的资源清理。比如动态申请的内存、打开的文件、网络连接这些系统不会替你收拾。classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);_capacityn;_top0;}~Stack(){cout~Stack()endl;free(_a);// 释放构造函数里malloc出来的内存_anullptr;_top_capacity0;}// 其余成员函数省略...private:int*_a;size_t _capacity;size_t _top;};六条关键特性函数名~ 类名。无参数无返回值。一个类只能有一个析构函数——不能重载没有参数怎么重载对象生命周期结束时自动调用。不写编译器自动生成——行为规则和构造一样内置类型不做处理自定义类型调用它的析构。显式写了析构函数后自定义类型成员的析构仍然会被自动调用编译器会帮你插入调用代码不需要你手动写。局部对象的析构顺序是后构造的先析构LIFO和栈帧的工作方式一致。3.2 什么时候必须写析构情况是否需要写析构例子类中没有资源申请不需要Date——存三四个int不需要析构类中有资源申请必须写Stack——有malloc必须free类中成员都是「会自己析构的」不需要MyQueue——两个Stack成员各自有析构不需要额外管判断标准很简单如果你的构造函数里 new/malloc/fopen 了析构函数里就必须写对应的 delete/free/fclose。其他情况没写不用担心反而多写可能制造问题。有了构造和析构以前在C里写Stack时频繁忘记Init()和Destroy()的问题就不再存在。对象创建时自动初始化离开作用域时自动清理——这就是RAII的雏形。四、拷贝构造函数对象的克隆术4.1 什么场景触发拷贝构造Dated1(2025,1,1);Dated2(d1);// 场景1用同类对象初始化另一个对象Date d3d1;// 场景2看起来是赋值但d3刚创建实际调的是拷贝构造两个场景本质一样用已存在的对象去初始化一个正在创建的新对象。4.2 为什么参数必须是引用// 错误写法——参数是值传递Date(Date d){_yeard._year;_monthd._month;_dayd._day;}如果参数是值传递调用Date d2(d1)时需要把d1传给参数d传值调用→又触发拷贝构造→又需要把d1传给参数……无限递归栈溢出正确写法必须是const 类名Date(constDated){_yeard._year;_monthd._month;_dayd._day;}const修饰保证了不会意外修改原对象引用避免了无限递归。4.3 浅拷贝的致命问题编译器默认生成的拷贝构造是浅拷贝值拷贝——把每个成员变量按字节复制过去。对于Date这种只含内置类型的类浅拷贝完美够用。对于Stack这种有指针指向动态内存的类灾难来了classStack{public:Stack(intn4){_a(int*)malloc(sizeof(int)*n);_capacityn;_top0;}~Stack(){free(_a);}// 没写拷贝构造——用编译器生成的浅拷贝private:int*_a;// 指向堆上的数组size_t _capacity;size_t _top;};intmain(){Stack st1;st1.Push(1);Stackst2(st1);// 浅拷贝st2._a 和 st1._a 指向同一块内存// st1 和 st2 析构时同一块内存被 free 两次 → 崩溃return0;}问题根源两个对象的_a指向同一块堆内存。一个对象修改数据另一个也被影响析构时同一块内存被释放两次直接崩。4.4 深拷贝的正确实现Stack(constStackst){// 1. 申请一块新的独立内存_a(int*)malloc(sizeof(int)*st._capacity);if(_anullptr)throwbad_alloc();// 2. 把数据拷贝过去注意用 _top 而非 _capacity只复制有效数据memcpy(_a,st._a,sizeof(int)*st._top);// 3. 复制其他值类型的成员_topst._top;_capacityst._capacity;}这样st1._a和st2._a指向两块独立的堆内存内容和大小相同但互不干涉。各自析构时各自释放自己的内存相安无事。五、赋值运算符重载对象间的数据转移5.1 赋值 vs 拷贝构造Dated1(2025,1,1);Dated2(2026,6,1);d2d1;// 赋值d2 已经存在把 d1 的数据拷给 d2Date d3d1;// 拷贝构造d3 正在创建用 d1 初始化 d3赋值和拷贝构造是两个独立的概念不要因为语法上都有而混掉关键在于对象是否已经存在。5.2 赋值运算符重载的基本格式classDate{public:Dateoperator(constDated){if(this!d)// 防止自己给自己赋值{_yeard._year;_monthd._month;_dayd._day;}return*this;// 返回 *this 支持连续赋值d1 d2 d3}};三点解释返回值是类名支持d1 d2 d3这种连续赋值。参数是const 类名和拷贝构造一样避免不必要的拷贝。自赋值检查if (this ! d)是必要的——如果自己赋给自己先释放再拷贝就等于自己把自己弄没了。Date这种纯值类型不检查也能活但如果有资源管理类似Stack不检查就是灾难。5.3 深拷贝的赋值运算符classStack{public:Stackoperator(constStackst){if(this!st){// 1. 释放旧资源free(_a);// 2. 申请新资源_a(int*)malloc(sizeof(int)*st._capacity);// 3. 拷贝数据memcpy(_a,st._a,sizeof(int)*st._top);// 4. 更新值类型成员_topst._top;_capacityst._capacity;}return*this;}};四步操作释放旧资源 → 申请新资源 → 拷贝数据 → 更新成员。顺序不能乱——先释放再申请是为了避免旧资源泄漏先检查自赋值再释放是为了避免自己把自己干掉了。5.4 前置和后置的重载这是运算符重载里最精巧的设计之一classDate{public:// 前置d1 → d1.operator()// 返回引用因为后的d1还在不需要产生临时对象Dateoperator(){*this1;return*this;}// 后置d1 → d1.operator(0)// 多一个 int 参数仅用于区分前置和后置没有实际意义// 返回值不能是引用因为返回的是加之前的临时副本Dateoperator(int){Datetmp(*this);// 先保存当前状态*this1;// 再自增returntmp;// 返回自增前的副本}};区别一目了然特性前置d后置d函数签名Date operator()Date operator(int)返回值引用自增后的自己值自增前的拷贝性能无额外拷贝有一次临时拷贝的构造和析构效率更高较低能用前置就不要用后置——不是为了炫技是少了那一次不必要的临时拷贝。这个原则所有带自增自减运算符的类都适用。5.5 流插入和流提取的重载和的重载有一个「坑」不能写成成员函数。// 错误写法——成员函数版本classDate{public:voidoperator(ostreamout){out_year-_month-_day;}};// 使用d1 cout; ← 和直觉相反谁是左操作数成员函数的第一个参数隐含是thisDate*这意味着d1 cout才是对的——d1是左操作数cout是右操作数。这和所有人的使用习惯反着来没法用。正确写法——全局函数配合友元classDate{friendostreamoperator(ostreamout,constDated);friendistreamoperator(istreamin,Dated);private:int_year;int_month;int_day;};ostreamoperator(ostreamout,constDated){outd._year年d._month月d._day日;returnout;// 返回 ostream 支持连续输出cout d1 d2}istreamoperator(istreamin,Dated){ind._yeard._monthd._day;returnin;// 返回 istream 支持连续输入cin d1 d2}friend给了这两个全局函数访问Date私有成员的权限。返回值是流对象的引用支持cout d1 d2这种链式调用。六、const成员函数classDate{public:voidPrint()const{// 等价于 void Print(const Date* const this)// 不能修改任何成员变量cout_year-_month-_dayendl;}};intmain(){Dated1(2025,1,1);d1.Print();// OK非const对象可以调用const成员函数constDated2(2025,1,1);d2.Print();// OKconst对象只能调用const成员函数// d2.Modify(); // 错误const对象不能调用非const成员函数return0;}const修饰的是隐含的this指针从Date* const变成const Date* const。权限规则只缩小不放宽——非const对象可以调用const成员函数const对象不能调用非const的。一个常被忽略的点只要一个成员函数不修改成员变量就应该加上const。不光是安全性更重要的是const对象调用它才能用——你不加constconst Date就不能调用Print()。总结四件事串起来构造函数管初始化替代手动Init()。内置类型不管自定义类型调用它们的构造。析构函数管清理有malloc/new就必须写。MyQueue那种全靠成员的不用写。拷贝构造函数管克隆。编译器默认浅拷贝——纯值类型够用有指针动态内存必须自己实现深拷贝。赋值运算符重载管两个已存在对象之间的数据转移。自赋值检查是必要的对资源型类尤其关键。以上四个合起来叫Big Four是C类设计的核心骨架。下面还有两个默认成员函数取地址重载绝大多数场景用编译器的即可不需要显式实现除非你想刻意禁止别人获取该类对象的地址。