View Categories

Переменные и константы

Переменная (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);

Подобные константы обычно объявляются в самом начале файла с программой

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх