目录
1.程序的翻译环境和执行环境
2.C语言程序的编译+链接
3.预处理
预处理指令 #define
宏和函数的对比
预处理操作符#和##的介绍
命令定义
预处理指令 #include
预处理指令 #undef
条件编译
正文
1.程序的翻译环境和执行环境
在C语言标准实现过程中,存在两种环境:翻译环境和执行环境,前者用于将源代码转化为可执行的机器指令,后者用于实际执行代码
2.C语言程序的编译、链接
一个C语言文件的翻译过程如下:
每个源文件通过编译转换成目标文件;再由链接器捆绑在一起形成一个单一完整的可执行程序;链接器可同时引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。
2.1 编译过程
编译过程分成三个阶段,预编译,编译和汇编。
预编译:进行头文件的包含,宏、定义的替换以及注释的删除等,预编译后会生成.i文件
编译:把C语言代码翻译成汇编代码,进行了语法词法语义的分析,并进行了符号汇总,生成.s文件 《编译原理》
汇编:把汇编代码转换成二进制指令,生产.o文件(object目标文件),形成符号表
链接:合并段表,并进行符号表的合并和重定位
原理:
推荐书籍:《程序员的自我修养》
3.预处理
3.1预定义符号
常见C语言内置的预定义符号:
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
可以尝试运行一下这个
printf("%s %d %s %s", __FILE__, __LINE__,__DATE__,__TIME__);
3.2 #define
3.2.1#define 定义标识符:
#define MAX 100
#define reg register //为register创建一个更短的名字
#define do_forever for(;;)// 形象直观
#define CASE break;case //自动补上break的case
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__, \
__DATE__,__TIME__ )//定义内容过长时\充当续行符(不能加其他东西)
注:#define 定义时不要加;
#define MAX 100; //会把MAX替换成100;
if(condition)
max = MAX;
else
max = 0;
想一想这段代码有什么问题?
3.2.2#define 定义宏
#define定义时加上参数替换即为宏,一般声明方式为:
#define name(parament-list) stuff
parament-list为逗号隔开的符号表(参数),它们可能出现在 stuff 中
注:左括号(必须紧挨name
由于宏展开替换是在预编译阶段,因此如果参数中有表达式时,不会计算它的值而是直接把表达式替换进去,宏的本质就是替换。
#define SQUARE(x) x*x
int a = 5;
printf("%d",SQUARE(a+1));//想一想这个打印出来是什么??
//实际上上面打印出来是11,打印的是5+1*5+1
//所以我们最好带上括号:#define SQUARE(x) (x)*(x)
//可是这样也有意外:
#define DOUBLE(x) (X)+(X)
int a=5;
printf("%d\n",10*DOUBLE(a));//这个呢??
//这个打印出来是55而不是100,因为表达式为:10*(5)+(5)
//所以最好再加上一个括号#define DOUBLE(x) ((x)+(x))
3.2.3替换规则
3.2.4#和##
首先明白,字符串有自动连接的功能:
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);
打印出来直接就是hello bit
所以我们可以这样写:
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);
(FORMAT也为一中宏参数类型,例如"%d" "%f"等,同样的还有type mem...)
#的作用是把一个宏参数变成字符串
#define PRINT(FORMAT, VALUE)\
printf("the value of "#VALUE" is "FORMAT"\n", VALUE);
...
PRINT("%d", 11);
输出:the value of 11 is 11
##的作用是把两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。需要注意的是连接之后必须是合法的存在的标识符,否则结果是未定义的。
#define ADD_TO_SUM(num, value) \
sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
3.2.5带副作用的宏参数
#define MAX(a,b) (a)>(b)? (a):(b)
int a=5;
int b=7;
int c=MAX(a++,b++);
printf("%d\n",c);
printf("%d\n",a);
printf("%d\n",b);
这个宏替换就很容易产生副作用:宏替换后,int c =MAX(a++,b++);被替换为:
int c =(a++)>(b++)?(a++):(b++);
因此最终结果输出为 8 6 9
宏和函数的对比:
1.使用宏,宏代码会插入到程序中,而函数代码只出现在一个地方
2.函数需要在调用和返回时进行函数栈帧的创建和销毁,而宏不需要,因此宏更快
3.宏是把表达式替换,函数是把表达式求值后代入(参数为表达式时),因此宏可能会因为操作符优先级或者副作用产生bug
4.宏的参数与类型无关,因此只要对参数操作合法即可,而函数参数与类型有关
5.宏不方便调试,不能递归,函数可以。
注:一般行业规定把宏名字全部大写,函数名不全大写
3.3#undef
#undef 可以用于移除一个宏定义,如果应该名字需要被重新定义,那么它的旧名字需要首先被移除。
#define A 10
#undef A
#define A 100
3.4命令行定义
许多C编译器运行在命令行中定义符号用于启动编译过程
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
gcc -D ARRAY_SIZE=10 programe.c
3.5条件编译
其实和条件语句很像:
1.#if 常量表达式//注意必须是常量表达式,因此在预编译阶段还没有给变量开辟空间
//...
#endif
如:
#define __DEBUG__ 1
#if __DEBUG__//预编译阶段,常量表达式由预处理器求值
//..
#endif
加个else:
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
3.6文件包含
#include会引入文件,在预编译阶段,预处理器会把这条指令删掉然后用包含文件的内容替换。如果出现10次那么就会被编译十次
头文件被包含的方式:
#include"filename.h" 以及#include<filename.h>
后者查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
前者是现在源文件所在目录下查找,如果没有找到,编译器就像查找库函数头文件一样在标准位置查找头文件。再找不到就提示编译错误。
所以理论上#include"stdio.h"也是可以的,但是效率低些。
如果文件嵌套包含,比如:
为了避免头文件的重复引入,可以在每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif//思考一下为什么这样可以避免重复引入
或者直接加上#pragma once
3.7其他预处理指令
自行查阅资料了解,如《高质量C/C++编程指南》等
附加:
offsetof宏的实现:
#define offsetof(StructType, MemberName) (size_t)&(((StructType *)0)->MemberName)
StructType是结构体类型名,MemberName是成员名。具体操作方法是:
1、先将0转换为一个结构体类型的指针,相当于某个结构体的首地址是0。此时,每一个成员的偏移量就成了相对0的偏移量,这样就不需要减去首地址了。
2、对该指针用->访问其成员,并取出地址,由于结构体起始地址为0,此时成员偏移量直接相当于对0的偏移量,所以得到的值直接就是对首地址的偏移量。
3、取出该成员的地址,强转成size_t并打印,就求出了这个偏移量。