个人主页:平行线也会相交
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创
收录于专栏【C/C++】
本文目录
- 程序的翻译环境和执行环境
- 翻译环境(C语言程序的编译+链接)
- 执行(运行)环境
- 预定义符号介绍
- 预处理指令
- 预定义指令#define
- 特别注意一个点:
- 在define定义标识符的时候,要不要最后加上;呢
- #define定义宏
- 特别注意
- 小总结
- 再次强调
- #define替换规则
- #和##
- 对于#
- 对于##
- 带副作用的宏参数
- 宏和函数的对比
- 为什么不用函数来完成此任务,原因有二:
- 宏和函数总结
- 了解一下
- 命名约定
- 预处理指令undef
- 命令行定义
- 条件编译
- 文件包含
- 其它预处理指令
程序的翻译环境和执行环境
从test.c到产生test.exe这个文件之前的这个阶段发生了什么事情,再由test.exe运行起来之后又会发生哪些事情呢?
在ANSIC的任何一次实现中,存在两个不同的环境。一个是翻译环境
,另一个就是执行环境
。
第一种是
翻译环境
,这个环境中源代码被转换成可执行的机器指令,机器指令就是只能处理二进制的指令。第二种是执行环境
,它用于实际执行代码。
翻译环境(C语言程序的编译+链接)
以下面代码为例:
1.预处理 选项 gcc -E test.c -o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
2.编译 选项 gcc -S test.c 编译完成之后就停下来,结果保存在test.s中。
3.汇编gcc -c test.c 汇编完成之后就停下来,结果保存在test.o中。
任何一个源文件都会作为一个单独的单元被编译器进行处理。
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
VIM学习资料
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
https://coolshell.cn/articles/5479.html
执行(运行)环境
程序执行的过程:
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2.程序的执行便开始,接着便调用main函数。
3.开始执行程序代码。这个时候出现将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。
4.终止程序。正常终止main函数;也可能是意外终止。
预定义符号介绍
预定义符号是本身就有的符号。
__FILE__ 进行编译的源文件
__LINE__ 文件当前的行号
__DATE__ 文件被编译的日期
__TIME__ 文件被编译的时间
__STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义
以上与定义符号都是语言内置的。
比如:
#include<stdio.h>
#define MAX 100 //这是我们自己定义的符号,并不是预定义符号
//预定义符号
int main()
{
/*printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);*/
//写日志文件
int i = 0;
int arr[10] = { 0 };
FILE* pf = fopen("log.txt", "w");
for (i = 0; i < 10; i++)
{
arr[i] = i;
fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\n",
__FILE__, __LINE__, __DATE__, __TIME__, i);
printf("%s\n", __FUNCTION__);
}
fclose(pf);
pf = NULL;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
预处理指令
#define
#include
#pragma pack(4) //设置结构体的默认对齐数
#if
#endif
#ifdef
#line
预定义指令#define
语法:
#define name stuff
举个例子:
#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__ )
#include<stdio.h>
#define MAX 100
#define STR "hehe"
#define reg register
int main()
{
reg int a;//等价于register int a;
int max = MAX;
printf("%d\n", max);
printf("%s\n", STR);
return 0;
}
特别注意一个点:
一定要注意这两者的区别:
左图并没有死循环,右图就陷入了死循环。
其实左图写出这样就好理解了,请看:
在define定义标识符的时候,要不要最后加上;呢
比如:
#define MAX 100;
#define MAX 100
建议不要加上;,这样容易导致问题,比如下面的场景:
if(condition)
max = MAX;
else
max = 0;
这里会出现语法错误。
#define定义宏
#define机制包括了一个规定,
允许把参数替换到文本中
,这种实现通常称为宏(macro)或定义宏(define macro)。简单来说我们定义的宏可以带有参数。
下面是宏的申明方式:
#define name(parament-list) stuff
其中的parament-list(parament-list意思是参数列表
)是由一个逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表中的左括号与name紧邻
。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
简单举例:
#include<stdio.h>
#define SQUARE(X) X*X
int main()
{
int ret = SQUARE(5);
//int ret = 5*5;
printf("%d\n", ret);
return 0;
}
特别注意
警告:这个宏存在一个问题,请看下面代码:
int a = 5;
printf("%d\n",SQUARE( a + 1 );
即:
#include<stdio.h>
#define SQUARE(X) X*X
int main()
{
//int ret = SQUARE(5);
int ret = 5*5;
//printf("%d\n", ret);
int ret = SQUARE(5 + 1);
printf("%d\n", ret);
return 0;
}
结果是不是36呢?请看:
为什么结果是11呢?宏的参数不是用来像函数一样进行传参的,而是进行替换的。
事实上5+1
替换到完成之后是这样的:5+1*5+1
,所以结果就为11。那假设我们要得到6*6=36这样的结果的话应该怎么办呢?代码应该如何改进呢?我们可以这样:
#define SQUARE(X) (X)*(X)
即:
最后结果就为36。
小总结
我们在写宏的时候:宏的参数如果是一个表达式,这个时候表达式里面这个操作符的优先级和替换进去之后这个表达式里面某些操作符的优先级如果不相同时,可能会导致这个表达式的计算顺序发生一些意外(不是我们期望的)。这时为了明确指明它的计算顺序,我们能够给宏替换进去的这些内容分别加上括号,让它变成一个独立的部分,这个时候就不容易出错了。
所以我们平时在写宏的时候不要吝啬括号。
现在我们在来举一个例子:
#include<stdio.h>
#define DOUBLE(X) X+X
int main()
{
int a = 5;
int ret = 10 * DOUBLE(a);
printf("ret=%d\n", ret);
return 0;
}
结果为什么是55呢?其实这里替换完成之后是这个样子的int ret = 10 *10 + 5;
,所以最后结果当然就是55了。那我们给X
加上括号,即#define DOUBLE(X) (X)+(X)
,大家可以自行去尝试,最后发现结果依然是55。
那我们试试该整体加上一个括号呢,即#define DOUBLE(X) ((X)+(X))
。
整体代码就变成了这样:
发现结果就变成了100了。
再次强调
注意:使用宏的时候不要吝啬括号,当然也不要过分的使用括号
,即#define DOUBLE (X) ((((X)+(X))))
这样就完全没有必要了。
所以:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,例如上面的#define DOUBLE(X) ((X)+(X))
,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。
如果是,它们首先被替换
。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1.宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
对于宏来讲是没有递归的概念的
。
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
对于注意第二点
举个栗子,请看:
#include<stdio.h>
#define MAX 10
int main()
{
printf("MAX=%d\n", MAX);
return 0;
}
所以,当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
。
#和##
对于#
我们如何把参数插入到字符串中呢?
我们先看一段代码:
#include<stdio.h>
int main()
{
printf("hello worle\n");
printf("hello ""world\n");
printf("hel""lo ""world\n");
return 0;
}
#include<stdio.h>
#define PRINT(X) printf("the value of "#X" is %d\n",X)
int main()
{
int a = 10;
int b = 20;
PRINT(a);
//等价于printf("the value of ""a"" is %d\n", a);
PRINT(b);
//等价于printf("the value of ""b"" is %d\n", b);
return 0;
}
这个时候就做到了一点:#的作用:把一个宏的参数直接转换为对应字符串,然后插入到字符串中
。
对于##
作用:
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
举个栗子:
#include<stdio.h>
#define CAT(X,Y) X##Y
int main()
{
int apple100 = 2023;
//printf("%d\n",apple100);
printf("%d\n", CAT(apple, 100));
//printf("%d\n", apple##100);
//printf("%d\n", apple100);
return 0;
}
注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
。
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预料的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作用
x++;//带副作用
注意:这种副作用是针对宏而言的,对于函数来说是不会有影响的。(因为当我们传一个参数的时候,如3+5会计算为8传过去,也就是函数的参数是计算好一个结果传过去,而不是替换过去。)
请看代码:
#include<stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 11;
//试着尝试猜想一下结果
int max = MAX(a++, b++);
printf("max=%d\n", max);
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
对于上述代码中的int max = MAX(a++, b++);
替换完成以后
就是int max = ((a++) > (b++) ? (a++) : (b++));
。
所以最后结果如下:
宏和函数的对比
宏和函数其实有时候特别相像。
请看:
#include<stdio.h>
int Max(int x, int y)
{
return (x > y ? x : y);
}
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 20;
int max = Max(a, b);//函数
max = MAX(a, b);//宏
return 0;
}
宏通常被应用于执行简单的运算。比如在两个数中找到较大的一个。
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
对于求不同类型的数据来求其最大值,我们就需要各种不同的函数,请看函数的写法:
#include<stdio.h>
int Max1(int x, int y)
{
return x > y ? x : y;
}
float Max2(float x, float y)
{
return x > y ? x : y;
}
int main()
{
int a = 10;
int b = 20;
float c = 30.0f;
float d = 40.0f;
int max1 = Max1(a, b);
float max2 = Max2(c, d);
printf("max1=%d\n", max1);
printf(",max2=%f\n", max2);
return 0;
}
那如果我们利用宏来求最大值呢?请看宏的写法:
#include<stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 20;
float c = 30.0f;
float d = 40.0f;
int max1 = MAX(a, b);
float max2 = MAX(c, d);
printf("max1=%d\n", max1);
printf("max2=%f\n", max2);
return 0;
}
此时利用宏来写程序就没问题,此时宏就相对来说更加干脆一些,只是完成了宏参数的替换。此时不同类型的数据求最大值我们都可以用同一个宏来搞定。
此时宏看起来也更加灵活一些。
在这里对于函数来说:函数在调用的时候会有函数调用和返回的开销,简单来说函数调用要做准备(我们可以通过函数调用时的汇编代码来看),这的确需要一些时间效率可能会相对低一些
。
在这里对于宏来说:宏就不会出现此问题。比如说上述代码中的int max1 = MAX(a, b);
在预处理阶段就完成了替换,替换成了int max1 = a > b ? a : b;
。并没有函数的调用和返回开销。所以此时此刻宏比函数效率更高一些。
为什么不用函数来完成此任务,原因有二:
1.用于调用函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在执行的规模和速度方面更胜一筹。
2.更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型、长整型、浮点型等可以用于来比较的类型。宏是类型无关的。
当然和宏相比函数也有劣势的地方:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没法调试的。(调试调的是可执行程序,即test.exe,而在编译中的预处理阶段就已经把宏的那些符号完成了替换,所以调试调的、执行的那个代码就已经不是我们看到的那个代码了,而是替换完成之后的那段代码,简单来说,宏已经被替换的不再是宏了。)
3.宏由于类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,导致程序容易出现错误。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。(宏由于没有类型检查,所以什么都可以传,我们就可以传个类型过去)
举个例子:
#include<stdio.h>
#define SIZEOF(type) sizeof(type)
int main()
{
int ret = SIZEOF(int);
printf("ret=%d\n", ret);
return 0;
}
这个地方我们发现宏的参数部分传了个类型过去。这个时候就比函数略有优势了。
再举个栗子:
#include<stdio.h>
#define MALLOC(num,type) (type*)(num*sizeof(type))
int main()
{
int* p = MALLOC(10, int);
//int* p = (int*)malloc(10 * sizeof(int));
return 0;
}
在这里,宏依然是传类型的,而函数就无法做到。
所以,我们要理性看待函数和宏的优缺点,它们各有优势,也各有缺点。
宏和函数总结
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的幅度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一段代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表示式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以使用宏时不要吝啬括号 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体的多个位置,所以带有副作用的函数求值可能会产生不可预料的后果 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏定参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 对于宏来讲是没有递归的概念的 | 函数当然可以递归啦 |
了解一下
在C99和C++中,引入了inline-内联函数,很好的解决宏的问题,也解决了函数的问题。内联函数具有宏的优点也有函数的优点,它把宏和函数的优点进行了一个很好的结合,可以代替宏来使用,可以了解一下。
命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:
把宏名全部大写,函数名不要全部大写(以免造成误会)。
预处理指令undef
这条指令用于移除一个宏定义。
举个栗子:
#undef NAME
如果现存的一个名字需要被重新定义,那么它的就名字首先要被移除。
命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中说明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
建议在Linux环境下进行演示。例如:
#include<stdio.h>
int main()
{
int arr[SZ] = { 0 };
int i = 0;
for (i = 0; i < SZ; i++)
{
arr[i] = i;
}
for (i = 0; i < SZ; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
编译指令:
gcc -D SZ=10 programe.c
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
举个栗子:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#ifdef DEBUG //如果DEBUG被定义过,那么printf("%d ", arr[i]);这个语句就参与编译,否则不参与。这就是条件编译(简单来说就是某个条件满足就参与编译,反之不参与)
printf("%d ", arr[i]);
#endif
}
return 0;
}
#include<stdio.h>
#define DEBUG //只要定义过就行,不给值也可以
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#ifdef DEBUG //如果DEBUG被定义过,那么printf("%d ", arr[i]);这个语句就参与编译,否则不参与。这就是条件编译
printf("%d ", arr[i]);
#endif
}
return 0;
}
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
事实上预处理指令中的一种是条件编译指令。
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值
如:
#define __DEBUG__1
#if __DEBUG__
//..
#endif
举个例子:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#if 0 //此时下面的语句不参与编译
printf("%d ", arr[i]);
#endif
}
return 0;
}
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#if 1 //此时下面的语句参与编译
printf("%d ", arr[i]);
#endif
}
return 0;
}
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
举个例子:
#include<stdio.h>
int main()
{
#if 1==1
printf("haha\n");
#elif 2==1
printf("hehe\n");
#else
printf("嘿嘿\n");
#endif
return 0;
}
3.判断是否被定义
//判断是否被定义的一种条件编译
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
#include<stdio.h>
int main()
{
#if defined(DEUBG)
printf("hehe\n");
#endif
return 0;
}
#include<stdio.h>
#define DEBUG 0
int main()
{
#if defined(DEBUG)
printf("hehe\n");
#endif
return 0;
}
现在我们反过来:
#include<stdio.h>
#define DEBUG 0
int main()
{
#if !defined(DEBUG)
printf("hehe\n");
#endif
return 0;
}
#include<stdio.h>
#define DEBUG 0
int main()
{
#ifndef DEBUG
printf("hehe\n");
#endif
return 0;
}
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option1();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
文件包含
我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现与#include指令的地方一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际上编译10次。
头文件被包含的方式:
- 本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program FILES (X86)\Microsoft Visual Studio 9.0\VC\include
注意按照自己的安装路径去找。
- 库文件包含
#include<filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用“”的形式包含?答案是肯定的,当然可以。
但是这样做查找到效率就低些,当然这样也不容易区分是库文件还是本地文件。
嵌套文件包含
如果出现了这样的场景
**如何解决这个问题?**答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
或者:
#pragma once
就可以避免头文件的重复引入。
其它预处理指令
#error
#pragma
#line
至此,本文结束。