Переменная (variable) - ячейка памяти с заданным именем, по которому к ней можно обратиться для записи и чтения данных.
C/C++ - язык со статической типизацей. У переменной при создании указывается тип и она сохраняет его на всём протяжении своего существования. Если в переменную записать данные другого типа - они будут по возможности преобразованы к типу самой переменной, если это невозможно - будет ошибка компиляции. В других языках, например Python или JavaScript, типизация динамическая, то есть в одну и ту же переменную можно наваливать данные разных типов и она каждый раз будет менять свой тип на новый.
Создание переменной #
Переменная создаётся при помощи конструкции тип_данных имя;
, это действие называется определением переменной (definition):
int a;
uint8_t b;
float c;
В этот момент процессор выделяет память под размер указанного типа по следующему свободному адресу в памяти и даёт его нам под управление по указанному имени
Можно создать несколько переменных одного типа, указав их через запятую:
int a, b, c;
float d, e, f;
Присвоение значения #
Переменной можно присвоить (записать) данные при помощи оператора =
из:
- Численной константы
- Другой переменной
- Результата функции
- Логического и арифметического выражения
int a;
a = 123;
a = 0xff + 123;
a = b;
a = b + 10;
a = foo();
Переменная может хранить данные только своего типа, поэтому присвоенные данные будут принудительно сконвертированы в её тип:
- Если
bool
переменной присвоить0
- она будетfalse
(0). Если любое отличное от нуля значение - она будетtrue
(1):
bool v;
v = 0; // false
v = 1; // true
v = -9; // true
- Если целочисленной переменной присвоить значение больше, чем она может хранить - в неё запишутся только младшие байты, которые поместятся в текущий тип:
uint8_t v;
v = 123; // = 123
v = 0xAB; // = 0xAB
v = 0xABCD; // = 0xCD
- Если целочисленной переменной присвоить тип меньшего размера - лишние байты "слева" будут заполнены нулями, т.е. число запишется полностью корректно:
uint16_t v;
v = 0xAB; // = 0xAB = 0x00AB
- Если беззнаковой переменной присвоить отрицательное значение - оно бинарно запишется как знаковое по правилам хранения знаковых чисел, то есть в результате получится некорректное число:
uint16_t v;
v = -10; // = 65413
- Если целочисленной переменной присвоить
float
- дробная часть будет отброшена, а не округлена:
uint16_t v;
v = 3.8; // = 3
- Если переменной
float
присвоить целочисленный тип - он конвертируется во float с нулевой дробной частью
float v;
v = 3; // ~ 3.0
Инициализация переменной #
Переменной можно присвоить значение и при создании, такой процесс называется инициализацией: тип_данных имя = значение;
int a = 1234;
float b = 3.14;
bool v = false;
При создании нескольких переменных одного типа можно выборочно их инициализировать:
int a, b = 123, c;
float d = 3.14, e, f = 0.25;
Числа в программе #
Любое число в коде программы является константой соответствующего типа, компилятор сам определяет её стандартный тип следующим образом:
123; // const int, знаковая!
3.14; // const double
3.; // const double
Также можно явно указать конкретный тип при помощи литерала - буквенное обозначение в конце числа:
Тип | Литерал |
---|---|
unsigned int |
U , u |
long |
L , l |
unsigned long |
UL , ul |
long long |
LL , ll |
unsigned long long |
ULL , ull |
float |
f |
123u; // unsigned int
12345L; // long
0xabcdull; // long long
3.14f; // float
3.f; // float
Автоматический тип #
C/C++ поддерживает автоматическую типизацию переменных, для этого нужно указать тип auto
при инициализации переменной, чтобы компилятор косвенно понимал, какой тип в ней будет храниться:
auto a = 12345; // int
auto b = 12345L; // long
auto bb = b; // long
auto c = 3.14; // double
auto d = 3.14f; // float
auto e; // ошибка! Не инициализировано
Это также позволяет визуально сократить программу в ситуациях, когда нужно создать переменную какого-то пользовательского типа с длинным названием. Например, функция foo()
возвращает данные типа my_very_fancy_type<blabla<XYZ_123>>*
:
auto a = foo(); // здесь a имеет тип my_very_fancy_type<blabla<XYZ_123>>*
Вес переменной #
Узнать вес переменной из программы можно при помощи оператора sizeof
:
short a;
sizeof(a); // 2 байта
Область видимости #
Любая переменная имеет область видимости (scope) - часть программы, в пределах которой есть доступ к этой переменной. Переменную "видно" из текущей строки кода то есть к ней можно обратиться для чтения и записи. Есть два варианта - глобальная и локальная области видимости:
- Глобальная (global) - переменную видно из любой точки программы
- Локальная (local) - переменную видно внутри блока кода, в котором она создана, а также во всех вложенных
Если проводить аналогии, то блок кода - это коробка, переменная - игрушка, область видимости - пространство внутри коробки с игрушкой. В таком случае программа - это главная самая большая коробка, в которой находятся все остальные коробки (комната). Игрушку видно из коробки, в которой она лежит, а также изо всех коробок внутри этой коробки. Нельзя увидеть игрушку, находясь в соседней коробке. Игрушка, которая лежит в главной коробке, имеет глобальную область видимости, её видно изо всех коробок. Игрушки во вложенных коробках - локальную область видимости:
int global; // глобальная
int main() {
int local; // локальная
// global видно здесь
// local видно здесь
// local2 НЕ видно здесь
{
int local2; // локальная
// global видно здесь
// local видно здесь
// local2 видно здесь
}
// local2 НЕ видно здесь
}
Глобальная переменная #
Создаётся на самом верхнем уровне программы, вне всех блоков кода и функций:
- Имеет глобальную область видимости - к ней можно обратиться из любого места в программе
- Выделяется в памяти однократно при запуске программы
- При создании автоматически инициализируется значением по умолчанию для своего типа данных - для численных типов это
0
- Существует в памяти на всём протяжении работы программы
- Компилятор может посчитать, сколько весят глобальные переменные - это будет минимальный объём памяти, который программа займет во время работы
int global1 = 0;
int global2; // автоматически инициализирована нулём (0)
int main() {
// global1 и global2 видно здесь
}
Локальная переменная #
Создаётся внутри блока кода:
- Имеет область видимости внутри своего блока кода, а также во всех вложенных в него блоках
- Выделяется в памяти заново каждый раз в том месте, где создаётся
- При создании не инициализируется и имеет значение, которое находилось в этой области памяти на момент создания - по сути получает случайное значение. Это сделано с целью оптимизации: инициализация выполняется не мгновенно
- Автоматически удаляется из памяти при выходе из области определения, т.е. после закрывающей скобки
}
своего блока
Локальные переменные в C/C++ являются автоматическими - удаляются из памяти при выходе из своей области видимости
{
int local1; // имеет случайное значение
int local2 = 0; // инициализация вручную
{
int local3;
// здесь существуют и доступны все: local1, local2, local3
}
// здесь local3 удаляется из памяти и больше недоступна
}
// здесь local1 и local2 удаляются из памяти и больше недоступны
Нужно обязательно инициализировать локальную переменную, если дальше по коду под её значением подразумевается 0, например для счётчика
Интересный момент - тип bool
при явном преобразовании и присвоении автоматически принимает значение 0
и 1
. А вот если создать его локально и не инициализировать - он хранит случайное 1-байтное число, а не только ноль и единицу.
Статическая переменная #
Cоздаётся как локальная (внутри блока кода), но с ключевым словом static
:
- Имеет локальную область видимости, как локальная
- Выделяется в памяти однократно при запуске программы, как глобальная
- Сама инициализируется значением по умолчанию, как глобальная
- Ручная инициализация выполнится только один раз при запуске программы
- Не удаляется из памяти и сохраняет значение на протяжении работы программы, как глобальная
{
static int var; // инициализирована 0
}
// здесь var уже недоступна, но существует в памяти и хранит значение
Если глобальная переменная объявлена со словом static
- она будет доступна только внутри текущего исходного файла. Подробнее об этом - в следующих уроках
Наглядный пример отличия локальной переменной от статической:
{
int local = 10; // каждый раз инициализируется ЗАНОВО
// здесь local ВСЕГДА равна 10
local = local + 1;
// здесь local равна 11
static int local_st = 10; // инициализируется ТОЛЬКО ПРИ ЗАПУСКЕ программы
local_st = local_st + 1;
// здесь local_st увеличивается на 1 с каждым вызовом (11, 12, 13, 14...)
}
// здесь local удаляется из памяти
// здесь local_st существует, но недоступна (вне области видимости)
Если вызвать этот блок кода несколько раз подряд, то локальная переменная всегда будет равна 10
, а статическая начнёт увеличиваться на единицу с каждым вызовом!
Порядок выполнения #
Переменная может использоваться только после создания - код программы выполняется сверху вниз и нельзя работать с тем, что ещё не существует:
{
//var = 4; // ошибка!
int var;
var = 3;
}
Конфликт имён #
Нельзя создавать переменные с одинаковыми именами и одинаковой областью видимости - будет ошибка компиляции. В то же время можно создавать переменные с одинаковыми именами и разной областью видимости - в этом случае новая переменная внутри своей области видимости "перекроет" имя старой. Перекрытие происходит в момент создания новой переменной:
int var; // #1 - глобальная
//int var; // ошибка, такое имя уже есть в этой области
int main() {
var = 1; // меняем ГЛОБАЛЬНУЮ var #1
int var; // #2 - "перекроет" глобальную var
var = 2; // меняем ЛОКАЛЬНУЮ var #2
{
int var; // #3 - перекроет локальную #2
var = 3; // меняем локальную var #3
}
var = 4; // меняем локальную var #2
//int var; // ошибка, уже есть в этом блоке - #2
}
Константы (constant) нужны для удобства программиста, чтобы помечать данные, которые нельзя изменять. Константы позволяют:
- Запретить менять то, что менять нельзя. Это касается как своего кода, так и работы с чужими библиотеками, где могут быть const сущности
- Повысить читаемость программы и избавиться от "магических чисел" в коде: номеров пинов, адресов регистров, количества светодиодов в ленте и т.д.
Константы #
По сути это переменная, которую можно использовать только для чтения. Ключевое слово const
добавляется перед или после типа данных: const тип имя
или тип const имя
:
- Константа создаётся в памяти по тем же правилам, что и переменная, но с ключевым словом
const
- Константа обязательно должна быть инициализирована значением соответствующего типа:
const тип_данных имя = значение;
- Константа подчиняется тем же правилам области видимости, что и переменная
- Менять значение константы нельзя, на то она и константа. Любая попытка изменения значения константы приведёт к ошибке компиляции
Любое самостоятельное число в программе является константой соответствующего типа данных
//const int a; // ошибка, не инициализирована
const int b = 123;
b = 456; // ошибка, изменение константы
123 = 456; // ошибка, изменение константы
Константа численного типа может быть "оптимизирована" компилятором - вырезана из программы и заменена своим значением, чтобы не занимать место в оперативной памяти:
const float pi = 3.14;
float a = pi * 2.0;
// скорее всего компилятор просто заменит константу pi на число 3.14
"Магические" числа #
Числа в коде программы, смысл и назначение которых не понятны из контекста, называются "магическими" (magic numbers). Использование таких чисел является плохой практикой и ухудшает читаемость программы, вынуждает писать дополнительные комментарии к коду. А если это чужой код без комментариев - поди разберись, что имел в виду уважаемый автор. Магические числа нужно задавать константами, тогда код станет самодокументированным:
// под setPin() подразумевается подача напряжения на пин контроллера
// что здесь происходит, кого включаем?
setPin(3);
// понятно, но избыточно
setPin(3); // подать питание на светодиод
const int LED_PIN = 3; // пин светодиода
// другое дело!
setPin(LED_PIN);
Подобные константы обычно объявляются в самом начале файла с программой