12.10 – Скрытый указатель "this"

Добавлено 7 июля 2021 в 08:53

Один из вопросов о классах, который часто задают начинающие в ООП программисты: «Когда вызывается функция-член, как 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.

Обобщим:

  1. Когда мы вызываем simple.setID(2), компилятор фактически вызывает setID(&simple, 2).
  2. Внутри setID() указатель this содержит адрес объекта simple.
  3. Любые переменные-члены внутри 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).

Теги

C++ / CppLearnCppthisДля начинающихОбучениеПрограммированиеУказатель / Pointer (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.