C++真正的入门:类和对象中
2026/7/24 15:18:48 网站建设 项目流程

文章目录

  • 前言
    • 本篇文章是对类和对象的补充,想了解类和对象前面内容的可以看看这篇博客 [https://blog.csdn.net/xsc_74759805/article/details/162817432?spm=1011.2415.3001.10575&sharefrom=mp_manage_link](https://blog.csdn.net/xsc_74759805/article/details/162817432?spm=1011.2415.3001.10575&sharefrom=mp_manage_link)。
  • 一、类的默认成员函数(续)
    • 1.运算符重载
      • 特点
      • 常见问题
      • 补充
    • 2.赋值运算符重载
      • 特点
      • 与拷贝构造的区别
    • 3. 取地址运算符重载
      • const成员函数
      • 取地址运算符重载
  • 二、初始化列表
    • 1.定义
    • 2.特点
    • 3.总结
    • 4.补充

前言

本篇文章是对类和对象的补充,想了解类和对象前面内容的可以看看这篇博客https://blog.csdn.net/xsc_74759805/article/details/162817432?spm=1011.2415.3001.10575&sharefrom=mp_manage_link。

一、类的默认成员函数(续)

1.运算符重载

特点

  1. 当运算符作用于类类型对象时,C++ 允许通过运算符重载为运算符赋予新语义。C++ 规定,类对象使用运算符时,会自动转换为调用对应重载函数;若无匹配的重载函数,代码编译报错。

  2. 运算符重载本质是命名特殊的函数,函数名由operator关键字 + 需要重载的运算符组合而成。和普通函数一致,拥有返回值、参数列表与函数体。

  3. 重载运算符的参数数量,和该运算符原本操作数数量相等:一元运算符对应 1 个参数,二元运算符对应 2 个参数;二元运算符左侧操作数传给第一个参数,右侧操作数传给第二个参数。

// 负号 -a,唯一操作数a传给唯一参数//全局Personoperator-(constPerson&p){returnPerson(-p.age);}// 使用:Person b = -a;// 操作数a → 第一个(唯一)参数 p//成员Person Person::operator-(){returnPerson(-this->age);}// 使用:Person b = -a;// 隐含:this = &a(占用唯一操作数),无需显式参数

注意
上述内容只适用于全局重载运算符,所有操作数全部显式作为函数参数,参数个数严格等于运算符操作数;当你写成员函数重载时由于this指针的隐藏,会少一个参数

  1. 若将运算符重载实现为类成员函数,第一个操作数会隐式绑定this指针,因此成员形式的运算符重载,参数个数比操作数数量少一个。

  2. 运算符重载不会改变原有运算符的优先级与结合性,和内置类型运算符规则保持相同。

  3. 以下 5 个运算符禁止重载:.*、::、sizeof、?:、.(选择题高频考点,需要熟记)。

  4. 重载运算符的参数中,至少要有一个类 / 自定义类型参数;不能修改内置类型运算符逻辑,例如不允许写int operator+(int x, int y)。
    原因
    如果允许全部是内置类型,编译器无法区分是调用你写的重载函数,还是语言原生内置运算。例:若允许 int operator+(int a, int b),编译器遇到 1+2 时,分不清该用原生加法还是你的自定义函数,产生二义性。彻底破坏 C++ 基础语法一致性。

合法示例// 自定义类classPoint{intx,y;};// 全局重载:至少一个Point(自定义类型)Pointoperator+(Point a,Point b){return{a.x+b.x,a.y+b.y};}// 成员重载:自带this(Point*),天然满足条件Pointoperator+(Point b){return{this->x+b.x,this->y+b.y};}
  1. 类不需要重载全部运算符,仅重载对当前类有实际意义的运算符即可。例如Date类重载operator-有业务含义,但重载operator+无实际作用。

  2. 自增运算符++分前置++a与后置a++,二者重载函数名均为operator++,无法直接区分。C++ 规定:后置++重载时额外增加一个int形参,以此和前置++形成函数重载,完成区分。

Date&operator++(){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;}
  1. 流运算符<<、>>必须重载为全局函数:如果写成成员函数,this会占据第一个参数(运算符左操作数),调用语法会变成对象<<cout,不符合阅读与使用习惯。
    实现为全局函数时,将ostream/istream流对象放在第一个参数,自定义类对象作为第二个参数,就能支持常规cout << 对象写法。
// 错误示范:成员函数重载<<classTest{public:ostream&operator<<(ostream&os){os<<"数据";returnos;}};intmain(){Test t;t<<cout;// 只能这么写,极度反人类// cout << t; 直接编译报错return0;}
#include<iostream>usingnamespacestd;classPerson{private:string name;intage;public:Person(string n,inta):name(n),age(a){}// 声明全局流运算符为友元,访问私有成员friendostream&operator<<(ostream&os,constPerson&p);friendistream&operator>>(istream&is,Person&p);};// 输出流 << 全局重载ostream&operator<<(ostream&os,constPerson&p){os<<"姓名:"<<p.name<<",年龄:"<<p.age;returnos;// 返回流引用,支持链式调用 cout << a << b;}// 输入流 >> 全局重载(对象不能const,要修改成员)istream&operator>>(istream&is,Person&p){is>>p.name>>p.age;returnis;}intmain(){Personp("张三",20);cout<<p<<endl;// 编译器翻译成operator<<(cout, p);Personp2("",0);cin>>p2;cout<<p2;return0;}
  1. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。

常见问题

  1. 重载为全局的面临对象访问私有成员变量的问题:
    方法 1:把私有成员改成 public 公有
    方法 2:类提供 getXXX 读取接口
#include<iostream>usingnamespacestd;classDate{private:intyear,month,day;// 私有不变public:Date(inty=2026,intm=1,intd=1):year(y),month(m),day(d){}// getter接口intgetYear()const{returnyear;}intgetMonth()const{returnmonth;}intgetDay()const{returnday;}};// 全局运算符重载,通过getter间接访问私有Dateoperator+(constDate&d,intn){returnDate(d.getYear(),d.getMonth(),d.getDay()+n);}intmain(){Dated(2026,7,14);Date res=d+10;cout<<res.getYear()<<"-"<<res.getMonth()<<"-"<<res.getDay();return0;}

方法 3:将全局重载函数声明为类的友元函数 friend
方法 4:不使用全局函数,把运算符重载为类的成员函数

  1. 调用运算符重载
    我们在使用运算符重载时,其实是在调用函数。
booloperator==(Dateconst&a1,Dateconst&a2){returna1._day==a2._day&&a1._month==a2._month&&a1._year==a2._year;}intmain(){Dated1(2024,7,5);Dated2(2024,7,6);// 运算符重载函数可以显示调用operator==(d1,d2);// 编译器会转换成operator==(d1, d2);d1==d2;return0;

写成成员函数则为

booloperator==(constDate&other)const{returny==other.y&&m==other.m&&d==other.d;}intmain(){Dated1(2026,7,14);Dated2(2026,7,14);Dated3(2026,8,1);// 隐式调用,编译器自动转换boolret1=d1==d2;cout<<"隐式调用 d1 == d2:"<<ret1<<endl;// 显式调用,直接调用成员函数 operator==boolret2=d1.operator==(d2);cout<<"显式调用 d1.operator==(d2):"<<ret2<<endl;}

注意这里的d1传的是左边隐藏的this指针,而d2则是代表右边other。

补充

typedefvoid(A::*PF)();//成员函数指针类型intmain(){// C++规定成员函数要加&才能取到函数指针PF pf=&A::func; A obj;//定义A类对象tmp// 对象调⽤成员函数指针时,使⽤.*运算符(obj.*pf)();return0;}

2.赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象之间的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

特点

  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。

原因
赋值运算符左侧必须是当前类对象,编译器要求operator=左操作数作为调用者this指针,全局函数无法隐式持有this,因此标准强制operator=只能是成员函数。对比:+、-、<<、>>可全局友元,=、()、[]、->只能成员。

classPerson{private:string name;public:// 构造Person(string n="unknown"):name(n){cout<<"构造函数:"<<name<<endl;}// 拷贝构造函数(观察拷贝触发)Person(constPerson&p){name=p.name;cout<<"拷贝构造触发!复制:"<<name<<endl;}// 方式1:错误示范,参数为值传递 Person pPersonoperator=(Person p){this->name=p.name;return*this;}// 方式2:标准推荐 const Person& pPersonoperator=(constPerson&p){this->name=p.name;return*this;}};intmain(){Persona("张三");Personb("李四");a=b;// 赋值调用return0;}

如果使用传值传递,实参b传给形参Person p(值传递),自动调用拷贝构造函数,复制一份临时对象p,消耗更多的内存。

  1. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
    原因
    如果写成void类型,则无法进行连续赋值。不支持连续赋值 a = b = c。同时因为自动调用拷贝构造函数效率低下。

  2. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝 / 浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的赋值重载函数。

  3. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。像 Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载,也不需要我们显式实现 MyQueue 的赋值运算符重载。这里还有一个小技巧,如果一个类显式实现了析构并释放资源,那么它就需要显式写赋值运算符重载,否则就不需要。

与拷贝构造的区别

赋值重载完成两个已经存在的对象之间的拷贝赋值,而拷贝构造函数用于一个对象拷贝初始化给另一个要创建的对象。
同时为什么赋值重载返回值是*this,因为虽然对象的内容改变,但还是原来的对象。

3. 取地址运算符重载

const成员函数

  1. 将 const 修饰的成员函数称之为 const 成员函数,const 修饰成员函数放到成员函数参数列表的后面。
  2. const 实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改。
    例如const 修饰 Date 类的 Print 成员函数后,Print 隐含的 this 指针原本为 Date* const this,会被修饰为 const Date* const this。
usingnamespacestd;classDate{public:Date(intyear=1,intmonth=1,intday=1){_year=year;_month=month;_day=day;}// void Print(const Date* const this) constvoidPrint()const{cout<<_year<<"-"<<_month<<"-"<<_day<<endl;}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.定义

  1. 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
    示例:
classPerson{private:intage;public:Person(inta):age(a){cout<<"构造"<<endl;}};
  1. 初始化列表本质:C++ 类的成员变量本质是在进入构造函数函数体之前就完成内存分配与初始化。
    初始化列表就是给成员变量提供初始值的唯一初始化阶段,相当于成员变量的定义初始化位置。
    构造函数大括号 {} 内部只是赋值操作,不是初始化。

  2. 初始化列表和函数体内赋值的关键区别在于,前者是对成员变量的初始化,是第一次赋值;而后者初始化已经完成,是对前者的值进行覆盖。

2.特点

  1. 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
    原因:
    初始化列表对应成员变量创建时的初始化阶段,每个成员一生只会初始化一次,因此语法禁止在初始化列表重复书写同一成员;而构造函数内部是赋值阶段,不受此限制。
classPerson{private:int_age;public:Person(inta):_age(a){_age=a;cout<<"构造"<<endl;}};

这是允许的,初始化列表只负责第一次创建变量时初始化;函数体内只是普通赋值运算,随便覆盖多少次都没问题,不受 “只能出现一次” 限制。

  1. 引用成员变量,const 成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
    原因:
    const 成员变量:必须初始化,不可二次赋值,const 修饰的变量一旦创建就不能修改,不存在 “先默认初始化、再函数内赋值” 的操作空间。
classTest{public:constintnum;// const成员Test(intx){num=x;// 错误:num已经默认初始化,不能赋值修改const}};intmain(){Testt(10);return0;}

引用本质是别名,不存在空引用、不存在默认引用,对象创建时就必须绑定一个已存在的变量,函数体内无法后期绑定。这时就会造成空引用。

classTest{public:int&ref;// 引用成员Test(int&x){ref=x;// 错误:ref先默认构造(非法空引用),再赋值,不允许}};intmain(){inta=100;Testt(a);return0;}

如果一个自定义类没有无参默认构造函数,编译器不会自动生成默认构造,当它作为另一个类的成员时,无法自动初始化,必须手动在初始化列表调用它的有参构造。

classB{public:// 只有有参构造,编译器不会生成默认构造 B()B(intdata){cout<<"B有参构造:"<<data<<endl;}};classA{public:B b;// B没有默认构造A(intx){b=B(x);// 错误:进入函数体前编译器尝试调用B(),不存在,直接报错}};

正确案例

// 无默认构造的类classB{public:B(intval){cout<<"B构造:"<<val<<endl;}};classDemo{public:// 三种必须初始化列表的成员constintc_num;// const成员int&ref_num;// 引用成员B obj_b;// 无默认构造类成员// 正确:全部在初始化列表初始化Demo(intc,int&r,intb_val):c_num(c),ref_num(r),obj_b(b_val){// 函数体内只能做普通赋值,不能初始化上面三类成员cout<<"构造函数体执行"<<endl;}};intmain(){intnum=999;Demod(100,num,520);return0;}
  1. C++11 支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使用的。

    注意这里若初始化列表有赋值,都优先使用初始化列表里的值。

  2. 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显式在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++ 并没有规定。**即有类内就地缺省值(C++11):用缺省值初始化,内置类型无缺省值:标准不强制初始化,值是随机垃圾。**对于没有显式在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
    即无论怎样初始化列表都会进行初始化,这样优先使用它,可以提高效率。

  3. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

    先初始化 a(类中第一个声明),初始化列表写 a(b),但此时 b 还未初始化,b 是内存随机垃圾值 → a = 随机脏数。再初始化 b,赋值传入的 100 → b = 100

3.总结

无论是否显式写初始化列表,每个构造函数都有初始化列表,无论是否在初始化列表显式初始化成员变量,每个成员变量都要走初始化列表初始化。

4.补充

对于自定义成员变量的初始化列表的使用,通常是因为其没有默认构造函数。在使用初始化列表时,我们不应该直接传递整个变量,而是给自定义成员变量传递参数进行赋值。

// 自定义类:只有带参构造,无默认构造classBook{public:string name;Book(string n):name(n){}};classShop{public:Book b;Shop():b("C++编程"){}};

还有一点内容放到后面讲解,请关注我的主页。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询