W poprzedniej lekcji (14.5 -- Członkowie publiczni i prywatni oraz specyfikatory dostępu) wspomnieliśmy, że zmienne składowe klasy są zwykle ustawiane jako prywatne. Programistom, którzy po raz pierwszy uczą się o klasach, często trudno jest zrozumieć, dlaczego chcesz to zrobić. W końcu ustawienie zmiennych jako prywatnych oznacza, że nie będą one dostępne publicznie. W najlepszym przypadku oznacza to więcej pracy podczas pisania zajęć. W najgorszym przypadku może się to wydawać całkowicie bezcelowe (zwłaszcza jeśli zapewniamy funkcje publicznego dostępu do danych prywatnego użytkownika).
Odpowiedź na to pytanie jest tak fundamentalna, że poświęcimy temu tematowi całą lekcję!
Zacznijmy od analogii.
We współczesnym życiu mamy dostęp do wielu urządzeń mechanicznych lub elektronicznych. Włączasz/wyłączasz telewizor za pomocą pilota. Wciśnięcie pedału gazu sprawia, że samochód jedzie do przodu. Włączasz światła, przełączając przełącznik. Wszystkie te urządzenia mają coś wspólnego: zapewniają prosty interfejs użytkownika (zestaw przycisków, pedał, przełącznik itp.), który pozwala na wykonywanie kluczowych czynności.
To, jak faktycznie działają te urządzenia, jest przed tobą ukryte. Po naciśnięciu przycisku na pilocie nie musisz wiedzieć, w jaki sposób pilot komunikuje się z telewizorem. Kiedy w samochodzie naciskasz pedał gazu, nie musisz wiedzieć, w jaki sposób silnik spalinowy wprawia koła w ruch. Robiąc zdjęcie, nie musisz wiedzieć, w jaki sposób czujniki zbierają światło i zamieniają je w pikselowany obraz.
To oddzielenie interfejsu i implementacji jest niezwykle przydatne, ponieważ pozwala nam używać obiektów bez konieczności rozumienia, jak działają – zamiast tego musimy jedynie zrozumieć, jak z nimi wchodzić w interakcję. To znacznie zmniejsza złożoność korzystania z tych obiektów i zwiększa liczbę obiektów, z którymi jesteśmy w stanie wejść w interakcję.
Implementacja i interfejsy w typach klas
Z podobnych powodów rozdzielenie interfejsu i implementacji jest przydatne w programowaniu. Ale najpierw zdefiniujmy, co rozumiemy przez interfejs i implementację w odniesieniu do typów klas.
Klasa interfejs typu klasy (zwanego także interfejsem klasy) określa, w jaki sposób użytkownik typu klasy będzie wchodzić w interakcję z obiektami typu klasy. Ponieważ spoza typu klasy można uzyskać dostęp tylko do publicznych składowych, publiczne składowe typu klasy tworzą jego interfejs. Z tego powodu interfejs składający się z elementów publicznych nazywany jest czasem interfejsem publicznym.
Interfejs to niejawna umowa pomiędzy autorem klasy a użytkownikiem klasy. Jeśli istniejący interfejs zostanie kiedykolwiek zmieniony, każdy kod, który go używa, może ulec uszkodzeniu. Dlatego ważne jest, aby interfejsy dla naszych typów klas były dobrze zaprojektowane i stabilne (nie zmieniały się zbytnio).
Klasa implementacją typu klasy składa się z kodu, który faktycznie sprawia, że klasa zachowuje się zgodnie z zamierzeniami. Obejmuje to zarówno zmienne składowe przechowujące dane, jak i treści funkcji składowych, które zawierają logikę programu i manipulują zmiennymi składowymi.
Ukrywanie danych
W programowaniu ukrywanie danych (tzw ukrywanie informacji lub abstrakcja danych) to technika używana do wymuszenia oddzielenia interfejsu od implementacji poprzez ukrywanie (uniemożliwienie dostępu) implementacji typu danych zdefiniowanego przez program przed użytkownikami.
Implementacja ukrywania danych w typie klasy C++ jest prosta. Po pierwsze, upewniamy się, że składowe danych typu klasy są prywatne (aby użytkownik nie miał do nich bezpośredniego dostępu). Instrukcje wewnątrz ciał funkcji składowych nie są już bezpośrednio dostępne dla użytkowników, więc nie musimy tam robić nic więcej. Następnie upewniamy się, że funkcje składowe są publiczne, aby użytkownik mógł je wywołać.
Przestrzegając tych zasad, zmuszamy użytkownika typu klasowego do manipulowania obiektami przy użyciu interfejsu publicznego i uniemożliwiamy mu bezpośredni dostęp do szczegółów implementacji.
Klasy zdefiniowane w C++ powinny używać ukrywania danych. I faktycznie, wszystkie klasy dostarczane przez standardową bibliotekę właśnie to robią. Struktury natomiast nie powinny stosować ukrywania danych, ponieważ posiadanie niepublicznych elementów składowych uniemożliwia traktowanie ich jako agregatów.
Definiowanie klas w ten sposób wymaga od autora klasy trochę dodatkowej pracy. A wymaganie od użytkowników klasy korzystania z publicznego interfejsu może wydawać się bardziej uciążliwe niż bezpośrednie zapewnianie publicznego dostępu do zmiennych składowych. Jednak takie postępowanie zapewnia dużą liczbę przydatnych korzyści, które pomagają zachęcać do ponownego użycia klas i łatwości ich konserwacji. Resztę lekcji spędzimy na omawianiu tych korzyści.
Nomenklatura
W programowaniu termin hermetyzacja zazwyczaj odnosi się do jednej z dwóch rzeczy:
- Zamknięcia jednego lub więcej elementów w pewnego rodzaju kontenerze.
- Łączenie danych i funkcji do operowania na instancjach tych danych.
W C++: typ klasy zawierający dane i publiczny interfejs dla tworzenie i manipulowanie obiektami tej klasy jest hermetyzowane. Ponieważ enkapsulacja jest warunkiem wstępnym ukrywania danych i ponieważ ukrywanie danych jest tak ważną techniką, tradycyjnie termin enkapsulacja często obejmuje również ukrywanie danych.
W tej serii tutoriali założymy, że wszystkie enkapsulowane klasy implementują ukrywanie danych.
Ukrywanie danych ułatwia korzystanie z klas i zmniejsza złożoność
Aby korzystać z klasy enkapsulowanej, nie musisz wiedzieć, jak jest ona zaimplementowana. Wystarczy zrozumieć jego interfejs: jakie funkcje składowe są publicznie dostępne, jakie przyjmują argumenty i jakie wartości zwracają.
Na przykład:
#include <iostream>
#include <string_view>
int main()
{
std::string_view sv{ "Hello, world!" };
std::cout << sv.length();
return 0;
}W tym krótkim programie szczegóły implementacji std::string_view nie są nam znane. Nie wiemy, ilu elementów danych ma std::string_view , jak się nazywają ani jakiego są typu. Nie wiemy, w jaki sposób length() funkcja składowa zwraca długość przeglądanego ciągu.
A najlepsze jest to, że nie musimy tego wiedzieć! Program po prostu działa. Wszystko, co musimy wiedzieć, to jak zainicjować obiekt typu std::string_view i co zwraca length() funkcja składowa.
Brak konieczności zajmowania się tymi szczegółami radykalnie zmniejsza złożoność programów, co z kolei zmniejsza liczbę błędów. Jest to bardziej niż jakikolwiek inny powód kluczowa zaleta enkapsulacji.
Wyobraź sobie, o ile bardziej skomplikowane byłoby C++, gdybyś musiał zrozumieć, w jaki sposób std::string, std::vector lub std::cout zostały zaimplementowane, aby z nich skorzystać!
Ukrywanie danych pozwala nam zachować niezmienniki
Wróciliśmy do lekcji wprowadzającej na temat klas (>14.2 -- Wprowadzenie do klas), przedstawiliśmy tę koncepcję of niezmienników klasy, które są warunkami, które muszą być spełnione przez cały czas życia obiektu, aby obiekt pozostał w prawidłowym stanie.
Rozważ następujący program:
#include <iostream>
#include <string>
struct Employee // members are public by default
{
std::string name{ "John" };
char firstInitial{ 'J' }; // should match first initial of name
void print() const
{
std::cout << "Employee " << name << " has first initial " << firstInitial << '\n';
}
};
int main()
{
Employee e{}; // defaults to "John" and 'J'
e.print();
e.name = "Mark"; // change employee's name to "Mark"
e.print(); // prints wrong initial
return 0;
}Ten program wypisuje:
John has first initial J Mark has first initial J
Nasz Employee struct ma niezmiennik klasy, który firstInitial powinien zawsze być równy pierwszemu znakowi name. Jeśli kiedykolwiek okaże się to nieprawdą, funkcja print() będzie działać nieprawidłowo.
Ponieważ name element jest publiczny, kod w main() możliwy do ustawienia e.name Do "Mark", a firstInitial element nie jest aktualizowany. Nasz niezmiennik jest uszkodzony, a nasze drugie wywołanie print() nie działa zgodnie z oczekiwaniami.
Kiedy dajemy użytkownikom bezpośredni dostęp do implementacji klasy, stają się oni odpowiedzialni za utrzymanie wszystkich niezmienników – czego mogą nie zrobić (albo poprawnie, albo w ogóle). Nałożenie tego ciężaru na użytkownika zwiększa złożoność.
Przepiszmy ten program, czyniąc nasze zmienne składowe prywatnymi i udostępniając funkcję składową ustawiającą imię i nazwisko pracownika:
#include <iostream>
#include <string>
#include <string_view>
class Employee // members are private by default
{
std::string m_name{};
char m_firstInitial{};
public:
void setName(std::string_view name)
{
m_name = name;
m_firstInitial = name.front(); // use std::string::front() to get first letter of `name`
}
void print() const
{
std::cout << "Employee " << m_name << " has first initial " << m_firstInitial << '\n';
}
};
int main()
{
Employee e{};
e.setName("John");
e.print();
e.setName("Mark");
e.print();
return 0;
}Ten program działa teraz zgodnie z oczekiwaniami:
John has first initial J Mark has first initial M
Jedyna zmiana z perspektywy użytkownika polega na tym, że zamiast przypisywać name wartość bezpośrednio, wywołuje on wartość członkowską funkcja setName(), która ustawia oba m_name i m_firstInitial. Użytkownik jest zwolniony z obowiązku utrzymywania tego niezmiennika!
Ukrywanie danych pozwala nam na lepsze wykrywanie błędów (i obsługę)
W powyższym programie niezmiennik, który m_firstInitial musi pasować do pierwszego znaku m_name istnieje, ponieważ m_firstInitial istnieje niezależnie od m_name. Możemy usunąć ten konkretny niezmiennik, zastępując element danych m_firstInitial funkcją składową, która zwraca pierwszy inicjał:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{ "John" };
public:
void setName(std::string_view name)
{
m_name = name;
}
// use std::string::front() to get first letter of `m_name`
char firstInitial() const { return m_name.front(); }
void print() const
{
std::cout << "Employee " << m_name << " has first initial " << firstInitial() << '\n';
}
};
int main()
{
Employee e{}; // defaults to "John"
e.setName("Mark");
e.print();
return 0;
}Jednak ten program ma inny niezmiennik klasy. Poświęć chwilę i zobacz, czy potrafisz określić, co to jest. Poczekamy tutaj i popatrzymy na tę wyschniętą farbę…
Odpowiedź jest taka, że m_name nie powinien to być pusty ciąg znaków (bo każdy Employee powinien mieć nazwę). Jeśli m_name jest ustawiony na pusty ciąg znaków, od razu nic złego się nie stanie. Ale jeśli następnie zostanie wywołany firstInitial() , front() element std::string będzie próbował uzyskać pierwszą literę pustego ciągu, co prowadzi do niezdefiniowanego zachowania.
W idealnym przypadku chcielibyśmy zapobiec m_name byciu kiedykolwiek pustym.
Jeśli użytkownik miał publiczny dostęp m_name element, mógłby po prostu ustawić m_name = "" i nie możemy nic zrobić, aby temu zapobiec.
Jednakże, ponieważ zmuszamy użytkownika do ustawienia m_name za pośrednictwem funkcji interfejsu publicznego setName(), możemy setName() sprawdzić, czy użytkownik podał prawidłową nazwę. Jeżeli nazwa nie jest pusta to możemy ją przypisać do m_name. Jeśli nazwa jest pustym ciągiem znaków, w odpowiedzi możemy zrobić dowolną liczbę rzeczy:
- Zignoruj żądanie ustawienia nazwy na „” i wróć do osoby dzwoniącej.
- Potwierdź.
- Zgłoś wyjątek.
- Zrób hokey-pokey. Czekaj, nie ten.
Chodzi o to, że możemy wykryć niewłaściwe użycie, a następnie zareagować w sposób, który naszym zdaniem jest najbardziej odpowiedni. To, jak najskuteczniej radzić sobie z takimi przypadkami, to temat na inny dzień.
Ukrywanie danych umożliwia zmianę szczegółów implementacji bez psucia istniejących programów
Rozważmy prosty przykład:
#include <iostream>
struct Something
{
int value1 {};
int value2 {};
int value3 {};
};
int main()
{
Something something;
something.value1 = 5;
std::cout << something.value1 << '\n';
}Chociaż ten program działa dobrze, co by się stało, gdybyśmy zdecydowali się zmienić szczegóły implementacji klasy w ten sposób?
#include <iostream>
struct Something
{
int value[3] {}; // uses an array of 3 values
};
int main()
{
Something something;
something.value1 = 5;
std::cout << something.value1 << '\n';
}Nie omawialiśmy jeszcze tablic, ale nie martw się tym. Chodzi o to, że ten program już się nie kompiluje, ponieważ element o nazwie value1 już nie istnieje, a instrukcja w main() wciąż używa tego identyfikatora.
Ukrywanie danych daje nam możliwość zmiany sposobu implementacji klas bez przerywania programów, które ich używają.
Oto hermetyzowana wersja oryginalnej wersji tej klasy, która wykorzystuje funkcje do dostęp m_value1:
#include <iostream>
class Something
{
private:
int m_value1 {};
int m_value2 {};
int m_value3 {};
public:
void setValue1(int value) { m_value1 = value; }
int getValue1() const { return m_value1; }
};
int main()
{
Something something;
something.setValue1(5);
std::cout << something.getValue1() << '\n';
}Teraz zmieńmy implementację klasy z powrotem na tablicę:
#include <iostream>
class Something
{
private:
int m_value[3]; // note: we changed the implementation of this class!
public:
// We have to update any member functions to reflect the new implementation
void setValue1(int value) { m_value[0] = value; }
int getValue1() const { return m_value[0]; }
};
int main()
{
// But our programs that use the class do not need to be updated!
Something something;
something.setValue1(5);
std::cout << something.getValue1() << '\n';
}Ponieważ nie zmieniliśmy publicznego interfejsu klasy, nasz program korzystający z tego interfejsu nie musiał się w ogóle zmieniać i nadal działa identycznie.
Analogicznie, gdyby gnomy wkradły się nocą do Twojego domu i zastąpiły elementy pilota od telewizora inną (ale kompatybilną) technologią, prawdopodobnie nawet byś tego nie zrobił uwaga!
Klasy z interfejsami są łatwiejsze do debugowania
I wreszcie, enkapsulacja może pomóc w debugowaniu programu, gdy coś pójdzie nie tak. Często, gdy program nie działa poprawnie, dzieje się tak dlatego, że jednej ze zmiennych składowych przypisano niepoprawną wartość. Jeśli każdy jest w stanie bezpośrednio ustawić zmienną składową, wyśledzenie, który fragment kodu faktycznie zmodyfikował zmienną składową do niewłaściwej wartości, może być trudne. Może to obejmować wyznaczenie punktu przerwania dla każdej instrukcji modyfikującej zmienną składową — a może być ich wiele.
Jeśli jednak element członkowski można zmienić tylko za pomocą pojedynczej funkcji składowej, możesz po prostu ustawić punkt przerwania dla tej pojedynczej funkcji i obserwować, jak każdy obiekt wywołujący zmienia wartość. Może to znacznie ułatwić ustalenie, kto jest winowajcą.
Preferuj funkcje niebędące składowymi od funkcji składowych
W C++, jeśli funkcję można rozsądnie zaimplementować jako funkcję niebędącą składową, lepiej zaimplementować ją jako funkcję niebędącą składową, a nie jako funkcję składową.
Ma to wiele zalet:
- Funkcje nieczłonkowskie nie są częścią interfejsu twojej klasy. Zatem interfejs Twojej klasy będzie mniejszy i prostszy, dzięki czemu klasa będzie łatwiejsza do zrozumienia.
- Funkcje niebędące składowymi wymuszają hermetyzację, ponieważ takie funkcje muszą działać poprzez publiczny interfejs klasy. Nie ma pokusy, aby uzyskać bezpośredni dostęp do implementacji tylko dlatego, że jest to wygodne.
- Funkcje niebędące składowymi nie muszą być brane pod uwagę przy wprowadzaniu zmian w implementacji klasy (o ile interfejs nie zmienia się w niekompatybilny sposób).
- Funkcje niebędące składowymi są zazwyczaj łatwiejsze do debugowania.
- Funkcje niebędące składowymi zawierające dane i logikę specyficzne dla aplikacji można oddzielić od części składowych wielokrotnego użytku class.
Jeśli masz wcześniejsze doświadczenie z nowoczesnym językiem OOP (takim jak Java lub C#), ta praktyka może być zaskoczeniem. Języki te korzystają z innego modelu pojęciowego, w którym klasa jest centrum wszechświata i wszystko wokół niej się rozwiązuje. W związku z tym w tych językach na pierwszym planie znajdują się funkcje składowe (a tak naprawdę Java i C# nie obsługują nawet funkcji niebędących członkami).
Najlepsza praktyka
Jeśli to możliwe, preferuj implementację funkcji jako niebędących członkami (szczególnie funkcje, które zawierają dane lub logikę specyficzną dla aplikacji).
Wskazówka
Oto uproszczony przewodnik, kiedy uczynić funkcję członkiem, a kiedy nie-członkiem:
- Użyj funkcji składowej, kiedy musisz. C++ wymaga, aby określone rodzaje funkcji były zdefiniowane jako elementy członkowskie. Jeden przykład zobaczymy w następnej lekcji, kiedy będziemy mówić o konstruktorach. Inne obejmują destruktory, funkcje wirtualne i określone operatory.
- Preferuj funkcję składową, gdy funkcja wymaga dostępu do prywatnych (lub chronionych) danych, które nie powinny być ujawniane.
- W przeciwnym razie preferuj funkcję niebędącą składową (szczególnie w przypadku funkcji, które nie modyfikują stanu obiektu).
Istnieją wyjątki od dwóch ostatnich - niektóre z nich przedstawimy podczas omawiania powiązanych z nimi tematów z.
Jeden typowy, ale wymagający przypadek ma miejsce, gdy preferowanie funkcji niebędącej członkiem wymaga dodania funkcji dostępu w celu ułatwienia. W takim przypadku będziesz musiał rozważyć kompromisy.
- Dodanie funkcji dostępu oznacza utworzenie jednej lub dwóch nowych funkcji składowych (gettera i ewentualnie settera), co zwiększa rozmiar i złożoność interfejsu klas. Może to nie być tego warte, chyba że będziesz mógł korzystać z tych nowych funkcji dostępu w wielu miejscach.
- Nie dodawaj funkcji dostępu do danych, które nie powinny być bezpośrednio dostępne (np. ze względu na stan wewnętrzny) lub które pozwoliłyby użytkownikowi na naruszenie niezmiennika klasy.
Powiązana treść
Artykuł Jak funkcje niebędące członkami poprawiają enkapsulację autorstwa Scotta Meyersa bada ideę preferując bardziej szczegółowo funkcje niebędące składowymi.
Zilustrujmy to trzema podobnymi przykładami, w kolejności od najgorszego do najlepszego:
#include <iostream>
#include <string>
class Yogurt
{
std::string m_flavor{ "vanilla" };
public:
void setFlavor(std::string_view flavor)
{
m_flavor = flavor;
}
const std::string& getFlavor() const { return m_flavor; }
// Worst: member function print() uses direct access to m_flavor when getter exists
void print() const
{
std::cout << "The yogurt has flavor " << m_flavor << '\n';
}
};
int main()
{
Yogurt y{};
y.setFlavor("cherry");
y.print();
return 0;
}Powyższa wersja jest najgorsza. Funkcja składowa print() uzyskuje bezpośredni dostęp m_flavor jeśli moduł pobierający dla smaku już istnieje. Jeśli implementacja klasy zostanie kiedykolwiek zaktualizowana, print() prawdopodobnie będzie również wymagać modyfikacji. Ciąg drukowany przez print() jest specyficzny dla aplikacji (inna aplikacja korzystająca z tej klasy może chcieć wydrukować coś innego, co będzie wymagało klonowania lub modyfikacji klasy).
#include <iostream>
#include <string>
class Yogurt
{
std::string m_flavor{ "vanilla" };
public:
void setFlavor(std::string_view flavor)
{
m_flavor = flavor;
}
const std::string& getFlavor() const { return m_flavor; }
// Better: member function print() has no direct access to members
void print(std::string_view prefix) const
{
std::cout << prefix << ' ' << getFlavor() << '\n';
}
};
int main()
{
Yogurt y{};
y.setFlavor("cherry");
y.print("The yogurt has flavor");
return 0;
}Powyższa wersja jest lepsza, ale nadal nie jest świetna. print() jest nadal funkcją składową, ale przynajmniej teraz nie ma bezpośredniego dostępu do żadnych elementów danych. Jeśli implementacja klasy zostanie kiedykolwiek zaktualizowana, print() będzie musiał zostać oceniony, aby określić, czy wymaga aktualizacji (ale nie będzie). Przedrostek dla funkcji print() jest teraz sparametryzowany, co pozwala nam przenieść przedrostek do funkcji niebędącej składową main(). Jednak funkcja nadal nakłada ograniczenia na sposób drukowania (np. zawsze drukuje jako przedrostek, spację, smak, nową linię). Jeżeli nie spełnia to potrzeb danej aplikacji, trzeba będzie dodać inną funkcję.
#include <iostream>
#include <string>
class Yogurt
{
std::string m_flavor{ "vanilla" };
public:
void setFlavor(std::string_view flavor)
{
m_flavor = flavor;
}
const std::string& getFlavor() const { return m_flavor; }
};
// Best: non-member function print() is not part of the class interface
void print(const Yogurt& y)
{
std::cout << "The yogurt has flavor " << y.getFlavor() << '\n';
}
int main()
{
Yogurt y{};
y.setFlavor("cherry");
print(y);
return 0;
}Powyższa wersja jest najlepsza. print() jest teraz funkcją niebędącą składową. Nie ma bezpośredniego dostępu do żadnych członków. Jeśli implementacja klasy kiedykolwiek się zmieni, print() nie będzie w ogóle trzeba oceniać. Dodatkowo każda aplikacja może udostępnić własną print() funkcję, która drukuje dokładnie tak, jak chce dana aplikacja.
Kolejność deklaracji członków klasy
Pisząc kod poza klasą, musimy zadeklarować zmienne i funkcje, zanim będziemy mogli z nich skorzystać. Jednakże wewnątrz klasy to ograniczenie nie istnieje. Jak zauważono w lekcji 14.3 -- Element funkcje, możemy uporządkować nasze elementy w dowolnej kolejności.
Jak więc powinniśmy je uporządkować?
Są tu dwie szkoły myślenia:
- Najpierw wypisz swoich prywatnych członków, a następnie wymień publiczne funkcje członków. Jest to zgodne z tradycyjnym stylem deklaracji przed użyciem. Każdy, kto spojrzy na kod Twoich zajęć, zobaczy, jak zdefiniowałeś elementy danych przed ich użyciem, co może ułatwić czytanie i zrozumienie szczegółów implementacji.
- Wymień najpierw członków publicznych, a członków prywatnych umieść na dole. Ponieważ ktoś, kto korzysta z Twojej klasy, jest zainteresowany interfejsem publicznym, umieszczenie członków publicznych na pierwszym miejscu powoduje, że na wierzchu znajdują się potrzebne im informacje, a szczegóły implementacji (które są najmniej ważne).
We współczesnym C++ częściej zalecana jest druga metoda (najpierw członkowie publiczni) jest częściej zalecana, szczególnie w przypadku kodu, który będzie udostępniany innym programistom.
Najlepsza praktyka
Deklaracja członków publicznych na pierwszym miejscu, następnie chronionych, a na końcu członków prywatnych. Zwraca to uwagę na interfejs publiczny i umniejsza szczegóły implementacji.
Nota autora
Większość przykładów w tej witrynie używa kolejności deklaracji odwrotnej do zalecanej. Jest to częściowo historyczne, ale uważamy, że ta kolejność jest bardziej intuicyjna podczas uczenia się mechaniki języka, gdzie koncentrujemy się na szczegółach implementacji i analizowaniu sposobu działania.
Dla zaawansowanych czytelników
Następująca kolejność jest zalecana w Przewodniku po stylu Google C++:
- Typy i aliasy typów (typedef, using, enum, zagnieżdżone struktury i klasy oraz przyjaciel typy)
- Stałe statyczne
- Funkcje fabryczne
- Konstruktory i operatory przypisania
- Destruktor
- Wszystkie inne funkcje (statyczne i niestatyczne funkcje składowe oraz funkcje zaprzyjaźnione)
- Elementy danych (statyczne i niestatyczne)

