【C语言预处理】超详细讲解

目录

一、预定义符号

二、#define

2.1#define定义标识符

2.2#define定义宏

2.3#define替换规则

2.4#和##的使用

2.5带副作用的宏参数

2.6宏和函数对比

2.7命名约定

三、#undef

四、命令行定义

五、条件编译

六、文件包含

6.1头文件被包含的方式

6.2嵌套文件包含

七、其他预处理指令


一、预定义符号

__FILE__        //进行编译的源文件

__LINE__        //文件当前的行号

__DATE__        //文件被编译的日期

__TIME__        //文件被编译的时间

__STDC__        //如果编译器遵循ANSI C, 其值为1,否则未定义

这些预定义符号都是语言内置的,举个例子:

printf("file:%s line:%d date:%s time:%s stdc:%d", __FILE__, __LINE__, __DATE__, __TIME__, __STDC__);

在预编译的时候就会将预定义符号替换,运行结果是:

但是在VS编译器里面使用__STDC__会出现错误,未定义的标识符。

二、#define

2.1#define定义标识符

语法:#define name stuff

例如:

#define MAX 1000

#define reg register           //为 register这个关键字,创建一个简短的名字

#define do_forever for(;;)     //用更形象的符号来替换一种实现

#define CASE break;case        //在写case语句的时候自动把 break写上。

// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。

#define DEBUG_PRINT printf("file:%s\tline:%d\t \

                        date:%s\ttime:%s\n" ,\

                        __FILE__,__LINE__ ,\

                        __DATE__,__TIME__ )

 在预编译之后会将#define定义的标识符进行替换,上面举了两个例子,其他的也一样。

问题:在define定义标识符的时候,要不要在最后加上;

//以下两句尽量用哪个?

#define MAX 1000

#define MAX 1000;

如果在#define定义标识符后面加上分号,就会出现以上情况,就会造成语法错误。因为直接将定义的标识符替换到了对应的语句中。,所以我们尽量不要在后面加上分号。

2.2#define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro)
下面是宏的声明方式:
#define name(parament-list) stuff
其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
例如:
#define SQUARE(x) x*x
这个宏接收一个参数x,如果在上述声明之后,把
SQUARE(5)
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5
如下图所示:

 但是这个宏会存在一个问题,如果我们传的是一个表达式

int a = 5;

printf("%d\n", SQUARE(a+1));

为什么最后的结果会是11呢?请看下图:

 这是因为宏的参数不加任何计算进行替换,替换产生的表达式并没有按照预想的次序井下求值。

很简单,只要我们再宏定义上加上两个括号,这个问题就迎刃而解了。

这样就得到了我们想要的结果。

定义中我们又发现了其他的问题:

例如以下宏定义:

#define DOUBLE(x) (x) + (x)

定义中我们使用了括号,但是又出现了新的问题

int a = 5;

printf("%d\n", 10*DOUBLE(a));

这将打印什么值呢?

看上去好像打印100,但事与愿违,打印出来的是55

预编译后如下图所示:

 乘法的优先级要高于加法,所以先计算10 * 5 = 50,再计算 + 5,最后的结果是55。

要解决这个问题也非常的容易,只需要在宏定义表达式两边加上一对括号就可以了

这样就会先算括号中的,再算括号外的,计算结果就是我们预期的了:

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

2.3#define替换规则

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

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

注意:

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

2.4#和##的使用

如何把参数插入到字符串中?

首先我们看一下这样的代码:

char* p = "hello ""bit\n";

printf("hello"" bit\n");

printf("%s", p);

我们发现字符串是有自动连接的特点的。

例如:

#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE)

PRINT("%d", 10);

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

 使用# ,把一个宏参数变成对应的字符串

int i = 10;
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
PRINT("%d", i+3);
代码中的#VALUE会被预处理器处理为:"VALUE",最终的输出结果应该是:
the value of i+3 is 13
##的作用:##可以吧位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value) sum##num += value;
ADD_TO_SUM(5, 10);        //作用是给sum5增加10

注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。 

2.5带副作用的宏参数

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

x+1;        //不带副作用

x++;        //带有副作用,因为后置++是自增运算,会改变x的值

