【C++】类和对象③(类的默认成员函数:拷贝构造函数 | 赋值运算符重载)

🔥个人主页:Forcible Bug Maker

🔥专栏:C++

目录

前言

拷贝构造函数

概念

拷贝构造函数的特性及用法

赋值运算符重载

运算符重载

赋值运算符重载

结语


前言

本篇主要内容:类的6个默认成员函数中的拷贝构造函数赋值运算符重载

在上篇文章中我们讲到了类的默认成员函数的构造函数和析构函数,这两个默认成员函数在对象的生命周期中起着至关重要的作用。而今天我们要讲的拷贝构造函数和赋值运算符重载,作为类默认成员函数的其中之二,则是在对象间的初始化和拷贝当中起着重要作用。再次强六个默认成员函数的共性,这些函数会在你不提供的情况下由编译器自动生成。接下来开始我们今天的内容。

拷贝构造函数

概念

在创建对象时,你可能需要创建一个与已经存在的对象一模一样的新对象。

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

拷贝构造函数的特性及用法

拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。

1. 拷贝构造函数是构造函数的一个重载形式

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

拿一个Date类来举例:

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

在 Date d2(d1); 这句,将调用拷贝构造函数,其中d1通过传引用传参传递给函数中的d,d2以this指针的形式隐式传递。在运行完拷贝构造函数之后,d2创建好就是和d1相同的一个对象了。接下来详细讲解为什么构造中不加应用会导致无穷调用。

void Fun1(Date d)
{
	cout << "Fun1" << endl;
}

int main()
{
	Date d1(2024, 4, 16);
	Fun1(d1);
	return 0;
}

运行以上代码,会发现,在进入函数Fun1之前,会先调用一次拷贝构造,将d1的值赋给d,下面是调试观察,可以看到调试进入Fun1前先进入了拷贝构造。

说明类的传值传递在调用时会先调用类内部的拷贝构造,如果不写拷贝构造中的引用,拷贝构造的传应用传递就会变成类的传值传递,而类的传值传递又需要先调用拷贝构造,最终逻辑形成了一个闭环,导致无穷调用。

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

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

上述代码中Date的拷贝构造函数我们并没有提供,由编译器默认生成。 

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

4. 编译器生成的默认拷贝构造函数既然已经可以完成字节序的拷贝,那么是否还有自己实现的必要?像日期类这样的类是没什么必要的,但是当类涉及资源的打开和关闭,开辟和释放时,默认生成的浅拷贝构造就无法解决问题了。

看如下的Stack类:

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

在Stack类中,我们在创建s2时使用了编译器默认生成的拷贝构造函数,在main函数代码运行的过程中,似乎并没有什么问题,但是当整个程序运行结束时,程序崩溃了。这是因为Stack类涉及到了堆中空间资源的开辟,由于编译器默认生成的拷贝构造是浅拷贝,s1和s2中的_array指针指向相同的堆空间,在程序运行到结尾会调用析构函数,s1和s2对象各调用一次析构时,会导致_array指向的堆空间被释放两次,最终程序崩溃。

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

5. 拷贝构造函数典型应用场景

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

代码示例:

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

为了提高效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能引用尽量使用引用。

6. 拷贝构造函数其实还有一种调用方式

类名  对象名 = 已有对象名

赋值运算符重载

运算符重载

运算符重载是C++中的一个重要特性,它允许我们为自定义的数据类型(如类)重新定义或重载已有的运算符,以便它们能像内置类型(如int、float等)的运算符那样工作。通过运算符重载,我们可以让自定义类型的对象像内置类型一样进行运算和操作提高代码的可读性和易用性
函数名字为:关键字operator后面接重载的运算符符号。
函数原型:返回值类型  operator操作符(参数列表)

简单来说,运算符重载就是给运算符“赋予新的意义”,让它在不同的数据类型上有不同的作用

重载的基本规则:

  1. 不能通过连接其他符号来创建新的操作符,如:operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,如,内置类型的+,不能改变含义
  4. 最为类成员函数重载时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this
  5.   .*  ::  sizeof  ?:  .  这五个运算符不能重载。 

上面代码中重载了一个全局的operator==,但是全局要求成员变量是公有的,这就牵扯到一个问题,如何保证封装性?

解决方式有三:

  1. 提供可以获取到成员变量的成员函数
  2. 使用友元(后面将会学习)
  3. 重载成成员函数
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

上面的代码就成功将==操作符重载成了成员函数,在使用时与全局的==并无区别。

赋值运算符重载

经过上面对操作符重载的简单介绍,赋值重载简单说就是字面意思,重载了=这一符号,使其可以用于对象之间的相互赋值。

1. 赋值运算符重载格式

  • 参数类型:const T&,传引用提高效率
  • 返回值类型:T& 返回引用可以提高返回效率,有返回值的目的是为了支持连续赋值
  • 检测是否给自己赋值
  • 返回*this:要符合连续赋值的含义

代码示例(完整的赋值运算符重载):

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;
	}
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。虽然编译器生成的默认赋值运算符可以完成字节序的值拷贝,但一旦涉及到资源的管理时,编译器生成的依然是不够用的,原因和默认生成的拷贝构造函数类型。

故:如果类中未涉及到资源管理,赋值运算符是否实现都可以一旦涉及到资源管理则必须要实现

再次对拷贝构造和赋值运算符重载做一个区分,两个函数虽然都可以用 = 拷贝复制现有的一个对象,但使用等于时的情形是完全不同的。以日期类为例:

//拷贝构造
Date d2 = d1;

//复制运算符重载
Date d2;
d2 = d1;

拷贝构造:一个已经存在的对象,拷贝给另一个要创建初始化的对象。

赋值运算符重载(复制拷贝):一个已经存在的对象,拷贝给另一个已经存在的对象。

结语

本篇博客主要讲了拷贝构造函数和赋值运算符重载,它们在类中扮演着至关重要的角色,是对象复制和赋值操作的基础,确保对象在复制和赋值过程中保持正确的状态和行为。如果没有正确地实现这两个函数,可能会导致数据不一致、内存泄漏或其他严重问题。因此,在编写自定义类时,通常需要仔细考虑是否需要显式定义拷贝构造函数和赋值运算符重载,并根据类的具体需求来实现它们。对于某些类(如包含动态分配内存的类),显式定义这两个函数是必不可少的。下篇博客将会讲到最后两个类的默认成员函数,以及操作符重载更多的使用情境。

本篇博客到此结束,感谢大家的支持!♥

  • 110
    点赞
  • 81
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 97
    评论
C++中,复制构造函数和拷贝构造函数是指同一个概念,用于创建一个对象的副本。它们被定义为的特殊成员函数,用于将一个对象的值复制到另一个对象中。 复制构造函数/拷贝构造函数的语法如下: ```cpp ClassName(const ClassName& obj) { // 复制obj的成员变量到新对象中 } ``` 其中,`ClassName`是的名称,`obj`是同对象的引用,用于初始化新创建的对象。 在使用复制构造函数时,编译器会自动调用它来创建一个对象的副本。例如: ```cpp ClassName obj1; // 创建一个对象obj1 ClassName obj2(obj1); // 使用obj1调用复制构造函数创建obj2,obj2是obj1的副本 ``` 需要注意的是,如果没有显式定义复制构造函数/拷贝构造函数,编译器会为提供一个默认的复制构造函数,该构造函数会逐个复制的成员变量。但是如果中有指针成员变量或资源管理等特殊情况,则需要自定义复制构造函数来确保正确地复制对象。 同时,复制构造函数/拷贝构造函数也可以通过赋值运算符重载来实现对象的复制。例如: ```cpp ClassName obj1; // 创建一个对象obj1 ClassName obj2 = obj1; // 使用赋值运算符重载实现对象的复制 ``` 这里的赋值运算符重载函数会被编译器解析为复制构造函数/拷贝构造函数的调用。 总结:复制构造函数/拷贝构造函数是用于创建对象的副本的特殊构造函数,它们采用同对象的引用作为参数,并使用该对象的值来初始化新创建的对象。如果未显式定义复制构造函数/拷贝构造函数,编译器会提供一个默认的复制构造函数/拷贝构造函数

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Forcible Bug Maker

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

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

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

打赏作者

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

抵扣说明:

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

余额充值