程序环境与预处理
- 程序环境
- 翻译环境(编译+链接)
- 预编译
- 编译
- 汇编
- 链接
- 执行环境
- 预处理
- 预定义符
- #define定义的标识符
- 宏
- #define定义宏
- #define替换规则
- 宏的命名约定
- 带副作用的宏参数
- 宏和函数的比较
- 其它
- #和##的使用
- 字符串常量化运算符#
- 标记粘贴运算符##
- 命令行定义
- 文件包含
- 常见的编译指令
- #undef
- 条件编译指令
- 嵌套文件包含
程序环境
在ANSI C的任何一种实现过程中,存在两个顺序依次的环境。
第一种环境是翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)。
第二种是执行环境,它用于执行实际代码。
下面我们详细的了解下翻译环境
翻译环境(编译+链接)
翻译环境包括编译和链接两个部分
在VS中,编译器叫cl.exe,链接器叫link.exe
如下:
一个工程中可能包含多个.c和.h文件。
如下:
而编译过程又包括预编译,编译和汇编。翻译过程可以细化如下:
预编译
我们通过gcc编译器来生成.i的文件,命令如下:
gcc test.c -E -o test.i
生成的.i文件和源代码 的比较如下:
1 展开头文件。预编译的时候会展开源文件中包含的所有头文件。如①
2 #define所定义的宏命令的符号会被替换掉,同时删除#define所定义的宏命令。如②
3 删除注释。如③
编译
gcc test.c -S
生成的文件如下:
可以观察到此时的格式为汇编代码。
1 词法,语法,语义分析,主要是编译器根据语法标准进行判断。
2 在语法分析的时候可以得到很多的符号,在整个编译和链接的过程中,我们将函数名和变量名作为它们对应的符号名。符号的汇总是有规则的,我们只汇总函数名,全局变量和静态数据。
在上述的代码中,被汇总的符号只有main。
而在下图的代码中,被汇总的符号有:b,main,printf
汇编
gcc test.c -c
1 汇编就是利用汇编器将汇编代码转换为机器可以执行的指令。 可以看到经过汇编后,该文件转换为二进制文件。
2 每一个目标文件都会有一个符号表,该符号表是对在编译时的符号分析的汇总,在符号表内,每一个符号都对应有一个符号值。对于函数和变量来说,这个符号值就是它们的地址。
如下:
在test.c文件中,Add函数只是声明,并没有定义。因此,它的地址是一个随机值。
链接
链接器可以将上述生成的目标文件链接起来形成一个可执行文件。
1 符号表的合并和重定向
将所有目标文件中的符号表,进行合并,舍弃无效的地址,合并为一个新的符号表。
以上面的为例。
2 合并段表。上述生成的每一个目标文件中都含有各种类型的段,
大致如下
合并段表就是将每一个目标文件中的相同的段进行合并。
3 与链接库的合并。会引入标准库中的函数等,如上述的printf函数。
执行环境
在程序的执行过程中:
(1)程序必须载入内存中。在有操作系统的环境中,一般这个由操作系统完成。在独立的环境中,程序的载入必须手动安排,也可以通过可执行代码置于只读内存中完成。
(2)程序的执行开始,接着调用main函数。
(3)开始执行程序代码,程序将使用一个运行时堆栈(stack),存放函数的局部变量和返回地址;程序同时也可以使用静态(static)内存,存储在静态内存中的变量在程序的整个执行过程中一直会保留它的值。
(4)终止程序。正常main函数执行结束或者因意外终止。
预处理
预定义符
如
__FILE__ 进行编译的源文件
__LINE__ 文件当前的行号
__DATE__ 文件被编译的日期
__TIME__ 文件被编译的时间
__STDC__ 如果编译器遵循ANSI C标准,其值为1,否则未定义
#include <stdio.h>
int main()
{
printf("%s %d %s %s\n",__FILE__,__LINE__,__DATE__ ,__TIME__);
return 0;
}
我们通过预编译可以看到,如下:
#define定义的标识符
#define MAX 1000
#define reg register
#define do_forever for(;;)
#define CASE break;case
#define DEBUG_PRINT printf("%s %s ",\ //语句过长,可以用\分行
__FILE__\
__DATE__)
这里我们举两个例子。
#include <stdio.h>
#define MAX 1000
#define reg register
#define do_forever for(;;)
#define CASE break;case
#define DEBUG_PRINT printf("%s %s ", \
__FILE__,\
__DATE__) //语句过长,可以用\分行
// int main()
// {
// printf("%s %d %s %s\n",__FILE__,__LINE__,__DATE__ ,__TIME__);
// return 0;
// }
int main()
{
do_forever;
return 0;
}
我们通过预编译可以看到,如下:
int main()
{
switch(2)
{
case 1:
break;
case 2:
break;
case 3:
break;
default :
break;
}
switch(2)
{
case 1:
CASE 2:
CASE 3:
break;
default :
break;
}
return 0;
}
值得我们注意的是,在定义时,要避免使用 ;,否则容易造成语法错误.
宏
#define定义宏
宏的声明方式
#define name(parament-list) stuff
其中的parament-list是一个由,隔开的符号表,他们可能出现在stuff中。
注意参数列表的左括号必须与name相邻,如果两者之间存在任何空白,参数列表会被解释为stuff的一部分。
我们通过3个例子说明。
#define SQUARE(X) X*X
int main()
{
printf("%d\n",SQUARE(5));
return 0;
}
我们通过预编译可以看到,如下:
#define SQUARE(X) X*X
int main()
{
printf("%d\n",SQUARE(5+1));
return 0;
}
我们看到其结果并不是我们认为的36,可以通过预编译结果看到问题
我们可以对宏定义中的stuff添加括号。
如下:
#define SQUARE(X) (X)*(X)
int main()
{
printf("%d\n",SQUARE(5+1));
return 0;
}
#define DOUBLE(X) (X)+(X)
int main()
{
printf("%d\n",3*DOUBLE(5));
return 0;
}
这里结果是20的原因,也是一样的,需要添加括号。
#define DOUBLE(X) ((X)+(X))
int main()
{
printf("%d\n",3*DOUBLE(5));
return 0;
}
因此,在定义宏的时候,应尽量可能多的增加括号,避免操作符的优先级或者邻近操作符之间的相互影响。
#define替换规则
(1)在预编译时,首先会对代码进行检查,看看是否包含任何由#define定义的符号,如果有它们首先被替换
(2)替换文本后插入到程序中原来文本所在的位置
(3)再次对文本进行检查,是否包含#define定义的符号。如果有,则重复上述操作。(这是因为可能存在一个宏调用另一个宏的情况)
注意:
(1)宏不能出现递归的情况,但可以出现在其它宏的定义中
(2)当预处理器搜索#define定义的符号的时候,在字符串常量中出现的所定义的符号,将不会被搜索替换。
宏的命名约定
宏名要全部大写;函数名不要全部大写
带副作用的宏参数
什么叫带有副作用,
如下:
int a = 10;
int b = a + 1;//没有副作用,a值没有改变
int c = a++; //有副作用,a值发生改变
下面通过一个例子来说明
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a=3;
int b=5;
int c=MAX(a++,b++);
printf("%d %d %d",a,b,c);
return 0;
}
结果为4,7,6
通过编译预处理的结果,如下图,我们分析:
后置++,是先运算,再++;首先3>5,不成立,他将会跳到:后的表达式。之后a++,b++,a=4,b=6…在跳到冒号之后,会将b的值作为条件表达式的结果赋给c,那么c=6,此后b再++,b=7
与之相类似的函数,对比如下:
int Max(int x,int y)
{
return (x>y?x:y);
}
int main()
{
int a=3;
int b=5;
int c=Max(a++,b++);//a=4 b=6 c=5
printf("%d %d %d",a,b,c);
return 0;
}
同上,a=3,b=5,首先会被带入函数,之后a++,b++,则a=4,b=6.在函数的内部3>5,不成立,则函数会返回5,那么c=5
宏和函数的比较
宏的优点:
(1) 在较小型的计算中,多使用宏
(2) 用于调用函数和从函数返回的代码可能比实际执行这个小型的运算所需要的时间更多
(3)宏不需要特定的类型。函数只能在类型合适的表达式上使用,宏可以使用于整型,长整型等。 宏的缺点:
(1)每次使用宏时,一份宏定义的代码将会插入程序中,除非宏比较短,否则会大幅度增加程序的长度。
(2)宏是不能调试的
(3)宏由于类型无关,也就不够严谨
(4)宏可能带来运算优先级的问题,导致程序容易出错
因此,当功能简单时,可以考虑使用宏来实现;当功能比较复杂时,可以使用函数来实现。
其它
#和##的使用
字符串常量化运算符#
我们观察下例
int main()
{
int a = 3;
printf("the num of" " value is %d\n",a);
return 0;
}
通过结果我们可以判断出两个相邻的字符串具有自动连接为一个字符串的特点。
因此,我们可以用#define进行以下操作
#define PRINT(format,n) printf("the num of" " value is " format"\n",n)
int main()
{
int a = 3;
printf("the num of" " value is %d\n",a);
PRINT("%d", a);
return 0;
}
可以看出当宏参数为字符串时,我们可以将其置于其它字符串中。
当宏的参数不是字符串的时候,我们可以使用#,将宏参数改为对应的字符串,
如下:
#define PRINT(format,n) printf("the num of" " value is " #format"\n",n)
int main()
{
int a = 3;
printf("the num of" " value is %d\n",a);
PRINT(%d, a);
return 0;
}
标记粘贴运算符##
宏定义内的标记粘贴运算符会合并两个参数。
#define tokenpaster(n) printf("token" #n " =%d",token##n)
int main()
{
int token34 = 40;
tokenpaster(34);
return 0;
}
我们可以通过预编译过程来看到宏中的替换,如下:
命令行定义
c编译器提供了一种能力,允许在命令行定义符号,用于启动编译过程。
如下例:
#include <stdio.h>
int main()
{
int a=SIZE;
printf("%d\n",a);
return 0;
}
当我们这样表示 a=SIZE时,可以看到编译器是给出错误提示的。
但是,我们仍然可以在命令行输入指令,使其输出正确的结果。
参考链接:https://blog.csdn.net/weixin_38184741/article/details/89818658
文件包含
#include 指令可以使得其包含的文件被编译
头文件的包含包括两种方式:本地文件包含和库文件包含。
两种包含方式的查找策略是不同的。
本地文件包含:
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到;那么就去标准库函数路径下去查找,如果找不到就提示错误。
库文件包含:#include <filename>
查找策略:直接去标准库函数路径下去查找,如果找不到就提示错误。
值得注意的是,对于库函数也可以使用#include "filename"
的方式,但是这样查找的方式效率低,而且不易区分是本地文件还是库文件。
常见的编译指令
#undef
#undef 移除一个宏定义
#undef NAME
条件编译指令
在编译程序时,如果条件成立,则可以进行编译
1
#if 常量表达式
//这里是内容
#endif
#define EXIST 1
#if EXIST
#define MAX 12
#endif
int main()
{
int a=0;
a=MAX;
printf("%d\n",a);
return 0;
}
可以看到MAX符号有被编译。
当我们改变条件,如下
#define EXIST 0
#if EXIST
#define MAX 12
#endif
int main()
{
int a=0;
a=MAX;
printf("%d\n",a);
return 0;
}
MAX 符号没有被编译。
2多条分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3 判断是否被定义(4种表达方式)
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
嵌套文件包含
#include 指令可以使得其包含的文件被编译,当同一个头文件被多次包含的时候,他也会多次被编译,会造成文件内容的重复,我们可以使用条件编译解决该问题。
#ifnedf __TEST_H__
#define __TEST_H__
//需要包含的头文件
#endif
当首次执行该文件时,是没有定义 __TEST_H__
的,那么他就会定义 __TEST_H__
,同时执行需要包含的头文件;当再次执行该文件时, __TEST_H__
已经被定义,就不会执行后面的预处理命令,这样,头文件就会只被包含一次。
也可以直接使用
#pragma once
参考链接:https://blog.csdn.net/cainiaochufa2021/article/details/125661575