程序环境和预处理
-
程序的翻译环境 -
程序的执行环境 -
详解:C语言程序的编译+链接 -
预定义符号介绍 -
预处理指令 #define -
宏和函数的对比 -
预处理操作符#和##的介绍 -
命令定义 -
预处理指令#include -
预处理指令 #undef -
条件编译
1.程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境
- 翻译环境,在这个环境中源代码被转化成可执行的机器指令
- 执行环境,用于实际执行代码
2.详解编译+链接
2.1翻译环境
test.c ,contact.c ,common.c都会各自经过编译器的处理,各自生成目标文件(win下:test.obj,contact.obj,common.obj)。然后通过链接器,链接obj和库生成可执行程序(.exe / .out)
比如用了fread,链接库就会链上LIBC.LIB,LIBCMT.LIB,MSVCRT.LAB
gcc test.c ---->直接生成.out
gcc test.c -E ---->预处理之后停下来 ,东西太多,重定向 > xxx(test.i)
预处理阶段
-
完成了头文件的包含
2.
-
#define定义的符号和宏的替换
-
-
-
注释删除
-
-
编译阶段
把C语言代码转换汇编代码
gcc test.i -S ------>生成test.s
汇编阶段
把汇编代码转换成机器指令(二进制指令)
gcc test.s -C —>test.out(obj)
- 汇编
- 生成符号表(test.o --elf格式)
- 每个.o文件是同样的段格式
- readelf工具可以看
- 把汇编指令转换成机器指令
此时发现符号表中看到的都是编译阶段汇总的全局符号。
int Add(int x,int y)
{
return x+y;
}
extern int Add(int x,int y)
int main()
{
int a=10;
int b=20;
int ret=Add(a,b);
return 0;
}
链接阶段
2.2运行环境(执行环境)
程序执行的过程
-
程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序载入必须由手工安排(嵌入式烧板子),也可能是通过可执行代码置入只读内存来完成 -
程序的执行便开始,接着便调用main函数 -
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
-
终止程序。正常终止main函数;也有可能是意外终止。
推荐:《程序员的自我修养》
3.预处理详解
3.1预定义符号
__FILE__
__LINE__
__DATE__
__TIME__
__STDC__
工程特别大的时候记录日志信息,方便排查。
这些预定义符号都是语言内置的。
#include<stdio.h>
int main()
{
printf("%s\n",__FILE__);
printf("%s\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
printf("%s\n",__FUNCTION__);
}
#include<stdio.h>
int main()
{
FILE* pf=fopen("log.txt","a+");
if(pf==NULL)
{
perror("fopen\n");
return 1;
}
for(int i=0;i<10;i++)
{
fprintf(pf,"%s %d %s %s %d\n",__FILE__,__LINE__,__DATE__,__TIME__,i);
}
fclose(fp);
fp=NULL;
return 0;
}
3.2#define
3.2.1 #define定义标识符
#define 是定义符号的
#define M 1000
int main()
{
int m=M;
printf("%d\n",m);
return 0;
}
举例;
#define MAX 1000
#define reg register
#define do_forever for(;;)
#define CASE break;case
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
#define Case break;case
int main()
{
int n=0;
switch(n)
{
case 1:
CASE 2:
CASE 3:
}
switch(n)
{
case 1:
break;
case 2:
break;
case 3:
}
}
3.2.2#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举例:
#define SQUARE(X) X*X
int main()
{
printf("%d\n",SQUARE(3));
printf("%d\n",SQUARE(3+1));
}
再来一个反例
#define DOUBLE(X) (X)+(X)
int main()
{
printf("%d\n",10*DOUBLE(4));
}
要注意宏的机理:完全替换,放入3+1时,3+1替换X
3.2.3#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。注意是全部替换,不要遗漏。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
#define M 100
#define MAX(X,Y) ( ((X)>(Y))? (X):(Y) )
int main()
{
int max=MAX(101,100);
int ma=MAX(101,M);
printf("M= %d\n",M);
return 0;
}
- 注意:
- 宏参数和#define 定义中可以出现其他#define定义的常量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
3.2.4 #和##
如何把参数插入到字符串中?
#define PRINT(X,FORMAT) printf("the value of "#X" is "FORMAT"",X)
void print(int x)
{
printf("the value of c is %d\n",x);
}
int main()
{
printf("hello world\n");
printf("hello ","world\n");
int a=10;
PRINT(a,"%d");
int b=20;
PRINT(b,"%d");
int c=30;
PRINT(c,"%d");
float f=5.5f;
PRINT(f,"%f");
printf("the value of ""f"" is ""%f",X);
}
3.2.5带有副作用的宏参数
int a=1;
int b=a+1;
int b=++a;
++a是有副作用的
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。
副作用就是表达式求值的时候出现的永久性效果。
例如
#define MAX(X,Y) ( (X)>(Y)?(X):(Y) )int main(){ int a=5; int b=8; int m=MAX(a++,b++); printf("") printf("%d\n",m);
3.2.6宏和函数的对比
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个.
#define MAX(a, b) ((a)>(b)?(a):(b))
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比 函数在程序的规模和速度方面更胜一筹。 [看反汇编]
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
#define MAX(a, b) ((a)>(b)?(a):(b))int max(int a,int b){ return a>b?a:b;}int main(){ int a=5; int b=10; int c=MAX(a,b); int d=max(a,b); return 0;}
当然和宏相比函数也有劣势的地方:
-
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。 -
宏是没法调试的。
- 代码经过编译,链接,生成可执行程序才能调试
-
宏由于类型无关,也就不够严谨。 -
宏可能会带来运算符优先级的问题,导致程序容易出现错。宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
-
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int* p=MALLOC(10,int);
}
-
属性 | #define定义宏 | 函数 |
---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非 常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 | 执行速度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 | 操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能 会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。 | 带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副 作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一 次,结果更容易控制。 | 参数类型 | 宏的参数与类型无关,只要对参数的操作是合法 的,它就可以使用于任何参数类型. | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的 | 调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 | 递归 | 宏是不能递归的 | 函数是可以递归的 | | | |
-
命名约定
3.3 #undef
这条指令用于移除一个宏定义
如果现存的一个名字需要被重新定义
#define M 100int main(){ int a=M;#undef M
3.4命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
#include<stdio.h>
int main()
{
int arr[M]={0};
for(int i=0;i<M;i++)
{
arr[i]=i;
}
for(int i=0;i<M;i++)
{
printf("%d ",i);
}
return 0;
}
gcc test.c -D M=xxx
3.5条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性地编译
#define PRINT
int main()
{
#ifdef PRINT
printf("hehe\n");
#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
#if 0
#define PRINT 1
int main()
{
#if PRINT
printf("hehe\n");
#endif
return 0;
}
#endif
int main()
{
#if 1==1
printf("hehe\n");
#elif 2==2
printf("haha\n");
#else
printf("hehei\n");
#endif
return 0;
}
int main()
{
#ifdef TEST
printf("test\n");
#endif
#ifndef HEHE
printf("hehe\n");
#endif
return 0;
}
3.6文件包含
3.6.1头文件被包含的方式
" "和< > 两者包含头文件的本质区别是:查找的策略的区别
3.6.2嵌套文件包含
从图中可以看出,comm.h头文件的内容在test.c中被重复包含两次。导致代码冗余(一份头文件至少几百行)
注:《高质量C++编程》附录中的卷子
笔试题:
1.头文件中的ifndef/define/endif 是干什么的
#include<filename.h> 和#include"filename.h"有什么区别?
4.其他预处理指令
参考《C语言深度解剖》
#error
#pragma
#line
#pragma pack();
|