W poprzednim rozdziale omówiliśmy struktury (13.7 — Wprowadzenie do struktur, elementów i wyboru elementów) i omówiliśmy, jak świetnie nadają się do łączenia wielu zmiennych składowych w jeden obiekt, który można inicjować i przekazywać jako jednostkę. Innymi słowy, struktury zapewniają wygodny pakiet do przechowywania i przenoszenia powiązanych wartości danych.
Rozważmy następującą strukturę:
#include <iostream>
struct Date
{
int day{};
int month{};
int year{};
};
void printDate(const Date& date)
{
std::cout << date.day << '/' << date.month << '/' << date.year; // assume DMY format
}
int main()
{
Date date{ 4, 10, 21 }; // initialize using aggregate initialization
printDate(date); // can pass entire struct to function
return 0;
}W powyższym przykładzie tworzymy Date obiekt, a następnie przekazujemy go do funkcji, która drukuje datę. Ten program wypisuje:
4/10/21
Przypomnienie
W tych tutorialach wszystkie nasze struktury są agregatami. Omawiamy agregaty na lekcji 13.8 -- Inicjowanie agregatu Struct.
Jakkolwiek użyteczne są struktury, mają one wiele braków, które mogą stanowić wyzwanie podczas prób budowania dużych, złożonych programów (szczególnie tych, nad którymi pracuje wielu programistów).
Problem z niezmiennikami klas
Być może największą trudnością ze strukturami jest to, że nie zapewniają one skutecznego sposobu dokumentowania i egzekwowania niezmienników klas. Na lekcji 9.6 — Assert i static_assert zdefiniowaliśmy niezmiennik jako „warunek, który musi być spełniony podczas wykonywania jakiegoś komponentu”.
W kontekście typów klas (które obejmują struktury, klasy i związki) a niezmiennik klasy to warunek, który musi zostać spełniony true przez cały okres istnienia obiektu, aby obiekt pozostał w prawidłowym stanie. Obiekt, który ma naruszony niezmiennik klasy, jest w nieprawidłowym stanie, a dalsze użytkowanie tego obiektu może spowodować nieoczekiwane lub niezdefiniowane zachowanie.
Kluczowa informacja
Użycie obiektu, którego niezmiennik klasy został naruszony, może skutkować nieoczekiwanym lub niezdefiniowanym zachowaniem.
Najpierw rozważ następujące kwestie struct:
struct Pair
{
int first {};
int second {};
};Klasa first i second elementy mogą być niezależnie ustawione na dowolną wartość, więc Pair struct nie ma niezmiennika.
Rozważmy teraz następującą prawie identyczną strukturę:
struct Fraction
{
int numerator { 0 };
int denominator { 1 };
};Z matematyki wiemy, że ułamek o mianowniku 0 jest matematycznie niezdefiniowany (ponieważ wartość ułamka to jego licznik podzielony przez jego mianownik -- a dzielenie przez 0 jest matematycznie niezdefiniowane). Dlatego chcemy mieć pewność, że denominator element obiektu Fraction nigdy nie jest ustawiony na 0. Jeżeli tak, to obiekt Ułamek znajduje się w nieprawidłowym stanie i dalsze użytkowanie tego obiektu może skutkować niezdefiniowanym zachowaniem.
Na przykład:
#include <iostream>
struct Fraction
{
int numerator { 0 };
int denominator { 1 }; // class invariant: should never be 0
};
void printFractionValue(const Fraction& f)
{
std::cout << f.numerator / f.denominator << '\n';
}
int main()
{
Fraction f { 5, 0 }; // create a Fraction with a zero denominator
printFractionValue(f); // cause divide by zero error
return 0;
}W powyższym przykładzie używamy komentarza do udokumentowania niezmiennika Ułamka. Udostępniamy również domyślny inicjator elementu członkowskiego, aby upewnić się, że mianownik jest ustawiony na 1 , jeśli użytkownik nie poda wartości inicjującej. Dzięki temu nasz obiekt Fraction będzie ważny, jeśli użytkownik zdecyduje się na zainicjowanie wartością obiektu Fraction. To dobry początek.
Ale nic nie stoi na przeszkodzie, abyśmy jawnie naruszyli ten niezmiennik klasy: kiedy tworzymy Fraction f, używamy inicjalizacji agregowanej, aby jawnie zainicjować mianownik do 0. Chociaż nie powoduje to natychmiastowego problemu, nasz obiekt jest teraz w nieprawidłowym stanie i dalsze używanie obiektu może spowodować nieoczekiwane lub niezdefiniowane zachowanie.
I dokładnie to widzimy później, gdy wywołujemy printFractionValue(f): program kończy się z powodu błędu dzielenia przez zero.
Na marginesie…
Niewielkim ulepszeniem byłoby assert(f.denominator != 0); na górze treści z printFractionValue. Dodaje to wartości dokumentacyjnej do kodu i sprawia, że staje się bardziej oczywiste, jaki warunek wstępny został naruszony. Jednak behawioralnie niczego to nie zmienia. Naprawdę chcemy wychwycić te problemy u źródła problemu (kiedy element członkowski jest inicjowany lub przypisano mu złą wartość), a nie gdzieś dalej (kiedy używana jest zła wartość).
Biorąc pod uwagę prostotę przykładu Fraction, proste uniknięcie tworzenia nieprawidłowych obiektów Fraction nie powinno być zbyt trudne. Jednakże w bardziej złożonej bazie kodu, która wykorzystuje wiele struktur, struktur z wieloma elementami lub struktur, których elementy członkowskie mają złożone relacje, zrozumienie, jaka kombinacja wartości może naruszyć jakiś niezmiennik klasy, może nie być tak oczywista.
Bardziej złożony niezmiennik klasy
Niezmiennik klasy dla Fraction jest prosty — the denominator członkiem nie może być 0. Jest to koncepcyjnie łatwe do zrozumienia i niezbyt trudne do uniknięcia.
Niezmienniki klas stają się większym wyzwaniem, gdy elementy struktury muszą mieć skorelowane wartości.
#include <string>
struct Employee
{
std::string name { };
char firstInitial { }; // should always hold first character of `name` (or `0`)
};W powyższej (źle zaprojektowanej) strukturze wartość znaku przechowywana w Member firstInitial powinien zawsze pasować do pierwszego znaku name.
Kiedy Employee obiekt jest inicjowany, użytkownik jest odpowiedzialny za utrzymanie niezmiennika klasy. A jeśli name kiedykolwiek zostanie przypisana nowa wartość, użytkownik jest również odpowiedzialny za jej zapewnienie firstInitial jest również aktualizowany. Ta korelacja może nie być oczywista dla programisty korzystającego z obiektu Employee, a nawet jeśli jest, może o tym zapomnieć.
Nawet jeśli napiszemy funkcje, które pomogą nam tworzyć i aktualizować obiekty Pracowników (zapewniając, że firstInitial jest zawsze ustawiany od pierwszego znaku name), nadal liczymy na to, że użytkownik będzie świadomy tych funkcji i będzie z nich korzystał.
Krótko mówiąc, poleganie na użytkowniku obiektu w zakresie utrzymywania niezmienników klas prawdopodobnie spowoduje powstanie problematycznego kodu.
Kluczowa informacja
Poleganie na użytkowniku obiektu w kwestii utrzymywania niezmienników klas może spowodować problemy.
Idealnie byłoby, gdybyśmy kuloodporni byli na nasze typy klas, tak aby obiekt albo nie mógł zostać wprowadzony w nieprawidłowy stan, albo mógł natychmiast zasygnalizować, że tak jest (zamiast pozwalać, aby niezdefiniowane zachowanie wystąpiło w losowym momencie w przyszłości).
Struktury (jako agregaty) po prostu nie mają mechaniki wymaganej do eleganckiego rozwiązania tego problemu.
Wprowadzenie do zajęć
Tworząc C++, Bjarne Stroustrup chciał wprowadzić możliwości, które umożliwiłyby programistom tworzenie typów zdefiniowanych w programie, których można było używać bardziej intuicyjnie. Interesowało go także znalezienie eleganckich rozwiązań niektórych częstych pułapek i wyzwań związanych z konserwacją, które nękają duże, złożone programy (takie jak wspomniany wcześniej problem niezmiennika klas).
Czerpiąc ze swoich doświadczeń z innymi językami programowania (szczególnie Simula, pierwszym obiektowym językiem programowania), Bjarne był przekonany, że możliwe jest opracowanie typu zdefiniowanego w programie, który byłby na tyle ogólny i potężny, że można go było używać niemal do wszystkiego. W ukłonie w stronę Simuli nazwał ten typ a klasa.
Podobnie jak struktury, a klasa jest typem złożonym zdefiniowanym przez program, który może mieć wiele zmiennych składowych różnych typów.
Kluczowa informacja
Z technicznego punktu widzenia struktury i klasy są prawie identyczne — dlatego każdy przykład zaimplementowany przy użyciu struktury może zostać zaimplementowany przy użyciu klasy i odwrotnie. Jednak z praktycznego punktu widzenia struktur i klas używamy inaczej.
Na lekcjach omawiamy zarówno techniczne, jak i praktyczne różnice pomiędzy strukturami i klasami 14.5 -- Członkowie publiczni i prywatni oraz specyfikatory dostępu
Powiązana treść
Na lekcji omawiamy, jak klasy rozwiązują problem niezmienniczy 14.8 — Korzyści z ukrywania danych (hermetyzowania).
Definiowanie klasy
Ponieważ klasa jest typem danych zdefiniowanym przez program, należy ją zdefiniować, zanim będzie można jej użyć. Klasy definiuje się podobnie jak struktury, z tą różnicą, że używamy metody class słowo kluczowe zamiast struct. Oto na przykład definicja prostej klasy pracowników:
class Employee
{
int m_id {};
int m_age {};
double m_wage {};
};Powiązana treść
W nadchodzącej lekcji omówimy, dlaczego zmienne składowe klasy są często poprzedzane literą „m_”. 14.5 -- Członkowie publiczni i prywatni oraz specyfikatory dostępu
Aby zademonstrować, jak podobne mogą być klasy i struktury, następujący program jest odpowiednikiem tego, który zaprezentowaliśmy na początku lekcji, ale Date jest teraz klasą, a nie strukturą:
#include <iostream>
class Date // we changed struct to class
{
public: // and added this line, which is called an access specifier
int m_day{}; // and added "m_" prefixes to each of the member names
int m_month{};
int m_year{};
};
void printDate(const Date& date)
{
std::cout << date.m_day << '/' << date.m_month << '/' << date.m_year;
}
int main()
{
Date date{ 4, 10, 21 };
printDate(date);
return 0;
}Wypisuje:
4/10/21
Powiązana treść
W nadchodzącej lekcji omówimy, czym jest specyfikator dostępu 14.5 -- Członkowie publiczni i prywatni oraz specyfikatory dostępu.
Większość standardowej biblioteki C++ to klasy
Używałeś już obiektów klas, być może nie zdając sobie z tego sprawy. Obydwa std::string i std::string_view są zdefiniowane jako klasy. Tak naprawdę większość typów niealiasowych w bibliotece standardowej definiuje się jako klasy!
Klasy są sercem i duszą C++ — są tak fundamentalne, że C++ pierwotnie otrzymało nazwę „C z klasami”! Kiedy już zaznajomisz się z klasami, większość czasu w C++ spędzisz na pisaniu, testowaniu i używaniu ich.
Czas quizu
Pytanie nr 1
Biorąc pod uwagę pewien zestaw wartości (wiek, numery adresów itp.), możemy chcieć wiedzieć, jakie są minimalne i maksymalne wartości w tym zestawie. Ponieważ wartości minimalne i maksymalne są ze sobą powiązane, możemy zorganizować je w strukturę w następujący sposób:
struct minMax
{
int min; // holds the minimum value seen so far
int max; // holds the maximum value seen so far
};Jednakże, jak napisano, ta struktura ma nieokreślony niezmiennik klasy. Co to jest niezmiennik?

