1   2   3   4
Ім'я файлу: prog_lecture_07.pdf
Розширення: pdf
Розмір: 205кб.
Дата: 29.09.2021
скачати

Лекція
7
Класи
1
Лекція
7
Класи
У
ційлекції...
7.1.
Члени класу
і
керування доступом
7.2.
Конструктори
і
деструктор
7.3.
Дружні
функції
і
класи
Як ми уже відзначали, мова С++ є універсальною. У першій частині курсу розглянуті його імперативні (чи процедурно-орієнтовані) властивості, що, в основному, забезпечуються засобами мови С і деякими додатковими механізмами (посиланнями, inline-функціями, операторами приведення типів і ін.). Основу парадигми
імперативного програмування складає алгоритм, що обробляєдані. У рамках цього підходу алгоритм і дані
відділені друг від друга. Головна роль в імперативному програмуванні приділяється алгоритму, а дані
знаходяться в тіні.
Загалом імперативний підхід можна описати в такий спосіб. Програма розбита на модулі, що реалізують різні алгоритми, як правило вузькоспеціалізовані. Модулі одержують дані, обробляють їх і передають іншим модулям. Координація роботи здійснюється головним модулем: він викликає підпрограми, передає їм дані і
повертає результат програми. Модулі є відносно самостійними. Вони можуть викликати один одного, самих себе і навіть головний модуль, вводити з зовнішнього світу додаткові дані і виводити результати.
Потреби програмування, поставленого на промислову основу, неможливо повною мірою задовольнити за допомогою імперативного програмування. Навіть усередині однієї команди необхідно погоджувати деталі
представлення даних і реалізації алгоритмів, спрямованих на рішення загальної задачі. Програмісти, що не погодили свої дії, ризикують створити несумісні модулі. Незначне відхилення від загального плану може привести до серйозних наслідків. У силу цих причин виникла необхідність створити таку концепцію програмування, що підвищила б продуктивність і надійність роботи програмістів, дозволивши застосувати конвеєрні методи роботи. Так виникла парадигма об'єктно-орієнтованогопрограмування.
В основу об'єктно-орієнтованого програмування покладене поняття об'єкта і принципи інкапсуляції,
прихованняінформації, абстракціїданих, успадкування і поліморфізму.
Основна ідея об'єктно-орієнтованого програмування полягає в тім, що всі обчислення відбуваються усередині об'єктів, що обмінюються між собою повідомленнями. У деякому сенсі об'єктно-орієнтоване програмування можна вважати вищим ступенем розвитку модульного програмування; тільки роль модулів тут грають об'єкти. В імперативному програмуванні вся інформація оброблялася усередині модулів, причому дані
не залежали від них. Тепер об'єкти наділені можливістю не тільки зберігати, але й обробляти дані. Наприклад,
якщо в імперативних програмах структура являє собою просту сукупність записів, то в об’єктно- орієнтованих — це активний об'єкт, що не просто зберігає інформацію, але й обробляє її. У цьому і полягає
принципінкапсуляції.
Отже, об'єктцеоб'єднанняданихіалгоритмів, щообробляютьцідані. Тепер ми можемо занурити масив
і метод його сортування в деякий об'єкт, а потім звернутися до цього об'єкта з проханням упорядкувати елементи масиву і видати результат. Основна перевага такого підходу — гнучкість. Якщо даний модуль є
частиною більш складної конструкції, необхідно лише погодити способи його оголошення з зовнішнім світом — те, що називають інтерфейсом. Його внутрішній світ інших програмістів не стосується. Як зберігаються дані усередині модуля і як вони обробляються, повинний знати тільки творець даного об'єкта. А
якщо інші програмісти виявляться занадто безцеремонними і захочуть змінити внутрішню структуру об'єкта? Як захистити його від стороннього втручання? Для цього використовується принцип прихованняінформації. В об’
єктно-орієнтованому програмуванні внутрішню структуру об'єкта можна розділити на три розділи: відкритий,
закритий і захищений. Не вдаючись у передчасні подробиці, помітимо, що відкритий розділ доступний усім сутностям програми (інакше кажучи, видний з будь-якої її точки), закритий розділ — лише внутрішнім сутностям об'єкта, а захищений — тільки об'єктам, створеним з даного об'єкта (спадкоємцям).
Для того щоб програма була ефективною, необхідно, щоб об'єкти добре відповідали розв'язуваній задачі.
Однак згодом об'єкт може морально застаріти. Наприклад, може з'явитися новий надшвидкісний алгоритм сортування. Що робити? Створити новий об'єкт? Змінити старий? Для того щоб полегшити задачу, вирішили розділити схему об'єкта і його реалізацію. Це дозволяє програмісту описати, що може робити об'єкт, не конкретизуючи, як це робити. У цьому полягає принцип абстракціїданих. Принципова схема об'єкта не повинна залежати від її конкретного наповнення. Необхідно лише вказати, що об'єкт повинний містити певну операцію. Якщо алгоритм знадобиться змінити, ми модифікуємо його реалізацію, але загальна структура об'єкта від цього не постраждає.
Уявимо собі, що нам знадобилося створити новий об'єкт, що володів би можливостями старого, але вмів би робити і щось нове. Що робили в минулому? Копіювали зміст старого модуля і додавали в нього нові функції.

Лекція
7
Класи
2
Тепер усе це можна робити набагато простіше! Ми просто створюємо об'єкт, що успадковує усі властивості
свого попередника, і додаємо в нього нові можливості. Цей механізм називається успадкуванням.
Об'єкт може одержувати різноманітну інформацію. Зрозуміло, для її обробки можна заздалегідь передбачити його реакцію, написавши відповідні функції. Однак в об’єктно-орієнтованих мовах існує можливість перевантажувати операції і функції, що дозволяє об'єкту самому конкретизувати їхній зміст у ході виконання програми. Що дозволяє спростити програмування. Наприклад, якщо перевантажити оператор
+
, то операція a + b може мати різний сенс. Якщо ці об'єкти — числа, застосовується звичайна операція додавання, якщо матриці — матрична (конечно, для цього необхідно визначити цю операцію). У цьому (і не тільки) полягає
сутність поліморфізму. У принципі поліморфізм виявляється в трьох ситуаціях: при перевантаженні функцій,
при перевантаженні операторів і при пізнім зв'язуванні. Однак ці терміни нам ще має бути освоїти.
Описуючи принципи об'єктно-орієнтованого програмування, ми оперували поняттям об'єкт. Однак об'єкт —
це фізична сутність, що виникає при виконанні програми, тобто сукупність комірок пам'яті, що зберігають дані і
код. Для того щоб створити об'єкт, необхідна його схема: які дані він містить, які функції обробляють ці дані, як організований доступ до цих даних і функцій, що називаються членамиоб'єкта. У мові С++ схемою об'єкта називається клас.
7.1.
ЧЛЕНИ
КЛАСУ
І
КЕРУВАННЯ
ДОСТУПОМ
Для оголошення класу в мові С++ призначене ключове слово class
. Усе, що розташовано між фігурними дужками, що слідують за цим ключовим словом, являє собою тілокласу, що складається з його членів. Варто пам'ятати, що клас — це логічна схема об'єкта. Отже, виділення пам'яті відбудеться лише тоді, коли об'єкт буде визначений. Розглянемо загальний вид оголошення класу.
class
ім'я{
члени класу;
private:
члени класу;
protected:
члени класу;
public:
члени класу;
}
<список об'єктів>;
Правила, за якими утворюється клас, нічим не відрізняються від правил, установлених для структур. (У
сучасних компіляторах між структурами і класами взагалі немає різниці, тобто ключові слова struct
і class
є взаємозамінними.)
Визначати об'єкти відразу після оголошення класів не обов’язково — це можна зробити в придатному місці
програми. Як показане вище, тіло класу розділяється на три розділи, позначені наступними ключовими словами
(специфікаторами доступу).
public //
Відкритий розділ private //
Закритий розділ protected //
Захищений розділ
Ключове слово public позначає розділ, у якому розміщаються функції і дані, доступні з будь-якої точки програми, — відкриті члени. Ключове слово private оголошує розділ, у якому розташовані закриті члени класу, що є доступними лише функціям-членам самого класу, а також дружнім функціям і класам. Зверніть увагу на те, що функції-члени можуть знаходитися в ніби безіменному розділі. Насправді, за замовчуванням функції і дані, оголошені в такому розділі, є закритими. Зміст специфікатора protected буде розкритий пізніше при описі спадкування класів.
Порядок проходження специфікаторів доступу не регламентується — вони можуть повторюватися неодноразово й у будь-якому порядку. Кожен наступний специфікатор припиняє дія попереднього. Як правило,
відкриті члени оголошуються останніми, але це стосується питань стилю, а не до синтаксичних правил.
Розглянемо конкретний приклад.
Доступ до члена класу
#include
class TComplex
{
double Re;
double Im;
public:
void print() {printf("Z = %lf + i*%lf \n",Re,Im);}
void init() {Re = 1; Im = 2;}
};
int main()

Лекція
7
Класи
3
{
TComplex Z;
// Z.Re = 1;
Помилка
!
Член
Re знаходиться в
закритому розділі
!
// Z.Im = 2;
Помилка
!
Член
Im знаходиться в
закритому розділі
!
Z.init(); //
Виклик функції
- члена init()
Z.print(); //
Виклик функції
- члена print()
return 0;
}
На екран виводиться число
Z = 1.000000 + i*2.000000
У цій програмі оголошений клас
TComplex
, що описує структуру комплексного числа і дії над ним. У
даному випадку дій небагато: задати початкове значення і вивести число на екран. Визначення інших операцій відкладемо на майбутнє. Поки нас цікавить два питання: як здійснюється доступ до членів класу і як створюється об'єкт класу.
Члени
Re
і
Im
(дійсна і уявна частини комплексного числа) у класі
TClass
є закритими. З цієї причини звернутися до них з функції main()
неможливо. Для того щоб забезпечити доступ до членів
Re
і
Im
, у відкритому розділі класу розміщені функції init()
і print()
. Вони є членами класу і, отже, мають прямий доступ до будь-якого іншого членуа класу.
Функції print()
і init()
відносяться до різних категорій. Функція print()
просто виводить на екран значення членів об'єкта
Z
, не змінюючи їх. Такі функції-члени називаються функціямидоступу.
Функція init()
привласнює полям
Re
і
Im задані значення. Такі функції-члени іменуються такими, що модифікують. Обидві функції init()
і print()
утворюють інтерфейс класу.
Зверніть увагу на те, як відбувається звертання до членів класу. Для цього використовується оператор доступу до членів класу «
».
Z.init(); //
Виклик функції
- члена init()
Z.print(); //
Виклик функції
- члена print()
Якщо в програмі оголошений вказівник на об'єкт класу, використовується оператор посилання на члени класу «
->
». Проілюструємо його застосування наступним прикладом.
Посилання на член класу
#include
#include
class TComplex
{
double Re;
double Im;
public:
void print() {printf("Z = %lf + i*%lf \n",Re,Im);}
void init() {Re = 1; Im = 2;}
};
int main()
{
TComplex* p = (TComplex*)malloc(sizeof(TComplex));
// Z.Re = 1;
Помилка
!
Член
Re знаходиться в
закритому розділі
!
// Z.Im = 2;
Помилка
!
Член
Im знаходиться в
закритому розділі
!
p->init(); //
Виклик функції
- члена init()
p->print(); //
Виклик функції
- члена print()
return 0;
}
Клас не може містити власний об'єкт, оскільки опис об'єкта повинен передувати його створенню, а в оголошенні
class TClass
{
TClass Z; //
Не можна
!
TClass* p; //
Можна
!
};
опис класу ще не довершено. Правда, вказівник на об'єкт класу може міститися усередині самого класу.
Крім того, при оголошенні полів класу не можна користатися ключовими словами auto
, extern
і
register

Лекція
7
Класи
4
7.1.1.
Структури й
об
'
єднання як різновиди класів
У мові С++ між класами, з одного боку, і структурами й об'єднаннями, з інший, не багато розходжень.
Розглянемо деякі з них.
Для початку подамо наш клас
TComplex у вигляді структури. Для цього досить просто замінити ключове слово class словом struct
Структуранайпростіший клас struct TComplex
{
double Re;
double Im;
public:
void print() {printf("Z = %lf + i*%lf \n",Re,Im);}
void init() {Re = 1; Im = 2;}
};
На структури поширюються усі властивості класів, навіть зв'язані зі спадкуванням. Таким чином, ключові
слова class
і struct цілком еквівалентні. Однак, щоб не виникало плутанини, структурою звичайно називають сукупність даних (без функцій-членів), а класом — структуру з функціями-членами. Єдиною відмінністю між структурами і класами є рівень доступу до їхніх членів, прийнята за замовчуванням: якщо рівень доступу (
public
, private чи protected
) не зазначений явно, усі члени структури вважаються відкритими, а всі члени класу — закритими.
З класами також тісно зв'язані об'єднання. Оскільки цей вид структури більш складний і зв'язаний з досить хитромудрим розподілом пам'яті, на нього поширюються деякі обмеження. Зокрема, до об'єднань не можна застосовувати механізм успадкування; вони не можуть містити віртуальні функції, а також статичні змінні і
посилання. Крім того, об'єднання не повинні містити об'єкти класів з перевантаженим оператором присвоювання, а також явно визначеними конструкторами і деструктором.
Не варто намагатися реалізувати клас
TClass у виді об'єднання.
union TComplex
{
double Re;
double Im;
public:
void print() {printf("Z = %lf + i*%lf \n",Re,Im);}
void init() {Re = 1; Im = 2;}
};
У цьому випадку змінні
Re
і
Im будуть зберігатися в одній і тій же області пам'яті, перекриваючи один одного. Тому при виклику функції print()
на екран замість числа
Z = 1.00000 + i*2.000000 буде виведене число
Z = 2.000000 + i*2.000000
Цікаво, а скільки пам'яті займає об'єкт класу, структури й об'єднання? Як і випливало очікувати — 17, 17 і 8
байт відповідно. Звідси випливає, що скільки б членів-функцій ні містив клас, на розмір його об'єкта впливає
лише кількість і тип його змінних-членів. Розмір функцій-членів не враховується. Крім того, розмір об'єкта
«порожнього класу» (тобто класу, що не має ни члена) також не є однозначно визначеним: різні компілятори повертають різні розміри (1 чи 2 байти).
7.1.2.
Функції
-
члени класу
,
що підставляються
Повернемося до нашого класу
TComplex
. Функції-члени print()
і init()
мають дуже простий зміст і
невеликі по обсязі. Отже, має сенс зробити їх що підставляються. Як відомо, для цього варто поставити перед їх визначенням ключове слово inline inline void print() {printf("Z = %lf + i*%lf \n",Re,Im);}
inline void init() {Re = 1; Im = 2;}
Тепер ці функції будуть не викликатися, а підставлятися у відповідне місце програми. Для членів класу все набагато простіше. Якщо визначення функції-члена міститься усередині класу, сама функція автоматично вважаєтьсятакою, що підставляється, або inline-функцією (хоча і не стає нею автоматично). Ключове слово inline для цього вказувати необов'язково, хоча і не заборонено. Зрозуміло, на функцію-член, що підставляються, поширюються ті ж обмеження, що і на звичайні inline-функції, тому, зокрема, не слід розміщати усередині оголошення класу великі функції — це знижує наочність класу і не робить функцію дійсно inline.
7.1.3.
Визначення функцій
-
членів поза класом
Як ми уже відзначили, визначення деяких функцій-членів класу варто розміщати за межами його оголошення, залишаючи всередині лише їх прототипи. Остання вимога обов'язкова! Саме воно робить конкретну функцію членом класу.

Лекція
7
Класи
5
Для того щоб зв'язати визначення функції-члена з класом, якому вона належить, перед її ім'ям вказується ім'я класу й оператор дозволу області видимості
::
. Перепишемо нашу програму, виходячи з нових розумінь.
Визначення функції
-
члена поза класом
#include
#include
class TComplex
{
double Re;
double Im;
public:
void print();
void init();
};
int main()
{
TComplex* p = (TComplex*)malloc(sizeof(TComplex));
// Z.Re = 1;
Помилка
!
Член
Re знаходиться в
закритому розділі
!
// Z.Im = 2;
Помилка
!
Член
Im знаходиться в
закритому розділі
!
p->init(); //
Виклик функції
- члена init()
p->print(); //
Виклик функції
- члена print()
return 0;
}
void TComplex::print() {printf("Z = %lf + i*%lf \n",Re,Im);}
void TComplex::init() {Re = 1; Im = 2;}
7.1.4.
Вказівник
this
При виклику функція-член одержує вказівник на зухвалий об'єкт. Цей вказівник позначається ключовим словом this
. Отже, вказівник this на об'єкт класу
T
має тип
T*
Повернемося до програми, у якій створюється об'єкт класу
TComplex
. Явне застосування вказівника this у
даному випадку зовсім зайве, але наочно демонструє механізм, що дозволяє функції-члену класу з'ясувати, який об'єкт її викликає.
Доступ до члена класу за допомогою вказівника
this
#include
#include
class TComplex
{
double Re;
double Im;
public:
void print();
void init();
};
int main()
{
TComplex* p = (TComplex*)malloc(sizeof(TComplex));
p->init(); //
Виклик функції
- члена init()
p->print(); //
Виклик функції
- члена print()
return 0;
}
void TComplex::print() {printf("Z = %lf + i*%lf \n",this->Re,this->Im);}
void TComplex::init() {this->Re = 1; this->Im = 2;}
Однак необхідно мати у виді, що змінна this
— не звичайний вказівник. Зокрема, їй не можна нічого привласнювати і вона не має адреси.

Лекція
7
Класи
6
Неправильне використання вказівника
this
#include
#include
class TComplex
{
double Re;
double Im;
public:
void print();
void init();
void noncorrect(TComplex*);
};
int main()
{
TComplex* p = (TComplex*)malloc(sizeof(TComplex));
p->init(); //
Виклик функції
- члена init()
p->print(); //
Виклик функції
- члена print()
TComplex* q = (TComplex*)malloc(sizeof(TComplex));
q->noncorrect(p);
return 0;
}
void TComplex::print() {printf("Z = %lf + i*%lf \n",this->Re,this->Im);}
void TComplex::init() {this->Re = 1; this->Im = 2;}
void TComplex::noncorrect(TComplex* p)
{
this = p; //
Помилка
: вказівнику this нічого не можна привласнювати
!
printf("&this = %p \n",&this); //
Помилка
: вказівник this не має
адреси
!
}
Вказівник this відіграє важливу роль при перевантаженні операторів (зокрема, для перевірки самоприсвоювання). При цьому варто пам'ятати, що вказівник this завжди вважається константним і ніколи не передається статичним функціям-членам.

  1   2   3   4

скачати

© Усі права захищені
написати до нас