ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



 

Часть 4

                                        Глава 2 Описания и Константы
           
                             Совершенство достигается только к моменту краха.
                                                             - С.Н. Паркинсон
           
             В этой  главе описаны основные типы (char,  int,  float и т.д.) и
           основные способы построения из них новых типов (функций,  векторов,
           указателей и т.д.).  Имя вводится в программе посредством описания,
           которое задает его тип и,  возможно, начальное значение. Даны поня-
           тия описания,  определения,  области видимости имен,  времени жизни
           объектов и типов. Описываются способы записи констант в С++, а так-
           же  способы определения символических констант.  Примеры просто де-
           монстрируют характерные черты языка. Более развернутый и реалистич-
           ный пример приводится в следующей главе для знакомства с выражения-
           ми и операторами языка С++.  Механизмы задания типов,  определяемых
           пользователем,  с  присоединенными операциями представлены в Главах
           4, 5 и 6 и здесь не упоминаются.
                2.1 Описания
           
             Прежде чем  имя  (идентификатор)  может  быть  использовано в С++
           программе,  он должно быть описано. Это значит, что надо задать его
           тип,  чтобы сообщить компилятору, к какого вида сущностям относится
           имя. Вот несколько примеров, иллюстрирующих разнообразие описаний:
           
             char ch;  int count = 1;  char* name = "Bjarne"; struct complex {
             float  re,  im;  };  complex cvar;  extern complex sqrt(complex);
             extern   int   error_number;   typedef   complex   point;   float
             real(complex*   p)   {   return  p->re;  };  const  double  pi  =
             3.1415926535897932385; struct user;
           
             Как можно видеть из этих примеров,  описание может делать  больше
           чем  просто ассоциировать тип с именем.  Большинство описаний явля-
           ются также определениями то есть они  также  определяют  для  имени
           сущность,  к которой оно относится.  Для ch, count и cvar этой сущ-
           ностью  является  соответствующий  объем  памяти,  который   должен
           использоваться как переменная - эта память будет выделена. Для real
           это   заданная   функция.   Для   constant    pi    это    значение
           3.1415926535897932385.  Для  complex  этой сущностью является новый
           тип.  Для point это тип complex, поэтому point становится синонимом
           complex. Только описания
           
             extern complex  sqrt(complex);  extern  int error_number;  struct
             user;
           
           не являются одновременно определениями. Это означает, что объект, к
           которому они относятся,  должен быть определен где-то еще. Код (те-
           ло) функции sqrt должен задаваться неким другим  описанием,  память
           для переменной error_number типа int должна выделяться неким другим
           описанием,  и какое-то другое описание типа user должно определять,
           что  он  из  себя представляет.  В С++ программе всегда должно быть
           только одно определение каждого имени,  но описаний может быть мно-
           го,  и все описания должны согласовываться с типом объекта, к кото-
           рому они относятся, поэтому в этом фрагменте есть две ошибки:
           
             int count;  int count;  //  ошибка:  переопределение  extern  int
             error_number;  extern int error_number; // ошибка: несоответствие
             типов
           
           а в этом - ни одной (об использовании extern см. #4.2):
           
             extern int  error_number;  extern  int  error_number;   Некоторые
             описания задают "значение" для сущностей, которые они
           определяют:
           
             struct complex { float re,  im;  };  typedef complex point; float
             real(complex*   p)   {   return   p->re  };  const  double  pi  =
             3.1415926535897932385;
           
             Для типов,  функций и  констант  "значение"  неизменно.  Для  не-
           константных  типов данных начальное значение может впоследствии из-
           меняться:
           
             int count = 1;  char* name = "Bjarne";  //...  count = 2;  name =
             "Marian";
           
             Из всех определений только
           
             char ch;
           
           не задает значение.  Всякое описание,  задающее значение,  является
           определением.
           
                2.1.1 Область Видимости
           
             Описание вводит имя в  области  видимости.  То  есть,  имя  может
           использоваться  только  в определенной части программы.  Для имени,
           описанного в функции (такое имя часто называют локальным),  эта об-
           ласть  видимости  простирается от точки описания до конца блока,  в
           котором появилось описание.  Для имени не в функции и не  в  классе
           (называемого  часто  глобально  видимым  именем)  область видимости
           простирается от точки описания до конца файла,  в котором появилось
           описание.  Описание имени в блоке может скрывать (прятать) описание
           во внутреннем блоке или глобальное имя. Это значит, что можно пере-
           определять имя внутри блока для ссылки на другой объект.  После вы-
           хода из блока имя вновь обретает свое прежнее значение. Например:
           
             int x; // глобальное x
           
             f() { int x; // локальное x прячет глобальное x x = 1; // присво-
                 ить локальному x {
                     int x;  // прячет первое локальное x x = 2;  // присвоить
                     второму локальному x
                 }
                 x = 3;     // присвоить первому локальному x
             }
           
             int* p = &x; // взять адрес глобального x
           
             Сокрытие имен неизбежно при написании  больших  программ.  Однако
           читающий человек легко может не заметить, что имя скрыто, и некото-
           рые ошибки,  возникающие вследствие этого, очень трудно обнаружить,
           главным образом потому, что они редкие. Значит сокрытие имен следу-
           ет минимизировать.  Использование для  глобальных  переменных  имен
           вроде i или x напрашивается на неприятности.
             С помощью применения операции разрешения области видимости ::
           можно использовать скрытое глобальное имя. Например:
           
             int x;
           
             f() {
                 int x = 1; // скрывает глобальное x
                 ::x = 2;   // присваивает глобальному x
             }
           
             Но возможности использовать скрытое локальное имя нет.
           
             Область видимости имени начинается в точке описания.  Это означа-
           ет,  что  имя  можно использовать даже для задания его собственного
           значения. Например:
           
             int x;
           
             f() {
                 int x = x; // извращение
             }
           
             Это не является недопустимым,  хотя и бессмысленно,  и компилятор
           предупредит,  что  x "used before set" ("использовано до того,  как
           задано"),  если вы попробуете так сделать. Можно, напротив, не при-
           меняя операцию ::,  использовать одно имя для ссылки на два различ-
           ных объекта в блоке. Например:
           
             int x;
           
             f() // извращение {
                 int y = x; // глобальное x int x = 22; y = x; // локальное x
             }
           
             Переменная y инициализируется значением глобального x,  11, а за-
           тем ему присваивается значение локальной переменной x, 22.
             Имена параметров функции считаются  описанными  в  самом  внешнем
           блоке функции, поэтому
           
             f(int x) {
                 int x; // ошибка
             }
           
           содержит ошибку,  так  как x определено дважды в одной и той же об-
           ласти видимости.
           
                2.1.2 Объекты и Адреса (Lvalue)
           
             Можно назначать и использовать переменные,  не  имеющие  имен,  и
           можно  осуществлять  присваивание выражениям странного вида (напри-
           мер,  *p[a+10]=7). Следовательно, есть потребность в имени "нечто в
           памяти".  Вот  соответствующая цитата из справочного руководства по
           С++:  "Объект есть область памяти. lvalue есть выражение, ссылающе-
           еся на объект" (#с.5).  Слово "lvalue" первоначально было придумано
           для значения "нечто,  что может стоять в левой части присваивания".
           Однако не всякое lvalue можно использовать в левой части присваива-
           ния; бывают lvalue, ссылающиеся на константу (см. #2.4).
           
                2.1.3 Время Жизни
           
             Если программист не указал  иного,  то  объект  создается,  когда
           встречается его описание,  и уничтожается, когда его имя выходит из
           области видимости, Объекты с глобальными именами создаются и иници-
           ализируются  один  раз  (только) и "живут" до завершения программы.
           Объекты, определенные описанием с ключевым словом static, ведут се-
           бя так же. Например*:
           
           ДДДДДДДДДДДДДДДДДДДД
           * Команда #include  была выброшена из примеров в этой
           главе для экономии места.  Она необходима в примерах,  производящих
           вывод, чтобы они были полными. (прим. автора)
             int a = 1;
           
             void f() {
                 int b = 1;  // инициализируется при каждом вызове f()  static
                 int c = 1;  // инициализируется только один раз cout << " a =
                 " << a++
                      << " b = " << b++
                      << " c = " << c++ << "\n";
             }
           
             main() {
                 while (a < 4) f();
             }
           
           производит вывод
           
             a = 1 b = 1 c = 1 a = 2 b = 1 c = 2 a = 3 b = 1 c = 3
           
             Не инициализированная явно статическая (static) переменная неявно
           инициализируется нулем.
           
             С помощью операций new и delete программист может также создавать
           объекты,  время  жизни  которых  управляется  непосредственно,  см.
           #3.2.4.
           
                2.2 Имена
           
             Имя (идентификатор) состоит из последовательности  букв  и  цифр.
           Первый символ должен быть буквой.  Символ подчерка _ считается бук-
           вой.  С++ не налагает ограничений на число символов в имени, но не-
           которые  части  реализации находятся вне ведения автора компилятора
           (в частности, загрузчик), и они, к сожалению, такие ограничения на-
           лагают.  Некоторые среды выполнения также делают необходимым расши-
           рить или ограничить набор символов,  допустимых  в  идентификаторе.
           Расширения  (например,  при допущении в именах символа $) порождают
           непереносимые программы.  В качестве имени не могут  использоваться
           ключевые слова С++ (см. #с.2.3). Примеры имен:
           
             hello this_is_a_most_unusially_long_name  DEFINED  foO bAr u_name
             HorseSense var0 var1 CLASS _class ___
           
             Примеры последовательностей символов,  которые не могут использо-
           ваться как идентификаторы:
           
             012 a fool $sys class 3var pay.due foo~bar .name if
           
             Буквы в верхнем и нижнем регистрах считаются различными,  поэтому
           Count и count - различные имена,  но вводить имена,  лишь  незначи-
           тельно отличающиеся друг от друга,  нежелательно. Имена, начинающи-
           еся с подчерка,  по традиции используются для  специальных  средств
           среды  выполнения,  поэтому  использовать  такие имена в прикладных
           программах нежелательно.
           
             Во время чтения программы компилятор всегда ищет наиболее длинную
           строку,  составляющую имя,  поэтому var10 - это одно имя,  а не имя
           var,  за которым следует число 10, и elseif - одно имя, а не ключе-
           вое слово else, после которого стоит ключевое слово if.
                2.3 Типы
           
             Каждое имя (идентификатор) в С++ программе имеет  ассоциированный
           с  ним тип.  Этот тип определяет,  какие операции можно применять к
           имени (то есть к объекту, на который оно ссылается), и как эти опе-
           рации интерпретируются. Например:
           
             int error number; float real(complex* p);
           
             Поскольку error_number  описано  как int,  его можно присваивать,
           использовать в арифметических выражениях и т.д.  Тогда как  функция
           real может вызываться с адресом complex в качестве параметра. Можно
           взять адрес любого из них.  Некоторые имена,  вроде int и  complex,
           являются именами типов. Обычно имя типа используется в описании для
           спецификации другого имени. Единственные отличные от этого действия
           над  именем  типа  - это sizeof (для определения количества памяти,
           которая требуется для хранения объекта типа) и new (для  размещения
           объекта типа в свободной памяти). Например:
           
             main() {
                 int* p = new int;  cout << "sizeof(int) =  "  <<  sizeof(int)
                 "\n";
             }
           
             Имя типа можно также использовать для задания явного преобразова-
           ния одного типа в другой, например:
           
             float f;  char* p;  //...  long ll = long(p);  // преобразует p в
             long int i = int(f); // преобразует f в int
           
                2.3.1 Основные Типы
           
             В С++ есть набор основных типов,  которые соответствуют  наиболее
           общим основным единицам памяти компьютера и наиболее общим основным
           способам их использования:
           
             char short int int long int
           
           для представления целых различных размеров,
           
             float double
           
           для представления чисел с плавающей точкой,
           
             unsigned char unsigned short int unsigned int unsigned long int
           
           для представления беззнаковых целых,  логических значений,  битовых
           массивов и т.п.  Для большей компактности записи можно опускать int
           в комбинациях из нескольких слов,  что не меняет смысла.  Так, long
           означает long int,  и unsigned тип означает тип unsigned int. В об-
           щем, когда в описании опущен тип, он предполагается int. Например:
           
             const a = 1; static x;
           
           все определяют объект типа int.
           
             Целый тип char наиболее удобен для хранения и обработки  символов
           на данном компьютере,  обычно это 8-битовый байт.  Размеры объектов
           С++ выражаются в единицах  размера  char,  поэтому  по  определению
           sizeof(char)==1.  В зависимости от аппаратного обеспечения char яв-
           ляется знаковым или беззнаковым целым.  Тип unsigned char, конечно,
           всегда беззнаковый,  и при его использовании получаются более пере-
           носимые программы, но из-за применения его вместо просто char могут
           возникать значительные потери в эффективности.
           
             Причина того, что предоставляется более чем один целый тип, более
           чем один беззнаковый тип и более чем один тип с плавающей точкой, в
           том,  чтобы дать возможность программисту воспользоваться характер-
           ными особенностями аппаратного обеспечения. На многих машинах между
           различными  разновидностями  основных типов существуют значительные
           различия в потребностях памяти,  временах доступа к памяти и време-
           нах вычислений.  Зная машину обычно легко, например, выбрать подхо-
           дящий тип для конкретной переменной.  Написать действительно  пере-
           носимую программу нижнего уровня сложнее.  Вот все,  что гарантиру-
           ется относительно размеров основных типов:
           
                1==sizeof(char)<=sizeof(short)<=     sizeof(int)<=sizeof(long)
                sizeof(float)<=sizeof(double)
           
             Однако обычно  разумно  предполагать,  что в char могут храниться
           целые числа в диапазоне 0..127 (в нем всегда могут храниться симво-
           лы  машинного  набора символов),  что short и int имеют не менее 16
           бит,  что int имеет размер, соответствующий целой арифметике, и что
           long  имеет  по меньшей мере 24 бита.  Предполагать что-либо помимо
           этого рискованно, и даже эти эмпирические правила применимы не вез-
           де. Таблицу характеристик аппаратного обеспечения для некоторых ма-
           шин можно найти в #с.2.6.
           
             Беззнаковые (unsigned) целые типы идеально подходят для  примене-
           ний, в которых память рассматривается как массив битов. Использова-
           ние unsigned вместо int с тем,  чтобы получить  еще  один  бит  для
           представления положительных целых, почти никогда не оказывается хо-
           рошей идеей. Попытки гарантировать то, что некоторые значения поло-
           жительны, посредством описания переменных как unsigned, обычно сры-
           ваются из-за правил неявного преобразования. Например:
           
             unsigned surprise = -1;
           
           допустимо (но компилятор обязательно сделает предупреждение).
                2.3.2 Неявное Преобразование Типа
           
             Основные типы можно свободно сочетать в присваиваниях и выражени-
           ях.  Везде, где это возможно, значения преобразуются так, чтобы ин-
           формация не терялась. Точные правила можно найти в #с.6.6.
           
             Существуют случаи,  в которых информация может теряться или иска-
           жаться.  Присваивание значения одного типа переменной другого типа,
           представление которого содержит меньшее число бит,  неизбежно явля-
           ется источником неприятностей.  Допустим,  например,  что следующая
           часть  программы  выполняется  на  машине с двоичным дополнительным
           представлением целых и 8-битовыми символами:
           
             int i1 = 256+255; char ch = i1 // ch == 255 int i2 = ch; // i2 ==
             ?
           
             В присваивании  ch=i1  теряется один бит (самый значимый!),  и ch
           будет содержать двоичный код "все-единицы"  (т.е.  8  единиц);  при
           присваивании i2 это никак не может превратиться в 511!  Но каким же
           может быть значение i2?  На DEC VAX, где char знаковое, ответ будет
           -1, на AT&T 3B-20, где char беззнаковые, ответ будет 255. В С++ нет
           динамического (т.е. действующего во время исполнения) механизма для
           разрешения  такого  рода проблем,  а выяснение на стадии компиляции
           вообще очень сложно, поэтому программист должен быть внимателен.
           
                2.3.3 Производные Типы
           
             Другие типы можно выводить из основных типов (и типов, определен-
           ных пользователем) посредством операций описания:
           
             * указатель
             & ссылка
             [] вектор () функция
           
           и механизма определения структур. Например:
           
             int* a;  float v[10];  char* p[20]; // вектор из 20 указателей на
             символ void f(int); struct str { short length; char* p; };
           
             Правила построения типов с помощью этих  операций  подробно  объ-
           ясняются в #с.8.3-4. Основная идея состоит в том, что описание про-
           изводного типа отражает его использование. Например:
           
             int v[10];  // описывает вектор i = v[3];  // использует  элемент
             вектора
           
             int* p;  // описывает указатель i = *p; // использует указываемый
             объект Вся сложность понимания записи производных типов  происте-
             кает из
           того, что операции * и & префиксные,  а операции [] () постфиксные,
           поэтому для формулировки типов в тех случаях, когда приоритеты опе-
           раций создают  затруднения,  надо  использовать  скобки.  Например,
           поскольку приоритет у [] выше, чем у *, то
           
             int* v[10];  //  вектор указателей int (*p)[10];  // указатель на
             вектор
           
             Большинство людей просто помнят,  как выглядят  наиболее  обычные
           типы.
           
             Описание каждого  имени,  вводимого в программе,  может оказаться
           утомительным, особенно если их типы одинаковы. Но можно описывать в
           одном  описании  несколько  имен.  В  этом случае описание содержит
           вместо одного имени список имен,  разделенных  запятыми.  Например,
           два имени можно описать так:
           
             int x, y; // int x; int y;
           
             При описании производных типов можно указать, что операции приме-
           няются только к отдельным именам (а не ко всем остальным  именам  в
           этом описании). Например:
           
             int* p, y; // int* p; int y; НЕ int* y; int x, *p; // int x; int*
             p; int v[10], *p; // int v[10]; int* p;
           
             Мнение автора таково,  что подобные конструкции делают  программу
           менее удобочитаемой, и их следует избегать.
           
                2.3.4 Тип void
           
             Тип void (пустой) синтаксически ведет себя как основной тип.  Од-
           нако использовать его можно только  как  часть  производного  типа,
           объектов типа void не существует.  Он используется для того,  чтобы
           указать,  что функция не возвращает значения,  или как базовый  тип
           для указателей на объекты неизвестного типа.
           
             void f()  //  f не возвращает значение void* pv;  // указатель на
             объект неизвестного типа
           
             Переменной типа указатель на void  (void  *),  можно  присваивать
           указатель  любого  типа.  На  первый взгляд это может показаться не
           особенно полезным,  поскольку void* нельзя разыменовать,  но именно
           это  ограничение и делает тип void* полезным.  Главным образом,  он
           применяется для передачи указателей функциям,  которые не позволяют
           сделать предположение о типе объекта, и для возврата из функций не-
           типизированных объектов. Чтобы использовать такой объект, необходи-
           мо применить явное преобразование типа. Подобные функции обычно на-
           ходятся на самом нижнем уровне системы, там, где осуществляется ра-
           бота с основными аппаратными ресурсами. Например:
           
             void* allocate(int size);  // выделить void deallocate(void*); //
             освободить
           
             f() {  int*  pi  =  (int*)allocate(10*sizeof(int));  char*  pc  =
                 (char*)allocate(10);
             //...
             deallocate(pi); deallocate(pc); }
                2.3.5 Указатели
           
             Для большинства типов T T* является типом  арифметический  указа-
           тель на T. То есть, в переменной типа T* может храниться адрес объ-
           екта типа T. Для указателей на вектора и указателей на функции вам,
           к сожалению, придется пользоваться более сложной записью:
           
             int* pi;  char**  cpp;  //  указатель  на  указатель  на char int
             (*vp)[10];  // указатель на вектор из 10 int'ов  int  (*fp)(char,
             char*); // указатель на функцию
                                     // получающую параметры (char, char*)
                                     // и возвращающую int
           
             Основная операция над указателем - разыменование,  то есть ссылка
           на объект, на который указывает указатель. Эта операция также назы-
           вается косвенным обращением. Операция разыменования - это унарное *
           (префиксное). Например:
           
             char c1 = 'a';  char* p = &c1; // в p хранится адрес c1 char c2 =
             *p; // c2 = 'a'
           
             Переменная, на которую указывает p,- это c1,  а значение, которое
           хранится в c1,  это 'a',  поэтому присваиваемое c2 значение *p есть
           'a'.
           
             Над указателями   можно   осуществлять  некоторые  арифметические
           действия.  Вот,  например, функция, подсчитывающая число символов в
           строке (не считая завершающего 0):
           
             int strlen(char* p) {
                 int i = 0; while (*p++) i++; return i;
             }
           
             Другой способ найти длину состоит в том,  чтобы сначала найти ко-
           нец строки, а затем вычесть адрес начала строки из адреса ее конца:
           
             int strlen(char* p) {
                 char* q = p; while (*q++) ; return q-p-1;
             }
           
             Очень полезными могут оказаться указатели на функции.  Они обсуж-
           даются в #4.6.7.
                2.3.6 Вектора
           
             Для типа T T[size] является типом "вектор из size элементов  типа
           T". Элементы индексируются (нумеруются) от 0 до size-1. Например:
           
             float v[3];  //  вектор  из  трех  float:  v[0],  v[1],  v[2] int
             a[2][5];  // два вектора из пяти int char* vpc;  // вектор из  32
             указателей на символ
           
             Цикл для  печати  целых значений букв нижнего регистра можно было
           бы написать так:
           
             extern int strlen(char*);
           
             char alpha[] = "abcdefghijklmnoprstuvwxyz";
           
             main() {
                 int sz = strlen(alpha);
           
                 for (int i=0; i.  Функция strlen() использовалась для подсчета
           числа символов в alpha; вместо этого можно было использовать значе-
           ние размера alpha (#2.4.4).  Если применяется набор символов ASCII,
           то выдача выглядит так:
           
             'a' = 97 = 0141 = 0x61
             'b' = 98 = 0142 = 0x62
             'c' = 99 = 0143 = 0x63
             ...
           
             Заметим, что задавать размер вектора alpha необязательно.  Компи-
           лятор  считает число символов в символьной строке,  указанной в ка-
           честве инициализатора.  Использование строки как инициализатора для
           вектора символов - удобное,  но к сожалению и единственное примене-
           ние строк. Аналогичное этому присваивание строки вектору отсутству-
           ет. Например:
           
             char v[9]; v = "строка"; // ошибка
           
           ошибочно, поскольку присваивание не определено для векторов. Конеч-
             но, для инициализации символьных массивов подходят не
           только строки.  Для  остальных  типов нужно применять более сложную
           запись.  Эту запись можно использовать и для  символьных  векторов.
           Например:
           
             int v1[] = { 1, 2, 3, 4 }; int v2[] = { 'a', 'b', 'c', 'd' };
           
             char v3[] = { 1, 2, 3, 4 }; char v4[] = { 'a', 'b', 'c', 'd' };
           
             Заметьте, что v4 - вектор из четырех (а не пяти) символов;  он не
           оканчивается нулем,  как того  требуют  соглашение  и  библиотечные
           подпрограммы.  Обычно применение такой записи ограничивается стати-
           ческими объектами.
           
             Многомерные массивы представляются как вектора векторов, и приме-
           нение  записи  через  запятую,  как это делается в некоторых других
           языках,  дает ошибку при компиляции,  так как запятая (,)  является
           операцией следования (см.  #3.2.2).  Попробуйте,  например, сделать
           так:
           
             int bad[5,2]; // ошибка
           
           и так:
           
             int v[5][2];  int bad = v[4,1];  // ошибка int good = v[4][1]; //
             ошибка
           
             Описание
           
             char v[2][5];
           
           описывает вектор из двух элементов, каждый из которых является век-
           тором типа char[5].  В следующем примере первый  из  этих  векторов
           инициализируется  первыми  пятью буквами,  а второй - первыми пятью
           цифрами.
           
             char v[2][5] = {
                 'a', 'b', 'c', 'd', 'e',
                 '0', '1', '2', '3', '4'
             }
           
             main() { for (int i = 0; i<2; i++) {
                     for (int j = 0; j<5; j++) cout << "v[" << i << "][" << j
                              << "]=" << chr(v[i][j]) << " ";
                     cout << "\n";
                 }
             }
           
           это дает в результате
           
             v[0][0]=a v[0][1]=b  v[0][2]=c  v[0][3]=d   v[0][4]=e   v[1][0]=0
             v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4
                2.3.7 Указатели и Вектора
           
             Указатели и вектора в С++ связаны очень тесно.  Имя вектора можно
           использовать как указатель на его первый элемент,  поэтому пример с
           алфавитом можно было написать так:
           
             char alpha[] = "abcdefghijklmnopqrstuvwxyz";  char*  p  =  alpha;
             char ch;
           
             while (ch = *p++) cout << chr(ch) << " = " << ch
                      << " = 0" << oct(ch) << "\n";
           
             Описание p можно было также записать как
           
             char* p = &alpha[0];
           
             Эта эквивалентность широко используется в вызовах функций,  в ко-
           торых  векторный параметр всегда передается как указатель на первый
           элемент вектора. Так, в примере
           
             extern int strlen(char*);  char v[] = "Annemarie";  char* p =  v;
             strlen(p); strlen(v);
           
           функции strlen  в  обоих  вызовах передается одно и то же значение.
           Вся штука в том,  что этого невозможно избежать; то есть не сущест-
           вует  способа описать функцию так,  чтобы вектор v в вызове функции
           копировался (#4.6.3).  Результат применения к указателям  арифмети-
           ческих операций +, -, ++ или -- зависит от типа объекта, на который
           они указывают.  Когда к указателю p типа T*  применяется  арифмети-
           ческая операция, предполагается, что p указывает на элемент вектора
           объектов типа T; p+1 означает следующий элемент этого вектора, а p-
           1 - предыдущий элемент.  Отсюда следует,  что значение p+1 будет на
           sizeof(T) больше значения p. Например, выполнение
           
             main() {
                 char cv[10]; int iv[10];
           
                 char* pc = cv; int* pi = iv;
           
                 cout <<  "char*  "  <<  long(pc+1)-long(pc) << "\n";  cout <<
                 "int* " << long(ic+1)-long(ic) << "\n";
             }
           
           дает
           
             char* 1 int* 4
           
           поскольку на моей машине каждый символ занимает один байт, а каждое
           целое занимает четыре байта.  Перед вычитанием значения  указателей
           преобразовывались  к типу long с помощью явного преобразования типа
           (#3.2.5).  Они преобразовывались к long,  а не к "очевидному"  int,
           поскольку  есть  машины,  на  которых указатель не влезет в int (то
           есть, sizeof(int). Например:
           
             void print_addr(address* p) {
                 cout << p->name << "\n"
                     << p->number << " " << p->street << "\n"
                     << p->town << "\n"
                     << chr(p->state[0]) << chr(p->state[1])
                     << " " << p->zip << "\n";
             }
           
             Объекты типа структура можно присваивать, передавать как парамет-
           ры функции и возвращать из функции в качестве результата. Например:
           
             address current;
           
             address set_current(address next) {
                 address prev = current; current = next; return prev;
             }
           
             Остальные осмысленные операции,  такие как сравнение (== и !=) не
           определены.  Однако пользователь может определить эти операции, см.
           Главу 6.  Размер объекта структурного типа нельзя вычислить  просто
           как сумму его членов. Причина этого состоит в том, что многие маши-
           ны требуют, чтобы объекты определенных типов выравнивались в памяти
           только  по  некоторым  зависящим  от архитектуры границам (типичный
           пример:  целое должно быть выравнено по границе слова),  или просто
           гораздо  более эффективно обрабатывают такие объекты,  если они вы-
           равнены в машине. Это приводит к "дырам" в структуре. Например, (на
           моей машине) sizeof(address) равен 24, а не 22, как можно было ожи-
           дать.
             Заметьте, что имя типа становится доступным сразу после того, как
           оно встретилось,  а не только после того, как полностью просмотрено
           все описание. Например:
           
             struct link{ link* previous; link* successor;
             }
           
             Новые объекты структурного типа не могут быть  описываться,  пока
           все описание не просмотрено, поэтому
           
             struct no_good { no_good member;
             };
           
           является ошибочным (компилятор не может установить размер no_good).
           Чтобы дать возможность двум (или более) структурным типам ссылаться
           друг на друга,  можно просто описать имя как имя структурного типа.
           Например:
           
             struct list; // должна быть определена позднее
           
             struct link { link* pre; link* suc; link* member_of;
             };
           
             struct list { link* head;
             }
           
             Без первого  описания  list  описание link вызвало бы к синтакси-
           ческую ошибку.
           
                2.3.9 Эквивалентность типов
           
             Два структурных типа являются различными даже когда они имеют од-
           ни и те же члены. Например:
           
             struct s1 { int a; }; struct s2 { int a; };
           
           есть два разных типа, поэтому
           
             s1 x; s2 y = x; // ошибка: несоответствие типов
           
             Структурные типы отличны также от основных типов, поэтому
           
             s1 x; int i = x; // ошибка: несоответствие типов
           
             Однако, существует  механизм  для  описания нового имени для типа
           без введения нового типа. Описание с префиксом typedef описывает не
           новую переменную данного типа, а новое имя этого типа. Например:
           
             typedef char* Pchar; Pchar p1, p2; char* p3 = p1;
           
             Это может служить удобной сокращенной записью.
                2.3.10 Ссылки
           
             Ссылка является другим именем объекта.  Главное применение ссылок
           состоит в спецификации операций для типов, определяемых пользовате-
           лем;  они обсуждаются в Главе 6. Они могут также быть полезны в ка-
           честве параметров функции.  Запись x& означает ссылка на x.  Напри-
           мер:
           
             int i = 1;  int& r = i; // r и i теперь ссылаются на один int int
             x = r // x = 1 r = 2; // i = 2;
           
             Ссылка должна быть инициализирована (должно быть что-то, для чего
           она является именем). Заметьте, что инициализация ссылки есть нечто
           совершенно отличное от присваивания ей.
           
             Вопреки ожиданиям,  ни одна операция на ссылку не действует. Нап-
           ример:
           
             int ii = 0; int& rr = ii; rr++; // ii увеличивается на 1
           
           допустимо, но rr++ не увеличивает ссылку;  вместо этого ++ применя-
           ется к int,  которым оказывается ii. Следовательно, после инициали-
           зации значение ссылки не может быть изменено;  она всегда ссылается
           на объект,  который ей было дано обозначать (денотировать) при ини-
           циализации.  Чтобы получить указатель на объект, денотируемый ссыл-
           кой rr, можно написать &rr.
           
             Очевидным способом  реализации ссылки является константный указа-
           тель, который разыменовывается при каждом использовании. Это делает
           инициализацию  ссылки  тривиальной,  когда  инициализатор  является
           lvalue (объектом, адрес которого вы можете взять, см. #с.5). Однако
           инициализатор  для &T не обязательно должен быть lvalue,  и даже не
           должен быть типа T. В таких случаях:
           
             [1] Во-первых,  если необходимо,  применяется преобразование типа
                (#с.6.6-8, #с.8.5.6),
           
             [2] Затем  полученное значение помещается во временную переменную
                и
           
             [3] Наконец,  ее адрес используется в качестве значения инициали-
                затора.
           
           Рассмотрим описание
           
             double& dr = 1;
           
           Это интерпретируется так:
           
             double* drp; // ссылка, представленная как указатель double temp;
             temp = double(1); drp = &temp;
           
             int x = 1; void incr(int& aa) { aa++; } incr(x) // x = 2 По опре-
             делению семантика передачи параметра та же, что семантика
           инициализации, поэтому параметр aa функции incr  становится  другим
           именем  для x.  Однако,  чтобы сделать программу читаемой,  в боль-
           шинстве случаев лучше всего избегать функций, которые изменяют зна-
           чение своих параметров.  Часто предпочтительно явно возвращать зна-
           чение из функции или требовать в качестве параметра указатель:
           
             int x = 1; int next(int p) { return p+1; } x = next(x); // x = 2
           
             void inc(int* p) { (*p)++; } inc(&x); // x = 3
           
             Ссылки также можно применять для определения функций, которые мо-
           гут использоваться и в левой, и в правой части присваивания. Опять,
           большая часть наиболее интересных  случаев  этого  встречается  при
           разработке  нетривиальных  типов,  определяемых пользователем.  Для
           примера давайте определим простой ассоциативный массив.  Вначале мы
           определим структуру пары следующим образом:
           
             struct pair { char* name; int val;
             };
           
             Основная идея состоит в том,  что строка имеет ассоциированное  с
           ней целое значение. Легко определить функцию поиска find(), которая
           поддерживает структуру данных,  состоящую из одного pair для каждой
           отличной  отличной от других строки,  которая была ей представлена.
           Для краткости представления используется очень простая (и  неэффек-
           тивная) реализация:
           
             const large = 1024; static pair vec[large+1};
           
             pair* find(char* p)
             /*
                 поддерживает множество пар "pair":
                 ищет p, если находит, возвращает его "pair", иначе возвращает
                 неиспользованную "pair"
             */
             { for (int i=0; vec[i].name; i++)
                     if (strcmp(p,vec[i].name)==0) return &vec[i];
           
                 if (i == large) return &vec[large-1];
           
                 return &vec[i];
             }
             Эту функцию  может  использовать  функция  value(),   реализующая
           массив целых, индексированный символьными строками (вместо обычного
           способа):
           
             int& value(char* p) {
                 pair* res  = find(p);  if (res->name == 0) { // до сих пор не
                 встречалось:
                   res->name =   new  char[strlen(p)+1];  //  инициализировать
                   strcpy(res->name,p); res->val = 0; // начальное значение 0
                 }
                 return res->val;
             }
           
             Для данной в качестве параметра строки value() находит целый объ-
           ект (а не значение соответствующего целого); после чего она возвра-
           щает ссылку на него. Ее можно использовать, например, так:
           
             const MAX = 256; // больше самого большого слова
           
             main()
             // подсчитывает число вхождений каждого слова во вводе
             { char buf[MAX];
           
                 while (cin>>buf) value(buf)++;
           
                 for (int i=0;  vec[i].name;  i++) cout << vec[i].name << ": "
                     << vec [i].val << "\n";
             }
           
             На каждом проходе цикл считывает одно слово из стандартной строки
           ввода cin в buf (см.  Главу 8),  а затем обновляет связанный с  ней
           счетчик с помощью find(). И, наконец, печатается полученная таблица
           различных слов во введенном тексте,  каждое с числом его  встречае-
           мости. Например, если вводится
           
             aa bb bb aa aa bb aa aa
           
           то программа выдаст:
           
             aa: 5 bb: 3
           
             Легко усовершенствовать  это в плане собственного типа ассоцииро-
           ванного массива с помощью класса с перегруженной  операцией  (#6.7)
           выбора [].
                2.3.11 Регистры
           
             Во многих машинных архитектурах можно  обращаться  к  (небольшим)
           объектам заметно быстрее, когда они помещены в регистр. В идеальном
           случае  компилятор  будет  сам  определять  оптимальную   стратегию
           использования всех регистров, доступных на машине, для которой ком-
           пилируется программа.  Однако  это  нетривиальная  задача,  поэтому
           иногда программисту стоит дать подсказку компилятору.  Это делается
           с помощью описания объекта как register. Например:
           
             register int i; register point cursor; register char* p;
           
             Описание register следует использовать только в тех случаях, ког-
           да  эффективность  действительно важна.  Описание каждой переменной
           как register засорит текст программы и может даже  увеличить  время
           выполнения  (обычно воспринимаются все инструкции по помещению объ-
           екта в регистр или удалению его оттуда).
           
             Невозможно получить адрес имени, описанного как register, регистр
           не может также быть глобальным.
           
                2.4 Константы
           
             С++ дает  возможность записи значений основных типов:  символьных
           констант, целых констант и констант с плавающей точкой. Кроме того,
           ноль  (0)  может  использоваться как константа любого указательного
           типа,  и символьные строки являются константами типа char[].  Можно
           также  задавать символические константы.  Символическая константа -
           это имя, значение которого не может быть изменено в его области ви-
           димости.  В С++ имеется три вида символических констант: (1) любому
           значению любого типа можно дать имя и использовать его как констан-
           ту,  добавив к его описанию ключевое слово const; (2) множество це-
           лых констант может быть определено как перечисление;  и  (3)  любое
           имя вектора или функции является константой.
           
                2.4.1 Целые Константы
           
             Целые константы предстают в четырех обличьях: десятичные, восьме-
           ричные,  шестнадцатиричные константа и символьные константы.  Деся-
           тичные  используются  чаще всего и выглядят так,  как можно было бы
           ожидать:
           
             0 1234 976 12345678901234567890
           
             Десятичная константа имеет тип int,  при условии, что она влезает
           в int, в противном случае ее тип long. Компилятор должен предупреж-
           дать о константах, которые слишком длинны для представления в маши-
           не.
             Константа, которая начинается нулем за которым идет x (0x), явля-
           ется шестнадцатиричным числом (с основанием 16), а константа, кото-
           рая начинается нулем за которым идет цифра,  является  восьмеричным
           числом (с основанием 8). Вот примеры восьмеричных констант:
           
             0 02 077 0123
           
           их десятичные эквиваленты - это 0,  2,  63, 83. В шестнадцатиричной
           записи эти константы выглядят так:
           
             0x0 0x2 0x3f 0x53
           
             Буквы a,  b,  c, d, e и f, или их эквиваленты в верхнем регистре,
           используются для представления чисел 10, 11, 12, 13, 14 и 15, соот-
           ветственно. Восьмеричная и шестнадцатиричная записи наиболее полез-
           ны  для  записи  набора битов применение этих записей для выражения
           обычных чисел может привести к неожиданностям. Например, на машине,
           где  int представляется как двоичное дополнительное шестнадцатирич-
           ное целое, 0xffff является отрицательным десятичным числом -1; если
           бы для представления целого использовалось большее число битов,  то
           оно было бы числом 65535.
           
                2.4.2 Константы с Плавающей Точкой
           
             Константы с плавающей точкой имеют тип double. Как и в предыдущем
           случае,  компилятор  должен  предупреждать о константах с плавающей
           точкой,  которые слишком велики,  чтобы их можно было  представить.
           Вот некоторые константы с плавающей точкой:
           
             1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15
           
           Заметьте, что  в  середине  константы  с  плавающей точкой не может
           встречаться пробел.  Например,  65.43 e-21 является не константой с
           плавающей  точкой,  а  четырьмя  отдельными  лексическими символами
           (лексемами):
           
             65.43 e - 21
           
           и вызовет синтаксическую ошибку.
           
             Если вы хотите иметь константу константа с плавающей точкой; типа
           float, вы можете определить ее так (#2.4.6):
           
             const float pi = 3.14159265;
                2.4.3 Символьные Константы
           
             Хотя в С++ и нет отдельного символьного типа данных, точнее, сим-
           вол может храниться в целом типе, в нем для символов имеется специ-
           альная и удобная запись.  Символьная константа - это символ, заклю-
           ченный в одинарные кавычки; например, 'a' или '0'. Такие символьные
           константы в действительности  являются  символическими  константами
           для целого значения символов в наборе символов той машины, на кото-
           рой будет выполняться программа (который не обязательно совпадает с
           набором символов, применяемом на том компьютере, где программа ком-
           пилируется).  Поэтому, если вы выполняетесь на машине, использующей
           набор символов ASCII, то значением '0' будет 48, но если ваша маши-
           на использует EBCDIC набор символов, то оно будет 240. Употребление
           символьных констант вместо десятичной записи делает программу более
           переносимой.  Несколько символов также имеют стандартные  имена,  в
           которых обратная косая \ используется как escape-символ:
           
                 '\b', возврат назад
                 '\f', перевод формата
                 '\n', новая строка
                 '\r', возврат каретки
                 '\t', горизонтальная табуляция
                 '\v', вертикальная табуляция
                 '\\', \ обратная косая (обратный слэш)
                 '\'', одинарная кавычка '
                 '\"', двойная кавычка "
                 '\0', null, пустой символ, целое значение 0
           
             Вопреки их внешнему виду каждое является  одним  символом.  Можно
           также представлять символ одно-,  дву- или трехзначным восьмеричным
           числом (символ \,  за которым идут восьмеричные цифры),  или одно-,
           дву- или трехзначным шестнадцатиричным числом (\x,  за которым идут
           шестнадцатиричные цифры). Например:
           
             '\6' '\x6' 6 ASCII ack
             '\60' '\x30' 48 ASCII '0'
             '\137' '\x05f' 95 ASCII '_'
           
             Это позволяет представлять каждый символ из машинного набора сим-
           волов,  и  в  частности вставлять такие символы в символьные строки
           (см. следующий раздел). Применение числовой записи для символов де-
           лает  программу  непереносимой между машинами с различными наборами
           символов.
                2.4.4 Строки
           
             Строковая константа - это последовательность символов,  заключен-
           ная в двойные кавычки "
           
             "это строка"
           
             Каждая строковая константа содержит на один  символ  больше,  чем
           кажется; все они заканчиваются пустым символом '\0' со значением 0.
           Например:
           
             sizeof("asdf")==5;
           
             Строка имеет тип "вектор из соответствующего числа символов", по-
           этому  "asdf"  имеет тип char[5].  Пустая строка записывается "" (и
           имеет  тип  char[1]).   Заметьте,   что   для   каждой   строки   s
           strlen(s)==sizeof(s)-1, поскольку strlen() не учитывает завершающий
           0.
           
             Соглашение о  представлении  неграфических  символов  с  обратной
           косой  можно  использовать также и внутри строки.  Это дает возмож-
           ность представлять двойные кавычки и escape-символ.  Самым  обычным
           символом этого рода является, безусловно, символ новой строки '\n'.
           Например:
           
             cout << "гудок в конце сообщения\007\n"
           
           где 7 - значение ASKII символа bel (звонок).
           
             В строке невозможно иметь "настоящую" новую строку:
           
               "это не строка, а синтаксическая ошибка"
           
             Однако в строке может стоять обратная косая,  сразу после которой
           идет новая строка; и то, и другое будет проигнорировано. Например:
           
               cout << "здесь все \ ok"
           
           напечатает
           
               здесь все ok
           
             Новая строка, перед которой идет escape (обратная косая), не при-
           водит к появлению в строке новой строки,  это просто договоренность
           о записи.
           
             В строке  можно  иметь пустой символ,  но большинство программ не
           будет предполагать,  что есть символы после него.  Например, строка
           "asdf\000hjkl" будет рассматриваться стандартными функциями,  вроде
           strcpy() и strlen(), как "asdf".
           
             Вставляя численную константу в строку с помощью восьмеричной  или
           шестнадцатиричной  записи благоразумно всегда использовать число из
           трех цифр.  Читать запись достаточно  трудно  и  без  необходимости
           беспокоиться  о том,  является ли символ после константы цифрой или
           нет. Разберите эти примеры:
           
               char v1[] = "a\x0fah\0129";  // 'a' '\xfa' 'h' '\12'  '9'  char
               v2[]  =  "a\xfah\129";  // 'a' '\xfa' 'h' '\12' '9' char v3[] =
               "a\xfad\127"; // 'a' '\xfad' '\127'
           
             Имейте в виду,  что двухзначной шестнадцатиричной записи на маши-
           нах с 9-битовым байтом будет недостаточно.
                2.4.5 Ноль
           
             Ноль можно употреблять как константу  любого  целого,  плавающего
           или указательного типа.  Никакой объект не размещается по адресу 0.
           Тип нуля определяется контекстом.  Обычно (но  не  обязательно)  он
           представляется набором битов все-нули соответствующей длины.
           
                2.4.6 Const
           
             Ключевое слово const может добавляться к описанию объекта,  чтобы
           сделать этот объект константой, а не переменной. Например:
           
             const int model = 145; const int v[] = { 1, 2, 3, 4 };
           
             Поскольку константе ничего нельзя присвоить, она должна быть ини-
           циализирована.  Описание чего-нибудь как const гарантирует, что его
           значение не изменится в области видимости:
           
             model = 145; // ошибка model++; // ошибка
           
             Заметьте, что const изменяет тип,  то  есть  ограничивает  способ
           использования объекта,  вместо того, чтобы задавать способ размеще-
           ния константы. Поэтому например вполне разумно, а иногда и полезно,
           описывать функцию как возвращающую const:
           
             const char* peek(int i) {
                 return private[i];
             }
           
             Функцию вроде этой можно было бы использовать для того, чтобы да-
           вать кому-нибудь читать строку,  которая не может быть затерта  или
           переписана (этим кем-то).
           
             С другой стороны,  компилятор может несколькими путями воспользо-
           ваться тем,  что объект является константой (конечно, в зависимости
           от того, насколько он сообразителен). Самое очевидное - это то, что
           для константы не требуется выделять  память,  поскольку  компилятор
           знает ее значение. Кроме того, инициализатор константы часто (но не
           всегда) является константным выражением,  то есть он может быть вы-
           числен  на  стадии  компиляции.  Однако для вектора констант обычно
           приходится выделять память,  поскольку компилятор в общем случае не
           может вычислить, на какие элементы вектора сделаны ссылки в выраже-
           ниях. Однако на многих машинах даже в этом случае может достигаться
           повышение  эффективности  путем  размещения векторов констант в па-
           мять, доступную только для чтения.
           
             Использование указателя вовлекает два объекта:  сам  указатель  и
           указываемый объект.  Снабжение описания указателя "префиксом" const
           делает объект, но не сам указатель, константой. Например:
           
             const char* pc = "asdf";  // указатель на константу pc[3] =  'a';
             // ошибка pc = "ghjk"; // ok Чтобы описать сам const указатель, а
             не указываемый объект, как
           константный, используется операция const*. Например:
           
             char *const cp = "asdf"; // константный указатель cp[3] = 'a'; //
             ok cp = "ghjk"; // ошибка
           
             Чтобы сделать константами  оба  объекта,  их  оба  нужно  описать
           const. Например:
           
             const char  *const  cpc  =  "asdf";  //  const указатель на const
             cpc[3] = 'a'; // ошибка cpc = "ghjk"; // ошибка
           
             Объект, являющийся константой при доступе к нему через один  ука-
           затель,  может быть переменной, когда доступ осуществляется другими
           путями. Это в частности полезно для параметров функции. Посредством
           описания параметра указателя как const функции запрещается изменять
           объект, на который он указывает. Например:
           
             char* strcpy(char* p, const char* q); // не может изменить q
           
             Указателю на  константу  можно  присваивать   адрес   переменной,
           поскольку  никакого  вреда  от  этого быть не может.  Однако нельзя
           присвоить адрес константы указателю,  на который не  было  наложено
           ограничение,  поскольку это позволило бы изменить значение объекта.
           Например:
           
             int a = 1;  const c = 2; const* p1 = &c; // ok const* p2 = &a; //
             ok int* p3 = &c; // ошибка *p3 = 7; // меняет значение c
           
             Как обычно, если тип в описании опущен, то он предполагается int.
                2.4.7 Перечисления
           
             Есть другой метод определения целых констант,  который иногда бо-
           лее удобен, чем применение const. Например:
           
             enum { ASM, AUTO, BREAK };
           
           перечисление определяет три целых константы,  называемых перечисли-
           телями,  и присваивает им значения. Поскольку значения перечислите-
           лей  по  умолчанию присваиваются начиная с 0 в порядке возрастания,
           это эквивалентно записи:
           
             const ASM = 0; const AUTO = 1; const BREAK = 2;
           
           Перечисление может быть именованным. Например:
           
             enum keyword { ASM, AUTO, BREAK };
           
             Имя перечисления становится синонимом  int,  а  не  новым  типом.
           Описание переменной keyword,  а не просто int, может дать как прог-
           раммисту,  так и компилятору подсказку  о  том,  что  использование
           преднамеренное. Например:
           
             keyword key;
           
             switch (key) { case ASM:
                 // что-то делает
                 break; case BREAK:
                 // что-то делает
                 break;
             }
           
           побуждает компилятор выдать предупреждение,  поскольку  только  два
           значения keyword из трех используются.
           
             Можно также задавать значения перечислителей явно. Например:
           
             enum int16  { sign=0100000,  // знак most_significant=040000,  //
                 самый значимый least_significant=1 // наименее значимый
             };
           
             Такие значения не обязательно должны быть различными,  возрастаю-
           щими или положительными.
                2.5 Экономия Пространства
           
             В ходе программирования нетривиальных разработок неизбежно насту-
           пает время,  когда хочется иметь больше  пространства  памяти,  чем
           имеется или отпущено. Есть два способа выжать побольше пространства
           из того, что доступно:
           
             [1] Помещение в байт более одного небольшого объекта и
           
             [2] Использование одного и того же пространства для хранения раз-
                ных объектов в разное время.
           
             Первого можно  достичь  с помощью использования полей,  второго -
           через использование объединений. Эти конструкции описываются в сле-
           дующих  разделах.  Поскольку  обычное их применение состоит чисто в
           оптимизации программы,  и они в большинстве  случаев  непереносимы,
           программисту  следует дважды подумать,  прежде чем использовать их.
           Часто лучше изменить способ управления  данными;  например,  больше
           полагаться  на  динамически  выделяемую память (#3.2.6) и меньше на
           заранее выделенную статическую память.
           
                2.5.1 Поля
           
             Использование char для представления двоичной переменной,  напри-
           мер,  переключателя включено/выключено,  может показаться экстрава-
           гантным,  но char является наименьшим объектом, который в С++ может
           выделяться независимо. Можно, однако, сгруппировать несколько таких
           крошечных переменных вместе в виде полей struct.  Член определяется
           как поле путем указания после его имени числа битов, которые он за-
           нимает. Допустимы неименованные поля; они не влияют на смысл имено-
           ванных  полей,  но  неким  машинно-зависимым образом могут улучшить
           размещение:
           
             struct sreg { unsigned enable :  1; unsigned page : 3; unsigned :
                 1;  // неиспользуемое unsigned mode : 2; unsigned : 4: // не-
                 используемое unsigned  access  :  1;  unsigned  length  :  1;
                 unsigned non_resident : 1;
             }
           
             Получилось размещение регистра 0 состояния DEC PDP11/45 (в  пред-
           положении, что поля в слове размещаются слева направо). Этот пример
           также иллюстрирует  другое  основное  применение  полей:  именовать
           части внешне предписанного размещения. Поле должно быть целого типа
           и используется как другие целые, за исключением того, что невозмож-
           но  взять  адрес поля.  В ядре операционной системы или в отладчике
           тип sreg можно было бы использовать так:
           
             sreg* sr0 = (sreg*)0777572;
             //...
             if (sr->access) { // нарушение доступа
                 // чистит массив
                 sr->access = 0;
             }
             Однако применение полей для упаковки нескольких переменных в один
           байт  не  обязательно  экономит  пространство.  Оно экономит прост-
           ранство,  занимаемое данными, но объем кода, необходимого для мани-
           пуляции этими переменными, на большинстве машин возрастает. Извест-
           ны программы,  которые значительно сжимались,  когда двоичные пере-
           менные преобразовывались из полей бит в символы! Кроме того, доступ
           к char или int обычно намного быстрее,  чем доступ к полю.  Поля  -
           это  просто удобная и краткая запись для применения логических опе-
           раций с целью извлечения информации из части слова или введения ин-
           формации в нее.
           
                2.5.2 Объединения
           
             Рассмотрим проектирование  символьной  таблицы,  в которой каждый
           элемент содержит имя и значение,  и значение может быть либо  стро-
           кой, либо целым:
           
             struct entry  {  char* name;  char type;  char* string_value;  //
                 используется если type == 's' int int_value;  // используется
                 если type == 'i'
             };
           
             void print_entry(entry* p) {
                 switch p->type { case 's':
                     cout << p->string_value; break;
                 case 'i':
                     cout << p->int_value; break;
                 default:
                     cerr << "испорчен type\n"; break;
                 }
             }
           
             Поскольку string_value и int_value  никогда  не  могут  использо-
           ваться одновременно,  ясно, что пространство пропадает впустую. Это
           можно легко исправить,  указав,  что оба они  должны  быть  членами
           union. Например, так:
           
             struct entry { char* name; char type; union {
                     char* string_value;  // используется если type == 's' int
                     int_value; // используется если type == 'i'
                 };
             };
           
             Это оставляет всю часть программы,  использующую entry, без изме-
           нений,  но обеспечивает,  что при размещении entry  string_value  и
           int_value имеют один и тот же адрес.  Отсюда следует, что все члены
           объединения вместе занимают лишь столько памяти,  сколько  занимает
           наибольший член.
             Использование объединений таким образом,  чтобы при чтении значе-
           ния всегда применялся тот член,  с применением которого оно записы-
           валось, совершенно оптимально. Но в больших программах непросто га-
           рантировать,  что объединения используются только таким образом,  и
           из-за неправильного использования могут появляться трудно  уловимые
           ошибки.  Можно  @капсулизировать  объединение таким образом,  чтобы
           соответствие между полем типа и типами членов  было  гарантированно
           правильным (#5.4.6).
           
             Объединения иногда  используют  для "объединения и преобразование
           типа" (это делают главным образом программисты, воспитанные на язы-
           ках, не обладающих средствами преобразования типов, где жульничест-
           во является необходимым).  Например, это "преобразует" на VAX'е int
           в int*, просто предполагая побитовую эквивалентность:
           
             struct fudge { union {
                     int i; int* p;
                 };
             };
           
             fudge a; a.i = 4096; int* p = a.p; // плохое использование
           
             Но на самом деле это совсем не преобразование: на некоторых маши-
           нах int и int* занимают неодинаковое количество памяти, а на других
           никакое целое не может иметь нечетный адрес.  Такое применение объ-
           единений  непереносимо,  а есть явный способ указать преобразование
           типа (#3.2.5).
           
             Изредка объединения умышленно применяют,  чтобы избежать преобра-
           зования типов.  Можно,  например,  использовать fudge, чтобы узнать
           представление указателя 0:
           
             fudge.p = 0; int i = fudge.i; // i не обязательно должно быть 0
           
             Можно также дать объединению имя,  то есть сделать его полноправ-
           ным типом. Например, fudge можно было бы описать так:
           
             union fudge { int i; int* p;
             };
           
           и использовать (неправильно) в точности как раньше. Имеются также и
           оправданные применения именованных объединений, см. #5.4.6.
                2.6 Упражнения
           
             1. (*1) Заставьте работать программу с "Hello, world" (1.1.1).
           
             2. (*1) Для каждого описания  в  #2.1  сделайте  следующее:  Если
                описание не является определением, напишите для него определе-
                ние.  Если описание является определением,  напишите для  него
                описание, которое при этом не является определением.
           
             3. (*1) Напишите описания для: указателя на символ; вектора из
                10 целых; ссылки на вектор из 10 целых; указателя на вектор из
                символьных строк;  указателя на указатель на символ; констант-
                ного целого;  указателя на константное целое;  и  константного
                указателя на целое. Каждый из них инициализируйте.
           
             4. (*1.5) Напишите программу, которая печатает размеры основных и
                указательных типов. Используйте операцию sizeof.
           
             5. (*1.5) Напишите программу,  которая печатает буквы 'a'...'z' и
                цифры  '0'...'9'  и  их числовые значения.  Сделайте то же для
                остальных печатаемых символов.  Сделайте то же,  но  используя
                шестнадцатиричную запись.
           
             6. (*1) Напечатайте набор битов, которым представляется указатель
                0 на вашей системе. Подсказка: #2.5.2.
           
             7. (*1.5) Напишите функцию,  печатающую порядок и мантиссу  пара-
                метра типа double.
           
             8. (*2) Каковы наибольшие и наименьшие значения,  на вашей систе-
                ме,  следующих типов:  char,  short, int, long, float, double,
                unsigned, char*, int* и void*? Имеются ли дополнительные огра-
                ничения на принимаемые ими значения?  Может ли, например, int*
                принимать нечетное значение? Как выравниваются в памяти объек-
                ты этих типов? Может ли, например, int иметь нечетный адрес?
           
             9. (*1) Какое самое длинное локальное имя  можно  использовать  в
                С++ программе в вашей системе? Какое самое длинное внешнее имя
                можно использовать в С++ программе в вашей  системе?  Есть  ли
                какие-нибудь ограничения на символы, которые можно употреблять
                в имени?
           
             10. (*2) Определите one следующим образом:
           
                  const one = 1;
           
                Попытайтесь поменять значение one на 2. Определите num следую-
                щим образом:
           
                  const num[] = { 1, 2 };
           
                Попытайтесь поменять значение num[1] на 2.
             11. (*1) Напишите функцию,  переставляющую  два  целых  (меняющую
                значения). Используйте в качестве типа параметра int*. Напиши-
                те другую переставляющую функцию, использующую в качестве типа
                параметра int&.
           
             12. (*1) Каков размер вектора str в следующем примере:
           
                  char str[] = "a short string";
           
                Какова длина строки "a short string"?
           
             13. (*1.5)  Определите таблицу названий месяцев года и числа дней
                в них.  Выведите ее. Сделайте это два раза: один раз используя
                вектор  для  названий  и  вектор  для  числа дней,  и один раз
                используя вектор структур, в каждой из которых хранится назва-
                ние месяца и число дней в нем.
           
             14. (*1)  С  помощью  typedef определите типы:  беззнаковый char,
                константный беззнаковый char, указатель на целое, указатель на
                указатель на char,  указатель на вектора символов, вектор из 7
                целых указателей, указатель на вектор из 7 целых указателей, и
                вектор из 8 векторов из 7 целых указателей.


Яндекс цитирования