Tại sao các thành viên dữ liệu tĩnh phải được định nghĩa bên ngoài lớp riêng trong C ++ (không giống như Java)?


41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Tôi không thấy cần phải A::xxác định riêng trong tệp .cpp (hoặc cùng tệp cho mẫu). Tại sao không thể A::xkhai báo và định nghĩa cùng một lúc?

Nó đã bị cấm vì lý do lịch sử?

Câu hỏi chính của tôi là, nó có ảnh hưởng đến bất kỳ chức năng nào không nếu staticcác thành viên dữ liệu được khai báo / xác định cùng một lúc (giống như Java )?


Như một cách thực hành tốt nhất, nói chung tốt hơn là bọc biến tĩnh của bạn trong một phương thức tĩnh (có thể là tĩnh cục bộ) để tránh các vấn đề về thứ tự khởi tạo.
Tamás Szelei

2
Quy tắc này thực sự được nới lỏng một chút trong C ++ 11. const thành viên tĩnh thường không phải được xác định nữa. Xem: en.wikipedia.org/wiki/
Kẻ

4
@afishwhoswimsaround: Chỉ định các quy tắc chung cho tất cả các tình huống không phải là một ý tưởng hay (nên áp dụng các thực tiễn tốt nhất với ngữ cảnh). Ở đây bạn đang cố gắng giải quyết một vấn đề không tồn tại. Vấn đề thứ tự khởi tạo chỉ ảnh hưởng đến đối tượng có hàm tạo và truy cập các đối tượng thời lượng lưu trữ tĩnh khác. Vì 'x' là int nên lần đầu tiên không áp dụng vì 'x' là riêng tư, lần thứ hai không áp dụng. Thứ ba, điều này không liên quan gì đến câu hỏi.
Martin York

1
Thuộc về Stack Overflow?
Cuộc đua nhẹ nhàng với Monica

2
C ++ 17 cho phép khởi tạo nội tuyến các thành viên dữ liệu tĩnh (ngay cả đối với các loại không nguyên) : inline static int x[] = {1, 2, 3};. Xem en.cppreference.com/w/cpp/lingu/static#Static_data_members
Vladimir Reshetnikov

Câu trả lời:


15

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ị atrong một tệp a.pasnhư 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 .gpitệp này được tạo bởi trình biên dịch cùng với các .otệp khi mỗi mô-đun được biên dịch. Vì vậy, biểu tượng toàn cầu MyStaticVariablechỉ đượ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.


42

Vì các thành viên tĩnh được chia sẻ giữa TẤT CẢ các thể hiện của một lớp, chúng phải được định nghĩa ở một và chỉ một nơi. Thực sự, chúng là các biến toàn cầu với một số hạn chế truy cập.

Nếu bạn cố gắng xác định chúng trong tiêu đề, chúng sẽ được xác định trong mọi mô-đun bao gồm tiêu đề đó và bạn sẽ gặp lỗi trong khi liên kết vì nó tìm thấy tất cả các định nghĩa trùng lặp.

Vâng, đây ít nhất là một phần của một vấn đề lịch sử có từ thời kỳ trước; một trình biên dịch có thể được viết để tạo ra một loại "static_members_of_everything.cpp" ẩn và liên kết đến đó. Tuy nhiên, nó sẽ phá vỡ tính tương thích ngược và sẽ không có lợi ích thực sự nào khi làm như vậy.


2
Câu hỏi của tôi không phải là lý do cho hành vi hiện tại, mà là sự biện minh cho ngữ pháp ngôn ngữ như vậy. Nói cách khác, giả sử nếu staticcác biến được khai báo / định nghĩa tại cùng một vị trí (như Java) thì điều gì có thể sai?
iammilind

8
@iammilind Tôi nghĩ bạn không hiểu rằng ngữ pháp là cần thiết vì giải thích câu trả lời này. Bây giờ tại sao? Do mô hình biên dịch của C (và C ++): các tệp c và cpp là tệp mã thực được biên dịch riêng như các chương trình riêng biệt, sau đó chúng được liên kết với nhau để thực hiện đầy đủ. Các tiêu đề không thực sự là mã cho trình biên dịch, chúng chỉ là văn bản để sao chép và dán bên trong các tệp c và cpp. Bây giờ nếu một cái gì đó được định nghĩa nhiều lần, nó không thể biên dịch nó, giống như cách nó sẽ không biên dịch nếu bạn có một vài biến cục bộ có cùng tên.
Klaim

1
@Klaim, còn staticcác thành viên trong template? Chúng được cho phép trong tất cả các tệp tiêu đề khi chúng cần hiển thị. Tôi không tranh luận câu trả lời này, nhưng nó cũng không phù hợp với câu hỏi của tôi.
iammilind

Các mẫu @iammilind không phải là mã thực, chúng là mã tạo mã. Mỗi phiên bản của một mẫu có một và chỉ một phiên bản tĩnh của mỗi khai báo tĩnh được cung cấp bởi trình biên dịch. Bạn vẫn phải xác định thể hiện nhưng như bạn xác định một mẫu của một thể hiện, nó không phải là mã thực, như đã nói ở trên. Các mẫu theo nghĩa đen là các mẫu mã cho trình biên dịch để tạo mã.
Klaim

