当前位置: 首页 > news >正文

C++初阶(5)类和对象(中)

1. 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类


空类中真的什么都没有吗?


并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date {};

这6个默认成员函数,重点是前4个:

  • 构造函数、析构函数、拷贝构造、赋值重载。

2. 构造函数

2.1 概念

对于以下Date类:

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;//d1.Init(2022, 7, 5);    忘了初始化d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦。

  • 痛点1:每次创建完对象都要调用初始化函数,太麻烦。
  • 痛点2:一旦忘了初始化,程序就很容易崩溃。
    • 轻则:访问到一堆随机值。
    • 重则:非法访问,程序崩溃。
  • 能否在对象创建时,就将信息设置进去呢?

构造函数是一个特殊的成员函数:

  • 函数字与类名相同;
  • 创建类类型对象时,由编译器自动调用,以保证每个数据成员都有一个合适的初始值;
  • 在对象整个生命周期内只调用一次。
  • 定义方式很特殊(不写返回值,函数名与类名相同)。
  • 调用方式很特殊(对象名+参数列表)。
  • 不显示实现,编译器也会自动生成。

2.2 特性

需要注意的是,构造函数虽然名称叫构造,但是:

  • 构造函数的功能并不是“开空间创建对象”,而是“初始化对象”。 
    (类似Init函数的功能)

局部的对象,建立栈帧的时候直接就在栈上给它开好空间了。

全局的对象,系统在静态区提前就开好了空间。

构造函数的特征如下:

  • 函数名与类名相同。
  • 构造函数没有返回值,而且不需要写void——规定。
  • 对象实例化时编译器自动调用对应的构造函数。(跟Init的最大区别)
  • 构造函数可以重载。

重载多个构造函数,就有多种初始化方式。

  • 无参——使用默认值来初始化
  • 带参
    • 无缺省——使用给定值来初始化
    • 半缺省(从右往左缺省)
    • 全缺省——既可以使用给定值来初始化,也可以使用默认值来初始化

问:一个类可以写多少个构造函数?

答:需要几个写几个,只要满足函数重载(不满足区分不开,会产生调用歧义)


无参构造、全缺省构造,满足函数重载,不会报错。

但是在无参调用时会报错——调用歧义。


所以一般都留全缺省——可替代无参的功能。

一般情况,建议每个类,都可以写一个全缺省的构造(好用)。


【注意】在函数声明和定义分离时,缺省参数只在声明给,在定义时不给,否则会报错。


自动调用观察:在调试窗口,对象实例化语句按F11就自动进入显式写出来的构造函数。



普通的函数调用:函数名+参数列表。

构造函数的函数调用:对象名+参数列表。

(没有参数列表——就不能写括号,编译器会自动匹配调用无参构造)


  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数
  • 一旦用户显式定义,编译器就不再生成。

关于编译器生成的默认成员函数,很多童鞋会有疑惑:

【疑惑】不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?

【解答】C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

问:我们没写,有没有构造函数?有->编译器自动生成——那现在使用的VS编译器自动生成的构造函数初始化成什么了?

答:啥事儿都没做,就还是随机值——VS是-858993460。

(其他编译器可能会把内置类型初始化为0,但是这是编译器自己的行为,C++的标准并没有规定默认构造函数对内置类型作什么处理)

编译器自动生成构造函数,对于内置类型成员变量,没有规定要不要做处理!(有些编译器会处理)

对于自定义类型成员变量才会调用这个类的不传参就可以调用的那个构造——默认构造。


若类的自定义类型成员没有默认构造(无参就能调用的构造),编译器会报错。


如果A没有显式实现的构造函数,那就是A默认生成的无参构造被Date默认生成的无参构造调用——一层套娃

此时VS下的_aa的_a是随机值。


再来看一个相似的例子:

编译器生成了Date::Date这个构造。


A的默认构造被Time默认生成的无参构造被Date默认生成的无参构造调用——两层套娃。

自定义类型的尽头还是内置类型,所以还得有人去进行初始化。

核心就是C++一开始设计的时候没有设计好,内置类型也应该作相应的处理。


问:自动生成的构造函数意义何在?

答:不处理内置类型,对于自定义类型还要依赖于它写的无参构造——若没写,也是调用自动生成的构造,对内置类型不做处理。


自动生成的构造的意义在于:当一个类只有自定义类型成员,且自定义类型成员具有显式实现的默认构造函数时,这个类就可以利用自动生成的构造,而不必自己实现构造。


注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁——声明缺省值

  • 内置类型成员变量在类中声明时可以给默认值(缺省值)

注意声明缺省值和定义初始化的区别:

  • 编译器自动生成的构造函数,可以使用声明缺省值,在对象实例化的时候给成员变量初始化。

声明缺省值不仅对于编译器自动生成的构造函数有效,对显式实现的构造函数也有效。

显式实现的构造函数,有对成员变量初始化操作时,以显式实现的构造函数为准,不用缺省值。

调试发现是先走缺省值,再走显式初始化——相当于覆盖了。

【注意】

  • 无参的构造函数、全缺省的构造函数,都称为默认构造函数
  • 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(无参就能调用的构造——无参调用不加括号)
  • 默认构造函数只能有一个。

类里面有多个默认构造函数时,实例化对象的时候会报错。(不实例化不会报错)


 【总结】

1、一般情况下都要自己写构造(自定义类型的尽头是内置类型)

2、少数情况下可以使用编译器默认生成的构造——那就是只有自定义类型,而且所有自定义类型都有自己写的构造。(类似MyQueue)

或者只有少量内置类型——直接用打补丁的方式给缺省值:


析构函数

3.1 概念

通过前面构造函数的学习,我们知道一个对象是怎么来的。

那一个对象又是怎么没呢的?


  • 痛点1:每次使用完对象都要调用销毁函数Destroy(),太麻烦。
  • 痛点2:一旦忘了销毁,程序就会出现内存泄露。
  • 能否在对象销毁时,就将资源自动释放呢?

与构造函数功能相反,析构函数的功能如下:

  • 析构函数不是完成对象本身的销毁,局部对象销毁工作是由编译器完成的。
  • 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

自动对象的创建和销毁都是编译器自动实现的。

  • 对象空间的建立:在函数栈帧建立的时候建立。
  • 对象空间的销毁:在函数栈帧销毁时销毁。
  • 局部对象生命周期:当前函数的栈帧。
  • 全局对象生命周期:程序。
  • 构造函数不是创建对象——编译器完成。
  • 析构函数不是销毁对象——编译器完成。

3.2 特性

析构函数是特殊的成员函数,其特征如下:

析构函数名是在类名前加上字符 ~。(按位取反)

没有返回值、没有参数——参数位置、返回值位置都是什么都不写
(这是规定,写了就报错)

这一点的存在就使得析构函数轻松许多了——这意味着析构函数没有重载。

一个类只能有一个析构函数——因为析构函数不能重载

对象生命周期结束时,C++编译系统系统自动调用析构函数

 出了作用域(对象生命周期结束),会自动调用析构函数:

例1:

例2:



