预处理详解

发布于:2024-12-23 ⋅ 阅读:(16) ⋅ 点赞:(0)

预定义符号

C 语言设置了一些预定义符号,可以直接使用。

预定义符号也是在预处理期间处理的。

__FILE__      //进行编译的源文件
__LINE__      //文件当前的行号
__DATE__      //文件被编译的日期
__TIME__      //文件被编译的时间(而非运行的时间)
__STDC__      //如果编译器遵循ANSIC,其值为 1,否则未定义
#include<stdio.h>

int main()
{
	printf("源文件为:%s\n", __FILE__);
	printf("行号为:%d\n", __LINE__);
	printf("日期为:%s\n", __DATE__);
	printf("时间为:%s\n", __TIME__);
	printf("%d\n0", __STDC__);
	return 0;
}

注意:

VS2022并未完全遵循ANSI C,因此在VS2022_STDC_未定义。

在这里插入图片描述

#define

#define定义标识符

若有些常量的值在程序中被大量使用且在不同环境下可能需要修改,可以使用#define来定义。

语法:

#define name stuff

示例:

#define MAX 1000

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

#define do_forever for(;;)	    //⽤更形象的符号来替换死循环

#define CASE break;case			//在写case时自动把 break加上

// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,  \
                          __DATE__,__TIME__ )

注意:

建议不要在#define定义标识符的时候在最后加上;

在下面的情况中,替换后,ifelse之间就是2条语句,而没有大括号的时候,if后边只能有一条语句,这里会出现语法错误。

#define MAX 1000;

//...

int main()
{
    //...

    if (condition)
        max = MAX;
    else
        max = 0;

    //...
}

#define定义宏

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

语法
#define name( parament-list) stuff
//parament-list:由逗号隔开的参数列表

注意:

parament-list的左括号必须与name紧邻,如果两者之间有任何空白,则parament-list可能被解释为stuff的一部分。

示例:

#include<stdio.h>

#define SQUARE(x) x * x

int main()
{
	printf("%d", SQUARE(5));
	//预处理阶段,SQUARE(5)被替换为 5 * 5
	return 0;
}
宏的常见错误
运算符优先级
#define SQUARE(x) x * x
int a = 5;
printf("%d", SQUARE(a + 1));
//预处理阶段
//SQUARE(a + 1)被替换为 a + 1 * a + 1
#define DOUBLE(x) x + x
int a = 5;
printf("%d", 10 * DOUBLE(a + 1);
//预处理阶段
//10 * DOUBLE(a)被替换为 10 * a + 1

宏的替换发生在预处理阶段,而预处理阶段a + 1尚未进行计算。

结论:

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

#define SQUARE(x) (x) * (x)
#define DOUBLE(x) (x) + (x)
宏的副作用

宏参数在宏的定义中出现不止一次的情况下,如果参数存在副作用,那么在使用该宏时就可能存在风险。

副作用就是表达式求值时出现的永久性效果

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

例如:

//宏
#include<stdio.h>

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

int main()
{
	int x = 3;
	int y = 5;
	int z = MAX(x++, y++);
	//预处理阶段替换为z = ( (x++) > (y++) ? (x++) : (y++)); 
	printf("x = %d y = %d z = %d", x, y, z);//输出 x = 4 y = 7 z = 6
	return 0;
}
//函数
#include<stdio.h>

int MAX(int x, int y)
{
	return x > y ? x : y;
}

int main()
{
	int x = 3;
	int y = 5;
	int z = MAX(x++, y++);
	printf("x = %d y = %d z = %d", x, y, z);//输出 x = 4 y = 6 z = 5
	return 0;
}

函数的传参:参数部分计算出结果后,再传参给函数。

宏的传参:直接替换。

因此,我们应该尽量避免给宏传递有副作用的参数

宏替换的规则

在程序中扩展#define定义的符号和宏,需历经以下步骤:

  1. 调用宏时,先检查参数是否有#define定义的符号,若有则先替换。
  2. 接着,将替换文本插入原位置,对于宏,用参数值替换参数名。
  3. 最后,再次扫描结果文件,若含#define定义的符号,重复上述处理。

注意:

  • 在宏参数和 #define 定义中,能够出现其他 #define 定义的符号。但是对于宏,不允许出现递归。
  • 当预处理器对 #define 定义的符号进行搜索时,不会搜索字符串常量中的内容。
宏与函数的对比

宏的优势:

  • 宏通常用于执行简单的运算。用于调用函数和从函数返回的代码所需时间,可能比实际执行小型计算工作所需的时间更多。因此,在运行简单的程序时,宏在程序的规模和速度上优于函数。
  • 函数的参数必须声明为特定的类型,只能在类型合适的表达式上使用。而宏可以适用于整型、长整型、浮点型等,宏的参数是类型无关的
  • 宏有时候可以做到函数做不到的事情,比如宏的参数可以出现类型,而函数无法实现。
#include<stdio.h>

#define MALLOC(num, type) (type*)malloc(num * sizeof(type));

int main()
{
	int* p = MALLOC(10, int);
	if (!p)
		return 1;

	//...

	free(p);
	p = NULL;
	return 0;
}

宏的劣势:

  • 每次使用宏时,宏定义的代码会插入到程序中,若宏的定义较长,可能会大幅增加程序的长度。
  • 宏无法进行调试(调试时程序已经运行,预处理阶段已经完成)。
  • 宏与类型无关,显得不够严谨。
  • 宏还可能引发运算符优先级和参数副作用的问题,致使程序容易出错。
属性 #define定义宏 函数
代码长度 每次使用时,宏代码会被插入程序中,除非宏很小,否则程序长度会大幅增长 代码只出现一处,每次调用同一份,通过函数栈帧执行
执行速度 更快 存在调用和返回的额外开销
操作符优先级 宏参数的求值是在周围表达式的上下文环境里,除非加括号,否则邻近操作符的优先级可能会导致不可预料的后果。 函数参数只在函数调用时求值一次,表达式的结果更易预测。
带有副作用的参数 参数可能被替换到宏体的多个位置,如果宏的参数被多次计算,求值可能会产生不可预料的后果。 参数只在传参时求值一次,结果更易控制
参数类型 宏的参数与类型无关,只要对参数的操作合法即可用于任何类型 参数与类型有关,不同类型需不同函数
调试 不方便 可以逐语句调试
递归 不能
命名约定

一般函数和宏的使用语法非常相似,可能比较难以区分。

因此我们的命名习惯为:

  • 宏的名字全部大写
  • 函数的名字不要全部大写

###

#
铺垫

字符串在打印时是具有拼接能力的,即多个字符串最终可以合并成一个字符串打印。

#include<stdio.h>

int main()
{
	printf("hello world!");//输出 hello world!
	printf("hello" " world!");//输出 hello world!
	return 0;
}
引入
#include<stdio.h>

int main()
{
	int a = 10;
	printf("the value of a = %d\n", a);

	float f = 3.5f;
	printf("the value of f = %f\n", f);

	return 0;
}

两次打印操作有些相似,那么可不可以用一个函数来替代呢?

显然,如果使用函数来实现上述功能,会出现一些传参的问题。

我们考虑用宏来解决问题。

由于预处理器对 #define 定义的符号进行搜索时,不会搜索字符串常量中的内容,我们尝试以下写法:

#include<stdio.h>

#define Print(n, format) printf("the value of ""n"" = "format"\n", n);

int main()
{
	int a = 10;
	Print(a, "%d");//输出 the value of n = 10
	float f = 3.5f;
	Print(f, "%f");//输出 the value of n = 3.500000
	return 0;
}

这种方式定义的宏,打印结果中的n并没有被替换,因为"n"也被认定为字符串。

由于参数n为后续format所要替换的值,我们也无法效仿format传入字符串参数。

#

#运算符能够将宏的一个参数转换为字符串字面量。它只允许在带参数的宏的替换列表中出现。

#运算符所执行的操作可以理解为“字符串化”。

#include<stdio.h>

#define Print(n, format) printf("the value of "#n" = "format"\n", n)

int main()
{
	int a = 10;
	Print(a, "%d");//预处理为 printf("the value of ""a"" = ""%d""\n", n);
	float f = 3.5f;
	Print(f, "%f");//预处理为 printf("the value of ""f"" = ""%f""\n", n);
	return 0;
}
##

##可以把位于它两边的符号合成一个新的符号。##被称为记号粘合。

##允许宏定义从分离的文本片段创建标识符。

#include<stdio.h>

#define count(x, y) x##y

int main()
{
	int num1 = 10;
	printf("%d", count(num, 1));
	return 0;
}

这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

##可以简化一些比较繁琐的情况。

例如:

写一个函数求 2 个数的较大值时,不同的数据类型就要写不同的函数。

int int_max(int x, int y)
{
	return x > y ? x : y;
}

float float_max(float x, float y)
{
	return x > y ? x : y;
}

//...

使用##可以大幅简化代码:

#define GENERIC_MAX(type)		\
type type##_max(type x, type y) \
{								\
	return x > y ? x : y;		\
}

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;
}

#undef

这条指令用于移除一个#define定义。

#define NAME 10

int main()
{
    //...

#undef NAME
}

命令行定义

许多 C 编译器具备一种能力,允许在命令行中定义符号,以此来启动编译过程。

例如,当依据同一个源文件编译一个程序的不同版本时,这个特性很有用处。(假定在某个程序中声明了一个某个长度的数组,如果机器内存有限,就需要一个很小的数组;而在另一个内存较大的机器上,就需要一个更大的数组)。

#include <stdio.h>
int main()
{
    int array[ARRAY_SIZE];//ARRAY_SIZE未定义

    //... 
    
    return 0;
}

Linuxgcc编译器的编译指令:

gcc -D ARRAY_SIZE=10 programe.c

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃,我们可以通过条件编译指令实现。

条件编译是一种在编译阶段根据特定条件决定是否将一段代码包含进最终生成的可执行文件或目标代码的技术。简单来说,就是通过预定义的条件,例如指定的宏、操作系统类型、编译器选项等,来控制哪些代码被实际编译,哪些代码被忽略。

常见的条件编译指令:

  1. 条件判断
#if 常量表达式
    //...
#endif
//常量表达式由预处理器求值
  1. 多条件的条件编译
#if 常量表达式
    //...
#elif 常量表达式
    //...
#else
    //...
#endif
  1. 判断是否被定义
判断是否被定义
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol
  1. 嵌套指令
#if defined(stmbol1)
    #ifdef OPTION1
        //...
    #endif
    #ifdef OPTION2
        //...
    #endif
#elif defined(stmbol2)
    #ifdef OPTION3
        //...
    #endif
#endif

条件编译与选择语句:

  1. 执行时机:
  • 条件编译发生在编译阶段,由预处理器根据定义的条件决定是否将某段代码包含在最终的编译结果中,可能会导致某些代码段完全不参与编译,可以一定程度减少程序长度,减少运行时间。
  • 选择语句作用于程序运行时的执行流程,所有代码都会被编译,根据条件的真假来决定执行哪部分代码。
  1. 灵活性:
  • 条件编译更多用于处理与编译环境、平台等相关的差异,灵活性相对较低。
  • 选择语句可以处理更复杂和动态的条件,在运行时根据各种变化的条件进行决策,灵活性更高。

头文件的包含

头文件被包含的方式

本地文件包含
#include "filename"

查找策略:当以""来包含头文件时,编译器首先会在源文件所在的目录下进行查找。倘若未能找到这个头文件,编译器就会如同查找库文件那样,在标准路径位置继续查找。要是最终还是找不到,就会提示编译错误。

库文件包含
#include <filename>

查找策略:当以<>来包含头文件时,编译器会直接前往标准路径进行查找,如果未能找到,就会提示编译错误。

对于库文件是否也可以使用""的形式包含?

可以,但是这样做的查找效率会变低,而且这样也不容易区分是库文件还是本地文件

嵌套文件包含

在预处理阶段,#include指令可以使头文件被编译,就像它实际出现于#include指令的地方一样。

这种替换的方式很简单:预处理器先删除这条指令,并用头文件的内容替换。

如果头文件被多次包含,那么展开的重复信息就会增多,这会给编译带来较大的压力。

test.c

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
    //...
     
    return 0;
}

test.h

void test();
struct  Stu
{
    int id;
    char name[20];
};

如果是在集体开发的环境中,如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果可能不堪设想。

那如何解决头文件被重复引用的问题?

答案:条件编译

方式 1

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif 

方式 2

#pragma once
//头⽂件的内容