Об'єктно-орієнтоване програмування:Іспит
Матеріал з USIC Wiki
Особливості передачі параметрів та повернення значення функції. Указники і відсилки (pointers and references).
Постулат Вейнберга Артур Блох. “Закон Мерфі”
- Фактичний параметр - той який ми передаємо у функцію.
- Формальний параметр - той з яким ми працюємо всередені функціїї. Він може мати інше ім'я.
Наприклад :
void func(int с)
{
c = 0;
};
int main()
{
int x;
func(x);
}
Тут при визові у функцію "func" передається фактичний параметр "х", а працювати функція буде з формальним параметром "c".
Варіанти передачі параметрів у функцію. Чи копіюються параметри при передачі, зміна формального/фактичного параметрів.
- const T
- Копіювання
- Незмінний формальний параметр, немає впливу на фактичний параметр
- Попереджає про неможливість зміни параметру як користувача так і самого себе.
- T
- Копіювання
- Змінний формальний параметр, немає впливу на фактичний параметр
- T&
- Відсилка без копіювання
- Повний доступ до фактичного параметру (C++)
- Використовується для повернення значення роботи функфії
- const T&
- Стала відсилка без копіювання
- Фактичний параметр незмінний (C++)
- Тільки копіювання значення
- T*
- Указник передається значенням
- Повний але непрямий доступ до фактичного параметру; передача масивів (T[]) (в стилі чистого С)
const T*
- Маловживаний тип параметру; доступ лише для читання: сталий фактичний параметр.
- Краще використовувати const T&
- T* const
- Незмінний формальний параметр-указник, немає впливу на фактичний параметр-адресу (this в C++)
- const T*const
- Незмінний указник на сталий фактичний параметр
Повернення результатів функції
Результати значення :
- T f(…);
- const T f(…);
Результати відсилки :
- T& f(…);
- const T& f(…);
Результати указники :
- T* f(…):
Головна проблема параметра-указника
Як відрізнити масив від скалярного значення?
Вихід: жорстка дисципліна програмування, додатковий параметр типу size_t з відповідним коментарем
void f (double * px, size_t size_of_px);
Або void f (double px[], size_t size_of_px); - "[]" дужки попереду px наглядныше вказують що буде передаваться масив.
Але обережно, px все одно працює як указник !
Створення і ініціалізація об’єктів, довизначення конструкторів, замовчуваний конструктор, обмеження прав доступу до конструктора.
Створення і ініціалізація об’єктів
class T { T(T1,…,Tn); ~T(); // конструктор копіювання // створює новий об'єкт, ідентичний // переданому параметром T(const T&); // Можливий варіант: T(T&); // але не Т(Т) };
Автоматичний виклик конструктора і деструктора
int main() { // Визначення об'єктів приводить // до виклику конструкторів. // Ось він: WrappedVector w1, w2(n); // Життя об'єктів завжди закінчується // автоматичним викликом їх деструкторів // ост тут: return 0; }
Поверхневе і глибоке копіювання об’єктів, ініціалізація і присвоєння, копіювальний конструктор.
Поверхневе копіювання
Поверхневе копіювання - копіює всі значення полів об'єкту. Замовчувані оператор присвоєння і копіювальний конструктор здійснюють поверхневе копіювання. Поверхневе копіювання підходить якщо ВСІ поля об'єкту є значення. Якщо поле є указником який вказує на динамічно створену пам'ять, поверхневе копіювання працює не так як хотілось би. Наприклад :
class C
{
int a,b;
};
void main()
{
C c1,c2;
C c3 = c1; // Викликається копіювальний конструктор
c1 = c2; // Викликається оператор присвоєння
// Все вищенаведене працює як задумано.
}
Але :
class C
{
public:
int a, *b;
C(int _B) : a(0) , b(new int(_B)) {}; // b - це вказівник. Під нього виділяється дин. пам'ять.
~C(){ delete b; }; // Звільняємо пам'ять виділену під b.
};
void main()
{
C c1( 1 ) , c3( 3 ); // c1.b = 1 , c3.b = 3
C c2 = c1; // Викликається копіювальний конструктор за замовченням. Копіюється сам вказівник , пам'ять під c1.b не виділяється.
// Адреса c3.b = c1.b. Тобто обидва вказівники вказують на оду ділянку пам'яті.
c3 = c2; // Викликається оператор присвоєння. Знову та ж сама сітуація
// тут викликаються деструктори створених об'эктів. Першим викликається деструктор с1, він видаляє пам'ять на яку вказує с1.b
// Другим с2. Деструктор с2 намагаэться видалити пам'ять на яку вказує с2.b, але с2.b вказує на туж пам'ять що і с1.b.
// Пам'ять с1.b була видалена і delete в кращому випадку видасть fatal error. В гіршому - невідомо що він видалить.
// Та ж проблема с деструктором с3.
}
Глибоке копіювання
Глибоке копіювання - копіює поля і робить копії дин. пам'яті на яку вказують поля. Глибоке копіювання вирішує проблему поверхневого. Щоб запровадити глибоке копіювання треба написати належний конструктор копіювання і перевизначити оператор присвоєння. Наприклад щоб попередній приклад правильно працював треба написати :
class C
{
public:
...
C(const C& _C) : a(_C.a) , b( new int( *(_C.b) )) // copy constructor
C& operator = (const C& _C) { a = _C.a; *b = *(_C.b); return *this; } ;
};
// Зараз пам'ять виділяється при виклику коп. конструктора.
Загальний вигляд конструктора копіювання
T(<const> T& <const>); // тут "<>" дужки - необов'язковий параметр
Але не Т(Т) !
Конструктор копіювання створює новий об'єкт, ідентичний переданому параметром.
При ініціалізації об'єкта об'єктом того ж типу викликається копіювальний конструктор а не оператор присвоєння
Наприклад :
WrappedVector w0(val); WrappedVector w1 = w0 , w2(w1); // присвоєння не викликає оператор присвоєння тому що це ініціалізація а не присвоєння.
Конструктор копіювання викликається кожного разу, коли параметр або результат передаються значеннями
Загальний вигляд оператора присвоєння
<const> T<&> operator=(<const> T<&> <const>)
{
...
return *this;
}
Бажано повертати відсилку, щоб використовувати оператор як lvalue; Наприклад :
a=b=c , or (a=b)._some_member;
Повернення константної відсилки може викликати проблеми при використанні контейнерів зі стандартної бібліотеки.
Резюме
Конструктор копіювання створює новий об'єкт
Присвоєння звичайно замінює існуючий об'єкт іншим об'єктом (навіть якщо не доводиться видаляти попередні значення)
Присвоєння не можна визначити поза класом , це не має смислу. Присвоєння в класі T має тип T&, щоб можна було викор-ти його як lvalue. Присвоєння повертає *this, конструктори не повертають нічого.
Довизначення (overloading) арифметичних операцій, оператора присвоєння, оператора індексування.
Довизначення арифметичних операцій. Вступ
Довизначенню підлягають операції, хоча б один аргумент яких належить програмованому типу: класу, структурі або переліку
Довизначення можливе як у формі члена класу (методу), так і позакласної функції (утиліти)
Усі АРИФМЕТИЧНІ операції у С++ можна довизначити.
Я тут наведу думки дяді Страуструпа на цю тему. Сподіваюсь мене не посадять у зв'язку з копірайтом. Також наступний текст піде російською, бо Страуструп був напевно москаль.
Операції += , -= , *= , /=
Операции присваивания типа *=,+=,-=,/= могут быть очень полезными для работы с пользовательскими типами, поскольку обычно запись с ними короче, чем с их обычными "двойниками" * и + , а кроме того они могут повысить скорость выполнения программы за счет исключения временных переменных:
inline complex& complex::operator+=(complex a)
{
re += a.re;
im += a.im;
return *this;
}
Такі операції визначаються як T& operator <будь яка з 4-х наведених>(const T&)
Отметим, что в определенной подобным образом операции не нужных никаких особых прав доступа к классу, к которому она применяется, т.е. эта операция не должна быть другом или членом этого класса.
Операції + , - , * , /
Тоді операції +,-,/ можуть бути визначены за допомогою вищенаведених :
complex complex::operator +(complex a , complex b)
{
return complex(a+=b);
}
// Загострюю увагу, тут повертається копія об'єкта а не відсилка !!!
Також десь в класі цей оператор повинен визначатись як "друг" , щоб мати доступ до "приватних" членів класу
class complex
{
...
friend complex operator +(complex a , complex b);
}
Подібним чином довизначаються всі інші оператори
Довизначення оператора присвоєння
Довизначення оператора присвоєння може бути потрібним якщо в класі є члени які створені у дин. пам'яті, якщо у присвоєнні треба зробити додаткові дії. Наприклад потрібно у кожному об'єкті вести лічільник об'єктів які "ссилаються" на нього. Це може бути потрібним щоб уникнути дублювання при копіюванні об'єктів і правильному видаленню їх потім.
Довизначення операції індексування
І знову Страуструп :
Операторная функция operator[] задает для объектов классов интерпретацию индексации. Второй параметр этой функций (индекс) может иметь произвольный тип. Это позволяет, например, определять ассоциативные массивы.
Функция operator[]() должна быть членом класса. Обычные отношения эквивалентности, справедливые для операций со встроенными типами, могут не выполняться для пользовательских типов
Загальний вигляд довизначення :
const T<&> operator [](<тип індексу> _index) // для читання T& operator [](<тип індексу> _index) // для повного доступу
за семантикою Т - це тип даних який зберігається у класі.
Узагальнені функції (function template).
Що че таке, або about
Узагальнені функції - спеціальні функції що можуть оперувати узагальненими типами. Узагальнені типи(generic types) - це типи що визначаються у період компіляції программи. Під кожний набор узагальнених параметрів функції компілюється окремий код функції. Тобто інакше кажучи для кожного різного набору параметрів - своя функція.
Загальний вигляд :
// зарезервовані слова typename і class не відрізняються компілятором, для компілятора це одне і теж саме.
template <typename/class ReturnType , Param_1_Type , ...> // Імена ТИПІВ параметрів і значення яке повертає функція
ReturnType MyTemplateFunction(Param_1_Type _Param1 , ....) // Ще раз, Param_1_Type - це ім'я ТИПУ параметру.
// _Param1 - ім'я формального параметру
Якщо визивати десь цю функцію з різними параметрами, компілятором буде згенеровано стільки різних функцій, скільки різних наборів параметрів. Наприклад :
void main()
{
MyTemplateFunction( 5 , 0 ) // two integers as parameters
float temp = MyTemplateFunction( 0.5f , 0.0f ) // two floats as parameters. returns float
temp = MyTemplateFunction( 5 , 0.0f ) // integer and float as parameters. returns float.
}
тут може бути сгенеровано 3 різних функції, якщо правда не робити явного преведення типів.
Взагалі кажучи скіки різних функцій буде згенеровано це проблеми компілятора. Нас цікавить той факт що, наприклад, написавши узагальнену функцію яка множить матриці, ми будемо мати множення матриць типу int/float/etc.
Чому узагальнены функції можуть бути тільки в .h(headers) файлах ?
Тому що компілятор має сгенерувати код функції під кожен набор параметрів. В .сpp файлах код генерується остаточно і не підлягає подальшим змінам. Коротко кажучи, для .сpp файлів, компілятор не може сдогадатися чи він сгенерував всі потрібні функції. Цієї причини достатьньо.
Загальне правило - всі узагальнені функції і класи треба реалізовувати у хедерах.
Взагалі то кажучи стандарт С++ надає можливість виносити реалізацію уз. функцій і класів у окремі модулі. Але це поки реалізовано небагатьма компіляторами. Популярні зараз компілятори наврядчи коли-небудь це реалізують.(писалося у 2008 році).
Параметризовані класи (class template).
Параметризовані класи - це класи що можуть оперувати узагальненими типами :). Ці класи генеруються подібно узаганеним функціям в період компіляції. Призначення і особливість параметризованих класів - схожа структура класу з різними типами даних що визначаються у період компіляції.
Визначення простого параметризованого класу :
template<typename/class Type>
class SomeClass
{
public:
Type a,b;
int Foo();
};
// Класс що має 2 члена типу Type
Так визначається функція поза пар. класом :
template<typename/class Type>
ReturnType ClassName::MethodName(Params){...}
Наприклад функція Foo з попереднього опису класу буде визначатися так :
template<typename Type>
int SomeClass::Foo( /* немає параметрів */ ){...}
Параметрами параметризированого класу не обов'язково мають бути ім'я типу. Це може бути строка, ім'я функції або константа. Наприклад :
typedef void(*ptrToSomeFunction)(int) ; // ptrToSomeFunction зараз ім'я типу. Цей тип - це указник на функцію що повертає void і приймає параметр типу int
template <typename T , int size , ptrToSomeFunction ptr >
class TestClass
{
public:
T* array;
ptrToSomeFunction ourPtr;
TestClass() { array = new T[size]; } ;
~TestClass() { delete[] array; };
void Foo(){ ourPtr = ptr; };
};
Визначення об'єктів класу :
SomeClass<int> A; // члени а,b мають тип int/ SomeClass< std::string& > B; // члени a,b мають тип std::string&
для цих двох визначень скомпылюється 2 окреми класи.
Спеціалізація параметризованих класів
Параметрізовані класи можна спеціалізувати(template specialization).
Спеціалізація пар. класу - написання більш конкретної реалізації цього класу. Це можна зробити шляхом підстановки конкретніх типів у параметри шаблону і більш звуженої реалізації методів. Тобто весь пар. клас переписується під конкретний набір параметрів.
Нехай є клас :
template <class T , class U>
class SomeClass
{
public:
T a;
U b;
void Foo() { /* do something */ };
};
Повна спеціалізація пар. класу - це підстановка конкретних типів або значень у всі параметри шаблону. Вищенаведений клас з повною спеціалізацією :
template <> // зверніть увагу на синтаксис. При повный спеціалізації ці дужки порожні.
class SomeClass<int,int> // ми вказуємо що ця реалізація буде використана якщо обидва параметри класу є int'ами.
{
public:
T a;
U b;
void Foo();
};
Повна спеціалізація використовується коли ми бажаємо передбачити поведінку класу для не стандартних умовах(значеннях параметрів). При повній спеціалізації зазвичай переписуються всі методи класу. Ті методи що не заміщуються , будуть взяті з стандартної реалізації. Як приклад навіщо потрібна спеціалізація можна навести алгоритм сортування. Нехай сортування виконує якийсь клас. Тоді сортування числових даних може робити стандартний алгоритм, але для сортування строк тексту він може не підійти. Тому ми можемо додати повну спеціалізацію у клас сортування, яка буде відповідати за сортування строк.
Часткова спеціалізація - щось середнє між звичайним пар. класом і повною спеціалізацією. При частковій спеціалізації реальний шаблонний параметр виводиться з шаблонного параметру спеціалізації. Це краще продемонструвати на прикладах. Також зверніть увагу на синтаксис :
// стандартне визначення. тут немає ніякої спеціалізації
template<typename T, typename U>
class Some{...};
template<typename T , int> // перший тип будь-який, другий тип повинен бути int. Це простий приклад часткової спеціалізації.
class Some{...}
// тут перший параметр виводиться з списку параметрів шаблону. Перший параметр повинен бути вказівником на T. Другий - int. template<class T , class U> class Some<T* , int>
Відкрите, закрите і захищене успадкування.
- Успадкування лише реалізації. private: відкрита і захищена частини базового класу може використовуватися в похідному класі
- protected: транзитивне (захищене) ― відкрита і захищена частини базового класу може використовуватися в усіх похідних класах
- Успадкування реалізації і поведінки ― public: відкрита частина базового класу стає одночасно відкритою частиною похідного класу
Формула (відкритого) успадкування :
- Підоб'єкт успадковує всі властивості (атрибути) і поведінку (відкриті методи) базового об'єкту
- Підоб'єкт може мати власні додаткові властивості і поведінку
- Підоб'єкт не має доступу до закритої частини базового об'єкту
Закрите успадкування :
- Підклас має доступ до відкритої і захищеної частини базового класу, але не передає цих прав своїм підкласам
Транзитивне (захищене) успадкування :
- Підклас передає своїм підкласам доступ до відкритої і захищеної частини базового класу так, як ніби вони складали його захищену частину
- Об'єкти що не є в цепочці наслідування класу, не мають доступу до захищеної частини.
Успадкування із спільного базового класу. Домішки (mix-in).
Успадкування із спільного базового класу :
class Rectangle: public Parallelogram; class Rhombus: public Parallelogram;
Mix-in: кратне успадкування :
class Cat: public Predator, public Pet // успадковує властивості хижака і питомця.
Тобто це успадкування інтерфейсу.
Якщо в прикладі успадкування із спільного базового класу з'являється ще один клас що успадковує методом кратного успадкування від класів Rectangle і Rhombus :
class Square : public Rectangle , public Rhombus;
В цьому випадку конструктор базового класу Parallelogram буде викликаний двічі тому що викличуться конструктори Rectangle і Rhombus, кожен з яких викличе конструктор Parallelogram'у.
Bіртуальне успадкування
Щоб позбутися цього порочного наслідування і не викликати двічи конструктор Parallelogram'у була придумана така штука як віртуальне успадкування. Виглядає воно так :
class Rectangle : virtual public Parallelogram; class Rhombus : virtual public Parallelogram; class Square : public Rectangle, public Rhombus;
Конструктор прямокутника (ромба) викличе конструктор паралелограма лише за умови, що його раніше не викликав конструктор квадрату.
Як працює віртуальне успадкування
Вирізки взято з "Marshall Cline's C++ FAQ Lite document, [1]"
Нехай є така ієрархія :
Base
/ \
/ \
virtual / \ virtual
Der1 Der2
\ /
\ /
\ /
Join
Якщо б в цієї ієрархії не було вірт. успадкування, порядок визову конструкторів був би такий(з урахуванням що Der1 визначено першим): Join() => Der1() => Base(), після завершення списків ініціалізації Base і Der1, але все ще в списку ініціалізації Join, буде викликано Der2 => Base. Бачите 2 інстанса Base y Join.
Перейдемо до віртуального успадкування.
Список ініціалізації конструктору останнього в ієрархії класа(в нашому випадку це Join) напряму(читай першим) викликає конструктор віртуального базового класу(Base).
Тому що підоб'єкт віртуального базового класу створюється в єдиному екземплярі на об'єкт, є спеціальні правила щоб переконатися що конструктор і деструктор віртуального базового класу будуть виконані раз на інстанс. Правилами С++ зазначено що віртуальні базові класи створюються попереду усіх не вірт. базових класів.
Тобто в нашому прикладі Join має спочатку визвати конструктор Base а потім вже похідні. Похідні у свою чергу взагалі не повинні визивати конструктора вірт. базового класу. Деякі компілятори спочатку втиху визивають конструктори базових невіртуальних для похідного класів, а потім вже баз. вірт-й.
Статичне і динамічне зв’язування: поліморфізм, віртуальні функції
Тип связывания или тип компоновки определяет соответствие имени объекту или функции в программе, исходный текст которой располагается в нескольких модулях. Различают статическое и динамическое связывание.
Статическое связывание бывает внешним или внутренним. Оно обеспечивается на стадии формирования исполнительного модуля, ещё до этапа выполнения программы.
При динамическом связывании компоновщик не имеют никакого представления о том, какой конкретно объект будет соответствовать данному обращению. Динамическое связывание обеспечивается транслятором в результате подстановки специального кода, который выполняется непосредственно в ходе выполнения программы.
В С++ полиморфизмом называется способность иметь несколько реализаций с тем же самым названием (именем). С++ поддерживает 3 типа полиморфизма: полиморфизм методов (перегруженные функции), полиморфизм операторов (перегруженные операторы) и полиморфизм классов, который состоит в том, что можно сослаться на класс-потомок так же, как и на его суперкласс.
"Статичний" поліморфізм : перезагрузка функцій/операторів, передача об'єкта належащего до класу нащадка, функції що приймає аргументом указник або відсилку на базовий клас. Поліморфізм може ще проявлятися у складних цепочках наслідування. Приклади :
class A : public B , public C; class D : public A , C; // Поліморфізм :)
class Base; class Derived : public Base; void Func(Base& _AsBase); // приймає референс на базовий клас ... Derived A; Func(A); // а ось тут функція приймає клас нащадок як базовий.
"Динамічний" поліморфізм або піздне зв'язування : віртуальні функції , вказівники/відсилки на базовий клас з віртуальними функціями.
Віртуальна функція - функція що може бути заміщена у класу нащадку. При виклику цієї функції як методу базового класу через вказівник/відсилку, викликається метод того класу до якого належав об'єкт котрий викликав метод.
Синтаксис :
class Some
{
....
virtual Foo(); // ключеве слово virtual
virtual Bar() = 0; // Пояснення далі
};
Вираз " = 0 " обозначає що ця функція повинна бути заміщена у класу нащадку. У базовому класі таким чином визначена функція може не мати реалізації. Такі віртуальні функції називаються чисто віртуальними(pure virtual).
Приклад "динамічного" поліморфізму :
class Base
{
...
virtual int Print(){std::cout << "Base::Print";};
};
class Derived : public Base
{
...
virtual int Print(){std::cout << "Base::Print";};
};
...
Base *D = new Derived; // указник на базовий клас. Насправді створюється клас нащадок. Ще один приклад статичного поліморфізму.
D->Foo(); // Викличиться метод класу нащадка. Піздне зв'язування, дин. поліморфізм.
Успадкування інтерфейсу і успадкування реалізації. Абстрактні класи.
Успадкування інтерфейсу - успадкування сигнатур функцій класу. Тобто успадкувуються всі функціїї, але їх можна перевизначити. Успадкування реалізації - успадкування сигнатур і реалізації функцій класу.
В С++ успадкування реалізації - це відкрите успадкування. Успадкування інтерфейсу - успадкування абстрактного класу.
Абстрактний клас - той що має хоч одну чисто віртуальну функцію(та що має " = 0 " на кінці сигнатури). Об'єкт абстрактного класу створити не можна, тому що в ньго не реалізован один і більше методів.
Успадкування інтерфейсу використовується тоді коли треба забеспечити різну функціональність схожим за семантикою класам. Доступ до функціональності цих класів зазвичай надається через указник/відсилку на базовий тип(абстрактний клас).
Наприклад :
class BaseVirtual
{
...
virtual void DoSmth() = 0; // pure virtual , so the class is abstract. No realization
virtual void DoByDefault(){...}; // Realization by default, it may be redefined in derived classes
void Do(); // simple function
};
class Derived : public BaseVirtual
{
...
virtual void DoSmth(){...};
virtual void DoByDefault(){/* another realization */};
}
У випадку з чисто віртуальною функцією DoSmth - це успадкування інтерфейсу. У випадку з простою віртуальною функцією DoByDefault - це успадкування інтерфейсу або реалызації.Наприклад якщо б у класі Derived не було перевизначення методу DoByDefault, то цей метод перейшов би до Derived з базового класу. Do - успадкування реалізації.