是否是所有类都需要写析构函数呢?

  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。

比如——Date类;

Date类没有资源需要清理,它实例化的对象的成员变量属于某个作用域,出了作用域,函数栈帧销毁,对象(里面的成员变量)也会跟着销毁。


  • 有资源申请时,一定要显式实现析构函数,否则会造成资源泄漏。

比如——Stack类。


析构函数可以显式调用:

typedef int DataType;
using namespace std;class Stack
{
public://构造函数Stack(size_t capacity = 3){cout << "Stack(size_t capacity = 3)" << endl;	//插入打印,帮助观察构造函数的调用情况_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// ...其他接口函数...//析构函数~Stack(){cout << "~Stack()" << endl;	//插入打印,帮助观察析构函数的调用情况if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};int main()
{Stack st;//显式调用析构函数st.~Stack();return 0;
}

执行结果:

显式调用析构函数,这样析构函数被调用了两次——对象生命周期结束会由编译器自动调用一次。

析构函数内部的if判断语句可以避免对已释放内存的指针继续free。

一般析构函数都不需要显式调用。


若未显式定义,系统会自动生成默认的析构函数。

关于编译器自动生成的析构函数,是否会完成一些事情呢?

打印观察析构函数的调用,发现没有创建Time类的对象,但是~Time析构函数被调用了。


我们看到,跟构造函数类似,编译器自动生成的析构函数的作用包括:

  • 对内置类型不做处理。
  • 对自定类型成员,调用它的析构函数。
class Stack
{
public://构造函数Stack(size_t capacity = 3){cout << "Stack(size_t capacity = 3)" << endl;	//插入打印,帮助观察构造函数的调用情况_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// ...其他接口函数...//析构函数(已抹去)private:DataType* _array;int _capacity;int _size;
};

编译器自动生成的析构函数,不会释放掉_array,但是不会报错。

内存泄露对C/C++程序不会报错,因为C/C++程序检测不出内存泄露。

所以对C/C++内存泄露才可怕——程序每次跑一次就泄露一点点。

相反程序直接崩溃没那么可怕。(C++进阶会介绍智能指针防止内存泄露)


来看两个之前的例子:

两个队列实现一个栈

同学们可以使用C++的方式,自己封装栈,实现上述oj题,深刻体会编译器生成析构函数的作用。


构造函数和析构函数的最大的特点就是自动调用,可以在显式写的构造和析构函数里面加上打印语句打印观察构造函数和析构函数的自动调用,或者调试观察。


【总结】

实践当中“2-b”这种场景蛮多的(类似MyQueue)——项目里面会有各种“类型的嵌套”。


拷贝构造函数

4.1 概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

拷贝构造函数

  • 只有单个形参;
  • 该形参是对本类类型对象的引用(一般常用const修饰);
  • 在用已存在的类类型对象创建新对象时,由编译器自动调用。
    (用旧对象来初始化创建的新对象——>拷贝一份旧对象)

4.2 特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  • 拷贝构造函数是构造函数的一个重载形式

拷贝构造是一个特殊的构造(参数唯一;参数限定为引用类型;必须显式调用)

可以理解为拷贝构造函数是构造函数的一个子分类。

拷贝构造是构造函数的一种使用场景的细分,因为这种构造非常特殊,它有很多特性需要单独拿出来理解,所以需要把它单独拿出来讨论。

栈类和日期类会贯穿整个类和对象的学习——“两种典型”。

构造的意思就是初始化。

以前是使用值来进行初始化——特定的值、半给值(函数缺省值)、不给值(声明缺省值)……

这里d2调用的也是构造,我们把d2这里调用的构造叫作拷贝构造。

拷贝构造的意思就是用同类型的对象拷贝初始化。

拷贝构造是按构造函数的方式来写的:

  • 没有返回值;
  • 和类名同名;
  • 参数上的要求:
  • 拷贝构造函数的参数只有一个,并且必须是类类型对象的引用
  • 使用传值方式编译器直接报错因为会引发无穷递归调用
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d)   // 正确写法{_year = d._year;        //_year默认就是使用this指针(d2)访问的,d是d1的别名_month = d._month;_day = d._day;}Date(const Date d)    // 错误写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}
private:int _year = 1;int _month = 1;int _day = 1;
};int main()
{Date d1;Date d2(d1);         //拷贝构造:在对象初始化的时候自动调用//调试观察:(拷贝)构造函数会先走声明缺省值,再走函数体return 0;
}

拷贝构造的参数不是引用,会有编译报错。

因为当参数是Date,而不是Date&,会引发无穷递归。

调用一个函数(拷贝构造),需要传参,传参会形成一个新的拷贝构造。


  • 自定义类型的传值传参会调用对应的拷贝构造。
    (内置类型传值传参直接值拷贝)

验证:

调试同样可以发现,按F11不是进入func函数,而是进入拷贝构造:
——自定义类型的传值传参会调用拷贝构造。

【传值传参】

d(形参)是d2(实参)的一份临时拷贝,相当于用d2(实参)去初始化d(形参)。

内置类型直接拷贝,自定义类型要调用拷贝构造。

main函数的栈帧之后要调用func,但是不会建立func的栈帧,而是会先建立Date拷贝构造的栈帧。

Date拷贝构造函数调用结束,栈帧销毁,再去建立func函数的函数栈帧。


如何避免拷贝构造?——传指针(传址传参)

自定义类型的传指针 == 内置类型的传值,内置类型传值直接拷贝。

所以说指针是一种非常特殊的内置类型。

C++把指针优化为了引用:

之前调用func需要去调用一个函数(拷贝构造)完成传参。

现在按F11可以直接进入func函数。


但是实际上编译器不会跑无穷递归,编译器进行了强制检查,语法上不允许这样定义函数,会直接报错。


那能不能使用指针来实现拷贝构造?

使用指针能实现(不会无穷递归),但是只是普通构造函数,不是拷贝构造(规定参数是引用)

所以会生成默认的拷贝构造。

这两个是参数不一样的重载函数,普通构造和拷贝构造互不干扰, 根据调用参数编译器自动判断该调用普通指针构造,还是拷贝构造。

int main()
{Date d1;Date d2(&d1);    //普通指针构造Date d3(d1);     //拷贝构造
}

有些地方规定拷贝构造的参数要加上const修饰,避免在写拷贝构造函数体的时候写反了:

结果:你要拷贝我的值,不仅没拷贝成功,反倒把我原本的值改成随机值 / 声明缺省值了。

这里d是d1的别名——权限的缩小。


拷贝构造必须显式调用,调用方式还可以是下面这种:

两种写法都是在初始化,都会调用拷贝构造。


  • 若未显式定义,编译器会生成默认的拷贝构造函数。
  • 默认的拷贝构造函数,对象按内存存储、按字节序完成拷贝——浅拷贝值拷贝)。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t)		//可写可不写{_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}

默认生成的构造函数、析构函数都是对于内置类型不作处理,自定义类型去调用它的构造、析构。

默认生成的拷贝构造函数会处理内置类型——值拷贝。

对于自定义类型也是去调用它的拷贝构造。

【注意】

  • 在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的;
  • 而自定义类型是调用其拷贝构造函数完成拷贝的。

编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

当然像日期类这样的类是没必要的。

那么下面的类呢?验证一下试试?

//栈类
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){cout << "Stack(size_t capacity = 3)" << endl;_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);st1.Push(3);st1.Push(4);Stack st2(st1);
}

默认拷贝构造可以完成内置类型的值拷贝,但是若拷贝对象有内存资源申请,程序会崩溃。

调试观察,拷贝完成了,但是程序崩溃了:

日期类的内置类型成员进行值拷贝没问题,但是栈类的内置类型进行值拷贝就有问题了。

画图分析:

【注意】

  • 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;
  • 一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

对于链表、树、队列……这些会申请内存资源的类,默认生成的拷贝构造都会有问题。

浅拷贝会导致两个问题:

1. st1在push一次之后,st2也被push了,而且st2的size还不变。
——这个问题在链表和树里面也存在,即指针指向一块资源的都存在这个问题。
——所以链表复制oj题 希望完成的是深拷贝

2. 析构两次

	//拷贝构造(深拷贝)Stack(const Stack& st){//1.开跟你一样大的空间(_array这个成员变量不是完全拷贝一样的内容,而是另开空间)_array = (DataType*)malloc(sizeof(DataType) * st._capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}//2.数据拷贝memcpy(_array, st._array, sizeof(DataType) * st._size);//3.(其他)成员变量拷贝_size = st._size;_capacity = st._capacity;}

复杂链表的拷贝还需要指定各新链表节点的指向,就只能一个结点一个结点地拷贝。

复杂链表的拷贝的真正难点还不在于深拷贝,而是在于random指针的拷贝。

运行之后两个st1、st2的_array应该是不同的地址。

代码测试:

//栈类
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){cout << "Stack(size_t capacity = 3)" << endl;_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}//深拷贝// Stack st2 = st1;Stack(const Stack& st){_array = (DataType*)malloc(sizeof(DataType) * st._capacity);//首先开跟你一样大的空间if (NULL == _array){perror("malloc申请空间失败!!!");return;}memcpy(_array, st._array, sizeof(DataType) * st._size);//其次把数据拷贝过来_size = st._size;_capacity = st._capacity;//最后完成内置类型的拷贝}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}bool  Empty(){return _size == 0;}DataType Top(){return _array[_size - 1];}void Pop(){--_size;}// 其他方法...~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};int main()
{Stack st1;st1.Push(1);Stack st2(st1);st2.Push(2);st2.Push(3);while (!st1.Empty()){cout << st1.Top() << " ";st1.Pop();}cout << endl;while (!st2.Empty()){cout << st2.Top() << " ";st2.Pop();}cout << endl;
}


编译器生成的默认拷贝构造函数中,内置类型完成浅拷贝,自定义类型会去调用它的拷贝构造。


【实践中总结】

  • 一般情况下,不需要显示写析构函数,就不需要写拷贝构造
    • 如果没有管理资源,一般情况不需要写拷贝构造,默认生成的拷贝构造就可以。
      如:Date。
    • 如果都是自定义类型成员(会去调用它的拷贝构造:编译器默认生成的、用户显式实现的),内置类型成员没有指向资源,也类似默认生成的拷贝构造就可以。
      如:MyQueue。
  • 如果内部有指针或者一些值指向资源,需要显示写析构释放,通常就需要显示写拷贝构造完成深拷贝。如:Stack Queue List等。

拷贝构造函数典型调用场景:

  • 函数参数类型为类类型对象。(对象拷贝传参)
    • 使用已存在对象,创建新对象。(对象拷贝初始化)
  • 函数返回值类型为类类型对象。(对象拷贝返回)

为了提高程序效率,一般:

  • 对象传参时,尽量使用引用类型;
  • 对象返回时,根据实际场景,能用引用尽量使用引用。

【总结】构造函数、析构函数、拷贝构造

  1. 显式写怎么写;
  2. 什么情况需要显式写;
  3. 什么情况不需要显式写;
  4. 不写,编译器自动生成会做什么事情;

5. 赋值运算符重载

5.1 运算符重载

5.1.1 日期类的比较大小重载

将商品按照生产日期进行排序,核心就是需要将日期进行比较大小。

或者按照出生日期进行比较,得出年龄大小的排序。

首先看到这个代码的错误在于传参方式:

  • 自定义类型传参建议引用传参
  • 传值传参——自定义类型传值要调用拷贝构造,浪费时间。
  • 传址传参——太麻烦了,调用的时候每个参数还要加&。
  • 引用传参——只用在形参位置加&,比较方便
    (引用传参,为了减少拷贝——引用相比于指针,语法上认为不开空间)
  • 引用传参,凡是内部不改变这个参数的,都建议给形参加上const修饰,避免不小心改了形参(引用形参——改了形参真的会影响实参)

5.1.1.1 日期类的大于比较

年大就大;

年份相等,再看月分:月大就大;

年份、月份都相等,再看日期:日大就大;

若大于函数叫Compare1,小于函数叫Compare2、等于函数叫Compare3,就无法做到见名知意,需要去看具体的代码逻辑才能知道这个函数在干嘛,否则就要添加具体的注释说明函数的功能。

所以应该将函数名设置得尽量见名知意,增强代码可读性。(不要用拼音Datedayu)


这样调用一个函数来比较大小还是太麻烦,而且不够直观。

自定义类型比较大小能不能直观一点,像内置类型比较大小一样:d1 > d2。


内置类型可以支持直接使用运算符比较大小:ch1 > ch2。

这是语言自己支持的:运算符用作字符型、整型、浮点型的比较。

内置类型是语言自己创造的,内置类型的比较直接调用相关的指令就能完成:

整型比较二进制位、浮点数先比整数部分,再比小数部分、指针底层还是一个整型数字……

内置类型,语言都知道应该怎么比较;

而自定义类型,语言不知道应该怎么比较,所以它不敢给你支持。所以:

自定义类型不支持直接使用运算符比较大小。


C++为了增强代码的可读性(直观性)引入了 运算符重载

  • 运算符重载是具有特殊函数名的函数
    • 具有其返回值类型,函数名字以及参数列表;
    • 其返回值类型与参数列表与普通的函数类似。
      (参数个数和操作符的操作数个数相等、返回值类型取决于表达式的结果类型)
    • 函数名字比较特殊:关键字operator + 需要重载的运算符符号
    • 函数原型:返回值类型 operator操作符(参数列表)

operator本意是“操作符”,它的核心作用是用于把运算符当作函数名来重载

【注意】

  • 不能通过连接其他符号(非操作符)来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数。(全是内置类型的话,不允许操作符重载)
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
    这个没有强制规定,内置类型的+可以重载成自定义类型的-,编译器检查不出来。
    但是一般不会这样做。
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  •  .* . :: sizeof ?: 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
    (对象成员函数指针访问、对象成员访问、域作用限定符、取大小、三目选择)
    (*作为乘、解引用都是可以重载的)

 .* 运算符平常几乎用不到,作用是通过成员函数指针,去调用成员函数

