椋鸟C语言笔记#37:预处理指令

萌新的学习笔记,写错了恳请斧正。


目录

预定义符号

预处理指令#define

#define定义的宏常量

使用续行符扩展#define的功能

#define定义宏

使用宏时不要使用有副作用的参数

编译器到底是怎么处理宏的

宏与函数的对比

约定俗成的命名规则

#与##运算符

#运算符(字符串化)

##运算符(记号粘合)

#undef指令

命令行定义

条件编译

#include包含头文件

包含库文件

包含本地头文件

标准路径

嵌套文件包含

其他预处理指令


预处理指令就是C语言中的一些特定格式的用于辅助编译器预处理的指令,这些指令在编译时的预处理阶段就被处理,并不会被保留到汇编文件中。

预定义符号

首先是一些内置的预定义符号,这些符号可以直接使用,在预处理阶段就被替换为对应的其他内容

包括(注意前后都是两个下划线):

  • __FILE__:替换为源文件的文件名(包括地址与后缀)
  • __LINE__:替换为文件中此符号所在行的行号
  • __DATE__:替换为文件被编译的日期
  • __TIME__:替换为文件被编辑的时间
  • __STDC__:如果编译器遵循ANSI C标准,这将被替换为1,否则未定义

预处理指令#define

#define定义的宏常量

#define可以在预处理时把程序中的某个字段直接替换为另一个字段,然后在继续编译

注意:

  • #define之间可以嵌套
  • #define的第一个字段(被替换字段)必须全连续(不能存在空白字符),而第二个字段可以存在空白字符甚至多行字符(需要续行符辅助)
  • #define的定义不能产生循环

使用示例如下(输出100):

#include <stdio.h>
#define M 100
#define __p printf
#define PRINT __p("%d", M)

int main()
{
    PRINT;
    return 0;
}

注:如果不写替换字段,则默认替换为1

使用续行符扩展#define的功能

上面提到可以使用续行符来完成多行的替换,具体怎么操作呢?

其实很简单,只要正常分行,前面每一行的结尾都加上一个反斜杠即可(反斜杠后就是换行符,之间不能再有任何其他字符!!!),比方说:

#include <stdio.h>
#define DEBUG_PRINT printf("file:%s\nline:%d\ndate:%s\ntime:%s\n",\
                            __FILE__, __LINE__,\
                            __DATE__, __TIME__ \
                        )

int main()
{
    DEBUG_PRINT;
    return 0;
}
#define定义宏

#define指令还包含一个机制,就是宏定义功能

宏定义可以理解为一种另类的“函数”,可以进行多字段的替换

其声明方式为:

#define name(parament_list) stuff
// name是宏的名称
// parament_list是参数列表,类似于函数的参数列表
// stuff是替换内容

其功能就是将stuff中所有的与参数列表相同的字段替换为我们使用时输入的字段

比方说下面就是定义了一个计算平方数的宏(注意这里后面替换字段打括号,不然替换后可能会有运算符顺序问题,再次声明宏是替换字符,不是算完给一个返回值):

#include <stdio.h>
#define SQUARE(x) ((x)*(x))

int main()
{
    printf("%d\n", SQUARE(7));
    return 0;
}

有人可能会认为宏没什么必要,函数能覆盖宏的功能,但其实不是这样。宏定义说到底还是字段的替换,是没有参数类型的说法的,参数不一定是有意义的变量,甚至可以是类型名、函数名乃至无意义的字符段。所以就衍生出许多有趣的用法,比方说我们平时大量使用动态内存分配时会觉得书写麻烦,就可以这样:

#include <stdio.h>
#include <stdlib.h>

#define MALLOC(p, size, TYPE) TYPE *p = ((TYPE*)malloc(size*sizeof(TYPE)))

int main()
{
    MALLOC(p, 114514, int);
    printf("%d\n", malloc_usable_size(p));
    free(p);
    p = NULL;
    return 0;
}

这里宏的有一个参数就是类型名,显然函数是做不到的

使用宏时不要使用有副作用的参数

带有副作用指参数表达式会影响某些变量的值,比方说自增自减表达式

比方说下面这段程序的输出结果是:x=6 y=10 z=9

#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))

int main()
{
    int x = 5, y = 8, z = MAX(x++, y++);
    printf("x=%d y=%d z=%d\n", x, y, z);
    return 0;
}

为什么呢,我们看看其预处理文件中的代码部分:

int main()
{
    int x = 5, y = 8, z = ((x++)>(y++)?(x++):(y++));
    printf("x=%d y=%d z=%d\n", x, y, z);
    return 0;
}

说到底还是因为宏是字段替换而不是向函数那样创建栈帧空间有形参和返回的概念

编译器到底是怎么处理宏的
  1. 在调用宏时首先对参数进行检查,看看有没有由#define定义的符号,如果有那就先替换
  2. 然后将替换字段插入到程序中原来字段的位置
  3. 最后再一次对结果进行扫描,如果又有由#define定义的符号,那就返回第一步

注意:在扫描#define定义的符号时,字符串中的内容(双引号包起来的内容)会被跳过

