Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
113 Когда явно указаны и размер, и список значений, возможны три варианта. При совпадении размера и количества значений все очевидно. Если список значений короче, чем заданный размер, оставшиеся элементы массива инициализируются нулями. Если же в списке больше значений, компилятор выводит сообщение об ошибке: int ia[ array_size ] = { 0, 1, 2 }; Символьный массив может быть инициализирован не только списком символьных значений в фигурных скобках, но и строковым литералом. Однако между этими способами есть некоторая разница. Допустим, const char cal2[] = "C++"; Размерность массива ca1 равна 3, массива ca2 – 4 (в строковых литералах учитывается завершающий нулевой символ). Следующее определение вызовет ошибку компиляции: const char ch3[ 6 ] = "Daniel"; Массиву не может быть присвоено значение другого массива, недопустима и инициализация одного массива другим. Кроме того, не разрешается использовать массив ссылок. Вот примеры правильного и неправильного употребления массивов: } Чтобы скопировать один массив в другой, придется проделать это для каждого элемента по отдельности: // ia ==> { 0, 1, 2, 0, 0 } const int array_size = 5; const char cal[] = {'C', '+', '+' }; // ошибка: строка "Daniel" состоит из 7 элементов const int array_size = 3; int ix, jx, kx; // правильно: массив указателей типа int* int *iar [] = { &ix, &jx, &kx }; // error: массивы ссылок недопустимы int &iar[] = { ix, jx, kx }; int main() { int ia3{ array_size ]; // правильно // ошибка: встроенные массивы нельзя копировать ia3 = ia; return 0; С++ для начинающих 114 } В качестве индекса массива может выступать любое выражение, дающее результат целого типа. Например: ia2[ get_index() ] = someVal; Подчеркнем, что язык С++ не обеспечивает контроля индексов массива – ни на этапе компиляции, ни на этапе выполнения. Программист сам должен следить за тем, чтобы индекс не вышел за границы массива. Ошибки при работе с индексом достаточно распространены. К сожалению, не так уж трудно встретить примеры программ, которые компилируются и даже работают, но тем не менее содержат фатальные ошибки, рано или поздно приводящие к краху. Упражнение 3.22 Какие из приведенных определений массивов содержат ошибки? Поясните. (c) int ia[ 4 * 7 - 14 ]; Упражнение 3.23 Следующий фрагмент кода должен инициализировать каждый элемент массива значением индекса. Найдите допущенные ошибки: } const int array_size = 7; int ia1[] = { 0, 1, 2, 3, 4, 5, 6 }; int main() { int ia3[ array_size ]; for ( int ix = 0; ix < array_size; ++ix ) ia2[ ix ] = ia1[ ix ]; return 0; int someVal, get_index(); (a) int ia[ buf_size ]; (d) int ia[ 2 * 7 - 14 ] (b) int ia[ get_size() ]; (e) char st[ 11 ] = "fundamental"; int main() { const int array_size = 10; int ia[ array_size ]; for ( int ix = 1; ix <= array_size; ++ix ) ia[ ia ] = ix; // ... С++ для начинающих 115 3.9.1. Многомерные массивы В С++ есть возможность использовать многомерные массивы, при объявлении которых необходимо указать правую границу каждого измерения в отдельных квадратных скобках. Вот определение двумерного массива: int ia[ 4 ][ 3 ]; Первая величина (4) задает количество строк, вторая (3) – количество столбцов. Объект ia определен как массив из четырех строк по три элемента в каждой. Многомерные массивы тоже могут быть инициализированы: }; Внутренние фигурные скобки, разбивающие список значений на строки, необязательны и используются, как правило, для удобства чтения кода. Приведенная ниже инициализация в точности соответствует предыдущему примеру, хотя менее понятна: int ia[4][3] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; Следующее определение инициализирует только первые элементы каждой строки. Оставшиеся элементы будут равны нулю: int ia[ 4 ][ 3 ] = { {0}, {3}, {6}, {9} }; Если же опустить внутренние фигурные скобки, результат окажется совершенно иным. Все три элемента первой строки и первый элемент второй получат указанное значение, а остальные будут неявно инициализированы 0. int ia[ 4 ][ 3 ] = { 0, 3, 6, 9 }; При обращении к элементам многомерного массива необходимо использовать индексы для каждого измерения (они заключаются в квадратные скобки). Так выглядит инициализация двумерного массива с помощью вложенных циклов: } int ia[ 4 ][ 3 ] = { { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }, { 9, 10, 11 } int main() { const int rowSize = 4; const int colSize = 3; int ia[ rowSize ][ colSize ]; for ( int = 0; i < rowSize; ++i ) for ( int j = 0; j < colSize; ++j ) ia[ i ][ j ] = i + j j; С++ для начинающих 116 Конструкция ia[ 1, 2 ] является допустимой с точки зрения синтаксиса С++, однако означает совсем не то, чего ждет неопытный программист. Это отнюдь не объявление двумерного массива 1 на 2. Агрегат в квадратных скобках – это список выражений через запятую, результатом которого будет последнее значение 2 (см. оператор “запятая” в разделе 4.2). Поэтому объявление ia[1,2] эквивалентно ia[2]. Это еще одна возможность допустить ошибку. 3.9.2. Взаимосвязь массивов и указателей Если мы имеем определение массива: int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; то что означает простое указание его имени в программе? ia; Использование идентификатора массива в программе эквивалентно указанию адреса его первого элемента: &ia[0] Аналогично обратиться к значению первого элемента массива можно двумя способами: ia[0]; Чтобы взять адрес второго элемента массива, мы должны написать: &ia[1]; Как мы уже упоминали раньше, выражение ia+1; также дает адрес второго элемента массива. Соответственно, его значение дают нам следующие два способа: ia[1]; ia; // оба выражения возвращают первый элемент *ia; *(ia+1); С++ для начинающих 117 Отметим разницу в выражениях: *ia+1 и *(ia+1); Операция разыменования имеет более высокий приоритет, чем операция сложения (о приоритетах операций говорится в разделе 4.13). Поэтому первое выражение сначала разыменовывает переменную ia и получает первый элемент массива, а затем прибавляет к нему 1. Второе же выражение доставляет значение второго элемента. Проход по массиву можно осуществлять с помощью индекса, как мы делали это в предыдущем разделе, или с помощью указателей. Например: int *pend = ia + 9; } Указатель pbegin инициализируется адресом первого элемента массива. Каждый проход по циклу увеличивает этот указатель на 1, что означает смещение его на следующий элемент. Как понять, где остановиться? В нашем примере мы определили второй указатель pend и инициализировали его адресом, следующим за последним элементом массива ia. Как только значение pbegin станет равным pend, мы узнаем, что массив кончился. Перепишем эту программу так, чтобы начало и конец массива передавались параметрами в некую обобщенную функцию, которая умеет печатать массив любого размера: } #include { int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; int *pbegin = ia; while ( pbegin != pend ) { cout << *pbegin <<; ++pbegin; #inc1ude { while ( pbegin != pend ) { cout << *pbegin << ' '; ++pbegin; } } int main() { int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; ia_print( ia, ia + 9 ); С++ для начинающих 118 Наша функция стала более универсальной, однако, она умеет работать только с массивами типа int. Есть способ снять и это ограничение: преобразовать данную функцию в шаблон (шаблоны были вкратце представлены в разделе 2.5): } Теперь мы можем вызывать нашу функцию print() для печати массивов любого типа: } Мы написали обобщенную функцию. Стандартная библиотека предоставляет набор обобщенных алгоритмов (мы уже упоминали об этом в разделе 3.4), реализованных подобным образом. Параметрами таких функций являются указатели на начало и конец массива, с которым они производят определенные действия. Вот, например, как выглядят вызовы обобщенного алгоритма сортировки: }; (Мы подробно остановимся на обобщенных алгоритмах в главе 12; в Приложении будут приведены примеры их использования.) В стандартной библиотеке С++ содержится набор классов, которые инкапсулируют использование контейнеров и указателей. (Об этом говорилось в разделе 2.8.) В следующем разделе мы займемся стандартным контейнерным типом vector, являющимся объектно-ориентированной реализацией массива. #inc1ude { while ( pbegin != pend ) { cout << *pbegin << ' '; ++pbegin; } int main() { int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; double da[4] = { 3.14, 6.28, 12.56, 25.12 }; string sa[3] = { "piglet", "eeyore", "pooh" }; print( ia, ia+9 ); print( da, da+4 ); print( sa, sa+3 ); #include { int ia[6] = { 107, 28, 3, 47, 104, 76 }; string sa[3] = { "piglet", "eeyore", "pooh" }; sort( ia, ia+6 ); sort( sa, sa+3 ); С++ для начинающих 119 3.10. Класс vector Использование класса vector (см. раздел 2.8) является альтернативой применению встроенных массивов. Этот класс предоставляет гораздо больше возможностей, поэтому его использование предпочтительней. Однако встречаются ситуации, когда не обойтись без массивов встроенного типа. Одна из таких ситуаций – обработка передаваемых программе параметров командной строки, о чем мы будем говорить в разделе 7.8. Класс vector , как и класс string, является частью стандартной библиотеки С++. Для использования вектора необходимо включить заголовочный файл: #include Существуют два абсолютно разных подхода к использованию вектора, назовем их идиомой массива и идиомой STL. В первом случае объект класса vector используется точно так же, как массив встроенного типа. Определяется вектор заданной размерности: vector< int > ivec( 10 ); что аналогично определению массива встроенного типа: int ia[ 10 ]; Для доступа к отдельным элементам вектора применяется операция взятия индекса: } Мы можем узнать размерность вектора, используя функцию size(), и проверить, пуст ли вектор, с помощью функции empty(). Например: } Элементы вектора инициализируются значениями по умолчанию. Для числовых типов и указателей таким значением является 0. Если в качестве элементов выступают объекты void simp1e_examp1e() { const int e1em_size = 10; vector< int > ivec( e1em_size ); int ia[ e1em_size ]; for ( int ix = 0; ix < e1em_size; ++ix ) ia[ ix ] = ivec[ ix ]; // ... void print_vector( vector { if ( ivec.empty() ) return; for ( int ix=0; ix< ivec.size(); ++ix ) cout << ivec[ ix ] << ' '; С++ для начинающих 120 класса, то инициатор для них задается конструктором по умолчанию (см. раздел 2.3). Однако инициатор можно задать и явно, используя форму: vector< int > ivec( 10, -1 ); Все десять элементов вектора будут равны -1. Массив встроенного типа можно явно инициализировать списком: int ia[ 6 ] = { -2, -1, О, 1, 2, 1024 }; Для объекта класса vector аналогичное действие невозможно. Однако такой объект может быть инициализирован с помощью массива встроенного типа: vector< int > ivec( ia, ia+6 ); Конструктору вектора ivec передаются два указателя – указатель на начало массива ia и на элемент, следующий за последним. В качестве списка начальных значений допустимо указать не весь массив, а некоторый его диапазон: vector< int > ivec( &ia[ 2 ], &ia[ 5 ] ); Еще одним отличием вектора от массива встроенного типа является возможность инициализации одного объекта типа vector другим и использования операции присваивания для копирования объектов. Например: } Говоря об идиоме STL 6 , мы подразумеваем совсем другой подход к использованию вектора. Вместо того чтобы сразу задать нужный размер, мы определяем пустой вектор: vector< string > text; 6 STL расшифровывается как Standard Template Library. До появления стандартной библиотеки С++ классы vector, string и другие, а также обобщенные алгоритмы входили в отдельную библиотеку с названием STL. // 6 элементов ia копируются в ivec // копируются 3 элемента: ia[2], ia[3], ia[4] vector< string > svec; void init_and_assign() { // один вектор инициализируется другим vector< string > user_names( svec ); // ... // один вектор копируется в другой svec = user_names; С++ для начинающих 121 Затем добавляем к нему элементы при помощи различных функций. Например, функция push_back() вставляет элемент в конец вектора. Вот фрагмент кода, считывающего последовательность строк из стандартного ввода и добавляющего их в вектор: } Хотя мы можем использовать операцию взятия индекса для перебора элементов вектора: cout << endl; более типичным в рамках данной идиомы будет использование итераторов: cout << endl; Итератор – это класс стандартной библиотеки, фактически являющийся указателем на элемент массива. Выражение *it; разыменовывает итератор и дает сам элемент вектора. Инструкция ++it; сдвигает указатель на следующий элемент. Не нужно смешивать эти два подхода. Если следовать идиоме STL при определении пустого вектора: vector У нас еще нет ни одного элемента вектора ivec; количество элементов выясняется с помощью функции size(). string word; while ( cin >> word ) { text.push_back( word ); // ... cout << " считаны слова: \n"; for ( int ix =0; ix < text.size(); ++ix ) cout << text[ ix ] << ' '; cout << " считаны слова: \n"; for ( vector С++ для начинающих 122 Можно допустить и противоположную ошибку. Если мы определили вектор некоторого размера, например: vector Имелась в виду инициализация вектора ivec значениями элементов ia, вместо чего получился вектор ivec размера 14. Следуя идиоме STL, можно не только добавлять, но и удалять элементы вектора. (Все это мы рассмотрим подробно и с примерами в главе 6.) Упражнение 3.24 Имеются ли ошибки в следующих определениях? (e) vector< string > svec( 10, string( "null" )); Упражнение 3.25 Реализуйте следующую функцию: const vector Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров “хвост” более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main() , обращающуюся к is_equal(). 3.11. Класс complex Класс комплексных чисел complex – еще один класс из стандартной библиотеки. Как обычно, для его использования нужно включить заголовочный файл: const int size = 7; int ia[ size ] = { 0, 1, 1, 2, 3, 5, 8 }; vector< int > ivec( size ); for ( int ix = 0; ix < size; ++ix ) int ia[ 7 ] = { 0, 1, 1, 2, 3, 5, 8 }; (a) vector< vector< int > > ivec; (b) vector< int > ivec = { 0, 1, 1, 2, 3, 5, 8 }; (c) vector< int > ivec( ia, ia+7 ); (d) vector< string > svec = ivec; bool is_equa1( const int*ia, int ia_size, С++ для начинающих 123 #include Комплексное число состоит из двух частей – вещественной и мнимой. Мнимая часть представляет собой квадратный корень из отрицательного числа. Комплексное число принято записывать в виде 2 + 3i где 2 – действительная часть, а 3i – мнимая. Вот примеры определений объектов типа complex : comp1ex< double > purei2( purei ); Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также определить массив элементов типа complex: }; Вот как определяются указатель и ссылка на комплексное число: complex< double > &ref = *ptr; Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.) 3.12. Директива typedef Директива typedef позволяет задать синоним для встроенного либо пользовательского типа данных. Например: // чисто мнимое число: 0 + 7-i comp1ex< double > purei( 0, 7 ); // мнимая часть равна 0: 3 + Oi comp1ex< float > rea1_num( 3 ); // и вещественная, и мнимая часть равны 0: 0 + 0-i comp1ex< long double > zero; // инициализация одного комплексного числа другим complex< double > conjugate[ 2 ] = { complex< double >( 2, 3 ), complex< double >( 2, -3 ) complex< double > *ptr = &conjugate[0]; С++ для начинающих 124 typedef int *Pint; Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов: Pint table [ 10 ]; Эта директива начинается с ключевого слова typedef, за которым идет спецификатор типа, и заканчивается идентификатором, который становится синонимом для указанного типа. Для чего используются имена, определенные с помощью директивы typedef? Применяя мнемонические имена для типов данных, можно сделать программу более легкой для восприятия. Кроме того, принято употреблять такие имена для сложных составных типов, в противном случае воспринимаемых с трудом (см. пример в разделе 3.14), для объявления указателей на функции и функции-члены класса (см. раздел 13.6). Ниже приводится пример вопроса, на который почти все дают неверный ответ. Ошибка вызвана непониманием директивы typedef как простой текстовой макроподстановки. Дано определение: typedef char *cstring; Каков тип переменной cstr в следующем объявлении: extern const cstring cstr; Ответ, который кажется очевидным: const char *cstr Однако это неверно. Спецификатор const относится к cstr, поэтому правильный ответ – константный указатель на char: char * |