Posts tagged ‘C/C++’

Dynamicze tablice w C

2008-05-08 15:17

Czasem życie zmusza do pisania w sławetnym i na szczęście zanikającym już języku C. A tam nie ma chociażby takich luksusów jak STL z niezastąpionymi pojemnikami. O wszelkie struktury danych trzeba zatroszczyć się samemu, co obejmuje także tak podstawowe jak dynamiczne tablice.
Jak wiadomo, takie tablice należy samodzielnie alokować, zmieniać ich rozmiar (z kopiowaniem zawartości) oraz zwalniać, gdy nie są już potrzebne. Najwyraźniej więc wybitnie przydatne są tu funkcje malloc i free, prawda? Otóż nieprawda :)

O wiele wygodniej jest bowiem używać funkcji realloc:

  1. tab = (int*)realloc(tab, tab_size * sizeof(int));

Jej nazwa wskazuje, że jej głównym przeznaczeniem jest zmiana rozmiaru już zaalokowanego kawałka pamięci. Zazwyczaj sprowadza się to zresztą do przydzielenia nowego, skopiowania tam zawartości starej porcji i zwolnienia jej. Jednakże w dwóch szczególnych przypadkach realloc zachowuje się inaczej – zależy to od parametrów, jakie jej podamy:

  • jeżeli pierwszym parametrem będzie wskaźnik pusty (NULL), funkcja przydzieli pamięć w ilości bajtów wyrażonej drugim parametrem – czyli zachowa się jak malloc
  • jeśli zaś drugim parametrem będzie zero, to funkcja spróbuje zwolnić kawałek pamięci, na który pokazuje wskaźnik będący pierwszym parametrem – czyli zachowa się jak free

To sprawia, że możemy używać funkcji realloc w postaci wywołania podobnego jak powyżej zawsze wtedy, gdy musimy zmienić rozmiar tablicy. Można na przykład dodać do niej element w taki oto sposób:

  1. int* AddInt(int* tab, size_t* tab_size, int elem)
  2. {
  3.     int* new_tab = (int*)realloc(tab, (*tab_size + 1) *sizeof(int));
  4.     new_tab[(*tab_size)++] = elem;
  5.     return new_tab;
  6. }

Nie musimy się martwić, czy tablica została wcześniej zaalokowana czy nie. Analogicznie wygląda usuwanie elementów z końca lub początku tablicy – wówczas nie trzeba przejmować się tym, czy z tablicy coś zostanie. Tak naprawdę opakowywanie obu tych operacji w funkcje nie jest nawet specjalnie potrzebne.

Jedynym warunkiem, aby to wszystko działało, jest odpowiednie zainicjowanie wskaźnika na tablicę oraz zmiennej przechowującej jej rozmiar – co jest aczkolwiek dość oczywiste:

  1. int* tab = NULL;
  2. size_t tab_size = 0;

I to właściwie wszystko. W sumie więc nie jest to może std::vector, ale w C nie ma co wybrzydzać :)

Tags: ,
Author: Xion, posted under Programming » 2 comments

Jak nie należy używać operatorów

2008-04-30 22:23

W wielu API funkcje mają bardzo prosty sposób powiadamiania o tym, czy ich wykonanie zakończyło się sukcesem czy porażką. Albo więc wykorzystują typ bool bezpośrednio, albo wpasowują się w konwencję, iż niezerowa wartość liczbowa jest tożsama z prawdą, a zero z fałszem. To sprawia, że możliwe jest pisanie warunków podobnych do poniższego:

  1. if (!RegisterClassEx(&wc))    Error("Can't register window class.");

Ładne to i opisowe – wręcz samodokumentujące się. Ale czasami tak się zrobić nie da, bo wartości zwracane nie chcą współpracować z tym modelem.

Przykład? To większość biblioteki runtime języka C oraz API systemów uniksowych. O ile tylko rezultatem funkcji należącej do któregoś z tych dwóch zbiorów nie jest wskaźnik, konwencja informowania o powodzeniu lub niepowodzeniu jest zwykle dość osobliwa. Według niej zero oznacza sukces, natomiast porażka wykonania jest sygnalizowana przez wartość mniejszą od zera – zazwyczaj -1. Oczywiście nijak nie pasuje to sposobu interpretowania liczb jako wartości logicznych. Sprawia to, że sprawdzanie rezultatu takich funkcji może wyglądać cokolwiek enigmatycznie:

  1. if (close(fd) < 0)    perror("closing file");&#91;/cpp]
  2. Ale nie wszystko stracone :) W przypadku funkcji typu boolowskiego możemy posłużyć się operatorem logicznej negacji (<code>!</code>), dzięki czemu zawierające je <code>if</code>y są całkiem przejrzyste. Okazuje się, że z powodu pewnego zbiegu okoliczności także te wspomniane przed chwilą funkcje można potraktować tak samo... o ile dodamy jeszcze jeden operator. A dokładniej - jeśli oprócz negacji logicznej dodamy też bitową (<code>~</code>):
  3. [cpp]if (!~close(fd))    perror("closing file");

Powód, dla którego to działa, jest dość prosty. W standardowym sposobie zapisu liczb całkowitych, stosowanym na zdecydowanej większości typowych i nietypowych maszyn (zwanym uzupełnieniem do 2 – U2), wartość -1 to w zapisie binarnym same jedynki. Negując je bitowo, otrzymujemy same zera – czyli zero, a więc logiczny fałsz. A odwrotnością fałszu jest oczywiście prawda i wszystko działa poprawnie. Wygląda więc tak, jakby skromna tylda zdołała “naprawić” funkcję, by zachowywała się zgodnie z oczekiwaniami…

Tylko czy aby na pewno nowy zapis jest bardziej sugestywny? Mam nadzieję, że każdy potrafi poprawnie odpowiedzieć na to pytanie we własnym zakresie :) Na koniec jednak muszę – dla spokoju sumienia – ostrzec wszystkich: zdecydowanie nie róbcie tego w domu :D

Tags: , ,
Author: Xion, posted under Programming » 4 comments

Czym jest NULL?

2008-04-24 21:42

Gdybym chciał byś złośliwy, to stwierdziłbym, że w C++ nawet ‘nic’ (czyli NULL) nie jest takie, jak być powinno. Ale ponieważ w rzeczywistości jestem wcieleniem łagodności (;]), napiszę raczej o tym, jak można zaradzić na niedogodności obecnej postaci wskaźnika pustego w C++.
Cały problem z NULL-em polega na tym, że nie jest on wartością odpowiedniego typu. Dwie klasyczne definicje tej stałej – jako 0 lub (void*)0 – mają zauważalne mankamenty. Pierwsza definiuje NULL jako liczbę, co sprawia, że dozwolone są bezsensowne podstawienia w rodzaju:

  1. int n = NULL;

Natomiast druga nie da się wprawdzie skonwertować na typ liczbowy, ale nie da się też niejawnie zmienić w wartość żadnego innego typu wskaźnikowego niż void*. A tego oczekiwalibyśmy po wskaźniku pustym. Dlatego z dwojga złego w standardzie przyjęto pierwszą definicję i NULL jest po prostu zerem.

To, czego naprawdę byśmy chcieli po NULL, to wartość 0, która:

  • może być podstawiona za dowolny typ wskaźnikowy
  • nie może być podstawiona za żaden inny typ, zwłaszcza liczbowy

To zaś da się osiągnąć, pisząc odpowiednią… klasę:

  1. #undef NULL
  2. const
  3.     class Null
  4.     {
  5.         public:
  6.             // konwersja na dowolny typ wskaźnikowy (także pointer-to-member)
  7.             template <typename T> operator T* () const { return 0; }
  8.             template <class C, typename T> operator T C::*() { return 0; }
  9.         private:
  10.             // zablokowanie pobierania adresu
  11.             void operator & () const;
  12.     }
  13. NULL;

Sztuczką są tu oczywiście szablony operatorów konwersji. Zapewniają one możliwość traktowania naszego NULL-a jako wartości dowolnego typu wskaźnikowego – łącznie ze wskaźnikami do składowych klas. A ze względu na brak innych konwersji, nie jest możliwe automatycznie przypisanie naszego pustego wskaźnika do zmiennej liczbowej.

Taki NULL jest na tyle dobry, że działa nawet z uchwytami Windows API, gdyż wewnętrznie są one zdefiniowane jako specyficzne wskaźniki. Dziwi więc, dlaczego nadal nie ma go chociażby w bibliotece standardowej C++. Najwyraźniej nawet jeśli chcemy mieć ‘nic’, trzeba się trochę napracować ;P

Tags: ,
Author: Xion, posted under Programming » 10 comments

RAII-skie kwiatki

2008-04-11 19:43

Jako język nie posiadający słowa kluczowego finally, C++ preferuje nieco inną metodę na radzenie sobie z nieprzewidzianymi “wyskokami” z funkcji i związaną z tym możliwością wycieku zasobów (resource leak). Ten inny sposób jest znany skądinąd jako RAII: Resource Acquisition Is Initialization i polega na związaniu z każdym użyciem zasobu jakiegoś obiektu lokalnego, np.:

  1. {
  2.    ThreadLock lock;
  3.    // (wykonywany tylko przez jeden wątek)
  4. }

Proste i całkiem wygodne, jeśli tylko posiadamy już (lub zechcemy napisać) odpowiednią klasę, która w konstruktorze pozyskuje dany zasób – tutaj blokadę muteksa – a w destruktorze go oddaje.

Ale ten nieskomplikowany mechanizm daje możliwości popełnienia błędów, które są na swój interesujące, ale w realnym kodzie na pewno niezbyt przyjemne :) Pierwszy z nich związany jest z faktem, że lokalnych obiektów nie tworzy się znowu aż tak dużo i można popełnić w ich składni drobne, acz wielce znaczące faux pas z nawiasami:

  1. ThreadLock lock();

Taki wiersz nie stworzy nam bowiem żadnego obiektu, ale zadeklaruje funkcję lock, zwracającą obiekt typu ThreadLock i niebiorącą żadnych argumentów. Zaskakujące? A to tylko prosta konsekwencja faktu, że cokolwiek, co można zinterpretować w C++ jako deklarację funkcji, zostanie tak właśnie zinterpretowane.

Można jednak ripostować, że nic takiego nie zdarzy się, jeśli do konstruktora naszego obiektu-blokady przekażemy chociaż jeden parametr. A zwykle tak właśnie będzie; tutaj np. byłoby nim odwołanie do obiektu typu mutex lub semafora, który chcemy zająć. Jednak nie zmienia to faktu, że w większości przypadków obiekt realizujący RAII wystarcza nam przez samo swoje istnienie, co z kolei sprawia, że w dalszym kodzie w ogóle się do niego nie odwołujemy. To zaś może spowodować, że pominiemy i tak nieużywany składnik jego deklaracji – czyli nazwę:

  1. ThreadLock (&mutex);

Takie zagranie również nie powinno wywołać protestów kompilatora, ale prawie na pewno nie jest tym, o co nam chodzi. Tworzony obiekt jest teraz bowiem nie lokalny, ale tymczasowy: jego zasięg ogranicza się do wyrażenia, w którym został wprowadzony. Czyli do… średnika kończącego powyższą instrukcję! Taki też zakres ma opakowana przez ów obiekt blokada międzywątkowa.

Jak zatem widać, jest tu kilka okazji do popełnienia błędów, które mogą być trudne do wykrycia. Powinniśmy więc zwrócić na nie uwagę tym bardziej, że wobec braku w C++ instrukcji finally technika RAII jest jedynym sensownym wyjściem dla lokalnego pozyskiwania i zwalniania zasobów.

Tags: , ,
Author: Xion, posted under Programming » 13 comments

Sinus, cosinus

2008-04-08 16:18

Jeśli w praktyce obliczamy wartości funkcji sinus lub cosinus dla danego kąta, to bardzo często zdarza się, że tak naprawdę potrzebujemy ich obu. Jest tak przy obliczaniu punktów okręgu, przy rozkładzie wektorów sił i jeszcze dla wielu innych okoliczności. Zależy nam naturalnie, aby policzyć to wszystko tak szybko, jak tylko się da, dlatego dobrze jest stosować funkcje w rodzaju sincos, które wyznaczają obie wartości jednocześnie.

Niestety nie każdy język programowania taką funkcję posiada. Mają ją języki shaderowe (GLSL, HLSL i asembler GPU) oraz np. Delphi, ale już nasz ulubiony C++ nie. Można by oczywiście uzupełnić ten brak poprzez taką implementację:

  1. void sincos(float angle, float* sine, float* cosine)
  2.     { *sine = sin(angle); *cosine = cos(angle); }

ale chyba nie trzeba nikogo przekonywać, że większego sensu ona nie ma :) Nie występuje tu bowiem żaden zysk na wydajności, bo wartości są obliczane oddzielnie.

Co więc można zrobić? Ano wykorzystać to, co drzemie w jednostce zmiennoprzecinkowej procesora, ale nie jest używane przez wszystkie języki wyższego poziomu. Istnieje mianowicie instrukcja FSINCOS, która wykonuje całą potrzebną “magię”. Należy ją tylko opakować:

  1. void sincos(float angle, float* sine, float* cosine)
  2. {
  3.     __asm
  4.     {
  5.         mov eax, sine
  6.         mov edx, cosine
  7.        
  8.         fld     angle
  9.         fsincos
  10.         fstp    dword ptr [edx]
  11.         fstp    dword ptr [eax]
  12.     }
  13. }

Jakkolwiek tajemniczo to może wyglądać, funkcja ta po prostu ładuje argument (kąt) na stos FPU, by potem odebrać wyniki – cosinus i sinus. W przypadku operowania na liczbach typu float nie ma możliwości podania zbyt dużego/małego argumentu, więc nie trzeba sprawdzać rejestru flag.

I tyle – funkcja mała, acz użyteczna. Asembler czasem się przydaje, proszę państwa ;P

Tags: , ,
Author: Xion, posted under Programming » 13 comments

Nagłówki i przestrzenie nazw

2008-03-30 16:11

Niestety, przestrzenie nazw w C++ nie mają takiego wdzięku jak pakiety w Javie czy assemblies w .NET. Zwłaszcza w połączeniu z plikami nagłówkowymi potrafią one sprawić nieco problemów. Jeśli bowiem nie będziemy uważali, to możemy dość łatwo zniwelować korzyści, jakie płyną z ich używania.

Ale po kolei. Jeśli mamy do czynienia z modułem kodu (plikiem .cpp), to możemy bez żadnych skrupułów umieszczać w nim deklaracje using lub using namespace. Jeśli tylko nie prowadzi to do niejednoznaczności, wszystko jest w porządku, bowiem zasięg tych deklaracji ogranicza się tylko i wyłącznie do tego jednego pliku.
Gorzej jest niestety z plikami nagłówkowymi. Ponieważ włączane są one tu i ówdzie przy pomocy dyrektywy #include, nie można tak po prostu wpisywać w nich deklaracji using namespace. Zostałyby one bowiem rozpropagowane do wszystkich plików, które dany nagłówek włączają, efektywnie niwelując wszelkie korzyści zamknięcia fragmentów kodu w przestrzeń nazw. Bo co to za namespace, który wszyscy “rozpakowują” i przenoszą jego zawartość do przestrzeni globalnej?…

Dlatego nie ma rady: w plikach nagłówkowych można używać wyłącznie nazw kwalifikowanych – poprzedzonych wszystkimi nazwami przestrzeni, jak np. XC::Base::GC::BlockInfo. W przeciwnym wypadku na pewno zaśmiecimy sobie którąś z przestrzeni (najczęściej globalną) identyfikatorami, które do niej nie należą – co będzie widoczne w systemach podpowiadania takich jak IntelliSense.

Tags: , ,
Author: Xion, posted under Programming » 4 comments

Tajemnice stałych referencji

2008-03-13 21:47

Zapewne wielu programistów C++ zetknęło się z taką lub podobną sytuacją. Oto w pocie czoła napisana funkcja w postaci podobnej do tej:

  1. void Foo(string s) { /* ... */ }

zostaje obejrzana przez bardziej doświadczonego kodera, który stwierdza: “Powinieneś użyć tutaj stałej referencji jako parametru – czyli napisać:

  1. void Foo(const string& s)

Unikniesz wtedy niepotrzebnego kopiowania stringa”. Wówczas możemy zrobić wielkie oczy i zdziwić się bardzo, zwłaszcza jeśli wiemy co nieco o referencjach w C++. Mimo to sugestia ta jest jak najbardziej na miejscu i powinniśmy się do niej stosować. Wstyd przyznać się, że przez dłuższy czas niesłusznie brałem ją tylko na wiarę, lecz na szczęście jakiś czas temu dowiedziałem się dokładnie, o co tutaj chodzi. Tą cenną wiedzą oczywiście się podzielę :)

Wiadomo, że w C++ referencje to w zasadzie to samo, co (raczej rzadko używane) stałe wskaźniki – czyli zmienne w typu T* const. Różnią się one aczkolwiek składnią: wszystkie dereferencje są dokonywane przezroczyście i kod wygląda tak, jakbyśmy posługiwali się zwykła zmienną docelowego typu, na który referencja wskazuje (czyli T). Podobnie stałe referencje są odpowiednikami stałych wskaźników na stałe (const T* const), czyli takich, które nie pozwalają ani na modyfikację obiektu wskazywanego, ani na zmianę samego wskazania.
W każdym przypadku wskaźniki muszą jednak na coś pokazywać; na coś, co ma określone miejsce w pamięci, czyli adres. W zasadzie podobna reguła dotyczy też referencji – z jednym małym, ale jakże ważnym wyjątkiem.

Otóż powyższą funkcję (w wersji ze stałą referencją jako parametrem) możemy bez problemów wywołać tak, jak poniżej:

  1. Foo ("Hello world");

Niby nic nadzwyczajnego, ale zauważmy, że tworzony jest tutaj tymczasowy obiekt string, na który następnie pokazuje referencja w ciele naszej funkcji. Błąd standardu lub kompilatora? Wręcz przeciwnie – it’s not a bug, it’s a feature :)
Po prostu w C++ stałe (i tylko stałe) referencje mogą poprawnie wskazywać na obiekty tymczasowe. Życie takich obiektów jest wówczas przedłużane aż do czasu wyjścia poza zasięg istnienia referencji. W naszym przypadku utworzony tymczasowy obiekt string będzie więc dostępny w całej funkcji.

Biorąc pod uwagę fakt, że zamiana deklaracji string s na const string& s nie kosztuje nas nic (wewnątrz funkcji do parametru odwołujemy się tak samo), możemy zerowym kosztem zyskać sporą optymalizację. Przekazanie referencji kosztuje przecież tyle samo co przekazanie wskaźnika i na pewno jest nieporównywalnie tańsze niż wykonywanie kopii całego napisu.
Dlatego też nie tylko w przypadku klasy string, ale i we wszystkich podobnych sytuacjach obiekty powinniśmy przekazywać właśnie przez stałe referencje.

Tags: , ,
Author: Xion, posted under Programming » 4 comments
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.