宏与函数的对比

宏优势:

  1. 一些简单的计算,比方说上面的取最大值的宏,使用宏拥有更高的运行效率
  2. 宏的参数是靠字段替换存在的,所以能完成更多特殊的功能,比方说上面的malloc

宏劣势:

  1. 宏可能会在预处理后大大加长代码长度
  2. 宏无法被调试,出了问题不好找
  3. 宏没有类型的概念,不够严谨,可能不利于代码的健壮性
  4. 极容易造成运算符优先级的问题,在定义时需要小心加括号
  5. 宏不能递归
约定俗成的命名规则

为区分宏定义与函数的定义,一般默认宏名为全大写,而函数名不要全大写

#与##运算符

#运算符(字符串化)

#运算符用于将宏的一个参数转换为字符串字面量,只能用在有参数的宏中。

比方说下面是一个打印某变量值的宏定义:

#include <stdio.h>
#define PRINT(n) printf("The value of "#n" is %d\n", n)

int main()
{
    int a = 10;
    PRINT(a);
    return 0;
}

这里#n的n照样被替换为a,随后字符串化使得这个a变成了字符串量,也就是说PRINT(a);会被预处理为如下一行代码:

printf("The value of ""a"" is %d\n", n);
//printf的格式控制字符串其实是可以由多个字符串拼起来的,中间不要有逗号分割即可
##运算符(记号粘合)

##用于把位于其两侧的符号合成为一个符号以使某侧的符号被判定为#define定义的标识符替换

比方说,我们用定义把A替换为B,然后下面有一个AC,这时如果什么都不管,AC还是AC(AC整体间没有空白字符分割会被当成一整个符号)。这时我们就可以把AC写成A##C,那么A和C这两个符号就会被编译器认为是两个符号,就会把A替换为B,然后B与C粘合产生新符号BC。

比如我们写一个生成取最大值函数的宏:

#include <stdio.h>
#define GENERIC_MAX(type)           \
    type type##_max(type x, type y) \
    {                               \
        return x > y ? x : y;       \
    }

GENERIC_MAX(int)
GENERIC_MAX(float)

int main()
{
    printf("%d\n", int_max(3, 4));
    printf("%f\n", float_max(3.0f, 4.0f));
}

#undef指令

这条指令用于移除一条#define定义(就是说这条定义到此为止了,下面不要再换了)

比方说下面输出0 1 0(只有第二个A在预处理时被替换为1):

#include <stdio.h>

int main()
{
    int A = 0;
    printf("%d\n", A);
    #define A 1
    printf("%d\n", A);
    #undef A
    printf("%d\n", A);
    return 0;
}

命令行定义

#define的定义也可以在编译时添加,比方说:

$ gcc -D SIZE=10 program.c
//Linux gcc环境下

这条指令就是在编译时指定#define SIZE 10

条件编译

条件编译就和条件语句差不多,是判断条件然后选择是否编译某段语句或执行某段预处理指令

常见的条件编译指令有:

1.如果常量表达式成立则编译中间的代码

#if 常量表达式
    //...
#endif

比如:

#define __DEBUG__ 1
#if __DEBUG__
    //...
#endif

2.多分支条件编译

#if 常量表达式
    //...
#elif 常量表达式
    //...
#elif 常量表达式
    //...
#elif 常量表达式
    //...
#else
    //...
#endif

3.判断是否被定义

下面两段含义相同,代表判断symbol是否被定义:

#if defined(symbol)
    //...
#endif

#ifdef symbol
    //...
#endif

下面两段含义相同,代表判断symbol是否未被定义:

#if !defined(symbol)
    //...
#endif

#ifndef symbol
    //...
#endif

条件编译指令可以嵌套:

//嵌套
#ifdef A
    #ifdef B
        printf("A and B is defined.\n");    //...
    #endif
#endif

#include包含头文件

#include包含头文件相当于把对应的文件直接复制到#include所在的位置

包含库文件
//包含库文件
#include <filename.h>

编译器将在标准路径下查找对应的头文件,如果找不到这报编译错误

包含本地头文件
//包含本地头文件
#include "filename.h"

编译器首先在源文件所在目录下查找,如果没找到则去标准路径继续查找,找不到则报编译错误

包含库文件也可以使用双引号,但是运行效率低且不易区分头文件,是不好的习惯

标准路径

Linux gcc下:/usr/include

嵌套文件包含

由于头文件中可能还包含了其他的头文件,有时部分头文件可能被包含多次,这会给编译器带来不必要的压力。如果是一个特大工程,这甚至会造成以小时甚至天计算的时间损失。

所以我们有两个解决方案:

1.条件编译

比如头文件test.h,我们把头文件内容写在如下代码中:

#ifndef __TEST_H__
#define __TEST_H__

...//头文件的内容

#endif    //__TEST_H__

这样这个头文件就最多只会被包含一次了

2.使用#pragma指令

只要在头文件前加一行指令即可确保次头文件只被包含一次:

#pragma once
//一些老版本的编译器不支持

其他预处理指令

不常用,不介绍


  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椋鸟Starling

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值