文章目录
1.程序的翻译和执行环境
对于C程序的实现,需要两种环境:
1.翻译环境,在这个环境中源代码被转换为可执行的机器指令。
2.执行环境,它用于实际执行代码。
2.详细编译+链接
2.1翻译环境
程序编译的过程:
我们可以看到,
第一步,编译器把每一个组成程序的源文件通过编译过程将其分别转换成目标代码
(object code)。
第二部,链接器(linker)将每一个目标文件捆绑在一起,形成一个单一而完整的可执行程序。
第三步,如果程序中的一些函数来自标准C函数库,链接器同时也会引入该库中的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2编译链接的几个阶段
由文件操作的部分内容得知:程序包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
总体上程序包括源程序文件(后缀为.c)通过编译(预编译、编译、汇编)
成为目标文件(windows环境后缀为.obj),在相关的库文件链接后
成为可执行程序(windows环境后缀为.exe)。
编译链接的大致过程:
预编译 | 作用:预处理(包含#include的头文件、替换或删除#define定义的符号、删除注释;操作文本…) |
---|---|
编译 | 作用:把C语言翻译成汇编代码(语法分析、词法分析、语义分析、符号汇总…) |
汇编 | 作用:把汇编代码翻译成了二进制指令,用来存放目标文件 |
链接 | 合并段表、符号表的合并和重定位 |
2.3运行环境
程序执行的过程:
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2.载入内存之后,程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈
(stack),存储函数的局部变量和返回地址
。程序同时也可以使用静态
(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
3.预定义符号
从上面的过程可知,系统编译一个C程序需要涉及很多的步骤,其中的第一个步骤是
预编译中的预处理
(preprocessing)阶段。C预处理器(preprocessor)在编译之前会进行一些文本性质的操作(包含#include的头文件、替换或删除#define定义的符号、删除注释...)
我们可以使用一些预处理指令进行编译:
符号 | 示例值 | 含义 |
---|---|---|
_ _FILE_ _ | “name.c” | 进行编译的源文件名 |
_ _LINE_ _ | 25 | 文件当前行的行号 |
_ _DATE_ _ | “Mar 12 2023” | 文件被编译的日期 |
__TIME__ | “20:00:00” | 文件被编译的时间 |
__STDC__ | 1 | 如果编译器遵循ANSI C,其值就为1,否则未定义 |
//例子:
printf("file:%s line:%d\n", __FILE__, __LINE__);
//这些预定义符号都是语言内置的。
4.#define
简单点说#define就是为数值命名一个符号。
#define A 10
int main()
{
printf("%d",A);
return 0;
}
//结果为10
4.1#define定义宏
同时,#define允许把参数替换到文本中,这种实现通常称为
宏
(macro)或定义宏
define macro)。
例子:用宏来定义一个加法
#include<stdio.h>
#define add(x,y) x+y
int main()
{
int a = 10, b = 20;
int sum;
sum=add(a,b);
printf("%d", sum);
return 0;
}
//结果是30
那如果是这样呢?
#include<stdio.h>
#define add(x,y) x+y
int main()
{
int a = 10, b = 20;
int sum=2*add(a,b);//将add(a,b)乘以一个2
printf("%d", sum);
return 0;
}
//结果是40
咦?结果是40
为什么不是60呢?2*add(a,b)=2*30=60❌
但实际上宏仅仅是把参数替换到函数之中,其中并没有对其做任何的运算,仅仅是把add(a,b)替换成了a+b。
原式应为 2*add(a,b)=2*a+b=2*10+20=40❎
如果要为60,应该对宏加上括号
#define add(x,y) (x+y)
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,
避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
4.2#define替换
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查
,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置
。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描
,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1.宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
4.3宏与函数
宏可以非常频繁的使用执行简单的计算
,比如在比较较大的数:
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2.其次,更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。换句话说,宏是类型无关的。
宏和函数相比的缺点:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨
。
4. 宏可能会带来运算符优先级
的问题,导致程容易出现错。
4.4命名约定
为了区分宏和函数我们规定:
把宏名全部大写
函数名不要全部大写
4.5#undef
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
5.条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#include<stdio.h>
#if 0 //此时条件为0不编译
int main()
{
int a=10;
printf("Hello Hurry");
return 0;
}
#endif
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
这些就是C语言有关程序环境和预处理的简单介绍了😉
如有错误❌望指正,最后祝大家学习进步✊天天开心✨🎉