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

头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。

根据经验,建议将:常量、宏定义、函数原型、结构体共用体枚举等的类型定义,c文件包含的库头文件放入头文件中;

全局变量的定义不是不可以放在头文件中,只是不建议放,因为在头文件被多个源文件包含时,容易引起重定义的问题。全局变量的定义就应该放在某个源文件中,然后在别的源文件中使用前用extern声明。至于全局变量的声明,干脆和定义放在一起就行了,没必要放在头文件中。

对于某个函数或者全局变量,如果不希望被外部引用,就加上static,并且不要放入头文件中。一般放在头文件中的,就默认是公开的,可以被外部访问的。

总结来说就是,c文件中一般就放全局变量和函数的定义。

每个c文件都应当对应一个同名的h文件(只是为了识别而约定俗成用同名,其实不一定)。

c也要包含自己的头文件。

注意:头文件在本质上是为了分类管理,导入头文件时,是直接展开的,跟直接写在c文件前面没有任何实质性区别。

犯了个错误,将全局变量放在了头文件,导致报错。

一定要十分注意这个问题。

 

引用头文件的语法

使用预处理指令#include可以引用用户和系统头文件。它的形式有以下两种:

#include <file>

这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。

#include "file"

这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。

引用头文件的操作

#include 指令会指示C预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 header.h,如下:

char *test (void);

和一个使用了头文件的主程序 program.c,如下:

int x;
#include "header.h"

int main (void)
{
   puts (test ());
}

编译器会看到如下的代码信息:

int x;
char *test (void);

int main (void)
{
   puts (test ());
}

在一个C文件中,函数的声明是很重要的。当我们在一个庞大的项目中,有很多个源文件,每一个源文件中都有很多个函数,并且需要在各个文件中相互穿插引用函数。

防止头文件重复包含

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中:什么情况下会重复包含?比如同一个c文件中引入了多次。

如果头文件中没有全局变量,那么重复包含头文件不会报错,只不过就是代码效率会有所降低,如果有全局变量,那么,多次引用就会报错。

Error: L6200E: Symbol a multiply defined (by public.o and main.o).

第一种方式:将#pragma once每个头文件的最上方

#pragma once用来防止某个头文件被多次include,
#pragma once是编译相关,就是说这个编译系统上能用,但在其他编译系统不一定可以,也就是说移植性差,不过现在基本上已经是每个编译器都有这个定义了。

第二种方式:#ifndef,#define,#endif用来防止某个宏被多次定义。

#ifndef,#define,#endif这个是C语言相关,这是C语言中的宏定义,通过宏定义避免文件多次编译。所以在所有支持C语言的编译器上都是有效的,如果写的程序要跨平台,最好使用这种方式

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

这种结构就是通常所说的包装器#ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

宏定义的名称是自己取的,比如这里的HEADER_FILE,在哪个文件中定义的,就代表着当前文件。

在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别。

方式1:

#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__

... ... //一些声明语句

#endif

方式2:

#pragma once

... ... //一些声明语句

1、#ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况。

2、#pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。

方式1 由语言支持所以移植性好,方式2 可以避免名字冲突
#pragma once方式产生于#ifndef之后,因此很多人可能甚至没有听说过。目前看来#ifndef更受到推崇。因为#ifndef受语言天生的支持,不受编译器的任何限制;而#pragma once方式却不受一些较老版本的编译器支持,换言之,它的兼容性不够好。也许,再过几年等旧的编译器死绝了,这就不是什么问题了。

补充:

我在头文件中定义了一个全局变量(对于变量来说,声明既是定义,但是带extern就肯定是声明),然后在一个c文件中重复包含了两次,虽然头文件已经进行了防止重复包含操作,但是仍然提示我有变量重复定义。于是我很疑惑,难道防止重复包含的命令不起作用了?

但其实是我脑筋一时没转过来,虽然在能够阻止在同一个文件中被重复包含,但是,不能阻止在别的c文件里也无效呀,非静态全局变量对所有文件是可见的,所以肯定还是重复定义。

预处理

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

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

我们日常说的编译器其实指的是这一整个工具链。

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

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

常见的预处理
(1)#include
(2)注释(替换成空格)
(3)#if  #elif  #endif  #ifdef
(4)宏定义(宏展开)

头文件包含
(1)#include <> 和 #include""的区别:<>专门用来包含系统提供的头文件(就是系统自带的,不是程序员自己写的),""用来包含自己写的头文件;更深层次来说:<>的话C语言编译器只会到系统指定目录去寻找这个头文件,而不会去当前目录下找,如果找不到就会提示这个头文件不存在。

系统指定目录指的是编译器中配置的或者操作系统配置的寻找目录,譬如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径。
(2)""包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。


总结+注意:规则虽然允许用双引号来包含系统指定目录,但是一般的使用原则是:如果是系统指定的自带的用<>,如果是自己写的在当前目录下放着用"",如果是自己写的但是集中放在了一起专门存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>。


头文件包含的真实含义就是:在#include<xx.h>的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句。过程在预处理中进行。

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

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

宏定义和typedef

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

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

编译

一般来说,无论是 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)一个C程序有多个.c文件组成,编译的时候多个.c文件是独立分开编译的。每个c文件编译的时候,编译器是按照从前到后的顺序逐行进行编译的。
(2)编译器编程时的顺序编译会导致函数/变量必须先定义/声明才能调用,这也是C语言中函数/变量声明的来源。
(3)链接过程中呢?应该说链接过程链接器实际上是在链接脚本指导下完成的。所以链接时的.o文件的顺序是由链接脚本指定的。如果链接脚本中明确指定了顺序则会优先考虑这个规则按照这个指定的顺序排布,如果链接脚本中没有指定具体的顺序则链接器会自动的排布。 

链接

C语言程序的组织架构:多个C文件+多个h文件
庞大、完整的一个C语言程序(譬如linux内核、uboot)由多个c文件和多个h文件组成的。


程序的生成过程就是:编译+链接。

编译阶段就是把源代码变成.o目标文件(二进制的机器码格式),目标文件里面有很多符号和代码段、数据段、bss段等分段。符号就是编程中的变量名、函数名等。

运行时,变量名、函数名通过链接能够和相应的内存对应起来。

.o的目标文件链接生成最终可执行程序的时候,其实就是把符号和相对应的段给链接起来,将各个独立分开的二进制的函数链接起来形成一个整体的二进制可执行程序。

链接的作用,就是根据既定的逻辑组织成最终的成品。

编译以文件为单位、链接以工程为单位
编译器工作时是将所有源文件依次读进来,单个为单位进行编译的。
链接的时候实际上是把第一步编译生成个单个的.o文件整体的输入,然后处理链接成一个可执行程序。

C语言中的符号有三种链接属性:外连接、内链接、无链接

外连接的意思就是外部链接属性,也就是说这家伙可以在整个程序范围内(言下之意就是可以跨文件)进行链接,譬如普通的函数和全局变量属于外连接。


内链接的意思就是(c文件内部)内部链接属性,也就是说这家伙可以在当前c文件内部范围内进行链接(言下之意就是不能在当前c文件外面的其他c文件中进行访问、链接)。static修饰的函数/全局变量属于内链接。


无连接的意思就是这个符号本身不参与链接,它跟链接没关系。所有的局部变量(auto的、static的)都是无连接的。

补充

为了使用1个函数,就包含了整个头文件,导致有很多用不到的函数声明?

由包含关键字#include构成的包含语句并不是C/C++语句,而是预处理语句。预处理语句在编译前处理,编译时就不存在了。如果用一个库函数,当然也要包含这个库函数所在的头文件,编译时打开这个头文件,在里头找到你需要的库函数,把它嵌入到你的代码中;编译完了那些头文件就都关闭了,只获取了一个你需要的函数。这事很好验证:找一个你编译、链接、执行成功的代码,记下它生成的可执行文件的大小;然后加入好多好多你用不着的头文件(尽管你用不着,但写上了,编译时就都打开),编译后再看看可执行文件是否增大了?答案是不会增大!??????????????????????

编译时分配。包括:全局、静态全局、静态局部三种变量。

运行时分配。包括:栈(stack): 局部变量。堆(heap): c语言中用到的变量被动态的分配在内存中。

分母为0属于什么错误

分母为0属于运行时错误,在编译时不会报错。

一些预定义宏

看这道题:

标准C中定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
__TIME__当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
__FILE__这会包含当前文件名,一个字符串常量。
__LINE__这会包含当前行号,一个十进制常量。
__STDC__当编译器以 ANSI 标准编译时,则定义为 1。

让我们来尝试下面的实例:

补充

多文件C语言项目:

