在C++头文件中定义常量变量

106

我正在开发的一个程序中有很多常量,它们适用于所有类。我想创建一个名为 "Constants.h" 的头文件,并能够声明所有相关的常量。然后在我的其他类中,我只需要包含#include"Constants.h"

我使用#ifndef ... #define ...语法使其正常工作。但是,我更喜欢使用const int...形式的常量。不过我不太确定如何做到这一点。


8
你能解释一下你对 const int 不理解的地方吗?如果这是你的问题,可以贴出编译失败的示例代码吗? - djechlin
对于C++的头文件,你应该使用.hpp文件格式来区分C语言的头文件,后者通常以.h格式保存。 - Kesto2
6个回答

134
你可以在头文件中简单地定义一系列 const ints:
// Constants.h
#if !defined(MYLIB_CONSTANTS_H)
#define MYLIB_CONSTANTS_H 1

const int a = 100;
const int b = 0x7f;

#endif

这是因为在C++中,命名空间作用域(包括全局命名空间)中显式声明为const且未显式声明为extern的名称具有内部链接性,因此当您将翻译单元链接在一起时,这些变量不会导致重复的符号。或者,您可以显式将常量声明为静态。
static const int a = 100;
static const int b = 0x7f;

这种方式更适用于C语言,并且更易于理解,尤其是对不熟悉C ++链接规则的人来说。
如果所有常量都是int类型,那么您可以使用另一种方法,将标识符声明为枚举。
enum mylib_constants {
    a = 100;
    b = 0x7f;
};

所有这些方法都只使用一个头文件,并允许声明的名称被用作编译时常量。使用 extern const int 和单独的实现文件可以防止名称被用作编译时常量。
请注意,使某些常量隐式具有内部链接的规则确实适用于指针,就像其他类型的常量一样。然而,棘手的问题是,将指针标记为“const”需要使用与大多数人用于使其他类型的变量成为常量的语法略有不同的语法。您需要执行以下操作:
int * const ptr;

为了使规则适用于常量指针,需要将其设置为常量指针。

还要注意的是,这是我喜欢在类型后一致地放置 const 的原因之一:int const 而不是 const int。我也将 * 放在变量旁边:即 int *ptr; 而不是 int* ptr;(也可以参考 this 讨论)。

我喜欢做这些事情,因为它们反映了 C++ 实际工作的一般情况。备选项(const intint* p)只是为了使某些简单的东西更易读而特殊处理。问题是,当您走出这些简单的情况时,特殊处理的备选方案会变得具有误导性。

因此,尽管前面的示例展示了 const 的常见用法,但我实际上建议人们像这样编写它们:

int const a = 100;
int const b = 0x7f;

并且

static int const a = 100;
static int const b = 0x7f;

1
你的具有隐式静态链接的规则在指针方面不起作用。 - yanpas
15
不过,你需要确保将 const 放在正确的位置上。将指针变量设为 const 需要在 * 后面添加 const。例如:int * const x。如果你只是使用 const int *x;,那么 const 关键字会应用于指针所指向的内容而不是指针本身。 - bames53
1
在C++17中,它变得更加简洁; 请参见此其他答案 - Jason C
1
如何理解“使用extern const int和单独的实现文件可以防止名称被用作编译时常量”? - John
1
@John 从编译器的角度看待这个问题。你有一个头文件包含“extern const int foo;”,而“foo”的值在另一个实现文件中设置。当你在另一个文件中使用foo时,在此文件中包含了该头文件,但编译器无法知道编译时应该使用哪个值,因为编译器在编译使用foo的源文件时并不知道设置foo值的文件是哪一个。这是由于C++的"分离编译"模型导致的。甚至可以有不同的实现文件以不同的方式设置foo,并根据链接的实现文件的不同来更改程序。 - bames53

34

我认为在这种情况下,命名空间更适合。

选项1:

#ifndef MYLIB_CONSTANTS_H
#define MYLIB_CONSTANTS_H

//  File Name : LibConstants.hpp    Purpose : Global Constants for Lib Utils
namespace LibConstants
{
  const int CurlTimeOut = 0xFF;     // Just some example
  ...
}
#endif

// source.cpp
#include <LibConstants.hpp>
int value = LibConstants::CurlTimeOut;

选项二:

#ifndef MYLIB_CONSTANTS_H
#define MYLIB_CONSTANTS_H
//  File Name : LibConstants.hpp    Purpose : Global Constants for Lib Utils
namespace CurlConstants
{
  const int CurlTimeOut = 0xFF;     // Just some example
  ...
}

namespace MySQLConstants
{
  const int DBPoolSize = 0xFF;      // Just some example
  ...
}
#endif



// source.cpp
#include <LibConstants.hpp>
int value = CurlConstants::CurlTimeOut;
int val2  = MySQLConstants::DBPoolSize;

我绝不会使用类来存储这种硬编码的常量变量。

我喜欢这个想法。但是,这是否意味着我必须初始化 valueval2 和其他所有常量?有没有一种方法可以在函数内部包含命名空间并使用与命名空间中相同的常量名称? - talekeDskobeDa
还要记得在 char * 中使用 const char * const - basil

32

C++17 inline变量

这个很棒的C++17特性允许我们:

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

编译和运行:
g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub上游

另请参阅:如何使用内联变量?

C++标准中的内联变量

C++标准保证地址相同。 C++17 N4659标准草案 10.1.6 "The inline specifier":

6.一个带有外部链接的内联函数或变量在所有翻译单元中都应该具有相同的地址。

cppreference https://en.cppreference.com/w/cpp/language/inline 解释说,如果没有给出 static,则它具有外部链接。

内联变量实现

我们可以通过以下方式观察其实现:

nm main.o notmain.o

其中包含:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

对于man nm命令中的符号,它是一个独特的全局符号。这是标准ELF符号绑定的GNU扩展。对于这样的符号,动态链接器将确保在整个进程中只使用一种此名称和类型的符号。

因此,我们可以看到有一个专用的ELF扩展来实现这个功能。

C++17标准草案关于“全局”const意味着static

以下是https://dev59.com/bmct5IYBdhLWcg3wgNnR#12043198提到的引用:

C++17 n4659标准草案 6.5 "程序和链接":

3.如果一个具有命名空间作用域(6.3.6)的名称是:

  • (3.1) —— 显式声明为静态变量、函数或函数模板;或
  • (3.2) —— 非内联非易失性const限定类型的非内部链接变量,既不显式声明为extern也未先前声明为外部链接;或
  • (3.3) —— 匿名联合体的数据成员。

则该名称具有内部链接。

“命名空间”作用域通常是我们口头上所说的“全局”。

附录C(信息性的)兼容性,C.1.2
第6条:“基本概念”给出了为什么从C中更改这个的理由:

6.5 [也是10.1.7]

更改:在C++中,如果一个文件作用域的const变量没有显式声明为extern,则它具有内部链接,而在C中它将具有外部链接。

原因:因为在C++中,const对象可以在翻译期间用作值,所以这个特性促使程序员为每个const对象提供显式初始化程序。此功能允许用户将const对象放在多个翻译单元中包含的源文件中。

对原始特性的影响:更改到良好定义的特性的语义。

转换的难度:语义转换。

使用范围:很少。

另请参见:为什么在C++中const意味着内部链接,而在C中不是?

在Ubuntu 18.04的GCC 7.4.0中进行了测试。


28
通常情况下,当在多个源文件中包含头文件时,不应该使用例如const int的语句。这是因为变量会在每个源文件(严格来说是翻译单元)中定义一次,由于全局const变量隐含为静态变量,会占用更多的内存。
相反,您应该拥有一个特殊的源文件Constants.cpp来实际定义变量,然后在头文件中将变量声明为extern
像这样的头文件:
// Protect against multiple inclusions in the same source file
#ifndef CONSTANTS_H
#define CONSTANTS_H

