拷贝构造函数
拷贝构造作用:一个已经存在的对象去初始化另一个要创建的对象
日常写代码中会出现如下场景:
class Data
{
public:Data(int year, int month, int day) // 拷贝构造函数{this->_year = year;this->_month = month;this->_day = day;}
private:int _year;int _month;int _day;
}int main()
{Data d1(2024.11.11);Data d2(d1);
}
在上面的代码中, 我们想要使用一个已存在的对象去初始化另一个对象, 此时拷贝构造的作用就体现出来了.
class Data
{
public:Data(Data& data) // 拷贝构造函数{this->_year = data._year;this->_month = data._month;this->_day = data._day;}
private:int _year;int _month;int _day;
}
拷贝构造函数特性:
- 是构造函数的一种重载形式.
- 参数只有一个,且必须是同类型对象的引用.
推荐拷贝构造函数加上 const 修饰
class Data
{
public:Data(const Data& data) // 拷贝构造函数{this->_year = data._year;this->_month = data._month;this->_day = data._day;}
private:int _year;int _month;int _day;
}
因为我们只是想要复制对象, 并不需要修改, 所以加上 const 修饰更加安全, 防止对象数据被修改.
参数必须是引用
上面说, 因为拷贝构造是不会修改对象属性的, 所以推荐加上 const 修改.
那如果我不传引用, 参数就是对象的一个副本, 这样即使修改了参数, 也不会影响外部的对象. 这样就可以不写 const.
class Data
{
public:Data(Data data) // 拷贝构造函数{this->_year = data._year;this->_month = data._month;this->_day = data._day;}
private:int _year;int _month;int _day;
}
上面的代码是错误的. 拷贝构造函数的参数有且只有一个, 必须是同类型对象的引用.
首先, 我们知道拷贝构造函数的作用: 一个已经存在的对象去初始化另一个要创建的对象.
如果, 传参用的不是引用, 那么那个参数就是原对象的副本, 这个副本是怎么来的?
这个副本也是通过那个"已存在的对象创建出来的" , 这不就是拷贝构造函数的作用吗?
这个副本是通过拷贝构造函数创建出来的, 而拷贝构造函数又需要这个副本, 就形成了一个死循环.
这样最终就会造成无限死循环.
编译器默认生成的拷贝构造函数
拷贝构造也是C++中的默认成员函数.
如果我们没有显示的写构造函数, 编译器就会帮我们生成一个默认的拷贝构造函数.
编译器默认生成的拷贝构造内置类型会赋值(值拷贝),自定义类型会去调用自定义类的拷贝构造.
在涉及到动态内存管理时, 使用默认生成的拷贝构造函数可能会出现问题. (浅拷贝问题)
class Stack
{
public:Stack(int size){_size = size;_array = (int*)malloc(sizeof(int) * size);if(_array == nullptr){perror("malloc 申请空间失败");}}~Stack(){if(_array){free(_array);}}
private:int* _array;int _size;
}int main()
{Stack s1(10);Stack s2(s1);
}
上面的代码中没有显示的写构造函数, 用的就是编译器生成的默认函数.
指针也属于C++的内置类型 (内置类型值拷贝), 所以通过 s1 去初始化 s2 时, s2 中的 _array 与 s1的 _array 指向的是同一块空间.
\
可能会导致的问题:
当 s1 和 s2 生命周期结束时
它们都会调用析构函数释放空间
s1 的 _array 和 s2 的 _array 所指向的空间是同一个
s1 和 s2 一共两次析构, 就会对那块空间进行两次释放
对一快空间进行两次释放, 会导致程序出错
所以当涉及到动态内存管理时, 需要格外注意浅拷贝问题.
赋值重载
复制重载是运算符重载的一种.
复制重载就是对 "=" 实现运算符重载.
运算符重载
Data d1(2024.11.11);
Data d2 = d1 + 100;
在代码中有一个 d1 对象, 我们想得到一个新的日期类 d2, d2 日期类 = d1日期 加上100天.
我们直接使用 "+" 运算符是无法进行计算的.
C++中, 普通的 "+" 运算符只能用于内置类型,
对于自定义类型是不适用的.
如果我们像是让 "+" 运算符支持自定义类型,
我们就需要去实现 "+" 运算符的重载.
为什么会出现运算符重载?
我们完全可以实现一个成员函数来完成这些功能
比如 "+" 运算符的功能, 我实现一个plus(int day) 成员函数
也是完全可以实现所需功能的.
使用运算符重载最主要好处是:
便于代码的理解, 当我们在代码中看到一个成员函数
我们需要去看函数接口的文档/讲解, 而在代码中使用 "+"运算符
很容易就让人理解这是在完成 "数据相加" 的操作, + 的原始作用就是 "数据相加"
所以使用运算符重载最重要的目的之一: 便于代码的理解.
// 返回值类型 operator运算符(参数列表)
Data operator+(Data d1, int x)
{}
这样就完成了 "+" 运算符的重载.
返回一个 Data 类型的数据, 需要两个参数, 一个 Data 类型, 一个 int 类型数据. {} 中就是函数体
运算符重载都是针对自定义类型的
所以运算符重载一般都写在类内, 当作一个成员函数
class Data
{
public:Data(int year, int month, int day){_year = year;_month = month;_day = day;}bool operator==(const Data& d){return _year == d.year && _month == d.month && _day == d.day;}
private:int _year;int _month;int _day;
}int main()
{Data d1(2024, 11, 11);Data d2(2024, 11, 11);if(d1 == d2) // 调用了 d1的operator==, 所以隐藏的this指向的就是d1{cout << "两个日期相等" << endl;}
}
写在内类, 第一个类型参数就不用我们自己写了.
因为成员函数默认会又一个 this 指针. 代替了我们手动传递.
operator= 赋值重载
赋值重载也是C++的默认成员函数之一.
当我们不显示的写时, 编译器会自动生成一个.
自动生成的赋值运算符重载, 对于内置类型时直接复制,
对于自定义类型则会调用对应类型的赋值运算符重载函数.
赋值运算符的重载只能写在内类
如果显示的写在了类外
那么类内和类外的两个赋值重载就会冲突
class Data
{
public:Data(int year, int month, int day){_year = year;_month = month;_day = day;}bool operator==(const Data& d){return _year == d.year && _month == d.month && _day == d.day;}Data& operator=(const Data& d) // 赋值重载{if(*this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
}int main()
{Data d1(2024, 11, 11);Data d2 = d1; // "=" 运算符重载if(d1 == d2) // "==" 运算符重载{cout << "两个日期相等" << endl;}return 0;
}
this 是调用的对象的地址, 返回 *this 就是返回对象本身.
前置++和后置++
前置++ 和 后置++的参数相同, 返回值不同.
这是构不成重载的, 所以C++为了区分前置++ 和 后置++做了点特殊处理.
前置++:
Data& operator++();
后置++:
Data& operator++(int)
可以看到前置++没有什么改变.
在后置++ 中增加了一个参数.
后置++中的那个参数完全没有用处
只是为了区别前置++和后置++
class Data
{
public:Data& operator++() // 前置++{}Data& operator++(int) // 后置++{}
private:int _year;int _month;int _day;
}int main()
{Data d1(2024, 11, 11);d1++;++d1;return 0;
}
可以看到后置++多的那个参数不用关心, 只是为了区分前置++和后置++, 也无需为那个参数传参
五个不能重载的运算符
- "::"
- "."
- "?:"
- ".*"
- "sizeof"
这五个运算符是不能被重载的.