C++11 左值、右值、左值引用和右值引用
在C++11中,引入了左值、右值、左值引用和右值引用等概念。
左值和右值
左值和右值就像他们的名称一样,左右代表着它们在表达式中可以出现的位置。
左值(Lvalue)
- 左值是具有标识符(有名字)的表达式,可以放在赋值号的左边(因此得名左值),通常可以取址(有内存位置)。
- 左值通常代表一个具体的对象或变量,如变量名、数组元素、结构体成员、引用等。
- 左值可以出现在赋值操作中,表示将右值赋值给左值,即修改左值的内容。
右值(Rvalue)
- 右值是无法取址的表达式,通常是临时的、不具有标识符的值,无法用于赋值操作的左边。
- 右值可以是字面常量、临时对象、函数返回值等,它们在表达式求值后不再存在。
- C++11引入了右值引用,用于处理右值,并允许对右值进行特殊操作,如移动语义。
引用
左值引用概念与C++98引用概念是一致的,但是与新引入的右值引用是两种不同引用类型,具有不同的语义和用法。
左值引用 (Lvalue Reference)
左值引用是最早存在于C++的引用类型。它绑定到左值(有名字的对象),可以用来修改绑定的对象。左值引用使用单个&符号来声明,例如:
int x = 10;
int& ref = x; // ref是x的左值引用
左值引用常常当做函数的参数,可以实现通过引用传递来修改函数外部的变量。
左值引用当做和原来对象一模一样,引用就是起给原来对象起了个别名。
右值引用 (Rvalue Reference)
右值引用是C++11引入的新概念,用来处理临时对象(右值)。它使用双&&符号来声明,例如:
int&& rref = 5; // rref是一个右值引用
形如上面引用的,就是右值引用。c++11引入右值引用的作用是:右值引用配合c++11新引入的移动语义,可以高效地将资源(如动态分配的内存)从一个对象转移到另一个对象,而无需进行深复制,避免额外的资源拷贝。
std::move
std::move 是C++11标准库中的一个函数模板,它用于将一个对象转换为右值引用,通常用于实现移动语义。std::move不执行实际的移动操作,就是告诉编译器将对象视为右值, 触发移动构造函数或移动赋值运算符的调用 。
#include <iostream>
#include <vector>
int main() {
std::vector<int> source = {1, 2, 3};
std::vector<int> destination;
destination = std::move(source); // 使用 std::move 将资源从 source 移动到 destination
// 此时 source 不再持有资源,它可能为空
// destination 现在持有原始资源
std::cout << "Source size: " << source.size() << std::endl; // 可能为空
std::cout << "Destination size: " << destination.size() << std::endl; // 3
return 0;
}
在上面的示例中,std::move 用于将 source 中的资源移动到 destination,从而避免了不必要的资源拷贝。
但是使用 std::move 不会强制执行移动操作,它只是提供了移动的提示,实际移动操作的实现 取决于对象的类型以及它是否定义了相应的移动构造函数和移动赋值运算符 。
移动构造函数
#include <iostream>
#include <string>
class MyString {
public:
// 普通构造函数
MyString(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 移动构造函数
MyString(MyString&& other) {
std::cout << "移动构造函数被调用" << std::endl;
length = other.length;
data = other.data;
other.length = 0;
other.data = nullptr;
}
// 析构函数
~MyString() {
delete[] data;
}
// 输出字符串内容
void print() {
std::cout << data << std::endl;
}
private:
size_t length;
char* data;
};
int main() {
MyString original("Hello, World!");
MyString copied(original); // 调用拷贝构造函数,创建一个新对象
MyString moved(std::move(original)); // 调用移动构造函数,移动资源
original.print(); // original 不再拥有资源,data 为空指针
copied.print(); // 输出 "Hello, World!"
moved.print(); // 输出 "Hello, World!"
return 0;
}
在上面的示例中,MyString 类具有移动构造函数,它在移动语义的情况下被调用。通过 std::move 将 original 的资源移动到 moved,这导致 original 不再拥有资源,而 moved 现在拥有原始资源。
移动赋值运算符
#include <iostream>
#include <string>
class MyString {
public:
// 普通构造函数
MyString(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 移动构造函数
MyString(MyString&& other) {
std::cout << "移动构造函数被调用" << std::endl;
length = other.length;
data = other.data;
other.length = 0;
other.data = nullptr;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) {
std::cout << "移动赋值运算符被调用" << std::endl;
if (this != &other) {
delete[] data;
length = other.length;
data = other.data;
other.length = 0;
other.data = nullptr;
}
return *this;
}
// 析构函数
~MyString() {
delete[] data;
}
// 输出字符串内容
void print() {
std::cout << data << std::endl;
}
private:
size_t length;
char* data;
};
int main() {
MyString original("Hello, World!");
MyString moved("Move me!");
moved = std::move(original); // 调用移动赋值运算符,移动资源
original.print(); // original 不再拥有资源,data 为空指针
moved.print(); // 输出 "Hello, World!"
return 0;
}
在上面的示例中,MyString 类具有移动赋值运算符,它在移动语义的情况下被调用。通过 std::move 将 original 的资源移动到 moved,这导致 original 不再拥有资源,而 moved 现在拥有原始资源。
左值引用和右值引用对原对象影响
左值引用
通过左值引用 修改原对象会直接影响原对象的值 。因为左值引用绑定到了对象本身,所以任何通过左值引用所做的修改都会反映在原对象上。例如:
int x = 10;
int& ref = x;
ref = 20; // 修改通过引用修改了原对象 x 的值为 20
右值引用
如果一个对象的资源(如内存、文件句柄等)已经被成功移动到另一个对象,那么原始对象就不再持有这些资源,因此它变得无效。这是右值引用和移动语义的基本原理之一: 通过右值引用,可以高效地将资源从一个对象转移到另一个对象,但这会导致原始对象的状态变为未定义或无效 。
例如:
#include <iostream>
#include <vector>
int main() {
std::vector<int> original = {1, 2, 3};
std::vector<int> &&rref = std::move(original); // 使用移动语义将资源从 original 移动到 rref
// 此时 original 不再持有 {1, 2, 3} 的资源,它已经变得无效
// rref 现在持有原始资源
std::cout << "Original size: " << original.size() << std::endl; // 可能导致未定义的行为
for (int num : rref) {
std::cout << num << " ";
}
return 0;
}
上面的例子,通过 std::move 将 original 的资源移动到 rref 后,original 不再持有这些资源, 它的状态变为无效 。因此, 在尝试访问 original 的属性或方法时,可能会导致未定义的行为。
所以,右值引用和移动语义的目的之一是实现资源的有效转移,但这也需要程序员在使用右值引用后谨慎处理原始对象的状态,以避免悬空引用或未定义行为。
右值引用不当使用
当使用右值引用时,如果不小心处理不当,可能会导致悬空引用(Dangling Reference)和资源泄漏的问题。下面将分别举例说明这两种情况。
悬空引用(Dangling Reference)
悬空引用是指引用了一个已经销毁或不再有效的对象的情况。这可能会导致未定义的行为 。考虑以下示例:
int&& getRValue() {
int temp = 42;
return std::move(temp); // 错误!返回了局部变量 temp 的右值引用
}
int main() {
int&& rref = getRValue(); // rref 引用了已经销毁的 temp 对象
std::cout << rref << std::endl; // 可能导致未定义的行为
return 0;
}
在这个例子中,函数 getRValue 返回了一个右值引用,但它引用的是一个局部变量 temp,该变量在函数退出后就会被销毁。因此,当我们在 main 函数中使用 rref 时,它引用的对象已经不存在,可能导致未定义的行为。
资源泄漏
右值引用通常在移动语义中使用,用于将资源从一个对象转移到另一个对象,避免不必要的拷贝。如果在移动资源后未适当地处理原始对象,可能会导致资源泄漏。考虑以下示例:
class Resource {
public:
Resource() {
data = new int[100];
}
~Resource() {
delete[] data;
}
// 移动构造函数
Resource(Resource&& other) {
data = other.data;
other.data = nullptr; // 错误!未将 other.data 置为 nullptr
}
private:
int* data;
};
int main() {
Resource res1;
Resource&& res2 = std::move(res1); // 使用移动语义将资源从 res1 移动到 res2
// 此时 res1 的 data 指针已经不再有效,但析构函数未能正确释放资源
// 导致资源泄漏
return 0;
}
在这个例子中,移动构造函数没有将原始对象 other 的指针 data 置为 nullptr,导致在原始对象析构时未能正确释放资源,从而发生了资源泄漏。
总结
- 左值引用用于绑定到有名对象,用来修改原对象和传递值。它在C++早期就存在。
- 右值引用用于绑定到临时对象,引入了移动语义,允许高效地管理资源。它在C++11引入。
- 左值引用和右值引用在参数传递、函数返回值、构造函数和赋值运算符中都有不同的语义。
- 使用右值引用需要小心处理原对象的状态,避免悬空引用和资源泄漏。
本文系作者 @何健源 原创发布在思维代码站点。未经许可,禁止转载。
暂无评论数据