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

  Юрий  | 

  |

  Обновл. 13 Сен 2021  | 

 94014

 ǀ   20 

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

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

  1. Дмитрий:

    В вашем примере :

    вызов функции setNumber(number) стоит выше определения этой функции. Каким образом компилятор понимает , что это такое , если он компилирует код последовательно сверху вниз?

    1. Евгениальный:

      В классах это работает чуть по другому, и мы действительно можем писать определение где угодно. Внутри классов компилятор видит вызов функции как объявление, и уже сам дальше ищет определение. Если я не ошибаюсь, то это происходит на так называемом «предварительном проходе». Но по поводу последнего предложения не уверен

  2. Andrey:

    Подскажите, а что в данной записи означает

    Чем является Mathem& и как оно
    в данном случае взаимодействует с объектом (*this)?

    1. Denis:

      Mathem& это тип возвращаемого значения: ссылка (знак амперсанда &) на тип Mathem. А так как мы знаем, что тип Mathem это класс, то ссылкой на этот тип является объектом этого класса.

      Компилятор (см. предыдущий урок) неявно указывает параметр *this всем методам любого класса, чтобы метод понимал с каким конкретно объектом он работает. Этот параметр мы, в свою очередь возвращаем в конце метода, у которого указан тип возвращаемого значения как ссылка на объект Mathem(т.е. Mathem&). Таким образом, мы получаем возможность друг за дружкой вызывать методы через оператор "точка" для одного и того же объекта.

      1. Олександр:

        Долго мучался с этим участком кода и вроде бы понял. Но…

        this — это указатель на объект.
        return *this — возвращает сам объект(так как мы разыменовываем указатель).
        Возвращаемым типом является Mathem&, тогда объект преобразуется в ссылку на объект.
        После из ссылки на объект получаем доступ к методам.

        И у меня возник вопрос зачем возвращаемым типом делать Mathem&, если можно сделать Mathem и должно работать так же. Оказалось что не так же. Когда возвращаемый тип Mathem, хоть и выполняются все три метода, но на поле класса влияет только первый. Почему так?

        1. Виталий:

          Перечитайте урок (№100) об отличиях передачи параметров  по значению, по ссылке и по адресу — тогда вы всё поймёте)
          Коротко — при передаче по ссылке вы работаете с одним объектом, а не создаёте его копии как было бы в случае с передачей по значению.

  3. andrej:

    уроки просто суперские!!! хотелось бы даже побольше вникнуть в отладку с дизассемблером! автору респект!

  4. Александ:

    Доброго времени суток, хотел бы поинтересоваться, если так называемый скрытый указатель this сохраняет адрес объекта класса, то почему в уроке в большинстве случаев вы ссылаетесь на "указатель" *this (что не совсем указатель, а скорее разыменованный указатель, то бишь в данном случае объект)? Прошу перепроверить, либо указать на ошибочность моего утверждения. Всего вам хорошего, без негатива.

    1. Фото аватара Юрий:

      Здравствуйте. В данном случае, используется *this, как указатель при объявлении. Объявляя указатель this, мы используем звёздочку (*).

  5. Константин:

    Материал превосходный. Автору и Переводчику Спасибо.
    Единственно что огорчило:"Хорошей новостью является то, что это всё происходит скрыто от нас (программистов) и не имеет значения, помните ли вы, как это работает или нет".
    Очень хочется "потрогать за вымя" как оно под капотом работает. Кто подскажет хороший материал о программировании в целом? А не только один конкретный язык.

  6. Дмитро:

    Спрятали, расковыряли.. Это для понимания, для обучения. В большой программе сокрытие данных удобно, это не извращение, это просто предотвращает множество ошибок. Что бы выучить язык, нужно на нем много писать, реализуя разные задачи. Тогда все, вначале через десятки и сотни затыков, понимается, и писаться начинает быстро, бегло и уже на автомате.

  7. Валерий:

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

    1. Steindvart:

      Думаю, "уродливы" они не по своим функциональным возможностям, а по формальному описанию их использования — нагромождение множества заковыристых синтаксических конструкций, символов.

      Это как с тернарным оператором. Его не любят не за то, что он делает, а за то, как он выглядит и, следовательно, что такой его вид бывает порой тяжело читать и анализировать 🙂

  8. Дмитрий:

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

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

    1. Cerberus:

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

  9. alexk:

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

    1. koh:

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

  10. name:

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

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

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

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

    1. kmish:

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

      1. Алексей:

        Просто и ясно.

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

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