//类
class OB
{
public:void func()	//类的成员函数{cout << "void func()" << endl;		//打印便于观察函数调用}
};// C语言最难理解的两个东西——函数指针 & 数组指针,指针变量的名字都是放在式子中间的
// 正常的变量定义:类型+变量名——void (*) () ptr就是典型的错误写法
// 函数指针变量的定义:void (*ptr)();// 同样的,函数指针的typedef也不是typedef void (*)() ptr——一般都是typedef+类型+重命名
// 跟函数指针变量的定义类似,要把新的类型名放到式子中间。
typedef void(OB::* PtrFunc)();//重命名·成员函数指针·类型// *PtrFunc是普通的函数指针;
// typedef void(* PtrFunc)()是重命名一般的函数指针的类型为PtrFunc;
// typedef void(OB::* PtrFunc)()是重命名·成员函数·指针的类型为PtrFunc
// 这个函数指针类型属于这个类域,需要声明一下类域int main()
{//定义一个函数指针——警告warning C4101: “ptr”: 未引用的局部变量void (*ptr)();//定义一个函数指针,并初始化——成员函数规定要加&才能取到函数指针,而普通函数的函数名就是地址PtrFunc fp = &OB::func;		//定义成员函数指针fp指向成员函数func的地址OB temp;//定义ob类对象temp//这个时候对象想“通过函数指针”,去调用成员函数,就需要使用到.*运算符(temp.*fp)();    //fp不是OB类的成员,但是OB类的对象可以通过.*运算符去访问fpreturn 0;
}

作用有点像temp.(*fp)(),但是这句代码是通不过的,因为fp不是temp的成员。


通过运算符重载,可以自定义自定义类型的比较方式。

每个类(自定义类型)都应该有自己的比较规则——由这个类的创造者来定义。

operator(关键字)+ 运算符   ==>  构成函数名(之前特意写比较函数的方式就退出历史舞台了)

注意:输出的时候带一个括号的原因——流插入运算符<<的优先级比大于操作符>更高。

这里还有一个问题就是类的成员变量为公有时才支持这么写,类的成员变量一般都是私有,不支持这么写,解决方法就是把操作符重载函数放到成员函数里面。

5.1.1.2 日期类的等于比较

问题:一个类到底要重载哪些运算符呢?——例如:一个日期类到底要重载哪些运算符呢?

答  :有实际意义的那些。

d1-d2  天数int
d1+d2  没有意义
d1*d2  没有意义

d1+int 日期Date
d1-int 日期Date
--d1   日期Date
++d1   日期Date

一个类要重载哪些运算符是看需求,看重载有没有价值和意义。

//日期类
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//private:int _year;int _month;int _day;
};// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{return  d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d3(2024, 4, 14);Date d4(2024, 4, 15);// 显式调用——体现不出核心优势(直观性强)operator==(d3, d4);// 转换调用(直接写)——编译器转换成operator==(d3, d4);d3 == d4;return 0;
}

明显缺陷:把数据设置成公有访问了——重载成全局,无法访问私有成员。

三种解决方案

  1. 提供想要访问的成员get(获取)和set(修改)     ——java喜欢用
  2. 友元                                                               ——后面会讲
  3. 重载为成员函数                                             ——推荐使用这种

方法①的注意点:

报错原因:

  • const对象只能调用const成员函数。

(const)常成员函数——成员变量不可修改。


方法③的注意点:

报错原因:

  • 因为规定操作符重载函数的参数个数,要与操作符的操作数个数一致;
  • ==是双目操作符,其重载函数就只能有两个参数;
  • 而成员函数都有一个隐式的this指针参数,其显式参数就只能有一个了;
//日期类
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d)// 这里需要注意的是,左操作数是this,指向调用函数的对象bool operator==(const Date& d){return  _year == d._year&& _month == d._month&& _day == d._day;}private:int _year;int _month;int _day;
};int main()
{Date d3(2024, 4, 14);Date d4(2024, 4, 15);// 参数变了,其显式调用的写法也会发生改变——可读性被牺牲地荡然无存。d3.operator==(d4);// 但转换调用的写法不变,只是转换的地方变了——编译器转换成d3.operator==(d4);d3 == d4;// 有两个操作数的时候,是按顺序来的:// 第一个操作数d3就是第一个函数参数,// 第二个操作数d4就是第二个函数参数。——顺序是固定好的return 0;
}

注意这里的转换调用和之前的转换调用就不一样了。

由于转换调用方式不变,当全局operator==和成员函数operator==同时存在时,会优先调用谁?

通过调试可以证明。通过反汇编也一样可以证明。

这也就意味着重载成了成员函数,再重载一份全局的就没有意义了。

C++的运算符重载绝大多数都直接重载为成员函数。


自定义类型,使用==会转换成调对应的重载函数(先去类里面找,再去全局找,都没有就报错)

内置类型,使用==直接就会转换成调对应的指令。


5.2 赋值运算符重载

5.2.0 赋值运算符重载位置

  • 赋值运算符只能重载成类的员函数,不能重载成全局函数。(默认成员函数)

重载成全局的时,需要注意没有this指针了,需要给两个参数。

// 编译失败:
// error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了。故赋值运算符重载只能是类的成员函数。

5.2.1 赋值运算符重载格式

5.2.1.1 最终格式简介
  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&。
    • 引用返回可以提高返回的效率;
    • 有返回值目的是为了支持连续赋值;
  • 赋值重载需要检测是否自己给自己赋值——减少不必要的操作。
  • 返回*this :要复合连续赋值的含义。
5.2.1.2 场景引入

我们已经知道,期望构建一个跟d1值一样的对象,可以使用拷贝构造:

但是若是期望让已存在的对象d2的值跟d1一样,应该怎么办呢?——赋值重载。

这里的d2 = d1就不是拷贝构造了——构造的含义是初始化,拷贝构造的含义是拷贝初始化。

=重载是默认成员函数:

  • 不能重载成全局的——会报错。
  • 不写,编译器会自动实现。

5.2.1.3 简单实现

赋值重载的显式实现如下:


优化①:给返回值

为了支持连续的赋值d1 = d2 = d4;所以需要返回值,从右往左赋值(结合性从右往左结合的)

因为赋值除了上面的用法,还有连续赋值的用法,这就需要赋值重载函数调用具有返回值。

连续赋值表达式的运算是从右往左的,因为=的结合性是从右往左。


优化②:引用返回

传值返回和引用返回的区别,是效率上的区别。

传值返回并不是d作为func的返回值,d出了作用域就销毁了。

传值返回会生成一个当前对象的拷贝,作为func的返回值。(可以调试观察,发现被调函数结束前,去到了拷贝构造函数体内走了一遍)

(而这个临时对象的生命周期肯定就不在这个被调函数内了)


有没有什么办法不生成d的拷贝?——引用返回。

传值返回,返回的是d的拷贝;引用返回,返回的是d的别名。

返回引用却报警告说返回局部变量的地址——这是一个基于底层的警告(引用的底层是地址)。

这个代码的问题就在于返回局部变量的地址,使用这个(已析构空间的)地址就会出错。


如果不是引用返回,而是传值返回,则ret不能是引用类型,因为func传值返回的是临时对象,临时对象具有常性,不能直接引用——权限的放大。

ret只能是const引用类型。

而这又涉及到const成员函数的内容,ret.Print()编译通不过。


ref是一个临时对象的别名。

ref如果不作为Date&,而是直接Date(就不用加const),那这里就会多一次拷贝:

Date ref = ……(拷贝构造)

但是这也会涉及到编译器的优化的问题。

【总结】

拷贝会到来性能的牺牲。

自定义类型传值返回要付出较大代价,涉及到各种拷贝,加引用可以提高效率(减少拷贝)。
(跟传值传参类似)


而引用返回,则ref在引用接收时,可以不加const。

ref是d的引用的引用,其实就是d的引用,而不是临时对象的引用。

传值返回,就能正常打印2024-4-14(不管是引用接收Date& ref,还是正常接收Date ref)

函数体内的静态局部变量、堆上的对象、……才能引用返回。

  • 出了函数作用域,对象析构→传值返回;
  • 出了函数作用域,对象不析构→引用返回;

注意:不能说在栈上的数据出了函数作用域一定被销毁,即不能依据返回对象是否在栈上判断是否能引用返回。可能在出了当前函数作用域,其他函数栈帧上的数据依旧存在。

例:指针。

d2 = d1,则*this就是d2——在栈上,但是不在当前栈帧上。

this是形参,出了作用域就销毁了,所以不能返回this,只能返回*this。

唯一指标:出了当前栈帧/函数作用域,返回对象生命周期到没到。

成员函数和普通函数都是一样的,只要函数调用,不管是成员函数,还是普通函数,都一样。都会建立栈帧,跟类无关,类只是说编译的时候会调用对应的类型,程序运行的时候都是调用函数,调用函数就是建立栈帧。成员函数就跟普通函数一样,只是编译器多做了一些事情,有隐含的this指针这些。运行的时候就是调用main函数,main函数再调用其它函数,每个函数调用都建立栈帧。



【广闻博识】NRVO

在VS2022上,传值返回也不会打印拷贝构造的调用信息:

情况①

VS2022,把拷贝构造给优化掉了,不接收返回值就不拷贝构造,接收返回值就直接操作待赋值对象,也不拷贝构造。

情况②

而ret既没有调用构造,也没有调用拷贝构造。

以上两种情况都属于编译器采用了命名返回值优化(NRVO)。

首先介绍一下返回值优化(RVO)命名返回值优化(NRVO)

以上关于NRVO的内容就解释了情况①,而情况②的解释如下:(也属于NRVO)

移动构造属于C++进阶部分的内容:


再回到赋值重载函数,传值返回会有多一次拷贝构造,可以优化为引用返回。

传值返回会多很多白白的拷贝,而且建立临时拷贝对象还得多析构几次。

引用返回就可以避免白白的拷贝,和无谓的析构。


优化③ 避免自赋值

不排除有人会写出这样的代码:

d1 = d1;    //自己给自己赋值

这时就要注意减少不必要的操作,提高程序效率

这样函数内部的赋值操作就白进行了,就可以加一个判断,在自赋值时直接返回,提高程序运行效率。
(数据量大的对象进行深拷贝时消耗非常大,所以宁愿每次赋值都判断一下,也要避免极端情况)

判断是否自赋值,要拿地址来判断。

注意区分“引用”、“取地址”。


【总结】标准的赋值重载

  1. 带返回值——支持连续赋值;
  2. 引用返回——提高效率;
  3. 判断自赋值——提高效率;

5.2.2 默认赋值运算符重载

讲完标准的赋值重载,下一个问题——

问:如果不写赋值重载会怎么样?

答:编译器会自动生成

问:自动生成的赋值重载会完成哪些事情呢?

答:编译器会生成一个默认赋值运算符重载:

  • 内置类型直接按字节拷贝。
  • 自定义类型会去调用对应的赋值重载。


既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?

当然像日期类这样的类是没必要的。

那么下面的类呢?验证一下试试?


【注意】

  • 如果类中未涉及到资源管理,赋值运算符是否实现都可以;
  • 一旦涉及到资源管理则必须要实现。

【总结】

  • 跟拷贝构造类似
    • Date或者MyQueue默认生成的赋值就够用了。
    • 但是类似Stack、List等都需要们自己实现赋值重载。

5.3 前置++重载、后置++重载


6. 日期类的实现

一个完整类的实现,一般都会声明和定义分离。

  • 类型、成员函数声明在头文件。
  • 成员函数定义在源文件。(指定类域)
  • 缺省参数只能在声明的时候给。

默认成员函数:日期类只需要写普通构造函数,析构函数、拷贝构造、赋值重载都不需要。

但是日期类还要实现其他的一些常用功能接口函数


6.1 比较运算符重载

首先来写一个“<重载”。

再来写一个“==重载”。

可能会图方便,写完一个<,剩下的直接ctrl + c + v,改一些细节,这样来写完6个函数。

实际上想图方便,这样反而麻烦了,其实剩下的可以直接复用 > 和 == 简化代码

以后写其他类的比较大小运算符重载,也只需要写> 和 ==,剩下的:

  • >=可以直接复用 > 和 ==
  • <可以直接复用>=
  • <=可以直接复用> 和 ==
  • !=可以直接复用==

优化:对于频繁复用的>、==可以声明成内联函数,直接在调用处展开,减少消耗。

但是我们知道内联函数不能声明和定义分离。

如果直接在.h的类体里面写函数定义,即使没写inline也默认内联函数。


好习惯:代码要边写边测试,不要一下子写太多的代码,写完了一堆再测试出一堆错误,不好改错

代码多了之后,该编译错误、运行错误都容易出问题。

这里写完日期类的比较大小重载,就测试一下,测试没问题之后。再继续写其他成员函数。


6.2 加(减)法运算符重载

运算符重载要求至少有一个类类型的参数,this可以算作是一个类型的(指针)参数。

6.2.1 日期+天数、日期+=天数

日期加天数,按照日期的特定进位方式,进行相加就可以了。

  • 核心思想——进位相加。
  • 核心问题——日期的进制:年满13进1,月天满31/30/29/28进1。

6.2.1.1 GetMonthDay()

注意:直接写比较复杂,可以增加一个成员函数 GetMonthDay() 获取当前月的天数,并且直接定义在类里面,使得成为内联函数。——这个类里面调用得最频繁的一个成员函数。
(内联不能声明和定义分离,因为内联不会进入符号表,链接的时候找不到。声明和定义分离就是链接的时候回去符号表里面拿着“修饰函数名”去找定义/函数地址)

实质:也就是把operator+里面比较麻烦的一个功能模块,给封装成一个函数。

GetMonthDay函数的常规实现方式:if-else、swicth-case、枚举。最好实现方式是“月份数组”。


