C++/Об'єктно-орієнтовне програмування/Класи

Матеріал з Вікіпідручника
< C++

Класи[ред.]

Основні поняття[ред.]

Об'єктно-орієнтовне програмування - це програмування, яке фокусується на даних, при чому дані і логіка, що стосується цих даних нерозривно поєднані. Разом дані і функції представляють собою єдиний клас як деяку абстракцію даних, а об'єкти є екземплярами класу. Абстракція даних — це виділення суттєвих характеристик об'єкта, котрі відрізняють його от інших об'єктів, чітко визначаючи його межі. В рамках ООП, передбачається виділення характеристик, суттєвих для вирішення даної задачі.

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

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

Поля (або властивості) — описують дані, які можуть зберігати в собі екземпляри класу. До полів в середині класу можна звертатися безпосередньо по іменам полів.

Методи класу — це функції, котрі можуть застосовуватися до екземплярів класу. Грубо кажучи, метод – це функція, яка оголошена всередині класу і призначена для роботи з його об'єктами.

Основні принципи ООП:

  1. Інкапсуляція — це принцип, згідно якого кожен клас повинен розглядатися як чорний ящик — користувач класу повинен бачити і користуватися лише інтерфейсною частиною класу (тобто списком задекларованих властивостей і методів класу) і не розбиратися в його внутрішній організації. Тому дані прийнято інкапсулювати (приховувати) в класі таким чином, щоб доступ до них для читання чи запису відбувався не на пряму, а за допомогою методів. Принцип інкапсуляції (теоретично) дозволяє мінімізувати кількість зв'язків між класами, і відповідно, спростити незалежну реалізацію і модифікацію класів.
  2. Спадкування — це створення нового класу-спадкоємця від уже існуючого батьківського класу. Наслідування відбувається з передачею усіх або деяких полів і методів від батьківського класу до класу спадкоємця. В класі-спадкоємці в процесі наслідування можливо, при необхідності, додавати нові властивості і методи. Набір класів, які пов'язані між собою відношенням наслідування, називають ієрархією.
  3. Поліморфізм — це явище, при якому функції (методу) з одним і тим самим ім'ям відповідає різна програмна реалізація в залежності від того, в якому контексті вона викликається (об'єкт якого класу викликається, або з якими параметрами).

Синтаксис[ред.]

Класи оголошуються за допомогою ключового слова class, і синтаксис має наступний формат:

class ім'я_класу { 
   специфікатор_доступу_1: 
   член_класу1; 
   специфікатор_доступу_2: 
   член_класу2; 
   ... 
} імена_об'єктів;

Де ім'я_класу є ідентифікатором класу, імена_об'єктів є не обов'язковим списком імен об'єктів цього класу. Тіло класу міститься у фігурних дужках і може містити оголошення членів класу, які можуть бути як полями даних так і методами, і при необхідності можуть бути вказані специфікатори доступу до них.

Наприклад:

// клас з ідентифікатором CRectangle
class CRectangle { 
  int x, y;                   // поля класу
  public:                     // специфікатор доступу
  void set_values (int,int);  // методи
  int area (void); 
} rect;                       // rect є об'єктом класу CRectangle

Синтаксис оголошення класу дуже схожий на оголошення структур даних, за винятком того, що ми можемо також додавати оголошення функцій членів класу, а також можемо вказувати специфікатори доступу.

Після оголошення класу і його об'єкта, в програмі ми можемо звернутися до відкритих членів об’єкту, через задавання імені класу, після якого через точку (.) вказують ім'я члену класу (функції або поля). Наприклад, для наведеного вище прикладу, десь у тілі програми можна написати такий код:

rect.set_values (3,4); 
myarea = rect.area();

Члени класу можна визначати за його методами, це в першу чергу стосується функцій. Інтерфейсну частину класу і реалізацію зазвичай розносять в різні файли, в файл заголовку (*.h) і файл початкового коду (*.cpp) відповідно. Для визначення методів за межами класу в такому випадку використовується спеціальний оператор розмежування області видимості ::, без використання якого не зрозуміло було б що це не є звичайна глобальна функція, а це є функція, що належить класу. Приклад:

void CRectangle::set_values (int a, int b) { 
    x = a; 
    y = b; 
}

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

Рівні доступу до членів класу[ред.]

По рівню доступу всі члени класу діляться на:

  • Public (відкриті) – доступні як в середині так і ззовні класу (в тому числі в його нащадках),
  • Private (закриті) – доступні лише в середині класу,
  • Protected (захищені) – доступні всередині класу і в середині всіх його нащадків.

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

class Flower {
    public: // після цього модифікатору слідують відкриті члени класу.
        void setHeight(float value)
        {
            if (value > 0)
                this->height = value;
        }

        float getHeight()
        {
            return height;
        }

        void setName(std::string value)
        {
            this->name = value;
        }

        float getName()
        {
            return name;
        }

    private: // закриті. 
        float height;    // Доступ до цих полів здійснюється за допомогою методів
        std::string name;
};

Конструктор і деструктор[ред.]

Об'єктам зазвичай необхідно задавати початкові значення змінним (полям класу) для того, щоб вони мали якийсь змістовний вміст, або виділяти динамічну пам'ять під час створення. Інакше при використанні об'єкту можна отримати непередбачувану поведінку при виконанні програми. Наприклад, що б сталося у попередньому прикладі, якби ми викликали метод, який рахує площу чотирикутника не задавши йому розмір за допомогою методу set_values()? Напевне вона б повернула, якийсь невизначений випадковий результат, оскільки б змінним x і y ніде б не задавались значення.

Для того, щоб уникнути такої поведінки, клас може мати спеціальний метод, який називається конструктором.

Конструктор класу — це спеціальна функція, котра автоматично викликається одразу після створення об'єкту. Він не має значення, що повертається, і називається так само як клас. Зазвичай конструктори використовуються для того, щоб задати початкові значення полям класу, адже вони не ініціалізуються автоматично і в них будуть знаходитися випадкові значення. Конструктор не можна викликати на пряму, так ніби це звичайний метод класу. Вони виконуються лише при створенні нового об'єкту класу.

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

class Flower {
    public: 

        Flower(float h, std::string n) // Конструктор
        {
            this->height = 0;
            if (h > 0)
                this->height = h;

            this->name = n;
        }

        //... інші методи

    private:  
        float height;    
        std::string name;
};

int main()
{
    // Передаємо значення в конструктор
    Flower *sunflower = new Flower(54.2, "Соняшник");
    return 0;
}

Деструктор класу забезпечує протилежну функціональність. Він автоматично викликається коли об'єкт знищується, або якщо виходить за межі простору видимості і більше не використовується (наприклад він був визначений як локальний об'єкт в середині функції, яка закінчила своє виконання), або якщо об'єкт був створений динамічно, і знищується за допомогою оператору delete.

Деструктор має називатися так само як клас, але на початок його імені ще додається символ "~", і він також не повертає ніякого значення. Використання деструктора в першу чергу необхідне, коли об'єкт виділяє динамічну пам'ять під час свого життєвого циклу, і в момент коли ми знищуємо цей об'єкт, цю пам'ять необхідно звільнити.

Перевантаження конструкторів[ред.]

Як і будь-яку звичайну функцію, конструктор також можна перевантажити багатьма функціями, які мають однакове ім'я але різний список формальних параметрів. Нагадаємо, що при виклику такої функції, компілятор буде викликати ту, параметри якої збігаються із списком аргументів, які передаються при її виклику за кількістю і типом. У випадку з конструкторами, які автоматично викликаються при створенні об'єкта, виконуватись буде той конструктор, аргументи якого збігаються з тими, що були передані при оголошенні об'єкта:

// перевантаження конструкторів класу 
#include <iostream> 
using namespace std; 
 
class CRectangle { 
 int width, height; 
 public: 
 CRectangle (); 
 CRectangle (int,int); 
 int area (void) {return (width*height);} 
}; 
 
CRectangle::CRectangle () {  // конструктор
 width = 5; 
 height = 5; 
} 
 
CRectangle::CRectangle (int a, int b) {  // конструктор з двома параметрами
 width = a; 
 height = b; 
} 
 
int main () { 
 CRectangle rect (3,4);  // буде викликаний другий конструктор, який має два параметри
 CRectangle rectb;       // буде викликаний конструктор за замовченням
 cout << "rect area: " << rect.area() << endl; 
 cout << "rectb area: " << rectb.area() << endl; 
 return 0; 
}

Порядок виклику конструкторів:

Якщо клас не має власних конструкторів, він ініціалізується так, ніби компілятор створив конструктор за замовченням за вас. Цей конструктор викликає конструктори без аргументів базових класів і полів класу. Чіткий порядок викликів конструкторів не залежить від того, використовуються конструктори по замовченню чи перевантажені конструктори, з аргументами чи без:

  1. Спочатку викликаються конструктори базових класів в порядку їх перерахування в списку наслідування (список, в якому після символу : перераховуються базові класи, розділені комами).
  2. Потім викликаються конструктори полів класу в порядку їх появи в об’явленні класу.
  3. Після того як будуть сконструйовані всі базові класи і поля, виконається конструктор нашого класу.

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

Конструктор за замовчанням[ред.]

Якщо програміст не оголошує жодного конструктора при визначені класу, компілятор вважає, що клас має конструктор за замовчуванням без аргументів. Таким чином оголошуючи клас як у нижченаведеному прикладі:

class CExample { 
 public: 
 int a,b,c; 
 void multiply (int n, int m) { a=n; b=m; c=a*b; }; 
 };

Компілятор вважає, що клас CExample має конструктор за замовчанням, тому якщо ви визначаєте об'єкти цього класу, вам необхідно просто оголосити їх без аргументів:

CExample ex;

Але як тільки ви оголошуєте власний конструктор для класу, компілятор більше не забезпечує існування автоматичного конструктора за замовчанням. Тому всі об'єкти цього класу слід створювати у відповідності із прототипом того конструктора, який існує в класі:

class CExample { 
 public: 
 int a,b,c; 
 CExample (int n, int m) { a=n; b=m; }; 
 void multiply () { c=a*b; }; 
};
// Лише таке визначення об'єкту буде правильним для цього прикладу
CExample ex (2,3); 
// А такий спосіб викличе помилку компіляції:
CExample ex;

Але компілятор не лише створює конструктор по замовчанню, якщо ви не визначили власний. Він забезпечує три спеціальні функції члени класу, які не явно створюються, якщо ви не створили власні. Цими функціями є:

  • конструктор копій;
  • оператор присвоювання, який копіює об'єкт;
  • і конструктор за замовчанням.

Конструктор копій і оператор присвоювання копіюють весь вміст об'єкту (всі дані полів), в поточний об'єкт. Конструктор копій, який неявно створить компілятор, якби нам довелося створювати його самим виглядав би так:

CExample::CExample (const CExample& rv) { 
   a=rv.a; b=rv.b; c=rv.c; 
}

Вказівники на класи[ред.]

В C++ можливо створювати вказівники на класи. Оскільки клас, є таким самим типом як і звичайні, ми можемо використовувати ім'я класу в якості імені типу, для оголошення вказівників. На приклад: CRectangle * prect;

є вказівником на об'єкт класу CRectangle.

Для того, щоб звернутися до члену класу, який заданий за допомогою вказівника, існує спеціальний оператор розіменування "->".

Приклади комбінацій з використанням операторів "*", "&", ".", "->", "[ ]":

Вираз інтерпретується як
*x Об'єкт на який вказує x
&x Адреса об'єкту x
x.y y - член об'єкту x
x->y y - член об'єкту на який вказує x
(*x).y y - член об'єкту на який вказує x (еквівалент попереднього випадку)
x[0] Перший елемент масиву, на який вказує x
x[1] Другий елемент масиву, на який вказує x
x[n] (n+1) елемент масиву, на який вказує x

Статичні члени класу[ред.]

Клас може мати статичні члени, як поля даних так і функції. Статичні поля даних також називають ще "змінними класу", оскільки таке поле буде зберігати лише одне унікальне значення для всіх об'єктів цього класу. Їхній вміст не залежить від об'єкту в якому це поле знаходиться.

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

// статичні члени класу 
#include <iostream> 
using namespace std; 
 
class CDummy { 
 public: 
 static int n; // статичне поле класу
 CDummy () { n++; }; // конструктор
 ~CDummy () { n--; }; // деструктор
}; 
 
int CDummy::n=0; // визначаємо статичне поле класу 
 
int main () { 
 CDummy a; 
 CDummy b[5]; 
 CDummy * c = new CDummy; 
 cout << a.n << endl; 
 delete c; 
 cout << CDummy::n << endl; 
 return 0; 
}

Статичні члени класу, мають ті самі властивості що і глобальні змінні, але вони відносяться до області видимості класу. Тому, щоб не допустити їх створення декілька раз, ми можемо помістити лише прототип в декларацію класу, але не можемо їх визначати там (ініціалізувати). Для того, щоб ініціалізувати статичне поле даних ми маємо помістити його формальне визначення за межі класу, в глобальній області видимості, як це було вказано у прикладі:

int CDummy::n=0; // визначаємо статичне поле класу

Оскільки значення цієї змінної є унікальним для всіх об'єктів одного класу, до нього можна звернутися як до числової змінної, як до члена будь якого об'єкту цього класу, або просто використовуючи ім'я класу (таке звернення можливе лише для статичних членів класу):

cout << a.n; 
cout << CDummy::n;

Ці дві строки коду, звертаються до однієї тієї самої змінної: до статичної змінної n, яка належить до класу CDummy, і є спільною для всіх об'єктів цього класу.