深入篇【C++】类与对象:拷贝构造函数详解


在这里插入图片描述

①.拷贝构造函数

Ⅰ.概念

在创建对象时,能否创建一个与已存在对象一模一样的新对象呢?

拷贝构造函数:只有单个形参,该形参是对本类型相同的对象的引用,一般是用const修饰,在用已存在的对象创建一个同类型的新对象时由编译器自动调用。

为什么要用const修饰?

防止将拷贝对象修改,我们只是要将拷贝对象,并不能将对象修改了。所以加上const来修饰拷贝的对象,防止错误修改。

Ⅱ.特征

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

1.重载形式之一

拷贝构造函数是构造函数的一个重载形式。
函数名字跟类名是一样的,只是参数列表不同

	Data(int year =2023, int month = 5, int day=4)//构造函数
	{
	 	_year = year;
		_month = month;
		_day = day;
	}
	//两个函数构成重载
	Data(const Data& d)//拷贝构造函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

2.参数唯一

拷贝构造的函数参数只有一个,理论上是两个的,一个是已存在的要被拷贝的对象,一个是新创建的要拷贝的对象,但新创建的对象传给了隐藏的this指针了。所以显示的只有一个参数。
在这里插入图片描述

3.形参必须传引用

构造函数的参数只有一个且必须是类类型对象的引用,如果使用传值方式编译器会直接报错,因为这样会引发无穷递归调用。

class Data
{

public:
	Data(int year =2023, int month = 5, int day=4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Data (const Data d)//这种形式是错误的,不可以这样写,不能传值过去
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Data(const Data& d)//正确的写法是这样,传引用过去
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year <<"-"<< _month <<"-"<< _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2023, 5, 3);
	Data d2(d1);
	d2.Print();

}

我们知道调用函数需要先传参,而对应内置类型,传参是直接以字节形式拷贝过去,但自定义类型就必须要用拷贝构造形式去完成。

也就是C++规定:在传参过程中
1.内置类型是直接拷贝过去的。
2.自定义类型必须调用拷贝构造完成拷贝。

所以对于自定义类型传参,如果使用传值形式,则传参就相当于又形参了一个新的拷贝构造函数,为什么呢?
因为采用传值形式的话,那么形参就是实参的一份拷贝。
将实参传过去,那么就必须调用一次拷贝构造函数形成形参。
所以如果传值过去,编译器会强制检查发现这样会引发无穷递归调用拷贝构造函数,然后报错。
在这里插入图片描述
所以形参必须给该类对象的引用。
当调用拷贝构造函数时,传参就不需要再调用拷贝函数了,因为传参使用的是引用传参,参数不是实参的一份临时拷贝,而就是实参本身,只不过是别名。
在这里插入图片描述
因为拷贝构造函数也是特殊的成员函数,是由编译器自动调用的,所以我们可以不去显示的去调用,编译器会帮我们调用,这是在拷贝构造函数已经写的情况下。

4.编译器的拷贝函数

如果没有显式的定义拷贝函数,那么编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数对象按照内存存储按照字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

默认生成的拷贝函数:
1.内置类型完成值拷贝/浅拷贝。
2.自定义类型会调用相对应的拷贝构造。

对于不需要申请动态资源的对象,浅拷贝就可以完成工作。

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& s)
	{
		_hour = s._hour;
		_minute = s._minute;
		_second = s._second;
		cout << "Time(const Time& s)" << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};
class Data
{

public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private://内置类型
	int _year=1;
	int _month=1;
	int _day=1;
	//自定义类型
	Time _t;
};
int main()
{
	Data d1;
	//用已存在的d1拷贝构造d2,这里会调用Data类的拷贝构造
	//但Data类没有显式的定义,所以编译器会生成一个默认的拷贝构造。
	//默认生成拷贝构造能否完成拷贝工作呢?
	Data d2(d1);

	d2.Print();
}

默认生成的拷贝构造是可以完成上面的拷贝任务的,因为默认的拷贝构造会对对象进行浅拷贝,而该场景就适合浅拷贝,因为没有动态资源的开辟,虽然Time定义的是自定义类型,但是浅拷贝可以完成任务就行了。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那还需要自己显式的写拷贝构造函数吗?当然对于Data日期类的是没有写的必要,但并不是每个类都是像日期类一样的。
当对象的拷贝需要深度拷贝时,就不能单单使用浅拷贝,这样会出问题的。
比如下面这个栈:

typedef int DataType;
struct stack//class可以定义一个类
{
public://访问限定符
	stack(int capacity = 4)//缺省值
	{
		cout << "stack(int capacipty=4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		_array[_size] = data;
		_size++;
	}
	void Pop()
	{
		if (Empty())
		{
			return;
		}
		--_size;

	}
	int Empty()
	{
		return _size == 0;
	}

	~stack()
	{
		cout << "~stack()" << endl;
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private://访问限定符
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	stack s1;//定义一个对象
	stack s2(s1);

根据调试可以发现d1初始化,然后将已存在的d1拷贝给新创建的d2。看起来都完美,但其实有一个致命的错误。
在这里插入图片描述
我们知道默认生成的拷贝函数是按照浅拷贝进行拷贝的,浅拷贝就是完全一样,全部复制过来。
这样是存在危险的,因为可能存在这样的情况:拷贝对象与被拷贝对象的成员指向了同一块空间。
在这里插入图片描述
这种情况会存在这样的问题:
1.同一块空间会析构两次,会报错。
当s2对象生命周期结束时,系统自动调用析构函数来清理数据,那么*a指向的空间就被销毁了。
而当s1对象生命周期结束时,又析构一次相同的空间,这样同一块空间就析构两次了。
在这里插入图片描述
编译器会出错的。

2.一个变量修改会影响另一个变量,因为两个变量都存在同一块空间里。

在这里插入图片描述

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

编译器默认生成的拷贝构造只能完成浅拷贝,而需要深度拷贝时还必须用我们自己写的拷贝构造函数。
其实自己写的拷贝构造函数就是为自定义类型的深度拷贝准备的。
所以总结一下,适合深度拷贝和浅拷贝的场景:

1.对于Data和MyQueue这样的类我们不需要自己写拷贝构造,因为浅拷贝就可以完成任务。
(MyQueue就是用栈来实现队列,而栈里面是要用自己写的拷贝函数,但实现队列时就不需要了)

class MyQueue
{
private:

	stack pushst;
	stack popst;
};

2.对于Stack这样的类,我们是需要自己写拷贝构造的,因为里面涉及要深度拷贝,有动态资源的开辟。

5.典型调用场景

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

class Data
{

public:
	Data(int year =2023, int month = 5, int day=4)
	{
	 	_year = year;
		_month = month;
		_day = day;
	}
	Data(const Data& d)//正确的写法是这样,传引用过去
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year <<"-"<< _month <<"-"<< _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
//典型调用场景:返回值是类类型对象,参数是类类型对象
Data Test(Data d)
{
	Data tmp(d);//用已存在的d(其实是d2)来创建新对象tmp
	return tmp;//返回对象tmp
}
int main()
{
	Data d1(2023, 5, 3);
	Data d2(d1);
	Data tmp=Test(d2);
	d2.Print();
}

在这里插入图片描述

在这里插入图片描述
注意:
为了提高此程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
就比如上面的Test函数,对象传参我们最好使用引用类型,那样就减少了一次调用拷贝函数的工作,提高了效率

Data Test(Data& d)
{
	Data tmp(d);//用已存在的d(其实是d2)来创建新对象tmp
	return tmp;//返回对象tmp
}

那能不能给返回值也使用引用呢?
答案是不能,要根据实际场景来对返回值使用引用,这样是对局部对象返回,不能使用引用,因为局部对象返回后,这个对象就销毁了,使用引用取别名那就对已经销毁的空间的非法访问了。所以不可以。

②.总结:

  • 1.拷贝构造函数是构造函数的一个重载形式。
  • 2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
    因为会引发无穷递归调用。
  • 3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  • 4.在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
    义类型是调用其拷贝构造函数完成拷贝的。
  • 5.类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
    时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
  • 6.拷贝构造函数典型调用场景:
    使用已存在对象创建新对象
    函数参数类型为类类型对象
    函数返回值类型为类类型对象
  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
设计并实现一个动态整型数组类Vect,要求: (1)实现构造函数重载,可以根据指定的元素个数动态创建初始为0的整型数组,或根据指定的内置整型数组动态创建整型数组。 (2)设计拷贝构造函数和析构函数,注意使用深拷贝。 (3)设计存取指定位置的数组元素的公有成员函数,并进行下标越界,若越界则输出“out of boundary”。 (4)设计获取数组元素个数的公有成员函数。 (5)设计用于输出数组元素的公有成员函数,元素之间以空格分隔,最后以换行符结束。 在main函数中按以下顺序操作: (1)根据内置的静态整型数组{1,2,3,4,5}构造数组对象v1,根据输入的整型数构造数组对象v2。 (2)调用Vect的成员函数依次输出v1和v2的所有元素。 (3)输入指定的下标及对应的整型数,设置数组对象v1的指定元素。 (4)根据数组对象v1拷贝构造数组对象v3。 (5)调用Vect的成员函数依次输出v1和v3的所有元素。 设计并实现一个动态整型数组类Vect,要求: (1)实现构造函数重载,可以根据指定的元素个数动态创建初始为0的整型数组,或根据指定的内置整型数组动态创建整型数组。 (2)设计拷贝构造函数和析构函数,注意使用深拷贝。 (3)设计存取指定位置的数组元素的公有成员函数,并进行下标越界,若越界则输出“out of boundary”。 (4)设计获取数组元素个数的公有成员函数。 (5)设计用于输出数组元素的公有成员函数,元素之间以空格分隔,最后以换行符结束。 在main函数中按以下顺序操作: (1)根据内置的静态整型数组{1,2,3,4,5}构造数组对象v1,根据输入的整型数构造数组对象v2。 (2)调用Vect的成员函数依次输出v1和v2的所有元素。 (3)输入指定的下标及对应的整型数,设置数组对象v1的指定元素。 (4)根据数组对象v1拷贝构造数组对象v3。 (5)调用Vect的成员函数依次输出v1和v3的所有元素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值