67157

Присвоєння об’єктів. Передача об’єктів функціям

Лекция

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

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

Украинкский

2014-09-04

62.5 KB

5 чел.

Лекція № 6

Тема: Присвоєння об'єктів. Передача об'єктів функціям

План

  1.  Присвоєння об'єктів.
  2.  Передача об'єктів функціям.
  3.  Конструктори, деструктори при передачі об'єктів

  1.  Присвоєння об'єктів

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

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

class myClass

{            int a, b;

       public:

             myClass()   { a = b = 0; }

             void Set(int c, int d) { a = c; b = d; }

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

};

 

int main()

{     myClass ObjA, ObjB;           // Створення об'єктів класу

 

      ObjA.Set(10, 20);

      ObjB.Set(0, 0);

      cout << "Об'єкт ObjA до присвоєння:" << endl;

      ObjA.Show();

      cout << "Об'єкт ObjB до присвоєння:" << endl;

      ObjB.Show();

      cout << endl;

 

      ObjB = ObjA;          // Присвоюємо об'єкт ObjA об'єкту ObjB.

 

      cout << "Об'єкт ObjA після виконання операції присвоєння:" << endl;

      ObjA.Show();

      cout << "Об'єкт ObjB після виконання операції присвоєння:" << endl;

      ObjB.Show();

 

      getch(); return 0;

}

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

Об'єкт ObjA до присвоєння:

a = 10; b = 20

Об'єкт ObjB до присвоєння:

a = 0; b = 0

Об'єкт ObjA після виконання операції присвоєння:

a = 10; b = 20

Об'єкт ObjB після виконання операції присвоєння:

a = 10; b = 20

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

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

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

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

class myClass

{            int c;

       public:

             myClass()     { c = 0; }

             void Set(int _c)    { c = _c; }

             void Show(char *s)  { cout << s << c << endl; }

};

 

void Fun(myClass obj)         // Визначення функції не члена класу

{     obj.Show("t2= ");           // Виведення числа 10.

      obj.Set(100);                  // Встановлює тільки локальну копію.

      obj.Show("t3= ");          // Виведення числа 100.

}

int main()

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

 

      Obj.Set(10);

      Obj.Show("t1= ");          // Виведення числа 10.

 

       Fun(Obj);            // Передача об'єкта функції не члена класу

 

       Obj.Show("t4= ");        // Як і раніше, виводиться 10, проте значення змінної "i" не змінилося

 

       getch(); return 0;

}

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

t1= 10

t2= 10

t3= 100

t4= 10

    Як підтверджують ці результати, модифікування об'єкта obj у функції Fun() не впливає на об'єкт Obj у функції main().

  1.  Конструктори, деструктори при передачі об'єктів

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

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

                     передачі об'єктів

class myClass

{            int n;

        public:

              myClass(int _n)  { n = _n; cout << "Створення об'єкта" << endl; }

              ~myClass()   { cout << "Руйнування об'єкта" << endl; }

              int Put()   { return n; }

};

 

void Get(myClass obj)

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

}

int main()

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

 

       Get(ObjA);

 

       getch(); return 0;

}

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

Створення об'єкта

n= 10

Руйнування об'єкта

