Язык C: константы и литералы
Константы — фиксированные величины, которые не изменяются во время выполнения программы. Эти фиксированные величины также называются литералами.
Константы могут относиться к любому из основных типов данных, например:
- целочисленная константа;
- константа с плавающей точкой;
- символьная константа;
- строковый литерал.
Используются также перечислимые константы.
С константами обращаются так же, как и с обычными переменными. Только значения констант не могут быть изменены после их определения.
Целочисленные литералы
Целочисленный литерал может быть десятичной, восьмеричной или шестнадцатеричной константой. Префикс определяет основание (радикс) целочисленной константы:
- 0x или 0X — шестнадцатеричной;
- 0 — восьмеричной;
- отсутствие префикса — десятичной.
Целочисленный литерал может также иметь суффикс. Он представляет собой комбинацию букв U и L, означающих целое беззнакового и длинного типа (соответственно). Суффикс может быть прописной или строчной буквой и располагаться в любом порядке.
Вот несколько примеров целочисленных литералов:
Ниже приведены другие примеры различных типов целочисленных литералов:
Литералы с плавающей точкой
Литерал с плавающей точкой состоит из целой части, десятичной точки, дробной части и экспоненты. Литералы с плавающей точкой можно представлять либо в десятичной форме, либо в экспоненциальной.
При представлении десятичной формы включают десятичную точку, экспоненту либо и то, и другое; а при представлении экспоненциальной формы — целую часть, дробную часть либо обе части. Знаковая экспонента обозначается символом e или E.
Вот примеры литералов с плавающей точкой:
Символьные константы
Символьные литералы заключаются в одинарные кавычки; например, ‘x’ может храниться в простой переменной типа char.
Символьный литерал может быть:
- обычным символом (например, ‘x’);
- управляющей последовательностью (например, ‘\t’);
- универсальным символом (например, ‘\u02C0’).
В языке С есть определенные символы, которые имеют особое значение, когда им предшествует обратный слэш, например, новая строка (\n) или табуляция (\t).
Вот список таких кодов управляющей последовательности:
- \\ — символ \
- \’ — символ ‘
- \” — символ “
- \? — символ ?
- \a — звуковой сигнал
- \b — возврат на одну позицию (backspace)
- \f — смена страницы
- \n — новая строка
- \r — возврат каретки
- \t — горизонтальная табуляция
- \v — вертикальная табуляция
- \ooo — восьмеричное число (от одной до трех цифр)
- \xhh … … — шестнадцатеричное число (из одной или нескольких цифр)
Ниже приведен пример, показывающий несколько символов управляющей последовательности:
Когда приведенный выше код компилируется и выполняется, выдается следующий результат:
Строковые литералы
Строковые литералы (константы) заключаются в двойные кавычки “”. Строка содержит символы, похожие на символьные литералы:
- обычные символы;
- управляющие последовательности;
- универсальные символы.
Можно разбить длинную строку на несколько строк, используя строковые литералы и разделяя их пробельными символами.
Вот примеры строковых литералов. Все три формы являются идентичными строками:
Определение констант
В языке С есть два простых способа определения констант:
- Использование препроцессора #define.
- Использование ключевого слова const.
Препроцессор #define
Ниже приведена форма использования препроцессора #define для определения константы:
Следующий пример детально объясняет эту форму:
Когда приведенный выше код компилируется и выполняется, выдается следующий результат:
Ключевое слово const
Префикс const используется для объявления констант с определенным типом следующим образом:
Пример, указанный ниже, детально объясняет эту форму:
Когда приведенный выше код компилируется и выполняется, выдается следующий результат:
Обратите внимание: хорошей практикой программирования является определение констант в ЗАГЛАВНЫХ БУКВАХ.
Так вы думаете, что знаете Const?
От переводчика:
Предлагаю вам перевод поста из блога Мэтта Стэнклиффа (Matt Stancliff), автора нашумевшей на хабре статьи Советы о том, как писать на С в 2016 году.
Здесь Мэтт делится знаниями о квалификаторе типа const . Несмотря на вызывающий заголовок, возможно, многое из того что здесь описывается будет вам известно, но, надеюсь, и что-нибудь новое тоже найдется.
Приятного чтения.
Думаете, что вы знаете все правила использования const для С? Подумайте еще раз.
Основы const
Скалярные переменные
Вы знакомы с простым правилом const в С.
const перед hello означает, что во время компиляции происходит проверка того, что hello никогда не меняется.
Если вы попытаетесь изменить или переопределить hello , компилятор остановит вас:
Кроме того, C не сильно беспокоится о том, где расположен const до тех пор пока он находится перед идентификатором, так что объявления const uint32_t и uint32_t const идентичны:
Скалярные переменные в прототипах
Сравните прототип и реализацию следующей функции:
Будет ли ругаться компилятор, если в реализации функции printTwo() указаны скалярные параметры с квалификатором const , а в прототипе без него?
Для скалярных аргументов совершенно нормально, что квалификаторы const не совпадают в прототипе и реализации функции.
Почему это хорошо? Все очень просто: ваша функция никак не может изменить a и b вне своей области видимости, поэтому const не оказывает никакого влияния то что вы ей передаете. Ваш компилятор достаточно умен, чтобы понять, что это будут копии a и b , так что в данном случае наличие или отсутствие const не оказывает никакого влияния на физические или ментальные модели вашей программы.
Ваш компилятор не волнует несовпадение квалификатора const для любых параметров не являющихся указателями или массивами, так как они копируются в функцию по значению и исходное значение передаваемых переменных всегда остается неизменным 1 .
Однако, ваш компилятор будет жаловаться на несоответствие const для параметров, являющихся указателями или массивами, так как в таком случае ваша функция будет иметь возможность манипулировать данными на которые ссылается передаваемый указатель.
Массивы
Вы можете указать const для всего массива.
const также может указываться после объявления типа:
Если вы попытаетесь изменить things[] , компилятор остановит вас:
Структуры
Обычные структуры
Вы можете указать const для всей структуры.
Если мы попытаемся изменить какой-либо член someStructA :
Мы получим ошибку, т.к. someStructA объявлена как const . Мы не можем изменять её члены после определения.
const внутри структуры
Вы можете указать const для отдельных членов структуры:
Если мы попытаемся изменить какие-либо члены someOtherStructB :
Мы получим ошибку только при изменении b , т.к. b объявлена как const :
Объявление всего экземпляра структуры с квалификатором const равносильно объявлению специальной копии структуры, в которой все члены определены как const . Если вам не нужна 100% const структура, вы можете указать const только для конкретных членов при объявлении структуры, только там где это необходимо.
Указатели
const для указателей — вот где начинается веселье.
Один const
Давайте использовать указатель на целое число в качестве примера.
- Хранилище данных — bob
- Хранилище указателя aFour , указывающего на bob
Как насчет обновления const -указателя без изменения значения на которое он указывает?
Это действительно работает и вполне допустимо. Мы объявили uint64_t const * , что означает «указатель на неизменяемые данные», но сам по себе указатель не является неизменяемым (заметьте также: const uint64_t * имеет тоже значение).
Как сделать неизменяемыми одновременно и данные и указатель? Знакомьтесь: двойной const .
Два const
Давайте добавим ещё один const и посмотрим как пойдут дела.
Ага, у нас получилось сделать и данные, и сам указатель неизменяемыми.
Что означает const *const ?
Значение тут кажется менее очевидным.
Значение настолько шатко, потому что на самом деле рекомендуется читать объявления переменных справа налево (или ещё хуже, спиралью).
В данном случае, если читать справа налево 2 , это объявление означает:
- неизменяемый указатель ( *const )
- на неизменяемую переменную ( uint64_t const )
- обычный, изменяемый указатель ( * означает, что сам указатель может изменяться)
- на неизменяемую переменную ( uint64_t const означает, что данные не могут изменяться)
Интерлюдия — объясняем объявления const
Но подождите, дальше — больше!
-
Не объявлять ни одного const и позволить изменять и сам указатель и данные на которые он указывает
Это распространенный шаблон для перебора последовательностей данных: переходить к следующему элементу, увеличивая указатель, но не позволяя указателю изменять данные.
Допустимое значение указателя это всегда скалярный адрес памяти ( uintptr_t ), поэтому здесь const оказывает тот же эффект, как и в случае с обычными целочисленными значениями, т.е. совершенно нормально, если ваша реализация использует const для определения параметров, но прототип вашей функции не обязан включать их, так как этот const защищает только адрес, но не данные.
Три const
Сколько способов мы можем использовать, чтобы добавить const к двойному указателю?
Давайте быстро это проверим.
Какие из этих операций допускаются, исходя из объявления выше?
Только первое присваивание не сработало, потому что, если мы прочитаем наше объявление справа налево:
- указатель ( * )
- на указатель ( * )
- на неизменяемую переменную ( uint64_t const )
Что, если мы хотим добавить еще один модификатор const на уровень глубже?
Учитывая два const 3 , что мы теперь можем сделать?
Теперь мы дважды защищены от изменений, потому что, если мы прочитаем наше объявление справа налево:
- указатель ( * )
- на неизменяемый указатель ( *const )
- на неизменяемую переменную ( uint64_t const )
Мы можем сделать чуть лучше чем два. Знакомьтесь: три const .
Что если мы хотим заблокировать все изменения при объявлении двойного указателя?
Что теперь мы (не)можем сделать?
Ничего не работает! Успех!
Поехали, ещё раз:
- неизменяемый указатель ( *const )
- на неизменяемый указатель ( *const )
- на неизменяемую переменную ( uint64_t const )
Дополнительные правила
-
Объявления const всегда безопасны (если вам не нужно изменять значения):
- const существует, чтобы помочь людям справиться со сложностями, немного легче:
помогает самодокументированию ожидаемого поведения переменных и параметров (служит простой защитой, если вы забудете что должно и не должно изменяться в будущем) - const всегда можно обойти с помощью явного приведения типов или копирования памяти.
Ваш компилятор, по своему усмотрению, может решить разместить неизменяемые переменные в месте доступном только для чтения, так что если вы попытаетесь обойти const вы можете столкнуться с неопределенным поведением.
-
Любые не- const данные могут быть присвоены const переменной.
Разрешено создание неизменяемых ссылок на изменяемые переменные:
Хаки приведения типов
Что если вы умны и создали изменяемый указатель на неизменяемое хранилище?
Ваш компилятор будет жаловаться, что вы отбрасываете const , но просто выдавая предупреждение 4 , которое вы можете отключить 5 .
Поскольку это C, вы можете отбросить квалификатор const явным преобразованием типа и избавиться от предупреждения (а также нарушения инициализации const ):
Теперь у вас нет предупреждений при компиляции поскольку вы явно указали компилятору игнорировать настоящий тип &hello и использовать вместо него uint32_t * .
Хаки памяти
Что если структура содержит const члены, но вы измените хранящиеся в ней данные после объявления?
Давайте объявим две структуры, различающиеся только константностью их членов.
Попробуем скопировать someOtherStructB в const someStructA .
Будет ли это работать?
Неа, это не работает, потому что прототип 6 для memcpy выглядит так:
memcpy не позволяет передавать ей неизменяемые указатели в качестве dst аргумента, так как dst изменяется при копировании (а someStructA неизменяема).
Хотя, проверка const параметров выполняется только прототипом функции. Будет ли жаловаться компилятор, если мы используем частично неизменяемую структуру с отдельными const полями в качестве dst ?
Что произойдет, если мы попытаемся скопировать const someStructA в изменяемую, но содержащую один const член someOtherStructB ?
Теперь проверка прототипа функции проходит и мы не получаем предупреждений о memcpy , даже не смотря на то, что мы перезаписали неизменяемый член не полностью неизменной структуры.
Заключение
Не создавайте изменяемых значений без необходимости. Будьте внимательны к тому, чтобы ваша программа на самом деле работала так, как вы планировали.
1 — это также означает, что можно абсолютно безопасно передавать const скаляры в функцию, использующую их как не- const параметры, так как она никак не может изменить исходные значения скалярных переменных.^
2 — в таких случаях может быть лучше написать uint64_t const * вместо const uint64_t * , поскольку оба этих объявления приводят в точности к одному и тому же результату, но читать ваше объявление справа налево становится удобней если квалификатор const следует за типом.^
3 — это также безусловно подтверждает, что правильный синтаксис для указателей это type *name , а не type* name и уж тем более не type * name потому что, когда мы добавляем const , указатель прикрепляется к следующему квалификатору, а не к предыдущему. Например:
Неправильно
Правильно
^
4 — ну, нужно будет использовать нестандартизированный флаг в зависимости от модели компилятора, поэтому процесс сборки может потребовать много избыточных флагов для совместимости с различными компиляторами, чтобы отключить эти предупреждения.^
5 — напоминаю: const проверяется только во время компиляции; он не изменяет поведение программы, только если вы не ухитритесь нарушить ограничения накладываемые const (не больше, чем изменение любого другого значения изменило бы поведение вашей программы), но, вероятно, работать это будет не так как вы ожидаете. Также: ваш компилятор может разместить неизменяемые lданные в доступные только для чтения сегменты кода, и попытка обойти эти const -блоки может привести к неопределенному поведению.^
6 — также обратите внимание на ключевое слово restrict в прототипе memcpy() . restrict означает «данные этого указателя не пересекаются с другими данными в текущей области видимости», что определяет каким образом memcpy() планирует обрабатывать её параметры.
Если при копировании указатель на место назначения, частично перекрывает указатель на место откуда берутся данные, нужно использовать функцию memmove() , её прототип не содержит квалификаторов restrict .
^
Зачем нужен const, если можно не менять int
Не понимаю, зачем во многих языках используется тип данных const, если можно создать int и не менять его? Прошу объяснить и привести пример.
С точки зрения семантики вы говорите, что это значение не будет изменяться — и программа проконтролирует это.
Самый банальный пример — число «пи», константа, которую можно объявить один раз и быть уверенным, что она не поменяется.
Даже в простых программах можно думать, что значение не меняется и где-то его поменять — а в сложных проектах, где множество неочевидных взаимосвязей между модулями и несколькими независимыми командами разработчиков гарантировать неизменяемость проще и удобнее через const.
Тут кстати хорошо напомнить вот о чём. Константы вшиваются сразу в код (при этом достигается копеечная, но всё же выгода) и это в .Net приводит к тому, что если вы меняете константы в одной сборке — то нужно перекомпилировать весь проект. Об этом написано во многих учебниках по шарпу, но случается забывают и наступают на эти грабли.
С точки зрения компилятора меняется следующее. Если у нас переменная — мы ей выделяем место в стеке/куче под хранение:
Если мы объявляем константы — то места под хранение не выделяется, во всех местах, где нам нужна константа — подставляется её конкретное значение:

Здесь ldc — это load constant, а работа со стеком исчезла — нет этой переменной.
Урок №37. const, constexpr и символьные константы
До этого момента, все переменные, которые мы рассматривали, были обычными. Их значения можно было изменить в любое время, например:
Тем не менее, иногда полезно использовать переменные, значения которых изменить нельзя — константы.
Константы
Возьмем к примеру величину силы тяжести на Земле: 9.8м/с^2 . Она вряд ли поменяется в ближайшее время. Использовать константу в этом случае будет наилучшим вариантом, так как мы предотвратим, таким образом, любое (даже случайное) изменение этого значения.
Чтобы сделать переменную константой — используйте ключевое слово const перед типом переменной или после него. Например:
Несмотря на то, что язык C++ позволяет размещать const как перед типом данных, так и после него, хорошей практикой считается размещать const перед типом данных.
Константы должны быть инициализированы при объявлении. Изменить их значения с помощью операции присваивания нельзя:
Объявление константы без её инициализации также вызовет ошибку компиляции:
Обратите внимание, константы могут быть инициализированы и с помощью неконстантных значений:
Ключевое слово const является наиболее полезным (и наиболее часто используемым) с параметрами функций:
Таким образом, при вызове функции константа-параметр сообщает и гарантирует нам то, что функция не изменит значение переменной myValue .
Время компиляции и время выполнения
Когда вы находитесь в процессе компиляции программы, то это время компиляции (англ. «compile time»). Компилятор проверяет вашу программу на синтаксические ошибки и, если их нет, конвертирует код в объектные файлы.
Временной промежуток с момента старта выполнения программы и до момента окончания её работы называется временем выполнения программы (англ. «runtime»). Код выполняется строка за строкой.
Спецификатор constexpr
В языке C++ есть два вида констант:
Константы времени выполнения. Их значения определяются только во время выполнения программы. Переменные типа usersAge и myValue выше являются константами времени выполнения, так как компилятор не может определить их значения во время компиляции. usersAge зависит от пользовательского ввода (который можно получить только во время выполнения программы), а myValue зависит от значения, переданного в функцию (это значение также определится только во время выполнения программы).
Константы времени компиляции. Их значения определяются во время компиляции программы. Например, переменная со значением силы тяжести на Земле является константой времени компиляции, так как мы её определяем во время написания программы (до начала её выполнения).
В большинстве случаев не важно какой тип константы вы используете: времени выполнения или времени компиляции. Однако, все же есть несколько ситуаций, когда C++ может потребовать константу времени компиляции вместо времени выполнения (например, при определении длины массива фиксированного размера — мы рассмотрим это несколько позже). Так как есть 2 типа констант, то компилятору нужно постоянно отслеживать, к какому из них относится какая переменная. Чтобы упростить это задание, в C++11 добавили спецификатор constexpr, который сообщает компилятору, что текущая переменная является константой времени компиляции: