[读者注意:作为对这个问题的降级评论的建议,我添加了这个注意通知:不要假设这个问题的任何部分都是真实的陈述:我提出这个问题,部分原因是我的一些知识是不正确的。因此,问题本身的部分或全部可能不正确。为了保持原始问题的完整性,为了说明我为什么错了,我决定只添加此通知,并按原样保留原始问题。]
在 C++(非 C)中,全局 const
数组使用内部链接进行优化。如果定义为全局const
数组放在一个单独的 中.cpp 文件,它会生成一个 undefined reference
链接器错误。见 undefined reference to array of constants .
因此,对于每个 .cpp 文件访问相同的常量数组,我们应该使用单独的 const
数组,最好是头文件形式,如下例:
foo.h :
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
foo.cpp :
#include "foo.h"
// ...
memcmp(Arr, MyArr, 10*sizeof(int));
bar.cpp :
#include "foo.h"
// ...
memcmp(Arr, MyArr2, 10*sizeof(int));
问题是:
自
foo.cpp
和 bar.cpp
有自己的Arr[]
.它们会被合并(优化)成一个拷贝吗?
最佳答案
In C++ (not C), a global const array uses internal linkage for optimization
“优化”可能不是正确的词。默认内部链接
为
const
文件范围对象允许我们定义 const
对象头文件无需前缀
static
,或将它们封装在匿名命名空间,以防止多定义链接错误。这个方便
和直观。优化可能会产生,也可能不会产生,这取决于这个和那个。
在这方面,“文件范围”当然是比“全局”更好的词。你会
一会儿看看为什么。
在这个分数上,数组没有什么特别之处。全部
const
文件范围默认情况下,对象具有内部链接,在 C++ 中。
因此,也许您的问题可以尖锐化为:C++ 是否保证不同的文件范围
const
具有相同名称、类型和字节的不同翻译单元中的对象值将合并到它们链接的程序中的单个拷贝中?
不,它没有。相反,C++ 标准在一个
具有相同地址的程序(对象和子对象除外):
C++11 [intro.object],第 6 段
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses4.
(强调我的)。后来的标准也有同样大意的词。
该脚注 [4] 提供了一个回旋余地的缝隙:
4) Under the “as-if” rule an implementation is allowed to store two objects at the same machine address or not store an object at all if the program cannot observe the difference.
但是如果不同的对象在程序中是可区分的,那么它们一定不能
有相同的地址 - 如果他们合并,他们会这样做。
并且即使标准没有做这个规定,相同的合并
文件范围
const
无论如何,来自不同翻译单元的对象都是不可行的。考虑:
数组.h
#ifndef ARRAY_H
#define ARRAY_H
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
#endif
foo.cpp
#include "array.h"
#include <iostream>
void foo()
{
std::cout << "Address of `Arr` in `foo.cpp` = " << Arr << std::endl;
}
bar.cpp
#include "array.h"
#include <iostream>
void bar()
{
std::cout << "Address of `Arr` in `bar.cpp` = " << Arr << std::endl;
}
main.cpp
extern void foo();
extern void bar();
int main()
{
foo();
bar();
return 0;
}
将所有这些源文件编译为目标文件:
g++ -Wall -c foo.cpp bar.cpp main.cpp
编译器遇到了
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
编译中
foo.cpp
至 foo.o
并相应地定义了一个对象在
foo.o
:$ readelf -s foo.o | grep Arr
6: 0000000000000000 40 OBJECT LOCAL DEFAULT 5 _ZL3Arr
_ZL3Arr
是文件范围符号 Arr
的名称修改:$ c++filt _ZL3Arr
Arr
40
是对象的大小(以字节为单位),适合 10 个 4 字节整数。对象是
LOCAL
:LOCAL
= 内部链接 = 链接器不可见 GLOBAL
= 外部链接 = 链接器可见 (这就是为什么“文件范围”比“全局”更好的原因)。
该对象在链接部分中定义,索引为
5
在 foo.o
. readelf
也可以告诉我们什么联动部分是:
$ readelf -t foo.o
There are 15 section headers, starting at offset 0x7e0:
Section Headers:
[Nr] Name
Type Address Offset Link
Size EntSize Info Align
Flags
[ 0]
NULL NULL 0000000000000000 0000000000000000 0
0000000000000000 0000000000000000 0 0
[0000000000000000]:
...
...
[ 5] .rodata
PROGBITS PROGBITS 0000000000000000 00000000000000e0 0
0000000000000053 0000000000000000 0 32
[0000000000000002]: ALLOC
...
...
第 5 节是
.rodata
,这是只读数据。 Arr
已放入只读数据因为它是
const
.出于同样的原因,同样的事情也适用于
bar.o
:$ readelf -s bar.o | grep Arr
6: 0000000000000000 40 OBJECT LOCAL DEFAULT 5 _ZL3Arr
所以每个
foo.o
和 bar.o
包含它自己的 40 字节对象 _ZL3Arr
即 LOCAL
和只读。编译全部完成并且我们还没有一个程序。所以如果
_ZL3Arr
在 foo.o
和 _ZL3Arr
在 bar.o
将要合并到程序中,它们必须由链接器合并。即使我们想要它,或者 C++ 允许它,链接器也不能这样做,因为
链接器看不到它们!
让我们进行链接并请求链接器的映射文件:
$ g++ -o prog main.o foo.o bar.o -Wl,-Map=prog.map
Mapfile 命中真正的全局 ( =
GLOBAL
) 符号:$ grep -Po 'foo' prog.map | wc -w
12
$ grep -Po 'bar' prog.map | wc -w
10
$ grep -Po 'main' prog.map | wc -w
8
映射文件命中
Arr
:$ grep -Po 'Arr' prog.map | wc -w
0
但是
readelf
可以看到局部符号,现在我们有了一个程序:$ readelf -s prog | grep Arr
36: 0000000000000b20 40 OBJECT LOCAL DEFAULT 16 _ZL3Arr
42: 0000000000000b80 40 OBJECT LOCAL DEFAULT 16 _ZL3Arr
所以
prog
包含两个 40 字节 LOCAL
名称为 _ZL3Arr
的对象,两者都在程序的链接部分 16 中,即...
$ readelf -t prog
There are 29 section headers, starting at offset 0x2ce8:
Section Headers:
[Nr] Name
Type Address Offset Link
Size EntSize Info Align
Flags
...
...
[16] .rodata
PROGBITS PROGBITS 0000000000000b00 0000000000000b00 0
00000000000000d1 0000000000000000 0 32
[0000000000000002]: ALLOC
...
...
再次,只读数据。
readelf
还说第一个_ZL3Arr
s 位于程序偏移量 0xb20
;第二在
0xb80
1. 所以当我们最终运行程序时,我们应该很高兴,但并不惊讶,看到:
$ ./prog
Address of `Arr` in `foo.cpp` = 0x55edf0dd6b20
Address of `Arr` in `bar.cpp` = 0x55edf0dd6b80
本地
Arr
引用 foo()
以及 bar()
引用的那个保持相距 0x60 个字节,分别在内存中距程序开始处 0xb20 和 0xb80 个字节。
显然你更喜欢只有一个
Arr
,不是两个,在程序中。到实现你必须编译:
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
在一个目标文件中,带有外部链接,所以链接器可以在那里看到它,
并在所有其他对象文件中引用该对象。像这样:
array.h(修订版)
#ifndef ARRAY_H
#define ARRAY_H
extern const int Arr[10];
#endif
array.cpp
#include "array.h"
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
其他文件同前。在
array.h
我们明确声明 Arr
具有外部链接,并且该声明在 array.cpp
中被编译器看到并得到尊重。 .编译并链接:
$ g++ -Wall -c main.cpp foo.cpp bar.cpp array.cpp
$ g++ -o prog main.o foo.o bar.o array.o
什么是
Arr
现在算在节目里吗?$ readelf -s prog | grep 'Arr'
60: 0000000000000b80 40 OBJECT GLOBAL DEFAULT 16 Arr
一。还在只读数据中。但现在
GLOBAL
.和 prog
同意只有一个
Arr
:$ ./prog
Address of `Arr` in `foo.cpp` = 0x562a4fb7bb80
Address of `Arr` in `bar.cpp` = 0x562a4fb7bb80
[1] 一些细心的读者可能想知道为什么我们看到的是偏移量而不是绝对地址
这里。这是因为我的 Ubuntu 17.10 工具链默认生成 PIE 可执行文件。
关于C++ 全局常量数组 : Is it guaranteed to be merged (optimized) into one copy?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48636462/