Руйнування об'єкта

    Як  бачимо,  тут  здійснюється  одне  звернення  до функції  конструктора (при створенні об'єкта ObjA), але чомусь відбувається два звернення до функції деструктора. Давайте з'ясуємо, у чому тут проблема.

    При передачі об'єкта функції створюється його копія (і ця копія стає параметром  у  функції).  Створення  копії  означає  появу  нового  об'єкта.  Коли  виконання функції завершується, копія аргумента (тобто параметр) руйнується. Тут виникає відразу два запитання. По-перше, чи викликається конструктор об'єкта при створенні копії? По-друге, чи викликається деструктор об'єкта під час руйнування копії? Відповіді на ці запитання можуть здивувати Вас. Оскільки під час виклику функції створюється копія аргумента, то звичайний конструктор об'єкта не викликається. Натомість викликається конструктор копії об'єкта, який визначає, як має створюватися копія об'єкта. Але, якщо в класі безпосередньо не визначено конструктор копії, то мова програмування C++ надає його за замовчуванням. Конструктор копії за замовчуванням створює побітову (тобто однакову) копію об'єкта. Оскільки звичайний конструктор використовують для ініціалізації тільки деяких даних об'єкта, то він не повинен викликатися для створення копії вже наявного об'єкта. Такий виклик змінив би його вміст. При передачі об'єкта функції потрібно використовувати поточний стан об'єкта, а не його початковий стан. Однак, коли функція завершує свою роботу, то руйнується копія об'єкта, яка використовується  як  аргумент,  для  чого  викликається  деструктор  цього  об'єкта. Потреба виклику деструктора пов'язана з виходом об'єкта з області видимості його функцією, у якій він використовується. Саме тому попередня програма виводила два звернення перед зверненням до деструктора. Перше відбулося при виході з області видимості параметра функції Put(), а друге – під час руйнування об'єкта ObjA у функції main() після завершення роботи коду програми.

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

Тема: Потенційні проблеми, які виникають при передачі об'єктів

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

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

                 функціям, у яких динамічно виділяється та звільняється область пам'яті

class myClass

{            int *p;

       public:

             myClass(int c)

                {      p = new int; *p = c;

                        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= 10

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

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

    Ця програма містить принципову помилку,  а  саме: при  створенні  у функції main() об'єкта ObjA виділяється область пам'яті, адреса якої присвоюється показнику ObjA.р. При передачі функції Get() об'єкт ObjA побітово копіюється в параметр obj. Це означає, що обидва об'єкти (ObjA і obj) матимуть однакове значення для показника р.  Іншими словами, в обох об'єктах (в оригіналі та його копії) член даних р вказуватиме на  одну  і  ту  саму  динамічно  виділену  область пам'яті. Після  завершення роботи функції Get() об'єкт obj руйнується за допомогою деструктора. Деструктор звільняє область пам'яті, яка адресується показником obj.р. Але ж ця (вже звільнена) область пам'яті –  та ж сама область, на яку все ще вказує член даних (початкового об'єкта) ObjA.р! Тобто, як на перший погляд – виникає серйозна помилка. Насправді  справи йдуть ще  гірше. Після  завершення  роботи  коду програми руйнується об'єкт ObjA  і динамічно виділена (ще під час його створення) пам'ять звільняється повторно. Йдеться про те, що звільнення однієї і тієї ж самої області динамічно  виділеної пам'яті удруге  вважається невизначеною операцією,  яка,  як правило (залежно від того, яка система динамічного розподілу пам'яті реалізована), спричиняє непоправну помилку. Одним  із шляхів вирішення проблеми, пов'язаної з руйнуванням (ще потрібних) даних деструктором об'єкта, який є параметром функції, полягає не в передачі самого об'єкта, а в передачі показника на нього або посилання. У цьому випадку копія об'єкта не створюється; отже, після завершення роботи функції деструктор  не  викликається. Ось  як  виглядає,  наприклад,  один  із  способів  виправлення попереднього коду програми.

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

                яких динамічно виділяється та звільняється область пам'яті

class myClass

{              int *p;

        public:

              myClass(int c)

                 {     p = new int; *p = c;

                         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;

}

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

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

*p= 10

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

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

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


 

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

33362. Типовая схема СУ на базе КР1816ВУ51 27 KB
  В случае если производительность процессора микроконтроллера достаточна для решения поставленной задачи эту проблему можно решить организацией системы шин к которым и подключаются все необходимые устройства. Кроме достаточной производительности микроконтроллер должен иметь возможность подключения внешней памяти данных. Микроконтроллер МК51 обладает такой возможностью.
33363. Состав и назначение элементов процессорного ядра, характеристика ОМК АТ90S8515 31 KB
  Организация памяти микроконтроллера Память микроконтроллеров VR семейства Clssic выполнена по Гарвардской архитектуре в которой разделены не только адресные пространства памяти программ и памяти данных но также и шины доступа к ним. В связи с тем что регистровая память находится в адресном пространстве ОЗУ об этих двух областях памяти обычно говорят как об одной. 6 регистров общего назначения R26 R31 X Y Z используется в качестве указателей при косвенной адресации памяти данных. Каждый регистр файла имеет свой собственный адрес в...
33364. Структура памяти ОМК АТ90S8515 30.5 KB
  Причем память данных состоит из трех областей: регистровая память статическое ОЗУ и память на основе EEPROM. В связи с тем что регистровая память находится в адресном пространстве ОЗУ об этих двух областях памяти обычно говорят как об одной. Память программ Память программ ёмкостью 4 К 16разрядных слов предназначена для хранения команд управляющих функционированием микроконтроллера.
33365. Порты ввода-вывода ОМК АТ90S8515 31.5 KB
  Конфигурирование каждой линии порта задание направления передачи данных может быть произведено программно в любой момент времени. Обращение к портам ввода вывода Обращение к портам производится через регистры ввода вывода причем под каждый порт в адресном пространстве ввода вывода зарезервировано по 3 адреса. По этим адресам размещаются три регистра: регистр данных порта PORTx регистр направления данных DDRx и регистр выводов порта PINx. Действительные названия регистров и их разрядов получаются подстановкой названия порта вместо...
33366. Таймер/счётчики ОМК АТ90S8515 38 KB
  Как правило эти выводы линии портов ввода вывода общего назначения а функции реализуемые этими выводами при работе совместно с таймерами счетчиками являются их альтернативными функциями. Выводы используемые таймерами счетчиками общего назначения Название T90S8515 Описание T0 PB0 Вход внешнего сигнала таймера T0 T1 PB1 Вход внешнего сигнала таймера T1 ICP ICP Вход захвата таймера T1 OC1 Выход схемы сравнения таймера T1 OC1 PD5 То же OC1B OC1B То же TOSC1 Вход для подключения резонатора TOSC2 Выход для подключения резонатора ...
33367. Универсальный асинхронный приемопередатчик ОМК АТ90S8515 38.5 KB
  Управление работой приемопередатчика осуществляется с помощью регистра управления UCR. Текущее состояние приемопередатчика определяется с помощью регистра состояния USR. При чтении регистра UDR выполняется обращение к регистру приемника при записи к регистру передатчика. Работа передатчика разрешается установкой в 1 разряда TXEN регистра UCR UCSRB.
33368. Система прерываний ОМК AT90S8515 63 KB
  При возникновении прерывания микроконтроллер сохраняет в стеке содержимое счетчика команд PC и загружает в него адрес соответствующего вектора прерывания. По этому адресу должна находиться команда относительного перехода к подпрограмме обработки прерывания. Кроме того последней командой подпрограммы обработки прерывания должна быть команда RETI которая обеспечивает возврат в основную программу и восстановление предварительно сохранённого счетчика команд. Младшие адреса памяти программ начиная с адреса 001 отведены под таблицу векторов...
33369. Канал SPI (синхронный последовательный порт) 38.5 KB
  Выводы используемые модулем SPI Название сигнала T90S8515 Описание SCK РВ7 Выход mster вход slve тактового сигнала MISO РВ6 Вход mster выход slve данных MOSI РВ5 Выход mster вход slve данных РВ4 Выбор ведомого устройства Спецификация интерфейса SPI предусматривает 4 режима передачи данных. Эти режимы различаются соответствием между фазой момент считывания сигнала тактового сигнала SCK его полярностью и передаваемыми данными. Задание режима передачи данных Разряд Описание CPOL Полярность тактового сигнала 0 генерируются...
33370. Система команд и способы адресации памяти данных 76.5 KB
  При прямой адресации адреса операндов содержатся непосредственно в слове команды.4 5 бит слова команды рис. Прямая адресация одного регистра общего назначения Примером команд использующих этот способ адресации являются команды работы со стеком PUSH Rr POP Rd команды инкремента INC Rd декремента DEC Rd а также некоторые команды арифметических операций.d4 5 бит слова команды рис.