【广闻博识】闰年

每年地球公转一圈是365天5小时48分46秒,公转4年补一天,补多了一点,其实没到一天。

经过32轮4年(128年)以后,差不多补多了一天,就在公转100年(25轮)的时候就不补一天。

不补又差不多差了0.25天,公转400年的时候又补一天......

闰年的循环就是一个多退少补的一个循环:四年一闰,百年不闰,四百年又闰。


闰年规则:《儒略历》——>《格列历》(1582年)即这个日期加天数得日期的代码。

用到古老一些的年份不适用。


假设修正法。

注意三个优化的地方。

由于GetMonthDay函数没有使用成员变量,将它放到全局域也是可以的,不必要写为成员函数。


6.2.1.2 operator+=、operator+
【流程分析】


【代码实现】

违背了+运算符的底层逻辑,d1 + 50首先得保证加完之后d1和50本身都不变。

实际上实现的是+=。

这就是拷贝构造的使用场景,用一个不能直接操作的*this去构造出一个tmp出来操作。


“+重载”更好的实现方式是直接复用“+=重载”。

出了作用域,tmp就不在了,就不能使用传引用返回,只能传值返回,有一个拷贝构造的消耗。


【代码测试】


反过来,“+=重载”也可以复用“+重载”

+  :复用展开后,两种写法消耗差不多,都是传值返回,有一个拷贝消耗

+=:上面一种更好,因为下面这个虽然在=的地方是赋值重载,没什么消耗,但是在+的地方展开复用代码后,第一句有一个拷贝构造,最后传值返回又一个拷贝,凭空多了两个拷贝,消耗更大,而上面这个直接改变自己,消耗更少,都是引用返回,最后都没有拷贝消耗


6.2.2 日期-天数、日期-=天数

加是进位,减是借位。

4月只有14天,减去50已经减没了,借位要借3月的天数。

【注意】

  • 日期进位,是减去本月的天数(4月)。
  • 日期借位,是加上上月的天数(3月)。

【流程分析】


【代码实现】

注意-=和+=之间的逻辑差异:

  • 向上一月借位,不能直接获取上月天数——不能直接 GetMonthDay (_year,_month-1)
    要先--month, 在判断修正month,最后传参month获取上月天数。
  • 向下一月进位,可以直接获取本月天数,减去本月天数,向下一月进位,再修正下一月。

【代码测试】

测试-(复用-=),就连带着-=一起测试了。

但是注意,这个测试不够完善,至少得测试一个跨闰年的——例如:±5000天。

测试网站


代码测试:


这个测试还是不够完善,注意到-=和+=的参数day(天数)是int类型,那么当加负天数、减负天数时,就会触发程序的bug(没有考虑到这方面的处理)。

测试来完善代码——因为形参是int,所以需要加一个判断语句。

加负天数 → 复用减正天数。

减负天数 → 复用加正天数。

代码测试

【注意】

  • 传size_t不能阻止传负值,会把负变正,输错错误的结果。

类和对象这部分,把栈类、日期类这两个类掌握了,类和对象这部分的内容就理解得差不多了。

手撕一个栈:理解栈的构造、拷贝构造、析构……

手撕一个日期类:理解运算符重载、引用传参、引用返回(传值返回)、……

6.2.3 operator++(前置++、后置++)

接下来重载++运算符,有前置++和后置++,就有两个operator++。

这时候就有 函数重载 运算符重载 的关系问题了。

前置++:返回++之后的值。

后置++:返回++之前的值。

本质区别:返回值不同。

但是返回值不同不能构成函数重载。


函数重载、操作符重载,虽然都用了重载这个词,但是它们各论各的,没有关系。

可能的关联就是多个同一个运算符的重载,可以构成函数重载。

同一运算符的两种行为:日期-天数、日期-日期。两个operator-运算符重载函数也构成函数重载,而且是天然构成函数重载(参数不同)。

但是两个++重载并不天然构成函数重载,因为都只需要一个this隐藏形参。

  • 单运算符,一个this隐藏形参就够了。(单运算符,只需要单操作数——一个操作数)

为了区分前置++、后置++,构成重载,祖师爷这里迫不得已做了一个特殊处理,给后置++,强行增加了一个int形参。

这个多的int形参不需要写形参名,因为接收值是多少不重要,也不会需要用。这个参数仅仅是为了跟前置++构成重载区分。

注意这里后置++返回的是局部临时对象的值,不能引用返回,只能传值返回。

遇到前置++,编译器会正常转换调用:++d1 -> d1.operator++()

遇到后置++,编译器会特殊转换调用:d1++ -> d1.operator++(1)——编译器随便给的一个值

这个特殊处理一般看不出来,因为通常都不会去显式调用。

通常都是直接使用操作符,然后遇到自定义类型,转换成调用对应的函数。

  • 内置类型的时候差别不大;
  • 自定义类型的时候,能用前置尽量使用前置——后置有两个拷贝消耗+析构消耗

【代码测试】

后置++没显式调用传参,这个形参int的值是不确定的,VS下是0。

这个形参int的意义不是传统的为了接收某个值,而是为了和前置++构成函数重载的区分。
(形参可以只写类型,不写名称接收)


颠倒一下前置++、后置++的代码逻辑,在语法上程序不会出现报错,也可以正常使用。

但是不符合默认使用习惯。(这里返回临时对象,要传值返回,没注意改)


6.2.4 operator--(后置--后置--)

6.2.5 日期-日期

日期减日期直接看不好减,跟之前的逻辑思路都不一样。

不好减的核心还是因为年、月、日的不规则性,每个月的天数不一样,闰年也会有影响。

年有闰年、平年,月有大月、小月,2月的天数还不尽相同。

6.2.5.1 思路1.对齐法

特点:逻辑比较复杂;在年份差距比较大时,消耗比较少。

对齐方式1:小的日期,进行月日对齐(向大的日期的月日对齐)。

月日对齐,算差了几年,计算出这几年的天数(注意是否包含着闰年)。

再用9.1减4.14,把年月日相减变成月日相减。


对齐方式2:两个日期,都进行月日对齐。

月日对齐,算差了几年,再分别用4.14-1.1以及9.1-1.1。


6.2.5.2 思路2.计数器法

特点:​​​​​​​逻辑简单,年份差距比较大时,消耗比较大——但是效率并没有差很多,因为计算机每秒运算上亿次,所以差距几乎可以忽略不计。

首先比较出谁大谁小,然后让小的向大的去++。

我的思路:

  • 首先判断if(*this > d),让小的日期(d)循环+=1,同时设置计数器,结束条件为==大的日期——等价于是说循环条件就是!=大的日期
  • else同样是让小的日期(*this)循环+=1,同时设置计数器,结束条件为==大的日期——等价于是说循环条件就是!=大的日期
  • 小缺陷:在if语句和else语句内代码比较相似,有点冗余。
  • 大缺陷:事先没有在保存的d和*this上变值,而是直接操作d和*this,违背了减法不改变操作数的准则。

教的思路:

  • 和我的一样,只是设置了一个正负指标参数,乘在了计算值上进行返回。

