Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
101 где cptr – указатель на объект типа const double. Тонкость заключается в том, что сам указатель – не константа, а значит, мы можем изменять его значение. Например: *pc = 3.14159; // ошибка Адрес константного объекта присваивается только указателю на константу. Вместе с тем, такому указателю может быть присвоен и адрес обычной переменной: pc = &dval; Константный указатель не позволяет изменять адресуемый им объект с помощью косвенной адресации. Хотя dval в примере выше и не является константой, компилятор не допустит изменения переменной dval через pc. (Опять-таки потому, что он не в состоянии определить, адрес какого объекта может содержать указатель в произвольный момент выполнения программы.) В реальных программах указатели на константы чаще всего употребляются как формальные параметры функций. Их использование дает гарантию, что объект, переданный в функцию в качестве фактического аргумента, не будет изменен этой функцией. Например: int strcmp( const char *str1, const char *str2 ); (Мы еще поговорим об указателях на константы в главе 7, когда речь пойдет о функциях.) Существуют и константные указатели. (Обратите внимание на разницу между константным указателем и указателем на константу!). Константный указатель может адресовать как константу, так и переменную. Например: int errNumb = 0; int *const currErr = &errNumb; Здесь curErr – константный указатель на неконстантный объект. Это значит, что мы не можем присвоить ему адрес другого объекта, хотя сам объект допускает модификацию. Вот как мог бы быть использован указатель curErr: const double *pc = 0; const double minWage = 9.60; // правильно: не можем изменять minWage с помощью pc pc = &minWage; double dval = 3.14; // правильно: не можем изменять minWage с помощью pc // хотя dval и не константа pc = &dval; // правильно dval = 3.14159; // правильно // В реальных программах указатели на константы чаще всего // употребляются как формальные параметры функций С++ для начинающих 102 if ( *curErr ) { } Попытка присвоить значение константному указателю вызовет ошибку компиляции: curErr = &myErNumb; // ошибка Константный указатель на константу является объединением двух рассмотренных случаев. const double *const pi_ptr = π Ни значение объекта, на который указывает pi_ptr, ни значение самого указателя не может быть изменено в программе. Упражнение 3.16 Объясните значение следующих пяти определений. Есть ли среди них ошибочные? (c) const int *pic; Упражнение 3.17 Какие из приведенных определений правильны? Почему? (e) const int *const cpic = ⁣ Упражнение 3.18 Используя определения из предыдущего упражнения, укажите правильные операторы присваивания. Объясните. (c) cpi = pic; (f) ic = *cpic; do_something(); errorHandler(); *curErr = 0; // правильно: обнулим значение errNumb const double pi = 3.14159; (a) int i; (d) int *const cpi; (b) const int ic; (e) const int *const cpic; (a) int i = -1; (b) const int ic = i; (c) const int *pic = ⁣ (d) int *const cpi = ⁣ (a) i = ic; (d) pic = cpic; (b) pic = ⁣ (i) cpic = ⁣ С++ для начинающих 103 3.6. Ссылочный тип Ссылочный тип, иногда называемый псевдонимом, служит для задания объекту дополнительного имени. Ссылка позволяет косвенно манипулировать объектом, точно так же, как это делается с помощью указателя. Однако эта косвенная манипуляция не требует специального синтаксиса, необходимого для указателей. Обычно ссылки употребляются как формальные параметры функций. В этом разделе мы рассмотрим самостоятельное использование объектов ссылочного типа. Ссылочный тип обозначается указанием оператора взятия адреса (&) перед именем переменной. Ссылка должна быть инициализирована. Например: int &refVal2; Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель: int *&ptrVal2 = pi; Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту, которую адресует refVal. refVal = min_val; Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например: refVal += 2; int ival = 1024; // правильно: refVal - ссылка на ival int &refVal = ival; // ошибка: ссылка должна быть инициализирована int ival = 1024; // ошибка: refVal имеет тип int, а не int* int &refVal = &ival; int *pi = &ival; // правильно: ptrVal - ссылка на указатель int min_val = 0; // ival получает значение min_val, // а не refVal меняет значение на min_val С++ для начинающих 104 прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично int ii = refVal; присваивает ii текущее значение ival, int *pi = &refVal; инициализирует pi адресом ival. Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например: int &rval3 = ival3, &rval4 = ival2; Константная ссылка может быть инициализирована объектом другого типа (если, конечно, существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например: const double &dr = dval + 1.0; Если бы мы не указали спецификатор const, все три определения ссылок вызвали бы ошибку компиляции. Однако, причина, по которой компилятор не пропускает таких определений, неясна. Попробуем разобраться. Для литералов это более или менее понятно: у нас не должно быть возможности косвенно поменять значение литерала, используя указатели или ссылки. Что касается объектов другого типа, то компилятор преобразует исходный объект в некоторый вспомогательный. Например, если мы пишем: const int &ri = dval; то компилятор преобразует это примерно так: // определено два объекта типа int int ival = 1024, ival2 = 2048; // определена одна ссылка и один объект int &rval = ival, rval2 = ival2; // определен один объект, один указатель и одна ссылка int inal3 = 1024, *pi = ival3, &ri = ival3; // определены две ссылки double dval = 3.14159; // верно только для константных ссылок const int &ir = 1024; const int &ir2 = dval; double dval = 1024; С++ для начинающих 105 const int &ri = temp; Если бы мы могли присвоить новое значение ссылке ri, мы бы реально изменили не dval , а temp. Значение dval осталось бы тем же, что совершенно неочевидно для программиста. Поэтому компилятор запрещает такие действия, и единственная возможность проинициализировать ссылку объектом другого типа – объявить ее как const Вот еще один пример ссылки, который трудно понять с первого раза. Мы хотим определить ссылку на адрес константного объекта, но наш первый вариант вызывает ошибку компиляции: int *&pi_ref = &ival; Попытка исправить дело добавлением спецификатора const тоже не проходит: const int *&pi_ref = &ival; В чем причина? Внимательно прочитав определение, мы увидим, что pi_ref является ссылкой на константный указатель на объект типа int. А нам нужен неконстантный указатель на константный объект, поэтому правильной будет следующая запись: int *const &piref = &ival; Между ссылкой и указателем существуют два основных отличия. Во-первых, ссылка обязательно должна быть инициализирована в месте своего определения. Во-вторых, всякое изменение ссылки преобразует не ее, а тот объект, на который она ссылается. Рассмотрим на примерах. Если мы пишем: int *pi = 0; мы инициализируем указатель pi нулевым значением, а это значит, что pi не указывает ни на какой объект. В то же время запись const int &ri = 0; означает примерно следующее: int temp = dval; const int ival = 1024; // ошибка: нужна константная ссылка const int ival = 1024; // все равно ошибка const int ival = 1024; // правильно С++ для начинающих 106 const int &ri = temp; Что касается операции присваивания, то в следующем примере: pi = pi2; переменная ival, на которую указывает pi, остается неизменной, а pi получает значение адреса переменной ival2. И pi, и pi2 и теперь указывают на один и тот же объект ival2. Если же мы работаем со ссылками: ri = ri2; то само значение ival меняется, но ссылка ri по-прежнему адресует ival. В реальных С++ программах ссылки редко используются как самостоятельные объекты, обычно они употребляются в качестве формальных параметров функций. Например: Matrix operator+( const Matrix&, const Matrix& ); Как соотносятся самостоятельные объекты-ссылки и ссылки-параметры? Если мы пишем: while (get_next_value( ival )) ... это равносильно следующему определению ссылки внутри функции: int &next_value = ival; (Подробнее использование ссылок в качестве формальных параметров функций рассматривается в главе 7.) Упражнение 3.19 int temp = 0; int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; int &ri = ival, &ri2 = ival2; // пример использования ссылок // Значение возвращается в параметре next_value bool get_next_value( int &next_value ); // перегруженный оператор int ival; С++ для начинающих 107 Есть ли ошибки в данных определениях? Поясните. Как бы вы их исправили? (i) const int &ival2 = 1; (j) const int &*prval2 = &ival; Упражнение 3.20 Если ли среди нижеследующих операций присваивания ошибочные (используются определения из предыдущего упражнения)? (d) *prval2 = ival2; Упражнение 3.21 Найдите ошибки в приведенных инструкциях: pi = &rval; 3.7. Тип bool Объект типа bool может принимать одно из двух значений: true и false. Например: else cout << " нет, наше слово не встретилось.\n"; Хотя bool относится к одному из целых типов, он не может быть объявлен как signed, unsigned , short или long, поэтому приведенное определение ошибочно: (a) int ival = 1.01; (b) int &rval1 = 1.01; (c) int &rval2 = ival; (d) int &rval3 = &ival; (e) int *pi = &ival; (f) int &rval4 = pi; (g) int &rval5 = pi*; (h) int &*prval1 = pi; (a) rval1 = 3.14159; (b) prval1 = prval2; (c) prval2 = rval1; (a) int ival = 0; const int *pi = 0; const int &ri = 0; (b) pi = &ival; ri = &ival; // инициализация строки string search_word = get_word(); // инициализация переменной found bool found = false; string next_word; while ( cin >> next_word ) if ( next_word == search_word ) found = true; // ... // сокращенная запись: if ( found == true ) if ( found ) cout << "ok, мы нашли слово\n"; С++ для начинающих 108 short bool found = false; Объекты типа bool неявно преобразуются в тип int. Значение true превращается в 1, а false – в 0. Например: } Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа bool. При этом 0 интерпретируется как false, а все остальное как true: // правильно: found == true 3.8. Перечисления Нередко приходится определять переменную, которая принимает значения из некоего набора. Скажем, файл открывают в любом из трех режимов: для чтения, для записи, для добавления. Конечно, можно определить три константы для обозначения этих режимов: const int append = 3; и пользоваться этими константами: // ошибка bool found = false; int occurrence_count = 0; while ( /* mumble */ ) { found = look_for( /* something */ ); // значение found преобразуется в 0 или 1 occurrence_count += found; // возвращает количество вхождений extern int find( const string& ); bool found = false; if ( found = find( "rosebud" )) // правильно: found == true // возвращает указатель на элемент extern int* find( int value ); if ( found = find( 1024 )) const int input = 1; const int output = 2; С++ для начинающих 109 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 ); Попытка присвоить переменной данного типа значение, отличное от одного из элементов перечисления (или передать его параметром в функцию), вызовет ошибку компиляции. Даже если попробовать передать целое значение, соответствующее одному из элементов перечисления, мы все равно получим ошибку: open_file( "Jonah", 1 ); Есть способ определить переменную типа open_modes, присвоить ей значение одного из элементов перечисления и передать параметром в функцию: om = append; open_file( "TailTell", om ); Однако получить имена таких элементов невозможно. Если мы напишем оператор вывода: cout << input << " " << om << endl; то все равно получим: bool open_file( string file_name, int open_mode); // ... // ошибка: 1 не является элементом перечисления open_modes open_modes om = input; // ... С++ для начинающих 110 1 3 Эта проблема решается, если определить строковый массив, в котором элемент с индексом, равным значению элемента перечисления, будет содержать его имя. Имея такой массив, мы сможем написать: << open_modes_table[ om ] << endl Будет выведено: input append Кроме того, нельзя перебрать все значения перечисления: // ... Для определения перечисления служит ключевое слово enum, а имена элементов задаются в фигурных скобках, через запятую. По умолчанию первый из них равен 0, следующий – 1 и так далее. С помощью оператора присваивания это правило можно изменить. При этом каждый следующий элемент без явно указанного значения будет на 1 больше, чем элемент, идущий перед ним в списке. В нашем примере мы явно указали значение 1 для input , при этом output и append будут равны 2 и 3. Вот еще один пример: enum Forms{ share, spere, cylinder, polygon }; Целые значения, соответствующие разным элементам одного перечисления, не обязаны отличаться. Например: enum Points { point2d=2, point2w, point3d=3, point3w=4 }; Объект, тип которого – перечисление, можно определять, использовать в выражениях и передавать в функцию как аргумент. Подобный объект инициализируется только значением одного из элементов перечисления, и только такое значение ему присваивается – явно или как значение другого объекта того же типа. Даже соответствующие допустимым элементам перечисления целые значения не могут быть ему присвоены: cout << open_modes_table[ input ] << " " // не поддерживается for ( open_modes iter = input; iter != append; ++inter ) // shape == 0, sphere == 1, cylinder == 2, polygon == 3 // point2d == 2, point2w == 3, point3d == 3, point3w == 4 С++ для начинающих 111 } Однако в арифметических выражениях перечисление может быть автоматически преобразовано в тип 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) и заключается в квадратные скобки. Размер массива нужно знать уже на этапе компиляции, а следовательно, он должен быть константным выражением, хотя не обязательно задается литералом. Вот примеры правильных и неправильных определений массивов: void mumble() { Points pt3d = point3d; // правильно: pt2d == 3 // ошибка: pt3w инициализируется типом int Points pt3w = 3; // ошибка: polygon не входит в перечисление Points pt3w = polygon; // правильно: оба объекта типа Points pt3w = pt3d; const int array_size = 1024; // правильно: pt2w преобразуется int С++ для начинающих 112 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 ia[ array_size ] = { 0, 1, 2 }; Если мы явно указываем список значений, то можем не указывать размер массива: компилятор сам подсчитает количество элементов: int ia[] = { 0, 1, 2 }; 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 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; // массив размера 3 |