目录
[2. 类的默认成员函数](#2. 类的默认成员函数)
[2.2 构造函数概念和举例](#2.2 构造函数概念和举例)
[2.3.1 无参构造函数](#2.3.1 无参构造函数)
[2.3.2 默认构造](#2.3.2 默认构造)
[2.3.3 全缺省构造](#2.3.3 全缺省构造)
[2.3.4 默认构造的函数类型](#2.3.4 默认构造的函数类型)
[3.1 为什么要有析构函数](#3.1 为什么要有析构函数)
[3.2 析构函数的概念和举例](#3.2 析构函数的概念和举例)
[3.3 析构函数的特点](#3.3 析构函数的特点)
[3.5 判断是否要写析构函数](#3.5 判断是否要写析构函数)
[4.1 拷贝构造的概念](#4.1 拷贝构造的概念)
[4.2 拷贝构造的特征](#4.2 拷贝构造的特征)
[4.3 是否必须进行拷贝构造](#4.3 是否必须进行拷贝构造)
[4.4 拷贝构造函数典型调用场景:](#4.4 拷贝构造函数典型调用场景:)
[5.1 运算符重载的概念](#5.1 运算符重载的概念)
[5.2 运算符重载有些需要注意的地方](#5.2 运算符重载有些需要注意的地方)
[5.3 为什么运算符重载既能够全局又能在类里(有必要搞清楚)](#5.3 为什么运算符重载既能够全局又能在类里(有必要搞清楚))
[5.4 赋值运算符](#5.4 赋值运算符)
[5.4.1 赋值运算符的格式](#5.4.1 赋值运算符的格式)
[5.4.2 赋值运算符只能重载成类的成员函数不能重载成全局函数](#5.4.2 赋值运算符只能重载成类的成员函数不能重载成全局函数)
[5.4.3 何时用](#5.4.3 何时用)
[5.5 前置++和后置++](#5.5 前置++和后置++)
[6.1 流提取和流插入](#6.1 流提取和流插入)
[6.2 获取某月天数](#6.2 获取某月天数)
[6.3 +=和+的区别](#6.3 +=和+的区别)
[6.4 两个日期相减(计算天数)](#6.4 两个日期相减(计算天数))
[6.5 为啥<<和>>要友元](#6.5 为啥<<和>>要友元)
[7.1 const 成员函数](#7.1 const 成员函数)
[7.2 取地址运算符重载](#7.2 取地址运算符重载)

1.前言
上文介绍了类和对象的一部分,算是C++的开胃小菜,不是特别难,在这一部分中有一些难以理解的知识点,需要我们两次甚至三次反复学习才能掌握的,一次是肯定不够的,我也是第一遍懵懵懂懂,第二次才拨云见雾,在上面的思维导图中,重点在于构造函数和析构函数,然后是拷贝构造函数和赋值运算符重载函数,然后我们会有个日期类的实现。
2. 类的默认成员函数
类和对象中有6个默认成员函数,但是比较重要的还是前面4个,尤其重要,需要反复学习观看,反复操练。
| 默认成员函数 | 作用 |
|---|---|
| 1.构造函数 | 完成对象的初始化工作 |
| 2.析构函数 | 完成对象的清理释放工作 |
| 3.拷贝构造函数 | 用已经有的对象去初始化一个新的对象 |
| 4.赋值运算重载 | 用一个对象去赋值另一个对象 |
| 5.const成员函数 | 不修改对象的数据 |
| 6.取地址操作符运算重载 | 获取对象地址和获取const对象地址 |
默认成员函数,就是用户没有显式自定义,让编译器自己去生成的构造函数叫默认成员函数。
class func{};
2.构造函数
cpp
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date d1;
d1.Init(2026, 7, 5);
return 0;
}
我们之前在创建栈的时候,或者创建日期类时,每次创建一个对象都要调用一个函数去初始化,设定相关信息,那么能不能在对象实例化的时候不去申请调用,而是编译器自发的去完成初始化呢,(我写的时候及其容易忘记调用初始化导致出错)于是前辈们就想到构造函数
2.2 构造函数概念和举例
是特殊成员函数,不是字面上的去开空间创造一个对象,而是自动去进行对象的初始化。构造函数的名字和类名相同,不带返回值,甚至可以不带参数。它在创建类对象时是自动调用,且只会调用一次。
那么有了构造函数后就会很方便了,我们之前在创建栈的时候,啥都写好了,可能就是会忘记去写一个,就会导致出现错误,程序运行不正常。现在有了构造函数,自动来,妈妈再也不用担心我粗心忘记了!
cpp
st.Init();//忘记调用了,没有初始化
cpp
class Date
{
public:
// 1.无参构造函数
Date()
{}
};
void TestDate()
{
Date d1; // 调用无参构造函数
}
这是构造函数(无参情况),最简单的举例。
- 函数名与类名相同。
- 没有返回值
- 对象实例化时自动调用。
- 构造函数可以重载。
- 不写任何构造函数,C++编译器会自动生成一个无参构造函数
- 只要是自己定义的构造函数,无论是否带参,编译器就不会生成默认构造函数。
2.3.1 无参构造函数
这是构造函数的基本特点。那么问题来了,上面无参构造函数是不是我们显示定义的,还是说没有显示定义系统给的呢?肯定是显示定义的啊,就算它没有任何参数,那也是用户自己显示定义的,不是什么都没有写。无参构造函数,属于默认构造函数的一种:用户自己写的无参构造函数。
把代码注释掉
2.3.2 默认构造
cpp
class Date
{
public:
// 1.无参构造函数
/* Date()
{}*/
};
void TestDate()
{
Date d1; // 调用无参构造函数
}
等于我自己不写任何构造函数,C++编译器会自动生成一个无参构造函数,也是属于默认构造的一种,这个函数是个空函数体,不初始化,只是创建对象。
PS:但是要注意,无参构造函数后面不能加括号,不然会造成歧义,因为编译器不知道这是构造函数还是函数声明,比如以下代码就是会warning。


还有一种是全缺省构造函数,我们已经知道什么是全缺省了,那么之前铺的路就为这里打好了基础
2.3.3 全缺省构造
cpp
// 1.全缺
Date(int year=2026,int month=8,int day=2)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date d1; // 全缺省
}

至此,总结一下,C++中的默认构造函数有三个:共同点是都不传参。
| 默认构造函数 | 是否显示 |
|---|---|
| 无参构造 | 用户自己构造的(显示) |
| 全缺省构造函数 | 用户显示构造 |
| 默认构造(编译器) | 没有显示定义 |
但是这三个函数,尤其是第一个和第二个,因为都不传参,虽然是函数重载了,但是只能二选一,不然编译器不知道该怎么选。报错原因是包含多个默认构造

结论:默认构造函数有三个但是只能选择一个使用。
2.3.4 默认构造的函数类型
编译器默认生成的构造函数有内置类型的函数和自定义类型的,为什么会说这两个类型,就是我感觉在日期类的自定义中,不去自己编写构造函数的情况下,编译器生成默认构造函数看不到,但结果是随机值,这不是我想要的,所以编译器生成的默认构造函数没啥用感觉?然后我就带着问题去查了一下。
比较综合的答案是: C++把类型分为内置类型和自定义类型。
内置类型:语言类,int/char/double
自定义类型:struct,class等
看看编译器对自定义成员是怎么调用的:
cpp
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time d;
return 0;
}

自定义类型中的成员
自定义类型成员会自动调用它自己的默认构造函数。我们可以通过监视窗口去看,但是调试是不会跳进去的。
但是如果我们不写,默认构造函数对内置类型不做处理,在某些编译器中会进行处理,但是我们还是默认他不处理吧,是否初始化是不确定的,这里就没有初始化,如果我强行进行打印,编译器就会报错,是能编译通过,但是无法运行
cpp
struct Time
{
int a;
double b;
int* p;
};
int main()
{
Time d;
return 0;
}

我在监视窗口看了一下d这个对象的内部数据,都是随机的垃圾值,野指针,这样证实了一个事情就是编译器默认构成对内置类型不做处理。

那么为了修补这个问题,当然不是说这个不初始化给修复了,就是做了一层修改,C++11中可以运行在内置类型的声明中加默认值,(全缺省可以在对象传参的时候带参构造函数,这样缺省值就会被替换掉)
cpp
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
3.析构函数
3.1 为什么要有析构函数
cpp
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
int main()
{
s.Destroy();
}
构造函数是初始化一个对象,设置好参数。那么有了开始是我们可能会忘记初始化,结束我就有可能会忘记destory,销毁栈空间。前辈既然考虑到前者,后者自然会有,要前后呼应,所以析构函数就诞生。
3.2 析构函数的概念和举例
析构函数:并非完成对对象本身的销毁,而是完成对象内部空间资源的清理释放工作。和构造函数是恰恰相反的。之前的栈空间申请空间后必须要自主申请调用destory
cpp
destory(p);
我们难免会忘记,至此,我们就引用析构函数,在构造函数之后。这里会自动调用析构函数,为了显式,我在析构里写了一个输出,在主函数里并没有调用,只是进行了实例化。
cpp
class Stack
{
public:
Stack(size_t capacity=4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("defeat");
return;
}
_capacity = capacity;_size = 0;
}
void Push(int data)
{
// CheckCapacity();
_a[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack";
free(_a);
_a = nullptr;
_capacity = _size = 0;
}
private:
int* _a;
size_t _size;
size_t _capacity;
};
int main()
{
Stack st;
st.Push(1);
st.Push(2);
return 0;
}

3.3 析构函数的特点
- 函数名和类名相同,前面要加~
- 不含任何参数,无返回值无返回类型
- 析构函数只有一个,不能重载,生命周期结束时自动调用
- 要是不显式定义,那么编译器会自动生成析构函数,对内置类型依旧不做处理,自定义会调用它的默认构造函数
但是我会有一个和之前一样的疑问,对于自定义函数,如果我们不写,编译器自动生成默认析构函数,会对自定义成员调用它的析构函数吗?
3.4自定义类型成员一定会自动析构
cpp
class Time
{
public:
Time()
{
cout << "Time()" << endl;
}
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date()
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
// 内置类型
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
程序运行结束后会输出~Time(),但是在main函数里没有创建Time类的,为什么会调用属于Time的析构函数呢?
解释如下:Date是自定义类型,main函数创了Date的实例化对象d,d中的private包含了四个对象,其中三个是内置类型,有一个是自定义类类型。内置类型是压根不需要进行资源清理,而_t是time类对象,所以在对象d进行销毁时,要将d里面的time类的_t对象销毁,所以调用time类的析构函数。
但是main中不能直接调用Time类的析构函数,只能销毁Date类对象,所以编译器会调用Date类的析构函数,在代码里我并没显示写Date类的析构,编译器就会自动生成Date类的默认析构函数,从而间接调用Time类的析构函数,那么在销毁清理Date类对象时,就能确保不但内置类型的内存被回收,自定义Time类的对象也会被清理。
结论:编译器并不能直接调用time类析构函数,所以会让编译器自动生成一个Date类的默认析构函数,从而再内部调用Time类析构完成目的。
它的完整执行流程是
- 实例化对象d,构造Date对象
- 构造内部成员_t,调用Time的默认构造;
- d出作用域,销毁Date对象
- 先调用Date类析构函数~Date();
- 销毁Date类内部成员_t,是Time类,调用~Time()
我在每个构造和析构都进行一个输出,方便证实:

这里也学习到一个额外的知识:构造顺序是先构造成员再构造本体;析构时反过来,先本体销毁在成员销毁
3.5 判断是否要写析构函数
先总结,就是如果要申请调配资源,那就要调用析构函数;如果没有要申请类里面的资源,那就没必要写。
比如stack类就要申请空间,构造和析构都要写,但是date类就不用写,因为并不涉及到释放资源,可以直接用编译器生成的默认构造。
要析构的:
cpp
class Stack
{
public:
Stack(size_t capacity=4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("defeat");
return;
}
}
}
_a是指针,指向一块堆中申请的空间,需要析构函数来释放清理资源
不用析构的:
cpp
class Date
{
private:
int _year;
int _month;
int _day;
};
并没有申请空间资源,内置类型只有内存分配,静态资源在栈上,不需要我们去释放。
前面总结:C++中有构造和析构是为了更好的方便管理资源,以免造成资源的清理不及时。C中每个函数部分都进行和初始化销毁,难免会遗漏,那么析构的自动调用就会很方便,不用担心忘记释放的烦恼。
cpp
//C
STInit(&st);
STDestroy(&st);
//C++
Stack st;
4.拷贝构造(重点)
4.1 拷贝构造的概念
拷贝构造是构造函数的重载形式,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
cpp
Date d1(2026,8,27);
Date d2(d1);//d2是d1的拷贝
其本质就是用一个已有的对象去初始化另一个对象
4.2 拷贝构造的特征
- 拷贝构造是构造函数的一个重载形式
- 拷贝构造的参数只有一个且必须引用,使用传值的话就会报错,会引发无穷递归
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
// Date(const Date d)//错误写法
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
为什么会引发无穷递归呢,因为传值传参首先要调用拷贝构造,那拷贝构造又是传值的话,它不会去拷贝而是再去调用拷贝构造,完成死循环。如果不理解的话也没关系,就记住要传引用就好了。或者用指针也可以,但是看着会比较别扭,因为不那么C++了。
cpp
Date(const Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
Date d2(&d1);

cpp
func(d);//自定义
func(10);//传值
cpp
Date(const Date& d)
{
cout<<"date"<<endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
cpp
void func(Date d)
{
}
void func(int i)
{
}
上面的代码当参数是自定义类型时,必定会先走拷贝构造,然后输出date,再去执行func(Date d)空函数体。
4.3 是否必须进行拷贝构造
如果不写拷贝构造,编译器会生成默认的拷贝。C++规定:
|--------|----------|
| 成员类型 | 是否拷贝构造 |
| 内置类型成员 | 直接拷贝/浅拷贝 |
| 自定义类型 | 必须进行拷贝构造 |
若未显式定义,编译器是会生成默认的拷贝构造函数:默认的拷贝构造函数对象按内存存储按字节序完成拷贝,不会申请新的空间资源,内置类型只是单纯按字节拷贝,而自定义函数则调用它的默认拷贝(没有源码,只有汇编能看到赋值,不会malloc)。
Date类就是不写也可以完成拷贝,因为它可以浅拷贝


但是栈就必须要自己写拷贝构造
cpp
int main()
{
Date d1(1,1,1);
Date d2(d1);
Stack st1;
Stack st2(st1);
return 0;
}

为什么会崩溃呢,因为实际上st1._a和st2._a指向的同一块空间,浅拷贝会造成两次析构,那第一次析构就释放空间了,第二次析构就会出错,析构的先后顺序是先析构st2,再析构st1,符合后进先出原则。并且浅拷贝还有另外一个问题是一次修改会修改另外变量的值

要解决这个问题就要进行深拷贝,深拷贝就是不会指向同一块堆空间,每个对象有自己的空间
cpp
Stack(const Stack&st)//正确的深拷贝,并不是浅拷贝
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("defeat");
return;
}
memcpy(_a, st._a, sizeof(int) * st._size);
_capacity = st._capacity; _size = 0;
}
深拷贝比较复杂,再下一节文章会仔细说明。
4.4 拷贝构造函数典型调用场景:
- 使用同一个类型的对象进行拷贝到新对象。
- 函数参数类型为自定义类型。
- 函数的返回值为类对象。
cpp
//使用同一个类型的对象进行拷贝到新对象。
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
//函数参数类型为自定义类型。
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
//返回值为类对象
Date temp(Date d)
{
Date d(d);
return d;
}
int main()
{
Date d1(2026,8,20);
Date d2(d1);
temp(d1);
}
当然传引用要避免野引用,这上次说过,这里就不详细赘述了。
总结:
Date和myqueue不需要自己写拷贝构造,因为不存在管理申请资源,一个是内置类型,一个可以调用自己的拷贝构造。栈就不一样了,栈需要自己构造函数,自己析构函数释放资源,就要显示写拷贝构造。
5.赋值运算符重载
5.1 运算符重载的概念
在日期类中,如果想要加上几周或者几天,或着要比较两个日期哪个早,哪个晚,其实比较麻烦,因为在C++还是C中,语法并没有定义这些,只有比较内置类型可以,因为编译器内部可以直接转换成指令,而自定义是没规定。为了方便自定义的计算和可读性,运算符重载诞生。
C++为了代码的可读性引入了运算符重载,它也是具有特殊函数名的函数,有返回类型,运算符函数名,运算符和参数列表。
运算符重载的函数模板为:返回值类型 operator+操作符(形参)
cpp
bool operator==(const Date& d1, const Date& d2);// 全局
cpp
bool Date::operator==( const Date& d2);// 类成员函数
5.2 运算符重载有些需要注意的地方
- 重载操作符必须有一个自定义类型。
- 类成员函数重载,形参比操作数看起来是数量少一个,因为第一个参数是隐藏的this指针。
- 不能随意添加其他符号新建一个操作符,比如@,#,¥
- 不能重载内置类型,这不改变其含义,必须要有一个自定义类型
- 是否重载,看运算符对这个类有没有意义。
- 运算符可以全局,也可以放在类成员函数里,不过前者就是两个参数都要写,类成员的第一个参数是this,是左操作数。
第一个:我们不能这样都是内置类型,必须要有一个是自定义类型,这并不会改变内置类型的含义
cpp
int operator+(const int x1,const int x2)
{
return x1+x2;
}
第二个:类成员函数中,形参要比操作数数目看起来少1
cpp
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& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
}
第三个:是否重载看有无意义
| 运算符 | 是否重载 |
|---|---|
| 日期-日期 | 构成,看还相差多少天 |
| 日期-天数 | 构成,算具体日期,还剩多少天 |
| 日期+天数 | 构成,算具体日期,未来第几天 |
| 日期+日期 | 不构成,没get到 |
第四个:全局函数中,函数参数要齐全不能少
cpp
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;
}
第五个:有些运算符不能重载
sizeof, ::, ., ?: ,.*(成员指针访问、作用域、大小、三目、点运算符不能重载。)
第六个:有几个参数个数就有几个操作符,全局是一一对应,类成员函数少一个。
5.3 为什么运算符重载既能够全局又能在类里(有必要搞清楚)
5.4 赋值运算符
赋值运算和拷贝构造需要搞清楚的它们之间的区别,前者是两个已经存在的对象进行拷贝;后者是用一个已经存在的对象去初始化另一个对象。
cpp
Date d1=d2;//拷贝构造
d1=d3;//赋值运算
5.4.1 赋值运算符的格式
- 参数类型:const T&,传引用可以增加效率
- 返回值类型,T&,返回值为引用可以提高返回的效率,支持连续赋值
- 返回*this,不存在空间销毁就不存在,*this是类对象本身,作用域不在空间内
- 查看是否自己给自己赋值
cpp
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;
_month = d._month;
_day = d._day;
}
//赋值运算
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
5.4.2 赋值运算符只能重载成类的成员函数不能重载成全局函数
cpp
// 赋值运算符重载成全局函数,重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}

这里引申出一个问题:
为什么赋值运算符只能在类里面实现而不能在全局重载?
原因:如果赋值运算符不显式实现,那么编译器会生成一个默认赋值运算重载,也就是浅拷贝,那么你再在类外自己实现了一个全局的赋值运算符重载,那么两者就会冲突,所以赋值运算符重载必须在类里面实现。
在没有显式写的话,编译器也会自动生成默认赋值运算符重载,其实和默认拷贝构造类似,以值的方式按字节进行进行拷贝。内置类型是直接赋值,自定义类型调用它自己的默认赋值运算符重载。
5.4.3 何时用
这里也和拷贝构造一样,就是比如日期类这种用你没有写,但是编译器自己生成的默认赋值运算重载已经可以完成值拷贝了,就没必要。但是比如栈这种就必要了,浅拷贝是会崩溃的,必须要进行深拷贝。
关键识别:看是否涉及到资源管理,如果没有,那么赋值运算符是否实现都可以,如果涉及到了,一般都会写显式的构造和析构函数,那么自然也要写上赋值运算符重载。
5.5 前置++和后置++
前置++是返回对象++以后的值,(先+1,再返回引用),而后置++是返回对象++之前的数据(先搞一个临时变量保存之前的值,再去+1),但这个并不会构成函数重载
cpp
Date& operator++();
Date operator++();
后置 ++ : 前置++ 和后置 ++ 都是一元运算符,为了让前置 ++ 与后置 ++ 形成能正确重载
C++ 规定:后置 ++ 重载时多增加 一个 int 类型的参数,但调用函数时该参数不用传递
cpp
Date& operator++();
Date operator++(int);
cpp
Date& operator++()
{
*this +=1;
return *this;
}
//前置++:返回+1之后的结果
// this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date operator++(int)
{
Date tmp =(*this);
*this +=1;
return tmp;
}
//后置++,是要先保存旧值,再去+1,故返回的是+1之前的值。所以要用临时变量去保存之前的*this
//而temp是临时变量,所以只能传值返回,不能传引用
6.日期类实现需要注意的
6.1 流提取和流插入
流提取的符号是<<,流插入的符号是>>,这是个双操作数,在全局上需要两个参数,这个在重载时一般为全局函数。如果按照之前习惯的写法
cpp
cout<<d;
这个是错误的,它调用的是库里的operator,而不是你自定义的,得用全局函数。
cpp
cout.operator<<(d)
为什么这么说,先说cout为啥能够自动识别类型,其实是运算符重载配合上函数重载的功劳。
cout是类ostream的对象<<被重载成了成员函数,编译器看到<<后面的东西会去查它的类型,然后去匹配函数重载中的相关类型。而运算符重载又会形成函数重载,因为只是参数类型不一样。通过函数重载,使得可以支持自定义类型,这个需要注意。
所以这个函数调用的是库里的类,而不是你自定义的Date类中,如果要调用自定义的就要这么写
cpp
d.operator<<(cout)
也就是
cpp
d<<cout
但是看起来很别捏,正常说是右边的流向左边,但是控制台流向类对象,好像不太对。<<的左操作符是cout,cout是库里的ostream类对象。那么你要去改的话,就要去修改库,但是这是不可能的,你写成员函数是肯定不行,因为Date类左操作数必须是this,右操作数是参数。
所以写成全局函数,让两个参数都显现出来,第一个是流,第二个参数是你的类。
cpp
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
流操作符的第一个参数是左操作数,cout是ostream类对象,cin是istream类对象;第二个参数是右操作数Date类对象d。返回值是ostream&和istream&,为了方便连续输入输出
核心区别就是在于:左操作数的身份是谁,如果是成员函数,第一个参数就必须是隐藏的this,也就是类对象;如果是全局函数,两个操作数就不是锁死的,没有this指针了
再补充一句,流既可以支持内置类型,也可以支持自定义类型,很方便。
6.2 获取某月天数
用哈希来表示,也就是要搞一个数组,来对应月份,记住第一个数据要写0,因为没有0越,从下标为1开始算的
cpp
int GetMonthDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
6.3 +=和+的区别
+=会修改对象当前的值,要用引用返回
+是不修改对象当前的值,所以不用引用返回,它是返回临时对象
同样-=和-同理,不过多赘述
cpp
Date& Date::operator+=(int day)
Date Date::operator+(int day)
6.4 两个日期相减(计算天数)
- 先假设默认一个日期大,一个日期小,设定一个标记是1,表示是大的比小的多几天。
2.比较之后,如果原先默认大的小于日期小的那一个,那就更改,同时把标记改为-1,表示小的-大的日期。
3.把小的那个一直加,直到小的和大的相等,那就是相差的日期,再和标记位相乘
6.5 为啥<<和>>要友元
全局函数无法访问私有成员,所以要友元函数,代表着临时访问。
7.const成员
7.1 const 成员函数
const成员的核心宗旨是只要成员函数内部不修改成员变量,都可以加上const(只读模式),这样const和普通对象都可以调用const函数,这里需要注意权限放大缩小平移的原则。
这里我个人可以引申为同级或者上级可以派遣同级或者下级,而下级不能越权指挥。
const修饰成员函数时,放在最后,含义是该成员函数内部不允许修改成员变量
cpp
class Date
{
public:
void Print()const
{
cout<<_year<<"年"<<_month<<"月"<<_day<<
"日"<<endl;
}
}
Date* const this 修饰的是this本身不能被改变,const Date* const this修饰的*this不能被改变,也就是this指向的对象也不会被改变。
7.2 取地址运算符重载
并不是很重要,了解即可,分为const对象取地址和普通对象取地址
cpp
//普通对象取地址
Date* operator&(Date *this)
{
return this;
}
//const对象取地址,两者构成函数重载
const Date* operator&(const Date* this)const
{
return this
}
结语:类和对象中就介绍到这里,这个部分还是很难的,感谢大家的观看阅读,如果这篇文章能够给你们带来哪怕一点点收获和解决困惑,那对我而言都是一种动力和成就感。如果本文对你来说有些帮助,可以点赞收藏关注博主,或者有什么不对的请批评指出,非常感谢!
下期预告:类和对象下篇,前面的一切都是在为后面打基础铺垫,所以我觉得还是不要放弃,虽然前面很难,但是后面应该会轻松一点。