| 1 2 3 4
Ім'я файлу: prog_lecture_07.pdf Розширення: pdfРозмір: 205кб.Дата: 29.09.2021скачати7.2.3. Конструктор за замовчуванням У розглянутих вище прикладах об'єкти класу TComplex створювалися за допомогою конструктора, що має один чи два параметри. Задамося питанням: «Що відбудеться, якщо в класі не визначений жодний конструктор?» У цьому випадку об'єкт класу створюється за допомогою конструкторазазамовчуванням, що, природно, не має параметрів. Він заповнює всі поля глобального об'єкта нулями. Якщо об'єкт є локальним, його поля будуть містити випадкові значення. Усі функції-члени класу, створеного за допомогою конструктора за замовчуванням, працюють у відповідності зі своїм описом. Конструктор за замовчуванням #include class TComplex Лекція 7Класи 15 { int Re; int Im; public: void print(){printf("Z = %d + i*%d \n",Re,Im);}; }Z; int main() { Z.print(); return 0; } У підсумку одержуємо наступний результат. Z = 0.0 + i*0.0 Якщо програміст визначить у класі хоча б один конструктор, навіть без параметрів, компілятор не буде генерувати автоматичний конструктор. 7.2.4. Конструктор копіювання Знову створений об'єкт можна ініціалізувати умістом раніше існував об'єкта. Для цього призначений конструкторкопіювання, що має наступний вид. ім 'я _класу ( ім 'я _класу &) Пояснимо необхідність цього конструктора на прикладі. Уявимо собі об'єкт класу TArray , що є масивом цілих чисел. Він задається двома параметрами — початковою адресою (вказівником) і розміром. У наступній програмі ми створюємо масив x , а потім — масив y , причому новий масив повинний бути копією старого. Використання конструктора копіювання #include class TArray { int *p; int size; public: TArray(long n, int x); TArray(TArray&); void view(); }; int main() { TArray x(10,1); x.view(); TArray y = x; y.view(); return 0; } TArray::TArray(long n, int x) { size = n; p = new int[size]; for (long i=0; i printf("\nAddress x = %p \n",p); } TArray::TArray(TArray& X) { size=X.size; p = new int[size]; // Глибоке копіювання for (long i=0; i printf("\nAddress y = %p \n",p); } void TArray::view() {
Лекція 7Класи 16 for(long i=0; i } У цій програмі реалізоване глибокекопіювання, що має на увазі створення абсолютно самостійної копії раніше існував об'єкта. Зверніть увагу на те, що в конструкторі копіювання поле p не дублюється. Замість цього йому привласнюється адреса знову виділеної пам'яті, у яку копіюються інші елементи масиву. У результаті ми побачимо на екрані наступні рядки. Адреса x = 007803A0 1 1 1 1 1 1 1 1 1 1 Адреса x = 00780340 1 1 1 1 1 1 1 1 1 1 А що відбудеться, якщо програміст не визначив конструктор копіювання? У цьому випадку буде викликаний конструкторкопіюваннязазамовчуванням. Він виконує побітове, тобто поверхневекопіювання, і замість нової копії ми одержимо два вказівники, що посилаються на той самий адресу. Адреса x = 007803A0 1 1 1 1 1 1 1 1 1 1 Адреса x = 007803A0 1 1 1 1 1 1 1 1 1 1 7.2.5. « Іменовані конструктори » Конструктори, як і звичайні функції, можна перевантажувати, варіюючи типи і кількість параметрів. Однак бувають ситуації, коли об'єкт класу повинний створюватися двома різними способами, що реалізуються конструкторами, що мають однакову кількість і тип параметрів. Яскравий приклад такої ситуації — ініціалізация комплексного числа. Як відомо, комплексні числа мають дві форми запису: у декартових і полярних координатах. У першому випадку дійсна і мнима частини числа задаються абсцисою й ординатою (z = x + iy), а в другому — радіусом і кутом, між радіусом-вектором і віссю абсцис (z = rcos( α )+ircos( α )). Обидві форми запису задаються парою дійсних чисел, тому прототипи конструкторів цілком збігаються, і розрізнити їхній виклик неможливо. Для того щоб розв’язати цю дилему, слід скористатися статичними функціями-членами, що викликають закритий конструктор і повертають створений об'єкт класу. Лапки означають, що ці функції насправді конструкторами не є, хоча виконують зовсім аналогічну задачу. Ідея цієї ідіоми полягає в тім, що статичні функції-члени викликаються ще до створення об'єкта зазначеного класу. Отже, з їхньою допомогою можна викликати закритий конструктор, передавши йому відповідний набір аргументів — декартові координати або радіус і кут. Використання « іменованих конструкторів » #include #include #define PI 3.141592 class TComplex { public: static TComplex Dekart(double, double); static TComplex Euler(double, double); void view(){printf("Z = %lf +i*%lf \n",Re,Im); } private: double Re; double Im; TComplex(double x, double y):Re(x),Im(y){} }; int main() { TComplex objDekart = TComplex::Dekart(1.0,0.0); TComplex objEuler = TComplex::Euler(1.0,PI/2); objDekart.view(); objEuler.view(); return 0; } TComplex TComplex::Dekart(double x, double y) { return TComplex(x,y); } TComplex TComplex::Euler(double r, double alpha) {
Лекція 7Класи 17 return TComplex(r*cos(alpha), r*sin(alpha)); } Результат роботи цієї програми приведений нижче. Z = 1.000000 +i*0.000000 Z = 0.000000 +i*1.000000 Відзначимо два моменти: 1) конструктор оголошений закритим, тому доступ до нього можливий лише через функції інтерфейсу; 2) інтерфейсні функції є статичними, тому їх можна викликати до створення нового об'єкта. Якби функції Dekart() і Euler() не були статичними, їх виклик став би можливий тільки після створення об'єкта. Однак оскільки конструктор оголошений закритим, створити об'єкт було б неможливо. Ключове слово static розриває це замкнуте коло. 7.2.7. Деструктор Деструктор знищує об'єкти класу, звільняючи зайняті ресурси. Ця функція, як і конструктор, не має значення, що повертається, однак, у відмінність він конструктора, вона не має одного параметра. Ім'я деструктора повинне збігатися з ім'ям класу, перед яким поставлена тильда. ім ’я _класу () { тіло деструктора } Чудовою особливістю деструктора є той факт, що він ніколи (за одним-єдиним виключенням) не викликається явно. Просто, коли об'єкт виходить з області видимості, програма сама викликає деструктор, що виконує запропоновані дії. Якщо програміст не визначив деструктор класу, компілятор згенерує деструкторза замовчуванням. Використання деструктора #include class TClass2 { public: int a; TClass2():a(1){cout << "Ctor TClass2" << endl;} TClass2() {cout << "Dtor TClass2" << endl;} }; class TClass1 { public: TClass2 *p; TClass1() { p = new TClass2; cout << "Ctor TClass1" << endl; } TClass1() { delete p; cout << "Dtor TClass1" << endl; } }; int main() { TClass1 a; return 0; } Наприклад, у даній програмі в модулі main() визначений об'єкт a класу TClass1 . Цей об'єкт містить усередині себе вказівник на об'єкт класу TClass2 . Отже, першим повинний викликатися конструктор класу TClass2 , а потім — конструктор класу TClass1 . Знищення об'єкта a відбувається «зсередини»: спочатку знищується об'єкт класу TClass2 , а потім — об'єкт класу TClass1 Ctor TClass2 Ctor TClass1 Ctor TClass2 Ctor TClass1 Якби в об'єкті класу TClass1 об'єкт класу TClass2 не створювався, а просто був його членом, деструктори викликалися б у зворотному порядку. Зворотний порядок викликів деструкторів
Лекція 7 Класи 18 #include class TClass2 { public: int a; TClass2():a(1){cout << "Ctor TClass2" << endl;} TClass2() {cout << "Dtor TClass2" << endl;} }; class TClass1 { public: TClass2 p; TClass1() { cout << "Ctor TClass1" << endl; } TClass1() { cout << "Dtor TClass1" << endl; } }; int main() { TClass1 a; return 0; } Тепер повідомлення мають наступний вид. Ctor TClass2 Ctor TClass1 Ctor TClass1 Ctor TClass2 Розглянемо ситуацію, у якій необхідно явно викликати деструктор. Явний виклик деструктора #include #include class TClass { public: int a; TClass() {cout << "Ctor TClass " << endl;} TClass(){cout << "Dtor TClass " << endl;} }; int main() { char memory[sizeof(TClass)]; void *p=memory; TClass* q = new(p) TClass; cout << q << endl; q->TClass(); return 0; } Результат роботи цієї програми виглядає так. Ctor TClass 0x0075fdc4 Dtor TClass Тут використаний так називаний синтаксисрозміщення. Це — різновид оператора new , що дозволяє створювати і розміщати об'єкт по заздалегідь заданій адресі. Спочатку ми резервуємо пам'ять для об'єкта — масив memory , а потім створюємо об'єкт за допомогою синтаксису розміщення. Саме тому, що ми занадто глибоко втрутилися в механізм розподілу пам'яті для нових об'єктів, нам доведеться самим подбати про її звільнення.
Лекція 7Класи 197.2.7. Передача аргументів і повернення значень Конструктори, конструктори копіювання і деструктор дозволяють продемонструвати механізм передачі аргументів і значень функцій, що повертаються. Об'єкти, як і звичайні змінні, можуть передаватися за значенням і за посиланням. Як це відбувається, ілюструється наступною програмою. Передача об 'єктів за значенням #include #include class TComplex { double Re; double Im; public: TComplex(double x, double y):Re(x),Im(y){ printf("Ctor\n");} TComplex(TComplex& z){ Re = z.Re; Im = z.Im; printf("Copy ctor\n");} TComplex(){printf("Dtor\n");} double getRe() { return Re;} double getIm() { return Im;} }; double modul(TComplex); int main() { printf("Start main\n"); TComplex z(1,1); printf(" Модуль z = %lf\n",modul(z)); printf("End modul\n"); printf("End main\n"); return 0; } double modul(TComplex z) { printf("Start modul\n"); return sqrt(z.getRe()*z.getRe() + z.getIm()*z.getIm()); } На екрані з'являться наступні рядки. Start main Ctor Copy ctor Start modul Dtor End modul Модуль z = 1.414214 End main Dtor Отже, після запуску головного модуля створюється об'єкт z класу TComplex . Про це свідчить повідомлення від конструктора — Ctor . Потім за допомогою конструктора копіювання створюється дублікат об'єкта z Конструктор копіювання сповіщає про це, виводячи рядок Copy ctor . Потім починається виконання функції modul() , виводиться результат і знищується тимчасова копія параметра. Про це говорить рядок Dtor , що виводиться на екран після виконання оператора return . Отже, на екрані з'являється результат обчислень. Виконання функції main() кінцю добігає кінця, і після виконання оператора return 0 викликається деструктор класу TComplex , що знищує об'єкт z Основний висновок, якому необхідно зробити, — при передачі об'єкта за значенням його копія створюється конструкторомкопіювання. Досить поставити після імені класу TComplex у прототипі і заголовку функції modul() символ амперсанду, що означає передачу параметра за посиланням, — і конструктор копіювання викликатися не буде. На екрані з'явиться наступне повідомлення. Start main Ctor Start modul End modul
Лекція 7 Класи 20 Модуль z = 1.414214 End main Розглянемо програму, що підсумовує два комплексних числа. У цьому випадку функція sum() повертає копію локального об'єкта, що зберігає результат. Після передачі ця копія й об'єкт знищуються. Повернення об ' єкта #include class TComplex { double Re; double Im; public: static int counter; TComplex(double x, double y):Re(x),Im(y) { counter++;printf("Ctor %d\n",counter); } TComplex(TComplex& z) { Re = z.Re; Im = z.Im; counter++; printf("Copy %d\n",counter);} TComplex(){printf("Dtor %d\n",counter);counter--;} double getRe() { return Re;} double getIm() { return Im;} void putRe(double x) { Re =x;} void putIm(double y) { Im = y;} }; TComplex sum(TComplex,TComplex); int TComplex::counter = 0; int main() { printf("Start main\n"); TComplex z(1,1), w(2,2), u(0,0); u = sum(z,w); printf("End sum\n"); printf("w + u = %lf + i*%lf\n",u.getRe(),u.getIm()); printf("End main\n"); return 0; } TComplex sum(TComplex u, TComplex v) { printf("Start \n"); TComplex w(0,0); w.putRe(u.getRe() + v.getRe()); w.putIm(u.getIm() + v.getIm()); return w; } От як виглядають повідомлення від конструкторів, конструкторів копіювання і деструкторів (коментарі додані для більшої ясності). Start main // Начало функції main() Ctor 1 // Створюється об ' єкт z Ctor 2 // Створюється об ' єкт w Ctor 3 // Створюється об ' єкт u Copy 4 // Створюється копія об ' єкта z Copy 5 // Створюється копія об ' єкта w Start sum // Починається виконання функції sum() Ctor 7 // Створюється локальний об ' єкт s Copy 7 // Створюється копія локального об ' єкта Dtor 7 // Знищується копія локального об ' єкта Dtor 7 // Знищується копія локального об ' єкта Dtor 5 // Знищується копія об ' єкта w Dtor 4 // Знищується копія об ' єкта z End sum // Виконання функції sum() довершене w + u = 3.000000 + i*3.000000 // Результат Dtor 3 // Знищення об ' єкта u Dtor 2 // Знищення об ' єкта w Dtor 1 // Знищення об ' єкта z
Лекція 7 Класи 21 Отже, відзначимо, що конструктор копіювання викликається в наступних ситуаціях. 1. При ініціалізації об'єкта під час оголошення. 2. При передачі об'єкта як параметр функції. 3. При поверненні об'єкта як значення, що повертається 7.3. ДРУЖНІ ФУНКЦІЇ І КЛАСИ У попередній програмі, коли нам знадобилося скласти два комплексних числа, довелося застосувати чотири інтерфейсні функції: getRe() , getIm() , putRe() , putIm() . Програма стала б простіше, якби функція sum() мала прямий доступ до полів класу TComplex . Для цього можна оголосити цю функцію дружньою, використовуючи ключове слово friend Використання дружніх функцій #include class TComplex { friend TComplex sum(TComplex, TComplex); double Re; double Im; public: static int counter; TComplex(double x, double y):Re(x),Im(y) { counter++;printf("Ctor %d\n",counter);} TComplex(TComplex& z) { Re = z.Re; Im = z.Im; counter++; printf("Copy %d\n",counter);} TComplex(){printf("Dtor %d\n",counter);counter--;} void print() {printf("Z = %lf + i*%lf\n",Re,Im);} }; TComplex sum(TComplex,TComplex); int TComplex::counter = 0; int main() { printf("Start main\n"); TComplex z(1,1), w(2,2), u(0,0); u = sum(z,w); printf("End sum\n"); u.print(); printf("End main\n"); return 0; } TComplex sum(TComplex u, TComplex v) { printf("Start \n"); TComplex w(0,0); w.Re= u.Re + v.Re; w.Im=u.Im + v.Im; return w; } Дружня функція має всі права члена класу. Отже, їй доступні всі поля — як відкриті, так і закриті. Для того щоб оголосити функцію дружньої, ключове слово friend слід помістити в оголошення класу. (Клас сам оголошує своїх друзів!) Розділимо клас TComplex на два: TNumber і TFunction . У першому класі залишимо змінні-члени, а до другого віднесемо усі функції. Використання дружніх функцій - членів #include class TNumber; class TFunction { public: void print(TNumber z);
Лекція 7 Класи 22 }; class TNumber { double Re; double Im; public: static int counter; TNumber(double x, double y):Re(x),Im(y) { counter++;printf("Ctor %d\n",counter);} TNumber(TNumber& z) { Re = z.Re; Im = z.Im; counter++; printf("Copy %d\n",counter);} TNumber(){printf("Dtor %d\n",counter);counter--;} friend void TFunction::print(TNumber); }; int TNumber::counter = 0; int main() { printf("Start main\n"); TNumber z(1,1); TFunction a; a.print(z); printf("End main\n"); return 0; } void TFunction::print(TNumber z) { printf("z = %lf + i*%lf\n", z.Re, z.Im); } У результаті одержуємо наступні повідомлення. Start main Ctor 1 Copy 2 z = 1.000000 + i*1.000000 Dtor 2 End main Dtor 1 Зверніть увагу на те, що функція print() , що належить класу TFunction , одержує параметр типу TNumber . Отже, цей тип повинний бути визначений раніше. Однак клас TNumber оголошує дружню функцію print() , що належить класу TFunction . Для розв’язання цієї проблеми в мові С++ передбачений механізм неповногооголошеннякласу. Працюючи з дружніми функціями, потрібно враховувати наступні обмеження. 1. Вони не успадковуються. 2. Вони не можуть мати специфікатори static і extern 3. У класі, функція-член якого є дружньою до не цілком оголошеного класу, слід розміщати тільки прототип. Реалізація функції повинна знаходитися після повного оголошення класу. Коли усі функції деякого класу є дружніми стосовно іншого класу, можна оголосити весь клас дружнім. Наприклад, попередню програму можна переробити в такий спосіб. Використання дружніх класів #include class TNumber; // Неповне оголошення TFunction { public: void print(TNumber z); // Апеляція до не цілком оголошеного класу }; class TNumber {
|