我认为您所考虑的限制与语义无关(如果初始化是在同一文件中定义的,为什么要更改某些内容?)而是与C ++编译模型有关,由于向后兼容性,该模型无法轻易更改,因为它会要么变得太复杂(同时支持新的编译模型和现有的编译模型),要么不允许编译现有的代码(通过引入新的编译模型并删除现有的编译模型)。
C ++编译模型源于C的编译模型,在C中,您可以通过包含(头)文件将声明导入到源文件中。这样,编译器可以递归地看到一个大的源文件,其中包含所有包含的文件以及这些文件中包含的所有文件。这具有IMO的一大优势,即它使编译器更易于实现。当然,您可以在包含的文件中编写任何内容,即声明和定义。将声明放在头文件中并将定义放在.c或.cpp文件中只是一个好习惯。
另一方面,可能有一个编译模型,在该模型中,编译器非常了解是要导入在另一个模块中定义的全局符号的声明,还是正在编译由以下模块提供的全局符号的定义:当前模块。仅在后一种情况下,编译器必须将此符号(例如,变量)放入当前目标文件中。
例如,在GNU Pascal中,您可以a
在如下文件中写入一个单位a.pas
:
unit a;
interface
var MyStaticVariable: Integer;
implementation
begin
MyStaticVariable := 0
end.
在同一源文件中声明和初始化全局变量的位置。
然后,您可以具有导入a并使用全局变量的不同单位
MyStaticVariable
,例如单位b(b.pas
):
unit b;
interface
uses a;
procedure PrintB;
implementation
procedure PrintB;
begin
Inc(MyStaticVariable);
WriteLn(MyStaticVariable)
end;
end.
和单位c(c.pas
):
unit c;
interface
uses a;
procedure PrintC;
implementation
procedure PrintC;
begin
Inc(MyStaticVariable);
WriteLn(MyStaticVariable)
end;
end.
最后,您可以在主程序中使用单位b和c m.pas
:
program M;
uses b, c;
begin
PrintB;
PrintC;
PrintB
end.
您可以分别编译以下文件:
$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas
然后使用以下命令生成可执行文件:
$ gpc -o m m.o a.o b.o c.o
并运行它:
$ ./m
1
2
3
这里的窍门是,当编译器在程序模块中看到一个uses指令(例如,在b.pas中使用a)时,它不包括相应的.pas文件,而是寻找一个.gpi文件,即预编译的文件。接口文件(请参阅文档)。这些.gpi
文件由编译器与.o
每个模块一起编译时生成。因此,全局符号MyStaticVariable
仅在目标文件中定义一次a.o
。
Java以类似的方式工作:当编译器将类A导入类B时,它会在类文件中查找A而不需要该文件A.java
。因此,可以将A类的所有定义和初始化都放在一个源文件中。
回到C ++,为什么在C ++中必须在单独的文件中定义静态数据成员的原因,与C ++编译模型的关系更多,而不是与链接器或编译器使用的其他工具施加的限制有关。在C ++中,导入一些符号意味着将其声明构建为当前编译单元的一部分。除其他外,这非常重要,因为模板的编译方式。但这意味着您不能/不应该在包含的文件中定义任何全局符号(函数,变量,方法,静态数据成员),否则这些符号可以在编译的目标文件中被多重定义。