C语言——预处理

283 阅读9分钟

前言:在VS2022中,使它在预处理完成后就停下来

点住解决方案资源管理器的项目名,右击鼠标,找到属性 image.png 但此时不能编译代码,可以用Ctrl+Fn+f7预处理到文件

接着Ctrl+o打开文件

image.png

image.png

image.png

.i文件就是预处理后的文件

一 #define

1 #define定义的标识符常量

标识符的内容是普通的常量

#define MAX 100
#define MIN 10.5
#define GG 'a'
#define STRING "str ing"
int main()
{
    int a = MAX;
    double b = MIN;
    char c = GG;
    char* str = STRING;
    return 0;
}

左边是text.c, 右边是test.i(预处理后的文件) image.png

标识符的内容有表达式

#define MAX1 100+200
#define ASS 1==1
#define MAX2 (100+200)

int main()
{
    int a1 = MAX1 * 3;
    int a2 = MAX2 * 3;
    int b = ASS;
    return 0;
}

左边是text.c, 右边是test.i(预处理后的文件)

image.png

所以#define定义的标识符仅仅是将它的内容替换进程序中,不会进行计算,

如果#define的标识符常量内容不加()可能会出现操作符优先级的问题

标识符的内容有分号

#define定义的标识符常量的内容仅仅是替换,不会考虑可能出现的后果,即使编译可能编不过去。

#define MAX 100;
#define MIN 10 50
int main()
{
    int a = MAX;
    int b = MIN;
    return 0;
}

image.png

所以最好不要加分号,另外在选择语句中多一条空语句没加大括号容易导致编译错误。

标识符的内容可以是一段代码

#define PRINT printf("abcdefg")
int main()
{
    PRINT;
    return 0;
}

image.png

标识符的内容还可以是一个类型

#define us unsigned
int main()
{
    us int a = 100;//就是unsigned int a = 100;
    return 0;
}

2 #define定义宏

#define允许把参数替换到标识符的内容中,这种实现方式通常称为宏.

申明方式

#define name(parament-list) stuff

parament-list:参数列表,由逗号隔开的符号表

stuff:内容

例:

#define ADD(a, b) (a+b) 
int main()
{
    int a = ADD(5, 6);//相当于int a = (5+6);
    return 0;
}

注意事项

1 左括号必须和宏名紧邻,否则()和()里的参数将都被视为标识符的内容.

例:

#define ADD (a, b) (a+b) 
int main()
{
    int a = ADD(3, 4);
    return 0;
}

image.png

2 宏也不会对传过去的参数求值,仅仅是替换.

#define MUL1(a, b)  a * b

#define MUL2(a, b)  ( a * b )

#define MUL3(a, b) ( (a) * (b) )

int main()
{
    int a =3 / MUL1(3, 4);   //预处理结果希望是3/12
    int b = 3 / MUL2(3+1, 4+1);//希望是3 / 20
    int c = 3 / MUL3(3 + 1, 4 + 1);// 3/20
    return 0;
}

预处理后的结果:

image.png

所以定义宏时,宏的内容尽可能多地加(),

避免使用宏时由于参数中的操作符和邻近操作符

出现运算符优先级的问题.

3 #define的替换规则

1 在调用宏时,首先对参数进行检查, 看是否有#define定义的标识符常量,它们首先被替换. 例:

#define MAX 100
#define ADD(a, b) ((a)+(b))
int main()
{
    int c = ADD(MAX, MAX);//MAX首先被替换成100,即int c = ADD(100, 100);
    return 0;
}

2 替换文本随后被插入到原来文本的位置。

    int c = ((100)+(100));
    //宏的参数名被值所替代,
    //就是说宏的参数名是a和b
    //传过去的100替代了它们

3 宏参数和#define定义的标识符可以出现其它#define定义的标识符

#define MAX 100
#define MIN (MAX-1)
int main()
{
    int max = MAX;//相当于int max = 100;
    int min = MIN;//相当于int min = 100-1;
    return 0;
}

4 当预处理器搜索#define定义的标识符时,程序中双引号和单引号的内容不被搜索.

    char a = 'MAX';
    printf("MAX\n");
    printf("%c", a);

image.png

4 #和##

它们出现在#define定义的标识符或宏的替换内容

例如:

#define name stuff
#define name(a, b) stuff
(它们出现在stuff中)

预备知识

c语言中一个字符串可以拆分成多个段来写,例:

    printf("abcefg\n");
    printf("abc""efg\n");
    printf("abc" "efg\n");//两个连续字符串之间有没有空格都一样

image.png

但两个连续的字符串之间不能有其它内容(空格除外)

image.png

#的使用

局限:仅在宏的替换内容中生效

功能:把传给宏的参数名变成一个字符串

例:

#define STRING(a) #a 
int main()
{
    int c = 0;
    printf( STRING(c) );
    return 0;
}

image.png

传过去的参数c代替了()里的a和内容里的a, 但内容的#c却直接转成字符串,可以被直接打印.

通过字符串可以分段的特性,可以写一个打印(变量名对应变量值)的宏。例:

#define PRINT(a)  printf("the result of "#a" is %d\n", a)//#a相当于传过来的变量名的字符串 
int main()
{
    int c = 0;
    int d = 1;
    PRINT(c);
    PRINT(d);
    return 0;
}

image.png

##的使用

局限:可以出现在#define定义的宏的内容中,也能出现在#define定义的标识符常量中.

功能1:可以将两个符号合并成一个符号

例:

#define N(a, b) a##b
int main()
{
    int class4 = 66;
    printf("%d", N(class , 4));//N(class, 4)就是class4, 一个符号
    //相当于 printf("%d", class4);
    return 0;
}

image.png

功能2:甚至可以把分开的数字合并起来.

例:

#define N codf##a
#define K 100##55
#define M 100##.2
int main()
{
    int codfa = 66;
    int k = K;
    double m = M;
    printf("%d\n", N);
    printf("%d\n", k);
    printf("%.2lf\n", m);
    return 0;
}

image.png

5 带副作用的参数

在达到某种目的的情况下,代码产生了额外的效果。

例:

    int a = 2;
    int b = a+1;//把3赋给b
    b = ++a;//也把3赋给b,
    //但是改变了a的值

当把这种带有副作用的参数传给宏时,结果可能都会难以控制,例:

#define MAX(a, b) ((a)>(b)?(a):(b))
int main()
{
	int a = 5;
	int b = 6;
	int c = MAX(a++, b++);
	//相当于 int c =( (a++)>(b++)?(a++):(b++) )
	return 0;
}

6 宏与函数对比

接下来介绍:相对于函数而言,宏的优缺点。

宏的优点

一般来讲,宏被应用于简单的运算,例如求和,求两数最大值 (1) 使用函数解决问题的步骤有函数调用、计算、函数返回,

但宏仅仅是替换,它没有函数调用和函数返回的开销,它只有计算的过程,

所以宏的效率更高一些;

(2) 函数的参数必须声明为特定的类型,否则任务可能会出错,

而宏的参数与类型无关,只是简单地替换.

例如比较一个浮点数和一个整数.

#define MAX(a, b) ((a)>(b)?(a):(b))
int Max(int a, int b)
{
    return a > b ? a : b;
}
int main()
{
    int a = 3;
    double b = 3.15;
    double c = MAX(a, b);
    double d = Max(a, b);
    printf("%.2lf\n", c);
    printf("%.2lf\n", d);
    return 0;
}

image.png

(3) 宏甚至可以传格式或者数据类型,因为宏是完成替换的,不管传的是什么

例:

#define PRINT(a, format) printf("The result of "#a" is "format ,a )
#define MALLOC(elenum, type) (type*)malloc(elenum* sizeof(type))
int main()
{
    double a = 3.15;
    PRINT(a, "%.2lf");
    int* pa = MALLOC(5, int);//向堆区申请开辟5个整型大小的空间
    if (pa == NULL)
    {
	perror("MALLOC");
        return 1;
    }
    free(pa);
    pa = NULL;
    return 0;
}

宏的缺点

(1)每次使用宏时,都会把替换内容的代码替换到程序中,如果替换内容较长,

可能会大幅增加程序的长度.

例:

#define PRINT printf("abcdefghijklnm----------")

int main()
{
	PRINT;
	PRINT;
	PRINT;
	return 0;
}

实际上编译时的代码:

image.png

但如果是函数,函数的代码只出现于一个地方.

每次使用函数都是调用同一份代码,不会增加程序的长度.

void Print(void)
{
	printf("abcdefg---\n");
}
int main()
{
	Print();
	Print();
	Print();
	return 0;
}

(2)使用宏不便于调试代码,因为调试的代码和真正编译的代码是不一致的,

而函数调试代码更直观;

(3)宏与类型无关,不够严谨.

(4) 宏可能会带来运算符优先级的问题.

而函数会先计算出实参的值,然后把值拷贝给形参,

可以避免运算符优先级问题.

(5)宏体中可能出现多个参数,在传带有副作用的参数时,可能会产生不可预料的后果;

而函数会先计算出实参的值,然后把值拷贝给形参,结果更容易控制.

总结对比

属性函数
代码长度每次使用宏时,宏体都会被替换到程序中,除非宏的内容比较短,否则程序长度会大幅度增长,加重编译器的负担每次使用函数都是调用同一份代码,不会增加程序的长度
操作符优先级宏不会对参数求值,只是替换,除非加上括号,否则邻近运算符、参数中运算符、宏体中运算符的优先级都可能使结果错误会在函数调用时对实参求值,把结果拷贝给形参,有效避免运算符优先级问题
执行速度更快存在函数调用和返回的额外开销,相对慢一些
参数类型宏与参数无关,传什么都可以(不保证编译不出现问题)函数的参数与类型有关,如果参数类型不同,就需要不同的函数,即使它们执行的任务是相同的
递归不能递归可以递归
带有副作用的参数参数可能会被替换到宏体的多个位置,传这种参数可能导致计算结果难以预测在传参时会计算一次实参,把计算结果拷贝给形参,参数值和结果都更容易控制

7 #undef

功能:用于移除一个宏定义或一个标识符常量.

移除一个标识符常量:

image.png

移除一个宏:

image.png image.png

二 条件编译

可以使用【条件编译指令】来控制某段代码需不需要进行编译.【与if类似】

1 单分支

#if 常量表达式  //常量表达式由预处理器求值

//某一段代码

#endif        //一定要加,与#if配对使用

常量表达式也可以包含由#define定义的标识符常量或宏

例:

#define CMP(a,b) a == b
#define NUM 1
int main()
{
    int  i = 0;
    int arr[10] = { 0 };
    for (i = 0; i < 10; i++)
    {
	arr[i] = i + 1;
#if NUM
	printf("%d ", arr[i]);//会打印
        
#endif
    }
#if CMP(1, 1)
	printf("%d", 55);//会打印
#endif
	return 0;
}

2 多分支

#if 常量表达式

#elif 常量表达式

#else

#endif

例:

#define NUM 2
int main()
{
#if NUM == 0
	printf("%d", 1);
	printf("a");
#elif NUM == 1
	printf("%d", 2);
	printf("a");
#else
	printf("%d", 3);//打印出:3a
	printf("a");
#endif
	return 0;
}

3 判断符号是否被定义

只有符号已经被#define定义成宏名或标识符常量才会执行下面的代码
如果是局部变量名和全局变量名,都不行
形式1#ifdef symbol
//代码

#endif

形式2:
#if defined(symbol)
//代码

#endif
如果符号没有被#define定义,执行这段代码
形式1:
#ifndef symbol
//代码

#endif

形式2#if !defined(symbol)
//代码

#endif

三 头文件包含

包含头文件时<>和""的区别

#include "test.h" —— 先在源文件所在目录下查找,

找不到再去编译器提供的标准位置的目录下查找.

#include <stdio.h> —— 直接去编译器提供的标准位置的目录下查找.

避免头文件重复包含

形式1:

#ifndef  __TEST_H__//如果没有定义该符号
#define __TEST_H__//首先定义这个符号

//头文件的内容

#endif
//下次如果再调用该头文件时,由于符号已经定义,不会把头文件的内容再进行拷贝

形式2: #pragma once