C语言之预处理详解

纲要:

  • 预定义符号
  • #define
    • #define定义标识符
    • #define定义宏
    • #define的替换规则
    • #与##
  • 几点注意#undef
    • 带副作用的宏参数
    • 宏和函数的对比
    • 命名约定
  • 命令行定义
  • 条件编译
    • 单分支条件编译
    • 多分支条件编译
    • 判断是否被定义
    • 嵌套指令
  • 文件包含
    • 头文件被包含的方式
    • 嵌套文件包含
  • 其他预处理指令
    • #error
    • #line
    • #pragma

注:此篇内容会微微涉及到:C语言之简易了解程序环境,但是对与此篇的理解影响不大

一.预定义符号

__FILE__//进行编译的源文件

__LINE__//文件当前的行号

__DATE__//文件被编译的日期

__TIME__//文件被编译的时间

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

__FUNCTION__//当前所在的函数

  我们来看一个例子:

void test()
{
printf("FILE: %s\n", __FILE__);//所在的文件
printf("LINE: %d\n", __LINE__);//所在的行
printf("DATE: %s\n", __DATE__);//被编译的日期
printf("TIME: %s\n", __TIME__);//被编译的时间
printf("FUNCTION: %s\n", __FUNCTION__);//所在的函数名称
}
int main()
{
test();
printf("FUNCTION: %s\n", __FUNCTION__);//所在的函数名称
return 0;
}

  注意:

    1.这些预定义符号都是语言内置的。不需要再引用其他的库函数

    2.这些预定义符号再预编译阶段就别替换了

  接下来我们来看看我们的编译器对 __STDC__ 的支持:

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

  VS 2019:

  gcc:

  我们可以看到VS对于STDC的支持并不是很好;

二.#define

  对于#define 定义的东西同样也是再预编译阶段就进行了替换。

1.#define定义标识符

  语法: #define name stuff

    在预编译时,将 name 替换为 stuff

  示例:

#define MAX 100

#define STR "HEHE"

#define reg register //register 这个关键字是请求编译器把变量储存在寄存器中,而不是放在内存里,可以提高访问效率
//但register 给你提供的地方很小,放不了很多变量 int main()
{
reg int age = 10; printf("%d\n", MAX);//100
printf("%s\n", STR);//HEHE
printf("%d\n", age);//10 return 0;
}

  即替换之后为:

int main()
{
register int age = 10; printf("%d\n",100);
printf("%s\n","hehe");
printf("%d\n",10); return 0;
}

 注意:

   在#define定义标识符时,尽量不要添加 ;   

   如:

#define MAX 1000;
//#define MAX 1000 int main()
{
int max, condition = 1;
if (condition)
max = MAX;//要是第一种加了 ; 就会很容易出现错误,因为在我们的认知中,一条语句结束就要加一个 ;
else
max = 0; return 0;
}

2.#define定义宏

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

  下面是宏的申明方式:

#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

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

  示例:

#define SQUARE(x) (x*x)

int main()
{
printf("%d\n", SQUARE(5));
return 0;
}
#define SQUARE (x) (x*x)//如果我们在E后敲一个空格,我们就会发现编译器就已经报了错

int main()
{
printf("%d\n", SQUARE(5));
return 0;
}
#define SQUARE(x) (x*x)
//我们再来换个数字来看看,换成一个表达式 int main()
{
printf("%d\n", SQUARE(2+3));//此时的结果会是25吗?
return 0;
}

  可是我们运行后发现结果为 11 为什么呢?

#define SQUARE(x) (x*x) //11
#define SQUARE(x) ((x)*(x)) //25

  提示:

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

  例:offsetof 的模拟实现

#include<stdlib.h>
//模拟实现offsetof的实现
#define OFFSETOF(type,member) ((int)&(((type*)0)->member)) struct test
{
int a;
char b;
double c;
}; int main()
{
struct test stu = { 0,0,0 };
printf("OFFSETOF:\n");
printf("%d\n",OFFSETOF(struct test, a));
printf("%d\n",OFFSETOF(struct test, b));
printf("%d\n",OFFSETOF(struct test, c));
printf("offsetof:\n");
printf("%d\n", offsetof(struct test, a));
printf("%d\n", offsetof(struct test, b));
printf("%d\n", offsetof(struct test, c));
return 0;
}

