W tym rozdziale omawialiśmy tematy związane z operatorami przeciążanie, a także przeciążone rzutowania typów i tematy związane z konstruktorem kopiującym.
Streszczenie
Przeciążanie operatorów to odmiana przeciążania funkcji, która pozwala przeciążać operatorów klas. Gdy operatory są przeciążone, intencje operatorów powinny być możliwie najbliższe pierwotnej intencji operatorów. Jeśli znaczenie operatora zastosowanego do klasy niestandardowej nie jest jasne i intuicyjne, użyj zamiast tego funkcji nazwanej.
Operatory mogą być przeciążane jako funkcja normalna, funkcja zaprzyjaźniona lub funkcja składowa. Poniższe praktyczne zasady mogą pomóc w określeniu, która forma jest najlepsza w danej sytuacji:
- Jeśli przeciążasz przypisanie (=), indeks dolny ([]), wywołanie funkcji (()) lub wybór elementu członkowskiego (->), zrób to jako funkcję składową.
- Jeśli przeciążasz operator jednoargumentowy, zrób to jako funkcję składową.
- Jeśli przeciążasz operator binarny, który modyfikuje jego lewy operand (np. operator+=), rób to jako funkcję składową, jeśli możesz.
- Jeśli przeciążasz operator binarny, który nie modyfikuje jego lewego operandu (np. operator+), zrób to jako normalną funkcję lub funkcję zaprzyjaźnioną.
Rysowanie typów może być przeciążane, aby zapewnić funkcje konwersji, których można użyć do jawnej lub niejawnej konwersji klasy na inny typ.
Konstruktor kopiujący to specjalny typ konstruktora używany do inicjowania obiektu z innego obiektu tego samego typu. Konstruktory kopiujące służą do bezpośredniej/jednolitej inicjalizacji z obiektu tego samego typu, inicjalizacji kopiowania (Frakcja f = Frakcja(5,3)) oraz podczas przekazywania lub zwracania parametru według wartości.
Jeśli nie podasz konstruktora kopiującego, kompilator go utworzy. Konstruktory kopiujące dostarczone przez kompilator będą używać inicjalizacji członkowskiej, co oznacza, że każdy członek kopii jest inicjowany z oryginalnego elementu członkowskiego. Konstruktor kopiujący może zostać pominięty w celach optymalizacji, nawet jeśli ma skutki uboczne, więc nie polegaj na tym, że twój konstruktor kopiujący faktycznie się wykonuje.
Konstruktory są domyślnie uważane za konstruktory konwertujące, co oznacza, że kompilator użyje ich do niejawnej konwersji obiektów innych typów na obiekty twojej klasy. Można tego uniknąć, używając jawnego słowa kluczowego przed konstruktorem. W razie potrzeby możesz także usuwać funkcje w swojej klasie, w tym konstruktor kopiujący i przeciążony operator przypisania. Spowoduje to błąd kompilatora, jeśli zostanie wywołana usunięta funkcja.
Operator przypisania może zostać przeciążony, aby umożliwić przypisanie do Twojej klasy. Jeśli nie podasz przeciążonego operatora przypisania, kompilator go utworzy. Przeciążone operatory przypisania powinny zawsze uwzględniać kontrolę samodzielnego przypisania (chyba że jest to obsługiwane w sposób naturalny lub używasz idiomu kopiowania i zamiany).
Nowi programiści często mylą się, gdy używany jest operator przypisania z konstruktorem kopiowania, ale jest to dość proste:
- Jeśli nowy obiekt musi zostać utworzony, zanim nastąpi kopiowanie, używany jest konstruktor kopiujący (uwaga: obejmuje to przekazywanie lub zwracanie obiektów przez wartość).
- Jeśli nie ma potrzeby tworzenia nowego obiektu, zanim nastąpi kopiowanie, używany jest operator przypisania.
Domyślnie konstruktor kopiujący i operatory przypisania dostarczone przez kompilator wykonują inicjalizację lub przypisanie członkowskie, co jest płytką kopią. Jeśli twoja klasa dynamicznie przydziela pamięć, prawdopodobnie doprowadzi to do problemów, ponieważ wiele obiektów będzie wskazywało tę samą przydzieloną pamięć. W takim przypadku musisz je wyraźnie zdefiniować, aby wykonać głęboką kopię. Jeszcze lepiej, jeśli możesz, unikaj samodzielnego zarządzania pamięcią i korzystaj z klas ze standardowej biblioteki.
Czas quizu
Pytanie nr 1
- Zakładając, że
Point jest klasą i point jest instancją tej klasy, czy powinieneś użyć przeciążenia funkcji normalnej/przyjacielskiej lub składowej dla następujących operatorów?
A) point + point
Pokaż rozwiązanie
binarny operator+ najlepiej jest zaimplementowany jako funkcja normalna/przyjacielska.
B) -point
Pokaż rozwiązanie
jednoargumentowy operator- najlepiej jest zaimplementowany jako element członkowski funkcja.
C) std::cout << point
Pokaż rozwiązanie
operator<< musi być zaimplementowana jako funkcja normalna/przyjacielska.
D) point = 5;
Pokaż rozwiązanie
operator= musi być zaimplementowana jako funkcja składowa.
Pytanie nr 2
Napisz klasę o nazwie Średnia, która będzie śledzić średnią wszystkich przekazanych do niej liczb całkowitych. Użyj dwóch elementów: pierwszy powinien być typu std::int32_t i używany do śledzenia sumy wszystkich liczb, które widziałeś do tej pory. Drugi powinien śledzić, ile liczb widziałeś do tej pory. Można je podzielić, aby znaleźć średnią.
a) Napisz wszystkie funkcje niezbędne do uruchomienia poniższego programu:
int main()
{
Average avg{};
std::cout << avg << '\n';
avg += 4;
std::cout << avg << '\n'; // 4 / 1 = 4
avg += 8;
std::cout << avg << '\n'; // (4 + 8) / 2 = 6
avg += 24;
std::cout << avg << '\n'; // (4 + 8 + 24) / 3 = 12
avg += -10;
std::cout << avg << '\n'; // (4 + 8 + 24 - 10) / 4 = 6.5
(avg += 6) += 10; // 2 połączenia połączone w łańcuch
std::cout << avg << '\n'; // (4 + 8 + 24 - 10 + 6 + 10) / 6 = 7
Average copy{ avg };
std::cout << copy << '\n';
return 0;
}
i wygeneruj wynik:
0
4
6
12
6.5
7
7
Pokaż rozwiązanie
#include <iostream>
#include <cstdint> // dla liczb całkowitych o stałej szerokości
class Average
{
private:
std::int32_t m_total{ 0 }; // suma wszystkich liczb, które do tej pory widzieliśmy
int m_numbers{ 0 }; // liczba liczb, które widzieliśmy do tej pory
public:
Average()
{
}
friend std::ostream& operator<<(std::ostream& out, const Average& average)
{
// Zajmij się przypadkiem, w którym nie widzieliśmy jeszcze żadnych liczb
if (average.m_numbers == 0)
{
out << 0;
return out;
}
// Nasza średnia to suma liczb, które widzieliśmy, podzielona przez liczbę liczb, które widzieliśmy
// Musimy pamiętać, aby tutaj wykonać dzielenie zmiennoprzecinkowe, a nie dzielenie przez liczby całkowite
out << static_cast<double>(average.m_total) / average.m_numbers;
return out;
}
// Ponieważ operator+= modyfikuje jego lewą stronę operand, napiszemy go jako element
Average& operator+=(std::int32_t num)
{
// Zwiększ naszą sumę o nową liczbę
m_total += num;
// I zwiększ liczbę o 1
++m_numbers;
// zwraca *to na wypadek, gdyby ktoś chciał połączyć += w łańcuch razem
return *this;
}
};
int main()
{
Average avg{};
std::cout << avg << '\n';
avg += 4;
std::cout << avg << '\n';
avg += 8;
std::cout << avg << '\n';
avg += 24;
std::cout << avg << '\n';
avg += -10;
std::cout << avg << '\n';
(avg += 6) += 10; // 2 połączenia połączone w łańcuch
std::cout << avg << '\n';
Average copy{ avg };
std::cout << copy << '\n';
return 0;
}
b) Czy dla tej klasy należy udostępnić zdefiniowany przez użytkownika konstruktor kopiujący lub operator przypisania?
Pokaż rozwiązanie
Nie. Ponieważ użycie inicjalizacji/kopiowania członkowskiego jest tutaj w porządku, dopuszczalne jest użycie ustawień domyślnych dostarczonych przez kompilator.
c) Dlaczego użycie std::int32_t zamiast int?
Pokaż rozwiązanie
int może mieć 16 bitów na niektórych platformach, co oznaczałoby, że nasz Average obiekt może mieć maksymalną wartość licznika wynoszącą 32 767. Użycie std::int32_t zagwarantuje 32-bitową wartość całkowitą, która daje nam znacznie większy zakres pracy.
Pytanie nr 3
Napisz od zera własną klasę tablicy liczb całkowitych o nazwie IntArray (nie używaj std::array ani std::vector). Użytkownicy powinni podać rozmiar tablicy podczas jej tworzenia, a tablica powinna być alokowana dynamicznie. Używaj instrukcji asercji, aby chronić się przed złymi danymi. Utwórz dowolne konstruktory lub przeciążone operatory potrzebne do poprawnego działania następującego programu:
#include <iostream>
IntArray fillArray()
{
IntArray a(5);
a[0] = 5;
a[1] = 8;
a[2] = 2;
a[3] = 3;
a[4] = 6;
return a;
}
int main()
{
IntArray a{ fillArray() };
std::cout << a << '\n';
auto& ref{ a }; // używamy tego odniesienia, aby uniknąć błędów przy samodzielnym przypisaniu kompilatora
a = ref;
IntArray b(1);
b = a;
a[4] = 7;
std::cout << b << '\n';
return 0;
}
Ten program powinien wyświetlić:
5 8 2 3 6
5 8 2 3 6
Pokaż rozwiązanie
#include <iostream>
#include <cassert> // dla potwierdzenia
class IntArray
{
private:
int m_length{ 0 };
int* m_array{ nullptr };
public:
explicit IntArray(int length)
: m_length{ length }
{
assert(length > 0 && "IntArray length should be a positive integer");
m_array = new int[static_cast<std::size_t>(m_length)] {};
}
// Kopiuj konstruktor wykonujący głęboką kopię
IntArray(const IntArray& array)
: m_length{ array.m_length }
{
// Przydziel nową tablicę
m_array = new int[static_cast<std::size_t>(m_length)] {};
// Kopiuj elementy z oryginalnej tablicy do nowej tablicy
for (int count{ 0 }; count < array.m_length; ++count)
m_array[count] = array.m_array[count];
}
~IntArray()
{
delete[] m_array;
}
// Jeśli otrzymujesz tutaj szalone wartości, prawdopodobnie zapomniałeś wykonać głęboką kopię w konstruktorze kopiującym
friend std::ostream& operator<<(std::ostream& out, const IntArray& array)
{
for (int count{ 0 }; count < array.m_length; ++count)
{
out << array.m_array[count] << ' ';
}
return out;
}
int& operator[] (const int index)
{
assert(index >= 0);
assert(index < m_length);
return m_array[index];
}
// Operator przypisania wykonujący głęboką kopię
IntArray& operator= (const IntArray& array)
{
// ochroniarz z własnym przydziałem
if (this == &array)
return *this;
// Jeśli ta tablica już istnieje, usuń ją, aby nie wyciekać pamięci
delete[] m_array;
m_length = array.m_length;
// Przydziel nową tablicę
m_array = new int[static_cast<std::size_t>(m_length)] {};
// Kopiuj elementy z oryginalnej tablicy do nowej tablicy
for (int count{ 0 }; count < array.m_length; ++count)
m_array[count] = array.m_array[count];
return *this;
}
};
IntArray fillArray()
{
IntArray a(5);
a[0] = 5;
a[1] = 8;
a[2] = 2;
a[3] = 3;
a[4] = 6;
return a;
}
int main()
{
IntArray a{ fillArray() };
// Jeśli otrzymujesz tutaj szalone wartości, prawdopodobnie zapomniałeś wykonać głęboką kopię w konstruktorze kopiującym
std::cout << a << '\n';
auto& ref{ a }; // używamy tego odniesienia, aby uniknąć błędów przy samodzielnym przypisaniu kompilatora
a = ref;
IntArray b(1);
b = a;
a[4] = 7; // zmień wartość w a, b nie powinna zmieniać
// Jeśli otrzymujesz tutaj szalone wartości, prawdopodobnie zapomniałeś sprawdzić samodzielne przypisanie
// Jeśli b kończy się na 7, prawdopodobnie zapomniałeś wykonać głęboką kopię w swoim zadaniu kopiowania
std::cout << b << '\n';
return 0;
}
Pytanie nr 4
Dodatkowy kredyt: To jest trochę trudniejsze.
Liczba zmiennoprzecinkowa to liczba z ułamkiem dziesiętnym, gdzie liczba cyfr po przecinku może być zmienna. Liczba stałoprzecinkowa to liczba ze składnikiem ułamkowym, w której liczba cyfr w części ułamkowej jest stała.
W tym quizie napiszemy zajęcia, które zaimplementują liczbę stałoprzecinkową zawierającą dwie cyfry ułamkowe (np. 12,34, 3,00 lub 1278,99). Załóżmy, że zakres klasy powinien wynosić od -32768,99 do 32767,99, że składnik ułamkowy powinien zawierać dowolne dwie cyfry, że nie chcemy błędów precyzji i że chcemy zaoszczędzić miejsce.
> Krok #1
Jakiego typu zmiennych składowych powinniśmy użyć, aby zaimplementować naszą liczbę stałoprzecinkową z 2 cyframi po przecinku? (Upewnij się, że przeczytałeś odpowiedź, zanim przejdziesz do kolejnych pytań)
Pokaż rozwiązanie
Istnieje wiele różnych sposobów wdrożenia liczby stałoprzecinkowej. Ponieważ liczba stałoprzecinkowa jest zasadniczo podprzypadkiem liczby zmiennoprzecinkowej (gdzie liczba cyfr po przecinku jest stała, a nie zmienna), użycie liczby zmiennoprzecinkowej może wydawać się oczywistym wyborem. Ale liczby zmiennoprzecinkowe mają problemy z precyzją. Mając stałą liczbę cyfr dziesiętnych, możemy rozsądnie wyliczyć wszystkie możliwe wartości ułamkowe (w naszym przypadku od 0,00 do 0,99), więc użycie typu danych, który ma problemy z precyzją, nie jest najlepszym wyborem.
Lepszym rozwiązaniem byłoby użycie 16-bitowej liczby całkowitej ze znakiem do przechowywania nieułamkowej części liczby i 8-bitowej liczby całkowitej ze znakiem do przechowywania ułamka komponent.
> Krok #2
Napisz klasę o nazwie FixedPoint2 , która implementuje rozwiązanie zalecane z poprzedniego pytania. Jeśli jedna (lub obie) części nieułamkowej i ułamkowej liczby są ujemne, liczbę należy traktować jako ujemną. Podaj przeciążone operatory i konstruktory wymagane do uruchomienia następującego programu. Na razie nie przejmuj się, że część ułamkowa jest poza zakresem (>99 lub <-99).
#include <cassert>
#include <iostream>
int main()
{
FixedPoint2 a{ 34, 56 };
std::cout << a << '\n';
std::cout << static_cast<double>(a) << '\n';
assert(static_cast<double>(a) == 34.56);
FixedPoint2 b{ -2, 8 };
assert(static_cast<double>(b) == -2.08);
FixedPoint2 c{ 2, -8 };
assert(static_cast<double>(c) == -2.08);
FixedPoint2 d{ -2, -8 };
assert(static_cast<double>(d) == -2.08);
FixedPoint2 e{ 0, -5 };
assert(static_cast<double>(e) == -0.05);
FixedPoint2 f{ 0, 10 };
assert(static_cast<double>(f) == 0.1);
return 0;
}
Ten program powinien dać wynik:
34.56
34.56
Wskazówka: Aby wyprowadzić liczbę, static_rzuć ją na wartość podwójną.
Pokaż rozwiązanie
#include <cassert>
#include <cstdint> // dla liczb całkowitych o stałej szerokości
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // Oto nasza część nieułamkowa
std::int8_t m_decimal{}; // Oto nasza część frakcyjna
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// Jeśli którakolwiek (lub obie) części nieułamkowej i ułamkowej liczby są ujemne, liczbę należy traktować jako ujemny
if (m_base < 0 || m_decimal < 0)
{
// Upewnij się, że podstawa jest ujemna
if (m_base > 0)
m_base = -m_base;
// Upewnij się, że liczba dziesiętna jest ujemna
if (m_decimal > 0)
m_decimal = -m_decimal;
}
}
explicit operator double() const
{
return m_base + (static_cast<double>(m_decimal) / 100);
}
};
// Nie wymaga to dostępu do wewnętrznych elementów class, więc można go zdefiniować poza klasą
std::ostream& operator<<(std::ostream& out, const FixedPoint2& fp)
{
out << static_cast<double>(fp);
return out;
}
int main()
{
FixedPoint2 a{ 34, 56 };
std::cout << a << '\n';
std::cout << static_cast<double>(a) << '\n';
assert(static_cast<double>(a) == 34.56);
FixedPoint2 b{ -2, 8 };
assert(static_cast<double>(b) == -2.08);
FixedPoint2 c{ 2, -8 };
assert(static_cast<double>(c) == -2.08);
FixedPoint2 d{ -2, -8 };
assert(static_cast<double>(d) == -2.08);
FixedPoint2 e{ 0, -5 };
assert(static_cast<double>(e) == -0.05);
FixedPoint2 f{ 0, 10 };
assert(static_cast<double>(f) == 0.1);
return 0;
}
> Krok #3
Zajmijmy się teraz przypadkiem, w którym część ułamkowa jest poza zakresem. Mamy tu dwie rozsądne strategie:
- Zamknij część ułamkową (jeśli >99, ustaw na 99).
- Potraktuj przepełnienie jako istotne (jeśli >99, zmniejsz o 100 i dodaj 1 do podstawy).
W tym ćwiczeniu potraktujemy przepełnienie dziesiętne jako istotne, ponieważ będzie to przydatne w następnym kroku.
Poniższe powinny run:
#include <cassert>
#include <iostream>
// Będziesz musiał to zrobić testDecimal znajomy FixPoint2
// aby funkcja mogła uzyskać dostęp do prywatnych elementów StałegoPoint2
bool testDecimal(const FixedPoint2 &fp)
{
if (fp.m_base >= 0)
return fp.m_decimal >= 0 && fp.m_decimal < 100;
else
return fp.m_decimal <= 0 && fp.m_decimal > -100;
}
int main()
{
FixedPoint2 a{ 1, 104 };
std::cout << a << '\n';
std::cout << static_cast<double>(a) << '\n';
assert(static_cast<double>(a) == 2.04);
assert(testDecimal(a));
FixedPoint2 b{ 1, -104 };
assert(static_cast<double>(b) == -2.04);
assert(testDecimal(b));
FixedPoint2 c{ -1, 104 };
assert(static_cast<double>(c) == -2.04);
assert(testDecimal(c));
FixedPoint2 d{ -1, -104 };
assert(static_cast<double>(d) == -2.04);
assert(testDecimal(d));
return 0;
}
I wygeneruj wynik:
2.04
2.04
Pokaż rozwiązanie
#include <cassert>
#include <cstdint> // dla liczb całkowitych o stałej szerokości
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // Oto nasza część nieułamkowa
std::int8_t m_decimal{}; // Oto nasza część frakcyjna
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// Jeśli którakolwiek (lub obie) części nieułamkowej i ułamkowej liczby są ujemne, liczbę należy traktować jako ujemny
if (m_base < 0 || m_decimal < 0)
{
// Upewnij się, że podstawa jest ujemna
if (m_base > 0)
m_base = -m_base;
// Upewnij się, że liczba dziesiętna jest ujemna
if (m_decimal > 0)
m_decimal = -m_decimal;
}
// Jeśli liczba dziesiętna jest poza zakresem (w dowolnym kierunku),
// dostosuj przecinek tak, aby mieścił się w granicach,
// i dostosowuję bazę tak, aby uwzględnić liczbę jednostek usuniętych z ułamka dziesiętnego
// h/t do czytnika Davida Pinheiro za uproszczenie tego
m_base += m_decimal / 100; // integer division
m_decimal = m_decimal % 100; // integer remainder
}
explicit operator double() const
{
return m_base + (static_cast<double>(m_decimal) / 100);
}
friend bool testDecimal(const FixedPoint2 &fp);
};
// Nie wymaga to dostępu do wewnętrznych elementów class, więc można go zdefiniować poza klasą
std::ostream& operator<<(std::ostream& out, const FixedPoint2& fp)
{
out << static_cast<double>(fp);
return out;
}
// Będziesz musiał to zrobić testDecimal znajomy FixPoint2
// aby funkcja mogła uzyskać dostęp do prywatnych elementów StałegoPoint2
bool testDecimal(const FixedPoint2 &fp)
{
if (fp.m_base >= 0)
return fp.m_decimal >= 0 && fp.m_decimal < 100;
else
return fp.m_decimal <= 0 && fp.m_decimal > -100;
}
int main()
{
FixedPoint2 a{ 1, 104 };
std::cout << a << '\n';
std::cout << static_cast<double>(a) << '\n';
assert(static_cast<double>(a) == 2.04);
assert(testDecimal(a));
FixedPoint2 b{ 1, -104 };
assert(static_cast<double>(b) == -2.04);
assert(testDecimal(b));
FixedPoint2 c{ -1, 104 };
assert(static_cast<double>(c) == -2.04);
assert(testDecimal(c));
FixedPoint2 d{ -1, -104 };
assert(static_cast<double>(d) == -2.04);
assert(testDecimal(d));
return 0;
}
> Krok 4
Teraz dodaj konstruktor, który przyjmuje wartość double. Powinien zostać uruchomiony następujący program:
#include <cassert>
#include <iostream>
int main()
{
FixedPoint2 a{ 0.01 };
assert(static_cast<double>(a) == 0.01);
FixedPoint2 b{ -0.01 };
assert(static_cast<double>(b) == -0.01);
FixedPoint2 c{ 1.9 }; // upewnij się, że obsługujemy jednocyfrowy zapis dziesiętny
assert(static_cast<double>(c) == 1.9);
FixedPoint2 d{ 5.01 }; // zapisane jako 5,0099999... więc będziemy musieli zaokrąglić to
assert(static_cast<double>(d) == 5.01);
FixedPoint2 e{ -5.01 }; // zapisaną jako -5,0099999... więc będziemy musieli zaokrąglić to
assert(static_cast<double>(e) == -5.01);
// Zajmij się przypadkiem, gdy liczba dziesiętna argumentu zaokrągli się do 100 (należy zwiększyć podstawę o 1)
FixedPoint2 f { 106.9978 }; // powinna być zapisana z podstawą 107 i dziesiętną 0
assert(static_cast<double>(f) == 107.0);
// Zajmij się przypadkiem, w którym liczba dziesiętna argumentu zaokrągli się do -100 (należy zmniejszyć podstawę o 1)
FixedPoint2 g { -106.9978 }; // powinna być przechowywana z podstawą -107 i dziesiętną 0
assert(static_cast<double>(g) == -107.0);
return 0;
}
Zalecenie: Ten będzie nieco trudny. Zrób to w trzech krokach. Najpierw rozwiąż przypadki, w których parametr double można przedstawić bezpośrednio (zmienne a do c powyżej). Następnie zaktualizuj swój kod, aby obsługiwał przypadki, w których parametr double zawiera błąd zaokrąglenia (zmienne d & e). Zmienne f i g powinny być obsługiwane przez obsługę przepełnienia, którą dodaliśmy w poprzednim kroku.
We wszystkich przypadkach: Pokaż wskazówkę
Wskazówka: Możesz przenieść cyfrę z prawej strony ułamka dziesiętnego na lewo od ułamka dziesiętnego, mnożąc przez 10. Pomnóż przez 100, aby przenieść się o dwa miejsca.
Dla zmiennych a do c: Pokaż wskazówkę
Wskazówka: Nieułamkową część liczby podwójnej można uzyskać poprzez statyczne rzutowanie liczby podwójnej na liczbę całkowitą. Aby otrzymać część ułamkową, możesz odjąć część podstawową.
Dla zmiennych d & e: Pokaż wskazówkę
Wskazówka: Możesz zaokrąglić liczbę (na lewo od przecinka) za pomocą funkcji std::round() (zawartej w nagłówku <cmath>) i przyjąć podłogę liczby (w stronę zera) użycie std::trunc().
Pokaż rozwiązanie
#include <cassert>
#include <cmath>
#include <cstdint> // dla liczb całkowitych o stałej szerokości
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // Oto nasza część nieułamkowa
std::int8_t m_decimal{}; // Oto nasza część frakcyjna
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// Jeśli którakolwiek (lub obie) części nieułamkowej i ułamkowej liczby są ujemne, liczbę należy traktować jako ujemny
if (m_base < 0 || m_decimal < 0)
{
// Upewnij się, że podstawa jest ujemna
if (m_base > 0)
m_base = -m_base;
// Upewnij się, że liczba dziesiętna jest ujemna
if (m_decimal > 0)
m_decimal = -m_decimal;
}
// Jeśli liczba dziesiętna jest poza zakresem (w dowolnym kierunku),
// dostosuj przecinek tak, aby mieścił się w granicach,
// i dostosowuję bazę tak, aby uwzględnić liczbę jednostek usuniętych z ułamka dziesiętnego
// h/t do czytnika Davida Pinheiro za uproszczenie tego
m_base += m_decimal / 100; // integer division
m_decimal = m_decimal % 100; // integer remainder
}
// Przedelegujemy do poprzedniego konstruktora, abyśmy nie musieli duplikować liczby ujemnej i logiki obsługi przepełnienia
FixedPoint2(double d) :
FixedPoint2(
static_cast<std::int16_t>(std::trunc(d)),
static_cast<std::int8_t>(std::round(d * 100) - std::trunc(d) * 100)
)
{
}
explicit operator double() const
{
return m_base + (static_cast<double>(m_decimal) / 100);
}
};
// Nie wymaga to dostępu do wewnętrznych elementów class, więc można go zdefiniować poza klasą
std::ostream& operator<<(std::ostream& out, const FixedPoint2& fp)
{
out << static_cast<double>(fp);
return out;
}
int main()
{
FixedPoint2 a{ 0.01 };
std::cout << a << '\n';
assert(static_cast<double>(a) == 0.01);
FixedPoint2 b{ -0.01 };
assert(static_cast<double>(b) == -0.01);
FixedPoint2 c{ 1.9 }; // upewnij się, że obsługujemy jednocyfrowy zapis dziesiętny
assert(static_cast<double>(c) == 1.9);
FixedPoint2 d{ 5.01 }; // zapisane jako 5,0099999... więc będziemy musieli zaokrąglić to
assert(static_cast<double>(d) == 5.01);
FixedPoint2 e{ -5.01 }; // zapisaną jako -5,0099999... więc będziemy musieli zaokrąglić to
assert(static_cast<double>(e) == -5.01);
// Zajmij się przypadkiem, gdy liczba dziesiętna argumentu zaokrągli się do 100 (należy zwiększyć podstawę o 1)
FixedPoint2 f { 106.9978 }; // powinna być zapisana z podstawą 107 i dziesiętną 0
assert(static_cast<double>(f) == 107.0);
// Zajmij się przypadkiem, w którym liczba dziesiętna argumentu zaokrągli się do -100 (należy zmniejszyć podstawę o 1)
FixedPoint2 g { -106.9978 }; // powinna być przechowywana z podstawą -107 i dziesiętną 0
assert(static_cast<double>(g) == -107.0);
return 0;
}
> Krok #5
Przeciążenie operator==, operator>>, operator- (jednoargumentowe) i operator+ (binarne).
Powinien uruchomić się następujący program:
#include <cassert>
#include <iostream>
int main()
{
assert(FixedPoint2{ 0.75 } == FixedPoint2{ 0.75 }); // Test równości
assert(!(FixedPoint2{ 0.75 } == FixedPoint2{ 0.76 })); // Test równości jest fałszywy
// Przetestuj dodatkowe przypadki -- h/t do czytelnika Sharjeel Safdar za te przypadki testowe
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }); // oba dodatni, bez przepełnienia dziesiętnego
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }); // oba dodatnie, z przekroczeniem dziesiętnym
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }); // obie wartości ujemne, brak przepełnienia dziesiętnego
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }); // oba ujemne, z przepełnieniem dziesiętnym
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }); // drugie ujemne, brak przepełnienia dziesiętnego
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }); // drugie ujemne, możliwe przepełnienie dziesiętne
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }); // pierwsza wartość ujemna, brak przepełnienia dziesiętnego
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }); // pierwsza wartość ujemna, możliwe przepełnienie dziesiętne
FixedPoint2 a{ -0.48 };
assert(static_cast<double>(a) == -0.48);
assert(static_cast<double>(-a) == 0.48);
std::cout << "Enter a number: "; // enter 5.678
std::cin >> a;
std::cout << "You entered: " << a << '\n';
assert(static_cast<double>(a) == 5.68);
return 0;
}
Pokaż wskazówkę
Wskazówka: Dodaj swoje dwa FixedPoint2 razem, wykorzystując podwójne rzutowanie, dodając wyniki i konwertując z powrotem do FixedPoint2. To elegancko radzi sobie z przepełnieniem miejsc dziesiętnych.
Pokaż wskazówkę
Wskazówka: W przypadku operator>> użyj konstruktora double, aby utworzyć anonimowy obiekt typu FixedPoint2 i przypisz go do FixedPoint2 parametru funkcji.
Pokaż rozwiązanie
#include <cassert>
#include <cmath>
#include <cstdint> // dla liczb całkowitych o stałej szerokości
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // Oto nasza część nieułamkowa
std::int8_t m_decimal{}; // Oto nasza część frakcyjna
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// Jeśli którakolwiek (lub obie) części nieułamkowej i ułamkowej liczby są ujemne, liczbę należy traktować jako ujemny
if (m_base < 0 || m_decimal < 0)
{
// Upewnij się, że podstawa jest ujemna
if (m_base > 0)
m_base = -m_base;
// Upewnij się, że liczba dziesiętna jest ujemna
if (m_decimal > 0)
m_decimal = -m_decimal;
}
// Jeśli liczba dziesiętna jest poza zakresem (w dowolnym kierunku),
// dostosuj przecinek tak, aby mieścił się w granicach,
// i dostosowuję bazę tak, aby uwzględnić liczbę jednostek usuniętych z ułamka dziesiętnego
// h/t do czytnika Davida Pinheiro za uproszczenie tego
m_base += m_decimal / 100; // integer division
m_decimal = m_decimal % 100; // integer remainder
}
FixedPoint2(double d) :
FixedPoint2(
static_cast<std::int16_t>(std::trunc(d)),
static_cast<std::int8_t>(std::round(d * 100) - std::trunc(d) * 100)
)
{
}
explicit operator double() const
{
return m_base + (static_cast<double>(m_decimal) / 100);
}
friend bool operator==(const FixedPoint2& fp1, const FixedPoint2& fp2)
{
return fp1.m_base == fp2.m_base && fp1.m_decimal == fp2.m_decimal;
}
FixedPoint2 operator-() const
{
// Rzutuj na podwójną, zrób podwójną ujemną, a następnie przekonwertuj z powrotem na StałyPoint2
// h/t do czytnika EmtyC dla tej wersji
return FixedPoint2{ -static_cast<double>(*this) };
}
};
// Nie wymaga to dostępu do wewnętrznych elementów class, więc można go zdefiniować poza klasą
std::ostream& operator<<(std::ostream& out, const FixedPoint2& fp)
{
out << static_cast<double>(fp);
return out;
}
std::istream& operator>>(std::istream& in, FixedPoint2& fp)
{
double d{};
in >> d;
fp = FixedPoint2{ d };
return in;
}
FixedPoint2 operator+(const FixedPoint2& fp1, const FixedPoint2& fp2)
{
return FixedPoint2{ static_cast<double>(fp1) + static_cast<double>(fp2) };
}
int main()
{
assert(FixedPoint2{ 0.75 } == FixedPoint2{ 0.75 }); // Test równości
assert(!(FixedPoint2{ 0.75 } == FixedPoint2{ 0.76 })); // Test równości jest fałszywy
// Przetestuj dodatkowe przypadki -- h/t do czytelnika Sharjeel Safdar za te przypadki testowe
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }); // oba dodatni, bez przepełnienia dziesiętnego
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }); // oba dodatnie, z przekroczeniem dziesiętnym
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }); // obie wartości ujemne, brak przepełnienia dziesiętnego
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }); // oba ujemne, z przepełnieniem dziesiętnym
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }); // drugie ujemne, brak przepełnienia dziesiętnego
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }); // drugie ujemne, możliwe przepełnienie dziesiętne
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }); // pierwsza wartość ujemna, brak przepełnienia dziesiętnego
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }); // pierwsza wartość ujemna, możliwe przepełnienie dziesiętne
FixedPoint2 a{ -0.48 };
assert(static_cast<double>(a) == -0.48);
assert(static_cast<double>(-a) == 0.48);
std::cout << "Enter a number: "; // enter 5.678
std::cin >> a;
std::cout << "You entered: " << a << '\n';
assert(static_cast<double>(a) == 5.68);
return 0;
}