Бизнес-информатика «Объектно-ориентированный анализ и проектирование»

advertisement
Бизнес-информатика
«Объектно-ориентированный анализ и проектирование»
Оъектно-ориентированное программирование (ООП) – технология, возникшая как
реакция на кризис программного обеспечения, когда методы структурного
программирования не справлялись со сложностью промышленного программного
продукта.
Специфика проблемы состоит в ом, что создаваемый продукт, является результатом
работы большого коллектива разработчиков. Возникающие при этом проблемы:
- минимизация конфликтов, между разными группами разработчиков;
- сопровождение продукта (устранение не выявленных ранее и возникающих
ошибок);
- возможность модификации.
Выходом является декомпозиция системы, которая существует и в структурном
программировании, когда алгоритм разбивается на функциональные модули
(функциональная декомпозиция). В ООП критериями декомпозиции связаны с
принадлежностью ее элементов к различным абстракциям предметной области. То есть
центральной проблемой становится анализ предметной области, вычленение отдельных
объектов, определение для каждого объекта его свойств, когда каждому объекту ставится
в соответствие программный продукт.
Критерии качества декомпозиции проекта связаны со сложностью реализации
проекта:
1. На какие компоненты можно разбить программу? Простота компонент, а,
следовательно, рост числа компонент ведет к росту сложности. При этом
усложняется организация связи между компонентами, разделяются действия,
связанные по сути.
2. Организация взаимосвязи между компонентами упрощается, если компонент
рассматривается как «черный ящик»:
известные функции
вход
Черный
ящик
выход
Совокупность входов и выходов, называемая интерфейсом, служит для получения
информации либо изменения состояния компоненты. При этом компоненты играют роль
сервера или клиента (клиент использует услуги сервера).
Для оценки качества программного продукта нужно учитывать:

сцепление внутри компоненты – показатель, характеризующий степень
взаимосвязи частей компоненты;

связанность между компонентами – показатель, определяемый на основе
анализа интерфейса между клиентом и сервером. При этом мера связанности –
это общее число входов и выходов.
1. Принципы ООП. Классы.
Любая программа – набор инструкций процессора.
Повышение уровня абстракции программы:
 функции;
 собственные типы данных (typedef тип New_name [размерность]); структуры;
 объединение в модули описаний типов и функций для их обработки
(интерфейс).
Класс – тип данных, определяемый пользователем (члены класса - это поля
(константы и переменные) и методы (функции их обработки)). Интерфейс класса –
заголовки методов класса. Конкретные величины типа «класс» - экземпляры класса, или
объекты. Сообщение – запрос на выполнение действия, содержащий набор необходимых
параметров (вызов функций). ООП реализует «событийно-управляемая модель», когда
данные активны и управляют вызовом того или иного фрагмента кода, программа
ожидает действий пользователя (в «директивной модели» код предлагает пользователю
делать те или иные действия).
Основные свойства ООП:
 Инкапсуляция – ограничение доступа к данным и включение методов,
образующих эти данные. Доступ к частям класса регулируются через ключи
доступа: public (открытые), private (закрытые), protected (защищенные).
Интерфейсом являются открытые поля и методы. Доступ в закрытую часть
осуществляется через собственные методы, в защищенную часть - через
собственные методы и методы классов-потомков.
 Наследование – механизм получения нового класса из существующего путем
изменения и добавления полей и методов класса. При это возникает иерархия
родственных типов с совместным интерфейсом
 Полиморфизм – возможность использовать в различных классах иерархии одно
имя для обозначения сходных по смыслу действий (перегрузка, шаблон).
Перегрузка операций и использование виртуальных методов позволяет выбрать
на этапе выполнения нужный метод из одноименных методов базового и
производного класса.
Описание классов.
Данные класса – поля, функции – методы.
class <имя>{
[private:]
<описание скрытых элементов>
public:
<описание доступных элементов>
};
Поля класса:
 могут иметь любой тип, кроме типа этого же класса;
 могут быть описаны с модификатором const (инициализируется через конструктор
лишь один раз);
 могут быть описаны с модификатором static.
Классы могут быть глобальными и локальными.
class monstr{
int health,ammo;
public:
monstr(int he=100, int am=10)
{health = he;
ammo = am;}
;
void draw( int x, int y, int scale, int position);
int get_health()
{return health;}
int get_ammo()
{return ammo;}
};
Метод, тело которого определено внутри класса, - встроенный.
Операция доступа к области видимости – « :: » .
void monstr::draw(int x, int y, int scale, int position)
{/*тело метода*/}
Метод может быть определен как встроенный и вне класса:
inline int monstr:: get_ammo()
{return ammo;}
Объекты класса.
monstr
Vasia,
Super(200,300),
stado[100],
*beavis = new monstr (10,20),
&butthead = Vasia;
int n= Vasia.get_ammo();
stado[5].draw(1,2,3,4);
cout << '\n'<<beavis->get_health() <<
'\n' << butthead.get_health()<< '\n'<<n;
У константного объекта значения полей менять нельзя, к нему можно применять лишь
константные методы:
class monstr {
int health,ammo;
public:
monstr_const(int he=100, int am=10)
{health = he;
ammo = am;}
void draw( int x, int y, int scale, int position);
int get_health() const
{return health;}
int get_ammo()
{return ammo;}
};
…
const monstr Dead(0,0);
cout << '\n'<< Dead.get_health();
Константный метод:
 объявляется с ключевым словом const после списка параметров;
 не может изменять значения полей класса;
 может вызывать только константные методы;
 может вызываться для любых объектов.
Указатель this
хранит константный указатель на вызвавший функцию объект и используется внутри
метода для ссылок на элементы объекта.
class monstr{
int health,ammo;
public:
monstr(int he=100, int am=10);
void draw( int x, int y, int scale, int position);
int get_health()
{return health;}
int get_ammo()
{return ammo;}
monstr &the_best( monstr &M)
{ if (health > M.health)
return *this;
return M;
}
};
monstr::monstr(int he, int am)
{health = he;
ammo = am;
}
void main()
{
monstr Vasia, Super(200,300;
monstr Best = Vasia.the_best(Super);
cout << '\n'<< Best.get_health();
}
This применяется для идентификации поля класса (при описании метода, когда имя поля
совпадает с именем формального параметра метода). Иначе можно использовать
операцию доступа к области видимости:
class monstr{
int health,ammo;
color skin;
char *name;
public:
…
void cure(int health, int ammo)
{ this->health +=health;
monstr::ammo += ammo;
}
};
...
monstr Best = Vasia.the_best(Super);
Best.cure(13, 18);
cout << '\n'<< Best.get_health();
Конструкторы
Основные свойства:
 Не возвращает значение. Нельзя получить указатель на конструктор.
 Класс может иметь несколько конструкторов с разными параметрами.
 Конструктор, вызываемый без параметров, наз. конструктором по умолчанию.
 Параметры конструктора могут иметь любой тип, кроме этого же класса. Только
один из конструкторов может содержать значения параметров по умолчанию.
 Если не указан ни один конструктор, он создается автоматически.
 Конструкторы не наследуются.



Конструкторы нельзя описывать с модификаторами const, virtual, static.
Конструкторы глобальных объектов вызываются до вызова main. Локальные
объекты создаются, когда становится активной область их действия.
Конструктор вызывается, если в программе встретилась какая-либо из
конструкций:
Описание объектов:
1. имя_класса имя_объекта [(список параметров)] // список параметров не должен
быть пустым
2. имя_класса (список параметров) // список параметров может быть пустым,
создается объект без имени
3. имя_класса имя_объекта = выражение // создается объект без имени и копируется
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
enum color{red, green, blue};
class monstr{
int health,ammo;
color skin;
char *name;
public:
monstr(int he=100, int am=10);
monstr(color sk);
monstr(char *nm);
void draw( int x, int y, int scale, int position);
int get_health()
{return health;}
int get_ammo()
{return ammo;}
};
monstr::monstr(int he, int am) /* конструктор по умолчанию, т.к. его можно
вызывать бех параметров */
{health = he;
ammo = am;
skin = red;
name = 0;
}
monstr::monstr(color sk)
{switch (sk)
{
case red: ammo = 10; skin = red; break;
case green: ammo = 20; skin = green; break;
case blue: ammo = 40; skin = blue;
}
health = 100; name = 0;
}
monstr::monstr(char *nm)
{health = 100;
ammo = 10;
skin= red;
name = new char [strlen(nm)+1];
strcpy (name , nm);
}
...
void monstr::draw(int x, int y, int scale, int position)
{ cout <<x<< y<< scale<< position;}
void main()
{
monstr Vasia, Super(200,300), stado[100];
cout << '\n'<< Best.get_health();
monstr Green(green);
monstr *m = new monstr("Ork");
}
Можно инициализировать поля в конструкторе с помощью списка инициализаторов,
расположенных через запятую между заголовком и телом конструктора:
monstr::monstr(int he, int am): health (he),
ammo(am),
skin (red), name (0){}
Это единственно возможный способ для инициализации полей-констант, полей-ссылок,
полей-объектов.
Конструктор копирования
- это специальный вид конструктора, получающий в качестве параметра указатель на
объект этого же класса:
<имя класса>::<имя класса>(const &<имя класса>) {/* тело конструктора*/}
Конструктор вызывается, когда объект копируется копированием существующего:
 при описании нового объекта с инициализацией другим объектом;
 при передаче объекта в функцию по значению;
 при возврате объекта из функции.
Если не указано конструктора копирования, он создается автоматически. При этом
осуществляется копирование полей, и если класс содержит указатели или ссылки,
копирование будет «неправильным» (одна область памяти).
…
monstr::monstr(const monstr &M)
{health = M.health;
ammo = M.ammo;
skin= M.skin;
if (M.name)
{ name = new char [strlen(M.name)+1];
strcpy (name , M.name);
}
else name = 0;
…
void main(
{…
monstr Vasia(blue);
monstr Super = Vasia; //конструктор копирования
monstr *m = new monstr("Ork");
monstr Green = *m; //конструктор копирования
}
Статические элементы класса
Статические поля – хранят данные общие для всех объектов класса. Они существуют для
всех объектов класса в одном экземпляре (не дублируются).
 Память пот статическое поле выделяется один раз при его инициализации с
помощью операции доступа к области действия.
class A{
public:
static int count;
…
};
…
int A::count; // инициализируется нулем
// int A::count = 10; // инициализируется другим образом

Статические поля доступны как через поля класса, так и через имя объекта:
A *a, b;
…
cout<< A::count << a->count << b.count;


Статические поля, описанные как private, можно изменить лишь с помощью
статических методов.
Память, занимая статическим полем, не учитывается при определении размера
объекта с помощью sizeof .
Статические методы – обрабатывают статические поля класса. Они обращаются
непосредственно к статическим полям и вызывают только статические методы класса, им
не передается указатель this. Обращение к статическим методам осуществляется так же, и
к статическим полям
class A{
static int count;
public:
static void inc_count() {count++;}
…
};
…
int A::count; // инициализируется нулем
void f()
{
A a;
// a.count ++ - нельзя, поле скрытое
a.inc_count(); //или A::inc_count();
}
Статические методы не могут быть константными и виртуальными.
Дружественные функции и классы
служат для непосредственного доступа извне к скрытым полям класса (расширяют
интерфейс класса).
Дружественные функции оформляют действия, не представляющие свойств класса,
нуждающиеся в доступе к его скрытым полям.
Дружественная функция
 объявляется внутри класса, к элементам которого нужен доступ, с ключевым
словом friend. Параметром передается объект или ссылка на объект;
 может быть обычной функцией или методом другого ранее определенного класса;
на нее не распространяется действие спецификаторов доступа место размещения в
классе безразлично;
 может быть дружественной нескольким классам.
class monstr; //Предварительное объявление класса
class hero{
public:
void kill( monstr &);
…
};
class monstr{
…
friend int steal_ammo(monstr &);
friend void hero::kill(monstr &);
};
int steal_ammo(monstr &M)
{return –M.ammo; }
void hero::kill(monstr &M)
{M.health = 0; M.ammo = 0;}
!!!Дружественные функции нарушают принцип инкапсуляции!!!
Если все методы класса должны иметь доступ к скрытым полям другого, весь класс
объявляется дружественным:
class hero{
…
friend class mis;
};
class mis{
…
void f1();
void f2();
};
Функции f1 и f2 дружественные по отношению к hero имеют доступ ко всем его полям.
Деструкторы
- особый вид метода для освобождения памяти, занимаемой объектом. Вызывается
автоматически при выходе объекта из области видимости:
 для локальных – при выходе из блока;
 для глобальных – при выходе из main;
 для заданных через указатели при использовании delete.
Деструктор начинается с тильды, за которой следует имя класса:
 не имеет аргументов и возвращаемого значения;
 не может быть const / static;
 не наследуется;
 может быть виртуальным;
 если не описан, создается автоматически как пустой.
Нужно создавать, если у объекта есть указатели на память, выделяемую
динамически. Может вызываться явным образом при указании полностью уточненного
имени (для объектов созданных с помощью перегруженной операции new).
…
monstr::~monstr()
{delete[] name;
}
…
void main()
{
…
monstr *m;
m -> ~monstr();
}
Перегрузка операций
Нельзя перегружать:
 ?: :: # ## sizeof
Перегрузка операций (функции-операции):
 сохраняются количество аргументов, приоритеты, правила ассоциации;
 нельзя использовать для стандартных типов;
 не могут иметь параметров по умолчанию;
 не наследуются (за исключением =);
 не могут быть static ;
 может определяться как
1. метод класса (количество параметров на 1 меньше, первым операндом
является объект, вызвавший операцию);
2. дружественная функция класса;
3. обычная функция.
Синтаксис:
<тип> operator <знак операции> (<список параметров>) {тело функции}
Перегружаться операция может
1) как метод класса:
class Point{
double x, y;
public:
Point operator +(Point&);
…
};
Point::Point operator +(Point& p)
{return Point(x + p.x, y + p.y};
}
2) как внешняя функция:
class Point{
double x, y;
public:
friend Point operator +(Point&, Point& );
…
};
Point operator +(Point& p1, Point& p2) //т.к. функция дружественная,
//:: - не нужно
{return Point(p1.x + p2.x, p1.y + p2.y};
}
…
Point p1(0,2), p2(-3,5);
Point p3 = p1 + p2; // p1.operator +(p2), operator +(p1,p2)
…
Если первый операнд имеет базовый тип, то перегрузка возможна лишь в виде
внешней функции.
Перегрузка унарных операций
class monstr{
…
public:
monstr& operator++(){ ++health; return *this;}
};
…
monstr Vasia;
cout << (++Vasia).get.health();
class monstr{
…
friend monstr& operator++(monstr &M);
};
…
monstr& operator++(monstr &M){++M.health; return M;}
Если функция недружественная, д.б. доступно изменяемое поле.
class monstr{
…
Void change_health(int he) {health=he;}
};
…
monstr& operator++(monstr &M){
int h = M.get_health();
h++;
M.change_health(h);
return M;}
Операции постфиксные должны иметь первый параметр типа int, чтобы отличить их
от префиксной формы:
class monstr{
…
monstr operator ++(int){
monstr M = *this;
health++;
return M;}
};
…
monstr Vasia;
cout << (Vasia++).get.health();
// распечатывается первоначальное здоровье (сохраненное в M), а в
//Vasia здоровье увеличивается на 1
class Point{
double x, y;
public:
…
// префиксный метод
Point& operator++()
{x++;
y++;
return *this;
}
};
При возвращении значения по ссылке предотвращается вызов конструктора
копирования (для создания возвращаемого значения и последующего вызова
деструктора). Поэтому префиксная запись эффективнее постфиксной.
// постфиксный метод
Point operator ++(int)
{Point old = *this;
x++;
y++;
return old;
}
…
};
Ссылка в этом случае не подходит, так как необходимо вернуть первоначальное состояние
объекта (сохраненное в old).
Перегрузка бинарных операций
class monstr{
…
public:
bool operator >(const monstr &M)
{if( health > M.health)
return true;
return false;
}
};
Если перегруженная операция определяется вне класса, она имеет 2 параметра типа
класса.
class monstr{
…
};
bool operator >(const monstr &M1, const monstr &M2)
{if(M1.get_health() > M2.get_health())
return true;
return false;}
friend monstr& operator ++(monstr &M){++M.health; return M;}
…
Перегрузка операции присваивания
Присваивание по умолчанию осуществляется как поэлементное копирование.
Перегруженная операция возвращает ссылку на объект, для которого вызывается, и имеет.
единственный параметр – ссылку на присваиваемый объект.
1. Если требуется конструктор копирования, то должна быть перегружена
операция присваивания.
2. Может быть определена только в форме метода класса.
3. Не наследуется.
const monstr& operator=(const monstr &M)
{
if (&M == this) return *this;
if (name) delete [] name;
if (M.name)
{
name = new char [strlenM.name) + 1];
strcpy(name, M.name);}
else name =0;
health = M.health;
ammo = M.ammo;
skin = M.skin;
return *this;
}
…
void main()
{
monstr A(10), B, C;
C = B = A;
…
}
Для перегрузки операции присваивания необходимо

убедиться, что нет присваивания вида x = x;

удалить предыдущее значение в динамически выделенной памяти;

выделить память под новые значения полей;

скопировать все значения полей;

возвратить значение объекта, на которое указывает this.
Рекомендации по составу классов




конструкторы, определяющие как инициализировать объекты;
набор методов, реализующих свойства класса (const указывает, что поля класса
не должны меняться);
набор операций (копирование, присваивание, сравнение, …);
класс исключений (сообщения об ошибке)
Если есть функции, работающие с несколькими классами без доступа к скрытым
полям, можно описать их вне классов. Для логической связи функции можно поместить в
общее пространство имен:
namespace Nashi{
class monstr{…};
class hero{…};
void change(hero, monstr);
…
}
Самостоятельная работа: разобрать задание из практикума «Реализация класса
треугольников»:
Библиотеки
 cstring – работа со строками в С;
 iomanip – манипуляторы, то есть функции, которые можно помещать в цепочку
помещения и извлечения для форматирования данных (dec, oct, hex, endl,
setprecision, setw).
sprintf – вывод в строку (stdio).
Этап 1. Создание многомодульного проекта. Раздел main.cpp. Меню, заглушки.
Тестирование, отладка первой версии.
Этап 2. Перемещение точки (перегрузка +), треугольника.
Этап 3. Поиск максимального по площади треугольника: перегрузка >, «не совсем
правильная» функция FindMax. Отладка (сбой): конструктор внутри функции,
модификация объекта, копирование по умолчанию. Напоминание о работе конструктора и
деструктора. Перегрузка операции присваивания, конструктор копирования.
Этап 4. Отношение включения. Алгоритм положения точки относительно вектора (5
вариантов) (самостоятельное изучение).
2. Наследование
Иерархия классов: производные классы получают элементы родительских, или
базовых, классов и могут дополнять или изменять их свойства. В начале иерархии –
наиболее общие черты для нижележащих классов. Класс может наследовать свойства 2-х
и более классов.
Ключи доступа
class
имя
[[,базовый_класс]]
{тело класса}
class
class
class
class
:[private
|
protected
|
public]
базовый_класс
A {…};
B {…};
C {…};
D: A, protected B, public C {…};
По умолчанию для классов ключ доступа private, для структур – public.
Обозначим
 public - 1
 protected - 2
 private – 3
Доступ в
произ. кл.
Базовый класс
(X)
1
2
3
Ключ
доступа
(Y)
1
2
3
1
2
-
2
2
-
3
3
-
max{X,Y}
Если ключ доступа private можно сделать поля базового класса доступными через
доступ к области видимости.
class Base {
…
public : void f();
…
};
class D: private Base {
…
public : Base::void f();
…
};
Простое наследование
Один родитель, конструкторы
деструкторы наследуются.
enum color{red, green, blue};
class monstr{
int health,ammo;
color skin;
и
операции
присваивания
не
наследуются,
char *name;
public:
monstr(int he=100, int am=10);
monstr(color sk);
monstr(char *nm);
monstr(monstr &M);
~monstr(){delete [] name;}
void draw( int x, int y, int scale, int position);
int get_health()
{return health;}
int get_ammo()
{return ammo;}
monstr & operator ++()
{ ++health; return *this;
}
monstr operator ++(int)
{monstr M(*this)
health++;
return M;
}
operator int()
{ return health;
}
bool operator >( monstr &M)
{ if (health > M.health)
return true;
return false;
}
const monstr& operator =(const monstr &M)
{
if (&M == this) return *this;
if (name) delete [] name;
if (M.name)
{
name = new char [strlenM.name) + 1];
strcpy(name, M.name);}
else name =0;
health = M.health;
ammo = M.ammo;
skin = M.skin;
return *this;
}
void change_health(int he)
{ health =he;
}
};
monstr::monstr(const monstr &M)
{health = M.health;
ammo = M.ammo;
skin= M.skin;
if (M.name)
{ name = new char [strlen(M.name)+1];
strcpy (name , M.name);
}
else name = 0;
}
monstr::monstr(int he, int am)
{health = he;
ammo = am;
skin = red;
name = 0;
}
monstr::monstr(color sk)
{switch (sk)
{
case red: health = 100; ammo = 10; skin = red;
name = 0; break;
case green: health = 100; ammo = 20; skin = green;
name = 0; break;
case blue: health = 100; ammo = 40; skin = blue;
name = 0; break;
}
}
monstr::monstr(char *nm)
{health = 100;
ammo = 10;
skin= red;
name = new char [strlen(nm)+1];
strcpy (name , nm);
}
void monstr::draw(int x, int y, int scale, int position)
{ cout <<x<< y<< scale<< position;}
class daemon : public monstr {
int brain;
public:
daemon (int br=100){brain = br};
daemon (color sk): monstr(sk){brain =10};
daemon (char *nm): monstr(nm){brain =10};
daemon (daemon &M) ): monstr(M){brain =10};
// ----------------const daemon & operator =(const daemon &M)
{
if (&M == this) return *this;
brain = M.brain;
monstr:: operator = (M);
return *this;
}
//--------------------void think();
void draw( int x, int y, int scale, int position);
};
void daemon:: think()
{…};
void daemon::draw(int x, int y, int scale, int position)
{ cout <<x<< y<< scale<< position;…}
Порядок вызова конструкторов:
1. Если в конструкторе производного класса нет явного вызова конструктора
базового класса, вызывается конструктор базового класса по умолчанию.
2. Конструкторы базовых классов вызываются, начиная с верхнего уровня.
Конструкторы элементов-объектов вызываются в порядке их объявления.
Последним вызывается конструктор класса.
3. При нескольких базовых классов конструкторы вызываются в порядке
объявления.
4. Если конструктор требует указания параметров, он должен быть явно вызван.
Вызов функций из базового класса предпочтительнее копированию.
Порядок наследования деструкторов:
1. Наследуются. Если не описан, формируется по умолчанию.
2. В деструкторе производного класса не требуется явного вызова деструктора
базового класса.
3. Деструкторы вызываются в порядке обратном вызову конструкторов.
Пример замещения функций (практикум)
#include <iostream>
using namespace std;
class Base {
public : void Display(){cout << “Hello, world !”<<endl;}
};
class Derived: public Base {
public :
void Display(){
Base:: void Display();
cout << “How are you ?” <<endl;}
};
class SubDerived: public Derived {
public :
void Display(){
Derived:: void Display();
cout << “Bye!” <<endl;}
};
int main()
{
SubDerived sd;
sd.Display();
return 0;
}
+++++++++++++++++++++
Hello, world!
How are you?
Bye!
Виртуальные методы
Работа с объектами часть осуществляется через указатели. При этом указателю на
базовый класс можно присвоить значение адреса объекта производного класса.
monstr *p;
p = new daemon;
p -> draw(1,1,1,1);
Вызов происходит в соответствии с типом указателя, а не фактическим типом
объекта, на который он ссылается, поэтому будет «нарисован» monstr, а не daemon. Этот
процесс - ранее связывание. Явное преобразование типа указателя
(daemon * p) -> draw(1,1,1,1)
не всегда возможно (параметр функции – указатель, список указателей …).
Позднее связывание через виртуальные методы.
virtual void draw( int x, int y, int scale, int position);





Если в базовом классе метод – виртуальный, то в производном классе с тем
же набором параметров – виртуальный, с отличающимся набором параметров
– обычный.
Виртуальные методы наследуются. Можно переопределять, при этом права
доступа изменить нельзя.
При переопределении, объекты производного класса могут получить доступ к
методу базового класса с помощью операции доступа к области видимости.
Виртуальный метод может быть friend , но не может быть static.
Если есть описание виртуального метода, то он должен быть определен хотя
бы как чисто виртуальный с признаком =0 вместо тела:
virtual void f(int) =0;
и он должен переопределяться в производном классе.
Для виртуального метода решение метод какого класса вызывать принимается в
зависимости от типа объекта, на который ссылается указатель.
monstr *p, *r;
r = new monstr;
p = new daemon;
r -> draw(1,1,1,1);
p -> draw(1,1,1,1);
p -> monstr:: draw(1,1,1,1);
Если объект производного класса вызывает виртуальный метод из другого метода
базового класса, то есть косвенно, то будет вызван метод производного класса.
Виртуальный – метод, ссылка на который разрешается на этапе выполнения
программы.
Для каждого класса с виртуальными методами создается таблица виртуальных
методов (vtbl) с адресами памяти виртуальных методов.
Каждый объект содержит срытое дополнительное поле ссылки (vptr)- заполняется
конструктором при создании объекта. При компиляции ссылки на методы – через vptr.
При выполнении адрес выбирается в момент обращения к методу.
Рекомендуется делать виртуальными деструкторы (возможны динамические
объекты, delete работает с size_t).
Объект, определенный через указатель или ссылку и содержащий виртуальные
методы – полиморфный.
Абстрактный класс – класс хотя бы с одним чисто виртуальным методом.
Объекты такого класса создавать нельзя.
 Нельзя использовать при явном приведении типов, для описания типа
параметра функции, типа возвращаемого функцией значения.
 Допускается объявлять ссылки и указатели на абстрактный класс, если при
инициализации не требуется создавать объект.
 Если в производном классе не определены все чисто виртуальные методы,
он – абстрактный.
Можно создавать функцию с параметрами – указателями на абстрактный класс, на
место которых при выполнении программы передается указатель на объект производного
класса. Получаются полиморфные функции.
Множественное наследование
Устранение неоднозначности
#include <fstream.h>
#include <iostream.h>
#include <stdlib.h>
class Base
{
protected:
int x;
public:
Base (int x1=0) { x = x1; }
};
class B: virtual public Base
{
public:
void AddB(int y) { x += y; }
};
class A: virtual public Base
{
public:
void AddA(int y) { x += 2*y; }
};
class D: public B, public A
{
public:
void Show() { cout << '='<< x<<endl; }
};
void main()
{
D d;
d.Show();
d.AddB(10);
d.Show();
d.AddA(10);
d.Show();
d.AddB(10);
d.Show();
}
Самостоятельная работа: разобрать задание из практикума «Функциональный
калькулятор»
3. Отношения между классами. Диаграммы класссов на языке
UML.
Язык UML – унифицированный язык моделирования является визуальным
средством представления моделей программ. Модель программы – ее графическое
представление в виде различных диаграмм, отражающих связи между объектами в
программном коде. Диаграммы используют для описания шаблонов проектирования
(design pattern), которые лежат в основе современного подхода к разработке
программного обеспечения. Одной из основных диаграмм является диаграмма классов,
описывающая классы и отражающая отношения между ними.
Класс изображается в виде прямоугольника, состоящего из 3-х частей, в которые
записываются:
в верхнюю часть – имя класса;
в среднюю – список полей (атрибутов), возможно, с указанием типов и значений;
в нижнюю – список методов (операций), возможно, с указанием списка типов
аргументов и типа возвращаемого значения.
Имя абстрактного класса и чисто виртуальных методов выделяются курсивом.
Перед именами могут стять символы ‘+’ – для public, ‘-’ – для private, ‘#’ – для
protected, ‘&’ – для static.
Элементы несущественные на данном этапе абстракции могут быть опущены.
Monstr
-ammo: int
-health: int
-name: char *
-skin: color
+monstr(he=100:int, am=10:int)
+monstr(color)
+monstr(char*)
+monstr(monstr &)
+ void draw( )
Отношения между классами – это
 Ассоциация – взаимодействие 2-х классов друг с другом. Над концами линии,
соединяющей классы, может быть поставлено число (или *) для обозначения,
сколько объектов (произвольное число) данного класса связано с одним
объектов другого.
квартира

1..*
дом
Ассоциация – наиболее абстрактная связь, которая в дальнейшем
конкретизируется
Наследование – отношение обобщения («is a …»), которое обозначается
линией со стрелкой в виде незакрашенного треугольника, направленной на
базовый класс.
BaseClass
SunB1

1
SunB2
Агрегация обозначает, что один класс содержит в качестве составной части
объекты другого класса («has a …»), то есть «целое - часть». Эта связь
помечается стрелкой в виде ромба, указывающей на «целое». Ромб не
закрашивается для нестрогой агрегации, когда «целое» может содержать не
все объекты на схеме.
Гараж
легковой
автомобиль
Автобус
Грузовик
В реализации на С++ удобно использовать в качестве полей указатели на
компоненты («части»). Если компонент отсутствует, соответствующий
указатель равен 0.
Для строгой агрегации, называемой композицией, при которой компонент не
может исчезнуть пока «целое» существует, ромб закрашивается.
Triangle

3
Point
Композицию проще всего реализовывать включением объектов компонентов
по значению. При включении по ссылке время существования компонентов
должно перекрывать время жизни объекта-«целое».
Зависимость- отношения использования , когда один класс (клиент)
пользуется услугами другого (сервер). Это происходит когда
a) метод клиента использует значения некоторых полей сервера;
b) метод клиента вызывает метод сервера;
c) метод клиента имеет сигнатуру, в которой упоминается сервер.
Эта связь помечается пунктирной стрелкой, указывающей на «сервер».
Triangle
Point
Проектирование программы с учетом будущих изменений.
Удачная декомпозиция характеризуется тем, что отдельные компоненты имеют
сильное внутреннее сцепление и слабую внешнюю связанность (это упоминалось в начале
представления курса).
Добавление новой функциональности довольно сложно выполнить в рамках
структурного программирования. Одной из причин этого для процедурнофункциональных систем – использование конструкций на базе оператора выбора switch:
typedef enum {m1, m2, m3} ModeType
void func( ModeType m)
{
switch (m)
{
case m1: Func1(); break;
…
case m3: Func3();
}
}
В результате, перечень значений выборки заранее фиксирован и вся система
разрабатывается с использованием ModeType. Добавление новой возможности для
ModeType приводит к необходимости просмотра всего кода. Соответственно, требуется
новое полное тестирование продукта.
В ООП грамотное использование полиморфизма и композиции классов позволяют
решить проблему (причем использование «чистого» наследования не достаточно гибко).
Самые удачные решения обобщаются в новом направлении, называемом шаблонами
проектирования (design pattern). Пионерская работа принадлежит «банде четырех» (Э.
Гамма (E.Gamma), Р. Хелм (R.Helm), Р Джонсон (R.Johnson), Дж. Влисседес (J.Vlissides)).
В этой работе - GoF95 (Gang of Four, 1995) – были приведены 23 шаблона, предсталяющие
собой диаграммы классов для решения часто встречающихся проблем с поясненими (имя,
методы, примеры).
При создании проекта важен совет: «Найдите то, что может измениться в вашем
дизайне и инкапсулируйте сущности, подверженные изменениям!» В нашем примере
сущностями являются вызываемые функции. Следовательно, надо реализовывать
функции как виртуальные методы полиморфных объектов. Соответствующая идея
состоит в том, что объект класса «переключатель» имеет дело с абстракцией «функция
вообще», реализованной через абстрактный класс (AbstrEn) с чисто виртуальным методом
(Func). Конкретные функции – это одноименные методы в производных классах.
«Переключатель» имеет информацию об адресах объектов этих (производных) классов,
например, через список адресов (поле «Переключателя»), расположенных в векторе.
Переключатель
pAE: std::vector< AbstrEn *>
Select():AbstrEn*
К
л
и
е
н
т
Адреса объектов:
En1,
En2,
…
1..*
AbstrEn
Func()
En1
En2
En3
Func()
Func()
Func()
4. Шаблоны классов.
Шаблон - это предписание для создания класса, в котором один или несколько типов
либо значений параметризованы.
Предположим, что нам нужно определить класс, поддерживающий механизм
очереди. Очередь - это структура данных для хранения коллекции объектов; они
помещаются в конец очереди, а извлекаются из ее начала. Поведение очереди описывают
аббревиатурой FIFO - "первым пришел, первым ушел".
Необходимо, чтобы наш класс Queue поддерживал следующие операции:
1. добавить элемент в конец очереди:
void add( item );
2. удалить элемент из начала очереди:
item remove();
3. определить, пуста ли очередь:
bool empty();
Определение Queue могло бы выглядеть так:
class Queue {
public:
Queue();
~Queue();
Type& remove();
void add( const Type & );
bool empty();
private:
// ...
};
Вопрос в том, какой тип использовать вместо Type? Предположим, что мы решили
реализовать класс Queue, заменив Type на int. Тогда Queue может управлять коллекциями
объектов типа int. Если бы понадобилось поместить в очередь объект другого типа, то его
пришлось бы преобразовать в тип int, если же это невозможно, компилятор выдаст
сообщение об ошибке.
Поскольку любой объект в коллекции имеет тип int, то язык C++ гарантирует, что в
очередь можно поместить либо значение типа int, либо значение, преобразуемое в такой
тип. Это подходит, если предстоит работа с очередями объектов только типа int. Если же
класс Queue должен поддерживать также коллекции объектов типа double, char,
комплексные числа или строки, подобная реализация оказывается слишком
ограничительной.
Эту проблему можно решить, создав копию класса Queue для работы с типом double,
затем для работы с комплексными числами, затем со строками и т.д. А поскольку имена
классов перегружать нельзя, каждой реализации придется дать уникальное имя: IntQueue,
DoubleQueue, ComplexQueue, StringQueue. При необходимости работать с другим классом
придется снова копировать, модифицировать и переименовывать.
Такой метод дублирования кода крайне неудобен. Создание различных уникальных
имен для Queue представляет лексическую сложность. Имеются и трудности
администрирования: любое изменение общего алгоритма придется вносить в каждую
реализацию класса. В общем случае процесс ручной генерации копий для
индивидуальных типов никогда не кончается и очень сложен с точки зрения
сопровождения.
К счастью, механизм шаблонов C++ позволяет автоматически генерировать такие
типы. Шаблон класса можно использовать при создании Queue для очереди объектов
любого типа. Определение шаблона этого класса могло бы выглядеть следующим
образом:
template <class Type>
class Queue {
public:
Queue();
~Queue();
Type& remove();
void add( const Type & );
bool is_empty();
bool is_full();
private:
// ...
};
Чтобы создать классы Queue, способные хранить целые числа, комплексные числа и
строки, программисту достаточно написать:
Queue<int> qi;
Queue<complex<double>> qc;
Queue qs;
В реализации Queue используются две абстракции шаблона:
1.
сам шаблон класса Queue предоставляет описанный выше открытый
интерфейс и пару членов: front и last. Очередь реализуется с помощью связанного списка;
2.
шаблон класса QueueItem представляет один узел связанного списка Queue.
Каждый помещаемый в очередь элемент сохраняется в объекте QueueItem, который
содержит два члена: value и next. Тип value будет различным в каждом экземпляре класса
Queue, а next - это всегда указатель на следующий объект QueueItem в очереди.
Прежде чем приступать к детальному изучению реализации этих шаблонов,
рассмотрим, как они объявляются и определяются. Вот объявление шаблона класса
QueueItem:
template <class T>
class QueueItem;
Как объявление, так и определение шаблона всегда начинаются с ключевого слова
template. За ним следует заключенный в угловые скобки список параметров шаблона,
разделенных запятыми. Список не бывает пустым. В нем могут быть параметры-типы,
представляющие некоторый тип, и параметры-константы, представляющие некоторое
константное выражение.
Параметр-тип шаблона состоит из ключевого слова class или typename (в списке
параметров они эквивалентны), за которым следует идентификатор. (Ключевое слово
typename не поддерживается компиляторами, написанными до принятия стандарта C++. )
Оба ключевых слова обозначают, что последующее имя параметра относится к
встроенному или определенному пользователем типу. Например, в приведенном выше
определении шаблона QueueItem имеется один параметр-тип T. Допустимым фактическим
аргументом для T является любой встроенный или определенный пользователем тип,
например, int, double, char*, complex или string.
У шаблона класса может быть несколько параметров-типов:
template <class T1, class T2, class T3>
class Container;
Однако ключевое слово class или typename должно предшествовать каждому.
Следующее объявление ошибочно:
template <typename T, U>
class collection; // ошибка: должно быть <typename T, class U> или
//
<typename T, typename U>
Объявленный параметр-тип служит спецификатором типа в оставшейся части
определения шаблона и употребляется точно так же, как любой встроенный или
определенный пользователем тип в обычном определении класса. Например, параметртип можно использовать для объявления данных и функций-членов, членов вложенных
классов и т.д.
Не являющийся типом параметр шаблона представляет собой обычное объявление.
Он показывает, что следующее за ним имя - это потенциальное значение, употребляемое в
определении шаблона в качестве константы. Так, шаблон класса Buffer может иметь
параметр-тип, представляющий типы элементов, хранящихся в буфере, и параметрконстанту, содержащий его размер:
template <class Type, int size>
class Buffer;
За списком параметров шаблона следует определение или объявление класса.
Шаблон определяется так же, как обычный класс, но с указанием параметров:
template <class Type>
class QueueItem {
public:
// ...
private:
// Type представляет тип члена
Type item;
QueueItem *next;
};
В этом примере Type используется для обозначения типа члена item. По ходу
выполнения программы вместо Type могут быть подставлены различные встроенные или
определенные пользователем типы. Такой процесс подстановки называется
конкретизацией шаблона.
Имя параметра шаблона можно употреблять после его объявления и до конца
объявления или определения шаблона. Если в глобальной области видимости объявлена
переменная с таким же именем, как у параметра шаблона, то это имя будет скрыто. В
следующем примере тип item равен не double, а типу параметра:
typedef double Type;
template <class Type>
class QueueItem {
public:
// ...
private:
// тип Item - не double
Type item;
QueueItem *next;
};
Член класса внутри определения шаблона не может быть одноименным его
параметру:
template >class Type>
class QueueItem {
public:
// ...
private:
typedef double Type; // ошибка: член не может иметь то же
//имя, что и параметр шаблона Type
Type item;
QueueItem *next;
};
Имя параметра шаблона может встречаться в списке только один раз. Поэтому
следующее объявление компилятор помечает как ошибку:
// ошибка: неправильное использование имени параметра шаблона Type
template <class Type, class Type>
class container;
Такое имя разрешается повторно использовать в объявлениях или определениях
других шаблонов:
// правильно: повторное использование имени Type в разных шаблонах
template <class Type>
class QueueItem;
template <class Type>
class Queue;
Имена параметров в опережающем объявлении и последующем определении одного
и того же шаблона не обязаны совпадать. Например, все эти объявления QueueItem
относятся к одному шаблону класса:
// все три объявления QueueItem
// относятся к одному и тому же шаблону класса
// объявления шаблона
template <class T> class QueueItem;
template <class U> class QueueItem;
// фактическое определение шаблона
template <class Type>
class QueueItem { ... };
У параметров могут быть аргументы по умолчанию (это справедливо как для
параметров-типов, так и для параметров-констант) - тип или значение, которые
используются в том случае, когда при конкретизации шаблона фактический аргумент не
указан. В качестве такого аргумента следует выбирать тип или значение, подходящее для
большинства конкретизаций. Например, если при конкретизации шаблона класса Buffer не
указан размер буфера, то по умолчанию принимается 1024:
template <class Type, size = 1024>
class Buffer;
В последующих объявлениях шаблона могут быть заданы дополнительные
аргументы по умолчанию. Как и в объявлениях функций, если для некоторого параметра
задан такой аргумент, то он должен быть задан и для всех параметров, расположенных в
списке правее (даже в другом объявлении того же шаблона):
template <class Type, size = 1024>
class Buffer;
// правильно: рассматриваются аргументы по умолчанию из обоих объявлений
template <class Type=string, int size>
class Buffer;
Отметим, что аргументы по умолчанию для параметров шаблонов не
поддерживаются в компиляторах, реализованных до принятия стандарта C++.
Внутри определения шаблона его имя можно применять как спецификатор типа
всюду, где допустимо употребление имени обычного класса. Вот более полная версия
определения шаблона QueueItem:
template <class Type>
class QueueItem {
public:
QueueItem( const Type & );
private:
Type item;
QueueItem *next;
};
Обратите внимание, что каждое появление имени QueueItem в определении шаблона
- это сокращенная запись для QueueItem<Type>.
Такую сокращенную нотацию можно употреблять только внутри определения
QueueItem (и, как мы покажем в следующих разделах, в определениях его членов,
которые находятся вне определения шаблона класса). Если QueueItem применяется как
спецификатор типа в определении какого-либо другого шаблона, то необходимо задавать
полный список параметров. В следующем примере шаблон класса используется в
определении шаблона функции display. Здесь за именем шаблона класса QueueItem
должны идти параметры, т.е. QueueItem<Type>.
template <class Type>
void display( QueueItem<Type> &qi )
{
QueueItem<Type> *pqi = &qi;
// ...
}
4.1. Определения шаблонов классов Queue и QueueItem
Ниже представлено определение шаблона класса Queue. Оно помещено в
заголовочный файл Queue.h вместе с определением шаблона QueueItem:
#ifndef QUEUE_H
#define QUEUE_H
// объявление QueueItem
template <class T> class QueueItem;
template <class Type>
class Queue {
public:
Queue() : front(0), last(0){}
~Queue();
Type& remove();
void add(const Type&);
bool is_empty() const {
return front == 0;
}
private:
QueueItem<Type> *front;
QueueItem<Type> *last;
};
#endif
При использовании имени Queue внутри определения шаблона класса Queue список
параметров <Type> можно опускать. Однако пропуск списка параметров шаблона
QueueItem в определении шаблона Queue недопустим. Так, объявление члена front
является ошибкой:
template <class Type>
class Queue {
public:
// ...
private:
// ошибка: список параметров для QueueItem неизвестен
QueueItem *front;
}
Упражнение 1.1
Найдите ошибочные объявления (или пары объявлений) шаблонов классов:
(a) template <class Type>
class Container1;
template <class Type, int size>
class Container1;
(b) template <class T, U, class V>
class Container2;
(c) template <typename myT, class myT>
class Container3 {};
(d) template <class Type, int *pi>
class Container4;
(e) template <class Type, int val = 0>
class Container6;
template <class T = complex<double>, int v>
class Container6;
Упражнение 1.2
Следующее определение шаблона List некорректно. Как исправить ошибку?
template <class elemenType>
class ListItem;
template <class elemType>
class List {
public:
List<elemType>(): _at_front(0), _at_end(0), _current(0), _size(0)
{}
List<elemType>( const List<elemType> & );
List<elemType>& operator=( const List<elemType> & );
~List();
void insert(ListItem<elemType> *ptr, elemType value);
int remove( elemType value );
ListItem *find( elemType value );
void display( ofstream &os = cout );
int size() { return _size; }
private:
ListItem<elemType> *_at_front;
ListItem<elemType> *_at_end;
ListItem<elemType> *_current;
int _size;
};
4.2. Конкретизация шаблона класса
В определении шаблона указывается, как следует строить индивидуальные классы,
если заданы один или более фактических типов или значений. По шаблону Queue
автоматически генерируются экземпляры классов Queue с разными типами элементов.
Например, если написать: Queue<int> qi; -то из обобщенного определения шаблона
автоматически создается класс Queue для объектов типа int.
Генерация конкретного класса из обобщенного определения шаблона называется
конкретизацией (или инстанцированием) шаблона. При такой конкретизации Queue для
объектов типа int каждое вхождение параметра Type в определении шаблона заменяется на
int, так что определение класса Queue принимает вид:
template <class int>
class Queue {
public:
Queue(): front(0), last(0){}
~Queue();
int& remove();
void add( const int & );
bool is_empty() const{
return front == 0;
}
private:
QueueItem<int> *front;
QueueItem<int> *last;
};
Чтобы создать класс Queue для объектов типа string, надо написать:
Queue<string> qs;
При этом каждое вхождение Type в определении шаблона будет заменено на string.
Объекты qi и qs являются объектами автоматически созданных классов.
Каждый конкретизированный по одному и тому же шаблону экземпляр класса
совершенно не зависит от всех остальных. Так, у Queue для типа int нет никаких прав
доступа к неоткрытым членам того же класса для типа string.
Конкретизированный экземпляр шаблона будет иметь соответственно имя
Queue<int> или Queue<string>. Части <int> и <string>, следующие за именем Queue,
называются фактическими аргументами шаблона. Они должны быть заключены в угловые
скобки и отделяться друг от друга запятыми. В имени конкретизируемого шаблона
аргументы всегда должны задаваться явно. В отличие от аргументов шаблона функции
(когда по параметру можно определить тип), аргументы шаблона класса никогда не
выводятся из контекста.
Конкретизированный шаблон класса Queue можно использовать в программе всюду,
где допустимо употребление типа обычного класса:
// типы возвращаемого значения и обоих параметров конкретизированы из
// шаблона класса Queue
extern Queue<complex<double>>;
Queue<complex<double>>& slianie(Queue<complex<double>>&,
Queue<complex<double>>&);
Объекты класса, конкретизированного по шаблону Queue,
используются так же, как объекты обычных классов:
объявляются и
extern Queue<double> eqd;
Queue<int> *pqi = new Queue<int>;
Queue<int> aqi[1024];
// объявление шаблона функции
template <class Type>
void bar(Queue<Type> &,
// ссылается на обобщенный шаблон
Queue<double> &); // ссылается на конкретизированный шаблон
Однако вне такого определения употребляются только конкретизированные
экземпляры. Например, в теле обычной функции всегда надо задавать фактические
аргументы шаблона Queue:
void foo2(Queue<int> &qi ){
Queue<int> *pqi = &qi;
…
}
Шаблон класса конкретизируется только тогда, когда имя объекта требует
определения шаблона. Не всегда определение класса должно быть известно. Например,
перед объявлением указателей и ссылок на класс его знать необязательно:
class Matrix;
Matrix *pm;
// правильно: определение класса Matrix знать необязательно
void inverse( Matrix & ); // тоже правильно
Поэтому объявление указателей и ссылок на шаблон класса не приводит к его
конкретизации. (Отметим, что в некоторых компиляторах, написанных до принятия
стандарта C++, шаблон конкретизируется при первом упоминании имени класса в тексте
программы.) Так, в функции foo2() объявляются указатель и ссылка на Queue, но это не
вызывает конкретизации шаблона Queue.
Определение класса необходимо знать, когда определяется объект этого типа. В
следующем примере определение obj1 ошибочно: чтобы выделить для него память,
компилятору необходимо знать размер класса Matrix:
class Matrix;
Matrix obj1;
// ошибка: класс Matrix не определен
class Matrix { ... };
Matrix obj2; // правильно
Таким образом, конкретизация происходит тогда, когда определяется объект класса,
конкретизируемый по этому шаблону. Эта «точка» называется точкой конкретизации
данного класса.
Если имеется указатель или ссылка на конкретизированный шаблон, то
конкретизация производится в момент обращения к объекту, на который они ссылаются.
В определенной выше функции foo2() класс Queue конкретизируется в следующих
случаях: когда разыменовывается указатель pqi (ссылка qi используется для получения
значения именуемого объекта) и когда pqi или qi употребляются для доступа к членам или
функциям-членам этого класса:
void foo2(Queue<int> &qi)
{
Queue<int> *pqi = &qi;
//Queue конкретизируется в результате вызова функции-члена
pqi->add(255);
…
}
Определение Queue<int> становится известным компилятору еще до вызова
функции-члена add() из foo2().
Напомним, что в определении шаблона класса Queue есть также ссылка на шаблон
QueueItem:
template <class Type>
class Queue {
public:
// ...
private:
QueueItem<Type> *front;
QueueItem<Type> *last;
};
При конкретизации Queue типом int члены front и last становятся указателями на
QueueItem. Следовательно, конкретизированный экземпляр Queue<int> ссылается на
экземпляр QueueItem, конкретизированный типом int. Но поскольку соответствующие
члены являются указателями, то QueueItem<int> конкретизируется лишь в момент их
разыменования в функциях-членах класса Queue<int>.
Наш класс QueueItem служит вспомогательным средством для реализации класса
Queue и не будет непосредственно употребляться в вызывающей программе. Поэтому
пользовательская программа способна манипулировать только объектами Queue.
Конкретизация шаблона QueueItem происходит лишь в момент конкретизации шаблона
класса Queue или его членов. (Далее рассмотрим конкретизации членов шаблона класса.)
В зависимости от типов, которыми может конкретизироваться шаблон, при его
определении надо учитывать некоторые нюансы. Почему, например, следующее
определение конструктора класса QueueItem не подходит для конкретизации общего вида?
template <class Type> class …
Если конкретизация производится для объемного типа (скажем, Matrix), то
накладные расходы могут стать неприемлемыми. Поэтому аргумент конструктора
объявляется как ссылка на константный тип:
QueueItem( const Type & );
Следующее определение приемлемо, если у типа, для которого конкретизируется
QueueItem, нет ассоциированного конструктора:
template <class Type>
class QueueItem {
// ...
public:
// потенциально неэффективно
QueueItem(const Type &t ) {
item = t; next = 0;
}
};
Если аргументом шаблона является тип класса с конструктором (например, string),
то item инициализируется дважды! Конструктор по умолчанию string вызывается для
инициализации item перед выполнением тела конструктора QueueItem. Затем для
созданного объекта item производится почленное присваивание. Избежать такого можно с
помощью явной инициализации item в списке инициализации членов внутри определения
конструктора QueueItem:
template <class Type>
class QueueItem {
// ...
public:
//item инициализируется в списке инициализации членов конструктора
QueueItem( const Type &t )
: item(t) {next = 0;}
};
4.3. Аргументы шаблона для параметров-констант
Параметр шаблона класса может и не быть типом. На аргументы, подставляемые
вместо таких параметров, накладываются некоторые ограничения.
template <int hi, int wid>
class Screen {
public:
Screen(): height(hi), width(wid), cursor(0)
{ }
// ...
private:
int cursor;
short height;
short width;
};
typedef Screen<24,80> termScreen;
termScreen scr;
Screen<8,24> scr2;
Выражение, с которым связан параметр, не являющийся типом, должно быть
константным, т.е. вычисляемым во время компиляции. В примере выше typedef termScreen
ссылается на экземпляр шаблона Screen<24,80>, где аргумент шаблона для hi равен 24, а
для wid - 80. В обоих случаях аргумент - это константное выражение.
Однако для шаблона BufPtr конкретизация приводит к ошибке, так как значение
указателя, получающееся при вызове оператора new(), становится известно только во
время выполнения:
template <int *ptr> class BufPtr {...};
// ошибка: аргумент шаблона нельзя вычислить во время компиляции
BufPtr<new int[24]> bp;
Не является константным выражением и значение неконстантного объекта. Его
нельзя использовать в качестве аргумента для параметра-константы шаблона. Однако
адрес любого объекта в области видимости пространства имен, в отличие от адреса
локального объекта, является константным выражением (даже если спецификатор const
отсутствует), поэтому его можно применять в качестве аргумента для параметраконстанты. Константным выражением будет и значение оператора sizeof:
template <int size> Buf {...};
template <int *ptr> class BufPtr {...};
int size_v = 512;
const int c_size_v = 1024;
Buf<1024> buf0;
// правильно
Buf<c_size_v> buf1; // правильно
Buf<sizeof(size_v) > buf2; // правильно: sizeof(int)
BufPtr<&size_v> bp0; // правильно
// ошибка: нельзя вычислить во время компиляции
Buf<size_v> buf3;
Вот еще один пример, иллюстрирующий использование параметра-константы для
представления константного значения в определении шаблона, а также применение его
аргумента для задания значения этого параметра:
template <class Type, int size>
class FixArray {
public:
FixArray(Type *ar): count(size)
{
for(int ix = 0; ix < size; ++ix)
array[ix] = ar[ix];
}
private:
Type array[size];
int count;
};
int ia[4] = {0, 1, 2, 3};
FixedArray<int, sizeof(ia)/sizeof(int)> iA(ia);
Выражения с одинаковыми значениями считаются эквивалентными аргументами для
параметров-констант шаблона. Так, все три экземпляра Screen ссылаются на один и тот же
конкретизированный из шаблона класс Screen<24,80>:
const int width = 24;
const int height = 80;
// все далее Screen< 24, 80 >
Screen<2*12, 40*2> scr0;
Screen<6+6+6+6, 20*2 + 40> scr1;
Screen<width, height> scr2;
Между типом аргумента шаблона и типом параметра-константы допустимы
некоторые преобразования. Их множество является подмножеством преобразований,
допустимых для аргументов функции. Рассмотрим следующие объявления:
extern void foo( char * );
extern void bar( void * );
typedef void (*PFV)( void * );
const unsigned int x = 1024;
template <class Type,
unsigned int size,
PFV handler> class Array { ... };
Array<int,
Array<int,
Array<int,
Array<int,
1024U, bar> a0; // правильно: преобразование не нужно
1024U, foo> a1; // ошибка: foo != PFV
1024, bar> a2; // правильно: 1024 преобразуется в unsigned int
x, bar> a3;
// правильно: преобразование не нужно
Объекты a0 и a3 класса Array определены правильно, так как аргументы шаблона
точно соответствуют типам параметров. Объект a2 также определен правильно, потому
что аргумент 1024 типа int приводится к типу unsigned int параметра-константы size с
помощью преобразования целых типов. Объявления a1, так как не существует
преобразования между любыми двумя типами функций.
Приведение значения 0 целого типа к типу указателя недопустимо:
template <int *ptr>
class BufPtr { ... };
BufPtr<0> nil; // ошибка: 0 имеет тип int
// неявное преобразование в нулевой указатель не применяется
4.4. Функции-члены шаблонов классов
Как и для обычных классов, функция-член шаблона класса может быть определена
либо внутри определения шаблона (и тогда называется встроенной), либо вне его.
Конструктор Queue является встроенным, так как определен внутри определения шаблона
класса:
template < class Type>
class Queue {
// ...
public:
// встроенный конструктор
Queue(): front(0), last(0){ }
// ...
};
При определении функции-члена шаблона вне определения самого шаблона следует
применять специальный синтаксис для обозначения того, членом какого именно шаблона
является функция. Определению функции-члена должно предшествовать ключевое слово
template, за которым следуют параметры шаблона. Так, конструктор Queue можно
определить следующим образом:
template < class Type>
class Queue {
public:
Queue();
private:
// ...
};
template < class Type>
inline Queue< Type> ::
Queue( ) { front = last = 0; }
За первым вхождением Queue (перед оператором ::) следует список параметров,
показывающий, какому шаблону принадлежит данная функция-член. Второе вхождение
Queue в определение конструктора (после оператора ::) содержит имя функции-члена, за
которым может следовать список параметров шаблона, хотя это и необязательно. После
имени функции идет ее определение;. в нем могут быть ссылки на параметр шаблона Type
всюду, где в определении обычной функции использовалось бы имя типа.
Функция-член шаблона класса сама является шаблоном. Стандарт C++ требует,
чтобы она конкретизировалась только при вызове либо при взятии ее адреса. (Некоторые
более старые компиляторы конкретизируют такие функции одновременно с
конкретизацией самого шаблона класса.) При конкретизации функции-члена используется
тип того объекта, для которого функция вызвана: Queue< string> qs;
Объект qs имеет тип Queue<string>. При инициализации объекта этого класса
вызывается конструктор Queue<string>. В данном случае аргументом, которым
конкретизируется функция-член (конструктор), будет string.
От того, в какой именно момент конкретизируется функция-член, зависит
разрешение имен в ее определении и объявление ее специализации.
Чтобы понять, как определяются и используются функции-члены шаблонов классов,
продолжим изучение шаблонов Queue и QueueItem:
template <class Type>
class Queue {
public:
Queue(): front(0), last(0) { }
~Queue();
Type& remove();
void add( const Type & );
bool empty() const {
return front == 0;
}
private:
QueueItem< Type> *front;
QueueItem< Type> *last;
};
Деструктор, а также функции-члены remove() и add() определены не в теле шаблона,
а вне его. Деструктор Queue опустошает очередь:
template < class Type>
Queue< Type> ::~Queue()
{
while (! is_empty() )
remove();
}
Функция-член Queue< Type> ::add() помещает новый элемент в конец очереди:
template <class Type> void Queue< Type> ::add( const Type &val )
{
// создать новый объект QueueItem
QueueItem< Type> *pt = new QueueItem< Type>(val);
if ( is_empty() )
front = last = pt;
else
{
last-> next = pt;
last = pt;
}
}
Функция-член Queue<Type>:: remove() возвращает значение элемента, находящегося
в начале очереди, и удаляет сам элемент.
#include < iostream>
#include < stвdlib>
template < class Type> Type Queue< Type> ::remove()
{
if (is_empty() )
{
cerr << "remove() вызвана для пустой очереди\n";
exit(1);
}
QueueItem< Type> *pt = front;
front = front-> next;
Type retval = pt->item;
delete pt;
return retval;
}
Поместим определения функций-членов в заголовочный файл Queue.h, включив его
в каждый файл, где возможны конкретизации функций
В следующей программе иллюстрируется использование и конкретизация функциичлена шаблона Queue:
#include < iostream>
#include "Queue.h"
int main()
{
// конкретизируется класс Queue< int>
// оператор new требует, чтобы Queue< int>
был определен
Queue< int> *p_qi = new Queue< int>;
int i;
for (i = 0; I < 10; ++i)
// конкретизируется функция-член add()
p_qi-> add(i);
int err_cnt = 0;
for(i = 0; i < 10; ++i) {
// конкретизируется функция-член remove()
int q = p_qi-> remove();
if (i != q) err_cnt++;
}
if (!err_cnt)
cout < < "!! queue executed ok\n";
else cerr <<
"?? queue errors: " < < err_cnt <<
return 0;
endl;
}
После компиляции и запуска программа выводит следующую строку:
!! queue executed ok.
В шаблоне класса могут быть объявлены статические данные-члены. Каждый
конкретизированный экземпляр имеет собственный набор таких членов.
Определение шаблона статического члена должно быть вынесено за пределы
определения самого шаблона класса, которое начинается с ключевого слово template с
последующим списком параметров. Определения таких членов помещаются в
заголовочный файл Queue.h и должны включаться во все файлы, где производится их
конкретизация. Статический член конкретизируется по шаблону только в том случае,
когда реально используется в программе. Сам такой член тоже является шаблоном.
Определение шаблона для него не приводит к выделению памяти: она выделяется только
для конкретизированного экземпляра статического члена. Каждая подобная
конкретизация соответствует конкретизации шаблона класса. Таким образом, обращение к
экземпляру
статического
члена
всегда
производится
через
некоторый
конкретизированный экземпляр класса.
5. Обработка исключительных ситуаций
Исключительная ситуация, или исключение — это возникновение непредвиденного
или аварийного события, которое может порождаться некорректным использованием
аппаратуры. Например, это деление на ноль или обращение по несуществующему адресу
памяти. Обычно эти события приводят к завершению программы с системным
сообщением об ошибке. C++ дает программисту возможность восстанавливать программу
и продолжать ее выполнение.
5.1. Общий механизм обработки исключений.
Место, в котором может произойти ошибка, должно входить в контролируемый блок
— составной оператор, перед которым записано ключевое слово try. Рассмотрим, каким
образом реализуется обработка исключительных ситуаций.
• Обработка исключения начинается с появления ошибки. Функция, в которой она
возникла, генерирует исключение. Для этого используется ключевое слово throw с
параметром, определяющим вид исключения. Параметр может быть константой,
переменной или объектом и используется для передачи информации об исключении его
обработчику.
• Отыскивается соответствующий обработчик исключения и ему передается
управление.
• Если обработчик исключения не найден, программа завершается аварийно, как
обычно. То есть вызывается функция terminate, которая вызывает функцию abort (при
этом можно вставлять собственную функцию).
Т.е. механизм исключений базируется на словах try, throw и catch.
Пример.
#include <iostream.h>
int factorial(int n)
{
if (n<0)
throw string(“Аргумент отрицателен”);
int temp=1;
while (n>0)
{
temp*=n;
n--;
}
return temp;
}
void main()
{
try
{
cout<<”3! = “<<factorial(3)<< endl;
cout<<”-1! = “<<factorial(-1)<< endl;
cout<<”5! = “<<factorial(5)<< endl;
}
catch(string error)
{
cout<<”Ошибка”<<endl;
}
}
5.2 . Синтаксис исключений
Ключевое слово try служит для обозначения контролируемого блока — кода, в
котором может генерироваться исключение. Блок заключается в фигурные скобки:
try{ …}
Генерация (порождение) исключения происходит по ключевому слову throw, которое
употребляется либо с параметром, либо без него:
throw [ выражение ];
Тип выражения, стоящего после throw, определяет тип порождаемого исключения.
При генерации исключения выполнение текущего блока прекращается, и происходит
поиск соответствующего обработчика и передача ему управления.
Обработчики исключений начинаются с ключевого слова catch, за которым в скобках
следует тип обрабатываемого исключения. Они должны располагаться непосредственно за
try-блоком. Можно записать один или несколько обработчиков в соответствии с типами
обрабатываемых исключений. Синтаксис обработчиков напоминает определение функции
с одним параметром — типом исключения. Существует три формы записи:
catch(Tип имя){ .. . /* тело обработчика */ }
catch(Tип){ .. . /* тело обработчика */ }
catch(...){ . . /* тело обработчика */ }
Первая форма применяется, когда имя параметра используется в теле обработчика
для выполнения каких-либо действий — например, вывода информации об исключении.
Вторая форма не предполагает использования информации об исключении, играет роль
только его тип. Многоточие вместо параметра обозначает, что обработчик перехватывает
все исключения. Так как обработчики просматриваются в том порядке, в котором они
записаны, обработчик третьего типа следует помещать после всех остальных.
5.3. Перехват исключений
Когда с помощью throw генерируется исключение, функции исполнительной
библиотеки C++ выполняют следующие действия:
1) создают копию параметра throw в виде статического объекта, который существует
до тех пор, пока исключение не будет обработано;
2) в поисках подходящего обработчика раскручивают стек, вызывая деструкторы
локальных объектов, выходящих из области действия;
3) передают объект и управление обработчику, имеющему параметр, совместимый
по типу с этим объектом.
При раскручивании стека все обработчики на каждом уровне просматриваются
последовательно, от внутреннего блока к внешнему, пока не будет найден подходящий
обработчик.
Пример.
#include <iostream.h>
class Obj
{
public:
Obj (char c)
{
label = c;
cout << "Конструируем объект " << label << endl;
}
~Obj();
{
сout << "Ликвидируем объект " << label << endl;
}
protected:
char label;
};
void fl() ;
void f2();
void main()
{
Obj a('a');
try
{
Obj b('b');
fl();
}
catch(float f)
{
cout << "Исключение float" << endl;
}
catch(int i)
{
cout << "Исключение int" << endl;
}
catch (... )
{
cout << "Исключение..." << endl;
}
}
void f1()
{
try
{
Obj c('c');
f2 ();
}
catch(char* pMsg)
{
cout << " Исключение char*" << endl;
}
}
void f2()
{
Obj d('d') ;
throw 10;
}
В результате работы этой программы на экран будет выведен следующий текст:
Конструируем объект а
Конструируем объект Ь
Конструируем объект с
Конструируем объект d
Ликвидируем объект d
Ликвидируем объект с
Ликвидируем объект b
Исключение int
Ликвидируем объект а
//main
//try
//f1
//f2
//выход
//выход
//выход
//catch
//выход
из f2 по throw int
из f1
из try
int
из main
Пример.
#include <fstream>
#include <iostream>
using namespace std;
class Hello
{
public:
Hello () {cout<< “Hello!”<<endl;}
~Hello () {cout<< “Hello!”<<endl;}
}
void fl()
{
ifstream ifs(“test.dat”);
if (!ifs)
{
cout << “Генерируем исключение” << endl;
throw “Ошибка открытия файла ”;
}
}
void f2()
{
Hello h;
f1();
}
int main()
{
try
{
cout << “In try” << endl;
f2();
cout << “From try” << endl;
}
catch(int i)
{
cout << "Исключение int" << endl;
}
catch(const char *p)
{
cout << "Исключение const char* - " <<p<< endl;
return 1;
}
catch (... )
{
cout << "Исключение..." << endl;
}
return 0;
}
В результате работы этой программы на экран будет выведен следующий текст:
In try
Hello!
Генерируем исключение
Bye!
Исключение const char* -
//
//конструктор
// f1
//выход из f1, деструктор Hello
Ошибка открытия файла
//
Итак, за ключевым словом throw следует выражение, которое создает объект
некоторого типа: throw работает с любым типом объекта. Это значит, что вы можно
"бросать" любое количество информации. Рассмотрим приведенное ниже определение
класса.
#include <iostream.h>
#include <string.h>
//Except — универсальный класс
//обработки исключительных ситуаций
class Except
{
protected:
char msg[80];
//сообщение об ошибке
char file[80]; //имя файла и строка, в
ошибка
int lineNum;
которой
возникла
public:
Except(char* pMsg, char* pFile, int nLine)
{
strncpy(msg, pMsg, sizeof msg);
msg[sizeof msg — 1] = ' \0 ' ;
strncpy (file, pFile, sizeof file};
file[sizeof file - 1] = ' \0';
lineNum = nLine;
}
virtual void display(ostream & out)
{
out << "Ошибка < " << msg << ">\n";
out << "обнаружена в строке #" << lineNum <<
" файла" << file << endl;
}
};
Генерация исключения при этом будет выглядеть следующим образом:
throw Except("Отрицательный аргумент факториала",__FILE__, __LINE__);
Соответствующий блок catch выглядит довольно просто:
void myFunc()
{
try
{
//...любой вызов
}
catch(Except &x) //захват объекта Except
{
//используем встроенную функцию-член
х.display(cerr) ;
Блок catch перехватывает исключение Except а затем использует встроенную
функцию-член display() для отображения сообщения об ошибке.
Объект cerr представляет собой поток вывода для сообщений об ошибках, т.е. почти
то же, что и cout. Разница между cout и cerr существенна только для профессиональных
программистов.
Класс Except является универсальным для сообщений об ошибках. Этот класс,
конечно, может быть расширен другими подклассами. Например, можно определить класс
InvArgExcep для хранения значения некорректного аргумента в дополнение к сообщению
об ошибке.
class InvArgExcept: public Except
{
protected:
int invArg;
public:
InvArgExcept (int arg, char* pFile,int nLine):
Except("Неверный аргумент", pFile, nLine)
{
invArg = arg;
}
virtual void display(ostream& out)
{
Except::display(out);
cout << "Аргумент равен " << invArg << endl;
}
};
Несмотря на то что в блоке catch указан класс Except, исключение InvArgExcept будет
успешно обработано, поскольку InvArgExcept является Excepе, а функция-член display()
полиморфна.
5.4. Исключения в конструкторах и деструкторах
Язык 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(){ ... // Обработка ошибки размера вектора
}
В обработчике может использоваться стандартный набор основных способов выдачи
сообщений об ошибке и восстановления. Внутри класса, определяющего исключение,
может храниться информация об исключении, которая передается обработчику. Смысл
этой техники заключается в том, чтобы обеспечить передачу информации об ошибке из
точки ее обнаружения в место, где для обработки ошибки имеется достаточно
возможностей.
Если в конструкторе объекта генерируется исключение, автоматически вызываются
деструкторы для полностью созданных в этом блоке к текущему моменту объектов, а
также для полей данных текущего объекта, являющихся объектами, и для его базовых
классов. Например, если исключение возникло при создании массива объектов,
деструкторы будут вызваны только для успешно созданных элементов.
Если объект создается в динамической памяти с помощью операции new и в
конструкторе возникнет исключение, память из-под объекта корректно освобождается.
5.5. Список исключений функции.
В заголовке функции можно перечислить сбить исключений, которые она может
порождать (в т.ч. и косвенно). Типы исключений перечисляются через запятую:
void func() throw(char, Monster* , … ) {...}
Если throw не указано, функция может порождать любое исключение, пустой список
после throw ( throw() ) означает, что функция не должна порождать исключений. Указание
списка исключений ни в чему не обязывает:
1. Если исключение предусмотрено, оно обрабатывается.
2. Если – нет,
2.1. вызывается стандартная функция unexpected(). При помощи функции
set_unexpected() можно установить собственную функцию, определяющую
действие программы в случае непредвиденной ситуации.
2.2. По умолчанию вызывается функция terminate(),
2.2.1. которая по умолчанию вызывает функцию abort(), завершающую выполнение
программы.
2.2.2. При помощи функции set_terminate() можно установить собственную
функцию, которая вызывается вместо abort() и определяется способ
завершения программы.
#include <iostream>
using namespace std;
void SoftAbort()
{
cerr<< "Program is terminated." << endl;
exit(1);
}
int main()
{
set_terminate(SoftAbort);
throw 5;
return 0;
}
6. Строки
В С строка - массив символов, заканчивающиеся спец. символом.
В С++ - это класс.
#include <cstring>
#include <string>
#include <iostream>
using namespace std;
int main ()
{ char c1[80],c2[80],c3[80];
string s1, s2, s3;
// pisnaivanie strok
strcpy (c1, "old");
strcpy(c2,c1);
s1="new";
s2=s1;
//concatenatuion
strcpy(c3,c1);
strcat(c3,c2);
s3 = s1 + s2;
//compare
if ( strcmp(c2, c3) == 0) cout << c2 << endl;
else cout <<c3<< endl;
if (s2 == s3) cout << s2<< endl;
else cout << s3<< endl;
)
Конструкторы и присваивание строк
string(); // пустой объект
string(const char*); // объект на основе старого типа
string(const char*,int n); // объект на основе старого тип (), записывает первые n
символов из строки, указанной первым параметром;
string(string &); // копирование
Присваивание
string& operator=(const string & str); //
string& operator=(const char*); //
string& operator=(const char c); //
string s1, s2(“Vasia”), s3(s2);
s1=’X’;
s3=”Vasia”;
s2=s1;
Операции
= , + , == , != , < , > , <= , >=, [] , << , >> , +=
Функции
Присваивание и добавление частей строк
assign(const string & str); // =
assign (const char *str , size_t n); //// вызывающей строке присваивает n символов
строки старого типа;
assign(const string & str , size_t pos, size_t n); // вызывающей строке присваиваются
от pos n символов (out_of_range if pos));
#include <stdexcept>
class out_of_range: public logic_error
{…
public: explicit out_of_range(const string
};
&what_arg);
append(const string & str); // +
append(const string & str , size_t pos, size_t n); // Добавляет к концу вызывающей
строки часть строки str от pos n символов (out_of_range if pos, length_error));
append (const char *str , size_t n); // добавляет n символов строки старого типа;
Преобразование строк
insert(size_t pos1, const string & str); // вставляет с pos1 (первые pos1 символов
сохраняются, остальные сдвигаюися (out_of_range if pos, length_error));
try
{
size_t n=4;
s2.insert(n, s1);
}
catch(out_of_range &range)
{cerr << "error for insert: \n" << range.what() <<endl;
}
insert(size_t pos1, const string & str , size_t pos2, size_t n); // вставляет с pos1 (первые
pos1 символов сохраняются) символы str c pos2 n символов, остальные сдвигаюися
(out_of_range if pos, length_error));
insert(size_t pos1, const char *str , size_t n); // вставляет с pos1 (первые pos1
символов сохраняются) n символов строки старого типа;
erase(size_t pos=0, size_t n=npos); // удаление n элементов
void clear();
replace(size_t pos1, size_t n1, const string & str); // вставляет с pos1 (первые pos1
символов сохраняются), удаляется n1 символов, остальные после (out_of_range if
pos, length_error));
replace(size_t pos1, size_t n1, const string & str, size_t pos2, size_t n2); // вставляет с
pos1 (первые pos1 символов сохраняются), удаляется n1 символов, вставляет с
pos1 символы str c pos2 n2 символов, остальные после (out_of_range if pos,
length_error));
replace(size_t pos1, size_t n1, const char* str, s size_t n2); // вставляет с pos1 (первые
pos1 символов сохраняются), удаляется n1 символов, вставляет с pos1 n2 символов
строки старого типа;
swap (string &str); //обмен содержимого 2-х строк
string substring(size_t
(out_of_range)
pos=0,
size_t
n=npos)
const;//
выделение
подстроки
const char * c_str() const; // преобразование в старый тип (строку нельзя менять!!)
const char * data() const; // преобразование в старый тип (строку нельзя менять!!)
size_t copy(char *str, size_t n, size_t pos=0); //не копирует нуль-символ, возвращает
количество скопированных элементов
Преобразование подстрок
size_t find(const string &str, size_t pos=0) const; /ищет самое левое вхождение строки
в вызывающую строку, начиная с позиции pos, возвращает позицию или npos;
size_t find(char c,size_t pos=0) const;
size_t rfind(const string &str, size_t pos=npos) const; /ищет самое правое вхождение
строки в вызывающую строку, до позиции pos, возвращает позицию или npos;
size_t rfind(char c,size_t pos= npos) const;
size_t find_first_of(const string &str, size_t pos=0) const; /ищет самое левое вхождение
любого символа строки str в вызывающую строку, начиная с позиции pos;
size_t find_first_of(char c, size_t pos=0) const; /ищет самое левое вхождение символа
с, начиная с позиции pos;
size_t find_last_of(const string &str, size_t pos= npos) const; /ищет самое правое
вхождение любого символа строки в вызывающую строку, начиная с позиции pos;
size_t find_last_of(char c, size_t pos= npos) const; /ищет самое правое вхождение
символа с, начиная с позиции pos;
size_t find_first_not_of(const string &str, size_t pos=0) const; /ищет самую левую
позицию, начиная с позиции pos, где ни один символ строки str не совпадает с
символом вызывающей строки;
size_t find_first_not_of(char c, size_t pos=0) const;
size_t find_last_ not_of(const string &str, size_t pos= npos) const;
size_t find_last_not_of(char c, size_t pos= npos) const;
Cуществуют варианты для строк старого стиля.
Сравнение частей строк
int compare(const string & str) const; // srtcmp;
int compare(size_t pos1, size_t n1, const string & str) const; // strcmp c pos1 n1
символов;
int compare(size_t pos1,size_t n1,const string &str,size_t pos2, size_t n2) const; // strcmp
c pos1 n1 символов; // strcmp c pos1 n1 символов pos1 n2 символов из str с pos2;
Характеристки строк
size size() const; // количеcтво элементов строки
size length() const; // количеcтво элементов строки
size max_size() const; // максимальная длина строки
size capacity() const; // объем строки, занимаемой строки
bool empty() const; //
7. Контейнерные классы
Стандартная библиотека шаблонов(STL)
Контейнеры
Последовательный
(Sequence container)
Итераторы
Алгоритмы
Ассоциативные
(Associative container)
Контейнеры - это объекты, хранящие внутри себя другие объекты. Существует
два вида контейнеров – последовательные и ассоциативные. Последовательные
контейнеры организованы в виде линейного списка. Ассоциативные позволяют
эффективно извлекать значения по ключу. Каждый контейнерный класс определяет набор
функций, которые можно применять к контейнеру.
Алгоритмы применяются к контейнерам. Они позволяют манипулировать
содержимым контейнера: инициализировать, сортировать, искать и преобразовывать
содержимое контейнера. Некоторые алгоритмы применяются к диапазону элементов.
Итераторы – это объекты, напоминающие указатели. Они осуществляют доступ к
элементам контейнера. Операции: =, разадресация (“*”), инкремент (++i , i++), == , !=.
Существует 5 видов итераторов:
1)
Итератор ввода (input_iterator_tag) – извлекает, но не хранит элементы.
Перемещается только вперед.
2)
Итератор вывода (output_iterator_tag) – хранит, но не извлекает элементы.
Перемещается только вперед.
3)
Прямой итератор (forward_iterator_tag) - хранит и извлекает значения.
Перемещается только вперед.
4)
Двунаправленный итератор (bidirectorial_iterator_tag) – хранит и извлекает
значения. Перемещается вперед и назад (поддерживает декремент).
5)
Итератор произвольного доступа (random_access_iterator_tag) – хранит и
извлекает значения (поддерживает операции двунаправленного и операции
сравнения). Обеспечивает произвольный доступ к элементам.
Итераторные классы и функции описаны в заголовочном файле <iterator>. Этот
файл подключается автоматически при использовании стандартных контейнеров.
Итератор является недействительным, если
 итератор не был инициализирован;
 контейнер, с которым он связан, изменил размеры;
 он указывает на конец последовательности.
