Notki oznaczone tagiem ‘wskaźniki’

Co może udawać obiekt w C++

2010-05-06 14:56

Możliwości przeciążania operatorów w C++ dla własnych typów obiektów sprawiają, że mogą one (tj. te obiekty) zachowywać się w bardzo różny sposób. Mogą na przykład "udawać" pewne wbudowane konstrukcje językowe, nierzadko wykonując ich zadania lepiej i wygodniej. Przykładów na to można podać co najmniej kilka - oto one:

  • Obiekt może zachowywać się jak funkcja, czyli udostępniać możliwość "wywołania" siebie z określonymi parametrami. Takie twory często nazywa się funktorami i bywają używane podczas pracy ze standardową biblioteką STL.
    Działają one przy tym w bardzo prosty sposób, zwyczajnie przeciążając operator nawiasów okrągłych (). Jest on na tyle elastyczny, że może przyjmować dowolne parametry i zwracać dowolne rezultaty, co pozwala nawet na stworzenie więcej niż jednego sposobu "wywoływania" danego obiektu. Jednym z bardziej interesujących zastosowań dla tego operatora jest implementacja w C++ brakującego mechanizmu delegatów, czyli wskaźników na metody obiektów.
  • Na podobnej zasadzie obiekt może udawać tablicę - wie o tym każdy, kto choć raz używał klas STL typu vector. Wymagane jest tu przeciążenie operatora indeksowania []. Daje ono wtedy dostęp do elementów obiektu-pojemnika, który zresztą nie musi być wcale indeksowany liczbami, jak w przypadku tablic wbudowanych (dowodem jest choćby kontener map). Ograniczeniem jest jedynie to, że indeks może być (naraz) tylko jeden, bo chociaż konstrukcja typu:
    v[3,4] = 5;

    jest składniowo najzupełniej poprawna, to działa zupełnie inaczej niż można by było się spodziewać :)

  • Obiekty mogą też przypominać wskaźniki, które wtedy określa się mianem 'sprytnych' (smart pointers). Dzieje się tak głównie za sprawą przeciążenia operatorów * (w wersji jednoargumentowej) i ->. Normalnie te operatory nie mają zastosowania wobec obiektów, ale można nadać im znaczenie. Wtedy też mamy obiekt, który zachowuje się jak wskaźnik, czego przykładem jest choćby auto_ptr z STL-a czy shared_ptr z Boosta.
  • Wreszcie, obiekt może też działać jako flaga boolowska i być używany jako część warunków ifów i pętli. Sprytne wskaźniki zwykle to robią, a innymi wartymi wspomnienia obiektami, które też takie zachowanie wykazują, są strumienie z biblioteki standardowej. Jest to spowodowane przeciążeniem operatora logicznej negacji ! oraz konwersji na bool lub void*.

Reasumując, w C++ dzięki przeciążaniu operatorów możemy nie tylko implementować takie "oczywiste" typy danych jak struktury matematyczne (wektory, macierze, itp.), ale też tworzyć własne, nowe (i lepsze) wersje konstrukcji obecnych w języku. Szkoda tylko, że często jest to wręcz niezbędne, by w sensowny sposób w nim programować ;)

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Funkcje jako dane wejściowe

2009-05-29 15:56

Sporo algorytmów jako swoje parametry przyjmuje różnego typu funkcje, które potem są wykorzystywane w trakcie ich działania. Prostym przykładem są tu wszelkiego rodzaju sortowania czy wyszukiwania, umożliwiające często podanie własnego predykatu (funkcji zwracającej wartość logiczną). W bardziej skomplikowanej wersji może chodzić chociażby o algorytm genetyczny lub przeszukujący drzewo gry, który wykorzystuje do działania jakąś funkcję oceniającą (np. osobników w populacji).

Na takie okazje różne języki programowania oferują różne narzędzia, lecz w większości przypadków sprowadzają się one do jednego z poniższych:

  • Wskaźniki lub delegaty. Przekazywanie funkcji przez wskaźnik jest proste i naturalne, ale ma pewne ograniczenia. W C(++) na przykład nie da się zwykłym wskaźnikiem pokazać na metodę konkretnego obiektu; docelowo może to być tylko funkcja globalna lub statyczna. Ten problem nie występuje zwykle w przypadku delegatów, którymi często można pokazać właściwie na wszystko - również na anonimową funkcję zdefiniowaną ad hoc:
    // sortowanie listy w oparciu o własny predykat (C# 2.0 wzwyż)
    objects.Sort (delegate (object a, object b)
        { return a.ToString().Length - b.ToString().Length; });

    Niektóre języki nie mają jednak ani wskaźników, ani delegatów - w nich zwykle stosuje się sposób następny.

  • Interfejsy i metody wirtualne. Ta technika polega na zdefiniowaniu interfejsu (lub klasy abstrakcyjnej) z metodą wirtualną, którą docelowo będzie wywoływać algorytm. Korzystający z niego programista implementuje po prostu wskazany interfejs (lub definiuje klasę pochodną) i nadpisuje wspomnianą metodę własną wersją. Ten sposób jest szczególnie popularny w Javie, gdzie wspomagają go inne mechanizmy językowe - jak choćby niestatyczność klas wewnętrznych lub możliwość ich definiowania inline - np. w wywołaniu funkcji.
    Zauważmy też, że implementacja naszej 'funkcji' w postaci klasy pozwala też na dodatkowe możliwości, jak na przykład posiadanie przez nią stanu (co aczkolwiek w wielu przypadkach, np. predykatów sortowania, jest wysoce niezalecane).
  • Funktory. To rozwiązanie jest specyficzne dla C++ i opiera się na dwóch występujących w tym języku feature'ach: szablonach i przeciążaniu operatorów. Dzięki temu algorytm korzystający z "czegoś co przypomina funkcję" może wyglądać choćby tak:
    template <typename T, typename SearchFunc>
        const T find(const std::vector<T>& v, SearchFunc pred)
        {
            for (int i = 0; i <v.size(); ++i)
                if (pred(v[i])) return v[i]// znaleziono element spełniający predykat
            return T();
        }

    Określenie "coś co przypomina funkcję" jest jak najbardziej na miejscu, gdyż tutaj predykatem może być cokolwiek, co da się jako funkcję potraktować - czyli wywołać operatorem (). Może więc to być zwykły wskaźnik, jakaś ładna zewnętrzna implementacja mechanizmu delegatów (np. FastDelegate) lub jakikolwiek obiekt z przeciążonym operatorem nawiasów (). Właśnie te ostatnie nazywa się zwykle funktorami, a jeśli ktoś nieco bardziej zagłębił się w bibliotekę STL, na pewno nie raz się z nimi spotkał.

Trzeba też powiedzieć, że właściwie istnieje też inny sposób: zamiast samej funkcji przekazywanie jej... nazwy. "I niby jak ją potem wywołać?", można zapytać. Ano to już indywidualna kwestia każdego języka - w tym przypadku zwykle interpretowanego :)

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Samowskaźnik i usuwanie siebie

2008-07-10 17:35

Bez używania mechanizmu odśmiecania istnieje zawsze ryzyko, że skończymy z odwołaniem do obiektu, który został już zniszczony. (Przy używaniu GC też istnieje taka możliwość, ale musielibyśmy niejako sami się postarać, aby wystąpiła). Dlatego zaleca się na przykład, by każdemu wywołaniu delete towarzyszyło zerowanie wskaźnika:

delete p; p = NULL;

dzięki czemu można ochronić się przez problemem wiszących wskaźników (dangling pointers).

Można? No cóż, nie do końca. Istnieje jeszcze możliwość, że obiekt zniszczy się sam, używając po prostu instrukcji delete this (jest to jak najbardziej legalne). A wtedy wszelkie odnoszące się do niego odwołania będą już nieważne.
Chyba że... obiekt sam je wyzeruje. Istnieje mianowicie śmieszny trik, polegający na przekazaniu do niego w konstruktorze wskaźnika na siebie (self-pointer):

Foo* pFoo = new Foo(/* parametry konstruktora */, &pFoo);

A mówiąc dokładniej: wskaźnika na ów wskaźnik (zaczyna się robić ciekawie, prawda? ;]). Musi być on oczywiście zapamiętany, a cała sztuczka polega na tym, że w destruktorze obiektu następuje jego zerowanie:

class Foo
{
    public:
        Foo(/* ... */, Foo** ppSelf = NULL) : m_ppSelf(ppSelf) { /* ... */ }
        ~Foo() { if (ppSelf) *ppSelf = NULL; }
    private:
        Foo* m_ppSelf;
};

I teraz obiekt może radośnie się usuwać kiedy tylko zechce. A tak swoją drogą, jeśli rzeczywiście może on zniknąć w każdej chwili, to może wypadałoby też, aby sprawdzał, czy przypadkiem... już nie istnieje:

void Foo::SomeMethod(/* ... */)
{
    if (!this) return;
    // ...
}

Pokręcone i przekombinowane? Powiedziałbym wręcz, że szalone :) Ale do takich sztuczek trzeba się uciekać, jeśli nie korzystamy z mechanizmów odśmiecania pamięci i nie potrafimy jednoznacznie określić czasu życia obiektów i ich wzajemnej przynależności.
Albo po prostu: gdy nie mamy lepszych pomysłów. Bo przecież w ostateczności lepiej chyba sprawdzi się zwyczajna flaga logiczna z metodą typu IsAlive, jeśli obiekt rzeczywiście może popełnić nagłe samobójstwo. Najlepiej jednak, żeby takich emo-obiektów było jak najmniej ;)

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks
Tagi:
Autor: Xion w Programowanie » Komentarze (2)

Wskaźniki i referencje jako parametry

2008-07-09 19:07

Kiedy w C++ chcemy przekazać do funkcji odwołanie do obiektu (zezwalające na jego modyfikację wewnątrz funkcji), mamy do wyboru dwie metody. Ta alternatywa to posłużenie się wskaźnikiem albo referencją:

void Function(Foo* pFoo);
void Function(Foo& foo)

Czy istnieje uniwersalna odpowiedź na to, którą wybrać? Chyba nie. Jeśli chodzi o wskaźnik, to za jego użyciem może przemawiać:

  • Możliwość przekazania odwołania pustego, jeśli parametr nie jest obowiązkowy. Obejmuje to oczywiście zdefiniowanie NULL jako domyślnej wartości dla tego parametru w deklaracji funkcji. Nie jest to możliwe dla referencji (w C++).
  • Fakt, że w wywołaniu funkcji bardziej widoczne jest to, iż przekazany do niej za pośrednictwem wskaźnika obiekt może się zmienić. Jeśli na przykład obiekt ten jest zmienną lokalną, to konieczne jest posłużenie się operatorem &, który daje o tym jakąś widoczną wskazówkę (nie tak jasną jak ref/out w C#, ale zawsze). Nie jestem też wielkim fanem notacji węgierskiej, lecz w przypadku wskaźników stosowanie przedrostka p wydaje mi się akurat wskazane i w tym kontekście też zwiększa czytelność wywołania funkcji, wskazując, że przekazywany obiekt (alokowany na stercie) też może się zmienić.
    Foo foo; Foo* pFoo = new Foo();
    Function (&Foo); Function (pFoo); // funkcja może zmienić obiekt

Z kolei referencje mogą się popisać innymi zaletami:

  • Nie można do nich przekazać odwołania pustego. To może być zaletą, jeśli taka sytuacja jest niepożądana. Ponadto brak konieczności sprawdzania tego, czy referencja jest "pusta", może lekko poprawić wydajność kodu generowanego przez kompilator.
  • Składnia użycia obiektu przekazywanego przez referencję zwykle bywa bardziej przejrzysta. Jest tak zwłaszcza wtedy, gdy używamy względem niego operatorów. Na przykład kolekcja dostępna przez wskaźnik musiałaby być indeksowana przez (*pArray)[i], zaś przez referencję po prostu jako array[i].

Widać więc, że jeśli kwestia odwołania pustego nie jest dla nas istotna, to decyzja może być trudna. Ale naturalnie jest tak tylko wtedy, gdy zechcemy się nad takimi sprawami zastanawiać ;]

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Wskaźnik to tylko adres

2008-03-06 21:01

Jak C++ długi i szeroki, wskaźniki zawsze sprawiają początkującym (i nawet tym nieco bardziej zaawansowanym) programistom pewne kłopoty. Sam widziałem to zdecydowanie za dużo razy :) Niby wszyscy wiedzą, że wskaźnik to takie coś, co - jak podpowiada sama nazwa - pokazuje na jakiś inny obiekt. Czyli jest to taka "zmienna, która pokazuje na inną zmienną".
Przy tej interpretacji nie jest trudno zorientować się, że przekazując wskaźnik chociażby jako argument funkcji, pozwalamy jej modyfikować obiekt, na który ów wskaźnik pokazuje - bez kopiowania tego obiektu. Bardzo podobnie działają zresztą referencje w językach typu C#, Delphi, Java, itp. Dlatego też zdawałoby się, że można by je z powodzeniem utożsamiać ze wskaźnikami w C/C++.

Uważam, że nic bardziej mylnego! Pomijam już taki drobiazg, że nad wspomnianymi referencjami czuwa odśmiecacz pamięci, który zapobiega wyciekom, podczas gdy wskaźników nic takiego nie dotyczy. Różnica jest bowiem znacznie głębsza. Te referencje są trochę "magiczne" - w tym sensie, że, ściśle mówiąc, właściwie nie wiadomo, jak fizycznie one działają (albo raczej: nie ma potrzeby, aby to wiedzieć). Grunt, że pokazują na jakiś obiekt, zaś obiekt ten może być wskazywany przez wiele referencji, a odwoływanie się do przez którąkolwiek z nich jest całkowicie równoważne.
W C/C++ w przypadku zwykłych wskaźników zasadniczo jest tak samo (z dokładnością do arytmetyki). Kłopoty zazwyczaj zaczynają się wtedy, gdy zostajemy uraczeni dwiema gwiazdkami i otrzymujemy wskaźnik na wskaźnik. Odpowiadająca mu "referencja do referencji" w C#, itp. jest bowiem czymś zupełnie bez sensu, jako że referencja nie jest przecież obiektem, na który można by pokazywać. I wówczas cała interpretacja wskaźników jako "czegoś, co w jakiś sposób pokazuje na coś" staje się co najmniej zastanawiająca.

A przecież dokładnie wiadomo, w jaki sposób wskaźniki 'pokazują'. Przecież wskaźnik to nic innego, jak zmienna, która zawiera adres pewnego miejsca w pamięci. Zwykle adres ten odnosi się do innej zmiennej, obiektu, czasem funkcji, itd. I tyle, nie ma tutaj żadnego nadprzyrodzonego połączenia między wskaźnikiem a obiektem wskazywanym.
Ważne jest więc, by uświadomić sobie, że wskaźnik to zwykła zmienna zawierająca po prostu jakąś wartość (tutaj jest to pewien adres). To zaś oznacza, że sama ta zmienna również posiada jakieś miejsce w pamięci, czyli też rezyduje pod jakimś adresem. Kiedy pójdziemy za tym tokiem rozumowania, nie ma większych problemów z interpretacją podwójnych, potrójnych i wielokrotnych wskaźników.
A więc żadnej magii - to tylko liczby. Nie ma się czego bać :)

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks
Tagi: ,
Autor: Xion w Programowanie » Komentarze (6)

Gwiazdka w deklaracjach

2008-02-24 19:42

Oto odwieczny dylemat programistów C/C++: jak deklarować wskaźniki? A dokładniej: w którym miejscu umieścić gwiazdkę? Wybór dotyczy dwóch wariantów składniowych:

int* p1;
int *p2;

Dla kompilatora są one oczywiście równoważne, jednak zwolennicy każdego z nich potrafią podać całe mnóstwo argumentów za jednym i przeciwko drugiemu. I tak: przyklejanie gwiazdki do typu docelowego wskaźnika (tutaj int) jest popierane tym, że jest ona częścią typu wskaźnikowego (czyli int*). Z drugiej strony kruczkiem jest, iż deklaracja:

int *a, b;

tworzy wskaźnik a oraz zwykłą zmienną b - co stanowi argument na rzecz przyklejenia gwiazdki jednak do nazwy zmiennej.

Czy to więc kwestia gustu? Być może. Ja osobiście preferuję styl int* p1;, a ostatnio odnalazłem jeszcze jeden argument, który może za tym przemawiać. Jeśli bowiem oprócz zadeklarowania wskaźnika zechcemy go też zainicjalizować:

int *p = 0;

to w konwencji int *p2; wygląda to na pierwszy rzut oka dość dziwnie. Zgodnie z interpretacją "*p jest intem" wychodzi bowiem także na to, iż "inicjalizujemy *p zerem". Można więc pomyśleć - zwłaszcza przy mniejszym doświadczeniu w programowaniu w C/C++ - że inicjujemy nie sam wskaźnik, lecz wartość, na którą on wskazuje. To rzecz jasna nieprawda; nadal inicjowany jest wskaźnik, otrzymuje on swój początkowy adres.
Wytrawni koderzy nie popełniliby naturalnie takiej pomyłki. Pamiętajmy jednak, że dobrze napisany kod powinien wymagać możliwie najmniej zastanowienia nad sprawami nieistotnymi. A znaczenie powyższej konstrukcji wydaje się właśnie taką nieistotną sprawą.

  • RSS
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks
Tagi: ,
Autor: Xion w Programowanie » Komentarze (15)
 



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