Jedno z pytań dotyczących klas, które często zadają nowi programiści, brzmi: „Kiedy wywoływana jest funkcja składowa, w jaki sposób C++ śledzi, do jakiego obiektu została wywołana?”.
Po pierwsze, zdefiniujmy prostą klasę do pracy. Ta klasa hermetyzuje wartość całkowitą i udostępnia pewne funkcje dostępu umożliwiające pobranie i ustawienie tej wartości:
#include <iostream>
class Simple
{
private:
int m_id{};
public:
Simple(int id)
: m_id{ id }
{
}
int getID() const { return m_id; }
void setID(int id) { m_id = id; }
void print() const { std::cout << m_id; }
};
int main()
{
Simple simple{1};
simple.setID(2);
simple.print();
return 0;
}Jak można się spodziewać, ten program generuje wynik:
2
W jakiś sposób, kiedy wywołujemy simple.setID(2);, C++ wie, że funkcja setID() powinna działać na obiekcie simple i że m_id w rzeczywistości odnosi się to simple.m_id.
Odpowiedź jest taka, że C++ wykorzystuje ukryty wskaźnik o nazwie this! Podczas tej lekcji przyjrzymy się this bardziej szczegółowo.
Ukryte this wskaźnik
Wewnątrz każdej funkcji składowej słowo kluczowe this jest stałym wskaźnikiem przechowującym adres bieżącego ukrytego obiektu.
W większości przypadków nie wspominamy this jawnie, ale tylko po to, aby udowodnić, że can:
#include <iostream>
class Simple
{
private:
int m_id{};
public:
Simple(int id)
: m_id{ id }
{
}
int getID() const { return m_id; }
void setID(int id) { m_id = id; }
void print() const { std::cout << this->m_id; } // use `this` pointer to access the implicit object and operator-> to select member m_id
};
int main()
{
Simple simple{ 1 };
simple.setID(2);
simple.print();
return 0;
}Działa to identycznie jak w poprzednim przykładzie i wypisuje:
2
Należy pamiętać, że print() funkcje składowe z dwóch poprzednich przykładów robią dokładnie to samo:
void print() const { std::cout << m_id; } // implicit use of this
void print() const { std::cout << this->m_id; } // explicit use of thisOkazuje się, że pierwsza jest skrótem od drugiej. Kiedy kompilujemy nasze programy, kompilator domyślnie poprzedza każdy element członkowski odwołujący się do ukrytego obiektu za pomocą this->. Pomaga to zachować zwięzłość naszego kodu i zapobiega konieczności jawnego pisania this-> w nadmiarze.
Przypomnienie
Używamy -> aby wybrać element ze wskaźnika do obiektu. this->m_id jest odpowiednikiem (*this).m_id.
Omawiamy operator-> w lekcji 13.12 -- Wybór elementu za pomocą wskaźników i referencji.
Jak is this set?
Przyjrzyjmy się bliżej temu wywołaniu funkcji:
simple.setID(2);Chociaż wywołanie funkcji setID(2) wygląda na to, że ma tylko jeden argument, w rzeczywistości ma dwa! Po skompilowaniu kompilator przepisuje wyrażenie simple.setID(2); w następujący sposób:
Simple::setID(&simple, 2); // note that simple has been changed from an object prefix to a function argument!Zauważ, że jest to teraz tylko standardowe wywołanie funkcji, a obiekt simple (który poprzednio był przedrostkiem obiektu) jest teraz przekazywany przez adres jako argument do funkcji.
Ale to tylko połowa odpowiedzi. Ponieważ wywołanie funkcji ma teraz dodany argument, definicję funkcji składowej również należy zmodyfikować, aby akceptowała (i używała) ten argument jako parametr. Oto nasza oryginalna definicja funkcji składowej dla setID():
void setID(int id) { m_id = id; }Sposób, w jaki kompilator przepisuje funkcje, jest szczegółem specyficznym dla implementacji, ale wynik końcowy jest mniej więcej taki:
static void setID(Simple* const this, int id) { this->m_id = id; }Zauważ, że nasza setId funkcja ma nowy parametr położony skrajnie po lewej stronie o nazwie this, który jest stałym wskaźnikiem (co oznacza, że nie można go ponownie wskazać, ale zawartość wskaźnika można modyfikować). Składnik m_id został również przepisany na this->m_id przy użyciu wskaźnika this .
Dla zaawansowanych czytelników
W tym kontekście słowo kluczowe static oznacza, że funkcja nie jest powiązana z obiektami klasy, lecz jest traktowana tak, jakby była normalną funkcją wewnątrz obszaru zasięgu klasy. Statyczne funkcje składowe omówimy w lekcji 15.7 -- Statyczne funkcje składowe.
Składając wszystko w całość:
- Kiedy wywołujemy
simple.setID(2), kompilator faktycznie wywołujeSimple::setID(&simple, 2), Isimplei jest przekazywany przez adres do funkcji. - Funkcja ma ukryty parametr o nazwie
this, który otrzymuje adressimple. - Członek zmienne wewnątrz setID() są poprzedzone
this->, co wskazuje nasimple. Zatem kiedy kompilator oceniathis->m_id, w rzeczywistości podejmuje decyzję osimple.m_id.
Dobrą wiadomością jest to, że wszystko dzieje się automatycznie i nie ma tak naprawdę znaczenia, czy pamiętasz, jak to działa, czy nie. Wszystko, co musisz pamiętać, to to, że wszystkie niestatyczne funkcje składowe mają this wskaźnik odnoszący się do obiektu, na którym funkcja została wywołana.
Kluczowa informacja
Wszystkie niestatyczne funkcje składowe mają this wskaźnik const, który przechowuje adres ukrytego obiektu.
this zawsze wskazuje obiekt, na którym się operuje
Nowi programiści są czasami zdezorientowani co do liczby this istniejących wskaźników. Każda funkcja członkowska ma pojedynczy this parametr wskaźnikowy, który wskazuje na niejawny obiekt. Rozważ:
int main()
{
Simple a{1}; // this = &a inside the Simple constructor
Simple b{2}; // this = &b inside the Simple constructor
a.setID(3); // this = &a inside member function setID()
b.setID(4); // this = &b inside member function setID()
return 0;
}Należy pamiętać, że this wskaźnik na przemian przechowuje adres obiektu a lub b w zależności od tego, czy wywołaliśmy funkcję składową obiektu a lub b.
Ponieważ this jest tylko parametrem funkcji (a nie elementem składowym), nie powoduje to, że instancje twojej klasy są większe pod względem pamięci.
Jawne odwoływanie się this
W większości przypadków nie musisz jawnie odwoływać się wskaźnik this . Jednakże jest kilka sytuacji, w których może to być przydatne:
Po pierwsze, jeśli masz funkcję składową, która ma parametr o tej samej nazwie co element danych, możesz je ujednoznacznić, używając this:
struct Something
{
int data{}; // not using m_ prefix because this is a struct
void setData(int data)
{
this->data = data; // this->data is the member, data is the local parameter
}
};This Something klasa ma element członkowski o nazwie data. Parametr funkcji setData() jest również nazywany data. Wewnątrz setData() funkcji data odnosi się do parametru funkcji (ponieważ parametr funkcji przesłania element danych), więc jeśli chcemy odwołać się do elementu data , używamy this->data.
Niektórzy programiści wolą jawnie dodawać this-> do wszystkich elementów klasy, aby było jasne, że odwołują się do elementu członkowskiego. Zalecamy, aby tego unikać, ponieważ powoduje to, że kod jest mniej czytelny i przynosi niewielkie korzyści. Użycie przedrostka „m_” jest bardziej zwięzłym sposobem na odróżnienie prywatnych zmiennych składowych od zmiennych niebędących składowymi (lokalnymi).
Zwracanie *this
Po drugie, czasami przydatne może być, aby funkcja składowa zwracała ukryty obiekt jako wartość zwracaną. Głównym powodem takiego działania jest umożliwienie „połączenia” funkcji składowych, dzięki czemu można wywołać kilka funkcji składowych na tym samym obiekcie w jednym wyrażeniu! Nazywa się to łączeniem funkcji (lub łączenie metod).
Rozważmy typowy przykład, w którym wyprowadzasz kilka bitów tekstu za pomocą std::cout:
std::cout << "Hello, " << userName;Kompilator ocenia powyższy fragment w następujący sposób:
(std::cout << "Hello, ") << userName;Najpierw operator<< używa std::cout i literał ciągu "Hello, " print "Hello, " do konsoli. Ponieważ jednak jest to część wyrażenia, operator<< musi również zwrócić wartość (lub void). Jeśli operator<< zwrócone void, skończyłoby się to jako częściowo ocenione wyrażenie:
void{} << userName;co wyraźnie nie ma żadnego sensu (a kompilator wyrzuciłby błąd). Zamiast tego operator<< zwraca przekazany obiekt strumienia, którym w tym przypadku jest std::cout. W ten sposób po obliczeniu pierwszego operator<< otrzymujemy:
(std::cout) << userName;co następnie wypisuje nazwę użytkownika.
W ten sposób musimy określić tylko std::cout raz, a następnie możemy połączyć ze sobą dowolną liczbę fragmentów tekstu za pomocą operator<< jak chcemy. Każde wywołanie operator<< zwraca std::cout a więc kolejne wywołanie to operator<< używa std::cout jako lewy operand.
Możemy zaimplementować tego rodzaju zachowanie również w naszych funkcjach składowych. Rozważmy następującą klasę:
class Calc
{
private:
int m_value{};
public:
void add(int value) { m_value += value; }
void sub(int value) { m_value -= value; }
void mult(int value) { m_value *= value; }
int getValue() const { return m_value; }
};Jeśli chcesz dodać 5, odjąć 3 i pomnożyć przez 4, musisz to zrobić w następujący sposób:
#include <iostream>
int main()
{
Calc calc{};
calc.add(5); // returns void
calc.sub(3); // returns void
calc.mult(4); // returns void
std::cout << calc.getValue() << '\n';
return 0;
}Jeśli jednak zwrócimy każdą funkcję *this przez referencję, możemy połączyć wywołania w łańcuch Oto nowa wersja Calc z funkcjami „łańcuchowymi”:
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() const { return m_value; }
};Zauważ to add(), sub() i mult() zwracają się *this przez odniesienie. W związku z tym możemy wykonać następujące czynności:
#include <iostream>
int main()
{
Calc calc{};
calc.add(5).sub(3).mult(4); // method chaining
std::cout << calc.getValue() << '\n';
return 0;
}Efektywnie skondensowaliśmy trzy linie w jedno wyrażenie! Przyjrzyjmy się bliżej, jak to działa.
Najpierw calc.add(5) jest wywoływane, co dodaje 5 Do m_value. add() następnie zwraca. odniesienie do *this, które jest odniesieniem do ukrytego obiektu calc, więc calc będzie obiektem używanym w późniejszej ocenie. Następnie calc.sub(3) ocenia, co odejmuje 3 z m_value i ponownie zwraca calcNa koniec calc.mult(4) mnoży m_value przez 4 i zwraca calc, co nie jest dalej używany i dlatego jest ignorowany.
Ponieważ każda funkcja została zmodyfikowana calc w trakcie jej wykonywania, m_value z calc zawiera teraz wartość (((0 + 5) - 3) * 4), która wynosi 8.
Jest to prawdopodobnie najczęstsze jawne użycie this i należy go wziąć pod uwagę za każdym razem, gdy sensowne jest posiadanie funkcji składowych, które można łączyć w łańcuch.
Ponieważ this zawsze wskazuje na ukryty obiekt, nie musimy sprawdzać, czy jest to wskaźnik zerowy przed wyłuszczeniem go.
Resetowanie klasy z powrotem do stanu domyślnego
Jeśli Twoja klasa ma domyślny konstruktor, możesz być zainteresowany zapewnieniem sposobu na przywrócenie domyślnego obiektu istniejącego obiektu stan.
Jak zauważono w poprzednich lekcjach (14.12 -- Delegowanie konstruktorów), konstruktory służą wyłącznie do inicjalizacji nowych obiektów i nie powinny być wywoływane bezpośrednio. Spowoduje to nieoczekiwane zachowanie.
Najlepszym sposobem przywrócenia klasy do stanu domyślnego jest utworzenie reset() funkcji składowej, utworzenie przez tę funkcję nowego obiektu (przy użyciu konstruktora domyślnego), a następnie przypisanie tego nowego obiektu do bieżącego obiektu ukrytego w następujący sposób:
void reset()
{
*this = {}; // value initialize a new object and overwrite the implicit object
}Oto pełny program demonstrujący tę reset() funkcję w działaniu:
#include <iostream>
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() const { return m_value; }
void reset() { *this = {}; }
};
int main()
{
Calc calc{};
calc.add(5).sub(3).mult(4);
std::cout << calc.getValue() << '\n'; // prints 8
calc.reset();
std::cout << calc.getValue() << '\n'; // prints 0
return 0;
}this oraz const obiektów
W przypadku funkcji składowych niebędących stałymi, this jest wskaźnikiem const do wartości innej niż stała (co oznacza, że this nie można wskazać czegoś innego, ale obiekt wskazujący może zostać zmodyfikowany). W przypadku funkcji składowych const this jest wskaźnikiem const do wartości const (co oznacza, że wskaźnika nie można wskazać na coś innego ani nie można wskazywać obiektu, który ma być modyfikowany).
Błędy generowane podczas próby wywołania funkcji składowej innej niż const na obiekcie const mogą być nieco tajemnicze:
error C2662: 'int Something::getValue(void)': cannot convert 'this' pointer from 'const Something' to 'Something &' error: passing 'const Something' as 'this' argument discards qualifiers [-fpermissive]
Kiedy wywołujemy funkcję składową inną niż const w obiekcie const, niejawny this parametr funkcji jest stałym wskaźnikiem do obiektu innego niż stały . Ale argument ma wskaźnik typu const do obiektu const . Konwersja wskaźnika na obiekt const na wskaźnik do obiektu innego niż const wymaga odrzucenia kwalifikatora const, czego nie można zrobić w sposób dorozumiany. Błąd kompilatora generowany przez niektóre kompilatory odzwierciedla narzekanie kompilatora na monit o wykonanie takiej konwersji.
Dlaczego this wskaźnik, a nie referencja
Ponieważ this wskaźnik zawsze wskazuje na ukryty obiekt (i nigdy nie może być wskaźnikiem zerowym, chyba że zrobiliśmy coś, co spowodowałoby niezdefiniowane zachowanie), możesz się zastanawiać, dlaczego this jest wskaźnikiem zamiast referencji. Odpowiedź jest prosta: kiedy this dodano do C++, referencji jeszcze nie było.
Jeśli this dziś dodano do języka C++, niewątpliwie byłaby to referencja, a nie wskaźnik. W innych, bardziej nowoczesnych językach podobnych do C++, takich jak Java i C#, this jest zaimplementowany jako odniesienie.

