Основы программирования

Наследство от языка Си
Индекс материала
Наследство от языка Си
Область Видимости
Объекты и Адреса (Lvalue)
Имена
Типы
Неявное Преобразование Типа
Тип void
Указатели
Вектора
Структуры
Эквивалентность типов
Регистры
Константы
Символьные Константы
Строки
Const
Перечисления
Экономия Пространства
Упражнения
Выражения и Операторы
Программа синтаксического разбора
Функция ввода
Таблица имен
Обработка ошибок
Драйвер
Параметры командной строки
Краткая сводка операций
Порядок вычисления
Побитовые логические операции
Преобразование типа
Свободная память
Сводка операторов
Проверки
Goto
Комментарии и Выравнивание
Упражнения
Функции и Файлы
Компоновка
Заголовочные Файлы
Один Заголовочный Файл
Множественные Заголовочные Файлы
Сокрытие Данных
Файлы как Модули
Как Создать Библиотеку
Функции
ередача Параметров
Возврат Значения
Векторные Параметры
Параметры по Умолчанию
Перегрузка Имен Функций
Незаданное Число Параметров
Все страницы

Описания и Константы

"Совершенство достигается только к моменту краха..." -С.Н. Паркинсон


В этой части описаны основные типы (char, int, float и т.д.) и основные способы построения из них новых типов (функций, векторов, указателей и т.д.). Имя вводится в программе посредством описания, которое задает его тип и, возможно, начальное значение. Даны понятия описания, определения, области видимости имен, времени жизни объектов и типов. Описываются способы записи констант в С++, а также способы определения символических констант. Примеры просто демонстрируют характерные черты языка.

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 = amp;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 «stream.h» была выброшена из примеров в этой главе для экономии места. Она необходима в примерах, производящих вывод, чтобы они были полными. (прим. автора)

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 amp;T 3B-20, где char беззнаковые, ответ будет 255. В С++ нет динамического (т.е. действующего во время исполнения) механизма для разрешения такого рода проблем, а выяснение на стадии компиляции вообще очень сложно, поэтому программист должен быть внимателен.



2.3.3 Производные Типы

Другие типы можно выводить из основных типов (и типов, определенных пользователем) посредством операций описания:

* указатель amp; ссылка [] вектор () функция

и механизма определения структур. Например:

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; // использует указываемый объект

Вся сложность понимания записи производных типов проистекает из того, что операции * и amp; префиксные, а операции [] () постфиксные, поэтому для формулировки типов в тех случаях, когда приоритеты операций создают затруднения, надо использовать скобки. Например, поскольку приоритет у [] выше, чем у *, то

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 = amp;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«sz; i++) (* char ch = alpha[i]; cout „„ "'" „« chr(ch) «« "'" «« " = " «« ch «« « = 0“ «« oct(ch) «« « = 0x“ «« hex(ch) «« «\n“; *) *)

Функция chr() возвращает представление небольшого целого в виде строки; например, chr(80) это "P" на машине, на которой используется набор символов ASCII. Функция oct() строит восьмеричное представление своего целого аргумента, а hex() строит шестнадцатеричное представление своего целого аргумента; chr() oct() и hex() описаны в «stream.h». Функция 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 = amp;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 предыдущий элемент. Отсюда следует, что значение 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)«sizeof(long) ).

Вычитание указателей определено только тогда, когда оба указателя указывают на элементы одного и того же вектора (хотя в языке нет способа удостовериться, что это так). Когда из одного указателя вычитается другой, результатом является число элементов вектора между этими указателями (целое число). Можно добавлять целое к указателю или вычитать целое из указателя; в обоих случаях результатом будет значение типа указателя. Если это значение не указывает на элемент того же вектора, на который указывал исходный указатель, то результат использования этого значения неопределён. Например:

int v1[10]; int v2[10];

int i = amp;v1[5]– amp;v1[3]; // 2 i = amp;v1[5]– amp;v2[3]; // результат неопределен

int* p = v2+2; // p == amp;v2[2] p = v2-2; // p неопределено




2.3.8 Структуры

Вектор есть совокупность элементов одного типа, struct является совокупностью элементов (практически) произвольных типов. Например:

struct address (* // почтовый адрес char* name; // имя «Jim Dandy» long number; // номер дома 61 char* street; // улица «South Street» char* town; // город «New Providence» char* state[2]; // штат 'N' 'J' int zip; // индекс 7974 *)

определяет новый тип, названный address (почтовый адрес), состоящий из пунктов, требующихся для того, чтобы послать кому-нибудь корреспонденцию (вообще говоря, address не


является достаточным для работы с полным почтовым адресом, но в качестве примера достаточен). Обратите внимание на точку с запятой в конце; это одно из очень немногих мест в С++, где необходимо ставить точку с запятой после фигурной скобки, поэтому люди склонны забывать об этом.

Переменные типа address могут описываться точно также, как другие переменные, а доступ к отдельным членам получается с помощью операции . (точка). Например:

address jd; jd.name = «Jim Dandy»; jd.number = 61;

Запись, которая использовалась для инициализации векторов, можно применять и к переменным структурных типов. Например:

address jd = (* «Jim Dandy», 61, «South Street», «New Providence», (*'N','J'*), 7974 *);

Однако обычно лучше использовать конструктор (#5.2.4). Заметьте, что нельзя было бы инициализировать jd.state строкой «NJ». Строки оканчиваются символом '\0', поэтому в «NJ» три символа, то есть на один больше, чем влезет в jd.state.

К структурным объектам часто обращаются посредством указателей используя операцию -». Например:

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 amp; означает ссылка на x. Например:

int i = 1; int amp; r = i; // r и i теперь ссылаются на один int int x = r // x = 1 r = 2; // i = 2;

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

Вопреки ожиданиям, ни одна операция на ссылку не действует. Например:

int ii = 0; int amp; rr = ii; rr++; // ii увеличивается на 1

допустимо, но rr++ не увеличивает ссылку; вместо этого + + применяется к int, которым оказывается ii. Следовательно, после инициализации значение ссылки не может быть изменено; она всегда ссылается на объект, который ей было дано обозначать (денотировать) при инициализации. Чтобы получить указатель на объект, денотируемый ссылкой rr, можно написать amp;rr.

Очевидным способом реализации ссылки является константный указатель, который разыменовывается при каждом использовании. Это делает инициализацию ссылки тривиальной, когда инициализатор является lvalue (объектом, адрес которого вы можете взять, см. #с.5). Однако инициализатор для amp;T не обязательно должен быть lvalue, и даже не должен быть типа T. В таких случаях:

1. Во-первых, если необходимо, применяется преобразование типа (#с.6.6-8, #с.8.5.6),

2. Затем полученное значение помещается во временную переменную и

3. Наконец, ее адрес используется в качестве значения инициализатора.

Рассмотрим описание

double amp; dr = 1;

Это интерпретируется так:

double* drp; // ссылка, представленная как указатель double temp; temp = double(1); drp = amp;temp;

int x = 1; void incr(int amp; 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( amp;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 amp;vec[i];

if (i == large) return amp;vec[large-1];

return amp;vec[i]; *)

Эту функцию может использовать функция value(), реализующая массив целых, индексированный символьными строками (вместо обычного способа):

int amp; 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 (0 x), является шестнадцатеричным числом (с основанием 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 = amp;c; // ok const* p2 = amp;a; // ok int* p3 = amp;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 amp;.

12. (*1) Каков размер вектора str в следующем примере:

char str[] = «a short string»;

Какова длина строки «a short string»?

13. (*1.5) Определите таблицу названий месяцев года и числа дней в них. Выведите ее. Сделайте это два раза: один раз используя вектор для названий и вектор для числа дней, и один раз используя вектор структур, в каждой из которых хранится название месяца и число дней в нем.

14. (*1) С помощью typedef определите типы: беззнаковый char, константный беззнаковый char, указатель на целое, указатель на указатель на char, указатель на вектора символов, вектор из 7 целых указателей, указатель на вектор из 7 целых указателей, и вектор из 8 векторов из 7 целых указателей.



Глава 3 Выражения и Операторы

 

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

– * Нам неизвестен русскоязычный термин, эквивалентный английскому indentation. Иногда это называется отступами. (прим. перев.)



3.1 Настольный калькулятор

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

r=2.5 area=pi*r*r

(pi определено заранее), то программа калькулятора напишет:

2.5 19.635

где 2.5 – результат первой введенной строки, а 19.635 – результат второй.

Калькулятор состоит из четырех основных частей: программы синтаксического разбора (parser'а), функции ввода, таблицы имен и управляющей программы (драйвера). Фактически, это миниатюрный компилятор, в котором программа синтаксического разбора производит синтаксический анализ, функция ввода осуществляет ввод и лексический анализ, в таблице имен хранится долговременная информация, а драйвер распоряжается инициализацией, выводом и обработкой ошибок. Можно было бы многое добавить в этот калькулятор, чтобы сделать его более полезным, но в существующем виде эта программа и так достаточно длинна (200 строк), и большая часть дополнительных возможностей просто увеличит текст программы не давая дополнительного понимания применения С++.




3.1.1 Программа синтаксического разбора

Вот грамматика языка, допускаемого калькулятором:

program: END // END – это конец ввода expr_list END

expr_list: expression PRINT // PRINT – это или '\n' или ';' expression PRINT expr_list


expression: expression + term expression – term term

term: term / primary term * primary primary

primary: NUMBER // число с плавающей точкой в С++ NAME // имя С++ за исключением '_' NAME = expression – primary ( expression )

Другими словами, программа есть последовательность строк. Каждая строка состоит из одного или более выражений, разделенных запятой. Основными элементами выражения являются числа, имена и операции *, /, +, – (унарный и бинарный) и =. Имена не обязательно должны описываться до использования.

Используемый метод обычно называется рекурсивным спуском это популярный и простой нисходящий метод. В таком языке, как С++, в котором вызовы функций относительно дешевы, этот метод к тому же и эффективен. Для каждого правила вывода грамматики имеется функция, вызывающая другие функции. Терминальные символы (например, END, NUMBER, + и -) распознаются лексическим анализатором get_token(), а нетерминальные символы распознаются функциями синтаксического анализа expr(), term() и prim(). Как только оба операнда (под)выражения известны, оно вычисляется; в настоящем компиляторе в этой точке производится генерация кода.

Программа разбора для получения ввода использует функцию get_token(). Значение последнего вызова get_token() находится в переменной curr_tok; curr_tok имеет одно из значений перечисления token_value:

enum token_value (* NAME NUMBER END PLUS='+' MINUS='-' MUL='*' DIV='/' PRINT=';' ASSIGN='=' LP='(' RP=')' *); token_value curr_tok;

В каждой функции разбора предполагается, что было обращение к get_token(), и в curr_tok находится очередной символ, подлежащий анализу. Это позволяет программе разбора заглядывать на один лексический символ (лексему) вперед и заставляет функцию разбора всегда читать на одну лексему больше, чем используется правилом, для обработки которого она была вызвана. Каждая функция разбора вычисляет «свое» выражение и возвращает значение. Функция expr() обрабатывает сложение и вычитание; она состоит из простого цикла, который ищет термы для сложения или вычитания:

double expr() // складывает и вычитает (* double left = term();

for(;;) // ``навсегда`` switch(curr_tok) (* case PLUS: get_token(); // ест '+' left += term();

break; case MINUS: get_token(); // ест '-' left -= term(); break; default: return left; *) *)

Фактически сама функция делает не очень много. В манере, достаточно типичной для функций более высокого уровня в больших программах, она вызывает для выполнения работы другие функции. Заметьте, что выражение 2-3+4 вычисляется как (2-3)+ 4, как указано грамматикой.

Странная запись for(;;) – это стандартный способ задать бесконечный цикл. Можно произносить это как «навсегда»*. Это вырожденная форма оператора for, альтернатива – while(1). Выполнение оператора switch повторяется до тех пор, пока не будет найдено ни + ни -, и тогда выполняется оператор return в случае default.

– * игра слов: «for» – «forever» (навсегда). (прим. перев.)

Операции +=, -= используются для осуществления сложения и вычитания. Можно было бы не изменяя смысла программы использовать left=left+term() и left=left-term(). Однако left+= term() и left-=term() не только короче, но к тому же явно выражают подразумеваемое действие. Для бинарной операции @ выражение x@=y означает x=x@y за исключением того, что x вычисляется только один раз. Это применимо к бинарным операциям

+ – * / % amp; ! ^ «„ “»

поэтому возможны следующие операции присваивания:

+= -= *= /= %= amp;= != ^= «„= “»=

Каждая является отдельной лексемой, поэтому a+ =1 является синтаксической ошибкой из-за пробела между + и =. (% является операцией взятия по модулю; amp;,! и ^ являются побитвыми операциями И, ИЛИ и исключающее ИЛИ; «„ и “» являются операциями левого и правого сдвига). Функции term() и get_token() должны быть описаны до expr().

Как организовать программу в виде набора файлов, обсудается в Главе 4. За одним исключением все описания в данной программе настольного калькулятора можно упорядочить так, чтобы все описывалось ровно один раз и до использования. Ислючением является expr(), которая обращается к term(), котрая обращается к prim(), которая в свою очередь обращается к expr(). Этот круг надо как-то разорвать;

Описание

double expr(); // без этого нельзя

перед prim() прекрасно справляется с этим.

Функция term() аналогичным образом обрабатывает умножние и сложение:

double term() // умножает и складывает (* double left = prim();

for(;;) switch(curr_tok) (* case MUL: get_token(); // ест '*' left *= prim(); break; case DIV: get_token(); // ест '/' double d = prim(); if (d == 0) return error(«деление на 0»); left /= d; break; default: return left; *) *)

Проверка, которая делается, чтобы удостовериться в том, что нет деления на ноль, необходима, поскольку результат дления на ноль неопределен и как правило является роковым. Функция error(char*) будет описана позже. Переменная d ввдится в программе там, где она нужна, и сразу же инициализруется. Во многих языках описание может располагаться только в голове блока. Это ограничение может приводить к довольно скверному искажению стиля программирования и/или излишним ошибкам. Чаще всего неинициализированные локальные переменные являются просто признаком плохого стиля; исключением являются переменные, подлежащие инициализации посредством ввода, и пременные векторного или структурного типа, которые нельзя удобно инициализировать одними присваиваниями*. Заметьте, что = является операцией присваивания, а == операцией сравнения.

– * В языке немного лучше этого с этими исключениями тоже надо бы справляться. (прим. автора)

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

double prim() // обрабатывает primary (первичные) (* switch (curr_tok) (* case NUMBER: // константа с плавающей точкой get_token(); return number_value; case NAME: if (get_token() == ASSIGN) (* name* n = insert(name_string); get_token(); n-»value = expr(); return n-»value; *) return look(name-string)-»value; case MINUS: // унарный минус get_token(); return -prim(); case LP: get_token(); double e = expr(); if (curr_tok != RP) return error(«должна быть )»); get_token(); return e; case END: return 1; default:

return error(«должно быть primary»); *) *)

При обнаружении NUMBER (то есть, константы с плавающей точкой), возвращается его значение. Функция ввода get_token() помещает значение в глобальную переменную number_value. Ипользование в программе глобальных переменных часто указывает на то, что структура не совсем прозрачна, что применялась нкоторого рода оптимизация. Здесь дело обстоит именно так. Торетически лексический символ обычно состоит из двух частей: значения, определяющего вид лексемы (в данной программе token _value), и (если необходимо) значения лексемы. У нас имеется только одна простая переменная curr_tok, поэтому для хранения значения последнего считанного NUMBER понадобилась глобальная переменная переменная number_value. Это работает только потму, что калькулятор при вычислениях использует только одно число перед чтением со входа другого.

Так же, как значение последнего встреченного NUMBER хранится в number_value, в name_string в виде символьной строки хранится представление последнего прочитанного NAME. Перед тем, как что-либо сделать с именем, калькулятор должен заглнуть вперед, чтобы посмотреть, осуществляется ли присваивание ему, или оно просто используется. В обоих случаях надо спрвиться в таблице имен. Сама таблица описывается в #3.1.3; здесь надо знать только, что она состоит из элементов вида:

srtuct name (* char* string; char* next; double value; *)

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

name* look(char*); name* insert(char*);

Обе возвращают указатель на name, соответствующее парметру – символьной строке; look() выражает недовольство, если имя не было определено. Это значит, что в калькуляторе можно использовать имя без предварительного описания, но первый раз оно должно использоваться в левой части присваивания.




3.1.2 Функция ввода

Чтение ввода – часто самая запутанная часть программы. Причина в том, что если программа должна общаться с человком, то она должна справляться с его причудами, условностями и внешне случайными ошибками. Попытки заставить человека вети себя более удобным для машины образом часто (и справедлво) рассматриваются как оскорбительные. Задача низкоуровневой программы ввода состоит в том, чтобы читать символы по одному и составлять из них лексические символы более высокого уроня. Далее эти лексемы служат вводом для программ более выского уровня. У нас ввод низкого уровня осуществляется get_token(). Обнадеживает то, что написание программ ввода низкого уровня не является ежедневной работой; в хорошей ситеме для этого будут стандартные функции.

Для калькулятора правила сознательно были выбраны такми, чтобы функциям по работе с потоками было неудобно эти правила обрабатывать; незначительные изменения в определении лексем сделали бы get_token() обманчиво простой. Первая сложность состоит в том, что символ новой строки

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

char ch

do (* // пропускает пропуски за исключением '\n' if(!cin.get(ch)) return curr_tok = END; *) while (ch!='\n' amp; amp; isspace(ch));

Вызов cin.get(ch) считывает один символ из стандартного потока ввода в ch. Проверка if(!cin.get(ch)) не проходит в случае, если из cin нельзя считать ни одного символа. В этом случае возвращается END, чтобы завершить сеанс работы кальклятора. Используется операция ! (НЕ), поскольку get() возврщает в случае успеха ненулевое значение.

Функция (inline) isspace() из «ctype.h» обеспечивает стандартную проверку на то, является ли символ пропуском (#8.4.1); isspace(c) возвращает ненулевое значение, если c является символом пропуска, и ноль в противном случае. Прверка реализуется в виде поиска в таблице, поэтому использвание isspace() намного быстрее, чем проверка на отдельные символы пропуска; это же относится и к функциям isalpha(), isdigit() и isalnum(), которые используются в get_token().

После того, как пустое место пропущено, следующий символ используется для определения того, какого вида какого вида лексема приходит. Давайте сначала рассмотрим некоторые случаи отдельно, прежде чем приводить всю функцию. Ограничители лесем '\n' и ';' обрабатываются так:

switch (ch) (* case ';': case '\n': cin »» WS; // пропустить пропуск return curr_tok=PRINT;

Пропуск пустого места делать необязательно, но он позвляет избежать повторных обращений к get_token(). WS – это стандартный пропусковый объект, описанный в «stream.h»; он используется только для сброса пропуска. Ошибка во вводе или конец ввода не будут обнаружены до следующего обращения к get _token(). Обратите внимание на то, как можно использовать несколько меток case (случаев) для одной и той же последовтельности операторов, обрабатывающих эти случаи. В обоих случаях возвращается лексема PRINT и помещается в curr_tok.

Числа обрабатываются так:

case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin »» number_value; return curr_tok=NUMBER;

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

Поскольку операция »» определена также и для чтения констант с плавающей точкой в double, программирование этого не составляет труда: сперва начальный символ (цифра или точка) помещается обратно в cin, а затем можно считывать контанту в number_value.

Имя, то есть лексема NAME, определяется как буква, за которой возможно следует несколько букв или цифр:

if (isalpha(ch)) (* char* p = name_string; *p++ = ch; while (cin.get(ch) amp; amp; isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok=NAME; *)

Эта часть строит в name_string строку, заканчивающуюся нулем. Функции isalpha() и isalnum() заданы в «ctype.h»; isalnum(c) не ноль, если c буква или цифра, ноль в противном случае.

Вот, наконец, функция ввода полностью:

token_value get_token() (* char ch;

do (* // пропускает пропуски за исключением '\n' if(!cin.get(ch)) return curr_tok = END; *) while (ch!='\n' amp; amp; isspace(ch));

switch (ch) (* case ';': case '\n': cin »» WS; // пропустить пропуск return curr_tok=PRINT; case '*': case '/': case '+': case '-': case '(': case ')': case '=': return curr_tok=ch; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin »» number_value; return curr_tok=NUMBER; default: // NAME, NAME= или ошибка if (isalpha(ch)) (* char* p = name_string; *p++ = ch; while (cin.get(ch) amp; amp; isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok=NAME; *) error(«плохая лексема»); return curr_tok=PRINT; *) *)

Поскольку token_value (значение лексемы) операции было определено как целое значение этой операции*, обработка всех операций тривиальна.


– * знака этой операции. (прим. перев.)




3.1.3 Таблица имен

К таблице имен доступ осуществляется с помощью одной функции

name* look(char* p, int ins =0);

Ее второй параметр указывает, нужно ли сначала поместить строку символов в таблицу. Инициализатор =0 задает параметр, который надлежит использовать по умолчанию, когда look() взывается с одним параметром. Это дает удобство записи, когда look(«sqrt2») означает look(«sqrt2»,0), то есть просмотр, без помещения в таблицу. Чтобы получить такое же удобство записи для помещения в таблицу, определяется вторая функция:

inline name* insert(char* s) (* return look(s,1);*)

Как уже отмечалось раньше, элементы этой таблицы имеют тип:

srtuct name (* char* string; char* next; double value; *)

Член next используется только для сцепления вместе имен в таблице.

Сама таблица – это просто вектор указателей на объекты типа name:

const TBLSZ = 23; name* table[TBLSZ];

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

Для нахождения элемента в таблице в look() принимается простой алгоритм хэширования (имена с одним и тем же хэш-кдом зацепляются вместе):

int ii = 0; // хэширование char* pp = p; while (*pp) ii = ii««1 ^ *pp++; if (ii « 0) ii = -ii; ii %= TBLSZ;

То есть, с помощью исключающего ИЛИ каждый символ во входной строке «добавляется» к ii («сумме» предыдущих символов). Бит в x^y устанавливается единичным тогда и только тода, когда соответствующие биты в x и y различны. Перед примнением в символе исключающего ИЛИ, ii сдвигается на один бит влево, чтобы не использовать в слове только один байт. Это можно было написать и так:

ii ««= 1; ii ^= *pp++;

Кстати, применение ^ лучше и быстрее, чем +. Сдвиг важен для получения приемлемого хэш-кода в обоих случаях. Операторы

if (ii « 0) ii = -ii; ii %= TBLSZ;


обеспечивают, что ii будет лежать в диапазоне 0...TBLS1; % – это операция взятия по модулю (еще называемая получнием остатка).

Вот функция полностью:

extern int strlen(const char*); extern int strcmp(const char*, const char*); extern int strcpy(const char*, const char*);

name* look(char* p, int ins =0) (* int ii = 0; // хэширование char* pp = p; while (*pp) ii = ii««1 ^ *pp++; if (ii « 0) ii = -ii; ii %= TBLSZ;

for (name* n=table[ii]; n; n=n-»next) // поиск if (strcmp(p,n-»string) == 0) return n;

if (ins == 0) error(«имя не найдено»);

name* nn = new name; // вставка nn-»string = new char[strlen(p)+1]; strcpy(nn-»string,p); nn-»value = 1; nn-»next = table[ii]; table[ii] = nn; return nn; *)

После вычисления хэш-кода ii имя находится простым промотром через поля next. Проверка каждого name осуществляется с помощью стандартной функции strcmp(). Если строка найдена, возвращается ее name, иначе добавляется новое name.

Добавление нового name включает в себя создание нового объекта в свободной памяти с помощью операции new (см. #3.2.6), его инициализацию, и добавление его к списку имен. Последнее осуществляется просто путем помещения нового имени в голову списка, поскольку это можно делать даже не проверяя, имеется список, или нет. Символьную строку для имени тоже нужно сохранить в свободной памяти. Функция strlen() исползуется для определения того, сколько памяти нужно, new – для выделения этой памяти, и strcpy() – для копирования строки в память.




3.1.4 Обработка ошибок

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

int no_of_errors;

double error(char* s) (* cerr «„ "error: " «« s «« «\n“; no_of_errors++; return 1; *)

Возвращается значение потому, что ошибки обычно встречаются в середине вычисления выражения, и поэтому надо либо полностью прекращать вычисление, либо возвращать значение, которое по всей видимости не должно вызвать последующих ошибок. Для простого калькулятора больше подходит последнее. Если бы get_token() отслеживала номера строк, то error() мола бы сообщать пользователю, где приблизительно обнаружена ошибка. Это наверняка было бы полезно, если бы калькулятор использовался неитерактивно.

Часто бывает так, что после появления ошибки программа должна завершиться, поскольку нет никакого разумного пути продолжить работу. Это можно сделать с помощью вызова exit(), которая очищает все вроде потоков вывода (#8.3.2), а затем завершает программу используя свой параметр в качестве ее возвращаемого значения. Более радикальный способ завершения программы – это вызов abort(), которая обрывает выполнение сразу же или сразу после сохранения где-то информации для оладчика (дамп памяти); о подробностях справьтесь, пожалуйста, в вашем руководстве.




3.1.5 Драйвер

Когда все части программы на месте, нам нужен только драйвер для инициализации и всего того, что связано с запуком. В этом простом примере main() может работать так:

int main() (* // вставить предопределенные имена: insert(«pi»)-»value = 3.1415926535897932385; insert("e")-»value = 2.7182818284590452354;

while (cin) (* get_token(); if (curr_tok == END) break; if (curr_tok == PRINT) continue; cout «„ expr() «« «\n“; *) return no_of_errors; *)

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

Основная работа цикла – читать выражения и писать ответ. Это делает строка:

cout «„ expr() «« «\n“;

Проверка cin на каждом проходе цикла обеспечивает завешение программы в случае, если с потоком ввода что-то не так, а проверка на END обеспечивает корректный выход из цикла, когда get_token() встречает конец файла. Оператор break осуществляет выход из ближайшего содержащего его оператора switch или оператора цикла (то есть, оператора for, оператора while или оператора do). Проверка на PRINT (то есть, на '\n' или ';') освобождает expr() от обязанности обрабатывать путые выражения. Оператор continue равносилен переходу к самому концу цикла, поэтому в данном случае

while (cin) (* // ... if (curr_tok == PRINT) continue; cout «„ expr() «« «\n“; *)

эквивалентно

while (cin) (* // ... if (curr_tok == PRINT) goto end_of_loop; cout «„ expr() «« «\n“; end_of_loop *)

Более подробно циклы описываются в #с.9.




3.1.6 Параметры командной строки

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

Как уже говорилось, программа запускается вызовом main(). Когда это происходит, main() получает два параметра указывающий число параметров, обычно называемый argc и вектор параметров, обычно называемый argv. Параметры – это символные строки, поэтому argv имеет тип char*[argc]. Имя программы (так, как оно стоит в командной строке) передается в качестве argv[0], поэтому argc всегда не меньше единицы. Например, в случае команды

dc 150/1.1934

параметры имеют значения:

argc 2 argv[0] «dc» argv[1] «150/1.1934»

Научиться пользоваться параметрами командной строки неложно. Сложность состоит в том, как использовать их без препрограммирования. В данном случае это оказывается совсем просто, поскольку поток ввода можно связать с символьной строкой, а не с файлом (#8.5). Например, можно заставить cin читать символы из стандартного ввода:

int main(int argc, char* argv[]) (* switch(argc) (* case 1: // читать из стандартного ввода break; case 2: // читать параметр строку cin = *new istream(strlen(argv[1]),argv[1]); break; default: error(«слишком много параметров»); return 1; *) // как раньше *)

Программа осталась без изменений, за исключением добаления в main() параметров и использования этих параметров в

операторе switch. Можно было бы легко модифицировать main() так, чтобы она получала несколько параметров командной стрки, но это оказывается ненужным, особенно потому, что неколько выражений можно передавать как один параметр: dc «rate=1.1934;150/rate;19.75/rate;217/rate»

Здесь кавычки необходимы, поскольку ; является разделтелем команд в системе UNIX.




3.2 Краткая сводка операций

Операции С++ подробно и систематически описываются в #с. 7; прочитайте, пожалуйста, этот раздел. Здесь же приводится операция краткая сводка и некоторые примеры. После каждой операции приведено одно или более ее общеупотребительных наваний и пример ее использования. В этих примерах имя_класса – это имя класса, член – имя члена, объект – выражение, дающее в результате объект класса, указатель – выражение, дающее в результате указатель, выр – выражение, а lvalue – выражение, денотирующее неконстантный объект. Тип может быть совершенно произвольным именем типа (со * () и т.п.) только когда он стоит в скобках, во всех остальных случаях существуют огранчения.

Унарные операции и операции присваивания правоассоцитивны, все остальные левоассоциативны. Это значит, что a=b=c означает a=(b=c), a+b+c означает (a+b)+c, и *p++ означает *(p ++), а не (*p)++.

В каждой очерченной части находятся операции с одинаквым приоритетом. Операция имеет приоритет больше, чем оперции из частей, расположенных ниже. Например: a+b*c означает a +(b*c), так как * имеет приоритет выше, чем +, а a+b-c ознчает (a+b)-c, поскольку + и – имеют одинаковый приоритет (и поскольку + левоассоциативен).

3.2.1 Круглые скобки

Скобками синтаксис С++ злоупотребляет; количество спосбов их использования приводит в замешательство: они применются для заключения в них параметров в вызовах функций, в них заключается тип в преобразовании типа (приведении к типу), в именах типов для обозначения функций, а также для разрешения конфликтов приоритетов. К счастью, последнее требуется не слишком часто, потому что уровни приоритета и правила ассоцативности определены таким образом, чтобы выражения «работали ожидаемым образом» (то есть, отражали наиболее привычный спсоб употребления). Например, значение

if (i«=0 !! max«i) // ...

очевидно. Тем не менее, всегда, когда программист сомнвается относительно этих правил, следует употреблять скобки, и некоторые программисты предпочитают немного более длинное и менее элегантное

if ( (i«=0) !! (max«i) ) // ...

При усложнении подвыражений употребление скобок станвится более обычным явлением, но сложные подвыражения являюся источником ошибок, поэтому если вы чувствуете потребность в скобках, попробуйте оборвать выражение и использовать дополнительную переменную. Есть и такие случаи, когда приоритты операций не приводят к «очевидному» результату. Например в

if (i amp;mask == 0) // ...

не происходит применения маски mask к i и последующей проверки результата на ноль. Поскольку == имеет приоритет вше, чем amp;, выражение интерпретируется как i amp;(mask==0). В этом случае скобки оказываются важны:

if ((i amp;mask) == 0) // ...

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

if (0 «= a «= 99) // ...

Оно допустимо, но интерпретируется оно как (0«=a)«=99, где результат первого подвыражения или 0 или 1, но не a (если только a не равно 1). Чтобы проверить, лежит ли a в диапазоне 0...99, можно написать

if (0«=a amp; amp; a«=99) // ...




3.2.2 Порядок вычисления

Порядок вычисления подвыражений в выражении неопределен. Например

int i = 1; v[i] = i++;

может вычисляться или как v[1]=1, или как v[2]=1. При отсутствии ограничений на порядок вычисления выражения может генерироваться более хороший код. Было бы замечательно, если бы компилятор предупреждал о подобных неоднозначностях, но большинство компиляторов этого не делают.

Относительно операций amp; amp; и !! гарантируется, что их левый операнд вычисляется раньше, чем правый. Например, b=(a=2,a=1) присвоит b 3.В #3.3.1приводятся примеры использования amp; amp; и !!. Заметьте, что операция следования , (запятая) логически отличается от запятой, которая используется для разделения параметров в вызове функции. Рассмотрим

f1(v[i],i++); // два параметра f2( (v[i],i++) ) // один параметр

В вызове f1 два параметра, v[i] и i++, и порядок вычиления выражений-параметров неопределен. Зависимость выражения -параметра от порядка вычисления – это очень плохой стиль, а также непереносимо. В вызове f2 один параметр, выражение с запятой, которое эквивалентно i++.

С помощью скобок нельзя задать порядок вычисления. Например, a*(b/c) может вычисляться и как (a*b)/c, поскольку * и / имеют одинаковый приоритет. В тех случаях, когда важен прядок вычисления, можно вводить дополнительную переменную, например, (t=b/c,a*t).



3.2.3 Увеличение и уменьшение*

– * Следовало бы переводить как «инкремент» и «декремент», однако мы следовали терминологии, принятой в переводной литратуре по C, поскольку эти операции унаследованы от C. (прим.

перев.)

Операция ++ используется для явного выражения приращения вместо его неявного выражения с помощью комбинации сложения и присваивания. По определению ++lvalue означает lvalue+=1, что в свою очередь означает lvalue=lvalue+1 при условии, что lvalue не вызывает никаких побочных эффектов. Выражение, обозначающее (денотирующее) объект, который должен быть увличен, вычисляется один раз (только). Аналогично, уменьшение выражается операцией –. Операции ++ и – могут применяться и как префиксные, и как постфиксные. Значением ++x является нвое (то есть увеличенное) значение x. Например, y=++x эквивлентно y=(x+=1). Значение x++, напротив, есть старое значение x. Например, y=x++ эквивалентно y=(t=x,x+=1,t), где t – перменная того же типа, что и x.

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

inline void cpy(char* p, const char* q) (* while (*p++ = *q++) ; *)

Напомню, что увеличение и уменьшение арифметических указателей, так же как сложение и вычитание указателей, осуществляется в терминах элементов вектора, на которые указывает указатель p++ приводит к тому, что p указывает на следующий элемент. Для указателя p типа T* по определению выполняется следующее:

long(p+1) == long(p)+sizeof(T);




3.2.4 Побитовые логические операции

Побитовые логические операции

amp; ! ^ ~ »» ««

применяются к целым, то есть к объектам типа char, short, int, long и их unsigned аналогам, результаты тоже цлые.

Одно из стандартных применений побитовых логических опраций – реализация маленького множества (вектор битов). В этом случае каждый бит беззнакового целого представляет один член множества, а число членов ограничено числом битов. Бнарная операция amp; интерпретируется как пересечение, ! как объединение, а ^ как разность. Для наименования членов такого множества можно использовать перечисление. Вот маленький прмер, заимствованный из реализации (не пользовательского итерфейса) «stream.h»:

enum state_value (* _good=0, _eof=1, _fail=2, _bad=4 *); // хорошо, конец файла, ошибка, плохо

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

cout.state = _good;

Например, так можно проверить, не был ли испорчен поток или допущена операционная ошибка:


if (cout.state amp;(_bad!_fail)) // не good

Еще одни скобки необходимы, поскольку amp; имеет более всокий приоритет, чем !.

Функция, достигающая конца ввода, может сообщать об этом так:

cin.state != _eof;

Операция != используется потому, что поток уже может быть испорчен (то есть, state==_bad), поэтому

cin.state = _eof;

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

state_value diff = cin.state^cout.state;

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

Следует заметить, что использование полей (#2.5.1) в действительности является сокращенной записью сдвига и маскрования для извлечения полей бит из слова. Это, конечно, моно сделать и с помощью побитовых логических операций, Например, извлечь средние 16 бит из 32-битового int можно следующим образом:

unsigned short middle(int a) (* return (a»»8) amp;0xffff; *)

Не путайте побитовые логические операции с логическими операциями:

amp; amp; !! !

Последние возвращают 0 или 1, и они главным образом ипользуются для записи проверки в операторах if, while или for (#3.3.1). Например, !0 (не ноль) есть значение 1, тогда как ~ 0 (дополнение нуля) есть набор битов все-единицы, который обычно является значением -1.




3.2.5 Преобразование типа

Бывает необходимо явно преобразовать значение одного тпа в значение другого. Явное преобразование типа дает значние одного типа для данного значения другого типа. Например:

float r = float(1);

перед присваиванием преобразует целое значение 1 к знчению с плавающей точкой 1.0. Результат преобразования типа не является lvalue, поэтому ему нельзя присваивать (если только тип не является ссылочным типом).

Есть два способа записи явного преобразования типа: трдиционная в C запись приведения к типу (double)a и функцинальная запись double(a). Функциональная запись не может прменяться для типов, которые не имеют простого имени. Например, чтобы преобразовать значение к указательному типу надо или использовать запись преобразования типа

char* p = (char*)0777;


или определить новое имя типа:

typedef char* Pchar; char* p = Pchar(0777);

По моему мнению, функциональная запись в нетривиальных случаях предпочтительна. Рассмотрим два эквивалентных примера

Pname n2 = Pbase(n1-»tp)-»b_name; //функциональная запись Pname n3 = ((Pbase)n2-»tp)-»b_name; // запись приведения // к типу Поскольку операция -» имеет больший приоритет, чем прведение, последнее выражение интерпретируется как

((Pbase)(n2-»tp))-»b_name

С помощью явного преобразования типа к указательным тпам можно симитировать, что объект имеет совершенно проивольный тип. Например:

any_type* p = (any_type*) amp;some_object;

позволит работать посредством p с некоторым объектом some_object как с любым типом any_type.

Когда преобразование типа не необходимо, его следует ибегать. Программы, в которых используется много явных преоразований типов, труднее понимать, чем те, в которых это не делается. Однако такие программы легче понимать, чем программы, просто не использующие типы для представления понятий блее высокого уровня (например, программу, которая оперирует регистром устройства с помощью сдвига и маскирования, вместо того, чтобы определить подходящую struct и оперировать ею, см. #2.5.2). Кроме того, правильность явного преобразования типа часто критическим образом зависит от понимания програмистом того, каким образом объекты различных типов обрабатваются в языке, и очень часто от подробностей реализации. Например:

int i = 1; char* pc = «asdf»; int* pi = amp;i;

i = (int)pc; pc = (char*)i; // остерегайтесь! значение pc может изм//ниться // на некоторых машинах // sizeof(int)«sizeof(char*) pi = (int*)pc; pc = (char*)pi; // остерегайтесь! значение pc может изм// ниться // на некоторых машинах char* // представляется иначе, чем int*

На многих машинах ничего плохого не произойдет, но на других результаты будут катастрофическими. Этот код в лучшем случае непереносим. Обычно можно без риска предполагать, что указатели на различные структуры имеют одинаковое представлние. Кроме того, любой указатель можно (без явного преобразвания типа) присвоить void*, а void* можно явно преобразовать к указателю любого типа.

В С++ явное преобразование типа оказывается ненужным во многих случаях, когда C (и другие языки) требуют его. Во мнгих программах явного преобразования типа можно совсем избжать, а во многих других его применение можно локализовать в

небольшом числе подпрограмм.




3.2.6 Свободная память

Именованный объект является либо статическим, либо автоматическим (см. #2.1.3). Статический объект размещается во время запуска программы и существует в течение всего выполнния программы. Автоматический объект размещается каждый раз при входе в его блок и существует только до тех пор, пока из этого блока не вышли. Однако часто бывает полезно создать нвый объект, существующий до тех пор, пока он не станет больше не нужен. В частности, часто полезно создать объект, который можно использовать после возврата из функции, где он создаеся. Такие объекты создает операция new, а впоследствии унитожать их можно операцией delete. Про объекты, выделенные с помощью операции new, говорят, что они в свободной памяти. Такими объектами обычно являются вершины деревьев или элеметы связанных списков, являющиеся частью большей структуры данных, размер которой не может быть известен на стадии копиляции. Рассмотрим, как можно было бы написать компилятор в духе написанного настольного калькулятора. Функции синтаксческого анализа могут строить древовидное представление выржений, которое будет использоваться при генерации кода. Например:

struct enode (* token_value oper; enode* left; enode* right; *);

enode* expr() (* enode* left = term();

for(;;) switch(curr_tok) (* case PLUS: case MINUS: get_token(); enode* n = new enode; n-»oper = curr_tok; n-»left = left; n-»right = term(); left = n; break; default: return left; *) *)

Получающееся дерево генератор кода может использовать например так:

void generate(enode* n) (* switch (n-»oper) (* case PLUS: // делает нечто соответствующее delete n; *) *)

Объект, созданный с помощью new, существует, пока он не будет явно уничтожен delete, после чего пространство, которое он занимал, опять может использоваться new. Никакого «сборщка мусора», который ищет объекты, на которые нет ссылок, и предоставляет их в распоряжение new, нет. Операция delete может применяться только к указателю, который был возвращен операцией new, или к нулю. Применение delete к нулю не вызвает никаких действий.

С помощью new можно также создавать вектора объектов. Например:

char* save_string(char* p) (* char* s = new char[strlen(p)+1]; strcpy(s,p); return s; *)

Следует заметить, что чтобы освободить пространство, вделенное new, delete должна иметь возможность определить размер выделенного объекта. Например:

int main(int argc, char* argv[]) (* if (argc « 2) exit(1); char* p = save_string(argv[1]); delete p; *)

Это приводит к тому, что объект, выделенный стандартной реализацией new, будет занимать больше места, чем статический объект (обычно, больше на одно слово).

Можно также явно указывать размер вектора в операции уничтожения delete. Например:

int main(int argc, char* argv[]) (* if (argc « 2) exit(1); int size = strlen(argv[1])+1; char* p = save_string(argv[1]); delete[size] p; *)

Заданный пользователем размер вектора игнорируется за исключением некоторых типов, определяемых пользователем (#5.5.5).

Операции свободной памяти реализуются функциями (#с.7.2.3):

void operator new(long); void operator delete(void*);

Стандартная реализация new не инициализирует возвращамый объект.

Что происходит, когда new не находит памяти для выделния? Поскольку даже виртуальная память конечна, это иногда должно происходить. Запрос вроде

char* p = new char[100000000];

как правило, приводит к каким-то неприятностям. Когда у new ничего не получается, она вызывает функцию, указываемую указателем _new_handler (указатели на функции обсуждаются в # 4.6.9). Вы можете задать указатель явно или использовать функцию set_new_handler(). Например:

#include «stream.h»

void out_of_store()

(* cerr «„ «операция new не прошла: за пределами памяти\n“; exit(1); *)

typedef void (*PF)(); // тип указатель на функцию

extern PF set_new_handler(PF);

main() (* set_new_handler(out_of_store); char* p = new char[100000000]; cout «„ "сделано, p = " «« long(p) «« «\n“; *)

как правило, не будет писать «сделано», а будет вместо этого выдавать

операция new не прошла: за пределами памяти

Функция _new_handler может делать и кое-что поумней, чем просто завершать выполнение программы. Если вы знаете, как работают new и delete, например, потому, что вы задали свои собственные operator new() и operator delete(), программа оработки может попытаться найти некоторое количество памяти, которое возвратит new. Другими словами, пользователь может сделать сборщик мусора, сделав, таким образом, использование delete необязательным. Но это, конечно, все-таки задача не для начинающего.

По историческим причинам new просто возвращает указатель 0, если она не может найти достаточное количество памяти и не был задан никакой _new_handler. Например

include «stream.h»

main() (* char* p = new char[100000000]; cout «„ "сделано, p = " «« long(p) «« «\n“; *)

выдаст

сделано, p = 0

Вам сделали предупреждение! Заметьте, что тот, кто задет _new_handler, берет на себя заботу по проверке истощения памяти при каждом использовании new в программе (за исключнием случая, когда пользователь задал отдельные подпрограммы для размещения объектов заданных типов, определяемых пользвателем, см. #5.5.6).




3.3 Сводка операторов

Операторы С++ систематически и полностью изложены в #с.9, прочитайте, пожалуйста, этот раздел. А здесь приводится краткая сводка и некоторые примеры.

Синтаксис оператора – оператор: описание (*список_операторов opt*) выражение opt


if оператор if ( выражение ) оператор if ( выражение ) оператор else оператор switch оператор switch ( выражение ) оператор

while ( выражение ) оператор do оператор while (выражение) for ( оператор выражение opt; выражение opt ) оператор

case константное_выражение : оператор default : оператор break ; continue ;

return выражение opt ;

goto идентификатор ; идентификатор : оператор

список_операторов: оператор оператор список_операторов

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




3.3.1 Проверки

Проверка значения может осуществляться или оператором if, или оператором switch:

if ( выражение ) оператор if ( выражение ) оператор else оператор switch ( выражение ) оператор

В С++ нет отдельного булевского типа. Операции сравнения

== != « „= “ »=

возвращают целое 1, если сравнение истинно, иначе возращают 0. Не так уж непривычно видеть, что ИСТИНА определена как 1, а ЛОЖЬ определена как 0.

В операторе if первый (или единственный) оператор выпоняется в том случае, если выражение ненулевое, иначе выполнется второй оператор (если он задан). Отсюда следует, что в качестве условия может использоваться любое целое выражение. В частности, если a целое, то

if (a) // ...

эквивалентно

if (a != 0) // ...

Логические операции amp; amp; !! ! наиболее часто используются в условиях. Операции amp; amp; и !! не будут вычислять второй аргмент, если это ненужно. Например:

if (p amp; amp; 1«p-»count) // ...

вначале проверяет, является ли p не нулем, и только если это так, то проверяет 1«p-»count.

Некоторые простые операторы if могут быть с удобством

заменены выражениями арифметического if. Например:

if (a «= d) max = b; else max = a;

лучше выражается так:

max = (a«=b) ? b : a;

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

Некоторые простые операторы switch можно по-другому зписать в виде набора операторов if. Например:

switch (val) (* case 1: f(); break; case 2; g(); break; default: h(); break; *)

иначе можно было бы записать так:

if (val == 1) f(); else if (val == 2) g(); else h();

Смысл тот же, однако первый вариант (switch) предпочттельнее, поскольку в этом случае явно выражается сущность действия (сопоставление значения с рядом констант). Поэтому в нетривиальных случаях оператор switch читается легче.

Заботьтесь о том, что switch должен как-то завершаться, если только вы не хотите, чтобы выполнялся следующий case. Например:

switch (val) (* // осторожно case 1: cout «„ „case 1\n“; case 2; cout „« «case 2\n“; default: cout «« «default: case не найден\n“; *)

при val==1 напечатает

case 1 case 2 default: case не найден

к великому изумлению непосвященного. Самый обычный спсоб завершить случай – это break, иногда можно даже использвать goto. Например:

switch (val) (* // осторожно

case 0: cout «„ „case 0\n“; case1: case 1: cout „„ «case 1\n“; return; case 2; cout «« «case 2\n“; goto case1; default: cout «« «default: case не найден\n“; return; *)

При обращении к нему с val==2 выдаст

case 2 case 1

Заметьте, что метка case не подходит как метка для упоребления в операторе goto:

goto case 1; // синтаксическая ошибка




3.3.2 Goto

С++ снабжен имеющим дурную репутацию оператором goto.

goto идентификатор; идентификатор : оператор

В общем, в программировании высокого уровня он имеет очень мало применений, но он может быть очень полезен, когда С++ программа генерируется программой, а не пишется непоредственно человеком. Например, операторы goto можно исползовать в синтаксическом анализаторе, порождаемом генератором синтаксических анализаторов. Оператор goto может быть также важен в тех редких случаях, когда важна наилучшая эффектиность, например, во внутреннем цикле какой-нибудь программы, работающей в реальном времени.

Одно из немногих разумных применений состоит в выходе из вложенного цикла или переключателя (break лишь прекращает вполнение самого внутреннего охватывающего его цикла или преключателя). Например:

for (int i = 0; i«n; i++) for (int j = 0; j«m; j++) if (nm[i][j] == a) goto found // найдено // не найдено // ...

found: // найдено // nm[i][j] == a

Имеется также оператор continue, который по сути делает переход на конец оператора цикла, как объясняется в #3.1.5.




3.4 Комментарии и Выравнивание

Продуманное использование комментариев и согласованное использование отступов может сделать чтение и понимание прораммы намного более приятным. Существует несколько различных стилей согласованного использования отступов. Автор не видит никаких серьезных оснований предпочесть один другому (хотя как и у большинства, у меня есть свои предпочтения). Сказаное относится также и к стилю комментариев.


Неправильное использование комментариев может серьезно повлиять на удобочитаемость программы, Компилятор не понимает содержание комментария, поэтому он никаким способом не может убедиться в том, что комментарий

1. осмыслен,

2. описывает программу и

3. не устарел.

Непонятные, двусмысленные и просто неправильные комметарии содержатся в большинстве программ. Плохой комментарий может быть хуже, чем никакой.

Если что-то можно сформулировать средствами самого язка, следует это сделать, а не просто отметить в комментарии. Данное замечание относится к комментариям вроде:

// переменная "v" должна быть инициализирована.

//переменная"v"должна использоваться только функцией «f()».

// вызвать функцию init() перед вызовом // любой другой функции в этом файле.

// вызовите функцию очистки «cleanup()» в конце вашей // программы.

// не используйте функцию «wierd()».

// функция «f()» получает два параметра.

При правильном использовании С++ подобные комментарии как правило становятся ненужными. Чтобы предыдущие комментрии стали излишними, можно, например, использовать правила компоновки (#4.2) и видимость, инициализацию и правила очиски для классов (см. #5.5.2).

Если что-то было ясно сформулировано на языке, второй раз упоминать это в комментарии не следует. Например:

a = b+c; // a становится b+c count++; // увеличить счетчик

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

Автор предпочитает:

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

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

3. Небольшое число комментариев в тех местах, где прорамма неочевидна и/или непереносима и

4. Очень мало что еще.


Например:

// tbl.c: Реализация таблицы имен /* Гауссовское исключение с частичным См. Ralston: «A first course ...» стр. 411. */

// swap() предполагает размещение стека AT amp;T sB20.

/**************************************

Copyright (c) 1984 AT amp;T, Inc. All rights reserved

****************************************/

Удачно подобранные и хорошо написанные комментарии – сщественная часть программы. Написание хороших комментариев может быть столь же сложным, сколь и написание самой програмы.

Заметьте также, что если в функции используются исключтельно комментарии //, то любую часть этой функции можно зкомментировать с помощью комментариев /* */, и наоборот.




3.5 Упражнения

1. (*1) Перепишите следующий оператор for в виде эквивалентного оператора while: for (i=0; i«max_length; i++) if (input_line[i] == '?') quest_count++;

2. (*1) Полностью расставьте скобки в следующих выражниях: a = b + c * d «« 2 amp; 8 a amp; 077 != 3 a == b !! a == c amp; amp; c « 5 c = x != 0 0 «= i « 7 f(1,2)+3 a = -1 + + b – – 5 a = b == c ++ a = b = c = 0 a[4][2] *= * b ? c : * d * 2 a-b,c=d

3. (*2) Найдите пять различных конструкций С++, значение которых неопределено.

4. (*2) Найдите десять различных примеров непереносимой С++ программы.

5. (*1) Что происходит в вашей системе, если вы делите на ноль? Что происходит при переполнении и потере значимости?

6. (*1) Полностью расставьте скобки в следующих выражниях: *p++ *–p ++a– (int*)p-»m *p.m *a[i]

7. (*2) Напишите функции: strlen(), которая возвращает длину строки, strcpy(), которая копирует одну строку в дргую, и strcmp(), которая сравнивает две строки. Разберитесь, какие должны быть типы параметров и типы возвращаемых значний, а потом сравните их со стандартными версиями, которые описаны в «string.h» и в вашем руководстве.

8. (*1) Посмотрите, как ваш компилятор реагирует на ошибки: a := b+1; if (a = 3) // ... if (a amp;077 == 0) // Придумайте ошибки попроще, и посмотрите, как компилятор на них реагирует.

9. (*2) Напишите функцию cat(), получающую два строковых параметра и возвращающую строку, которая является конкатенцией параметров. Используйте new, чтобы найти память для рзультата. Напишите функцию rev(), которая получает строку и переставляет в ней символы в обратном порядке. То есть, после вызова rev(p) последний символ p становится первым.

10. (*2) Что делает следующая программа?

void send(register* to, register* from, register count) // Полезные комментарии несомненно уничтожены. (* register n=(count+7)/8; switch (count%8) (* case 0: do (* *to++ = *from++; case 7: do (* *to++ = *from++; case 6: do (* *to++ = *from++; case 5: do (* *to++ = *from++; case 4: do (* *to++ = *from++; case 3: do (* *to++ = *from++; case 2: do (* *to++ = *from++; case 1: do (* *to++ = *from++; while (–n»0); *) *) Зачем кто-то мог написать нечто похожее?

11. (*2) Напишите функцию atoi(), которая получает стрку, содержащую цифры, и возвращает соответствующее int. Наример, atoi(«123») – это 123. Модифицируйте atoi() так, чтобы помимо обычной десятичной она обрабатывала еще восьмеричную и шестнадцатиричную записи С++. Модифицируйте atoi() так, чтобы обрабатывать запись символьной константы. Напишите функцию itoa(), которая строит представление целого параметра в виде строки.

12. (*2) Перепишите get_token() (#3.1.2), чтобы она за один раз читала строку в буфер, а затем составляла лексемы, читая символы из буфера.

13. (*2) Добавьте в настольный калькулятор из #3.1 такие функции, как sqrt(), log() и sin(). Подсказка: предопределите имена и вызывайте функции с помощью вектора указателей на функции. Не забывайте проверять параметры в вызове функции.

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

15. (*1.5) Преобразуйте настольный калькулятор так, чтбы вместо статических переменных name_string и number_value использовалась структура символа symbol: struct symbol (* token_value tok; union (* double number_value; char* name_string; *); *);

16. (*2.5) Напишите программу, которая выбрасывает коментарии из С++ программы. То есть, читает из cin, удаляет // и /* */ комментарии и пишет результат в cout. Не заботьтесь о приятном виде выходного текста (это могло бы быть другим, блее сложным упражнением). Не беспокойтесь о правильности программ. Остерегайтесь // и /* и */ внутри комментариев, строк и символьных констант.

17. (*2) Посмотрите какие-нибудь программы, чтобы понять принцип различных стилей комментирования и выравнивания, кторые используются на практике.




Глава 4 Функции и Файлы

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



4.1 Введение

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

Рассмотрим пример с калькулятором. Он был представлен в виде одного исходного файла. Если вы его набили, то у вас нверняка были небольшие трудности с расположением описаний в правильном порядке, и пришлось использовать по меньшей мере одно «фальшивое» описание, чтобы компилятор смог обработать взаимно рекурсивные функции expr(), term() и prim(). В тексте уже отмечалось, что программа состоит из четырех частей (лесического анализатора, программы синтаксического разбора, таблицы имен и драйвера), но это никак не было отражено в тексте самой программы. По сути дела, калькулятор был написан по-другому. Так это не делается; даже если в этой программе «на выброс» пренебречь всеми соображениями методологии прораммирования, эксплуатации и эффективности компиляции, автор все равно разобьет эту программу в 200 строк на несколько файлов, чтобы программировать было приятнее.

Программа, состоящая из нескольких раздельно компилирумых файлов, должна быть согласованной в смысле использования имен и типов, точно так же, как и программа, состоящая из оного исходного файла. В принципе, это может обеспечить и копоновщик*. Компоновщик – это программа, стыкующая отдельно скомпилированные части вместе. Компоновщик часто (путая) нзывают загрузчиком. В UNIX'е компоновщик называется ld. Однко компоновщики, имеющиеся в большинстве систем, обеспечивают очень слабую поддержку проверки согласованности.

– * или линкер. (прим. перев.)

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

находятся в отдельно компилируемых частях. Средства, которые это обеспечивают, в вашей системе будут. С++ разработан так, чтобы способствовать такой явной компоновке*.

– * C разработан так, чтобы в большинстве случаев позвлять осуществлять неявную компоновку. Применение C, однако, возросло неимоверно, поэтому случаи, когда можно использовать неявную линковку, сейчас составляют незначительное меньшинтво. (прим. автора)




4.2 Компоновка

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

// file1.c: int a = 1; int f() (* /* что-то делает */ *)

// file2.c: extern int a; int f(); void g() (* a = f(); *)

a и f(), используемые g() в файле file2.c,– те же, что определены в файле file1.c. Ключевое слово extern (внешнее) указывает, что описание a в file2.c является (только) описнием, а не определением. Если бы a инициализировалось, extern было бы просто проигнорировано, поскольку описание с иницилизацией всегда является определением. Объект в программе должен определяться только один раз. Описываться он может много раз, но типы должны точно согласовываться. Например:

// file1.c: int a = 1; int b = 1; extern int c;

// file2.c: int a; extern double b; extern int c;

Здесь три ошибки: a определено дважды (int a; является определением, которое означает int a=0;), b описано дважды с разными типами, а c описано дважды, но не определено. Эти вды ошибок не могут быть обнаружены компилятором, который за один раз видит только один файл. Компоновщик, однако, их онаруживает.

Следующая программа не является С++ программой (хотя C программой является):

// file1.c: int a; int f() (* return a; *)

// file2.c: int a; int g() (* return f(); *)

Во-первых, file2.c не С++, потому что f() не была описана, и поэтому компилятор будет недоволен. Во-вторых, (когда file2.c фиксирован) программа не будет скомпонована, посколку a определено дважды.

Имя можно сделать локальным в файле, описав его static. Например:

// file1.c: static int a = 6; static int f() (* /* ... */ *)

// file2.c: static int a = 7; static int f() (* /* ... */ *)

Поскольку каждое a и f описано как static, получающаяся в результате программа является правильной. В каждом файле своя a и своя f().

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

Рассмотрим два файла:

// file1.c: const int a = 6; inline int f() (* /* ... */ *) struct s (* int a,b; *)

// file1.c: const int a = 7; inline int f() (* /* ... */ *) struct s (* int a,b; *)

Раз правило «ровно одно определение» применяется к контантам, inline-функциям и определениям функций так же, как оно применяется к функциям и переменным, то file1.c и file2.c не могут быть частями одной С++ программы. Но если это так, то как же два файла могут использовать одни и те же типы и константы? Коротко, ответ таков: типы, константы и т.п. могут определяться столько раз, сколько нужно, при условии, что они определяются одинаково. Полный ответ несколько более сложен (это объясняется в следующем разделе).




4.3 Заголовочные Файлы

Типы во всех описаниях одного и того же объекта должны быть согласованными. Один из способов это достичь мог бы сотоять в обеспечении средств проверки типов в компоновщике, но большинство компоновщиков – образца 1950-х, и их нельзя измнить по практическим соображениям*. Другой подход состоит в обеспечении того, что исходный текст, как он передается на рассмотрение компилятору, или согласован, или содержит инфомацию, которая позволяет компилятору обнаружить несогласованости. Один несовершенный, но простой способ достичь согласванности состоит во включении заголовочных файлов, содержащих интерфейсную информацию, в исходные файлы, в которых содежится исполняемый код и/или определения данных.

– * Легко изменить один компоновщик, но сделав это и напсав программу, которая зависит от усовершенствований, как вы будете переносить эту программу в другое место? (прим. автра)

Механизм включения с помощью #include – это чрезвычайно простое средство обработки текста для сборки кусков исходной программы в одну единицу (файл) для ее компиляции. Директива

#include «to_be_included»

замещает строку, в которой встретилось #include, содежимым файла «to_be_included». Его содержимым должен быть иходный текст на С++, поскольку дальше его будет читать комплятор. Часто включение обрабатывается отдельной программой, называемой C препроцессором, которую команда CC вызывает для преобразования исходного файла, который дал программист, в файл без директив включения перед тем, как начать собственно компиляцию. В другом варианте эти директивы обрабатывает итерфейсная система компилятора по мере того, как они встречются в исходном тексте. Если программист хочет посмотреть на результат директив включения, можно воспользоваться командой

CC -E file.c

для препроцессирования файла file.c точно также, как это сделала бы CC перед запуском собственно компилятора. Для включения файлов из стандартной директории включения вместо кавычек используются угловые скобки « и ». Например:

#include «stream.h» //из стандартной директории включения #define «myheader.h» // из текущей директории

Использование «» имеет то преимущество, что в программу фактическое имя директории включения не встраивается (как правило, сначала просматривается /usr/include/CC, а потом usr /include). К сожалению, пробелы в директиве include сущесвенны:

#include « stream.h » // не найдет «stream.h»

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

Следующее эмпирическое правило относительно того, что следует, а что не следует помещать в заголовочные файлы, яляется не требованием языка, а просто предложением по разуному использованию аппарата #include.

В заголовочном файле могут содержаться:

Определения типов struct point (* int x, y; *) Описания функций extern int strlen(const char*); Определения inline-функ-й inline char get()(*return *p++;*) Описания данных extern int a; Определения констант const float pi = 3.141593 Перечисления enum bool (* false, true *); Директивы include #include «signal.h» Определения макросов #define Case break;case Комментарии /* проверка на конец файла */

но никогда

Определения обычных функций char get() (* return *p++; *) Определения данных int a;

Определения сложных константных объектов const tbl[]=(*/* ... */ *)

В системе UNIX принято, что заголовочные файлы имеют суффикс (расширение) .h. Файлы, содержащие определение данных или функций, должны иметь суффикс .c. Такие файлы часто назвают, соответственно, «.h файлы» и «.c файлы». В #4.7 описваются макросы. Следует заметить, что в С++ макросы гораздо менее полезны, чем в C, поскольку С++ имеет такие языковые конструкции, как const для определения констант и inline для исключения расходов на вызов функции.

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




4.3.1 Один Заголовочный Файл

Проще всего решить проблему разбиения программы на неколько файлов поместив функции и определения данных в подхдящее число исходных файлов и описав типы, необходимые для их взаимодействия, в одном заголовочном файле, который включаеся во все остальные файлы. Для программы калькулятора можно использовать четыре .c файла: lex.c, syn.c, table.c и main.c, и заголовочный файл dc.h, содержащий описания всех имен, кторые используются более чем в одном .c файле:

// dc.h: общие описания для калькулятора

enum token_value (* NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' *);

extern int no_of_errors; extern double error(char* s); extern token_value get_token(); extern token_value curr_tok; extern double number_value; extern char name_string[256];

extern double expr(); extern double term(); extern double prim();

struct name (* char* string; name* next; double value; *);

extern name* look(char* p, int ins = 0); inline name* insert(char* s) (* return look(s,1); *)

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

// lex.c: ввод и лексический анализ #include «dc.h»

#include «ctype.h»

token_value curr_tok; double number_value; char name_string[256];

token_value get_token() (* /* ... */ *)

Заметьте, что такое использование заголовочных файлов гарантирует, что каждое описание в заголовочном файле объета, определенного пользователем, будет в какой-то момент включено в файл, где он определяется. Например, при компилции lex.c компилятору будет передано:

extern token_value get_token(); // ... token_value get_token() (* /* ... */ *)

Это обеспечивает то, что компилятор обнаружит любую нсогласованность в типах, указанных для имени. Например, если бы get_token() была описана как возвращающая token_value, но при этом определена как возвращающая int, компиляция lex.c не прошла бы изза ошибки несоответствия типов.

Файл syn.c будет выглядеть примерно так:

// syn.c: синтаксический анализ и вычисление

#include «dc.h»

double prim() (* /* ... */ *) double term() (* /* ... */ *) double expr() (* /* ... */ *)

Файл table.c будет выглядеть примерно так:

// table.c: таблица имен и просмотр

#include «dc.h»

extern char* strcmp(const char*, const char*); extern char* strcpy(char*, const char*); extern int strlen(const char*);

const TBLSZ = 23; name* table[TBLSZ];

name* look(char* p; int ins) (* /* ... */ *)

Заметьте, что table.c сам описывает стандартные функции для работы со строками, поэтому никакой проверки согласованости этих описаний нет. Почти всегда лучше включать заголвочный файл, чем описывать имя в .c файле как extern. При этом может включаться «слишком много», но это обычно не окзывает серьезного влияния на время, необходимое для компилции, и как правило экономит время программиста. В качестве примера этого, обратите внимание на то, как strlen() заново описывается в main() (ниже). Это лишние нажатия клавиш и воможный источник неприятностей, поскольку компилятор не может проверить согласованность этих двух определений. На самом дле, этой сложности можно было бы избежать, будь все описания extern помещены в dc.h, как и предлагалось сделать. Эта «нережность» сохранена в программе, поскольку это очень типично для C программ, очень соблазнительно для программиста, и чаще приводит, чем не приводит, к ошибкам, которые трудно обнаржить, и к программам, с которыми тяжело работать. Вас предуредили!

И main.c, наконец, выглядит так:

// main.c: инициализация, главный цикл и обработка ошибок

#include «dc.h»

int no_of_errors;

double error(char* s) (* /* ... */ *)

extern int strlen(const char*);

main(int argc, char* argv[]) (* /* ... */ *)

Важный случай, когда размер заголовочных файлов станвится серьезной помехой. Набор заголовочных файлов и библиотеку можно использовать для расширения языка множеством общи специальноприкладных типов (см. Главы 5-8). В таких случаях не принято осуществлять чтение тысяч строк заголовоных файлов в начале каждой компиляции. Содержание этих файлов обычно «заморожено» и изменяется очень нечасто. Наиболее плезным может оказаться метод затравки компилятора содержанием этих заголовочных фалов. По сути, создается язык специального назначения со своим собственным компилятором. Никакого стадартного метода создания такого компилятора с затравкой не принято.




4.3.2 Множественные Заголовочные Файлы

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

Рассматривая организацию калькулятора, мы замечаем, что error() используется почти каждой функцией программы, а сама использует только «stream.h». Это обычная для функции ошибок ситуация, поэтому error() следует отделить от main():

// error.h: обработка ошибок

extern int no_errors;

extern double error(char* s);

// error.c

#include «stream.h» #include «error.h»

int no_of_errors;

double error(char* s) (* /* ... */ *)

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

// table.h: описания таблицы имен

struct name (* char* string; name* next; double value; *);

extern name* look(char* p, int ins = 0); inline name* insert(char* s) (* return look(s,1); *)

// table.c: определения таблицы имен

#include «error.h» #include «string.h» #include «table.h»

const TBLSZ = 23; name* table[TBLSZ];

name* look(char* p; int ins) (* /* ... */ *)

Заметьте, что описания функций работы со строками теперь включаются из «string.h». Это исключает еще один возможный источник ошибок.

// lex.h: описания для ввода и лексического анализа

enum token_value (* NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' *);

extern token_value curr_tok; extern double number_value; extern char name_string[256];

extern token_value get_token();

Этот интерфейс лексического анализатора достаточно бепорядочен. Недостаток в надлежащем типе лексемы обнаруживает себя в необходимости давать пользователю get_token() фактческие лексические буферы number_value и name_string.

// lex.c: определения для ввода и лексического анализа

#include «stream.h» #include «ctype.h» #include «error.h» #include «lex.h»

token_value curr_tok; double number_value; char name_string[256];

token_value get_token() (* /* ... */ *)

Интерфейс синтаксического анализатора совершенно прозрчен: // syn.c: описания для синтаксического анализа и вычисления

extern double expr(); extern double term();

extern double prim();

// syn.c: определения для синтаксического анализа и // вычисления

#include «error.h» #include «lex.h» #include «syn.h»

double prim() (* /* ... */ *) double term() (* /* ... */ *) double expr() (* /* ... */ *)

Главная программа, как всегда, тривиальна:

// main.c: главная программа

#include «stream.h» #include «error.h» #include «lex.h» #include «syn.h» #include «table.h» #include «string.h»

main(int argc, char* argv[]) (* /* ... */ *)

Сколько заголовочных файлов использовать в программе, зависит от многих факторов. Многие из этих факторов сильнее связаны с тем, как ваша система работает с заголовочными фалами, нежели с С++. Например, если в вашем редакторе нет средств, позволяющих одновременно видеть несколько файлов, использование большого числа файлов становится менее привлкательным. Аналогично, если открывание и чтение 10 файлов по 50 строк в каждом требует заметно больше времени, чем чтение одного файла в 500 строк, вы можете дважды подумать, прежде чем использовать в небольшом проекте стиль множественных зголовочных файлов. Слово предостережения: набор из десяти зголовочных файлов плюс стандартные заголовочные файлы обычно легче поддаются управлению. С другой стороны, если вы разбили описания в большой программе на логически минимальные по рамеру заголовочные файлы (помещая каждое описание структуры в свой отдельный файл и т.д.), у вас легко может получиться нразбериха из сотен файлов.




4.3.3 Сокрытие Данных

Используя заголовочные файлы пользователь может опредлять явный интерфейс, чтобы обеспечить согласованное исползование типов в программе. С другой стороны, пользователь может обойти интерфейс, задаваемый заголовочным файлом, вводя в .c файлы описания extern.

Заметьте, что такой стиль компоновки не рекомендуется:

// file1.c: // «extern» не используется int a = 7; const c = 8; void f(long) (* /* ... */ *)

// file2.c: // «extern» в .c файле extern int a; extern const c; extern f(int); int g() (* return f(a+c); *)

Поскольку описания extern в file2.c не включаются вместе с определениями в файле file1.c, компилятор не может проверить согласованность этой программы. Следовательно, если только загрузчик не окажется гораздо сообразительнее среднго, две ошибки в этой программе останутся, и их придется икать программисту.

Пользователь может защитить файл от такой недисциплинрованной компоновки, описав имена, которые не предназначены для общего пользования, как static, чтобы их областью видмости был файл, и они были скрыты от остальных частей прораммы. Например:

// table.c: определения таблицы имен

#include «error.h» #include «string.h» #include «table.h»

const TBLSZ = 23; static name* table[TBLSZ];

name* look(char* p; int ins) (* /* ... */ *)

Это гарантирует, что любой доступ к table действительно будет осуществляться именно через look(). «Прятать» константу TBLSZ не обязательно.




4.4 Файлы как Модули

В предыдущем разделе .c и .h файлы вместе определяли часть программы. Файл .h является интерфейсом, который ипользуют другие части программы, .c файл задает реализацию. Такой объект часто называют модулем. Доступными делаются только те имена, которые необходимо знать пользователю, отальные скрыты. Это качество часто называют сокрытием данных, хотя данные – лишь часть того, что может быть скрыто. Модули такого вида обеспечивают большую гибкость. Например, реализция может состоять из одного или более .c файлов, и в виде .h файлов может быть предоставлено несколько интерфейсов. Инфомация, которую пользователю знать не обязательно, искусно скрыта в .c файлах. Если важно, что пользователь не должен точно знать, что содержится в .c файлах, не надо делать их доступными в исходом виде. Достаточно эквивалентных им выхоных файлов компилятора (.o файлов).

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

В других случаях может возникнуть та проблема, что мдуль определяет множество объектов, а не новый тип. Например, модуль table определяет одну таблицу, и если вам нужно две таблицы, то нет простого способа задать вторую таблицу с пмощью понятия модуля. Решение этой проблемы приводится в Глве 5.

Каждый статически размещенный объект по умолчанию иницализируется нулем, программист может задать другие (константные) значения. Это только самый примитивный вид инциализации. К счастью, с помощью классов можно задать код, который выполняется для инициализации перед тем, как модуль какимлибо образом используется, и/или код, который запускаеся для очистки после последнего использования модуля, см. #5.5.2.




4.5 Как Создать Библиотеку

Фразы типа «помещен в библиотеку» и «ищется в какой-то библиотеке» используются часто (и в этой книге, и в других), но что это означает для С++ программы? К сожалению, ответ звисит от того, какая операционная система используется; в этом разделе объясняется, как создать библиотеку в 8-ой весии системы UNIX. Другие системы предоставляют аналогичные возможности.

Библиотека в своей основе является множеством .o файлов, полученных в результате компиляции соответствующего множества .c файлов. Обычно имеется один или более .h файлов, в которых содержатся описания для использования этих .o файлов. В кчестве примера рассмотрим случай, когда нам надо задать (обычным способом) набор математических функций для некоторго неопределенного множества пользователей. Заголовочный файл мог бы выглядеть примерно так:

extern double sqrt(double); // подмножество «math.h» extern double sin(double); extern double cos(double); extern double exp(double); extern double log(double);

а определения этих функций хранились бы, соответственно, в файлах sqrt.c, sin.c, cos.c, exp.c и log.c.

Библиотеку с именем math.h можно создать, например, так:

$ CC -c sqrt.c sin.c cos.c exp.c log.c $ ar cr math.a sqrt.o sin.o cos.o exp.o log.o $ ranlib math.a

Вначале исходные файлы компилируются в эквивалентные им объектные файлы. Затем используется команда ar, чтобы создать архив с именем math.a. И, наконец, этот архив индексируется для ускорения доступа. Если в вашей системе нет ranlib комады, значит она вам, вероятно, не понадобится. Подробности посмотрите, пожалуйста, в вашем руководстве в разделе под зголовком ar. Использовать библиотеку можно, например, так:

$ CC myprog.c math.a

Теперь разберемся, в чем же преимущества использования math.a перед просто непосредственным использованием .o фалов? Например:

$ CC myprog.c sqrt.o sin.o cos.o exp.o log.o

Для большинства программ определить правильный набор .o файлов, несомненно, непросто. В приведенном выше примере они включались все, но если функции в myprog.c вызывают только функции sqrt() и cos(), то кажется, что будет достаточно

$ CC myprog.c sqrt.o cos.o

Но это не так, поскольку cos.c использует sin.c.

Компоновщик, вызываемый командой CC для обработки .a файла (в данном случае, файла math.a) знает, как из того мнжества, которое использовалось для создания .a файла, извлечь только необходимые .o файлы.

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

никогда не видны пользователю), и, кроме того, обеспечить, что в результате в программу будет включено минимальное колчество определений.




4.6 Функции

Обычный способ сделать что-либо в С++ программе – это вызвать функцию, которая это делает. Определение функции яляется способом задать то, как должно делаться некоторое действие. Функция не может быть вызвана, пока она не описана.



4.6.1 Описания Функций

Описание функции задает имя функции, тип возвращаемого функцией значения (если таковое есть) и число и типы парамеров, которые должны быть в вызове функции. Например:

extern double sqrt(double); extern elem* next_elem(); extern char* strcpy(char* to, const char* from); extern void exit(int);

Семантика передачи параметров идентична семантике иницализации. Проверяются типы параметров, и когда нужно произвдится неявное преобразование типа. Например, если были заданы предыдущие определения, то

double sr2 = sqrt(2);

будет правильно обращаться к функции sqrt() со значением с плавающей точкой 2.0. Значение такой проверки типа и преоразования типа огромно.

Описание функции может содержать имена параметров. Это может помочь читателю, но компилятор эти имена просто игноррует.



4.6.2 Определения Функций

Каждая функция, вызываемая в программе, должна быть гдто определена (только один раз). Определение функции – это описание функции, в котором приводится тело функции. Напрмер:

extern void swap(int*, int*); // описание

void swap(int*, int*) // определение (* int t = *p; *p =*q; *q = t; *)

Чтобы избежать расходов на вызов функции, функцию можно описать как inline (#1.12), а чтобы обеспечить более быстрый доступ к параметрам, их можно описать как register (#2.3.11). Оба средства могут использоваться неправильно, и их следует избегать везде где есть какие-либо сомнения в их полезности.




4.6.3 Передача Параметров

Когда вызывается функция, дополнительно выделяется пмять под ее формальные параметры, и каждый формальный парметр инициализируется соответствующим ему фактическим парметром. Семантика передачи параметров идентична семантике инициализации. В частности, тип фактического параметра сопотавляется с типом формального параметра, и выполняются все

стандартные и определенные пользователем преобразования тпов. Есть особые правила для передачи векторов (#4.6.5), средство передавать параметр без проверки типа параметра (#4.6.8) и средство для задания параметров по умолчанию (#4.6.6). Рассмотрим

void f(int val, int amp; ref) (* val++; ref++; *)

Когда вызывается f(), val++ увеличивает локальную копию первого фактического параметра, тогда как ref++ увеличивает второй фактический параметр. Например:

int i = 1; int j = 1; f(i,j);

увеличивает j, но не i. Первый параметр – i, передается по значению, второй параметр – j, передается по ссылке. Как уже отмечалось в #2.3.10, использование функций, которые именяют переданные по ссылке параметры, могут сделать програму трудно читаемой, и их следует избегать (но см. #6.5 и #8.4). Однако передача большого объекта по ссылке может быть гораздо эффективнее, чем передача его по значению. В этом случае параметр можно описать как const, чтобы указать, что ссылка применяется по соображениям эффективности, а также чтобы не позволить вызываемой функции изменять значение обекта:

void f(const large amp; arg) (* // значение «arg» не может быть изменено *)

Аналогично, описание параметра указателя как const соощает читателю, что значение объекта, указываемого указателем, функцией не изменяется. Например:

extern int strlen(const char*); // из «string.h» extern char* strcpy(char* to, const char* from); extern int strcmp(const char*, const char*);

Важность такой практики возрастает с размером программы.

Заметьте, что семантика передачи параметров отлична от семантики присваивания. Это важно для const параметров, сслочных параметров и параметров некоторых типов, определяемых пользователем (#6.6).




4.6.4 Возврат Значения

Из функции, которая не описана как void, можно (и долно) возвращать значение. Возвращаемое значение задается опратором return. Например:

int fac(int n) (*return (n»1) ? n*fac(n-1) : 1; *)

В функции может быть больше одного оператора return: int fac(int n) (* if (n » 1) return n*fac(n-1); else return 1; *)

Как и семантика передачи параметров, семантика возврата функцией значения идентична семантике инициализации. Возврщаемое значение рассматривается как инициализатор переменной возвращаемого типа. Тип возвращаемого выражения проверяется на согласованность с возвращаемым типом и выполняются все стандартные и определенные пользователем преобразования тпов. Например:

double f() (* // ... return 1; // неявно преобразуется к double(1) *)

Каждый раз, когда вызывается функция, создается новая копия ее параметров и автоматических переменных. После возрата из функции память используется заново, поэтому возврщать указатель на локальную переменную неразумно. Содержание указываемого места изменится непредсказуемо:

int* f() (* int local = 1; // ... return amp;local; // так не делайте *)

Эта ошибка менее обычна, чем эквивалентная ошибка при использовании ссылок:

int amp; f() (* int local = 1; // ... return local; // так не делайте *)

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

int amp; f() (* return 1;*) // так не делайте




4.6.5 Векторные Параметры

Если в качестве параметра функции используется вектор, то передается указатель на его первый элемент. Например:

int strlen(const char*);

void f() (* char v[] = «a vector» strlen(v); strlen(«Nicholas»); *);

Иначе говоря, при передаче как параметр параметр типа T[] преобразуется к T*. Следовательно, присваивание элементу векторного параметра изменяет значение элемента вектора, кторый является параметром. Другими словами, вектор отличается от всех остальных типов тем, что вектор не передается (и не может передаваться) по значению.

Размер вектора недоступен вызываемой функции. Это может быть неудобно, но эту сложность можно обойти несколькими спсобами. Строки оканчиваются нулем, поэтому их размер можно легко вычислить. Для других векторов можно передавать второй

параметр, который задает размер, или определить тип, содержщий указатель и индикатор длины, и передавать его вместо просто вектора (см. также #1.11). Например:

void compute1(int* vec_ptr, int vec_size); // один способ

struct vec (* // другой способ int* ptr; int size; *);

void compute2(vec v);

С многомерными массивами все хитрее, но часто можно вместо них использовать векторы указателей, которые не требют специального рассмотрения. Например:

char* day[] = (* «mon», «tue», «wed», «thu», «fri», «sat», «sun» *);

С другой стороны, рассмотрим определение функции, котрая работает с двумерными матрицами. Если размерность извесна на стадии компиляции, то никаких проблем нет:

void print_m34(int m[3][4]) (* for (int i = 0; i«3; i++) (* for (int j = 0; j„4; j++) cout «« " " «« m[i][j]; cout «« «\n“; *) *)

Матрица, конечно, все равно передается как указатель, а размерности используются просто для удобства записи.

Первая размерность массива не имеет отношения к задаче поиска положения элемента (#2.3.6). Поэтому ее можно передвать как параметр:

void print_mi4(int m[][4], int dim1) (* for (int i = 0; i«dim1; i++) (* for (int j = 0; j„4; j++) cout «« " " «« m[i][j]; cout «« «\n“; *) *)

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

void print_mij(int m[][], int dim1, int dim2) // ошибка (* for (int i = 0; i«dim1; i++) (* for (int j = 0; j„dim2; j++) cout «« " " «« m[i][j]; // сюрприз! cout «« «\n“; *) *)

Во-первых, описание параметра m[][] недопустимо, покольку для нахождения положения элемента должна быть известна вторая размерность многомерного массива. Во-вторых, выражение m[i][j] интерпретируется (правильно) как *(*(m+i)+j), но нпохоже, чтобы это имел в виду программист. Вот правильное решение:

void print_mij(int** m, int dim1, int dim2) (* for (int i = 0; i«dim1; i++) (* for (int j = 0; j„dim2; j++) cout «« " " «« (int*)m[i*dim2+j]; // туманно cout «« «\n“; *) *)

Выражение, которое применяется для доступа к элементам, эквивалентно тому, которое генерирует компилятор, когда он знает последнюю размерность. Чтобы немного прояснить програму, можно ввести дополнительную переменную:

int* v = (int*)m; // ... v[i*dim2+j]




4.6.6 Параметры по Умолчанию

Часто в самом общем случае функции требуется больше праметров, чем в самом простом и более употребительном случае. Например, в библиотеке потоков есть функция hex(), порождащая строку с шестнадцатиричным представлением целого. Второй параметр используется для задания числа символов для предтавления первого параметра. Если число символов слишком мало для представления целого, происходит усечение, если оно сликом велико, то строка дополняется пробелами. Часто програмист не заботится о числе символов, необходимых для предсталения целого, поскольку символов достаточно. Поэтому для нуля в качестве второго параметра определено значение «использвать столько символов, сколько нужно». Чтобы избежать засорния программы вызовами вроде hex(i,0), функция описывается так:

extern char* hex(long, int =0);

Инициализатор второго параметра является параметром по умолчанию. То есть, если в вызове дан только один параметр, в качестве второго используется параметр по умолчанию. Напрмер:

cout «„ „**“ «« hex(31) «« hex(32,3) «« «**“;

интерпретируется как

cout «„ „**“ «« hex(31,0) «« hex(32,3) «« «**“;

и напечатает:

** 1f 20**

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

int f(int, int =0, char* =0); // ok int g(int =0, int =0, char*); // ошибка int f(int =0, int, char* =0); // ошибка

Заметьте, что в этом контексте пробел между * и = явлется существенным (*= является операцией присваивания):

int nasty(char*=0); // синтаксическая ошибка




4.6.7 Перегрузка Имен Функций

Как правило, давать разным функциям разные имена – мысль хорошая, но когда некоторые функции выполняют одинаковую рботу над объектами разных типов, может быть более удобно дать им одно и то же имя. Использование одного имени для различных действий над различными типами называется перегрузкой (overloading). Метод уже используется для основных операций С ++: у сложения существует только одно имя, +, но его можно применять для сложения значений целых, плавающих и указателных типов. Эта идея легко расширяется на обработку операций, определенных пользователем, то есть, функций. Чтобы уберечь программиста от случайного повторного использования имени, имя может использоваться более чем для одной функции только если оно сперва описано как перегруженное имя функции. Напрмер:

overload print; void print(int); void print(char*);

Что касается компилятора, единственное общее, что имеют функции с одинаковым именем, это имя. Предположительно, они в каком-то смысле похожи, но в этом язык ни стесняет програмиста, ни помогает ему. Таким образом, перегруженные имена функций – это главным образом удобство записи. Это удобство значительно в случае функций с общепринятыми именами вроде sqrt, print и open. Когда имя семантически значимо, как это имеет место для операций вроде +, * и «« (#6.2) и в случае конструкторов (#5.2.4 и #6.3.1), это удобство становится сщественным. Когда вызывается перегруженная f(), компилятор должен понять, к какой из функций с именем f следует обртиться. Это делается путем сравнения типов фактических парметров с типами формальных параметров всех функций с именем f. Поиск функции, которую надо вызвать, осуществляется за три отдельных шага:

1. Искать функцию соответствующую точно, и использовать ее, если она найдена,

2. Искать соответствующую функцию используя встроенные преобразования и использовать любую найденную функцию и

3. Искать соответствующую функцию используя преобразвания, определенные пользователем (#6.3), и если множество преобразований единственно, использовать найденную функцию.

Например:

overload print(double), print(int);

void f(); (* print(1); print(1.0); *)

Правило точного соответствия гарантирует, что f напечтает 1 как целое и 1.0 как число с плавающей точкой. Ноль, char или short точно соответствуют параметру. Аналогично, float точно соответствует double.

К параметрам функций с перегруженными именами стандарные С++ правила неявного преобразования типа (#с.6.6) примняются не полностью. Преобразования, могущие уничтожить иформацию, не выполняются. Остаются int в long, int в double, ноль в long, ноль в double и преобразования указателей: преобразование ноль в указатель void*, и указатель на произвоный класс в указатель на базовый класс (#7.2.4).

Вот пример, в котором преобразование необходимо:

overload print(double), print(long);

void f(int a); (* print(a); *)

Здесь a может быть напечатано или как double, или как long. Неоднозначность разрешается явным преобразованием типа (или print(long(a)) или print(double(a))).

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

overload pow; int pow(int, int); double pow(double, double); // из «math.h» complex pow(double, complex); // из «complex.h» complex pow(complex, int); complex pow(complex, double); complex pow(complex, complex);

Процесс поиска подходящей функции игнорирует unsigned и const.




4.6.8 Незаданное Число Параметров

Для некоторых функций невозможно задать число и тип всех параметров, которые можно ожидать в вызове. Такую функцию описывают завершая список описаний параметров многоточием (...), что означает «и может быть, еще какие-то неописанные параметры». Например:

int printf(char* ...);

Это задает, что в вызове printf должен быть по меньшей мере один параметр, char*, а остальные могут быть, а могут и не быть. Например:

printf(«Hello, world\n»); printf(«Мое имя %s %s\n», first_name, second_name); printf(«%d + %d = %d\n»,2,3,5);

Такая функция полагается на информацию, которая недотупна компилятору при интерпретации ее списка параметров. В случае printf() первым параметром является строка формата, содержащая специальные последовательности символов, позволящие printf() правильно обрабатывать остальные параметры. %s означает «жди параметра char*», а %d означает «жди параметра int». Однако, компилятор этого не знает, поэтому он не может убедиться в том, что ожидаемые параметры имеют соответствущий тип. Например: printf(«Мое имя %s %s\n»,2);

откомпилируется и в лучшем случае приведет к какой-нбудь странного вида выдаче. Очевиднще хуже, каждый вызов функции с автоматическим объектом класса включает по меньшей мере один вызов программ выделения и освобождения свободной памяти. Это сделало бы также невозможным реализацию inline-функций членов, которые обращаются к данным закрытой части. Более того, такое изменение сделает невозможным совместную компоновку C и С++ программ (поскольку C компилятор обрабатывает struct не так, как это будет делать С++ компилятор). Для С++ это было сочтено неприемлемым.



 

 
Цемент(LAFARGE,ОСКОЛ) Качество - сухие смеси.