Концепция статического ключевого слова с точки зрения встроенного C


9
static volatile unsigned char   PORTB   @ 0x06;

Это строка кода в заголовочном файле микроконтроллера PIC. @Оператор используется для хранения значения PORTB внутри адрес 0x06, который является регистром внутри контроллера PIC , который представляет PORTB. До этого момента у меня есть четкая идея.

Эта строка объявлена ​​как глобальная переменная внутри заголовочного файла ( .h). Итак, из того, что я узнал о языке C, «статическая глобальная переменная» не видна ни в каком другом файле - или просто статические глобальные переменные / функции не могут использоваться вне текущего файла.

Тогда как это ключевое слово PORTBможет быть видимо для моего основного исходного файла и многих других заголовочных файлов, которые я создал вручную?

В моем основном исходном файле я только добавил заголовочный файл. #include pic.hЭто как-то связано с моим вопросом?


2
нет проблем с вопросом, но неправильный раздел SE я боюсь
gommer

static обычно используется внутри функции, чтобы указать, что переменная создается один раз и сохраняет свое значение от одного выполнения функции до следующего. глобальная переменная - это переменная, созданная вне любой функции, так что она видна везде. static global не имеет смысла.
Финбарр

8
@Finbarr Неправильно. staticглобальные переменные видны внутри всего единого блока компиляции и не экспортируются за пределы этого. Они очень похожи на privateчленов класса в ООП. Т.е. каждая переменная, которая должна быть разделена между различными функциями внутри модуля компиляции, но не должна быть видимой вне того, что в действительности должно быть cu static. Это также уменьшает «забивание» глобального пространства имен программы.
JimmyB

2
Re «Оператор @ используется для хранения значения PORTB внутри адреса 0x06» . В самом деле? Не больше ли похоже на «Оператор @ используется для хранения переменной« PORTB »по абсолютному адресу памяти 0x06» ?
Питер Мортенсен

Ответы:


20

Ключевое слово «статический» в Си имеет два принципиально разных значения.

Ограничивающая область

В этом контексте «static» объединяется с «extern» для управления областью действия переменной или имени функции. Static заставляет имя переменной или функции быть доступным только в пределах одного модуля компиляции и доступно только для существующего кода после объявления / определения в тексте единицы компиляции.

Это ограничение само по себе действительно означает что-то, только если у вас есть более одного модуля компиляции в вашем проекте. Если у вас есть только один модуль компиляции, он все равно работает, но эти эффекты в основном бессмысленны (если вы не любите копаться в объектных файлах, чтобы прочитать то, что сгенерировал компилятор).

Как уже отмечалось, это ключевое слово в этом контексте соединяется с ключевым словом «extern», что делает обратное - делая имя переменной или функции связываемым с тем же именем, что и в других единицах компиляции. Таким образом, вы можете посмотреть на «static» как на требование, чтобы переменная или имя находились в текущей единице компиляции, тогда как «extern» разрешает связывание блоков кросс-компиляции.

Статическая жизнь

Статическое время жизни означает, что переменная существует на протяжении всей программы (как бы долго это ни было.) Когда вы используете «static» для объявления / определения переменной внутри функции, это означает, что переменная создается за некоторое время до ее первого использования ( Это означает, что каждый раз, когда я это испытывал, переменная создается до запуска main () и впоследствии не уничтожается. Даже когда выполнение функции завершено и она возвращается к своему вызывающему. И так же, как статические переменные времени жизни, объявленные вне функций, они инициализируются в один и тот же момент - до запуска main () - в семантический ноль (если инициализация не предусмотрена) или в указанное явное значение, если оно задано.

Это отличается от переменных функции типа 'auto', которые создаются новыми (или, как если бы они были новыми) каждый раз при входе в функцию, а затем уничтожаются (или, как если бы они были уничтожены) при выходе из функции.

В отличие от воздействия применения «static» к определению переменной вне функции, что напрямую влияет на ее область действия, объявление переменной функции (очевидно, в теле функции) как «static» не влияет на ее область действия. Область действия определяется тем, что она была определена в теле функции. Статические переменные времени жизни, определенные в функциях, имеют ту же область видимости, что и другие «автоматические» переменные, определенные в теле функции - область действия функции.

Резюме

Таким образом, ключевое слово «static» имеет разные контексты с тем, что составляет «очень разные значения». Причина, по которой он использовался двумя способами, например, в том, чтобы избегать использования другого ключевого слова. (Об этом было долгое обсуждение.) Чувствовалось, что программисты могут терпеть использование, и значение избегания использования еще одного ключевого слова в языке более важно (чем аргументы в противном случае.)

(Все переменные, объявленные вне функций, имеют статическое время жизни и не нуждаются в ключевом слове «static», чтобы сделать это истинным. Так что этот вид освобождает ключевое слово, которое будет использоваться там, чтобы означать нечто совершенно иное: «видимое только в одной компиляции блок. «Это взломать, вроде.)

Особое примечание

статический изменчивый тип unsigned char PORTB @ 0x06;

Слово «статический» здесь следует интерпретировать как означающее, что компоновщик не будет пытаться сопоставить несколько вхождений PORTB, которые могут быть найдены в более чем одном модуле компиляции (при условии, что в вашем коде более одного).

Он использует специальный (непереносимый) синтаксис для указания «местоположения» (или числового значения метки, которое обычно является адресом) PORTB. Таким образом, компоновщик получает адрес, и ему не нужно его искать. Если бы у вас было две единицы компиляции, использующие эту линию, они все равно указывали бы на одно и то же место. Так что здесь нет необходимости обозначать его как «внешний».

Если бы они использовали 'extern', это могло бы создать проблему. Затем компоновщик сможет увидеть (и попытается сопоставить) несколько ссылок на PORTB, найденных в нескольких компиляциях. Если все они указывают адрес, подобный этому, и адреса по какой-то причине НЕ совпадают [ошибка?], То что он должен делать? Пожаловаться? Или? (Технически, с «extern» практическим правилом было бы то, что только ОДНА единица компиляции будет указывать значение, а другие не должны.)

Проще пометить его как «статический», избегая того, чтобы компоновщик беспокоился о конфликтах, и просто возложил вину за любые ошибки за несовпадающие адреса на того, кто бы ни изменил адрес, на то, что не должно быть.

В любом случае, переменная рассматривается как имеющая «статическое время жизни». (И «изменчивый».)

Декларация не является определение , но все определения деклараций

В C определение создает объект. Это также заявляет об этом. Но объявление обычно (см. Примечание к пуле ниже) не создает объект.

Ниже приведены определения и декларации:

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

Ниже приведены не определения, а только объявления:

extern int b;
extern int f();

Обратите внимание, что объявления не создают фактический объект. Они только объявляют подробности об этом, которые затем может использовать компилятор, чтобы помочь сгенерировать правильный код и, при необходимости, предоставить предупреждения и сообщения об ошибках.

  • Выше я говорю «обычно», советуем. В некоторых случаях объявление может создать объект и, следовательно, повышается до определения компоновщиком (а не компилятором). Таким образом, даже в этом редком случае компилятор C все еще думает, что объявление является только объявлением. Это фаза компоновщика, которая делает любые необходимые продвижения некоторой декларации. Имейте это в виду.

    В приведенных выше примерах, если окажется, что есть только объявления для "extern int b;" во всех связанных модулях компиляции компоновщик отвечает за создание определения. Имейте в виду, что это событие времени соединения. Во время компиляции компилятор полностью не осведомлен. Это может быть определено только во время соединения, если объявление этого типа наиболее продвигается.

    Компилятор знает, что "static int a;" не может быть продвинут компоновщиком во время компоновки, так что это фактически определение во время компиляции .


3
Отличный ответ, +1! Только один незначительный момент: они могли бы использовать это extern, и это был бы более правильный способ сделать это на С: объявить переменную externв файле заголовка для многократного добавления в программу и определить ее в некотором файле без заголовка для компиляции и связаны ровно один раз. В конце концов, PORTB это должно быть ровно один экземпляр переменной , в которой различные куб может ссылаться. Таким образом, использование staticздесь является своего рода ярлыком, который они использовали, чтобы избежать необходимости использования другого файла .c в дополнение к файлу заголовка.
JimmyB

Я также хотел бы отметить, что статическая переменная, объявленная внутри функции, не изменяется при вызове функций, что может быть полезно для функций, которым необходимо сохранять некоторую информацию о состоянии (я использовал ее специально для этой цели в прошлом).
Питер Смит

@ Питер, я так и сказал. Но, возможно, не так хорошо, как вы хотели бы?
Йонк

@JimmyB Нет, они не могли бы использовать «extern» вместо этого для объявлений переменных функций, которые ведут себя как «static». 'extern' уже является опцией для объявления переменных (не определений) внутри тел функций и служит другой цели - обеспечивает доступ во время соединения к переменным, определенным вне какой-либо функции. Но возможно, я тоже неправильно понимаю вашу точку зрения.
Йонк

1
@JimmyB Внешняя связь определенно была бы возможна, хотя я не знаю, является ли она «более правильной». Одно из соображений заключается в том, что компилятор может генерировать более оптимизированный код, если информация найдена в модуле перевода. Для встроенных сценариев сохранение циклов на каждом операторе ввода-вывода может иметь большое значение.
Корт Аммон

9

statics не видны за пределами текущей единицы компиляции или "единицы перевода". Это не то же самое, что и тот же файл .

Обратите внимание, что вы включаете файл заголовка в любой исходный файл, где вам могут понадобиться переменные, объявленные в заголовке. Это включение делает заголовочный файл частью текущей единицы перевода и (экземпляром) переменной, видимой внутри него.


Спасибо за ответ. "Блок компиляции", извините, я не понимаю, вы можете объяснить этот термин. Позвольте мне задать вам еще один вопрос, даже если мы хотим использовать переменные и функции, написанные внутри другого файла, мы должны сначала ВКЛЮЧИТЬ этот файл в наш основной ИСТОЧНИК ФАЙЛ. Тогда почему ключевое слово "static volatile" в этом заголовочном файле.
Электро Вояджер


1
Довольно подробное обсуждение на stackoverflow.com/questions/572547/what-does-static-mean-in-c
Питер Смит

3
@ElectroVoyager; если вы включите один и тот же заголовок, содержащий статическое объявление, в несколько исходных файлов c, то каждый из этих файлов будет иметь статическую переменную с тем же именем, но они не будут одинаковыми .
Питер Смит

2
Ссылка @JimmyB: Files included by using the #include preprocessor directive become part of the compilation unit.Когда вы включаете файл заголовка (.h) в файл .c, думайте о нем как о вставке содержимого заголовка в исходный файл, и теперь это ваш модуль компиляции. Если вы объявите эту статическую переменную или функцию в файле .c, вы можете использовать их только в том же файле, который в конце станет еще одним модулем компиляции.
gustavovelascoh

5

Я постараюсь обобщить комментарии и ответ @ JimmyB с пояснительным примером:

Предположим, этот набор файлов:

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);
}

static.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();
}

Вы можете скомпилировать и запустить код, используя gcc -o static_test static_src.c static_test.c -DUSE_STATIC=1; ./static_testдля использования статического заголовка или gcc -o static_test static_src.c static_test.c -DUSE_STATIC=0; ./static_testдля использования нестатического заголовка.

Обратите внимание, что здесь присутствуют два модуля компиляции: static_src и static_test. Когда вы используете статическую версию header ( -DUSE_STATIC=1), версия varи say_helloбудет доступна для каждого модуля компиляции, то есть оба модуля могут использовать их, но убедитесь, что даже если var_add_one()функция увеличивает свою var переменную, когда основная функция печатает свою var переменную , это еще 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

Теперь, если вы попытаетесь скомпилировать и запустить код, используя нестатическую версию ( -DUSE_STATIC=0), он выдаст ошибку связывания из-за дублированного определения переменной:

$ 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

Надеюсь, это поможет вам прояснить этот вопрос.


4

#include pic.hпримерно означает «скопировать содержимое файла pic.h в текущий файл». В результате каждый файл, который включает в себя, pic.hполучает свое собственное локальное определение PORTB.

Возможно, вы удивляетесь, почему не существует единого глобального определения PORTB. Причина довольно проста: вы можете только определить глобальную переменную в одном файле C, так что если вы хотите использовать PORTBв нескольких файлах в вашем проекте, вы должны pic.hс декларацией о PORTBи pic.cс его определением . Позволяя каждому файлу C определять свою собственную копиюPORTB упрощает сборку кода, так как вам не нужно включать в проект файлы, которые вы не писали.

Дополнительным преимуществом статических переменных и глобальных является то, что вы получаете меньше конфликтов имен. Файл AC, который не использует аппаратные функции MCU (и, следовательно, не включает в себя pic.h), может использовать имя PORTBдля своих собственных целей. Не то чтобы это было хорошей идеей, но когда вы разрабатываете, например, математическую библиотеку, не зависящую от MCU, вы будете удивлены, насколько легко случайно повторно использовать имя, которое используется одним из MCU.


«Вы будете удивлены, насколько легко случайно повторно использовать имя, которое используется одним из MCU», - смею надеяться, что все математические библиотеки используют только строчные имена, и что все среды MCU используют только прописные буквы для регистра имена.
vsz

@vsz LAPACK для одного полон исторических заглавных букв.
Дмитрий Григорьев

3

Уже есть несколько хороших ответов, но я думаю, что причина путаницы должна быть решена просто и прямо:

Объявление PORTB не является стандартным C. Это расширение языка программирования C, которое работает только с компилятором PIC. Расширение необходимо, потому что PIC не были разработаны для поддержки C.

Использование staticключевого слова здесь сбивает с толку, потому что вы никогда не использовали бы staticэтот способ в обычном коде. Для глобальной переменной вы бы использовали externв заголовке, а не static. Но PORTB не является нормальной переменной . Это хак, который говорит компилятору использовать специальные инструкции по сборке для регистрации ввода-вывода. Объявление PORTB staticпомогает заставить компилятор делать правильные вещи.

При использовании в области файла staticограничивает область действия переменной или функции этим файлом. «Файл» означает файл C и все, что копируется в него препроцессором. Когда вы используете #include, вы копируете код в ваш C-файл. Вот почему использование staticв заголовке не имеет смысла - вместо одной глобальной переменной каждый файл, содержащий #include заголовка, получит отдельную копию переменной.

Вопреки распространенному мнению, staticвсегда означает одно и то же: статическое распределение с ограниченной областью действия. Вот что происходит с переменными до и после объявления 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) |
+------------------------+-------------------+--------------------+

Что сбивает с толку, так это то, что поведение переменных по умолчанию зависит от того, где они определены.


2

Причина, по которой основной файл может видеть «статическое» определение порта, заключается в директиве #include. Эта директива эквивалентна вставке всего файла заголовка в исходный код в той же строке, что и сама директива.

Компилятор микрочипа XC8 обрабатывает файлы .c и .h одинаково, поэтому вы можете поместить определения переменных в любое из них.

Обычно заголовочный файл содержит «внешнюю» ссылку на переменные, которые определены в другом месте (обычно это файл .c).

Переменные порта должны быть указаны по конкретным адресам памяти, которые соответствуют фактическому оборудованию. Таким образом, фактическое (не внешнее) определение должно существовать где-то.

Я могу только догадываться, почему корпорация Microchip решила поместить фактические определения в файл .h. Вероятное предположение состоит в том, что они просто хотели один файл (.h) вместо 2 (.h и .c) (чтобы было проще для пользователя).

Но если вы поместите фактические определения переменных в файл заголовка, а затем включите этот заголовок в несколько исходных файлов, то компоновщик будет жаловаться, что переменные определены несколько раз.

Решение состоит в том, чтобы объявить переменные как статические, тогда каждое определение рассматривается как локальное для этого объектного файла, и компоновщик не будет жаловаться.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.