Khái niệm về từ khóa tĩnh theo quan điểm của C nhúng


9
static volatile unsigned char   PORTB   @ 0x06;

Đây là một dòng mã trong tệp tiêu đề vi điều khiển PIC. Các @nhà điều hành được sử dụng để lưu trữ các giá trị PORTB bên trong địa chỉ 0x06, đó là một thanh ghi bên trong bộ điều khiển PIC đại diện cho PORTB. Cho đến thời điểm này, tôi có một ý tưởng rõ ràng.

Dòng này được khai báo là biến toàn cục bên trong tệp tiêu đề ( .h). Vì vậy, từ những gì tôi biết về ngôn ngữ C, "biến toàn cục tĩnh" không thể nhìn thấy đối với bất kỳ tệp nào khác - hoặc, đơn giản, các biến / hàm toàn cầu tĩnh không thể được sử dụng bên ngoài tệp hiện tại.

Sau đó, làm thế nào từ khóa này PORTBcó thể hiển thị với tệp nguồn chính của tôi và nhiều tệp tiêu đề khác mà tôi đã tạo thủ công?

Trên tệp nguồn chính của tôi, tôi chỉ thêm tệp tiêu đề #include pic.hĐiều này có liên quan gì đến câu hỏi của tôi không?


2
không có vấn đề gì với câu hỏi nhưng phần SE sai tôi sợ
gommer

static thường được sử dụng bên trong một hàm để xác định rằng biến được tạo một lần và giữ giá trị của nó từ một thực thi của hàm này sang hàm tiếp theo. một biến toàn cục là một biến được tạo bên ngoài bất kỳ chức năng nào để nó có thể nhìn thấy ở mọi nơi. tĩnh toàn cầu không thực sự có ý nghĩa.
Finbarr

8
@Finbarr Sai. statictoàn cầu có thể nhìn thấy bên trong toàn bộ đơn vị biên dịch và không được xuất ra ngoài đó. Họ rất giống privatecác thành viên của một lớp trong OOP. Tức là mọi biến cần được chia sẻ giữa các hàm khác nhau trong một đơn vị biên dịch nhưng không được nhìn thấy bên ngoài mà cu thực sự nênstatic. Điều này cũng làm giảm "tắc nghẽn" không gian tên toàn cầu của chương trình.
JimmyB

2
Re "Toán tử @ được sử dụng để lưu trữ giá trị PORTB bên trong địa chỉ 0x06" . Có thật không? Không giống như "Toán tử @ được sử dụng để lưu trữ biến" PORTB "tại địa chỉ bộ nhớ tuyệt đối 0x06" ?
Peter Mortensen

Câu trả lời:


20

Từ khóa 'tĩnh' trong C có hai nghĩa cơ bản khác nhau.

Phạm vi giới hạn

Trong ngữ cảnh này, 'static' kết hợp với 'extern' để kiểm soát phạm vi của một biến hoặc tên hàm. Tĩnh làm cho tên biến hoặc tên hàm chỉ khả dụng trong một đơn vị biên dịch duy nhất và chỉ khả dụng đối với mã tồn tại sau khai báo / định nghĩa trong văn bản đơn vị biên dịch.

Giới hạn này tự nó thực sự chỉ có nghĩa là một cái gì đó nếu và chỉ khi bạn có nhiều hơn một đơn vị biên dịch trong dự án của bạn. Nếu bạn chỉ có một đơn vị biên dịch, thì nó vẫn thực hiện mọi thứ nhưng những hiệu ứng đó chủ yếu là vô nghĩa (trừ khi bạn thích đào sâu vào các tệp đối tượng để đọc những gì trình biên dịch tạo ra.)

Như đã lưu ý, từ khóa này trong ngữ cảnh này kết hợp với từ khóa 'extern', điều này ngược lại - bằng cách làm cho tên biến hoặc tên hàm có thể liên kết với cùng tên được tìm thấy trong các đơn vị biên dịch khác. Vì vậy, bạn có thể xem 'tĩnh' khi yêu cầu biến hoặc tên được tìm thấy trong đơn vị biên dịch hiện tại, trong khi 'extern' cho phép liên kết đơn vị biên dịch chéo.

