C语言自学--预处理详解

目录

1、预定义符号

2、使用#define定义常量

3、使用#define定义宏

4、存在副作用的宏参数

5、宏替换规则

6、宏函数对比

7、#和##运算符

8、命名规范

9、#undef指令

10、命令行定义

11、条件编译

12、头文件包含

13、其他预处理指令


不知不觉来到了C语言学习最后一章,今天就把最后的焚诀交出来

1、预定义符号

C语言提供了一些可直接使用的预定义符号,这些符号在预处理阶段就会被处理。

c 复制代码
__FILE__     // 当前源文件名
__LINE__     // 当前代码行号  
__DATE__     // 源文件编译日期
__TIME__     // 源文件编译时间
__STDC__     // 若编译器遵循ANSI C标准则值为1,否则未定义
cpp 复制代码
//打印输出当前文件的文件名
printf("file:%s line:%d\n", __FILE__);

//打印输出当前文件的文件行号
printf("file:%s line:%d\n", __LINE__);

//打印输出当前文件的文件日期
printf("file:%s line:%d\n", __DATE__ );

//打印输出当前文件的文件时间
printf("file:%s line:%d\n", __TIME__ );

//编译器遵循ANSI C标准则值为1,否则未定义
//vs没有完全遵循ANSI C标准
//printf("file:%s line:%d\n", __STDC__;

2、使用#define定义常量

基本语法:

cpp 复制代码
#define name  stuff
c 复制代码
#define MAX 1000
#define reg register //为register这个关键字,创建⼀个简短的名字
 
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
#define CASE break;case  //在写case语句的时候⾃动把break写上。
c 复制代码
// 如果定义的stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t" \
                          "date:%s\ttime:%s\n", \
                          __FILE__, __LINE__,   \
                          __DATE__, __TIME__)

思考:在define定义标识符的时候,要不要在最后加上;?

cpp 复制代码
#define MAX 1000;
#define MAX 1000
  • 建议不要加上**;**这样容易出问题,例如:
cpp 复制代码
 if(condition)
     max = MAX;
 else
     max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有⼀ 条语句。这⾥会出现语法错误。
*

cpp 复制代码
 if(condition)
     max =1000;;//替换后,这里就两条语句,会编译错误
 else
     max = 0;

3、使用#define定义宏

#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏 (definemacro)。 下⾯是宏的申明方式:

cpp 复制代码
#define name( parament-list ) stuff

注意:parament-list 是一个逗号分隔的符号表,可能出现在 stuff 中。参数列表的左括号必须紧贴 name 之后,若两者之间存在任何空白,该参数列表将被视为 stuff 的一部分。

cpp 复制代码
#define SQUARE( x )  x * x
cpp 复制代码
#include <stdio.h>
#define SQUARE(n) n * n 
int main()
{
	int a = 5;
	int ret = SQUARE(a);
	printf("%d",ret);
	return 0;
}

警告:该宏存在一个问题,请观察以下代码片段:

cpp 复制代码
#include <stdio.h>
#define SQUARE(n) n * n 
int main()
{
	int a = 5;
	int ret = SQUARE(a+1);
	printf("%d",ret);
	return 0;
}

为什么结果不正确呢?替换文本时,参数x被替换成a+1,所以这条语句实际上变成了:

cpp 复制代码
printf ("%d\n",a + 1 * a + 1 );

在宏定义上加上两个括号,这个问题便轻松的解决了:

cpp 复制代码
 #define SQUARE(x)  (x) * (x)

这样预处理之后就产生了预期的效果:

cpp 复制代码
 printf ("%d\n",(a + 1) * (a + 1) );

这里还有⼀个宏定义:

cpp 复制代码
#define DOUBLE(x) (x) + (x)

定义中我们使⽤了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

cpp 复制代码
#include <stdio.h>
#define SQUARE(x) (x) + (x) 
int main()
{
	int a = 5;
	//int ret = SQUARE(a + 1);
	printf("%d",10*SQUARE(a));
	return 0;
}

这将打印什么值呢?看上去,好像打印100,但事实上打印的是55. 我们发现替换之后:

cpp 复制代码
 printf ("%d\n",10 * (5) + (5));

乘法运算先于宏定义的加法,所以出现了 55 . 这个问题,的解决办法是在宏定义表达式两边加上⼀对括号就可以了。

cpp 复制代码
#define DOUBLE( x)   ( ( x ) + ( x ) )

提示:

所有用于对数值表达式进行求值的宏定义都应采用加括号的方式,以避免使用宏时因参数中的运算符或邻近运算符之间产生不可预料的相互影响。


4、存在副作用的宏参数

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

cpp 复制代码
x+1;//不带副作⽤
 
x++;//带有副作⽤ 
cpp 复制代码
#include <stdio.h>
#define MAX(a, b)  ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	return 0;
}

这⾥我们得知道预处理器处理之后的结果是什么:

cpp 复制代码
 z = ( (x++) > (y++) ? (x++) : (y++));

所以输出的结果是:x=6y=10z=9


5、宏替换规则

程序扩展#define定义的符号和宏时,需遵循以下流程:

  1. 参数检查阶段:调用宏时,首先检查参数是否包含其他#define定义的符号。若存在,则优先替换这些符号。

  2. 文本替换阶段:将替换文本插入程序原位置。对于宏定义,参数名称会被对应的值替换。

  3. 二次扫描阶段:重新扫描处理后的文本,检查是否仍包含#define定义的符号。若存在,则重复上述替换过程。

注意事项:

  • 宏参数和#define定义中允许引用其他#define符号,但禁止出现递归定义。
  • 预处理器在搜索#define符号时,不会检查字符串常量的内容。

6、宏函数对比

宏通常用于执行简单运算。例如,要在两个数中找出较大值时,使用以下宏更为高效:

cpp 复制代码
 #define MAX(a, b) ((a)>(b)?(a):(b))

为什么不使用函数来完成这个任务?主要有两个原因:

  1. 调用函数和返回值的开销可能超过实际执行这个小型计算所需的时间。因此,宏在程序规模和执行速度方面更具优势。
  2. 更重要的是,函数参数必须声明为特定类型,这限制了函数只能用于类型匹配的表达式。而宏则适用于整型、长整型、浮点型等所有支持">"比较操作的类型,具有类型无关的特性。

宏相比函数的劣势:

  1. 每次调用宏时,其定义代码会被直接插入程序,可能导致代码量显著增加(除非宏定义非常简短)
  2. 宏不支持调试功能
  3. 宏不进行类型检查,缺乏严谨性
  4. 宏可能引发运算符优先级问题,容易导致程序错误

但宏也具有函数无法替代的特性,例如:宏参数可以包含类型声明,这是函数所不具备的。

cpp 复制代码
#define MALLOC(num, type) \
     (type )malloc(num  sizeof(type))
 ...
 //使⽤
MALLOC(10, int);//类型作为参数
 
//预处理器替换之后:
 (int )malloc(10  sizeof(int));

7、#和##运算符

7.1、 #运算符

#运算符用于将宏参数转换为字符串字面量。它只能出现在带参宏的替换列表中,其作用可理解为"字符串化"。例如,当我们定义变量int a = 10;时,若想输出the value of a is 10,可以这样编写:

cpp 复制代码
#define PRINT(n) printf("the value of "#n " is %d", n);

当我们按照下⾯的⽅式调用的时候: PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为a,时⼀个字符串 代码就会被预处理为:

cpp 复制代码
 printf("the value of ""a" " is %d", a);

运⾏代码就能在屏幕上打印:

cpp 复制代码
the value of a is 10

7.2 、##运算符

它能够将位于两侧的符号合并成一个新符号,这种特性使得宏定义可以从分散的文本片段构建标识符。 ## 这种操作称为记号粘合。需要注意的是,粘合后必须形成一个合法的标识符,否则结果将是未定义的。举个例子,当我们编写一个求两数较大值的函数时,针对不同数据类型就需要编写不同的函数。

cpp 复制代码
int int_max(int x, int y)
{
   return x>y?x:y;
}
   float float_max(float x, float y)
{
   return x>yx:y;
}

但是这样写起来太繁琐了,现在我们这样写代码试试:

cpp 复制代码
//宏定义
#define GENERIC_MAX(type)      \
type type##_max(type x, type y)\
{                              
    return (x>y?x:y);         
}

使用宏,定义不同函数

cpp 复制代码
 GENERIC_MAX(int)     //替换到宏体内后int##_max ⽣成了新的符号int_max做函数名
 
GENERIC_MAX(float)   //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名

int main()
{
 //调⽤函数 
     int m = int_max(2, 3);
     printf("%d\n", m);
     float fm = float_max(3.5f, 4.5f);
     printf("%f\n", fm);
     return 0;
 }

输出:

cpp 复制代码
3
4.500000

8、命名规范

通常来说,函数和宏的使用语法非常相似,因此语言本身无法帮助我们区分二者。为此,我们通常会采用以下命名习惯:

  • 宏名全部使用大写字母
  • 函数名不使用全大写形式

9、#undef指令

cpp 复制代码
 #undef NAME
 //如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

10、命令行定义

大多数C编译器都支持在命令行中定义符号的功能,这主要用于控制编译过程。例如,当我们需要基于同一源代码编译程序的不同版本时,这个特性就特别实用。假设程序中声明了一个固定长度的数组:在内存受限的机器上,我们需要较小的数组;而在内存充足的机器上,则可以使用更大的数组。

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

编译指令:

cpp 复制代码
 //linux 环境演⽰
 gcc -D ARRAY_SIZE=10 programe.c

11、条件编译

