预处理

预处理

程序编译的四个阶段

  1. 预处理(Preprocessing):在这个阶段,预处理器会处理源代码中的预处理指令,包括宏展开、文件包含、条件编译等。
  2. 编译(Compilation):在编译阶段,编译器将预处理后的代码转换为汇编语言或机器语言。
  3. 汇编(Assembly):在汇编阶段,汇编器将目标代码转换为可重定位目标文件(Object File)。
  4. 链接(Linking):在链接阶段,链接器将可重定位目标文件和库文件进行链接,生成最终的可执行文件。

这四个阶段将按顺序执行。

其中,预处理阶段,主要有以下几个任务:

  1. 宏替换:预处理器会根据#define指令定义的宏,将源代码中的宏标识符替换为相应的宏定义文本。这个过程称为宏替换或宏展开。宏替换可以用来简化代码、增加可读性,以及定义常量或函数宏等。
  2. 头文件展开:预处理器会根据#include指令将指定的头文件内容插入到源代码中。头文件中通常包含了函数原型、宏定义、结构体声明等,这样可以将相同的声明和定义在多个源文件中共享,提高了代码的复用性。
  3. 条件编译:预处理器通过条件编译指令(如#ifdef#ifndef#if等)对代码的编译进行条件判断。根据不同的条件结果,选择性地编译或排除一部分代码。条件编译可以用来实现不同平台的兼容性、调试模式和发布模式的切换等。
  4. 注释删除:预处理器会删除源代码中的注释部分,包括单行注释//和多行注释/* */,因为注释对于编译器来说是无效的代码。
  5. 其他预处理指令处理:预处理器还可以处理和执行其他的预处理指令,如#pragma指令用于向编译器发出特定的指令或提示。

预处理指令

预处理指令以#字符开头,用于在编译过程之前对源代码进行一些处理。下面是一些常见的预处理指令。

  1. #include:用于包含头文件,将指定的文件内容插入到当前位置。
  2. #define:用于定义宏,将一个标识符替换为指定的文本。
  3. #ifdef / #ifndef / #endif:用于条件编译,可以根据条件判断选择性地编译一段代码。
  4. #if / #else / #elif / #endif:用于条件编译,可以根据条件判断选择性地编译一段代码,可以使用表达式进行条件判断。
  5. #pragma:用于向编译器发出特定的指令或提示。

头文件包含

#include 是C语言中的一个预处理指令,用于包含头文件(Header File)。通过使用#include指令,可以将指定的头文件的内容插入到源代码中,以便在代码中可以使用该头文件中定义的函数、结构体、常量等。

一般形式为:

#include <header_file>
#include "header_file"
  • <header_file>:使用<>包含的头文件是系统提供的标准头文件,预处理器会在系统的标准目录中查找这些头文件。
#include <stdio.h>
#include <stdlib.h>
  • "header_file":使用""包含的头文件是用户自定义的头文件,预处理器会在当前文件所在目录或指定的搜索路径中查找这些头文件。
#include "myheader.h"
#include "test/utils.h"
#include "../calc.h"

宏定义

#define 是C语言中的一个预处理指令,用于定义宏(Macro)。通过使用#define指令,可以给一个标识符(通常是一个变量名或函数名)分配一个值或表达式,以后在代码中使用该标识符时会被替换为相应的值或表达式。宏定义的一般形式为:

#define 标识符 值
  1. 定义常量宏:

    #define PI 3.1415926
    #define MAX_SIZE 100
    

    在代码中使用宏时,预处理器会将宏名称替换为相应的值,比如:

    double circleArea = PI * radius * radius;
    

    会被替换为:

    double circleArea = 3.1415926 * radius * radius;
    
  2. 定义函数宏:

image-20230826154807573

#define SQUARE(x) ((x) * (x))

在代码中使用函数宏时,预处理器会将函数宏名称替换为相应的表达式,比如:

int result = SQUARE(5 + 3);

会被替换为:

int result = ((5+3) * (5+3));

3.定义条件编译宏:

#define DEBUG

在代码中使用条件编译宏时,可以使用#ifdef#ifndef等条件编译指令判断是否定义了宏,从而选择性地编译一部分代码,例如:

#include <stdio.h>
#define DEBUG
int main()
{
    int number = 100;
#ifdef DEBUG
    printf("你好, 世界!\n");
#else
    printf("hello, world!\n");
#endif

#ifndef DEBUG
    number += 100;
#endif // !DEBUG
    printf("number = %d\n", number);

	return 0;
}
  • #define DEBUG:定义了一个宏DEBUG,不定义该宏就不存在
  • #ifdef#ifndef 判断指令的使用方法
    • #ifdef...#endif#ifndef...#endif
    • #ifdef...#else...#endif#ifndef...#else...#endif
  • #ifdef指令表示如果定义了某个宏,如:DEBUG
  • #ifndef指令表示如果没有定义某个宏,如:DEBUG

条件编译宏

条件编译宏是一种在C语言中使用预处理指令控制编译过程的技术。通过定义和使用条件编译宏,可以在源代码中根据条件判断选择性地编译一部分代码,从而实现不同的编译路径或功能。

常用的条件编译宏有以下几个:

  1. #ifdef#ifndef:这指令用于判断某个宏是否已经被定义。

    #ifdef DEBUG
        // 定义了 DEBUG 宏对应的代码块
    #else
        // 没有定义 DEBUG 宏对应的代码块
    #endif
    
    • 如果宏 DEBUG 已经被定义,则编译 #ifdef 后面的代码块
    • 如果宏 DEBUG 未定义,则编译 #else 后面的代码块
    #include <stdio.h>
    
    int main()
    {
    #ifdef _WIN32
        printf("这是win32平台\n");
    #else
        printf("这不是win32平台\n");
    #endif // _WIN32
    }
    

    通过上面的代码我们就可以轻而易举地判断出当前是不是WIN32平台。

  2. #if:该指令用于在编译时对表达式进行求值,根据结果判断是否编译代码块中的内容。此处的表达式要求在预处理阶段值是可以被求出的,常见的包括宏定义的值、常量、运算表达式等。

    #if (VALUE == 1)  // 小括号可以省略不写
        // 在VALUE为1时执行的代码
    #elif (VALUE == 2)
        // 在VALUE为2时执行的代码
    #else
        // 在其他情况下执行的代码
    #endif
    

    根据宏 VALUE 的值,编译器会根据条件选择性地编译 #if#elif#else 后面的代码块。

    #define VALUE 5
    int main()
    {
        int number = 5;
    #if VALUE > 5
        number += 10;
    #elif VALUE < 5
        number *= 10;
    #else
        number++;
    #endif 
        printf("number = %d\n", number);
    }
    

    执行程序输出的结果为6

  3. #endif:用于结束条件编译块。

pragma

#pragma 是C和C++语言中的一个预处理指令,用于向编译器发出特定的指示或命令。它的作用是告诉编译器执行一些与编译器相关的特定操作,或者对编译器进行设置。

#pragma directive

其中,directive 表示具体的指示或命令,不同的编译器支持的 pragma 指令可能有所不同。

一些常见的 pragma 指令用法包括:

  1. #pragma once:用于防止头文件的重复包含。#pragma once 告诉编译器只包含一次当前的头文件,避免重复引用。

    #pragma once
    // 头文件的内容
    
  2. #pragma pack:用于设置结构体的内存对齐方式。#pragma pack 可以设置结构体成员的对齐方式,以便在内存中紧凑地存储数据。

    // 将当前的对齐方式压栈,并设置为 n 字节对齐
    #pragma pack(push, n)   
    // 结构体定义和成员
    #pragma pack(pop)       // 恢复之前的对齐方式
    
  3. #pragma warning:用于控制编译器警告的输出级别。#pragma warning 可以修改编译器输出的警告信息级别。

    // 禁用指定警告
    #pragma warning(disable: warning_number)   
    // 恢复指定警告到默认级别
    #pragma warning(default: warning_number)   
    

其他

井号运算符

在C和C++中,# 运算符(井号运算符)用于将宏参数转换为字符串常量。它通常与宏定义一起使用,用于将宏参数的值转换为字符串形式。

版本1

#include <stdio.h>
#define STRINGIZE(x) #x

int main() 
{
    int number = 666;
    const char* str = STRINGIZE(number);
    printf("%s\n", str);

    return 0;
}

执行程序输出的结果为:number

  • STRINGIZE 是一个宏定义,它使用 # 运算符将其参数 x 转换为字符串常量。
  • STRINGIZE(number)展开之后就会将变量number变成字符串number

很显然上面的结果不是我们想要的,因此可以对定义的宏进行升级改造。

版本2

#include <stdio.h>
#define STRINGIZE(x) printf(""#x" value is %d\n", (x))

int main() 
{
    int number = 666;
    STRINGIZE(number);

    return 0;
}

程序输出的结果:

number value is 666
  • 在程序中将STRINGIZE定义成了一个函数宏,对应的是printf
  • 使用#x将参数转换成为字符串之后,如果想要和其它字符串进行拼接,需要将#x放到一个上引号中,即:“#x”

需要注意的是,# 运算符只能用于宏定义中,不能在其他上下文中使用。它的作用是在预处理阶段将宏参数转换为字符串常量,而不是在运行时进行字符串操作。这意味着,# 运算符不能用于将变量或表达式转换为字符串,只能用于宏参数的字符串化操作。

拼接运算符

## 是宏预处理运算符,称为连接运算符或拼接运算符。它只能在宏定义中使用,用于将两个符号(可以是标识符、关键字或其他字符)连接在一起形成一个新的标识符。

下面是一个使用##运算符的示例:

#include <stdio.h>
#define NAME(n) yyds##n
#define STRNAME(n) "yyds_"#n""
#define STRINGIZE(x) printf("yyds"#x" value is %d\n", yyds##x)

int main() 
{
    int NAME(1) = 100;
    int NAME(2) = 200;
    int NAME(3) = 300;
    STRINGIZE(1);
    STRINGIZE(2);
    STRINGIZE(3);

    printf("%s\n", STRNAME(Leifeng));
    printf("%s\n", STRNAME(9527));

    return 0;
}

程序执行的结果如下:

yyds1 value is 100
yyds2 value is 200
yyds3 value is 300

在上面的程序中用到了三个自定义宏:

  • 通过NAME宏可以得到一个标识符,用于表示变量的名字
  • 通过STRNAME宏可以得到一个字符串,可以用于printf打印
  • 通过STRINGIZE宏来打印变量的值

通过使用##运算符,可以根据宏的参数动态地生成标识符。这在一些特定的宏定义情况下非常有用,比如生成一系列类似名称的变量或函数。

需要注意的是,##运算符只能在宏定义中使用,不能在其他地方使用,否则会导致编译错误。此外,使用##运算符时应谨慎,确保正确的拼接和标识符的命名规则。

posted @ 2023-09-08 16:01  ihuahua1415  阅读(145)  评论(0)    收藏  举报
*/