C语言预处理和编译问题总结

C语言预处理和编译问题总结,第1张

头文件

为什么需要头文件?
从之前可以看到,函数的声明是很重要的。当我们在一个庞大的项目中,有很多个源文件,每一个源文件中都有很多个函数,并且需要在各个文件中相互穿插引用函数。


怎么解决函数的声明问题?靠头文件。

#include包含头文件时,用<>和""的区别
<>用来包含系统自带的头文件,系统自带指的是不是你写的,是编译器或者库函数或者 *** 作系统提供的头文件。
""用来包含项目目录中的头文件,这些一般是我们自己写的。

防止重复包含头文件
#ifndef __A_H__
#define __A_H__
// C语言头文件中的声明
#endif

写程序时,最好不要在头文件中定义变量。因为这时该头文件被多个源文件包含时,就会出现重复定义问题。全局变量的定义就应该放在某个源文件中,然后在别的源文件中使用前是extern声明。

预处理C语言的预处理详解_绘夜的博客-CSDN博客_c预处理

由源码到可执行程序的过程
(1)源码.c->(编译)->elf可执行程序
(2)源码.c->(编译)->目标文件.o->(链接)->elf可执行程序
(3)源码.c->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
(4)源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序

预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。

预处理的意义
(1)编译器本身的主要目的是编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。
(2)预处理器帮编译器做一些编译前的杂事。

编程中常见的预处理
(1)#include(#include <>和#include ""的区别)
(2)注释
(3)#if  #elif  #endif  #ifdef
(4)宏定义

gcc中只预处理不编译的方法
(1)gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。
(2)gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。

总结:宏定义被预处理时的现象有:第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义);第二,typedef重命名语言还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理器处理的)

头文件包含
(1)#include <> 和 #include""的区别:<>专门用来包含系统提供的头文件(就是系统自带的,不是程序员自己写的),""用来包含自己写的头文件;更深层次来说:<>的话C语言编译器只会到系统指定目录(编译器中配置的或者 *** 作系统配置的寻找目录,譬如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件(隐含意思就是不会找当前目录下),如果找不到就会提示这个头文件不存在。
(2)""包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。
总结+注意:规则虽然允许用双引号来包含系统指定目录,但是一般的使用原则是:如果是系统指定的自带的用<>,如果是自己写的在当前目录下放着用"",如果是自己写的但是集中放在了一起专门存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>。
(3)头文件包含的真实含义就是:在#include的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句。过程在预处理中进行。

注释
(1)注释是给人看的,不是给编译器看的。
(2)编译器既然不看注释,那么编译时最好没有注释的。实际上在预处理阶段,预处理器会拿掉程序中所有的注释语句,到了编译器编译阶段程序中其实已经没有注释了。

条件编译
(1)有时候我们希望程序有多种配置,我们在源代码编写时写好了各种配置的代码,然后给个配置开关,在源代码级别去修改配置开关来让程序编译出不同的效果。
(2)条件编译中用的两种条件判定方法分别是#ifdef 和 #if
区别:#ifdef XXX判定条件成立与否时主要是看XXX这个符号在本语句之前有没有被定义,只要定义了(我们可以直接#define XXX或者#define XXX 12或者#define XXX YYY)这个符号就是成立的。
的格式是:#if (条件表达式),它的判定标准是()中的表达式是否为true还是flase,跟C中的if语句有点像。

一般来说,无论是 C、C++、还是 pas, 首先要把源文件编译成中间代码文件,在 Windows 下也就是 .obj 文件,UNIX 下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的 Object File 合成执行文件,这个动作叫作链接(link)。 编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在 C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O 文件或是 OBJ 文件)。链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O 文件或是 OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在 Windows 下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是 Archive File,也就是 .a文件。

总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。而在链接程序时,链接器会在所有的 Object File 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一是:Link 2001错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File。

*** 作系统究竟是个什么玩意?
像人类社会一样的计算机软件系统(有些人只埋头干活,有些人只做管理)
(1)人类社会最开始时人人都干活,这时候没有专业分工,所有人都直接做产生价值的工作。当时是合适的,因为当时生产力低下,人口稀少。这就像裸机程序一样(裸机程序的特点是:代码量小,功能简单、所有代码都和直接目的有关,没有服务性代码)。
(2)后来人口增加生产力提高,有一部分人脱离了直接产生价值的体力劳动专职指挥(诞生了阶级)。本质上来说是合理的,因为资源得到了更大限度的使用,优化了配置,提升了整体效率。程序也是一样,当计算机技术发展,计算机性能和资源大量增加,这时候写代码也要产生阶级也要进行分工,不然如果所有代码都去参加直接性的工作,则整体系统效率不高。(因为代码很难进行资源的优化配置)。
(3)解决方案就是 *** 作系统。 *** 作系统就是分出来的管理阶级, *** 作系统的代码本身并不直接产生价值,它的主要任务是管理所有资源,它主要为直接产生价值、直接劳动的那些程序(各种应用程序)提供服务。所以 *** 作系统既是管理者也是服务者。
(4)裸机程序就好象小公司, *** 作系统下的程序就好象大型跨国公司;裸机程序就好象小国家, *** 作系统下程序就好象大国家;如果我们要做一个产品,软件系统到底应该是裸机还是基于 *** 作系统呢?本质上取决于产品本身的复杂度。只有极简单的功能、使用极简单的CPU(譬如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于 *** 作系统来开发。

*** 作系统的调用通道:API函数
(1) *** 作系统负责管理和资源调配,应用程序负责具体的直接劳动,他们之间的接口就是API函数。当应用程序需要使用系统资源(譬如内存、譬如CPU、譬如硬件 *** 作)时就通过API向 *** 作系统发出申请,然后 *** 作系统响应申请帮助应用程序执行功能。

C库函数和API的关系
(1)单纯的API只是提供了极简单没有任何封装的服务函数,这些函数应用程序是可用的,但是不太好用。应用程序为了好用,就对这个API进行了二次封装,把它变得好用一些,于是就成了C库函数。
(2)有时完成一个功能,有相应的库函数可以完成,也有API可以完成,用哪个都行。譬如读写文件,API的接口是open write read close;库函数的接口是fopen fwrite fread fclose。fopen本质上是使用open实现的,只是进行了封装。封装肯定有目的(添加缓冲机制)。
不同平台(windows、linux、裸机)下库函数的差异
(1)不同 *** 作系统API是不同的,但是都能完成所有的任务,只是完成一个任务所调用的API不同。
(2)库函数在不同 *** 作系统下也不同,但是相似性要更高一些。这是人为的,因为人下意识想要屏蔽不同 *** 作系统的差异,因此在封装API成库函数的时候,尽量使用了同一套接口,所以封装出来的库函数挺像的。但是还是有差异,所以在一个 *** 作系统上写的应用程序不可能直接在另一个 *** 作系统上面编译运行。于是乎就有个可移植性出来了。
(3)跨 *** 作系统可移植平台,譬如QT、譬如Java语言。

*** 作系统的重大意义:软件体系分工
(1)有了 *** 作系统后,我们做一个产品可以首先分成2部分:一部分人负责做 *** 作系统(开发驱动的);一部分人负责用 *** 作系统实现具体功能(开发应用)。实际上上层应用层的功能进一步复杂化后又分了好多层。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1324428.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存