C++真正的入门:类和对象中

📅 发布时间:2026/7/18 17:20:13
C++真正的入门:类和对象中 文章目录前言本篇文章是对类和对象的补充想了解类和对象前面内容的可以看看这篇博客 [https://blog.csdn.net/xsc_74759805/article/details/162817432?spm1011.2415.3001.10575sharefrommp_manage_link](https://blog.csdn.net/xsc_74759805/article/details/162817432?spm1011.2415.3001.10575sharefrommp_manage_link)。一、类的默认成员函数续1.运算符重载特点常见问题补充2.赋值运算符重载特点与拷贝构造的区别3. 取地址运算符重载const成员函数取地址运算符重载二、初始化列表1.定义2.特点3.总结4.补充前言本篇文章是对类和对象的补充想了解类和对象前面内容的可以看看这篇博客https://blog.csdn.net/xsc_74759805/article/details/162817432?spm1011.2415.3001.10575sharefrommp_manage_link。一、类的默认成员函数续1.运算符重载特点当运算符作用于类类型对象时C 允许通过运算符重载为运算符赋予新语义。C 规定类对象使用运算符时会自动转换为调用对应重载函数若无匹配的重载函数代码编译报错。运算符重载本质是命名特殊的函数函数名由operator关键字 需要重载的运算符组合而成。和普通函数一致拥有返回值、参数列表与函数体。重载运算符的参数数量和该运算符原本操作数数量相等一元运算符对应 1 个参数二元运算符对应 2 个参数二元运算符左侧操作数传给第一个参数右侧操作数传给第二个参数。// 负号 -a唯一操作数a传给唯一参数//全局Personoperator-(constPersonp){returnPerson(-p.age);}// 使用Person b -a;// 操作数a → 第一个唯一参数 p//成员Person Person::operator-(){returnPerson(-this-age);}// 使用Person b -a;// 隐含this a占用唯一操作数无需显式参数注意上述内容只适用于全局重载运算符所有操作数全部显式作为函数参数参数个数严格等于运算符操作数当你写成员函数重载时由于this指针的隐藏会少一个参数若将运算符重载实现为类成员函数第一个操作数会隐式绑定this指针因此成员形式的运算符重载参数个数比操作数数量少一个。运算符重载不会改变原有运算符的优先级与结合性和内置类型运算符规则保持相同。以下 5 个运算符禁止重载.*、::、sizeof、?:、.选择题高频考点需要熟记。重载运算符的参数中至少要有一个类 / 自定义类型参数不能修改内置类型运算符逻辑例如不允许写int operator(int x, int y)。原因如果允许全部是内置类型编译器无法区分是调用你写的重载函数还是语言原生内置运算。例若允许 int operator(int a, int b)编译器遇到 12 时分不清该用原生加法还是你的自定义函数产生二义性。彻底破坏 C 基础语法一致性。合法示例// 自定义类classPoint{intx,y;};// 全局重载至少一个Point自定义类型Pointoperator(Point a,Point b){return{a.xb.x,a.yb.y};}// 成员重载自带thisPoint*天然满足条件Pointoperator(Point b){return{this-xb.x,this-yb.y};}类不需要重载全部运算符仅重载对当前类有实际意义的运算符即可。例如Date类重载operator-有业务含义但重载operator无实际作用。自增运算符分前置a与后置a二者重载函数名均为operator无法直接区分。C 规定后置重载时额外增加一个int形参以此和前置形成函数重载完成区分。Dateoperator(){cout前置endl;//...return*this;}Dateoperator(int){Date tmp;cout后置endl;//....returntmp;}intmain(){Dated1(2024,7,5);Dated2(2024,7,6);// 编译器会转换成d1.operator();d1;// 编译器会转换成d1.operator(0);d1;return0;}流运算符、必须重载为全局函数如果写成成员函数this会占据第一个参数运算符左操作数调用语法会变成对象cout不符合阅读与使用习惯。实现为全局函数时将ostream/istream流对象放在第一个参数自定义类对象作为第二个参数就能支持常规cout 对象写法。// 错误示范成员函数重载classTest{public:ostreamoperator(ostreamos){os数据;returnos;}};intmain(){Test t;tcout;// 只能这么写极度反人类// cout t; 直接编译报错return0;}#includeiostreamusingnamespacestd;classPerson{private:string name;intage;public:Person(string n,inta):name(n),age(a){}// 声明全局流运算符为友元访问私有成员friendostreamoperator(ostreamos,constPersonp);friendistreamoperator(istreamis,Personp);};// 输出流 全局重载ostreamoperator(ostreamos,constPersonp){os姓名p.name年龄p.age;returnos;// 返回流引用支持链式调用 cout a b;}// 输入流 全局重载对象不能const要修改成员istreamoperator(istreamis,Personp){isp.namep.age;returnis;}intmain(){Personp(张三,20);coutpendl;// 编译器翻译成operator(cout, p);Personp2(,0);cinp2;coutp2;return0;}不能通过连接语法中没有的符号来创建新的操作符比如operator。常见问题重载为全局的面临对象访问私有成员变量的问题方法 1把私有成员改成 public 公有方法 2类提供 getXXX 读取接口#includeiostreamusingnamespacestd;classDate{private:intyear,month,day;// 私有不变public:Date(inty2026,intm1,intd1):year(y),month(m),day(d){}// getter接口intgetYear()const{returnyear;}intgetMonth()const{returnmonth;}intgetDay()const{returnday;}};// 全局运算符重载通过getter间接访问私有Dateoperator(constDated,intn){returnDate(d.getYear(),d.getMonth(),d.getDay()n);}intmain(){Dated(2026,7,14);Date resd10;coutres.getYear()-res.getMonth()-res.getDay();return0;}方法 3将全局重载函数声明为类的友元函数 friend方法 4不使用全局函数把运算符重载为类的成员函数调用运算符重载我们在使用运算符重载时其实是在调用函数。booloperator(Dateconsta1,Dateconsta2){returna1._daya2._daya1._montha2._montha1._yeara2._year;}intmain(){Dated1(2024,7,5);Dated2(2024,7,6);// 运算符重载函数可以显示调用operator(d1,d2);// 编译器会转换成operator(d1, d2);d1d2;return0;写成成员函数则为booloperator(constDateother)const{returnyother.ymother.mdother.d;}intmain(){Dated1(2026,7,14);Dated2(2026,7,14);Dated3(2026,8,1);// 隐式调用编译器自动转换boolret1d1d2;cout隐式调用 d1 d2ret1endl;// 显式调用直接调用成员函数 operatorboolret2d1.operator(d2);cout显式调用 d1.operator(d2)ret2endl;}注意这里的d1传的是左边隐藏的this指针而d2则是代表右边other。补充typedefvoid(A::*PF)();//成员函数指针类型intmain(){// C规定成员函数要加才能取到函数指针PF pfA::func; A obj;//定义A类对象tmp// 对象调⽤成员函数指针时使⽤.*运算符(obj.*pf)();return0;}2.赋值运算符重载赋值运算符重载是一个默认成员函数用于完成两个已经存在的对象之间的拷贝赋值这里要注意跟拷贝构造区分拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。特点赋值运算符重载是一个运算符重载规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用否则会传值传参会有拷贝。原因赋值运算符左侧必须是当前类对象编译器要求operator左操作数作为调用者this指针全局函数无法隐式持有this因此标准强制operator只能是成员函数。对比、-、、可全局友元、()、[]、-只能成员。classPerson{private:string name;public:// 构造Person(string nunknown):name(n){cout构造函数nameendl;}// 拷贝构造函数观察拷贝触发Person(constPersonp){namep.name;cout拷贝构造触发复制nameendl;}// 方式1错误示范参数为值传递 Person pPersonoperator(Person p){this-namep.name;return*this;}// 方式2标准推荐 const Person pPersonoperator(constPersonp){this-namep.name;return*this;}};intmain(){Persona(张三);Personb(李四);ab;// 赋值调用return0;}如果使用传值传递实参b传给形参Person p值传递自动调用拷贝构造函数复制一份临时对象p消耗更多的内存。有返回值且建议写成当前类类型引用引用返回可以提高效率有返回值目的是为了支持连续赋值场景。原因如果写成void类型则无法进行连续赋值。不支持连续赋值 a b c。同时因为自动调用拷贝构造函数效率低下。没有显式实现时编译器会自动生成一个默认赋值运算符重载默认赋值运算符重载行为跟默认拷贝构造函数类似对内置类型成员变量会完成值拷贝 / 浅拷贝一个字节一个字节的拷贝对自定义类型成员变量会调用它的赋值重载函数。像 Date 这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的赋值运算符重载就可以完成需要的拷贝所以不需要我们显式实现赋值运算符重载。像 Stack 这样的类虽然也都是内置类型但是_a 指向了资源编译器自动生成的赋值运算符重载完成的值拷贝 / 浅拷贝不符合我们的需求所以需要我们自己实现深拷贝对指向的资源也进行拷贝。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载也不需要我们显式实现 MyQueue 的赋值运算符重载。这里还有一个小技巧如果一个类显式实现了析构并释放资源那么它就需要显式写赋值运算符重载否则就不需要。与拷贝构造的区别赋值重载完成两个已经存在的对象之间的拷贝赋值而拷贝构造函数用于一个对象拷贝初始化给另一个要创建的对象。同时为什么赋值重载返回值是*this因为虽然对象的内容改变但还是原来的对象。3. 取地址运算符重载const成员函数将 const 修饰的成员函数称之为 const 成员函数const 修饰成员函数放到成员函数参数列表的后面。const 实际修饰该成员函数隐含的 this 指针表明在该成员函数中不能对类的任何成员进行修改。例如const 修饰 Date 类的 Print 成员函数后Print 隐含的 this 指针原本为 Date* const this会被修饰为 const Date* const this。usingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// void Print(const Date* const this) constvoidPrint()const{cout_year-_month-_dayendl;}private:int_year;int_month;int_day;};取地址运算符重载取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载一般这两个函数编译器自动生成的就足够我们使用不需要显示调用。除非是一些很特殊的场景比如我们不想让别人获取到当前类对象的地址就可以自己手动实现一份随意返回一个地址。classDate{public:Date*operator(){returnthis;// return nullptr;}constDate*operator()const{returnthis;// return nullptr;}private:int_year;// 年int_month;// ⽉int_day;// ⽇};二、初始化列表1.定义之前我们实现构造函数时初始化成员变量主要使用函数体内赋值构造函数初始化还有一种方式就是初始化列表初始化列表的使用方式是以一个冒号开始接着是一个以逗号分隔的数据成员列表每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。示例classPerson{private:intage;public:Person(inta):age(a){cout构造endl;}};初始化列表本质C 类的成员变量本质是在进入构造函数函数体之前就完成内存分配与初始化。初始化列表就是给成员变量提供初始值的唯一初始化阶段相当于成员变量的定义初始化位置。构造函数大括号 {} 内部只是赋值操作不是初始化。初始化列表和函数体内赋值的关键区别在于前者是对成员变量的初始化是第一次赋值而后者初始化已经完成是对前者的值进行覆盖。2.特点每个成员变量在初始化列表中只能出现一次语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。原因初始化列表对应成员变量创建时的初始化阶段每个成员一生只会初始化一次因此语法禁止在初始化列表重复书写同一成员而构造函数内部是赋值阶段不受此限制。classPerson{private:int_age;public:Person(inta):_age(a){_agea;cout构造endl;}};这是允许的初始化列表只负责第一次创建变量时初始化函数体内只是普通赋值运算随便覆盖多少次都没问题不受 “只能出现一次” 限制。引用成员变量const 成员变量没有默认构造的类类型变量必须放在初始化列表位置进行初始化否则会编译报错。原因const 成员变量必须初始化不可二次赋值const 修饰的变量一旦创建就不能修改不存在 “先默认初始化、再函数内赋值” 的操作空间。classTest{public:constintnum;// const成员Test(intx){numx;// 错误num已经默认初始化不能赋值修改const}};intmain(){Testt(10);return0;}引用本质是别名不存在空引用、不存在默认引用对象创建时就必须绑定一个已存在的变量函数体内无法后期绑定。这时就会造成空引用。classTest{public:intref;// 引用成员Test(intx){refx;// 错误ref先默认构造非法空引用再赋值不允许}};intmain(){inta100;Testt(a);return0;}如果一个自定义类没有无参默认构造函数编译器不会自动生成默认构造当它作为另一个类的成员时无法自动初始化必须手动在初始化列表调用它的有参构造。classB{public:// 只有有参构造编译器不会生成默认构造 B()B(intdata){coutB有参构造dataendl;}};classA{public:B b;// B没有默认构造A(intx){bB(x);// 错误进入函数体前编译器尝试调用B()不存在直接报错}};正确案例// 无默认构造的类classB{public:B(intval){coutB构造valendl;}};classDemo{public:// 三种必须初始化列表的成员constintc_num;// const成员intref_num;// 引用成员B obj_b;// 无默认构造类成员// 正确全部在初始化列表初始化Demo(intc,intr,intb_val):c_num(c),ref_num(r),obj_b(b_val){// 函数体内只能做普通赋值不能初始化上面三类成员cout构造函数体执行endl;}};intmain(){intnum999;Demod(100,num,520);return0;}C11 支持在成员变量声明的位置给缺省值这个缺省值主要是给没有显式在初始化列表初始化的成员使用的。注意这里若初始化列表有赋值都优先使用初始化列表里的值。尽量使用初始化列表初始化因为那些你不在初始化列表初始化的成员也会走初始化列表如果这个成员在声明位置给了缺省值初始化列表会用这个缺省值初始化。如果你没有给缺省值对于没有显式在初始化列表初始化的内置类型成员是否初始化取决于编译器C 并没有规定。**即有类内就地缺省值C11用缺省值初始化内置类型无缺省值标准不强制初始化值是随机垃圾。**对于没有显式在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数如果没有默认构造会编译错误。即无论怎样初始化列表都会进行初始化这样优先使用它可以提高效率。初始化列表中按照成员变量在类中声明顺序进行初始化跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。先初始化 a类中第一个声明初始化列表写 a(b)但此时 b 还未初始化b 是内存随机垃圾值 → a 随机脏数。再初始化 b赋值传入的 100 → b 1003.总结无论是否显式写初始化列表每个构造函数都有初始化列表无论是否在初始化列表显式初始化成员变量每个成员变量都要走初始化列表初始化。4.补充对于自定义成员变量的初始化列表的使用通常是因为其没有默认构造函数。在使用初始化列表时我们不应该直接传递整个变量而是给自定义成员变量传递参数进行赋值。// 自定义类只有带参构造无默认构造classBook{public:string name;Book(string n):name(n){}};classShop{public:Book b;Shop():b(C编程){}};还有一点内容放到后面讲解请关注我的主页。