)
系列文章目录精学C系列1 精学C第一站感谢佬们支持目录系列文章目录前言一、C关键字二、命名空间命名空间的嵌套命名空间的名字命名空间的使用三、C输入输出四、缺省参数概念缺省参数的分类1 全缺省(什么都不传)2 半缺省(传几个且不全传)3 全传五、函数重载定义规则原理六、引用引用特性引用的应用做参数做输出型参数做返回值小总结常引用const引用/右值引用指针和引用的区别底层七、内联函数延伸八、auto关键字、范围for例子范围forauto使用细则auto与指针引用结合起来使用在同一行定义多个变量auto不能推导的类型auto不能作为函数的参数也不能用来声明数组九、nullptr总结前言(C的博客也是拖着一直没写)C的学习是非常漫长且艰辛的,作为最早的面向对象的语言,会有一些语法设计上的坑;而其他语言由于发展较晚,在设计语法时便可以规避这些坑.就我而言,C的学习相比C,数据结构需要另一种思路:简单的语法学习后,更多的要学习思想:面向对象的封装继承多态;还有对STL的学习,要模拟实现,学习接口,还有模板等等.多的不说,我们直接开始!一、C关键字早期的C在关键字上比较C语言没有增加太多毕竟最开始只是为了在兼容C语言的基础上来解决一下遗漏的一些问题。C98/C03 关键字共63个从C语言继承的关键字32个autoC98中含义不同C11后重新定义breakcasecharconstcontinuedefaultdodoubleelseenumexternfloatforgotoifintlongregisterreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhileC新增的关键字31个asm汇编语句boolcatchclassconst_castdeletedynamic_castexplicitfalsefriendinlinemutablenamespacenewoperatorprivateprotectedpublicreinterpret_caststatic_casttemplatethisthrowtruetrytypeidtypenameusingvirtualwchar_texportC98/03有但很少用C11 新增关键字10个alignas对齐说明符alignof对齐查询char16_t16位字符类型char32_t32位字符类型constexpr常量表达式decltype类型推导noexcept异常说明符nullptr空指针常量static_assert静态断言thread_local线程局部存储注意auto在C语言和C98中是存储类说明符表示自动存储期在C11中重新定义为类型推导关键字。register在C11中被弃用C17中移除。export在C98/03中用于模板分离编译但实现困难C11中已弃用。C11还引入了final和override作为上下文关键字不是保留关键字但有特殊含义。从整体来看,早期的c在设计关键字还是相对保守的学完这些关键字我们就会发现,他们还是好理解的只不过从C11之后,新功能就变多了,比如constexpr,exchange,quoted等,你说他没用吧,学了之后发现确实设计出来有用,但是感觉用的不多如果你想稍微学学,万一用的时候再查,不深入学就有点记不住不学的话感觉又很落伍 C都快蜕变成一个新语言了不过这都是学习到后期才该考虑的东西了二、命名空间在学习C语法之前我们要对C的语法有一个总体的认知就是C的新语法大多都是解决C语言的遗留问题或者对C语言进行优化的有了这个认知之后我们的学习思路便会清晰很多命名空间解决的是命名冲突的问题。例int rand0; int main() { printf(%d,rand); return 0; }结果为0倘若我们引出头文件stdlib.h的话就会是这样redefinition重定义 所以是因为变量名rand和库函数rand冲突了所以报错所以对于命名冲突问题我们会遇到这么几种情况1 我们定义的函数可能会和库里的有冲突2 进入公司做项目后/或者和别的同学协同开发时由于项目可能会很庞大每个人和每个人对函数、变量等命名可能会相同。C语言无法解决除非是换名字这样的话名字就会变得冗长还不易协作于是引出命名空间namespace把变量包进去#includestdlib.h #includecstdio namespace Sun { int rand0; } int main() { printf(%d,rand); return 0; }但是打印出来却不是0我们补充一个问题在C/C中两个大括号框起来的区域就是一个域。默认的是全局域函数内部为函数域命名空间内部为命名空间域。而命名空间域本质其实是全局域所以rand本质也是一个全局变量会放到静态区对于域来说要么影响生命周期要么则影响访问对于搜索变量定义来说优先级是这样的首先找局部域再找全局域如果不指定命名空间域或者展开命名空间他就不主动进去寻找所以弄来弄去最后这个rand是在main函数的局部域找的由于包了stdlib的头文件所以rand对应的是那个随机数函数打印是函数名也就是函数的地址咯要想达到目的所以只能这样#includestdlib.h #includecstdio namespace Sun { int rand0; } int main() { printf(%d,Sun::rand); return 0; }指定了命名空间域就好啦就可以打印出0命名空间亦可以定义函数类型。因为这些都有可能冲突#includeiostream using namespace std; namespace SunS { int rand10; int Add(int a,int b) { return ab; } struct Node { int val; struct Node* next; }; } int main() { coutSunS::randendl; coutSunS::Add(1,2); SunS::Node head; head.val0; head.nextNULL; }命名空间还可以嵌套namespace N1 { int a; int b; int Add(int left,int right) { return leftright; } namespace N2 { int c; int d; int Sub(int left,int right) { return left-right; } } } int main() { cout加法 N1::Add(1,2)endl; cout减法 N1::N2::Sub(3,1)endl; }图示代码在N1命名空间中又嵌套了另一空间N2所以调用的时候也要逐层的调用命名空间的名字命名空间的名字是可以相同的如果名字相同的话不同地方的两个命名空间会合并到一个命名空间。命名空间的使用1 加命名空间名称及作用域限定符能做到最好的命名隔离但是它使用不方便例#includeiostream int main() { std::coutxxx; }每次使用命名空间域里的内容都要加上限定符非常麻烦但是非常安全。2 全部展开最无脑的做法 用起来方便但有风险全部展开 即展开全局了 隔离实效了。所以这个行为是慎用的但平时是无所谓的。例#includeiostream using namespace std; int main() { coutxxx; }3 单独展开某一个其他的不展开用于展开命名空间中常用using std::cout;相比前两种这种做法又没有风险又方便。一般我们只展开最常用的展开三、C的输入输出首先我们得先来介绍一下C的新的头文件规则我们知道c语言的风格是以.h结尾的当开始C后祖师爷为了解决臃肿的头文件格式就改成了不带.h的简洁版而对于旧的c语言头文件也给了一种新的格式即c开头的。例如cmath最清晰的格式见如下图摘自第六版C primer plus#includeiostream using namespace std; int main() { coutxxx; }但是为什么包了头文件还必须展开命名空间呢因为c库的实现放在了std命名空间里这个库也是c标准库。下来我们开始正式介绍C的输入输出简单来说cout printfcin scanfendl \n类比下来是这样coutxxxendl; printf(xxx\n);正式介绍流插入操作符(看着很像移位操作符?)当然相对于printfcout和还是有优势的就是对于不同类型不用写对应的格式也就是自动识别类型他的底层我们暂且不论。也就是如下操作int i10; double d3.14; couti dendl; printf(%d %lf,i,d);不过显然如果想控制小数位数这类“定制输出”的话C语言会比C方便很多对应我们有cinid;当然cin也可以自动识别类型。四、缺省参数概念所谓缺省参数是声明/定义函数时为函数的参数指定的一个缺省值就是默认值如果我们传入了值就用这个值否则就用缺省的默认值例void func(int a) { coutaendl; } int main() { func(1); }如果我们不想传参数 就在形参后面给一个缺省void func(int a0) { coutaendl; } int main() { func(); }缺省参数的分类例#includeiostream #includecstdio using std::cout; using std::endl; void func(int a10,int b20,int c30) { couta b cendl; } int main() { return 0; }1 全缺省什么都不传所谓全缺省就是所有参数都用给定的缺省值一个都不传func();2 半缺省传几个且不全传半缺省的话就是传入部分参数其余的使用缺省参数。不过要注意这个传参顺序是固定的。不能指定只传某一个。毕竟解析参数是从右向左所以如果要使用缺省值的话必须从最右边的参数开始中间不能间隔。func(1);//a1 func(1,2);//a1,b23 全传func(1,23);//a1,b2c3另外缺省参数必须是常量/全局变量且带有缺省参数的函数不能在定义和声明中同时出现因为怕两个地方不一样例如test.h中void func(int a10);test.cppvoid func(int a20) {}五、函数重载函数重载这里要理解为一个函数有多种功能。但是按照底层原理来看就是多个函数分别实现了不同功能但是他们有相同的函数名定义具体定义是这样的C限定了在同一作用域中可以有几个功能相似的同名函数这些函数只要在以下三个里面有一个满足即构成函数重载规则1 类型不同void add(int a,int b) { coutabendl; } void add(double a,double b) { coutabendl; } int main() { add(1,2); add(1.1,2.2); return 0; }这里同时控制参数的类型不同别的都相同2 参数个数不同int A(int a) { return a; } int A() { return 1; } int main() { coutA()endl; coutA(2)endl; }3 参数的顺序不同void f(int a,char b) { coutint a,char bendl; } void f(char b,int a) { coutchar b,int aendl; } int main() { f(10,c); f(c,10); return 0; }相同的参数改变他的顺序也可以构成重载。注意返回值不同不能构成重载因为我们调用的时候如果不接受返回值,那就无法区分两个函数了例:给定int f(double j) void f(double j) f(1.1) f(1.1)再来一个容易混淆的点。如果是以下这种情况还能构成重载吗都是带有缺省参数的情况1 一个是参数a,一个是参数a并带有缺省参数void f(int a) { coutf(int b)endl; } void f(int a10) { coutf(int a)10endl; } int main() { f(10); f(); }两次调用都不能成功如果改成这样一个不带参数 另一个是有参数并加上缺省参数void f() { coutf(int b)endl; } void f(int a10) { coutf(int a)10endl; }如果传参f(5);不会报错,毕竟这里指定了需要参数,所以只能匹配第二个f如果不传参数的话这样两个函数都可以被匹配,还是不行的f();原理那么为什么可以这么做呢原理是什么?首先C一定不支持而C却支持简单来看,我们在调用一个函数时,一定会用到汇编的一个指令,那就是call或者bl例如如下代码int add(int a, int b) { return a b; } int main() { add(1,2); }bl __Z3addiibl是调用,后面那一坨是我们的函数名他的规则是这样的__Z这是名称修饰的前缀表示这是一个 C 函数非 C 语言函数所有 Itanium ABI 下 C 函数名的固定前缀。3add3表示函数名add的长度为 3 个字符add是原始函数名。ii表示函数的参数类型。每个i代表一个int类型参数所以ii表示该函数接受两个int类型参数。所以只要我们控制参数的个数、类型、顺序 就能生成不同的,被修饰后的函数名 调用起来自然就没有歧义啦bench 在这里可以理解为对这段汇编调用进行基准测试benchmark的简写用来验证不同重载函数在调用时的性能差异。六、引用引用算是又一个C引入的新东西由于指针的管理较为困难所以引入了引用。引用的核心就两个字取别名。他不是定义一个新的变量而是对旧的变量去啦一个别名编译器不会为引用变量开辟空间。他和他引用的变量占一块空间。例#includecstdio int main() { int a10; int ba; int *pb; printf(a:%p,b: %p,p: %p,a,b,p); }其中,b为引用变量,p为指针1 引用特性首先 引用在定义时必须初始化例:int a10; int b;其次 一个变量可以有多个引用 (也就是一个变量可以有多个别名)int a10; //a为变量 int ba; //b为引用 int cb; //c为引用当然 由于引用本质也是个变量 我们也可以给引用变量再起别名int db;一旦引用了某个实体 就不能再引用其他实体了int a10; //a为变量 int ba; //b为引用 int c20; //c为变量 bc;对于这一段代码的最后一行 C和java的理解是不一样的由于C的引用不能修改指向所以bc;的意思是把c的值赋值给b 也就是仍在基础变量的范畴 不涉及引用修改了b之后,由于b为a的别名 所以a也为20只不过由于是赋值 所以c的地址与a,b不同如图所示2引用的应用引用出现之后,还是可以给我们的编码方案提供一种新的选择的做参数举一个简单的例子 我们写一个交换两个变量值的函数swap之前我们写的话 要通过指针传参的时候也得传地址void Swap(int *a,int *b) { int temp*a; *a*b; *btemp; } int main() { int a0; int b1; Swap(a,b); couta bendl; }现在有了引用之后,我没可以直接用引用作为参数传参是也不用传地址 a1,b1直接作为a,b的别名 修改了a1,b1 主函数的a,b也能被修改void Swap(int a1,int b1) { int tempa; ab; btemp; } int main() { int a0; int b1; Swap(a,b); couta bendl; }同时你会发现 两次的函数都使用了同一个名字Swap,但是调用也不会有报错因为传引用传参和传指针传参构成了函数重载但是坏消息是 如果此时增加一个传值传参的版本虽然也不会报错但是Swap(a,b)两个函数都可以调用 会有调用歧义的问题输出型参数输出型参数是我们很熟悉的问题 最早是由于c语言函数只能返回一个值所以一旦我们想带出其他的值 需要传入一个指针变量作为输出型参数例如力扣有个题是这样的他的c语言借口模板是这样的其中 nums是原数组,numsSize ,其中returnSize是我们要返回数组的长度,也就是2(这个虽然原题没有要求 但是是力扣要求的)int* singleNumber(int* nums, int numsSize, int* returnSize) { }所以我们在返回数组之前,必须有一步就是 设定returnSize的值*returnSize2;当然有了C之后,我们就可以用引用了int* singleNumber(int* nums, int numsSize, int returnSize) { //…… returnSize2; }做返回值首先我们回顾传值返回Add函数返回了c,他返回的是真的c吗?不是 只是c的拷贝 因为这个c只是临时变量 出了函数作用域就会销毁 到时候c的值就丢了为了拿到c的值 所以编译器创建了一个c的拷贝那现在又有问题了 这个c的拷贝放在哪里?1 如果这个c比较小(4/8个字节) 那就会放到寄存器中2 如果c比较大 临时变量就会放在调用Add函数的函数栈帧中此时如果改成传引用返回由于返回值变成了c的别名 所以不会生成临时变量但是这其中又有了新的问题1 首先c依然是临时变量,出了作用域还是会销毁,那我们访问c的时候实际上是在访问已经销毁的空间2 访问之后 如果原来c的空间里的内容没有被清理 我们还是可以拿到正确的值但如果被清理了,那我们取到的就是一个随机值这个“清不清的行为”是取决于编译器实现的当然如果有更严格的编译器 你根本无法访问已经销毁的空间就算我们某次侥幸拿到了正确的值,也是很容易制造出问题的例如int a,b1; int retAdd(a,b); coutretendl; cout11111111111\n; coutretendl;我们通过一个cout,成功把c的空间覆盖了 所以ret也就变成了1小总结我们对以上知识做一下总结 再给一个例子 这样不仅可以加深我们的理解 面试的时候给面试官讲故事也比生涩的背书效果更好我们使用内存就像向房东租房子当你租完退房的时候,假设你在房子里面留了一件你的物品如果恰好没人来住(没有人来访问空间),或者看到这个物品没有动 那就没事但如果有人来占了房子,看见了这个物品,发现不是自己的,占了放了别的东西,你再来找你的东西假定新的租客没发现你,你也不能发现你的东西了那么这段话有什么用呢?所以我们得出以下规则如果函数返回时,出了函数作用域之后如果返回对象还在那么就可以使用传引用返回如果已经返回给系统了就必须使用传值返回目前我们可以先给一个不销毁的例子int count() { static int c0; c; //…… return c; }再次总结1 引用在传参数和传返回值在某些场景下,可以提升性能2 引用在传参和传返回值时做输出型参数和输出型返回值常引用我们先给两组代码int a10; int ba;这样是没问题的但是下面这样是不行的const int c10; int dc;(报错)为什么呢?这就要引出一种叫权限的概念权限可以缩小,权限可以平移,但是权限不能放大什么意思呢?我们拿代码来看看看第一段代码a是正常变量,能读能写,b也是正常的引用变量,所以用b当a的别名属于是权限平移看第二段代码c是一个const变量,只能读不能写;但是d是一个正常变量,能读能写,你让一个能读能写的正常变量去当const变量就是想把权限放大,这是不行的所以我们可以对这段代码做修改,变成权限平移const int c10; const int dc;我们再给一段权限放大的代码众所周知,类型转换,截断,整形提升都会生成临时变量,而临时变量一定是一个const变量double dd1.11; int iidd; //int riidd; //权限放大 const int riidd;第三行代码中,如果想用rii去给dd当别名首先dd会先类型转换成int变量(正如第二行所示),生成一个类似ii的临时变量然后再让rii去充当dd的别名所以看似是rii充当dd的别名,其实是给ii这个临时变量充当别名所以会有权限放大的问题,一定会报错而改成const,则没有问题我们再给看看权限缩小所谓权限缩小,就是让一个又能读又能写的变量变成只能读的变量int i30; const int ri3i3;或者说我们给个拷贝const int c30; int d3c3;由于拷贝不会修改原有的变量,所以不涉及权限.所以无论谁的拷贝赋值给谁都可以只有引用和指针才涉及到权限的问题我们再系统地给一组例子第一个函数我们返回值类型为int,所以返回的一定是x的拷贝,也就是一个临时变量int func1() { static int x1; return x; }这个时候接受返回值就会涉及到权限问题了int ret1func1(); //权限放大 const intret2func1(); //权限平移 int ret3func1(); //只是拷贝,不涉及权限那我买再来修改一下代码上面我们刚刚学习到,如果一个函数要返回的值在函数销毁以后不销毁,那么就可以使用返回引用int func2() { static int x2; return x; }下来我的权限问题就会变成这样int ret1func2(); //权限平移 const intret2func2(); //权限缩小所以如果x作为一个大对象或者后面学习的深拷贝的对象,那么我们是应该用引用返回的,达到减少拷贝的目的.而如果其中不涉及对x的修改,我们可以改成const 引用返回当然传参也是一样的const引用/右值引用(借用c primer plus的代码)我们延伸一下对const引用传参的讨论,借用一下C primer plus的代码如果我们直接使用引用传参,这个参数就会有被修改的风险double cube(double a) { a*a*a; return a; } double refcube(double ra) { ra*ra*ra; return ra; } int main() { double x3.0; coutcube(x)is cube ofxendl; coutrefcube(x)is cube ofxendl; }这一段代码非常的精妙首先第一行不用多说,只是一个拷贝下来第二行,在调用refcube之后,x的值在ra被修改的时候也被修改了,所以最后都是27但是传引用传参时,必须保证实参数是正常变量,而不是一个具体的值或者别的例如如下coutrefcube(x2.0)is cube ofxendl;x2.0是一个表达式,他的结果是一个临时变量,一定不是一个变量,所以不能用引用接受传参那这样怎么办呢既然你的表达式结果是一个常量,我们不妨使用const引用 (当然这样的话,ra就不能修改了)double refcube(const double ra) { //ra*ra*ra; return ra*ra*ra; }也就可以顺利接受参数为x2.0的传参啦当然不论我们传什么样的参数,ra始终是一个const别名,不会做修改还能怎么办呢于是我们引出了左值右值的概念我们把可以取地址的称为左值,或者表现为可以出现在赋值符号的左边和右边我们把不能取地址的称为右值,可以出现在赋值符号的右侧,但不能是左侧左值我们很熟悉,右值一般是什么呢?字面值,表达式返回值,函数返回值(当然得是传值返回)10; xy; fmin(x,y);所以要想传参数,我们也可以使用C新出的右值引用double Cude(double d) { return d*d*d; } int main() { double a3.0; double zCude(a3.0); }那么这个和上面的const引用有什么区别呢?相比于const引用既可以接受左值,又可以接受右值;而且还不能区分你传的是左值还是右值右值引用由于只能接受右值,更像是在倒逼传参的程序员,你必须传一个右值给我,或者move(左值);涉及到自定义类型尤其还有深拷贝的话,如果程序员不写一个移动构造的话,那么遇到拷贝构造或者赋值的时候只能走拷贝构造而非代价更小的移动构造了指针和引用的区别指针和引用用途上是相似的,只有两方面不同1 首先是使用场景由于我们的引用在定义时必须初始化,而指针不做要求所以例如我们熟悉的链表就只能由指针实现struct ListNode { int data; ListNode *next; };2 其次也就是一些莫名其妙的语法特性指针可以在任何时候指向一个同类型实体,而引用在引用了一个实体后,则不能再引用其他实体(换句话说,指针可以改变指向,而引用不可以)有空指针NULL,但是没有空引用用sizeof计算大小时,sizeof(引用)为引用类型的大小,而sizeof(指针)永远为4/8字节另外,引用1即引用的实体值1,而指针1是地址1个(指向)类型的大小野指针/野引用: 众所周知,指针指向的空间被释放后会出现“野指针”的问题;而其实,如果我们假定引用变量指向的“本体”变量销毁之后,这个引用变量也就变成了类似的“野引用”底层语法层面来看,引用是没有开空间的.而指针开了4/8个字节的空间但是抛开别的不谈,你不开空间,我们存在哪呢?我们同时对指针和引用转汇编之后,发现其实是差不多的所以我们得出结论引用的底层还是指针实现的.即使上层差异很大,但是下层是一样的只不过在平时的探讨中,探讨引用的底层压根没有意义,我们只需要知道他是给变量的别名,这就够了七、内联函数以inline函数修饰的函数叫内联函数条件符合时编译时C会在调用内联函数的地方展开没有函数压栈的开销提高程序运行的效率。内联函数是用来对标宏函数的是一种相对于宏函数更安全的函数你 比方说你有一个短小的函数1-10行比方说swap函数会有频繁的调用10w次调这么多次开销太大了如果是C语言会怎么做呢 -宏函数如果是C呢C的大佬觉得宏函数不行搞出了内联函数进行优化首先我们先列举宏的几大优点缺点优点可维护性好 如[(#define N 6)-》 (#define N 7)]效率高 可以减少栈帧建立缺点可读性差,复杂(要通过加括号来规避一大堆优先级的问题)没有类型安全检查不方便调试对此我们给出如下例子:#define ADD(a,b) ((a)(b))一个看似简单的加法 为什么要加这么多括号呢?就是为了规避一些场景场景1:int aADD(1,3)*3;有了最外部的括号,才能得到正确结果12,否则就变成了13*310了场景2:int x1; int y2; int aADD(x/y,xy); // ((x/y)(xy))有了内部的括号,才能保证x/y和xy这两个运算可以顺利进行那既然宏这么恶心,所以内联函数的时候我们就可以写正常的函数了inline int add(int a,int b) { return ab; }看起来非常简单 但是我们要注意:内联函数几乎没有缺点,所以C里不在使用宏inline几乎解决了宏函数的缺点,并同时兼顾了他的优点当然当然,想作为宏函数必须满足条件那么这个条件是什么呢?inline关键字对于编译器来说仅仅是个建议,编译器并不放心你是否内联正确,如果你自己定义的内联函数过长(内部实现代码长度,可理解为汇编),或者有递归等,编译器优化会忽略掉内联为什么呢?内联函数是本质一种空间换时间的做法,当有(内联函数过长、递归)两种情况时,同时搭配多次的调用;此时再展开的话,导致编译出来的程序变大例:我没有一个50行的函数,和1000次的调用(每次调用都是一个call func)不内联内联100050行1000*50行这样就得不偿失了另外inline不建议有声明和定义分离,分离会导致链接错误,这是因为inline在编译时展开后,就没有函数地址了,所以也就不会放进符号表里,链接也就无法找到了.延伸我这里做一点延伸,用于激发大家的思考在后面的C11以及之后的版本中,引入了constexpr这一种关键字,他的作用是对于一些能提交计算、确定结果的“东西”,允许在编译器就确定这个“东西”的值最开始只能用在一些常量上,在后续的版本才逐步加大范围除了安全考量之外,constexpr最关键的思想是把一些工作、负担从运行时转到编译时,inline是在减少运行时的栈帧开销,constexpr是减少运行时的计算,让运行时直接拿到值这说明C一直想往同一种设计哲学上靠:zero-overhead principle(零开销原则)的一个延伸方向:C从一开始的设计哲学就是——能在编译期确定的东西,就不要拖到运行时去做,让运行时只承担真正必须在运行时才能确定的那部分工作毕竟编译期跑一遍代码是免费的(不占用程序实际运行的时间),运行时的每一条指令才是真正要花用户CPU时间的成本后续我们学C的一些看似不相关的特性(模板、constexpr、inline、甚至一部分RAII的设计考量),实际上都在有意无意地往这方面靠,只不过constexpr和inline是把这件事摆在了明面上八、auto关键字、范围for例子:我们先给一个例子int TestAuto() { return 10; } int main() { int a10; auto ba; auto ca; auto dTestAuto(); couttypeid(a).name()endl; couttypeid(b).name()endl; couttypeid(c).name()endl; couttypeid(d).name()endl; }目前来看,auto的用法还是有点局限的,他要和另一个知识点范围for结合起来一起用才行范围for以前我们遍历数组是这样的int arr[]{1,2,3,4,5,6,7}; for (int i0;isizeof(arr)/sizeof(int);i) { coutarr[i]endl; }现在我们可以这样for (auto e:arr) { couteendl; }(这里的e是对其中数组元素的拷贝)auto使用细则1 auto与指针、引用结合起来使用用auto声明指针类型时,用auto和auto*没任何区别但是用auto声明引用类型时,必须加int i10; auto ai; //int * auto*bi; //int * auto ci; //int 这些有什么用呢?以前我们修改数组元素是这样的for (int i0;isizeof(arr)/sizeof(int);i) { arr[i]; }现在我们修改可以这样(首先我们知道,直接写e的话是对arr元素的拷贝,所以想要修改必须加上)int arr[]{1,2,3,4,5}; for (auto e:arr) e--; for (auto e:arr) coute ;但如果用指针的话是不行的,直接就会报错for (auto *e:arr) e--;因为每次的e获取的是数组的值,也就是int,他无法从int转为int*2 在同一行定义多个变量在同一行的多个变量必须是同一类型,否则编译不过int i1,j2; auto a1,b2; auto c3,d4.0;auto不能推导的类型1 auto不能作为函数的参数void TestAuto(auto a) {}2 也不能用来声明数组auto arr[]{1,2,3,4,5};九、nullptr首先我们要搞明白为什么要引出这个nullptr我们在C98中一直用的NULL实际上是个宏 也就是……#define NULL 0这就导致我们在一些使用场景下可能会出现问题例:(但是不知道为什么在clion里面NULL变成long了,g更严格,直接编译不通过)void func(long) { coutfunc(long)endl; } void func(long*) { coutfunc(long*)endl; } int main() { long*pNULL; long i0; func(i); func(p); func(NULL); }所以为了弥补C98的这个坑 C11引入了nullptr,是作为关键字引入的引出如下规则:在使用nullptr时,不用引头文件在C11中,sizeof(nullptr)和sizeof((void*)0)所占字节数相同为了提升代码的健壮性,我们最好用nullptr总结做总结这次是我们学习C的第一篇博客,由于未进入面向对象,还不算太难,有很多语法细节都是对C做补充和修订.害! 好戏还在后面呢水平有限还请各位大佬指正。如果觉得对你有帮助的话还请三连关注一波。希望大家都能拿到心仪的offer哦。每日gitee侠今天你交gitee了嘛