6.4. Вызов методов по указателю на класс

advertisement
Вопрос № 1: Определение ООП языка программирования. Основные свойства ООЯП
– абстракция, инкапсуляция, наследование , полиморфизм.
Основные понятия в ООП – объект и сообщение. Объект моделирует
сущность реального мира или некоторое абстрактное понятие, сообщение определяет
способы взаимодействия объектов. Программа в свете ООП – совокупность объектов,
определенным образом взаимодействующих между собой.
Объекты характеризуются: состоянием (статические свойства объекта и их
текущие значения); поведением (как объект проявляет себя во внешнем мире;
определяется совокупностью методов, обеспечивающих передачу сообщений между
объектами); индивидуальностью.
Сообщение определяется своим именем и всегда имеет адресата – кому оно
адресовано. Кроме того, сообщение может иметь дополнительные аргументы,
передаваемые адресату.
Поведение объекта определяется составом операций, которые можно
выполнять над любым экземпляром множества: включить новый элемент в множество,
вывести значения всех элементов множества, определить новое множество, являющееся
пересечением двух множеств и т.п. Чтобы выполнить какую-либо операцию с некоторым
экземпляром множества, надо послать этому экземпляру сообщение. При этом всегда
выделяются: адресат сообщения (экземпляр множества, которому посылается некоторое
сообщение); имя сообщения (какое именно действие должно быть выполнено);
возможно, дополнительные параметры (в зависимости от конкретного сообщения).
Можно ввести следующие определения (используя и терминологию языка
С++):Объект – инкапсулированная абстракция, которая включает в себя информацию о
состоянии и четко определенное множество протокола доступа (поведение).Класс –
множество объектов, объединенных общностью структуры и поведения.
ООЯП – это язык программирования, обладающий набором определенных
свойств. Главными свойствами ООЯП являются абстракция, инкапсуляция,
наследование, полиморфизм.
Абстракция – это свойство, позволяющее создавать новые типы данных,
выделяя общие абстрактные свойства этих типов.
Инкапсуляция – скрытие информации; механизм, который объединяет данные
и код, манипулирующий с этими данными, а также защищает и то, и другое от внешнего
вмешательства или неправильного использования.Наследование – это процесс,
посредством которого один объект может приобретать свойства другого (точнее, объект
может наследовать свойства другого объекта и добавлять к ним черты, характерные
только для него). Например, роза или ромашка – это разные разновидности некоторого
общего (родительского) класса, называемого цветком. В свою очередь, цветок – это
разновидность еще более общего класса растение, и т.д. В каждом случае порожденный
класс наследует все связанные с родителем качества и добавляет к ним свои собственные
определяющие характеристики.Полиморфизм – это свойство, которое позволяет одно и
то же имя использовать для решения двух или более схожих, но технически разных задач.
Например, в не ОО языках программирования нахождение абсолютной величины числа
требует нескольких различных функций – в зависимости от типов аргумента и результата
(например, в С – это abs() для данных типа int, labs() для данных типа long и
fabs() для данных типа double). В ООЯП каждая из этих функций может быть
названа abs(). Тип данных, который используется при вызове функции, определяет,
какая конкретно версия функции действительно выполняется.
Вопрос № 3: Конструкторы и деструктор: назначение и типы. Определение и
реализация конструкторов; параметры по умолчанию. Назначение и использование
деструктора, Определение и реализация деструктора.
По тому, каким образом конструктор инициализирует состояние класса,
конструкторы определяются как инициализирующие и копирующий.
Инициализирующие конструкторы содержат отдельные значения,
используемые для инициализации состояния полей экземпляра класса. В списке
параметров может быть указан нуль, один или более параметров любых типов. Один из
инициализирующих конструкторов, имеющий пустой список параметров, имеет
специальное наименование – пустой конструктор. Пустой конструктор также
инициализирует состояние экземпляра класса, используя для этого предопределенные
значения (в соответствии с требованиями задачи).
Копирующий конструктор инициализирует состояние класса значением
другого экземпляра этого класса (создает копию существующего экземпляра класса). В
списке параметров указывается единственный параметр, имеющий тип «ссылка на
экземпляр класса».
По тому, кто определяет конструкторы, последние делятся на конструкторы
по умолчанию (не требуют какого-либо упоминания в определении класса) и явно
определенные программистом.
Конструкторы по умолчанию: только пустой и копирующий. Пустой
конструктор по умолчанию не инициализирует состояние экземпляра класса.
Копирующий конструктор по умолчанию при инициализации осуществляет побайтное
копирование состояния указанного существующего экземпляра класса.
Конструкторы, определенные программистом: любые. Они осуществляют
инициализацию состояния экземпляра класса в соответствии с логикой, определенной в
конструкторе. Если программист определяет свои конструкторы, будут использоваться
именно они. Если есть хотя бы один инициализирующий конструктор, определенный
программистом, пустой конструктор по умолчанию не используется (даже если
программист не определил собственный пустой конструктор).
Прототип конструктора не имеет типа возвращаемого значения (конструктор
ничего не возвращает); имя конструктора совпадает с именем класса; в классе может быть
определено несколько конструкторов.
Деструктор служит для разрушения экземпляра класса. Опять же, память,
занятая экземплярами класса, освобождается в соответствии с используемыми
средствами языка. Локальный объект уничтожается, когда осуществляется выход за
пределы области видимости для этого объекта. Динамический объект уничтожается при
выполнении оператора delete. Временный объект уничтожается по окончании
вычислений, в которых он используется. В момент уничтожения объектов (перед
уничтожением) для них вызывается деструктор.
В классе может быть определен только один деструктор. Также существует
деструктор по умолчанию или явно определенный программистом. Деструктор по
умолчанию не выполняет никаких действий. Деструктор, определенный
программистом, выполняет действия, указанные в его определении.
Прототип деструктора также не имеет типа возвращаемого значения; имя
деструктора также совпадает с именем класса, но начинается символом ~.
Тип метода
Прототип
Примечания
Инициализирует состояние
Пустой конструктор
имя_класса();
предопределенными
значениями
Тип – любой; инициализирует
Инициализирующие
имя_класса(тип
состояние значениями,
конструкторы
параметр, ...);
заданными в списке
аргументов
Инициализирует состояние
значением указанного в
списке аргументов
Копирующий
имя_класса(const
экземпляра данного класса;
конструктор
имя_класса &
модификатор const указывает,
параметр);
что для инициализации
экземпляра класса можно
использовать константы
Деструктор
~ имя_класса ();
Вопрос № 2: Разработка класса в ООП: определение и реализация класса. Члены
класса. Уровни видимости. Inline функции.
Прежде всего, необходимо привести словесное описание разрабатываемого класса. При
разработке класса нужно представить определение класса, которое включает в себя:

