Створення похідних об'єктів

Ієрархія об’єктів і ієрархія класів: композиція об’єктів і успадкування. Базовий клас, похідний клас, уточнення привілеїв доступу до членів базового класу, захищена область класу, область видимості і оператор розв’язування області видимості

Ієрархія об’єктів і ієрархія класів

Об'єктно-орієнтовне програмування

Параметризовані класи (class template): визначення шаблону, конкретизація

Параметризовані класи (class template): визначення шаблону, конкретизація

 

Розглянемо специфікацію масиву цілих

 

class IntArray

{

public:

IntArray (int sz=defaultArraySize){ init(sz,0);}

IntArray (int* array, int sz){init(sz,array);}

IntArray (const IntArray&){init(a._size, a.ia);}

~IntArray() {delete []ia;}

 

bool operator== (const IntArray&) const;

bool operator!= (const IntArray&) const;

 

IntArray& operator= (const IntArray&);

int& operator[] (int index);

 

int size() const;

void sort();

int min() const;

int max() const;

int find (int value) const;

private:

int _size;

int *ia;

bool _isSorted;

static const int defaultArraySize;

void init(int sz, int* array);

}

 

В ньому забезпечено зберігання додаткової інформації про розмір масиву, при

індексуванні можна виконувати перевірку виходу індексу за межі масиву. Також

передбачено зручні методи присвоєння масивів, сортування, пошуку, обчислення

мінімуму та максимуму, тощо.

 

Тепер уявімо собі, нам потрібен масив дійсних, комплексних, чи ще якихось

інших чисел. Тепер доведеться все переписувати, замінюючи де потрібно, int

на double ? — Звичайно, ні. Подібно узагальненим функціям, визначимо параметризований

масив Array елементів типу elemType

 

template <class elemType>

class Array

{

public:

Array (int sz=defaultArraySize);

Array (elemType* array, int sz);

Array (const Array&);

~Array() {delete []ia;}

 

bool operator== (const Array&) const;

bool operator!= (const Array&) const;

 

Array& operator= (const Array&);

elemType& opetator[] (int index);

 

int size() const;

void sort();

elemType min() const;

elemType max() const;

elemType find (elemType value) const;

private:

int _size;

elemType *ia;

static const int defaultArraySize=100;

void init(int sz, elemType* array);

}

 

Так виглядатимуть конкретизації — оголошення масивів конкретних типів

 

Array ia(sz);

Array cha(sz);

Array da(sz);

Array ca(sz);

 

Ось ще два приклади параметризованих класів

 

template <class T>

сlass Stack

{

public:

Stack();

void clear();

bool empty();

const T& top();

int pop();

void push(const T& value);

int size();

}

 

template <class T>

class Queue<T>

{

public:

Queue();

Queue(const Queue&);

virtual ~Queue();

 

Queue& operator=(const Queue&);

int operator==(const Queue&) const;

int operator!=(const Queue&) const;

 

void clear();

void append(const T&);

const T& front() const;

void pop();

 

int length() const;

bool empty() const;

};

 

 

Зміст (обєктно-орієнтоване програмування)

 

 

Розглянемо простий приклад ієрархії двох взаємно пов'язаних класів

 

class IntArray

{

public:

IntArray (int sz=defaultArraySize);

IntArray (int* array, int sz);

IntArray (const IntArray&);

~IntArray() {delete []ia;}

 

bool operator== (const IntArray&) const;

bool operator!= (const IntArray&) const;

 

IntArray& operator= (const IntArray&);

int& operator[] (int index);

// Константне індексування довелося додати

// для реалізації функції

// int BoundedStack::top() const

int& operator[] (int index) const;

 

int size() const;

void sort();

int min() const;

int max() const;

int find (int value) const;

private:

int _size;

int *ia;

static const int defaultArraySize;

void init(int sz, int* array);

};

class BoundedStack

{

private:

static const int bos; //bottom of stack

int _top;

IntArray stackArray;

 

public:

class BadStack{};

explicit BoundedStack(int size):

stackArray(size),_top(bos){};

bool empty() const

{return _top==bos;}

bool full() const

{return _top==stackArray.size()-1;}

int top() const

{return stackArray[_top];}

int pop()

{

if (empty())

throw BadStack();

_top--;

}

void push(int value)

{

if (full())

throw BadStack();

stackArray[++_top]=value;

}

//delegating:

int size() const

{return stackArray.size();}

};