2
@iammilind: Các mẫu thường được khởi tạo trong mọi tệp đối tượng, bao gồm các biến tĩnh của chúng. Trên Linux với các tệp đối tượng ELF, trình biên dịch đánh dấu các phần khởi tạo là các ký hiệu yếu , điều đó có nghĩa là trình liên kết kết hợp nhiều bản sao của cùng một phần khởi tạo. Công nghệ tương tự có thể được sử dụng để cho phép xác định các biến tĩnh trong các tệp tiêu đề, vì vậy lý do nó không được thực hiện có lẽ là sự kết hợp giữa các lý do lịch sử và các cân nhắc về hiệu suất biên dịch. Toàn bộ mô hình biên dịch sẽ hy vọng được sửa chữa khi tiêu chuẩn C ++ tiếp theo kết hợp các mô-đun .
han

6

Lý do có thể xảy ra cho điều này là vì điều này giữ cho ngôn ngữ C ++ có thể thực hiện được trong các môi trường nơi tệp đối tượng và mô hình liên kết không hỗ trợ việc hợp nhất nhiều định nghĩa từ nhiều tệp đối tượng.

Một khai báo lớp (được gọi là khai báo vì lý do chính đáng) được kéo vào nhiều đơn vị dịch thuật. Nếu khai báo chứa các định nghĩa cho các biến tĩnh, thì bạn sẽ kết thúc với nhiều định nghĩa trong nhiều đơn vị dịch (Và hãy nhớ rằng, các tên này có liên kết bên ngoài.)

Tình huống đó là có thể, nhưng yêu cầu trình liên kết xử lý nhiều định nghĩa mà không phàn nàn.

(Và lưu ý rằng điều này mâu thuẫn với Quy tắc Một Định nghĩa, trừ khi nó có thể được thực hiện theo loại biểu tượng hoặc loại phần được đặt trong đó.)


6

Có một sự khác biệt lớn giữa C ++ và Java.

Java hoạt động trên máy ảo của riêng mình, tạo mọi thứ vào môi trường thời gian chạy của chính nó. Nếu một định nghĩa xảy ra được nhìn thấy nhiều lần, sẽ chỉ đơn giản là hành động trên cùng một đối tượng mà môi trường thời gian chạy mà ultimatelly biết.

Trong C ++, không có "chủ sở hữu tri thức tối thượng": C ++, C, Fortran Pascal, v.v ... tất cả đều là "người dịch" từ mã nguồn (tệp CPP) sang định dạng trung gian (tệp OBJ hoặc tệp ".o", tùy thuộc vào HĐH) trong đó các câu lệnh được dịch thành hướng dẫn máy và tên trở thành địa chỉ gián tiếp được trung gian bởi một bảng ký hiệu.

Một chương trình không phải do trình biên dịch tạo ra, mà bởi một chương trình khác ("trình liên kết"), kết hợp tất cả các OBJ với nhau (bất kể ngôn ngữ chúng đến từ đâu) bằng cách trỏ lại tất cả các địa chỉ hướng tới các ký hiệu, hướng tới định nghĩa hiệu quả.

Theo cách liên kết hoạt động, một định nghĩa (cái tạo ra không gian vật lý cho một biến) phải là duy nhất.

Lưu ý rằng C ++ không tự liên kết và trình liên kết không được cung cấp bởi thông số kỹ thuật C ++: trình liên kết tồn tại do cách các mô-đun hệ điều hành được xây dựng (thường là trong C và ASM). C ++ phải sử dụng nó theo cách của nó.

Bây giờ: một tệp tiêu đề là một cái gì đó được "dán vào" một số tệp CPP. Mỗi tệp CPP được dịch độc lập với nhau. Một trình biên dịch dịch các tệp CPP khác nhau, tất cả các nhận trong cùng một định nghĩa sẽ đặt " mã tạo " cho đối tượng được xác định trong tất cả các OBJ kết quả.

Trình biên dịch không biết (và sẽ không bao giờ biết) nếu tất cả các OBJ đó sẽ được sử dụng cùng nhau để tạo thành một chương trình duy nhất hoặc riêng biệt để tạo thành các chương trình độc lập khác nhau.

Trình liên kết không biết làm thế nào và tại sao các định nghĩa tồn tại và chúng đến từ đâu (thậm chí nó không biết về C ++: mọi "ngôn ngữ tĩnh" có thể tạo ra các định nghĩa và tham chiếu được liên kết). Nó chỉ biết có các tham chiếu đến một "biểu tượng" nhất định được "xác định" tại một địa chỉ kết quả nhất định.

Nếu có nhiều định nghĩa (không nhầm lẫn định nghĩa với tham chiếu) cho một biểu tượng nhất định, trình liên kết không có kiến ​​thức (là bất khả tri ngôn ngữ) về việc phải làm gì với chúng.

