C++中宏和常量的区别是什么?

27

我在一次技术面试中被问到了这个问题:

C++中const与宏之间有什么区别?

我的答案是宏是预处理指令,如果使用宏,由于在编译前替换为常量表达式,因此可能很难调试应用程序。相比之下,const可以具有类型标识符,并且易于调试。

还有其他区别吗?应该优先使用哪种方法?

编辑:

根据IBM的C++文档:

#defineconst类型限定符之间的一些区别如下:

  1. #define指令可用于创建数字、字符或字符串常量的名称,而任何类型的const对象都可以声明。

  2. 与使用#define创建的常量不同,const对象受变量作用域规则的约束。与const对象不同,宏的值不会出现在编译器使用的中间源代码中,因为它们进行内联扩展。内联扩展使得宏值对调试器不可用。

  3. 宏可以用于常量表达式,例如数组大小的定义,而const对象不能。(我认为我们肯定需要使用宏来定义array_size)。

  4. 编译器不会对宏进行类型检查,包括宏参数。


1
规则“宏可以用于常量表达式,例如数组边界,而const对象不行。”仅适用于C语言,而不适用于C ++。 - Murali Krishna
7个回答

27
宏和常量远非相同的东西,每个都有其适用的情况,而你的答案只是涉及了这种差异的表面。此外,C++有两种不同类型的常量。
使用const限定符定义的常量最好被视为无法修改的变量。它具有变量的所有属性:它有一个类型,有一个大小,有链接性,可以取其地址。(如果可以试图卸载一些这些属性,例如从未使用地址的常量可能不会发射到可执行文件中,但这仅凭借着“好像规则”.) 你唯一不能对const数据做的事情是改变其值。使用enum定义的常量略有不同。它具有类型和大小,但没有链接性,不能取其地址,其类型是独特的。这两者都在第七个转换阶段进行处理,因此它们不能是除lvalue或rvalue之外的任何内容。(抱歉我在上一句话中使用了行话,但否则我将不得不写几段话来解释)。
宏的约束要少得多: 它可以扩展为任何标记序列,只要整个程序保持良好形式即可。它没有变量的任何属性。应用sizeof或&等操作符到宏可能会有用,也可能不会,这取决于宏扩展的内容。有时将宏定义为扩展为数值文字,这样的宏有时被认为是常量,但它们实际上不是:“编译器本身”(即第七个转换阶段)将其视为数字文字。
现在通常认为最好不要使用宏,而应该使用常量。宏不遵守所有其他标识符的相同作用域规则,这可能会令人困惑,如果使用常量,则可以为第七个转换阶段及调试器提供更多信息。然而,如果需要执行无法通过其他方式完成的操作,则可以使用宏。(那些在这个意义下有效地扩展到数字字面量的宏通常不止如此,但我不会说从来没有。)
编辑:以下是一个宏执行有趣操作的示例。它绝不是常量。可能有一种方法可以在没有宏的情况下获得相同的效果(如果您知道一种不涉及字符串流的方法,我很想听听!),但我认为这是说明宏的强大和危险的好例子(对于后者,请考虑如果在一个非常特定的上下文之外使用它会发生什么...)
static double elapsed()
{ ... }
#define ELAPSED '[' << std::fixed << std::setprecision(2) << elapsed() << "] "

// usage:
for (vector<string>::iterator f = files.begin(); f != files.end(); f++) {
    cout << ELAPSED << "reading file: " << *f << '\n';
    process_file(*f);
}

@Zack- +1。你能否举个例子,说明在哪些情况下必须使用宏来完成任务? - Mahesh
我想不出一个简短的例子。一个常见的情况是当你需要从许多样板中组装一个顶层结构时:例如,参见http://mxr.mozilla.org/mozilla-central/source/xpcom/glue/nsISupportsImpl.h#515 -- 如果没有宏,那里所做的事情将更容易出错和乏味。 (虽然我不希望将XPCOM作为一个好的例子,但你看到的最好被认为是长期以前做出的错误设计决策的瘢痕,现在难以纠正。) - zwol
@Mahesh:宏的字符串连接功能在调试时非常方便。#define DEBUG(x) cout<<#x"="<<x; 而其使用##进行标记粘贴的功能可以用于创建新的标识符。 - Terminal
@Mahesh:我更新了我的答案,并提供了一个宏的示例,该宏可以完成其他方式无法完成的一些小而非平凡的任务。 - zwol

16

对于许多原因来说,我们应该更喜欢使用 const int sum = 1; 而不是 #define sum 1:

基于作用域的机制:

#define 不考虑作用域,所以无法创建类级别的名称空间。而常量变量可以在类中具有作用域。

避免编译错误时出现奇怪的魔幻数字:

如果你在使用 #define,那么这些宏定义将在预处理期间被替换掉。所以如果你在编译过程中收到错误信息,那么会很困惑,因为错误信息将不指向宏名称而是数值,并且看起来像突然出现的一个数值,你需要在代码中耗费大量时间进行跟踪。

易于调试:

同样由于上述原因,在调试期间使用 #define 不会提供任何帮助。
为了避免上述两种情况,使用 const 将是更好的选择。


