当前位置: 首页 > news >正文

【C++】C++11新特性第一弹(列表初始化、新式声明、范围for和STL中的变化)

C++11新特性

  • C++11新特性
  • github地址
  • 0. 前言
  • 1. C++与C++11简介
    • C++的发展简史
    • C++11的意义
    • 小故事:C++11命名的由来
  • 2. 统一的列表初始化
    • C++98中传统的{}初始化
    • C++11中统一的列表初始化
      • 列表初始化
      • std::initializer_list
        • 引入
        • initializer_list介绍
        • vector补充支持initializer_list的构造
        • map相关
  • 3. C++11的新声明
    • 1. auto
      • 1. C++类型系统演进
        • 1.1 从C到C++的类型困境
        • 1.2 typedef的局限性
          • 1. const pstring p1
          • 2. const pstring* p2
        • 关键总结
        • 对比其他写法
        • 核心规则
      • 2. auto关键字的革命性意义
        • 2.1 auto的用法
        • 2.2 auto使用时的注意细节
          • 1. auto与指针和引用结合起来使用
          • 2. 在同一行定义多个变量
          • 3. auto不能推导的场景
            • 1. auto不能作为函数的参数
            • 2. auto不能直接用来声明数组
      • 3. auto核心机制深度剖析
        • 3.1 类型推导规则
        • 3.2 auto推导时的类型退化(Type Decay)
      • 推导函数指针的场景:
    • 2. decltype
    • 3. nullptr
      • 1. `nullptr` 的背景
      • 2. `nullptr` 的特性
        • 2.1 类型安全
        • 2.2 不能被隐式转换为整数
        • 2.3 可以用于所有指针类型
        • 2.4 在重载函数中消除歧义
      • 3. nullptr`与 NULL 比较
        • 3.1 定义差异
        • 3.2 使用场景
        • 3.3 编译器支持
      • 4. `nullptr` 在实际编程中的使用
        • 4.1 初始化指针
        • 4.2 在函数重载中避免歧义
  • 4. 范围for循环
    • 1. 语法
      • 1.1 C++98中遍历数组的方式
      • 1.2 C++11范围for循环
    • 2. 范围for的使用条件
    • 3. 范围for语法糖的底层实现
    • 4. auto与范围for的协同效应
      • 4.1 最佳实践模式
      • 4.2 性能优化要点
    • 5. 性能分析
  • 5. C++11容器相关
    • 新容器
    • 新接口
      • c系列的迭代器
      • {}列表初始化的构造函数
      • emplace系列和右值引用
      • 移动构造和移动赋值
  • 6. 结语

C++11新特性

github地址

有梦想的电信狗

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 的主要改进体现在以下几个方面:

  1. 语法层面更简洁:引入了自动类型推导、范围for循环、lambda表达式等,让代码更简洁直观。
  2. 性能与效率提升:通过右值引用和移动语义,减少了不必要的拷贝,显著提升运行效率。
  3. 并发支持:C++11 标准库首次引入了多线程支持,为现代并发编程提供了统一接口。
  4. 更强的库支持:新增了智能指针、正则表达式、哈希容器等工具,进一步增强了标准库的实用性。
  5. 安全性与可维护性:通过 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这个结构体来进行初始化的演示
// 结构体Point
struct Point {Point(int x, int y):_x(x), _y(y){ }int _x;int _y;
};

C++98中传统的{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定

// C++98 花括号{} 初始化数组
int arr[] = {1, 2, 3};	
int arr1[6] = {0};
// C++98 花括号{} 初始化结构体, 属于聚合初始化
Point p = {1, 2};

C++11中统一的列表初始化

列表初始化

  • 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};

可以认为**{}列表初始化是支持了多参数的构造函数的隐式类型转换**:

  • C++98中,单参数的构造函数支持隐式类型转换
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};	// 调用构造函数
    

std::initializer_list

引入
  • 有了统一的列表初始化,思考以下场景,以下两行代码使用的语法一样吗?
vector<int> v = { 1, 2, 3 };
Point p = { 1, 2 };		

看起来都是**{}列表初始化**,但其实是完全不同的语法

  • vector<int> v = { 1, 2, 3 };这里的语法不是 {}列表初始化,调用的是构造函数,这里会先调用initializer_list的构造函数,再调用C++11vector新增的构造函数
  • 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的基本定义:

在这里插入图片描述

  • 可以看到,标准库中将const T的列表称为initializer_list

  • initializer_list可以当成 C++11 中新增的类来使用,有相应的构造函数和迭代器

  • initializer_list只支持读数据,不支持写入,因为initializer_listiteratorconst_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 }; // C++11中不支持
int* arr = { 1, 2, 3 };	 	 // C++11中不支持

在这里插入图片描述

vector补充支持initializer_list的构造
// 补充支持 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();

这种冗长的类型声明带来两个个主要问题:

  1. 类型拼写错误风险增加
  2. 代码维护成本指数级上升

聪明的宝子已经想到了,我们可以尝试用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 char* const p5;  // 常量指针指向常量字符

在*左边的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;//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化return 0;
}

在这里插入图片描述

可以看到,auto对类型进行了自动推导。

2.2 auto使用时的注意细节

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

//无法通过编译,使用auto定义变量时必须对其进行初始化auto e; 
1. auto与指针和引用结合起来使用

auto声明指针类型时,用autoauto*没有任何区别,但用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声明指针类型时,用autoauto*没有任何区别。
  • auto声明引用类型时则必须加&
2. 在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

int main() {auto a = 1, b = 2;//该行代码会编译失败,因为c和d的初始化表达式类型不同auto c = 3, d = 4.0;return 0;
}

在这里插入图片描述

  • 可以看到第五行有相应的报错信息。
3. auto不能推导的场景
1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
  • 形参在传参前没有初始值,编译器无法根据其初始值进行推导类型
2. auto不能直接用来声明数组

在这里插入图片描述

  • 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
  • auto在实际中最常见的优势用法就是和C++11提供的新式for循环lambda表达式等进行配合使用。

3. auto核心机制深度剖析

3.1 类型推导规则

推导规则遵循模板参数推导的黄金法则:

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变量的步骤:

  1. 解析初始化表达式
  2. 推导表达式类型(去除引用const限定)
  3. 应用类型修饰符(& *等)
  4. 生成最终变量类型
3.2 auto推导时的类型退化(Type Decay)

auto推导时的类型退化机制:

const char* const str = "hello";
auto s1 = str;  // 推导为:const char*
auto* s2 = str; // 推导为:const char*
auto& s3 = str; // 推导为:const char* const&

退化规则:

  1. 去除顶层const
  2. 数组退化为指针
  3. 函数退化为函数指针

推导函数指针的场景:

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();

2. decltype

  • 关键字 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;	// 可以传入函数指针
};
  • 实例化 B 类时,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的类型是doubledecltype(&x) p;				// p的类型是int const *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 作为整数常量可能会导致不明确的重载匹配,编译器无法确定应该调用哪个版本的函数。

例如:有如下代码

//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,它是一个具有明确类型的空指针常量。

2. nullptr 的特性

2.1 类型安全

nullptr 具有独特的类型 std::nullptr_t,这使得它与任何类型的指针都不相同。它是一个空指针常量,专门用来表示指针的空值。

int* p1 = nullptr; // 正确,p1 是一个空的 int 指针
int x = nullptr;   // 错误,不能将 nullptr 赋给一个非指针类型
2.2 不能被隐式转换为整数

NULL 可能被隐式转换为 0 不同,nullptr 无法被转换为整数类型。这消除了类型不安全的隐患。

nullptr_t nt = nullptr;
int x = nt;  // 错误,无法将 nullptr_t 转换为 int
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);  // 传递 nullptr,调用 func(int*)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 是更好的选择,因为它确保指针明确为空。

int* p = nullptr;  // 更安全和明确的空指针初始化
4.2 在函数重载中避免歧义

在重载函数中,nullptr 的使用可以避免由于 NULL 引发的歧义问题。

void foo(int* p);
void foo(double* p);foo(nullptr);  // 调用 foo(int*)

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循环

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的使用条件

  1. for循环迭代的范围必须是确定的
    • 对于数组而言,就是数组中第一个元素和最后一个元素的范围;
    • 对于类而言,应该提供beginend的方法,beginend就是for循环迭代的范围。
  2. 迭代的对象要实现++和==的操作。

以下代码就有错误,因为for的范围不确定

//不知道数组的长度
void TestFor(int array[]){for(auto& e : array)cout<< e <<endl;
}

3. 范围for语法糖的底层实现

范围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底层的关键点:

  • 依赖ADL查找begin/end方法
  • 使用右值引用避免不必要的拷贝
  • 迭代器有效性要求与普通循环相同

4. auto与范围for的协同效应

4.1 最佳实践模式

//对于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&)。
  • 避免在遍历过程中修改容器结构(如添加/删除元素),否则可能导致未定义行为。

4.2 性能优化要点

  1. 避免隐式拷贝:

    for (auto x : huge_container) {}  // 拷贝开销
    for (const auto& x : huge_container) {} // 正确方式
    //使用const引用减少拷贝开销。
    
  2. 右值容器处理:

    for (auto&& x : get_temporary()) {}  // 延长临时对象生命周期
    
  3. 迭代器失效场景:

    std::vector<int> vec{1,2,3};
    for (auto& x : vec) {if (x == 2) vec.push_back(4); // 导致迭代器失效
    }
    

5. 性能分析

测试案例(循环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

结论:现代编译器对两种循环方式的优化能力相当,因此不用担心性能问题。

5. C++11容器相关

新容器

在这里插入图片描述

  • C++11STL新增了四个容器,分别是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++”时代的奠基石。如果你能熟练掌握这些特性,就已经站在了通往更高版本的桥梁上。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

http://www.dtcms.com/a/353439.html

相关文章:

  • Flutter 开发技巧 AI 快速构建 json_annotation model 的提示词
  • 使用git上传项目的步骤
  • 煤矸石检测数据集VOC+YOLO格式3090张2类别
  • This method can cause UI unresponsiveness if invoked on the main thread.
  • week5-[字符数组]数和
  • 【技巧】PostgreSQL自动转换类型 int转bool 转int
  • 苍穹外卖项目实战(日记十一)-记录实战教程及问题的解决方法-(day3-3)完善菜品分页查询功能
  • 怎么理解API?
  • 用户体验设计 | 什么是 AX?从 UX 到 AX 的演进
  • 数据结构——算法效率的度量(时间复杂度与空间复杂度)
  • Elasticsearch 启动反复重启排查实录:从“内存不足”到“vm.max\_map\_count 过小”
  • 图表可视化地理趋势-Telerik WPF Chart
  • 智能汽车制造:海康NVR管理平台/工具EasyNVR多品牌NVR管理工具/设备实现无插件视频监控直播方案
  • R语言贝叶斯方法在生态环境领域中的技术应用
  • 攻克 Java 分布式难题:并发模型优化与分布式事务处理实战指南
  • APP与WEB测试的区别?
  • 人工智能在医疗领域中辅助外科手术的应用综述
  • 【VSCode】使用VSCode创建Java C/S架构项目
  • 如何用Renix实现网络测试自动化: 从配置分离到多厂商设备支持
  • 【网络编程】NtyCo协程服务器的框架(轻量级的协程方案,人称 “小线程”)
  • 从浏览器无法访问到Docker容器的 FastAPI 服务地址【宿主机浏览器和容器不在同一个网络层面:端口映射】
  • 前端AI应用实践指南:从基础概念到高级实现
  • 云手机的未来发展怎么样?
  • 数据结构(C语言篇):(二)顺序表
  • 状态设计模式
  • 手机冻结技术发展时间轴
  • Flutter项目详解
  • 深度学习实战117-各种大模型(Qwen,MathGPT,Deepseek等)解高考数学题的应用,介绍架构原理
  • C++工程实战入门笔记6-函数(三)关于base16编码的原理和函数模块化实战
  • LINUX --- 网络编程(二)