extern const int CONSTANT_1;

#endif

而这是一个源文件中的内容:

const int CONSTANT_1 = 123;

4
最好在头文件中初始化它,这样它就可以作为编译时常量使用。 - Mike Seymour
15
在头文件中可以使用const int,因为默认情况下const变量具有内部链接。是否应该这样做而不是使用单个实例是另一个问题;通常只要值在头文件中可用,就没有太大区别。 - Mike Seymour
5
C++17中的内联变量是实现这一目标的更棒方式,对于那些可以使用该版本的人来说:https://dev59.com/bmct5IYBdhLWcg3wgNnR#53541011 - Ciro Santilli OurBigBook.com
1
即使全局常量默认具有静态链接,源文件中是否应该使用关键字extern? - Jamāl
2
@Jamāl,除非有先前的声明强制它具有外部链接性,例如包含带有extern声明的头文件。 - Some programmer dude
显示剩余4条评论

2

不要创建大量的全局变量,你可以考虑创建一个类,其中包含许多公共静态常量。这仍然是全局的,但是这样它被包装在一个类中,所以你知道常量来自哪里,并且它应该是一个常量。

Constants.h

#ifndef CONSTANTS_H
#define CONSTANTS_H

class GlobalConstants {
  public:
    static const int myConstant;
    static const int myOtherConstant;
};

#endif

Constants.cpp

#include "Constants.h"

const int GlobalConstants::myConstant = 1;
const int GlobalConstants::myOtherConstant = 3;

那么您可以这样使用它:
#include "Constants.h"

void foo() {
  int foo = GlobalConstants::myConstant;
}

55
为什么要使用类来模拟命名空间,而不直接使用命名空间? - Mike Seymour
至少在C语言中使用结构体可能会有意义。 - Radzor

0

看起来bames53的答案可以扩展到在命名空间和类声明中定义整数和非整数常量值,即使它们被包含在多个源文件中也是如此。不必将声明放在头文件中,但需要将定义放在源文件中。以下示例适用于Microsoft Visual Studio 2015,z/OS V2.2 XL C/C++ on OS/390以及GNU/Linux 4.16.14(Fedora 28)上的g++(GCC)8.1.1 20180502。请注意,常量仅在单个头文件中声明/定义,该头文件被包含在多个源文件中。

在foo.cc中:

#include <cstdio>               // for puts

#include "messages.hh"
#include "bar.hh"
#include "zoo.hh"

int main(int argc, const char* argv[])
{
  puts("Hello!");
  bar();
  zoo();
  puts(Message::third);
  return 0;
}

在messages.hh文件中:

#ifndef MESSAGES_HH
#define MESSAGES_HH

namespace Message {
  char const * const first = "Yes, this is the first message!";
  char const * const second = "This is the second message.";
  char const * const third = "Message #3.";
};

#endif

在 bar.cc 文件中:
#include "messages.hh"
#include <cstdio>

void bar(void)
{
  puts("Wow!");
  printf("bar: %s\n", Message::first);
}

在zoo.cc中:
#include <cstdio>
#include "messages.hh"

void zoo(void)
{
  printf("zoo: %s\n", Message::second);
}

In bar.hh:

#ifndef BAR_HH
#define BAR_HH

#include "messages.hh"

void bar(void);

#endif

在zoo.hh文件中:
#ifndef ZOO_HH
#define ZOO_HH

#include "messages.hh"

void zoo(void);

#endif

这将产生以下输出:

Hello!
Wow!
bar: Yes, this is the first message!
zoo: This is the second message.
Message #3.

数据类型char const * const表示指向常量字符数组的常量指针。第一个const是必需的,因为(根据g++)“ISO C ++禁止将字符串常量转换为'char *'”。第二个const是必需的,以避免由于多个定义而导致链接错误(然后不够常量)。如果省略其中一个或两个const,则您的编译器可能不会抱怨,但源代码的可移植性较差。

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