c++赋值运算符=重载

本文深入探讨了C++中赋值运算符=的重载机制,特别是针对包含指针成员变量的类如何正确实现深拷贝,避免内存泄漏等问题。通过对比浅拷贝与深拷贝的区别,提供了具体的实现代码示例。

编译器会自动为类构造四个函数
1.无参构造函数(无参且函数体是空的)
2.析构函数(无参,且函数体是空的)
3.默认拷贝构造函数,对属性进行值拷贝
4.赋值运算符的重载 operator=, 对属性进行值拷贝(也就是说,在我们实际重载之前,编译器以及写过一个值拷贝的构造函数了)
我们先来了解一下编译器自动生成的赋值=重载函数是什么样,看下面的cpp

class person {
public:
	int m_A;
	person(const int &a) { 
		m_A = a;
	}
	//下面那三行就是编译器自动生成的一个重载函数,可写可不写,最终结果都一样。
	//person& operator=(person& p) {
		//m_A = p.m_A; //仅仅是简单的值拷贝
		//return *this;  }
};

int main() {
	person p1(10);
	person p2(20);
	p2 = p1;
	cout << p2.m_A << endl;
}

我们不难看出,如果仅仅只是简单的类的赋值,编译器自动生成的完全够用。 但是你想一想,如果涉及到指针形式,只进行简单的值拷贝就会出现内存重复释放的问题,这就会爆断点,是个很可怕的事情,所以我们学习怎么在类中给赋值运算符=进行深拷贝重载相当必要。

下面写一个问题代码,我们看看指针形式的时候会怎么出错。

#include<iostream>
using namespace std;
class person {
public:
	int *m_A;
	person(int a) {
		m_A = new int(a);
	}
	person& operator=(person& p) {
		m_A = p.m_A;  //这个构造函数只是简单的把传入的p的m_A的指针赋值给了当前对象的m_A;我们假设传入的那个对象的m_A地址是0x1111
	    return *this;  //那么经过简单的赋值拷贝后当前对象的m_A地址也是0x1111,而不是新地址,这就埋下了祸根
	}
	~person() {
		if (m_A != NULL) {  //祸根开始显现
			delete m_A;   //两个类的对象,因此析构函数会调用两次,第一个调用的时候先判断,指针不为空,指针指向的内容被释放,
			m_A = NULL;   //第二次再调用,我们再判断当前对象的m_A是否为空(注意,一定是当前对象,而不是上一次调用的对象),发现也不为空,这时候我们就要去释放那个地址指向的内存了,但是编译器发现,那块内存已经被释放掉了,这时问题就爆出来了,就是内存重复释放。
		}
	}
};

int main() {
	person p1(10);
	person p2(20);
	p2 = p1;
}

在这里插入图片描述

下面给出正确的例子:

#include<iostream>
using namespace std;
class person {
public:
	int* m_A;//定义了一个int类型的指针,之后会在构造函数中利用new向堆区申请一块内存存放传进来的数据
	person(int a) {
		m_A = new int(a);
	}
	person & operator=(person &p) {
		if (m_A != NULL) {
			delete m_A;
			m_A = NULL;
	 }
	//m_A = p.m_A;编译器提供的浅拷贝书写格式
    //浅拷贝就是把地址简单复制
		m_A = new int(*p.m_A);
    //这个才是深拷贝,把复制那个地址中的内容,再在堆区申请一份内存把这个内容存进去得到一个新地址。
		return *this;//返回当前对象
	}
	~person() {
		if (m_A != NULL) {
			delete m_A;
			m_A = NULL;
		}

	}

};
int main() {
	person p1(10);
	person p2(20);
	p2 = p1;
	cout << *p2.m_A << endl;
}
//使用这个判断语句的目的是使代码更加健壮,好习惯,涉及到指针一定要想着判断指针是否为空
if (m_A != NULL) {
			delete m_A;
			m_A = NULL;
	 }

=运算符重载必须依靠类的成员函数来实现:
赋值运算符=重载:必须依托类的成员函数来实现,不能用全局函数来实现
赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数

