C++

Прихований покажчик this

Як методи класу знають, до якого обєкта вони належать? Прихований покажчик this, його роль у викликах методів. Явне використання this для розвязання конфліктів імен. Method chaining через return *this.

Загадка: як метод знає свій об'єкт?

Розглянемо простий клас із методом:

#include <iostream>

using namespace std;

class Counter
{
private:
    int value;

public:
    Counter(int initialValue) : value(initialValue) {}

    void increment()
    {
        value++;  // Яка саме змінна value збільшується?
    }

    int getValue() const
    {
        return value;  // Яке саме value повертається?
    }
};

int main()
{
    Counter counter1(10);
    Counter counter2(20);
    
    counter1.increment();  // Як метод знає, що це counter1?
    counter2.increment();  // Як метод знає, що це counter2?
    
    cout << "counter1: " << counter1.getValue() << "\n";  // 11
    cout << "counter2: " << counter2.getValue() << "\n";  // 21
    
    return 0;
}

Обидва об'єкти counter1 і counter2 викликають один і той самий метод increment(). Але кожен виклик збільшує свою власну змінну value. Як метод increment() розуміє, до якого об'єкта він належить у кожному конкретному виклику?

Відповідь: через прихований покажчик this.

Що таке покажчик this

Покажчик this — це прихований параметр, який автоматично додається компілятором до кожного нестатичного методу класу. Цей покажчик містить адресу об'єкта, для якого викликано метод.

Трансформація виклику методу компілятором

Коли ви пишете:

counter1.increment();

Компілятор внутрішньо перетворює це на щось подібне до:

increment(&counter1);  // Адреса counter1 передається як неявний аргумент

Об'єкт, що знаходився перед крапкою (counter1), перетворюється на аргумент функції — його адреса передається методу.

Трансформація сигнатури методу

Відповідно, сам метод також трансформується. Коли ви пишете:

void increment()
{
    value++;
}

Компілятор внутрішньо додає неявний параметр:

void increment(Counter* const this)  // Прихований параметр this
{
    this->value++;  // Доступ до поля через this
}
Тип покажчика this:
  • Для неконстантного методу: ClassName* const this — константний покажчик на неконстантний об'єкт (можна змінювати об'єкт, але не сам покажчик)
  • Для константного методу: const ClassName* const this — константний покажчик на константний об'єкт (не можна змінювати ні об'єкт, ні покажчик)

Трансформація доступу до полів

Кожне звернення до поля класу всередині методу неявно отримує префікс this->:

int getValue() const
{
    return value;  // Ви пишете це
}

// Компілятор перетворює на:
int getValue(const Counter* const this) const
{
    return this->value;  // Фактичний код
}

📝 Що робить компілятор

Ваш код:

counter1.increment();

Компілятор перетворює на:

increment(&counter1);

Метод отримує:

void increment(Counter* const this) {
    this->value++;
}

🎯 Результат

Коли counter1.increment() викликається:

  • this вказує на counter1
  • this->value означає counter1.value

Коли counter2.increment() викликається:

  • this вказує на counter2
  • this->value означає counter2.value

Один метод, багато об'єктівthis робить це можливим!

Візуалізація this у пам'яті

Розглянемо детальніше, як this працює в пам'яті:

class Point
{
private:
    int x;
    int y;

public:
    Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {}

    void print() const
    {
        cout << "Point(" << x << ", " << y << ")\n";
        // Фактично: this->x та this->y
    }

    void move(int dx, int dy)
    {
        x += dx;
        y += dy;
        // Фактично: this->x += dx; this->y += dy;
    }
};

int main()
{
    Point p1(10, 20);
    Point p2(30, 40);
    
    p1.print();  // this вказує на p1
    p2.print();  // this вказує на p2
    
    return 0;
}

Стан у пам'яті під час виконання p1.print():

During p1.print() execution
Filter
NameTypeValue
thisconst Point* const0x00007FF7BFEFF5A0
this->xint10
this->yint20
p1 (address)Point0x00007FF7BFEFF5A0
Running
Process: 12842

Стан у пам'яті під час виконання p2.print():

During p2.print() execution
Filter
NameTypeValue
thisconst Point* const0x00007FF7BFEFF5B0
this->xint30
this->yint40
p2 (address)Point0x00007FF7BFEFF5B0
Running
Process: 12842

Зверніть увагу: адреса this змінюється залежно від того, який об'єкт викликав метод! Під час p1.print() покажчик this дорівнює &p1. Під час p2.print() покажчик this дорівнює &p2.

Ключовий момент: Існує один екземпляр кожного методу в пам'яті програми, але він працює з різними об'єктами завдяки покажчику this, який вказує на конкретний об'єкт під час виклику.

Коли this змінює значення: кілька об'єктів

Розглянемо послідовність викликів методів:

#include <iostream>

using namespace std;

class Demo
{
private:
    int id;

public:
    Demo(int demoId) : id(demoId)
    {
        // В конструкторі this вказує на обєкт, що створюється
        cout << "Constructor: this = " << this << ", id = " << id << "\n";
    }

    void printInfo() const
    {
        cout << "Method: this = " << this << ", id = " << id << "\n";
    }
};

int main()
{
    Demo obj1(100);  // this = адреса obj1 в конструкторі
    Demo obj2(200);  // this = адреса obj2 в конструкторі
    
    obj1.printInfo();  // this = адреса obj1 в методі
    obj2.printInfo();  // this = адреса obj2 в методі
    obj1.printInfo();  // this знову = адреса obj1
    
    return 0;
}
g++ Demo.cpp -o demo && ./demo
$ ./demo
Constructor: this = 0x7ff7bfeff5a0, id = 100
Constructor: this = 0x7ff7bfeff5a8, id = 200
Method: this = 0x7ff7bfeff5a0, id = 100
Method: this = 0x7ff7bfeff5a8, id = 200
Method: this = 0x7ff7bfeff5a0, id = 100

Покажчик this динамічно змінюється при кожному виклику методу, завжди вказуючи на поточний об'єкт.

Явне використання this: коли це потрібно

У більшості випадків вам не потрібно писати this-> явно — компілятор додає це автоматично. Проте є ситуації, коли явне вказування this корисне або необхідне.

Розв'язання конфлікту імен

Якщо параметр методу має таке саме ім'я, як поле класу, виникає неоднозначність:

class Rectangle
{
private:
    int width;
    int height;

public:
    // ❌ Проблема: параметри мають ті самі імена, що й поля
    Rectangle(int width, int height)
    {
        width = width;    // ⚠️ Присвоєння параметра самому собі!
        height = height;  // ⚠️ Присвоєння параметра самому собі!
        // Поля width і height залишаються неініціалізованими
    }
};

У цьому коді width і height всередині конструктора завжди означають параметри, а не поля. Присвоєння width = width; присвоює параметр самому собі, а не полю класу.

Рішення 1: Використання this->

Rectangle(int width, int height)
{
    this->width = width;    // ✅ this->width — поле, width — параметр
    this->height = height;  // ✅ this->height — поле, height — параметр
}

Тепер this->width явно означає поле класу, а width — параметр.

Рішення 2: Інші імена параметрів (рекомендовано у курсі)

Rectangle(int w, int h) : width(w), height(h)  // ✅ Списки ініціалізації
{
}

// Або
Rectangle(int rectWidth, int rectHeight) : width(rectWidth), height(rectHeight)
{
}
Найкраща практика для курсу:Уникайте дублювання імен між параметрами та полями класу. Якщо все ж таки дублюєте — використовуйте this-> для явного розрізнення. Але найкраще — використовувати списки ініціалізації (MIL), де неоднозначність неможлива.

Передача поточного об'єкта в інші функції

Іноді потрібно передати поточний об'єкт як аргумент у функцію або метод:

class Logger
{
public:
    void log(const string& message, const void* objectPtr)
    {
        cout << "[LOG] " << message << " from object at " << objectPtr << "\n";
    }
};

class Worker
{
private:
    int workerId;
    Logger& logger;

public:
    Worker(int id, Logger& log) : workerId(id), logger(log) {}

    void doWork()
    {
        // Передаємо адресу поточного об'єкта в логер
        logger.log("Starting work", this);
        
        cout << "Worker " << workerId << " is working...\n";
        
        logger.log("Finished work", this);
    }
};

int main()
{
    Logger mainLogger;
    
    Worker worker1(1, mainLogger);
    Worker worker2(2, mainLogger);
    
    worker1.doWork();
    worker2.doWork();
    
    return 0;
}
g++ Worker.cpp -o worker && ./worker
$ ./worker
[LOG] Starting work from object at 0x7ff7bfeff5a0
Worker 1 is working...
[LOG] Finished work from object at 0x7ff7bfeff5a0
[LOG] Starting work from object at 0x7ff7bfeff5b0
Worker 2 is working...
[LOG] Finished work from object at 0x7ff7bfeff5b0

Тут this використовується для передачі адреси поточного об'єкта Worker у метод logger.log(), щоб логер міг ідентифікувати, який саме об'єкт виконує роботу.

Method Chaining: ланцюжки викликів методів

Одне з найпотужніших застосувань this — це method chaining (ланцюжок методів), коли кілька методів викликаються послідовно на одному об'єкті.

Проблема без method chaining

Розглянемо клас для математичних операцій:

class Calculator
{
private:
    int value;

public:
    Calculator() : value(0) {}

    void add(int x)
    {
        value += x;
    }

    void subtract(int x)
    {
        value -= x;
    }

    void multiply(int x)
    {
        value *= x;
    }

    int getResult() const
    {
        return value;
    }
};

int main()
{
    Calculator calc;
    
    // ❌ Потрібно багато окремих рядків
    calc.add(10);
    calc.subtract(3);
    calc.multiply(2);
    
    cout << "Result: " << calc.getResult() << "\n";  // ((0 + 10) - 3) * 2 = 14
    
    return 0;
}

Кожен метод повертає void, тому ми не можемо поєднати виклики в один вираз.

Рішення: повернення *this

Якщо метод повертає посилання на поточний об'єкт (return *this;), ми можемо викликати наступний метод одразу на результаті:

class Calculator
{
private:
    int value;

public:
    Calculator() : value(0) {}

    // Методи повертають посилання на поточний об'єкт
    Calculator& add(int x)
    {
        value += x;
        return *this;  // Повертаємо поточний об'єкт
    }

    Calculator& subtract(int x)
    {
        value -= x;
        return *this;
    }

    Calculator& multiply(int x)
    {
        value *= x;
        return *this;
    }

    int getResult() const
    {
        return value;
    }
};

int main()
{
    Calculator calc;
    
    // ✅ Ланцюжок викликів на одному рядку!
    calc.add(10).subtract(3).multiply(2);
    
    cout << "Result: " << calc.getResult() << "\n";  // 14
    
    return 0;
}
Як це працює?
  1. calc.add(10) додає 10 до value і повертає посилання на calc (через return *this)
  2. Результат — це calc, тому наступний виклик стає calc.subtract(3)
  3. calc.subtract(3) віднімає 3 і знову повертає calc
  4. calc.multiply(2) множить на 2 і повертає calc
  5. Фінальний результат — оновлений calc з value = 14

Чому *this, а не this?

Calculator& add(int x)
{
    value += x;
    return *this;  // Повертаємо посилання на об'єкт
}

// Тип повернення: Calculator&
// Дозволяє: calc.add(5).subtract(2)
  • this — це покажчик типу Calculator*
  • *this — це розіменований покажчик, тобто сам об'єкт типу Calculator
  • return *this; повертає посилання на об'єкт (Calculator&), що дозволяє використовувати оператор . для наступного виклику
Патерн Fluent Interface:Method chaining — це частина патерну Fluent Interface (текучий інтерфейс), який робить код більш читабельним, схожим на природну мову:
calc.add(10).subtract(3).multiply(2).divide(7);
// Читається як послідовність дій
Приклади у стандартній бібліотеці:
  • std::cout << "Hello" << " " << "World"operator<< повертає std::ostream&
  • std::string& string::append(...) — дозволяє str.append("a").append("b")

Практичний приклад: побудовник запитів (Query Builder)

Method chaining особливо корисний для побудови складних об'єктів чи запитів:

#include <iostream>
#include <string>

using namespace std;

class QueryBuilder
{
private:
    string query;

public:
    QueryBuilder() : query("") {}

    QueryBuilder& select(const string& columns)
    {
        query = "SELECT " + columns;
        return *this;
    }

    QueryBuilder& from(const string& table)
    {
        query += " FROM " + table;
        return *this;
    }

    QueryBuilder& where(const string& condition)
    {
        query += " WHERE " + condition;
        return *this;
    }

    QueryBuilder& orderBy(const string& column)
    {
        query += " ORDER BY " + column;
        return *this;
    }

    string build() const
    {
        return query + ";";
    }
};

int main()
{
    QueryBuilder qb;
    
    string sql = qb.select("name, age, city")
                   .from("users")
                   .where("age >= 18")
                   .orderBy("name")
                   .build();
    
    cout << "Generated SQL:\n" << sql << "\n";
    
    return 0;
}
g++ QueryBuilder.cpp -o query && ./query
$ ./query
Generated SQL:
SELECT name, age, city FROM users WHERE age >= 18 ORDER BY name;

Завдяки method chaining, побудова SQL-запиту виглядає природно та читабельно, схожа на сам SQL-синтаксис!

this у конструкторах та деструкторах

Покажчик this доступний не тільки у звичайних методах, але й у конструкторах та деструкторах.

this у конструкторі

class Resource
{
private:
    int id;
    static int nextId;

public:
    Resource()
    {
        id = nextId++;
        cout << "Resource created at " << this << " with ID " << id << "\n";
    }

    ~Resource()
    {
        cout << "Resource destroyed at " << this << " with ID " << id << "\n";
    }
};

int Resource::nextId = 1;

int main()
{
    Resource r1;
    Resource r2;
    
    return 0;
}
g++ Resource.cpp -o resource && ./resource
$ ./resource
Resource created at 0x7ff7bfeff5a0 with ID 1
Resource created at 0x7ff7bfeff5a8 with ID 2
Resource destroyed at 0x7ff7bfeff5a8 with ID 2
Resource destroyed at 0x7ff7bfeff5a0 with ID 1

У конструкторі this вказує на об'єкт, що створюється. У деструкторі — на об'єкт, що знищується.

Обережно з this у конструкторі!У конструкторі об'єкт ще не повністю ініціалізований. Не передавайте this у функції, які можуть викликати методи об'єкта, бо деякі поля можуть бути ще неініціалізованими:
void registerResource(Resource* ptr);  // Зовнішня функція

Resource::Resource()
{
    id = nextId++;
    
    // ❌ Небезпечно! Поля можуть бути не повністю ініціалізовані
    registerResource(this);
}
Це може призвести до невизначеної поведінки, якщо registerResource викликає методи об'єкта.

Властивості покажчика this

🔒 Константний покажчик

this є константним покажчиком — ви не можете змінити, на що він вказує:

void method()
{
    this = nullptr;  // ❌ Помилка компіляції!
    // cannot assign to variable 'this' with const-qualified type
}

Але ви можете змінювати об'єкт, на який вказує this (якщо метод не константний).

🚫 Немає в static методах

this доступний тільки в нестатичних методах. У статичних методах немає прив'язки до конкретного об'єкта:

class Test
{
public:
    static void staticMethod()
    {
        // this не існує тут!
        // cout << this;  // ❌ Помилка
    }

    void normalMethod()
    {
        cout << this;  // ✅ Працює
    }
};

📍 Унікальний для кожного виклику

Значення this змінюється при кожному виклику методу, завжди вказуючи на поточний об'єкт:

obj1.method();  // this == &obj1
obj2.method();  // this == &obj2
obj1.method();  // this == &obj1 знову

🔄 Автоматичне керування

Компілятор автоматично додає this до кожного методу та управляє його значенням. Програміст не може (і не повинен) створювати або змінювати this вручну.

Порівняння: з this і без this

Розглянемо один і той самий код двома способами:

class Person
{
private:
    string name;
    int age;

public:
    Person(string n, int a) : name(n), age(a) {}

    void printInfo() const
    {
        cout << "Name: " << name << "\n";
        cout << "Age: " << age << "\n";
    }

    void celebrateBirthday()
    {
        age++;
        cout << name << " is now " << age << "\n";
    }
};

Обидва варіанти абсолютно еквівалентні — компілятор додає this-> автоматично у першому випадку. Використання явного this-> — це питання стилю. У цьому курсі ми не використовуємо this-> без необхідності (для читабельності).

Реальні приклади використання this

Приклад 1: Порівняння з іншим об'єктом

Метод, що перевіряє, чи поточний об'єкт ідентичний іншому об'єкту (за адресою):

class Student
{
private:
    string name;
    int studentId;

public:
    Student(const string& n, int id) : name(n), studentId(id) {}

    bool isSameObject(const Student& other) const
    {
        // Порівнюємо адресу поточного об'єкта з адресою іншого
        return this == &other;
    }

    bool hasEqualData(const Student& other) const
    {
        // Порівнюємо дані (не адреси)
        return name == other.name && studentId == other.studentId;
    }

    void printInfo() const
    {
        cout << "Student: " << name << " (ID: " << studentId << ")\n";
    }
};

int main()
{
    Student s1("Ivan", 101);
    Student s2("Ivan", 101);  // Ті самі дані, але інший об'єкт
    Student& s3 = s1;         // Посилання на s1
    
    cout << "s1.isSameObject(s2): " << s1.isSameObject(s2) << "\n";  // 0 (false)
    cout << "s1.isSameObject(s3): " << s1.isSameObject(s3) << "\n";  // 1 (true)
    
    cout << "s1.hasEqualData(s2): " << s1.hasEqualData(s2) << "\n";  // 1 (true)
    
    return 0;
}
g++ Student.cpp -o student && ./student
$ ./student
s1.isSameObject(s2): 0
s1.isSameObject(s3): 1
s1.hasEqualData(s2): 1

Метод isSameObject використовує this для перевірки, чи це той самий об'єкт у пам'яті, а не просто об'єкт з однаковими даними.

Приклад 2: Повернення копії чи посилання

class Counter
{
private:
    int count;

public:
    Counter(int initial = 0) : count(initial) {}

    // Повертає посилання — дозволяє chaining
    Counter& increment()
    {
        count++;
        return *this;
    }

    // Повертає копію — не дозволяє chaining
    Counter incrementAndCopy()
    {
        count++;
        return *this;  // Створюється копія поточного об'єкта
    }

    int getCount() const
    {
        return count;
    }
};

int main()
{
    Counter c1(0);
    
    // Chaining з посиланням
    c1.increment().increment().increment();
    cout << "c1 count: " << c1.getCount() << "\n";  // 3
    
    Counter c2(0);
    
    // Без chaining — копія повертається
    Counter temp = c2.incrementAndCopy();
    cout << "c2 count: " << c2.getCount() << "\n";       // 1
    cout << "temp count: " << temp.getCount() << "\n";   // 1
    
    return 0;
}
Різниця між Counter& і Counter:
  • return *this; з типом Counter& — повертає посилання на поточний об'єкт (без копіювання)
  • return *this; з типом Counter — створює копію поточного об'єкта
У першому випадку можливий chaining, у другому — ні.

Приклад 3: Самоприсвоєння у перевантаженому операторі

При перевантаженні оператора присвоєння важливо перевіряти самоприсвоєння (коли об'єкт присвоюється сам собі):

class DynamicArray
{
private:
    int* data;
    int size;

public:
    DynamicArray(int sz) : size(sz)
    {
        data = new int[size];
    }

    ~DynamicArray()
    {
        delete[] data;
    }

    // Перевантажений оператор присвоєння
    DynamicArray& operator=(const DynamicArray& other)
    {
        // ✅ Перевірка самоприсвоєння через this
        if (this == &other)
        {
            cout << "Self-assignment detected, skipping\n";
            return *this;  // Нічого не робимо
        }

        // Звільняємо старі дані
        delete[] data;

        // Копіюємо нові дані
        size = other.size;
        data = new int[size];
        for (int i = 0; i < size; i++)
        {
            data[i] = other.data[i];
        }

        return *this;
    }

    int getSize() const { return size; }
};

int main()
{
    DynamicArray arr1(5);
    DynamicArray arr2(10);
    
    arr1 = arr2;  // Нормальне присвоєння
    arr1 = arr1;  // Самоприсвоєння — виявляється через this
    
    return 0;
}
Чому важлива перевірка самоприсвоєння?Без перевірки if (this == &other) у випадку arr1 = arr1;:
  1. Виконується delete[] data; — видаляємо дані arr1
  2. Потім намагаємося копіювати з other.data, але other — це той самий arr1, чиї дані щойно видалили!
  3. Результат: невизначена поведінка, ймовірно краш програми
Перевірка через this == &other запобігає цій проблемі.

Поширені помилки з this

Практичне завдання: система управління завданнями

Резюме

🔍 Що таке this

Прихований покажчик типу ClassName* const, що автоматично додається компілятором до кожного нестатичного методу класу. Містить адресу об'єкта, для якого викликано метод.

obj.method();
// this == &obj всередині method()

⚙️ Як працює this

Компілятор трансформує виклик методу:

// Ваш код
obj.method(arg);

// Компілятор перетворює на
method(&obj, arg);

// Метод отримує
void method(Class* const this, Type arg)

📌 Коли використовувати явно

  1. Конфлікт імен: параметр і поле мають однакові імена
  2. Передача об'єкта: потрібно передати адресу поточного об'єкта в іншу функцію
  3. Method chaining: return *this; для ланцюжків викликів
  4. Перевірка самоприсвоєння: if (this == &other)

✨ Method Chaining

Патерн, де метод повертає *this, дозволяючи викликати кілька методів підряд:

Calculator& add(int x) {
    value += x;
    return *this;
}

calc.add(5).sub(2).mul(3);

⚠️ Обмеження this

  • thisконстантний покажчик (неможливо змінити)
  • Недоступний у static методах
  • Значення змінюється при кожному виклику
  • Не можна видаляти (delete this) для об'єктів зі стеку

🎯 Практичні застосування

  • Fluent Interface: API з nature-подібним синтаксисом
  • Query Builders: побудова SQL/запитів
  • Stream operators: cout << x << y
  • Builder Pattern: покроковаконструювання об'єктів
Головне правило: У більшості випадків вам не потрібно писати this-> явно — компілятор робить це автоматично. Використовуйте this тільки там, де це необхідно або покращує читабельність коду.

У наступній статті ми розглянемо розділення класу на заголовковий файл та файл реалізації — як організувати код класів у великих проєктах!

Copyright © 2026