目录
该篇博客是对上一篇博客【C/C++】程序环境,探索程序的执行过程的衍生,这里的一些知识需要用到这篇博客,如果对此不了解的可以在此查看了解。
1.预定义符号
在预处理阶段有一些预定义符号是可以直接使用的,如下:
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
//当写代码时想要获得文件位置、行号、日期和时间可以使用以上命令
示例1:
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
我们可以清晰的看到这些预定义符号的作用。
示例2:
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __STDC__);
return 0;
}
//报出如下错误
error C2065: “__STDC__”: 未声明的标识符
- 我使用的是VS2019,由报错可知,该编译器不遵循ANSIC
2.#define
#define的作用为于以下两点
- #define 定义标识符
- #define 定义宏
2.1#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__ )
提问:
在define定义标识符的时候,要不要在后面加上”;“
可以通过一个例子来解答此事:
#define MAX 100;
#define MAX 100
//建议不要加符号
//在以下场景下会报错
if(condition)
max = MAX;
//添加分号,替换后为:max = MAX;;表示有两条语句
//在if下如果有两条或两条以上语句需要使用用大括号,否则报错
else
max = 0;
- 如果要加”;“需注意这个问题。
2.2#define定义宏
#define机制包括一个规定,允许把参数替换到文本中,这种实现通常称为宏或宏定义
下面事宏的声明方式:
#define name(parament-list) stuff
- parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
示例:
#define Max(a,b) ((a)>(b))?(a):(b)
int main()
{
int a = 10;
int b = 20;
int c = Max(a, b);
//替换后为:int c = ((a)>(b))?(a):(b);
printf("%d\n", c);
return 0;
}
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被释放为stuff的一部分。
如:
#define AQUARE(x) x*x
2.3#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在替换宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替代。
- 替换文件随后被插入到程序中原来文本的位置。对于宏、参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#define M 10
printf("hello M\n")
//在上诉字符串中有M,但不识别。
2.4 ‘#’ 和 ‘##’
如何把参数传入到字符串
我们先来看看下面的代码:
int main()
{
char* p = "hello ""world\n";
printf("hello"" world\n");
printf("%s", p);
return 0;
}
- 当我们把这个字符串分开,使用两个分号包含,依然可以出现所要的结果。
- 我们通过这个代码可以看出字符串是有自动连接的特点的。
#的作用
#define PRINT(value,format) printf("the "#value" is "format"\n", value)
int main()
{
int a = 10;
PRINT(a, "%d");
int b = 20;
PRINT(b, "%d");
float c = 3.2f;
PRINT(c, "%f");
return 0;
}
- 这里我们可以看出使用”#“,把一个宏参数变成对应的字符串。
##的作用
##可以把位于两端的符号合成为一个符号。
它允许宏定义从分离的文本片段创建标识符。
#define CAT(A,B) A##B
int main()
{
int AAABBB = 100;
printf("%d\n", CAT(AAA, BBB));
return 0;
}
- 这样的连接必须产生一个合法的标识符,否则其结果就是无效的。
2.5带副作用的宏参数
//方法1
int a = 1;
int b = a + 1;
//方法2
int a = 1;
int b = ++a;
- 方法2就是带有副作用的代码,给b赋值的同时,改变了a的值
- 带有副作用的意思是,完成某一块的时候,其他的区域也受到了影响
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int x = 4;
int y = 5;
int z = MAX(x++, y++);
printf("z=%d x=%d y=%d\n", z, x, y);
return 0;
}
- 该代码就是有副作用的,输出结果时,同时改变了,x和y的值。
2.6宏和函数对比
宏通常被应用于执行简单的运算。
比如在两个数中找出最大值。
#define MAX(a,b) ((a)>(b)?(a):(b))
为什么不用函数完成这个任务?
宏的优点
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
对于简单的运算,函数的调用需要进行压栈和销毁操作,真正的运行操作是很少的。
而宏的操作是直接进行操作,不需要进行其它的操作,相对于函数更加简便。
- 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏适用于整形、长整型、浮点型等可以用于比较的类型。
宏是类型无关的。
宏的缺点:
- 每次调用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
函数只有一份,每次调用都是在哪一个地方使用。
- 宏没法调试
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级问题,导致程序容易出现问题。
宏能够做到函数做不到的事
宏的参数可以出现类型,而函数做不到。
#define CALLOC(num,type) (type*)calloc(num,sizeof(type))
int main()
{
int* a = CALLOC(10, int);
return 0;
}
宏和函数的对比
- 代码长度
#define定义的宏:每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。
函数:函数代码只出现于一个地方;每次使用这个函数时,都会调用那个地方的同一份代码。 - 执行速度
#define定义的宏:更快
函数:存在函数的调用和返回的额外开销,所以会相对慢一些。
但对于比较大的程序,这点时间并不影响。 - 操作符号优先级
#define定义的宏:宏参数的求值时在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏书写的时候多些括号。
函数:函数参数只在函数调用的时候求值一次,它的结果值传递给函数。 - 带有副作用的参数
#define定义的宏:参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。
函数:函数参数只在传参的时候求值一次,结果更容易控制。 - 参数类型
#define定义的宏:宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。
函数:函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
int x = 10;
int y = 20;
ADD(x + y);//宏——将括号内的三个元素作为参数传递
add(x + y);//函数——将x和y相加后作为一个参数传递
- 调试
#define定义的宏:宏是不方便调试的
函数:函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 - 递归
#define定义的宏:宏是不能递归的
函数:函数是可以递归的
2.7命令约定
一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者。
我们平时的习惯是:
把宏名全部大写
函数名不要全部大写
但是有例外:
offset —— 宏
getchar —— 有些编译器上,也是宏
3.#undef
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重定义,那么它的旧名字首先要被移除。
- 如图,第一个M可以正常使用,而第二个M在使用#undef后便无法使用。
4.命令行定义
- 在VS环境下无法演示,需要在Linux环境下演示。
#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;
}
许多编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性就有点用处。
看上述代码,假定某个程序中声明一个某个很长的数组,如果机器内存有限,我们需要一个很小的数组,但是另一个机器内存大些,我们需要一个数组能够大些。
- 我们在命令行输入指令改变array数组的大小。
//linux环境演示:
gcc test.c -D ARRAY_SIZE=10
//test.c为代码所在源文件
在此目录下之前有一个test.c所以将文件名命名为test1.c
5.条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
调试性的代码(为方便调试写的代码),删除可惜(万一下次遇到同样的问题),保留又碍事,所以我们可以选择性的编译。
示例:
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
//如果标识符被定义,则执行条件语句。
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
示例2:
#include <stdio.h>
//#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
- 如上述所示,当定义了_DEBUG_(标识符,名字随便起)后,可以使用条件编译指令#ifdef _DEBUG_开始,使用#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
- 这些条件编译指令不是很难理解,自己代入应用后即可。
6.文件包含
我们已经知道,#include指令可以使另一个文件被编译。就像它实际出现于#include指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,就实际被编译10次。
6.1头文件被包含的方式
(1)本地文件包含
#include"filename"
查找策略:先在源文件所在目录下查找,如果该头文件为找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就报错。
(2)库文件包含
#include<filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
是不是可以说,对于库文件也可以使用“”的形式包含?
答案:可以
但是这样查找的效率就低些,当然这样也不容易区分是库函数还是本地文件。
6.2嵌套文件包含
comm.h和comm.c是公共模块
test1.h和test1.c使用了公共模块
test2.h和test2.c使用了公共模块
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
方法1:
使用条件编译,判断头文件是否被多次包含。
方法2:
#pragma once
- 两种方法都可以防止头文件重复被包含。