При написании программ часто необходимо объявлять переменные, но в этом частом действии есть много деталей, и без понимания этих деталей вы можете написать неожиданные программы или запутаться в том, что написали другие.
- Декларации и определения
- Область применения и видимость переменных
- область действия файла
- Объявите, прежде чем использовать
- Объем блока
- Диапазон просмотра
- Класс хранения переменных
- Продолжительность хранения переменных
- Ограничения задатчика начального значения
- Невозможно обратиться к переменной регистрового хранилища
- Ссылка
- отсутствие связи
- межэлектродная связь
- Экстернальность (экстернальный линаж)
- предварительные определения
- Заметки о GCC
- Объявления и определения функций
- Резюме
Декларации и определения
Рассмотрим пример, иллюстрирующий синтаксис деклараций.
#include<stdio.h>
int i = 10;
int main(void) {
printf("%dn", i);
}
где int
называется спецификатором, который указывает тип переменной, i
называется декларатором, который указывает имя переменной, за которым следует = 10
, который называется инициализатором которая конфигурирует память переменной и устанавливает начальное значение переменной.
Теоретически, объявление не обязательно должно иметь инициализатор, но объявление с инициализатором называется дифиницией. Вы можете объявить одну и ту же переменную несколько раз, но определить ее только один раз, потому что одна и та же переменная не может быть выделена в разных местах памяти, иначе у вас было бы две переменные с одинаковым именем. Пример.
#include<stdio.h>
int i;
int i = 10;
int i;
int main(void) {
printf("%dn", i);
}
Имеется три объявления переменной i
, но только второе объявление имеет начальный сеттер, который завершает определение переменной i
. Простое объявление без начального задатчика только дает компилятору знать, что будет переменная указанного типа и имени, но не выделяет память, которая будет выделена для этой переменной, только если начальный задатчик завершит определение. Если одна и та же переменная определена более одного раза, возникнет ошибка определения дублирующей переменной, например.
#include<stdio.h>
int i;
int i = 10;
int i = 10;
int main(void) {
printf("%dn", i);
}
При компиляции вы увидите следующую ошибку.
main.c:5:5: error: redefinition of 'i'
5 | int i = 10;
| ^
main.c:4:5: note: previous definition of 'i' was here
4 | int i = 10;
| ^
Обратите внимание, что формулировка сообщения об ошибке — ‘redifinition’, а не ‘redeclaration’, что является ошибкой из-за повторных определений, а не повторных деклараций.
Область применения и видимость переменных
Расположение объявленной переменной влияет на область исходного файла, в которой можно использовать имя переменной, и на область, в которой можно получить доступ к содержимому переменной по этому имени; первое называется областью видимости, а второе — видимостью.
область действия файла
Переменная, объявленная вне любой функции, действует в области видимости файла, т.е. от объявленного места до конца исходного файла, как, например, здесь
#include<stdio.h>
int i = 10;
void print_i(void) {
printf("%dn", i);
}
int main(void) {
printf("%dn", i);
print_i();
}
Переменная i
из предыдущего объявления может быть использована либо в print_i
, либо в main
.
Общефайловые декларации также называют внешними декларациями, поскольку они находятся вне всех функций.
Объявите, прежде чем использовать
Важно отметить, что декларации должны быть сделаны до их использования, но если изменить порядок на следующий, произойдет ошибка.
#include<stdio.h>
void print_i(void) {
printf("%dn", i);
}
int i = 10;
int main(void) {
printf("%dn", i);
print_i();
}
Сообщение об ошибке при компиляции выглядит следующим образом.
main.c: In function 'print_i':
main.c:4:20: error: 'i' undeclared (first use in this function)
4 | printf("%dn", i);
| ^
main.c:4:20: note: each undeclared identifier is reported only once for each function it appears in
Это происходит потому, что i
объявляется после print_i
, поэтому в print_i
i
является переменной, которая еще не была объявлена. Если есть конкретная причина, по которой переменная i
должна быть определена после print_i
, то можно отделить объявление от определения, например, так.
#include<stdio.h>
int i;
void print_i(void) {
printf("%dn", i);
}
int i = 10;
int main(void) {
printf("%dn", i);
print_i();
}
и ошибок не будет.
Объем блока
Если переменная объявлена в блоке, она действительна только в пределах блока и не может быть использована вне блока, отсюда и название «область видимости блока», например.
#include<stdio.h>
int main(void) {
int i = 10;
printf("%dn", i);
}
Поскольку переменная i
объявлена внутри блока функции main
, она действительна только внутри этого блока. Если мы намеренно добавим слой блоков и объявим в них i
, то это будет выглядеть следующим образом.
#include<stdio.h>
int main(void) {
{
int i = 10;
}
printf("%dn", i);
}
компилятор выдаст следующую ошибку.
main.c: In function 'main':
main.c:8:20: error: 'i' undeclared (first use in this function)
8 | printf("%dn", i);
| ^
main.c:8:20: note: each undeclared identifier is reported only once for each function it appears in
Компилятор считает, что i
не объявлен, потому что он вызывается с помощью printf
вне блока, который объявляет i
, что находится вне допустимого диапазона i
. Если на i
ссылаются во внутреннем блоке, проблемы нет: i
не объявлен.
#include<stdio.h>
int main(void) {
{
int i = 10;
printf("%dn", i);
}
}
Диапазон просмотра
Обычно видимая область видимости совпадает с допустимой областью видимости, но если переменная с тем же именем объявлена внутри блока, что и вне блока, возникает эффект маскировки, т.е. на переменную внутри блока можно ссылаться только по этому имени, но не вне блока.
#include<stdio.h>
int i = 300;
int main(void) {
int i = 100;
{
int i = 10;
printf("%dn", i);
}
printf("%dn", i);
}
Результат следующий
10
100
Видно, что внутренний блок использует i
для ссылки на переменную, объявленную в самом внутреннем блоке, в то время как то же имя в main
i
относится к переменной, объявленной в main
, т.е. переменная, объявленная во внутреннем блоке, не видна в блоке. Переменные с одинаковым именем, объявленные вне блока, как на внешнем уровне, так и внутри main
i
, имеют меньший видимый диапазон, чем допустимый диапазон.
Класс хранения переменных
Как упоминалось выше, память конфигурируется только тогда, когда переменная полностью определена, так когда именно она конфигурируется? Это подводит нас к классу хранения переменной.
Продолжительность хранения переменных
При объявлении переменной вы можете добавить к верхнему индикатору один из следующих четырех типов хранения.
- авто
- регистр
- статический
- extern
Различные типы хранения влияют на продолжительность хранения переменной и делятся на следующие категории.
- auto storage duration: переменные типа хранения
auto
иregister
относятся к этому типу длительности, они могут быть использованы только для переменных в блоках,auto
также является значением по умолчанию для переменных в блоках, когда тип хранения не указан во время объявления. Этот тип постоянных переменных конфигурирует память и устанавливает начальное значение каждый раз, когда переменная входит в блок, и освобождает память, когда выходит из блока.register
по сути то же самое, что иauto
, но компилятор будет пытаться поместить эту переменную в регистр процессора, если это возможно, и на практике это редко используется из-за ограниченного количества регистров в процессоре, которые действительно помещаются в регистр. - статическая длительность: переменные типа хранения
static
иextern
являются частью этого типа длительности, и будут выделены в память и установлены в начальные значения после выполнения программы, до вызова функцииmain
, и память будет освобождена в конце программы. Независимо от того, где определена переменная, этот тип переменной длительности будет конфигурировать память и устанавливать начальное значение только один раз. Правильное объявление диапазона файлов, в котором не указан тип хранения, рассматривается какextern
.
Поскольку тип хранения auto
может использоваться только для переменных внутри блока, легко понять, что он совпадает с допустимым диапазоном переменной, поэтому здесь приведен пример типа хранения static
.
#include<stdio.h>
void inc(void) {
static int j = 10;
printf("%dn", j++);
}
int main(void) {
inc();
inc();
inc();
}
Результат следующий
10
11
12
Вы видите, что хотя j
определен в блоке функций inc
, поскольку это статический
тип хранения, он будет конфигурировать память и устанавливать начальное значение только один раз перед вызовом main
, а затем продолжит использоваться, Вместо перенастройки памяти и установки начального значения каждый раз, когда вызывается inc
. Когда вызывается inc
, переменная j
увеличивается на предыдущее значение. Если убрать static
и использовать в блоке тип хранения по умолчанию auto
, или явно обозначить его auto
, например, так.
#include<stdio.h>
void inc(void) {
int j = 10; // 預設是 auto 儲存類型
printf("%dn", j++);
}
int main(void) {
inc();
inc();
inc();
}
Результатом будет постоянное значение 10: Вход в полноэкранный режим
10
10
10
Это происходит потому, что каждый раз, когда вы вводите блок inc
, он реконфигурирует память и устанавливает начальное значение j
, поэтому каждый раз, когда вы вызываете inc
, значение j
будет равно 10.
Обратите внимание, что для объявлений диапазона файлов по умолчанию используется extern
, если тип хранения не указан.
Ограничения задатчика начального значения
Для переменных длительности static
в качестве начальных задатчиков при их объявлении можно использовать только операторы констант, и никакие другие переменные использовать нельзя. Например
#include<stdio.h>
int i = 22;
int j = i + 1;
int main(void) {
}
При компиляции появится следующее сообщение об ошибке.
main.c:4:9: error: initializer element is not constant
4 | int j = i + 1;
| ^
Если переменная имеет длительность auto
, то такого ограничения нет, и для формирования задатчика начального значения можно использовать другие переменные, напр.
#include<stdio.h>
int main(void) {
int i = 22;
int j = i + 1;
}
Он будет компилироваться и запускаться нормально.
Невозможно обратиться к переменной регистрового хранилища
Поскольку регистровые
переменные типа хранения могут быть помещены в регистр, который не имеет адреса памяти, такие переменные не могут быть использованы в операциях взятия адреса, напр.
#include<stdio.h>
int main(void) {
register int i;
printf("%pn", &i);
}
При компиляции возникнет следующая ошибка.
main.c: In function 'main':
main.c:6:5: error: address of register variable 'i' requested
6 | printf("%pn", &i);
| ^~~~~~
Объект, к которому вы хотите обратиться, является register
переменной типа storage и не может быть обращен.
Ссылка
Вам может показаться странным, что extern
и static
переменные имеют одинаковую длительность, так зачем же иметь два типа хранилища? Это связано с тем, что помимо различия в длительности переменной, тип хранения также определяет связь переменной.
Перед описанием компоновки мы хотели бы ввести специальный термин, единица трансляции, который обозначает результат обработки одного необработанного файла через препроцессор, т.е. необработанный файл плюс заголовочные файлы, за вычетом частей, опущенных условными инструкциями препроцессора, т.е. то, что фактически передается компилятору для компиляции. Компилятор — это тот, кто фактически компилирует содержимое.
Связывание относится к способности объединять другие одноименные объявления в одно, и подразделяется на три типа связывания в зависимости от типа хранилища.
- нет связи.
- внутренняя связь.
- внешний линаж.
Они описаны отдельно ниже.
отсутствие связи
Все объявления внутри блока, которые не относятся к типу хранения extern
, не имеют связи, что означает, что они не могут быть интегрированы с другими одноименными объявлениями, т.е. в том же допустимом диапазоне не может быть других одноименных объявлений, например, следующих объявлений внутри блока и определенных переменных, которые могли бы вызвать ошибки при компиляции.
#include<stdio.h>
int main(void) {
int i; // 區塊內預設是 `auto`
int i = 10 // 區塊內預設是 `auto`
printf("%dn", i);
}
Сообщение об ошибке выглядит следующим образом
main.c: In function 'main':
main.c:5:9: error: redeclaration of 'i' with no linkage
5 | int i = 10
| ^
main.c:4:9: note: previous declaration of 'i' was here
4 | int i;
| ^
main.c:7:5: error: expected ',' or ';' before 'printf'
7 | printf("%dn", i);
| ^~~~~~
Компилятор считает, что переменная i
была многократно объявлена без конкатенации. Если вы измените программу на эту, она будет компилироваться и запускаться следующим образом
#include<stdio.h>
int main(void) {
int i;
printf("%dn", i);
}
Вы можете подумать, что это не завершает определение переменной i
, так что же именно является переменной i
при выполнении программы? Компилятор фактически автоматически завершит определение объявления auto
за вас, используя текущее содержимое сконфигурированной памяти в качестве начального значения, и если вы запустите программу, вы увидите переменную как странное значение (что не обязательно то, что вы увидите).
32551
Поскольку это текущее содержимое памяти, оно может быть разным при каждом запуске, например, если я запущу его снова, результат будет следующим
32710
Для обеспечения стабильных результатов не забудьте установить начальное значение для переменной auto
типа хранения.
Поскольку объявления без extern
в блоке являются несвязанными, даже если это статические
объявления, которые длятся дольше, чем блок, например.
#include<stdio.h>
int main(void) {
static int i;
static int i = 20;
printf("%dn", i);
}
Та же ошибка возникнет при компиляции, так как он является несвязанным.
main.c: In function 'main':
main.c:3:16: error: redeclaration of 'i' with no linkage
3 | static int i = 20;
| ^
main.c:4:16: note: previous declaration of 'i' was here
4 | static int i;
| ^
Однако, в отличие от auto
, компилятор объявит начальное значение определения равным 0 для static
в блоках, которые не завершены, так что если программа будет изменена следующим образом.
#include<stdio.h>
int main(void) {
static int i;
printf("%dn", i);
}
В результате
0
межэлектродная связь
Все статические
объявления, действующие в области видимости файла, имеют эту связь, что означает, что в области видимости файла могут быть объявления с одинаковыми именами, но эти объявления с одинаковыми именами в конечном итоге объединяются в одно, поэтому мы можем отделить переменные определения от простых объявлений, или даже одно в начале и одно в конце, как в
#include<stdio.h>
static int i;
int main(void) {
printf("%dn", i);
}
static int i = 10;
Внутренняя связанность также означает, что переменная не открыта для использования другими единицами перевода, так что если в разных единицах перевода есть статические
объявления с одинаковым именем, то это отдельные переменные.
Экстернальность (экстернальный линаж)
Все декларации extern
, а также неstatic
декларации, которые действительны в области видимости файла, имеют эту связь. Это означает, что в различных единицах перевода, составляющих полную программу, могут быть объявления с одинаковыми именами, и что эти объявления с одинаковыми именами в конечном итоге будут объединены в одно объявление, например.
-
main.c
#include<stdio.h> extern int i; int main(void) { printf("%dn", i); }
-
foo.c
extern int i = 10;
также могут быть скомпилированы и выполнены нормально.
Это полезно для написания библиотек, чтобы декларации extern
можно было записать в заголовочный файл таблицы, что позволит импортировать другие исходные файлы и найти определение в библиотеке при соединении, например.
-
main.c
#include<stdio.h> #include "foo.h" int main(void) { printf("%dn", i); }
-
foo.h
extern int i;
-
foo.c
#include "foo.h" int i = 10;
Все raw файлы, которым необходимо использовать библиотеку foo
, могут просто импортировать foo.h
и использовать переменную i
, определенную в foo.c
.
Индикатор extern
не ограничивается использованием вне функции, например, если вы измените main.c
только что на что-то вроде
#include<stdio.h>
int main(void) {
extern int i;
printf("%dn", i);
}
Вы также можете обратиться к переменной i
, определенной в foo.c
.
В частности, GCC предупреждает об определении переменных, помеченных extern
, например.
#include<stdio.h>
extern int i = 10; // 定義變數 i
int main(void) {
printf("%dn", i);
}
вы увидите предупреждающее сообщение следующего содержания
main.c:3:12: warning: 'i' initialized and declared 'extern'
3 | extern int i = 10;
| ^
10
Программа все еще может быть запущена.
предварительные определения
Для внешних объявлений, которые не имеют примитивного задатчика и имеют тип хранения static
или не указывают тип хранения, это называется предварительным определением. Если определение переменной с таким же именем не найдено в единице трансляции, компилятор автоматически завершит определение и установит значение примитива в 0, например.
#include<stdio.h>
static int i;
int j;
int main(void) {
static int k;
printf("%dn", i);
printf("%dn", j);
printf("%dn", k);
}
Если i
и j
не определены, и в исходном файле не найдено определение переменной с таким же именем, компилятор автоматически определит их и установит начальное значение равным 0. Результат будет следующим
0
0
0
Обратите внимание, что внешние объявления без типа хранения будут рассматриваться как внешние
типы хранения, если они конфликтуют с существующими объявлениями одноименной переменной, например.
#include<stdio.h>
static int i = 10;
int i;
int main(void) {
static int k;
printf("%dn", i);
}
Это приведет к ошибке компиляции.
main.c:4:5: error: non-static declaration of 'i' follows static declaration
4 | int i;
| ^
main.c:3:12: note: previous definition of 'i' was here
3 | static int i = 10;
| ^
Ошибка возникает потому, что i
объявляется сначала как static
, а затем как extern
, что является значением по умолчанию, когда тип хранения не добавлен. Если декларации одинаковые, то проблем нет.
#include<stdio.h>
int i = 10;
extern int i;
int main(void) {
static int k;
printf("%dn", i);
}
Тем не менее, хорошей практикой является явное определение этого параметра во время объявления, а не полагаться на неявное поведение компилятора по умолчанию.
Заметки о GCC
В GCC 9 и более ранних версиях неопределенные переменные с одинаковыми именами объединяются в одну переменную, например, следующие три файла.
-
main.c
#include<stdio.h> int i; int main(void) { printf("%dn", i); }
-
foo.c
int i;
-
foo1.c
int i = 10;
Фактический результат выглядит следующим образом.
10
i
в этих трех файлах на самом деле является одной и той же переменной. Это может показаться удобным, но могут возникнуть потенциальные проблемы, например, если вы не писали foo1.c
, поэтому вы не знаете, что он содержит определенную переменную i
, и что эта i
является ключевой переменной, которая управляет определенной функцией в foo1.c
. code> является ключевой переменной, которая управляет определенной функцией в main.c
, и если значение i
будет изменено в main.c
, это может вызвать ошибку программы.
Чтобы избежать этого, начиная с GCC 10, по умолчанию неопределенные переменные рассматриваются как отдельные переменные, так что если в другой единице трансляции есть переменная с таким же именем, на этапе соединения будет выдана ошибка.
/usr/bin/ld: /tmp/cc3PEcNo.o:(.bss+0x0): multiple definition of `i'; /tmp/ccEpD2Pm.o:(.bss+0x0): first defined here
/usr/bin/ld: /tmp/ccWlu8wn.o:(.data+0x0): multiple definition of `i'; /tmp/ccEpD2Pm.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
Он будет жаловаться, что i
повторно определен. Обратите внимание, что это ошибка компоновщика (ld), поскольку отдельные файлы компилируются успешно, но множественные i
не обнаруживаются до стадии компоновки.
Если у вас есть старый проект, который нужно поддерживать, и вы сильно полагаетесь на GCC 9, который помогает вам объединять одноименные неопределенные переменные в одну переменную, вы можете добавить флаг -fcommon
в GCC 10, чтобы компилировать компоновщик так же, как в GCC 9; и наоборот, если вы используете GCC 9 и не хотите случайно попасться на умолчания GCC 9, Вы можете добавить флаг -fno-common
, чтобы удалить подход по умолчанию.
Объявления и определения функций
Та же концепция может быть применена к функциям, которые также могут отделять простые объявления от определений, например.
#include<stdio.h>
int fact(int);
int fact(int n) {
return (n < 2) ? 1 : n * fact(n - 1);
}
int main(void) {
printf("%dn", fact(5));
}
Строка 3 — это объявление функции, в котором указывается тип возвращаемого значения и тип аргумента, с точкой с запятой в конце, когда функция просто объявлена. Этот формат также облегчает создание библиотек, например, мы можем разбить только что созданную программу на следующие части
-
fact.c
int fact(int n) { return (n < 2) ? 1 : n * fact(n - 1); }
-
fact.h
int fact(int);
-
main.c
#include<stdio.h> #include"fact.h" int main(void) { printf("%dn", fact(5)); }
Все исходные файлы, которые должны использовать функцию fact
, могут быть просто импортированы в fact.h
и связаны с fact.c
для компиляции.
Важно отметить, что функция по умолчанию без типа доступа является extern
, что, если оставить это без внимания, может привести к дублированию функций, например, добавив еще одну версию функции fact
в вышеупомянутый main.c
, такую как
#include<stdio.h>
#include"fact.h"
int fact(int n) {
if(n < 2) return 1;
return n * fact(n - 1);
}
int main(void) {
printf("%dn", fact(5));
}
По ссылке будет видно, что существует две функции fact
, и возникает ошибка дублирования определения: Enter fullscreen mode
/usr/lib/gcc/x86_64-alpine-linux-musl/9.3.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/
ccgamBCl.o: in function `fact':
main.c:(.text+0x0): multiple definition of `fact'; /tmp/ccGKjPhP.o:fact.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
Чтобы избежать этого, функции, которые не предназначены для обнародования в других исходных файлах, должны быть явно связаны с директивой static
, чтобы они были связаны только внутри: Ввод полноэкранного режима
#include<stdio.h>
#include"fact.h"
static int fact(int n) {
if(n < 2) return 1;
return n * fact(n - 1);
}
int main(void) {
printf("%dn", fact(5));
}
Тем не менее, скомпилировать ссылку таким образом все же возможно.
main.c:4:12: error: static declaration of 'fact' follows non-static declaration
4 | static int fact(int n) {
| ^~~~
In file included from main.c:2:
fact.h:1:5: note: previous declaration of 'fact' was here
1 | int fact(int);
| ^~~~
Это происходит потому, что мы импортировали одноименный заголовочный файл из другой версии, что вызвало конфликт типов доступа между двумя объявлениями.
#include<stdio.h>
// #include"fact.h"
static int fact(int n) {
if(n < 2) return 1;
return n * fact(n - 1);
}
int main(void) {
printf("%dn", fact(5));
}
Тогда вы сможете скомпилировать и использовать правильную версию функции в том же файле.
Резюме
В этой статье дается систематическое и подробное объяснение самых основных действий программ на языке С. Надеюсь, она будет полезна для начинающих, так как при соблюдении основных правил они смогут решать все виды программ без какой-либо правдоподобной путаницы.