Одне з найчастіших питань, які початківці задають з приводу класів: «При виклику методу класу, як C++ відстежує об’єкт, який його викликав?». Відповідь полягає в тому, що C++ для цих цілей використовує прихований вказівник *this!
Прихований вказівник *this
Нижче наведено простий клас, який містить цілочисельне значення і має конструктор і функції доступу. Зверніть увагу, деструктор тут не потрібен, так як мова C++ може очистити пам’ять після змінної-члена самостійно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> class Another { private: int m_number; public: Another(int number) { setNumber(number); } void setNumber(int number) { m_number = number; } int getNumber() { return m_number; } }; int main() { Another another(3); another.setNumber(4); std::cout << another.getNumber() << '\n'; return 0; } |
Результат виконання програми:
4
При виклику another.setNumber(4);
мова C++ розуміє, що функція setNumber() працює з об’єктом another
, а m_number
— це фактично another.m_number
. Розглянемо детально, як це все працює.
Візьмемо, наприклад, наступний рядок:
1 |
another.setNumber(4); |
Хоча на перший погляд здається, що у нас тут тільки один аргумент, але насправді їх у нас два! Під час компіляції рядок another.setNumber(4);
конвертується компілятором в наступне:
1 |
setNumber(&another, 4); // об'єкт another конвертувався з об'єкту, який знаходився перед крапкою, в аргумент функції! |
Тепер це всього лише стандартний виклик функції, а об’єкт another
(який раніше був окремим об’єктом і знаходився перед крапкою) тепер передається по адресі в якості аргументу функції.
Але це тільки половина справи. Оскільки у виклику функції тепер є два аргументи, то і метод потрібно змінити відповідним чином (щоб він приймав два аргументи). Отже, наступний метод:
1 |
void setNumber(int number) { m_number = number; } |
Конвертується компілятором в:
1 |
void setNumber(Another* const this, int number) { this->m_number = number; } |
При компіляції звичайного методу, компілятор неявно додає до нього параметр *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, який вказує на адресу об’єкта, з яким в даний момент виконується операція, наприклад:
1 2 3 4 5 6 7 8 9 |
int main() { Another X(3); // *this = &X всередині конструктора Another Another Y(4); // *this = &Y всередині конструктора Another X.setNumber(5); // *this = &X всередині методу setNumber Y.setNumber(6); // *this = &Y всередині методу setNumber return 0; } |
Зверніть увагу, вказівник *this по черзі містить адреси об’єктів X
або Y
залежно від того, який метод викликаний і зараз виконується.
Явне вказування вказівника *this
У більшості випадків вам не потрібно явно вказувати вказівник *this. Проте, іноді це може бути корисним. Наприклад, якщо у вас є конструктор (або метод), який має параметр з тим же ім’ям, що і змінна-член, то усунути неоднозначність можна за допомогою вказівника *this:
1 2 3 4 5 6 7 8 9 10 11 |
class Something { private: int data; public: Something(int data) { this->data = data; } }; |
Тут конструктор приймає параметр з тим же ім’ям, що і змінна-член. В цьому випадку data
відноситься до параметру, а this->data
відноситься до змінної-члену. Хоча це допустима практика, але рекомендується використовувати префікс m_
для всіх імен змінних-членів вашого класу, так як це допомагає запобігти дублюванню імен в цілому!
Ланцюжки методів класу
Іноді буває корисно, щоб метод класу повертав об’єкт, з яким працює, у вигляді значення, що повертається. Таким чином це дозволить кільком методам об’єднатися в «ланцюжок», працюючи при цьому з одним об’єктом! Ми насправді користуємося цим вже давно. Наприклад, коли ми виводимо дані за допомогою std::cout по частинам:
1 |
std::cout << "Hello, " << userName; |
В цьому випадку std::cout є об’єктом, а оператор <<
— методом, який працює з цим об’єктом. Компілятор обробляє вищенаведений фрагмент наступним чином:
1 |
(std::cout << "Hello, ") << userName; |
Спочатку оператор <<
використовує std::cout і рядковий літерал Hello
для виводу Hello
в консоль. Однак, оскільки це частина виразу, оператор <<
також повинен повернути значення (або void). Якщо оператор <<
повертає void, то ми отримуємо наступне:
1 |
(void) << userName; |
Що явно не має ніякого сенсу (компілятор видасть помилку). Однак, замість цього, оператор <<
повертає вказівник *this, що в цьому контексті є просто std::cout. Таким чином, після обробки першого оператора <<
, ми отримуємо:
1 |
(std::cout) << userName; |
Що призводить до виводу імені користувача (userName
).
Таким чином, нам потрібно вказати об’єкт (в даному випадку, std::cout) один раз, і кожен виклик функції передаватиме цей об’єкт наступній функції, що дозволить нам об’єднати декілька методів разом.
Ми самі можемо реалізувати таку поведінку. Розглянемо наступний клас:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Mathem { private: int m_value; public: Mathem() { m_value = 0; } void add(int value) { m_value += value; } void sub(int value) { m_value -= value; } void multiply(int value) { m_value *= value; } int getValue() { return m_value; } }; |
Якщо ви хочете додати 7, відняти 5 і помножити все на 3, то потрібно зробити наступне:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int main() { Mathem operation; operation.add(7); // повертає void operation.sub(5); // повертає void operation.multiply(3); // повертає void std::cout << operation.getValue() << '\n'; return 0; } |
Результат:
6
Проте, якщо кожна функція повертатиме вказівник *this, то ми зможемо з’єднати ці виклики методів в один ланцюжок. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Mathem { private: int m_value; public: Mathem() { m_value = 0; } Mathem& add(int value) { m_value += value; return *this; } Mathem& sub(int value) { m_value -= value; return *this; } Mathem& multiply(int value) { m_value *= value; return *this; } int getValue() { return m_value; } }; |
Зверніть увагу, add(), sub() і multiply() тепер повертають вказівник *this, тому наступне буде коректним:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { Mathem operation; operation.add(7).sub(5).multiply(3); std::cout << operation.getValue() << '\n'; return 0; } |
Результат:
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. Таким чином ви зможете поєднати декілька методів в один «ланцюжок». Це найчастіше використовується при перевантаженні операторів, але про це трохи пізніше.