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

static volatile unsigned char   PORTB   @ 0x06;

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

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

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

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

нет проблем с вопросом, но я боюсь, что неправильный раздел SE
static обычно используется внутри функции, чтобы указать, что переменная создается один раз и сохраняет свое значение от одного выполнения функции до следующего. глобальная переменная создается вне какой-либо функции, поэтому она видна везде. static global на самом деле не имеет смысла.
@Финбарр Неправильно. staticглобальные объекты видны внутри всей отдельной единицы компиляции и не экспортируются за ее пределы. Они очень похожи privateна членов класса в ООП. Т.е. каждая переменная, которая должна быть разделена между различными функциями внутри модуля компиляции, но не должна быть видимой вне этого cu, должна быть static. Это также уменьшает «затирание» глобального пространства имен программы.
Re «Оператор @ используется для хранения значения PORTB внутри адреса 0x06» . Действительно? Разве это не больше похоже на «Оператор @ используется для хранения переменной «PORTB» по абсолютному адресу памяти 0x06» ?

Ответы (6)

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

Ограничение области

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

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

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

Статическое время жизни

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

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

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

Краткое содержание

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

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

Специальное примечание

статический изменчивый беззнаковый символ 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;" компоновщик не может продвигать его во время компоновки, так что это фактически определение во время компиляции .

Отличный ответ, +1! Только один незначительный момент: они могли бы использовать extern, и это был бы более правильный способ C: объявить переменную externв заголовочном файле для многократного включения в программу и определить ее в каком-то не-заголовочном файле для компиляции и подключен ровно один раз. В конце концов, PORTB предполагается , что это ровно один экземпляр переменной, на которую могут ссылаться разные cu. Таким образом, использование staticздесь является своего рода ярлыком, который они использовали, чтобы избежать необходимости в другом файле .c в дополнение к файлу заголовка.
Я также хотел бы отметить, что статическая переменная, объявленная внутри функции, не изменяется при вызовах функций, что может быть полезно для функций, которым необходимо сохранять некоторую информацию о состоянии (в прошлом я использовал ее специально для этой цели).
@ Питер, я думаю, что сказал это. Но, может быть, не так хорошо, как вам хотелось бы?
@JimmyB Нет, вместо этого они не могли использовать «extern» для объявлений функциональных переменных, которые ведут себя как «статические». 'extern' уже является опцией для объявлений переменных (не определений) в телах функций и служит другой цели - обеспечивает доступ во время компоновки к переменным, определенным вне какой-либо функции. Но, возможно, я тоже неправильно понимаю вашу точку зрения.
@JimmyB Внешняя связь определенно возможна, хотя я не знаю, «более ли это правильно». Одним из соображений является то, что компилятор может создавать более оптимизированный код, если информация находится в единице трансляции. Для встроенных сценариев экономия циклов на каждом операторе ввода-вывода может иметь большое значение.
Чтобы добавить: помните, что директива #include является препроцессором, и это может действительно создать несколько PORTB во всей компиляции, если они не будут обнаружены защитой включения . Это то, что считается многократным появлением.
@CortAmmon Я думаю, что это «более правильный» способ, потому что в этом случае он staticработает только потому, что он опирается на (непереносимое) расширение ( @ 0x06;), которое позволяет размещать несколько (статических) экземпляров переменной в одной памяти. расположение. Использование externэтого не будет проблемой.
@jonk Действительно, я считаю, что вы меня неправильно поняли. Обратите внимание, что мы говорим не об объявлениях/определениях переменных внутри функций, а о глобальных . Обратите внимание на другое значение staticдля глобальных переменных в отличие от локальных переменных функции.
«Технически, с «extern» эмпирическое правило будет заключаться в том, что только ОДНА единица компиляции будет указывать значение, а другие - нет». - Это должно прояснить, что @ 0x06является частью определения переменной, а не ее объявления. В программе может быть только одно определение (нестатической) переменной, все остальное приводит к ошибке во время компоновки (или раньше). Вы можете объявить (нестатическую) переменную несколько раз через extern, но может быть только одно определение.
@JimmyB Я оставлю ваши комментарии, чтобы другие могли их интерпретировать. Я уже писал компилятор C. Я знаю немного больше, чем многие из этого опыта. Но я не уверен, что вы говорите. Это может быть время, которое я должен потратить, пытаясь разобрать его. Когда у меня будет время и желание, я могу попытаться понять это. Тем не менее, я считаю, что то, что я написал, соответствует моему опыту, и я хотел бы следовать вашим мотивам и комментариям.
@JimmyB И да, я ХОРОШО осознаю разницу между объявлениями и определениями, особенно в отношении глобальных переменных и внешних переменных. Опять же, я не понимаю вашей проблемы с моим письмом. Но я хотел бы улучшить то, что я написал, а это значит, что я хотел бы понять вашу критику (если это так).
На самом деле не хотел критиковать :) И после того, как я перечитал ваш ответ, я заметил, что вы действительно указали на разные значения static. Я просто хочу сказать, что в этом случае используется статическое объявление (и определение) в заголовочном файле, т.е. ссылка на одну и ту же переменную/ячейку памяти из нескольких файлов .c, работает только из-за (непереносимости , не- стандартный) @ 0x06;спецификатор адреса, в то время как "правильный" способ C будет использовать externв объявлении(ях) и отдельном единственном определении переменной. Без @ 0x06спецификатора адреса каждый
@JimmyB Если вы внимательно читаете меня, вы должны увидеть, где я говорю именно то, что, как мне кажется, вы говорите. Смотрите ближе к концу. Может третий абзац снизу?
статический экземпляр переменной будет выделен в свою собственную отдельную ячейку памяти, чего избегает extern.
Перечитайте это сейчас, я не согласен с абзацем, начинающимся с «Если бы они использовали« внешний », это могло бы создать проблему». - externне создало бы проблем, потому что это гарантирует наличие только одного определения переменной.
@JimmyB Я не говорю, что «внешний» - это проблема. На самом деле, мы с вами согласны в том, что это не проблема. Пока вы определяете только один раз, конечно. Я говорю о том, почему автор, возможно, выбрал другой путь, используя вместо этого «статический». Вот и все.

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

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

Спасибо за ваш ответ. "Compilation Unit", извините, я не понимаю, можете ли вы объяснить этот термин. Позвольте мне задать вам еще один вопрос, даже если мы хотим использовать переменные и функции, записанные внутри другого файла, мы должны сначала ВКЛЮЧИТЬ этот файл в наш основной ИСТОЧНИК. Тогда почему ключевое слово «static volatile» в этом заголовочном файле.
Довольно подробное обсуждение на stackoverflow.com/questions/572547/what-does-static-mean-in-c
Спасибо за все объяснения, но все же я не совсем понимаю, почему ключевое слово static предназначено для глобальных переменных. Я написал программу, которая включает заголовочный файл "includer.h" на C++. Внутри этого файла я добавил static int var= 56 и функцию static void say_hello(). После включения этого файла в мой основной исходный файл я могу использовать как функцию, так и переменную. Но на форуме сказано так: «Статические глобальные переменные не видны вне файла C, в котором они определены». «Статические функции не видны вне файла C, в котором они определены».
@ЭлектроВояджер; если вы включаете один и тот же заголовок, содержащий статическое объявление, в несколько исходных файлов c, то каждый из этих файлов будет иметь статическую переменную с тем же именем, но это не одна и та же переменная .
Из ссылки @JimmyB: Files included by using the #include preprocessor directive become part of the compilation unit.когда вы включаете файл заголовка (.h) в файл .c, подумайте об этом как о вставке содержимого заголовка в исходный файл, и теперь это ваша единица компиляции. Если вы объявите эту статическую переменную или функцию в файле .c, вы сможете использовать их только в том же файле, который в конце будет другой единицей компиляции.
Правильным термином является единица перевода , а не «единица компиляции». См. 5.1.1.1/1: «Исходный файл вместе со всеми заголовками и исходными файлами, включенными в директиву предварительной обработки, #includeизвестен как единица перевода предварительной обработки . После предварительной обработки единица перевода предварительной обработки называется единицей перевода ».

Я попытаюсь обобщить комментарии и ответ @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 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. Когда вы используете статическую версию заголовка ( -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

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

#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 используют только верхний регистр для регистрации имена.
@vsz LAPACK, например, полон исторических имен, написанных заглавными буквами.

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

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

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

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

Вопреки распространенному мнению, 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) |
+------------------------+-------------------+--------------------+

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

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

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

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

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

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

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

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