这一部分的语法会存在怪怪的,但不必纠结。记住即可。
文章目录
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
不写,编译器自动生成;写了就不生成。
构造函数主要完成初始化的工作
析构函数主要完成清理的工作
拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值给另一个对象
取地址重载主要是普通对象和const对象取地址,这两个很少会自己实现
- 我们不实现时,编译器生成的默认构造函数和析构函数,针对成员变量:内置类型不处理,自定义类型会调这个成员对象的构造和析构
- 我们不实现时,编译器生成拷贝构造和operator=,会完成按字节的值拷贝(浅拷贝)
- 也就是说有些类,我们是不需要去实现拷贝构造和operator=的,因为编译器默认生成的可以用。
考虑一个事,既然编译器默认替我们生成可以处理,那么我们手动生成的默认就是肯定是默认生成的plus版,一定是有原来功能的。
1.构造函数
构造函数的概念
构造函数—>在对象构造时调用的函数,这个函数完成初始化的工作。注意完成的不是构造!!
特性
- 没有返回值
- 函数名和类名相同
- 实例化时自动调用
- 构造函数可以重载
- 对于当前的类,如果没有显式构造函数,编译器会生成默认无参的构造函数(语法坑,c++的双标),一旦用户显式定义(如果显式定义拷贝构造也是)编译器不再生成。而该默认构造函数会做如下的事。
- 针对内置类型的成员变量没有做处理
- 针对自定义类型的成员变量,调用它的默认构造函数初始化。不过对于该成员变量的类来说,默认构造函数初始化有三种。不过对应编译器自己生成的我们没法验证
-
无参的构造函数和全缺省的构造函数是重载,语法上可以同时存在,但是都是默认构造函数。编译错误–编译器无法知道是用哪个默认构造函数
- 调用默认构造函数有三种
- 自己实现无参的构造函数
- 自己实现的全缺省构造函数
- 我们不写,编译器自己生成
- 以上的特点都是不用传参数
- 调用默认构造函数有三种
实例
构造函数的存在场景
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year,int month,int day)
{
//c++要用必须要先声明。但是这里却没有
_year=year;//这里凭空访问_year,这里的参数实际是隐含的this指针.
_month=month;
_day=day;
}
//void Init(Data* this,int year,int month,int day)
//this->_year=year;
void P()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
//编译器处理后实际代码
void P(Date* this)
{
cout<<this->_year<<"-"<<this->_month<<"-"<<this->_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();//在没有初始化INIT前调用了,会出现随机值,很危险。c++通过6个默认成员函数来处理
d1.Init(2020,4,7);
Date d2;
d2.Iint(2020,4,7);
d1.Print();
d2.Print();
}
构造函数的重载
class Date
{
public:
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
Date()
{
_year=0;
_month=0;
_day=0;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year,_month,_day;
};
int main()
{
Date d1(2020,4,8);
d1.Print();
//构造函数可以重载不带参数的。
//Note:无参是不能加括号的
Date d2();//error
Date d2;//
d2.Print();
}
全缺省方式及注意
既然有一个写了就没有无参了,要写两个,有没有更好的方式呢。
更加推荐全缺省方式。
class Date
{
//更好的方式---全缺省
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
};
注意:Date()的全缺省和无参是构成重载的,但是两者不能一起。因为调用的时候会不明确
//等价
class Date()
{
Date()
{
_year=0;
_month=1;
_day=1;
}
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=month;
}
};
///Date()的全缺省和无参是构成重载的,但是两者不能一起。因为调用的时候会不明确
int main()
{
Date d1;//可以调用全缺省的也可以调用无参的。这种选择是不能发生的。
d1.Print();
}
注意:对于全缺省和无参的直接使用类名+对象名
实例化,没有类名+对象名()的用法
至于编译通过是编译器把这个当成函数的声明了;
class Date()
{
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
};
int main()
{
Date d1;
Date d2();
d2.Print();
}
编译器生成的默认构造函数的作用
C++里面把类型分为两类:内置类型(基本类型),自定义类型。
内置类型:int/char/double/指针/内置类型数组
等等
自定义类型:struct/class
的类型
对于自定义类型,回去调用它的默认构造函数(无参数/全缺省)进行初始化。
class Time
{
public:
Time()
{
_hour=0;
_minute=0;
_second=0;
cout<<"Time()"<<endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
d1.Print();//此时发现还是随机值,似乎这个构造函数还不如不存在。好像没做任何事情
//但是我们再来一个类
//此时进行调试,发现_t对象是构造了的,初始化好了,虽然d1的其他变量没有初始化。
}
class A{
public:
A(int a=10)
{
cout<<"A()"<<endl;
_a=a;
}
private:
int _a;
};
class Date{
public:
private:
int _year;
int _month;
int _day;
A _a;
};
int main(void){
Date d1;
}
成员变量的命名风格
如果这样处理,根据就近原则,两个变量都是函数的参数。
class Date
{
Date(int year=0,int month=1,int day=1)
{
year=year;
month=month;
day=month;
}
};
因此一般对成员变量前加_达到区分的目的
class Date
{
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
};
2.析构函数
2.1概念
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
对象生命周期结束的时候调用。
2.2特性
- 析构函数名是在类名前面加上字符
- 无参数无返回值
- 因此无法重载
- 一个类有且只有一个析构函数,若未显式定义,系统会生成默认的析构函数
- 对象生命周期结束时,c++编译系统自动调用析构函数
- 关于编译器自动完成的析构函数,是否会完成一些事情
- 和构造函数的双标特性一样
析构函数存在场景
将类里面的动态开辟的内存清理了。
对于日期类来说,其成员变量在对象生命周期结束时自动释放函数(main
)栈帧,因此析构函数写不写都无所谓。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
~Date()
{
cout<<"~Date()"<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//析构:对象声明周期到了以后,自动调用。完成对象里面的资源清理工作,不是完成d1和d2的销毁。d1,d2的销毁是该对象所在的函数栈帧结束的时候销毁
Date d1;
Date d2;
}
但是对放在堆区上的内存,需要我们在析构函数中手动释放。
构造析构的顺序
至于构造函数的执行顺序和析构函数的执行顺序。考虑到是栈帧。
先构造d1,d2,s1,s2,析构先析构s2,再析构s1
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
~Date()
{
cout<<"~Date()"<<endl;
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int n=10)
{
_a=(int*)malloc(sizeof(int)*n);
cout<<"malloc:"<<_a<<endl;
_size=0;
_capacity=n;
}
~Stack()
{
if(_a)
{
free(_a);
cout<<"free:"<<_a<<endl;
_a=nullptr;
_size=_capacity=0;
}
}
private:
int *_a;
int _size;
int _capacity;
};
int main()
{
Date d1;
Date d2;
Stack s1;
Stack s2;
}
编译器生成的默认析构函数的作用
当前类由编译器自动生成的析构函数是否会完成什么事情
- 对当前类的内置类型、基本类型 int/char 不会处理
- 对当前类中的自定义类型,调用它的析构函数
class Time
{
public:
~Time()
{
cout<<"~Time()"<<endl;
}
};
class Date
{
public:
Date(int year=0,int month=0,int day=0)
{
//....
}
private:
int _year;
int _month;
int _day;
};
编译器生成的默认析构函数价值
当一个类包含了其他类,比如leetcode里面的题目的时候,我们可以不写当前的类的默认构造,加入其他类,这样就能自然而然地初始化其他类和销毁其他类
class MyQueue{
public:
void Push(){}
private:
Stack s1,s2;
}
3.拷贝构造函数
3.1概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器调用
3.2特征
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
- 默认生成的是浅拷贝
- 浅拷贝是存在问题的,对于像Stack这样的类,会导致同一块空间析构的时候释放两次。所以有些类是要手动实现深拷贝。
- 假如不析构就会产生内存泄漏,因此一定要浅拷贝。
3.3拷贝构造的无穷递归
原因是这样的,如果没有&,当调用拷贝构造的时候,由之前的函数栈帧的学习中,对于拷贝构造函数的参数,将d1拷贝给参数发生的。而拷贝这个过程就是拷贝构造函数要进行的过程。因此发生了语义上的无穷递归。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
//Date d2(d1)
Date(Date d)//error:存在递归拷贝
{
_year=d._year;
_month=d._month;
_day=d._day;
}
private:
int _year=0;
int _month=0;
int _day=0;
};
int main()
{
Date d1(2020,5,12);
Date d2(2020,5,12);//如果上面发生更改,下面也要改。就很恶心
//于是产生了拷贝构造
Date d2(d1);<----->Date d2=d1;//***
}
下面说明的是,进行func
的时候,需要将d1拷贝给参数d。
于是将d1拷贝给参数d的这个过程,需要调用拷贝构造函数。
于是可想而知,假如func
函数是拷贝构造函数,就会存在自身无穷地调用自身。
而进行引用作为参数的时候,是不进行传值拷贝的
void func(Date d)
{
}
void fund(Date& d)
{
}
int main()
{
Date d1(2020,5,12);
func(d1);//这里调试可以发现跳到Date的拷贝构造函数
fund(d1);//这里调用直接就进入fund了,因为是引用
}
递归的过程:
//Date d2(d1) //对象初始化的时候自动调用构造函数(此时为拷贝构造)
//调用之前先传参:Date d(d1)--->此时的传参又发生拷贝构造。如此就递归下去了
Date(Date d)//error:存在递归拷贝
{
_year=d._year;
_month=d._month;
_day=d._day;
}
由此说明传值的时候要用拷贝构造
因此解决方案是:传引用来处理。&d和&d1的地址同。
Date(Date& d)//sol:引用解决
{
_year=d._year;
_month=d._month;
_day=d._day;
}
3.4加上const的原因
同时忘了防止自己不小心赋值反了,把传引用的部分用const。导致拷贝错误还改变了自身的值。
Data(const Data& d)
{
d._year=_year;
_month=d._month;
_day=d._day;
}
3.5编译器默认生成的的拷贝构造——浅拷贝
因此对于malloc
的类我们要手动实现进行深拷贝。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year=0;
int _month=0;
int _day=0;
};
int main()
{
Date d1(2020,5,12);
Date d2(d1);
}
浅拷贝是传值拷贝,所以对于指针相关的就会导致将指针的值传过去。也就是指向了同一个地址。因此会导致同一块空间析构两次,abort
操作系统发送信号终止进程。
class Stack
{
public:
Stack(int n=10)
{
_a=(int*)malloc(sizeof(int)*n);
cout<<"malloc:"<<_a<<endl;
_size=0;
_capacity=n;
}
~Stack()
{
if(_a)
{
free(_a);
cout<<"free:"<<_a<<endl;
_a=nullptr;
_size=_capacity=0;
}
}
private:
int *_a;
int _size;
int _capacity;
};
int main()
{
//浅拷贝问题
Stack st1(10);
Stack st2(st1);
Stack st3(30);
//st1=st3;(赋值operatro=)
}
3.6编译器默认生成的拷贝构造作用
默认生成的拷贝构造:
- 对于内置类型成员,会完成按字节序的拷贝(浅拷贝)
- 对于自定义类型成员,会调用该成员的类拷贝构造函数。(类似MyQueue中只包含两个Stack)
拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理细节是不一样的,这个和构造和析构是不一样的
#include<iostream>
#include<malloc.h>
using namespace std;
class A{
public:
A(){}
A(const A& a){
cout<<"拷贝:A()"<<endl;
}
};
class Data{
public:
Data(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
~Data()
{
cout<<"~Data()"<<endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Data d1(2022,1,1);
Data d2(d1);
return 0;
}
4.运算符重载
4.1运算符重载
c++为了增强代码的可读性引入了运算符的重载,运算符重载是具有特殊名的函数。
4.1.1运算符重载的规则
双操作数的运算符重载的时候,规定第一个操作数是左操作数,第二个参数是右操作数。
- 函数名 operator操作符
- 运算符有几个操作数,operator重载的函数就有几个参数
- 不能通过连接其他符号来创建新的操作符
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数目少1成员函数的。操作符有一个默认的形参this,限定为第一个形参,第一个形参为发起对象
-
.*
、::
、sizeof
、?:
、.
不能重载。注意不是*,是.*
4.1.2运算符重载的实例
对于重载中需要成员变量访问,有四种方式。
- 写成员变量的获取封装
- 将成员变量改成公有
- 友元函数(破坏了一些封装性)
- 将运算符重载写到类里。**访问限定符只对类外面的才会受到限制,类内部的是不受限制的。**不过注意以成员函数的形式会有一个默认的
this
,因此参数要少一个。
- 将成员函数改成公有
class Date
{
public:
Date(int year=0,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;
}
int _year=0;
int _month=0;
int _day=0;
}
bool IsDateEqual(const Date& e1,const Date& e2)
{
}///但是这么写麻烦,还要记函数。直接运算符重载就简洁明了
bool operator==(const Date& d1,const Date& d2)
{
return d1._year==d2._year&&d1._month==d2.month&&d1._day==d2._day;//然而是私有的。改成公有
}
int main()
{
Date d1(2020,5,12);
Date d2(2020,5,12);
//比如Date的对象想比较大小相等
cout<<d1==d2<<endl;///编译器转化: ->operator==(d1,d2);这里就是一个函数调用
cout<<operator==(d1,d2)<<endl;///实质内容,但是一般这么写可读性差
}
- 将运算符写到类里
#include<iostream>
using namespace std;
class Data{
public:
Data(int year=0,int month=0,int day=0)
{
_year=year;
_month=month;
_day=day;
}
Data(const Data& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
///这里有隐藏的this指针的一个参数
//bool operator>(const *this,d2);
bool operator>(const Data& d2)
{
if(_year > d2._year) return true;
else if( _year == d2._year && _month > d2._month ) return true;
else if( _year == d2. _year && _month == d2._month && _day > d2._day ) return true;
else return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2021,1,20);
Data d2(2021,1,16);
cout<< ( d1 > d2 ) <<endl;
cout<< (d1.operator>(d2) ) <<endl;
//d1调用,使用operator>(&d1,d2);
return 0;
}
4.1.3运算符重载的原理
VS下先去类里找有没有重载,没有再去看全局里有没有,再没有就报错。
linux下验证一下。
不过不重要。取决于编译器。
4.2赋值运算符重载
拷贝构造和赋值的区别。
拷贝构造:一个已经存在的对象拷贝初始化一个马上创建实例化的对象
赋值:两个已经存在的对象,之间进行赋值拷贝。
int main()
{
Date d1(2020,4,11);
d1.Print();
Date d2(2020,4,15);
d2.Print();
d1=d2
//两个对象都已经存在且初始好了,现在我想把d2赋值(拷贝)给d1 复制
Date d3(d1);//d3 还不存在,构造d3时用d1去初始化
Date d4 =d1 ;//特别注意:这里是拷贝构造,不是operator=,就是上文拷贝构造函数的等价。因为d4还不存在。
}
- 运算符重载是为了让自定义类型可以像内置类型一样去使用
- 自定义类型传参数和返回值时,在可以的情况下,尽量使用引用,减少拷贝
- 和默认的浅拷贝一样,也会产生问题同一块空间释放两次。和默认的拷贝构造一个道理.
4.2.1赋值运算符的返回值
//d3 = d1
//d3 = d3
//运算符重载是为了让自定义类型可以像内置类型一样去使用
void operator=(const Date &d)
{
if(this!=&d)//针对自己给自己赋值,防止浪费
{
this->_year=d._year;
this->_month=d._month;
this->_day=d._day;
}
}
出错的场景:
int i=0,j=1,k=2;
i=j=k;///连续赋值
因此考虑到连续赋值的情况,因此返回值要传对象。
Data operator=(const Data& d2)
{
if( this != &d2 )
{
_year=d2._year;
_month=d2._month;
_day=d2._day;
}
return *this;
}
并且考虑到对象生命周期,为了效率可以传对象的引用。
Data& operator=(const Data& d2)
{
if( this != &d2 )
{
_year=d2._year;
_month=d2._month;
_day=d2._day;
}
return *this;
}
4.2.2赋值运算符的实例
Tips:如果是自己给自己赋值,加一个this != & d
以地址的比较提高效率。
#include<iostream>
using namespace std;
class Data{
public:
Data(int year=0,int month=0,int day=0)
{
_year=year;
_month=month;
_day=day;
}
Data(const Data& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
cout<<"Data(const Data&d)"<<endl;
}
bool operator>(const Data& d2)
{
if(_year > d2._year) return true;
else if( _year == d2._year && _month > d2._month ) return true;
else if( _year == d2. _year && _month == d2._month && _day > d2._day ) return true;
else return false;
}
Data& operator=(const Data& d2)
{
if( this != &d2 )//针对自己给自己赋值,防止浪费
{
_year=d2._year;
_month=d2._month;
_day=d2._day;
}
return *this; //出了作用域还存在,可以引用返回,减少一层拷贝构造
}
void Print() const
{
cout<<_year<<" "<<_month<<" "<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2021,1,20);
Data d2(20l21,1,16);
Data d3(d2);
d3 = d2 = d1;;///当想重新赋值的时候,这里是重载赋值运算符的意义
d2.Print();
d3.Print();
return 0;
}
4.2.3赋值运算符的主要五个特性及编译器生成的默认赋值运算重载
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个。
编译器默认生成赋值重载,跟拷贝构造做的事情类似。
对于内置类型成员,按照字节序进行浅拷贝。
对于自定义类型成员,调用它的operator=。
因此如果当前类有动态开辟的内存,同样需要手动写赋值运算符重载进行深拷贝。
5.实现一个完善的日期类
5.1前置++和后置++(占位参数)
前置++和后置++的区别:
++d1—>d1.operator++(&d1);
d1+±–>d1.operator++(&d1,0(int));这个int是编译器自己传的。
由此可以看出,如果是自定义类型的++,前置++的拷贝次数更少,效率更高。
//++d1
Date& operator++()
{
*this += 1;
return *this;
}
//d1++;
Date operator++(int)//为了构成函数重载
{
Date res = *this;
*this += 1;
return res;
}
5.2实例
5.2.1复用运算符重载
实现了>
就可以利用>
来实现>=
和<
。
实现了=
就可以实现!=
bool operatpr>(const Data& d);
bool operatpr<(const Data& d);
bool operatpr>=(const Data& d);
bool operatpr<=(const Data& d);
bool operatpr==(const Data& d);
bool operatpr!=(const Data& d);
不仅Data类可以实现成这样子,其他类也可以复用。
5.2.2日期加减
注意日期在减一个负数的时候,判断一下,然后这部分要复用一下日期加的。
所以有日期和数字的加减。还有两个日期之间的减法获得多少天。
这里虽然可以更暴力的从小到大跑天数减,但是我们可以复用刚才的运算符重载达到更加简洁的效果。
int Date::operator-(const Data& d)
{
Data max=*this;
Data min=d;
int flag=1;
if(* this < d)
{
max = d;
min = *this;
flag = -1;
}
int count=0;
while(min != max )
{
++min;
++count;
}
return count* flag;
}
5.2.3判断星期
以1900年1月1日的星期一为标准。
5.2.4实现
#include<iostream>
#include<algorithm>
using namespace std;
class Date
{
public:
//四年一润百年不润,四百年一润
bool isYear(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true;
return false;
}
int getMaxDays(int year, int month)//检查对应年的对应月的最大天数
{
int days = 0;
static int months[] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
days = months[month - 1];
if (month == 2 && isYear(year) )
{
days=29;
}
return days;
}
Date(int year = 0, int month = 1, int day = 1)//构造函数
{
if (year >= 0 && month > 0 && month < 13 && day>0 && day <= getMaxDays(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
throw "输入非法";
}
}
Date(const Date& d) //拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Date& d)//判断日期相等
{
return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
bool operator<(const Date& d) //判断日期小于
{
if (this->_year != d._year) return this->_year < d._year;
if (this->_month != d._month) return this->_month < d._month;
return this->_day < d._day;
}
bool operator<=(const Date& d) //判断日期小于等于
{
return *this < d || *this == d;
}
bool operator>(const Date& d)//判断日期大于
{
return !( *this <= d );
}
bool operator>=(const Date& d)//判断日期大于等于
{
return ! ( *this < d);
}
bool operator!=(const Date& d)///日期不等于
{
return !( *this == d );
}
Date operator+(int days)//日期的加天数
{
if (days < 0)
{
return *this - (-days);
}
Date res(*this);
res._day += days;
while (res._day >= getMaxDays(res._year, res._month))
{
res._day -= getMaxDays(res._year, res._month);
res._month++;
if (res._month == 13)
{
res._month = 1;
res._year++;
}
}
return res;
}
//++d1
Date& operator++()
{
*this += 1;
return *this;
}
//d1++;
Date operator++(int)//为了构成函数重载
{
Date res = *this;
*this += 1;
return res;
}
Date operator-(int days)//日期减后是多少天
{
Date res = *this;
if (days < 0)
{
return res + (-days);
}
while (days)
{
int cnt = min(days, res._day);
res._day -= cnt;
days -= cnt;
if (res._day == 0)
{
res._month--;
if (res._month == 0) --res._year, res._month = 12;
if (res._year < 0)
{
throw "年份为负数";
}
res._day = getMaxDays(res._year, res._month);
}
}
return res;
}
Date& operator-=(int days)//日期-=
{
*this = (*this - days); //调用重载的=
return *this;
}
long long operator-(const Date& d)//两个日期中间差多少天
{
Date d1 = *this; Date d2(d); //均为拷贝构造
//交换大小
if (d1 < d2)
{
swap(d1._year, d2._year); swap(d1._month, d2._month); swap(d1._day, d2._day);
}
//方便点的做法是全部转化成天数后再加减
long long cnt = 0;
for (int i = 0; i < d1._year; i++)
{
if (isYear(i)) cnt += 366;
else cnt += 365;
}
for (int i = 1; i < d1._month; i++)
{
cnt += getMaxDays(d1._year, i);
}
for (int i = 1; i <= d1._day; i++)
{
cnt++;
}
for (int i = 0; i < d2._year; i++)
{
if (isYear(i)) cnt -= 366;
else cnt -= 365;
}
for (int i = 1; i < d2._month; i++)
{
cnt -= getMaxDays(d2._year, i);
}
for (int i = 1; i <= d2._day; i++)
{
cnt--;
}
return cnt;
}
//--d;
Date& operator--()
{
*this -= 1;
return *this;
}
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
Date& operator+=(int days) //将日期变成+后的天数
{
if (days < 0)
{
return *this -= -days;
}
(*this)._day += days;
while ( (*this)._day >= getMaxDays( (*this)._year, (*this)._month))
{
(*this)._day -= getMaxDays((*this)._year, (*this)._month);
(*this)._month++;
if ((*this)._month == 13)
{
(*this)._month = 1;
(*this)._year++;
}
}
return *this;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
friend ostream& operator <<(ostream& os,const Date& d) //重载输出
{
return os << d._year << "-" << d._month << "-" << d._day;
}
~Date()
{
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d(2021,9,6);
cout << d << endl;
cout << (d + 100) << endl;
cout << (d == (d + 1) )<< endl;
cout << (d < d + 1) << endl;
d += 10;
cout << d << endl;
Date d2 = d;//拷贝构造
cout << d2 << endl;
Date d3(2021, 9, 8);
cout << d3 << endl;
d2 = d3;///重载赋值运算符
cout << d2 << endl;
//测试日期相减
cout << d3 << endl;
cout << d << endl;
cout << d3 - d << endl;
Date dq(2020, 2, 5); Date dqq(2021, 9, 8);
cout << dq - dqq << endl;
//测试日期减后是什么时候
cout << d3 - 20 << endl;
d3 -= 20;
cout << d3 << endl;
d3 += 20;
cout << d3 << endl;
//测试++
++d3;
cout << d3 << endl;
cout << d3++ << endl;
return 0;
}
5.3声明和定义分离后的一些issue
然后就发现调用的部分没有后置const,连锁反应报错
//d1-d2
int operator-(const Date& d) const
{
}
void Fun(const Date& d3,const Date& d4)
{
cout<< d3 - d4 <<endl; // d3.operator(&d3,d4);发生error,因为这边的d3是const,const对象调用非const成员函数。operator-的隐含的this指针不是const,权限放大。所以一般后面加上const,这样打包给别人用的使用const和非const都能用。这也就是侯捷讲的部分
}
int main()
{
Date d1(2019,4,18);
Date d2(2020,4,18);
Fun(d1,d2);
cout<< d1 - d2 <<endl;//d1.operator-(&d1,d2);
}
6.const成员
-
和基础部分一样,要注意引用和指针的权限问题
-
对象调const成员函数
-
成员函数调用const成员函数
- 绝大部分调用const成员函数涉及this指针
issue:
- const对象可以调用非const成员函数吗?NO
- 非const对象可以调用const成员函数吗?OK
- const成员函数内可以调用其他的非const成员函数吗? NO
- 非const成员函数内可以调用其他的const成员函数吗? OK
结论:什么时候会给成员函数加const
- 只要成员函数中不直接或间接修改成员变量,成员变量和函数最好加上const
- 因此像Data类中比较大小之类的都可以加上,像
+=
就不能,+
可以。
1.对象调const成员函数
此场景主要是对于const对象和非const对象调用成员函数。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
void Print()/* Date* const this,const保护this指针不能变*/
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021,9,8);
d1.Print();//&d-Date* ,传过去只读,权限可以缩小,所以可以调用
}
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
void Print()/* Date* const this*/
{
}///void Print(Date* const this) 权限放大了。需要加const变成 const Date* const this。但是恶心的地方在于this是隐含的。所以把const放在成员函数后面。
void Print() const{
}
/*---->编译器处理:
void Print(const Date* this)
{
//注意我们的const修饰的是*this,不是this
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
const Data d2;
d2.Print();//&d2 - const Data*
//这里的&d 是 const Date* d; 表示*d 不能变
}
2.成员函数调用const成员函数
class Date
{
public:
void f1() //void f1(Date* this)
{
f2();//this->f2(this);
}
void f2() const; //void f2(const Date* this)-->缩小
{
}
void f3()//void f3(Date* this) --->放大了
{
}
void f4() const//void f4(const Date* this)
{
f3();//this ->f3(this);
}
};
7.取地址及const取地址操作符重载
能直接取自定义类型的地址,是因为编译器有默认的取地址重载。
class AA()
{
public:
AA(){}
};
int main()
{
AA aa1;
cout<<&aa1<<endl;
const AA aa2;
cout<<&aa2<<endl;
}
假设让别人能取普通对象的地址,不给取const对象的地址。但是实际上没有这样的需求。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
Date* operator&()///10 // Date*
{
cout<<"operator&()"<<endl;
return this;
}
const Date* operator() const //const Date* 10行和16行参数不同,构成重载.下面的this已经是const了,所以返回值要加const
{
cout<<"operator() const"<<endl;
return this;
return nullptr;///假设某个类我不想让别人取到我的地址,就返回空。基本上没有这么用的。
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1;
Date d2;
const Date d3;
cout<< &d1 <<endl;///不写&重载也能获得,编译器存在默认
cout<< &d2 <<endl;
cout<< &d3 <<endl;//调用不了10行的函数,const对象不能调用非const成员函数
//但是输出的时候仍然成立,因为还有一个默认的函数
}
8.练习题
8.1构造和析构的顺序
- 构造函数顺序和析构函数顺序
- 对于同一个生命周期的按照先后顺序来
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
全部变量的构造在main
函数之前,局部变量和局部静态变量谁先定义谁先构造。局部静态变量的销毁在main
函数销毁后销毁。
构造顺序:C A B D
析构顺序:B A D C
全局变量和全局静态变量看顺序。
8.2编译器优化
优化的时机:传参或传返回值过程中存在连续的构造、拷贝构造,就会被优化。
- 编译器优化都发生在返回和传参时
- 新产生的临时对象作为返回值又要进行拷贝构造时可能存在编译器优化
class Widget{
public:
Widget(){}
Widget& Widget(const Widget& widget){
cout<<"Widget(const Widget& )"<<endl;
}
}
Widget f(Widget u)
{
return u;
}
int main()
{
Widget y = f(x);
}
对于此种情况,分析的应该是3次拷贝构造。
但是打印出来2次—— 一次调用里面,连续构造函数,会被编译器优化,合二为一。也就是拿u
直接拷贝构造y
。
class Widget{
public:
Widget(){}
Widget& Widget(const Widget& widget){
cout<<"Widget(const Widget& )"<<endl;
}
Widget& operator=(const Wideget& widget){
cout<<"operator="<<endl;
}
}
Widget f(Widget u)
{
return u;
}
int main()
{
Widget x;
Widget y;
y = f(x);//此时是赋值,不能优化
}
此时是y=f(x)
是2次拷贝构造和1次赋值。注意此时是不会进行优化的,由于因为17
行是赋值不是构造。
class Widget{
public:
Widget(){}
Widget& Widget(const Widget& widget){
cout<<"Widget(const Widget& )"<<endl;
}
Widget& operator=(const Wideget& widget){
cout<<"operator="<<endl;
}
}
Widget f(Widget u)
{
return u;
}
int main()
{
f(Widget());
}
匿名对象也有优化,把构造和传参的拷贝构造合二为一了。
Widget f(Widget u)
{
Widget v(u);
Widget w=v;
return w;
}
int main()
{
Widget x;
Widget y= f(f(x));
}
最后是7次拷贝构造。