355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Мюррей Хилл » C++ » Текст книги (страница 9)
C++
  • Текст добавлен: 26 сентября 2016, 11:55

Текст книги "C++"


Автор книги: Мюррей Хилл


Соавторы: Бьярн Страустрап
сообщить о нарушении

Текущая страница: 9 (всего у книги 26 страниц) [доступный отрывок для чтения: 10 страниц]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

char* p = new char[100000000];

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

#include «stream.h»

void out_of_store()

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

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

extern PF set_new_handler(PF);

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

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

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

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

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

include «stream.h»

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

выдаст

сделано, p = 0

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

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

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

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

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

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

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

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

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

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

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

3.3.1 Проверки

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

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

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

== != « „= “ »=

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

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

if (a) // ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

case 2 case 1

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

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

3.3.2 Goto

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

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

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

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

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

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

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

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

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

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

1. осмыслен,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Например:

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

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

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

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

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

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

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

3.5 Упражнения

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Итерация свойственна человеку, рекурсия божественна.

Л. Питер Дойч

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

4.1 Введение

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

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

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

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

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

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

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


    Ваша оценка произведения:

Популярные книги за неделю