四、拷贝构造函数
4.1 概念
在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 函数名和类名相同,没有返回值。
- 可以使用函数法或赋值法调用拷贝构造函数。
- 拷贝构造函数的参数只有一个且必须是同类类型对象的引用,而且一般用const修饰以限制引用权限,防止误操作修改拷贝源的属性。
- 如果使用传值传参的方式编译器直接报错,因为会引发无穷递归调用。
实现日期类的拷贝构造函数
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(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);//使用d1拷贝构造d2//写法二:赋值法Date d2 = d1;return 0;
}
4.3 拷贝构造函数不能使用传值传参
传值传参的底层是在栈中开辟空间拷贝参数的值。如果拷贝构造函数的参数是同类类型对象的值,那么实例化形参就又要调用拷贝构造。这样就形成了死递归。
注意:类对象传值传参,传值返回都会调用拷贝构造函数构造临时对象(出作用域还要析构)。由此可以看出,传引用比传值更高效尤其对于自定义类型,传值不仅要开空间(尤其对于深拷贝)还要调用拷贝构造函数和析构函数(空间时间消耗)代价更大。
提示:还可以传同类型对象的指针实现拷贝的功能,但要注意的是使用指针实现的函数不是拷贝构造函数(不符合语法);同时指针实现的拷贝函数在使用起来效果也不如引用。如:
Date d2(&d1); Date d2 = &d1;
4.4 编译器自动生成的拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。 默认生成的拷贝构造函数对于内置类型是逐字节拷贝的(aka 浅拷贝 or 值拷贝),而自定义类型是调用其拷贝构造函数完成拷贝的。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数// 对于内置类型(_hour,_minute等)是按照字节方式直接拷贝的,而自定义类型(Time _t)是调用其拷贝构造函数完成拷贝的。Date d2(d1);return 0;
}
编译器自动生成的拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然,因为默认生成的拷贝构造函数不能解决深拷贝的问题。
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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 s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);//调用的是默认生成的拷贝构造函数,进行值拷贝return 0;
}
编译运行上面的代码发现程序崩掉了,程序为什么会崩溃掉呢?
-
注意:类中如果没有涉及资源申请时,拷贝构造函数写不写都可以,像Date类;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝,像Stack类。
-
此处默认的浅拷贝出现的问题:
- 由于两个对象中的指针指向同一块空间,一个对象修改会影响另外一个对象。
- 函数返回,调用析构函数时,对同一块内存空间free两次造成程序崩溃。
解决方法:自己显示实现深拷贝
再看下面这个例子:
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& st){//此处实现栈结构的深拷贝//........}//其他方法的实现....~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType *_array;size_t _size;size_t _capacity;
};class MyQueue{
//对于内置类型进行值拷贝
int sz = 0;
//对于自定义类型调用它们的拷贝构造
Stack output;
Stack input;
};int main()
{MyQueue mq1;MyQueue mq2 = mq1;// 像MyQueue类型这种未直接涉及资源申请的类可以不写拷贝构造,// 但前提是其自定义类型成员中涉及资源申请的类实现了深拷贝。return 0;
}
像MyQueue类型这种未直接涉及资源申请的类可以不写拷贝构造,但前提是其自定义类型成员中涉及资源申请的类实现了深拷贝。
总结:
- 涉及资源申请的类需要显示的写拷贝构造,以实现类的深拷贝。比如:Stack,Queue
- 未涉及资源申请的类不需要写拷贝构造,默认生成的就会完成类的值拷贝/浅拷贝。比如:Date
- 未直接涉及资源申请的类也不需要写拷贝构造,默认生成的就会调用其自定义类型成员的拷贝构造函数。比如:Myqueue