Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
146 Неверна и инструкция pia + 2=1; // ошибка Хотя pia+2 дает адрес ia[2], присвоить ему значение нельзя. Если мы хотим изменить элемент ia[2], то нужно воспользоваться операцией разыменования. Корректной будет следующая запись: *(pia + 2) = 1; // правильно Операция присваивания имеет результат – значение, которое было присвоено самому левому операнду. Например, результатом такой операции ival = 0; является 0, а результат ival = 3.14159; равен 3. Тип результата – int в обоих случаях. Это свойство операции присваивания можно использовать в подвыражениях. Например, следующий цикл } может быть переписан так: } Заметим, что вокруг выражения присваивания необходимы скобки, поскольку приоритет этой операции ниже, чем операции сравнения. Без скобок первым выполняется сравнение: next_char() != '\n' 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' ) { // сделать что-то ... } // ... С++ для начинающих 147 и его результат, true или false, присваивается переменной ch. (Приоритеты операций будут рассмотрены в разделе 4.13.) Аналогично несколько операций присваивания могут быть объединены, если это позволяют типы операндов. Например: // ... } Обеим переменным ival и jval присваивается значение 0. Следующий пример неправилен, потому что типы pval и ival различны, и неявное преобразование типов невозможно. Отметим, что 0 является допустимым значением для обеих переменных: // ... } Верен или нет приведенный ниже пример, мы сказать не можем, , поскольку определение jval в нем отсутствует: } Это правильно только в том случае, если переменная jval определена в программе ранее и имеет тип, приводимый к int. Обратите внимание: в этом случае мы присваиваем 0 значение jval и инициализируем ival. Для того чтобы инициализировать нулем обе переменные, мы должны написать: } В практике программирования часты случаи, когда к объекту применяется некоторая операция, а результат этой операции присваивается тому же объекту. Например: int main () { int ival, jval; ival = jval = 0; // правильно: присваивание 0 обеим переменным int main () { int ival; int *pval; ival = pval = 0; // ошибка: разные типы int main() { // ... int ival = jval = 0; // верно или нет? // ... int main() { // правильно: определение и инициализация int ival = 0, jval = 0; // ... С++ для начинающих 148 } Для более компактной записи С и С++ предоставляют составные операции присваивания. С использованием такого оператора данный пример можно переписать следующим образом: } Общий синтаксис составного оператора присваивания таков: a op= b; где op= является одним из десяти операторов: += -= *= /= %= <<= >>= &= ^= |= Запись a op= b в точности эквивалентна записи a = a op b. Упражнение 4.6 Найдите ошибку в данном примере. Исправьте запись. } Упражнение 4.7 Следующие выражения синтаксически правильны, однако скорее всего работают не так, как предполагал программист. Почему? Как их изменить? 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; int main() { float fval; int ival; int *pi; fval = ival = pi = 0; С++ для начинающих 149 (c) ival += ival + 1; 4.5. Операции инкремента и декремента Операции инкремента (++) и декремента (--) дают возможность компактной и удобной записи для изменения значения переменной на единицу. Чаще всего они используются при работе с массивами и коллекциями – для изменения величины индекса, указателя или итератора: } Выражение ix_vec++ является постфиксной формой оператора инкремента. Значение переменной ix_vec увеличивается после того, как ее текущее значение употреблено в качестве индекса. Например, на первой итерации цикла значение ix_vec равно 0. Именно это значение применяется как индекс массива ivec, после чего ix_vec увеличивается и становится равным 1, однако новое значение используется только на следующей итерации. Постфиксная форма операции декремента работает точно так же: текущее значение ix_ia берется в качестве индекса для ia, затем ix_ia уменьшается на 1. Существует и префиксная форма этих операторов. При использовании такой формы текущее значение сначала уменьшается или увеличивается, а затем используется новое значение. Если мы пишем: (a) if ( ptr = retrieve_pointer() != 0 ) (b) if ( ival = 1024 ) #include #include { int ia[10] = {0,1,2,3,4,5,6,7,8,9}; vector // неверно: ошибки с границами индексов в // обоих случаях int ix_vec = 0, ix_ia = 9; while ( ix_vec < 10 ) С++ для начинающих 150 ivec[ ++ix_vec ] = ia[ --ix_ia ]; значение ix_vec увеличивается на единицу и становится равным 1 до первого использования в качестве индекса. Аналогично ix_ia получает значение 8 при первом использовании. Для того чтобы наша программа работала правильно, мы должны скорректировать начальные значения переменных 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] или любую книгу по математике.) Например, можно написать: // правильно int ix_vec = -1, ix_ia = 8; while ( ix_vec < 10 ) С++ для начинающих 151 complex< double > с = a * b + a / b; Комплексные и арифметические типы разрешается смешивать в одном выражении: complex< double > complex_obj = a + 3.14159; Аналогично комплексные числа инициализируются арифметическим типом, и им может быть присвоено такое значение: complex_obj = dval; Или complex_obj = ival; Однако обратное неверно. Например, следующее выражение вызовет ошибку компиляции: double dval = complex_obj; Нужно явно указать, какую часть комплексного числа – вещественную или мнимую – мы хотим присвоить обычному числу. Класс комплексных чисел имеет две функции, возвращающих соответственно вещественную и мнимую части. Мы можем обращаться к ним, используя синтаксис доступа к членам класса: double im = complex_obj.imag(); или эквивалентный синтаксис вызова функции: #inc1ude // ... double dval = 3.14159; int ival = 3; // ошибка: нет неявного преобразования // в арифметический тип double re = complex_obj.real(); double re = real(complex_obj); С++ для начинающих 152 double im = imag(complex_obj); Класс комплексных чисел поддерживает четыре составных оператора присваивания: +=, -= , *= и /=. Таким образом, complex_obj += second_complex_obj; Поддерживается и ввод/вывод комплексных чисел. Оператор вывода печатает вещественную и мнимую части через запятую, в круглых скобках. Например, результат выполнения операторов вывода cout << complexO << " " << complex1 << endl; выглядит так: ( 3.14159, -2.171 ) ( 3.14159, 0.0 ) Оператор ввода понимает любой из следующих форматов: 3.14159 ( 3.14159 ) ( 3.14, -1.0 ) Кроме этих операций, класс комплексных чисел имеет следующие функции-члены: sqrt() , abs(), polar(), sin(), cos(), tan(), exp(), log(), log10() и pow(). Упражнение 4.9 Реализация стандартной библиотеки С++, доступная нам в момент написания книги, не поддерживает составных операций присваивания, если правый операнд не является комплексным числом. Например, подобная запись недопустима: complex_obj += 1; (Хотя согласно стандарту С++ такое выражение должно быть корректно, производители часто не успевают за стандартом.) Мы можем определить свой собственный оператор для реализации такой операции. Вот вариант функции, реализующий оператор сложения для complex : complex< double > complex0( 3.14159, -2.171 ); comp1ex< double > complex1( complexO.real() ); // допустимые форматы для ввода комплексного числа // 3.14159 ==> comp1ex( 3.14159 ); // ( 3.14159 ) ==> comp1ex( 3.14159 ); // ( 3.14, -1.0 ) ==> comp1ex( 3.14, -1.0 ); // может быть считано как // cin >> a >> b >> с // где a, b, с - комплексные числа С++ для начинающих 153 } (Это пример перегрузки оператора для определенного типа данных, детально рассмотренной в главе 15.) Используя этот пример, реализуйте три других составных оператора присваивания для типа complex } Упражнение 4.10 Стандарт С++ не специфицирует реализацию операций инкремента и декремента для комплексного числа. Однако их семантика вполне понятна: если уж мы можем написать: cval += 1; что означает увеличение на 1 вещественной части cval, то и операция инкремента выглядела бы вполне законно. Реализуйте эти операции для типа complex #include { return cval += complex #include #include // определения операций... int main() { complex< double > cval ( 4.0, 1.0 ); cout << cval << endl; cval += 1; cout << cval << endl; cval -= 1; cout << cval << endl; cval *= 2; cout << cval << endl; cout /= 2; cout << cval << endl; С++ для начинающих 154 } 4.7. Условное выражение Условное выражение, или оператор выбора, предоставляет возможность более компактной записи текстов, включающих инструкцию if-else. Например, вместо: else is_equal = false; можно употребить более компактную запись: bool is_equa1 = !strcmp( strl, str2 ) ? true : false; Условный оператор имеет следующий синтаксис: expr11 ? expr2 : expr3; Вычисляется выражение expr1. Если его значением является true, оценивается expr2, если false, то expr3. Данный фрагмент кода: { return ( ia < ib ) ? ia : ib; } эквивалентен } Приведенная ниже программа иллюстрирует использование условного оператора: #include #include // определения операций... int main() { complex< double > cval( 4.0, 1.0 ); cout << cval << endl; ++cva1; cout << cval << endl; bool is_equal; if (!strcmp(str1,str2)) is_equal = true; int min( int ia, int ib ) int min(int ia, int ib) { if (ia < ib) return ia; else return ib; С++ для начинающих 155 } Результатом работы программы будет: Большим из 10 и 20 является 20 Значение 10 четно. 4.8. Оператор sizeof Оператор sizeof возвращает размер в байтах объекта или типа данных. Синтаксис его таков: sizeof object; Результат имеет специальный тип size_t, который определен как typedef в заголовочном файле cstddef. Вот пример использования обеих форм оператора sizeof: #include { int i = 10, j = 20, k = 30; cout << " Большим из " << i << " и " << j << " является " << ( i > j ? i : j ) << end1; cout << " Значение " << i << ( i % 2 ? " нечетно." : " четно." ) << endl; /* условный оператор может быть вложенным, * но глубокая вложенность трудна для восприятия. * В данном примере max получает значение * максимальной из трех величин */ int max = ( (i > j) ? (( i > k) ? i : k) : ( j > k ) ? j : k); cout << " Большим из " << i << ", " << j << " и " << k << " является " << max << endl; sizeof ( type name ); sizeof ( object ); С++ для начинающих 156 size_t element_size = array_size / sizeof( int ); Применение sizeof к массиву дает количество байтов, занимаемых массивом, а не количество его элементов и не размер в байтах каждого из них. Так, например, в системах, где int хранится в 4 байтах, значением array_size будет 12. Применение sizeof к указателю дает размер самого указателя, а не объекта, на который он указывает: size_t pointer_size = sizeof ( pi ); Здесь значением pointer_size будет память под указатель в байтах (4 в 32-битных системах), а не массива ia. Вот пример программы, использующей оператор sizeof: #include // sizeof возвращает размер всего массива size_t array_size = sizeof ia; // sizeof возвращает размер типа int int *pi = new int[ 3 ]; С++ для начинающих 157 } Результатом работы программы будет: pi: 4 *pi: 4 st1: 12 st2: 12 ps: 4 *ps:12 short : 2 short* : 4 short& : 2 short[3] : 6 Из данного примера видно, что применение sizeof к указателю позволяет узнать размер памяти, необходимой для хранения адреса. Если же аргументом sizeof является ссылка, мы получим размер связанного с ней объекта. Гарантируется, что в любой реализации С++ размер типа char равен 1. size_t char_size = sizeof( char ); #include #include #include // ia = sizeof int; // ошибка ia = sizeof( int ); // правильно int *pi = new int[ 12 ]; cout << "pi: " << sizeof( pi ) << " *pi: " << sizeof( pi ) << endl; // sizeof строки не зависит от // ее реальной длины string stl( "foobar" ); string st2( "a mighty oak" ); string *ps = &stl; cout << " st1: " << sizeof( st1 ) << " st2: " << sizeof( st2 ) << " ps: sizeof( ps ) << " *ps: " << sizeof( *ps ) << endl; cout << "short :\t" << sizeof(short) << endl; cout << "shorf" :\t" << sizeof(short*) << endl; cout << "short& :\t" << sizeof(short&) << endl; cout << "short[3] :\t" << sizeof(short[3]) << endl; // char_size == 1 С++ для начинающих 158 Значение оператора sizeof вычисляется во время компиляции и считается константой. Оно может быть использовано везде, где требуется константное значение, в том числе в качестве размера встроенного массива. Например: int array[ sizeof( some_type_T )]; 4.9. Операторы new и delete Каждая программа во время работы получает определенное количество памяти, которую можно использовать. Такое выделение памяти под объекты во время выполнения называется динамическим, а сама память выделяется из хипа (heap). (Мы уже касались вопроса о динамическом выделении памяти в главе 1.) Напомним, что выделение памяти объекту производится с помощью оператора new, возвращающего указатель на вновь созданный объект того типа, который был ему задан. Например: int *pi = new int; размещает объект типа int в памяти и инициализирует указатель pi адресом этого объекта. Сам объект в таком случае не инициализируется, но это легко изменить: int *pi = new int( 1024 ); Можно динамически выделить память под массив: int *pia = new int[ 10 ]; Такая инструкция размещает в памяти массив встроенного типа из десяти элементов типа int . Для подобного массива нельзя задать список начальных значений его элементов при динамическом размещении. (Однако если размещается массив объектов типа класса, то для каждого из элементов вызывается конструктор по умолчанию.) Например: string *ps = new string; размещает в памяти один объект типа string, инициализирует ps его адресом и вызывает конструктор по умолчанию для вновь созданного объекта типа string. Аналогично string *psa = new string[10]; размещает в памяти массив из десяти элементов типа string, инициализирует psa его адресом и вызывает конструктор по умолчанию для каждого элемента массива. Объекты, размещаемые в памяти с помощью оператора new, не имеют собственного имени. Вместо этого возвращается указатель на безымянный объект, и все действия с этим объектом производятся посредством косвенной адресации. После использования объекта, созданного таким образом, мы должны явно освободить память, применив оператор delete к указателю на этот объект. (Попытка применить // правильно: константное выражение С++ для начинающих 159 оператор delete к указателю, не содержащему адрес объекта, полученного описанным способом, вызовет ошибку времени выполнения.) Например: delete pi; освобождает память, на которую указывает объект типа int, на который указывает pi. Аналогично delete ps; освобождает память, на которую указывает объект класса string, адрес которого содержится в ps. Перед уничтожением этого объекта вызывается деструктор. Выражение delete [] pia; освобождает память, отведенную под массив pia. При выполнении такой операции необходимо придерживаться указанного синтаксиса. (Об операциях new и delete мы еще поговорим в главе 8.) Упражнение 4.11 Какие из следующих выражений ошибочны? (a) vector (b) vector (c) vector (d) vector (e) vector (f) delete svec; (g) delete pvecl; (h) delete [] pvec2; (i) delete pvl; (j) delete pv2; 4.10. Оператор “запятая” Одно выражение может состоять из набора подвыражений, разделенных запятыми; такие подвыражения вычисляются слева направо. Конечным результатом будет результат самого правого из них. В следующем примере каждое из подвыражений условного оператора представляет собой список. Результатом первого подвыражения условного оператора является ix, второго – 0. |