3-й семестр «Программирование и алгоритмические языки» I. ООП на С++ Лекция 1. Принципы ООП. Любая программа – набор инструкций процессора. Повышение уровня абстракции программы: функции; собственные типы данных (typedef тип New_name [размерность]); структуры; объединение в модули описаний типов и функций для их обработки (интрефейс). Класс – тип данных, определяемый пользователем (поля данных и функции их обработки). Интерфейс класса – заголовки методов класса. Конкретные величины типа «класс» - экземпляры класса, или объекты. Сообщение – запрос на выполнение действия, содержащий набор необходимых параметров (вызов функций). ООП реализует «событийно-управляемая модель», когда данные активны и управляют вызовом того или иного фрагмента кода, программа ожидает действий пользователя (в «директивной модели» код предлагает пользователю делать те или иные действия). Основные свойства ООП: инкапсуляция; наследование; полиморфизм – возможность использовать в различных классах иерархии одно имя для обозначения сходных по смыслу действий (перегрузка, шаблон). Описание классов. Данные класса – поля, функции – методы. 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: 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;} 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 <знак операции> (<список параметров>) {тело функции} 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), а здоровье увеличивается на 1 в //Vasia 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. Конец четвертой лекции ========================================================= Перегрузка операций new, delete не требуется передавать параметр типа класса; Первым параметром для new, new[] передается размер объекта типа size_t(<stddef.h>), при вызове передается в функции неявным образом; new, new[] определяются с типом возвращаемого значения void*; delete определяется с типом возврата void и первый аргумент типа void*; операции – статические элементы класса. Поведение операций должно соответствовать действиям, выполняемым по умолчанию. class Obj{ …}; class pObj{ … private: Obj *p; … }; Стандарт: pObj *p = new pObj; Для экономии памяти новая операция new класса pObj выделяет большой блок памяти, затем размещать в нем указатели на Obj . Для этого в класс pObj вводятся статические поля для указания размера блока и указатель на свободную ячейку блока. Неиспользуемые ячейки связываются в список. Используем union для размещения в поле или указателя на объект или связи со следующей свободной ячейкой. class pObj{ public: static void* operator new(size_t SIZE); static void operator delete(void * ObjD, size_t SIZE); … private: union { Obj *p; pObj *next; }; static const int BLOCK_SIZE; static pObj *headFree; … }; void * pObj:: operator new(size_t SIZE) { if (SIZE != sizeof(pObj)) //память задана неверно return:: operator new(SIZE); // стандартная операция pObj *з = headFree; // указатель на первую свободную ячейку if (p) headFree = p->next; else { //выделение нового блока pObj *newblock = static_cast<pObj*>(::operator new(BLOCK_SIZE sizeof(pObj))); //связь ячеек for(int i = 1; i < BLOCK_SIZE -1; ++i ) newblock[i].next = &newblock[i+1]; newblock[BLOCK_SIZE -1].next = 0; p = newblock; headFree = newblock[1]; } return p; }; … // инициализация статических полей pObj *pObj::headFree; // =0 const int pObj:: BLOCK_SIZE = 1024; … void pObj::operator delete(void * ObjD, size_t SIZE) { if (ObjD == 0) return; if (SIZE != sizeof(pObj)) //память задана неверно {:: operator delete(ObjD); // стандартная операция return; } pObj *p = static_cast<pObj*>(ObjD); p->next = headFree; headFree = p; * }; Конец пятой лекции ========================================================= Перегрузка операции присваивания приведения типа operator <имя нового типа> () {тело функции} Тип возвращаемого значения и параметры не указываются. monstr:: operator in(){return health;} … monstr Vasia; cout << int(Vasia); … Перегрузка операции вызова функции Функциональный класс, т.е. тот в котором определена операция вызова функции, не требует наличия других полей и методов, кроме определения вызова функции. class if_greater { public: int operator() (int a, int b) const { return a>b;} }; … if_greater x; cout << x(1,5) << x.operator()(1,5)<< if_greater()(5,1); // конструктор по умолчанию «операцией » является «()» Можно записать x(1,5) , а также x.operator()(1,5) Рекомендации по составу классов консрукторы, определяющие как инициализировать объекты; набор методов, реализующих свойства класса (const указывает, что поля класса не должны меняться); набор операций (копирование, присваивание, сравнение, …); класс исключений (сообщения об ошибке) Если есть функции работающие с несколькими классами без доступа к скрытым полям, можно описать их вне классов. Для логической связи функции можно поместить в общее пространство имен: namespace Nashi{ class monstr{…}; class hero{…}; void change(hero, monstr); … } Задание из практикума «Реализация класса треугольников» Библиотека cstring – работа со строками в С; iomanip – манипуляторы, то есть функции, которые можно помещать в цепочку помещения и извлечения для форматирования данных (dec, oct, hex, endl, setprecision, setw). sprintf – вывод в строку (stdio). Конец седьмой лекции ========================================================= Раздел main.cpp. Меню, заглушки. Тестирование, отладка первой версии. Этап 2. Перемещение точки (перегрузка +), треугольника. Конец восьмой лекции ========================================================= Этап 3. Поиск максимального по площади треугольника: перегрузка >, «не совсем правильная» функция FindMax Отладка (сбой): конструктор внутри функции, модификация объекта, копирование по умолчанию. Напоминание о работе конструктора и деструктора. Перегрузка операции присваивания, конструктор копирования. Конец девятой лекции ========================================================= Этап 4. Отношение включения. Алгоритм положения точки относительно вектора (5 вариантов) (самостоятельное изучение). Наследование Иерархия классов: производные классы получают элементы родительских, или базовых, классов и могут дополнять или изменять их свойства. В начале иерархии – наиболее общие черты для нижележащих классов. Класс может наследовать свойства 2-х и более классов. Ключи доступа class имя :[private [[,базовый_класс]] {тело класса} class class class class | protected | public] базовый_класс A {…}; B {…}; C {…}; D: A, protected B, public C {…}; По умолчанию для классов ключ доступа private, для структур – public. Обозначмм public - 1 protected - 2 private – 3 Ключ Доступ в доступа произ. кл. (Y) Базовый max{X,Y} класс (X) 1 2 3 1 2 3 1 2 - 2 2 - 3 3 - Если ключ доступа 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). Объект, определенный через указатель или ссылку и содержащий виртальные методы – полиморфный. Абстрактный класс – класс хотя бы с одним чисто виртуальным методом. Объекты такого класса создавать нельзя. Нельзя использовать при явном приведении типов, для описания типа параметра функции, типа возвращаемого функцией значения. Допускается объявлять ссылки и указатели на абстрактный класс, если при инициализации не требуется создавать объект. Если в производном классе не определены все чисто виртуальные методы, он – абстрактный. Можно создавать функцию с параметрами – указателями на абстрактный класс, на место которых при выполнении программы передается указатель на объект производного класса. Получаются полиморфные функции.