21.x — Rozdział 21, podsumowanie i quiz

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

  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

B) -point

Pokaż rozwiązanie

C) std::cout << point

Pokaż rozwiązanie

D) point = 5;

Pokaż rozwiązanie

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

b) Czy dla tej klasy należy udostępnić zdefiniowany przez użytkownika konstruktor kopiujący lub operator przypisania?

Pokaż rozwiązanie

c) Dlaczego użycie std::int32_t zamiast int?

Pokaż rozwiązanie

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

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

> 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

> 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

> 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ę

Dla zmiennych a do c: Pokaż wskazówkę

Dla zmiennych d & e: Pokaż wskazówkę

Pokaż rozwiązanie

> 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ę

Pokaż wskazówkę

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
533 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze