Tôi nghĩ rằng giới hạn mà bạn đã xem xét không liên quan đến ngữ nghĩa (tại sao phải thay đổi nếu khởi tạo được xác định trong cùng một tệp?) Mà là với mô hình biên dịch C ++, vì lý do tương thích ngược, có thể dễ dàng thay đổi vì nó sẽ bị thay đổi hoặc trở nên quá phức tạp (hỗ trợ một mô hình biên dịch mới và mô hình hiện có cùng một lúc) hoặc sẽ không cho phép biên dịch mã hiện có (bằng cách giới thiệu một mô hình biên dịch mới và loại bỏ mô hình hiện có).
Mô hình biên dịch C ++ bắt nguồn từ mô hình của C, trong đó bạn nhập khai báo vào tệp nguồn bằng cách bao gồm các tệp (tiêu đề). Theo cách này, trình biên dịch sẽ nhìn thấy chính xác một tệp nguồn lớn, chứa tất cả các tệp được bao gồm và tất cả các tệp được bao gồm từ các tệp đó, theo cách đệ quy. Điều này có IMO một lợi thế lớn, cụ thể là nó giúp trình biên dịch dễ thực hiện hơn. Tất nhiên, bạn có thể viết bất cứ điều gì trong các tệp được bao gồm, tức là cả khai báo và định nghĩa. Nó chỉ là một thực hành tốt để đặt khai báo trong các tệp tiêu đề và định nghĩa trong các tệp .c hoặc .cpp.
Mặt khác, có thể có một mô hình biên dịch trong đó trình biên dịch biết rất rõ nếu nó đang nhập khai báo một ký hiệu toàn cục được xác định trong một mô-đun khác hoặc nếu nó đang biên dịch định nghĩa của ký hiệu toàn cục được cung cấp bởi mô-đun hiện tại . Chỉ trong trường hợp sau, trình biên dịch phải đặt ký hiệu này (ví dụ: một biến) trong tệp đối tượng hiện tại.
Ví dụ: trong GNU Pascal, bạn có thể viết một đơn vị a
trong một tệp a.pas
như thế này:
unit a;
interface
var MyStaticVariable: Integer;
implementation
begin
MyStaticVariable := 0
end.
trong đó biến toàn cục được khai báo và khởi tạo trong cùng một tệp nguồn.
Sau đó, bạn có thể có các đơn vị khác nhau nhập a và sử dụng biến toàn cục
MyStaticVariable
, ví dụ: đơn vị b ( b.pas
):
unit b;
interface
uses a;
procedure PrintB;
implementation
procedure PrintB;
begin
Inc(MyStaticVariable);
WriteLn(MyStaticVariable)
end;
end.
và một đơn vị c ( c.pas
):
unit c;
interface
uses a;
procedure PrintC;
implementation
procedure PrintC;
begin
Inc(MyStaticVariable);
WriteLn(MyStaticVariable)
end;
end.
Cuối cùng, bạn có thể sử dụng các đơn vị b và c trong một chương trình chính m.pas
:
program M;
uses b, c;
begin
PrintB;
PrintC;
PrintB
end.
Bạn có thể biên dịch các tệp này một cách riêng biệt:
$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas
và sau đó tạo ra một tệp thực thi với:
$ gpc -o m m.o a.o b.o c.o
và chạy nó:
$ ./m
1
2
3
Mẹo ở đây là khi trình biên dịch thấy lệnh sử dụng trong mô-đun chương trình (ví dụ: sử dụng a trong b.pas), nó không bao gồm tệp .pas tương ứng, nhưng tìm tệp .gpi, nghĩa là được biên dịch trước tập tin giao diện (xem tài liệu ). Các .gpi
tệp này được tạo bởi trình biên dịch cùng với các .o
tệp khi mỗi mô-đun được biên dịch. Vì vậy, biểu tượng toàn cầu MyStaticVariable
chỉ được xác định một lần trong tệp đối tượng a.o
.
Java hoạt động theo cách tương tự: khi đó trình biên dịch nhập một lớp A vào lớp B, nó xem tệp lớp cho A và không cần tệp A.java
. Vì vậy, tất cả các định nghĩa và khởi tạo cho lớp A có thể được đặt trong một tệp nguồn.
Quay trở lại C ++, lý do tại sao trong C ++, bạn phải xác định các thành viên dữ liệu tĩnh trong một tệp riêng có liên quan nhiều hơn đến mô hình biên dịch C ++ hơn là các giới hạn được áp dụng bởi trình liên kết hoặc các công cụ khác được trình biên dịch sử dụng. Trong C ++, nhập một số ký hiệu có nghĩa là xây dựng khai báo của chúng như là một phần của đơn vị biên dịch hiện tại. Điều này rất quan trọng, trong số những thứ khác, bởi vì cách mà các mẫu được biên dịch. Nhưng điều này ngụ ý rằng bạn không thể / không nên xác định bất kỳ ký hiệu toàn cục nào (hàm, biến, phương thức, thành viên dữ liệu tĩnh) trong một tệp được bao gồm, nếu không các ký hiệu này có thể được định nghĩa nhân trong các tệp đối tượng được biên dịch.