Posts tagged ‘C/C++’

Procent od funkcji drukujących

2008-03-08 23:58

Wszyscy znamy funkcję printf – część standardowej biblioteki C – oraz jej liczne warianty z przedrostkami i przyrostkami, służące wypisywaniu tekstów do różnych miejsc na różne sposoby. Jeśli z nich korzystamy, to czasem zdarza się, że chcemy wydrukować komunikat dany jako pojedynczy, znany już napis. A wówczas można wyprodukować coś, co w najprostszej wersji będzie wyglądało tak:

  1. char* s;
  2. // ...
  3. printf (s);

Tak się jednak stringów nie wypisuje – nawet mimo tego, iż w większości przypadków działa to bez problemów. Możemy bowiem trafić na przypadek złośliwy, a błędy objawiające się tylko czasami są, jak wiemy, jednymi z najgorszych…

Rzecz w tym, że w rodzinie funkcji printfopodobnych za to, co wydrukujemy, odpowiadają dwie rzeczy. Drugą z nich jest lista danych, mogąca mieć dowolnie dużo elementów; stąd też funkcje te przyjmują zmienną liczbę argumentów. Ale pierwszą jest tak zwany format, który mówi, jak te elementy należy interpretować: jako liczby całkowite, zmiennoprzecinkowe czy w końcu napisy. Ten argument jest łańcuchem znaków, występuje przed pozostałymi i w odróżnieniu od nich jest obowiązkowy.
Wywołanie printf(s); w istocie oznacza więc, że s nie jest tekstem do wypisania, ale formatem służącym interpretacji ewentualnych dalszych parametrów. Skoro jednak kolejnych argumentów nie ma, to wydaje się, że nie ma też problemu – zwłaszcza, że w wyniku tekst spod s faktycznie jest wypisywany. Jest tak jednak tylko momentu, gdy natrafimy na łańcuch zawierający znaczek procenta (%).

Jak wiemy, format dla funkcji typu printf może bowiem zawierać (i zwykle zawiera) znaczniki odnoszące się do jej dalszych argumentów. Niemal zawsze wpisujemy je ręcznie w kodzie, bo dokładnie wiemy, co chcemy wydrukować – np.:

  1. int n;
  2. printf ("n = %i", n);

To sprawia, że bardzo łatwo zacząć je traktować identycznie jak sekwencje ucieczki, czyli podciągi \n, \t, itd., zamieniane przez kompilator na odpowiadające im znaki (tutaj: końca wiersza i tabulatora). W wynikowych stringach nie ma więc po nich śladu, zamiast tego są odpowiednie znaki, których nie da się normalnie wpisać z klawiatury.
Ale znaczniki formatujące nie są interpretowane przez kompilator. Podczas działania programu w tym łańcuchu nadal siedzi %d, %f i każdy inny znacznik rozpoczynający się od procenta. Jeśli więc łańcuch s z wywołania printf(s); przypadkiem zawiera znak procenta, to funkcja mylnie potraktuje go i znaki po nim występujące jako znacznik formatujący. Zachowanie może być wtedy różne – w najlepszym wypadku ów procent i następny znak zostaną po prostu “zjedzone” – ale zawsze będzie różne od naszych oczekiwań.

Konkluzja? Jest oczywiście taka, aby zawsze pamiętać o formacie i nawet jeśli wypisujemy “tylko” łańcuch znaków, umieścić w nim %s:

  1. printf ("%s", s);

Różnica mała, lecz ważna :)

Tags: ,
Author: Xion, posted under Programming » 3 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

Stałe i zmienne pola statyczne

2008-02-27 1:06

Modyfikator static ma w C++ kilka różnych funkcji, a jedną z nich jest deklarowanie składników statycznych w klasach. Jak doskonale wiadomo taki składnik jest wspólny dla wszystkich obiektów tejże klasy i można go używać nawet, jeśli takie obiekty w ogóle nie istnieją.
Zwykle nie ma większych problemów ze statycznymi metodami, natomiast w przypadku pól sprawa często potrafi się skomplikować. Zwłaszcza, że statyczne elementy zmienne i stałe deklarujemy nieco inaczej…

Przede wszystkim należy pamiętać, że statyczne zmienne są właściwie pewnym rodzajem zmiennych globalnych, różniącym się prawie wyłącznie pod względem składniowym. A w przypadku zmiennych globalnych (deklarowanych w nagłówkach poprzez extern) konieczne jest ich przypisanie do jakiegoś modułu kodu, czyli pliku .cpp. Dzięki temu linker może potem powiązać odwołania do tej zmiennej z nią samą niezależnie od miejsca jej użycia.
To samo dotyczy statycznych pól:

  1. // foo.hpp
  2. class Foo   { private:   static int ms_iBar; };
  3.  
  4. // foo.cpp
  5. int Foo::ms_iBar = 0;

