官方文档中文版:
https://zh.cppreference.com/w/cpp
官方文档英文版:
一. C++版本迭代
1)C++98 (1998):第一个正式国际标准,莫定了基础(模板、STL、异常等)。
2)C++03 (2003):纯技术修正版,无新特性,主要修复缺陷和提升一致性。
3)C++11 (C++0x):里程碑式重大更新 ,现代化C++的开端。
核心新特性:auto、范围for、智能指针、移动语义与右值引用、Lambda表达式、nullptr、变参模板、constexpr、线程支持库等,大幅提升生产力与性能。
4)C++14 :在C++11基础上小幅改进(如泛型Lambda、二进制字面量、返回值自动推导)。
5)C++17 :进一步增强语言与库(结构化绑定、ifconstexpr、文件系统库、并行算法等)。
6)C++20 :又一个重大版本 ,引入概念(Concepts)、协程、范围(Ranges)、三路比较运算符等。
7)C++23 :补充完善(如std::expected、this推导、多维下标操作等)。
8)C++26(进行中):预计未来完成。
C++11是转折点,它从 "增强版C" 转向更安全、现代、表达力强的语言,被视为 "C++2.0" 。此前版本演进缓慢,此后每3年一更新标准。
9)新标准一般用的比较少,因为编译器支持还不太好。委员会指定语法规则和库,真正实现是由各编译器完成的。
10)如何看新标准?
-
从官网看,我们之前用的参考手册:https://legacy.cplusplus.com/reference/,并不是官方的文档,只更新到C++11,和一点点之后的内容,想要看新特性只能去官网:https://en.cppreference.com/w/。
-
不仅可以看特性,还可以看各大编译器对这些新特性的支持情况。以C++11和C++23、C++26为例(只截取部分):

右半部分是编译器对新特性的支持情况(绿色是支持了,红色是还没支持),可以看到主流编译器对C++11的支持已经很好了。

23和26这两个最新的标准都还支持的不太好。

二. 列表初始化
1)C++98中,数组和结构体可以用{ }进行初始化。
C++11以后,想统一初始化方式,对{ }进行了扩展,希望一切对象都可以用{ }进行初始化,这种初始化方式称为列表初始化。
2)内置类型支持,自定义类型也支持。自定义类型的支持本质是隐式类型转换,中间会产生临时对象,最终优化成了直接构造。
3)列表初始化的过程中可以省略等号=。
4)这种初始化方式在某些场景下,比如容器push、insert多参数构造对象时会很方便。
5)这种方法依托构造函数。
cpp
void test01() // C++98{}初始化
{
struct Point
{
int _x;
int _y;
};
// 数组可以
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
// 结构体可以
Point p = { 1, 2 };
for(const auto& e : array1)
cout << e << " ";
cout << endl; // 1 2 3 4 5
for(const auto& e : array2)
cout << e << " ";
cout << endl; // 0 0 0 0 0
cout << p._x << " " << p._y << endl; // 1 2
}
void test02() // C++11之后{}初始化
{
// 一切皆可用列表初始化,且可以不加=
// 自定义类型(隐式类型转换)
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;
};
// 构造+拷贝构造->优化为直接构造
Date d1 = { 2000, 1, 1 };
Date d2{ 2001, 2, 2 };
const Date& d3 = { 2002, 3, 3 }; // 引用的是{}构造的临时对象,需要加const
Date d4{ 2001 }; // 单参数构造的隐式类型转换
//Date d5 2001 ; // 报错,只有{}初始化才可以不写等号
// 内置类型支持
int n1 = { 1 };
double n2{ 2.0 };
// 方便的使用场景
vector<Date> v;
v.push_back(d1); // 有名对象
v.push_back(Date(2026, 4, 13)); // 匿名对象
v.push_back({ 2004, 11, 8 }); // {}初始化,最方便
}

三. std::initializer_list
1)上面的初始化方式已经很方便了,但是他需要依托构造函数,在初始化容器对象时还是不太方便。比如一个vector对象,想用N个值初始化,那你需要实现一个接收N个值的构造函数,那我又想用N-1、N-2、...个值初始化呢,每一个都实现一个构造函数么?很明显是不现实的。
2)所以C++11库中提出了一个类:std::initializer_list。这个类本质 是底层开一个数组,将数据拷贝过来,类内有两个指针分别指向数组的开始和结束。
支持迭代器遍历。
3)需要N个值初始化的容器支持一个std:initializer_list的构造函数,也就支持任意多个值构成的{x1,x2,x3...}进行初始化了。
STL中的容器支持任意多个值构成的{x1,x2,x3...}进行初始化,就是通过std::initializer_list的构造函数支持的。


cpp
void test03() // initializer_list
{
initializer_list<int> il{ 1,2,1,5,9,77,31 };
// 内层pair对象的{}初始化(参数个数确定) 和 外层map的initializer_list构造(参数个数不确定) 结合到⼀起用了
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
// initializer_list版本的赋值支持
//vector<int> v1({ 1,2,3,4,5,6 }); // 直接构造
vector<int> v1{ 1,2,3,4,5,6 }; // 构造+拷贝构造-> 直接构造
v1 = { 10,20,30,40,50 };
}

四. 右值引用
1)左值和右值
- C++98时就有引用的语法,之前的在C++11之后称为左值引用。
C++11新增了右值引用语法特性,无论是左值还是右值都是给对象取别名。
- 左值 是一个表示数据的表达式(如变量名、对象名、解引用的指针、[]返回的元素等)。一般有持久状态存储在内存中,可以获取地址。左值出现在赋值符号的左边、右边都可以。
const修饰的左值不给被赋值,但是可以取他的地址。
-
右值 也是一个表示数据的表达式,要么是字面值常量,要么临时对象,或者是匿名对象、move后的值(move是一个函数,后面介绍),他们的生命周期通常只在当前一行(可通过一定方式延长生命周期,后面会介绍)。右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
-
左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value的缩写。
现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象;而rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址,如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
cpp
void test04() // 常见的左值与右值
{
// 左值可以取地址
int a = 1;
cout << &a << endl;
const int b = 2;
cout << &b << endl;
int* p = &a;
*p = 10;
cout << &(*p) << endl;
string s("llllll");
cout << &s << endl;
s[3] = 's'; // 传引用返回
// &s[3] 的类型是 char*, cout 对 char* 有特殊重载
// 尝试输出从该地址开始直到'\0'为止的所有字符,而不是输出地址值,所以这里强转一下
cout << (void*)&s[3] << endl;
// 右值不可取地址
double x = 5.3, y = 3.1;
10; // 字面量
x + y; // 临时对象
fmin(x, y); // 库函数:比较来两个浮点数大小,传值返回,返回临时对象
string("lllll"); // 匿名对象
// 全部报错,&要求左值
//cout << &10 << endl;
//cout << &(x + y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("lllll") << endl;
}

2)左值引用和右值引用
-
从语法层面看,左值引用和右值引用都是取别名,不开空间。
-
左值引用就是给左值取别名,右值引用就是给右值取别名。
-
左值引用不能直接引用右值,但是const左值引用可以引用右值:const T& r = 右值;
右值引用不能直接引用左值,但是右值引用可以引用move之后的左值:T&& rr = move(左值);

cpp
void test05() // 左值引用和右值引用
{
// 左值
int a = 1;
const int b = 2;
int* p = &a;
*p = 10;
string s("llllll");
s[3] = 's';
// 左值引用
int& r1 = a;
const int& r2 = b;
int*& r3 = p;
char& r4 = s[0];
// 左值不能直接引用右值,但const左值引用可以
//int& r5 = 10; // 报错
const int& r5 = 10; // 正确
double x = 5.3, y = 3.1;
// 右值
10; // 字面量
x + y; // 临时对象
fmin(x, y); // 库函数:比较来两个浮点数大小,传值返回,返回临时对象
string("lllll"); // 匿名对象
// 右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("lllll");
// 右值不能直接引用左值,但右值引用可以引用move后的左值
int&& rr5 = move(a);
cout << a << endl; // 未指定行为,a的内容随机
string&& rr6 = move(s);
cout << s << endl; // 未指定行为,s的内容随机
}
3)左值和右值的参数匹配
-
C++98中,我们只实现一个const左值引用作为参数的函数,传递左值和右值都可以匹配。
-
C++11之后,细分了左值和右值,分别重载左值引用、const左值引用、右值引用作为形参版本的函数,根据我们之前学过的理论,有更匹配的参数列表一定会去调更匹配的 。所以实参是左值会
匹配左值引用版本,实参是const左值会匹配const左值引用版本,实参是右值会匹配右值引用版本。
cpp
// 左值和右值的参数匹配
void f(int& x)
{
cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
cout << "const左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
cout << "右值引用重载 f(" << x << ")\n";
}
void test06()
{
int i = 1; // 左值
const int ci = 2; // const左值
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&)。如果没有 f(int&&) 则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
}