Trọn đời tĩnh

Thời gian tồn tại tĩnh có nghĩa là biến tồn tại trong suốt thời lượng của chương trình (tuy nhiên dài như vậy.) Khi bạn sử dụng 'static' để khai báo / định nghĩa một biến trong một hàm, điều đó có nghĩa là biến đó được tạo ra trước khi sử dụng lần đầu tiên ( điều đó có nghĩa là, mỗi lần tôi trải nghiệm nó, biến đó được tạo trước khi main () bắt đầu) và không bị hủy sau đó. Ngay cả khi thực hiện chức năng đã hoàn thành và nó sẽ trả về cho người gọi nó. Và giống như các biến số trọn đời tĩnh được khai báo bên ngoài các hàm, chúng được khởi tạo cùng một lúc - trước khi hàm main () bắt đầu - đến một số không ngữ nghĩa (nếu không cung cấp khởi tạo) hoặc cho một giá trị rõ ràng được chỉ định, nếu được cung cấp.

Điều này khác với các biến chức năng loại 'tự động', được tạo mới (hoặc, nếu như mới) mỗi khi chức năng được nhập và sau đó bị hủy (hoặc, nếu như chúng bị phá hủy) khi chức năng thoát.

Không giống như tác động của việc áp dụng 'tĩnh' đối với định nghĩa biến ngoài hàm, tác động trực tiếp đến phạm vi của nó, khai báo một biến hàm (trong thân hàm, rõ ràng) là 'tĩnh' không ảnh hưởng đến phạm vi của nó. Phạm vi được xác định bởi thực tế là nó đã được xác định trong một thân hàm. Các biến số trọn đời tĩnh được xác định trong các hàm có cùng phạm vi với các biến 'tự động' khác được xác định trong các thân hàm - phạm vi hàm.

Tóm lược

Vì vậy, từ khóa 'tĩnh' có các ngữ cảnh khác nhau với số lượng "ý nghĩa rất khác nhau". Lý do nó được sử dụng theo hai cách, như thế này, là để tránh sử dụng từ khóa khác. (Có một cuộc thảo luận dài về nó.) Có cảm giác rằng các lập trình viên có thể chịu đựng được việc sử dụng và giá trị của việc tránh một từ khóa khác trong ngôn ngữ là quan trọng hơn (so với các đối số khác.)

(Tất cả các biến được khai báo bên ngoài hàm đều có thời gian tồn tại tĩnh và không cần từ khóa 'tĩnh' để biến điều đó thành sự thật. Vì vậy, loại từ khóa này đã giải phóng từ khóa được sử dụng ở đó để có nghĩa hoàn toàn khác: 'chỉ hiển thị trong một phần tổng hợp duy nhất đơn vị. 'Đó là một hack, thuộc loại.)

Lưu ý cụ thể

biến động tĩnh không dấu char PORTB @ 0x06;

Từ 'tĩnh' ở đây nên được hiểu là có nghĩa là trình liên kết sẽ không khớp với nhiều lần xuất hiện của PORTB có thể được tìm thấy trong nhiều đơn vị biên dịch (giả sử mã của bạn có nhiều hơn một.)

Nó sử dụng cú pháp đặc biệt (không di động) để chỉ định "vị trí" (hoặc giá trị số của nhãn thường là địa chỉ) của PORTB. Vì vậy, trình liên kết được cung cấp địa chỉ và không cần tìm địa chỉ cho nó. Nếu bạn có hai đơn vị biên dịch sử dụng dòng này, dù sao thì chúng cũng sẽ chỉ đến cùng một vị trí. Vì vậy, không cần phải dán nhãn 'extern', ở đây.

Nếu họ sử dụng 'extern' thì nó có thể gây ra vấn đề. Trình liên kết sau đó sẽ có thể thấy (và sẽ cố gắng khớp) nhiều tham chiếu đến PORTB được tìm thấy trong nhiều phần tổng hợp. Nếu tất cả trong số họ chỉ định một địa chỉ như thế này và các địa chỉ KHÔNG giống nhau vì một lý do nào đó [nhầm lẫn?], Thì nó phải làm gì? Than phiền? Hoặc là? (Về mặt kỹ thuật, với 'extern', quy tắc ngón tay cái sẽ là chỉ có MỘT đơn vị biên dịch sẽ chỉ định giá trị và các đơn vị khác không nên.)

Thật dễ dàng để gắn nhãn là 'tĩnh', tránh làm cho trình liên kết lo lắng về xung đột và chỉ đơn giản là đổ lỗi cho bất kỳ sai lầm nào đối với các địa chỉ khớp sai khi bất kỳ ai thay đổi địa chỉ thành địa chỉ không nên.

Dù bằng cách nào, biến được coi là có 'vòng đời tĩnh'. (Và 'không ổn định'.)

Một tuyên bố không phải là một định nghĩa , nhưng tất cả các định nghĩa là khai báo

Trong C, một định nghĩa tạo ra một đối tượng. Nó cũng tuyên bố nó. Nhưng một tuyên bố không thường (xem ghi chú dưới đây) tạo ra một đối tượng.

Sau đây là định nghĩa và khai báo:

static int a;
static int a = 7;
extern int b = 5;
extern int f() { return 10; }

Dưới đây không phải là định nghĩa, mà chỉ là khai báo:

extern int b;
extern int f();

Lưu ý rằng các khai báo không tạo ra một đối tượng thực tế. Họ chỉ khai báo các chi tiết về nó, mà trình biên dịch có thể sử dụng để giúp tạo mã chính xác và cung cấp các thông báo lỗi và cảnh báo, nếu phù hợp.

  • Ở trên, tôi nói "thông thường", khuyên nhủ. Trong một số trường hợp, một khai báo có thể tạo ra một đối tượng và do đó được trình liên kết quảng bá (không bao giờ bởi trình biên dịch.) Vì vậy, ngay cả trong trường hợp hiếm hoi này, trình biên dịch C vẫn cho rằng khai báo chỉ là khai báo. Đây là giai đoạn liên kết thực hiện bất kỳ chương trình khuyến mãi cần thiết nào của một số tuyên bố. Hãy ghi nhớ điều này một cách cẩn thận.

    Trong các ví dụ trên, hóa ra chỉ có khai báo cho một "extern int b;" trong tất cả các đơn vị biên dịch được liên kết, sau đó trình liên kết chịu trách nhiệm tạo ra một định nghĩa. Hãy lưu ý rằng đây là một sự kiện thời gian liên kết. Trình biên dịch hoàn toàn không biết, trong quá trình biên dịch. Nó chỉ có thể được xác định tại thời điểm liên kết, nếu một tuyên bố loại này hầu hết được quảng bá.

    Trình biên dịch nhận thức được rằng "static int a;" không thể được thúc đẩy bởi trình liên kết tại thời gian liên kết, vì vậy đây thực sự là một định nghĩa tại thời gian biên dịch .


3
Câu trả lời tuyệt vời, +1! Chỉ có một điểm nhỏ: Họ có thể đã sử dụng externvà đó sẽ là cách làm C đúng đắn hơn: Khai báo biến externtrong tệp tiêu đề được in nhiều lần trong chương trình và xác định nó trong một số tệp không phải tiêu đề được biên dịch và liên kết chính xác một lần. Rốt cuộc, PORTB được cho là chính xác một trường hợp của biến mà các cu khác nhau có thể tham chiếu. Vì vậy, việc sử dụng staticở đây là một loại phím tắt mà họ đã sử dụng để tránh cần một tệp .c khác ngoài tệp tiêu đề.
JimmyB

Tôi cũng sẽ lưu ý rằng một biến tĩnh được khai báo trong một hàm không bị thay đổi trong các lệnh gọi hàm có thể hữu ích cho các hàm cần giữ lại một số loại thông tin trạng thái (trước đây tôi đã sử dụng nó cho mục đích này).
Peter Smith

@Peter Tôi nghĩ tôi đã nói thế. Nhưng có lẽ không như bạn đã thích?
jonk

@JimmyB Không, họ không thể sử dụng 'extern', thay vào đó, cho các khai báo biến hàm hoạt động như 'tĩnh'. 'extern' đã là một tùy chọn cho khai báo biến (không phải định nghĩa) trong các thân hàm và phục vụ mục đích khác - cung cấp quyền truy cập thời gian liên kết đến các biến được xác định bên ngoài bất kỳ hàm nào. Nhưng có thể tôi cũng đang hiểu nhầm quan điểm của bạn.
jonk

1
@JimmyB Liên kết bên ngoài chắc chắn sẽ có thể, mặc dù tôi không biết nếu nó "đúng hơn". Một xem xét là trình biên dịch có thể phát ra mã được tối ưu hóa hơn nếu thông tin được tìm thấy trong đơn vị dịch thuật. Đối với các kịch bản nhúng, việc lưu chu kỳ trên mỗi câu lệnh IO có thể là một vấn đề lớn.
Cort Ammon

9

statics không thể nhìn thấy bên ngoài đơn vị biên dịch hiện tại hoặc "đơn vị dịch". Đây không giống như cùng một tập tin .

Lưu ý rằng bạn bao gồm tệp tiêu đề vào bất kỳ tệp nguồn nào trong đó bạn có thể cần các biến được khai báo trong tiêu đề. Sự bao gồm này làm cho tệp tiêu đề là một phần của đơn vị dịch hiện tại và (một ví dụ) biến hiển thị bên trong nó.


Cảm ơn vì đã trả lời. "Đơn vị biên soạn", Xin lỗi tôi không hiểu, bạn có thể giải thích thuật ngữ đó. Hãy để tôi hỏi bạn một câu hỏi nữa, ngay cả khi chúng tôi muốn sử dụng các biến và hàm được viết bên trong một tệp khác, trước tiên chúng tôi phải BAO GỒM tệp đó vào TẬP TIN NGUỒN chính của chúng tôi. Vậy thì tại sao từ khóa "tĩnh dễ bay hơi" trong tệp tiêu đề đó.
Electro Voyager



3
@ElectroVoyager; nếu bạn bao gồm cùng một tiêu đề chứa một khai báo tĩnh trong nhiều tệp nguồn c, thì mỗi tệp đó sẽ có một biến tĩnh có cùng tên, nhưng chúng không phải là cùng một biến .
Peter Smith

2
Từ liên kết @JimmyB: Files included by using the #include preprocessor directive become part of the compilation unit.Khi bạn bao gồm tệp tiêu đề (.h) của mình trong tệp .c, hãy nghĩ về nó như chèn nội dung của tiêu đề vào tệp nguồn và bây giờ, đây là đơn vị biên dịch của bạn. Nếu bạn khai báo biến hoặc hàm tĩnh đó trong tệp .c, bạn chỉ có thể sử dụng chúng trong cùng một tệp, ở cuối, sẽ là một đơn vị biên dịch khác.
gustavigsascoh

5

Tôi sẽ cố gắng tóm tắt các bình luận và câu trả lời của @ JimmyB bằng một ví dụ giải thích:

Giả sử tập hợp các tệp này:

static_test.c:

#include <stdio.h>
#if USE_STATIC == 1
    #include "static.h"
#else
    #include "no_static.h"
#endif

void var_add_one();

void main(){

    say_hello();
    printf("var is %d\n", var);
    var_add_one();
    printf("now var is %d\n", var);
}

tĩnh.h:

static int var=64;
static void say_hello(){
    printf("Hello!!!\n");
};

no_static.h:

int var=64;
void say_hello(){
    printf("Hello!!!\n");
};

static_src.c:

#include <stdio.h>

#if USE_STATIC == 1
    #include "static.h"
#else
    #include "no_static.h"
#endif

void var_add_one(){
    var = var + 1;
    printf("Added 1 to var: %d\n", var);
    say_hello();
}

Bạn có thể biên dịch và chạy mã bằng gcc -o static_test static_src.c static_test.c -DUSE_STATIC=1; ./static_testcách sử dụng tiêu đề tĩnh hoặc gcc -o static_test static_src.c static_test.c -DUSE_STATIC=0; ./static_testđể sử dụng tiêu đề không tĩnh.

Lưu ý rằng hai đơn vị biên dịch có mặt ở đây: static_src và static_test. Khi bạn sử dụng phiên bản tĩnh của tiêu đề ( -DUSE_STATIC=1), một phiên bản varsay_hellosẽ có sẵn cho mỗi đơn vị biên dịch, đây là, cả hai đơn vị đều có thể sử dụng chúng, nhưng kiểm tra xem mặc dù var_add_one()hàm có tăng biến của nó hay var không, khi hàm chính in biến của nó var , nó vẫn là 64:

$ gcc -o static_test static_src.c static_test.c -DUSE_STATIC=1; ./static_test                                                                                                                       14:33:12
Hello!!!
var is 64
Added 1 to var: 65
Hello!!!
now var is 64

Bây giờ, nếu bạn cố gắng biên dịch và chạy mã, sử dụng phiên bản không tĩnh ( -DUSE_STATIC=0), nó sẽ đưa ra lỗi liên kết do định nghĩa biến trùng lặp:

$ gcc -o static_test static_src.c static_test.c -DUSE_STATIC=0; ./static_test                                                                                                                       14:35:30
/tmp/ccLBy1s7.o:(.data+0x0): multiple definition of `var'
/tmp/ccV6izKJ.o:(.data+0x0): first defined here
/tmp/ccLBy1s7.o: In function `say_hello':
static_test.c:(.text+0x0): multiple definition of `say_hello'
/tmp/ccV6izKJ.o:static_src.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
zsh: no such file or directory: ./static_test

Hy vọng điều này có thể giúp bạn làm rõ vấn đề này.


4

#include pic.hđại khái có nghĩa là "sao chép nội dung của pic.h vào tệp hiện tại". Kết quả là, mọi tệp bao gồm pic.hđịnh nghĩa cục bộ của nó PORTB.

Có lẽ bạn tự hỏi tại sao không có định nghĩa toàn cầu duy nhất về PORTB. Lý do khá đơn giản: bạn chỉ có thể định nghĩa một biến toàn cầu trong một tập tin C, vì vậy nếu bạn muốn sử dụng PORTBtrong nhiều tập tin trong dự án của bạn, bạn sẽ cần pic.hcó một lời tuyên bố của PORTBpic.cvới nó định nghĩa . Để mỗi tệp C xác định bản sao riêng của nó PORTBgiúp việc xây dựng mã dễ dàng hơn, vì bạn không phải đưa vào các tệp dự án mà bạn không viết.

Một lợi ích bổ sung của các biến tĩnh so với toàn cầu là bạn nhận được ít xung đột đặt tên hơn. Tệp AC không sử dụng bất kỳ tính năng phần cứng MCU nào (và do đó không bao gồm pic.h) có thể sử dụng tên PORTBcho mục đích riêng của mình. Không phải là mục đích tốt để thực hiện mục đích đó, nhưng khi bạn phát triển, ví dụ như một thư viện toán học không biết MCU, bạn sẽ ngạc nhiên về việc vô tình sử dụng lại một tên được sử dụng bởi một trong những MCU ngoài kia.


"Bạn sẽ ngạc nhiên về việc dễ dàng vô tình sử dụng lại tên được sử dụng bởi một trong những MCU ngoài kia" - Tôi dám hy vọng rằng tất cả các thư viện toán học chỉ sử dụng tên viết thường và tất cả các môi trường MCU chỉ sử dụng chữ hoa để đăng ký tên.
vsz

@vsz LAPACK cho một người có đầy đủ các tên mũ lịch sử.
Dmitry Grigoryev

3

Đã có một số câu trả lời hay, nhưng tôi nghĩ nguyên nhân của sự nhầm lẫn cần được giải quyết một cách đơn giản và trực tiếp:

Khai báo PORTB không chuẩn C. Đây là phần mở rộng của ngôn ngữ lập trình C chỉ hoạt động với trình biên dịch PIC. Phần mở rộng là cần thiết vì PIC không được thiết kế để hỗ trợ C.

Việc sử dụng statictừ khóa ở đây gây nhầm lẫn bởi vì bạn sẽ không bao giờ sử dụng staticcách đó trong mã thông thường. Đối với một biến toàn cục, bạn sẽ sử dụng externtrong tiêu đề, không static. Nhưng PORTB không phải là một biến bình thường . Đây là một bản hack cho trình biên dịch sử dụng các hướng dẫn lắp ráp đặc biệt để đăng ký IO. Khai báo PORTB staticgiúp lừa trình biên dịch thực hiện đúng.

Khi được sử dụng ở phạm vi tệp, staticgiới hạn phạm vi của biến hoặc hàm đối với tệp đó. "Tệp" có nghĩa là tệp C và bất cứ thứ gì được sao chép vào nó bởi bộ tiền xử lý. Khi bạn sử dụng #include, bạn đang sao chép mã vào tệp C của mình. Đó là lý do tại sao sử dụng statictrong một tiêu đề không có ý nghĩa - thay vì một biến toàn cục, mỗi tệp # bao gồm tiêu đề sẽ nhận được một bản sao riêng của biến đó.

Trái với niềm tin phổ biến, staticluôn có nghĩa tương tự: phân bổ tĩnh với phạm vi hạn chế. Đây là những gì xảy ra với các biến trước và sau khi được khai báo static:

+------------------------+-------------------+--------------------+
| Variable type/location |    Allocation     |       Scope        |
+------------------------+-------------------+--------------------+
| Normal in file         | static            | global             |
| Normal in function     | automatic (stack) | limited (function) |
| Static in file         | static            | limited (file)     |
| Static in function     | static            | limited (function) |
+------------------------+-------------------+--------------------+

Điều khiến nó khó hiểu là hành vi mặc định của các biến phụ thuộc vào nơi chúng được xác định.


2

Lý do tệp chính có thể thấy định nghĩa cổng "tĩnh" là do lệnh #incoide. Lệnh đó tương đương với việc chèn toàn bộ tệp tiêu đề vào mã nguồn của bạn trên cùng một dòng với chính lệnh đó.

Trình biên dịch microchip XC8 xử lý các tệp .c và .h giống hệt nhau để bạn có thể đặt các định nghĩa biến của mình vào một trong hai.

Normaly một tệp tiêu đề chứa tham chiếu "bên ngoài" đến các biến được xác định ở nơi khác (thường là tệp .c).

Các biến cổng cần được chỉ định tại các địa chỉ bộ nhớ cụ thể phù hợp với phần cứng thực tế. Vì vậy, một định nghĩa thực tế (không bên ngoài) cần thiết để tồn tại ở đâu đó.

Tôi chỉ có thể đoán tại sao tập đoàn Microchip chọn đưa các định nghĩa thực tế vào tệp .h. Một dự đoán có khả năng là họ chỉ muốn một tệp (.h) thay vì 2 (.h và .c) (để giúp mọi người dễ dàng hơn).

Nhưng nếu bạn đặt các định nghĩa biến thực tế trong một tệp tiêu đề và sau đó đưa tiêu đề đó vào nhiều tệp nguồn thì trình liên kết sẽ phàn nàn rằng các biến được xác định nhiều lần.

Giải pháp là khai báo các biến là tĩnh sau đó mỗi định nghĩa được coi là cục bộ đối với tệp đối tượng đó và trình liên kết sẽ không khiếu nại.

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.