C语言关于预处理命令的理解

简介: C语言关于预处理命令的理解

预定义符号

C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号是在预处理期间执行的:

__FILE__ //进⾏编译的源⽂件

__LINE__ //⽂件当前的⾏号

__DATE__ //⽂件被编译的⽇期

__TIME__ //⽂件被编译的时间

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

__STDC__提示错误表明,证明VS2022并不完全遵照ANSI C标准🤡

#define定义常量

基本语法:#define 常量名 值或表达式

#define的多种用法及注意事项:

1、定义常量

#defineMAX 1000

#define STR   “hehe”

2、简化名称

#definereg register //register这个关键字,创建⼀个简短的名字

3、效果替换

#definedo_forever for(;;) //⽤更形象的符号来替换⼀种实现

for(;;)中判断条件为空则语句陷入死循环

4、简略代码

#defineCASE break;case //在写case语句的时候⾃动把break写上

5、如果定义的stuff过⻓,可以分⾏写,除了最后⼀⾏外,每⾏的后⾯加⼀个反斜杠(续⾏符)

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

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

__FILE__,__LINE__ , \

__DATE__,__TIME__ )

定义最后不加分号

#define定义宏

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

简单形式的声明方式:#define 宏名 替换文本

函数形式的声明方式#define 宏名(参数列表) 替换文本

参数列表的左括号必须与宏名紧邻,如果两者之间有空⽩,参数列表就会被视为stuff的⼀部分

#define SQUARE(X) X*X
int main()
{
  int a = 5;
  printf("%d\n", SQUARE(a));
  return 0;
}

但是,这样的书写方式还可能会造成一些问题,当我们将括号内的a改为a+2时:

#define SQUARE(X) X*X
int main()
{
  int a = 5;
  printf("%d\n", SQUARE(a+2));
  return 0;
}

结果为17而非49,这是因为a+2在进行替代时的结果是: a+2*a+2  而非 (a+2)*(a+2)

所以我们需要在宏定义的时候加上括号: #define SQUARE(X) (X)*(X)

结论:⽤于对数值表达式进⾏求值的宏定义都视情况加上括号,避免在使⽤宏时由于参数中的操作符或邻近操作符之间不可预料的相互作⽤。

带有副作用的宏参数

概念:带有副作用的宏是指在使用宏时,会对其参数或其他代码产生意外或非预期的影响。这些副作用可能包括但不限于改变参数值、多次执行某段代码、修改全局变量等。

       由于宏是简单的文本替换,在展开过程中没有类型检查和语法分析,因此如果在宏定义中使用了具有副作用的表达式或语句,可能会导致程序行为出现错误,并且很难进行调试:

#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
    int num = 5;
    int result = SQUARE(num++);  // 带有副作用
    printf("Result: %d\n", result);
    printf("Num: %d\n", num);
    return 0;
}

在上面的示例中,在主函数中将 `num++` 传递给SQUARE宏时,变成了 `(num++) * (num++)` ,这是一种未定义的行为,因为C语言没有规定函数实参的求值顺序,所以无法确定先增加哪个操作数,所以最终结果取决于编译器和平台。

结论:避免使用具有副作用的宏可以提高代码的可读性和可维护性。如果需要执行一系列复杂操作,建议使用函数而不是宏来实现,因为函数会进行参数求值顺序的明确定义和类型检查。

宏和函数的对比

命名约定

把宏名全部⼤写

函数名不要全部⼤写

宏常用于执行简单的运算,比如找两个数中的最大数:

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

执行该计算宏与函数相比的优势:

  1. 用于函数执行调用和返回所需要的时间会更多
  2. 函数的参数类型需要事先声明,而宏可以适用于各种类型之间的比较

很明显的,在使用宏时只会进行mov(数据传送:第一个参数是目的操作数,第二个参数是源操作数)操作而在使用函数时要进行mov mov call(调用函数)mov这四个操作,其中call操作后还要进入函数内部执行一系列操作......

执行该计算宏与函数相比的劣势:

  1. 每次使用宏时都会将一份宏定义的代码插入程序中,若宏定义较长则大幅度增加程序长度
  2. 调试时程序已经开始执行而宏是预处理命令所以宏不能被调试
  3. 宏与类型无关,不够严谨(双刃剑)
  4. 宏可能会带来运算符优先级的问题,容易导致程序出错
//每次使用宏时都会将一份宏定义的代码插入程序中
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
  //宏
  int n = MAX(100, 101);   
  //int n = ((100) > (101) ? (100) : (101));
  int m= MAX(100, 101);
  //int m = ((100) > (101) ? (100) : (101));
  return 0;
}

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

#define MALLOC(n,type) (type*)malloc(n*sizeof(type))
int main()
{
  int* p = MALLOC(10, int);
  return 0;
}

宏与函数的对比

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

结论:计算逻辑简单用宏,计算逻辑复杂用函数

#运算符(字符串化运算符)

作用:用于将宏参数转换为字符串常量(只能在宏定义的替换文本中使用)

当我们有⼀个变量 int a = 10; 的时候,我们想打印出 the value of a is 10;就可以写:

#definePRINT(n) printf("the value of "#n " is %d", n);

