跳到主要内容
C++11 列表初始化、新式声明、范围 for 与 STL 变化 | 极客日志
C++
C++11 列表初始化、新式声明、范围 for 与 STL 变化 C++11 引入统一列表初始化、auto 类型推导、nullptr 空指针及范围 for 循环等特性。文章对比 C++98 与 C++11 差异,详解 initializer_list 机制、auto 与 decltype 用法规范、nullptr 类型安全及 STL 容器新接口如 emplace 和移动语义。这些改进提升代码简洁性、性能与安全性,是现代 C++ 开发基础。
筑梦师 发布于 2026/3/29 更新于 2026/4/23 1 浏览C++11 新特性
0. 前言
在经历了漫长的发展历程后,C++11 终于在 2011 年横空出世。它不仅是对 C++98/03 的修补和扩展,更像是一次脱胎换骨的升级,被很多开发者称为'现代 C++ 的起点'。
如果说 C++98 给了我们一个强大但略显笨重的工具箱,那么 C++11 就是为它装上了新的润滑油和精密零件,让我们能更高效、更优雅地使用这门语言。
本文将围绕 C++11 中的几个核心特性展开介绍:
统一的列表初始化 :让初始化不再混乱,{} 成为万能钥匙。
新式声明(auto、decltype、nullptr) :让代码更简洁、更安全。
范围 for 循环 :写起来更清爽,读起来更直观。
STL 的新变化 :容器、迭代器接口、右值引用等为性能与便利性提供支持。
在讲解过程中,我们会对比 C++98 的旧写法,配合代码示例,帮助大家真正体会到 C++11 的'现代感'。如果你正打算从传统 C++ 过渡到现代 C++,这将是一份不错的入门参考。
1. C++ 与 C++11 简介
C++ 的发展简史
C++ 最初由 Bjarne Stroustrup 在 1979 年提出,作为 C 语言的扩展,它引入了面向对象编程的概念,使得 C++ 在保持高效执行性能的同时,具备了更强的抽象和封装能力。
1998 年 :第一个 C++ 国际标准正式发布,被称为 C++98。这是 C++ 发展史上里程碑式的事件,确立了语言的基本框架和标准库体系。
2003 年 :标准委员会发布了一份技术勘误表(TC1),修复了 C++98 标准中的一些缺陷。修订后的版本被称为 C++03。由于核心语言未做改动,通常人们将其统称为 C++98/03。
2000 年代初 :委员会计划在 2007 年发布新标准,最初称之为 C++07,但由于进度拖延,后来被称为 C++0x(x 表示不确定是哪一年完成)。
2011 年 :经过长达 10 年的准备与争论,第二个真正意义上的 C++ 标准终于落地,正式定名为 C++11。
C++11 的意义
相比于 C++98/03,C++11 是一次真正意义上的'语言进化',它不仅修复了前标准中的大量缺陷(约 600 处),还引入了超过 140 个新特性。这些变化使得 C++11 更加现代化,甚至有人称它为'一种新的语言'。
C++11 的主要改进体现在以下几个方面:
语法层面更简洁 :引入了自动类型推导、范围 for 循环、lambda 表达式等,让代码更简洁直观。
性能与效率提升 :通过右值引用和移动语义,减少了不必要的拷贝,显著提升运行效率。
并发支持 :C++11 标准库首次引入了多线程支持,为现代并发编程提供了统一接口。
更强的库支持 :新增了智能指针、正则表达式、哈希容器等工具,进一步增强了标准库的实用性。
安全性与可维护性 :通过 nullptr、显式删除函数(=delete)、强枚举类型等机制,使得代码更加严谨和健壮。
总的来说,C++11 不仅是 C++98/03 的自然延续,更是一次跨越式升级。它让 C++ 更加适用于系统开发、库开发、并发编程等复杂领域,同时提升了开发效率,降低了代码维护成本。
小故事:C++11 命名的由来
1998 年是 C++ 标准化的起点,委员会原计划每五年更新一次标准。在讨论 C++03 后的下一个版本时,最初的目标是 2007 年发布,所以称为 C++07。然而由于标准制定的复杂性,2007 年未能完成,2008 年也无望,于是改名为 C++0x,其中 x 表示未知数。直到 2011 年才最终完成,因此正式定名为 C++11。
2. 统一的列表初始化
我们用 Point 这个结构体来进行初始化的演示。
struct Point {
Point (int x, y) : _x(x), _y(y) {}
_x;
_y;
};
int
int
int
C++98 中传统的{}初始化 在 C++98 中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
int arr[] = {1 , 2 , 3 };
int arr1[6 ] = {0 };
Point p = {1 , 2 };
C++11 中统一的列表初始化
列表初始化 C++11 扩大了用大括号括起的列表{}的使用范围,使其可用于所有的内置类型和用户自定义类型的初始化。
使用列表初始化时,可添加等号 =,也可不添加。
C++11 以后是想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做{}列表初始化。
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后编译器优化了以后变成直接构造。
int x = 1 ;
int y (2 ) ;
int a1[] = {1 , 2 , 3 };
int z = {3 };
int w{4 };
int a2[] = {1 , 2 , 3 };
Point p0 (0 , 0 ) ;
Point p1 = {1 , 1 };
Point p2{2 , 2 };
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 };
可以认为{}列表初始化是支持了多参数构造函数的隐式类型转换:
C++98 中,单参数的构造函数支持隐式类型转换。
Point ptr1 = {1 , 1 };
Point* ptr2 = new Point[2 ]{Point (1 , 2 ), Point (3 , 4 )};
Point* ptr3 = new Point[2 ]{ptr1, ptr1};
Point* ptr4 = new Point[2 ]{{2 , 2 }, {3 , 3 }};
const Point& rp = {1 , 8 };
实际在使用时,可以多使用{}列表初始化,但不建议去掉初始化用的=,因为以下两种写法容易混淆。
Point ptr1 (0 , 0 ) ;
Point ptr2{0 , 0 };
std::initializer_list
引入 有了统一的列表初始化,思考以下场景,以下两行代码使用的语法一样吗?
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 中新增的构造函数。
initializer_list 介绍 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 };
const int * arr = {1 , 2 , 3 };
int * arr = {1 , 2 , 3 };
vector 补充支持 initializer_list 的构造
vector (std::initializer_list<T> il) {
reserve (il.size ());
for (auto & e : il)
push_back (e);
}
map 相关 map<string, string> dict = {{"sort" , "排序" }, {"insert" , "插入" }};
3. C++11 的新声明
1. auto
1. C++ 类型系统演进
1.1 从 C 到 C++ 的类型困境 传统 C 风格代码中,复杂的类型声明严重阻碍了代码可读性。以 STL 容器迭代器为例:
std::map<std::string, std::vector<std::pair<int , double >>>::iterator it = data.begin ();
聪明的宝子已经想到了,我们可以尝试用 typedef 解决问题,但 typedef 也有其缺陷和局限性。
1.2 typedef 的局限性 虽然 typedef 能缓解部分问题,但存在严重缺陷:
typedef char * pstring;
int main () {
const pstring p1;
const pstring* p2;
return 0 ;
}
在 C++ 中,const 的修饰规则取决于它出现的位置和类型别名的展开方式。
1. const pstring p1
pstring 的类型是 char*(指针类型)。
const 修饰的是变量 p1 本身,即 p1 是一个常量指针(指针本身不可变,但指向的字符可变)。
展开后等价于:char* const p1;。
错误原因:常量指针 p1 必须在声明时初始化(否则编译失败)。
2. const pstring* p2
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,修饰的是指针指向的对象;在 右边的 const,修饰的是对象本身。
核心规则
const 修饰的是其右侧的符号(若右侧无符号,则修饰左侧的符号)。
typedef 定义的别名会保留原始类型的修饰关系,const 直接修饰别名类型本身。
可以看到,const 在和 typedef 联合使用时,有这么多的注意事项,那有没有什么好的解决方案呢?有的!
2. auto 关键字的革命性意义 在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11 中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
2.1 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;
return 0 ;
}
2.2 auto 使用时的注意细节 使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种'类型'的声明,而是一个类型声明时的'占位符',编译器在编译期会将 auto 替换为变量实际的类型。
1. auto 与指针和引用结合起来使用 用 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 声明引用类型时则必须加&。
2. 在同一行定义多个变量 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main () {
auto a = 1 , b = 2 ;
auto c = 3 , d = 4.0 ;
return 0 ;
}
3. auto 不能推导的场景
1. auto 不能作为函数的参数 形参在传参前没有初始值,编译器无法根据其初始值进行推导类型。
2. auto 不能直接用来声明数组 为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。
auto 在实际中最常见的优势用法就是和 C++11 提供的新式 for 循环和 lambda 表达式等进行配合使用。
3. auto 核心机制深度剖析
3.1 类型推导规则 const int cx = 42 ;
auto v1 = cx;
auto & v2 = cx;
int arr[5 ];
auto arr1 = arr;
auto & arr2 = arr;
void func (int ) ;
auto f1 = func;
auto & f2 = func;
解析初始化表达式
推导表达式类型(去除引用和 const 限定)
应用类型修饰符(& *等)
生成最终变量类型
3.2 auto 推导时的类型退化(Type Decay) const char * const str = "hello" ;
auto s1 = str;
auto * s2 = str;
auto & s3 = str;
去除顶层 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" , "插入" }};
auto it = dict.begin ();
2. decltype 关键字 decltype 的作用:将变量的类型声明为表达式指定的类型。
auto pf = malloc;
cout << typeid (pf).name () << endl;
typeid().name 只能获取到变量的类型,不能用于声明/定义变量。
我们可以使用 auto 来定义变量,但 auto 定义时必须完成初始化。
C++11 更新了 decltype 关键字来解决这里的问题。
关键字 decltype 的作用:将变量的类型声明为表达式指定的类型。
场景一 :类内的成员变量是个函数指针,但是声明函数指针类型太繁琐了,有没有什么简单的书写方式呢?
class A {
private :
decltype (malloc) pf;
};
decltype (pf) _ptr = malloc;
decltype (malloc) _ptr2;
decltype(malloc) pf; pf 是一个函数指针,类型和 malloc 的类型一致,这里 decltype 完成类型的自动推导。
场景二 :类内的模板参数需要传入函数指针时,使用 decltype 简化书写。
template <class Func >
class B {
private :
Func _pf;
};
实例化 B 类时,decltype 作为模板实参,显式实例化模板。
auto pf = malloc;
B<decltype (pf)> b1;
B<decltype (malloc)> b2;
int main () {
const int x = 1 ;
double y = 2.2 ;
decltype (x * y) ret;
decltype (&x) p;
cout << typeid (ret).name () << endl;
cout << typeid (p).name () << endl;
return 0 ;
}
3. nullptr
1. 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 作为整数常量可能会导致不明确的重载匹配,编译器无法确定应该调用哪个版本的函数。
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;
return 0 ;
}
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖。
在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void*)0。
为了避免这些问题,C++11 引入了 nullptr,它是一个具有明确类型的空指针常量。
2. nullptr 的特性
2.1 类型安全 nullptr 具有独特的类型 std::nullptr_t,这使得它与任何类型的指针都不相同。它是一个空指针常量,专门用来表示指针的空值。
int * p1 = nullptr ;
int x = nullptr ;
2.2 不能被隐式转换为整数 与 NULL 可能被隐式转换为 0 不同,nullptr 无法被转换为整数类型。这消除了类型不安全的隐患。
nullptr_t nt = nullptr ;
int x = nt;
2.3 可以用于所有指针类型 nullptr 可以用于任何类型的指针,无论是普通指针、智能指针还是类类型指针。
int * p1 = nullptr ;
std::shared_ptr<int > p2 = nullptr ;
2.4 在重载函数中消除歧义 由于 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 );
return 0 ;
}
在这个例子中,func(nullptr) 会调用 func(int*),而不会引发重载歧义问题。
3. nullptr 与 NULL 比较
3.1 定义差异
NULL 是一个宏,通常被定义为 0,但它是整数类型,不具有指针类型的明确区分。
nullptr 是一个具有明确类型 std::nullptr_t 的常量,只有在指针上下文中才有效。
3.2 使用场景
使用 NULL 时可能会引发一些类型混淆或重载解析问题,尤其是在复杂的函数重载中。
使用 nullptr 能有效避免这些问题,因为它具有强类型系统,不会与整数类型混淆。
3.3 编译器支持 大部分现代编译器都已经支持 nullptr,因此可以放心使用。在 C++11 标准之前,C++ 编译器大多只能使用 NULL 来表示空指针。
4. nullptr 在实际编程中的使用
4.1 初始化指针 在初始化指针时,使用 nullptr 是更好的选择,因为它确保指针明确为空。
4.2 在函数重载中避免歧义 在重载函数中,nullptr 的使用可以避免由于 NULL 引发的歧义问题。
void foo (int * p) ;
void foo (double * p) ;
foo (nullptr );
4. 范围 for 循环
1. 语法
1.1 C++98 中遍历数组的方式 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 循环。
1.2 C++11 范围 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 来跳出整个循环。
2. 范围 for 的使用条件
for 循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
迭代的对象要实现++和==的操作。
void TestFor (int array[]) {
for (auto & e : array)
cout << e << endl;
}
3. 范围 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 循环。
依赖 ADL 查找 begin/end 方法
使用右值引用避免不必要的拷贝
迭代器有效性要求与普通循环相同
4. auto 与范围 for 的协同效应
4.1 最佳实践模式
std::vector<std::vector<std::string>> complex_data;
for (const auto & inner_vec : complex_data) {
for (const auto & str : inner_vec) {
std::cout << str << ' ' ;
}
std::cout << '\n' ;
}
若只需读取数据,使用 const 引用(const auto&)避免不必要的拷贝。
若需修改字符串内容,可去掉 const 并使用普通引用(auto&)。
避免在遍历过程中修改容器结构(如添加/删除元素),否则可能导致未定义行为。
4.2 性能优化要点 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) {}
5. 性能分析 std::vector<int > data (1'000'000 ) ;
for (size_t i = 0 ; i < data.size (); ++i) {
data[i] *= 2 ;
}
for (auto & x : data) {
x *= 2 ;
}
循环类型 指令缓存命中率 分支预测失败率 执行时间 (ms) 传统索引循环 92% 1.2% 2.45 范围 for 循环 95% 0.8% 2.38
结论 :现代编译器对两种循环方式的优化能力相当,因此不用担心性能问题。
5. C++11 容器相关
新容器
C++11 中 STL 新增了四个容器,分别是 array(定长数组),forward_list(单链表),unordered_map(哈希),unordered_set(哈希)
以上容器这里暂不做介绍
新接口
c 系列的迭代器
新增了一系列 c 开头的迭代器接口,这些是由于 C++ 标准委员会认为普通对象和 const 对象都调用 begin 和 end 容易混淆
其实不容易混淆:const 版本的 begin() 和 end() 中的 const 修饰的是 this 指针(非静态成员函数隐藏的第一个参数),所以参数类型不同,构成函数重载,因此:
普通对象调用普通的 begin() 和 end() 函数,const 对象调用 const 版本的 begin() 和 end() 函数,因此很容易区分
在实际中 c 系列开头的迭代器返回函数使用并不多
{}列表初始化的构造函数
emplace 系列和右值引用
所有容器新增了 emplace 系列,会涉及&&右值引用和...模板的可变参数,会带来性能上的提升
移动构造和移动赋值
6. 结语 从列表初始化到新式声明,从范围 for 到 STL 新接口,C++11 的诸多特性无不体现出一个目标:让开发者在保持高性能的同时,写出更简洁、更安全、更现代化的代码 。
当然,C++11 并不是终点。之后的 C++14、C++17、C++20 都在不断丰富和强化语言本身,但 C++11 无疑是整个'现代 C++'时代的奠基石。如果你能熟练掌握这些特性,就已经站在了通往更高版本的桥梁上。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online