C语言---预处理详解

1.预定义符号

在C语言中有一些内置的预定义符号

__FILE__
__LINE__
__DATE__
__TIME__
__STDC__
//进行编译的源文件
//文件当前的行号
//文件被编译的日期
//文件被编译的时间
//如果编译器遵循ANSI C,其值为1,否则未定义

 编译器在__STDC__报错,说明,vs编译器是没有遵循ANSI C的


2.#define

        2.1定义标识符

语法:
#define name stuff

我们通常会在一条语句写完后带上分号";",但是在#define定义的时候不要加分号!!!

你看出问题所在了吗?

        #define定义的符号是替换的,那么M就会被替换成100;所以加上分号是一件危险的事情.

       2.2定义宏

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

下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

 但是宏有个特点,我们先来看段代码.

我们想要的结果是8*8=64,结果输出的却是23,这与我们的预期不符,是为什么呢?

        因为宏是替换进去的,此时n = 3+5*3+5,结果自然就是23,所以我们在定义宏的时候需要加括号以保证正确性,不要吝啬括号.

结论:对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用.

        2.3#define 替换规则

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

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

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

        2.4#和##

下面我们来探讨如何把参数插入到字符串中!

字符串是有自动拼接的效果的

因此我们可以这样写代码

但是这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中

还有一个方法是使用 # ,把一个宏参数变成对应的字符串.

此时使用#,就会把参数代入到字符串中

而##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符

        2.5带副作用的宏参数

        当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:

x+1;//不带副作用
x++;//带有副作用

下面我们写一个宏来证明具有副作用的参数所引起的问题。

        2.6宏和函数对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?
原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

宏的缺点:当然和函数相比宏也有劣势的地方:

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

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

宏与函数对比图:

属 性#define定义宏函数
代 码 长 度每次使用时,宏代码都会被插入到程序中。除了非常
小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
执 行 速 度更快存在函数的调用和返回的额外开
销,所以相对慢一些
操 作 符 优 先 级宏参数的求值是在所有周围表达式的上下文环境里,
除非加上括号,否则邻近操作符的优先级可能会产生
不可预料的后果,所以建议宏在书写的时候多些括
号。
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。
带 有 副 作 用 的 参 数参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果。
函数参数只在传参的时候求值一
次,结果更容易控制。
参 数 类 型宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型。
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
相同的。
调 试宏是不方便调试的函数是可以逐语句调试的
递 归宏是不能递归的函数是可以递归的

        2.7命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
所以给友友两个建议:

  • 把宏名全部大写
  • 函数名不要全部大写
     

3.#undef

在C语言中,#undef是一个预处理指令,用于取消定义一个已经定义的宏。

宏定义是一种在编译时用特定的值或代码片段替换标识符的方式。通过#define指令可以定义一个宏,而#undef指令可以取消定义宏。


4.命令行定义

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

Linux环境下演示:

指令:

gcc test.c -D ARRAY_SIZE=100 -o test


5.条件编译

条件编译是一种预处理指令,它允许根据条件来选择性地编译代码。条件编译指令在编译阶段进行处理,可以根据条件的真假来决定是否编译代码。

例如 :  调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

条件编译使用#if#ifdef#ifndef#elif#else#endif等预处理指令来实现。下面列举条件编译指令:

  1. #if#endif:用于指定一个条件,如果条件为真,则编译条件之间的代码。
    #if CONDITION
        // 代码块
    #endif
    
  2. #ifdef#endif:用于检查一个标识符是否已经定义,如果已定义,则编译条件之间的代码。
    #ifdef IDENTIFIER
        // 代码块
    #endif
    
  3. #ifndef#endif:与#ifdef相反,用于检查一个标识符是否未定义,如果未定义,则编译条件之间的代码。
    #ifndef IDENTIFIER
        // 代码块
    #endif
  4. #elif:用于指定一个新的条件,如果前面的条件为假,且当前条件为真,则编译条件之间的代码。
    #if CONDITION1
        // 代码块1
    #elif CONDITION2
        // 代码块2
    #else
        // 代码块3
    #endif
    

条件可以是任何可以求值为非零或零的表达式,通常使用预定义的宏来表示条件。例如,#ifdef指令通常用于检查是否定义了某个宏,如下所示:

#include <stdio.h>

#define DEBUG

int main() {
    #ifdef DEBUG
        printf("Debug mode\n");
    #else
        printf("Release mode\n");
    #endif

    return 0;
}

如果定义了宏DEBUG,则会编译输出"Debug mode";否则,会编译输出"Release mode"。

条件编译可以用于在不同的编译环境下编译不同的代码,或者根据不同的条件来选择性地包含或排除代码。它在处理平台特定代码、调试代码和配置选项等方面非常有用。


6.文件包含

        我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:

  •         预处理器先删除这条指令,并用包含文件的内容替换。
  •         这样一个源文件被包含10次,那就实际被编译10次。

6.1头文件被包含的方式:

本地文件包含:

#include "test.h"

查找策略:

        先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。

Linux环境的标准头文件的路径:

/usr/include

VS环境的标准头文件的路径:

你如果是默安装路径的话:C:\Program Files (x86)\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.30.30705\include

注意 : 按照自己的安装路径去找每个人的机器各有差异

库文件包含:

#include <stdio.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了

6.2嵌套文件包含

comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复.

通过前面的学习我们应该都知道了,有不明白的友友可以看这篇程序环境

头文件在预处理的时候会直接在原地展开,如果一个一个头文件按800行代码计算,如果你重复包含多个的话,那么就会造成冗余,降低程序的运行效率.

那有没有什么办法可以避免呢?

1.条件编译

每个头文件的开头写:

#ifndef __TEST_H__   // 如果未定义标识符__TEST_H__
#define __TEST_H__   // 定义标识符__TEST_H__
// 头文件的内容
#endif //__TEST_H__

通过这种方式,当多个源文件需要包含同一个头文件时,只有第一次包含会生效,后续的包含会被忽略,避免了头文件的重复包含问题。

2.#pragma once

#pragma once是一种用于防止头文件多重包含的预处理指令。与传统的条件编译指令相比,#pragma once更简洁和直观。

使用#pragma once可以确保头文件只被包含一次。当编译器遇到#pragma once指令时,它会检查当前的头文件是否已经被包含过,如果是,则直接跳过该头文件的包含,否则继续包含该头文件。

要使用#pragma once,只需要在头文件的开头添加一行#pragma once即可

#pragma once

// 头文件的内容

本章内容已完:
        如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我会在未来的更新中持续探讨与c/c++相关的内容。我会为您带来更多关于编程技术问题的深入解析、应用案例和趣味玩法等。感兴趣的话给博主点个关注,获取最新的内容消息!

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值