Один из частых вопросов, которые новички задают по поводу классов: «При вызове метода класса, как 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. Таким образом, вы сможете соединить несколько методов в одну «цепочку». Это чаще всего используется при перегрузке операторов, но об этом несколько позже.
Подскажите, а что в данной записи означает
Чем является Mathem& и как оно
в данном случае взаимодействует с объектом (*this)?
Mathem& это тип возвращаемого значения: ссылка (знак амперсанда &) на тип Mathem. А так как мы знаем, что тип Mathem это класс, то ссылкой на этот тип является объектом этого класса.
Компилятор (см. предыдущий урок) неявно указывает параметр *this всем методам любого класса, чтобы метод понимал с каким конкретно объектом он работает. Этот параметр мы, в свою очередь возвращаем в конце метода, у которого указан тип возвращаемого значения как ссылка на объект Mathem(т.е. Mathem&). Таким образом, мы получаем возможность друг за дружкой вызывать методы через оператор "точка" для одного и того же объекта.
Долго мучался с этим участком кода и вроде бы понял. Но…
this — это указатель на объект.
return *this — возвращает сам объект(так как мы разыменовываем указатель).
Возвращаемым типом является Mathem&, тогда объект преобразуется в ссылку на объект.
После из ссылки на объект получаем доступ к методам.
И у меня возник вопрос зачем возвращаемым типом делать Mathem&, если можно сделать Mathem и должно работать так же. Оказалось что не так же. Когда возвращаемый тип Mathem, хоть и выполняются все три метода, но на поле класса влияет только первый. Почему так?
Перечитайте урок (№100) об отличиях передачи параметров по значению, по ссылке и по адресу — тогда вы всё поймёте)
Коротко — при передаче по ссылке вы работаете с одним объектом, а не создаёте его копии как было бы в случае с передачей по значению.
уроки просто суперские!!! хотелось бы даже побольше вникнуть в отладку с дизассемблером! автору респект!
Доброго времени суток, хотел бы поинтересоваться, если так называемый скрытый указатель this сохраняет адрес объекта класса, то почему в уроке в большинстве случаев вы ссылаетесь на "указатель" *this (что не совсем указатель, а скорее разыменованный указатель, то бишь в данном случае объект)? Прошу перепроверить, либо указать на ошибочность моего утверждения. Всего вам хорошего, без негатива.
Здравствуйте. В данном случае, используется *this, как указатель при объявлении. Объявляя указатель this, мы используем звёздочку (*).
Материал превосходный. Автору и Переводчику Спасибо.
Единственно что огорчило:"Хорошей новостью является то, что это всё происходит скрыто от нас (программистов) и не имеет значения, помните ли вы, как это работает или нет".
Очень хочется "потрогать за вымя" как оно под капотом работает. Кто подскажет хороший материал о программировании в целом? А не только один конкретный язык.
Спрятали, расковыряли.. Это для понимания, для обучения. В большой программе сокрытие данных удобно, это не извращение, это просто предотвращает множество ошибок. Что бы выучить язык, нужно на нем много писать, реализуя разные задачи. Тогда все, вначале через десятки и сотни затыков, понимается, и писаться начинает быстро, бегло и уже на автомате.
Единственное, что пока не смог понять — почему автор так не любит указатели?
Именно указатели были самой яркой фишкой С. И вишенька на торте — указатель на функцию.
Как там в 12 стульях… Вы оцените красоту игры.
Все плюсы (++) — это именно работа с указателями (неявная, правда).
А у автора указатель на функцию — самая уродливая конструкция языка.
Как по мне — использование extern в описании локальной (пусть даже константной переменной) на порядок уродливее.
Думаю, "уродливы" они не по своим функциональным возможностям, а по формальному описанию их использования — нагромождение множества заковыристых синтаксических конструкций, символов.
Это как с тернарным оператором. Его не любят не за то, что он делает, а за то, как он выглядит и, следовательно, что такой его вид бывает порой тяжело читать и анализировать 🙂
Не понятно для чего указывать: return *this; в
Без указывания: return *this; результат идентичен.
Поясните пожалуйста.
Не вернуть значение из функции, если заявлено, что она его возвращает, — undefined behaviour, и компилятор об этом честно предупреждает: https://godbolt.org/z/iA19xJ — соответственно, даже если результат и работает, гарантий, что он продолжит работать при любом изменении в любой части программы (или даже при изменении версии компилятора) уже не будет.
Чем дальше по этим урокам двигаюсь, тем больше вижу серьезных методических огрехов в этом учебном материале.
Кто мне сможет ответить на простой вопрос — зачем было "прятать" всю "кухню" от программиста, переходя от C к C++ и городить все эти "классы", если теперь мы лезем "внутрь" и пытаемся разобраться в том как же там компилит компилятор с этим указателем this !? …
При 1-м изучении это оставляем твердую уверенность, что создатели C++ ООП были какими то мазахистами: "давайте все спрячем, чтобы это нам НЕ мешало", а … теперь все "расковыряем" чтобы посмотреть как все это прячется !? …
Это совершенно НЕЛОГИЧНО !!! …
Может быть это this и нужно, но прогер должен прилично пописать кода, прежде чем ему начнут объяснять вот эти детали с this …
Короче, я НЕ ПОНЯЛ … для чего все это …
Основное преимущество данного … учебного материала для меня в том, что оно одновременно обьясняет базы попутно обьясняя как они работают. Да, вы можете запомнить что в 4 + 3 "+" заставляет получить интересную цифру что скорее всего нам нужна. но понимание того, что "+" является оператором сложения лучше раскроет эту тему)
да, этот учебник сложнее большинства простеньких постепенных курсов. но если вы не способны принять усложненный вариант — к чему вам идти дальше по программированию?)
можно уточнить "this" это указатель или все таки ссылка?
исходя из прототипа:
похоже, что функция возвращает ссылку типа "Mathem".
ps
и, да, я помню, что ссылки реализованы через константные указатели
т.е. const type * == type &
this — это указатель, *this — разыменованный указатель, Mathem& — ссылка на разыменованный указатель.
Просто и ясно.