目录1. 前言2. 内联函数2.1 内联函数的概念2.2 内联函数的特性3. 引用3.1 引用概念3.2 引用特性3.3 常引用3.4 使用场景3.4.1 引用做参数3.4.2 引用做返回值很重点3.4.3 值和引用的作为返回值类型的性能比较PS3.4.4 引用和指针的区别4. auto 关键字 (C11)4.1 auto 概念4.2 auto 的使用方法5. 基于范围的 for 循环语法糖5.1 范围 for5.2 for 循环的使用条件是6. 指针空值 nullptr1. 前言之前上文已经具体阐述了 C 的一些入门知识最重点的是函数重载和缺省函数的使用方法、使用类型搞清楚了函数重载的使用特征什么才能叫做函数重载包括知道这个轮子是怎么来的也就是能够实现函数重载的底层原理。还有其他的命名空间的使用这在后面类和对象使用也较为频繁而命名空间呢又会涉及到全局域、局部域和命名空间域如何去分别访问的问题这就牵扯出不同域的优先级关系我们也探讨过一番了。再就是为了兼容 C 语言C 既可以包容 C 语言的输入输出方式但同时也有自己独立的方式这就是 cout 和 cin使用它们也需要一定的条件包含头文件和展开命名空间 std这里不要和 STL 组件库搞混那么这些都是我上文的内容这里进行一个提要。接下来我们要进一步学习了解其他的关于 C 的基础部分还有内联函数、引用和其他的一些 C11 特性需要深入探索。2. 内联函数2.1 内联函数的概念内联函数好像不能和函数重载、命名空间一样进行字面推理啥是内联函数内部关联不能根据这个名词来猜测那就直接上定义吧。内联函数在我学习之后的理解就是不会建立函数栈帧直接原地展开但是要短小精悍展开与否不看代码得看编译器那么就会有人想这个好像和宏定义差不多宏定义也是要展开。但是宏定义在预处理阶段就被处理掉了呀。其实以inline为关键字的函数叫做内联函数编译时C编译器会在调用内联函数的地方进行展开就不会有建立栈帧的开销从根本上说会一定程度上提高程序运行效率。来举个例子吧我用一个add函数先不做inline处理看看反汇编代码是如何的这里有一个call在之前也已经讲过这个call指令的作用是将call指令下一条指令的地址入栈是因为call执行时会进入Add指令的内部在Add指令走完之后会回到主函数中黄色框框是call指令的地址而红色框框是call指令的下一条指令地址这个地址可以看作一个标记位等call命令回到主函数的时候就发挥作用了是找到回家的路以上都是正常的调用函数然后建立函数栈帧的过程。那要是在函数前面加上inline关键字呢在编译期间编译器会用函数体替换函数的调用。在演示之前我们需要做一些设置我默认设置没有进行过设置inline会被禁用debug版本下内联不起作用否则不方便调试。做完这两步我们再来看此时我们在add上加了inline。inline int add(int one ,int two) { return one two; }反汇编1可以看出设置之后调出反汇编inline之前前面还有pushpush行为也就是压栈操作这里直接就是mov 和add已经看不到call add了。那么如果我把这条代码加长会怎么样呢还会进行展开吗实践出真知inline int add(int one ,int two) { if (one 1) return 1; return one * add(two - 1, one); } int main() { int ret 0; retadd(1,3); return 0; }以上我把内联函数的内容修改成了递归然后去看反汇编很显然不会变成内联变成了call指令。这也是就我上面所述的即使你加了inline也不一定具体要看编译器说了算的。反汇编2再者就是为啥不能替代宏函数呢既然内联函数也是直接展开。宏函数的优势是不用建立栈帧提高调用效率但同时很复杂一旦写错容易多处出问题不容易修复不可检查因为它在预处理阶段就被文本替换掉了而且每个人对一个宏的理解不一样会导致可读性很差。只能说各有千秋不能完全替换在多数场景还是会优先选择内联函数高频调用复用率很高的代码。至于为何太长的函数不给展开呢举个例子某个功能函数有30行也不多至少没有50行以上。然后总代码量有1000行有100处位置需要反复调用这个功能函数如果不使用内联函数那么代码量会有多少就是100030行结束了吧。每次调用的都是一个函数所以不存在使用一次就30。但是这个功能函数变成了内联函数呢它是要被展开的已经不是像调用函数那样成call。它是100个地方都要展开了合计有约100*30这个到最后变成可执行程序就会很大这也让我想到一个游戏安装包不大也就6个G然后全部解析后完成安装就变成了50甚至100G。这都是有可能发生的。所以说如果太长的函数强行使用内联函数就会导致文件过大这显然不好所以就是要让编译器来观察。这就引出了它的特性。2.2 内联函数的特性这里其实是对内联函数的一些总结在2.1也有体现内联函数在一定范围上比宏函数要较为方便功能函数加了inline后不一定成为内联函数原地展开要看编译器的处理方式如果编译器决定用内联函数在编译阶段会用函数体来代替函数调用。缺陷是如果函数体本身比较大那么目标文件会更大优点是不用调用减少了栈帧开销inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。我来演示一下这个错误#pragma once #includeiostream using namespace std; inline void F(int a); #includef.h inline void F(int i) { cout i 10 endl; } #includef.h int main() { F(10); return 0; }提示的是无法解析的外部符号在主函数中引用了在前面是error LNK也就是链接错误。这个也和四大阶段有关系有定义为什么还找不到呢按理说有个函数调用call是没错的之前我们这样写根本没问题声明和定义分离声明都在头文件里。但不巧的是这次是内联函数是要展开的。展开后给我单独一个声明并没有定义就没有分配内存空间也就不会产生地址符号表只是会给声明打一个未定义的外部引用有.obj文件但是没有真的分配内存符号表就不会确认它的地址在最后也就链接不到无法变成可执行文件。这个逻辑是很严谨的。这里就是类似于只声明不定义的情况编译能通过链接会错误。所以链接的时候找不到地址了。所以内联函数不能声明和定义分离。3.引用3.1 引用概念这一part也是C入门学习的重头戏它和指针有异曲同工之妙引用不是新定义的变量而是给它取个外号取个别名。比如某个明星的真名和艺名是不一样的。很多人也有外号或者乳名小名。而且编译器也不会给引用的开辟一个新内存空间就简单的举个例子。你本名叫张三那要是改成张四。难道还要单独给张四重新盖个房子买辆车开玩笑吧。而且也不止可以有一个引用。可以有多个。说了概念看看到底怎么个事类型引用变量名(对象名) 引用实体int main() { int a1; //类型 引用变量名(对象名) 引用实体 int ia a; cout a endl; cout a endl; cout ia endl; cout ia endl; }可以见得ia是a的别名然后ia和a的取值一样地址也一样但是要甄别第一个是取别名输出那里才是取地址。符号一样意思不一样。你改变了ia的值其实也会改变a。3.2引用特性绑定要一辈子一生一世引用在定义时必须初始化不能只给声明不然就会编译错误。其他类型只会给个随机值可以是int或者char但是不会报错其实就是只是给声明没有分配空间。int main() { int a1; //类型 引用变量名(对象名) 引用实体 int ia; cout a endl; cout a endl; cout ia endl; cout ia endl; }本体分身一致性引用可以有多个别名改变每个别名都会改变所有的名字的值int main() { int a1; //类型 引用变量名(对象名) 引用实体 int iaa; int ra ia; int ba ra; cout a endl; cout ra endl; cout ba endl; cout ba endl; }引用一旦引用一个实体再不能引用其他实体这个无法演示。知道就好一旦绑定终身绑定。引用在定义时必须初始化要有绑定对象且对象有明确的内存空间引用可以使用多个别名更改值的话会一起更改但是地址始终不变不能换绑要始终如一。3.3 常引用常量引用必须要常量才行记住一个秘诀就是引用的权限被放大是绝对不行的可以平移或者缩小。示范一下我会举出一些正常的例子和一些错误的例子虽然不多但是非常精辟很讲究。void testconst() { int a 0; int b a;//正常引用 const int ra a;//权限缩小 int rd 10;//权限放大 double d 3.14; int rrd d;//类型不同编译出错 const int rrrd d; const int c 10; int rc c;//权限放大 const int rrc c;//平移 }首先说明一点我是用权限的放大缩小平移来说明因为这个便于理解也便于解释这个C中核心的理解方式。而且是否看放大缩小是从右往左看左边权限大右边小那就是放大。第一个是正常的引用也是最经典的给a取一个别名别名叫b。可读可写第二个ra是const引用只读a是可读可修改权限是属于缩小了第三个单独的10是一个字面量是一个右值代表一个临时的值。就是随时会不见的值为只读属性。而C的安全规则中一个非const的引用是普通引用只能绑定一个左值就是有名字能够存在查得到的不会凭空消失的变量。这是为了防止无意修改一个临时值会造成混乱就是如果你成立这个写法等于原本一个只读的东西变成可以读可以修改的入口。这样左边权限大右边小是权限放大。编译器不允许一个可修改的引用去指向一个不可修改的右值解决方案使用const引用相当于把权限从能修改变成不修改告诉编译器我不乱动不修改我自己绑定的值那就允许你绑定。右边是10是字面常量只读左边const intrd常量引用rd也是只有只读权限这叫权限平移。因为我把左边的权限变小了。第四个这里涉及类型转换因为d是double要赋值给int就会产生临时变量临时变量是右值是只读属性。那么你去用普通引用可读可写去绑定它就是权限放大。解决方案而const int rrrd d。把左边权限变小了变成和右边一个等级就是权限平移。你也可以理解为当右边权限小于左边时降低左边权重或者增加右边权重就可以编译通过。第五个int rcc是报错的左边权限大是普通引用右边是常量只读。所以是权限放大解决方案就是缩小左边权限形成平衡。然后我们分别进行改错改完之后的代码如下void testconst() { int a 0; int b a;//正常引用 const int ra a;//权限缩小 const int rd 10;//权限放大-平移左边缩小 double d 3.14; const int rrd d;//类型不同编译出错 const int rrrd d; int c 10; int rc c;//权限放大-权限平移右边权限放大 const int rrc c;//平移 }3.4 使用场景引用分为两大场景一个是引用做参数二是引用做返回值。我会详细介绍这俩个应用。3.4.1 引用做参数引用作为函数参数是 C 中非常常见且重要的用法它可以替代指针实现函数对实参的直接修改同时语法更简洁、更安全。1. 基本用法(形参的改变会改变实参)在函数形参列表中使用 符号声明引用参数。函数内部对引用参数的修改会直接影响外部的实参。void Swap(int a, int b) { int temp a; a b; b temp; } int main() { int x 10, y 20; Swap(x, y); // 调用后x 变为 20y 变为 10 return 0; }在以上举例中a 和b分别是x和y的别名。函数内部交换a和b的值实际上就是交换了x和y的值。2. 与指针传参的比较引用传参和指针传参都能实现类似的效果但引用稍微简洁一点引用传参不调用时不用取地址函数内部也不需要解引用看的更舒服。// 指针 void SwapByPointer(int* a, int* b) { int temp *a; *a *b; *b temp; } // 引用 void SwapByReference(int a, int b) { int temp a; a b; b temp; } int main() { int x 10, y 20; SwapByPointer(x, y); // 需要取地址 SwapByReference(x, y); // 直接传递变量更直观 return 0; }交换之前x和y的值a和b的值交换之后xy和ab值3. 避免拷贝提高效率对于大型对象如结构体、类对象使用引用传参可以避免值传递时的拷贝开销#include time.h struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 clock(); for (size_t i 0; i 10000; i) TestFunc1(a); size_t end1 clock(); // 以引用作为函数参数 size_t begin2 clock(); for (size_t i 0; i 10000; i) TestFunc2(a); size_t end2 clock(); // 分别计算两个函数运行结束后的时间 cout TestFunc1(A)-time: end1 - begin1 endl; cout TestFunc2(A)-time: end2 - begin2 endl; } int main() { TestRefAndValue(); return 0; }在时间开销上它们的差距是9-0要是把数据变大那么差距会更明显3.4.2 引用做返回值很重点我们有引用返回和传值返回int count() { static int n 0; n; return n; } int main() { //TestRefAndValue(); int ret count(); return 0; }这个n是直接返回到ret的吗这是个值得探讨的问题其实不是n会生成一个临时变量作为表达式的返回值给到ret。那么为什么会生成一个临时变量呢因为出了作用域这个函数栈帧就会销毁count中的n就没了。但是上面例子的n是static是在静态区里。其实编译器没有做过多处理不管怎么样它都会生成一个临时变量。这并不取决于是否跳出作用域而是取决于返回类型如果是int做返回值传值返回就会。若是引用作返回值就不会生成临时变量这个好处是减少了一层拷贝增加了它的效率。int count() { static int n 0; n; return n; } int main() { //TestRefAndValue(); int ret count(); return 0; }这个时候它返回的不是装载n的临时变量了是返回的n的别名这个需要记住概念。再来看看传值返回和传引用的效率在小数据上看不出来但是一旦大量数据进行返回就会明显有区别3.4.3值和引用作为返回值类型的性能比较#include time.h struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 clock(); for (size_t i 0; i 100000; i) TestFunc1(); size_t end1 clock(); // 以引用作为函数的返回值类型 size_t begin2 clock(); for (size_t i 0; i 100000; i) TestFunc2(); size_t end2 clock(); // 计算两个函数运算完成之后的时间 cout TestFunc1 time: end1 - begin1 endl; cout TestFunc2 time: end2 - begin2 endl; } int main() { TestReturnByRefOrValue(); }给出测试结果第一个是传值返回第二个是传引用返回。两者差异非常明显。所以结论是在面对大对象的时候传引用返回会极大提高效率。PS虽然传引用返回方便但是要谨慎用引用做返回值出了函数作用域对象不在了就不能引用返回还在就可以引用。为什么要这么说我给出一个测试是出了作用域的案子int count() { int n 0; n; return n; } int main() { //TestRefAndValue(); int ret count(); return 0; }这个时候没有static了n是局部变量在局部域返回的n的别名n的值给到ret时候n所在的域已经被销毁了被回收了。那里面所存在的数据是不稳定的给到ret的值也是不一定对的思路图是如此。如果函数栈帧结束销毁回收了但是侥幸没有清理可能ret是正确的但是回收后再次被其他数据填充了那ret就是不对的是没有指向的随机空间。测试结果看看这个时候ret的值是对的但是不全对来看一个被覆盖掉的案例这个时候ret是n的别名的别名返回n的别名再给这个别名引用们就是别名的别名。int count(int x) { int n x; n; return n; } int main() { //TestRefAndValue(); int ret count(10); cout ret endl; count(20); cout ret endl; return 0; }你看这里就是值不同第一次打印是11因为函数结束并没有清理栈帧空间此时第103行已经结束ret是n的别名的别名再去执行104行ret已经是越界了没有清理空间的话就打印11。再次调用就会被覆盖成n20n成为21。这是需要重点注意的。下面这个案例会更明显第一次调用n是11栈帧没有被清理打印的11.再调用别的函数就会随机值因为被覆盖了已经不是原来的栈帧空间了有别的数据进去了。加上static就不会引发随机值错误因为count栈帧空间的销毁不会干扰n。n是静态变量出了作用域还在返回n的别名没有问题。总结引用可以再任何场景都引用传参但是要谨慎虽然传引用返回方便但是要谨慎用引用做返回值出了函数作用域对象不在了就不能引用返回还在就可以引用3.4.4 引用和指针的区别在语法概念上引用是不开空间因为引用是变量的别名引用和变量公用一个空间。指针是开空间因为指针存储变量地址。之前也演示过确实是同一个地址。引用在定义时必须进行初始化而指针没有要求可以声明而不定义引用没有NULL引用指针有NULL引用在size中含义不同引用结果为引用类型大小而指针就是固定的4/8字节是地址空间所占字节数取决于32位或者64位平台指针可以多级一级二级甚至三级越多越复杂但是引用只有别名的别名不存在多级引用访问实体方式不同指针需要显式解引用引用编译器自己处理引用在绑定一个实体后就不能换绑但是指针可以随时指向任何一个同类型的实体变量。然后我会实践一部分总结来加深引用和指针的区别光文字其实并不会感觉到什么。总结1总结2总结4.1总结4.2总结4.34.auto关键字(C11)4.1 auto概念auto翻译过来是自动化。C11中标准委员会赋予了auto全新的含义即auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期推导而得。int TestAuto(int a ) { static int i a; return i; } int TestAuto2() { return 100; } int main() { int a 10; auto b a; auto c a; auto d TestAuto(a); auto e TestAuto2(); cout typeid(b).name() endl; cout typeid(c).name() endl; cout typeid(d).name() endl; cout typeid(e).name() endl; return 0; }auto是根据右边的表达式自动推导左边的类型这里简单了解一下就行。auto主要用于后面很复杂的类型难于拼写或者难以解读的打个比方说英文字符很长你记不住就会想到用简写的类似比喻。我从网上找了一个代码案例这里auto能发挥它的最大作用。#includevector #includemap int main() { vectorint v; auto it v.begin(); std::mapstd::string, std::stringdict; auto dit dict.begin (); return 0; }4.2 auto的使用方法auto对多个变量进行推导必须是同一类型的否则编译器会报错因为auto只会推导第一个类型然后用推导出来的类型来定义后面的。但是auto不是随便用的它不能用来推导形参也不能声明数组。为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法其实最常用还是和后面要讲的基于范围for循环来搭配使用更香5.基于范围的for循环语法糖5.1 范围forvoid 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; }依次取数组中的数组传给ee来自动推导类型当然e可以变成任何变量不和标准库冲突就行。因为比较方便所以很甜叫语法糖5.2 for循环的使用条件是范围必须确定对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。int main() { for (auto e : array) { cout e endl; e * 2; } for (auto e : array) cout e endl; }for的范围是需要确定的以上代码不确定的话就要出问题6.指针空值nullptr这个也是要稍微注意的点在良好的C/C编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现 不可预料的错误比如未初始化的指针。如果一个指针没有合法的指向我们基本都是按照如下方式对其进行初始化。int* p1 NULL; int* p2 0;处于好奇想知道NULL的底层设定是怎么样的我去查了一下stddef.h可以大致了解到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后就会再次进入第一个函数f(int)。如图前两个调用的都是第一个函数第三个进行强制转化后才是进入第二个对比如果一定要让他按照指针类型去走那就必须要进行强制转化而nullptr是代表指针空值所以在后续表示指针空值时建议最好使用nullptr。感谢大家观看后续类和对象会慢慢更新后面就真的慢慢踏入C大门的现在还只是门槛。
