一、移动语义
1.定义:
在C++ 中,移动语义是一种优化技术。
移动语义允许资源的“移动”而不是“拷贝”。在传统的 C++ 中,当一个对象被赋值或传递给函数时,通常会发生拷贝操作,这会导致性能下降,尤其是在处理大型对象时。移动语义通过引入右值引用和移动构造函数、移动赋值运算符,允许程序员将资源的所有权从一个对象转移到另一个对象,而不是进行深拷贝。
移动语义的作用:
2. 作用
- 提高性能:移动语义主要用于避免对象中资源的不必要复制,特别是对于那些包含动态分配内存、文件句柄等资源的对象。它通过转移资源所有权来高效地处理对象的构造、赋值等操作,从而提升程序的运行效率。
- 优化资源管理:在管理资源的类中,移动语义可以方便地将资源从一个对象转移到另一个对象,使得资源能够更灵活地被重新分配和利用。
下面是一个简单的 String 类,展示移动语义的实现:
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t len;
public:
// 普通构造函数
String(const char* cstr = "") {
len = std::strlen(cstr);
data = new char[len + 1];
std::strcpy(data, cstr);
}
// 移动构造函数
String(String&& rhs) noexcept : data(rhs.data), len(rhs.len) {
rhs.data = nullptr;
rhs.len = 0;
}
// 移动赋值运算符
String& operator=(String&& rhs) noexcept {
if (this!= &rhs) {
delete[] data;
data = rhs.data;
len = rhs.len;
rhs.data = nullptr;
rhs.len = 0;
}
return this;
}
~String() {
delete[] data;
}
void print() const {
std::cout << data << std::endl;
}
};
在这个 String 类中:
1)移动构造函数 String(String&& rhs) 接收一个右值引用 rhs 。它将 rhs 对象中的 data 指针和 len 成员的值转移到新构造的对象中,然后将 rhs 对象的 data 指针设为 nullptr , len 设为 0 ,表示资源所有权已经转移。
2)移动赋值运算符 String& operator=(String&& rhs) 的作用类似。它先释放当前对象的资源( delete[] data ),然后将 rhs 对象的资源转移到当前对象,最后将 rhs 对象的资源相关成员重置。
String getString() {
String temp("Hello");
return temp;
}
int main() {
String str = getString();
str.print();
return 0;
}
在 main 函数中, getString 函数返回一个临时 String 对象。在没有移动语义的情况下,这个临时对象的资源( data 中的字符数组)会被复制到 str 对象中。但有了移动语义, str 对象通过移动构造函数直接接管了临时对象的资源,避免了一次可能很耗时的字符串复制操作。
二、右值引用
右值引用的概念:
在C++中,右值引用是一种引用类型,用于绑定到右值。右值通常是临时对象或者即将销毁的值,如字面常量(如 3.14 、 'a' )、函数返回的临时对象等。右值引用使用 && 来表示,它的出现主要是为了支持移动语义和完美转发。
1.函数返回值作为右值引用:
#include <iostream>
class MyClass {
public:
MyClass() {}
MyClass(const MyClass&) {
std::cout << "Copy constructor called." << std::endl;
}
MyClass(MyClass&&) noexcept {
std::cout << "Move constructor called." << std::endl;
}
};
MyClass getObject() {
MyClass obj;
return obj;
}
int main() {
MyClass newObj = getObject();
return 0;
}
在这个例子中, getObject 函数返回一个 MyClass 类型的临时对象。在 main 函数中, newObj 接收这个返回值。如果没有右值引用和移动语义,会调用复制构造函数来复制这个临时对象。但由于C++的编译器会识别这个临时返回值是右值,并且 MyClass 有移动构造函数,所以会调用移动构造函数,避免了不必要的复制。
2.字面常量作为右值引用
#include <iostream>
void printValue(int&& num) {
std::cout << "The value is: " << num << std::endl;
}
int main() {
printValue(10);
return 0;
}
这里定义了一个函数 printValue ,它的参数是一个右值引用 int&& num 。在 main 函数中,将字面常量 10 传递给 printValue 函数,因为 10 是右值,所以可以绑定到右值引用 num ,函数就可以使用这个右值进行操作。
使用右值引用实现移动语义
1. 实现步骤及示例代码(以自定义的资源管理类为例)
- 步骤一:定义类和成员变量
首先,定义一个类,这个类包含需要移动的资源。假设我们创建一个简单的 String 类,它内部有一个字符指针来存储字符串内容。
class String {
private:
char* data;
size_t length;
public:
// 构造函数
String(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 其他成员函数(如打印字符串)
void print() const {
std::cout << data << std::endl;
}
// 析构函数
~String() {
delete[] data;
}
// 移动构造函数(重点)
String(String&& other) noexcept {
// 步骤二:资源转移
data = other.data;
length = other.length;
// 步骤三:将源对象资源指针置空
other.data = nullptr;
other.length = 0;
}
// 移动赋值运算符(重点)
String& operator=(String&& other) noexcept {
if (this!= &other) {
// 释放当前对象资源
delete[] data;
// 资源转移
data = other.data;
length = other.length;
// 将源对象资源指针置空
other.data = nullptr;
other.length = 0;
}
return *this;
}
};
- 步骤二:在移动构造函数和移动赋值运算符中进行资源转移
在移动构造函数 String(String&& other) 中,将 other 对象中的 data 指针和 length 属性赋值给新对象。这样,新对象就获得了资源的所有权。
- 步骤三:将源对象资源指针置空或重置相关属性
在移动构造函数和移动赋值运算符完成资源转移后,需要将源对象( other )的相关资源指针置空(如 other.data = nullptr )或者重置相关属性(如 other.length = 0 ),以表明资源已经被转移,防止源对象的析构函数错误地释放已经转移的资源。
String getString() {
String temp("Hello");
return temp;
}
int main() {
String str = getString();
str.print();
return 0;
}
在 main 函数中, getString 函数返回一个临时对象 temp 。由于这个临时对象是右值,在初始化 str 对象时,会调用移动构造函数,将 temp 对象中的字符串资源转移到 str 对象中,而不是进行资源复制,提高了效率。