目录
🙊 拷贝构造函数的补充🙊
💖 拷贝构造函数特点
通过前面的介绍,我们知道了以下几个被祖师爷专门创造出来的 “ 天选之子 ”,最重要的是其中的四个,构造函数和析构函数完成初始化和清理,拷贝构造和复制重载完成拷贝和复制。它们有以下几个特点:
1、如果我们不写,编译器就自动生成
2、如果我们写了,编译器就不会自动生成
3、默认生成的构造函数和析构函数对内置类型不做处理
4、默认生成的构造函数对自定义类型会调用对应的构造函数和析构函数
5、构造函数在对象实例化时自动调用,析构函数在对象生命周期结束时自动调用,拷贝构造函数也是在某一动作完成后自动调用
💖 系统默认生成拷贝构造函数相关说明
看如下代码,当不写拷贝构造的时候,系统默认生成的拷贝构造函数是什么样的呢?
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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;
}*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
输出结果如下:
我们发现不写拷贝构造函数时,调用系统默认生成的拷贝构造函数,对内置类型也进行了处理。所以日期类并不需要自己写拷贝构造函数,使用编译器自动生成的拷贝构造函数即可,那既然拷贝构造函数可以处理内置类型,是不是拷贝构造函数都不用自己去写呢?
我们定义一个栈,然后调用系统自动生成的拷贝构造函数,代码如下:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
cout << "Stack(size_t capacity = 10)" << endl;
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Date d1(2023, 2, 5);
d1.Print();
Date d2(d1);
Date d3 = d1; // 拷贝构造
d2.Print();
Stack st1;
Stack st2(st1);
return 0;
运行结果如下:
我们发现运行报错了,因为如果是一个日期类对象,按照值拷贝的方式将 d1 依次拷贝给 d2,而像栈这样的类里面有一个成员是一个指针,指向堆上的一块空间,如果按照值拷贝的方式将其拷贝给 d2,那么最后会造成两个问题:
1、两个栈对象指向同一块地址空间,此时如果给一个栈 push 数据,另一个栈再 push 数据,就会造成数据覆盖的问题。
2、st1 和 st2 两个对象在构造的时候 st1 先定义并构造建立栈帧,st2 后定义并构造建立栈帧,由于栈帧是进程的一块空间,构造顺序和析构顺序保持后进先出的原则,所以 st2 先析构,st1 后析构,由于二者的 _array 都指向同一块空间,析构 st2 后,_array 指向的空间被释放,而此时再析构 st1,_array 指向的空间又被释放了一次,由于此时空间已经被释放,重复释放程序崩溃。
所以如果要对栈这样的对象,要进行深拷贝,st1 和 st2 都需要有各自独立的空间,并存入相同的数据。
经过以上分析,默认拷贝构造函数会出现浅拷贝 (值拷贝) 问题,所以默认拷贝构造函数有时需要我们自己去定义,防止浅拷贝的问题发生。
代码如下:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
cout << "Stack(size_t capacity = 10)" << endl;
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
_array = (DataType*)malloc(sizeof(DataType)*st._capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
exit(-1);
}
memcpy(_array, st._array, sizeof(DataType)*st._size);
_size = st._size;
_capacity = st._capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Date d1(2023, 2, 5);
d1.Print();
Date d2(d1);
Date d3 = d1; // 拷贝构造
d2.Print();
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(4);
Stack st2(st1);
return 0;
运行结果表明插入数据不会相互影响,且程序正常运行。
结论: 自己实现了析构函数释放空间,涉及到了资源管理,就需要实现拷贝构造。
通过以上分析我们知道系统生成的拷贝构造函数针对内置类型会浅拷贝,那么对自定义类型会作何处理?
我们定义一个 MyQueue 类,默认生成构造函数和析构函数,这里内置类型是使用缺省值的方式。
class MyQueue
{
public:
// 默认生成构造
// 默认生成析构
// 默认生成拷贝构造
private:
Stack _pushST;
Stack _popST;
int _size = 0;
};
int main()
{
MyQueue q1;
MyQueue q2(q1);
return 0;
运行结果如下:
我们可以看到,默认生成的拷贝构造对自定义类型完成了深拷贝。所以默认生成的拷贝构造对内置类型完成浅拷贝(值拷贝),对自定义类型去调用成员的拷贝构造函数。
🙊运算符重载🙊
💖 运算符重载概念
C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1、不能通过连接其他符号来创建新的操作符:比如operator@
2、重载操作符必须有一个类类型参数
3、用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5、.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
💖 运算符重载示例代码
运算符重载是为了增强程序的可读性而设计的,比如比较一个日期的大小,或者用日期 - 日期来确定相差几天,如果比较两个日期相等,可以写一个函数,注意要用引用的方式传参,避免传值传参需要调用拷贝构造的问题。如果还需要比较大小等功能,还需要写很多函数,如下面代码:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool Equal(const Date& x1, const Date& x2)
{
//...
}
bool decrease(const Date& x1, const Date& x2)
{
//...
}
int main()
{
Date d1(2023, 2, 3);
Date d2(2023, 3, 4);
Equal(d1, d2);
return 0;
}
大家会发现,如果直接以下面这种方式去写,是不是方便很多?
int main()
{
Date d1(2023, 2, 3);
Date d2(2023, 3, 4);
d1 == d2;
d1 < d2;
return 0;
}
但是内置类型是编译器定义的,编译器知道如何使用,可以直接使用运算符,编译器对自定义类型不知道如何比较,所以自定义类型不能直接使用这些运算符。为了使自定义类型对象也可以使用这些运算符,引出了运算符重载的概念。
💖 实现一个比较相等的运算符
比较相等的运算符的返回值是 bool 值,运算符有几个操作数就有几个参数,如果是单操作数的运算符就有一个参数等等。如果有两个操作数,第一个参数就是左操作数,第二个参数就是右操作数。
注意:
在类里面可以随意使用成员,在类外面私有成员不能随意访问。看如下代码:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
//d1 == d2 d1.operator==(d2)
bool operator==(const Date& d, const Date& d2)
{
return d._year == d._year
&& d._month == d._month
&& d._day == d._day;
}
int main()
{
Date d1(2023, 2, 3);
Date d2(2023, 3, 4);
//operator== (d1, d2);
cout << (d1 == d2) << endl;
d1 == d2;
return 0;
}
注意:
1、类外私有成员不能随意访问,所以此时先将类的成员权限改为 public
2、可以使用 operator== (d1,d2) 进行显示调用,也可以使用 d1 == d2 进行隐式调用
3、如果是隐式调用,编译器就会将其转换成调用 operator==() 函数。
4、进行打印的时候,因为在 c++ 中流插入运算符 << 优先级高需要加 () 。这是将成员权限改成 public 实现运算符重载的情况。
刚才介绍的是将其放入类外的方式,下面介绍将其放入类内的方式。代码如下
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//d1 == d2 d1.operator==(d2)
bool operator==(const Date& d, const Date& d2)
{
return d._year == d._year
&& d._month == d._month
&& d._day == d._day;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 3);
Date d2(2023, 3, 4);
//operator== (d1, d2);
cout << (d1 == d2) << endl;
d1 == d2;
return 0;
}
此时运行结果如下:
注意此时编译器会报错,operator== 的参数太多了,因为放入类内的这种方式成员函数有隐藏参数 this,所以此时需要将代码做出以下修改。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//d1 == d2 d1.operator==(d2)
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
这种书写方式实际上是两个参数,d1 就是 this ,d2 就是 d,在下面进行调用的时候也需要进行书写方式的修改。
int main()
{
Date d1(2023, 2, 3);
Date d2(2023, 3, 4);
//operator== (d1, d2);
cout << (d1 == d2) << endl;//成员函数就转换成d1.operator(d2);
d1 == d2;
return 0;
}
总结:
如果是全局函数,编译器在调用的时候转换成 d1.operator==(d2); 的形式,如果是成员函数,编译器就转换成 d1.operator(d2); 的形式。
接下来再写一个小于赋值重载
// d1 < d2
bool operator<(const Date& d)
{
/* if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
else
{
return false;
}*/
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
基于 = 和 < 的运算符重载之上实现 <= 、 > 、 >= 和 != 的重载,代码如下:
// d1 <= d2
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
//d1 > d2
bool operator>(const Date& d)
{
return !(*this <= d);
}
//d1 >= d2
bool operator>=(const Date& d)
{
return !(*this < d);
}
//d1 != d2
bool operator!=(const Date& d)
{
return !(*this == d);
}
利用复用的方式可以简化代码,可以减少出错。
💖 赋值运算符重载
注意看下面的这段代码,提出以下三个问题:
1、这里的赋值运算符可不可以不使用引用?
2、这里的返回值为什么是 Date& ?
3、为什么这里需要加 if 判断?
// Date operator=(const Date d)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
解答:
1、不用引用不会造成无穷递归,因为要调用 operator=() 就要先传参,由于是值传递,所以传参会先调用拷贝构造函数,拷贝出一份 Date d,拷贝完成以后再执行 operator=() 函数。虽然这种方式也可以运行,但一般我们要加引用避免多进行一次拷贝构造函数的调用,所以这里我们需要使用引用传递的方式。
2、上述代码如果要进行连续赋值如 d3 = d2 = d1,是先将 d1 赋值给 d2,然后表达式 d2 = d1 的返回值作为赋值的右操作数再进行赋值,由于出了作用域 *this 仍存在,所以这里直接加引用即可,这里如果不使用引用返回,会先调用拷贝构造函数拷贝出 *this 再返回。
3、如果是自己给自己赋值如 d1 = d1,那么这里就直接返回。
4、这里因为赋值重载也是天选之子,所以编译器可以自己写,针对此种只需要浅拷贝的赋值重载可以不用自己写,但是对于需要深拷贝的赋值重载,还是需要我们自己去实现。
5、Date d5 = d1 是拷贝构造,因为拷贝构造 d5 没有被实例化出来,使用实例化的对象 d1 去初始化 d5,而 d6 = d1,是赋值重载,因为赋值重载中 d5 和 d1 都是已经实例化出来的对象。
🙊写一个日期类🙊
💖 日期类的头文件
日期类的头文件内容如下:
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print();
int GetMonthDay(int year, int month);
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);
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
// d1 - 100
Date operator-(int day);
// d1 - d2;
int operator-(const Date& d);
// ++d1
Date& operator++();
// d1++
// int参数 仅仅是为了占位,跟前置重载区分
Date operator++(int);
private:
int _year;
int _month;
int _day;
};
💖 日期类的实现
这里主要介绍 +、+=、-、-= 、前置++、后置++、前置–、后置– 的实现过程。
💖 日期 += 天数的逻辑与实现
日期 += 天数的逻辑如下:
1、先加到 “ 天 ” 位,超过当前月的天数就进位,即先将这个月的天数减掉
2、判断如果 “ 月 ” 位也超过了 12,就将 “ 年 ” 位加一,且 “ 月 ” 位重新变成 1
3、由于 += 运算符也可以连续赋值,如 d1 = d2 += 100,所以这里的返回值也为 Date&
// d1 += 100
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
💖 日期 + 天数的逻辑与实现
日期代码如下:
// d1 + 100
Date& Date::operator+(int day)
{
//拷贝构造函数将d1拷贝给tmp
Date tmp(*this);
//接下来用tmp来进行+=
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._year;
tmp._month = 1;
}
}
//*this未改变,而返回tmp时由于是值返回,所以调用拷贝构造函数将tmp进行拷贝然后返回
return tmp;
}
💖 += 和 + 复用的两种方式
由于以上的 += 和 + 的代码十分相似,所以可以选择用 += 复用 +,也可以选择用 + 来复用 +=,下面先介绍用 + 来复用 += 的方式,代码如下:
// d1 += 100
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
下面介绍用 += 来复用 + 的代码实现:
// d1 += 100
// d1 + 100
Date& Date::operator+(int day)
{
//拷贝构造函数将d1拷贝给tmp
Date tmp(*this);
//接下来用tmp来进行+=
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._year;
tmp._month = 1;
}
}
//*this未改变,而返回tmp时由于是值返回,所以调用拷贝构造函数将tmp进行拷贝然后返回
return tmp;
}
Date& Date::operator+=(int day)
{
*this = *this + day;
return *this;
}
对比两种写法,先实现 +,再去复用 + 实现 += 的方法更好还是先实现 +=,再复用 += 实现 + 的方法更好呢?
答案是第一种方法更好,由于 operator+() 在拷贝 d1 给 tmp 时调用一次拷贝构造函数,最后传值返回又调用了一次拷贝构造函数。因此无论使复用还是不服用,operator+() 都会调用两次拷贝构造函数,而第一种写法自己实现 +=,没有发生拷贝行为,而第二种写法复用实现 +=,发生了拷贝行为,所以第一种方法更好一些。
💖 日期前置 ++ 逻辑与实现
前置 ++ 只有日期类对象一个操作数,调用的时候传给了 this 指针,前置 ++ 返回加之后的值,且除了作用域还在,所以传引用返回。代码如下:
// ++d1
Date& Date::operator++()
{
*this += 1;
return *this;
}
💖 日期后置 ++ 的逻辑与实现
为了方便识别前置 ++ 和后置 ++,编译器做了特殊处理加了一个参数为了构成函数重载。后置 ++ 要返回加之前的值。
// d1++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
💖 日期 -= 的逻辑与实现
-= 的实现是借助了借位的思想,首先判断如果输入的 day 是负数,那么 -= day 就相当于 += -day (同理 += 也是这样,这里我们先不对 += 做修改),如果 day 为正数就正常判断。
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += -day;
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
💖 日期 - 的逻辑与实现
日期 - day 的实现并不复杂,复用了 -= 后写出的代码如下:
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
💖 日期前置 – 的逻辑与实现
前置 – 类似于前置 ++,代码实现如下:
Date& Date::operator--()
{
*this -= 1;
return *this;
}
💖 日期后置 – 的逻辑与实现
后置 – 类似于后置 ++,代码实现如下:
// d1-- -> d1.operator--(1)
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
💖 日期 - 日期的逻辑与实现
首先定义 max 和 min 以及 flag,假设第一个日期大,第二个日期小,就更换 max、min 和 flag,然后用计数的方式判断如果 min != max,就 ++min,直到 min 与 max 相等返回 ++ 的次数 * flag 就得出了相差的天数。
// d1 - d2;
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n*flag;
}