什么是宏?宏和函数之间的区别?


16

我不太了解宏观概念。什么是宏?我不明白它与功能有何不同?函数和宏均包含代码块。那么宏和函数有何不同?


15
您是在指任何特定的编程语言吗?
mkrieger1

15
你需要更加详细; 指的是三个非常不同的概念,大致为C样式的文本/预处理器宏,Lisp宏和应用程序宏。
克莱里斯

Answers:


7

注意

在观察这个答案的两极分化的投票方式之后,我想补充以下澄清。

答案的撰写并未牢记技术准确性和广泛的概括性。这是一个谦虚的尝试,用简单的语言向编程新手解释宏和函数之间的差异,而又不试图做到完整或准确(实际上,它远非准确)。起草答案时,我想到的是C编程语言,但很抱歉,我没有明确提及它并引起任何(潜在的)混乱。

我真的很喜欢JörgW Mittag答案。阅读它是很有见地的(我所知不多),并且在它发布后不久就对其进行了投票。我刚开始软件工程堆栈交换,到目前为止的经验和讨论都非常有见地。

我将在这里留下这个答案,因为它可能对其他软件开发新手有所帮助,他们试图理解一个概念而不会陷入技术上的准确性。


宏和函数均表示代码的独立单元。它们都是辅助程序模块化设计的工具。从编写源代码的程序员的角度来看,它们看起来非常相似。但是,它们在程序执行生命周期中的处理方式不同。

宏只定义一次,并在程序中的许多地方使用。宏在预处理阶段内联扩展。因此,一旦源代码被编译,它在技术上就不会保留为单独的实体。与其他语句一样,宏定义中的语句也成为程序指令的一部分。

编写宏的动机是使程序员更容易编写和管理源代码。宏通常用于较简单的任务,在这些任务中,编写完整的功能可能会降低性能/运行时间。宏胜于功能的情况示例包括:

  • 使用常数值(例如数学或科学值)或某些程序特定的参数。

  • 打印日志消息或处理断言。

  • 执行简单的计算或条件检查。

使用宏时,很容易在一个地方进行更改/更正,该宏可以在程序中使用宏的任何地方立即获得。要使更改生效,需要对程序进行简单的重新编译。

另一方面,功能代码被编译为程序中的单独单元,并且仅在需要时才在程序执行期间加载到内存中。功能代码保留了程序其余部分的独立身份。如果多次调用该函数,则已加载的代码将被重用。当正在运行的程序中遇到函数调用时,运行时子系统会将控制权传递给该函数,并保留正在运行的程序的上下文(返回指令地址)。

但是,调用函数时会遇到轻微的性能损失(上下文切换,保留主程序指令的返回地址,传递参数和处理返回值等)。因此,仅对于复杂的代码块(针对处理更简单情况的宏)才需要使用函数。

凭经验,程序员会做出明智的决定,即一段代码是否适合作为宏或整个程序体系结构中的函数。


9
函数内联呢?
内森·库珀

1
在C语言&co中,宏也可以是函数调用。因此,在两者之间进行区分没有多大意义。
草帽鸡

5
C风格的宏只是自包含的-它们不引入词法作用域,并且在替换时可以无限制地访问作用域中的本地名称。
没用的

@Sombrero Chicken:也许您可以举一个例子?您可以具有包含CONTAIN函数调用的宏,但是(AFAIK)该宏不是函数调用。
jamesqf

3
关于此答案的很多事情都有误导性。您提到的示例绝不是“首选”宏的任何情况-事实恰恰相反。除非有充分的理由,否则不应将宏用于这些用途。通常,性能根本不是使用宏的好理由。由于其他注释中提到的原因,以这种方式证明使用宏的合理性可能会导致易于出错的代码,并带来各种意外后果,尤其是在较大的代码库中。
Ben Cottrell

65

不幸的是,在编程中术语“宏”有多种不同的用法。

在Lisp语言族以及受其启发的语言中,以及许多现代功能或受功能启发的语言(例如Scala和Haskell)以及某些命令性语言(例如Boo)中,宏是一段在编译时运行的代码(或至少在运行时之前没有编译器的实现),并且可以在编译期间将抽象语法树(或特定语言中的任何等效内容,例如Lisp中的S-Expressions)转换为其他内容。例如,在许多Scheme实现中,for是一个宏,它扩展为对主体的多个调用。在静态类型的语言中,宏通常是类型安全的,即,它们不能生成类型不正确的代码。

在C语言家族中,宏更像是文本替换。这也意味着他们可能会产生格式错误的代码,甚至语法上不合法的代码。

在宏汇编器中,“宏”指的是“虚拟指令”,即CPU本身不支持但有用的指令,因此汇编器允许您使用这些指令并将其扩展为CPU可以理解的多个指令。

在应用程序脚本编制中,“宏”是指用户可以“记录”和“回放”的一系列动作。

从某种意义上说,所有这些都是可执行代码,这意味着它们在某种意义上可以看作是函数。但是,例如,对于Lisp宏,其输入和输出是程序片段。对于C,它们的输入和输出是令牌。前三个还具有非常重要的区别,它们是在编译时执行的。实际上,顾名思义,C预处理程序宏实际上是在代码甚至到达编译器之前执行


1
“在静态类型的语言中,宏通常是类型安全的,即它们无法生成类型不正确的代码” –真的吗?您指的是什么语言?AFAICS,这通常只能保证使用依赖类型的语言。在Haskell中,TH宏仅在语法上是安全的,但类型检查器此后运行。(因此,在类型方面,它们提供的保证甚至少于C ++模板)。
6

1
除了应用程序脚本编写,我认为您提供的三个示例没有什么不同。所有人都在程序中执行某种替换,而不是在执行时跳转到共享代码中。
IMSoP

也许您可以使用诸如M4之类的宏语言的一般概念来扩展对C宏的提及,因为对于C而言,基本上是该规范定义了CPP辅助宏语言并使其与C标准化使用
。– JoL

1
一般的主题似乎是宏生成的代码被解释为较大程序源代码的一部分,而不是在程序运行时执行。
jpmc26

1
@ jpmc26我想说的一般主题是替换,即您做一件小事,然后扩展为一件大事。宏语言M4并非专门用于代码。您可以使用它生成任何文本。还有键盘宏,就像上面提到的答案一样。按下1或2个按键,它们会扩展为更多的按键。vim宏也是如此。在单个键下保存大量常规模式命令序列,然后通过运行执行该序列的常规模式命令来调用该序列。
JoL

2

在C语言家族中,宏定义(预处理器命令)指定参数化的代码模板,该模板在宏调用时替换而无需在定义中进行编译。这意味着所有自由变量都应在宏调用的上下文中进行绑定。i++通过多次使用参数,可以重复使用具有副作用的参数参数。1 + 2某些参数的参数的文本替换x发生在编译之前,并且可能导致意外的行为x * 37io 9)。宏定义的宏主体中的错误将仅在宏调用时进行编译时显示。

函数定义指定代码与结合到功能体的上下文中自由变量; 不是函数调用

但是,看起来像是负数的宏提供了对调用,行号和源文件以及作为string的参数的访问。


2

用稍微抽象些的术语来说,宏是语法,而函数是数据。一个函数(摘要)封装了对数据的一些转换。它以其参数作为评估数据,对其进行一些操作,并返回一个结果,该结果也只是数据。

相反,宏采用一些未评估的语法并对其进行操作。对于类似C的语言,语法位于令牌级别。对于具有类似LISP宏的语言,它们获得的语法表示为AST。宏必须返回新的语法块。


0

一个宏观一般是指东西是在地方扩张,编译或在目标语言单个指令前处理过程中更换的宏“通话”。在运行时,通常不会指示宏的开始和结束位置。

这与子例程不同,子例程是可重用的代码段,它们分别位于内存中,在运行时控制传递给该子例程。大多数编程语言中的“函数”,“过程”和“方法”都属于此类。

正如JörgW Mittag的答案所讨论的,确切的细节因语言而异:在某些语言(例如C)中,宏在源代码中执行文本替换;在某些语言中,宏执行文本替换。在某些语言(例如Lisp)中,它执行诸如抽象语法树之类的中间形式的操作。也有一些灰色区域:有些语言对“内联函数”有表示法,它们像函数一样定义,但像宏一样扩展到已编译的程序中。

大多数语言都鼓励程序员独立思考每个子例程,为输入和输出定义类型协定,并隐藏有关调用代码的其他信息。调用宏不会引起的子例程通常会对性能产生影响,并且宏可能能够以子例程无法执行的方式来操纵程序。因此,与子例程相比,宏可以被认为是“较低级别的”,因为它们的运行方式不太抽象。


0

宏在编译期间执行,函数在运行时执行。

例:

#include <stdio.h>

#define macro_sum(x,y) (x+y)

int func_sum(x,y) {
    return x+y;
}

int main(void) {
    printf("%d\n", macro_sum(2,3));
    printf("%d\n", func_sum(2,3));
    return 0;
}

因此,在编译过程中,代码实际上更改为:

#include <stdio.h>

int func_sum(x,y) {
    return x+y;
}

int main(void) {
    printf("%d\n", (2+3));
    printf("%d\n", func_sum(2,3));
    return 0;
}
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.