程序的翻译环境
程序的执行环境
详解:C语言程序的编译+链接
预定义符号介绍
预处理指令 #define
宏和函数的对比
预处理操作符#和##的介绍
命令定义
预处理指令#include
预处理指令 #undef
条件编译
在ANSI C的任何一种实现中,存在两个不同的环境
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)
把C语言代码转换汇编代码
把汇编代码转换成机器指令(二进制指令)
gcc test.s -C —>test.out(obj)
此时发现符号表中看到的都是编译阶段汇总的全局符号。
//add.c
int Add(int x,int y)
{
return x+y;
}
//test.c
extern int Add(int x,int y)
int main()
{
int a=10;
int b=20;
int ret=Add(a,b);
return 0;
}
程序执行的过程
程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序载入必须由手工安排(嵌入式烧板子),也可能是通过可执行代码置入只读内存来完成
程序的执行便开始,接着便调用main函数
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
终止程序。正常终止main函数;也有可能是意外终止。
推荐:《程序员的自我修养》
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
工程特别大的时候记录日志信息,方便排查。
这些预定义符号都是语言内置的。
#include
int main()
{
printf("%s\n",__FILE__);
printf("%s\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
printf("%s\n",__FUNCTION__);//函数名
}
#include
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;
}
#define 是定义符号的
#define M 1000
int main()
{
int m=M;
printf("%d\n",m);
return 0;
}
举例;
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#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:
}
}
提问:在define定义标识符的时候,后面要不要加上“;”
理论上可以,但是实际不建议
#define M 1000;
int main(){
int m=M;//-->int m=1000; ; //有时候会导致出错
int b=10;
if(a>10) b=M; //b=1000; //; 多了一个;
else b=-M;
return 0;
}
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举例:
//#define 定义宏
#define SQUARE(X) X*X
int main()
{
printf("%d\n",SQUARE(3));//---->替换成printf("%d\n",3*3);
printf("%d\n",SQUARE(3+1));//7
}
再来一个反例
#define DOUBLE(X) (X)+(X)
int main()
{
printf("%d\n",10*DOUBLE(4));//44
}
要注意宏的机理:完全替换,放入3+1时,3+1替换X
所以定义宏的时候建议加括号,是完全括号
#define SQUARE(X) ((X)*(X))
在程序中扩展#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);//前面的M不会被替换
return 0;
}
如何把参数插入到字符串中?
#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");
//printf("the value of ""a"" is %d",X);
//打印: the value of a is 10
int b=20;
PRINT(b,"%d");
//打印: the value of c is 20;
int c=30;
PRINT(c,"%d");
//打印: the value of c is 30;
float f=5.5f;
PRINT(f,"%f");
printf("the value of ""f"" is ""%f",X);
}
“##” 把两个符号合成一个符号
#define CAT(X,Y) X##Y
int main()
{
int student1=100;
printf("%d\n",CAR(student,1));///输出100
//printf("%d\n",student##101);--->student1
}
int a=1;
int b=a+1;//b=2,a=1
int b=++a;//b=2,a=2
++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);//9 // int m= ( (a++)>(b++) ? (a++) : (b++))}
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个.
#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定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非 常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能 会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副 作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法 的,它就可以使用于任何参数类型. | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
命名约定
这条指令用于移除一个宏定义
如果现存的一个名字需要被重新定义
#define M 100int main(){ int a=M;#undef M//之后不想用M了 printf("%d\n",M);}
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
#include
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
//手动定义大小
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性地编译
#define PRINT ///看有无定义PRINT,没有定义PRINT就不会编译5行的printf
int main()
{
#ifdef PRINT
printf("hehe\n");
#endif
}
常见的条件编译指令
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1 //0的时候不参与编译,看常量表达式的值
#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()
{
/* #ifdef PRINT
printf("hehe\n");
#endif */
#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;
}
//define TEST //给不给值无所谓
int main()
{
//如果TEST定义了,下面参与编译
#ifdef TEST // <------>#if defined(TEST)
printf("test\n");
#endif
//如果HEHE不定义,下面参与编译
#ifndef HEHE //<-------> #if !defined(HEHE)
printf("hehe\n");
#endif
return 0;
}
本地文件包含
#include "filename"
库文件包含
#include
" "和< > 两者包含头文件的本质区别是:查找的策略的区别
查找策略:"先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
Linux环境的标准头文件的路径
VS环境的标准头文件的路径
注意按照自己的安装路径去找
从图中可以看出,comm.h头文件的内容在test.c中被重复包含两次。导致代码冗余(一份头文件至少几百行)
注:《高质量C++编程》附录中的卷子
笔试题:
1.头文件中的ifndef/define/endif 是干什么的
#include 和#include"filename.h"有什么区别?
参考《C语言深度解剖》
#error
#pragma
#line
#pragma pack();//设置对齐数