<< Пред. стр. 10 (из 121) След. >>
Конечно, можно определить три константы для обозначения этих режимов:const int input = 1;
const int output = 2;
const int append = 3;
и пользоваться этими константами:
bool open_file( string file_name, int open_mode);
// ...
open_file( "Phoenix_and_the_Crane", append );
Подобное решение допустимо, но не вполне приемлемо, поскольку мы не можем гарантировать, что аргумент, передаваемый в функцию open_file() равен только 1, 2 или 3.
Использование перечислимого типа решает данную проблему. Когда мы пишем:
enum open_modes{ input = 1, output, append };
мы определяем новый тип open_modes. Допустимые значения для объекта этого типа ограничены набором 1, 2 и 3, причем каждое из указанных значений имеет мнемоническое имя. Мы можем использовать имя этого нового типа для определения как объекта данного типа, так и типа формальных параметров функции:
void open_file( string file_name, open_modes om );
input, output и append являются элементами перечисления. Набор элементов перечисления задает допустимое множество значений для объекта данного типа. Переменная типа open_modes (в нашем примере) инициализируется одним из этих значений, ей также может быть присвоено любое из них. Например:
open_file( "Phoenix and the Crane", append );
Попытка присвоить переменной данного типа значение, отличное от одного из элементов перечисления (или передать его параметром в функцию), вызовет ошибку компиляции. Даже если попробовать передать целое значение, соответствующее одному из элементов перечисления, мы все равно получим ошибку:
// ошибка: 1 не является элементом перечисления open_modes
open_file( "Jonah", 1 );
Есть способ определить переменную типа open_modes, присвоить ей значение одного из элементов перечисления и передать параметром в функцию:
open_modes om = input;
// ...
om = append;
open_file( "TailTell", om );
Однако получить имена таких элементов невозможно. Если мы напишем оператор вывода:
cout << input << " " << om << endl;
то все равно получим:
1 3
Эта проблема решается, если определить строковый массив, в котором элемент с индексом, равным значению элемента перечисления, будет содержать его имя. Имея такой массив, мы сможем написать:
cout << open_modes_table[ input ] << " "
<< open_modes_table[ om ] << endl
Будет выведено:
input append
Кроме того, нельзя перебрать все значения перечисления:
// не поддерживается
for ( open_modes iter = input; iter != append; ++inter )
// ...
Для определения перечисления служит ключевое слово enum, а имена элементов задаются в фигурных скобках, через запятую. По умолчанию первый из них равен 0, следующий – 1 и так далее. С помощью оператора присваивания это правило можно изменить. При этом каждый следующий элемент без явно указанного значения будет на 1 больше, чем элемент, идущий перед ним в списке. В нашем примере мы явно указали значение 1 для input, при этом output и append будут равны 2 и 3. Вот еще один пример:
// shape == 0, sphere == 1, cylinder == 2, polygon == 3
enum Forms{ share, spere, cylinder, polygon };
Целые значения, соответствующие разным элементам одного перечисления, не обязаны отличаться. Например:
// point2d == 2, point2w == 3, point3d == 3, point3w == 4
enum Points { point2d=2, point2w, point3d=3, point3w=4 };
Объект, тип которого – перечисление, можно определять, использовать в выражениях и передавать в функцию как аргумент. Подобный объект инициализируется только значением одного из элементов перечисления, и только такое значение ему присваивается – явно или как значение другого объекта того же типа. Даже соответствующие допустимым элементам перечисления целые значения не могут быть ему присвоены:
void mumble() {
Points pt3d = point3d; // правильно: pt2d == 3
// ошибка: pt3w инициализируется типом int
Points pt3w = 3;
// ошибка: polygon не входит в перечисление Points
pt3w = polygon;
// правильно: оба объекта типа Points
pt3w = pt3d;
}
Однако в арифметических выражениях перечисление может быть автоматически преобразовано в тип int. Например:
const int array_size = 1024;
// правильно: pt2w преобразуется int
int chunk_size = array_size * pt2w;
3.9. Тип “массив”
Мы уже касались массивов в разделе 2.1. Массив – это набор элементов одного типа, доступ к которым производится по индексу – порядковому номеру элемента в массиве. Например:
int ival;
определяет ival как переменную типа int, а инструкция
int ia[ 10 ];
задает массив из десяти объектов типа int. К каждому из этих объектов, или элементов массива, можно обратиться с помощью операции взятия индекса:
ival = ia[ 2 ];
присваивает переменной ival значение элемента массива ia с индексом 2. Аналогично
ia[ 7 ] = ival;
присваивает элементу с индексом 7 значение ival.
Определение массива состоит из спецификатора типа, имени массива и размера. Размер задает количество элементов массива (не менее 1) и заключается в квадратные скобки. Размер массива нужно знать уже на этапе компиляции, а следовательно, он должен быть константным выражением, хотя не обязательно задается литералом. Вот примеры правильных и неправильных определений массивов:
extern int get_size();
// buf_size и max_files константы
const int buf_size = 512, max_files = 20;
int staff_size = 27;
// правильно: константа
char input_buffer[ buf_size ];
// правильно: константное выражение: 20 - 3
char *fileTable[ max_files-3 ];
// ошибка: не константа
double salaries[ staff_size ];
// ошибка: не константное выражение
int test_scores[ get_size() ];
Объекты buf_size и max_files являются константами, поэтому определения массивов input_buffer и fileTable правильны. А вот staff_size – переменная (хотя и инициализированная константой 27), значит, salaries[staff_size] недопустимо. (Компилятор не в состоянии найти значение переменной staff_size в момент определения массива salaries.)
Выражение max_files-3 может быть вычислено на этапе компиляции, следовательно, определение массива fileTable[max_files-3] синтаксически правильно.
Нумерация элементов начинается с 0, поэтому для массива из 10 элементов правильным диапазоном индексов является не 1 – 10, а 0 – 9. Вот пример перебора всех элементов массива:
int main()
{
const int array_size = 10;
int ia[ array_size ];
for ( int ix = 0; ix < array_size; ++ ix )
ia[ ix ] = ix;
}
При определении массив можно явно инициализировать, перечислив значения его элементов в фигурных скобках, через запятую:
const int array_size = 3;
int ia[ array_size ] = { 0, 1, 2 };
Если мы явно указываем список значений, то можем не указывать размер массива: компилятор сам подсчитает количество элементов:
// массив размера 3
int ia[] = { 0, 1, 2 };
Когда явно указаны и размер, и список значений, возможны три варианта. При совпадении размера и количества значений все очевидно. Если список значений короче, чем заданный размер, оставшиеся элементы массива инициализируются нулями. Если же в списке больше значений, компилятор выводит сообщение об ошибке:
// ia ==> { 0, 1, 2, 0, 0 }
const int array_size = 5;
int ia[ array_size ] = { 0, 1, 2 };
Символьный массив может быть инициализирован не только списком символьных значений в фигурных скобках, но и строковым литералом. Однако между этими способами есть некоторая разница. Допустим,
const char cal[] = {'C', '+', '+' };
const char cal2[] = "C++";
Размерность массива ca1 равна 3, массива ca2 – 4 (в строковых литералах учитывается завершающий нулевой символ). Следующее определение вызовет ошибку компиляции:
// ошибка: строка "Daniel" состоит из 7 элементов
const char ch3[ 6 ] = "Daniel";
Массиву не может быть присвоено значение другого массива, недопустима и инициализация одного массива другим. Кроме того, не разрешается использовать массив ссылок. Вот примеры правильного и неправильного употребления массивов:
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;
}
Чтобы скопировать один массив в другой, придется проделать это для каждого элемента по отдельности:
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();
ia2[ get_index() ] = someVal;
Подчеркнем, что язык С++ не обеспечивает контроля индексов массива – ни на этапе компиляции, ни на этапе выполнения. Программист сам должен следить за тем, чтобы индекс не вышел за границы массива. Ошибки при работе с индексом достаточно распространены. К сожалению, не так уж трудно встретить примеры программ, которые компилируются и даже работают, но тем не менее содержат фатальные ошибки, рано или поздно приводящие к краху.
Упражнение 3.22
Какие из приведенных определений массивов содержат ошибки? Поясните.
(a) int ia[ buf_size ]; (d) int ia[ 2 * 7 - 14 ]
(b) int ia[ get_size() ]; (e) char st[ 11 ] = "fundamental";
(c) int ia[ 4 * 7 - 14 ];
Упражнение 3.23
Следующий фрагмент кода должен инициализировать каждый элемент массива значением индекса. Найдите допущенные ошибки:
int main() {
const int array_size = 10;
int ia[ array_size ];
for ( int ix = 1; ix <= array_size; ++ix )
ia[ ia ] = ix;
// ...
}
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,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 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;
}
Конструкция
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;
&ia[0]
Аналогично обратиться к значению первого элемента массива можно двумя способами:
// оба выражения возвращают первый элемент
*ia;
ia[0];
Чтобы взять адрес второго элемента массива, мы должны написать:
&ia[1];
Как мы уже упоминали раньше, выражение
ia+1;
также дает адрес второго элемента массива. Соответственно, его значение дают нам следующие два способа:
*(ia+1);
ia[1];
Отметим разницу в выражениях:
*ia+1
и
*(ia+1);
Операция разыменования имеет более высокий приоритет, чем операция сложения (о приоритетах операций говорится в разделе 4.13). Поэтому первое выражение сначала разыменовывает переменную ia и получает первый элемент массива, а затем прибавляет к нему 1. Второе же выражение доставляет значение второго элемента.
Проход по массиву можно осуществлять с помощью индекса, как мы делали это в предыдущем разделе, или с помощью указателей. Например:
#include
int main()
{
int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
int *pbegin = ia;
int *pend = ia + 9;
while ( pbegin != pend ) {
cout << *pbegin <<;
++pbegin;
}
Указатель pbegin инициализируется адресом первого элемента массива. Каждый проход по циклу увеличивает этот указатель на 1, что означает смещение его на следующий элемент. Как понять, где остановиться? В нашем примере мы определили второй указатель pend и инициализировали его адресом, следующим за последним элементом массива ia. Как только значение pbegin станет равным pend, мы узнаем, что массив кончился.
Перепишем эту программу так, чтобы начало и конец массива передавались параметрами в некую обобщенную функцию, которая умеет печатать массив любого размера:
#inc1ude
void ia_print( int *pbegin, int *pend )
{
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 );
}
Наша функция стала более универсальной, однако, она умеет работать только с массивами типа int. Есть способ снять и это ограничение: преобразовать данную функцию в шаблон (шаблоны были вкратце представлены в разделе 2.5):
#inc1ude
template
void print( elemType *pbegin, elemType *pend )
{
while ( pbegin != pend ) {
cout << *pbegin << ' ';
++pbegin;
}
}
Теперь мы можем вызывать нашу функцию print() для печати массивов любого типа:
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 );
}
Мы написали обобщенную функцию. Стандартная библиотека предоставляет набор обобщенных алгоритмов (мы уже упоминали об этом в разделе 3.4), реализованных подобным образом. Параметрами таких функций являются указатели на начало и конец массива, с которым они производят определенные действия. Вот, например, как выглядят вызовы обобщенного алгоритма сортировки:
#include
int main()
{
int ia[6] = { 107, 28, 3, 47, 104, 76 };
string sa[3] = { "piglet", "eeyore", "pooh" };
sort( ia, ia+6 );
sort( sa, sa+3 );
};
(Мы подробно остановимся на обобщенных алгоритмах в главе 12; в Приложении будут приведены примеры их использования.)
В стандартной библиотеке С++ содержится набор классов, которые инкапсулируют использование контейнеров и указателей. (Об этом говорилось в разделе 2.8.) В следующем разделе мы займемся стандартным контейнерным типом vector, являющимся объектно-ориентированной реализацией массива.
3.10. Класс vector
Использование класса vector (см. раздел 2.8) является альтернативой применению встроенных массивов. Этот класс предоставляет гораздо больше возможностей, поэтому его использование предпочтительней. Однако встречаются ситуации, когда не обойтись без массивов встроенного типа. Одна из таких ситуаций – обработка передаваемых программе параметров командной строки, о чем мы будем говорить в разделе 7.8. Класс vector, как и класс string, является частью стандартной библиотеки С++.
Для использования вектора необходимо включить заголовочный файл:
#include
Существуют два абсолютно разных подхода к использованию вектора, назовем их идиомой массива и идиомой STL. В первом случае объект класса vector используется точно так же, как массив встроенного типа. Определяется вектор заданной размерности:
vector< int > ivec( 10 );
что аналогично определению массива встроенного типа:
int ia[ 10 ];
Для доступа к отдельным элементам вектора применяется операция взятия индекса:
void simp1e_examp1e()
{
const int e1em_size = 10;
vector< int > ivec( e1em_size );