文章目录
Day11 移动语义回顾
一、移动语义基础概念
- 移动语义:通过“转移资源所有权”而非“复制资源”,提升程序效率,尤其适用于临时对象或大对象的处理。
- 移动构造函数和移动赋值运算符:C++11引入的特殊成员函数,分别用于从右值中构造对象或赋值给现有对象。
- 与拷贝语义的区别:
- 移动语义不会拷贝内存,只是指针赋值,效率更高。
- 编译器不会自动生成移动构造和移动赋值,需要手动实现。
二、自定义 String
类的移动语义实现
class String
{
friend std::ostream& operator<<(std::ostream& os, const String& rhs);
public:
// 默认构造函数
String()
: _pstr(nullptr) {
cout << "String()" << endl;
}
// 构造函数:深拷贝字符串
String(const char* pstr)
: _pstr(new char[strlen(pstr) + 1]()) {
cout << "String(const char* pstr)" << endl;
strcpy_s(_pstr, strlen(pstr) + 1, pstr);
}
// 拷贝构造函数:深拷贝另一个 String 对象
String(const String& rhs)
: _pstr(new char[strlen(rhs._pstr) + 1]()) {
cout << "String(const String& rhs)" << endl;
strcpy_s(_pstr, strlen(rhs._pstr) + 1, rhs._pstr);
}
// 移动构造函数:资源转移,不再复制字符串内容
String(String&& rhs)
: _pstr(rhs._pstr) {
cout << "String(String&& rhs)" << endl;
rhs._pstr = nullptr; // 清空源对象
}
// 拷贝赋值运算符
String& operator=(const String& rhs) {
cout << "String& operator=(const String& rhs)" << endl;
if (this != &rhs) //1、防止自复制
{
delete[] _pstr;//2、释放左操作数
//3、深拷贝
_pstr = new char[strlen(rhs._pstr) + 1]();
strcpy_s(_pstr, strlen(rhs._pstr) + 1, rhs._pstr);
}
//4、返回*this
return *this;
}
// 移动赋值运算符
String& operator=(String&& rhs) {
cout << "String& operator=(String&& rhs)" << endl;
if (this != &rhs)//1、自移动
{
delete[] _pstr;//2、释放左操作数
//3、浅拷贝
_pstr = rhs._pstr;
rhs._pstr = nullptr;
}
//返回*this
return *this;
}
~String() {
cout << "~String()" << endl;
delete[] _pstr;
_pstr = nullptr;
}
private:
char* _pstr;
};
输出运算符重载:
std::ostream& operator<<(std::ostream& os, const String& rhs)
{
if (rhs._pstr) {
os << rhs._pstr;
}
return os;
}
三、测试函数:验证移动与拷贝行为
void test() {
String s1("hello");
cout << "s1 = " << s1 << endl;
String s2 = s1; // 拷贝构造
cout << "s2 = " << s2 << endl;
String s3 = "world"; // 隐式调用构造函数,然后移动构造
//上面这条代码有个隐式转换的过程,"world" --> String("world")
//String("world")是临时对象/匿名对象
cout << "s3 = " << s3 << endl;
String s4("hainan");
cout << "s4 = " << s4 << endl;
s4 = String("shangfa"); // 临时对象移动赋值
cout << "s4 = " << s4 << endl;
s4 = std::move(s4); // 自移动,测试保护机制
cout << "s4 = " << s4 << endl;//warning!使用已移动的 from 对象: s4 (lifetime.1)。
//移动赋值运算符函数中没有判断if(this != &rhs)之前,
// 打印s4的值,输出结果为空,因为在输出流重载运算符函数中,已经进行了判空操作,若指向为空,直接执行return
cout << "11111" << endl;
/*std::move可以将左值转换为右值,实质上没有做任何移动,只是在底层做了强制转换static_cast<T &&>(lvalue)
如果以后不想再使用某个左值,可以使用std::mobe将其转换为右值,以后就不再使用了*/
s2 = std::move(s1); // s1 被移动后已失效
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
}
四、左值与右值的补充说明
void testRightValue()
{
int a = 10;
int b = 20;
int* pflag = &a;
string s1("hello");
string s2("world");
int& ref = a;
//int& ref2 = 10;//error!非常量引用的初始值必须为左值
const int& ref2 = 10;
/*const左值引用既可以绑定到左值,也可以绑定到右值*/
const int& ref3 = a;
&a;//左值
&b;//左值
&pflag;//左值
& *pflag;//左值
&s1;//左值
&s2;//左值
//表达式必须为左值或函数指示符
//&(a + b);//左值
//&(s1 + s2);//左值
&ref;//左值
//C++11之前是不能识别右值的,C++11之后新增语法可以识别右值
int&& rref = 10;//右值引用
//int&& rref2 = b;//error!无法将右值引用绑定到左值
//还有一个问题,左值引用是左值还是右值?
&rref;//右值引用在此处是左值(右值引用在作为函数参数的时候,体现出来的是左值的含义)
}
//那么,右值引用可以是右值吗?(右值引用作为函数返回类型的时候是右值)
int&& func()
{
return 10;
}
int main()
{
test();
//&func();//error!“&”要求左值
return 0;
}
右值引用作为函数返回值
int&& func() {
return 10;
}
右值引用返回的对象仍然是右值,不能取地址。
五、知识总结
类型 | 是否可绑定左值 | 是否可绑定右值 | 备注 |
---|---|---|---|
左值引用 (T& ) |
✅ | ❌ | 常用于一般变量引用 |
const 左值引用 (const T& ) |
✅ | ✅ | 可以绑定临时对象,常用于函数参数 |
右值引用 (T&& ) |
❌ | ✅ | 主要用于移动语义 |
如何区分左值与右值?
- 能否取地址(&) 是判断左值的重要标准。
- 表达式结果是否具名或可持久存在。
六、附加说明:关于 std::move
std::move(x)
实质上是static_cast<T&&>(x)
,并不真正“移动”。- 它只是告诉编译器:“我不再使用这个对象了,可以安全地‘偷走’资源”。
- 右值的存放位置(冷知识):右值短暂的存在于栈上