C ++全局const数组:是否可以保证合并(优化)为一个副本?

时间:2018-02-06 06:05:35

标签: c++ c++11

[读者注意:作为对这个问题的降级评论的建议,我添加了这个注意事项:不要将这个问题的任何部分作为真实陈述:我提出了问题,部分是因为一些我的知识不正确。因此,部分或全部问题本身可能不正确。为了保持原始问题的整体性,为了说明我错误的原因,我决定只添加此通知,并保留原始问题。]

在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.cppbar.cpp有自己的Arr[]。它们会被合并(优化)成一个副本吗?

1 个答案:

答案 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.cppfoo.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.obar.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可执行文件。