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).

