C语言进阶——程序环境和预处理(下)

简介: 宏不仅会因为优先级问题造成影响,还会因为参数传递导致副作用,因为宏参数在传递后,会原封不动的进行替换,某些操作会对参数本身造成影响,而函数就没有这种问题

🪴2.3.4、带有副作用的参数

 宏不仅会因为优先级问题造成影响,还会因为参数传递导致副作用,因为宏参数在传递后,会原封不动的进行替换,某些操作会对参数本身造成影响,而函数就没有这种问题


宏:举一个比较极端的例子,来说明宏传参有副作用这件事

//计算两数+1后的较大值
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
  int x = 1;
  int y = 2;
  int z = MAX(++x, ++y);
  printf("x = %d y = %d z = %d\n", x, y, z);
  return 0;
}

结果: x = 2 y = 4 z = 4


这次的宏体没有问题,问题就出在宏参数上,下面是替换后的代码


int z = ((++x) > (++y) ? (++x) : (++y));    //预编译处理后

走读代码:


1.首先明确为前置++,先+1,再使用

2.把 x 和 y 进行比较,因为是前置++,此时 x = 2  y = 3,显然为假,走后面的语句

3.找出较大值后,执行 ++y ,结果为 4,将这个值返回给 z

4.此时 x = 2,y = 4,z = 4

按照预料值,计算结果应该为 x = 2,y = 3,z = 3,因为宏参数计算存在副作用,所以计算结果才会有出入,即使换成后置,也会有问题。较为稳妥的做法,就是先把 x、y 分别+1,再作为宏参数传递使用。


函数:跟上面一样的功能,不过换成了函数形式


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

结果: x = 2 y = 3 z = 3


可以看到此时的运算结果是正确的,原因很简单,函数在传递参数时,只会进行一次运算,也就是说,实参 ++x、++y 在传递给形参后,已经变成了 2、3,所以运算就没有问题。


当然这里是有意设置的(对参数 x、y造成了影响),实际使用中,最好不要传递自增和自减,避免出现副作用


🪴2.3.5、参数类型

 宏是没有规定参数类型的,而函数规定了参数类型,这就注定函数只能完成指定类型的操作,而宏可以适用于所有数据类型,这点上宏是比较好的


宏:比如模拟实现一个适用于所有数据类型的加法程序


#define ADD(x, y) ((x) + (y))
int main()
{
  printf("%d\n", ADD(2, 3));
  printf("%f\n", ADD(1.1, 2.2));    //甚至能用于浮点型
  printf("%c\n", ADD('A', 32));
  return 0;
}

结果: 5


          3.30000


          a


函数:一样的操作,不过把宏换成了函数


int add(int x, int y)
{
  return x + y;
}
int main()
{
  printf("%d\n", add(2, 3));
  printf("%f\n", add(1.1, 2.2));    //调用失败,类型不匹配
  printf("%c\n", add('A', 32));
  return 0;
}

结果: 5


          0.000000


          a


浮点型在内存中存储规则不同与整型,强行计算会出问题,因此编译器直接给出了 0.000000 这个结果


🪴2.3.6、能否调试

 宏是直接替换的,代码都在一行中,是调试不了的(当然可以通过汇编指令观察到细节);而函数相对来说比较独立,能够进入到函数体中,进行逐步调试


宏:直接替换后,无法调试,或者说想调试很困难,只能通过汇编代码逐细节观察


函数:函数具有完整的躯干,可以进入到函数体内,逐语句调试


🪴2.3.7、能否递归

 宏是不能递归的,因为递归需要两个必要条件:限制条件和接近条件,而宏定义是直接替换的,条件是不能设置的;函数能实现递归,递归思想:大事化小,可以通过对同一个函数的不断调用,来完成复杂的任务。


宏:在一个宏体内放入当前宏体名,这是不合理的(参考 2.2.3 宏的替换规则),举个栗子


//递归打印数位,从低位开始
#define TEST(x) (if(x < 10) printf("%d\n",(x));  else { printf("%d ",x % 10) ; TEST(x / 10);})
int main()
{
  TEST(123456);
  return 0;
}

结果: 编译失败,报了很多种错误


函数:一模一样的代码(除了宏名部分),让函数来完成


void test(int x)
{
  //当然这是不好的代码风格,这里是为了和上面对比
  if (x < 10) printf("%d\n", (x));  else { printf("%d ", x % 10); test(x / 10); }
}
int main()
{
  test(123456);
  return 0;
}

结果: 6 5 4 3 2 1


函数成功完成了任务,这个例子很好的说明了宏是无法使用递归的


🪴2.3.8、结论

 函数和宏各有各的好处,要根据实际需求选择使用,使用时要注意优先级和副作用问题


属性 #define 定义的宏 函数

代码长度 如果多次调用,替换后代码会很长 一份代码,多次调用

运行速度 预处理阶段直接替换,比函数更快 需要进行调用、返回等操作

操作符优先级 可能存在隐藏的优先级问题 相对隔离,不必担心此问题

带有副作用的参数 替换后,可能会进行多次运算 只有在传递时进行运算

参数类型 没有固定类型,合法就行 类型固定,使用时要与之相匹配

能否调试 不方便调试 可以进行逐语句调试

能否递归 不能递归 可以递归

 函数和宏都是很好的工具,关于函数的更多知识可以点击这里,宏的优缺点如下:


优点:


1.宏的运行速度比函数更快

2.宏与类型无关,涉及多类型的简单算法推荐使用宏

缺点:


1.当多次调用宏时,除非宏体很短,否则会大幅度增加程序的长度

2.宏是不方便调试的

3.宏没有类型,不够严谨

4.宏在使用时,可能会出现优先级和副作用问题

  🌱2.4、#undef 移除宏定义

 除了能 #define 定义符号外,还能 #undef 移除宏定义


语法:


#undef name    

//name 是已经定义好的符号名或宏名,必须合法(存在)


#define MAX 100
#define ADD(x,y) (x + y)
int main()
{
#undef MAX  //取消定义的标识符 MAX
  printf("%d\n", MAX);
#undef ADD  //取消定义的宏 ADD
  printf("%d\n", ADD(2, 3));
  return 0;
}

结果: 报错, 显示 MAX ADD未定义


  🌱2.5、命令行定义

 C语言中允许在命令行中定义符号,当然是在编译前定义(得在 Linux 上用 gcc 命令行编译的方式展示)


举个栗子


//命令行定义
int main()
{
  int arr[ARR_SIZE];
  int i = 0;
  for (i = 0; i < ARR_SIZE; i++)
  arr[i] = i;
  for (i = 0; i < ARR_SIZE; i++)
  printf("%d ", arr[i]);
  printf("\n");
  return 0;
}

在 Linux 环境下 gcc 中输入指令: gcc -D ARR_SIZE=10 test.c        //假设文件为 test.c


此时程序中的数组大小就变为了10,显然命令行定义的方式能让程序更加灵活,环境适配性更强


结果: 1 2 3 4 5 6 7 8 9 10


  🌱2.6、条件编译

 之前学过分支与循环,其中的条件满足时才会执行语句。C语言中还提供了一组条件编译函数,这些函数能决定后续语句是否需要编译。


🪴2.6.1、单分支条件编译

语法:


#if   #endif

// #if 后面跟条件表达式,当条件成立,后续代码才会编译


// #endif 条件编译块结束的标志,每个 #if 都必须有一个 #endif 与之匹配

int main()
{
#if 1 > 2
  printf("hello "); //条件不成立,此条语句不参与编译
#endif
  printf("world\n");
  return 0;
}

结果: world


看看预编译产生的 .i 文件


19999c9e682847c697d77e2687a230a7.png


 注意:有 #if 就要有 #endif ,二者互为彼此存在的必要条件


🪴2.6.2、多分支条件编译

 多分支是在单分支基础上增加了两条语句:否则(#else) 与 否则如果(#elif)


语法:


#if   #elif   #else   #endif

//其中,#if   #elif 后面都需要跟条件表达式


//如果前两个都为假,那就编译 #else 后的语句


// #endif 服务于 #if ,不可缺失


//当然多分支可写的更细,这就不就展开叙述


int main()
{
#if 1>2
  printf("1\n");
#elif 4>3
  printf("2\n");    //只有这条语句参与编译
#else
  printf("3\n");
#endif
  return 0;
}

结果: 2


看看预编译产生的 .i 文件

2c63033afd684f9dba6992a84b79bf91.png

不难看出,多分支条件编译就跟多分语句一样,只会选择一个通道进行编译


 注意:在使用多分支编译语句时,逻辑要严谨,设计要合理


🪴2.6.3、判断是否定义过宏

 我们可以定义宏、取消宏,还可以判断宏是否已定义


语法:


#if defined( )   #endif

//这个是判断宏有没有定义过,如果定义了,就执行后续语句


#if !defined( )   #endif

//这个是上面的反面,逻辑取反嘛,如果没定义,就执行后续语句


下面这俩是上面判断语句的另一种写法(个人比较推荐下面的写法,不需要加 ( ) 号)


#ifdef   #endif

//判断是否定义过,如果定义过,执行后续语句


#ifndef   #endif

//判断是否没有定义过,如果没有定义,执行后续语句


#define ADD(x, y) ((x) + (y))
int main()
{
//判断是否定义过
#if defined(ADD)
  printf("Yes\n");    //这个宏是已经定义了的
#endif
//判断是否没定义
#ifndef SUB
  printf("Yes\n");    //这个宏没定义
#endif
  return 0;
}

结果: Yes


          Yes


两种写法我都展示了,展示的还是不同的逻辑,判断定义就是这么用的


🪴2.6.4、嵌套使用条件编译

 下面演示一段三种条件编译语句混合的代码:


//#define OS_UNIX
#define OS_MSDOS
int main()
{
#if defined(OS_UNIX)
#ifdef UNIX_1
  printf("Welcome to UNIX_1\n");
#endif
#ifndef UNIX_2
#define UNIX_2
  printf("Increase UNIX_2\n");
#endif
#elif defined(OS_MSDOS)
  printf("Welcome to Windows\n");
#else
  printf("The system does not exist");
#endif
  return 0;
}

结果: Welcom to Windows


这段代码中包含了单分支、多分支、判断定义的知识 ,可以嵌套使用,灵活强大


 那么这种条件编译在现实中存在吗?


 答:存在,且使用很频繁,比如下图为VS中某头文件的定义截图

edf2088410f547ddbdbc16d39a78e14f.png

  🌱2.7、文件包含

 最后再来谈谈C语言中头文件的包含方式,分为自定义头文件和库文件的包含


🪴2.7.1、自定义头文件的包含

 自定义头文件在包含时,只能用 " " 引出自定义头文件名,如果像库函数头文件那样包含,是不会成功的,因为< >这种包含方式,是在标准路径下寻找头文件(C语言自带库函数头文件位于此目录下),显然这个路径中是不会有我们自定义头文件的,因此只能使用 " " 引出自定义头文件。


 " " 包含头文件的查找策略是:先在当前目录下寻找目标头文件,找到了就打开,如果没找到,就会跑到标准路径下寻找,再找不到,就打开失败。显然,如果是头文件不存在的情况下,需要查找两次,效率会比较低。

f8bb83d6a9d0471d8828e1d6a7fb0f71.png


36cfd052a976410182a77ae02ffa488e.png


🪴2.7.2、库函数头文件的包含

 库函数头文件在包含时,一般使用 < > 引出库文件名,被< >引出的头文件,编译器会直接去标准路径下寻找,只要没写错,那一般都能找到。" " 引头文件时,虽然要查找两次,但最终也会找到标准路径下,那么能否使用 " " 引库函数头文件呢?


 答案是不推荐,如果使用 " " 引库函数头文件的话,可以正常打开,但会拖慢运行速度,毕竟要查找两次。同时我们在使用时,就不能一眼辨别出,哪些是自定义头文件,哪些是库函数头文件了。


#include<stdio.h> //库函数头文件的包含风格
#include"Add.h" //自定义头文件的包含风格

🪴2.7.3、避免多次展开同一头文件

 头文件在被成功调用后,在预编译阶段会被展开(详情转至 1.1.1),光是 stdio.h 这个头文件就被展开了一万多行代码,如果不做特殊处理,然后多包含几次头文件,那光是在预编译阶段就会出现很多很多行代码了,并且这些代码还是重复的,为此要对头文件做一些特殊处理,避免其被多次展开


方法1(比较远古的方法)


 通过给头文件打标记,避免多次展开,会用到条件编译


#if !defined __TEST_H__ //打个标记,如果是第一次被引用
#define __TEST_H__  //就会创建一个标识符,然后开始预处理头文件中的内容
//预处理头文件中的内容
#endif

结果: 当第一次展开头文件时,没有识别到标记 __TEST_H__  之后会定义标记,再展开头文件;等后续在次文件中再次展开头文件时,识别到标记,不会继续展开代码,这样在预编译阶段就不会重复展开头文件了(不得不佩服前人的智慧)


方法2(现在比较常用的方法)


 这个就比较简单了,在头文件第一行直接使用提前定义好的工具就行,底层原理还是条件编译


#pragma once  //在头文件首行放置此条语句,可以避免重复展开

实际运用


 比如我们在VS中创建一个头文件,当文件创建完成后,编译器会自动在首行添加方法2中的语句,现在编译器太智能了,再比如下图为 stdio.h 这个头文件的首行

e6b348bd0a234db18e2fedcc14a362c1.png

 足以看出这个东西是真实存在的,我们在创建自定义头文件时,可不敢把首行代码删除。


推荐了解其他预处理指令


#error

#pragma

#line

……

🌳总结

 以上就是关于C语言程序环境和预处理的所有内容了,如果你在看完此文后能对C语言代码的运行有一个新的认识,那么本文就值了;如果你对涉及命令行的操作还不太熟悉,没有关系,现在可以先了解,等以后学习了 Linux 的相关知识后,再回来解决就行了。

830cd069aebc46dba519ce854502e23d.png

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


 写在最后:本文结束后,我们C语言的学习就可以宣布毕业了!从7月16日发布第一篇文章,到10月16结束最后一篇文章,历时三个月,此专栏的文章数达到了19篇,收获了近一万的文章阅读量和大量的点赞、收藏和评论,在此感谢一直支持我博客创作的朋友们,你们的支持是我坚持创作的最大动力!感谢那个拥有坚定信念的自己,一直坚持学习,砥砺前行。


 当然,只是C语言整体知识系列划上了句号,其他文章还是会继续更新下去的, 比如 数据结构 | C 这个系列,还有高深一些的 C语言高阶——函数栈帧的创建和销毁 ,其他好玩的小程序、有意义的题解等。新的旅途即将开始,就像章北海一样,我们的征途将是更加广袤的大海!

90d15b0f47024483aba0a7bb7b365743.jpg

目录
相关文章
|
1月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
1月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
19天前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
21 2
|
1月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
1月前
|
存储 文件存储 C语言
深入C语言:文件操作实现局外影响程序
深入C语言:文件操作实现局外影响程序
|
1月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
1月前
|
Linux C语言 iOS开发
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
19 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
C语言
C语言及程序设计初步例程-4 C语言程序初体验
贺老师教学链接  C语言及程序设计初步 本课讲解 让程序会计算:求a和b两个数之和 #include &lt;stdio.h&gt; int main( ) { int a,b,sum; scanf("%d %d", &amp;a, &amp;b); sum=a+b; printf("%d\n", sum); return 0; } 用户界面友好(或罗
1087 0
|
C语言 数据处理
《C语言及程序设计》实践项目——C语言程序初体验
返回:贺老师课程教学链接  C语言及程序设计初步   【项目1-输出点阵图】编一个程序,用你的姓名读音首字母,组成类似的趣图提示:printf("……\n");语句会输出双引号中的内容,'\n'完成换行[参考解答]【项目2-完成简单计算】(1)编程序,输入长方形的两边长a和b,输出长方形的周长和面积 提示:边长可以是整数也可以是小数;实现乘法的运算符是*[参考解答] (2)编程序,输入两个电
1267 0