В этой статье описываются разные способы объявления переменных, доступных в языке C/C++. Рассматриваются лучшие практики объявления, приводящие к более лаконичному и читаемому коду. Особое внимание уделено объявлению переменных-указателей и стилю такого объявления, используя который можно избежать некоторых распространённых ошибок.

1. Пообъявляем тут всякое...

В статически типизированных языках, к которым в полной мере относятся язык C/C++, перед непосредственным использованием переменной её необходимо заранее объявить. Это объявление подразумевает под собой чёткое указание типа переменной, и, при необходимости, её инициализационного значения:

  1. /* Объявление переменной var1 без инициализации */
  2. int32_t var1;
  3. /* Объявление переменной var2 с инициализацией */
  4. int32_t var2 = 10;

В коде выше показан пример объявления переменных var1 и var2 таким образом, что типы этих переменных соответствуют int32_t (32-битному целому числу со знаком, способным хранить целочисленные значения в диапазоне -2147483648 ... +2147483647). Этот тип данных, а также похожие типы int8_t, int16_t, uint8_t, uint16_t и др. описаны и могут быть использованы в программе при подключении заголовочного файла stdint.h. Пример также демонстрирует объявление переменной без инициализации (для var1) и с инициализацией переменной целочисленным значением 10 (для var2).

В момент выполнения программы операционной системой (например 32-х битной) эти строчки кода «попросят» её выделить в оперативной памяти ячейки с некими адресами, которые выберет сама система, и размерностью в 32 бита каждая (помним, что тип переменных int32_t). Тут правильнее будет сказать, что для каждой переменной выделятся 4 идущие друг за другом ячейки размерностью по 1 байт (8 бит) каждая, но я опущу пока эти подробности для простоты объяснения. Также важно чётко для себя уяснить, что общий размер ячейки для хранения значения переменной будет определяться не разрядностью операционной системы, а исключительно типом этой переменной! Графически эти ячейки памяти можно изобразить так:

Графическое представление ячеек в оперативной памяти

Рис. 1. Графическое представление ячеек в оперативной памяти при выполнении программы. Адрес занимает 32 бита, так как предполагаем, что используется 32-х битная операционная система. Значение переменной занимает 32 бита, так как предполагается, что её тип соответствует int32_t.

Адрес ячеек памяти представлен в виде 32-битного числа, записанного в шестнадцатеричной системе счисления. Так очень удобно представлять адреса потому, что один байт, состоящий из 8 бит, можно удобно представить всего 2-мя шестнадцатеричными цифрами, например 6A. Обратите внимание, что переменная var1 после объявления содержит некоторое «мусорное» значение, которое, собственно, мы не устанавливали. Это происходит из-за того, что при отсутствии начальной инициализации операционная система просто выделяет память под переменную с теми значениями битов, которые были выставлены каким-то процессом, ранее занимавшим именно этот участок памяти. Здесь надо быть очень внимательным и случайно не использовать значение этой переменной с мусором в каких-либо расчётах, так как результат таких расчётов вряд ли будет адекватным. Имеющий опыт программирования в C/C++ может заявить, что мусорные значения имеют только объявленные локальные переменные, а вот статические и глобальные переменные по умолчанию инициализируются значением 0. Да, это так, но лучше лишний раз не рисковать и инициализировать все переменные перед их непосредственным использованием.

2. Так, int32_t, uint_32_t…а можно поэлегантнее?

Перечисленные в заголовочном файле stdint.h стандартные типы данных очень важны для написания стабильного и переносимого на разные платформы кода. Само название типа (например, int32_t) содержит информацию о том, сколько бит будет выделено под переменную, вне зависимости от производителя системы, её разрядности и других параметров! В контраст такой чёткости можно привести стандартный тип int, который на одних системах может занимать 32 бита (4 байта), а на других 16 бит (2 байта), делая поведение программы отчасти непредсказуемым. Хорошо, будем всегда и везде использовать типы данных из stdint.h, вот только бы сделать так, чтобы сами названия типов выглядели не такими длинным и неуклюжими, хотя на вкус и цвет, как говориться... Тем не менее в языке С/С++ есть возможность переопределить название типа так, как мы хотим с помощью ключевого слова typedef. Конечно, выбор всегда за вами, как именно переопределять типы, но я рекомендую присмотреться на обозначения, которые сам часто использую в своих проектах:

  1. #include <stdint.h>
  2. typedef int8_t s8; /* Целое со знаком. 8 бит */
  3. typedef uint8_t u8; /* Целое без знака. 8 бит */
  4. typedef int16_t s16; /* Целое со знаком. 16 бит */
  5. typedef uint16_t u16; /* Целое без знака. 16 бит */
  6. typedef int32_t s32; /* Целое со знаком. 32 бит */
  7. typedef uint32_t u32; /* Целое без знака. 32 бит */
  8. typedef int64_t s64; /* Целое со знаком. 64 бит */
  9. typedef uint64_t u64; /* Целое без знака. 64 бит */
  10. typedef float f32; /* Число с плавающей точкой. 32 бит */
  11. typedef double f64; /* Число с плавающей точкой. 64 бит */

Такое переопределение можно вынести в отдельный заголовочный файл и подключать его к тем исходным файлам, где необходимо использовать эти типы. С подобным переопределением, чтобы создать переменную var целого беззнакового типа, занимающую 16 бит (с диапазоном возможных значений от 0 до 65535) нужно всего то написать следующую элегантную строчку:

  1. u16 var;

3. Объявление переменных-указателей

Язык C/C++ славится своей возможностью объявлять не только переменные для хранения конкретных значений, но и переменные-указатели (или просто указатели), которые способны хранить адреса, указывающие на переменные, хранящие какие-либо значения. Благодаря этому можно создавать сколь-угодно сложные структуры данных, быстро передавать в функцию указатели на эти структуры и работать с ними, хранить в них адрес выделенного блока в оперативной памяти и т.д. Наличие указателей и средств работы с ними делают язык очень гибким и быстрым, за что его и любят многие программисты. 

Для того, чтобы объявить указатель, необходимо перед его именем поставить символ звёздочки *, а затем перед звёздочкой написать тот тип переменной, на которую будет указывать объявляемый указатель. Вот пример, в котором сперва объявляется переменная целочисленного типа, а затем и указатель, которому тут-же присваивается адрес этой переменной (адрес любой переменной можно получить с помощью унарного оператора &):

  1. int32_t var = 10; /* Объявление переменной */
  2. int32_t *pvar = &var; /* Объявление указателя */
  3. *pvar = 20; /* Доступ к значению переменной var через разыменование указателя pvar */

Для наглядности вновь изобразим создаваемые ячейки памяти с помощью блок-схемы:

Графическое представление ячеек в оперативной памяти

Рис. 2. Графическое представление ячеек в оперативной памяти: var представляет собой переменную типа int32_t с хранимым значением 10, pvar представляет собой указатель на переменную var типа int32_t (т.е в указателе хранится адрес этой переменной).

Имея указатель с валидным адресом переменной на которую он указывает, доступ к значению этой переменной можно осуществить также с помощью унарного оператора звёздочка *. В этом случае звёздочка не является частью имени типа (сравните строки 2 и 3 в коде выше)! Будьте внимательны, так как при попытке разыменования указателя, который содержит «мусорное» значение адреса, запросто может произойти ошибка сегментации – segmentation fault – с последующим непредвиденным завершением работы программы. А может и не произойти, что, поверьте, гораздо хуже, так как какие-то данные могут записаться туда, куда не следует и никто об этом не узнает…до поры до времени. Выход из ситуации – инициализируйте указатели перед использованием (либо адресом реально существующей переменной, либо специальным значением NULL для того, чтобы явно сообщить, что на данный момент указатель никуда не указывает!).

Обратите внимание на то, что на 2-й строчке, звёздочка стоит непосредственно перед именем указателя pvar, а не перед int32_t, хотя она является частью имени типа переменной-указателя! Такой способ объявления де-факто является стандартом и он призван избавить программиста от одной нелепой ошибки, которая может возникнуть при объявлении нескольких указателей в одной строке. Вот наглядный пример, демонстрирующий подобную ошибку:

  1. int32_t* pvar1, pvar2, pvar3;

Тут, неопытный программист может подумать, что он объявил три указателя на int32_t, но это не так! На самом деле указателем будет только pvar1, а переменные pvar2 и pvar3 будут объявлены просто как переменные типа int32_t. Не очень-то и очевидно, правда? И чтобы не допускать подобных ошибок было принято решение эту звёздочку ставить всегда рядышком с именем указателя. Тогда если мы захотим объявить три указателя, то следует написать так:

  1. int32_t *pvar1, *pvar2, *pvar3;

Будьте внимательны и не допускайте подобных ошибок!

4. Выводы

Умение грамотно объявлять переменные нужных типов для дальнейшей работы является важным навыком при программировании на языке C/C++. Будучи разработанным для того, чтобы быть быстрым и мощным, язык во многих случаях не следит за программистом, перекладывая полностью на его плечи ответственность за написание валидного кода. В статье были рассмотрены некоторые моменты, следуя которым можно избавиться от досадных и распространённых ошибок, а также даны рекомендации для написания более читаемого и красивого кода. Кратко содержание статьи можно суммировать в виде следующих тезисов:

  1. Всегда инициализируйте ваши переменные перед их использованием, чтобы их мусорные значения не повлияли на результаты расчётов. Особое внимание должно уделяться инициализации указателей;
  2. Всегда подключайте заголовочный файл stdint.h и используйте надёжные типы данных вроде int23_t, unit16_t и т.д, занимающие на разных платформах и системах одно и то-же количество бит в памяти;
  3. При необходимости переопределяйте названия типов на более короткие, информативные и удобные в написании и отладке, например, s16 или u64;
  4. При объявлении указателей всегда ставьте символ звёздочка «*» ближе к имени указателя. Будьте особо внимательны при объявлении нескольких указателей в одну строку!