C语言---预处理详解

2024-09-23 20:55:59 浏览数 (3)

1.预定义符号

C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。

代码语言:javascript复制
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
代码语言:javascript复制
int main()
{
    printf("%sn", __FILE__);//文件名
    printf("%dn", __LINE__);//行号
    printf("%sn", __DATE__);//日期
    printf("%sn", __TIME__);//时间
    //printf("%sn", ____STDC___); //无法运行,索命gcc并没有完全遵循ANSI C
    //但是在vscode环境中是可以运行的
    return 0;
}

2.#define定义常量

代码语言:javascript复制
//#define M 100
//#define STR "hehe"
///*
//像这种#定义的M是怎么使用的呢?
//在预编译阶段的时候会将M后面的内容替换到M出现的位置
//*/
//int main()
//{
//    int a = M;
//    int arr[M] = { 0 };//在预编译阶段M已经被替换为100了
//    printf("%dn", M);
//    printf("%sn", STR);//这里的STR已经被替换成hehe了
//    return 0;
//}

/*
更加牛逼的写法
*/
//#define CASE break;case
//int main()
//{
//    int n = 0;
//    scanf("%d", &n);
//    switch (n)
//    {
//    case 1:
//        break;
//    case 2:
//        break;
//    case 3:
//        break;
//
//    }
//    return 0;
//}
/*
上面的就是正常的写法了,但是我们将上面的CASE定义为break;case后代码就能进行建议更改了

*/
//int main()
//{
//    int n = 0;
//    scanf("%d", &n);
//    switch (n)
//    {
//    case 1:
//        //..
//    CASE 2://break;case 2这就是替换过来的
//        //..
//    CASE 3:
//        //..
//        
//    
//
//    }
//    return 0;
//}
/*
但是这个不是一种很好的写法,没有一个很好的阅读体验
*/

//那么我们在使用#define的时候要不要在后面加上分号呢?
//有时候加上也不会有太大的问题,但是不建议加
//加上可能会导致问题

//#define M 100;
//int main()
//{
//    int a = M;//如果在上面对M进行定义的时候在后面加上了分号的话
//    //那么这里的M会被替换为100;
//    printf("%d", M);//这种直接会报错
//    return 0;
//}

//所以我们在对变量进行定义的时候别在后面加分号,加上分号的话可能会出现两个语句

后面不能加分号

3.#define定义宏

define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(define macro)。

宏其实是有参数的

下⾯是宏的申明⽅式:

define name( parament-list ) stuff

parament-list是参数列表,意味着宏是有参数的

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的

⼀部分。

如果name和括号直接加了空格的话,编译器会认为这里是将name定义为后面的东西的

代码语言:javascript复制
//实现一个宏,求平方
//#define SQUARE(n) n*n
SQUARE是宏的名字,括号内是宏的参数,宏的参数会替换到宏的体内去
//
//int main()
//{
//    int x = 0;
//    scanf("%d", &x);
//    int ret = SQUARE(5   1);//计算的是x的平方
//    //相当于这里的SQUARE(x)被替换为x*x
//    printf("%d", ret);
//    return 0;
//}
/*
宏和函数有不同点,宏没有参数类型,也没有函数的大括号
宏呢,适合完成那些相对简单的任务

*/
//但是我们假设传的是5 1的话,那么宏里面的计算内容就是这样的
//5 1*5 1
//得到的就是11
//所以我们得到的结果在宏内的参数是不会进行运算的
//而是直接将参数替换到宏内的运算里面
//宏的参数是不进行计算直接替换进去的

//但是我们给5 1加上一个括号就能计算(5 1)*(5 1)的内容了

//我们在写宏的时候不能吝啬符号


//求一个数的二倍
#define double(n) n n
int main()
{
    int n = 0;
    scanf("%dn", &n);
    int ret = double(n);
    printf("%d", ret);
    ret = 10 * double(5);
    //当我们想计算10*(5 5)的时候
    //这里的double可能会被替换为5 5
    //那么代码就会变成这样10*5 5

    //解决方法就是在宏的定义中将n n整体加上括号
    printf("%d", ret);
    return 0;

}
/*
所以我们在写宏的时候一定不要吝啬括号
*/

我们在写宏的时候一定不要吝啬括号

所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的

操作符或邻近操作符之间不可预料的相互作⽤

4.带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可

能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。

副作用就是表达式的求值会出现永久的效果

代码语言:javascript复制
int main()
{
    int a = 10;
    //int b = a   1;//b=11,a=10;//a没有做出改变
    int b =   a;//b=11,a=11;这个表达式为了获得b,a获得了牺牲
    //那么下面的代码就具有副作用,为了得到一个新的值,改变了自身
    return 0;

}
/*
x 1  不带有副作用
  x  带有副作用

下面 的写法对a进行了彻底的改变
*/
代码语言:javascript复制
//如果我们将这种带有副作用的表达式放到宏的参数里面会发生什么现象呢?


写一个宏,求两个数的较大值
//#define MAX(x,y) ((x)>(y)?(x):(y))//x大于y的话,那么较大值就是x,否则就是y
//int main()
//{
//    int a = 10;
//    int b = 20;
//    int m=MAX(a,b);
//    printf("%d", m);
//    return 0;
//}
在这个宏内,参数都出现了两次


//当我们对参数进行改变,变成a  ,b  
#define max(x,y) ((x)>(y)?(x):(y))
int main()
{
    int a = 10;
    int b = 20;
    //int m=max(a  ,b  );//因为是后置  ,所以我们将a先传过去,b先传过去

    //替换后的代码--后置  ,先使用再  
    int m = ((a  ) > (b  ) ? (a  ) : (b  ));
             //10      20     x         21   
             // 在前面b先用,再加加,所以到后面,b就是21了  ,再后面又是b  ,所以先使用21的值,然后再自增
   //所以这个表达式执行完之后,a变成了11,b变成了22
    printf("%d", m);
    printf("%dn", a);
    printf("%dn", b);
    return 0;
}
//在这个宏内,参数都出现了两次

5.宏替代的规则

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

宏是不能出现递归的,不能在宏内自己调用自己

代码语言:javascript复制
#define M 15
#define max(x,y) ((x)>(y)?(x):(y))
int main()
{
    int m = MAX(M, 15);
    //根据替换规则,这里的M是宏定义的符号,所以M先被替换
    // 
    // 替换过程,首先对参数进行检查,再对本身进行检查
    //int m = MAX(100, 15);
    //int m = ((100)>(15)?(100):(15))
    printf("%d", m);
    printf("MAX(M,15)");//字符串内的宏并不会进行运算
    return 0;
}

6.宏函数的对比

代码语言:javascript复制
//宏
#define max(x,y) ((x)>(y)?(x):(y))

//函数
int Max(int x, int y)
{
    return x > y ? x : y;
}
int main()
{
    int m = MAX(10, 20);
    printf("%dn", m);


    m = Max(10, 20);
    return 0;
}
//函数是有函数体的,函数具有数据类型,但是宏没有

宏通常被应⽤于执⾏简单的运算。⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。

那为什么不⽤函数来完成这个任务?

原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆的。

和函数相⽐宏的劣势:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
  2. 宏是没法调试的。
  3. 宏由于类型⽆关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏的参数是直接带进去不进行计算的

但是函数的参数是计算好之后再带进去的

宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到

代码语言:javascript复制
#define MALLOC(n,type) (type*)malloc(n*sizeof(type))
int main()
{
    //开辟10个整型字节大小的空间
    //int *p=(int *)malloc(10 * sizeof(int));

    int *p=MALLOC(10, int);//函数是不能传类型的,那么我们写一个宏

    return 0;
}

宏和函数的一个对比

7.#和##

#运算符

运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执⾏的操作可以理解为”字符串化“

代码语言:javascript复制
//#define PRINT(format,n)   printf("the value of n is "format"n",n)
//int main()
//{
//    int a = 10;
//    //创建一个宏
//    PRINT("%d",a);
//    //printf("the value of a is %dn", a);
//    
//    int b = 20;
//    PRINT("%d", b);
//    //printf("the value of b is %dn", b);
//    
//    float f = 5.5f;
//    PRINT("%f", f);
//    //printf("the value of f is %dn", f);
//    return 0;
//}
//写一个宏,既能适用于浮点型又能适用于整型
//#define PRINT(format,n) printf("the value of n is "fromat,n)
/*
这个宏的参数是打印的数据类型和要打印的数--format和n
因为在打印字符串的时候
printf("helloworld");
printf("hello""world");
这两种打印结果是一样的,都是helloworld


那么我们这里就写
printf("the value of n is "fromat,n)
因为我们传的format是"%d"
那么和前面的代码进行组合的话得到的就是
printf("the value of n is ""%d",n)
无异于
printf("the value of n is %d",n)

我们在后面加上"n"
就是相当于三个字符串进行相加
"the value of n is "
fromat
"n"
就是这三个字符串
*/
/*
the value of n is 10
the value of n is 20
the value of n is 5.500000
但是还没有达到我们要的结果
我们想将n改变成我们要打印的字母

那么我们应该怎么做呢?
*/

#define PRINT(format,n)   printf("the value of "  #n  " is "format"n",n)
int main()
{
    int a = 10;
    //创建一个宏
    PRINT("%d", a);
    //printf("the value of a is %dn", a);

    int b = 20;
    PRINT("%d", b);
    //printf("the value of b is %dn", b);

    float f = 5.5f;
    PRINT("%f", f);
    //printf("the value of f is %dn", f);
    return 0;
}

/*
对宏进行修改
printf("the value of" #n "is "format"n",n)
"the value of"这个是一个字符串
#n  当我们传过来的是n是a的话,那么#a就变成了"a",就间接的变成字符串了

那么替换后的代码就是这样的
printf("the value of" "a" "is ""%d""n",n)

那么#n的作用就是将n变为对应的字符串
*/

/*
打印结果:
the value of a is 10
the value of b is 20
the value of f is 5.500000
就是我们所想要的结果

*/

对于宏的参数内的元素n,出现#n的地方就会替换为“n”,将n变成字符串

##运算符

可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

这⾥我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。

代码语言:javascript复制
//这个宏是生成函数的模版
#define FUNC(type)
type type##_max(type x,type y)
{
    return x>y?x:y;
}
//使用模版调用函数,函数名字叫int_max
FUNC(int)
/*
替换步骤:
##就是将左右两边的int 和_max合成一个符号
那么这里的int_max就是函数名字
int int_max(int x,int  y)
{
    return x>y?x:y;
}
*/
//使用模版调用函数,函数名字叫float_max
FUNC(float)
int main()
{
    printf("%dn", int_max(3, 5));
    return 0;
}

8.命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。那我们平时的⼀个习惯是:

把宏名全部⼤写

函数名不要全部⼤写

9.#undef

这条指令⽤于移除⼀个宏定义。

代码语言:javascript复制
#define M 100
int main()
{
    printf("%dn", M);
#undef M
    printf("%dn", M);//将M的宏定义取消后M就用不了了
    return 0;
}
/*
如果现存一个名字需要被重新定义,那么它的旧名字首先要被移除

‘*/

10.命令行定义

许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些。)

之前学习的关机程序

shutdown -s -t 60

这里的-s 和 -t是命令行的参数

代码语言:javascript复制
#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;
}
代码语言:javascript复制
//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

11.条件编译

满足条件--参与编译

不满足条件,就不参与编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。

调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译

所以我们进行选择性编译

代码语言:javascript复制
.
#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
代码语言:javascript复制
#if 0
int main()
{
#if 1
    printf("hehen");//1为真就打印hehe,如果是0的话,那么就不打印
#endif
    return 0;
}
#endif
//我们现在不想要这整段代码,那么我们就使用条件编译,在一开始加上#if 0,末尾加上#endif
//现在相当与这个代码被注释掉了

//#if后面一定要是常量表达式,不能使用变量
代码语言:javascript复制
#define m 3
int main()
{
#if m==1
    printf("hehen");
#elif m==3
    printf("hahan");
#elif m==4
    printf("hihin");
#else
    printf("呵呵n");
#endif

    return 0;
}
代码语言:javascript复制
//#define zhangsan 0
只要定义过张三,就行了,就能进行后面的打印了,值不在乎是多大
//int main()
//{
//#if defined(zhangsan)//有没有定义过张三,没有的话这个判断就是假的
//    //那么这个张三就不会打印了
//    printf("zhangsann");
//#endif
//    return 0;
//}


//另一种写法
//#define zhangsan 0
只要定义过张三,就行了,就能进行后面的打印了,值不在乎是多大
//int main()
//{
//#ifdef zhangsan//有没有定义过张三,没有的话这个判断就是假的
//    //那么这个张三就不会打印了
//    printf("zhangsann");
//#endif
//    return 0;
//}
//定义了就参与编译,没被定义就不参与编译


//还存在相反的逻辑
//如果没有被定义的话会怎么样


//int main()
//{
//#if !defined(zhangsan)//如果没有被定义的话就进行打印操作,定义了这个条件就是假的
//    
//    printf("zhangsann");
//#endif
//    return 0;
//}


//另一种写法
int main ()
{
#ifndef zhangsan//如果没有被定义的话就进行打印操作,定义了这个条件就是假的

    printf("zhangsann");
#endif
    return 0;
}
代码语言:javascript复制
#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
代码语言:javascript复制
#define size 10
#ifndef size
#error 没有定义error
#endif
//#error确保再编译之前,满足某些条件

12.头文件的包含

头文件的两种形式:

1.#include ---一般指标准库中头文件的包含

2.#include "xxx.h"---这种就是自己创建的头文件,本地文件的包含

本地文件的包含

include "filename"

查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。如果找不到就提⽰编译错误

Linux环境的标准头⽂件的路径:/usr/include

VS环境的标准头⽂件的路径:[C:Program Files (x86)Microsoft Visual Studio 12.0VCinclude](C:Program Files (x86)Microsoft Visual Studio 12.0VCinclude)

这个是vs2013的

库文件的包含

查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。

所有的文件都能使用双引号进行查找,但是对于标准库的查找的话,第一次的查找很浪费时间的

我们应该对头文件进行区分,使用正确的方法找到好的包含方式

嵌套文化的包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。如何解决头⽂件被重复引⼊的问题?答案:条件编译。每个头⽂件的开头写:

代码语言:javascript复制
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

//或者
#pragma once

第一次包含之后就定义了TESTH,那么在第二次头文件引用的时候会进行判断,因为TESTH已经

被定义过了,那么后面的头文件就不能被引用了

这个时候我们就会发现条件编译起到了作用了

pragma once这种条件编译是最简单的,作用也是防止头文件重复

  1. 头⽂件中的 ifndef/define/endif是⼲什么⽤的?

防止头文件被多次包含

  1. #include 和 #include "filename.h" 有什么区别?

在查找策略上不同

前者针对标准库头文件的引用

后者针对的是本地我们自己创建的文件的引入

13.其他预处理指令

代码语言:javascript复制
#error
#pragma-
#line
...
不做介绍,⾃⼰去了解。
#pragma pack()在结构体部分介绍---设置默认对齐数--在求结构体的大小的时候会用到

define

include

if

endif

ifdef

ifndef

elif

pragma

undef

else

0 人点赞