资讯详情

资讯详情

【C++——类和对象(下)】

目录一、运算符重载1.全局运算符重载的访问问题2. 运算符重载3.流插入 、流提取 重载1流插入运算符重载2流提取运算符重载4.赋值运算符重载5.取地址运算符重载1const成员函数2取地址运算符重载二、构造函数深入三、类型转换四、static 静态成员五、友元六、内部类1.匿名对象七、对象拷贝时的编译器优化一、运算符重载C 允许对类类型对象重载运算符给运算符赋予自定义行为C规定类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编译报错运算符重载本质是特殊函数函数名格式operator运算符返回值类型operator运算符(参数列表){函数体;}规则参数个数和运算符操作数保持一致。二元运算符左操作数对应第一个参数右操作数对应第二个参数。调用两种写法显式调用operator(x1,x2)简写x1x2编译器自动转换重载之后运算符优先级、结合性不会改变与对应内置类型运算符保持一致不能创造全新运算符五个运算符不能重载.*、::域作用限定符、sizeof、?:三目运算符、.对象.成员。重载运算符至少要有一个类类型参数不能修改内置类型运算符行为。Tips一个类需要重载哪些运算符是看哪些运算符重载后有意义1.全局运算符重载的访问问题如果把运算符重载写成全局函数无法直接访问类的 private 私有成员三种解决方案修改成员为 public破坏封装不推荐提供GetXxx()get 接口获取私有成员外部获取它的拷贝例intGetYear(){return_year;}3.将运算符重载作为类的成员函数写成成员函数第一个操作数直接由隐含this指针接管参数列表参数数量相比全局版本少一个重载运算符函数调用//成员函数调用x1.operator(x2);//简写形式x1x2;Tips如果全局版本和成员版本同时存在优先调用成员版本2. 运算符重载分为前置d1、后置d1函数名都是operator。C 语法约定后置 多增加一个 int 类型哑元参数用来做函数重载区分//后置 d1Dateoperator(int);//前置 d1Dateoperator();3.流插入 、流提取 重载cout 对象流插入、cin 对象流提取不能重载为成员函数因为二元运算符左操作数是cout/cin成员函数会抢占左操作数为对象本身this指针默认抢占了第一个形参位置调用语法变成对象 cout不符合习惯所以、重载为全局函数ostream/istream放到第一个形参位置,第二个形参位置当类类型对象ostream、istream必须传引用流对象不支持拷贝构造。如果需要访问私有成员可以使用friend友元声明1流插入运算符重载//友元声明写在类内部friendostreamoperator(ostreamout,constDated);//全局实现ostreamoperator(ostreamout,constDated){outd._year年d._month月d._day日endl;returnout;}返回值是左操作数因此返回ostream引用目的支持连续输出coutd1d2;2流提取运算符重载读取修改对象内容第二个参数不加 constfriendistreamoperator(istreamin,Dated);istreamoperator(istreamin,Dated){cout请依次输入年月日;ind._yeard._monthd._day;returnin;}4.赋值运算符重载赋值重载是默认成员函数用于两个已经存在的对象之间赋值拷贝区分拷贝构造 vs 赋值重载拷贝构造新对象正在创建用已有对象初始化例Date d4 d2;赋值重载两个对象都已经存在例d1 d2;intmain(){Dated1(2026,9,26);Dated2(2026,9,27);d1d2;//赋值重载Dated3(d2);//拷贝构造创建新对象Date d4d2;//拷贝构造return0;}赋值重载特点赋值运算符operator必须重载为成员函数。参数推荐const 类名避免传值带来拷贝返回值推荐类引用支持连续赋值d1d2d3;提升效率。返回*this用户没有实现编译器自动生成默认赋值重载行为类似默认拷贝构造内置类型浅拷贝逐字节复制自定义类型调用它的赋值重载如果一个类显式实现了析构并释放资源也就是类内部管理堆资源malloc/new必须手写赋值重载否则浅拷贝导致 double free 崩溃连续赋值和返回*this赋值表达式的返回值是左操作数操作数从右向左连续赋值赋值运算符重载也应该实现这样的效果#includeiostreamDateoperator(constDated){_yeard._year;_monthd._month;_dayd._day;return*this;}如果要实现d3d1返回值应该为d3d1传给dd是d1的别名d3传给thisthis是存储d3地址的指针所以这里返回*this完整示例#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}//赋值运算符重载Dateoperator(constDated){_yeard._year;_monthd._month;_dayd._day;return*this;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};5.取地址运算符重载1const成员函数const写在成员函数括号后面称为 const 成员函数voidPrint()const;底层含义修饰该函数的隐含this指针此时this指针类型变成const Date* const thisconst 成员函数内部不能修改类的成员变量使用场景constDated1(2026,9,29);d1.Print();因为d1是 const 对象传递给普通成员函数后发生权限放大const 指针赋值给普通指针编译报错d1地址的类型是const Date*指针类型默认情况下void Date::Print();这里的指针是Date*严格来说这里是Date* const thisconst修饰指针本身要想编译不报错这里this指针的类型也应该为const Date*但是由于this指针无法修改因此这里采用const修饰的成员函数放到成员函数参数列表的后面把void Date::Print();变为void Date::Print() const;const实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员函数进行修改**编码建议**凡是不修改成员变量的成员函数全部加上const修饰2取地址运算符重载取地址也是默认成员函数绝大多数场景编译器自动生成版本就够用极少手动实现。提供两套重载分别适配普通对象、const 对象普通对象取地址运算符重载Date*operator(){returnthis;}const对象取地址运算符重载constDate*operator()const{returnthis;}二、构造函数深入构造函数初始化除了在函数体内赋值还可以采用初始化列表的方式初始化列表语法格式以一个冒号开始接着以一个逗号分隔的数据成员列表每个“成员变量”后面跟一个放在括号中的初始值或表达式类名(参数列表):成员1(初始值),成员2(初始值),成员3(表达式){//函数体赋值}初始化列表核心要点可以理解为成员变量定义初始化的地方比函数体内赋值更早执行。每一个成员变量初始化列表最多只能出现一次。三类成员必须写在初始化列表初始化否则编译报错引用成员变量const 修饰成员变量没有默认构造函数的自定义类型成员C11 支持类内给成员缺省值这个缺省值给没有写在初始化列表的成员使用。⚠️初始化列表初始化顺序按照类中成员声明顺序执行和初始化列表书写先后无关建议初始化列表书写顺序和类里面声明顺序保持一致规避 bug每个成员都要走初始化列表尽量使用初始化列表初始化不在初始化列表初始化的成员这个成员在声明位置给了缺省值初始化列表会用这个缺省值初始化没有给缺省值对于没有显示在初始化列表实现初始化的内置类型成员是否初始化取决于编译器C并没有规定大概率是随机值自定义类型成员会调用这个成员类型的默认构造函数如果没有默认构造会编译错误#includeiostreamusingnamespacestd;classTime{public:Time(inthour0):_hour(hour){coutTime()endl;}private:int_hour;};classDate{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}//声明 给初始化列表缺省值private:int_year1;int_month1;int_day1;int*ptr(int*)malloc(12);//缺省值可以是表达式形式Time _t1;};intmain(){//对象定义Dated1(2026,10,1);return0;}初始化列表中按照成员变量在类中声明顺序进行初始化跟成员在初始化列表出现的先后顺序无关建议声明顺序和初始化顺序保持一致三、类型转换单参数构造函数支持内置类型隐式转换为自定义类对象#includeiostreamusingnamespacestd;classA{public:A(inta){_a1a;}A(constAaa){_a1aa._a1;}voidPrint(){cout_a1endl;}private:int_a1;};intmain(){Aaa1(1);aa1.Print();//隐式类型转换先用2构造临时A对象再拷贝构造aa2A aa22;aa2.Print();return0;}现代编译器会做优化上面这种连续的「构造 拷贝构造」直接合并变成直接构造对象多参数支持列表初始化上面的是单个参数列表初始化单参数直接转的本质是一个内置类型转为自定义类型多个参数不支持上面的单参数写法采用下面的写法A aa5{1,1};constAraa6{2,2};如果不想允许隐式类型转换在构造函数前面加上explicit关键字。explicit A(int a);禁止单参数构造函数的隐式转换。四、static 静态成员静态成员变量用static修饰的成员变量重点✨静态成员变量必须在类外面初始化不属于某个对象存放在静态全局区所有对象共享同一份。#includeiostreamusingnamespacestd;classA{private:staticint_scount;//类内只是声明};intA::_scount0;//类外初始化定义intmain(){coutsizeof(A)endl;return0;}**静态成员函数**用static修饰的成员函数例staticintGetACount(){return_scount;}静态成员函数没有 this 指针静态成员函数只能访问静态成员不能直接访问非静态成员没有 this普通非静态成员函数可以访问静态成员访问静态成员两种方式//类名::静态成员 突破类域访问推荐coutA::GetACount()endl;//对象.静态成员 访问A a;couta.GetACount()endl;静态成员也是类的成员依然受public/private/protected访问限定符限制五、友元友元提供了一种突破类访问限定符封装的方式可以访问类的私有、保护成员友元分为友元函数和友元类在函数声明或者类声明的前面加friend并且把友元声明写在类内部友元函数外部友元函数可以访问类的私有和保护成员友元函数仅仅是一种声明不是类的成员函数友元函数可以在类的任何地方声明不受类访问限定符限制一个函数可以是多个类的友元函数friendostreamoperator(ostreamout,constDated);友元类友元类中的成员函数都可以是另一个类的友元函数都可以访问另一个类中的私有和保护成员classA{friendclassB;//...};classB{//...};友元类的关系是单向的不具有交换性友元类的关系不能传递Tips友元有时提供了便利但是会增加耦合度破坏封装所以友元不宜多用六、内部类如果一个类定义在另一个类的内部那这个类就叫内部类内部类是独立的类跟定义在全局相比它只是受外部类类域和访问限定符管控外部类对象内存中不会包含内部类对象内部类默认是外部类的友元类适用场景内部类本质也是一种封装当A类和B类紧密关联A类实现出来主要就是给B类使用那么可以考虑把A类设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其他就不用考虑了#includeiostreamusingnamespacestd;classA{private:staticint_k;int_h1;public:classB//B默认是A的友元{public:voidfoo(constAa){//默认友元可以直接访问私有cout_kendl;couta.hendl;}private:int_b1;};};intmain(){coutsizeof(A)endl;//只有_hA::B b;//受类域限制无法直接定义需要指定类域return0;}1.匿名对象没有变量名的临时对象生命周期只在当前这一行语句结束#includeiostreamusingnamespacestd;intmain(){A aa1;//有名对象A();//匿名对象A(1);//匿名对象solution st;coutst.Sum_Solution(10)endl;//有名对象调用coutSolution().Sum_Solution(10)endl;//匿名对象调用//排序inta[]{5,6,3,7,2,1};//有名对象greaterintgt;sort(a,a5,gt);//匿名对象sort(a,a5,greaterint());return0;}TipsA aa2();这种定义方法是错误的因为这样编译器无法识别是函数声明还是对象定义七、对象拷贝时的编译器优化现代编译器会为了尽可能提高程序的效率在不影响正确性的情况下尽可能减少一些传参和传返回值过程中可以省略的拷贝如何优化C标准并没有严格规定各个编译器会根据情况自行处理当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化有些更新更“激进”的编译还会进行跨行跨表达式的合并优化1.直接构造优化A aa11;语法上1先构造一个临时对象临时对象再拷贝构造编译器在这里优化为直接构造const A aa21;直接构造2.传参场景优化void f1(A aa){}变为void f1(A aa){}减少中间拷贝构造A aa1(1);f1(aa1);变为f1(A(1);匿名对象先构造在拷贝构造在连续构造中编译器优化合二为一直接构造如果再改为f1(1);1走隐式类型转换类型转换过程中会产生临时对象临时对象再进行拷贝构造这里编译器优化合二为一直接构造3.传返回值Af2(){A aa;returnaa;}传值传参返回会产生临时对象也是构造加拷贝构造会被编译器合二为一优化跨行、跨语句不一定会触发拷贝优化不要业务逻辑依赖编译器拷贝优化行为
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →