Posts tagged ‘pointers’

Go is Like Better C, Mostly

2013-01-09 22:55

The Go programming language is was on my (long) list of things to look into for quite some time now. Recently, at last, I had the opportunity to go through the most part of a comprehensive tour of Go from the official website, as well as write few bits of some Go code by myself.

Go-pherToday I’d like to recap on some of my impressions. You can treat it as “unboxing” of the Go language, much like when people post movies of their first hands-on experiences with new devices. Except, it will be just text – I’m not cool enough to do videos yet ;)

Some trivia

We all like to put stuff into our various mental buckets, so let’s do that with Go too.

Go is a compiled, statically typed programming language that runs directly on the hardware, without any underlying virtual machine or other bytecode-based runtime. That sounds good from the speed viewpoint and indeed, Go comes close to C in raw performance of equivalent programs.

Syntax of Go is C-like, at least in the fact that it’s using curly braces to delimit blocks of code. Some visual clutter is intentionally omitted, though. Semicolons are optional, for example, and idiomatic Go code omits them at all times.
But more surprisingly, parentheses around if and for conditions are straight out forbidden. As a result, it’s mandatory to use curly braces even for blocks that span just one line:

  1. if obj == nil {
  2.     return
  3. }

If you’re familiar with reasoning that suggests doing that in other C-like languages, you shouldn’t have much problems adapting to this requirement.

No-fuss static typing

Go is type-safe and requires all variables to be declared prior to use. For that it provides very nice sugar in the form of := operator, coupled with automatic type inference:

  1. s := "world"
  2. fmt.Printf("Hello %s!\n", s)

But of course, function arguments and return values have to be explicitly typed. Coming from C/C++/Java/etc. background, those type declarations might look weird at first, for they place the type after the name:

  1. func Greet(whom string) string {
  2.     return fmt.Sprintf("Hello, %s! How are you?", whom)
  3. }

As you can see, this also results in putting return type at the end of function declarations – something that e.g. C++ also started to permit.

But shorthand variable declarations are not the only way Go improves upon traditional idioms of static typing. Its interfaces are one of the better known features here. They essentially offer the support for duck typing (known from Python, among others) in a compiled language.
The trick is that objects do not specify which interfaces they implement: it’s just apparent by their methods. We can, however, state what interfaces we require for our parameters and variables, and those constraints will be enforced by the compiler. Essentially, this allows for accepting arbitrary values, as long as they “quack like a duck”, while retaining the overall type safety.

As an example, we can have a function that accepts a very general io.Writer:

  1. func SendGreetings(w io.Writer, name string) {
  2.     fmt.Fprintf(w, "Hello, %s!", name)
  3. }

and use it with anything that looks like something you could write into: file objects, networked streams, gzipped HTTP responses, and so on. Those objects won’t have to declare or even know about io.Writer; it’s sufficient that they implement a proper Write method.

Pointers on steroids

Talking about objects and interfaces sounds a bit abstract, but we shall not forget that Go is not a very high level language. You still have pointers here like in C, with the distinction between passing an object by address and copying it by value like in C++. Those two things are greatly simplified and made less error prone, however.

First, you don’t need to remember all the time whether you interact with object directly or through a pointer. There’s no -> (“arrow”) operator in Go, so you just use dot (.) for either. This makes it much easier to change the type of variable (add or remove *) if there’s need.

Second, most common uses for pointers from C (especially pointer arithmetic) are handled by dedicated language mechanism. Strings, for example, are distinct type with syntactic support and not just arrays of chars, neither a standard library class like in C++. Arrays (called slices) are also well supported, including automatic reallocation based on capacity, with the option of reserving the exact amount of memory beforehand.

Finally, the common problems with pointer aliasing don’t really exist in Go. Constraints on pointer arithmetic (i.e. prohibiting it outright) mean that compiler is able to track how each and every object may be used throughout the program. As a side effect, it can also prevent some segmentation faults, caused by things like local pointers going out of scope:

  1. func Leak() *int {
  2.     i := 42
  3.     return &i
  4. }

The i variable here (or more likely: the whole stack frame) will have been preserved on heap when function ends, so the pointer does not become immediately invalid.

Packages!

If you ever coded a bit in some of the newer languages, then coming to C or C++ you will definitely notice (and complain about) one thing: lack of proper package management. This is an indirect result of the header/implementation division and the reliance on #include‘ing header files as means of specifying dependencies. Actually, #includes are not even that: they work only for compiler and not linker, and are in some sense abused when working with precompiled headers.

What about Go?… Turns out it does the right thing. There are no separate header and implementation units, only modules (.go files). Unless you are using GCC frontend or interfacing with C code, the compiler itself is also unified.

But most importantly, there are packages and normal import statements. You can have qualified and unqualified imports, and you can alias things you’re importing into different names. Packages themselves are based on directory structure rooted in $GOROOT, much like e.g. Python ones are stored under $PYTHONPATH.

The only thing you can want at this point is the equivalent of virtualenv. Note that it’s not as critical as in interpreted languages: standalone compiled binaries do not have dependency problems, after all. But it’s still a nice thing to have for development. So far, people seem to be using their own solutions here.

Tags: , , , , , ,
Author: Xion, posted under Programming » Comments Off on Go is Like Better C, Mostly

Etykiety też mają adresy

2011-08-12 21:25

Natrafiłem niedawno na kapitalną ciekawostkę, sponsorowaną przez literkę C – język C, rzecz jasna. A jeśli już o C mowa, to jednym z pierwszym skojarzeń są oczywiście wskaźniki; prawdopodobnie nie jest ono zresztą specjalnie pozytywne ;) Wśród nich mamy wskaźniki na funkcje, które znane są z kilku nieocenionych zastosowań (weźmy na przykład funkcję qsort), ale przede wszystkim z pokrętnej i niezbyt oczywistej składni.

Zalecam jednak przypomnienie jej sobie, bo dzisiaj będzie właśnie o wskaźnikach na funkcje, tyle że pokazujących na… etykiety. Tak, te właśnie etykiety (labels), które normalnie są celem instrukcji gotozłej, niezalecanej, i tak dalej. Okazuje się bowiem, że etykiety też mają swoje adresy, które w dodatku można pobrać i wykorzystać jako wskaźniki:

  1. #include <stdio.h>
  2.  
  3. int main() {
  4.     first:
  5.     second:
  6.     printf("first = %p, second = %p, last = %p", &&first, &&second, &&last);
  7.     last:
  8.     return 0;
  9. }

Wymagany jest do tego podwójny znak ampersandu (&), co można traktować jako osobny operator lub rodzaj specjalnej składni… W każdym razie jest on konieczny, aby kompilator wiedział, że następująca dalej nazwa jest etykietą. Mają one bowiem swoją własną przestrzeń nazw, co oznacza, że możliwe jest występowanie np. zmiennej start i etykiety start w tym samym zasięgu.
Przykładowym wynikiem działania programu jest poniższa linijka:

first = 0x4004f8, second = 0x4004f8, last = 0x400519

Pierwsze dwa adresy są sobie równe i nie powinno to właściwie być zaskakujące. Jak można się bowiem łatwo domyślić, adresy etykiet to w istocie adresy instrukcji, które są nimi opatrzone. Dla osób programujących w asemblerze powinno być to dziwnie znajome :) W powyższym przykładzie zarówno first, jak i second mają adresy odpowiadające położeniu w pamięci kodu wywołania funkcji printf.

Wróćmy jednak do wspomnianych wcześniej wskaźników na funkcje. Mając bowiem pobrany adres etykiety, możemy go przypisać do takiego właśnie wskaźnika. Dzięki temu możemy etykiety możemy “wywoływać”, i to nawet z innych funkcji!
Jak to jednak w ogóle działa? Ilustruje to poniższy przykład, w którym funkcja wywołuje w ten sposób sama siebie:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4.  
  5. typedef int (*find_func)(int*, int, int);
  6. int find(int* tab, int n, int x) {
  7.     int i = 0;
  8.     top:
  9.     if (n == 0) return -1;
  10.     else {
  11.         if (*tab == x)  return i;
  12.         ++i; ++tab; --n;
  13.  
  14.         find_func tail_call = (find_func)&&top;
  15.         tail_call(tab, n, x);
  16.         return -2;  // nieosiągalne
  17.     }
  18. }
  19.  
  20. int main(int argc, char* argv[]) {
  21.     int tab[10] = { 2, 3, 6, 4, 8, 3, 9, 1, 7, 0 };
  22.     printf ("Element '%d' znaleziony na pozycji %d\n", 1, find(tab, 10, 1));
  23. }

Po pobieżnym przyjrzeniu się widać, że jest to zwykłe liniowe przeszukiwanie tablicy, w dodatku w sposób który wygląda na rekurencyjny… Jest to jednak specjalny rodzaj tzw. rekurencji ogonowej (tail recursion). Występuje ona wówczas, gdy wywołanie rekurencyjne jest ostatnią instrukcją funkcji. Taki przypadek może być wówczas zoptymalizowany przez co sprytniejsze kompilatory poprzez wyeliminowanie konieczności ponownego odkładania argumentów na stos. Kolejny poziom rekursji wykorzystuje po prostu te same argumenty – jest to możliwe, o ile nie ma konieczności rekurencyjnych powrotów i składania kolejnych wyników.
W powyższym przykładzie rekursja ogonowa występuje jednak niezależnie od jakichkolwiek optymalizacji, gdyż jest ona zawarta w “wywołaniu” etykiety top. Chociaż pozornie wygląda to jak wywołanie funkcji, nie powoduje utworzenia dodatkowej ramki stosu ani ponownego odłożenia na nim argumentów. Docelowa “funkcja” operuje na istniejącym stosie. W istocie więc mamy tu do czynienia z pewną wersją instrukcji goto, czyli zwykłego skoku.

Ciekawiej zaczyna się robić wtedy, gdy spróbujemy “wywołać” etykietę z innej funkcji, co – jak wspomniałem – jest zupełnie możliwe. Możliwe jest wówczas gładkie przekazanie jej wszystkich parametrów oraz zmiennych lokalnych, co automatycznie podpada pod kategorię Rzeczy Podejrzanych i Potencjalnie Niebezpiecznych :) Niemniej jednak jest to możliwe:
#include

int foo = 1, i;

typedef int (*args_func)(int argc, char* argv[]);
args_func process(int argc, char* argv[]) {
if (foo == 1) {
return &&inside;
}
if (foo == -1) {
inside:
for (i = 1; i < argc; ++i) printf("Arg #%d: %s\n", i, argv[i]); } return 0; } int main(int argc, char* argv[]) { process(0, NULL)(0, NULL); // sic }[/c] Powyższy program przekazuje swoje argumenty do funkcji process (która je wypisuje) mimo iż zupełnie tego nie widać w jej wywołaniu. Zresztą normalne wywołanie tej funkcji jedynie zwraca adres jej etykiety, pod który później skaczemy z maina bez dokładania nowej ramki do stosu.

W tym kodzie jest też kilka innych smaczków (jak chociażby rola zmiennej foo), których odkrycie pozostawiam jednak co bardziej dociekliwym czytelnikom :) Zaznaczę tylko, że cała ta (chyba) niezbyt praktyczna zabawa z pobieraniem adresów etykiet jest w gruncie rzeczy rozszerzeniem GCC i nie jestem pewien, czy będzie działać w jakimkolwiek innym kompilatorze.

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

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:
    1. 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ć ;)

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:
    1. // sortowanie listy w oparciu o własny predykat (C# 2.0 wzwyż)
    2. objects.Sort (delegate (object a, object b)
    3.     { 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& 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(); }[/cpp] 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 :)

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

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:

  1. 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):

  1. 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:

  1. class Foo
  2. {
  3.     public:
  4.         Foo(/* ... */, Foo** ppSelf = NULL) : m_ppSelf(ppSelf) { /* ... */ }
  5.         ~Foo() { if (ppSelf) *ppSelf = NULL; }
  6.     private:
  7.         Foo* m_ppSelf;
  8. };

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:

  1. void Foo::SomeMethod(/* ... */)
  2. {
  3.     if (!this) return;
  4.     // ...
  5. }

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 ;)

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

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ą:

  1. void Function(Foo* pFoo);
  2. 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ć.
    1. Foo foo; Foo* pFoo = new Foo();
    2. 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ć ;]

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

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ć :)

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


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