|
|
|||
|
wm-help.net -> Электронная библиотека -> C++/C#/C -> Бьярн Страустрап. Введение в язык Си++ -> Выражения и операторыВыражения и операторы
Глава 3Выражения и операторыС другой стороны, мы не можем
игнорировать эффективность C++ имеет небольшой, но гибкий набор различных видов операторов для контроля потока управления в программе и богатый набор операций для манипуляции данными. С наиболее общепринятыми средствами вас познакомит один законченный пример. После него приводится резюмирующий обзор выражений и с довольно подробно описываются явное описание типа и работа со свободной памятью. Потом представлена краткая сводка операций, а в конце обсуждаются стиль выравнивания *1 и комментарии.3.1 Настольный калькулятор
С операторами и выражениями вас познакомит приведенная здесь программа настольного калькулятора, предоставляющего четыре стандартные арифметические операции над числами с плавающей точкой. Пользователь может также определять переменные. Например, если вводится r=2.5 area=pi*r*r
2.5 19.635
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 // число с плавающей точкой в C++
NAME // имя C++ за исключением '_'
NAME = expression
- primary
( expression )
enum token_value {
NAME NUMBER END
PLUS='+' MINUS='-' MUL='*' DIV='/'
PRINT=';' ASSIGN='=' LP='(' RP=')'
};
token_value curr_tok;
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;
}
}
double expr(); // без этого нельзя
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;
}
}
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");
}
}
srtuct name {
char* string;
char* next;
double value;
}
name* look(char*); name* insert(char*);
3.1.2 Функция вводаЧтение ввода - часто самая запутанная часть
программы. Причина в том, что если программа
должна общаться с человеком, то она должна
справляться с его причудами, условностями и
внешне случайными ошибками. Попытки заставить
человека вести себя более удобным для машины
образом часто (и справедливо) рассматриваются
как оскорбительные. Задача низкоуровневой
программы ввода состоит в том, чтобы читать
символы по одному и составлять из них
лексические символы более высокого уровня. Далее
эти лексемы служат вводом для программ более
высокого уровня. У нас ввод низкого уровня
осуществляется 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 '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;
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;
}
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;
}
}
3.1.3 Таблица именК таблице имен доступ осуществляется с помощью одной функции name* look(char* p, int ins =0);
inline name* insert(char* s) { return look(s,1);}
srtuct name {
char* string;
char* next;
double value;
}
const TBLSZ = 23; name* table[TBLSZ];
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.1934 параметры имеют значения: argc 2 argv[0] "dc" argv[1] "150/1.1934" Научиться пользоваться параметрами командной строки несложно; сложность состоит в том, как использовать их без перепрограммирования. В данном случае это оказывается совсем просто, поскольку поток ввода можно связать с символьной строкой, а не с файлом (#8.5). Например, можно заставить cin читать символы из стандартного ввода:
int main(int argc, char* argv[])
{
switch(argc) {
case 1: // читать из стандартного ввода
break;
case 2: // читать параметр строку
cin = *new istream(strlen(argv[1]),argv[1]);
break;
default:
error("слишком много параметров");
return 1;
}
// как раньше
}
Программа осталась без изменений, за исключением добавления в main() параметров и использования этих параметров в операторе switch. Можно было бы легко модифицировать main() так, чтобы она получала несколько параметров командной строки, но это оказывается ненужным, особенно потому, что несколько выражений можно передавать как один параметр: dc "rate=1.1934;150/rate;19.75/rate;217/rate" Здесь кавычки необходимы, поскольку ; является разделителем команд в системе UNIX. 3.2 Краткая сводка операций
Операции C++ подробно и систематически
описываются в #с.7; прочитайте, пожалуйста, этот
раздел. Здесь же приводится краткая сводка и
некоторые примеры. После каждой операции
приведено одно или более ее общеупотребительных
названий и пример ее использования. В этих
примерах имя_класса - это имя класса, член - имя
члена, объект - выражение, дающее в результате
объект класса, указатель - выражение, дающее в
результате указатель, выр - выражение, а lvalue -
выражение, денотирующее неконстантный объект.
Тип может быть совершенно произвольным именем
типа (со *, () и т.п.) только когда он стоит в скобках,
во всех остальных случаях существуют
ограничения. Сводка Операций (часть 1):: разрешение области видимости имя_класса :: член :: глобальное :: имя
В каждой отчерченной части находятся операции с одинаковым приоритетом. Операция имеет приоритет больше, чем операции из частей, расположенных ниже. Например: a+b*c означает a+(b*c), так как * имеет приоритет выше, чем +, а a+b-c означает (a+b)-c, поскольку + и - имеют одинаковый приоритет (и поскольку + левоассоциативен). Сводка Операций (часть 2)
3.2.1 Круглые скобкиСкобками синтаксис C++ злоупотребляет; количество способов их использования приводит в замешательство: они применяются для заключения в них параметров в вызовах функций, в них заключается тип в преобразовании типа (приведении к типу), в именах типов для обозначения функций, а также для разрешения конфликтов приоритетов. К счастью, последнее требуется не слишком часто, потому что уровни приоритета и правила ассоциативности определены таким образом, чтобы выражения "работали ожидаемым образом" (то есть, отражали наиболее привычный способ употребления). Например, значение if (i<=0 || max 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 Увеличение и уменьшение* 5Операция ++ используется для явного выражения
приращения вместо его неявного выражения с
помощью комбинации сложения и присваивания. По
определению ++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)
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 = 0 Вам сделали предупреждение! Заметьте, что тот, кто задает _new_handler, берет на себя заботу по проверке истощения памяти при каждом использовании new в программе (за исключением случая, когда пользователь задал отдельные подпрограммы для размещения объектов заданных типов, определяемых пользователем; см. #5.5.6). 3.3 Сводка операторов
Операторы C++ систематически и полностью изложены в #с.9, прочитайте, пожалуйста, этот раздел. А здесь приводится краткая сводка и некоторые примеры. Синтаксис оператора
оператор:
описание
{список_операторов opt}
выражение opt
if ( выражение ) опреатор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
while ( выражение ) оператор
do оператор while (выражение)
for ( оператор выражение opt ; выражение opt ) оператор
case константное_выражение : оператор
default : оператор
break ;
continue ;
return выражение opt ;
goto идентификатор ;
идентификатор : оператор
список_операторов:
оператор
оператор список_операторов
Заметьте, что описание является оператором, и что нет операторов присваивания и вызова процедуры. Присваивание и вызов функции обрабатываются как выражения. 3.3.1 ПроверкиПроверка значения может осуществляться или оператором if, или оператором switch: if ( выражение ) оператор if ( выражение ) оператор else оператор switch ( выражение ) оператор В C++ нет отдельного булевского типа. Операции сравнения == != <<=> >= возвращают целое 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 1 Заметьте, что метка case не подходит как метка для употребления в операторе goto: goto case 1; // синтаксическая ошибка 3.3.2 GotoC++ снабжен имеющим дурную репутацию оператором goto. goto идентификатор; идентификатор : оператор В общем, в программировании высокого уровня он имеет очень мало применений, но он может быть очень полезен, когда C++ программа генерируется программой, а не пишется непосредственно человеком. Например, операторы goto можно использовать в синтаксическом анализаторе, порождаемом генератором синтаксических анализаторов. Оператор goto может быть также важен в тех редких случаях, когда важна наилучшая эффективность, например, во внутреннем цикле какой- нибудь программы, работающей в реальном времени. Одно из немногих разумных применений состоит в выходе из вложенного цикла или переключателя (break лишь прекращает выполнение самого внутреннего охватывающего его цикла или переключателя). Например: for (int i = 0; i 3.4 Комментарии и ВыравниваниеПродуманное использование комментариев и
согласованное использование отступов может
сделать чтение и понимание программы намного
более приятным. Существует несколько различных
стилей согласованного использования отступов.
Автор не видит никаких серьезных оснований
предпочесть один другому (хотя как и у
большинства, у меня есть свои предпочтения).
Сказанное относится также и к стилю
комментариев. // переменная "v" должна быть инициализирована. // переменная "v" должна использоваться только функцией "f()". // вызвать функцию init() перед вызовом // любой другой функции в этом файле. // вызовите функцию очистки "cleanup()" в конце вашей программы. // не используйте функцию "wierd()". // функция "f()" получает два параметра. При правильном использовании C++ подобные комментарии как правило становятся ненужными. Чтобы предыдущие комментарии стали излишними, можно, например, использовать правила компоновки (#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 Упражнения
|