在这里插入图片描述
原因:
其实,之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。

  在前面的讲述中我们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?

   为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。
### 重载赋值运算符 `=` 的方法与实现 在 C++ 中,赋值运算符 `=` 的重载是实现自定义类行为的重要组成部分。默认情况下,编译器会为每个类自动生成一个赋值运算符,该运算符对类的所有数据成员执行逐成员的浅拷贝。然而,在涉及资源管理(如动态内存、文件句柄等)时,必须显式重载赋值运算符以避免资源泄漏或重复释放等问题。 #### 基本格式 赋值运算符重载函数的格式包括以下几个关键部分: - **返回值类型**:通常是类类型的引用(`类名&`),以支持链式赋值操作。 - **函数名**:使用 `operator=` 表示重载赋值运算符。 - **参数列表**:通常是一个常量引用类型的参数(`const 类名&`),以避免不必要的拷贝。 - **返回值**:应返回当前对象的引用(`*this`)。 示例代码如下: ```cpp class BGPart { public: int s; BGPart& operator=(const BGPart& p) { if (this != &p) { s = p.s * 10; } return *this; } }; ``` 此实现中,首先检查是否是自赋值(即对象赋值给自己),以避免不必要的操作;然后执行实际的赋值逻辑;最后返回当前对象的引用以支持链式赋值[^4]。 #### 支持多参数赋值 虽然赋值运算符通常只接受一个参数(即另一个对象的引用),但也可以接受额外的参数,前提是这些参数必须具有默认值。例如: ```cpp Array& operator=(const Array& arr, int a = 100); ``` 这种方式允许在赋值过程中传递额外的控制参数,但并不常见,主要用于特定的扩展需求[^1]。 #### 避免自赋值问题 在实现赋值运算符时,必须处理自赋值的情况。如果不进行判断,直接对对象自身的资源进行释放和重新分配,可能导致未定义行为。因此,应在函数开头加入类似 `if (this != &p)` 的判断[^4]。 #### 返回类型的重要性 返回类型为类的引用(`类名&`)是为了支持连续赋值,例如 `a = b = c;`。若返回类型为 `void` 或者返回一个临时对象,则无法支持这种链式操作。因此,返回 `*this` 是标准做法[^2]。 #### 默认赋值运算符的行为 如果程序没有显式提供赋值运算符重载函数,编译器会自动生成一个默认版本。该默认实现会对每个成员变量进行逐个赋值。在大多数情况下,这种默认行为是合适的,除非类中包含指针或需要深拷贝的资源[^3]。 --- ### 示例代码 以下是一个完整的示例,展示了如何实现一个带有资源管理的类的赋值运算符: ```cpp #include <iostream> using namespace std; class MyString { private: char* data; public: MyString(const char* str = "") { data = new char[strlen(str) + 1]; strcpy(data, str); } ~MyString() { delete[] data; } MyString(const MyString& other) { data = new char[strlen(other.data) + 1]; strcpy(data, other.data); } MyString& operator=(const MyString& other) { if (this != &other) { char* newData = new char[strlen(other.data) + 1]; strcpy(newData, other.data); delete[] data; data = newData; } return *this; } void print() const { cout << data << endl; } }; int main() { MyString s1("Hello"); MyString s2("World"); s2 = s1; s2.print(); // 输出: Hello } ``` 此示例中,`MyString` 类管理一个动态分配的字符数组。赋值运算符重载函数确保在赋值时进行深拷贝,避免了指针共享的问题。 --- ### 注意事项 - **自赋值检查**:所有赋值运算符实现都应包含 `if (this != &other)` 以避免自赋值带来的问题。 - **资源释放顺序**:在重新分配资源之前,先创建新资源,再释放旧资源,以确保异常安全。 - **返回类型**:必须返回当前对象的引用,以支持链式赋值。 - **默认行为**:仅当默认赋值行为不满足需求时才需要手动重载。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ad_m1n

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

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

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

打赏作者

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

抵扣说明:

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

余额充值