Śledzenie wykonywania programu to świetna metoda na znajdowanie przyczyn wielu błędów. Ale żeby coś sensownie śledzić, to najpierw zwykle trzeba dojść do interesującego fragmentu kodu – to zaś umożliwiają punkty przerwania, czyli po polsku breakpointy ;)
W najbardziej podstawowej wersji działają one niezwykle prosto i zwyczajnie zatrzymują debuger na wskazanej instrukcji. W Visual Studio możemy jednak wykorzystać w sposób znacznie bardziej zaawansowany; wystarczy bowiem – po postawieniu breakpointa kliknąć weń prawym przyciskiem myszy i już ukazuje się nam wielce interesujące menu podręczne. Mamy tam kilka przydatnych opcji, do których należą między innymi:
if
, a punkt przerwania chcemy ustawić nie na sprawdzaniu jej warunku, lecz w środku jej bloku.OutputDebugString
czy nawet dedykowanych logerów.I tyle właśnie potrafią breakpointy w VS. Całkiem sporo, jak na jedną niepozorną, czerwoną kropkę :]
W C# obiekty zwykle dostępne są poprzez referencje. Zatem zmienna typu T
(jeśli jest on klasą) nie zawiera samego obiektu T
, lecz tylko odwołanie do niego. Porównując dwie takie zmienne przy pomocy operatora ==
domyślnie sprawdzamy więc, czy pokazują one na ten sam obiekt. Podobnie jest też przy użyciu domyślnej wersji metody System.Object.Equals
; robi ona dokładnie to samo, co wspomniany operator. Można mądrze powiedzieć, że oba mechanizmy sprawdzają relację identyczności obiektów.
Czasami jednak chodzi nam o coś innego: chcemy sprawdzić, czy dwa obiekty są równe, np. w sensie zawartości swoich pól. Taka równość może zachodzić także wtedy, gdy obiektyte zostały stworzone zupełnie niezależnie. Znów można mądrze stwierdzić, że chcąc dokonać takiego sprawdzenia, realizujemy dla obiektów semantykę wartości. Co wtedy zrobić?… Ano przeciążyć tudzież nadpisać wspomnianą metodę Equals
i/lub operator ==
.
I tu zaczynają się schody, bo wcale nie jest łatwo zrobić to poprawnie, a jeszcze trudniej jest zrobić to sensownie. Wszystko zależy od tego, czy nasza klasa ma realizować wyłącznie ową nieszczęsną semantykę wartości czy też czasami będziemy jednak sprawdzać, czy dwie referencje pokazują na ten sam obiekt (a nie na dwa równe obiekty).
==
, jak i metodę Equals
– i to przeciążyć tak, aby działały tak samo. Wtedy zwykle zajmujemy się najpierw operatorem, a później wykorzystujemy go w metodzie:
W takiej sytuacji prawdopodobnie powinniśmy też posłużyć się strukturą. Najpewniej chodzi nam bowiem o typ, który ma zachowywać się jak podstawowy: czyli coś w stylu wektora, macierzy, kwaternionu, itp.
Equals
. To jej powinniśmy używać, do sprawdzenia relacji równości między obiektami. Kiedy zaś chcemy zwyczajnie porównać referencje, możemy wtedy uciec się do operatora ==
.Uff, całkiem to zawiłe, prawda? Niestety nie jest łatwo uchwycić różnicę między równością a identycznością – a w językach, które obiekty realizują przez referencję sytuacja komplikuje się jeszcze bardziej. Zaś już chyba całkiem rozmywa się wtedy, gdy niektóre typy traktujemy per wartość, a niektóre per referencja…
A tak przecież jest C#. I jednocześnie podobno to jeden z prostszych języków do nauki i użytkowania :D
Żeby w C++ użyć w naszym programie kodu pochodzącego ‘z zewnątrz’ – a więc jakiejś biblioteki – trzeba się trochę napocić. Musimy bowiem odpowiednio “nakarmić” informacjami zarówno kompilator, jak i linker, co czasami skutkuje tym, że konieczne są w sumie trzy kroki. Są to:
#pragma
bezpośrednio w kodzie).#include
. To ukłon w stronę kompilatora, aby mógł on poprawnie zidentyfikować wszystkie nazwy (funkcje, klasy, itp.) pochodzące z biblioteki.std::list
zamiast list
) lub skorzystanie z dyrektyw using
.To w sumie całkiem sporo pracy, wynikającej z niezbyt zautomatyzowanego sposobu budowania programów. W innych językach (głównie w tych, których pojęcie ‘projektu’ jest częścią ich samych, a nie tylko IDE) drugi z tych kroków często nie występuje w ogóle, bo został całkowicie zintegrowany z trzecim. Zawsze jednak trzeba wyraźnie wskazać, gdzie znajduje się kod biblioteczny, który ma zostać dołączony do naszego skompilowanego programu – czyli wykonać krok pierwszy. Ta czynność, często zapominana (bo zwykle nie znajdująca odzwierciedlenia w samym kodzie) jest bowiem najważniejsza.
Jak wiadomo, do konstruktorów w C++ możemy doczepić listy inicjalizacyjne, pozwalające nadawać początkowe wartości polom macierzystej klasy (i nie tylko zresztą). Związany jest z tym pewien pozornie zaskakujący fakt, dotyczący kolejności inicjalizacji tych pól:
Są one zawsze ustawiane w porządku zgodnym z ich deklaracjami w klasie. Tak więc powyżej inicjalizowane będzie najpierw pole a
, zaś potem b
– mimo że w konstruktorze kolejność jest odwrotna. Oczywiście w takim prostym przypadku nie ma to znaczenia, ale jeśli między polami występują zależności, wtedy może to być przyczyną “dziwnych” błędów. Porządniejsze kompilatory ostrzegają na szczęście przed rozbieżnymi kolejnościami pól w klasie i na liście w konstruktorze.
Można naturalnie zapytać, dlaczego w ogóle jest to zorganizowane tak nieintuicyjnie. Pierwsza nasuwająca się odpowiedź – “bo to C++” – nie jest bynajmniej zadowalająca :) Tak naprawdę przyczyną jest to, iż konstruktorów może być naturalnie więcej niż jeden:
i mogą mieć one różną kolejność pozycji na liście inicjalizacyjnej. Gdyby to ona liczyła się bardziej, wtedy różne obiekty tej samej klasy byłyby konstruowane na różne sposóby. Co gorsza, sposób tej konstrukcji (kolejność inicjalizacji pól) musiałby zostać gdzieś zapamiętany na czas życia obiektu, jako że jego pola powinny być później zniszczone w kolejności odwrotnej do konstrukcji – zgodnie z zasadami języka.
Krótko mówiąc, zastosowanie “intuicyjnego” podejścia skutkowałoby nie tylko znacznie większymi niejasnościami, ale i dodatkowymi kłopotami zarówno dla programisty, jak i kompilatora.
Do programowania używamy głównie pełnowymiarowych IDE. W repertuarze narzędzi koderskich dobrze jest jednak mieć także edytor plików tekstowych bez całej tej projektowo-kompilacyjnej otoczki. Przydaje się on w wielu różnych sytuacjach, począwszy chociażby od typowego przypadku, gdy chcemy podejrzeć zawartość jakiegoś pliku z kodem bez odpalania całego “ciężkiego” środowiska. Poza tym wcale nie tak rzadko zdarza się, że musimy skrobnąć coś w języku, którego niekoniecznie używamy intensywnie na co dzień (jak choćby którymś z języków skryptowych).
Minimum, jakie taki programistyczny edytor powinien spełniać, to oczywiście podświetlanie składni. Wypadałoby też, by zawierał on większość funkcjonalności, jaką w zakresie manipulowania kodem oferuje IDE, np. wyszukiwanie w oparciu o wyrażenia regularne czy wyświetlanie numerowania linii i białych znaków. Dobrze by też było, gdyby umożliwiał o edycję kilku plików naraz. Prawdopodobnie najważniejsze jest jednak to, aby takie edytor był lekki: uruchamiał się jak najszybciej i tak też działał, nie zajmując przy tym wielu zasobów systemowych.
Pewnie każdy ma swój ulubiony program tego rodzaju, ale mimo to nie omieszkam polecić aplikacji, którą sam używam w charakterze “mniejszego edytora”: Programmer’s Notepad. Umie ona wszystko to, o czym wspomniałem wyżej, i jeszcze wiele więcej. Wśród poważniejszych braków, jakie mogę jej zarzucić, przypominam sobie jedynie brak wbudowanych narzędzi do konwersji między różnymi 8-bitowymi standardami kodowania znaków (z przeróżnymi UTF-ami itp. nie ma bowiem problemu). Ponieważ jednak możliwe jest dodawanie też własnych narzędzi uruchamianych z wiersza poleceń, w razie potrzeby można sobie odpowiednią funkcjonalność dodać samodzielnie :)
W C++ dynamiczne tablice z więcej niż jednym wymiarem tworzy się zwykle w sposób dość złożony. Najpierw bowiem – w przypadku dwóch wymiarów – tworzymy tablicę wskaźników na jej wiersze, a następnie same wiersze:
Skądinąd wiadomo bowiem, że zapis tab[i]
jest tylko cukierkiem składniowym zastępującym *(tab + i)
. Dlatego też w wyniku pierwszej dereferencji (indeksowania) musimy otrzymać wskaźnik (na wiersz tablicy), aby ponowną operację przeprowadzić drugi raz i dostać się do pojedynczego elementu.
Kiedy zaś tablica 2D jest umieszczona w jednym kawałku pamięci, wtedy element tab[i][j]
powinien zostać obliczony inaczej – np. jako *(tab + j * width + i)
, jeśli elementy są układane w pamięci wierszami. Kompilator musiałby więc skądś wiedzieć, jaka jest szerokość (width
) tablicy, a ponadto nie rozpatrywać każdej pary nawiasów kwadratowych osobno, lecz traktować je jako łączną operację indeksowania. Zwłaszcza pierwszy wymóg nie wydaje się rozsądny.
Warto jednak – jeśli zależy nam efektywności – używać drugiego sposobu przechowywania tablic dwuwymiarowych:
Dostęp do elementów jest wtedy szybszy, bo oszczędzamy sobie jednej dereferencji wskaźnika (która może być kosztowna, jeśli tablica jest porozrzucana po pamięci). Szczegóły indeksowania można zaś opakować w zgrabną klasę z przeciążonymi operatorami.
Albo po prostu użyć gotowego rozwiązania, np. klasy multi_array
z Boosta :)
Informatyka zna świetne rozwiązania wielu złożonych problemów, takich jak sortowanie czy wyszukiwanie najkrótszej drogi. Użycie tych powszechnie znanych algorytmów – nawet tych najbardziej skomplikowanych – jest zwykle całkiem proste. Albo bowiem dysponujemy już gotową implementacją, albo bez większych problemów możemy takową samodzielnie popełnić – z ewentualnymi usprawnieniami własnymi.
Często jednak zdarza się, że trzeba wymyślić coś znacznie mniejszego, rozwiązującego mniej modelowy, ale za to bardziej praktyczny problem. Niepotrzebne jest wtedy arcydzieło algorytmiki, lecz coś, co po prostu będzie dobrze działać. Osobiście uważam, że opracowywanie właśnie takich małych rozwiązań jest jedną z najciekawszych rzeczy w programowaniu w ogóle.
Przykłady? Jest ich tak dużo i są tak odmienne od siebie, że chyba niemożliwe jest podanie choćby kilku odpowiednio reprezentatywnych. Może to być krótki kod parsujący jakiś prosty tekst i wyciągający z niego pewnie informacje. Może to być metoda na przeskalowanie obrazka z zachowaniem jego aspektu (ilorazu szerokości do wysokości). Może to być również kod pozycjonujący jakąś kontrolkę wewnątrz okna o zmiennym rozmiarze. Może też chodzić o wyznaczenie rezultatu pojedynczego ataku w turowej grze strategicznej czy RPG. A nawet o, jak to ktoś ładnie nazwał, “silnik do pauzy” w owej grze ;] I tak dalej…
Niby nic skomplikowanego, czasami wręcz oczywistego – a jednak przecież trzeba to w miarę potrzeb wymyślać i zakodowywać. Bo gotowych rozwiązań problemów tak specyficznych po prostu nie ma. A bez takich małych algorytmów nie działałby żaden program – także ten, który musi używać również i tych “dużych” rozwiązań.
Może więc właśnie tutaj tkwi istota programowania?… Kto wie :)