C++11语法糖:auto和范围for循环详解

devtools/2025/3/14 22:20:40/

C++11语法糖:浅谈auto和范围for循环

  • C++11语法糖:浅谈auto和范围for循环
  • github地址
  • 前言
  • 一、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)
  • 二、基于范围的for循环
    • 1. 语法
      • 1.1 C++98中遍历数组的方式
      • 1.2 C++11范围for循环
    • 2. 范围for的使用条件
    • 3. 范围for语法糖的底层实现
    • 4. auto与范围for的协同效应
      • 4.1 最佳实践模式
      • 4.2 性能优化要点
    • 5. 性能分析
    • 结语

C++11语法糖:浅谈auto和范围for循环

github地址

有梦想的电信狗

前言

本文简单介绍C++11中的语法糖:auto和范围for循环的使用

一、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. 函数退化为函数指针

二、基于范围的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

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

结语

现代C++通过auto和范围for循环的组合,显著提升了代码的表达能力和可维护性。在实践中需注意:

  1. 平衡类型明确性与代码简洁性
  2. 理解底层实现机制以避免误用
  3. 结合新特性持续优化代码质量

以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流

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


http://www.ppmy.cn/devtools/166761.html

相关文章

【MySQL篇】MySQL内置函数

目录 1&#xff0c;日期函数 2&#xff0c;字符串函数 3&#xff0c;数学函数 4&#xff0c;其他函数 实战OJ 1&#xff0c;日期函数 日期类型在之前文章【数据类型】中有描述 传送门&#xff1a;【MySQL篇】数据类型_mysql 数据类型-CSDN博客 函数名称描述current_dat…

Shell编程:深入了解 Bash 数组操作

Bash 脚本是一种强大的工具&#xff0c;广泛用于自动化任务和处理系统管理操作。数组作为 Bash 脚本中的重要数据结构&#xff0c;能够帮助开发人员高效地管理和操作多个值。本文将详细介绍 Bash 数组的创建、访问、修改和常见操作技巧&#xff0c;帮助你在脚本编写中更加得心应…

Maven工具基础知识(一)

第一章、Maven概述 一、概述 官网地址&#xff1a;Welcome to Apache Maven – Maven Maven是一个基于Java的项目管理工具&#xff0c;专注于项目构建、依赖管理和项目信息标准化。其核心目标 是简化开发流程&#xff0c;通过标准化项目结构和自动化构建流程&#xff…

docker拉取 sentinel 并启动

拉取镜像 docker pull bladex/sentinel-dashboard:latest # 默认拉取最新版启动镜像 访问 账号 密码都是默认的 sentinel

用Qt手搓AI助手,挑战24小时开发DeepSeek Assistant!

一、项目需求分析与技术选型 DeepSeekAssistant是一款基于深度求索&#xff08;DeepSeek&#xff09;API的智能对话助手&#xff0c;核心需求包括&#xff1a; 用户界面友好&#xff1a;支持多轮对话展示数据持久化&#xff1a;历史记录存储与检索异步网络通信&#xff1a;AP…

2025年汇丰控股投行业务扩张战略升级版 笔记

2025年汇丰控股投行业务扩张战略升级版 一、战略重构&#xff1a;后疫情时代的全球投行突围战 2025年12月&#xff0c;汇丰控股&#xff08;HSBC Holdings&#xff09;宣布升级其投行业务扩张计划&#xff0c;拟将投资总额从30亿美元提升至35亿美元&#xff0c;并将招聘规模扩…

4.3 数组和集合的初始及赋值

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的 版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商…

火绒企业版V2.0全面支持Linux与国产化系统!免费试用助力国产化终端安全升级

国产化浪潮下的安全新挑战 随着信创产业的加速推进&#xff0c;国产操作系统&#xff08;统信UOS、麒麟OS等&#xff09;和ARM架构服务器逐步成为政企核心业务的基础设施。然而&#xff0c;针对国产化系统的勒索攻击、网页篡改、供应链漏洞等威胁频发&#xff0c;传统安全方案…