67180

Повернення об’єктів функціями. Потенційні проблеми

Лекция

Информатика, кибернетика и программирование

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

Украинкский

2014-09-04

74.5 KB

2 чел.

Лекція № 7

Тема: Повернення об'єктів функціями. Потенційні проблеми

План

  1.  Особливості механізму повернення об'єктів функціями.
  2.  Потенційна проблема при поверненні об'єктів функціями.

  1.  Особливості механізму повернення об'єктів функціями

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

Приклад 1. Демонстрація механізму повернення об'єкта функцією

 

class strClass

{            char str[80];

       public:

             void Set(char *s) { strcpy(str, s); }

             void Show()   { cout << "Рядок: " << str << endl; }

};

 

// Ця функція повертає об'єкт типу strClass.

strClass Init()

{       strClass obj; char str[80];

        cout << "Введіть рядок: "; cin >> str;

        obj.Set(str);

        return obj;

}

 

int main()

{     strClass Obj;              // Створення об'єкта класу

 

      Obj = Init();         // Присвоюємо об'єкт, повернутий функцією Init(), об'єкту Obj

      Obj.Show();

 

      getch(); return 0;

}

   У  наведеному  прикладі  функція  Init()  створює  локальний  об'єкт  obj  класу strClass, а потім зчитує рядок з клавіатури. Цей рядок копіюється в рядок obj.str, після чого об'єкт obj повертається функцією Init() і присвоюється об'єкту Obj у функції main().

  1.  Потенційна проблема при поверненні об'єктів функціями.

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

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

     Розглянемо таку некоректну версію попереднього коду програми.

Приклад 2.  Демонстрація механізму появи помилки, яка виникає при поверненні об'єкта

                   функцією

class strClass

{             char *s;

        public:

              strClass()   { s = 0; }

              ~strClass()   { if(s) delete[]s; cout << "Звільнення s-пам'яті" << endl; }

              void Set(char *str) { s = new char[strlen(str)+1]; strcpy(s, str); }       // Завантаження рядка.

              void Show()   { cout << "s= " << s << endl; }

};

 

// Ця функція повертає об'єкт типу strClass.

strClass Init()

{       strClass obj; char str[80];

        cout << "Введіть рядок: "; cin >> str;

        obj.Set(str);

        return obj;

}

 

int main()

{      strClass Obj;          // Створення об'єкта класу

 

       // Присвоюємо об'єкт, повернутий функцією Init(), об'єкту Obj.

       Obj = Init();   // Ця настанова генерує помилку!!!!

       Obj.Show();   // Відображення "сміття".

 

       getch(); return 0;

}

     Результати виконання цієї програми мають такий вигляд:

Введіть рядок: Привіт

Звільнення s-пам'яті.

Звільнення s-пам'яті.

s= тут появиться сміття

Звільнення s-пам'яті.

 

     Зверніть  увагу  на  те,  що  виклик  деструктора ~strClass()  відбувається  тричі!  Вперше він викликається при виході локального об'єкта obj з області видимості у момент його повернення з функції  Init(). Другий виклик деструкторa ~strClass() відбувається  тоді, коли руйнується  тимчасовий об'єкт,  який повертається функцією Init(). Коли функція повертає об'єкт,  то автоматично створюється невидимий (для Вас) тимчасовий об'єкт, який зберігає повернуте значення. У нашому випадку цей об'єкт просто є побітовою копією об'єкта obj. Отже, після повернення з функції використовується деструктор тимчасового об'єкта. Оскільки область пам'яті, що виділяється для зберігання рядка, який вводить користувач, вже була звільнена (причому двічі!), то під час виклику функції Show() на екран буде виведено "сміття". Нарешті, після завершення роботи коду програми викликається деструктор об'єкта Obj, який належить функції main(). Ситуація тут ускладнюється ще й тим, що під час першого виклику деструктора звільняється область пам'яті, виділена для зберігання рядка, отримуваного функцією Init(). Таким чином, у цій ситуації погано не тільки те, що решта два звернення до деструктора ~strClass() спробують звільнити вже  звільнену  динамічно  виділену  область  пам'яті,  але  вони можуть  зруйнувати систему динамічного розподілу пам'яті.

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

.

Тема: Механізми створення та використання конструктора копії

План

Використання конструктора копії для ініціалізації одного об'єкта іншим.

Механізм використання конструктора копії для передачі об'єкта функції.

Механізм використання конструктора копії при поверненні функцією об'єкта.

Конструктори копії та їх альтернативи

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

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

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

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

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

    Найпоширеніший формат конструктора копії об'єкта має такий вигляд:

               ім'я_класу (const ім'я_класу &obj)

               {

                       // Тіло конструктора копії

               }

    У  цьому  записі  елемент  &obj  означає  посилання  на  об'єкт,  який  використовується для ініціалізації іншого об'єкта.

    У мові  програмування C++  визначено  дві  ситуації,  у  яких  значення  одного об'єкта передається  іншому:  при присвоєнні  та  ініціалізації.  Ініціалізація  об'єкта може виконуватися трьома способами, тобто у випадках, коли:

●  один об'єкт безпосередньо ініціалізує інший об'єкт, як, наприклад, в оголошенні;

●  копія об'єкта передається як аргумент параметру функції;

●  генерується тимчасовий об'єкт (найчастіше як значення, що повертається функцією).

    Наприклад, нехай завчасно створено об'єкт ObjY типу myClass. Тоді у процесі виконання  таких  подальших  настанов  буде  викликано  конструктор  копії  класу myClass:

   myClass ObjX = ObjY;         // Об'єкт ObjY безпосередньо ініціалізує об'єкт ObjX. 

   Fun1(ObjY);                        // Об'єкт ObjY передається як аргумент 

   ObjY = Fun2();                    // Об'єкт ObjY приймає об'єкт, що повертається функцією. 

У перших двох випадках конструктору копії буде передано посилання на об'єкт ObjY, а в третьому – посилання на об'єкт, який повертається функцією Fun2().

Використання конструктора копії для ініціалізації одного об'єкта іншим  

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

Приклад. Демонстрація механізму використання конструктора копії для ініціалізації

                одного об'єкта іншим

class myClass

{           int *p;

     public:

            myClass(int c)          // Визначення звичайного конструктора

            {  p = new int; *p = c;

                cout << "Виділення p-пам'яті звичайним конструктором" << endl;

            }

            myClass(const myClass &obj)         // Визначення конструктора копії

            { p = new int; *p = *obj.p;

               cout << "Виділення p-пам'яті конструктором копії" << endl;

            }

            ~myClass()

            { delete p; cout << "Звільнення p-пам'яті" << endl; }

};

 

int main()

{      myClass ObjA(10);                 // Викликається звичайний конструктор.

      myClass ObjB = ObjA;           // Викликається конструктор копії.

      getch(); return 0;

}

    

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

Виділення p-пам'яті звичайним конструктором

Виділення p-пам'яті конструктором копії

Звільнення p-пам'яті

Звільнення p-пам'яті

Звільнення p-пам'яті

    Результати виконання цієї програми вказують на те, що при створенні об'єкта ObjA  викликається  звичайний  конструктор. Але  коли  об'єкт ObjA  використовують для ініціалізації об'єкта ObjB, то викликається конструктор копії. Його використання гарантує, що об'єкт ObjB виділить для своїх членів-даних власну область динамічної  пам'яті.  Без  конструктора  копії  об'єкт ObjB  просто  був  би  точною  копією об'єкта ObjA, а член ObjA.р вказував би на ту ж саму область динамічної пам'яті, що і член ObjB.р.

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

           myClass ObjA(2), ObjB(3);

           //...

           ObjB = ObjA;

У цьому записі настанова ObjB = ObjA здійснює операцію присвоєння, а не операцію копіювання.

Механізм використання конструктора копії для передачі об'єкта функції

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

Приклад. Демонстрація механізму використання конструктора копії для передачі об'єкта

                функції

class myClass

{           int *p;

     public:

            myClass(int c)           // Визначення звичайного конструктора

            {   p = new int; *p = c;

                 cout << "Виділення p-пам'яті звичайним конструктором" << endl;

            }

            myClass(const myClass &obj)      // Визначення конструктора копії

           {   p = new int; *p = *obj.p;

                cout << "Виділення p-пам'яті конструктором копії" << endl;

           }

           ~myClass()

          {   delete p; cout << "Звільнення p-пам'яті" << endl; }

          int Put() { return *p; }

};

 

// Ця функція приймає один об'єкт-параметр.

void Get(myClass obj)

{     cout << "*p= " << obj.Put() << endl;

}

 

int main()

{     myClass ObjA(10);         // Створення об'єкта класу

     Get(ObjA);

     getch(); return 0;

}

    

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

Виділення p-пам'яті звичайним конструктором

Виділення p-пам'яті конструктором копії

*p= 10

Звільнення p-пам'яті

Звільнення p-пам'яті

    У процесі виконання цієї програми тут відбувається таке: коли у функції main()  створюється  об'єкт  ObjA, "стараннями"  звичайного  конструктора  виділяється область пам'яті, адреса якої присвоюється покажчику ObjA.р. Потім об'єкт ObjA передається функції Get(),  а  саме –  її параметру  obj. У цьому  випадку  викликається конструктор копії, який для об'єкта ObjA створює побітову його копію з іменем obj. Конструктор копії виділяє пам'ять для цієї копії, а значення покажчика на виділену  область  пам'яті  присвоює  члену  р  об'єкта-копії.  Потім  значення,  яке  адресується покажчиком р початкового об'єкта,  записується в область пам'яті, адреса якої  зберігається  в  покажчику  р  об'єкта-копії.  Таким  чином,  області  пам'яті, що адресуються покажчиками ObjA.р  і obj.р, є різними та незалежними одна від одної, але значення (на які вказують ObjA.р і obj.р), що зберігаються в них, однакові. Якби конструктор копії у класі myClass не був завчасно визначений, то внаслідок створення за замовчуванням побітової копії члени ObjA.р і obj.р указували б на одну і ту саму область пам'яті. Після завершення роботи функції Get() об'єкт obj виходить з області видимості. Цей вихід супроводжується викликом його деструктора, який звільняє область пам'яті, яка адресується покажчиком obj.p. Нарешті, після завершення роботи фун-кція main()  виходить  з  області  видимості  об'єкт ObjA, що  також  супроводжується викликом його  деструктора  і  відповідним  звільненням  області пам'яті,  яка  адресується  покажчиком  ObjA.р. Як  бачимо,  використання  конструктора  копії  усуває негативні побічні ефекти, пов'язані з передачею об'єкта функції.

Механізм використання конструктора копії при поверненні функцією об'єкта

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

Приклад. Демонстрація механізму використання конструктора копії для створення

                тимчасового об'єкта, що повертається функцією

class myClass

{      public:

             myClass()   { cout << "Звичайний конструктор" << endl; }

             myClass(const myClass &obj) { cout << "Конструктор копії" << endl; }

};

 

myClass Fun()

{    myClass obj;         // Викликається звичайний конструктор.

     return obj;             // Опосередковано викликається конструктор копії.

}

 

int main()

{     myClass ObjA;        // Викликається звичайний конструктор.

      ObjA = Fun();         // Викликається конструктор копії.

       getch(); return 0;

}

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

Звичайний конструктор

Звичайний конструктор

Конструктор копії

   Тут  звичайний  конструктор  викликається  двічі:  перший  раз  при  створенні об'єкта  ObjA  у  функції main(),  другий –  при  створенні  об'єкта  obj  у  функції  Fun(). Конструктор копії викликається в ту мить, коли генерується тимчасовий об'єкт як значення, яке повертається з функції Fun().

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

Конструктори копії та їх альтернативи

   Як  неодноразово  згадувалося  раніше, C++ –  потужна  мова  програмування. Вона має багато засобів, які надають їй надзвичайно широкі можливості, але при цьому  її можна назвати складною мовою. Конструктори копії – один з важливих засобів, який багато програмістів-початківців вважають основою складності мови, оскільки механізм  їх роботи не сприймається на  інтуїтивному рівні. Такі програмісти часто не розуміють, чому конструктори копії мають таке важливе значення для  ефективної  роботи  коду  програми.  Багато  з  них  не  відразу  знаходять  точну відповідь на запитання: коли потрібен конструктор копії, а коли – ні? Здебільшого у них виникає наступне запитання: чи не існує йому простішої альтернативи? Від-

повідь також непроста: і так, і ні! Такі мови програмування, як Java і С#, не мають конструкторів копії, оскільки в жодній з них не створюються побітові копії об'єктів. Йдеться про те, що як Java, так  і С# динамічно виділяють пам'ять для всіх об'єктів, а програміст оперує цими об'єктами виключно через посилання. Тому при передачі об'єктів функції як параметрів або при поверненні їх з функцій в копіях об'єктів немає ніякої потреби. Той факт, що ні мова Java, ні мова С# не потребують конструкторів копії, робить ці мови дещо простішими,  але  за простоту  теж потрібно платити. Робота  з об'єктами виключно за допомогою посилань (а не безпосередньо, як у мові програмування C++) накладає обмеження на типи операцій, які може виконувати програміст. Понад це, таке використання об'єктних посилань у мовах Java і С# не дає змоги точно визначити, коли об'єкт буде зруйновано. У мові програмування C++ об'єкт завжди руйнується при виході з області видимості.

    Мова C++ надає програмісту повний контроль над ситуаціями, які виникають у процесі роботи коду програми, тому вона є дещо складнішою, ніж мови Java  і С#. Це – ціна, яку ми платимо за потужні засоби програмування.


 

А также другие работы, которые могут Вас заинтересовать

67209. Проектування, компонування та подання форм за допомогою CSS 863 KB
  Можна сказати, що використання великої кількості маркерів class та id порушує принцип KISS (принцип збереження максимальної простоти). Проте складні компонування часто створюють конфлікти в каскадуванні – конфлікти, які найпростіше вирішуються додаванням до елементів маркерів...
67210. Синтаксис. Предмет синтаксиса 175.5 KB
  Синтаксический ярус – самый высокий в системе языка. С фонетикой синтаксис связан посредством интонации: основная синтаксическая единица языка – предложение – всегда интонационно оформлено. Интонация выражает утверждение, вопрос, побуждение, восклицание; интонационно выделяются вводные слова, конструкции и т.д.
67211. ЭФФЕКТОРЫ 90 KB
  К эффекторам относятся мышцы скелетные гладкие и сердечная и железы внешней секреции. Скелетные мышцы называются также произвольными поскольку их сокращением и расслаблением можно сознательно управлять хотя частичное сокращение обеспечивающее мышечный тонус регулируется без участия сознания.
67212. Предмет психологии, ее задачи 99.75 KB
  Предмет психологии ее задачи. Общее представление о психологии как науке. Соотношение научной и житейской психологии. Система феноменов которые изучаются в современной психологии жизненная роль соответствующих явлений.
67213. Основные направления современной психологии 98.51 KB
  Все что происходит внутри человека изучить невозможно то есть человек выступает как черный ящик. Объективно изучать регистрировать можно только реакции внешние действия человека и стимулы ситуации которые эти реакции обусловливают. Основная задача бихевиоризма –подчеркивает Уотсон –заключается в накоплении наблюдений...
67215. Сознание и самосознание. Свойства сознания 102.02 KB
  Определение сознания. Основные признаки сознания. Психологические характеристики сознания человека. Соотношение сознания и бессознательного впервые было изучено в рамках теории и практики психоанализа.
67216. Ощущения и восприятие 79.04 KB
  Ощущения и восприятие. Ощущения считаются самыми простыми из всех психических явлений. Способность к ощущениям имеется у всех живых существ обладающих нервной системой. Качество это основная особенность данного ощущения отличающая его от других видов ощущений и варьирующая в пределах данного вида ощущения.
67217. Внимание и память 48.64 KB
  Особенности внимания как психического процесса и состояния человека. Определение внимания. Факторы определяющие избирательность и направленность внимания. Функции внимания.