C++11 核心特性详解:列表初始化、新式声明、范围 for 与 STL 变化
介绍 C++11 标准的核心新特性。包括统一的列表初始化语法,简化了对象构造;引入 auto、decltype 和 nullptr 关键字,提升类型推导的安全性与代码简洁度;新增基于范围的 for 循环,优化遍历逻辑;以及 STL 容器的更新,如 initializer_list 支持、emplace 系列函数和移动语义。这些改进旨在提高开发效率、增强代码安全性并优化性能,是现代 C++ 编程的基础。

介绍 C++11 标准的核心新特性。包括统一的列表初始化语法,简化了对象构造;引入 auto、decltype 和 nullptr 关键字,提升类型推导的安全性与代码简洁度;新增基于范围的 for 循环,优化遍历逻辑;以及 STL 容器的更新,如 initializer_list 支持、emplace 系列函数和移动语义。这些改进旨在提高开发效率、增强代码安全性并优化性能,是现代 C++ 编程的基础。

C++ 最初由 Bjarne Stroustrup 在 1979 年提出,作为 C 语言的扩展,它引入了 面向对象编程 的概念,使得 C++ 在保持高效执行性能的同时,具备了更强的抽象和封装能力。
相比于 C++98/03,C++11 是一次真正意义上的'语言进化',它不仅修复了前标准中的大量缺陷(约 600 处),还引入了 超过 140 个新特性。这些变化使得 C++11 更加现代化,甚至有人称它为'一种新的语言'。
C++11 的主要改进体现在以下几个方面:
nullptr、显式删除函数(=delete)、强枚举类型等机制,使得代码更加严谨和健壮。总的来说,C++11 不仅是 C++98/03 的自然延续,更是一次 跨越式升级。它让 C++ 更加适用于 系统开发、库开发、并发编程 等复杂领域,同时提升了开发效率,降低了代码维护成本。
1998 年是 C++ 标准化的起点,委员会原计划 每五年更新一次标准。在讨论 C++03 后的下一个版本时,最初的目标是 2007 年发布,所以称为 C++07。然而由于标准制定的复杂性,2007 年未能完成,2008 年也无望,于是改名为 C++0x,其中 x 表示未知数。直到 2011 年才最终完成,因此正式定名为 C++11。
我们用 Point 这个结构体来进行初始化的演示:
// 结构体 Point
struct Point {
Point(int x, int y) : _x(x), _y(y) {}
int _x;
int _y;
};
在 C++98 中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定:
// C++98 花括号{} 初始化数组
int arr[] = {1, 2, 3};
int arr1[6] = {0};
// C++98 花括号{} 初始化结构体,属于聚合初始化
Point p = {1, 2};
C++11 扩大了用大括号括起的列表 { } 的使用范围,使其可用于所有的内置类型和用户自定义的类型的初始化。
=,也可不添加。C++11 以后是想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做**{}列表初始化**。内置类型的初始化对比:
C++98:
// C++98 初始化内置类型
int x = 1; // 初始化
double y = 2.2;
int y(2); // C++ 中内置类型 的 构造函数 初始化
int a1[] = {1, 2, 3}; // C++98 初始化一个数组
C++11:
// C++11 {}列表初始化 内置类型
int z = {3};
int w{4};
int a2[] = {1, 2, 3}; // C++11 可以不写赋值的等号
初始化自定义类型:以下写法 本质都会调用构造函数:
// 以下写法 本质都会调用构造函数
// C++98
Point p0(0, 0);
Point p1 = {1, 1};
// C++11
Point p2{2, 2};
C++11{}列表初始化可以去掉=,更好地支持以下写法:
// C++ 11 更好地支持了这种写法
int* p1 = new int[3]{1, 2, 3};
int* p2 = new int[4]{2, 4, 6, 8};
int* p3 = new int[5]{0};
可以认为**{}列表初始化是支持了多参数的构造函数的隐式类型转换**:
Point ptr1 = {1, 1}; // 调用构造函数*1
Point* ptr2 = new Point[2]{Point(1, 2), Point(3, 4)}; // 调用构造函数*2
Point* ptr3 = new Point[2]{ptr1, ptr1}; // 这里没有调用构造函数
// 下面这行这里语法的本质是 支持了 多参数构造函数的隐式类型转换
Point* ptr4 = new Point[2]{{2, 2}, {3, 3}}; // 调用构造函数 *2
{}也可以构造临时对象:
// Point& rp0 = { 1, 8 }; // {1, 8}会生成一个 Point 的临时对象,临时对象具有常性,需要用常引用
const Point& rp = {1, 8};
总结:
Point ptr1(0, 0); // 调用构造函数
Point ptr2{0, 0}; // 调用构造函数
vector<int> v = {1, 2, 3};
Point p = {1, 2};
看起来都是**{}列表初始化**,但其实是完全不同的语法。
vector<int> v = { 1, 2, 3 };这里的语法不是 {}列表初始化,调用的是构造函数,这里会先调用initializer_list的构造函数,再调用C++11为vector新增的构造函数。Point p = { 1, 2 };,直接调用两个参数的构造函数,支持了多参数的构造函数隐式类型转换。vector<int> v = { 1, 2, 3 }的语法是调用C++11中新增的构造函数。
C++11设计了一个新的类型,为initializer_list,用**{}括起来的,逗号分隔的常量列表**,就是initializer_list。
auto il = {1, 2 ,5};,变量il的类型就是initializer_list。initializer_list的基本定义:template<class T>
class initializer_list {
const T* _start;
const T* _finish;
};
const T的列表称为initializer_list。initializer_list可以当成 C++11 中新增的类来使用,有相应的构造函数和迭代器。initializer_list只支持读数据,不支持写入,因为initializer_list的iterator和const_iterator的类型都是const T*。initializer_list对象,本质是调用initializer_list的构造函数:auto il_1 = {1, 2, 3};
initializer_list<int> il_2 = {10, 20, 30};
initializer_list成员函数的设计,我们可以简单推断initializer_list的底层实现:template<class T>
class initializer_list {
const T* _start;
const T* _finish;
};
initializer_list,C 语言中的这种写法不被支持了,不然和initializer_list的设计相冲突,不能发生隐式类型转换:// 以下写法 C++11 中不支持
const int* arr = {1, 2, 3};
int* arr = {1, 2, 3};
// 补充支持 initializer_list 的构造函数
vector(std::initializer_list<T> il) {
reserve(il.size());
for(auto& e : il) push_back(e);
}
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
传统 C 风格代码中,复杂的类型声明严重阻碍了代码可读性。以 STL 容器迭代器为例:
std::map<std::string, std::vector<std::pair<int, double>>>::iterator it = data.begin();
这种冗长的类型声明带来两个个主要问题:
聪明的宝子已经想到了,我们可以尝试用
typedef解决问题,但typedef也有其缺陷和局限性。
虽然 typedef 能缓解部分问题,但存在严重缺陷:
typedef char* pstring;
int main() {
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
在 C++ 中,
const的修饰规则取决于它出现的位置和类型别名的展开方式。
• pstring 的类型是 char*(指针类型)。
• const 修饰的是 变量 p1 本身,即 p1 是一个 常量指针(指针本身不可变,但指向的字符可变)。
• 展开后等价于:char* const p1;。
• 错误原因:常量指针 p1 必须在声明时初始化(否则编译失败)。
• pstring 的类型是 char*。
• const 修饰的是 pstring 类型的对象,即 p2 是一个 指向常量指针的指针。
• 展开后等价于:char* const* p2;。
• 正确:p2 本身是一个普通指针,可以指向其他 const pstring 类型的对象(无需初始化)。
| 声明 | const 修饰的对象 | 展开后的等价形式 | 编译结果 |
|---|---|---|---|
const pstring p1; | 指针 p1 本身(常量指针) | char* const p1; | 失败 |
const pstring* p2; | pstring 类型的对象(指向的指针是常量) | char* const* p2; | 成功 |
• 若想修饰 指向的字符(而不是指针本身),应使用 const char*:
// 指向常量字符的指针(指针可变,字符不可变)
const char* p3;
char const* p4; // 同上
• 若想同时修饰 指针和指向的字符:
const char* const p5; // 常量指针指向常量字符
在*左边的 const,修饰的是指针指向的对象 在*右边的 const,修饰的是对象本身
• const 修饰的是其右侧的符号(若右侧无符号,则修饰左侧的符号)。
• typedef 定义的别名会保留原始类型的修饰关系,const 直接修饰别名类型本身。
可以看到,
const在和typedef联合使用时,有这么多的注意事项,那有没有什么好的解决方案呢?
有的有的!
在早期
C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11 中,标准委员会赋予了
auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto() {
return 10;
}
int main() {
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用 auto 定义变量时必须对其进行初始化
return 0;
}
可以看到,
auto对类型进行了自动推导。
使用
auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种'类型'的声明,而是一个类型声明时的'占位符',编译器在编译期会将auto替换为变量实际的类型。
//无法通过编译,使用 auto 定义变量时必须对其进行初始化
auto e;
用
auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须 加&。
int main() {
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
auto d = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
可以看到:
c 是 x 的别名,d 是 x 的赋值auto 声明指针类型时,用 auto 和 auto* 没有任何区别。auto 声明引用类型时则必须加 &。当在
同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main() {
auto a = 1, b = 2; //该行代码会编译失败,因为 c 和 d 的初始化表达式类型不同
auto c = 3, d = 4.0;
return 0;
}
// 此处代码编译失败,auto 不能作为形参类型,因为编译器无法对 a 的实际类型进行推导
void TestAuto(auto a) {}
C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。auto 在实际中最常见的优势用法就是和 C++11 提供的新式 for 循环 和 lambda 表达式 等进行配合使用。推导规则遵循模板参数推导的黄金法则:
const int cx = 42;
auto v1 = cx; // 推导为 int (去除 const)
auto& v2 = cx; // 推导为 const int&
特殊情况处理:
int arr[5];
auto arr1 = arr; // 推导为 int*
auto& arr2 = arr; // 推导为 int(&)[5]
void func(int);
auto f1 = func; // void(*)(int),此处为函数指针
auto& f2 = func; // void(&)(int)
编译器处理 auto 变量的步骤:
去除引用 和 const 限定)& * 等)auto 推导时的类型退化机制:
const char* const str = "hello";
auto s1 = str; // 推导为:const char*
auto* s2 = str; // 推导为:const char*
auto& s3 = str; // 推导为:const char* const&
退化规则:
int i = 10;
auto p = &i;
auto pf = malloc; // 函数指针类型 也可以 自动识别
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
decltype 的作用:将变量的类型声明为表达式指定的类型引入:
使用 auto 定义一个函数指针:
auto pf = malloc; // 函数指针类型 auto 也可以 自动识别
cout << typeid(pf).name() << endl;
那么我们如何定义一个和 pf 同类型的变量呢?
// 以下这种写法不可取
typeid(pf).name _ptr;
typeid().name 只能获取到变量的类型,不能用于声明/定义变量auto 来定义变量,但 auto 定义时必须完成初始化auto pf1 = pf; // 如果我只想声明一个变量,不定义,该如何做呢?
C++11 更新了 decltype 关键字来解决这里的问题。关键字 decltype 的作用:将变量的类型声明为表达式指定的类型
场景一:类内的成员变量是个函数指针,但是声明函数指针类型太繁琐了,有没有什么简单的书写方式呢?
class A {
private:
decltype(malloc) pf;
};
// 一些其他使用
// decltype 可以推导出变量的类型,再定义变量,或者作为模板实参
decltype(pf) _ptr = malloc;
decltype(malloc) _ptr2; // 单纯先声明一个变量,不初始化
decltype(malloc) pf; pf 是一个函数指针,类型和 malloc 的类型一致,这里 decltype 完成类型的自动推导。场景二:类内的模板参数需要传入函数指针时,使用 decltype 简化书写
template<class Func>
class B {
private:
Func _pf; // 可以传入函数指针
};
decltype 作为模板实参,显式实例化模板auto pf = malloc; // 函数指针类型 也可以 自动识别
B<decltype(pf)> b1; // decltype 自动推导函数指针类型
B<decltype(malloc)> b2;
场景三: decltype 推理表达式的类型
int main() {
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret 的类型是 double
decltype(&x) p; // p 的类型是 int const *
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
return 0;
}
nullptr 的背景在 C++ 中,NULL 被广泛用来表示空指针。NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
这种实现方式存在一定问题:
可以看到,NULL 可能被定义为字面常量 0,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
NULL 实际上是 0,它可能被错误地解释为整数 0。在某些情况下,编译器可能无法正确区分 0 是空指针还是数字常量。NULL 作为整数常量可能会导致不明确的重载匹配,编译器无法确定应该调用哪个版本的函数。例如:有如下代码
//C++ 中的空指针
void f(int) { cout << "f(int)" << endl; }
void f(int*) { cout << "f(int*)" << endl; }
int main() {
f(0);
f(NULL);
f(nullptr); // 空指针关键字
cout << sizeof(nullptr) << endl; // 8 字节
return 0;
}
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖。
在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void*)0。
为了避免这些问题,C++11 引入了 nullptr,它是一个具有明确类型的空指针常量。
nullptr 的特性nullptr 具有独特的类型 std::nullptr_t,这使得它与任何类型的指针都不相同。它是一个空指针常量,专门用来表示指针的空值。
int* p1 = nullptr; // 正确,p1 是一个空的 int 指针
int x = nullptr; // 错误,不能将 nullptr 赋给一个非指针类型
与 NULL 可能被隐式转换为 0 不同,nullptr 无法被转换为整数类型。这消除了类型不安全的隐患。
nullptr_t nt = nullptr;
int x = nt; // 错误,无法将 nullptr_t 转换为 int
nullptr 可以用于任何类型的指针,无论是普通指针、智能指针还是类类型指针。
int* p1 = nullptr; // 对于普通指针
std::shared_ptr<int> p2 = nullptr; // 对于智能指针
由于 nullptr 有明确的类型,它可以帮助解决重载函数中由于 NULL 造成的歧义问题。
void func(int* p) { std::cout << "Called func(int*)\n"; }
void func(double* p) { std::cout << "Called func(double*)\n"; }
int main() {
func(nullptr); // 传递 nullptr,调用 func(int*)
return 0;
}
在这个例子中,func(nullptr) 会调用 func(int*),而不会引发重载歧义问题。
NULL 是一个宏,通常被定义为 0,但它是整数类型,不具有指针类型的明确区分。nullptr 是一个具有明确类型 std::nullptr_t 的常量,只有在指针上下文中才有效。NULL 时可能会引发一些类型混淆或重载解析问题,尤其是在复杂的函数重载中。nullptr 能有效避免这些问题,因为它具有强类型系统,不会与整数类型混淆。大部分现代编译器都已经支持 nullptr,因此可以放心使用。在 C++11 标准之前,C++ 编译器大多只能使用 NULL 来表示空指针。
nullptr 在实际编程中的使用在初始化指针时,使用 nullptr 是更好的选择,因为它确保指针明确为空。
int* p = nullptr; // 更安全和明确的空指针初始化
在重载函数中,nullptr 的使用可以避免由于 NULL 引发的歧义问题。
void foo(int* p);
void foo(double* p);
foo(nullptr); // 调用 foo(int*)
该语法前文已经介绍过:
void TestFor() {
int array[] = {1, 2, 3, 4, 5};
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此
C++11中引入了基于范围的 for 循环。
for 循环后的括号由冒号' :'分为两部分:
void TestFor_2() {
int array[] = {1, 2, 3, 4, 5};
for (auto& e : array) e *= 2;
for (auto& e : array) cout << e << endl;
}
范围
for与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
begin 和 end 的方法,begin 和 end 就是 for 循环 迭代的范围。以下代码就有错误,因为 for 的范围不确定
//不知道数组的长度
void TestFor(int array[]) {
for (auto& e : array) cout << e << endl;
}
范围 for 循环的等价转换:
for (auto& elem : container) {
...
}
// 转换为
{
auto&& __range = container;
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
auto& elem = *__begin;
...
}
}
可以看到,范围 for 的底层依然是基本的 for 循环。
范围 for 底层的关键点:
begin/end 方法//对于 STL 中的长类型使用 auto
std::vector<std::vector<std::string>> complex_data;
for (const auto& inner_vec : complex_data) {
// 对每个子向量,使用范围 for 遍历内层字符串
for (const auto& str : inner_vec) {
std::cout << str << ' ';
}
std::cout << '\n';
}
注意事项:
const 引用(const auto&)避免不必要的拷贝。const 并使用普通引用(auto&)。迭代器失效场景:
std::vector<int> vec{1, 2, 3};
for (auto& x : vec) {
if (x == 2) vec.push_back(4); // 导致迭代器失效
}
右值容器处理:
for (auto&& x : get_temporary()) {} // 延长临时对象生命周期
避免隐式拷贝:
for (auto x : huge_container) {} // 拷贝开销
for (const auto& x : huge_container) {} // 正确方式
//使用 const 引用减少拷贝开销。
测试案例(循环 100 万次):
std::vector<int> data(1'000'000);
// 传统 for 循环
for (size_t i = 0; i < data.size(); ++i) {
data[i] *= 2;
}
// 范围 for 循环
for (auto& x : data) {
x *= 2;
}
GCC 12 优化结果:
| 循环类型 | 指令缓存命中率 | 分支预测失败率 | 执行时间 (ms) |
|---|---|---|---|
| 传统索引循环 | 92% | 1.2% | 2.45 |
| 范围 for 循环 | 95% | 0.8% | 2.38 |
结论:现代编译器对两种循环方式的优化能力相当,因此不用担心性能问题。
C++11 中 STL 新增了四个容器,分别是 array(定长数组),forward_list(单链表),unordered_map(哈希),unordered_set(哈希)const 版本的 begin() 和 end() 中的 const修饰的是 this 指针(非静态成员函数隐藏的第一个参数),所以参数类型不同,构成函数重载,因此:
begin() 和 end() 函数,const 对象调用 const 版本的 begin() 和 end() 函数,因此很容易区分emplace 系列,会涉及**&&右值引用和…模板的可变参数**,会带来性能上的提升容器新增了移动构造(也叫移动拷贝构造)和移动赋值,同样也会带来性能上的提升
右值引用和移动构造带来的性能提升,我们在之后的文章中做讲解
从列表初始化到新式声明,从范围 for 到 STL 新接口,C++11 的诸多特性无不体现出一个目标:让开发者在保持高性能的同时,写出更简洁、更安全、更现代化的代码。
当然,C++11 并不是终点。之后的 C++14、C++17、C++20 都在不断丰富和强化语言本身,但 C++11 无疑是整个'现代 C++'时代的奠基石。如果你能熟练掌握这些特性,就已经站在了通往更高版本的桥梁上。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online