1.预处理符号
C语言中设置了一些预定义符号,可以直接使用,预定义符号是在预处理期间处理的。
__FILE__//代表当前进行编译的源文件
__LINE__//文件当前行号
__DATE__//文件当前日期
__TIME__//文件当前时间
__STDC__//如果编译器遵循ANSIC,其值为1,否则未定义
接下来,我们可以试验一下:
#include<stdio.h>int main()
{printf("%s\n",__FILE__);printf("%s\n",__DATE__);printf("%s\n",__TIME__);printf("%d\n",__LINE__);return 0;
}
经过预处理之后,代码变成了:
int main()
{printf("%s\n","test.c");//目前在编译的文件名printf("%s\n","July 19 2024");printf("%s\n","18:17:54");printf("%d\n",8);return 0;
}
2.#define定义字符常量
基本语法:
#define 名字 内容
即 #define name stuff
例子:
#define MAX 1000
#define reg register//为register这个关键字创了一个简短的名字
#define do_forever for(;;)//变成死循环了
#define CASE break;case//在写case语句时自动把break写上
//如果定义的语句过长,可以分成几行写,除最后一行外,每行最后都要加上一个反斜杠'\'(续行符)。注意:其后不能有空格,应直接回车,否则,续的就不是原来的语句了。
#define DEBUG_PRINT printf("file:%s\tline:%d\t\date:%s\ttime:%s\n",\__FILE__,__LINE__, \__DATE__,__TIME__)
思考:
在define定义标识符的时候,如果在末尾加上‘;’怎样?
比如:
#define MAX 1000
#define MAX 1000;
建议不要加‘;’,因为可能会导致一些问题。
例:
当用了:
printf("%d\n",MAX);
就会出问题。
如果是这个:
if(condition)max=MAX;
elsemax=0;
那么等替换之后,if和else之间就是两条语句,而没有大括号时if和else后面只能接一条语句,else就不知道是和哪个else匹配的,就会出现语法错误。
3.#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或者定义宏(define macro)。
下面是其
定义方式:
#define name(parament-list) stuff
其中的parament-list是一个由逗号隔开的符号表,带表参数,它可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果二者之间有任何的空白存在,参数列表就会解释为stuff的一部分。
举例:
#define f(x) x*x
4.带有副作用的宏参数
所谓的副作用就是在实现预想的结果的同时,影响了其他的结果。
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预料的后果。
例如:
#define MAX(a,b) ((a)>(b)?(a):(b))
.....
int x=5;
int y=8;
int z=MAX(x++,y++);
5.宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,他们首先被替换。
2.替换的文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3.最后,再对结果文本进行扫描,看看它是否包含任何由#define定义的符号。如果是,则重复上述处理过程。
注意:
1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2.当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索。
例:
#define M 10
但是“MARY"中的M是不会被替换的。
6.宏与函数的对比
宏通常被应用于执行简单的运算。
比如在两个数中找到较大数。
#define MAX(a,b) ((a)>(b)?(a):(b))
而如果用函数来完成,有一下缺点:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间要多。所以在小型计算中,宏比函数在程序的规模和速度上要更胜一筹。
2.更为重要的是,函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用,反之,宏可以适用于整型,长整型,浮点型等可以用>来比较大小的类型。宏的参数是类型无关的。
宏有些时候可以做到函数做不到的事。比如:宏的参数可以出现类型,而函数不可以:
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
和函数相比,宏的劣势:
1.每次使用宏时,一份宏定义的代码将插入到程序中。除非这段代码较短,否则可能大幅度增加程序的长度。
2.宏没法调试
3.宏由于是类型无关的,不够严谨。
4.宏可能带来运算符优先级的问题,导致程序容易出错。
至于第四点,有个例子:
#define MAX(a,b) a*b
//当a=3+1 b=1+2时
7.#和##
一.#
'#'运算符将宏的一个参数转换为字符串常量。它仅允许出现在带参数的宏的替换列表中。
‘#'运算符所执行的操作可以理解为“字符串量化”。
适用场景:
当我们想要打印出“The value of x is %d"的情况
例如:
The value of b is 12
The value of a is 1
而以上的语句大致相同,需要改变的是某个数据的名字以及数据的具体大小。
为了方便后面的讲解,我们需要补充一个知识点:
在c语言中,printf("Hello world!")与printf("Hello""world!”)的结果是一样的。
回到原来的问题:我们把这个语句定义为宏
即:
#define PRINT(n) printf("The value of"#x"is%d\n",x);
当我们使用这个宏时:
int a=1;
PRINTF(a);
就会变成:
The value of a is 1
而#x就是将x转化为"x"这个字符串。
如果还想在字符串中加一些内容,可以直接#内容。
注意:我们需要使用这个符号的一个原因是:宏在替换时,不会替换字符串内的内容。
二.##
'##'字符用在宏定义里可以把两个符号粘连在一起,合成一个符号。被称为记号粘合。
这样的连接产生的标识符必须是合法的,否则就是未定义的。
它有如下应用场景:
当我们想要找出两个数中的较小数时,不同的数据类型得写不同的函数,但是,这样的函数结构是类似的,于是,我们就想到用宏,然而,这些不同函数的名称也要随着参数改变,同时要是合法的,而宏的参数只有一个,那就是类型,所以,我们想到用记号粘合。
int int_max(int x,int y)
{return x>y?x:y;
}
float float_max(float x,float y)
{return x>y?x:y;
}
改法:
#define TYPE_MAX(type) \
type type##_max(type x,type y)\
{ \return (x>y?x:y); \
}
具体使用:
TYPE_MAX(int)
//在预编译中替换为:
//int_max(int x,int y)
//{
// return x>y?x:y;
}
TYPE_MAX(float)
//在预编译中替换为:
//float float_max(float x,float y)
//{
// return x>y?x:y;
//}int main()
{int m=int_max(2,3);float n=float_max(3.5f,4.5f);return 0;
}
8.命名约定
由于宏与函数的使用语法相似,所以语言上无法帮我们区分二者,所以我们通常用一下命名习惯区分:
1.宏的全名全部大写。
2.函数名不要全部大写。
当然,这个规定不一定要遵守,c语言本身就有一些没有遵守,例如:
offsetof———宏:计算结构体成员相较于结构体起始位置的偏移量。
9.#undef
这个指令用于移除一个宏定义。是可以直接写入主函数的,不过需要注意的是:它与#define相同,不用单独加';'来表示语句的结束。
例如:
#define NAME 10int main()
{#undef NAMEreturn 0;
}
10.命令行定义
许多编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本时,这个能力就能起到作用。
假如某个程序中声明了一个某长度的数组,如果机器的内存有限,我们需要一个很小的数组,但是另一个机器的内存大些,我们需要一个更大的数组时,就可以利用这个能力。
好处:代码可以根据需求快速调整。
11.条件编译
利用条件编译指令我们可以很方便的编译或放弃一条或一组语句。
例如:某些语句起到的是调试作用,我们希望在某些条件下使用。
#define _DEBUG_//当不想要ifdef后面的语句时,就注释掉这句int main()
{#ifdef _DEBUG_printf("6\n");#endifreturn 0;
}
常见的条件编译指令:
#if 常量表达式 //如果为假,后面的语句就不参与编译//...
#endif
//注意:一定要以#endif结尾//多分支条件编译#if 常量表达式//...
#elif 常量表达式//...
#else//...
#endif//判断是否被定义
#if defined(symbol)
#ifdef symbol#if !definede(symbol)
#ifndef symbol
注意:这些指令是可以相互嵌套的
12.头文件的包含
一.本地文件的包含:
#include"filename"
查找方式:先在源文件下的目录里找,如果该文件未被找到,编译器就会像找库函数一样在标准位置查找头文件。
如果找不到就显示编译错误。
二.库文件包含
#include<filename>
查找方式:直接在标准文件下查找,如果找不到就提示编译错误。
当然,库文件也可以用""包含,不过,这样的话效率就会低一些,也会没那么容易区分本地文件和库文件。
三.嵌套文件的包含
如果一个同文件被多次包含,编译器在预处理时就会多次替换头文件的内容,这样的重复包含对编译的压力很大。所以,我们可以使用一些方法避免这样的情况出现。
利用条件编译防止重复包含。
方法一:
在每个头文件里加上:
#ifndef __TEST_H__
#define __TEST_H__
//头文件中的内容
#endif
方法二:
在每个头文件中加上:
#pragma once
13.其他预处理指令
目前不想写了,可参考《C语言深度剖析》这本书。