总之,const 是首选,因为我们可以控制它的作用域并且易于调试。但问题是为什么要优先选择全局常量而不是宏。 - Amm Sokun
@Amm Sokun:此外,总是会产生不必要的副作用,因此它们在某种意义上是有害的。至于在C++中使用宏作为命名常量,很少有这样的需要,使用宏作为命名常量通常出现在C语言中。 - Alok Save
我曾经遇到过魔法数字,但神奇的数字一定非常特别。 - sean e

3

(此前发布在static const vs #define - 以下是复制内容,因为这个问题似乎更受关注...如果不合适,请告诉我...)

每件事情都有利弊,取决于使用方式:

  • 常量
    • 正确的作用域/标识符冲突问题得到很好的处理
    • 强类型,用户指定一个类型
      • 你可以尝试像这样“类型”#define,如#define S std::string("abc"),但是常量避免了在每个使用点重复构造不同的临时对象
    • 一个定义规则的复杂性
    • 可以取地址,创建const引用等
  • 宏定义
    • “全局”作用域/更容易导致冲突的用法,可能会产生难以解决的编译问题和意外的运行时结果,而不是合理的错误消息;缓解这种情况需要:
      • 长,晦涩和/或集中协调的标识符,对它们的访问不能从隐式匹配已使用/当前/Koenig查找的命名空间、命名空间别名等中受益。
      • 通常需要使用所有大写字符,并保留给预处理器定义(企业级预处理器使用的重要指南,可以期望第三方库遵循),观察其中暗示迁移现有常量或枚举到宏定义需要改变大小写(因此影响客户端代码)。 (就我个人而言,我大写枚举的第一个字母,但不是常量,所以我也会受到影响-也许是重新考虑的时候了。)
    • 更多的编译时操作可能:字符串文字串联、字符串化(取其大小)
      • 缺点在于,给定#define X "x"和一些客户端使用ala "pre" X "post",如果你想或需要使X成为可运行时更改的变量而不是常量,那么你就有麻烦了,而从const char*const std::string转换更容易,因为它们已经强制用户合并连接操作。
    • 不能直接在定义的数字常量上使用sizeof
    • 无类型(GCC不会警告如果与unsigned比较)
    • 一些编译器/链接器/调试器链可能不会显示标识符,因此您将被降级为查看“魔术数字”(字符串等...)
    • 不能取地址
    • 替换值在创建#定义的上下文中不需要合法(或离散),因为它在每个使用点进行评估,所以您可以引用尚未声明的对象,依赖于不需要预先包含的“实现”,创建“常量”例如{1,2},可以用于初始化数组,或#define MICROSECONDS *1E-6等。(绝对不推荐这样做!)
    • 一些特殊的东西,如__FILE____LINE__,可以并入宏替换中
  • 枚举
    • 仅适用于整数值
    • 正确的作用域/标识符冲突问题得到很好的处理
    • 强类型,但是对于一个足够大的有符号或无符号int大小,你没有控制(在C++03中)
    • 不能取地址
    • 更强的使用限制(例如增量- template <typename

      通常情况下,我使用常量并认为它们是一般用途中最专业的选项(尽管其他选项对这个老懒程序员来说更加简单)。


2
另一个区别是,const 变量有内存,并且可以被指针引用。而宏只是编译前将发生的自动完成,因此名称在编译时会丢失。

此外,宏可以不仅仅是一个常量。它可以是一个表达式或任何语法上正确的内容,甚至可以是整个函数定义。

宏用于描述编程选择,例如堆栈大小;而 const 用于描述真实世界的常量,例如圆周率或自然对数 e 的值。


+1 是为了明确指出宏也可以定义任何表达式或函数。其中一个有趣的例子是 BOOST_FOREACH,它允许您以清晰、简洁的方式循环遍历序列中的每个元素。 - Chris Frederick

1

define可以重新定义,但const会导致编译器错误:

示例: 源代码:main.cpp

#define int_constance 4
#define int_constance 8 // ok, compiler will warning ( redefine macro)

const int a = 2;
const int a = 4; // redefine -> error

int main(int argc, char** argv)
{
   std::cout << int_constance ; // if remove second #define line, output will be 8

   return 0;
}

0

宏不尊重作用域,而且宏的名称可能无法在符号调试器中使用。Dan Saks有一篇相当完整的文章,介绍了宏(没有),常量对象和枚举常量的相对优点。像Stephen Dewhurst一样,Saks更喜欢使用枚举常量来表示整数值,因为它们不占用存储空间(更准确地说,枚举常量既没有存储期限也没有链接)。


-2
一个宏总是有一个类型,例如#define FIVE 5是 int 类型。
const 变量相对于宏的优点可能在于内存使用:使用宏时,值可能必须重复复制到其使用的每个位置,而 const 变量不会在内存中复制。(但我不确定这种差异)

4
宏并没有类型。在你的例子中,数字5具有类型,但是宏定义FIVE没有类型,因为它在引入类型之前就从程序中消失了(在第4个翻译阶段而不是第7个)。这不仅仅是在规则上追求完美:如果我写了#define L_(x) x##L; #define L(x) L_(x); 然后用L(FIVE);那么展开的结果将会是5L,它的类型是long。 - zwol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接