文章目录
- 四、拷贝构造函数
- 干嘛的?
- 写拷贝构造函数的注意事项
- 正确写法
- 不显示定义拷贝构造函数的情况
- 浅拷贝
- :one:示例:内置类型
- :two:示例:自定义类型
- 一个提问
- 深拷贝
- 五、赋值运算符重载
- 运算符重载
- 函数原型
- 注意
- 调用时的两种书写方式
- 完整实现代码
- 赋值运算符重载
- 干嘛的?
- 连续赋值
- 总结赋值运算符重载格式
- 默认生成的复制重载函数的行为
- 默认生成的函数行为总结
- 赋值运算符是否可以重载为全局函数
书接上回: 【C++】类和对象(三)构造与析构
四、拷贝构造函数
干嘛的?
拷贝构造函数:用同类型的其他对象 构造一个(初始化 )新的对象
代码演示:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//Date d2(d1);Date(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(2024, 1, 28);Date d2(d1);d1.Print();d2.Print();return 0;
}
写拷贝构造函数的注意事项
注意:拷贝构造函数 参数位置必须要传引用 !
为什么参数位置必须要传引用?
传值做拷贝构造函数的参数 🆚 传引用做拷贝构造函数的参数
C++规定 调用拷贝构造函数时,
1️⃣自定义类型本身 作为实参传递过去(传值传参),都会先调用拷贝构造。
🌰例如:
如果传值传参 会发生无穷递归的问题:
2️⃣自定义类型的引用 作为实参传递过去(传引用传参),就不会先调用拷贝构造。
除此之外,为了 防止写反方向的类似问题,我们一般给 引用前面加const修饰
例如:可能出现赋值方向写反的问题
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//Date d2(d1);Date(Date& d){//赋值方向写反的问题d._year = this->_year;d._month = this->_month;d._day = this->_day;//_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(2024, 1, 28);Date d2(d1);d1.Print();d2.Print();return 0;
}
写反方向的情况如下:
正确写法
加入const修饰 保护要赋值给别人的对象
Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}
不显示定义拷贝构造函数的情况
浅拷贝
程序员不显示定义拷贝构造函数 ,则编译器会自动生成拷贝构造函数。并且
1️⃣ 对内置类型的成员变量进行值拷贝(浅拷贝)。
2️⃣对自定义类型的成员变量 调用它的拷贝构造。
1️⃣示例:内置类型
2️⃣示例:自定义类型
class Time
{
public:~Time(){cout << "~Time()" << endl;}// 注意:拷贝构造函数 也属于构造函数 编译器就不会自动生成 构造函数// 但是下面一句代码 可以强制编译器生成默认构造Time() = default;Time(const Time& t){cout << "Time(const Time& t)" << endl;_hour = t._hour;_minute = t._minute;_second = t._second;}private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date d2(d1);/*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;// 自定义类型Time _t;
};int main()
{Date d1(2024, 1, 28);Date d2(d1);d1.Print();d2.Print();return 0;
}
代码演示:对自定义类型的成员变量 调用它的拷贝构造
一个提问
❓对于内置类型、自定义类型的成员变量,即使程序员不提供拷贝构造函数,编译器都会自动进行拷贝,那么拷贝构造函数是不是就可以不用我们写了呢?
答案当然不是的。在有些情况,编译器默认生成的拷贝构造会出现问题,我们可以看看如下的问题情况。
问题代码:
#include<iostream>
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 st1;Stack st2(st1);return 0;
}
问题描述:经过调试,我们发现 _array的值是一个地址也被拷贝过来 ,这意味着两个对象指向同一块空间。
出了函数作用域,会对他们析构,把
st2
对象的 _array空间释放。但st2置空并不影响st1,st1依然指向那块空间,导致st1成为野指针,析构st1时,_array空间再次被释放,相当于同一块空间被释放了两次。
所以,对于动态开辟的内存空间 都要使用深拷贝。深拷贝就是,为要拷贝st1的st2再开辟一块同样大小的新空间。
深拷贝
对于动态开辟的内存空间,必须程序员自己实现深拷贝!
代码如下:
#include<iostream>
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;}Stack(const Stack& s){DataType* tmp = malloc(sizeof(s._capacity * sizeof(DataType)));if(tmp == nullptr){perror("malloc fail");exit(-1);}memcpy(tmp , a._array,sizeof(DataType)*s._size);_array = tmp;_size = s._size;_capacity = s._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 st1;Stack st2(st1);return 0;
}
可以看到经过深度拷贝的两个指针分别开辟了两块不同的空间。
五、赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型、函数名字、参数列表。其返回值类型、参数列表与普通的函数类似。
函数原型
函数名:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
🌰
bool operator==(const Date& y)
bool operator<(const Date& y)
注意
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* (点星运算符) :: (域作用限定符) sizeof (计算变量大小的运算符) ?: (三目运算符) . (点操作符) 注意以上5个运算符不能重载。这个经常在笔试选择题中出 现。
注意:区分 运算符重载 跟 函数重载,二者没有关系。
运算符重载:让自定义类型的对象可以使用运算符。通过函数定义了该运算符的行为
函数重载:允许函数名相同 参数不同的函数存在,通过函数名修饰规则可以找到对应的函数
代码示例🌰
这里展示两个运算符重载函数 他们是用来 比较自定义对象 日期年月日的大小
调用时的两种书写方式
d1.operator==(d2)
d1 == d2
完整实现代码
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this// operator运算符 做函数名bool operator==(const Date& y){return _year == y._year&& _month == y._month&& _day == y._day;}// operator运算符 做函数名bool operator<(const Date& y){//如果 年小就小if (_year < y._year){return true;}//如果年相等else if (_year == y._year){//如果月小就小if (_month < y._month){return true;}else if (_month == y._month){return _day < y._day;}}return false;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 1, 28);Date d2(2024, 2, 27);//通过对象调用成员函数的方式 调用运算符重载函数 比较日期对象d1和d2的大小cout << d1.operator==(d2) << endl;cout << d1.operator<(d2) << endl;//另一种调用方式 加括号的原因:流插入运算符的优先级高于等于号cout << (d1 == d2) << endl; // cout << (d1.operator==(d2)) << endl;cout << (d1 < d2) << endl; // cout << (d1.operator==(d2)) << endl;bool ret1 = d1 < d2;bool ret2 = d1.operator<(d2);int i = 0;int j = 1;bool ret3 =i<j;return 0;
}
通过反汇编窗口,我们可以看到
对于自定义类型的运算符重载函数的两种调用方式底层的汇编代码都是一样的。
而对于内置类型的运算符比较,是直接有cmp指令支持的,不用程序员自己规定大小比较方式。
赋值运算符重载
干嘛的?
赋值运算符重载:对已经存在的同类型对象,一个拷贝赋值给另一个,就用到了赋值运算符重载
代码示例
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//d2 = d1;Date& operator=(const Date& d){if (this != &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(2024, 1, 28);Date d2(1990, 2, 1);Date d3(d2); // 拷贝构造,同类型一个存在的对象进行初始化要创建的对象d2 = d1;// 已经存在的对象,一个拷贝赋值给另一个d2.Print();d3.Print();return 0;
}
栗子结果
连续赋值
int i = 0, j = 1;
i = j = 10;
注意:
- 对于内置类型,10先赋值给j ,表达式的返回值为j ,j再作为下一次赋值的右操作数 以此支持连续赋值。
同理,对于自定义类型,为了支持连续赋值,我们需要拿到表达式的结果,所以在写赋值重载函数时,我们要返回被赋值好的对象 *this。- 函数传值返回会调用拷贝构造函数,为了避免浪费,赋值运算符函数的返回值使用引用返回。
- 为了防止自己给自己赋值,我们需要加一个判断。
总结赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
默认生成的复制重载函数的行为
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
默认生成的函数行为总结
赋值运算符是否可以重载为全局函数
❓运算符重载可以在全局重载,那赋值运算符重载可以在全局重载嘛?
注意: 赋值运算符只能重载成类的成员函数不能重载成全局函数
原因::赋值运算符如果不显式在类里面实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数