const int BoundedStack::bos=-1;

const int IntArray::defaultArraySize=256;

 

Таку ієрархію називають агрегацією або композицією . Обмежений

стек BoundedStack містить в собі масив IntArray , призначений для зберігання

елементів стеку. Виконання операцій над стеком делегується відповідним операціям

над масивом.

 

Цікаво зрозуміти, в який спосіб краще приєднати масив до стеку: так як ми

це зробили, а саме включенням атрибутом

 

IntArray stackArray ; //(1)

 

так, що масив стає частиною стеку, чи приєднання указником

 

IntArray * stackArray ; //(2)

 

при якому стек знає свій масив.

Ситуація схожа на клас string з відкладеним копіюванням. У випадку (1) агрегований

об'єкт стає частиною агрегату. Тому копіювання або знищення агрегату приводить

до копіювання або, відповідно, знищення агрегованого. У випадку (2) вони живуть

відносно незалежним життям і один об'єкт може бути агрегованим кільком агрегатам.

 

Суть агрегації можна висловити такими словами: стек використовує свій масив,

але сам стек не є масивом.

 

Візьмемо інший приклад: прямокутник і квадрат. Тут співвідношення інше — кожен

квадрат є прямокутником. Таку ієрархію називають успадкуванням. Візьмемо базовий

клас

 

class Rectangle

{

private:

short _height;

short _width;

public:

Rectangle( short heigh, short width);

void displayArea();

};

Rectangle::Rectangle(short height, short width)

{

_height = height;

_width = width;

}

void Rectangle::displayArea()

{

cout << "Area is: " <<_height * _width <<'\n';

}

 

 

Тепер визначимо квадрат його підкласом

 

class Square : public Rectangle

{

public:

Square(short side );

};

Square::Square( short side ) : Rectangle( side, side ){}

 

Конструктор підкласу читається так: для того щоб створити квадрат, спочатку

треба створити прямокутник з рівними сторонами. Це, власне, поки що вся особливість

підкласу — обмеження на довжини сторін.

 

Клас прямокутника має, крім конструктора, єдиний відкритий метод — обчислення

площі. Цей метод стає доступним квадратам теж.

 

int main()

{

Rectangle* myRectangle;

Square* mySquare;

 

myRectangle = new Rectangle( 10, 15 );

myRectangle->displayArea();

 

mySquare = new Square( 10 );

// *mySquare має крім типу Square також тип Rectangle

// а тому одержить доступ до його відкритого методу

mySquare->displayArea();

 

return 0;

}

 

Трохи несподіваним стило те, що квадрат більше не знає довжини свої сторони,

оскільки вона зберігається в закритих атрибутах суперкласу. Як ми зараз побачимо,

права доступу підкласів необхідно визначити точніше. Для цього в батьківському

класі визначається ще один рівень доступу — захищена частина класу protected

. Доступ до неї одержують лише підоб'єкти даного супероб'єкту.

 

 

class IntArray

{

public:

IntArray (int sz=defaultArraySize);

IntArray (int* array, int sz);

IntArray (const IntArray&);

~IntArray() {delete []ia;}

 

bool operator== (const IntArray&) const;

bool operator!= (const IntArray&) const;

 

IntArray& operator= (const IntArray&);

int& operator[] (int index);

int& operator[] (int index) const;

 

int size() const;

void sort();

int min() const;

int max() const;

int find (int value) const;

protected:

int *ia;

private:

int _size;

static const int defaultArraySize;

void init(int sz, int* array);

};

class BoundedStack: public IntArray

{

private:

static const int bos; //bottom of stack

int _top;

// IntArray stackArray;

public:

class BadStack{};

explicit BoundedStack(int size):

IntArray(size),_top(bos){};

bool empty() const

{return _top==bos;}

bool full() const

{return _top==size()-1;}

int top() const

{return ia[_top];}

void pop()

{

if (empty())

throw BadStack();

_top--;

}

void push(int value)

{

if (full())

throw BadStack();

ia[++_top]=value;

}

};

 

Тепер стек вже не містить більше в собі масиву, стек сам стає масивом (ще

не ясно, чи це добре). Зникає потреба в делегуванні розміру стеку, оскільки

напряму використовується потрібна функція масиву. Але є й небажані наслідки.

В результаті успадкування у нас з'явився гібрид масиву і стеку. Дійсно всі

відкриті методи супероб'єкту відкриті для підоб'єкту, а тому визначивши стек

 

BoundedStack s (100);

 

можна звертатися до нього, як до масиву

 

cout<<s.size(); //OK

 

cout<<s.size(); //OK

s[55] = 10; //???

cout <<s[99];

 

Успадкування вимагає деталізації.

 

 

6.2 Відкрите, закрите і захищене успадкування, успадкування типу і успадкування реалізації; абстрактні класи

Відкрите, закрите і захищене успадкування, успадкування типу і успадкування реалізації; абстрактні класи

 

Закрите успадкування частково вирішує проблему

 

class BoundedStack : private IntArray

 

Тепер всі відкриті і захищені методи суперкласу IntArray стануть закритими

методами класу BoundedStack . Попередній запис s.size() стане тепер неможливим.

Доводиться або знову делегувати функцію

 

int BoundedStack::size() const

{return IntArray::size();}

 

 

або знову користуватись засобами порушення прав доступу, схожими на дружні

функції.

 

class BoundedStack: private IntArray

{

. . . . . . . . .

public:

//відкривання окремої функції

using IntArray::size;

. . . . . . . . .

}

 

Цей тип успадкування називають закритим . Закрите успадкування дає

підкласу доступ до своєї захищеної і відкритої частин, але не делегує відкриту

частину до інтерфейсу підоб'єкту. Тепер стало зрозумілим, чому при перших прикладах

успадкувань писали public . Ці успадкування були відкритими . Права

доступу ті ж самі, але відкрита частина підоб'єкту тепер складається з власної

відкритої частини, доповненої відкритою частиною супероб'єкту. Тому кажуть,

що при відкритому успадкування успадковується тип супероб'єкту (власне його

інтерфейс), а при закритому успадкуванні — лише реалізація супероб'єкту.

 

Закрите успадкування закриває в батьківському класі все, відкрите відкриває

все, що можна відкрити. Існує ще один проміжний спосіб успадкування — захищене ( protected ).

Захищена частина суперкласу залишається захищеною, а не закритою, в підкласі.

 

class BoundedStack: protected IntArray

{

private:

static const int bos; //bottom of stack

protected:

int _top;

. . . . . . . . .

}

 

 

Клас PeekbackStack матиме доступ до захищених частин BoundedStack і IntArray

. При цьому в інтерфейс PeekbackStack потраплять всі відкриті методи BoundedStack

.

 

class PeekbackStack : public BoundedStack

{

public:

PeekbackStack(int size):BoundedStack (size){};

bool PeekbackStack::peekback(int, int&) const;

};

Ось реалізація

bool PeekbackStack::peekback(int index, int& value) const

{

if (empty())

throw BadStack();

// захищений атрибут BoundedStack

if (index<0||index>_top)

{

// захищений метод IntArray

value=ia[_top];

return false;

}

value=ia[index];

return true;

 

Тепер ми скажемо, що обмежений стек з підгляданням — це звичайний обмежений

стек, доповнений методом підглядання. Обидва вони реалізовані на базі обмеженого

масиву.

 

Корисно реалізувати декілька типів даних на базі інших. Наприклад, стек на

базі списку, черга на базі циклічного масиву. Зробіть це самостійно.

 

Уявімо собі, що у нашому розпорядженні є декілька стеків, наприклад, обмежений

стек на базі масиву і необмежений стек на базі списків. Ми розв'язуємо задачу,

для якої підходить будь-який стек. Він може взагалі не мати реалізації, покладаючись

на свої підкласи

 

template

class Stack

{

public:

Stack();

virtual bool empty() const=0;

virtual const T& top() const=0;

virtual void pop()=0;

virtual void push(const T& value)=0;

};

 

Такі класи називаються абстрактними, або чисто інтерфейсними класами. Клас

обмеженого стеку доведеться трохи підправити, оскільки в абстрактному класі

ми передбачили передачу параметрів і результатів вільну від копіювання, а тому

чисті типи замінені gсевдонімами

 

class BoundedStack: public Stack<int>, protected IntArray

{

private:

static const int bos; //bottom of stack

protected:

int _top;

public:

class BadStack{};

explicit BoundedStack(int size);

bool empty() const;

bool full() const;

const int& top() const;

void pop();

void push(const int& value);

};

 

 

 

Функція size() для об'єктів типу Stack стала недоступною, оскільки вона була

успадкована підкласом з іншого батьківського класу. При необхідності її появи

в інтерфейсі

 

int BoundedStack::size() const

{return IntArray::size();}