L2

Laboratorium 2 #

Pamięć #

Na tym laboratorium twoim zadaniem jest stworzenie niestandardowych typów danych oraz eksperymentowanie z ich czasem życia. Tym razem zadanie podzielone jest na sześć etapów. W kodzie znajdują się wskazówki, gdzie należy umieścić rozwiązania poszczególnych etapów (szukaj komentarzy zawierających STAGE N).

Skorzystaj z kodu startowego oraz dołączonego do niego pliku Makefile. Można w nim znaleźć dwie zmienne: CXXFLAGS oraz LDFLAGS. Pierwsza powinna być używana przy tworzeniu obiektów. Druga natomiast jest przekazana przy linkowaniu obiektów do plików wykonywalnych lub bibliotek.

Makefile

main.cpp

vector3.hpp vector3.cpp

holey_string.hpp holey_string.cpp

memory_manipulation.hpp memory_manipulation.cpp

Etap 0: Deklaracja enumeracji w stylu C++ #

Na rozgrzewkę twoim zadaniem będzie utworzenie dwóch enumeracji opisujących kolory oraz typy owoców. W tym celu utwórz plik fruit.hpp i zadeklaruj dwie enumeracje: Color oraz FruitType. Pierwsza z nich dopuszcza 4 kolory:

  • Red
  • Orange
  • Green
  • Violet

Druga natomiast opisuje trzy rodzaje owoców:

  • Apple
  • Orange
  • Plum

Następnie zdefiniuj strukturę Fruit składającą się z tych dwóch enumeracji. Na końcu pliku fruit.hpp zdefiniuj 6 statycznych wyrażeń stałych (static constexpr) zmiennych typu Fruit opisujące dojrzałe i niedojrzałe jabłko, pomarańczę oraz śliwkę.

Wyrażenie stałe może być użyte do wykonania obliczeń jeszcze w trakcie kompilacji. Na warsztatach użyjemy tego jako ułatwienie definiowania zmiennej statycznej. Wyrażenia stałe mogą być definiowane w całości w plikach nagłówkowych. Na dalszych wykładach szerzej zostanie poruszony temat obliczeń w trakcie kompilacji.

Zwróć uwagę, że przy instancjonowaniu dojrzałej pomarańczy przekazujemy dwa razy enumerację o symbolu Orange. Enumerację w stylu C++ wprost wyrażają, który Orange powinien zostać użyty w trakcie tworzenia instancji owoców przy pomocy nazwy klasy poprzedzającej wartość enumeracji.

Etap 1: Trójwymiarowy wektor #

W pliku vector3.hpp zadeklarowana jest struktura, która ma reprezentować wektor trójwymiarowy. Jako element jej definicji znajdziesz using internal_representation, który definiuje, jak wewnętrznie przechowywane są informacje o trzech liczbach rzeczywistych. Twoim zadaniem jest stworzyć definicję struktury internal_representation w taki sposób, aby dostęp do trzech liczb typu double można było wykonać poprzez trzy oddzielne zmienne x, y oraz z, albo poprzez trójelementową tablicę typu double. Stworzona struktura powinna mieć rozmiar 3 * sizeof(double) oraz alignment taki jak typ double. Proszę zwrócić uwagę na dwie linie zawierające static_assert. Jest to sposób na upewnienie się, że zdefiniowany przez ciebie typ będzie traktowany jako blok trzech liczb. Zastanów się, dlaczego akurat tak wyglądają sprawdzenia poprawności.

Struktura Vector3 ma zdefiniowane pole v stworzonego przez ciebie typu internal_representation. W czterech funkcjach, które musisz teraz zaimplementować w pliku vector3.cpp, będzie ona dostępna jako pole v.

Dwie z tych funkcji to tzw. konstruktory, o których mowa będzie na kolejnych laboratoriach. Twoim zadaniem jest ustawić w nich wartości x, y oraz z pola v zgodnie z przekazanymi argumentami (brak argumentów oznacza wypełnienie zerami). Funkcja length służy do wyliczenia długości euklidesowej wektora (Podpowiedź: funkcja sqrt znajduje się w nagłówku cmath). Funkcja mul służy do pomnożenia wektora przez liczbę. Do zaimplementowania funkcji length oraz mul użyj możliwości dostępu do v z perspektywy tablicy typu double.

Jako rozszerzenie klasy Vector3 zadeklaruj dwie wolne funkcje w pliku vector3.hpp:

  • vector3_add - funkcja wykonuje dodawanie wektorów oraz przyjmuje dwie stałe referencje na typ Vector3 reprezentujące lewą i prawą stronę operatora dodawania. Funkcja powinna zwracać nowy Vector3 przechowujący wynik dodawania.
  • vector3_print - funkcja formatuje i wypisuje na standardowe wyjście współrzędne wektora oraz jego długość ([x,y,z] length). Przyjmuje jako argument jedną stałą referencję na wektor, który należy wypisać na standardowe wyjście.

Ciała funkcji powinny zostać zaimplementowane w pliku vector3.cpp.

Po skończeniu implementacji struktury Vector3 przejdź do funkcji main w pliku main.cpp. Mając już wszystkie konieczne operacje na wektorach, możemy wyrazić wektor [3,5,7] jako kombinację liniową wektorów bazowych pomnożonych przez pewne stałe. Zdefiniuj trzy wektory bazowe jako zmienne automatyczne i wykorzystując funkcje mul oraz vector3_add oblicz wynikowy wektor. Na koniec wypisz wynik na standardowe wyjście przy pomocy funkcji vector3_print.

W ramach przypomnienia: wektory z bazy kanonicznej to [1,0,0], [0,1,0] oraz [0,0,1] (Podpowiedź: = {x,y,z} zainicjalizuje wektor wartościami podanymi w klamrach).

Etap 2: Tablice wektorów #

Kiedy typ Vector3 działa jak trzeba, możemy przejść do deklarowania tablic w funkcji main. Twoim zadaniem jest zadeklarować trzy rodzaje tablic:

  • automatyczna (na stosie),
  • dynamiczną (na stercie) - oznacza to wykonanie wszystkich akcji związanych z obsługą otrzymanej pamięci,
  • używając std::vector (używając obiektu, który zarządza pamięcią wewnętrznie).

Do każdej tablicy wstaw 10 obiektów typu Vector3 o wartościach {i,i,i}, gdzie i - numer wstawianego wektora. Przy każdym wstawieniu pobierz adres pierwszego elementu oraz wypisz go na standardowe wyjście. Czy w każdym wypadku te adresy będą identyczne w czasie kolejnych iteracji pętli? Po zakończeniu wstawiania przejdź po tablicy ponownie oraz wypisz długość wektora na standardowe wyjście.

Etap 3: Memory dumper (pol. drukarz pamięci) #

W tym etapie twoim celem jest napisanie funkcji, która przyjmie dowolny wskaźnik oraz ilość bajtów do wypisania na standardowe wyjście. W każdej linii wypisz 8 bajtów na dwa sposoby: jako liczbę heksadecymalną oraz znak ASCII (jeśli jest to możliwe). Aby dopełnić obraz wypisanych bajtów, na początku linii wypisz adres pierwszego bajta (Podpowiedź: std::hex służy do formatowania wartości jako liczby heksadecymalnej).

Przykładowo funkcja dla pamięci zajmowanej przez Vector3{1,2,3} wypisze na standardowe wyjście

0x75c1bc3000f0: 00 00 00 00 00 00 f0 3f | .......? |
0x75c1bc3000f8: 00 00 00 00 00 00 00 40 | .......@ |
0x75c1bc300100: 00 00 00 00 00 00 08 40 | .......@ |

a dla ciągu znaków Hello world!

0x76a298300240: 48 65 6c 6c 6f 20 77 6f | Hello wo |
0x76a298300248: 72 6c 64 21 | rld! |

Definicja funkcji jest przygotowana w pliku main.cpp. Po wykonaniu implementacji wypisz na ekran pamięć zajmowaną przez każdą tablicę z etapu drugiego.

Etap 4: Dziurawy ciąg znaków #

W pliku holey_string.hpp zdefiniowana jest struktura reprezentująca 16 elementowy ciąg znaków. Znaki w tej strukturze są specjalne, pomimo wykorzystania typu char każdy znak zajmuje 2 bajty.

Twoim zadaniem jest zdefiniować ten specjalny typ znaku holey_char (Podpowiedź: alignas) i zaimplementować trzy funkcje:

  • print - ta funkcja wypisuje taki specjalnie przygotowany ciąg znaków na standardowe wyjście (długość ciągu znaków ustal na podstawie terminującego zera - jak w języku C),
  • assign - ta funkcja przypisuje otrzymany std::string do specjalnego ciągu znaków (zaterminuj string w stylu C - ustawiając ostatni bajt na zero),
  • hide - ta funkcja przypisuje otrzymany std::string do dziur powstałych pomiędzy znakami. W przypadku ostatniej funkcji wykonanie funkcji hide nie powinno wpłynąć na zawartość ciągu znaków oraz kolejne wywołania funkcji print. Po laboratorium zastanów się, czy ta funkcja przypadkiem nie łamie jakichś zasad 🤔

Po zaimplementowaniu powyższych funkcji przejdz do pliku main.cpp oraz stwórz automatyczny obiekt typu HoleyString. Wykonaj na nim funkcję assign ze stringiem "hello" oraz hide z "world". Po wykonaniu każdej z tych dwóch operacji wykonaj funkcję print oraz wypisz pamięć zajmowaną przez obiekt funkcją dump_memory.

Etap 5: Manipulacja pamięcią #

W standardowej bibliotece C znajdują się dwie bardzo przydatne funkcje: memcpy oraz memmove. Obydwie służą do przekopiowania bloku pamięci ze wskazanego adresu do docelowego. Różni je jednak bardzo subtelny szczegół: fakt nachodzenia się bloków źródłowego oraz docelowego. Funkcja memcpy zakłada, że podane bloki nie nachodzą na siebie, a memmove dopuszcza, aby bloki nachodziły na siebie. Zachęcam do przeczytania instrukcji dla standardowej biblioteki, aby zapoznać się z funkcjami (man 3p memcpy oraz mam 3p memmove).

Twoim zadaniem jest zaimplementować obydwie funkcje w pliku memory_manipulation.cpp. W celu sprawdzenia implementacji w pliku main.cpp wykonaj następujące przekształcenia:

  • Hello world! -> Hello Hello!
  • Hello world once again! -> Hello world world once!

Zabronione jest użycie funkcji std::memcpy oraz std::memmove w implementacjach i przykładzie użycia!

Rozwiązanie #

Makefile

main.cpp fruit.hpp

vector3.hpp vector3.cpp

holey_string.hpp holey_string.cpp

memory_manipulation.hpp memory_manipulation.cpp