4)右值引用和移动语义
1. 左值引用使用场景
左值引用主要使用场景是:在函数中 左值引用传参 和 左值引用传返回值 时减少拷贝,同时还可以修改实参和修改返回对象。
左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的addStrings和generate函数。C++98 中的解决方案只能是被迫使用输出型参数 解决。
那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。
cpp
// 两个字符串相加
// 传值返回需要拷贝
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if(next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
// 生成杨辉三角
// 这里的传值返回拷贝整个二维数组代价太大
//vector<vector<int>> generate(int numRows)
void generate(int numRows, vector<vector<int>>& vv) // 输出型参数,减少拷贝代价
{
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
//return vv;
}
既然返回右值也解决不了,那么C++11之后究竟是如何解决的呢?用到两个新的默认成员函数 -- 移动构造和移动赋值。
2. 移动构造和移动赋值
① 移动构造函数 是一种构造函数,类似于拷贝构造:Type(const T& x){};
移动构造函数要求第一个参数是该类类型的右值引用,如果还有其他参数(一般都没有),额外的参数必须有缺省值。Type(T&& x){};
拷贝构造和移动构造参数不同可以构成重载,意义就是:将左值(拷贝构造)与右值(移动构造)区分开。
② 移动赋值 是一个赋值运算符的重载,他跟之前的拷贝赋值构成重载。
移动赋值函数要求第一个参数是该类类型的右值引用。
③ 对于像string/vector这样的深拷贝 的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义 。因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要交换出 引用的右值对象的资源 ,而不是 像拷贝构造和拷贝赋值那样去拷贝资源 ,从提高效率。
下面的laosi::string实现了移动构造和移动赋值,我们结合场景理解移动语义提效的原理(用自己实现的是为了方便看调用了哪个函数)。
cpp
#pragma once
#include <assert.h>
#include <string.h>
#include <algorithm>
namespace laosi
{
class string
{
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
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)
{
::swap(_str, s._str); // 调用全局的std::swap
::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)
{
_str[0] = '\0';
_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;
}
char & operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
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';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
};
}



④ 可以看到移动构造和移动赋值的实现 很简单,核心就是调用一次swap ,交换当前对象和传过来的右值的资源。
这时我们会考虑 那么被交换的值怎么办,他不是成了随机值了?
其实是没关系的,因为只有内置类型才有字面量,自定义类型是没有的,所以string对象的右值都是临时对象或匿名对象,他们的生命周期只在当前一行,马上就要销毁了。既然你都要销毁了,与其带走有用的资源不如把这个资源交换给我,你带着我的随机值走,我还减少了一次拷贝,只是改变了一下底层指针的指向,swap的开销是很小的。
这就是右值的应用与意义的体现:右值引用的出现使参数匹配时区分了左值和右值,这样即将要销毁的右值我直接交换他的资源,减少拷贝提高效率。而需要长期存在不能随意交换资源的左值还是老老实实调用拷贝构造进行深拷贝。
⑤ 回到我们前面 两个字符串相加 和 杨辉三角 部分的深拷贝问题,C++11之后究竟是如何解决的?
右值引用借助移动构造起作用。
下图中addStrings函数的返回值str本身是左值 ,但是编译器认为此时str的生命周期只到这一行了,返回之后函数栈帧销毁,str也要析构了,所以特殊处理后认为str是右值 ,调的是移动构造。

自 C++11 起,所有标准库容器都提供了移动构造函数和移动赋值运算符。所以返回时会调用跟右值更匹配的移动构造,只交换不拷贝资源,明显提高效率。

cpp
void test07()
{
// 构造
laosi::string s1("llllllll");
// 左值->拷贝构造
laosi::string s2(s1);
// 右值,构造+移动构造->编译器优化为直接构造
laosi::string s3(laosi::string("sssssssss"));
// 移动构造
laosi::string s4 = move(s1); // 将左值强转成右值
}
⑥ VS下有优化,连续的 构造+拷贝构造 或者 构造+移动构造 会优化为直接构造。

Linux下编译时可以关闭优化:-fno-elide-constructors
bash
[lsy@hcss-ecs-116a c++test]$ g++ test.cpp -o test -std=c++11 -fno-elide-constructors
[lsy@hcss-ecs-116a c++test]$ ./test
string(char* str)-构造
string(const string& s) -- 拷贝构造
string(char* str)-构造
string(string&& s) -- 移动构造
~string() -- 析构
string(string&& s) -- 移动构造
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
3. 变量表达式都是左值属性
① 之前说右值具有常性不可修改,但是引入移动语义之后我们要交换右值的资源,把我们不要的资源给右值让它带走,这不就是修改右值了么?语法设计上是如何解决这个矛盾的?
② 变量表达式都是左值属性 ,也就意味着一个右值被右值引用绑定后,这个右值引用变量的属性是左值 。