Основные методы работы с итераторами:
iterator begin() , const iterator begin() const – указывает на первый элемент
контейнера.
2)
iterator end(), const iterator end() const – указывает на элемент следующий за
последним.
3)
reverse_iterator rbegin() – указывает на первый элемент обратной
последовательности (reverse_iterator – адаптер итератеров) .
4)
reverse_iterator rend() – указывает на элемент, следующий за последним, в
обратном порядке.
Пример:
1)
vector <int> v;
…
for ((vector <int>) reverse_iterator
cout << *ш << “ “;
i =v.rbegin(); i != v. rend() ; ++ i )
- вывод элементов вектора в обратном направлении.
Методы общие для всех контейнерных классов:
1)
size() – количество элементов.
2)
max_size() – максимальный размер контейнера (порядка миллиарда
элементов).
3)
empty() – булевская функция, указывающая пуст ли контейнер.
7.1. Векторы.
Вектор – это структура, реализующая произвольный доступ к элементам, добавление
в конец и удаление из конца.
Конструкторы:
1) explicit vector(); // конструктор по умолчанию; explicit означает, что при создании
объекта запрещено неявное преобразование типа при присваивании
значения другого типа;
2) explicit vector(size_t , const T& value = T()); // создает вектор длины n и заполняет
одинаковыми элементами типа T; изменение размера дорого, поэтому
задание начального размера полезно;
3) template <class initer> vector(initer first, initer last); // первый конструктор
копирования (last не входит);
4) vector (const vector<T>& x); // второй конструктор копирования.
vector <int> v4(v1);
vector <int> v2(10, 1);
vector<int> v3(v1.begin(), v1.begin() + 2);
Методы:
1) reference operator[](size_t num ); // reference - ccылка на элемент без проверки
выхода за границы;
2) reference at(size_t num ); // reference - ccылка на элемент c проверкой выхода за
границы;
Пример.
for (int i = 0; i<v.size(); i++) cout << v[i] << “ “;
try
{//…
v.at(i) = v.at(_);
}
catch(out_of_range) {…}
3)
4)
5)
6)
7)
8)
9)
10)
11)
12)
13)
14)
15)
void push_back(const T& val); // вставка в конец;
void pop_back();//удаление из конца;
iterator insert(iterator position, const T& val); - так как может произойти
перераспределение памяти, возвращаемое значение может отличаться
от position;
void insert(iterator position, size_t n, const T& val);- вставка n одинокавых
элементов;
template<class InIter> void insert(iterator position, InIter start, Initer end); вставка элеметов подходящего типа, заданных диапозоном (например, из
маасива);
iterator erase(iterator position); - удаление одного элемента;
iterator erase(iterator start, iterator end);
void clear(); // очистка вектора;
reference back(); // reference - ccылка на последний элемент;
reference front(); // ссылка на первый элемент;
size_t capacity() const; // определяет количество оперативной памяти;
void reserve(size_t n); // выделение памяти для хранения n элементов;
void resize(size_t n, T val = T()); // увеличение или уменьшение размера вектора;
val присваивается новым значениям, вставляемым в конец вектора.
Можно сравнивать вектора, используя операции:
==, !=, <, <=, операция
присваивания =. Сравнение - лексикографическое: вектора равны, если равны их размеры
и попарно равны элементы.
Вектор логических значений (vector <bool>) необходим для оптимизации
распределения памяти. Один элемент занимает 1 бит. Для него определен новый метод –
void flip(); – инвертирование вектора.
7.2. Двухсторонние очереди (deque).
Класс deque обеспечивает работу с двухсторонней очередью, реализующей
произвольный доступ к элементам, добавление в любой конец и удаление из любого
конца.
Конструкторы:
1)
explicit deque(); // конструктор по умолчанию
2)
explicit deque(size_t , const T& value = T()); // создает очередь длины n и
заполняет одинаковыми элементами типа T : deque<int> v2(10, 1);
3)
template <class InIter> deque(InIter first, InIter last); // первый конструктор
копирования::
deque<int> v3(v1.begin(), v1.begin() + 2);
4)
deque(const deque<T>& x); // второй конструктор копирования:
deque <int> v4(v1);
Методы работы с двухсторонней очередью те же, что и с вектором:
1) reference operator[](size_t num ); // reference - ccылка на элемент без проверки
выхода за границы;
2) reference at(size_t n) ; // ccылка на n – ый элемент;
3)
void push_back(const T& val); // вставка в конец;
4) void pop_back(); // удаление из конца;
5) iterator insert(iterator position, const T& val);
6) void insert(iterator position, size_t num, const T& val);
7) template<class InIter> void insert(iterator position, InIter start, Initer end);
8) iterator erase(iterator position);
9) iterator erase(iterator start, iterator end);
void clear(); // очистка вектора;
reference back(); // reference - ccылка на последний элемент;
reference front(); // ссылка на первый элемент;
capacity() и reserve();
void resize(size_t n, T val = T()); // увеличение или уменьшение размера вектора;
val присваивается новым значениям, вставляемым в конец очереди;
Кроме этого, имеются методы:
14) void push_front(const T& val) ;
15) void pop_front();
10)
11)
12)
Нет
13)
Можно сравнивать очереди, используя операции сравнения как для векторов.
7.3. Списки (List).
List – двухсвязный список, каждый узел которого содержит ссылку на предыдущий и
последующий элементы. Списки не поддерживают произвольный доступ к элементу.
Конструкторы:
1)
explicit list(); // конструктор по умолчанию;
2)
explicit list(size_t , const T& value = T()); // создает список длины n и
заполняет одинаковыми элементами типа T: list<int> v2(10, 1);
3)
template <class InIter> list(InIter first, InIter last); // первый конструктор
копирования: list<int> v3(v1.begin(), v1.begin() + 2);
4)
list (const list <T>& x); // второй конструктор копирования:
list<int> v4(v1);
Методы работы те же, что и с двухсторонней очередью. Кроме того,:
1)
void remove(const T &val); // удаляет все элементы со значением val;
2)
void reverse();// меняет порядок следования на противоположный;
3)
void sort();
4)
void merge(list<T> &ob); // объединение (на базе «слияния») двух
упорядоченных списков;
5)
void splice(iterator I, list<T> &ob);// вставляет (за счет изменения
указателей) содержимое объекта ob (который становится пустым) перед
элементом списка, заданным итератором I;.
6)
void splice(iterator I, list<T> &ob, iterator element); // «удаляет» из объекта ob
элемент, на который установлен итератор element, и вставляет его в
позицию списка, заданную итератором I;
7)
void splice(iterator I, list<T> &ob, iterator start, iterator end);// «удаляет» из
объекта ob элементы, лежащие в диапозоне от start до end и вставляет его в
позицию списка заданную итератором I;.
8)
void swap(list<T> &ob);// меняет местами элементы из списка и объекта ob;
9)
void unique() ; // удаляет дубликаты.
7.4. Стеки
Стек – адаптер контейнера.
template <class T, class Container = deque<T>>
class stack{
protected:
Container c;
public:
explicit stack(const Container& = Container());
bool empty() {return c.empty();}
value_type& top(){return c.back();}
void push(const value_type& x){c.push_back(x);}
void pop(){c.pop_back();
…
}
Пример.
stack <int, vector<int>> st;
7.5. Очереди (queue)
Очередь - адаптер контейнера, который можно реализовать на основе списка или
двухсторонней очереди (но не вектора), допускающая добавление в конец и удаление из
начала.
template <class T, class Container = deque<T>>
class queue{
protected:
Container c;
public:
explicit queue(const Container& = Container());
bool empty() {return c.empty();}
value_type& front(){return c.front();}
value_type& back(){return c.back();}
void push(const value_type& x){c.push_back(x);}
void pop(){c.pop_front();}
…
}
7.6. Очередь с приоритетами(priority_queue)
Основано на контейнере с произвольным доступом, то есть двухсторонней очереди
или векторе. Определен порядок выборки из очереди: «максимальный» элемент
относительно операции «меньше».
template <class T, class Container = vector<T>,
class Compare = less<typename Container:: value_type>>
class priority_queue{
protected:
Container c;
Compare comp;
public:
explicit priority_queue(const Compare& = Compare()),
const Container& = Container());
…
bool empty() {return c.empty();}
const value_type& top() const{return c.back();}
void push(const value_type& x);
void pop();
…
}
#include <iostream>
#include <vector>
#include <functional>
#include <queue>
using namespace std;
void main()
{
priority_queue <int, vector<int>, greater<int> > P;
int x;
P.push(20);
P.push(10);
P.push(15);
P.push(34);
P.push(5);
while (!P.empty())
{
cout << P.top()<< endl;
P.pop();
}
}
Будет выдано:
5
10
15
20
34
В заголовочном файле functional существуют шаблоны сравнения: less<тип>,
greater<тип>, less_equal<тип>, greater_equal<тип>. Для определения другого порядка
выборки можно определить свой функциональный класс (то есть класс с элементом –
вызовом функции).
8. Ассоциативные контейнеры
Как уже указывалось, ассоциативные контейнеры обеспечивают быстрый доступ к
данным за счет того, что они, как правило, построены на основе сбалансированных
деревьев поиска (стандартом регламентируется только интерфейс контейнеров, а не их
реализация). Существует пять типов ассоциативных контейнеров: словари (mар), словари с
дубликатами (multimap), множества (set), множества с дубликатами (multiset) и битовые
множества (bitset). Словари часто называют также ассоциативными массивами или
отображениями.
Словарь построен на основе пар значений, первое из которых представляет собой
ключ для идентификации элемента, а второе — собственно элемент. Можно сказать, что
ключ ассоциирован с элементом, откуда и произошло название этих контейнеров.
Например, в англо-русском словаре ключом является английское слово, а элементом —
русское. Обычный массив тоже можно рассматривать как словарь, ключом в котором
служит номер элемента. В словарях, описанных в STL, в качестве ключа может
использоваться значение произвольного типа. Ассоциативные контейнеры описаны в
заголовочных файлах <mар> и <set>.
Для хранения пары «ключ—элемент» используется шаблон pair, описанный в
заголовочном файле <utility>:
template<class T1, class T2> struct pair{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair();
pair(const T1 & x, const T2& y);
template <class U, class V> pair(const pair<U, V> &p);
};
Шаблон pair имеет два параметра, представляющих собой типы элементов пары.
Первый элемент имеет имя first, второй — second. Определено два конструктора: один
должен получать два значения для инициализации элементов, второй (конструктор
копирования) - ссылку на другую пару. Конструктора по умолчанию у пары нет, то есть
при создании объекта ему требуется присвоить значение явным образом.
Для пары определены проверка на равенство и операция сравнения на меньше (все
остальные операции отношения генерируются в STL автоматически на основе этих двух
операций). Пара p1 меньше пары р2, если p1.first < p2.first или р1.first == p2.first &&
p1.second < p2.second.
Для присваивания значения паре можно использовать функцию make_pair:
template<class T1, class T2> pair<T1, T2> make_pair(const T1& x, const T2& y);
Пример формирования пар:
#include <iostream>
#include <utility>
using namespace std;
int main(){
pair<int, double> p1(10, 12.3), p2(pl);
p3 = make_pair(20, 12.3); // Или р3 = pair <int, double>(20, 12.3)
cout << "p1: " << p2.first << ' ' << p2.second << '\n';
cout << "p3: " << p3.first << ' ' << p3.second << '\n';
p3.first -= 10;
if (p1 == p2) cout << "p1 == p3\n";
p1.second -= 1;
if (p3 > p1) cout << "p3 > p1\n";
}
Результат работы программы:
p1: 10 12.3
p2: 20 12.3
p1 == p3
p2 > p1
Заголовочный файл <utility> при использовании <map> или <set> подключается
автоматически.
8.1. Словари (map)
В словаре (mар) все ключи (первые элементы пар) должны быть уникальны.
Элементы в словаре хранятся в отсортированном виде, поэтому для ключей должно быть
определено отношение «меньше». Шаблон словаря содержит три параметра: тип ключа,
тип элемента и тип функционального объекта, определяющего отношение «меньше»:
template<class Key, class T, class Compare = less<Key> >
class map {
public:
typedef pair<const Key, T> value_type;
explicit map(const Compare& comp = Compare());
template<class lnputlter> map(InputIter f i r s t ,
const Compare& comp = Compare());
map(const map<Key, T, Compare>& x);
…
};
InputIter last,
Таким образом, тип элементов словаря value_type определяется как пара элементов
типа Key и Т.
Первый конструктор создает пустой словарь, используя указанный функциональный
объект. Второй конструктор создает словарь и записывает в него элементы, определяемые
диапазоном указанных итераторов. Время работы этого конструктора пропорционально
количеству записываемых элементов, если они упорядочены, и квадрату количества
элементов, если нет. Третий конструктор является конструктором копирования.
Как и для всех контейнеров, для словаря определены деструктор, операция
присваивания и операции отношения. Для доступа к элементам по ключу определена
операция []: Т& operator[](const Key &х);
С помощью этой операции можно не только получать значения элементов, но и
добавлять в словарь новые.
#include <iostream>
#include <fstream>
#include <string>
#include <map>
using namespace std;
typedef map<string, long, less<string>> map_sl;
int main() {
map_sl m1;
ifstream in("phonebook.txt");
string str;
long num;
while(!in.eof()) {
in >> num;
in.get();
getline(in, str);
m1[str] = num;
cout << str << ' ' << num << '\n';
}
m1["Petya P."] = 2134622;
map_sl :: iterator i;
cout << "m1:" << '\n';
for(i = m1.begin(); i != m1.end(); ++i)
cout << (*i).first < < ' ' < < (*i).second << '\n';
i = m1.begin(); i++;
cout << "The second element: ";
cout << i->first << ' ' << (*i).second << '\n';
cout << "Vasia: " << m1["Vasia"] << '\n';
// вставка позже
getline(cin, str);
if(m1.find(str) != m1.end())
cout << m1[str];
else {
cout << (*m1.upper_bound(str)).first << ' ';
cout << (*m1.lower_bound(str)).first << ' ';
}
cout << '\n';
// вставка еще позже
map_sl m2;
m2.insert(map_sl::value_type("Lena", 3157725));
str = "Anna";
num = 5536390;
m2.insert(make_pair(str, num));
num = 5172549;
m2.insert(make_pair(str, num));
i = m1.begin();
m2.insert(*i);
m2["Lena"] = 5555555;
for(i = m2.begin(); i != m2.end(); i++)
cout << i->first << ' ' << i->second << '\n';
return 0;
}
Для итераторов словаря допустимы операции инкремента и декремента, но не
операции + и -. Ниже приведен результат работы программы (обратите внимание, что
словарь выводится в упорядоченном виде):
Petya К. 1001002
Ivanova N.M. 3563398
Vovochka 1180316
Vasia 2334476
ml:
Ivanova N.M. 3563398
Petya K. 1001002
Petya P. 2134622
Vasia 2334476
Vovochka 1180316
The second element: Petya К. 1001002
Vasia: 2334476
Для поиска элементов в словаре определены следующие функции:
iterator find(const key_type &x); //возвращает итератор на найденный элемент в случае
успешного поиска или end() в противном случае.
const_iterator find(const key_type& x) const;
iterator lower_bound(const key_type& x); //возвращает итератор на первый элемент,
ключ которого больше x, или end(), если такого нет
const_iterator lower_bound(const key_type& x) const;
iterator upper_bound(const key_type& x); //возвращает итератор на первый элемент,
ключ которого не меньше х, или end(), если такого нет (если элемент с ключом х есть в
словаре, будет возвращен итератор на него).
const_iterator upper_bound(const key_type &x) const;
size_t count(const key_type& x) const;// возвращает количество элементов, ключ которых
равен х (таких элементов может быть 0 или 1).
Для вставки и удаления элементов определены функции:
pair<iterator, bool> insert(const value_type& x);
iterator insert(iterator position, const value_type& x);
template<class InputIter> void insert(InputIter first, InputIter last);
void erase(iterator position);
size_t erase(const key_type& x);
void erase(iterator first, iterator last);
void clear();
Первая форма функции используется для вставки в словарь пары «ключ—значение».
Функция возвращает пару, состоящую из итератора, указывающего на вставленное
значение, и булевого признака результата операции: true, если записи с таким ключом в
словаре не было (только в этом случае происходит добавление), и false в противном случае
(итератор указывает на существующую запись). Время работы функции пропорционально
логарифму количества элементов в словаре.
Таким образом, скорректировать существующую запись, используя функцию вставки,
нельзя. Это делается с помощью операции доступа по индексу.
Вторая форма функции insert применяется для ускорения процесса вставки. С этой
целью ей передается первым параметром позиция словаря, начиная с которой требуется
осуществлять поиск места вставки. Вставка выполняется только в случае отсутствия
значения х в словаре. Функция возвращает итератор на элемент словаря с ключом,
содержащимся в х.
Например, если известно, что элементы будут помещаться в словарь в порядке
возрастания, можно передавать первым параметром в функцию вставки позицию
предыдущего элемента (в этом случае время вставки является константой):
#include <iostream>
#include <string>
#include <map>
using namespace std;
typedef map<string, long, less<string> > map_sl;
typedef pair<string, long> pair_sl;
int main(){
pair_sl
pi[3]
=
{pair_sl("Anna",
2234567),
pair_sl("Maria",
1234567), pair_sl("John", 3345779)};
map_sl m1;
map_sl :: iterator i = m1.begin();
for(int k = 0; k < 3; k++)
i = m1.insert(i, pi[k]);
for(i = m1.begin(); i != m1.end(); i++)
cout << (*i).first << ' ' << i->second << \n';
// позже
pair <map_sl :: iterator, map_sl :: iterator> p;
p = m1.equal_range("John");
cout << (p.first)->first << ' ' << (p.second)->first << '\n';
return 0;
}
Третья форма функции insert используется для вставки группы элементов,
определяемой диапазоном итераторов. Функции удаления элементов и очистки словаря
аналогичны одноименным функциям других контейнеров: первая форма функции erase
удаляет элемент словаря из позиции, заданной итератором, вторая - по заданному ключу, а
третья удаляет диапазон элементов.
Операции вставки в словарь не приводят к порче связанных с ними итераторов и
ссылок, а операции удаления делают недействительными только итераторы и ссылки,
связанные с удаляемыми элементами.
Для обмена всех элементов двух словарей применяется функция swap:
template <class Key, class Т, class Compare> void swap(map<Key, T, Compare>& x,
map<Key, T, Compare>& y);
Функция equal_range возвращает пару итераторов (lowerbound(x), upperbound(x))
для переданного ей значения х:
pair<iterator,iterator> equal_range(const key_type& x);
pair<const_iterator, const_iterator> equal_range(const key_type& x) const;
После вызова функции оба итератора будут указывать на элемент с заданным
ключом, если он присутствует в словаре, или на первый элемент, больший него, в
противном случае.
// алфавит
#include <iostream>
#include <map>
using namespace std;
int main() {
map<char, int> m;
int i;
for(i = 0; i < 10; i++)
/* Один из вариантов добавления пары
m.insert(pair<char,int>('A'+i,i));*/
m.insert(make_pair((char)'A'+i, i));
char ch;
cout << "Please enter the key: ";
cin >> ch;
map<char, int> :: iterator p;
p = m.find(ch);
if(p != m.end())
cout << p->second << \n';
else
cout << "This key isn't been in map\n";
return 0;
}
8.2. Словари с дубликатами (multimap)
Как уже упоминалось, словари с дубликатами (multimap) допускают хранение
элементов с одинаковыми ключами. Поэтому для них не определена операция доступа по
индексу [ ], а добавление с помощью функции insert выполняется успешно в любом случае.
Функция возвращает итератор на вставленный элемент. Элементы с одинаковыми
ключами хранятся в словаре в порядке их занесения. При удалении элемента по ключу
функция erase возвращает количество удаленных элементов. Функция equal_range
возвращает диапазон итераторов, определяющий все вхождения элемента с заданным
ключом. Функция count может вернуть значение, большее 1. В остальном словари с
дубликатами аналогичны обычным словарям.
#include <iostream>
#include <fstream>
#include <string>
#include <map>
using namespace std;
int main() {
multimap<string, long, less<string> > m1;
ifstream in("phones.txt");
string str;
long num;
while(!in.eof()) {
in >> num;
in.get();
getline(in, str);
ml.insert(make_pair(str, num));
c o u t < < st r < < ' ' < < n u m < < ' \ n' ;
}
M1.insert(make_pair("Vasia", 2525257));
multimap<string, long, less<string> > :: iterator i;
for(i = m1.begin(); i != m1.end(); i++)
cout << i->first << ' ' << i->second << '\n';
return 0;
}
антонимы через string
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main() {
map<string, string> m;
int i;
m.insert(pair<string,string>("yes","no"));
m.insert(pair<string,string>("good","bad"));
m.insert(pair<string,string>("left","right"));
m.insert(pair<string, string>("up", "down"));
string s;
cout << "Please enter word: ";
cin >> s;
map<string, string> :: iterator p;
p = m.find(s);
if(p ! = m.end())
cout << "Oppoiste: " << p->second << '\n';
else
cout << "This word isn't been in list\n";
return 0;
}
8.3. Множества (set)
Множество — это ассоциативный контейнер, содержащий только значения
ключей, то есть тип value_type соответствует типу Key. Значения ключей должны быть
уникальны. Шаблон множества имеет два параметра: тип ключа и тип функционального
объекта, определяющего отношение «меньше»:
template <class Key, class Compare = less<Key> > class set{
public:
typedef Key key_type;
typedef Key value_type;
explicit set(const Compare& comp = Compare());
template<class InputIter> set(InputIter first, InputIter last,
const Compare& comp = Compare());
set(const set<Key, Compare>& x);
pair<iterator, bool> insert(const value_type& x);
iterator insert(iterator position, const value_type& x);
template <class InputIter> void insert(InputIter first, InputIter last);
void erase(iterator position);
size_t erase(const key_type& x);
void erase(iterator first, iterator last);
void swap(set<Key, Compare>& x, set<Key, Compare> &y);
void clear();
iterator find(const key_type& x) const;
size_ type count(const key_type& x) const;
iterator lower_bound (const key_type& x) const;
iterator upper_bound(const key_type& x) const;
pair<iterator,iterator> equal_range(const key_type& x) const;
Из описания, приведенного с сокращениями, видно, что интерфейс множества
аналогичен интерфейсу словаря. Ниже приведен простой пример, в котором создаются
множества целых чисел:
#include <iostream>
#include <set>
using namespace std;
typedef set<int, less<int> > set_i;
set_i :: iterator i;
int main(){
int a[4] = {4, 2, 1, 2};
set_i s1;
set_i s2(a, a+4);
set_i s3(s2);
s2.insert(10);
s2.insert(6);
for(i = s2.begin(); i != s2.end(); i++)
cout << *i << ' ';
cout << '\n';
pair <set_i :: iterator, set_i :: iterator> p;
p = s2.equal_range(2);
cout << *(p.first) << ' ' << *(p.second) << '\n';
p = s2.equal_range(5);
cout << *(p.first) << ' ' << *(p.second) << '\n';
return 0;
}
Результат работы программы:
1 2 4 6 10
2 4
6 6
Как и для словаря, элементы в множестве
Повторяющиеся элементы в множество не заносятся.
хранятся
отсортированными.
8.4. Множества с дубликатами (multiset)
Во множествах с дубликатами ключи могут повторяться, поэтому операция вставки
элемента всегда выполняется успешно, и функция insert возвращает итератор на
вставленный элемент. Элементы с одинаковыми ключами хранятся в словаре в порядке их
занесения. Функция find возвращает итератор на первый найденный элемент или end(),
если ни одного элемента с заданным ключом не найдено.
При работе с одинаковыми ключами в multiset часто пользуются функциями count,
lower_bound, upper_bound и equal_range, имеющими тот же смысл, что и для словарей с
дубликатами.
8.5. Битовые множества (bitset)
Битовое множество представляет собой шаблон для представления и обработки
длинных последовательностей битов. Фактически bitset — это битовый массив, для
которого обеспечиваются операции произвольного доступа, изменения отдельных битов и
всего массива. Биты нумеруются справа налево, начиная с 0.
Шаблон битового множества определен в заголовочном файле <bitset>. Параметром
шаблона является длина битовой последовательности, которая должна быть константой:
template<size_t N> class bitset {. . .};
Для адресации отдельного бита в bitset введен класс reference:
class reference {
friend class bitset;
reference();
public:
~reference();
Reference& operator=(bool x); // для b[i] = x;
Reference& operator= (const reference& x); // для b[i] = b [ j ];
bool operator~() const; // инверсия b[i]
operator bool() const; // для x = b [ i ];
reference& flip(); // для инверсии b[ i ];
};
Конструкторы позволяют создать битовое множество из всех нулей, из значения типа
long или из строки типа string*:
Bitset(); // 1
bitset(unsigned long val); // 2
explicit bitset(const string& str, string::size_t pos = 0, string:: size_t n = string::npos); // 3
Первый конструктор создает битовое множество из нулей, второй принимает
значение типа long и инициализирует каждый бит множества соответствующим битом
внутреннего представления этого значения. Третий конструктор принимает строку,
которая должна состоять из нулей и единиц (если это не так, порождается исключение
invalid_argument) и инициализирует каждый бит множества в соответствии со значением
символа строки. Второй и третий параметры конструктора задают позиции начала строки и
количества символов, которые используются для инициализации. По умолчанию
используется вся строка. Примеры создания битовых множеств:
bitset <100> b1; // сто нулей
bitset <1б> b2 (OxfOf); // 0000111100001111
bitset <1б> bЗ ("ООООППООООПП"); // 0000111100001111
bitset <5> b4 ("00110011", 3); // 10011
bitset <3> b5 ("00110101", 1, 3); // 011
С битовыми множествами можно выполнять следующие операции:
bool operator==(const bitset<N>& rhs) const;
bool operator!=(const bitset<N>& rhs) const;
bitset<N>& operator&=(const bitset<N>& rhs);
bitset<N>& operator|=(const bitset<N>& rhs);
bitset<N>& operator^=(const bitset<N>& rhs);
bitset<N> operator<<(size_t pos) const;
bitset<N> operator>>(size_t pos) const;
bitset<N>& operator<<=(size_t pos);
bitset<N>& operator>>=(size_t pos);
bitset<N>& set();
bitset<N>& set(size_t pos, int val = true);
bitset<N>& reset();
bitset<N>& reset(size_t pos);
bitset<N> operator~() const;
bitset<N>& flip();
bitset<N>& flip(size_t pos);
reference operator[](size_t pos); // b [ i ];
Множества можно сравнивать на равенство (==) и неравенство (!=). Операции << и >>
создают битовые наборы, сдвинутые на pos бит влево или вправо соответственно. При
сдвиге освобождающиеся позиции заполняются пулями. Операция set устанавливает все
биты множества в 1, reset - в 0. Операция ~ создает дополнительный набор. С помощью flip
можно инвертировать значение каждого бита
или бита, заданного параметром роs.
Доступ к отдельному биту можно выполнять с помощью операции индексации. Если
значение индекса выходит за границы набора, порождается исключение out_of_range.
В шаблоне bitset определены методы преобразования в длинное целое и в строку, а
также анализа значений множества:
unsigned long to_ulong() const; // в unsigned long
string to_string() const; // в string
size_t count() const; // количество битовых 1
size_t size() const; // количество битов
bool test(size_t pos) const; // true, если b[pos] == 1
bool any() const; // true, если хотя бы один бит равен 1
bool none() const; // true, если ни один бит не равен 1
Определены также обычные операции ввода и вывода << и >> . Биты множества
выводятся с помощью символов '0' и '1' слева направо, самый старший бит слева.
В битовом множестве не определены итераторы, поэтому оно не является
контейнером в чистом виде, поскольку не полностью обеспечивает стандартный интерфейс
контейнеров.
#include<iostream>
#include<bitset>
#include<string>
using namespace std;
int main() {
bitset<8> b1(8); // 00001000
string s1 = "00110110";
bitset<5> b2(s1, 3); //10110
string s2 = "01011010";
bitset<6> b3(s2, 1, 6); // 101101
bitset<8> b4;
if(b4.none())
cout << "All nulls\n";
if(bl.any())
cout << b1 << '\n';
cout << b2.size() << '\n';
cout << b3.count() << '\n';
b3 <<= 1; //011010
cout << b3 << '\n';
cout << ~b2 << '\n';
cout << b2.flip() << '\n';
return 0;
}
Вывод на экран:
All nulls
00001000
5
4
011010
01001
01001
Download