Урок №121. Скрытый указатель *this

  Юрий  | 

    | 

  Обновл. 14 Июн 2019  | 

 10130

 ǀ   7 

Один из частых вопросов, которые новички задают по поводу классов: «При вызове метода класса, как 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 (63 оценок, среднее: 4,87 из 5)
Загрузка...

Комментариев: 7

  1. Аватар Валерий:

    Единственное, что пока не смог понять — почему автор так не любит указатели?
    Именно указатели были самой яркой фишкой С. И вишенька на торте — указатель на функцию.
    Как там в 12 стульях… Вы оцените красоту игры.
    Все плюсы (++) — это именно работа с указателями (неявная, правда).
    А у автора указатель на функцию — самая уродливая конструкция языка.
    Как по мне — использование extern в описании локальной (пусть даже константной переменной) на порядок уродливее.

  2. Аватар Дмитрий:

    Не понятно для чего указывать: return *this; в

    Без указывания: return *this; результат идентичен.
    Поясните пожалуйста.

    1. Аватар Cerberus:

      Не вернуть значение из функции, если заявлено, что она его возвращает, — undefined behaviour, и компилятор об этом честно предупреждает: https://godbolt.org/z/iA19xJ — соответственно, даже если результат и работает, гарантий, что он продолжит работать при любом изменении в любой части программы (или даже при изменении версии компилятора) уже не будет.

  3. Аватар alexk:

    Чем дальше по этим урокам двигаюсь, тем больше вижу серьезных методических огрехов в этом учебном материале.
    Кто мне сможет ответить на простой вопрос — зачем было "прятать" всю "кухню" от программиста, переходя от C к C++ и городить все эти "классы", если теперь мы лезем "внутрь" и пытаемся разобраться в том как же там компилит компилятор с этим указателем this !? …
    При 1-м изучении это оставляем твердую уверенность, что создатели C++ ООП были какими то мазахистами: "давайте все спрячем, чтобы это нам НЕ мешало", а … теперь все "расковыряем" чтобы посмотреть как все это прячется !? …
    Это совершенно НЕЛОГИЧНО !!! …
    Может быть это this и нужно, но прогер должен прилично пописать кода, прежде чем ему начнут объяснять вот эти детали с this …
    Короче, я НЕ ПОНЯЛ … для чего все это …

    1. Аватар koh:

      Основное преимущество данного … учебного материала для меня в том, что оно одновременно обьясняет базы попутно обьясняя как они работают. Да, вы можете запомнить что в 4 + 3 "+" заставляет получить интересную цифру что скорее всего нам нужна. но понимание того, что "+" является оператором сложения лучше раскроет эту тему)
      да, этот учебник сложнее большинства простеньких постепенных курсов. но если вы не способны принять усложненный вариант — к чему вам идти дальше по программированию?)

  4. Аватар name:

    можно уточнить "this" это указатель или все таки ссылка?

    исходя из прототипа:

    похоже, что функция возвращает ссылку типа "Mathem".

    ps
    и, да, я помню, что ссылки реализованы через константные указатели
    т.е. const type * == type &

    1. Аватар kmish:

      this — это указатель, *this — разыменованный указатель, Mathem& — ссылка на разыменованный указатель.

Добавить комментарий

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