Часть IIОсновы языкаКод программы и данные, которыми программа манипулирует, записываются в память
компьютера в виде последовательности битов. Бит – это мельчайший элемент
компьютерной памяти, способная хранить либо 0, либо 1. На физическом уровне
это соответствует электрическому напряжению, которое, как известно, либо есть
, либо нет. Посмотрев на содержимое памяти компьютера, мы увидим что-нибудь
вроде:
Теперь мы можем говорить, например, о байте с адресом 1040 или о слове с адресом
1024 и утверждать, что байт с адресом 1032 не равен байту с адресом 1040. 3. Типы данных С++В этой главе приводится обзор встроенных, или элементарных, типов данных языка С++. Она начинается с определения литералов, таких, как 3.14159 или pi, а затем вводится понятие переменной, или объекта, который должен принадлежать к одному из типов данных. Оставшаяся часть главы посвящена подробному описанию каждого встроенного типа. Кроме того, приводятся производные типы данных для строк и массивов, предоставляемые стандартной библиотекой С++. Хотя эти типы не являются элементарными, они очень важны для написания настоящих программ на С++, и нам хочется познакомить с ними читателя как можно раньше. Мы будем называть такие типы данных расширением базовых типов С++. 3.1. ЛитералыВ С++ имеется набор встроенных типов данных для представления целых и вещественных чисел, символов, а также тип данных “символьный массив”, который служит для хранения символьных строк. Тип char служит для хранения отдельных символов и небольших целых чисел. Он занимает один машинный байт. Типы short, int и long предназначены для представления целых чисел. Эти типы различаются только диапазоном значений, которые могут принимать числа, а конкретные размеры перечисленных типов зависят от реализации. Обычно short занимает половину машинного слова, int – одно слово, long – одно или два слова. В 32-битных системах int и long, как правило, одного размера. Типы float, double и long double предназначены для чисел с плавающей точкой
и различаются точностью представления (количеством значащих разрядов) и диапазоном.
Обычно float (одинарная точность) занимает одно машинное слово, double (двойная
точность) – два, а long double (расширенная точность) – три.
20 // десятичный Если литерал начинается с 0, он трактуется как восьмеричный, если с 0х или
0Х, то как шестнадцатеричный. Привычная запись рассматривается как десятичное
число. 1). Буква U (или u) в конце определяет литерал как unsigned int, а две буквы – UL или LU – как тип unsigned long. Например: 128u 1024UL 1L 8Lu Литералы, представляющие действительные числа, могут быть записаны как с десятичной точкой, так и в научной (экспоненциальной) нотации. По умолчанию они имеют тип double. Для явного указания типа float нужно использовать суффикс F или f, а для long double - L или l, но только в случае записи с десятичной точкой. Например: 3.14159F 0/1f 12.345L 0.0 3el 1.0E-3E 2. 1.0L Слова true и false являются литералами типа bool. 'a' '2' ',' ' ' (пробел) Специальные символы (табуляция, возврат каретки) записываются как escape-последовательности . Определены следующие такие последовательности (они начинаются с символа обратной косой черты): новая строка \n горизонтальная табуляция \t забой \b вертикальная табуляция \v возврат каретки \r прогон листа \f звонок \a обратная косая черта \\ вопрос \? одиночная кавычка \' двойная кавычка \" escape-последовательность общего вида имеет форму \ooo, где ooo – от одной до трех восьмеричных цифр. Это число является кодом символа. Используя ASCII-код, мы можем написать следующие литералы: \7 (звонок) \14 (новая строка) \0 (null) \062 ('2') Символьный литерал может иметь префикс L (например, L'a'), что означает специальный
тип wchar_t – двухбайтовый символьный тип, который применяется для хранения
символов национальных алфавитов, если они не могут быть представлены обычным
типом char, как, например, китайские или японские буквы. "" (пустая строка) "a" "\nCC\toptions\tfile.[cC]\n" "a multi-line \ string literal signals its \ continuation with a backslash" Фактически строковый литерал представляет собой массив символьных констант,
где по соглашению языков С и С++ последним элементом всегда является специальный
символ с кодом 0 (\0). L"a wide string literal" Строковый литерал типа wchar_t – это массив символов того же типа, завершенный
нулем. "two" "some" породит массив из восьми символов – twosome и завершающий нулевой символ. Результат конкатенации строк разного типа не определен. Если написать: // this is not a good idea "two" L"some" то на каком-то компьютере результатом будет некоторая осмысленная строка, а на другом может оказаться нечто совсем иное. Программы, использующие особенности реализации того или иного компилятора или операционной системы, являются непереносимыми. Мы крайне не рекомендуем пользоваться такими конструкциями. Упражнение 3.1Объясните разницу в определениях следующих литералов: (a) 'a', L'a', "a", L"a" (b) 10, 10u, 10L, 10uL, 012, 0*C (c) 3.14, 3.14f, 3.14L Упражнение 3.2Какие ошибки допущены в приведенных ниже примерах? (a) "Who goes with F\144rgus?\014" (b) 3.14e1L (c) "two" L"some" (d) 1024f (e) 3.14UL (f) "multiple line comment" 3.2. ПеременныеПредставим себе, что мы решаем задачу возведения 2 в степень 10. Пишем: #include <iostream> Задача решена, хотя нам и пришлось неоднократно проверять, действительно ли
10 раз повторяется литерал 2. Мы не ошиблись в написании этой длинной последовательности
двоек, и программа выдала правильный результат – 1024. cout << "2 в степени X\t"; cout << 2 * ... * 2; где Х последовательно увеличивается на 1, а вместо отточия подставляется нужное число литералов? Да, мы справились с задачей. Заказчик вряд ли будет вникать в детали, удовлетворившись полученным результатом. В реальной жизни такой подход достаточно часто срабатывает, более того, бывает оправдан: задача решена далеко не самым изящным способом, зато в желаемый срок. Искать более красивый и грамотный вариант может оказаться непрактичной тратой времени. В данном случае “метод грубой силы” дает правильный ответ, но как же неприятно и скучно решать задачу подобным образом! Мы точно знаем, какие шаги нужно сделать, но сами эти шаги просты и однообразны. Привлечение более сложных механизмов для той же задачи, как правило, значительно
увеличивает время подготовительного этапа. Кроме того, чем более сложные механизмы
применяются, тем больше вероятность ошибок. Но даже несмотря на неизбежные ошибки
и неверные ходы, применение “высоких технологий” может принести выигрыш в скорости
разработки, не говоря уже о том, что эти технологии значительно расширяют наши
возможности. И – что интересно! – сам процесс решения может стать привлекательным. #include <iostream> value, pow, res и cnt – это переменные, которые позволяют хранить, модифицировать
и извлекать значения. Оператор цикла for повторяет строку вычисления результата
pow раз. int pow( int val, int exp ) { for ( int res = 1; exp > 0; --exp ) res = res * val; return res; } Теперь получить любую степень нужного числа не составит никакого труда. Вот как реализуется последняя наша задача – напечатать таблицу степеней двойки от 0 до 15: #include <iostream> extern int pow(int,int); int main() { int val = 2; int exp = 15; Конечно, наша функция pow() все еще недостаточно обобщена и недостаточно надежна. Она не может оперировать вещественными числами, неправильно возводит числа в отрицательную степень – всегда возвращает 1. Результат возведения большого числа в большую степень может не поместиться в переменную типа int, и тогда будет возвращено некоторое случайное неправильное значение. Видите, как непросто, оказывается, писать функции, рассчитанные на широкое применение? Гораздо сложнее, чем реализовать конкретный алгоритм, направленный на решение конкретной задачи. 3.2.1. Что такое переменнаяПеременная, или объект – это именованная область памяти, к которой мы имеем доступ из программы; туда можно помещать значения и затем извлекать их. Каждая переменная С++ имеет определенный тип, который характеризует размер и расположение этой области памяти, диапазон значений, которые она может хранить, и набор операций, применимых к этой переменной. Вот пример определения пяти объектов разных типов: int student_count; double salary; bool on_loan; strins street_address; char delimiter; Переменная, как и литерал, имеет определенный тип и хранит свое значение в некоторой области памяти. Адресуемость – вот чего не хватает литералу. С переменной ассоциируются две величины:
В выражении ch = ch - '0'; переменная ch находится и слева и справа от символа операции присваивания. Справа расположено значение для чтения (ch и символьный литерал '0'): ассоциированные с переменной данные считываются из соответствующей области памяти. Слева – значение местоположения: в область памяти, соотнесенную с переменной ch, помещается результат вычитания. В общем случае левый операнд операции присваивания должен быть l-значением. Мы не можем написать следующие выражения: // ошибки компиляции: значения слева не являются l-значениями // ошибка: литерал - не l-значение 0 = 1; // ошибка: арифметическое выражение - не l-значение salary + salary * 0.10 = new_salary; Оператор определения переменной выделяет для нее память. Поскольку объект имеет только одну ассоциированную с ним область памяти, такой оператор может встретиться в программе только один раз. Если же переменная, определенная в одном исходном файле, должна быть использована в другом, появляются проблемы. Например: // файл module0.C // определяет объект fileName string fileName; // ... присвоить fileName значение С++ требует, чтобы объект был известен до первого обращения к нему. Это вызвано необходимостью гарантировать правильность использования объекта в соответствии с его типом. В нашем примере модуль module1.C вызовет ошибку компиляции, поскольку переменная fileName не определена в нем. Чтобы избежать этой ошибки, мы должны сообщить компилятору об уже определенной переменной fileName. Это делается с помощью инструкции объявления переменной: // файл module1.C // использует объект fileName // fileName объявляется, то есть программа получает Объявление переменной сообщает компилятору, что объект с данным именем, имеющий
данный тип, определен где-то в программе. Память под переменную при ее объявлении
не отводится. (Ключевое слово extern рассматривается в разделе 8.2.) 3.2.2. Имя переменнойИмя переменной, или идентификатор, может состоять из латинских букв,
цифр и символа подчеркивания. Прописные и строчные буквы в именах различаются.
Язык С++ не ограничивает длину идентификатора, однако пользоваться слишком длинными
именами типа gosh_this_is_an_impossibly_name_to_type неудобно. Таблица 3.1. Ключевые слова C++
Чтобы текст вашей программы был более понятным, мы рекомендуем придерживаться общепринятых соглашений об именах объектов:
если такое имя состоит из нескольких слов, как, например, birth_date, то принято либо разделять слова символом подчеркивания (birth_date), либо писать каждое следующее слово с большой буквы (birthDate). Замечено, что программисты, привыкшие к ОбъектноОриентированномуПодходу предпочитают выделять слова заглавными буквами, в то время как те_кто_много_писал_на_С используют символ подчеркивания. Какой из двух способов лучше – вопрос вкуса. 3.2.3. Определение объектаВ самом простом случае оператор определения объекта состоит из спецификатора типа и имени объекта и заканчивается точкой с запятой. Например: double salary; double wage; int month; int day; int year; unsigned long distance; В одном операторе можно определить несколько объектов одного типа. В этом случае их имена перечисляются через запятую: double salary, wage; int month, day, year; unsigned long distance; Простое определение переменной не задает ее начального значения. Если объект
определен как глобальный, спецификация С++ гарантирует, что он будет инициализирован
нулевым значением. Если же переменная локальная либо динамически размещаемая
(с помощью оператора new), ее начальное значение не определено, то есть она
может содержать некоторое случайное значение. int main() { // неинициализированный локальный объект int ival; Начальное значение может быть задано прямо в операторе определения переменной. В С++ допустимы две формы инициализации переменной – явная, с использованием оператора присваивания: int ival = 1024; string project = "Fantasia 2000"; и неявная, с заданием начального значения в скобках: int ival( 1024 ); string project( "Fantasia 2000" ); Оба варианта эквивалентны и задают начальные значения для целой переменной
ival как 1024 и для строки project как "Fantasia 2000". double salary = 9999.99, wage = salary + 0.01; int month = 08; day = 07, year = 1955; Переменная становится видимой (и допустимой в программе) сразу после ее определения, поэтому мы могли проинициализировать переменную wage суммой только что определенной переменной salary с некоторой константой. Таким образом, определение: // корректно, но бессмысленно int bizarre = bizarre; является синтаксически допустимым, хотя и бессмысленным. // ival получает значение 0, а dval - 0.0 int ival = int(); double dval = double(); В следующем определении: // int() применяется к каждому из 10 элементов vector< int > ivec( 10 ); к каждому из десяти элементов вектора применяется инициализация с помощью int().
(Мы уже говорили о классе vector в разделе 2.8. Более
подробно об этом см. в разделе 3.10 и главе 6.) #include <cmath> #include <string> abs() – стандартная функция, возвращающая абсолютное значение параметра. Упражнение 3.3Какие из приведенных ниже определений переменных содержат синтаксические ошибки? (a) int car = 1024, auto = 2048; (b) int ival = ival; (c) int ival( int() ); (d) double salary = wage = 9999.99; (e) cin >> int input_value; Упражнение 3.4Объясните разницу между l-значением и r-значением. Приведите примеры. Упражнение 3.5Найдите отличия в использовании переменных name и student в первой и второй строчках каждого примера: (a) extern string name; string name( "exercise 3.5a" ); (b) extern vector<string> students; vector<string> students; Упражнение 3.6Какие имена объектов недопустимы в С++? Измените их так, чтобы они стали синтаксически правильными: (a) int double = 3.14159; (b) vector< int > _; (c) string namespase; (d) string catch-22; (e) char 1_or_2 = '1'; (f) float Float = 3.14f; Упражнение 3.7В чем разница между следующими глобальными и локальными определениями переменных? string global_class; int global_int; int main() { 3.3. УказателиУказатели и динамическое выделение памяти были вкратце представлены в разделе
2.2. Указатель – это объект, содержащий адрес
другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно
указатели используются для работы с динамически созданными объектами, для построения
связанных структур данных, таких, как связанные списки и иерархические деревья,
и для передачи в функции больших объектов – массивов и объектов классов – в
качестве параметров.
Вот несколько примеров: int *ip1, *ip2; complex<double> *cp; string *pstring; vector<int> *pvec; double *dp; Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long: long *lp, lp2; В следующем случае fp интерпретируется как объект типа float, а fp2 – указатель на него: float fp, *fp2; Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны: string *ps; string* ps; Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую: //внимание: ps2 не указатель на строку! string* ps, ps2; Можно предположить, что и ps, и ps2 являются указателями, хотя указатель –
только первый из них. int ival = 1024; Ниже приводятся примеры определения и использования указателей на int pi и pi2: //pi инициализирован нулевым адресом int *pi = 0; Указателю не может быть присвоена величина, не являющаяся адресом: // ошибка: pi не может принимать значение int pi = ival Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом объекта другого типа. Если определены следующие переменные: double dval; double *ps = &dval; то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции: // ошибки компиляции // недопустимое присваивание типов данных: int* <== double* pi = pd pi = &dval; Дело не в том, что переменная pi не может содержать адреса объекта dval – адреса
объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов
запрещены сознательно, потому что интерпретация объектов компилятором зависит
от типа указателя на них. // правильно: void* может содержать // адреса любого типа void *pv = pi; pv = pd; Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать
этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его
значение другому указателю или сравнить с какой-либо адресной величиной. (Более
подробно мы расскажем об указателе типа void в разделе 4.14.) int ival = 1024;, ival2 = 2048; int *pi = &ival; мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi: // косвенное присваивание переменной ival значения ival2 *pi = ival2; Когда мы применяем операцию взятия адреса (&) к объекту типа int, то получаем
результат типа int* int **ppi = π int *pi2 = *ppi; Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия: int i, j, k; int *pi = &i; // i = i + 2 К указателю можно прибавлять целое значение, можно также вычитать из него.
Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер
области памяти, отводимой объекту соответствующего типа. Если тип char занимает
1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double
увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать?
Если объекты одного типа расположены в памяти друг за другом, то увеличение
указателя на 1 приведет к тому, что он будет указывать на следующий объект.
Поэтому арифметические действия с указателями чаще всего применяются при обработке
массивов; в любых других случаях они вряд ли оправданы. int ia[10]; int *iter = &ia[0]; int *iter_end = &ia[10]; Упражнение 3.8Даны определения переменных: int ival = 1024, ival2 = 2048; int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0; Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки? (a) ival = *pi3; (e) pi1 = *pi3; (b) *pi2 = *pi3; (f) ival = *pi1; (c) ival = pi2; (g) pi1 = ival; (d) pi2 = *pi1; (h) pi3 = &pi2; Упражнение 3.9Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код pi = &ival; pi = pi + 1024; почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке? Упражнение 3.10Данная программа содержит ошибку, связанную с неправильным использованием указателей: int foobar(int *pi) { *pi = 1024; return *pi; } В чем состоит ошибка? Как можно ее исправить? Упражнение 3.11Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована? Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной? 3.4. Строковые типыВ С++ поддерживаются два типа строк – встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.) 3.4.1. Встроенный строковый типКак уже было сказано, встроенный строковый тип перешел к С++ по наследству от С. Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций для манипулирования строками. Например: // возвращает длину строки int strlen( const char* ); Стандартная библиотека С является частью библиотеки С++. Для ее использования мы должны включить заголовочный файл: #include <cstring> Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал, например const char *st = "Цена бутылки вина\n"; компилятор помещает все символы строки в массив и затем присваивает st адрес
первого элемента массива. Как можно работать со строкой, используя такой указатель? while (*st++ ) { ... } st разыменовывается, и получившееся значение проверяется на истинность. Любое
отличное от нуля значение считается истинным, и, следовательно, цикл заканчивается,
когда будет достигнут символ с кодом 0. Операция инкремента ++ прибавляет 1
к указателю st и таким образом сдвигает его к следующему символу. int string_length( const char *st ) { int cnt = 0; if ( st ) while ( *st++ ) ++cnt; return cnt; } Строка встроенного типа может считаться пустой в двух случаях: если указатель на строку имеет нулевое значение (тогда у нас вообще нет никакой строки) или указывает на массив, состоящий из одного нулевого символа (то есть на строку, не содержащую ни одного значимого символа). // pc1 не адресует никакого массива символов char *pc1 = 0; // pc2 адресует нулевой символ const char *pc2 = ""; Для начинающего программиста использование строк встроенного типа чревато ошибками из-за слишком низкого уровня реализации и невозможности обойтись без адресной арифметики. Ниже мы покажем некоторые типичные погрешности, допускаемые новичками. Задача проста: вычислить длину строки. Первая версия неверна. Исправьте ее. #include <iostream> const char *st = "Цена бутылки вина\n"; int main() { В этой версии указатель st не разыменовывается. Следовательно, на равенство
0 проверяется не символ, на который указывает st, а сам указатель. Поскольку
изначально этот указатель имел ненулевое значение (адрес строки), то он никогда
не станет равным нулю, и цикл будет выполняться бесконечно. #include <iostream> const char *st = "Цена бутылки вина\n"; int main() Ошибка состоит в том, что после завершения цикла указатель st адресует не исходный
символьный литерал, а символ, расположенный в памяти после завершающего нуля
этого литерала. В этом месте может находиться что угодно, и выводом программы
будет случайная последовательность символов. st = st – len; cout << len << ": " << st; Теперь наша программа выдает что-то осмысленное, но не до конца. Ответ выглядит так: 18: ена бутылки вина Мы забыли учесть, что заключительный нулевой символ не был включен в подсчитанную длину. st должен быть смещен на длину строки плюс 1. Вот, наконец, правильный оператор: st = st – len - 1; а вот и и правильный результат: 18: Цена бутылки вина Однако нельзя сказать, что наша программа выглядит элегантно. Оператор st = st – len - 1; добавлен для того, чтобы исправить ошибку, допущенную на раннем этапе проектирования программы, – непосредственное увеличение указателя st. Этот оператор не вписывается в логику программы, и код теперь трудно понять. Исправления такого рода часто называют заплатками – нечто, призванное заткнуть дыру в существующей программе. Гораздо лучшим решением было бы пересмотреть логику. Одним из вариантов в нашем случае может быть определение второго указателя, инициализированного значением st: const char *p = st; Теперь p можно использовать в цикле вычисления длины, оставив значение st неизменным: while ( *p++ ) 3.4.2. Класс stringКак мы только что видели, применение встроенного строкового типа чревато ошибками
и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому
достаточно распространена разработка собственного класса или классов для представления
строкового типа – чуть ли не каждая компания, отдел или индивидуальный проект
имели свою собственную реализацию строки. Да что говорить, в предыдущих двух
изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости
и переносимости программ. Реализация стандартного класса string стандартной
библиотекой С++ призвана была положить конец этому изобретению велосипедов.
Класс string стандартной библиотеки С++ реализует все перечисленные операции
(и гораздо больше, как мы увидим в главе 6). В данном
разделе мы научимся пользоваться основными операциями этого класса. #include <string> Вот пример строки из предыдущего раздела, представленной объектом типа string и инициализированной строкой символов: #include <string> string st( "Цена бутылки вина\n" ); Длину строки возвращает функция-член size() (длина не включает завершающий нулевой символ). cout << "Длина " << st << ": " << st.size() << " символов, включая символ новой строки\n"; Вторая форма определения строки задает пустую строку: string st2; // пустая строка Как мы узнаем, пуста ли строка? Конечно, можно сравнить ее длину с 0: if ( ! st.size() ) // правильно: пустая Однако есть и специальный метод empty(), возвращающий true для пустой строки и false для непустой: if ( st.empty() ) // правильно: пустая Третья форма конструктора инициализирует объект типа string другим объектом того же типа: string st3( st ); Строка st3 инициализируется строкой st. Как мы можем убедиться, что эти строки совпадают? Воспользуемся оператором сравнения (==): if ( st == st3 ) // инициализация сработала Как скопировать одну строку в другую? С помощью обычной операции присваивания: st2 = st3; // копируем st3 в st2 Для конкатенации строк используется операция сложения (+) или операция сложения с присваиванием (+=). Пусть даны две строки: string s1( "hello, " ); string s2( "world\n" ); Мы можем получить третью строку, состоящую из конкатенации первых двух, таким образом: string s3 = s1 + s2; Если же мы хотим добавить s2 в конец s1, мы должны написать: s1 += s2; Операция сложения может конкатенировать объекты класса string не только между собой, но и со строками встроенного типа. Можно переписать пример, приведенный выше, так, чтобы специальные символы и знаки препинания представлялись встроенным типом, а значимые слова – объектами класса string: const char *pc = ", "; string s1( "hello" ); string s2( "world" ); Подобные выражения работают потому, что компилятор знает, как автоматически преобразовывать объекты встроенного типа в объекты класса string. Возможно и простое присваивание встроенной строки объекту string: string s1; const char *pc = "a character array"; s1 = pc; // правильно Обратное преобразование, однако, не работает. Попытка выполнить следующую инициализацию строки встроенного типа вызовет ошибку компиляции: char *str = s1; // ошибка компиляции Чтобы осуществить такое преобразование, необходимо явно вызвать функцию-член с несколько странным названием c_str(): char *str = s1.c_str(); // почти правильно Функция c_str() возвращает указатель на символьный массив, содержащий строку
объекта string в том виде, в каком она находилась бы во встроенном строковом
типе. const char * (В следующем разделе мы расскажем о ключевом слове const). Правильный вариант инициализации выглядит так: const char *str = s1.c_str(); // правильно К отдельным символам объекта типа string, как и встроенного типа, можно обращаться с помощью операции взятия индекса. Вот, например, фрагмент кода, заменяющего все точки символами подчеркивания: string str( "fa.disney.com" ); int size = str.size(); for ( int ix = 0; ix < size; ++ix ) if ( str[ ix ] == '.' )
replace( str.begin(), str.end(), '.', '_' ); replace() – один из обобщенных алгоритмов, с которыми мы познакомились в разделе 2.8 и которые будут детально разобраны в главе 12. Эта функция пробегает диапазон от begin() до end(), которые возвращают указатели на начало и конец строки, и заменяет элементы, равные третьему своему параметру, на четвертый. Упражнение 3.12Найдите ошибки в приведенных ниже операторах: (a) char ch = "The long and winding road"; (b) int ival = &ch; (c) char *pc = &ival; (d) string st( &ch ); (e) pc = 0; (i) pc = '0'; Упражнение 3.13Объясните разницу в поведении следующих операторов цикла: while ( st++ ) ++cnt; Упражнение 3.14Даны две семантически эквивалентные программы. Первая использует встроенный строковый тип, вторая – класс string: // ***** Реализация с использованием C-строк ***** #include <iostream> #include <cstring> Что эти программы делают? Упражнение 3.15Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения 3.5. Спецификатор constВозьмем следующий пример кода: for ( int index = 0; index < 512; ++index ) ... ; С использованием литерала 512 связаны две проблемы. Первая состоит в легкости
восприятия текста программы. Почему верхняя граница переменной цикла должна
быть равна именно 512? Что скрывается за этой величиной? Она кажется случайной... index < bufSize В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления всего одного объекта! Теперь значение 512 локализовано. int bufSize = 512; // размер буфера ввода // ... for ( int index = 0; index < bufSize; ++index ) Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок – использование операции присваивания (=) вместо сравнения (==): // случайное изменение значения bufSize if ( bufSize = 1 ) // ... В результате выполнения этого кода значение bufSize станет равным 1, что может
привести к совершенно непредсказуемому поведению программы. Ошибки такого рода
обычно очень тяжело обнаружить, поскольку они попросту не видны. const int bufSize = 512; // размер буфера ввода мы превращаем переменную в константу со значением 512, значение которой не может быть изменено: такие попытки пресекаются компилятором: неверное использование оператора присваивания вместо сравнения, как в приведенном примере, вызовет ошибку компиляции. // ошибка: попытка присваивания значения константе if ( bufSize = 0 ) ... Раз константе нельзя присвоить значение, она должна быть инициализирована в месте своего определения. Определение константы без ее инициализации также вызывает ошибку компиляции: const double pi; // ошибка: неинициализированная константа Давайте рассуждать дальше. Явная трансформация значения константы пресекается компилятором. Но как быть с косвенной адресацией? Можно ли присвоить адрес константы некоторому указателю? const double minWage = 9.60; // правильно? ошибка? Должен ли компилятор разрешить подобное присваивание? Поскольку minWage – константа, ей нельзя присвоить значение. С другой стороны, ничто не запрещает нам написать: *ptr += 1.40; // изменение объекта minWage! Как правило, компилятор не в состоянии уберечь от использования указателей
и не сможет сигнализировать об ошибке в случае подобного их употребления. Для
этого требуется слишком глубокий анализ логики программы. Поэтому компилятор
просто запрещает присваивание адресов констант обычным указателям. const double *cptr; где cptr – указатель на объект типа const double. Тонкость заключается в том, что сам указатель – не константа, а значит, мы можем изменять его значение. Например: const double *pc = 0; const double minWage = 9.60; // правильно: не можем изменять minWage с помощью pc Адрес константного объекта присваивается только указателю на константу. Вместе с тем, такому указателю может быть присвоен и адрес обычной переменной: pc = &dval; Константный указатель не позволяет изменять адресуемый им объект с помощью
косвенной адресации. Хотя dval в примере выше и не является константой, компилятор
не допустит изменения переменной dval через pc. (Опять-таки потому, что он не
в состоянии определить, адрес какого объекта может содержать указатель в произвольный
момент выполнения программы.) // В реальных программах указатели на константы чаще всего // употребляются как формальные параметры функций int strcmp( const char *str1, const char *str2 ); (Мы еще поговорим об указателях на константы в главе 7,
когда речь пойдет о функциях.) int errNumb = 0; int *const currErr = &errNumb; Здесь curErr – константный указатель на неконстантный объект. Это значит, что мы не можем присвоить ему адрес другого объекта, хотя сам объект допускает модификацию. Вот как мог бы быть использован указатель curErr: do_something(); if ( *curErr ) { Попытка присвоить значение константному указателю вызовет ошибку компиляции: curErr = &myErNumb; // ошибка Константный указатель на константу является объединением двух рассмотренных случаев. const double pi = 3.14159; const double *const pi_ptr = π Ни значение объекта, на который указывает pi_ptr, ни значение самого указателя не может быть изменено в программе. Упражнение 3.16Объясните значение следующих пяти определений. Есть ли среди них ошибочные? (a) int i; (d) int *const cpi; (b) const int ic; (e) const int *const cpic; (c) const int *pic; Упражнение 3.17Какие из приведенных определений правильны? Почему? (a) int i = -1; (b) const int ic = i; (c) const int *pic = ⁣ (d) int *const cpi = ⁣ (e) const int *const cpic = ⁣ Упражнение 3.18Используя определения из предыдущего упражнения, укажите правильные операторы присваивания. Объясните. (a) i = ic; (d) pic = cpic; (b) pic = ⁣ (i) cpic = ⁣ (c) cpi = pic; (f) ic = *cpic; 3.6. Ссылочный типСсылочный тип, иногда называемый псевдонимом, служит для задания объекту дополнительного
имени. Ссылка позволяет косвенно манипулировать объектом, точно так же, как
это делается с помощью указателя. Однако эта косвенная манипуляция не требует
специального синтаксиса, необходимого для указателей. Обычно ссылки употребляются
как формальные параметры функций. В этом разделе мы рассмотрим самостоятельное
использование объектов ссылочного типа. int ival = 1024; // правильно: refVal - ссылка на ival int &refVal = ival; // ошибка: ссылка должна быть инициализирована int &refVal2; Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель: int ival = 1024; // ошибка: refVal имеет тип int, а не int* int &refVal = &ival; int *pi = &ival; // правильно: ptrVal - ссылка на указатель int *&ptrVal2 = pi; Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту, которую адресует refVal. int min_val = 0; // ival получает значение min_val, // а не refVal меняет значение на min_val refVal = min_val; Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например: refVal += 2; прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично int ii = refVal; присваивает ii текущее значение ival, int *pi = &refVal; инициализирует pi адресом ival. Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например: // определено два объекта типа int int ival = 1024, ival2 = 2048; // определена одна ссылка и один объект int &rval = ival, rval2 = ival2; // определен один объект, один указатель и одна ссылка Константная ссылка может быть инициализирована объектом другого типа (если, конечно, существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например: double dval = 3.14159; // верно только для константных ссылок Если бы мы не указали спецификатор const, все три определения ссылок вызвали
бы ошибку компиляции. Однако, причина, по которой компилятор не пропускает таких
определений, неясна. Попробуем разобраться. double dval = 1024; const int &ri = dval; то компилятор преобразует это примерно так: int temp = dval; const int &ri = temp; Если бы мы могли присвоить новое значение ссылке ri, мы бы реально изменили
не dval, а temp. Значение dval осталось бы тем же, что совершенно неочевидно
для программиста. Поэтому компилятор запрещает такие действия, и единственная
возможность проинициализировать ссылку объектом другого типа – объявить ее как
const. const int ival = 1024; // ошибка: нужна константная ссылка Попытка исправить дело добавлением спецификатора const тоже не проходит: const int ival = 1024; // все равно ошибка const int *&pi_ref = &ival; В чем причина? Внимательно прочитав определение, мы увидим, что pi_ref является ссылкой на константный указатель на объект типа int. А нам нужен неконстантный указатель на константный объект, поэтому правильной будет следующая запись: const int ival = 1024; // правильно Между ссылкой и указателем существуют два основных отличия. Во-первых, ссылка обязательно должна быть инициализирована в месте своего определения. Во-вторых, всякое изменение ссылки преобразует не ее, а тот объект, на который она ссылается. Рассмотрим на примерах. Если мы пишем: int *pi = 0; мы инициализируем указатель pi нулевым значением, а это значит, что pi не указывает ни на какой объект. В то же время запись const int &ri = 0; Что касается операции присваивания, то в следующем примере: int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; pi = pi2; переменная ival, на которую указывает pi, остается неизменной, а pi получает
значение адреса переменной ival2. И pi, и pi2 и теперь указывают на один и тот
же объект ival2. int &ri = ival, &ri2 = ival2; ri = ri2; то само значение ival меняется, но ссылка ri по-прежнему адресует ival. // пример использования ссылок // Значение возвращается в параметре next_value Как соотносятся самостоятельные объекты-ссылки и ссылки-параметры? Если мы пишем: int ival; while (get_next_value( ival )) ... это равносильно следующему определению ссылки внутри функции: int &next_value = ival; (Подробнее использование ссылок в качестве формальных параметров функций рассматривается в главе 7.) Упражнение 3.19Есть ли ошибки в данных определениях? Поясните. Как бы вы их исправили? (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; (i) const int &ival2 = 1; (j) const int &*prval2 = &ival; Упражнение 3.20Если ли среди нижеследующих операций присваивания ошибочные (используются определения из предыдущего упражнения)? (a) rval1 = 3.14159; (b) prval1 = prval2; (c) prval2 = rval1; (d) *prval2 = ival2; Упражнение 3.21Найдите ошибки в приведенных инструкциях: (a) int ival = 0; const int *pi = 0; const int &ri = 0; (b) pi = &ival; 3.7. Тип boolОбъект типа bool может принимать одно из двух значений: true и false. Например: // инициализация строки string search_word = get_word(); // инициализация переменной found Хотя bool относится к одному из целых типов, он не может быть объявлен как signed, unsigned, short или long, поэтому приведенное определение ошибочно: // ошибка short bool found = false; Объекты типа bool неявно преобразуются в тип int. Значение true превращается в 1, а false – в 0. Например: bool found = false; int occurrence_count = 0; while ( /* mumble */ ) Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа bool. При этом 0 интерпретируется как false, а все остальное как true: // возвращает количество вхождений extern int find( const string& ); bool found = false; if ( found = find( "rosebud" )) // правильно: found == true // возвращает указатель на элемент 3.8. ПеречисленияНередко приходится определять переменную, которая принимает значения из некоего
набора. Скажем, файл открывают в любом из трех режимов: для чтения, для записи,
для добавления. const int input = 1; const int output = 2; const int append = 3; и пользоваться этими константами: bool open_file( string file_name, int open_mode); // ... Подобное решение допустимо, но не вполне приемлемо, поскольку мы не можем гарантировать,
что аргумент, передаваемый в функцию 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 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 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 константы Объекты buf_size и max_files являются константами, поэтому определения массивов
input_buffer и fileTable правильны. А вот staff_size – переменная (хотя и инициализированная
константой 27), значит, salaries[staff_size] недопустимо. (Компилятор не в состоянии
найти значение переменной staff_size в момент определения массива salaries.) int main() { const int array_size = 10; int ia[ array_size ]; for ( int ix = 0; ix < array_size; ++ 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() Чтобы скопировать один массив в другой, придется проделать это для каждого элемента по отдельности: const int array_size = 7; int ia1[] = { 0, 1, 2, 3, 4, 5, 6 }; int main() { В качестве индекса массива может выступать любое выражение, дающее результат целого типа. Например: 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 ) 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 ) Конструкция 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 <iostream> 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, мы узнаем, что массив кончился. Перепишем эту программу так, чтобы начало и конец массива передавались параметрами в некую обобщенную функцию, которая умеет печатать массив любого размера: #include <iostream> void ia_print( int *pbegin, int *pend ) { Наша функция стала более универсальной, однако, она умеет работать только с массивами типа int. Есть способ снять и это ограничение: преобразовать данную функцию в шаблон (шаблоны были вкратце представлены в разделе 2.5): #include <iostream> template <c1ass e1emType> 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 ); Мы написали обобщенную функцию. Стандартная библиотека предоставляет набор обобщенных алгоритмов (мы уже упоминали об этом в разделе 3.4), реализованных подобным образом. Параметрами таких функций являются указатели на начало и конец массива, с которым они производят определенные действия. Вот, например, как выглядят вызовы обобщенного алгоритма сортировки: #include <a1gorithm> int main() { int ia[6] = { 107, 28, 3, 47, 104, 76 }; string sa[3] = { "piglet", "eeyore", "pooh" }; sort( ia, ia+6 ); (Мы подробно остановимся на обобщенных алгоритмах в главе 12; в Приложении
будут приведены примеры их использования.) 3.10. Класс vectorИспользование класса vector (см. раздел 2.8) является
альтернативой применению встроенных массивов. Этот класс предоставляет гораздо
больше возможностей, поэтому его использование предпочтительней. Однако встречаются
ситуации, когда не обойтись без массивов встроенного типа. Одна из таких ситуаций
– обработка передаваемых программе параметров командной строки, о чем мы будем
говорить в разделе 7.8. Класс vector, как и класс string,
является частью стандартной библиотеки С++. #include <vector> Существуют два абсолютно разных подхода к использованию вектора, назовем их идиомой массива и идиомой STL. В первом случае объект класса vector используется точно так же, как массив встроенного типа. Определяется вектор заданной размерности: vector< int > ivec( 10 ); что аналогично определению массива встроенного типа: int ia[ 10 ]; Для доступа к отдельным элементам вектора применяется операция взятия индекса: 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 ) Мы можем узнать размерность вектора, используя функцию size(), и проверить, пуст ли вектор, с помощью функции empty(). Например: void print_vector( vector<int> ivec ) { if ( ivec.empty() ) return; for ( int ix=0; ix< ivec.size(); ++ix ) Элементы вектора инициализируются значениями по умолчанию. Для числовых типов и указателей таким значением является 0. Если в качестве элементов выступают объекты класса, то инициатор для них задается конструктором по умолчанию (см. раздел 2.3). Однако инициатор можно задать и явно, используя форму: vector< int > ivec( 10, -1 ); Все десять элементов вектора будут равны -1. int ia[ 6 ] = { -2, -1, О, 1, 2, 1024 }; Для объекта класса vector аналогичное действие невозможно. Однако такой объект может быть инициализирован с помощью массива встроенного типа: // 6 элементов ia копируются в ivec vector< int > ivec( ia, ia+6 ); Конструктору вектора ivec передаются два указателя – указатель на начало массива ia и на элемент, следующий за последним. В качестве списка начальных значений допустимо указать не весь массив, а некоторый его диапазон: // копируются 3 элемента: ia[2], ia[3], ia[4] vector< int > ivec( &ia[ 2 ], &ia[ 5 ] ); Еще одним отличием вектора от массива встроенного типа является возможность инициализации одного объекта типа vector другим и использования операции присваивания для копирования объектов. Например: vector< string > svec; void init_and_assign() { // один вектор инициализируется другим vector< string > user_names( svec ); // ... // один вектор копируется в другой Говоря об идиоме STL , мы подразумеваем совсем другой подход к использованию вектора. Вместо того чтобы сразу задать нужный размер, мы определяем пустой вектор: vector< string > text; Затем добавляем к нему элементы при помощи различных функций. Например, функция push_back()вставляет элемент в конец вектора. Вот фрагмент кода, считывающего последовательность строк из стандартного ввода и добавляющего их в вектор: string word; while ( cin >> word ) { text.push_back( word ); // ... } Хотя мы можем использовать операцию взятия индекса для перебора элементов вектора: cout << "считаны слова: \n"; for ( int ix =0; ix < text.size(); ++ix ) cout << text[ ix ] << ' '; cout << endl; более типичным в рамках данной идиомы будет использование итераторов: cout << "считаны слова: \n"; for ( vector<string>::iterator it = text.begin(); it != text.end(); ++it ) cout << *it << ' '; cout << endl; Итератор – это класс стандартной библиотеки, фактически являющийся указателем
на элемент массива. *it; разыменовывает итератор и дает сам элемент вектора. Инструкция 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 ) ivec.push_back( ia[ ix ] ); Имелась в виду инициализация вектора ivec значениями элементов ia, вместо чего
получился вектор ivec размера 14. Упражнение 3.24Имеются ли ошибки в следующих определениях? (a) vector< vector< int > > ivec; Упражнение 3.25Реализуйте следующую функцию: 3.11. Класс complexКласс комплексных чисел complex – еще один класс из стандартной библиотеки. Как обычно, для его использования нужно включить заголовочный файл: #include <complex> Комплексное число состоит из двух частей – вещественной и мнимой. Мнимая часть
представляет собой квадратный корень из отрицательного числа. Комплексное число
принято записывать в виде где 2 – действительная часть, а 3i – мнимая. Вот примеры определений объектов типа complex: // чисто мнимое число: 0 + 7-i complex< double > purei( 0, 7 ); // мнимая часть равна 0: 3 + Oi complex< float > rea1_num( 3 ); // и вещественная, и мнимая часть равны 0: 0 + 0-i complex< long double > zero; // инициализация одного комплексного числа другим complex< double > purei2( purei ); Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также определить массив элементов типа complex: complex< double > conjugate[ 2 ] = { complex< double >( 2, 3 ), complex< double >( 2, -3 ) }; Вот как определяются указатель и ссылка на комплексное число: complex< double > *ptr = &conjugate[0]; complex< double > &ref = *ptr; Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.) 3.12. Директива typedefДиректива typedef позволяет задать синоним для встроенного либо пользовательского типа данных. Например: typedef double wages; typedef vector<int> vec_int; typedef vec_int test_scores; typedef bool in_attendance; typedef int *Pint; Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов: // double hourly, weekly; wages hourly, weekly; // vector<int> vecl( 10 ); Эта директива начинается с ключевого слова typedef, за которым идет спецификатор
типа, и заканчивается идентификатором, который становится синонимом для указанного
типа. 3.13. Спецификатор volatileОбъект объявляется как volatile (неустойчивый, асинхронно изменяемый), если
его значение может быть изменено незаметно для компилятора, например переменная,
обновляемая значением системных часов. Этот спецификатор сообщает компилятору,
что не нужно производить оптимизацию кода для работы с данным объектом. volatile int disp1ay_register; volatile Task *curr_task; volatile int ixa[ max_size ]; volatile Screen bitmap_buf; display_register – неустойчивый объект типа int. curr_task – указатель на неустойчивый
объект класса Task. ixa – неустойчивый массив целых, причем каждый элемент такого
массива считается неустойчивым. bitmap_buf – неустойчивый объект класса Screen,
каждый его член данных также считается неустойчивым. 3.14. Класс pairКласс pair (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл: #include <utility> Например, инструкция pair< string, string > author( "James", "Joyce" ); создает объект author типа pair, состоящий из двух строковых значений. string firstBook; if ( Joyce.first == "James" && Если нужно определить несколько однотипных объектов этого класса, удобно использовать директиву typedef: typedef pair< string, string > Authors; Authors proust( "marcel", "proust" ); Authors joyce( "James", "Joyce" ); Authors musil( "robert", "musi1" ); Вот другой пример употребления пары. Первое значение содержит имя некоторого объекта, второе – указатель на соответствующий этому объекту элемент таблицы. class EntrySlot; extern EntrySlot* 1ook_up( string ); typedef pair< string, EntrySlot* > SymbolEntry; SymbolEntry current_entry( "author", 1ook_up( "author")); (Мы вернемся к рассмотрению класса pair в разговоре о контейнерных типах в главе 6 и об обобщенных алгоритмах в главе 12.) 3.15. Типы классовМеханизм классов позволяет создавать новые типы данных; с его помощью введены
типы string, vector, complex и pair, рассмотренные выше. В главе
2 мы рассказывали о концепциях и механизмах, поддерживающих объектный и
объектно-ориентированный подход, на примере реализации класса Array. Здесь мы,
основываясь на объектном подходе, создадим простой класс String, реализация
которого поможет понять, в частности, перегрузку операций – мы говорили о ней
в разделе 2.3. (Классы подробно рассматриваются в главах
13, 14 и 15). Мы дали краткое описание класса для того, чтобы приводить более
интересные примеры. Читатель, только начинающий изучение С++, может пропустить
этот раздел и подождать более систематического описания классов в следующих
главах.) #include <iostream> class String; istream& operator>>( istream&, String& ); Класс String имеет три конструктора. Как было сказано в разделе 2.3,
механизм перегрузки позволяет определять несколько реализаций функций с одним
именем, если все они различаются количеством и/или типами своих параметров.
Первый конструктор String object("Danny"); // доступ к члену для objects (.); // objects имеет размер 5 sizes[ 0 ] = object.size(); // доступ к члену для pointers (->) Она возвращает соответственно 5, 4 и 0. String namel( "Yadie" ); String name2( "Yodie" ); // bool operator==(const String&) Объявление функции-члена должно находиться внутри определения класса, а определение функции может стоять как внутри определения класса, так и вне его. (Обе функции size() и c_str() определяются внутри класса.) Если функция определяется вне класса, то мы должны указать, кроме всего прочего, к какому классу она принадлежит. В этом случае определение функции помещается в исходный файл, допустим, String.C, а определение самого класса – в заголовочный файл (String.h в нашем примере), который должен включаться в исходный: // содержимое исходного файла: String.С // включение определения класса String Напомним, что strcmp() – функция стандартной библиотеки С. Она сравнивает две
строки встроенного типа, возвращая 0 в случае равенства строк и ненулевое значение
в случае неравенства. Условный оператор (?:) проверяет значение, стоящее перед
знаком вопроса. Если оно истинно, возвращается значение выражения, стоящего
слева от двоеточия, в противном случае – стоящего справа. В нашем примере значение
выражения равно false, если strcmp() вернула ненулевое значение, и true – если
нулевое. (Условный оператор рассматривается в разделе 4.7.) inline bool String::operator==(const String &rhs) { // то же самое } Определение встроенной функции должно находиться в заголовочном файле, содержащем
определение класса. Переопределив оператор == как встроенный, мы должны переместить
сам текст функции из файла String.C в файл String.h. inline bool String::operator==(const char *s) { return strcmp( _string, s ) ? false : true; } Имя конструктора совпадает с именем класса. Считается, что он не возвращает значение, поэтому не нужно задавать возвращаемое значение ни в его определении, ни в его теле. Конструкторов может быть несколько. Как и любая другая функция, они могут быть объявлены встроенными. #include <cstring> // default constructor inline String::String() inline String::String( const char *str ) { if ( ! str ) { _size = 0; _string = 0; } else { _size = str1en( str ); _string = new char[ _size + 1 ]; strcpy( _string, str ); } // copy constructor Поскольку мы динамически выделяли память с помощью оператора new, необходимо
освободить ее вызовом delete, когда объект String нам больше не нужен. Для этой
цели служит еще одна специальная функция-член – деструктор, автоматически вызываемый
для объекта в тот момент, когда этот объект перестает существовать. (См. главу
7 о времени жизни объекта.) Имя деструктора образовано из символа тильды (~)
и имени класса. Вот определение деструктора класса String. Именно в нем мы вызываем
операцию delete, чтобы освободить память, выделенную в конструкторе: inline String& String::operator=( const char *s ) { if ( ! s ) { _size = 0; delete [] _string; _string = 0; } else { _size = str1en( s ); delete [] _string; _string = new char[ _size + 1 ]; strcpy( _string, s ); } return *this; } При реализации операции присваивания довольно часто допускают одну ошибку: забывают проверить, не является ли копируемый объект тем же самым, в который происходит копирование. Мы выполним эту проверку, используя все тот же указатель this: inline String& String::operator=( const String &rhs ) { // в выражении // namel = *pointer_to_string // this представляет собой name1, // rhs - *pointer_to_string. if ( this != &rhs ) { Вот полный текст операции присваивания объекту String объекта того же типа: inline String& String::operator=( const String &rhs ) { if ( this != &rhs ) { delete [] _string; _size = rhs._size; if ( ! rhs._string ) Операция взятия индекса практически совпадает с ее реализацией для массива Array, который мы создали в разделе 2.3: #include <cassert> inline char& Операторы ввода и вывода реализуются как отдельные функции, а не члены класса. (О причинах этого мы поговорим в разделе 15.2. В разделах 20.4 и 20.5 рассказывается о перегрузке операторов ввода и вывода библиотеки iostream.) Наш оператор ввода может прочесть не более 4095 символов. setw() – предопределенный манипулятор, он читает из входного потока заданное число символов минус 1, гарантируя тем самым, что мы не переполним наш внутренний буфер inBuf. (В главе 20 манипулятор setw() рассматривается детально.) Для использования манипуляторов нужно включить соответствующий заголовочный файл: #include <iomanip> inline istream& operator>>( istream &io, String &s ) { // искусственное ограничение: 4096 символов const int 1imit_string_size = 4096; char inBuf[ limit_string_size ]; // setw() входит в библиотеку iostream // он ограничивает размер читаемого блока до 1imit_string_size-l io >> setw( 1imit_string_size ) >> inBuf; s = mBuf; // String::operator=( const char* ); return io; } Оператору вывода необходим доступ к внутреннему представлению строки String. Так как operator<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода: inline ostream& operator<<( ostream& os, const String &s ) { return os << s.c_str(); } Ниже приводится пример программы, использующей класс String. Эта программа берет слова из входного потока и подсчитывает их общее число, а также количество слов "the" и "it" и регистрирует встретившиеся гласные. #include <iostream> #include "String.h" int main() { int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0; // Слова "The" и "It" Протестируем программу: предложим ей абзац из детского рассказа, написанного одним из авторов этой книги (мы еще встретимся с этим рассказом в главе 6). Вот результат работы программы: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, 1ike a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more. Shyly, she asks, "I mean, Daddy, is there?" Слов: 65 Упражнение 3.26В наших реализациях конструкторов и операций присваивания содержится много повторов. Попробуйте вынести повторяющийся код в отдельную закрытую функцию-член, как это было сделано в разделе 2.3. Убедитесь, что новый вариант работоспособен. Упражнение 3.27Модифицируйте тестовую программу так, чтобы она подсчитывала и согласные b, d, f, s, t. Упражнение 3.28Напишите функцию-член, подсчитывающую количество вхождений символа в строку String, используя следующее объявление: class String { public: // ... int count( char ch ) const; // ... }; Упражнение 3.29Реализуйте оператор конкатенации строк (+) так, чтобы он конкатенировал две строки и возвращал результат в новом объекте String. Вот объявление функции: class String { public: // ... String operator+( const String &rhs ) const; // ... };Назад Вперед Содержание |
2012-05-28 13:02:12 Камолиддин хорощая и удобная книга 2012-06-08 22:09:51 Павел В разделе 3.2.2 некоторые имена написаны по 2-3 раза. 2012-06-09 15:11:53 Павел Как выполнить упражнение 3.25 с const? У меня только без него получается 2012-07-01 17:26:10 FeelUs const int ival = 1024; // правильно int *const &piref = &ival; - неправильно, т.к. piref мы можем разыменовать и изменить ival 2013-01-03 08:10:26 Tercius const int ival = 1024; // правильно int *const &piref = &ival; Правильно будет const int*const &piref=&ival; 2013-05-27 22:10:40 harvester На каком вообще языке эти примеры? 2016-03-27 11:47:36 sergei поучительно Оставить комментарий: |