C宏有什么用?


73

我已经写了一些C语言,而且我读得足够好,可以大致了解它的功能,但是每次遇到宏时,它都会使我完全失望。我最终不得不记住宏是什么,并在阅读时将其替换在脑海中。我遇到的那些直观易懂的东西总是像小的迷你函数,所以我一直想知道为什么它们不只是函数。

我可以理解需要在预处理器中为调试或跨平台构建定义不同的构建类型,但是定义任意替换的功能似乎仅对使本来就很难理解的语言有用。

为什么为C引入了如此复杂的预处理器?还有没有人有使用它的示例,这会让我理解为什么#debug样式的条件编译时,为什么它似乎仍然用于简单的目的以外的用途?

编辑:

阅读了许多答案后,我仍然听不懂。最常见的答案是内联代码。如果inline关键字不这样做,则它有充分的理由不这样做,或者实现需要修复。我不明白为什么需要一种完全不同的机制来表示“真正内联此代码”(除了在内联之前编写的代码)。我也不理解提到的想法,即“如果它太愚蠢而无法放入函数中”。当然,任何需要输入并产生输出的代码都最好放在函数中。我想我可能不明白,因为我不习惯于编写C的微观优化,但是预处理器感觉就像是解决一些简单问题的复杂解决方案。


5
C预处理器并不复杂,它非常非常简单。
亚历克斯

17
C预处理器非常简单,但是它使您的代码非常非常复杂。;)
weberc2 2013年


2
我偶然发现了这个gcc.gnu.org/onlinedocs/cpp/Macros.html#Macros,它有助于我了解c语言中的宏。也许这会对其他人有所帮助
pyk 2015年

@Ayxan请不要inline code用于强调(使用粗体斜体)。话虽如此,像C这样的语言名称根本不需要强调。
user694733 '19

Answers:


55

我最终不得不记住宏是什么,并在阅读时将其替换在脑海中。

这似乎不能很好地反映宏的命名。我认为如果它是一个log_function_entry()宏,则不必仿真预处理器。

我遇到的那些直观易懂的东西总是像小的迷你函数,所以我一直想知道为什么它们不只是函数。

通常,它们应该是,除非它们需要对通用参数进行操作。

可以与任何<运算符一起使用。

宏不仅具有功能,还使您可以使用源文件中的符号执行操作。这意味着您可以创建一个新的变量名,或引用宏所在的源文件和行号。

在C99中,宏还允许您调用可变函数,例如 printf

其中的格式类似于printf。如果警卫队为真,它将输出消息以及打印该消息的文件和行号。如果是函数调用,它将不知道您从中调用过的文件和行,而使用a则vaprintf需要更多的工作。


3
谢谢。这是有帮助的。我认为这对他们来说是合理的用法。它提供了易于使用和清晰的调试功能,这些功能无法轻松替换。
杰克·瑞安

19

通过比较C使用宏的几种方式以及如何在中实现它们,该摘录大致总结了我对此的看法D

从DigitalMars.com复制

早在C发明时,编译器技术就是原始的。在前端安装文本宏预处理器是添加许多强大功能的简单明了的方法。程序的日益庞大和复杂性表明,这些功能存在许多固有的问题。D没有预处理器;但D提供了更具扩展性的方法来解决相同的问题。

巨集

预处理器宏为NET添加了强大的功能和灵活性C。但是它们有一个缺点:

  • 宏没有范围的概念。从定义到源末尾,它们都是有效的。他们对.h文件,嵌套代码等进行了广泛的修改。当#include对成千上万的宏定义行进行处理时,避免无意中的宏扩展成为问题。
  • 调试器不知道宏。仅了解宏扩展而不了解宏本身的调试器会破坏尝试用符号数据调试程序的意图。
  • 宏使得不可能对源代码进行标记化,因为先前的宏更改可以任意地重做标记。
  • 宏的纯文本基础导致任意使用和不一致的用法,使使用宏的代码易于出错。(通过模板引入了一些解决此问题的尝试C++。)
  • 宏仍被用来弥补语言表达能力的不足,例如头文件周围的“包装器”。

以下是宏的常用用法以及D中相应功能的枚举:

  1. 定义文字常量:

    • C预处理方法

    • D

  2. 创建值或标志的列表:

    • C预处理方法

    • D

  3. 设置函数调用约定:

    • C预处理方法

    • D

      可以在块中指定调用约定,因此无需为每个函数进行更改:

  4. 简单的通用编程:

    • C预处理方法

      根据文本替换选择要使用的功能:

    • D

      D 启用作为其他符号别名的符号声明:

DigitalMars网站上还有更多示例。


17
大多数人会认为,您编写的D方式是不必要的。
OTZ

在我看来,令人讨厌的预处理器指令是不必要的,而且我(有点)是C程序员。
weberc2 2012年

1
D是否允许人们以一种方式声明函数,如果其参数为编译时常量,则编译器将计算结果为编译时常量?显然,在这样的函数中可以做的事情是有限的,但是将foo=bit_reverse(0x12345678);as为foo=0x1E6A2C48,但foo=bit_reverse(bar);生成一个函数调用将很有用。出于这种目的,可以将C宏与源自gcc的扩展名一起使用,但这有点令人讨厌。
2013年

1
@supercat我认为如果函数声明为,它可能会不断折叠函数pure
布拉德·吉尔伯特

1
@supercat实际上,最近几年我实际上并没有真正关注d的开发。
布拉德·吉尔伯特

16

它们是C语言之上的一种编程语言(一种简单的语言),因此对于在编译时进行元编程非常有用……换句话说,您可以编写宏代码,从而以更少的行和时间生成C代码。直接用C编写

它们对于编写“多态”或“重载”的“函数式”表达式也非常有用。例如,最大宏定义为:

对任何数字类型都有用;在C语言中,您无法编写:

即使您想要,因为您不能重载函数。

更不用说条件编译和包含文件(它们也是宏语言的一部分)...


@AndrewC我以为这有点题外话了...然后删除!;)
fortran 2013年

12

宏允许某人在编译期间修改程序行为。考虑一下:

  • C常数允许在开发时修正程序行为
  • C变量允许在执行时修改程序行为
  • C宏允许在编译时修改程序行为

在编译时,这意味着未使用的代码甚至都不会进入二进制文件,并且只要将其与宏预处理器集成在一起,构建过程就可以修改值。示例:make ARCH = arm(假定转发宏定义为cc -DARCH = arm)

简单示例:(从glibclimits.h中,定义long的最大值)

如果我们要编译32位或64位,则在编译时进行验证(使用#define __WORDSIZE)。使用multilib工具链时,使用参数-m32和-m64可能会自动更改位大小。

(要求POSIX版本)

在编译期间请求POSIX 2008支持。标准库可能支持许多(不兼容)标准,但是使用此定义,它将提供正确的函数原型(例如:getline(),no gets()等)。如果该库不支持该标准,则可能在编译时发出#error,而不是在执行期间崩溃。

(硬编码路径)

在编译期间定义一个硬代码目录。例如,可以使用-DLIBRARY_PATH = / home / user / lib进行更改。如果那是const char *,那么在编译过程中如何配置它?

(pthread.h,编译时的复杂定义)

可能会声明原本不会被简化的大块文本(总是在编译时)。使用函数或常量(在编译时)无法执行此操作。

为了避免真正复杂化并避免建议不良的编码风格,我不会提供在不同,不兼容的操作系统中编译的代码示例。为此,请使用交叉构建系统,但是应该清楚的是,预处理器允许在没有构建系统帮助的情况下进行操作,而不会由于缺少接口而中断编译。

最后,考虑条件编译在嵌入式系统上的重要性,因为嵌入式系统上的处理器速度和内存有限,并且系统非常异构。

现在,如果您提出要求,是否可以将所有宏常量定义和函数调用替换为正确的定义?答案是肯定的,但是不会仅仅消除编译期间更改程序行为的需要。仍然需要预处理器。


11

请记住,宏(和预处理器)来自C的早期。它们曾经是执行内联“函数”的唯一方法(因为inline是一个非常新的关键字),但它们仍然是强制内联的唯一方法。

另外,宏是您完成诸如在编译时将文件和行插入字符串常量之类的技巧的唯一方法。

如今,通过更新的机制可以更好地处理宏曾经是唯一的方法。但是他们仍然时有发生。


8

除了内联以提高效率和条件编译外,宏还可以用于提高低级C代码的抽象级别。C并没有真正使您与内存和资源管理的本质细节以及数据的精确布局相隔离,它支持非常有限形式的信息隐藏和其他用于管理大型系统的机制。使用宏,您不再局限于仅使用C语言的基本构造:您可以定义自己的数据结构和编码构造(包括类和模板!),同时仍然名义上编写C!

预处理程序宏实际上提供了在编译时执行的图灵完备语言。在C ++方面,这是令人印象深刻(且稍有些令人恐惧)的示例之一:Boost预处理程序库使用C99 / C ++ 98预处理程序来构建(相对)安全的编程结构,然后将其扩展为任何基础声明和代码。您输入的是C还是C ++。

在实践中,当您不愿意使用更安全的语言来使用高级构造时,建议将预处理器编程作为最后的选择。但是有时候知道自己的后背靠在墙上并且黄鼠狼正在关闭...


1
为什么使用预处理器定义数据结构比使用结构更好?当然,开始使用预处理器定义类和模板的时间就是您可能考虑使用C ++或支持这些结构的另一种语言的时间。
杰克·瑞安

取决于您要拥有的自由度:宏允许您一致地生成整个范围的数据结构。您说得很对:正如我所写,这是不得已的做法。但有时,您必须使用所获得的工具。
Pontus Gagge,2009年

1
您实际上可以使用结构和其他结构,然后在预处理器中添加一些句法糖
Andrea

2
这是一篇有关如何使用C宏使数据结构用法更具可读性和可管理性的文章。-使用C宏的多态数据结构-coredump
coredump

7

计算机愚蠢

我已经在许多用于UNIX的免费软件游戏程序中看到以下代码摘录:

/ *
*位值。
* /
#define BIT_0 1
#define BIT_1 2
#define BIT_2 4
#define BIT_3 8
#define BIT_4 16
#define BIT_5 32
#define BIT_6 64
#define BIT_7 128
#define BIT_8 256
#define BIT_9 512
#define BIT_10 1024
#define BIT_11 2048
#定义BIT_12 4096
#定义BIT_13 8192
#定义BIT_14 16384
#定义BIT_15 32768
#定义BIT_16 65536
#定义BIT_17 131072
#定义BIT_18 262144
#定义BIT_19 524288
#定义BIT_20 1048576
#定义BIT_21 2097152
#define BIT_22 4194304
#define BIT_23 8388608
#define BIT_24 16777216
#define BIT_25 33554432
#define BIT_26 67108864
#define BIT_27 134217728
#define BIT_28 268435456
#define BIT_29 536870912
#define BIT_30 1073741824
#define

一个更简单的方法是:

#define BIT_0 0x00000001
#define BIT_1 0x00000002
#define BIT_2 0x00000004
#define BIT_3 0x00000008
#define BIT_4 0x00000010
...
#define BIT_28 0x10000000
#define BIT_29 0x20000000
#define BIT_30 0x40000000
#define BIT_31 0x80000000

还有一种更简单的方法是让编译器进行计算:

#define BIT_0(1)
#define BIT_1(1 << 1)
#define BIT_2(1 << 2)
#define BIT_3(1 << 3)
#define BIT_4(1 << 4)
...
#define BIT_28(1 << 28)
#define BIT_29(1 << 29)
#define BIT_30(1 << 30)
#define BIT_31(1 << 31)

但是为什么要麻烦定义32个常量呢?C语言还具有参数化的宏。您真正需要的是:

#定义BIT(x)(1 <<(x))

无论如何,我想知道编写原始代码的人是使用计算器还是只是将其全部计算在纸上。

那只是宏的一种可能用途。


6

我将补充说的话。

因为宏可以处理文本替换,所以它们使您可以做一些非常有用的事情,而这些是使用函数无法实现的。

在某些情况下,宏可能会真正有用:

这是一个非常流行且经常使用的宏。例如,当您需要遍历数组时,这非常方便。

在这里,是否其他程序员a在decleration中再添加五个元素并不重要。该for-loop将始终通过所有元素进行迭代。

C库用于比较内存和字符串的函数很难使用。

你写:

要么

检查是否str指向"Hello, world"。我个人认为这两种解决方案看起来都非常难看和令人困惑(尤其是!strcmp(...))。

这是一些人(包括I)在需要使用strcmp/比较字符串或内存时使用的两个简洁的宏memcmp

现在,您可以编写如下代码:

这是很多清晰的意图!

这是宏用于功能无法完成的事情的情况。宏不应该用来代替函数,但是它们还有其他很好的用途。


很好的例子!

5

宏真正发光的一种情况是使用它们进行代码生成时。

我曾经在一个旧的C ++系统上工作,该系统使用插件系统以他自己的方式将参数传递给插件(使用自定义的类似地图的结构)。一些简单的宏被用来处理这个奇怪的问题,并允许我们在插件中使用带有正常参数的真实C ++类和函数而没有太多问题。宏生成的所有粘合代码。


4

给定问题中的注释,您可能不完全理解调用函数会带来相当大的开销。参数和键寄存器可能必须在进库时复制到堆栈中,而在出库时将其展开。对于较旧的Intel芯片尤其如此。宏允许程序员(几乎)保留函数的抽象,但避免了函数调用的昂贵开销。内联关键字是建议性的,但是编译器可能并不总是正确。“ C”的荣耀和危险在于,您通常可以按自己的意愿弯曲编译器。

在您的日常工作中,这种微优化(避免函数调用)在日常应用程序编程中通常会更糟,然后变得毫无用处,但是如果您编写的是由操作系统内核调用的对时间要求严格的函数,则它可以产生巨大的变化。


1
我可以理解,内联可以作为优化的用处,但是我不明白为什么需要预处理器来执行此操作。为什么inline关键字不总是有效?使用preprocesor进行“真正的内联”似乎是一个常见的技巧,最好通过更改编译器来解决。
杰克·瑞安

1
内联代码是有代价的。编译器有一些经验法则可以平衡收益(更快的代码)和成本(简单代码)。如果您的经验法则碰巧是错误的,则宏可让您将编译器推开,以获取所需的结果。
查尔斯E.格兰特

1
存在向后兼容性和历史意外的问题。人们不希望修改数十亿行的“ C”代码,因此此时对“ C”语言的更改需要很小,并且应尽可能向后兼容。
查尔斯E.格兰特

4

与常规函数不同,您可以在宏中控制流(如果,为,...,……)。这是一个例子:


3

这对于内联代码和避免函数调用开销很有用。如果要稍后更改行为而无需编辑很多位置,也可以使用它。它对复杂的事情没有用,但是对于要内联的简单代码行来说,也不错。



2

宏使您摆脱粘贴粘贴的片段,而这些片段是您无法以其他任何方式消除的。

例如(实际代码,VS 2010编译器的语法):

在这里,您可以将同名的字段值传递给脚本引擎。这是复制粘贴的吗?是。DisplayName用作脚本的字符串和编译器的字段名称。那不好吗?是。如果您重构代码并重命名LocalNameRelativeFolderName(就像我所做的那样),而忘记对字符串做同样的事情(就像我所做的那样),那么脚本将以您不期望的方式工作(实际上,在我的示例中,它取决于您是否忘记了在单独的脚本文件中重命名该字段,但是如果该脚本用于序列化,那将是100%的错误)。

如果为此使用宏,则将没有空间容纳该错误:

不幸的是,这为其他类型的错误打开了一扇门。您可以输入宏,但不会出现损坏的代码,因为编译器不会显示所有预处理后的样子,您可能会打错字。其他人可能使用相同的名称(这就是为什么我使用尽快“释放”宏的原因#undef)。因此,请明智地使用它。如果您看到另一种摆脱复制粘贴的代码的方法(例如函数),请使用该方法。如果您发现使用宏删除带有复制粘贴的代码是不值得的,请保留复制粘贴的代码。


1

显而易见的原因之一是,通过使用宏,代码将在编译时进行扩展,从而获得了伪函数调用,而没有调用开销。

否则,您也可以将其用于符号常量,这样就不必在多个地方编辑相同的值来更改一件小事情。


0

宏..用于当您的&#(* $&编译器拒绝内联某些东西时。

那应该是励志海报,不是吗?

认真地说,是Google预处理程序滥用(您可能会在#1结果中看到类似的SO问题)。如果编写的宏超出了assert()的功能,通常会尝试查看编译器是否实际上内联了类似的函数。

其他人会反对使用#if进行条件编译..他们希望您:

而不是

..用于调试目的,因为您可以在调试器中看到if()但看不到#if。然后我们进入#ifdef vs #if。

如果其代码行数少于10行,请尝试内联它。如果无法内联,请尝试对其进行优化。如果它太愚蠢而不能用作函数,请创建一个宏。


0

虽然我不是宏的忠实拥护者,并且不再倾向于编写太多C,但根据我当前的任务,类似这样的事情(显然可能会有一些副作用)很方便:

现在,我已经几年没有写过类似的东西了,但是那样的“函数”遍及了我职业生涯早期维护的代码。我想扩展可以认为很方便。


2
int ohNo = MIN(functionWithSideEffect(x), y++);
Dour High Arch

0

关于宏之类的功能,例如,我没有看到任何人这么提:

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

通常,由于许多原因,建议避免使用不必要的宏,因为主要是可读性。所以:

什么时候应该在功能上使用这些?

几乎永远不会,因为还有一个更具可读性的替代方法inline,请参见https://www.greenend.org.uk/rjk/tech/inline.htmlhttp://www.cplusplus.com/articles/2LywvCM9/(第二篇链接是一个C ++页面,但据我所知这一点适用于c编译器。

现在,稍有不同是宏是由预处理器处理的,而内联是由编译器处理的,但是现在没有实际的区别。

什么时候适合使用这些?

对于小功能(最多两个或三个衬管)。目的是在程序运行时获得一些优势,因为宏(和内联函数)之类的函数是在预处理(或内联的情况下进行编译)期间进行的代码替换,而不是存在于内存中的实际函数,因此没有函数调用的开销(链接页面中有更多详细信息)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.