C语言的程序的实现里边有两种不同的环境,翻译环境和执行环境。
在翻译环境中源代码被转换为可执行的机器指令(二进制的指令)。 执行环境用于实际执行代码 。
翻译环境
源文件(.c文件)通过编译器生产目标文件,再通过链接器引入标准库函数或者个人编写的库函数,形成一个单一且完整的可执行程序。
程序的编译大致分为编译、链接这两个阶段。编译又可以细分尾预编译、编译和汇编三个步骤。
预编译完成的工作有:1、(#include)头文件的包含2、(#define)定义符号的替换3、注释的删除。然后生产.i文件。
加上一个编译选项 -E 就可以使得 GCC 在进行完第一阶段的预处理之后停下来,生成一个默认后缀名为.i 的文本文件
编译完成的工作是将C语言代码翻译成汇编代码,(其中有语法分析、词法分析、语义分析和符号汇总(汇总的是函数接口名))生成.s文件。
加上一个编译选项 -S 就可以使得 gcc 在进行完第一和第二阶段之后停下来,生成一 个默认后缀名为.s 的文本文件。
汇编完成的任务是将汇编指令翻译成机器指令(二进制指令),形成符号表(这里的符号表里边包含这编译过程中的符号汇总,以及各类符号的地址),生成.o文件。
-c 则是让编译器在对汇编语言文件进行编译后停下来,这里会生成一个待链接的可执行文件,即为后缀名为.o的文件
链接完成工作的有:1、合并段表2、符号表的合并和重定位(对汇编形成的符号表进行合并)。
比如说,我们写一个Add.c的源文件和一个Test.c的源文件,在Add.c中实现加法的逻辑代码实现,在Test.c中对Add.c进行调用,再输出打印。下边的图解很清晰,请仔细查看
运行环境
程序的执行过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
预处理详解
#define定义符号
语法: #define name stuff 例如:#define MAX 1000
#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro)。
语法:#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#include<stdio.h>
#define SQUARE(x) (x) * (x)
int main(){
int a = 5;
printf("%d\n" ,SQUARE( a ) );//printf ("%d\n",a * a ),输出25
printf("%d\n" ,SQUARE( a + 1) );//printf ("%d\n",a + 1 * a + 1 )//输出11
}
对于这个问题的解决方法就是在宏定义上加上两个括号
#define SQUARE(x) (x) * (x)
注意:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。 1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。 2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。 3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。 注意: 1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。 2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#和##
使用 # ,把一个宏参数变成对应的字符串。
#define PRINT(N) printf("the value of "#N" is %d\n", N)
int main()
{
int a = 10;
PRINT(a);//输出:the value of a is 10
int b = 20;
PRINT(b);//输出:the value of b is 20
return 0;
}
使用##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define H(name,num) name##num
int main(){
int girl1=1;
printf("%d",H(girl,1));//输出1
}
宏和函数的对比
属 性 | #define定义宏 | 函数 |
代 码 长 度 | 每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 |
执 行 速 度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操 作 符 优 先 级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。 |
调 试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递 归 | 宏是不能递归的 | 函数是可以递归的 |
命名习惯
把宏名全部大写 函数名不要全部大写
#undef
#undef指令用于移除一个宏定义。
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件 编译指令。
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
常见条件编译指令
//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
文件包含
库文件包含
#include <文件名>
//查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
本地文件包含
#include "文件名"
//查找策略:先在源文件所在目录下查找,如果该头文件未找到,
//编译器就像查找库函数头文件一样在标准位置查找头文件。