顺义企业网站建站公司,it运维是做什么的,艺客网站首页,邯郸网络技术公司1. 什么是C 2. C发展史 3. C的重要性 4. C关键字 5. 命名空间 6. C输入输出 7. 缺省参数 8. 函数重载 9. 引用 10. 内联函数 11. auto关键字(C11) 12. 基于范围的for循环(C11) 13. 指针空值---nullptr(C11)05. 1. 什么是C C语言是结构化和模块化的语言… 1. 什么是C 2. C发展史 3. C的重要性 4. C关键字 5. 命名空间 6. C输入输出 7. 缺省参数 8. 函数重载 9. 引用 10. 内联函数 11. auto关键字(C11) 12. 基于范围的for循环(C11) 13. 指针空值---nullptr(C11)05. 1. 什么是C C语言是结构化和模块化的语言适合处理较小规模的程序。对于复杂的问题规模较大的 程序需要高度的抽象和建模时C语言则不合适。为了解决软件危机 20世纪80年代 计算机 界提出了OOP(object oriented programming面向对象)思想支持面向对象的程序设计语言应运而生。 1982年Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念发明了一 种新的程序语言。为了表达该语言与C语言的渊源关系命名为C。因此C是基于C语言而 产生的它既可以进行C语言的过程化程序设计又可以进行以抽象数据类型为特点的基于对象的 程序设计还可以进行面向对象的程序设计。 C是在C的基础之上容纳进去了面向对象编程思想并增加了许多有用的库以及编程范式 等。熟悉C语言之后对C学习有一定的帮助
本章节主要目标 1. 补充C语言语法的不足以及C是如何对C语言设计不合理的地方进行优化的比如作用 域方面、IO方面、函数方面、指针方面、宏方面等。2. 为后续类和对象学习打基础。 2. C的发展史 1979年贝尔实验室的本贾尼等人试图分析unix内核的时候试图将内核模块化于是在C 语言的基础上进行扩展增加了类的机制完成了一个可以运行的预处理程序称之为C with classes。 语言的发展就像是练功打怪升级一样也是逐步递进由浅入深的过程。我们先来看下C的历史版本。
阶段内容C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符 重载等C1.0类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符 重载等C2.0更加完善支持面向对象新增保护成员、多重继承、对象的初始化、抽象类、静 态成员以及const成员函数C3.0进一步完善引入模板解决多重继承产生的二义性问题和相应构造和析构的处 理C98C标准第一个版本绝大多数编译器都支持得到了国际标准化组织(ISO)和美 国标准化协会认可以模板方式重写C标准库引入了STL(标准模板库)C03C标准第二个版本语言特性无大改变主要修订错误、减少多异性C05C标准委员会发布了一份计数报告(Technical ReportTR1)正式更名 C0x即计划在本世纪第一个10年的某个时间发布C11增加了许多特性使得C更像一种新语言比如正则表达式、基于范围for循 环、auto关键字、新容器、列表初始化、标准线程库等C14对C11的扩展主要是修复C11中漏洞以及改进比如泛型的lambda表 达式auto的返回值类型推导二进制字面常量等C17在C11上做了一些小幅改进增加了19个新特性比如static_assert()的文 本信息可选Fold表达式用于可变的模板if和switch语句中的初始化器等C20自C11以来最大的发行版引入了许多新的特性比如模块(Modules)、协 程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性还有对已有 特性的更新比如Lambda支持模板、范围for支持初始化等C23制定ing
C还在不断的向后发展。但是现在公司主流使用还是C98和C11所有大家不用追求最 新重点将C98和C11掌握好等工作后随着对C理解不断加深有时间可以去琢磨下更 新的特性。
关于C2X最新特性的讨论链接
3. C的重要性
3.1 语言的使用广泛度
下图数据来自TIOBE编程语言社区2023年最新的排行榜在30多年的发展中C/C几乎一 致稳居前5。 TIOBE 编程语言社区排行榜是编程语言流行趋势的一个指标每月更新这份排行榜排名基于互 联网上有经验的程序员、 课程和第三方厂商的数量。排名使用著名的搜索引擎诸如 Google、 MSN、Yahoo!、Wikipedia、YouTube 以及 Baidu 等进行计算。 注意排名不能说明那个语言好那个不好每门编程语言都有适应自己的应用场景。
3.2 在工作领域
1. 操作系统以及大型系统软件开发
所有操作系统几乎都是C/C写的许多大型软件背后几乎都是C写的比如 Photoshop、Office、JVM(Java虚拟机)等究其原因还是性能高可以直接操控硬件。
2. 服务器端开发
后台开发主要侧重于业务逻辑的处理即对于前端请求后端给出对应的响应现在主流采 用java但内卷化比较严重大厂可能会有C后台开发主要做一些基础组件中间件、 缓存、分布式存储等。服务器端开发比后台开发跟广泛包含后台开发一般对实时性要求 比较高的比如游戏服务器、流媒体服务器、网络通讯等都采用C开发的。
3. 游戏开发
PC平台几乎所有的游戏都是C写的比如魔兽世界、传奇、CS、跑跑卡丁车等市面上 相当多的游戏引擎都是基于C开发的比如Cocos2d、虚幻4、DirectX等。三维游戏领 域计算量非常庞大底层的数学全都是矩阵变换想要画面精美、内容丰富、游戏实时性 搞这些高难度需求无疑只能选C语言。比较知名厂商腾讯、网易、完美世界、巨人网 络等。
4. 嵌入式和物联网领域
嵌入式就是把具有计算能力的主控板嵌入到机器装置或者电子装置的内部能够控制这些 装置。比如智能手环、摄像头、扫地机器人、智能音响等。
谈到嵌入式开发大家最能想到的就是单片机开发(即在8位、16位或者32位单片机产品或者 裸机上进行的开发)嵌入式开发除了单片机开发以外还包含在soc片上、系统层面、驱动 层面以及应用、中间件层面的开发。
常见的岗位有嵌入式开发工程师、驱动开发工程师、系统开发工程师、Linux开发工程 师、固件开发工程师等。
知名的一些厂商比如以华为、vivo、oppo、小米为代表的手机厂以紫光展锐、乐鑫为 代表的芯片厂以大疆、海康威视、大华、CVTE等具有自己终端业务厂商以及海尔、海 信、格力等传统家电行业。
随着5G的普及物联网(即万物互联)也成为了一种新兴势力比如阿里lot、腾讯lot、京 东、百度、美团等都有硬件相关的事业部。
5. 数字图像处理
数字图像处理中涉及到大量数学矩阵方面的运算对CPU算力要求比较高主要的图像处理 算法库和开源库等都是C/C写的比如OpenCV、OpenGL等大名鼎鼎的Photoshop 就是C写的。
6. 人工智能
一提到人工智能大家首先想到的就是python认为学习人工智能就要学习python这个 是误区python中库比较丰富使用python可以快速搭建神经网络、填入参数导入数据就 可以开始训练模型了。但人工智能背后深度学习算法等核心还是用C写的。
7. 分布式应用
近年来移动互联网的兴起各应用数据量业务量不断攀升后端架构要不断提高性能和并发 能力才能应对大信息时代的来临。在分布式领域好些分布式框架、文件系统、中间组件等 都是C开发的。对分布式计算影响极大的Hadoop生态的几个重量级组件HDFS、 zookeeper、HBase等也都是基于Google用C实现的GFS、Chubby、BigTable。包括分 布式计算框架MapReduce也是Google先用C实现了一套之后才有开源的java版本。
除了上述领域外在科学计算、浏览器、流媒体开发、网络软件等都是C比较适合的场景 作为一名老牌语言的常青树C一直霸占编程语言前5名肯定有其存在的价值。
4. C关键字(C98)
C总计63个关键字C语言32个关键字 5. 命名空间
我们首先来看一下一个C程序的样子。
#includeiostream//头文件
using namespace std;//命名空间stdint main()
{cout hello world endl;//输出并换行return 0;
}
5.1 命名空间定义 定义命名空间需要使用到namespace关键字后面跟命名空间的名字然后接一对{}即可{} 中即为命名空间的成员。现在我们来介绍一下命名空间注意C是兼容90%的C语言。 当我们加入头文件stdlib.h时程序就报错了因为该文件下有一个rand函数和我们当前的全局变量rand出现了命名冲突的问题。
#include stdio.h
#include stdlib.h
int rand 10;
// C语言没办法解决类似这样的命名冲突问题所以C提出了namespace来解决
int main()
{printf(%d\n, rand);return 0;
}
// 编译后后报错error C2365: “rand”: 重定义以前的定义rand是“函数” 在C/C中变量、函数和后面要学到的类都是大量存在的这些变量、函数和类的名称将都存在于全局作用域中可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化 以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的。
命名冲突解决方法如下不指定命名空间的名字不能访问该全局变量。
#include stdio.h
#include stdlib.hnamespace yu //是命名空间的名字一般开发中是用项目名字做命名空间名。
{int rand 10;
}//这里没有分号int main()
{printf(%p\n, rand);//这里rand是函数需要%p打印// :: 域作用限定符printf(%d\n, yu::rand);//这里rand是变量需要%d打印return 0;
}
命名空间中可以定义变量 / 函数 / 类型
namespace yu //是命名空间的名字一般开发中是用项目名字做命名空间名。
{int rand 10;int Add(int left, int right){return left right;}struct Node{struct Node* next;int val;};
}//这里没有分号//命名空间变量
yu::rand;
yu:Add(1,2);
struct yu::Node node;
命名空间可以嵌套
namespace yu
{namespace yu1{int rand 10;}namespace yu2{int rand 10;}
}printf(%d\n, yu::yu1::rand);//输出
printf(%d\n, yu::yu2::rand);//输出
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps一个工程中的test.h和上面test.cpp中两个yu会被合并成一个
// test.h
namespace yu
{int Add(int left, int right){return left right;}
}
5.2 命名空间使用
命名空间中成员该如何使用呢比如
#include stdio.h
#include stdlib.h
namespace yu
{// 命名空间中可以定义变量/函数/类型int a 0;int b 1;int Add(int left, int right){return left right;}struct Node{struct Node* next;int val;};
}
int main()
{// 编译报错error C2065: “a”: 未声明的标识符printf(%d\n, a);return 0;
}
命名空间的使用有三种方式
加命名空间名称及作用域限定符
int main()
{printf(%d\n, yu::a);return 0;
}使用using将命名空间中某个成员引入
using yu::b;
int main()
{printf(%d\n, N::a);printf(%d\n, b);return 0;
}使用using namespace 命名空间名称 引入
using namespce yu;
int main()
{printf(%d\n, yu::a);printf(%d\n, b);Add(10, 20);return 0;
}总结 std是C标准库的命名空间如何展开std使用更合理呢 1. 在日常练习中建议直接using namespace std即可这样就很方便。2. using namespace std展开标准库就全部暴露出来了如果我们定义跟库重名的类型/对象/函数就存在冲突问题。该问题在日常练习中很少出现但是项目开发中代码较多、规模大就很容易出现。所以建议在项目开发中使用像std::cout这样使用时指定命名空间 using std::cout展开常用的库对象/类型等方式。 6. C输入输出 新生婴儿会以自己独特的方式向这个崭新的世界打招呼C刚出来后也算是一个新事物那C是否也应该向这个美好的世界来声问候呢我们来看下C是如何来实现问候的。
#includeiostream
// std是C标准库的命名空间名C将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{cout Hello world!!! endl;return 0;
}
说明 1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时必须包含 iostream 头文件 以及按命名空间使用方法使用std。2. cout和cin是全局的流对象endl是特殊的C符号表示换行输出他们都包含在包含 iostream 头文件中。3. 是流插入运算符是流提取运算符。 4. 使用C输入输出更方便不需要像printf/scanf输入输出时那样需要手动控制格式。 C的输入输出可以自动识别变量类型。5. 实际上cout和cin分别是ostream和istream类型的对象和也涉及运算符重载等知识 这些知识我们我们后续才会学习所以我们这里只是简单学习他们的使用。后面我们还有有 一个章节更深入的学习IO流用法及原理。 注意早期标准库将所有功能在全局域中实现声明在.h后缀的头文件中使用时只需包含对应头文件即可后来将其实现在std命名空间下为了和C头文件区分也为了正确使用命名空间 规定C头文件不带.h旧编译器(vc 6.0)中还支持格式后续编译器已不支持因 此推荐使用std的方式。
#includeiostream
// std是C标准库的命名空间名C将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{int a 1;double d 1.1;// 可以自动识别变量的类型cout a endl;cout d endl;// 可以多种类型拼接cout a d a d endl;return 0;
}
运行结果 // ps关于cout和cin还有很多更复杂的用法比如控制浮点数输出精度控制整形输出进制格式等等。因为C兼容C语言的用法这些又用得不是很多我们这里就不展开学习了。后续如果有需要我们再配合文档学习。如果需要可以使用C语言的printf去控制精度。 7. 缺省参数
7.1缺省参数概念 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参。
#includeiostreamusing std::cout;
using std::endl;void Func(int a 0 ,int b 1)
{cout a endl;cout b endl endl;
}
int main()
{Func(); // 没有传参时使用参数的默认值Func(10); // 传参时使用指定的实参 - 传参顺序:10-aFunc(10, 20); // 传参时使用指定的实参 - 传参顺序:10-a,20-breturn 0;
}
运行结果 7.2 缺省参数分类 全缺省参数
void Func(int a 10, int b 20, int c 30)
{cout a a endl;cout b b endl;cout c c endl;
}
半缺省参数
void Func(int a, int b 10, int c 20)
{cout a a endl;cout b b endl;cout c c endl;
}
注意 1. 半缺省参数必须从右往左依次来给出不能间隔着给2. 缺省参数不能在函数声明和定义中同时出现 //a.h
void Func(int a 10);// a.cpp
void Func(int a 20)
{}//如果声明与定义位置同时出现恰巧两个位置提供的值不同\
那编译器就无法确定到底该用那个缺省值。//规定在声明的地方写缺省参数
//a.h
void Func(int a 10);// a.cpp
void Func(int a)
{} 3. 缺省值必须是常量或者全局变量4. C语言不支持编译器不支持 8. 函数重载 自然语言中一个词可以有多重含义人们可以通过上下文来判断该词真实的含义即该词被重载了。 比如以前有一个笑话国有两个体育项目大家根本不用看也不用担心。一个是乒乓球一个是男足。前者是“谁也赢不了”后者是“谁也赢不了”
8.1 函数重载概念 函数重载是函数的一种特殊情况C允许在同一作用域中声明几个功能类似的同名函数这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同常用来处理实现功能类似数据类型 不同的问题。
#includeiostream
using namespace std;//重载函数名相同参数不同类型不同个数不同类型顺序不同返回值可同可不同// 1、参数类型不同
int Add(int left, int right)
{cout int Add(int left, int right) endl;return left right;
}
double Add(double left, double right)
{cout double Add(double left, double right) endl;return left right;
}// 2、参数个数不同
void f()
{cout f() endl;
}
void f(int a)
{cout f(int a) endl;
}// 3、参数类型顺序不同
void f(int a, char b)
{cout f(int a,char b) endl;
}
void f(char b, int a)
{cout f(char b, int a) endl;
}
int main()
{Add(10, 20);Add(10.1, 20.2);f();f(10);f(10, a);f(a, 10);return 0;
}
问下面这个能构成重载吗 --- 构成参数不同构成重载但是实际上不会写这样的代码不传参的时候调用存在二义性。
#includeiostream
using namespace std;
void f()
{cout f() endl;
}
void f(int a 0)
{cout f(int a 0) endl;
}
8.2 C支持函数重载的原理--名字修饰(name Mangling)
为什么C支持函数重载而C语言不支持函数重载呢
在C/C中一个程序要运行起来需要经历以下几个阶段预处理、编译、汇编、链接。 1. 实际项目通常是由多个头文件和多个源文件构成而通过C语言阶段学习的编译链接我们可以知道【当前a.cpp中调用了b.cpp中定义的Add函数时】编译后链接前a.o的目标文件中没有Add的函数地址因为Add是在b.cpp中定义的所以Add的地址在b.o中。那么怎么办呢
2. 所以链接阶段就是专门处理这种问题链接器看到a.o调用Add但是没有Add的地址就会到b.o的符号表中找Add的地址然后链接到一起。
3. 那么链接时面对Add函数链接接器会使用哪个名字去找呢这里每个编译器都有自己的 函数名修饰规则。
4. 由于Windows下vs的修饰规则过于复杂而Linux下g的修饰规则简单易懂下面我们使 用了g演示了这个修饰后的名字。
5. 通过下面我们可以看出gcc的函数修饰后名字不变。而g的函数修饰后变成【_Z函数长度 函数名类型首字母】。
采用C语言编译器编译后结果 结论在linux下采用gcc编译完成后函数名字的修饰没有发生改变。
采用C编译器编译后结果 结论在linux下采用g编译完成后函数名字的修饰发生改变编译器将函数参 数类型信息添加到修改后的名字中。
Windows下名字修饰规则 结论对比一下发现修饰后的格式为? 函数名 YA 返回值 参数1 参数2 Zint类型对应的是字母Hvoid类型对应的是字母Xdouble类型对应的是字母N。扩展float类型对应的是字母M
对比Linux会发现windows下vs编译器对函数名字修饰规则相对复杂难懂但道理都是类似的我们就不做细致的研究了。【扩展学习C/C函数调用约定和名字修饰规则--有兴趣好奇的同学可以看看里面 有对vs下函数名修饰规则讲解】
C/C的调用约定
6. 通过这里就理解了C语言没办法支持重载因为同名函数没办法区分。而C是通过函数修饰规则来区分只要参数不同修饰出来的名字就不一样就支持了重载。
7. 如果两个函数函数名和参数是一样的返回值不同是不构成重载的因为调用时编译器没办法区分。
9. 引用
9.1 引用概念 引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。
比如李逵在家称为铁牛江湖上人称黑旋风。
类型 引用变量名(对象名) 引用实体 注意引用类型必须和引用实体是同种类型的。
9.2 引用特性
1. 引用在定义时必须初始化2. 一个变量可以有多个引用可以给别名起别名3. 引用一旦引用一个实体再不能引用其他实体此时引用只是赋值不改变指向 9.3 常引用
void TestConstRef()
{// 权限不能放大const int a 10;int ra a; // 该语句编译时会出错a为常量const int ra a;int b 10; // 该语句编译时会出错b为常量const int b 10;int c 10;const int rc c;//权限可以缩小double d 12.34;int rd d; // 该语句编译时会出错类型不同const int rd d;//产生了临时变量12此时是给临时变量取别名
}
9.4 使用场景
1. 做参数
void Swap(int left, int right)
{int temp left;left right;right temp;
}2. 做返回值
int Count()
{int n 0;n;return n;
}int main()
{int ret Count();cout ret endl;return 0;
}
上面这个程序ret接收的是n还是n的拷贝这里接收的是n的拷贝当Count函数调用完后局部变量就销毁了所以我们这里是传值返回。如果我们引用返回呢
int Count()
{int n 0;n;return n;
}int main()
{int ret Count();cout ret endl;return 0;
}
这里的ret值是不确定的它返回的是n的别名然后赋值给ret但是这要取决于编译器在释放了函数栈帧后有没有将n这个变量的值清理掉。vs编译器下函数栈帧释放后不会将值清理掉。
运行结果 下面代码输出什么结果为什么 ret是上面c的别名但是局部变量c出了函数作用域返回对象就销毁了不能用引用返回否则结果是不确定滴。 注意如果函数返回时出了函数作用域如果返回对象还在(还没还给系统)则可以使用 引用返回如果已经还给系统了则必须使用传值返回。
那引用作返回值我们该如何使用呢我们可以给变量增加static修饰。
int Add(int a, int b)
{static int c a b;return c;
}
int main()
{int ret Add(1, 2);cout Add(1, 2) is : ret endl;Add(3, 4);cout Add(1, 2) is : ret endl;return 0;
}
运行结果 这里输出这样的结果是因为静态局部变量只会初始化一次后面再调用就不会执行。
9.5 传值、传引用效率比较 以值作为参数或者返回值类型在传参和返回期间函数不会直接传递实参或者将变量本身直接返回而是传递实参或者返回变量的一份临时的拷贝因此用值作为参数或者返回值类型效 率是非常低下的尤其是当参数或者返回值类型非常大时效率就更低。
#include iostream
using namespace std;#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;
}
运行结果 值和引用的作为返回值类型的性能比较
#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();return 0;
} 通过上述代码的比较发现传值和引用在作为传参以及返回值类型上效率相差很大。
9.6 引用和指针的区别
在语法概念上引用就是一个别名没有独立空间和其引用实体共用同一块空间。 在底层实现上实际是有空间的因为引用是按照指针方式来实现的。
int main()
{int a 10;// 语法ra没有开空间int ra a;ra 20;// 语法pa开了空间int* pa a;*pa 20;return 0;
} 这里可以验证引用就是变量的别名没有开空间语法上这里引用只占一个字节。我们来看下引用和指针的汇编代码对比 现象底层上引用和指针的汇编代码一样引用是按照指针方式来实现的。
引用和指针的不同点: 1. 引用概念上定义一个变量的别名指针存储一个变量地址。 2. 引用在定义时必须初始化指针没有要求 3. 引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体 4. 没有NULL引用但有NULL指针 5. 在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32 位平台下占4个字节) 6. 引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小 7. 有多级指针但是没有多级引用 8. 访问实体方式不同指针需要显式解引用引用编译器自己处理 9. 引用比指针使用起来相对更安全 10. 内联函数
10.1 概念
稍微介绍一下宏 优点 1.增强代码的复用性。2.提高性能。 缺点 1.不方便调试宏。因为预编译阶段进行了替换2.导致代码可读性差可维护性差容易误用。3.没有类型安全的检查 。 C有哪些技术替代宏 1. 常量定义换用const enum2. 短小函数定义换用内联函数 所以C出现内联函数以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数调 用建立栈帧的开销内联函数提升程序运行的效率。 如果在上述函数前增加inline关键字将其改成内联函数在编译期间编译器会用函数体替换函数的 调用。
查看方式
1. 在release模式下查看编译器生成的汇编代码中是否存在call Add2. 在debug模式下需要对编译器进行设置否则不会展开(因为debug模式下编译器默认不 会对代码进行优化以下给出vs2013的设置方式) 这里就直接展开了没有call去函数内部。
10.2 特性
1. inline是一种以空间换时间的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率。
2. inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议将函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下图为 《Cprime》第五版关于inline的建议 3. inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。?
// F.h
#include iostream
using namespace std;
inline void f(int i);// F.cpp
#include F.h
void f(int i)
{cout i endl;
}// main.cpp
#include F.h
int main()
{f(10);return 0;
}// 链接错误main.obj : error LNK2019: 无法解析的外部符号 void __cdecl \
f(int) (?fYAXHZ)该符号在函数 _main 中被引用11. auto关键字(C11)
11.1 auto简介 在早期C/C中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量但遗憾的 是一直没有人去使用它大家可思考下为什么 C11中标准委员会赋予了auto全新的含义即auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{return 10;
}
int main()
{int a 10;auto b a;auto c a;auto d TestAuto();//typeid -输出变量的类型cout typeid(b).name() endl;cout typeid(c).name() endl;cout typeid(d).name() endl;//auto e; 无法通过编译使用auto定义变量时必须对其进行初始化return 0;
}
运行结果 【注意】使用auto定义变量时必须对其进行初始化在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。因此auto并非是一种“类型”的声明而是一个类型声明时的“占位符”编译器在编译期会将auto替换为变量实际的类型。
11.2 类型别名思考
随着程序越来越复杂程序中用到的类型也越来越复杂经常体现在
1. 类型难于拼写2. 含义不明确导致容易出错
#include string
#include map
int main()
{std::mapstd::string, std::string m{ { apple, 苹果 }, { orange,橙子 },{pear,梨} };std::mapstd::string, std::string::iterator it m.begin();while (it ! m.end()){//....}return 0;
}
std::map::iterator 是一个类型但是该类型太长了特别容 易写错。聪明的同学可能已经想到可以通过typedef给类型取别名比如
#include string
#include map
typedef std::mapstd::string, std::string Map;
int main()
{Map m{ { apple, 苹果 },{ orange, 橙子 }, {pear,梨} };Map::iterator it m.begin();while (it ! m.end()){//....}return 0;
}
使用typedef给类型取别名确实可以简化代码但是typedef有会遇到新的难题 在编程时常常需要把表达式的值赋值给变量这就要求在声明变量的时候清楚地知道表达式的 类型。然而有时候要做到这点并非那么容易因此C11给auto赋予了新的含义。
11.3 auto的使用细则
1. auto与指针和引用结合起来使用
用auto声明指针类型时用auto和auto*没有任何区别但用auto声明引用类型时则必须加 2. 在同一行定义多个变量
当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错因为编译 器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量。 11.4 auto不能推导的场景
1. auto不能作为函数的参数
// 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}2. auto不能直接用来声明数组 3. 为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C11提供的新式for循环还有 lambda表达式等进行配合使用。
12. 基于范围的for循环(C11)
12.1 范围for的语法
在C98中如果要遍历一个数组可以按照以下方式进行
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循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。
void TestFor()
{int array[] { 1, 2, 3, 4, 5 };// 依次取数组中的数赋给e// 自动判断结束自动往后走for (auto e : array)e * 2;for (auto e : array)cout e ;
}
注意与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。
12.2 范围for的使用条件
1. for循环迭代的范围必须是确定的 对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供 begin和end的方法begin和end就是for循环迭代的范围。 注意以下代码就有问题因为for的范围不确定。 2. 迭代的对象要实现和的操作。(关于迭代器这个问题以后会讲现在提一下没办法 讲清楚现在大家了解一下就可以了)
13. 指针空值nullptr(C11)
13.1 C98中的指针空值
在良好的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)
{cout f(int) endl;
}
void f(int*)
{cout f(int*) endl;
}
int main()
{f(0);f(NULL);f((int*)NULL);return 0;
}
运行结果 程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的 初衷相悖。 在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器 默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void *)0。
注意 1. 在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入 的。2. 在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。3. 为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。