3.#define的替换规则

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

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。

2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

4.#与##

  在此之前,我们先来看一给引例:

//对于它,我们要是放在宏里该怎么实现?
int main()
{
int a = 4;
printf("a=%d", a);
return 0;
}

  我们先来试着写一下:

  我们要想到,这写出来不能只打印整形,要兼顾其他的类型

  我们发现好像有点困难

  这时,就需要 # 来帮忙了

  1.#

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

    我们发现,现在只需写成这样,便可满足上面的要求了:

#define print(num,data) printf("The value of "#num " is " data"\n",num);

int main()
{
int a = 3;
print(a,"%d");
return 0;
}

    可能有人会对printf中的那么多 “ ” 感到疑惑。,我们继续来看一个例子:

int main()
{
printf("Hello"" World ""!\n");//它会打印出什么
return 0;
}

    我们发现字符串是有自动连接的特点的。这时,只要参考这个例子就可以理解上面那个例子为什么要那样写了

  2.##

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

例:

#define STR "HELLO "##"WORLD!"
#define NUM 100##999
#define ADD_TO_SUM(num, value) sum##num += value . int main()
{
printf("%s\n", STR);//HELLO WORLD!
printf("%d\n", NUM);//100999 int sum5 = 0;
ADD_TO_SUM(5, 10);//作用是:给sum5增加10
printf("%d",sum5); return 0;
}

    注:

      在拼凑变量名时,这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

三.几点注意

  在我们写#define定义的时候,往往会出现一些摸不到头脑的问题,下面我就来提一提。

1.带副作用的宏参数

  我们先看一个例子:

int main()
{
int a = 10;
int b = 20;
int c = MAX(a++, b++);
printf("%d\n", c);
printf("a=%d b=%d\n", a, b);
return 0;
}

  它的结果会是什么呢?我们可以好好想一想。

  运行结果:

  是不是没有想到呢,我们再来补充一点注释来看:

#define MAX(X,Y)  ((X)>(Y)?(X):(Y))

int main()
{
//int m = 5;
//int n = m + 1;//n = 6 m = 5
//int n = ++m; //n = 6 m = 6 int a = 10;
int b = 20; //传递给MAX宏的参数是带有副作用的
int c = MAX(a++, b++); //int c = ((a++) > (b++) ? (a++) : (b++)); printf("%d\n", c);//?
printf("a=%d b=%d\n", a, b); return 0;
}

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

  如:

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

2.宏和函数的对比

  在这分别有一个求最大值的宏和函数,哪个好一点呢?

#define MAX(X,Y)  ((X)>(Y)?(X):(Y))

int INT_max(int a, int b)
{
return a > b ? a : b;
} int main()
{
printf("%d\n", INT_max(1, 5));
printf("%d\n", MAX(1, 5));
return 0;
}

  要是我选择,我选择用宏来实现,为什么呢?

  我们看到利用宏:

  利用函数:

  我们发现:在这个例子中,宏转成的汇编语言要比函数少的多!

  宏的优点:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。

  但是并不是这样说,宏就没有缺点了

  宏的缺点:

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2. 宏是没法调试的。

3. 宏由于类型无关,也就不够严谨。

4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

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

  例如:

#define MALLOC(type,num) ((type*)malloc((num)*sizeof(type)))//动态开辟内存

int main()
{
int* p = MALLOC(int, 10);//开辟10个整形的空间
//...
free(p);//释放内存
p = NULL;//及时置NULL
return 0;
}

    宏和函数的对比:、

3.命名约定

  一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:

    1.把宏名全部大写

    2.函数名不要全部大写

四.#undef

  #undef 是用来撤销宏定义的,例:

#include <stdio.h>

#define MAX 100

int main()
{
printf("%d\n", MAX);
#undef MAX
printf("%d\n", MAX); return 0;
}

  我们运行会发现,在第二个printf语句中的MAX是未定义的

  注:

    如果现存的一个符号内容需要被重新定义,那么它的旧内容首先要被移除。

