Khai báo biến trong tệp tiêu đề - tĩnh hay không?


91

Khi cấu trúc lại một số, #definestôi gặp các khai báo tương tự như sau trong tệp tiêu đề C ++:

static const unsigned int VAL = 42;
const unsigned int ANOTHER_VAL = 37;

Câu hỏi đặt ra là, sự khác biệt, nếu có, thì static sẽ tạo ra gì? Lưu ý rằng không thể bao gồm nhiều tiêu đề do #ifndef HEADER #define HEADER #endifthủ thuật cổ điển (nếu điều đó quan trọng).

Liệu tĩnh có nghĩa là chỉ một bản sao của VALđược tạo ra, trong trường hợp tiêu đề được bao gồm bởi nhiều hơn một tệp nguồn?


Câu trả lời:


107

staticnghĩa là sẽ có một bản sao VALđược tạo cho mỗi tệp nguồn mà nó được bao gồm trong đó. Nhưng điều đó cũng có nghĩa là nhiều phần bao gồm sẽ không dẫn đến nhiều định nghĩa về VALđiều đó sẽ xung đột tại thời điểm liên kết. Trong C, nếu không có nó, staticbạn sẽ cần đảm bảo rằng chỉ có một tệp nguồn được xác định VALtrong khi các tệp nguồn khác khai báo nó extern. Thông thường người ta sẽ làm điều này bằng cách định nghĩa nó (có thể bằng bộ khởi tạo) trong tệp nguồn và đặt externkhai báo trong tệp tiêu đề.

static các biến ở cấp độ toàn cầu chỉ hiển thị trong tệp nguồn của chính chúng cho dù chúng đến đó thông qua bao gồm hay trong tệp chính.


Lưu ý của người biên tập: Trong C ++, constcác đối tượng không có statichoặc externtừ khóa trong khai báo của chúng là ngầm định static.


Tôi là một fan hâm mộ của câu cuối cùng, vô cùng hữu ích. Tôi đã không bỏ phiếu cho câu trả lời 'vì 42 là tốt hơn. chỉnh sửa: văn phạm
RealDeal_EE'18

"Tĩnh có nghĩa là sẽ có một bản sao của VAL được tạo cho mỗi tệp nguồn mà nó được đưa vào." Điều đó dường như ngụ ý rằng sẽ có hai bản sao của VAL nếu hai tệp nguồn bao gồm tệp tiêu đề. Tôi hy vọng điều đó không đúng và luôn có một phiên bản VAL, bất kể có bao nhiêu tệp bao gồm tiêu đề.
Brent212,

4
@ Brent212 Trình biên dịch không biết liệu một khai báo / định nghĩa đến từ tệp tiêu đề hay tệp chính. Vì vậy, bạn hy vọng vô ích. Sẽ có hai bản sao của VAL nếu ai đó ngớ ngẩn và đưa một định nghĩa tĩnh vào tệp tiêu đề và nó được đưa vào hai nguồn.
Justsalt

1
giá trị const có mối liên kết nội bộ trong C ++
adrianN

112

Các thẻ staticexterntrên các biến phạm vi tệp xác định liệu chúng có thể truy cập được trong các đơn vị dịch khác (tức là khác .choặc .cpptệp) hay không.

  • staticcung cấp liên kết nội bộ có thể thay đổi, ẩn nó khỏi các đơn vị dịch khác. Tuy nhiên, các biến có liên kết nội bộ có thể được xác định trong nhiều đơn vị dịch.

  • externcung cấp cho liên kết bên ngoài có thể thay đổi, làm cho nó hiển thị với các đơn vị dịch khác. Thông thường, điều này có nghĩa là biến chỉ phải được xác định trong một đơn vị dịch.

Mặc định (khi bạn không chỉ định statichoặc extern) là một trong những khu vực mà C và C ++ khác nhau.

  • Trong C, các biến trong phạm vi tệp là extern(liên kết bên ngoài) theo mặc định. Nếu bạn đang sử dụng C, VALis staticANOTHER_VALis extern.

  • Trong C ++, các biến trong phạm vi tệp là static(liên kết nội bộ) theo mặc định nếu có constexterntheo mặc định nếu không. Nếu bạn đang sử dụng C ++, cả hai VALANOTHER_VALđều được static.

Từ bản nháp của đặc điểm kỹ thuật C :

