ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

c++string类(auto/范围for/迭代器)

c++string类(auto/范围for/迭代器) String类为什么学习string类C语言中字符串是以’\0’结尾的一些字符的集合为了操作方便C标准库中提供了一些str系列的库函数但是这些库函数与字符串是分离开的不太符合OOP的思想而且底层空间需要用户自己管理稍不留神可能还会越界访问。auto和范围forauto关键字在这里补充2个C11的小语法方便我们后面的学习。在早期C/C中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量后来这个不重要了。C11中标准委员会变废为宝赋予了auto全新的含义即auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期推导而得。用auto声明指针类型时用auto和auto*没有任何区别但用auto声明引用类型时则必须加当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错因为编译器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量。auto不能作为函数的参数可以做返回值但是建议谨慎使用auto不能直接用来声明数组auto的原理是什么?一句话概括auto的本质是“编译时的类型推导引擎”它复用了 C 模板中“模板参数推导”的完整规则在编译期根据初始化表达式确定类型且完全不引入任何运行时开销**。要想真正理解auto不能把它简单理解为“弱类型语言中的 var”而是要把它看作编译器在编译期执行的一段“类型匹配算法”。下面我把它的核心推导规则掰开揉碎并结合你之前问的“范围 for”给你讲透。1. 核心原理auto等价于模板中的T当你写下autoxexpr;编译器内部会将其视为一个隐式的模板函数调用templatetypenameTvoidfunc(T param){...}func(expr);// 此时 T 被推导为什么类型auto x 就是什么类型结论auto的类型推导规则和函数模板templatetypename T void func(T t)中对形参T的推导规则完全一致。2. 三大核心推导规则面试常考根据你声明的形式不同推导规则分为以下三类这是理解auto最关键的地方规则一auto不带修饰——丢弃引用、丢弃顶层 const/volatile这对应模板中的T传值。如果初始化表达式是引用丢弃引用。如果初始化表达式有顶层const或volatile丢弃它们底层 const 保留。constinta10;constintrefa;autoxa;// int丢弃顶层 constautoyref;// int丢弃引用 和 顶层 constconstint*constptra;autozptr;// const int* 指针本身是顶层 const 被丢弃但指向的 const int 是底层 const保留规则二auto左值引用——保留引用和所有 cv 限定符这对应模板中的T。必须用左值初始化。保留原始的 cv 限定符const/volatile 全部保留。constinta10;constintrefa;autoxa;// const int保留 constautoyref;// const int保留引用和 const// auto z 10; // 编译错误10 是右值不能绑定到非常量左值引用规则三auto万能引用——依据初始化表达式推导为左值或右值引用这对应模板中的T转发引用/万能引用。如果初始化表达式是左值推导为左值引用T。如果初始化表达式是右值推导为右值引用T。inta10;autoxa;// int 左值引用autoy20;// int 右值引用constautoza;// const int 注意加 const 会改变推导结果3. 特殊场景大括号初始化列表{}auto对大括号有特殊处理这点和模板略有不同需要单独记忆autox{1,2,3};// std::initializer_listint C11 起autoy{1};// int C17 起直接列表初始化变成单值autoz{1};// std::initializer_listint C11 起注意模板templatetypename T void func(T param)遇到func({1,2,3})会编译失败但auto可以推导出initializer_list这是auto独有的特殊规则。4. 函数返回值中的autoC14 起原理完全一样编译器根据return语句推导类型autofunc(){return10;// 推导为 int}但不能用于虚函数因为是编译期确定的且函数内所有return语句必须推导出完全一致的类型。5. 结合你之前的“范围 for”来看贯通理解范围for的展开代码中第一步就是auto __range vec;。为什么要用auto 而不是auto如果vec是左值普通变量auto 推导为vec左值引用不产生拷贝。如果vec是临时对象如getVector()auto 推导为getVector()的右值引用延长临时对象的生命周期。这就是auto 万能引用完美解决“左右值通用且零拷贝”问题的绝佳体现。6. 经典致命陷阱vectorbool的代理类这是auto最常见翻车的地方理解了就能秒杀面试题std::vectorboolflags{true,false};autobflags[0];// 推导类型并不是 bool因为vectorbool为了节省空间operator[]返回的是一个临时代理类std::vectorbool::reference能隐式转换为bool。使用auto会原样保留这个代理类如果后续flags被修改或销毁这个代理类就会变成悬垂引用导致未定义行为。正确写法显式指定类型bool b flags[0];或auto b static_castbool(flags[0]);。7. 总结一下底层逻辑场景推导规则的本质适用例子auto传值推导剥离引用/顶层 cv需要副本时auto左值引用推导保留 cv修改原对象避免拷贝auto万能引用推导完美转发泛型编程、范围 for 循环auto{}特殊处理为initializer_list初始化列表场景记住核心口诀auto只是编译器在编译期“填空”的占位符它不关心运行时只关心初始化表达式在编译期的类型结构。这使它成为 C 泛型编程中“零成本抽象”的基石之一。范围for对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因此C11中引入了基于范围的for循环。for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围自动迭代自动取数据自动判断结束。范围for可以作用到数组和容器对象上进行遍历范围for的底层很简单容器遍历实际就是替换为迭代器这个从汇编层也可以看到范围for的本质是什么?范围forRange-basedforloopC11 引入的本质就是一种语法糖。它在编译时会被编译器展开成一个基于迭代器的传统for循环。为了让你看透它的底裤我把标准规定的展开逻辑用代码“扒”给你看。1. 最核心的本质C17 标准之前假设我们写了一个最普通的范围forstd::vectorintvec{1,2,3};for(intx:vec){std::coutx;}编译器会将其展开为类似于下面的代码{auto__rangevec;// 1. 万能引用绑定到范围表达式auto__begin__range.begin();// 2. 获取起始迭代器auto__end__range.end();// 3. 获取结束迭代器哨兵for(;__begin!__end;__begin){// 4. 循环条件与自增intx*__begin;// 5. 解引用并赋值/初始化给循环变量std::coutx;}}2. 层层拆解这个展开过程这才是真正的本质(1) 范围推导auto __range vec;这里使用了万能引用转发引用这保证了如果vec是左值__range是左值引用不会发生拷贝。如果vec是临时对象右值__range会延长临时对象的生命周期避免悬垂引用。(2) 迭代器的获取begin()和end()的查找规则编译器不是傻傻地只调用成员函数。C 标准规定编译器会按以下优先级查找begin和end优先调用成员函数如果容器有.begin()和.end()就用它们。退而求其次ADL 参数依赖查找如果没有成员函数编译器会尝试调用全局的begin()和end()函数比如数组特化版本或者你自己写的全局函数。这一点让原生数组也能使用范围forintarr[]{1,2,3,4};for(intx:arr){...}// 展开时找不到 .begin()但找到了全局的 std::begin(arr)C11 提供// 它返回指向 arr[0] 的指针指针就是天然的迭代器。(3) 循环变量的初始化严格等价于auto推导注意展开代码中的int x *__begin;。这里的初始化和你的声明形式强相关你写的声明本质展开赋值/初始化效果for (int x : vec)int x *__begin;拷贝元素修改x不影响容器for (auto x : vec)auto x *__begin;拷贝元素for (auto x : vec)auto x *__begin;引用原元素修改x会修改容器for (const auto x : vec)const auto x *__begin;只读引用避免拷贝for (auto x : vec)auto x *__begin;万能引用完美转发元素常用于模板3. C17 和 C20 的重要“补丁”理解了上面的本质再看新增的特性就一目了然了C17 强化了范围推导将__range的推导和begin/end的获取分离保证了begin/end在表达式求值时的顺序安全防止范围表达式是临时对象且begin带有副作用时出错。C20 引入初始化语句允许你在范围for中定义初始化变量。for(autovecgetVector();autoelem:vec){...}本质就是把auto __range getVector();放到了for的小括号里生命周期不变。4. 本质背后的“坑”面试高频考点理解了本质下面这些坑你就能轻松避开了不要边遍历边修改导致迭代器失效的容器比如vector的push_back。因为展开代码中__begin是固定的插入元素导致内存重新分配后__begin立即失效继续必定崩溃。临时对象的只读问题for(intx:getVector()){...}// OK__range 延长了临时对象生命周期for(intx:getVector()){...}// 编译错误因为 __range 是右值引用不能绑定非常量左值引用但展开代码中试图将 __range 的元素绑定给 int逻辑上不合理。指针作为迭代器原生数组的展开中__begin和__end是指针指针支持!比较和自增完美适配。总结一句话范围for的本质 编译器帮你自动写了auto 定义范围 自动调用begin/end获取迭代器 自动维护循环条件!和自增 自动将解引用后的值赋给你的循环变量。它没有引入任何运行时开销开启-O2优化后性能与手写迭代器循环完全一致它只是让你的代码更简洁、更安全避免越界和手写end()的失误。c迭代器怎样使用?C 迭代器Iterator是连接容器和算法的桥梁它提供了一种统一的方式来访问容器中的元素而无需关心容器的底层实现。迭代器的行为类似指针支持解引用*和递增等操作。下面从“如何获取”、“如何遍历”、“如何操作”、“常见注意事项”四个方面来介绍迭代器的使用并附上代码示例。1. 如何获取迭代器每个标准容器如vector、list、map等都提供成员函数来返回迭代器成员函数返回类型说明begin()iterator指向第一个元素end()iterator指向最后一个元素之后的位置哨兵cbegin()const_iterator只读迭代器指向第一个元素cend()const_iterator只读迭代器指向末尾之后rbegin()reverse_iterator反向迭代器指向最后一个元素rend()reverse_iterator反向迭代器指向第一个元素之前#includevector#includeiostreamintmain(){std::vectorintvec{1,2,3,4,5};autoitvec.begin();// 普通迭代器autocitvec.cbegin();// 只读迭代器autoritvec.rbegin();// 反向迭代器从后向前// 使用迭代器访问元素std::cout*itstd::endl;// 输出 1std::cout*citstd::endl;// 输出 1std::cout*ritstd::endl;// 输出 5return0;}2. 如何遍历容器(1) 使用for循环经典方式std::vectorintvec{10,20,30,40};// 正向遍历for(autoitvec.begin();it!vec.end();it){std::cout*it ;}// 反向遍历for(autoitvec.rbegin();it!vec.rend();it){std::cout*it ;}(2) 使用for循环 const_iterator只读for(autoitvec.cbegin();it!vec.cend();it){// *it 100; // 错误不能修改std::cout*it ;}(3) 使用范围for循环C11 起底层依赖迭代器for(intx:vec){std::coutx ;}// 等价于使用 begin()/end() 迭代器(4) 使用std::for_each算法#includealgorithmstd::for_each(vec.begin(),vec.end(),[](intx){std::coutx ;});3. 迭代器的常见操作迭代器支持的操作取决于其类型输入、输出、前向、双向、随机访问。大多数容器如vector、deque提供随机访问迭代器而list、set提供双向迭代器。操作说明适用于*it解引用获取元素所有迭代器it-member通过指针访问成员用于对象所有迭代器it/it前进到下一个元素所有迭代器--it/it--后退到前一个元素双向/随机访问迭代器it n前进 n 个元素随机访问迭代器it - n后退 n 个元素随机访问迭代器it n返回偏移 n 后的迭代器随机访问迭代器it - n返回偏移 -n 后的迭代器随机访问迭代器it1 - it2计算两个迭代器的距离元素个数随机访问迭代器it1 it2比较迭代器位置随机访问迭代器示例随机访问迭代器std::vectorintvec{1,2,3,4,5};autoitvec.begin();it2;// 指向 3std::cout*itstd::endl;// 3autoit2it1;// 指向 4std::cout*it2std::endl;// 4std::cout(it2-it)std::endl;// 1对于list双向迭代器不支持、等随机访问操作只能使用和--。4. 注意事项与常见陷阱(1) 迭代器失效当容器发生插入或删除元素时原有的迭代器可能失效变得不可用。vector插入/删除可能导致所有迭代器失效因为可能重新分配内存。list插入/删除不会使指向其他元素的迭代器失效只影响被删除的迭代器。map/set插入/删除不影响其他迭代器。std::vectorintvec{1,2,3};autoitvec.begin();vec.push_back(4);// 可能重新分配it 失效// *it 10; // 危险未定义行为解决办法插入/删除后重新获取迭代器或使用能返回新迭代器的函数如insert、erase返回有效的迭代器。autoitvec.begin();itvec.insert(it,0);// insert 返回指向新元素的迭代器itvec.erase(it);// erase 返回指向被删元素之后元素的迭代器(2) 不要混用不同类型的迭代器iterator和const_iterator可以比较但不能将const_iterator赋值给iterator除非用const_cast但不推荐。(3) 不要解引用end()或rend()end()指向末尾之后的哨兵位置解引用它是未定义行为。autoitvec.end();// *it; // 错误(4) 范围遍历时注意边界使用!比较迭代器而非因为只有随机访问迭代器支持而!所有迭代器都支持。// 通用写法for(autoitvec.begin();it!vec.end();it){...}(5) 使用auto简化声明C11 起推荐使用auto避免写出冗长的迭代器类型名。5. 迭代器与算法的配合标准库算法如std::find、std::sort、std::copy通常接受两个迭代器表示范围。#includealgorithm#includevectorstd::vectorintvec{5,2,8,1,9};// 查找元素autoitstd::find(vec.begin(),vec.end(),8);if(it!vec.end()){std::coutFound at position: (it-vec.begin())std::endl;}// 排序需要随机访问迭代器std::sort(vec.begin(),vec.end());// 复制到输出流std::copy(vec.begin(),vec.end(),std::ostream_iteratorint(std::cout, ));总结使用begin()/end()获取迭代器用*解引用用前进。根据容器类型选择合适操作随机访问 vs 双向。注意插入/删除时的迭代器失效问题及时更新迭代器。范围for循环是简洁的遍历方式但明确使用迭代器能提供更灵活的控制。迭代器是 STL 的核心掌握它就能轻松使用各种算法和容器。
返回列表