静态const vs #define


212

使用static constvars比使用#define预处理器更好吗?还是取决于上下文?

每种方法的优点/缺点是什么?


14
斯科特·迈耶斯(Scott Meyers)非常详尽地论述了这一主题。他在“有效的C ++第三版”中的项目2。两种特殊情况(1)对于类特定的常量,在类范围内首选static const;(2)命名空间或匿名作用域const优于#define。
艾瑞克(Eric)

2
我更喜欢枚举。因为它是两者的混合体。除非您创建空间变量,否则不会占用空间。如果只想用作常量,则枚举是最佳选择。它在C / C ++ 11 std中具有类型安全性,并且还是一个完美的常数。#define类型为unsafe,如果编译器无法对其进行优化,则const会占用空间。
siddhusingh

1
我决定使用#define还是static const(对于字符串)决定是由初始化方面决定的(以下答案未提及):如果仅在特定的编译单元中使用常量,则我继续使用static const,否则我将使用#define-避免静态顺序初始化失败 isocpp.org/wiki/faq/ctors#static-init-order
Martin Dvorak

如果constconstexpr或者enum或者在你的情况下,任何变化的作品,那么它宁愿#define
Phil1970

@MartinDvorak“ 避免静态订单初始化惨败 ”常量的问题如何?
curiousguy18年

Answers:


139

就我个人而言,我讨厌预处理器,因此我总是选择与一起使用const

a的主要优点#define是它不需要内存来存储在程序中,因为它实际上只是用文字值替换了一些文本。它还具有没有类型的优点,因此可以将其用于任何整数值而不会生成警告。

const”的优点是可以确定范围,并且可以在需要传递指向对象的指针的情况下使用它们。

不过,我不知道您对“ static”部分的理解是什么。如果您要全局声明,则将其放在匿名名称空间中,而不要使用static。例如

namespace {
   unsigned const seconds_per_minute = 60;
};

int main (int argc; char *argv[]) {
...
}

8
字符串常量尤其是可能受益于#defined的常量之一,至少可以将它们用作较大字符串常量的“构建块”。有关示例,请参见我的回复。
AnT

62
#define不使用任何内存的优点是不准确的。示例中的“ 60”必须存储在某个位置,无论它是static const还是#define。实际上,我已经看到编译器在其中使用#define导致大量(只读)内存消耗,而静态const不使用不需要的内存。
吉拉德·纳尔

3
#define就像您输入过一样,因此它绝对不是来自内存。
牧师

27
@theReverend文字值是否可以免于消耗机器资源?不,他们可能会以不同的方式使用它们,也许它不会出现在堆栈或堆中,但是有时程序会连同编译到其中的所有值一起加载到内存中。
Sqeaky

13
@ gilad-naor,您一般来说是正确的,但是像60这样的小整数有时实际上可能是一种部分异常。一些指令集具有直接在指令流中编码整数或整数子集的能力。例如,MIP添加立即(cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/addi.html)。在这种情况下,确实可以说#defined整数不使用空间,因为在已编译的二进制文件中,它占用了指令中仍然存在的一些备用位。
ahcox 2015年

241

#defines,consts和(您忘记了)enums 之间的优缺点,具体取决于用法:

  1. enums:

    • 仅适用于整数值
    • 正确确定范围/标识符冲突问题可以很好地处理,尤其是在C ++ 11枚举类中,其中的枚举enum class X被范围消除了歧义X::
    • 强类型,但具有足够大的有符号或无符号int大小,您无法在C ++ 03中对其进行控制(尽管您可以指定一个位字段,如果枚举是struct /的成员,则应将它们打包到其中类/联合),而C ++ 11默认为,int但可以由程序员明确设置
    • 无法使用地址-没有一个地址,因为枚举值已在使用时被有效地内联替换
    • 较强的使用约束(例如,递增- template <typename T> void f(T t) { cout << ++t; }将不会编译,尽管您可以使用隐式构造函数,强制转换运算符和用户定义的运算符将枚举包装到类中)
    • 每个常量的类型都从封闭的枚举中template <typename T> void f(T)获取,因此,当从不同的枚举中传递相同的数值时,它们将获得不同的实例化,而所有枚举都与任何实际f(int)实例化都不同。每个函数的目标代码可能是相同的(忽略地址偏移量),但是我不希望编译器/链接器消除不必要的副本,尽管您可以在需要时检查编译器/链接器。
    • 即使使用typeof / decltype,也不能期望numeric_limits提供有意义的值和组合集的有用信息(实际上,源代码中甚至没有标明“合法”组合,请考虑enum { A = 1, B = 2 }-是A|B程序逻辑中的“合法”)透视?)
    • 枚举的类型名称可能出现在RTTI,编译器消息等的各个位置。-可能有用,可能会混淆
    • 您不能在没有翻译单元实际看到值的情况下使用枚举,这意味着库API中的枚举需要在标头中公开这些值,make并且其他基于时间戳的重新编译工具在更改它们时会触发客户端重新编译(不好! )

  1. consts:

    • 正确确定范围/标识符冲突问题得到了很好的处理
    • 强,单一,用户指定的类型
      • 您可以尝试“键入” #defineala #define S std::string("abc"),但常量避免在每个使用点重复构造不同的临时对象
    • 一个定义规则的并发症
    • 可以获取地址,创建对它们的const引用等。
    • 最类似于非const值,如果在两者之间切换,则可最大程度地减少工作和影响
    • 值可以放置在实现文件中,从而可以进行本地化的重新编译,并且只需客户端链接即可获取更改

  1. #defines:

    • “全局”范围/更容易出现用法冲突,这可能会产生难以解决的编译问题和意外的运行时结果,而不是合理的错误消息;缓解这种情况需要:
      • 长的,模糊的和/或集中协调的标识符,对它们的访问不能从隐式匹配使用的/当前的/ Koenig查找的名称空间,名称空间别名等中受益。
      • 尽管最好的做法是允许模板参数标识符为单字符大写字母(可能后跟数字),但通常保留并期望其他使用不带小写字母的标识符,这是预处理器定义所期望的(在OS和C / C ++库之外)标头)。这对于使企业规模的预处理器使用保持可管理性很重要。可以期望第三方图书馆遵守。观察到这意味着将现有const或枚举迁移到定义或从定义迁移涉及到大小写的更改,因此需要对客户端源代码进行编辑,而不是“简单”重新编译。(就我个人而言,我将枚举的首字母大写但不使用consts,因此我也很想在这两个字母之间迁移-也许是时候重新考虑一下。)
    • 可能有更多的编译时操作:字符串文字串联,字符串化(取其大小),串联为标识符
      • 缺点是给定的#define X "x"和某些客户端用法ala "pre" X "post",如果您希望或需要使X为运行时可更改的变量而不是常量,则可以强制对客户端代码进行编辑(而不仅仅是重新编译),而从const char*const std::string给定它们,则过渡更容易已经迫使用户把串联操作(例如"pre" + X + "post"用于string
    • 不能sizeof直接在定义的数字文字上使用
    • 无类型(与相比,GCC不会发出警告unsigned
    • 一些编译器/链接器/调试器链可能不提供标识符,因此您将被简化为查看“魔术数字”(字符串,无论如何...)
    • 不能拿地址
    • 在创建#define的上下文中,替换值不必是合法的(或离散的),因为在每个使用点都会对它进行评估,因此您可以引用尚未声明的对象,取决于不需要的“实现”被预先包括在内,创建“常量”(例如{ 1, 2 }可用于初始化数组#define MICROSECONDS *1E-6等)(绝对不建议这样做!)
    • 一些特殊的东西,例如__FILE____LINE__可以合并到宏替换中
    • 您可以在#if条件语句中测试语句的存在和值,以有条件地包含代码(比后预处理“ if”功能更强大,因为如果预处理程序未选择则无需编译代码,则可以使用#undef-ine或redefine等)。
    • 替代文本必须公开:
      • 在其使用的翻译单元中,这意味着供客户端使用的库中的宏必须位于标头中,因此make,其他基于时间戳的重新编译工具将在更改它们时触发客户端重新编译(不好!)
      • 或在命令行上,需要更加小心以确保重新编译客户端代码(例如,提供定义的Makefile或脚本应作为依赖项列出)

我个人的看法:

通常,我使用consts并认为它们是常规用法中最专业的选择(尽管其他方法对老的懒惰程序员很有吸引力)。


1
很棒的答案。一个小问题:我有时使用根本不在标头中的局部枚举,只是为了使代码清晰起见,就像在小型状态机中那样。因此,它们不必始终都在标头中。
kert 2014年

优点和缺点混合在一起,我非常希望看到一个比较表。
Unknown123 '19

@ Unknown123:随时发布一个-我不在乎您是否从这里窃取了您认为值得的任何观点。干杯
Tony Delroy

48

如果这是一个C ++问题,并且它#define是替代问题,那么它是关于“全局”(即文件作用域)常量的,而不是关于类成员的。说到C ++中的此类常量,这static const是多余的。在C ++中const,默认情况下具有内部链接,因此没有必要声明它们static。因此,它实际上是关于const#define

最后,const最好使用C ++ 。至少因为此类常量是类型化的和作用域化的。除了极少数例外,没有任何理由更喜欢#defineconst

字符串常量BTW是此类异常的一个示例。使用#defined字符串常量,可以使用C / C ++编译器的编译时串联功能,如

#define OUT_NAME "output"
#define LOG_EXT ".log"
#define TEXT_EXT ".txt"

const char *const log_file_name = OUT_NAME LOG_EXT;
const char *const text_file_name = OUT_NAME TEXT_EXT;

PS同样,以防万一,当有人提到static const替代时#define,通常意味着他们在谈论C,而不是C ++。我想知道这个问题是否被正确标记了...


1
根本没有理由偏爱#define ”,而不是什么?头文件中定义的静态变量?
curiousguy18年

9

#define 可能导致意外结果:

#include <iostream>

#define x 500
#define y x + 5

int z = y * 2;

int main()
{
    std::cout << "y is " << y;
    std::cout << "\nz is " << z;
}

输出不正确的结果:

y is 505
z is 510

但是,如果将其替换为常量:

#include <iostream>

const int x = 500;
const int y = x + 5;

int z = y * 2;

int main()
{
    std::cout << "y is " << y;
    std::cout << "\nz is " << z;
}

它输出正确的结果:

y is 505
z is 1010

这是因为#define只需替换文本即可。因为这样做会严重破坏操作顺序,所以我建议改用常量变量。


1
我有一个不同的意想不到的结果: y有值5500,一个小端串联x和5
用锤子代码


4
  • 键入静态const(具有类型),编译器可以检查其有效性,重新定义等。
  • #define可以不定义地重新定义。

通常,您应该首选静态const。它没有缺点。预处理程序应主要用于条件编译(有时可能用于真正肮脏的度量标准)。


3

#define建议不要使用preprocessor指令定义常量,不仅要在中使用C++,而且要在中使用C。这些常量将没有类型。甚至在C有人提出使用const常量。



2

总是比其他一些工具(如预处理器)更喜欢使用语言功能。

ES.31:请勿将宏用于常量或“函数”

宏是错误的主要来源。宏不遵循通常的作用域和类型规则。宏不遵循通常的参数传递规则。宏确保人类读者看到的东西不同于编译器看到的东西。宏使工具构建复杂化。

来自C ++核心准则


0

如果要定义要在类的所有实例之间共享的常量,请使用静态const。如果常量是特定于每个实例的,则只需使用const(但请注意,该类的所有构造函数都必须在初始化列表中初始化此const成员变量)。

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.