определение имени класса (определяет новый тип; абстракция, с
которой будем иметь дело);

определение состояния класса (состав, типы и имена полей в
классе, предназначенных для хранения информации, а также уровни их защиты);
данные, определяющие состояние класса, получили название членов-данных
класса;

определение методов класса (определение прототипов функций,
которые обеспечат необходимую обработку информации). На этом этапе
приводится словесное описание того, что мы хотим получить от класса, не
указывая, как мы этого добьемся.
Class имя_класса{
struct имя_класса{
уровень_видимости:
уровень_видимости:
описания_полей_класса
описания_полей_класса
прототипы_функцийпрототипы_функцийметодов_класса
методов_класса
уровень_видимости: . . .};
уровень_видимости:
. . .};
Уровень_видимости задается одним из трех ключевых слов:

Private – определяет закрытую
часть класса, не доступную извне класса;

protected – пока для нас
аналогичен private; различия между ними
проявляются при использовании наследования;

public – определяет открытую
часть класса, видимую и доступную извне класса.
Реализация класса предполагает разработку и написание на языке всех
функций – методов класса. Каждая функция должна быть определена (и только один
раз). Определение функции имеет обычный вид:
тип_результата имя_функции (тип пар1, …) {тело_функции}
Функцию можно определить со спецификатором inline. Такие функции
называются встроенными:
inline тип_результата имя_функции (тип пар1, …)
{тело_функции}
Спецификатор inline указывает компилятору, что он должен пытаться каждый
раз генерировать в месте вызова код, соответствующий указанной функции, а не
создавать отдельно (один раз) код функции и затем вызывать его, используя обычный
механизм вызова. Отличия в использовании обычных и встроенных функций.
тип_результата имя_класса::имя_функции (тип пар1, …)
{ тело_функции }
Определения функций могут быть размещены вне класса или включены в
определение класса; в последнем случае получаем inline-функции
Вопрос №4: Методы класса: классификация методов, доступ к членам класса,
неявный параметер this. Определение и реализация методов.Использование методов
для экземпляров класса
Методы класса можно классифицировать по двум независимым критериям –
по функциональному назначению и по их отношению к классу.
По функциональному назначению методы класса делятся на следующие
категории:

конструкторы – предназначены для инициализации состояния
экземпляров класса при их создании;

деструкторы – предназначены для выполнения каких-то
дополнительных действий в момент уничтожения экземпляров класса;

селекторы – предназначены для обработки состояния класса без
его изменения;

модификаторы – предназначены для изменения состояния
класса;

итераторы – предназначены для организации
последовательного доступа к элементам данных, определяющих состояние
некоторого (одного) экземпляра класса.
По отношению к классу методы делятся на следующие две категории:

функция-член класса – функция, принадлежащая самому классу и
не существующая вне класса; прототипы функций-членов класса включены в
определение класса;

функция-друг класса – внешняя по отношению к классу функция,
которая может существовать вне класса, но имеет доступ к закрытой (и
защищенной) части класса. Прототип функции-друга класса также включается в
определение класса, но начинается специальным ключевым словом friend.
Конструкторы и деструкторы класса могут быть реализованы только
функциями-членами класса и имеют специальный синтаксис. Другие методы класса
имеют обычный синтаксис функций языка С++ и могут быть реализованы и функциямичленами, и функциями-друзьями класса. Мы пока ограничимся рассмотрением только
функций-членов класса.
Пусть есть два экземпляра класса Rational – ob1 и ob2. Чтобы
сложить их значения, надо одному из них (например, ob1) послать сообщение: «сложи
себя (свое состояние) с экземпляром ob2». На языке это выглядит так:
ob1.add(ob2);
При вызове функции add() на стеке параметров выделяется область,
относящаяся к данной функции. В этой области размещаются:

неявный параметр this, который указывает на адресата метода
(т.е. на операнд ob1),
Использование модификатора const – методы-селекторы класса
Модификатор const может использоваться для объявления функцийчленов класса (селекторов), которые не будут изменять состояние класса и
сообщают об этом компилятору. Это дает возможность использовать такие
функции для экземпляров класса, объявленных как константы, например:
Если реализация метода приводится вне определения класса, тогда модификатор
const должен появиться в двух местах: в прототипе функции-члена класса и в
определении функции:
Вопрос № 5 : Перегрузка функций: правила перегрузки, выбор функции.
Перезрузка операций: правила перегрузки, перегрузка бинарных и унарных
операций. Использование перегруженных функций и операций.
Перегрузка функций и операторов. Правила выбора перегруженной
функции. Функции – члены и друзья класса. Что выбирать – друзей или членов при
перегрузке операторов. Правила перегрузки операторов преобразования типа.
Перегруженные функции – это функции с одним и тем же именем, но
имеющие разные списки параметров. Параметры могут отличаться типами и/или
количеством. Тип возвращаемого функцией значения во внимание не принимается.
Примеры: void f(int);void f(char);void f(long);void
f(float, int);void f(int, int, int);
Важно! перегружаемые функции не должны иметь совпадающие списки
параметров (в том числе и при использовании параметров по умолчанию).
Перегрузка операторов
Правила перегрузки операторов: можно перегрузить почти все унарные и
бинарные операторы (за редким исключением).
Составное имя функции – operator знак_операции.
Приоритет оператора, правило ассоциативности и количество операндов
изменить нельзя!
Унарные операторы перегружаются аналогично, за исключением ++ и - (они могут иметь префиксную и постфиксную формы записи):
Префиксная ++: Rational operator ++();
Постфиксная ++: Rational operator ++(int);
Использование перегруженных операторов:
Rational a(1,5), b(2,7), c;
c = a + b; // классическая запись: c = a.operator +(b);
a++;
//
классическая запись: a.operator ++(int);
++a;
//
классическая запись: a.operator ++();
Если определены несколько функций с одинаковым именем и разными
списками параметров (перегруженные функции) и в программе встречается вызов
функции, компилятор должен выбрать одну из перегруженных функций.
Существует определенный алгоритм выбора функций, в соответствии с которым
выбирается функция, наилучшим образом соответствующая вызову. Если не будет
установлено соответствие ни одной из перегруженных функций или будет
установлено неоднозначное соответствие, на этапе компиляции генерируется
сообщение об ошибке.
Правила сравнения:
1. Точные совпадения;
2. Расширения (преобразования, увеличивающие только длину объекта, на
затрагивая его значения, например, char в int);
3. Стандартные преобразования (могут привести к искажению значения объекта,
например, long в int, так как длина int может быть меньше, чем длина
long, или float а int);
4. Преобразования, требующие временные переменные;
5. Преобразования, определенные пользователем.
Преобразования, требующие временные переменные: параметр объявлен
как ссылка, а аргумент требует преобразования (например, преобразование из
float в int&) или задан выражением, значение которого прежде всего должно
быть вычислено.
Вопрос № 7: Преобразования типа: назначение, использование. Правила
преобразования типа. Возможные проблемы.
Возможны два способа преобразования типа для вновь создаваемого
класса: преобразование из уже существующего типа в новый и преобразование
нового разрабатываемого типа в уже существующий.
Преобразование из существующего типа в новый выполняется с
помощью одноаргументного конструктора.
Пример для класса Rational:
Rational x = Rational(23);// явный вызов
Rational y = 23;
// неявный вызов
Возможны любые использования:
Rational a(1,2), b(1), c;
c = a + b;
c = a + 1; // эквивалентно c = a + Rational(1);
c = 2 + a; // эквивалентно c = Rational(2) + a;
Преобразование из нового типа в существующий выполняется с
помощью перегрузки оператора преобразования типа.
Оператор преобразования типа обязательно перегружается как функциячлен класса.
Прототип:
operator имя_типа ();
Реализация:
имя_класса::operator имя_типа() {
... }
Использование: неявно при вычислении выражений или явно с
помощью обычного оператора преобразования типа: имя_типа(выражение).
Пример для класса Rational:
class Rational{
private: int num, den; . . .
public: ...operator float() { return (float) num / den; }
...
};
Использование:
Rational x(3,2);
float f = x;
Еще пример использования перегруженного преобразования типа – для
потока вывода:
while(cin >> n)
// здесь используется преобразование
типа ostream к типу
// void *
cout << n;
Возможные неприятности:
Если в классе Rational есть одноаргументный конструктор
(преобразование int в Rational) и в нем будет перегружен оператор
преобразования типа для int (преобразование Rational в int), тогда
конструкция:
Rational a(1,2); ... a + 1 ...
вызовет сообщение об ошибке: два преобразования типа, определенные
пользователем; что выбрать: int + int или Rational + Rational?
Вопрос № 6 : друзья класса: их назначение, области применения. Определение и
использование функции – друга класса. Различия между членами и друзьями класса.
Функции и перегруженные операции – члены и друзья класса.
Назначение методов – создание интерфейса между внешним миром и закрытой
(защищенной) частями класса. Еще один способ получить доступ к закрытой части класса –
использование внешних функций, объявленных как друг класса. Функции-друзья, как и
члены, являются интерфейсом класса.
Функция становится другом после ее объявления в классе с использованием
спецификатора friend, например:
Другом класса могут быть: глобальная функция (см. примеры, приведенные выше);
функция-член другого класса; все функции-члены другого класса, т.е. сам класс (при этом
функции-друзья одного класса не становятся автоматически друзьями другого класса). Почти
любой метод может быть сделан другом. Исключения: конструкторы, деструктор, кое-что
еще.
Расположение объявления friend в определении класса (в открытой, закрытой
или защищенной частях класса) роли не играет – функция-друг класса является внешней по
отношению к этому классу. По этой же причине функция-друг класса, не являясь методом
класса, не имеет адресата и, следовательно, не имеет указателя this (все аргументы в
функцию передаются через список параметров).
Перегруженные операторы – друзья класса:
Бинарный оператор
Унарный оператор (префиксный и
постфиксный)
Объявление
friend тип operator знак_оп(op1, friend тип operator знак_оп(op1)
op2)
friend тип operator знак_оп(op1,
int)
Реализация
тип operator знак_оп(тип op1,
тип operator знак_оп(тип op1) {
тип op2)
... }
{ ... }
тип operator знак_оп(тип op1,
int) { ... }
Использование
op1 знак_оп op2
знак_оп op1
эквивалентно: operator знак_оп(op1,
эквивалентно: operator знак_оп(op1)
op2)
op1 знак_оп
эквивалентно: operator знак_оп(op1,
int)
Любой метод, представляющий интерфейс класса, за некоторыми (небольшими)
исключениями (к ним относятся конструкторы, деструктор и кое-что еще), может быть
реализован и функцией-членом, и функцией-другом класса.
Общее: – доступ к закрытой части класса, – хотя бы один аргумент – экземпляр класса
Различие:
Член класса
Друг класса
- из n параметров один (первый)
- все n параметров в списке параметров
параметр неявный, остальные – в
списке параметров
- неявный параметр – адресат
- все параметры равноправны; адресата
сообщения; доступен через this
сообщения нет; this не определено
- адресат сообщения (первый аргумент) - порядок и типы аргументов
– обязательно экземпляр класса
определяются прототипом функции
Rational x(1,3);
Rational x(1,3);
x + 1 - все в порядке
x + 1 - все в порядке
1 + x - ошибка!
1 + x - все в порядке
Функции-члены класса:
- конструкторы, деструкторы, виртуальные функции;
- операции, требующие в качестве операндов основных типов lvalue (например, =, +=, ++
и т.д.)
- операции, изменяющие состояние объекта
Функции-друзья класса:
- операции, требующие неявного преобразования операндов (например, +, - и т.д.)
- операции, первый операнд которых не соответствует типу экземпляра класса (например,
<< и >>).
При прочих равных условиях лучше выбирать функции-члены класса.
Вопрос № 8: классы, использующие свободную память : определение и реализация,
использование экземпляров класса, возникающие проблемы. Копирующий конструктор и
деструктор, перегрузка операции присваивания: определение и использование.
При решении задач часто приходится сталкиваться с ситуацией, когда состояние
класса определяется как массив элементов, конкретное количество которых оценить сложно. В
таких ситуациях состояние класса определяется как указатель, а необходимый объем памяти
выделяется во время выполнения программы. Пример такого класса: функция, заданная
таблично. Состояние класса включает в себя информацию о размере таблицы (количество
узлов в задании функции) и массивы для хранения значений аргумента (х) и соответствующих
значений функции (y). Массивы определяются через соответствующие указатели, размер
таблицы определяет требуемый объем памяти.
Возникает вопрос: кто будет выделять память под динамическую часть состояния
экземпляра класса? – Ответ очевиден: конструктор.
Очевидно, что выделенную память после ее использования необходимо вернуть. Эта
операция выполняется обычно тогда, когда разрушается соответствующий экземпляр
класса. Как было сказано выше, при разрушении экземпляра класса работает деструктор
класса. Следовательно, в определение класса должен быть включен и деструктор.
Перегруженный оператор присваивания (объявление в определении класса;
функция-член класса):
. . .
Function& operator =(const Function&);
. . .
Перегруженный оператор присваивания должен:
- освободить память, занимаемую экземпляром класса – адресатом оператора
присваивания (указанного слева от присваивания),
- выделить (если это необходимо) новую память для нового значения экземпляра класса –
адресата,
- скопировать в нее значение экземпляра класса, указанного справа от присваивания, если
они есть (параметр оператора присваивания),
- проверить возможность использования присваивания типа x = x.
Реализация:
Function& Function::operator =(const Function &f)
{
if(this != &f){
// проверка ситуации x = x
delete [] xPtr;
delete [] yPtr;
xPtr = yPtr = NULL;
if((size = f.size) != 0){
// проверка ситуации,
когда у объекта f нет
//
памяти
xPtr = new float[size];
yPtr = new float[size];
for(int i = 0; i < size; i++){
xPtr[i] = f.xPtr[i];
yPtr[i] = f.yPtr[i];
}}}return *this;}
Если в программе есть такая функция и выполняется действие f2 =
myfunc(f1), тогда:
- при вызове функции, так как аргументы функции передаются в нее по значению,
значение f1 копируется в стек (работает копирующий конструктор);
- когда функция завершает свою работу и выполняет оператор return, значение tmp
копируется для замещения вызова функции (опять работает копирующий конструктор);
- параметры функции и объявленная в ней временная переменная tmp разрушаются
(работает деструктор);
и, наконец, работает перегруженный оператор присваивания.
Вопрос № 9: типы отношений между классами. Контейнерные классы:
определение , видимость членов класса. Реализация и вызов конструкторов и
деструкторов вложенных классов. Реализация и спользование методов.
Между классами существует два типа отношений, свойственные
абстракциям данных: отношение части и отношение разновидности.
Отношение части определяет агрегатный тип, состоящий из экземпляров
других типов. Если между двумя классами существует отношение части, тогда
говорят, что класс 1 ЕСТЬ_ЧАСТЬ класса 2, или класс 2 СОСТОИТ_ИЗ класса 1.
Пример отношения части: комната ЕСТЬ_ЧАСТЬ квартиры, или лепесток
ЕСТЬ_ЧАСТЬ цветка, или точка ЕСТЬ_ЧАСТЬ окружности. На языке С++
отношение части реализуется с помощью контейнерных классов.
Отношение разновидности определяет один тип как некую
разновидность другого. Если между двумя классами существует отношение
разновидности, тогда говорят, что класс 1 ЕСТЬ_НЕК(оторая разновидность) класса
2. Например, роза ЕСТЬ_НЕК(оторый) цветок. На языке С++ отношение
разновидности реализуется с помощью производных классов.
Удобно представлять отношения между классами графически. Будем
обозначать отношение части двойной стрелкой, направленной от части к целому
(рис. 5.1, а), а отношение разновидности – простой стрелкой, направленной от более
конкретного (уточняющего) класса к более общему (рис. 5.1, b; обычно стрелка
определяет, какой из двух классов является большим или более общим).
Определение контейнерного класса представляет собой обычное
определение класса. Отличие от обычных классов заключается в том, что состояние
контейнерного класса включает в себя экземпляры других классов (а не только
стандартных типов данных).
Реализация методов контейнерного класса не отличается от реализации
методов обычных классов, рассмотренных ранее. Здесь только надо учитывать, что в
контейнерном классе используются экземпляры других классов, и это
использование представляет собой обычное внешнее использование класса. Это
значит, что состояние некоторого класса, являющегося частью контейнерного,
доступно только через собственный интерфейс этого класса.
Учитывая это, рассмотрим реализацию методов контейнерного класса.
Расстояние между центрами двух окружностей – это, по сути, расстояние
между двумя точками. В классе Point есть соответствующий метод.
Следовательно, в классе Circle нужно просто использовать его для вычисления
расстояния между точкой center, представляющей центр окружности-адресата, и
точкой c.center, представляющей центр окружности, заданной аргументом c.
Следует особо остановиться на реализации конструкторов контейнерных
классов. Дело в том, что их реализация отличается от реализации
конструкторов обычных классов.
Рассмотрим инициализирующий конструктор класса Point.
Point::Point(int x0, int y0)
{
x = x0;
y = y0; }
По аналогии с ним, можно было бы реализовать один из
инициализирующих конструктор контейнерного класса Circle следующим
образом:
Circle::Circle(Point p, int r)
{
center = p;
rad = r;
}
Вопрос № 10: Производные классы: простое наследование, основные понятия и
определения. Правила определения производного класса, типы наследования, видимость
членов класса. Реализация и использование коснтрукторов и деструкторов базового и
производного класса. Использование экземпляров базового и производного классов.
Указатели на базовый и производные классы.
Отношение разновидности между классами в С++ реализуется с помощью
производных классов и поддерживается механизмом наследования, являющимся свойством
объектно-ориентированных языков программирования. При этом более общий класс
(суперкласс) в С++ называется базовым классом, а более конкретный (подкласс) –
производным. С точки зрения отношения разновидности (и механизма наследования)
производный класс есть базовый, обладающий дополнительными возможностями.
Производный класс наследует все свойства базового класса и добавляет что-то свое (рис. 6.1).
Отсюда, базовый класс – более абстрактное понятие (шире, чем производный). Производный
класс – более конкретное понятие (больше – по объему информации, чем базовый класс).
В производном классе имена методов (и членов – данных) могут совпадать с
именами из базового класса.
Отношение между базовым и производным классами можно
сформулировать следующим образом:
Производный класс ЕСТЬ базовый ПЛЮС ОБЯЗАТЕЛЬНО что-то свое.
В соответствии с приведенной формулировкой, производный класс
наследует от базового все его свойства (и состояние, и методы) и расширяет и уточняет
их своими свойствами.
6.2. Простое наследование: правила определения производного класса
Правила объявления производных классов. Две проблемы: могут ли (и как)
методы производного класса обращаться к членам базового класса (доступность изнутри
производного класса) и как члены базового класса, унаследованные производным
классом, могут быть доступны извне производного класса.
Первая проблема решается уровнем видимости членов в базовом классе:
class B{ private:
// закрытая часть класса; не доступна никому, кроме методов класса (в том числе
// не доступна и из производного класса)
protected:
// защищенная часть класса; доступна из методов базового и производного класса
// (но не доступна извне класса)
public:
// открытая часть класса; доступна везде };
Вторая проблема решается способом наследования:
class D: тип_наследования B{ . . . };
тип_наследования: одно из private, protected, public.
Тип наследования не смягчает, но усиливает защиту данных: члены класса,
имевшие менее защищенный уровень видимости в базовом классе, в производном классе
приобретают уровень видимости, определяемый типом наследования.
Уровень видимости в
Тип наследования
базовом классе
private
protected
public
protected
private
protected
protected
public
private
protected
public
определении класса приводятся только те его члены, которые отличают
производный класс от базового. В нашем случае приводятся радиус окружности, собственные
методы определения пересечения окружностей и вывода и, конечно же, необходимые
конструкторы.
Теперь рассмотрим реализацию методов.
Начнем с конструкторов.
Проблема, возникающая здесь, заключается в следующем. Конструктор должен
инициализировать состояние экземпляра класса. Доступ к собственной части состояния
проблем не вызывает. Но как быть с частью состояния, унаследованной от базового класса?
Если состояние базового класса включено в область protected, оно доступно из
производного класса, и тогда конструктор может быть реализован следующим образом:
Circle::Circle(){x = 0; y = 0; rad = 0;}// пустой конструктор
Если же состояние базового класса включено в private область, оно не доступно
из производного класса, но также должно быть инициализировано. Состояние класса
инициализируют конструкторы класса. Следовательно, необходимо иметь возможность из
конструктора производного класса вызвать конструктор базового класса. Эта возможность
реализуется следующим образом:
<имя_производного_класса>::<имя_производного_класса>(<параметры>):
<имя_базового_класса>(<аргументы>) // вызов конструктора базового
класса
{< дополнительные операции >}
Инициализация собственного состояния может быть реализована в
соответствии с правилами контейнерных классов, тогда конструктор производного
класса может выглядеть и так:
<имя_производного_класса>::<имя_производного_класса>(<параметры>):
<имя_базового_класса>(<аргументы>),
<имя_члена_данного1_класса>(<значение>), ...
{< дополнительные операции>}
И опять же действует правило по умолчанию: если вызов конструктора базового
класса отсутствует, по умолчанию вызывается пустой конструктор базового класса.
Вопрос № 11: вызов методов класса по указателю. Понятие статического и
динамического связывания. Виртуальные функции, их назначение, реализауция ,
использование. Виртуальные деструкторы. Использование перегруженной
операции выводав поток. Абстрактные классы их назначение, определение и
использование.
Представление экземпляров классов в памяти – производный класс есть
базовый плюс что-то свое:
Объявления указателей:
B *bPtr; D *dPtr;
Указатели на тип определяют начальный адрес области памяти и ее
размер, поэтому bPtr адресует только состояние, определяемое базовым классом, а
dPtr – состояние, определяемое производным классом.
Определение значений указателей традиционное:
bPtr = &obj1; dPtr = &obj2; B *b1Ptr, *b2Ptr = bPtr; D *d1Ptr, *d2Ptr = dPtr;
b1Ptr = new B; d1Ptr = new D;
Можно указателю на базовый класс присвоить значение указателя на
производный класс. В этом случае из производного класса будет использоваться
только его часть, относящаяся к базовому классу:
bPtr = &obj2; b1Ptr = d1Ptr; b2Ptr = new D;
Обратное присваивание невозможно; записи вида d1Ptr = b1Ptr; вызовут
сообщения об ошибках на этапе компиляции (присваивания указателей разного
типа).
6.4. Вызов методов по указателю на класс
Рассмотрим определение базового и производного классов:
Базовый класс
Производный класс
class B{ . . .
class D: public B{ . . .
public: . . .
public: . . .
void f();};
void f();};
Пусть определены следующие объекты базового и производного классов:
D obj2, *dPtr = &obj2;
B obj1, *b1Ptr = &obj1, *b2Ptr = &obj2;
Имеем следующее представление (рис. 6-3):
Выполняем методы:
obj1.f()
Вызывается метод f()базового класса для
экземпляра базового класса obj1.
b1Ptr->f()
Вызывается метод f()базового класса по
указателю базового класса b1Ptr на экземпляр базового класса obj1.
obj2.f()
Вызывается метод f()производного класса
для экземпляра производного класса класса obj2.
dPtr->f()
Вызывается метод f()производного класса по
указателю производного класса dPtr на экземпляр производного класса obj2.
b2Ptr->f()
Какой метод f()- базового или производного
класса - вызывается здесь по указателю b2Ptr на экземпляр obj2 производного
класса?
Вопрос № 12: Понятие и назначение итераторов. Проектирование, реализация и
использование итератора(на примере динамического списка).
Класс-итератор – это специальный класс, с помощью которого можно получить последовательный
доступ к элементам некоторого экземпляра контейнерного класса.
Состояние:
1.
Ссылка на экземпляр Итерируемого класса
2.
Текущая позиция.
Особенности:
1.
Класс-итератор должен видеть закрытое состояние контейнерного класса.
2.
Он должен иметь собственное состояние, чтобы следить за тем, какие элементы в составе
экземпляра контейнерного класса получены, а какие нет.
3.
Он должен иметь собственные методы, с помощью которых он выполняет свою задачу.
4.
Он должен отслеживать ситуацию, когда все элементы состояния обработаны, и сообщать об
этом.
struct Item {Point*info; Item*next;};
class Queue {private: Item*first, *last; public: Queue():first(NULL),last(NULL){} int put(Point*); Point* get();
friend ostream& os, const Queue&); friend class QueueIt;};
class QueueIt{private: Queue* ptr; Item*cur; public:QueueIt(Queue&p):ptr(&p),cur(p.first){} Point*operator()();
void init(){cur=ptr->first;} Point*QueueIt::operator()(){Point*res=NULL; if(cur){res=cur->info; cur=cur->next;}else
cur=ptr->first; return res;}
Использование:
Void main(){
Queue q; Point *p; …
q.put(*p);
q.put(*p); …
float s=0;
QueueIt qit(q);
While(p=qit()) s+=p->area();
Пример использования динамического связывания: список
Наиболее часто динамическое связывание используется с контейнерными
классами, содержащими указатель на базовый класс; в такие контейнерные классы можно
включать информацию, относящуюся и к базовому, и к любым производным классам.
Рассмотрим пример – список, содержащий и точки, и окружности.
struct Item{
Point *info;
Item *next;
// конструктор
Item():info(NULL), next(NULL){}
Item(Point *p):info(p), next(NULL){}
};
class List{
private:
Item *head;
public:
List():head(NULL){}
void insert(Point *p){p->next = head; head = p;}
void print(); };
void List::print()
{
for(Item *cur = head; cur; cur = cur->next){
cur->info->print();
cout << endl;
}}
Использование класса:
List mylist;
Point *p = new Point(1,2);
mylist.insert(p);
p = new Cicle(1,2,1);
mylist.insert(p);
mylist.print();
получим:
Circle with center in Point (1, 2) and radius 1
Point (1, 2)
Виртуальные деструкторы
Если и базовый, и производный классы используют динамическую память, нужно
определять виртуальный деструктор. Деструктор для каждого класса имеет уникальное имя.
Для задания виртуального деструктора необходимо в базовом классе определить деструктор,
указав перед ним слово virtual. В результате деструкторы данного класса и всех
производных от него классов приобретают свойство виртуальности.
Вопрос № 13: Множественное наследование: определение, реализация,
использование экземпляров производного и базовых классов. Возможные
неоднозначности, их устранение. Виртуальные классы, их назначение.
Определение и реализация производных классов, использующих виртуальные
базовые классы. Вызов конструкторов виртуального класса.
Механизм множественного наследования позволяет создать
производный класс на основе нескольких базовых классов. Все, что относится к
простому наследованию, все также действует во множественном наследовании.
Наследование - это процесс, посредством которого один объект может
приобретать свойства другого (точнее, объект может наследовать свойства другого
объекта и добавлять к ним черты, характерные только для него). Например, роза или
ромашка - это разные разновидности некоторого общего (родительского) класса,
называемого цветком. В свою очередь, цветок - это разновидность еще более общего
класса растение, и т.д. В каждом случае порожденный класс наследует все связанные
с родителем качества и добавляет к ним свои собственные определяющие
характеристики.
B1 B2
D
class B1{… f()}
class B2{… f()}
class D: public B1, public B2 {
D(…):B1(…),B2 (…){…}}
f(){B1::f();B2::f();}
};
B1*p;
p=new D(…);
p->g1()
B2*p1;
p1=p; //error
p1=new D(…);
p->g2(…); //error
Виртуальный базовый класс
class B{…}
class D1:virtual public B{…D1(…):B(…){…}…};
class D2:virtual public B{…D2(…):B(…){…}…};
Виртуальный базовый класс определяет структуру
множественного наследования. Виртуальный метод используется для
динамического связывания.
class D12:public D1, public D2{}
class B{private: int x,y; public: B (int x0=0,int
y0=0):x(x0),y(y0){}…};
class D1:virtual public B{private: int z, public: D1(int
x0=0;int y0=0,int z0=0): B(x0,y0),z(z0){}…};
class D2:virtual public B{private: int w; public: D2(int
x=0,int y=0,int w=0):B(x,y)w(w){}…};
class D12:public D1,public D2{public: D12(int x=0,int y=0int
z=0,int w=0):D1(x,y,z),B(x,y){}…};
Вопрос № 14: Файловый потоковый ввод-вывод: иерархия и назначение классов. Основные
методы для организации потокового ввода-вывода. Определение состояния потока.
Организация работы с файлами: классы, основные методы. Реализация произвольного
доступа к файлам.
ios
istream ostream
ifstream iostream ofstream
fstream
istream operator >> (осуществляет форматированный ввод, может не вводить пробелы)
getline (куда, максимум сколько, откуда?)
get (вывод одного символа)
read (куда, сколько)
ostream operator << (осуществляет форматированный вывод)
put (выводит текстовю информацию в поток)
write (откуда, сколько)
Работа с файлами
-файл должен быть открыт
-обработка (чтение, запись)
-закрыть
открытие файла: либо конструктором, либо open
-ifstream открытие для чтения
-ofstream открытие для записи
-fstream открытие для чтения/записи
ifstream(ofstream,fstream) имя (имя файла, режим)
режим: опция | доп. условия
ios:: in - если файл не существует - сообщение об ощибке
ios:: out - если файл не существует он создается
ios:: out - если файл существует он пересоздается
ios:: app - если файл не существует он создается
ios:: app - если файл существует инф. записывается в конец файла
ios:: binary точная копия состояния ОП без преобразования
fstream f(myfile.dat;ios::in|ios::out|ios::busy)
fstream f;
f.open (имя файла, режим);
f.close();
fstream - произвольный доступ к содержимому файла (есть указатель чтения/записи) после
чтения указатель перемещается на некоторое число байтов, если открыть режим app, то при
read указатель в начале файла, а при write указатель перемещается в конец файла Есть функции
- установить позицию указателя (seek) и прочитать позицию (tell)
f.seekp(в режиме чтения) f.seekq(в режиме записи)
f.seekp (смещение, начло отсчета) смещение -длинное целое со знаком
ios::beg -начало отсчета
ios::end - конец
ios:: cur
clear - очистить состояние файла
ifstream if ("myfile");
if (!if){ cur<<"file not exist\n"; return 1;}
fseekq(0,ios::end);
streampos =if.tellq();
cout<<l<<endl;
if.close;
Билет 14. Шаблоны: назначение и типы шаблонов. Шаблоны функций:
определение, реализация. Использование функций шаблона.
Параметризованные классы: определение и реализация. Использование
экземпляров класса шаблона. Использование механизма наследования в
шаблонах классов.
Шаблоны дают возможность определять при помощи одного фрагмента кода целый набор взаимосвязанных
функций (перегруженных), называемых шаблонными ф-ями, или наборсвязанных классов называемых шаблонными
классами.
Шаблоны:
1.
Функций (параметризованные ф-ии)
2.
Классов (параметризованные классы)
Шаблон ф-ии: template <class T> void swap (T&p1, T&p2){
T tmp=p1; p1=p2; p2=tmp;}
Использование ф-ий:
int i1, i2; swap(i1, i2); //компиллятор сгенерирует ф-ю, подставляя вместо T int
double d1, d2; swap(d1, d2); //компиллятор сгенерирует ф-ю, подставляя вместо T double
swap (i1, d1)// ошибка, несоответствующие типы (нельзя приравнять int кdouble).
В случае множественной параметризации: template <class A, class B>…
Шаблон ф-ии может быть перегружен как другими шаблонами с другими параметрами, так и если ввести не
шаблонную функцию с тем же именем но другим набором параметров.
Применение шаблонов классов увеличивает возможности повторного использования кода, когда мклассы для
конкретного типа данныхмогут создаваться на основе родовой версии класса.
Пример: стек на статическом векторе.
Template <classT> class Stack {static const int sz=100; T arr[sz]; int top; public: Stack(); int put(const T&); T get();};
Template <classT> int Stack <T>::put(const T&a){arr[top++]=a; return 0;}
Stack<T>::Stack():top(0){}
Void main {Stack <int> st; st.put(12); cout<<st.get()<<endl;…}
Реализация шаблонов ф-й и классов должна находиться в.h-файле. Также существуют и т.н. нетиповые параметры.
Напр: template<class T, int elements> использ: Stack<float,100>a;
C точки зрения правил C++ два класса, сгенерированныеиз одного шаблона, не связаны друг с другом никакими
отношениями. Например, если имеется class Shape{…}; class Circle:public Shape{…};, то нельзя обращаться со Stack <Circle> как
со Stack<Shape>.
Шаблоны и наследование являются механизмом построения новых типов из уже существующих и написания
полезного кода, использующего различные формы общности. Комбинация этих двух механизмов является основой для многих
полезных методов программирования. Все основные идеи наследования при этом остаются неизменными, что позволяет
построить иерархическую структурушаблонов, аналогичную иерархии классов.
struct Slink {Slink*next; Slink():next(NULL){} Slink (Slink*a):next (a){}}
template <classT> struct Tlink: public Slink {T info; Tlink (const T&s): info(s);};
Этот пример показывает, что создание производного шаблона класса от класса, не являющегося шаблоном,
является хорошим способом обеспечения общей реализации для набора шаблонов.
Download