Klasy znajomych
Klasa przyjaciół to klasa, która ma dostęp do prywatnych i chronionych członków innej klasy.
Oto przykład:
#include <iostream>
class Storage
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{ }
// Uczyń klasę Display przyjacielem Storage
friend class Display;
};
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
// Ponieważ Display jest przyjacielem Storage, członkowie Display mogą uzyskać dostęp do prywatnych członków Storage
void displayStorage(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // najpierw wyświetl podwójnie
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
void setDisplayIntFirst(bool b)
{
m_displayIntFirst = b;
}
};
int main()
{
Storage storage { 5, 6.7 };
Display display { false };
display.displayStorage(storage);
display.setDisplayIntFirst(true);
display.displayStorage(storage);
return 0;
}Ponieważ Display klasa jest przyjacielem Storage, Display członkowie mogą uzyskać dostęp do prywatnych członków dowolnego Storage obiektu, do którego mają dostęp.
Ten program daje następujący wynik:
6.7 5 5 6.7
Kilka dodatkowych uwag na temat klas znajomych.
Po pierwsze, mimo że Display jest przyjacielem Storage, Display nie ma dostępu do *this wskaźnik Storage obiektów (ponieważ *this jest w rzeczywistości parametrem funkcji).
Po drugie, przyjaźń nie jest wzajemna. To, że Display jest przyjacielem Storage nie oznacza, Storage jest także przyjacielem Display. Jeśli chcesz, aby dwie klasy były dla siebie przyjaciółmi, obie muszą zadeklarować drugą jako przyjaciela.
Nota autora
Przepraszam, jeśli ta klasa jest dla Ciebie zbyt bliska!
Przyjaźń klasowa również nie jest przechodnia. Jeśli klasa A jest przyjacielem B, a B jest przyjacielem C, nie oznacza to, że A jest przyjacielem C.
Dla zaawansowanych czytelników
Przyjaźń też nie jest dziedziczona. Jeśli klasa A czyni B przyjacielem, klasy wywodzące się z B nie są przyjaciółmi A.
Deklaracja klasy zaprzyjaźnionej działa jak deklaracja forward dla zaprzyjaźnionej klasy. Oznacza to, że nie musimy przekazywać dalej deklaracji, że klasa jest zaprzyjaźniona przed dodaniem jej do grona znajomych. W powyższym przykładzie friend class Display działa zarówno jako deklaracja forward Display i deklaracja znajomego.
Funkcje członka znajomego
Zamiast czynić przyjaciela całą klasą, możesz uczynić jednego członka członkiem funkcją przyjaciela. Odbywa się to podobnie do zaprzyjaźniania funkcji niebędącej członkiem, z tą różnicą, że zamiast niej używana jest nazwa funkcji członkowskiej.
Jednak w rzeczywistości może to być trochę trudniejsze, niż oczekiwano. Przekształćmy poprzedni przykład, aby utworzyć Display::displayStorage przyjacielską funkcję członkowską. Możesz spróbować czegoś takiego:
#include <iostream>
class Display; // deklaracja forward dla klasy Display
class Storage
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{
}
// Uczyń Display::displayStorage Funkcja składowa zaprzyjaźniona z klasą Storage
friend void Display::displayStorage(const Storage& storage); // błąd: Pamięć nie widziała pełnej definicji klasy Display
};
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
void displayStorage(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // najpierw wyświetl podwójnie
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
};
int main()
{
Storage storage { 5, 6.7 };
Display display { false };
display.displayStorage(storage);
return 0;
}Okazuje się jednak, że to nie zadziała. Aby pojedynczy element członkowski stał się przyjacielem, kompilator musi zapoznać się z pełną definicją klasy zaprzyjaźnionej funkcji członkowskiej (a nie tylko deklaracją forward). Ponieważ klasa Storage nie widziała jeszcze pełnej definicji klasy Display , kompilator popełni błąd w momencie, gdy spróbujemy uczynić funkcję składową zaprzyjaźnioną.
Na szczęście można to łatwo rozwiązać, przenosząc definicję klasy Display przed definicją klasy Storage (w tym samym pliku lub przenosząc definicję of Display do pliku nagłówkowego i #włączając go przed zdefiniowaniem Storage ).
#include <iostream>
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
void displayStorage(const Storage& storage) // błąd kompilacji: kompilator nie wie, czym jest pamięć
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // najpierw wyświetl podwójnie
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
};
class Storage
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{
}
// Uczyń Display::displayStorage Funkcja składowa zaprzyjaźniona z klasą Storage
friend void Display::displayStorage(const Storage& storage); // w porządku teraz
};
int main()
{
Storage storage { 5, 6.7 };
Display display { false };
display.displayStorage(storage);
return 0;
}Jednak mamy teraz inny problem. Ponieważ funkcja składowa Display::displayStorage() używa Storage jako parametr referencyjny i właśnie przenieśliśmy definicję Storage poniżej definicji Display, kompilator będzie narzekał, że nie wie, czym jest Storage . Nie możemy tego naprawić, zmieniając kolejność definicji, ponieważ wtedy cofniemy poprzednią poprawkę.
Na szczęście można to również naprawić w kilku prostych krokach. Po pierwsze, możemy dodać class Storage jako deklarację forward, tak aby kompilator zgodził się z odwołaniem do Storage zanim zobaczy pełną definicję klasy.
Po drugie, możemy przenieść definicję Display::displayStorage() poza klasę, po pełnej definicji Storage .
Oto jak to wygląda np.:
#include <iostream>
class Storage; // deklaracja forward dla klasy Storage
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
void displayStorage(const Storage& storage); // deklaracja forward dla Storage, zainstalowana tutaj w zakresie komunikacji
};
class Storage // pełna definicja klasy Storage
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{
}
// Uczyń Display::displayStorage Funkcja składowa zaprzyjaźniona z klasą Storage
// Wymaga zobaczenia pełnej definicji klasy Display (ponieważ displayStorage jest członkiem)
friend void Display::displayStorage(const Storage& storage);
};
// Teraz możemy zdefiniować Display::displayStorage
// Wymaga zobaczenia pełnej definicji klasy Storage (gdy uzyskujemy dostęp do członków Storage)
void Display::displayStorage(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // najpierw wyświetl podwójnie
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
int main()
{
Storage storage { 5, 6.7 };
Display display { false };
display.displayStorage(storage);
return 0;
}Teraz wszystko skompiluje się poprawnie: deklaracja forward class Storage wystarczy, aby spełnić deklarację Display::displayStorage() wewnątrz klasy Display . Pełna definicja Display spełnia deklarację Display::displayStorage() jako przyjaciela Storage. A pełna definicja klasy Storage wystarczy, aby spełnić definicję funkcji składowej Display::displayStorage().
Jeśli jest to nieco zagmatwane, zobacz komentarze w programie powyżej. Najważniejsze jest to, że deklaracja przekazania klasy spełnia odniesienia do klasy. Jednakże uzyskanie dostępu do elementów klasy wymaga, aby kompilator zapoznał się z pełną definicją klasy.
Jeśli wydaje się to uciążliwe – rzeczywiście tak jest. Na szczęście ten taniec jest konieczny tylko dlatego, że staramy się zrobić wszystko w jednym pliku. Lepszym rozwiązaniem jest umieszczenie definicji każdej klasy w oddzielnym pliku nagłówkowym, a definicje funkcji składowych w odpowiednich plikach .cpp. W ten sposób wszystkie definicje klas będą dostępne w plikach .cpp i nie będzie konieczne przestawianie klas ani funkcji!
Czas quizu
Pytanie nr 1
W geometrii punkt to pozycja w przestrzeni. Możemy zdefiniować punkt w przestrzeni 3D jako zbiór współrzędnych x, y i z. Na przykład Point { 2.0, 1.0, 0.0 } byłby punktem w przestrzeni współrzędnych x=2,0, y=1,0 i z=0,0.
W fizyce wektor to wielkość, która ma wielkość (długość) i kierunek (ale nie ma położenia). Możemy zdefiniować wektor w przestrzeni 3D jako wartość x, y i z reprezentującą kierunek wektora wzdłuż osi x, y i z (można z nich wyprowadzić długość). Na przykład Vector { 2.0, 0.0, 0.0 } byłby wektorem reprezentującym kierunek wzdłuż dodatniej osi x (tylko) o długości 2,0.
Vector można zastosować do Point w celu przesunięcia Point do nowej pozycji. Odbywa się to poprzez dodanie kierunku wektora do położenia punktu, aby uzyskać nową pozycję. Na przykład Point { 2.0, 1.0, 0.0 } + Vector { 2.0, 0.0, 0.0 } dałoby Point { 4.0, 1.0, 0.0 }.
Takie punkty i wektory są często używane w grafice komputerowej (z punktami reprezentującymi wierzchołki kształtu, a wektory reprezentują ruch kształtu).
Biorąc pod uwagę następujący program:
#include <iostream>
class Vector3d
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Vector3d(double x, double y, double z)
: m_x{x}, m_y{y}, m_z{z}
{
}
void print() const
{
std::cout << "Vector(" << m_x << ", " << m_y << ", " << m_z << ")\n";
}
};
class Point3d
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point3d(double x, double y, double z)
: m_x{x}, m_y{y}, m_z{z}
{ }
void print() const
{
std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ")\n";
}
void moveByVector(const Vector3d& v)
{
// zaimplementuj tę funkcję jako przyjaciel klasy Vector3d
}
};
int main()
{
Point3d p { 1.0, 2.0, 3.0 };
Vector3d v { 2.0, 2.0, -3.0 };
p.print();
p.moveByVector(v);
p.print();
return 0;
}> Krok #1
Utwórz Point3d przyjazną klasę Vector3d i zaimplementuj funkcję Point3d::moveByVector().
> Krok #2
Zamiast tworzyć klasę Point3d przyjacielem klasy Vector3d, uczyń funkcję składową Point3d::moveByVector przyjacielem klasy Vector3d.
> Krok #3
Zaimplementuj rozwiązanie z poprzedniego kroku, używając 5 oddzielnych plików: Point3d.h, Point3d.cpp, Vector3d.h, Vector3d.cpp i main.cpp.
Dziękujemy czytelnikowi Shivie za sugestię i rozwiązanie.

