Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

cpp

.pdf
Скачиваний:
13
Добавлен:
02.03.2016
Размер:
3.09 Mб
Скачать

Бьерн Страуструп.

Язык программирования С++

{

 

 

char* p

= new char[100000000];

 

cout <<

"done, p = " << long(p) << '\n';

 

}

 

 

выдаст

done, p = 0

Память не выделена, и вам сделано предупреждение! Отметим, что, задав реакцию на такую ситуацию в функции new_handler, пользователь берет на себя проверку: исчерпана ли свободная память. Она должна выполняться при каждом обращении в программе к new (если только пользователь не определил собственные функции для размещения объектов пользовательских типов; см.$$R.5.5.6).

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

Полное и последовательное описание операторов С++ содержится в $$R.6. Советуем ознакомиться с этим разделом. Здесь же дается сводка операторов и несколько примеров.

Синтаксис операторов

оператор: описание

{ список-операторов opt } выражение opt ;

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

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

while ( выражение ) оператор

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

for (начальный-оператор-for выражение opt; выражение opt) оператор

case выражение-константа : оператор default : оператор

break ; continue ;

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

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

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

список-операторов оператор

начальный-оператор-for: описание

выражение opt ;

Обратите внимание, что описание является оператором, но нет операторов присваивания или вызова функции (они относятся к выражениям).

3.3.1 Выбирающие операторы

Значение можно проверить с помощью операторов if или switch:

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

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

90

Бьерн Страуструп.

Язык программирования С++

В языке С++ среди основных типов нет отдельного булевского (тип

со значениями истина, ложь). Все

операции отношений:

 

== != < > <= >=

дают в результате целое 1, если отношение выполняется, и 0 в противном случае. Обычно определяют константы TRUE как 1 и FALSE как 0.

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

if (a) // ...

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

if (a != 0) ...

Логические операции

&&|| !

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

if (p && l<p->count) // ...

сначала проверяется значение p, и только если оно не равно нулю, то проверяется отношение l<p- >count. Некоторые простые операторы if удобно заменять выражениями условия. Например, вместо оператора

if (a <= b) 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();

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

91

Бьерн Страуструп.

Язык программирования С++

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

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

switch (val) { // возможна ошибка case 1:

cout << "case 1\n"; case 2:

cout << "case 2\n"; default:

cout << "default: case not found\n";

}

при val==1 напечатает к большому удивлению непосвященных:

case 1 case 2

default: case not found

Имеет смысл отметить в комментариях те редкие случаи, когда стандартный переход на следующий вариант оставлен намеренно. Тогда этот переход во всех остальных случаях можно смело считать ошибкой. Для завершения оператора в варианте чаще всего используется break, но иногда используются return и даже 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 not found\n"; return;

}

Здесь при значении val равном 2 мы получим:

case 2 case 1

Отметим, что метку варианта нельзя использовать в операторе goto:

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

3.3.2 Оператор goto

Презираемый оператор goto все-таки есть в С++:

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

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

92

Бьерн Страуструп.

Язык программирования С++

Есть немногие ситуации и в обычных программах, когда применение goto оправдано. Одна из них - выход из вложенного цикла или переключателя. Дело в том, что оператор break во вложенных циклах или переключателях позволяет перейти только на один уровень выше. Приведем пример:

void f()

{

int i; int j;

for ( i = 0; i < n; i++) for (j = 0; j<m; j++)

if (nm[i][j] == a) goto found;

//здесь a не найдено

//...

found:

// nm[i][j] == a

}

Есть еще оператор continue, который позволяет перейти на конец цикла. Что это значит, объяснено в $$3.1.5.

3.4 Комментарии и расположение текста

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

Однако можно заполнить программу такими комментариями, что читать и понимать ее будет только труднее. Транслятор не в силах понять комментарий, поэтому он не может убедиться в том, что комментарий:

[1] осмысленный,

[2]действительно описывает программу,

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

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

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

//переменную "v" необходимо инициализировать.

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

//до вызова любой функции из этого файла

//необходимо вызвать функцию "init()".

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

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

//функция "f()" имеет два параметра.

При правильном программировании на С++ такие комментарии обычно оказываются излишними. Чтобы именно эти комментарии стали ненужными, можно воспользоваться правилами связывания ($$4.2) и областей видимости, а также правилами инициализации и уничтожения объектов класса ($$5.5).

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

a = b + c; count++;

//a принимает значение b+c

//увеличим счетчик count

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

93

Бьерн Страуструп.

Язык программирования С++

учебниках по языкам программирования, подобных этой книге. Это одна из многих причин, по которой учебная программа отличается от настоящей.

Можно рекомендовать такой стиль введения комментариев в программу:

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

[2]снабжать комментарием каждое определение класса или шаблона типа;

[3]комментировать каждую нетривиальную функцию, указав: ее назначение, используемый алгоритм (если только он неочевиден) и, возможно, предположения об окружении, в котором работает функция;

[4]комментировать определение каждой глобальной переменной;

[5]давать некоторое число комментариев в тех местах, где алгоритм неочевиден или непереносим;

[6]больше практически ничего.

Приведем пример:

//tbl.c: Реализация таблицы имен.

/*

Использован метод Гаусса см. Ральстон "Начальный курс по ..." стр. 411.

*/

// в swap() предполагается, что стек AT&T начинается с 3B20.

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

Авторские права (c) 1991 AT&T, Inc Все права сохранены

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

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

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

3.5 Упражнения

1. (*1) Следующий цикл for перепишите с помощью оператора while:

for (i=0; i<max_length; i++)

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

Запишите цикл, используя в качестве его управляющей переменной указатель так, чтобы условие имело вид *p=='?'.

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

94

Бьерн Страуструп.

Язык программирования С++

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

4.(*2) Приведите 10 разных примеров непереносимых конструкций на С++.

5.(*1) Что произойдет при делении на нуль в вашей программе на С++? Что будет в случае переполнения или потери значимости?

6.(*1) Укажите порядок вычисления следующих выражений, задав их полную скобочную структуру:

*p++

*--p ++a-- (int*)p->m *p.m

*a[i]

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

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

void f(int a, int b)

{

if (a = 3) // ...

if (a&077 == 0) // ...

a := b+1;

}

Посмотрите, какова будет реакция на более простые ошибки.

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: *to++ = *from++; case 6: *to++ = *from++; case 5: *to++ = *from++; case 4: *to++ = *from++; case 3: *to++ = *from++; case 2: *to++ = *from++; case 1: *to++ = *from++;

} while (--n>0);

}

}

Каков может быть смысл этой функции?

11. (*2) Напишите функцию atoi(), которая имеет параметр - строку цифр и возвращает соответствующее ей целое. Например, atoi("123") равно 123. Измените функцию atoi() так, чтобы она могла переводить в число последовательность цифр не только в десятичной, но и в восьмеричной и шестнадцатеричной записи, принятой в С++. Добавьте возможность перевода символьных констант С++. Напишите функцию itoa() для перевода целого значения в строковое представление.

12. (*2) Перепишите функцию get_token() ($$3.12) так, чтобы она читала целую строку в буфер, а затем

95

Бьерн Страуструп.

Язык программирования С++

выдавала лексемы, читая по символу из буфера.

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

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

15. (*1.5) Переделайте программу калькулятора, используя структуру symbol вместо статических переменных name_string и number_value:

struct symbol { token_value tok; union {

double number_value; char* name_string;

};

};

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

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

96

Бьерн Страуструп.

Язык программирования С++

ГЛАВА 4.

Итерация присуща человеку, а рекурсия - богу. - Л. Дойч

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

4.1 Введение

Роль файла в языке С++ сводится к тому, что он определяет файловую область видимости ($$R.3.2). Это область видимости глобальных функций (как статических, так и подстановок), а также глобальных переменных (как статических, так и со спецификацией const). Кроме того, файл является традиционной единицей хранения в системе, а также единицей трансляции. Обычно системы хранят, транслируют и представляют пользователю программу на С++ как множество файлов, хотя существуют системы, устроенные иначе. В этой главе будет обсуждаться в основном традиционное использование файлов.

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

Вернемся к примеру с калькулятором. Решение было дано в виде одного файла. Когда вы попытаетесь его транслировать, неизбежно возникнут некоторые проблемы с порядком описаний. По крайней мере одно "ненастоящее" описание придется добавить к тексту, чтобы транслятор мог разобраться в использующих друг друга функциях expr(), term() и prim(). По тексту программы видно, что она состоит из четырех частей: лексический анализатор (сканер), собственно анализатор, таблица имен и драйвер. Однако, этот факт никак не отражен в самой программе. На самом деле калькулятор не был запрограммирован именно так. Так не следует писать программу. Даже если не учитывать все рекомендации по программированию, сопровождению и оптимизации для такой "зряшной" программы, все равно ее следует создавать из нескольких файлов хотя бы для удобства.

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

4.2 Связывание

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

// file1.c

97

Бьерн Страуструп.

Язык программирования С++

int

a =

1;

 

int

f()

{ /* какие-то операторы */ }

 

// file2.c extern int a; int f();

void g() { a = f(); }

В функции g() используются те самые a и f(), которые определены в файле 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 описано дважды, но неопределено. Такие ошибки (ошибки связывания) транслятор, который обрабатывает файлы по отдельности, обнаружить не может, но большая их часть обнаруживается редактором связей.

Следующая программа допустима в С, но не в С++:

//file1.c: int a;

int f() { return a; }

//file2.c:

int a;

int g() { return f(); }

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

Если имя описано как static, оно становится локальном в этом файле. Например:

//file1.c: static int a = 6;

static int f() { /* ... */ }

//file2.c:

static int a = 7;

static int f() { /* ... */ }

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

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

Обычно говорят, что имена типов, т.е. классов и перечислений, не подлежат связыванию. Имена

98

Бьерн Страуструп.

Язык программирования С++

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

// file1.c:

struct S { int a; char b; }; extern void f(S*);

// file2.c:

struct S { int a; char b; }; void f(S* p) { /* ... */ }

Но будьте осторожны: опознать идентичность двух описаний класса не в состоянии большинство систем программирования С++. Такое дублирование может вызвать довольно тонкие ошибки (ведь классы в разных файлах будут считаться различными).

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

//file1.c: typedef int T; const int a = 7;

inline T f(int i) { return i+a; }

//file2.c:

typedef void T; const int a = 8;

inline T f(double d) { cout<<d; }

Константа может получить внешнее связывание только с помощью явного описания:

// file3.c:

extern const int a; const int a = 77;

// file4.c:

extern const int a; void g() { cout<<a; }

В этом примере g() напечатает 77.

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

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

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

#include "включаемый-файл"

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

99

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]