Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

С++ для начинающих

141

Стандартная библиотека С++ имеет заголовочный файл limits, содержащий различную информацию о встроенных типах данных, в том числе и диапазоны значений для каждого типа. Заголовочные файлы climits и cfloat также содержат эту информацию. (Об использовании этих заголовочных файлов для того, чтобы избежать переполнения и потери значимости, см. главы 4 и 6 [PLAUGER92]).

Арифметика вещественных чисел создает еще одну проблему, связанную с округлением. Вещественное число представляется фиксированным количеством разрядов (разным для разных типов float, double и long double), и точность значения зависит от используемого типа данных. Но даже самый точный тип long double не может устранить ошибку округления. Вещественная величина в любом случае представляется с некоторой ограниченной точностью. (См. [SHAMPINE97] о проблемах округления вещественных чисел.)

Упражнение 4.1

double dvall = 10.0, dva12 = 3.0; int ivall = 10, iva12 = 3;

dvall / dva12;

Вчем разница между приведенными выражениями с операцией деления? ivall / iva12;

Упражнение 4.2

Напишите выражение, определяющее, четным или нечетным является данное целое число.

Упражнение 4.3

Найдите заголовочные файлы limits, climits и cfloat и посмотрите, что они содержат.

4.3. Операции сравнения и логические операции

Таблица 4.2. Операции сравнения и логические операции

Символ операции

Значение

Использование

 

 

 

!

Логическое НЕ

!expr

<

Меньше

expr1 < expr2

<=

Меньше или равно

expr1 <= expr2

>

Больше

expr1 > expr2

>=

Больше или равно

expr1 >= expr2

==

Равно

expr1 == expr2

!=

Не равно

expr1 != expr2

&&

Логическое И

expr1 && expr2

||

Логическое ИЛИ

expr1 || expr2

Примечание. Все операции в результате дают значение типа bool

Примечание [O.A.1]: Как должны быть оформлены ссылки на книги, указанные в библиографии? Пришлите ваши пожелания.

С++ для начинающих

142

Операции сравнения и логические операции в результате дают значение типа bool, то есть true или false. Если же такое выражение встречается в контексте, требующем целого значения, true преобразуется в 1, а false в 0. Вот фрагмент кода, подсчитывающего количество элементов вектора, меньших некоторого заданного

vector<int>::iterator iter = ivec.beg-in() ; while ( iter != ivec.end() ) {

//эквивалентно: e1em_cnt = e1em_cnt + (*iter < some_va1ue)

//значение true/false выражения *iter < some_va1ue

//превращается в 1 или 0

e1em_cnt += *iter < some_va1ue; ++iter;

значения:

}

Мы просто прибавляем результат операции меньшек счетчику. (Пара += обозначает составной оператор присваивания, который складывает операнд, стоящий слева, и операнд, стоящий справа. То же самое можно записать более компактно: elem_count = elem_count + n. Мы рассмотрим такие операторы в разделе 4.4.)

Логическое И (&&) возвращает истину только тогда, когда истинны оба операнда. Логическое ИЛИ (||) дает истину, если истинен хотя бы один из операндов. Гарантируется, что операнды вычисляются слева направо и вычисление заканчивается, как только результирующее значение становится известно. Что это значит? Пусть даны

expr1 && expr2

два выражения: expr1 || expr2

Если в первом из них expr1 равно false, значение всего выражения тоже будет равным false вне зависимости от значения expr2, которое даже не будет вычисляться. Во втором выражении expr2 не оценивается, если expr1 равно true, поскольку значение всего выражения равно true вне зависимости от expr2.

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

while ( ptr != О &&

ptr->va1ue < upperBound && ptr->va1ue >= 0 &&

notFound( ia[ ptr->va1ue ] ))

выражений в одном операторе AND:

{ ... }

Указатель с нулевым значением не указывает ни на какой объект, поэтому применение к нулевому указателю операции доступа к члену вызвало бы ошибку (ptr->value). Однако, если ptr равен 0, проверка на первом шаге прекращает дальнейшее вычисление подвыражений. Аналогично на втором и третьем шагах проверяется попадание величины

С++ для начинающих

143

ptr->value в нужный диапазон, и операция взятия индекса не применяется к массиву ia, если этот индекс неправилен.

Операция логического НЕ дает true, если ее единственный оператор равен false, и

bool found = false;

//пока элемент не найден

//и ptr указывает на объект (не 0) while ( ! found && ptr ) {

found = 1ookup( *ptr ); ++ptr;

наоборот. Например:

}

Подвыражение

! found

дает true, если переменная found равна false. Это более компактная запись для

found == false

Аналогично

if ( found )

эквивалентно более длинной записи

if ( found == true )

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

// Внимание! Порядок вычислений не определен! if ( ia[ index++ ] < ia[ index ] )

определен. Вот пример, где возможна подобная ошибка:

// поменять местами элементы

Программист предполагал, что левый операнд оценивается первым и сравниваться будут элементы ia[0] и ia[1]. Однако компилятор не гарантирует вычислений слева направо, и в таком случае элемент ia[0] может быть сравнен сам с собой. Гораздо лучше

if ( ia[ index ] < ia[ index+1 ] ) // поменять местами элементы

написать более понятный и машинно-независимый код:

++index;

С++ для начинающих

144

Еще один пример возможной ошибки. Мы хотели убедиться, что все три величины ival,

// Внимание! это не сравнение 3 переменных друг с другом if ( ival != jva1 != kva1 )

jval и kval различаются. Где мы промахнулись?

// do something ...