使用条件编译指令可以方便地在编译程序时选择性地包含或排除特定语句(或语句组)。

例如:调试代码删除可惜,保留又影响整洁,因此我们可以通过选择性编译来解决这个问题。

cpp 复制代码
#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 //__DEBUG__
    }
    return 0;
}

常见的条件编译指令:

cpp 复制代码
1.
#if  常量表达式        
//...
#endif
//常量表达式由预处理器求值。

如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
cpp 复制代码
2.
多个分⽀的条件编译
#if 常量表达式       
        //...
#elif 常量表达式        
        //...
#else
        //...
#endif
cpp 复制代码
3.
判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
cpp 复制代码
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

12、头文件包含

12.1 、头文件被包含的方式:

12.1.1、 本地文件包含
cpp 复制代码
#include "filename"

头文件查找步骤:

  • 优先在当前源文件所在目录搜索
  • 若未找到,则转到标准库目录继续查找
  • 若仍未找到,编译器将报错终止编译

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

cpp 复制代码
/usr/include

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

cpp 复制代码
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径,注意按照⾃⼰的安装路径去找。
12.1.2、 库文件包含
cpp 复制代码
 #include <filename.h>

编译器在查找头文件时,会优先在标准路径下搜索。如果未能找到,则会报编译错误。需要注意的是,库文件确实可以使用双引号("")的方式包含,但这种做法存在两个问题:首先,查找效率会降低;其次,这种方式难以区分库文件和本地文件。

12.2、嵌套文件包含

重复包含头文件会导致多次编译,例如同一个头文件被包含10次就会编译10次,这会增加编译负担。

cpp 复制代码
test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
	return 0;
}
cpp 复制代码
test.h头文件
void test();
struct  Stu
{
	int id;
	char name[20];
};

如果直接这样编写代码,test.c 文件会包含 test.h 头文件 5 次,导致 test.h 的内容在预处理阶段被复制 5 份到 test.c 中。如果 test.h 文件较大,预处理后的代码量会急剧膨胀。对于大型工程而言,公共头文件被多个源文件频繁引用且未做任何处理,后果将非常严重。

如何避免头文件被重复包含?解决方案是使用条件编译。

每个头文件的开头写:

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif   //__TEST_H_

或者:

cpp 复制代码
 #pragma once

13、其他预处理指令

cpp 复制代码
 #error
 #pragma
 #line
 ...

#pragma pack()在结构体部分介绍。

#pragma 指令

#pragma 是编译器特定的指令,用于提供额外的功能或控制编译器的行为。不同编译器支持的 #pragma 指令可能不同。

常见的 #pragma 用法:

cpp 复制代码
#pragma once  // 防止头文件被重复包含(非标准但广泛支持)
#pragma message("自定义编译消息")  // 输出编译时消息
#pragma pack(1)  // 设置结构体对齐方式

#error 指令

#error 用于在编译时强制生成错误消息并停止编译,通常用于检查不满足的编译条件。

示例:

cpp 复制代码
#if !defined(MAX_SIZE)
#error "MAX_SIZE must be defined"
#endif

#line 指令

#line 用于修改编译器报告的行号和文件名,主要用于代码生成工具。

示例:

cpp 复制代码
#line 100 "modified.c"  // 将下一行设为第100行,文件名改为modified.c

_Pragma 操作符

C99引入的标准化替代#pragma的方式,可以在宏中使用。

示例:

cpp 复制代码
_Pragma("pack(1)")  // 等同于 #pragma pack(1)

#warning 指令

GCC等编译器支持的扩展,用于生成编译警告而不停止编译。

示例:

cpp 复制代码
#warning "This is a warning message"

14、尾声

不知不觉,已经更新完C语言这个章节了,感谢各位读者支持,我们下一个章节再见。

相关推荐
沐知全栈开发5 小时前
Vue3 计算属性
开发语言
冰糖雪梨dd5 小时前
JS中new的过程发生了什么
开发语言·javascript·原型模式
川石课堂软件测试6 小时前
全链路Controller压测负载均衡
android·运维·开发语言·python·mysql·adb·负载均衡
杨福瑞6 小时前
C语言⽂件操作讲解(总)
c语言·开发语言
hz_zhangrl7 小时前
CCF-GESP 等级考试 2025年9月认证C++四级真题解析
开发语言·c++·算法·程序设计·gesp·c++四级·gesp2025年9月
止水编程 water_proof8 小时前
Java--网络编程(二)
java·开发语言·网络
润 下8 小时前
C语言——深入解析C语言指针:从基础到实践从入门到精通(三)
c语言·开发语言·经验分享·笔记·学习·程序人生·其他
知白守黑2678 小时前
docker网络
开发语言·php
细节控菜鸡8 小时前
【2025最新】ArcGIS for JS 范围裁剪(只保留特定区域显示),实现精准地理范围聚焦
开发语言·javascript·arcgis