Урок №37. const, constexpr и символьные константы

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 124748

 ǀ   23 

До этого момента, все переменные, которые мы рассматривали, были обычными. Их значения можно было изменить в любое время, например:

Тем не менее, иногда полезно использовать переменные, значения которых изменить нельзя — константы.

Константы

Возьмем к примеру величину силы тяжести на Земле: 9.8м/с^2. Она вряд ли поменяется в ближайшее время. Использовать константу в этом случае будет наилучшим вариантом, так как мы предотвратим, таким образом, любое (даже случайное) изменение этого значения.

Чтобы сделать переменную константой — используйте ключевое слово const перед типом переменной или после него. Например:

Несмотря на то, что язык C++ позволяет размещать const как перед типом данных, так и после него, хорошей практикой считается размещать const перед типом данных.

Константы должны быть инициализированы при объявлении. Изменить их значения с помощью операции присваивания нельзя:

Объявление константы без её инициализации также вызовет ошибку компиляции:

Обратите внимание, константы могут быть инициализированы и с помощью неконстантных значений:

Ключевое слово const является наиболее полезным (и наиболее часто используемым) с параметрами функций:

Таким образом, при вызове функции константа-параметр сообщает и гарантирует нам то, что функция не изменит значение переменной myValue.

Время компиляции и время выполнения


Когда вы находитесь в процессе компиляции программы, то это время компиляции (англ. «compile time»). Компилятор проверяет вашу программу на синтаксические ошибки и, если их нет, конвертирует код в объектные файлы.

Временной промежуток с момента старта выполнения программы и до момента окончания её работы называется временем выполнения программы (англ. «runtime»). Код выполняется строка за строкой.

Спецификатор constexpr

В языке C++ есть два вида констант:

   Константы времени выполнения. Их значения определяются только во время выполнения программы. Переменные типа usersAge и myValue выше являются константами времени выполнения, так как компилятор не может определить их значения во время компиляции. usersAge зависит от пользовательского ввода (который можно получить только во время выполнения программы), а myValue зависит от значения, переданного в функцию (это значение также определится только во время выполнения программы).

   Константы времени компиляции. Их значения определяются во время компиляции программы. Например, переменная со значением силы тяжести на Земле является константой времени компиляции, так как мы её определяем во время написания программы (до начала её выполнения).

В большинстве случаев не важно какой тип константы вы используете: времени выполнения или времени компиляции. Однако, все же есть несколько ситуаций, когда C++ может потребовать константу времени компиляции вместо времени выполнения (например, при определении длины массива фиксированного размера — мы рассмотрим это несколько позже). Так как есть 2 типа констант, то компилятору нужно постоянно отслеживать, к какому из них относится какая переменная. Чтобы упростить это задание, в C++11 добавили спецификатор constexpr, который сообщает компилятору, что текущая переменная является константой времени компиляции:

Использовать его вы, скорее всего, не будете, но знать о нем не помешает.

Правило: Любая переменная, которая не должна изменять свое значение после инициализации, должна быть объявлена с помощью спецификатора const (или constexpr).

Имена констант


Некоторые программисты пишут имена констант заглавными буквами. Другие используют обычные имена, только с префиксом k. Мы же не будем их как-то выделять, так как константы — это те же обычные переменные, просто с фиксированными значениями, вот и всё. Особой причины их выделять — нет. Однако, это дело привычки.

Символьные константы

На предыдущем уроке мы говорили о магических числах — литералы, которые используются в программе в виде констант. «Поскольку использовать их не рекомендуется, то какой выход?» — спросите вы. А я отвечу: «Использовать символьные константы». Символьная константа — это тот же литерал (магическое число), только с идентификатором. Есть 2 способа объявления символьных констант в языке C++. Первый способ хороший, а второй — не очень. Рассмотрим сначала плохой способ.

Плохой способ: Использовать макросы-объекты с текст_замена в качестве символьных констант.

Раньше этот способ широко использовался, так что вы все еще можете его увидеть в старых программах.

Как мы уже знаем, макросы-объекты имеют две формы: с текст_замена и без текст_замена. Рассмотрим первый вариант с текст_замена. Он выглядит следующим образом:

#define идентификатор текст_замена

Как только препроцессор встретит эту директиву, все дальнейшие появления идентификатор будут заменены на текст_замена. идентификатор обычно пишется заглавными буквами с нижним подчёркиванием вместо пробелов.

Например:

Во время компиляции программы, препроцессор заменит все идентификаторы MAX_STUDENTS_PER_CLASS на литерал 30.

Согласитесь, это гораздо лучше, нежели использовать магические числа, как минимум, по нескольким причинам. MAX_STUDENTS_PER_CLASS дает понимание того, что это за значение и зачем оно используется (это понятно даже без комментариев). Во-вторых, если число нужно будет изменить — достаточно будет внести правки только в директиву #define, все остальные идентификаторы MAX_STUDENTS_PER_CLASS в программе будут автоматически заменены новым значением при повторной компиляции.

Рассмотрим еще один пример:

Здесь понятно, что MAX_STUDENTS_PER_CLASS и MAX_NAME_LENGTH не являются одним и тем же объектом, хоть и имеют одинаковые значения.

Так почему же этот способ плохой? На это есть две причины:

   Во-первых, макросы обрабатываются препроцессором, который заменяет идентификаторы на определенные значения. Эти значения не отображаются в отладчике (во время отладки вашей программы). При компиляции int max_students = numClassrooms * 30; в отладчике вы увидите int max_students = numClassrooms * MAX_STUDENTS_PER_CLASS;. «А как тогда узнать значение MAX_STUDENTS_PER_CLASS?» — спросите вы. А я отвечу: «Вам придется самостоятельно найти это в коде». А процесс поиска может занять некоторое время (в зависимости от размеров вашей программы).

   Во-вторых, эти директивы всегда имеют глобальную область видимости (о ней мы поговорим позже). Это означает, что значения #define в одной части кода могут конфликтовать со значениями #define в другой части кода.

Правило: Не используйте директиву #define для создания символьных констант.

Хороший способ: Использовать переменные со спецификатором const.

Например:

Такие значения отображаются в отладчике, а также следуют всем правилам обычных переменных (в том числе и по области видимости).

Правило: Используйте спецификатор const для создания символьных констант.

Использование символьных констант в программе


Во многих программах символьные константы используются часто (в разных местах кода). Это могут быть физические или математические константы (например, число Пи или число Авогадро), или специфические значения в вашей программе. Чтобы не писать их каждый раз, когда они необходимы — просто определите их в одном месте и используйте везде, где они нужны. Таким образом, если вам придется изменить их значения — достаточно будет зайти в один файл и внести в него правки, а не искать эти константы по всей программе.

Алгоритм использования символьных констант в вашей программе:

   Шаг №1: Создайте заголовочный файл для хранения констант.

   Шаг №2: В заголовочном файле объявите пространство имен.

   Шаг №3: Добавьте все ваши константы в созданное пространство имен (убедитесь, что все константы имеют спецификатор const).

   Шаг №4: #include заголовочный файл везде, где нужны константы.

Например, файл constants.h:

Используйте оператор разрешения области видимости :: для доступа к константам в файлах .cpp:

Если в программе много констант разных типов, то сделайте отдельные файлы для каждого из этих типов. Таким образом, отделив математические константы от специфических значений (которые могут быть разными в разных программах), вы сможете подключать файл с математическими константами к любой другой программе.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (754 оценок, среднее: 4,93 из 5)
Загрузка...

Комментариев: 23

  1. Илья:

    Вот это плохо?

    Как сокращение чтобы меньше писать было.

    1. Андрей:

      Директива #define используется для создания макросов в языках программирования C и C++. В данном случае, #define canvas_x Globals::Get().canvasImage->getSize().x создает макрос canvas_x, который заменяет Globals::Get().canvasImage->getSize().x в коде. Это может быть полезно для сокращения длинных выражений и упрощения кода. Однако, использование макросов может привести к ошибкам, если они не используются правильно. Например, если Globals::Get().canvasImage равен nullptr, то canvas_x вызовет ошибку при попытке получить его размер. Кроме того, макросы могут затруднить отладку кода, так как они могут заменяться на неожиданный код во время компиляции.Таким образом, использование #define для создания макросов может быть полезным для сокращения длинных выражений и упрощения кода, но может привести к ошибкам, если они не используются правильно.

  2. Максим:

    Использование #define-ов вместо констант скрывает в себе одну неочевидную пакость, связанную с тем, что оный в действительности не производит вычислений, а просто подставляет строки

    Пусть мы усложнили код из примера, определив отдельно число студентов-мужчин, и число студентов-женщин в классе. А общее число студентов в классе будет «просто» равно сумме тех и других

    Всё на первый взгляд выглядит гладко, но по факту последняя строка после компиляции превратится в

    , а никак не в требуемое

  3. Алексей:

    Здравствуйте, Юрий! Пытаюсь скомпилировать программу на основе вашего последнего кода, но постоянно 3 ошибки:
    E0276 — имя перед которым стоит :: должно определять класс или пространство имён.
    C2653 — constants не является классом или пространством имён.
    C2065 — pi необъявленный идентификатор.
    Хотя код к пространству имен идентичен вашему в примере:

    Единственное, присутствует double, т.к. при отсутствие выдаёт ошибку: Отсутствует явный тип данных.
    Помогите, пожалуйста, как написать пространство без ошибок.
    Visual Studio Community 2019 v. 16.1.1

    1. Дмитрий:

      Алексей, предположу,что ошибки из-за опечатки в названии вашего пространства имен для констант, должно быть constants (вы пропустили букву t в конце), а далее по коду вы пытаетесь использовать constants и конечно компилятор такого пространства имен не находит.

    2. Анатолий:

      Возможно вы создали только файл .срр с исходным кодом. Необходимо так же создать файл заголовка с расширением .h и описать в нем пространство имен. Файл заголовка должен быть подключен к компиляции. Если все это есть — ищите ошибки в написании пространства имен, файла заголовков (в том числе и подключенную строку #include проверьте).

  4. somebox:

    9,8 м/с2 — это не сила тяжести, а ускорение свободного падения. Сила тяже для каждого объекта рассчитывается отдельно.

  5. Константин:

    Привет, Юра! Создал файл geoConstants.h со следующим содержимым:

    ,где enterNumber() простейшая функция ввода числа,
    подключил к своему проекту, всё прекрасно работает (т.е. при выполнении программы я ввожу некие свои константы, например, w — влажность в процентах и т.д.), но в пустое консольное окно, где только лишь моргает курсор. Любые попытки применить cout или std::cout или using std или using namespace std для отображения ввода очередного значения заканчиваются ошибкой. А мне нужно много таких константных переменных создать. Как быть?

    1. Artem:

      Ты же создал пространство имён, поэтому нужно его использовать, т.е.
      using namespace constants;

    2. Анатолий:

      Куда вы пытаетесь вставить вывод в консоль? Не уверен, но логично предположить что в функции определения пространства имен не получится использовать cout.
      cout нужно использовать либо в функции enterNumber… но это не логично, т.к. вы вводите разные переменные через одну функцию ввода. Либо… как вариант… принимать значения ДО определения пространства имен и уже там вводить константы из других переменных. Тоже не красиво, но в таких условиях я не вижу еще каких-то выходов.

  6. Константин:

    Юрий, пожалуйста, дайте пример кода с run time константой (например высота башни с которой бросают мячик) и место где она хранится, это не тот же файл где лежат compile константы (типа сила тяжести и пр.)?

  7. San_Rembak:

    У меня выдаёт ошибку E2140 (выражение должно относиться к целочисленному типу или типу перечисления без области видимости). Что делать? Я чёт не пойму в чём прикол.

    1. Фото аватара Юрий:

      Возведение в степень в C++ выполняется не оператором ^, а функцией pow. ^ — это побитовое исключающее ИЛИ (смотрите урок о побитовых операторах). Правильно следующее:

      1. San_Rembak:

        Ой затупил xdd. Понял, спасибо большое =DDD

    2. Владимир:

      S = pi * r ^ 2

      Вот где сидит чертик. Изменяем возведение в степень на умножение и все работает.

  8. aleksandr:

    Скажите, почему myValue не инициализирована ?

    1. aleksandr:

      А при выполнении :

      выдает :
      5
      6
      7
      8

      а, что тогда константа ?

      1. Фото аватара Юрий:

        Константа — это значение, которое используется в функции printInteger. Параметр функции имеет локальную область видимости, а константа — это параметр функции и изменять значение константы нельзя в функции printInteger. Вы можете выполнять любые операции с константой в printInteger, но не присваивать ей другое значение. Так как константа имеет локальную область видимости, то в main() она никак не видна. И то, что вы передаете разные значения, то этому следует процесс выделения памяти для константы внутри функции, а затем удаление этой памяти. При каждом новом вызове этот процесс повторяется. Смотрите урок 15.

        1. aleksandr:

          Скажите пожалуйста, я правильно понял — значение константы нужно для выполнения какого то кода ВНУТРИ функции и после } это
          значение "умирает".
          А повторное использование функции в теле главной это уже другая история, с присвоением другого значения константы .

          Спасибо.

        2. Фото аватара Юрий:

          Да, верно. В printInteger вы просто выполняете операции с константой.

    2. Фото аватара Юрий:

      В уроке ведь говорится, что есть константы runtime (значения определяются только во время выполнения) и константы compile-time (значения определяются во время компиляции). Константам нельзя присваивать значения через знак равенства после объявления. Использовать в качестве параметров в функции можно — их значения таким образом могут быть как runtime, так и compile-time. В функции они используются в качестве параметров для гарантии того, что сами функции в своем теле не изменят значения, которые вы будете передавать.

      В printInteger константа может инициализироваться как во время выполнения (например, пользователь вводит своё число и затем вы передаете его в функцию для вывода), так и во время компиляции (например, вы передаете уже готовое значение для выполнения математических операций внутри функции).

  9. Виталий:

    >>»В заголовочном файле объявите пространство имен (урок 24).»

    Вот только в уроке 24 «объявление пространства имен в заголовочных файлах» не объясняется…

    1. Фото аватара Юрий:

      Пример объявления пространства имен приведен ниже — на примере файла constants.h. Буквально сразу, после перечисления в этом уроке. Если хотите подробнее прочитать о пространствах имен — урок 53, исправил.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *