1. 类的6个默认成员函数
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 赋值运算符重载
6. const成员函数
7. 取地址及const取地址操作符重载
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date {};
对于以下Date类:
#include
using namespace std;
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. 无返回值。
- 3. 对象实例化时编译器自动调用对应的构造函数。
- 4. 构造函数可以重载。
class Date
{
public:
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.重载写法:但是和上面的无参调用存在歧义,不能和1写法同时存在
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
6. 关于编译器生成的默认成员函数,很多会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在0.类中声明时可以给默认值。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
// C++11支持,但是此时仍然是声明,此时没有对象
// 声明时给了缺省值
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
总结:默认生成的构造函数,会处理自定义类型去调用其他的默认构造,而不处理内置类型。
注:Data* p也是内置类型。
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。可以不传参数就调用构造,都可叫默认构造。这三个函数只能存在一个,否则产生歧义。
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;//此时无参,重载与无参出现歧义
}
编译不通过:
总结:
- 1.一般情况下,尽量自己写构造函数。
- 2.成员内部都是自定义类型,或者声明给了缺省值,此时可以让编译器自己生成构造函数。
- 3.内置类型都当作未处理,都是随机值。
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 1. 析构函数名是在类名前加上字符 ~。
- 2. 无参数无返回值类型。
- 3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_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++;
}
// 其他方法...
//相当于destroy函数
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~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 d;
return 0;
}
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
- 1. 拷贝构造函数是构造函数的一个重载形式。
- 2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日";
}
private:
int _year;
int _month;
int _day;
};
// 浅拷贝
// 这里可以想象结构体传参,d1传d,传值拷贝
// 这里参数可以写出指针或者引用
void func(Date d)
{
d.Print();
}
int main()
{
Date d1;
func(d1);
return 0;
}
运行结果:
上面的日期类传值拷贝没有问题,那能说明我们上面的特征结论时错误的嘛?我们来看一下栈类的情况。
#include
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Print()
{
//...
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
//栈这里也传值拷贝,可以吗???
void func(Stack st)
{
st.Print();
}
int main()
{
Stack st1;
func(st1);
return 0;
}
这里我们就崩溃了,为什么???
这是因为栈类除了单纯存储了普通数据_top和_capacity,还存储了一个指针_array,而C++有析构函数,传值拷贝后,指针就都指向了同一块空间,当st出了作用域,st对象就会调用析构函数去释放指针所指向的空间,然后st1又会再一次调用一次析构函数,此时就是对空指针进行释放,所以程序就报错了,所以C++传值拷贝是有风险的。而上面的日期类仅仅存储普通数据,所以上面的Date类没有问题,所以浅拷贝/值拷贝是有问题的,解决Stack类需要用到我们的深拷贝。
规定:自定义类型的对象拷贝的时候,调用一个函数,这个函数就叫做拷贝构造。
我们首先来看一下为什么拷贝构造函数参数必须类类型对象的引用,而不能是传值。
调用这个函数的时候,d1作为参数先要传参,d1去调用了拷贝构造函数dd,此时dd作为参数又要传参,dd又要去调用拷贝函数构造......只要传入的参数是对象,参数本身就会自动调用拷贝构造函数。这样就是一个无穷递归。
调用拷贝 -> 先传参(传值传参) -> 形成新的拷贝构造函数 -> ......
所以这里我们就需要传引用/传地址解决。
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& dd)
{
//这里依然是浅拷贝
_year = dd._year;
_month = dd._month;
_day = dd._day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日";
}
private:
int _year;
int _month;
int _day;
};
// 浅拷贝
// 这里可以想象结构体传参,d1传d,传值拷贝
// 这里参数可以写出指针或者引用
void func(Date d)
{
d.Print();
}
int main()
{
Date d1;
func(d1);
cout << "\n";
Date d2(d1);
func(d2);
return 0;
}
C++规定:自定义类型对象传参拷贝时,必须调用拷贝构造。
那我们的栈类还能这样写吗?
Stack(Stack& stt)
{
_array = stt._array;
_capacity = stt._capacity;
_size = stt._size;
}
上面这种方法仍然是错误的,是浅拷贝,同样会调用两次析构函数。这里需要用到我们的深拷贝,思想是开辟一个资源一样大的空间,然后在把值拷贝过来。
#include
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
//Stack(Stack& s)
//{
// 下面这种方法仍然是错误的,是浅拷贝
// _array = s._array;
// _capacity = s._capacity;
// _size = s._size;
//}
Stack(Stack& stt)
{
_array = (int*)malloc(sizeof(int) * stt._capacity);
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, stt._array, sizeof(int) * stt._size);
_size = stt._size;
_capacity = stt._capacity;
}
void Print()
{
//...
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void func(Stack st)
{
st.Print();
}
int main()
{
Stack st1;
func(st1);
Stack st2(st1);//构造一个st2对象,用st1初始化
func(st2);
return 0;
}
通过监视窗口我们可以看到st1对象和st2对象指向的是不同的空间,但是内容都是一样的,此时调用的析构函数释放的空间就是不用的。
不知道我们有没有发现,上面的Date类就算没有拷贝构造函数,也是能正常运行的,这是因为拷贝构造函数时一个默认成员函数,对内置类型会完成值拷贝,而对自定义类型Stack需要调用它的拷贝构造函数,需要字节写拷贝函数。但是并不是所有的自定义类型都要写拷贝构造函数,比如下面的MyQueue类:
#include
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
//Stack(Stack& s)
//{
// 下面这种方法仍然是错误的,是浅拷贝
// _array = s._array;
// _capacity = s._capacity;
// _size = s._size;
//}
Stack(Stack& stt)
{
_array = (int*)malloc(sizeof(int) * stt._capacity);
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, stt._array, sizeof(int) * stt._size);
_size = stt._size;
_capacity = stt._capacity;
}
void Print()
{
//...
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
class MyQueue
{
Stack _pushst;
Stack _popst;
int _size;
};
int main()
{
MyQueue q1;
MyQueue q2(q1);
return 0;
}
运行结果:
这里也完成了我们的深拷贝。Date和MyQueue默认生成的拷贝构造函数就i可以使用,对应内置类型成员完成值拷贝,对应自定义类型成员调用这个成员的拷贝构造。但是Stack类需要字节写拷贝构造函数,完成深拷贝。像我们的顺序表、链表和二叉树都需要深拷贝。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
#include
using namespace std;
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
//为防止写反,这里加const
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;
}
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
#include
using namespace std;
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
5. 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
#include
using namespace std;
class Date
{
public:
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()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022, 1, 13);
Test(d1);
return 0;
}
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。
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;
};
int main()
{
Date d1;
Date d2(2023, 10, 24);
int x = 1, y = 2;
bool ret1 = x > y;
bool ret2 = x == y;
//内置类型对象可以直接用各种运算符,内置类型都是简单类型
//那自定义类型呢?
//这里可以想象一下结构体的比较
d1 == d2;//error
d1 > d2;//error
return 0;
}
运行结果:
这里需要想象一下结构体的比较,比较结构体是比较内部的成员,这里的类也是类似的。
#include
using namespace std;
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 Geater(Date x1, Date x2)
{
if (x1._year > x2._year)
return true;
else if (x1._year == x2._year && x1._month > x2._month)
return true;
else if (x1._year == x2._year && x1._month == x2._month && x1._day > x2._day)
return true;
else
return false;
}
bool Equal(Date x1, Date x2)
{
//成员私有不可访问
//我们可以改一下上面的限制符private
return x1._year == x2._year
&& x1._month == x2._month
&& x1._day == x2._day;
}
int main()
{
Date d1;
Date d2(2023, 10, 24);
cout << Geater(d1, d2) << endl;
cout << Equal(d1, d2) << endl;
return 0;
}
运行结果:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能通过重载运算法改变其内置类型的运算规则含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* ::(域作用限定符) sizeof ?:(三目运算符) . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
#include
using namespace std;
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& x1, const Date& x2)
{
if (x1._year > x2._year)
return true;
else if (x1._year == x2._year && x1._month > x2._month)
return true;
else if (x1._year == x2._year && x1._month == x2._month && x1._day > x2._day)
return true;
else
return false;
}
//这里会调用拷贝构造
bool operator==(const Date& x1, const Date& x2)
{
return x1._year == x2._year
&& x1._month == x2._month
&& x1._day == x2._day;
}
int main()
{
Date d1;
Date d2(2023, 10, 24);
cout << operator>(d1, d2) << endl;
cout << operator==(d1, d2) << endl;
//这里就可以直接写成内置类型比较的形式
//由于流插入 << 优先级比 > 和 == 优先级高,所以这里需要加括号
cout << (d1 > d2) << endl; //operator>(d1,d2)
cout << (d1 == d2) << endl; //operator==(d1,d2)
return 0;
}
这里需要分清运算符重载和函数重载之间的关系,它们之间没有任何关系
- 运算符重载:自定义类型可以直接使用运算符
- 函数重载:可以允许参数不同的同名函数
但是这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证? 这里其实可以将函数移入到Date类中。
但是这里报错了为什么呢?编译器提示:error C2804: 二进制“operator >”的参数太多,因为类中含有一个隐藏的this参数。
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//这里隐藏了一个this参数
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator>(const Date& x2)
{
if (_year > x2._year)
return true;
else if (_year == x2._year && _month > x2._month)
return true;
else if (_year == x2._year && _month == x2._month && _day > x2._day)
return true;
else
return false;
}
//这里会调用拷贝构造
bool operator==(const Date& x2)
{
return _year == x2._year
&& _month == x2._month
&& _day == x2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 10, 24);
cout << (d1 > d2) << endl; //d1.operator>(d2) ==> d1.operator>(&d1,d2)
cout << (d1 == d2) << endl; //d1.operator==(d2) ==> d1.operator==(&d1,d2)
return 0;
}
我们再来实现一下+运算符重载
#include
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int GetMonthDay(int year, int month)
{
assert(year >= 1 && month >= 1 && month <= 12);
int MonthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && (year % 4 == 0 && year % 100 != 0)
|| (year % 400 == 0))
return 29;
return MonthArray[month];
}
//d1 + 100
void operator+(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 10, 24);
d2 + 50;
return 0;
}
通过监视窗口观察我们实际上实现的是+=运算符重载,因为d2对象的内容被改变了,而+运算符重载不会改变对象的内容。
d2 += 50;//这里不支持连续+=,因为日期不能+=日期
要实现连等,我们需要接收返回值,所以上面的+=运算符重载是需要返回值的。
#include
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int GetMonthDay(int year, int month)
{
assert(year >= 1 && month >= 1 && month <= 12);
int MonthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
return 29;
return MonthArray[month];
}
//d1 += 100
Date operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 10, 22);
d2 += 50;//这里不支持连续+=
int i = 0;
// + 操作符有返回值
int ret1 = i + 50;
int j = 0;
// += 操作符有返回值
int ret2 = j += i += 50;
return 0;
}
上面我们返回的*this仍然是值拷贝,但是*this就是d2,d2的生命周期是在main函数中的,所以上面的运算符重载的返回值可以使用引用。
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
现在我们再来写我们的+运算符重载,首先我们要知道+和+=的区别,+操作符是不改变自身的。
#include
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int GetMonthDay(int year, int month)
{
assert(year >= 1 && month >= 1 && month <= 12);
int MonthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && (year % 4 == 0 && year % 100 != 0)
|| (year % 400 == 0))
return 29;
return MonthArray[month];
}
// d2 + 50
Date& operator+(int day)
{
Date tmp(*this);//拷贝一份d1
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
tmp._month++;
if (tmp._month == 13)
{
tmp._month = 1;
tmp._year++;
}
}
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 10, 22);
Date ret = d2 + 50;
return 0;
}
这样就实现了我们的+运算符重载对象之间互不影响。
1. 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// d1 = d3
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2(2023, 10, 26);
//一个已经存在的对象去拷贝初始化另一个对象
Date d3(d2);//拷贝构造函数
//两个已经存在的对象的拷贝
d1 = d3;//赋值运算符重载
d1.Print();
}
如果我们想要连续赋值呢?很明显连等是错误的。因为我们上面写的函数是没有返回值的。
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// d1 = d3
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2(2023, 10, 26);
//一个已经存在的对象去拷贝初始化另一个对象
Date d3(d2);//拷贝构造函数
//两个已经存在的对象的拷贝
d1 = d3;//赋值运算符重载
d1.Print();
//连续赋值
d2 = d1 = d3;
}
这样就实现了连等的操作。
但是我们能对象给对象自己赋值,很明显是可以的。但是按照我们上面的代码太复杂,自己赋值自己就没必须再逐一拷贝。
Date& operator=(const Date& d)//d是d1对象的别名,共用一块空间
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有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;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,完成值拷贝,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
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;
Date d2;
d1 = d2;
return 0;
}
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实 现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
#include
using namespace std;
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必 须要实现。
现在我们再来完善一下我们的日期类。当我们的输入的日期非法程序依然能够打印出来。
注:当构造函数在声明和定义出现全缺省参数时,规定在声明的地方写全缺省参数,定义的地方不写。
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (_year < 1 || _month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
assert(false);
}
}
我们上面的运算符重载写了>运算符重载、=运算符重载和+=运算符重载。我们在来写一下!运算符重载等其他比较运算符重载,可以采用复用。
bool operator>(const Date& x2)
{
if (_year > x2._year)
return true;
else if (_year == x2._year && _month > x2._month)
return true;
else if (_year == x2._year && _month == x2._month && _day > x2._day)
return true;
else
return false;
}
bool operator==(const Date& x2)
{
return _year == x2._year
&& _month == x2._month
&& _day == x2._day;
}
bool operator!=(const Date& x2)
{
return !(*this == x2);
}
bool operator>=(const Date& x2)
{
return *this > x2 || *this == x2;
}
bool operator<(const Date& x2)
{
return !(*this >= x2);
}
我们再来看一下下面两种复用哪种更优?下面这个是+操作符重载复用+=操作符重载
Date operator+=(int day)
{
//如果传入的day是负数
if(day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
这种是+=操作符重载复用+操作符重载的实现
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date operator+(int day)
{
Date tmp(*this);//拷贝一份d1
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
tmp._month++;
if (tmp._month == 13)
{
tmp._month = 1;
tmp._year++;
}
}
return tmp;
}
Date& operator+=(int day)
{
*this = *this + day;
return *this;
}
上面的第一种写法+操作符重载复用+=操作符重载,+操作符重载进行了两次拷贝对象,而后面的那种写法+=操作符重载复用+操作符重载,+操作符重载进行了两次拷贝对象,+=操作符重载进行了两次拷贝对象,还有=操作符重载的一次。
我们来实现一下-=操作符重载和-操作符重载,这里仍然是让-操作符重载复用-=操作符重载
Date& operator-= (int day)
{
//如果传入的day是负数
if(day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
结果验证:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器\
自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存\
一份,然后给this + 1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
*this += 1;
return temp;
}
Date& operator--()
{
*this -= 1;
return *this;
}
Date operator--(int)
{
Date temp(*this);
*this -= 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2023, 10, 27);
d = d1++; // d: 2023,10,27 d1:2023,10,28
d = ++d1; // d: 2023,10,29 d1:2023,10,29
return 0;
}
// d1 - d2
Date operator-(const Date& d)
{
//假设左大右小
int flag = 1;
Date max = *this;
Date min = d;
//假设错了,左小右大,此时相减就是负数
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
我们再来回顾一下我们上面的Print函数,每次该成员函数都要我们自己去实现,比较麻烦,我们可以通过运算符重载去实现打印,由于此时是自定义类型,所以可以通过重载流插入运算符实现。
cout是ostream类型的对象,cin是istream类型的对象
内置类型可以支持流插入操作,是因为库里面已经实现过了。
#include
using namespace std;
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void operator << (ostream& out)//cout是ostream的对象
{
out << _year << "年" << _month << "月" << _day << "日";
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 5);
cout << d1;//报错,why??
d1 << cout;//运行成功!!
return 0;
}
运行结果:
双操作数的运算符,第一个参数是左操作数,第二个操作数是右操作数,作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this,所以对于第二种写法,第一个参数是d1,第二个参数是cout,可以转化为d1.operator<<(&d1,cout),对于第一种写法参数就不匹配,所以报错。虽然第二种写法是正确的,但是看着不习惯,要想第一种此恶法正确,我们必须第一个参数设置为cout。因此对与该函数我们可以写成全局函数。
#include
using namespace std;
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;
};
void operator << (ostream& out,Date &d)//cout是ostream的对象
{
out << d._year << "年" << d._month << "月" << d._day << "日";
}
int main()
{
Date d1(2023, 11, 5);
cout << d1;
//d1 << cout;
return 0;
}
此时就转化成了operator<<(cout,d1),虽然上面我们确实通过将该函数放到全局变量中实现了我们的流插入操作符函数,但是使用该函数的时候我们必须要将类的成员变量设置公开,那么这样不就与封装性相悖吗?我们可以通过友元来解决。
#include
using namespace std;
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//友元函数
friend void operator << (ostream& out,Date &d);//cout是ostream的对象
private:
int _year;
int _month;
int _day;
};
void operator << (ostream& out,Date &d)//cout是ostream的对象
{
out << d._year << "年" << d._month << "月" << d._day << "日";
}
int main()
{
Date d1(2023, 11, 5);
cout << d1;
//d1 << cout;
return 0;
}
上面使用了友元函数,这里介绍一下,友元函数就是相当你有一个私人游泳池(相当于类的私有成员变量),然后你的非常要好朋友可以直接去你的私人游泳池游泳(访问私有变量),此时你不介意。我们的流插入操作符还可以支持连续使用,想要连续使用就必须设置一个返回值,所以我们要修改一下上面的代码
#include
using namespace std;
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//友元函数
friend ostream& operator << (ostream& out,const Date &d);//cout是ostream的对象
private:
int _year;
int _month;
int _day;
};
//流插入的内容d不会改变,可以加上const修饰
ostream& operator << (ostream& out,const Date &d)//cout是ostream的对象
{
out << d._year << "年" << d._month << "月" << d._day << "日";
return out;
}
int main()
{
Date d1(2023, 11, 5);
Date d2;
//赋值运算符顺序:从右往左
//流插入运算符顺序:从左往右
//由运算符的结合性决定
//这里的返回值必须是cout
cout << d1 << d2;
return 0;
}
我们再来实现一下流提取运算符重载
#include
using namespace std;
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//友元函数
friend ostream& operator << (ostream& out,const Date &d);//cout是ostream的对象
friend istream& operator >> (istream& in,Date &d);//cin是istream的对象
private:
int _year;
int _month;
int _day;
};
//流插入的内容d不会改变,可以加上const修饰
ostream& operator << (ostream& out,const Date &d)//cout是ostream的对象
{
out << d._year << "年" << d._month << "月" << d._day << "日";
return out;
}
//流插入的内容d会改变,不可以加上const修饰
istream& operator >> (istream& in,Date &d)//cin是istream的对象
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1(2023, 11, 5);
Date d2;
//赋值运算符顺序:从右往左
//流插入运算符顺序:从左往右
//由运算符的结合性决定
//这里的返回值必须是cout
cin >> d1 >> d2;
cout << d1 << d2;
return 0;
}
运行结果:
总结:其他的运算符一般实现成成员函数,流插入和流提取操作符必须实现到全局中,这样才能让流对象作第一个参数。流本质是为了解决自定义类型输入和输出问题,C语言的printf函数只能指定内置类型,同时打印输出的时候还要指定输出格式,C语言的printf函数和scanf函数无法解决自定义类型的输入输出问题。C++通过面向对象和运算符重载来解决。
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
31 };
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看看下面的代码
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
int main()
{
Test();
return 0;
}
当我们给对象加上const限制时,这里报错了为什么呢?
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this,这个隐藏的this类型的Date* cosnt this,而我们传入的类型时const Date* this。这里就会存在权限放大的问题,我们需要在隐藏的this类型的前面加上cosnt,但是this类型是隐藏的,我们应该怎么加呢?C++规定在函数之后写上cosnt就可以对隐藏的this类型的前面加上cosnt。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
int main()
{
Test();
return 0;
}
加上之后非const类型对象也能运行,因为只是进行了权限的缩小。运行结果:
总结:类似于比较运算符,打印函数和+-运算符,不会修改任何成员,所以在写该类函数的时候我们可以加上const。成员函数定义的原则:1、能定义成cosnt的成员函数都应该定义成cosnt,这样const对象(权限平移)和非const对象(权限缩小)都可以调用。2、要修改成员变量的成员函数,不能定义成cosnt。3、流插入和流提取不能加上cosnt,它们不是成员函数,没有this指针,是全局函数。
请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?不可以,权限不能放大
2. 非const对象可以调用const成员函数吗?可以,权限可以缩小
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
cout << "Date * operator&()" << endl;
return this;
}
const Date* operator&()const
{
cout << "const Date * operator&()const" << endl;
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
运行结果:
为什么这里要写两个取地址操作符重载呢?因为普通对象返回Date*,而const对象返回cosnt Date*,这两个是不同的,参数也是不同的,所以能构成重载。如果我们注释第一个非const成员函数。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date* operator&()
//{
// cout << "Date * operator&()" << endl;
// return this;
//}
const Date* operator&()const
{
cout << "const Date * operator&()const" << endl;
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
运行结果:
此时我们发现就会调用非cosnt成员函数,此时发生了参数和返回值权限的缩小。这个就和我们吃饭一样,有好吃的有自己相吃的就去吃,没有的就将就一下。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date* operator&()
//{
// cout << "Date * operator&()" << endl;
// return this;
//}
/*const Date* operator&()const
{
cout << "const Date * operator&()const" << endl;
return this;
}*/
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
运行结果:
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!