1、简单的C语言程序(项目)只有一个C文件(a.c),编译的时候gcc a.c -o a,执行的时候./a
2、复杂的C语言程序(项目)是由多个C文件构成的。譬如一个项目中包含2个c文件(a.c, b.c),编译的时候 gcc a.c b.c -o ab,执行的时候 ./ab


实验:
在a.c和b.c中分别定义main函数,各自单独编译时没问题;但是两个文件作为一个项目来编译gcc a.c b.c -o ab的时候,就会报错。multiple definition of `main'
为什么报错?
因为a.c和b.c这时候组成了一个程序,而一个程序必须有且只能有一个main函数。


为什么需要多文件项目?为什么不在一个.c文件中写完所有的功能?
因为一个真正的C语言项目是很复杂的,包含很多个函数,写在一个文件中不利于查找、组织、识别,所以人为的将复杂项目中的很多函数,分成了一个一个的功能模块,然后分开放在不同的.c文件中,于是乎有了多文件项目。
所以,在b.c中定义的一个函数,很可能a.c中就会需要调用。你在任何一个文件中定义的任何一个函数,都有可能被其他任何一个文件中的函数来调用。但是大家最终都是被main函数调用的,有可能是直接调用,也可能是间接调用。

多文件项目中,跨文件调用函数:
在调用函数前,要先声明该被调用函数的原型。只要在调用前声明了该函数,那么调用时就好像这个函数是定义在本文件中的函数一样。

添加头文件就是这个作用。

跨文件的变量引用:

(1)通过实验验证得出结论:在a.c中定义的全局变量,在a.c中可以使用,在b.c中不可以直接使用,编译时报错 error: ‘g_a’ undeclared (first use in this function)
(2)想在b.c中使用a.c中定义的全局变量,有一个间接的使用方式。在a.c中写一个函数,然后函数中使用a.c中定义的该全局变量,然后在b.c中先声明函数,再使用函数。即可达到在b.c中间接引用a.c中变量的目的。
(3)想在b.c中直接引用a.c中定义的全局变量g_a,则必须在b.c中引用前先声明g_a,如何声明变量? extern int g_a;    

extern关键字:
extern int g_a;    这句话是一个全局变量g_a的声明,这句话告诉编译器,我在外部(程序中
不是本文件的另一个文件)某个地方定义了一个全局变量 int g_a,而且我现在要在这里引用它
告诉你编译器一声,不用报错了。

注意:对于函数来说,默认为extern。

不需要额外在声明时加extern,加不加是等价的。

但是,对于变量来说,需要加extern才能保证访问的是同一个全局变量。

问题:
1、我只在b.c中声明变量,但是别的文件中根本就没有定义这个变量,会怎么样?
答案是编译报错(连接错误)undefined reference to `g_b'
2、我在a.c中定义了全局变量g_a,但是b.c中没有声明g_a,引用该变量会怎么样?
答案是直接抱错了,未定义
3、在a.c中定义,在b.c中声明,a.c和b.c中都没有引用该变量,会怎么样?
答案是不会出错。只是白白的定义了一个变量没用,浪费了


结论:不管是函数还是变量,都有定义、声明、引用三要素。其中,定义是创造这个变量或者函数,声明是向编译器交代它的原型,引用是使用这个变量或函数。所以如果没有定义只有声明和引用,编译时一定会报错。undefined reference to `xxx'

在一个程序里面,一个函数可以定义一次,引用可以有无数次,声明可以有无数次。因为函数定义或者变量的定义实际上是创造了这个函数/变量,所以只能有一次。(多次创造同名的变量会造成变量名重复,冲突;多次创造同名的函数也会造成函数名重名冲突)。声明是告诉编译器变量/函数的原型,在每个引用了这个全局变量/函数的文件之前都要声明该变量/函数

局部变量能不能跨文件使用?
不能。因为局部变量属于代码块作用域。他的作用域只有他定义的那个函数内部。

静态局部变量能不能跨文件使用?
不能。因为本质上还是个局部变量。

讨论跨文件使用问题,只用讨论全局变量和函数就可以了。

函数和全局变量在C语言中可以跨文件引用,也就是说他们的连接范围是全局的,具有文件连接属性,总之意思就是全局变量和函数是可以跨文件看到的(直接影响就是,我在a.c和b.c中各自定义了一个函数func,名字相同但是内容不同,编译报错。)

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值