Переменные
Что такое переменные
Переменные - это фундаментальные объекты данных в языке С, без них не пишется ни одна программа.
Они предназначены для хранения данных и манипулирования этими данными.
Каждая переменная обязательно должна иметь:
- тип, позволяющий определить какие данные мы будем хранить;
- идентификатор, состоящий из буквенно-цифровых символов;
Переменную можно проинициализировать и присвоить ей начальное значение, в зависимости от типа.
Еще мы можем добавить к объявлению переменной квалификаторы и классы памяти.
Сама по себе переменная - это некоторая ячейка памяти, которая хранит данные, которые мы туда помещаем, а переменной она называется потому что данные могут меняться с течением времени и обстоятельств.
Ключевые слова в языке C
В любом языке присутсвуют “ключевые слова”, под ними я подразумеваю встроенные идентификаторы языка, т.е зарезервированные языком наборы некоторых идентификаторов (имен). Например к таким идентификаторам могут относиться типы данных и другие конструкции языка:
1
2
3
4
5
6
7
8
9
10
auto enum restrict unsigned
break extern return void
case float short volatile
char for signed while
const goto sizeof _Bool
continue if static _Complex
default inline struct _Imaginary
do int switch
double long typedef
else register union
Данный набор взят из стандарта ISO/IEC 9899:1999
Данные ключевые слова нельзя использовать в качестве имен переменных, функций, стуктур и т.д
Помимо ключевых слов языка, существуют также зарезервированные компилятором дополнительные команды препроцессора, ключевые слова и атрибуты.
Для GCC:
Имена переменных
Имена переменных и других объектов в коде используются для идентификации, и для удобного использования объектов при обращении к ним.
“имя переменной” - это ее идентификатор, стандарт определяет некоторые правила для идентификаторов. Разрешается использовать латинские буквы верхнего и нижнего регистра, нижнее подчеркивание и цифры:
1
2
3
4
5
6
a b c d e f g h i j k l m
n o p q r s t u v w x y z
A B C D E F G H I J K L M
N O P Q R S T U V W X Y Z _
0 1 2 3 4 5 6 7 8 9
Данный набор символов относится не только к именам переменных, он относится к любым идентификаторам в коде, будь то имена функций структур или констант!
Имя идентификатора не может начинаться с цифры или состоять только из цифр
Пример валидных имен переменных:
1
2
3
4
5
6
int _name_;
char nAmEoFvAr;
int _1_2_3_4_5;
int my;
int a,b,c,d,e;
int var_name1;
Пример НЕ валидных имен переменных:
1
2
3
4
int 1_name;
char 123;
int variable-name;
int if;
Также стоит отметить, что большинство идентификаторов начинающихся с нижнего подчеркивания ‘_’, могут быть зарезервированы разными библиотеками и если вы не хотите нарваться на неприятности, то лучше не использовать нижнее подчеркивание в начале имени идентификатора.
Имена переменным, функциям, структурам и другим объектам стоит давать осознанные. Также не стоит делать имена слишком длинными или слишком короткими. Есть некоторые соглашения об именах идентификаторов например Snake case или Camel case.
Я использую оба соглашения но в С - Snake case, в С++ - Camel case. Последний свой проект я разрабатывал согласно GNU Coding Standards в котором есть отдельный раздел про naming. При написании кода для ядра Linux существует свой стиль - Linux kernel coding style.
Подводя итог:
- Идентификаторы не могут начинаться с цифры;
- Идентификаторы могут состоять из цифр и нижнего подчеркивания;
- Идентификаторы могут состоять из латинских букв верхнего и нижнего регистра;
- В качестве имени идентификатора нельзя использовать зарезервированые ключевые слова;
- Идентификатор может начинаться с нижнего подчеркивания, но имя может быть зарезервировано;
Область видимости (scope)
Область видимости (scope) - это область в коде программы, в которой видимость ограничина одним из четырех типов областей:
function scope
Каждая функция имеет свою область видимости ограниченную фигурными скобками ‘{}’.
1
2
3
4
5
int main()
{
/* область видимости ограничена этим блоком */
return 0;
}
Это говорит нам о том, что любые переменные(идентификаторы) объявленные внутри функции будут ограничены областью видимости данной функции. Например, объявим две функции:
1
2
3
4
5
6
7
8
9
10
11
int fn1() {
int a = 10;
return 0;
}
int fn2() {
int b = 20;
// переменная 'a' - недоступна в данной функции
return 0;
}
В данном случае функция fn1()
и fn2()
имеют свои области видимости и все переменные внутри функций являются локальными по отношению к функции в которой они объявлены.
Время жизни таких переменных(идентификаторов) ограничено областью видимости функции.
block scope
Область видимости на уровне блока представляет собой отдельную область видимости внутри пары фигурных скобок:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
int main()
{ /* Область видимости на уровне функции */
int test = 10;
{ /* Область видимости на уровне блока 1 */
printf("(block #1 (start)): %d\n", test);
int test = 20;
{ /* Область видимости на уровне блока 2 */
printf("(block #2 (start)): %d\n", test);
int test = 30;
printf("(block #2 (end)): %d\n", test);
}
printf("(block #1 (end)): %d\n", test);
}
printf("(main scope): %d\n", test);
return 0;
}
На выходе мы получим следующий результат:
1
2
3
4
5
(block #1 (start)): 10
(block #2 (start)): 20
(block #2 (end)): 30
(block #1 (end)): 20
(main scope): 10
При первом вызове printf()
мы получаем число 10, находясь внутри области видимости первого блока. Это говорит нам о том что первый вложенный блок “видит” переменную test
со значением 10 из области видимости функции main()
. Это также было бы справедливо и для остальных вложенных блоков.
В первом блоке мы объявили еще одну переменную с таким же именем test
, тем самым “перекрывая” переменную test
из области видимости функции main
. Поэтому в выводе второго вложенного блока мы получили значение 20. Тоже самое справедливо и для третьего вывода, где мы получили значение 30.
При выходе из второго вложенного блока переменная test
со значением 30 перестает существовать, но так как мы находимся в первом вложенном блоке в выводе мы видим число 20. Тоже самое справедливо и при выходе из первого вложенного блока, поэтому при выходе из всех блоков на уровне области видимости функции у нас по прежнему переменная test
остается со значением 10.
file scope
Область видимости на уровне файла относится к области видимости на уровне всего файла где пишется код. Данную область видимости еще называют глобальной областью видимости, а переменные объявленные внутри этой области - глобальными переменными.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* main.c */
#include <stdio.h>
int test = 100; /* Переменная имеет область видимости на уровне файла */
void fn() {
printf("fn() test = %d\n", test);
}
int main() {
fn();
printf("main() test = %d\n", test);
int test = 200;
printf("main() test = %d\n", test);
return 0;
}
В результате получаем:
1
2
3
fn() test = 100
main() test = 100
main() test = 200
function prototype scope
Область видимости на уровне прототипа функции относится только лишь к прототипам функций и означает, что параметры функции в прототипе функции имеют область видимости до конца объявления этого прототипа.
1
2
3
void func1 (int a, int b);
void func2 (int a, int b);
void func3 (int a, int b);
В данном случае параметры с одинаковыми именами a и b могут быть объявлены сколько угодно раз.
Другой пример:
1
void func(int a, int a);
Выдаст соответствующую ошибку:
1
2
3
4
5
6
s.c:4:20: error: redefinition of parameter 'a'
4 | void f( int a, int a );
| ~~~~^
s.c:4:13: note: previous definition of 'a' with type int
4 | void f( int a, int a );
| ~~~~^
Что говорит о том, что в области видимости прототипа функции мы попытались переопределить переменную a второй раз
Классы памяти
Класс памяти (storage class) - определяет тип хранения объекта. Только один класс памяти может быть определен для объекта. Если класс памяти не указан явно, то объект получит класс памяти по умолчанию, выбор определенного класса по умолчанию зависит от того где объект определен, внутри функции(internal) или за ее пределами (external). Для объектов объявленных за пределами функций по умолчанию применяется класс памяти extern
, а для объектов объявленных внутри функции auto
.
1
2
3
4
5
6
7
8
9
10
/* main.c */
int z; /* класс памяти по умолчанию будет extern */
int main()
{
int a; /* класс памяти по умолчанию будет auto */
return 0;
}
extern
Данный класс памяти может применяться как к переменным так и к функциям. Используется он обычно в контексте внешнего связывания и говорит компилятору о том, что переменная или функция объявлены где-то в другом файле.
1
2
3
4
5
/* functions.c */
int test = 100;
void println(const char *str) {
printf("%s\n", str);
}
1
2
3
/* main.c */
extern int test;
extern void println(const char *str);
Функции по умолчанию имею класс памяти extern
, как и глобальные переменные, поэтому явного указания не требуется. Однако, явное использование extern
для функций может быть полезно для ясности кода.
Одна из интересных конструкций где еще может применяться класс памяти extern
, это в коде на C++:
1
2
3
extern "C" {
unsigned void bump(int, char);
};
Данная конструкция применяется в языке C++ для того чтобы компилятор С++ не манглировал ссылку на функцию bump.
Если например хочется использовать какие-то функции из кода на С++ в коде на С, то такая конструкция позволит не манглировать имена функций, что позволит использовать данные функции внутри кода на С.
1
2
3
4
5
extern "C" {
void p(int){
/* не манглируется */
}
};
При использовании extern "C"
компилятор отключает механизм манглирования имен, который применяется по умолчанию в C++, что позволяет функциям иметь “C-стиль” интерфейс, который легко использовать из кода на языке С.
auto
Данный класс памяти применяется по умолчанию к локальным переменным внутри функции, явное указание данного ключевого слова не имеет смысла. Продолжительность жизни таких переменных ограничена блоком.
Например:
1
2
3
4
5
6
7
8
9
int main() {
int a; /* класс памяти = auto */
{
int b; /* класс памяти = auto */
} /* в конце блока память выделенная под b будет освобождена */
return 0;
}/* в конце блока память выделенная под a будет освобождена */
static
Данный класс памяти может применяться как переменным так и к функциям в зависимости от контекста использования данный класс памяти ведет себя по разному.
При использовании внутри функции перед объявлением переменной:
1
2
3
4
5
6
7
8
9
10
11
void func() {
static int a = 0;
a++;
printf("a = %d\n", a);
}
int main() {
func(); /* Выведет a = 1 */
func(); /* Выведет a = 2 */
return 0;
}
Переменная ‘a’ имеет статическую продолжительность жизни. Это означает, что переменная будет создана только один раз при первом запуске функции и в дальнейшем она будет сохранять свое значение между другими вызовами данной функции.
При использовании ключевого параметра static
для глобальных переменных или функций внутри текущего файла, переменные или функции становятся доступными только на уровне текущего файла, в других файлах получить доступ к таким функциям или переменным не представляется возможным.
1
2
3
/* main.c */
static int test = 10;
static void println(const char *str);
1
2
3
/* functions.c */
test = 100; /* переменная test не доступна */
println(); /* функция println() не доступна */
register
Данный класс памяти указывает компилятору на то, что переменная должна быть размещена в регистре процессора по возможности. Это не жесткое требование и компилятор может проигнорировать его.
Переменную с таким классом памяти можно объявлять только внутри блока. Адресация для данного класса памяти запрещена. При объявлении массива с данным классом памяти, конвертировать такой массив в указатель не получится.
1
2
3
4
5
int main()
{
register int a = 500;
return 0;
}
Данное ключевое слово используется редко, так как современные компиляторы хорошо оптимизируют код и размещают переменные в регистрах более эффективно.
Квалификаторы
const
Объект объявленный с квалификатором const
становится неизменяемым. Важно заметить, что при использовании данного квалификатора необходимо инициализировать объект начальным значением. Это отличает константы от обычных переменных, которые могут быть объявлены без начального значения и присвоены позже.
1
2
3
4
5
const int FOO = 314; /* Правильно */
/* Неправильно */
const int FOO;
FOO = 100;
volatile
К объекту объявленному с квалификатором volatile
не будут применяться оптимизации компилятора, а также объект не будет подвержен кэшированию. Самый простой пример, который можно описать, связанный с данным квалификатором volatile
- это обращение к памяти в цикле.
1
2
3
4
5
6
7
8
int main()
{
volatile int *p = (int *)0xfffffffa;
while(1) {
if (*p)
break;
}
}
В данном случае компилятор не будет производить оптимизации над указателем *p
, что позволит в цикле производить постоянное обращение к памяти. Если бы мы не указали квалификатор volatile
то при оптимизации кода, адрес памяти в указателе *p
был бы сохранен и никаких обращений к памяти не происходило бы.
restrict
Квалификатор restrict
дает обещание компилятору о том, что программист гарантирует, что доступ к объекту через указатель объявленный как restrict
будет осуществляться только через этот указатель и этот указатель не будет пересекаться с памятью других указателей. Это позволяет компилятору лучше оптимизировать код.
Данный квалификатор может быть применен только к указателю.
Хорошим примером могут служить две функции memcpy и memmove.
Прототип функции memcpy:
1
void *memcpy(void dest[restrict .n], const void src[restrict .n], size_t n);
Прототип функции memmove:
1
void *memmove(void dest[.n], const void src[.n], size_t n);
При использовании memcpy
использовать пересекающиеся области памяти нельзя, но используя memmove
это делать можно:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <string.h>
int main() {
char src[] = "Hello, world!";
char dest[20];
/* Копируем строку в dest (в непересекающуюся область памяти) */
memcpy(dest, src, strlen(src) + 1);
/* Перемещаем "world!" на место "Hello, " используя одну и туже область памяти*/
memmove(src + 7, src, 7);
return 0;
}
Заключение
В заключение можно привести пример прототипа объявления переменной:
1
[квалификатор][класс хранения] тип имя [ = начальное значение]
Где:
- квалификатор: Может быть
const
,volatile
,restrict
или отсутствовать. - класс хранения: Может быть
auto
,register
,static
,extern
или отсутствовать. - тип: Тип данных переменной (например,
int
,double
,char
,struct
,enum
, указатель и т.д.). - имя: Имя переменной, используемое для обращения к ней в коде.
- начальное значение: Необязательное начальное значение переменной.