您现在的位置是:首页 >技术杂谈 >【C++】类和对象——拷贝构造函数的概念、拷贝构造函数的特征网站首页技术杂谈

【C++】类和对象——拷贝构造函数的概念、拷贝构造函数的特征

鳄鱼麻薯球 2024-07-16 12:01:02
简介【C++】类和对象——拷贝构造函数的概念、拷贝构造函数的特征

1.拷贝构造函数

  在前面我们已经介绍了构造函数和析构函数的作用和使用方法,而拷贝构造函数则是在对象初始化时调用的一种特殊构造函数。拷贝构造函数可以帮助我们创建一个新的对象,该对象的值和另一个对象完全相同。拷贝构造函数是 C++ 中一个重要的概念,它可以用于复制一个对象,以便于在程序中进行各种操作。

在这里插入图片描述

1.1拷贝构造函数的概念

  拷贝构造函数是一种特殊类型的构造函数,用于创建一个对象的副本。

  其中“类名”为类的名称,“源对象”为被复制的对象的引用,它的原型为:

类名(const 类名& 源对象);

  拷贝构造函数有两种情况:

(1)如果没有显式地定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。 这个默认的拷贝构造函数会简单地复制每个数据成员的值。

(2)如果显式地定义了拷贝构造函数,编译器就不会生成默认的拷贝构造函数。 这时,程序员需要自己实现拷贝构造函数,以确保正确地复制对象的数据。

  在拷贝构造函数中,通常需要将源对象的数据成员的值复制到新对象中。特别地,如果类中包含指针成员,需要确保拷贝出来的对象不与源对象共享这些指针所指向的内存。

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

1.2拷贝构造函数的特征

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

(1)拷贝构造函数是构造函数的一个重载形式。

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

  我们定义了一个日期类Date,该类有一个带默认参数的构造函数和一个拷贝构造函数。默认构造函数用于创建一个指定年月日的日期对象,而拷贝构造函数则用于创建一个新的日期对象,并将其初始化为另一个已存在的日期对象的副本。

  实现拷贝构造函数的方式是将已存在的日期对象的年、月、日成员变量的值分别复制到新创建的日期对象的对应成员变量中。

  在main函数中,创建两个日期对象d1和d2,其中d2通过调用拷贝构造函数并传入d1对象的引用而被创建,并与d1对象完全相同。需要注意的是,如果拷贝构造函数的实现方式有误,比如像上面代码中,拷贝构造函数形参应该写成Date(const Date& d)而不是Date(const Date d),否则在编译和运行程序时会出现各种问题,包括编译器报错和程序无限递归等。

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;
}

编译器报错引发程序无限递归:
在这里插入图片描述

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

  我们定义了两个类Time和Date,其中Time类包含了三个int类型的数据成员_hour、_minute和_second,并分别对它们设置了默认初始值。该类还有一个拷贝构造函数,在创建一个新的时间对象时,通过将已存在的时间对象的_hour、_minute、_second成员变量的值分别复制到新创建的时间对象的对应成员变量中来初始化该新对象。

  Date类包含了与日期相关的_year、_month以及_day成员变量,并且还包含了一个_time对象,用于表示一个具体的时间点。

  在main函数中,创建了一个日期对象d1,其默认初始化值为1970年1月1日,同时_time对象的_hour、_minute、_second成员变量默认为1。接着,在创建新的日期对象d2时,通过调用拷贝构造函数并传入d1对象的引用来将d2对象初始化为d1对象的副本。需要注意的是,虽然Date类没有显式定义拷贝构造函数,但是编译器会自动为该类生成一个默认的拷贝构造函数,以保证对象能够正确地被复制。

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类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

(4)编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了。

// 这里会发现下面的程序会崩溃掉,这里就需要使用深拷贝去解决。
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;
}

  在main函数中,首先创建一个栈对象s1,并且向该对象中入栈了四个元素,然后再创建了一个新的栈对象s2,并将现有的栈对象s1作为参数传递进去,从而通过拷贝构造函数的调用将s2初始化为s1的副本。

  需要注意的是,由于Stack类定义的拷贝构造函数只是进行了浅拷贝,即仅仅将指针类型成员变量所指向的内存空间进行了复制,没有对该内存空间进行复制,这将会导致复制构造函数失效并出现内存相关的问题,比如程序退出时程序崩溃等。为了解决这个问题,可以使用深度拷贝来实现,确保新的对象和原对象各自独立占有自己的内存空间,而不是共享同一内存空间。

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

在这里插入图片描述

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

  1.使用已存在对象创建新对象

  2.函数参数类型为类类型对象

  3.函数返回值类型为类类型对象

  我们定义了一个日期类 Date,该类包含一个带3个参数的构造函数、一个拷贝构造函数和一个析构函数。

  在main函数中,我们首先创建一个名为d1的Date对象,通过调用带参数的构造函数,将其初始化为指定年月日为2022年1月13日。接着,我们调用了名为Test的函数,并将d1对象以值传递的形式传入。在函数内部,又通过调用Date类的拷贝构造函数,将传入的对象在内存中复制出一个新的对象temp。最后,函数返回值通过值传递的方式返回到main函数中。

  这里需要注意的是,因为传递参数的过程中涉及到复制构造函数的调用,所以在传参的过程中会调用Date类的拷贝构造函数,复制出一个新的临时对象。需要注意的是,在Test函数的执行过程中,函数内部所创建的临时对象temp在函数返回后也会被销毁。因此,在控制台的输出信息中可以看到,在Test函数中,先调用了拷贝构造函数,在函数结束时,临时对象temp被销毁,最后在main函数中,对象d1依旧存在,而且在程序结束时会调用Date的析构函数进行销毁。

class Date
{
public:
Date(int year, int minute, int day)
{
	cout << "Date(int,int,int):" << this << endl;
}	

Date(const Date& d)
{
	cout << "Date(const Date& d):" << this << endl;
}

~Date()
{
	cout << "~Date():" << this << endl;
}

private:
	int _year;
	int _month;
	int _day;
};

Date Test(Date d)
{
	Date temp(d);
	return temp;
}

int main()
{
	Date d1(2022,1,13);
	Test(d1);
	return 0;
}

  为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。