Przez większą część tego i ostatniego rozdziału głosiliśmy zalety dostępu elementy sterujące, które zapewniają mechanizm kontrolowania tego, kto może uzyskać dostęp do różnych członków klasy. Dostęp do członków prywatnych mają tylko inni członkowie klasy, a do członków publicznych mają dostęp wszyscy. W lekcji 14.6 — Funkcje dostępu omówiliśmy korzyści wynikające z utrzymywania prywatności danych i tworzenia publicznego interfejsu, z którego mogą korzystać osoby niebędące członkami.
Są jednak sytuacje, w których taki układ jest niewystarczający lub nie jest idealny.
Rozważmy na przykład klasę pamięci masowej, która koncentruje się na zarządzaniu pewnym zestawem danych. Powiedzmy teraz, że chcesz również wyświetlić te dane, ale kod obsługujący wyświetlanie będzie miał wiele opcji i dlatego jest złożony. Można umieścić zarówno funkcje zarządzania pamięcią masową, jak i funkcje zarządzania wyświetlaniem w tej samej klasie, ale spowodowałoby to bałagan i spowodowałoby skomplikowany interfejs. Można je także rozdzielić: klasa pamięci zarządza pamięcią masową, a inna klasa wyświetlania zarządza wszystkimi możliwościami wyświetlania. To tworzy przyjemny podział odpowiedzialności. Jednak klasa wyświetlania nie byłaby wówczas w stanie uzyskać dostępu do prywatnych składowych klasy pamięci i mogłaby nie być w stanie wykonać swojego zadania.
Alternatywnie istnieją przypadki, w których składniowo wolelibyśmy użyć funkcji niebędącej składową zamiast funkcji składowej (pokażemy przykład poniżej). Dzieje się tak często w przypadku przeciążania operatorów – temat ten omówimy w przyszłych lekcjach. Ale funkcje niebędące składowymi mają ten sam problem — nie mogą uzyskać dostępu do prywatnych składowych klasy.
Jeśli funkcje dostępu (lub inne publiczne funkcje składowe) już istnieją i są wystarczające dla dowolnej możliwości, którą próbujemy zaimplementować, to świetnie — możemy (i powinniśmy) po prostu z nich skorzystać. Ale w niektórych przypadkach te funkcje nie istnieją. Co wtedy?
Jedną z opcji byłoby dodanie do klasy nowych funkcji składowych, aby umożliwić innym klasom lub funkcjom niebędącym członkami wykonywanie zadań, których w innym przypadku nie byłyby w stanie wykonać. Ale możemy nie chcieć zezwalać na publiczny dostęp do takich rzeczy - być może te rzeczy w dużym stopniu zależą od implementacji lub są podatne na niewłaściwe użycie.
Naprawdę potrzebujemy jakiegoś sposobu na obalenie systemu kontroli dostępu w każdym przypadku.
Przyjaźń to magia
Odpowiedzią na nasze wyzwanie jest przyjaźń.
W treści klasy deklaracja znajomego (za pomocą słowa kluczowego friend słowo kluczowe) może zostać użyte, aby poinformować kompilator, że jakaś inna klasa lub funkcja jest teraz klasą lub funkcją przyjaciel. W C++ a przyjaciel to klasa lub funkcja (członkowa lub niebędąca członkiem), której przyznano pełny dostęp do prywatnych i chronionych elementów innej klasy. W ten sposób klasa może selektywnie przyznać innym klasom lub funkcjom pełny dostęp do swoich członków, nie wpływając na nic innego.
Kluczowa informacja
Przyjaźń jest zawsze przyznawana przez klasę, do której członków będzie dostępny (nie przez klasę lub funkcję pragnącą dostępu). Pomiędzy kontrolą dostępu a nadawaniem przyjaźni, klasa zawsze zachowuje możliwość kontrolowania tego, kto może uzyskać dostęp do jej członków.
Na przykład, jeśli nasza klasa pamięci masowej uczyniła klasę wyświetlającą zaprzyjaźnioną, wówczas klasa wyświetlająca będzie mogła uzyskać bezpośredni dostęp do wszystkich członków klasy pamięci. Klasa wyświetlania może wykorzystać ten bezpośredni dostęp do zaimplementowania wyświetlania klasy pamięci, pozostając jednocześnie strukturalnie odrębna.
Kontrola dostępu nie ma wpływu na deklarację znajomego, więc nie ma znaczenia, gdzie w treści klasy zostanie umieszczona.
Teraz, gdy wiemy, czym jest przyjaciel, przyjrzyjmy się konkretnym przykładom, w których przyjaźń jest przyznawana funkcjom niebędącym członkami, funkcjom członkowskim i innym klasom. W tej lekcji omówimy funkcje znajomych niebędących członkami, a następnie w następnej lekcji przyjrzymy się klasom znajomych i funkcjom członków znajomych. 15.9 — Klasy znajomych i funkcje członków znajomych.
Przyjazne funkcje niebędące członkami
A Funkcja zaprzyjaźniona to funkcja (członkowa lub niebędąca członkiem), która może uzyskać dostęp do prywatnych i chronionych członków klasy tak, jakby była członkiem tej klasy. Pod każdym innym względem funkcja zaprzyjaźniona jest funkcją normalną.
Przyjrzyjmy się przykładowi prostej klasy, która czyni funkcję niebędącą członkiem zaprzyjaźnioną:
#include <iostream>
class Accumulator
{
private:
int m_value { 0 };
public:
void add(int value) { m_value += value; }
// Here is the friend declaration that makes non-member function void print(const Accumulator& accumulator) a friend of Accumulator
friend void print(const Accumulator& accumulator);
};
void print(const Accumulator& accumulator)
{
// Because print() is a friend of Accumulator
// it can access the private members of Accumulator
std::cout << accumulator.m_value;
}
int main()
{
Accumulator acc{};
acc.add(5); // add 5 to the accumulator
print(acc); // call the print() non-member function
return 0;
}W tym przykładzie zadeklarowaliśmy funkcję niebędącą członkiem o nazwie print() która przyjmuje obiekt klasy Accumulator. Ponieważ print() niebędący członkiem klasy Accumulator, normalnie nie miałaby dostępu do prywatnego członek m_value. Jednakże klasa Accumulator zawiera deklarację znajomego tworzącą print(const Accumulator& accumulator) przyjaciela, jest to teraz dozwolone.
Zauważ, że ponieważ print() jest funkcją niebędącą składową (a zatem nie ma ukrytego obiektu), musimy jawnie przekazać Accumulator obiekt do print() , z którym chcemy pracować.
Definiowanie znajomego niebędącego członkiem wewnątrz klasy
Podobnie jak funkcje składowe można definiować wewnątrz klasy, jeśli jest to pożądane, tak samo zaprzyjaźnione funkcje niebędące członkami można również definiować wewnątrz klasy. Poniższy przykład definiuje przyjazną funkcję niebędącą członkiem print() wewnątrz klasy Accumulator klasę:
#include <iostream>
class Accumulator
{
private:
int m_value { 0 };
public:
void add(int value) { m_value += value; }
// Friend functions defined inside a class are non-member functions
friend void print(const Accumulator& accumulator)
{
// Because print() is a friend of Accumulator
// it can access the private members of Accumulator
std::cout << accumulator.m_value;
}
};
int main()
{
Accumulator acc{};
acc.add(5); // add 5 to the accumulator
print(acc); // call the print() non-member function
return 0;
}Chociaż można założyć, że ponieważ print() jest zdefiniowana wewnątrz Accumulator, czyni to print() członkiem Accumulator, ale tak nie jest. Ponieważ print() jest zdefiniowana jako zaprzyjaźniona, zamiast tego jest traktowana jako funkcja niebędąca składową (tak jakby została zdefiniowana na zewnątrz Accumulator).
Składniowo preferujemy zaprzyjaźnioną funkcję niebędącą składową
We wstępie do tej lekcji wspomnieliśmy, że czasami wolelibyśmy użyć funkcji niebędącej składową niż funkcji składowej. Pokażmy teraz przykład.
#include <iostream>
class Value
{
private:
int m_value{};
public:
explicit Value(int v): m_value { v } { }
bool isEqualToMember(const Value& v) const;
friend bool isEqualToNonmember(const Value& v1, const Value& v2);
};
bool Value::isEqualToMember(const Value& v) const
{
return m_value == v.m_value;
}
bool isEqualToNonmember(const Value& v1, const Value& v2)
{
return v1.m_value == v2.m_value;
}
int main()
{
Value v1 { 5 };
Value v2 { 6 };
std::cout << v1.isEqualToMember(v2) << '\n';
std::cout << isEqualToNonmember(v1, v2) << '\n';
return 0;
}W w tym przykładzie zdefiniowaliśmy dwie podobne funkcje, które sprawdzają, czy dwa Value obiekty są równe. isEqualToMember() jest funkcją składową, a isEqualToNonmember() jest funkcją niebędącą składową.Skupmy się na tym, jak te funkcje są zdefiniowane.
W isEqualToMember(), przekazujemy jeden obiekt w sposób niejawny, a drugi jawnie który m_value należy do ukrytego obiektu, podczas gdy v.m_value należy do jawnego parametru.
W isEqualToNonmember(), oba obiekty są przekazywane jawnie. Prowadzi to do lepszej równoległości w implementacji funkcji, ponieważ element m_value jest zawsze jawnie poprzedzony jawnym parametrem.
Nadal możesz preferować składnię wywołania v1.isEqualToMember(v2) za isEqualToNonmember(v1, v2). Ale kiedy omówimy przeciążenie operatorów, ten temat pojawi się ponownie.
Wielu przyjaciół
Funkcja może być jednocześnie przyjacielem więcej niż jednej klasy. Rozważmy na przykład następujący przykład:
#include <iostream>
class Humidity; // forward declaration of Humidity
class Temperature
{
private:
int m_temp { 0 };
public:
explicit Temperature(int temp) : m_temp { temp } { }
friend void printWeather(const Temperature& temperature, const Humidity& humidity); // forward declaration needed for this line
};
class Humidity
{
private:
int m_humidity { 0 };
public:
explicit Humidity(int humidity) : m_humidity { humidity } { }
friend void printWeather(const Temperature& temperature, const Humidity& humidity);
};
void printWeather(const Temperature& temperature, const Humidity& humidity)
{
std::cout << "The temperature is " << temperature.m_temp <<
" and the humidity is " << humidity.m_humidity << '\n';
}
int main()
{
Humidity hum { 10 };
Temperature temp { 12 };
printWeather(temp, hum);
return 0;
}W tym przykładzie warto zwrócić uwagę na trzy rzeczy. Po pierwsze, ponieważ printWeather() używa obu Humidity i Temperature jednakowo, tak naprawdę nie jest to prawdą. ma sens, aby była członkiem którejkolwiek. Funkcja niebędąca członkiem działa lepiej. Po drugie, ponieważ printWeather() jest przyjacielem obu Humidity i Temperature, może uzyskać dostęp do prywatnych danych z obiektów obu klas.Na koniec zwróć uwagę na następujący wiersz na górze przykładu:
class Humidity;To jest deklaracja forward dla class Humidity identyfikator, który zostanie zdefiniowany później. Jednak w przeciwieństwie do funkcji, klasy nie mają zwracanych typów ani parametrów, zatem deklaracje przekazywania klas są zawsze po prostu class ClassName (chyba że są szablonami klas).
Bez tej linii kompilator powiedziałby nam, że nie wie, czym jest Humidity podczas analizowania deklaracji znajomego wewnątrz Temperature.
Czy przyjaźń nie narusza zasady ukrywania danych?
Nie. Przyjaźń jest przyznawana przez klasę ukrywającą dane w oczekiwaniu, że znajomy uzyska dostęp do swoich prywatnych członków. Pomyśl o przyjacielu jako o przedłużeniu samej klasy, z tymi samymi prawami dostępu. W związku z tym oczekuje się dostępu, a nie naruszenia.
Właściwe użycie przyjaźni może sprawić, że program będzie łatwiejszy w utrzymaniu, umożliwiając oddzielenie funkcjonalności, gdy ma to sens z punktu widzenia projektu (w przeciwieństwie do konieczności utrzymywania ich razem ze względu na kontrolę dostępu). Lub gdy bardziej sensowne jest użycie funkcji niebędącej członkiem zamiast funkcji członkowskiej.
Jednakże, ponieważ znajomi mają bezpośredni dostęp do implementacji klasy, zmiany w implementacji klasy zazwyczaj będą wymagały wprowadzenia zmian także u znajomych. Jeśli klasa ma wielu przyjaciół (lub ci znajomi mają przyjaciół), może to wywołać efekt domina.
Wdrażając funkcję znajomego, jeśli to możliwe, preferuj korzystanie z interfejsu publicznego zamiast bezpośredniego dostępu do członków. Pomoże to odizolować funkcję zaprzyjaźnioną od przyszłych zmian w implementacji i spowoduje, że mniej kodu będzie trzeba modyfikować i/lub ponownie testować w późniejszym czasie.
Najlepsza praktyka
Funkcja zaprzyjaźniona powinna preferować korzystanie z interfejsu klasy zamiast bezpośredniego dostępu, gdy tylko jest to możliwe.
Preferuj funkcje niebędące przyjaciółmi od funkcji zaprzyjaźnionych
W lekcji 14.8 — Korzyści z ukrywania danych (hermetyzowania), wspomnieliśmy, że powinniśmy preferować funkcje niebędące członkami zamiast funkcji składowych. Z tych samych powodów, które tam podano, powinniśmy preferować funkcje inne niż zaprzyjaźnione.
Na przykład w poniższym przykładzie, jeśli implementacja Accumulator zostanie zmieniona (np. zmienimy nazwę m_value), implementacja print() będzie musiała zostać zmieniona również:
#include <iostream>
class Accumulator
{
private:
int m_value { 0 }; // if we rename this
public:
void add(int value) { m_value += value; } // we need to modify this
friend void print(const Accumulator& accumulator);
};
void print(const Accumulator& accumulator)
{
std::cout << accumulator.m_value; // and we need to modify this
}
int main()
{
Accumulator acc{};
acc.add(5); // add 5 to the accumulator
print(acc); // call the print() non-member function
return 0;
}Lepszy pomysł jest następujący:
#include <iostream>
class Accumulator
{
private:
int m_value { 0 };
public:
void add(int value) { m_value += value; }
int value() const { return m_value; } // added this reasonable access function
};
void print(const Accumulator& accumulator) // no longer a friend of Accumulator
{
std::cout << accumulator.value(); // use access function instead of direct access
}
int main()
{
Accumulator acc{};
acc.add(5); // add 5 to the accumulator
print(acc); // call the print() non-member function
return 0;
}W tym przykładzie print() wykorzystuje dostęp funkcja value() aby uzyskać wartość m_value zamiast bezpośredniego dostępu m_value . Teraz, jeśli implementacja Accumulator ulegnie kiedykolwiek zmianie, print() nie będzie wymagana aktualizacja.
Najlepsza praktyka
Wolę zaimplementować funkcję jako nie-przyjaciel, jeśli to możliwe i rozsądne.
Zachowaj ostrożność podczas dodawania nowych członków do publicznego interfejsu istniejącej klasy, ponieważ każda funkcja (nawet trywialna) dodaje pewien poziom bałaganu i złożoności. W przypadku Accumulator powyżej całkowicie uzasadnione jest posiadanie funkcji dostępu umożliwiającej uzyskanie aktualnej zakumulowanej wartości. W bardziej złożonych przypadkach lepszym rozwiązaniem może być użycie przyjaźni zamiast dodawania wielu nowych funkcji dostępu do interfejsu klasy.

