138
Теория и практика С++ Язык программирования С++ История С и С++. История начинается с 1970 год. (Bell Labs - бывшая американская, а ныне франко- американская корпорация, крупный исследовательский центр в области телекоммуникаций, электронных и компьютерных систем). Два сотрудника Кен Томпсон и Денис Ричи являлись ведущими инженерами команды перед которыми была поставлена задача написать переносимую ОС. Что это значит? Было несколько моделей компьютеров с разным устройством и хотелось написать систему, которая могла работать на разных моделях. Для этой цели был разработан язык С для ОС Unix. К языку предъявлялись следующие требования: 1. Эффективность, как язык Ассемблер. 2. Более удобен, чем Ассемблер. Например – сложение двух чисел. ASM C mov AX, 2 mov BX, [1234] add AX, BX mov [DX], AX a = b +c Более сложные вещи пишутся еще более громоздко. 3. Переносимость на уровне текстов исходных программ. Т.е. если есть новая платформа, то для нее достаточно написать компилятор С, которых мог перевести программу на С для новой платформы. На языке ASM это было бы неудобно. У инструкций ASM есть операнды, которые могут отличаться на различных машинах. Т.е. Язык ASM наиболее близок к машинному коду. Хотелось от этого абстрагироваться. И такой язык был разработан - С. Но в погоне за эффективностью он ближе к машине, чем к программисту. Например: 1. Нет специального символьного типа. 2. В нем отсутствует булевский тип для логических значений (true и false). Любое число 0 – true. 0 = false. Казус: if (a = 0) { } а сравнивается с нулем. На самом деле программист хотел написать a == 0. Ошибка, всегда будет false (в результате присваивания возвратился ноль) и в условие никогда не попадем. С точки зрения компилятора инструкция легальна. Ошибки не видит. 1980 год. 1 1

C++ теория

Embed Size (px)

Citation preview

Page 1: C++ теория

Теория и практика С++

Язык программирования С++История С и С++.

История начинается с 1970 год. (Bell Labs - бывшая американская, а ныне франко-американская корпорация, крупный исследовательский центр в области телекоммуникаций, электронных и компьютерных систем). Два сотрудника Кен Томпсон и Денис Ричи являлись ведущими инженерами команды перед которыми была поставлена задача написать переносимую ОС. Что это значит? Было несколько моделей компьютеров с разным устройством и хотелось написать систему, которая могла работать на разных моделях.

Для этой цели был разработан язык С для ОС Unix. К языку предъявлялись следующие требования:1. Эффективность, как язык Ассемблер.2. Более удобен, чем Ассемблер.

Например – сложение двух чисел.ASM Cmov AX, 2mov BX, [1234]add AX, BXmov [DX], AX

a = b +c

Более сложные вещи пишутся еще более громоздко.

3. Переносимость на уровне текстов исходных программ.

Т.е. если есть новая платформа, то для нее достаточно написать компилятор С, которых мог перевести программу на С для новой платформы. На языке ASM это было бы неудобно. У инструкций ASM есть операнды, которые могут отличаться на различных машинах. Т.е. Язык ASM наиболее близок к машинному коду. Хотелось от этого абстрагироваться. И такой язык был разработан - С.

Но в погоне за эффективностью он ближе к машине, чем к программисту. Например: 1. Нет специального символьного типа.2. В нем отсутствует булевский тип для логических значений (true и false). Любое число 0 – true. 0 = false.

Казус:if (a = 0) {

}

а сравнивается с нулем. На самом деле программист хотел написать a == 0. Ошибка, всегда будет false (в результате присваивания возвратился ноль) и в условие никогда не попадем. С точки зрения компилятора инструкция легальна. Ошибки не видит.

1980 год.Bell Labs – Бьерн Страуструп.Предложил новый язык С++, который является развитием языка С. Изначально язык назывался C++ with classes. Т.е. С и еще что-то. Необходимость этого языка возникла из-за необходимости писать программы работающие с большими БД.

Большинство программистов разрабатывают системы автоматизации (банка, магазина), т.е. автоматизация каких то процессов в бизнесе. Язык С++ добавляет что-то к языку С и при этом сохраняется обратная совместимость, т.е. С является подмножеством языка С++.

Почему так сделали, а не написали все заново при этом убрав многие ошибки в языке С? Потому что очень много кода написано на языке С. По истории все.

1

1

Page 2: C++ теория

Теория и практика С++

Порядок создания программы 1. С помощью текстового редактора напишите свою программу и

сохраните ее в файле. Это будет исходный код вашей программы.

2. Скомпилируйте исходный код. Для этого необходимо запустить программу, которая транслирует исходный код во внутренний язык, называемый машинным языком рабочего компьютера. Файл, содержащий транслированную программу — это объектный код вашей программы.

3. Свяжите объектный код с дополнительным кодом. Например, программы на C++ обычно используют библиотеки. Библиотека C++ содержит объектный код для набора компьютерных подпрограмм, называемых функциями. В процессе связывания ваш объектный код комбинируется с объектным кодом для используемых функций и с некоторым стандартным кодом запуска для формирования версии времени выполнения вашей программы. Файл, содержащий этот финальный продукт, называется исполняемым кодом.

1. Начальные сведения о языке С++Программа 1_1//программа отображает на экране сообщение#include <iostream> //директива препроцессораusing namespace std; //включает в программу определения

int main() //заголовок функции{ //начало тела функции cout << "I love C++"; //сообщение cout << "\n"; //начать новую строку return 0; //завершение функции main()} //конец тела функции

Результат:Come up and C++ me some time

Функция main()Программа С++ строится из отдельных блоков, называемых функциями. Программа 1_1 имеет

следующую структуру:

int main(){

операторыreturn 0;

}Эти строки во-первых, означают, что перед нами функция с именем main(), и, во-вторых, они содержат

описание работы этой функции. Вся совокупность приведенных выше строк составляет определение функции. Это определение состоит из двух частей: первой строки int main(), которая называется заголовком функции, и остальной части, заключенной в фигурные скобки, которая является телом функции.

Заголовок функции определяет интерфейс между функцией и остальной частью программы, а тело функции содержит инструкции для компьютера. Каждая завершенная инструкция называется оператором. Каждый оператор должен заканчиваться точкой с запятой.

Заключительный оператор в функции main(), называется оператором возврата, завершает функцию.

Что такое return 0? Дело в том, что иногда возникает необходимость запускать несколько команд в сложном сочетании. Это делается с помощью скриптов командной оболочки (язык сценариев). Под Windows - bat-файлы. Из сценария можно запустить программу на исполнение и посмотреть, какой результат она вернет операционной системе. Например это может использоваться для контроля над ошибками. Если программа вернет 0, то ошибка нет надо что-то сделать.

2

2

Page 3: C++ теория

Теория и практика С++

Заголовок функции в роли интерфейсаВ общем случае функция С++ активизируется или вызывается другой функцией, а заголовок функции

описывает интерфейс между нею и той функцией, которая ее вызывает. Слово, стоящее перед именем функции, называется возвращаемым типом функции; оно описывает информацию, передаваемую из функции обратно в ту функцию, которая ее вызывала. Информация в круглых скобках, следующих за именем функции, называется списком аргументов или списком параметров. Этот список содержит описание информации, передаваемой из вызывающей в вызываемую функцию.

Препроцессор C++ и файл iostream#include <iostream> //директива препроцессора

В языке C++, как и в С, используется препроцессор. Препроцессор — это программа, которая выполняет обработку файла исходного кода перед началом собственно компиляции.Эта директива заставляет препроцессор добавить содержимое файла iostream в вашу программу. Добавление или замена текста в исходном коде перед его компиляцией является одним из обычных действий препроцессора.

Имена заголовочных файловФайлы, такие как iostream, называются заголовочными (поскольку они включаются в начало файла).

Заголовочные файлы имеют расширение h и содержат прототипы функций.

Пространства именusing namespace std; //включает в программу определения

Это называется директивой using. Сейчас самое главное — просто запомнить ее.Поддержка пространства имен — это средство C++, предназначенное для упрощения разработки крупных программ и программ, в которых комбинируется существующий код от нескольких поставщиков, а также для помощи в организации таких программ. Одна из потенциальных проблем заключается в том, что вы можете работать с двумя готовыми продуктами, в каждом из которых присутствует, скажем, функция f1(). При использовании функции f1() компилятор не будет знать, какая конкретно версия этой функции имеется в виду. Средство пространств имен позволяет поставщику упаковать свой продукт в модуль, называемый пространством имен. Вы, в свою очередь, можете использовать название этого пространства имен для указания, продукт какого производителя вам необходим. Так, например, компания Micro может поместить свои определения в пространство имен Micro. Тогда полное имя. функции f1() будет выглядеть как Mi-cro::f1(). Аналогично, Macro::f1() может обозначать версию функции f1() от компании Macro.

КомментарииКомментарии обозначаются двойной наклонной чертой (//). Комментарий – это написанное программистом

примечание, которое предназначается для тех, кто будет изучать эту программу.

Вывод данных с использованием объекта coutcout << "Come up and C++ me some time.";

Информация, заключенная в двойные кавычки, является сообщением, которое должно быть выведено на экран. Любая последовательность символов, заключенная в двойные кавычки, называется строкой символов. Обозначение << указывает на то, что этот оператор отправляет данную строку в объект cout. cout - предопределенный объект, способный отображать на экране разнообразные данные – строки, числа и отдельные числа.

Символ новой строкиcout << "\n";

Комбинация символов \n является специальной формой записи важного понятия, называемого символом новой строки. Символ новой строки перемещает курсор на начало новой строки. Его можно комбинировать со строкой символов.

cout << "Come up and C++ me some time.\n";

Стиль форматирования исходного кода программ

3

3

Page 4: C++ теория

Теория и практика С++

1. В строке присутствует один оператор2. Открывающая и закрывающая фигурные скобки для функции располагаются каждая в отдельной строке3. Операторы тела функции располагаются с отступом от фигурных скобок4. Символы пробела не ставятся с обеих сторон круглых скобок, следующих за именем функции

Дополнительные сведения об операторахПрограмма представляет собой совокупность функций, а каждая функция – это совокупность операторов.

Программа 1_2//программа p1_2.cpp - отображает на экране значение переменной#include <iostream>using namespace std;

void main(){ int fleas; //создает целочисленную переменную fleas = 38; //присваивает этой переменной значение cout << "My cat has "; cout << fleas; //отображает на экране значение переменной fleas cout << " fleas.\n"; system("pause");

}

Результат:My cat has 38 fleas.

В программе 1_2 содержатся два новых вида операторов. Во-первых, оператор объявления создает переменную. Во-вторых, оператор присваивания присваивает этой переменной некоторое значение.

Результат выполнения программы:My cat has 38 fleas.

Операторы объявления и переменныеДля хранения в ПК какого-либо элемента информации необходимо указать, в каком месте памяти он будет

храниться, а также объем области памяти, требуемой для его хранения. Это можно сделать при помощи оператора объявления. Например:

int fleas;

Тип хранимой информации определяется словом int, который соответствует целому числу (положительные и отрицательные числа). Для идентификации области памяти в которой будет хранится значение используется слово fleas. Поскольку это значение может изменяться, fleas называется переменной.

Оператор присваиванияОператор присваивания присваивает значение некоторой области памяти. Например:

fleas = 38;

Операцию присваивания можно использовать последовательно. Например:int x;int y;int z;x = y = z = 88;

ВВОД ДАННЫХ

Программа 1_3// ввод и вывод#include <iostream>using namespace std;

void main()

4

4

Page 5: C++ теория

Теория и практика С++

{ int fleas; cout << "How many fleas does your cat have?\n"; cin >> fleas; //ввод в программу С++ //следующая строка объединяет выводимые данные cout << "Well, that's " << fleas << " fleas too many!\n"; system("pause");}

Результат:How many fleas does your cat have?112Well, that's 112 fleas too many!

Ввод данных в программу осуществляется при помощи объекта cin.

ФУНКЦИИИмеются две разновидности функций: функции с возвращаемыми значениями и без них.

Применение функций с возвращаемым значениемФункция с возвращаемым значением в результате своей работы выдает значение, которое можно присвоить

переменной. Например функция sqrt(), возвращает квадратный корень числа.

x = sqrt(6.25); //возвращает значение 2.5 и присваивает его переменной x

Выражение sqrt(6.25) вызывает функцию sqrt(). Выражение sqrt(6.25) называется вызовом функции, сама функция называется вызываемой, а функция, в которой находится вызов функции, называется вызывающей.

Значение в круглых скобках ( в данном случае 6.25) представляет собой информацию, которая передается в функцию. Это значение называется аргументом или параметром. Функция sqrt() вычисляет результат, который будет равным 2,5, и отправляет это значение обратно вызывающей функции. Это отправляемое значение называется возвращаемым значением функции.

Перед тем, как использовать функцию, компилятор С++ должен знать типы аргументов и возвращаемого значения функции. Эта информация передается компилятору с помощью прототипа функции.

ПОМНИТЕВ программе С++ для каждой используемой функции должен быть прототип.

Прототип функции информирует программу о типах данных.Прототип функции sqrt() выглядит так:

double sqrt(double); //прототип функции

Первое ключевое слово double означает, что функция sqrt() возвращает значение типа double. Ключевое слово double в круглых скобках означает, что функции sqrt() необходим аргумент типа double.

Если в программе применяется функция sqrt(), необходимо включить в код ее прототип. Это можно обеспечить двумя способами:

Вручную ввести прототип функции в исходных код программы. Включить в программу заголовочный файл cmath, который содержит прототип этой функции.

Итак, прототип описывает информацию, посылаемую в функцию и из функции. А определение функции содержит код функции.

Прототип функции должен находиться в программе до того места, где функция применяется впервые.

Программа 1_4// использование функции sqrt()#include <iostream>using namespace std;#include <cmath>

void main(){ double cover; //использовать вещественные числа типа double cout << "How many square feet of sheets do you have?\n"; cin >> cover;

5

5

Page 6: C++ теория

Теория и практика С++

double side; //создать еще одну переменную side = sqrt(cover); //вызвать функцию и присвоить возвращаемое значение переменной cout << "You can cover a square with sides of " << side; cout << "\nfeet with your sheets.\n";}

Результат:"How many square feet of sheets do you have?123.21You can cover a square with sides of 11.1feet with your sheets.

Разновидности функцийНекоторым функциям требует более одного элемента информации. Эти функции используют несколько

аргументов, разделяемых запятыми. Например прототип функции pow():

double pow(double, double); //прототип функции с двумя аргументами

Пример использования функции:

answer = pow(5.0, 8.0); //вызов функции со списком аргументов

Пример прототипа функции, которая не принимает аргументов:

int rand(void); //прототип функции, не принимающей аргументов

Пример использования функции без аргументов:

myGuess = rand(); //вызов функции, не имеющей аргументов

Пример прототипа функции, которая не возвращает значения:

void bucks(double); //прототип функции, не имеющей возвращаемого значения

Пример использования функции bucks():

bucks(1234.56); //вызов функции, возвращаемое значение отсутствует

Функции, определяемые пользователемДля пользовательской функции необходимо поместить прототип функции до ее вызова (обычно перед

функцией main()), а также необходимо написать исходный код определяемой функции.

Программа 1_5//программа p1_5.cpp - определение собственной функции#include <iostream>using namespace std;void simon(int); //прототип функции simon()

void main(){ simon(3); //вызов функции simon() cout << "Pick an integer: "; int count; cin >> count; simon(count); //повторный вызов этой функции}

void simon(int n) //определение функции simon(){ cout << "Simon says touch your toes " << n << " times.\n";} //в функциях без возвращаемого значения не требуется оператор return

Результат:Simon says touch your toes 3 times.Pick an integer: 512Simon says touch your toes 512 times.

Формат определения функции

6

6

Page 7: C++ теория

Теория и практика С++

Сначала идет заголовок функции, затем следует тело функции, заключенное в фигурные скобки. В общем случае формат следующий:

тип имя_функции(список_аргументов){

операторы}

ОПРЕДЕЛЯЕМАЯ ПОЛЬЗОВАТЕЛЕМ ФУНКЦИЯ С ВОЗВРАЩАЕМЫМ ЗНАЧЕНИЕМ

Программа 1_6//программа p1_6.cpp - преобразование стоунов в фунты#include <iostream>using namespace std;

int stonetolb(int); //прототип функции

void main(){ int stone; cout << "Enter the weight in stone: "; cin >> stone; int pounds=stonetolb(stone); cout << stone << " stone are "; cout << pounds << " pounds.\n";}

int stonetolb(int sts){ return 14 * sts;}

Результат:Enter the weight in stone: 1414 stone are 196 pounds.

ВОПРОСЫ ДЛЯ ПОВТОРЕНИЯ (1)1. Какой оператор следует использовать, чтобы вывести фразу "Hello, world" и перейти на начало строки?2. Какой оператор следует использовать, чтобы создать целочисленную переменную с именем cheese?3. Какой оператор следует использовать, чтобы присвоить переменной cheeses значение 32?4. Какой оператор следует использовать для ввода значения с клавиатуры и присвоения его переменной

cheeses?5. Какой оператор следует использовать для вывода предложения "We have X varieties of cheese",

где буква Х будет заменяться текущим значением переменной cheeses?6. Какую информацию о функции дает следующий заголовок функции:

int froop(double t)

7. В каком случае при определении функции не используется ключевое слово return?

УПРАЖНЕНИЯ ПО ПРОГРАММИРОВАНИЮ (1)1. Напишите программу, которая отображает на экране ваше имя и адрес.

2. Напишите программу, которая запрашивает расстояние в сантиметрах и преобразует его в миллиметры.

3. Напишите программу, которая состоит из двух определяемых пользователем функций и выводит на экран следующие данные:

Three blind miceThree blind miceSee how they runSee how they run

7

7

Page 8: C++ теория

Теория и практика С++

Одна функция, вызываемая дважды, должна отображать две первые строки, а другая, также вызываемая дважды, должна отображать остальные выходные данные.

4. Напишите программу, в которой функция main() вызывает определяемую пользователем функцию, принимающую в качестве аргумента значение температуры в градусах по Цельсию и возвращающую эквивалентное значение в градусах по Фаренгейту. По запросу программы температуру в градусах Цельсия вводит пользователь. Затем программа отображает результат. Данные, выводимые на экран, имеют следующий вид:

Please enter a Celsius value: 2020 degrees Celsius is 68 degrees Fahrenheit.

Для справок: формула для выполнения преобразования:

Fahrenheit = 1.8 x Celsius + 32.0

5. Напишите программу, в которой функция main() вызывает определяемую пользователем функцию. Эта функция принимает значение расстояния в световых годах и возвращает расстояние в астрономических единицах. Программа должна запрашивать ввод значения в световых годах и отображать результат, как показано в следующем примере:

Enter the number of light years: 4.24.2 light are 265608 astronomical unitsДля дробных чисел используйте тип double и следующую формулу преобразования:1 световой год = 63240 астрономических единиц

2. Представление данных переменнымиСуществует две группы типов данных: базовые и составные, или производные. Базовые типы данных служат

для представления целых чисел и чисел с плавающей точкой.

Имена переменныхДля переменных рекомендуется выбирать имена, отражающие их назначение.Существует несколько простых правил именования, которые обязательны для применения:

В именах можно использовать только следующие символы: буквы алфавита, цифры и символ подчеркивания (_).

Первый символ имени не может быть цифрой. Символы верхнего и нижнего регистров рассматриваются как разные. В качестве имен нельзя использовать ключевые слова языка С++. Имена, начинающие с двух символов подчеркивания или с символа подчеркивания и следующей за ним

буквы в верхнем регистре, зарезервированы для использования реализацией языка. Имена, начинающиеся с одного символа подчеркивания, зарезервированы реализацией языка в качестве глобальных идентификаторов.

В языке С++ на длину имени не накладывается никаких ограничений, т.е. учитывается каждый символ.Примеры допустимых и недопустимых имен С++:

int poodle; //допустимоеint Poodle; //допустимое и отличное от имени poodleint POODLE; //допустимое и еще более отличное от имени poodleInt terrier; //недопустимое – должно быть ключевое слово int, а не Intint my_stars3; //допустимоеint _Mystars3; //допустимое, но зарезервировано – начинается с

символа //подчеркиванияint 4ever; //недопустимое, так как начинается с цифрыint double; //недопустимое – double является ключевым словом С++int begin; //допустимоеint __fools; //допустимое, но зарезервированное – начинается с

двух //символов подчеркиванияint the_very_best_variable_i_can_be_version_112;

//допустимоеint honky-tonk; //недопустимое – дефис не допускается

Целочисленные типы данных

8

8

Page 9: C++ теория

Теория и практика С++

Целые числа – это числа без дробной части.В языке С++ различные типы данных различаются по объему памяти, используемому для их хранения.

Большой блок памяти может представлять большой диапазон целых чисел. Кроме того, одни типы данных )со знаком) могут представлять и положительные, и отрицательные значения, тогда как другие типы данных (без знака) не могут представлять отрицательные значения. Для описания объема памяти, используемого для хранения целочисленного значения, обычно применяют термин размерность. Чем больше памяти отводится для хранения значения, тем больше го размерность. Базовые типы целочисленных данных языка С++ (в порядке размерности) именуются char, short, int и long. Каждый их этих типов данных подразделяется на две разновидности: со знаком и без знака. В результате имеется набор из восьми различных типов целочисленных данных.

Типы данных short, int и longРазмерность – это количество битов или разрядов.

Размерность данных типа short не меньше 16 разрядов. Размерность данных типа int не меньше размерности данных типа short. Размерность данных типа long не меньше 32 разрядов и не меньше размерности данных типа int.

Примеры объявления переменных:

short score;int temperature;long position;

Следующая программа позволяет узнать размерность целых типов в конкретной системе.

Программа 2_1//программа p2_1.cpp - предельные значения целочисленных типов данных#include <iostream>using namespace std;#include <climits>

void main(){ //оператор sizeof возвращает размерность типа данных или переменной cout << "short is " << sizeof(short) << " bytes.\n"; cout << "int is " << sizeof(int) << " bytes.\n"; cout << "long is " << sizeof(long) << " bytes.\n\n"; //максимальное значение cout << "Maximum value:\n"; cout << "short: " << SHRT_MAX << "\n"; cout << "int: " << INT_MAX << "\n"; cout << "long: " << LONG_MAX << "\n\n"; //минимальное значение cout << "Minimum value:\n"; cout << "short: " << SHRT_MIN << "\n"; cout << "int: " << INT_MIN << "\n"; cout << "long: " << LONG_MIN << "\n\n"; //максимальное значение безнаковых типов cout << "Maximum unsigned value:\n"; cout << "unsigned short: " << USHRT_MAX << "\n"; cout << "unsigned int: " << UINT_MAX << "\n"; cout << "unsigned long: " << ULONG_MAX << "\n";}

Результат:short is 2 bytes.int is 4 bytes.long is 4 bytes.

Maximum value:short: 32767int: 2147483647long: 2147483647

9

9

Page 10: C++ теория

Теория и практика С++

Minimum value:short: -32768int: -2147483648long: -2147483648

Maximum unsigned value:unsigned short: 65535unsigned int: 4294967295unsigned long: 4294967295

ТИПЫ ДАННЫХ БЕЗ ЗНАКАВ программе 2_2 иллюстрируется использование беззнаковых типов данных. В ней также показано, что

может произойти, если в программе будет предпринята попытка выйти за границы диапазона возможных значений для целочисленных данных какого-либо типа. И здесь вы можете увидеть директиву препроцессора #define, которая дает команду заменить в программе все имена ZERO числами 0.

Программа 2_2// предельные значения целочисленных типов данных#include <iostream>using namespace std;

int main(){ const int ZERO = 0; short sam = SHRT_MAX; unsigned short sue = sam; cout << "Sam has " << sam << " dollars and Sue has " << sue; cout << " dollars deposited.\nAdd $1 to each account.\nNow "; sam = sam + 1; sue = sue + 1; cout << "Sam has " << sam << " dollars and Sue has " << sue; cout << " dollars deposited.\nPoor Sam!\n"; sam = ZERO; sue = ZERO; cout << "Sam has " << sam << " dollars and Sue has " << sue; cout << " dollars deposited.\n"; cout << "Take $1 from each account.\nNow "; sam = sam - 1; sue = sue - 1; cout << "Sam has " << sam << " dollars and Sue has " << sue; cout << " dollars deposited.\nLucky Sue!\n";}

Результат:Sam has 32767 dollars and Sue has 32767 dollars deposited.Add $1 to each account.Now Sam has -32768 dollars and Sue has 32768 dollars deposited.Poor Sam!Sam has 0 dollars and Sue has 0 dollars deposited.Take $1 from each account.Now Sam has -1 dollars and Sue has 65535 dollars deposited.Lucky Sue!

Целочисленная константаЦелочисленная константа – это константа, записываемая явно, например 212 или 1776. В языке С++

целочисленные константы могут записываться в трех системах счисления: десятичная, восьмеричная и шестнадцатеричная. Правила записи чисел в различных системах счисления следующие. Если первая цифра находится в диапазоне 1-9, то число является десятичным. Если первая цифра равна 0, а вторая находится в диапазоне от 1 до 7, то число является восьмеричным. Если первые два символа – 0х или 0Х, то речь идет о шестнадцатеричном числе.

Программа 2_3// демонстрирует использование шестнадцатеричных / восьмеричных констант

10

10

Page 11: C++ теория

Теория и практика С++

#include <iostream>using namespace std;

void main(){ int chest = 42; //десятичная целочисленная константа int waist = 0x42; //шестнадцатеричная целочисленная константа int inseam = 042; //восьмеричная целочисленная константа cout << "Monsieur cuts a striking figure!\n"; cout << "chest = " << chest << "\n"; cout << "waist = " << waist << "\n"; cout << "inseam = " << inseam << "\n";}

Результат:Monsieur cuts a striking figure!chest = 42waist = 66inseam = 34

По умолчанию объект cout отображает целые числа в десятичном виде, независимо от того, как они записаны в программе.

Тип данных char: символы и малые целые числа

Программа 2_4// предельные значения типа данных char#include <iostream>using namespace std;#include <climits>

void main(){ //оператор sizeof возвращает размерность типа данных или переменной cout << "char is " << sizeof(char) << " bytes.\n"; //максимальное значение cout << "Maximum value:\n"; cout << "char: " << CHAR_MAX << "\n"; //минимальное значение cout << "Minimum value:\n"; cout << "char: " << CHAR_MIN << "\n"; //максимальное значение безнаковых типов cout << "Maximum unsigned value:\n"; cout << "unsigned char: " << UCHAR_MAX << "\n";}

Тип данных char также предназначен для хранения одного символа из следующей таблицы0 · 32 [пробел] 64 @ 96 ` 128 · 160 [пробел] 192 А 224 а 1 · 33 ! 65 A 97 a 129 · 161 Ў 193 Б 225 б 2 · 34 " 66 B 98 b 130 · 162 ў 194 В 226 в 3 · 35 # 67 C 99 c 131 · 163 Ј 195 Г 227 г 4 · 36 $ 68 D 100 d 132 · 164 ¤ 196 Д 228 д 5 · 37 % 69 E 101 e 133 · 165 Ґ 197 Е 229 е 6 · 38 & 70 F 102 f 134 · 166 ¦ 198 Ж 230 ж 7 · 39 ' 71 G 103 g 135 · 167 § 199 З 231 з 8 * * 40 ( 72 H 104 h 136 · 168 Ё 200 И 232 и 9 * * 41 ) 73 I 105 i 137 · 169 © 201 Й 233 й 10 * * 42 * 74 J 106 j 138 · 170 Є 202 К 234 к 11 · 43 + 75 K 107 k 139 · 171 « 203 Л 235 л 12 · 44 , 76 L 108 l 140 · 172 ¬ 204 М 236 м 13 * * 45 - 77 M 109 m 141 · 173 205 Н 237 н 14 · 46 . 78 N 110 n 142 · 174 ® 206 О 238 о 15 · 47 / 79 O 111 o 143 · 175 Ї 207 П 239 п

11

11

Page 12: C++ теория

Теория и практика С++

16 · 48 0 80 P 112 p 144 · 176 ° 208 Р 240 р 17 · 49 1 81 Q 113 q 145 ‘ 177 ± 209 С 241 с 18 · 50 2 82 R 114 r 146 ’ 178 І 210 Т 242 т 19 · 51 3 83 S 115 s 147 · 179 і 211 У 243 у 20 · 52 4 84 T 116 t 148 · 180 ґ 212 Ф 244 ф 21 · 53 5 85 U 117 u 149 · 181 µ 213 Х 245 х 22 · 54 6 86 V 118 v 150 · 182 ¶ 214 Ц 246 ц 23 · 55 7 87 W 119 w 151 · 183 · 215 Ч 247 ч 24 · 56 8 88 X 120 x 152 · 184 ё 216 Ш 248 ш 25 · 57 9 89 Y 121 y 153 · 185 № 217 Щ 249 щ 26 · 58 : 90 Z 122 z 154 · 186 є 218 Ъ 250 ъ 27 · 59 ; 91 [ 123 { 155 · 187 » 219 Ы 251 ы 28 · 60 < 92 \ 124 | 156 · 188 ј 220 Ь 252 ь 29 · 61 = 93 ] 125 } 157 · 189 Ѕ 221 Э 253 э 30 · 62 > 94 ^ 126 ~ 158 · 190 ѕ 222 Ю 254 ю 31 · 63 ? 95 _ 127 · 159 · 191 ї 223 Я 255 я

Применение данных типа char демонстрируется в программе 2_5.

Программа 2_5// применение данных типа char#include <iostream>using namespace std;

void main(){ char ch; cout << "Enter a character:\n"; cin >> ch; cout << "Holla! "; cout << "Thank you for the " << ch << " character.\n";}

Результат:Enter a character:MHolla! Thank you for the M character.

Программа 2_6// сопоставление данных типа char и int#include <iostream>using namespace std;

void main(){ char c = 'M'; //переменной с присваивается ASCII-код символа М int i = c; //тот же код сохраняется в переменной типа int cout << "The ASCII code for " << c << " is " << i << "\n"; cout << "Add one to the character code:\n"; c = c + 1; i = c; cout << "The ASCII code for " << c << " is " << i << "\n"; cout << "\nDone\n";}

В этой программе показано, как определяются символьные константы: символ заключается в одинарные кавычки, например 'M'.

Коды управляющих последовательностей в языке С++Имя символа Код С++ Имя символа Код С++

Новая строка \n Сигнал \aГоризонтальнаятабуляция

\t Обратная наклоннаячерта

\\

Вертикальная табуляция \v Вопросительный знак \?

12

12

Page 13: C++ теория

Теория и практика С++

Возврат на одну позицию \b Одинарная кавычка \'Возврат каретки \r Двойная кавычка \"

Программа 2_7// использование управляющих последовательностей#include <iostream>using namespace std;

void main(){ cout << "\aOperation \"HyperHype\" is now activated!\n"; cout << "Enter your agent code:________\b\b\b\b\b\b\b\b"; long code; cin >> code; cout << "\aYou entered " << code << "...\n"; cout << "\aCode verified! Proceed with Plan Z3!\n";}

Результат:Operation "HyperHype" is now activated!Enter your agent code:007_____You entered 7...Code verified! Proceed with Plan Z3!

Тип данных boolПеременная Boolean – это переменная, которая может принимать два значения: true (истина) или false

(ложь). Также необходимо учитывать, что ненулевые значения интерпретируются как значения true, а нулевые значения – как значения false.

Примеры:bool isready = true;

Литералы true и false могут быть преобразованы в данные типа int путем повышения типа, при этом значение true преобразуется в 1, а false – в 0:

int ans = true; //переменной ans присваивается значение 1int promise = false; // переменной promise присваивается значение 0

Кроме того, любое числовое значение может быть преобразовано в значение типа bool неявно. Любое ненулевое значение преобразуется в значение true, а нулевое – в значение false:

bool start = -100; //переменной start присваивается значение truebool stop = 0; //переменной stop присваивается значение false;

Квалификатор constДля создания константы используется ключевое слово const:

const int MONTH = 12;

Обратите внимание: инициализация константы осуществляется вместе с ее объявлением. Следующая последовательность операторов некорректна:

const int toes; //в этом операторе значение константы toes не определеноtoes = 10; //слишком поздно!

Типы данных с плавающей точкойПредусмотрено три типа данных с плавающей точкой: float, double и long double.

Размерность данных с плавающей точкой:float – минимум 32 разряда;double – минимум 48 разрядов;long double – 80, 96 или 128 разрядов.

Арифметические операции

13

13

Page 14: C++ теория

Теория и практика С++

Имеются пять основных арифметических операции: сложение (+), вычитание (-), умножение (*), деление (/) и деление по модулю (%) (получение остатка от деления). В каждой из этих операций используются два числа (называемых операндами).

Разновидности операции деленияЕсли оба операнда являются целыми числами, то выполняется операция деления целых чисел. Это означает,

что любая дробная часть результата отбрасывается, и результат становится целым числом. Если оба или один из операндов – числа с плавающей точкой, то дробная часть результат сохраняется и результат будет числом с плавающей точкой.

Где объявляются переменныеКак правило, переменные объявляют в трех местах: внутри функций, в определении параметров функции и

за пределами всех функций. Соответственно такие переменные называются локальными, формальными параметрами и глобальными.

Локальные переменныеПеременные, объявленные внутри функции, называются локальными. Локальные переменные можно

использовать только в операторах, расположенных внутри блока, где они объявлены. Иначе говоря, локальные переменные невидимы снаружи их блока. Напомним, что блок ограничен открывающей и закрывающей фигурными скобками.

Чаще всего локальные переменные объявляются внутри функций. Рассмотрим два примера.

void func1(void){

int x;x = 10;

}

void func2(void){

int x;x = -199;

}

Переменная x объявлена дважды: сначала — в функции func1(), а затем — в функции func2(). Переменная х из функции func1() не имеет никакого отношения к переменной x, объявленной внутри функции func2(). Каждая из этих переменных существует только внутри блока, где она была объявлена.

Из соображений удобства и по традиции большинство программистов объявляют переменные; используемые в функции, сразу после открывающей фигурной и перед всеми остальными операторами. Однако следует иметь в виду, что локальные переменные можно объявлять в любом месте блока.

Локальную переменную можно проинициализировать неким значением. Оно будет присваиваться переменной каждый раз при входе в блок. Рассмотрим в качестве мера программу, которая десять раз выводит на печать число 10.

Программа 2_8// присвоение значение переменной во время объявления#include <iostream>using namespace std;

void f();

int main(){

int i;f();f();f();

}

void f(){

int j = 3;

cout << j << " ";

14

14

Page 15: C++ теория

Теория и практика С++

j = j + 1; // Этот оператор не имеет долговременного эффекта. }

Формальные параметрыЕсли функция имеет аргументы, следует объявить переменные, которые будут принимать их значения. Эти

переменные называются формальными параметрами. Внутри функции они ничем не отличаются от других локальных переменных. Как показано в приведенном ниже фрагменте программы, объявление таких переменных должно размещаться сразу после имени функции и заключаться в скобки.

/* Функция возвращает 1, если символ с является частью строки s; в противном случае она возвращает 0 */ int is_in(char *s, char с){

while(*s)if(*s == c) return 1;else s++;return 0;

}

Функция is_in() имеет два параметра: вис. Она возвращает 1, если символ с является частью строки s, в противном случае функция возвращает 0.

Тип формальных параметров задается при их объявлении. После этого их можно использовать как обычные локальные переменные. Учтите, что, как и локальные переменные, формальные параметры являются динамическими и разрушаются при выходе из функции.

Глобальные переменныеВ отличие от локальных, глобальные переменные доступны из любой части программы и могут быть

использованы где угодно. Кроме того, они сохраняют свои значения на всем протяжении выполнения программы. Объявления глобальных переменных должны размещаться вне всех функций. Эти переменные можно использовать в любом выражении, в каком бы блоке оно не находилось.

В приведенном ниже фрагменте программы переменная count объявлена вне функций. Несмотря на то что ее объявление расположено до функции main(), переменную можно было бы с таким же успехом объявить в другом месте, но вне функций и до ее первого использования. И все же лучше всего размещать объявление глобальных переменных в самом начале программы.

Программа 2_9// глобальные переменные#include <iostream>using namespace std;

int count; /* Переменнная count является глобальной */

void func1();void func2();

void main(){

count = 100;func1();

}

void func1(void){

int temp;

temp = count;func2();cout << "count = " << count << "\n"; /* Выведет число 100. */

}

void func2(void){

int count;

15

15

Page 16: C++ теория

Теория и практика С++

for(count=1; count<10; count++)cout << '.';

} Результат: . . . . . . . . . .count = 100

Присмотритесь к этой программе повнимательнее. Заметьте, что, хотя ни функция main(), ни функция func1() не содержат объявления переменной count, обе эти функции успешно ее используют. Однако внутри функции func2() объявлена локальная переменная count. Когда функция func2() ссылается на переменную count она использует лишь локальную переменную, а не глобальную. Если глобальная и локальная переменные имеют одинаковые имена, то все ссылки на имя переменной внутри блока будут относиться к локальной переменной и не влиять на ее глобальную тезку. Возможно, это удобно, однако об этой особенности легко забыть, и тогда действия программы могут стать непредсказуемыми.

ВОПРОСЫ ДЛЯ ПОВТОРЕНИЯ (2)1. Определите следующие переменные:

a. Типа short со значением 80b. Типа unsigned int со значением 42110c. Целочисленного типа со значением 3 000 000 000

2. Эквивалентны ли следующие операторыchar grade = 65;char grade = 'A';

3. Как с помощью программы найти, какому символу соответствует код 88?

УПРАЖНЕНИЯ ПО ПРОГРАММИРОВАНИЮ (2)1. Напишите программу, которая запрашивает рост в сантиметрах, а затем выражает его в метрах и

сантиметрах. Пусть в программе используется символ подчеркивания, чтобы указать место, где вводить ответ.

Результат:Enter your height in cm: 182Your height: 1 m 82 cm

2. Напишите программу, которая запрашивает ваш рост в метрах и ваш вес в килограммах (для хранения этой информации используйте две переменные). Пусть программа вычислит и отобразит на экране ваш ИМТ (индекс массы тела). Чтобы вычислить ИМТ необходимо разделить вес в килограммах на рост в метрах, возведенный в квадрат

Результат:Enter your height in m: 1.82Enter your weight in kg: 70Your IMT = 21.1327

3. Напишите программу, которая запрашивает длину пути, который вы проехали, в километрах и количество израсходованных литров бензина, а затем отображает на экране значение, показывающее, расход бензина в литрах на 100 километров.

Результат:Enter km: 250Enter litre: 20The charge on 100 km = 8

3. Составные типы данныхСоставные типы данных формируются на основе базовых целочисленных типов и типов с плавающей

точкой. Это классы, указатели, структуры и массивы.

МассивыМассив представляет собой набор переменных с одним именем и разными индексами. Каждая такая

переменная называется элементом массива. Количество хранящихся в массиве элементов называется размером массива. Все элементы массива имеют одинаковый тип.

Объявление массива должно содержать три аргумента:

16

16

Page 17: C++ теория

Теория и практика С++

тип каждого элемента имя массива количество элементов в массиве

например:short months[12]; //создает массив с именем months, из 12 элементов типа short

Общая форма записи для объявления массива выглядит так:

typeName arrayName[arraySize];

Выражение arraySize, которое задает количество элементов, должно быть константой, и не может быть переменной.

Программа 3_1// программа p3_1.cpp - небольшие массивы целых чисел#include <iostream>using namespace std;

void main(){ int yams[3]; //создается массив из трех элементов yams[0] = 7; //присвоение значения первому элементу yams[1] = 8; yams[2] = 6; int yamcosts[3] = {20, 30, 5}; //создание и инициализация массива cout << "Total yams = "; cout << yams[0] + yams[2] + yams[3] << "\n"; cout << "The package with " << yams[1] << " yams costs "; cout << yamcosts[1] << " center per yam.\n"; int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1]; total = total + yams[2] * yamcosts[2]; cout << "The total yam expense is " << total << " cents.\n"; cout << "Size of yams array = " << sizeof yams << " bytes.\n"; cout << "Size of one element = " << sizeof yams[0] << " bytes.\n";}

Результат:Total yams = 14The package with 8 yams costs 30 center per yam.The total yam expense is 410 cents.Size of yams array = 12 bytes.Size of one element = 4 bytes.

Правила инициализации массивовФорму инициализации массива можно использовать только при определении массива. После этого

применять ее нельзя, нельзя также целиком присвоить один массив другому:

int cards[4] = {3, 6, 8, 10}; //допустимоint hand[4]; //допустимоhand[4] = {5, 6, 7, 9}; //недопустимоhand = cards; //недопустимо

Однако можно, используя индексы, присваивать значения элементам массива индивидуально.Если массив инициализируется частично, то транслятор устанавливает для оставшихся элементов нулевые

значения. Таким образом, можно легко присвоить всем элементам массива нуль – нужно лишь явно присвоить нуль первому элементу и предоставить компилятору инициализировать оставшиеся элементы:

float totals[500] = {0};

СтрокиСтрока – это ряд символов, хранящихся в последовательно расположенных байтах памяти. Строки имеют

одну особенность: последний символ каждой строки – нулевой символ. Это символ, имеющий нулевой код ASCII (записывается как \0); он служит для обозначения конца строки.

char dog[5] = {'b', 'e', 'a', 'u', 'x'}; //не строка!

17

17

Page 18: C++ теория

Теория и практика С++

char cat[5] = {'f', 'a', 't', 's', '\0'}; //строка

Оба массива являются массивами символов, но только второй из них – строка. В строках нулевой символ играет фундаментальную роль. С++ имеет много функций, которые обрабатывают строки. Их работа строится по единому принципу: функция обрабатывает строку посимвольно до достижения нулевого символа.

Имеется более лучший способ присвоения строки символьному массиву. Нужно просто использовать строку в кавычках, называемую строковой константой или строковым литералом.

char bird[10] = "Mr. Cheep"; //подразумевается символ \0 в концеchar fish[] = "Bubbles"; //предоставим компилятору определять размер массива

ПОМНИТЕВычисляя минимальный размер массива, в который необходимо записать строку, не забывайте учитывать

и конечный нулевой символ.

Обратите внимание на то, что строковая константа (двойные кавычки) и символьная константа (одинарные кавычки) не являются взаимозаменяемыми. Символьная константа, такая как 'S', является короткой записью кода для символа. В системе ASCII запись 'S' – это просто другой способ записи числа 83. Таким образом, оператор

char shirt_size = 'S'; //допустимо

присваивает значение 83 переменной shirt_size. При этом "S" представляет собой строку, состоящую из двух символов, S и \0.

char shirt_size = "S"; //запрещенное смешение типов

Конкатенация строкЛюбые две строковые константы, разделенные только пробельными символами (пробелами, знаками

табуляции и символами новой строки) автоматически объединяются в одну. Таким образом, все следующие операторы вывода эквивалентны:

cout << "This is" " a map";cout << "This is a map";cout << "This is"" a map";

Использование строк в массивеСуществует два наиболее распространенных способа записи строки в массив: первый – инициализация

массива строковой константой, второй – считывание в массив результатов ввода с клавиатуры или из файла. В программе 3_2 используется функция стандартной библиотеки strlen(), чтобы узнать длину строки. Стандартный заголовочный файл cstring обеспечивает объявление для этой и многих других функций, связанных со строками.

Программа 3_2// программа p3_2.cpp - сохранение строк в массиве#include <iostream>#include <cstring> //для функции strlen()using namespace std;

void main(){ const int Size = 15; char name1[Size]; //пустой массив char name2[Size] = "C++owboy"; //инициализация массива cout << "Howdy! I'm " << name2; cout << "! What's your name?\n"; cin >> name1; cout << "Well, " << name1 << ", your name has "; cout << strlen(name1) << " letters and is stored\n"; cout << "in am array of " << sizeof name1 << " bytes.\n"; cout << "Your initial is " << name1[0] << ".\n"; name2[3] = '\0'; //нулевой символ cout << "Here are the first 3 characters of my name: "; cout << name2 << "\n";

18

18

Page 19: C++ теория

Теория и практика С++

}

Результат:Howdy! I'm C++owboy! What's your name?BasicmanWell, Basicman, your name has 8 letters and is storedin am array of 15 bytes.Your initial is B.Here are the first 3 characters of my name: C++

Проблемы при вводе строкПрограмма 3_3// программа p3_3.cpp - чтение более чем одной строки#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

const int ArSize = 20;char name[ArSize];char dessert[ArSize];cout << "Введите свое имя: ";cin >> name;cout << "Введите свой любимый десерт: ";cin >> dessert;cout << "У меня есть вкусный " << dessert;cout << " для вас, " << name << ".\n";

}

Результат:Enter your name:Alistar DreebEnter your favorite dessert:I have some delicious Dreeb for you, Alistar.

В программе с клавиатуры вводится имя пользователя и его любимый десерт, а затем отображается информации.

Проблема заключается в том, что принцип работы cin основан на допущении, что для ограничения строки используются пробельные символы. Это означает, что cin считывает только одно слово, когда он становится входным для символьного массива, и помещает строку в массив, автоматически добавляя конечный нулевой символ.

На практике у нас получается следующее: cin считает Alistar как полную первую строку и помещает ее в массив name, а слово Dreeb остается в очереди ввода. Когда cin ищет очередь ввода для ответа на вопрос о любимом десерте, то находит там слово Dreeb. Тогда cin, недолго думая, "проглатывает" Dreeb" и помещает слово в массив dessert.

Следующая проблема заключается в следующем. cin не дает возможности предотвратить помещение 30-символьной строки в 20-символьный массив

Ввод ориентированный на строки: функции getline()Функция getline() читает целую строку, используя переданный с помощью клавиши ENTER символ

новой строки, чтобы пометить конец ввода. Для обращения к этому методу используется следующий вызов функции: cin.getline(). Функция требует передачи двух параметров. Первый – имя массива, предназначенного для сохранения введенной строки, а второй ограничивает количество символов, которые нужно считать.

cin.getline(name,20);

Программа 3_4// программа p3_4.cpp - чтение более одного слова с помощью функции getline

19

19

Page 20: C++ теория

Теория и практика С++

#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

const int ArSize = 20;char name[ArSize];char dessert[ArSize];cout << "Введите ваше имя: ";cin.getline(name, ArSize); // читать до символа новой строкиcout << "Введите ваш любимый десерт: ";cin.getline(dessert, ArSize);cout << "У меня есть вкусный " << dessert;cout << " для вас, " << name << ".\n";

}

Результат:Enter your name:Dirk HammernoseEnter your favorite dessert:Ice-creamI have some delicious Ice-cream for you, Dirk Hammernose.

Теперь программа читает полные имена и отображает для пользователя заказанные десерты! Функция getline() воспринимает строку "в один присест". Она читает вводимые данные до конца символа новой строки, который обозначает конец строки, однако не сохраняет его, а заменяет при сохранении строки нулевым символом.

Смешанный ввод строк и чиселСмешанный ввод строк и чисел с помощью ориентированный на строки функций может вызывать

затруднения.

Программа 3_5// программа p3_5.cpp - чередование ввода чисел и строк#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "В каком году построен ваш дом? \n";int year;cin >> year;cout << "По какому адресу он расположен? \n";char address[80];cin.getline(address, 80);cout << "Год постройки: " << year << endl;cout << "Адрес: " << address << endl;cout << "Готово! \n";

}

Результат:What year was your house built?1996What is its street address?Year built: 1996Address:Done!

20

20

Page 21: C++ теория

Теория и практика С++

При работе с данной программой пользователь никогда не получит возможности ввести адрес. Проблема состоит в том, что объект cin, считав значение года, оставляет символ новой строки, сгенерированный с помощью клавиши ENTER, во входной очереди. Затем функция cin.getline() воспринимает символ новой строки так, как будто это пустая строка, и присваивает нулевую строку элементу массива address. Чтобы исправить ошибку, следует считать и отбросить символ новой строки перед чтением адреса. Это можно сделать при помощи функции cin.get().

cin >> year;cin.get();

СТРУКТУРЫСтруктура – более универсальная форма данных, чем массив, поскольку может содержать элементы,

относящиеся к различным типам данных.Структура – определяемый пользователем тип данных. Объявление структуры служит для того, чтобы

задавать свойства типов данных. После определения типа можно создавать переменные этого вновь созданного типа. Таким образом, создание структуры предусматривает два этапа.

Сначала создается описание структуры. Затем можно создавать структурные переменные.

struct inflatable //описание структуры{

char name[20]; //элемент типа arrayfloat volume; //элемент типа floatdouble price; //элемент типа double

};

Ключевое слово struct указывает на то, что код определяет компоновку структуры. Идентификатор inflatable – имя, или дескриптор, для этой формы. Таким образом, inflatable представляет собой имя нового типа данных. Между фигурными скобками находится список типов данных, которые будут содержаться в структуре. Каждый элемент списка – это оператор объявления. Каждый отдельный элемент в списке называется элементом структуры.

Когда шаблон готов, можно создавать переменные данного типа:

inflatable hat; //hat - структурная переменная типа inflatableinflatable woopie_cushion; //структурная переменная типа inflatableinflatable mainfraim; //структурная переменная типа inflatable

Поскольку переменная hat имеет тип inflatable, для обращения к отдельным элементам можно использовать оператор принадлежности (.). Например, выражение hat.volume указывает на элемент vol-ume структуры.

Программа 3_6// программа p3_6.cpp - простая структура#include <iostream>using namespace std;struct inflatable{ char name[20]; float volume; double price;};

void main(){ inflatable guest = { "Glorious Gloria", 1.88, 29.99 };

inflatable pal =

21

21

Page 22: C++ теория

Теория и практика С++

{ "Audacious Arthur", 3.12, 32.99 };

cout << "Expand your guest list with " << guest.name; cout << " and " << pal.name << "!\n"; cout << "You can have both for $"; cout << guest.price + pal.price << "!\n";}

Результат:Expand your guest list with Glorious Gloria and Audacious Arthur!You can have both for $62.98!

Примечание к программеСуществует один важный момент, который состоит в том, чтобы определить, где следует размещать

объявление структуры. Возможны два варианта. Первый (внутреннее объявление) – поместить объявление структуры внутри функции main() сразу после отрывающей скобки. Второй (внешнее объявление) - поместить объявление структуры перед функцией main(). Внешнее объявление может использовать всеми функциями, следующими за main(), тогда как внутреннее объявление может быть использовано только той функцией, в которой оно содержится.

Другие свойства структурМожно передавать структуры функциям в качестве аргументов, а также использовать структуру, как

возвращаемое значение функции. Кроме того, допускается использование оператора присвоения (=), чтобы присвоить значение одной структуры другой структуре того же типа. Данный вид операции называется поэлементным присваиванием.

Программа 3_7// программа p3_7.cpp - присвоение значений структурам#include <iostream>using namespace std;

struct inflatable{ char name[20]; float volume; double price;};

void main(){ inflatable bouquet = { "sunflowers", 0.20, 12.49 }; inflatable choice; cout << "bouquet: " << bouquet.name << " for $"; cout << bouquet.price << "!\n"; choice = bouquet; //присвоение значения одной структуры другой структуре cout << "choice: " << choice.name << " for $"; cout << choice.price << "!\n";}

Результат:bouquet: sunflowers for $12.49!choice: sunflowers for $12.49!

Указатели и свободная память

22

22

Page 23: C++ теория

Теория и практика С++

Три фундаментальных свойства, которые должна учитывать компьютерная программа при сохранении данных. Перечислим их:

где хранится информация какое значение там, хранится вид хранящейся информации

Для реализации этих свойств использовался один из возможных методов — определение простой переменной. Оператор объявления определяет тип и символическое имя значения, а также заставляет программу выделить область памяти для значения и внутренними средствами отслеживать ее адрес.

Теперь рассмотрим второй метод. Он приобретает особое значение при разработке классов C++. Этот метод основан на указателях, которые являются переменными, сохраняющими адреса значений вместо непосредственно самих значений. Но прежде чем, рассматривать указатели, обратимся к способу явного определения адресов обычных переменных. Этот способ заключается в применении к переменной операции определения адреса, представленной знаком &. Например, если home – переменная, &home является ее адресом.

Программа 3_8// использование оператора & для определения адреса#include <iostream>using namespace std;

void main(){ int donuts = 6; double cups = 4.5; cout << "donuts value = " << donuts; cout << " and donuts address = " << &donuts << "\n"; cout << "cups value = " << cups; cout << " and cups address = " << &cups << "\n";}

Результат:donuts value = 6 and donuts address = 0012FF7Ccups value = 4.5 and cups address = 0012FF74

При отображении адресов объект cout использует шестнадцатеричное представление, потому что оно обычно применяется для записи адресов памяти. В нашей реализации значение переменной cups хранится в ячейке памяти с меньшей величиной адреса по сравнению с переменной donuts. Разность между двумя адресами составляет 8. Это имеет смысл, поскольку переменная cups имеет тип double, размерность которого составляет восемь байта. Конечно, в разных системах значения адресов будут различными. Кроме того, значение переменной donuts может предшествовать в памяти значению переменной cups. При этом разница адресов будет составлять 8 байтов, поскольку переменная donuts имеет тип int.

Используя в таком случае обыкновенные переменные, можно обработать значение как именованную величину, а его адрес как производную величину. Теперь ознакомимся с принципом использования указателей, который занимает важное место в философии языка C++ относительно программного управления памятью.

В соответствии с новой стратегией обработки хранящихся в памяти данных местоположение значения трактуется как именованная величина, а значение — как производная (порожденная) величина. Специальный тип переменной – указатель – содержит адрес значения. Таким образом, имя указателя представляет местонахождение значения в памяти. Применяя операцию *, называемую косвенным значением или операцией разыменованием, получаем значение, хранящееся по данному адресу. Предположим, что manly – указатель. Тогда выражение manly представляет собой адрес, а *manly – значение по данному адресу. Комбинация *manly становится эквивалентом обычной переменной типа int. Эти моменты отражены в программе 3_9. Здесь также показано, как объявлять указатель.

Программа 3_9// наша первая переменная-указатель#include <iostream>using namespace std;

void main(){ int updates = 6; //объявление переменной int *p_updates; //объявление указателя на значением типа int

23

23

Page 24: C++ теория

Теория и практика С++

p_updates = &updates; //присвоение адреса значения типа int указателю //выражения значений двумя способами cout << "Values: updates = " << updates; cout << ", *p_updates = " << *p_updates << "\n"; //выражение адресов двумя способами cout << "Addresses: &updates = " << &updates; cout << ", p_updates = " << p_updates << "\n"; //использование указателя для изменения значения *p_updates = *p_updates + 1; cout << "Now updates = " << updates << "\n";}

Результат:Values: updates = 6, *p_updates = 6Addresses: &updates = 0012FF7C, p_updates = 0012FF7CNow updates = 7

Очевидно, что переменная updates типа int и переменная-указатель p_updates являются всего лишь двумя сторонами одной и той же медали. Переменная updates представляет прежде всего значение, а операция & используется для получения адреса, тогда как переменная p_updates представляет адрес, а операция * служит для получения значения. Поскольку p_updates указывает на значение updates, выражения *p_updates и updates совершенно эквиваленты. Выражение *p_updates можно использовать точно так же, как переменную типа int. Как видно из программы, можно даже присваивать значения переменной *p_updates. При этом изменяется указываемое значение переменной updates.

int jumbo = 23;int *pe = &jumbo;

эти выражения эквива-лентны

эти выражения эквива-лентны

jumbo*pe

&jumbope

значение23

адрес0x2ac8

Две стороны одной медали

Рисунок 1

Объявление и инициализация указателейРассмотрим процесс объявления указателей. Компьютеру необходимо отслеживать тип значения, на которое

ссылается указатель. Например, адрес переменной типа char выглядит так же, как и адрес переменной типа double, но для типов char и double используется различное количество байтов и различные внутренние форматы для хранения значений. Поэтому при объявлении указателя необходимо точно определять, на какой тип данных он указывает.

Последний пример содержит следующее объявление:

int *p_updates;

Это выражение указывает, что комбинация *p_updates принадлежит к типу int. Поскольку операция * выполняется по отношению к указателю, переменная p_updates сама должна быть указателем. Мы говорим, что p_updates указывает на значение типа int. Мы также говорим, что типом для p_updates должен быть указатель на int или, более кратко, int *. Повторим: p_updates является указателем (адресом), a *p_updates — переменной типа int, но не указателем (рис. 2).

24

24

Page 25: C++ теория

Теория и практика С++

100010021004100610081010101210141016

адрес впамяти

имяпеременной

int ducks = 12;

ducks

birddog

12

1000

birddog

ducksуказываетна

int *birddog = &ducks;создает переменную

и сохраняет в нейзначение 12ducks

создает переменную исохраняет в ней адрес

birddog ducks

Рисунок 2В указателях хранится адрес

Отметим, что отделять пробелами знак операции * не обязательно. Программисты, работающие на С, традиционно применяют следующую форму записи:

int *ptr;

При использовании данной формы записи подчеркивается то, что комбинации *ptr имеет значение типа int. Широко распространена и другая форма этого выражения:

int* ptr;

В ней акцентируется внимание на том, что int* — это тип "указатели на int". Компилятору же совершенно безразлично, где будут размещены пробелы. Однако не следует упускать из виду, что, например, объявление

int* p1, p2;

создает один указатель (р1) и одну обычную переменную (р2), другими словами, необходимо использовать знак * для каждого имени объявляемой переменной-указателя.

Для объявления указателей на другие типы переменных используется аналогичный синтаксис:

double *tax_ptr; // tax_ptr указывает на тип doublechar *str; // str указывает на тип char

Поскольку tax_ptr объявляется как указатель на тип double, компилятор определяет, что значение *tax_ptr принадлежит к типу double. Другими словами, он распознает, что *tax_ptr представляет собой величину, которая хранится в формате с плавающей точкой и занимает (в большинстве систем) восемь байтов. Переменная-указатель никогда не бывает просто указателем. Она всегда указывает на определенный тип данных. Так, переменная tax_ptr имеет тип "указатель на тип double" (или тип double*), a str — "указатель на тип char" (или char*). Несмотря на то, что обе переменные являются указателями, они указывают на два различных типа данных. Подобно массивам, указатели являются производными от других типов данных.

Заметим, что в то время, как tax_ptr и str указывают на типы данных, имеющие различную размерность, сами по себе две переменные — tax_ptr и str — имеют одинаковый размер. Иными словами, адрес переменной типа char имеет тот же размер, что и адрес переменной типа double (точно так же, как номер дома 1016 может означать адрес большого универмага, а 1024 — адрес маленького коттеджа). По размеру или значению адреса ничего нельзя сказать о размере или типе переменной, так же, как и по номеру дома — о здании, которое находится по этому адресу. Обычно для хранения адреса требуются два или четыре байта, в зависимости от компьютера.

Для инициализации указателя можно использовать оператор объявления. В этом случае инициализируется указатель, а не указываемое значение. Таким образом, следующие операторы:

int higgens = 5;int *pt = &higgens;

присваивают указателю pt (но не *pt) значение &higgens.

В программе 3_10 демонстрируется инициализация указателя с присвоением ему адреса.

Программа 3_10

25

25

Page 26: C++ теория

Теория и практика С++

// программа p3_10.cpp - инициализация указателя#include <iostream>using namespace std;

void main(){ int higgens = 5; int *pi = &higgens; cout << "Value of higgens = " << higgens << "; Address of higgens = " << &higgens << "\n"; cout << "Value of *pi = " << *pi << "; Address of pi = " << pi << "\n";}

Результат:Value of higgens = 5; Address of higgens = 0012FF7CValue of *pi = 5; Address of pi = 0012FF7C

Можно видеть, что программа присваивает адрес переменной higgens указателю pi, но не значению *pi.Отметим однако, что неосторожное использование указателей может привести к нежелательным

последствиям. Крайне важно не забывать о том, что при создании указателя в C++ компьютер выделяет память для хранения адреса, но не для хранения данных, на которые указывает этот адрес. Чтобы выделить места для данных, требуется дополнительное действие. Если вы такое действие не выполните (как в приведенном ниже примере), то окажетесь на прямом, пути к краху:

long *fellow; //создание указателя на переменную типа long*fellow = 223323; //помещение значения по неопределенному адресу

Конечно, переменная fellow является указателем. Но на что она указывает? Программа не присвоила ей адрес. Куда же будет помещено значение 223323? Мы не в состоянии ответить на этот вопрос. Поскольку переменная fellow не была инициализирована, она может принять любое значение. Какое бы значение она ни имела, программа будет интерпретировать его как адрес, по которому нужно сохранить число 223323. Если fellow имеет значение 1200, то компьютер попытается поместить данные по адресу 1200, даже если окажется; что этот адрес находится в середине кода программы. Понятно одно: куда бы ни указывал указатель fellow, это не то место, куда вы намерены поместить число 223323. Ошибки подобного рода коварны. Кроме того, их сложно выявлять.

Присваивание указателейУказатель можно присваивать другому указателю.

Программа 3_10_1//присваивание указателей#include <stdio.h>void main(){ int x; int *p1, *p2;

p1 = &x; p2 = p1;

printf("%p", p2); //Выводит адрес переменной x, а не ее значение!}

Теперь на переменную x ссылаются оба указателя p1 и p2. Адрес переменной x выводится на экран с помощью форматного спецификатора %p и функции printf().

Указатели и числаУказатели не принадлежат к целочисленным типам, даже, несмотря на то, что в компьютере, как правило,

адреса хранятся как целочисленные значения. Более того, указатели принципиально отличаются от типа целочисленных значений. Целочисленные значения являются числами, с которыми можно производить сложение, вычитание, деление и т.д. Указатель же описывает местонахождение, и поэтому, например, перемножение двух адресов лишено смысла. Различие между указателями и целочисленными значениями

26

26

Page 27: C++ теория

Теория и практика С++

проявляется в операциях, в которых участвуют данные обоих типов; другими словами, нельзя просто присвоить целочисленное значение переменной – указателю:

int *pt;pt = 0xB8000000; // несоответствие типа

Здесь левая часть является указателем на тип int, поэтому ей можно присвоить значение адреса, а вот правая — просто целочисленное значение. Вы, возможно знаете, что 0xB8000000 представляет собой комбинированный адрес области видеопамяти, состоящий из сегмента и смещения, но программа к такому выводу прийти никак не может: ведь ничто в операторе прямо не указывает на то, что данное значение является адресом. Язык С позволяет выполнять подобные присвоения, однако в С++ принято более строгое соблюдение типов данных, и компилятор выдаст сообщение об ошибке несовпадения типов. Чтобы использовать числовое значение как адрес, необходимо выполнить приведение типов для преобразования числа в приемлемый для значения адреса тип:

int *pt;pt = (int *) 0xB8000000; // сейчас типы совпадают

Теперь обе части оператора присваивания представляют собой адреса целочисленных значений, поэтому присвоение возможно.

Выделение памяти с помощью оператора newДо сих пор мы присваивали указателям адреса переменных. Переменная является именованной областью

памяти которая резервируется во время компиляции, а указатели всего лишь служат псевдонимами для областей памяти, к которым можно в любом случае обратиться напрямую по имени. Истинная ценность указателей проявляется тогда, когда для хранения данных выделяется неименованная область памяти во время выполнения программы. В этом случае указатели становятся единственным способом доступа к этим областям памяти. В языке С можно распределять память при помощи библиотечной функции malloc(). Ее можно использовать и в C++, но этот язык предоставляет нам более эффективное средство — оператор new.

Создадим неименованное хранилище значений типа int во время исполнения программы и будем обращаться к этим значениям с помощью указателя. Здесь ключевое значение имеет оператор new. Мы указываем оператору new, для какого типа данных выделяется память. Оператор new ищет блок памяти нужного размера и возвращает адрес этого блока. Присваивая этот адрес указателю, мы получаем желаемое. Ниже приводится пример реализации описанной технологии:

int *pn = new int;

Выражение new int сообщает программе о том, что необходимо некоторое новое хранилище, подходящее для размещения значения типа int. Оператор new анализирует тип для вычисления необходимого количества байтов. Затем он отыскивает область памяти и возвращает адрес. Далее присваиваем адрес переменной pd, которая объявляется как указатель на тип int. Теперь рd является адресом, а *рd — значением, хранящимся по этому адресу. Сравним это с присваиванием указателю адреса переменной:

int higgens;int *pt = &higgens;

В обоих примерах (рn и pt) указателю присваивается адрес переменной типа int. Во втором случае можно также обращаться к переменной типа int по имени: higgens. В первом же случае обращение возможно исключительно через указатель. Возникает вопрос: так как область памяти, на которую указывает рn, не имеет имени, как же к ней обращаться? Мы говорим, что рn указывает на объект данных. Но это не "объект" в смысле "объектно-ориентированного программирования"; это всего лишь "объект" в смысле "предмет". "Объект данных", под которым понимается любой блок памяти, выделенный для хранения элементарной группы данных, является более, общим понятием, чем "переменная". Поэтому переменная также является объектом данных, но память, на которую указывает рn, не является переменной. Способ управления объектами данных с помощью указателей может показаться на первый взгляд довольно неудобным, однако он обеспечивает большие возможности по управлению организацией памяти в программе.

Общая форма записи для получения и назначения области памяти для одного данных, которым может быть структура, так и основной тип, имеет следующий вид:

имяТипа имя_указателя = new имяТипа

Тип данных используется дважды: один раз — для указания вида запрашиваемой памяти и второй раз — для объявления подходящего указателя. Конечно, если вы уже объявили указатель нужного типа, то можете использовать его вместо объявления нового.

Программа 3_11 иллюстрирует использование оператора new с двумя различными типами данных.

27

27

Page 28: C++ теория

Теория и практика С++

Программа 3_11// программа p3_11.cpp - использование оператора new#include <iostream>using namespace std;

void main(){ int *pt = new int; //выделение области памяти для значений типа int *pt = 1001; //место хранения значения cout << "int "; cout << "value = " << *pt << ": location = " << pt << "\n"; double *pd = new double; //выделение области памяти для double *pd = 10000001.0; //место хранения значения типа double cout << "double "; cout << "value = " << *pd << ": location = " << pd << "\n"; cout << "size of pt = " << sizeof pt; cout << ": size of *pt = " << sizeof *pt << "\n"; cout << "size of pd = " << sizeof pd; cout << ": size of *pd = " << sizeof *pd << "\n";}

Результат выполнения программы:int value = 1001: location = 00480030double value = 1e+07: location = 00481DB0size of pt = 4: size of *pt = 4size of pd = 4: size of *pd = 8

Программа использует оператор new для выделения памяти объектам данных типа int и double. Это происходит при выполнении программы. Указатели pt и pd указывают на эти два объекта данных. Без них невозможно обращаться к выделенным участкам памяти. Благодаря указателям, можно использовать выражения *рt и *pd просто как переменные. Значения выражениям *pt и *pd, присваиваются для того, чтобы присвоить значения новым объектам данных. Аналогично, выражения *pt и *pd используются для отображения этих значений с помощью объекта cout.

Программа демонстрирует одну из причин, по которой необходимо объявлять тип данных, на которые указывает указатель. Адрес сам по себе определяет только начало области памяти, в которой хранится объект, но не его тип или занимаемый размер. Взгляните на адреса двух значений из нашего примера. Они – всего лишь числа без обозначения типа или размера. К тому же заметим, что размерность указателя на тип int совпадает с размерностью указателя на тип double: оба являются просто адресами. Но поскольку в программе объявлены типы указателей, программа знает, что значение *pd относится к типу double с размерностью восемь байтов, в то время как значение *pt относится к типу int размерностью четыре байта. Когда программа выводит значение *pd, конструкция cout определяет, сколько байтов нужно прочесть и как их представить.

Высвобождение памяти с помощью оператора deleteИспользование оператора new для запроса памяти представляет собой наиболее эффективную сторону

механизма управления памятью в С++. Другой стороной является оператор delete, который позволяет высвободить память, после того, как ее использование завершено. Это – важный шаг к наиболее эффективному использованию памяти. Память, которая возвращается, или высвобождается, может быть повторно использована другими модулями программы. В тексте программы за оператором delete должен следовать указатель на блок памяти, ранее зарезервированный с помощью оператора new:

int *ps = new int; //выделение памяти с помощью оператора new//использование памяти

delete ps; //очистка памяти с помощью оператора delete по окончании//выполнения программы

В этом примере очищается область памяти, на которую указывает ps, но сам указатель ps не удаляется. Его можно использовать повторно, например, для указания на другую выделенную область памяти. Операторы new и delete всегда необходимо применять сбалансированно, иначе может произойти утечка памяти; другими словами, наступит момент, когда выделенную память нельзя будет больше использовать. Если утечка памяти чересчур велика, она может привести к останову программы при поиске дополнительной памяти.

Не следует пытаться повторно очистить блок памяти, который уже освобожден. Результат такого действия не определен. Нельзя также использовать оператор delete для освобождения памяти, созданной при объявлении переменных:

28

28

Page 29: C++ теория

Теория и практика С++

int *ps = new int; // допустимоdelete ps; // допустимоdelete ps; // уже недопустимоint jugs = 5; // допустимоint *pi = &jugs; // допустимоdelete pi; // недопустимо, память не выделена оператором new

Важным залогом корректной работы оператора delete является то, что он должен применяться по отношению к области памяти, выделенной с помощью оператора new. Это не значит, что нужно использовать тот же указатель, что и для new; важно, чтобы это был тот же самый адрес:

int *ps = new int; //распределение памятиint *pq = ps; //второму указателю присваивается адрес того же самого блока памятиdelete pq; //применение оператора delete со вторым указателем

Использование оператора new для создания динамических массивовЕсли программа оперирует лишь одним значением, проще объявить простую переменную, чем использовать

оператор new и указатель для управления единственным небольшим объектом данных. Однако более часто встречающимся случаем является применение оператора new для работы с большими порциями данных/ например, с массивами, строками и структурами — здесь от него больше пользы. Предположим, что создается программа, для которой массив может либо потребоваться, либо нет, в зависимости от информации, получаемой в ходе выполнений программы. Если массив создается путем объявления, часть памяти будет отведена под него уже при компиляции программы. Независимо от того, использует программа созданный массив или нет, память он занимать будет в любом случае. Выделение памяти для массива в процессе компиляции называется статическим связыванием. Однако оператор new позволяет либо создавать массив во время выполнения программы, если возникает необходимость, либо не создавать, если такой не возникнет. Можно также выбирать размер массива в процессе выполнения программы. Процесс создания массива в ходе выполнения программы называется динамическим связыванием. Такой массив называется динамическим. При статическом связывании необходимо определить размер массива во время создания программы, а при динамическом связываний программа сама принимает решение о размере создаваемого массива в ходе своего выполнения.

Теперь рассмотрим два главных вопроса, касающихся динамических массивов: как использовать оператор языка C++ new для создания массива и указатель — для доступа к элементам массива.

Создание динамического массива с помощью оператора newСоздать динамический массив в C++ просто. Необходимо указать оператору new тип элементов массива и

их количество. В соответствии с требованиями синтаксиса должно быть указано имя типа, а за ним — количество элементов в квадратных скобках. Например, если нужно задать массив из десяти переменных типа int, необходимо использовать следующее выражение:

int *psome = new int [10]; //блок из 10 значений типа int

Оператор new возвращает адрес первого элемента выделенного блока. В приведенном примере это значение присваивается указателю psome.

Удаление динамического массива:

delete [] psome; //высвобождается память, отведенная для динамического массива

Использование динамического массиваКак выполняется обращение к какому-либо из этих элементов? С первым элементом нет никаких проблем.

Поскольку psome указывает на первый элемент массива, выражение *psome является значением первого элемента. Остается еще девять элементов, к которым надо получить доступ. Для этого нужно просто применять указатель так, как будто он является именем массива. Иными словами, для первого элемента можно использовать обозначение psome[0] вместо *psome, для второго – psome[1] и т.д.

Программа 3_12// использование оператора new для создания массивов#include <iostream>using namespace std;

void main(){ double *p3 = new double [3]; //область памяти для трех элементов типа double //p3 трактуется как имя массива

29

29

Page 30: C++ теория

Теория и практика С++

p3[0] = 0.2; p3[1] = 0.5; p3[2] = 0.8; cout << "p3[1] is " << p3[1] << ".\n"; p3 = p3 + 1; //приращение указателя cout << "p3[0] is : " << p3[0] << " end "; cout << "p3[1] is " << p3[1] << ".\n"; p3 = p3 - 1; //указывает на начало высвобождения памяти delete [] p3;}

Результат:p3[1] is 0.5.p3[0] is : 0.5 end p3[1] is 0.8.

Принципиальная разница между именем массива и указателем проявляется в следующей строке:

p3 = p3 + 1; //правильно для указателей, неправильно для имен массивов

Значение имени массива изменять нельзя, однако указатель является переменной, следовательно, его значение можно изменять. Обратите внимание на результат прибавления единицы к p3. Теперь выражение p3[0] ссылается на тот элемент массива, который раньше был вторым. Таким образом, после прибавления единицы к p3 он указывает на второй элемент вместо первого.

Указатели, массивы и арифметика указателейКорни близкого подобия указателей и имен массивов следует искать в арифметике указателей, а также в

особенностях внутренней обработки массивов в языке C++. Для начала обратимся к арифметике. В результате прибавления единицы к целой переменной ее значение увеличивается на единицу. Однако, прибавив единицу к переменной-указателю, мы увеличим ее значение на число байтов, соответствующее размерности типа, на который она указывает. Если прибавить единицу к указателю на тип double, то числовое значение указателя в системах с 8-байтовыми типами double увеличится на 8. А если прибавить единицу к указателю на тип short, то значение указателя увеличится на 2 при условии, что размерность типа short составляет 2 байта.

Программа 3_13// операции сложения с указателями#include <iostream>using namespace std;

void main(){ double wages[3] = {10000.0, 20000.0, 30000.0}; short stacks[3] = {3, 2, 1}; //Два способа получения адреса массива double *pw = wages; //имя массива = адрес short *ps = &stacks[0]; //второй способ: использование операции //извлечения адреса элемента массива cout << "pw = " << pw << ", *pw = " << *pw << "\n"; pw = pw +1; cout << "add 1 to the pw pointer:\n"; cout << "pw = " << pw << ", *pw = " << *pw << "\n\n"; cout << "ps = " << ps << ", *ps = " << *ps << "\n"; ps = ps +1; cout << "add 1 to the ps pointer:\n"; cout << "ps = " << ps << ", *ps = " << *ps << "\n\n"; cout << "access two elements with arra notation\n"; cout << stacks[0] << " " << stacks[1] << "\n"; cout << "access two elements with pointer notation\n"; cout << *stacks << " " << *(stacks + 1) << "\n"; cout << sizeof wages << " = size of wages array\n"; cout << sizeof pw << " = size of pw pointer\n";}

Результат:pw = 1245028, *pw = 10000

30

30

Page 31: C++ теория

Теория и практика С++

add 1 to the pw pointer:pw = 1245036, *pw = 20000

ps = 1245060, *ps = 3add 1 to the ps pointer:ps = 1245062, *ps = 2

access two elements with arra notation3 2access two elements with pointer notation3 224 = size of wages array4 = size of pw pointer

Указатели и строкиОсобая связь между массивами и указателями распространяется также и на использование строк.

Рассмотрим следующий код:

char flower[10] = "rose";сout << flower <<"s are red\n";

Имя массива — это адрес его первого элемента, поэтому flower в операторе cout — это адрес элемента char, который содержит символ r. Объект cout предполагает, что адрес массива типа char — это адрес строки, поэтому он выводит символ по этому адресу и затем продолжает вывод символов, пока не достигнет нулевого символа (\0). Одним словом, если присвоить оператору cout адрес символа, он выведет все символы, начиная с данного и заканчивая первым нулевым символом, который встретится за ним.

Решающим здесь является не то, что flower — это имя массива, а то, что flower выступает адресом элемента char. Это подразумевает, что можно также использовать переменную-указатель на char в качестве аргумента оператора cout, так как она тоже является адресом элемента char. Безусловно, такой указатель должен указывать на начало строки

Программа 3_14// использование указателей на строки#include <iostream>#include <cstring>using namespace std;

void main(){ char animal[20] = "bear"; //массив animal хранит значение bear const char *bird = "wren"; //указатель bird хранит адрес строки char *ps; //не инициализировано cout << animal << " and "; //отображение строки bear cout << bird << "\n"; //отображение строки wren cout << "Enter a kind of animal: "; cin >> animal; ps = animal; cout << ps << "s.\n"; cout << "Before using strcpy():\n"; cout << animal << " at " << (int *)animal << endl; cout << ps << " at " << (int *)ps << endl; ps = new char[strlen(animal)+1]; //получение новой памяти strcpy(ps, animal); //копирование строки в новую область памяти cout << "After using strcpy():\n"; cout << animal << " at " << (int *)animal << endl; cout << ps << " at " << (int *)ps << endl; delete [] ps;}

Результат:bear and wrenEnter a kind of animal: fox

31

31

Page 32: C++ теория

Теория и практика С++

foxs.Before using strcpy():fox at 1245040fox at 1245040After using strcpy():fox at 1245040fox at 9461788

Использование оператора new для создания динамических структурЕсли требуется выделять область памяти только для такого количества структур, которое необходимо

программе в определенный момент, снова приходит на помощь оператор new. Он позволяет создавать динамические структуры. Опять же, слово "динамические" означает то, что память выделяется во время выполнения программы, а не во время компиляции. Кроме того, рассмотренная выше техника работы со структурами применима к классам, так как классы во многом похожи на структуры.

Использование оператора new при работе со структурами происходит в два этапа: создание структуры и доступ к ее элементам. Чтобы создать структуру, следует указать ее тип с оператором new. Например, создать безымянную структуру типа inflatable и присвоить ее адрес подходящему указателю можно следующим образом:

inflatable *ps = new inflatable;

Здесь указателю ps присваивается адрес части свободной памяти, достаточной для хранения структуры типа inflatable.

Получить доступ к элементам структуры сложнее. При создании динамической структуры нельзя применять оператор принадлежности (в виде точки) к имени структуры, поскольку у структуры нет имени. Все, что у вас есть, — это ее адрес. В языке C++ имеется оператор, специально предназначенный для этого случая, — оператор принадлежности в виде стрелки (->). Этот оператор, который образован дефисом и знаком "больше", играет для указателей на структуры ту же роль, что и оператор в виде точки для имен структур. Например, если ps указывает на структуру типа inflatable, то ps->price — это элемент price указанной структуры.

Второй, менее изящный подход основан на том факте, что если ps – указатель на структуру, то *ps представляет значение, на которое указывает, т.е. саму структуру. Далее, поскольку ps – это структура, то (*ps).price – это элемент price структуры. В соответствии с правилами приоритета операторов в языке С++ в этой конструкции следует использовать круглые скобки.

Программа 3_15// использование оператора new для создания структуры#include <iostream>using namespace std;struct inflatable //шаблон структуры{ char name[20]; float volume; double price;};

void main(){ inflatable *ps = new inflatable; //выделение области памяти для структуры cout << "Enter name of inflatable item: "; cin.get(ps->name,20); //первый элемент доступа к элементу структуры cout << "Enter volume in cubic feet: "; cin >> (*ps).volume; //второй метод доступа к элементу структуры cout << "Enter price: $"; cin >> ps->price; cout << "Name: " << (*ps).name << "\n"; //второй метод cout << "Volume: " << ps->volume << " cubic feet\n"; cout << "Price: $" << ps->price << "\n"; //первый метод}

Результат:Enter name of inflatable item: Bill GaitsEnter volume in cubic feet: 1.4Enter price: $17.99

32

32

Page 33: C++ теория

Теория и практика С++

Name: Bill GaitsVolume: 1.4 cubic feetPrice: $17.99

Пример использования операторов new и deleteРассмотрим пример использования операторов new и delete для управления хранением в памяти данных,

введенных с клавиатуры. В следующей программу определяется функция, которая возвращает указатель на введенную строку. Эта функция считывает вводимые данные во временный массив большого размера и затем использует оператор new [ ] для создания блока памяти, размер которого точно подходит для сохранения введенной строки. После этого функция возвращает указатель на этот блок. Такой подход позволяет эффективно экономить память в программах, где выполняется чтение большого количества строк.

Программа 3_16// использование оператора delete#include <iostream>#include <cstring>using namespace std;char* getname(void); //прототип функции

void main(){ char *name; //создается указатель, но не область хранения name = getname(); //указателю name присваивается адрес строки cout << name << " at " << (int *) name << "\n"; delete [] name; //высвобождение памяти name = getname(); //повторное использование высвобождаемой памяти cout << name << " at " << (int *) name << "\n"; delete [] name; //повторное высвобождение памяти}//---------------------------------------------------------------------------char* getname() //возврат указателя на новую строку{ char temp[80]; //временная область хранения cout << "Enter last name: "; cin >> temp; char *pn = new char[strlen(temp) + 1]; strcpy(pn,temp); //копирование строки в область хранения //меньшего размера после выхода из функции return pn; //после выхода из функции массив temp утрачивается}

Результат:Enter last name: FriderikFriderik at 9461788Enter last name: PookPook at 9461788

ВОПРОСЫ ДЛЯ ПОВТОРЕНИЯ (3)1. Как бы вы объявили следующие объекты данных?

a. actors — массив 30 элементов типа char.b. betsie — массив 100 элементов типа short.c. chuck — массив 13 элементов типа float.d. dipsea — массив 64 элементов типа long double.

2. Объявите массив из пяти элементов типа int и заполните его первыми пятью целыми нечетными числами.

3. Сконструируйте оператор, который присваивает сумму первого и последнего элемента массива из вопроса 2 переменной even.

4. Сконструируйте оператор, который выводит значение второго элемента массива ideas типа float.

5. Объявите массив типа char и присвойте ему значение строки "cheeseburger".

33

33

Page 34: C++ теория

Теория и практика С++

6. Придумайте объявление структуры, описывающей рыбу. Структура должна включать вид, вес в целых граммах и длину в долях метра.

7. Объявите переменную типа, описанного в вопросе 6, и присвойте ей начальное значение.

8. Предположим, что переменная ted имеет тип double. Объявите указатель на ted и используйте его для вывода значения этой переменной.

9. Предположим, что treacle является массивом 10 переменных типа float. Объявите указатель, который указывает на первый элемент массива treacle, и используйте указатель для вывода значений первого и последнего элемента массива.

10. Создайте фрагмент кода, который запрашивает ввод с клавиатуру положительных целых чисел, а затем создает динамический массив из полученного количества значений типа int.

11. Напишите фрагмент кода, который динамически определяет структуру типа, описанного в вопросе 6, а затем считывает значение для элемента этой структуры, описывающего вид.

Упражнения по программированию (3)1. Напишите программу на C++, которая запрашивает и выводит информацию как показано ниже. Обратите

внимание на то, что необходимо обеспечить возможность принимать имена, состоящие более чем из одного слова. Заметьте также, что программа понижает отметку на один балл, т. е. выводит следующую по алфавиту букву относительно введенной пользователем (в американских школах принято буквенное обозначение оценок, А — наивысший балл.).

What is your first name? Betty SueWhat is your last паше? YewWhat letter grade do you deserve? ВWhat is your age? 22Name: Yew, Betty SueGrade: СAge: 22

2. Структура CandyBar включает три элемента. Первый элемент содержит имя производителя конфеты-батончика. Второй — вес (определенный с помощью вещественного числа) конфеты-батончика, а третий — количество калорий (целочисленное значение). Напишите программу, которая объявляет такую структуру и создает переменную типа CandyBar с именем snack, присваивая элементам этой структуры значения "Mocha Munch", 2.3 и 350 соответственно. Присваивание (инициализация) должно быть частью объявления snack. В результате программа должна вывести содержимое переменной snack.

3. Структура CandyBar содержит три элемента, как описано в упражнении по программированию 2. Напишите программу, которая создает массив из трех структур CandyBar, присваивает им значения на ваш выбор и затем выводит содержимое каждой структуры.

4. Уильям Вингейт предлагает услуги по учету пиццы. Для каждой пиццы он записывает такую информацию:

Имя компании-производителя пиццы, которое может содержать более одного слова Диаметр пиццы Вес пиццы

Придумайте структуру, которая может хранить такую информацию, и напишите программу, в которой используется переменная специального типа для такой структуры. Программа должна предлагать пользователю ввести данные для каждого из названных пунктов и затем выводить эту информацию. Используйте объекты cin (или его методы) и cout.

5. Напишите программу к упражнению 2, но используйте оператор new для выделения структуре области памяти вместо объявления структурной переменной. Программа должна также запрашивать диаметр пиццы до запроса названия компании — производителя пиццы.

6. Выполните упражнение 3, но вместо объявления массива трех структур типа CandyBar используйте оператор new для динамического выделения памяти массиву.

4. Циклы и выражения сравненияЦиклы предназначены для выполнения повторяющихся действий.

34

34

Page 35: C++ теория

Вход

инициализирующеевыражение

выражениеусловия

Тело цикла

выражениеобновления

Выход

истина

ложь

Теория и практика С++

Цикл for (составные части)for (инициализация; условие продолжения цикла; обновление переменной цикла)

тело цикла

Программа 4_1// представление цикла for#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

int i; // создадим счетчик// инициализация; проверка; обновлениеfor (i = 0; i < 5; i++)cout << "C++ знает циклы.\n";cout << "C++ знает, где остановиться. \n";

}

Результат:C++ knows loops.C++ knows loops.C++ knows loops.C++ knows loops.C++ knows loops.C++ knows when to stop.

Рассматриваемый цикл начинается с присвоения значения 0 целочисленной переменной i:

i = 0

Эта часть цикла называется инициализацией цикла. Затем в той части, которая называется проверкой цикла, программа проверяет, является ли значение переменной i меньше, чем 5:

i < 5

Если это так, то программа выполняет следующий оператор, который образует тело цикла:

35

35

Page 36: C++ теория

Теория и практика С++

cout << "C++ knows loops\n";

Затем программа использует ту часть цикла, где происходит обновление переменной цикла (в нашем случае i увеличивается на единицу):

i++

При этом применяется оператор ++, который называется оператором инкремента (приращения). Он увеличивает на единицу значение своего операнда.

В качестве условия продолжения цикла может применяться не только выражение сравнения, образующее значения тип "истина-ложь", а и любое выражение, результат вычисления которого соответствует типу bool.

Программа 4_2// использование числовой проверки в цикле#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите начальное значение для обратного отсчета: ";int limit;cin >> limit;int i;for (i = limit; i; i--) // quits when i is 0

cout << "i = " << i << "\n";cout << "Готово, теперь i = " << i << "\n";

}

Результат:Enter the starting countdown value: 4i = 4i = 3i = 2i = 1Done now that i = 0

Изменение шага циклаВ приведенных ранее примерах переменная цикла увеличивалась или уменьшалась на 1 при каждой

итерации цикла. Это приращение переменной цикла (или шаг цикла) можно изменить в выражении обновления переменной цикла.

Программа 4_3// цикл указанным пользователем шагом#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите целое число: ";int by;cin >> by;cout << "Шагаем по " << by << ":\n";for (int i = 0; i < 100; i = i + by)

cout << i << endl;}

Доступ к символам строки с помощью цикла for

36

36

Page 37: C++ теория

Теория и практика С++

Цикл for предоставляет непосредственный доступ поочередно к каждому символу строки. Например, нижеследующая программа позволяет вводить строку, а затем отображать ее посимвольно в обратном порядке. При этом функция strlen() подсчитывает количество символов в строке, а затем это значение используется в выражении инициализации цикла для присвоения i значения позиции последнего символа в строке, не считая пустого символа. Для перемещения по строке в обратном порядке в данной программе используется оператор (--), который уменьшается на 1 индекс массива на каждой итерации цикла.

Программа 4_4// применение for к строке#include "stdafx.h"#include <iostream>#include <string>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите слово: ";string word;cin >> word;// отобразить символы в обратном порядкеfor (int i = word.size() - 1; i >= 0; i--)

cout << word[i];cout << "\nГотово.\n";

}

Результат:Enter a word: animallamina

Операторы инкремента (++) и декремента (--)Эти переменные предназначены для увеличения или уменьшения переменной на 1. Существует две

разновидности каждого из операторов: в префиксном варианте изменение значения выполняется до обработки операнда, как, например, в выражении ++х; а в постфиксном – после, как, например, в выражении х++.

Программа 4_5// программа p4_5.cpp - оператор инкремента#include <iostream>using namespace std;

void main(){ int a = 20; int b = 20; cout << "a = " << a << ": b = " << b << "\n"; cout << "a++ = " << a++ << ": ++b = " << ++b << "\n"; cout << "a = " << a << ": b = " << b << "\n";}

Результат:a = 20: b = 20a++ = 20: ++b = 21a = 21: b = 21

Комбинированные операторы присваиванияВыражение i = i + by можно заменить i += by.

Операция Действие (L – левый операнд, R – правый операнд)+= L + R присваивается L-= L - R присваивается L

37

37

Page 38: C++ теория

Теория и практика С++

*= L * R присваивается L/= L / R присваивается L%= L % R присваивается L

Составные операторы или блокиФормат оператор for позволяет включить в тело цикла только один оператор. Но в С++ существует

специальный синтаксический прием, позволяющий обойти это ограничение и вставить в тело цикла сколько угодно операторов. Он заключается в использовании фигурных скобок для построения составного оператора или блока.

Программа 4_6// использование блока операторов#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Изумительный Счетчик сложит и вычислит для вас среднее ";cout << "из пяти значений. \n";cout << "Пожалуйста, введите пять значений: \n";double number;double sum = 0.0;for (int i = 1; i <= 5; i++){ // здесь - начало блока

cout << "Значение " << i << ": ";cin >> number;sum += number;

} // конец блока здесьcout << "В самом деле, пять замечательных значений! ";cout << "Их сумма равна " << sum << endl;cout << "а среднее - " << sum / 5 << ".\n";cout << "Изумительный Счетчик прощается с вами! \n";

}

Результат:The Amazing Accounto will sum and average five numbers for you.Please enter five values:Value 1: 1942Value 2: 1948Value 3: 1957Value 4: 1974Value 5: 1980Five exquisite choices indeed! They sum to 9801and average to 1960.2.The Amazing Accounto bids you adieu!

Составные операторы обладают интересным свойством. Если внутри блока определить новую переменную, то эта переменная будет продолжать существовать до тех пор, пока программа выполняется внутри блока.

Оператор "запятая" (или дополнительные синтаксические приемы)При помощи запятой можно расположить несколько операторов в одной строке.

j++, i--;

Программа 4_7// обращение порядка элементов массива#include "stdafx.h"#include <iostream>#include <string>using namespace std;

38

38

Page 39: C++ теория

Теория и практика С++

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите слово: ";string word;cin >> word;// физическая модификация объекта stringchar temp;int i, j;for (j = 0, i = word.size() - 1; j < i; --i, ++j){ // начало блока

temp = word[i];word[i] = word[j];word[j] = temp;

} // конец блокаcout << word << "\nГотово.\n";

}

Результат:Enter a word: partsstrapDone

Выражения сравненияОснованием для принятия решений в условиях являются выражения сравнения.Выражения сравнения - это такие выражения, которые возвращают одно из двух значений True (Истина)

или False (Ложь). В выражениях сравнения используются операторы сравнения

Оператор Назначение< Меньше<= Меньше или равно> Больше>= Больше или равно== Равно!= Не равно

Программа 4_8// сравнение строк с использованием массивов#include "stdafx.h"#include <iostream>#include <cstring> // прототип для strcmp()using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

using namespace std;char word[5] = "?ate";for (char ch = 'a'; strcmp(word, "mate"); ch++){

cout << word << endl;word[0] = ch;

}cout << "По окончании цикла word содержит " << word << endl;

}

Результат:?ateaatebatecate

39

39

Page 40: C++ теория

Теория и практика С++

dateeatefategatehateiatejatekatelateAfter loop ends, word is mate

Программа 4.8.1// compstr2.cpp -- сравнение строк с использованием класса string#include "stdafx.h"#include <iostream>using namespace std;#include <string> // класс string

int main(){

setlocale(LC_CTYPE, "RUS");

string word = "?ate";for (char ch = 'a'; word != "mate"; ch++){

cout << word << endl;word[0] = ch;

}cout << "По окончании цикла word содержит " << word << endl;return 0;

}

Вернемся к циклу forКод в листинге 4.8.2 использует цикл для вычисления и сохранения первых 16 факториалов. Факториалы, которые являются удобным примером автоматизации обработки, вычисляются следующим образом. Ноль факториал, записываемый как 0!, определен как равный 1. Затем, 1! равен 1*0!, то есть 1. Далее, 2! равно 2*1!, или 2. Затем 3! равно 3*2!, или 6, и так далее. То есть факториал каждого целого числа равен произведению этого чис-ла на факториал предыдущего числа. Программа использует один цикл для вычисления значений последовательных факториалов, помещая их в массив. Затем она использует второй цикл для отображения результатов. Также программа представляет применение внешнего объявления для значений.

Программа 4.8.2// еще о циклах for#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 16; // пример внешнего объявления

void main(){

setlocale(LC_CTYPE, "RUS");

double factorials[ArSize];factorials[1] = factorials[0] = 1.0;for (int i = 2; i < ArSize; i++)

factorials[i] = i * factorials[i-1];for (int i = 0; i < ArSize; i++)

cout << i << "! = " << factorials[i] << endl;}Результат:

40

40

Page 41: C++ теория

условие продолжения цикла

тело цикла

ложь

истина

цикл While(с предусловием)

вход

выход

Теория и практика С++

0! = 11! = 12! = 23! = 64! = 245! = 1206! = 7207! = 50408! = 403209! = 36288010! = 3.6288e+00611! = 3.99168e+00712! = 4.79002e+00813! = 6.22702e+00914! = 8.71783e+01015! = 1.30767e+012

Цикл whileЦикл while представляет собой цикл for, область управления которого не содержит частей инициализации

и обновления переменной цикла. У него имеется только условие продолжения цикла и тело:

while (условие продолжения цикла)тело цикла

Сначала программа проверяет условие продолжения цикла. Если в результате вычисления этого выражения получается логическое значение true, выполняются операторы в теле цикла.

Программа 4_9В цикле осуществляется последовательное обращение к каждому символу строки и отображение его ASCII-кода. Цикл завершается по достижении нулевого символа.// демонстрация цикла while#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 20;

int main(){

setlocale(LC_CTYPE, "RUS");char name[ArSize];cout << "Ваше имя, пожалуйста: ";cin >> name;

41

41

Page 42: C++ теория

Теория и практика С++

cout << "Вот ваше имя, посимвольно и в кодах ASCII:\n";int i = 0; // начать с начала строкиwhile (name[i] != '\0') // обрабатывать до конца строки{

cout << name[i] << ": " << int(name[i]) << endl;i++; // не забудьте этот шаг

}return 0;

}

Результат:Your first name, plase: MuffyHere is your name, verticalized and ASCIIized:M: 77u: 117f: 102f: 102y: 121

Построение цикла задержки

Программа 4.9.1// waiting.cpp -- использование clock() в цикле временной задержки#include "stdafx.h"#include <iostream>using namespace std;#include <ctime> // описывает функцию clock() и тип clock_t

int main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите время задержки в секундах: ";float secs;cin >> secs;clock_t delay = secs * CLOCKS_PER_SEC; // преобразовать в системные

// единицы времени (тики)cout << "starting\a\n";clock_t start = clock();while (clock() - start < delay ) // ждать истечения времени

; // обратите внимание на эту точку с запятойcout << "готово \a\n";return 0;

}

Цикл do whileЦикл do while обладает постусловием. Это означает, что сначала выполняется тело цикла и только после

этого вычисляется выражение условия продолжения цикла.

doтело цикла

while (условие продолжения цикла);

42

42

Page 43: C++ теория

условие продолжения цикла

тело цикла

ложь

истина

цикл Do While(с постусловием)

вход

выход

Теория и практика С++

Программа 4_10В данной программе, когда пользователь выполняет запрос на ввод данных, программа должна принять данные до их проверки.// программа p4_10.cpp - демонстрация цикла do while#include "stdafx.h"#include <iostream>using namespace std;

int main(){

setlocale(LC_CTYPE, "RUS");

int n;cout << "Угадайте мое любимое число в диапазоне 1-10 \n";do{

cin >> n; // выполнить тело} while (n != 7); // затем проверитьcout << "Да, 7 - мое любимое. \n" ;return 0;

}

Результат:Enter numbers in the range 1-10 to find my favorite number947Yes, 7 is my favorite.

43

43

Page 44: C++ теория

Теория и практика С++

Циклы и ввод текстаЕсли в программе предполагается использовать цикл для ввода текста с клавиатуры, то необходимо знать,

когда ввод должен быть прекращен. Как это сделать? Можно, в частности, воспользоваться специальным символом сигнальной метки в качестве признака окончания ввода.

Применение для ввода простого cinЕсли программа собирается использовать цикл для чтения текстового ввода с клавиатуры, она должна

каким-то образом узнать, когда ей следует остановиться. Как она узнает об этом? Один способ заключается в использовании некоторого специального символа, иногда называемого сигнальным символом (sentinel character) в качестве сигнала останова. Например, листинг 5.3.1 прекращает чтение ввода, когда программа встречает символ #.

Программа 5.3.1// чтение символов в цикле while#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

char ch;int count = 0; // использовать базовый вводcout << "Вводите символы; для выхода введите #:\n";cin >> ch; // получить символwhile (ch != '#') // проверить символ{

cout << ch; // отобразить символ++count; // посчитать символcin >> ch; // получить следующий символ

}cout << endl << count << " символов прочитано \n";

}

Не выводит пробелов. cin пропускает пробелы и символы перевода строки. Эти символы не выводятся, не могут быть подсчитаны и не отображаются.

Спасение в виде cin.get (char)Класс istream (определенный в iostream), к которому относится объект cin, включает функцию-член,

которая отвечает этому требованию. В частности, функция cin.get(ch) читает из ввода следующий символ, даже если это пробел, и присваивает его переменной ch. Заменив cin >> ch вызовом этой функции, вы можете устранить недостатки программы из листинга 5.3.1. В листинге 5.3.1 показан результат.

Программа 5.3.2// использование cin.get(char)#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

char ch;int count = 0;cout << "Вводите символы. Для выхода введите #:\n";cin.get(ch); // использовать функцию cin.get(ch)while (ch != '#'){

cout << ch;++count;cin.get(ch); // использовать ее снова

44

44

Page 45: C++ теория

Теория и практика С++

}cout << endl << count << " символов прочитано \n";

}

Условие конца файлаКак показано в листинге 5.3.2, применение символа вроде # для обозначения конца ввода, не всегда

подходит, потому что такой ввод может быть частью совершенно легитимного ввода. То же верно и любого другого символа, такого как @ или %. Если ввод поступает из файла, вы можете задействовать более мощную технику — обнаружение конца файла (end-of-file — EOF). Средства ввода C++ взаимодействуют с операционной системой для обнаружения момента достижения конца файла и предоставляют эту информацию программе.

Если среда программирования предусматривает проверку EOF, вы можете использовать программу, подобную приведенной в листинге 5.3.2, с перенаправленными файлами либо с клавиатурным вводом, в котором эмулируется EOF. Это выглядит удобным, поэтому давайте посмотрим, как это делается.

Когда cin обнаруживает EOF, он устанавливает двум битам (eofbit и failbit) значение 1. Вы можете использовать функцию-член по имени eof() для проверки состояния eofbit; вызов cin.eof() возвращает булевское значение true, если EOF был обнаружен, и false — в противоположном случае. Аналогично, функция-член fail() возвращает true, если eofbit или failbit установлены в 1. Обратите внимание, что методы eof() и fail() сообщают результат самой последней попытки чтения; то есть они сообщают о прошлом, а не заглядывают в будущее. Поэтому проверки cin.eof() и cin.fail() должны всегда следовать за попытками чтения.

Программа 5.3.3// чтение символов до конца файла#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

char ch;int count = 0;cin.get(ch); // попытка прочитать символwhile (cin.fail() == false) // проверка на EOF{

cout << ch; // отобразить символ++count;cin.get(ch); // попытка читать следующий символ

}cout << endl << count << " символов прочитано \n";

}

Поскольку эта программа запускалась под управлением Windows ХР, для эмуляции условия EOF нажималась комбинация <Ctrl+Z> и затем <Enter>.

Программа 4_11Чтение потока ввода прекращается, когда программа встречает символ #. Теряются пробелы, так как cin пропускает символы пробела и новой строки.// чтение символов с помощью цикла while#include "stdafx.h"#include <iostream>using namespace std;

int main(){

setlocale(LC_CTYPE, "RUS");

char ch;int count = 0; // использовать базовый ввод

45

45

Page 46: C++ теория

Теория и практика С++

cout << "Вводите символы; для выхода введите #:\n";cin >> ch; // получить символwhile (ch != '#') // проверить символ{

cout << ch; // отобразить символ++count; // посчитать символcin >> ch; // получить следующий символ

}cout << endl << count << " символов прочитано \n";return 0;

}

Результат:see ken run#doseekenrun9 characters read

Функция cin.get(char)Функция cin.get(char) читает из входного потока любые символы, в то время как cin пропускает

символы пробела и новой строки.

Программа 4_12Функция cin.get(ch) читает из входного потока следующий символ, даже если им является пробел, а затем присваивает его переменной ch.// исползование функции cin.get(char)#include <iostream>using namespace std;

void main(){ char ch; int count = 0; cin.get(ch); //применить функцию cin.get(char) while (ch != '#') { cout << ch; count++; cin.get(ch); //применить еще раз } cout << "\n" << count << " characters read\n";}

Результат:Did you use a #22Did you use a14 characters read

Вложенные циклы и двумерные массивыДвумерный массив похож на таблицу, имеющей строки и столбцы данных.

Инициализация двумерного массиваint maxtemps[4][5] = {

{94, 98, 87, 103, 101},{98, 99, 91, 107, 105},{93, 91, 90, 101, 104},{95, 100, 88, 105, 103}

};Доступ к элементам массива с помощью индексовmaxtemps[0][0] maxtemps[0][1] maxtemps[0][2] maxtemps[0][3] maxtemps[0][4]maxtemps[1][0] maxtemps[1][1] maxtemps[1][2] maxtemps[1][3] maxtemps[1][4]maxtemps[2][0] maxtemps[2][1] maxtemps[2][2] maxtemps[2][3] maxtemps[2][4]

46

46

Page 47: C++ теория

Теория и практика С++

maxtemps[3][0] maxtemps[3][1] maxtemps[3][2] maxtemps[3][3] maxtemps[3][4]

Программа 4_13В программе применяется двумерный массив и вложенный цикл. // программа p4_13.cpp - двумерные массивы и вложенные циклы#include <iostream>using namespace std;const int CITIES = 5;const int YEARS = 4;

void main(){ const char* cities[CITIES] = //массив указателей { //на пять строк "Gribble City", "Gribbletown", "New Gribble", "San Gribble", "Gribble Vista" }; int maxtemps[YEARS][CITIES] = //двумерный массив { {95, 99, 86, 100, 104}, {95, 97, 90, 106, 102}, {96, 100, 940, 107, 105}, {97, 102, 89, 108, 104} }; cout << "Maximum temperatures for 1999 - 2002\n\n"; for (int city = 0; city < CITIES; city++) { cout << cities[city] << ":\t"; for (int year = 0; year < YEARS; year++) cout << maxtemps[year][city] << "\t"; cout << "\n"; }}

Результат:Maximum temperatures for 1999 - 2002

Gribble City: 95 95 96 97Gribbletown: 99 97 100 102New Gribble: 86 90 940 89San Gribble: 100 106 107 108Gribble Vista: 104 102 105 104

ВОПРОСЫ ДЛЯ ПОВТОРЕНИЯ (4)1. В чем заключается различие между циклом с предусловием и циклом с постусловием?

2. Что будет выведено в результате выполнения следующего фрагмента кода, если включить его в реальную программу:

int i;for (i = 0; i < 5; i++)

cout << i;cout << "\n";

3. Что будет выведено в результате выполнения следующего фрагмента кода, если включить его в реальную программу:

int j;for (j = 0; j < 11; j += 3)

cout << j;cout << "\n" << j << "\n";

47

47

Page 48: C++ теория

Теория и практика С++

4. Что будет выведено в результате выполнения следующего фрагмента кода, если включить его в реальную программу:

int j = 5;while (++j < 9)

cout << j++ << "\n";

5. Что будет выведено в результате выполнения следующего фрагмента кода, если включить его в реальную программу:int k = 8;do

cout << " k = " << k << "\n";while (k++ < 5);

6. Создайте цикл for, который выводит значения 1, 2, 4, 8, 16, 32, 64 при увеличении в 2 раза значения счетчика на каждом шаге цикла.

7. Каким образом можно включить в тело цикла несколько операторов?

Получение случайного число в диапазоне от 1 до N.srand ((unsigned) time(NULL));;r = rand() % N + 1;

Упражнения по программированию (4)1. Напишите программу, которая запрашивает у пользователя ввод двух целых чисел. Затем эта программа

должна вычислить и отобразить сумму всех целых чисел, находящихся в пределах между двумя введенными целыми числами и включая их. При этом предполагается, что первым вводится меньшее целое число. Например, если пользователь вводит 2 и 9, то программа сообщает, что сумма всех целых чисел от 2 до 9 составляет 44.

2. Напишите программу, которая запрашивает ввод чисел. После ввода каждого числа сообщается общая сумма введенных до сих пор чисел. Программа завершается после ввода нуля.

3. Дафна сделала вклад на сумму $100 под простые проценты, которые составляют 10%. Итак, ежегодно ее вклад дает доход в сумме 10% от первоначального вклада, или $10:

проценты = 0.10 * начальный остаток

В то же время Клео сделала вклад на сумму $100 под сложные проценты, которые составляют 5%. Другими словами, 5% от текущего остатка, с учетом начисленных ранее процентов:

проценты = 0.05 * текущий остаток

Доход Клео за первый год составит 5% от суммы вклада $100, что даст остаток $105. В следующем году ее доход в виде 5% от суммы $105 составит $5.25 и т.д. Напишите программу, которая определяет, сколько лет потребуется для того, чтобы сумма вклада Клео превысила сумму вклада Дафны, а затем отображает сумму обоих вкладов в этот момент.

4. Допустим, что читатель занимается продажей книги "C++ для дураков". Напишите программу, которая требует ввода объема ежемесячных продаж этой книги в течение года (в экземплярах книг, а не в денежном выражении). В этой программе должен быть использован цикл, предлагающий ввести данные продаж за каждый месяц. Для этого используется массив указателей типа char *, инициализированных с присвоением строковых названий месяцев года. Введенные данные сохраняются в массиве значений типа int. После этого программа должна найти сумму содержимого массива и выдать отчет об общем объеме продаж за год.

5. Выполните упражнение 4, однако на сей раз воспользуйтесь двумерным массивом для хранения введенных данных о продажах в течение трех лет. Выдайте отчет об общем объеме продаж за каждый год в отдельности и в целом за все годы.

6. Разработайте структуру с именем саr, в которой хранится следующая информация об автомобиле: его марка в виде строки (массива символов), а также год его выпуска в виде целого числа. Напишите программу, которая запрашивает пользователя о том, сколько автомашин следует ввести в каталог. Затем программа должна использовать оператор new для создания нового динамического массива в соответствии с указанным количеством структур саr. Далее она должна выдать запрос ввода марки (которая может состоять из нескольких слов) и года выпуска автомашины для каждой структуры. Следует заметить, что этот процесс требует некоторого внимания, поскольку при этом поочередно осуществляется чтение

48

48

Page 49: C++ теория

Теория и практика С++

строковых и числовых данных. И, наконец, программа должна отобразить содержимое каждой структуры. Результат выполнения программы должен выглядеть примерно следующим образом:

How many cars do you wish to catalog? 2.Car #1:Please enter the make: Hudson HornetPlease enter the year made: 1952Car #2:Please enter the make: KaiserPlease enter the year made: 1951Here is your collection:1952 Hudson Hornet1951 Kaiser

7. Напишите программу, использующую массив char и цикл для чтения по одному слову за раз до тех пор, пока не будет введено слово done. Затем программа должна сообщить количество введенных слов (исключая done). Пример запуска должен быть таким:

Вводите слова (для завершения введите слово done) :anteater birthday category dumpsterenvy finagle geometry готово for sureВы ввели 7 слов.

Вы должны включить заголовочный файл cstring и использовать функцию strcmp() для выполнения проверки.

8. Напишите программу, соответствующую описанию программы из упражнения 7, но применив объект класса string вместо символьного массива. Включите заголовочный файл string и используйте операции отношений для выполнения проверки.

9. Напишите программу, использующую вложенные циклы, которая запрашивает ввод количества отображаемых строк. Затем она отображает указанное количество строк, заполненных звездочками. Причем количество звездочек в строке соответствует ее порядковому номеру. Перед звездочками вставляются точки так, чтобы в каждой строке общее количество символов было равно указанному количеству строк.

How many line? 5....*...**..***.*********

5. Операторы ветвления и логические операцииОператор if

Оператор if указывает программе выполнить оператор или группу операторов, если проверочное условие истинно, и пропустить этот оператор или группу операторов, если условие ложно.

if (проверяемое условие)оператор;

49

49

Page 50: C++ теория

проверяемое условие

оператор

ложь

истина

оператор if

вход

выход

Теория и практика С++

Программа 5_1Программа подсчитывает количество пробелов и общее количество символов вводимых данных. Для этого можно использовать функцию cin.get(char) в цикле while для считывания символов, а затем – оператор if для выявления и подсчета символов пробела.// программа p5_1.cpp - использование оператора if#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");char ch;int spaces = 0;int total = 0;cin.get(ch);while (ch != '.') //выход по достижении конца предложения{

if (ch == ' ') //проверка, является ли ch пробеломspaces++;;

total++; //выполняется каждый разcin.get(ch);

}cout << spaces << " пробелов, " << total;cout << " - общее количество символов в предложении\n";

}

Результат:This is a sample.3 spaces, 16 character total in sentence

Оператор if elseОператор if else позволяет решить, какой из двух операторов или блоков должен выполниться.

if (проверяемое условие)оператор 1;

elseоператор 2;

50

50

Page 51: C++ теория

вход

оператор 1 оператор 2

выход

проверочноеусловие

оператор if else

истина ложь

Теория и практика С++

Если проверяемое условие является истинным или ненулевым, программа выполняет оператор 1 и пропускает оператор 2. В противном случае, когда значение проверяемого условия является ложным или нулевым, программа пропускает оператор 1 и выполняет оператор 2.

51

51

Page 52: C++ теория

Теория и практика С++

Программа 5_2Программа изменяет вводимый текст следующим образом: вместо каждой буквы в строке ввода в строку вывода подставляется другая, код которой на 1 больше, в то время как символы новой строки передаются в неизменном виде.// использование оператора if else#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");char ch;cout << "Печатайте, а я буду повторять.\n";cin.get(ch);while (ch != '.'){

if (ch == '\n')cout << ch; //выполняется, если ch - символ новой строки

elsecout << ++ch; //выполняется в противном случае

cin.get(ch);}// попробуйтеcout << "\nИзвините за легкое беспокойство.\n";

}

Результат:Печатайте, а я буду повторять.This is a sample.Uijt!jt!b!tbnqmfИзвините за легкое беспокойство.

Конструкция if else if elseКонструкция if else if else позволяет реализовать выбор более чем из двух возможных вариантов.

Программа 5_3Следующая шуточная программа предлагает вам угадать число.// использование конструкции if else if else#include "stdafx.h"#include <iostream>using namespace std;

const int Fave = 27;

void main(){

setlocale(LC_CTYPE, "RUS");int n;cout << "Попробуйте угадать мое любимое число из диапазона 1-100: ";do{

cin >> n;if (n < Fave)

cout << "Слишком маленькое - попробуйте еще: ";else if (n > Fave)

cout << "Слишком большое - попробуйте еще: ";else

cout << Fave << " - правильно!\n";} while (n != Fave);

}

Результат:Enter a number in the range 1-100 to find my favorite number: 50

52

52

Page 53: C++ теория

Теория и практика С++

Too high - guess again: 25Too low - guess again: 37Too high - guess again: 2727 is right!

ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯНад условными выражениями можно выполнять действия логической математики (логические операции), а именно:

OR (ИЛИ) — возвращает значение True, если хотя бы одно из участвующих в операции выражений имеет значение True. В случае, когда все выражения имеют значение False, возвращается значение False. Записывается как ||.

Программа 5_4В программе операция || в операторе if выполняется для проверки как строчных, так и прописных версий символа.// использование логического ИЛИ#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");cout << "Эта программа может переформатировать ваш жесткий диск\n"

"и уничтожит все данные.\n""Хотите продолжить? <y/n> ";

char ch;cin >> ch;if (ch == 'y' || ch == 'Y') //y или Y

cout << "Вас предупредили!\a\a\n";else if (ch == 'n' || ch == 'N') //n или N

cout << "Мудрый выбыбор ... прощайте\n";else

cout << "Это не было ни y, ни n, поэтому предполагаю, ""что нужно очистить диск в любом случае.\a\a\a\n";

}

Результат:This program may reformat your hard diskand destroy all your data.Do you wish to continue? <y/n> NA wise choice ... bye

AND (И) — возвращает значение True (Истина), если все участвующие в операции выражения имеют значение True. В остальных случаях возвращается значение False (Ложь). Записывается как &&.

Программа 5_5В программе массив qualify используется для хранения адресов четырех строк.// использование логического И#include "stdafx.h"#include <iostream>using namespace std;

const char *qualify[4] = //массив указателей на строки{ "гонки на 10,000 метров.\n", "перетягивание каната.\n", "состязание мастеров на каноэ.\n",

53

53

Page 54: C++ теория

Теория и практика С++

"фестиваля по бросанию пирожков.\n"};

void main(){

setlocale(LC_CTYPE, "RUS");int age;cout << "Введите свой возраст в годах: ";cin >> age;int index;if (age > 17 && age < 35)

index = 0;else if (age >= 35 && age < 50)

index = 1;else if (age >=50 && age < 65)

index = 2;else

index = 3;cout << "Вы квалифицированы для " << qualify[index];

}

Результат:Enter your age in years: 87You qualify for the pie-throwing festival.

NOT (HE) — операция отрицания. Возвращает обратное для значения выражения значение, то есть если выражение равно True, то возвращается False и наоборот, если значение выражения равно False, то возвращается значение True. Записывается как !.

Операция ! отрицает, или обращает, истинность выражения, следующего за ней.Обычно выражение отношения можно представить яснее без применения операции !:

if (!(х > 5)) // if (x <= 5) яснее

Однако операция ! может быть полезна с функциями, которые возвращают значения true/false, либо значения, которые могут интерпретироваться подобным образом. Например, strcmp(s1, s2) возвращает не ноль (true), если две строки в стиле С, s1 и s2, отличаются друг от друга, и ноль, если они одинаковы. Это значит, что !strcmp(s1, s2) равно true, если две строки эквивалентны.

Код в листинге 5.5.1 использует технику применения операции ! к значению, возвращаемому функцией для проверки экранного ввода на предмет возможности присваивания типу int. Пользовательская функция is_int(), возвращает true, если ее аргумент находится в диапазоне допустимых значений для присваивания типу int. Затем программа применяет проверку условия while(!is_int (num)), чтобы отклонить значения, которые не входят в диапазон.

Программа 5.5.1// использование логической операции НЕ#include "stdafx.h"#include <iostream>using namespace std;

bool is_int(double);

void main(){

setlocale(LC_CTYPE, "RUS");

double num;cout << "Введите целое: ";cin >> num;while(!is_int(num)) // продолжать, пока num не выражает int{

cout << "Out of range -- please try again: ";cin >> num;

}

54

54

Page 55: C++ теория

Теория и практика С++

int val = int(num); // приведение типаcout << "Вы ввели целое " << val << "\nПока\n";

}

bool is_int(double x){

if(x <= INT_MAX && x >= INT_MIN) // проверка предельных значений climitsreturn true;

elsereturn false;

}

Библиотека символьных функций cctypeЭти функции упрощают выполнение таких задач, как определение вида символа (строчная или прописная

буква, цифра или знак препинания и т.д.). Например, функция isaplha(ch) возвращает ненулевое значение, если ch - буква, и ноль - в противоположном случае. Аналогично, ispunct(ch) возвращает значение true, только если ch - знак препинания, такой как запятая или точка. (Эти функции возвращают значение типа int, а не bool, но int неявно конвертируется в bool.)

Использовать эти функции намного удобнее, чем операции И и ИЛИ. Например, вот как пришлось бы использовать И и ИЛИ, чтобы убедиться, что ch — буквенный символ:

if((ch >= 'a' && ch <= 'z') || If(ch >= 'A' && ch <= 'Z'))

Сравните с применением isalpha():if (isalpha(ch))

Имя функции Возвращаемое значениеisalnum() true, если аргумент является алфавитно-цифровым символом, т.е. буквой или цифройisalpha() true, если аргумент является символом алфавитаisblank() true, если аргумент – пробел или горизонтальная табуляцияiscntrl() true, если аргумент является управляющим символомisdigit() true, если аргумент является десятичной цифрой (0 – 9)isgraph() true, если аргумент является любым печатаемым символом, отличном от пробелаislower() true, если аргумент является буквой нижнего регистраisprint() true, если аргумент является любым печатаемым символом, включая пробелispunct() true, если аргумент является символом пунктуацииisspace() true, если аргумент является стандартным пробельным символом: пробелом, символом

подачи листа, символом новой строки, возврата каретки, горизонтальной или вертикальной табуляции

isxdigit() true, если аргумент является шестнадцатеричной цифрой (0 – 9, a – f или A – F)tolower() Если аргумент является символом верхнего регистра, функция возвращает его аналог в

нижнем регистре; в противном случае возвращается аргумент без измененийtoupper() Если аргумент является символом нижнего регистра, функция возвращает его аналог в

верхнем регистре; в противном случае возвращается аргумент без изменений

Программа 5_6Программа для подсчета общего количества введенных букв, пробелов, цифр, знаков пунктуаций и других символов.// использование библиотеки ctype.h#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите текст для анализа; для окончания ввода введиет @.\n";char ch;int whitespace = 0;int digits = 0;

55

55

Page 56: C++ теория

Теория и практика С++

int chars = 0;int punct = 0;int others = 0;cin.get(ch); //получение первого символаwhile (ch != '@') //проверить признак окончания ввода{if(isalpha(ch)) //этот символ - алфавитный?

chars++;else if(isspace(ch)) //этот символ - пробел?

whitespace++;else if(isdigit(ch)) //этот символ - цифра

digits++;else if(ispunct(ch)) //этот символ - знак пунктуации?

punct++;else

others++;cin.get(ch); //получение следующего символа}cout << chars << " букв, "

<< whitespace << " пробелов, "<< digits << " цифр, "<< punct << " знаков препинания, "<< others << " прочих.\n";

}

Результат:Enter text for analysis, and type @ to terminate input.This "map", 555 times.@12 letters, 3 whitespace, 3 digits, 4 punctuation, 0 others.

Оператор ?:Оператор ?: может использовать вместо оператора if else. Этот оператор называется условным и

записывается как ?:. Его общая форма выглядит так:

выражение1 ? выражение2 : выражение3

Если выражение1 истинно, то значением всего условного выражения будет вырежение2. В противном случае значением всего выражения будет выражение3.

5 > 3 ? 10 : 12 // 5 > 3 истинно, поэтому значение выражения – 103 == 9 ? 25 : 18 //3 == 9 ложно, поэтому значение выражения – 18

Программа 5_7В программе условный оператор используется для определения большего из двух значений.// использование условного оператора#include "stdafx.h"#include <iostream>using namespace std;

void main(){

setlocale(LC_CTYPE, "RUS");

int a, b;cout << "Введите два числа: ";cin >> a >> b;cout << "Большее из " << a << " и " << b;int c = a > b ? a : b;cout << " - " << c << "\n"; //если a > b, то c = a; в противном случае c = b

}

Результат:Enter two integers: 25 27The larger of 25 and 27 is 27

56

56

Page 57: C++ теория

Теория и практика С++

Оператор switchОператор switch обеспечивает выбор из расширенного списка.

switch (целочисленное выражение){

case метка1 : оператор(ы) break;

case метка2 : оператор(ы) break;

…default : оператор(ы)

}

Встретив ключевое слово switch, программа переходит к строке, помеченной значением, соответствующим значению целочисленного выражения. Например, если целочисленное выражение имеет значение 4, программа переходит к строке с меткой case 4:. Если целочисленное выражение не соответствует ни одной из меток, программа переходит к строке, помеченной меткой default. Эта метка необязательна: если она опущена и совпадение с метками отсутствует, программа переходит к оператору, следующему за switch.

Программа 5_8В программе показано совместное использование операторов switch и break для реализации простого меню. Для отображения набора вариантов в программе применяется функция showmenu(). Затем оператор switch выбирает действие, исходя из ответа пользователя.// использование оператора switch#include <iostream>using namespace std;void showmenu();void report();void comfort();

void main(){ showmenu(); int choice; cin >> choice; while (choice != 5) { switch(choice) { case 1 : cout << "\a\n"; break; case 2 : report(); break; case 3 : cout << "The boss was in all day.\n"; break; case 4 : comfort(); break; default : cout << "That's not a choice.\n"; } showmenu(); cin >> choice; } cout << "Bye!\n";}//---------------------------------------------------------------------------void showmenu(){ cout << "Please enter 1, 2, 3, 4, or 5:\n" "1) alarm 2) report\n" "3) alibi 4) comfort\n" "5) quit\n";}//---------------------------------------------------------------------------

57

57

Page 58: C++ теория

Теория и практика С++

void report(){ cout << "It's been an excellent week for business.\n" "Sales are up 120%. Expanses are down 35%.\n";}//---------------------------------------------------------------------------void comfort(){ cout << "Your employes think you are the finest CEO\n" "in the indusrty. The board of directors think\n" "you are the finest CEO in the industry.\n";

}

Как отмечалось ранее, этой программе необходимы операторы break, чтобы ограничить выполнение определенной частью оператора switch. Дабы убедиться, что это именно так, вы можете удалить операторы break из листинга 5_8 и посмотреть, как программа будет работать после этого. Так, например, вы обнаружите, что ввод 2 заставит программу выполнить все операторы, ассоциированные с метками 2, 3, 4 и default. C++ ведет себя подобным образом, потому что такое поведение иногда может быть полезным. Во-первых, за счет этого можно легко использовать множественные метки. Например:

Программа 5_9#include "stdafx.h"#include <iostream>using namespace std;

void main(){

char choice;cin >> choice;while(choice != 'Q' && choice != 'q'){

switch(choice){

case 'a':case 'A': cout << "A1\n";

break;case 'b':case 'B': cout << "B1\n";

break;case 'c':case 'C': cout << "C1\n";

break;case 'd':case 'D': cout << "D1\n";

break;default: cout << "That's right.\n";

}cin >> choice;

}}

Операторы break и continueОператоры break и continue позволяют программе пропускать фрагменты кода. Оператор break можно

использовать в операторе switch и в любых циклах. Он вызывает переход к оператору, следующему за оператором switch или циклом. Оператор continue используется в циклах. Результат его действия следующий: программа пропускает остальную часть тела цикла и начинает новую итерацию цикла.

Программа 5_9Пользователь вводит строку текста. Цикл отображает на экране каждый символ; если встречается точка, то используется оператор break для завершения цикла. Затем программа подсчитывает количество пробелов, но

58

58

Page 59: C++ теория

Теория и практика С++

не остальных символов. Оператор continue используется для пропуска связанной с подсчетом части цикла, когда символ не является пробелом.// использование операторов continue и switch#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 80;

void main(){

setlocale(LC_CTYPE, "RUS");

char line[ArSize];int spaces = 0;cout << "Введите строку текста:\n";cin.get(line, ArSize);cout << "Полная строка:\n" << line << endl;cout << "Строка до первой точки: \n";for (int i = 0; line[i] != '\0'; i++){

cout << line[i]; //отображение символаif (line[i] == '.') //выход, если это точка

break;if (line[i] != ' ') //пропуск остальной части цикла

continue;spaces++;

}cout << "\n" << spaces << " пробелов\n";cout << "Готово.\n";

}

Результат:Enter a line of text:This is a sample. Buy!This is a sample.3 spacesDone.

Использование перечислителей в качестве метокВ листинге 5.9.1 иллюстрируется применение enum для определения набора взаимосвязанных констант в операторе switch. В общем случае входной поток cin не распознает перечислимые типы (он не может знать, как вы определите их), поэтому программа читает выбор как int. Когда оператор switch сравнивает значение int с персчислимой меткой case, он приводит перечисление к типу int. Точно также перечисления приводятся к int в проверочном условии цикла while.

Программа 5.9.1// enum.cpp -- использование enum#include "stdafx.h"#include <iostream>using namespace std;

// create named constants for 0 - 6enum {red, orange, yellow, green, blue, violet, indigo};

void main(){

setlocale(LC_CTYPE, "RUS");

cout << "Введите код цвета (0-6): ";int code;cin >> code;while (code >= red && code <= indigo){

59

59

Page 60: C++ теория

Теория и практика С++

switch (code){

case red : cout << "Ее губы были красными.\n"; break;case orange : cout << "Ее волосы были рыжими.\n"; break;case yellow : cout << "Ее туфли были желтыми.\n"; break;case green : cout << "Ее ногти были зелеными.\n"; break;case blue : cout << "Ее костюм был синим.\n"; break;case violet : cout << "Ее глаза были фиолетовыми.\n"; break;case indigo : cout << "Ее настроение было цвета индиго.\n";

break;}cout << "Введите код цвета (0-6): ";cin >> code;

}cout << "Всего наилучшего\n";

}

Циклы считывания чиселПредположим, что вы пишете программу, которая должна читать серию чисел в массив. Вы хотите дать возможность пользователю прервать ввод до наполнения массива. Один из способов сделать это — воспользоваться поведением cin. Рассмотрим следующий код:

int n;cin >> n;

Что случится, если пользователь ответит на запрос, введя слово вместо числа?При этом произойдет пять событий:

1. Значение n останется неизменным.2. Некорректный ввод останется во входной очереди.3. Будет выставлен флаг ошибки в объекте cin.4. Вызов метода cin, будучи преобразованным к типу bool, вернет false.

Тот факт, что метод вернет false, означает, что вы можете использовать нецифровой ввод для прерывания цикла чтения чисел. Тот факт, что нецифровой ввод выставляет флаг ошибки cin, означает, что вы должны сбросить этот флаг перед тем, как программа снова сможет читать ввод. Метод clear(), который также сбрасывает условие конца файла (endof-file — EOF) позволяет сбросить флажок некорректного ввода. Рассмотрим несколько примеров, иллюстрирующих эту технику.

Программа 5_10Предположим, что нужно создать программу, которая подсчитывает средний вес выловленной за день рыбы. Количество пойманной рыбы ограничивается пятью штуками, поэтому все данные могут быть размещены в пятиэлементном массиве, но возможно, что рыбы было поймано и меньше пяти штук. В программе используется цикл, который прерывается, если массив полон или вводится отличающееся от числового значение.// нечисловой ввод прерывает выполнение цикла#include "stdafx.h"#include <iostream>using namespace std;

const int Max = 5;

void main(){

setlocale(LC_CTYPE, "RUS");

// получить данныеdouble fish[Max];cout << "Пожалуйста, введите вес пойманных рыб.\n";cout << "Можно ввести до " << Max << " рыб <q - для завершения>.\n";cout << "рыба #1: ";int i = 0;while (i < Max && cin >> fish[i]){

60

60

Page 61: C++ теория

Теория и практика С++

if (++i < Max)cout << "рыба #" << i+1 << ": ";

}// вычислить среднееdouble total = 0.0;for (int j = 0; j < i; j++)

total += fish[j];// вывести результатыif (i == 0)

cout << "Нет рыбы\n";else

cout << total / i << " = средний вес " << i << " рыб\n";cout << "Готово.\n";

}

Результат:Please enter the weights of your fish.You may enter up to 5 fish <q to terminate>.fish #1: 30fish #2: 35fish #3: 25fish #4: 40fish #5: 1529 = average weight of 5 fishDone.

Программа 5_11В программе 5_10 не предпринималась попытка считать какие-либо входные данные после ввода

нечислового значения. Предположим, что необходимо ввести ровно пять значений набранных в гольфе очков для получения среднего значения. Если пользователь вводит значение, не являющееся числовым, программа должна вывести сообщение, настаивая на вводе числового значения. Для проверки результатов нечислового ввода можно использовать значение выражения cin.

Предположим, выяснилось, что пользователь ввел неверные данные. Необходимо выполнить три действия:

Сбросить флаг ошибки cin для приема нового вводимого значения. Избавиться от неверно введенного значения. Предложить пользователю повторить попытку.

Обратите внимание: прежде чем избавляться от неверно введенного значения, необходимо сбросить флаг ошибки cin.// программа p5_11.cpp - отбрасывание нечислового ввода#include "stdafx.h"#include <iostream>using namespace std;

const int Max = 5;

void main(){

setlocale(LC_CTYPE, "RUS");

// ввести данныеint golf[Max];cout << "Пожалуйста, введите ваши результаты в гольфе.\n";cout << "Необходимо ввести " << Max << " раундов.\n";int i;for (i = 0; i < Max; i++){

cout << "раунд #" << i+1 << ": ";while (!(cin >> golf[i])){

cin.clear(); // очистить вводwhile (cin.get() != '\n')continue; // отбросить неверный вводcout << "Пожалуйста введите число: ";

61

61

Page 62: C++ теория

Теория и практика С++

}}// вычислить среднееdouble total = 0.0;for (i = 0; i < Max; i++)total += golf[i];// сообщить результатcout << total / Max << " = средний результат " << Max << " раундов\n";

}

Результат:Please enter your golf scores.You must enter 5 rounds.round #1: 88round #2: 87round #3: must i?please enter a number: 103round #4: 94round #5: 8691.6 = average score 5 rounds

Текстовый ввод-вывод и текстовые файлыДавайте еще раз рассмотрим концепцию текстового ввода-вывода. Когда вы используете cin для ввода,

программа рассматривает ввод в форме последовательности байт, интерпретируемых как коды символов. Независимо от типа данных назначения, ввод начинается как символьные данные — то есть текстовые. Объект cin затем берет на себя ответственность за преобразование текста в другие типы. Чтобы увидеть, как это работает, давайте рассмотрим, как разный код обрабатывает одну и ту же строку ввода.

Предположим, что у вас есть следующая простая входная строка;

38.5 19.2

Давайте посмотрим, как эта строка ввода обрабатывается cin при использовании с данными разных типов. Для начала попробуем тип char:

char ch;cin >> ch;

Первый символ входной строки присваивается ch. В данном случае первый символ — десятичная цифра 3, и двоичный код этой цифры помещается в ch. Введенное значение и целевая переменная имеют тип char, поэтому никакого преобразования не требуется. (Обратите внимание, что сохраняется не числовое значение 3, а код символа '3'.) После этого десятичная цифра 8 будет следующим символом во входной очереди, и будет следующим значением, которое поступит в следующей операции ввода.

Теперь попробуем тип int:

int n;cin >> n;

В этом случае cin читает до первого нецифрового символа. То есть он читает цифру 3 и цифру 8, оставляя точку в качестве следующего символа во входной очереди, cin вычисляет, что эти два символа соответствуют числовому значению 38, и двоичный код 36 копируется в n.

Дальше попробуем тип double:

double x;cin >> х;

В этом случае cin выполняет чтение до первого символа, который не может быть частью числа с плавающей точкой. То есть, он читает цифру 3, цифру 8, символ точки и цифру 5, оставляя пробел в качестве следующего символа во входной очереди. cin вычисляет, что эти четыре символа соответствует числовому значению 38.5, и двоичный код (в формате плавающей точки) числа 38.5 копируется в х.

Теперь попробуем тип символьного массива:

char word[50];cin >> word;

62

62

Page 63: C++ теория

Теория и практика С++

В этом случае cin читает до первого пробельного символа. То есть он читает цифру 3, цифру 8, символ точки, цифру 5, оставляя пробел во входной очереди. cin помещает коды этих четырех символов в массив word и добавляет ограничивающий нулевой символ. Никаких преобразований не требуется.

И, наконец, попробуем другой вариант ввода для типа символьного массива:

char word[50];cin.getline(word, 50);

Теперь cin читает вплоть до символа новой строки. Все символы до заключительной цифры 2 помещаются в массив word, и к ним добавляется нулевой символ-ограничитель. Символ новой строки отбрасывается, и следующим символом во входной очереди будет первый символ после перевода строки. Опять-таки, никакого преобразования не осуществляется.

При выводе происходит обратный процесс. То есть целые преобразуются в последовательности десятичных цифр, а числа с плавающей точкой — в последовательности десятичных цифр и некоторых других символов (например, 284.53 или -1.587Е+06). Символьные данные преобразования не требуют.

Основная идея состоит в том, что всякий ввод начинается с текста. Поэтому файловый эквивалент консольного ввода — это текстовый файл, то есть файл, в котором каждый байт хранит код некоторого символа. Не все файлы являются текстовыми. Например, базы данных и электронные таблицы хранят числовые данные в числовом виде — то есть в двоичной целочисленной форме, или двоичном представлении чисел с плавающей точкой. Также файлы, созданные текстовыми процессорами, могут хранить текстовую информацию, но они также хранят и нетекстовые данные, описывающие форматирование, шрифты, принтеры и тому подобное.

Запись текстового файла

Вы должны включать заголовочный файл fstream. Заголовочный файл fstream определяет класс ofstream для обработки вывода. Вы должны объявлять один или более переменных типа ofstream. Вы должны учитывать пространство имен std; например, вы можете использовать директиву using или

префикс std:: для таких элементов, как ofstream, Вы должны ассоциировать конкретный объект ofstream с определенным файлом; одним из способов

сделать это является применение метода open(). По окончании работы с файлом вы должны использовать метод close() для закрытия файла. Вы можете использовать ofstream с операцией >> для чтения данных различных типов.

Вот как вы объявляете такие объекты:

ofstream outFile; // outFile - объект типа ofstreamofstream fout; // fout - объект типа ofstream

А вот как вы можете ассоциировать объекты с конкретными файлами:

outFile.open("fish.txt"); // outFile используется для записи в файл fish.txtchar filename[50];cin >> filename; // пользователь указывает имя файлаfout.open(filename); // fout используется для чтения указанного файла

Обратите внимание, что метод open() требует в качестве аргумента строки в стиле С. Это может быть строковый литерал или строка, сохраненная в символьном массиве. Вот как вы можете использовать эти объекты:

double wt = 125.8;outFile << wt; // записать число в fish.txtchar line[81] = "This is a sample.";fout << line << endl; // записать строку текста

Важный момент заключается в том, что после того, как вы объявили объект ofstream и ассоциировали его с файлом, то используете его точно так же, как использовали бы cout. Все операции и методы, доступные cout, такие как <<, endl и setf(), также доступны для всех объектов ofstream, подобных outFile и fout из предыдущих примеров.

Короче говоря, ниже представлены основные шаги, которые нужно пройти, чтобы использовать файловый вывод:

1. Включить заголовочный файл fstream.2. Создать объект ofstream.3. Ассоциировать объект ofstream с файлом.4. Использовать объект ofstream.B той же манере, как вы используете cout.

63

63

Page 64: C++ теория

Теория и практика С++

Программа в листинге 5.11 демонстрирует этот подход. Она запрашивает информацию от пользователя, посылает вывод на экран, а затем посылает тот же вывод в файл. Вы можете просмотреть полученный файл в текстовом редакторе.

Программа 5_11// outfile.cpp -- запись файла#include "stdafx.h"#include <iostream>using namespace std;#include <fstream>

int main(){

setlocale(LC_CTYPE, "RUS");

char automobile[50];int year;double a_price;double d_price;ofstream outFile; // создать объект вывода

outFile.open("carinfo.txt"); // ассоциировать его с файломcout << "Введите производителя и модель автомобиля: ";cin.getline(automobile, 50);cout << "Введите год выпуска: ";cin >> year;cout << "Введите запрашиваемую цену: ";cin >> a_price;d_price = 0.913 * a_price;

// отобразить информацию на экране через coutcout << fixed;cout.precision(2);cout.setf(ios_base::showpoint);cout << "Производитель и модель: " << automobile << endl;cout << "Год: " << year << endl;cout << "Начальная цена $" << a_price << endl;cout << "Окончательная цена $" << d_price << endl;

// теперь ту же информацию вывести через outFile вместо coutoutFile << fixed;outFile.precision(2);outFile.setf(ios_base::showpoint);outFile << "Производитель и модель: " << automobile << endl;outFile << "Год: " << year << endl;outFile << "Начальная цена $" << a_price << endl;outFile << "Окончательная цена $" << d_price << endl;outFile.close(); // закрыть файлreturn 0;

}

Чтение текстового файла Вы должны включить заголовочный файл fstream. Заголовочный файл fstream определяет класс ifstream для обработки ввода. Вы должны объявлять одну или более переменных, или объектов, типа ifstream которые можете назвать

по своему усмотрению, учитывая принятые для этого соглашения. Вы должны ассоциировать конкретный объект ifstream с конкретным файлом; один из способов сделать

это — воспользоваться методом open(). Завершив работу с файлом, вы должны вызвать метод close(), чтобы закрыть его. Вы можете использовать объект ifstream с операцией << для чтения данных различных типов. Вы можете использовать объект ifstream с методом get() для чтения отдельных символов и с методом

getline() — для чтения целых строк.

64

64

Page 65: C++ теория

Теория и практика С++

Вы можете использовать объект ifstream с такими методами, как eof() и fail(), чтобы отслеживать успешность попыток ввода.

Сам объект ifstream, когда присутствует в проверочных условиях, преобразуется в булевское значение true, если последняя попытка чтения была успешной, и false — в противном случае.

Следует отметить, что хотя заголовочный файл iostream представляет предопределенный объект istream по имени cin, вы должны объявлять свой собственный объект ifstream, выбирая для него имя и ассоциируя с файлом. Вот как объявляются такие объекты:

ifstream inFile; // inFile - объект типа ifstreamifstream fin; // fin - объект типа ofstream

А вот как с ними можно ассоциировать конкретные файлы:

inFile.open("bowling.txt"); // inFile используется для чтения файла bowling.txtchar filename[50];cin >> filename; // пользователь указывает имя файлаfin.open(filename); // fout используется для чтения указанного файла

Метод open() требует в качестве аргумента строки в стиле С. Это может быть литеральная строка или же строка, сохраненная в символьном массиве. Вот как можно использовать эти объекты:

double wt;inFile >> wt; // читает число из bowling.txtchar line[81];fin.getline(line, 81); // читать строку текста

Важный момент, который следует отметить: когда вы объявили объект ifstream и ассоциировали его с определенным файлом, то после этого можете использовать его точно так же, как используете cin. Все операции и методы, доступные cin, также доступны объектам ifstream, как это демонстрируют inFile и fin в предыдущих примерах.

Что случится, если вы попытаетесь открыть для ввода несуществующий файл? Эта ошибка приведет к тому, что все последующие попытки использовать объект ifstream для ввода будут обречены на провал. Предпочтительный способ проверки того, удалось ли открыть файл, заключается в применении метода is_open(). Вы можете использовать для этого код вроде следующего:

inFile.open("bowling.txt");if(!inFile.is_open()){

exit(EXIT_FAILURE);}

Метод is_open() возвращает true, если файл открыт успешно, поэтому выражение !in-File.is_open() вернет true, если попытка не удастся. Прототип функции exit() находится в заголовочном файле cstdlib, где также определена константа EXIT_FAILURE как значение аргумента, используемого для взаимодействия программы с операционной системой. Функция exit() прерывает программу.

Программа в листинге 5.12 открывает файл, указанный пользователем, читает из файла числа, после чего сообщает количество прочитанных значений, их сумму и их среднюю величину. Здесь важно правильно спроектировать входной цикл. Отметим, что в этой программе интенсивно используются операторы if.

Программа 5.12// чтение файла#include "stdafx.h"#include <iostream>using namespace std;#include <fstream>

const int SIZE = 60;

int main(){

setlocale(LC_CTYPE, "RUS");

char filename[SIZE];ifstream inFile; // объект для обработки файлового вводаcout << "Введите имя файла данных: ";

65

65

Page 66: C++ теория

Теория и практика С++

cin.getline(filename, SIZE);inFile.open(filename); // ассоциировать inFile с файломif (!inFile.is_open()) // не удалось открыть файл{

cout << "Не удалось открыть файл " << filename << endl;cout << "Программа прервана.\n";exit(EXIT_FAILURE);

}

double value;double sum = 0.0;int count = 0; // количество прочитанных элементовinFile >> value; // получить первое значениеwhile (inFile.good()) // пока ввод успешен и не достигнут EOF{

++count; // еще один элемент прочитанsum += value; // вычислить текущую суммуinFile >> value; // получить следующее значение

}

if (inFile.eof())cout << "Достигнут конец файла.\n";

else if (inFile.fail())cout << "Ввод прекращен из-за несоответствия данных.\n";

elsecout << "Ввод прекращен по неизвестной причине.\n";

if (count == 0)cout << "Никакие данные не обработаны.\n";

else{

cout << "Прочитано элементов: " << count << endl;cout << "Сумма: " << sum << endl;cout << "Среднее: " << sum / count << endl;

}inFile.close(); // завершить работу с файлом

return 0;}

Замечания по программеВместо жесткого кодирования имени файла программа из листинга 5.12 сохраняет введенное пользователем

имя в символьном массиве filename. Затем этот массив используется в качестве аргумента open():

inFile.open(filename);

Как говорилось ранее, весьма желательно проверять, удалась ли попытка открытия файла. Вот несколько причин возможных неудач: файл может не существовать, он может находиться в другом каталоге или папке, к нему может быть запрещен доступ, либо пользователь может допустить опечатку, вводя его имя, или пропустить его расширение.

Вы должны уделять особое внимание правильному проектированию цикла чтения файла. Есть несколько вещей, которые нужно проверять при чтении файла. Во-первых, программа не должна пытаться читать после достижения ЕОF. Метод eof() возвращает true, когда последняя попытка чтения данных столкнулась с EOF. Во-вторых, программа может столкнуться с несоответствием типа. Например, программа из листинга 5.12 ожидает файла, содержащего только числа. Метод fail() возвращает true, когда последняя попытка чтения сталкивается с несоответствием типа. (Этот метод также возвращает true при достижении ЕОF). И, наконец, что-нибудь может пойти не так — например, файл окажется поврежденным или случится сбой оборудования. Метод bad() вернет true, если самая последняя попытка чтения столкнется с такой проблемой. Вместо того чтобы проверять все эти условия индивидуально, проще применить good(), которая возвращает true, если все идет хорошо:

while (inFile.good()) // пока ввод успешен и не достигнут EOF{

...}

66

66

Page 67: C++ теория

Теория и практика С++

Затем при желании можно воспользоваться другими методами, чтобы определить, почему именно был прерван цикл:

if (inFile.eof())cout << "Достигнут конец файла.\n";

else if (inFile.fail())cout << "Ввод прекращен из-за несоответствия данных.\n";

elsecout << "Ввод прекращен по неизвестной причине.\n";

Этот код находится сразу после цикла, поэтому он исследует, почему цикл был прерван. Поскольку eof() проверяет только EOF, a fail() проверяет как EOF, так и несоответствие типа, здесь вначале проверятся именно EOF Таким образом, если выполнение дойдет до else if, то достижение конца файла (EOF), как причина выхода из цикла, будет исключена, и значение true, полученное от fail(), недвусмысленно укажет на несоответствие типа.

Важно также понимать, что good() сообщает лишь о самой последней попытке чтения ввода. Это значит, что попытка чтения должна непосредственно предшествовать вызову good(). Стандартный способ обеспечения этого — иметь один оператор ввода непосредственно перед началом цикла, перед первой проверкой условия цикла, а второй — в конце цикла, непосредственно перед следующей проверкой условия цикла:

inFile >> value; // получить первое значениеwhile (inFile.good()) // пока ввод успешен и не достигнут EOF{

++count; // еще один элемент прочитанsum += value; // вычислить текущую суммуinFile >> value; // получить следующее значение

}

Вопросы для самоконтроля1. Рассмотрим следующие два фрагмента кода для подсчета пробелов и переводов строк:

// Версия 1while(cin.get(ch)) // выйти по eof{

if(ch == ' ')spaces++;

if(ch == '\n')newlines++;

}// Версия 2while(cin.get(ch)) // выйти по eof{

if(ch == ' ')spaces++;

else if(ch == '\n')newlines++;

}

Какие преимущества (если они есть) у второй формы перед первой?

2. Какой эффект будет иметь замена в листинге 5_2 ++ch на ch+1?3. Внимательно рассмотрите следующую программу:

#include "stdafx.h"#include <iostream>using namespace std;

int main(){

char ch;int ct1, ct2;ct1 = ct2 = 0;while((ch = cin.get()) != '$'){

cout << ch;ct1++;

67

67

Page 68: C++ теория

Теория и практика С++

if(ch = '$')ct2++;

cout << ch;}cout << "ct1 = " << ct1 << ", ct2 = " << ct2 << endl;

return 0;}

Предположим, вы осуществите следующий ввод, где означает нажатие клавиши <Enter>:

HiSend $10 or $20 now!

Каким будет вывод? (Помните, что ввод буферизуется.)

4. Сконструируйте логические выражения для представления следующих условий:а. weight больше или равен 115, но меньше 125,б. ch равно q или Q,в. х — четное, но не равно 26.г. х — четное, но не кратно 26.д. donation находится в диапазоне 1000-2000 или guest равно 1,е. ch — буква в нижнем или верхнем регистре (предполагая, что буквы нижнего регистра кодируются последовательно и буквы верхнего регистра также кодируются последовательно, но между нижним и верхним регистром имеется промежуток).

5. На английском языке предложение "I will not not speak" означает то же, что и "I will speak". На языке C++ !!х — это то же самое, что и х?

6. Сконструируйте условное выражение, которое эквивалентно абсолютному значению переменной. То есть, если переменная х положительна, значением выражения будет просто значение х, но если х — отрицательна, то значением выражения должно быть -х, то есть положительное.

7. Перепишите следующий фрагмент с применением switch:

if(ch == 'A')a_grade++;

else if(ch == 'B')b_grade++;

else if(ch == 'C')c_grade++;

else if(ch == 'D')d_grade++;

elsef_grade++;

8. Рассмотрим следующий фрагмент кода:

int line = 0;char ch;while(cin.get()){

if(ch == 'Q')break;

if(ch != '\n')continue;

line++;}Перепишите этот код без операторов break и continue.

Упражнения по программированию (5)1. Напишите программу, которая считывает ввод с клавиатуры до появления символа @ и отображает эти

данные за исключением цифр. При этом каждый прописной символ преобразуется в строчный и наоборот (не забудьте о семействе cctype).

2. Напишите программу, которая считывает до десяти значений "пожертвований" в массив значений типа double. Программа должна прерывать ввод при вводе нечислового значения. Необходимо выводить отчет

68

68

Page 69: C++ теория

Теория и практика С++

о средней величине "пожертвований", а также о том, сколько значений в массиве превышают среднее значение.

3. Напишите заготовку программы, управляемой меню. Программа должна отображать меню, состоящее из четырех пунктов, каждый из которых помечен буквой. Если пользователь отвечает вводом буквы, отличающейся от меток, программа должна предлагать ему ввести допустимый ответ до тех пор, пока это не будет сделано. Затем программа должна использовать оператор switch для выбора простого действия, исходя из ответа пользователя. Результат выполнения программы мог бы выглядеть примерно так:

Пожалуйста, введите одно из следующих значений:c) хищник p) пианистt) дерево g) играfПожалуйста, введите a c, p, t, или g: qПожалуйста, введите a c, p, t, or g: tКлен – это дерево.

4. Вступив в Благотворительный орден программистов (Benevolent Order of Programmers — ВОР), вы можете быть известны на собраниях ВОР под своим действительным именем, по названию должности или по секретному псевдониму ВОР. Напишите программу, которая может перечислять членов ордена по действительным именам, по должностям, по секретным псевдонимам или по индивидуально заданным опциям. В основу программы положите следующую структуру:

// Структура имен членов Благотворительного Ордена Программистов (БОП)struct bop{char fullname[strsize]; // настоящее имяchar title[strsize]; // должностьchar bopname[strsize]; // секретный псевдоним ВОРint preference; // 0 = полное имя, 1 = титул, 2 = имя БОП};

В программе создайте небольшой массив таких структур и инициализируйте его подходящими значениями. Программа должна выполнять цикл, который пользователю возможность выбирать различные варианты:

а. отображение по имениb. отображение по должностис. отображение по секретному псевдонимуd. отображение по индивидуально заданным опциямq. выход

Учтите, что вариант "отображение по индивидуально заданным опциям" означает не вывод значения элемента preference, а отображение данных членов ордена в соответствии с установленными для них опциями. Результат выполнения программы может выглядеть примерно так:

Отчет о Благотворительном Ордене Программистовa. отображать по именам b. отображать по должностямc. отображать по именам БОП d. отображать по предпочтениямq. выйтиВаш выбор: aWimp MachoRaki RhodesCelia LaiterBoppy HipmanPat HandСледующий выбор: dWimp MachoJunior ProgrammerHIPSAnalyst TraineeLOOPYСледующий выбор: qПока!

5. Королевство Нейтрония, где денежной единицей служит тварп, использует следующую шкалу налогообложения:Первые 5 000 тварпов — налог 0%

69

69

Page 70: C++ теория

Теория и практика С++

Следующие 10 000 тварпов — налог 10%Следующие 20 000 тварпов — налог 15%Свыше 35 000 тварпов — налог 20%Например, если некто зарабатывает 38 000 тварпов, то он должен заплатить налогов 5000 х 0.00 + 10000 х 0.10 + 20000 х 0.15 + 3000 х 0.20, или 4 600 тварпов. Напишите программу, которая использует цикл для запроса доходов и выдачи подлежащего к выплате налога. Цикл должен прерываться, когда пользователь вводит отрицательное или нечисловое значение.

6. Необходимо создать программу, которая отслеживает денежные взносы в Общество защиты нравственности. Она предлагает пользователю ввести количество взносов, затем имя и сумму взноса каждого спонсора. Информация должна храниться в динамическом массиве структур. Каждая структура содержит два элемента: массив символов имени и значение типа double — сумму взноса. После считывания всех данных программа отображает имена и суммы взносов всех спонсоров, которые внесли более $10000. В начале этого списка должен находиться заголовок, указывающий, что следующие спонсоры являются почетными членами общества. После этого выводится список остальных спонсоров. Этот список должен следовать после заголовка "Спонсоры". Если в какой-либо категории отсутствуют спонсоры, программа выводит слово "нет". Помимо вывода двух категорий программа не осуществляет сортировку данных.

7. Напишите программу, которая читает слова по одному за раз, пока не будет введена отдельная буква q. После этого программа должна сообщить количество слов, начинающихся с гласных, количество слов, начинающихся с согласных, а также количество слов, не попадающих ни в одну из этих категорий. Одним из возможных подходов может быть применение isaipha() для различия между словами, начинающимися с букв, и остальными, с последующим применением if или switch для идентификации тех слов, прошедших проверку isaipha(), которые начинаются с гласных. Пример запуска может выглядеть так:

Вводите слова (q — для выхода):The 12 awesome oxen ambledquietly across 15 meters of lawn. q5 слов начинаются с гласных4 слов начинаются с согласных2 остальных

8. Напишите программу, которая открывает текстовый файл, читает его символ за символом до самого конца и сообщает количество символов в файле.

9. Выполните упражнение 6, но измените его так, чтобы данные можно было получать из файла. Первым элементом файла должно быть количество жертвователей, а остальная часть состоять из пар строк, в которых первая строка содержит имя, а вторая — сумму пожертвования. То есть файл должен выглядеть примерно так:

4Sam Stone2000Freida Flass100500Tammy Tubbs5000Rich Raptor55000

6. Функции языка С++Чтобы воспользоваться функцией в языке С++, необходимо выполнить следующие действия:

предусмотреть определение функции предусмотреть прототип функции вызвать функцию

Если вы используете библиотечную функцию, то эта функция уже определена и скомпилирована для вас. К тому же вы можете воспользоваться стандартным библиотечным заголовочным файлом, чтобы предоставить своей программе доступ к прототипу. Все что вам остается — правильно вызвать эту функцию.

Когда вы создаете собственные функции, то должны самостоятельно обработать все три аспекта — определение, прототипирование и вызов. В листинге 6.1 демонстрируются все три шага на небольшом примере.

70

70

Page 71: C++ теория

Теория и практика С++

Программа 6_1#include "stdafx.h"#include <iostream>using namespace std;

void simple(); // прототип функции

int main(){

setlocale(LC_CTYPE, "RUS");

cout << "main() вызовет функцию simple():\n";simple(); // вызов функции

return 0;}

// определение функцииvoid simple(){

cout << "Я - функция simple.\n";}

Результат:main() вызовет функцию simple():Я - функция simple.

Определение функцииФункции можно разбить на две следующие категории: функции, которые не возвращают значений, и

функции, возвращающие значения. Функции без возвращаемых значений называются функциями типа void, они имеют следующую общую форму:

void имяФункции(списокАргументов){

оператор(ы)return; // необязательный оператор

}

В списке списокАргументов указываются типы и количество аргументов (параметров), передаваемых функции. Необязательный оператор return обозначает конец функции.

Функция с возвращаемым значением вычисляет значение, которое она возвращает вызывающей ее функции. Общая форма подобной функции имеет следующий вид:

имяТип имяФункции(списокАргументов){

оператор(ы)return значение; //тип возвращаемого значения - имяТипа

}

Возвращаемое значение не может быть массивом.

Программа 6_2// использование прототипов и вызовы функций#include "stdafx.h"#include <iostream>using namespace std;

void cheers(int); // прототип: нет значения возвратаdouble cube(double x); // прототип: возвращает double

int main(){

setlocale(LC_CTYPE, "RUS");

71

71

Page 72: C++ теория

Теория и практика С++

cheers(5); //вызов функцииcout << "Введите число: ";double side;cin >> side;double volume = cube(side); //вызов функцииcout << side << " в кубе равно ";cout << volume << " кубических футов.\n";cheers(cube(2)); //защита прототипа в действии

return 0;}

void cheers(int n){ for (int i=0; i < n; i++) cout << "Спасибо! "; cout << endl;}

double cube(double x){ return x * x * x;}

Результат:Cheers! Cheers! Cheers! Cheers! Cheers!Give me a number: 5A 5-foot cube has a volume of 125 cubic feet.Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers!

Аргументы функции и передача по значениюОбычно аргументы передаются по значению, другими словами, числовое значение аргумента передается

функции, где оно присваивается новой переменной. Например, в программе 6_2 продемонстрировано обращение к функции:

double volume = cube(side);

Здесь side – это переменная, которая в процессе выполнения программы получает значение 5. Напомним, что заголовок функции cube() был таким:

double cube(double x)

Эта функция при обращении к ней создает новую переменную типа double с именем x и присваивает ей значение 5. Благодаря такому приему данные в функции main() изолируются от действий, которые происходят в функции cube(), так как cube() использует в своей работе всего лишь копию переменной side, а не саму переменную.

Переменные, в том числе параметры, объявленные внутри какой-либо функции, являются ее собственностью. Во время вызова функции компьютер выделяет память, необходимую для этих переменных. По завершению выполнения функции память, которая использовалась переменными, высвобождается. Такие переменные называются локальными, поскольку сфера их действия не выходит за пределы функции.

Программа 6_2_1//передача параметров по значению#include "stdafx.h"

int sqr(int x);

void main(){

int t = 10;printf("%d %d", sqr(t), t);

}

72

72

Page 73: C++ теория

Теория и практика С++

int sqr(int x){

x = x * x;return x;

}

В этом примере значение аргумента функции sqr(), т.е. число 10 копируется в параметр х. После выполнения присваивания x = x * x модифицируется только переменная х. Значение переменной t, указанной при вызове функции sqr(), по-прежнему равно 10.

Помните, что в функцию передается лишь копия аргумента. То, что произойдет с этой копией внутри функции, никак не отразится на оригинале.

Функции с несколькими аргументамиФункция может иметь не один, а несколько аргументов. В обращении к такой функции аргументы

отделяются друг от друга запятыми:

n_chars('R', 25);

Аналогично при определении функции в ее заголовке указываются список объявляемых параметров с разделением запятыми:

void n_chars(char c, int n) //два аргумента

Программа 6_3Программа представляет собой пример реализации функции с двумя аргументами. Как можно видеть в этом листинге, изменение значения формального параметра функции не влияет на дынные в вызывающей программе.// функция с двумя аргументами#include "stdafx.h"#include <iostream>using namespace std;

void n_chars(char, int);

void main(){ setlocale(LC_CTYPE, "RUS");

int times; char ch; cout << "Введите символ: "; cin >> ch; while (ch != 'q') //q для выхода из программы { cout << "Введите число: "; cin >> times; n_chars(ch, times); //функция с двумя аргументами cout << "\nВведите другой символ или нажмите клавишу q для выхода: "; cin >> ch; } cout << "Значение переменной times равно " << times << ".\n"; cout << "Готово\n";}

void n_chars(char c, int n) //Отображает значение с n раз{ while (n-- > 0) //продолжается, пока n не станет равным 0 cout << c;}

Результат:Enter a character: WEnter an integer: 50WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWEnter another character or press the q-key to quit: aEnter an integer: 20aaaaaaaaaaaaaaaaaaaa

73

73

Page 74: C++ теория

Теория и практика С++

Enter another character or press the q-key to quit: qThe value of times is 20.Bye

Функции и массивы

Программа 6_4Программа инициализирует массив некоторыми значениями и использует функцию sum_arr() для вычисления суммы.// программа p6_4.cpp - функция с аргументом-массивом#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 8;int sum_arr(int arr[], int n); //прототип

void main(){

setlocale(LC_CTYPE, "RUS");int cookies[ArSize] = {1,2,4,8,16,32,64,128};int sum = sum_arr(cookies, ArSize);cout << "Всего съедено печенья: " << sum << "\n";

}

//возвращает сумму элементов массива целых чиселint sum_arr(int arr[], int n){

int total = 0;for (int i = 0; i < n; i++)

total = total + arr[i];return total;

}

Результат:Total cookies eaten: 255

При вызове функции sum_arr(cookies, ArSize) ей передается адрес первого элемента массиваcookies и количество элементов этого массива. В функции sum_arr() адрес массива cookies присваивается переменной-указателю arr, а значение ArSize – переменной n типа int.

Таким образом прототип

int sum_arr(int arr[], int n)

то же самое, что и

int sum_arr(int *arr, int n)

Обозначения int *arr и int arr[] имеют одно и то же значение тогда и только тогда, когда они используются в заголовке или в прототипе функции. Обе записи означают, что аrr представляет собой указатель на int. Следует отметить, что обозначение массива вида (int arr []) в символической форме напоминает нам, что аrr указывает не только на тип int, но и на первый элемент int в массиве элементов типа int. Мы используем обозначение массива, когда указатель ссылается на первый элемент массива, и обозначение указателя, когда указатель ссылается на изолированное значение. Во всех других контекстах обозначения int *arr и int arr[] не идентичны. Например, нельзя воспользоваться обозначением int tip[] для объявления указателя в теле функции.

При условии, что переменная аrr фактически является указателем, имеет смысл и вся остальная часть функции. Как уже говорилось ранее, где рассматривались динамические массивы, для получения доступа к элементам массива можно использовать обозначение "массив со скобками" как с именем массива, так и с указателем. Независимо от того, является ли аrr указателем или именем массива, выражение аrr[3] обозначает четвертый элемент массива. И, по-видимому, будет полезно напомнить сейчас о том, что имеют место следующие два тождества:

arr[i] = *(arr + i) // два обозначения одной и той же величины &arr[i] = arr + i // два обозначения одного и того же адреса

74

74

Page 75: C++ теория

Теория и практика С++

Помните, что добавление 1 к указателю, содержащему имя массива, фактически означает добавление величины, равной размеру в байтах типа значения, на которое ссылается указатель. Увеличение значения указателя и изменение индексов элементов массива — это два эквивалентных способа отсчета элементов от начала массива.

Особенности использования массивов в качестве аргументов

Рассмотрим эти особенности на примере программы из листинга 6.4. При вызове функции sum_arr(cook-ies, ArSize) ей передается адрес первого элемента массива cookies и количество элементов этого массива. В функции sum_arr() адрес массива cookies присваивается переменной-указателю аrr, а значение ArSize — переменной n типа int. Это значит, что программа из листинга 6.4 на самом деле не передает указанной функции содержимое массива, а только сообщает ей, где находится массив (т.е. его адрес), что представляют собой его элементы (т.е. их тип) и сколько таких элементов содержит массив (переменная n). Обладая этой информацией, функция далее работает с исходным массивом. Если передать функции обычную переменную, то она будет работать с ее копией, но если передается массив, то она будет работать с его оригиналом. Однако, по сути дела, такое различие не нарушает принципа "передачи по значению", принятый в языке C++. Функция sum_arr() фактически передает значение, которое затем присваивается новой переменной. Разница лишь в том, что это значение является одиночным адресом, а не содержимым массива.

Приносит ли соответствие между именами массивов и указателями какую-либо пользу? Приносит, и немалую. Использование адресов массивов в качестве аргументов позволяет экономить время и память, необходимые для копирования всего массива: ведь дополнительные расходы ресурсов для копирования массивов большого размера могут оказаться недопустимо высокими. Программа потребует не только дополнительных объемов оперативной памяти, ей придется тратить время копирование крупных блоков данных. С другой стороны, работа с исходными данными повышает вероятность непреднамеренного искажения данных. Эта проблема решается при помощи спецификатор const (ниже применение этого спецификатора будет рассмотрено более подробно). Но сначала внесем в листинг 6.4 изменения, которые позволят нам продемонстрировать особенности функций, работающих с массивами. В листинге 6.5 проиллюстрирована ситуация, когда массивы cookies и аrr имеют одно и то же значение, а также показано, что понятие указателя придает функции sum_arr большую универсальность, чем может показаться на первый взгляд.

Программа 6_5// программа p6_5.cpp - перадача функции аргумента, представляющего массив#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 8;

int sum_arr(int arr[], int n); //прототип

void main(){

setlocale(LC_CTYPE, "RUS");

int cookies[ArSize] = {1,2,4,8,16,32,64,128};cout << cookies << " = адрес массива, ";cout << sizeof cookies << " = sizeof cookies\n";int sum = sum_arr(cookies, ArSize);cout << "Всего съедено печенья: " << sum << "\n";sum = sum_arr(cookies, 3);cout << "Первые три едока умяли " << sum << " печений.\n";sum = sum_arr(cookies + 4, 4);cout << "Последние пять едоков схрумкали " << sum << " печений.\n";

}

//возвращает сумму массива целых чиселint sum_arr(int arr[], int n){

int total = 0;cout << arr << " = arr, ";

75

75

Page 76: C++ теория

Теория и практика С++

cout << sizeof arr << " = sizeof arr\n";for (int i = 0; i < n; i++)total = total + arr[i];return total;

}

Результат:1245032 = адрес массива, 32 = sizeof cookies1245032 = arr, 4 = sizeof arrВсего съедено печенья: 2551245032 = arr, 4 = sizeof arrПервые три едока уняли 7 печений.1245048 = arr, 4 = sizeof arrПоследние пять едоков схрумкали 240 печений.

Примечания к программеВ листинге 6.5 проиллюстрированы некоторые наиболее интересные моменты использования функций,

выполняющих обработку массивов. Прежде всего обратите внимание на то, что cookies и аrr указывают на один и тот же адрес, как и предполагалось. Однако выражение sizeof cookies равно 32, в то время как sizeof arr равно всего лишь 4. Это объясняется тем, что sizeof cookies определяет размер всего массива, в то время как sizeof arr — величину переменной-указателя (данная программа выполнялась в системе с 4-байтовой адресацией). В частности, именно поэтому нужно явно передавать функции sum_arr() значение размера массива, а не использовать для этой цели конструкцию sizeof arr.

Функция sum_arr() может определить количество элементов массива только на основе информации, предоставляемой вторым аргументом. Это хорошая возможность ввести функцию в заблуждение. Например, при втором использовании этой функции программа вызывает ее следующим образом:

sum = sum_arr(cookies, 3);

Сообщив функции, что массив cookies содержит три элемента, вы заставляете ее вычислить сумму первых трех элементов.

Однако зачем ограничиваться только этим? Можно обмануть доверчивую функцию и относительно адреса ее первого элемента:

sum = sum_arr(cookies + 4, 4);

Поскольку переменная cookies выступает в качестве адреса первого элемента, выражение cookies + 4 представляет собой адрес пятого элемента. Этот оператор суммирует пятый, шестой, седьмой и восьмой элементы массива. Просматривая выходные данные, обратите внимание на то, что при третьем обращении функция присваивает переменной аrr адрес, который отличается от адреса, указанного в первых двух обращениях. Еще следует добавить, что в качестве аргумента вместо cookies + 4 можно использовать &cookies[4], поскольку оба эти выражения эквивалентны.

ПОМНИТЕЧтобы указать вид массива и количество содержащихся в нем элементов функции, которая выполняет его

обработку, передавайте ей эти сведения в виде двух отдельных аргументов:

void fillArray(int arr[], int size); // прототип

He пытайтесь использовать квадратные скобки, чтобы передать сведения о размере массива:

void fillArray(int arr[size]); //недопустимая запись прототипа

Программа 6_6//передача массивов в качестве параметров#include "stdafx.h"#include <stdio.h>#include <ctype.h>

void print_upper(char *string);

76

76

Page 77: C++ теория

Теория и практика С++

void main(){

char s[80];gets(s);print_upper(s);printf("\nUpper string: %s", s);

}

/* Выводит строку, состоящую из прописных букв. */void print_upper(char *string){

register int t;

for(t=0; string[t]; ++t){

string[t] = toupper(string[t]);putchar(string[t]);

}}

Результат:this is a sampleTHIS IS A SAMPLEUpper string: THIS IS A SAMPLE

После вызова функции print_upper() буквы, хранящиеся в массиве s, заменяются прописными, если вы хотите изменять содержимое массива s, перепишите программу следующим образом.

Программа 6_7//передача массивов в качестве параметров#include "stdafx.h"#include <stdio.h>#include <ctype.h>

void print_upper(char *string);

void main(){

char s[80];gets(s);print_upper(s);printf("\nString not changed: %s", s);

}

void print_upper(char *string){

register int t;

for(t=0; string[t]; ++t)putchar(toupper(string[t]));

}

Результат:this is a sampleTHIS IS A SAMPLEString not changed: this is a sample

В этой версии программы содержимое массива s остается постоянным, поскольку его значение в функции print_upper() не изменяется.

Стандартная библиотечная функция gets() представляет собой классический пример передачи массива в качестве параметра. Рассмотрим ее аналог xgets(), хотя реальная функция gets() намного сложнее.

Программа 6_8

77

77

Page 78: C++ теория

Теория и практика С++

//передача массивов в качестве параметров#include "stdafx.h"#include <stdio.h>

char* xgets(char *s);

void main(){

char s[80];char *d;d = xgets(s);

printf("%s", d);}/* Упрощенный варианта стандартной библиотечной функции gets(). */char *xgets(char *s){

char ch, *p;int t;

p = s;/* Функция xgets() возвращает указатель на строку s */

for(t=0; t<80; ++t){

ch = getchar();switch(ch){

case '\n':s[t] = '\0'; // Признак конца строкиreturn p;

case '\b':if(t>0) t--;break;

default:s[t] = ch;

}}s[79] = '\0';return p;

}

Аргументом функции xgets() должен быть указатель на символьную переменную. Разумеется, в качестве аргумента можно задать имя символьного массива, которое по определению является указателем типа char *. Сначала функция xgets() выполняет цикл for, счетчик которого изменяется с 0 до 79. Это предотвращает ввод с клавиатуры более длиной строки. Если пользователь попытается ввести больше 80 символов, функция выполнит оператор return.

Другие примеры функций, выполняющих обработку массивовРассмотрим простой случай. Предположим, что мы приняли решение использовать массив, чтобы

отслеживать стоимость в долларах своего недвижимого имущества (в случае отсутствия такового остается вообразить, что оно у нас есть). Необходимо принять решение о том, какой тип данных использовать. Естественно, тип double предоставляет большие возможности в смысле диапазона представления величин, чем int или long (а вдруг мы получим крупное наследство), к тому же он обеспечивает достаточно значащих цифр, чтобы представить величину с необходимой точностью. Далее нужно решить, сколько элементов должен содержать массив (при работе с динамическими массивами, созданными с помощью оператора new, принятие этого решения можно отложить, но мы сейчас рассматриваем упрощенный вариант). Допустим, у нас имеется не более трех видов недвижимого имущества, так что можно использовать массив из трех чисел типа double.

Теперь рассмотрим операции, которые могут выполняться над массивом недвижимого имущества. Две основные — запись значений в массив и отображение содержимого массива. Добавим еще одну операцию в этот список: переоценку стоимости недвижимости. Чтобы упростить задачу, предположим, что стоимость всех видов недвижимости увеличивается или уменьшается в одной и той же пропорции. Теперь предусмотрим функцию

78

78

Page 79: C++ теория

Теория и практика С++

для каждой операции и создадим соответствующий программный код. Затем выполним все описанные выше действия.

Заполнение массива

Поскольку функция, в качестве аргумента которой используется имя массива, работает с исходным массивом, а не с его копией, для присвоения значений элементам массива можно применить вызов функции. Одним из аргументов такой функции будет имя массива, который подлежит заполнению. Вторым аргументом будет размер массива. Кроме того, нужно предусмотреть в функции код, обеспечивающий выход из программы прежде, чем массив будет заполнен. Поскольку допускается ввод количества элементов, которое меньше максимального, целесообразно предусмотреть в функции возврат фактического числа введенных значений. Исходя из этого, получим следующий прототип функции:

int fill_array(double ar[], int limit);

Рассматриваемая функция принимает аргумент в виде имени массива и аргумент, определяющий максимальное число подлежащих считыванию элементов, после чего возвращает фактическое число считанных элементов. Например, если эта функция применяется при работе с массивом, состоящим из пяти элементов, то ей следует передать в качестве второго аргумента число 5. Если вы ввели в массив только три значения, функция возвратит число 3.

Чтобы записать в массив следующие один за другим значения, можно воспользоваться циклом, однако в нем следует предусмотреть досрочное прерывание его выполнения. Для этого можно использовать специальное значение, обозначающее конец ввода. Поскольку ни один из видов собственности не может иметь отрицательную цену, для указания завершения ввода можно использовать какое-либо отрицательное число. Кроме того, функция должна реагировать на ввод недопустимых значений, например, прекращать дальнейший ввод данных. Таким образом, можно представить функцию в виде следующего программного кода:

int fill_array(double ar[], int limit){ double temp; int i; for (i = 0; i < limit; i++) { cout << "Введите значение #" << (i + 1) << ": "; cin >> temp; if (!cin) { cin.clear(); while (cin.get() != '\n') continue; cout << "Неправильный ввод; процесс вода пркращен. \n"; break; } else if (temp < 0) break; ar[i] = temp; } return i;}

Обратите внимание на то, что программный код выводит для пользователя подсказку (запрос ввода данных). Если пользователь вводит неотрицательное значение, оно присваивается очередному элементу массива. В случае ввода отрицательного значения выполнение цикла прекращается. Если пользователь вводит только допустимые значения, выполнение цикла заканчивается после считывания значения с номером limit. Последней в цикле выполняется операция увеличения i на единицу. Когда выполнение цикла закончится, i будет на 1 больше последнего индекса массива, т.е. равно числу введенных элементов, и функция возвратит это значение вызывающей программе.

Отображение массива и его защита с помощью спецификатора const

Создание функции, отображающей содержимое массива на экране, не сопряжено с трудностями. Вы передаете функции имя массива и количество вводимых элементов, а функция, в свою очередь, использует

79

79

Page 80: C++ теория

Теория и практика С++

цикл, чтобы отобразить каждый элемент. Но при этом необходимо соблюсти еще одно условие — отображающая функция не должна вносить изменения в исходный массив. Если в задачи функции не входит изменение передаваемых ей данных, нужно гарантировать, что она это не будет делать. В случае передачи обычных аргументов такая защита обеспечивается автоматически, поскольку среда C++ передает их по значению, а функция работает с копиями данных. Но функция, которая использует массивы, работает с оригиналами этих массивов — именно благодаря этому функция fill_array() способна выполнить свою задачу. Чтобы предотвратить случайное изменение в функции аргумента-массива, можно при объявлении формального аргумента воспользоваться ключевым словом const;

void show_array(const double ar[], int n);

Изменение элементов массива

Третья операция выполняемая над нашим массивом, - это операция умножения каждого элемента массива на один и тот же коэффициент переоценки. Нам необходимо передать функции три аргумента — коэффициент, массив и число элементов. При этом возвращаемые значения не нужны, так что функция примет вид:

void revalue(double r, double ar[], int n){

for(int i = 0; i < n; i++)ar[i] *= r;

}

Поскольку эта функция предназначена для изменения значений элементов массива, при объявлении массива аr не требуется использовать спецификатор const.

Программа 6_9// функция массивов и const#include "stdafx.h"#include <iostream>using namespace std;

const int Max = 5;// прототипы функцийint fill_array(double ar[], int limit);void show_array(const double ar[], int n); // не изменяет данныхvoid revalue(double r, double ar[], int n);

int main(){

setlocale(LC_CTYPE, "RUS");

double properties[Max];int size = fill_array(properties, Max);show_array(properties, size);cout << "Введите коэффициент переоценки: ";double factor;cin >> factor;revalue(factor, properties, size);show_array(properties, size);cout << "Готово.\n";return 0;

}

int fill_array(double ar[], int limit){

double temp;int i;for (i = 0; i < limit; i++){

cout << "Введите значение #" << (i + 1) << ": ";cin >> temp;if (!cin) // неправильный ввод{

80

80

Page 81: C++ теория

Теория и практика С++

cin.clear();while (cin.get() != '\n')continue;cout << "Неправильный ввод; процесс ввода прекращен.\n";break;

}else if (temp < 0) // сигнал завершения

break;ar[i] = temp;

}return i;

}

// следующая функция может использовать,// но не изменять, массив по адресу arvoid show_array(const double ar[], int n){

for (int i = 0; i < n; i++){

cout << "Элемент #" << (i + 1) << ": $";cout << ar[i] << endl;

}}

void revalue(double r, double ar[], int n){

for(int i=0; i<n; i++)ar[i] *= r;

}

Функции, в которых используются диапазоны массивовИтак, для функций обрабатывающих массивы, необходима информация о типе данных массива, адресе

начала массива и количестве элементов в нем. Традиционный для языка С++ подход заключается в передаче указателя на начало массива (первый аргумент) и размере массива (второй аргумент) (указатель сообщает функции местонахождение массива и тип его данных).

Существует еще один метод предоставления функции необходимой информации – указание диапазона элементов. Это осуществляется путем передачи двух указателей: один идентифицирует начало массива, а второй – конец.

В стандартной библиотеке шаблонов C++, например, в основном реализовано использование диапазона "ячейка, следующая за последним элементом". Другими словами, в случае массива аргумент, указывающий его конец, представляет собой указатель на ячейку памяти, следующую за последним элементом. Для примера рассмотрим следующее объявление:

double elboud[20];

В данном случае два указателя — elboud и elboud + 20 — определяют диапазон. Во-первых, слово elboud, как имя массива, указывает на его первый элемент. Выражение elboud + 19 указывает на последний элемент (т.е. elboud[19]), поэтому выражение elboud[20] указывает на ячейку, следующую за концом массива. Аргументы, обозначающие диапазон, указывают функции, какие элементы следует обрабатывать. Листинг 6.10 содержит вариант программы, в котором для задания диапазона используются два указателя.

Программа 6.10// функция с диапазоном массива#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 8;int sum_arr(const int *begin, const int *end);

81

81

Page 82: C++ теория

Теория и практика С++

int main(){

setlocale(LC_CTYPE, "RUS");

int cookies[ArSize] = {1,2,4,8,16,32,64,128};// некоторые системы требуют предварить int словом static,// чтобы разрешить инициализацию массиваint sum = sum_arr(cookies, cookies + ArSize);cout << "Всего съедено печенья: " << sum << endl;sum = sum_arr(cookies, cookies + 3); // первые 3 элементcout << "Первые три едока умяли " << sum << " печений. \n";sum = sum_arr(cookies + 4, cookies + 8); // последние 4 элементаcout << "Последние пять едоков схрумкали " << sum << " печений. \n";return 0;

}// возвращает сумму элементов целочисленного массиваint sum_arr(const int * begin, const int * end){

const int *pt;int total = 0;for (pt = begin; pt != end; pt++)

total = total + *pt;return total;

}

Результат:Всего съедено печенья: 255Первые три едока умяли 7 печений.Последние пять едоков схрумкали 240 печений.

Указатели и спецификатор const

Использование спецификатора const с указателями связано с некоторыми тонкостями. При работе с указателями ключевое слово const можно применять двумя различными способами. Первый способ предусматривает ссылку указателя на постоянный объект, что предотвращает возможность использования указателя для изменения величины, на которую он указывает. Второй способ состоит в том, чтобы сам указатель сделать константой, и таким образом воспрепятствовать попыткам вносить изменения там, куда указывает указатель. Рассмотрим это более подробно.

Сначала объявим указатель pt, который указывает на константу:

int age =39;const int *pt = &age;

Такое объявление утверждает, что pt указывает на данные типа const int (в рассматриваемом случае — 39), и, следовательно, невозможно использовать pt с целью изменения этого значения. Другими словами, значением *pt является константа (const), изменение которой не допускается:

*pt += 1; // НЕПРАВИЛЬНО, поскольку pt указывает на тип const intcin >> *pt; // НЕПРАВИЛЬНО по той же причине

В чем же здесь заключается тонкий момент? При таком объявлении указателя pt величина, на которую он указывает, не обязательно является константой, это константа лишь "с точки зрения" указателя pt. Например, pt указывает на значение age, однако тип age - не const. Значение age можно изменить непосредственно, используя для этой цели переменную age. Однако косвенное изменение посредством указателя pt невозможно:

*pt = 20; // НЕПРАВИЛЬНО, поскольку pt указывает на const int age = 20; // ПРАВИЛЬНО, поскольку age не объявлена как const

Если раньше мы присваивали адрес обычной переменной обычному указателю, то сейчас адрес обычной переменной присваивается указателю-на-const. При этом остается еще два варианта: присваивание адреса переменной типа const указателю-на-const и присваивание адреса const обычному указателю. Можно ли использовать эти варианты? Первый — да, а второй — нет:

82

82

Page 83: C++ теория

Теория и практика С++

const float g_earth = 9.80;const float *ре = &g_earth; // ПРАВИЛЬНОconst float g_moon = 1.63;float *pm = &g_moon; // НЕПРАВИЛЬНО

В первом случае вы не можете воспользоваться ни константой g_earth, ни константой ре, чтобы изменить значение 9.80. В языке C++ второй вариант не допускается по одной простой причине — присвоив pm адрес g_moon, уже нетрудно обмануть систему и использовать pm для изменения значения g_moon. Таким образом, спецификатор const величины g_moon превращается в фикцию, поэтому в C++ запрещено присваивать адрес величины со статусом const указателю, не имеющему статуса const.

В случае использования указателей на указатели ситуация несколько усложняется.

int age = 39; // age++ - допустима* операцияint *pd = &age; // *pd = 39 — допустимая операцияconst int *pt = pd; // *pt = 39 - недопустимая операция

Однако когда используется два уровня косвенной адресации, смешанное применение указателей-констант и указателей, не указывающих на константы, небезопасно для данных. Если бы подобные операции в среде C++ допускались, были бы возможны следующие действия:

const int **pp2;int *p1;const int n = 13;pp2 = &p1; // операция недопустима, но предположим, что допустима*рр2 = &n; // допустимо, оба значения константы, но p1 теперь указывает на n*p1 = 10; // допустимо, но изменяет значение константы п

Здесь адрес, не являющийся константой (*p1), присваивается указателю-константе (рр2), что позволяет использовать указатель p1 для изменения значения константы. Поэтому правило, допускающее присвоение адреса без спецификатора const указателю со спецификатором const, действует только при условии одного уровня косвенной адресации. Примером может служить применение указателя, который ссылается на базовый тип данных.

Предположим, у вас есть массив константных данных:

const int months[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

Запрет на присваивание адреса константного массива означает, что вы не можете передать имя такого массива в качестве аргумента функции, используя не константный формальный аргумент:

int sum (int arr[], int n) ; // должен быть const int arr[]...int j = sum(months, 12); // не допускается

Этот вызов функции пытается присвоить const-указатель (months) не константному указателю (агг), и компилятор не пропускает такой вызов.

Касательно еще одного тонкого момента рассмотрим следующее объявление:

int age = 39;const int *pt = &age;

const во втором объявлении только предотвращает изменение того значения, на которое указывает pt, в данном случае 39. Это не предотвращает изменение самого pt. То есть вы вполне можете присвоить ему другой адрес:

int sage = 80;pt = &sage; // может указывать на другое место

Но вы по-прежнему не сможете использовать pt для изменения того значения, на которое он указывает.Второй способ применения const делает невозможным изменение самого указателя:

int sloth = 3;const int *ps = &sloth; // указатель на const intint *const finger = &sloth; // const-указатель на int

83

83

Page 84: C++ теория

Теория и практика С++

Последнее объявление переустанавливает ключевое слово const. finger может указывать только на sloth и ни на что другое. Однако оно позволяет применить finger для изменения значения самого sloth. finger и *ps — являются константами, а *finger и ps — нет.

Можно объявить константный указатель на константный объект:

double trouble = 2.0Е30;const double *const stick = &trouble;

Здесь stick может указывать только на trouble, и stick не может применяться для изменения значения trouble. И stick, и *stick являются const.

Функции и строкиСущественное различие между строкой и обычным массивом заключается в том, что в строке имеется

встроенный символ завершения. Отсюда следует, что длину строки как аргумент передавать необязательно. Вместо этого в функции можно применить цикл, чтобы поочередно проанализировать каждый символ строки, пока цикл не достигнет завершающего null-символа.

Программа 6_7Программа подсчитывает, сколько раз определенный символ появляется в заданной строке.// программа p6_7.cpp - функция со строковым аргументом#include "stdafx.h"#include <iostream>using namespace std;

int c_in_str(const char *str, char ch);

void main(){

setlocale(LC_CTYPE, "RUS");

char mmm[15] = "minimum"; //строка в массиве char *wail = "ululate"; //wail указывает на строку int ms = c_in_str(mmm, 'm'); int us = c_in_str(wail, 'u'); cout << ms << " m символов в " << mmm << "\n"; cout << us << " u символов в " << wail << "\n";}

//эта функция подсчитывает число символов ch в строке strint c_in_str(const char *str, char ch){ int count = 0; while (*str) //выйти из программы, когда *str будет равен '\0' { if (*str == ch) count++; str++; //переместить указатель на следующий символ - char } return count;}

Результат:3 m символов в minimum2 u символов в ululate

Функции, возвращающие строкиТеперь предположим, что вы хотите создать функцию, которая возвращает строку. Правда, функция это

сделать неспособна, зато (что даже лучше) она может возвратить адрес строки.

Программа 6_8

84

84

Page 85: C++ теория

Теория и практика С++

В программе объявляется функция buildstr(), которая возвращает указатель. Эта функция принимает два аргумента: символ и число. Используя оператор new, функция создает строку, длина которой равна принятому числу, после чего она инициализирует каждый элемент, присваивая ему символьное значение. Затем функция возвращает указатель на новую строку.// программа p6_8.cpp - функция возвращает указатель на символ#include "stdafx.h"#include <iostream>using namespace std;

char *buildstr(char c, int n); //прототип

void main(){

setlocale(LC_CTYPE, "RUS");

int times;char ch;cout << "Введите символ: ";cin >> ch;cout << "Введите число: ";cin >> times;char *ps = buildstr(ch, times);cout << ps << endl;delete [] ps; //высвобождение памятиps = buildstr('+', 20); //повторное использование указателяcout << ps << "-DONE-" << ps << endl;delete [] ps; //высвобождение памяти

}

//создает строку, состоящую из n символов cchar *buildstr(char c, int n){ char *pstr = new char[n + 1]; pstr[n] = '\0'; //завершить строку while (n-- > 0) pstr[n] = c; //заполнить остальную часть строки return pstr;}

Результат:Введите символ: VВведите число: 46VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV++++++++++++++++++++-DONE-++++++++++++++++++++

Функции и структурыСтруктуры ведут себя по отношению к функциям как переменные базовых типов данных, принимающих

только одно значение. Структуры можно передавать по значению точно так же, как и обычные переменные: в этом случае функция работает с копией исходной структуры. Кроме того, функция может возвращать структуру.

Программа 6_9Программа суммирует два временных интервала 1 час 25 минут и 3 часа 50 минут. // программа p6_9.cpp - использование структур в функциях#include "stdafx.h"#include <iostream>using namespace std;

struct travel_time{

int hours;int mins;

};

const int Mins_per_hr = 60;

85

85

Page 86: C++ теория

Теория и практика С++

travel_time sum(travel_time t1, travel_time t2);void show_time(travel_time t);

void main(){

setlocale(LC_CTYPE, "RUS");

travel_time day1 = {5, 45}; //5 часов 45 минутtravel_time day2 = {4, 55}; //4 часа 55 минутtravel_time trip = sum(day1, day2);cout << "Всего за два дня: ";show_time(trip);travel_time day3 = {4, 32};cout << "Всего за три дня: ";show_time(sum(trip, day3));

}//---------------------------------------------------------------------------travel_time sum(travel_time t1, travel_time t2){

travel_time total;total.mins = (t1.mins + t2.mins) % Mins_per_hr;total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;return total;

}//---------------------------------------------------------------------------void show_time(travel_time t){ cout << t.hours << " часов, " << t.mins << " минут\n";}

Результат:Всего за два дня: 10 часов, 40 минутВсего за три дня: 15 часов, 12 минут

86

86

Page 87: C++ теория

Теория и практика С++

Функции и объекты класса stringХотя строки в стиле С и класс string служат в основном одним и тем же целям, класс string больше похож

на структуру, чем на массив. Например, вы можете присвоить структуру другой структуре и объект — другому объекту. Вы можете передать структуру как единое целое функции, и точно так же можете передать объект. Если вам требуется несколько строк, вы можете объявить одномерный массив объектов string вместо двумерного массива char.

Листинг 6_10_1 представляет короткий пример, который объявляет массив объектов string и передает его функции, отображающей их содержимое.

Программа 6_10_1//обработка массива объектов string#include "stdafx.h"#include <iostream>using namespace std;#include <string>

const int SIZE = 5;void display(const string sa[], int n);

void main(){

setlocale(LC_CTYPE, "RUS");

string list[SIZE]; // массив из 5 объектов stringcout << "Введите " << SIZE << " ваших любимых астрономических объектов:\n";for (int i = 0; i < SIZE; i++){

cout << i + 1 << ": ";getline(cin, list[i]);

}cout << "Ваш список: \n";display(list, SIZE);

}

void display(const string sa[], int n){

for (int i = 0; i < n; i++)cout << i + 1 << ": " << sa[i] << endl;

}РезультатВведите 5 ваших любимых астрономических объектов:1: Moon2: Sun3: Saturn4: Orion5: JupiterВаш список:1: Moon2: Sun3: Saturn4: Orion5: Jupiter

РекурсияФункция может вызывать саму себя. Такое свойство называется рекурсией.Когда рекурсивная функция вызывает саму себя, только что вызванная функция также вызывает саму себя и

т.д. до бесконечности, если, конечно, ее программный код не содержит ничего, что способно прервать эту последовательность вызовов. Обычно рекурсивный вызов реализуют как часть оператора if. Например:

void recurs(список_аргументов){

операторы1

87

87

Page 88: C++ теория

Теория и практика С++

if (проверка)recurs(аргументы)

операторы2}

Рекурсивные вызовы порождают интересную цепь событий. Пока проверяемое выражение оператора if остается истинным, в результате каждого вызова функции recurs() выполняется блок операторы1, а затем производится очередной вызов функции recurs(), при этом блок операторы2 остается вне досягаемости. Когда проверяемое выражение примет значение false, начинает выполняться блок операторы2. Далее, по завершении выполнения текущего блока, управление программой передается к предыдущей версии функции recurs(), которая его вызвала. Затем эта версия функции recurs() заканчивает выполнение своего блока операторы2 и завершается сама, возвращая управление предыдущему вызову функции, и т.д. Таким образом, если функция recurs() подвергнется пяти рекурсивным вызовам, первый блок операторов операторы1 будет выполнен пять раз в том порядке, в каком эти функции были вызваны, затем блок операторы2 будет выполнен пять раз в порядке, обратном последовательности вызова функций.

Программа 6_10// программа p6_10.cpp - применение рекурсии#include "stdafx.h"#include <iostream>using namespace std;void countdown(int n);

void main(){ countdown(4); //вызов рекурсивной функции}//---------------------------------------------------------------------------void countdown(int n){ cout << "Counting down ... " << n << "\n"; if (n > 0) countdown(n - 1); //функция вызывает саму себя cout << n << ": Kaboom!\n";}

Результат:Counting down ... 4Counting down ... 3Counting down ... 2Counting down ... 1Counting down ... 00: Kaboom!1: Kaboom!2: Kaboom!3: Kaboom!4: Kaboom!

Указатели на функцииФункции, как и элементы данных, имеют адреса. Адрес функции — это адрес в памяти, где находится начало

кода функции на машинном языке. Назначение: передачи разных адресов функций в разные моменты времени. То есть первая функция может вызывать разные функции в разное время.

Основы указателей на функцииЧтобы реализовать этот план, понадобится сделать следующее:

Получить адрес функции. Объявить указатель на функцию. Использовать указатель на функцию для ее вызова.

Получение адреса функции

88

88

Page 89: C++ теория

Теория и практика С++

Получить адрес функции очень просто: вы просто используете имя функции без скобок. То есть, если имеется функция think(), то ее адрес записывается как think. Чтобы передать функцию в качестве аргумента, вы просто передаете ее имя.

process(think); // передача адреса think() функции process()thought(think()); //передача возвращаемого значения think() функции thought()

Объявление указателя на функциюУказатель на функцию должен специфицировать, на функцию какого типа он будет указывать. Это значит,

что объявление должно идентифицировать тип возврата функции и ее сигнатуру (список аргументов). То есть объявление должно предоставлять ту же информацию о функции, которую предоставляет и ее прототип.

double pam(int); // прототип

Вот как должно выглядеть объявление соответствующего типа указателя:

double (*pf)(int); // pf указывает на функцию, которая принимает// один аргумент типа int и возвращает тип double

Обратите внимание, что это выглядит подобно объявлению раm(), но роль pam играет (*pf). Поскольку раm — функция, то же самое представляет собой (*pf). И если (*pf) — функция, то pf — указатель на нее.

Объявление требует скобок вокруг *pf, чтобы обеспечить правильный приоритет операций. Скобки имеют более высокий приоритет, чем операция *, поэтому *pf (int) означает, что pf() — функция, которая возвращает указатель, в то время как (*pf) (int) означает, что pf — указатель на функцию:

double (*pf)(int); // pf указывает на функцию, возвращающую doubledouble *pf(int); // pf() - функция, возвращающая указатель на double

После правильного объявления pf вы можете присваивать ему адрес подходящей функции:

double pam(int);double (*pf)(int);pf = pam; // pf теперь указывает на функцию pam()

Заметьте, что раm() должна соответствовать pf как по типу возврата, так и по сигнатуре. Компилятор отвергнет несоответствующие присваивания:

double ned(double);int ted(int);double (*pf)(int);pf = ned; // неверно — несоответствие сигнатурыpf = ted; // неверно — несоответствие типа возврата

Вся сложность использования указателей на функцию заключается в написании прототипов, в то время как передать адрес очень просто.

Использование указателя для вызова функцииТеперь как использовать указатель для вызова указанной им функции. Ключ к этому — в объявлении

указателя. Вспомним, что там (*pf) играет ту же роль, что имя функции. Поэтому все, что вы должны сделать — использовать (*pf), как если бы это было имя функции:

double pam(int);double (*pf)(int);pf = pam; // pf теперь указывает на функцию pam()double x = pam(4); // вызвать pam(), используя ее имяdouble у = (*pf)(5); // вызвать pam(), используя указатель pf

В действительности C++ позволяет вам использовать pf, как если бы это было имя функции:

double у = pf(5); // также вызывает раm(), используя указатель pf

89

89

Page 90: C++ теория

Теория и практика С++

Первая форма вызова более неуклюжа, чем эта, но она напоминает о том, что код использует указатель на функцию.

Пример с указателем на функциюВ этом примере вызывается функция estimate() дважды — один раз передавая ей адрес функции

betsy(), а второй — адрес функции раm().

#include "stdafx.h"#include <iostream>using namespace std;

double betsy(int);double pam(int);// второй аргумент - указатель на функцию double, // которая принимает аргумент типа intvoid estimate(int lines, double (*pf)(int));

void main(){

setlocale(LC_CTYPE, "RUS");

int code;cout << "Сколько строк кода нужно написать? ";cin >> code;cout << "Вот оценка Бетси: \n";estimate(code, betsy);cout << "Вот оценка Пэм: \n";estimate(code, pam);

}

double betsy(int lns){

return 0.05 * lns;}

double pam(int lns){

return 0.03 * lns + 0.0004 * lns * lns;}

void estimate(int lines, double (*pf)(int)){

cout << lines << " строк потребует ";cout << (*pf)(lines) << " часов\n";

}

Упражнения по программированию1. Напишите программу, которая многократно приглашает пользователя вводить пары чисел до тех пор, пока

хотя бы в одной паре числа не будут равны 0. С каждой парой программа должна использовать функцию для вычисления среднего гармонического этих чисел. Функция должна возвращать ответ main() для отображения результата. Среднее гармоническое чисел — это инверсия среднего значения их инверсий; она вычисляется следующим образом:

harmonic mean =2.0 * х * у / (х + у)

#include "stdafx.h"#include <iostream>using namespace std;

double sr(int, int);

void main(){

90

90

Page 91: C++ теория

Теория и практика С++

setlocale(LC_CTYPE, "RUS");

int x, y;cout << "Введите пару чисел: ";cin >> x >> y;while (x != 0 || y != 0){

cout << "Среднее гармоническое равно = " << sr(x, y) << endl << endl;cout << "Введите пару чисел: ";cin >> x >> y;

}cout << endl << "Конец программы" << endl;

}

double sr(int a, int b){

return 2.0 * a * b / (a + b);}

2. Напишите программу, которая запрашивает у пользователя 5 результатов игры в гольф, сохраняя их в массиве. При этом необходимо обеспечить возможность прерывания ввода до ввода всех 5 результатов. Программа должна отобразить все результаты в одной строке и сообщить среднее их значение. Реализуйте ввод, отображение и вычисление среднего в трех отдельных функциях, работающих с массивами.

#include "stdafx.h"#include <iostream>using namespace std;

const int Max = 5;

int fill_array(double ar[], int limit);void show_array(const double ar[], int n, double aver); // не изменяет данныхdouble revalue(double ar[], int n);

void main(){

setlocale(LC_CTYPE, "RUS");

double golf[Max]; // результаты игры в гольфdouble av; // среднееint size = fill_array(golf, Max);av = revalue(golf, size);show_array(golf, size, av);

cout << "Готово.\n";}

int fill_array(double ar[], int limit){ double temp; int i; for (i = 0; i < limit; i++) { cout << "Введите значение #" << (i + 1) << ": "; cin >> temp; if (!cin) { cin.clear(); while (cin.get() != '\n') continue; cout << "Неправильный ввод; процесс вода пркращен. \n"; break; } else if (temp < 0)

91

91

Page 92: C++ теория

Теория и практика С++

break; ar[i] = temp; } return i;}

// следующая функция может использовать,// но не изменять, массив по адресу arvoid show_array(const double ar[], int n, double aver){

for (int i = 0; i < n; i++)cout << ar[i] << ' ';

cout << " Среднее = " << aver << endl;}

// вычисление среднегоdouble revalue(double ar[], int n){

double sr = 0.0;for(int i=0; i<n; i++)

sr += ar[i];return sr;

}

3. Пусть имеется следующее объявление структуры:

struct box{

char maker[40];float height;float width;float length;float volume;

};

a) Напишите функцию, принимающую структуру box по значению и отображающую все ее члены.b) Напишите функцию, принимающую адрес структуры box и устанавливающую значение члена volume

равным произведению остальных трех членов.c) Напишите простую программу, использующую эти две функции.

#include "stdafx.h"#include <iostream>using namespace std;

struct box{

char maker[40];float height;float width;float length;float volume;

};

void show(box);void mul(box *);

void main(){

setlocale(LC_CTYPE, "RUS");

box x = {"one", 10.0, 12.0, 14.0, 16.0};show(x);mul(&x);show(x);

}

92

92

Page 93: C++ теория

Теория и практика С++

void show(box a){

cout << a.maker << endl;cout << a.height << endl;cout << a.width << endl;cout << a.length << endl;cout << a.volume << endl;

}

void mul(box *m){

m->volume = m->height * m->length * m->width;}

4. Многие лотереи штатов организованы подобно той, что смоделирована в листинге ниже.

// вероятность выигрыша#include "stdafx.h"#include <iostream>using namespace std;

long double probability(unsigned numbers, unsigned picks);

void main(){

setlocale(LC_CTYPE, "RUS");

double total, choices;cout << "Укажите общее количество номеров и\n""количество номеров, которые нужно угадать:\n";while ((cin >> total >> choices) && choices <= total){

cout << "У вас один шанс из ";cout << probability(total, choices); // вычисление числа вариантовcout << " чтобы выиграть.\n";cout << "Следующие два числа (q для выхода): ";

}cout << "Удачи!\n";

}//---------------------------------------------------------------------------// следующая функция вычисляет вероятность правильного// угадывания picks чисел из numbers возможныхlong double probability(unsigned numbers, unsigned picks){

long double result = 1.0; // здесь - локальные переменныеlong double n;unsigned p;for (n = numbers, p = picks; p > 0; n--, p--)

result = result * n / p ;return result;

}

Во всех их вариациях вы должны выбрать несколько чисел из одно го набора, называемого полем номеров. Вы также указываете один номер (называемый меганомером, или супершаром) из второго диапазона, такого как 1-27. Чтобы выиграть главный приз, вы должны правильно угадать все номера. Шанс выиграть вычисляется как вероятность угадывания всех номеров в поле, умноженная на вероятность угадывания меганомера. Например, вероятность выигрыша в описанном здесь примере вычисляется как вероятность угадывания 5 номеров из 47, умноженная на вероятность угадывания одного номера из 27. Модифицируйте листинг для вычисления вероятности выигрыша в такой лотерее.

// вероятность выигрыша#include "stdafx.h"

93

93

Page 94: C++ теория

Теория и практика С++

#include <iostream>using namespace std;

long double probability(unsigned numbers, unsigned picks);

void main(){

setlocale(LC_CTYPE, "RUS");

double total, choices;long double lot, megalot, itogo;cout << "Укажите общее количество номеров и\n""количество номеров, которые нужно угадать:\n";while ((cin >> total >> choices) && choices <= total){

lot = probability(total, choices); // вычисление числа вариантов

cout << "Укажите общее количество номеров и\n" "и меганомер, который необходимо угадать\n";

cin >> total >> choices; megalot = probability(total, choices); itogo = lot * megalot;

cout << "У вас один шанс из ";cout << itogo;cout << " чтобы выиграть.\n";cout << "Следующие два числа (q для выхода): ";

}cout << "Удачи!\n";

}//---------------------------------------------------------------------------// следующая функция вычисляет вероятность правильного// угадывания picks чисел из numbers возможныхlong double probability(unsigned numbers, unsigned picks){

long double result = 1.0; // здесь - локальные переменныеlong double n;unsigned p;for (n = numbers, p = picks; p > 0; n--, p--)

result = result * n / p ;return result;

}

5. Определите рекурсивную функцию, принимающую целый аргумент и возвращающую его факториал. Напомним, что факториал 3 записывается, как 3! и вычисляется как 3x2! и так далее, причем 0! равно 1. Вообще, если п больше нуля, то n! = n * (n-1)!. Протестируйте функцию в программе, использующей цикл, в котором пользователь может вводить различные значения, для которых программа вычисляет и отображает факториалы.

// еще о циклах for#include "stdafx.h"#include <iostream>using namespace std;

int factorial(int, int, int);

const int ArSize = 16; // пример внешнего объявления

void main(){

setlocale(LC_CTYPE, "RUS");

double factorials[ArSize];factorials[1] = factorials[0] = 1.0;for (int i = 2; i < ArSize; i++)

94

94

Page 95: C++ теория

Теория и практика С++

factorials[i] = i * factorials[i-1];for (int i = 0; i < ArSize; i++)

cout << i << "! = " << factorials[i] << endl;}

int factorial(int n, int i, int fact){

if(i < n){

fact = i * fact;i++;factorial(n, i, fact);

}

6. Напишите программу, использующую следующие функции: Fill_array() принимает в качестве аргумента имя массива элементов типа double и размер этого массива. Она приглашает пользователя ввести значения double для помещения их в массив. Ввод прекращается при наполнении массива либо когда пользователь вводит нечисловое значение, и возвращает действительное количество элементов. Show_array() принимает в качестве аргументов имя массива значений double и его размер, и отображает содержимое массива. Reverse_array() принимает в качестве аргумента имя массива значений double и его размер, и изменяет порядок его элементов на противоположный. Программа должна использовать эти функции для наполнения массива, обращения порядка его элементов, кроме первого и последнего, с последующим отображением.

#include "stdafx.h"#include <iostream>using namespace std;

const int Max = 5;

int fill_array(double[], int);void show_array(double[], int);void reverse_array(double[], int);

void main(){

setlocale(LC_CTYPE, "RUS");

double massiv[Max]; // результаты игры в гольфint size = fill_array(massiv, Max);reverse_array(massiv, size);show_array(massiv, size);

cout << "Готово.\n";

}

int fill_array(double ar[], int limit){ double temp; int i; for (i = 0; i < limit; i++) { cout << "Введите значение #" << (i + 1) << ": "; cin >> temp; if (!cin) { cin.clear(); while (cin.get() != '\n') continue; cout << "Неправильный ввод; процесс вода прекращен. \n"; break; } ar[i] = temp;

95

95

Page 96: C++ теория

Теория и практика С++

} return i;}

void show_array(double ar[], int limit){

for(int i=0; i<limit; i++)cout << ar[i] << endl;

}

void reverse_array(double ar[], int limit){

int x1 = 1;int x2 = limit - 2;double temp;for(; x1<x2; x1++, x2--){

temp = ar[x2];ar[x2] = ar[x1];ar[x1] = temp;

}}

8. Работа с функциямиВстроенные функции

Встроенные (inline) функции — предназначенны для повышения быстродействия программ. Основное различие между обычными и встроенными функциями заключается не к программном коде, а в том, как компилятор встраивает их в программу.

Конечный продукт процесса компиляции — это исполняемая программа, которая состоит из набора команд на машинном языке. Когда мы запускаем программу, операционная система загружает эти команды в оперативную память компьютера. Поэтому каждая команда имеет свой собственный адрес в памяти. Затем компьютер последовательно выполняет эти команды. Иногда в программе встречается цикл или условные переходы. Такое перепрыгивание с места на место в коде и запоминание соответствующих адресов при использовании функций приводит к дополнительным затратам времени.

Описанному выше процессу существует альтернатива — встроенные функции C++. Скомпилированный код такой функции встраивается в код программы; другими словами, компилятор подставляет вместо вызова функции ее код. В результате программе не нужно выполнять переход и возвращаться назад.

Программа 8_1#include "stdafx.h"#include <iostream>using namespace std;

inline double square(double x) { return x * x;}

int main(void){

double a, b;double c = 13.0;a = square(5.0);b = square(4.5 + 7.5); // можно передавать выраженияcout << "a = " << a << ", b = " << b << "\n";cout << "c = " << c;cout << ", c squared = " << square(c++) << "\n";cout << "Now c = " << c << "\n";

return 0;}

96

96

Page 97: C++ теория

Теория и практика С++

Ссылочные переменные

В языке C++ вводится новый составной тип данных — ссылочная переменная. Ссылка представляет собой имя, которое является альтернативным или псевдонимом для ранее объявленной переменной. Например, сделав twain ссылкой на переменную clemens, можно взаимозаменяемо использовать эти имена для представления данной переменной.

Создание ссылочных переменных

Напомним, что в языках С и C++ символ & используется для обозначения адреса переменной. Однако в C++ символу & придается дополнительный смысл; он также задействуется для объявления ссылок. Например, чтобы rodents стало альтернативный именем для переменной rats, необходимы следующие операторы:

int rats;int &rodents = rats; // rodents становился псевдонимом имени rats

Объявление ссылки позволяет взаимозаменяемо использовать идентификаторы rats и rodents для ссылки на одно и то же значение и одну и ту же ячейку памяти.

Программа 8_2#include "stdafx.h"#include <iostream>using namespace std;

int main(void){

setlocale(LC_CTYPE, "RUS");

int rats = 101;int &rodents = rats; // rodents - это ссылкаcout << "rats = " << rats << ", rodents = " << rodents << "\n";rodents++;cout << "rats = " << rats << ", rodents = " << rodents << "\n";cout << "адрес rats = " << &rats << ", адрес rodents = " << &rodents << "\n";

return 0;}Решение : rats = 101, rodents = 101rats = 102, rodents = 102адрес rats = 0012FF60, адрес rodents = 0012FF60

Ссылки очень напоминают указатели, хотя между ними существуют отличия. Например, можно создать как ссылку, так и указатель, чтобы ссылаться на переменную rats:

int rats = 101;int &rodents = rats; // rodents - это ссылкаint *prats = &rats; // prats - это указатель

Затем выражения rodents и *prats могут заменять имя rats, а выражения &rodents и prats могут подменять обозначение &rats. С этой точки зрения ссылка во многом подобна указателю. Однако между ссылками и указателями существуют различия по-мимо обозначений. Одно из таких различий состоит в том, что ссылку необходимо инициализировать в момент ее объявления. Нельзя сначала объявить ссылку, а затем присвоить ей значение, как это делается для указателей:

int rat;int &rodent;rodent = rat; // Это действие недопустимо.

Ссылка во многом аналогична указателю со спецификатором const. Ее следует инициализировать в момент создания, после чего ссылка остается привязанной к определенной переменной до конца программы. Таким образом, конструкция

97

97

Page 98: C++ теория

Теория и практика С++

int &rodents = rats;

по сути, является замаскированной записью выражения, подобного следующему:

int *const pr = &rats;

В данном случае ссылка rodents играет ту же роль, что и выражение *рr.

Программа 8_3#include "stdafx.h"#include <iostream>using namespace std;

void main(void){

setlocale(LC_CTYPE, "RUS");

int rats = 101;int &rodents = rats; // rodents - это ссылкаcout << "rats = " << rats;cout << ", rodents = " << rodents << endl;cout << "адрес rats = " << &rats;cout << ", адрес rodents = " << &rodents << endl;int bunnies = 50;rodents = bunnies; // можно ли изменить ссылку?cout << "bunnies = " << bunnies;cout << ", rats = " << rats;cout << ", rodents = " << rodents << endl;cout << "адрес bunnies = " << &bunnies;cout << ", адрес rodents = " << &rodents << endl;

}Результат : rats = 101, rodents = 101адрес rats = 0012FF60, адрес rodents = 0012FF60bunnies = 50, rats = 50, rodents = 50адрес bunnies = 0012FF48, адрес rodents = 0012FF60

Ссылки в роли параметров функцийЧаще всего ссылки используются в качестве параметров функции, при этом имя переменной в функции

становится псевдонимом переменной из вызывающей программы. Такой метод передачи аргументов называется передачей по ссылке. Передача параметров по ссылке позволяет вызываемой функции получить доступ к переменным в вызывающей функции.

98

98

Page 99: C++ теория

Теория и практика С++

Теперь сравним, как используются ссылки и указатели при решении простой компьютерной задачи: обмен значениями двух переменных. Функция обмена должна иметь возможность изменять значения переменных в вызывающей программе. Это означает, что обычный подход, связанный с передачей переменных по значению, здесь неприемлем, поскольку функция выполнит обмен содержимым лишь копий исходных переменных, но не их самих. Однако если передавать ссылки, функция получит возможность работать с исходными данными. Вместо этого для получения доступа к исходным данным можно передавать указатели. Листинг 8.4 демонстрирует все три метода, включая и тот, который не дает желаемого результата, так что вы можете сравнить их.

Программа 8_4// обмен значениями с помощью ссылок и указателей#include "stdafx.h"#include <iostream>using namespace std;

void swapr(int &a, int &b); // a, b - псевдонимы данных типа intvoid swapp(int *p, int *q); // p, q - адреса данных типа intvoid swapv(int a, int b); // a, b - новые переменные

void main(int argc, char *argv[]){

setlocale(LC_CTYPE, "RUS");

int wallet1 = 300;int wallet2 = 350;cout << "wallet1 = $" << wallet1 << " wallet2 = $" << wallet2 << endl;cout << "Использование ссылок для обмена содержимым:\n";swapr(wallet1, wallet2); // передача переменных

99

99

Page 100: C++ теория

Теория и практика С++

cout << "wallet1 = $" << wallet1 << " wallet2 = $" << wallet2 << endl;cout << "Использование указателей для повторного обмена содержимым:\n";swapp(&wallet1, &wallet2); // передача адресов переменныхcout << "wallet1 = $" << wallet1;cout << " wallet2 = $" << wallet2 << endl;cout << "Попытка использования передачи по значению:\n";swapv(wallet1, wallet2); // передача значений переменныхcout << "wallet1 = $" << wallet1;cout << " wallet2 = $" << wallet2 << endl;

}

void swapr(int & a, int & b) // использование ссылок{

int temp;temp = a; // использование a, b для хранения значений переменныхa = b;b = temp;

}void swapp(int * p, int * q) // использование указателей{

int temp;temp = *p; // использование *p, *q для хранения значений переменных*p = *q;*q = temp;

}void swapv(int a, int b) // попытка использования значений{

int temp;temp = a; // использование a, b для хранения значений переменныхa = b;b = temp;

}Результатwallet1 = $300 wallet2 = $350Использование ссылок для обмена содержимым:wallet1 = $350 wallet2 = $300Использование указателей для повторного обмена содержимым:wallet1 = $300 wallet2 = $350Попытка использования передачи по значению:wallet1 = $300 wallet2 = $350

Как и ожидалось, оба метода — метод указателей и метод ссылок — успешно реализовали обмен содержимым двух бумажников (wallet), в то время как метод передачи по значению завершился неудачей.

Замечания по программеПрежде всего, обратите внимание на то, как вызывается каждая функция:

swapr(wallet1, wallet2); // передача переменныхswapp(&wallet1, &wallet2); // передача адресов переменныхswapv(wallet1, wallet2); // передача значений переменных

Передача аргументов по ссылке (swapr(wallet1, wallet2)) и передача по значению (swapv (wallet1, wallet2)) выглядят идентичными. Единственный способ определить, что функция swapr() передает аргументы по ссылке — это обратиться к прототипу или к описанию функции. В то же время, наличие операции взятия адреса (&) явно говорит о том, что функции передается адрес значения (swapp (&wallet1, &wallet2)). (Напомним, что объявление типа int *р означает, что р — это указатель значения типа int, и поэтому аргумент, соответствующий р, должен быть адресом, таким как, например, &wallet1.)

Далее сравним программный код функций swapr() (передача по ссылке) и swapv() (передача по значению). Единственное видимое различие между ними связано с объявлением параметров:

void swapr (int & a, int & b)void swapv(int a, int b)

100

100

Page 101: C++ теория

Теория и практика С++

Внутреннее различие между ними, естественно, состоит в том, что в функции swapr() переменные а и b служат псевдонимами имен wallet1 и wallet2, так что обмен значениями между а и b вызывает обмен значениями между переменными wallet1 и wallet2. В то же время в функции swapv() переменные а и b — это новые переменные, которые копируют значения переменных wallet1 и wallet2. В этом случае обмен значениями между а и b никак не влияет на переменные wallet1 и wallet2.

И, наконец, сравним функцию swapr() (передача по ссылке) и swapp() (передача указателей). Первое различие кроется в объявлении параметров:

void swapr (int &a, int &b)void swapp (int *p, int *q)

Второе различие состоит в том, что вариант с указателем требует применения операции разыменования (*) во всех случаях, когда функция использует переменные р и q.

Как уже говорилось, ссылочную переменную необходимо инициализировать при ее определении. Можно считать, что аргументы-ссылки функции инициализируются с присваиванием значений, передаваемыми при ее вызове. Таким образом, вызов функции

swapr(walletl, wallet2);

инициализирует формальный параметр а с присваиванием значения wallet1, а формальный параметр b — с присваиванием значения wallet2.

Свойства и особенности ссылокДля использования ссылочных аргументов характерен ряд особенностей, о которых вам следует знать.

Сначала обратимся к листингу 8.5. Представленная здесь программа использует две функции для возведения значения аргумента в куб. Одна из них принимает аргумент типа double, в то время как другая получает ссылку на значение типа double. Программный код процедуры возведения в куб выглядит несколько необычно, дабы нагляднее проиллюстрировать работу со ссылками.

Листинг 8_5// обычные и ссылочные аргументы#include "stdafx.h"#include <iostream>using namespace std;

double cube(double a);double refcube(double &ra);

void main(int argc, char *argv[]){

setlocale(LC_CTYPE, "RUS");

double x = 3.0; cout << cube(x); cout << " = куб числа " << x << endl; cout << refcube(x); cout << " = куб числа " << x << endl;}

double cube(double a){ a *= a * a; return a;}

double refcube(double &ra){ ra *= ra * ra; return ra; }

Результат:27 = куб числа 3

101

101

Page 102: C++ теория

Теория и практика С++

27 = куб числа 27

Временные переменные, ссылочные аргументы и спецификатор constC++ может создавать временную переменную, если передаваемый аргумент не соответствует определению

ссылочного аргумента. Рассмотрим случаи, когда C++ генерирует временную переменную, и выясним, почему целесообразно вводить ограничение, требующее, чтобы ссылка использовалась со спецификатором const.

Прежде всего, в каких случаях создается временная переменная? При условии, что ссылочный параметр имеет спецификатор const, компилятор генерирует временную переменную в двух случаях:

• Когда тип фактического аргумента выбран правильно, но сам параметр не является lvalue (L-значением).• Когда тип фактического параметра выбран неправильно, но может быть преобразован в правильный тип.

Аргумент, являющийся значением lvalue (L-значением), представляет собой объект данных, на который можно ссылаться. Примером может служить переменная, элемент массива, элемент структуры, ссылка и разыменованный указатель — все они являются L-значениями. К L-значениям не относятся литеральные константы и выражения, состоящие из нескольких элементов. Предположим, мы переопределили функцию refcube() так, что у нее имеется аргумент в виде ссылочной константы:

double refcube(const double &ra){

return ra * ra * ra;}

Теперь рассмотрим следующий программный код:

double side = 3.0;double *pd = &side;double &rd = side;long edge = 5L;double lens[4] = { 2.0, 5.0, 10.0, 12.0 };double c1 = refcube (side); // га - это sidedouble c2 = refcube (lens [2]); // га - это lens[2]double сЗ = refcube(rd); // га - это rd и sidedouble c4 = refcube(*pd); // га - это *pd и sidedouble c5 = refcube(edge); // га - временная переменнаяdouble c6 = refcube(7.0); // га - временная переменнаяdouble c7 = refcube(side + 10.0); // га - временная переменная

Аргументы side, lens[2], rd и *pd — именованные объекты данных типа double, что позволяет генерировать ссылки на них. При этом временные переменные не нужны. (Напомним, что элемент массива ведет себя так же, как и переменная того же типа.) Однако объект edge, хоть и является переменной, имеет неверный тип. Ссылка на объект типа double не может ссылаться на данные типа long. Тип аргументов 7.0 и side + 10.0 допустим, но они не являются именованными объектами данных. В каждом из этих случаев компилятор генерирует временную анонимную переменную и превращает rа в ссылку на нее. Такие временные переменные существуют на протяжении обращения к функции, после чего компилятор может удалить их.

Почему такое поведение вполне оправдано для ссылок-констант и недопустимо в других случаях? Вернемся к функции swapr() из листинга 8.4:

void swapp(int * p, int * q) // использование указателей{

int temp;temp = *p; // использование *p, *q для хранения значений переменных*p = *q;*q = temp;

}

Что произойдет, если выполнить представленный ниже программный код в условиях менее жестких правил ранних версий C++?

long a = 3, b = 5;swapr(a, b);

102

102

Page 103: C++ теория

Теория и практика С++

Здесь имеет место несоответствие типов, поэтому компилятор создает две временных переменных типа int, инициализирует их значениями 3 и 5, а затем производит обмен содержимым временных переменных, оставляя при этом значения а и b неизменными.

Одним словом, если назначение функции со ссылочными аргументами состоит в том, чтобы модифицировать переменные, передаваемые в качестве аргументов, ситуация, которую создают временные переменные, препятствуют достижению этой цели. В этом случае решение заключается в том, чтобы запретить создание временных переменных, и новый стандарт C++ реализует именно этот подход.

Теперь рассмотрим функцию refcube(). В ее задачу входит простое использование переданных значений без их модификации. В этом случае временные переменные не приносят никакого вреда; они придают функции более универсальный характер в отношении разнообразия аргументов, с которыми она способна работать. Следовательно, если объявление функции указывает, что ссылка имеет тип const, среда C++ при необходимости генерирует временные переменные там. По сути, функция C++, принимающая формальный ссылочный аргумент со спецификатором const и с несоответствующим фактическим аргументом имитирует традиционные действия, выполняемые при передаче аргументов по значению. При этом гарантируется, что исходные данные не подвергнутся изменениям, а для хранения соответствующего значения применяется временная переменная.

Использование ссылок при работе со структурамиСобственно говоря, ссылки были введены, прежде всего, для использования именно с этими типами, а не с

основными встроенными типами данных.Метод использования ссылок на структуры ничем не отличается от метода применения ссылок на

переменные основных типов. Достаточно воспользоваться оператором ссылки & при объявлении параметра структуры. Программа из листинга 8.6 именно это и делает. Кроме того, она реализует интересную идею — функция возвращает ссылку на структуру. Такое поведение несколько отличается от случая, когда функция возвращает структуру. Потребуется принять некоторые меры предосторожности. Часто лучше будет присвоить возвращаемой ссылке спецификатор const. Эти моменты будут разъяснены в нескольких последующих примерах. Программа содержит функцию use(), которая отображает два элемента структуры и выполняет приращение значения третьего элемента. Таким образом, третий элемент отслеживает количество обращений к определенной структуре со стороны функции use().

Листинг 8_5// использование ссылок на структуру#include "stdafx.h"#include <iostream>using namespace std;

struct sysop{ char name[26]; char quote[64]; int used;};

const sysop & use(sysop & sysopref); // функция, возвращающая ссылку

int _tmain(int argc, _TCHAR* argv[]){

setlocale(LC_CTYPE, "RUS");// ПРИМЕЧАНИЕ: некоторые реализации требуют использования// ключевого слова static в двух объявлениях структуры,// чтобы сделать возможной инициализацию

sysop looper = { "Цикличный", "Я выполняю переходы.", 0 };

use(looper); // looper имеет тип sysop cout << "Looper: " << looper.used << " вызов(а)\n"; sysop copycat;

103

103

Page 104: C++ теория

Теория и практика С++

copycat = use(looper); cout << "Looper: " << looper.used << " вызов(а)\n"; cout << "Copycat: " << copycat.used << " вызов(а)\n"; cout << "use(looper): " << use(looper).used << " вызов(а)\n"; return 0;}

// use() возвращает передаваемую ей ссылкуconst sysop & use(sysop & sysopref){ cout << sysopref.name << " говорит:\n"; cout << sysopref.quote << endl; sysopref.used++; return sysopref; }

Замечания по программеПрограмма охватывает три новых области. Первая заключается в использовании ссылки на структуру, что

наглядно демонстрирует первый вызов функции:

use(looper);

При этом функции use() передастся по ссылке структура looper, благодаря чему идентификатор syso-pref становится синонимом структуры looper. Когда функция use() отображает элементы name и quote структуры sysopref, на самом деле отображаются элементы структуры looper. Кроме того, когда функция увеличивает значение sysopref.used на 1, она фактически увеличивает значение looper.used, как это видно из выходных данных программы:

Вторым новшеством является использование ссылки в качестве возвращаемого значения. Обычно механизм возврата копирует возвращаемое значение во временную область памяти, к которой вызывающая программа затем осуществляет доступ. Однако возврат ссылки означает, что вызывающая программа выполняет прямой доступ к возвращаемому значению без использования приготовленной заранее копии. В обычных случаях такая ссылка указывает на ссылку, которая вначале была передана функции, так что вызывающая функция в результате осуществляет прямой доступ к одной из собственных переменных. Здесь, например, sysopref является ссылкой на looper, поэтому возвращаемое значение представляет собой переменную looper, ранее определенную в функции main(). Рассмотрим следующую строку кода:

copycat = use(looper);

Если бы функция use() просто возвращала структуру, содержимое sysopref копировалось бы во временную область хранения возвращаемых значений. Затем содержимое этой области копировалось бы в переменную copycat. Однако, поскольку функция use() возвращает ссылку на looper, содержимое этой переменной будет скопировано непосредственно в copycat. Это иллюстрирует одно из основных преимуществ возврата ссылки на структуру вместо самой структуры — эффективность.

На память!Функция, которая возвращает ссылку, фактически является псевдонимом переменной, на которую ссылается.

Третья новинка, которая исследуется в программе, заключается в использовании вызова функции для доступа к элементу структуры:

cout << "use(looper): " << use(looper).used << " вызов(а)\n";

Запись use(looper).used реализует доступ к элементу структуры looper. Если бы функция возвращала структуру вместо ссылки на нее, данный код осуществлял бы доступ к элементу used временной копии возвращаемого значения looper.

Будьте осмотрительными при выборе объекта, на который указывает возвращаемая ссылкаВажнее всего избегать ситуации, когда возвращаемая ссылка указывает на область памяти, которая

прекращает существование после завершения работы функции.Ниже приводится пример кода, который не следует использовать:

104

104

Page 105: C++ теория

Теория и практика С++

const sysop & clone2(sysop & sysopref){

sysop newguy; // первый шаг к серьезной ошибкеnewguy = sysopref; // копирование информацииreturn newguy; // возврат ссылки на копию

}

В результате выполнения этого кода возвращается ссылка на временную переменную (newguy), которая прекращает существование сразу после завершения работы функции. Подобно этому следует избегать ситуаций, когда на такие временные переменные возвращаются указатели.

Проще всего избежать подобной ошибки путем реализации возврата ссылки, которая была передана функции в качестве аргумента. Такой параметр будет ссылаться на данные, используемые вызывающей функцией. Таким образом, возвращаемая ссылка будет ссылаться на те же данные.

Второй метод заключается в использовании операции new для создания новой области хранения. Вот как решается подобная задача с помощью ссылки:

const sysop & clone(sysop & sysopref){ sysop *psysop = new sysop; *psysop = sysopref; // копирование информации return *psysop; // возврат ссылки на копию}

Первый оператор создает безымянную структуру sysop. Указатель psysop указывает на эту структуру, поэтому выражение *psysop означает саму структуру. Создается впечатление, что приведенный выше фрагмент программы возвращает структуру.

Однако объявление функции указывает, что она возвращает ссылку на эту структуру. Затем функцию можно использовать следующим образом:

sysop & jolly = clone(looper);

В результате переменная jolly становится ссылкой на новую структуру. Такое решение связано с одним затруднением: потребуется использовать оператор delete для освобождения памяти, выделенной операцией new, когда она более не будет использоваться. Вызов функции clone() скрывает обращение к операции new, в результате чего будет сложнее впоследствии вспомнить о необходимости применить операцию delete. Но есть шаблон auto_ptr, который помогает автоматизировать процесс освобождения памяти.

Причины использования спецификатора const при объявлении возвращаемой ссылкиВ предыдущем примере функция use() возвращает значение const sysop &. Может возникнуть вопрос

о назначении спецификатора const в данном случае. Он не означает, что структура sysop сама по себе является константой. Смысл спецификатора лишь в том, что возвращаемое значение нельзя напрямую использовать для модификации структуры. Например, при отсутствии спецификатора const был бы возможным следующий код:

use(looper).used =10;

Поскольку функция use() возвращает ссылку на структуру looper, этот код будет иметь такой же эффект, как и следующий фрагмент:

use(looper); // отображение структуры, приращение элемента usedlooper.used = 10; // повторная установка значения 10 для used

Рассмотрим еще один вариант:

sysop newgal = ("Polly Morf", "Polly's not a hacker.", 0};use(looper) = newgal;

Той же цели можно добиться с помощью следующего кода:

sysop newgal = {"Polly Morf", "Polly's not a hacker.", 0};use(looper); // отображение структуры, приращение элемента usedlooper = newgal; // замена содержимого looper содержимым newgal

105

105

Page 106: C++ теория

Теория и практика С++

Одним словом, опуская спецификатор const можно создавать более краткий, но более сложный для понимания код.

Как правило, следует избегать конструкций, смысл которых неочевиден. Это повышает вероятность сложных для выявления ошибок.

Использование ссылок на объект классаВ языке C++ для передачи функциям объектов классов обычно практикуется использование ссылок.

Например, ссылочные параметры используются в функциях, принимающих объекты классов string, ostream, istream, ofstream и ifstream в качестве аргументов.

Рассмотрим пример, где используется класс string и демонстрируются различные решения, в том числе и неудачные. Замысел состоит в том, чтобы создать функцию, которая присоединяет данную строку к обеим сторонам другой строки. В листинге 8.7 представлены три функции, предназначенные для решения этой задачи. Однако одно из решений настолько ошибочное, что может привести к сбою программы или даже отказу компиляции.

Листинг 8_7// различные решения#include "stdafx.h"#include <iostream>#include <string>using namespace std;

string version1(const string & s1, const string & s2);const string & version2(string & s1, const string & s2); // побочный эффектconst string & version3(string & s1, const string & s2); // неудвчное решение

int _tmain(int argc, _TCHAR* argv[]){

setlocale(LC_CTYPE, "RUS");

string input; string copy; string result;

cout << "Введите строку: "; getline(cin, input); copy = input; cout << "Вы ввели строку: " << input << endl; result = version1(input, "***"); cout << "Измененная строка: " << result << endl; cout << "Исходная строка: " << input << endl; result = version2(input, "###"); cout << "Измененная строка: " << result << endl; cout << "Исходная строка: " << input << endl;

cout << "Восстановление исходной строки.\n"; input = copy; result = version3(input, "@@@"); cout << "Измененная строка: " << result << endl; cout << "Исходная строка: " << input << endl;

return 0;}

string version1(const string & s1, const string & s2){ string temp;

temp = s2 + s1 + s2;;

106

106

Page 107: C++ теория

Теория и практика С++

return temp;}

const string & version2(string & s1, const string & s2) // побочный эффект{ s1 = s2 + s1 + s2;

// возврат ссылки, переданной функции, надежен return s1; }

const string & version3(string & s1, const string & s2) // неудачное решение{ string temp;

temp = s2 + s1 + s2;// возврат ссылки на локальную переменную ненадежен

return temp;}

Замечания по программеПервый вариант функции наиболее прямолинеен:

string version1(const string & s1, const string & s2){ string temp;

temp = s2 + s1 + s2;; return temp;}

Функция принимает два аргумента типа string и использует класс string для создания новой строки, которая обладает требуемыми свойствами. Обратите внимание, что оба аргумента функции представляют собой ссылки со спецификатором const.

Результат работы функции такой же, как если бы ей передавались два объекта типа string:

string version4(string si, string & s2) // тот же результат

В этом случае аргументы s1 и s2 являются вновь созданными объектами типа string. Таким образом, применять ссылки эффективнее, поскольку в этом случае функция не будет создавать новые объекты и копировать в них данные из исходных объектов. Здесь спецификатор const указывает, что функция использует, но не изменяет исходные строки.

Объект temp является новым и локальным по отношению к функции version1(). Он прекращает существование после завершения работы функции. Таким образом, возврат объекта temp в качестве ссылки невозможен, поэтому для функции задан тип string. Это означает, что содержимое объекта temp будет скопировано во временную область хранения возвращаемых значений. Затем в функции main() содержимое области хранения копируется в строку с именем result:

result = version1(input, "***");

Функция version2() не создает временную строку. Вместо этого она напрямую изменяет исходную строку:

const string & version2(string & s1, const string & s2) // побочный эффект{ s1 = s2 + s1 + s2; // возврат ссылки, переданной функции, надежен return s1; }

Эта функция может изменять значение s1, поскольку переменная s1, в отличие от s2, объявлена без спецификатора const.

Поскольку s1 является ссылкой на объект (input) функции main(), возврат этой переменной в качестве ссылки вполне допустим. По той же причине строка:

107

107

Page 108: C++ теория

Теория и практика С++

result = version2(input, "###");

эквивалентна следующему фрагменту кода:

version2(input, "###"); // input прямо изменен функцией version2()result = input; // ссылка на s1 является ссылкой на input

Однако в результате возникает побочный эффект — изменение значения input:

Исходная строка: sampleИзмененная строка: ### sample ###Исходная строка: ### sample ###

Таким образом, если исходная строка не должна изменяться, такое решение ошибочно.Третий вариант функции в листинге 8.7 служит напоминанием о возможной ошибке:

const string & version3(string & s1, const string & s2) // неудачное решение { string temp; temp = s2 + s1 + s2; // возврат ссылки на локальную переменную ненадежен return temp; }

Он содержит неисправимую ошибку — возврат ссылки на переменную, объявленную локально внутри функции version3().

Эта функция компилируется (с выводом предупреждающего сообщения), но при попытке выполнения программы происходит сбой. Непосредственно ошибку вызывает следующая операция присваивания:

result = version3(input, "@@@");

Осуществляется попытка ссылки на адрес памяти, который более не используется.

Еще один урок ООП: объекты, наследование и ссылкиОбъекты типа ofstream могут использовать методы ostream, что позволяет для файловых операций

ввода-вывода применять те же формы, что и для консольного ввода-вывода. Особенность языка, позволяющая передавать свойства из одного класса в другой, называется наследованием. Короче говоря, ostream называется базовым классом (поскольку класс ofstream основан на нем), а класс ofstream носит название производного (так как наследует класс ostream). Производный класс наследует методы базового класса. Это означает, что объект ofstream может использовать функции базового класса, такие как методы форматирования precision() и setf().

Второй аспект наследования состоит в том, что ссылка на базовый класс может указывать на объект производного класса, не требуя приведения типов. На практике это позволяет определять функцию, обладающую параметром-ссылкой на базовый класс. Эта функция может взаимодействовать с объектами базового класса, а также с производными объектами. Например, функция с параметром типа ostream & может принимать объект ostream, такой как cout или ofstream.

Указанные аспекты демонстрируются в листинге 8.8, где одна и та же функция используется для записи данных в файл и отображения тех же данных на экране. Изменяется только аргумент, который передается вызываемой функции. Представленная программа рассчитывает фокусное расстояние объектива телескопа (его основного зеркала или линзы) и отдельных окуляров. Затем вычисляется кратность увеличения каждого окуляра телескопа. Кратность увеличения равна фокусному расстоянию телескопа, деленному на фокусное расстояние используемого окуляра, так что вычисления здесь несложные. Кроме того, в программе используются некоторые методы форматирования, которые, как обещалось, одинаково успешно выполняются как с объектами типа cout, так и с объектами типа ofstream (в нашемпримере — fout).

Листинг 8_8#include "stdafx.h"#include <iostream>#include <fstream>

108

108

Page 109: C++ теория

Теория и практика С++

#include <cstdlib>using namespace std;

void file_it(ostream & os, double fo, const double fe[],int n);const int LIMIT = 5;

int _tmain(int argc, _TCHAR* argv[]){

setlocale(LC_CTYPE, "RUS");

ofstream fout;const char *fn = "ep-data.txt";fout.open(fn);if (!fout.is_open()){

cout << "Невозможно открыть " << fn << ". Пока.\n";exit(EXIT_FAILURE);

}double objective;cout << "Введите фокусное расстояние объектива телескопа в мм: ";cin >> objective;double eps[LIMIT];cout << "Введите фокусные расстояния, в мм, " << LIMIT << " окуляров:\n";for (int i = 0; i < LIMIT; i++){

cout << "Окуляр #" << i + 1 << ": ";cin >> eps[i];

}file_it(fout, objective, eps, LIMIT);file_it(cout, objective, eps, LIMIT);cout << "Конец\n";return 0;

}

void file_it(ostream & os, double fo, const double fe[], int n){

ios_base::fmtflags initial;initial = os.setf(ios_base::fixed); // сохранение исходного

// состояния форматированияos.precision(0);os << "Фокусное расстояние объектива: " << fo << " мм\n";os.setf(ios::showpoint);os.precision(1);os.width(12);os << "коэффициент увеличения";os.width(15);os << "окуляра" << endl;for (int i = 0; i < n; i++){

os.width(12);os << fe[i];os.width(15);os << int (fo/fe[i] + 0.5) << endl;

}os.setf(initial); // восстановление исходного состояния форматирования

}

Замечания по программеГлавная идея программы из листинга 8.8 состоит в демонстрации того факта, что параметр типа ostream &

может ссылаться на объект ostream вроде cout и на объект ofstream, такой как fout. Однако программа также иллюстрирует использование методов форматирования объекта ostream для обоих типов параметров.

Метод setf() позволяет устанавливать различные состояния форматирования. Например, при вызове метода setf(ios_base::fixed) объект переводится в режим использования фиксированной десятичной точки. При вызове метода setf(ios_base::showpoint) объект переводится в режим отображения завершающей десятичной точки, даже если последующие цифры представляют нули. Метод precision()

109

109

Page 110: C++ теория

Теория и практика С++

указывает количество цифр, отображаемых справа от десятичной точки (если объект выводится в режиме fixed). Все эти установки сохраняются до тех пор, пока не будут изменены в результате следующего вызова метода. В результате вызова метода width() устанавливается ширина поля для следующей операции вывода. Эта установка действует только для отображения единственного значения, а затем возвращается в принимаемое по умолчанию состояние. (По умолчанию ширина поля равна нулю. Затем эта величина повышается до точного соответствия отображаемому значению.)

Функция file_it() содержит два интересных вызова метода:

ios_base::fmtflags initial;initial = os.setf(ios_base::fixed); // сохранение исходного // состояния форматирования

. . .os.setf(initial); // восстановление исходного состояния форматирования

Метод setf() возвращает копию всех установок форматирования, которые действовали до его вызова. Обозначение ios_base::fmtflags является причудливым именем типа данных, необходимых для хранения этой информации. В результате операции присваивания в переменной initial сохраняются настройки, которые действовали на момент вызова функции file_it(). Затем переменная initial может использоваться в качестве аргумента функции setf(), чтобы восстановить исходные значения всех установок форматирования. Таким образом, эта функция восстанавливает состояние объекта, которое существовало до его передачи функции file_it().

Более полное знакомство с классами поможет вам лучше уяснить работу этих методов, а также причину, по которой в программе часто используются обозначения вроде ios_base.

И последнее: каждый объект хранит собственные параметры форматирования. Поэтому, когда программа передает объект cout функции file_it(), его настройки форматирования изменяются, а затем восстанавливаются. То же самое происходит с объектом fout, когда он передается функции file_it().

Когда целесообразно использовать ссылочные аргументыСуществуют две главных причины использования ссылочных аргументов:

Чтобы сделать возможным изменение объекта данных вызывающей функции. Чтобы ускорить работу программы за счет передачи ссылки вместо полной копии объекта данных.

Аргументы, определяемые по умолчаниюТакие аргументы представляют собой значения, которые используются автоматически, если

соответствующие фактические параметры в обращении к функции опущены. Например, если функция void wow(int n) описана так, что n по умолчанию имеет значение 1, то обращение к функции wow() означает то же самое, что и вызов wow (1). Это свойство позволяет более гибко использовать функции. Предположим, что функция left() возвращает первые n символов строки, при этом сама строка и число n являются аргументами. Точнее, функция возвращает указатель на новую строку, представляющую собой выбранный фрагмент исходной строки. Например, в результате вызова функции left("theory", 3) создается новая строка "the" и возвращается указатель на нее. Теперь предположим, что для второго аргумента по умолчанию установлено значение 1. Вызов left ("theory", 3) будет выполнен, как и раньше, поскольку указанная величина 3 переопределит значение, принимаемое по умолчанию. Однако вызов left("theory") теперь уже не будет ошибочным. Он подразумевает, что значение второго аргумента равно 1, поэтому будет возвращен указатель на строку "t". Этот вид выбора значений по умолчанию удобен для программ, которые часто извлекают строки длиной в один символ, но иногда требуется извлекать более длинные строки.

Как установить значение по умолчанию? Для этой цели следует воспользоваться прототипом функции. Поскольку компилятор использует прототип, чтобы узнать, сколько аргументов имеет функция, прототип функции также должен сообщить программе о возможности использования аргументов, заданных по умолчанию. Метод состоит в том, что значения аргументам присваиваются в прототипе. Например, пусть имеется прототип, соответствующий следующему описанию функции left():

char* left(const char *str, int n = 1);

Требуется, чтобы эта функция возвращала новую строку, поэтому ее типом будет char*, то есть указатель на значение типа char. Если нужно сохранить исходную строку неизменной, следует использовать спецификатор const для первого аргумента. А чтобы аргумент n имел значение 1, определенное по умолчанию, присвоим это значение аргументу n. Принимаемое по умолчанию значение аргумента — это значение, задаваемое при инициализации. Таким образом, данный прототип инициализирует n значением 1.

110

110

Page 111: C++ теория

Теория и практика С++

Если аргумент n опускается, для него принимается значение 1, но если данный аргумент передается, то новое значение переопределит значение 1.

Для функции со списком аргументов принимаемые по умолчанию значения присваиваются в направлении справа налево. Иначе говоря, нельзя присвоить значение по умолчанию некоторому аргументу, пока не будут присвоены значения для всех остальных аргументов, размещенных справа от него:

int harpo(int n, int m = 4, int j = 5); // ПРАВИЛЬНОint chico(int n, int m = 6, int j); // НЕПРАВИЛЬНОint groucho(int k = 1, int m = 2, int n = 3); // ПРАВИЛЬНО

Например, прототип функции harpo() допускает реализацию вызова функции с одним, двумя или тремя аргументами:

beeps = harpo(2); // то же, что и harpo(2,4,5)beeps = harpo(1,8); // то же, что и (1,8,5)beeps = harpo (8,7,6); // аргументы, определенные по умолчанию, не используются

Фактические значения присваиваются соответствующим формальным аргументам в направлении слева направо. Пропуск аргументов не допускается. Таким образом, следующее выражение не допускается:

beeps = harpo(3, ,8); // неправильно, аргументу m не будет присвоено значение 4

Аргументы, определенные по умолчанию, не являются особо выдающимся достижением в программировании. Они лишь служат удобным дополнением. Когда вы перейдете к разработке классов, то убедитесь в том, что этот прием позволяет сократить количество конструкторов, методов и перегрузок методов, которые необходимо определить.

Программа в листинге 8.9 находит применение аргументам по умолчанию.Обратите внимание, что только в прототипе указаны принимаемые по умолчанию значения. На определении

функции наличие принимаемых по умолчанию аргументов не отражается.

Листинг 8_9// функция с определенным по умолчанию// аргументом, предназначенная для обработки строк

#include "stdafx.h"#include <iostream>using namespace std;

const int ArSize = 80;char* left(const char * str, int n = 1);

int _tmain(int argc, _TCHAR* argv[]){

setlocale(LC_CTYPE, "RUS");char sample[ArSize];cout << "Введите строку:\n";cin.get(sample, ArSize);char *ps = left(sample, 4);cout << ps << endl;delete [] ps; // удаление старой строкиps = left(sample);cout << ps << endl;delete [] ps; // удаление новой строкиreturn 0;

}

// Эта функция возвращает указатель на новую строку, состоящую// из n первых символов строки str.char* left(const char * str, int n){

if(n < 0)n = 0;

111

111

Page 112: C++ теория

Теория и практика С++

char *p = new char[n+1];int i;for (i = 0; i < n && str[i]; i++)

p[i] = str[i]; // копирование символовwhile (i <= n)

p[i++] = '\0'; // установка значений '\0' для оставшейся// части строки

return p;}

112

112