Программирование_3семестр

advertisement
1.Объектно-ориентированный подход к программированию. Идеология разработки
программ в объектно-ориентированном программировании. Суть инкапсуляции,
наследования, полиморфизма.
Объектно-ориентированное программирование (ООП) - это метод программирования,
при использовании которого главными элементами программ являются объекты. В языках
программирования понятие объекта реализовано как совокупность свойств (структур
данных, характерных для данного объекта), методов их обработки (подпрограмм
изменения их свойств) и событий, на которые данный объект может реагировать и,
которые приводят, как правило, к изменению свойств объекта.
Другим фундаментальным понятием является класс. Класс предоставляет механизм для
создания объектов. Класс – это тип, который служит для определения переменных класса
- объектов (экземпляров) класса В классе отражены важнейшие концепции объектноориентированного программирования: инкапсуляция, наследование, полиморфизм.
1) Объединение данных и свойственных им процедур обработки в одном объекте,
называется инкапсуляцией и является одним из важнейших принципов
ООП(доступность объекта исключительно посредством его свойств и методов).
2) Наследование - это механизм получения нового класса на основе уже существующего.
Существующий класс может быть дополнен или изменен для создания нового класса.
Существующие классы называются базовыми, а новые – производными. Производный
класс наследует описание базового класса; затем он может быть изменен добавлением
новых членов, изменением существующих функций- членов и изменением прав доступа.
3)
Полиморфизм - присвоение
единого
имени
процедуре,
которая
передается вверх и вниз по иерархии объектов, с выполнением этой процедуры способом,
соответствующим каждому объекту в иерархии(возможность оперировать объектами, не
обладая точным знанием их типов).
Термин полиморфизм дословно означает "множество форм".
Применительно к языкам, говорят о полиморфизме, когда функции или операторы
в различных условиях проявляют себя по-разному.
Процедурный полиморфизм мы уже рассматривали – перегрузка функций. В этом
случае имя функции становится многозначным – ему соответствуют разные алгоритмы.
Еще вид процедурного полиморфизма – это перегрузка операций.
Другой вид полиморфизма связан с наследованием и реализуется с помощью
виртуальных функций.
Рассматривается указатель на базовый класс, который может содержать как адреса
объектов своего класса, так и объектов производных классов. Кроме того, механизм
виртуальных функций позволяет с помощью указателя с типом базового класса
обращаться к переопределенным методам в производных классах. Рассмотрим, как эту
возможность можно реализовать.
Проблема доступа к методам, переопределенным в производных классах, через указатель
на базовый класс решается в C++ посредством использования виртуальных функций.
Чтобы сделать некоторый нестатический метод виртуальным, надо в базовом классе
предварить его заголовок спецификатором virtual, метод
становится виртуальной
функцией. Если эту функцию переопределить в производном классе даже без
спецификатора virtual, в производном классе также создается виртуальная функция.
Сигнатуры функций должны различаться только их принадлежность разным классам, а
алгоритмы могут быть различными. Все сказанное похоже на механизм замещения.
Однако виртуальность этих функций (или полиморфизм) проявляется в том, что выбор
требуемой из множества определенных в иерархии классов виртуальных функций с одним
именем осуществляется не во время компиляции программы, а динамически, по
конкретному значению указателя базового типа, с помощью которого и вызывается
функция.
Какая функция будет вызываться зависит от типа указателя и типа того объекта, адрес
которого присвоен указателю.
Виртуальность функций проявляется только в том случае, если она вызывается через
указатель или ссылку на базовый класс.
Указатель на базовый класс может принимать конкретные значения.
Если значение указателя к моменту вызова функции есть адрес объекта базового класса,
вызывается вариант функции из базового класса.
Если этот указатель имеет значение адреса объект производного класса (фактически
указывает на данные базового класса в объекте производного класса), то вызывается
вариант функции из производного класса.
Рассмотрим вышесказанное на примере.
class A {
public:
virtual void F1 ( );
virtual int F2 ( char* ); };
class B : public A {
public:
virtual void F1 ( );
virtual int F2 ( char* );};
class C : public A {
public:
void F1 ( );
int F2 ( char* ); };
int main () {
A * ap = new A;
B * bp = new B;
C * cp = new C;
ap-> F1 ( );
// вызов функции базового класса А
ap = bp ;
ap-> F1 ( );
// вызов замещенной функции класса В
ap = cp;
ap -> F1 ( ) ;
// вызов замещенной функции класса С
return 0;
}
Вызов виртуальной функции через указатель на базовый класс позволяет в зависимости от
значения этого указателя (не от типа этого указателя, а от значения!) вызывать варианты
этой функции из разных классов.
2.Понятие класса как структурированного типа. Определение класса
Класс – это структурированный тип, состоящий из фиксированного набора
возможно разнотипных данных и совокупности функций для обработки этих
данных.
Таким образом класс в языке С++ рассматривается как естественное расширение
понятия структуры.
Определение класса дается с помощью конструкции, называемой спецификацией
класса:
ключ_класса имя_класса
// заголовок класса
{список_компонентов_класса};
// тело класса
Определение класса, как и структурного типа, всегда заканчивается ';',
ключ_ класса – одно из слов class, struct, union,
имя_класса
– правильный идентификатор,
список_ компонентов_класса – (или членов класса - members) определения и описания
типизированных данных (полей данных - data members) и принадлежащих классу функций
(методов класса – member functions).
3.Объектно-ориентированный подход к программированию. Определение класса.
Создание объектов статических и динамических. Инициализация. Обращение к
компонентным данным и функциям.
Объектно-ориентированное программирование (ООП) - это метод программирования,
при использовании которого главными элементами программ являются объекты. В языках
программирования понятие объекта реализовано как совокупность свойств (структур
данных, характерных для данного объекта), методов их обработки (подпрограмм
изменения их свойств) и событий, на которые данный объект может реагировать и,
которые приводят, как правило, к изменению свойств объекта.
Определение класса.
Класс – это структурированный тип, состоящий из фиксированного набора
возможно разнотипных данных и совокупности функций для обработки этих данных.
Таким образом класс в языке С++ рассматривается как естественное расширение
понятия структуры.
Определение класса дается с помощью конструкции, называемой спецификацией
класса:
ключ_класса имя_класса
// заголовок класса
{список_компонентов_класса};
// тело класса
Определение класса, как и структурного типа, всегда заканчивается ';',
ключ_ класса – одно из слов class, struct, union,
имя_класса
– правильный идентификатор,
список_ компонентов_класса – (или членов класса - members) определения и описания
типизированных данных (полей данных - data members) и принадлежащих классу функций
(методов класса – member functions).
Создание объектов статических и динамических. Инициализация. Обращение к
компонентным данным и функциям.
1)Определение статического объекта:
<имя класса> <имя объекта> ( параметры конструктора);
Пустой список параметров не допустим, если в классе конструктор с параметрами.
2)Определение динамического объекта:
<имя класса> * <имя указателя на объект> = new <имя класса> ( параметры
конструктора);
Как только объект определен появляется возможность обращаться к его
компонентам
1) с помощью квалифицированного имени:
имя_объекта. имя_ класса :: имя_компонента
имя_объекта. имя_класса :: имя_компонентной_функции(аргументы)
2) с помощью уточненного имени:
имя_объекта. имя_компонента
имя_объекта. имя_компонентной_функции(аргументы)
4.Внешнее и внутреннее определение компонентных функций. inline-функции.
При объявлении функции вне класса, внутри тела класса помещается только прототип или
описание функции
Все экземпляры (объекты) класса будут использовать один код функции, и компилятор
будет генерировать код для вызова функции при каждом обращении к функции.
При полном объявлении функции внутри класса функция по умолчанию считается
подставляемой, т.е. при каждом вызове этих функций их код “встраивается“ в точку
вызова. Это, так называемые, inline-функции. Вместо команд передачи управления
единственному экземпляру тела функции компилятор в каждое место вызова функции
помещает команды кода операторов тела функции.
Это может увеличить скорость выполнения программы, но увеличивает также размер кода
программы. Если метод (функция) большой по объему, не следует настраивать
компилятор на генерацию встроенного кода и следует объявить функцию вне класса,
используя квалифицированное имя функции.
Например.
class Men { char * name;
…
void setName ( char*); //-функция- член класса
… };
void Men:: setName (char*n)
{ name = n ;}
5.Статический компонент класса. Инициализация, обращение.
Каждый объект одного и того же класса имеет собственную копию полей данных.
То есть данные класса тиражируются при каждом определении объекта класса. Обычные
данные отличаются друг от друга тем какому объекту они принадлежат.
Иногда возникает необходимость объявить данные общие для всех создаваемых
объектов данного класса.
Например, в следующих случаях:
- если нужен счетчик создаваемых объектов данного класса,
- или для объектов, связанных в список, для работы со всеми объектами списка (например,
в функции просмотра списка) нужен указатель на начало списка – общий для всех
связанных объектов,
- может понадобиться указатель на последний элемент списка.
Такое поле данных должно быть определено в классе как статическое, то есть
иметь атрибут static.
Статический компонент класса не дублируется при создании новых объектов
класса. Каждый статический компонент класса существует в единственном экземпляре и
память на него выделяется при его инициализации и сохраняется до окончания действия
программы.
Инициализация статического компонента размещается в глобальной области после
определения класса следующим образом:
тип_ компонента имя_класса :: имя_компонента инициализатор.
С этого момента статический компонент класса становится доступным (если он открыт)
даже до объявления объектов данного класса по квалифицированному имени:
имя_ класса :: имя_ компонента.
После объявления объектов к статическому полю данных можно также обращаться
как к обычному полю данных, то есть с использованием операций выбора (. и ->).
При этом любые изменения (в любом объекте) статического элемента становятся
общими для всех объектов данного класса.
6.Статическая компонентная функция, назначение, вызов.
Если статический компонент класса имеет статус private или protected , то к нему
извне можно обращаться только через компонентные функции, при наличии уже
определенного объекта.
Возможность обращения к статическому элементу без имени конкретного объекта
(и до объявления объектов) дает статическая компонентная функция, определенная в
классе со спецификатором static.
Такая функция сохраняет все свойства обычных компонентных функций, но
дополнительно такую функцию можно вызвать следующим образом:
имя класса :: вызов статической функции;
То есть такие функции можно вызывать до определения конкретных объектов класса, и,
не используя конкретных объектов.
…
class goods1
// класс “ товар1”
{
// закрытые по умолчанию данные – члены класса
char name[40] ; // наименование товара
float price ;
// закупочная цена
static int percent;
// торговая наценка
public : // открытые члены класса компонентные функции:
void vvod ( )
// ввод данных о товаре
{ cin >> name; cin>> price; }
static void SetPer ( int newper) // статическая функция для
{ percent = newper;}
//изменения статического элемента
void vivod ( )
// вывод данных о товаре
{ cout <<”\n” << name ;
cout <<”: Розничная цена =”
<< price*(1.0+ goods::percent*0.01);
}
};
int goods1::percent =25 ;
//внешнее описание и инициализация статического компонента
/* т.к. percent – собственный (закрытый) компонент класса, то последующее обращение
к нему возможно только с использованием объектов и открытых компонентных функций
или используя общедоступную статическую компонентную функцию */
7.Статусы доступа к членам класса. Спецификаторы доступа.
Служебное слово struct определяет доступность компонентов класса. В любом месте
программы можно получить доступ к компонентам через уточненное имя и указатель.
Таким образом, не выполняется основной принцип абстракции данных инкапсуляция (сокрытие) данных внутри объекта. Для изменения видимости компонент в
определении класса можно использовать спецификаторы доступа:
1) Данные или метода класса, описанные после ключевого слова public, являются
открытыми (общедоступными). Это означает, что к ним можно обращаться, из функций
данного класса без формирования уточненного имени, из функций других классов, из
внешних функций программы с использованием уточненных имен. Обычно в классах
открывают функции общего назначения, чтобы с их помощью можно было бы посылать
сообщения объектам.
2) Данные и методы, описанные после ключевого слова private, являются закрытыми
(частными, приватными). Это означает, что к ним можно обращаться только из функций
данного класса (открытых или закрытых), из программы к ним обратиться нельзя. Обычно
поля данных класса закрывают, выполняется, так называемый, принцип инкапсуляции
данных внутри объекта. Иногда закрывают и функции внутреннего назначения.
3) Данные и методы, описанные после ключевого слова protected, являются
защищенными. Такие члены класса доступны только методам данного класса и методам
классов, производных от данного. Защищенные отличаются от закрытых членов класса,
если имеет место наследование.
Закрытые члены класса недоступны и в производных классах!
Ключевые слова public, private, protected могут встречаться в описании класса в
любом порядке и количестве.
8.Создание объектов (экземпляров класса) статических и динамических.
Инициализация. Обращение к компонентным данным и функциям.
1)Определение статического объекта:
<имя класса> <имя объекта> ( параметры конструктора);
Пустой список параметров не допустим, если в классе конструктор с параметрами.
2)Определение динамического объекта:
<имя класса> * <имя указателя на объект> = new <имя класса> ( параметры
конструктора);
Как только объект определен появляется возможность обращаться к его
компонентам
1) с помощью квалифицированного имени:
имя_объекта. имя_ класса :: имя_компонента
имя_объекта. имя_класса :: имя_компонентной_функции(аргументы)
2) с помощью уточненного имени:
имя_объекта. имя_компонента
имя_объекта. имя_компонентной_функции(аргументы)
9.Компонентные данные и компонентные функции. Статические компоненты класса
(данные и функции).
Как уже отмечалось, каждый вновь создаваемый объект класса имеет свою
собственную копию компонентных данных. Чтобы эти компоненты класса были в
единственном экземпляре и не тиражировались при определении каждого нового объекта
класса, они должны быть объявлены в классе как статические (static):
static имя_типа имя_статического_компонента_данных;
Как видим, статический компонент данных является частью класса, но не является
частью объекта этого класса. Все объекты класса, в котором были объявлены статические
компоненты данных, так и объекты его производных классов, теперь могут совместно
использовать эти общие для них компоненты.
Статические компоненты данных класса размещаются в памяти отдельно от его
нестатических компонентов, причем память статическим компонентам выделяется только
после их определения вне определения класса.
Для внешнего определения статических компонентных данных можно воспользоваться
следующими конструкциями:
имя_типа имя_класса::имя_статического_компонента_данных;
или
имя_типа имя_класса::имя_статического_компонента_данных = инициализатор;
или
имя_типа имя_класса::имя_статического_компонента_данных(инициализатор);
Эти конструкции должны быть размещены в глобальной области (global scope) после
определения класса.
Как видим, в отличие от обычных компонентных данных статические компоненты
класса необходимо еще дополнительно определять вне определения класса. Заметим, что
по умолчанию значением статического компонента данных встроенного типа является 0,
преобразованный в соответствующий тип.
Напомним , что конструкторы статических компонентных данных класса в единице
трансляции вызываются до вызова функции main() в порядке их определений. После
выхода из функции main() будут вызваны деструкторы для каждого такого объекта в
порядке, обратном вызовам их конструкторов.
Итак, статическому компоненту данных память выделяется только после его
определения вне класса, и лишь после этого он становится доступным. Статические
компоненты данных после определения можно использовать в программе еще до
определения объектов данного класса. По сути, статический компонент данных – это
просто глобальная переменная, область видимости которой ограничена классом, в
котором она и была объявлена. Основной смысл поддержки в С++ статических
компонентов данных класса состоит в том, что теперь отпадает необходимость в
использовании глобальных переменных. При этом не следует забывать о том, что
использование классов наряду с глобальными переменными почти всегда нарушает
принцип инкапсуляции. Заметим также, что в некоторых случаях класс используется
просто как область действия, в которую глобальные имена помещаются под видом
статических компонентов, чтобы не засорять глобальное пространство имен.
Для обращения к статическим компонентам данных используются следующие имена:
квалификация без указания имени объекта
имя_класса::имя_статического_компонента_данных
квалификация с указанием имени объекта
имя_объекта .имя_класса::имя_статического_компонента_данных
уточненное имя
имя_объекта .имя_статического_компонента_данных
Другой способ доступа к статическим компонентам данных предусматривает явное
использование указателя на объект класса:
либо с помощью оператора выбора члена –>
имя_указателя_на_объект_класса->имя_статического_компонента_данных
либо с помощью оператора разыменования * и оператора выбора члена.
(*имя_указателя_на_объект_класса) .имя_статического_компонента_данных
Так как на статические компоненты класса распространяются правила статуса доступа
(объявление статического компонента можно поместить как в закрытом, так и в открытом
разделах определения класса), то для обращения к ним извне, как правило, используются
общедоступные статические компонентные функции.
Формат объявления статической компонентной функции:
static имя_типа имя_статической_компонентной_функции
(список_формальных_параметров_функции);
Формат определения статической компонентной функции: static
имя_типа имя_статической_компонентной_функции
(список_формальных_параметров_функции) { тело_функции}
Формат внешнего определения статической компонентной функции:
имя_типаимя_класса::имя_статической_компонентной_функции
(список_формальных_параметров_функции) { тело_функции}
Статическая компонентная функция сохраняет все основные особенности обычных
(нестатических) компонентных функций, т.е. к ней можно обращаться, используя имя уже
существующего объекта класса либо указатель на такой объект.
Дополнительно статическую компонентную функцию можно вызвать, используя
квалификацию без указания имени объекта:
имя_класса::имя_статической_компонентной_функции(список_аргументов_вызова)
С помощью такой квалификации статические компонентные функции можно вызывать
до определения конкретных объектов класса, так как для доступа к статическим
компонентам данных этим функциям вовсе не требуется, чтобы они вызывались для
конкретного объекта класса.
Статическая компонентная функция непосредственно может ссылаться на статические
компоненты (данные и функции) только своего класса. Так как статическую
компонентную функцию можно вызвать без ссылки на объект класса, то ей не передается
указатель this, который всегда неявно передается нестатическим компонентным функциям
класса при их вызове для указания того объекта, для которого они и вызывались. Далее
будет сказано об особой роли этой переменной, которой невозможночтонибудьприсвоить, и адрес которой недоступен.
Если локальная статическая переменная позволяет обычной функции “помнить о
прошлом” этой переменной, то это справедливо и по отношению к статическим
компонентам данных класса – они тоже “помнят о своем прошлом”. Поэтому их
применение может быть весьма полезным, например, для хранения информации о
количестве объектов класса, существующих в каждый конкретный момент времени
работы программы, или, например, для координации доступа к разделяемым ресурсам,
таким, как дисковые файлы или принтер. Страуструп настоятельно рекомендует
пользоваться спецификатором static только внутри функций и классов для объявления
соответственно локальных и нелокальных статических объектов.
10.Друзья классов: дружественные функции и дружественные классы.
Не редко возникает необходимость использования каких-либо внешних по отношению к
классу функций общего назначения для обработки закрытых данных класса, при этом эти
функции не могут быть методами класса.
Для этих целей в языке С++ предусмотрено объявление в классе дружественных
функций.
Дружественной функцией класса называют функцию, которая не является
компонентной функцией класса, но имеет доступ к защищенным и собственным
компонентам класса.
Дружественная функция обладает следующими свойствами:
1) должна быть описана в теле класса со спецификатором friend;
2) не является компонентной функцией (методом) класса, в котором она определена как
дружественная;
3) может быть глобальной:
class A { friend void f (…); … } ;
void f (…) {…};
4) может быть компонентной функцией (методом) другого ранее определенного класса; и
тогда при описании в классе надо использовать полное имя функции, включающее имя
класса, которому она принадлежит:
class A {… void f1 (…); …};
class B {… friend void A :: f1(…); …} ;
5) может быть дружественной по отношению к нескольким классам
class A;
// неполное определение класса A
class B {…friend void f2 (A, B); …}; //полное определение класса B
class A {…friend void f2 (A, B) ; …}; //полное определение класса A
void f2 (A tip1, B tip2) {тело функции} // определение функции f2
Дружественные классы.
Если все компонентные функции (методы) одного класса являются
дружественными для другого класса, то класс является дружественным этому другому
классу.
Объявление дружественного класса:
class X {…};
class Y {… friend class X; …};
Все приватные и защищенные члены класса Y могут обрабатываться функциями
класса X, то есть являются доступными в дружественном классе.
Дружественный класс может быть определен позже, нежели описан как
дружественный:
class X1 {… friend class X2 ;…} ;
class X2 {…};
11.Конструкторы и деструкторы. Назначение, формат определения, свойства.
Список инициализации.
Конструкторы.
Для многих объектов естественно требовать, чтобы они были инициализированы
(то есть получали начальное значение) при их определении.
Для упрощения процесса инициализации объектов предусмотрен специальный
метод, называемый конструктором.
Конструктор – это компонентная функция (метод класса), вызываемая
автоматически при создании объекта класса и выполняющая необходимые
инициализирующие действия.
Формат определения конструктора в теле класса:
имя_ класса (список_параметров) инциализатор_конструктора
{операторы_тела_конструктора}
Рассмотрим особенности конструкторов.
1) Имя конструктора должно совпадать с именем класса.
2) Конструктор не может возвращать результат, даже тип void не допустим.
3) Конструктор вызывается при определении объекта, или при размещении объекта в
памяти с помощью операции new.
4) Основное назначение конструктора – превращать участки памяти в объекты
класса, то есть проводить инициализацию полей данных объектов. Кроме того,
предусмотрены также такие сопутствующие действия как - открытие файлов, вывод
сообщений, инициализация объектов вспомогательных классов и тому подобное.
5) Существуют два способа инициализации полей данных создаваемых объектов.
Во – первых, можно в теле конструктора присваивать значения полям данных
объектов. Эти значения обычно предоставляют параметры конструктора. Инициализатор,
помещенный между списком параметров и телом конструктора, при этом опускается.
Например, в классе Men можно так ввести конструктор:
class Men {
char * name; int age;
public:
Men (char*, int);
//прототип конструктора класса
};
Men :: Men (char*n, int a) //внешнее определение конструктора
{name = n; age=0;}
Второй способ предусматривает применение инициализатора ("ctor-initializer").
Инициализатор представляет собой список инициализаторов полей данных объекта,
расположенный после списка параметров и отделенный от него ':' (двоеточием). Каждый
инициализатор относится к конкретному (не статическому) полю данных и имеет вид:
имя_поля_данных (список выражений).
Инициализаторы в списке отделяются друг от друга запятыми. Для полей данных
простых типов в списке выражений используется одно выражение, в которое могут
входить константы, параметры конструктора и имена уже инициализированных полей
данных.
6) В определении класса могут присутствовать несколько конструкторов. Конструкторы
могут иметь различное число параметров, необходимое для инициализации создаваемого
объекта. Параметры могут иметь умалчиваемые значения. При этом допускается только
один конструктор с умалчиваемыми значениями или конструктор без параметров.
7) Перечислим названия (виды) конструкторов: конструктор общего вида, конструктор
без параметров (единственный, но необязательный), конструктор умолчания,
конструктор копирования (единственный).
8) Если в классе программист не определил ни одного конструктора, то по умолчанию
формируются конструктор без параметров и конструктор копирования с прототипами
соответственно (их автоматически добавляет компилятор):
T::T();
T::T(const T&);
при создании экземпляров класса компилятор автоматически выделяет под них память,
хотя в этом случае поля данных не инициализируются.
9) Параметром конструктора не может быть его собственный объект, но может быть
ссылка на него.
10) Нельзя получить адрес конструктора.
11) Конструктор нельзя явно вызывать как обычный метод класса. Конструктор неявно
вызывается при создании именованного объекта класса или безымянного, например, в
следующих конструкциях:
имя_класса имя_объекта (аргументы конструктора);
имя_класса (аргументы конструктора);
Последний случай, например, используется при создании объекта в динамической
памяти с использованием операции new:
имя_класса*имя_указателя = new имя_класса (аргументы конструктора);
Деструкторы.
Для выполнения действий, которые сопровождают удаление объектов, в каждом
классе явно или неявно определяется специальный метод, называемый деструктором.
Деструктор - это компонентная функция класса (метод класса), которая
автоматически выполняется, когда экземпляр класса уничтожается.
Деструктор вызывается либо при выходе объекта за пределы области действия
объекта, либо при освобождении динамической памяти операцией delete, выделенной
объекту при его создании с помощью операции new.
Назначение деструктора – выполнение действий, сопровождающих удаление
объекта. Наиболее важное это освобождение ресурсов, включенных в объект при его
создании или при выполнении действий над объектом. Такими ресурсами могут быть
участки памяти, динамически выделяемые для полей данных объекта, файлы, открытые
при создании объекта и связанные с ним, и другие ресурсы. Деструкторы могут быть
нужны и при уничтожении объектов, не захвативших никаких ресурсов, например, для
вывода завершающих фраз.
Класс может иметь несколько конструкторов, но деструктор может быть только
один.
Формат определения деструктора в теле класса:
~имя_класса () {операторы_тела_деструктора};
Рассмотрим свойства деструкторов:
1) Между тильдой и именем класса нет пробелов.
2) У деструктора нет типа результата даже типа void и нет параметров даже типа void.
3) Деструктор выполняется неявно, автоматически, как только объект уничтожается. Его,
как правило, никогда не вызывают, но можно вызывать и явно, если он определен в
классе. Формат вызова конструктора:
имя_объекта. ~имя_класса ();
имя_указателя_на_объект_класса -> ~имя_класса ();
при этом объект будет продолжать существовать, только выполнятся те действия, которые
записаны в теле деструктора.
12.Перегрузка функций. Перегрузка конструкторов. Виды конструкторов. Список
инициализации.
Перегрузка функции – это использование в программе одного имени для
обозначения разных функций. Чтобы перегрузить функцию, надо записать в программе
все варианты одноименных функций, которые будут использоваться. Перегруженные
функции должны отличаться друг от друга типом хотя бы одного формального параметра
и (или) количеством параметров. Компилятор, анализируя аргументы функции в ее
вызове, будет связывать вызов с конкретной перегруженной функцией. Иными словами:
компилятор по контексту использо-вания имени функции связывает вызов функции с
нужным вариантом одноименной функции.
Пример программы вычисления площадей круга и прямоугольника. Алгоритмы
вычисления площадей геометрических фигур оформлены в виде перегруженных функций.
Перегруженные функции различаются количеством параметров.
#include <iostream.h>
#include <conio.h>
#include <math.h>
float area (float r); //вычисление площади круга
float area (float w, float h); //вычисление площади прямоугольника
void main()
{ cout « "s1 = " « area(3,4) « " s2 = " « area(5);
getch();}
float area (float r)
{ return M_PI * r* r;}
float area (float w, float h)
{ return w*h;}
Перегрузка конструкторов приобретает особое значение, из-за того, что конструктору
нельзя назначать произвольное имя, оно всегда должно совпадать с именем класса.
Наличие в классе нескольких конструкторов, имеющих одно имя, но разный состав
параметров, возможно благодаря реализации средства перегрузки функций. Таким
образом, перегрузка для конструкторов просто неизбежна.
Мы уже рассматривали конструкторы, которые могут быть включены в класс:
class A { int x , y ;
public:
A (int, int ); // конструктор с параметрами для инициализации данных
A( );
// конструктор по умолчанию
A ( A& ) ; // конструктор копирования
…};
Часто при разработке класса предусматривается несколько вариантов
инициализации объектов и значит должно быть несколько конструкторов с параметрами,
например,
сlass Window {
int x;
// х-координата левого верхнего угла окна
int y;
// y-координата левого верхнего угла окна
int w;
// ширина окна
int h;
// высота окна
public:
Window ( ) // конструктор по умолчанию
{ x = y = 0; w = h= 100; }
Window ( int w1, int h1 ) // конструктор инициализации размеров окна
{ x = y = 0; w = w; h = h; }
Window ( int x1, int y1, int w1, int h1 ) // инициализация положения и
{ x = x1; y = y1; w = w1; h = h1; }
//размеров окна
Window (Window & win) // конструктор копирования
{ x = win.x; y = win.y; w = win.w; h = win.h; }
};
Можно создавать, например, следующие объекты:
Window w1;
Window w2 (300, 250);
Window w3 (5, 10, 400, 300);
Window w4 = w3;
13.Конструктор с параметрами. Создание объектов (экземпляров класса) и массивов
объектов статических и динамических.
Конструктор с параметрами или конструктор общего вида.
Если в классе определен конструктор общего вида, то с его помощью можно
создавать объекты с нужными значениями полей данных.
Форматы определений:
имя_класса имя_объекта (аргументы_конструктора);
указатель_на_объект = new имя_класса (аргументы_конструктора);
имя_класса имя_массива [размер_массива] =
{имя_класса (аргументы_конструктора_для_0-го_экземпляра), …,
имя_ класса (аргументы_конструктора_для_последнего_экземпляра)
};
Примеры создания объектов для класса Men:
Men one ("Иван", 25);
//one.name = = "Иван", one.age==25
Men* ptr = new Men ("Олег", 55);
// ptr->name == "Олег", ptr->age==55
Создание.
1)Определение статического объекта:
<имя класса> <имя объекта> ( параметры конструктора);
Пустой список параметров не допустим, если в классе конструктор с параметрами.
2)Определение динамического объекта:
<имя класса> * <имя указателя на объект> = new <имя класса> ( параметры
конструктора);
Как только объект определен появляется возможность обращаться к его
компонентам
1) с помощью квалифицированного имени:
имя_объекта. имя_ класса :: имя_компонента
имя_объекта. имя_класса :: имя_компонентной_функции(аргументы)
2) с помощью уточненного имени:
имя_объекта. имя_компонента
имя_объекта. имя_компонентной_функции(аргументы)
14.Конструктор с аргументами, задаваемыми по умолчанию. Конструктор по
умолчанию.
Конструктор с аргументами, задаваемыми по умолчанию.
Этот конструктор является частным случаем конструктора общего вида. При его
определении параметрам задаются умалчиваемые значения.
Этот конструктор позволяет при его вызове с параметрами инициализировать
данные создаваемого объекта указанными значениями параметров. Также возможен вызов
конструктора и без параметров, при этом поля данных будут инициализироваться
значениями по умолчанию.
Men m1;
// m1.name == "", m1.age==0
Men m2 ("Иванов", 45) // m2.name =="Иванов", m2.age == 45
Men m3 ("Петров")
//m3.name == "Петров", m3.age= =0
Men m4 (18)
// ошибка!
Последний случай (ошибка) указывает, что нельзя задавать параметр, перескакивая
через умалчиваемое значение, этот аргумент (18) компилятором будет трактоваться как
значение для параметра n, и естественно будет сообщение об ошибке несоответствия
типов, так как n это указатель на char.
Пример:
struct mag {
int a, b, c;
// конструктор с умалчиваемыми значениями параметров
mag (int aa =1, int bb =2, int cc =3) {a = aa; b =bb; c = cc;}
void shownumber (void) { cout << a << b << c;}
};
mag one;
// объект инициализируется значениями 1, 2, 3
mag one1 (10);
// объект инициализируется значениями 10, 2, 3
mag one2 (10, 20); // объект инициализируется значениями 10, 20, 3
mag one3 (10, 20, 30); // объект инициализируется значениями 10, 20, 30
Конструктор по умолчанию.
Это разновидность конструктора без параметров (присутствует в классе в единственном
экземпляре).
Присутствует, когда надо единообразно инициализировать поля данных, или
инициализирующие действия вообще не связаны с данными.
Этот конструктор не имеет параметров.
class A {
int x, y;
public:
A ( );
};
A :: A ( ) {x = 0 ; y = 0;} //единообразная инициализация полей
//A :: A () { } //инициализация данных в конструкторе не проводится
Конструктор умолчания вызывается, когда для создания объектов используются
следующие определения:
имя_класса имя_объекта;
имя_класса имя_массива_объектов [размер];
указатель_на_объекты_класса = new имя_класса;
указатель_на_объекты_класса = new имя_класса[размер];
При наличии конструктора по умолчанию, предложение:
A one;
создает объект со значениями полей данных x=0 и y=0.
Если в конструкторе умолчания не проводилась инициализация данных, такой
конструктор предоставляет возможность создавать неинициализированные объекты даже
при наличии в определении класса еще одного конструктора с параметрами.
Вызов конструктор по умолчанию схож по форме с вызовом конструктора с
умалчиваемыми значениями и при написании конструкции:
имя_класса имя_объекта;
компилятор не знает какой конструктор вызывать при создании объекта и выдает
сообщение об ошибке.
Существует правило:
При наличии в классе конструктора с параметрами, задаваемыми по
умолчанию, объявлять в классе еще и конструктор по умолчанию нельзя!
Вот если в определении класса вообще нет конструктора, компилятор автоматически
предоставляет конструктор по умолчанию следующего вида:
имя_класса () { }
который участвует в создании неинициализированных объектов.
15.Конструктор копирования. Назначение. Копирование по умолчанию.
Определение конструктора копирования при наличии в классе указателя на
динамический участок памяти.
Конструктор копирования вызывается, когда:
1) надо создать объект, полностью совпадающий с уже созданным;
2) надо передать по значению в некоторую функцию экземпляр класса, при этом в стеке
создается локальная копия объекта, с которым и работает функция;
3) надо создать объект полностью идентичный объекту, который возвращает некоторая
функция посредством оператора return.
В большинстве случаев компилятор предоставляет нам конструктор копирования
по умолчанию, который обеспечивает правильное копирование.
Определение конструктора копирования.
При поиске формы определения конструктора копирования следует учесть
следующие соображения.
Во-первых, объект, копия которого создается, не должен копироваться в
конструктор копирования, а должен передаваться в него по ссылке. Иначе в конструкторе
копирования вызывался бы сам конструктор копирования и возникала бы бесконечная
рекурсия.
И во-вторых – необходимо ввести запрет на модификацию копируемого объекта, и
для этого следует перед объектом - параметром поставить ключевое слово const.
Учитывая вышесказанное, формат определения конструктора копирования для
описанного выше класса должен быть следующим:
T ( const T & obj ) { x = obj.x; y = obj. y;}
Если пользователь определил в классе конструктор копирования, то в случаях
создания объектов – копий будет вызываться конструктор копирования, определенный в
классе. Однако свой конструктор копирования нужен не всегда. Копирование по
умолчанию (когда конструктор копирования предоставляет компилятор) осуществляет
как правило правильное копирование полей данных и без явного определения в классе
конструктора копирования. Обязательное его определение должно быть в тех случаях,
когда класс содержит поля, являющиеся указателями на динамические участки памяти,
при создании так называемых ресурсоемких объектов.
16.Перегрузка функций.
Перегрузка функции – это использование в программе одного имени для
обозначения разных функций.
Использование одного имени для обозначения разных функций, выполняющих
сходные действия, делает программу более понятной и уменьшает сложность
программирования. Например, можно в программе иметь несколько функций сортировки
с одинаковыми именами sort, выполняющих сортировку массивов разных типов.
Перегрузка функции ассоциирует одно имя со схожими действиями.
Чтобы перегрузить функцию, надо записать в программе все варианты одноименных
функций, которые будут использоваться. Перегруженные функции должны отличаться
друг от друга типом хотя бы одного формального параметра и (или) количеством
параметров. Компилятор, анализируя аргументы функции в ее вызове, будет связывать
вызов с конкретной перегруженной функцией. Иными словами: компилятор по контексту
использо-вания имени функции связывает вызов функции с нужным вариантом
одноименной функции.
Цель перегрузки функции состоит в том, чтобы функция с одним именем по-разному
выполнялась и возвращала разнообразные значения при обращении к ней с разными по
типам и количеству фактическими параметрами. Для обеспечения перегрузки функции
необходимо для каждого имени определить, сколько разных функций связано с ним (т. е.
сколько вариантов сигнатур допустимы при обращении к ним).
Сигнатура - совокупность параметров функции и возращаемого значения.
При этом эти функции должны иметь разные сигнатуры
Пример программы вычисления площадей круга и прямоугольника. Алгоритмы
вычисления площадей геометрических фигур оформлены в виде перегруженных функций.
Перегруженные функции различаются количеством параметров.
#include <iostream.h>
#include <conio.h>
#include <math.h>
float area (float r); //вычисление площади круга
float area (float w, float h); //вычисление площади прямоугольника
void main()
{ cout « "s1 = " « area(3,4) « " s2 = " « area(5);
getch();}
float area (float r)
{ return M_PI * r* r;}
float area (float w, float h)
{ return w*h;}
17.Указатели на компонентные данные и методы класса.
Определить указатель можно:
1) При помощи операции '. * ' разыменования указателей на компоненты класса:
имя_объекта. *указатель_на_компонент,_данных
имя_ объекта.*указаталь_ на_ метод (параметры)
Слева от операции '. * ' кроме имени конкретного объекта может помещаться ссылка на
объект.
2) Если определен указатель на объект класса и введены указатели на компоненты того же
класса, то доступ к компонентам конкретных объектов можно получить с помощью
бинарной операции ' ->* ' доступа к компонентам класса через указатель на объект:
указатель_на_объект__класса ->*указатель_на_компонент_данных
указатель_на_объект_класса ->*указатель_на_метод (параметры)
Первым (левым) операндом должен быть указатель на объект класса, значение которого адрес объекта класса.
Второй (правый) операнд - указатель на компонент класса. Результат выполнения
операции ' ->* ' -это либо компонент данных, либо компонентная функция класса. Если
второй операнд - это лево допустимый компонент данных, то и результат применения
операции ' ->* ' ( а также операции '. * ') есть l-значение.
18. Применение статического элемента класса в связанных списках объектов
класса.
Определение статического объекта и массива статических экземпляров класса с явным
присваиванием значений:
<имя класса> <имя объекта> ( параметры конструктора);
<имя класса> <имя массива> [размер массива] =
{ <имя класса>( параметры конструктора для 0-го экземпляра), …, <имя класса> (
параметры конструктора для последнего экземпляра) };
СТАТИЧЕСКИЕ ЭЛЕМЕНТЫ КЛАССА
Если вы создаете статический метод, ваша программа может вызывать такой метод, даже
если объекты не были созданы. Например, если класс содержит метод, который может
быть использован для данных вне класса, вы могли бы сделать этот метод статическим.
Ниже приведен класс menu, который использует esc-последовательность драйвера ANSI
для очистки экрана дисплея. Если в вашей системе установлен драйвер ANSI.SYS, вы
можете использовать метод clear_screen для очистки экрана. Поскольку этот метод
объявлен как статический, программа может использовать его, даже если объекты типа
menu не существуют. Следующая программа CLR_SCR.CPP использует метод clear_screen
для очистки экрана дисплея:
#include <iostream.h>
class menu
{
public:
static void clear_screen(void);
// Здесь должны быть другие методы
private:
int number_of_menu_options;
};
void menu::clear_screen(void)
{
cout << ‘\033′ << «[2J»;
}
void main(void)
{
menu::clear_screen();
}
Так как программа объявляет элемент clear_screen как статический, она может
использовать эту функцию для очистки экрана, даже если объекты типа menu не
существуют. Функция clear_screen использует esc-последовательность ANSI Esc[2J для
очистки экрана.
Использование в ваших программах методов класса
По мере создания методов класса возможны ситуации, когда функция, созданная вами для
использования классом, может быть полезна для операций вашей программы, которые не
включают объекты класса. Например, в классе menu была определена функция
clear_screen, которую вы, возможно, захотите использовать в программе. Если ваш класс
содержит метод, который вы захотите использовать вне объекта класса, поставьте перед
его прототипом ключевое слово static и объявите этот метод как public:
public:
static void clear_screen(void);
Внутри вашей программы для вызова такой функции используйте оператор глобального
разрешения, как показано ниже:
menu::clear_screen();
19. Указатель this . Применение указателя this в связанных списках объектов
класса.
Каждый раз при создании объекта класса строится указатель, называемый this, и
содержащий адрес этого объекта. Правильнее сказать, указатель определяется не в
момент создания объекта, а в момент вызова любого из методов объекта.
В связи с неявным определением this является константным указателем, т.е. по
умолчанию происходит определение:
имя класса *const this = адрес обрабатываемого объекта
При работе с компонентами класса можно использовать указатель this:
Class point { int x, y ;
public:
point( int xx=0, int yy=0)
{this-> x=xx ; this ->y =yy ;} ;
void print ( void)
{ cout<< this->x <<” “ << this->y;} ;
};
Эквивалентно:
Class point { int x, y ;
public:
point( int xx=0, int yy=0)
{ x=xx ; y =yy ;} ;
void print ( void)
{cout<< x <<” “ <<y;} ;
};
В таком использовании нет никаких преимуществ.
Иногда используется при конфликте имен, когда имена формальных параметров
функций совпадают с именами компонентов класса:
Class point { int x, y ;
Class point { int x, y ;
public:
public:
point( int x=0, int y=0)
point( int x=0, int y=0)
{this-> x=x ; this ->y =y ;} ;
{ point::x=x ; point::y =y ;} ;
// используя this
// используя квалифицированное имя
…
Часто функции - члены класса работают с данными объекта класса (или с
указателем на объект класса), а объект еще не существует. Для этого также используется
указатель this, обозначающий указатель на тот объект, для которого вызывается данная
функция.
Class A
{ int x, y ;
public:
A ( int xx=0, int yy =0): x(xx), y(yy){ }
A func ( ) ;
…
};
A A :: func( ) { if ( x%2) x++; // функция x преобразующая в четное
return * this;
}
void main ()
{ A a1 (17, 55)
A a2 = a1 . func ();
}
/* Можно объявить так
A* A:: func ( )
{…
return this} */
Чаще всего this используется при организации связанных списков, звеньями которых
должны быть объекты класса и встает необходимость включать в связи указатель на тот
объект, который в данный момент обрабатывается.
Пример: Очередь
#include <iostream>
using namespace std;
class que
{ static que*first ; // указатель(адрес)первого элемента очереди
que*next ; // указатель на следующий элемент очереди
char bukva; //содержимое элемента очереди
public: // общедоступные функции
que(char c) { bukva = c } ;
// конструктор
void add (void) ; // функция добавления элемента в очередь
static void print (void); // вывод содержимого очереди
};
que * que :: first = NULL; //инициализация статического компонента
// определения функций:
void que::add(void)
{ que* list= first ; // текущий указатель устанавливается на
// начало очереди
que * uk ; // вспомогательный указатель при продвижении по
//очереди
while(list!=NULL) { uk = list ; list=list->next }//продвижение по очереди
if( uk!=NULL) {uk->next=this; }//присоединение в конец очереди
else first = this;
// очередь пустая
this->next=NULL; }
void que::print (void)
{ que *list = first ; // устанавливаем на начало очереди
if ( list = = NULL) {cout << “ список пуст”; return; }
else cout<<”содержимое списка :”
while( list!=NULL)
{ cout<< list->bukva; list= list->next; }//выводим и продвигаемся по
// очереди
}
int main( )
{ //формируем объекты класса
que A( ‘a’) ; que B(’b’ ) ; que C(‘c’); que D(‘d’);
que::print( );
// выводим фразу, что список пуст
A.add( );
que::print( );
return 0;
}
B.add( );
D.add( );//включаем в список
// элементы
//выводим список
C.add( );
20. Перегрузка стандартных операций. Варианты определения функцииперегрузки.
Возможность использовать знаки стандартных операций для
записи выражений как для встроенных, так и для абстрактных
типов данных. Примером абстрактного типа данных является
класс в языке С++.
Перегрузка операций - это распространение действий стандартных
операций на операнды, для которых эти операции не предполагались или
предание стандартным операциям другое назначение.
Формат определения операции-функции:
Тип возвращаемого значения operator знак_ операции
( спецификация формальных параметров)
{ тело операции-функции }
Механизм перегрузки операций во многом схож с механизмом
определения функций, если принять, что:
- конструкция operator <знак операции> есть имя некоторой функции;
- список формальных параметров – список операндов с указанием их
типов, которые участвуют в операции;
- тип возвращаемого результата – это тип значения, которое возвращает
операция;
- тело операции-функции – это алгоритм нового действия операции.
Большинство операций перегружаемы, однако, не все.
Операции, не допускающие перегрузки:
. - операция доступа к элементу класса
.* - операция доступа к указателю на элемент класса
?: - условная операция
: : - операция разрешения области видимости
sizeof - размер объекта
# - препроцессорное преобразование строк
## - препроцессорная конкатенация строк
Перегрузка унарных операций
либо как компонентная функция без параметров, либо как глобальная
(возможно дружественная) функция с одним параметром. В первом случае
выражение A Z означает вызов Z.operator A (), во втором - вызов operator
A(Z).
могут перегружаться только через нестатическую компонентную функцию
без параметров. Вызываемый объект класса автоматически
воспринимается как операнд.
функции), должны иметь один параметр типа класса. Передаваемый через
этот параметр объект воспринимается как операнд.
Синтаксис:
а) в первом случае (описание в области класса):
тип_возвр_значения operator знак_операции ()
б) во втором случае (описание вне области класса):
тип_возвр_значения operator знак_операции(идентификатор_типа)
Примеры.
Перегрузка бинарных операций
либо как компонентная функция с одним параметром, либо как глобальная
(возможно дружественная) функция с двумя параметрами. В первом
случае xAy означает вызов x.operatorA(y), во втором – вызов operator
A(x,y).
нестатическими компонентными функциями с параметрами. Вызываемый
объект класса автоматически воспринимается в качестве первого операнда.
области класса, должны иметь два
операнда, один из которых должен иметь тип класса.
Примеры
Рассмотрим перегрузку ряда операций для класса Complex:
class Complex {
float re , im ;
public:
Complex ( float, float ) ; // конструктор для инициализации
friend float real (Complex); // функция – друг для получения re
friend float image (Complex); // функция – друг для получения im
Complex operator – ( ) ;// перегрузка одноместной операции ”-” , метод
//класса
Complex operator – ( Complex&) ; // перегрузка двуместной операции
// ”-” , метод класса
friend Complex operator + ( Complex&, Complex&) ; // перегрузка “+”//друг
};
Complex :: Complex (float r , float i )
{re = r ; im = i ; }
Complex Complex:: operator-( )
{ return Complex (-re, -im); }
Complex Complex :: operator-( Complex& z) // передача по ссылке , чтобы
{ return Complex (re – z.re, im- z.im); } // не копировать объект в стек
Complex operator+( Complex &z1, Complex &z2)
{ return Complex (z1.re + z2.re , z1.im+ z2.im ) ; }
float real (Complex z){ return z.re ; }
float image (Complex z ){ return z.im ;}
void main ( )
{ Complex c1 (1, 2) ; // создание объекта c1
Complex c2 (3, 4) ; // создание объекта c2
Complex c3 = - c2 ; // копирование в c3 результата вызова c2.operator-( );
Complex c4=c2-c1; //копирование в c4 результата вызова c2.operator-(с1 );
Complex c5=c2+c1// копирование в c5 результата вызова operator+(с2,с1 );
Видим в двух последних строках, что синтаксис правил
использования функций - перегрузки операций, определенных как методы
класса или как друзья класса, совершенно одинаков.
Однако определение операций-функций как дружественных создает
преимущество в смысле симметрии и, главное, в тех случаях, когда для
выполнения действий над операндами требуется преобразование типа
операндов.
Дело в том, что компилятор автоматически выполняет
преобразование типа для аргументов функций, но не для объекта, для
которого вызывается функция-член.
Если функция-оператор (другое название операции - функции)
реализуется как друг и получает оба аргумента как параметры функции, то
компилятор выполняет автоматическое преобразование типов двух
аргументов операции.
=, [], ->
операции–функции с названиями не могут быть внешними, а должны быть
нестатическими методами того класса для которого они определены.
Примеры.
1) class person{…};
class adresbook
{ // содержит в качестве компонентных данных множество объектов
//типа person, представляемых как динамический массив, список или
//дерево
…
public:
person& operator[](int); //доступ к i-му объекту
};
person& adresbook : : operator[](int i){. . .}
void main()
{ adresbook persons;
person record;
…
record = persons [3];
}
21. Перегрузка операции присваивания. Перегрузка присваивания при наличии
в классе указателя на динамический участок памяти. Различие между
копированием и присваиванием. Блокировка копирования и присваивания.
Операция отличается тремя особенностями:
рация не наследуется;
операции поразрядного копирования объекта, стоящего справа от знака
операции, в объект, стоящий слева.
.
Это гарантирует, что первым операндом всегда будет леводопустимое
выражение.
Формат перегруженной операции присваивания:
имя_класса& operator=( имя_класса&);
Отметим две важные особенности функции operator=.
Во-первых, в ней используется параметр-ссылка. Это необходимо
для предотвращения создания копии объекта, передаваемого через
параметр по значению. В случае создания копии, она удаляется вызовом
деструктора при завершении работы функции. Но деструктор освобождает
распределенную память, еще необходимую объекту, который является
аргументом. Параметр-ссылка помогает решить эту проблему.
Во-вторых, функция operator=() возвращает не объект, а ссылку на
него. Смысл этого тот же, что и при использовании параметра-ссылки.
Функция возвращает временный объект, который удаляется после
завершения ее работы. Это означает, что для временной переменной будет
вызван деструктор, который освобождает распределенную память. Но она
необходима для присваивания значения объекту. Поэтому, чтобы избежать
создания временного объекта, в качестве возвращаемого значения
используется ссылка.
Блокировка копирования и присваивания:
Для того, чтобы исключить непреднамеренное дублирование объекта
класса, надо в его закрытой части объявить конструктор копирования и
перегрузку операции присваивания.
private :
A ( A& );
A operator = ( A &) ;
И тогда ни одно из приведенных в главной функции дублирований
объектов скомпилировать не удастся.
Различие между копированием и присваиванием:
Дублирование объекта может быть выполнено и с помощью операции присваивания и с
помощью вызова конструктора копирования.
class A { int x;
public:
A ( int ax=0){ x=ax; }
int GetX ( ) { return x; }
…};
void main ()
{ A m1(5) , m2;
m2 = m1;
// операция присваивания
A m3 = m1 ; // дублирование вызовом конструктора копирования
cout<< m1.GetX ( ) << m2.GetX ( ) << m3.GetX ( );
}
В предложении:
A m3 = m1 ;
создается новый объект, и ему передаются данные копируемого объекта. В этом случае
вызывается конструктор копирования по умолчанию.
В ряде случаев объекты должны создаваться в единственном экземпляре, создание
идентичных объектов должно быть запрещено.
22. Перегрузка операций >> и << для типов пользователя (на примере перегрузки
операций ввода/вывода данных некоторого структурного типа и для
ввода/вывода объектов некоторого пользовательского класса).
(взято из лабораторной, операции перегружаются так же, как и все другие)
//Перегруженная операция вывода для класса комплексного числа
std::ostream & operator << (std::ostream &s, ComplexNum &n) {
s << n.a << "+" << n.b << "i";
return s;
}
//Перегруженная операция ввода для класса комплексного числа
std::istream & operator >> (std::istream &s, ComplexNum &n) {
s >> n.a >> n.b;
return s;
}
23. Перегрузка операций инкремента и декремента на примере класса, который
имеет данные (два) числовых типов.
Рассмотрим перегрузку операций инкремента (++) и декремента (- -), которые могут
быть префиксными и постфиксными .
Принято соглашение, что префиксные операции (++) и (- -), ничем не отличаются
от обычной перегрузки унарных операций. Т.е. дружественные функции перегрузки
содержат один параметр, а компонентные функции перегрузки – не имеют параметров и
определяют те же префиксные операции.
Постфиксные операции–функции должны иметь еще один дополнительный
параметр типа int и тогда компонентная функция перегрузки имеет только один параметр
int, а дружественная два – первый типа класс, а второй типа int.
Операция – функция вызывается с нулевым целым параметром.
Рассмотрим перегрузку префиксных и постфиксных операций для класса “пара
чисел”, при этом перегрузку операции инкремента
произведем с помощью
дружественной функции-операции, а декремента – с помощью компонентной функцииоперации.
#include <iostream.h>
class pair {
int N
double X
public:
pair ( int n , double x)
{ N=n; X= x; }
friend pair & operator ++ ( pair &) ;
// префиксная
friend pair & operator++ ( pair& , int ) ; // постфиксная
pair& operator- - ( )
// префиксная
{ N= N-1 ; X - = 1.0 ;
return * this ; }
pair& operator- - ( int k )
// постфиксная
{ N= N-1+k ; X - = 1.0 +k;
return * this ;}
void display( )
{ cout<< “\n N = “ <<N <<” X= “ << X ; }
};
pair & operator ++ ( pair & P)
// префиксная
{ P. N + = 1 ; P.X +=1.0 ;
return P; }
pair & operator ++ ( pair & P, int k)
// постфиксная
{ P. N + = 1+k ; P.X +=1.0 +k ;
return P; }
void main ( )
{ pair A ( 9, 19.0); A.display( );
++A; A.display ();
- - A; A.display ();
A++ ; A.display ();
A- - ; A.display (); }
Результат:
N = 9 X = 19
N = 10 X = 20
N = 9 X = 19
N = 10 X = 20
N = 9 X = 19
24. Преобразование типов в классах пользователя, явные и неявные.
Иногда бывает необходимость преобразовать переменные
некоторого класса в базовые типы.
Проблема решается с помощью специального оператора
преобразования типа.
Рассмотрим это преобразование для класса stroka.
Преобразование типа stroka в тип char*.
Надо в описание класса включить компонентную функцию:
operator char* ( ) { return ch; } (1)
И тогда в предложении
stroka s1(“string”);
char* s = s1;
произойдет преобразование объекта s1 к типу char* , правило (1), и
переменной s присвоится значение“string”.
Аналогично можно определить в классе преобразование:
operator int ( ) { return len; } (2)
И тогда в предложении :
int l = s1;
переменная класса (объект s1) преобразуется к целому значению, по
правилу, описанному в операторе преобразования (2).
25. Отношения включения классов и наследования классов.
Семантика отношений между классами может быть реализована по
схеме наследования и по схеме включения.
Об отношении включения говорят, используя выражение “включает
как часть” (has a – владеет, содержит в себе как часть).
При наследовании базовый класс – представляет объекты общего
вида, производный класс описывает более конкретные объекты, которые
являются разновидностью (частным случаем объектов базового класса).
Об отношении наследования можно сказать, используя выражение
“является частным случаем” (is a).
Например, самолет является частным случаем транспортного
средства. Это отношение наследования классов.
Самолет имеет крылья, мотор – здесь отношение включения.
26. Наследование. Суть метода. Определение производного класса. Влияния
формата определения производного классов и спецификаторов доступа на
доступ наследуемых элементов.
Наследование - это механизм получения нового класса на основе уже
существующего. Существующий класс может быть дополнен или изменен
для создания нового класса.
Существующие классы называются базовыми, а новые –
производными. Производный класс наследует описание базового класса;
затем он может быть изменен добавлением новых членов, изменением
существующих функций-членов и изменением прав доступа.
Производный класс получает в наследство поля данных и методы
базового класса. При этом наследуемые компоненты не перемещаются в
производный класс, а остаются в базовом классе. В каждый объект
производного класса входит безымянный объект базового класса со всеми
своими полями и методами.
Из наследуемых компонентов базового класса для объектов
производного класса доступны компоненты со статусом public и protected.
Любой производный класс может в свою очередь быть базовым для
других классов и таким образом формируется структура, называемая
иерархией классов, определяющая для каждого класса приложения
родственные связи (“родитель - потомок”) его с другими классами
приложения.
Класс является прямым базовым классом, если он входит в список
базовых при определении производного класса.
А если сам базовый класс является производным от некоторого
родителя, причем этот родитель не входит в список базовых классов, то
этот родитель является непрямым (косвенным) базовым классом.
Иерархию производных классов принято отображать в виде
направленного ациклического графа (НАГ), где стрелкой изображают
связь “ производный от”.
Производные классы располагаются ниже базовых. В том же порядке
они должны располагаться в программе и так их объявления рассматривает
компилятор.
A (базовый класс – прямая база для B )
B (производный от А класс – прямая база для С )
С ( производный класс – с прямой базой В и косвенной А )
На практике часто возникает необходимость создать производный
класс, наследующий возможности нескольких классов.
Наличие в определении производного класса несколько прямых
базовых классов называют множественным наследованием.
В иерархии классов соглашение относительно доступности
компонентов класса следующее:
private – член класса может использоваться только функциями – членами
данного класса и функциями – “друзьями” своего класса. В производном
классе он недоступен.
protected – то же, что и private, но дополнительно член класса с данным
атрибутом доступа может использоваться функциями-членами и
функциями – “друзьями” классов, производных от данного.
public – член класса может использоваться любой функцией, которая
является членом данного или производного класса, а также к public членам возможен доступ извне через имя объекта.
Следует иметь в виду, что объявление friend не является атрибутом
доступа и не наследуется.
27. Наследование. Передача параметров конструктора в базовый класс.
Конструкторы с инициализацией по умолчанию в иерархии классов.
Поскольку конструкторы не наследуются, при создании
производного класса наследуемые им данные-члены должны
инициализироваться конструктором базового класса. Конструктор
базового класса вызывается автоматически и выполняется до конструктора
производного класса. Параметры конструктора базового класса
указываются в определении конструктора производного класса. Таким
образом, происходит передача аргументов от конструктора
производного класса конструктору базового класса.
Например.
class Basis
{ int a,b;
public:
Basis(int x,int y){a=x;b=y;}
};
class Inherit:public Basis
{int sum;
public:
Inherit(int x,int y, int s):Basis(x,y){sum=s;}
};
Объекты класса конструируются снизу вверх: сначала базовый,
потом компоненты-объекты (если они имеются), а потом сам производный
класс. Таким образом, объект производного класса содержит в качестве
подобъекта объект базового класса.
Уничтожаются объекты в обратном порядке: сначала производный,
потом его компоненты-объекты, а потом базовый объект.
Таким образом, порядок уничтожения объекта противоположен по
отношению к порядку его конструирования.
28. Множественное наследование. Порядок вызовов конструкторов и
деструкторов базовых классов при множественном наследовании.
Один класс может наследовать атрибуты двух и более классов одновременно. Для этого
используется список базовых классов, в котором каждый из базовых классов отделен от
других запятой. Общая форма множественного наследования имеет вид:
class имя_порожденного_класса: список базовых классов
{
...
};
В следующем примере класс Z наследует оба класса X и Y:
#include <iostream.h>
class X {
protected:
int a;
public:
void make_a(int i) { a = i; }
};
class Y {
protected:
int b;
public:
void make_b(int i) { b = i; }
};
// Z наследует как от X, так и от Y
class Z: public X, public Y {
public:
int make_ab() { return a*b; }
};
int main()
{
Z i;
i.make_a(10);
i.make_b(12);
cout << i .make_ab();
return 0;
}
Поскольку класс Z наследует оба класса X и Y, то он имеет доступ к публичным и
защищенным членам обоих классов X и Y.
В предыдущем примере ни один из классов не содержал конструкторов. Однако
ситуация становится более сложной, когда базовый класс содержит конструктор.
Например, изменим предыдущий пример таким образом, чтобы классы X, Y и Z
содержали конструкторы:
#include <iostream.h>
class X {
protected:
int a;
public:
X() {
a = 10;
cout << "Initializing X\n";
}
};
class Y {
protected:
int b;
public:
Y() {
cout << "Initializing Y\n";
b = 20;
}
};
// Z наследует как от X, так и от Y
class Z: public X, public Y {
public:
Z() { cout << "Initializing Z\n"; }
int make_ab() { return a*b; }
};
int main()
{
Z i;
cout << i.make_ab();
return 0;
}
Программа выдаст на экран следующий результат:
Initializing X
Initializing Y
Initializing Z
200
Обратим внимание, что конструкторы базовых классов вызываются в том порядке, в
котором они указаны в списке при объявлении класса Z.
В общем случае, когда используется список базовых классов, их конструкторы
вызываются слева направо. Деструкторы вызываются в обратном порядке — справа
налево.
29. Множественное наследование. Прямое и косвенное наследование.
См вопрос 26, 28
30. Иерархия производных классов в виде графа (НАГ).
Иерархию производных классов принято отображать в виде направленного
ациклического графа (НАГ), где стрелкой изображают связь "производный от".
Производные классы располагаются ниже базовых. В том же порядке они должны
располагаться в программе и так их объявления рассматривает компилятор (рис. 4.2).
A
B
(базовый класс – прямая база для B)
(производный от А класс – прямая база для С)
(производный класс – с прямой базой В и косвенной А)
Рис. 4.2. Иерархия наследования
Последовательность объявления классов должна быть такой:
class A {…};
class B: public A {…};
class C: public B {…};
C
31. Дублирование объектов
базового класса, косвенно наследуемого
при
множественном наследовании
На практике часто возникает необходимость создать производный класс, наследующий
возможности нескольких классов.
Наличие в определении производного класса несколько прямых базовых классов
называют множественным наследованием. Пример (рис.4.3):
class A { …} ;
class B {… } ;
class C { … };
class D : public A , public B , public C { . . . } ;
Родители в определении перечисляются через запятую.
А
В
С
D
Рис. 4.3 Схема множественного наследования
Как и в случае одиночного наследования, при создании объекта производного
класса сначала конструируются объекты базовых классов (в том порядке, в котором
базовые классы перечислены в объявлении производного), и лишь после этого
составляется объект производного класса.
Деструкторы выполняются в обратном порядке.
При множественном наследовании никакой класс более одного раза не может быть
прямым базовым классом. Однако класс более одного раза может быть непрямым
базовым. Пример дублирования базового класса приведен на рис. 4.4.
X
X
Y
Z
U
Рис.4.4. Дублирование непрямого базового класса
И соответственно объявления классов:
class X {long double x;};
class Y: public X {double y;};
class Z : public X {int z;};
class U:public Y, public Z {…};
Размеры объектов при дублировании базового класса:
sizeof(X)= 12 (long double)
sizeof(Y)= 20 (long double+ double)
sizeof(Z)= 16 (long double+ int)
sizeof(U)= 36 (long double+ double + long double+ int)
32. Виртуальные базовые классы. Примеры иерархии классов (НАГ), с участием
виртуальных базовых классов.
При наследовании, особенно множественном могут возникать неоднозначности при
доступе к одноименным компонентам разных базовых классов. Способ устранения
неоднозначностей – использование квалифицированных имен компонентов (включающих
имена классов и операцию принадлежности "::"). Для данного примера:
U::Y::X::x или U::Z::X::x
Чтобы устранить дублирование объектов непрямого базового класса при
множественном наследовании, этот базовый класс наследуется как виртуальным. Слово
virtual помещается в спецификатор базового класса. Причем это делается не в объявлении
самого базового класса (X), а в классах, производных от него. Пример графа приведен на
рис. 4.5.
X
Y
Z
U
Рис.4.5. Виртуальное наследование классов
И соответствующее определение классов:
class X {long double x;};
class Y: virtual public X {double y;};
class Z : virtual public X {int z;};
class U:public Y, public Z {…};
При реализации виртуального наследования компилятор добавляет в производный
класс в качестве данного указатель на виртуальный базовый класс. Это можно
обнаружить, если определить размеры объектов классов с виртуальным базовым.
Размеры объектов без дублирования базового класса:
sizeof(X)= 12 (long double)
sizeof(Y)= 24 (long double+ double+ type*)
sizeof(Z)= 20 (long double+ int + type*)
sizeof(U)= 32 (long double+ double + int+ type*+ type*)
Итак, класс производный от виртуального включает:
1) объект (данные) базового класса;
2) указатель на объект базового класса:
3) данные производного класса.
Отметим, что виртуальность класса – это не свойство класса, а результат
особенностей процедуры наследования.
Один и тот же класс при множественном наследовании может, включен в
производный класс при непрямом наследовании и как виртуальный и как не виртуальный
(рис.4.6).
X
X
B
Y
X
Z
C
A
Рис.4.6. Виртуальное и не виртуальное наследование
Определение классов примера:
class X { long double} ;
class Y : virtual public X { double y} ;
class Z : virtual public X { int z } ;
class B : public X { int b } ;
class C : public X { int c } ;
class A : public B , public Y , public Z , public C { . . . } ;
Размеры объектов:
sizeof(X)= 12 (long double)
sizeof(Y)= 24 (long double+ double+ type*)
sizeof(Z)= 20 (long double+ int + type*)
sizeof(B)= 16 (long double+ int)
sizeof(c)= 16 (long double+ int)
sizeof(A)= 64 (long double+ long double + long double +int + int + double + int type*+ type*)
Объект класса А включает три экземпляра класса X: один виртуальный, общий для
классов Y и Z, и два не виртуальных, относящихся к классам В и С.
Виртуальный класс может быть прямым родителем (рис.4.7):
class X { . . . } ;
class A : virtual public X { . . . } ;
class B : virtual public X { . . . } ;
class D : public A, public B , virtual public X { . . . } ;
X
A
B
D
Рис.4.7. Виртуальное наследование
33. Перегрузка операций при наследовании классов.
Перегрузка операций ввода/вывода при наследовании
Рассмотрим какие особенности появляются при перегрузке операций ввода/вывода
для классов, находящихся в отношении наследования. Перегрузку операций ввода/вывода
нельзя выполнить с помощью методов класса, чаще эти операции вводятся как
дружественные. Покажем примером обращение из дружественных функций производного
класса к дружественным функциям базового класса (рис. 4.7).
Bas
Dir
Рис. 4.7. Наследование классов
//определение базового класса
class Bas {
protected: int k;
friend istream & operator >> ( istream&in, Bas&b);
friend ostream & operator << ( ostream&out, const Bas&b);
};
istream & operator>>( istream&in,Bas&b)
{cout<<"k= ";
in>>b.k;
return in;
}
ostream & operator<<( ostream&out, const Bas&b)
{out<<"k= "<<b.k<<endl;
return out;
}
//определение производного класса
class Dir : Bas {
double z;
friend istream & operator>>( istream&in, Dir&d);
friend ostream & operator<<( ostream&out, const Dir&b);
};
istream & operator>>( istream&in,Dir&d)
{cout << "Bas: ";
operator >> (in, dynamic_cast< Bas&>(d));
cout<< "Dir::z= ";
in>>d.z;
return in;
}
ostream & operator<<( ostream&out, const Dir&d)
{ out << "Bas: ";
out << dynamic_cost<const Bas&>(d);
out << "Dir::z= " << d.z << endl;
return out;
}
Перегрузка оператора для ссылки/указателя на базовый объект работает и при
использовании этого же перегруженного оператора для дочерних объектов.
34. Перегрузка операции преобразования типов в классах пользователя при
наследовании классов
??? что-то от Витали
class A {};
class B : public A {};
std::ostream &operator << (std::ostream &s, const A &a) {
s << "Printing something with base type A\n";
}
int main() {
B b;
A a;
std::cout << a << b;
}
35. Полиморфизм. Какие виды полиморфизма знаете. Понятие виртуальной
функции. Режимы раннего и позднего связывания. Полиморфные классы.
Термин полиморфизм дословно означает "множество форм".
Применительно к языкам, говорят о полиморфизме, когда функции или операторы
в различных условиях проявляют себя по-разному.
Процедурный полиморфизм мы уже рассматривали – перегрузка функций. В этом
случае имя функции становится многозначным – ему соответствуют разные алгоритмы.
Еще вид процедурного полиморфизма – это перегрузка операций.
Другой вид полиморфизма связан с наследованием и реализуется с помощью
виртуальных функций.
Рассматривается указатель на базовый класс, который может содержать как адреса
объектов своего класса, так и объектов производных классов. Кроме того, механизм
виртуальных функций позволяет с помощью указателя с типом базового класса
обращаться к переопределенным методам в производных классах. Рассмотрим, как эту
возможность можно реализовать.
Проблема доступа к методам, переопределенным в производных классах, через
указатель на базовый класс решается в C++ посредством использования виртуальных
функций.
Чтобы сделать некоторый нестатический метод виртуальным, надо в базовом
классе предварить его заголовок спецификатором virtual, метод становится виртуальной
функцией. Если эту функцию переопределить в производном классе даже без
спецификатора virtual, в производном классе также создается виртуальная функция.
Сигнатуры функций должны различаться только их принадлежность разным классам, а
алгоритмы могут быть различными. Все сказанное похоже на механизм замещения.
Однако виртуальность этих функций (или полиморфизм) проявляется в том, что
выбор требуемой из множества определенных в иерархии классов виртуальных функций с
одним именем осуществляется не во время компиляции программы, а динамически, по
конкретному значению указателя базового типа, с помощью которого и вызывается
функция.
Какая функция будет вызываться зависит от типа указателя и типа того объекта,
адрес которого присвоен указателю.
Виртуальность функций проявляется только в том случае, если она вызывается
через указатель или ссылку на базовый класс.
Указатель на базовый класс может принимать конкретные значения.
Если значение указателя к моменту вызова функции есть адрес объекта базового
класса, вызывается вариант функции из базового класса.
Если этот указатель имеет значение адреса объект производного класса
(фактически указывает на данные базового класса в объекте производного класса), то
вызывается вариант функции из производного класса.
Рассмотрим вышесказанное на примере.
class A {
public:
virtual void F1 ( );
virtual int F2 ( char* ); };
class B : public A {
public:
virtual void F1 ( );
virtual int F2 ( char* );};
class C : public A {
public:
void F1 ( );
int F2 ( char* ); };
int main () {
A * ap = new A;
B * bp = new B;
C * cp = new C;
ap-> F1 ( );
// вызов функции базового класса А
ap = bp ;
ap-> F1 ( );
// вызов замещенной функции класса В
ap = cp;
ap -> F1 ( ) ;
// вызов замещенной функции класса С
return 0;
}
Вызов виртуальной функции через указатель на базовый класс позволяет в
зависимости от значения этого указателя (не от типа этого указателя, а от значения!)
вызывать варианты этой функции из разных классов.
Если бы функции F1 были бы обычными компонентными функциями, то при всех
трех вызовах вызывалась бы всегда функция базового класса. То есть вызов через
указатель не виртуальных компонентных функций зависит от типа указателя. Если
указатель на базовый класс, то вызывается базовая функция, если указатель на
производный класс, то вызывается функция производного класса.
Если из текста программы однозначно следует, какая функция вызывается,
компилятор включает в текст оператор call c именем функции, компоновщик заменяет
имя на фактический адрес функции. Такой процесс вызова функции носит название
раннего связывания.
При использовании виртуальных функций, выбор требуемой функции из числа
возможных переносится на время выполнения программы. Такой процесс носит название
позднего связывания или динамического связывания. Это означает, что на этапе
компиляции компилятор не определяет какой из методов должен быть вызван, а передает
ответственность программе, которая принимает решение на этапе выполнения, когда уже
точно известно, каков тип объекта, на который указывает наш указатель. Все сказанное
относится также к вызову методов по ссылке на базовый класс.
Для реализации динамического связывания компилятор создает таблицу
виртуальных функций. Обычно таблица виртуальных функций – это массив или
связанный список с указателями на виртуальные функции (virtual table pointer – vptr).
Такой массив или список компилятор автоматически включает в реализацию каждого
класса, в котором определены или унаследованы виртуальные функции, то есть каждый
класс имеет собственную таблицу, элементы такой таблицы адресуют коды виртуальных
функций именно этого класса. Каждый объект такого класса включает дополнительный
член - указатель (pointer) на таблицу виртуальных функций класса.
Механизм идентификации типа во время выполнения программы (RTTI) позволяет
определять, на какой тип в текущий момент времени ссылается указатель.
Работа с виртуальными функциями.
Механизм виртуальных функций можно использовать как для вертикальной
цепочки классов, производных друг от друга так и для горизонтальной группы классов,
находящихся на одном уровне иерархии и производных от одного итого же базового
класса.
Рассмотрим последний случай.
class A {
public :
virtual void Func ();};
class B : public A {
virtual void Func();};
class C : public A {
virtual void Func();};
void A:: Func() {cout<<"A"; }
void B:: Func() {cout<<"B"; }
void C:: Func() {cout<<"C"; }
int main () {
A* ap; // указатель на базовый класс А
B* bp = new B;
C* cp = new C;
ap = bp;
ap->Func(); // вызывается функция B:: Func()
ap =cp ;
ap->Func(); // вызывается функция C:: Func()
return 0;
}
Рассмотрим случай, когда некоторая внешняя функция в качестве аргумента имеет
указатель на базовый класс. В теле функции через указатель вызывается виртуальный
метод Func() того класса, адрес объекта которого будет передан в функцию при вызове.
void F ( A* a) { a->Func(); }
int main () {
A* ap ;
B* bp = new B;
C* cp = new C;
ap = bp;
F( ap ); // вызывается B:: Func()
ap = cp;
F( ap ); // вызывается C:: Func()
return 0;}
В качестве аргументом функции F() можно использовать и ссылку на базовый
класс:
void F ( A& a) { a . Func(); }
int main () {
A* ap ;
B* bp = new B;
C* cp = new C;
ap = bp;
F( *ap ); // вызывается B:: Func()
ap = cp;
F( *ap ); // вызывается C:: Func()
return 0;
}
Можно использовать механизм виртуальных функций, если указатели на базовый
класс инициализировать адресами объектов производных классов:
int main () {
A* bp = new B;
A* cp = new C;
bp->Func();
// вызывается функция B:: Func()
cp->Func();
// вызывается функция C:: Func()
cp=bp;
cp->Func();
// вызывается функция B:: Func()
return 0;}
Механизм виртуального вызова может быть подавлен с помощью использования
полного квалифицированного имени.
Через указатель на базовый класс нельзя обращаться к не виртуальным
компонентной функции производного класса.
Механизм виртуальных функций снимает этот запрет и позволяет с помощью
указателя на базовый класс обращаться к виртуальным функциям производных классов
(после присваивания этому указателю значения указателя на соответствующий объект
производного класса).
Классы, включающие виртуальные функции, играют особую роль в объектноориентированном программировании. Они носят название полиморфных классов.
Отметим важный момент. Если базовый класс содержит хотя бы один виртуальный метод,
то рекомендуется всегда снабжать этот класс виртуальным деструктором, даже если он
ничего не делает. Наличие такого виртуального деструктора предотвратит некорректное
удаление объектов производного класса, адресуемых через указатель на базовый класс,
так как в противном случае деструктор производного класса вызван не будет.
36. Замещение функций в производных классах. Виртуальные функции.
Реально в конкретных задачах вызываются лишь функции производных классов.
"Исходная" виртуальная функция базового класса часто нужна только для того, чтобы в
производных классах было, что замещать.
Содержимое базовой функции в этом случае не имеет значения и может быть пустым:
class A {
public :
virtual void Func () { }
};
Также можно объявить в базовом классе чистую виртуальную функцию, которая
вводится с помощью такого определения:
virtual тип имя ( спецификация параметров) = 0;
Это некоторая абстракция и такую функцию обязательно надо замещать в
производных класса, в которых она и наполнится разумным содержанием.
Объявление такой функции в базовом классе носит формальный характер для
указания на виртуальность функций с данным именем.
Класс, в котором есть хотя бы одна чистая виртуальная функция, называется
абстрактным классом.
Перечислим основные свойства абстрактного класса:
- Невозможно создать самостоятельных объектов абстрактного класса.
- Абстрактный
класс
может использоваться только в качестве базового
класса.
- Если в производном классе от абстрактного базового класса происходит
замещение чистой виртуальной функции, то производный класс не является абстрактным.
Если замещение не производится, то производный класс также является
абстрактным. Пример:
class A {
// абстрактный класс
public:
virtual int Func ( char*) =0;
void F ( );};
class B: public A {
// В - не абстрактный класс
int Func ( char*);};
class C: public A {
// С – также абстрактный класс
void F ( ); };
- Абстрактные классы предназначены для представления общих понятий, которые
предстоит конкретизировать. На базе общих понятий строятся частные производные
классы для описания конкретных объектов.
- Абстрактный класс может иметь поля данных, а также методы, отличные от чисто
виртуальных, и как всякий класс может иметь явно определенный конструктор.
- Конструктор абстрактного класса не может использоваться для создания
объектов, но может использоваться при наследовании. С его помощью инициализируются
поля данных абстрактного базового класса, входящие в объект производного класса.
- Указатель на абстрактный класс может использоваться в качестве формального
параметра. Соответствующий фактический параметр должен иметь тип указателя на
объекты производного (уже не абстрактного) класса.
- Абстрактный класс может быть производным как от абстрактного, так и от
обычного классов.
Рассмотрим в качестве примера абстрактного класса класс "фигура на плоскости",
производный от не абстрактного класса "точка на плоскости" [3].
Определим в файле point.h базовый класс:
class point {
protected:
double x, y;
public:
point (double x1=0.0, double y1=0.0): x(x1), y(y1) {}
void move (double x1=0.0, double y1=0.0) {x=x1; y=y1;}
};
Определим в файле figure.h абстрактный производный класс:
#include "point.h" // включаем определение базового класса
#include <string>
class figure :public point {
protected:
double dx, dy; // "габариты" фигуры
public:
figure (double x1=0.0, double y1=0.0, double dx1=0.0, double dy1=0.0):
point (x1, y1), dx(dx1), dy(dy1) {}
//изменить на заданную величину габариты
void grow (double k) {dx +=k; dy +=k;}
//вычислить площадь еще неизвестной фигуры
virtual double area() = 0; //чистая виртуальная функция
virtual string className() = 0; //чистая виртуальная функция
friend ostream & operator << (ostream & out, figure& );
};
ostream & operator << (ostream & out, figure &fig )
{out<<fig.className() << ":\t center: x= " << fig.x<< ",\ty= "<<fig.y;
out<< ";\n\tdx= "<<fig.dx<< ",\tdy= "<<fig.dy;
<<";\tarea = "<< fig.area();
return out;}
Определим производные классы "эллипс" и "прямоугольник":
//файл realfigures.h
#include "figure.h"
struct ellipse: public figure {
ellipse (double x1=0.0, double y1=0.0, double dx1=0.0, double dy1=0.0):
figure(x1, y1, dx1, dy1){}
virtual double aerea ()
{return ((dx/2)*(dy/2)*3/14159);}
string className() {return string("ellipse")}
};
struct square: public figure {
square (double x1=0.0, double y1=0.0, double dx1=0.0, double dy1=0.0):
figure(x1, y1, dx1, dy1){}
virtual double aerea ()
{return dx*dy;}
string className() {return string("square")}
};
Программа:
#include <iostream>
using namespace std;
#include "realfigures.h"
int main() {
ellipse A(10.0, 8.0, 20.0, 20.0), B;
cout<<"Object A:\n "<<A<<endl;
A.move(5.0,5.0);
cout<<" A.move(5.0,5.0):\n" <<A<<endl;
A.grow(-18.0);
cout<<" A.grow(-18.0):\n" <<A<<endl;
B=A;
cout<<"Object B:\n "<<B<<endl;
square C (1.0, 3.0, 5.0, 6.0), D;
cout<<"Object C:\n "<<C<<endl;
C.grow(-5.0);
cout<<" C.grow(-5.0):\n" <<C<<endl;
D. move (-10, 50);
cout<<" D.move(-10, 50):\n" <<D<<endl;
D.grow(10.0);
cout<<"Object D:\n "<<D<<endl;
return 0;
}
Результат выполнения программы:
Object A:
ellipse:
centre: x=10,
y=8
dx=20
dy=20
area=314.159
A.move(5.0,5.0):
ellipse:
centre: x=5,
y=5
dx=20
dy=20
area=314.159
Object B:
ellipse:
centre: x=5,
y=5
dx=2
dy=2
area=3.14159
Object C:
square:
centre: x=1,
y=3
dx=5
dy=6
area=30
C.grow(-5.0):
square:
centre: x=1,
y=3
dx=0
dy=1
area=0
D.move(-10, 50):
square:
centre: x=-10,
y=50
dx=0
dy=0
area=0
Object D:
square:
centre: x=-10,
y=50
dx=10
dy=10
area=100
Присваивание при наследовании c виртуальными функциями рассмотрим на
примере:
class Bas { //определение базового класса
int k;
public:
Bas(int k1=0): k(k1){ } //конструктор с параметром с умолчанием
//перегрузка операции вывода
friend ostream & operator<<( ostream&out, const Bas&b);
};
ostream & operator<<( ostream&out, const Bas&b)
{out<<"k= "<<b.k<<endl; return out; }
class Dir : Bas {//определение производного класса
double z;
public:
Dir (int k1=0, double z1=0): Bas(k1), z(z1) {} //конструктор
//перегрузка операции вывода
friend ostream & operator<<( ostream&out, const Dir&b);
};
ostream & operator << ( ostream&out, const Dir&b)
{ out<<"Bas:: ";
/*при вызове операции – функции базового класса проводится операция преобразования
типа ссылки на объект производного класса к ссылке на объект базового класса,
повышающее преобразование, так как в иерархии классов базовый класс выше
производного*/
out<< dynamic_cost<const Bas&>(d);
out<<"Dir::z= "<<d.z<<endl; return out; }
int main()
Dir one(15,4.0),two;
Bas *p1=&one,*p2=&two;
*p1=*p2; // присваивания только полей базового класса!
cout<<two<<endl
two=one; //присваивание полей и производно класса
cout<<two<<endl;
}
Bas::k=15
Dir::z=0
Bas::k=15
Dir::k=4
В первом случае автоматически вызывался метод для базового класса:
Bas& Bas::operator=(const Bas&)
Во втором случае автоматически вызывался метод для производного класса:
Dir& Dir::operator=(const Dir&)
Чтобы с помощью указателей на базовый класс можно было обратиться к методу
присваивания производного класса Dir, надо явно определить в базовом классе функциюперегрузку присваивания и определить ее как виртуальную.
Но в произвольном классе функция перегрузки должна иметь те же параметры.
Решение может быть таким. В базовом классе определить:
virtual Bas& operator=(const Bas&b)
{ k=b.k;
return *this;
}
Функция та же, что и представляется классу по умолчанию, кроме виртуальности.
В производном классе следует определить, например, такую функцию:
virtual Dir& operator=(const Bas&d)
{ this-> Bas::operator=(d);
const Dir& r= dynamic_cost<const Dir&>(d); //формируем ссылку на объект //производного
класса, используя понижающее преобразование типа
z=r.z;
return *this;
}
В этом случае уже при первом присваивании *p1=*p2; произойдет правильное
присваивание объектов и базовой части и производной.
Деструкторы при наследовании с виртуальными функциями рассмотрим на
примере:
class Bas {
public:
~Bas(){cout<<”~Bas”<<endl;}
};
class Dir:public Bas {
int* Arr;
public:
Dir():Arr(new int[10]){ }
~Dir(){
delete[]Arr;
cout<<”~Dir”<<endl;}
};
int main()
{Bas*b1=new Dir;
delete b1;
cout<<”New object”<<endl;
Dir obj;
return 0;}
Результат:
~Bas
New object
~Dir
~Bas
При уничтожении объекта операцией delete выполнен только деструктор базового
класса. В памяти остался беспризорный массив из 10 элементов. Деструктор не
виртуальный и через указатель на базовый класс будет вызван деструктор базового класса.
Рекомендуется деструктор базового класса определять, как виртуальный и при
необходимости переопределять его в производном классе.
virtual ~Bas(){cout<<”~Bas”<<endl;}
и тогда:
~Dir
~Bas
New object
~Dir
~Bas
37. Пустая и чистая виртуальные функции. Абстрактный класс, назначение,
свойства. Дать пример использования.
Абстрактным называется класс, в котором есть хотя бы одна чистая виртуальная функция.
Чистой виртуальной функцией называется компонентная функция, которая имеет
следующее определение:
virtual тип имя_функции (список_формальных_параметров) = 0;
Чистая виртуальная функция ничего не делает и недоступна для вызовов. Ее назначение –
служить основой для подменяющих ее функций в производных классах. Абстрактный
класс может использоваться только в качестве базового для производных классов.
Механизм абстрактных классов разработан для представления общих понятий, которые в
дальнейшем предполагается конкретизировать. При этом построение иерархии классов
выполняется по следующей схеме. Во главе иерархии стоит абстрактный базовый класс.
Он используется для наследования интерфейса. Производные классы будут
конкретизировать и реализовывать этот интерфейс. В абстрактном классе объявлены
чистые виртуальные функции, которые, по сути, есть абстрактные методы.
Пример.
class Base{
public:
Base(); // конструктор по умолчанию
Base(const Base&); // конструктор копирования
virtual ~Base(); // виртуальный деструктор
virtual void Show()=0; // чистая виртуальная функция
// другие чистые виртуальные функции
protected: // защищенные члены класса
private:
// часто остается пустым, иначе будет мешать будущим разработкам
};
class Derived: virtual public Base{
public:
Derived(); // конструктор по умолчанию
Derived(const Derived&); // конструктор копирования
Derived(параметры); // конструктор с параметрами
virtual ~Derived(); // виртуальный деструктор
void Show(); // переопределенная виртуальная функция
// другие переопределенные виртуальные функции
// другие перегруженные операции
protected:
// используется вместо private, если ожидается наследование
private:// используется для деталей реализации
};
Объект абстрактного класса не может быть формальным параметром функции, однако
формальным параметром может быть указатель на абстрактный класс. В этом случае
появляется возможность передавать в вызываемую функцию в качестве фактического
параметра значение указателя на производный объект, заменяя им указатель на
абстрактный базовый класс. Таким образом, мы получаем полиморфные объекты.
Абстрактный метод может рассматриваться как обобщение переопределения. Поведение
родительского класса изменяется для потомка. Для абстрактного метода, однако,
поведение просто не определено. Любое поведение задается в производном классе.
Одно из преимуществ абстрактного метода является чисто концептуальным: программист
может мысленно наделить нужным действием абстракцию сколь угодно высокого уровня.
38. Преобразование типов указателей в
иерархии классов. Работа
виртуальными функциями. Пустая и чистая виртуальные функции.
с
dynamic_cast < type-id > ( expression )
Параметр type-id должен быть указателем или ссылкой на ранее определенный тип класса
или "указателем на void".Тип операнда expression должен быть указателем, если typeid является указателем, или l-значением, если type-id является ссылкой.
В поведении dynamic_cast в управляемом коде имеется два критических изменения:
 dynamic_cast на указатель к базовому типу упакованного перечисления вызывает
сбой во время выполнения; вместо преобразованного указателя будет возвращено
значение 0.
 dynamic_cast больше не создает исключение, если type-id является внутренним
указателем на тип значения, в результате чего приведение вызывает сбой во время
выполнения. Приведение теперь возвращает значение указателя 0, а исключение не
создается.
Если type-id является указателем на однозначно доступный прямой или косвенный
базовый класс операнда expression, то результатом будет указатель на уникальный
подобъект типа type-id. Например:
// dynamic_cast_1.cpp
// compile with: /c
class B { };
class C : public B { };
class D : public C { };
void f(D* pd) {
C* pc = dynamic_cast<C*>(pd); // ok: C is a direct base class
// pc points to C subobject of pd
B* pb = dynamic_cast<B*>(pd); // ok: B is an indirect base class
// pb points to B subobject of pd }
Этот тип преобразования называется "восходящим приведением типа", поскольку при нем
указатель перемещается вверх по иерархии классов: от производного класса к классу, от
которого он является производным. Восходящее приведение типа является неявным
преобразованием.
Если type-id является указателем void*, выполняется проверка во время выполнения,
чтобы определить фактический тип операнда expression. Результатом является указатель
на полный объект, на который указывает операнд expression. Например:
// dynamic_cast_2.cpp
// compile with: /c /GR
class A {virtual void f();};
class B {virtual void f();};
void f() {
A* pa = new A;
B* pb = new B;
void* pv = dynamic_cast<void*>(pa);
// pv now points to an object of type A
pv = dynamic_cast<void*>(pb);
// pv now points to an object of type B }
Если type-id не является указателем void*, то выполняется проверка во время выполнения,
чтобы определить, может ли объект, на который указывает операнд expression, быть
преобразован в тип, указанный параметром type-id.
Если тип операнда expression является базовым классом типа, заданного параметром typeid, то выполняется проверка во время выполнения, чтобы определить, указывает ли
операнд expression на полный объект типа, заданного параметром type-id.Если это так, то
результатом является указатель на полный объект типа, заданного параметром typeid. Например:
// dynamic_cast_3.cpp
// compile with: /c /GR
class B {virtual void f();};
class D : public B {virtual void f();};
void f() {
B* pb = new D; // unclear but ok
B* pb2 = new B;
D* pd = dynamic_cast<D*>(pb); // ok: pb actually points to a D
D* pd2 = dynamic_cast<D*>(pb2); // pb2 points to a B not a D }
Этот тип преобразования называется "нисходящим приведением типа", поскольку при нем
указатель перемещается вниз по иерархии классов: от заданного класса к производному от
него классу.
В случаях множественного наследования возникают возможности для
неоднозначности.Рассмотрим для примера иерархию классов, показанную на следующем
рисунке.
Для типов среды CLR результатом динамического приведения dynamic_cast является
либо холостая команда, если преобразование было выполнено неявно, либо инструкция
MSIL isinst, которая выполняет динамическую проверку и в случае сбоя преобразования
возвращает nullptr.
В следующем примере используется динамическое приведение (dynamic_cast), чтобы
определить, является ли класс экземпляром определенного типа:
// dynamic_cast_clr.cpp
// compile with: /clr
using namespace System;
void PrintObjectType( Object^o ) {
if( dynamic_cast<String^>(o) )
Console::WriteLine("Object is a String");
else if( dynamic_cast<int^>(o) )
Console::WriteLine("Object is an int"); }
int main() {
Object^o1 = "hello";
Object^o2 = 10;
PrintObjectType(o1);
PrintObjectType(o2); }
Указатель на объект типа D можно безопасно привести к B или C.Однако если в
результате приведения D указывает на объект A, какой экземпляр объекта A будет
являться результатом? Это может привести к ошибке неоднозначного приведения.Чтобы
обойти эту проблему, можно выполнить два однозначных приведения. Например:
// dynamic_cast_4.cpp
// compile with: /c /GR
class A {virtual void f();};
class B {virtual void f();};
class D : public B {virtual void f();};
void f() {
D* pd = new D;
B* pb = dynamic_cast<B*>(pd); // first cast to B
A* pa2 = dynamic_cast<A*>(pb); // ok: unambiguous }
В этой иерархии объект A является виртуальным базовым классом. При использовании
заданного экземпляра класса E и указателя на подобъект Aдинамическое
приведение dynamic_cast к указателю на B вызовет сбой в силу
неоднозначности.Необходимо сначала выполнить обратное приведение к полному
объекту E, затем однозначным образом вернуться вверх по иерархии, чтобы дойти до
нужного объекта B.
При использовании заданного объекта типа E и указателя на подобъект D можно
выполнить три преобразования, чтобы перейти от подобъекта D к крайнему слева
подобъекту A.Можно выполнить преобразование dynamic_cast из указателя D в
указатель E, затем преобразование (либо dynamic_cast, либо неявное преобразование)
из E в B, и наконец неявное преобразование из B в A. Например:
// dynamic_cast_5.cpp
// compile with: /c /GR
class A {virtual void f();};
class B : public A {virtual void f();};
class C : public A { };
class D {virtual void f();};
class E : public B, public C, public D {virtual void f();};
void f(D* pd) {
E* pe = dynamic_cast<E*>(pd);
B* pb = pe; // upcast, implicit conversion
A* pa = pb; // upcast, implicit conversion }
Оператор dynamic_cast также можно использовать для "перекрестного приведения". В
иерархии классов из предыдущего примера можно выполнить приведение указателя,
например, из подобъекта B в подобъект D, если полный объект имеет типE.
С учетом перекрестного приведения можно выполнить преобразование из указателя на
объект D в крайний левый подобъект A всего за два шага.Можно перекрестное
приведение из D в B, а затем неявное преобразование из B в A.Например:
// dynamic_cast_6.cpp
// compile with: /c /GR
class A {virtual void f();};
class B : public A {virtual void f();};
class C : public A { };
class D {virtual void f();};
class E : public B, public C, public D {virtual void f();};
void f(D* pd) {
B* pb = dynamic_cast<B*>(pd); // cross cast
A* pa = pb; // upcast, implicit conversion }
Значение пустого указателя преобразуется в значение пустого указателя целевого типа
при помощи оператораdynamic_cast.
Если при использовании оператора dynamic_cast < type-id > ( expression ) невозможно
точно преобразовать операнд expression в тип type-id, то проверка во время выполнения
приводит к сбою приведения. Например:
// dynamic_cast_7.cpp
// compile with: /c /GR
class A {virtual void f();};
class B {virtual void f();};
void f() {
A* pa = new A;
B* pb = dynamic_cast<B*>(pa); // fails at runtime, not safe;
// B not derived from A }
39. Библиотека классов ввода/вывода. Понятия потока. Иерархия потоковых
классов.
На этом шаге мы приведем иерархию классов библиотеки ввода-вывода и дадим
краткую характеристику каждого класса .
В отличие от стандартной библиотеки (в которой находятся средства, например, для
работы со строками, или математические функции), унаследованной компиляторами
языка С++ от языка С, библиотека ввода-вывода С++является не библиотекой функций, а
библиотекой классов. Это первая "промышленная" библиотека классов, разработанная для
распространения совместно с компиляторами. Именно эту библиотеку рекомендуют
изучать,
начиная
знакомиться
с
принципами
объектно-ориентированного
программирования. Одним из базовых принципов ООП является предположение о том,
что объекты "знают", что нужно делать при появлении обращения (сообщения)
определенного типа, т.е. для каждого типа адресованного ему обращения объект имеет
соответствующий механизм обработки. Если мы используем объектcout, представляющий
выходной поток, то как уже неоднократно показано на примерах, для каждого из базовых
типов (int, long, double, ...) этот объект cout выбирает соответствующую процедуру
обработки и выводит значение в соответствующем виде. Объект cout не может перепутать
и вывести, например, целое число в формате с плавающей точкой. От таких ошибок,
которые были возможны в языках С или FORTRAN, когда программист сам определял
форму внешнего представления, библиотека классов ввода-вывода хорошо защищена.
Библиотека потоковых классов построена на основе двух базовых
классов: ios и streambuf. Класс streambufобеспечивает буферизацию данных во всех
производных классах, которыми явно или неявно пользуется программист. Обращаться к
его методам и данным из прикладных программ обычно не нужно.
Класс streambuf обеспечивает взаимодействие создаваемых потоков с физическими
устройствами. Он обеспечивает производные классы достаточно общими методами для
буферизации данных. Класс ios и производные классы содержат указатель на
класс streambuf, но об этом можно до времени не вспоминать. Методы и данные
класса streambuf программист явно обычно не использует. Этот класс нужен другим
классам библиотеки ввода-вывода. Он доступен и программисту-пользователю для
создания новых классов на основе уже существующего класса из iostream. Однако
необходимость в построении таких производных классов возникает достаточно редко, и
мы не будем рассматривать класс streambuf. Класс ios содержит компоненты (данные и
методы), которые являются общими, как для ввода, так и для вывода.
При работе с потоковой библиотекой ввода-вывода программист обычно достаточно
активно использует следующие классы:
 ios - базовый потоковый класс;
 istream - класс входных потоков;
ostream - класс выходных потоков;
iostream - класс двунаправленных потоков ввода-вывода;
istrstream - класс входных строковых потоков;
ostrstream - класс выходных строковых потоков;
strstream - класс двунаправленных строковых потоков (ввода-вывода);
ifstream - класс входных файловых потоков;
ofstream - класс выходных файловых потоков;
fstream - класс двунаправленных файловых потоков (ввода-вывода);
constream - класс консольных выходных потоков.
Диаграмма взаимозависимости перечисленных классов изображена на рисунке 1.
Следует отметить, что эта диаграмма потоковых классов упрощена. В реальной схеме
присутствуют промежуточные классы и реализовано более сложное множественное
наследование. Кроме того, программист, как упоминалось, обычно не учитывает наличия
второго базового класса streambuf, и он не показан на схеме.









Рис.1. Упрощенная схема иерархии потоковых классов
Как наглядно видно из диаграммы классов, класс ios является базовым для
классов ostream, istream, и опосредовано базовым для всех остальных потоковых классов.
Все общие средства потоковых классов помещаются в класс ios. Например, при помощи
методов и данных класса ios осуществляется управление процессом передачи символов из
буфера и в буфер. При выполнении этих действий необходимы, например, сведения о
нужном основании счисления (восьмеричное, десятичное, шестнадцатеричное), о
точности представления вещественных чисел, и т.д. Класс ios содержит эти сведения, т.е.
(методы) функции и данные, относящиеся к состояниям потоков и позволяющие менять
их свойства.
Потоковые классы, их данные и методы становятся видимыми и доступными в
программе, если в нее включен нужный заголовочный файл:
 iostream.h - для классов ios, istream, ostream, stream;
 strstrea.h - для классов istrstream, ostrstream, strstream;
 fstream.h - для классов ifstream, ofstream, fstream;
 constrea.h - для класса constream.
Так как класс ios является базовым для остальных потоковых классов, то включение в
текст
программы
любого
из
заголовочных
файлов strstrea.h,
constrea.h или fstream.h автоматически подключает к программе файл iostream.h.
Соответствующие проверки выполняются на этапе препроцессорной обработки.
В заключение перечислим отличительные особенности применения механизма потоков.
Потоки обеспечивают:
 буферизацию при обменах с внешними устройствами;
 независимость программы от файловой системы конкретной операционной
системы;
 контроль типов передаваемых данных;
 возможность удобного обмена для типов, определенных пользователем.
На следующем шаге мы более подробно остановимся на стандартных потоках вводавывода.
40.Определение файлового потока и присоединение к нему физического файла с
помощью конструктора класса с параметрами.
Альтернативный способ определения файловых потоков с присоединением потока к
физическому файлу
При создании объекта – файлового потока можно использовать конструктор с
параметрами. Первый параметр конструктора – имя физического файла, второй – мода
открытия файла – дизъюнкция флагов, представленных выше.
Примеры определения потоков:
ifstream input (“file1.dat”);
Создается поток с именем input для чтения данных из файла с именем file1.dat.
если такой файл не существует в текущем каталоге, то конструктор завершает работу
аварийно. Проверка:
if(! input) { cout << “ошибка при открытии файла file1.dat” << endl; exit(0);}
ofstream output (“ file2.res”);
Создается выходной поток с именем output для записи данных. Если файл с
именем file2.res не существует в текущем каталоге, он будет создан, открыт и
присоединен к потоку output. Если файл уже существует, то предыдущий вариант будет
удален и пустой файл создается заново. Проверка:
if(! output) { cout << “ошибка при открытии файла file2.res”<<endl; exit(0); }
fstream ioput (“file3.bin” , ios :: out | ios :: in | ios :: binary);
Создается двунаправленный файловый поток с именем ioput для двоичного обмена
данными с файлом (для чтения и записи данных). Если файл с именем file3.bin не
существует, он будет создан, открыт и присоединен к потоку ioput. Если файл уже
существует, он присоединяется к потоку для чтения и записи данных. Проверка:
if(! ioput) {cout << “ошибка при открытии файла file3.bin” << endl; exit(0);}
41. Шаблоны функций.
Шаблон функции (иначе параметризированная функция) определяет общий набор
операций (алгоритм), которые будут применяться к данным различных типов. При этом
тип данных, над которыми функция должна выполнять операции, передается ей в виде
параметра на стадии компиляции.
В С++ параметризированная функция создается с помощью ключевого слова
template.
Для параметризации используется список формальных параметров шаблона,
который следует после слова template, заключенный в угловые скобки < >. Каждый
параметр обозначается словом class, за которым следует имя параметра. Имя параметра –
это название типа, его можно использовать для обозначения типов параметров функции,
типа возвращаемого результата и для объявления типов локальных переменных функции.
Формат шаблона функции:
template<class тип_данных>
тип_воз_значения имя_функции(список_параметров){тело_функции}
Основные свойства параметров шаблона функции
-Имена параметров шаблона должны быть уникальными во всем определении шаблона.
- Список параметров шаблона не может быть пустым.
-В списке параметров шаблона может быть несколько параметров, и
каждому из них должно предшествовать ключевое слово class.
-Имя параметра шаблона имеет все права имени типа в определенной
шаблоном функции.
-Определенная с помощью шаблона функция может иметь любое
количество не параметризованных формальных параметров. Может быть
не параметризованное и возвращаемое функцией значение.
- В списке параметров прототипа шаблона имена параметров не обязаны
совпадать с именами тех же параметров в определении шаблона.
- При конкретизации параметризованной функции необходимо, чтобы при вызове
функции типы фактических параметров, соответствующие одинаково параметризованным
формальным параметрам, были одинаковы.
Шаблоны функций предназначены для того чтобы отделить алгоритм обработки данных
от конкретных типов данных, с которыми он работает, передавая тип в качестве
параметра.
42.Шаблоны классов.
Шаблоны классов предоставляют аналогичную возможность, позволяя создавать
параметризованные классы.
Параметризованный класс создает семейство родственных классов, которые можно
применять к любому типу данных, передаваемому в качестве параметра.
Наиболее широкое применение шаблоны находят при создании контейнерных классов.
Контейнерным называется класс, который предназначен для хранения каким-либо
образом организованных данных и работы с ними.
Преимущество использования шаблонов состоит в том, что как только алгоритм работы с
данными определен и отлажен, он может применяться к любым типам данных без
переписывания кода.
Шаблон класса (иначе параметризованный класс) используется для построения родового
класса. Создавая родовой класс, вы создаете целое семейство родственных классов,
которые можно применять к любому типу данных.
Таким образом, тип данных, которым оперирует класс, указывается в качестве параметра
при создании объекта, принадлежащего к этому классу.
Подобно тому, как класс определяет правила построения и формат отдельных объектов,
шаблон класса определяет способ построения отдельных классов.
В определении класса, входящего в шаблон, имя класса является не именем отдельного
класса, а параметризованным именем семейства классов.
Общая форма объявления параметризованного класса:
template <class тип_данных> class имя_класса { . . . };
Отметим, что язык позволяет вместо ключевого слова class перед параметром шаблона
использовать другое ключевое слово — typename, то есть написать:
template < typename Т> class point { /* ... */ };
После объявления Т используется внутри шаблона точно так же, как имена других типов.
43.Смысл и форма объявления параметризованного класса. Основные свойства
параметризованных классов.
Шаблон класса (иначе параметризованный класс) используется для построения
родового класса. Создавая родовой класс, вы создаете целое семейство родственных
классов, которые можно применять к любому типу данных.
Таким образом, тип данных, которым оперирует класс, указывается в качестве
параметра при создании объекта, принадлежащего к этому классу.
Подобно тому, как класс определяет правила построения и формат отдельных
объектов, шаблон класса определяет способ построения отдельных классов.
В определении класса, входящего в шаблон, имя класса является не именем
отдельного класса, а параметризованным именем семейства классов.
Общая форма объявления параметризованного класса:
template <class тип_данных> class имя_класса { . . . };
Отметим, что язык позволяет вместо ключевого слова class перед параметром
шаблона использовать другое ключевое слово — typename, то есть написать:
template < typename Т> class point { /* ... */ };
После объявления Т используется внутри шаблона точно так же, как имена других
типов.
Основные свойства шаблонов классов.
Компонентные функции параметризованного класса автоматически являются
параметризованными. Их не обязательно объявлять как параметризованные с помощью
template.
Дружественные функции, которые описываются в параметризованном классе, не
являются автоматически параметризованными функциями, т.е. по умолчанию такие
функции являются дружественными для всех классов, которые организуются по
данному шаблону.
Если friend-функция содержит в своем описании параметр типа параметризованного
класса, то для каждого созданного по данному шаблону класса имеется собственная
friend-функция.
В рамках параметризованного класса нельзя определить friend-шаблоны
(дружественные параметризованные классы).
С одной стороны, шаблоны могут быть производными (наследоваться) как от
шаблонов, так и от обычных классов, с другой стороны, они могут использоваться в
качестве базовых для других шаблонов или классов.
Шаблоны функций, которые являются членами классов, нельзя описывать как virtual.
Локальные классы не могут содержать шаблоны в качестве своих элементов.
44.Параметры шаблонов. Спецификация шаблонов.
Параметрами шаблонов могут быть абстрактные типы или переменные встроенных
типов, таких как int.
Первый вид параметров мы уже рассмотрели. При инстанцировании на их место
подставляются аргументы либо встроенных типов, либо типов, определенных
программистом.
Второй вид используется, когда для шаблона предусматривается его настройка
некоторой константой. Например, можно создать шаблон для массивов, содержащих n
элементов типа Т:
template <class Т, int n> class Array { / * . . . * / };
Тогда, объявив объект
Array<point, 20> ар мы создадим массив из 20 элементов типа point.
Приведем менее тривиальный пример использования параметров второго вида:
void f1() { cout << "I am f1 () . " << endl; } void f2() { cout << "I am f2 ( ) . " << endl; }
template<void (*pf)()>
struct A { void Show() { pf();} };
int main() {
A<&fl> aa;
aa. Show(); // вывод: I am f1(). A<&f2> ab;
ab. Show();// вывод: I am f2(). return 0;
}
Здесь параметр шаблона имеет тип указателя на функцию. При инстанцировании
класса в качестве аргумента подставляется адрес соответствующей функции.
Естественно, у шаблона может быть несколько параметров.
Параметры шаблона могут иметь умалчиваемые значения:
template<class T=char, int size=64> class arr {
T data [size];
int length;
public:
arr():length(size){ }
T& operator [ ] (int i) {
if( i<0 | | i> size){ cout<<”Index error”; exit(1);} return data[i];
}
};
int main () {
arr<double,5> rf;
arr<int>ri; // массив int , умалчание для size
arr< > rc; // умолчание для обоих параметров for(int i=0; i<5; i++)
{ rf[i]=i; ri=i*i; rc=’A’+i; }
for(int i=0; i<5; i++)
cout<<rf[i]<< “ “<< ri[i]<< “ “ <<rc[i]<< ‘\t’; return 0;
}
0 0 A 1 1 B 2 4 C 3 9 D 4 16 E
45. Определите шаблон класса “вектор” - одномерный массив.
template <class T> // Т – параметр шаблона
class Vector // Vector - имя семейства классов
{T* data ; // данные класса
int size ; // размер пространства
public:
Vector(int); // конструктор
~Vector () { delete [ ]data; } // деструктор
// перегрузка операции “ [ ]”
T& operator [ ] ( int i) { return data[i];}
friend ostream & operator << ( ostream& , Vector <T>); };
template <class T> // внешнее определение конструктора шаблона
Vector :: Vector(int n)
{data = new T[n];
size =n;} // определение перегрузки операции <<
ostream & operator << ( ostream& out , Vector X)
{ out<<<X[i]<<" "; return out;}
Теперь можно объявлять объекты конкретных классов, порожденных из шаблона:
имя параметризованного класса( шаблона)
< фактические параметры шаблона>
имя объекта ( параметры конструктора);
т.е. имя конкретного класса будет:
имя параметризованного класса( шаблона)
< фактические параметры шаблона>
Vector A( 5) ; // вектор А – массив из пяти значений типа
//char, здесь Vector - имя класса
int main() {clrscr(); // статическте объекты:
Vector X(5);
Vector C(5); // динамический объект:
Vector*p= new Vector (10);
// заполнение элементов значениями и вывод элементов вектора,
// используя перегруженную операцию [ ]
for(int j =0; j<< (*p)[j]<<" ";} return ;}
46. В чем смысл использования шаблонов? Как определяются компонентные
функции параметризованных классов вне определения шаблона класса?
Шаблон функции (иначе параметризированная функция) определяет общий набор
операций (алгоритм), которые будут применяться к данным различных типов. При этом
тип данных, над которыми функция должна выполнять операции, передается ей в виде
параметра на стадии компиляции.
В С++ параметризированная функция создается с помощью ключевого слова template.
Для параметризации используется список формальных параметров шаблона,
который следует после слова template, заключенный в угловые скобки < >. Каждый
параметр обозначается словом class, за которым следует имя параметра. Имя параметра –
это название типа, его можно использовать для обозначения типов параметров функции,
типа возвращаемого результата и для объявления типов локальных переменных функции.
Шаблоны представляют собой мощное и эффективное средство обращения с
различными типами данных, которое можно назвать параметрическим полиморфизмом, а
также обеспечивают безопасное использование типов.
Однако следует иметь в виду, что программа, использующая шаблоны, содержит
полный код для каждого порожденного типа, что может увеличить размер исполняемого
файла.
Если метод описывается вне шаблона, его заголовок должен иметь следующие
элементы:
template <описание параметров шаблона>
возвр_тип имя_класса <параметры шаблона> ::
имя_функции (список_параметров функции)
Пример:
template <class T> class point {
public:
point(T _x = 0, T _y = 0) : x(_x), y(_y) {}
void Show();
private:
T x, y;
};
template <class T> void point <T>::Show() {
cout << " (" << x << ", " <<y<< ")" <<endl;
47. Методология обобщенного программирования. Краткие сведения об STL.
Источник 1:
Стандартная библиотека шаблонов (Standard Template Library, STL) состоит из двух
основных частей: набора контейнерных классов и набора обобщенных алго- ритмов.
Контейнеры — это объекты, содержащие другие однотипные объекты. Контейнер- ные
классы являются шаблонными, поэтому хранимые в них объекты могут быть как
встроенных, так и пользовательских типов. Эти объекты должны допускать ко
пирование и присваивание. Встроенные типы этим требованиям удовлетворяют; то же
самое относится к классам, если конструктор копирования или операция присваивания не объявлены в них закрытыми или защищенными. В контейнерных классах
реализованы такие типовые структуры данных, как стек, список, очередь и т. д.
Обобщенные алгоритмы реализуют большое количество процедур, применимых к
контейнерам — например, поиск, сортировку, слияние и т. п., но они не являются
методами контейнерных классов. Наоборот, алгоритмы представлены в STL в фор- ме
глобальных шаблонных функций. Благодаря этому достигается их универсаль- ность:
эти функции можно применять не только к объектам контейнерных классов, но и к
массивам. Независимость от типов контейнеров достигается за счет косвен- ной связи
функции с контейнером: в функцию передается не сам контейнер, а пара адресов, first,
last, задающая диапазон обрабатываемых элементов.
Реализация указанного механизма взаимодействия базируется на использовании так
называемых итераторов. Итераторы — это обобщение концепции указателей: они
ссылаются на элементы контейнера. Их можно инкрементировать для последовательного продвижения по контейнеру, как обычные указатели, а также разыменовывать для получения или изменения значения элемента.
Источник 2:
Во многих случаях алгоритм можно выразить независимо от деталей
представления обрабатываемых им данных. В тех случаях, когда найденный способ
выражения приемлем с точки зрения накладных расходов и не содержит логических
искажений, оказывается удобным при разработке программных систем или их отдельных
частей воспользоваться принципами обобщённого программирования.
Непосредственную поддержку парадигмы обобщённого программирования в языке
программирования обеспечивают шаблоны. Шаблон представляет собой
параметризованное определение некоторого элемента программы. При этом разработчик
всего лишь указывает компилятору правило для генерации (порождения) одного или
нескольких конкретных экземпляров этого элемента программы. Порождение
конкретного экземпляра элемента программы по его шаблону называется
инстанцированием шаблона. Задача генерации всех необходимых экземпляров
возлагается на компилятор и производится им автоматически по мере необходимости.
Стандартная библиотека обеспечивает стандартные порождаемые классы и
функции, которые реализуют наиболее популярные и широко используемые алгоритмы и
структуры данных. Поскольку библиотека построена на основе классов-шаблонов,
входящие в неё алгоритмы и структуры применимы почти ко всем типам данных.
48. Понятие и набор контейнерных классов библиотеки STL.
Контейнеры – это объекты, предназначенные для хранения совокупностей других
объектов. Выделяют базовые контейнеры, примером которых является динамический
массив, и ассоциативные контейнеры. Последние позволяют получить доступ к значению
объекта по заданному ключу.
Соответствующие средства стандартной библиотеки определены в
пространстве имён std и представлены набором заголовочных файлов:
49. Общие свойства контейнеров.
Контейнеры STL можно разделить на два типа: последовательные и ассоциативные.
Последовательные контейнеры обеспечивают хранение конечного количества
однотипных объектов в виде непрерывной последовательности. К базовым
последовательным контейнерам относятся векторы (vector), списки (list) и двусторонние
очереди (deque). Есть еще специализированные контейнеры (или адаптеры контейнеров),
реализованные на основе базовых, — стеки (stack), очереди (queue) и очере ди с
приоритетами (priority_queue).
Кстати, обычный встроенный массив C++ также может рассматриваться как
последовательный контейнер. Проблема с массивами заключается в том, что их размеры
нужно указывать в исходном коде, а это часто бывает неизвестно заранее. Если же
выделять память для массива динамически (операцией new), алгоритм усложняется из-за
необходимости отслеживать время жизни массива и вовремя освобождать па- мять.
Использование контейнера вектор вместо динамического массива упрощает жизнь
программиста.
Для использования контейнера в программе необходимо включить в нее
соответствующий заголовочный файл. Тип объектов, сохраняемых в контейнере, задается
с помощью аргумента шаблона, например:
vector aVect; // создать вектор aVect целых чисел (типа int)
list department; // создать список department из элементов типа Man
Ассоциативные контейнеры обеспечивают быстрый доступ к данным по ключу. Они
построены на основе сбалансированных деревьев. Есть пять типов ассоциативных
контейнеров: словари (map), словари с дубликатами (multimap), множества (set),
множества с дубликатами (multiset) и битовые множества (bitset).
50. Обобщенные алгоритмы STL - как набор глобальных шаблонных функций.
STL обеспечивает общецелевые, стандартные классы и функции, которые реализуют
наиболее популярные и широко используемые алгоритмы и структуры данных.
STL строится на основе шаблонов классов, и поэтому входящие в нее алгоритмы и
структуры применимы почти ко всем типам данных.
Ядро библиотеки образуют три элемента: контейнеры, алгоритмы и итераторы.
Контейнеры (containers) - это объекты, предназначенные для хранения других элементов.
Например, вектор, линейный список, множество.
Ассоциативные контейнеры (associative containers) позволяют с помощью ключей
получить быстрый доступ к хранящимся в них значениям.
В каждом классе-контейнере определен набор функций для работы с ними. Например,
список содержит функции для вставки, удаления и слияния элементов.
Алгоритмы (algorithms) выполняют операции над содержимым контейнера. Существуют
алгоритмы для инициализации, сортировки, поиска, замены содержимого контейнеров.
Многие алгоритмы предназначены для работы с последовательностью (sequence), которая
представляет собой линейный список элементов внутри контейнера.
Итераторы (iterators) - это объекты, которые по отношению к контейнеру играют роль
указателей. Они позволяют получить доступ к содержимому контейнера примерно так же,
как указатели используются для доступа к элементам массива.
Каждый алгоритм выражается шаблоном функции или набором шаблонов функций.
Таким образом, алгоритм может работать с очень разными контейнерами, содержащими
значения разнообразных типов. Алгоритмы, которые возвращают итератор, как правило,
для сообщения о неудаче используют конец входной последовательности. Алгоритмы не
выполняют проверки диапазона на их входе и выходе. Когда алгоритм возвращает
итератор, это будет итератор того же типа, что и был на входе. Алгоритмы в STL
реализуют большинство распространенных универсальных операций с контейнерами,
такие как просмотр, сортировка, поиск, вставка и удаление элементов.
Алгоритмы определены в заголовочном файле <algorithm.h> .
Ниже приведены имена некоторых наиболее часто используемых функций-алгоритмов
STL.
1. Немодифицирующие операции.
for_earch() - выполняет операции для каждого элемента последовательности
find() - находит первое вхождение значения в последовательность
find_if() - находит первое соответствие предикату в последовательности
count() - подсчитывает количество вхождений значения в последовательность
count_if() - подсчитывает количество выполнений предиката в последовательности
search() - находит первое вхождение последовательности как
подпоследовательности
search_n() - находит n-е вхождение значения в последовательность
2. Модифицирующие операции.
copy() - копирует последовательность, начиная с первого элемента
swap() - меняет местами два элемента
replace() - заменяет элементы с указанным значением
replace_if() - заменяет элементы при выполнении предиката
replace_copy() - копирует последовательность, заменяя элементы с указанным
значением
replace_copy_if() - копирует последовательность, заменяя элементы при
выполнении предиката
fill() - заменяет все элементы данным значением
3.
4.
5.
6.
remove() - удаляет элементы с данным значением
remove_if() - удаляет элементы при выполнении предиката
remove_copy() - копирует последовательность, удаляя элементы с указанным
значением
remove_copy_if() - копирует последовательность, удаляя элементы при выполнении
предиката
reverse() - меняет порядок следования элементов на обратный
random_shuffle() - перемещает элементы согласно случайному равномерному
распределению ("тасует" последовательность)
transform() - выполняет заданную операцию над каждым элементом
последовательности
unique() - удаляет равные соседние элементы
unique_copy() - копирует последовательность, удаляя равные соседние элементы
Сортировка.
sort() - сортирует последовательность с хорошей средней эффективностью
partial_sort() - сортирует часть последовательности
stable_sort() - сортирует последовательность, сохраняя порядок следования равных
элементов
lower_bound() - находит первое вхождение значения в отсортированной
последовательности
upper_bound() - находит первый элемент, больший чем заданное значение
binary_search() - определяет, есть ли данный элемент в отсортированной
последовательности
merge() - сливает две отсортированные последовательности
Работа с множествами.
includes() - проверка на вхождение
set_union() - объединение множеств
set_intersection() - пересечение множеств
set_difference() - разность множеств
Минимумы и максимумы.
min() - меньшее из двух
max() - большее из двух
min_element() - наименьшее значение в последовательности
max_element() - наибольшее значение в последовательности
Перестановки.
next_permutation() - следующая перестановка в лексикографическом порядке
prev_permutation() - предыдущая перестановка в лексикографическом
51.Итераторы как обобщение концепции указателей. Назначение. Основные
операции, выполняемые с любыми итераторами.
Чтобы понять, зачем нужны итераторы, давайте посмотрим, как можно реализовать
шаблонную функцию для поиска значения value в обычном массиве, хранящем объекты
типа Т. Например, возможно следующее решение:
template <class Т>
Т* Find(T* ar, int п, const Т& value) {
for (int l = 0; l < п; ++l)
if (*(ar + l) == value) return ar + l;
Обратите внимание, что при продвижении по массиву адрес следующего элемента
вычисляется с использованием арифметики указателей, то есть он отличается от адреса
предыдущего элемента на некоторое фиксированное число байтов, требуемое для
хранения одного элемента.
Попытаемся теперь расширить сферу применения нашей функции — хорошо бы,
чтобы она решала задачу поиска заданного значения среди объектов, хранящихся в
виде линейного списка!
Однако, к сожалению, ничего не выйдет: адрес следующего элемента в списке нельзя
вычислить, пользуясь арифметикой указателей.
Элементы списка могут размещаться в памяти самым причудливым образом, а
информация об адресе следующего объекта хранится в одном из полей текущего
объекта.
Авторы STL решили эту проблему, введя понятие итератора как более абстрактной
сущности, чем указатель, но обладающей похожим поведением.
Для всех контейнерных классов STL определен тип iterator, однако реализация его в
разных классах разная.
Например, в классе vector, где объекты размещаются один за другим, как в массиве,
тип итератора определяется посредством
typedef Т* iterator.
А вот в классе list тип итератора реализован как встроенный класс iterator,
поддерживающий основные операции с итераторами.
К основным операциям, выполняемым с любыми итераторами, относятся:
• Разыменование итератора: если р — итератор, то *р — значение объекта,
на который он ссылается (позицию которого он сохраняет).
• Присваивание одного итератора другому.
• Сравнение итераторов на равенство и неравенство (== и !=).
• Перемещение его по всем элементам контейнера с помощью
префиксного(++р) или постфиксного (р++) инкремента.
Так как реализация итератора специфична для каждого класса, то при объявлении
объектов типа итератор всегда указывается область видимости в форме имя_шаблона ::,
например:
vector<int> :: iterator iter 1;
1ist<Man> :: iterator iter2:
Организация циклов просмотра элементов контейнеров тоже имеет некоторую
специфику.
Так, если i — некоторый итератор, то вместо привычной формы
for (i =0; i < n; ++i)
используется следующая:
for (i = first ; i!= last; ++i)
где first — значение итератора, указывающее на первый элемент в
контейнере,
а last — значение итератора, указывающее на воображаемый элемент,
который следует за последним элементом контейнера.
Операция сравнения < здесь заменена на операцию !=, поскольку операции
< и > для итераторов в общем случае не поддерживаются.
Как было сказано выше, для всех контейнерных классов определены
унифицированные методы begin() и end(), возвращающие “адреса” first и last
соответственно.
Вообще, все типы итераторов в STL принадлежат одной из пяти категорий:
входные,
выходные,
прямые,
двунаправленные итераторы и
итераторы произвольного доступа.
Входные итераторы (InputIterator) используются алгоритмами STL для чтения
значений из контейнера, аналогично тому, как программа может вводить данные из
потока cin.
Выходные итераторы (OutputIterator) используются алгоритмами для записи
значений в контейнер, аналогично тому, как программа может выводить данные в
поток cout.
Прямые итераторы (ForwardIterator) используются алгоритмами для навигации
по контейнеру только в прямом направлении, причем они позволяют и читать, и
изменять данные в контейнере.
Двунаправленные итераторы (BidirectionalIterator) имеют все свойства прямых
итераторов, но позволяют осуществлять навигацию по контейнеру и в прямом, и в
обратном направлениях (для них дополнительно реализованы операции префиксного и
постфиксного декремента).
Итераторы произвольного доступа (RandomAccessIterator) имеют все свойства
двунаправленных итераторов плюс операции (наподобие сложения указателей) для
доступа к произвольному элементу контейнера.
В то время как значения прямых, двунаправленных и итераторов произвольного
доступа могут быть сохранены, значения входных и выходных итераторов
сохраняться не могут (аналогично тому, как не может быть гарантирован ввод тех
же самых значений при вторичном обращении к потоку cin). Следствием является
то, что любые алгоритмы, базирующиеся на входных или выходных итераторах,
должны быть однопроходными.
Вернемся к функции find(), которую мы безуспешно пытались обобщить для
любых типов контейнеров.
В STL аналогичный алгоритм имеет следующий прототип:
template <class InputIterator, class Т>
InputIterator find (InputIterator first, InputIterator last, const T& value);
Итераторы — это обобщение концепции указателей: они “ссылаются” на
элементы контейнера.
Их можно инкрементировать, как обычные указатели, для последовательного
продвижения по контейнеру.
А также разыменовывать для получения или изменения значения элемента.
Для них должны быть определены операции сравнения, операция присваивания.
Итераторы предназначены для предоставления единого метода последовательного
доступа (перебора) элементов контейнера, не зависящего от вида контейнера и типа
элементов в нем.
Итератор может пробегать как все элементы, так и некоторое их подмножество.
В результате использования итераторов алгоритм может «не замечать», с каким
контейнером он работает!
Тем самым появляется возможность унификации алгоритмов.
Итератор обеспечивает:
 единый механизм перебора элементов контейнера, не зависящий ни от его вида, ни
от его реализации, ни от типа элементов;

множественный доступ к контейнеру, позволяющий работать с контейнером сразу
нескольким клиентам, при этом каждый из клиентов будет пользоваться своим
итератором.
При последовательном переборе элементов контейнера итератор не обязательно
перемещается только по смежным (соседним) элементам! Все зависит от контейнера.
!!! Алгоритм последовательного перебора элементов для каждого вида итератора и
для различных контейнеров может быть определен по-своему.!!!
Но для алгоритма – пользователя, который пользуется итератором, обращаясь к
элементам контейнера, совокупность перебираемых элементов представляется в виде
последовательности, имеющей начало и конец.
Итератор – это абстракция, обобщающая понятие указателя.
Подобно тому, как значением указателя является адрес элемента массива, так
значением итератора служит позиция элемента в контейнере.
Внешне одинаковые операции, получаемые при операциях с итераторами разных
контейнеров, могут быть по - разному получены внутри контейнеров.
Так перемещение итератора к следующему элементу контейнера (операция ++) поразному реализуется в векторе и в списке.
!!! Следовательно,
для каждого класса контейнеров должен быть определен свой класс (или несколько
классов) итераторов (локальный или дружественный), предоставляющий средства для
создания итератора для обхода элементов контейнера.
52.Механизм обработки особых ситуаций (механизм исключений).
Механизм обработки исключений является общим средством управления программой.
С его помощью можно обрабатывать не только аварийные, но и любые другие
ситуации, возникшие в результате выполнения программы.
Но именно для выхода из тупиковых положений служит механизм исключений –
средство, позволяющее отделить выявление особой ситуации от обработки
информации о ней [1].
Механизм обработки исключений (МОИ)
Для реализации МОИ в язык С++ введены следующие ключевые слова: try
(контролировать, пытаться, пробовать), catch (ловить, перехватывать), throw (бросать,
генерировать, посылать).
Общая схема посылки и обработки исключений:
try{ операторы
throw выражение_1;
операторы
throw выражение_2
операторы
}
catch (спецификация_исключения_1)
{операторы_обработки_исключения_1}
catch (спецификация_исключения_2)
{операторы_обработки_исключения_2}
Место, в котором может произойти ошибка, должно входить в контролируемый
блок — составной оператор, перед которым записано ключевое слово try:
try {операторы}
Среди операторов, заключенных в фигурные скобки могут быть любые операторы
языка, описания объектов и функций, определения локальных переменных. Кроме того,
в блок контроля за исключениями помещаются специальные операторы, генерирующие
исключения и имеющие формат:
throw выражение;
С помощью выражения формируется специальный объект, называемый
исключением. Все исключения создаются как временные объекты, а тип и значение
каждого исключения определяется формирующим его выражением.
Оператор
throw выполняет посылку исключения, то есть передает управление и пересылает
исключение непосредственно за блок контроля.
В этом месте обязательно располагается ловушка или обработчик исключения,
ловушек может быть несколько (сколько исключений).
Обработчики исключений должны располагаться непосредственно за try-блоком.
Они начинаются с ключевого слова catch, за которым в скобках следует тип
обрабатываемого исключения. Обработчик имеет формат:
catch (спецификация_исключения)
{операторы_обработки_исключения}
Можно записать один или несколько обработчиков в соответствии с типами
обрабатываемых исключений.
Внешне и функционально обработчик исключений похож на определение функции
с одним параметром – типом исключения, не возвращающей значения.
Когда за блоком контроля размещены несколько ловушек, то они должны
отличаться друг от друга.
Существует три формы записи:
catch(тип_исключения имя) { /* тело обработчика */ }
catch(тип_исключения){ /* тело обработчика */}
catch(...) {/* тело обработчика */}
Первая форма применяется, когда имя параметра используется в теле обработчика
для выполнения каких-либо действий — например, вывода информации об
исключении.
Вторая форма не предполагает использования информации об исключении, играет
роль только его тип.
Многоточие обозначает, что обработчик перехватывает все исключения. Так как
обработчики просматриваются в том порядке, в котором они записаны, обработчик
третьего типа следует помещать после всех остальных.
Пример:
catch(int i){ // Обработка исключений типа int}
catch(const char *){ // Обработка исключений типа const char*}
catch(Overflow){// Обработка исключений класса Overflow}
catch(...){ // Обработка всех необслуженных исключений}
После обработки исключения управление передается первому оператору,
находящемуся непосредственно за обработчиками исключений.
Туда же, минуя код всех обработчиков, передается управление, если исключение в
try-блоке не было сгенерировано.
53.Синтаксис и семантика механизма исключений. Спецификация исключений.
Исключения - возникновение непредвиденных ошибочных ситуаций, например
деление на ноль при операциях с плавающей точкой. Обычно эти условия завершают
программу пользователя с системным сообщением об ошибке. Обработка исключений
в С++ дает возможность программисту восстанавливать программу из этих условий и
продолжать ее выполнение.
Язык С++ имеет чувствительный к контексту механизм обработки особых ситуаций.
Контекст для установки исключения - это блок try.
Обработчики объявлены в конце блока try с использованием ключевого слова catch.
Простой пример:
vect::vect(int n)
{ if (n < 1)
throw(n);
p = new int[n];
if (p == 0)
throw("FREE STORE EXHAUSTED");
}
void g()
{ try { vect a(n), b(n);
...
}
catch(int n) { ... } //отслеживает все неправильные размеры
catch(char* error) {...} //отслеживает превышение свободной памяти
}
Установленные исключения
Синтаксически выражения throw возникает в двух формах:
throw;
throw выражение;
Выражение throw устанавливает исключение. Выражение throw без аргумента повторно
устанавливает текущее исключение. Обычно оно используется, когда для дальнейшей
обработки исключения необходим второй обработчик, вызываемый из первого.
void foo()
{ int i;
...
throw (i);
}
main()
{ try {
foo();
}
catch(int i) { ... }
}
Если пользователь хочет выводить дополнительную информацию или использовать ее для
принятия решения относительно действий обработчика, то допустимо формирование в
виде объекта.
enum error {bounds, heap, other};
class vect_error
{ private:
error e_type;
int ub, index, size;
public:
vect_error(error, int, int); //пакет вне заданных пределов
vect_error(error, int); //пакет вне памяти
}
Теперь выражение throw может быть более информативным
...
throw vect_error(bounds, i, ub);
...
Блоки try
Синтаксически блок try имеет такую форму
try
составной оператор
список обработчиков
Блок try - контекст для принятия решения о том, какие обработчики вызываются для
установленного исключения.
try {
...
throw("SOS");
...
io_condition.eof(argv[i]);
throw(eof);
...
}
catch (const char*) {...}
catch (io_condition& x) {...}
Выражение throw соответствует аргументу catch, если он:
 точно соответствует.
 общий базовый класс порожденного типа представляет собой то, что
устанавливается.
 объект установленного типа является типом указателя, преобразуемым в тип
указателя, являющегося аргументом catch.
Обработчики catch
Синтаксически обработчик catch имеет следующую форму
catch (формальный аргумент)
составной оператор
catch (char* message)
{ cerr << message << endl;
}
catch (...) //действие по умолчанию
{
cerr << "THAT'S ALL FOLKS." << endl;
abort();
}
Спецификация исключения
Синтаксис
Заголовок_функции throw(список типов)
void foo() throw(int, over_flow);
void noex(int i) throw();
Terminate() и unexpected()
Обработчик terminate() вызывается, когда для обработки исключения не поставлен другой
обработчик. По умолчанию вызывается функция abort().
Обработчик unexpected() вызывается, когда исключения не было в списке спецификации
исключения
Пример кода, реализующего исключение
Пример 18.1.
#include "vect.h"
void g(int n)
{ try {
// блок try - контекст для принятия решения о том, какие
// обработчики вызываются для установленного исключения
vect a(n);
}
catch (int n) // обработчик
{
cerr << "SIZE ERROR " << n << endl;
g(10);
}
catch (const char* error) // обработчик
{
cerr << error << endl;
abort();
}
}
void main()
{
extern void g(int n);
g(-1);
}
Файл vect.h:
#include <iostream.h>
class vect
{ private:
int* p;
int size;
public:
vect() { size = 11; p = new int[size]; }
vect(int n);
~vect() { delete [] p; }
int& element(int i);
int ub() const { return (size - 1); }
};
vect::vect(int n)
{ if(n < 1) // устанавливается исключение
p = new int[n];
оговоренное предусловие
throw (n); //
if(p == 0) // оговоренное постусловие
throw ("FREE STORE EXHAUSTED"); // устанавливается
// исключение
}
int& vect::element(int n)
{ if(n < 0 || n > size-1)
throw ("ILLEGAL NUMBER OF ELEMENT");
// устанавливается исключение
return (p[n]);
}
54.Исключения в конструкторах.
Язык C++ не позволяет возвращать значение из конструктора и деструктора.
Механизм исключений дает возможность сообщить об ошибке, возникшей в
конструкторе или деструкторе объекта.
Для иллюстрации создадим класс Vector, в котором ограничивается
количество запрашиваемой памяти:
class Vector {
public :
class Size { }; // Класс исключения
enum {max = 32000}; // Максимальная длина вектора
Vector (int n) {if (n<0 || n>max) throw Size (); ...} // Конструктор
При использовании класса Vector можно предусмотреть перехват
исключений типа Size:
try {Vector *р = new Vector(i);}
catch (Vector::Size){ // Обработка ошибки размера вектора }
Обрабатывать исключения, возникающие в конструкторах, можно двумя
способами. Во-первых, можно создавать блок контроля за исключениями и набор
обработчиков в том месте, где вызывается конструктор, то есть создаются объекты
класса. Во-вторых, введена специальная форма генерации и обработки исключений
непосредственно в конструкторах.
Внешнее определение конструктора может выглядеть так:
имя_ класса::имя_класса (спецификация_параметров)
try:список _инициализаторов
{операторы_тела_конструктора}
последовательность_ обработчиков_ исключений
Обработчики перехватывают исключения, возникшие при инициализации и
при выполнении операторов тела конструктора.
В следующей программе [5] рассматриваются оба способа.
Определим класс точек на плоскости с счетчиком их количества.
#include <iostream>
#include <string>
using namespace std;
#define print(X) cout<<#X"= "<<X<<endl;
class point{ double x, y;
static int N;
public:
point(double xn=0.0, double yn=0.0;)
static int& count(){return N;} };
int point::N=0;
point ::point(double xn=0.0, double yn=0.0) try: x(xn), y(yn) {
N++;
if(N==1) throw "The begin! ";
if(N>2) throw string "The end! ";
}
catch (const char*ch)
{cout<<ch<<endl;}
int main(){
try{ print(point::count());
point A(0.0,1.0);
print(A.count());
point B;
print(point::count());
point C;
print(point::count());
point D(1.0,2.0);
print(D.count());}
catch (const string ch)
{cout<<ch<<endl;}
return 0;}
Результат:
point::count()=0
The begin
A.count()=1
point::count()=2
The end
Исключение типа const char* послано при первом обращении к
конструктору при создании первого объекта. Оно обработано в конструкторе,
выводится сообщение "The begin!".
Исключение типа string посылается, когда число объектов превысит 2. Оно
перехватывается в основной программе ("The end! ").
Download