😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
⏰发布时间⏰:2024-06-14 12:55:32
本文未经允许,不得转发!!!
🎄一、概述
上篇文章介绍了C++11的 移动构造函数,在清楚了 移动构造函数 之后,应该知道了移动语义、右值引用了,接下来只要了解移动赋值运算符的调用时机,什么情况要使用,怎样声明、定义移动赋值运算符即可。本文主要介绍这些知识,但为了文章的完整性,还是需要将移动语义、右值引用介绍一次,了解的可以直接从第二小节开始看。
移动构造函数、移动赋值函数都是C++11增加的,了解这两个函数之前需要先弄清楚 移动语义 和 右值引用。
✨1.1 移动语义
移动语义是C++11才实现的,它的意义是对于一些即将被销毁的对象,可以不复制它,而是窃取它的资源,以此来减少一次复制,达到大幅度提升性能的效果。
在C++11标准之前,没有直接的方法移动对象,即使某些情况不需要拷贝对象,也不得不拷贝。例如,函数返回非引用的返回值时,会先构造一个临时对象作为返回值,函数调用时将该临时对象赋值给接收对象,调用结束后会销毁该临时对象。这里的临时对象是肯定会被销毁的,如果能够将临时对象的资源直接移交给接收对象,那么就可以减少一次拷贝。旧标准则只能拷贝,然后再销毁临时对象的资源。
C++11支持将即将被销毁的对象移动到接收对象,而不是复制。如下图,临时对象的资源被移动到接收对象,减少一次复制。
下面代码演示了拷贝赋值运算符函数的过程,等学习了下面的移动赋值运算符就可以编写移动的代码了。
// g++ 15_Copy_Date.cpp -std=c++11
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
#define MAX_NEW_MEM (64*1000*1000)
class CDate
{
public:
CDate(int year, int mon, int day); // 构造函数声明
CDate(const CDate& date); // 拷贝构造函数声明
~CDate(); // 析构函数声明
CDate operator+(int day); // 加号运算符声明
CDate& operator=(const CDate &date);// 拷贝赋值运算符声明
void show()
{
cout << "Date: " << m_year << "." << m_mon << "." << m_day << ", this=" << this << endl;
//cout << "Date: " << str << endl;
}
private:
int m_year;
int m_mon;
int m_day;
char *str;
};
// 构造函数定义
CDate::CDate(int year, int mon, int day)
{
m_year = year;
m_mon = mon;
m_day = day;
str = new char[MAX_NEW_MEM];
sprintf(str, "%4d.%02d.%02d", year,mon,day);
cout << "Calling Constructor" << ", this=" << this <<endl;
}
// 拷贝构造函数定义
CDate::CDate(const CDate& date)
{
m_year = date.m_year;
m_mon = date.m_mon;
m_day = date.m_day;
str = new char[MAX_NEW_MEM];
memcpy(str, date.str, MAX_NEW_MEM);
cout << "Calling Copy Constructor" << ", this=" << this << ", Copy Data" <<endl;
}
// 析构函数定义
CDate::~CDate()
{
cout << "Calling Destructor" << ", this=" << this <<endl;
delete [] str;
}
// 加号运算符函数定义
CDate CDate::operator+(int day)
{
CDate temp = *this;
temp.m_day += day;
cout << "Calling operator+" << ", this=" << &temp << endl;
return temp;
}
// 赋值运算符函数定义
CDate& CDate::operator=(const CDate& date)
{
if(this == &date) // 赋值给自身
return *this;
delete [] str; // 释放旧的数据
m_year = date.m_year;
m_mon = date.m_mon;
m_day = date.m_day;
str = new char[64];
sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
cout << "Calling operator=" << ", this=" << this << ", Copy Data" <<endl;
return *this;
}
int main()
{
CDate date(2024,06,14);
CDate date1(2024,06,14);
cout << endl;
date1 = std::move(date+1);// std::move 强制将 date+1 的求值结果转为右值
date1.show();
cout << endl;
return 0;
}
运行结果如下:
1、std::move 强制将 date+1 的求值结果转为右值,避免编译器优化;
2、temp 对象赋值给date1之后,就销毁了,如果可以直接将temp对象的资源给到date1,就可以减少一次复制。
✨1.2 右值引用
前面介绍了C++11的移动语义,那这个移动语义是怎样实现的呢?
C++11标准引入的一种新的引用类型——右值引用,来实现移动操作。右值引用只能关联到右值,而右值要么是字面值,要么是求值过程中产生的临时对象。右值引用的定义使用了&&
,可以参考下面代码:
int &&rri = 13; // 定义了右值引用 rri
上个小节的临时对象只会在程序中很短暂的停留,使用完之后会被立即销毁。C++11标准通过 右值引用 来完全地接管这些临时对象,然后将临时对象的资源 移动 到接收对象,并且要保证这个临时对象可以被正常销毁。这就是C++11标准实现移动语义的原理。
关于左值、右值、右值引用还有疑惑的,可以参考这篇文章:【C++ | 左值、右值】一文了解C++的左值、右值、左值引用(&)、右值引用(&&)
🎄二、移动赋值运算符(move assignment operator)
移动赋值运算符(move assignment operator),是C++11标准增加的在给对象赋值时移动旧对象资源的成员运算符函数。
类的移动赋值运算符原型通常是这样的:类类型& operator=(类类型 &&);
。以CDate
类为例,其移动赋值运算符函数声明如下:
CDate& operator=(CDate &&date); // 移动赋值运算符声明
移动赋值运算符的几个特点:
1、函数名为operator=
,返回值为该类类型的引用,因为需要返回当前对象,以支持连续赋值a=b=c;
2、第一个参数必须是一个自身类类型的右值引用(&&),且其他参数都有默认值。
3、第一个参数不能声明为 const 右值引用的原因是该引用在函数内会被修改(移动资源)。
4、移动赋值运算符执行后,需要保证右值引用的对象能够被正常销毁。
下面看看怎样声明定义自己的移动赋值运算符:
CDate(CDate &&date) noexcept; // 声明
CDate& CDate::operator=(CDate &&date) noexcept
{
if(this == &date) // 赋值给自身
return *this;
delete [] str; // 释放旧的数据
m_year = date.m_year;
m_mon = date.m_mon;
m_day = date.m_day;
str = date.str;
date.str = NULL;
sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
cout << "Move operator=" << ", this=" << this <<endl;
return *this;
}
移动赋值运算符解析:
- 1、处理普通数据成员,直接使用旧对象的进行赋值;
- 2、处理指向堆内存的指针,直接堆内存地址给新对象的指针,旧对象指针指向NULL。
语句str = date.str;
将旧对象的str直接给到正在创建的对象,而不重新new、复制。
语句date.str = NULL;
将旧对象的str赋值为NULL,保证其可以被正常销毁,delete NULL;
、delete[] NULL
不会造成任何问题。 - 3、由于移动操作“ 窃取” 资源, 它通常不分配任何资源。 因此, 移动操作通常不会抛出任何异常。不抛出异常的函数应该使用
noexcept
通知标准库,避免编译器为了处理异常而作一些额外的工作。
🎄三、使用移动赋值运算符
1、使用移动赋值运算符就需要弄清楚移动赋值运算符是什么时候会被调用的?
如果使用一个右值(即将被销毁的对象)给同类型的对象赋值时,就会调用该类的移动赋值运算符函数。
2、怎样的类需要定义移动赋值运算符函数?
如果该类的成员存在指针指向new分配的堆内存,则可以根据需要定义移动赋值运算符。
3、什么时候编译器会提供默认移动赋值运算符?
如果该类没有定义拷贝赋值运算符函数,且没有定义移动赋值运算符函数,则编译器会提供一个 默认的移动赋值运算符函数。
如果该类定义了拷贝赋值运算符,则编译器不会提供 默认的移动赋值运算符,需要用到 移动赋值运算符 的地方都会调用拷贝赋值运算符。
如果该类定义了移动赋值运算符,则编译器不会提供 默认的拷贝赋值运算符,需要用到 拷贝赋值运算符 的时候,编译器会报错。
4、默认的移动赋值运算符做了什么工作?
默认的移动赋值运算符 所做的工作与默认拷贝赋值运算符的工作一样,执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。
下面是第一小节的代码,加了移动赋值运算符后的例子:
// g++ 15_Move_operator=.cpp -std=c++11
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
#define MAX_NEW_MEM (64*1000*1000)
class CDate
{
public:
CDate(int year, int mon, int day); // 构造函数声明
CDate(const CDate& date); // 拷贝构造函数声明
~CDate(); // 析构函数声明
CDate operator+(int day); // 加号运算符声明
CDate& operator=(const CDate &date);// 拷贝赋值运算符声明
CDate& operator=(CDate &&date) noexcept;// 移动赋值运算符声明
void show()
{
cout << "Date: " << m_year << "." << m_mon << "." << m_day << ", this=" << this << endl;
//cout << "Date: " << str << endl;
}
private:
int m_year;
int m_mon;
int m_day;
char *str;
};
// 构造函数定义
CDate::CDate(int year, int mon, int day)
{
m_year = year;
m_mon = mon;
m_day = day;
str = new char[MAX_NEW_MEM];
sprintf(str, "%4d.%02d.%02d", year,mon,day);
cout << "Calling Constructor" << ", this=" << this <<endl;
}
// 拷贝构造函数定义
CDate::CDate(const CDate& date)
{
m_year = date.m_year;
m_mon = date.m_mon;
m_day = date.m_day;
str = new char[MAX_NEW_MEM];
memcpy(str, date.str, MAX_NEW_MEM);
cout << "Calling Copy Constructor" << ", this=" << this << ", Copy Data" <<endl;
}
// 析构函数定义
CDate::~CDate()
{
cout << "Calling Destructor" << ", this=" << this <<endl;
delete [] str;
}
// 加号运算符函数定义
CDate CDate::operator+(int day)
{
CDate temp = *this;
temp.m_day += day;
cout << "Calling operator+" << ", this=" << &temp << endl;
return temp;
}
// 赋值运算符函数定义
CDate& CDate::operator=(const CDate& date)
{
if(this == &date) // 赋值给自身
return *this;
delete [] str; // 释放旧的数据
m_year = date.m_year;
m_mon = date.m_mon;
m_day = date.m_day;
str = new char[64];
sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
cout << "Calling operator=" << ", this=" << this << ", Copy Data" <<endl;
return *this;
}
// 移动赋值运算符定义
CDate& CDate::operator=(CDate &&date) noexcept
{
if(this == &date) // 赋值给自身
return *this;
delete [] str; // 释放旧的数据
m_year = date.m_year;
m_mon = date.m_mon;
m_day = date.m_day;
str = date.str;
date.str = NULL;
sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
cout << "Move operator=" << ", this=" << this <<endl;
return *this;
}
int main()
{
CDate date(2024,06,14);
CDate date1(2024,06,14);
cout << endl;
date1 = std::move(date+1);// std::move 强制将 date+1 的求值结果转为右值
date1.show();
cout << endl;
return 0;
}
运行结果:可以看到相比于第一小节的代码,这里调用了移动赋值运算符,减少了一次拷贝。
🎄四、总结
👉本文介绍C++11的移动赋值运算符,讲解了为什么需要移动赋值运算符,怎样声明、定义移动赋值运算符,怎样使用移动赋值运算符。
移动赋值运算符的目的就是为了减少一次拷贝,提升性能。C++11主要通过右值引用来实现移动语义,在使用右值(即将销毁的对象)去给同类型对象赋值时,调用移动赋值运算符而不调用拷贝赋值运算符,以此减少一次拷贝。
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