③ 这个属性影响到参数匹配,右值引用本身的属性是左值是为了能转移右值的资源。

4. 编译器的优化
① 之前是将连续的两次拷贝/移动构造二合一优化为一次。
② 需要注意的是在vs2019的release和vs2022及之后的debug和release的优化比较激进,直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。
③ 编译器认为str的构造修改就是为了拷贝/移动给ret,不如直接让str变成ret的别名,实现零拷贝。 原本合二为一的优化进一步变为合三为一。

④ 这样优化后,实现了零拷贝,那么是否右值引用的移动语义的提效就失去了意义呢?
并不是的,因为这种优化方式并不是C++标准规定的,是编译器自行实现的。也就意味着并不是所有的编译器都有优化,没有优化的情况下需要移动构造减少拷贝。
5)延长临时对象生命周期
右值引用和const左值引用可以用于为临时对象延长生命周期,但这些对象都无法修改。
延长到什么时间看r2、r3的生命周期。
cpp
void test09()
{
string s1 = "Test";
// const的左值引用延长生命周期
const string& r2 = s1 + s1;
//r2 = "Test"; // 不能通过const的引用修改
// 右值引用延长生命周期
string&& r3 = s1 + s1;
r3 += "Test"; // 能通过到非const的引用修改
cout << r3 << endl; // TestTestTest
}

6)右值引用和移动语义在容器插入中的提效
-
前面我们看的都是在传值中的提效,容器是深拷贝的类,都会实现移动构造+移动赋值,他们在容器的插入过程中的提效也是一大应用场景。
-
查看文档可以发现C++11及之后的标准中,所有标准库容器的插入类操作函数(如insert、push、push_back...),都增加了右值引用版本的重载。核心目的,是让容器能够充分利用 移动语义,显著提升性能。


-
当实参是左值时,容器内部调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。
当实参是右值,容器内部调用移动构造,将右值对象的资源到容器空间的对象上。
- 以list为例,看一下如何重载右值引用版本(只展示框架和与插入有关的接口):
cpp
#pragma once
#include <assert.h>
#include <initializer_list>
namespace laosi
{
// 结点结构
template <class T>
struct list_node
{
list_node* _next;
list_node* _prev;
T _data;
list_node(const T& x = T())
:_next(nullptr)
,_prev(nullptr)
,_data(x)
{}
list_node(T&& x)
:_next(nullptr)
,_prev(nullptr)
,_data(move(x))
{}
};
// 迭代器类
template <class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> self;
Node* _node; // 一个指向结点的指针
list_iterator(Node* node)
:_node(node)
{}
Ptr operator->() // T*, const T*
{
return &_node->_data;
}
Ref operator*() // T&, const T&
{
return _node->_data;
}
self& operator++() // 前置++
{
_node = _node->_next;
return *this;
}
self& operator--() // 前置--
{
_node = _node->_prev;
return *this;
}
bool operator!=(const self& it) const
{
return _node != it._node;
}
bool operator==(const self& it) const
{
return _node == it._node;
}
};
// list类
template <class T>
class list
{
typedef list_node<T> Node;
private:
Node* _head; // 头结点
int _size;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
_size = lt._size;
for (auto& e : lt)
{
push_back(e);
}
}
~list()
{
clear();
delete _head;
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// lt2 = lt1
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
void insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
newnode->_prev = prev;
newnode->_next = cur;
prev->_next = newnode;
cur->_prev = newnode;
_size++;
}
void insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(move(x));
newnode->_prev = prev;
newnode->_next = cur;
prev->_next = newnode;
cur->_prev = newnode;
_size++;
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
void push_front(T&& x)
{
insert(begin(), move(x));
}
};
}


7)类型分类
- C++11之后,右值被细分为两个核心类别:
① 纯右值
符合传统右值定义,没有身份、只能被移动。
典型例子:字面量(42、true、nullptr)、临时对象、返回右值引用的表达式(如move(x)的返回值)
② 将亡值
C++11 新增概念:有身份 ,同时也是可移动的。
典型例子:
返回右值引用的函数调用(如move(x))。
转换为右值引用的转型表达式(如static_cast<int&&>(x))。
访问类成员时的将亡值(如临时对象的成员)。
- 泛左值( generalized value,简称glvalue**)**,和右值同一级别的概念。泛左值包括左值和将亡值。

右值让C++的效率又有了明显的提升,是C++11中非常重要的一部分,内容也很多,接下篇吧...