在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境,它用于实际执行代码。
从总体结构上来讲,编译器会将每一个源文件进行单独编译,最终形成一个目标文件(后缀为.obj)
然后,链接器将多个目标文件和所需要的链接库进行链接形成可执行程序(后缀为.exe)
编译又可分为三个阶段
1.预编译阶段 :
预编译的目的是处理预编译指令
具体规则如下 :
(1).处理 “#include"预编译指令,将”#include"所包含的文件插入到该预编译指令的位置
(2).删除所有的注释
(3).将 #define所定义的符号进行替换
(4).处理所有的条件预编译指令,如 “#if” “#ifdef” “#elif” “#else” “#endif”
(5).保留所有的 #pragma编译器指令,因为编译器要使用它们.
经过预编译阶段,生成后缀名为 .i 的文件
2 .编译阶段 :
编译阶段的目的是将 c语言代码转换成汇编代码
程序在编译阶段所做的事情主要有以下几件 :
(1).词法分析
源代码会被输入到扫描器,扫描器扫描过后会产生一些记号,这些记号为 关键字 ,标识符 , 字面量以及一些特殊符号,与此同时,扫描器也会完成其他工作,如将标识符放到符号表中,字面量放到文字表中.
(2).语法分析
语法分析器对扫描器扫描所产生的记号进行语法分析,检查是否有语法错误
(3).语义分析
前面的语法分析器仅仅对语法进行了检查,但对语句是否有意义并未做检查,语义分析器对语句是否有意义进行检查
(4).符号汇总
对全局变量和函数进行汇总
经过编译阶段,生成后缀名为 .s 的文件
3.汇编阶段 :
汇编阶段的目的是将汇编代码转换成二进制指令,即最终得到的目标文件(后缀为.obj)
在汇编阶段将汇总的符号形成符号表
链接阶段
链接阶段的目的是将目标文件及链接库通过链接器形成可执行程序
程序在链接阶段所做的事情 :
(1).合并段表
目标文件是按照 elf 文件格式进行组织的,elf 文件结构由多个段组成 ,合并段表就是将目标文件的相同段进行合并
(2).符号表的合并和重定位
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
用例 :
#include
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
}
(1). #define定义标识符
#define name stuff
用例 :
#define MAX 100
#define DEBUG_PRINT printf("%s\t%d\t\
%s\t%s\t\
__FILE__,__LINE__\
__DATE__,__TIME__");
(2). #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏
#define name(parament-list) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。 注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部 分。
宏使用时的注意事项 :
(1).
#include
#define SQUARE( x ) x * x
int main()
{
int a = 5;
printf("%d\n" ,SQUARE(a + 1) );
}
打印结果并不是我们所想要的 36 ,这是因为宏替换到文本中变成了 5 + 1 * 5 + 1 = 11
(2).
#include
#define DOUBLE(x) (x) + (x)
int main()
{
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
}
打印结果并不是我们所想要的 100 ,宏替换到文本中变成了 10 * (5) + (5) = 55
因此我们在写宏的时候 , 记得要加上括号,否则可能会因为操作符优先级的问题导致没有得到我们想要的结果
(3).
#include
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5,y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
//输出的结果是什么?
}
替换之后为 ( (x++) > (y++) ? (x++) : (y++) ) ,因为为后置++ , 5 > 8为假,执行 (y++) ,因此最终x = 6,y = 10,z = 9
使用 # ,把一个宏参数变成对应的字符串
#include
#define print(data,format) printf("the value of " #data " is "format,data)
int main()
{
int a = 10;
print(a, "%d");
}
// 打印结果为 the value of a is 10
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
#include
#define ADD_TO_SUM(num,value) sum##num += value
int main()
{
int sum5 = 10;
ADD_TO_SUM(5, 10);
printf("%d\n", sum5);
}
// 打印结果为 20
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执 行 速 度 | 更快 | 存在函数的调用和返回的额外开销, 所以相对慢一些 |
操 作 符 优 先 级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加 上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一 次,它的结果值传递给函数。表达式 的求值结果更容易预测 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参 数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次, 结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数, 即使他们执行的任务是相同的 |
调 试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
(1).
#if 常量表达式
//…
#endif
#include
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", i);
#if 1
printf("hehe\n");
#endif
}
}
(2).
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif
#include
int main()
{
#if 2 + 3
printf("5\n");
#elif 3 - 4
printf("-1\n");
#elif 5 - 2
printf("3\n");
#else 5 -5
printf("0\n");
#endif
}
(3). 判断是否被定义
#ifdef symbol
//…
#endif
#include
#define PRINT
int main()
{
#ifdef PRINT
printf("hehe\n");
#endif
}
#ifndef symbol
//…
#endif
#include
int main()
{
#ifdef PRINT
printf("hehe\n");
#endif
}
#if defined(symbol)
//…
#endif
#include
#define PRINT
int main()
{
#if defined(PRINT)
printf("hehe\n");
#endif
}
#if !defined(symbol)
//…
#endif
#include
int main()
{
#if !defined(symbol)
printf("hehe\n");
#endif
}
头文件包含的两种方式 :
(1). 库文件包含
#include
查找头文件直接去标准路径下查找,若找不到提示编译错误
(2). 本地文件包含
#include "filename.h"
查找头文件首先去当前工程的目录下去查找,若查不到再去标准路径下去查找
防止头文件重复包含的两种方式 :
(1).
#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif
(2).
#pragma once