C语言-程序环境与预处理

C语言-程序环境与预处理,第1张

本文主要对源代码经编译链接形成结果的过程中各时段处理的指令与形成结果进行解析。


程序的编译环境与执行环境

在ANSIC的任何一种实现中存在两种不同的环境

第一种是编译环境,在此环境中源代码被转换为可执行的机器指令(二进制码)

第二种是执行环境,在此环境中执行机器指令

程序员编写的代码程序是源程序,通过编译环境(编译器),源程序会转化成可执行文件(二进制的机器指令),机器通过运行环境( *** 作系统)来运行指令,最后得到程序员想要的结果。


在一个较大的项目中多个源文件各自通过编译器编译成为多个目标文件(后缀obj)并在链接器中加上链接库的作用生成可执行文件。


链接库指的是库函数所在的库文件或者是第三方的库文件。


VS,DEV等等是集成开发环境(英文名ide),集成开发环境有编辑,编译,链接与调试等功能。


VS环境的编译器为文件cl.exe,链接器为文件link.exe,这两个文件都能在安装包中找到。


不同环境的编译器与链接器名一般不同。


源文件需要经过编译与链接两个大过程才能运行,而编译又分为预编译,编译与汇编三个小过程,链接只有一个链接的过程。


在预编译环节进行的事项:

1.处理包含的头文件,此时被包含的头文件的代码会被拷贝一份并送入某程序包含此头文件的位置。


2.#define定义的符号的替换,#define使用的本质就是算式替换,次替换在预编译环节完成,此时也有其他的预处理指令被处理

3.对代码中注释掉的内容(不必要的内容)进行删除,提高效率。


上述处理又叫文本 *** 作,文本 *** 作过后会将.c文件转化为.i文件。


在编译环节进行的事项:

把C语言的代码翻译成为汇编代码(二进制代码),并进行语法分析,词法分析,语义分析与符号汇总。


其中符号汇总为把文件中的全局符号(如函数符号main,printf等)汇总统计起来,注意:被汇总统计的符号一般都是全局符号,局部的符号因为声明周期较短而不被统计。


最后将.i文件转化为.s文件

在汇编环节进行的事项:

把汇编语言中的指令变为二进制指令,并根据编译环节中统计的所有符号制作一个符号表。


最后将.s文件转化为.o文件(此时使用的是linux环境,.o文件相当于windows环境的obj文件,也就是目标文件)。


在链接环境进行的事项:

1.合成段表,2.符号表的合并与重定位。


3.链接多个.o(.obj)目标文件生成exe文件(可执行文件)

符号表的合并:多个.o文件(多个符号表)中可能有相同的符号,若有符号唯一则其单独占有一片空间(地址),若符号有重复则这个这个符号只在合并后的符号表中出现一次并且其地址为这个符号最初定义的文件所记录的地址。


通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。


链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被 *** 作系统装入执行的统一整体。


在此过程中会发现被调用的函数未被定义。


需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。


运行环境:

程序执行时必须先载入内存中,在有 *** 作系统的环境中这个环节由 *** 作系统完成。


在独立的环境中程序的载入应该手动完成,也可能是通过可执行代码置于内存来完成。


之后程序开始运行,从main函数开始,这时候程序将使用一个运行时堆栈(栈区)来储存函数的局部变量与返回地址,程序也可以使用静态内存,储存于静态内存中的变量在程序的整个执行过程中一直保留着他们的值,直到程序终止(程序可能正常终止也可能遇到错误终止)。


预处理:

预定义符号:由语言内置的特殊意义的符号。


_FILE_:进行编译的源文件的文件标识符

_LINE_:文件当前的行号

_DATE_:文件被编译时的日期(运行时编译就是此刻)

_TIME_:文件被编译的时间

_STDC_:如果编译器遵循ANSIC 则此符号值为1,否则值不确定

_FUNC_:为此代码所在函数的函数名 与_FUNCTION_相同

预定义符号的内容都是当前程序运行时的信息。


若想记录当前信息(编译的时间,日期,文件标识符等),则创建文件并用fprintf将此时所有信息存入文件中,便于日后查看。


#define定义标识符常量和宏

#define定义标识符常量:

#define name stuff(内容) 注意:结尾不用分号,若加了分号则name被替换为 内容; 不便观察

用法举例如:#define NUM 100 #define STR “abcdef”

在预处理阶段#define定义的标识符会被替换为对应的值或者表达式。


在正确的语法下可随便用#define定义内容。


可以:#define reg register register为一个类型修饰符 int ret n=100; 的语法正确,#define本质上还是替换内容。


#define定义宏:

#define机制包括了一个规定,允许把在外传入的参数替换到文本中,这种实现通常称为宏或定义宏。


#define 宏名(参数列表(无类型)) (需要替换的内容)

注意:宏名与(参数列表)需要紧贴,不然宏会被认为是参数列表的替换,书写错误。


宏的本质实际上也是算式替换,只不过多了一个传参机制。


使用宏时也需注意:

举例:                          

 此时得到的结果应该是10的平方(100),但最后结果为19

因为宏的使用本质上是算式替换,所以此时为:9+1*9+1=19,

此种情况表明传入表达式使可能会改变计算的路径,需要注意。


优化:将x*x改为(x)*(x),同理之前的比大小宏中定义时的参数也可以加上括号防止计算路径的改变。


也可以将(x)*(x)改为((x)*(x))进一步规范。


最好将参数与结果都加上括号规范。


#define替换规则(步骤):

1.宏中传入参数若有被#define定义过的标识符常量,标识符常量会优先完成替换。


2.进行宏替换

3.检查文件中是否还有未被替换的#define定义过的标识符常量,有就再重复上述过程。


注意:1.宏参数与#define定义中可以出现其他#define定义的符号,但是对于宏来说不能出现递归,2.当预处理器搜索#define定义的符号时,字符串常量的内容并不被检索到。


#与##

#的作用:

使用printf时若printf括号中是几个字符串,则这几个字符串的内容会被连续打印出来。


如printf("asd""acd");结果为asdacd

若此时定义一个宏:#define PRINT(N) printf("the value of "#N"is %d\n",N)

使用宏打印结果为the value of (传过来的参数的名字) is (传过来的参数的值)

#N实际上替换为:"传过来的参数的名字"

若改成#define PRINT(N,format) printf("the value of"#N"is "format"\n",N)

format可以是"%d","%lf"等其他内容,这就实现了多类型通用的“打印函数”。


##的作用:

##可以把位于它两边的符号合并成为一个符号

注意:合并后产生的符号必须是一个合法的标识符,否则结果不可预测。


如#define CAT(name,num) name##num CAT(class,105)的结果为class105(class与105被合并成为一个整体)。


若main中已经定义 int class105=100;就可以使用printf("%d",CAT(class,100));

 带副作用的宏参数:

如使用时对#define定义的标识符的值进行改动,则在之后的程序中使用时可能会出现错误。


举例:

 结果为:9 6 10。


解析:c计算那一行:a先与b比较,比较过后a为6 b为9,结果为假-意味着后面的(a++)不进行而(b++)进行,c先被赋值为9,b再变为10,随后打印。


宏与函数对比:

代码长度

宏:因为宏的使用原理为替换,若宏较长则会导致源代码较长。


函数:函数代码只出现与一个地方放,每次调用这个函数时会调用函数的代码。


运行速度

宏:替换代码费时较小。


函数:时间与空间上的开销都较大。


*** 作符优先级

宏:除非在定义时加上括号规范,不然可能会出现错误。


函数:因为调用的是一块代码,所以运算的正确性更高。


副作用

宏:参数可能被替换到宏中的多个位置。


函数:函数参数只在传参时求值一次,结果更容易控制。


参数类型

宏:宏适用于多类型的参数。


函数:参数列表是固定的。


调试

宏:不方便调试。


函数:可以逐语句调试。


递归

宏:无法递归。


函数:可以递归。


命名约定:一般宏名全大写,函数名不全大写。


#undef (定义的标识符或宏名字); 作用为清除名字对应的#define定义效果

此语句可以写在main中任意地方,所以当不用宏时就可以写此语句。


命令行定义:如果在源文件中有未定义的变量或其他形式存在的代码,在编译环节命令行中通过特殊符号可以完善未定义的内容,从而让编译通过。


条件编译:

 我们的目的是对数组中元素赋值,为了检查是否正确写了printf检查,不用时printf可以注释掉,但在实际情况中检查需要的代码偏多,注释又有点麻烦,这时候使用条件编译语句将一个或多个语句控制是否会被编译。


条件编译语句:

 1.常量表达式结果为真则编译,否则不编译。


 2.多分支的条件编译,与if使用类似,#endif表示条件编译语句结束。


3.判断是否被定义(有多个写法):

#ifdef MAX

语句...;

#endif

#if defined(MAX)与#ifdef MAX 的作用一样,若MAX已经定义,则编译执行语句(如此时printf),否则不编译执行语句。


可以#if !defined(MAX)来判断MAX是否未定义,未定义则执行编译语句。


这两个条件被称为反向条件

#ifdef MAX对应的反向语句为#ifndef MAX

嵌套定义:

 注意:#endif是与上面最近的#ifdef配套的。


文件包含:

#include指令可以使另一个文件在此编译,就像它实际出现于#include指令的地方一样。


替换原理:预处理器先删除这条指令,并用包含文件的内容替换。


这样如果一个源文件被包含10次,也就会被编译10次,效率会下降。


头文件被包含的方式:

1.本地文件包含

#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未被找到,则编译器就像查找库函数头文件那样从标准位置查找头文件(按照安装路径查找),若还是找不到就提示编译错误。


2.库函数包含

#include

查找策略:查找头文件直接去标准路径下去查找,找不到则提示编译错误。


库函数也可以用双引号“”包含,但是这样查找的效率变低,也不易区分库函数是库文件还是本地文件。


由于包含文件本质上是把被包含文件全部代码复制进入包含的区域中,在完成大项目时可能会出现以下情况:

 

不同的test文件每个都包含了同一个文件,这几个test文件在链接合并时需要加载编译包含的同一块代码,造成空间与时间的浪费。


解决方法:

1.

在编译环节命令行每个头文件前两行写上:

#ifndef _TEST_H_

#define _TEST_H_

......

在代码最后加上#endif

解法2.

在每个头文件开头写上#pragma once 也可以防止一个文件被包含多次。


其他预处理指令:

 

 除此之外还有其他预处理指令,可自行搜索。


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

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

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

发表评论

登录后才能评论

评论列表(0条)

保存