6.2.2 Liên kết của các mã định danh ... -5- Nếu khai báo mã định danh cho một hàm không có mã định danh lớp lưu trữ, thì liên kết của nó được xác định chính xác như thể nó được khai báo với mã định danh lớp lưu trữ bên ngoài. Nếu khai báo mã định danh cho một đối tượng có phạm vi tệp và không có mã định danh lớp lưu trữ, thì liên kết của nó là bên ngoài.

Từ bản nháp của đặc tả C ++ :

7.1.1 - Bộ chỉ định lớp lưu trữ [dcl.stc] ... -6- Tên được khai báo trong phạm vi không gian tên mà không có bộ chỉ định lớp lưu trữ có liên kết bên ngoài trừ khi nó có liên kết nội bộ do đã khai báo trước đó và với điều kiện là không khai báo const. Các đối tượng được khai báo const và không được khai báo rõ ràng extern có liên kết nội bộ.


47

Tính năng tĩnh sẽ có nghĩa là bạn nhận được một bản sao cho mỗi tệp, nhưng không giống như những người khác đã nói rằng làm như vậy là hoàn toàn hợp pháp. Bạn có thể dễ dàng kiểm tra điều này với một mẫu mã nhỏ:

test.h:

static int TEST = 0;
void test();

test1.cpp:

#include <iostream>
#include "test.h"

int main(void) {
    std::cout << &TEST << std::endl;
    test();
}

test2.cpp:

#include <iostream>
#include "test.h"

void test() {
    std::cout << &TEST << std::endl;
}

Chạy nó cung cấp cho bạn kết quả này:

0x446020
0x446040


5
Cảm ơn vì ví dụ!
Kyrol

Tôi tự hỏi nếu TESTconst, nếu LTO sẽ có thể tối ưu hóa nó vào một vị trí bộ nhớ duy nhất. Nhưng -O3 -fltoGCC 8.1 thì không.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Sẽ là bất hợp pháp nếu nó làm như vậy - ngay cả khi nó không đổi, static đảm bảo rằng mỗi phiên bản là cục bộ của đơn vị biên dịch. Mặc dù vậy, nó có thể nội dòng giá trị hằng số nếu được sử dụng như một hằng số, nhưng vì chúng ta lấy địa chỉ của nó nên nó phải trả về một con trỏ duy nhất.
slicedlime

6

constcác biến trong C ++ có liên kết nội bộ. Vì vậy, sử dụng statickhông có tác dụng.

Ah

const int i = 10;

one.cpp

#include "a.h"

func()
{
   cout << i;
}

hai.cpp

#include "a.h"

func1()
{
   cout << i;
}

Nếu đây là chương trình C, bạn sẽ gặp lỗi 'nhiều định nghĩa' cho i(do liên kết bên ngoài).


2
Chà, việc sử dụng staticcó tác dụng là nó báo hiệu một cách gọn gàng ý định và nhận thức về những gì người ta đang mã hóa, điều này không bao giờ là một điều xấu. Đối với tôi, điều này giống như bao gồm virtualkhi ghi đè: chúng ta không cần phải làm như vậy, nhưng mọi thứ trông trực quan hơn rất nhiều - và nhất quán với các khai báo khác - khi chúng ta làm vậy.
underscore_d

Bạn có thể gặp lỗi nhiều định nghĩa trong C. Đây là hành vi không xác định mà không cần chẩn đoán
MM

5

Khai báo tĩnh ở cấp mã này có nghĩa là biến thể chỉ hiển thị trong đơn vị biên dịch hiện tại. Điều này có nghĩa là chỉ mã trong mô-đun đó mới thấy biến đó.

nếu bạn có tệp tiêu đề khai báo một biến tĩnh và tiêu đề đó được bao gồm trong nhiều tệp C / CPP, thì biến đó sẽ là "cục bộ" cho các mô-đun đó. Sẽ có N bản sao của biến đó cho N vị trí mà tiêu đề được đưa vào. Chúng không liên quan đến nhau chút nào. Bất kỳ mã nào trong bất kỳ tệp nguồn nào trong số đó sẽ chỉ tham chiếu đến biến được khai báo trong mô-đun đó.

Trong trường hợp cụ thể này, từ khóa 'tĩnh' dường như không mang lại bất kỳ lợi ích nào. Tôi có thể thiếu một cái gì đó, nhưng nó dường như không quan trọng - tôi chưa bao giờ thấy bất cứ điều gì được làm như thế này trước đây.

Đối với nội tuyến, trong trường hợp này biến có thể được nội tuyến, nhưng đó chỉ vì nó được khai báo là const. Trình biên dịch có thể có nhiều khả năng nội tuyến các biến tĩnh của mô-đun, nhưng điều đó phụ thuộc vào tình huống và mã được biên dịch. Không có gì đảm bảo rằng trình biên dịch sẽ nội tuyến 'tĩnh'.


Lợi ích của 'static' ở đây là nếu không, bạn đang khai báo nhiều hình cầu có cùng tên, một hình cầu cho mỗi mô-đun bao gồm tiêu đề. Nếu người liên kết không phàn nàn thì đó chỉ vì nó đang cắn vào lưỡi và lịch sự.

Trong trường hợp này, do const, staticđược ngụ ý và do đó tùy chọn. Hệ quả là không dễ mắc phải nhiều lỗi định nghĩa như Mike F đã tuyên bố.
underscore_d


2

Để trả lời câu hỏi, "tĩnh có nghĩa là chỉ một bản sao của VAL được tạo ra, trong trường hợp tiêu đề được bao gồm bởi nhiều hơn một tệp nguồn?" ...

KHÔNG . VAL sẽ luôn được định nghĩa riêng biệt trong mọi tệp bao gồm tiêu đề.

Các tiêu chuẩn cho C và C ++ thực sự gây ra sự khác biệt trong trường hợp này.

Trong C, các biến có phạm vi tệp là ngoại lệ theo mặc định. Nếu bạn đang sử dụng C, VAL là tĩnh và ANOTHER_VAL là ngoài.

Lưu ý rằng trình liên kết Hiện đại có thể phàn nàn về ANOTHER_VAL nếu tiêu đề được bao gồm trong các tệp khác nhau (cùng một tên chung được xác định hai lần) và chắc chắn sẽ khiếu nại nếu ANOTHER_VAL được khởi tạo thành một giá trị khác trong tệp khác

Trong C ++, các biến phạm vi tệp là tĩnh theo mặc định nếu chúng là const, và extern theo mặc định nếu chúng không phải. Nếu bạn đang sử dụng C ++, cả VAL và ANOTHER_VAL đều tĩnh.

Bạn cũng cần tính đến thực tế là cả hai biến đều được chỉ định là const. Lý tưởng nhất là trình biên dịch sẽ luôn chọn nội tuyến các biến này và không bao gồm bất kỳ bộ nhớ nào cho chúng. Có rất nhiều lý do tại sao có thể phân bổ bộ nhớ. Những người tôi có thể nghĩ đến ...

  • tùy chọn gỡ lỗi
  • địa chỉ được lấy trong tệp
  • trình biên dịch luôn phân bổ lưu trữ (các kiểu const phức tạp không thể dễ dàng được nội dòng, vì vậy sẽ trở thành một trường hợp đặc biệt cho các kiểu cơ bản)

Lưu ý: Trong máy trừu tượng có một bản sao của VAL trong mỗi đơn vị dịch riêng biệt bao gồm tiêu đề. Trong thực tế, trình liên kết có thể quyết định kết hợp chúng bằng cách nào, và trình biên dịch có thể tối ưu hóa một số hoặc tất cả chúng trước.
MM

1

Giả sử rằng các khai báo này ở phạm vi toàn cục (nghĩa là không phải là biến thành viên), thì:

static có nghĩa là 'liên kết nội bộ'. Trong trường hợp này, vì nó được khai báo là const nên trình biên dịch có thể tối ưu hóa / nội tuyến. Nếu bạn bỏ qua const thì trình biên dịch phải cấp phát bộ nhớ trong mỗi đơn vị biên dịch.

Bằng cách bỏ qua tĩnh , liên kết là ngoại lệ theo mặc định. Một lần nữa, bạn đã được lưu bởi const ness - trình biên dịch có thể tối ưu hóa / sử dụng nội tuyến. Nếu bạn bỏ const thì bạn sẽ nhận được lỗi nhân các ký hiệu được xác định tại thời điểm liên kết.


Tôi tin rằng trình biên dịch phải cấp phát không gian cho một const int trong mọi trường hợp, vì một mô-đun khác luôn có thể nói "extern const int anything; something (& anything);"

1

Bạn cũng không thể khai báo một biến static mà không xác định nó (điều này là do các phần tử sửa đổi lớp lưu trữ static và extern loại trừ lẫn nhau). Một biến tĩnh có thể được xác định trong tệp tiêu đề, nhưng điều này sẽ khiến mỗi tệp nguồn bao gồm tệp tiêu đề có bản sao riêng của biến, có thể không phải như dự định.


"... nhưng điều này sẽ khiến mỗi tệp nguồn bao gồm tệp tiêu đề có bản sao riêng của biến, có thể không phải như dự định." - Do lỗi trật tự khởi tạo tĩnh , có thể yêu cầu một bản sao trong mỗi đơn vị dịch.
jww

1

hăng sô biến theo mặc định là tĩnh trong C ++, nhưng ngoại lệ C. Vì vậy, nếu bạn sử dụng C ++, điều này không có nghĩa là sử dụng cấu trúc nào.

(7.11.6 C ++ 2003 và Apexndix C có các mẫu)

Ví dụ trong so sánh các nguồn biên dịch / liên kết như chương trình C và C ++:

bruziuz:~/test$ cat a.c
const int b = 22;
int main(){return 0;}
bruziuz:~/test$ cat b.c
const int b=2;
bruziuz:~/test$ gcc -x c -std=c89 a.c b.c
/tmp/ccSKKIRZ.o:(.rodata+0x0): multiple definition of `b'
/tmp/ccDSd0V3.o:(.rodata+0x0): first defined here
collect2: error: ld returned 1 exit status
bruziuz:~/test$ gcc -x c++ -std=c++03 a.c b.c 
bruziuz:~/test$ 
bruziuz:~/test$ gcc --version | head -n1
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609

có ý nghĩa trong vẫn bao gồm static. Nó báo hiệu ý định / nhận thức về những gì lập trình viên đang làm và duy trì tính ngang bằng với các kiểu khai báo khác (và, fwiw, C) thiếu ẩn ý static. Nó giống như bao gồm virtualvà gần đây overridetrong các khai báo của các hàm ghi đè - không cần thiết, nhưng tự ghi lại nhiều hơn và, trong trường hợp sau, có lợi cho phân tích tĩnh.
underscore_d

Tôi hoàn toàn đồng ý. Ví dụ như đối với tôi trong cuộc sống thực, tôi luôn viết nó một cách rõ ràng.
bruziuz

"Vì vậy, nếu bạn sử dụng C ++, điều này không có nghĩa là sử dụng cấu trúc nào ..." - Hmm ... Tôi vừa biên dịch một dự án constchỉ sử dụng trên một biến trong tiêu đề với g++ (GCC) 7.2.1 20170915 (Red Hat 7.2.1-2). Nó dẫn đến khoảng 150 ký hiệu được nhân lên (một cho mỗi đơn vị dịch mà tiêu đề được bao gồm). Tôi nghĩ chúng ta cần một trong hai static, inlinehoặc một ẩn danh / namespace giấu tên để tránh sự liên kết bên ngoài.
jww

Tôi đã thử baby-example với gcc-5.4 với khai báo const intbên trong phạm vi không gian tên và trong không gian tên chung. Và nó được biên dịch và tuân theo quy tắc "Các đối tượng được khai báo const và không được khai báo rõ ràng extern có liên kết nội bộ." ".... Có thể trong dự án vì lý do nào đó mà tiêu đề này được đưa vào các nguồn được biên dịch C, nơi các quy tắc hoàn toàn khác.
bruziuz

@jww Tôi đã tải lên ví dụ với vấn đề liên kết với C và không có vấn đề đối với C ++
bruziuz

0

Static ngăn không cho một đơn vị biên dịch khác sử dụng biến đó để trình biên dịch có thể chỉ "nội dòng" giá trị của biến nơi nó được sử dụng và không tạo bộ nhớ lưu trữ cho nó.

Trong ví dụ thứ hai của bạn, trình biên dịch không thể giả định rằng một số tệp nguồn khác sẽ không lưu trữ nó, vì vậy nó thực sự phải lưu trữ giá trị đó trong bộ nhớ ở đâu đó.


-2

Tĩnh ngăn trình biên dịch thêm nhiều trường hợp. Điều này trở nên ít quan trọng hơn với bảo vệ #ifndef, nhưng giả sử tiêu đề được bao gồm trong hai thư viện riêng biệt và ứng dụng được liên kết, hai phiên bản sẽ được đưa vào.


giả sử rằng theo "thư viện" bạn có nghĩa là đơn vị dịch , thì không, bao gồm bảo vệ hoàn toàn không làm gì để ngăn chặn nhiều định nghĩa, vì chúng chỉ bảo vệ chống lại các bao gồm lặp lại trong cùng một đơn vị dịch . vì vậy, họ không làm bất cứ điều gì để làm cho static"ít quan trọng hơn". và thậm chí với cả hai, bạn có thể kết thúc với nhiều định nghĩa được liên kết nội bộ, mà có thể là không có ý định.
underscore_d
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.