close

Вход

Забыли?

вход по аккаунту

?

prokushev

код для вставкиСкачать
Федеральное агенТство по образованию
Государственное образовательное учреждение
высшего профессионального образования
САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
АЭРОКОСМИЧЕСКОГО ПРИБОРОСТРОЕНИЯ
Л. А. Прокушев
ПРОГРАММИРОВАНИЕ
НА ЯЗЫКЕ ВЫСОКОГО УРОВНЯ
Объектно-ориентированное
программирование на С++
Конспект лекций
Санкт-Петербург
2009
УДК 004
ББК 32.81
П80
Рецензенты:
канд. техн. наук В. Н. Попов;
канд. техн. наук С. Л. Козенко
Утверждено
редакционно-издательским советом университета
в качестве конспекта лекций
Прокушев Л. А.
П80 Программирование на языке высокого уровня. Объектно-ориентированное программирование на С++: конспект лекций /
Л. А. Прокушев. – СПб.: ГУАП, 2009. – 147 с.
Рассматриваются основные концепции и понятия объектноориентированного программирования на языке высокого уровня
С++: объект, класс, инкапсуляция, наследование, полиморфизм, абстракция типов. Разъясняются особенности С++ как языка
объектно-ориентированного программирования: ввод-вывод потока, конструкторы и деструкторы объектов, перегрузка операций и
функций, наследование и контроль доступа, множественное наследование, виртуальные функции, абстрактные классы, шаблоны
функций и классов. Приведены примеры программ с комментариями по каждой теме для самостоятельного выполнения студентами с
целью практического освоения С++.
Конспект лекций предназначен для студентов, изучающих дисциплину «Программирование на языке высокого уровня».
УДК 004
ББК 32.81
© ГУАП, 2009
© Л. А. Прокушев, 2009
Технологии программирования
Структурное и объектно-ориентированное программирование
Разработка программного обеспечения (ПО) ЭВМ в настоящее
время осуществляется с использованием двух основных технологий — структурного (процедурного) программирования и объектноориентированного программирования (ООП).
Структурное программирование.
Данная технология предполагает выполнение последовательности этапов разработки программ для решения задач с использованием ЭВМ.
1. Постановка задачи – формулирование задачи и целей ее решения на естественном языке и установление критериев решения
задачи. Результат этапа – техническое задание на разработку ПО.
2. Формализация задачи с использованием математического аппарата и получение ее абстрактной математической модели в виде
формул и уравнений.
3. Выбор численного метода из возможных вариантов с учетом
требований по времени и точности решения и занимаемого объема
памяти ЭВМ.
4. Алгоритмизация – построение общего плана решения, т. е. алгоритма задачи в виде логической последовательности этапов (шагов, действий, операций), приводящих от исходных данных к искомому результату за конечное время на языке понятном человеку.
Могут быть использованы различные способы представления алгоритма – словесный, графический (схемы алгоритмов), алгоритмический язык высокого уровня (ЯВУ). ЯВУ – формализованный язык
для описания данных и набор правил (инструкций, операторов) их
обработки для реализации алгоритма задачи. Типичные ЯВУ структурного программирования – Си, Паскаль.
5. Программирование – перевод алгоритма задачи на язык ЭВМ (систему команд), т.е. кодирование алгоритма. Процесс разработки программы делится на следующие этапы: 1) запись алгоритма на ЯВУ в
виде исходного файла в памяти (например, prog1.cpp); 2) компиляция и
3
редактирование связей (объектный файл – prog1.obj); 3) загрузка программы в оперативную память (исполняемый файл – prog1.exe); 4) исполнение программы; 5) получение результатов программы.
6. Отладка программы – поиск и исправление ошибок в программе. Этот процесс разбивается на два этапа: 1) синтаксическая
отладка – исправление формальных ошибок, связанных с нарушением норм языка программирования, с помощью ЭВМ; 2) семантическая отладка – исправление логических (смысловых) ошибок с
применением специальных тестовых данных.
7. Исполнение (эксплуатация) программы с любыми допустимыми данными и получение результатов решения задачи.
8. Интерпретация результатов и поддержка программы в процессе эксплуатации – изменение программы в соответствии с требованиями пользователей, а также исправление ошибок, выявленных
в процессе ее эксплуатации.
Существование программы можно разделить на три периода: 1)
разработка (этапы 1–4); 2) реализация (этапы 5, 6); 3) сопровождение (этапы 7, 8).
Функциональная декомпозиция.
При решении сложной задачи разработка и реализация ее алгоритма потребует написания длинной программы, которую трудно
отлаживать из-за возможного большого числа ошибок. Внесение изменений вызовет необходимость дополнительного выполнения этапов разработки и реализации программы в целом.
Целесообразно такую задачу разбить на легко решаемые подзадачи, которые в совокупности дают решение исходной задачи. Такой метод решения задач называется функциональной декомпозицией. Для применения этого метода на ЭВМ используется принцип
модульного программирования. Каждая подзадача реализуется в
виде отдельной подпрограммы (функции, процедуры). Для решения всей задачи создается главная функция, которая вызывает другие функции, передавая им исходные аргументы и получая промежуточные результаты.
Большую программу целесообразно разделить на несколько программных модулей (автономно компилируемых файлов), например,
файл подпрограмм и файл главной функции, а для их соединения в
общую программу создается файл проекта. При этом файл подпрограмм можно рассматривать как библиотеку готовых подпрограмм,
которые можно использовать в других задачах, что сократит время
их решения.
4
Для структурного программирования характерно то, что данные
и методы их обработки (функции, процедуры) отделены. Данные
рассматриваются как пассивные элементы, обрабатываемые функциями.
Концепции объектно-ориентированного программирования
(ООП).
Идеи ООП впервые были выдвинуты и реализованы в 60-е годы
20-го века авторами языков Simula-67 (Дал и Нигард) и Smaltalk.
Главная причина возникновения ООП связана с поиском путей
для создания очень сложных программ, отражающих сложность
мира и способов его описания. ООП представляет собой технологию
программирования, в основе которой лежит подход, позволяющий
людям формировать модели объектов реального мира. Чтобы справиться со сложностями жизни, человечеству потребовалось выработать способность обобщать, классифицировать и генерировать абстракции.
ООП базируется на следующих ключевых понятиях: 1) объект
(object); 2) класс (class); 3) инкапсуляция (encapsulation); 4) наследование (inharitance); 5) полиморфизм (polimorphism); 6) абстракция
типов (abstraction).
Раскроем эти понятия в общем плане.
Объект. Наш мир состоит из объектов, которые обладают некоторыми свойствами и которым присуще определенное поведение.
Объект — это логическая единица, которая содержит совокупность
данных (атрибутов), определяющих его отличительные свойства, и
правила, описывающие его поведение. В программе объект содержит данные (DATA) и код (CODE), который манипулирует этими
данными.
Класс. Объекты, обладающие некоторым общим набором атрибутов (отличительных черт) и схожим поведением объединяют в
классы объектов. Например, из реального мира, в котором существует множество конкретных собак, можно выделить абстрактный
класс СОБАКА, отличающийся от классов других животных. Такой подход позволяет разрабатывать и развивать идеи, касающиеся
собак, абстрагируясь от особенностей отдельной особи. Таким образом, класс – это новый обобщенный тип объектов. Он определяет,
каким образом ведут себя любые объекты этого типа, как они создаются (порождаются), как с ними взаимодействуют другие объекты,
как они уничтожаются. После описания класса можно объявить его
конкретные экземпляры-объекты.
5
Инкапсуляция – это объединение набора данных и структур данных с группой методов (функций) манипулирования этими данными в единое целое, называемое классом, в котором также определены механизмы защиты данных и функций, которые могут быть
частными (private), общими (public), или защищенными (protected)
и установлены права доступа к данным.
Наследование – это механизм создания новых, производных
классов, которые наследуют данные и функции от одного или нескольких ранее определенных базовых классов. При этом возможны переопределение или добавление новых данных и методов. В результате создается разветвленная иерархия классов (дерево классов), корнем которой является наиболее общее понятие.
Например:
СОБАКА
сторожевая
охотничья
домашняя ...
овчарка ....
лайка
болонка ...
...
Из производного класса можно получить определенный доступ
к данным и функциям базового класса. Таким образом, наследование дает возможность передачи некоторых свойств одного объекта
другому. Его важной особенностью является возможность поддержки концепции классификации объектов. Наследование позволяет
строить иерархию объектов, переходя от более общего к более частному, уточняя и конкретизируя объект.
Полиморфизм по своей сути означает то, что одно и то же имя
может быть использовано для логически связанных, но различных
целей, т.е. имя определяет порядок действий, которые в зависимости от типа данных могут существенно отличаться. Например, можно определить функцию вычисления суммы двух величин, которая
будет получать целые или вещественные аргументы summa(x, y).
При вызове функции анализируется тип аргументов и выполняется
код, соответствующий этому типу. В языке С++ полиморфизм поддерживается на этапе компиляции и на этапе выполнения программы (раннее и позднее связывание).
Абстракция типов. Обобщение понятия класса позволяет определить абстрактный класс, который имеет одну важную черту –
нельзя создать объект этого класса. Абстрактный класс должен использоваться как базовый класс, от которого наследуются другие
производные классы (например, абстрактный класс СОБАКА).
6
Резюме.
Рассмотренные ключевые понятия класса позволяют сказать,
что в ООП введено понятие класса объектов и реализованы механизмы формализации, позволяющие: 1) описывать структуру объекта;
2) описывать действия с объектами; 3) использовать специальные
правила наследования объектов; 4) устанавливать различную степень защиты компонентов объектов и определять различные права доступа к ним; 5) передавать свойства одних объектов другим; 6)
передавать сообщения между объектами.
Таким образом, в основе ООП лежит идея моделирования объектов реального мира посредством иерархически связанных классов.
Можно дать следующее определение ООП – это технология создания
новых типов данных и объектов, которые наследуют определенные
черты существующих (ранее созданных) типов данных и объектов.
Важное место в ООП занимает структура программы. Необходима большая работа на предварительных этапах по созданию классов объектов с целью формирования эффективных библиотек классов. Эти этапы носят название объектно-ориентрованная разработка (ООР) классов.
Использование подходов, принятых в ООП, дает следующие преимущества:
1) предоставляется возможность создания более сложных программ при снижении трудоемкости программирования;
2) стиль написания программ повышает доступность ее восприятия и упрощает внесение возможных изменений (модификация
программ);
3) существенно упрощается процедура поиска ошибок. повышается надежность программ;
4) сложные программы могут быть декомпозированы на изолированные подзадачи, что позволяет создавать большие системы (проекты) коллективом одновременно работающих программистов.
Полезно иметь в виду такой момент – функциональная декомпозиция и ООП не являются отдельными не связанными друг с другом
методами. ООП разбивает задачу на объекты. Объекты не только содержат данные, но и функции, для которых требуются алгоритмы.
Нередко эти алгоритмы настолько сложны, что требуют дробления
на более мелкие части с использованием функциональной декомпозиции. Таким образом, опытным программистам необходимо знать
оба метода и уметь ими пользоваться.
7
Особенности языка С++
Язык С++ является надстройкой над языком С (его надмножеством). Он сохраняет все алгоритмические возможности структурного (процедурного) программирования и дополняет их идеями ООП.
С++ расширяет существующие конструкции языка С. Программы
на С могут компилироваться в среде С++, но не наоборот. Появляются новые средства для ввода-вывода данных и известных в С операций, а также совсем новые конструкции и правила организации
программ для реализации концепций ООП. Помимо возможностей
предоставляемых С, С++ обеспечивает гибкие и эффективные средства определения новых типов. Можно разделить программу на несколько фрагментов, определив новые типы, отражающие базовые
концепции предметной области. Такой способ разработки программ
часто называют абстракцией данных. Объекты типов, определяемых пользователем, содержат необходимую информацию, свою для
каждого типа. Такие объекты можно удобно и безопасно использовать даже в контексте, где их тип нельзя определить во время компиляции. Подобные методы дают более короткие и простые в сопровождении программы.
Ввод-вывод в С++
Классы и потоки ввода-вывода.
Ввод-вывод в С++, также как и в С, рассматривается как поток
данных, управляемый с помощью функций ввода-вывода. В С++
для поддержки ввода-вывода данных используется целая иерархия
классов.
Основополагающим базовым классом является класс ios. Производными от него являются классы: istream – поддерживает базовые
операции ввода, ostream – поддерживает базовые операции вывода.
Двунаправленный поток поддерживается классом iostream, производным от istream и ostream, что можно представить схемой:
ios
____________|_________
|
|
istream
ostream
|______________________|
|
iostream
8
Объявления этих классов ввода-вывода помещено в заголовочный файл, который нужно подключить к программе строкой:
#include<iostream.h>
Начав с определения потока как абстракции для моделирования прохождения потока данных от источника к приемнику, класс
iostream обеспечивает иерархию классов для управления буферированным и непосредственным вводом-выводом для устройств и
файлов. Пользователь может создавать собственные классы вводавывода на базе стандартных классов.
Стандартный ввод-вывод.
С++ поддерживает четыре предопределенных потоковых объекта:
1. cin – стандартный ввод, как правило с клавиатуры, аналог
stdin в С (объект класса istream);
2. cout – стандартный вывод, как правило на экран, аналог stdout
в C (объект класса ostream);
3. cerr – стандартный небуферизованный вывод ошибок, как правило на экран, аналог stderr в С (объект класса ostream);
4. clog – буферизованный вывод ошибок, нет аналога в С (объект
класса ostream).
Можно переназначать стандартные потоки на другие устройства
и файлы.
Для каждого потока «перегружены» (переопределены) два оператора:
>> – оператор извлечения (чтения) из потока (для ввода с клавиатуры: «считать из»);
<< – оператор записи (вставки) в поток (для вывода на экран: «вывести на»).
Замещающие функции способны воспринимать аргументы любых основных типов данных (char, char*(string), int, long, float, double)
и могут быть расширены для того, чтобы воспринимать аргументы
типа class.
Ввод для встроенных типов
Файл заголовков iostream.h содержит объявление объекта вида:
istream cin;
Класс iostream определяет оператор ввода >> для набора стандартных типов:
9
class istream
{ // …
public:
istream & operator >> (char&);
istream & operator >> (char*);
istream & operator >> (short&);
istream & operator >> (int&);
istream & operator >> (long&);
istream & operator >> (float&);
istream & operator >> (double&);
}
// общедоступные функции
// символ
// символьная строка
// короткий целый
// целый
// длинный целый
// действительный
// двойной действительный
Операция >> (извлечения, чтения) заменяет функцию ввода
scanf() и лучше защищает от ошибок. Левый операнд – это объект
типа класса istream. Правый операнд может быть любого типа, для
которого определен ввод потока. По умолчанию оператор >> опускает пробельные символы, а затем считывает символы, соответствующие типу вводимого данного. Функции преобразования и форматирования данных зависят от их типов.
Для ввода-вывода можно строить цепочки операций слева направо. Например, так можно ввести несколько данных в С++:
int i; float f;
cin >> I >> f;
Для всех числовых типов в случае, когда первый непробельный
символ не является цифрой, или знаком, или точкой (для вещественных чисел) поток входит в состояние ошибки и вплоть до сброса состояния ошибки любой дальнейший ввод запрещен.
Для целых типов short, int, long действие операции >> по умолчанию заключается в пропуске пробельных символов и преобразовании целого значения путем чтения символов ввода до символа, который не является допустимым для данного типа.
Для типов с плавающей точкой float и double действие операции
>> состоит в пропуске пробельных символов и преобразовании значения с плавающей точкой до символа, который не может быть частью числа с плавающей точкой.
Вывод для встроенных типов
Файл заголовков iostream.h содержит объявление объекта вида:
ostream cout;
10
Класс ostream определяет оператор вывода << для встроенных
типов:
class ostream
{ // …
public:
// общедоступные функции
ostream & operator << (char);
// символ
ostream & operator << (char*);
// символьная строка
ostream & operator << (int i);
// целый
ostream & operator << (long);
// длинный целый
ostream & operator << (double); // двойной действительный
ostream & put(char);
// символ
}
Операция <<, называемая вставкой, заменяет функцию вывода
printf(). Левый операнд – это объект cout типа класса ostream. Правый операнд может иметь любой тип, для которого определен вывод
потоком для встроенных типов.
Функция-оператор << возвращает обращение по адресу, для которого она была вызвана, так что можно применить другой оператор ostream, т.е. можно строить цепочки операций вывода в порядке
слева направо.
Например:
int i, k=234; double d ; char ch=’A’;
cout<<"Значение k=" << k <<’\n’;
// в конце символ перевода строки
cin>> i >> d;
// ввод данных i, d
cout<<"i="<<i<<" d="<<d<<’\n’; // вывод значений i, d
cout<<"ch="<<ch;
// вывод символа A
Пример 1.
Потоки ввода-вывода.
#include<iostream.h>
// подключение библиотек ввода-вывода
void main()
{ int i;
char str[ ]="Пример вывода в С++\n";
cout<<"Вывод строки"<<str;
cout<<"Введите целое и длинное целое число: ";
long l; // переменная описывается перед первым использованием
cin>>i>>l;
//ввод данных
cout<<"i="<<i<<" l="<<l<<’\n;
cout<<"Введите строку: ";
11
cin>>str;
cout<<"Введена строка: "<<str;
}
Замечание. Можно также использовать любые библиотечные
функции ввода-вывода языка С (printf(), scanf() и др.). Кроме того, в
другом контексте преопределенные операторы << и >> задают операции сдвига.
Пример 2.
Встроенные типы вставок (вывода данных). Целые типы преобразуются по правилам (по умолчанию) для printf (если эти правила
не изменены путем установки флагов ios).
#include<iostream.h>
// подключение библиотек ввода-вывода
#include<stdio.h>
// для printf
#include<conio.h>
// для консольных функций
void main()
{ int i=5; int *pi=&i; long l=12345678; double d=345.6789; char ch=’A’;
clrscr(); // чистка экрана результатов
cout<<"Вывод с помощью функции printf():\n");
printf("i=%d адрес i pi=%#x l=%ld d=%.4f ch=%c\n", i,pi,l,d,ch);
cout<<"Вывод с помощью потока cout:\n";
cout << "i="<< i << " адрес i=" << &I << " l =" << l << " d=" <<d;
cout<<" ch="<<ch<<’\n’;
getch();
// задержка экрана результатов
}
Результаты программы:
Вывод с помощью функции printf():
i=5 адрес i pi=0xfff4 l=12345678 d=345.6789 ch=A
Вывод с помощью потока cout:
i=5 адрес i=0x8f61fff4 l=12345678 d=345.6789 ch=A
Управление вводом-выводом
Символьные извлечения.
Для типа char действие операции >> состоит в пропуске пробельных символов и чтения следующего (непробельного) символа. Если
требуется прочесть следующий символ (любой), то можно использовать одну из функций-элементов get() класса istream:
12
сhar ch;
cin.get(ch); // ch устанавливается на следующий символ потока,
// даже если это пробельный символ.
Следующий вариант get позволяет управлять числом извлекаемых символов, их размещением и оконечным символом:
get(char* buf, int max, int term=’\n’).
Эта функция считывает символы из входного потока в символьный массив buf до тех пор, пока не будет считано max -1 символов,
либо пока не встретится символ, заданный term, в зависимости от
того, что будет раньше. Завершающий пустой символ (нуль-байт –
‘\0’) добавляется автоматически. По умолчанию оконечным символом (терминатором), который не требуется задавать, является ‘\n’.
Сам терминатор в массив buf не считывается и из входного потока
istream не удаляется. Массив buf должен иметь размер не менее max
символов.
Пример 3.
Ввод строки цифр и преобразование их в целое число типа int с
помощью функции atoi(s) и вывод числа в 10-ом, 16-ом, 8-ом форматах с использованием идентификаторов dec, hex, oct, вставленных
в поток вывода для управления форматом выходного потока, endl
аналог ‘\n’. (см. Манипуляторы).
#include<iostream.h>
// подключение библиотек ввода-вывода
#include<conio.h>
// для консольных функций
#include<stdlib.h>
// для функции atoi()
const int size=35;
// размер буфера строки
void main()
{ clrscr();
int value;
// переменная для числа
char s[size];
// буфер строки
cout<<"Value= "; cin.get(s, size,’\n’);
// ввод строки числа
value=atoi(s);
// преобразование строки в число
cout<<"Decimal="<<dec<<value<<" Hexadecimal=0x"<<hex<<value
<<" Octal=0"<<oct<<value<<endl;
getch();
}
13
Результаты программы:
Value=123
Decimal=123 Hexadecimal=0x7b Octal=0173
Замечание. Для ввода строки вместо функции cin.get(s, size,’\n’);
можно использовать cin>>s. Этот оператор сработает, но если пользователь введет более 34 символов, оператор ввода продолжит запись
за пределами буфера s, при этом, возможно, данные и код, располагающиеся за буфером, будут уничтожены. Такая операция может
привести к краху системы!
Пример 4.
Использование функции get для безопасного чтения строк.
#include<iostream.h>
void main()
{ char s[35];
// буфер строки
char c; // переменная символа
cout<<"Введите строку не более 34 символов:\n";
cin.get(s, 35);
// пропущен 3-й аргумент по умолчанию ‘\n’
cout<<"Вы ввели строку: "<<s<<endl;
if(cin.get(c) && c!=’\n’)
// если следующий символ не ‘\n’, то вывод
cout<<"Достигнута максимальная длина строки\n";
}
При вводе строки длиной более 34 символов она будет усечена и
ввод будет безопасным. Одна проблема все же остается. Символ ‘\n’
или другой символ, завершающий ввод, остается в потоке и должен
быть прочитан еще одним оператором cin.get(), как показано последним оператором if. Если cin.get() не читает символ ‘\n’, ввод будет усечен. Этот факт необходимо учитывать при написании программ.
Проблему непрочитанного символа ‘\n’ можно решить с помощью метода cin.getline() с тремя параметрами (совпадет с get()), при
этом символ разделитель ‘\n’ также помещается в принимающую
строку символов (s).
Для ввода неформатированных и непреобразованных двоичных
данных используется функция cin.read((char*)&x, sizeof(x)).
Форматирование ввода-вывода.
14
Форматирование ввода и вывода определяется флагами состояния формата, перечисленными в классе ios, по умолчанию. Их можно изменить с помощью специальных функций.
Ширина вывода.
По умолчанию вставки выводят минимальное число символов,
которыми может быть представлен операнд правой части. Для изменения используются функции:
int ios::width(int w);
// устанавливает поле шириной w
// символов и возвращает предыдущую
// ширину,
int ios::width();
// возвращает предыдущую ширину,
// не внося изменений.
По умолчанию width=0, то есть вывод выполняется без заполнителей. При w≠0, если длина числа меньше w используются заполнители, иначе выводится фактическое число без усечения.
Например:
int i=123;
int oldw=cout.width(6);
cout<< i; // вывод будет 123 (перед числом 3 пробела), затем width=0
cout.width(oldw);
// восстановление предыдущей ширины width=6
После каждой форматной вставки ширина обнуляется, например:
int i, j;
cout.width(4);
cout<< i <<" "<< j <<;
Здесь i будет выведено четырьмя символами, однако пробел и j будут иметь минимально необходимое число символов.
Заполнители и дополнение вправо и влево.
Символ-заполнитель и направление дополнения зависят от установок внутренних флагов, отвечающих за эти параметры.
По умолчанию символом-заполнителем является пробел. Изменить данное умолчание позволяет функция fill:
int i=123;
cout.fill ("*");
cout.width(6);
cout<< i;
// на экран будет выведено ***123
15
По умолчанию устанавливается выравнивание по правому краю
(дополнение символами-заполнителями). Это можно изменить
функциями setf и unsetf:
int i=56;
cout.width(6);
cout.fill ("#");
cout.setf (ios::left, ios::adjustfield);
cout<< i; // на экране: 56####
Второй аргумент сообщает setf, какие биты флага должны быть
установлены. Первый аргумент сообщает, в какие именно значения
устанавливаются эти биты.
Можно также использовать манипуляторы setfill, setiosflags,
resetiosflags.
Манипуляторы.
Это специальные операции (похожие на функции) для более простого изменения ширины и других параметров форматирования.
Для их использования программа должна иметь строку:
#include<iomanip.h>
Манипуляторы принимают в качестве аргумента ссылку на поток и возвращают ссылку на тот же поток. Поэтому манипуляторы
могут объединяться в цепочку вставок (или извлечений из потока),
для того чтобы изменять состояние потока в виде побочного эффекта, без фактического выполнения каких-либо вставок или извлечений. В таблице ниже в столбце синтаксис задано направление потока: входной – ins >>, выходной – outs <<.
Манипулятор
ния
dec
СинтаксисДействие
Установка флага форматирова
с преобразованиями:
outs << dec
10-ым
ins >> dec
hex
outs << hex
16-ым
ins >> hex
oct
outs << oct
8-ым
ins >> oct
ws
ins >> ws
endl
outs << endl
ends
outs << ends
flush
outs << flush
setbase(int)
outs << setbase(n)
16
Извлечение пробелов
Вставка символа новой строки
и очистка потока
Вставка конечного нулевого
символа в строку
Очистка ostream
Установка системы счисления
(0, 8, 10, 16). 0 это по умолча-
resetiosflags(long)
ins>>resetiosflags(1)
outs<<resetiosflags(1)
setiosflags(long)
ins >> setiosflags(1)
outs << setiosflags(1)
setfill(int)
ins >> setfill(n)
outs >> setfill(n)
setprecision(int)
ins >> setprecision(n)
outs>>setprecision(n)
setw(int)
ins >> setw(n)
outs << setw(n)
нию 10-ая с.с. при выводе и
правила С для литералов целых чисел при вводе
Очистка битов в ins или outs
аргументом 1
Установка битов в ins или outs
аргументом 1
Установка символа
заполнителя в n
Установка точности представления чисел с плавающей
точкой, равной n разрядам
Установка
ширины
поля
в значение n
Пример 5.
Применение функций и манипуляторов форматирования вывода
в программе.
#include<iostream.h>
#include<conio.h>
#include<iomanip.h>
void main()
{ clrscr();
int i=36, j=45;
cout<<"Вывод с установленной шириной поля 5 числа i:\n";
int oldw=cout.width(5);
// установка ширины поля вывода
cout<< i <<’\n’;
cout<<"После каждой вставки формата ширина поля=0:\n";
cout<< " oldw=" << oldw << ‘\n’;
cout<< i << ‘\n’;
cout<< "Установка поля 5 влияет только на 1-ю переменную:\n";
cout.width(5);
cout<< i << " " << j << ‘\n’;
cout<< "Манипулятор setw(5) упрощает вывод переменных:\n";
cout<< setw(5) << i << setw(5) << j << ‘\n’;
cout<< "Манипуляторы dec, hex, oct изменяют сист. счисл. \n";
cout<< "(оставляя эти изменения в силе) — вывод i:\n";
cout<< dec << i << " " << hex << i << " " << oct << i <<endl;
cout<< "Манипулятор endl аналог ‘\n’ очищает поток"<<endl;
cout<< "i= " << i <<endl;
cout<< "Заполнитель пробел можно заменить на *"<<endl;
cout.fill ( ‘*’ );
// задание символа заполнения функцией fill
17
cout<< setw(6) << i << endl; // установка ширины поля 6 числа i
cout<< "Выравнивание можно изменить функцией setf:"<<endl;
cout.width(6);
cout.setf(ios::left, ios::adjustfield);
cout<< "i= " << i <<endl;
getch();
}
Вывод чисел с плавающей точкой.
Большие и малые числа выводятся в экспоненциальном формате
(е, Е), а не фиксированном. Например, число 1234567.8 печатается
1.2345678Е+07.
Числа с нулевой дробной частью печатаются как целые. Например, число 95.0 печатается 95.
Для вывода десятичных чисел в фиксированном формате можно
использовать следующие операторы:
cout.setf(ios::fixed, ios::floated);
cout.setf(ios::showpoint);
Первый вызов функции setf обеспечивает печать чисел в фиксированном, а не экспоненциальном представлении. Второй вызов задает
печать десятичной точки, даже если дробная часть числа равна 0.
Управление числом десятичных позиций при выводе выполняется с помощью манипулятора установки точности:
cout<< setprecision(2) << x;
например, вместо числа 16.38567 печатается 16.39.
Пример 6.
Если отсутствует переназначение, то ввод с клавиатуры отображается на экран. Следующая программа копирует cin в cout.
#include<iostream.h>
void main()
{ char ch;
while (cin >> ch)
cout << ch;
}
Обратите внимание, что cin >> ch может рассматриваться как булевское выражение. Этот эффект стал возможен благодаря определениям в классе ios. А именно, такие выражения как (cout) или (cin
>> ch) рассматриваются как указатель на переменную, значение ко18
торой определяется кодом ошибки потока. Нулевой указатель (трактуемый как «ложь») индицирует ошибку в потоке, а ненулевой указатель («истина») означает ее отсутствие. Можно использовать операцию отрицания (!), в этом случае (!cout) будет «истина» при возникновении ошибки в потоке cout и «ложь», если они отсутствуют:
if (!cout) errmsg("Output error!");
cin — это входной поток, подключенный к стандартному выводу.
Он может правильно обрабатывать все стандартные типы данных.
Как известно, в С вывод подсказки (приглашения) без символа новой строки (‘\n’) в стандартный выходной поток stdout требует обращения к fflush(stdout), чтобы эта подсказка могла появиться. В С++
обращение к cin автоматически очищает cout.
Рассмотрим примеры простых программ на С++ с отличиями от
С-программ.
Пример 7.
Применение констант и встроенных функций (inline).
#include<iostream.h>
const float pi=3.14159;
inline float area (const float r) { return pi*(r)*(r); }
void main()
{ float radius;
cout << "Введите радиус круга: ";
cin >> radius;
cout << "Площадь круга= " << area (radius) << ‘\n’;
}
Идентификатор константы ведет себя как обычная переменная
(т.е. ее областью действия является блок, в котором она определена) за исключением того, что она не может находиться в левой части оператора присваивания. Директива #define является морально
устаревшей для С++.
Ключевое слово inline указывают компилятору, что, если возможно, следует вставлять код в том месте, где он встретил обращение. Это эквивалентно макрорасширению и используется там, где
необходимо ликвидировать накладные расходы, связанные с вызовом функции. Рекомендуется применять встроенную функцию, составленную из одной-двух строк.
19
Ссылки и указатели как параметры функции.
По умолчанию С и С++ передают аргументы функции, используя
вызов по значению, чем создается копия аргумента, которая может
быть изменена в функции, но исходное значение аргумента не меняется. Для изменения значения аргументов параметры функции объявляются как указатели (*параметр).
Пример 8.
Переставить значения двух аргументов в функции с указателями.
void swap1 (int *a, int *b)
{ int tmp;
tmp=*a; *a=*b; *b=tmp;
}
Для вызова функции необходимо задать адреса аргументов:
swap1(&i, &j);
// i, j — аргументы функции
В С++ можно использовать в функции ссылки (&) на изменяемые
параметры, а в теле функции и при вызове применяются имена параметров и аргументов:
void swap2 (int &a, int &b)
{ int tmp=a; a=b; b=tmp; }
Вызов функции:
swap2 (i, j);
20
Классы, инкапсуляция, объекты
Самое фундаментальное понятие объектно-ориентированного
проектирования и программирования заключается в том, что программа является моделью некоторых аспектов реальности. Классы
в программе представляют собой фундаментальные понятия прикладной области, и в частности, фундаментальные понятия моделируемой «реальности». Объекты реального мира и средства реализации представляются объектами классов.
Анализ отношений между классами и внутри частей класса –
это центральное место в проектировании системы. При разработке класса приходится учитывать разного рода отношения (внутри
класса, наследования, включения, использования), которые реализуются средствами языка. Поскольку класс в С++ – это тип, классы и взаимоотношения поддерживаются компилятором и в основном доступны статическому анализу.
Чтобы вписаться в проект, класс не просто должен представлять
собой полезное понятие – он должен обеспечить подходящий интерфейс для взаимодействия с другими классами. Идеальный класс
имеет минимальную и строго определенную зависимость от остального мира и интерфейс, предоставляющий минимум информации о
классе остальному миру.
Класс в С++ – это ключевое понятие. К определяемым пользователем в языке С типам struct (структура) и union (объединение) в
языке С++ добавляется тип class (класс). В языке С определение
структур данных и обрабатывающих их функций разделены. С++
избавляется от этого недостатка, используя принцип инкапсуляции (пакетирования) – соединения данных и функций их обработки (кода) в единое целое, получившем название класс.
В С++ отдельный класс (определяемый посредством типов struct,
union или class) включает в себя данные, называемые элементами
или членами, и функции, называемые методами или функциямичленами, класса. Как правило, конкретному классу присваивается
имя класса, несущее смысловую нагрузку, например, Point (точка).
Классы обеспечивают сокрытие информации, гарантированную
инициализацию данных, неявное преобразование определяемых
пользователем типов, динамическое определение типа, контроль
пользователя над управлением памятью и механизм перегрузки
операторов. С++ предоставляет лучшие, чем С, средства для проверки типов и поддержки модульного программирования. Кроме того,
С++ содержит усовершенствования, такие как: символические кон21
станты, встраивание функций в место вызова, аргументы функций
по умолчанию, перегруженные имена функций, операторы управления динамической памятью и ссылки. Это позволяет реализовывать типы, определяемые пользователем, с необходимой степенью
эффективности.
Управление доступом к элементам класса
Основное различие между классами С++ и структурами С состоит в правилах доступа к их элементам. Элементы структуры языка С доступны для любого выражения или функции из их области
действия. В С++ имеется возможность управления правами доступа
к элементам структуры, объединения или класса. Элементы класса
получают атрибуты доступа либо по умолчанию, либо при использовании спецификаторов доступа, которые имеют следующие наименования и значения:
public (общий, открытый) – элемент может быть использован любой функцией;
private (частный, закрытый) – элемент доступен только методам
класса и «друзьями « класса, в котором они объявлены;
protected (защищенный) – то же, что и private, однако элемент может быть использован методами и друзьями классов, производных
от объявленного класса, но только в объектах производного класса.
Когда говорят о классах в С++, то включают в это понятие не
только тип данных class, но также struct и union со своими правами
доступа.
Для типа class все компоненты являются по умолчанию private,
но можно изменить уровень доступа; для типа ctruct все компоненты по умолчанию — public, но можно изменить права доступа; для
типа union все компоненты по умолчанию — public и этот уровень не
может быть изменен.
Обычно ограничения на уровень доступа касаются элементов
данных: как правило, данные имеют атрибут private, а методы –
public. Этим ограничениям больше соответствует класс с ключевым
словом class, который и используется для разработки классов в программах.
Режим доступа (по умолчанию или заданный) действует для последующих объявлений элементов класса, пока не встретится другой атрибут доступа в любом порядке, например:
class X
22
// заголовок класса
{ int i;
char ch;
public:
int j, k;
protected:
int m;
};
// private по умолчанию
// общий
// защищенный
// определение класса заканчивается точкой
// с запятой (;)
Список элементов класса
Список элементов представляет собой последовательность объявлений данных (любого типа, включая нумераторы, битовые поля и другие классы) и объявлений и определений функций, каждое
из которых может иметь необязательные модификаторы доступа.
Определенные таким образом объекты называют элементами (членами) класса (class members). Спецификаторы класса памяти auto,
register, extern недопустимы. Элементы могут быть объявлены со
спецификатором static.
Функция-элемент класса называется методом класса, а функция с модификатором friend называется дружественной функцией.
Данные класса – это то, что класс «знает « (пассивная часть класса),
а методы – это то, что класс «умеет делать» (активная часть).
Определение методов класса
Существует два способа включения метода в класс.
Первый способ – метод может быть определен внутри класса как
встраиваемая функция с одним-двумя операторами. Компилятор
осуществляет макрорасширение таких функций, снижая накладные расходы, связанные с обращением к функции и выходом из нее,
например:
class Point
{ int X, Y;
int Getx() { return X; }
}
// встроенная функция
Второй способ – объявление метода внутри класса в виде прототипа функции и определение метода в произвольном месте программы с помощью операции определения области действия функции ::
(два двоеточия), например:
23
class Point
{ int X, Y;
int Getx();
}
int Point :: Getx()
{ return X; }
// объявление метода
// определение метода
// вне класса
Имя Point :: Getx называется полным или квалифицированным
именем метода класса. В С++ разные функции могут иметь функции с одинаковыми именами. Операция :: и имя класса позволяют
компилятору определить принадлежность функции конкретному
классу.
Определение метода внутри класса (способ 1) не требует модификатора Point::, так как и так очевидно, что Getx принадлежит классу
Point. Кроме того, модификатор Point:: перед Getx служит и другой
цели. Он определяет, что переменная Х в операторе return X является
собственным элементом класса Point, к которому можно обращаться непосредственно по имени.
Вывод. Тело метода Point::Getx() находится внутри области действия класса Point вне зависимости от физического расположения.
Метод Getx() имеет доступ ко всем внутренним переменным класса
Point.
Вызов метода и доступ к элементам класса
Методы выполняют операции над объектами класса. Поэтому
при обращении к функции Getx необходимо указать, с каким объектом класса Point следует работать. Если бы Getx была обычной
функцией языка С (или не методом С++) эта проблема не возникла
бы: функция вызывалась бы простым обращением к Getx().
Чтобы вызвать метод класса в той части программы, которая не
является частью класса, надо использовать имя объекта и операцию точка (.) для доступа к элементу класса, аналогично обращению к элементам структуры языка С, согласно форме:
имя_объекта . имя_метода (список аргументов)
имя_объекта . имя_элемента_данных
Например, пусть описаны объекты класса:
Point p1, p2, *pp;
Тогда возможные обращения к элементам класса имеют вид:
24
p1. X;
p2.Y;
p1. Getx();
// доступ к элементу Х объекта р1
// доступ к элементу Y объекта р2
// вызов метода Getx()
С другой стороны, метод класса может вызвать другой метод того же класса или использовать данные-элементы класса непосредственно, не используя операцию точка.
Вывод. Операцию точка надо использовать только тогда, когда
метод класса вызывается в функциях, не являющихся элементами
класса.
Если имеется указатель на объект типа Point, следует применять
операцию вызова метода посредством указателя, используя операцию «стрелка» ( ->): pp->Getx().
Рассмотрим программу, которая иллюстрирует особенности языка С++, обсуждавшиеся выше.
Пример 9.
Программа моделирования работы класса «очередь» с фиксированной длиной очереди.
#include<iostream.h>
#include<conio.h>
const int sizeq= 5 ;
class Queue
{ int q[sizeq]; int head, tail;
public:
void init(void); void qput(int);
int qget(void); void show();
};
// описание методов класса:
void Queue::init(void)
{ tail=head=0;
for(int i=0; i<sizeq; i++)
q[i]=0;
}
void Queue::qput(int i) { if(tail==sizeq)
{ cout << "Очередь полна ";
// размер очереди
// класс «очередь»
// массив очереди
// начало и конец очереди
// прототипы методов класса:
// инициализация очереди
// постановка в очередь
// выход из очереди
// вывод данных очереди
// метод инициализации очереди
// исходное обнуление очереди
// метод постановки в очередь
// если все места заняты, то сообщение
25
return; }
q[tail++]=i;
// возврат из функции
// увеличение очереди
}
int Queue::qget(void)
// метод выхода из очереди
{ if(head==tail)
// если достигнут конец очереди, то сообщение
{
cout << "Очередь пуста ";
return 1111;
// возврат какого-либо значения
}
int i=q[head];
// значение уходящего элемента очереди
q[head++];
// сдвиг начала очереди к концу
return i; // возврат значения элемента очереди
}
void Queue::show()
// метод вывода значений очереди
{cout<<"Значения: head="<<head<<" tail="<<tail<<"\nЭлементы очереди: ";
for(int i=0; i<sizeq; i++)
cout<<q[i]<<" ";
cout<<"\n";
}
int main()
// главная функция
{ int i;
clrscr(); // чистка экрана результатов
cout<<"Создаем два объекта a, b:"<<"\n";
Queue a, b;
// созданы объекты-очереди a, b
cout<<"Инициализация очереди а: ";
a.init();
a.show();
// вывод данных очереди а
// заполнение очереди а значениями:
a.qput(7);
a.qput(9);
a.qput(11);
cout<<"Очередь а: ";
a.show();
// вывод данных очереди а
cout<<"Вывод данных очереди а в цикле:\n";
for(i=0; i<sizeq; i++)
// цикл вывода из очереди а
cout << a.qget() << " ";
cout<<"\n";
a.show();
// вывод данных очереди а
cout<<"\nИнициализация очереди b: ";
b.init();
b.show();
// вывод данных очереди b
26
for(i=0; i<sizeq+1; i++)
// цикл заполнения очереди b
b.qput(i*i);
cout<<"\nОчередь b: ";
b.show();
// вывод данных очереди b
cout<<"Вывод данных очереди b в цикле:\n";
for(i=0; i<sizeq+1; i++) // вывод данных в цикле
cout<<b.qget()<<"";
cout<<"\n";
b.show();
// вывод данных очереди b
getch(); // задержка экрана результатов
return 0;
// нормальное окончание программы
}
Результаты программы:
Создаем два объекта a, b:
Инициализация очереди а: Значения: head=0 tail=0
Элементы очереди: 0 0 0 0 0
Очередь а: Значения: head=0 tail=3
Элементы очереди: 7 9 11 0 0
7 9 1 Очередь пуста 1111
Значения: head=3 tail=3
Элементы очереди: 7 9 11 0 0
Инициализация очереди b: Значения: head=0 tail=0
Элементы очереди: 0 0 0 0 0
Очередь полна
Очередь b: Значения: head=0 tail=5
Элементы очереди: 01 4 9 16
Вывод данных очереди b в цикле: 01 4 9 16 Очередь пуста 1111
Значения: head=5 tail=5
Элементы очереди: 01 4 9 16
Особенностью этого примера является то, что функция main() стоит не на первом месте, как в программах на языке С. Обычно в программах С++ методы классов определяются перед main(). По мере
возрастания объема программ объявления классов следует выделять в файлы заголовков, а определения методов класса располагать
в отдельно компилируемых исходных файлах С++.
Конструкторы и деструкторы
При создании объекты обычно инициализируются. Например,
в классе очереди Queue (пример 9) это было реализовано функцией
27
init(). Среди функций класса имеются специальные методы, которые
определяют особенности создания, инициализации, копирования и
уничтожения объектов данного класса. Их называют конструкторы
и деструкторы. Как и обычные методы, конструкторы и деструкторы могут описываться в пределах или вне класса.
Конструктор это метод, основной целью которого является
инициализация переменных объекта данного класса или распределение памяти для их хранения (constructor – создатель объекта).
С++ предоставляет возможность делать это автоматически при объявлении объекта, то есть при его создании. С объектом всегда связано либо явное, либо не явное выполнение конструктора. Конструктор вызывается в тот момент, когда создается объект класса, то есть
ему выделяется место в памяти. Нельзя вызвать конструктор в явном виде в программе как обычную функцию. Он вызывается явно
компилятором при создании объекта и неявно при выполнении оператора new, применяемого к объекту, а также при копировании объекта данного класса. Если конструктор не описан, то компилятор
создает автоматически конструктор по умолчанию.
Конструктор имеет то же имя, что и класс, в котором он определен, но не может иметь тип возвращаемого значения, в том числе void. Конструктор в большинстве случаев никаких сообщений
не выдает. Он может иметь параметры, в том числе по умолчанию,
может быть перегруженной функцией, то есть класс может иметь
несколько разных конструкторов. Конструктор, не имеющий аргументов, называется конструктором по умолчанию.
Пример 10.
Представим объявление и описание конструктора в классе
Queue.
class Queue
// класс "очередь"
{ int q[sizeq]; // массив очереди
int head, tail;
// начало и конец очереди
public:
// прототипы методов класса:
Queue (void); // это объявление конструктора в классе
void qput(int);
// постановка в очередь
int qget(void); // выход из очереди
void show();
// вывод данных очереди
};
// описание (определение) конструктора Queue вне класса:
28
Queue :: Queue (void)
{ tail=head=0;
for(int i=0; i<sizeq; i++)
q[i]=0;
}
// исходное обнуление очереди
Метод класса деструктор (destructor) разрушает объект данного
класса и может вызываться явно с помощью оператора delete, либо неявно, выполняя действия перед окончанием работы объекта
(освобождение памяти, восстановление экрана, закрытие файлов и
т. д.). Если деструктор не объявлен явно, компилятор генерирует
его автоматически.
Деструктор имеет имя класса с символом тильда (~) перед ним.
В отличие от конструктора он не может получать аргументы и быть
перегруженной функцией.
Для статических объектов память распределяется компилятором: конструктор вызывается перед main, а деструктор — после main.
Деструктор вызывается неявно, когда автоматическая (auto) переменная выходит из области действия. Например, объект является
локальным внутри функции, и функция возвращает управление. В
случае глобальных переменных деструкторы вызываются как часть
процедуры выхода после main.
При создании динамического объекта с помощью оператора new
для удаления объекта, хранящегося в динамической памяти, явно
используется оператор delete.
Деструкторы вызываются строго в обратной последовательности
вызова соответствующих конструкторов.
Инициализация конструкторов класса
Объекты классов с конструкторами могут быть инициализированы следующими способами.
Первый – при помощи задаваемого в круглых скобках списка
выражений, который используется как список передаваемых конструктору аргументов.
Второй – использование знака присваивания (=), за которым следует отдельное значение. Оно может иметь тип первого аргумента,
принимаемого конструктором данного класса, в этом случае дополнительных аргументов либо не существует, либо они имеют значения по умолчанию. В этом случае для создания объекта вызывается соответствующий конструктор. Значение может также являть29
ся объектом данного типа класса. В таком случае вызывается конструктор копирования, инициализирующий объект.
Пример 11.
Демонстрация работы конструктора и деструктора в программе.
#include<iostream.h>
#include<conio.h>
class Number
// описание класса
{ int i;
// элемент private
public:
// прототипы методов класса
Number (int j);
// объявление конструктора
~Number();
// объявление деструктора
void show();
// вывод на экран
};
Number :: Number (int i)
// заголовок конструктора
{ i=j;
// инициализация элемента данных
класса
cout<<"Работает конструктор: i="<< i <<'\n';
}
Number :: ~Number()
// заголовок деструктора
{ cout<<"Работает деструктор: i="<< i <<'\n';
}
void Number :: show()
// метод вывода данных на экран
{ cout<<"i= "<< i <<’\n’;
}
void main()
// главная функция
{ clrscr();
// чистка экрана результатов
Number ob1(25);
// вызов конструктора и создание объектов
Number ob2(5);
cout<<"Вывод из объекта ob1: ";
ob1.show();
// вывод данных объекта
cout<<"Вывод из объекта ob2: ";
ob2.show();
}
Результаты программы:
Работает конструктор: i=25
Работает конструктор: i=5
Вывод из объекта ob1: i=25
30
Вывод из объекта ob1: i=5
Работает деструктор
Работает деструктор
Объекты класса могут объявляться (значит, и создаваться) сразу после описания класса до точки с запятой. Конструктор может
иметь любое число параметров. Например, он может принимать в
качестве аргумента имя самого объекта.
Пример 12.
Демонстрация работы программы с объектом, созданным встроенным конструктором до начала действий функции main(). Имя объекта является параметром конструктора. Показан порядок конструирования объектов и разрушения встроенным деструктором.
#include<iostream.h>
#include<conio.h>
class Number1
// описание класса
{ char *str;
// указатель на строку
public:
Number1 (char *name) // встроенный конструктор
{ cout<<"Создан объект с именем " <<name<<" класса Number1\n";
str = name;
// присваивание имени элементу str класса
}
~Number1 // встроенный деструктор
{ cout<< "Объект " << str <<" разрушен\n";
}
} one ("one");
// создание объекта one с инициализацией
void main()
{ // clrscr();
// после чистки экрана не виден объект one
Nimber1 two («two»); // вызов конструктора для объекта two
// getch();
// при задержке не видно работы деструктора
}
Результаты программы:
Создан объект с именем one класса Number1
Создан объект с именем two класса Number1
Объект two разрушен
Объект one разрушен
31
Комментарий к программе.
Для статических объектов память распределяется компилятором: конструктор вызывается перед main, а деструктор – после
main. Если конструктор не определен, С++ использует неявный или
встроенный. В случае автоматических объектов освобождение памяти происходит, когда нарушается область действия (заканчивается включающий блок). Любой определенный программистом деструктор вызывается, когда уничтожаются статические или автоматические объекты.
В примере компилятор автоматически вызвал конструктор при
входе в программу для создания элемента локального статического
объекта one. При создании автоматического объекта two конструктор был вызван явно. При выходе из функции main() завершается область определения объекта two и компилятор неявно вызывает деструктор. Второй раз деструктор вызывается при завершении всей
программы для разрушения объекта one. Таким образом, разрушение объектов деструктором выполняется в порядке, обратном их
созданию конструктором.
Полиморфизм. Перегрузка функций
Слово полиморфизм греческого происхождения: "наличие в пределах
одного вида резко отличающихся по облику особей". В языке С каждая
функция должна иметь уникальное имя. Одним из путей реализации полиморфизма в С++ является перегрузка функций (overloading), то есть
использование одного имени для разных функций. Компилятор различает
такие функции по числу и типу параметров. В С++ могут перегружаться
любые функции, в том числе и методы класса. При этом действуют следующие ограничения.
1. Не могут перегружаться функции, имеющие совпадающие
тип и число аргументов, но разные типы возвращаемых значений.
Например, если объявить функции: int fun(long i) и long fun(long i), то
при вызове функции: fun(70000) перед компилятором встанет неразрешимая проблема выбора, так как при вызове функции тип возвращаемого значения никак не может быть указан.
2. Не могут перегружаться функции, имеющие неявно совпадающие типы аргументов, например, int и int&.
Пример 13.
Рассмотрим механизмы описания и использования перегружаемых функций получения суммы двух аргументов.
32
#include<iostream.h>
// перегрузка функции add:
int add(int i, int j)
{ cout<<"Сложение целых чисел\n";
return i + j;
}
double add(double i, double j)
{ cout<<"Сложение вещественных чисел\n";
return i + j;
}
void main ()
{ cout << "2 + 3=" << add(2,3) << ‘\n’;
cout << "2.5 + 2.0=" << add(2.5, 2.0) << ‘\n’;
}
Полезность таких функций в том, что они повышают ясность
программ. Особенно удобна перегрузка конструкторов, инициализирующих объекты.
Перегрузка конструкторов
Конструкторы могут быть перегружены, что позволяет создавать
объекты в зависимости от значений, используемых при инициализации.
Пример 14.
Демонстрация перегруженных конструкторов.
class X
{ int integer_part;
double double_part;
public:
X ( int i ) { integer_part = i; } // конструктор с инициализацией
X ( double d)
// перегруженный конструктор
{ double_part = d; }
};
void main()
{ X one (10);
// вызов конструктора X::X(int)
X two (3.14);
// вызов конструктора
X::X(double)
}
33
Функция с параметрами по умолчанию
С++ разрешает описание функций или объявление их прототипов с формальными параметрами, имеющими значение по умолчанию, которые должны быть последними в списке при объявлении
функции.
Например:
char func (char ch, int i, int k=5)
{ тело функции }
В функции func при ссылке на нее с тремя аргументами, например, при вызове функции:
ret = func ('A', 10, 25);
параметры получат значения: ch = 'A', i = 10, k = 25.
Но если при вызове функции список аргументов будет состоять
из двух элементов, например:
ret = func ('A', 10);
то параметры получат значения: ch = 'A', i = 10, k = 5 (по умолчанию).
Если функция описана, например, так:
char Fun (int a = 5, int j = 7)
{ тело функции },
то она может вызываться с двумя или одним аргументом или даже
вовсе без аргументов.
Конструктор по умолчанию
Конструктор, не имеющий параметров, называется конструктором по умолчанию. Если отсутствует определенный пользователем
конструктор по умолчанию, компилятор С++ генерирует собственный конструктор по умолчанию для описанного класса – это конструктор, например, класса Х, который не принимает никаких аргументов X :: X().
Конструктор, как и любая функция, может иметь любое количество параметров, в том числе и параметры по умолчанию. Например, конструктор
X :: X (int, int=0)
может принимать один или два аргумента. Если будет только один
аргумент, то недостающий второй аргумент будет принят как 0.
Аналогично, конструктор
X :: X (int=5, int=6)
34
может принимать два аргумента, один аргумент, либо не принимать
аргументов вообще, причем в каждом случае принимаются значения, соответствующие умолчаниям.
Однако конструктор по умолчанию X :: X() не принимает аргументов вообще, и его не следует путать с конструктором, например,
X :: X (int=0), который может либо принимать один аргумент, либо не
принимать аргументов. При вызове конструкторов следует избегать
неоднозначностей. В следующем примере возможна неоднозначная
трактовка компилятором конструктора по умолчанию и конструктора, принимающего целый параметр.
Пример 15.
Неоднозначность конструкторов.
class X
{ public:
X();
X( int = 0);
};
void main()
{ X one (10);
X two;
}
// так МОЖНО: используется конструктор X::X(int)
// так НЕЛЬЗЯ: X::X() или X::X(int=0) ?
Конструктор копирования
Это особый тип конструктора. Конструктор копирования для
класса Х – это конструктор, который имеет один аргумент ссылочного типа X& и, возможно, параметры по умолчанию. Ссылочный тип
тесно связан с типом указатель (X*) и позволяет передавать в функцию не сам объект, а только ссылку на него.
По умолчанию, объекты класса можно копировать. В частности,
объект некоторого класса можно проинициализировать при помощи копирования объекта того же класса. Это можно сделать даже
там, где объявлен конструктор.
Например, для класса X корректное описание конструктора копирования может быть таким:
class X
{ public:
X() {...}
X(const X&) {...}
// конструктор по умолчанию
// конструктор копирования
35
X(const X&, int i=4) {...}
};
// конструктор копирования с // параметром по умолчанию
Конструктор копирования вызывается тогда, когда выполняется
копирование объекта, обычно при объявлении объекта с его инициализацией, например:
X one;
// вызов конструктора по умолчанию
X two = one;
// вызов конструктора копирования
X two (one);
// вызов конструктора копирования с параметром
По умолчанию, копия объекта класса содержит копию каждого члена, т.е. объект two будет полной копией объекта one. Аналогично, объекты класса могут по умолчанию почленно копироваться
при помощи операции присваивания:
X three;
three = one;
Подобное почленное копирование приемлемо, если оно выполняется для статических и автоматических переменных класса. Выделение динамической памяти для объектов с использованием указателей и их копирование будет рассмотрено ниже.
Обобщение перегрузки конструкторов
и инициализации объектов
Перегрузка – важная концепция языка С++. Суть ее в том, что
несколько различных функций (методов или обычных) определяются с одним именем в рамках единой области действия. Основная
идея состоит в том, что вызовы перегруженных функций определяются путем сравнения типов аргументов с типами формальных параметров в определении функции.
Среди возможных осложнений при использовании перегруженных функций – функции с аргументами по умолчанию, с переменным числом аргументов. Когда компилятор сталкивается с сильно
перегруженной функцией, он стремится установить наибольшее соответствие между вызовом функции и соответствующим прототипом функции. Если такое соответствие установить не удается, компилятор генерирует сообщение об ошибке.
Наиболее распространенный случай использования перегруженных функций – перегрузка конструкторов для обеспечения
36
нескольких вариантов создания и инициализации новых объектов
класса.
Пример 16.
Демонстрация возможных случаев использования перегрузки
конструкторов и инициализации объектов.
class X
{ int i;
public:
X ();
X ( int x );
X (const X&);
};
void main ()
{ X one;
X two (1);
X three = 1;
X four = one;
X five ( two );
}
// тела функций для ясности опущены
// 1 – конструктор по умолчанию
// 2 – конструктор с параметром
// 3 – конструктор копирования
// вызов
// вызов
// вызов
// вызов
// вызов
конструктора 1
конструктора 2
конструктора 2
конструктора 3
конструктора 3
Конструктор может присваивать значения своим элементам двумя способами.
Первый – он может принимать значения в качестве параметров и
выполнять присваивания элементам-переменным собственно в теле
конструктора, например:
class X
{
int a, b;
public:
X (int i, int j) { a = i; b = j; }
};
Второй – он может использовать находящийся до тела конструктора список инициализаторов, например:
class X
{
};
int a, b;
public:
X (int i, int j) : a( i ), b( j ) { тело конструктора }
37
В обоих случаях инициализация вида X x (1, 2); присваивает значения элементам класса: a = 1, b =2.
Второй способ, а именно список инициализаторов, обеспечивает механизм передачи значений конструкторам базового класса из
производного при наследовании.
Указатель объекта this
При перегрузке операций используется скрытый указатель объекта на самого себя – *this. Вызов метода устанавливает неявный
указатель на объект, по отношению к которому осуществляется вызов. Он предоставляет функциям возможность доступа к действующим объектам. Если this — указатель на класс Х, то возвращаемое
значение должно быть *this.
К отдельным элементам объекта, задействованным в вызове метода, можно обращаться, используя выражение вида this -> элемент.
Замечание. Указатель this доступен только для методов данного
класса, но не для дружественных функций.
Пример 17.
Рассмотрим короткую программу, иллюстрирующую действие
указателя this.
#include<iostream.h>
class X
{
int i;
public:
void loadi ( int val )
{ this -> i = val; }
// аналогично i = val;
int geti ()
{ return this -> i; }
// аналогично return i;
}
void main ()
{
X c;
c.loadi (100);
cout << c.geti ();
}
Результат: вывод числа 100
Полиморфизм. Перегрузка операций
Отличительной особенностью С++ является способность перегрузки (переопределения) существующих операций. Встроенные
38
операции, такие как +, являются изначально перегруженными даже в языке С.
Пример 18.
Перегрузка встроенной операции +.
void main ()
{
int i = 2, j = 3; k;
float a = 2.5, b = 1.3, c;
k = i + j;
c = a + b;
cout<< "k=" << k <<" c=" << c;
}
Результаты: k=5 c=3.8
Для операндов типа int и float используется одна и та же операция, но, очевидно, что код генерируется разный, так как целые и вещественные числа представляются в памяти по разному. С++ просто расширяет эту идею, давая возможность переопределять операции на уровне пользователя.
За небольшим исключением большинству операций языка С++
может быть придано специальное значение относительно вновь
определенных классов. Для встроенных типов данных значение
операции изменить нельзя. Например, класс, который определяет
список указателей, может использовать операцию + для добавления
объекта в список.
Когда операция перегружена, ни одно из ее исходных значений
не теряется. Просто вводится новая операция относительно нового конкретного класса. Для этого создается специальная функцияоперации, которая определяет действие этой операции согласно
форме:
тип имя_класса :: operator # ( список аргументов)
{
// операторы, определяющие действие функции-операции
...
},
где тип – тип возвращаемого значения, # – конкретный знак операции.
Часто возвращаемое значение того же типа, что и класс, хотя
возможен и другой тип этого значения. Функция-операции должна
39
быть или элементом класса или дружественной функцией, при этом
имеются небольшие отличия.
Пример 19.
Рассмотрим программу, в которой перегружаются операции:
+, ++, --, = относительно класса Vector, который определяет трехмерный вектор в евклидовом пространстве с координатами x, y, z. Операция сложения двух векторов выполняется как сложение соответствующих координат.
#include<iostream.h>
#include<conio.h>
class Vector
{ int x, y, z;
public:
Vector(int mx=0, int my=0, int mz=0);
Vector operator + (Vector t);
Vector operator = (Vector t);
Vector operator ++ (); Vector operator - - ();
void show(void);
};
Vector::Vector(int mx, int my, int mz) { x=mx; y=my; z=mz; }
// Перегрузка операции +
Vector Vector::operator + (Vector t)
{ Vector temp;
temp.x = x + t.x;
temp.y = y + t.y;
temp.z = z + t.z;
return temp;
}
// Перегрузка операции =
Vector Vector::operator = (Vector t) { x = t.x;
y = t.y;
z = t.z;
return *this; 40
// координаты вектора
// конструктор с 3-мя
// параметрами по умолчанию
// функция-операция +
// функция-операция =
// функция-операция ++
// функция-операция - // функция вывода координат
// конструктор с 3-мя параметрами
// инициализация 3-х координат
// использование указателя this
// temp.x = this->x + t.x; и т. д.
// использование указателя this
// this->x = t.x; и т. д.
// возврат указателя this
}
// Перегрузка операции ++ (унарной префиксной и постфиксной
одинаково)
Vector Vector::operator ++ ()
{
x++; y++; z++;
// увеличение координат на 1
return *this; // возврат указателя this
}
// Перегрузка операции -- (унарной префиксной и постфиксной
одинаково)
Vector Vector::operator -- ()
{
x--; y--; z--;
// уменьшение координат на 1
return *this; // возврат указателя this
}
void Vector::show(void)
// функция вывода 3-х
координат
{
cout <<" x=" << x << " y=" << y << " z=" << z << ‘\n’;
}
void main()
// главная функция
{ clrscr();
// чистка экрана
Vector a(3, 2, 1), b(10, 20, 30), c; // три объекта с инициализацией
cout<<"вектор исходный а: "; a.show();
// вывод вектора а
cout<<"вектор исходный b: "; b.show(); // вывод вектора b
c=a + b;
// перегруженный оператор +
cout<<"вектор c=a+b: "; c.show();
// вывод вектора с
c=a+b+c;
// перегруженный оператор +
cout<<"вектор c=a+b+c: "; c.show(); // вывод вектора с
c = b = a; // перегруженный оператор =
cout<<"вектор c=b=a: ";
c.show(); // вывод вектора с
++b;
// перегруженный оператор ++
cout<<"вектор ++b: "; b.show();
// вывод вектора b
b- -; // перегруженный оператор -cout<<"вектор b- -: ";
b.show(); // вывод вектора b
getch();
}
Результаты программы:
вектор исходный а: x=3, y=2, z=1
вектор исходный b: x=10, y=20, z=30
41
вектор c=a+b: x=13, y=22, z=31
вектор c=a+b+c: x=26, y=44, z=62
вектор c=b=a: x=3, y=2, z=1
вектор ++b: x=4, y=3, z=3
вектор b- -: x=3, y=2, z=1
Комментарии к программе.
Как видно, функция-операции имеет только один параметр, когда сами функции определяют бинарные операции (+, =). Первый операнд использует this-указатель, а второй – передается как параметр
функции.
Если используются методы, то не нужны параметры для унарных операций и требуется лишь один параметр для бинарных операций. Объект, активизирующий операцию, передается в функциюоперацию неявно через указатель this.
В этом примере важно то, что возвращаемое значение имеет тип
vector, что позволяет использовать выражение вида a+b+c. Перегруженная операция + не изменяет значения своих операндов, а перегруженная операция = модифицирует операнд слева.
Динамическое выделение памяти
Для работы с объектами переменного размера необходимо выделять динамическую память нужного объема, с использованием
в языке С функций malloc(), calloc(). Однако С++ имеет операторы
new и delete, которые делают динамическое размещение и удаление
объектов более простым, надежным, и они гарантируют вызов конструкторов и деструкторов. Пары конструктор/деструктор являются типичным механизмом для работы с объектами переменного размера.
Операция динамического выделения памяти имеет синтаксис:
указатель = new имя <инициализатор_имени>
Имя может быть любого типа кроме функции, однако указатели на функции допустимы. Оператор new пытается создать объект
с типом имени, распределив (при возможности) sizeof (имя) байтов в
динамической области памяти ("куче"). Продолжительность существования в памяти данного объекта – от точки его создания и до
тех пор, пока операция delete не отменит распределенную для него
память, либо до конца программы. В случае успешного завершения
new возвращает указатель нового объекта. Объекты, распределяе42
мые new, за исключением массивов, могут инициализироваться соответствующим выражением в круглых скобках:
int_ptr = new int (3);
Если имя – это массив, то возвращаемый new указатель ссылается на первый элемент массива. Для размещения массива используется такая форма записи:
указатель = new тип [size],
где size – размер массива (в элементах).
Например, для динамического размещения массива:
int counts [50];
counts = new int [50];
При создании динамического объекта с помощью new программист несет ответственность за его последующее уничтожение, так
как компилятор не может узнать, когда объект больше не нужен.
Для его уничтожения используется оператор delete (стереть), при
этом вызывается соответствующий деструктор:
delete указатель_на_объект;
Для уничтожения массива используется форма:
delete [ ] указатель;
Например, delete [ ] counts;
Приводимая далее программа демонстрирует одно из наиболее
популярных применений конструкторов и деструкторов – распределение памяти оператором new и возврат выделенной памяти оператором delete. Конструктору будет передаваться параметр "максимальный размер области памяти", выделяемой динамически под
вводимую строку.
Пример 20.
Демонстрация работы программы с динамическим объектом
(строкой переменной длины) с помощью операторов new, delete. Длина строки является параметром для конструктора.
#include<iostream.h>
#include<conio.h>
#include<string.h>
class String1
{ char *str;
// указатель на строку
43
int maxlength;
// максимум длины строки
public:
String1 (int size) // конструктор с параметром размера
строки
{ str = new char [size];
// выделение динамической памяти строке
maxlength = size;
// инициализация длины строки
}
~String1()
// встроенный деструктор
{ delete str;
// освобождение динамической памяти
}
int inputstr ();
// прототип метода ввода строки
void outputstr ();
// прототип метода вывода строки
};
int String1:: inputstr ()
// метод ввода строки
{ cout<<"Введите строку не более " << maxlength << " символов:\n";
cin.get(str, maxlength);
// чтение строки с пробелами
return strlen(str);
// возврат длины строки
}
void String1:: outputstr ()
// метод вывода строки
{ cout << str <<endl;
}
void main ()
// главная функция
{ clrscr();
int length;
// длина строки
String1 mystr = String (128); // вызов конструктора
length = mystr.inputstr ();
// ввод строки и возврат ее длины
cout << "Введенная строка имеет длину " << length << " символов:\n";
mystr.outputstr();
// вывод строки на экран
}
Результаты программы:
Введите строку не более 128 символов:
Это короткая строка.
Введенная строка имеет длину 20 символов:
Это короткая строка.
Динамическое выделение памяти в конструкторе
Можно выделять динамически память под объекты любого типа,
определенного к этому моменту. Под простые переменные динамически выделять память невыгодно, так как она расходуется не толь44
ко под переменные, но и под информацию о выделении памяти. Целесообразно выделять память под большие объекты, например, массивы неизвестного заранее размера. Это можно делать в конструкторе класса, элементом которого является массив. При этом динамическое выделение памяти можно использовать и для объекта.
Выше (пример 9) была рассмотрена модель очереди фиксированной длины, но более адекватной является модель очереди с переменным размером.
Пример 21.
Рассмотрим работу программы с классом Queue (очередь) с переменной длиной очереди.
#include<iostream.h>
#include<conio.h>
class Queue
// класс "очередь"
{ int *q; // указатель очереди
unsigned size;
// переменная размера очереди
int head, tail;
// начало и конец очереди
public:
// прототипы методов класса:
Queue (int sz); // конструктор очереди
~Queue();
// деструктор
void qput(int i);
// постановка в очередь
int qget(); // выход из очереди
void show();
// вывод данных очереди
};
// описание методов класса:
Queue :: Queue(int sz)
// конструктор инициализации очереди
{ size=sz;
// инициализация размера очереди
q=new int [size];
// выделение памяти заданного размера
tail=head=0;
// исходное обнуление очереди
for(int i=0; i<sizeq; i++) q[i]=0;
cout<<"Очередь размера " << size <<" инициализирована\n";
}
Queue :: ~Queue ()
// деструктор очереди
{ delete q;
cout << "Очередь разрушена деструктором явно\n";
}
void Queue :: qput (int i)
// метод постановки в очередь
{ if ( tail == size )
// если все места заняты, то сообщение
45
{ cout << "Очередь полна ";
return;
}
q [ tail++ ] = i;
// увеличение очереди
}
int Queue :: qget ( ) // метод выхода из очереди
{ if ( head==tail )
// если достигнут конец очереди, то сообщение
{ cout << "Очередь пуста ";
return 1111; // возврат какого-либо значения
}
int i = q [ head ];
// значение уходящего элемента очереди
q [ head++ ];
// сдвиг начала очереди к концу
return i; // возврат значения элемента очереди
}
void Queue::show() // метод вывода приватных данных очереди
{ cout<<"Значения: head="<<head<<" tail="<<tail<<" Элементы очереди: ";
for (int i=0; i<sizeq; i++) cout<< q [ i ] <<" ";
cout<<"\n";
}
int main()
// главная функция
{ clrscr();
// чистка экрана результатов
cout <<"Действия очереди переменного размера:\n";
Queue *pq;
// указатель на очередь
int s;
// переменная размера очереди
cout <<"\nВведите размер очереди s= ";
cin >> s;
// ввод размера очереди
pq = new Queue (s);
// динамическое создание очереди
if (! pq)
// проверка объема динамической памяти
{ cout << "Недостаточно памяти\n";
return 0;
// аварийный выход из программы
}
else cout << "Объект класса Queue создан\n";
for(i=0; i < s; i++)
// цикл заполнения очереди данными
pq -> qput (2*i+1);
cout <<"\nОчередь pq: ";
pq -> show();
// вывод значений элементов очереди
cout << "Вывод элементов из очереди pq: ";
for (i=0; i<s; i++)
// цикл вывода из очереди
cout << pq -> qget() << " ";
46
cout << "\n";
pq -> show();
// вывод значений элементов очереди
delete pq;
// разрушение очереди явно
cout << "Очередь pq разрушена\n";
getch();
// задержка экрана результатов
return 1;
// нормальное окончание программы
}
Результаты программы:
Действия очереди переменного размера:
Введите размер очереди s= 8
Очередь размера 8 инициализирована
Объект класса Queue создан
Очередь pq: Значения: head=0 tail=8 Элементы очереди: 1 3 5 7 9 11 13 15
Вывод элементов из очереди pq: 1 3 5 7 9 11 13 15
Значения: head=8 tail=8 Элементы очереди: 1 3 5 7 9 11 13 15
Очередь разрушена деструктором явно
Очередь pq разрушена
Копирование динамических объектов
Конструкторы инициализируют объект, то есть они создают среду, в которой работают методы класса. Иногда создание такой среды
подразумевает захват каких-то ресурсов – таких как файл, память,
которые должны быть освобождены после использования. Некоторым классам требуется функция, которая будет гарантированно
вызвана при уничтожении объекта – это деструктор, который очищает память и освобождает ресурсы. Деструктор вызывается неявно, когда автоматическая переменная выходит из области видимости, удаляется объект, хранящийся в динамической памяти и т.д.
Наиболее часто деструктор используется для освобождения памяти
выделенной конструктором. Пары конструктор/деструктор являются типичным механизмом в С++ для работы с объектами переменного размера.
Пример 22.
Рассмотрим модельный пример, в котором есть массив элементов
типа Name. Конструктор класса Table должен выделить область динамической памяти для хранения элементов массива. Для освобождения памяти используется деструктор ~Table.
47
class Name
{
const char* s;
...
};
class Table
{
Name* p;
int size;
public:
Table ( int s=15 )
{ p = new Name[ size=s ]; }
~Table { delete [ ] p; }
...
};
// символьный указатель
// указатель на массив
// размер массива
// конструктор с параметром по умолчанию
// выделение динамической памяти
// деструктор освобождения памяти
Функция, использующая класс Table:
void f ()
{
Table t1;
Table t2 = t1;
// копирующая инициализация — проблема!
Table t3;
t3 = t2;
// копирующее присваивание — проблема!
}
Так как t1 и t2 являются объектами класса Table, выражение t2 =
t1 по умолчанию означает почленное копирование t1 в t2 (см. выше).
При наличии у объекта элементов, являющихся указателями, такая интерпретация присваивания может вызвать неожиданный и
обычно нежелательный эффект.
Указатели используются для динамического выделения памяти
объектам переменного размера. Почленное копирование обычно является неправильным при копировании объектов, имеющих ресурсы, управляемые парой конструктор/деструктор.
В этом примере конструктор Table по умолчанию вызван дважды – по одному разу для t1 и t3. Он не вызывается для t2, так как эта
переменная проинициирована копированием. Однако деструктор
Table вызывался три раза – по одному разу для t1, t2 и t3!
По умолчанию копирование интерпретируется как почленное
копирование, поэтому t1, t2 и t3 к концу функции f() будут содержать
указатели на массив имен (Name* p), выделенный в динамической
памяти при создании t1. Не осталось указателя на массив имен, выделенный при создании t3, так как он перезаписан присваиванием
t3 = t2.
48
В результате при отсутствии автоматической сборки мусора эта
память будет навсегда потеряна для программы. С другой стороны,
массив, созданный для t1, будет и в t1, и в t2, и в t3, поэтому он будет
трижды удаляться. Результат непредсказуем и, вероятно, приведет
к катастрофе!
Можно избежать подобных аномалий, определив, что понимать
под копированием Table:
class Table
{
...
Table ( const Table&);
Table& operator = (const Table&);
};
// копирующий конструктор
// копирующее присваивание
Программист может определить любой подходящий смысл этих
операций копирования, но традиционным решением является поэлементное копирование хранимых элементов.
Например:
// копирующий конструктор:
Table :: Table (const Table& t)
{
p = new Name [ sz = t.sz ];
for (int i = 0; i < sz; i++)
p[ i ] = t.p[ i ];
}
// выделение памяти
// поэлементное копирование
// в цикле
// перегрузка оператора присваивания (функция-операции = ):
Table& Table :: operator = (const Table& t)
{
if ( this != &t)
// проверка: не присваивание ли
// самому себе (t=t)!
{
delete [ ] p;
// уничтожение старого массива
p = new Name [ sz = t.sz ];
// выделение новой памяти
for (int i = 0; i < sz; i++)
p[ i ] = t.p[ i ]; // поэлементное копирование
}
return *this;
// возвращение скрытого указателя
}
49
Копирующий конструктор и копирующее присваивание значительно отличаются. Основная причина различия состоит в том, что
копирующий конструктор инициализирует "чистую" (неинициализированную) память, в то время как копирующий оператор присваивания должен корректно работать с существующим объектом.
Основная стратегия при реализации оператора присваивания проста: 1) защита от присваивания самому себе, 2) удаление старых
элементов, 3) инициализация и копирование новых элементов. Как
правило, все нестатические элементы должны быть скопированы.
Пример 23.
Рассмотрим работу программы с перегруженным конструктором для получения трех вариантов создания новых объектов класса
«строка» с динамическим выделением памяти для строки. Использована перегрузка операции + для сложения двух строк (конкатенация) и операции = для копирующего присваивания.
#include<iostream.h>
#include<conio.h>
#include<string.h>
// библиотека функций для работы со строками
class String
// класс "строка"
{ char *str;
// указатель на строку
int length;
// длина строки в символах
public:
// конструкторы:
String(char *text);
// 1- й использует существующую строку
String(int size=80); // 2- й с параметром размера строки по
умолчанию
String(String& other_str);
// 3- й копирования строки по ссылке
~String()
// встроенный деструктор
{ delete str; }
// освобождение динамической
памяти
String operator + (String& arg);
// функция-операции
// сложения двух строк
String& String::operator=(String& st); // функция-операции
// присваивания
int getlen(void);
// прототип метода возврата длины строки
void showstr(void); // прототип метода вывода строки на экран
};
// Описание методов класса
50
String::String(char *text)
// конструктор с указателем
// на готовую строку
{ length=strlen(text);
// длина строки по функции из <string.h>
str=new char[length+1]; // выделение динамической памяти
// для строки
strcpy(str, text);
// копирование строки функцией из <string.h>
}
String::String(int size) // конструктор с параметром размера строки
{ length=size;
// инициализация длины строки
str=new char[length+1]; // выделение динамической памяти
// для строки
*str='\0';
// символ конца строки
}
String::String(String& other_str)
{ length=other_str.length; str=new char [length + 1];
strcpy(str, other_str.str); }
// конструктор копирования по ссылке (&)
// длина другой строки по ссылке
// выделение динамической памяти
// строке
// копирование другой строки в str
String String::operator + (String& arg) // функция-операции + для
// строк
{ String temp(length + arg.length); // инициализация с длиной
// двух строк
strcpy(temp.str, str); // копирование строки str
strcat(temp.str, arg.str);
// конкатенация двух строк
return temp;
// возврат объекта с
// содержанием двух строк
}
String& String::operator=(String& st)
// функция-операции
// присваивания
{ if (this != &st)
// проверка: не присваивание
ли самому себе (t=t)!
{ delete str;
// уничтожение старой строки
length=st.length;
// длина новой строки
str=new char[length+1];
// выделение динамической памяти
новой строке
strcpy(str,st.str);
// копирование новой строки
}
return *this;
// возвращение скрытого указателя
51
}
int String::getlen(void)
{ return (length); }
void String::showstr(void) { cout<< str <<'\n'; }
// функция определения длины строки
// возврат длины строки
// функция вывода строки с пробелами
void main()
// главная функция
{ clrscr();
// чистка экрана
String Astring("This is the ready string.");// создание строки с текстом
cout<<"Astring: "; Astring.showstr(); // вывод строки Astring
String Bstring;
// инициализация по умолчанию
cout <<"Max length Bstring= " << Bstring.getlen() <<’\n’; // длина=80
Bstring="Bstring to Cstring by reference"; // инициализация
// присваиванием
cout<<"Bstring: "; Bstring.showstr(); // вывод строки Bstring
String Cstring(Bstring);
// копирование Bstring в Cstring по ссылке
cout<<"Cstring: "; Cstring.showstr(); // вывод строки Сstring
Astring="The quick brown fox"; // инициализация
// присваиванием
cout<<"Astring: "; Astring.showstr(); // вывод строки Аstring
Вstring=" jumps over Bill.";
// инициализация присваиванием
cout<<"Bstring: "; Bstring.showstr(); // вывод строки Bstring
Cstring = Astring + Bstring;
// сложение строк с присваиванием
cout<<"Cstring = Astring + Bstring:\n";
Cstring.showstr();
// вывод строки Сstring
// getch();
}
Результаты программы:
Astring: This is the ready string.
Max length Bstring= 80
Bstring: Bstring to Cstring by reference
Cstring: Bstring to Cstring by reference
Astring: The quick brown fox
Bstring: jumps over Bill.
Cstring = Astring + Bstring: The quick brown fox jumps over Bill.
Комментарии к программе.
Перегруженная операция + использует только один явный аргумент. Компилятор интерпретирует выражение Astring + Bstring как
Astring.(operator + (Bstring)), результатом является возможность досту52
па к двум строковым объектам. Функция-операции использует указатель *this для слагаемых, например, первая и вторая строки в теле
функции-операции + аналогичны таким:
String temp(this->length + arg.length);
strcpy(temp.str, this->str);
Действие перегруженной операции копирующего присваивания
было рассмотрено выше (пример 22).
53
НАСЛЕДОВАНИЕ И КОНТРОЛЬ ДОСТУПА
Базовые и производные классы
Простое наследование.
Обычно классы не существуют сами по себе. Как правило, программа работает с несколькими различными, но связанными структурами данных.
В С++ решением проблемы «похожие, но различные» является
разрешение классам наследовать характеристики и поведение от
одного или нескольких классов. Этот интуитивный переход и составляет, пожалуй, наибольшее различие между языками С и С++.
Одной из важнейших концепций языка С++ является возможность наследования свойств одного класса другим — это простое наследование, что можно показать так: A<-B (класс В наследник класса
А). Класс, на основе которого строится другой класс, называется базовым (base) классом (предок, родитель). Построенный на его основе
новый класс называется производным (derived) (потомок). Из одного
класса могут порождаться многие классы:
Базовый класс
Производный класс 1
...
Производный класс N
Производный класс сам может быть базовым для других классов, что порождает иерархию классов:
Базовый класс А
Производный класс В
Базовый класс для С
Производный класс C
Класс С — производный от А и В
Контроль доступа в классах.
Производный класс наследует из базового данные-элементы и
функции-элементы в зависимости от режимов доступа в базовом
классе и модификаторов доступа при наследовании.
Шаблон описания производного класса имеет вид:
class Производный_класс : модификатор_доступа Базовый_класс
{
тело производного класса;
} объекты производного класса (не обязательно) ;
54
например:
class Point : public Location { . . .};
Как известно, в С++ элементы класса могут иметь один из трех
режимов доступа: public (общий), private (закрытый), protected (защищенный). Общие элементы могут быть доступны другим функциям программы. Закрытые элементы доступны только функциямэлементам или функциям-друзьям класса. Защищенные элементы
также доступны только тем же функциям.
Модификаторы доступа (public, protected, private (по умолчанию))
в производном классе используются для изменения прав доступа к
наследуемым элементам базового класса в соответствии с установленными правилами.
Режим доступа
в базовом классе
private
protected
public
private
protected
public
private
protected
public
Модификатор доступа
в производном классе
public
protected
private
Права доступа к элементам базового класса из производного
нет доступа
protected
public
нет доступа
protected
protected
нет доступа
private
private
Как видно из таблицы, в производных классах доступ к элементам базовых классов может быть только ужесточен, но не облегчен. Кроме того, производный класс наследует из базового данные и
функции-элементы, но не конструкторы и деструкторы.
При создании новых классов необходимо ясно понимать взаимосвязь между базовыми и производными классами и влияние модификатора доступа. С++ позволяет изменять права доступа без раскрытия данных перед методами, не принадлежащими к данному
семейству классов или друзьям. То есть элементы базового класса,
которые предполагается использовать в производном классе, должны быть либо public, либо protected.
Модификатор доступа public не изменяет уровня доступа, то есть
protected элементы базового класса остаются protected и в производном классе, что гарантирует их недоступность везде, кроме других производных классов, объявленных с модификатором public, и
функций-друзей.
Наследование типа private используется в классах по умолчанию,
являясь одновременно и наиболее распространенным способом на55
следования. Таким образом, получаем довольно редкую ситуацию,
при которой то, что задано по умолчанию является и нормой.
Элементы класса protected — это нечто среднее между открытыми и закрытыми элементами.
При выборе режима доступа к элементам действуют следующие
правила:
— закрытые (private) элементы доступны только в классе, в котором они объявлены и функциям-друзьям;
— защищенные (protected) элементы доступны элементам их собственного класса и всем элементам производного класса и функциямдрузьям, но только в объектах производного класса. Вне класса защищенные элементы недоступны;
— открытые (public) элементы доступны во всей программе.
Порядок описания секций доступа произвольный, но если встраиваемая функция-элемент ссылается на другой элемент, он должен
быть объявлен до реализации функции-элемента. Во избежание
ошибок, рекомендуется всегда явно указывать модификаторы доступа, вне зависимости от установленных по умолчанию.
Пример 25.
Рассмотрим следующий класс.
class X
{ private:
int A;
// доступен только элементам этого класса
void fa();
// доступен только элементам этого класса
protected:
int B;
// доступен элементам этого класса и производного
void fb();
// доступен элементам этого класса и производного
public:
int C;
// доступен всем, кто его использует
void fc(); // доступен всем, кто его использует
};
Внешние операторы не могут вызывать методы fa, fb и использовать элементы А и В.
Когда базовый класс объявлен private для производного класса,
то это существенно влияет на наследуемые элементы.
Пример 26.
Рассмотрим следующие классы.
class Base
56
{
protected: int x;
public: int y;
};
class Derived: private Base { public: void f();
};
// защищенный элемент х
// общий элемент y
// класс Base закрыт!
Метод f() класса Derived имеет доступ к элементам х и у, наследованным от класса Base. Но поскольку Base объявлен закрытым классом для Derived, статус х и у изменился на private в классе
Derived.
Добавим класс Derived1, производный от класса Derived.
class Derived1: public Derived
{ public: void f1();
};
В этом классе метод void f1() не имеет доступа к элементам х и у, несмотря на то, что в классе они имели статус protected и public .
Пример 27.
Рассмотрим варианты наследования режимов доступа к элементам классов при простом наследовании по иерархии классов X <- Y
<- Z.
#include<iostream.h>
#include<conio.h>
class X
// базовый класс X для классов Y, Z
{ protected: int i, j;
// защищенные переменные доступные в Y, Z
public: // прототипы открытых методов:
void get_ij();
// ввод чисел
void put_ij(); // вывод чисел
};
// Класс Y производный от X с сохранением режимов доступа
// к элементам Х:
class Y: public X { int k; // private по умолчанию в классе Y
public: // прототипы открытых методов класса Y:
int get_k(); // возврат числа k
void make_k(); // вычисление результата k
};
57
// Класс Z производный от Y и Х имеет доступ к переменным i, j
// класса Х и нет доступа к закрытой переменной k класса Y:
class Z: public
Y
{ public:
void f();
// прототип функции, изменяющей i, j
};
// Описание методов классов:
void X::get_ij()
// функция ввода чисел i, j
{ cout<<"Введите два числа: ";
cin >> i >> j;
}
void X::put_ij()
// функция ввода чисел i, j
{ cout<<"i="<<i<<" j="<<j<<endl;
}
int Y::get_k()
// функция возврата числа k
{ return k;
}
void Y::make_k()
// функция вычисления числа k
{ k=i*j;
}
void Z::f()
// функция, изменяющяя i, j класса Х
{ i=2; j=3;
}
void main()
// главная функция
{ clrscr(); // чистка экрана
Y var1;
// создан объект var1 класса Y
Z var2;
// создан объект var2 класса Z
var1.get_ij();
// ввод чисел i, j
var1.put_ij();
// вывод чисел i, j
var1.make_k();
// вычисление числа k=i*j
cout<<"k=i*j="<<var1.get_k(); // вывод числа k=i*j
cout<<’\n’;
// переход на новую строку экрана
var2.f();
// изменение элементов i, j класса Х
// в классе Z
var2.put_ij();
// вывод чисел i, j
var2.make_k();
// вычисление числа k=i*j
cout<<"k=i*j="<<var2.get_k(); // вывод результата k=i*j
cout<<’\n’;
getch();
}
58
Результаты программы:
Введите два числа: 3 7
i=3 j=7
k=i*j=21
i=2 j=3
k=i*j=6
Если в данной программе заменить модификатор доступа в классе Y при наследовании класса X на private, то функция void Z::f() не будет иметь право доступа к переменным i, j.
Использование конструкторов с параметрами при наследовании
Рассмотрим два класса: базовый и производный с конструкторами.
class Base
// базовый класс
{ private: int count;
// закрытый элемент "счетчик"
public:
// открытые методы базового класса
Base() {count=0;}
// конструктор базового класса
void Setcount (int n)
// метод установки счетчика
{count=n;}
int Getcount ()
// метод возврата значения счетчика
{ return count;}
};
class Derived : public Base
// производный класс с доступом к
Base
{ public:
// открытые методы Derived
Derived() : Base() {}
// конструктор производного класса
void Changecount (int n) // метод изменения счетчика
{ Setcount (Getcount ()+n);}
};
Покажем взаимосвязи классов Base и Derived. Класс Derived есть
производный от класса Base с открытым доступом, сохраняющим
статус доступа к элементам класса Base.
Производный класс наследует count из базового класса. Но так
как count закрыт в Base, в Derived нельзя получить к нему непосредственный доступ, например,
void Changecount (int n) { count += n;}
// ???
59
Этот оператор вызовет сообщение об ошибке при компиляции,
поэтому необходимо использовать наследуемые методы: Setcount,
Getcount.
В отличие от других элементов базового класса конструкторы и
деструкторы никогда не наследуются. Обычно в производном классе имеется конструктор, если он есть и в базовом классе. В таком
случае конструктор производного класса должен вызывать конструктор базового класса. В строке Derived(): Base() { } объявляется
конструктор производного класса и вызывается конструктор базового класса. Нельзя вызвать конструкторы базового класса в операторах. В нашем примере тело конструктора пустое, но там могут
быть и операторы, например:
Derived (): Base() { cout<< "Объект инициализирован" << endl; }
Конструкторы не обязаны быть встраиваемыми. Например, производный класс объявлен так:
class Derived : public Base
{ public:
Derived();
// прототип
...
};
Реализация конструктора может иметь вид:
Derived::Derived()
// заголовок конструктора производного класса
: Base() // вызов конструктора базового класса
{ операторы конструктора} // тело конструктора
Конструкторы производных классов всегда сначала вызывают
конструктор базового класса, чтобы убедиться, что наследуемые
элементы данных правильно созданы и инициализированы. В данном случае конструктор базового класса устанавливает закрытый
элемент count=0. Если базовый класс, в свою очередь, является производным, то процесс вызова конструкторов продолжается по всей
иерархии. Если в некотором классе Х конструктор не определен,
то С++ создает конструктор по умолчанию вида: Х::Х(), то есть конструктор без аргументов. Если конструктор производного класса в
явном виде не вызывает один из конструкторов своих базовых классов, то инициализируется конструктор по умолчанию (без аргументов).
Производный класс (Derived) может не иметь конструктор, если
конструкторы базовых классов (Base) не имеют аргументов. Если же
конструктор базового класса имеет аргументы, то каждый произво60
дный класс должен иметь конструктор с аргументами для того, чтобы передать их в базовые классы по иерархии. Чтобы передать аргументы в базовый класс, нужно определить их после объявления
конструктора производного класса по форме:
Derived::Derived
(список аргументов): //
заголовок
конструк
// тора Derived
Base1 (список аргументов), ... , BaseN (список аргументов)
{ тело конструктора Derived ;},
где двоеточие (:) отделяет заголовок конструктора производного
класса от вызовов конструкторов базовых классов с их списками
аргументов. Список аргументов базового класса может состоять из
констант, глобальных параметров и/или параметров для конструктора производного класса. Объект инициализируется во время выполнения программы, поэтому можно использовать переменные.
Рассмотрим следующую задачу. Основным элементом графического изображения является отдельная точка на экране монитора
(пиксел). Информацию, определяющую точку, можно разделить на
два типа: 1-й – описывает положение точки на экране, то есть ее координаты; 2-й – задает ее состояние (цвет, видимость). Первое более
важно, так как, не зная координат, невозможно изобразить точку.
Таким образом, нужно создать более общий базовый класс с именем, например, Location, который должен содержать данные о координатах точки (х, у), а производный класс, например, Point наследует
все то, что имеет класс Location и, кроме того, добавляет специфическую информацию о точке.
Пример 28.
Рассмотрим программу простого наследования и использования конструкторов с параметрами в задаче установки пиксела с заданным цветом в точке экрана с координатами (х, у) в графическом
режиме. Создаются базовый класс Location для задания координат
точки экрана и производный класс для изображения точки на экране по схеме Location <- Point.
#include<iostream.h>
#include<conio.h>
#include<graphics.h>
const char* path="c:\\borlandc\\bgi";
// доступ к библиотеке графики
// путь к функциям
// библиотеки bgi
61
class Location // базовый класс
{ protected: int x, y;
// защищенные элементы для доступа из Point
public: // открытые методы класса Location:
Location (int Initx, int Inity); // прототип конструктора
int getx()
// метод возврата элемента х
{ cout<<"x="<<x<<endl; return x; }
int gety()
// метод возврата элемента у
{ cout<<"y="<<y<<endl; return y; }
void show_mess()
// метод вывода сообщения
{ cout<<"Нажмите любую клавишу"; }
};
// Класс производный от Location, сохраняющий режимы доступа (public)
class Point: public Location
{ int color;
// закрытый элемент класса Point
public:
// открытые методы класса Point
Point (int Initx, int Inity, int Initc); // прототип конструктора Point
void putpixel()
// метод вывода пиксела на экран
{ ::putpixel(x,y,color); // операция :: устраняет неоднозначность
// вызова метода и библиотечной // функции с таким же именем
};
// Описание конструктора базового класса:
Location::Location (int Initx, int Inity)
// заголовок конструктора
{ x=Initx; y=Inity;
// инициализация элементов класса
cout<<"Работает конструктор Location\n";
}
// :Описание конструктора производного класса
Point :: Point (int Initx, int Inity, int Initc): // заголовок конструктора Point
Location(Initx,Inity)
// вызов конструктора базового класса
{ color=Initc; cout<<"Работает конструктор Point\n";
// тело
//конструктора
}
void main()
// главная функция
{ int gr=DETECT, gm;
// данные графического режима
initgraph(&gr, &gm, path);
// инициализация графического режима
Point mypoint(300,200,4);
// создание и инициализация объекта
// (точка)
mypoint.putpixel();
// изображение точки на экрана
mypoint.getx();
// получение координаты х
mypoint.gety();
// получение координаты у
mypoint.show_mess(); // вывод сообщения
62
getch();
closegraph();
// задержка экрана результатов
// закрытие графического режима
}
Результаты программы:
Работает конструктор Location
Работает конструктор Point
x=300
y=200
Нажмите любую клавишу
На экране по заданным координатам (300, 200) должна быть
цветная точка.
Пример 29.
Рассмотрим еще один модельный пример иерархии простого наследования классов: X <- Y <- Z. Класс Х задает переменную х, класс
Y — переменную у, класс Z — переменную z=x*y. Каждый класс содержит функцию show(), которая различается по полным именам
X::show() и т.д. На этом примере можно экспериментировать, изменяя режим доступа к переменным х, у, модификатор доступа при наследовании классов.
В программе показана передача режима доступа к элементам
классов при простом наследовании с использованием конструкторов с параметрами и функций с одинаковыми именами в базовом и
производных классах.
#include<iostream.h>
#include<conio.h>
class X
// базовый класс X для классов Y, Z
{ protected: int x;
// защищенный элемент х, доступный в Y, Z
public:
// прототипы открытых методов
// класса X:
X(int i);
// конструктор с аргументом класса X
~X();
// деструктор класса X
int getx() { return x;}
// встроенная функция возврата x
void putx (int i) {x=i;}
// встроенная функция задания x
void show();
// функция вывода на экран для класса Х
};
// Класс Y производный от Х с сохранением режимов доступа
// к классу X:
class Y: public X 63
{ protected: int y;
// защищенная переменная, доступная в Z
public:
// прототипы открытых методов класса Y:
Y(int i, int j); // конструктор с аргументами класса Y
~Y(); // деструктор класса Y
int gety () { return y;} // встроенная функция возврата y
void puty (int i) { y=i;} // встроенная функция задания y
void show();
// функция вывода на экран для класса Y
};
// Класс Z производный от Y и Х имеет доступ к переменным Х::x и Y::у:
class Z: public Y
{ protected: int z;
// защищенная переменная z
public:
// прототипы методов класса Z:
Z(int i, int j);
// конструктор с аргументами класса Z
~Z();
// деструктор класса Z
void makez();
// функция вычисления z
void show();
// функция вывода на экран для класса Z
};
// Описание методов:
X::X(int i)
// конструктор с аргументом класса X
{ x=i; cout<<"Конструктор X\n";}
X::~X()
// деструктор класса X
{ cout<<"Деструктор X\n";}
void X::show()
// функция вывода на экран для класса Х
{ cout<<"x="<<x<<endl;}
Y::Y(int i, int j): X(i)
// конструктор Y передает аргумент i классу X
{ y=j; cout<<"Конструктор Y\n";}
Y::~Y()
// деструктор класса Y
{ cout<<"Деструктор Y\n";}
void Y::show()
// функция вывода на экран класса для Y
{ cout<<"x="<<x<<" y="<<y<<endl;}
Z::Z(int i, int j): Y(i,j)
// конструктор Z передает аргументы i, j классу Y
{ cout<<"Конструктор Z\n";}
Z::~Z()
// деструктор класса Z
{ cout<<"Деструктор Z\n";}
64
void Z::show()
// функция вывода на экран класса для Z
{ cout"x="<<x<<" y="<<y<<" z="<<z<<endl;}
void Z::makez() { z=x * y;}
// функция вычисления переменной z
// с использованием переменных из X,Y
void main()
{ clrscr(); Z zobj(3, 5);; zobj.makez(); zobj.show(); zobj.Y::show(); zobj.X::show(); zobj.putx(7); zobj.puty(9);
zobj.makez();
zobj.show(); zobj.Y::show(); zobj.X::show(); getch();
}
// главная функция
// чистка экрана
// создан и инициирован объект класса Z
// вычисление z=x*y
// вызов функции вывода для класса Z
// вызов функции вывода для класса Y !
// вызов функции вывода для класса X !
// изменение переменной х в классе Х
// изменение переменной у в классе Y
// вычисление z=x*y
// вызов функции вывода для класса Z
// вызов функции вывода для класса Y !
// вызов функции вывода для класса X !
// задержка экрана результатов
Результаты программы:
Конструктор X
Конструктор Y
Конструктор Z
x=3 y=5 z=15
x=3 y=5
x=3
x=7 y=9 z=63
x=7 y=9
x=7
Деструктор Z
Деструктор Y
Деструктор X
Конструкторы и деструкторы классов выводят сообщения, демонстрируя их вызовы при наследовании классов. Следует обратить
внимание на передачу параметров конструкторами классов: от производного своему базовому по цепочке Z <- Y<- X.
65
Попытка просто изменить режим доступа при наследовании с
public на private без каких-либо изменений в программе может вызвать ошибки компиляции. Для корректного изменения режимов
доступа необходимо использовать функции getx(), gety(), которые в
предыдущем примере не использовались. Можно попробовать изменить режим доступа к переменным x, y на private.
Множественное наследование
В рассмотренном выше примере классы X и Y по сути равноправны, но при использовании простого наследования класс Y отличается от класса X числом параметров конструктора. Избавится от неравноправия классов можно, используя множественное наследование.
Суть его в том, что производный класс может быть наследником нескольких базовых классов, например, для классов X, Y, Z по схеме наследования (X, Y) <- Z, которая в программе может принимать вид:
class Z : public X, public Y
{ тело класса Z };
Пример 30.
Изменим программу примера 29 в соответствии со схемой множественного наследования (X, Y) <- Z.
#include<iostream.h>
#include<conio.h>
class X
// базовый класс X для класса Z
{ protected: int x;
// защищенный элемент х, доступный в Z
public: // прототипы открытых методов класса X:
X(int i);
// конструктор с параметром класса X
~X(); // деструктор класса X
int getx () { return x; } // встроенная функция возврата x
void putx (int i) { x=i; } // встроенная функция задания x
void show();
// функция вывода на экран для класса Х
};
class Y // базовый класс Y для класса Z
{ protected: int y;
// защищенная переменная, доступная в Z
public: // прототипы открытых методов класса Y:
Y(int i);
// конструктор с параметром класса Y
~Y();
// деструктор класса Y
int gety () { return y; }
// встроенная функция возврата y
void puty (int i) { y=i; }
// встроенная функция задания y
66
void show();
// функция вывода на экран для класса Y
};
// Класс Z производный от Х и Y имеет доступ к переменным Х::x и Y::у:
class Z : public Х, public Y
{ protected: int z;
// защищенная переменная z
public:
// прототипы открытых методов класса Z:
Z(int i, int j);
// конструктор с параметрами класса Z
~Z();
// деструктор класса Z
void makez ();
// функция вычисления z
void show();
// функция вывода на экран для класса Z
};
// Описание методов:
X::X(int i)
// конструктор с параметром класса X
{ x = i; cout<<"Конструктор X\n"; }
// инициализация элемента х
X::~X()
// деструктор класса X
{ cout<<"Деструктор X\n"; }
void X::show()
// функция вывода на экран класса Х
{ cout<<" x = "<< x <<endl; }
Y::Y(int i): // конструктор с параметром класса Y
{ y = i; cout<<"Конструктор Y\n"; }
// инициализация элемента у
Y::~Y()
// деструктор класса Y
{ cout<<"Деструктор Y\n"; }
void Y::show()
// функция вывода на экран класса Y
{ cout<<" y = "<< y <<endl; }
Z::Z(int i, int j):X(i), Y(j)
// конструктор Z с параметрами i, j
// для классов X, Y
{ cout<<"Конструктор Z\n"; }
Z::~Z()
// деструктор для класса Z
{ cout<<"Деструктор Z\n"; }
void Z::show()
// функция вывода на экран класса Z
{ cout<<" z = "<< z <<endl; }
void Z::makez()
// функция вычисления переменной z
{ z=x * y; }
// с использованием переменных из X,Y
void main()
// главная функция
{ clrscr();
// чистка экрана
Z zobj(3, 5);; // создан и инициирован объект класса Z
zobj.makez();
// вычисление z=x*y
zobj.show(); // вызов функции вывода для класса Z
zobj. X::show(); // вызов функции вывода для класса X !
zobj. Y::show();
// вызов функции вывода для класса Y !
zobj.putx(7);
// изменение переменной х в классе Х
67
zobj.puty(9);
zobj.makez();
zobj.show();
zobj. X::show();
zobj. Y::show();
getch();
// изменение переменной у в классе Y
// вычисление z=x*y
// вызов функции вывода для класса Z
// вызов функции вывода для класса X !
// вызов функции вывода для класса Y !
// задержка экрана результатов
}
Результаты программы:
Конструктор X
Конструктор Y
Конструктор Z
z=15
x=3
y=5
z=63
x=7
y=9
Деструктор Z
Деструктор Y
Деструктор X
Полиморфизм. Виртуальные функции
Концепция полиморфизма является очень важной в ООП. Этот
термин, как показано выше, используется для описания процесса,
при котором различные реализации функции могут быть доступны
с использованием одного имени. В С++ полиморфизм поддерживается и во время компиляции, и во время исполнения программы.
Рассмотренные выше перегрузки функций и операций — это примеры полиморфизма во время компиляции.
Предположим, что нам требуется создать иерархию классов для
различных геометрических фигур (точка, линия, окружность, прямоугольник и др.) и разработать соответствующие методы для отображения фигур на экране компьютера (Show). При этом разные типы фигур
будут отличаться тем, каким методом они это делают. Те же вопросы
возникают для стирания, перемещения и других манипуляций с фигурами на экране. Рассмотренные ранее методы классов позволяют решить эту задачу определением функции Show для каждого класса. Но
такой подход имеет существенный недостаток. При введении нового
класса потребуется вносить изменения и выполнять повторную компиляцию вследствие появления нового метода с именем Show.
68
Заложенные в С++ механизмы определения того, какой метод
следует в данный момент использовать, позволяют решить эту задачу одним из трех способов.
Имеются отличия в типах параметров в объявлении функции,
например, при перегрузке функций – Show(int, char) и Show(int, *char)
не одно и то же.
Задана операция доступа к области действия, поэтому Circle::Show
отличается от Point::Show и ::Show.
Объект класса идентифицирует метод: Acircle.Show вызывает метод Circle::Show, а Apoint.Show – Point::Show. Аналогично и в случае
указателей на объекты: pointptr->Show инициирует Point::Show.
Любой из указанных способов позволяет определить нужную
функцию на этапе компиляции и поэтому носит название раннего
(или статического) связывания.
Стандартная графическая библиотека предоставляет в распоряжение пользователя объявления классов в соответствующих исходных h-файлах и методы в объектных obj-файлах или библиотечных lib-файлах. Накладываемые при этом ограничения раннего связывания не позволяют пользователю легко добавлять новые
классы. С++ предоставляет гибкий механизм для решения этих
проблем с помощью специальных методов, называемых виртуальными функциями.
Полиморфизм во время исполнения программы реализуется с
помощью виртуальных функций в классах, связанных отношением наследования. Виртуальные функции позволяют производным
классам обеспечивать разные версии функции базового класса.
Можно объявить виртуальную функцию в базовом классе и затем
переопределить ее в любом производном классе. Решение о том, какая именно версия должна исполняться в данный момент, определяется на этапе исполнения программы и поэтому носит название
позднего (или динамического) связывания. Класс, обеспечивающий
интерфейс для множества других классов, называется полиморфным типом.
Для простоты можно сказать, что виртуальная функция — это
функция, вызов которой зависит от типа объекта. На практике это
означает, что решение о том, какую функцию Show вызывать, откладывается до момента выявления типа объекта на стадии исполнения. В языке С можно передать объект данных функции, при этом
необходимо было задать его тип, когда писалась программа. В ООП
можно писать виртуальные функции так, чтобы объект сам определял, какую функцию необходимо вызвать во время исполнения про69
граммы. Это достигается использованием указателей на базовые типы и виртуальных функций.
Указатели на производные типы.
Для лучшего понимания виртуальных функций важно понимать
принцип взаимодействия классов в С++, связанных отношением наследования — указатели на базовый тип и на производный тип зависимы: указатель на базовый класс может ссылаться на объект этого
класса или любой объект производного класса, но указатель производного класса не может ссылаться на объекты базового класса:
Указатель на базовый класс
Объект базового класса
Объект производного
класса
Объект производного
класса
Указатель на
производный
класс
Пусть есть цепь наследования классов: А <- B <- C. В программе
объекты классов могут объявляться так:
A obA; B obB; C obC;
A* p;
// объявлен указатель на базовый класс типа A*
p = &obB;
// указателю р присвоен адрес объекта obB
Объект obB можно представлять как особый вид объекта класса
А (так золотая рыбка — особая разновидность рыб, но это рыба). Но
объект типа А не является особым видом объектов классов В и С. Например, вы обладаете многими чертами ваших родителей, наследуемых от них и их родителей (цвет волос, глаз). Ваши родители и их
родители – часть вас, но вы не являетесь их частью.
Все элементы класса С, наследуемые от классов А и В, могут быть
доступны через использование указателя р. Однако на элементы,
объявленные (собственные) в классе С нельзя ссылаться, используя
р. Если требуется иметь доступ к элементам, объявленным в производном классе, используя указатель на базовый класс, его надо
привести к указателю на производный класс так: (( С*)р) -> f(), где
функция f() является элементом класса С, причем внешние скобки
необходимы. И, наконец, указатель р изменяется при операциях ++
и -- относительно базового класса.
Пример 31.
Рассмотрим программу использования указателей на базовый
класс из производных классов по цепи наследования: Base <- Derive
<- Derive1.
70
#include<iostream.h>
#include<conio.h>
#include<stdio.h>
class Base // базовый класс Base
{ public:
void show(void)
{ cout<<"In Base class\n"; }
};
class Derive: public Base
// производный класс от Base
{ public:
void show(void)
{ cout<<"In Derive class\n"; }
};
class Derive1: public Derive
// производный класс от Base, Derive
{ public:
void show(void)
{ cout<<"In Derive1 class\n"; }
};
void main()
// главная функция
{ clrscr();
Base bobj, *pb;
// объект и указатель базового класса
Derive dobj, *pd;
// объекты и указатели производных классов
Derive1 d1obj, *pd1;
pb=&bobj;
bobj.show();
pb->show();
// указатель на объект класса Base
// вызов объектом функции show() класса Base
// вызов указателем функции show() класса Base
pd=&dobj;
dobj.show();
pd->show();
// указатель на объект класса Derive
// вызов объектом функции show() класса Derive
// вызов указателем функции show() класса Derive
pd1=&d1obj;
d1obj.show();
pd1->show();
// указатель на объект класса Derive1
// вызов объектом функции show() класса Derive1
// вызов указателем функции show() класса Derive1
pb=&dobj;
pb->show();
// базовый указатель на объект класса Derive
// вызов указателем функции show() класса Base !
71
pb=&d1obj;
pb->show();
// базовый указатель на объект класса Derive1
// вызов указателем функции show() класса Base !
((Derive1 *)pb)->show(); getch();
}
// вызов show() класса Derive1 // указателем Base, приведенным
// к типу Derive1
Результаты программы:
In Base class
In Base class
In Derive class
In Derive class
In Derive1 class
In Derive1 class
In Base class !
In Base class !
In Derive1 class
Виртуальные функции.
Полиморфизм во время исполнения программы поддерживается
использованием производных типов и виртуальных функций, которые объявляются со спецификатором virtual в базовом классе и переопределяемых в производных классах. При этом прототипы (интерфейс) таких функций должны быть одинаковы (имена, тип возвращаемого значения, число и типы аргументов не меняются). В противном случае функция будет рассматриваться как перегруженная,
а не виртуальная.
Виртуальная функция должна быть элементом класса (она не
может иметь спецификатор friend). Однако виртуальная функция
может быть "другом" другого класса. Допустимо, чтобы деструктор
имел спецификатор virtual, однако для конструктора это запрещено. Вследствие запретов и различий между перегрузкой обычных
функций и переопределением виртуальных функций для них часто
используется термин "замещение" (overriding).
Слово virtual означает "может быть замещено позднее в классе,
производном от этого". Если функция объявлена виртуальной, то
она сохраняет это свойство для любого производного класса (на любом уровне вложенности). Если в некотором производном классе
функция не замещает виртуальную функцию (не объявлена, про72
пущена или имеет другой прототип), то вызывается версия виртуальной функции базового класса. При замещении функции в производном классе спецификатор virtual можно не повторять, хотя это
не ошибка.
Пример 32.
Модифицируем программу примера 31 так, что в классе Base
объявим функцию show() как виртуальную:
class Base
{ public:
virtual void show(void)
{ cout<<"In Base class\n";}
};
// базовый класс Base
// виртуальная функция класса
В главной функции исключим операторы вызова объектом функции show() (например, bobj.show(); и др.) и оставим вызовы функции
show() только с помощью указателей (pb->show(); и др.).
Результаты программы:
In Base class
In Derive class
In Derive1 class
In Derive class
In Derive1 class
Особенность использования виртуальных функций — при вызове функции, объявленной виртуальной, определяется версия функции в зависимости от того, на объект какого класса будет ссылаться
указатель базового класса.
Базовый класс задает основной интерфейс виртуальных функций, который будут иметь производные классы. Но производные
классы задают свой метод согласно принципу полиморфизма: "один
интерфейс – разные методы". Отделение интерфейса и реализации
функций позволяет создавать библиотеки классов (class libraries).
Возникает вопрос: как осуществляется вызов виртуальной функции для активного объекта? Реализации компиляторов пользуются
технологией преобразования имени виртуальной функции в индекс в
таблице, содержащей указатели на функции, называемой "таблицей
виртуальных функций" (virtual function table — vtbl). Каждый класс
с виртуальными функциями имеет свою vtbl, идентифицирующую
его виртуальные функции. Это можно изобразить схематически:
73
Объект класса Base:
vtbl:
1
2
*pf1
*pf2
f1()
f2()
Вызов виртуальных функций реализуется как непрямой вызов
по vtbl. Эта таблица создается во время компиляции, а связывание
происходит во время выполнения программы (отсюда термин позднее связывание). Функции в vtbl позволяют корректно использовать
объект даже в тех случаях, когда ни размер объекта, ни расположение его данных не известны в месте вызова.
Пример 33.
Рассмотрим программу использования виртуальных функций
при расширяющемся наследовании. Базовый класс Figure описывает плоскую фигуру для вычисления площади, которой достаточно двух измерений. Виртуальная функция show_area() печатает значение площади фигуры. На основе этого класса создаются производные классы – Triangle (треугольник), Rectangle (прямоугольник),
Circle (круг с одним измерением), в которых определены конкретные формулы (методы) вычисления площадей. Классы связаны расширяющимся наследованием по схеме: Figure <- (Triangle, Rectangle,
Circle).
#include<iostream.h>
#include<conio.h>
class Figure
// базовый класс
{ protected:
// защищенные элементы, доступные
// в производных классах
double x, y;
public:
void set_dim (double i, double j=0) // 2-й параметр по умолчанию
{ x = i, y = j; }
// задание измерений фигур
virtual void show_area()
// виртуальная функция Figure
{ cout<<"Площадь не определена для Figure\n";}
};
class Triangle: public Figure
// производный класс
{ public:
void show_area()
// виртуальная функция Triangle
{ cout<<"Треугольник с высотой "<< x <<" и основанием "<< y;
cout<<" имеет площадь = "<< x*0.5*y <<"\n";
}
74
};
class Rectangle: public Figure // производный класс
{ public:
void show_area()
// виртуальная функция Rectangle
{ cout<<"Прямогольник со сторонами "<< x <<" и "<< y;
cout<<" имеет площадь = "<< x*y <<"\n";
}
};
class Circle: public Figure
// производный класс
{ public:
void show_area()
// виртуальная функция Circle
{ cout<<"Круг с радиусом "<< x;
cout<<" имеет площадь = "<< 3.14*x*x <<"\n";
}
};
void main ()
{ clrscr();
Figure f, *p;
// объявление объекта и указателя класса Figure
Triangle t;
// объявление объекта класса Triangle
Rectangle r;
// объявление объекта класса Rectangle
Circle c;
// объявление объекта класса Circle
p = &f;
// указатель базового типа Figure
p -> set_dim (1,2);
// задание размерностей объекта Figure
p -> show_area();
// вывод сообщения о площади объекта типа Figure
p = &t;
// базовый указатель на объект типа Triangle
p -> set_dim (3,4);
// задание размерностей объекта типа Triangle
p -> show_area();
// вывод сообщения о площади объекта типа Triangle
p = &r;
// базовый указатель на объект типа Rectangle
p -> set_dim (5,6);
// задание размерностей объекта типа Rectangle
p -> show_area();
// вывод сообщения о площади объекта
// типа Rectangle
p = &c;
// базовый указатель на объект типа Circle
p -> set_dim (2);
// задание размерностей объекта типа Circle
p -> show_area();
// вывод сообщения о площади объекта типа Circle
getch();
// задержка экрана результатов
}
Результаты программы:
Треугольник с высотой 3 и основанием 4 имеет площадь = 6
Прямогольник со сторонами 5 и 6 имеет площадь = 30
Круг с радиусом 2 имеет площадь = 12.56
75
Пример 34.
Рассмотрим программу использования виртуальных функций
в классах с конструкторами. Вводится базовый класс Value, который задает некоторое значение. В этом классе описана виртуальная
функция getvalue(), печатающая полученное значение. На основе
этого класса строится производный класс Mult, вычисляющий произведение двух чисел. Он тоже имеет виртуальную функцию. Оба
класса используют конструкторы.
#include<iostream.h>
#include<conio.h>
class Value
// базовый класс
{ protected:
int value;
public:
Value (int n) { value = n; }
// конструктор с параметром
virtual int getvalue () { return value; } // виртуальная функция
};
class Mult: public Value
// производный класс
{ protected:
int mult;
public:
Mult (int n, int m): Value (n) // конструктор производного класса
{ mult = m;}
int getvalue () { return value * mult;}
};
void main ()
{ clrscr ();
cout<<"Работа программы";
Value *basep;
// указатель базового типа
basep = new Value (10);
// дин-ое создание объекта класса Value
cout<<"Для базового класса Value число = "<<basep->getvalue()<<endl;
delete basep;
// освобождение динамической
// памяти объекта Value
basep = new Mult (10, 2); // базовый указатель на объект типа Mult
cout<<"Для производ. класса Mult число = "<<basep->getvalue()<<endl;
delete basep;
//освобождение динамической памяти
// объекта типа Mult
}
76
Результаты программы:
Работа программы
Для базового класса Value число = 10
Для производ. класса Mult число = 20
Обычные или виртуальные функции.
Поскольку обычные функции выполняются несколько быстрее,
чем виртуальные, то в том случае, когда скорость работы важнее,
чем возможность расширения, следует использовать обычные функции. В противном случае, предпочтение следует отдавать виртуальным функциям.
Предположим, что объявляется класс Base с методом Action. Должен ли он быть виртуальным? Общее правило: если существует
вероятность, что в производном от Base классе будет использована функция, перекрывающая Action, которой нужен доступ к Base,
то Action должна быть виртуальной. Но она должна быть обычной
функцией, если очевидно, что в производных типах Action будет выполнять те же действия (даже если это вызывает инициацию других
виртуальных функций) или же производные типы не будут пользоваться Action.
Чистые виртуальные функции и абстрактные классы
Когда виртуальные функции вызываются из производного класса,
но не замещаются, то вызывается функция базового класса. Однако
во многих случаях нет смыслового определения виртуальной функции в базовом классе. Например, в базовом классе Figure примера 33
для функции show_area() используется просто "заглушка" с сообщением. При создании библиотеки классов для виртуальных функций
еще неизвестно, будет ли смысловое содержание в контексте базового
класса. Существует два способа справиться с этой проблемой.
Первый — выдать предупреждающее сообщение. Но это полезно
не во всех случаях. Например, может быть виртуальная функция,
которая должна быть определена в производном классе, и иметь некоторое значение. В классе Figure площадь в общем случае не определена.
Другим решением этой проблемы в С++ являются чистые виртуальные функции (pure) – это функции объявленные в базовом классе как виртуальные, но не имеющие описания. Производный класс
должен определить свою собственную версию виртуальной функции, так как нельзя просто использовать версию базового класса.
Иначе говоря, чистая виртуальная функция – это пустое место,
77
ожидающее своего заполнения производным классом. Объявляется
чистая виртуальная функция, как и обычная виртуальная, но завершается =0.
Например, в классе:
class AbstractClass
{ public:
virtual void f1 ();
virtual void f2 () = 0;
};
// виртуальная функция
// чистая виртуальная функция
Компилятору не потребуется реализация функции f2, в отличие
от остальных функций, поскольку она не содержит своего тела.
Если класс содержит хотя бы одну чистую виртуальную функцию, он называется абстрактным классом (abstract class). Подобно
любой абстрактной идее, абстрактный класс описывает нереализованные концепции.
Абстрактный класс имет одну важную черту – нельзя создать объект этого класса. Например, нельзя объявить объект типа
AbstractClass, иначе компилятор выдаст ошибку. Причина, по которой абстрактные классы не могут использоваться для объявления
объекта та, что одна или более функций не имеют определения.
Это всего лишь схема, на основе которой можно создать производный класс. Он должен использоваться только как интерфейс и в
качестве базового класса, от которого наследуются другие классы.
Иначе говоря, абстрактный класс представляет собой интерфейс к
множеству реализаций общего понятия. Конечно, другие функцииэлементы класса AbstractClass могут вызывать чистую виртуальную
функцию. Например, функция f1 может вызвать f2:
void AbstractClass :: f1()
{ f2 ();
// вызвать чистую виртуальную функцию
...
}
Чтобы использовать абстрактный класс, следует вывести из него
новый класс:
class Myclass : public AbstractClass
{ public:
virtual void f2 (); // бывшая чистая виртуальная функция
...
}
78
Производный класс Myclass наследует чистую виртуальную
функцию, но объявляет ее без =0. В другом месте следует реализовать функцию-элемент f2:
void Myclass :: f2 ()
{ . . . }
// операторы функции-элемента
Обращения к первоначальной чистой виртуальной функцииэлементу, к примеру, в данном случае AbstractClass:: f1(), теперь преадресованы Myclass :: f2(). Чистые виртуальные функции подобны
крючкам, на которые вы можете цеплять код производных классов.
Теперь, когда все виртуальные функции-элементы реализованы,
компилятор сможет воспринять объекты типа Myclass:
Myclass myobject;
Выполнение оператора myobject.f1 приведет к вызову наследованной функции f1, которая и вызовет f2 из Myclass.
Абстрактный класс не может быть использован как тип аргумента и как тип возврата функции. Однако, даже если базовый класс
абстрактный, можно создать указатель на объект базового класса,
если при инициализации не требуется создания временного объекта, или ссылку и применить их для использования механизма виртуальных функций. Например:
class Point { /*…*/ };
class Shape
// абстрактный класс
{ Point center;
// объект класса Point
public:
move(Point p) { center=p; draw(); } // вызов draw допустим
virtual void rotate(int)=0;
// чистая виртуальная
функция
virtual void draw()=0; // чистая виртуальная функция
} shape x;
// ошибка: попытка создания объекта
// абстрактного класса
shape f();
// ошибка: абстрактный класс
// не может быть типом возврата
int q(shape s);
// ошибка: абстрактный класс не может
// быть типом аргумента функции
shape* sp;
// указатель на абстрактный класс допустим
shape& h(shape&);
// ссылка на абстрактный класс в качестве типа
// возврата или аргумента функции допустима
79
Предположим, что В является производным классом от абстрактного базового класса А. Тогда для каждой чистой виртуальной функции fun() в А класс В должен обеспечивать определение fun, либо объявлять fun как чисто виртуальную функцию. Если этого не сделано,
то компилятор сообщит об ошибке. Например, описание производного класса от Shape с замещением чисто виртуальных функций:
class Circle: public Shape
// производный класс
{ int radius;
public:
virtual void rotate(int) { }
// действия отсутствуют
void draw ();
// замещение Shape:: draw
};
Circle c;
Абстрактные классы часто не предназначены для создания дальнейших производных классов. Порождение классов чаще всего используется для реализации. Однако из абстрактного класса можно сконструировать новый интерфейс, создав производный от него
расширенный абстрактный класс. Тогда новый абстрактный класс
должен в свою очередь быть реализован путем создания производного неабстрактного класса.
Чисто виртуальная функция, которая не определена в производном классе, остается чисто виртуальной, поэтому такой производный класс, как и базовый, является абстрактным. Это позволяет
строить реализацию поэтапно:
class Poligon: public Shape
// новый абстрактный класс
{ public:
void rotate(int);
// замещение Shape:: rotate
// draw не замещен
};
Poligon p;
// ошибка: объявление объекта абстрактного
// класса
class Newpoligon: public Poligon
{ public:
void rotate(int);
void draw ();
};
Newpoligon np;
80
// производный класс
// замещение Shape:: rotate
// замещение Shape::draw
// правильно
Пример 35.
Рассмотрим модифицированную версию программы примера
33. В классе Figure функцию show_area() определим как чистую
виртуальную:
class Figure
// базовый класс
{ protected:
// защищенные элементы, доступные
// в производных классах
double x, y;
public:
void set_dim (double i, doublej=0) // 2-й параметр по умолчанию
{ x = i; y = j; }
// задание измерений фигур
virtual void show_area() = 0;
// чистая виртуальная функция
};
В производных классах остаются переопределенные функции
show_area(). В главной функции из объявления: Figure f, *p; необходимо удалить объект f и три строки операторов с ним связанных,
иначе компилятор укажет на ошибку создания объекта абстрактного типа.
Результаты программы:
Треугольник с высотой 3 и основанием 4 имеет площадь = 6
Прямогольник со сторонами 5 и 6 имеет площадь = 30
Круг с радиусом 2 имеет площадь = 12.56
Производные классы с конструкторами и деструкторами
Каждый класс — базовый и производный может иметь конструктор и деструктор. В каком порядке выполняются эти функции
при простом и множественном наследовании? Рассмотрим этот вопрос на модельных примерах.
Простое наследование.
Пример 36.
Программа иллюстрирует порядок работы конструктора и деструктора по схеме Base <- Derive1 <- Derive2. Она не делает ничего,
кроме создания объектов типа Derive1 и Derive2.
#include<iostream.h>
#include<conio.h>
class Base
{ public:
Base () { cout << "Конструктор базового класса Base\n"; }
81
~Base () { cout << "Деструктор базового класса Base\n"; }
};
class Derive1: public Base
{ public:
Derive1 () { cout << "Конструктор производного класса Derive1\n"; }
~Derive1 () { cout << "Деструктор производного класса Derive1\n"; }
};
class Derive2: public Derive1
{ public:
Derive2 () { cout << "Конструктор производного класса Derive2\n"; }
~Derive2 () { cout << "Деструктор производного класса Derive2\n"; }
};
void main ()
{ clrscr ();
Derive1 d1; cout << endl;
Derive2 d2; cout << endl;
getch();
}
Результаты программы:
Конструктор базового класса Base
Конструктор производного класса Derive1
Конструктор базового класса Base
Конструктор производного класса Derive1
Конструктор производного класса Derive2
Деструктор производного класса Derive2
Деструктор производного класса Derive1
Деструктор базового класса Base
Деструктор производного класса Derive1
Деструктор базового класса Base
// объект d1 создан
// объект d2 создан
// объект d2 уничтожен
// объект d1 уничтожен
Комментарии к программе:
Функции-конструкторы выполняются при каждом создании
объекта, начиная с класса Base — это естественный порядок выполнения. Для создания объекта производного класса необходима
инициализация базового класса, так что конструктор должен выполняться.
С другой стороны, деструктор в производном классе должен выполняться раньше выполнения деструктора базового класса. Если
деструктор базового класса выполнится раньше, то деструктор про82
изводного класса вообще не сможет выполниться. Поэтому деструкторы вызываются в порядке, обратном вызову конструкторов.
Множественное наследование.
В языке С++ производный класс может наследовать от нескольких базовых классов. При объявлении производного класса базовые
классы перечисляются через запятую. Причем при создании объекта конструкторы выполняются в порядке следования базовых классов слева направо.
Пример 37.
Программа иллюстрирует порядок работы конструктора и деструктора по схеме множественного наследования: (Base1, Base2) <Derive. Она не делает ничего, кроме создания объектов типа Base1,
Base2, Derive. Рассмотрим модифицированную программу примера
36.
#include<iostream.h>
#include<conio.h>
class Base1
{ public:
Base1 () { cout << "Конструктор базового класса Base1\n"; }
~Base1 () { cout << "Деструктор базового класса Base1\n"; }
};
class Base2
{ public:
Base2() { cout << "Конструктор производного класс Base2 \n"; }
~ Base2() { cout << "Деструктор производного класса Base2\n"; }
};
class Derive: public Base1, Base2
{ public:
Derive () { cout << "Конструктор производного класса Derive\n"; }
~Derive () { cout << "Деструктор производного класса Derive\n"; }
};
void main ()
{ clrscr ();
Base1 b1; cout << endl;
Base2 b2; cout << endl;
Derive d; cout << endl;
getch();
}
83
Результаты программы:
Конструктор базового класса Base1
Конструктор базового класса Base2
Конструктор базового класса Base1
Конструктор базового класса Base2
Конструктор производного класса Derive
Деструктор производного класса Derive
Деструктор базового класса Base2
Деструктор базового класса Base1
Деструктор базового класса Base2
Деструктор базового класса Base1
// объект b1 создан
// объект b2 создан
// объект d создан
// объект d уничтожен
// объект b2 уничтожен
// объект b1 уничтожен
Результаты работы программы показывают, что деструкторы
выполняются в порядке, обратном по отношению к выполнению
конструкторов.
Виртуальные деструкторы
Методы и деструкторы (но не конструкторы !) могут быть виртуальными. Виртуальные деструкторы обычно применяются, когда в
некотором классе необходимо удалить объекты производного класса, на которые ссылаются указатели на базовый класс. Типичной
является ситуация, когда динамически создается объект производного класса, а используется указатель на базовый класс.
Пример 38.
Рассмотрим простое наследование по схеме Base <- Derive1 <Derive2. Программа (на основе примера 36) иллюстрирует порядок
работы конструкторов и деструкторов в классах при динамическом
выделении памяти объекту производного класса через указатель на
базовый класс и уничтожение объектов с использованием виртуального деструктора. Если объявить деструктор базового класса виртуальным, то все деструкторы производных классов также являются
виртуальными.
#include<iostream.h>
#include<conio.h>
class Base
{ public:
Base () { cout << "Конструктор базового класса Base\n"; }
84
virtual ~Base () { cout << "Деструктор базового класса Base\n"; }
};
class Derive1: public Base
{ public:
Derive1 () { cout << "Конструктор производного класса Derive1\n"; }
~Derive1 () { cout << "Деструктор производного класса Derive1\n"; }
};
class Derive2: public Derive1
{ public:
Derive2 () { cout << "Конструктор производного класса Derive2\n"; }
~Derive2 () { cout << "Деструктор производного класса Derive2\n"; }
};
void main ()
{ clrscr ();
Base *pb = new Derive2;
// выделение памяти объекту Derive2
if (! pb)
{ cout << "Недостаточно памяти\n";
return 1;
// аварийное окончание программы
}
cout << endl;
delete pb;
// вызов всех деструкторов производных и
// базового классов
return 0;
}
Результаты программы:
Конструктор базового класса Base
Конструктор производного класса Derive1
Конструктор производного класса Derive2
// создание объекта
//типа Derive2
Деструктор производного класса Derive2
Деструктор производного класса Derive1
Деструктор базового класса Base
// уничтожение объек// та типа Derive2
Комментарии к программе:
При разрушении объекта с помощью операции delete через указатель на базовый класс были корректно вызваны деструкторы всех
классов в нужном порядке. Если бы деструктор базового класса не
был объявлен виртуальным, то вызывался бы только деструктор
85
базового класса (проверьте это). Динамическая память, выделенная объекту конструктором, при разрушении объекта обычным деструктором не освобождалась бы корректно.
Пример 39.
Рассмотрим модифицированную программу на основе примеров
33 и 35, в которой используется абстрактный базовый класс Figure с
чистой виртуальной функцией show_area() и виртуальным деструктором. Базовый класс Figure описывает плоскую фигуру с двумя измерениями, которые задаются конструктором со вторым параметром по умолчанию. Нельзя создать объект данного класса, но можно определить указатель на тип класса. От этого класса на основе
расширяющегося наследованиия строятся производные классы по
схеме: Figure <- (Triangle, Rectangle, Circle). В производных классах
определены свои функции show_area().
С помощью указателя на базовый класс можно сослаться на объекты производных классов и вывести данные о них на экран в процессе исполнения программы. Виртуальный деструктор последовательно уничтожает объекты в порядке обратном тому, в котором
они создавались конструктором.
#include<iostream.h>
#include<conio.h>
class Figure
// абстрактный базовый класс
{ protected:
// защищенные элементы, доступные в про
// изводных классах
double x, y;
public:
Figure (double i, double j=0) // конструктор с параметром
// по умолчанию
{ x = i; y = j; // задание измерений фигур
cout<<"Конструктор базового класса Figure\n";
}
virtual void show_area() = 0;
// чистая виртуальная функция
virtual ~Figure ()
// виртуальный деструктор Figure
{ cout<<"Деструктор базового класса Figure\n";}
};
class Triangle: public Figure
// производный класс Triangle
{ public:
Triangle (double i, double j):Figure ( i, j)
// конструктор Triangle
{ cout<<"Конструктор Triangle\n"; }
86
void show_area()
// виртуальная функция Triangle
{ cout<<"Треугольник с высотой "<< x <<" и основанием "<< y;
cout<<" имеет площадь = "<< x*0.5*y <<endl;
}
~Triangle()
// виртуальный деструктор Triangle
{ cout<<"Деструктор классаTriangle \n";}
};
class Rectangle: public Figure // производный класс Rectangle
{ public:
Rectangle (double i, double j): Figure ( i, j) // конструктор Rectangle
{ cout<<"Конструктор Rectangle\n"; }
void show_area()
// виртуальная функция Rectangle
{ cout<<"Прямогольник со сторонами "<< x <<" и "<< y;
cout<<" имеет площадь = "<< x*y <<endl;
}
~Rectangle()
// виртуальный деструктор Rectangle
{ cout<<"Деструктор класса Rectangle\n"; }
};
class Circle: public Figure
// производный класс Circle
{ public:
Circle (double i): Figure ( i)
// конструктор Circle
{cout<<"Конструктор Circle\n"; }
void show_area()
// виртуальная функция Circle
{ cout<<"Круг с радиусом "<< x;
cout << " имеет площадь = " << 3.14*x*x << endl;
}
~Circle() // виртуальный деструктор Circle
{ cout<<"Деструктор класса Circle\n"; }
};
void main ()
{ clrscr();
cout << "Работа программы:\n";
Figure *p;
// объявление указателя класса Figure
Triangle t (3,4);
// создание объекта класса Triangle
Rectangle r (5,6);
// создание объекта класса Rectangle
Circle c (2);
// создание объекта класса Circle
p = &t;
// базовый указатель на объект типа Triangle
p -> show_area();
// вывод сообщения о площади объекта типа Triangle
p = &r;
// базовый указатель на объект типа Rectangle
p -> show_area();
// вывод сообщения о площади объекта типа Rectangle
p = &c;
// базовый указатель на объект типа Circle
87
p -> show_area();
// вывод сообщения о площади объекта типа Circle
}
Результаты программы:
Работа программы:
Конструктор базового класса Figure
Конструктор Triangle
Конструктор базового класса Figure
Конструктор Rectangle
Конструктор базового класса Figure
Конструктор Circle
Треугольник с высотой 3 и основанием 4 имеет площадь = 6
Прямогольник со сторонами 5 и 6 имеет площадь = 30
Круг с радиусом 2 имеет площадь = 12.56
Деструктор класса Circle
Деструктор базового класса Figure
Деструктор класса Rectangle
Деструктор базового класса Figure
Деструктор классаTriangle
Деструктор базового класса Figure
Обобщенный пример наследования
Мы рассмотрели и исследовали объявленные в начале основополагающие концепции ООП – объект, класс, инкапсуляция, наследование, полиморфизм, абстракция типов. Теперь после изучения
возможностей концепции наследования классов (простое, множественное, управление режимами доступа, виртуальные функции,
абстрактные классы) на демонстрационных примерах полезно рассмотреть обобщающий пример.
Постановка задачи.
Рассмотрим разработку полного графического примера, в котором выводятся на экран монитора геометрические фигуры, с которыми можно совершать некоторые действия, например создавать,
уничтожать, перемещать по экрану и т. д. Прообразом этой задачи
является пример 28.
Вместо ограниченного по своим возможностям класса Location в
качестве базового класса опишем свойства и функции "некоторой
фигуры" (Figure). Местоположение ее определяется двумя координатами экрана (X, Y), инициализация которых задается встроенным
конструктором, а для получения значений текущих координат ис88
пользуются встроенные методы класса GetX(), GetY(). Параметры X, Y
объявлены защищенными элементами базового класса, чтобы производные классы могли их наследовать и использовать.
В качестве основных действий, которые можно совершать с этой
фигурой, могут быть: показ на экране (Show), удаление (Hide), перемещение (Drag). Описание этих действий представлено чистыми
виртуальными функциями без их тел, что делает класс абстрактным. Для этого класса нельзя создать реальный объект на экране,
но можно определить указатель или массив указателей на базовый
тип и использовать их при создании объектов производных классов. При этом могут применяться только наследуемые виртуальные
функции, которые видимы через базовый указатель из производного класса.
Базовому классу наследует производный класс (Figure <- Point),
который описывает простейший реальный объект "точка", координаты которой наследуются из базового класса (X, Y), а также задается свой параметр видимости точки на экране как защищенный
элемент Visible с булевыми значениями. Объявлены обычные методы: функция Isvisible(), определяющая видимость точки, и функция
Moveto(), задающая новые координаты точки. Как невиртуальные
методы они могут наследоваться производными классами. Их можно сделать виртуальными, объявив их как virtual, и тогда в производных от Point классах необходимо замещать их "своими" виртуальными методами.
Чистые виртуальные функции класса Figure замещаются конкретными виртуальными функциями (Show(), Hide(), Drag()) в классе
Point. Виртуальная функция Drag() использует обычный метод своего класса Moveto(), который определяет новые координаты объекта,
а также обычную функцию общего назначения (т.е. не метод класса) Getdelta(). Последняя определяет направление смещения точки
(или другой реальной фигуры) на экране по нажатию пользователем
клавиш-стрелок клавиатуры (вправо, влево, вверх, вниз) и окончание процесса смещения объекта на экране при нажатии клавиши
<Enter>.
В главной функции программы демонстрируется создание и перемещение точки по экрану, создание "звездного" неба с использованием стандартной функции получения случайных чисел X, Y для
координат точки random(max), где max – максимальное число для координаты экрана с установленной разрешающей способностью. Обработка программы выполняется в среде компилятора Borlandc 3.1.
89
Пример 40.
Рассмотрим программу для демонстрации описанной выше постановки задачи.
#include<iostream.h>
#include<conio.h>
#include<graphics.h>
// для графической библиотеки
#include<stdlib.h>
// для функций random(), srand(), kbhit()
#include<dos.h>
// для функции delay()
const char* path= "c:\\borlandc\\bgi";
// путь к библиотеке bgi-файлов
enum Boolean { false, true };
// булевы значения
// прототип обычной функции (не метод), определенной ниже:
Boolean Getdelta (int& Deltax, int& Deltay);
class Figure
// абстрактный базовый класс "фигура"
{ protected:
// защищенные элементы, доступные
// в производных классах
int X, Y;
// координаты объекта на экране
public:
Figure (int Initx, int Inity) // конструктор базового класса Figure
{ X = Initx, Y = Inity; }
// инициализация координат
// встроенные функции-методы получения текущих координат объекта:
int Getx () { return X; } int Gety () { return Y; }
// чистые виртуальные функции-методы базового класса:
virtual void Show() = 0;
// изображение объекта
virtual void Hide() = 0;
// стирание объекта с экрана
virtual void Drag ( int Dragby) = 0; // перемещение объекта
};
// производный класс Point с открытым доступом к базовому классу Figure:
class Point: public Figure
// класс «точка"
{ protected:
Boolean Visible;
// параметр видимости точки
public:
Point (int Initx, int Inity);
// конструктор класса Point
// виртуальные функции-методы класса Point:
void Show();
// изображение точки на экране
void Hide();
// стирание точки с экрана
void Drag (int Dragby);
// перемещение точки по экрану
// невиртуальные методы класса Point:
90
Boolean Isvisible() { return Visible;} // проверка точки на видимость
void Moveto (int Newx, int Newy); // смещение точки на новое место
};
// методы класса Point:
Point:: Point (int Initx, int Inity): // конструктор класса Point
Figure (Initx, Inity)
// передача аргументов базовому классу
{ Visible = false;
// инициализация параметра Point
}
void Point:: Show()
// метод изображения точки на экране
{ if ( ! Visible)
// если точка невидима, то
{ Visible = true;
// установка видимости точки
putpixel (X, Y, getcolor() ); // изображение цветной точки
}
}
void Point:: Hide()
// метод стирания точки с экрана
{ if (Visible)
// если точка видима, то
{ Visible = false;
// установка невидимости точки
putpixel (X, Y, getbkcolor() ); // закрашивание точки цветом фона
}
}
void Point:: Moveto (int Newx, int Newy) // метод смещения точки
{ if (Visible) Hide ();
// стирание точки
X = Newx;
// новые координаты точки X и Y
Y = Newy;
Show ();
// изображение точки
}
void Point:: Drag( int Dragby) // метод смещения фигуры на величину
Dragby
{ int Deltax, Deltay;
// смещение по X, Y
int Figurex, Figurey;
// координаты по X, Y
Show();
// изображение точки
while ( Getdelta ( Deltax, Deltay) )
// цикл перемещения точки:
{ Figurex = Getx()+( Deltax * Dragby ); // смещение по X
Figurey = Gety()+( Deltay * Dragby ); // смещение по Y
Moveto ( Figurex, Figurey); // перемещение точки на новое место
}
}
/* обычная функция (вне классов) определения направления
смещения объекта, которая анализирует код нажатой
управляющей клавиши и возвращает разрешение на смещение
(true) в заданном направлении через параметры ссылки (&),
91
либо отмену смещения (false) при нажатии клавиши <Enter>;
используется в методе Point:: Drag():
*/
Boolean Getdelta (int& Deltax, int& Deltay)// & - ссылки на смещение
{ char Keychar;
// переменная символа
клавиши
Boolean Quit;
// переменная смещения
Deltax = 0;
// начальные значения смещений
Deltay = 0;
do { Keychar = getch ();
// цикл анализа кода нажатой клавиши:
if ( Keychar == 13)
// если код = 13 (<Enter>), то
return ( false);
// конец смещения объекта;
if (Keychar == 0)
// если код = 0, то это скэн-код клавиши
{ Quit = true;
// подтверждение смещения
Keychar = getch ();
// чтение 2- го байта кода
switch ( Keychar)
// анализ кода управления:
{ case 72: Deltay = -1; break; // стрелка вниз
case 80: Deltay = 1; break; // стрелка вверх
case 75: Deltax = -1; break; // стрелка влево
case 77: Deltax = 1; break; // стрелка вправо
default : Quit = false;
// неправильная клавиша
}
}
} while ( ! Quit);
// продолжение цикла чтения кода клавиш
return ( true);
// разрешение смещения объекта
}
void main()
// главная функция:
{ int graphdriver = DETECT, graphmode;
// данные режима графики
initgraph (&graphdriver, &graphmode, path); // инициализация графики
int n, i, x, y, maxx, maxy, maxcolor, seed, color; // рабочие переменные
int DelayTime;
// временная задержка
x=100;
// координаты экрана (х,у)
y=200;
Point Apoint ( x, y);
// создание и инициализация точки
Apoint.Show();
// изображение точки
Apoint.Drag(20);
// перемещение точки по экрану
getch ();
// задержка образа экрана
/*
// изображение статического "звездного неба":
cout<< "Enter number of points (<32000): ";
cin >> n;
// ввод количества точек
92
for (i=0; i<n; i++)
// цикл создания точек
{ x = random(639);
// случайные коодинаты точки (х, у)
y = random(479);
Point Apoint (x,y);
// создание и инициализация точки
Apoint.Show();
// изображение точки
}
*/
/*
// изображение мерцающих цветных точек
maxx = getmaxx();
// количество пикселей по оси Х экрана
maxy = getmaxy();
// количество пикселей по оси Y экрана
maxcolor = getmaxcolor();
// количество цветов
cout<< "Enter number of points (<2000): ";
cin >> n;
// ввод количества точек
cout<<"Enter DelayTime (millisecond) = ";
cin >> DelayTime;
// ввод временной задержки
while ( ! kbhit() )
// цикл пока не нажата любая клавиша
{ seed = random (RAND_MAX);
// начальное случайное число
srand (seed);
// запуск генератора случайных чисел
for (i=0; i<n; i++)
// цикл создания точек
{ x = random(maxx);
// случайные координаты по x
y = random(maxy);
// и по y
color = random(maxcolor);
// случайное значение цвета
setcolor(color);
// установка цвета
Point Apoint ( x, y);
// создание и инициализация точки
Apoint.Show();
// изображение точки
}
delay (DelayTime);
// задержка по времени
srand ( seed);
// запуск генератора случайных чисел
for (i=0; i<n; i++)
// цикл создания точек
{ x = random (maxx);
// случайные коодинаты точки (х, у)
y = random (maxy);
color = random (maxcolor);
// случайное значение цвета
if ( color == getpixel (x, y))
// если цвет точки =
//случайному, то
{ Apoint.Moveto (x, y);
// смещение точки и
Apoint.Hide();
// стирание
}
}
}
getch ();
// задержка экрана
93
closegraph();
}
// закрытие графического режима
Модульность классов
Для разработки связанных наследованием классов целесообразно использование принципов модульного программирования, когда программа состоит из файлов заголовков, файлов отдельно компилируемых модулей, соединяемых в единое целое с помощью файла проекта программы.
Суть в том, что, имея встроенные элементы данных, методы и
права доступа, класс обладает прирожденной модульностью. Поэтому при разработке программ следует выделять объявления каждого класса или группы взаимосвязанных классов в файл заголовков,
с возможностью его дальнейшего расширения, либо добавления новых файлов заголовков при разработке новых классов. Описания
невстроенных в класс методов при этом выделяются в другой файл
(или файлы) реализации методов. После их компиляции образуются объектные файлы. Возможно группирование нескольких объектных файлов, содержащих классы, в библиотеку классов и расширение этой библиотеки. При этом алгоритмы методов остаются
скрытыми, но они в объектной форме становятся доступными другим программистам. Последние могут использовать полученную
библиотеку классов для разработки своих новых специализированных классов, не прибегая к исходному тексту программ.
Большая часть усилий по разработке приложений в С++ концентрируется на построении иерархии классов и трансформации полученного дерева наследования в код программы.
Размещение нескольких классов в одном модуле (файле).
Пример 41.
Рассмотрим разработку независимо компилируемого модуля, в
состав которого входят классы Figure и Point на основе программы
примера 40, то есть тела классов и функций должны быть скопированы из примера 40. Объявления этих двух классов поместим в
файл figures.h:
enum Boolean { false, true };
// булевы значения
class Figure
// абстрактный базовый класс "фигура"
{ // тело класса Figure
};
// производный класс Point с открытым доступом к базовому классу Figure:
class Point: public Figure
// класс «точка"
94
{ // тело класса Point
};
Этот процесс может продолжаться неопределенно долго – допускается определять другие производные классы от Figure, Point и т.
д. Более того, как известно, допускается множественное наследование от более чем одного базового класса.
Определения всех невстроенных методов указанных двух классов образуют файл figpoint.cpp:
#include "figures.h"
// подключение файла объявления
классов
#include<graphics.h> // подключение библиотеки графики
#include<conio.h>
// для функции getch()
// прототип обычной функции (не метод):
Boolean Getdelta (int& Deltax, int& Deltay);
// методы класса Point:
Point:: Point (int Initx, int Init):
// конструктор класса Point
Figure (Initx, Inity)
// передача аргументов базовому классу
{ Visible = false;
// инициализация параметра Point
}
void Point:: Show()
// метод изображения точки на экране
{ // тело метода Show
}
void Point:: Hide()
// метод стирания точки с экрана
{ // тело метода Hide
}
void Point:: Moveto (int Newx, int Newy) // метод смещения точки
{ // тело метода Moveto
}
void Point::Drag(int Dragby)
//метод перемещения точки
// с дискретом Dragby
{ // тело метода Drag
}
// обычная функция Getdelta:
Boolean Getdelta (int& Deltax, int& Deltay)// & - ссылки на смещение
{ // тело функции Getdelta
}
Рассмотрим головную программу, демонстрирующую возможности
классов Figure и Point, которая сохраняется в файле, например,
mainpoin.cpp:
95
#include "figures.h"
// подключение файла объявления классов
#include<graphics.h> // подключение библиотеки графики
#include<conio.h>
// для функции getch()
#include<iostream.h>
// для ввода-вывода данных
#include<stdlib.h>
// для функций random(), srand(), kbhit()
#include<dos.h>
// для функции delay()
const char* path= "c:\\borlandc\\bgi";
// путь к bgi-библиотеке
void main()
// главная функция:
{
// тело функции main
}
Создание проекта программы.
Для того чтобы разработать единую программу из нескольких
разнотипных файлов, необходимо создать проект программы и откомпилировать его в среде BORLANDC3.1. Последовательность разработки проекта программы включает следующие этапы.
1. Создать папку (каталог), где будут размещаться все файлы проекта, например, под именем FIGPOINT.
2. Записать в папку проекта разработанные текстовые файлы
(срр-файлы, h-файлы): figpoint.cpp, mainpoin.cpp, figures.h.
3. Выполнить команду меню Project-Open project. В окне OpenProject File перейти в поле Files и, начиная с нужного диска, найти
свою папку. Перейти в поле Open-Project File, где вместо символа * набрать имя файла проекта, например, figpoint.prj и щелкнуть кнопку
ОК. Внизу экрана появится окно Project:Figpoint с высвеченной строкой.
4. Выполнить команду меню Project-Add item. В окне Add item to
Project List перейти в поле Files. Выделяя срр-файлы в любом порядке, кнопкой Add включить их в файл проекта и закрыть окно кнопкой Done.
5. Запустить файл проекта на компиляцию и выполнение командой Run-Run или клавишами <Ctrl-Enter>.
6. Выполнить отладку исходных файлов, выделяя в окне Project
строку файла и нажав <Enter>.
7. После отладки проекта программы получить результаты работы exe-файла.
Проект программы может включать следующие файлы: 1) исходные срр-файлы; 2) файлы заголовков разработчика программы
(h-файлы); 3) объектные obj-файлы; 4) файл проекта prj-файл; 5) исполняемый ехе-файл; 6) файл настройки среды dsk-файл, 7) копии
файлов (bak-файлы).
96
Расширяющяся иерархия классов
Одно из замечательных свойств классов – способность расширяющегося наследования для создания иерархии классов в виде дерева классов. Несмотря на то, что возможность расширения библиотечных приложений присуща большинству языков программирования, наличие виртуальных функций и наследования делает такое расширение более естественным. Абстрактный класс является
интерфейсом, образуя корень дерева иерархии. Иерархия классов
– это механизм последовательного построения ветвей в виде производных классов. Наследуя все, что присуще базовым классам,
одновременно имеется возможность включения дополнительных
средств для работы новых объектов. Определяемые пользователем
классы и версии виртуальных функций становятся действительными расширениями заданной иерархии возможностей в силу того,
что все эти средства были изначально заложены в язык С++.
Пример 42.
На основе примеров 40 и 41 создадим новый проект, в котором определим производный класс Circle (окружность) по иерархии: Figure<-Point<-Circle. Класс Circle наследует от базовых классов
элементы-данные X, Y, которые задают центр окружности, Visible (видимость) и имеет свой элемент Radius. Из функций класс включает
замещаемые виртуальные методы для работы с окружностью: Show
(отображение), Hide (стирание), а также наследует из Point виртуальный метод Drag (перемещение). Для смещения окружности на новое место был включен свой метод Moveto, который внешне повторяет метод Point::Moveto, но коды их различны, поскольку отличаются
методы Show и Hide. Однако, если объявить метод Point::Moveto виртуальным, то он будет работать с окружностью как с "большой" точкой и тогда метод Circle::Moveto можно исключить. В класс Circle добавлены также методы увеличения (Expand) и уменьшения (Contract)
окружности.
Создадим новые файлы для реализации класса Circle и включим
их в папку (каталог) с именем CIRCLE.
Дополним описанием класса Circle заголовочный файл figures.h,
чем расширим его возможности, и включим в папку CIRCLE:
class Circle: public Point
// производный класс от Point, Figure
{ protected:
int Radius;
// защищенный элемент
97
public:
Circle (int Initx, int Inity, int Initradius); // конструктор Circle
// виртуальные методы:
void Show ();
// отображение окружности
void Hide ();
// стирание окружности
// невиртуальные методы:
void Expand ( int Expandby);
// расширение окружности
void Contract (int Contractby);
// сжатие окружности
// используется виртуальный метод Point::Moveto
// void Moveto(int Newx, int Newy);
// смещение окружности
};
В файл circle.cpp включены методы класса Circle:
#include<graphics.h>
#include "figures.h"
Circle::Circle(int Initx, int Inity, int Initradius): // конструктор Circle
Point(Initx, Inity)
// вызов конструктора Point
{ Radius=Initradius;
// инициализация Radius
}
void Circle::Show()
// метод отображения
{ Visible=true;
// окружность видима
circle(X, Y, Radius);
// отображение окружности
}
void Circle::Hide()
// метод стирания окружности
{ int Tempcolor;
Tempcolor=getcolor();
// сохранение текущего цвета
setcolor(getbkcolor());
// установка цвета фона
Visible=false;
// окружность невидима
circle(X, Y, Radius);
// стирание окружности
setcolor(Tempcolor);
// возврат текущего цвета
}
void Circle::Expand(int Expandby)
// метод расширения окружности
{ Hide();
// стирание окружности
Radius += Expandby;
// увеличение радиуса
if(Radius < 0) Radius=0;
// если Radius<0, то Radius=0
Show();
// отображение окружности
}
void Circle::Contract(int Contractby)
// метод сжатия окружности
{ Expand(-Contractby);
// Radius=(Radius-Contractby)
}
98
/* свой метод, который заменен виртуальным методом Point::Moveto
void Circle::Moveto(int Newx, int Newy)
{ Hide();
X=Newx;
Y=Newy;
Show();
}
*/
Файл maincirc.cpp содержит главную функцию:
#include "figures.h"
#include<iostream.h>
#include<conio.h>
#include<graphics.h>
#include<stdlib.h>
// random(), srand(), kbhit()
#include<dos.h>
// delay()
const char *path= "c:\\borlandc\\bgi";
void main()
{ int gdriver = DETECT, gmode;
// параметры графики
initgraph (&gdriver, &gmode, path);
// инициализация графики
int x, y, n, i; // координаты центра окружности
cout<<"Input x (<" << getmaxx() << "): ";
cin >> x;
cout<<"Input y (<" << getmaxy() << "): ";
cin >> y;
cout << "Move point and circle using key-arrow\n";
Point Mypoint (x,y);
// создание объекта точка
Mypoint.Show();
// отображение точки
Mypoint.Drag(20);
// перемещение точки
Mypoint.Hide();
// стирание точки
Point *ppoint;
// базовый указатель
Circle Mycircle(x, y, 50);
// создание объекта окружность
ppoint=&Mycircle;
// указатель на окружность
// Mycircle.Show();
// отображение своим методом
ppoint->Show();
// отображение базовым указателем
ppoint->Drag(40);
// перемещение окружности
ppoint->Hide();
// стирание точки
getch ();
ppoint->Moveto(200, 250);
// смещение окружности
ppoint->Drag(20);
// перемещение окружности
Mycircle.Expand(75);
// увеличение окружности
99
ppoint->Drag(40);
// перемещение окружности
Mycircle.Contract(50);
// уменьшение окружности
ppoint->Drag(40);
// перемещение окружности
cout<<"Input circles quantity: ";
cin >> n;
// ввод количества окружностей
x=y=100;
// координаты х, у
while( ! kbhit())
// цикл до нажатия любой клавиши
{ x=random(getmaxx()); // случайное значение х
if(x<50) x=100; // проверка пределов значения х
if(x>600) x=500;
y=random(getmaxy());
// случайное значение у
if(y<50) y=100;
// проверка пределов значения у
if(y>400) y=300;
for(i=1; i<=n; i++)
// цикл расширения окружностей
{ setcolor(random(15));
// случайный цвет
Mycircle.Expand(i);
// увеличение окружности
Mycircle.Moveto(x,y);
// смещение окружности
delay(100);
// задержка времени
}
for(i=n; i>=1; i--)
// цикл уменьшения окружностей
{ setcolor(random(15));
// случайный цвет
Mycircle.Contract(i);
// уменьшение окружности
delay(100);
// задержка времени
}
}
cout<<"Input circles quantity <=5: ";
cin >> n;
// ввод количества окружностей
x=y=100;
// координаты х, у
Circle *pcircle[5];
// массив указателей на окружности
for(i=0;i<n;i++) // цикл по окружностям
{ pcircle[i]=new Circle(x+i*5,y+i*5,20); // инициализация окружности
setcolor(random(15));
// случайный цвет
pcircle[i]->Expand(i*10); // увеличение окружности
pcircle[i]->Contract(i*10); // уменьшение окружности
pcircle[i]->Drag(20);
// перемещение окружности
}
closegraph();
// закрытие графики
}
В папку CIRCLE необходимо скопировать также из папки FIGPOINT
файл функций базовых классов figpoint.cpp. Таким образом, в папке CIRCLE должны находится файлы: figures.h, figpoint.cpp, circle.cpp,
100
maincirc.cpp, на основе которых разрабатывется проект с именем, например, circle.prj.
Узловые классы
Класс в иерахии представляет общее понятие, для которого производные классы могут считаться специализациями. Типичный
класс, спроектированный как составная часть иерархии, узловой
класс, опирается на услуги базовых классов, чтобы обеспечить свои
собственные услуги. То есть он вызывает функции-элементы базового класса. Типичный базовый класс обеспечивает не просто реализацию интерфейса, описанного его базовым классом (как это делает
класс реализации для абстрактного типа). Он также сам добавляет
новые функции, обеспечивая, таким образом, широкий интерфейс.
Это часто называют " программированием отличий" или "программированием по расширению". Узловому классу обычно нужны конструкторы, и часто не простые. Этим узловой класс отличается от
абстрактных типов, которые редко имеют конструкторы.
Рассмотренный выше пример 42 пригоден для создания наследования для объектов с одной опорной точкой (центр окружности,
сектора и дуги окружности, линии, заданной исходной точкой и
т.п.). Для создания линии наследования объектов с двумя опорными точками (например, прямоугольник) можно начать ее с класса
Point, используя его как узел разветвления (базовый класс) для новой ветви наследования в виде производного класса Rectangle (прямоугольник).
Пример 43.
Разработаем новый проект для работы с объектом "прямоугольник" с целью реализации расширяющегося наследования классов
по иерархии Figure<-Point<-(Circle, Rectanglе). При этом сохраняются
все возможности ранее разработанных классов.
Класс Rectanglе наследует от базового класса Figure элементыданные X, Y, которые задают координаты левого верхнего угла прямоугольника, и включает элементы X1, Y1, как координаты правого нижнего угла прямоугольника, а также наследует элемент Visible
(видимость) класса Point. Из функций класс включает замещаемые
виртуальные методы для работы с прямоугольником: Show (отображение), Hide (стирание), а также наследует из Point виртуальный метод Drag (перемещение). Используются невиртуальные методы Getx1, Gety1 для получения координат X1, Y1 и свой метод Moveto
для смещения прямоугольника на новое место. Конструктор клас101
са инициирует свои элементы и передает аргументы своим базовым
классам.
Заголовочный файл figures.h дополним описанием класса
Rectangle:
class Rectangle: public Point
{ protected:
int X1, Y1;
// координаты правого нижнего угла
public:
Rectangle (int Initx, int Inity, int Initx1,int Inity1): // конструктор Rectangle
Point(Initx, Inity)
// вызов конструктора Point
{ X1=Initx1; Y1=Inity1;
// инициализация данных Rectangle
}
// прототипы виртуальных методов:
void Show ();
// изображение прямоугольника
void Hide ();
// стирание прямоугольника
void Drag (int Dragby);
// перемещение прямоугольника
// невиртуальные методы:
int Getx1() { return X1;}
// получение координат угла
int Gety1() { return Y1;}
void Moveto(int Newx, int Newy,
int Newx1, int Newy1);
// смещение объекта на новое место
};
Создадим файл rectangl.cpp с методами класса Rectangle:
#include<graphics.h>
#include "figures.h"
Boolean Getdelta (int& Deltax, int& Deltay);
// методы класса Rectangle:
void Rectangle::Show()
// метод показа прямоугольника
{ if (! Visible)
// если прямоугольник не видим, то
Visible=true;
// теперь он видим
rectangle(X, Y, X1, Y1);
// вывод прямоугольника на экран
}
void Rectangle::Hide()
// метод стирания прямоугольника
{ int Tempcolor;
// переменная для цвета
Tempcolor=getcolor();
// сохранение текущего цвета
setcolor(getbkcolor());
// установка цвета фона
if (Visible)
// если прямоугольник видим, то
Visible=false;
// теперь он невидим
rectangle(X, Y, X1, Y1);
// стирание фоном
102
setcolor(Tempcolor);
// возврат текущего цвета
}
// виртуальный метод перемещения прямоугольника по экрану:
void Rectangle::Drag( int Dragby) // Dragby — шаг смещения
{ int Deltax, Deltay;
// смещение по Х, Y
int Fx, Fy, Fx1, Fy1;
// координаты прямоугольника
Show();
// показ прямоугольника
Fx=Getx();
// текущие координаты объекта
Fy=Gety();
Fx1=Getx1();
Fy1=Gety1();
while (Getdelta(Deltax, Deltay)) // цикл перемещения прямоугольника
{ Fx += (Deltax * Dragby); // новые координаты прямоугольника
Fy += (Deltay * Dragby);
Fx1+= (Deltax * Dragby);
Fy1+= (Deltay * Dragby);
Moveto (Fx, Fy, Fx1, Fy1); // прямоугольник на новом месте
}
}
// метод смещения прямоугольника:
void Rectangle::Moveto(int Newx, int Newy, int Newx1, int Newy1)
{ Hide();
// стирание прямоугольника
X=Newx;
// новые координаты
Y=Newy;
X1=Newx1;
Y1=Newy1;
Show();
// объект на новом месте
}
Создадим файл главной программы mainrec.cpp для демонстрации действий с прямоугольником:
#include "figures.h"
#include<iostream.h>
#include<conio.h>
#include<graphics.h>
#include<stdlib.h>
// random(), srand(), kbhit()
#include<dos.h>
// delay()
const char *path= "c:\\borlandc\\bgi";
void main()
// главная функция
{ int gdriver = DETECT, gmode;
// установка режимов графики
initgraph (&gdriver, &gmode, path);
// инициализация графики
103
int x, y, x1, y1, n, i,maxx, maxy, maxcol,col;
// рабочие переменные
// создание одного прямоугольника:
cout<<"Input coordinates of rectangle:\n";
// ввод координат:
cout<<"left up x="; cin >> x;
// левый верхний угол
cout<<"left up y="; cin >> y;
cout<<"right down x1="; cin >> x1;
// правый нижний угол
cout<<"right down y1="; cin >> y1;
Rectangle Myrect(x, y, x1, y1);
// создание прямоугольника
Myrect.Show();
// показ прямоугольника
Myrect.Drag(20);
// перемещение прямоугольника
// создание серии прямоугольников с помощью массива указателей:
Rectangle *prect[20]; // массив указателей
cout<< "Input rectangle quantity (<=10): ";
// задать число
// прямоугольников
cin >> n;
for (i=0; i<n; i++)
// цикл по прямоугольникам
{ setcolor(i+1);
// изменение цвета
prect[i]=new Rectangle(x+i*30, y+i*30,
x1+i*30, y1+i*30); // новый прямоугольник
prect[i]->Show();
// показ прямоугольника
prect[i]->Drag(20);
// перемещение прямоугольника
}
getch();
// создание пульсирующей серии прямоугольников:
cout<< "Input rectangles quantity (<=20): ";
// задать число объектов
cin >> n;
clearviewport();
// чистка экрана
maxx=getmaxx();
// max пикселов по Х
maxy=getmaxy();
// max пикселов по Y
maxcol=getmaxcolor();
// max цветов
col=1;
// начальный цвет
while(! kbhit())
// цикл до нажатия клавиши
{ for (i=0; i<n; i++)
// цикл по прямоугольникам
{ prect[i]=new Rectangle(maxx/2-i*10, maxy/2-i*5,
maxx/2+i*10, maxy/2+i*5); // новый прямоугольник
setcolor(col++);
// изменение цвета
if(col==maxcol) col=1;
// если max, то начальный цвет
prect[i]->Show();
// показ прямоугольника
delay(500);
// задержка показа
}
for (i=n-1; i>=0; i--)
// обратный цикл
104
{ prect[i]->Hide();
delay(500);
}
}
for (i=0; i<n; i++) delete prect[i];
прямоугольников
closegraph();
}
// стирание прямоугольника
// задержка показа
//цикл
// уничтожения
// закрытие режима графики
Для разработки проекта создадим папку с именем RECTANGL, в
которую включим ранее разработанные и новые файлы: figures.h,
figpoint.cpp, circle.cpp, rectangl.cpp, mainrect.cpp. На основе этих файлов
создадим проект программы с именем файла rect.prj для демонстрации действий с прямоугольниками.
Для демонстрации возможностей работы с объектами всех разработанных классов можно разработать новый проект c именем папки, например, FIGURES на основе проекта RECTANGL. При этом нужно создать новую главную функцию с именем, например, mainfig.
cpp, которая включала бы возможные действия с различными объектами (точка, окружность, прямоугольник).
Подводя итог, можно сказать, что узловой класс:
1) опирается на свои базовые классы, как для своей реализации,
так и для предоставления услуг своим пользователям;
2) предоставляет более широкий интерфейс (то есть с большим
числом открытых методов) по сравнению с базовыми классами;
3) в своем открытом интерфейсе опирается в первую очередь (но
не обязательно только) на виртуальные функции;
4) зависит от своих (прямых и не прямых) базовых классов;
5) понятен только в контексте своих базовых классов;
6) может служить базовым классом для дальнейшего порождения классов;
7) может быть использован для создания объектов.
Не все узловые классы будут удовлетворять пунктам 1, 2, 6 и 7, но большинство – удовлетворяют. Класс, не удовлетворящий пункту 6, напоминает конкретный тип, и его можно назвать конкретным узловым классом.
Такие классы иногда называют классами-листьями. Класс, не удовлетворяющий пункту 7, напоминает абстрактный тип, и его можно назвать
абстрактным узловым классом. Пункт 4 подразумевает, что для компиляции узлового класса программист должен включить объявления всех
105
его прямых и непрямых базовых классов, а также все объявления, от которых в свою очередь зависят последние. Этим узловой класс отличается от
абстрактного типа. Пользователь абстрактного типа не зависит от классов,
использующихся для его реализации, и может не включать их в текст для
компиляции.
Множественное наследование классов
Рассмотренная выше иерерхия классов графических объектов
представляет простое (одиночное) наследование, когда производный класс наследует одному непосредственному базовому классу,
расширяя его возможности. Как было показано выше (см. пример
37), класс (потомок) может иметь более одного базового класса (родителей).
В общем случае, класс создается из решетки базовых классов.
Поскольку исторически совокупности связей между классами образовывали деревья, решетку классов часто также называют иерерхией классов. Классы пытаются проектировать так, чтобы пользователя без необходимости не интересовало, каким образом класс составляется из других классов. В частности, механизм виртуальных
вызовов гарантирует, что когда мы вызываем функцию f() для некоторого объекта, вызывается одна и та же функция, независимо от
того, какой класс в иерархии содержит объявление f(), использованное для вызова. Рассмотрим особенности множественного наследования более подробно на следующем модельном примере.
Пусть класс Task (задача) определяет решаемые задачи:
class Task
{ // ...
public:
delay(int);
void wait();
void check(Task*);
};
а класс Display (отображение) – вывод данных:
class Display
{ // ...
public:
void draw();
void write (Display*);
};
106
На их основе определим производный класс Device (прибор):
class Device: public Task, public Display
{ // ...
public:
void transmit();
};
Кроме операций, определенных для Device, можно воспользоваться объединением операций Task и Display. Например:
void f (Device& s)
{ s.draw();
s.delay(10);
s.transmit();
}
// Display::draw()
// Task::delay()
// Device::transmit()
Виртуальные функции работают как обычно. Например:
class Task
{ // ...
virtual void wait()=0;
};
class Display
{ // ...
virtual void draw()=0;
};
class Device: public Task, public Display
{ // ...
void wait();
// замещение Task::wait()
void draw(); // замещение Display::draw()
}
Такой подход гарантирует, что функции Device::draw() и
Device::wait() будут вызваны для класса Device, интерпретированного
как Display и Task соответственно.
Разрешение неоднозначности.
Два базовых класса могут иметь методы с одинаковым именем.
Например:
107
class Task
{ // ...
virtual info* debug();
};
class Display
{ // ...
virtual info* debug();
};
При использовании Device неоднозначности для этих функций
должны быть устранены использованием квалифицированного
имени функции:
void f (Device* pd)
{
info* d;
d=pd->debug();
d=pd->Task::debug();
d=pd->Display::debug();
}
// ошибка: неоднозначно
// правильно
// правильно
Повторяющиеся базовые классы.
При задании более чем одного класса возникает вероятность того, что какой-либо класс дважды окажется базовым для другого
класса. Например, если бы каждый из классов Task и Display был
производным от класса Link, у Device было бы два Link:
class Link
{
// ...
Link* next;
// указатель на список
};
class Task: public Link
{
// Link используется для хранения списка задач Task
// ...
};
class Display: public Link
{ // Link используется для хранения списка объектов Display
// ...
};
Это не вызывает никаких проблем. Используются два отдельных
объекта Link для представления связей, и эти списки не взаимодействуют друг с другом. Естественно, обращаясь к элементам класса
108
Link, возможна неоднозначность, требующая разрешения, как показано выше. Объект Device можно представить в графическом виде
следующим образом:





Как правило, базовый класс, который повторяется (как Link) является деталью реализации, которую не следует использовать вне
непосредственно производных от него классов. Если к такому базовому классу нужен доступ из места, где видна более чем одна копия базового класса, во избежание неоднозначности ссылка должна
быть явно квалифицирована. Например:
void links (Device* p)
{
p->next=0;
// ошибка: неоднозначно, какой Link?
p->Link::next=0;
// ошибка: неоднозначно, какой Link?
p->Task::Link::next=0;
// правильно
p->Display::Link::next=0;
// правильно
};
Это в точности тот же механизм, который используется для разрешения неоднозначности при обращении к элементам классов.
Виртуальные базовые классы.
В тех случаях, когда общий базовый класс не должен быть представлен в виде двух отдельных объектов, нужно воспользоваться
виртуальным базовым классом. Например:
class Link
{
//...
};
class Task: public virtual Link
{
// ...
};
class Display: public virtual Link
{ // ...
};
class Device: public Task, public Display
{ // ...
};
109
Все части объекта Device могут использовать единственную копию Link.
Графическая схема наследования принимает вид:




В графе наследования все виртуальные базовые классы с одним
именем будут представлены одним единственным объектом этого
класса. С другой стороны, каждый невиртуальный базовый класс
будет иметь отдельный подобъект, представляющий его.
Программирование виртуальных базовых классов.
При определении функций класса с виртуальным базовым классом программист, в общем случае, не может знать, будет ли базовый
класс использоваться совместно с другими производными классами. Это может представлять некоторую проблему при реализации
алгоритмов, которые требуют, чтобы функция базового класса вызывалась ровно один раз. Например, язык гарантирует, что конструктор виртуального базового класса вызывается только один
раз. Конструктор виртуального базового класса вызывается (явно
или неявно) из конструктора объекта (конструктора самого "нижнего" производного класса). Например:
class A
{ // нет конструктора };
class B
{ public: B();
// конструктор по умолчанию };
class С
{ public: С(int);
// конструктор с параметром };
class D: public virtual A, public virtual B, public virtual C
{ D() {/*...*/}
// ошибка: нет конструктора с параметром для С
D (int i):C(i) {/*...*/}
// правильно
};
Конструктор виртуального базового класса вызывается до конструкторов производных классов. При необходимости программист
может смоделировать эту схему, вызывая функцию виртуального
базового класса только из самого "нижнего" производного класса.
Рассмотрим следующий пример.
110
Пусть есть базовый класс Window (окно), который "знает", как рисовать свое содержание:
class Window
{
// ...
virtual void draw(); // виртуальная функция изображения окна
};
Кроме того, есть классы с различными способами оформления окон
и дополнительными средствами представления данных:
class Winborder: public virtual Window
// окно с рамкой
{
// код оформления окна рамкой
void owndraw();
// отобразить рамку
void draw();
// замещение виртуальной функции
};
class Winmenu: public virtual Window
// окно с меню
{
// код представления меню
void owndraw();
// отобразить меню
void draw();
// замещение виртуальной функции
};
Функции owndraw() не обязаны быть виртуальными, поскольку
предполагается, что они будут вызваны из виртуальной функции
draw(), которая "знает" тип объекта, для которого она вызвана. Из
этого можно создать новый класс Clock (часы):
class Clock: public Winborder, public Winmenu
{
// код, описывающий часы
void owndraw();
// отобразить циферблат и стрелки
void draw();
// замещение виртуальной функции
};
Связи классов можно представить следующим графом:




Виртуальные функции draw() можно написать с использованием
функций owndraw() так, чтобы код, вызывающий любую draw(), косвенно вызывал при этом Window::draw() ровно один раз независимо от
типа окна:
void Winborder::draw()
111
{
};
void
{
};
void
{
};
Window::draw();
owndraw();
// отобразить окно
// отобразить рамку
Winmenu::draw()
Window::draw();
owndraw();
// отобразить окно
// отобразить меню
Clock: public Winborder::draw()
Window::draw();
// отобразить окно
Winborder::owndraw();
// отобразить рамку
Winmenu::owndraw();
// отобразить меню
owndraw();
// отобразить циферблат и стрелки
Замещение функций виртуальных базовых классов.
Производный класс может заместить виртуальную функцию
своего непосредственного или косвенного виртуального базового
класса. В частности, два различных класса могут заместить различные виртуальные функции виртуального базового класса. Таким
способом несколько производных классов могут внести свой вклад
в реализацию интерфейса, представленного в виртуальном базовом
классе.
Например, класс Window мог бы иметь функцию setcolor() (установить цвет) и prompt() (выдать приглашение на ввод). Тогда Winborder
мог бы заместить функцию setcolor() для управления цветом, а
Winmenu мог бы заместить функцию prompt() для взаимодействия с
пользователем:
class Window
// абстрактный класс
{
// ...
virtual setcolor(Color)=0;
virtual void prompt()=0;
};
class Winborder: public virtual Window
{
// ...
setcolor(Color);
// управление цветом фона
};
class Winmenu: public virtual Window
{
// ...
void prompt();
// взаимодействие с пользователем
};
class Mywindow: public Winborder, public Winmenu
112
{
};
// ...
Что если различные производные классы заместят одну и ту же
функцию? Тогда если мы порождаем от этих классов новый производный класс, мы должны в нем заместить эту функцию. То есть, одна
функция должна замещать все остальные. Например, Mywindow мог
бы заместить prompt() для улучшения интерфейса Winmenu:
class Mywindow: public Winborder, public Winmenu
{
// ...
void prompt();
// свое взаимодействие с пользователем
};
В графическом виде это выглядит так:
    
 
   
   
Если два класса замещают функцию базового класса, то порождение от них производного класса, не замещающего эту функцию
недопустимо. В этом случае не может быть создана таблица виртуальных функций, потому что вызов этой функции с завершенным
объектом будет неоднозначен. Такой конфликт разрешается добавлением замещающей функции в самый "нижний" производный
класс.
Использование множественного наследования.
Простейшим и наиболее очевидным применением множественного наследования является "склеивание" двух никаким образом
не связанных класов вместе в качестве реализации третьего класса. Такое использование множественного наследования достаточно эффективно. Оно позволяет программисту избежать написания
большого количества функций, которые переадресуют вызовы друг
другу. Множественное наследование позволяет "братским" классам
совместно использовать информацию без введения зависимости от
единственного общего базового класса в программе. Это является
случаем так называемого ромбовидного наследования (например,
см. выше классы Clock, Mywindow). Виртуальный базовый класс в
противоположность обыкновенному базовому классу требуется в
тех случаях, когда базовый класс не должен повторяться.
113
Важным назначением использования абстрактных классов является предоставление интерфейса с полным отсутствием деталей реализации, то есть интерфейсный базовый класс не должен содержать
данных. В этом случае не нужен конструктор, поскольку нет данных,
которые требуется инициализировать. Многие классы требуют некоторой формы очистки до уничтожения объекта. Так как абстрактный класс не может знать, требуется ли в производном классе такая
очистка, он должен предположить, что требуется. Можно объявить
виртуальный деструктор в базовом классе и заместить его в производных классах, чтобы гарантировать правильную очистку данных.
При проектировании на основе абстрактных классов почти весь
пользовательский код защищен от изменений в реализации иерархии и не нуждается в перекомпиляции после таких изменений.
Кроме того, пользователи иерархии абстрактных классов меньше
рискуют попасть в зависимость от способа реализации внешней системы, чем пользователи классической иерархии. Пользователи абстрактных классов не могут случайно воспользоваться механизмами реализации, потому что им доступны только средства, явно указанные в иерархии, и ничто не наследуется неявно из зависящего от
реализации базового класса.
Резюме.
Абстрактный класс является интерфейсом. Иерархия классов – средством последовательного построения классов. Естественно, каждый класс предоставляет своим пользователям интерфейс,
и некоторые абстрактные классы обеспечивают полезную функциональность, но, тем не менее, "интерфейс" и "строительные блоки" – вот главные роли абстрактных классов и иерархии классов.
Классические иерархии имеют тенденцию смешивать вопросы реализации с интерфейсами для пользователя. В этих случаях на помощь приходит абстрактные классы. Иерархии абстрактных классов предоставляют ясный и мощный способ выражения концепций,
не загроможденных вопросами реализации, и при этом не приводят к значительным накладным расходам. Результатом проектирования должна быть система, предоставляемая пользователю в виде иерархии абстрактных классов и реализуемая при помощи классической иерархии. Подводя итог, можно сказать, что абстрактные
типы предназначены для того, чтобы:
1) определить одно понятие таким образом, чтобы позволить сосуществовать в программе нескольким реализациям;
2) обеспечить приемлемое быстродействие и затраты памяти
благодаря использованию виртуальных функций;
114
3) минимизировать зависимость каждой реализации от других
классов.
Пример 44.
Рассмотрим пример построения иерархии множественного наследования классов на основе абстрактного класса. За основу возьмем пример 42 с иерархией простого наследования Figure<-Point<Circle, но произведем в нем необходимые изменения и дополним новыми классами.. Класс Figure сделаем интерфейсным абстрактным
базовым классом с чисто виртуальными функциями (Show, Hide), а
его данные (X, Y) и функции с ними связанные (Getx, Gety) передадим
в класс Point, где они кажутся более уместными, определяя местоположение точки. В классе Point замещены виртуальные функции
Show, Point.
Разработаем новый класс для работы с текстовым объектом
Message, который резко отличается от графических объектов. К его
данным относятся указатель на текст (*Msg), и параметры текста:
шифт (Font), размер поля текста (Field). В этом классе замещаются
виртуальные функции Show, Hide.
Создадим еще один класс Msgcirc, который наследует свойства
двух родительских базовых классов: Circle и Message, а также замещает виртуальные функции Show, Point.
Иерархию наследования (Figure<-Point<-(Circle, Message)<-Msgcirc)
можно отразить следующим графом:





Каждый класс (кроме Figure) имеет свой конструктор.
В главной функции создаются три объекта класса Msgcirc (Small,
Medium, Large). Демонстрируется появление и исчезновение разноцветных объектов в цикле (до нажатия любой клавиши для окончания цикла). Программа представлена единым файлом для большей
ясности:
#include<iostream.h>
#include<conio.h>
#include<graphics.h>
#include<stdlib.h>
// для random(), kbhit()
#include<dos.h>
// для delay()
#include<string.h>
115
enum Boolean {false, true};
const char *path= "c:\\borlandc\\bgi";
// логические значения
// путь к функциям графики
class Figure
// абстрактный интерфейсный
класс
{ public:
virtual void Show()=0;
// 2 чистые виртуальные функции
virtual void Hide()=0;
};
class Point: public Figure
// производный класс Point
{ protected:
int X, Y;
// координаты точки
Boolean Visible;
// видимость точки
public:
Point (int Initx, int Inity);
// конструктор Point
int Getx() { return X; }
// текущий Х
int Gety() { return Y; }
// текущий Y
// виртуальные методы:
void Show();
// показ точки
void Hide();
// стирание точки
// невиртуальный метод:
Boolean Isvisible()
// определение видимости точки
{ return Visible; }
};
class Circle: public Point
// производный класс Circle
{ protected:
int Radius;
// радиус окружности
public:
Circle (int Initx, int Inity, int Initradius); // конструктор Circle
// виртуальные методы:
void Show (); // показ окружности
void Hide();
// стирание окружности
};
class Message: public Point
// производный класс Message
{ private:
char *Msg;
// указатель на текст
int Font;
// шрифт
int Field;
// размер
public:
// конструктор:
Message(int Msgx, int Msgy, int Msgfont, int Msgfield, char *Text);
116
// виртуальные методы:
void Show();
// показ текста
void Hide();
// стирание текста
};
// Производный класс от Circle и Message:
class Msgcirc: public Circle, public Message
{ public:
// конструктор:
Msgcirc(int Mcircx, int Mcircy, int Mcircrad, int Font, char *Msg);
// виртуальные методы:
void Show();
// показ текста в окружности
void Hide();
// стирание текста и
окружности
};
// Методы класса Point:
Point :: Point(int Initx, int Inity)
// конструктор точки
{ X=Initx; Y=Inity; Visible = false;
}
void Point:: Show()
// показ точки
{ if( ! Visible)
// если точка невидима, то
{ Visible = true;
// задать видимость
setcolor(RED);
// цвет точки
putpixel (X, Y, getcolor() );
// изображение точки на экране
}
}
void Point:: Hide()
// стирание точки
{ if(Visible)
// если точка видима, то
{ Visible = false;
// задать невидимость точки
putpixel (X, Y, getbkcolor() );
// стирание точки цветом фона
}
}
// Методы класса Circle:
Circle::Circle (int Initx, int Inity, int Initradius): // конструктор окружности
Point(Initx, Inity)
// вызов конструктора точки
{ Radius=Initradius;
// начальный радиус
}
void Circle::Show()
// показ окружности
{ if( ! Isvisible())
// если невидима, то
{ Visible=true;
// задать видимость
circle(X, Y, Radius);
// изображение окружности
}
117
}
void Circle::Hide()
// стирание окружности
{ int Tempcolor;
// переменная цвета
Tempcolor=getcolor();
// сохранение текущего цвета
setcolor(getbkcolor());
// задать цвет фона
Visible=false;
// окружность невидима
circle(X, Y, Radius);
// стирание окружности фоном
setcolor(Tempcolor);
// востановление текущего цвета
}
// Методы класса Message:
// конструктор Message:
Message::Message(int Msgx, int Msgy, int Msgfont, int Msgfield, char *Text):
Point(Msgx,Msgy)
// вызов конструктора точки
{
Font=Msgfont;
// шрифт текста
Field=Msgfield;
// размер текста
Msg=Text;
// указатель на текст
}
void Message::Show()
// показ теста
{
int size=Field / (8*strlen(Msg));
// размер текста (8 пикселей
// на символ)
settextjustify(CENTER_TEXT, CENTER_TEXT);// центр теста
settextstyle(Font, HORIZ_DIR, size); // стиль текста
outtextxy (X,Y,Msg); // вывод текста
}
void Message::Hide() // стирание текста
{ int Tempcolor;
Tempcolor=getcolor(); // сохранение цвета
setcolor(getbkcolor()); // цвет фона
Visible=false; // текст невидим
int size=Field / (8*strlen(Msg)); // размер текста
settextjustify(CENTER_TEXT, CENTER_TEXT); // центр текста
settextstyle(Font, HORIZ_DIR, size); // стиль текста
outtextxy (X,Y,Msg); // текст цветом фона
setcolor(Tempcolor); // текущий цвет
}
// Методы класса Msgcirc:
// конструктор Msgcirc:
Msgcirc::Msgcirc(int Mcircx, int Mcircy, int Mcircrad, int Font, char *Msg):
Circle(Mcircx,Mcircy,Mcircrad),
// вызов конструкторов
// Circle
Message(Mcircx,Mcircy,Font,2*Mcircrad,Msg)
// и Message
118
{}
void Msgcirc::Show()
{
Circle::Show();
Message::Show();
}
void Msgcirc::Hide()
{
Circle::Hide();
Message::Hide();
}
// вывод окружности и текста
// вывод окружности
// вывод текста
// стирание окружности и текста
// стирание окружности
// стирание текста
void main()
// главная функция
{ int gdriver = DETECT, gmode;
// параметры графики
initgraph (&gdriver, &gmode, path);
// инициирование графики
int col;
// пременная цвета
// создать и инициировать три объекта:
Msgcirc Small(250,100, 25, SANS_SERIF_FONT, "You");
Msgcirc Medium(250,150,100, TRIPLEX_FONT, "World");
Msgcirc Large(250, 250, 225, GOTHIC_FONT, "Universe");
while(!kbhit()) // цикл показа объектов до нажатия любой клавиши
{ randomize();
// рандомизация
col = random(16);
// случайный цвет
if (col == BLACK) col++;
// исключение черного цвета
setcolor(col);
// текущий цвет
Small.Show();
delay(500);
Medium.Show();
delay(500);
Large.Show();
delay(500);
Large.Hide();
delay(500);
Medium.Hide();
delay(500);
Small.Hide();
delay(500);
}
closegraph();
// вывод объектов с задержкой
// стирание объектов с задержкой
// закрытие графического режима
}
119
ШАБЛОНЫ
В С++ предусмотрена еще одна реализация идеи полиформизма – шаблоны функций (Function Templates) и шаблоны классов (Class
Templates). Подобно тому, как класс представляет собой схематическое построение объекта (то есть его программную модель), так и
шаблон представляет схематическое описание построения функций
и классов.
С точки зрения проектирования шаблоны служат двум, мало связанным между собой целям:
обобщенному программированию;
политике параметризации.
На ранних стадиях проектирования программного продукта операции
являются просто операциями. Затем, когда настает время описать типы
операндов, шаблоны приобратают огромную важность. Без шаблонов определения функций стали бы повторяться. Операция, реализующая алгоритм для операндов разных типов, является кандидатом на реализацию в
виде шаблона. Если все операнды вписываются в единую иерархию классов
и если есть необходимость добавлять новые типы операндов во время выполнения программы, тип операнда лучше всего представить классом – часто абстрактным классом. Если типы операнда не вписываются в единую
иерархию, и особенно если критично быстродействие, эти операции лучше
реализовать в виде шаблона.
Шаблоны являются средством универсализации программирования, так как обеспечивают простой способ введения разного рода
общих концепций и простые методы их совместного использования.
Алгоритмы становятся обобщенными по множеству параметров.
Этот стиль использования шаблонов называется обобщенным программированием. Шаблоны обеспечивают непосредственную поддержку обобщенного программирования с использованием типов в
качестве параметров при определении класса или функции, поэтому шаблоны иногда называют параметризованными типами (или
генераторами типов), поскольку они указывают лишь спецификации для функций и классов, но не детали настоящей реализации.
Шаблон зависит только от тех свойств параметра-типа, которые он
явно использует, и не требует, чтобы различные типы, используемые в качестве аргументов, были связаны каким-либо другим образом. В частности, типы аргументов шаблона не должны принадлежать к одной иерархии наследования.
Шаблоны особенно полезны в библиотеках классов, которыми
пользуются программисты. Методы, которые можно использовать
120
при проектировании и реализации стандартной библиотеки, позволяют программисту спрятать сложную реализацию за простыми интерфейсами. Например, sort(v) может служить интерфейсом
для множества разных алгоритмов сортировки элементов различных типов, хранящихся в самых разнообразных контейнерах (вектор, список, стек и т.д.). Функция сортировки, наиболее подходящая для конкретного v, будет выбрана автоматически. Стандартная
сортировка является обобщенной по типам контейнеров, поскольку
к ней можно обращаться для произвольных контейнеров, удовлетворяющих стандартам.
Шаблоны являются механизмом времени компиляции, поэтому
их использование не влечет дополнительных накладных расходов
во время исполнения по сравнению с «программированием вручную».
Шаблоны функций
Шаблонные функции описывают общие свойства функций, подобно рецепту приготовления пирога. Шаблоны функций, обычно
объявляемые в заголовочном файле, имеют следующую форму:
template <class T>
void f (T param)
{ // тело функции
}
Префикс template <class T> указывает, что объявлен шаблон
(template), и что в объявлении на месте Т будет указан фактический
тип, определяемой пользователем функции. Т можно заменить на
любое другое имя. Слово class обозначает любой тип данных, а не
обязательно класс С++. Необходим, по крайней мере, один параметр типа Т в скобках для передачи функции данных для обработки. Можно задать значение (T param), указатель (T* param) или ссылку (T& param).
Пример 45.
Демонстрация работы шаблона функции для вычисления квадрата числа. В программе задаются прототипы действительных
функций для обработки данных различных типов.
#include<iostream.h>
#include<conio.h>
// шаблон функции:
121
template <class T> T sqrt (T a)
{ return a*a; }
// прототипы реальных функций:
int sqrt (int x);
float sqrt (float x);
long sqrt (long x);
long double sqrt (long double x);
// главная функция:
void main ()
{ int i = 10; float f = 1.1; long l = 12345; long double ld = 12.3E12;
clrscr();
cout << "int i = " << sqrt (i) <<endl;
cout << "float f = " << sqrt (f) <<endl;
cout << "long l = " << sqrt (l) <<endl;
cout << "long double ld = " << sqrt (ld) <<endl;
getch();
}
В шаблоне можно объявлять несколько параметров и возвращать
значение типа Т, например:
template <class T> T f (int a, T b)
{ // тело функции
}
В этой версии шаблонная функция f возвращает значения типа Т
и имеет два параметра — целое с именем а и неопределенный объект
типа Т. Пользователю нужно задать действительный тип данных
для Т. Компилятор использует шаблон для применения к реальной
функции, которую можно вызывать подобно другим функциям. Например, чтобы использовать этот шаблон, можно объявить следующий прототип:
double f (int a, double b);
Если бы f была обычной функцией, то пришлось бы обеспечить
ее реализацию. Но поскольку f – шаблонная функция, компилятор
сам реализует код функции, заменив Т на double. Конечно, над типом, для которого будет вызываться шаблонная функция, должны
быть определены операции над переменными типа Т, которые используются в теле функции.
Рассмотрим примеры, в которых проясняется, как с помощью
шаблонов можно уменьшить размер и сложность программ.
122
Пример 46.
Объявляется два шаблона функций min() и max() для определения
минимума или максимума из двух значений. В программе объявляются прототипы действительных функций для обработки данных
различных типов.
#include<iostream.h>
#include<conio.h>
// шаблоны функций:
template <class T> T max (T a, T b)
{ if (a > b) return a;
return b;
}
template <class T> T min (T a, T b)
{ if (a < b) return a;
return b;
}
// прототипы реальных функций с данными разных типов:
int max (int a, int b);
double max (double a, double b);
char max (char a, char b);
int min (int a, int b);
double min (double a, double b);
char min (char a, char b);
// главная функция:
void main()
{ int i1=100, i2=200;
double d1=3.14, d2=9.8;
char c1= ‘A’, c2 = ‘Z’;
clrscr();
cout<<"max(i1=" << i1 << ", i2=" << i2 <<"): " << max(i1, i2) <<endl;
cout<<"max(d1="<<d1<<", d2="<< d2 <<"): " << max(d1, d2) <<endl;
cout<<"max(c1="<< c1<<", c2="<< c2 <<"): " << max(c1, c2) <<endl;
cout<<"min(i1=" << i1 << ", i2=" << i2 <<"): " << min(i1, i2) <<endl;
cout<<"min(d1="<< d1<<", d2=" << d2 <<"): " << min(d1, d2) <<endl;
cout<<"min(c1="<< c1<<", c2=" << c2 <<"): " << min(c1, c2) <<endl;
getch();
}
Результаты программы:
max(i1=100, i2=200): 200
123
max(d1=3.14, d2=9.8): 9.8
max(c1=A, c2=Z): Z
min(i1=100, i2=200): 100
min(d1=3.14, d2=9.8): 3.14
min(c1=A, c2=Z): A
В шаблоне функции можно использовать несколько параметров
и не обязательно одинакового типа, например:
template <class T1, class T2>
T1 max (T1 a, T2 b)
{ if (a > b) return a;
return b;
}
При этом возникает вопрос, почему возвращаемый тип Т1, а не Т2?
В С++ нет механизма изменения типа возвращаемого значения,
поэтому надо было сделать какой-либо выбор. Для того чтобы эти
функции возвращали результат без потери значения, при вызове
функции желательно переменную или константу "старшего" типа
использовать в качестве первого параметра.
Пример 47.
Использование шаблона функции с двумя параметрами разных
типов:
#include<iostream.h>
template <class T1, class T2>
T1 max (T1 a, T2 b)
{ if (a > b) return a;
return b;
}
void main()
{ int i=5; double d=3.23; long l=123456; long double ld=1.2E123;
cout<< "max(d, i)=" << max(d, i) << endl;
cout<< "max(i, d)=" << max(i, d) << endl;
cout<< "max(ld, l)=" << max(ld, l) << endl;
// В двух следующих вызовах будет неверный результат,
// это связано не с шаблоном, а преобразованием типов:
cout<< "max(i, l)=" << max(i, l) << endl;
cout<< "max(l, 5.5)=" << max(l, 5.5) << endl;
// А в этом вызове результат верный:
cout<< "max(5.5, l)=" << max(5.5, l) << endl;
}
124
Перегрузка шаблонов функций
Шаблон функции описывает, как сгенерировать функцию по соответствующему набору аргументов шаблона. Таким образом, шаблоны можно использовать для генерирования типов и исполнимого кода. Версия шаблона для конкретного аргумента шаблона называется специализацией. Сгенерированные функции являются
обычными функциями, которые подчиняются всем стандартным
правилам для функций.
Можно объявить несколько шаблонов функции с одним и тем же
именем и даже объявить комбинацию шаблонов и обычных функций с одинаковым именем. Когда вызывается перегруженная функция, требуется механизм разрешения перегрузки для нахождения
требуемой функции или шаблона функции. Например:
template <class T> T sqrt (T);
template <class T> complex <T> sqrt (complex <T>);
double sqrt (double);
void f (complex<double> z)
{ sqrt (2);
// sqrt<int> (int)
sqrt (2.0);
// sqrt (double)
sqrt ( z );
// sqrt <double> (complex<double>)
}
Аналогично тому, как шаблон функции является обобщением
понятия функции, так и правила разрешения при наличии шаблонов функций являются обобщением правил разрешения перегрузки функций. Для каждого шаблона осуществляется поиск специализации, наилучшим образом соответствующей списку аргументов. Затем применяются обычные правила разрешения перегрузки
функций применительно к этим специализациям и обычным функциям.
1. Если обычная функция и специализация подходят одинаково
хорошо, предпочтение отдается обычной функции: для sqrt(2.0) выбирается sqrt(double), а не sqrt <double> (double).
2. Ищется набор специализаций шаблонов функций, которые примут участие в разрешении перегрузки, с учетом всех шаблонов функции. С вызовом sqrt(z) к рассмотрению будут приняты sqrt<double> (complex<double>) и sqrt <complex <double> >
(complex<double>).
125
3. Если могут быть вызваны два шаблона функции, и один из них
более специализирован, чем другой, то он и рассматривается: с вызовом sqrt(z) предпочтение отдается sqrt<double> (complex<double>).
4. Разрешается перегрузка для набора шаблонов функций, а
также для обычных функций: для sqrt(2) более точным соответствием обладает sqrt<int> (int) по сравнению с sqrt (double).
5. Если ни одного соответствия не найдено, вызов считается
ошибочным. Если процесс заканчивается с одинаково подходящими вариантами, вызов является неоднозначным и это тоже ошибка.
Например:
template <class T> max (T, T);
const int s = 7;
void f ()
{ max (1, 2); // max<int> (1, 2)
max (‘a’, ‘b’);
// max<char> (‘a’, ‘b’)
max (2.7, 4.9);
// max<double> (2.7, 4.9)
max (s, 7);
// max<int> (int(s), 7) – используется
// тривиальное преобразование
max (‘a’, 1);
// ошибка: неоднозначность (стандартные
// преобразования не применяются
max (2.7, 4);
// ошибка: неоднозначность (стандартные
// преобразования не применяются
}
Две неоднозначности можно разрешить при помощи явного квалификатора:
void f ()
{ max<int> (‘a’, 1);
// max (int (‘a’), 1)
max<double> (2.7, 4);
// max (2.7, double (4))
}
Шаблоны классов
Шаблонные классы предоставляют пользователям еще большие
возможности, чем шаблонные функции, позволяя построить отдельные классы аналогично шаблонам функций. Имя шаблона класса
должно быть уникальным, то есть перегрузка для шаблона класса
не возможна (в отличие от шаблона функции). Шаблон класса задает параметризованный тип и обеспечивает скелет обобщенного
класса для его последующей реализации с помощью типов данных,
определенных пользователем. В качестве параметра при задании
126
шаблона класса можно использовать не только тип, но и другие параметры, например целочисленные значения, но они должны быть
константными выражениями.
Класс, генерируемый из шаблона класса, является совершенно
нормальным классом. Поэтому использование шаблона не подразумевает каких-либо дополнительных механизмов времени исполнения сверх того, что использовались бы для написания класса вручную.
Объявление шаблона класса имеет следующий общий вид:
template <class T>
class Anyclass
{
// закрытые, защищенные и открытые элементы класса
};
// точка с запятой обязательна, как при описании класса,
где Т – неопределенный тип (возможен любой идентификатор), который необходимо задать при использовании класса. Как и в случае шаблонных функций, Т впоследствии можно заменить любым
встроенным типом, другим классом, указателем и т.д. Anyclass –
имя шаблонного класса.
В теле шаблона класса тип Т может использоваться для объявления данных-элементов, типов возвращаемых методами значений,
параметров и прочих элементов пока еще неопределенных типов,
например:
Т* ptr;
// указатель типа Т
Как и шаблоны функций, шаблоны классов чаще всего объявляются в заголовочных файлах.
Параметры шаблонов.
Параметрами шаблонов могут быть: параметры-типы, параметры обычных типов, такие как int, и параметры-шаблоны. Естественно, у шаблонов может быть несколько параметров. Например:
template<class T, T val> class Buffer {/*...*/};
Как видно, параметр шаблона может быть использован для определения последующих параметров шаблона. Целые аргументы используются обычно для задания размеров и границ. Например:
template<class T, int n>
class Buffer
{
T v [n];
// массив типа Т
int sz;
// переменная для размера массива
public:
127
Buffer (): sz (n) { } // конструктор с инициализацией размера
// ...
};
Объекты класса Buffer:
Buffer<char, 127> cbuf; // буфер для 127 символов
Buffer<Record, 8> rbuf; // буфер для 8 записей
Аргумент шаблона может быть константным выражением, адресом объекта или функции, или неперегруженным указателем на
элемент. Указатель, используемый в качестве аргумента шаблона,
должен иметь форму &of, где of является именем объекта или функции, либо в форме f, где f является именем функции. Указатель на
элемент должен быть в форме &X::of, где of является именем элемента. В частности, строковый литерал не допустим в качестве аргумента шаблона. Целый аргумент шаблона должен быть константой:
void f (int i)
{ Buffer<int, i> bx; }
// ошибка: требуется константное выражение
И наоборот, параметр шаблона, не являющийся типом, должен
быть константой в теле шаблона, поэтому попытка изменения значения этого параметра приводит к ошибке.
Элементы шаблона класса объявляются и определяются так же,
как и для обычного класса. Их можно определить как внутри, так
и вне шаблона класса. Элементы шаблона класса в свою очередь являются шаблонами, параметризованными при помощи аргументов
шаблона.
Пример 48.
Представим объявление шаблона класса Database, реализующего
базу данных с небольшим числом записей.
template <class T>
// шаблон класса с неопределенным типом
class Database
// имя класса
{
T* rp;
// указатель на записи
int num;
// число записей
public:
Database (int n)
// встроенный конструктор
{ rp = new T[num=n]; }
// указатель на динамическую память
~Database()
// встроенный деструктор
{ delete [ ] rp;}
// очистка динамической памяти
void Donothing ();
// прототип некоторой функции
T& Getrecord (int recnum); // прототип функции с параметром
128
// номера записи, возвращающей ссылку на объект типа Т
};
// описание функций шаблона класса:
// образец шаблона некоторой функции из шаблона класса Database:
template <class T>
// шаблон типа Т
void Database <T>:: Donothing ()
// заголовок функции
{
// тело функции пустое
}
// описание функции-элемента шаблона класса Database:
template <class T>
// шаблон типа Т
T& Database <T>:: Getrecord (int recnum)// метод получения записи
{ T* crp = rp;
// текущий указатель
if (0 <= recnum && recnum < num)
// проверка диапазона
while ( recnum-- > 0)
// цикл, пока число записей > 0
crp++;
// сдвиг указателя на одну запись
return *crp;
// возврат указателя на запись
}
Комментарии.
Рассмотрим объявление шаблона класса Database более внимательно. Природа типа Т неизвестна, но уже можно описать указатель rp типа Т и конструктор выделяет динамическую память для
массива объектов типа Т, присваивая адрес начала массива указателю rp и устанавливая элемент num равным требуемому числу записей. Деструктор должен удалить массив с указателем rp с помощью
оператора delete [ ] rp.
Как и для обычных классов, методы шаблонного класса могут
быть встраиваемыми, как конструктор и деструктор, либо могут
описываться вне класса. Например, функция Donothing, которая не
выполняет действий, демонстрирует формат описания метода шаблонного класса как шаблонной функции с оператором разрешения
области видимости (Database <T>::).
В классе объявляется также метод Getrecord, возвращающий
ссылку на объект типа Т, идентифицируемый номером записи
recnum. Это еще один пример операции, не требующей знания реального типа объекта. В функции указатель rp типа Т присваивается
указателю того же типа crp, который ссылается на первую запись,
сохраненную в объекте класса Database. Оператор if проверяет, соответствует ли параметр recnum допустимому диапазону значений.
Если да, то цикл while уменьшает параметр recnum до нуля и смещает указатель crp на одну запись размера sizeof (T) в базе данных.
129
При компиляции вместо Т будет использован тип реальных объектов. Функция возвращает разыменованное значение указателя crp,
то есть ссылку на любой объект, адресуемый crp.
Поскольку шаблонный класс Database и его шаблонные функции
являются объявлениями, включим их в заголовочный файл db.h,
чтобы воспользоваться им затем в основной программе.
Пример 49.
Заголовочный файл db.h используем для реализации действительного класса Record с целью получения базы данных четырьмя
способами создания объектов класса с помощью шаблона класса.
Программу сохраним в файле database.cpp в директории, где находится файл db.h:
# include<iostream.h>
#include<string.h>
#include"db.h"
// реальный класс Record для создания записей в базе данных:
class Record
{
char name[41];
// символьный массив – private
public:
Record ()
// конструктор по умолчанию
{ name[0] = 0;}
// обнуление базы данных (БД)
Record ( const char* s)
// конструктор заполнения БД
{ Assign(s); }
// функцией копирования
void Assign (const char* s)
// метод копирования строки s
{ strncpy(name, s, 40); }
// как записи в БД
char* Getname ()
// метод возврата указателя
{ return name; }
// на строку записи в БД
};
void main()
// главная функция
{ int rn;
// индекс числа записей
clrscr();
// создание объектов с помощью шаблона класса Database:
Database <Record> db (3);
// БД из 3 записей типа Record
Database <Record*> dbp (3);
// БД из 3 указателей типа Record
Database <Record> *pdb;
// указатель на БД
Database <Record*> *ppdb;
// указатель на БД указателей
// создание БД из записей типа Record:
130
}
cout << "Database of 3 Records:" << endl;
db.Getrecord(0). Assign ("A. Pushkin");
db.Getrecord(1). Assign ("M. Lermontov");
db.Getrecord(2). Assign ("L. Tolstoy");
for (rn = 0; rn < 3; rn++)
cout << db.Getrecord (rn).Getname() << endl;
// создание БД из указателей типа Record:
cout << "Database of 3 Record pointers:" << endl;
dbp.Getrecord(0) = new Record ("I. Repin");
dbp.Getrecord(1) = new Record ("V. Serov");
dbp.Getrecord(2) = new Record ("M. Vrubel");
for (rn = 0; rn < 3; rn++)
cout << dbp.Getrecord (rn)->Getname() << endl;
// создание указателя на БД записей типа Record:
cout << "Pointer to database of 3 Records:" << endl;
pdb = new Database <Record> (3);
pdb->Getrecord (0). Assign ("Rafael");
pdb->Getrecord (1). Assign ("Leonardo");
pdb->Getrecord (2). Assign ("Mikelangelo");
for (rn = 0; rn < 3; rn++)
cout << pdb->Getrecord (rn).Getname() <<endl;
// создание указателя на БД из указателей типа Record:
cout << "Pointer to database of 3 Record pointers:" << endl;
ppdb = new Database <Record*> (3);
ppdb->Getrecord(0) = new Record ("A. Einshtein");
ppdb->Getrecord(1) = new Record ("L. Landau");
ppdb->Getrecord(2) = new Record ("A. Sakharov");
for (rn = 0; rn < 3; rn++)
cout << pdb->Getrecord (rn)->Getname() << endl;
getch();
Комментарии к программе.
Объявлен класс Record, который программа использует для запоминания в Database. Класс Record очень прост, в нем объявлен в
качестве элемента только символьный массив name. На самом деле
вместо Record может быть любой произвольный класс, так как в шаблоне класса Database нет ограничений на тип объектов.
В функции используются четыре способа создания объектов с
помощью шаблона класса. В объявлении
Database <Record> db (3);
131
определяется объект db с именем шаблонного класса Database и задается Record в качестве класса, замещающего Т в шаблоне. Выражение в скобках (3) – это инициализатор, передаваемый конструктору класса Database.
Вместо использования типа Record можно определить базу данных из 100 вещественных значений:
Database <double> dbd (100);
Поскольку класс Database написан для сохранения объектов произвольных типов, он называется контейнерным классом. Обычно,
наилучшие контейнерные классы являются полностью обобщенными.
В функции следующее объявление
Database <Record*> dbp (3);
определяет объект dbp как указатель класса Database, состоящий из
трех указателей на Record.
Третье объявление
Database <Record> *pdb;
определяет pdb как указатель на объект класса Database с незаданным числом объектов типа Record.
Последнее объявление
Database <Record*> *ppdb;
объединяет предыдущие два объявления для создания указателя
на базу данных указателей, которые ссылаются на объекты типа
Record.
В программе объект класса Database используется так же, как и
любой другой не шаблонный объект. Например, для создания базы
данных первым способом в операторе
db.Getrecord (0). Assign ("A. Pushkin");
объектом db вызывается функция Getrecord (0) для доступа к записи
с индексом 0. Затем в строке вызывается метод Assign этого объекта
для задания строкового значения.
В программе демонстрируется также использование других объектов класса Database. Представляет интерес второй способ заполнения базы данных с помощью указателя следующим оператором:
dbp.Getrecord (0) = new Record ("I. Repin");
Поскольку функция Getrecord возвращает ссылку на объект указателю dbp, она может использоваться в левой части оператора присваивания. В данном случае создается новый объект типа Record, а
132
его адрес присваивается в базе данных указателю записи с индексом 0, ссылку на которую возвращает функция Getrecord.
В третьем способе для заполнения базы данных оператором
pdb = new Database <Record> (3);
создается указатель pdb типа Database на базу данных из трех
объектов-записей класса Record, которые заполняются методом
Assign с использованием оператора, например,
pdb->Getrecord (0). Assign ("Rafael");
Четвертый способ объединяет два предыдущих для создания
указателя ppdb типа Database на массив указателей типа Record:
ppdb = new Database <Record*> (3);
С помощью указателей создаются новые объекты-записи типа
Record:
ppdb->Getrecord(0) = new Record ("A. Einshtein");
Важно то, что во всех этих примерах не нужно информировать
компилятор с помощью приведения типов о типах данных, используемых классом Database. Благодаря обобщенной природе шаблонов, можно избежать приведения типов и создавать классы, которые могут поддерживать множество различных типов данных.
Примечание.
Четыре примера объявлений Database из функции main можно
использовать в качестве образца для создания объектов большинства типов шаблонов класса. Создание шаблонов с нуля – довольно трудная задача. Поэтому проще всего создать рабочий класс, который обрабатывает реальные данные, а затем преобразовать его в
универсальный шаблон, заменив реальные типы данных меткамизаполнителями <class T>.
133
ДРУЗЬЯ
Дружественные функции
Одним из важных принципов С++ является защита данных от
несанкционированного использования с помощью режимов доступа к элементам класса. Обычно доступ к собственным (private) элементам класса ограничивается методами этого класса. Однако возникают ситуации, когда необходимо, чтобы к закрытым элементам
данных класса имела доступ функция, не принадлежащая этому
классу и даже из другого класса. Это можно сделать, объявив эту
функцию с помощью ключевого слова friend (друг) как дружественную функцию (friend function). Например:
class X
{
int n;
public:
friend void fr (void);
};
Функция fr может обращаться к элементу n.
Обычное объявление функции-элемента гарантирует три логически разные вещи:
функция имеет право доступа к закрытой части объявления
класса;
функция находится в области видимости класса;
функция должна вызываться для объекта класса (имеется указатель this).
Объявив функцию как friend, мы наделяем ее только первым
свойством. Причиной введения функций-друзей явилась ситуация,
когда одна и та же функция должна использовать закрытые элементы двух или более классов. Объявление друзей не является (в общем
случае) нарушением принципа инкапсуляции данных, поскольку
сам класс разрешает доступ функции-другу к закрытой части класса. Это можно рассматривать как модель отношения людей, когда
наши близкие друзья могут приходить к нам в дом, в отличие от чужих людей.
Объявление friend можно поместить и в закрытой и в открытой части объявления класса. Так же как и функции-элементы, функциидрузья явно указываются в объявлении класса, друзъями которого
они являются. Поэтому они в той же мере являются частью интерфейса класса, как и функции-элементы.
134
Пример 50.
Представим программу использования функции-друга для двух
классов (Box, Line). Она должна изобразить на экране цветные прямоугольники и линии, сравнивая попарно их цвета (color) функциейдругом samecolor(). Если цвета совпадают, выдается сообщение Same
color, иначе Different color.
#include<iostream.h>
#include<conio.h>
class Line;
// предварительное объявление класса Line
class Box
// класс "прямоугольник"
{ int color;
// цвет рамки
int upx, upy;
// координаты левого верхнего угла
int lowx, lowy;
// координаты правого нижнего угла
public:
friend int samecolor (Line l, Box b);
// функция-друг
void setcolor (int c);
// цвет
void definebox (int x1, int y1, int x2, int y2);
// координаты
void showbox ();
// вывод на экран
};
class Line
// класс «линия»
{ int color;
// цвет линии
int x0, y0;
// координаты начала линии
int len;
// длина линии
public:
// прототипы функций:
friend int samecolor (Line l, Box b); // функция-друг
void setcolor (int c);
// цвет рамки
void defineline (int x, int y, int l);
// координаты и длина линии
void showline ();
// вывод линии
};
// Описание методов класса Box:
void Box :: setcolor (int c)
// установка цвета
{ color = c;
}
void Box :: definebox (int x1, int y1, int x2, int y2) // координаты
{ upx = x1; upy = y1; lowx = x2; lowy = y2;
}
void Box :: showbox ()
// метод вывода прямоугольника
{ window(upx, upy,lowx, lowy);
// координаты окна
textbackground (color);
// установка цвета фона
clrscr();
// чистка окна
135
textbackground (BLACK);
window (1, 1, 80, 25);
// цвет фона
// окно экрана
}
// Описание методов класса Line:
void Line :: setcolor (int c)
// установка цвета линии
{ color = c;
}
void Line :: defineline (int x, int y, int l)
// определение линии
{ x0 = x; y0 = y; len = l;
// координаты и длина линии
}
void Line :: showline ()
// метод вывода линии
{ textcolor (color);
// цвет линии
gotoxy (x0, y0);
// начало линии
for (int k=0; k<len; k++)
// цикл вывода линии
cprintf("%c", '-');
textcolor (WHITE);
// смена цвета линии
}
// Описание функции-друга:
int samecolor (Line l, Box b)
{ if (l.color == b.cololr) // если цвета линии и окна одинаковы, то
return 1;
// возврат 1, иначе
return 0;
// возврат 0
}
void main ()
// главная функция
{ Line l;
// создание объекта линии
Box b;
// создание объекта прямоугольника
textbackground (BLACK);
// установка цвета фона
clrscr ();
// чистка экрана
b.definebox (5, 5, 25, 10);
// задание координат прямоугольника
b.setcolor (RED);
// цвет прямоугольника
l.defineline (5, 15, 30);
// задание координат и длины линии
l.setcolor (BLUE);
// цвет линии
b.showbox ();
// вывод прямоугольника
l.showline ();
// вывод линии
gotoxy (1,1);
// координаты курсора
if (samecolor (l, b))
// если цвета линии и окна одинаковы
cputs ("Same colors\n"); // сообщение:"Цвета одинаковы"
else cputs ("Different colors\n");
// иначе «Цвета различны"
getch ();
// задержка экрана
}
136
Комментарии к программе.
В программе дано предварительное объявление класса Line, похожее на прототип (class Line;), которое используется в классе Box,
до того как определен класс Line, поэтому необходимо сделать такое
уведомление.
Дружественной может быть не только внешняя функция, как в
примере, но и метод другого класса. Например:
class X
{
// …
int* next();
};
class Y
{
friend int* X:: next();
// …
};
В примере, рассмотренном выше, можно было бы объявить
функцию samecolor методом класса Box, изменив его описание:
class Box
{ ...
public:
int samecolor ( Line l); // метод класса Box
...
};
В классе Line необходимо объявить функцию-друга:
class Line
{ ...
public:
friend int Box::samecolor (Line l);
// функция-друг
...
};
В классе Line использовано полное имя функции Box::samecolor().
Кроме того, можно не указывать в качестве аргумента объект класса Box. Новая функция-друг Box::samecolor() примет вид:
int Box::samecolor ( Line l )
{ if (l.color == color)
return 1;
return 0;
}
// используется указатель this -> color
137
Дружественные классы
При определении класса можно объявить сразу все методы другого класса дружественными одним объявлением:
class X {...};
class Y
{ ...
friend class X;
...
};
// дружественный класс
Ясно, что классы-друзья должны использоваться только для отражения тесно связанных концепций. Объявление класса Х другом
предполагает, что закрытые и защищенные элементы класса Y могут использоваться в классе Х, то есть любой метод класса Х может
иметь доступ к закрытым элементам класса Y. Часто существует
выбор между реализацией класса в качестве элемента (вложенного
класса) или в качестве друга.
Дружба классов не наследуется и не транзитивна (как и в жизни
людей). Если некто В является другом А, а D – друг В, то отсюда не
следует, что D – друг А.
Например:
class А
{
friend class В;
int a;
};
class B
{
friend class C;
};
class C
{
void f (A* p)
{ p-> a++; // ошибка: С – не друг класса А, хотя он друг класса
В
}
};
class D: public B
{
void f (A* p)
{ p-> a++; // ошибка: D – не друг А, хотя он производный от В
// друга класса А
}
};
138
Иногда можно встретить неопределенную операцию, объявленную как дружественная, но, вообще говоря, функции-друзья должны употребляться экономно – если они присутствуют в проекте, то
зачастую это признак того, что иерархия классов нуждается в исправлении. Объявление спецификатора friend снижает одно из основных преимуществ ООП – инкапсуляцию данных и функций и модульность. (Если вы предоставили свою квартиру на время отпуска
приятелю, то, вернувшись, вы можете застать ее не в том порядке,
каком оставляли.) Лучше, чтобы как можно больше классов были
друг другу "чужаками". Объявление friend должно употребляться
только тогда, когда это действительно необходимо, например в случае переплетенной иерархии классов. В частности, если возникает
необходимость сделать целый класс дружественным по отношению
к другому классу, следует рассмотреть возможность создания общего производного класса с целью доступа через его посредство к требуемым элементам.
При работе с дружественными классами следует учитывать следующие обстоятельства:
в классе должны быть перечислены все его друзья;
класс, в котором другой класс объявлен другом, дает этому другу
доступ к своим обычно скрытым элементам;
порядок объявлений не играет роли, однако обычно классыдрузья объявляются последними, чтобы встраиваемые функции
класса могли обращаться к скрытым частям другого класса;
производные классы от классов-друзей не наследуют доступа к
скрытым элементам исходного класса.
Пример 51.
Рассмотрим программу, демонстрирующую, как дружественный
класс получает доступ к закрытым и защищенным элементам другого.
#include<iostream.h>
class Pal
{
friend class Friend;
// Friend – друг класса Pal
private:
int x;
// доступен в Pal и Friend
protected:
void dx () { x *= x;}
// доступен в Pal и Friend
public:
Pal () { x=100; }
// конструктор – доступен всем пользователям
139
Pal ( int n ) { x = n; } // конструктор – доступен всем пользователям
};
class Friend
// класс-друг класса Pal
{ private:
Pal palob;
// объект класса Pal доступен методам Friend
public:
void Showvalues ();
// метод доступен всем пользователям
};
void Friend::Showvalues ()
// метод вывода доступен всем
//пользователям
{ Pal apal (1234);
// иницирован объект класса Pal
cout << "Before, palob.x = " << palob.x << endl;
palob.dx ();
cout << "After, palob.x = " << palob.x << endl;
cout << "apal.x = " << apal.x << endl;
}
void main ()
{
Friend afriend;
// создан объект класса Friend
afriend.Showvalues ();
// вывод данных объектов класса Pal
}
Результаты программы:
Before, palob = 100
After, palob = 10000
apal.x = 1234
Комментарий к программе.
Программа начинается с объявления класса Pal (приятель). Класс
Friend объявляется как друг класса Pal. Значит, методы класса Friend
имеют доступ ко всем элементам класса Pal. А класс Pal не имеет доступа к закрытым и защищенным элементам класса Friend. В классе
Friend объявляется закрытый объект класса Pal с именем palob.
Запустив на выполнение программу, можно убедиться, что, хотя
класс Friend не состоит в родстве с Pal, метод Friend::Showvalues имеет
непосредственный доступ к закрытому элементу х и к защищенному методу dx класса Pal. Если бы Friend не был другом Pal, компилятор бы не дал право на существование таким выражениям, как
palob.dx и apal.x.
140
Статические элементы класса
Некоторые элементы класса могут быть объявлены с модификатором класса памяти static и их называют статическими элементами класса. Эти элементы являются частью класса, но не частью
объекта этого класса. Объявление статических элементов-данных
не является описанием, то есть под эти данные не выделяется память. Их описание должно быть задано в другом месте программы. Существует ровно одна копия статического элемента-данного,
в отличие от обычных элементов, когда каждый объект класса имеет свои независимые элементы. Тем самым все объекты ссылаются на одно и тоже место в памяти. Изменив значение статического
элемента в одном объекте, получим изменившееся значение во всех
других объектах класса.
Аналогично функции-элементы класса также могут быть объявлены статическими (static). Таким функциям требуется доступ к
элементам класса, но не требуется, чтобы они вызывались для конкретного объекта. Они не получают указатель this, соответственно
могут обращаться только к статическим элементам-данным класса посредством операций точка (.) или стрелка (->). Статическая
функция-элемент не может быть виртуальной. К статическим элементам (данным и функциям) можно обращаться, даже если не
создано ни одного объекта класса, надо только использовать полное имя элемента класса. Если функция f() является статической
функцией-членом класса cl, можно вызвать эту функцию: cl::f(). К
статическим элементам можно обращаться так же, как и к любым
другим элементам класса.
Пример 52.
Рассмотрим программу, в которой демонстрируется использование статических элементов класса для подсчета числа существующих и созданных объектов класса.
#include<iostream.h>
#include<conio.h>
class st
{
static int count1;
static int count2;
public:
static void showcount ();
st ();
~st();
};
// статические элементы-данные
// статическая функция элемент вывода
// прототип конструктора
// прототип деструктора
141
st::st ()
// конструктор
{
cout<<"constructor"<<endl;
count1++;
// счетчики объектов
count2++;
}
st::~st ()
// деструктор
{
cout<<"destructor"<<endl;
count2 --; // уменьшение значения счетчика
}
void st::showcount ()
// функция вывода значений счетчиков
{
cout << "Create objects: " << count1 << endl;// создано объектов
cout << "Exist objects: " << count2 << endl; // существует объектов
}
int st:: count1;
// описание типа статических элементов
int st:: count2;
// для выделения памяти
void main ()
// главная функция
{
clrscr();
// чистка экрана результатов
cout<<"1 show:"<<endl;
// 1-й заголовок вывода данных
st::showcount ();
// вывод данных счетчиков объектов
st a, b, c, *p;
// создание трех объектов и указателя
cout<<"2 show:"<<endl; // 2-й заголовок вывода данных
a.showcount ();
// вывод данных счетчиков объектов
{
st x, y, z;
// создание объектов во внутреннем
// блоке
cout<<"3 show:"<<endl;
// 3-й заголовок вывода данных
st::showcount ();
// вывод данных счетчиков объектов
cout<<"4 show:"<<endl;
// 4-й заголовок вывода данных
z.showcount ();
// вывод данных счетчиков о бъектов
}
// конец видимости внутреннего блока
p = new st;
// создание объекта в динамической
// памяти
cout<<"5 show:"<<endl; // 5-й заголовок вывода данных
st::showcount ();
// вывод данных счетчиков объектов
delete p;
// уничтожение динамического
// объекта
cout<<"6 show:"<<endl; // 6-й заголовок вывода данных
st::showcount ();
// вывод данных счетчиков объектов
getch ();
// задержка экрана результатов
}
142
Результаты программы (в колонках):
1 show:
3 show:
Create objects: 0
Create objects: 6
Exist objects: 0
Exist objects: 6
constructor
4 show:
constructor
Create objects: 6
constructor
Exist objects: 6
2 show:
destructor
Create objects: 3
destructor
Exist objects: 3
destructor
Constructor
constructor
constructor
5 show:
Create objects: 7
Exist objects: 4
destructor
6 show:
Create objects: 7
Exist objects: 3
destructor
destructor
destructor
Комментарии к программе.
С помощью вывода значений счетчиков и действий конструктора
и деструктора можно следить за процессом в системе. Следует обратить внимание на то, как влияет на существование объектов наличие внутреннего блока в скобках { }, который является областью
видимости объектов x, y, z. Сообщение о них выведено двумя способами: 3 show – полным именем функции вывода и 4 show – из любого
объекта (в примере z). Выход за пределы внутреннего блока приводит к автоматическим вызовам деструктора для уничтожения объектов этого блока. Объект, созданный динамически, должен быть
уничтожен явно (см. 5 и 6 show).
143
Библиографический список
1. Страуструп Б. Язык программирования С++: Пер. с англ.
Спб.; М.: Невский диалект-БИНОМ, 1999. 991 с.
2. Березин Б. И., Березин С. Б. Начальный курс С и С++. М.:
Диалог-МИФИ, 2001. 288 с.
144
Содержание
Технологии программирования........................ Структурное и объектно-ориентированное
программирование............................................. Особенности языка С++........................................ Ввод-вывод в С++............................................... Ввод для встроенных типов. ................................ Вывод для встроенных типов............................... Управление вводом-выводом................................ Классы, инкапсуляция, объекты...................... Управление доступом к элементам класса.............. Список элементов класса..................................... Определение методов класса................................ Вызов метода и доступ к элементам класса............. Конструкторы и деструкторы............................... Инициализация конструкторов класса.................. Полиморфизм. Перегрузка функций..................... Перегрузка конструкторов.................................. Функция с параметрами по умолчанию................. Конструктор по умолчанию................................. Конструктор копирования................................... Обобщение перегрузки конструкторов и
инициализации объектов.................................... Указатель объекта this......................................................
Полиморфизм. Перегрузка операций.................... Динамическое выделение памяти......................... Динамическое выделение памяти в конструкторе... Копирование динамических объектов................... НАСЛЕДОВАНИЕ И КОНТРОЛЬ ДОСТУПА.................... Базовые и производные классы............................ Использование конструкторов с параметрами
при наследовании.............................................. Множественное наследование.............................. Полиморфизм. Виртуальные функции.................. Чистые виртуальные функции и абстрактные
классы.............................................................. Производные классы с конструкторами и
деструкторами................................................... Виртуальные деструкторы................................... 3
3
8
8
9
10
12
21
22
23
23
24
27
29
32
33
34
34
35
36
38
38
42
44
47
54
54
59
66
68
77
81
84
145
Обобщенный пример наследования....................... 88
Модульность классов ......................................... 94
Расширяющяся иерархия классов........................ 97
Узловые классы................................................. 101
Множественное наследование классов................... 106
ШАБЛОНЫ................................................................ 120
Шаблоны функций............................................. 121
Перегрузка шаблонов функций............................ 125
Шаблоны классов............................................... 126
ДРУЗЬЯ..................................................................... 134
Дружественные функции.................................... 134
Дружественные классы....................................... 138
Статические элементы класса.............................. 141
Библиографический список................................. 144
146
Учебное издание
Прокушев Лев Антонович
ПРОГРАММИРОВАНИЕ
НА ЯЗЫКЕ ВЫСОКОГО УРОВНЯ
Объектно-ориентированное
программирование на С++
Конспект лекций
Редактор А. В. Подчепаева
Верстальщик А. Н. Колешко
Сдано в набор 15.10.09. Подписано к печати 29.12.09. Формат 60×84 1/16.
Бумага офсетная. Печ. л. 9,25. Уч.-изд. л. 8,62.
Тираж 100 экз. Заказ № 844.
Редакционно-издательский центр ГУАП
190000, Санкт-Петербург, Б. Морская ул., 67
Документ
Категория
Без категории
Просмотров
4
Размер файла
1 425 Кб
Теги
prokushev
1/--страниц
Пожаловаться на содержимое документа