67180

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

Лекция

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

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

Украинкский

2014-09-04

74.5 KB

3 чел.

Лекція № 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  і С#. Це – ціна, яку ми платимо за потужні засоби програмування.


 

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

84164. Философия французского просвещения XVIII века и ее представители 41.15 KB
  Таким образом жизнь человека определяется не Богом а его собственными делами и поэтому у церкви нет никаких оснований и никакого права насаждать предрассудки суеверия и страхи перед Богом. Равенство между людьми возможно поразному например с точки зрения естественного права равенство людей несомненно и обязательно поскольку естественные права человека проистекают из природы человека а она у всех людей одна. Но с появлением частной собственности возникла цивилизация основанная на неравенстве жестокости и эксплуатации народа и она же...
84165. Немецкая классическая философия и ее главные проблемы. Философия Канта: понятие «вещи в себе» и трансцендентального знания. Антиномии чистого разума 40.59 KB
  Поскольку познание вещи есть результат рассудочной деятельности а не просто суммой ощущений органов чувств то на первый взгляд проблема познания состоит в том насколько корректно взаимодействуют категории рассудка с чувственным сознанием снимающим информацию с вещи. В этом случае вопрос познавательной способности человека сводился бы только к тому насколько категории рассудка адекватно схватывают суть результатов чувственного познания. Вещь лишь провоцирует всплеск активности разума ощущениями которые мгновенно преобразуются в...
84166. Философия Фихте и Шеллинга. Основоположения «наукоучения» в философии Фихте. Понятие «абсолютного тождества» в философии Шеллинга 42.24 KB
  Кроме того следует признать недостаточным у Канта и то что он всего лишь только описал формы мышления умственные категории и законы мысли но не выявил основного единого общего принципа познания. Ведь только наличием подобного единого общего принципа познания можно объяснить не только слаженность форм мышления умственных категорий и законов мысли но и само их внутреннее единство между собой. И это есть вопрос не только выявления недостаточности кантовской философии это вопрос концептуальный потому что задача раскрытия данного единого...
84167. Абсолютный идеализм Гегеля. Система и метод философии Гегеля. История как процесс саморазвития «абсолютного духа» 35.72 KB
  А что же такое Абсолютный Разум сам по себе Абсолютный Разум сам по себе это есть некая идея его самоочевидного существования идея о том что Абсолютный Разум вообще существует безотносительно того что такое есть при этом Абсолютный Разум. Но если эта идея содержит в себе самоочевидное существование Абсолютного Разума как такового то она же содержит в себе и то каков есть этот Абсолютный Разум по своей природе. Потому что если идея определяет что Абсолютный Разум есть то она же эта самая же идея определяет и то что такое есть...
84168. Антропологический принцип философии Фейербаха. Фейербах о религии как отчуждении родовой сущности человека 40.99 KB
  Фейербах о религии как отчуждении родовой сущности человека. Прежде всего духовное начало не может быть истинным бытием поскольку единственно истинным бесспорным и самоочевидным бытием является не я человека а то что ему дано в ощущениях. Кроме того органом познания является не логическое мышление а непосредственно чувства человека. Потому что не логическое мышление а именно физические чувства человека являются поставщиками бесспорных ощущений.
84169. Характеристика философии позитивизма. Основные этапы ее развития, виднейшие представители 48.8 KB
  Суть этого метода состояла в том чтобы отказаться от традиционной философии как средства познания и создать новую философию которую они называли позитивная синтетическая философия. Таким образом поскольку богословие и традиционная философия оказались практически бесполезными то от них следует отказаться а вместо них создать новую систему объединения научных знаний которая с одной стороны сохранила бы в себе всеобъемлющий характер теологии и философии но с другой стороны приобрела бы характер точной науки. Это есть её главное...
84170. Философия экзистенциализма. Понятие экзистенции. Светский (Хайдеггер, Сартр, Камю) и религиозный (Ясперс) экзистенциализм 37.5 KB
  Поскольку экзистенция есть осознание человеком своей конечности временности то основной характеристикой бытия является время. Но внутреннее переживание человеком будущего есть не что иное как страх смерти осознание человеком своей конечности. Следовательно в дорефлексионном состоянии человеческое сознание было неспособно осознавать себя и воспринимать своё индивидуальное существование и таким образом не способно было осознавать мир ибо не было индивидуальным сознанием то есть сознанием способным вмещать в себя нечто что не есть оно...
84171. Аналитическая философия ХХ столетия. Философская программа неопозитивизма и ее кризис. «Постпозитивизм» и философия науки 46.26 KB
  Постпозитивизм и философия науки. Аналитическая философия пыталась выяснить законы возникновения научного знания корни его противоречий а также причины постоянного опровержения достижений науки развитием самой же науки. Таким образом предмет философии неопозитивизма это язык который и только который является по мнению неопозитивистов виновником несовершенства и постоянной изменчивости основ науки Мур. Это позволит формировать весь язык науки только из логически обоснованных единиц так называемых атомов научных понятий.
84172. Русская философия XIX века: западничество и славянофильство 30-50 гг. Оценка исторического прошлого России и разработка славянофильской идеологии 39.27 KB
  Оценка исторического прошлого России и разработка славянофильской идеологии. Западники стремились к европеизации России к переходу русской жизни на западноевропейские образцы а славянофилы отстаивали традиционные формы национальной жизни России и боролись за ограждение их от иностранного влияния. Западники считали что Россия должна повторить исторический путь стран Западной Европы а славянофилы наоборот полагали что исторический путь России самобытен и неповторим. Проблему национальной самобытности России поставил в 2030х годах XIX...