37 预处理器与预处理指令、宏定义(定义常量、数据类型、替换文本、嵌套与取消)、带参宏(细节处理、与函数的区别)

news2024/10/4 21:22:40

目录

1 预处理器

2 预处理指令

2.1 位置

2.2 格式

2.3 换行

2.4 结束符

2.5 位置限制

3 宏定义

3.1 语法格式

3.2 使用宏定义常量

3.3 使用宏定义数据类型

3.4 宏定义的替换文本

3.5 宏定义嵌套

3.6 取消宏定义

4 带参数的宏定义

4.1 语法格式

4.2 案例演示

4.3 注意事项

4.3.1 宏名和形参列表之间不能有空格

4.3.2 可以省略形参的数据类型

4.3.3 形参和表达式建议使用小括号包裹

4.3.4 宏定义的优先级高于编译器的语法解析

4.3.5 不会进行语法检查

4.4 带参宏定义和函数的区别

4.4.1 对比总结表

4.4.2 案例对比演示


1 预处理器

        C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。

        预处理器的任务是对源代码进行初步处理,生成经过预处理的中间代码,然后再将这些中间代码送入编译器进行编译

        预处理器的主要任务包括:

  • 宏替换:用特定的文本替换宏定义中的标识符。
  • 文件包含:将指定的文件内容插入到当前文件中。
  • 条件编译:根据条件选择性地编译某些代码段。
  • 其他任务:如删除注释、展开行号和文件名信息等。

2 预处理指令

        预处理指令以 # 号开头,用于指导预处理器执行不同的任务。预处理指令具有以下特点:

2.1 位置

        预处理指令通常应该放在代码的开头部分,但在某些情况下也可以放在代码的其他地方

        强烈建议将预处理指令放在文件的顶部,以提高代码的可读性和可维护性。

2.2 格式

        预处理指令都以 # 开头,指令前面可以有空白字符(比如空格或制表符),# 和指令的其余部分之间也可以有空格,但为了兼容老的编译器,一般不留空格。

// 推荐写法
#include <stdio.h> // 可以使用格式化代码工具

// 不推荐写法
    #include<stdio.h>
#include    <stdio.h>
#     include     <stdio.h>

2.3 换行

        预处理指令默认是一行的,如果需要折行,可以在行尾使用反斜杠 \

#include <std\
io.h>

2.4 结束符

        预处理指令不需要分号作为结束符,指令结束是通过换行符来识别的

#include <stdio.h>; 	// 这里有分号会报错
#define PI 3.14;    	// 分号会成为 PI 的值的一部分

2.5 位置限制

        预处理指令通常不能写在函数内部,尽管某些编译器的扩展允许这样做,但强烈不建议这么做,以保持代码的可移植性和一致性。

int main () 
{
    // 一般不允许写在这里
    #include <stdio.h>
    return 0;
}

3 宏定义

3.1 语法格式

        宏定义是 C 语言预处理器的一种功能,用于用一个标识符(宏名称)来表示一个替换文本如果在后面的代码中出现了宏名称,预处理器会将它替换为对应的文本,这一过程称为宏替换宏展开

        宏定义的基本语法形式如下:

#define 宏名称 替换文本
  • 宏名称:宏的名称,是一个标识符,通常使用大写字母表示,以便与变量名区分开来
  • 替换文本:宏名称在代码中的每次出现都会被替换为这段文本【纯粹的文本替换

3.2 使用宏定义常量

        宏定义常量是一种常见的用法,可以用来定义一些固定的数值,这样可以在代码中统一管理和修改这些常量。

#include <stdio.h>

// 定义常量 PI
#define PI 3.14

int main() {
    // 定义变量保存半径,值通过用户输入获取
    double radius;
    printf("请输入半径:");
    scanf("%lf", &radius);

    // 计算面积并输出
    double area = PI * radius * radius;
    printf("圆的面积:%.2f\n", area);

    return 0;
}

        在上面的示例中,使用宏定义声明了常量 PI,在后续代码中,每次出现 PI 都会被预处理器替换为 3.14。

        可以在终端中输入预处理指令:gcc -E 源文件名.c -o 源文件名.i ,然后查看生成的 “源文件名.i” 的文件内容,如下所示:

3.3 使用宏定义数据类型

        在 C 语言中,宏定义不仅可以用来定义常量,还可以用来定义数据类型。通过宏定义数据类型,可以使代码更具可读性和可维护性。下面是一个具体的示例,展示了如何使用宏定义来定义布尔类型。

#include <stdio.h>

// 宏定义布尔类型
#define BOOL int
#define TRUE 1
#define FALSE 0

int main() {
    // 使用宏定义的布尔类型表示真假两种状态
    BOOL isPass = FALSE;
    BOOL isOk = TRUE;

    if (isPass) {
        printf("Pass\n");
    } else {
        printf("Not Pass\n");
    }

    if (isOk) {
        printf("Ok\n");
    } else {
        printf("Not Ok\n");
    }

    return 0;
}

        在上面的示例中,使用宏定义声明了 BOOL、TURE、FALSE,在后续代码中,每次出现 BOOL 都会被预处理器替换为 int,每次出现 TRUE 都会替换成 1,每次出现 FALSE 都会替换成 0。

3.4 宏定义的替换文本

        宏定义的替换文本可以包含任何字符,它可以是字面量、表达式、if 语句、函数调用等。预处理程序对替换文本不作任何检查,直接进行文本替换纯粹的文本替换。如果有错误,只能在编译已被宏展开后的源程序时发现。

#include <stdio.h>

// 宏定义
#define M (n * n + 3 * n)
#define PRINT_SUM printf("sum=%d\n", sum)

int main()
{
    int n = 3;
    int sum = 3 * M + 4 * M + 5 * M;
    // 宏展开 3 * (n * n + 3 * n) + 4 * (n * n + 3 * n) + 5 * (n * n + 3 * n);
    PRINT_SUM;
    // 宏展开 printf("sum=%d\n", sum);

    return 0;
}

3.5 宏定义嵌套

        宏定义允许嵌套,即在宏定义的替换文本中可以使用已经定义的宏名。在宏展开时,预处理程序会层层替换这些宏名。

#include <stdio.h>

// 定义常量 PI
#define PI 3.1415926

// 定义计算圆面积的宏,使用已定义的 PI
#define S PI * r * r

int main() {
    int r = 2;
    printf("%f\n", S); // 宏替换变为 printf("%f", 3.1415926 * r * r);

    return 0;
}

        可以在终端中输入预处理指令:gcc -E 源文件名.c -o 源文件名.i ,然后查看生成的 “源文件名.i” 的文件内容,如下所示:

3.6 取消宏定义

#undef 宏名

        如需取消宏定义,可以使用 “#undef 宏名” 命令。“#undef 宏名” 命令用于取消先前通过 #define 指令定义过的宏,使得该宏在后续代码中不再有效

        如果尝试取消一个未定义的宏,#undef 指令将被忽略,不会产生错误。

#include <stdio.h>

// 定义常量 PI
#define PI 3.14159

void func1()
{
    printf("PI=%f\n", PI); // 这里可以使用 PI
}

int main()
{
    printf("PI=%f\n", PI); // 这里可以使用 PI

#undef PI // 取消宏定义

    // 下面的代码会出错,因为 PI 已经被取消定义
    printf("PI=%f\n", PI);

    return 0;
}

void func2()
{
    // 下面的代码会出错,因为 PI 已经被取消定义
    printf("PI=%f\n", PI);
}

        一旦宏被取消定义,之后在代码中再尝试使用该宏将导致编译错误,报错如下所示:


4 带参数的宏定义

4.1 语法格式

        C 语言允许宏带有参数。在宏定义中的参数称为 “形式参数”,在宏调用中的参数称为 “实际参数”,这一点与函数类似。对带参数的宏,在展开过程中不仅要进行文本替换,还要用实际参数去替换形式参数

        带参宏定义的一般形式如下所示:

#define 宏名(形参列表) 替换文本
// 这里的“宏名”和紧随其后的左括号“(”之间不能有任何空格
  • 宏名:宏的名称,通常使用大写字母表示
  • 形参列表:宏的形式参数列表,用逗号分隔
  • 替换文本:宏定义的替换文本,可以包含形式参数

        带参宏调用的一般形式如下所示:

宏名(实参列表);
  • 实参列表:宏调用时的实际参数列表,用逗号分隔

4.2 案例演示

        使用宏定义,返回两数最大值。

#include <stdio.h>

// 定义带参数的宏
//  1. MAX 就是带参数的宏
//  2. (a,b) 就是形参
//  3. (a>b) ? a : b是带参数的宏对应字符串,该字符串中可以使用形参
// #define MAX(a, b) (a > b) ? a : b
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 建议使用括号包裹形参

int main()
{
    int x, y, max;
    printf("输入两个数字: ");
    scanf("%d %d", &x, &y);

    // 调用带参数的宏
    // 1. MAX(x, y); 调用带参数宏定义
    // 2. 在宏替换时,预处理器会进行文本替换,同时会使用实参去替换形参
    // 3. 即 MAX(x, y) 宏替换成:((x) > (y) ? (x) : (y))
    max = MAX(x, y);
    printf("最大值: %d\n", max);

    return 0;
}

4.3 注意事项

4.3.1 宏名和形参列表之间不能有空格

        带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。否则会导致编译错误。因为:预处理器在处理宏定义时,会严格按照语法解析宏定义。如果宏名和形参列表之间有空格,预处理器会将空格之后的内容视为宏定义的替换文本的一部分,而不是形参列表。这会导致宏定义的语法错误或不符合预期的行为。

        但是,对于函数而言,函数名和紧随其后的左括号 “(” 之间可以有空格。

#define MAX(a, b) ((a) > (b) ? (a) : (b))  // 正确
#define MAX (a, b) ((a) > (b) ? (a) : (b))  // 错误

4.3.2 可以省略形参的数据类型

        在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。但是,对于函数而言,在函数原型声明或定义时,形参名虽然可以省略,但是形参的数据类型不可以省略。

        在带参宏调用中,实际参数包含了具体的数据,需要用它们去替换形式参数,因此实际参数必须指明数据类型,同函数调用时实参一样。

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 10;
int max = MAX(x, y);  // 正确

4.3.3 形参和表达式建议使用小括号包裹

        在宏定义中,替换文本内的形参建议使用括号括起来以避免出错。特别是当形参参与复杂的表达式时,括号可以确保表达式的正确性,以避免因运算符优先级导致的错误。

        宏定义可能会导致意外的副作用,特别是在带参数的宏中。为了避免副作用,可以使用括号包围宏定义中的表达式。 

#include <stdio.h>

// 使用括号的宏定义
#define ADD_WITH_PARENS(a, b) ((a) + (b))
#define SQ_WITH_PARENS(y) ((y) * (y))

// 不使用括号的宏定义
#define ADD_WITHOUT_PARENS(a, b) a + b
#define SQ_WITHOUT_PARENS(y) y * y

int main()
{
    int x = 3, y = 2;

    // 使用括号的宏定义
    int result1_with_parens = ADD_WITH_PARENS(x + 1, y + 1);
    int result2_with_parens = SQ_WITH_PARENS(x + 1);

    // 输出结果
    printf("使用括号的宏定义:\n");
    printf("result1_with_parens = %d\n", result1_with_parens); // 期望结果是 7,实际结果是 7
    printf("result2_with_parens = %d\n", result2_with_parens); // 期望结果是 16,实际结果是 16

    // 不使用括号的宏定义
    int result1_without_parens = ADD_WITHOUT_PARENS(x + 1, y + 1);
    int result2_without_parens = SQ_WITHOUT_PARENS(x + 1);

    // 输出结果
    printf("\n不使用括号的宏定义:\n");
    printf("result1_without_parens = %d\n", result1_without_parens); // 期望结果是 7,实际结果是 7
    // 替换过程:x + 1 +  y + 1
    printf("result2_without_parens = %d\n", result2_without_parens); // 期望结果是 16,实际结果是 7
    // 替换过程:x + 1 * x + 1

    return 0;
}

4.3.4 宏定义的优先级高于编译器的语法解析

        宏定义的替换发生在编译之前,因此优先级高于编译器的语法解析

#include <stdio.h>

#define X 10
#define Y 20
#define Z ((X) + (Y))

int main()
{
    int result = Z * 10;
    printf("%d", result); // 结果是 300,而不是 210,表达式不加括号的话就是 210

    return 0;
}

4.3.5 不会进行语法检查

        宏定义可以包含复杂的表达式、if 语句、函数调用等,但预处理器不会对这些内容进行语法检查,因此错误只能在编译阶段发现

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define PRINT_IF_TRUE(cond, msg) if (cond) { printf("%s\n", msg); }

4.4 带参宏定义和函数的区别

4.4.1 对比总结表

特性带参宏定义(Macro)函数(Function)
本质文本替换可重用的代码块
处理时机编译前由预处理器处理编译阶段由编译器处理
计算不对表达式进行计算,仅是文本替换会对表达式进行计算,执行代码
内存占用不占用内存(在编译前已处理)会占用内存(代码段)
开销没有函数调用的开销(直接替换)有函数调用开销(参数传递、栈帧管理等)
优化无法享受编译器的优化(因为仅是文本替换)可以享受编译器的优化(类型检查、代码优化等)
安全性容易引入副作用,特别是在复杂表达式中类型安全,编译阶段进行类型检查和优化
可读性代码可读性较差,难以调试代码可读性和可维护性更好,易于理解和调试
适用场景适用于简单、高效的代码片段替换适用于需要复用、类型安全、可调试的代码块

4.4.2 案例对比演示

        分别使用函数和带参数的宏计算平方值。

函数实现:

#include <stdio.h>

// 定义计算平方的函数
int SQ(int y)
{
    return y * y;
}

int main()
{
    int i = 1;
    while (i <= 5)
    {
        printf("%d\n", SQ(i++)); // 1 4 9 16 25
    }
    printf("i=%d", i); // i=6

    return 0;
}

带参数的宏实现:

#include <stdio.h>

// 定义计算平方的宏
#define SQ(y) ((y) * (y))

int main()
{
    int i = 1;

    while (i <= 5)
    {
        // SQ(i++) 会被宏替换为 ((i++) * (i++)),
        printf("%d\n", SQ(i++)); // 错误操作,i++ 会执行两次,最终无法得到我们想要的结果

        // 修改成下面的就可以了
        // printf("%d\n", SQ(i));
        // i++;
    }
    printf("i=%d", i); // i=7

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188712.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ElasticSearch学习笔记(三)Ubuntu 2204 server elasticsearch集群配置

如果你只是学习elasticsearch的增、删、改、查等相关操作&#xff0c;那么在windows上安装一个ES就可以了。但是你如果想在你的生产环境中使用Elasticsearch提供的强大的功能&#xff0c;那么还是建议你使用Linux操作系统。 本文以在Ubuntu 2204 server中安装elasticsearch 8.…

go的一些知识点

一.package 1.新建项目 新建一个itying文件夹&#xff0c;在里面使用命令 就能生成一个go项目。生成一个go.mod 2.调用别的包的代码 按照下面的目录层级生成代码 //clac.go package calcfunc Add(x, y int) int {return x y } func Sub(x, y int) int {return x - y }…

【Web】复现n00bzCTF2024 web题解(全)

目录 File Sharing Portal 方法一&#xff1a; 方法二&#xff1a; Focus-on-yourSELF Passwordless File Sharing Portal 附件的Dockerfile给了这么一段 # Add the cron job to the crontab RUN mkdir /etc/cron.custom RUN echo "*/5 * * * * root rm -rf /app…

<<迷雾>> 第6章 加法机的诞生(1)--全加器 示例电路

全加器的符号 info::操作说明 鼠标单击开关切换开合状态 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhttps://book.xiaogd.net/cyjsjdmw-examples/assets/circuit/cyjsjdmw-ch03-01-full-adder.txt 原图 全加器的逻辑电路实现 info::操作说明 鼠标单击…

ACT调试pycharm报错

在运行ACT 代码时&#xff0c;根据官方readme使用命令行需要在wandb选择的时候输入3 但是&#xff0c;使用pycharm运行的时候会报错 wandb.errors.UsageError: api_key not configured (no-tty). call wandb.login(key[your_api_key]) 网上搜索都是说要注册什么key&#xf…

平衡BST:AVL树的实现与机制

目录 AVL树的简介 AVL节点的构建 AVL树体的构建 具体片段解析 旋转算法 AVL树的验证 AVL树的简介 AVL树是一种自平衡的二叉搜索树&#xff0c;它在19世纪60年代由Adelson-Velsky和Landis首次提出。在AVL树中&#xff0c;任何节点的两个子树的高度最大差别为1&#xff0c;这…

python-FILIP/字符串p形编码/数字三角形

一&#xff1a;FILIP 题目描述 给你两个十进制正整数 a,b​&#xff0c;输出将这两个数翻转后的较大数。 「翻转」在本题中的定义详见「说明 / 提示」部分。输入 第一行&#xff0c;两个十进制正整数 a,b。输出 第一行&#xff0c;a 和 b 翻转后的较大数。样例输入1 734 893 样…

《凡人修仙传》TXT精校全本|知轩藏书校对版!

看了动漫版&#xff0c;准备重温下原著&#xff0c;有好几年没看了。 最近找到了知轩藏书的校对版&#xff0c;堪称精校&#xff0c;nice&#xff01; TXT&#xff0c;14.5MB&#xff1a; https://pan.quark.cn/s/c6446be393fa

二叉树进阶学习——从中序和后续遍历序列构建二叉树

1.题目解析 题目来源&#xff1a;106.从中序和后序遍历序列构造二叉树 测试用例 2.算法原理 后序遍历&#xff1a;按照左子树->右子树->根节点的顺序遍历二叉树&#xff0c;也就是说最末尾的节点是最上面的根节点 中序遍历&#xff1a;按照左子树->根节点->右子树…

gm/ID设计方法学习笔记(一)

前言&#xff1a;为什么需要gm/id &#xff08;一&#xff09;主流设计方法往往侧重于强反型区&#xff08;过驱>0.2V&#xff09;&#xff0c;低功耗设计则侧重于弱反型区&#xff08;<0&#xff09;&#xff0c;但现在缺乏对中反型区的简单和准确的手算模型。 1.对于…

C++系列-二叉搜索树

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 二叉搜索树 二叉搜索树又称二叉排序树&#xff0c;它或者是一颗空树&#xff0c;或者是具有以下性质的树 若它的左子树不为空&#xff0c;则左子树上的所有节点的值都小于根节点…

大数据实时数仓Hologres(四):基于Flink+Hologres搭建实时数仓

文章目录 基于FlinkHologres搭建实时数仓 一、使用示例 二、方案架构 1、架构优势 2、Hologres核心优势 三、实践场景 四、项目准备 1、创建阿里云账号AccessKey 2、准备MySQL数据源 五、构建实时数仓​编辑 1、管理元数据 2、构建ODS层 2.1、创建CDAS同步作业OD…

鸿蒙网络管理模块03——多播DNS管理

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、概述 多播DNS也简称MDNS(Multicast DNS)&#xff0c;他主要提供局域网内的本地服…

NVIDIA Ampere 架构

全球超强弹性数据中心的核心。 文章目录 前言一、突破性创新1. 第三代 Tensor 核心2. 多实例 GPU (MIG)3. 第三代 NVLink4. 结构化稀疏5. 第二代 RT 核心6. 更聪明、快速的内存二、为规模化部署而优化1. 为各种服务器优化性能2. 统一计算和网络加速3. 密度优化的设计4. 安全部署…

leetcode练习 路径总和II

给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum 22 输出&a…

ElasticSearch备考 -- 查询模版

一、题目 ### 基础版 Create a search template for the above query, so that the template (i) is named "with_response_and_tag", (ii) has a parameter "with_min_response" to represent the lower bound of the response field, (iii) has a parame…

二叉树进阶学习——从前序和中序遍历序列构造二叉树

1.题目解析 题目来源&#xff1a;105.从前序与中序遍历序列构造二叉树——力扣 测试用例 2.算法原理 首先要了解一个概念 前序遍历&#xff1a;按照 根节点->左子树->右子树的顺序遍历二叉树 中序遍历&#xff1a;按照 左子树->根节点->右子树的顺序遍历二叉树 题目…

10款好用的开源 HarmonyOS 工具库

大家好&#xff0c;我是 V 哥&#xff0c;今天给大家分享10款好用的 HarmonyOS的工具库&#xff0c;在开发鸿蒙应用时可以用下&#xff0c;好用的工具可以简化代码&#xff0c;让你写出优雅的应用来。废话不多说&#xff0c;马上开整。 1. efTool efTool是一个功能丰富且易用…

java入门基础(一篇搞懂)

​ 如果您觉得这篇文章对您有帮助的话 欢迎您分享给更多人哦 感谢大家的点赞收藏评论&#xff0c;感谢您的支持&#xff01;&#xff01;&#xff01; 首先给大家推荐比特博哥&#xff0c;java入门安装的JDk和IDEA社区版的安装视频 JDK安装与环境变量的配置 IDEA社区的安装与使…

多线程-初阶(1)

本节⽬标 • 认识多线程 • 掌握多线程程序的编写 • 掌握多线程的状态 • 掌握什么是线程不安全及解决思路 • 掌握 synchronized、volatile 关键字 1. 认识线程&#xff08;Thread&#xff09; 1.1 概念 1) 线程是什么 ⼀个线程就是⼀个 "执⾏流". 每个线…