C++(入门篇)(下)

📅 2026/8/5 22:17:25
C++(入门篇)(下)
欢迎阅读 欢迎来到「C 入门篇」讲解本文将带你从 C 语言的“历史包袱”出发逐步深入理解引用、auto 类型推导、基于范围的 for 循环和 nullptr这四个让 C 代码更简洁安全的现代特性。在开始之前建议你先了解背景知识C 语言中的指针和类型推导存在诸多隐患如野指针、类型冗长、容器遍历繁琐。C11 引入的这组特性旨在提升代码的可读性、安全性和简洁性让程序员更关注业务逻辑少被底层语法细节困扰。明确学习目标掌握以下四个核心知识点——①引用理解引用与指针的本质区别掌握引用作为函数参数和返回值的用法以及引用绑定的规则②auto 关键字C11掌握 auto 的类型推导机制学会用 auto 简化冗长类型声明并注意 auto 与 const、引用配合时的推导规则③基于范围的 for 循环C11学会用简洁语法遍历数组和容器了解如何通过引用修改元素并熟悉其底层迭代器原理④指针空值 nullptrC11明确 nullptr 与 NULL 的区别理解 nullptr 如何解决重载时的二义性问题并将其作为现代 C 中表示空指针的唯一标准。一.引用1.1 引用的概念引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。比如李逵在家称为铁牛江湖上人称黑旋风其中铁牛和黑旋风都是他的称号格式类型 别名 引用对象void TestRef() { int a 10; int ra a; //定义引用类型 printf(%p\n, a); printf(%p\n, ra); }要点引用类型必须和引用实体是同种类型的1.2 引用的特性引用在定义时必须初始化一个变量可以有多个引用引用一旦引用一个实体则不能在引用其他实体这也是引用不能完全替代指针的原因不产生空间语法上void TestRef() { int a 10; // int ra; // 该条语句编译时会出错 int ra a; int rra a; printf(%p %p %p\n, a, ra, rra); }1.3 常引用常引用的出现首先引用的权限引用的对象因此const对象想要引用它那么权限应该const因此不加const会报错权限只能缩小或平移不能放大常引用不能改变相对的但是可以通过引用对象改变而改变。int main() { int a 100; const int b a; b 20;//报错因为常引用不能修改 a 200; //b也应该等于200虽然常引用不能改变但是可以通过改变引用对象从而改变常引用 const int c 100; const int b c; //那么此时常引用才能真正算是真正不能改变 }权限变化要点常数隐式类型转换和返回值产生临时变量都带有常性要用常引用void TestConstRef() { const int a 10; //int ra a; // 该语句编译时会出错a为常量 const int ra a; // int b 10; // 该语句编译时会出错b为常量 const int b 10; double d 12.34; //int rd d; // 该语句编译时会出错类型不同 const double rd d; }1.4 使用场景做参数void Swap(int left, int right)//想通过形参修改实参 { int temp left; left right; right temp; } int Add(const int left, const int right)//只想节省空间不想通过修改形参改变实参 { return left right; }做返回值int Count() { static int n 0; n; return n; }要点做参数此时在函数体里面对形参改变会影响实参。做返回值如果返回对象是局部的话会导致出了作用域该对象被清空因此出现引用的对象是随机的所以当引用对象是局部的话要采用值返回。1.5 引用作用①修改返回值②提升效率减少拷贝1.6 引用和指针的区别引用不需要开空间指针需要开空间引用在定义时必须初始化指针没有要求引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体没有NULL引用但有NULL指针在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小有多级指针但是没有多级引用访问实体方式不同指针需要显式解引用引用不需要引用更安全二.auto关键字C11随着程序越来越复杂程序中用到的类型也越来越复杂经常体现在1.类型难于拼写2.含义不明确导致容易出错auto关键字我们这里讲的是C11在之前的版本auto关键字有别的作用至于有什么作用其实不太关键因为如果关键的话也不会在C11进行修改了作用可以识别右边表达式的类型格式auto 变量 表达式int main() { int a 10; auto b a; auto c a; auto d TestAuto(); }要点auto定义变量必须对其初始化auto不能作函数参数数组类型同一行声明多个变量void TestAuto() { auto a 1, b 2; auto c 3, d 4.0; // 该行代码会编译失败因为c和d的初始化表达式类型不同 } // 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) {} void TestAuto() { int a[] {1,2,3}; auto b[] {456}; }auto声明指针类型时auto和auto*无区别但用auto声明引用必须要加int main() { int x 10; auto a x; auto* b x; auto c x; cout typeid(a).name() endl; cout typeid(b).name() endl; cout typeid(c).name() endl; *a 20; *b 30; c 40; return 0; }三.基于范围的for循环C11在C11以前中如果要遍历一个数组可以按照以下方式进行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; }对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因 此C11中引入了基于范围的for循环。for循环后的括号由冒号“”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。格式for类型 变量数组名void TestFor() { int array[] { 1, 2, 3, 4, 5 }; for(auto e : array) e * 2; for(auto e : array) cout e ; return 0; }我们通常用auto来推导是什么类型注意与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环3.1范围for的使用条件for循环迭代的范围必须是确定的对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。不过这是后面要讲的内容先了解一下即可注意以下代码就有问题因为for的范围不确定void TestFor(int array[]) { for(auto e : array) cout e endl; }迭代的对象要实现和的操作。(关于迭代器这个问题以后会讲现在提一下没办法 讲清楚现在大家了解一下就可以了)四.指针空值nullptr(C11)在C11之前指针空值都是由NULL充当但是在良好的C/C编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现不可预料的错误比如未初始化的指针。如果一个指针没有合法的指向我们基本都是按照如下方式对其进行初始化void TestPtr() { int* p1 NULL; int* p2 0; }NULL实际是一个宏在传统的C头文件(stddef.h)中可以看到如下代码#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif可以看到NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量。不论采取何种定义在使用空值的指针时都不可避免的会遇到一些麻烦比如void f(int) { coutf(int)endl; } void f(int*) { coutf(int*)endl; } int main() { f(0); f(NULL); f((int*)NULL); return 0; }程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的 初衷相悖。 在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void *)0。因此在C11中才急需一个只表示void*的关键字因此nullptr就出现了注意1.在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的。2. 在C11中sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。3.为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。 闭幕 恭喜你完成了 C11 核心语法特性的学习今天我们系统梳理了引用、auto 关键字、基于范围的 for 循环和指针空值 nullptr这四大实用特性。为了巩固知识并进一步拓展建议你动手实践在本地 IDE 或在线编译器中编写代码尝试不同的测试用例亲身体验每个知识点的用法。深入思考引用引用和指针有什么区别为什么引用在作为函数参数时比指针更安全什么是悬垂引用如何避免auto 关键字auto在什么情况下会推导出引用类型auto和auto有什么区别auto能用于函数返回值或形参吗基于范围的 for 循环基于范围的 for 循环底层原理是什么如果遍历容器时修改元素应该用auto还是auto遍历时删除元素会有什么问题指针空值 nullptrnullptr和NULL有什么区别为什么 C11 引入nullptr而不继续用NULLnullptr的类型是什么延伸挑战编写一个函数使用引用交换两个整数的值对比传指针的实现方式。使用auto简化以下代码的变量声明vectorstring v; mapint, vectorstring::iterator it; pairint, string p;。使用基于范围的 for 循环遍历vectorint将所有元素乘以 2 并输出要求修改原数组。解释以下代码的问题并修复int* p NULL; if (p nullptr) { delete p; }。如果你觉得本文对你有所帮助欢迎 点赞 / 收藏 关注作者获取更多题解 留言交流你的疑问或优化思路深入思考参考答案引用引用必须初始化且不能改变绑定对象更安全指针可以为空且可重新指向。悬垂引用指引用绑定的对象已被销毁应避免返回局部变量的引用。auto 关键字auto推导出值类型去除引用和 constauto推导引用类型。auto不能用于函数形参但可用于返回值C14 起。基于范围的 for 循环底层使用迭代器。修改元素应使用auto遍历时删除元素会导致迭代器失效应避免。指针空值 nullptrnullptr是std::nullptr_t类型可隐式转换为任意指针类型NULL是宏通常为0可能被误匹配为int重载。️延伸挑战参考解法1. 引用交换整数对比传指针/* * 功能演示引用作为函数参数的使用 * 引用版本简洁、安全直接操作原变量 * 指针版本需要解引用调用时需取地址 */ #include iostream using namespace std; // ----- 使用引用交换推荐 ----- // 形参为引用直接绑定实参无需拷贝 void SwapByRef(int a, int b) { int temp a; // 交换两个变量的值 a b; b temp; } // ----- 使用指针交换对比 ----- // 形参为指针需要解引用操作调用时需传地址 void SwapByPtr(int* a, int* b) { int temp *a; // 通过指针解引用访问值 *a *b; *b temp; } int main() { int x 10, y 20; // 引用版本直接传变量简洁直观 SwapByRef(x, y); cout After SwapByRef: x x , y y endl; // 指针版本需传地址 SwapByPtr(x, y); cout After SwapByPtr: x x , y y endl; return 0; }2. auto 简化变量声明/* * 功能演示 auto 关键字的类型推导 * 用 auto 简化冗长的类型声明提高代码可维护性 */ #include iostream #include vector #include map #include string using namespace std; int main() { // ----- 原始写法类型冗长 ----- vectorstring v {apple, banana, cherry}; // 迭代器类型写起来很长 vectorstring::iterator it1 v.begin(); mapint, vectorstring m; m[1] {hello, world}; // 嵌套类型更复杂 mapint, vectorstring::iterator it2 m.begin(); pairint, string p {42, answer}; // ----- 使用 auto 简化推荐 ----- auto it3 v.begin(); // 自动推导为 vectorstring::iterator auto it4 m.begin(); // 自动推导为 mapint, vectorstring::iterator auto p2 make_pair(42, answer); // 自动推导为 pairint, string // 遍历输出验证 for (auto it v.begin(); it ! v.end(); it) { cout *it ; } cout endl; return 0; }3. 基于范围的 for 循环遍历并修改元素cpp/* * 功能演示基于范围的 for 循环的使用 * 重点修改元素必须用 autoauto 只会操作副本 * 同时演示只读场景用 auto 即可 */ #include iostream #include vector using namespace std; int main() { vectorint nums {1, 2, 3, 4, 5}; // ----- 错误示范使用 auto值拷贝无法修改原数组 ----- for (auto x : nums) // x 是 nums 中元素的副本 { x x * 2; // 修改的是副本原数组不变 } // ----- 正确示范使用 auto引用直接修改原元素 ----- for (auto x : nums) // x 是 nums 中元素的引用 { x x * 2; // 直接修改原数组元素 } return 0; }4. nullptr 与 NULL 的区别及正确使用pp/* * 功能演示 nullptr 和 NULL 的区别 * 重点nullptr 是类型安全的空指针NULL 本质是 0 * 场景函数重载时 nullptr 能正确匹配指针版本 */ #include iostream using namespace std; // ----- 重载函数用于演示 NULL 的二义性 ----- void func(int x) { cout call func(int): x endl; } void func(void* p) { cout call func(void*) endl; } int main() { // ----- 问题演示NULL 的二义性 ----- // func(NULL); // 编译警告/错误NULL 被当作 0匹配 func(int) // 因为 NULL 通常是 #define NULL 0所以优先匹配 int 版本 // ----- 正确使用nullptr 类型安全 ----- func(nullptr); // 明确匹配 void* 版本输出call func(void*) // ----- 正确使用delete 空指针是安全的 ----- int* p nullptr; // 推荐使用 nullptr 初始化 if (p ! nullptr) { delete p; // 但 p 为 nullptr不会执行 delete } // 直接 delete nullptr 也是安全的无需 if 判断 delete nullptr; // 安全delete 空指针不执行任何操作 // ----- 对比 NULL 可能带来的问题 ----- int* q NULL; // 旧写法可能被误认为 0 // delete q; // 虽然安全但 NULL 的类型不明确 return 0; }补充说明C11 引入nullptr后应始终使用nullptr代替NULL避免重载决议时的二义性提高代码的类型安全性。在delete空指针时C 标准保证delete nullptr是安全的不需要额外的if判断。