Ответ в теме: 3 Именование, назначение и использование

      Комментарии к записи Ответ в теме: 3 Именование, назначение и использование отключены
#3959

Дополнение…

В языке Си есть несколько типов констант: целые константы, такие как 10, или 01C; константы с плавающей точкой, как 1.0 и 6.022e+23; символьные константы, такие как a, \x10. Так же Си поддерживает строковые литералы, такие как Hello World и \n. Все их можно назвать литералами.

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

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

В Си есть несколько механизмов создания именованных символических констант: объекты отмеченные const, перечисления и объекто-подобные макросы. Каждый из этих механизмов имеет свои достоинства и недостатки.

Объекты отмеченные const

Такие объекты подчиняются правилам пространств имен и их типо-совместимость может быть проверена компилятором. Поскольку это именованные объекты (в отличие от макросов) некоторые инструменты отладки могут показывать их имя. Но объекты потребляют память.

Для константных объектов можно указывать точный тип, как ниже:

const unsigned int buffer_size = 256;

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

– размер битового поля в структуре
– размер массива (кроме массивов переменной длинны)
– значение константы перечисления
– значение константы в case

В любом из этих случаев в качестве rvalue значения должен использоваться литерал.

Зато можно получить адрес константного объекта.

const int max = 15;
int a[max]; /* неверно вне функции! */
const int *p;
 
/* можно получить адрес константного объекта */
p = &max;

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

Перечислимые константы могут использоваться там где нужно заменять целочисленные константы. Перечисления не разрешают указывать тип константы, их тип всегда int.

enum  max = 15 ;
int amax; /* Корректно вне функции */
const int *p;
 
p = &max; /* ошибка: нельзя получить адрес */

Объекто-подобные макросы

Программисты на Си часто определяют символические константы с помощью объектно-подобных макросов. Например:

#define buffer_size 256

Так как подстановка всех макросов происходит до фазы компиляции, никаких упоминаний о buffer_size в программе не будет и компилятор не сможет сохранить имя макроса, что бы на него могли ссылаться отладчики. Проще говоря в отладчике будут видны разбросанные по исходному коду числа 256 вместо имени макроса buffer_size.

Макросы не подчиняются правилам пространств имен и по тому могут подставляться в непредвиденных местах и с неизвестным заранее результатом.

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

Обобщим

В следующей таблице собраны выше обозначенные различия между разными видами констант.

Макросы Перечисления const объекты
Подставляется Препроцессором При компиляции В рантайме
Потребляет память Нет Нет Да
Видно в отладчиках Нет Да Да
Проверка типа Нет Нет Да
Вычисление во время компиляции Да Да Нет

Смысл числа 18 не совсем ясен в данном примере:

if age >= 18 
   /* Совершаем действия */

else 
  /* Совершаем другое действие */


Правильное решение

Правильным решением будет замена целочисленного литерала на константу ADULT_AGE, которая сама говорит о своем назначении:

enum  ADULT_AGE=18 ;
/* ... */
if (age >= ADULT_AGE )
   /* Совершаем действие */

else 
  /* Совершаем другое действие */

Пример с ошибкой №2

Если вы надеетесь, что будете всегда помнить порядок следования параметров функций типа fgets, то можете писать и так:

char buffer[256];
/* ... */
fgets(buffer, 256, stdin);

Правильное решение (enum)

Но гораздо понятнее будет с именованной константой:

enum  BUFFER_SIZE=256 ;
 
char buffer[BUFFER_SIZE];
/* ... */
fgets(buffer, BUFFER_SIZE, stdin);

Правильное решение (sizeof)

Справедливости ради нужно отметить, что читабельность кода можно повысить не только применением именованных констант, но и другими методами. Так выше приведенный пример можно переписать с использованием sizeof:

char buffer[256];
/* ... */
fgets(buffer, sizeof(buffer), stdin);

Это решение чуть более надежное, ибо гарантирует, что функции передается правильный размер буфера. Почти всегда sizeof вычисляется на стадии компиляции, исключая случаи с массивами переменной длинны. Правда у sizeof есть свои недостатки, которые мы обязательно рассмотрим в будущем.

Пример с ошибкой №3

В следующем примере значения localhost и 1234 вбиты прямо в логику программы, следовательно их можно изменить только в ручную корректируя код.

LDAP *ld = ldap_init("localhost", 1234);
if (ld == NULL) {
  perror("ldap_init");
  return 1;
}

Правильное решение

Правильным решением будет определение макросов, которые можно передать с помощью опци -D командной строки компилятора.

 /* может быть передано через командную строку компилятора */
#ifndef PORTNUMBER
#  define PORTNUMBER 1234
#endif
 
 /* может быть передано через командную строку компилятора */
#ifndef HOSTNAME
#  define HOSTNAME "localhost"
#endif
 
/* ... */
 
LDAP *ld = ldap_init(HOSTNAME, PORTNUMBER);
if (ld == NULL) {
  perror("ldap_init");
  return 1;
}

Исключение

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

x = -b + sqrt(b*b) - 4*a*c / 2*a;

читается намного удобнее, чем

enum  TWO = 2 ;     /* a scalar */
enum  FOUR = 4 ;    /* a scalar */
enum  SQUARE = 2 ;  /* an exponent */
x = -b + sqrt(pow(b)), SQUARE - FOUR*a*c/ TWO * a;

То есть в первую очередь нужно руководствоваться здравым смыслом.