五.命令行定义

  许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

  例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。

  (假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

  示例:

#include <stdio.h>
int main()
{
int array[NUM];
int i = 0;
for (i = 0; i < NUM; i++)
{
array[i] = i;
}
for (i = 0; i < NUM; i++)
{
printf("%d ", array[i]);
}
printf("\n");
return 0;
}

  这时我们就可以在命令行里定义NUM的大小了,命令 gcc -D NUM=10 test.c

六.条件编译

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

比如说:

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

1.单分支条件编译

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

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

#define DEBUG 1

int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", i);
#if DEBUG
printf("hehe\n");
#endif
}
return 0;
}
//条件编译  - 满足条件就参与编译,不满足条件就不参与编译

#define DEBUG 0

int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", i);
#if DEBUG
printf("hehe\n");
#endif
}
return 0;
}

    在上面改变了DEBUG的值,运行结果也随之变化!

2.多分支条件编译

//2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

    同样是满足条件就执行,但在一个过程中只执行一个!(从#if到所匹配的#endif结束)

int main()
{
int a = 10;
#if a-2
printf("First\n");
#elif 3-1
printf("Second\n");
#elif 5-5
printf("Third\n");
#else
{
printf("hehe\n");
printf("hehe\n");
}
#endif return 0;
}

 

3.判断是否被定义

  定义就执行

3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
#define __DEBUG__ 0

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

    不过它有两种方式可供选择:

#define PRINT 0

int main()
{
//定义了PRINT才打印hehe --- 第一种写法
#ifdef PRINT
printf("hehe\n");
#endif
return 0;
} #define PRINT int main()
{
//定义了PRINT才打印hehe --- 第二种写法
#if defined(PRINT)
printf("hehe\n");
#endif return 0;
}
#define PRINT 0

int main()
{
//没有定义PRINT才打印hehe --- 第一种写法
#ifndef PRINT
printf("hehe\n");
#endif
return 0;
} #define PRINT
int main()
{
//没有定义PRINT才打印hehe --- 第二种写法 #if !defined(PRINT)
printf("hehe\n");
#endif
return 0;
}

4.嵌套指令

//简单示例
//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
#define PASS
#define HAHA void haha()
{
printf("haha\n");
} void ha()
{
printf("ha\n");
} int main()
{
#ifdef PASS
#ifdef HAHA
haha();
#endif // haha #ifdef HAHA
ha();
#endif // ha #endif // DEBUG return 0;
}

七.文件包含

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

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

1.头文件被包含的方式

  1.<name>  : 包含库里的文件

    程序怎么查找这个文件呢:

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

  2."name"  : 包含我们自己写的文件

    程序怎么查找这个文件呢:

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

  

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 //__TEST_H__

  或者:

#pragma once //只使用一次

八.其他预处理指令

1.#error

  在程序编译时,只要遇到 #error 就会生成一个错误提示消息,并停止编译,语法格式:

#error error-message

  示例:

#define test

int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", i);
if (i == 5)
{
#ifdef test
#error this is a test!
#endif
}
}
return 0;
}

2.#line

  改变当前行数和文件名称,基本形式:

#line number "filename"

  示例:

#include<stdio.h>

int main()
{
printf("filename :%s\n",__FILE__);
printf("line :%d\n",__LINE__); #line 100 "test.c"
printf("filename :%s\n", __FILE__);
printf("line :%d\n", __LINE__); return 0;
}

  注:文件名可以不写

3.#pragma

  它的作用是设定编译器的状态或指示编译器完成一些特点的动作,在这我们只挑出几个来说:

  1.#pragma message

    message 参数:在编译信息输出窗口中输出相应的信息

  示例:

#pragma message("This is a test!")
int main()
{
return 0;
}

  2.pragma once

    这个在刚刚我们就已经提过了;

    它的作用是将头文件只编译一次;

  3.pragma pack

    在结构体内存章节,我们就已经对它有了介绍

   对此就介绍到这

|------------------------------------------------------------------

到此,对于预处理详解便到此结束!

因笔者水平有限,若有错误,还望指正

C语言之预处理详解的更多相关文章

  1. C语言内存对齐详解(2)

    接上一篇:C语言内存对齐详解(1) VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式.VC 中提供了#pr ...

  2. C语言内存对齐详解(3)

    接上一篇:C语言内存对齐详解(2) 在minix的stdarg.h文件中,定义了如下一个宏: /* Amount of space required in an argument list for a ...

  3. c语言贪吃蛇详解3.让蛇动起来

    c语言贪吃蛇详解3.让蛇动起来 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识点. 上次 ...

  4. c语言贪吃蛇详解-2.画出蛇

    c语言贪吃蛇详解-2.画出蛇 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识点. 蛇的身 ...

  5. c语言贪吃蛇详解1.画出地图

    c语言贪吃蛇详解-1.画出地图 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识点. 首先 ...

  6. c语言贪吃蛇详解5.GameOver功能与显示成绩

    c语言贪吃蛇详解5.GameOver功能与显示成绩 以前我们已经做出来了一个能吃东西变长的蛇.不过它好像不会死... 现在就来实现一下game over的功能吧. 写个函数判断蛇是否撞到自己或者撞到墙 ...

  7. c语言贪吃蛇详解4.食物的投放与蛇的变长

    c语言贪吃蛇详解4.食物的投放与蛇的变长 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识 ...

  8. 一个简单的C语言程序(详解)

    C Primer Plus之一个简单的C语言程序(详解) #include <stdio.h> int main(void) //一个简单的 C程序 { int num; //定义一个名为 ...

  9. [转帖]rename(Perl语言版本) 详解

    rename(Perl语言版本) 详解 2019-03-19 22:51:23 wayne17 阅读数 464更多 分类专栏: Ubuntu之路   版权声明:本文为博主原创文章,遵循CC 4.0 B ...

随机推荐

  1. iPhone 12 导入通讯录排序 Bug

    iPhone 12 导入通讯录排序 Bug iOS iOS 通讯录排序问题 Huawei OK solution iOS 切换中英文,修复排序通讯录 bug Awesome iOS Contacts ...

  2. user tracker with ETag

    user tracker with ETag 用户追踪, without cookies clear cache bug 实现原理 HTTP cache hidden iframe 1px image ...

  3. js 触发长按事件

    为网站添加触摸功能 <button id="btn1">长按触发</button> <button id="btn2">长按 ...

  4. 【SpringMVC】 4.2 异常处理

    SpringMVC学习记录 注意:以下内容是学习 北京动力节点 的SpringMVC视频后所记录的笔记.源码以及个人的理解等,记录下来仅供学习 第4章 SpringMVC 核心技术 4.2异常处理   ...

  5. C# 类中操作主窗体控件

    主窗体程序: using System; using System.Collections.Generic; using System.ComponentModel; using System.Dat ...

  6. 2021年-在windwos下如何用TOMACT发布一个系统(完整配置案列)

    2021年新年第一篇:博主@李宗盛-关于在Windwos下使用TOMCAT发布一个系统的完成配置案列. 之前写过关于TOMCAT的小篇幅文档,比较分散,可以作为对照与参考. 此篇整合在一起,一篇文档写 ...

  7. Asp.Net Core学习笔记:(二)视图、模型、持久化、文件、错误处理、日志

    TagHelper 入门 优点:根据参数自动生成,不需要手写超链接,类似Django模板里面的url命令. 在ViewImport中添加TagHelper @addTagHelper *,Micros ...

  8. Vue学习笔记-API调试工具--->国产apipost按装(比postman好按装好用)

    一  使用环境: windows 7 64位操作系统 二   Vue学习笔记-API调试工具--->apipost按装 1.下载: https://www.apipost.cn/ (比postm ...

  9. 第46天学习打卡(四大函数式接口 Stream流式计算 ForkJoin 异步回调 JMM Volatile)

    小结与扩展 池的最大的大小如何去设置! 了解:IO密集型,CPU密集型:(调优)  //1.CPU密集型 几核就是几个线程 可以保持效率最高 //2.IO密集型判断你的程序中十分耗IO的线程,只要大于 ...

  10. 更换 grub 主题

    默认的 grub 界面比较简陋 然后突然有想法了,想换个主题 具体操作 1.下载 grub 主题包 去这个地址下载主题(应该是这个地址): https://www.gnome-look.org/bro ...