【代码测试】


题外话:static修饰函数(静态链接属性)。

这就是静态链接属性的意义,这同时也是声明和定义分离的意义。

直接定义在Date.h,在Date.cpp和Test.cpp中都包含了Date.h,两个源文件中头文件展开,项目就会有两个func()函数,产生冲突。

声明和定义分离就没有这样的问题。定义不能有两份,声明没有限制。
(可以有两份声明在Date.cpp和Test.cpp)

如果不想声明和定义分离,可以加上static修饰,static修饰变量改变生命周期,static修改函数改变外部链接属性。——不进符号表(内联也不进符号表,不支持声明和定义分离,在.h写了,在两个.cpp就都包含了)

注意区分static、inline修饰函数二者的区别。


日期类常见的操作基本上就是这些,日期类再重载其他的一些算术操作意义就不大了。

6.3 流插入、流提取运算符重载

现在一直都是调用Print函数来打印,没法像内置类型一样使用库函数printf(“%d”, d1)。

这是C语言printf和scanf的局限性——只能打印内置类型,不能直接打印自定义类型。

C++新增了流插入和流提取可以直接支持内置类型,也可以显式重载成支持任意自定义类型。

stream、ostream类型都在iostream头文件,被封装在std这个C++标准库(命名空间)内。

cin是一个istream类型的全局对象。

cout是一个ostream类型的全局对象。(cin、cout的c是控制台console的意思)

类型里面有众多成员函数,其中包括控制精度的成员函数,所以类型对象cin、cout控制精度时需要调用对应的成员函数。

根据函数重载的调用时的参数匹配(函数名修饰)规则,达到自动识别类型调用对应函数的功能。

#include<iostream>
using namespace std;
int main()
{// 在io需求⽐较高的地方,如部分大量输入的竞赛题中,加上以下3⾏代码// 可以提高C++IO效率ios_base::sync_with_stdio(false);    //关闭同步流cin.tie(nullptr);                    //去除cin、cout的绑定cout.tie(nullptr);return 0;
}

由于C++的cin和cout的效率比较低, 竞赛的时候建议关闭同步流。

因为C++要兼容C,比如说cin、cout、printf、scanf混着用,而它们底层都有缓冲区。

缓冲区:输入/出的时候不是直接输入/出到对应目标,而是遇到换行、主动刷新等才会刷新出去,底层类似于是一个数组,遇到标志才出去。

比如说C语言输出两个数据,C++输出两个数据,而且它们是交错的,1、3进入C++的缓冲区,2、4进入C语言的缓冲区,可能C++遇到刷新标志了但C语言没遇到,就会导致输出顺序不是1234了。

所以C++底层会想一些办法,保证无论谁先遇到,顺序都是1324——关了同步流就无法保证了。


cin和cout的缓冲区在默认情况下是绑在一起的,连续输入几个值,没有遇到刷新标志,也不会输出,就会有一定的消耗。

传空指针nullptr==0(C++11)去除绑定。


这两个步骤,三句代码可以提高C++输入输出的效率(竞赛的时候)。

C++的输入输出效率没有C语言高,一部分就是因为C++要兼容C语言。


回到刚才的问题,cout、cin默认就可以支持内置类型,自定义类型呢?

直接写cout<<d1会报错,所以需要自己造,但是无法去库里面的ostream类里面把operator<<给改了,只能在自己的类里面重载一个新的流插入运算符。
(后面发现在自己的类里面重载也是一个大坑)


6.3.1 流插入重载

cout << d1的两个操作数:ostream类型的对象、类类型的对象。


6.3.1.1 重载为成员函数

重载为成员函数默认有一个类类型的参数,还需要一个ostream类型的参数。

//Date.h
class Date
{
public://……void operator<<(ostream& out);     //这里必须得引用,因为istream和ostream把拷贝构造禁止了。private:int _year;int _month;int _day;
};//Date.cpp//流插入
void Date::operator<<(ostream& out)   
{out << _year << "年" << _month << "月" << _day << "日" << endl;//连续的operator<<()函数调用——调用的都是库里面的operator<<(),因为输出的都是内置类型
}
//这里的out就是cout,都是ostream类实例化的一个对象,cout是实参,out是形参

连续的函数调用注意:

1.流插入运算符的结合性是从左往右,所以是out << _year的返回值out1去调用out1<< "年",以此类推。

2.正因为cout输出一段往往是多个函数调用,而printf输出一段只有一个函数调用,所以cout的效率低的问题就更明显了,才会考虑关闭同步流

【代码测试】

运算符重载中,参数顺序和操作数顺序是一致的。

读起来就好像是控制台流插入到日期类里面去了,

编译器只能检测是否按默认语法跑,不能检测逻辑。所以就会像之前交换前/后置++代码一样,语法上可以这么写,这么用。但是读的人和后面其他使用的人就搞不懂了。

结论:operator<<想重载为成员函数,可以,但是用起来不符合正常逻辑,不建议这样处理,建议重载为全局函数

想要符合<<正常的使用习惯,就需要让ostream作第一个参数,但是Date成员函<<永远无法让ostream作第一个参数。(成员函数的第一个参数永远是隐藏的this指针)

所以建议重载为全局函数。

【思考】C++为什么要搞IO流?

  • 因为C语言的printf和scanf具有局限性,只能支持内置类型的输入,输出。对于自定义类型就需要写对应的print函数和输入函数(里面仍是printf和scanf:自定义类型的尽头是内置类型就像是重载的输出自定义类型的<<里面仍是“<<内置类型”,)
  • 而C++的IO流可以实现流插入和流提取的重载,以支持自定义类型的输出、输入。

6.3.1.2 重载为全局函数

普通重载函数可以选择重载为成员函数 or 全局函数,推荐重载为成员函数——因为方便访问成员变量。

流插入重载函数不建议重载为成员函数,因为Date* this占据了一个参数位置,使用d<<cout不符合使用习惯。(左操作数对应第一个参数、右操作数对应第二个参数)

//Date.h
class Date
{
public://……//private:int _year;int _month;int _day;
};
void operator<<(ostream& out, const Date& d);//Date.cpp//重载成全局函数——>这个时候成员变量私有的问题就出来了——先暂时设置成公有,后面再解决
void operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
//缺点:不能多次调用cout << d1 << d2;

【代码测试】

本质就是操作数、参数的顺序问题,操作数在这里就就变成实参了,操作数的顺序就是实参的顺序,其顺序要和形参的顺序一致。


改进一下解决不能连续流插入的问题:

//Date.h
class Date
{
public://……//private:int _year;int _month;int _day;
};
void operator<<(ostream& out, const Date& d);//Date.cpp//改进
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}

出了作用域out还在——因为是全局对象, 所以可以使用传引用返回。

由于重载的<<在末尾加上了换行, 所以使用时cout<<d1在最后没加<<endl; 也会换行。

【代码测试】

之前重载为成员函数也有这个问题没改进,但是主要问题不在这就没管它。


6.3.2 流提取重载

【代码实现】

