Я думаю, що обмеження, яке ви розглянули, не пов'язане з семантикою (чому щось має змінюватися, якщо ініціалізація була визначена в одному файлі?), А скоріше з компіляційною моделлю 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
Примітка тут полягає в тому, що коли компілятор бачить директиву використання в програмному модулі (наприклад, використовує b.pas), він не включає відповідний файл .pas, а шукає .gpi-файл, тобто попередньо скомпільований файл інтерфейсу (див . документацію ). Ці .gpi
файли генеруються компілятором разом з .o
файлами при складанні кожного модуля. Отже глобальний символ MyStaticVariable
визначається лише один раз у об’єктному файлі a.o
.
Java працює аналогічно: коли компілятор імпортує клас A до класу B, він переглядає файл класу для A і не потребує цього файлу A.java
. Таким чином, усі визначення та ініціалізації для класу A можна помістити в один вихідний файл.
Повертаючись до C ++, причина, чому в C ++ ви повинні визначати статичні члени даних в окремому файлі, більше пов'язана з моделлю компіляції C ++, ніж з обмеженнями, накладеними лінкером або іншими інструментами, які використовує компілятор. У C ++ імпорт деяких символів означає побудувати їх декларацію як частину поточної одиниці компіляції. Це дуже важливо, серед іншого, через спосіб складання шаблонів. Але це означає, що ви не можете / не повинні визначати жодних глобальних символів (функцій, змінних, методів, статичних членів даних) у включеному файлі, інакше ці символи можуть бути багатозначно визначені у складених файлах об'єктів.