C++11 发展背景
C++11 是 C++ 标准演进中的里程碑,从 C++98 到 C++11 间隔了 8 年。它标准化了许多既有实践,引入了大量新特性,极大地提升了开发效率和代码安全性。在此之后,C++ 更新周期稳定为每三年一次。
列表初始化
C++98 的传统方式
在 C++98 中,数组和结构体可以使用 {} 进行初始化,但语法较为分散。
struct Point { int _x; int _y; };
int main() {
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11 的统一初始化
C++11 引入列表初始化(List Initialization),试图实现一切对象皆可用 {} 初始化。这不仅支持内置类型,也支持自定义类型。对于自定义类型,本质上是利用构造函数进行隐式转换,编译器优化后通常变为直接构造,避免了不必要的临时对象开销。
值得注意的是,{} 初始化可以省略 = 号,这在容器操作中尤为方便。
#include<iostream>
#include<vector>
using namespace std;
struct Point { int _x; int _y; };
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) :_year(year), _month(month), _day(day) {
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d) :_year(d._year), _month(d._month), _day(d._day) {
cout << "Date(const Date& d)" << endl;
}
private:
int _year; int _month; int _day;
};
int main() {
// 内置类型
int x1 = { 2 };
int x2{ 2 };
// 自定义类型
Date d1 = { 2025, 1, 1 }; // 直接构造,未调用拷贝构造
Date d6{ 2024, 7, 25 }; // 省略=号
const Date& d7{ 2024, 7, 25 }; // 引用延长生命周期
vector<Date> v;
// 相比有名对象或匿名对象传参,{} 初始化更简洁高效
v.push_back({ 2025, 1, 1 });
return 0;
}
std::initializer_list
虽然列表初始化很方便,但在处理容器时,如果不想写多个重载构造函数,std::initializer_list 提供了更好的支持。它本质上是一个轻量级的数组视图,内部维护指向数据开始和结束的指针。
STL 容器普遍增加了接受 initializer_list 的构造函数和赋值运算符,这使得我们可以用任意数量的值来初始化容器。
#include<iostream>
#include<vector>
#include<map>
using namespace std;
int main() {
// initializer_list 本质是底层开一个数组
std::initializer_list<int> mylist = { 10, 20, 30 };
cout << sizeof(mylist) << endl;
// 容器初始化
vector<int> v1({ 1, 2, 3, 4, 5 });
vector<int> v2 = { 1, 2, 3, 4, 5 };
// map 的 key-value 对初始化
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
// 赋值操作也支持
v1 = { 10, 20, 30, 40, 50 };
return 0;
}
总结:任意类型都支持列表初始化,自定义类型依赖构造函数。initializer_list 主要是为 STL 容器设计的,让容器支持变长参数初始化,本质是将列表值传给容器的对应构造函数。
右值引用和移动语义
C++98 只有左值引用,C++11 新增了右值引用。无论是左值还是右值引用,本质上都是给对象取别名,不开辟新的内存空间。
左值和右值的区别
- 左值 (lvalue):表示数据的表达式,有持久状态,可获取地址,能出现在赋值符号左边。现代定义中,lvalue 意为 locator value,即存储在内存中有明确地址的对象。
- 右值 (rvalue):表示数据的表达式,通常是字面量常量或临时对象,不能取地址,只能出现在赋值符号右边。rvalue 意为 read value,提供数据值但不可寻址。
#include<iostream>
using namespace std;
int main() {
int b = 1;
int* p = new int(0);
// 左值:可取地址
cout << &b << endl;
cout << &p << endl;
// 右值:不可取地址
// cout << &10 << endl; // 编译错误
double x = 1.1, y = 2.2;
// cout << &(x + y) << endl; // 编译错误
return 0;
}
左值引用和右值引用
Type& r1 = x;是左值引用,绑定左值。Type&& rr1 = y;是右值引用,绑定右值。- 左值引用不能直接绑定右值,但
const左值引用可以。 - 右值引用不能直接绑定左值,但可以通过
std::move将左值强转为右值。
变量表达式本身属性是左值,这意味着即使一个右值被右值引用绑定后,该引用变量在表达式中仍表现为左值属性。这一点在函数重载匹配时非常关键。
#include<iostream>
using namespace std;
void f(int& x) { cout << "左值引用" << endl; }
void f(const int& x) { cout << "const 左值引用" << endl; }
void f(int&& x) { cout << "右值引用" << endl; }
int main() {
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&)
f(std::move(i)); // 调用 f(int&&)
int&& x = 1;
f(x); // x 是左值属性,调用 f(int&)
f(std::move(x)); // 调用 f(int&&)
return 0;
}
引用延长生命周期
右值引用可用于为临时对象延长生命周期。const 左值引用也能延长临时对象生存期,但对象不可修改。
#include<iostream>
#include<string>
using namespace std;
int main() {
std::string s1 = "Test";
// const 左值引用延长生命周期
const std::string& r2 = s1 + s1;
// r2 += "Test"; // 错误:const 不可修改
// 右值引用延长生命周期且可修改
std::string&& r3 = s1 + s1;
r3 += "Test"; // OK
cout << r3 << '\n';
return 0;
}
移动构造和移动赋值
对于包含动态资源(如深拷贝的 string、vector)的类,移动构造和移动赋值至关重要。它们通过'窃取'右值对象的资源指针,而非复制数据,从而大幅提升性能。
- 移动构造函数:第一个参数为右值引用,其他参数需有默认值。
- 移动赋值运算符:第一个参数为右值引用,与拷贝赋值构成重载。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
namespace lrq {
class string {
public:
typedef char* iterator;
iterator begin() { return _str; }
iterator end() { 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) {
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s) :_str(nullptr) {
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s) push_back(ch);
}
// 移动构造
string(string&& s) {
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 拷贝赋值
string& operator=(const string& s) {
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s) {
delete[] _str;
_str = nullptr;
_size = 0;
reserve(s._capacity);
for (auto ch : s) push_back(ch);
}
return *this;
}
// 移动赋值
string& operator=(string&& s) {
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string() {
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
if (_str) strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
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';
}
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() {
lrq::string s1("xxxxx");
lrq::string s2 = s1; // 拷贝构造
lrq::string s3 = lrq::string("yyyyy"); // 移动构造(优化后直接构造)
return 0;
}
解决传值返回问题
在 C++98 中,返回局部对象会导致拷贝开销。C++11 的移动语义配合编译器优化(如 RVO/NRVO),使得返回临时对象变得高效。
当存在移动构造/赋值时,编译器会优先选择移动操作而非拷贝操作,避免深拷贝带来的性能损耗。在 Release 模式下,编译器甚至可能直接将返回值构造到目标位置,完全消除中间对象。
注意:右值引用变量在用于表达式时属性是左值,这一设计看似奇怪,实则是为了允许我们在需要时再次使用这些对象,同时保持其作为右值的语义特征。


