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

10/01/2011 - 20:54
Дэн Сакс
 
   Использование символов для представления постоянных числовых величин — это одна  из основных рекомендаций, которую узнают большинство программистов в первую очередь.  Например, вместо того чтобы писать:
 
char buffer[256];
...
fgets(buffer, 256, stdin);
 
лучше описать символ, скажем buffer_size, представляющий размерность буфера, и использовать его вместо литеральной константы. 
 
char buffer[buffer_size];
...
fgets(buffer, buffer_size, stdin);
 
   Си и Си++ предлагают несколько способов описания таких символов. Итак, давайте их рассмотрим. 

Макросы

   Си программисты обычно определяют символические константы в виде макроса. Например, код:
 
#define buffer_size 256
 
определяет buffer_size как макрос, чье значение равно 256. 
 
  Замену макросов на их числовое значение выполняет препроцессор. Это происходит еще до того как компилятор приступит к обработке кода. Например, описание функции, приведенной выше, препроцессор преобразует из:
 
char buffer[buffer_size];
...
fgets(buffer, buffer_size, stdin);
 
в: 
 
char buffer[256];
...
fgets(buffer, 256, stdin);
 
   Поздние фазы компиляции уже не видят никаких макросов, они видят только исходный текст после замены макроса. В этом заключается один неприятный момент: многие компиляторы не сохраняют имена макросов среди символов, которые они передают в отладчики.
 
   Кроме того у макросов есть еще более серьезная проблема: их имена не подчиняются правилам видимости, которые применимы к другим именам. Это значит, что вы не можете  ограничить видимость макроса локальной областью. Например: 
 
void foo()
{
   #define max 16 // non-local
   int a[max];
   ...
}
 
Здесь max вовсе не является локальным макросом функции foo! 
 
Также вы не можете описать макрос как элемент Си++ класса или пространства имен.
 
   В некотором смысле имена макросов еще более распространены чем глобальные имена. Глобальные имена по крайней мере могут быть скрыты именами во внутренних областях. Макросы даже не уважают внутренние области действия. Следовательно, макросы могут заменяться и в непредусмотренных для этого местах. Например, после замены макроса:
 
#define max 16
...
void sort(int a[], size_t max);
 
код становится таким: 
 
void sort(int a[], size_t 16);
 
 К сожалению, такие неумышленные макро-подстановки не всегда выявляются компилятором; а если это и происходит, то полученное сообщение может оказаться весьма невнятным.
 
  Поскольку имена макросов ведут себя не так, как другие имена, программисты Си и Си++ приняли для макросов соглашение об именах, чтобы отличать их от всех остальных видов имен. Наиболее распространенное соглашение заключается в написании имен макроса полностью в верхнем регистре, например:
 
#define BUFFER_SIZE  256
char buffer[BUFFER_SIZE ];
...
fgets(buffer, BUFFER_SIZE , stdin);

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

   Как Си, так и Си++, предлагают альтернативы, позволяющие избежать небезопасного применения  макросов. Одна из таких альтернатив заключается в использовании констант перечислимого типа.
 
  Определение перечислимого типа позволяет задавать тип вместе с постоянными значениями. Например, запись вида:
 
enum color {red, green, blue};
 
описывает перечислимый тип color и константные значения: red, green, blue. По умолчанию, red имеет значение - 0, green – 1, а значение blue - 2. Однако вы можете присвоить константам и любые другие значения:
 
enum color  { red = 1, green = 2, blue = 4 };
 
 Большинство частей описания перечислимого необязательны, включая имя типа. Например, запись:
 
enum { blue = 4 };
 
опускает имя типа и все, кроме одной константы перечислимого типа. Она просто описывает константу blue, чье значение равно 4.
 
   Эту упрощенную форму описания перечисления можно использовать для записи любых целочисленных констант.
 
enum { buffer_size = 256 };
 
   Здесь buffer_size описывается как целочисленная константа 256. 
 Константа перечислимого типа - это статическая константа и ее можно использовать для задания размерности массива:
 
char buffer[buffer_size];
 
   В отличие от макросов, константы перечислимого типа подчиняются обычным правилам видимости. Это значит, что вы можете описывать локальные константы перечислимого типа внутри функций или классов.
 
   К сожалению, константы перечислимого типа должны иметь целые значения, поэтому вы не можете использовать их для записи float констант:
 
//усекается к 3
enum { pi = 3.14159 };
 
Такие округления, как правило, вызывают предупреждение компилятора.

Константные объекты

    И Си и Си++ предоставляют возможность определения символической константы в виде  константного объекта. Например, вот так:
 
int const buffer_size = 256;
 
Порядок, в котором написаны int и const, для компилятора ничего не значит. Вы можете  записать объявление и так:
 
const int buffer_size = 256;
 
По причинам, описанным в другой моей статье, я предпочитаю записывать const справа.
 
   К сожалению, данное выше описание (неважно, каким путем вы его записали) имеет в языке Си несколько иное значение, чем в Си++. Программа на Си++ может использовать buffer_size как размерность массива, а программа на Си – не может. Например, описание:
 
int const buffer_size = 256;
 
компилируется одинаково хорошо как в Си, так и в Си++, а вот следующее описание:
 
char buffer[buffer_size]; 
 
компилируется только в Си++. В Си оно выдает ошибку компиляции. 
 
   Я предполагаю, что большинство Си++ программистов предпочитают описывать символические константы как константные объекты, а не константы перечислимого типа. Само по себе описание константного объекта выглядит более наглядным. Например, когда вы пишете:
 
int const buffer_size = 256;
 
описание абсолютно ясно говорит о том, что buffer_size является целочисленной константой, чье значение равно 256. При объявлении константы перечислимого типа  отнюдь не так уж ясно, что:
 
enum { buffer_size = 256 };
 
является по существу  тем же самым.
 
  Другое, более существенное преимущество заключается в том, что описание константного объекта позволяет вам устанавливать точный тип константы. Например:
 
unsigned int const buffer_size = 256;
 
описывает buffer_size как константу, чей тип есть беззнаковое целое, в отличие от просто int (которому знак присвоен по умолчанию). Напротив:
 
enum { buffer_size = 256 };
 
описывает buffer_size как простой int. Оно является простым int, даже если вы определяете значение константы, используя литерал без знака:
 
enum { buffer_size = 256u };
 
  Как я объяснял в одной из своих статей, числовой литерал с индексом u, или U, имеет целочисленный тип без знака и в этом случае 256u имеет тип unsigned int. Однако, даже несмотря на точный тип, используемый для определения значения константы перечислимого типа, константа перечислимого типа будет иметь тип int, если значение может быть представлено как int.  
 
   Большую часть времени точный тип целочисленной символической константы не имеет особого значения. Например, если вы опишете buffer_size как:
 
unsigned int const buffer_size = 256;
 
или:
 
enum { buffer_size = 256 };
 
массив, описанный в Си++ как:
 
char buffer[buffer_size];
 
будет иметь 256 элементов. 
 
Один из тех редких случаев, когда это будет иметь значение касается использования перегруженных функций. Например:
 
int func(int i);
unsigned int func(unsigned int ui);
 
   Способ, с помощью которого вы опишете buffer_size, повлияет на то, какая функция func(buffer_size) будет вызываться. Если buffer_size – это константа перечислимого типа, func(buffer_size) вызывает func(int). Если buffer_size – целая константа без знака, она вызывает func(unsigned int).

Однако я предпочитаю константы перечислимого типа

   Несмотря на упомянутые выше недостатки, я предпочитаю описывать символические константы как константы перечислимого типа, а не константные объекты. Проблема константных объектов заключается в том, что они могут приводить к потере производительности. У констант перечислимого типа этого недостатка нет.
 
  Дэн Сакс является президентом Saks & Associates, компании по обучению и консультированию относительно вопросов Си/Си++. Он также является внештатным редактором C/C++ Users Journal. 
 
   Вольный перевод - ChipEnable.Ru Перевод местами хромает, но на лучшее я пока не способен. 

Комментарии   

# САБ 11.01.2011 07:05
Очень хорошая статья. Вопросам стиля на сайте уделено очень мало внимания, а ведь стиль играет не последнюю роль в борьбе с ошибками еще на этапе написания программ.

Однако последний абзац поставил в тупик напрочь. В оригинале автор никаких примеров к последнему абзацу не привел?
Ответить | Ответить с цитатой | Цитировать
# Pashgan 11.01.2011 16:05
Нет. Примеров не привел. Вот дословно последний абзац.

Цитата:
Despite the disadvantages that I just mentioned, I generally prefer defining symbolic constants as enumeration constants rather than as const objects. The problem with const objects is that they may incur a performance penalty, which enumeration constants avoid. That should keep you on the edge of your seat until next time. See you then.
Статья называется "Symbolic Constants"

Видимо нужно искать разъяснений в других его статьях.
Ответить | Ответить с цитатой | Цитировать
# BSVi 11.01.2011 10:37
Ужасненькая статья, передай афтару, чтобы выпил йаду )

#define buffer_size 256
Ну ктож так пишет, дефайны прнято писать большими буквами.

Более того, в дефайне можно задать тип, к примеру:
#define BUFFER_SIZE 100500UL

Главное приемущество const - строгая типизация и типобезопасность.

Ну, и куча других косяков. Совершенно непонятно, почему афтар выбрал enum O_o
Ответить | Ответить с цитатой | Цитировать
# САБ 11.01.2011 10:54
Цитирую BSVi:
#define buffer_size 256
Ну ктож так пишет, дефайны прнято писать большими буквами.
Так он же дальше это же самое и пишет. Тип задать можно, но не меньше int.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 11.01.2011 15:55
Оооу.. оооу. BSVi, читай внимательнее.

Цитата:

Поскольку имена макросов ведут себя не так, как другие имена, программисты Си и Си++ приняли для макросов соглашение об именах, чтобы отличать их от всех остальных видов имен. Наиболее распространенное соглашение заключается в написании имен макроса полностью в верхнем регистре, например:
Код:
#define BUFFER_SIZE 256
char buffer[BUFFER_SIZE ];
...
fgets(buffer, BUFFER_SIZE , stdin);
Ответить | Ответить с цитатой | Цитировать
# BSVi 12.01.2011 08:32
Упс, проморгал. Чего-ж это он с начала не начал писать правильно, а только в конце упомянул.
Ответить | Ответить с цитатой | Цитировать
# boolshit 11.01.2011 18:14
М-да, талдычит про неограниченную область видимости для #define - и ни слова про #undef. Так нечестно! :)
Ответить | Ответить с цитатой | Цитировать
# SerjB 12.01.2011 04:05
Спасибо за статьи и переводы!
Ответить | Ответить с цитатой | Цитировать
# StranegerTJ 16.01.2011 14:05
Тема ни очем. !!!!!!!!!!!!
Ответить | Ответить с цитатой | Цитировать
# JoJo 16.01.2011 15:41
Как и твой комментарий
Ответить | Ответить с цитатой | Цитировать
# teiggery 02.02.2011 08:37
ну что вы так сразу говн..ть статью! автору и за это спасибо. мне помогает. не должно быть все идеально правильно. когда находишь ошибки, начинаешь разбираться уже в этих вопросах. автору респект
Ответить | Ответить с цитатой | Цитировать
# Misha 21.02.2016 11:30
день добрый! подскажите пожалуйста у меня в Atmel Studio ранее переменные и массивu хранились в оперативные памяти, потом что-то произошло и в каждом проекте который бы я не создал переменные и массивu хранятся в памяти программ prog а нужно в data, как это изменить ???
Ответить | Ответить с цитатой | Цитировать

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

Защитный код
Обновить