C语言学习笔记—宏

0、简述

        先了解一下“预处理”,预处理是C和C++在编译之前需要完成的工作,其作用是:把源代码提交给编译器之前对源代码进行设计和修改。预处理的本质目的是删除预处理指令,并用生成的等效C代码替换它们,从而得到提交给编译器的最终源代码。

        在头文件和源文件中,C预处理指令都是以 # 号开头的代码行。这些行只对C预处理器有意义,对C编译器是没有意义的。因为最后提交给C编译器的源代码都已经完成了预处理工作并替换成了等效的C代码。

        由此可见,预处理器就是在把源代码提交给编译器之前对源代码进行设计和修改,而C语言中有各种各样的预处理指令,其中一些非常的重要,它可以让代码变得更加“优雅”,比如宏定义和条件编译等指令。

一、宏的常用应用场景

        1、定义一个常量

        2、宏函数

        拓展:宏函数是在预处理时被展开,故无参数类型检查,减少函数调用开销,由于是文本直接替换会使代码膨胀增加可执行文件的大小。C++中的内联函数,其在编译阶段替换,故有参数类型检查。

        3、循环展开

        4、头文件保护

        5、代码生成

        6、条件编译

二、宏的定义和解除

        1、宏的定义使用 #define 指令

        2、宏的解除使用 #undef  指令

三、操作符 # 和 ##

        

        #:该操作符将 参数 转换为 由双引号括起来的字符串形式

        ##:该操作符将 参数 宏定义中的其他元素连接起来,通常形成 变量名。

        拓展:当宏定义比较长的时候,可使用 “ \ ” 将其分成多行,“ \ ” 让预处理器知道该行未结束,其余内容在下一行。注意 “ \ ” 不会被换行符替换,它指示下一行是当前行的延续。

四、宏的“威力”

        在某种角度看来,用一定的技巧处理危险和微妙的东西,这可以让开发变得更加优雅。

        例1:定义一个宏

       

        拓展:const修饰的变量严格应称为“只读变量”,其修饰的整型值不能定义数组大小,而宏定义的整型值可以用来定义数组的大小。

        例2:定义一个能接收参数的 类似函数 的宏

        

        实际参数(x和y)会替换宏中相应的形式参数(a和b),经过预处理后被等效替换成了 (x+y)。类函数宏中可以接受输入参数,故在一些简单的逻辑上面可以使用类函数的宏。

        拓展:在大多现代编译器中,我们可以在编译之前查看预处理结果。例如在使用gcc时,可以使用-E完成源代码预处理,然后得到预处理之后的源代码结果。

        这里有一个重要定义:翻译单元 编译单元是预处理后准备传递给编译器的C代码。在一个翻译单元中,所有的指令都被所包含的文件内容或宏展开替换,并生成一段长的、扁平的C代码。

        例3:使用宏生成循环

        

        在这里,我们使用了一组不同的、看起来不像C的指令集,然后经过预处理,得到了一个功能完整正确的C代码,这是宏的一个重要应用(定义一种新的特定领域语言DSL)并用它编写代码。

        例4:可变参数的宏

        它可以接受可变数量的实参。同一个可变参数宏有时接受2个实参,有时4个实参,有时8个实参,当不确定同一宏在不同用法下的实参数量时,可变参数宏非常方便。

        

        变参宏用 ... 表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开,可变参数宏使用C99标准新增的 __VA_ARGS__ 预定义标识符来表示前面的变参列表。

        拓展:在 __VA_ARGS__ 前面加上宏连接 ## ,这样的好处是,当变参列表非空时,##的作用是连接 fmt, 和 变参列表,各个参数之间用逗号隔开,宏可以正常使用。当变参列表位空时,## 还有一个特殊好处是它将固定参数 fmt 后面的逗号删除掉,这样宏就也可以用了。

        当定义一个变参宏时,除了使用预定义标识符 __VA_ARGS__ 表示变参列表外,还可以使用args... 来表示一个变参列表。

        

       注意:为了避免变参列表为空时的语法错误,我们也需要添加一个连接符##。

五、宏的优缺点

        软件设计试图将相似的算法和概念打包到几个可管理和可重用的模块中,但宏试图将所有的内容线性化和扁平化。故,在软件设计中使用宏作为一些逻辑构建块时,在最终的翻译单元中,有关宏的信息可能在预处理之后丢失,基于此原因,我们在使用宏时应最好遵循以下经验法则:

                如果宏可以写成C函数,那么应该将宏改写为C函数!

                (宏应该被等效的C函数替换,这么说是基于软件设计的需要,在某些情况下则不必如此。在提高性能是关键需求的情况下,有必要有一组线性指令来提高性能)

        调试角度来看:

                开发人员日常工作的一部分就是使用 编译错误 来定位 语法错误。也许还使用 日志 和 编译警告来检测错误并修复。在这个时候,有些老版本C编译器对宏一无所知,开发人员看到的带宏实际C代码和C编译器看到的预处理后的代码,是两个不同的世界。因此开发人员很难理解编译器报告的信息。但在如今,著名的C语言编译器gcc更加了解预处理阶段,它们从开发人员视角来保存、使用和报告编译信息。在现代编译器的帮助下,这个问题不再那么严重。

        关于二进制文件大小和程序性能之间权衡的问题:

                软件设计试图让每个软件组件在一个巨大的层次结构中,处于合适的位置,而不是将它们按线性顺序排列,尽管在大多数情况下线性顺序排列对软件性能的影响很小,但在本质上是不利的。

                在一个大的二进制文件和多个小的二进制文件之间权衡,它们都能提供相同的功能,但前者有着更好的性能。当需要性能时,可以牺牲设计,把东西放到线性结果中(例如避免使用循环,而使用循环展开)。

                性能问题起始于为设计阶段定义的问题选择合适的算法,下一步通常称为优化 或 性能调优。在这个阶段,获得性能等同于让CPU以线性顺序的方式计算,而不是强迫它在代码的不同部分之间跳转。这个理念可能会与设计理念冲突。因此,应该针对每个问题分别进行处理和平衡。

                进一步了解循环展开,这种主要应用在嵌入式开发,特别是在处理能力有限的环境中,其技术是删除循环并使其线性化,以提高性能并避免运行迭代时的循环开销。

        综上,如果深入了解C宏的预处理结果带来的影响,就能够利用好它们的优点。

六、条件编译

        条件编译是C语言的另一个独特特性,它能让你根据不同的条件得到预处理后的不同翻译单元。

        有不同的预处理指令可用于条件编译,如下所示:

                1、#ifdef

                2、#ifndef

                3、#else

                4、#elif

                5、#endif

        可以在编译命令中使用 “-D” 选项来定义宏,将宏传递给编译命令:

                gcc -DCONDITION -E main.c

        这是一个很好的特性,因为它可以允许在源文件之外定义宏,当只有一个源代码,但要为不同的系统编译时(Linux 或 MacOS),这一特性特别有用,因为它们有不同的默认宏定义和库。

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值