C++11 右值引用与移动语义详解
前情提示
在深入右值引用之前,建议熟悉以下参考文档以辅助理解:
- 准官方参考文档:cppreference(推荐,同步更新)
- 标准 C++:isocpp.org(社区讨论为主)
注意:legacy.cplusplus.com 仅适用于 C++98/11 早期版本,后续标准已不再维护。
1. C++11 的历史发展
C++11 是 C++ 自 C++98 以来最重要的更新。它标准化了许多实践,并引入了大量新特性。在最终由 ISO 于 2011 年采纳前,该项目曾被称为'C++0x'。
1.1 版本迭代策略
C++03 与 C++11 之间间隔了约 8 年。此前委员会曾尝试制定 C++08,但因规划过大、特性过多导致延期。此后,C++ 确立了每 3 年发布一次新标准的节奏。
1.2 编译器支持情况
语言标准被广泛使用通常有 5~10 年的缓冲期。目前大多数公司仍在使用 C++11 或 C++14,部分企业开始采用 C++17。C++23 等新特性因上层库完善度不足,普及率尚低。
不同编译器对特性的支持程度不同,例如 VS (MSVC) 和 Clang 的进度可能不一致。开发时需注意目标环境的编译器版本。
2. 列表初始化:{}
2.1 C++98 中的初始化
在 C++98 中,数组和结构体通常使用 0 进行初始化,语法较为分散。
2.2 C++11 的统一初始化
C++11 引入 {} 初始化(列表初始化),旨在实现一切对象皆可统一初始化。内置类型和自定义类型均支持,本质涉及临时对象的构造与优化。
int i = {1}; // 省略等号
int j{2}; // 直接初始化
Date d{2025, 11, 15}; // 自定义类型
2.3 std::initializer_list
对于容器初始化,C++11 提供了 std::initializer_list。底层通过指针管理数据,支持任意数量的值初始化。
auto il = {10, 20, 30}; // 类型为 initializer_list<int>
vector<int> v = {1, 2, 3};
3. 右值引用 && 移动语义
C++98 仅有左值引用。C++11 新增右值引用,两者均为对象别名,不占用额外空间。
3.1 左值和右值
- 左值:可取地址,表示持久存在的对象(如变量名)。
- 右值:不可取地址,通常是字面常量或临时对象。
int x = 10;
int* p = &x; // 合法,x 是左值
// int* q = &10; // 错误,10 是右值
3.2 引用规则
- 左值引用 (
&) 绑定左值。 - 右值引用 (
&&) 绑定右值。 const左值引用可延长临时对象生命周期。std::move()可将左值强制转换为右值引用。
int&& r = 10; // 右值引用绑定字面量
int&& rr = std::move(x); // 左值转为右值引用
3.3 移动构造与移动赋值
针对深拷贝类(如 string, vector),移动语义可避免资源重复分配。
- 移动构造函数:第一个参数为右值引用,窃取资源而非复制。
- 移动赋值运算符:同理,释放当前资源后接管新资源。
class MyString {
public:
// 移动构造
MyString(MyString&& other) noexcept
: _str(other._str), _size(other._size) {
other._str = nullptr; // 原对象置空
}
// 移动赋值
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] _str;
_str = other._str;
_size = other._size;
other._str = nullptr;
}
return *this;
}
private:
char* _str;
size_t _size;
};
3.4 传值返回优化
函数返回局部对象时,传统方式需经历拷贝构造。C++11 允许编译器优化(RVO/NRVO),若未定义移动语义,则回退到拷贝;若定义了移动语义且对象为右值,则调用移动构造。
关闭优化编译(如 -fno-elide-constructors)可观察实际调用次数。
3.5 容器接口提效
STL 容器的 push_back 和 insert 在 C++11 后增加了右值引用重载。传入左值触发拷贝,传入右值触发移动,显著提升性能。
完整代码示例与实践演示
list.h
#pragma once
#include <algorithm>
#include <initializer_list>
namespace jqj {
template<class T>
struct list_node {
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& x = T()) :_next(nullptr), _prev(nullptr), _data(x) {}
list_node(T&& x) :_next(nullptr), _prev(nullptr), _data(std::move(x)) {}
};
template<class T, class Ref, class Ptr>
struct list_iterator {
using Self = list_iterator<T, Ref, Ptr>;
using Node = list_node<T>;
Node* _node;
list_iterator(Node* node) :_node(node) {}
Ref operator*() { return _node->_data; }
Ptr operator->() { return &_node->_data; }
Self& operator++() { _node = _node->_next; return *this; }
Self operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; }
bool operator!=(const Self& s) const { return _node != s._node; }
bool operator==(const Self& s) const { return _node == s._node; }
};
template<class T>
class list {
using Node = list_node<T>;
public:
using iterator = list_iterator<T, T&, T*>;
using const_iterator = list_iterator<T, const T&, const T*>;
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }
void empty_init() {
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
list() { empty_init(); }
list(std::initializer_list<T> il) {
empty_init();
for (auto& e : il) push_back(e);
}
~list() { clear(); delete _head; _head = nullptr; }
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x) { insert(end(), std::move(x)); }
iterator insert(iterator pos, const T& x) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
void clear() {
iterator it = begin();
while (it != end()) it = erase(it);
}
iterator erase(iterator pos) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
return iterator(next);
}
size_t size() const { return _size; }
private:
Node* _head;
size_t _size = 0;
};
}
Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <algorithm>
#include <string.h>
#include "list.h"
using namespace std;
namespace Alice {
class string {
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
string(const char* str) :_size(strlen(str)), _capacity(_size) {
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s) {
cout << "string(const string&) - 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s) push_back(ch);
}
string(string&& s) {
cout << "string(string&&) - 移动构造" << endl;
swap(s);
}
string& operator=(const string& s) {
cout << "operator= (const) - 拷贝赋值" << endl;
if (this != &s) {
_str[0] = '\0'; _size = 0;
reserve(s._capacity);
for (auto ch : s) push_back(ch);
}
return *this;
}
string& operator=(string&& s) {
cout << "operator= (&&) - 移动赋值" << endl;
swap(s);
return *this;
}
~string() {
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
void reserve(size_t new_capacity) {
if (new_capacity > _capacity) {
char* tmp = new char[new_capacity + 1];
if (_str) strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = new_capacity;
}
}
void push_back(char ch) {
if (_size >= _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch) {
push_back(ch);
return *this;
}
const char* c_str() const { return _str; }
size_t size() const { return _size; }
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main() {
jqj::list<Alice::string> lt;
cout << "**************************" << endl;
Alice::string s1("111111111111111111");
lt.push_back(s1); // 左值,触发拷贝
cout << "**************************" << endl;
lt.push_back("2222222222222222222222222222222222"); // 临时对象,触发移动
cout << "**************************" << endl;
lt.push_back(move(s1)); // 显式 move,触发移动
cout << "**************************" << endl;
return 0;
}
总结
右值引用与移动语义的核心价值在于资源的所有权转移。通过合理设计移动构造函数和赋值运算符,结合 std::move 的使用,可以显著减少深拷贝带来的内存开销。在实际开发中,应优先关注对象的生命周期管理,利用编译器优化机制,编写高效且安全的 C++ 代码。


