Я думаю, что ограничение, которое вы рассмотрели, связано не с семантикой (почему что-то должно измениться, если инициализация была определена в том же файле?), А с моделью компиляции 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.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
Хитрость здесь в том , что когда компилятор видит использование директивы в программном модуле (например , использует в b.pas), она не включает в себя соответствующий .pas файл, но выглядит для .gpi файла, т.е. для предварительно скомпилированных файл интерфейса (см. документацию ). Эти .gpi
файлы генерируются компилятором вместе с .o
файлами при компиляции каждого модуля. Таким образом, глобальный символ MyStaticVariable
определяется только один раз в объектном файле a.o
.
Java работает аналогичным образом: когда компилятор затем импортирует класс A в класс B, он ищет файл класса для A и не нуждается в этом файле A.java
. Таким образом, все определения и инициализации для класса A могут быть помещены в один исходный файл.
Возвращаясь к C ++, причина, по которой в C ++ вы должны определять статические элементы данных в отдельном файле, больше связана с моделью компиляции C ++, чем с ограничениями, налагаемыми компоновщиком или другими инструментами, используемыми компилятором. В C ++ импорт некоторых символов означает создание их объявления как части текущего модуля компиляции. Это очень важно, среди прочего, из-за способа компиляции шаблонов. Но это означает, что вы не можете / не должны определять какие-либо глобальные символы (функции, переменные, методы, члены статических данных) во включаемом файле, в противном случае эти символы могут быть многократно определены в скомпилированных объектных файлах.