1.程序的翻译环境和执行环境
1.1翻译环境
1.2执行环境
2.预处理详解
2.1预定义符号
2.2#define
2.2.1#define定义标识符
2.2.2#define是否需要加上;
2.2.3#define定义宏
2.2.4#define替换
2.2.5#以及##的使用
2.2.6 带有副作用的宏定义
2.2.7宏定义与函数的比较
1.程序的翻译环境和执行环境
在ANSI C(标准c)中存在两个不同的环境
第一种是编译环境,在这个环境中源代码被转换为可执行的机器指令,计算机执行的是二进制的指令,但是我们所写的c语言代码是文本信息,计算机无法理解,翻译环境则将c语言的代码翻译成了二进制的指令,放在可执行程序中。
第二种是执行环境,用于执行实际的代码,即使执行二进制代码。
源文件经过编译器处理后,生成了对应的目标文件,各种目标文件与链接库一起经过链接器的处理就形成了可执行程序。
1.1翻译环境
我们在vs下分文件写了以下两段代码进行测试:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
extern Add(int, int);
int main()
{int a = 10;int b = 20;int c = Add(a, b);printf("%d", c);return 0;
}
//test.c文件
#define _CRT_SECURE_NO_WARNINGS
int Add(int a, int b)
{return a + b;
}
//add.c文件
将程序运行之后,我们找到文件所在的位置,打开debug目录,发现生成了对应的.obj文件
同时还生成了可执行程序.exe
以上只是笼统的介绍,下面我们将在vscode gcc环境下进行观察。
在此之前我们需要了解以下内容:
1.预处理,选项gcc -E test.c -o test.i,预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。-->test.i
2.编译 选项gcc -S test.c编译完成之后就停下来,结果存在test.s中--->test.s
3.汇编gcc -c test.c汇编完成后就停下来,结果保存在test.o文件中 --->test.c
将代码以-o的形式打出来,观察到在代码上方多出了接近800行的代码,与此同时,原本代码中的#include<stdio.h>也不见了,其实这800行代码就是头文件#include<stdio.h>的内容,不仅如此我们发现#define定义的值也全都替换了,我们在代码中所写的注释也不见了,
其实代码在此时进行了预编译
1.2执行环境
程序执行的过程:
1.程序首先载入内存中。 在有操作系统的环境中,该操作一般由操作系统来完成。在独立的环境中,程序的载入可以由手工完成,也可以通过可执行代码置入只读内存来完成。
2.程序的执行开始。 接着便调用 main 函数。
3.开始执行程序代码。 这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。
4.终止程序。 正常终止main函数,也可能是意外终止。
2.预处理详解
2.1预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
我们又观察到__STDC__在vs编译的环境下报错:未定义,则说明vs不支持ANSIC
2.2#define
2.2.1#define定义标识符
语法:#define name stuff
#include<stdio.h>
#define M 100
#define STR "abc"
#define FOR for(;;)int main()
{printf("%d\n", M);printf("%s\n", STR);FOR{printf("1");}return 0;
}
这一段代码是有错误的比如#define FOR for(;;),这只是为了说明#define 的用法。
#define DEBUG_PRINT printf("file:%s\tline:%d\t date:gs\ttime:%s\n",__FILE__,__LINE__,__DATE__,__TIME__)
另外像上方这个#define的内容比较多,我们对其可以换行,但是换行之后会报错,我们可以使用'\'这个续行符,使用方法是在每一行的末尾加上\;
#define DEBUG_PRINT printf("file:%s\tline:%d\t date:gs\ttime:%s\n",\
__FILE__,__LINE__,__DATE__,__TIME__)
2.2.2#define是否需要加上;
先来一段代码,首先这一段代码是有错误的
#include<stdio.h>
#define M 100;
int main()
{int a = 0;int b = 0;if (a > 5)b = M;elseb = -1;return 0;
}
在vs上else会标红,什么原因呢。
如果我们将上面的代码进行一下预编译,就会出现下面的结果:
#include<stdio.h>
#define M 100;
int main()
{int a = 0;int b = 0;if (a > 5)b = 100;;elseb = -1;return 0;
}
可以看到b=100后面跟了两个分号,相当于
b=100;
(空语句);
而else不知道该与谁匹配,所以报错了
2.2.3#define定义宏
语法:#define name(parament-list) stuff
需要注意的是参数列表的左括号必须与name紧邻,若两者之间有空白存在,参数列表就会成为stuff的一部分
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 2;int b = 0;int c = MAX(a,b);printf("%d", c);return 0;
}
那么宏定义如何实现的呢?我们依然是对该段代码进行预编译
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 2;int b = 0;int c = ((a)>(b)?(a):(b));printf("%d", c);return 0;
}
需要注意的是后面的x和y都需要分别加上括号,因为其中x,y可能是表达式,例如下面代码
#include<stdio.h>
#define SQUARE(x) x*x
int main()
{int a = 3;int b = SQUARE(a);printf("%d", b);return 0;
}
此时代码还可以正常运行
#include<stdio.h>
#define SQUARE(x) x*x
int main()
{int a = 3;int b = SQUARE(a+2);printf("%d", b);return 0;
}
对于这个代码我们期待的运算结果是25,但运算结果是11,问题出在哪里,老办法先对该段代码进行预编译:
#include<stdio.h>
#define SQUARE(x) x*x
int main()
{int a = 3;int b = a+2*a+2;printf("%d", b);return 0;
}
结合的顺序出现了问题。
你不会天真的以为只给x和y加上()就没事了吧,看下面的代码
#include<stdio.h>
#define DOUBLE(x) (x)+(x)
int main()
{int a = 3;int b =10*DOUBLE(a);printf("%d", b);return 0;
}
我猜已经不需要分析了,你已经知道了DOUBLE后面的stuff需要整体加上()!如下:
#include<stdio.h>
#define DOUBLE(x) ((x)+(x))
int main()
{int a = 3;int b =10*DOUBLE(a);printf("%d", b);return 0;
}
2.2.4#define替换
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.2.5#以及##的使用
#的使用
#include<stdio.h>
int main()
{
int a=10;
int b=20;
printf("the value of a is %d",a);
printf("the value of b is %d",b);
return 0;
}
在这样的情景下,我们想打印出a,b的值显得非常麻烦,那有没有简便的方法来实现这个功能呢? 封装一个函数? 经过尝试后发现这样是行不通的,打印的值的类型有很多种,封装函数无法实现,但是我们刚刚学习了宏定义,那么我们来尝试一下宏定义实现PRINT()
#include<stdio.h>
#define PRINT(n,format) printf("the value of "#n" is "format"\n",n);
int main()
{int a = 10;PRINT(a,"%d");return 0;
}
printf可以有以下的用法,一个字符串可以分为几个字符串来打印。
#include<stdio.h>
int main()
{int a = 10;printf("hello world\n");printf("hello ""world\n");return 0;
}
format是类型的意思
这里的#n就会变成"n"
#用来将宏参数转换为字符串,也就是宏参数的开头和末尾添加引号。
##的使用
1.可以把位于它两边的符号合成一个符号。
2.它允许宏定义从分离的文本片段创建标识符。
3.##只能在宏定义中使用
#include<stdio.h>
#define CAT(x,y) x##y
int main()
{int ab = 100;printf("%d", CAT(a, b));return 0;
}
2.2.6 带有副作用的宏定义
什么是副作用?
#include<stdio.h>
int main()
{int a = 10;int b= a + 1;printf("%d\n%d", a, b);return 0;
}
上面这段代码旨在让b比a大1,,下面这段是有副作用的:
#include<stdio.h>
int main()
{int a = 10;int b= ++a;printf("%d\n%d", a, b);return 0;
}
这段代码想改变b的同时也改变了a,具有一定的副作用。下面是关于副作用的代码
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 5;int b = 6;int c = MAX(a++, b++);printf("%d\n", c);printf("%d\n", a);printf("%d\n",b);return 0;
}
将宏定义的内容替换进去后得到的结果是
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 5;int b = 6;int c = ((a++)>(b++)?(a++):(b++))printf("%d\n", c);printf("%d\n", a);printf("%d\n",b);return 0;
}
首先是a++与b++比较,b++较大,则表达式1不执行,表达式2执行且b在经过比较时经历了++此时b为7则c为7,a只经过一次++,最后表达式二执行b再次++,得到结果a=6,b=8,c=7;
2.2.7宏定义与函数的比较
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
2.更为重要的是函数的参数必须声明为特定的类型所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型
宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没法调试的。3.宏与类型无关,当然就不够严谨
4.宏可能带来运算优先级的问题,导致程序容易出错
属性 | #define定义宏 | 函数 |
代 码 长 度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每都调用那个次使用这个函数时,地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周国表达式的上下文环境里除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括身 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数头型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |