Символические константы
10/01/2011 - 20:54
Pavel Bobkov
Дэн Сакс
Использование символов для представления постоянных числовых величин — это одна из основных рекомендаций, которую узнают большинство программистов в первую очередь. Например, вместо того чтобы писать:
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 Перевод местами хромает, но на лучшее я пока не способен.
Comments
Однако последний абзац поставил в тупик напрочь. В оригинале автор никаких примеров к последнему абзацу не привел?
Quote: Статья называется "Symbolic Constants"
Видимо нужно искать разъяснений в других его статьях.
#define buffer_size 256
Ну ктож так пишет, дефайны прнято писать большими буквами.
Более того, в дефайне можно задать тип, к примеру:
#define BUFFER_SIZE 100500UL
Главное приемущество const - строгая типизация и типобезопасность.
Ну, и куча других косяков. Совершенно непонятно, почему афтар выбрал enum O_o
Quote:Code:
#define BUFFER_SIZE 256
char buffer[BUFFER_SIZE ];
...
fgets(buffer, BUFFER_SIZE , stdin);
RSS feed for comments to this post