Rozważ następujący krótki program:
#include <iostream>
enum class FruitType
{
apple,
banana,
cherry
};
class Fruit
{
private:
FruitType m_type { };
int m_percentageEaten { 0 };
public:
Fruit(FruitType type) :
m_type { type }
{
}
FruitType getType() { return m_type; }
int getPercentageEaten() { return m_percentageEaten; }
bool isCherry() { return m_type == FruitType::cherry; }
};
int main()
{
Fruit apple { FruitType::apple };
if (apple.getType() == FruitType::apple)
std::cout << "I am an apple";
else
std::cout << "I am not an apple";
return 0;
}W tym programie nie ma nic złego. Ponieważ jednak klasa enum class FruitType ma być używana w połączeniu z klasą Fruit , istnienie jej niezależnie od klasy pozostawia nam możliwość wywnioskowania, w jaki sposób są one połączone.
Typy zagnieżdżone (typy składowe)
Jak dotąd widzieliśmy typy klas z dwoma różnymi rodzajami elementów członkowskich: członkami danych i funkcjami składowymi. Nasza Fruit klasa w powyższym przykładzie zawiera oba te elementy.
Typy klas obsługują inny rodzaj elementów: typy zagnieżdżone (tzw typy elementów). Aby utworzyć typ zagnieżdżony, wystarczy zdefiniować typ wewnątrz klasy, pod odpowiednim specyfikatorem dostępu.
Oto ten sam program, co powyżej, przepisany tak, aby korzystał z typu zagnieżdżonego zdefiniowanego w Fruit klasę:
#include <iostream>
class Fruit
{
public:
// FruitType został przeniesiony wewnątrz klasy, pod specyfikatorem dostępu publicznego
// Zmieniliśmy także jego nazwę Type i uczyniliśmy z niej wyliczenie, a nie wyliczenie klasa
enum Type
{
apple,
banana,
cherry
};
private:
Type m_type {};
int m_percentageEaten { 0 };
public:
Fruit(Type type) :
m_type { type }
{
}
Type getType() { return m_type; }
int getPercentageEaten() { return m_percentageEaten; }
bool isCherry() { return m_type == cherry; } // Wewnątrz elementów Fruit nie musimy już poprzedzać modułów wyliczających FruitType::
};
int main()
{
// Uwaga: Poza klasą dostęp do modułów wyliczających uzyskujemy teraz poprzez przedrostek Fruit::
Fruit apple { Fruit::apple };
if (apple.getType() == Fruit::apple)
std::cout << "I am an apple";
else
std::cout << "I am not an apple";
return 0;
}Jest kilka rzeczy, na które warto zwrócić uwagę.
Najpierw zauważ, że FruitType jest teraz zdefiniowany wewnątrz klasy, gdzie został zmieniono nazwę Type z powodów, które wkrótce omówimy.
Drugi, zagnieżdżony typ Type został zdefiniowany na górze klasy. Nazwy typów zagnieżdżonych muszą być w pełni zdefiniowane, zanim będzie można ich użyć, dlatego zwykle są definiowane jako pierwsze.
Najlepsza praktyka
Zdefiniuj dowolne typy zagnieżdżone na górze typu klasy.
Po trzecie, typy zagnieżdżone podlegają normalnym regułom dostępu. Type jest definiowany w public specyfikatorze dostępu, dzięki czemu nazwa typu i moduły wyliczające mogą być bezpośrednio dostępne dla public.
Po czwarte, typy klas pełnią rolę obszaru zasięgu dla nazw zadeklarowanych wewnątrz nich, tak samo jak robią to przestrzenie nazw. Dlatego w pełni kwalifikowana nazwa Type Jest Fruit::Type i w pełni kwalifikowana nazwa apple enumeratora to Fruit::apple.
W obrębie elementów klasy nie musimy używać w pełni kwalifikowanej nazwy. Na przykład w funkcji składowej isCherry() uzyskujemy dostęp do modułu wyliczającego cherry bez Fruit:: kwalifikatora zakresu.
Poza klasą musimy używać pełnej nazwy (np. Fruit::apple). Zmieniliśmy nazwę FruitType Do Type abyśmy mogli uzyskać do niej dostęp jako Fruit::Type (zamiast bardziej nadmiarowego Fruit::FruitType).
Na koniec zmieniliśmy nasz typ wyliczeniowy z o zasięgu na bez zakresu. Ponieważ sama klasa działa teraz jako region zasięgu, używanie również modułu wyliczającego o zasięgu jest w pewnym stopniu zbędne. Zmiana na wyliczenie bez zakresu oznacza, że możemy uzyskać dostęp do modułów wyliczających ponieważ Fruit::apple zamiast dłuższych Fruit::Type::apple musielibyśmy użyć, gdyby moduł wyliczający miał zakres.
Zagnieżdżone definicje typów i aliasy typów
Typy klas mogą również zawierać zagnieżdżone definicje typów lub aliasy typów:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
public:
using IDType = int;
private:
std::string m_name{};
IDType m_id{};
double m_wage{};
public:
Employee(std::string_view name, IDType id, double wage)
: m_name { name }
, m_id { id }
, m_wage { wage }
{
}
const std::string& getName() { return m_name; }
IDType getId() { return m_id; } // może użyć niekwalifikowanej nazwy w klasie
};
int main()
{
Employee john { "John", 1, 45000 };
Employee::IDType id { john.getId() }; // musi użyć w pełni kwalifikowanej nazwy poza klasą
std::cout << john.getName() << " has id: " << id << '\n';
return 0;
}Wypisuje:
John has id: 1
Zauważ, że wewnątrz klasy możemy po prostu używać IDType, ale poza klasy musimy użyć w pełni kwalifikowanej nazwy Employee::IDType.
Zalety aliasów typów omawiamy w lekcji 10.7 -- Typedefs i aliasy typów, a tutaj służą temu samemu celowi. Bardzo często klasy w standardowej bibliotece C++ korzystają z zagnieżdżonych deflektorów typów. W chwili pisania tego tekstu std::string definiuje dziesięć zagnieżdżonych deflektorów typów!
Klasy zagnieżdżone i dostęp do klasy zewnętrznej. członkowie
Dość rzadko zdarza się, aby klasy miały inne klasy jako typ zagnieżdżony, ale jest to możliwe. W C++ klasa zagnieżdżona nie ma dostępu do this wskaźnika klasy zewnętrznej (zawierającej), więc klasy zagnieżdżone nie mają bezpośredniego dostępu do elementów klasy zewnętrznej. Dzieje się tak, ponieważ instancja klasy zagnieżdżonej może zostać utworzona niezależnie od klasy zewnętrznej (w takim przypadku nie byłoby żadnych elementów klasy zewnętrznej). dostęp!)
Jednakże, ponieważ klasy zagnieżdżone są członkami klasy zewnętrznej, mogą uzyskać dostęp do dowolnych prywatnych elementów klasy zewnętrznej znajdujących się w zasięgu.
Zilustrujmy to przykładem:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
public:
using IDType = int;
class Printer
{
public:
void print(const Employee& e) const
{
// Drukarka nie może uzyskać dostępu do wskaźnika „this” pracownika
// abyśmy nie mogli wydrukować bezpośrednio m_name i m_id
// Zamiast tego musimy przekazać obiekt Employee do użycia
// Ponieważ Drukarka jest członkiem Employee,
// możemy uzyskać bezpośredni dostęp do prywatnych członków e.m_name i e.m_id
std::cout << e.m_name << " has id: " << e.m_id << '\n';
}
};
private:
std::string m_name{};
IDType m_id{};
double m_wage{};
public:
Employee(std::string_view name, IDType id, double wage)
: m_name{ name }
, m_id{ id }
, m_wage{ wage }
{
}
// usunięto funkcje dostępu w tym przykładzie (ponieważ nie są używane)
};
int main()
{
const Employee john{ "John", 1, 45000 };
const Employee::Printer p{}; // utwórz instancję obiektu wewnętrznego klasa
p.print(john);
return 0;
}Wypisuje:
John has id: 1
Jest jeden przypadek, w którym klasy zagnieżdżone są częściej używane w bibliotece standardowej, większość klas iteratorów jest implementowanych jako klasy zagnieżdżone kontenera, po którym mają iterować przykład std::string::iterator jest zaimplementowany jako klasa zagnieżdżona std::string. Iteratorami zajmiemy się w następnym rozdziale.
Typy zagnieżdżone i deklaracje forward
Typ zagnieżdżony można zadeklarować do przodu w klasie, która go otacza. Typ zagnieżdżony można następnie zdefiniować później w klasie otaczającej lub poza nią. Na przykład:
#include <iostream>
class outer
{
public:
class inner1; // OK: deklaracja forward wewnątrz klasy otaczającej OK
class inner1{}; // OK: definicja typu forward zadeklarowanego wewnątrz klasy otaczającej
class inner2; // OK: deklaracja forward wewnątrz klasy otaczającej OK
};
class inner2 // OK: definicja typu forward zadeklarowanego poza klasą otaczającą
{
};
int main()
{
return 0;
}Jednakże typu zagnieżdżonego nie można zadeklarować w przód przed definicją otaczającej klasy.
#include <iostream>
class outer; // okej: można przekazać dalej deklarację typu niezagnieżdżonego
class outer::inner1; // błąd: nie można przekazać dalej zadeklarowania typu zagnieżdżonego przed definicją klasy zewnętrznej
class outer
{
public:
class inner1{}; // uwaga: zadeklarowany tutaj typ zagnieżdżony
};
class outer::inner1; // OK (ale zbędny), ponieważ typ zagnieżdżony został już zadeklarowany jako część definicji klasy zewnętrznej
int main()
{
return 0;
}Chociaż możesz zadeklarować typ zagnieżdżony po zdefiniowaniu klasy otaczającej, ponieważ klasa otaczająca będzie już zawierać deklarację dla typu zagnieżdżonego, robienie tego jest zbędne.

