12.10 – Скрытый указатель "this"
Один из вопросов о классах, который часто задают начинающие в ООП программисты: «Когда вызывается функция-член, как C++ отслеживает, для какого объекта она была вызвана?». Ответ заключается в том, что C++ использует скрытый указатель с именем "this
"! Давайте рассмотрим "this
" подробнее.
Ниже приведен простой класс, который содержит число int
и предоставляет конструктор и функции доступа. Обратите внимание, что деструктор здесь не требуется, потому что C++ может очищать целочисленные переменные-члены за нас.
class Simple
{
private:
int m_id;
public:
Simple(int id)
: m_id{ id }
{
}
void setID(int id) { m_id = id; }
int getID() { return m_id; }
};
А вот пример программы, в которой используется этот класс:
int main()
{
Simple simple{1};
simple.setID(2);
std::cout << simple.getID() << '\n';
return 0;
}
Как и следовало ожидать, эта программа дает следующий результат:
2
Каким-то образом, когда мы вызываем simple.setID(2);
, C++ знает, что функция setID()
должна работать с объектом simple
, и что m_id
на самом деле относится к simple.m_id
. Давайте посмотрим, как это работает.
Скрытый указатель *this
Взгляните на следующую строку кода из приведенного выше примера:
simple.setID(2);
Хотя вызов функции setID()
выглядит так, будто имеет только один аргумент, на самом деле у него их два! При компиляции компилятор преобразует simple.setID(2);
в следующее:
// обратите внимание, что simple был изменен
// с объектного префикса на аргумент функции!
setID(&simple, 2);
Обратите внимание, что теперь это просто стандартный вызов функции, а объект simple
(который раньше был объектным префиксом) теперь передается по адресу в качестве аргумента функции.
Но это только половина ответа. Поскольку теперь в вызов функции добавлен аргумент, необходимо изменить определение функции-члена, чтобы принять (и использовать) этот аргумент в качестве параметра. Следовательно, следующая функция-член:
void setID(int id) { m_id = id; }
преобразуется компилятором в:
void setID(Simple* const this, int id) { this->m_id = id; }
Когда компилятор компилирует обычную функцию-член, он неявно добавляет к функции новый параметр с именем "this
". Указатель this
– это скрытый константный указатель, который содержит адрес объекта, для которого была вызвана функция-член.
Осталось позаботиться еще об одной детали. Внутри функции-члена также необходимо обновить все члены класса (функции и переменные), чтобы они ссылались на объект, для которого была вызвана эта функция-член. Это легко сделать, добавив к каждому из них префикс "this->
". Таким образом, в теле функции setID() m_id
(который является переменной-членом класса) был преобразован в this->m_id
. Таким образом, когда "this
" указывает на адрес simple
, this->m_id
преобразуется в simple.m_id
.
Обобщим:
- Когда мы вызываем
simple.setID(2)
, компилятор фактически вызываетsetID(&simple, 2)
. - Внутри
setID()
указательthis
содержит адрес объектаsimple
. - Любые переменные-члены внутри
setID()
имеют префикс "this->
". Поэтому, когда мы говоримm_id = id
, компилятор на самом деле выполняетthis->m_id = id
, который в этом случае обновляетsimple.m_id
доid
.
Хорошая новость в том, что всё это происходит автоматически, и на самом деле не имеет значения, помните вы, как это работает или нет. Всё, что вам нужно помнить, это то, что все обычные функции-члены имеют указатель "this
", который указывает на объект, для которого функция была вызвана.
"this
" всегда указывает на объект, над которым работает
Начинающие программисты иногда не понимают, сколько существует указателей this
. Каждая функция-член имеет параметр указатель this
, который устанавливается равным адресу объекта, над которым выполняется операция. Рассмотрим следующий код:
int main()
{
Simple A{1}; // this = &A внутри конструктора Simple
Simple B{2}; // this = &B внутри конструктора Simple
A.setID(3); // this = &A внутри функции-члена setID
B.setID(4); // this = &B внутри функции-члена setID
return 0;
}
Обратите внимание, что указатель this
поочередно содержит адрес объекта A
или B
в зависимости от того, вызвали ли мы функцию-член для объекта A
или B
.
Поскольку "this
" – это просто параметр функции, он не добавляет никакого использования памяти вашему классу (только к вызову функции-члена, поскольку этот параметр необходимо передать в функцию и сохранить в памяти).
Явная ссылка на "this
"
В большинстве случаев вам никогда не нужно явно ссылаться на указатель this
. Однако есть несколько случаев, когда это может быть полезно:
Во-первых, если у вас есть конструктор (или функция-член), у которых есть параметр с тем же именем, что и у переменной-члена, вы можете устранить их неоднозначность, используя "this
":
class Something
{
private:
int data;
public:
Something(int data)
{
this->data = data; // this->data - член, data - локальный параметр
}
};
Обратите внимание, что наш конструктор принимает параметр с тем же именем, что и переменная-член. В этом случае "data
" относится к параметру, а "this->data
" относится к переменной-члену. Хотя это приемлемая практика кодирования, но мы считаем, что использование префикса "m_
" во всех именах переменных-членов обеспечивает лучшее решение, так как полностью предотвращает дублирование имен!
Некоторые разработчики предпочитают явно добавлять this->
ко всем членам класса. Мы рекомендуем вам этого не делать, так как это делает ваш код менее читабельным и малоэффективным. Использование префикса m_
– более удобный способ отличать переменные-члены от (локальных) переменных, не являющихся членами.
Объединение функций-членов в цепочку
Во-вторых, иногда может быть полезно, чтобы функция-член класса возвращала в качестве возвращаемого значения объект, с которым она работала. Основная причина для этого состоит в том, чтобы позволить последовательности функций-членов быть «связанными» вместе, чтобы несколько функций-членов могли вызываться для одного и того же объекта! На самом деле вы занимаетесь этим уже долгое время. Рассмотрим распространенный пример, когда вы выводите более одного фрагмента текста с помощью std::cout
:
std::cout << "Hello, " << userName;
В этом случае std::cout
– это объект, а operator<<
– функция-член, которая работает с этим объектом. Компилятор вычисляет приведенный выше фрагмент следующим образом:
(std::cout << "Hello, ") << userName;
Во-первых, operator<<
использует std::cout
и строковый литерал "Hello" для вывода "Hello" в консоль. Однако, поскольку это часть выражения, operator<<
также должен возвращать значение (или void
). Если operator<<
вернет void
, вы получите следующее:
(void) << userName;
что явно не имеет никакого смысла (и компилятор выдаст ошибку). Вместо этого operator<<
возвращает *this
, которым в данном контексте является объект std::cout
. Таким образом, после вычисления первого оператора <<
мы получим:
(std::cout) << userName;
который затем печатает имя пользователя.
Таким образом, нам нужно указать объект (в данном случае std::cout
) только один раз, и вызов каждой функции передает его для работы следующей функции, что позволяет нам объединять несколько команд в одну цепочку.
Мы сами можем реализовать такое поведение. Рассмотрим следующий класс:
class Calc
{
private:
int m_value{0};
public:
void add(int value) { m_value += value; }
void sub(int value) { m_value -= value; }
void mult(int value) { m_value *= value; }
int getValue() { return m_value; }
};
Если вы хотите сложить 5, вычесть 3 и умножить на 4, вам нужно будет сделать следующее:
#include <iostream>
int main()
{
Calc calc{};
calc.add(5); // возвращает void
calc.sub(3); // возвращает void
calc.mult(4); // возвращает void
std::cout << calc.getValue() << '\n';
return 0;
}
Однако, если мы заставим каждую функцию возвращать *this
, мы сможем объединить вызовы в цепочку. Вот новая версия Calc
с «связываемыми» функциями:
class Calc
{
private:
int m_value{};
public:
Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }
int getValue() { return m_value; }
};
Обратите внимание, что add()
, sub()
и mult()
теперь возвращают *this
. Следовательно, это позволяет нам делать следующее:
#include <iostream>
int main()
{
Calc calc{};
calc.add(5).sub(3).mult(4);
std::cout << calc.getValue() << '\n';
return 0;
}
Мы эффективно свели три строки в одно выражение! Давайте подробнее рассмотрим, как это работает.
Сначала вызывается calc.add(5)
, которая добавляет 5 к нашему значению m_value
. add()
затем возвращает *this
, который является просто ссылкой на calc
, поэтому calc
будет объектом, используемым в последующем вычислении. Затем вычисляется calc.sub(3)
, которая вычитает 3 из m_value
и снова возвращает calc
. Наконец, calc.mult(4)
умножает m_value
на 4 и возвращает calc
, который больше не используется и поэтому игнорируется.
Поскольку каждая функция изменяла calc
при выполнении, m_value
в calc
теперь содержит значение (((0 + 5) - 3) * 4), которое равно 8.
Резюме
Указатель "this
" – это скрытый параметр, неявно добавляемый к любой нестатической функции-члену. В большинстве случаев прямой доступ к нему не требуется, но при необходимости возможен. Стоит отметить, что this
– это константный указатель – вы можете изменить значение базового объекта, на который он указывает, но не можете заставить его указывать на что-то еще!
Имея функции, которые возвращали бы *this
вместо void
, вы можете объединить эти функции в цепочку. Это чаще всего используется при перегрузке операторов для классов (о чем мы поговорим подробнее в главе 13).