Урок №129. Прихований вказівник *this

  Юрій  | 

  Оновл. 3 Лют 2021  | 

 160

Одне з найчастіших питань, які новачки задають з приводу класів: «При виклику методу класу, як C++ відстежує те, який об’єкт його викликав?». Відповідь полягає в тому, що C++ для цих цілей використовує прихований вказівник *this!

Прихований вказівник *this

Нижче наведено простий клас, який містить цілочисельне значення і має конструктор і функції доступу. Зверніть увагу, деструктор тут не потрібен, так як мова C++ може очистити пам’ять після змінної-члена самостійно:

Результат виконання програми:

4

При виклику another.setNumber(4); C++ розуміє, що функція setNumber() працює з об’єктом another, а m_number — це фактично another.m_number. Розглянемо детально, як це все працює.

Візьмемо, наприклад, наступний рядок:

Хоча на перший погляд здається, що у нас тут тільки один аргумент, але насправді їх у нас два! Під час компіляції рядок another.setNumber(4); конвертується компілятором в наступне:

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

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

Конвертується компілятором в:

При компіляції звичайного методу, компілятор неявно додає до нього параметр *this. Вказівник *this — це прихований константний вказівник, що містить адресу об’єкта, який викликає метод класу.

Є ще одна деталь. Всередині методу також необхідно оновити всі члени класу (функції і змінні), щоб вони посилалися на об’єкт, який викликає цей метод. Це легко зробити, додавши префікс this-> до кожного з них. Таким чином, в тілі функції setNumber(), m_number (змінна-член класу) буде конвертована в this->m_number. І коли *this вказує на адресу another, то this->m_number буде вказувати на another.m_number.

З’єднуємо все разом:

   При виклику another.setNumber(4) компілятор фактично викликає setNumber(&another, 4).

   Всередині setNumber() вказівник *this містить адресу об’єкта another.

   До будь-яких змінних-членів всередині setNumber() додається префікс this->. Тому, коли ми говоримо m_number = number, компілятор фактично виконує this->m_number = number, який, в цьому випадку, оновлює another.m_number на number.

Доброю новиною є те, що це все відбувається приховано від нас (програмістів), і не має значення, чи пам’ятаєте ви, як це працює чи ні. Все, що вам потрібно запам’ятати — всі звичайні методи класу мають вказівник *this, який вказує на об’єкт, пов’язаний з викликом методу класу.

Вказівник *this завжди вказує на поточний об’єкт

Початківці іноді плутають, скільки вказівників *this існує. Кожен метод має в якості параметру вказівник *this, який вказує на адресу об’єкта, з яким в даний момент виконується операція, наприклад:

Зверніть увагу, вказівник *this по черзі містить адреси об’єктів X або Y залежно від того, який метод викликаний і зараз виконується.

Явне вказування вказівника *this

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

Тут конструктор приймає параметр з тим же ім’ям, що і змінна-член. В цьому випадку data відноситься до параметру, а this->data відноситься до змінної-члену. Хоча це допустима практика, але рекомендується використовувати префікс m_ для всіх імен змінних-членів вашого класу, так як це допомагає запобігти дублюванню імен в цілому!

Ланцюжки методів класу

Іноді буває корисно, щоб метод класу повертав об’єкт, з яким працює, у вигляді значення, що повертається. Таким чином це дозволить кільком методам об’єднатися в «ланцюжок», працюючи при цьому з одним об’єктом! Ми насправді користуємося цим вже давно. Наприклад, коли ми виводимо дані за допомогою std::cout по частинам:

В цьому випадку std::cout є об’єктом, а оператор << — методом, який працює з цим об’єктом. Компілятор обробляє вищенаведений фрагмент наступним чином:

Спочатку оператор << використовує std::cout і рядковий літерал Hello для виведення Hello в консоль. Однак, оскільки це частина виразу, оператор << також повинен повернути значення (або void). Якщо оператор << повертає void, то отримуємо наступне:

Що явно не має ніякого сенсу (компілятор видасть помилку). Однак, замість цього, оператор << повертає вказівник *this, що в цьому контексті є просто std::cout. Таким чином, після обробки першого оператора <<, ми отримуємо:

Що призводить до виводу імені користувача (userName).

Таким чином, нам потрібно вказати об’єкт (в даному випадку, std::cout) один раз, і кожен виклик функції буде передавати цей об’єкт наступній функції, що дозволить нам об’єднати декілька методів разом.

Ми самі можемо реалізувати таку поведінку. Розглянемо наступний клас:

Якщо ви хочете додати 7, відняти 5 і помножити все на 3, то потрібно зробити наступне:

Результат:

6

Проте, якщо кожна функція буде повертати вказівник *this, то ми зможемо з’єднати ці виклики методів в один ланцюжок. Наприклад:

Зверніть увагу, add(), sub() і multiply() тепер повертають вказівник *this, тому наступне буде коректним:

Результат:

6

Ми фактично вмістили три окремі рядки в один вираз! Тепер розглянемо це детально:

   Спочатку викликається operation.add(7), який додає 7 до нашого m_value.

   Потім add() повертає вказівник *this, який є посиланням на об’єкт operation.

   Потім виклик operation.sub(5) віднімає 5 з m_value і повертає operation.

   multiply(3) множить m_value на 3 і повертає operation, який вже ігнорується.

   Проте, оскільки кожна функція модифікувала operation, m_value об’єкта operation тепер містить значення ((0 + 7) - 5) * 3), яке дорівнює 6.

Висновки

Вказівник *this є прихованим параметром, який неявно додається до кожного методу класу. У більшості випадків нам не потрібно звертатися до нього напряму, але при необхідності це можна зробити. Варто відзначити, що вказівник *this є константним вказівником — ви можете змінити значення вихідного об’єкта, але ви не можете змусити вказівник *this вказувати на щось інше!

Якщо у вас є функції, які повертають void, то повертайте *this замість void. Таким чином ви зможете поєднати декілька методів в один «ланцюжок». Це найчастіше використовується при перевантаженні операторів, але про це трохи пізніше.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (4 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *