Proszę, nie czuj się oszukany, gdy powiem, że ta książka nie ma na celu nauczenia Cię programowania w języku C. Wprawdzie dowiesz się, jak tworzyć programy w C, ale najważniejsza umiejętność, jaką możesz zdobyć podczas lektury, to ścisłe programowanie defensywne. Obecnie zbyt wielu programistów przyjmuje założenie, że tworzony przez nich kod będzie działać zawsze i nigdy nie ulegnie awarii. W szczególności dotyczy to osób, które poznały przede wszystkim nowoczesne języki programowania, rozwiązujące wiele problemów za programistów. Dzięki lekturze tej książki i wykonaniu przedstawionych ćwiczeń dowiesz się, jak pisać oprogramowanie, które samo będzie w stanie bronić się przez złośliwą aktywnością i defektami. Programuję w języku C z konkretnego powodu: uważam, że ten język jest zepsuty. Charakteryzuje się wieloma decyzjami projektowymi, które miały sens w latach 70. ubiegłego stulecia, a teraz są zupełnie pozbawione sensu. Wszystko, od niczym nieograniczonego użycia wskaźników aż po bezlitośnie zepsute ciągi tekstowe kończone znakiem NULL, można winić za praktycznie wszelkie luki w zabezpieczeniach, wykrywane w programach utworzonych w języku C. Według mnie język C jest tak bardzo zepsuty, że choć jest dość powszechnie stosowany, to jednak pozostaje językiem, w którym najtrudniej utworzyć bezpieczny kod. Mógłbym w tym miejscu pokusić się o stwierdzenie, że napisanie bezpiecznego kodu jest łatwiejsze w asemblerze niż w C. Szczerze mówiąc — wkrótce się przekonasz, że jestem pod tym względem niezwykle szczery — nie uważam, że należy tworzyć nowy kod źródłowy w C. W takim razie mógłbyś zapytać, dlaczego zamierzam uczyć Cię tego języka. Odpowiedź jest prosta: ponieważ chcę, abyś stał się lepszym programistą — z dwóch następujących powodów. Po pierwsze, w języku C brakuje niemalże wszystkich nowoczesnych funkcji zabezpieczeń, co wymaga od programisty większej czujności i wiedzy o działaniu kodu. Jeżeli potrafisz utworzyć bezpieczny i niezawodny kod w języku C, to bez wątpienia będziesz umiał również tworzyć bezpieczny i niezawodny kod w dowolnym innym języku programowania. Zaprezentowane w tej książce techniki możesz zastosować w praktycznie każdym języku programowania, którego będziesz później używać. Po drugie, jeśli znasz C, zyskujesz bezpośredni dostęp do ogromnej ilości starszego kodu źródłowego, a ponadto masz opanowaną składnię bazową wielu języków pochodnych od C. Kiedy więc poznasz C, znacznie łatwiej będziesz mógł nauczyć się programowania w językach C++, Java, Objective-C i JavaScript. Także inne języki programowania staną się dla Ciebie łatwiejsze do opanowania. Nie zamierzam Cię zniechęcać; mam nadzieję, że lektura dostarczy Ci niesamowitej frajdy, będzie łatwa i wciągająca. Dużą zaletą książki są zawarte w niej projekty, których prawdopodobnie nie realizowałeś w innych językach programowania. Książka jest łatwa — wykorzystałem sprawdzone wzorce ćwiczeń, które powinieneś wykonać w języku C, a nowy materiał wprowadzam powoli. Jest wciągająca, ponieważ pokazuje, jak zepsuć kod źródłowy, a następnie zabezpieczyć go, co pomaga w jeszcze lepszym zrozumieniu poszczególnych koncepcji. Dowiesz się więc, jak przepełnić stos i jak uzyskać dostęp do niezarezerwowanej pamięci. Poznasz też częste mankamenty w programach utworzonych w języku C. Dzięki temu będziesz wiedział, czego należy unikać. Podobnie jak w przypadku pozostałych moich książek, lektura tej może być pewnym wyzwaniem. Jednak jeśli przez to przebrniesz, staniesz się programistą znacznie lepszym i pewniejszym siebie.  

Niezdefiniowane zachowania

Zanim zakończysz lekturę, zajmiesz się debugowaniem, analizą i poprawianiem praktycznie każdego uruchamianego programu w C, a następnie utworzysz nowy, solidny kod w języku C, którego potrzebujesz. Jednak tak naprawdę nie zamierzam uczyć Cię oficjalnego języka C. Wprawdzie poznasz język i dowiesz się, w jaki sposób go używać, ale oficjalny język C nie należy do zbyt bezpiecznych. Większość programistów nie tworzy solidnego kodu, co wynika z tak zwanego niezdefiniowanego zachowania (ang. undefined behavior). Niezdefiniowane zachowanie to część standardu ANSI C (ang. American National Standards Institute) wymieniającego wszystkie sposoby, na jakie kompilator języka C może odczytać tworzony przez Ciebie kod źródłowy. Nawet jeżeli będziesz tworzyć kod zgodny ze standardem ANSI C, działanie kompilatora wcale nie musi być spójne. Z niezdefiniowanym zachowaniem mamy do czynienia, gdy program w C odczytuje koniec ciągu tekstowego, co jest niezwykle często występującym błędem podczas programowania w języku C. Warto w tym miejscu przedstawić nieco kontekstu — C definiuje ciąg tekstowy jako bloki pamięci zakończone bajtem NULL, inaczej bajtem 0 (upraszczam tutaj definicję). Ponieważ wiele ciągów tekstowych pochodzi z zewnątrz programu, więc bardzo często zdarza się, że program w języku C otrzymuje ciąg tekstowy niezakończony bajtem NULL. W takim przypadku program próbuje wczytać do pamięci kolejne dane, co nieuchronnie prowadzi do awarii. Każdy inny język programowania opracowany po C próbuje uniknąć tego rodzaju sytuacji, ale nie C. Język C w bardzo niewielkim stopniu stara się chronić przed wystąpieniem niezdefiniowanego zachowania, co skłania programistów C do wniosku, że nie trzeba się przejmować tym problemem. Powstaje więc kod pełny potencjalnych pułapek związanych z brakiem znaku NULL na końcu ciągu tekstowego. Kiedy wskazujesz problematyczny kod, zwykle słyszysz: „To jest niezdefiniowane zachowanie, nie muszę się tym przejmować”. Tego rodzaju ślepe zaufanie do C przyczynia się do powiększania się problemu niezdefiniowanego zachowania, co sprawia, że kod w języku C jest często bardzo niebezpieczny. Kiedy tworzę kod w języku C, staram się uniknąć niezdefiniowanego zachowania. W tym celu opracowuję kod w taki sposób, aby wyeliminować niejasności, lub też tworzę kod, który próbuje bronić się przed niezdefiniowanym zachowaniem. Okazuje się jednak, że to zadanie bywa wręcz niemożliwe do zrealizowania, ponieważ istnieje tak ogromna liczba sytuacji, w których może wystąpić niezdefiniowane zachowanie, że stanowi to prawdziwy węzeł gordyjski połączonych ze sobą usterek kodu źródłowego w C. W książce pokażę Ci, jak można wywołać niezdefiniowane zachowanie, jak można go uniknąć (o ile to możliwe) oraz jak wywoływać je w kodzie źródłowym utworzonym przez innych programistów (o ile to możliwe). Powinieneś mieć jednak świadomość, że ucieczka przed właściwie losową naturą niezdefiniowanego zachowania jest praktycznie niemożliwa i można co najwyżej starać się tworzyć pozbawiony go kod.  

Ostrzeżenie Przekonasz się, że fanatycy języka C będą bardzo często próbowali zbagatelizować istnienie niezdefiniowanego zachowania. To kategoria programistów, którzy nie tworzą zbyt dużej ilości kodu źródłowego w C, ale mają nieco wiedzy o niezdefiniowanym zachowaniu i posługując się nią, próbują okazać swoją wyższość nad początkującymi programistami C. Jeżeli natkniesz się na takie osoby, proszę, nie przejmuj się nimi. Najczęściej nie są one praktykującymi programistami C, są aroganckie, agresywne i zamęczą Cię niezliczonymi pytaniami, starając się okazać Ci swoją wyższość, a na pewno nie pomogą Ci podczas tworzenia kodu źródłowego. Jeżeli kiedykolwiek będziesz potrzebował pomocy w trakcie tworzenia kodu źródłowego w C, po prostu napisz do mnie na adres help@learncodethehardway.org. Pomogę Ci z przyjemnością.  
 

C to język zarazem świetny i paskudny

  Niezdefiniowane zachowanie to jeden z dodatkowych powodów, dla którego nauka języka C jest dobrym posunięciem, jeśli chcesz stać się lepszym programistą. Gdy będziesz umiał tworzyć dobry, solidny kod w C, wykorzystując przekazaną przeze mnie wiedzę, poradzisz sobie w każdym języku programowania. Oczywiście, C ma także dobre strony — pod pewnymi względami to naprawdę elegancki język. Jego składnia jest naprawdę prosta, biorąc pod uwagę potężne możliwości samego języka. Bez wątpienia w tym należy się dopatrywać przyczyny tego, że przez ostatnie 45 lat tak wiele języków programowania zapożyczyło składnię z C. Warto również dodać, że C oferuje niezwykle duże możliwości przy minimalnym wykorzystaniu technologii. Kiedy już poznasz C, docenisz elegancję i piękno tego języka, a jednocześnie dostrzeżesz jego wady. C jest starym językiem i podobnie jak piękny pomnik — z daleka wygląda fantastycznie, natomiast z bliska widać wszystkie rysy i pęknięcia. Dlatego też zamierzam zaprezentować najnowszą wersję języka C, współdziałającą z najnowszymi wydaniami kompilatorów. Otrzymasz w ten sposób wiedzę o praktycznym, prostym i kompletnym podzbiorze C, który działa doskonale, sprawdza się wszędzie i pozwala na uniknięcie wielu pułapek. To będzie język C, którego osobiście używam w pracy, a nie encyklopedyczna wersja C, nieudolnie forsowana przez zatwardziałych fanatyków. Wiem, że używany przeze mnie język C jest solidny, ponieważ już od ponad dwóch dekad tworzę w nim przejrzysty i niezawodny kod, przeznaczony do przeprowadzania ogromnych operacji. Jak dotąd kod ten mnie nie zawiódł. Prawdopodobnie przetworzył już tryliony transakcji, skoro jest wykorzystywany przez firmy takie jak Twitter i Airbnb. Rzadko zdarzały się awarie lub udane ataki na luki w zabezpieczeniach. Utworzony przeze mnie kod od lat stanowi podstawę świata sieci Ruby on Rails — gdzie działa znakomicie i nawet chroni przed atakami wykorzystującymi luki w zabezpieczeniach. Inne serwery WWW w internecie często padają ofiarami nawet najprostszych ataków. Wypracowany przeze mnie styl tworzenia kodu źródłowego okazuje się niezawodny. Co ważniejsze, takie nastawienie do tworzenia kodu w języku C powinno być przyjmowane przez każdego programistę. To podejście do języka C i ogólnie programowania opiera się na jak najlepszym wykonaniu zadania i na założeniu, że nic nie działa prawidłowo. Inni programiści — co zaskakujące, nawet dobrzy programiści C — mają tendencję do zakładania, że wszystko będzie działać zgodnie z oczekiwaniami, choć jednocześnie liczą na ratunek ze strony niezdefiniowanego zachowania lub systemu operacyjnego; z reguły jednak nie sprawdza się to jako rozwiązanie. Pamiętaj o tym, gdy ktokolwiek zarzuci Ci, że kod przedstawiony w książce nie jest „rzeczywistym C”. Jeżeli taka osoba nie ma takiego samego doświadczenia i takich osiągnięć jak moje, może warto wykorzystać przedstawioną tutaj wiedzę i pokazać adwersarzowi, dlaczego jego kod nie jest zbyt bezpieczny. Czy to oznacza, że opracowany przeze mnie kod jest doskonały? Oczywiście, że nie. To jest kod w języku C. Utworzenie idealnego kodu w języku C jest niemożliwe, zresztą podobnie jak w każdym innym języku programowania. Stanowi to frajdę i jednocześnie frustrację w programowaniu. Mógłbym wziąć kod opracowany przez innego programistę i rozłożyć go na części pierwsze — podobnie ktoś inny może postąpić z kodem utworzonym przeze mnie. Każdy kod zawiera pewne wady. Ale najważniejsza różnica polega na tym, że zawsze staram się przyjąć założenie o ułomności mojego kodu, a następnie próbuję eliminować mankamenty. Oto mój prezent dla Ciebie — postaraj się dotrzeć do końca tej książki i poznać nastawienie określane mianem programowania defensywnego, które doskonale mi służy od ponad dwóch dekad. To nastawienie pomogło mi w napisaniu wysokiej jakości, niezawodnego oprogramowania.    

Czego się nauczysz?

  Celem tej książki jest dostarczenie Ci solidnej wiedzy z zakresu języka C, abyś umiał z jego wykorzystaniem samodzielnie tworzyć oprogramowanie lub modyfikować kod źródłowy opracowany przez innych. Po lekturze powinieneś sięgnąć po książkę napisaną przez twórców języka C — Briana W. Kernighana i Dennisa Ritchiego, Język ANSI C. Programowanie. Wydanie II (Helion 2010). Dzięki mojej książce:

  • poznasz podstawy składni C,
  • poznasz kompilację, pliki Makefile i linkery,
  • dowiesz się, jak wyszukiwać błędy i jak ich unikać,
  • opanujesz praktyki programowania defensywnego,
  • przekonasz się, jak łamać kod utworzony w C,
  • zobaczysz, jak tworzyć podstawowe oprogramowanie dla systemu UNIX.

Zanim dotrzesz do ostatniego ćwiczenia, będziesz miał wystarczającą wiedzę, aby tworzyć proste oprogramowanie systemowe, biblioteki oraz inne mniejsze projekty.    

Jak czytać tę książkę?

Ta książka jest przeznaczona dla programistów, którzy opanowali już przynajmniej jeden inny język programowania. Jeżeli nie znasz jeszcze żadnego, proponuję Ci inną moją książkę, Learn Python the Hard Way (Addison-Wesley 2013), która jest przeznaczona dla początkujących i doskonale sprawdza się jako pierwsza książka poświęcona programowaniu. Po zakończeniu lektury Learn Python the Hard Way powróć do tej książki. Jeżeli masz już doświadczenie w tworzeniu kodu, na początku moja książka może wydawać się nieco dziwna. Nie przypomina innych, gdzie czytasz akapit po akapicie, a następnie wpisujesz wskazane polecenia. Zamiast tego przygotowałem do każdego ćwiczenia klip wideo — kod wprowadzasz na samym początku, a dopiero później omawiam, co zostało zrobione. Takie podejście sprawdza się lepiej, ponieważ polega na wyjaśnianiu tego, co już zrobiłeś, zamiast na odwoływaniu się do czegoś zupełnie abstrakcyjnego, gdy nie masz o tym żadnego pojęcia. W zastosowanej przeze mnie strukturze książki istnieje kilka reguł, których koniecznie powinieneś przestrzegać:

  • O ile nie powiem inaczej, najpierw obejrzyj wideo dla danego ćwiczenia.
  • Samodzielnie wpisz cały kod, nie kopiuj go i nie wklejaj!
  • Wpisz kod dokładnie w takiej postaci, w jakiej jest przedstawiony w książce, łącznie z komentarzami.
  • Uruchom kod i upewnij się o otrzymaniu takich samych danych wyjściowych.
  • Jeżeli wprowadzony kod zawiera jakiekolwiek błędy, usuń je.
  • Wykonaj zadania dodatkowe, ale możesz pominąć te, na których zupełnie utknąłeś.
  • Zawsze staraj się najpierw samodzielnie rozwiązać problem, a dopiero później szukaj pomocy.

Jeżeli zastosujesz się do powyższych reguł i wykonasz wszystkie ćwiczenia podane w książce, a mimo to nadal nie będziesz umiał tworzyć kodu w języku C, to będziesz wiedział, że przynajmniej spróbowałeś. Język C nie jest dla każdego, ale próba jego opanowania i tak czyni Cię lepszym programistą.    

Wideo

Do każdego ćwiczenia przygotowałem wideo, do niektórych ćwiczeń nawet więcej niż tylko jedno. Klipy te powinny być uznawane za istotny składnik całości — mają ogromny wpływ na zastosowaną metodę edukacyjną. Wynika to z prostej przyczyny: wiele problemów pojawiających się podczas programowania w C to interaktywne kwestie związane z awarią kodu, debugowaniem i wydawaniem poleceń. Język C wymaga znacznie większej interakcji w celu usunięcia problemów, co jest przeciwieństwem sytuacji w językach takich jak Python i Ruby, gdzie kod po prostu działa. Dlatego też pewne zagadnienia (na przykład wskaźniki lub zarządzanie pamięcią) znacznie lepiej jest objaśnić na wideo, ponieważ wtedy mogę pokazać, jak faktycznie zachowuje się komputer. O ile nie zostanie wskazane inaczej, przed przystąpieniem do lektury danego ćwiczenia należy więc najpierw obejrzeć wideo, a dopiero później zabrać się do wykonywania ćwiczeń. W niektórych ćwiczeniach jeden klip wideo wykorzystuję do zaprezentowania problemu, natomiast rozwiązanie znajdziesz w kolejnym klipie. W większości pozostałych ćwiczeń używam wideo do przedstawienia problemu, a następnie odsyłam Cię do ćwiczeń praktycznych, co wieńczy poznawanie danego zagadnienia.    

Podstawowe umiejętności

  Zgaduję, że masz doświadczenie w tworzeniu kodu w mniej wymagającym języku programowania. Używalnymi językami pozwalającymi na ucieczkę od niechlujnego myślenia i niepewnych sztuczek są między innymi Python i Ruby. Być może programowałeś wcześniej w języku LISP, który udawał, że komputer to wyłącznie funkcjonalna fantazja dla małych dzieci. A może poznałeś Prolog i uważasz, że cały świat powinien być bazą danych, po której się poruszasz i w której szukasz wskazówek. Co gorsza, jestem pewien, że używałeś zintegrowanych środowisk programistycznych (ang. integrated development environment) i Twój mózg jest pełen dziur w pamięci, więc możesz mieć nawet problem z wpisaniem pełnej nazwy funkcji bez naciskania klawiszy Ctrl+spacja po każdych trzech znakach. Niezależnie od tego, jakie masz doświadczenie, prawdopodobnie będziesz mógł nieco poprawić umiejętności w wymienionych poniżej aspektach.  

Czytanie i pisanie

  To dotyczy w szczególności osób, które intensywnie korzystały ze zintegrowanych środowisk programistycznych. Zauważyłem ogólną prawidłowość, że programiści zbyt często jedynie przeglądają tekst i mają problemy z czytaniem ze zrozumieniem. Po prostu tylko przeglądają kod, którego sposób działania powinni dokładnie przeanalizować, i nie poświęcają wystarczająco dużo czasu na jego zrozumienie. Inne języki programowania oferują narzędzia pozwalające programistom na uniknięcie rzeczywistego pisania kodu. Gdy taka osoba stanie przed koniecznością utworzenia kodu w C, to pojawia się prawdziwy kłopot. Trzeba zacząć od uzmysłowienia sobie, że każdy ma taki problem. Rozwiązaniem jest zwolnienie tempa oraz skrupulatne czytanie tekstu i pisanie kodu. Na początku może się to wydawać bolesne i irytujące, ale jeśli często będziesz robił przerwy, zadanie stanie się łatwiejsze do wykonania.    

Zwracanie uwagi na szczegóły

  Każdy ma z tym problem i to jest jedna z najczęstszych przyczyn powstawania nieprawidłowo działającego oprogramowania. W innych językach programowania być może nie trzeba na to zwracać dużej uwagi, ale w przypadku C wymagane są pełna koncentracja i skupienie — kod jest uruchamiany bezpośrednio w maszynie, a sama maszyna jest bardzo wybredna. Gdy programujesz w C, nie ma miejsca na podejście w stylu „podobne do” lub „prawie” i dlatego trzeba zwracać dużą uwagę na szczegóły. Dwukrotnie sprawdzaj kod. Ponadto zakładaj, że może działać nieprawidłowo, o ile nie udowodnisz, że jest inaczej.    

Wychwytywanie różnic

  Poważnym problemem osób posiadających doświadczenie w pracy z innymi językami programowania jest to, że przystosowali mózg do wychwytywania różnic w znanym im języku, a nie w C. Kiedy utworzony przez siebie kod porównujesz z moim, Twój wzrok zatrzymuje się na znakach, o których sądzisz, że nie mają znaczenia, lub które pozostają dla Ciebie nieznane. Pokażę Ci, jak starać się wychwytywać popełnione błędy. Pamiętaj jednak, że jeśli Twój kod nie jest dokładnie taki sam jak w tej książce — będzie błędny.    

Planowanie i debugowanie

  Uwielbiam inne, łatwiejsze języki programowania, ponieważ mogę z nimi eksperymentować. Mam możliwość wprowadzenia koncepcji w interpreterze danego języka i natychmiast otrzymuję wynik. Takie rozwiązanie doskonale sprawdza się podczas wypróbowywania pomysłów. Czy jednak zwróciłeś uwagę, że jeśli stosujesz podejście sztuczki aż do chwili, gdy kod działa, to ostatecznie przygotowane rozwiązanie nie działa? Język C jest trudniejszy, ponieważ wymaga wcześniejszego zaplanowania oczekiwanego wyniku. Oczywiście możesz poeksperymentować z ideami, ale wcześniej niż w innych językach programowania musisz zabrać się do rzeczywistej pracy nad projektem. Pokażę Ci, jak planować kluczowe aspekty programu, jeszcze zanim przystąpisz do tworzenia kodu — to również pomaga w staniu się lepszym programistą. Nawet niewielki etap planowania może ułatwić późniejszą pracę nad oprogramowaniem. Nauka języka C czyni z Ciebie lepszego programistę, ponieważ z wymienionymi powyżej kwestiami spotykasz się w jej trakcie odpowiednio wcześnie i często. Jeżeli napiszesz kod niechlujnie, po prostu nie będzie działał. Wielką zaletą języka C jest jego prostota — można go opanować samodzielnie. To doskonały język, pozwalający na poznanie maszyny i poszerzenie umiejętności w zakresie programowania.    


Fragment pochodzi z książki:

Programowanie w C. Sprytne podejście do trudnych zagadnień, których wolałbyś unikać (takich jak język C)

Wydawnictwo Helion 2016 Spis treści >> Przejdź do księgarni >>