Значения 0, 1 и 0 дают в результате вычисления такого выражения true. Почему? Сначала проверяется ival != jval, а потом итог этой проверки (true/false

if ( ival != jva1 && ival != kva1 && jva1 != kva1 )

преобразованной к 1/0) сравнивается с kval. Мы должны были явно написать:

// сделать что-то ...

Упражнение 4.4

Найдите неправильные или непереносимые выражения, поясните. Как их можно

(a) ptr->iva1 != 0 (с) ptr != 0 && *ptr++

(e) vec[ iva1++ ] <= vec[ ival ];

изменить? (Заметим, что типы объектов не играют роли в данных примерах.)

(b) ival != jva1 < kva1 (d) iva1++ && ival

Упражнение 4.5

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

4.4. Операции присваивания

int ival = 1024;

Инициализация задает начальное значение переменной. Например: int *pi = 0;

В результате операции присваивания объект получает новое значение, при этом старое

ival = 2048;

пропадает:

pi = &iva1;

С++ для начинающих

145

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

Что происходит, если тип объекта не совпадает с типом значения, которое ему хотят присвоить? Допустим,

ival = 3.14159; // правильно?

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

случае вещественное значение 3.14159 преобразуется в целое значение 3, и это значение присваивается переменной ival.

Если неявное приведение типов невозможно, компилятор сигнализирует об ошибке:

pi = ival; // ошибка

Неявная трансформация типа int в тип указатель на int невозможна. (Набор допустимых неявных преобразований типов мы обсудим в разделе 4.14.)

Левый операнд операции присваивания должен быть l-значением. Очевидный пример неправильного присваивания:

1024 = ival; // ошибка

int value = 1024;

Возможно, имелось в виду следующее: value = ival; // правильно

Однако недостаточно потребовать, чтобы операнд слева от знака присваивания был l-

const int array_size = 8;

int ia[ array_size ] = { 0, 1, 2, 2, 3, 5, 8, 13 };

значением. Так, после определений int *pia = ia;

выражение

array_size = 512; // ошибка

ошибочно, хотя array_size и является l-значением: объявление array_size константой не дает возможности изменить его значение. Аналогично

ia = pia; // ошибка

ia тоже l-значение, но оно не может быть значением массива.

С++ для начинающих

146

Неверна и инструкция

pia + 2=1; // ошибка

Хотя pia+2 дает адрес ia[2], присвоить ему значение нельзя. Если мы хотим изменить элемент ia[2], то нужно воспользоваться операцией разыменования. Корректной будет следующая запись:

*(pia + 2) = 1; // правильно

Операция присваивания имеет результат значение, которое было присвоено самому левому операнду. Например, результатом такой операции

ival = 0;

является 0, а результат

ival = 3.14159;

равен 3. Тип результата int в обоих случаях. Это свойство операции присваивания

extern char next_char(); int main()

{

char ch = next_char(); while ( ch != '\n' ) {

// сделать что-то ...

ch = next_char();

}

// ...

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

}

extern char next_char(); int main()

{

char ch;

while (( ch = next_char() ) != '\n' ) { // сделать что-то ...

}

// ...

может быть переписан так:

}

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

next_char() != '\n'

С++ для начинающих

147

и его результат, true или false, присваивается переменной ch. (Приоритеты операций будут рассмотрены в разделе 4.13.)

Аналогично несколько операций присваивания могут быть объединены, если это

int main ()

{

int ival, jval;

ival = jval = 0; // правильно: присваивание 0 обеим переменным

позволяют типы операндов. Например:

// ...

}

Обеим переменным ival и jval присваивается значение 0. Следующий пример неправилен, потому что типы pval и ival различны, и неявное преобразование типов

int main ()

{

int ival; int *pval;

ival = pval = 0; // ошибка: разные типы

невозможно. Отметим, что 0 является допустимым значением для обеих переменных:

// ...

}

Верен или нет приведенный ниже пример, мы сказать не можем, , поскольку определение

int main()

{

// ...

int ival = jval = 0; // верно или нет?

// ...

jval в нем отсутствует:

}

Это правильно только в том случае, если переменная jval определена в программе ранее и имеет тип, приводимый к int. Обратите внимание: в этом случае мы присваиваем 0 значение jval и инициализируем ival. Для того чтобы инициализировать нулем обе

int main()

{

//правильно: определение и инициализация int ival = 0, jval = 0;

//...

переменные, мы должны написать:

}

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

С++ для начинающих

148

 

 

int arraySum( int ia[], int sz )

 

 

 

 

 

 

{

 

 

 

int sum = 0;

 

 

 

for ( int i = 0; i < sz; ++i )

 

 

 

sum = sum + ia[ i ];

 

 

 

return sum;

 

 

 

}

 

 

 

 

 

 

 

Для более компактной записи С и С++ предоставляют составные операции присваивания.

Сиспользованием такого оператора данный пример можно переписать следующим

int arraySum( int ia[], int sz )

{

int sum = 0;

for ( int i =0; i < sz; ++i )

// эквивалентно: sum = sum + ia[ i ]; sum += ia[ i ];

return sum;

образом:

}

Общий синтаксис составного оператора присваивания таков:

a op= b;

где op= является одним из десяти операторов:

+=

-=

*=

/=

%=

<<=

>>=

&=

^=

|=

Запись a op= b в точности эквивалентна записи a = a op b.

Упражнение 4.6

int main() { float fval; int ival; int *pi;

fval = ival = pi = 0;

Найдите ошибку в данном примере. Исправьте запись.

}

Упражнение 4.7

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