Uploaded by pk_ekran

UTF-8 39 39 Posobie po OOP izd 2 ispravlennoe i dopolnennoe

advertisement
Министерство образования и науки Российской Федерации
Федеральное государственное бюджетное образовательное учр еждение
высшего образования
«Владимирский государственный университет
имени Александра Григорьевича и Николая Григорьевича Столетовых»
И.Р. ДУБОВ, В.И.БЫКОВ
ЯЗЫКИ ПРОГРАММИРОВАНИЯ.
ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
ПРОГРАММИРОВАНИЕ
Учебное пособие
Владимир 2017
УДК 519.682.2
ББК 32.973
Д79
Рецензенты:
Доктор технических наук, профессор
зав. кафедрой информационных систем и программной инженерии
Владимирского государственного университета
имени Александра Григорьевича и Николая Григорьевича Столетовых
И. Е. Жигалов
Кандидат физико-математических наук
Генеральный директор ООО «Фирма «Инрэко ЛАН»
К.В. Демидов
Печатается по решению редакционно-издательского совета ВлГУ
Д79
Дубов, И.Р. Языки программирования. Объектноориентированное программирование: Учеб. пособие / И.Р. Дубов,
В.И. Быков; Владим. гос. ун-т. им. А. Г. и Н. Г. Столетовых. –
Владимир: Изд-во ВлГУ, 2017. – 100 с.
ISBN
Рассматриваются основные положения объектно-ориентированного программирования и средства их поддержки в языке C++. Особое внимание уделено сложным вопросам, которые обычно трудно воспринимаются студентами: реализация позднего
связывания объектов и методов, обобщенные классы, особенности использования множественного наследования.
Предназначено для студентов специальности 09.03.01 "Информатика и вычислительная техника" при изучении дисциплины «Языки программирования».
Ил. 19. Библиогр.: 3 назв.
УДК 519.682.2
ББК 32.973
ISBN
 Владимирский государственный
университет, 2017
 Дубов И.Р., Быков В.И., 2017
ВВЕДЕНИЕ
При создании программных систем можно выделить три основных стадии: анализ, проектирование, программирование. Задача анализа – построение модели предметной области. Задача проектирования – разработка модели программной системы, учитывающей предметную область и будущую реализацию. Задача программирования
(кодирования) – реализация программной системы с помощью конкретных языков программирования. Объектно-ориентированный подход к выполнению всех перечисленных стадий стал уже стандартом
при создании сложных программных продуктов.
Цель данного пособия – свести воедино некоторые вопросы
проектирования и программирования. При этом осуществляется попытка рассмотреть применение средств объектно-ориентированного
программирования вместе с реализацией на достаточно низком уровне.
Традиционно изучение программирования начинается со структурного программирования. Опыт преподавания показывает, что переход к объектному стилю мышления требует от обучающегося значительных усилий. В данном пособии сделана попытка сгладить острые углы этого перехода, в нем больше внимания уделяется тем вопросам, которые вызывают наибольшую сложность при изучении:
позднее связывание, виртуальные базовые классы, перегрузка операторов. Таким образом, пособие следует рассматривать как дополнение
к имеющейся обширной литературе по объектно-ориентированному
подходу [1, 2, 3], ни в коей мере не претендующее на полноту изложения.
В качестве языка программирования выбран C++. Это связано с
тем, что в этом языке реализовано большинство положений объектноориентированного подхода, например, такие возможности, как обобщенные классы, множественное наследование, перегрузка операторов. Кроме этого, в наиболее распространенных языках программирования Java и C# за основу был взят синтаксис именно языка C++. Для
упрощения работы с пособием в него включены начальные сведения о
языке C++ с обсуждением некоторых деталей, важных для объектноориентированного программирования.
3
Глава 1. ОСНОВНЫЕ ЭЛЕМЕНТЫ ЯЗЫКА С++
1.1. Примеры простейших программ
Основные элементы программы на языке С++ показаны в нижеследующем примере:
1:
2:
3:
4:
5:
6:
#include <iostream> //директива препроцессора
int main() //главная функция
{
//операторная скобка
Std::cout << "Hello, world!\n";
//cout – глобальная переменная для выходного потока
//<< - перегруженный оператор вставки в поток
return 0; //возврат из функции (здесь – возврат в ОС)
}
//операторная скобка
Директива #include обеспечивает подключение библиотеки. В
данном примере библиотека iostream содержит средства ввода/вывода. В ней, например, объявлена глобальная переменная сout,
связанная с устройством вывода. В этой же библиотеке объявлена переменная cin, связанная с устройством ввода. В следующем примере
приводятся объявления переменных, операторы и реализуется ввод
данных.
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
#include <iostream>
using namespace std; // пространство имен
// для элементов библиотеки iostream
void main() // может не возвращать значение
{
int integer1, integer2, sum; //объявление переменных
//целого типа
cout << "введите 1-е число"; //вставка в поток
cin >> integer1; //извлечение из входного потока
cout << "введите 2-е число"; //вставка в поток
cin >> integer2; //извлечение из входного потока
sum=integer1+integer2; //операторы сложения
//и присваивания
cout << "сумма равна" << sum << endl;
//endl – манипулятор потока: вывод содержимого
// буфера и новая строка
}
4
Препроцессор – это часть компилятора, выполняющая предварительную обработку исходного текста программы. Препроцессор
заменяет определенным образом предназначенные для него директивы и формирует окончательный исходный модуль, который после
препроцессирования будет транслироваться.
Конструкция cout<<a<<b<<c – это конкатенация двуместных
операторов <<. Оператор cout<<a возвращает левый операнд cout,
который оказывается левым операндом в cout<<b. Далее cout оказывается левым операндом в cout<<c. Конкатенация операторов используется очень часто, например, x+y+z – это конкатенация операторов сложения (выполняется слева направо), x=y=z=1 – это конкатенация операторов присваивания (выполняется справа налево).
Директивы препроцессора
Препроцессор в соответствии с директивами формирует общий
исходный модуль на С++, который в дальнейшем проходит этапы
компиляции и сборки (см. рис. 1).
iostream.h
исходный
модуль
hello.cpp
hello.obj
#include <iostream>
исходный
модуль
препроцессор
общий
исходный
модуль
компилятор
объектный
модуль
hello.exe
загрузочный
модуль
компоновщик
другие
объектные
модули
Рис. 1. Порядок препроцессирования, компиляции и сборки программы
Директивы включения файлов используются для включения в
исходный модуль программы заголовочных файлов библиотек. Символы <> предназначены для указания стандартных заголовочных
файлов, поиск которых выполняется в системном каталоге библиотечных файлов. В символах "" указывается пользовательский заголовочный файл, поиск которого ведется сначала в текущем каталоге, а
затем – в системном каталоге.
5
19: #include <iostream>
20: #include "myio.h"
Директивы условной компиляции позволяют исключать из обработки фрагменты исходного текста программы. Управление трансляцией необходимо в тех случаях, когда текст содержит код программы для выполнения на различных платформах, в текст включены отладочные операторы.
21: #define DEBUG //определение идентификатора, идентификатор
//"виден" только препроцессору
22: #ifdef DEBUG
23: ...
//фрагмент транслируется, если идентификатор DEBUG
//был определен директивой #define
24: #else
25: ...
//фрагмент транслируется, если идентификатор DEBUG
//не был определен
26: #endif
27: #ifndef DEBUG
28: ...
//фрагмент транслируется, если идентификатор DEBUG
//не был определен
29: #endif
Один и тот же файл может быть включен директивой #include в несколько других файлов. Директивы условной компиляции
позволяют предотвратить повторное включение содержимого. В следующем примере заголовочного файла, при повторном включении
символ STRINGH уже будет определен и текст до #endif будет пропущен.
30:
31:
32:
33:
#ifndef STRINGH
#define STRINGH
/* содержимое заголовочного файла */
#endif
В некоторых средах разработки в данной ситуации вместо директив условной компиляции можно использовать нестандартную директиву #pragma once для однократного включения заголовочного
файла в процесс компиляции.
6
34:
35:
#pragma once
/* содержимое заголовочного файла */
Директива #define, кроме описанного определения идентификатора, может использоваться для определения макросов. Для этого
после имени, определяемого в директиве, указывается последовательность символов. Далее препроцессор заменяет вхождение имени макроса на заданную последовательность. Макрос может иметь формальные параметры, которые указываются в скобках после имени
макроса. В этом случае дальнейшее вхождение макроса (макроподстановка) с фактическими параметрами заменяется на тело макроса, в
котором формальные параметры заменяются на фактические.
36:
37:
38:
39:
#define MAX_SIZE 1024 // макроопределение
int vec[MAX_SIZE]; // макроподстановка
#define MAX(x,y) ((x > y) ? x : y) // макроопределение
// с параметрами
cout << MAX(3, 4); // макроподстановка
Операторы
В языке С++ имеются следующие основные операторы. Оператор присваивания "=" позволяет присвоить новое значение переменной, в его правой части должно быть выражение. В языке предусмотрены основные арифметические операции: "()" – скобки; "*" – умножение; "/" – деление; "%" – деление по модулю. Логические операции
имеют следующие обозначения: "==" – равенство; "!=" – неравенство;
">" – больше; "<" – меньше; ">=" – больше или равно; "<=" – меньше
или равно. Операции булевской логики обозначаются следующим образом: "!" – логическое "НЕ"; "&&" – логическое «И»; "||" – логическое «ИЛИ». Логические выражения имеют результат типа bool, который принимает значение либо true (истина), либо false (ложь).
40:
41:
(1==2) // false
(1!=2) // true
Целочисленные значения могут быть приведены к типу bool
так, что значение ноль соответствует false, а все другие целые зна7
чения соответствуют true. При обратном преобразовании true
приводится к единице, а false к нулю:
42:
43:
i = (5==5)*3;
cout << i; //результат 3
Инструкция ветвления
Формально ветвление определяется следующим образом: if
(выражение) оператор. Оператор выполняется тогда, когда
выражение истинно.
44:
45:
46:
47:
48:
49:
50:
51:
#include <iostream>
void main (){
int num1, num2;
num1 = 3;
num2 = 2;
if (num1 > num2) num1= num2;
cout << num1;
}
1.2. Типы данных
Фундаментальные типы
В С++ имеются следующие целочисленные типы, которые могут
быть дополнительно объявлены беззнаковыми с помощью
модификатора unsigned.
52:
53:
54:
55:
char c;
//символьный (однобайтовый)
short s1;
short int s2; //короткое целое
int i;
//целое (слово)
long l1;
long int l2; //длинное целое
Вещественные типы данных различаются диапазоном представления чисел и точностью.
56:
57:
58:
float f; // машинное слово
double d; // два слова
long double q; // 3 или 4 слова
8
Литералы
Рассмотрим основные виды констант:
59:
60:
61:
62:
63:
64:
65:
66:
67:
20
024
0x14
128u
1L
1024UL
1.01E-3
12.1F
1.0L
//десятичная
//восьмеричная
//шестнадцатеричная
//целое без знака
//длинное целое
//длинное целое без знака
//экспоненциальная форма
//обычная точность
//двойная точность
В символьные строки можно вставлять символы, не имеющие
печатаемых знаков, это так называемые escape-последовательности:
68:
69:
70:
71:
72:
73:
’\n’
’\r’
’\\’
’\?’
’\’’
’\”’
//перевод строки
//возврат каретки
//«\»
//?
//'
//"
Можно использовать восьмеричные коды символов в escapeпоследовательностях:
74:
75:
76:
\0
//null
\12 //new line
\062 //'2'
Объявление и определение имен
Объявление имени указывает, чем имя является (именем переменной, именем функции, именем структуры и т.д.). Оно состоит из
объявления типа и объявления идентификатора. Объявление типа состоит из названия типа и модификаторов типа, например, int* (модификатор «*»), const int (модификатор «const») и т.п. Объявление идентификатора состоит из идентификатора и модификаторов
идентификатора, например, f()(модификатор «()»), f[] (модификатор «[]»). Часто полезно указывать модификатор идентификатора const для указателя, который не должен изменяться, например,
9
int* const pk. Объявление имени может встречаться в программе
произвольное количество раз.
Определение имени указывает, как реализуется программный
объект. Определение имени совмещается с объявлением имени. Определение имени может быть сделано только один раз.
77:
78:
79:
int f();
//объявление функции
int f() {return 0;} //определение функции
int a;
//и объявление, и определение
//переменной
Определение переменной является исполняемым оператором,
т.к. им выполняется создание переменной. Переменные могут быть
определены без инициализации и с инициализацией.
80:
81:
82:
int x; //неинициализированная переменная равная 0
double price = 109.9, discount = 0.16; //инициализация
double sale_price(price* discount);
//инициализация
Типизированные именованные константы задаются с помощью
модификатора типа const, т.е. путем конструирования типа.
83:
84:
85:
86:
const double pi = 3.14159; //определение константы
double x = pi; //правильно
pi = x; //ошибка трансляции, т.к. нарушение типизации при
//присваивании
const char*const str=”Hello”; // первый const –
// модификатор типа, второй – модификатор идентификатора
Производный перечисляемый тип
Перечисляемый тип записывается при помощи ключевых слов
enum class и задает перечень типизированных констант. Элементы
перечисления находятся в области видимости списка, и для доступа к
ним требуется явное указание имени перечисления. Это предохраняет
код от конфликтов имен элементов различных перечислений.
87:
88:
89:
90:
//Status – идентификатор для нового типа
enum class Status {fail, pass, stop};
Status test;
test = Status::pass;
if (test == Status::stop){/* ... */ }
10
Для обратной совместимости в C++ остается возможность задать перечисляемый тип, элементы которого доступны без указания
имени типа. Он объявляется при помощи зарезервированного слова
enum. Этим способом не рекомендуется пользоваться из-за возможных конфликтов имен.
91:
92:
93:
94:
95:
96:
enum Status {fail, pass, stop};
Status test;
test = pass;
if (test == stop) {/* ... */}
//объявление констант без введения имени для нового типа
enum {noon = 0, midnight = 1};
int time = noon;
Производный тип структура
Структура позволяет конструировать новые агрегирующие типы
данных. Формально новый тип объявляется следующим образом:
struct
имя-нового-типа
{объявление-полей}. Здесь
struct – ключевое слово.
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
struct Time{
int hour;
int minute;
int second;
}
void main(){
Time t1, t2;
Time t3 = {12, 20, 0}; //инициализация структуры
t1.hour = 2;
t1.minute = 30;
t1.second = 10;
t2.hour=3;
if (t1.hour > t2.hour)//...
t2 = t1; //присваивание побайтное
cout << t2.minute << endl;
}
Типизированные указатели
Переменная типа указатель предназначена для хранения адреса
некоторого объекта. Тип такой переменной определяется типом того
11
объекта, на который указывает указатель, а перед идентификатором
указателя записывается символ "*". Имя сконструированного таким
образом типа включает в себя и имя типа референта, и символ "*".
Например, int* – это тип указателя на целое число.
113:
114:
int *ip1, *ip2; // две переменных типа указатель
char * cp1, cp2; //cp1 - указатель,
//но cp2 – не указатель, а переменная типа char
С указателями используют следующие операторы. Имеется одноместный оператор "*" обращения по адресу, его также называют
оператором раскрытия ссылки или оператором разыменовывания.
Одноместный оператор "&", возвращает адрес переменной. С переменной можно выполнять любые действия, имея ее адрес:
115:
116:
117:
118:
119:
120:
int i = 1024, *ip = &i;
int k, *kp = &k;
*kp=i; //*kp это референт указателя
k=*ip;
//1024 в k
*kp = *ip; //1024 в k
kp = ip;
//kp и ip будут указывать
//на одно место в памяти
Предусмотрены следующие операции над указателями, в результате выполнения которых получается новое значение указателя:
указатель + целое; указатель – целое; целое + указатель. В этих операциях целочисленное слагаемое указывает количество элементов,
которое при вычислении смещения умножается на размер референта.
Размер референта определяется по типу указателя.
121:
122:
123:
124:
125:
int a[] = {2,3,5};
int *ip = a;
cout << ip; //результат 0*00b0
cout << ip – 1; //результат 0*00ae
cout << ip + 1; //результат 0*00b2
Если для инициализации переменной типа char* используют
текстовую строку, то данный указатель указывает на первый символ
текстовой строки. При выводе переменной типа char* в поток, опе-
12
ратор << выводит не значение указателя, а строку до символа с нулевым кодом, который добавляется транслятором автоматически.
126:
127:
char *st = "строка символов"; //в конец будет добавлен \0
cout << st; //в поток выводится строка до символа \0
Для указателя, который никуда не указывает, используют значение nullptr. Это особое значение позволяет проверять, является ли
значение переменной указателя корректным. Исторически для тех же
целей используют число 0 либо макроопределение NULL.
128:
129:
130:
131:
132:
double* a;
a = nullptr;
if (a == nullptr){
cout << "null!";
}
Ссылочный тип
Ссылкой задается синоним переменной. Адрес ссылки совпадает с адресом соответствующей переменной.
133:
134:
135:
int val = 10;
int &ref = val;
//ref и val - синонимы
ref = ref + 2; // val == 12
Основное назначение ссылочного типа – передача параметров
при вызове функций. Передача параметра по ссылке равносильна передаче адреса параметра.
Массивы
Массив – совокупность последовательно расположенных элементов одного типа. В С++ имеется только низкоуровневое понятие
массива. Здесь массив не является особым типом данных. Для обращения к массиву используется адрес первого элемента массива.
136:
137:
138:
int ia[10]; //отведение места в памяти под массив
const bufsize = 512;
char buffer[bufsize]; //размерность массива //только константа
13
При объявлении массива можно сразу инициализировать его
элементы. Количество инициализирующих значений должно быть не
более количества элементов в массиве. Индексация элементов массива начинается от нуля.
139:
140:
141:
const arraysize=3;
int ia[arraysize]={10,11,12};
//ia[0]==10
//ia[1]==11
//ia[2]==12
int ib[]={10,11,12};
Фактически имя массива – это указатель, поэтому если ia[1]
– это целое число, то ia – это указатель на целое число. С именем
массива возможны любые операции, предусмотренные для указателей. Например, записи *(buf+1) и buf[1] – эквивалентны. Поэтому в C++ нет контроля выхода индекса массива за границы. Изменить
значение имени массива нельзя, так как имя массива – константа.
При инициализации массива символами следует помнить о добавлении символа \0 в конец строки символов.
142:
143:
144:
char ca[]={’A’,’B’,’C’}; //массив из трех элементов
char cb[]="ABC"; //массив из четырех элементов, так как
//добавляется символ \0. cb[0]==*cb==’A’
cb=cb+1; //ошибка трансляции - невозможно изменить
//значение имени массива
Многомерные массивы объявляются и инициализируются аналогично одномерному массиву.
145:
146:
147:
148:
149:
int ia[3][4]; //3 строки, 4 столбца
int ib[3][4]= //внутренние скобки можно опустить
{
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
cout<<ib[0][0]; // 0;
cout<<ib[0][2]; // 2;
cout<<ib[1][2]; // 6;
14
Автоматический вывод типа
Ключевое слово auto, указанное вместо типа переменной, компилятором воспринимается как указание вывести тип переменной по
инициализирующему выражению. Это позволяет, во-первых, сократить запись кода для сложно определяемых типов, а во-вторых –
уменьшить количество изменений кода при пересмотре типов переменных.
150:
151:
152:
153:
auto v = new vector<vector<int>>();
v->push_back(vector<int>(100));
auto d = 1.2; // Тип double. Если заменить
// инициализацию на 1.2L, то d и d2 будут long double
auto d2 = d * 2;
В случае использования инициализатора массива для переменной с автоматическим выводом типа, тип определяется как
initializer_list<T>, где T – тип элементов инициализатора.
Этот тип может использоваться в цикле по диапазону элементов.
154:
155:
auto arr = { 2, 4, 6 }; // initializer_list<int>
for (auto i : arr) { cout << i; } // тип для i – int
Описатель decltype позволяет компилятору назначать тип переменной по типу некоторого выражения без инициализации:
156:
157:
158:
159:
long int longInt;
decltype(longInt) longInt2; // long int
double g();
decltype(longInt * g()) h(decltype(longInt) k);
// тип для параметра k – long int,
// для возвращаемого занчения double
Синонимы типов
Синонимы типов используются для определения имен производных
типов. Это позволяет сократить запись и уменьшить вероятность ошибок в
использовании типов. Синоним типа задается в виде using имя = определение-типа, где using – зарезервированное слово.
160:
//angle – новый вводимый тип
15
161:
162:
163:
164:
//double – ранее определенный тип
using angle = double;
//String - идентификатор для нового типа,
//являющегося указателем на символьную строку
using String = char*;
//Names - имя массива, в котором элементы
//имеют тип String
using Names = String[3];
String congr = "Hello"; // congr – указаетель на строку
//person - массив из трех символьных строк
Names person = { "Иванов", "Иван", "Иванович" };
Имеется еще один способ введения синонимов, который был изначально включен в язык C++, но сейчас не рекомендуется использовать.
Синонимы типов можно определять с помощью оператора typedef.
Применить его в шаблонных классах с формальными типами не возможно,
кроме этого его синтаксис более сложный для понимания, чем using. Однако разработано огромное количество различных библиотек, в которых
он используется. Ниже показано, как с помощью typedef записать те же
синонимы типов, которые были записаны с помощью using:
165:
166:
167:
typedef double angle;
typedef char *String;
typedef String Names[3];
1.3. Динамическое распределение памяти
Программа, загруженная в оперативную память, состоит из следующих сегментов (рис. 2):
 CODE – содержит машинные команды;
 DATA – содержит константы и глобальные переменные;
 STACK – программный стек, в который помещаются код возврата из процедур, фактические параметры процедур, локальные
переменные процедур;
 HEAP – динамически распределяемая память, предназначенная
для повторного использования.
16
оперативная память
программа
CODE
машинные
команды
DATA
константы и
глобальные
переменные
STACK
программный
стек
mov a, 3
int m
Time * p
HEAP
динамически
распределяемая память
(*p)
тип Time
Рис. 2. Сегменты программы, размещенной в оперативной памяти
Оператор new обеспечивает выделение в динамически распределяемой памяти неименованной переменной заданного типа. В качестве параметра в операторе указывается тип создаваемой переменной.
Оператор new возвращает указатель на выделенную переменную. В
том случае, когда память не может быть выделена, оператор new возвращает 0. Освобождение памяти осуществляется оператором delete. В операторе delete задается указатель, содержащий адрес
освобождаемой памяти.
168:
169:
170:
float *fptr; //объявление указателя
fptr = new float; //создание неименованной вещественной
//переменной в куче
delete fptr; //освобождение памяти
Для распределения памяти под массив используется специальная форма оператора new, в которой после имени типа указывается
размер массива. Для освобождения памяти в данном случае нужно
обязательно использовать оператор delete[], иначе будет утечка
памяти. В следующем примере указатель ia – это переменная, распределяемая в программном стеке. Ее значение может быть изменено.
После выделения памяти под массив целых чисел, в переменную ia
заносится адрес массива, созданного в куче. Массив с адресом ib
17
создается в программном стеке. При этом ib – это константа типа
указатель на целое число, то есть изменить ib невозможно. Массив
из программного стека удаляется автоматически при выходе из функции.
171:
172:
173:
174:
175:
176:
177:
178:
179:
void f(){
int *ia=new int[10];
int ib[20];
if(ia!=0)
for(int i=0;i<10;i++)
ia[i]=0;
...
delete[]ia;
}
В куче может быть выделена память под переменную любого
типа, в том числе и под структуру. Для обращения к полям неименованной переменной типа структура, заданной указателем, в С++ предусмотрен специальный оператор "->".
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
struct Time{ //объявление структуры
int hour;
int minute;
int second;
};
void main(){
Time t; //объявление переменной типа структуры,
//она размещается в программном стеке
Time *pt; //объявление указателя на структуру,
//самой переменной типа структуры еще нет
pt=new Time; //создание неименованной структуры в куче
//и занесение указателя на нее в переменную pt
t.hour=12;
(*pt).hour=11; //использование поля hour структуры
pt->hour=10; //эквивалентная запись для доступа
//к полю структуры
...
delete pt; //освобождение памяти от записи, значение
//переменной pt становится неопределенным
}
18
1.4. Дополнительные операторы присваивания
Составной оператор присваивания
Составной оператор присваивания в общем виде может быть записан как a op=b, где op – арифметическая или логическая операция, a – изменяемый операнд, b – операнд, в котором указывается, на
сколько изменяется операнд a. Составной оператор присваивания
равносилен оператору присваивания a=a op b. Имеются следующие составные операторы присваивания: *=, /=, %=, +=, -=, >>=, <<=,
&=, ^=, |=.
195:
196:
197:
sum+=1;
product*=c;
sum+=ia[i];
//эквивалентно sum=sum+1;
//эквивалентно product= product*c;
//эквивалентно sum= sum+ia[i];
Операторы инкремента и декремента
Операторы инкремента и декремента представляют собой унарные операции увеличения "++" и уменьшения "--" операнда на единицу. В префиксной форме данных операторов сначала выполняется
действие, а потом используется полученное значение. В постфиксной
форме выполняется действие над операндом, но возвращается то значение операнда, которое было до выполнения действия.
198:
199:
array[++top]=value; //префиксная форма эквивалентна
//top=top+1; array[top]=value;
value=array[top--]; //постфиксная форма эквивалентна
//value=array[top]; top=top-1;
1.5. Инструкции
Составной оператор
Чтобы задать несколько операторов там, где по синтаксису ожидается один, применяется составной оператор, также называемый
блоком. Так как объявление переменных – это оператор, то в составном операторе могут быть объявлены локальные переменные. Они
перестают существовать при завершении составного оператора.
19
200:
201:
202:
203:
204:
205:
206:
if(x>1)
{
//начало составного оператора
float y; //объявление локальной переменной
y=2*x;
...
}
x=y; //ошибка - переменная y вне блока не существует
Ветвление
Инструкция ветвления имеет две формы: сокращенную
if(выражение) оператор1
и полную
if(выражение) оператор1 else оператор2.
В сокращенной форме оператор1 выполняется, если выражение принимает значение true или приводится к нему. В полной
форме оператор1 выполняется, если выражение равно true, в
противном случае выполняется оператор2.
207:
208:
209:
210:
if(counter)
counter--;
else
counter=10;
Инструкция выбора
Инструкция выбора switch предназначена для выполнения одного из набора взаимоисключающих действий. В инструкции switch
используется оператор break, который прерывает выполнение
switch и выполняет переход к оператору, следующему за инструкцией выбора.
211:
212:
213:
214:
215:
216:
217:
218:
219:
void main(){
int n;
int cnt0,cnt1,cnt2; //объявление переменных
...
switch(n) { //выражение целого типа
case 0:
//0 – константа
++cnt0; break;
case 1:
//1 – константа
++cnt1; break;
case 2:
++cnt2; break;
default: //все остальные случаи
20
220:
221:
++cnt; break;
}; //конец switch
}
Самая распространенная ошибка при использовании switch –
пропуск оператора break. Если необходимо выполнить одни и те же
действия при разных значениях выражения выбора, то константы выбора должны следовать друг за другом.
222:
223:
224:
225:
226:
227:
228:
switch(n){
case 0:
++cnt0; break;
case 1:
++cnt1; break;
case 2: //при отсутствии break будут выполняться
//следующие case
case 3:
case 4:
++cnt; break;
};
Инструкция цикла
Инструкция цикла с предусловием имеет следующую общую
форму записи: while(выражение) оператор. Оператор выполняется до тех пор, пока выражение не равно нулю.
229:
230:
231:
232:
233:
234:
235:
236:
237:
238:
239:
240:
//Обнуление элементов массива
float a[10];
int i=0;
while(i<10){
a[i]=0;
i++;
}
//Определение длины символьной строки
int len=0;
char *tp="abcde";
while(*tp++)++len;
cout<<len;
Инструкция цикла с постусловием имеет следующую общую
форму записи: do оператор while (выражение). Выход из
цикла выполняется, если выражение равно false.
21
241:
242:
243:
244:
245:
int cnt=10;
do{
--cnt;
a[cnt]=0;
}while (cnt>0);
Итеративный цикл имеет следующую общую форму записи:
for (инициализация; выражение1; выражение2) оператор. Инициализация предназначена для выполнения некоторых
действий перед входом в цикл, как правило, это инициализация счетчика цикла. Инициализация может содержать несколько операторов, разделенных запятой. Выражение1 управляет последовательностью выполнения цикла, выражение2 изменяет переменную цикла.
Последовательность действий при выполнении оператора цикла
такова: сначала выполняется инициализация. Далее, если выражение1 равно true, то выполняется оператор, а затем вычисляется выражение2. Цикл повторяется, начиная с вычисления выражение1. Если выражение1 равно false, то цикл завершается.
246:
247:
248:
249:
250:
251:
252:
253:
254:
//обработка элементов массива
const int sz=24;
int ia[sz];
for(int i=0;i<sz;++i)
ia[i]=i;
//вычисление факториала
int k, product;
for(k=10,product=1; k>0; k--)
product=k*product;
Еще один пример – выполнение действий со списком
(см.рис. 3). Здесь переменная цикла – текущий указатель на элементы
списка:
255:
256:
for(p=head; p!=0; p=p->next)
p->info=2;
22
info
head
next
info
next
info
0
p
Рис. 3. Список
Имеется вторая форма итерационного цикла, предназначенная
для обхода всех элементов некоторого контейнера. Ниже приведен
пример перебора всех элементов массива:
257:
258:
259:
260:
261:
double arr[] = { 4.1, 3.2, 11,9 };
double sum = 0.0;
for (double& a : arr) {
sum = sum + a;
}
В данном примере переменная цикла объявлена как ссылка на тип
double, поэтому ей поочередно присваиваются ссылки на элементы
массива. Если же объявить не ссылочный тип, то будет выполняться
копирование каждого элемента массива в переменную цикла. На самом деле в использовании контейнера элементов в данном виде цикла
кроется довольно сложный механизм так называемых итераторов из
стандартной библиотеки C++. Поэтому для глубокого понимания
цикла, основанного на диапазоне, требуется освоить указанный материал. В приведенном примере использование массива в качестве контейнера становится возможным благодаря приведению его к классу,
имеющему методы begin() и end().
Операторы безусловного перехода
Оператор break прекращает выполнение ближайшего вложенного оператора while, do, for, switch.
262:
263:
264:
265:
266:
267:
location=-1;
for(int ix=0; ix<size; ++ix)
if(val==ia[ix]){
location=ix;
break; //выход из цикла
}
23
Оператор continue прекращает выполнение только текущей
итерации в инструкции цикла и передает управление на конец тела
цикла.
268:
269:
270:
271:
while(counter>0){
if(sum==0) continue; //обход последующих операторов
++counter;
};
Оператор безусловного перехода имеет вид goto метка. Метка – это идентификатор. Оператор goto не должен выполнять переход вперед через объявление переменных.
272:
273:
274:
275:
276:
while(counter>0){
if(sum==0) goto m; //обход последующих операторов
++counter;
};
m: counter=0;
1.6. Функции
Объявление функции – это заголовок со списком типов формальных параметров, записанный до полного определения функции.
277:
278:
279:
280:
281:
282:
283:
284:
285:
286:
287:
//нахождение куба числа (передается указатель на целое
//число, и это число заменяется своим кубом)
#include <iostream>
void cube(int *); //объявление функции
void main(){
int number=5;
cube(&number);
cout<<”Куб числа = ”<<number;
}
void сube(int *nptr) { //определение функции
*nptr=*nptr * *nptr * *nptr;
}
Определение функции состоит из объявления и тела функции:
тип идентификатор([список аргументов]) {тело функции}.
Выход из функции и возврат значения выполняются оператором
return. Если предполагается, что функция не должна ничего возвращать, то ей указывают тип возвращаемого значения void. Тип
возвращаемого значения может быть любым стандартным типом (на24
пример, int или double), производным типом (например, int& или
double*), пользовательским типом (например, полученным с помощью enum или struct).
Параметры передаются либо по значению (в том числе можно
передавать значение адреса переменной), либо по ссылке:
288:
289:
290:
291:
292:
293:
//Передача параметров по значению
double max(double x1, double x2){
if(x1>x2)
return x1; //выход из функции и возврат значения
else
return x2; //выход из функции и возврат значения
}
При вызове функции указывается ее идентификатор и фактические параметры. Функция, возвращающая значение, обычно вызывается в выражении.
294:
295:
296:
297:
298:
void main(){
double a1=3,a2=2,a3;
a3=max(a1,a2); //вызов функции,
//a1 и a2 – фактические параметры
cout<<a3;
}
Если функция не возвращает значения, то используется оператор return без параметров. В такой функции, если она завершается
естественным образом, оператор return может быть опущен.
Передача адреса переменной в качестве параметра позволяет
изменять значение внешних переменных внутри функции:
299:
300:
301:
302:
303:
304:
305:
306:
307:
308:
void swap(double *a, double *b){ //передача параметров
//по значению, но значениями являются адреса a и b
double c;
c=*a;
*a=*b;
*b=c;
}
void main(){
double x=5,y=1;
swap(&x,&y); // передача фактических адресов
}
25
Передача параметра по ссылке реализуется как передача адреса
параметра. Так как ссылка, по определению, есть синоним переменной, то при вызове функции формальный параметр становится синонимом фактического параметра. Поэтому изменение формального параметра, переданного по ссылке, приводит к изменению фактического
параметра.
309:
310:
311:
312:
313:
314:
315:
316:
317:
318:
void swap(double &a, double &b){
double c;
c=a;
a=b;
b=c;
}
void main() {
double x=5, y=1;
swap(x,y); //в качестве фактических параметров
//передаются синонимы
}
Так как в С++ отсутствует высокоуровневое понятие массива, то
передача массива в качестве параметра сводится к передаче указателя
на начальный элемент массива. Следствием этого является необходимость передачи в качестве параметра функции размера массива.
319:
320:
321:
322:
323:
324:
325:
326:
327:
328:
double sum(int n, double *a) {
double s=0.0;
for(int i=0;i<n;i++)
s=s+a[i];
retutn s;
}
void main(){
double arr[3]={3,2,1};//arr – указатель на массив
cout<<sum(3,arr);
}
Для передачи адреса массива возможны следующие эквивалентные формы записи:
329:
330:
331:
double sum(int n, double a[10]){...}
double sum(int n, double a[]){...}
double sum(int n, double *a){...}
26
Передача параметров по значению при вызове функции предполагает, что копия параметра помещается в программный стек. Кроме
этого, все локальные переменные функции размещаются в стеке при
каждом входе в процедуру. Это позволяет реализовать рекурсивный
вызов функций, то есть вызов функции в своем же теле.
332:
333:
334:
335:
336:
337:
338:
339:
340:
int fact(int k){
if(k==1)
return 1;
else
return k*fact(k-1); //рекурсивный вызов
}
void main(){
cout<<fact(3);
}
Перегрузка функций
В С++ допускается объявление функций с одинаковыми именами, но различными списками формальных параметров. При трансляции выполняется так называемое декорирование (искажение) имен.
Оно заключается в том, что транслятор искажает имена, формируя
сигнатуры функций. Сигнатура функции – это внутреннее имя
функции, которое образуется добавлением к заданному имени функции обозначений типов параметров. Например, для функций int
square(int),
double
square(double),
Complex
square(Complex) будут сформированы сигнатуры приблизительно
следующего вида: square$i, square$d, square$Complex.
Конкретный способ искажения имен зависит от версии транслятора. В
месте вызова функции транслятор формирует сигнатуру функции и
проверяет, имеется ли в таблице имен такая сигнатура. В сигнатуру не
включается тип возвращаемого значения, так как в контексте вызова
функции невозможно однозначно определить необходимый тип возвращаемого значения как, например, в выражении cout <<
square(2).
341:
342:
343:
struct Complex{double Re; double Im;};
int square (int x){return x*x;}
//функция для целых чисел
double square (double x) {return x*x;}
//функция для вещественных чисел
27
344:
345:
346:
347:
348:
349:
350:
351:
352:
353:
Complex square(Complex x){
//функция для комплексных чисел
Complex w;
w.Im=2*x.Im*x.Re;
w.Re=x.Re*x.Re-x.Im*x.Im;
return w;
}
void main(){
double z=3.1
cout<<square(z);
}
Шаблоны функций
Шаблон функции позволяет создать обобщенное описание
функции без указания фактических типов параметров. По мере необходимости из шаблона генерируются функции с конкретными типами
параметров. Шаблон записывается в общем виде так:
template<список-формальных-типов> функция.
Каждый раз, когда транслятор встречает в исходном тексте программы вызов функции, имеющей имя, совпадающее с именем шаблона, он транслирует шаблон функции, подставляя вместо формальных типов параметров фактические типы. Формальные типы могут
заменяться любыми фактическими, в том числе пользовательскими
типами.
354:
355:
356:
357:
358:
359:
360:
361:
362:
363:
364:
365:
366:
367:
368:
369:
370:
//шаблон функции определения максимального
//из трех чисел
template<typename T>
T maximum(T value1, T value2, T value3){
T max=value1;
if(value2>max)
max=value2;
if(value3>max)
max=value3;
return max;
}
void main(){
int i1=3, i2=3, i3=-5, ir;
double d1=3.14, d2=0.15, d3=8.1, dr;
ir=maximum(i1, i2, i3); //целые параметры
dr=maximum(d1, d2, d3); //вещественные параметры
//...
}
28
Реализация шаблонов функций базируется на механизме перегрузки функций. При создании параметризованных функций генерируются функции с одинаковыми именами, но из-за декорирования, в
котором используются типы параметров, сигнатуры оказываются различными. Если же транслятор встречает параметризованную функцию с теми же типами параметров, которые использовались ранее, то
новая функция не генерируется, а используется ранее полученная
функция. Очевидно, что нельзя задавать шаблон, в котором формальный тип объявлен только для возвращаемого значения, а не для параметров, так как такие функции имели бы одинаковые декорированные
имена.
Заголовочные файлы и файлы реализации
В С++ отсутствует поддержка модульности программ на уровне
языка программирования. Модульность программного материала
формируется на уровне соглашений. Принято, чтобы объявления
функций располагались в файлах с расширением .h, а определения
функций – в файлах с расширением .cpp (рис. 4).
//файл MyMath.h
int abs(int i);
//файл MyMath.cpp
int abs(int i)
{return (i<0 ? –i : i);}
//файл main.cpp
#include <iostream>
#include "MyMath.h"
void main(){
cout<<abs(2*(-3));
}
Рис. 4. Файлы одной программы
Компилятор транслирует cpp-файлы, включая в них заголовочные файлы в соответствии с директивами #include. В результате
получаются объектные модули. В одном из объектных модулей оказывается реализация функции, полученная из ее определения. А другие модули используют из заголовочных файлов определение функции, что позволяет им правильно использовать эту функцию. При
компоновке объектных модулей в загрузочный модуль, ссылки заменяются на реальный адрес функции.
29
Лямбда-функции
Лямбда-функция – это неименованная функция, записанная
непосредственно в месте использования ее в качестве объекта, передаваемого для последующего исполнения. Программный код с лямбда-функциями выглядит менее загроможденным, чем код с отдельными объявлениями именованных функций, которые используются
однократно. В общем виде лямбда-функция записывается следующим
образом: [список-захвата](список-параметров) -> тип
{ код }, где список-параметров содержит формальные параметры, тип – это тип возвращаемого значения, код – тело функции,
список-захвата – список переменных, доступных в месте объявления, которые должны существовать в момент выполнения лямбдафункции.
В следующем примере в именованной функции sum последний
параметр – функция одного аргумента для преобразования каждого
параметра для суммирования:
371:
372:
373:
double sum(double x1, double x2, double x3, double
op(double)) {
return op(x1) + op(x2) + op(x3);
}
В качестве функции преобразования здесь может быть подставлена
любая функция, имеющая подходящую сигнатуру. Используем для
этого лямбда-функцию, в которой опускаем тип возвращаемого значения, т.к. он может быть выведен компилятором:
374:
auto r = sum(3.0, 2.0, 7.0,
[](double x) {return x * x;});
В теле лямбда-функции можно использовать переменные из окружения, в котором она определяется. Они должны быть указаны в
списке захвата и, в результате, оказываются доступными в момент
выполнения лямбда-функции. Для реализации данного механизма
вместо лямбда-функции создается объект анонимного класса, в котором и хранятся либо копии переменных из списка захвата, либо ссылки на них. Реальная исполняемая функция – метод указанного объекта
– в месте вызова выполняет код, записанный в лямбда-функции, с
30
подстановкой захваченных значений или ссылок вместо указанных
переменных. Изменим функцию из предыдущего примера следующим образом:
375:
376:
377:
template <typename T, typename TOp> T sum(T x1, T x2, T
x3, TOp op) {
return op(x1) + op(x2) + op(x3);
}
В лямбда-функции, которая передается в качестве последнего
параметра, используем переменную из окружения, указав ее в списке
захвата:
378:
379:
380:
int two = 2;
r = sum<double>(3.0, 2.0, 7.0,
[two](double x) -> double {return pow(x, two); });
В примере показан захват значения переменной, т.е. ее копии.
Для захвата ссылки нужно перед именем переменной указать символ
&. Если в начале списка захвата написать символ «=», то не указанные в списке переменные (но используемые в теле лямбда-функции)
будут передаваться по значению, а если написать символ «&», то они
будут передаваться по ссылке:
381:
382:
383:
384:
385:
386:
[]
// без захвата переменных
[=]
// захват всех переменных по значеню
[&]
// захват всех переменных по ссылке
[t, &q]// захват переменной t по значению,
// переменной q – по ссылке
[&, t] // захват всех переменных, кроме t, по ссылке
[=, &q]// захват всех переменных, кроме q, по значению
Глава 2. ОБЪЕКТНАЯ МОДЕЛЬ ПРОГРАММЫ
Рассмотрим определения понятия объект с разных точек зрения [1]:
1) с точки зрения человеческого восприятия, объект – это осязаемый или видимый предмет, имеющий определенное поведение;
2) с точки зрения исследователя в некоторой предметной области объект – это опознаваемый предмет или сущность (реальная или
31
абстрактная), имеющие важное функциональное значение в данной
предметной области;
3) с точки зрения проектирования системы, объект – это сущность, обладающая состоянием, поведением и индивидуальностью.
4) с точки зрения программирования, объект – это совокупность
данных (переменная), существующая в машинном представлении как
единое целое, допускающее обращение по имени или указателю.
Далее будем рассматривать объект с точек зрения проектирования системы и программирования. Группы объектов могут иметь
схожие черты, которые составляют понятие класса. Класс – это описание структуры и поведения схожих объектов. Будем использовать
понятия «экземпляр класса» и «объект» как синонимы.
2.1. Объявление классов и создание объектов в программе на С++
Тип класс задает модель атрибутов (поля структуры) и операций (функции, связанные со структурой). Определение класса может
содержать определения методов, либо объявления методов. В последнем случае определения методов должны быть даны вне объявления
класса.
387: class Time{
388: private:
389:
int hour; //объявление поля
390:
int minute;
391:
int second;
392: public:
393:
//объявления методов
394:
Time(); //конструктор
395:
void setTime(int, int, int);
396:
void print24();
397:
void print12();
398: };
В примере зарезервированные слова private, public –
это модификаторы доступа, они обеспечивают управление доступа к
элементам класса. Указание модификатора private запрещает доступ к полям и методам извне класса, public – позволяет доступ извне класса. Определение метода, записанное вне объявления класса,
должно содержать название класса.
32
399: //Определения методов класса
400: Time::Time(){ //инициализирует поля данного класса
401: hour=0;
402: minute=0;
403: second=0;
404: }
405: void Time::setTime(int h, int m, int s) {
406: hour=h;
407: minute=m;
408: second=s;
409: }
410: void Time::print24(){
411: cout<<hour<<”:”<<minute<<”:”<<second;
412: }
413: void Time::print12(){
414: if(hour==0||hour==12)
415:
cout<<12;
416: else
417:
cout<<hour%12;
418: cout<<”:”<<minute<<”:”<<second;
419: }
Объект класса – это переменная данного класса. При выполнении оператора определения переменной вызывается конструктор
класса.
420: void main(){ // головная функция
421:
Time t; //вызов конструктора класса Time::Time()
422:
t.setTime(14,32,11);
423:
t.print24();
424:
t.print12();
425:
Time *tptr;
426:
tptr=new Time; //динамическое выделение памяти под
//объект и вызов конструктора
427:
(*tptr).setTime(1, 2, 3);
428:
delete tptr; //удаление объекта, на который
//указывает tptr
429: }
Если в классе не объявлен конструктор, то он создается компилятором автоматически. Если в классе имеется несколько конструкторов, то используется механизм перегрузки функций, то есть конструкторы должны различаться списками параметров.
33
2.2. Состояние
Состояние объекта – перечень всех возможных свойств объекта и текущих значений каждого из этих свойств. Перечень свойств
объекта (абстракции сущности) – как правило, статический, так как
эти свойства отражают неизменяемую основу природы объекта.
Свойства объекта характеризуются своими значениями, которые могут быть либо простыми количественными значениями, либо представляют собой состояния других объектов и изменяются как правило
динамически.
Приведем пример: технологический производственный процесс,
как объект, имеет простые количественные характеристики: количество выпускаемой продукции в единицу времени, количество работников и т.д. В то же время он характеризуется состоянием таких объектов, как технологические агрегаты, транспортные средства и так
далее, то есть тех объектов, которые могут существовать независимо
от технологического процесса и над ними могут совершаться определенные действия.
2.3. Поведение
Поведение характеризует то, как объект воздействует на другие
объекты или подвергается воздействию со стороны других объектов с
точки зрения изменения состояния этих объектов и передачи сообщений. Операция (действие, сообщение, метод, функция) – это определенное воздействие одного объекта на другой с целью вызвать соответствующую реакцию.
Категории операций над объектами:
1) функция управления – операция, которая изменят состояние
объекта;
2) функция доступа – операция, дающая доступ для определения состояния объекта без его изменения (операция чтения);
3) функция реализации – операция над информацией из внешних источников, не изменяющая состояния данного объекта;
4) вспомогательная функция – операция, используемая вышеперечисленными операциями, и не предназначенная для самостоятельного использования другими объектами;
5) конструктор – операция инициализации объекта;
6) деструктор – операция разрушения объекта.
34
Совокупность всех методов, относящихся к конкретному объекту, образует протокол объекта. Протокол инкапсулирует (охватывает) внутреннее состояние объекта. Наличие внутреннего состояния
означает, что порядок изменения состояния определяется последовательностью операций над объектом и предшествующим его состоянием. Под термином инкапсуляция подразумевают указанное сокрытие
внутренней структуры данных и реализации методов объекта от остальной программы.
Рассмотрим пример, в котором конструируется класс "Стек".
Методы класса реализуют следующие функции: push – добавление
элемента в стек; pop – возврат значения элемента из вершины и удаление его; isEmpty – возвращает true, если стек пуст; isFull –
возвращает true, если стек заполнен; copyTo – копирует данный
стек в другой стек.
430: class Stack{
431: private:
432:
int * s; //указатель на первый элемент массива
433:
int top; //указатель на вершину стека
434:
int size; //размер стека
435: public:
436:
void push(int i);
//управление
437:
int pop();
//управление
438:
bool isEmpty();
//доступ
439:
bool isFull();
//доступ
440:
void copyTo(Stack *st); //реализация
441:
Stack(int sz);
//конструктор
442:
~Stack();
//деструктор
443: };
В С++ у конструктора нет имени. Однако для удобства в исходном коде конструкторам приписывают имя класса. Можно объявить
несколько конструкторов, отличающихся списками параметров. Деструктор объявляется как имя класса с символом "~". Он всегда один
и не имеет параметров.
Конструктор вызывается сразу же после выделения памяти под
объект. Его назначение – инициализация полей объекта. Если ни один
конструктор не объявлен в классе, то компилятор сам генерирует код
конструктора без параметров. Назначение деструктора – выполнение
35
действий по освобождению ресурсов. Если деструктор не объявлен в
классе, то компилятор сам генерирует код деструктора.
Существует два способа создания объектов: объявление локальных переменных и распределение объектов в динамически распределяемой памяти с помощью оператора new. Локальные объекты создаются в программном стеке и удаляются автоматически при завершении выполнения функции. Динамически распределенные объекты
должны удаляться явно при помощи оператора delete, в противном случае не удаленные объекты будут оставаться в памяти как "мусор".
Рассмотрим продолжение предыдущего примера со стеком. Ниже приводится реализация методов класса и головная функция, создающая локальный объект и динамически распределенный объект.
444: Stack::Stack (int sz) {
445: size=sz;
446: s=new int[size];//массив элементов(s – указатель на него)
447: top=-1;
//несуществующий индекс массива
448: };
449: Stack::~Stack(){
450: delete []s;
451: };
452: int Stack::pop(){
453: int last=s[top]; //значение на вершине стека
454: top=top-1;
455: return last;//возвращение значения выталкиваемого
//элемента
456: };
457: bool Stack::isEmpty(){
458: return top<0;
459: };
460: bool Stack::isFull(){
461: return top>=size-1;
462: };
463: void Stack::copyTo(Stack *st){
464: while(!st->isEmpty())
465:
st->pop();
466: for(int i=0; i<=top; i++)
467:
st->push(s[i]);
468: };
469: void Stack::push(int i){
470: if(!isFull()){
471:
top=top+1;
472:
s[top]=i;
473: }
36
474: };
475: void main(){
476: Stack st1(10); //создание локального объекта st1
477: Stack *pst2;
478: pst2=new Stack(10); //создание объекта в куче
479: st1.push(77);
480: pst2->push(33);
481: pst2->push(st1.pop()); //изменение состояния стека
482: pst2->copyTo(&st1);
483: delete pst2; //вызов деструктора Stack::~Stack()
484: } //здесь вызывается деструктор для локального объекта
2.4. Индивидуальность
Индивидуальность – это совокупность тех свойств объекта, которые отличают его от всех других объектов. Структурная неопределенность – это нарушение индивидуальности объектов.
Для создания тождественных объектов используются копирующие конструкторы. Копирующий конструктор – это конструктор, в списке формальных параметров которого стоит ссылка на объект того же класса. Если конструктор копирования не объявлен в
классе, то компилятор генерирует его. Конструктор копирования по
умолчанию реализует побайтовое копирование. В следующем примере имеет место структурная неопределенность, т.к. после побайтового
копирования два экземпляра стека ссылаются на один и тот же массив. В результате при разрушении этих объектов делается две попытки удалить один и тот же массив.
485: void main(){
486: Stack st1(10), st2(10);
487: st1=st2;
488: } //ошибка освобождения памяти при выполнении
//деструктора второго объекта
Если объект класса имеет сложную структуру, не допускающую
побайтового копирования, необходимо разрабатывать конструктор
копирования в явном виде.
489: class Stack{
490: int size; int top; int *s;
491: public:
492: Stack(int sz){
493:
size=sz; top=-1;
37
494:
s=new int[size];
495: }
496: Stack(const Stack &st){// копирующий конструктор
497:
size=st.size;
498:
top=st.top;
499:
s=new int[size];
500:
for(int i=0; i<=top; i++) //копирование массива
501:
s[i]=st.s[i];
502: }
503: };
504: void f(Stack st){
//при вызове функции создается таблица адресов и
//значений. В нее помещается копия фактического
//параметра - объекта класса Stack
505: //...
506: }
507: void main(){
508: Stack st1(100);
509: // ...
510: Stack st2(st1); //вызов конструктора копирования
511: f(st1); // передача параметра по значению
512: }
2.4. Инкапсуляция
Инкапсуляция – это объединение данных и поведения в один
объект и сокрытие реализации. В С++ сокрытие информации реализуется при помощи модификаторов доступа. Для полей и методов, открытых любому клиенту класса, указывается модификатор public.
Полям и методам, которые могут быть доступны со стороны потомков класса, указывается модификатор protected. Те поля и методы,
которые могут быть использованы только внутри класса, называются
закрытыми и входят в раздел private. Инкапсуляция позволяет
уменьшить сложность программы и уменьшить вероятность внесения
ошибок при ее модификации. Это происходит благодаря тому, что
специфические для класса части кода, подверженные частым изменениям, локализуются, и их изменение не влияет на другие части программы.
Глава 3. ОТНОШЕНИЯ МЕЖДУ КЛАССАМИ
Класс – это описание структуры и поведения объектов, связанных отношением общности. Между классами выделяют три типа
38
отношений: наследование, ассоциация, зависимость. Наиболее общее
отношение – зависимость. Наследование и ассоциация являются частными, но очень важными случаями зависимости.
В языке UML (Unified Modeling Language) класс обозначается
прямоугольником с тремя частями (рис. 5), из которых обязательной
является только первая:
1) имя класса,
2) поля (атрибуты) – указываются названия полей и их типы,
3) методы – указываются сигнатуры методов.
Для полей и методов могут быть указаны модификаторы доступа следующими символами: «+» – public, «-» – private, «#» – protected.
Наследуемые поля при необходимости можно повторить в потомках,
предварив их символом «/». Статические поля и методы подчеркиваются.
List
Element
- head: Element
+ void add(Element*)
+ List& Create()
Рис. 5. Обозначение класса
Графическая нотация предназначена для выражения технических решений с различной глубиной детализации. Поэтому на диаграммах следует показывать только то, что требуется для понимания
определенной идеи и не следует затмевать изображение ненужными
деталями. Например, на концептуальной схеме показываются отношения между классами предметной области. В этом случае часто показывают названия классов без полей и методов. При фиксации начальных решений в проектировании программной системы на диаграммах часто не показывают атрибуты классов, но уделяют внимание методам.
3.1. Отношение наследования
Наследование – это такое отношение между классами, при котором один класс повторяет структуру и поведение другого класса
(простое наследование) или нескольких других классов (множественное наследование). Отношение наследования задает на множестве
классов иерархию, в которой определяются пары суперкласс – подкласс. В подклассе структура соответствующего суперкласса допол39
няется новыми полями, а поведение может дополняться новыми методами, существующие методы могут переопределяться и уточняться.
Обозначение наследования в UML показано на рис. 6.
A
C
Рис. 6. Обозначение наследования
В С++ наследование обозначается следующим образом:
513:
514:
class C : public A{...} // простое наследование
class C : public A, public B{...}
// множественное наследование
В зависимости от модификатора доступа, указанного в наследовании, уровень доступа к полям и методам суперкласса может быть
изменен:
Доступ
в суперклассе
Открытый
Закрытый
Защищенный
Открытый
Закрытый
Защищенный
Открытый
Закрытый
Защищенный
Модификатор
доступа при наследовании
public
public
public
private
private
private
protected
protected
protected
Пример добавления поля и метода:
515: class Circle{
516: private:
517: int radius;
518: public:
519: void show();
520: };
521: class Eye : public Circle{
522: private:
523: int color; // добавленное поле
524: public:
40
Уровень доступа
в подклассе
Открытый
Закрытый
Защищенный
Закрытый
Недоступный
Закрытый
Защищенный
Недоступный
Защищенный
525: void close(); // добавленный метод
526: //...
527: };
528: void main(){
529: Circle c;
530: Eye e;
531: e.show();
532: c.show();
533: e.close();
534: c.close(); //ошибка, этот метод имеется
//только в подклассе
535: }
Следует обратить внимание, что поля класса доступны в методах класса без явного их объявления в методах. Механизм доступа к
полям и методам обеспечивается тем, что объявление метода содержит скрытый (необъявленный) параметр – указатель на объект, для
которого выполняется метод. Этот указатель необходим для доступа к
полям объекта. Неформально объявление метода можно записать таким образом:
536: // Неформальная запись!!!
537: void show(Circle* const this){ // указатель на объект
538:
int r = this->radius;
539:
//...
540: }
Переопределение методов
Переопределение (замещение) – объявление в подклассе метода
с таким же именем и списком параметров, как и в суперклассе. При
этом новая реализация метода в подклассе заменяет реализацию метода в суперклассе. Переопределение может быть не виртуальным,
либо виртуальным. Способ переопределения метода оказывает влияние на то, какой именно метод будет вызываться для объекта подкласса при выполнении программы. Поэтому говорят о раннем связывании и позднем связывании объекта и выполняемого метода.
Не виртуальное переопределение метода приводит к раннему
связыванию объекта и метода, то есть к связыванию на этапе компиляции. В этом случае вызываемый метод для указателя на объект оп41
ределяется по типу указателя или ссылки, а не по типу фактического
объекта.
541: class Circle{
542: public:
543: void show(){}
544:
//...
545: };
546: class Eye : public Circle{
547: public:
548: void show(){}
549: //...
550: };
551: void main(){
552: Circle *pс; //указатель на объект суперкласса
553: pс=new Circle; //фактический объект - класса Circle
554: pс->show(); //вызов метода Circle::show()
555: delete pс;
556: pс=new Eye; //фактический объект - класса Eye
557: pс->show(); //вызов метода Circle::show()
558: delete pс;
559: }
Виртуальное переопределение приводит к тому, что метод, вызываемый для объекта, на который указывает указатель суперкласса,
определяется во время выполнения программы по фактическому типу объекта (рис. 7). Поэтому в данном случае говорят о позднем связывании объекта и метода.
Если в базовом классе имеется виртуальный метод, помеченный
спецификатором virtual, то методы в подклассах, имеющие такую
же сигнатуру считаются также виртуальными, не зависимо от того,
указан для них спецификатор virtual или нет. Рекомендуется для
замещаемых методов указывать спецификатор override, что позволяет избежать ошибок в условиях сложного и глубокого наследования. Если окажется, что методу, помеченному override, не соответствует виртуальный метод в родительском классе, то компилятор сообщит об ошибке.
При проектировании не для всех классов предусматривается
создание объектов. Некоторые классы предназначены только для указания общих свойств своих подклассов на определенном уровне иерархии, которые позже должны быть реализованы в конкретных под42
классах. Классы, для которых не определены реализации объектов,
называются абстрактными классами. Те методы, которые не реализуются в абстрактном классе, называются абстрактными методами. В
С++ они объявляются со спецификатором "=0". Такие методы должны переопределяться в классах потомках.
560: class Figure{
561: int x,y;
562: public:
563: virtual void show()=0;
564: virtual void hide(){}
565: virtual void move(int newx, int newy) {
566:
hide();
567:
x=newx; y=newy;
568:
show();
569: }
570: };
//в классах потомках не переопределяется метод move
571: class Circle : public Figure{
572: int radius;
573: public:
574: void show() override{}
575: void hide() override{}
576: };
577: class Face : public Circle{
578: int eyeColor;
579: public:
580: void show() override {}
581: void hide() override {}
582: virtual void closeEyes(){} //добавленный метод
583: };
584: void main(){
585: Circle *cp;
586: cp = new Face; //фактический объект класса Face
587: cp->show(); //вызывается метод Face::show()
588: cp->move(10,20);
//Вызывается Figure::move(), так как он наследуется.
//А в нем вызываются методы Face::show() и
//Face::hide() в соответствии с фактическим
//классом объекта *cp.
589: delete cp;
590: }
43
CODE
DATA
STACK
HEAP
cp
методы
указатель
на объект
v_ptr
VMT
объект
Рис. 7. Позднее связывание объекта и методов
Таблица виртуальных методов
В C++ позднее связывание реализовано при помощи таблицы
виртуальных методов VMT (virtual method table). На этапе трансляции
транслятор строит VMT для всех классов (см. рис. 8). Количество
элементов в VMT равно количеству виртуальных методов в классе,
включая наследуемые виртуальные методы. Не виртуальные методы в
VMT не заносятся. Таблицы виртуальных методов – это глобальные
константы, представляющие собой массивы адресов методов.
Установление связи между объектом и соответствующими его
фактическому классу методами осуществляется на этапе выполнения
в момент создания объекта в памяти. В создаваемом объекте выделяется дополнительное поле для указателя на VMT. Конструктор заносит в это поле адрес VMT соответствующего класса. Вызов виртуального метода, например,
591: cp->move(10,20);
преобразуется в косвенный вызов функции, в котором используется
смещение адреса метода в VMT:
592: (*(cp->v_ptr[2]))(cp, 10, 20);
44
VMT класса
Figure
машинный код
методов
&show()=0
&hide()
Figure::hide(){}
&move()
cp
объект
класса Circle
объект
класса Circle
VMT класса
Circle
v_ptr
centerX
&show()
&hide()
centerY
&move()
Figure::move(){}
Circle::show(){}
Circle::hide(){}
radius
VMT класса
Face
v_ptr
Face::show(){}
centerX
centerY
radius
&show()
&hide()
Face::hide(){}
&move()
v_ptr
объект
класса Face centerX
&closeEye()
Face::closeEyes(){}
centerY
radius
eyeColor
исполнение
компиляция
Рис. 8. Формирование таблиц виртуальных методов
Уточнение методов
Уточнение метода при наследовании осуществляется в том случае, когда метод подкласса добавляется к существующему методу суперкласса, но при этом повторно использует код метода суперкласса,
вызывая его.
593: class Figure{
594: public:
595: virtual void show(){/*...*/}
596: };
597: class Circle : public Figure{
598: public:
599: void show() override {
600:
Figure::show(); //вызов метода суперкласса
45
601:
//рисование окружности
602: }
603: };
604: class Face : public Circle{
605: public:
606: virtual void show(){
607:
Circle::show(); //вызов метода суперкласса
608:
//рисование других элементов
609: }
610: };
611: void main(){
612: Face F;
613: F.show();
// будут вызваны три метода:
// Figure::show();
// Circle::show();
// Face::show();
614: }
Особенности работы с конструкторами и деструкторами
В С++ автоматическое уточнение методов реализовано для конструкторов и деструкторов. Конструктор играет особую роль, он никогда не является виртуальным. Конструктор не возвращает значение.
В классе может быть объявлено несколько конструкторов с разными
списками параметров, то есть может быть перегрузка конструкторов.
Конструкторы не наследуются. При создании объектов сначала выполняются конструкторы суперкласса, начиная с базового класса.
Каждый конструктор обрабатывает выделенную под объект память
как объект «своего класса», например, инициализирует поля объекта.
По этой причине использование виртуальных методов в конструкторе
может приводить к ошибкам. А вот не виртуальные методы можно
использовать без ограничений.
615:
616:
617:
618:
619:
620:
621:
622:
class A {...}
class B : public A{...}
class C : public B{...}
void main(){
C* cp = new C;// здесь вызываются конструкторы
// всех предков A::A(); B::B() и C::C()
//...
delete cp; // деструкторы вызываются в обратном порядке
}
46
Деструктор выполняет действия по уничтожению объекта. Деструкторы суперклассов выполняются в порядке, обратном выполнению конструкторов. Сначала выполняются команды тела деструктора,
потом разрушаются поля, добавленные к родительскому классу подклассом. После этого выполняются деструкторы родительских классов вплоть до базового. Использование виртуальных методов в деструкторе родительского класса недопустимо, так как поля подкласса к
этому моменту уже разрушены.
Деструкторы не наследуются. Если в объявление класса не добавлен деструктор, то он генерируется компилятором. Деструкторы
могут быть объявлены виртуальными.
623: class B{
624: public:
625: B(){};
626: ~B(){};
627: };
628: class D : public B{
629: public:
630: D(){};
631: ~D(){};
632: }
633: void f(){
634: B *p = new D(); //создается объект класса D
635: delete p;
//ошибка!!! - уничтожается объект класса B
636: }
В данном случае надо объявить виртуальный деструктор, тогда
для полиморфной переменной будет выполняться деструктор, соответствующий фактическому типу объекта:
637: class B{
638: B(){}
639: virtual ~B(){}
640: };
Запрет наследования
В некоторых случаях, чаще всего при создании библиотек,
предназначенных для использования сторонними разработчиками,
может возникнуть необходимость запрета свойств наследования для
47
отдельных методов, либо для класса в целом. Для этого имеется спецификатор final. Если он указан для виртуального метода, то замещение этого метода будет запрещено в подклассах. Если же final
указан для класса, то создавать дочерние классы от этого класса будет
не возможно.
641: class B {
642:
virtual void f() final;
643: };
644: class D final : public B {
645:
// Ошибка – запрещено замещение этого метода
// void f() override;
646: };
647: // Ошибка - запрещено наследование в D
// class C : public D {};
Различие понятий «подкласс» и «подтип»
В языке C++ понятия тип и класс совпадают. Однако при проектировании программных систем очень важным является различие понятий подкласса и подтипа в теоретическом аспекте, т.к. через эти
понятия определяется возможность повторного использования кода.
Некоторый тип S называют подтипом другого типа T, если экземпляр типа S может быть использован в любом месте кода, в котором используется экземпляр типа T. Это требование означает, что для
того, чтобы S являлся подтипом T необходимо, чтобы он имел такие
же открытые методы и поля как в T. Другими словами, подтип внешне должен быть таким же как родительский тип.
Некоторый класс D называют подклассом другого класса B, если он наследует структуру и поведение класса B. Это означает, что
экземпляр подкласса внутренне устроен так же, как экземпляр родительского класса.
Рассмотрим на примере, в каких случаях класс может быть подклассом, подтипом и одновременно и подклассом, и подтипом.
648: class T { // базовый тип
649: public:
650:
virtual void f() = 0;
651:
virtual void g() = 0;
652: };
653: class S1 : public T { // реализация интерфейса
654: public:
48
655:
void f() override { cout << "S1::f"; }
656:
void g() override { cout << "S1::g"; }
657: };
658: class S2 : public T { // реализация интерфейса
659: public:
660:
void f() override { cout << "S2::f"; }
661:
void g() override { cout << "S2::g"; }
662: };
663: class B { // базовый класс
664: public:
665:
int k;
666: public:
667:
void f(){ cout << "B::f"; };
668: };
669: class D1 : public B { // подкласс
670: };
671: class D2 : private B { // подкласс
672: public:
673:
void q(){
674:
cout << this->k;
675:
f();
676:
}
677: };
678: S1 s1;
679: S2 s2;
680: T& t = s1; // S1 - подтип T
681: t.f();
682: t = s2; // S2 - подтип T
683: D1 d1;
684: D2 d2;
685: B& b = d1; // D1 - и подкласс, и подтип B
686: // b = d2; - ошибка, D2 - подкласс, но не подтип B
В примере имеется класс T, содержащий только чистые виртуальные функции. Такой класс называют интерфейсом, а чистые виртуальные функции – абстрактными методами. Классы S1 и S2 реализуют абстрактные методы. S1 и S2 не наследуют структуру T, т.к.
структуры нет, и не наследуют поведение, т.к. абстрактные методы не
задают поведение. Таким образом, S1 и S2 не являются подклассом
T, но при этом экземпляры S1 и S2 могут быть использованы везде,
где требуется интерфейс T. Таким образом S1 и S2 – это подтипы типа T. При этом говорят, что S1 и S2 имеют одинаковый тип, хотя при
этом у них нет ни общей структуры, ни общих методов.
В том же примере, в классе B имеется структура и конкретный
(не абстрактный) метод. Поэтому любой подкласс, в данной случае
D1 и D2, наследует и структуру, и поведение родительского класса.
Класс D1 дает открытый доступ к открытым родительским полям и
49
методам. Следовательно объект класса D1 может использоваться вместо объекта класса B, а это означает, что подкласс D1 одновременно
является подтипом B. Но в классе D2 указано закрытое наследование.
Это означает, что поля и методы родительского класса оказываются в
потомке закрытыми и доступны только в самом потомке. Поэтому
подкласс D1 не является подтипом B.
3.2. Множественное наследование
Необходимость во множественном наследовании возникает в
тех случаях, когда в одном классе необходимо совместить свойства
базовых понятий из различных предметных областей. В C++ имеется
возможность объявить класс потомком более одного базового класса.
Однако множественное наследование может приводить к существенным проблемам. Две основные из них: неопределенность в наименовании элементов суперклассов и повторное наследование.
Неопределенность в наименовании
Неопределенность в наименовании возникает в том случае, когда в разных суперклассах одного подкласса используются одинаковые имена для элементов класса – полей или методов.
687: class Amplifier{
688: protected:
689: float maxPower; //максимальная мощность на нагрузке
690: //...
691: };
692: class Chip{
693: protected:
694:
float maxPower; //максимальная потребляемая мощность
695:
//...
696: };
697: class DCAmplifier : public Amplifier, public Chip{
698: public:
699: void f(float x)
700: //...
701: };
702: void DCAmplifier::f(float x) {
703: maxPower=x;
//неопределенность: какое из полей
//суперклассов должно быть изменено?
704: //...
705: }
50
Для решения проблемы в С++ используется дополнительный
квалификатор, указывающий на соответствующий класс:
706: void DCAmlifier::f(float x){
707: Chip::maxPower=x;
708: Amplifier::maxPower=0.5*x;
709: }
Решение проблемы неопределенности наименования для методов класса.
710: #include <iostream>
711: void printA(){ //внешняя глобальная функция
712: cout<<"это внешняя printA"<<endl;
713: };
714: class Base1{
715: public:
716: int a;
717: Base1(int m) {a=m;};
718: void printA(){
719: cout<<"это Base1::a "<<a<<endl;
720: }
721: };
722: class Base2{
723: public:
724: float a;
725: Base2(float r) {a=r};
726: void printA(){
727:
cout<<"это Base2::a "<<a<<endl;
728: }
729: void print2A(){
730:
cout<<"это 2*Base2::a "<<2*a<<endl;
731: }
732: };
733: class Derived : public Base1, public Base2{
734: public:
735: char a;
736: Derived (char l, int m, float r): Base1(m), Base2(r)
737:
{a=l;};
738: void printA(){
739:
cout<<"это Derived::a "<<a<<endl;
740: }
741: void printAll(){
742:
printA();
743:
Base1::printA(); // вызов метода первого предка
744:
Base2::printA(); // вызов метода второго предка
745:
::printA(); // вызов глобальной функции
51
746: }
747: };
748: void main(){
749: Base1 b1(10), *base1Ptr;
750: Base2 b2(2.3), *base2Ptr;
751: Derived d('z', 5, 7.1);
752: b1.printA();
753: b2.printA();
754: d.printA(); //вызывается Derived::PrintA()
755: d.print2A(); //вызывается Base2::print2A(),
//так как он не переопределен
756: d.Base1::printA();
757: d.Base2::printA();
758: base1Ptr=&d;
759: base1Ptr->printA(); //вызывается Base1::printA(),
//так как методы не виртуальные
760: base2Ptr=&d;
761: base2Ptr->printA();//вызывается Base2::printA()
762: }
Повторное наследование
Повторное наследование имеет место в тех случаях, когда суперклассы некоторого подкласса, в свою очередь, являются подклассами одного и того же суперкласса более высокого уровня.
763:
764:
765:
766:
class Element {...};
class Amplifier : public Element {...};
class Chip : public Element {...};
class DCAmplifier : public Amplifier, public Chip {...};
Внешне проблема повторного наследования напоминает проблему неопределенности в наименовании, однако на самом деле имеется принципиальное отличие: сколько бы раз подкласс не наследовал
структуру и поведение общего суперкласса, он детализирует свойства
общего суперкласса, то есть только одного суперкласса.
52
Element
-data
Amplifier
Chip
DCAmplifier
Рис. 9. Диаграмма классов для повторного наследования
На рис. 9 класс DCAmplifier дважды наследует поля класса
Element. В этом случае оказывается, что методы, наследуемые от
первого непосредственного суперкласса, обращаются к одной копии
полей базового класса, а методы, наследуемые от второго суперкласса, обращаются ко второй копии полей базового класса (рис. 10).
поля Element
поля Chip
объект класса Chip
поля Element
поля Amplifier
объект класса Amplifier
поля DCAmplifier
объект класса DCAmplifier
Рис. 10. Повторное наследование полей
Для исключения дублирования структуры базового класса в С++
реализуется механизм виртуального наследования. В этом случае в
записи наследования класса указывают ключевое слово virtual.
53
767:
768:
769:
class Element{/*...*/};
class Chip : virtual public Element{/*...*/}
class Amplifier : virtual public Element{/*...*/}
При виртуальном наследовании в объекте подкласса создается
указатель на структуру, наследуемую от виртуального базового класса. Объем памяти, выделяемой под объект, определяется полями объекта с добавлением указателя. Конструкторы вызываются сначала для
полей виртуального базового класса, потом для указателя на объект
базового класса и далее – для добавленных полей.
Для объектов классов Chip и Amplifier принципиально ничего не изменилось, но доступ к наследуемым полям осуществляется
через указатель (см. рис. 11). Но при множественном наследовании
указатели дают ссылку на структуру объекта виртуального базового
класса, которая наследуется только один раз (см. рис. 12).
770:
class DCAmplifier : public Amplifier, public Chip
{/*...*/};
В результате методы, наследуемые классом DCAmplifier от
непосредственных предков, будут манипулировать одними и теми же
полями структуры, наследуемой от базового класса. Следовательно
проблема повторного наследования решена.
Поля Element
Поля Element
Поля Chip
Поля Amplifier
Объект класса Chip
Объект класса Amplifier
Рис. 11. Изменение структуры объектов класса при виртуальном наследовании
Как известно, в случае простого наследования при создании
объекта выполняются конструкторы всех суперклассов, начиная с базового класса и заканчивая конструктором данного класса. При множественном наследовании эта последовательность для непосредст54
венных суперклассов уточняется в списке инициализаторов в реализации конструктора. Инициализаторы позволяют указать параметры,
с которыми будут вызываться конструкторы суперклассов. Серьезная
проблема заключается в том, что в конструкторах непосредственных
суперклассов могут вызываться различающиеся конструкторы базового класса. Поэтому в том классе, в котором возникает проблема повторного наследования, необходимо явно указать, какой конструктор
виртуального базового класса должен быть вызван.
771: class A {
772: public:
773:
A(){/*...*/}
774:
A(int i){ /*...*/}
775:
A(double d){ /*...*/}
776: };
777: class B : virtual public A {
778: public:
779:
B(): A(7){ /*...*/} //конструктор для A
//с целым параметром
780: };
781: class C : virtual public A {
782: public:
783:
C(): A(3.141){ /*...*/} //конструктор для А
//с действительным параметром
784: };
785: class D : public B, public C {
786: public:
787:
D(): A(3), B(), C(){/*...*/}
788: };
789: void main(){
790: D d(); //вызов конструктора класса А в конструкторах
// В и С игнорируется, вызывается А(3),
//потом выполняется В::В(), потом С::С()
791: }
Это единственная ситуация, в которой в инициализаторах указывается конструктор того класса, который не является непосредственно предшествующим предком. Конструкторы виртуальных классов должны обязательно вызываться первыми, до вызова конструкторов не виртуальных классов. Это делается для того, чтобы избежать
неопределенность в выборе конструктора и его параметров для каждого виртуального базового класса. Деструкторы выполняются в порядке, обратном выполнению конструкторов.
55
поля Element
поля Chip
объект класса Chip
поля Amplifier
объект класса Amplifier
поля DCAmplifier
объект класса DCAmplifier
Рис. 12. Структура класса при повторном наследовании
и объявлении базового класса виртуальным
3.3. Отношение ассоциации
Между экземплярами классов могут возникать связи, имеющие
различный характер. В общем случае связь имеет место, если экземпляр одного класса каким-либо образом использует адрес или ссылку
на экземпляр другого класса. Например, связь имеет место если один
объект вызывает метод другого объекта, другой пример – один объект
создает другой объект и возвращает ссылку на него. Обобщение связей между объектами называют отношением ассоциации классов.
Частным случаем ассоциации является отношение агрегации.
Агрегация обобщает такие связи, в которых объект одного класса
использует в своем составе объекты других классов в качестве составных частей. Обязанностью агрегирующего объекта является предоставление доступа к агрегируемым элементам, иначе говоря, обеспечивается навигация по дереву объектов. Может использоваться такая схема, в которой действия, адресованные агрегату, переадресуются его компонентам, т.е. реализуется делегирование обязанностей.
Обозначения разных видов ассоциации показаны на рис. 13.
В общем случае предполагается, что агрегирующий объект содержит ссылку на агрегируемый объект в некий период своего жизненного цикла, но не отвечает за создание и уничтожение агрегируе56
мого объекта. В этом случае агрегацию еще называют агрегацией по
ссылке.
Если агрегирующий объект определяет время существования
агрегируемых объектов, то говорят о композиции: при создании объекта-агрегата должны создаваться и агрегируемые объекты, а при
удалении объекта-агрегата предварительно должны быть уничтожены
агрегируемые объекты. Композиция известна также как агрегация по
значению.
C1
A
C
C2
P
P
1)
2)
3)
Рис. 13. Обозначения отношений: 1) ассоциация, 2) агрегация, 3) композиция
Перечислим типовые ситуации, в которых возникает отношение
ассоциации классов:
1) методу данного класса передается объект используемого
класса или указатель на такой объект в качестве параметра;
2) поле данного класса является объектом используемого класса,
ссылкой или указателем на такой объект (композиция или агрегация);
3) в методе данного класса создаются локальные объекты используемого класса;
4) метод данного класса выполняет действия над глобальным
объектом используемого класса.
В качестве примера рассмотрим программную модель некоторого электронного устройства. В следующем примере показаны варианты ассоциации классов.
792: #include <iostream>
793: #include <vector>
794: using namespace std;
795: class Chip{
796: public:
797:
void initiate();
798: };
799: class Log{
800: public:
57
801:
802:
void writeInfo(string info){
cout << info.c_str() << endl; // обращение к
// глобальной переменной cout
}
803:
804: };
805: class Sensor {
806: public:
807:
string getValue(){
808:
return "0.6";
809:
}
810: };
811: class Beep{
812: public:
813:
void on(int seconds){ }
814: };
815: class Device{
816: private:
817:
Chip chip; // композиция
818:
vector<Sensor> sensors; // агрегация по ссылке
819: public:
820:
void output(Log& log){ // параметр - ссылка на объект
821:
for (Sensor& sensor : sensors){
822:
log.writeInfo(sensor.getValue());
823:
}
824:
}
825:
void reset(){
826:
Beep beep; // локальный объект
827:
beep.on(10);
828:
chip.initiate();
829:
}
830:
void add(Sensor& sensor) {
831:
sensors.push_back(sensor); // добавляется ссылка
// на объект
832:
}
833: };
834: void main(){
835:
Sensor s1;
836:
Sensor s2;
837:
Device device;
838:
device.add(s1);
839:
device.add(s2);
840:
Log log;
841:
device.output(log);
842: };
В данном примере экземпляр класса Log существует одновременно с
экземпляром класса Device в момент выполнения метода output()
и передается ему в качестве параметра. Это и есть связь – экземпляр
ассоциации между классами. Объект chip класса Сhip объявляется
полем класса Device, тем самым реализуется композиция. В поле
sensors, имеющем тип vector из стандартной библиотеки, хра58
нятся ссылки на объекты класса Sensor, поэтому между Device и
Sensor имеет место агрегация по ссылке. Объект beep является локальным в реализации метода reset(). Объект cout класса
ostream оказывается глобальным по отношению к классу Log и используется в методе writeInfo().
На диаграммах могут быть показаны свойства отношения ассоциации, если это необходимо (рис. 14). Ассоциация может иметь имя,
поясняющее то действие, в котором экземпляры ассоциированных
классов оказываются связанными. Вместо имени на одном или двух
концах ассоциации могут быть записаны роли классов в ассоциации.
На концах ассоциации могут быть указаны стрелки, показывающие
направление зависимости между классами. Для ассоциации может
быть указана множественность, показывающая, какое количество
объектов одновременно оказываются связанными связью данного типа ассоциации. Типовые значения множественности: 1..1 (один к одному), 1..* (один ко многим), *..* (многие ко многим). Для самой ассоциации и для ее концов могут быть указаны стереотипы, поясняющие используемое типовое решение.
Beep
Log
<<parameter>>
<<local>>
Записать
<<global>>
Device
cout : ostream
1
1
1
*
Chip
Sensor
Рис. 14. Детализация свойств ассоциации
В C++ композиция реализуется следующим образом. При выполнении конструктора класса, предварительно выполняются конструкторы для всех его полей. После выполнения деструктора класса
выполняются деструкторы для всех полей. Используемые конструкторы полей при необходимости указываются явно в виде инициализаторов полей. Если в конструкторе класса не указаны необходимые
59
конструкторы полей, то транслятор использует конструкторы без параметров.
843: class Mouth{
844: float capacity;
845: public:
846: Mouth(float c) : capacity(c) { }
847: ~Mouth(){}
848: };
849: class Eye{
850: float radius;
851: int color;
852: public:
853: Eye(float r, int c) : radius(r), color(c) { }
854: ~Eye(){}
855: };
856: class Face{
857: Eye leftEye;
858: Eye rightEye;
859: Mouth mouth;
860: public:
861: Face(float eRadius, int eColor, float mCapacity):
leftEye(3, eColor), rightEye(3, eColor), mouth(10){
862:
leftEye=rightEye;
863: }
864: ~Face(){}
865: };
866: void main(){
867: Face face(5, 1, 10); //здесь вызываются конструктор
//агрегирующего объекта и конструкторы
//агрегируемых объектов
868: } //здесь автоматически вызываются деструкторы
//агрегируемых объектов и деструктор
//агрегирующего объекта
Видимость на уровне классов и на уровне объектов
При рассмотрении отношения использования возникает вопрос:
может ли один объект некоторого класса использовать неоткрытые
поля и методы другого объекта того же класса. В С++ такое использование является корректным, то есть реализуется видимость на
уровне классов.
869: class A{
870: private:
60
871: int m;
872: public:
873: void f(A &p){
874:
p.m=0; //изменяется закрытое поле объекта р
875: };
876: };
877: void main(){
878: A p1,p2;
879: p2.f(p1); //изменение поля p1.m другим объектом
//того же класса
880: }
Важно различать понятия "видимость" и "доступ к элементам
классов". Элементы классов могут быть видимы, но недоступны.
Приведем пример:
881: int m; //глобальная переменная
882: class A{
883: private:
884: int m
885: };
886: class B : public A{
887: void f(){m=0;} //ошибка, A::m описана как private
888: }
Если бы спецификаторы доступа управляли видимостью, то поле
A::m было бы невидимым, и была изменена глобальная переменная m.
3.4. Отношение реализации
В С++ интерфейсом называют абстрактный класс, который содержит только абстрактные методы. Отношение между классом, который использует интерфейс, и самим интерфейсом не является ассоциацией, т.к. объекты абстрактного класса не могут быть созданы, и,
следовательно, данное отношение не может обобщать взаимодействие
объектов. Зависимость между интерфейсом и классом, который реализует абстрактные методы интерфейса, называют реализацией (рис.
15).
889: class IA{
890: public:
891:
virtual void g() = 0;
892:
virtual void f() = 0;
893: };
894: class A : public IA{ // реализация интерфейса
61
895: public:
896:
void g() override {/*...*/};
897:
void f() override {/*...*/ };
898: };
899: class Client{
900: public:
901:
void func(IA& ia){ // использование интерфейса
902:
ia.f();
903:
}
904: };
905: void main(){
906:
A a;
907:
IA& ia = a;
908:
Client client;
909:
client.func(ia);
910: }
зависимость
Client
реализация
«interface»
IA
+f()
+g()
A
+f()
+g()
Рис. 15. Обозначение отношений использования и реализации интерфейса
Хотя отношение реализации очень похоже на наследование, но
таковым не является. При реализации нет наследования структуры и
поведения, т.к. они отсутствуют в интерфейсе.
3.5. Отношение дружественности
Отношения между классами ограничиваются квалификаторами
доступа и последовательностью объявления классов. Гибкость механизма ограничения доступа, точнее – ослабление ограничения, дает
объявление дружественных классов. Для этого в определении класса
указывается, какие другие классы, методы классов или функцииутилиты могут иметь доступ к закрытым элементам данного класса.
911: class Chip {
912: friend class Device; //объявление дружественного класса
913: private:
914: void f(){} //закрытый метод
915: };
916: class Device{
917: Chip chip;
918: public:
919:
void g(){
920:
chip.f(); // закрытая функция здесь доступна
62
921: }
922: };
Аналогично можно объявлять дружественные функции и отдельные методы классов.
923: class Complex{
924: private:
925: double rp;
926: double ip;
927: public:
928: Complex(double a, double b);
929: friend double abs(Complex& x);
930: };
931: Complex::Complex(double a, double b)
932: {rp=a; ip=b;}
933: double abs(Complex& x)
934: {return sqrt(x.rp*x.rp+x.ip*x.ip);} //доступны закрытые
//поля класса
935: void main(){
936: Complex x(3.1,-0.5);
937: cout<<abs(x);
938: };
3.6. Отношение наполнения
Отношение наполнения – это отношение между классами, при
котором классы формируются из общей структуры, называемой
обобщенным классом, путем указания фактических типов вместо
формальных типов (рис. 16). Термины «обобщенный класс», «шаблон
класса», «шаблонный класс» являются синонимами.
T
vector<int>
bind<int>
vector
Рис. 16. Обозначение отношение наполнения
Обобщенный класс представляет собой описание класса с неопределенными предварительно типами, которые указываются в виде
формальных параметров класса. Обобщенный класс обобщает классы
с одинаковыми структурой и поведением. Например, динамически
распределяемые массивы целых чисел и действительных чисел отли63
чаются друг от друга только типом элементов массива, а общая
структура классов и алгоритмы методов одинаковы:
939: class IntArray{//целочисленный массив
940: int *a;
941: int size;
942: public:
943: IntArray(int sz); //создание массива
944: ~IntArray();//удаление массива
945: };
946: class FloatArray{//массив вещественных чисел
947: float *a;
948: int size;
949: public:
950: FloatArray(int sz);
951: ~FloatArray();
952: };
953: IntArray::IntArray(int sz){
954: size=sz;
955: a=new int[size];
956: }
957: FloatArray::FloatArray(int sz) {
958: size=sz;
959: a=new float[size];
960: }
961: IntArray::~IntArray(){
962: delete[] a;
963: }
964: FloatArray::~FloatArray(){
965: delete[] a;
966: }
967: void main(){
968: IntArray intArray(10);
969: FloatArray floatArray(20);
970: }
Шаблон класса позволяет обобщить оба класса в единую запись.
Объявления конкретных классов с заданным типом элементов массива могут быть получены из шаблона путем указания конкретного типа:
971: template<typename T>
972: T* a;
973: int size;
974: public:
975: Array(int sz);
class Array{
64
976: ~Array();
977: void initiate();//инициализация элементов
978: };
979: template<typename T> Array <T>::Array(int sz){
980: size=sz;
981: a=new T[size];
982: }
983: template<typename T> Array <T>::~Array()
984: {delete[] a;}
985: template<typename T> void Array <T>::initiate(){
986: int i;
987: for(i=0; i<size; i++)
988:
a[i]=0;
989: }
990: void main(){
991: Array<int> intArray(10);//создание варианта
//целочисленного массива
992: Array<float> floatArray(20);//создание варианта
//массива действительных чисел
993: intArray.initiate();
994: floatArray.initiate();
995: }
Специализация методов обобщенного класса
Пусть в примере предполагается параметризация шаблона символьным типом, но при этом начальными значениями должны быть
пробелы, а не символы с кодом 0. Тогда необходимо добавить специальную реализацию метода initiate().
996: void Array<char>::initiate(){
997: int i;
998: for(i=0; i<size; i++)
999:
a[i]=’ ’;
1000:};
1001:void main(){
1002: Array <float> floatArray(20);
1003: Array <char> charArray(10);
1004: floatArray.initiate(); //инициализация нулями
1005: charArray.initiate(); //инициализация пробелами
1006:};
65
Эквивалентность типов
Два конкретных класса, полученных из обобщенного класса, являются эквивалентными, если совпадают имена шаблонов и фактические параметры, включая и типы, и константы, имеют одинаковые
значения.
1007: template<typename T, int SIZE>
1008: class Array{
1009:
T *a;
1010: public:
1011:
Array();
1012:
~Array(){delete[] a;}
1013:
void initiate();
1014: };
1015: template<typename T, int SIZE>
1016: Array <T, SIZE>::Array()
1017:
{a=new T[SIZE];};
1018: template<typename T, int SIZE>
1019: void Array <T, SIZE>::initiate(){
1020:
int i;
1021:
for(i=0; i<SIZE; i++)
1022:
a[i]=0;
1023: }
1024: void Array <char, 10>::initiate(){
1025:
int i;
1026:
char blank=’ ’;
1027:
for(i=0; i<10; i++)
1028:
a[i]=blank;
1029: };
1030: void main(){
1031:
Array <float, 15> floatArray;
1032:
Array <char, 10> charArray10;
1033:
Array <char, 6> charArray6;
1034:
Array <char, 6> charArray;
1035: //классы объектов charArray10 и charArray6 различны
1036: //классы объектов charAttay6 и charArray эквивалентны
1037:
floatArray.initiate();
1038:
charArray10.initiate(); //инициализация пробелами
1039:
charArray6.initiate(); //инициализация нулями
1040:
charArray.initiate();
1041: };
Отношения между обобщенными классами
Использование обобщенного класса в конкретном классе показано в следующем примере:
66
1042: class AgrStack{ // стек на базе массива
1043:
int top;
1044:
Array<int> arr;
1045: public:
1046:
AgrStack(int n): arr(n) {top=-1;}
1047:
~AgrStack(){}
1048: void init(){
1049:
arr.initiate();
1050:
}
1051: //...
1052: };
1053: void main(){
1054:
AgrStack st(4);
1055:
st.init();
1056: }
Использование обобщенного класса другим обобщенным классом и дружественные отношения между обобщенными классами показаны в следующем примере:
1057: template<typename Type>class List;
//предварительное объявление шаблонного класса
1058: template<typename InfoType> class Element{//элемент списка
1059: friend class List<InfoType>;
//список - дружественный класс
1060: protected:
1061:
InfoType info;
1062:
Element *next;
1063: public:
1064: Element(InfoType &t): info(t)
1065:
{next=0;}
1066: };
1067: template<typename Type> class List{
1068: protected:
1069:
Element <Type> *head;
1070: public:
1071:
List() {head=0;}
1072:
~List();
1073:
void add(Type &val);
1074: };
1075: template<typename Type> List <Type>::~List(){
1076:
Element <Type> *ptr;
1077:
while(head!=0){
1078:
ptr=head;
1079:
head=head->next;
1080:
delete ptr;
67
1081:
}
1082: }
1083: template<typename Type> void List <Type>::add(Type &val){
1084:
Element <Type> *ptr;
1085:
ptr=new Element<Type>(val);
1086:
ptr->next=head;
1087:
head=ptr;
1088: }
1089: void main(){
1090:
List <int> lst1; //локальные объекты
1091:
List <char> lst2;
1092:
int i=3;
1093:
char ch=’V’;
1094:
lst1.add(i);
1095:
lst2.add(ch);
1096:
//...
1097:
List <int> *plist;
1098:
plist=new List<int>; //динамический объект
1099:
plist->add(i);
1100:
delete plist;
1101: }
В отношении наследования возможны различные сочетания
обобщенных и конкретных классов. Рассмотрим пример, в котором
обобщенный класс является подклассом конкретного суперкласса.
1102: class Base{
1103: float x;
1104: public:
1105:
Base(float newx)
1106:
{x=newx;}
1107: };
1108: template<typename Type> class Derived : public Base{
1109:
Type y;
1110: public:
1111:
Derived(float newx,Type &newy): Base(newx),y(newy){;}
1112: };
1113: void main(){
1114:
char c=’z’;
1115:
Derived <char> d(3.0,c);
1116: }
В следующем примере конкретный класс является подклассом
обобщенного класса:
1117: class IntStack : public Array<int>{
68
1118:
int top;
1119: public:
1120:
IntStack(int n): Array<int>(n){top=-1;}
1121:
int isEmpty(){return top==-1;}
1122:
void initiate(){
1123:
Array<int>::initiate();
1124:
top=-1;
1125:
}
1126: };
1127: void main(){
1128:
IntStack st(10);
1129:
st.initiate();
1130: }
В следующем примере обобщенный класс является подклассом
обобщенного класса:
1131: template<typename Type> class Stack : public Array<Type>{
1132:
int top;
1133: public:
1134:
Stack(int n): Array<Type>(n),top(-1) {;}
1135:
int isEmpty(){return top==-1;}
1136:
void initiate(){
1137:
Array<Type>::initiate();
1138:
top=-1;
1139:
}
1140: };
1141: void main(){
1142:
Stack<char> st(15);
1143:
st.initiate();
1144: }
В стандарт языка C++ входит стандартная библиотека шаблонов, которую разработчики называют неофициальным названием STL
(Standard Template Library). Она предоставляет набор шаблонных
классов контейнеров (стек, вектор, список и т.д.), обобщенных алгоритмов (сортировка, поиск и т.д.), а также вспомогательных функций.
Использование этой библиотеки значительно повышает эффективность и надежность разработки программного обеспечения.
69
Глава 4. ОСОБЕННОСТИ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО
ПРОГРАММИРОВАНИЯ НА С++
4.1. Перегрузка операторов
Для выполнения действий над переменными встроенных типов
в С++ предусмотрен набор операторов (+, -, /, *, ++, -- и др.). Операторы могут быть бинарными (двуместными) и унарными (одноместными) в зависимости от количества аргументов. Одному и тому же
символу операции даже для встроенных типов соответствуют различные операции. Например, оператор умножения "*" для целых и для
действительных чисел реализуется различными последовательностями машинных команд. Такие операторы называются перегружаемыми
или совместно используемыми. С++ предусматривает перегрузку
операторов для новых конструируемых типов данных со следующими
ограничениями:
1) невозможно изменить старшинство операторов;
2) запрещено изменять назначение операторов для встроенных
типов, так как это может привести к изменению языка;
3) запрещено конструировать новые операторы. Например, объявить оператор "**" нельзя.
При создании нового типа автоматически генерируется код для
оператора присваивания "=" и оператора вычисления адреса "&". Оба
эти оператора могут быть перегружены явно.
Функция-оператор может быть или утилитой (в том числе дружественной каким-либо классам), или методом какого-либо класса.
При перегрузке операторов "()", "[]", "->", "=" функция-оператор
должна быть методом класса, для других операторов это необязательно. Если функция-оператор является методом класса, то самый левый
операнд должен быть объектом или ссылкой данного класса. Если же
самый левый операнд должен быть переменной встроенного типа, а
правый – объектом класса, то функция-оператор может быть только
утилитой. Для перегрузки операторов используется зарезервированное слово operator.
Ниже приведен пример перегрузки операторов для типов, не являющихся классом.
1145: struct Complex{float re,im;};
70
1146: Complex operator+(Complex c1,Complex c2) {
1147:
Complex c;
1148:
c.re=c1.re+c2.re;
1149:
c.im=c1.im+c2.im;
1150:
return c;
1151: };
1152: Complex operator*(Complex c1,Complex c2){
1153:
Complex c;
1154:
c.re=c1.re*c2.re-c1.im*c2.im;
1155:
c.im=c1.im*c2.re+c2.im*c1.re;
1156:
return c;
1157: };
1158: void main(){
1159:
Complex x1={1,2};
1160:
Complex x2={3,4};
1161:
Complex x3;
1162:
x3=x1+x2;
1163:
x1=x1+x1; //в сложении передается параметр по значению,
//а не по ссылке
1164:
x3=x1*x2;
1165: }
Перегрузка операторов для класса имеет некоторые особенности. Оператор присваивания не наследуется так же, как не наследуются конструкторы и деструкторы.
1166: class Comp{
1167: protected:
1168:
float re,im;
1169: public:
1170:
Comp(){re=0.0;im=0.0;}
1171:
Comp(float x,float y)
1172:
{re=x;im=y;}
1173: friend Comp operator+(const Comp& c1, const Comp& c2);
1174:
int operator==(const Comp& c)const
1175:
{return ((re==c.re)&&(im==c.im));}
1176:
Comp operator!()const { //комплексно сопряженное число
1177:
Comp c;
1178:
c.re=re;
1179:
c.im=-im;
1180:
return c;
1181:
}
1182:
float& operator[](int i) { //оператор скобки
1183:
if(i==1)return re;
1184:
else return im;
1185:
}
1186: };//класс Comp
1187: Comp operator+(const Comp& c1, const Comp& c2){
71
1188:
Comp c;
1189:
c.re=c1.re+c2.re;
1190:
c.im=c1.im+c2.im;
1191:
return c;
1192: }
1193: //наследование
1194: class NComp : public Comp{
1195:
char name;
1196: public:
1197:
NComp(){name=’U’;}
1198:
NComp(float x,float y,char n): Comp(x,y){name=n;}
1199: };
1200: //головная программа
1201: void main(){
1202:
Comp y1(1,2);
1203:
Comp y2(3,4);
1204:
Comp y3;
1205:
y1=y1+y1;
1206:
if(y1==y2)
1207:
y3=y1+y2;
1208:
y1=!y1;
1209:
y1[1]=0.0;
1210:
y1[2]=y1[1];
1211:
NComp z1(1,2,’D’);
1212:
NComp z2(1,2,’A’);
1213:
if(z1==z2) //функция-оператор == наследуется
1214: // z1=z1+z2; //ошибка типизации
1215:
else
1216:
z1[1]=2*z1[2]; //функция-оператор [] наследуется
1217: }
В классе может быть задан оператор преобразования типа. В
этом случае имя оператора – это имя типа, в который должен быть
преобразован экземпляр объявляемого типа. Тип возвращаемого значения указывать не нужно, т.к. он задается именем самого оператора.
1218: class Point {
1219: private:
1220:
double x = 1;
1221:
double y = 1;
1222: public:
1223:
operator double() { // оператор преобразования типа
1224:
return x;
1225:
}
1226: };
1227: Point point;
1228: double d = point; // выполнение оператора
// преобразования типа
72
4.2. Потоковый ввод/вывод в С++
В языке С++ отсутствуют какие-либо средства ввода/вывода.
Эти возможности реализуются различными стандартными библиотеками. Одна из библиотек, называемая iostream, использует возможности объектно-ориентированного программирования. В ней используются следующие понятия: поток – последовательность байтов, входной поток – средство перемещения байтов от внешнего устройства в
оперативную память, выходной поток – средство перемещения байтов из оперативной памяти к устройству (рис. 17).
В библиотеке потокового ввода/вывода обеспечиваются возможности низкого уровня (неформатный ввод/вывод, управление передачей отдельных байтов) и высокого уровня (совокупность байтов
имеет свойства, определяемые типами – форматный ввод/вывод).
Средства ввода/вывода распределены по следующим библиотечным файлам:
1) iostream.h – основная библиотека со стандартными потоками
ввода/вывода.
2) iomanip.h – библиотека с манипуляторами для форматного
ввода/вывода.
3) fstream.h – библиотека файловой системы ввода/вывода.
4) strstream.h – библиотека внутреннего форматного ввода/вывода для выполнения операций над строками символов.
В библиотеке iostream.h объявлены следующие объекты, которые используются в программах в качестве глобальных переменных:
cin – поток, связанный со стандартным входным устройством
(обычно клавиатура);
cout – поток, связанный со стандартным выходным устройством (обычно экран);
cerr – поток, связанный со стандартным устройством вывода
сообщений об ошибке (не буферизованный быстрый вывод);
clog – поток, связанный со стандартным устройством вывода
сообщений об ошибке (буферизованный поток, который выводится
после заполнения буфера).
Глобальные объекты принадлежат специальным классам, являющимся потомками основных классов потоков: cin принадлежит
73
классу, производному от istream; cout, cerr, clog принадлежат классу, производному от ostream.
Потоковый вывод
Класс ostream обеспечивает форматный и неформатный вывод
при помощи перегруженного оператора включения в поток "<<", вывод символов при помощи метода put, неформатный вывод при помощи метода write, использование манипуляторов для вывода целых чисел в десятичный, восьмеричный и шестнадцатеричный форматы записи, выравнивание по ширине, указание формата представления чисел.
ios
абстрактный поток
поток ввода
поток вывода
istream
ostream
iostream
входной/выходной
поток
ifstream
ofstream
поток ввода из файла
поток вывода из файла
Рис. 17. Упрощенная диаграмма наследования основных классов
Оператор вставки в поток "<<" является совместно используемым оператором (для стандартных типов данных оператор "<<" – это
оператор сдвига). В библиотеке имеется ряд определений операторов
вставки в поток данных различных стандартных типов. Приведем для
примера некоторые заголовки.
1229: ostream& ostream::operator<<(const int&){...}
1230: ostream& ostream::operator<<(int){...}
74
Выражению cout<<121 соответствует перегруженный оператор (метод класса) с параметром типа int. В соответствии с объявлением это выражение возвращает ссылку на первый параметр – объект
потока cout. Возврат ссылки на поток в операторе вставки в поток
позволяет использовать конкатенацию операторов.
1231: cout<<121<<0<<13; //равносильно последовательному
//выполнению cout<<121; cout<<0; cout<<13;
Имеется особая реализация оператора вставки в поток для типа
char*, которая выводит не адрес символа, а строку символов до первого символа с нулевым кодом.
1232: char *string="test"; //строка оканчивающаяся нулем
1233: cout<<sting; //выводятся символы "test",
//начиная с указателя string
Формат представления данных при выводе изменяется либо методами потока, либо специальными функциями-утилитами, которые
называются манипуляторами.
1234: double f=sqrt(2.0);
1235: cout<<f<<endl;
1236: cout.precision(3);
1237: cout<<f<<endl;
1238: cout<<setprecision(2)<<f<<endl;
//результат 1.414214
//метод для потока
//результат 1.414
//результат 1.41
В рассмотренном примере используется манипулятор endl.
Прототип манипулятора задается в библиотеке приблизительно следующим образом:
1239: ostream& endl(ostream&);
В поток вставляется адрес функции-манипулятора, после чего выполняется вызов функции. Манипулятор endl вставляет в поток символ
перевода строки и освобождает буфер. В библиотеке имеются также и
другие манипуляторы: ends, flush, dec, hex, oct, ws.
75
Потоковый ввод
В библиотеке имеется набор перегруженных операторов извлечения из входного потока ">>" для различных стандартных типов
данных. Приведем некоторые из них.
1240: istream& istream::operator>>(int&);
1241: istream& istream::operator>>(char&);
Благодаря возврату ссылки на тот же поток становится возможной конкатенация операторов извлечения из потока.
1242: cin>>x>>y; //равносильно последовательному выполнению
//операторов cin>>x; cin>>y;
В библиотеке предусмотрен оператор приведения типа возвращаемого из оператора извлечения возвращаемого значения к типу
void*. Благодаря преобразованию возвращаемого типа в указатель
средствами базового класса оператор извлечения может использоваться в логическом выражении.
1243: while(cin>>x) //цикл ввода данных
1244:
{if(x>y)...}
Класс istream предусматривает методы для чтения, например
метод get() возвращает код символа; метод eof() – возвращает 0,
если не был обнаружен символ конец файла, и 1 в противном случае.
1245: while((c=cin.get())!=EOF){...} //здесь EOF - символ
//конца файла
При вводе текстовых строк следует учитывать, что запись информации в память не контролируется. Поэтому размер буфера для
текстовой строки должен быть выбран с запасом. Строка вводится до
первого пробела. После окончания чтения потоком к строке добавляется символ с нулевым кодом.
1246: char buffer[1024];
1247: cin>>buffer; //чтение
1248: cout<<buffer; //вывод
76
Перегрузка операторов потокового ввода и вывода
Для организации ввода и вывода конструируемых типов данных
(например объектов какого-либо класса) следует перегрузить соответствующие операторы извлечения из потока и вставки в поток.
1249: class Complex{
1250: friend ostream &operator<<(ostream&, const Complex&);
1251: friend istream &operator>>(istream&, Complex&);
1252: private:
1253:
float re;
1254:
float im;
1255: };
1256: ostream &operator<<(ostream &output, const Complex &comp){
1257:
output<<"re="<<comp.re<<" im="<<comp.im;
1258:
return output;
1259: }
1260: istream &operator>>(istream &input, Complex &comp){
1261:
input>>comp.re;
1262:
input>>comp.im;
1263:
return input;
1264: }
1265: void main(){
1266:
Complex A, B;
1267:
cin>>A>>B;
1268:
cout<<A<<B;
1269:
//...
1270: }
Перегружаемые операторы << и >> при создании новых типов
объектов объявляются как утилиты. Они не могут быть объявлены
как методы класса, так объект нового класса появляется только в правой части списка параметров.
Работа с файлами последовательного доступа
Для создания файла последовательного доступа необходимо
объявить переменную класса ofstream или производного от него
класса. Связывание физического файла с файловой переменной может
быть выполнено при вызове конструктора, как показано в примере,
или методом open(). Для вывода в файл используются операторы
77
вставки в поток, наследуемые от класса ostream, и перегруженные
функции-операторы для сконструированных классов.
1271: #include <iostream>
1272: #include <fstream>
1273: using namespace std;
1274: void main(){
1275:
ofstream outputFile("x.dat", ios::out);
1276:
if(!outputFile)
1277:
cerr<<”ошибка”<<endl;
1278:
else{
1279:
int index;
1280:
float fNum;
1281:
while(cin>>index>>fNum)
1282:
outputFile<<index<<’ ’<<fNum<<endl;
1283:
}
1284: }//выполнение деструктора, закрытие файла
Первый параметр конструктора – имя физического файла, второй параметр – константа, определяющая режим открытия файла. Эти
константы объявлены внутри библиотечного класса ios.
Некоторые режимы создания файлов:
ios::out – файл для вывода;
ios::in – файл для ввода;
ios::app – добавлять данные только в конец файла;
ios::noreplace – если файл существует, то генерировать
ошибку файловой операции;
ios::nocreate – если файл не существует, то не создавать
новый;
ios::trunc – очистить файл.
Для чтения данных из файла последовательного доступа необходимо создать переменную класса ifstream или производного от
него. Ввод данных осуществляется перегруженными операторами извлечения из потока, наследуемыми от класса istream, и перегруженными функциями-операторами для сконструированных классов.
1285: #include<iostream>
1286: #include<fstream>
1287: int main(){
1288:
int index;
1289:
float fNum;
1290:
ifstream inputFile("x.dat", ios::in);
78
1291:
while(inputFile >> index >> fNum){
1292:
cout<<index<<' '<<fNum<<endl;
1293:
}
1294: }
4.3. Константные методы, константные поля
и объекты-константы
Константными объявляются методы, которые не должны изменять поля объекта. В объявлении константного метода после списка
параметров записывается модификатор const. Как правило, константными являются методы доступа к свойствам объекта. Компилятор позволяет выявить следующие ошибки: константный метод изменяет поля, константный метод вызывает не константный метод. Использование константных методов повышает надежность программы.
Объекты могут быть объявлены константами. Такой объект не
может быть изменен после конструирования, поэтому все поля должны быть сразу инициализированы конструктором. Очевидно, что для
объекта-константы допустимо использование только конструкторов,
деструктора и константных методов.
1295: class Time{
1296: private:
1297:
int hour;
1298: public:
1299:
void setTime(int hr){
1300:
hour=hr;
1301:
}
1302:
int getHour() const { // константный метод
1303:
return hour;
1304:
}
1305:
Time(int hr) {
1306:
hour = hr;
1307:
}
1308: };
1309: void main(){
1310:
const Time noon(24); // константный объект
1311:
//noon.setTime(0); - ошибка!!! – вызов не константного
// метода для константного объекта
1312:
cout<<noon.getHour(); //правильно
1313: }
79
Различие константных и не константных методов кроется в типе
указателя на объект this, который скрытно передается методу объекта. Это приводит к тому, что не константный и константный методы даже с одинаковыми именами имеют различные сигнатуры:
- не константному методу getHour() соответствует сигнатура:
getHour(Time*const this);
- константному методу getHour()const соответствует сигнатура: getHour(const Time*const this).
Как видно из сигнатуры, в пределах константного метода текущий объект имеет константный тип. А это означает, что его поля
нельзя изменять. Тем самым компилятор исключает использование не
константных методов внутри константного метода.
Если поле объекта не должно изменяться на всем протяжении
существования объекта, то оно может быть объявлено константным.
Значение константного поля устанавливается только инициализатором, указываемым в конструкторе.
1314: class Inc{
1315: private:
1316:
int value;
1317: public:
1318:
const int increment; // константное поле
1319:
Inc(int v, int i): increment(i) {
1320:
value=v;
1321:
}
1322:
void addIncrement(){
1323:
value=value+increment;
1324:
//increment=2; - ошибка!!!
1325:
}
1326: };
1327: void main(){
1328:
Inc counter(10, 2);
1329:
counter.addIncrement();
1330:
int i = counter.increment;
1331: // counter. increment=2; - ошибка!!!
1332: }
4.4. Статические методы и статические поля класса
Если имеется необходимость совмещения полей различных объектов одного класса, то это поле объявляется статическим. Статическое поле часто называют полем класса, а нестатическое поле – полем
80
объекта. Статическое поле по своей сути – глобальная переменная,
связанная с классом, поэтому оно может использоваться до создания
объектов. Статический метод – это такой метод класса, который выполняет действия только со статическими полями. Статический метод
также может быть использован до создания экземпляров класса.
1333: class Student{
1334:
int id; //порядковый номер студента
1335:
static int counter; //счетчик всех созданных объектов
1336: public:
1337:
Student() {
1338:
counter=counter+1;
1339:
id=counter;
1340:
}
1341:
static int howMany()//статический метод
1342:
{return counter;} //используется статическое поле
1343:
void getId(){
1344:
cout<<id<<endl;
1345:
cout<<counter<<endl;
1346:
}
1347: };
1348: int Student::counter=0; //использование статического поля
//до создания экземпляров класса
1349: void main(){
1350:
cout<<Student::howMany();
1351:
Student st1, st2, st3;
1352:
cout<<Student::howMany();
1353:
cout<<st2.howMany(); // не рекомендуется
1354:
st2.getId();
1355: }
4.5. Конструктор копирования и конструктор преобразования
Для объекта любого класса по умолчанию определена операция
поэлементного копирования. Однако такое копирование может приводить к структурной неопределенности (нарушению индивидуальности объекта). Поэтому для копирования объектов необходимы специальные методы. Особый случай – копирование в момент создания
объекта. Необходимость в таком копировании возникает при создании именованного объекта по образу другого объекта, а также при
создании неименованного (временного) объекта, например при передаче объекта в качестве фактического параметра функции по значению. Для этой цели служат специальный конструктор – конструктор
81
копирования. Конструктором копирования является конструктор, у
которого первый параметр – ссылка или указатель на объект объявляемого класса.
Имеются особенности использования конструкторов при наследовании классов и при агрегации объектов. В последнем случае различаются следующие ситуации:
1) поля агрегирующего объекта имеют конструкторы копирования, сам объект – не имеет;
2) поля и агрегирующий объект имеют конструкторы копирования.
Конструктор копирования по умолчанию вызывает конструктор
копирования родительского класса. Если для дочернего класса явно
задан конструктор копирования, то его реализация должна явным образом вызывать конструктор копирования родительского класса. В
противном случае используется конструктор без параметров из родительского класса, и, поэтому, не будет выполнено копирование полей,
объявленных в родительском классе.
Конструктор копирования по умолчанию агрегирующего объекта вызывает конструкторы агрегируемых объектов. Если в агрегирующем классе явно задан конструктор копирования, то он должен
содержать инициализаторы для копирования полей. В противном
случае не будет выполнено копирование полей, т.к. для каждого поля
будет вызван конструктор без параметров.
1356: class Base{
1357: public:
1358:
char name;
1359:
int id;
1360:
Base(){name=’U’, id=0;} //конструктор без параметров
1361:
Base(const Base& sb) : //конструктор копирования
1362:
name(sb.name), id(sb.id){}
1363: };
1364: class Array : public Base{
1365: public:
1366:
int size;
1367:
float *ptr;
1368:
Array(int sz){
1369:
size=sz;
1370:
ptr=new float[size];
1371:
}
1372:
Array(const Array& sa); //конструктор копирования
82
1373: };
1374: Array::Array(const Array& sa): Base(sa){
//если явно не вызывать конструктор копирования предка,
//то копирование будет неправильным, так как будет
//вызван конструктор по умолчанию.
1375:
size=sa.size;
1376:
ptr=new float[size];
1377:
for(int i = 0; i<size; i++){
1378:
ptr[i]=sa.ptr[i];
1379:
}
1380: }
1381: class TwoArrays{
1382: public:
1383:
Array fArr;
1384:
Array sArr;
1385:
TwoArrays(): fArr(3), sArr(4){;}
//нет явно заданного конструктора копирования
//в агрегирующем классе, но компилятор его сгенерирует
1386: };
1387: class NewTwoArrays{
1388: public:
1389:
Array fArr;
1390:
Array sArr;
1391:
NewTwoArrays(): fArr(3), sArr(4){;}
1392:
NewTwoArrays(const NewTwoArrays & nta):
fArr(nta.fArr), sArr(nta.sArr){;}
//конструктор копирования агрегирующего класса
//с инициализаторами полей – конструкторами
//копирования
1393: };
1394: void func(Array param){//функция-утилита. Параметр
//передается по значению
1395:
for(int i=0; i<param.size;i++)
1396:
param.ptr[i]=0;
1397: }
1398: void main(){
1399:
Base b1;
1400:
Base b2(b1);
1401:
Array x1(3);
1402:
Array x2(x1);
1403:
TwoArrays t1;
1404:
TwoArrays t2=t1; // конструктор копирования
1405:
NewTwoArrays n1;
1406:
NewTwoArrays n2=n1; // конструктор копирования
1407:
func(x2); // вызов конструктора копирования для
// фактического параметра
1408: }
83
Неявная инициализация объекта может выполняться не только с
использованием конструктора копирования, но и конструктора, принимающего один параметр какого-либо другого типа. Такой конструктор называются конструктором преобразования. Пусть в нижеследующем примере имеется конструктор с одним целочисленным параметром:
1409: class User {
1410: private:
1411:
int userId;
1412: public:
1413:
User(int id): userId(id) {}
1414: };
Тогда следующий код будет корректным, и приведет к вызову данного конструктора в процессе инициализации переменой:
1415: User u = 3; // получаем объект класса U c userId == 3
Однако во многих ситуациях такое поведение может быть ненужным и даже вредным из-за сокрытия ошибок. Чтобы предотвратить возможные ошибки, и не давать использовать конструктор преобразования без явного его указания, необходимо перед конструктором поставить спецификатор explicit.
1416:
explicit User(int id): userId(id) {}
Тогда на этапе компиляции неявная инициализация будет считаться
ошибочной из-за невозможности преобразовать целый тип в объект.
Допустимым будет только явный вызов соответствующего конструктора:
1417: User u(3); // явное указание конструктора
4.6. Функции преобразования типов
Все безопасные преобразования типов выполняются неявно. Например, приведение указателя на объект дочернего класса к указателю на объект базового класса не требует никаких дополнительных
84
действий. Необходимость явного преобразования типов данных возникает в тех случаях, когда преобразование является потенциально
опасным и транслятору нужно дать особые указания о способе преобразования.
Для некоторых видов преобразования требуется информации о
типе объекта на стадии выполнения – RTTI (Run Time Type
Identification). Оператор typeid возвращает информацию о типе
объекта в виде константной ссылки на объект класса type_info. В
этом служебном объекте имеется название типа, декорируемое название типа, но главное – в классе type_info определены операции
сравнения на равенство и неравенство. Если аргумент оператора
typeid ссылка или разыменованный указатель на полиморфный
объект, то возвращается информация о фактическом типе объекта
времени выполнения. Если аргумент – это указатель или объект не
полиморфного типа, то результат применения typeid представляет
статический тип, определяемый на этапе компиляции. В операторе
typeid можно указать имя типа, тогда будет предоставлена информацию об этом типе.
Функция dynamic_cast<> преобразует тип указателя или
ссылки, проверяя корректность преобразования на этапе выполнения.
Корректным считается преобразование, в котором ссылке или указателю родительского типа присваивается ссылка или указатель дочернего типа соответственно. В случае некорректного преобразования
указателей, функция dynamic_cast<> возвращает значение
nullptr. В случае некорректного преобразования ссылок, функция
dynamic_cast<> возбуждает исключение bad_cast. При анализе
dynamic_cast<> компилятор проверяет, есть ли виртуальные методы в базовых классах аргумента. Дело в том, что при наличии виртуальных методов в объект класса включается указатель на таблицу
виртуальных методов. По этому указателю на этапе выполнения можно определить фактический класс, которому принадлежит объект. Если же в классе нет ни одного виртуального метода, то в объект не
включается информация о его типе, и использование функции dynamic_cast<> оказывается невозможным.
85
B1
B2
C
D1
D2
Рис. 18. Пример наследования для приведения типов с помощью dynamic_cast
Для примера на рисунке 18 показаны иерархия наследования, в
которую входит и множественное, и простое наследование. Пусть в
этих классах есть виртуальные методы:
1418: class B1{
1419: public: virtual ~B1(){};
1420: };
1421: class B2{
1422: public: virtual ~B2(){};
1423: };
1424: class C : public B1, public B2 {};
1425: class D1 : public C {};
1426: class D2 : public C {};
Тогда использование функции dynamic_cast<> позволяет выполнить следующее приведение типов:
1427: D1 d1;
1428: auto b1Ptr = dynamic_cast<B1*>(&d1); // b1Ptr != nullprt
1429: auto b2 = dynamic_cast<B2*>(b1); // b2 != nullptr
1430: auto d2 = dynamic_cast<D2*>(&d1); // d2 == nullptr
1431: auto cPtr = new C;
1432: d2Ptr = dynamic_cast<D2*>(cPtr); // d2Ptr == nullptr
1433: auto eq1 = typeid(b1Ptr) == typeid(B1*); // eq1 == true
1434: auto eq2 = typeid(*b1Ptr) == typeid(B1); // eq2 == false
1435: auto eq3 = typeid(*b1Ptr) == typeid(D1); // eq3 == true
Здесь видно, что указатель на объект класса D1 можно привести к типу указателя на объект базового класса B1. В другом случае указатель
b1Ptr типа B1* приводится к типу B2*. Хотя классы B1 и B2 не
86
связаны отношением наследования, приведение здесь корректное, т.к.
фактический тип объекта, на который указывает b1Ptr – это D1, а B2
является его предком. В третьем случае приведение указателя типа
D1* к типу D2* является не корректным, т.к. класс D1 и класс D2 не
связаны между собой отношением наследования, поэтому результат
приведения nullptr. В четвертом случае попытка привести тип указателя объекта на базовый класс к указателю на объект дочернего
класса является не корректной и результат – nullptr.
Функция static_cast<> преобразует данные одного типа в
данные другого типа используя конструктор копирования или конструктор преобразования. На этапе компиляции проверяется корректность преобразования. Если подходящего конструктора нет, или подходящий конструктор имеет спецификатор explicit, то компилятор дает ошибку. Статически можно преобразовывать не типизированный указатель void* в типизированный указатель и наоборот,
однако при этом отсутствует проверка на соответствие типа указателя
типу фактической переменной.
1436:
cout << static_cast<int>(true); // результат 1
Эту функцию надо использовать с осторожностью и, при возможности, обходится без нее.
Имеется еще две функции приведения типов. Функция reinterpret_cast<> может приводить тип в совершенно другой, никак
не связанный с первым. Например, можно привести целое к указателю, указатель к целому, указатели разных типов друг к другу. Обычно
это машинно-зависимые операции. Функция const_cast<> убирает
квалификатор const.
1437: long w = 0x1122334455667788;
1438: auto iPtr = reinterpret_cast<short*>(&w);//*iPtr == 0x7788
1439: const double PI = 3.141;
1440: auto pPtr = const_cast<double*>(&PI);
Исторически до появления в языке C++ рассмотренных функций
приведения типов, существовал способ, которое называют приведением в стиле C. Формат его записи такой: (тип) операнд. Этот
87
способ приведения считается устаревшим и опасным. Он действует
как последовательное применение преобразований const_cast,
static_cast и reinterpret_cast. Поэтому при быстром чтении кода с преобразованиями в стиле C достаточно сложно понять,
какой замысел был у разработчика и правильный ли получится результат.
1441: class B {};
1442: class A {};
1443: class D : public B {};
1444: void f(int* i){/*...*/}
1445: void f(double* d){/*...*/}
1446: void main(){
// Использование
1447:
f((double*)nullptr); // выбор перегруженной функции
1448:
int n1 = 1, n2 = 4;
1449:
// вещественное деление
1450:
double result = ((double)n1) / n2;
// Трудноуловимые ошибки
1451:
auto pa = new A;
1452:
auto pb = (B*)pa; // не связанные типы!
1453:
1454:
auto pd = (D*)new B(); // нарушение наследования!
1455: }
4.7. Обработка исключений
В процессе выполнения программы могут возникать такие ситуации, в которых дальнейшее выполнение программы оказывается бессмысленным или даже опасным, т.к. результат будет ложным. Например, при попытке деления на ноль результат не может быть представлен в ограниченной разрядной сетке компьютера. Другой пример,
в момент обращения к файлу для чтения оказывается, что файл физически отсутствует. Подобные ситуации называют исключительными
ситуациями, или кратко – исключениями. Механизм обработки исключений позволяет выполнить набор действий для продолжения выполнения программы разумным образом.
Фрагмент программы, в котором требуется обеспечить обработку
исключительных ситуаций, оформляют в виде блока try. Блок try, в
общем случае, заканчивается несколькими блоками catch, в которых
записана обработка соответствующих исключений. Обработчик
88
catch(...) выполняется при любом исключении, если это исключение не было перехвачено другим обработчиком.
1456: int *k = nullptr;
1457: try {
1458:
// обращение к памяти по указателю, равному nullptr
*k = 3;
1459: } catch (...) {
1460:
cout << "error" << endl;
1461: }
Программные исключения возбуждаются инструкцией throw с
параметром, который называется объектом исключения. Обработчик,
которому передается управление, идентифицируется типом объекта
исключения.
1462: try{
1463:
throw -1;
1464: } catch (double d){
1465:
cout << d << endl;
1466: } catch (int k){ // тип параметра совпадает с типом
// объекта исключения
1467:
cout << k << endl;
1468: } catch (...) {
1469:
cout << "error" << endl;
1470: }
Если исключение возникает в некоторой функции, но не обрабатывается в ней, то выполнение функции прекращается так, как будто
исключение появилось в месте вызова функции:
1471: double sqrte(double d) {
1472:
if (d < 0) throw d; // объект исключения типа double
1473:
return sqrt(d);
1474: }
1475: void main(int argc, char * argv[]) {
1476:
try {
1477:
double s = sqrte(-1.0);
1478:
cout << s << endl;
1479:
}
1480:
catch (int k) {
1481:
cout << k << endl;
1482:
}
1483:
catch (double d) { // этот обработчик сработает
1484:
cout << d << endl;
89
1485:
1486: }
}
Процедуру поиска подходящего обработчика исключения называют раскруткой стека, и заключается она в следующем. Непосредственно перед выполнением блока try в программный стек помещается информация о всех блоках catch, связанных с данным блоком
try. До этого в стеке были размещены адрес возврата из выполняемой функции, фактические параметры и локальные переменные исполняемой функции. При возникновении исключительной ситуации
выполняется поиск обработчика с таким же типом параметра, как тип
созданного объекта исключения.
Если подходящий обработчик найден, то ему передается управление. При этом объект исключения становится локальной переменной. Если обработчик принимает параметр по значению, а не по
ссылке, то создается копия объекта исключения.
Если подходящий обработчик не найден, то исполнение текущей
функции прекращается. Для этого разрушаются все локальные переменные, которые к этому моменту были созданы и стек освобождается от них. Далее поиск подходящего обработчика выполняется уже в
той части стека, которая соответствует вызвавшей функции. Если
подходящий обработчик так и не будет найден, то аварийно завершается выполнение программы, и управление передается операционной
системе.
Обработчик с параметром базового типа перехватывает исключение с объектом дочернего типа. Поэтому блок catch с параметром
дочернего типа должен быть объявлен раньше, чем блок catch с параметром базового типа:
1487: class Base {};
1488: class Derived : public Base {};
1489: void main(int argc, char * argv[]) {
1490:
try {
1491:
throw Derived();
1492:
}
1493:
catch (const Derived &cDerived) {
1494:
cerr << "caught Derived";
1495:
}
1496:
catch (const Base &cBase) {
1497:
cerr << "caught Base";
90
1498:
1499: }
}
В некоторых случаях требуется в catch выполнить частичную
обработку исключительной ситуации, а после этого продолжить поиск подходящего обработчика на более высоком уровне. Тогда можно
использовать в блоке catch инструкцию throw без параметра. В
этом случае продолжится процедура раскручивания стека с тем же
самым объектом, который был создан в момент возбуждения исходного исключения. Вызов throw без параметров называют повторным
возбуждением исключения. Если в промежуточном обработчике указать передачу параметра исключения по ссылке, то можно модифицировать объект исключения прежде, чем будет выполнено повторное
возбуждение исключения.
4.7. Пространства имен
Пространство имен является областью видимости имен и позволяется избежать коллизию, которая возникает при введение одинаковых имен, например, разными разработчиками программы. Пространство имен задается таким образом: namespace имя {объявления-имен}. Пространства имен могут быть вложенными. Имя, объявленное внутри пространства имен является локальным именем.
Полное имя задается указанием всех пространств имен, содержащих
данное имя, разделенных оператором разрешения области видимости
::.
1500: class C1 {};
1501: namespace P1{
1502:
class С1 {};
1503:
class D1 {};
1504:
namespace P2{
1505:
class C1 {
1506:
D1 d1; // в области видимости
1507:
::C1 c1; // из глобальной области видимости
1508:
};
1509:
}
1510:
class E1 : public P2::C1 {}; // квалифицированное имя
1511: }
1512: void main(){
1513:
P1::С1 x1; // полностью квалифицированное имя
1514:
P1::P2::C1 x2; // полностью квалифицированное имя
91
1515: }
При использовании имени компилятор пытается подобрать квалифицированное имя, добавляя к нему последовательно имя текущего
пространства имен, имя пространства имен, включающее данное пространство имен и т.д. до тех пор, пока не получится правильное имя.
Если правильное имя не будет найдено, то фиксируется ошибка компиляции. Пространство имен, в котором располагаются все другие
пространства имен, не имеет имени и обозначается оператором разрешения области видимости с пустым левым операндом.
Для сокращения записи квалифицированных имен можно использовать директиву using namespace. Тогда компилятор пытается подставить заданное пространство имен к используемым именам.
Используя объявление using квалифицированное-имя-типа,
можно указать транслятору использовать локальное имя как локальный псевдоним.
1516: using namespace P1; // доступ к локальным именам
// пространства имен P1
1517: D1 d2; // фактический тип P1::D1
1518: using P1::P2::C1; // доступ к одному типу
1519: C1 x0; // С1 – локальный синоним
Documents
Reports
Rules
WeekReport
DalyReport
Рис. 19. Отношения: пакет Documents включает пакеты Acts и Reports;пакет Reposrts
включает классы WeeklyReposrt и DalyReport;пакет Documents компиляционно зависит
от пакета Rules
92
В UML понятию пространство имен соответствует конструкция
пакет. Для пакетов (пространств имен) обычно показывают отношение включения и компиляционную зависимость между пакетами (рис.
19).
КОНТРОЛЬНЫЕ ВОПРОСЫ И ЗАДАНИЯ
Вопросы и задания к главе 1
1) Объяснить назначение следующих объявлений, выявить правильные и ошибочные объявления:
1520: int i;
1521: const int ic;
1522: const int *pic
1523: int *const cpi;
2) Почему следующая инициализация ошибочна?
1524: char st[11] = “fundamental”
3) В следующем фрагменте программы имеется две ошибки индексации, найдите их:
1525: void main(){
1526:
const int arraySize = 10;
1527:
int ia[arraySize];
1528:
for (int ix = 1; ix <= arraySize; ++ix)
1529:
ia[ix] = ix;
1530: //...
1531: }
4) Дано объявление переменных:
1532: unsigned int ui1 = 3, ui2 = 7;
Какой будет результат вычисления следующих выражений?
1533: ui1 & ui2
1534: ui1 && ui2
1535: ui1 | ui2
1536: ui1 || ui2
93
5) Составить функцию, в которой определяется, входит ли подстрока символов в другую строку символов.
6) Составить функцию, в которой определяется, являются ли два
целочисленных массива одинаковыми.
7) Дана функция двоичного поиска в сортированном целочисленном массиве. Требуется преобразовать ее в шаблонную функцию и
составить программу, в которой выполняется поиск заданного элемента в массиве целых и вещественных чисел.
1537: const notFound = -1;
1538: int binSearch(int *ia, int sz, int val){
1539:
int low = 0;
1540:
int high = sz-1;
1541:
while (low <= high){
1542:
int mid = (low+high)/2;
1543:
if (val == ia[mid]) return mid;
1544:
if (val < ia[mid])
1545:
high = mid-1;
1546:
else low = mid+1;
1547:
}
1548:
return notFound;
1549: }
Вопросы и задания к главе 2
1) Перечислите достоинства и недостатки динамического распределения объектов и локального объявления объектов.
2) Разработайте класс связного списка символьных строк. Какие
отношения возникают между объектом списка и элементами списка,
между отдельными элементами списка?
3) Покажите, в каких условиях программа будет выполняться
различным образом в зависимости от того, объявлен ли деструктор
некоторого класса виртуальным или нет.
Вопросы и задания к главе 3
1) Попытайтесь сформулировать правило, определяющее, в каких случаях следует применять наследование классов, а в каких агрегацию.
2) Объясните, почему в строго типизированных языках запрещено присваивать экземпляру класса потомка значение экземпляра
класса предка.
94
3) Приведите пример множественного наследования из реальной
жизни, не связанной с вычислительной техникой.
4) Можно ли значение nullptr считать полиморфным объектом?
5) Объясните, почему обобщенные классы часто используются
для гомогенных контейнеров (контейнеров, содержащих элементы
одного типа) и не используются для гетерогенных контейнеров (контейнеры, содержащие элементы разных типов).
6) Позволяет ли использование обобщенных классов сократить
объем машинного кода, получаемого после трансляции программы,
почему?
Вопросы к главе 4
1) В каких случаях следует использовать статические методы?
2) Имеется ли перегрузка операторов в тех языках программирования,
которые
не
поддерживают
средства
объектноориентированного программирования?
3) Добавить в класс конструктор, конструктор копирования, деструктор:
1550: class vect {
1551: private:
1552:
int size;
1553:
double* v;
1554: };
4) Привести пример класса с конструктором преобразования и
его использованием.
5) Что произойдет, если во время раскрутки стека при возникновении исключительной ситуации возникнет исключение в деструкторе объекта.
6) Написать функцию нахождения действительных корней квадратичного уравнения. Функция должна возбуждать различные исключения в случаях, когда действительных корней нет и имеется бесконечное количество корней.
7)
В
какой
ситуации
функция
приведения
типа
dynamic_cast<D*>(bp) вернет указатель не равный nullptr,
если bp имеет статический тип B*, а D – это класс, являющийся потомком класса B.
95
ЗАКЛЮЧЕНИЕ
Процесс создания сложных программных систем включает в себя различные этапы: анализ, проектирование, программирование, сопровождение. На каждом из этих этапов разработчики используют
различные методы работы, различные модели и различные формальные языки, в терминах которых фиксируются решения. Значительную
сложность представляют переходы от одного этапа к другому, так как
именно на стыке перечисленных видов деятельности необходимо выполнить преобразование полученных решений, и на основе предыдущего этапа, синтезировать модели последующего этапа.
Объектно-ориентированный подход позволяет использовать на
всех этапах проектирования программной системы общий стиль работы, единую систему моделей. Это в значительной степени облегчает
переходы от анализа к проектированию, от проектирования к программированию. В данном учебном пособии, посвященном этапу
программирования, материал изложен таким образом, чтобы кроме
специальных вопросов программирования можно было получить
представление о том, как связаны проектные модели с программными
моделями.
Вместе с тем в пособие включены вопросы внутренней реализации объектно-ориентированных средств языка С++. Необходимость в
этом вызвана тем, что при программировании необходимо представлять границы возможностей реализации проекта в виде программы.
Кроме этого, только при знании технических деталей реализации
возможен быстрый поиск ошибок в программе в процессе отладки и,
в конечном счете, создание достаточно надежных программ.
96
БИБЛИОГРАФИЧЕСКИЙ СПИСОК
1. Буч, Г. Объектно-ориентированный анализ и проектирование
с примерами приложений. М.: Вильямс, 2010. – 720 с.
2. Липпман, С.Б., Лажойе, Ж., Му, Б. Э. Язык программирования
C++. Базовый курс. М.: Вильямс, 2017. – 1120 с.
3. Страуструп, Б. Программирование: принципы и практика с
использованием С++, 2-е изд.: Пер. с англ. - М.: ООО "И. Д. Вильямс", 2016. – 1328 с.
97
ОГЛАВЛЕНИЕ
ВВЕДЕНИЕ .......................................................................................................... 3
ГЛАВА 1. ОСНОВНЫЕ ЭЛЕМЕНТЫ ЯЗЫКА С++ ....................................... 4
1.1. Примеры простейших программ ................................................................................. 4
1.2. Типы данных .................................................................................................................... 8
1.3. Динамическое распределение памяти ....................................................................... 16
1.4. Дополнительные операторы присваивания ............................................................ 19
1.5. Инструкции .................................................................................................................... 19
1.6. Функции .......................................................................................................................... 24
ГЛАВА 2. ОБЪЕКТНАЯ МОДЕЛЬ ПРОГРАММЫ ..................................... 31
2.1. Объявление классов и создание объектов в программе на С++ .......................... 32
2.2. Состояние ........................................................................................................................ 34
2.3. Поведение........................................................................................................................ 34
2.4. Индивидуальность ........................................................................................................ 37
2.4. Инкапсуляция ................................................................................................................ 38
ГЛАВА 3. ОТНОШЕНИЯ МЕЖДУ КЛАССАМИ ........................................ 38
3.1. Отношение наследования ............................................................................................ 39
3.2. Множественное наследование .................................................................................... 50
3.3. Отношение ассоциации ................................................................................................ 56
3.4. Отношение реализации ................................................................................................ 61
3.5. Отношение дружественности ...................................................................................... 62
3.6. Отношение наполнения ............................................................................................... 63
ГЛАВА 4. ОСОБЕННОСТИ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО
ПРОГРАММИРОВАНИЯ НА С++ ................................................................. 70
4.1. Перегрузка операторов ................................................................................................ 70
4.2. Потоковый ввод/вывод в С++ .................................................................................... 73
4.3. Константные методы, константные поля и объекты-константы ...................... 79
4.4. Статические методы и статические поля класса .................................................... 80
98
4.5. Конструктор копирования и конструктор преобразования ................................. 81
4.6. Функции преобразования типов ................................................................................ 84
4.7. Обработка исключений ................................................................................................ 88
4.7. Пространства имен ....................................................................................................... 91
КОНТРОЛЬНЫЕ ВОПРОСЫ И ЗАДАНИЯ .................................................. 93
ЗАКЛЮЧЕНИЕ ................................................................................................. 96
БИБЛИОГРАФИЧЕСКИЙ СПИСОК ............................................................. 97
99
Учебное издание
ДУБОВ Илья Ройдович
БЫКОВ Валерий Ильич
ЯЗЫКИ ПРОГРАММИРОВАНИЯ.
ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ
Учебное пособие
Редактор
Корректор
Компьютерная верстка И.Р. Дубов
ЛР №
. Подписано в печать
.
Формат 60х84/16. Бумага для множит. техники. Гарнитура Таймс.
Печать офсетная. Усл. печ. л.
. Уч.-изд. л.
. Тираж
экз.
Заказ
Редакционно-издательский комплекс
Владимирского государственного университета.
600000, Владимир, ул. Горького, 87
Download