WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 || 3 | 4 |

«Б.Страуструп Язык программирования Си++ Москва 1991 Оглавление Предисловие Заметки для читателя Структура этой книги Замечания по реализации Упражнения Замечания по проекту языка ...»

-- [ Страница 2 ] --

Использование указателя вовлекает два объекта: сам указатель и указываемый объект. Снабжение описания указателя "префиксом" const делает объект, но не сам указатель, константой. Например:

const char* pc = "asdf";

// указатель на константу pc[3] = 'a';

// ошибка pc = "ghjk";

// ok Чтобы описать сам const указатель, а не указываемый объект, как константный, используется операция const*. Например:

char *const cp = "asdf";

// константный указатель cp[3] = 'a';

// ok cp = "ghjk";

// ошибка Чтобы сделать константами оба объекта, их оба нужно описать const. Например:

const char *const cpc = "asdf";

// const указатель на const cpc[3] = 'a';

// ошибка cpc = "ghjk";

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

char* strcpy(char* p, const char* q);

// не может изменить q Указателю на константу можно присваивать адрес переменной, поскольку никакого вреда от этого быть не может. Однако нельзя присвоить адрес константы указателю, на который не было наложено ограничение, поскольку это позволило бы изменить значение объекта.

Например:

int a = 1;

const c = 2;

const* p1 = &c;

// ok const* p2 = &a;

// ok int* p3 = &c;

// ошибка *p3 = 7;

// меняет значение c Как обычно, если тип в описании опущен, то он предполагается int.

2.4.7 Перечисления Есть другой метод определения целых констант, который иногда бо- лее удобен, чем применение const. Например:

enum { ASM, AUTO, BREAK };

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

const ASM = 0;

const AUTO = 1;

const BREAK = 2;

Перечисление может быть именованным. Например:

enum keyword { ASM, AUTO, BREAK };

Имя перечисления становится синонимом int, а не новым типом.

Описание переменной keyword, а не просто int, может дать как прог- раммисту, так и компилятору подсказку о том, что использование преднамеренное. Например:

keyword key;

switch (key) { case ASM:

// что-то делает break;

case BREAK:

// что-то делает break;

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

Можно также задавать значения перечислителей явно. Например:

enum int16 { sign=0100000, // знак most_significant=040000, // самый значимый least_significant=1 // наименее значимый };

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

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

[1] Помещение в байт более одного небольшого объекта и [2] Использование одного и того же пространства для хранения раз- ных объектов в разное время.

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

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

2.5.1 Поля Использование char для представления двоичной переменной, например, переключателя включено/выключено, может показаться экстравагантным, но char является наименьшим объектом, который в С++ может выделяться независимо. Можно, однако, сгруппировать несколько таких крошечных переменных вместе в виде полей struct. Член определяется как поле путем указания после его имени числа битов, которые он за- нимает. Допустимы неименованные поля;

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

struct sreg { unsigned enable : 1;

unsigned page : 3;

unsigned :

1;

// неиспользуемое unsigned mode : 2;

unsigned : 4: // не- используемое unsigned access : 1;

unsigned length : 1;

unsigned non_resident : 1;

} Получилось размещение регистра 0 состояния DEC PDP11/45 (в предположении, что поля в слове размещаются слева направо). Этот пример также иллюстрирует другое основное применение полей: именовать части внешне предписанного размещения. Поле должно быть целого типа и используется как другие целые, за исключением того, что невозмож- но взять адрес поля. В ядре операционной системы или в отладчике тип sreg можно было бы использовать так:

sreg* sr0 = (sreg*)0777572;

//...

if (sr->access) { // нарушение доступа // чистит массив sr->access = 0;

} Однако применение полей для упаковки нескольких переменных в один байт не обязательно экономит пространство. Оно экономит прост- ранство, занимаемое данными, но объем кода, необходимого для мани- пуляции этими переменными, на большинстве машин возрастает. Извест- ны программы, которые значительно сжимались, когда двоичные пере- менные преобразовывались из полей бит в символы! Кроме того, доступ к char или int обычно намного быстрее, чем доступ к полю. Поля - это просто удобная и краткая запись для применения логических опе- раций с целью извлечения информации из части слова или введения ин- формации в нее.

2.5.2 Объединения Рассмотрим проектирование символьной таблицы, в которой каждый элемент содержит имя и значение, и значение может быть либо стро- кой, либо целым:

struct entry { char* name;

char type;

char* string_value;

// используется если type == 's' int int_value;

// используется если type == 'i' };

void print_entry(entry* p) { switch p->type { case 's':

cout << p->string_value;

break;

case 'i':

cout << p->int_value;

break;

default:

cerr << "испорчен type\n";

break;

} } Поскольку string_value и int_value никогда не могут использо- ваться одновременно, ясно, что пространство пропадает впустую. Это можно легко исправить, указав, что оба они должны быть членами union. Например, так:

struct entry { char* name;

char type;

union { char* string_value;

// используется если type == 's' int int_value;

// используется если type == 'i' };

};

Это оставляет всю часть программы, использующую entry, без изме- нений, но обеспечивает, что при размещении entry string_value и int_value имеют один и тот же адрес. Отсюда следует, что все члены объединения вместе занимают лишь столько памяти, сколько занимает наибольший член.

Использование объединений таким образом, чтобы при чтении значе- ния всегда применялся тот член, с применением которого оно записы- валось, совершенно оптимально. Но в больших программах непросто га- рантировать, что объединения используются только таким образом, и из-за неправильного использования могут появляться трудно уловимые ошибки. Можно @капсулизировать объединение таким образом, чтобы соответствие между полем типа и типами членов было гарантированно правильным (#5.4.6).

Объединения иногда используют для "объединения и преобразование типа" (это делают главным образом программисты, воспитанные на язы- ках, не обладающих средствами преобразования типов, где жульничест- во является необходимым). Например, это "преобразует" на VAX'е int в int*, просто предполагая побитовую эквивалентность:

struct fudge { union { int i;

int* p;

};

};

fudge a;

a.i = 4096;

int* p = a.p;

// плохое использование Но на самом деле это совсем не преобразование: на некоторых маши- нах int и int* занимают неодинаковое количество памяти, а на других никакое целое не может иметь нечетный адрес. Такое применение объ- единений непереносимо, а есть явный способ указать преобразование типа (#3.2.5).

Изредка объединения умышленно применяют, чтобы избежать преобра- зования типов. Можно, например, использовать fudge, чтобы узнать представление указателя 0:

fudge.p = 0;

int i = fudge.i;

// i не обязательно должно быть Можно также дать объединению имя, то есть сделать его полноправ- ным типом. Например, fudge можно было бы описать так:

union fudge { int i;

int* p;

};

и использовать (неправильно) в точности как раньше. Имеются также и оправданные применения именованных объединений, см. #5.4.6.

2.6 Упражнения 1. (*1) Заставьте работать программу с "Hello, world" (1.1.1).

2. (*1) Для каждого описания в #2.1 сделайте следующее: Если описание не является определением, напишите для него определе- ние. Если описание является определением, напишите для него описание, которое при этом не является определением.

3. (*1) Напишите описания для: указателя на символ;

вектора из 10 целых;

ссылки на вектор из 10 целых;

указателя на вектор из символьных строк;

указателя на указатель на символ;

констант- ного целого;

указателя на константное целое;

и константного указателя на целое. Каждый из них инициализируйте.

4. (*1.5) Напишите программу, которая печатает размеры основных и указательных типов. Используйте операцию sizeof.

5. (*1.5) Напишите программу, которая печатает буквы 'a'...'z' и цифры '0'...'9' и их числовые значения. Сделайте то же для остальных печатаемых символов. Сделайте то же, но используя шестнадцатиричную запись.

6. (*1) Напечатайте набор битов, которым представляется указатель 0 на вашей системе. Подсказка: #2.5.2.

7. (*1.5) Напишите функцию, печатающую порядок и мантиссу пара- метра типа double.

8. (*2) Каковы наибольшие и наименьшие значения, на вашей систе- ме, следующих типов: char, short, int, long, float, double, unsigned, char*, int* и void*? Имеются ли дополнительные огра- ничения на принимаемые ими значения? Может ли, например, int* принимать нечетное значение? Как выравниваются в памяти объек- ты этих типов? Может ли, например, int иметь нечетный адрес?

9. (*1) Какое самое длинное локальное имя можно использовать в С++ программе в вашей системе? Какое самое длинное внешнее имя можно использовать в С++ программе в вашей системе? Есть ли какие-нибудь ограничения на символы, которые можно употреблять в имени?

10. (*2) Определите one следующим образом:

const one = 1;

Попытайтесь поменять значение one на 2. Определите num следую- щим образом:

const num[] = { 1, 2 };

Попытайтесь поменять значение num[1] на 2.

11. (*1) Напишите функцию, переставляющую два целых (меняющую значения). Используйте в качестве типа параметра int*. Напиши- те другую переставляющую функцию, использующую в качестве типа параметра int&.

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

char str[] = "a short string";

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

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

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

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

* Нам неизвестен русскоязычный термин, эквивалентный английскому indentation. Иногда это называется отступами. (прим. перев.) 3.1 Настольный калькулятор С операторами и выражениями вас познакомит приведенная здесь программа настольного калькулятора, предоставляющего четыре стан- дартные арифметические операции над числами с плавающей точкой.

Пользователь может также определять переменные. Например, если вво- дится r=2.5 area=pi*r*r (pi определено заранее), то программа калькулятора напишет:

2. 19. где 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 вычисляется только один раз. Это применимо к бинарным операциям + - * / % & | ^ << >> поэтому возможны следующие операции присваивания:

+= -= *= /= %= &= |= ^= <<= >>= Каждая является отдельной лексемой, поэтому a+ =1 является син- таксической ошибкой из-за пробела между + и =. (% является операци- ей взятия по модулю;

&,| и ^ являются побитовыми операциями И, ИЛИ и исключающее ИЛИ;

<< и >> являются операциями левого и правого сдвига). Функции 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' && isspace(ch));

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

Функция (inline) isspace() из обеспечивает стандартную проверку на то, является ли символ пропуском (#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 - это стандартный про- пусковый объект, описанный в ;

он используется только для сброса пропуска. Ошибка во вводе или конец ввода не будут обнаруже- ны до следующего обращения к 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) && isalnum(ch)) *p++ = ch;

cin.putback(ch);

*p = 0;

return curr_tok=NAME;

} Эта часть строит в name_string строку, заканчивающуюся нулем.

Функции isalpha() и isalnum() заданы в ;

isalnum(c) не ноль, если c буква или цифра, ноль в противном случае.

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

token_value get_token() { char ch;

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

} while (ch!='\n' && 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) && 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...TBLSZ-1;

% - это операция взятия по модулю (еще называемая получением остатка).

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

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. параметры имеют значения:

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)++.

Сводка Операций (часть 1) ----------------------------------------------------------------¬ ¦ :: разрешение области видимости имя_класса :: член ¦ ¦ :: глобальное :: имя ¦ +---------------------------------------------------------------+ ¦ -> выбор члена указатель->член ¦ ¦ [] индексация указатель [ выр ] ¦ ¦ () вызов функции выр (список_выр) ¦ ¦ () построение значения тип (список_выр) ¦ ¦ sizeof размер объекта sizeof выр ¦ ¦ sizeof размер типа sizeof ( тип ) ¦ +---------------------------------------------------------------+ ¦ ++ приращение после lvalue++ ¦ ¦ ++ приращение до ++lvalue ¦ ¦ -- уменьшение после lvalue-- ¦ ¦ -- уменьшение до --lvalue ¦ ¦ ~ дополнение ~ выр ¦ ¦ ! не ! выр ¦ ¦ - унарный минус - выр ¦ ¦ + унарный плюс + выр ¦ ¦ & адрес объекта & lvalue ¦ ¦ * разыменование * выр ¦ ¦ new создание (размещение) new тип ¦ ¦ delete уничтожение (освобождение) delete указатель ¦ ¦ delete[] уничтожение вектора delete[ выр ] указатель ¦ ¦ () приведение (преобразование типа) ( тип ) выр ¦ +---------------------------------------------------------------+ ¦ * умножение выр * выр ¦ ¦ / деление выр / выр ¦ ¦ % взятие по модулю (остаток) выр % выр ¦ +---------------------------------------------------------------+ ¦ + сложение (плюс) выр + выр ¦ ¦ - вычитание (минус) выр - выр ¦ L---------------------------------------------------------------- В каждой очерченной части находятся операции с одинаковым приори- тетом. Операция имеет приоритет больше, чем операции из частей, расположенных ниже. Например: a+b*c означает a+(b*c), так как * имеет приоритет выше, чем +, а a+b-c означает (a+b)-c, поскольку + и - имеют одинаковый приоритет (и поскольку + левоассоциативен).

Сводка Операций (часть 2) ----------------------------------------------------------------¬ ¦ << сдвиг влево lvalue << выр ¦ ¦ >> сдвиг вправо lvalue >> выр ¦ +---------------------------------------------------------------+ ¦ < меньше выр < выр ¦ ¦ <= меньше или равно выр <= выр ¦ ¦ > больше выр > выр ¦ ¦ >= больше или равно выр >= выр ¦ +---------------------------------------------------------------+ ¦ == равно выр == выр ¦ ¦ != не равно выр != выр ¦ +---------------------------------------------------------------+ ¦ & побитовое И выр & выр ¦ +---------------------------------------------------------------+ ¦ ^ побитовое исключающее ИЛИ выр ^ выр ¦ +---------------------------------------------------------------+ ¦ | побитовое включающее ИЛИ выр | выр ¦ +---------------------------------------------------------------+ ¦ && логическое И выр && выр ¦ +---------------------------------------------------------------+ ¦ || логическое включающее ИЛИ выр || выр ¦ +---------------------------------------------------------------+ ¦ ? : арифметический if выр ? выр : выр ¦ +---------------------------------------------------------------+ ¦ = простое присваивание lvalue = выр ¦ ¦ *= умножить и присвоить lvalue = выр ¦ ¦ /= разделить и присвоить lvalue /= выр ¦ ¦ %= взять по модулю и присвоить lvalue %= выр ¦ ¦ += сложить и присвоить lvalue += выр ¦ ¦ -= вычесть и присвоить lvalue -= выр ¦ ¦ <<= сдвинуть влево и присвоить lvalue <<= выр ¦ ¦ >>= сдвинуть вправо и присвоить lvalue >>= выр ¦ ¦ &= И и присвоить lvalue &= выр ¦ ¦ |= включающее ИЛИ и присвоить lvalue |= выр ¦ ¦ ^= исключающее ИЛИ и присвоить lvalue ^= выр ¦ +---------------------------------------------------------------+ ¦, запятая (следование) выр, выр ¦ L---------------------------------------------------------------- 3.2.1 Круглые скобки Скобками синтаксис С++ злоупотребляет;

количество способов их использования приводит в замешательство: они применяются для заклю- чения в них параметров в вызовах функций, в них заключается тип в преобразовании типа (приведении к типу), в именах типов для обозна- чения функций, а также для разрешения конфликтов приоритетов. К счастью, последнее требуется не слишком часто, потому что уровни приоритета и правила ассоциативности определены таким образом, что- бы выражения "работали ожидаемым образом" (то есть, отражали наибо- лее привычный способ употребления). Например, значение if (i<=0 || max

очевидно. Тем не менее, всегда, когда программист сомневается от- носительно этих правил, следует употреблять скобки, и некоторые программисты предпочитают немного более длинное и менее элегантное if ( (i<=0) || (max

При усложнении подвыражений употребление скобок становится более обычным явлением, но сложные подвыражения являются источником оши- бок, поэтому если вы чувствуете потребность в скобках, попробуйте оборвать выражение и использовать дополнительную переменную. Есть и такие случаи, когда приоритеты операций не приводят к "очевидному" результату. Например в if (i&mask == 0) //...

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

if ((i&mask) == 0) //...

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

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

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

3.2.2 Порядок вычисления Порядок вычисления подвыражений в выражении неопределен. Например int i = 1;

v[i] = i++;

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

Относительно операций && и || гарантируется, что их левый операнд вычисляется раньше, чем правый. Например, b=(a=2,a=1) присвоит b 3.

В #3.3.1 приводятся примеры использования && и ||. Заметьте, что операция следования, (запятая) логически отличается от запятой, которая используется для разделения параметров в вызове функции.

Рассмотрим 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 Увеличение и уменьшение Операция ++ используется для явного выражения приращения вместо его неявного выражения с помощью комбинации сложения и присваива- ния. По определению ++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 Побитовые логические операции Побитовые логические операции & | ^ ~ >> << применяются к целым, то есть к объектам типа char, short, int, long и их unsigned аналогам, результаты тоже целые.

Одно из стандартных применений побитовых логических операций - реализация маленького множества (вектор битов). В этом случае каж- дый бит беззнакового целого представляет один член множества, а число членов ограничено числом битов. Бинарная операция & интерпре- тируется как пересечение, | как объединение, а ^ как разность. Для наименования членов такого множества можно использовать перечисле- ние. Вот маленький пример, заимствованный из реализации (не пользо- вательского интерфейса) :

enum state_value { _good=0, _eof=1, _fail=2, _bad=4 };

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

cout.state = _good;

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

if (cout.state&(_bad|_fail)) // не good Еще одни скобки необходимы, поскольку & имеет более высокий приори- тет, чем |.

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

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)&0xffff;

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

&& || !

Последние возвращают 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*)&some_object;

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

Когда преобразование типа не необходимо, его следует избегать.

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

int i = 1;

char* pc = "asdf";

int* pi = &i;

i = (int)pc;

pc = (char*)i;

// остерегайтесь! значение pc может измениться // на некоторых машинах // sizeof(int)

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 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 main() { char* p = new char[100000000];

cout << "сделано, p = " << long(p) << "\n";

} выдаст сделано, p = Вам сделали предупреждение! Заметьте, что тот, кто задает _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) //...

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

if (p && 1count) //...

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

Некоторые простые операторы 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 Заметьте, что метка case не подходит как метка для употребления в операторе goto:

goto case 1;

// синтаксическая ошибка 3.3.2 Goto С++ снабжен имеющим дурную репутацию оператором goto.

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

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

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

for (int i = 0;

i

i++) for (int j = 0;

j

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&T sB20.

/************************************** Copyright (c) 1984 AT&T, Inc. All rights reserved ****************************************/ Удачно подобранные и хорошо написанные комментарии - существенная часть программы. Написание хороших комментариев может быть столь же сложным, сколь и написание самой программы.

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

3.5 Упражнения 1. (*1) Перепишите следующий оператор for в виде эквивалентного оператора while:

for (i=0;

i

i++) if (input_line[i] == '?') quest_count++;

2. (*1) Полностью расставьте скобки в следующих выражениях:

a = b + c * d << 2 & 8 a & 077 != 3 a == b || a == c && 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(), которая сравнивает две строки. Разберитесь, какие должны быть типы параметров и типы возвращаемых значений, а потом сравните их со стандартными версиями, которые описаны в и в вашем руководстве.

8. (*1) Посмотрите, как ваш компилятор реагирует на ошибки:

a := b+1;

if (a = 3) //... if (a&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. Однако компоновщики, имеющиеся в большинстве систем, обеспечивают очень слабую поддержку проверки согласованности.

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

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 // из стандартной директории включения #define "myheader.h" // из текущей директории Использование <> имеет то преимущество, что в программу факти- ческое имя директории включения не встраивается (как правило, сна- чала просматривается /usr/include/CC, а потом usr/include). К сожа- лению, пробелы в директиве include существенны:

#include < 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 Определения макросов #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 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() используется почти каждой функцией программы, а сама использует только . Это обычная для функции ошибок ситуация, поэтому error() следует отделить от main():

// error.h: обработка ошибок extern int no_errors;

extern double error(char* s);

// error.c #include #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 #include "table.h" const TBLSZ = 23;

name* table[TBLSZ];

name* look(char* p;

int ins) { /*... */ } Заметьте, что описания функций работы со строками теперь включаются из . Это исключает еще один возможный источник ошибок.

// 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 #include #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 #include "error.h" #include "lex.h" #include "syn.h" #include "table.h" #include 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 #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);

// подмножество 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. Значение такой проверки типа и преобразования типа огромно.

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

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

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& 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& arg) { // значение "arg" не может быть изменено } Аналогично, описание параметра указателя как const сообщает чита- телю, что значение объекта, указываемого указателем, функцией не изменяется. Например:

extern int strlen(const char*);

// из 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 &local;

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

int& f() { int local = 1;

//... return local;

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

int& 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

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

i++) { for (int j = 0;

j

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

i++) { for (int j = 0;

j

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);

// из complex pow(double, complex);

// из 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);

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

Очевидно, если параметр не был описан, то у компилятора нет ин- формации, необходимой для выполнения над ним проверки типа и преоб- разования типа. В этом случае char или short передаются как int, а float передается как double. Это не обязательно то, чего ждет поль- зователь.

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

extern int fprintf(FILE*, char*...);

// из extern int execl(char*...);

// из extern int abort(...);

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

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

void error(int...);

main(int argc, char* argv[]) { switch(argc) { case 1:

error(0,argv[0],0);

break;

case 2:

error(0,argv[0],argv[1],0);

default:

error(1,argv[0],"с",dec(argc-1),"параметрами",0);

} } Функцию ошибок можно определить так:

#include void error(int n...) /* "n" с последующим списком char*, оканчивающихся нулем */ { va_list ap;

va_start(ap,n);

// раскрутка arg for (;

;

) { char* p = va_arg(ap,char*);

if(p == 0) break;

cerr << p << " ";

} va_end(ap);

// очистка arg cerr << "\n";

if (n) exit(n);

} Первый из va_list определяется и инициализируется вызовом va_start(). Макрос va_start получает имя va_list'а и имя последнего формального параметра как параметры. Макрос va_arg используется для выбора неименованных параметров по порядку. При каждом обращении программист должен задать тип;

va_arg() предполагает, что был пере- дан фактический параметр, но обычно способа убедиться в этом нет.

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

va_end() аннулирует все эти изменения.

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

void error(char* p) { /*... */ } void (*efct)(char*);

// указатель на функцию void f() { efct = &error;

// efct указывает на error (*efct)("error");

// вызов error через efct } Чтобы вызвать функцию через указатель, например, efct, надо сна- чала этот указатель разыменовать, *efct. Поскольку операция вызова функции () имеет более высокий приоритет, чем операция разыменова- ния *, то нельзя писать просто *efct("error"). Это означает *efct("error"), а это ошибка в типе. То же относится и к синтаксису описаний (см. также #7.3.4).

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

void (*pf)(char*);

// указатель на void(char*) void f1(char*);

// void(char*) int f2(char*);

// int(char*) void f3(int*);

// void(int*) void f() { pf = &f1;

// ok pf = &f2;

// ошибка: не подходит возвращаемый тип pf = &f3;

// ошибка: не подходит тип параметра (*pf)("asdf");

// ok (*pf)(1);

// ошибка: не подходит тип па- раметра int i = (*pf)("qwer");

// ошибка: void присваивается int'у } Правила передачи параметров для непосредственных вызовов функции и для вызовов функции через указатель одни и те же.

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

Например:

typedef int (*SIG_TYP)();

// из typedef void (*SIG_ARG_TYP);

SIG_TYP signal(int,SIG_ARG_TYP);

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

typedef void (*PF)();

PF edit_ops[] = { // операции редактирования cut, paste, snarf, search };

PF file_ops[] = { // управление файлом open, reshape, close, write };

-------------------- * Мышь - это указывающее устройство по крайней мере с одной кнопкой. Моя мышь красная, круглая и с тремя кнопками. (прим. авто- ра) Затем определяем и инициализируем указатели, определяющие действия, выбранные в меню, которое связано с кнопками (button) мы- ши:

PF* button2 = edit_ops;

PF* button3 = file_ops;

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

(button2[3])();

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

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

typedef int (*CFT)(char*,char*);

int sort(char* base, unsigned n, int sz, CFT cmp) /* Сортирует "n" элементов вектора "base" в возрастающем порядке с помощью функции сравнения, указываемой "cmp". Размер эле- ментов "sz".

Очень неэффективный алгоритм: пузырьковая сортировка */ { for (int i=0;

i

i++) for (int j=n-1;

i

j--) { char* pj = base+j*sz;

// b[j] char* pj1 = pj-sz;

// b[j-1] if ((*cmp)(pj,pj1) < 0) // переставить b[j] и b[j-1] for (int k=0;

k

k++) { char temp = pj[k];

pj[k] = pj1[k];

pj1[k] = temp;

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

Тип функции sort() был выбран тем же, что и тип подпрограммы из стандартной C библиотеки qsort(). Настоящая программа использует qsort(). Поскольку sort() не возвращает значения, она должна быть описана как void, но в то время, когда определялась qsort(), тип void в C введен не был. Аналогично этому, было бы честнее использо- вать в качестве типа параметра void* вместо char*. Такую сортирую- щую функцию можно использовать, например, для сортировки следующей таблицы:

struct user { // пользователь char* name;

// имя char* id;

// идентификатор int dept;

// подразделение };

typedef user* Puser;

user heads[] = { // начальники "McIlroy M.D.", "doug", 11271, "Aho A.V.", "ava", 11272, "Weinberger P.J.", "pjw", 11273, "Schryer N.L.", "nls", 11274, "Schryer N.L.", "nls", 11275, "Kernighan B.W.", "bwk", };

void print_id(Puser v, int n) { for (int i=0;

i

i++) cout << v[i].name << "\t" << v[i].id << "\t" << v[i].dept << "\n";

} Чтобы иметь возможность сортировать, надо сначала определить под- ходящие функции сравнения. Функция сравнения должна возвращать от- рицательное значение, если ее первый параметр меньше второго, ноль, если они равны, и положительное значение в остальных случаях:

int cmp1(char* p, char* q) // Сравнивает строки name { return strcmp(Puser(p)->name, Puser(q)->name);

} int cmp2(char*p, char* q) // Сравнивает числа dept { return Puser(p)->dept-Puser(q)->dept;

} Эта программа сортирует и печатает:

main () { sort((char*)heads,6,sizeof(user),cmp1);

print_id(heads,6);

// в алфавитном порядке cout << "\n";

sort((char*)heads,6,sizeof(user),cmp2);

print_id(heads,6);

// по порядку подразделений } Можно взять адрес inline-функции, как, впрочем, и адрес перегру- женной функции(#с.8.9).

4.7 Макросы Макросы* определяются в #с.11. В C они очень важны, но в С++ при- меняются гораздо меньше. Первое правило относительно них такое: не используйте их, если вы не обязаны это делать. Как было замечено, почти каждый макрос проявляет свой изъян или в языке, или в прог- рамме. Если вы хотите использовать макросы, прочитайте, пожалуйста, вначале очень внимательно руководство по вашей реализации C препро- цессора.

Простой макрос определяется так:

#define name rest of line Когда name встречается как лексема, оно заменяется на rest of line. Например:

named = name после расширения даст:

named = rest of line Можно также определить макрос с параметрами. Например:

#define mac(a,b) argument1: a argument2: b При использовании mac должно даваться две строки параметра. После расширения mac() они заменяют a и b. Например:

expanded = mac(foo bar, yuk yuk) после расширения даст expanded = argument1: foo bar argument2: yuk yuk Макросы обрабатывают строки и о синтаксисе С++ знают очень мало, а о типах С++ или областях видимости - ничего. Компилятор видит только расширенную форму макроса, поэтому ошибка в макросе диаг- ностируется когда макрос расширен, а не когда он определен. В ре- зультате этого возникают непонятные сообщения об ошибках.

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

#define Case break;

case #define nl <<"\n" #define forever for(;

;

) #define MIN(a,b) (((a)<(b))?(a):(b)) Вот совершенно ненужные макросы:

#define PI 3. #define BEGIN { #define END } А вот примеры опасных макросов:

#define SQUARE(a) a*a #define INCR_xx (xx)++ #define DISP = Чтобы увидеть, чем они опасны, попробуйте провести расширения в следующем примере:

int xx = 0;

// глобальный счетчик void f() { int xx = 0;

// локальная переменная xx = SQUARE(xx+2);

// xx = xx+2*xx+2 INCR_xx;

// увеличивает локальный xx if (a-DISP==b) { // a-= 4==b //...

} } Если вы вынуждены использовать макрос, при ссылке на глобальные имена используйте операцию разрешения области видимости :: (#2.1.1) и заключайте вхождения имени параметра макроса в скобки везде, где это возможно (см. MIN выше).

Обратите внимание на различие результатов расширения этих двух макросов:

#define m1(a) something(a) // глубокомысленный комментарий #define m2(a) something(a) /* глубокомысленный комментарий */ например, int a = m1(1)+2;

int b = m2(1)+2;

расширяется в int a = something(1) // глубокомысленный комментарий+2;

int b = something(1) /* глубокомысленный комментарий */+2;

С помощью макросов вы можете разработать свой собственный язык.

Pages:     | 1 || 3 | 4 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.