1.拷贝函数的概念
像名字一样,我们在生活中会拷贝一个完全一样的对象,像这种操作我们怎么执行呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
2.拷贝函数的特征
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。如下图,这就是一个简单的显示写出拷贝构造的一段代码。为什么会引发无穷递归?在逻辑上,我们调用Date d3=d1这样的代码我们会去找拷贝构造函数,如果我们没有取别名,他就会在函数中一直循环调用拷贝构造函数(Date(Date d))就像这样的代码Date调用完拷贝构造里面的参数接着调用,一直循环。所以我们用别名来解决这个问题。
-
#include<iostream> using namespace std; class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year = 1; int _month = 1; int _day = 1; }; int main() { Date d1(2024,4,20); d1.Print(); Date d3 = d1; Date d3(d1);//两种形式等价 d3.Print(); return 0; }
如上代码我们会发现我们不去显示调用拷贝构造编译器会默认生成一个拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
-
既然编译器自己就会给我们生成默认拷贝构造函数,那我们显示写出拷贝构造函数还有意义么?对于这种日期类当然没有,那其他的呢?我们会发现的代码会崩掉?为什么呢?
#include<iostream> using namespace std; typedef int DateType; class Stack { public: Stack(int capacity=10) { _array =(DateType*) malloc(sizeof(DateType) * capacity); if (_array == NULL) { perror("malloc fail"); } _capacity = capacity; _size = 0; } ~Stack() { if (_array) { free(_array); _size = 0; _capacity = 0; _array = NULL; } } void push(DateType& x) { _array[_size++] = x; } private: int _size; int _capacity; DateType* _array; }; int main() { Stack S1; S.push(1); S.push(2); S.push(3); S.push(4); Stack S2 = S1; return 0; }
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
-
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
3.赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
格式是返回类型 operater 后面跟上要从在的运算符 参数列表
ps:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
下面是一个基本的运算符重载:
大家可以看到清晰的报错,为什么呢?因为我们的成员变量是私有的,外部是无法访问的,但是这又引出了问题,如果我们把成员变量置为公有,封装性又无法保证,所以我们干脆把运算符重载放在类的内部,然后我们的左参数就不用显示写了,因为是隐含的this指针。(后面我们还会学到友元的方法。)
赋值运算符重载
格式:
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
这便是我们日期类的一个简单的赋值运算符重载,在《C++ prime》一书中还有这样的要求:
我们看下图代码
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 =”必须是非静态成员
我们把赋值运算符重载写在了全局范围内,并且按照全局的要求加以改变,但是我们的编译器会报错,原因是:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。