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

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

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

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

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

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

149

 

 

(a) if ( ptr = retrieve_pointer() != 0 )

 

 

 

 

 

 

(b) if ( ival = 1024 )

 

 

 

(c) ival += ival + 1;

 

 

 

 

 

 

 

 

4.5. Операции инкремента и декремента

 

Операции инкремента (++) и декремента (--) дают возможность компактной и удобной

 

записи для изменения значения переменной на единицу. Чаще всего они используются

 

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

 

 

 

#include <vector>

 

 

 

 

 

 

#include <cassert>

 

 

 

int main()

 

 

 

{

 

 

 

int ia[10] = {0,1,2,3,4,5,6,7,8,9};

 

 

 

vector<int> ivec( 10 );

 

 

 

int ix_vec = 0, ix_ia = 9;

 

 

 

while ( ix_vec < 10 )

 

 

 

ivec[ ix_vec++ ] = ia[ ix_ia-- ];

 

 

 

int *pia = &ia[9];

 

 

 

vector<int>::iterator iter = ivec.begin();

 

 

 

while ( iter != ivec.end() )

 

 

 

assert( *iter++ == *pia-- );

 

или итератора:

 

 

 

}

 

 

 

 

 

 

 

Выражение

 

 

 

ix_vec++

 

 

 

 

 

 

 

 

является постфиксной формой оператора инкремента. Значение переменной ix_vec

 

увеличивается после того, как ее текущее значение употреблено в качестве индекса.

 

Например, на первой итерации цикла значение ix_vec равно 0. Именно это значение

 

применяется как индекс массива ivec, после чего ix_vec увеличивается и становится

 

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

 

Постфиксная форма операции декремента работает точно так же: текущее значение ix_ia

 

берется в качестве индекса для ia, затем ix_ia уменьшается на 1.

 

Существует и префиксная форма этих операторов. При использовании такой формы

 

текущее значение сначала уменьшается или увеличивается, а затем используется новое

 

 

 

// неверно: ошибки с границами индексов в

 

 

 

 

 

 

// обоих случаях

 

 

 

int ix_vec = 0, ix_ia = 9;

 

 

 

while ( ix_vec < 10 )

 

значение. Если мы пишем:

 

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

150

 

ivec[ ++ix_vec ] = ia[ --ix_ia ];

 

 

 

 

 

 

значение ix_vec увеличивается на единицу и становится равным 1 до первого использования в качестве индекса. Аналогично ix_ia получает значение 8 при первом использовании. Для того чтобы наша программа работала правильно, мы должны

// правильно

int ix_vec = -1, ix_ia = 8; while ( ix_vec < 10 )

скорректировать начальные значения переменных ix_ivec и ix_ia: ivec[ ++ix_vec ] = ia[ --ix_ia ];

В качестве последнего примера рассмотрим понятие стека. Это фундаментальная абстракция компьютерного мира, позволяющая помещать и извлекать элементы в последовательности LIFO (last in, fist out – последним вошел, первым вышел). Стек реализует две основные операции поместить (push) и извлечь (pop).

Текущий свободный элемент называют вершиной стека. Операция push присваивает этому элементу новое значение , после чего вершина смещается вверх (становится на 1 больше). Пусть наш стек использует для хранения элементов вектор. Какую из форм операции увеличения следует применить? Сначала мы используем текущее значение, потом увеличиваем его. Это постфиксная форма:

stack[ top++ ] = value;

Что делает операция pop? Уменьшает значение вершины (текущая вершина показывает на пустой элемент), затем извлекает значение. Это префиксная форма операции уменьшения:

int value = stack[ --top ];

(Реализация класса stack приведена в конце этой главы. Стандартный класс stack рассматривается в разделе 6.16.)

Упражнение 4.8

Как вы думаете, почему язык программирования получил название С++, а не ++С?

4.6. Операции с комплексными числами

Класс комплексных чисел стандартной библиотеки С++ представляет собой хороший пример использования объектной модели. Благодаря перегруженным арифметическим операциям объекты этого класса используются так, как будто они принадлежат одному из встроенных типов данных. Более того, в подобных операциях могут одновременно принимать участие и переменные встроенного арифметического типа, и комплексные числа. (Отметим, что здесь мы не рассматриваем общие вопросы математики комплексных чисел. См. [PERSON68] или любую книгу по математике.) Например, можно написать:

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

151

#inc1ude <complex>

comp1ex< double > a; comp1ex< double > b;

// ...

complex< double > с = a * b + a / b;

Комплексные и арифметические типы разрешается смешивать в одном выражении:

complex< double > complex_obj = a + 3.14159;

Аналогично комплексные числа инициализируются арифметическим типом, и им может

double dval = 3.14159;

быть присвоено такое значение: complex_obj = dval;

int ival = 3;

Или

complex_obj = ival;

Однако обратное неверно. Например, следующее выражение вызовет ошибку

//ошибка: нет неявного преобразования

//в арифметический тип

компиляции:

double dval = complex_obj;

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

double re = complex_obj.real();

ним, используя синтаксис доступа к членам класса: double im = complex_obj.imag();

double re = real(complex_obj);

или эквивалентный синтаксис вызова функции: