W tym rozdziale omówiliśmy istotę C++ — klasy! To najważniejszy rozdział w serii tutoriali, ponieważ przygotowuje grunt pod większość tego, co jeszcze przed nami.
Przegląd rozdziału
W programowaniu proceduralnym nacisk położony jest na tworzenie „procedur” (które w C++ nazywane są funkcjami), które implementują logikę naszego programu. Do tych funkcji przekazujemy obiekty danych, funkcje te wykonują operacje na danych, a następnie potencjalnie zwracają wynik do wykorzystania przez osobę wywołującą.
Przy Programowanie obiektowe (często w skrócie OOP), nacisk kładziony jest na tworzenie zdefiniowanych przez program typów danych, które zawierają zarówno właściwości, jak i zestaw dobrze zdefiniowanych zachowań.
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.
A klasa to zdefiniowany przez program typ złożony, który łączy w sobie zarówno dane, jak i funkcje działające na tych danych.
Funkcje należące do typu klasy to zwane funkcjami składowymi. Obiekt, na którym wywoływana jest funkcja składowa, jest często nazywany obiektem ukrytym. Funkcje, które nie są funkcjami składowymi, nazywane są funkcjami nieskładkowymi w celu odróżnienia ich od funkcji składowych. Jeśli Twój typ klasy nie ma elementów członkowskich danych, wolisz zamiast tego używać przestrzeni nazw.
A funkcja składowa const jest funkcją składową, która gwarantuje, że nie zmodyfikuje obiektu ani nie wywoła żadnych funkcji składowych innych niż stałe (ponieważ mogą one modyfikować obiekt). Funkcja członkowska, która nie modyfikuje (i nigdy nie będzie) modyfikować stanu obiektu, powinna mieć postać const, aby można ją było wywoływać zarówno na obiektach innych niż const, jak i const.
Każdy członek typu klasy ma właściwość zwaną poziomem dostępu , która określa, kto może uzyskać dostęp do tego elementu. System poziomów dostępu jest czasami nieformalnie nazywany kontrolą dostępu. Poziomy dostępu są definiowane dla poszczególnych klas, a nie dla poszczególnych obiektów.
Członkowie publiczni to członkowie typu klasy, do którego nie ma żadnych ograniczeń dostępu. Dostęp do członków publicznych może uzyskać każdy (o ile są objęci zakresem). Dotyczy to także innych członków tej samej klasy. Dostęp do elementów publicznych można uzyskać także publicznie, co nazywamy kodem istniejącym poza członkami danego typu klasy. Przykłady elementów publicznych obejmują funkcje niebędące członkami, a także elementy innych typów klas.
Domyślnie wszyscy członkowie struktury są członkami publicznymi.
Członkowie prywatni są członkami typu klasy, do którego dostęp mają tylko inni członkowie tej samej klasy.
Domyślnie członkowie klasy są prywatni. Klasa zawierająca elementy prywatne nie jest już agregacją i dlatego nie może już używać inicjalizacji agregowanej. Rozważ nadanie nazw swoim prywatnym członkom zaczynając od przedrostka „m_”, aby ułatwić odróżnienie ich od nazw zmiennych lokalnych, parametrów funkcji i funkcji składowych.
Możemy jawnie ustawić poziom dostępu naszych członków, używając specyfikatora dostępu. Struktury powinny generalnie unikać używania specyfikatorów dostępu, tak aby wszystkie elementy członkowskie miały domyślnie wartość public.
An funkcja dostępu to trywialna publiczna funkcja składowa, której zadaniem jest pobranie lub zmiana wartości prywatnej zmiennej składowej. Funkcje dostępu występują w dwóch odmianach: pobierające i ustawiające. Gettery (czasami nazywane akcesorami) to publiczne funkcje składowe, które zwracają wartość prywatnego składowej zmienna. Settery (czasami nazywane mutatorami) to publiczne funkcje składowe, które ustawiają wartość prywatnej zmiennej składowej.
Klasa interfejs typu klasy definiuje, w jaki sposób użytkownik typu klasy będzie współdziałał 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 czasami interfejsem publicznym.
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.
W programowaniu ukrywanie danych (tzw ukrywanie informacji lub abstrakcja danych) to technika używana do wymuszenia oddzielenia interfejsu i implementacji poprzez ukrycie implementacji typu danych zdefiniowanego w programie przed użytkowników.
Termin hermetyzacja jest czasami używany także w odniesieniu do ukrywania danych. Jednak termin ten jest również używany w odniesieniu do łączenia danych i funkcji (bez względu na kontrolę dostępu), więc jego użycie może być niejednoznaczne.
Definiując klasę, wolisz najpierw zadeklarować elementy publiczne, a na końcu prywatne. Zwraca to uwagę na interfejs publiczny i umniejsza szczegóły implementacji.
A constructor to specjalna funkcja składowa używana do inicjowania obiektów typu klasowego. Aby utworzyć obiekt typu klasy niezagregowanej, należy znaleźć pasujący konstruktor.
A Lista inicjatorów elementów członkowskich umożliwia inicjowanie zmiennych składowych z poziomu konstruktora. Zmienne członkowskie na liście inicjatorów składowych powinny być wymienione w kolejności, w jakiej są zdefiniowane w klasie. Wolę używać listy inicjatorów elementów do inicjowania elementów zamiast przypisywać wartości w treści konstruktora.
Konstruktor, który nie przyjmuje żadnych parametrów (lub ma wszystkie parametry domyślne), nazywany jest konstruktorem domyślnym. Konstruktor domyślny jest używany, jeśli użytkownik nie podał wartości inicjujących. Jeśli obiekt typu klasy niezagregowanej nie ma konstruktorów zadeklarowanych przez użytkownika, kompilator wygeneruje konstruktor domyślny (aby klasa mogła być inicjowana wartościowo lub domyślnie). Konstruktor ten nazywany jest niejawnym konstruktorem domyślnym.
Konstruktory mogą delegować inicjalizację innemu konstruktorowi z tego samego typu klasy. Proces ten nazywany jest czasami łączeniem konstruktorów , a takie konstruktory nazywane są konstruktorami delegującymi. Konstruktory mogą delegować lub inicjować, ale nie jedno i drugie.
A obiekt tymczasowy (czasami nazywany obiektem anonimowym lub obiektem bez nazwy) to obiekt, który nie ma nazwy i istnieje tylko przez czas trwania pojedynczego wyrażenia.
A konstruktor kopiujący to konstruktor używany do inicjowania obiektu za pomocą istniejącego obiektu tego samego typu. Jeśli nie udostępnisz konstruktora kopiującego dla swoich klas, C++ utworzy dla ciebie publiczny niejawny konstruktor kopiujący , który wykonuje inicjalizację członkowską.
Klasa reguła as-if mówi, że kompilator może modyfikować program według własnego uznania, aby wygenerować bardziej zoptymalizowany kod, o ile te modyfikacje nie wpływają na „obserwowalne zachowanie programu”. Jedynym wyjątkiem od reguły „jak gdyby” jest elizja kopiowania. Elizja kopiowania to technika optymalizacji kompilatora, która pozwala kompilatorowi usunąć niepotrzebne kopiowanie obiektów. Kiedy kompilator optymalizuje wywołanie konstruktora kopiującego, mówimy, że konstruktor został pominięty.
Napisana przez nas funkcja konwersji wartości na typ zdefiniowany w programie lub z typu zdefiniowanego przez program nazywa się konwersją zdefiniowaną przez użytkownika. Konstruktor, którego można użyć do wykonania niejawnej konwersji, nazywany jest konstruktorem konwertującym. Domyślnie wszystkie konstruktory konwertują konstruktory.
Możemy użyć jawnego słowa kluczowego, aby poinformować kompilator, że konstruktor nie powinien być używany jako konstruktor konwertujący. Takiego konstruktora nie można używać do inicjalizacji kopiowania ani inicjalizacji listy kopiowania, ani nie można go używać do wykonywania niejawnych konwersji.
Domyślnie ustaw dowolny konstruktor, który akceptuje pojedynczy argument, jako jawny. Jeśli niejawna konwersja między typami jest zarówno semantycznie równoważna, jak i wydajna (na przykład konwersja z std::string Do std::string_view), możesz rozważyć uczynienie konstruktora niejawnym. Nie należy jawnie kopiować ani przenosić konstruktorów, ponieważ nie wykonują one konwersji.
Funkcje składowe (w tym konstruktory) mogą być constexpr. Począwszy od C++ 14, funkcje składowe constexpr nie są domyślnie stałe.
Czas quizu
Nota autora
Quiz o blackjacku, który był częścią tej lekcji, został przeniesiony do lekcji 17.x — Podsumowanie i quiz z rozdziału 17.
Pytanie nr 1
a) Napisz klasę o nazwie Point2d. Point2d powinna zawierać dwie zmienne składowe typu double: m_x, I m_y, obie domyślne do 0.0.
Dostarcz konstruktor i a print() .
Powinien uruchomić się następujący program:
#include <iostream>
int main()
{
Point2d first{};
Point2d second{ 3.0, 4.0 };
// Point2d third{ 4.0 }; // should error if uncommented
first.print();
second.print();
return 0;
}To powinno zostać wydrukowane:
Point2d(0, 0) Point2d(3, 4)
b) Teraz dodaj funkcję składową o nazwie distanceTo() , która przyjmuje inny Point2d jako parametr i oblicza odległość między nimi. Mając dwa punkty (x1, y1) i (x2, y2), odległość między nimi można obliczyć za pomocą wzoru std::sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)). Słowo kluczowe std::sqrt funkcja mieszka w nagłówku cmath.
Powinien uruchomić się następujący program:
#include <cmath>
#include <iostream>
int main()
{
Point2d first{};
Point2d second{ 3.0, 4.0 };
first.print();
second.print();
std::cout << "Distance between two points: " << first.distanceTo(second) << '\n';
return 0;
}To powinno zostać wydrukowane:
Point2d(0, 0) Point2d(3, 4) Distance between two points: 5
Pytanie nr 2
W lekcji 13.10 -- Przekazywanie i zwracanie struktur, napisaliśmy krótki program przy użyciu struktury Fraction . Rozwiązanie referencyjne wygląda następująco:
#include <iostream>
struct Fraction
{
int numerator{ 0 };
int denominator{ 1 };
};
Fraction getFraction()
{
Fraction temp{};
std::cout << "Enter a value for numerator: ";
std::cin >> temp.numerator;
std::cout << "Enter a value for denominator: ";
std::cin >> temp.denominator;
std::cout << '\n';
return temp;
}
Fraction multiply(const Fraction& f1, const Fraction& f2)
{
return { f1.numerator * f2.numerator, f1.denominator * f2.denominator };
}
void printFraction(const Fraction& f)
{
std::cout << f.numerator << '/' << f.denominator << '\n';
}
int main()
{
Fraction f1{ getFraction() };
Fraction f2{ getFraction() };
std::cout << "Your fractions multiplied together: ";
printFraction(multiply(f1, f2));
return 0;
}Konwertuj Fraction ze struktury na klasę. Konwertuj wszystkie funkcje na (niestatyczne) funkcje składowe.
Nota autora
Uwaga: ten quiz nie jest zgodny z najlepszymi praktykami dotyczącymi tego, kiedy należy używać funkcji niebędącej składową, a kiedy należy. Celem jest sprawdzenie, czy rozumiesz, jak konwertować funkcje niebędące składowymi na funkcje składowe.
Pytanie nr 3
Dlaczego w poprzednim rozwiązaniu quizu utworzono konstruktor Fraction explicit?
Pytanie nr 4
W poprzednim pytaniu quizu, dlaczego lepiej byłoby pozostawić getFraction() i print() jako nie-członek?

