C++入门-----拷贝构造

学习目标

    • 1. 拷贝构造函数的概念及使用
    • 2. 特征
    • 3. 注意的点
      • 3.1 防止无穷递归
      • 3.2 防止原对象被修改
    • 4. 默认生成拷贝构造
      • 4.1 浅拷贝
      • 4.2 为什么要自己实现拷贝构造函数
      • 4.3 其对于内置类型和自定义类型的处理方式
    • 5. 总结

1. 拷贝构造函数的概念及使用

只有单个形参,该形参是对本类的类型对象的引用(一般常用const修饰),当已存在的类类型对象创建新对象时由编译器自动调用。

用下列代码来解释这句话

class Date
{
public:
  //这是默认构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//这是拷贝构造函数
	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;
}
  1. 只有单个形参,例如:
Date(const Date& d)
  1. 该形参是对本类的类型对象的引用,d就是d1的引用

  2. 当已存在的类类型对象创建新对象时由编译器自动调用
    -------d1是已存在的Date类类型,它想要给新对象d2初始化。

2. 特征

对象在调用函数的时候就是根据参数来找到拷贝构造函数的,类里面有默认构造函数和拷贝构造函数同时存在,所以当用户用一个同类型对象初始化另一个对象,那就是拷贝构造。(用d1初始化d2)

拷贝构造函数默认构造函数一样是特殊的成员函数,它们构成函数重载。(都是类名,形参不同)

3. 注意的点

3.1 防止无穷递归

  • 疑问

都知道拷贝构造函数的形参格式是对象的引用,那么为什么要这样做?

因为不采用对象的引用会产生无穷递归,观察下列代码:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{//内容省略}
	Date(const Date d)//不采用对象的引用
	{//内容省略}
private:
//成员省略
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

上述代码没有采用成员的引用,那么在d2调用拷贝构造函数,想用d1给自己进行初始化时,满足了调用拷贝构造函数的条件,就会有这样的现象:

C++入门-----拷贝构造_第1张图片

  • 结论

因此形参采用对象引用是必须的,采用了以后d就相当于d1的别名,就没有实参拷贝给形参那种说法了,直接就是它本身。

3.2 防止原对象被修改

  • 疑问

那为什么要有const?

上面的例子讲到:形参d采用引用接收d1后,直接就是d1本身,那么对d的成员修改其实就是修改d1的成员,但我们如果不想看到这种事情发生,就要用一定的规则去限制它不要乱动我原来的成员。-------那就是const指针。

4. 默认生成拷贝构造

4.1 浅拷贝

若未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

  1. 内置类型成员,会按照字节序一一拷贝。

如果把拷贝构造函数注释掉以后:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{//内容省略}
	//Date(const Date d)//不采用对象的引用
	//{//内容省略}
private:
//成员省略
};
int main()
{
	Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,
//则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}
  • 结论

未显式定义时,编译器会调用系统生成的默认拷贝构造函数,并且成功将d1的成员拷贝到d2。(按字节序)

4.2 为什么要自己实现拷贝构造函数

  • 疑问

那既然有默认的,何必自己实现?

拷贝构造函数典型调用场景:

  1. 使用已存在对象创建新对象
  2. 函数参数类型为类类型对象
  3. 函数返回值类型为类类型对象

再看下面的代码:

// 这里会发现下面的程序会崩溃掉
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);
	Stack s2(s1);
return 0;

这里其实就是实现了一个栈,然后想把s1的内容拷贝给s2。

但是这里的实现方式和Date类不同,因为Date类并没有在堆上开辟空间malloc,也就意味着没有需要析构的内容;而Stack类有,所以就会造成这样的情况:
C++入门-----拷贝构造_第2张图片

上述图片的补充:

  1. s2对象使用s1进行了拷贝构造,而Stack类没有显式定义拷贝构造函数,则编译器会给Stack类生成一份默认拷贝构造函数,按照字节序原封不动地将s1的值拷贝到s2对象中,所以它们会指向同一块内存空间。
  2. 程序退出的时候,s2先进行空间释放,调用析构函数,s1并不知道s2已经把那块空间释放了,一块空间造成了多次释放,程序必然崩溃。

由此:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

4.3 其对于内置类型和自定义类型的处理方式

  • 结论

我们如果不写拷贝构造,默认生成的拷贝构造函数会对内置类型和自定义类型都进行拷贝处理,自定义类型是去调用自己类里面实现的,如果没有就会在自己类里再生成一个默认的,与上面同理。但是与构造和析构是不一样的处理方式。

5. 总结

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式;
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  3. 在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
  4. 为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

你可能感兴趣的:(C++,c++,开发语言,算法)