【C语言进阶】预处理那些事儿(二)

简介: 【C语言进阶】预处理那些事儿(二)

🔖#define替换规则

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

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

 注意:

宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归

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

🔖#和##

 先来看下面的代码

int main()
{
  char* p = "hello ""world\n";
  printf("hello ""world\n");
  printf("%s", p);
  return 0;
}

31c14f606f5246aba402ad2adcf60e98.png

 通过结果可以看出:对于两个字符串"hello "和"world\n"编译器会自动对它们进行合并得到一个字符串hello world\n。字符串是有自动连接的特点我们是否可以写出下面这种代码?

//这里我们写了一个宏用来打印信息
//我们希望传a的时候它可以打印出:
//a的值是:0
//传b的时候它可以打印出:
//b的值是:10
#define PRINT(x) printf("x的值是:%d\n", x)
int main()
{
  int a = 0;
  PRINT(a);
  int b = 10;
  PRINT(b);
  return 0;
}

575294b2ac1d4180910fc8d8c014abc0.png

 结果不尽如人意,printf("x的值是:%d\n", x)中的第一个x并没有被替换掉,那如何实现我们的需求呢?此时就需要用到#,它可以把宏参数转换成字符串,不信我们试试,对上面的代码稍作修改:

#define PRINT(x) printf(#x"的值是:%d\n", x)
int main()
{
  int a = 0;
  PRINT(a);
  int b = 10;
  PRINT(b);
  return 0;
}

d8cd8b6cfb3c43199f79251d8021c81b.png

 此时我们的需求就得以实现,以PRINT(a);为例分析一下过程:首先用a去替换宏参数x,再把x带入到后面的宏体中,此时a是一个整型,再通过#把整型a转换成一个字符串"a",再利用两个字符串可以自动合并的特性,最终就实现了我们的需求。再来看看预处理后得到的结果:

a46666e8f80a49a889e3714be5ece487.png

 此时我们定义的宏PRINT只能打印整型变量,因为宏体里的格式化打印已经被固定为%d了,我们可以对当前的宏稍作修改,使它也可以打印浮点型数据,即把格式也当作宏参数传递:

#define PRINT(format, x) printf(#x"的值是:"format"\n", x)
int main()
{
  int a = 0;
  PRINT("%d",a);
  int b = 10;
  PRINT("%d",b);
  float c = 2.5f;
  PRINT("%f",c);
  return 0;
}

 经过预处理替换后得到下面的结果:

3f6657a4187e4c01a18c934bd9916597.png

 总结: #可以把宏参数转换成对应的字符串来进行显示,了解了#的作用,接下来了解##,它可以把位于它两边的符号合并成一个符号,它允许宏定义从分离的文本片段创建标识符。如下面的代码:

#define GET(str1, str2) str1##str2
int main()
{
  int Addstudent = 10;
  printf("%d\n", GET(Add, student));
  return 0;
}

8ce9d213233241e1b0c93997ad077324.png


 再看一下他们在编译替换后得到的结果:(printf("%s\n", GET("Hello ", "world"));再gcc环境下会报错)

61bdf4fbf2c34d7cb58d690f087164ea.png

🔖带有副作用的宏参数

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

 先来解释一下什么叫副作用,例如:

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

 上面代码中定义了a = 10,然后想给b赋值成11,这里是通过++a来实现的,它存在的问题是在给b赋值11的同时,a的值也发生了永久性的变化,变成了11,这就叫做副作用,如果写成这样:int b = a + 1;这样就没有副作用了。

 接下来我们看一个带有副作用的宏参数的实例:

#define MAX(x, y) ((x)>(y) ? (x) : (y))
int main()
{
  int a = 4;
  int b = 5;
  int c = MAX(a++, b++);
  printf("c = %d\n", c);
  printf("a = %d\n", a);
  printf("b = %d\n", b);
  return 0;
}

5e4d485c0bb44e36afed7eaadbac6bd1.png

 出现这种结果的原因在于:宏参数是不加运算进行替换的,所以会把副作用传到宏定义中,下面来看看预处理阶段替换后的结果:

a652aa179de94beba79cd4fb7336f31a.png

 与宏参数不同的是:函数的实参会经过运算把最终的结果传递给形参

int GetMax(int x, int y)
{
  return x > y ? x : y;
}
int main()
{
  int a = 4;
  int b = 5;
  //int c = MAX(a++, b++);
  int c = GetMax(a++, b++);
  printf("c = %d\n", c);
  printf("a = %d\n", a);
  printf("b = %d\n", b);
  return 0;
}

e06de347fe9847ffacbb33ecad8c441d.png

🔖宏和函数的对比

 宏通常被应用于执行简单的运算,主要有以下两个方面的原因:

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

 宏的缺点:

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

宏是没法调试的(调试是在已经生成可执行程序后进行的,宏早已被替换)

宏由于与类型无关,也就不够严谨

宏可能带来运算符优先级的问题,导致程序容易出现错误

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

#define MALLOC(type, num) (type*)malloc(sizeof(type)*num)
int main()
{
  int* arr = MALLOC(int, 10);
  char* str = MALLOC(char, 10);
  float* ret = MALLOC(float, 10);
  return 0;
}

d8f658ef8640432ea80b8060c46585b4.png

将宏和函数做如下对比:

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

总结:代码逻辑比较简单可以写成宏,如果代码逻辑比较复杂就写成函数。

🔖命名约定

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

🔖#undef

 这条指令用于移除一个宏定义,例如:

#define MAX 100
int main()
{
  printf("%d\n", MAX);
#undef MAX
  printf("%d\n", MAX);//这里的MAX就成未定义了
  return 0;
}

📖命令行定义

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

#include <stdio.h>
int main()
{
  int arr[SIZE];//这里并没有给SIZE任何初值
    for(int i = 0; i < SIZE; i++)
    {
        arr[i] = i;
    }
    for(int i = 0; i < SIZE; i++)
    {
        printf("%d ",arr[i]);
    }
  return 0;
}

 通过下面这条指令对这段代码进行编译gcc text.c -D SIZE=10 -o text.exe。

ef7aed0b670a4c358e8d7cac6dbe89f4.png

 这就是在命令行中指定某些符号的大小,指定大小后的替换动作也是在预处理阶段完成的,我们可以看看预处理后得到的文件

43c72ecee1fc4b7b87f1d03cf28f8ff1.png

📖条件编译

 借用条件编译指令,我们在编译一个程序的时候可以对一条语句或者一组语句选择性的进行编译。举个🌰:

#define PRINT s//只要定义了就行,可以是任何数字或者字母,甚至没有也可以
int main()
{
#ifdef PRINT
  printf("hehe\n");//只要PRINT定义了,这段代码就可以被编译,没定义就不能被编译
#endif
  return 0;
}

 #ifdef和endif必须是成对出现的。只要#ifdef后面的标识符被定义了,那么#ifdef和endif之间的代码就可以被编译,反知则不能被编译。

ff0a73802ca143a98143a479bb40a494.png

 上图中的源文件中没有对PRINT的定义,所以经过预编译,#ifdef和endif之间的代码块就会被删掉。

 常见的条件编译指令:

  1. 单分支
#if 常量表达式 
//...
#endif
//常量表达式由预处理器求值,
//值为真编译中间的代码块,否则不编译

多分支的条件编译

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

判断是否被定义

#if defined(symbol)//判断symbol是否被定义
//...
#endif
#ifdef symbol//和上面的语句等价,判断symbol是否被定义
//...
#endif
#if !defined(symbol)//如果定义了symbol则不编译下面的代码块
//...
#endif
#ifbdef symbol//如果定义了symbol则不编译代码块
//...
#endif

嵌套指令

#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

📖文件包含

 对于#include相信大家都不陌生,这条指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样。这种替换方式很简单,预处理器先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含几次,就会被编译几次。

🔖头文件被包含的两种方式

  1. 本地文件包含
#include "filename"

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

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

 查找策略: 查找头文件直接去标准路径下查找,如果找不到就提示编译错误。这意味着:对库文件的包含也可以用双引号,但是这样查找的效率就低一些,当然这样也不容易区分是库文件还是本地文件了。

🔖头文件被重复包含

 上面提到过,一个头文件被包含几次,就会被编译几次,就像下面这样:

 当然,大家在写代码的时候应该不会出现上面这种极端情况,在一个源文件里边显式的把一个头文件包含多次。我们可能遇到的是:a包含了b,c也包含了b,d包含了a和c,那这样一来d就间接的把b给包含了两次。如何解决这种问题呢?有以下两种方案:

  1. 条件编译
#ifndef _THE_TEXT_//如果_THE_TEXT_没有被定义就编译下面的代码,否则就不编译
//第一次包含的时候,由于_THE_TEXT_没有定义,所以会编译下面的代码
//当第二次包含的时候,由于第一次包含已经定义了_THE_TEXT_,第二次就不再编译
#define _THE_TEXT_
struct S
{
    int a;
    char c;
};
#endif

17ff890b391d42c988e98dd8f65839ca.png

 可见此时虽然包含了三次,但是预处理的时候只拷贝了一份,这将大大的降低代码冗余。

  1. #pragma once

 #pragma once也可以解决头文件被包含多次的问题,比条件编译更简洁,只需要这一条预处理指令:

#pragma once
struct S
{
    int a;
    char c;
};

b58ac7d5a09d49ddbaa362373f00de56.png

📖模拟实现offsetof

#define OFFSETOF(type, number) (size_t)&(((type*)0)->number)//把0变成一个结构体变量的地址,找到其中的成员再取地址,就是偏移量
struct S
{
  char a;
  int b;
  char c;
};
int main()
{
  printf("%d\n", OFFSETOF(struct S, a));
  printf("%d\n", OFFSETOF(struct S, b));
  printf("%d\n", OFFSETOF(struct S, c));
}

21972a92774f469182cf9acd597a8869.png

d0440f1752074ecca489b1fa2d9da50b.png

📖交换一个二进制数的奇数位和偶数位

#define SWAP(x) (((x&0x55555555)<<1)+((x&0xaaaaaaaa)>>1))
//(x&0x55555555)<<1:保留所有的奇数位,然后左移一位
//(x&0xaaaaaaaa)>>1:保留所有的偶数位,然后右移一位
int main()
{
  int a = 10;
  int b = SWAP(a);
  printf("%d\n", b);
  return 0;
}

04158a6060244cf3bb88bb53c5926c13.png

 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!

目录
相关文章
|
1天前
|
C语言
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)(下)
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)
6 0
|
1天前
|
自然语言处理 编译器 Linux
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(下)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
5 0
|
1天前
|
程序员 编译器 C语言
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(中)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
11 0
|
1天前
|
存储 程序员 编译器
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(上)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
10 0
|
1天前
|
存储 编译器 C语言
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose(下)
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose
7 0
|
1天前
|
存储 数据库 C语言
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose(上)
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose
8 0
|
1天前
|
自然语言处理 编译器 C语言
c语言预处理
c语言预处理
8 0
|
1天前
|
C语言 C++
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(中)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
8 0
|
1天前
|
编译器 数据库 C语言
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
9 0
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
|
1天前
|
存储 C语言
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(下)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
8 0