[读者注意:作为对这个问题的降级评论的建议,我添加了这个注意事项:不要将这个问题的任何部分作为真实陈述:我提出了问题,部分是因为一些我的知识不正确。因此,部分或全部问题本身可能不正确。为了保持原始问题的整体性,为了说明我错误的原因,我决定只添加此通知,并保留原始问题。]
在C ++(而不是C)中,全局const
数组使用内部链接进行优化。如果全局const
数组的定义位于单独的 .cpp 文件中,则会生成undefined reference
链接器错误。请参阅undefined reference to array of constants。
因此,对于每个 .cpp 文件来访问相同的const数组,我们应该使用单独的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[]
。它们会被合并(优化)成一个副本吗?
答案 0 :(得分:2)
在C ++(而不是C)中,全局const数组使用内部链接进行优化
"优化"也许不是正确的词。默认内部链接
对于const
文件范围对象,我们可以在其中定义const
个对象
头文件,而不必前缀static
,或将它们包含在匿名中
命名空间,以阻止多定义链接错误。这很方便
而直观。优化可能会产生与否,具体取决于此和。
"文件范围"肯定是一个更好的词,"全球"在这方面。你' 11 在一段时间内看到原因。
这个分数上的阵列并没有什么特别之处。 所有 const
文件范围
默认情况下,对象在C ++中具有内部链接。
所以也许你的问题可以强化为: C ++是否保证不同的文件范围 const
具有相同名称,类型和字节方式的不同翻译单元中的对象
值将合并到它们链接的程序中的单个副本?
不,它没有。相反,C ++标准 probibits 中的不同对象 程序(除了对象和子对象)具有相同的地址:
C ++ 11 [intro.object],第6段
除非对象是零字段或零大小的基类子对象,否则为地址 该对象的大小是它占用的第一个字节的地址。有两个对象 如果一个是另一个的子对象,则不是位字段可以具有相同的地址,或者 如果至少有一个是零大小的基类子对象,它们的类型不同; 否则,他们应具有不同的地址 4 。
(强调我的)。后来的标准也有相同的效果。
脚注[4]提供了一个蠕动室的缝隙:
4)在“as-if”规则下,允许实现存储两个对象 相同的机器地址或不存储 如果程序无法观察到差异,那就完全是对象。
但是如果程序中可以区分不同的对象,那么它们就不能 拥有相同的地址 - 他们会这样做,他们合并了。
即使标准没有做出这样的规定,合并相同 无论如何,来自不同翻译单元的文件范围const
对象都是不可行的。
考虑:
<强> array.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
=外部链接=链接器可见(那就是&#34;文件范围&#34;比#34;全球&#34;更好的词。)
该对象在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 ++允许它,链接器也不能这样做,因为
链接器无法看到它们!
让我们进行链接并询问链接器的mapfile:
$ 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
Mapfile命中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
中的第一个是程序偏移0xb20
;第二
在0xb80
1 。所以,当我们最终运行该程序时,我们应该感到高兴,
但并不惊讶,看到:
$ ./prog
Address of `Arr` in `foo.cpp` = 0x55edf0dd6b20
Address of `Arr` in `bar.cpp` = 0x55edf0dd6b80
Arr
引用的本地foo()
和bar()
引用的本地Arr
仍然存在
相隔0x60字节,分别是内存中程序启动时的0xb20和0xb80字节。
显然你会更喜欢在程序中只有一个const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
,而不是两个。至
实现你必须编译:
#ifndef ARRAY_H
#define ARRAY_H
extern const int Arr[10];
#endif
只有一个目标文件,带有外部链接,因此链接器可以在那里看到它, 并在所有其他目标文件中引用该对象。像这样:
array.h(修订版)
#include "array.h"
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
<强> array.cpp 强>
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}}
<小时/> [1]一些亲密的读者可能想知道为什么我们看到偏移而不是绝对地址 这里。这是因为我的Ubuntu 17.10工具链默认生成PIE可执行文件。