MAX宏可以证明具有副作用的参数所引起的问题,例如:

2.6宏和函数对比

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

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

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

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

宏的缺点:相比于函数也有劣势的地方

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

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

 #define MALLOC(num, type) (type*)malloc(num * sizeof(type))

属性#define定义宏函数
代码长度

每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长

函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级

在宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号

函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用的参数

参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果

函数参数只在传参的时候求值一次,结果更容易控制
参数类型

宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型

函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
调试宏是不方便调试的函数是可以逐语句调试的
递归宏 是不能递归的函数是可以递归的

2.7命名约定

一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者;我们平时的习惯是:把宏名全部大写;函数名不要全部大写

三、#undef

这条指令用于移除一个宏定义

#undef NAME

//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除

四、命令行定义

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

 

五、条件编译

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

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

#include <stdio.h>
#define __DEBUG__

int main()
{
    int i = 0;
    int arr[10] = {0};
    for(i=0; i<10; i++)
    {
        arr[i] = i;
        #ifdef __DEBUG__
        printf("%d\n", arr[i]);
        #endif
    }
    return 0;
}

常见的条件编译指令:

1.#if  常量表达式

2.多个分支的条件编译

3.判断是否被定义

 图中写法一和写法二的意思一样:

 图中写法三和写法四的意思一样:

4.嵌套指令

六、文件包含

我们已经知道,#include指令可以使另一个文件被编译。就像它实际出现于#include指令的地方一样。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次。

6.1头文件被包含的方式

6.1.1 本地文件包含

#include "filename.h"

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

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

/usr/include

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

//这是VS2013的默认路径

C:\Program Files (x86)\Mircrosoft Visual Studio 12.0\VC\include

6.1.2 库文件包含

#include <filename.h>

查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

总结:对于库文件也可以使用 " " 的形式包含,因为如果在源文件所在目录下未找到,就在库函数中查找头文件,所以一样会找到。

6.2嵌套文件包含

comm.h和comm.c是公共模块;

test1.h和test1.c使用了公共模块;

test2.h和test2.c使用了公共模块;

test.h和test.c使用了test1模块和test2模块

这样最终程序中就会出现两份comm.h的内容,这样就造成了文件内容的重复。

如何解决这个问题?条件编译

每个头文件的开头这样写:

#ifndef __TEST_H__

#define __TEST_H__

//内容

#endif

或者这样写:

#pragma once

这样就可以避免头文件的重复引入

七、其他预处理指令

#error

#pragma

#line

...

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言预处理C语言编译过程中的一个阶段,它在编译之前对源代码进行一些文本替换和宏替换等操作,以便生成最终的编译代码。 C语言预处理器使用以 "#" 开头的预处理指令来实现预处理的功能。最常用的预处理指令包括: 1. #include 指令:用于包含头文件,将头文件中的内容插入到源代码中。例如:#include <stdio.h> 就会在源代码中插入stdio.h头文件中的内容。 2. #define 指令:用于定义宏,将一个标识符替换为一个表达式或一个语句。例如:#define PI 3.1415926 就会将所有出现的 PI 替换为 3.1415926。 3. #ifdef 和 #ifndef 指令:用于条件编译,根据宏的定义来决定编译哪些代码。例如:#ifdef DEBUG 就会编译 DEBUG 宏被定义的代码。 4. #undef 指令:用于取消宏的定义。例如:#undef PI 就会取消之前定义的 PI 宏。 5. #if 和 #endif 指令:用于条件编译,根据表达式的值来决定编译哪些代码。例如:#if (x > y) 就会编译 x > y 的代码。 6. #pragma 指令:用于指定编译器的特定行为。例如:#pragma once 可以确保头文件只被包含一次。 预处理器还可以使用宏定义、条件编译等方式来实现代码的重用、优化和调试等功能。宏定义可以将一段代码封装成一个宏,以便在代码中反复使用。条件编译可以根据不同的编译条件编译不同的代码,以便在不同的环境中实现代码的灵活性。 总之,C语言预处理C语言编译过程中必不可少的一个环节,它可以对源代码进行一系列的文本替换和宏替换等操作,使得代码更加灵活和可读性更强。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值