类与对象——赋值运算符重载

文章讲述了C++中的赋值运算符重载,包括自定义格式、防止循环赋值、以及未显式定义时的默认行为。着重介绍了Date类和MyQueue类在赋值操作中的应用,涉及深拷贝问题。

1、赋值运算符重载 ——格式

//(1)参数类型:const Date& ,引用传参可以提高传参效率。
//(2)返回值类型:Date& , 引用返回可以提高返回的效率,有返回值的目的是为了支持连续赋值
//(3)检测是否自己给自己赋值
//(4)返回 *this : 要复合连续赋值的含义。


#include <iostream>
using namespace std;

class Date
{
public:
	//构造函数会频繁调用,所以放在类里面定义作为 inline
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//赋值运算符重载
	//d3=d1;
	Date& operator=(const Date& d) //用引用传参,减少拷贝
	{
		if (this != &d) //防止自己给自己赋值
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
	
		return *this; //出了函数的作用域,*this并没有被销毁,因此可以用传引用返回。
	}

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

void TestDate1()
{
	Date d1(2022, 7, 24);
	//Date d2(d1); //拷贝构造,d2还未创建好

	Date d3(2022, 8, 24);
	d3 = d1;  //赋值运算符重载,d3已经创建好
	   //相当于:d3.operator=(&d3,d1);

	/*Date d4;
	d4 = d3 = d1;

	d4 = d4;*/
}

int main()
{
	TestDate1();
	return 0;
}

2、在类中未显式定义时,编译器自动生成的默认赋值运算符重载

//当我们没写赋值运算符重载时,编译会自动生成一个默认赋值运算符重载。(对于内置类型成员变量来说,会逐字节进行赋值;
//而对于自定义类型成员变量来说,需要调用它自己的赋值运算符重载完成赋值。)
//(这一点与拷贝构造一致)
//(1)不需要写的类型:A、日期类(都是内置类型成员变量);  B、My Queue类(自定义类型成员变量st1,st2;会去调用它们的赋值运算符重载)(前提条件是Stack类需要写赋值运算符重载) 
//(2)需要写的类:Stack类——因为内置类型成员变量: DateType* _array 逐字节赋值之后, 两个 _array指向同一个空间(造成两个栈操作同一个空间)
//  ( 对于Stack类而言,值拷贝无法实现,需要运用深拷贝去解决。)
//   Stack st1;
//   Stack st2;
//   st2.Push(1);
//   st2.Push(2);
//   st1 = st2;

3、MyQueue类的知识总结

// My Queue类 实现躺赢 —— 构造、拷贝构造、赋值重载以及析构 的默认生成都可以使用 {前提条件是 My Queue类的自定义类型成员变量的类Stack 
//  都写了构造、拷贝构造、赋值重载以及析构 函数}。
//  class MyQueue
//  {
//  private:
//	  Stack _st1;
//	  Stack _st2;
//  };
### C++赋值运算符重载的实现使用 #### 1. 赋值运算符重载的意义 在 C++ 中,默认情况下,编译器会为每一个提供一个默认的赋值运算符。这个默认的赋值运算符通过逐成员复制(member-wise copy)完成对象之间的赋值操作。然而,在某些情况下,这种简单的逐成员复制并不能满足需求,例如当中包含动态分配内存时,或者需要执行特定的资源管理逻辑时,就需要手动重载赋值运算符以确保正确性和安全性[^4]。 #### 2. 赋值运算符重载的基本形式 赋值运算符重载通常定义为的成员函数,并具有以下特点: - 它是一个成员函数; - 参数建议为 `const` 型的引用,避免不必要的拷贝; - 返回值型应为当前型的引用 (`T&`),以便支持链式赋值操作[^4]。 下面展示了一个典型的赋值运算符重载实现: ```cpp #include <iostream> #include <cstring> class String { public: // 构造函数 String(const char* str = nullptr) { if (str) { m_data = new char[strlen(str) + 1]; strcpy(m_data, str); } else { m_data = new char[1]; m_data[0] = '\0'; } } // 拷贝构造函数 String(const String& other) { m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); } // 赋值运算符重载 String& operator=(const String& other) { if (this != &other) { // 自我赋值检查 delete[] m_data; // 释放原有资源 m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); } return *this; // 支持链式赋值 } // 析构函数 ~String() { delete[] m_data; } // 输出字符串内容 void print() const { std::cout << m_data << std::endl; } private: char* m_data; }; int main() { String s1("Hello"); String s2("World"); s2 = s1; // 调用赋值运算符 s2.print(); // 输出 "Hello" String s3; s3 = s1 = s2; // 链式赋值 s3.print(); // 输出 "Hello" return 0; } ``` 在这个例子中,`String` 封装了一块动态分配的字符数组。为了防止浅拷贝带来的问题(如双倍删除),必须显式地重载赋值运算符。具体来说,赋值运算符实现了深拷贝逻辑:先释放原有的内存资源,再重新分配一块新的内存并将源对象的内容复制过去。 #### 3. 关键点解析 ##### (1)自我赋值检查 在上面的例子中可以看到这样的判断条件: ```cpp if (this != &other) { ... } ``` 这是为了避免自我赋值引发的问题。如果目标对象和源对象相同,则无需执行任何操作。否则可能会导致未定义行为,例如重复释放同一块内存区域。 ##### (2)返回引用 赋值运算符应该返回对当前对象的引用 (`return *this;`),这使得多个赋值表达式可以连在一起形成所谓的“链式赋值”。例如: ```cpp s3 = s1 = s2; ``` ##### (3)析构旧数据 在处理动态分配的数据结构时,务必记得销毁现有的资源后再进行新资源的初始化工作。这样能够有效预防内存泄漏或悬空指针等问题的发生。 #### 4. 默认赋值运算符的行为 如果没有特别指定,C++ 编译器会自动生成一个默认版本的赋值运算符。它的作用是对所有非静态成员逐一调用它们各自的赋值操作。对于基本数据型而言,这只是简单地把右侧数值赋予左侧;而对于复合型(即子对象),则递归触发其内部的赋值过程[^3]。 但是需要注意的是,这种方式仅适用于那些不需要特殊对待的情形——尤其是涉及到动态存储管理的时候往往不够安全可靠,因此有必要自行定制化解决方案。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值