Jest jednak drobna różnica między statycznymi stałymi a zmiennymi polami. W tym drugim przypadku możemy zmiennej nadać początkową wartość; robimy to już w pliku .cpp przy jej definicji – tak jak powyżej. Zrobienie tego od razu w deklaracji jest niedopuszczalne.
Natomiast dla stałych wartość podać musimy i zwykle czynimy to dla odmiany właśnie w pliku nagłówkowym, jeszcze wewnątrz bloku class:

  1. // foo.hpp
  2. class Foo   { public:   static const int BAR = 10; };
  3.  
  4. // foo.cpp
  5. const int Foo::BAR;

Wówczas z kolei nie możemy wręcz inicjalizować stałej ponownie w pliku .cpp!

Dość dziwne, prawda? Ale w miarę łatwo to wyjaśnić. Otóż stała ma, jak sama nazwa wskazuje stałą wartość. Dzięki temu, że jest podana w nagłówku, kompilator może zoptymalizować każde miejsce jej normalnego użycia. Zamiast odwoływania się do pamięci, wpisze po prostu wartość stałej “na sztywno” w kodzie wynikowym. Będzie więc to podobne do działania #define.
Tym niemniej jest to pewna niedogodność, jeśli statyczne stałe i statyczne zmienne definiuje się inaczej. Ale przecież to nie jedyna dziwna cecha C++, która sprawia, że tak ten język lubimy ;-)

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

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:

  1. int* p1;
  2. 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:

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

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

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

C++, czyli skodź to sam

2008-02-21 20:45

Zwolennicy C++ często argumentują, że w języku tym można bardzo, bardzo wiele. Kluczowe jest tu właśnie słówko ‘można’. Powiedziałbym mianowicie, że swego rodzaju potencjalność jest jego istotną cechą…

O co dokładnie chodzi? Najlepiej chyba mówią o tym przykłady – takie jak te poniższe:

  • W C++ nie ma delegatów, znanych np. z Delphi lub C#. Istnieje jednak konstrukcja wskaźników na składowe, przy pomocy których można owych delegatów zaimplementować.
  • W C++ nie występuje też fraza finally w blokach try. W większości przypadków można jednak obejść się bez niej, stosując technikę znaną jako RAII (Resource Acquistion Is Initializon– pozyskanie zasobu inicjalizuje zmienną).
  • W C++ nie ma wbudowanego w język mechanizmu odśmiecania pamięci (garbage collecting). Jednak dzięki temu, że typy zdefiniowane przez użytkownika potrafią niemal doskonale imitować typy wbudowane, można napisać własne sprytne wskaźniki, które wspierają odśmiecanie.
  • W C++ nie istnieje też uniwersalna klasa bazowa, z której wywodziłyby się wszystkie inne. Niemniej można taką wprowadzić, jeśli miałoby to nam ułatwić posługiwanie się obiektami.

Można więc to i tamto; powyższa lista z pewnością nie jest kompletna. Dawniej takie możliwości doskonale świadczyły o C++ jako o języku niebywale elastycznym. Robiły też dobrze dla jego efektywności oraz wstecznej kompatybilności z C. Krótko mówiąc, były niewątpliwymi zaletami.

Teraz jednak wydają się być listą braków, a w najlepszym przypadku niepodjętych, a koniecznych decyzji projektowych. Niezbędnych głównie dlatego, że twórcy nowszych języków nie wahali się ich podjąć. Dzięki temu w wielu przypadkach oszczędzili pracy i trudnych wyborów programistom, którzy z tych języków korzystają.
A programistom C++ wciąż pozostają możliwości… Niestety, aplikacji nie buduje się z kodu, który można by napisać, lecz z kodu już napisanego. Dlatego korzystanie z owych możliwości zamienia się często w uzupełnianie braków. I realizujemy wtedy stare powiedzenie: Jeśli programista chce mieć coś napisane, powinien napisać to sobie sam :P

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

Przesłanianie kontra nadpisywanie

2008-02-14 19:08

Jednym ze swego rodzaju kruczków programowania obiektowego jest różnica między przesłanianiem (hide) a nadpisywaniem (override) metod w klasach pochodnych. Niby nic w tym trudnego, dopóki nie uświadomimy sobie, że metody wirtualne – w przeciwieństwie do zwykłych – mogą podlegać obu tym procesom i że w ich przypadku różnią się one w dość istotny sposób.

Jeśli mianowicie mamy jakąś metodę wirtualną w klasie bazowej, to w klasie pochodnej teoretycznie możemy ją zarówno nadpisać, jak i przesłonić. Gdy zdecydujemy się na to pierwsze wyjście, wówczas nasz obiekt staje się polimorficzny. Posługując się wskaźnikiem lub referencją do obiektu klasy bazowej tak naprawdę możemy operować na obiekcie klasy pochodnej, a wywołując metodę wirtualną nie wiemy, która jej wersja będzie użyta. Wiemy jedynie, że będzie to ta “właściwa” (z klasy, do której należy nasz obiekt). No cóż, to podstawa OOP-u, jakkolwiekby na to nie patrzeć :)

  1. // klasa bazowa
  2. class CBase { public: virtual void Foo() { /* ... */ } };
  3.  
  4. // klasa pochodna
  5. class CDerived : public CFoo
  6. {
  7.    public:
  8.       // nadpisana metoda
  9.       void Foo()   { /* ... */ }
  10. };

Nie zawsze jednak musi tak być. Jeżeli metodę wirtualną się przesłoni, wtedy nic takiego nie zadziała i zostanie wywołana wersja z klasy bazowej. Typ obiektu nie zostanie bowiem rozpoznany w trakcie działania programu, a całe wywołanie będzie zapisane “na sztywno” w wynikowym kodzie. W gruncie rzeczy będzie tak samo, jakbyśmy przesłoni dowolną inną (niewirtualną) metodę.
O ile oczywiście możemy, bowiem w C++ metody wirtualnej przesłonić się po prostu nie da. Nadpisywanie jest po prostu wykonywane automatycznie i nie można z niego zrezygnować. W innych językach (jak w Delphi i C#) wymaga ono słowa kluczowego override. Tam też metody wirtualne można przesłaniać, stosując modyfikatory – odpowiednio reintroduce i new.

Żeby było zabawniej, przesłanianie i nadpisywanie może występować dla tej samej metody w tej samej hierarchii dziedziczenia. Metoda wirtualna zdefiniowana w klasie A może być na przykład nadpisywana w kolejnych klasach pochodnych A1, A2, A3, itd. I nagle w klasie B zostanie ona przesłonięta. Ba, ta przesłonięta wersja sama może być wirtualna i nadpisywana w dalszych klasach pochodnych: B1, B2, B3, …
Nie spotyka się tego często, jako że podobne wielopiętrowe drzewa dziedziczenia są rzadkie (i takie być powinny). Warto jednak o tym wiedzieć, bo nigdy nie wiadomo, kiedy podobny kruczek przybierze w naszym kodzie postać wielkiego kruka i zacznie nas dziobać zagadkowymi błędami ;]

Tags: , ,
Author: Xion, posted under Programming » 1 comment

Konstruktory bywają złe

2008-02-04 11:42

Konstruktor to taka dziwna funkcja składowa, która jest wywoływana w trakcie tworzenia obiektu. Jest więc w zasadzie odpowiedzialna za jego przygotowania do użytku, czyli odpowiednią inicjalizację. Tak przynajmniej jest w teorii.
W rzeczywistości jednak konstruktor powinien robić jak najmniej. Jest tak z kilku powodów:

  1. Konstruktor działa na nie w pełni utworzonym obiekcie, co sprawia, że nie zachowuje się on do końca tak, jak inne metody. Najlepszym przykładem jest to, że w konstruktorze nie działają polimorficzne wywołania funkcji wirtualnych dla obiektu, który jest tworzony.
  2. Jeżeli w konstruktorze coś pójdzie źle, i zostanie wyrzucony wyjątek, to destruktor nie zostanie wywołany, aby nasz niekompletny obiekt posprzątać. Jest tak właśnie z tego powodu, że ów obiekt będzie wtedy “niezupełnie skonstruowany”, zaś destruktory w założeniu mają zwalniać tylko w pełni zainicjowane obiekty.
  3. Konstruktory są wywołane w wielu różnych sytuacjach i czasami trudno jest śledzić, kiedy. Do takich sytuacji należą wszelkie instrukcje wprowadzające zmienne należące do danej klasy, konstruktory klas pochodnych, a także konstruktory klas, które zawierają instancje naszej klasy w charakterze pól. Łącznie może być ich całkiem dużo i lepiej upewnić się, że przy okazji są wykonywane tylko te czynności, które są niezbędnie konieczne.

Pewne duże obiekty mogą oczywiście wymagać skomplikowanej inicjalizacji. Wówczas, zamiast pakować całą tę logikę do konstruktora, lepiej jest prawdopodobnie oddzielić inicjalizację od właściwego tworzenia obiektu. W tym celu można go zaopatrzyć w metodę w rodzaju Create czy Load, która dokonuje właściwego przygotowania obiektu do działania.
Może wydawać się to lekkim zawracaniem głowy, ale dla nietrywialnych obiektów ma sens. Pozbywamy się wad wymienionych powyżej, a jednocześnie zyskujemy dodatkowe możliwości (choćby dlatego, że wspomniana metoda może być wirtualna).

Tags: ,
Author: Xion, posted under Programming » 1 comment
 


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