Nó giống như hợp nhất một số thành phố để tạo thành một thị trấn lớn: nếu bạn thấy có hai " Quảng trường thời gian " và một số người từ bên ngoài yêu cầu đến " Quảng trường thời gian ", bạn không thể quyết định trên cơ sở kỹ thuật thuần túy (không có bất kỳ kiến ​​thức nào về chính trị đã gán những cái tên đó và sẽ chịu trách nhiệm quản lý chúng) ở nơi chính xác để gửi chúng.


3
Sự khác biệt giữa Java và C ++ đối với các ký hiệu toàn cục không được kết nối với Java có máy ảo, mà là với mô hình biên dịch C ++. Về mặt này, tôi sẽ không đặt Pascal và C ++ vào cùng một danh mục. Thay vào đó, tôi sẽ nhóm C và C ++ lại với nhau thành "các ngôn ngữ trong đó các khai báo đã nhập được bao gồm và được biên dịch cùng với tệp nguồn chính" trái ngược với Java và Pascal (và có thể là OCaml, Scala, Ada, v.v.) khai báo nhập khẩu được trình biên dịch tra cứu trong các tệp được biên dịch trước có chứa thông tin về các ký hiệu được xuất ".
Giorgio

1
@Giorgio: tham chiếu đến Java có thể không được hoan nghênh, nhưng tôi nghĩ rằng câu trả lời của Emilio chủ yếu là đúng bằng cách đi đến ý chính của vấn đề, cụ thể là giai đoạn tệp / liên kết đối tượng sau khi biên dịch riêng biệt.
ixache

5

Nó được yêu cầu bởi vì nếu không trình biên dịch không biết đặt biến ở đâu. Mỗi tệp cpp được biên dịch riêng và không biết về cái khác. Trình liên kết giải quyết các biến, hàm, v.v. Cá nhân tôi không thấy sự khác biệt giữa các thành viên vtable và tĩnh (chúng ta không phải chọn tập tin vtable được xác định trong).

Tôi chủ yếu cho rằng các nhà văn biên dịch dễ dàng thực hiện theo cách đó. Các vars tĩnh bên ngoài lớp / struct tồn tại và có lẽ vì lý do nhất quán hoặc bởi vì nó sẽ 'dễ thực hiện hơn' đối với các trình soạn thảo trình biên dịch mà họ đã xác định hạn chế đó trong các tiêu chuẩn.


2

Tôi nghĩ rằng tôi đã tìm thấy lý do. Xác định staticbiến trong không gian riêng biệt cho phép khởi tạo nó thành bất kỳ giá trị nào. Nếu không được khởi tạo thì nó sẽ được mặc định là 0.

Trước C ++ 11, việc khởi tạo trong lớp không được phép trong C ++. Vì vậy, người ta không thể viết như:

struct X
{
  static int i = 4;
};

Vì vậy, bây giờ để khởi tạo biến người ta phải viết nó bên ngoài lớp là:

struct X
{
  static int i;
};
int X::i = 4;

Như đã thảo luận trong các câu trả lời khác, int X::ibây giờ là toàn cầu và khai báo toàn cầu trong nhiều tệp gây ra nhiều lỗi liên kết biểu tượng.

Do đó, người ta phải khai báo một staticbiến lớp bên trong một đơn vị dịch thuật riêng biệt. Tuy nhiên, vẫn có thể lập luận rằng cách sau đây sẽ hướng dẫn trình biên dịch không tạo nhiều biểu tượng

static int X::i = 4;
^^^^^^

0

A :: x chỉ là một biến toàn cục nhưng không gian tên thành A và với các hạn chế truy cập.

Ai đó vẫn phải khai báo nó, giống như bất kỳ biến toàn cục nào khác, và điều đó thậm chí có thể được thực hiện trong một dự án được liên kết tĩnh với dự án chứa phần còn lại của mã A.

Tôi sẽ gọi tất cả các thiết kế xấu này, nhưng có một vài tính năng bạn có thể khai thác theo cách này:

  1. Lệnh gọi của hàm tạo ... Không quan trọng đối với một int, nhưng đối với một thành viên phức tạp hơn có thể truy cập các biến tĩnh hoặc toàn cục khác, nó có thể rất quan trọng.

  2. trình khởi tạo tĩnh - bạn có thể để khách hàng quyết định A :: x nên được khởi tạo.

  3. trong c ++ và c, vì bạn có toàn quyền truy cập vào bộ nhớ thông qua các con trỏ, vị trí vật lý của các biến là rất quan trọng. Có những thứ rất nghịch ngợm mà bạn có thể khai thác dựa trên vị trí của một biến trong một đối tượng liên kết.

Tôi nghi ngờ đây là "tại sao" tình huống này đã phát sinh. Đây có lẽ chỉ là một sự tiến hóa của C biến thành C ++ và vấn đề tương thích ngược khiến bạn không thể thay đổi ngôn ngữ ngay bây giờ.


2
điều này dường như không cung cấp bất cứ điều gì đáng kể qua các điểm được thực hiện và giải thích trong 6 câu trả lời trước
gnat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.