//Date.h
class Date
{
public://……//private:int _year;int _month;int _day;
};
// 重载为全局函数
//void operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
//注意:
// 1.要改成istream;
// 2.不加const——从io流中提取出来的值,要放入日期类的对象中//Date.cppistream& operator>>(istream& in, Date& d)
{cout << "请依次输入年月日:>";//扩展in >> d._year >> d._month >> d._day;//主体return in;//主体
}


优化一下,检查输入的日期合法。

//Date.h
class Date
{
public://……bool CheckDate();//private:int _year;int _month;int _day;
};
// 重载为全局函数
//void operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
//注意:
// 1.要改成istream;
// 2.不加const——从io流中提取出来的值,要放入日期类的对象中//Date.cpp// 在日期类的构造、日期类的输入的时候,
// 可以加一步检查——月、日是否合理:
// 1 < _month < 12 && 1 < _day < GetMonthDay(_year, _month)
// 连比不能直接写
bool Date::CheckDate()
{if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month)){return false;}else{return true;}
}Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "日期非法" << endl;}
}istream& operator>>(istream& in, Date& d)
{cout << "请依次输入年月日:>";//扩展in >> d._year >> d._month >> d._day;//主体if (!d.CheckDate())//扩展{cout << "日期非法" << endl;}return in;//主体
}

重载成全局函数,不能访问私有成员变量的问题还没有解决。

  1. 提供get和set;
  2. 友元函数声明;

如果想在类外面的函数里面,通过对象直接访问私有成员变量,可以在类里面把外部函数声明成友元(一般放在最前面,放在公有public私有private都无所谓,因为它不是类的成员)

  • 友元分为:友元类友元函数

这里使用的是友元函数。

6.4 完整代码

参考源文件、头文件的相关内容。

7. const成员

const成员函数将const修饰的“成员函数”称之为const成员函数。

  • const修饰类成员函数,实际修饰该成员函数隐含的this指针
  • 表明在该成员函数中不能对类的任何成员进行修改。

请思考下面的几个问题:

1. const对象可以调用非const成员函数吗?

2. 非const对象可以调用const成员函数吗?

3. const成员函数内可以调用其它的非const成员函数吗?

4. 非const成员函数内可以调用其它的const成员函数吗?

我们来看看下面的代码


有时候我们会用const对象去调用相关的成员函数,可能会存在一个调用不上的问题——可能会存在一个权限的放大的问题。

【图解说明】

最开始想着把this指针做成隐参数,不用传参很方便,没预料到后来需要对this指针加const修饰不好修饰的情况,就只好选择把这个const加在括号外这种蹩脚的方式。

  • 括号后的const修饰的是*this
  • 括号里隐含的const修饰的是this

至于括号里的其他参数需要用const修饰可以直接写在括号里面。

const成员函数的优势:隐藏this指针权限最小, 所以无论是不是const对象, 都能调得动。


【代码测试】优化了Print()函数的实现


函数声明在编译器看来:


【代码测试】优化了比较大小重载的实现

完善了之前的比较运算符重载函数、打印函数,全都加上了const修饰。

 包括+、-(天数)、-(日期)都可以加上const。

但是+=、-=不能加上const。const对象调用+=本身就不合理。


结论:不修改类的成员变量的成员函数,都可以在()后面加上const。


8. 取地址重载、const取地址重载

这两个 默认成员函数 一般不用重新定义 ,编译器默认会生成。

【区别】

  • 参数类型不同;(构成重载)
  • 返回值类型不同;

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可。

一般情况下,自定义类型要用运算符都得重载,但是这两个属于默认成员函数,我们不实现,编译器会自己实现,我们实现了编译器就不会自己实现了。

一般不需要我们自己实现,除非不想让别人取到这个类型对象的真实地址

(给空地址nullptr、给假地址0xffffffff)

只有特殊情况,才需要重载,比如想让别人获取到指定的内容!


实践中一般没有这样的需求。


文章转载自:

http://ovLlYQOz.bsrqy.cn
http://3OvHeiHO.bsrqy.cn
http://fXee7ads.bsrqy.cn
http://uYwxx90G.bsrqy.cn
http://Q1zaeDhd.bsrqy.cn
http://PXE2Th9g.bsrqy.cn
http://3YgEpu2q.bsrqy.cn
http://mGsIUqZt.bsrqy.cn
http://1AICwPUf.bsrqy.cn
http://seYNwEBN.bsrqy.cn
http://Jv2npniX.bsrqy.cn
http://aJXtH5ew.bsrqy.cn
http://5q0uWPDE.bsrqy.cn
http://jk5sYtMO.bsrqy.cn
http://i6A8idPK.bsrqy.cn
http://WeZcC5SM.bsrqy.cn
http://qoBdEjAG.bsrqy.cn
http://svYGK1vm.bsrqy.cn
http://zZbz22yx.bsrqy.cn
http://1Hi04XMy.bsrqy.cn
http://M0Pho5d0.bsrqy.cn
http://hGF2Zs1b.bsrqy.cn
http://LwEc7su5.bsrqy.cn
http://FZt77O6d.bsrqy.cn
http://1Lq7Z1cz.bsrqy.cn
http://ijYnyvd6.bsrqy.cn
http://5eckoH7N.bsrqy.cn
http://IcdZlJkG.bsrqy.cn
http://MjwUFFMb.bsrqy.cn
http://7BB5PiCv.bsrqy.cn
http://www.dtcms.com/a/374287.html

相关文章:

  • Linux I/O 访问架构深入分析
  • 实现一个可中断线程的线程类
  • Java全栈学习笔记31
  • 算法之双指针
  • js定义变量时let和cons的使用场景
  • DataLens:一款现代化的开源数据分析和可视化工具
  • 人工智能-python-深度学习-神经网络-MobileNet V1V2
  • TDengine 选择函数 Last() 用户手册
  • MySQL的数据模型
  • vulnhub:Kioptrix level 2
  • C++ Int128 —— 128位有符号整数类实现剖析
  • 前端部署,又有新花样?
  • Neural Jacobian Field学习笔记 - omegaconf
  • C++(day8)
  • 设计模式:模板方法模式
  • 英发睿能闯关上市:业绩波动明显,毅达创投退出,临场“移民”
  • 华清远见25072班网络编程day1
  • 深入理解 AbstractQueuedSynchronizer (AQS):Java 并发的排队管家
  • 32位CPU架构是如何完成两数(32位)相加的指令的?
  • 深度学习中的损失函数都有哪些,大模型时代主要用的损失函数有哪些,中间有什么区别?
  • java:io流相关类的继承关系梳理
  • PAT 1004 Counting Leaves
  • Linux操作系统shell脚本语言-第六章
  • 基于Springboot + vue3实现的小区物业管理系统
  • 自动化测试DroidRun
  • 把一段 JSON 字符串还原成一个实体对象
  • YOLO系列论文梳理(AI版)
  • ARM内核知识概念
  • 图论相关经典题目练习及详解
  • 深圳比斯特|多维度分选:圆柱电池品质管控的自动化解决方案