24.7 — Wywoływanie funkcji dziedziczonych i zachowanie przesłaniania

Domyślnie klasy pochodne dziedziczą wszystkie zachowania zdefiniowane w klasie bazowej. W tej lekcji przeanalizujemy bardziej szczegółowo, w jaki sposób wybierane są funkcje składowe, a także w jaki sposób możemy to wykorzystać do zmiany zachowań w klasie pochodnej.

Gdy funkcja składowa jest wywoływana na obiekcie klasy pochodnej, kompilator najpierw sprawdza, czy w klasie pochodnej istnieje jakakolwiek funkcja o tej nazwie. Jeśli tak, uwzględniane są wszystkie przeciążone funkcje o tej nazwie, a proces rozpoznawania przeciążenia funkcji służy do ustalenia, czy istnieje najlepsze dopasowanie. Jeśli nie, kompilator przechodzi w górę łańcucha dziedziczenia, sprawdzając po kolei każdą klasę nadrzędną w ten sam sposób.

Innymi słowy, kompilator wybierze najlepiej pasującą funkcję z klasy najczęściej pochodnej z co najmniej jedną funkcją o tej nazwie.

Wywoływanie funkcji klasy bazowej

Najpierw przyjrzyjmy się, co się dzieje, gdy klasa pochodna nie ma funkcji pasującej, ale klasę bazową robi:

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

Wypisuje:

Base::identify()
Base::identify()

Po wywołaniu base.identify() , kompilator sprawdza, czy funkcja o nazwie identify() została zdefiniowana w klasie Base. Tak, więc kompilator sprawdza, czy jest zgodny. Jest, tak się nazywa

Po wywołaniu derived.identify() , kompilator sprawdza, czy funkcja o nazwie identify() została zdefiniowana w klasie Derived . Tak nie jest. Zatem przechodzi do klasy nadrzędnej (w tym przypadku Base) i tam próbuje ponownie. Base zdefiniował funkcję identify() , więc jej używa. Innymi słowy, Base::identify() został użyty, ponieważ Derived::identify() nie istnieje.

Oznacza to, że jeśli zachowanie zapewniane przez klasę bazową jest wystarczające, możemy po prostu użyć zachowania klasy bazowej.

Redefinicja zachowań

Gdybyśmy jednak zdefiniowali Derived::identify() w klasie Derived , zamiast tego zostałby użyty.

Oznacza to, że możemy sprawić, że funkcje będą działać inaczej w przypadku naszych klas pochodnych, redefiniując je w klasie pochodnej!

Załóżmy na przykład, że chcemy derived.identify() print Derived::identify(). Możemy po prostu dodać funkcję identify() w klasie Derived , tak aby zwracała poprawną odpowiedź, gdy wywołamy funkcję identify() z Derived obiektem.

Aby zmodyfikować sposób, w jaki funkcja zdefiniowana w klasie bazowej działa w klasie pochodnej, po prostu przedefiniuj funkcję w klasie pochodnej.

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const { std::cout << "Derived::identify()\n"; }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

Wypisuje:

Base::identify()
Derived::identify()

Pamiętaj, że podczas redefiniowania funkcji w klasie pochodnej klasie pochodnej, funkcja pochodna nie dziedziczy specyfikatora dostępu funkcji o tej samej nazwie w klasie bazowej. Używa dowolnego specyfikatora dostępu, w którym jest zdefiniowany w klasie pochodnej. Dlatego funkcja zdefiniowana jako prywatna w klasie bazowej może zostać ponownie zdefiniowana jako publiczna w klasie pochodnej i odwrotnie!

#include <iostream>

class Base
{
private:
	void print() const 
	{
		std::cout << "Base";
	}
};
 
class Derived : public Base
{
public:
	void print() const 
	{
		std::cout << "Derived ";
	}
};
 
 
int main()
{
	Derived derived {};
	derived.print(); // calls derived::print(), which is public
	return 0;
}

Dodawanie do istniejącej funkcjonalności

Czasami nie chcemy całkowicie zastąpić funkcji klasy bazowej, ale zamiast tego chcemy dodać do niej dodatkową funkcjonalność, gdy zostanie wywołana z obiektem pochodnym. W powyższym przykładzie zwróć uwagę, że Derived::identify() całkowicie ukrywa Base::identify()! To może nie być to, czego chcemy. Możliwe jest, że nasza funkcja pochodna wywoła podstawową wersję funkcji o tej samej nazwie (w celu ponownego użycia kodu), a następnie dodała do niej dodatkową funkcjonalność.

Aby funkcja pochodna wywołała funkcję bazową o tej samej nazwie, po prostu wykonaj normalne wywołanie funkcji, ale poprzedź ją kwalifikatorem zasięgu klasy bazowej. Na przykład:

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const
    {
        std::cout << "Derived::identify()\n";
        Base::identify(); // note call to Base::identify() here
    }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

Wypisuje:

Base::identify()
Derived::identify()
Base::identify()

Po wywołaniu derived.identify() jest wykonywane, następuje Derived::identify(). Po wydrukowaniu Derived::identify() następnie wywołuje funkcję Base::identify(), która drukuje Base::identify().

To powinno być całkiem proste. Dlaczego musimy używać operatora rozpoznawania zakresu (::)? Gdybyśmy zdefiniowali Derived::identify() w ten sposób:

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const
    {
        std::cout << "Derived::identify()\n";
        identify(); // no scope resolution results in self-call and infinite recursion
    }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

Funkcja wywołania identify() bez kwalifikatora rozdzielczości zakresu domyślnie będzie wynosić identify() w bieżącej klasie, czyli Derived::identify(). Spowodowałoby to Derived::identify() wywołanie samego siebie, co doprowadziłoby do nieskończonej rekurencji!

Jest jedna trudność, na którą możemy natknąć się, próbując wywołać zaprzyjaźnione funkcje w klasach bazowych, takie jak operator<<. Ponieważ zaprzyjaźnione funkcje klasy bazowej w rzeczywistości nie są częścią klasy bazowej, użycie kwalifikatora rozdzielczości zakresu nie będzie działać. Zamiast tego potrzebujemy sposobu, aby nasza Derived klasa tymczasowo wyglądała jak klasa Base , tak aby można było wywołać odpowiednią wersję funkcji.

Na szczęście jest to łatwe do zrobienia, używając static_cast. Oto przykład:

#include <iostream>

class Base
{
public:
    Base() { }

	friend std::ostream& operator<< (std::ostream& out, const Base&)
	{
		out << "In Base\n";
		return out;
	}
};

class Derived: public Base
{
public:
    Derived() { }

 	friend std::ostream& operator<< (std::ostream& out, const Derived& d)
	{
		out << "In Derived\n";
		// static_cast Derived to a Base object, so we call the right version of operator<<
		out << static_cast<const Base&>(d); 
		return out;
    }
};

int main()
{
    Derived derived {};

    std::cout << derived << '\n';

    return 0;
}

Ponieważ Derived jest-Bazą, możemy static_cast nasz Derived obiekt przekształcić w Base odniesienie, dzięki czemu zostanie wywołana odpowiednia wersja operator<< która używa Base .

Wypisuje:

In Derived
In Base

Rozdzielczość przeciążenia w pochodnych klasy

Jak zauważono na początku lekcji, kompilator wybierze najlepiej pasującą funkcję z klasy najczęściej pochodnej z co najmniej jedną funkcją o tej nazwie.

Najpierw przyjrzyjmy się prostemu przypadkowi, w którym przeciążono funkcje składowe:

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
};


int main()
{
    Derived d{};
    d.print(5); // calls Base::print(int)

    return 0;
}

W przypadku wywołania d.print(5) kompilator nie znalazł funkcji o nazwie print() w Derived, więc sprawdza Base gdzie znajduje dwie funkcje o tej nazwie. Używa procesu rozpoznawania przeciążenia funkcji, aby określić, czy Base::print(int) jest lepszym dopasowaniem niż Base::print(double). Dlatego Base::print(int) zostaje wywołany, tak jak byśmy się tego spodziewali.

Przyjrzyjmy się teraz przypadkowi, który nie zachowuje się tak, jak byśmy się tego spodziewali:

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    void print(double) { std::cout << "Derived::print(double)"; } // this function added
};


int main()
{
    Derived d{};
    d.print(5); // calls Derived::print(double), not Base::print(int)

    return 0;
}

W przypadku wywołania d.print(5), kompilator znajduje jedną funkcję o nazwie print() w Derived, dlatego będzie brał pod uwagę tylko funkcje w Derived przy próbie określenia, do której funkcji należy rozwiązać. Ta funkcja jest również najlepiej pasującą funkcją w Derived dla tego wywołania funkcji. Dlatego to wywołuje Derived::print(double).

Ponieważ Base::print(int) ma parametr, który lepiej pasuje do argumentu int 5 niż Derived::print(double), mogłeś się spodziewać, że to wywołanie funkcji zostanie rozwiązane na Base::print(int). Ale ponieważ d jest Derived, istnieje co najmniej jedna print() funkcja w Derived, I Derived jest bardziej pochodna niż Base, funkcje w Base nie są nawet brane pod uwagę.

A co jeśli faktycznie chcemy d.print(5) rozwiązać Base::print(int)? Jednym z niezbyt dobrych sposobów jest zdefiniowanie Derived::print(int):

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    void print(int n) { Base::print(n); } // works but not great, as we have to define 
    void print(double) { std::cout << "Derived::print(double)"; }
};

int main()
{
    Derived d{};
    d.print(5); // calls Derived::print(int), which calls Base::print(int)

    return 0;
}

Chociaż to działa, nie jest to świetne rozwiązanie, ponieważ musimy dodać funkcję Derived dla każdego przeciążenia, w które chcemy wpaść Base. Może to być wiele dodatkowych funkcji, które w zasadzie kierują wywołania do Base.

Lepszą opcją jest użycie deklaracji using w Derived aby wszystkie Base funkcje o określonej nazwie były widoczne od wewnątrz Derived:

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    using Base::print; // make all Base::print() functions eligible for overload resolution
    void print(double) { std::cout << "Derived::print(double)"; }
};


int main()
{
    Derived d{};
    d.print(5); // calls Base::print(int), which is the best matching function visible in Derived

    return 0;
}

Umieszczając deklarację using using Base::print; wewnątrz Derived, mówimy kompilatorowi, że wszystkie Base funkcje o nazwie print powinny być widoczne w Derived, co spowoduje, że będą kwalifikować się do rozwiązania problemu przeciążenia. W efekcie Base::print(int) wybrano Derived::print(double).

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
90 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze