C语言零基础彻底掌握预处理上篇

目录
  • 1、#define的深度认识
    • 1.1 数值宏常量
    • 1.2 字符串宏常量
    • 1.3 用宏充当注释符号
    • 1.4 用宏替换多条语句
    • 1.5 宏定义的使用建议
  • 2、#undef 撤销宏
    • 2.1 宏的定义位置和有效范围
    • 2.2 宏的取消
    • 2.3 一道笔试题

1、#define的深度认识

1.1 数值宏常量

宏定义数值常量相信大家都不陌生,相信很多小伙伴用过,这里我们就简单的提一下,我们前面也讲过,#define 本质上是替换,它可以出现在代码的任何地方,也可以把任何东西都定义成宏,编译器会在预编译的时候进行替换掉,举例:

#dfeine PI 3.1415926

这样在以后的代码中你就可以用 PI 来代替 3.1415926 那么这样做的好处是什么呢?假设在未来的某一天,你要提升这个精度,如果你代码中出现 3.1415926 过多的话,你提升精度还得一个个修改, 如果使用宏定义的话,你只需要改一次即可。

1.2 字符串宏常量

除了宏定义常量之外,还经常用来定义字符串,特别是路径:

① #define PATH_1 D:\code\lesson1\test

②#define PATH_1 "D:\code\lesson1\test"

以上哪个是正确的呢?如果觉得太长还可以用续行符:

③ #define PATH_1 "D:\code\lesson1\\test"

很显然第一个肯定是不对的,字符串需要用 "" 引起来,第三个也不对,第二个呢?我们去实践证明下(以上写法都不推荐!):

在Linux平台环境下:

在Windows环境下:

很显然他们都有同样的警告,都是未知转义序列,也无法正确打印出我们的路径,在前面我们讲到,' \ ' 是转义字符,当我们要打印路径的时候需要用转义字符 ' \ ' 去还原 ' \ ' 的字面意思,所以这里打印路径要用 \\ !

注意:Windows路径分隔是用 ' \ ',而Linux路径分隔是用 ' / ',所以如上测试用例改成 ' / ' 的话是不会报警告的。

所以要正确的打印如上用例应该这样写:

//不使用续行符
#define PATH_1 "D:\\code\\lesson1\\test"
//使用续行符
#define PATH_1 "D:\\code\\lesson1\\\test"

1.3 用宏充当注释符号

因为 Linux 环境能直接查看预处理过程,便于我们验证,所以我们下边会在 Linux 环境下测试。

我们先简单了解下程序的翻译过程:

  1. 预处理-E:头文件展开,去注释,宏替换,条件编译...
  2. 编译-S:将预处理后的C语言翻译成汇编语言
  3. 汇编-c:将汇编语言转化为可目标二进制文件( 可被链接 )
  4. 链接:将目标二进制文件与相关库链接,形成可执行程序

这里我们来看一段用宏充当注释符号的代码:

#include <stdio.h>
#define BSC //
int main()
{
	BSC printf("hello world\n");
	printf("you can see me!\n");
	return 0;
}

这里我们要探讨一个什么问题呢?如果替换成功,则不会执行第一个函数,如果替换失败,则我们会看到两行打印:

这究竟是为什么呢?我们可以执行:

[lwp@localhost code]$ gcc -E test.c -o test.i

把预处理后的结果保留下来为 test.i 文件,接着我们可以去用 vim 编辑器查看一下它与源文件的区别在哪,究竟是如何替换的:

通过上图我们可以发现,在预处理之后的文件中,并没有去成功通过宏替换注释掉第一个 printf 函数,由此可见,在预处理阶段,是先执行去掉注释,然后在进行宏替换,如上代码,本质是直接定义了一个空宏,我们特别不推荐这样写代码!(C语言注释风格也一样不行,感兴趣可以下去尝试下)

1.4 用宏替换多条语句

先看一段代码:

#include <stdio.h>
#define INIT_VALUE(a, b) a = 0; b = 0;
int main()
{
    int flag = 0;
    scanf("%d", &flag);
    int a = 100;
    int b = 200;
    if (flag)
        INIT_VALUE(a, b);
    else
        printf("%d, %d\n", a, b);
     return 0;
}

我想请问,这段代码有问题吗?应该如何改进呢?这段代码明显是编译不会通过的,但是可以通过执行预处理指令,发现预处理并没有出问题,那么,我们可以看一下预处理之后的结果与源文件的区别在哪:

通过预处理之后的结果我们可以看到,宏替换多了一个分号。于是有小伙伴就讨论出来如下三种解决方法:

  • 去掉宏定义的最后一个分号
  • 规范代码风格,给 if 和 else 加上大括号
  • 给宏定义要替换的部分用大括号括起来

第一种解决方法肯定是不行的,去掉最后一个分号并不能解决问题,if else 在没有大括号的情况下后面只能跟一条语句,所以第一条行不通。

第二种解决方案看似不错,但是我们有没有想过,并不是所有人都会有良好的代码风格,我们作为程序员,写出的宏应该具有健壮性,所以第二条不可取。

第三种解决方案我们看着好像靠谱,但是我们通常写完一条语句中后面都会带上分号,那可想而知会出现这种情况:{a = 0, b = 0;}; 大括号外是不能跟分号的,所以这个方法也不可取!

最好的解决方法是什么呢?使用 do while 结构:

#include <stdio.h>#define INIT_VALUE(a, b) do{a = 0; b = 0;}while(0)int main(){ int flag = 0; scanf("%d", &flag); int a = 100; int b = 200; if (flag) INIT_VALUE(a, b); else printf("%d, %d\n", a, b); return 0;}#include <stdio.h>
#define INIT_VALUE(a, b) do{a = 0; b = 0;}while(0)
int main()
{
    int flag = 0;
    scanf("%d", &flag);
    int a = 100;
    int b = 200;
    if (flag)
        INIT_VALUE(a, b);
    else
        printf("%d, %d\n", a, b);
     return 0;
}

循环会被看成一条复合语句,所以 if 不带大括号也没事(建议带上),这样我们的宏就会更健壮,也不会出错,同时你也可以在中间添加续行符,让他们的格式更清晰!同时我也有个小建议,宏定义的结尾最好都不要带分号。

结论: 当我们需要宏进行多条语句替换的时候,推荐使用 do-while-zero结构。

1.5 宏定义的使用建议

【建议1】在宏定义体的结尾省略分号。

【建议2】函数宏的调用不能省略参数。

【建议3】函数宏的定义中,每个参数都应该以小括号括起来,避免替换之后出现优先级的问题。

2、#undef 撤销宏

2.1 宏的定义位置和有效范围

第一个问题,宏定义的位置有限制要求吗?

答案:源文件的任何地方,宏都可以定义,与是否在函数内外无关。

第二个问题,宏的有效范围有多大呢?

#include <stdio.h>
void test()
{
     printf("test: %d\n", M);
 }
int main()
{
     test();
 #define M 10
     printf("main: %d\n", M);
     return 0;
 }

这段代码我们就发现编译不通过了,那么我们来进入预处理文件来对比下源文件:

答案:宏的作用范围,从定义处开始,往后都是有效的!

2.2 宏的取消

这里我们用一个例子就能很好的证明了:

 #include <stdio.h>                                                                                                                     

  #define M 10
  int main()
  {
       printf("%d\n", M);
  #undef M
       printf("undef: %d\n", M);
       return 0;
  } 

我们来查看如上代码的预处理之后的结果:

结论:undef 是取消宏的意思,可以用来限定宏的有效范围!

2.3 一道笔试题

#include <stdio.h>
int main()
{
#define X 3
#define Y X*2
#undef X
#define X 2
	int z = Y;
	printf("%d\n", z);
	return 0;
}

请问小伙伴们,这段代码打印什么?

这里我就不截图给大家看了,感兴趣的可以自行下去敲一敲,经过Linux平台和windows平台的测试,最终打印的都是 4,因为编译器都是从上到下扫描代码的,以最近的宏定义为准。

到此这篇关于C语言零基础彻底掌握预处理上篇的文章就介绍到这了,更多相关C语言预处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言零基础彻底掌握预处理下篇

    目录 1.条件编译 1.1 条件编译如何使用 1.2 用 #if 模拟 #ifdef 1.3 为何要有条件编译 2.文件包含 2.1 #include 究竟干了什么 2.2 防止头文件重复包含的条件编译是如何做到的 3.选学内容 3.1 #error 预处理 3.2 #line 预处理 3.3 #pragma 预处理 3.3.1 #pragma message 3.3.2 #pragma once 3.3.3 #pragma warning 3.3.4 #pragma pack 3.4 # 和

  • C语言程序的编译与预处理基础定义讲解

    目录 程序的翻译环境和执行环境 1.翻译环境 2.运行环境 预处理详解 预定义符号 #define #define定义宏 #define替换规则 #和## 带副作用的宏参数 宏和函数对比 命名约定 #undef 命令行定义 条件编译 文件包含 程序的翻译环境和执行环境 在ANSIC的任何一种实现中,存在两个不同的环境:翻译环境和执行环境 翻译环境:源代码被转换为可执行的机器指令. 执行环境:实际执行代码. 1.翻译环境 组成一个程序的每个源文件通过编译分别转换成目标文件(object code)

  • C语言中的程序环境与预处理详情

    目录 1.程序的翻译环境和执行环境 2.详解编译和链接 2.1程序翻译环境下的编译和链接 2.2深入编译和链接过程 2.3运行环境 3.预处理详解 3.1预定义符号 3.2#define 3.2.1#define定义的标识符 3.2.2#define定义宏 3.2.3#define替换规则 3.3.4#和## 3.2.5带副作用的宏参数 3.2.6宏和函数对比 3.3#undef 3.4命令行定义 3.5条件编译 3.6文件包含 3.6.1头文件被包含的方式 3.6.2嵌套文件包含 1.程序的翻

  • C语言深入探究程序的编译之预处理

    目录 1.程序的翻译环境和执行环境 2.详解编译与链接 2.1翻译环境 2.2编译本身也分为几个阶段 2.3运行环境 3.预处理详解 3.1预处理符号 3.2#define 3.2.1#define定义标识符 3.2.2#define定义宏 3.2.3#define的替换规则 3.2.4 宏和函数对比 3.2.5命名约定 3.3#undef 3.4条件编译 1.程序的翻译环境和执行环境 在ANSI C中任何一种实现中,存在两个不同的环境. 第一种是翻译环境,在这个环境中源代码被转换为可执行的机器

  • C语言详细分析宏定义与预处理命令的应用

    目录 宏定义与预处理命令 预处理命令 - 宏定义 定义符号常量 定义傻瓜表达式 定义代码段 预定义的宏 函数 VS 宏定义 预处理命令 - 条件式编译 示例 宏定义与预处理命令 预处理阶段:处理宏定义与预处理命令: 编译期:检查代码,分析语法.语义等,最后生成.o或.obj文件: 链接期:链接所有的.o或.obj文件,生成可执行文件. 预处理命令 - 宏定义 定义符号常量 #define PI 3.1415926 #define MAX_N 10000 定义傻瓜表达式 #define MAX(a

  • C语言预编译#define(预处理)

    目录 一.预定义符号 二.#define 定义标识符 三.#define 定义宏 四.#define 替换规则: 五.#和## 两个符号(少见) 六.宏和函数的对比 七.#undef 一.预定义符号 预定义符号是系统本身定义的: FILE 进行编译的源文件的位置 LINE 文件当前的行号 DATE 文件被编译的日期 TIME 文件被编译的时间 STDC 如果编译器遵循 ASNSI C,其值为1,否者未定义 二.#define 定义标识符 语法:#define name stuff (用stuff

  • C语言超全面define预处理指令的使用说明

    目录 前言 #define 定义宏(无参) #define 定义宏函数 宏的更多规则特性 宏的缺点 常见预处理指令 前言 C语言中源代码到可执行文件的第一阶段,也就是预处理阶段,会检查源文件中的预处理指令语句和宏定义,并对源代码进行相应的替换,预处理过程还会删除程序中的注释和多余的空白符号. 预处理指令是以#开头的代码行,#必须是该行除了空白符外的第一个字符,#后是指令关键字,在#和指令关键字之间允许存在若干个空白字符,define是宏定义命令.在C语言程序中允许用一个标识符来表示一个字符串,称

  • 一起来学习C语言的程序环境与预处理

    目录 1.程序的翻译环境和执行环境 2.gcc C语言编译器来演示编译过程 2.1编译 2.2编译: 2.3运行环境 3详解预处理 3.1预定义符号 3.2#define 3.2.1#define定义标识符 3.2.2 #define定义宏 3.2.3 #define替换规则 3.2.4 #和## 3.2.5带副作用的宏参数 3.2.6宏和函数对比 3.2.7 命名的约定 3.3 undef 3.4命令行定义 3.5 条件编译 常见的条件编译指令: 3.6文件包含 3.6.1头文件被包含的方式

  • 一起来看看C语言的预处理注意点

    目录 C 预处理器 1.取消已定义宏 2.使用#ifdef来调试 常用预定义宏 预处理器运算符 1.宏延续运算符 2.字符串常量化运算符# 3.标记粘贴运算符## 参数化的宏 总结 C 预处理器 C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤.简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理. 指令 描述 #define 定义宏 #include 包含一个源代码文件 #undef 取消已定义的宏 #ifdef 如果宏已经定义,则

  • C语言零基础彻底掌握预处理上篇

    目录 1.#define的深度认识 1.1 数值宏常量 1.2 字符串宏常量 1.3 用宏充当注释符号 1.4 用宏替换多条语句 1.5 宏定义的使用建议 2.#undef 撤销宏 2.1 宏的定义位置和有效范围 2.2 宏的取消 2.3 一道笔试题 1.#define的深度认识 1.1 数值宏常量 宏定义数值常量相信大家都不陌生,相信很多小伙伴用过,这里我们就简单的提一下,我们前面也讲过,#define 本质上是替换,它可以出现在代码的任何地方,也可以把任何东西都定义成宏,编译器会在预编译的时

  • C语言零基础讲解指针和数组

    目录 一.指针和数组分析-上 1.数组的本质 2.指针的运算 3.指针的比较 4.小结 二.指针与数组分析-下 1.数组的访问方式 2.下标形式 VS 指针形式 3.a 和 &a 的区别 4.数组参数 5.小结 一.指针和数组分析-上 1.数组的本质 数组是一段连续的内存空间 数组的空间大小为 sizeof(array_type) * array_size 数组名可看做指向数组第一个元素的常量指针 下面看一段代码: #include <stdio.h> int main() { int

  • C语言零基础入门(1)

    目录 1.C语言简介 1.1C语言发展史 1.2C语言的特点 1.3算法及其表示 1.4常用算法介绍 总结 1. C语言简介 1.1 C语言发展史 C语言是一种广泛使用的面向过程的计算机程序设计语言,既适合于系统程序设计,又适合于应用程序设计.C语言的发展历程大致如图1-1所示: 图1-1 C语言的发展历程 1.2 C语言的特点 C语言是一种通用的程序设计语言,语言本身简洁.灵活.表达能力强,被广泛用于系统软件和应用软件的开发,并且具有良好的可移植性. C语言的特点可概括如下: (1)简洁.紧凑

  • C语言零基础入门(2)

    目录 1.数组 1.1一维数组 1.1.1一维数组的定义 1.1.2一维数组的初始化 1.1.3一维数组的引用 1.2二维数组及多维数组 1.2.1二维数组的定义 1.2.2二维数组的初始化 1.2.3二维数组的引用 总结 1. 数组 数组是一组相同类型变量的有序集合,用于存放一组相同类型的数据.这一组变量用数组名和从0开始的下标标识,使用内存中一块连续的存储空间.依据数组中元素下标的个数分为一维数组.二维数组和多维数组. 1.1 一维数组 1.1.1 一维数组的定义 一维数组定义的一般形式为:

  • 零基础易语言入门教程(五)之逻辑型数据类型

    在上篇文章给大家介绍了零基础易语言入门教程(四)之数据类型,上篇针对数值到文本类型知识,今天给大家介绍下逻辑型数据. 具体方法和步骤如下所示: 1.逻辑型数据非真即假: 首先申请一个局部变量(A)类型为:逻辑型,编写代码为:A=1>2,那么输出的结果应为假,因等于1是赋值与1,然后代码中写道1大于2,所以这是假的,见下图所示: 2.关系运算符: 在上图大家需注意的是,A后面的等于号是赋值符号,而后面的≥,≠,<一些符号则是关系运算符. 关系运算符不是非要设置变量给其赋值才可以使用的,同样他可以

  • 零基础易语言入门教程(六)之逻辑型命令

    逻辑型命令,就是非真即假的. 具体方法和步骤如下所示: 1.如果(): 属于逻辑型,不是真就是假,这种时间我们基本在编写程序时,会有两个选择方向,见下图所示: 2.如上图,如果命令属于逻辑型数据,且有两条输出方向,当我们在如果命令里填写的为真,那么我们的系统将会显示输出真的一个,反之则为假. 3.如果()命令在我们编写程序时属于常用命令,他在运行时我们需要给他一个条件,然后才能输出内容,有了条件我们在运行时给他一个输出方向即可, 以上所述是小编给大家介绍的零基础易语言入门教程(六)之逻辑型命令的

  • 零基础易语言入门教程(四)之数据类型

    我们一起了解下易语言的数据类型,跟我们现实生活是一样的,分为文本型和数值型,即是我们所说的文科生和理科生的区别. 参考文章:详解易语言中的数据类型 方法和步骤如下所示: 1.数值型(到数值命令): 使用该命令可将文本型等一类数据更改为数值型:我们来输入一行代码看看其作用: 2.到文本()命令: 我们先输入一行代码试试,见下图 3.小结: 每一行代码前后的数据类型必须转换为同一种,方可进行相连,相加,"+"在数据为文本型时是连接作用,数值型的跟数学里的符号一样. 以上所述是小编给大家介绍

  • 零基础易语言入门教程(三)之了解控制台程序

    易语言简介: 易语言是一门以中文作为程序代码编程语言.以"易"著称.创始人为吴涛.早期版本的名字为E语言.易语言最早的版本的发布可追溯至2000年9月11日.创造易语言的初衷是进行用中文来编写程序的实践.从2000年至今,易语言已经发展到一定的规模,功能上.用户数量上都十分可观. 易语言和其它编程语言一样都有后台程序,它也不一定必须是窗口程序的了,下面小编带大家了解易语言的控制台程序. 方法和步骤如下所示: 1.延时命令: 首先学习一个第一个命令,该命令可将其脚本界面延时.1000毫秒

  • 零基础易语言入门教程(二)之编程思路

    易语言简介: 易语言是一门以中文作为程序代码编程语言.以"易"著称.创始人为吴涛.早期版本的名字为E语言.易语言最早的版本的发布可追溯至2000年9月11日.创造易语言的初衷是进行用中文来编写程序的实践.从2000年至今,易语言已经发展到一定的规模,功能上.用户数量上都十分可观. 上一篇跟大家讲了零基础易语言入门教程(一)编写第一个程序,然后接下来大家应该自己把支持库和易语言组件里面的控件全部认真的看一下,下面我直接跟大家分享下易语言简单编程思路. 方法和步骤如下所示: 1.易语言程序

随机推荐