C++【默认成员函数(下)】
上节说到类的默认成员函数有6个,上节只谈了两个,这节来谈谈剩下的四个默认成员函数。
1、拷贝构造函数
在创建对象时可以通过拷贝构造函数创建一个与已存在对象一模一样的对象,拷贝构造函数是使用一个对象去初始化另一个对象。拷贝构造函数:只有单个形参,该形参是对本类类型的引用(一般使用const修饰),在用已存在的类类型对象创建新的对象时由编译器自己调用。
拷贝构造函数的特性:
1、拷贝构造函数是构造函数的一种重载形式。(所以定义形式是一样的)
2、形参有且仅有一个,必须是要拷贝的类类型对象的引用。使用传值方式编译器会直接报错,因为会引发无穷递归。为了保证正确传参,在形参位置上一般加上const修饰。
C++对于内置类型传参时,实参会直接拷贝给形参。而对于自定义类型进行传参时,实参要先通过自定义类型的拷贝构造函数完成拷贝。所以拷贝构造函数的形参如果写成传值方式的话,就会引发无穷递归,但是使用传引用传参就不会调用拷贝构造函数。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date d) // 错误写法:编译报错,会引发无穷递归
Date(const Date& d) // 正确写法
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
如果用户没有显示定义拷贝构造函数,编译器就会自动生成拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节字序进行拷贝,这种拷贝方式叫做值拷贝,也叫浅拷贝。编译器自动生成的拷贝构造函数会对内置类型完成值拷贝,对自定义类型调用自定义类型自己的拷贝构造函数。
如果要拷贝的类对象的成员变量中进行了动态的内存申请,此时如果只进行浅拷贝(值拷贝)就会出现一个问题:拷贝出来的对象并没有开辟新的空间,而仅仅是把要拷贝的值拿过来,这样就会导致两个对象中的成员变量指向同一块空间,在对象生命周期结束调用析构函数时就会发生错误,因为同一块内存空间不能被释放两次。其实,这种情况下不仅仅是析构函数会出问题,当一个对象对开辟空间内进行管理时,拷贝的对象也会对应的发生改变(核心就是两个对象中的成员变量指向同一块内存空间)
#include <iostream>
using namesapce std;
class Stack
{
public:
Stack(int defaultCapacity=4)
{
_arr = (int*)malloc(sizeof(int) * defaultCapacity);
if (nullptr == _arr)
{
perror("malloc fail");
return;
}
_capacity = defaultCapacity;
_top = 0;
}
//用户不写,编译器会自己生成(只进行浅拷贝)
//Stack(const Stack& obj)
//{
//
//}
void Push(int val)
{
if (_top == _capacity)
{
int* tmp = (int*)realloc(_arr,sizeof(int) * _capacity*2);
if (nullptr == tmp)
{
perror("realloc fail");
return;
}
_arr = tmp;
_capacity *= 2;
}
_arr[_top++] = val;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);//调用编译器自己生成的拷贝构造函数
s1.Push(1);
s1.Push(1);
s1.Push(1);
return 0;
}
就像上面的代码定义了 一个stack类用来实现栈的功能,顺序表要进行动态的内存空间开辟,假设用户没有显示的写拷贝构造函数,那么在Stack s2(s1);这句代码执行时就会调用编译器自己生成的拷贝构造函数,进行数值插入并进行调试,我们可以看到:
编译器生成的拷贝构造函数只进行了浅拷贝,导致对s1进行数据插入也会影响s2的问题。这里可以明显地看到s1和s2中的_arr指向的是同一块内存空间。 为了解决这一问题,就需要用户进行显示定义构造拷贝函数。
//用户自己显示定义(深拷贝)
Stack(const Stack& obj)
{
_arr = (int*)malloc(sizeof(int) * obj._capacity);
if (nullptr == _arr)
{
perror("copy malloc fail");
return;
}
memcpy(_arr, obj._arr, sizeof(int)*obj._top);
_top = obj._top;
_capacity = obj._capacity;
}
再来进行调试可以看到:
可以看到s1和s2指向两块不同的空间,在后续改变两者中任意一个对象都不会影响第二者。
总结:1、只有内置类型成员的类类型进行拷贝时,不用自己写拷贝构造函数。
2、编译器自动生成的拷贝函数只会进行浅拷贝,对于自定义类型成员会去调用他的拷贝构造函数。在自定义类类型中需要用户显示定义拷贝构造函数。
2、赋值运算符重载
2.1运算符重载
对于内置数据类型,编译器知道如何使用操作符。比如1和2的大小,逻辑上是固定的,只要在编译器内设置好逻辑,编辑器就能直接进行判断大小。而对于用户自定义的数据类型,编译器事先不知道设计该类型的逻辑,就无法使用具体的操作符了。这时候需要用户在自定义类中对运算符进行重载,也就是告诉编译器应该怎么去使用怎么去判断,那么编译器也就知道怎么去判断了。
运算符重载是具有特殊函数名称的函数,也具有返回类型,函数名字和参数列表,其返回值类型和参数列表与普通的函数类似。函数名字为:关键字operator后面接操作符符号。
函数原型: 返回值类型 operator 操作符(参数列表)
注意:1、 不能随意通过连接其他符号来创建新的操作符:比如operator@
2、重载操作符必须有一个类类型参数
3、用于内置类型的运算符,其含义不能改变。例如内置类型+ 不能让它变成-的意思。
4、作为类成员函数时,其形参看起来比数目少一,因为成员函数的第一个参数为隐藏的this.
5、 .* :: sizeof ?: . 以上五个运算符不能被重载。
这里举一个例子:
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
bool Date::operator<(const Date& obj)
{
if (_year < obj._year)
{
return true;
}
else if (_year == obj._year && _month < obj._month)
{
return true;
}
else if (_year == obj._year && _month == obj._month && _day < obj._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2020,12,01);
Date d2(2024,12,24);
cout << (d1<d2) << endl;
reutrn 0;
}
上面编译器就可以进行Date类的小于判断了。
2.2、赋值运算符重载
用户没有进行显示定义赋值运算符函数时,编译器会自动生成一个默认赋值运算符重载函数,以值得方式进行字节拷贝。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载函数。
赋值运算符函数必须定义为成员函数,因为涉及到this指针的生命周期。
赋值运算符重载(主要是对已经存在的两个对象之间进行赋值拷贝)。
赋值运算符的重载格式:
参数类型: const T&, 传引用可以提高传参效率。
返回值类型:T&,返回引用可以提高传参效率,有返回的目的是支持连续赋值。
检测是否自己给自己赋值。
返回*this :要复合连续赋值的含义。
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//用一个已经存在的对象去初始化另一个刚创建的对象
Date(const Date& obj)
{
_year = obj._year;
_month = obj._month;
_day = obj._day;
}
//对已经存在的两个对象进行拷贝
Date& operator=(const Date& obj)
{
_year = obj._year;
_month = obj._month;
_day = obj._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2008, 10, 1);
Date d2;
d2 = d1;//调用赋值操作符重载
Date d3 = d1;//调用构造拷贝函数
return 0;
}
总结:1、一些涉及到资源管理,就必须用户自己实现赋值操作符重载。
2、默认生成的赋值运算符重载函数和默认拷贝构造函数的行为一致。对于内置类型成员变量进行值拷贝,对于自定义类型成员会去调用该自定义类类型自己的赋值重载函数。