当我们输入PRINT(a)时,代码被预处理为:

printf("the value of ""a" " is %d", a);

#define Print(n,format) printf("the value of " #n " is" format"\n",n);
                                             //"a"      //%d
                                             //"b"      //%f
int main()
{
  int a = 10;
  //printf("the value of a is %d"\n",a);
  Print(a,"%d");
  float b = 3.14f;
  //printf("the value of b is %f"\n",a);
  Print(b, "%f");
  return 0;
}

#undef移除宏

作用:移除⼀个宏定义

格式:#undef 宏名

条件编译

我们可以通过条件编译指令决定一条/组语句是否进行编译:满足条件就编译,不满足就放弃

1、单分支条件编译

格式:#if   常量表达式

                   //...

          #endif

#define FLAG 2
int main()
{
#if FLAG == 1;
  printf("hehe\n");
#endif
  return 0;
}

2、多分支条件编译

格式:#if   常量表达式

                   //...

          #elif 常量表达式

                   //...

          #else

                   //...

          #endif

#define FLAG 3
int main()
{
#if FLAG == 1;
  printf("hehe\n");
#elif FLAG == 2
  printf("haha\n");
#else 
  printf("heihei\n");
#endif
  return 0;
}

3、判断是否被定义

格式

//symbol被定义时为真的两种写法:

#if defined(symbol)  

#ifdef symbol          (建议这种写法)

//symbol未被定义时为真的两种写法:

#if !defined(symbol)

#ifndef symbol        (建议这种写法)

//symbol被定义时为真的两种写法
int main()
{
//#if defined(MAX)
//  printf("hehe\n");
//#endif
#ifdef MAX 
  printf("haha\n");
#endif // MAX 
  return 0;
}
//symbol未被定义时为真的两种写法
int main()
{
//#if !defined(MAX)
//  printf("hehe\n");
//#endif
#ifndef MAX 
  printf("haha\n");
#endif // MAX 
  return 0;
}

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

头文件的包含

本地头文件包含

格式:#include ”自定义头文件名“

查找方式:先在源文件所在目录下查找,如果该头文件未找到,则与查找库函数头文件一样在标准位置查找头文件,如果找不到就报错

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

/usr/include

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

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

//这是VS2013的默认路径

注意按照⾃⼰的安装路径去找

库文件包含

格式:#include <库文件名>

查找方式:在标准路径下寻找,如果找不到就报错

库⽂件也可以使⽤ “” 的形式包含,但是查找效率低,同时也不容易区分是库⽂件还是本地⽂件

嵌套文件包含

//test.h自定义头文件
void test();
struct Stu
{
int id;
char name[20];
};
//test.c源文件
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}

       如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。那么如何解决头⽂件被重复引⼊的问题?答案:条件编译

每个头⽂件的开头写:

#ifndef__TEST_H__

#define__TEST_H__

//非自定义/自定义头文件

#endif

或者

#pragmaonce

///非自定义/自定义头文件

//方法一
#ifdef  _ADD_H_
#define _ADD_H_
void test();
struct Stu
{
  int id;
  char name[20];
};
#endif 
//方法二:
#pragma once
void test();
struct Stu
{
  int id;
  char name[20];
};

~over~

相关文章
|
2月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
2月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
2月前
|
自然语言处理 编译器 Linux
【C语言篇】编译和链接以及预处理介绍(上篇)1
【C语言篇】编译和链接以及预处理介绍(上篇)
42 1
|
27天前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
24 2
|
2月前
|
编译器 Linux C语言
【C语言篇】编译和链接以及预处理介绍(下篇)
【C语言篇】编译和链接以及预处理介绍(下篇)
32 1
【C语言篇】编译和链接以及预处理介绍(下篇)
|
2月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
2月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
2月前
|
存储 C语言
【C语言篇】编译和链接以及预处理介绍(上篇)2
【C语言篇】编译和链接以及预处理介绍(上篇)
37 0
|
3月前
|
存储 C语言
C语言程序设计核心详解 第七章 函数和预编译命令
本章介绍C语言中的函数定义与使用,以及预编译命令。主要内容包括函数的定义格式、调用方式和示例分析。C程序结构分为`main()`单框架或多子函数框架。函数不能嵌套定义但可互相调用。变量具有类型、作用范围和存储类别三种属性,其中作用范围分为局部和全局。预编译命令包括文件包含和宏定义,宏定义分为无参和带参两种形式。此外,还介绍了变量的存储类别及其特点。通过实例详细解析了函数调用过程及宏定义的应用。
|
3月前
|
Shell Linux API
C语言在linux环境下执行终端命令
本文介绍了在Linux环境下使用C语言执行终端命令的方法。首先,文章描述了`system()`函数,其可以直接执行shell命令并返回结果。接着介绍了更强大的`popen()`函数,它允许程序与命令行命令交互,并详细说明了如何使用此函数及其配套的`pclose()`函数。此外,还讲解了`fork()`和`exec`系列函数,前者创建新进程,后者替换当前进程执行文件。最后,对比了`system()`与`exec`系列函数的区别,并针对不同场景推荐了合适的函数选择。