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 calls chained together
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> // for fixed width integers
class Average
{
private:
std::int32_t m_total{ 0 }; // the sum of all numbers we've seen so far
int m_numbers{ 0 }; // the count of numbers we've seen so far
public:
Average()
{
}
friend std::ostream& operator<<(std::ostream& out, const Average& average)
{
// Handle case where we haven't seen any numbers yet
if (average.m_numbers == 0)
{
out << 0;
return out;
}
// Our average is the sum of the numbers we've seen divided by the count of the numbers we've seen
// We need to remember to do a floating point division here, not an integer division
out << static_cast<double>(average.m_total) / average.m_numbers;
return out;
}
// Because operator+= modifies its left operand, we'll write it as a member
Average& operator+=(std::int32_t num)
{
// Increment our total by the new number
m_total += num;
// And increase the count by 1
++m_numbers;
// return *this in case someone wants to chain +='s together
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 calls chained together
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 }; // we're using this reference to avoid compiler self-assignment errors
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> // for assert
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)] {};
}
// Copy constructor that does a deep copy
IntArray(const IntArray& array)
: m_length{ array.m_length }
{
// Allocate a new array
m_array = new int[static_cast<std::size_t>(m_length)] {};
// Copy elements from original array to new array
for (int count{ 0 }; count < array.m_length; ++count)
m_array[count] = array.m_array[count];
}
~IntArray()
{
delete[] m_array;
}
// If you're getting crazy values here you probably forgot to do a deep copy in your copy constructor
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];
}
// Assignment operator that does a deep copy
IntArray& operator= (const IntArray& array)
{
// self-assignment guard
if (this == &array)
return *this;
// If this array already exists, delete it so we don't leak memory
delete[] m_array;
m_length = array.m_length;
// Allocate a new array
m_array = new int[static_cast<std::size_t>(m_length)] {};
// Copy elements from original array to new array
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() };
// If you're getting crazy values here you probably forgot to do a deep copy in your copy constructor
std::cout << a << '\n';
auto& ref{ a }; // we're using this reference to avoid compiler self-assignment errors
a = ref;
IntArray b(1);
b = a;
a[4] = 7; // change value in a, b should not change
// If you're getting crazy values here you probably forgot to do self-assignment check
// If b ends in 7, you probably forgot to do a deep copy in your copy assignment
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> // for fixed width integers
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // here's our non-fractional part
std::int8_t m_decimal{}; // here's our factional part
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// If either (or both) of the non-fractional and fractional part of the number are negative, the number should be treated as negative
if (m_base < 0 || m_decimal < 0)
{
// Make sure base is negative
if (m_base > 0)
m_base = -m_base;
// Make sure decimal is negative
if (m_decimal > 0)
m_decimal = -m_decimal;
}
}
explicit operator double() const
{
return m_base + (static_cast<double>(m_decimal) / 100);
}
};
// This doesn't require access to the internals of the class, so it can be defined outside the class
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>
// You will need to make testDecimal a friend of FixedPoint2
// so the function can access the private members of FixedPoint2
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> // for fixed width integers
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // here's our non-fractional part
std::int8_t m_decimal{}; // here's our factional part
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// If either (or both) of the non-fractional and fractional part of the number are negative, the number should be treated as negative
if (m_base < 0 || m_decimal < 0)
{
// Make sure base is negative
if (m_base > 0)
m_base = -m_base;
// Make sure decimal is negative
if (m_decimal > 0)
m_decimal = -m_decimal;
}
// If decimal is out of bounds (in either direction),
// adjust the decimal so it's in bounds,
// and adjust base to account for the number of units removed from the decimal
// h/t to reader David Pinheiro for simplifying this
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);
};
// This doesn't require access to the internals of the class, so it can be defined outside the class
std::ostream& operator<<(std::ostream& out, const FixedPoint2& fp)
{
out << static_cast<double>(fp);
return out;
}
// You will need to make testDecimal a friend of FixedPoint2
// so the function can access the private members of FixedPoint2
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 }; // make sure we handle single digit decimal
assert(static_cast<double>(c) == 1.9);
FixedPoint2 d{ 5.01 }; // stored as 5.0099999... so we'll need to round this
assert(static_cast<double>(d) == 5.01);
FixedPoint2 e{ -5.01 }; // stored as -5.0099999... so we'll need to round this
assert(static_cast<double>(e) == -5.01);
// Handle case where the argument's decimal rounds to 100 (need to increase base by 1)
FixedPoint2 f { 106.9978 }; // should be stored with base 107 and decimal 0
assert(static_cast<double>(f) == 107.0);
// Handle case where the argument's decimal rounds to -100 (need to decrease base by 1)
FixedPoint2 g { -106.9978 }; // should be stored with base -107 and decimal 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> // for fixed width integers
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // here's our non-fractional part
std::int8_t m_decimal{}; // here's our factional part
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// If either (or both) of the non-fractional and fractional part of the number are negative, the number should be treated as negative
if (m_base < 0 || m_decimal < 0)
{
// Make sure base is negative
if (m_base > 0)
m_base = -m_base;
// Make sure decimal is negative
if (m_decimal > 0)
m_decimal = -m_decimal;
}
// If decimal is out of bounds (in either direction),
// adjust the decimal so it's in bounds,
// and adjust base to account for the number of units removed from the decimal
// h/t to reader David Pinheiro for simplifying this
m_base += m_decimal / 100; // integer division
m_decimal = m_decimal % 100; // integer remainder
}
// We'll delegate to the prior constructor so we don't have to duplicate the negative number and overflow handling logic
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);
}
};
// This doesn't require access to the internals of the class, so it can be defined outside the class
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 }; // make sure we handle single digit decimal
assert(static_cast<double>(c) == 1.9);
FixedPoint2 d{ 5.01 }; // stored as 5.0099999... so we'll need to round this
assert(static_cast<double>(d) == 5.01);
FixedPoint2 e{ -5.01 }; // stored as -5.0099999... so we'll need to round this
assert(static_cast<double>(e) == -5.01);
// Handle case where the argument's decimal rounds to 100 (need to increase base by 1)
FixedPoint2 f { 106.9978 }; // should be stored with base 107 and decimal 0
assert(static_cast<double>(f) == 107.0);
// Handle case where the argument's decimal rounds to -100 (need to decrease base by 1)
FixedPoint2 g { -106.9978 }; // should be stored with base -107 and decimal 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 equality true
assert(!(FixedPoint2{ 0.75 } == FixedPoint2{ 0.76 })); // Test equality false
// Test additional cases -- h/t to reader Sharjeel Safdar for these test cases
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }); // both positive, no decimal overflow
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }); // both positive, with decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }); // both negative, no decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }); // both negative, with decimal overflow
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }); // second negative, no decimal overflow
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }); // second negative, possible decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }); // first negative, no decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }); // first negative, possible decimal overflow
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> // for fixed width integers
#include <iostream>
class FixedPoint2
{
private:
std::int16_t m_base{}; // here's our non-fractional part
std::int8_t m_decimal{}; // here's our factional part
public:
FixedPoint2(std::int16_t base = 0, std::int8_t decimal = 0)
: m_base{ base }, m_decimal{ decimal }
{
// If either (or both) of the non-fractional and fractional part of the number are negative, the number should be treated as negative
if (m_base < 0 || m_decimal < 0)
{
// Make sure base is negative
if (m_base > 0)
m_base = -m_base;
// Make sure decimal is negative
if (m_decimal > 0)
m_decimal = -m_decimal;
}
// If decimal is out of bounds (in either direction),
// adjust the decimal so it's in bounds,
// and adjust base to account for the number of units removed from the decimal
// h/t to reader David Pinheiro for simplifying this
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
{
// Cast to double, make the double negative, then convert back to FixedPoint2
// h/t to reader EmtyC for this version
return FixedPoint2{ -static_cast<double>(*this) };
}
};
// This doesn't require access to the internals of the class, so it can be defined outside the class
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 equality true
assert(!(FixedPoint2{ 0.75 } == FixedPoint2{ 0.76 })); // Test equality false
// Test additional cases -- h/t to reader Sharjeel Safdar for these test cases
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }); // both positive, no decimal overflow
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }); // both positive, with decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }); // both negative, no decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }); // both negative, with decimal overflow
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }); // second negative, no decimal overflow
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }); // second negative, possible decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }); // first negative, no decimal overflow
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }); // first negative, possible decimal overflow
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;
}