132 78 22MB
Polish Pages 616 Year 2013
Tytuł oryginału: Head First C Tłumaczenie: Piotr Rajca ISBN: 978-83-246-5235-8 © 2013 Helion S.A. Authorized Polish translation of the English edition of Head First C, 1st edition, ISBN 9781449399917 © David Griffiths and Dawn Griffiths. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Wydawnictwo HELION dołożyło wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie bierze jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Wydawnictwo HELION nie ponosi również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: [email protected] WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/cruszg.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/cruszg_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Poleć książkę na Facebook.com • Kup w wersji papierowej • Oceń książkę
• Księgarnia internetowa • Lubię to! » Nasza społeczność
Opinie o książce C. Rusz głową! „Całkiem możliwe, że C. Rusz głową! mogłoby się okazać najlepszą książką o C wszech czasów. I wcale nie mówię tego lekkim tonem. Bez trudu mógłbym sobie wyobrazić, że ta książka staje się obowiązkową lekturą na wszystkich akademickich kursach programowania w języku C. Większość książek poświęconych programowaniu prowadzi całkowicie przewidywalną ścieżką, prezentując kolejno słowa kluczowe, struktury sterujące, składnię, operatory, typy danych, procedury. Mogą one służyć jako bardzo użyteczne źródło informacji encyklopedycznych oraz jako dobre wprowadzenie do języka, mające charakter nieco akademicki. Jednak ta książka przedstawia podejście całkowicie odmienne. Ona uczy, jak zostać prawdziwym programistą C. Bardzo żałuję, że nie miałem jej 15 lat temu!” — Dave Kitabjian, kierownik działu rozwoju oprogramowania, NetCarrier Telecom „C. Rusz głową! jest przystępnym, lekkim wprowadzeniem do programowania w języku C, napisanym w klasycznym stylu serii Rusz głową!. Obrazki, żarty, ćwiczenia i laboratoria delikatnie, choć konsekwentnie prezentują Czytelnikowi podstawy języka C — tablice, wskaźniki, struktury oraz funkcje — a następnie przechodzą do bardziej zaawansowanych zagadnień związanych ze standardem POSIX oraz programowaniem w systemie operacyjnym Linux, takich jak procesy oraz wątki”. — Vince Milner, programista
Opinie o innych książkach z serii Rusz głową! „Książka Java. Rusz głową! Kathy i Berta upodabnia drukowane strony książki do graficznego interfejsu użytkownika w stopniu, jakiego jeszcze nigdy nie widziałeś. W lekko drwiący, nowoczesny sposób autorzy tej książki sprawiają, że nauka Javy staje się absorbująca, a Czytelnik wciąż zastanawia się: »Co też oni nowego wymyślą?«”. — Warren Keuffel, „Software Development Magazine” „Oprócz zajmującego stylu, który zmienia Czytelnika z nowicjusza w zachwyconego wojownika Javy, książka Java. Rusz głową! oferuje całą masę praktycznych zagadnień, które w innych książkach są pozostawione jako przerażające »zagadnienia do opracowania we własnym zakresie«. To mądra, lekko kpiąca, nowoczesna i praktyczna pozycja — nie ma wielu książek, które mogą rościć sobie prawa do takiej oceny, sprostać jej, a jednocześnie uczyć Czytelników serializacji obiektów i protokołów sieciowego uruchamiania aplikacji”. — Dr Dan Russell, kierownik badań nad nauką i doświadczeniami użytkowników w IBM Almaden Research Center (uczy także sztucznej inteligencji na Uniwersytecie Stanforda) „Ta książka jest szybka, nonszalancka, zabawna i absorbująca. Uważaj — przez przypadek możesz się jeszcze czegoś nauczyć!” — Ken Arnold, zatrudniony wcześniej na stanowisku starszego inżyniera w Sun Microsystems; współautor (wraz z Jamesem Goslingiem, twórcą języka Java) książki The Java Programming Language „Czuję się, jakby właśnie zdjęto z mojej głowy tysiące kilogramów książek”. — Ward Cunningham, twórca wiki oraz założyciel Hillside Group „To właściwy ton dla przeciętnego, niedbale wyluzowanego programistycznego guru drzemiącego w każdym z nas. Doskonała książka do poznania praktycznych strategii programowania — zmusza mój mózg do działania bez konieczności przebijania się przez wyświechtane i nieświeże profesorskie gadki”. — Travis Kalanick, założyciel sieci Scour i witryny Red Swoosh; umieszczony na liście TR100 „Są książki, które się kupuje, które się zatrzymuje, które się trzyma na biurku, a teraz — dzięki wydawnictwu O’Reilly i zespołowi Head First — jest jeszcze przedostatnia kategoria: książki z serii Rusz głową!. Te książki są zużyte, wymięte, noszone zawsze przy sobie. SQL. Rusz głową! jest zawsze na samym wierzchu sterty moich książek. Rany… ta książka jest u mnie, nawet w wydaniu PDF, wystrzępiona i podarta”. — Bill Sawyer, ATG Curriculum Manager, Oracle „Zachwycająca przejrzystość, humor oraz znacząca doza mądrości zawarte w tej książce sprawiają, że nawet osoby niemające nic wspólnego z programowaniem zaczynają dobrze myśleć o rozwiązywaniu problemów”. — Cory Doctorow, współredaktor Boing Boing; autor książek Down and Out in the Magic Kingdom oraz Someone Comes to Town, Someone Leaves Town
Opinie o innych książkach z serii Rusz głową! „Dostałem tę książkę wczoraj, zacząłem ją czytać i… nie mogłem przestać. Bez dwóch zdań — jest odlotowa. Jest zabawna, lecz opisuje wiele zagadnień, które zostały przedstawione w bardzo trafny sposób. Naprawdę jestem pod wrażeniem”. — Erich Gamma, IBM Distinguished Engineer oraz współautor książki Wzorce projektowe. Rusz głową! „Jedna z najbardziej zabawnych i najdowcipniejszych książek o projektowaniu oprogramowania, jakie kiedykolwiek czytałem”. — Aaron LaBerge, VP Technology, ESPN.com „To, co kiedyś było długim procesem nauki pełnym prób i błędów, zostało zgrabnie zredukowane do postaci zajmującej książki”. — Mike Davidson, prezes Newsvine, Inc. „Elegancki projekt jest wizytówką każdego rozdziału tej książki, a każde pojęcie jest prezentowane z równą dozą pragmatyzmu i dowcipu”. — Ken Goldstein, wiceprezes Disney Online „Ja ♥ Head First HTML with CSS and XHTML — ta książka uczy wszystkiego, co powinieneś wiedzieć, przedstawiając informacje w zabawny sposób”. — Sally Applin, projektantka interfejsów użytkownika i artystka „Zwykle czytając książkę lub artykuł na temat wzorców projektowych, muszę raz na jakiś czas zatrzymać się, aby sprawdzić, czy zwracam uwagę. Nie w przypadku tej książki. Nieważne, jak głupio to zabrzmi, ale dzięki tej książce nauka wzorców projektowych to sama przyjemność. Podczas gdy inne książki na temat wzorców projektowych są nudne jak »bla, bla, bla«, ta książka jest jak śpiew na platformie »Bawmy się, dziewczyno!«”. — Eric Wuehler „Dosłownie kocham tę książkę. W rzeczywistości pocałowałem tę książkę na oczach mojej żony”. — Satish Kumar
Inne książki wydawnictwa O’Reilly o tej samej tematyce: C in a Nutshell Język C. Programowanie C. Leksykon kieszonkowy Algorytmy w C C i C++. Bezpieczne programowanie. Receptury
Inne książki z serii Rusz głową! Analiza i projektowanie obiektowe. Rusz głową! C#. Rusz głową! Excel. Rusz głową! Head First Ajax. Edycja polska Head First Algebra. Edycja polska Head First Design Patterns. Edycja polska Head First EJB. Edycja polska Head First HTML with CSS & XHTML. Edycja polska Head First JavaScript. Edycja polska Head First Object-Oriented Analysis and Design. Edycja polska Head First PHP & MySQL. Edycja polska Head First Ruby on Rails. Edycja polska Head First Servlets & JSP. Edycja polska. Wydanie II Head First Software Development. Edycja polska Head First Web Design. Edycja polska Head First. Fizyka. Edycja polska Head First. Sieci komputerowe. Edycja polska Head First. Statystyka. Edycja polska HTML5. Rusz głową! Java. Rusz głową! Wydanie II jQuery. Rusz głową! Mobile Web. Rusz głową! SQL. Rusz głową! Wzorce projektowe. Rusz głową!
Dennisowi Ritchiemu (1941–2011), ojcu języka C
O autorach
Autorzy C. Rusz głową!
iths David Griff
Dawn Griffiths
David Griffiths zaczął programować w wieku 12 lat, kiedy obejrzał film dokumentalny poświęcony pracom Seymour Papert. W wieku 15 lat napisał implementację języka LOGO opracowanego przez Papert. Po zakończeniu studiów matematycznych na uniwersytecie zaczął pisać kod przeznaczony dla komputerów oraz artykuły w czasopismach dla ludzi. Pracował jako instruktor zwinnych metod programowania, programista, parkingowy, ale nie w takiej kolejności. Potrafi programować w ponad 10 językach i pisać prozę tylko w jednym, a kiedy nie pisze ani nie zajmuje się doradztwem, spędza większość czasu ze swoją uroczą żoną — i współautorką tej książki — Dawn. Przed C. Rusz głową! David napisał także dwie inne książki tej serii: Head First Rails oraz Head First Programming. Można go śledzić na Twitterze: http://twitter.com/dogriffiths.
viii
Wprowadzenie
Dawn Griffiths zaczynała jako matematyk na jednym z czołowych angielskich uniwersytetów, gdzie ukończyła studia matematyczne z wyróżnieniem. Później rozpoczęła karierę w branży produkcji oprogramowania i dysponuje już 15-letnim doświadczeniem w branży IT. Przed połączeniem sił z Davidem przy pracy nad książką C. Rusz głową! Dawn napisała dwie inne książki z tej serii (Head First. Statystyka. Edycja polska oraz Head First 2D Geometry) i brała udział w pracach nad wieloma innymi. Kiedy nie pracuje nad żadną z książek Rusz głową!, spędza czas, poprawiając swoje umiejętności z zakresu tai-chi, biegając, robiąc koronki i gotując. Uwielbia także podróżować i spędzać czas ze swym mężem, Davidem.
Spis treści
Spis treści (skrócony) Wprowadzenie
xxvii
1
Zaczynamy poznawać C. Dajmy nurka
1
2
Pamięć i wskaźniki. Na co wskazujesz?
41
2,5 Łańcuchy znaków. Teoria łańcuchów
83
3
Tworzenie małych programów narzędziowych. Rób jedną rzecz, ale rób ją dobrze
101
4
Stosowanie wielu plików źródłowych. Podziel go, rozbuduj go
155
5
1. laboratorium C. Arduino
205
Struktury, unie i pola bitowe. Wytocz swoje własne struktury
215
6
Struktury danych i pamięć dynamiczna. Budowanie mostów
265
7
Zaawansowane funkcje. Odpicuj swoje funkcje na maksa!
309
8
Biblioteki statyczne i dynamiczne. Wymienialny kod
349
2. laboratorium C. OpenCV
387
9
Procesy i wywołania systemowe. Przekraczanie granic
395
10
Komunikacja pomiędzy procesami. Dobrze jest porozmawiać
427
11
Gniazda i komunikacja sieciowa. Nie ma drugiego takiego miejsca jak 127.0.0.1
465
12
Wątki. To równoległy świat
499
3. laboratorium C. Blasteroidy
521
A
Pozostałości. Dziesięć najważniejszych rzeczy (których nie opisaliśmy)
537
B
Zagadnienia programowania w C. Powtórka z całego materiału
551
Spis treści (z prawdziwego zdarzenia)
W
Wprowadzenie Twój mózg jest skoncentrowany na C. Kiedy Ty starasz się czegoś nauczyć, Twój mózg robi Ci przysługę i stara się, by ta wiedza nie została utrwalona. Twój mózg myśli sobie: „Lepiej zostawić miejsce na naprawdę ważne rzeczy, takie jak dzikie zwierzęta, których należy unikać, albo rozważania, czy jeżdżenie na snowboardzie w stroju Adama to dobry pomysł”. Jak zatem możesz oszukać swój mózg i przekonać go, że Twoje życie zależy od znajomości C? Dla kogo jest przeznaczona ta książka? Wiemy, co sobie myślisz Metapoznanie
xxviii xxix xxxi
Zmuś swój mózg do posłuszeństwa
xxxiii
Przeczytaj to
xxxiv
Zespół recenzentów technicznych
xxxvi
Podziękowania
xxxvii
ix
Spis treści
Zaczynamy poznawać C
1
Dajmy nurka Czy chcesz zajrzeć do głowy komputera? Musisz napisać kod działający naprawdę szybko, na przykład na potrzeby nowej gry? A może program na Arduino? Albo we własnej aplikacji na iPhone’a użyć biblioteki napisanej przez kogoś innego? Jeśli tak, to skorzystaj z pomocy bohaterskiego C. C działa na znacznie niższym poziomie niż większość innych języków programowania, a zatem zrozumienie go daje nam znacznie większe pojęcie o tym, co się naprawdę dzieje w programie. C pozwala także lepiej zrozumieć inne języki programowania. A zatem bierz się do pracy, przygotuj kompilator, a już niedługo zaczniesz poznawać C.
C to język do pisania małych, szybkich programów
2
Ale jak wygląda skompilowany program napisany w C?
5
A jak można uruchomić program?
x
9
Dwa rodzaje poleceń
14
Oto kod, jakim aktualnie dysponujemy
15
Liczenie kart? W języku C?
17
Wartości logiczne to nie tylko sprawdzanie równości
18
Jak aktualnie wygląda nasz kod?
25
Pociąg do Switcherado
26
Czasami jeden raz nie wystarcza…
29
Pętle często mają taką samą strukturę…
30
Instrukcji break używamy, by wydostać się z pętli…
31
Twój niezbędnik C
40
Spis treści
Pamięć i wskaźniki
2
Na co wskazujesz? Jeśli naprawdę chcesz zrobić coś odlotowego w języku C, musisz się dowiedzieć, w jaki sposób zarządza on pamięcią. Język C daje nam bardzo dużą kontrolę nad tym, jak programy korzystają z pamięci komputera. W tym rozdziale zajrzysz za kulisy i zobaczysz, co dokładnie dzieje się podczas zapisywania i odczytywania zmiennych. Dowiesz się, jak działają tablice, jak unikać paskudnych błędów w trakcie wykonywania operacji na pamięci, a przede wszystkim przekonasz się, że opanowanie operacji na wskaźnikach i adresowania pozwoli Ci stać się rewelacyjnym programistą C.
Złapaliśmy wiatr w żagle, kapitanie!
Kod C zawiera wskaźniki
42
Grzebiemy w pamięci
43
Stawiamy żagle ze wskaźnikami
44
Spróbujmy przekazać wskaźnik do zmiennej
47
Stosowanie wskaźników
48
Jak przekazać łańcuch znaków do funkcji?
53
Zmienne tablicowe są jak wskaźniki…
54
Co myśli komputer, wykonując nasz kod?
55
Jednak zmienne tablicowe nie są tak do końca wskaźnikami
59
Dlaczego tablice naprawdę zaczynają się od 0?
61
Dlaczego wskaźniki mają typ?
62
Stosowanie wskaźników do wprowadzania danych
65
Używając funkcji scanf(), uważaj!
66
Alternatywą dla scanf() jest fgets()
67
Literały łańcuchowe nie mogą być nigdy modyfikowane
72
Jeśli chcesz zmienić łańcuch — skopiuj go
74
Ściąga z pamięci
80
Twój niezbędnik C
81
Kurs na Cancún!
latitude Arr! Wiosenne wakacje!
32 31 4 100 000 xi
Spis treści
Łańcuchy znaków
2,5
Teoria łańcuchów Korzystanie z łańcuchów nie ogranicza się do ich odczytywania. Przekonałeś się już, że w języku C łańcuchy znaków są w rzeczywistości tablicami danych typu char. Pozostaje jednak pytanie, co język C pozwala nam z nimi robić. W tym właśnie momencie na scenę wkracza string.h. To część standardowej biblioteki języka C, przeznaczona do wykonywania operacji na łańcuchach znaków. Jeśli chcemy połączyć ze sobą dwa łańcuchy, skopiować jeden łańcuch do drugiego bądź też porównać dwa łańcuchy znaków, to wszystkie te operacje można wykonać przy użyciu funkcji zadeklarowanych w pliku nagłówkowym string.h. W tym rozdziale dowiesz się, jak można stworzyć tablicę łańcuchów, a następnie przyjrzysz się nieco bliżej przeszukiwaniu zawartości łańcuchów przy użyciu funkcji strstr().
Desperacko poszukuję Franka
84
Utwórz tablicę tablic
85
Odnajdywanie łańcucha zawierającego określony tekst
86
Stosowanie funkcji strstr()
89
Czas na przegląd kodu
94
Tablica tablic czy tablica wskaźników?
98
Twój niezbędnik C
99
Szukaj łańcucha
Porównaj ze sobą dwa łańcuchy zn akó
w
łańcucha Utwórz kopię
xii
r st
ing
.h
znaków
Podziel łańcuch na małe fragmen
ty
Spis treści
Tworzenie małych programów narzędziowych
3
Rób jedną rzecz, ale rób ją dobrze Każdy system operacyjny udostępnia niewielkie programy narzędziowe. Niewielkie programy narzędziowe pisane w języku C wykonują wyspecjalizowane zadania, takie jak odczytywanie i zapisywanie plików czy też filtrowanie danych. Jeśli chcemy wykonać bardziej złożone zadanie, można nawet połączyć ze sobą kilka takich programów. W jaki jednak sposób tworzy się takie małe programy narzędziowe? W tym rozdziale przyjrzymy się elementom używanym podczas ich tworzenia. Dowiesz się, jak korzystać z opcji wiersza poleceń, jak zarządzać strumieniami informacji, czym są przekierowania, i błyskawicznie zdobędziesz nowe narzędzia.
Małe programy narzędziowe mogą rozwiązywać wielkie problemy
102
Oto sposób wykorzystania programu
106
Ale my nie używamy plików…
107
Możesz skorzystać z przekierowania
108
Przedstawiamy standardowy strumień błędów
118
Domyślnie strumień błędów jest wyświetlany na ekranie
119
fprintf() zapisuje dane w strumieniu
120
Zaktualizujmy kod, by korzystał z funkcji fprintf()
121
Niewielkie programy narzędziowe są elastyczne
126
Nie zmieniaj programu geo2json
127
Różne zadania wymagają różnych narzędzi
128
Połącz wejście i wyjście przy użyciu potoku
129
Program narzędziowy bermuda
130
A jeśli chcemy przekazywać wyniki do więcej niż jednego pliku?
135
Stwórz swoje własne strumienie danych
136
Nieco więcej o funkcji main()
139
Niech biblioteka wykona pracę za nas
147
Twój niezbędnik C
154 Standardowy strumień błędów jest podłączony do ekranu.
Standardowy strumień wejściow y jest podłączony do klawiatury.
Standardowy strumień wyjściowy jest podłączony do ekranu.
xiii
Spis treści
Stosowanie wielu plików źródłowych
4
Podziel go, rozbuduj go Jeśli piszesz duże programy, nie chcesz mieć równie dużych plików źródłowych. Czy jesteś sobie w stanie wyobrazić, jak trudna i czasochłonna byłaby pielęgnacja dużego programu korporacyjnego napisanego w formie jednego pliku źródłowego? W tym rozdziale dowiesz się, jak C pozwala na dzielenie kodu źródłowego na małe, poręczne fragmenty oraz jak potem utworzyć z nich jeden duży program. W międzyczasie dowiesz się także nieco o niuansach związanych z typami danych i poznasz swojego nowego najlepszego przyjaciela — program make.
gcc -c
gcc -o
xiv
Krótki przewodnik po typach danych
160
Nie umieszczaj czegoś dużego w czymś małym
161
Użyj rzutowania, by zapisać wartość zmiennoprzecinkową w zmiennej całkowitej
162
O nie… to bezrobotni aktorzy…
166
Zobaczmy, co się stało z kodem
167
Kompilatory nie lubią niespodzianek
169
Oddziel deklaracje od definicji
171
Tworzenie pierwszego pliku nagłówkowego
172
Jeśli oprogramowałeś często używane operacje…
180
Możesz rozdzielić kod, umieszczając go w osobnych plikach
181
Za kulisami kompilacji
182
Współdzielony kod trzeba umieścić w osobnym pliku źródłowym
184
To nie jest kosmiczna technologia… a może jest?
187
Nie rekompiluj każdego pliku
188
Najpierw skompiluj źródła do plików obiektowych
189
Ciągłe śledzenie zmian w plikach jest trudne
194
Zautomatyzuj kompilacje, używając narzędzia make
196
Jak działa make
197
Przekaż informacje o kodzie, używając pliku makefile
198
Startujemy!
203
Twój niezbędnik C
204
pis is tr Spis treści
1. laboratorium C
Arduino ! " #$ % % ! % % &
xv
Spis treści
Struktury, unie i pola bitowe
5
Wytocz swoje własne struktury Większość rzeczy w realnym życiu jest nieco bardziej złożona niż proste liczby. Do tej pory poznałeś jedynie podstawowe typy danych dostępne w języku C, ale co można zrobić, by wykroczyć poza to, co dają nam liczby i łańcuchy znaków — gdybyśmy chcieli odwzorować przedmioty z realnego świata? W języku C złożoności realnego świata można odwzorowywać przy użyciu struktur — typu struct. W tym rozdziale dowiesz się, jak łączyć podstawowe typy danych przy użyciu struktur, a nawet jak radzić sobie z niewiadomymi życia przy użyciu unii. Jeśli z kolei wolisz proste odpowiedzi typu „tak” lub „nie”, to mogą Ci się przydać pola bitowe.
To jest Marceli…
…ale do funkcji zostaj e przekazany jego klon.
Czasami musisz rozdawać wiele danych
216
Rozmowa trójstronna
217
Twórz swoje własne strukturalne typy danych przy użyciu struct
218
Po prostu daj im rybkę
219
Odczytuj pola struktur, używając operatora "."
220
Czy można umieścić jedną strukturę wewnątrz drugiej?
225
Jak zaktualizować zawartość struktury?
234
Ten kod klonuje żółwie
236
Potrzebny będzie wskaźnik na strukturę
237
(*t).age kontra *t.age
238
Czasami rzeczy podobnego typu wymagają danych różnych typów
244
Unie pozwalają używać bloku pamięci na różne sposoby
245
Jak stosować unie?
246
Zmienna typu enum przechowuje symbol
253
Czasami potrzebujemy kontroli na poziomie bitów
259
Pola bitowe zawierają dowolną liczbę bitów
260
Twój niezbędnik C
264
Oto żółw „t”.
xvi
Spis treści
Struktury danych i pamięć dynamiczna
6
Budowanie mostów Czasami prosta struktura nie wystarcza. Aby zamodelować bardzo złożone dane, często będziemy musieli łączyć ze sobą struktury. W tym rozdziale zobaczysz, jak korzystać ze wskaźników na dane typu struct, by łączyć niestandardowe typy danych w duże i złożone struktury danych. Poznasz kluczowe zasady, ucząc się tworzyć listy połączone. Dowiesz się także, jak sprawić, by Twoje struktury danych radziły sobie ze zmienną liczbą informacji, wykorzystując do tego celu dynamiczne przydzielanie pamięci na stercie i zwalniając używaną pamięć, kiedy nie będzie już potrzebna. A gdy porządkowanie pamięci stanie się zbyt trudne, dowiesz się, jak może nam pomóc program valgrind. Czy potrzebujesz elastycznego sposobu przechowywania danych?
266
Listy połączone przypominają łańcuchy danych
267
Listy połączone pozwalają na dodawanie elementów
268
Tworzenie struktur rekurencyjnych
269
Tworzenie wysp w języku C…
270
Wstawianie elementów pośrodku listy
271
Pamięć dynamiczną rezerwuj na stercie
276
Zwróć pamięć, kiedy nie będzie już potrzebna
277
Proś o pamięć, wywołując malloc()…
278
Popraw kod, używając funkcji strdup()
284
Zwalniaj pamięć, gdy jej nie potrzebujesz
288
Przegląd systemu SPIES
298
Detektywi oprogramowania: stosowanie valgrind
300
Skorzystaj z programu valgrind kilkakrotnie, by zebrać więcej dowodów
301
Przeanalizuj dowody
302
Sprawdzenie poprawek
305
Twój niezbędnik C
307
Wyspa Skalista
Wyspa Zjaw
32 bajty danych na stercie w miejscu o adresie 4204853
Wyspa Chmur xvii
Spis treści
Zaawansowane funkcje
7
Odpicuj swoje funkcje na maksa! Proste funkcje są w porządku, jednak czasami możemy potrzebować czegoś więcej. Do tej pory koncentrowaliśmy się na sprawach podstawowych, ale co zrobić, kiedy do osiągnięcia celu będziemy potrzebowali większych możliwości i większej elastyczności? W tym rozdziale zobaczysz, jak podnieść IQ swojego kodu, przekazując funkcje jako parametry. Dowiesz się, jak można sortować, wykorzystując funkcje — komparatory. A na samym końcu nauczysz się, jak dzięki zastosowaniu zmiennej liczby argumentów tworzyć superelastyczne funkcje.
Szukając Pana Doskonałego…
310
Przekaż kod do funkcji
314
Musisz przekazać funkcji find() nazwę funkcji testującej
315
Każda nazwa funkcji jest wskaźnikiem do tej funkcji…
316
…ale nie ma żadnego typu danych reprezentującego funkcję
317
Jak utworzyć wskaźnik do funkcji
318
Posortuj to, używając standardowej biblioteki C
323
Użyj wskaźnika do funkcji, by określić porządek sortowania
324
Automatyzacja generowania listów do Jana
332
Stwórz tablicę wskaźników do funkcji
336
Zapewnij swoim funkcjom elastyyyyyczność
341
Twój niezbędnik C
348
Maszyna
xviii
Spis treści
Biblioteki statyczne i dynamiczne
8
Wymienialny kod Poznałeś już ogromne możliwości bibliotek standardowych. Nadszedł czas, byś użył mocy swojego własnego kodu. W tym rozdziale dowiesz się, jak tworzyć swoje własne biblioteki oraz jak wielokrotnie używać tego samego kodu w różnych programach. Co więcej, nauczysz się współużytkowania kodu w trakcie działania programu, co jest możliwe dzięki bibliotekom łączonym dynamicznie. Poznasz sekrety mistrzów kodowania. A pod koniec tego rozdziału będziesz już potrafił pisać kod, który w łatwy i efektywny sposób będzie można skalować oraz którym będzie można równie łatwo zarządzać.
Rodzynki, mąka, masło, anchois…
Kod, który możesz zabrać do banku
350
Nawiasy kątowe dołączają standardowe pliki nagłówkowe
352
A co zrobić, jeśli będziesz chciał współużytkować jakiś kod?
353
Współużytkowanie plików nagłówkowych
354
Współużytkowanie plików .o poprzez określanie pełnej ścieżki dostępu
355
Archiwum zawiera pliki .o
356
Utwórz archiwum, używając polecenia ar…
357
I w końcu kompiluj inne programy
358
Siłownia Rusz Głową wchodzi na scenę globalną
363
Obliczanie spalonych kalorii
364
Ale zagadnienie jest nieco bardziej skomplikowane…
367
Programy składają się z wielu fragmentów…
368
Łączenie dynamiczne następuje podczas działania programu
370
Czy pliki .a można łączyć podczas działania programu?
371
Najpierw utwórz plik obiektowy
372
Nazewnictwo bibliotek dynamicznych zależy od platformy systemowej
373
Twój niezbędnik C
385
Czy to ptak? Czy samolot? Nie, to przenaszalny plik obiektowy z metadanymi.
xix
Spis treści
2. laboratorium C
OpenCV $ ' "
( )
* %&$ % % " % +,&
xx
Spis treści
Procesy i wywołania systemowe
9
Przekraczanie granic Czas, by zacząć myśleć kreatywnie. Dowiedziałeś się już, że można tworzyć złożone aplikacje, łącząc niewielkie programy narzędziowe wywoływane z poziomu wiersza poleceń. Ale co zrobić, gdy będziemy chcieli korzystać z innych programów we własnym kodzie? W tym rozdziale dowiesz się, jak korzystać z usług systemowych, by tworzyć i kontrolować działanie procesów. Dzięki temu Twoje programy uzyskają dostęp do poczty elektronicznej, WWW oraz wszelkich innych narzędzi zainstalowanych na komputerze. Po przeczytaniu tego rozdziału zyskasz moc pozwalającą wykraczać poza język C.
Wywołania systemowe są Twoją gorącą linią z systemem operacyjnym
396
Wtem ktoś włamał się do systemu…
400
Bezpieczeństwo nie jest jedynym problemem
401
Funkcja exec() zapewnia większą kontrolę
402
Istnieje wiele funkcji exec()
403
Funkcje z tablicą argumentów: execv(), execvp() oraz execve()
404
Przekazywanie zmiennych środowiskowych
405
Większość wywołań systemowych zawodzi w taki sam sposób
406
Czytaj doniesienia, używając RSS
414
exec() jest końcem rodu naszego programu
418
Uruchamianie procesu potomnego przy użyciu funkcji fork() i exec()
419
Twój niezbędnik C
425
To jest Twój proces newshound.
y Uruchamia on odrębn proces dla każdego z trzech kanałów RSS.
newshound
Wszystkie procesy potomne działają jednocześnie.
xxi
Spis treści
Komunikacja pomiędzy procesami
10
Dobrze jest porozmawiać Tworzenie procesów to tylko połowa sukcesu. Co zrobić, gdy chcemy kontrolować proces po jego uruchomieniu? Albo przesłać do niego jakieś dane? Albo odczytać generowane przez niego wyniki? Komunikacja pomiędzy procesami zapewnia procesom możliwość podejmowania wspólnych działań w celu wykonania zadania. W tym rozdziale pokażemy Ci, jak możesz zwiększyć możliwości swojego kodu, pozwalając mu na komunikowanie się z innymi programami w systemie.
#include int main() { char name[30]; fgets(name, 30, stdin); printf(“Witaj, %s\n”, name); return 0; } Plik Edycja Okno Pomoc
> ./greetings > Kiedy naciśniesz kombinację klawiszy Ctrl+C, program przestaje działać. Ale dlaczego?
xxii
Przekierowania strumieni wejściowych
428
Zajrzyjmy do wnętrza standardowego procesu
429
Przekierowanie zastępuje deskryptor
430
Funkcja fileno() zwraca deskryptor
431
Czasami trzeba poczekać…
436
Bądź w kontakcie ze swymi potomkami
440
Połącz swoje procesy potokami
441
Studium przypadku: otwieranie doniesień w przeglądarce
442
W procesie potomnym
443
W procesie rodzicielskim
443
Otwieranie strony w przeglądarce
444
Śmierć procesu
449
Przechwytywanie sygnałów i wykonywanie własnego kodu
450
Struktury sigaction są rejestrowane przy użyciu funkcji sigaction()
451
Modyfikacja kodu i wykorzystanie procedury obsługi sygnałów
452
Używaj polecenia kill, by wysyłać sygnały
455
Wysyłanie do procesu sygnału pobudki
456
Twój niezbędnik C
464
Spis treści
Gniazda i komunikacja sieciowa
11
Nie ma drugiego takiego miejsca jak 127.0.0.1 Programy działające na różnych komputerach muszą się ze sobą komunikować. Dowiedziałeś się już, jak można używać operacji wejścia-wyjścia, by korzystać z plików, oraz w jaki sposób mogą się ze sobą porozumiewać programy działające na tym samym komputerze. Teraz jednak masz zamiar sięgnąć po resztę świata i nauczyć się pisać w języku C programy, które będą mogły komunikować się z innymi programami działającymi w tej samej sieci oraz na całym świecie. Po przeczytaniu tego rozdziału będziesz potrafił pisać programy działające jako serwery oraz programy pracujące jako klienty.
Internetowy serwer puk-puk
466
Prezentacja serwera puk-puk
467
PNAR — jak serwery komunikują się z internetem
468
Gniazdo nie jest typowym strumieniem danych
470
Czasami serwer nie uruchamia się prawidłowo
474
Dlaczego mama zawsze powtarzała Ci, byś sprawdzał błędy
475
Odczyt danych przesyłanych przez klienta
476
Serwer może rozmawiać tylko z jednym klientem naraz
483
Możesz użyć fork(), by obsłużyć oba klienty jednocześnie
484
Pisanie klienta WWW
488
To zadanie klienta
489
Utwórz gniazdo dla adresu IP
490
Funkcja getaddrinfo() pobiera adresy domen
491
Twój niezbędnik C
498
Serwer Klient oraz serwer prowadzą konwersację o ściśle określonej strukturze, nazywanej protokołem.
Klient telnet
Serwer będzie rozmawiał z kilkoma klientami jednocześnie.
Klient telnet Klient telnet
xxiii
Spis treści
Wątki
12
To równoległy świat Programy często muszą robić kilka rzeczy naraz. Wątki POSIX mogą sprawić, że nasz kod będzie sprawniej reagował na poczynania użytkownika, a to dzięki wydzieleniu kilku fragmentów, które będą działać jednocześnie. Ale uważaj! Wątki są potężnym narzędziem, jednak na pewno byś nie chciał, żeby sobie wzajemnie przeszkadzały. W tym rozdziale dowiesz się, jak zainstalować sygnalizację świetlną i wytyczyć linie, które zapobiegną programistycznym karambolom w Twoim kodzie. Po przeczytaniu tego rozdziału będziesz wiedział, jak tworzyć wątki POSIX oraz jak używać mechanizmów synchronizacji, by chronić integralność ważnych danych.
Zadania są sekwencyjne… lub nie…
500
…a procesy nie zawsze są właściwą odpowiedzią
501
Proste procesy robią po jednej rzeczy naraz
502
Zatrudnij dodatkowych pracowników: skorzystaj z wątków
503
Jak się tworzy wątki?
504
Utwórz wątki, używając funkcji pthread_create()
505
Ten kod nie jest wielobieżny
510
Potrzeba Ci sygnalizacji świetlnej
511
Użyj muteksu jako sygnalizacji świetlnej
512
Twój niezbędnik C
519
A Zmienna Te dwa auta reprez dwa wątki. Oba chcentują skorzystać z tej sa ą mej współdzielonej zmien nej.
B xxiv
lna uniemożliwia Sygnalizacja świet tej samej do wątkom dostęp nej w tym współdzielonej zmien . sie cza m samy
pis tr ttreści Spis
3. laboratorium C
Blasteroidy $ - %- %%- %- %( &. / #
xxv
Spis treści
Pozostałości
A
Dziesięć najważniejszych rzeczy (których nie opisaliśmy) Nawet po tym wszystkim, co już napisaliśmy, wciąż jeszcze pozostaje coś, czego nie wyjaśniliśmy. Jest jeszcze parę zagadnień, o których według nas powinieneś się dowiedzieć. Nie czulibyśmy się dobrze, gdybyśmy zupełnie je zignorowali. Wymagają one choć krótkiego wyjaśnienia, a my naprawdę nie chcemy oddawać Ci do rąk książki, której nie byłbyś w stanie podnieść bez solidnego treningu na lokalnej siłowni. A zatem zanim odłożysz tę książkę na półkę, przeczytaj zamieszczone tu informacje.
gcc
1. Operatory
538
2. Dyrektywy preprocesora
540
3. Słowo kluczowe static
541
4. Jak duże to jest?
542
5. Automatyczne testowanie
543
6. Więcej o gcc
544
7. Więcej o programie make
546
8. Narzędzia programistyczne
548
9. Tworzenie graficznego interfejsu użytkownika
549
10. Materiały
550
Zagadnienia programowania w C
B
Procesy i komunikacja
568
Funkcja exit() powoduje natychmiastowe zakończenie programu.
Dodatek B
xxvi
Procesy mogą wymieniać ze sobą dane przy użyciu potoków.
Funkcja fork() tworzy kopię bieżącego procesu.
execl() = lista argumentów. execle() = lista argumentów oraz zmienne środowiskowe. execlp() = lista argumentów oraz przeszukanie ścieżki. execv() = tablica argumentów. execve() = tablica argumentów oraz zmienne środowiskowe. execvp() = tablica argumentów oraz przeszukanie ścieżki.
Wywołania funkcji fork() oraz exec() tworzą proces potomny.
Funkcja system() wykona polecenie zapisane w łańcuchu znaków, tak jakby zostało ono wpisane w wierszu poleceń.
Procesy i komunikacja
Funkcja pipe() tworzy potok komunikacyjny.
Funkcja waitpid() pozwala poczekać na zakończenie procesu.
Powtórka z całego materiału Czy kiedykolwiek pomyślałeś, że fajnie by było, gdyby te wszystkie wspaniałe fakty dotyczące C zostały zebrane w jednym miejscu? W tym dodatku zostały zebrane wszystkie zagadnienia oraz zasady związane z pisaniem w języku C, które przedstawiliśmy w całej książce. Przejrzyj je wszystkie i przekonaj się, czy je zapamiętałeś. Przy każdym fakcie został podany numer rozdziału, z którego on pochodzi, a zatem nie będziesz miał problemów z odnalezieniem dodatkowych informacji, gdybyś ich potrzebował. Możesz nawet wyciąć te strony z książki i przykleić je sobie na ścianie.
Skorowidz
573
Wprowadzenie Nie mogę uwierzyć, że umieścili te wszystkie rzeczy w książce o programowaniu w C.
odpowiemy na palące W tej części książki orzy umieścili aut ego acz pytanie: „Dl e niezwykłe rzeczy?”. w książce te wszystki
jesteś tutaj xxvii
Jak korzystać z tej książki
Dla kogo jest przeznaczona ta książka? Jeśli możesz odpowiedzieć twierdząco na wszystkie z poniższych pytań:
1 Czy znasz już jakiś inny język programowania? 2 Czy chcesz doskonale opanować język C, napisać coś wspaniałego, zarobić fortunę, a na emeryturę przenieść się na swoją własną wyspę?
No dobrze, być może te plany są cież nieco zbyt dalekosiężne. Ale prze od czegoś trzeba zacząć, prawda?
3 Czy wolisz coś robić i wykorzystywać zdobytą wiedzę, czy też słuchać kogoś na niekończących się wykładach? to jest to książka dla Ciebie.
Kto prawdopodobnie nie powinien sięgać po tę książkę? Jeśli możesz odpowiedzieć twierdząco na przynajmniej jedno z poniższych pytań:
1 Czy szukasz szybkiego wprowadzenia lub podręcznika do pisania w języku C? 2 Czy wolisz, by 15 wrzeszczących małp wyrwało Ci paznokcie z palców u stóp, niż nauczyć się czegoś nowego? Czy uważasz, że książka o programowaniu w C powinna opisywać każde możliwe zagadnienie, a przy okazji jej lektura powinna być śmiertelnie nurząca, i to im bardziej, tym lepiej? to ta książka nie jest dla Ciebie.
[Notatka z działu marketingu: ta książka jest dla każdego, kto ma kartę kredytową… zresztą czeki też przyjmujemy].
xxviii Wprowadzenie
Wprowadzenie
Wiemy, co sobie myślisz „Jakim cudem może to być poważna książka o programowaniu w C?” „Po co te wszystkie obrazki?” „Czy w taki sposób można się czegokolwiek nauczyć?” Twój mózg myśli, że właśnie TO jest istotne.
Wiemy, co sobie myśli Twój mózg Twój mózg pragnie nowości. Zawsze szuka, przegląda i wyczekuje czegoś niezwykłego. W taki sposób został stworzony i to pomaga Ci przetrwać. Zatem co Twój mózg robi z tymi wszystkimi rutynowymi, zwyczajnymi, normalnymi informacjami, jakie do niego docierają? Otóż robi wszystko, co tylko może, aby nie przeszkadzały w jego najważniejszym zadaniu — zapamiętywaniu rzeczy, które mają prawdziwe znaczenie. Twój mózg nie traci czasu i energii na zapamiętywanie nudnych informacji; one nigdy nie przechodzą przez filtr „to jest w oczywisty sposób nieważne”. W jaki sposób Twój mózg wie, co jest istotne? Załóżmy, że jesteś na codziennej przechadzce i nagle przed Tobą staje tygrys. Co się dzieje w Twojej głowie?
Wspaniale. Pozostało jeszcze jedynie 600 głupich, nudnych i drętwych stron.
Neurony płoną. Emocje szaleją. Adrenalina napływa falami. I właśnie dlatego Twój mózg wie, że…
To musi być ważne! Nie zapominaj o tym!
ózg Twój m, że uważa nie warto TEGO iętywać. zapam
Ale wyobraź sobie, że jesteś w domu albo bibliotece. Jesteś w bezpiecznym miejscu — przytulnym i pozbawionym tygrysów. Uczysz się. Przygotowujesz się do egzaminu. Albo rozgryzasz jakiś trudny problem techniczny, którego rozwiązanie według szefa powinno zająć Ci tydzień, a najdalej dziesięć dni. Jest tylko jeden drobny problem. Twój mózg stara Ci się pomóc. Próbuje zapewnić, że te w oczywisty sposób nieistotne informacje nie zajmą cennych zasobów w Twojej głowie. Zasobów, które powinny zostać wykorzystane na zapamiętanie naprawdę ważnych rzeczy. Takich jak tygrysy. Takich jak zagrożenie, jakie niesie ze sobą pożar. Takich jak to, że nigdy nie powinieneś publikować na Facebooku tych zdjęć z imprezy. Co gorsza, nie ma żadnego sposobu, aby powiedzieć mózgowi: „Hej, mój mózgu, dziękuję ci bardzo, ale niezależnie od tego, jak nudna jest ta książka i jak nieznaczne są emocje, których aktualnie doznaję, to jednak naprawdę chciałbym zapamiętać wszystkie te informacje”.
jesteś tutaj xxix
Jak korzystać z tej książki
Wyobrażamy sobie, że Czytelnik tej książki jest uczniem neś to coś poznać, Czego zatem trzeba, żeby się czegoś nauczyć? W pierwszej kolejności powinie nie do wtłocze o a następnie postarać się tego czegoś nie zapomnieć. I nie chodzi tu jedynie cji, informa jania przyswa ie dziedzin w głowy suchych faktów. Najnowsze badania prowadzone tylko niż więcej czegoś wymaga się uczenie że ą, neurobiologii i psychologii nauczania pokazuj a. czytania tekstu. My wiemy, co potrafi pobudzić nasze mózgi do działani Oto niektóre z głównych założeń niniejszej książki:
ą, sobie inaniem że uczenie się staje się zdecydowanie bardziej efektywne (studia nad przypom ność zapamiętywania i przekazywaniem informacji wykazują, że użycie rysunków poprawia efektyw zrozumiałe. Wystarczy bardziej znacznie się stają je o 89%). Co więcej, rysunki sprawiają, że informac następnej stronie, na nie a okolicy), jego w (lub u rysunk na rednio umieścić słowa bezpoś którego te słowa , problem ć rozwiąza stanie w będą się uczące osoby że stwo, odobień a prawdop dotyczą, wzrośnie niemal dwukrotnie. końcowych studenci Stosuj dialogi i personifikuj. Według najnowszych badań w testach dni, w pierwszej bezpośre sposób w wana uzyskiwali wyniki o 40% lepsze, jeśli treść była przekazy historyjki. Używaj są lepsze w wykładó Od . formalny sposób w osobie i w konwencji rozmowy, a nie podczas stymulującej — uważny bardziej byłbyś Kiedy e. poważni zbyt się traktuj Nie języka potocznego. rozmowy przy obiedzie czy podczas wykładu? neuronów do Zmuś ucznia do głębszego zastanowienia. Innymi słowy, jeśli nie zmusisz zmotywowany, być musi Czytelnik . aktywnego wysiłku, w Twojej głowie nie zdarzy się nic wielkiego iem wniosków wyciągan ów, problem waniem rozwiązy zaangażowany, zaciekawiony i podekscytowany e wyzwań, stawiani poprzez możliwe jest ego wszystki tego ie osiągnięc i zdobywaniem nowej wiedzy. A poprzez oraz ienia zapraszanie do rozwiązywania ćwiczeń i zadawanie pytań zmuszających do zastanow . zmysłów wielu i ych mózgow nakłanianie do działań, które wymagają zaangażowania obu półkul
niż same słowa i sprawiaj Zwizualizuj to sobie. Rysunki są znacznie łatwiejsze do zapamiętania
znalazł się kiedyś Musisz zdobyć uwagę Czytelnika i zachować ją na dłużej. Każdy
pierwszej strony. Mózg w sytuacji, gdy bardzo chciał się czegoś nauczyć, lecz zasypiał po przeczytaniu nieoczekiwane. Jednak wzrok, ające przykuw dziwne, ące, zwraca uwagę na rzeczy niezwykłe, interesuj będzie to zagadnienie Jeśli nudne. być musi nie wcale nego technicz nia poznawanie nowego zagadnie interesujące, Twój mózg przyswoi je znacznie szybciej. ji są w znacznej mierze Wyzwól emocje. Teraz już wiemy, że zdolności do zapamiętywania informac Zapamiętujemy zależy. nam zależne od ich zawartości emocjonalnej. Zapamiętujemy to, na czym jących historii wzrusza myśli na tu mamy nie w sytuacjach, w których coś odczuwamy. Oczywiście podekscytowanie, radosne ść, ciekawo nie, zaskocze jak emocje takie o chłopcu i jego psie. Chodzi nam o ym rozwiązaniu „a niech to…” i uczucie satysfakcji — „Jestem wielki!” — jakie odczuwamy po poprawn że znamy sprawy, sobie zdaniu zagadki, nauczeniu się czegoś, co powszechnie uchodzi za trudne, lub i. więcej szczegółów technicznych niż Zenek z działu inżynieri
xxx
Wprowadzenie
Wprowadzenie
Metapoznanie — myślenie o myśleniu Jeśli naprawdę chcesz się czegoś nauczyć i jeśli chcesz się tego nauczyć szybciej i dokładniej, to zwracaj uwagę na to, jak Ci na tym zależy. Myśl o tym, jak myślisz. Poznawaj sposób, w jaki się uczysz. Większość z nas w okresie dorastania nie uczestniczyła w kursach metapoznania ani teorii nauczania. Oczekiwano od nas, że będziemy się uczyć, jednak nie uczono nas, jak mamy to robić.
Zastanawiam się, jak zmusić mózg do zapamiętania tych informacji…
Zakładamy jednak, że jeśli trzymasz w ręku tę książkę, to chcesz nauczyć się programowania w C. I prawdopodobnie nie chcesz na to tracić zbyt wiele czasu. Jeśli chcesz wykorzystać to, co przeczytałeś w tej książce, musisz to zapamiętać. A oprócz tego musisz to zrozumieć. Aby w jak największym stopniu wykorzystać zarówno tę, jak i dowolną inną książkę lub jakikolwiek inny sposób uczenia się, musisz wziąć odpowiedzialność za swój mózg. Myśl o tym, czego się uczysz. Sztuczka polega na tym, aby przekonać mózg, że poznawany materiał jest naprawdę ważny. Kluczowy dla Twojego dobrego samopoczucia. Tak ważny jak tygrys stojący przed Tobą. W przeciwnym razie będziesz nieustannie prowadzić wojnę z własnym mózgiem, który ze wszystkich sił będzie się starać, aby nowe informacje nie zostały utrwalone.
Jak w takim razie ZMUSIĆ mózg do traktowania programowania jak głodnego tygrysa? Można to zrobić w sposób wolny i męczący lub szybki i bardziej efektywny. Wolny sposób polega na wielokrotnym powtarzaniu. Oczywiście wiesz, że jesteś w stanie nauczyć się i zapamiętać nawet najnudniejsze zagadnienie, mozolnie je wkuwając. Po odpowiedniej ilości powtórzeń Twój mózg stwierdzi: „Wydaje się, że to nie jest dla niego szczególnie ważne, lecz w kółko to czyta i powtarza, więc przypuszczam, że jakąś wartość to jednak musi mieć”. Szybszy sposób polega na zrobieniu czegokolwiek, co zwiększy aktywność mózgu, zwłaszcza jeśli czynność ta wyzwoli kilka różnych typów aktywności. Wszystkie zagadnienia, o jakich pisaliśmy na poprzedniej stronie, są kluczowymi elementami rozwiązania i udowodniono, że wszystkie z nich potrafią pomóc w zmuszeniu mózgu do tego, aby pracował na Twoją korzyść. Na przykład badania wykazują, że umieszczenie słów na opisywanych rysunkach (a nie w innych miejscach tekstu na stronie, na przykład w nagłówku lub wewnątrz akapitu) sprawia, że mózg stara się zrozumieć relację pomiędzy słowami a rysunkiem, a to z kolei zwiększa aktywność neuronów. Większa aktywność neuronów to większa szansa, że mózg uzna informacje za warte zainteresowania i, ewentualnie, zapamiętania. Prezentowanie informacji w formie konwersacji pomaga, gdyż ludzie zwykle wykazują większe zainteresowanie w sytuacjach, gdy uważają, że biorą udział w rozmowie, gdyż oczekuje się od nich, że będą śledzić jej przebieg i brać w niej czynny udział. Zadziwiające jest to, iż mózg zdaje się nie zważać na to, że rozmowa jest prowadzona z książką! Z kolei jeśli sposób przedstawiania informacji jest formalny i suchy, mózg postrzega to tak samo jak w sytuacji, gdy uczestniczysz w wykładzie na sali pełnej sennych studentów. Nie ma potrzeby wykazywania jakiejkolwiek aktywności. Jednak rysunki i przedstawianie informacji w formie rozmowy to jedynie początek…
jesteś tutaj xxxi
Jak korzystać z tej książki
Oto, co zrobiliśmy: Używaliśmy rysunków, ponieważ Twój mózg zwraca większą uwagę na obrazy niż na tekst. Jeśli chodzi o mózg, to faktycznie jeden obraz jest wart tysiąca słów. W sytuacjach gdy pojawiał się zarówno tekst, jak i rysunek, umieszczaliśmy tekst na rysunku, gdyż mózg działa bardziej efektywnie, gdy tekst jest wewnątrz czegoś, co opisuje, niż kiedy jest umieszczony w innym miejscu i stanowi część większego fragmentu tekstu. Stosowaliśmy powtórzenia, wielokrotnie podając tę samą informację na różne sposoby i przy wykorzystaniu różnych środków przekazu oraz odwołując się do różnych zmysłów. Wszystko po to, aby zwiększyć szansę, że informacja zostanie zakodowana w większej liczbie obszarów Twojego mózgu. Korzystaliśmy z pomysłów i rysunków w nieoczekiwany sposób, ponieważ Twój mózg liczy na nowości i ich pragnie. Poza tym staraliśmy się zawrzeć w nich chociaż trochę emocji, gdyż mózg jest skonstruowany w taki sposób, że zwraca uwagę na biochemię związaną z emocjami. Prawdopodobieństwo zapamiętania czegoś jest większe, jeśli „to coś” sprawia, że cokolwiek poczujemy, nawet jeśli to uczucie nie jest niczym więcej jak lekkim rozbawieniem, zaskoczeniem lub zainteresowaniem. Używaliśmy bezpośrednich zwrotów i przekazywaliśmy treści w formie konwersacji, gdyż mózg zwraca większą uwagę, jeśli sądzi, że prowadzisz rozmowę, niż gdy jesteś jedynie biernym słuchaczem prezentacji. Mózg działa w ten sposób nawet wtedy, gdy czytasz zapis rozmowy. Zamieściliśmy w książce ponad 80 ćwiczeń, ponieważ mózg uczy się i pamięta więcej, gdy coś robi, niż gdy o czymś czyta. Poza tym podane ćwiczenia stanowią wyzwania, choć nie są przesadnie trudne, gdyż właśnie takie preferuje większość osób. Stosowaliśmy wiele stylów nauczania, gdyż Ty możesz preferować instrukcje opisujące krok po kroku sposób postępowania, ktoś inny analizowanie zagadnienia opisanego w ogólny sposób, a jeszcze inne osoby — przejrzenie przykładowego fragmentu kodu. Jednak niezależnie od ulubionego sposobu nauki każdy skorzysta na tym, że te same informacje będą przedstawiane kilkakrotnie na różne sposoby. Podawaliśmy informacje przeznaczone dla obu półkul Twojego mózgu, gdyż im bardziej mózg będzie zaangażowany, tym większe jest prawdopodobieństwo nauczenia się i zapamiętania podawanych informacji i tym dłużej możesz się koncentrować na nauce. Ponieważ angażowanie tylko jednej półkuli mózgu często oznacza, że druga będzie mogła odpocząć, będziesz mógł się uczyć bardziej produktywnie przez dłuższy czas. Dodatkowo zamieszczaliśmy opowiadania i ćwiczenia prezentujące więcej niż jeden punkt widzenia, ponieważ mózg uczy się łatwiej, gdy jest zmuszony do przetwarzania i podawania własnej opinii. Stawialiśmy przed Tobą wyzwania — zarówno poprzez włączenie ćwiczeń, jak i stawianie pytań, na które nie zawsze można odpowiedzieć w prosty sposób, a to dlatego, że mózg uczy się i pamięta, gdy musi nad czymś popracować. Pomyśl sam — nie można zdobyć dobrej kondycji, obserwując ćwiczenia w telewizji. Jednak dołożyliśmy wszelkich starań, aby zapewnić, że gdy pracujesz, to robisz dokładnie to, co trzeba, aby ani jeden dendryt nie musiał przetwarzać trudnego przykładu ani analizować tekstu zbyt lapidarnego lub napisanego niezrozumiałym żargonem. Personifikowaliśmy tekst. W historyjkach, przykładach, rysunkach i we wszelkich innych możliwych miejscach tekstu staraliśmy się personifikować tekst, gdyż jesteś osobą, a Twój mózg zwraca większą uwagę na osoby niż na rzeczy.
xxxii Wprowadzenie
Wprowadzenie
Oto, co możesz zrobić, aby zmusić swój mózg do posłuszeństwa Wytnij te porady A zatem zrobiliśmy, co było w naszej mocy. Reszta zależy od Ciebie. Możesz zacząć i przyklej na lodówce. od poniższych porad. Posłuchaj swojego mózgu i określ, które sprawdzają się w Twoim
przypadku, a które nie dają pozytywnych rezultatów.
1 Zwolnij. Im więcej rozumiesz, tym mniej musisz
6
zapamiętać. Nie ograniczaj się jedynie do czytania. Przerwij na chwilę lekturę i pomyśl. Kiedy znajdziesz w tekście pytanie, nie zaglądaj od razu na stronę z odpowiedzią. Wyobraź sobie, że ktoś faktycznie zadaje Ci pytanie. Im bardziej zmusisz swój mózg do myślenia, tym większa będzie szansa, że się czegoś nauczysz i coś zapamiętasz.
Twój mózg pracuje najlepiej, gdy dostarcza mu się dużo płynów. Odwodnienie (które może następować, nawet zanim poczujesz pragnienie) obniża zdolność percepcji.
7
8
„Nie ma głupich pytań”.
4 Niech lektura tej książki będzie ostatnią rzeczą, jaką robisz przed pójściem spać. A przynajmniej ostatnią rzeczą stanowiącą wyzwanie intelektualne. Pewne elementy procesu uczenia się (a w szczególności przenoszenie informacji do pamięci długoterminowej) następują po odłożeniu książki. Twój mózg potrzebuje trochę czasu dla siebie i musi dodatkowo przetworzyć dostarczone informacje. Jeśli w czasie niezbędnym do tego dodatkowego „przetwarzania” zmusisz go do innej działalności, część z przyswojonych informacji może zostać utracona.
5 Rozmawiaj o zdobywanych informacjach. Na głos. Mówienie aktywuje odmienne fragmenty mózgu. Jeśli próbujesz coś zrozumieć lub zwiększyć szansę na zapamiętanie informacji na dłużej, powtarzaj je na głos. Jeszcze lepiej, jeśli będziesz się starać je na głos komuś wytłumaczyć. W ten sposób nauczysz się szybciej, a oprócz tego będziesz mógł odkryć kwestie, o których nie wiedziałeś podczas czytania tekstu książki.
Poczuj coś! Twój mózg musi wiedzieć, że to, czego się uczysz, ma znaczenie. Z zaangażowaniem śledź zamieszczane w tekście opowiadania. Nadawaj własne tytuły zdjęciom. Zalewanie się łzami ze śmiechu po przeczytaniu głupiego dowcipu i tak jest lepsze od braku jakiejkolwiek reakcji.
3 Czytaj fragmenty oznaczone jako Chodzi tu o wszystkie fragmenty umieszczone z boku tekstu. Nie są to fragmenty opcjonalne — stanowią one część podstawowej zawartości książki! Nie pomijaj ich.
Posłuchaj swojego mózgu. Uważaj, kiedy Twój mózg staje się przeciążony. Jeśli spostrzeżesz, że zaczynasz czytać pobieżnie i zapominać to, o czym przeczytałeś przed chwilą, to najwyższy czas, żeby sobie zrobić przerwę. Po przekroczeniu pewnego punktu nie będziesz się uczył szybciej, „wciskając” do głowy więcej informacji, a co gorsza, może to zaszkodzić całemu procesowi nauki.
2 Wykonuj ćwiczenia. Rób notatki. Umieszczaliśmy je w tekście, jednak jeśli zrobilibyśmy je za Ciebie, to niczym nie różniłoby się to od sytuacji, w której ktoś za Ciebie wykonywałby ćwiczenia fizyczne. I nie ograniczaj się jedynie do czytania ćwiczeń. Używaj ołówka. Można znaleźć wiele dowodów na to, że fizyczna aktywność podczas nauki może poprawić jej wyniki.
Pij wodę. Dużo wody.
9
Pisz jak najwięcej kodu. Istnieje tylko jeden sposób, by nauczyć się programowania w C: pisanie kodu — im będzie go więcej, tym lepiej. I właśnie to będziesz robił podczas lektury tej książki. Pisanie programów jest umiejętnością, a jedynym sposobem jej nabycia jest ciągłe praktykowanie. Mamy zamiar dać Ci ku temu wiele okazji: w każdym rozdziale zostały umieszczone ćwiczenia stawiające przed Tobą problemy, które możesz rozwiązać. Nie pomijaj ich — podczas rozwiązywania ćwiczeń możesz się bardzo wiele nauczyć. Zamieściliśmy także rozwiązania wszystkich ćwiczeń — nie obawiaj się zerknąć na rozwiązanie, jeśli utkniesz! (Łatwo jest utknąć na jakiejś drobnostce). Staraj się jednak rozwiązać problem, zanim zajrzysz do rozwiązania. I koniecznie, zanim przejdziesz do dalszej części książki, postaraj się uruchomić programy, nad którymi pracujesz.
jesteś tutaj xxxiii
Jak korzystać z tej książki
Przeczytaj to Ta książka jest doznaniem poznawczym, a nie podręcznikiem. Celowo usunęliśmy z niej wszystko, co mogłoby Ci przeszkadzać w uczeniu się i poznawaniu materiału zamieszczonego w danym miejscu książki. Podczas pierwszej lektury tej książki powinieneś zaczynać od samego początku, gdyż jej dalsze fragmenty bazują na wiedzy, którą powinieneś zdobyć wcześniej.
Zakładamy, że nowością jest dla Ciebie język C, ale nie programowanie. Zakładamy, że miałeś już wcześniej kontakt z programowaniem. Mamy nadzieję, że może w niewielkim stopniu, ale jednak zapoznałeś się już wcześniej z innymi językami, takimi jak JavaScript, oraz takimi rzeczami jak pętle czy zmienne. C jest w rzeczywistości całkiem zaawansowanym językiem, dlatego jeśli wcześniej nie miałeś żadnego kontaktu z programowaniem, to być może, zanim zaczniesz lekturę tej książki, powinieneś sięgnąć po jakąś inną. Doskonała będzie na przykład Head First Programming.
Na swoim komputerze musisz zainstalować kompilator C. W tej książce będziemy używać kompilatora Gnu Compiler Collection (gcc), ponieważ jest on dostępny bezpłatnie, a poza tym uważamy, że jest naprawdę dobry. Musisz się zatem upewnić, że będzie on dostępny na Twoim komputerze. Jeśli masz komputer z systemem Linux, to na szczęście będziesz miał także gcc. Jeśli używasz komputera Mac, to powinieneś zainstalować narzędzia Xcode/Developer tools. Można je pobrać bądź to z App Store, bądź też z witryny firmy Apple. Jeśli natomiast używasz komputera z systemem Windows, to masz do wyboru dwie możliwości. Pierwszą z nich jest środowisko Cygwin (http://www.cygwin.com), udostępniające pełną symulację systemu operacyjnego UNIX, włącznie z kompilatorem gcc. Jeśli jednak zależy Ci jedynie na tym, by w jak najprostszy sposób tworzyć programy działające w systemie Windows, to być może będziesz wolał zainstalować środowisko Minimalist GNU for Windows (MinGW), które można pobrać ze strony http://www.mingw.org. Wszystkie kody zamieszczone w tej książce mogą działać we wszystkich systemach operacyjnych — bardzo się staraliśmy, by żaden z napisanych programów nie działał wyłącznie na jednej platformie systemowej. Od czasu do czasu pojawią się jakieś różnice, jednak na pewno zostaną one wyraźnie opisane w treści książki.
Najpierw przedstawiamy proste zagadnienia związane z pisaniem w C, a potem od razu zaczynamy wykorzystywać zdobytą wiedzę. Podstawowe zagadnienia związane z językiem C zostały opisane w rozdziale 1. Dzięki temu, kiedy dotrzesz do rozdziału 2., będziesz już tworzył programy potrafiące robić coś realnego, użytecznego oraz — a jakże! — zabawnego. Dalsza część książki bazuje na tych zdobywanych umiejętnościach posługiwania się językiem C, zmieniając Cię błyskawicznie z nieopierzonego kurczaka C w programistycznego mistrza ninja.
xxxiv Wprowadzenie
Wprowadzenie
Aktywności NIE są opcjonalne. Ćwiczenia i aktywności nie są jedynie dodatkami; są one elementem treści tej książki. Niektóre z nich mają wspomóc Twoją pamięć, inne — ułatwić zrozumienie, a jeszcze inne — pomóc w wykorzystaniu nabytej wiedzy i umiejętności. Nie pomijaj tych ćwiczeń!
Powtórzenia są celowe i ważne. Jedną z cech, która wyróżnia książki z serii Rusz głową!, jest to, że nam naprawdę zależy, żebyś wszystko zrozumiał. Chcemy także, byś kończąc lekturę tej książki, pamiętał wszystko, co w niej przeczytałeś. Większość książek informacyjnych i encyklopedycznych nie zwraca uwagi na przyswojenie i zapamiętanie informacji, jednak w tej chodzi o naukę, dlatego znajdziesz w niej wiele pojęć, które pojawiają się kilka razy.
Przykładowe kody są jak najbardziej zwięzłe. Czytelnicy niejednokrotnie zwracali nam uwagę, że przeglądanie 200 wierszy kodu w poszukiwaniu dwóch linijek, które należy zrozumieć, może być frustrujące. W większości przykładów zamieszczonych w tej książce ich kod został w jak największym stopniu skrócony, dzięki czemu fragmenty, których musisz się nauczyć, są przejrzyste i proste. Nie należy zatem oczekiwać, że podawane przykłady będą solidne ani nawet że będą kompletne — zostały one opracowane wyłącznie pod kątem nauki, a nie zapewnienia pełnej funkcjonalności.
Do ćwiczeń z cyklu „Wysil szare komórki” nie podawaliśmy odpowiedzi. Do niektórych w ogóle nie można podać jednej dobrej odpowiedzi, w innych przypadkach to doświadczenie, które zdobywasz, rozwiązując ćwiczenia, ma dać Ci możliwość określenia, czy i kiedy podana odpowiedź będzie poprawna. W niektórych ćwiczeniach z tej serii znajdziesz także podpowiedzi, które ułatwią Ci znalezienie rozwiązania.
jesteś tutaj xxxv
Zespół recenzentów
Zespół recenzentów technicznych Dave Kitabjian
Vince Milner
Recenzenci techniczni książki: Dave Kitabjian posiada dwa dyplomy z elektroniki oraz projektowania komputerów, jak również 20-letnie doświadczenie w konsultowaniu, integracji, projektowaniu oraz tworzeniu systemów informatycznych przeróżnych klientów, zaczynając od firmy z grupy Fortune 500, a kończąc na technologicznych start-upach. Prywatnie Dave lubi grać na gitarze i pianinie oraz spędzać czas z żoną i trójką dzieci. Vince Milner od ponad 20 lat pisze w C (oraz innych językach) oprogramowanie przeznaczone na wiele różnych platform. Kiedy nie studiuje, żeby przygotować się do obrony dyplomu z matematyki, można go zobaczyć przegrywającego w gry planszowe z 6-latkami i unikającego przeprowadzki.
xxxvi Wprowadzenie
Wprowadzenie
Podziękowania Dla naszego redaktora: Chcieliśmy bardzo podziękować Brianowi Sawyerowi — przede wszystkim za to, że poprosił nas o napisanie tej książki. Brian wierzył w nas w trakcie pracy nad tym projektem, zapewnił możliwość wypróbowania nowych pomysłów i nie panikował za bardzo, gdy przekraczaliśmy kolejne terminy.
Brian Sawyer
Zespół wydawnictwa O’Reilly: Wielkie podziękowania należą się następującym osobom, które pomagały nam podczas pracy nad tą książką: Karen Shaner za jej doskonałe umiejętności wyszukiwania obrazków oraz, ogólnie, za zapewnienie gładkiego postępu prac; Laurie Petrycki za zapewnienie nam dobrego wyżywienia i odpowiednio wysokiej motywacji podczas pobytu w Bostonie; Brianowi Jepsonowi za wprowadzenie nas we wspaniały świat Arduino, a także zespołowi wydania wstępnego za udostępnienie do pobrania wczesnej wersji tej książki. Wreszcie, chcieliśmy także podziękować Rachel Monaghan oraz zespołowi produkcyjnemu za doskonałe przeprowadzenie książki przez cały proces produkcji oraz za tak ciężką, choć niezauważalną pracę. Wszyscy jesteście wspaniali. Rodzina, przyjaciele i koledzy: W czasie naszej podróży do świata serii Rusz głową! poznaliśmy wielu nowych przyjaciół. W szczególności chcieliśmy podziękować Lou Barr, Brettowi McLaughlinowi oraz Sandersowi Kleinfeldowi za to, że tak wiele nas nauczyli. David: Chciałem podziękować Andy’emu Parkerowi, Joemu Broughtonowi, Carlowi Jacquesowi oraz Simonowi Jonesowi, a także wielu innym przyjaciołom, którzy podczas gdy pisałem tę książkę, mieli ze mną tak ograniczony kontakt. Dawn: Praca nad tą książką byłaby znacznie trudniejsza bez mojej niesamowitej sieci wsparcia tworzonej przez rodzinę i przyjaciół. W szczególności chciałam podziękować Mamie i Tacie, Carlowi, Steve’owi, Gill, Jacqui, Joycowi oraz Paulowi. Jestem wam bardzo wdzięczna za wsparcie i zachętę. Lista pozostałych osób: Nasz zespół recenzentów technicznych wykonał doskonałą robotę, utrzymując nas w ryzach i zapewniając, że opisywane zagadnienia są dokładne i prawidłowe. Jesteśmy także niezmiernie wdzięczni wszystkim osobom, które przekazywały nam swoje uwagi po ukazaniu się pierwszych wersji niniejszej książki. Uważamy, że dzięki wam ta książka stała się znacznie, znacznie lepsza. I w końcu chcieliśmy podziękować Kathy Sierra oraz Bertowi Batesowi za stworzenie tej niesamowitej serii książek.
jesteś tutaj xxxvii
xxxviii Wprowadzenie
1. !
Dajmy nurka Czy nie ubóstwiasz niebieskiego oCeanu? Wskakuj — woda jest cudowna!
Czy chcesz zajrzeć do głowy komputera? Musisz napisać kod działający naprawdę szybko, na przykład na potrzeby nowej gry? A może program na Arduino? Albo we własnej aplikacji na iPhone’a użyć biblioteki napisanej przez kogoś innego? Jeśli tak, to skorzystaj z pomocy bohaterskiego C. C działa na znacznie niższym poziomie niż większość innych języków programowania, a zatem zrozumienie go daje nam znacznie większe pojęcie o tym, co się naprawdę dzieje w programie. C pozwala także lepiej zrozumieć inne języki programowania. A zatem bierz się do pracy, przygotuj kompilator, a już niedługo zaczniesz poznawać C.
to jest nowy rozdział
1
Jak działa C
C to język do pisania małych, szybkich programów Język C został stworzony do pisania małych i szybkich programów. Działa na znacznie niższym poziomie niż większość innych języków programowania, a to oznacza, że tworzy kod znacznie bliższy temu, co komputery naprawdę są w stanie zrozumieć.
Sposób działania C Tak naprawdę komputery rozumieją tylko jeden język: kod maszynowy — binarny strumień składający się jedynie z zer i jedynek. Kod napisany w języku C konwertujemy na kod maszynowy za pomocą kompilatora.
#include
indows W systemie W nosił ie dz bę ik pl n te exe, nazwę rocks. s. ck ro a nie
int main() {
Plik Edycja Okno Pomoc Kompilacja
puts(“C jest czaderskie!”); return 0;
> gcc rocks.c -o rocks >
} rocks.c
rocks
1
Kod źródłowy Zaczynasz od utworzenia pliku źródłowego. Plik źródłowy zawiera kod napisany w języku C, który jest zrozumiały dla człowieka.
2
Kompilacja
Kod wynikowy
Plik źródłowy jest następnie przetwarzany przy użyciu kompilatora. Kompilator sprawdza kod w poszukiwaniu błędów, a kiedy uzna, że wszystko jest w porządku, kompiluje go.
Kompilator tworzy nowy plik, nazywany plikiem wykonywalnym. Zawiera on kod maszynowy — strumień jedynek i zer, które jest w stanie zrozumieć komputer. I to właśnie jest program, który możesz wykonać.
Język C jest używany, w przypadku gdy duże znaczenie mają szybkość działania, niewielkie rozmiary oraz możliwość przenoszenia. Większość systemów operacyjnych została napisana w języku C. Także większość innych języków programowania została napisana w C. Dodatkowo przeważająca większość gier jest pisana w C.
2
Rozdział 1.
3
Istnieją trzy standardy języka C, z którymi można się zetknąć. ANSI C pochodzi z późnych lat 80. i jest używany w najstarszym kodzie. W nowym standardzie — C99, istniejącym od 1999 roku, poprawiono sporo różnych rzeczy. Natomiast w najnowszym standardzie — C11, opracowanym w 2011 roku, dodano kilka nowych, świetnych możliwości. Różnice pomiędzy tymi trzema standardami nie są wielkie, jednak będziemy o nich wspominać.
Zaczynamy poznawać C
Zaostrz ołówek Spróbuj odgadnąć, co robi każdy z tych fragmentów kodu. fragment kodu. Opisz, co według Ciebie robi ten
int card_count = 11;
.......................................
if (card_count > 10)
.......................................
puts(“Nowe rozdanie. Licytujemy.”); int c = 10; while (c > 0) { "$ c = c - 1;
....................................... ....................................... ....................................... ....................................... ....................................... .......................................
} &'*+/22245 /"'&
.......................................
678945:
.......................................
?67+
.......................................
6 @AB $8
.......................................
@ "DDD*26"D$8
.......................................
char suit = ‘K’;
.......................................
switch(suit) {
.......................................
6 EFG
.......................................
puts(“Trefle”); break; 6 EHG puts(“Kara”); break; 6 E?G
....................................... ....................................... ....................................... ....................................... .......................................
puts(“Piki”);
.......................................
break;
.......................................
puts(“Serca”); }
.......................................
....................................... ....................................... .......................................
jesteś tutaj
3
Fragmenty bez tajemnic
Zaostrz ołówek Rozwiązanie
int card_count = 11;
Nie przejmuj się, jeśli jeszcze nie rozumiesz wszystkiego. Zostanie to szczegółowo wyjaśnione w dalszej części książki. int oznacza liczbę całkowitą.
Tworzy zmienną całkowitą i przypisuje jej wartość 11. ........................................................... Czy wartość zmiennej jest większa od 10? ...........................................................
if (card_count > 10) puts(“Nowe rozdanie. Licytujemy.”); To wyświetla łańcuch znaków w wierszu poleceń lub na terminalu. Nawiasy klamrowe definiują int c = 10; instrukcję blokową.
Jeśli jest, to wyświetlamy komunikat. ...........................................................
Tworzy zmienną całkowitą i przypisuje jej wartość 10. ...........................................................
while (c > 0) {
Jak długo wartość jest większa od zera… ...........................................................
"$
…wyświetlamy komunikat… ........................................................... …i dekrementujemy wartość zmiennej. ...........................................................
c = c - 1; }
To koniec powtarzanego bloku kodu. ...........................................................
&'*+/22245 /"'&
To jest komentarz. ...........................................................
678945:
Tworzymy tablicę 20 znaków. ...........................................................
?67+ 6 @AB $8
To oznacza: „Wszystko, co użytkownik wpisze, zapisz w tablicy ex”.
@ "DDD*26"D$8
Wyświetlamy komunikat na ekranie. ........................................................... Zapisujemy w tablicy to, co wpisze użytkownik. ........................................................... Wyświetlamy komunikat zawierający wpisany tekst. ...........................................................
To wstawi łańcuch znaków w miej sce %s Tworzymy zmienną znakową i zapisujemy w niej K. ...........................................................
char suit = ‘K’; switch(suit) { 6 EFG
Instrukcja switch sprawdza zmienną, porównując ją do różnych wartości.
Czy jest nią „T”? ...........................................................
puts(“Trefle”);
Jeśli tak, to wyświetlamy słowo „Trefle”. ...........................................................
break;
Następnie pomijamy pozostałe testy. ...........................................................
6 EHG
Czy jest nią „K”? ...........................................................
puts(“Kara”);
Jeśli tak, to wyświetlamy słowo „Kara”. ...........................................................
break;
Następnie pomijamy pozostałe testy. ...........................................................
6 E?G
Czy jest nią „P”? ...........................................................
puts(“Piki”);
Jeśli tak, to wyświetlamy słowo „Piki”. ...........................................................
break;
Następnie pomijamy pozostałe testy.
puts(“Serca”); }
4
Sprawdzamy wartość zmiennej. ...........................................................
Rozdział 1.
...........................................................
W przeciwnym razie…
...........................................................
...wyświetlamy słowo „Serca”.
...........................................................
To już koniec sprawdzania.
...........................................................
Zaczynamy poznawać C
Ale jak wygląda skompilowany program napisany w C? Aby stworzyć pełny program, musisz zapisać jego kod w pliku źródłowym. Pliki źródłowe programów pisanych w języku C można utworzyć w dowolnym edytorze tekstów, a ich nazwy zazwyczaj mają rozszerzenie .c. Przyjrzyjmy się typowemu plikowi źródłowemu programu w języku C.
1
To jest tylko konwencja, jednak ać. powinieneś się do niej zastosow
Komentarz opisuje przeznaczenie kodu umieszczonego w tym pliku, a dodatkowo może także zawierać informacje o licencji oraz prawach autorskich. Nie ma potrzeby umieszczania komentarza — ani tu, ani w żadnym innym miejscu pliku — jednak jest to dobrym zwyczajem i większość programistów używających języka C będzie takiego komentarza oczekiwać. /*
Komentarz zaczyna się od znaków /*.
'F +2M6 "
Te znaki * są opcjonalne. Poprawiają tylko wygląd komentarza.
'H M67O ?Q6R6 " ' 645AUV6HQX6 MY6 "
Komentarz kończy się znakami */.
2
*/
#include
C jest językiem o bardzo, bardzo małych możliwościach… bez zastosowania bibliotek zewnętrznych nie da się w nim zrobić praktycznie niczego. Będziesz musiał poinformować kompilator o tym, jakiego kodu zewnętrznego chcesz używać, podając pliki nagłówkowe odpowiednich bibliotek. Plikiem nagłówkowym, którego będziemy używać zdecydowanie najczęściej, jest stdio.h. Biblioteka stdio zawiera kod pozwalający odczytywać dane z terminala i wyświetlać je w nim.
int main() { int decks; ?6 Q$ 6 @$Z6 if (decks < 1) { F +6 Q$ return 1; } ^`6 67@D$ 6 'q4 return 0; }
3 ! " #$" % Cały kod pisany w języku C jest umieszczany wewnątrz funkcji. Najważniejsza funkcja, którą znajdziemy w każdym programie pisanym w tym języku, nosi nazwę main(). Stanowi ona punkt początkowy, od którego zaczyna się wykonywanie całego kodu programu.
A zatem przyjrzyjmy się funkcji main() nieco bardziej szczegółowo.
jesteś tutaj
5
Funkcja main()
"# Komputer zacznie wykonywać nasz program od funkcji main(). Jej nazwa ma znaczenie: jeśli w kodzie programu nie będzie funkcji main(), nie będzie go można wykonać. Funkcja main() zwraca wartość typu int. Co to oznacza? Cóż, kiedy komputer wykonuje program, musi dysponować jakimś sposobem określenia, czy został on wykonany prawidłowo, czy też nie. Robi to, sprawdzając wartość wynikową zwracaną przez funkcję main(). Jeśli każemy jej zwrócić wartość 0, będzie to oznaczało, że program został wykonany prawidłowo. Jeśli natomiast zwrócimy inną wartość, będzie to oznaczało, że pojawiły się jakieś problemy. To typ wyniku. W przypadku funkcji main() zawsze powinien to być typ int.
int main()
Ponieważ funkcja nosi nazwę „main”, to właśnie od niej rozpocznie się wykonywanie programu. Gdyby funkcja miała jakieś parametry, zostałyby one tu wymienione.
{ int decks;
?6 Q$ 6 @$Z6 Zawartość funkcji jest zawsze umieszczana pomiędzy nawiasami klamrowymi.
if (decks < 1) { F +6 Q$ return 1; } ^`6 67@D$ 6 'q4 return 0; }
Nazwa funkcji jest podawana za typem wyniku. Za nazwą mogą natomiast zostać podane parametry funkcji, oczywiście jeśli w ogóle takie są. Ostatnim elementem funkcji jest jej zawartość. Zawartość funkcji musi być zapisana wewnątrz pary nawiasów klamrowych.
Dla maniaków Funkcja printf() służy do wyświetlania sformatowanych danych wynikowych. Zastępuje ona znaki formatujące wartościami zmiennych. Oto przykład: wiony Pierwszy parametr zostanie wsta ów. znak uch łańc jako
Pierwszy parametr
!" ##$
#%&'* Drugi parametr zostanie wstawiony jako liczba całkowita.
Drugi parametr
Wywołując funkcję printf(), można w niej podać dowolnie wiele parametrów, musimy się jednak przy tym upewnić, że użyliśmy tyle samo znaków formatujących %.
6
Rozdział 1.
ć Jeśli chcesz sprawdzi status wykonania programu, to w systemie Windows wpisz:
!+,01 natomiast w systemach Linux lub Mac OS wpisz:
!+23
Zaczynamy poznawać C
Magnesiki z kodem ! ! "! # $ % & /* 'F +2M6 " 'H M67O ?Q6R6 " ' 645AUV6HQX6 MY6 " */ ...................................................... ...................................................... .......... main() { 676x9|: Wpisz dwuliterowy symbol MQM 6x określający nazwę karty. 6 @4 $6x int val = 0; 6x95:~~GHG val = 10; 6x95:~~GG ............................................ 6x95:~~"""""""""""" val = 10; """"""""""""" 6x95:~~"""""""""""
*
97":+; 1 5&&
*
int
............................................ } else { Ta funkcja konwertuje tekst na liczbę. val = atoi(card_name); } M@D$
4$4
............. 0;
6 !7
if
1 5&
Nasz kod działa dobrze. Dzięki połączeniu dwóch warunków przy użyciu operatora logicznego możemy sprawdzać całe zakresy, a nie konkretne wartości. Teraz dysponujemy już podstawową strukturą programu liczącego karty.
Komputer twierdzi, że to była niska karta. Liczba się powiększyła! Warto robić większe zakłady! Dużo większe!
Niewidoczne urządzeni e komunikacyjne
jesteś tutaj
21
Wywiad z gcc
Kompilator bez tajemnic W tym tygodniu tematem wywiadu jest:
Czy gcc kiedykolwiek coś dla nas zrobił?
Zacznę od podziękowań, że w swoim napiętym harmonogramie znalazłeś nieco czasu na wywiad z nami.
Stary, to naprawdę żaden problem. Cała przyjemność po mojej stronie. gcc, powiedz, czy to prawda, że potrafisz mówić w wielu językach?
Płynnie posługuję się ponad sześcioma milionami form komunikacji… Naprawdę?
Nie, żartuję. Ale faktycznie znam kilka języków. Oczywiście jest to C, lecz oprócz niego także C++ oraz Objective-C. Radzę sobie także z Pascalem, Fortranem, PL/I i kilkoma innymi. A… no i liznąłem także nieco Go. A jeśli chodzi o stronę sprzętową, to jesteś w stanie generować kod maszynowy na bardzo wiele różnych platform, czy tak?
Tak, praktycznie na każdy procesor. Właściwie, kiedy projektanci tworzą nowy rodzaj procesora, to pierwszą rzeczą, na jakiej im zależy, jest uruchomienie na nim mnie w jakiejś postaci.
Wspominałeś o dwóch stronach swojej osobowości.
Mam także stronę wewnętrzną: system służący do konwertowania kodu pośredniego na kod maszynowy, który jest rozumiany przez wiele różnych platform sprzętowych. Trzeba jeszcze do tego dodać znajomość konkretnych formatów plików wykonywalnych stosowanych w niemal wszystkich systemach operacyjnych, o których kiedykolwiek słyszałeś… A jednak, pomimo tego wszystkiego, czasami uważają cię za zwyczajny translator. Czy uważasz, że to jest sprawiedliwe? Przecież bez wątpienia to nie wszystko, czym jesteś.
No cóż, oczywiście, że robię znacznie więcej niż zwyczajne tłumaczenie. Potrafię na przykład wykrywać błędy w kodzie. Takie jak?
Z rzeczy oczywistych — potrafię wykrywać błędy w nazwach zmiennych. Jednak wykrywam także błędy o bardziej subtelnej naturze, takie jak powtórzona definicja jakiejś zmiennej. Umiem także ostrzegać programistów, by nie nadawali jakiejś zmiennej takiej samej nazwy, jaką ma już istniejąca funkcja i tak dalej.
W jaki sposób udało ci się osiągnąć tak niesamowitą elastyczność?
A zatem potrafisz także dbać o jakość kodu?
Przypuszczam, że tajemnicą mojego sukcesu są dwie strony mojej osobowości. Strona zewnętrzna rozumie pewne rodzaje kodu źródłowego.
O, tak. I nie tylko o jakość, lecz także o wydajność jego działania. Jeśli wykryję, że fragment kodu umieszczony wewnątrz pętli mógłby równie dobrze działać poza nią, mogę go błyskawicznie tam przenieść.
Napisanego w takim języku jak C?
Właśnie tak. Ta zewnętrzna strona mojej osobowości potrafi konwertować ten język na kod pośredni. Wszystkie moje osobowości rozumiejące różne języki generują ten sam rodzaj kodu.
22
Rozdział 1.
Czyli w rzeczywistości robisz całkiem sporo!
Tak właśnie uważam. Ale robię to po cichu. gcc, dziękuję ci bardzo.
Zaczynamy poznawać C
BĄDŹ kompilatorem Każdy z fragmentów kodu, które są przedstawione na tej stronie, reprezentuje kompletny plik źródłowy programu napisanego w języku C. Twoim zadaniem jest wcielić się w rolę kompilatora i określić, czy każdy z nich uda się skompilować, a jeśli nie, to dlaczego. Aby zdobyć dodatkowe punkty, powiedz, jakie według Ciebie będą wyniki wygenerowane przez skompilowany program oraz czy działają one zgodnie z przeznaczeniem. A #include int main() { int card = 1; if (card > 1) card = card - 1; if (card < 7) puts(“Niska karta”); else { puts(“As!”); } return 0; }
B #include int main() { int card = 1; if (card > 1) { card = card - 1; if (card < 7) puts(“Niska karta”); else puts(“As!”); } return 0; }
#include int main() { int card = 1; if (card > 1) { card = card - 1; if (card < 7) puts(“Niska karta”); } else puts(“As!”); return 0; }
D #include int main() { int card = 1; if (card > 1) { card = card - 1; if (card < 7) puts(“Niska karta”); else puts(“As!”); return 0; }
jesteś tutaj
23
Kod skompilowany
BĄDŹ kompilatorem. Rozwiązanie Każdy z fragmentów kodu, które są przedstawione na tej stronie, reprezentuje kompletny plik źródłowy programu napisanego w języku C. Twoim zadaniem jest wcielić się w rolę kompilatora i określić, czy każdy z nich uda się skompilować, a jeśli nie, to dlaczego. Aby zdobyć dodatkowe punkty, powiedz, jakie według Ciebie będą wyniki wygenerowane przez skompilowany program oraz czy działają one zgodnie z przeznaczeniem. A #include
Kod można skompilować. Program wyświetla komunikat „Niska karta”. Kod nie działa jednak prawidłowo, gdyż klauzula else jest połączona z nieodpowiednią instrukcją if.
int main() { int card = 1; if (card > 1) card = card - 1; if (card < 7) puts(“Niska karta”); else { puts(“As!”); } return 0; }
B #include
Kod można skompilować. Program nic nie wyświetla i nie działa prawidłowo, gdyż klauzula else jest dołączona do niewłaściwej instrukcji if.
int main() { int card = 1; if (card > 1) { card = card - 1; if (card < 7) puts(“Niska karta”); else puts(“As!”); } return 0; }
24
Rozdział 1.
#include int main() { int card = 1; if (card > 1) { card = card - 1; if (card < 7) puts(“Niska karta”); } else puts(“As!”); return 0; }
D
Kod można skompilować. Program wyświetla komunikat „As!” i jest prawidłowo napisany.
#include int main() { int card = 1; if (card > 1) { card = card - 1; if (card < 7) puts(“Niska karta”); else puts(“As!”); return 0; }
Tego programu nie uda się skompilować, gdyż brakuje jednego nawiasu klamrowego.
Zaczynamy poznawać C
Jak aktualnie wygląda nasz kod? int main() { 676x9|: MQM 6x 6 @4 $6x int val = 0; 6x95:~~GHG val = 10; 6x95:~~GG val = 10; 6x95:~~GG val = 10; 6x95:~~GG val = 11; } else { val = atoi(card_name); } &'V M6 M6 |'& 4ZZ R6 Q + $ &' M6 MA5QH'& else if (val == 10) R6 Q + $ return 0; }
Hm… A czy można coś zrobić z tą sekwencją instrukcji if? Wszystkie one sprawdzają tę samą wartość, konkretnie card_name[0], a większość z nich także robi to samo — przypisuje zmiennej val wartość 10. Zastanawiam się, czy w języku C można wyrazić to w jakiś bardziej efektywny sposób.
Często zdarza się, że programy pisane w języku C muszą kilkakrotnie sprawdzać tę samą wartość, a następnie dla każdego przypadku wykonywać bardzo podobne fragmenty kodu. Teraz możesz używać sekwencji instrukcji if i najprawdopodobniej wszystko będzie działało bardzo dobrze. Jednak język C pozwala na zapisanie takiej logiki działania także w innej postaci.
W języku C testy logiczne można także wykonywać, używając instrukcji switch.
jesteś tutaj
25
Instrukcja switch
Pociąg do Switcherado Czasami, kiedy tworzymy kod z logiką warunkową, konieczne jest wielokrotne sprawdzanie wartości tej samej zmiennej. Aby uniknąć konieczności pisania wielu instrukcji if, w języku C wprowadzono alternatywne rozwiązanie: instrukcję !+. Instrukcja switch jest nieco podobna do if, jednak w odróżnieniu od niej pozwala na testowanie wielu wartości jednej zmiennej: !+ 'L
Jeśli wartość zmiennej train == 37, to do zmiennej winnings dodaj 50 i przeskocz na koniec instrukcji.
! OJ ~ q5 " *
Jeśli wartość zmiennej train == 65, to do zmiennej winnings dodaj 80, A NASTĘPNIE dodaj do tej samej zmiennej 20 i przeskocz na koniec instrukcji.
! S[ +$ winnings = winnings + 80; ! &% ~ 45 " *
Jeśli wartość zmiennej train == 12, to do zmiennej winnings dodaj 20 i przeskocz na koniec instrukcji.
7 winnings = 0; }
Jeśli zmienna train ma jakąkolwiek inną wartość, to przypisz zmiennej winnings wartość ZERO.
Kiedy komputer dotrze do instrukcji switch, sprawdza wartość zmiennej, jaka została w niej podana, a następnie próbuje odnaleźć pasującą do tej wartości klauzulę case. Jeśli uda się ją znaleźć, wykonuje cały kod aż do napotkania pierwszej instrukcji break. Komputer będzie wykonywał kod aż do momentu, gdy każemy mu przerwać wykonywanie instrukcji !+.
'" %
Brakujące instrukcje break mogą być przyczyną błędnego działania kodu.
W większości programów w języku C każda klauzula case kończy się instrukcją break. Ułatwia to zrozumienie kodu, choć może nieco pogarszać efektywność jego działania.
26
Rozdział 1.
Zaczynamy poznawać C
Zaostrz ołówek Przyjrzyjmy się jeszcze raz poniższemu fragmentowi naszego programu cards: int val = 0; 6x95:~~GHG val = 10; 6x95:~~GG val = 10; 6x95:~~GG val = 10; 6x95:~~GG val = 11; } else { val = atoi(card_name); }
Czy sądzisz, że byłbyś w stanie przepisać ten kod, używając instrukcji switch? Odpowiedź zapisz poniżej:
jesteś tutaj
27
Kod z instrukcją switch
Zaostrz ołówek Rozwiązanie
Twoim zadaniem było przepisanie kodu z użyciem instrukcji switch.
int val = 0;
int val = 0;
switch(card_name[0]) {
6x95:~~GHG val = 10;
case ’K’:
6x95:~~GG
case ’Q’:
val = 10;
case ’J’:
6x95:~~GG
val = 10;
val = 10;
break;
6x95:~~GG
case ’A’:
val = 11;
val = 11;
} else {
break;
val = atoi(card_name);
default:
}
val = atoi(card_name); }
Nie istnieją
CELNE SPOSTRZEŻENIA Q
Instrukcją switch można zastąpić sekwencję instrukcji if.
głupie pytania
P: Dlaczego miałbym używać P: Czy w instrukcjach !+ instrukcji !+ zamiast if?
O
Q
Instrukcja switch sprawdza pojedynczą wartość.
Q
Komputer rozpocznie wykonywanie kodu od pierwszej pasującej klauzuli case.
: Na użycie instrukcji switch możesz się zdecydować, jeśli musisz wykonać wiele testów operujących na tej samej zmiennej.
Q
Kod będzie wykonywany do pierwszej napotkanej instrukcji break bądź aż do końca instrukcji switch.
P: Jakie są zalety korzystania
Upewnij się, że umieściłeś instrukcje break we wszystkich właściwych miejscach, w przeciwnym razie instrukcje switch mogą działać nieprawidłowo.
O: Jest ich kilka. Przede wszystkim:
Q
28
Rozdział 1.
z instrukcji !+?
przejrzystość. Dzięki niej od razu wiadomo, że cały blok kodu operuje na jednej zmiennej. W przypadku sekwencji instrukcji if wcale nie jest to aż tak oczywiste. Poza tym można skorzystać z przechodzenia kolejnych sekcji case, by wielokrotnie używać tego samego kodu.
mogą być używane wyłącznie zmienne? Nie można w nich używać wartości?
O: Owszem, można. Instrukcja
switch może sprawdzać, czy dwie wartości są sobie równe.
P
: Czy w instrukcjach !+ można sprawdzać łańcuchy znaków?
O: Nie, nie można używać
instrukcji switch do porównywania ani łańcuchów znaków, ani jakichkolwiek tablic. Instrukcje switch mogą operować wyłącznie na pojedynczych wartościach.
Zaczynamy poznawać C
Czasami jeden raz nie wystarcza… Nauczyłeś się już całkiem sporo o języku C, jednak wciąż pozostaje jeszcze wiele ważnych rzeczy, o których musisz się dowiedzieć. Zobaczyłeś już, jak pisać programy określające sposób działania w zależności od wielu różnych sytuacji, istnieje jednak pewne kluczowe zagadnienie, którym się jeszcze nie zajmowaliśmy. Co zrobić, kiedy chcemy, by nasz program wykonywał jakąś czynność wiele, wiele, wiele razy?
Dwie karty??? O kurczę…
Stosowanie pętli w języku C Pętle są szczególnym rodzajem instrukcji sterujących. Instrukcje sterujące określają, czy dany blok kodu zostanie wykonany, czy nie. Natomiast pętle określają, ile razy dany blok kodu zostanie wykonany. Najprostszym rodzajem pętli dostępnym w języku C jest pętla while. Wykonuje ona umieszczony wewnątrz niej blok kodu dopóty, dopóki określony w niej warunek logiczny jest spełniony.
Ten warunek jest sprawdzany przed wykonaniem zawartości pętli.
+9 G= ;'L Zawartość pętli jest umieszczana pomiędzy nawiasami klamrowymi.
"""&'FMM6"'& }
Jeśli zawartość pętli składa się z jednego wiersza kodu, nawiasy klamrowe nie są potrzebne.
Po wykonaniu całego bloku kodu umieszczonego wewnątrz pętli komputer sprawdza, czy warunek podany na jej początku wciąż jest spełniony, a jeśli jest, to zawartość pętli jest wykonywana jeszcze raz.
while (more_balls) keep_juggling();
Przynajmniej jeden do while & (!while4&, & po&& !$3 4 & ! &przynamniej jeden raz$ 7 do { &'HM'& while (have_not_won);
jesteś tutaj
29
Pętle for
Pętle często mają taką samą strukturę… Pętli while możemy używać zawsze wtedy, gdy konieczne jest wielokrotne wykonanie jakiegoś fragmentu kodu. Jednak w bardzo wielu przypadkach nasze pętle będą miały taką samą strukturę: Wykonać jakąś prostą operację przed rozpoczęciem pętli, Ì na przykład ustawić wartość licznika. Wykonać prosty test na początku pętli. Ì Wykonać jakąś operację na końcu pętli, na przykład zmienić Ì wartość licznika. Na przykład poniższa pętla while liczy od 1 do 10: To jest kod aktualizujący, który zmienia wartość licznika i jest wykonywany na samym końcu pętli.
To jest kod początkowy pętli.
int counter = 1; while (counter < 11) {
To jest warunek pętli.
@ M67Q 6`D$6 counter++; }
Pamiętaj: counter++ oznacza „pow wartość zmiennej counter o jede iększ n”.
Takie pętle składają się z kodu, który przygotowuje zmienne używane w danej pętli, jakiejś logiki warunkowej sprawdzanej przed każdym wykonaniem pętli oraz jakiegoś kodu umieszczonego na końcu pętli, który aktualizuje licznik lub wykonuje jakąś inną, podobną operację.
…a dzięki instrukcji for tworzenie takich pętli jest łatwe Ponieważ pętle tworzone według takiego wzorca występują bardzo często, projektanci języka C stworzyli instrukcję for, dzięki której kod takich pętli może być nieco krótszy i bardziej zwarty. Oto kod realizujący dokładnie to samo zadanie, ale napisany z użyciem pętli for: Ten fragment inicjalizuje zmienną używaną w pętli.
! *
To jest warunek sprawdzany przed każdym wykonaniem pętli.
! 5&*! 9&&*! RR'L
To kod, który zostanie zrealizowany po każdym wykonaniu zawartości pętli.
@ M67Q 6`D$6 }
Ponieważ nasza pętla zawiera tylko jeden wiersz kodu, moglibyśmy pominąć nawiasy klamrowe.
Pętle for są bardzo często używane w programach pisanych w języku C — równie często, a może nawet częściej niż pętle while. Nie tylko umożliwiają one nieznaczne skrócenie kodu, lecz także ułatwiają jego zrozumienie, gdyż cały kod używany do sterowania działaniem pętli — czyli związany ze sprawdzaniem i aktualizacją zmiennej counter — jest umieszczony w samej instrukcji for, a nie rozsiany w kodzie umieszczonym wewnątrz niej.
30
Rozdział 1.
Każda pętla for musi mieć jakąś zawartość.
Zaczynamy poznawać C
Instrukcji break używamy, by wydostać się z pętli… W języku C możemy tworzyć pętle sprawdzające warunek logiczny na samym jej początku lub końcu. Ale co zrobić, jeśli będziemy chcieli przerwać działanie pętli gdzieś w środku kodu umieszczonego wewnątrz pętli? Zawsze można odpowiednio zmienić strukturę kodu, jednak czasami łatwiej jest wyjść z pętli natychmiast, w dowolnym jej miejscu, używając do tego instrukcji " :
'" %
Instrukcja break jest używana do przerywania działania pętli oraz instrukcji switch.
Używając instrukcji break, upewnij się, że wiesz, z której instrukcji chcesz wyjść.
+ C=+ CF'L eat_cake(); if (feeling_queasy) { &'M67 M 7'& " * }
Instrukcja „break” powoduje natychmiastowe wyjście z pętli.
drink_coffee(); }
Użycie instrukcji break spowoduje natychmiastowe wyjście z aktualnie wykonywanej pętli i pominięcie całej reszty umieszczonego w niej kodu. Stosowanie tych instrukcji jest bardzo przydatne, gdyż czasami stanowią one najszybszy i najprostszy sposób zakończenia działania pętli. Warto jednak unikać stosowania zbyt wielu instrukcji break, gdyż mogą one utrudniać zrozumienie działania kodu.
…a instrukcji continue, by ją kontynuować Jeśli chcemy pominąć dalszą część kodu umieszczonego wewnątrz pętli i wrócić do jej początku, możemy skorzystać z pomocy instrukcji continue: + C=+ CF'L if (not_lunch_yet) { &'6M6 `'& ! * } eat_cake(); }
Instrukcja continue przenosi nas z powrotem na początek pętli.
Opowieści z krypty Break nie przerywa instrukcji if. 15 stycznia 1990 roku uległ awarii system rozmów długodystansowych firmy AT&T, przez co 60 tysięcy osób nie było w stanie korzystać z usług telefonicznych. A co było przyczyną tej awarii? Otóż programista, który pisał w języku C kod odpowiadający za wymianę połączeń, spróbował użyć instrukcji break, by zakończyć wykonywanie instrukcji if. Jednak instrukcje break nie działają w instrukcjach if. W efekcie program pomijał całą sekcję kodu, co było przyczyną błędu, który doprowadził do przerwania 70 milionów rozmów telefonicznych w ciągu dziewięciu godzin.
jesteś tutaj
31
Pisanie funkcji
( ) # Zanim wypróbujemy magiczne moce nowo poznanych pętli, udajmy się na krótki objazd, by przyjrzeć się funkcjom. Do tej pory w każdym tworzonym programie musiałeś zdefiniować jedną funkcję — main(): Ta funkcja zwraca wartość typu int.
To jest nazwa funkcji.
int main()
Wewnątrz tych nawiasów nic nie ma.
{ Zawartość funkcji jest zapisana pomiędzy nawiasami klamrowymi.
To jest zawartość funkcji — to właśnie ona robi to, co trzeba.
*QM+ QMQM2M$ return 0; Po zakończeniu funkcja zwraca wartość.
}
Niemal wszystkie funkcje w języku C wyglądają podobnie. Na przykład poniższy program zawiera napisaną przez nas niestandardową funkcję, która jest wywoływana przez funkcję main(): #include Zwracana jest wartość typu int.
Q { if (a > b) return a;
Ta funkcja przyjmuje dwa argumenty: a oraz b. Oba są wartościami typu int.
return b; } int main()
W tym miejscu wywołujemy funkcję.
{ ~ A55A555 @ D$ return 0; }
Funkcja larger() nieznacznie różni się od funkcji main(), gdyż pobiera argumenty, nazywane także parametrami. Argument jest zmienną lokalną, której wartość jest przekazywana przez kod wywołujący funkcję. Funkcja larger() pobiera dwa argumenty — a oraz b — i zwraca większą z przekazanych liczb.
32
Rozdział 1.
Uprzejmy przewodnik po standardach % ""& main() # &int, zatem na jej &5! +( &return$6 & 4&& )! &( 7(& ( $ & kompilatora C99 w razie ! &return
$ )& ## **4 # ( parametr -std=99$
Zaczynamy poznawać C
*+ #
Nie istnieją
głupie pytania
Większość funkcji pisanych w języku C zwraca jakąś wartość. Jednak od czasu do czasu zdarzają się funkcje niedysponujące żadnymi użytecznymi informacjami, które mogłyby zwrócić. Takie funkcje zazwyczaj coś robią, a nie wyliczają. Funkcje zawsze powinny zawierać instrukcję return, wyjątkiem są te spośród nich, w których typem wyniku jest 17. Typ void oznacza, że funkcja nic nie zwraca.
void complain() { 6 M }
Instrukcja return nie jest potrzebna, gdyż funkcja nic nie zwraca.
W języku C słowo kluczowe void oznacza, że coś nie ma znaczenia. Jeśli tylko poinformujemy kompilator, że nie interesuje nas zwracanie wyniku z funkcji, to nie będziemy musieli umieszczać w niej instrukcji return.
Łańcuchy przypisań Niemal wszystko w języku C zwraca jakąś wartość; nie dotyczy to wyłącznie funkcji. W rzeczywistości nawet przypisania mają swoją wartość wynikową. Przyjrzyjmy się na przykład poniższej instrukcji:
Przypisanie „x = 4” ma wartość 4.
P
: Czy zastosowanie typu 17 w definicji funkcji oznacza, że wewnątrz niej nie można używać instrukcji ?
O: W takiej funkcji wciąż można
używać instrukcji return, jednak może to spowodować wygenerowanie ostrzeżenia przez kompilator. Poza tym umieszczanie instrukcji return w takiej funkcji jest bezcelowe.
P: Naprawdę? Dlaczego? O: Gdyż próba pobrania wartości z funkcji typu void spowoduje zgłoszenie błędu przez kompilator.
A zatem także zmiennej y zostanie przypisana wartość 4.
y = (x = 4);
Powyższy wiersz kodu przypisze wartość U zarówno zmiennej x, jak i y. Okazuje się, że możemy nieco skrócić ten kod, usuwając z niego nawiasy:
x = 4; y = x = 4; Instrukcja ta przypisuje zmiennej liczbę U. Interesujące jest to, że samo wyrażenie 8~U ma wartość równą wartości przypisywanej, czyli U. A dlaczego to ma znaczenie? Ponieważ oznacza to, że możemy stosować ciekawe sztuczki, takie jak tworzenie łańcuchów przypisań:
Takie łańcuchowe przypisania można bardzo często zobaczyć w kodzie, w którym kilku zmiennym trzeba przypisać tę samą wartość.
jesteś tutaj
33
Pomieszane wiadomości Poniżej został przedstawiony kod prostego programu napisanego w C. Brakuje w nim jednego fragmentu. Twoim zadaniem jest dopasowanie proponowanych bloków kodu (widocznych poniżej z lewej strony) do wyników, które zostałyby wygenerowane przez program w razie umieszczenia w nim danego kodu. Nie wszystkie wiersze wyników zostaną użyte, a niektóre z nich mogą zostać wykorzystane więcej niż jeden raz. Narysuj linie łączące proponowane bloki kodu z generowanymi przez nie wynikami.
( ,
#include int main() { int x = 0; int y = 0; 7 8q Tutaj umieść u. proponowany blok kod
@@8M x = x + 1; } return 0; }
Dopasuj proponowane bloki kodu do generowanych przez nie wyników.
F5TDF*
%%PS
F5FRT*
&&OP[\
F5FR%* F;P'
Zamiast wyświetlić pełną długość łańcucha, powyższa funkcja poinformuje, że łańcuch ma 4 lub 8 bajtów długości. Co się stało? Dlaczego funkcja uważa, że przekazany przez nas łańcuch jest krótszy?
WYSIL
SZARE KOMÓRKI Jak sądzisz, dlaczego operator sizeof(msg) zwrócił wartość mniejszą od faktycznej długości łańcucha? Dlaczego miałby zwracać różne długości na różnych komputerach?
jesteś tutaj
53
Zmienne tablicowe
Zmienne tablicowe są jak wskaźniki… Kiedy tworzymy tablicę, zmienna tablicowa będzie używana jako wskaźnik do miejsca pamięci, w którym zaczyna się tablica. Kiedy wewnątrz funkcji język C zobaczy następujący wiersz kodu: 67¦9:~$ `66 6 M $
Zmienna quote będzie reprezentować adres pierwszego znaku łańcucha.
J
e
d
z
c
... \0
komputer zarezerwuje na stosie miejsce dla każdego znaku łańcucha oraz dodatkowe — dla znaku \0. Jednak dodatkowo skojarzy on adres pierwszego znaku ze zmienną quote. Za każdym razem, gdy zmienna ta zostanie zastosowana w kodzie, komputer zastąpi ją adresem pierwszego znaku łańcucha. W rzeczywistości zmienna tablicowa jest bardzo podobna do wskaźnika: $*¦ 67M @D$¦
Zmiennej „quote” można użyć jako zmiennej wskaźnikowej, choć w rzeczywistości jest ona tablicą.
%&/3&%; < 9
Jeśli napiszesz testowy program do wyświetlania adresu, to zobaczysz wyniki takie jak te.
> ./where_is_quote U
!+F
7 7 ./skip 77 >
Dlaczego wskaźniki mają typ? Skoro wskaźniki są tylko adresami, to dlaczego zmienne wskaźnikowe mają typy? Dlaczego nie możemy zapisywać wszystkich wskaźników w jakiejś jednej, ogólnej zmiennej wskaźnikowej? Wynika to z faktu, że arytmetyka wskaźników jest podstępna. Jeśli dodamy & do wskaźnika typu char, to wskaźnik ten wskaże sąsiednią komórkę pamięci. Ale dzieje się tak dlatego, że wartość typu char zajmuje tylko jeden bajt pamięci. A co, jeśli będziemy mieli wskaźnik typu int? Dane typu int zazwyczaj zajmują 4 bajty pamięci, a zatem jeśli dodamy 1 do wskaźnika typu int, to skompilowany kod będzie w rzeczywistości powiększał adres o 4.
int*
long*
char* short*
e Zmienne wskaźnikowe mają różn typy, zależnie od danych, na jakie wskazują.
9:~A4| printf(”nums ma adres %p\n”, nums); Pamiętaj, że te adresy są wyświetlane w postaci liczb szesnastkowych.
printf(”nums + 1 ma adres %p\n”, nums + 1);
Kiedy wykonamy ten kod, dwa wyświetlone adresy będą oddalone od siebie o więcej niż 1 bajt. A zatem typy wskaźników istnieją po to, by kompilator wiedział, o ile modyfikować wskaźniki podczas wykonywania na nich operacji arytmetycznych. Wskaźni k (nums + 1) jest oddalony od wskaźnika nums o 4 bajty.
62
Rozdział 2.
Plik Edycja Okno Pomoc
> ./print_nums
7 gcc monte.c -o monte && ./monte monte.exe has stopped working
FaulSeg!
Wpadka!
Błąd magistrali!
Co jest nie w porządku z tym kodem?
70
Rozdział 2.
Błąd s
egmen
tacji!
Pamięć i wskaźniki : 7
?
7
CO O TYM SĄDZISZ?
Czas zaprząc do pracy naszą intuicję. Nie staraj się zbytnio analizować. Po prostu zgaduj. Przeczytaj poniższe odpowiedzi i wybierz tylko jedną z nich — tę, którą uważasz za prawidłową. Jak sądzisz, na czym polega problem?
Łańcucha znaków nie można zmieniać.
Zamieniamy znaki, umieszczając je poza łańcuchem.
Łańcuch nie znajduje się w pamięci.
Przyczyną jest coś innego.
jesteś tutaj
71
Dobra intuicja : 7
?
7
CO O TYM SĄDZISZ? ROZWIĄZANIE
Miałeś skorzystać ze swojej intuicji. Twoim zadaniem było przeczytanie poniższych odpowiedzi i wybranie tylko jednej z nich — tej, którą uważasz za prawidłową. Na czym według Ciebie polegał problem?
Łańcucha znaków nie można zmieniać.
Zamieniamy znaki, umieszczając je poza łańcuchem.
Łańcuch nie znajduje się w pamięci.
Przyczyną jest coś innego.
Literały łańcuchowe nie mogą być nigdy modyfikowane Zmiennej wskazującej na literał łańcuchowy nie można użyć do zmiany zawartości łańcucha: 67'6 ~$H$
Za pomocą tej zmiennej nie można zmieniać tego łańcucha.
Gdybyśmy jednak utworzyli łańcuch jako tablicę, to w takim przypadku moglibyśmy go modyfikować: 676 9:~$H$
Wszystko sprowadza się do tego, w jaki sposób język C korzysta z pamięci…
72
Rozdział 2.
Pamięć i wskaźniki
W pamięci: char *cards=”JQK” Aby zrozumieć, dlaczego powyższy wiersz kodu powoduje błędy, musimy pogrzebać nieco w pamięci komputera i przekonać się, co dokładnie komputer będzie w niej robić. Najwyższy adres
1 2 $ $*" Kiedy komputer wczytuje program do pamięci, umieszcza wszystkie wartości stałe, takie jak literał łańcuchowy $H$, w specjalnym obszarze pamięci służącym do przechowywania stałych. Ten blok pamięci jest przeznaczony tylko do odczytu.
STOS 2
2
cards
Stos jest tym obszarem pamięci, którego komputer używa do przechowywania zmiennych lokalnych, czyli zmiennych stosowanych wewnątrz funkcji. To właśnie w tym obszarze zostanie umieszczona zmienna cards.
STERTA
3 .
$*" 2= .............) { $@6$' t t = ...........
.........
1 .........;
Wyliczanie adresów w taki spos ób jest nazywane „arytmetyką wskaźnik ów”.
} puts(””); }
Tablica tablic czy tablica wskaźników? Dowiedziałeś się już, jak utworzyć tablicę tablic, by przechować grupę łańcuchów znaków. Jednak można to także zrobić w inny sposób — tworząc tablicę wskaźników. Tablica wskaźników jest dokładnie tym, czego się można spodziewać: listą adresów zapisaną w tablicy. Jest ona bardzo przydatna, jeśli chcemy szybko i wygodnie stworzyć listę literałów łańcuchowych. 67' xx9:~$7$$Y$$µ8$
To jest tablica przechowująca wskaźniki.
W tablicy znajdą się wskaźniki, z których każdy wskazuje na jeden z tych literałów łańcuchowych.
Do tablicy wskaźników można się odwoływać dokładnie tak samo jak do tablicy tablic.
98
Rozdział 2,5
Łańcuchy znaków
178
Twój niezbędnik C Przeczytałeś już rozdział 2,5, a do swojego niezbędnika dodałeś wiadomości dotyczące wskaźników i pamięci. Pełną listę porad i wskazówek znajdziesz w dodatku B. Tablica łańcuchów znaków je s tablicą ta t blic.
kowy Plik nagłów ra ie w za string.h ne cz te uży funkcje do operacji na łańcuchach znaków.
Funkcja strstr(a, b) zwraca adres łańcucha b wewnątrz łańcucha a.
ic Tablicę tabl tworzy się, isu p używając za [...][...]. s char string
Funkcja strcmp() porównuje dwa łańcu chy znaków.
Funkcja strchr() określa położenie znaku wewnątrz łańcucha.
Funkcja czy łą strcat() dwa ą b o ze s y łańcuch w ó znak .
Funkcja strcpy() kopiuje jeden łańcuch znaków do drugiego.
Funkcja strlen() określ a długość łańcucha znaków.
jesteś tutaj
99
100
Rozdział 2,5
3.9 4/ $ - 4
Rób jedną rzecz, ale rób ją dobrze Wszystko sprowadza się do wyboru odpowiedniego narzędzia do konkretnego zadania…
Każdy system operacyjny udostępnia niewielkie programy narzędziowe. Niewielkie programy narzędziowe pisane w języku C wykonują wyspecjalizowane zadania, takie jak odczytywanie i zapisywanie plików czy też filtrowanie danych. Jeśli chcemy wykonać bardziej złożone zadanie, można nawet połączyć ze sobą kilka takich programów. W jaki jednak sposób tworzy się takie małe programy narzędziowe? W tym rozdziale przyjrzymy się elementom używanym podczas ich tworzenia. Dowiesz się, jak korzystać z opcji wiersza poleceń, jak zarządzać strumieniami informacji, czym są przekierowania, i błyskawicznie zdobędziesz nowe narzędzia.
to jest nowy rozdział
101
Małe programy narzędziowe
Małe programy narzędziowe mogą rozwiązywać wielkie problemy Mały program narzędziowy to program napisany w języku C, który wykonuje jedno zadanie i robi to bardzo dobrze. Może on wyświetlać zawartość pliku na ekranie komputera lub generować listę aktualnie działających procesów. Może też wyświetlać bądź drukować 10 pierwszych wierszy pliku. Większość systemów operacyjnych jest dostarczanych z bogatym zestawem takich programów, które można wykonywać z poziomu wiersza poleceń lub okna terminala. Czasami, kiedy mamy do rozwiązania duży problem, możemy go podzielić na sekwencję małych problemów, a następnie napisać narzędzia do rozwiązania każdego z nich.
Mały program narzędziowy realizuje jedno zadanie i robi to dobrze. ie jak Linux Systemy operacyjne takją się z setek ada skł i ośc cał w l niema narzędziowych. niewielkich programów
Znajomy napisał mi aplikację z internetową mapą. Uwielbiam publikować na niej dane z moich wycieczek rowerowych. Mam jednak problem, polega on na nieprawidłowym formacie danych zapisywanych przez GPS. h To jest format danych zapisywanyc są przez odbiornik GPS. Informacje cinkami. w nim oddzielane od siebie prze To jest szerokość geograficzna.
To jest długość geograficzna.
42.363400,-71.098465,Speed = 21 42.363327,-71.097588,Speed = 23 42.363255,-71.096710,Speed = 17
To jest format danych wymagany przez mapę. Nosi on nazwę JavaScript Object Notation, w skrócie JSON.
data=[ {latitude: 42.363400, longitude: -71.098465, info: 'Speed = 21'}, {latitude: 42.363327, longitude: -71.097588, info: 'Speed = 23'}, {latitude: 42.363255, longitude: -71.096710, info: 'Speed = 17'}, ... ]
Jeśli jakaś część programu wymaga przekonwertowania danych z jednego formatu na drugi, to jest to zadanie, które w prosty sposób można wykonać właśnie przy użyciu małego programu narzędziowego.
102
Rozdział 3.
Dane są takie same, jednak ich format jest trochę inny.
Tworzenie małych programów narzędziowych
Hej, kto z nas nie zabrał na wyjazd długiego wydruku kodu tylko po to, by przekonać się, że szybko stanie się on zupełnie… nieczytelny? Oczywiście wszystkich nas to kiedyś spotkało. Jednak po chwili zastanowienia powinieneś być w stanie poukładać kod w początkowej kolejności.
:
Ten program potrafi odczytać rozdzielone przecinkami dane, a następnie wyświetlić je w formacie JSON. Sprawdź, czy będziesz w stanie określić, jakiego kodu brakuje.
#include
int main() { float latitude; float longitude; char info[80]; int started = .........;
puts(”data=[”);
Używamy funkcji scanf(), by wczytać kilka wartości wpisywanych przez użytkownika.
Jakie to będą wartości? Pamiętaj: funkcja scanf() zawsze używa wskaźników.
Funkcja scanf() zwraca liczbę wartości, jakie udało się jej odczytać.
while (scanf(”%f,%f,%79[^\n]”, ........., ........., .........) == 3) { if (started) printf(”,\n”); else
To sposób, by powiedzieć: „Zwróć wszystkie znaki aż do końca wiersza”. Uważaj na wartość przypisywaną zmiennej „started”.
started = ............. ; printf(”{latitude: %f, longitude: %f, info: ’%s’}”, ........, ........., .........); } puts(”\n]”);
Jakie wartości należy wyświetlić?
return 0; }
jesteś tutaj 103
Hej, kto z nas nie zabrał na wyjazd długiego wydruku kodu tylko po to, by przekonać się, że szybko stanie się on zupełnie… nieczytelny? Oczywiście wszystkich nas to kiedyś spotkało. Jednak po chwili zastanowienia powinieneś być w stanie poukładać kod w początkowej kolejności. Ten program potrafi odczytać rozdzielone przecinkami dane, a następnie wyświetlić je w formacie JSON. Miałeś sprawdzić, czy będziesz w stanie określić, jakiego kodu brakuje.
:
#include
int main() { float latitude; float longitude; char info[80];
Na początku zmiennej „started” musi być przypisana wartość 0, odpowiadająca logicznemu fałszowi. Czy pamiętałeś o dodaniu operatora „&” przed zmiennymi liczbowymi? Do funkcji scanf() należy przekazywać wskaźniki.
0 int started = .........;
puts(”data=[”); &latitude &longitude info while (scanf(”%f,%f,%79[^\n]”, ........., ........., .........) == 3) { if (started) printf(”,\n”);
Będziesz wyświetlał przecinek wyłącznie wtedy, gdy wcześniejszy wiersz został już wyświetlony.
else 1 started = ............. ;
Po rozpoczęciu pętli powinieneś przypisać zmiennej „started” wartość 1, reprezentującą logiczną prawdę.
longitude info printf(”{latitude: %f, longitude: %f, info: ’%s’}”, latitude ........, ........., .........); } puts(”\n]”); return 0; }
104
Rozdział 3.
ów &, Tutaj nie potrzebujesz operator ości, wart a używ tf() prin cja funk gdyż h. nnyc zmie sów a nie adre
Tworzenie małych programów narzędziowych
Jazda próbna A zatem co się stanie, kiedy skompilujesz ten kod i go uruchomisz? Co on zrobi? To są dane, które wpisałeś.
To są wyświetlane dane.
Dane wejściowe i wyjściowe są wymieszane.
Plik Edycja Okno Pomoc JSON
>./geo2json data=[ 42.363400,-71.098465,Speed = 21 {latitude: 42.363400, longitude: , {latitude: 42.363327, longitude: , {latitude: 42.363255, longitude: , ... ... ... {latitude: 42.363182, longitude: , {latitude: 42.362385, longitude: ] >
-71.098465, info: ’Speed = 21’}42.363327,-71.097588,Speed = 23 -71.097588, info: ’Speed = 23’}42.363255,-71.096710,Speed = 17 -71.096710, info: ’Speed = 17’}42.363182,-71.095833,Speed = 22
-71.095833, info: ’Speed = 22’}42.362385,-71.086182,Speed = 21 -71.086182, info: ’Speed = 21’}^D
Po kilku kolejnych godzinach wpisywania…
Program pozwala wpisać dane GPS z klawiatury, a następnie wyświetla je w formacie JSON na ekranie. Problem z tym programem polega jednak na tym, że dane wejściowe i wyjściowe są wymieszane. Co więcej, tych wszystkich danych na ekranie jest bardzo dużo. Pisząc mały program narzędziowy, raczej nie chcemy wpisywać danych ręcznie; zwłaszcza duże ilości danych będziemy chcieli pobierać, wczytując je z pliku. Poza tym w jaki sposób będą używane dane w formacie JSON? Bez wątpienia wyświetlenie ich na ekranie nie będzie szczególnie użytecznym rozwiązaniem.
W końcu, aby zatrzymać program, musisz nacisnąć kombinację klawiszy Ctrl+D.
Naprawdę nie są nam potrzebne wyniki wyświetlane na ekranie komputera. Potrzebujemy ich w pliku, abyśmy mogli wykorzystać je w internetowej aplikacji prezentującej mapę wycieczek. O tu… zobaczcie, pokażę Wam…
Czy zatem program działa dobrze? Czy robi to, co powinien? Czy musisz coś zmienić w jego kodzie?
jesteś tutaj 105
Jak to działa
Oto sposób wykorzystania programu Wczytujemy plik z danymi z rowerowego odbiornika GPS.
1
Odbiornik GPS tworzy plik o nazwie gpsdata.csv, w którym dane każdej z zarejestrowanych lokalizacji są zapisywane w osobnym wierszu.
To jest odbiornik służący do rejes GPS trowania lokalizacji na ro werze.
Dane są zapisywane w tym pliku.
gpsdata.csv
2
Program geo2json musi ! 1! gpsdata.csv wiersz po wierszu…
Wczytujemy ten plik.
To jest nasz program narzędziowy geo2json.
geo2json
3 4
! % JSON w pliku output.json.
4 Zapisujemy ten plik.
Program narzędziowy zapisze dane w tym pliku.
106
Rozdział 3.
Strona WWW, na której jest prezentowana mapa, wczytuje 1!
Aplikacja z mapą wczytuje dane z pliku output.json, a następnie wyświetla je na mapie prezentowanej na stronie WWW.
output.json
Tworzenie małych programów narzędziowych
Ale my nie używamy plików… Problem polega na tym, że program, który napisałeś, nie odczytuje ani nie zapisuje plików — aktualnie wczytuje on dane z klawiatury i wyświetla je na ekranie komputera.
Dane są wczytywane z klawiatury.
Nasz program narzędziowy konwertuje je na nowy format.
geo2json
na ekranie, Dane są wyświetlane u. plik w ne wa isy zap nie a JSON Okno Pomoc Plik Edycja >./geo2json data=[ ed = 21 1.098465,Spe 42.363400,-7
,Speed = 23 7,-71.097588 = 21'}42.36332 info: 'Speed -71.098465, 400, longitude: {latitude: 42.363 ,Speed = 17 5,-71.096710 = 23'}42.36325 info: 'Speed -71.097588, 327, longitude: {latitude: 42.363
,
,Speed = 22 2,-71.095833 = 17'}42.36318 info: 'Speed -71.096710, 255, longitude: {latitude: 42.363
,
, ... ... ...
A takie rozwiązanie nie wystarcza. Użytkownik nie będzie chciał wpisywać wszystkich danych, jeśli są one zapisane w jakimś pliku. A jeśli dane w formacie JSON będą wyświetlane na ekranie, to mapa prezentowana na stronie WWW nie będzie miała jak z nich skorzystać. Musisz zatem napisać program, który będzie operował na plikach. Ale jak to zrobić? Jeśli chcesz używać plików zamiast klawiatury i ekranu, to jakie fragmenty kodu trzeba będzie zmienić? Czy w ogóle trzeba będzie coś zmieniać w kodzie?
WYSIL
SZARE KOMÓRKI Czy nasz program może zacząć korzystać z plików bez wprowadzania w nim żadnych zmian? A nawet bez rekompilacji kodu?
Dla maniaków Programy narzędziowe wczytujące dane wiersz po wierszu, przetwarzające je i wyświetlające są nazywane filtrami. Jeśli używasz komputera z systemem UNIX lub zainstalowałeś Cygwin na komputerze z systemem Windows, to możesz skorzystać z kilku takich filtrów: head — ten program wyświetla kilka pierwszych wierszy pliku; tail — ten filtr wyświetla wiersze umieszczone na końcu pliku; sed — to edytor strumieniowy pozwalający na wykonywanie takich operacji jak wyszukiwanie i zastępowanie łańcuchów znaków. W dalszej części rozdziału dowiesz się, jak można łączyć filtry ze sobą, tworząc tak zwane łańcuchy filtrów.
jesteś tutaj 107
Przekieruj dane
Możesz skorzystać z przekierowania W pisanym programie używasz funkcji scanf() oraz printf(), by czytać dane z klawiatury i wyświetlać je na ekranie. Jednak w rzeczywistości funkcje te nie komunikują się bezpośrednio ani z klawiaturą, ani z ekranem monitora. Zamiast tego korzystają ze standardowego strumienia wejściowego oraz standardowego strumienia wyjściowego. Standardowy strumień wejściowy oraz standardowy strumień wyjściowy są tworzone przez system operacyjny podczas uruchamiania programu.
e za Program otrzymuje dan wego rdo pośrednictwem standa . strumienia wejściowego
System operacyjny kontroluje, w jaki sposób dane trafiają do standardowego strumienia wejściowego i wydostają się ze standardowego strumienia wyjściowego. Jeśli uruchamiasz program z poziomu wiersza poleceń lub w oknie terminala, to system będzie przekazywał wszystkie informacje o naciskanych klawiszach do standardowego strumienia wejściowego. Jeśli system operacyjny odczyta jakieś dane ze standardowego strumienia wyjściowego, to domyślnie wyświetli je na ekranie. Funkcje scanf() oraz printf() ani nie wiedzą, ani nie przejmują się tym, skąd pochodzą dane oraz gdzie później trafiają. Po prostu odczytują je ze standardowego strumienia wejściowego i zapisują w standardowym strumieniu wyjściowym. Może Ci się teraz wydawać, że to wszystko jest bardzo skomplikowane. W końcu dlaczego pisany program nie może odczytywać danych prosto z klawiatury i wyświetlać ich na ekranie? Czy tak nie byłoby prościej? Cóż, istnieje jeden bardzo dobry powód, dla którego systemy operacyjne komunikują się z programami za pośrednictwem standardowego strumienia wejściowego i standardowego strumienia wyjściowego:
Można przekierować standardowy strumień wejściowy oraz standardowy strumień wyjściowy tak, by odczytywały i zapisywały dane gdzieś indziej, na przykład w plikach.
108
Rozdział 3.
Program wyświetla dane, korzystając ze standardowego strumienia wyjściowego
Tworzenie małych programów narzędziowych
Standardowy strumień wejściowy można przekierować, używając
Operator < informuje system operacyjny, że standardowy strumień wejściowy programu powinien zostać podłączony do pliku gpsdata.csv, a nie do klawiatury. A zatem możemy przesłać do programu dane z pliku. Teraz pozostaje jeszcze przekierowanie wyników.
-71.098465, -71.097588, -71.096710, -71.095833, -71.094955, -71.094078,
info: info: info: info: info: info:
’Speed ’Speed ’Speed ’Speed ’Speed ’Speed
= = = = = =
21’}, 23’}, 17’}, 22’}, 14’}, 16’},
-71.086182, info: ’Speed = 21’}
geo2json gpsdata.csv
jesteś tutaj 109
Przekierowanie wyjścia
…a standardowy strumień wyjściowy, używając > Aby przekierować standardowy strumień wyjściowy do pliku, musisz skorzystać z operatora >. Teraz przekierowujesz zarówno standardowy strumień wejściowy, jak i standardowy strumień wyjściowy. %&/3&% % & (
> ./geo2json < gpsdata.csv > output.json > Wyniki programu zostaną zapisane w pliku output.json.
Teraz na ekranie komputera nic się nie pojawi; wszystkie wyniki trafią do pliku output.json.
data=[ {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: {latitude: ]
42.363400, 42.363327, 42.363255, 42.363182, 42.363110, 42.363037, 42.362965, 42.362892, 42.362820, 42.362747, 42.362675, 42.362602, 42.362530, 42.362457, 42.362385,
longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude: longitude:
-71.098465, -71.097588, -71.096710, -71.095833, -71.094955, -71.094078, -71.093201, -71.092323, -71.091446, -71.090569, -71.089691, -71.088814, -71.087936, -71.087059, -71.086182,
info: info: info: info: info: info: info: info: info: info: info: info: info: info: info:
’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed ’Speed
= = = = = = = = = = = = = = =
21’}, 23’}, 17’}, 22’}, 14’}, 16’}, 18’}, 22’}, 17’}, 23’}, 14’}, 19’}, 16’}, 16’}, 21’}
output.json
Ponieważ przekierowujesz standardowy strumień wyjściowy, na ekranie nie pojawią się żadne wyniki generowane przez program. Jednak tym razem program utworzył plik o nazwie output.json.
geo2json
Plik output.json jest właśnie tym, czego potrzebuje aplikacja prezentująca mapę wycieczek rowerowych. Zobaczmy, jak to wszystko działa.
output.json
110
Rozdział 3.
Tworzenie małych programów narzędziowych
Jazda próbna Teraz nadszedł doskonały moment, by sprawdzić, czy nowy plik z danymi wygenerowany przez program może zostać wykorzystany do wyświetlenia lokalizacji na mapie. W tym celu zrobisz kopię strony z internetową mapą i umieścisz ją w tym samym katalogu, w którym znajduje się już plik output.json. Następnie spróbuj otworzyć tę stronę w przeglądarce.
$" %
Kod dokumentu znajduje się w przykładach dołączonych do książki, które można pobrać z serwera FTP wydawnictwa Helion pod adresem ftp://ftp.helion.pl/przyklady/ CRUSZG.zip.
gpsapp
To strona WWW prezentująca mapę.
map.html To jest plik utworzony przez nasz program.
Mapa działa.
output.json
Mapa na stronie WWW jest w stanie odczytać dane z pliku wynikowego.
Super! Teraz mogę publikować swoje wycieczki na WWW.
jesteś tutaj
111
Błędne dane
Jednak pojawił się problem z niektórymi danymi… Twój program najwyraźniej jest w stanie odczytywać dane z odbiornika GPS i zapisywać je w formacie wymaganym przez internetową mapę. Jednak po kilku dniach pojawił się problem…
Podczas jazdy odbiornik GPS spadł mi kilka razy na ziemię, a teraz mapa nie chce wyświetlać punktów.
Co się stało? Problem polega na tym, że w pliku z odbiornika GPS pojawiły się błędne dane:
{latitude: 42.363255, longitude: -71.096710, info: ’Speed = 17’}, {latitude: 423.63182, longitude: -71.095833, info: ’Speed = 22’},
W tej liczbie kropka dziesiętna jest w niewłaściwym miejscu.
Jednak program geo2json w żaden sposób nie sprawdza odczytywanych danych, po prostu zapisuje je w innym formacie i wyświetla na wyjściu.
Wydaje się, że naprawienie tego błędu nie będzie problemem. Trzeba jedynie sprawdzać poprawność danych.
112
Rozdział 3.
Tworzenie małych programów narzędziowych
Ćwiczenie
Do programu geo2json musisz dodać nowy fragment kodu, który będzie sprawdzał poprawność zarejestrowanych szerokości i długości geograficznych. Nie musi to być nic wymyślnego. Jeśli szerokość lub długość geograficzna przekroczą zadane granice wartości oczekiwanych, powinieneś wyświetlić komunikat o błędzie i zakończyć program z kodem statusu o wartości 2:
#include int main() { float latitude; float longitude; char info[80]; int started = 0; puts(”data=[”); while (scanf(”%f,%f,%79[^\n]”, &latitude, &longitude, info) == 3) { if (started) printf(”,\n”); else started = 1;
będzie mniejsza od Jeśli wartość zmiennej latitude zasygnalizować błąd, –90 lub większa od 90, to masz 2. Tak samo należy zwracając kod statusu o wartościlongitude będzie postąpić, jeśli wartość zmiennej 180. mniejsza od –180 lub większa od
................................................................ ................................................................ ................................................................ ................................................................ ................................................................ ................................................................ ................................................................ ................................................................ ................................................................ printf(”{latitude: %f, longitude: %f, info: ’%s’}”, latitude, longitude, info); } puts(”\n]”); return 0; }
jesteś tutaj
113
Szerokość i długość
Rozwiązanie ćwiczenia
Do programu geo2json miałeś dodać nowy fragment kodu, który będzie sprawdzał poprawność zarejestrowanych szerokości i długości geograficznych. Nie miało to być nic wymyślnego. Jeśli szerokość lub długość geograficzna przekroczą zadane granice wartości oczekiwanych, miałeś wyświetlić komunikat o błędzie i zakończyć program z kodem statusu o wartości 2:
#include int main() { float latitude; float longitude; char info[80]; int started = 0; puts(”data=[”); while (scanf(”%f,%f,%79[^\n]”, &latitude, &longitude, info) == 3) { if (started) printf(”,\n”); else started = 1; if ((latitude < –90.0) || (latitude > 90.0)) { Te wiersze spowodują zakończenie wykonywania funkcji main(), zwracając przy tym kod błędu 2.
printf(”Nieprawidłowa wartość szerokości geograficznej: %f\n”, latitude); return 2; } if ((longitude < –180.0) || (longitude > 180.0)) { printf(”Nieprawidłowa wartość długości geograficznej: %f\n”, longitude);
Te wiersze sprawdzają, czy wartości szerokości i długości geograficznej mieszczą się w dopuszczalnych granicach. Te wiersze oste wyświetlają prbłędach. komunikaty o
return 2; }
printf(”{latitude: %f, longitude: %f, info: ’%s’}”, latitude, longitude, info); } puts(”\n]”); return 0; }
114
Rozdział 3.
Tworzenie małych programów narzędziowych
Jazda próbna No dobrze, zatem kod sprawdzający poprawność szerokości i długości geograficznej jest na miejscu. Ale czy to wystarczy, by nasz program poradził sobie z nieprawidłowymi danymi? Przekonajmy się. Skompiluj kod, a następnie uruchom program, przekazując do niego błędne dane: To polecenie spowoduje skompilowanie programu. Następnie uruchomisz program, przekazując do niego błędne dane.
%&/3&% % & (
> gcc geo2json.c -o geo2json
Wyniki zapiszesz w pliku output.json.
> ./geo2json < gpsdata.csv > output.json >
WTF? A gdzie jest komunikat o błędzie? To znaczy „Wrażenie Totalnie Frustrujące”.
I gdzie podziały się wszystkie punkty?
Hm… Dziwne. Dodałeś przecież kod sprawdzający poprawność danych, ale wydaje się, że po uruchomieniu programu nic się nie zmieniło. Jednak teraz na mapie nie są wyświetlane żadne punkty. O co chodzi?
WYSIL
SZARE KOMÓRKI Przeanalizuj uważnie kod. Co się według Ciebie stało? Czy kod robi to, o co nam chodziło? Dlaczego nie pojawiły się żadne komunikaty o błędach? Dlaczego aplikacja z mapą uznała, że cały plik output.json jest nieprawidłowy?
jesteś tutaj
115
Analiza kodu
Analiza kodu Internetowa mapa zgłasza problemy związane z plikiem output.json, a zatem otwórz go i sprawdź jego zawartość. To jest zawartość pliku output.json.
data=[ {latitude: 42.363400, longitude: -71.098465, info: ’Speed = 21’}, {latitude: 42.363327, longitude: -71.097588, info: ’Speed = 23’}, {latitude: 42.363255, longitude: -71.096710, info: ’Speed = 17’}, + 66 U4|"|A´5q Och… także komunikat o błędzie został umieszczony w pliku wynikowym.
Kiedy otworzysz plik, wyraźnie zobaczysz, co się stało. Program odnalazł błąd w danych i natychmiast zakończył działanie. Nie przetworzył żadnych dalszych danych, a co więcej, wyświetlił komunikat o błędzie. Problem polega na tym, że ponieważ standardowy strumień wyjściowy został przekierowany do pliku output.json, w pliku tym pojawił się także komunikat o błędzie. Dlatego program nic nie wyświetlił, a my nie zobaczyliśmy żadnego komunikatu o błędzie. Oczywiście, moglibyśmy sprawdzać kod statusu po zakończeniu programu, jednak zależy nam na tym, by komunikaty o błędach były wyświetlane.
Ale w jaki sposób można wyświetlać komunikaty o błędach, jeśli standardowy strumień wyjściowy jest przekierowywany?
Dla maniaków Jeśli program napotka jakieś problemy związane z danymi, kończy się, zwracając kod statusu o wartości 2. Ale w jaki sposób możesz sprawdzić ten kod po zakończeniu programu? Cóż, to zależy od używanego systemu operacyjnego. W przypadku systemów Mac OS, Linux, niektórych systemów UNIX oraz w przypadku korzystania z Cygwin w systemie Windows ten kod statusu można wyświetlić w następujący sposób: Plik Edycja Okno Pomoc
$ echo $? 2
W przypadku uruchamiania programu w wierszu poleceń w systemie Windows wyświetlenie wartości kodu statusu wygląda nieco inaczej: Plik Edycja Okno Pomoc
C:\> echo %ERRORLEVEL% 2
Oba powyższe polecenia robią dokładnie to samo: wyświetlają liczbę zwracaną w momencie kończenia działania programu.
116
Rozdział 3.
Tworzenie małych programów narzędziowych
Czyż nie byłoby cudownie, gdyby istniał jakiś specjalny strumień wyjściowy dla błędów, tak żeby nie trzeba ich było mieszać z wynikami zapisywanymi w standardowym strumieniu wyjściowym? Wiem jednak, że to tylko fantazje…
jesteś tutaj
117
Standardowy strumień błędów
Przedstawiamy standardowy strumień błędów Standardowy strumień wyjściowy jest domyślnym sposobem wyświetlania danych przez programy. Ale co zrobić, gdy zdarzy się coś wyjątkowego, takiego jak błąd? Można przypuszczać, że z takimi rzeczami jak komunikaty o błędach będziemy chcieli postępować nieco inaczej. I to właśnie z tego powodu wymyślono standardowy strumień błędów. Jest to drugi strumień wyjściowy tworzony specjalnie w celu wyświetlania komunikatów o błędach. Ludzie mają zazwyczaj dwoje uszu i jedne usta, jednak procesy komputerowe są poskładane nieco inaczej. Każdy proces ma jedno ucho (standardowy strumień wejściowy) oraz dwoje ust (standardowy strumień wyjściowy oraz standardowy strumień błędów).
Człowiek
To jest jedno ucho.
ucho. A to jest drugie
Jedne usta. Wiele zas tosowań.
Proces
To jest standardowy strumień wejściowy. Jedyne ucho procesu.
To jest standardowy strumień wyjściowy.
Zobaczmy, w jaki sposób system operacyjny używa tych wszystkich strumieni.
118
Rozdział 3.
ucha. Nie ma drugiego
To jest standardowy str umień błędów.
Tworzenie małych programów narzędziowych
Domyślnie strumień błędów jest wyświetlany na ekranie Czy pamiętasz, jak wspominaliśmy, że w momencie uruchamiania nowego programu system operacyjny kojarzy standardowy strumień wejściowy z klawiaturą, a standardowy strumień wyjściowy z ekranem monitora? Właśnie… Dokładnie w tym samym momencie system operacyjny tworzy także standardowy strumień błędów, który — podobnie jak standardowy strumień wyjściowy — jest domyślnie kojarzony z ekranem monitora. Standardowy strumień wejściow y jest podłączony do klawiatury.
Standardowy strumień błędów jest podłączony do ekranu.
Standardowy strumień wyjściowy jest podłączony do ekranu.
Oznacza to, że jeśli standardowy strumień wejściowy oraz standardowy strumień wyjściowy zostaną przekierowane do plików, to standardowy strumień błędów wciąż będzie wyświetlał zapisywane w nim komunikaty na ekranie monitora. Standardowy strumień wejściowy pobiera dane z pliku.
Standardowy strumień wyjściowy zapisuje dane w pliku.
Standardowy strumień błędów wyświetla komunikaty na ekranie.
I to jest naprawdę świetne rozwiązanie, gdyż oznacza, że nawet gdy standardowy strumień wyjściowy zostanie gdzieś przekierowany, to domyślnie wszystkie komunikaty zapisywane w standardowym strumieniu błędów będą widoczne na ekranie. A zatem możesz rozwiązać nasz problem niewidocznego komunikatu o błędzie w bardzo prosty sposób — wystarczy zapisać go w standardowym strumieniu błędów.
Ale jak to zrobić?
jesteś tutaj
119
fprintf()
fprintf() zapisuje dane w strumieniu Widziałeś już, że funkcja printf() wyświetla dane, zapisując je w standardowym strumieniu wyjściowym. Nie wiedziałeś natomiast, że jest to wersja bardziej ogólnej funkcji o nazwie fprintf():
esz Kiedy wywołuj ę cj funk printf(), ona ści w rzeczywisto cję wywołuje funk fprintf().
To wywołanie przekaże dane do strumienia.
$RQM$ Te dwa wywołania dają identyczne wyniki.
7#0"FV#'* A to są dane, które zostaną wysłane do strumienia.
stdout to standardowy strumień wyjściowy.
Funkcja fprintf() pozwala nam określić, gdzie mają zostać wysłane dane. Możemy jej kazać wysłać dane do stdout (standardowego strumienia wyjściowego) lub stderr (standardowego strumienia błędów).
Nie istnieją
głupie pytania
P
P: A zatem czy wywołanie!
7 :::'
O: Owszem, jest. I jak się zapewne domyślasz, jest to
O: Tak. Oba wywołania dają identyczne rezultaty.
: Dostępne są strumienie stdout oraz stderr. Czy jest zatem także strumień stdin?
standardowy strumień wejściowy.
P: Czy mogę zapisywać w nim dane? O: Nie. W standardowym strumieniu wejściowym nie można niczego zapisywać.
P: A czy mogę z niego czytać? O: Tak, korzystając z funkcji fscanf(), która jest bardzo podobna do funkcji scanf(), przy czym wymaga określenia używanego strumienia.
robi dokładnie to samo co !
:::'?
W rzeczywistości, za kulisami, funkcja scanf(...) po prostu wywołuje funkcję fscanf(stdin,...).
P: Czy mogę przekierować standardowy strumień błędów?
O: Tak; operator > przekierowuje standardowy strumień wyjściowy, natomiast operator 2> przekierowuje standardowy strumień błędów.
P: A zatem mogę użyć polecenia geo2json 2> errors.txt?
O: Tak. 120
Rozdział 3.
Tworzenie małych programów narzędziowych
Zaktualizujmy kod, by korzystał z funkcji fprintf() Wprowadzając dosłownie kilka niewielkich zmian, możesz sprawić, że komunikaty o błędach będą wyświetlane w standardowym strumieniu błędów.
#include int main() { float latitude; float longitude; char info[80]; int started = 0; puts(”data=[”); while (scanf(”%f,%f,%79[^\n]”, &latitude, &longitude, info) == 3) { if (started) printf(”,\n”); else started = 1; if ((latitude < -90.0) || (latitude > 90.0)) { $+ 66 @D$ 7#` 7Y GHG!CC ! ° # 7'* return 2; } if ((longitude < -180.0) || (longitude > 180.0)) {
Zamiast funkcji printf() wywołujemy funkcję fprintf().
$++66 @D$ 7#` 7Y GH7YCG!CC ! ° # C7'* return 2; }
Jako pierwszy parametr wywołania funkcji musimy podać stderr.
printf(”{latitude: %f, longitude: %f, info: ’%s’}”, latitude, longitude, info); } puts(”\n]”); return 0; }
Oznacza to, że kod powinien działać w dokładnie taki sam sposób jak wcześniej, z tym wyjątkiem, że komunikaty o błędach powinny się pojawiać w standardowym strumieniu błędów, a nie standardowym strumieniu wyjściowym.
Uruchommy program i przekonajmy się.
jesteś tutaj
121
Jazda próbna
Jazda próbna Jeśli ponownie skompilujesz program i wykonasz go, przekazując przy tym do niego nieprawidłowe dane, to uzyskasz następujące efekty:
%&/3&%> ?#!
> gcc geo2json.c -o geo2json > ./geo2json < gpsdata.csv > output.json ` 7Y GHG!CC ! P%O:SO&].
Q
Funkcja scanf() wczytuje dane ze standardowego strumienia wejściowego.
Q
Domyślnie standardowy strumień wejściowy odczytuje dane z klawiatury.
Q
Istnieje możliwość przekierowania standardowego strumienia wejściowego w taki sposób, by odczytywał dane zapisane w pliku. Służy do tego operator < używany w wierszu poleceń.
Q
Standardowy strumień błędów jest zarezerwowany do przekazywania informacji o błędach.
Q
Standardowy strumień błędów można przekierować, używając operatora 2>.
Tworzenie małych programów narzędziowych
¥FLĂOHWDMQH Mamy powody, by sądzić, że poniższy program został użyty podczas przekazywania tajnych wiadomości: #include
i % 2 oznacza „reszta z dzielenia wartości zmiennej i przez 2”.
int main() { char word[10]; int i = 0; while (scanf(”%9s”, word) == 1) { i = i + 1; if (i % 2) fprintf(stdout, ”%s\n”, word); else fprintf(stderr, ”%s\n”, word); } return 0; }
Przechwyciliśmy plik o nazwie secret.txt oraz skrawek papieru z instrukcjami:
Uruchamiać poleceniem: secret_messages < secret.txt > message1.txt 2> message2.txt
^¹º»Hª??YºYº V*¼½¾®ª*®¼H V©¿©YªF¼RH¿ º*©¼©ÀF¼¬R¼H©¼*Y¼¬
secret.txt
Operator > przekierowuje standardowy strumień wyjściowy.
Operator 2> przekierowuje ów. standardowy strumień błęd
Twoim zadaniem jest odszyfrowanie dwóch tajnych wiadomości. Zapisz je poniżej.
!
jesteś tutaj 123
Ściśle tajne rozwiązanie
¥FLĂOHWDMQHļUR]ZLÈ]DQLH Mamy powody, by sądzić, że poniższy program został użyty podczas przekazywania tajnych wiadomości: #include int main() { char word[10]; int i = 0; while (scanf(”%9s”, word) == 1) { i = i + 1; if (i % 2) fprintf(stdout, ”%s\n”, word); else fprintf(stderr, ”%s\n”, word); } return 0; }
Przechwyciliśmy plik o nazwie secret.txt oraz skrawek papieru z instrukcjami: ^¹º»Hª??YºYº V*¼½¾®ª*®¼H V©¿©YªF¼RH¿ º*©¼©ÀF¼¬R¼H©¼*Y¼¬
Uruchamiać poleceniem: secret_messages < secret.txt > message1.txt 2> message2.txt
secret.txt
Twoim zadaniem było odszyfrowanie dwóch tajnych wiadomości. Zapisz je poniżej.
ŁÓDŹ
KUP
PODWODNA
SZEŚĆ
WYNURZY
JAJEK
SIĘ
I
O
BUTELKĘ
DZIEWIĄTEJ
MLEKA
WIECZOREM
124
!
Rozdział 3.
Tworzenie małych programów narzędziowych
System operacyjny bez tajemnic Tematem dzisiejszego wywiadu jest: Czy System Operacyjny ma znaczenie? Systemie operacyjny, jest nam naprawdę bardzo miło, że znalazłeś dziś dla nas trochę czasu.
A, rozumiem. A ty masz dużo do czynienia z programami narzędziowymi?
Podział czasu — w tym jestem mistrzem.
Czy nie taki właśnie jest mój los? To zależy od systemu operacyjnego. Systemy uniksowe wykonują zadania, korzystając z wielu programów narzędziowych. W systemie Windows są one używane rzadziej, jednak wciąż mają duże znaczenie.
Zgodziłeś się wystąpić, pod warunkiem że pozostaniesz anonimowy; czy to prawda? Wy nie pytacie, ja nie odpowiadam. Nazywajcie mnie po prostu SO. Czy to ma znaczenie, jakim SO jesteś? Wiele osób przykłada bardzo dużą wagę do tego, jakiego systemu operacyjnego używa. Patrząc jednak z punktu widzenia nas, czyli prostych programów pisanych w języku C, wszystkie zachowujemy się i działamy bardzo podobnie.
Tworzenie niewielkich programów narzędziowych, które potrafią ze sobą współpracować, jest niemal filozofią, prawda? O tak, to droga życia. Czasami, kiedy postawią przed tobą wielki problem do rozwiązania, łatwiej jest podzielić go na wiele prostszych zadań.
Czy to dzięki standardowej bibliotece języka C?
A potem napisać odpowiedni program narzędziowy do rozwiązania każdego z nich?
Ech… Jeśli piszecie w C, to podstawowe możliwości i sposoby wszędzie są takie same. Ja zawsze powtarzam: kiedy wyłączą światło, wszystkie jesteśmy takie same. Wiecie, o co mi chodzi?
Właśnie tak. A potem skorzystać z systemu operacyjnego — czyli mnie — by połączyć je ze sobą.
Och, oczywiście. Powiedz nam teraz, czy odpowiadasz za wczytywanie programów do pamięci? Zgadza się, to ja robię z nich procesy. Czy to ważne zadanie? Lubię tak o tym myśleć. W końcu wiecie… nie wystarczy wrzucić programu do pamięci i pozostawić go na pastwę procesora. Trzeba go jeszcze odpowiednio przygotować. Dlatego oprócz tego muszę jeszcze przydzielić pamięć dla programu i podłączyć program do standardowych strumieni danych, aby mogły robić różne ważne rzeczy, takie jak wyświetlanie na ekranie i wczytywanie z klawiatury. Czyli tak jak zrobiłeś w przypadku programiku geo2json? Ten gość to prawdziwy program narzędziowy. Och, przepraszam. Spoko. Chodzi mi o to, że on faktycznie jest jak program narzędziowy: prosty program operujący na danych tekstowych.
Czy takie rozwiązanie ma jakieś zalety? Jedną z jego ogromnych zalet jest prostota. Jeśli dysponujecie zbiorem niewielkich programów, możecie je łatwiej przetestować. A poza tym, kiedy już napiszecie program narzędziowy, będziecie mogli go użyć także w innych projektach. A co z wadami? No cóż… programy narzędziowe nie wyglądają tak fajnie. Zazwyczaj są używane z poziomu wiersza poleceń, zatem nie są, jakby to ująć, atrakcyjne wizualnie. Czy to ma jakieś znaczenie? Nie tak duże, jak można by przypuszczać. Jeśli będziecie dysponować grupą dobrych narzędzi do rozwiązywania jakiegoś problemu, to zawsze będziecie mogli podłączyć je czy to do jakiegoś atrakcyjnego interfejsu, jakiejś aplikacji na komputery biurkowe, czy też aplikacji internetowej. A teraz przepraszam, ale muszę was wywłaszczyć. Oczywiście, SO, było nam naprawdę bardzo przyj… zzzzzzzz...
jesteś tutaj 125
Narzędzia wielokrotnego użytku
Niewielkie programy narzędziowe są elastyczne Jedną ze wspaniałych cech niewielkich programów narzędziowych jest ich elastyczność. Jeśli napiszemy program, który robi jedną rzecz, lecz robi to naprawdę dobrze, to istnieje bardzo duże prawdopodobieństwo, że będziemy w stanie używać go w bardzo wielu kontekstach. Jeśli napiszemy program, który potrafi wyszukiwać teksty wewnątrz pliku, to najprawdopodobniej okaże się on przydatny przy wielu okazjach. Zastanówmy się na przykład nad naszym programem geo2json. Napisałeś go, by wyświetlać na mapie dane rejestrowane podczas wycieczek rowerowych, prawda? Ale czy oznacza to, że nie możemy używać go w jakichś innych celach… takich jak badania… chociażby…
To jest szerokość geograficzna 34°.
To jest szerokość geograficzna 26°.
. To jest długość geograficzna -76°
. To jest długość geograficzna -64°
Aby się przekonać, jak elastyczny jest nasz program narzędziowy, zastosujemy go do rozwiązania zupełnie innego problemu. Zamiast do zwyczajnego wyświetlania danych na internetowej mapie wykorzystamy go do rozwiązania nieco bardziej złożonego problemu. Załóżmy, że — podobnie jak wcześniej — wczytujemy cały zestaw danych z odbiornika GPS, jednak zamiast wyświetlać je wszystkie, chcemy wyświetlić tylko te dane, które zawierają się w prostokącie bermudzkim. Oznacza to, że wyświetlane mają być wyłącznie dane spełniające następujące warunki: ((latitude > 26) && (latitude < 34)) ((longitude > -76) && (longitude < -64))
Od czego masz zatem zacząć?
126
Rozdział 3.
Tworzenie małych programów narzędziowych
Nie zmieniaj programu geo2json Nasz program geo2json wyświetla wszystkie przekazane do niego dane. Co zatem powinniśmy zrobić? Czy mamy go zmodyfikować, tak by nie tylko eksportował dane, lecz także je sprawdzał? Oczywiście moglibyśmy tak zrobić, trzeba jednak pamiętać, że małe programy narzędziowe:
robią jedną rzecz i robią ją bardzo dobrze A zatem tak naprawdę nie mamy zamiaru modyfikować programu geo2json, gdyż chcemy, by realizował on tylko jedno zadanie. Jeśli program będzie robić coś bardziej złożonego, przysporzy to problemów użytkownikom oczekującym, że będzie on działał w dokładnie taki sam sposób jak wcześniej.
Ja naprawdę nie chcę filtrować danych. Chciałabym wyświetlać je wszystkie.
A zatem, skoro nie chcemy zmieniać programu geo2json, to co możemy zrobić?
0 $ / $ - 4 Niewielkie programy narzędziowe, takie jak geo2json, są tworzone zgodnie z poniższymi zasadami:
* Mogą wczytywać dane ze standardowego strumienia wejściowego. * Mogą wyświetlać dane, korzystając ze standardowego strumienia wyjściowego. * Operują raczej na danych tekstowych, a nie na nieczytelnych formatach binarnych. * Każdy z nich wykonuje jedno proste zadanie.
jesteś tutaj 127
Dwa programy narzędziowe
Różne zadania wymagają różnych narzędzi Jeśli chcemy pominąć dane, które nie leżą wewnątrz prostokąta bermudzkiego, musimy stworzyć odrębne narzędzie, które będzie właśnie do tego służyło. A zatem będziemy mieli dwa programy narzędziowe: nowy program — bermuda — będzie filtrował dane, pozostawiając tylko te z nich, które leżą wewnątrz prostokąta bermudzkiego, a następnie nasz stary program geo2json będzie konwertował je na format wymagany przez internetową mapę. Oto, w jaki sposób połączymy oba te programy: Do programu bermuda będą przekazywane wszystkie dane. Te dane zawierają zdarzenia, które odbyły się zarówno wewnątr z prostokąta bermudzkiego, jak i poza nim.
bermuda
Ten program narzędziowy będzie przekazywał wyłącznie dane leżące wewnątrz prostokąta bermudzkiego.
A zatem do programu geo2json trafią wyłącznie dane leżące w prostokącie bermudzkim.
Program geo2json będzie działał dokładnie tak samo jak wcześniej.
geo2json
Rozdzielając problem na dwa zadania, możemy uniknąć konieczności modyfikowania programu geo2json. Oznacza to, że jego aktualni użytkownicy wciąż będą mogli z niego korzystać. Pozostaje jednak pytanie:
Jak połączyć ze sobą oba programy?
128
Rozdział 3.
Wygenerujesz mapę zawierającą wyłącznie dane leżące w prostokącie bermudzkim.
Tworzenie małych programów narzędziowych
Połącz wejście i wyjście przy użyciu potoku Dowiedziałeś się już, jak można skorzystać z przekierowań, by połączyć standardowy strumień wejściowy oraz standardowy strumień wyjściowy z plikiem. Jednak teraz chodzi nam o połączenie standardowego strumienia wyjściowego programu bermuda ze standardowym strumieniem wejściowym programu geo2json, mniej więcej w następujący sposób:
Symbol | jest potokiem łączącym standardowy strumień wyjściowy jednego procesu ze standardowym strumieniem wejściowym innego procesu.
bermuda
Dane pojawiające się na wyjściu programu bermuda… Potok może posłużyć do połączenia standardowego strumienia wyjściowego jednego procesu ze standardowym strumieniem wejściowym innego procesu.
To jest potok.
W ten sposób zawsze gdy program bermuda odnajdzie dane leżące w obszarze prostokąta bermudzkiego, zapisze je w standardowym strumieniu wyjściowym. Potok przekaże te dane ze standardowego strumienia wyjściowego programu bermuda do standardowego strumienia wejściowego programu geo2json.
geo2json. jście programu …trafiają na we
geo2json
Nad samym sposobem działania potoku czuwa system operacyjny. Wszystko, co musimy zrobić, by całość zadziałała, to wydanie odpowiedniego polecenia, takiego jak to przedstawione poniżej: To jest potok. System operacyjny wykona oba programy w tym samym momencie.
bermuda | geo2json Dane generowane na wyjściu prog ramu bermuda stają się danymi wejściowymi programu geo2 json.
A zatem nadszedł czas, byś napisał program narzędziowy bermuda.
jesteś tutaj 129
Notatki narzędziowe
Program narzędziowy bermuda Program narzędziowy bermuda będzie działał w bardzo podobny sposób jak program geo2json: będzie odczytywał wiersz po wierszu przekazywane dane GPS i przesyłał je do standardowego strumienia wyjściowego. Pojawią się jednak dwie ważne różnice. Przede wszystkim nie wszystkie dane będą przekazywane do standardowego strumienia wyjściowego — trafią tam jedynie dane leżące wewnątrz prostokąta bermudzkiego. Druga różnica polega na tym, że program bermuda zawsze będzie wyświetlał dane w tym samym formacie, którego używa odbiornik GPS, czyli w formacie CSV. Oto, jak wygląda pseudokod tego nowego programu:
Wczytaj
szerokoś ć i długo ść geogr oraz wsz aficzną elkie inn e dane z wiersza pliku: jeśli sze rokość z awiera s od 26 do ię w prz 34, to: edziale jeśli dłu gość geo graficzna w przedz mieści s iale od – ię 64 do –7 6, to: wyświet l szeroko ść i dług geografic ość zną oraz inne dan e
Przełóżmy ten pseudokod na C.
130
Rozdział 3.
Tworzenie małych programów narzędziowych
/ " Twoim zadaniem jest uzupełnienie kodu programu bermuda. Wyciągnij fragmenty kodu z basenu i umieść je w pustych miejscach kodu poniżej. Nie będziesz musiał użyć wszystkich fragmentów kodu z basenu.
#include
int main() { float latitude; float longitude; char info[80]; while (scanf(”%f,%f,%79[^\n]”, ........., ........., .........) == 3) if ((......... > .........) ......... (......... < .........)) if ((......... > .........) ......... (......... < .........)) printf(”%f,%f,%s\n”, ........., ........., .........); return 0; }
Uwaga: każdy fragment kodu z basenu może zostać użyty tylko raz!
&longitude
&&
-76
info info latitude latitude
&latitude
longitude 26 -64
&info
longitude || yeti
longitude
34
&&
latitude
||
jesteś tutaj
131
Kod z basenu
/ " Twoim zadaniem było uzupełnienie kodu programu bermuda. Miałeś wyciągnąć fragmenty kodu z basenu i umieścić je w pustych miejscach kodu poniżej.
#include
int main() { float latitude; float longitude; char info[80]; &longitude .........) &latitude ..........., info while (scanf(”%f,%f,%79[^\n]”, ........., == 3) latitude > .........) 26 latitude < .........)) && 34 if ((......... ......... (......... -64 && longitude > .........) -76 longitude < .........)) if ((......... ......... (......... latitude longitude .........); info printf(”%f,%f,%s\n”, ........., ........., return 0; }
Uwaga: każdy fragment kodu z basenu może zostać użyty tylko raz!
&info || yeti ||
132
Rozdział 3.
Tworzenie małych programów narzędziowych
Jazda próbna Skoro już napisałeś program bermuda, nadszedł czas, by wypróbować go wraz z programem geo2json i sprawdzić, czy pojawiły się jakieś doniesienia o tajemniczych zdarzeniach występujących w obszarze prostokąta bermudzkiego. Po skompilowaniu obu programów wywołaj okno konsoli, a następnie uruchom oba programy, używając poniższego polecenia: Pamiętaj: jeśli korzystasz z systemu Windows, to nie musisz używać sekwencji znaków „./”.
To jest potok łączący oba procesy.
$" % Plik spooky.csv znajduje się w przykładach dołączonych do książki, które można pobrać z serwera FTP wydawnictwa Helion pod adresem ftp://ftp.helion.pl/przyklady/CRUSZG.zip. To jest plik zawierający wszystkie zdarzenia.
Kiedy łączymy ze sobą "&QÁ"&4 M"6 " dwa programy, możemy je potraktować jako jeden program. Program bermuda filtruje zdarzeni Program geo2json przekonwertuje a, które chcemy zignorować. dane o zdarzeniach na format JSON.
Wygenerowane wyniki zapiszemy w tym pliku.
Łącząc ze sobą dwa odrębne programy przy użyciu potoku, można je potraktować, jak gdyby były jednym programem, i przekierowywać ich standardowy strumień wejściowy oraz standardowy strumień wyjściowy tak samo jak wcześniej. %&/3&%;,>
"&QÁ"&4 M"6 "
jesteś tutaj 133
Nie ma głupich pytań Nie istnieją
głupie pytania
P: Dlaczego ważne jest, by
P: Jeśli zatem dwa programy są
O: Gdyż dzięki temu łatwiej jest łączyć
: Nie. Oba programy będą działały w tym samym czasie — kiedy pierwszy będzie generował dane, drugi może je odczytywać i ich używać.
niewielkie programy narzędziowe korzystały ze standardowego strumienia wejściowego oraz standardowego strumienia wyjściowego?
programy ze sobą przy użyciu potoków.
P: Dlaczego to ma znaczenie? O: Niewielkie programy narzędziowe
zazwyczaj nie potrafią samodzielnie rozwiązać całego problemu, a jedynie niewielkie problemy techniczne, takie jak konwersja danych z jednego formatu na drugi. Jednak łącząc je ze sobą, można rozwiązywać za ich pomocą duże problemy.
P: Czym tak naprawdę jest potok? O: Szczegóły zależą od systemu operacyjnego. Potoki mogą być fragmentami pamięci bądź plikami tymczasowymi. Najważniejsze jednak jest to, że z jednej strony przyjmują dane, a z drugiej strony, w odpowiedniej kolejności, wysyłają te same dane.
połączone potokiem, to czy pierwszy z nich musi zakończyć działanie, zanim drugi będzie mógł zostać uruchomiony?
O
P: Dlaczego małe programy
narzędziowe korzystają z danych tekstowych?
O: Gdyż jest to najbardziej otwarty format.
Jeśli mały program narzędziowy będzie korzystał z danych tekstowych, oznacza to, że dowolny inny programista może odczytać i przeanalizować generowane przez niego wyniki — wystarczy w tym celu otworzyć plik w dowolnym edytorze tekstów. Formaty binarne są zazwyczaj bardziej zagmatwane i trudne do zrozumienia.
P: Jeśli kilka procesów będzie
ze sobą połączonych przy użyciu potoków, a następnie użyję operatorów > oraz przechwyci zawartość standardowego strumienia wyjściowego ostatniego procesu tworzącego potok.
P: Czy podczas uruchamiania
programów bermuda i geo2json te nawiasy są naprawdę konieczne?
O: Tak. To właśnie dzięki nim zawartość pliku trafi do standardowego strumienia wejściowego programu bermuda.
P: Czy przy użyciu potoków można połączyć ze sobą kilka programów?
O: Tak, wystarczy pomiędzy każdą
ich parą umieścić znaki |. Seria takich połączonych ze sobą programów nosi nazwę potoku.
CELNE SPOSTRZEŻENIA Q
Q
134
Rozdział 3.
Jeśli chcesz wykonywać inne zadania, to rozważ napisanie innego małego programu narzędziowego. Projektuj programy narzędziowe korzystające ze standardowego strumienia wejściowego oraz standardowego strumienia wyjściowego.
Q
Małe programy narzędziowe zazwyczaj odczytują i zapisują dane tekstowe.
Q
Przy użyciu potoku można połączyć standardowy strumień wyjściowy jednego procesu ze standardowym strumieniem wejściowym innego procesu.
Tworzenie małych programów narzędziowych
A jeśli chcemy przekazywać wyniki do więcej niż jednego pliku? Dowiedzieliśmy się już, jak można odczytywać dane z jednego pliku i zapisywać je w innym, korzystając przy tym z przekierowań. Jak jednak postąpić, gdy program ma zrobić coś bardziej złożonego, na przykład wysyłać dane do więcej niż jednego pliku? Wyobraźmy sobie, że musimy stworzyć kolejny program narzędziowy, który będzie odczytywał dane z pliku, a następnie rozdzielał je i zapisywał w kilku innych plikach.
ufos.csv
4% 4%
categorize spooky.csv
Na czym zatem polega nasz problem? Nie możemy zapisywać w plikach, prawda? Problem polega na tym, że korzystając z przekierowań, możemy zapisywać dane co najwyżej w dwóch plikach — w jednym, korzystając ze standardowego strumienia wyjściowego, oraz w drugim, korzystając ze standardowego strumienia błędów. Co zatem możemy zrobić?
disappearances.csv
other.csv
jesteś tutaj 135
Strumienie danych wedle potrzeb
Stwórz swoje własne strumienie danych Kiedy program jest uruchamiany, system operacyjny tworzy dla niego trzy plikowe strumienie danych: standardowy strumień wejściowy, standardowy strumień wyjściowy oraz standardowy strumień błędów. Jednak czasami pojawia się konieczność tworzenia innych strumieni danych w trakcie działania programu. Na szczęście system operacyjny nie ogranicza nas wyłącznie do tych strumieni, które otrzymaliśmy podczas uruchamiania programu. Możemy tworzyć własne już w czasie działania programu. Każdy strumień danych jest reprezentowany przez wskaźnik do pliku, a można je tworzyć przy użyciu funkcji ': To wywołanie utworzy strumień danych pozwalający czytać z pliku. To wywołanie utworzy strumień danych pozwalający zapisywać w pliku.
To jest nazwa pliku.
To jest tryb „r” oznaczający „odczytywanie” (ang. read).
³0, =5 # :T###'* To jest nazwa pliku.
³0,=5 #:T###'*
Funkcja fopen() pobiera dwa parametry: nazwę pliku oraz tryb. Trybem może być w, jeśli chcemy zapisywać w pliku, r, jeśli chcemy z niego odczytywać, oraz a, jeśli mamy zamiar dodawać dane na końcu pliku. Po utworzeniu strumienia danych można zapisywać w nim dane przy użyciu funkcji ', zupełnie tak samo jak wcześniej. A co w przypadku, gdy trzeba odczytać jakieś dane ze strumienia? Wówczas można skorzystać z funkcji !
':
To jest tryb „w” oznaczający „zapisywanie” (ang. write).
Dostępne tryby to: „w” = zapisywanie, „r” = odczytywanie, „a” = dopisywanie.
=#` :##! C## F #'* !
=#J\?° @° # !'* Kiedy już skończymy używać strumienia danych, należy go zamknąć. Co prawda wszystkie strumienie danych są automatycznie zamykane podczas kończenia działania programu, niemniej samodzielne zamykanie strumieni zawsze jest dobrym pomysłem:
! ='* !='* A zatem spróbujmy użyć strumieni.
136
Rozdział 3.
Tworzenie małych programów narzędziowych
Zaostrz ołówek To jest kod programu, którego zadaniem jest odczytanie wszystkich danych z pliku wygenerowanego przez odbiornik GPS i zapisanie ich do jednego z trzech innych plików. Sprawdź, czy będziesz w stanie uzupełnić brakujące miejsca. #include #include #include int main() { char line[80]; µ©R¼'~ $ M"6 $"""""""""" µ©R¼'A~ $ "6 $"""""""""" µ©R¼'4~ $ 6 "6 $"""""""""" µ©R¼'|~ $7"6 $"""""""""" while (..........(in, ”%79[^\n]\n”, line) == 1) { $ªµY$ ..........(file1, ”%s\n”, line); $*6$ ..........(file2, ”%s\n”, line); else ..........(file3, ”%s\n”, line); } ..........(file1); ..........(file2); ..........(file3); return 0; }
Nie istnieją
głupie pytania
P: Ilu strumieni danych mogę używać? O: To zależy od systemu operacyjnego, jednak
zazwyczaj można używać do 256 strumieni. Kluczowe znaczenie ma tu ograniczona liczba dostępnych strumieni, zatem warto dbać o to, by zawsze zamykać strumienie, kiedy nie są już potrzebne.
P
: Dlaczego typ FILE jest zapisywany wielkimi literami?
O: Ze względów historycznych. Kiedyś był on
definiowany jako makro, a nazwy makr są zazwyczaj zapisywane wielkimi literami. O makrach dowiesz się więcej w dalszej części książki.
jesteś tutaj 137
Zapis i odczyt
Zaostrz ołówek Rozwiązanie
To jest kod programu, którego zadaniem jest odczytanie wszystkich danych z pliku wygenerowanego przez odbiornik GPS i zapisanie ich do jednego z trzech innych plików. Miałeś sprawdzić, czy będziesz w stanie uzupełnić brakujące miejsca.
#include #include #include int main() { char line[80]; "r" µ©R¼'~ $ M"6 $"""""""""" "w" µ©R¼'A~ $ "6 $"""""""""" "w" µ©R¼'4~ $ 6 "6 $"""""""""" "w" µ©R¼'|~ $7"6 $"""""""""" fscanf while (..........(in, ”%79[^\n]\n”, line) == 1) { $ªµY$ fprintf ..........(file1, ”%s\n”, line); $*6$ fprintf ..........(file2, ”%s\n”, line); else fprintf ..........(file3, ”%s\n”, line); } fclose ..........(file1); fclose ..........(file2); fclose ..........(file3); return 0; }
Program działa, ale…
ufos.csv
Jeśli skompilujesz i uruchomisz ten program, używając następującego polecenia:
C!!! C:!¶! CWW:E! C disappearances.csv
to program wczyta wiersz po wierszu dane zapisane w pliku spooky.csv i rozdzieli je, zapisując w trzech innych plikach: ufos.csv, disappearances.csv oraz other.csv. To super; co jednak zrobić, w przypadku gdy użytkownik będzie chciał rozdzielić dane w inny sposób? Jeśli na przykład będzie chciał wyszukiwać inne słowa lub zapisywać dane w plikach o innych nazwach? Czy będzie w stanie to zrobić bez ponownej kompilacji programu?
138
Rozdział 3.
other.csv
Tworzenie małych programów narzędziowych
Nieco więcej o funkcji main() Chodzi o to, że każdy program, który będziesz pisał, powinien dawać użytkownikowi możliwość modyfikowania sposobu jego działania. Jeśli jest to program dysponujący graficznym interfejsem użytkownika, to zapewne powinieneś go wyposażyć w okno ustawień. Jeśli natomiast jest to program wykonywany z poziomu wiersza poleceń, taki jak nasz program 6 , to powinien on zapewniać możliwość przekazywania argumentów wiersza poleceń. To jest pierwsze słowo, którego chcemy poszukiwać.
Wszystkie dane o syrenach będą zapisywane w tym pliku.
To oznacza, że chcemy poszukiwać Elvisa.
:E! CF 7:!1,11:!1+=:!1
Wszystkie inne dane trafią do tego pliku.
Wszystkie przypadki zauważenia . Elvisa będą trafiały do tego pliku
Ale w jaki sposób można odczytywać argumenty wiersza poleceń wewnątrz programu? Na razie za każdym razem, gdy pisaliśmy funkcję ', była ona definiowana bez żadnych argumentów. Okazuje się jednak, że w rzeczywistości istnieją dwie postacie tej funkcji. Oto ta druga z nich: C!!+ C1?@' { """"MM6"""" }
Funkcja main() może odczytywać argumenty wiersza poleceń jako tablicę łańcuchów znaków. Oczywiście język C nie udostępnia wbudowanego typu reprezentującego łańcuchy znaków, zatem w rzeczywistości jest to tablica wskaźników typu char. Oto przykład:
·:E! C··F ·· 7:!1··,1··1:!1··+=:!1· To jest argv[0].
To jest argv[1].
To jest argv[2]. To jest argv[3]. To jest argv[4].
To jest argv[5].
Pierwszym argumentem jest . nazwa uruchamianego programu
Podobnie jak we wszystkich tablicach w języku C, także i w tym przypadku konieczna jest znajomość jej długości. To właśnie dlatego funkcja main() ma dwa argumenty. Pierwszy z nich, argc, jest liczbą określającą liczbę elementów w tablicy. Argumenty wiersza poleceń zapewniają naszym programom ogromną elastyczność i warto pomyśleć o tym, w jaki sposób użytkownicy mogliby chcieć zmodyfikować działanie naszego programu. Dzięki temu program może się dla nich stać znacznie bardziej użyteczny i cenny.
'" %
Pierwszy argument zawiera nazwę programu uruchomionego przez użytkownika.
To oznacza, że pierwszym właściwym argumentem jest 9A:.
No dobrze, zobaczmy, jak można przydać nieco elastyczności naszemu programowi categorize.
jesteś tutaj 139
Magnesiki z kodem
Magnesiki z kodem To jest zmodyfikowana wersja programu categorize, który pobiera -() , , ! # ! $ $( ) $ % , 1
:E! CF 7:!1,11:!1+=:!1 #include #include #include 667'9: { char line[80]; """""""""""~""""""""""" $¬ q/D$ return 1; } µ©R¼'~ $ M"6 $$$ FILE *file1 = fopen( ........... , ”w”); FILE *file2 = fopen( ........... , ”w”); FILE *file3 = fopen( ........... , ”w”);
140
Rozdział 3.
Tworzenie małych programów narzędziowych
while (fscanf(in, ”%79[^\n]\n”, line) == 1) { if (strstr(line, ...........)) fprintf(file1, ”%s\n”, line); else if (strstr(line, ...........)) fprintf(file2, ”%s\n”, line); else fprintf(file3, ”%s\n”, line); } fclose(file1); fclose(file2); fclose(file3); return 0; }
6
5 argv[5] argv[2]
argc
argv[4]
argv[1] argv[3]
jesteś tutaj
141
Magnesiki z kodem. Rozwiązanie
Magnesiki z kodem. Rozwiązanie To jest zmodyfikowana wersja programu categorize, który pobiera poszukiwane -(. ! , # ! ! # ! $ $( ) $ % , 1
:E! CF 7:!1,11:!1+=:!1 #include #include #include 667'9: { char line[80];
6 argc """""""""""~""""""""""" $¬ q/D$ return 1; } µ©R¼'~ $ M"6 $$$
argv[2] FILE *file1 = fopen( ........... , ”w”); argv[4] FILE *file2 = fopen( ........... , ”w”);
argv[5] FILE *file3 = fopen( ........... , ”w”);
142
Rozdział 3.
Tworzenie małych programów narzędziowych
while (fscanf(in, ”%79[^\n]\n”, line) == 1) { argv[1] if (strstr(line, ..............)) fprintf(file1, ”%s\n”, line); argv[3] else if (strstr(line, .............)) fprintf(file2, ”%s\n”, line); else fprintf(file3, ”%s\n”, line); } fclose(file1); fclose(file2); fclose(file3); return 0; }
5
jesteś tutaj 143
Jazda próbna
Jazda próbna W porządku, wypróbujmy nową wersję naszego kodu. Będziesz do tego potrzebował testowego pliku z danymi, noszącego nazwę spooky.csv.
|5"´qA|¤´"A|45FM~® 4´"|5U|´5¤U"qqABqFM~ªµY 4B"A|4BA¤A"A|UqFM~V 4´"|U|5q¤4"q|B5FM~¼ 4"´´4A¤´"55q|AFM~R |5"UB5A¤|"|||U5FM~*6 4"44UUU¤A"U5qAFM~ªµY 4B"U5A|45¤"54´|4FM~V |"´Bq|¤B"Uq|BFM~¼ 44"5q4q¤´"AB4A|BFM~¼ 4"ABq¤´"U´UA|AFM~¼
spooky.csv
Teraz wykonaj program 6 , przekazując do niego kilka argumentów wiersza poleceń określających poszukiwane teksty oraz nazwy plików:
%&/3&%B !&!?
;! C³
:!1,11:!1+=:!1
W efekcie wykonania programu zostaną utworzone następujące pliki:
144
Rozdział 3.
Tworzenie małych programów narzędziowych
4´"|5U|´5¤U"qqABqFM~ªµY
Jeśli przetworzysz plik elvises.csv przy użyciu programu geo2json, to będziesz mógł wyświetlić dane na mapie.
4"44UUU¤A"U5qAFM~ªµY
aliens.csv |5"´qA|¤´"A|45FM~® 4B"A|4BA¤A"A|UqFM~V 4"´´4A¤´"55q|AFM~R |5"UB5A¤|"|||U5FM~*6 4B"U5A|45¤"54´|4FM~V
the_rest.csv 4´"|U|5q¤4"q|B5FM~¼ |"´Bq|¤B"Uq|BFM~¼ 44"5q4q¤´"AB4A|BFM~¼ 4"ABq¤´"U´UA|AFM~¼ Elvis opuścił budynek.
elvises.csv
:# " 3 Choć w laboratoriach Rusz Głową nigdy nie popełniamy błędów (hm...), to jednak w programach używanych w realnych zastosowaniach należy sprawdzać, czy nie pojawią się jakieś problemy podczas otwierania plików do zapisu i odczytu. Na szczęście, jeśli podczas otwierania strumienia danych wystąpi jakiś problem, funkcja fopen() zwróci wartość 0. Oznacza to, że chcąc sprawdzić wystąpienie błędu, powinieneś zmienić poniższy wiersz kodu: FILE *in = fopen(”nie_istnieje.txt”, ”r”);
na następujący fragment: FILE *in; ~ $x "8$$$ $2 M"D$ return 1; }
jesteś tutaj 145
Opcje wiersza poleceń
Zasłyszane w Pizzerii Rusz Głową Z anchois i ananasem, na grubym cieście. Zróbcie ją migiem — potrzebuję jej do natychmiastowej dostawy.
Całkiem możliwe, że każdy program, który napiszesz, będzie potrzebował opcji. Jeśli będzie to program do internetowych pogawędek, to będzie potrzebował ustawień użytkownika. Jeśli będzie to gra, to użytkownik może chcieć zmienić kształt krwawych rozprysków. Jeśli natomiast będzie to narzędzie uruchamiane z poziomu konsoli, to zapewne będzie musiało korzystać z opcji wiersza poleceń. Opcje wiersza poleceń są niewielkimi przełącznikami, które bardzo często pojawiają się w programach narzędziowych:
znie Wyświetla wszystkie procesy włąc z informacjami o ich środowisku.
ps –ae tail –f logfile.out
146
Rozdział 3.
Wyświetla końcówkę pliku, lecz czeka, aż na końcu pliku zostaną dopisane nowe dane.
Tworzenie małych programów narzędziowych
Niech biblioteka wykona pracę za nas Wielu programistów używa opcji wiersza poleceń, a w bibliotece języka C dostępna jest funkcja, która nieco ułatwia ich obsługę. Nosi ona nazwę C', a każde jej wywołanie zwraca kolejną opcję podaną w wierszu wywołania programu. Zobaczmy, jak działa ta funkcja. Wyobraźmy sobie, że piszesz program, który może przyjmować kilka różnych opcji: Używać 4 silników.
Włączony tryb odlotowości.
!=¶P¶ ^ 0 7F Ten program wymaga jednej opcji posiadającej dodatkową wartość (-e — określającej liczbę używanych silników) oraz drugiej (-a — tryb odlotowości), która jest zwyczajnym przełącznikiem typu włączony/ wyłączony. Opcje te można obsługiwać, wywołując funkcję getopt() w pętli, jak pokazaliśmy na poniższym przykładzie: Musisz dodać ten plik nagłówkowy.
; 4 %&#,&unistd.h & ! )) & ! &$8 ! &, ,)) & %3F$%3F# ,)! ) "&4&, ( & $
#include To oznacza, że „opcja a jest prawidłowa, podobnie jak opcja e”.
... Tutaj jest umieszczony kod obsługujący poszczególne opcje.
Wczytujemy argument dla opcji „e”.
dbają Ostatnie dwa wiersze yli ocz esk prz y śm aby to, o je. poza przeczytane opc
+!+5C C! C1# #''V5,
³' !+!+'L Ten dwukropek oznacza, że opcja e potrzebuje argumentu. ... case ’e’: C =! 5 C* ... owuje liczbę optind przech ów odczytanych ak zn łańcuchów } leceń, której z wiersza po przeskoczyć za opcje.
C!D5 7* używamy, by
C1R5 7*
Wewnątrz pętli umieściłeś instrukcję switch, która obsługuje każdą z prawidłowych opcji. Łańcuch znaków ae: informuje funkcję getopt(), że prawidłowymi opcjami są a oraz e. Za opcją e został poza tym zapisany dwukropek, informujący funkcję getopt(), że po opcji -e musi być podany dodatkowy argument. Funkcja zapewnia dostęp do tego argumentu za pośrednictwem zmiennej optarg. Po zakończeniu pętli modyfikujesz wartości zmiennych oraz argc, aby przeskoczyć za opcje i pobrać pozostałe parametry podane w wierszu poleceń. Zmiana ta sprawi, że tablica będzie mieć następującą postać:
'" %
Po przetworzeniu argumentów zerowym argumentem nie będzie już nazwa programu.
95: będzie wskazywać na pierwszy argument wiersza poleceń, podany za opcjami.
FRM To jest argv[0].
To jest argv[1].
To jest argv[2].
jesteś tutaj 147
Zagadkowa pizza
Kawałki pizzy Wygląda na to, że ktoś poodgryzał kawałki kodu pizzy. Przekonaj się, czy będziesz potrafił zastąpić kawałki pizzy i odtworzyć program x
.
#include #include 667'9: { 67'M~$$ 76~5 int count = 0; char ch; 7 67~ 6$""""""""""""""""""""""""""""$~¼Yµ switch (ch) { case ’d’: ............... = ..............; Q case ’t’: ............... = ..............; Q default: $ 6G@ GD$ return ...............; }
148
Rozdział 3.
Tworzenie małych programów narzędziowych
argc -= optind; ~ 76 puts(”Grube ciasto.”); M95: $º @ "D$M $V+$ for (count = ...........; count < ..........; count++) 96: return 0; }
71F
argc optarg
+!
1 0
1 t
:
jesteś tutaj 149
Pizza wyjaśniona
Zagadkowa pizza. Rozwiązanie Wygląda na to, że ktoś poodgryzał kawałki kodu pizzy. Miałeś zastąpić kawałki pizzy i odtworzyć program x
.
#include #include 667'9: { 67'M~$$ 76~5 int count = 0;
Za opcją „d” jest umieszczony dwu gdyż wymaga ona podania argumen kropek, tu.
char ch;
t : 7 67~ 6$""""""""""""""""""""""""""""""""""""$~¼Yµ switch (ch) { case ’d’:
71F
optarg
............... = ...................;
Zmienna delivery będzie wskazywać na argument opcji „d”.
Q case ’t’:
+!
1
............... = ....................;
Pamiętaj: w języku C przypisaniu zmiennej wartości 1 odpowiada przypisanie jej wartości reprezentującej logiczną prawdę.
Q default: $ 6G@ GD$
return .....................; 1 }
150
Rozdział 3.
Tworzenie małych programów narzędziowych
argc -= optind; ~ 76 puts(”Grube ciasto.”); M95: $º @ "D$M składnik pizzy Po przetworzeniu opcji pierwszy a argv[0]. żeni wyra iu użyc przy ać można pobr
$V+$
0
argc
for (count = ..................; count < .................; count++) 96: return 0; }
Pętla będzie działać tak długo, jak długo wartość count będzie mniejsza od argc.
jesteś tutaj
151
Jazda próbna
Jazda próbna A teraz wypróbuj poprawiony program do zamawiania pizzy:
Skompiluj program. Podczas pierwszych dwóch prób wykonania programu nie będziesz używał żadnych opcji.
Następnie zastosuj opcję „d”, a jako jej argument podaj „zaraz”.
Teraz wypróbuj opcję „t”. Pamiętaj: ta opcja nie lubi żadnych argumentów.
I w końcu spróbuj pominąć argument opcji „d” — w efekcie zostanie wyświetlony błąd.
%&/3&% 9
;C!!7= :!D7=
;:E7=
!+ KY 7 anchois ;:E7=
!+
KY 7 anchois ananas ;:E7= D7
!+
¸ : KY 7 anchois ananas ;:E7= D7 D
!+
Grube ciasto. ¸ : KY 7 anchois ananas ;:E7= D7 7=
C DD7 `
! M 'M >
Program działa! W tym rozdziale dowiedziałeś się całkiem sporo. Poznałeś tajniki standardowego strumienia wejściowego, standardowego strumienia wyjściowego oraz standardowego strumienia błędów. Dowiedziałeś się, jak rozmawiać z plikami przy użyciu przekierowań oraz samodzielnie tworzonych strumieni danych. I wreszcie, na samym końcu rozdziału dowiedziałeś się, jak obsługiwać argumenty i opcje wiersza poleceń. Wielu programistów korzystających z języka C spędza wiele czasu na pisaniu niewielkich, prostych programów narzędziowych, a większość programów tego typu, jakie można znaleźć w systemach operacyjnych, takich jak Linux, zostało napisanych właśnie w C. Jeśli zachowasz odpowiednią uwagę podczas projektowania programów narzędziowych i zadbasz o to, by programy te robiły tylko jedną rzecz i robiły ją bardzo dobrze, to znajdziesz się na prostej drodze, by stać się doskonałym programistą C.
152
Rozdział 3.
Tworzenie małych programów narzędziowych Nie istnieją
głupie pytania
P: Czy mogę łączyć opcje, zapisując
je jako -td zamiast -d -t?
O
: Tak, możesz. Funkcja getopt() poradzi sobie z takim zapisem.
P: A co ze zmianą kolejności opcji? O: Ze względu na sposób odczytywania
P: Jeśli zatem program zauważy
w wierszu poleceń wartość rozpoczynającą się od znaku minusa, to potraktuje ją jako opcję?
O: Jeśli odczyta ją, zanim dotrze do
głównych argumentów wiersza poleceń, to tak — potraktuje ją jako opcję.
opcji nie ma znaczenia, czy zapiszesz je jako: ¤ ¤, ¤¤ , czy też jako ¤ .
P: A co, jeśli będę chciał przekazać
argument mający wartość ujemną, na przykład set_temperature -c -4? Czy funkcja nie pomyśli, że 4 jest opcją, a nie wartością argumentu?
O: Aby uniknąć niejednoznaczności,
można oddzielić główne argumenty wywołania programu od jego opcji przy użyciu dwóch znaków minusa: --. A zatem podane wywołanie możesz zapisać jako: set_temperature -c -- -4. Funkcja getopt() zakończy odczytywanie opcji, gdy napotka sekwencję dwóch znaków minusa, czyli dalsza część wiersza poleceń zostanie potraktowana jako zwyczajne argumenty.
CELNE SPOSTRZEŻENIA Q
Istnieją dwie wersje funkcji main() — jedna z argumentami wiersza poleceń oraz druga — bez żadnych argumentów.
Q
Argumenty wiersza poleceń są przekazywane do funkcji main() jako liczba argumentów oraz tablica wskaźników na łańcuch znaków zawierająca podane argumenty.
Q
Opcje wiersza poleceń to argumenty poprzedzone znakiem minusa (-).
Q
W obsłudze opcji wiersza poleceń może nam pomóc funkcja getopt().
Q
Prawidłowe opcje definiuje się, przekazując je do funkcji getopt() w formie łańcucha znaków, takiego jak ae:.
Q
Umieszczenie dwukropka (:) za opcją w tym łańcuchu oznacza, że opcja pobiera dodatkowy argument.
Q
Funkcja getopt() zapewnia dostęp do argumentu opcji za pośrednictwem zmiennej optarg.
Q
Po odczytaniu wszystkich opcji należy przeskoczyć za nie, korzystając ze zmiennej optind.
jesteś tutaj 153
Niezbędnik C
C jest bardzo małym językiem programowania. Oto wszystkie jego zarezerwowane słowa kluczowe (ich kolejność nie ma znaczenia). Każdy program napisany w języku C można podzielić na te słowa kluczowe oraz kilka symboli. Jeśli któregoś z tych słów spróbujesz użyć jako nazwy, kompilator będzie bardzo, ale to bardzo zły.
auto int char return do static F F7 for + const
if case register default else !+ float unsigned enum signed
break long continue short double struct T union goto 17 1
jesteś tutaj 179
Udostępnianie kodu file_hider
Jeśli oprogramowałeś często używane operacje… Istnieją spore szanse, że kiedy zaczniesz pisać więcej programów w C, pojawią się pewne funkcje i możliwości, które chciałbyś wykorzystywać wielokrotnie w różnych programach. Spójrzmy na przykład na specyfikację dwóch programów przedstawione po prawej stronie.
Wczytać zawartość pliku i utworzyć jego nową wersję zaszyfrowaną prz y użyciu operacji XOR.
Szyfrowanie przy użyciu operacji XOR jest bardzo prostym sposobem ukrywania tekstu poprzez określenie bitowej alternatywy wykluczającej każdego znaku tego tekstu oraz jakiejś wartości. Nie jest to metoda szczególnie bezpieczna, lecz wyjątkowo łatwa do zaimplementowania. Co więcej, ten sam kod, który jest używany do zaszyfrowania tekstu, może także posłużyć do jego odszyfrowania. Oto funkcja służąca do szyfrowania przekazanego łańcucha znaków: void oznacza, że funkcja nic nie zwraca.
17 !F!+ C'
y Do funkcji przekazujem licy tab do nik wskaź znaków. ą !+ !* cał ąda egl Pętla prz To oznacza, że licy tab zawartość będziemy obliczać + C'L i zamienia każdy alternatywę jej element na jego wykluczającą każdego nik. zaszyfrowany odpowied C5 CO&* znaku i liczby 31.
{
ider message_h ów rię łańcuch Wczytać se ego standardow znaków ze o wejścioweg strumienia owym rd a d na stan ić tl ie św y iw ich wyjściowym strumieniu wane iki zaszyfro odpowiedn R. operacji XO przy użyciu
CRR* } }
Obliczenia matematyczne na znakach? Możemy tak robić, gdyż char jest liczbowym typem danych.
…dobrze byłoby je udostępnić Wygląda na to, że oba powyższe programy będą musiały korzystać z tej samej funkcji szyfrującej — 6M . Wystarczyłoby zatem skopiować jej kod z jednego programu do drugiego, prawda? Takie rozwiązanie nie jest nawet takie złe, zakładając, że kopiowany kod nie jest zbyt duży; ale co zrobić, jeśli trzeba by skopiować naprawdę bardzo dużo kodu? Albo co trzeba by zrobić, gdyby sposób działania funkcji 6M sprawiał, że w przyszłości trzeba by ją zmienić? Gdyby istniały dwie kopie kodu funkcji, to zmiany trzeba by wprowadzać w dwóch miejscach. Aby nasz kod dobrze się skalował, koniecznie musimy znaleźć jakiś sposób pozwalający na wielokrotne stosowanie tego samego fragmentu kodu — sposób na udostępnienie zbioru funkcji w wielu różnych programach.
Jak możesz to zrobić?
180
Rozdział 4.
WYSIL
SZARE KOMÓRKI Wyobraź sobie, że masz grupę funkcji, których chciałbyś używać w różnych programach. Gdybyś to Ty zaprojektował język C, to w jaki sposób zapewniłbyś możliwość takiego współdzielenia kodu?
Stosowanie wielu plików źródłowych
Możesz rozdzielić kod, umieszczając go w osobnych plikach Jeśli dysponujesz fragmentem kodu, którego chciałbyś używać w kilku różnych plikach, to sensownym rozwiązaniem byłoby umieszczenie go w osobnym pliku .c. Gdyby kompilator potrafił w jakiś sposób automatycznie dołączać taki współdzielony kod podczas kompilacji programu, to można by takiego kodu używać jednocześnie w wielu aplikacjach. Dzięki temu, jeśli kiedyś pojawi się konieczność dokonania zmian w tym współdzielonym kodzie, wystarczy je wprowadzić w jednym miejscu. ny To jest współużytkowa fragment kodu.
Odczytuje zawartość pliku, zapisuje plik.
Szyfruje tekst.
Odczytuje tekst ze standardowego strumienia wejściowego, wyświetla tekst.
Kompilator wkompiluje współużytkowany fragment kodu do każdego programu.
file_hider
Musimy znaleźć jakiś sposób, by powiedzieć kompilatorowi, że ma stworzyć program na podstawie wielu plików źródłowych.
message_hider
Jeśli chcemy skorzystać z osobnego pliku .c zawierającego współużytkowany kod, to pojawia się pewien problem. Do tej pory tworzyliśmy wyłącznie programy na podstawie tylko jednego pliku źródłowego. A zatem jeśli mieliśmy program blitz_hack, to był on tworzony na podstawie jednego pliku źródłowego o nazwie blitz_hack.c. Jednak teraz chcemy znaleźć jakiś sposób, by przekazać kompilatorowi grupę plików źródłowych i powiedzieć mu: „Słuchaj, masz skompilować program na podstawie tych wszystkich plików”. Jak to zrobić? Jakiej składni wywołania programu gcc użyć w takim przypadku? A przede wszystkim, co — z punktu widzenia kompilatora — oznacza wygenerowanie jednego programu wykonywalnego na podstawie wielu plików źródłowych? Jak taki program miałby działać? W jaki sposób połączyć poszczególne pliki?
Aby zrozumieć, w jaki sposób kompilator może stworzyć jeden program na podstawie wielu plików źródłowych, trzeba się przyjrzeć, jak działa kompilacja…
jesteś tutaj
181
Jak działa kompilacja
Za kulisami kompilacji Aby zrozumieć, w jaki sposób kompilator może skompilować kilka plików źródłowych i wygenerować na ich podstawie jeden program wykonywalny, musimy zajrzeć za kulisy i przekonać się, jak w rzeczywistości wygląda proces kompilacji.
1
Hm… A zatem mam skompilować te wszystkie pliki źródłowe i zrobić z nich jeden program? Zobaczmy, co uda mi się z tego przyrządzić…
, ? #$
Pierwszą operacją, jaką musi przeprowadzić kompilator, jest modyfikacja kodu źródłowego. Musi on dodać do kodu zawartość wszystkich plików nagłówkowych, o których został poinformowany przy użyciu dyrektyw #include. Oprócz tego może się także zdarzyć, że kompilator będzie musiał rozwinąć lub pominąć niektóre fragmenty programu. Po wykonaniu tych operacji kod źródłowy będzie już gotowy do faktycznej kompilacji. Może to robi ć, korzystając z #define oraz poleceń #i Zobaczysz, ja fdef. korzystać, w k z nich części książk dalszej i.
„Dyrektywa” to takie wymyślne określenie „polecenia”.
W pierwszej kolejności umieszczę w kodzie źródłowym kilka dodatkowych składników…
2 2 , $ + Może się wydawać, że C jest językiem programowania całkiem niskiego poziomu, jednak prawda jest taka, że jego poziom nie jest aż tak niski, by mógł go zrozumieć komputer. Tak naprawdę komputer może zrozumieć wyłącznie kod maszynowy bardzo niskiego poziomu, a jedynym sposobem wygenerowania takiego kodu jest przetłumaczenie kodu źródłowego w C na symbole w języku asemblera, takie jak te: ¦¤4U @Q@8 Q @8@8 @8@8
Wygląda tajemniczo? Język asemblera opisuje pojedyncze instrukcje, które będzie musiał wykonać procesor podczas wykonywania programu. Kompilator C posiada cały zestaw przepisów dotyczących sposobu tłumaczenia poszczególnych elementów języka. Przepisy te informują kompilator, w jaki sposób przekształcić instrukcję if lub wywołanie funkcji na sekwencję instrukcji w języku asemblera. Jednak nawet język asemblera nie ma odpowiednio niskiego poziomu, by mógł go zrozumieć komputer. Właśnie dlatego niezbędny jest…
182
Rozdział 4.
A zatem w przypadku tej instrukcji „if” muszę zacząć od umieszczenia na stosie…
Stosowanie wielu plików źródłowych Czas zrobić z tego kodu symbolicznego coś strawnego.
3
$ + , +
Kompilator będzie musiał przekształcić kody symboliczne na kod maszynowy nazywany także kodem obiektowym. Jest to rzeczywisty kod binarny, który będzie wykonywany przez układy scalone procesora. To naprawdę pikantny kawał zapisany w kodzie maszynowym.
&description);
CELNE SPOSTRZEŻENIA Q
Q
Podczas wywoływania funkcji wartości są kopiowane do zmiennych będących jej parametrami. Można tworzyć wskaźniki na struktury, podobnie jak na dane innych typów.
Q
Wyrażenie ¤ ma to samo znaczenie co ' ".
Q
Zapis ¤ niweluje konieczność stosowania nawiasów i sprawia, że kod staje się bardziej czytelny.
jesteś tutaj 243
Inne typy danych
Czasami rzeczy podobnego typu wymagają danych różnych typów Struktury pozwalają nam modelować bardziej skomplikowane obiekty z realnego świata. Istnieją jednak takie informacje, których nie można zapisać przy użyciu jednego typu danych:
Dziś w promocji: Liczba całkowita Liczba zmiennoprzecinkowa
6 jabłek 1,5 kg truskawek 0,5 l soku z pomarańczy
Liczba zmiennoprzecinkowa
A zatem, gdybyśmy chcieli zapisać, dajmy na to, ilość czegoś, a tą ilością mogą być liczba, waga lub objętość, to w jaki sposób moglibyśmy to zrobić? No cóż, zawsze można by utworzyć strukturę zawierającą kilka pól, taką jak ta przedstawiona poniżej: M 6 """ 76 7 """
Jednak nie jest to dobre rozwiązanie, i to z kilku powodów:
Ì
7 +!
Ì
21 #$+ ! 1!
Ì
3 # $+ H1Q
Naprawdę byłoby bardzo użyteczne, gdybyśmy mogli stworzyć jeden typ danych reprezentujący ilość, a dopiero potem, dla konkretnej informacji, zdecydować, czym ta ilość będzie — liczbą, wagą czy objętością.
W języku C można to zrobić, korzystając z unii.
244
Rozdział 5.
ą ilość. Wszystkie dane opisuj
Struktury, unie i pola bitowe
Unie pozwalają używać bloku pamięci na różne sposoby Zawsze gdy tworzymy strukturę, komputer umieści jej pola w pamięci, jedno za drugim: To jest miejsce na wiek, wyrażony jako liczba typu int. To jest wskaźnik na znaki określające nazwę.
char *name
int age
float weight
To jest liczba zmiennoprzecinko do przechowywanwa ia wagi.
º~4B´"q Unie, tworzone przy użyciu słowa kluczowego union, są inne. Unia będzie wykorzystywać przydzielony jej obszar pamięci do przechowywania wartości tylko jednego z pól podanych w jej definicji. A zatem gdybyśmy mieli unię o nazwie ¦M ze zdefiniowanymi polami 6, 7 oraz (reprezentującymi odpowiednio: liczbę, wagę oraz objętość), to komputer przydzieliłby jej wystarczająco dużo miejsca w pamięci, by można było zapisać wartość największego z pól, a następnie pozostawił nam możliwość wyboru, którego z pól chcemy użyć. Niezależnie od tego, czy użylibyśmy pola 6, 7, czy , dane zostałyby zapisane w tym samym miejscu pamięci. ¦M
Unia wygląda zupełnie jak struktura, przy czym jest definiowana przy użyciu słowa kluczowego union.
typedef union {
Jeśli wartość typu float zajmuje 4 bajty, a wartość typu short 2 bajty, to unia będzie mieć 4 bajty wielkości.
short count; float weight;
Każde z tych pól będziesamym przechowywane w tym miejscu.
float volume; } quantity; ów, To są dane różnych typ ają ilość. jednak wszystkie wyraż
Objętość soku Liczba pomarańczy
Waga winogron
jesteś tutaj 245
Unie
Jak stosować unie?
Nie istnieją
głupie pytania
Po zadeklarowaniu zmiennej typu możemy przypisać jej wartość na kilka różnych sposobów.
P: Dlaczego unia zawsze ma rozmiar
Określanie wartości pierwszego pola według standardu C89
O: Komputer musi zapewnić, by unia zawsze
Jeśli w zmiennej typu mamy zamiar zapisać wartość pierwszego pola, to możemy w tym celu użyć zapisu stosowanego w standardzie C89. Według tego zapisu, by przypisać wartość pierwszemu polu unii, wystarczy umieścić ją w nawiasach klamrowych: quantity q = {4};
W tym przypadku polu count została przypisana wartość 4.
Wyznaczone inicjalizatory określają wartości innych pól Wyznaczone inicjalizatory określają wartości pól unii na podstawie ich nazwy; oto przykład:
F5L:C+5&:[N*
W tym przypadku w unii zostanie zapisana zmiennoprzecinkowa wartość pola weight.
największego pola?
miała tę samą wielkość. Jedynym sposobem, w jaki może to zrobić, jest zapewnienie, że będzie ona na tyle duża, by można w niej było zapisać wartość dowolnego z jej pól.
P: Dlaczego używając zapisu ze standardu C89, można ustawić wyłącznie wartość pierwszego pola? Dlaczego nie można by ustawić wartości pierwszego pola zmiennoprzecinkowego w razie przekazania wartości typu float?
O: Aby uniknąć niejednoznaczności. Gdyby
w unii dostępne było pole typu oraz drugie typu Q, to czy wyrażenie 4"A powinno zapisać wartość typu , czy typu Q? Dzięki temu, że zapisywana jest wyłącznie wartość pierwszego pola, dokładnie wiemy, w jaki sposób dane zostaną zainicjowane.
Określenie wartości przy użyciu zapisu z kropką Trzecim sposobem określania wartości unii jest utworzenie zmiennej w jednym wierszu kodu, a w drugim określenie wartości wybranego pola: quantity q; :1 5O:J*
Pamiętaj: niezależnie od wybranego sposobu ustawiania wartości unii zawsze będzie w niej przechowywana tylko jedna informacja. Unie zapewniają nam jedynie możliwość tworzenia zmiennych, które mogą przechowywać wartości kilku różnych typów.
246
Rozdział 5.
Uprzejmy przewodnik po standardach Wyznaczone inicjalizatory & ( , "& $; ( , & 4& & 4 # ** ! &$ )# !& ( & ,4 & & niektórych wersji ! & ) ( ($ ! &Objective C! 4 ^^nie$
Struktury, unie i pola bitowe Wygląda na to, że te wyznaczone inicjalizatory mogłyby być przydatne także w przypadku korzystania ze struktur. Zastanawiam się, czy także tam mogę ich używać.
Tak, wyznaczonych inicjalizatorów można używać także do określania początkowych wartości pól struktur. Mogą się one okazać niezwykle użyteczne, gdy korzystamy ze struktur zawierających bardzo dużo pól i początkowo chcemy określić wartości jedynie kilku z nich. Poza tym stanowią one także doskonały sposób poprawiania czytelności kodu: M 6 6 67'6 77
Ten inicjalizator określi wartości pól gears oraz height, lecz nie ustawi wartości pola color.
Q ""5L:+C+5&J:C 5%&N*
Unie są często stosowane razem ze strukturami Tworząc unię, deklarujemy jednocześnie nowy typ danych. Oznacza to, że wartości tego nowego typu możemy używać, gdzie tylko zechcemy, tak samo jak danych dowolnych innych typów, takich jak lub 6. Na przykład możemy użyć ich w strukturze: typedef struct { const char *name; const char *country; quantity amount; } fruit_order;
Do wartości zapisanych w takim połączeniu struktury i unii odwołujemy się, korzystając z operatorów " (kropki) oraz ¤, w sposób opisany już wcześniej: j, gdyż jest to nazwa Fragment .amount pojawił się tuta m struktury. pole cej będą tity, zmiennej typu quan
W tym miejscu korzystamy z podwójnego wyznaczonego identyfikatora. .amount odnosi się do pola struktury, a .weight do unii zapisanej w polu .amount.
=7 5L# "Y ## #: :C+5P:%N* #U 7%:%C° # : :C+ : '* To wywołanie wyświetli tekst: „Zamówiono jabłka o wadze 4.2 kg”.
jesteś tutaj 247
Pomieszane drinki
Pomieszane drinki Nadeszła Noc z Margaritą w Salonie Rusz Głową, jednak po następnej kolejce okazało się, że barmanowi najwyraźniej pomieszały się receptury drinków. Sprawdź, czy będziesz potrafił znaleźć pasujące fragmenty kodu dla różnych rodzajów margarity. Oto podstawowe składniki:
M x6 x
M 6 ¦ 6 x6 A oto różne rodzaje margarity: C 5L%:closes i->opens żaden dodatkowy } kod.
}
Nie istnieją
głupie pytania
P: Inne języki programowania, takie jak Java, udostępniają wbudowane listy połączone. Czy w C są jakieś wbudowane struktury danych?
O: W rzeczywistości w języku C nie ma wbudowanych żadnych typów reprezentujących struktury danych. Trzeba je tworzyć samemu.
P: Co musiałbym zrobić, chcąc
skorzystać z siedemsetnego elementu listy? Czy musiałbym zacząć od samego początku i odczytać kolejno wszystkie elementy aż do tego, który mnie interesuje?
O: Właśnie tak.
272
Rozdział 6.
P: To niezbyt fajnie. Sądziłem, że P: Pokazaliście typ struct, który listy połączone są lepsze od tablic.
O: Nie należy myśleć o strukturach
zawiera wskaźnik do kolejnej struktury tego samego typu. Czy typ struct może rekurencyjnie zawierać sam siebie?
P: Jeśli zatem potrzebuję
O: Nie. P: Dlaczego nie? O: Język C musi znać dokładną
danych w kategoriach „lesze” i „gorsze”. Są one odpowiednie lub nieodpowiednie do konkretnego zastosowania.
struktury danych, która pozwala na szybkie wstawianie elementów, to powinienem użyć listy połączonej, a jeśli szukam możliwości bezpośredniego dostępu, to tablicy?
O: Właśnie.
wielkość pamięci, jaką typ struct będzie zajmował w pamięci. Gdyby możliwe było tworzenie rekurencyjnych kopii tego samego typu wewnątrz niego samego, to jedna struktura tego typu miałaby inną wielkość od innej.
Struktury danych i pamięć dynamiczna
Jazda próbna Wykorzystajmy funkcję display(), by wyświetlić wyspy na trasie wycieczki. Dodaj ją do kodu i skompiluj go jako program o nazwie tour. M~$M ? M«$$5B55$$A55$ªRR 6M~$M V $$5B55$$A55$ªRR xQ~$M 7$$5B55$$A55$ªRR 7~$M *$$5B55$$A55$ªRR amity.next = &craggy; craggy.next = &isla_nublar; isla_nublar.next = &shutter; ~$M $$5B55$$A55$ªRR isla_nublar.next = &skull; skull.next = &shutter; display(&amity);
Doskonale. Kod tworzy listę połączoną struktur typu island, a wstawianie do niej kolejnych elementów jest bardzo łatwe i nie wymaga wiele pracy. No dobrze, dysponujesz już zatem znajomością podstawowych zagadnień związanych ze strukturami rekurencyjnymi oraz listami i możesz zająć się pracą nad głównym programem. Tym razem Twoim zadaniem jest odczytanie zawartości pliku tekstowego o następującej postaci: Wyspa Delfinów Anielska Wyspa Tu dalej znajduje się jeszcze więcej wysp.
Wyspa Dzikiego Kota M X+
Pracownicy linii lotniczej wciąż dopisują do listy nowe wyspy, dlatego póki nie uruchomisz programu, nie będziesz wiedział, jaka jest długa. W każdym wierszu pliku została zapisana nazwa jednej wyspy. Przekształcenie zawartości tego pliku na listę połączoną nie powinno być większym problemem, prawda?
%&/3&%8)+!& &
66"6¤ZZ"& M ? M« 675B55¤A55 M V 675B55¤A55 M 7 675B55¤A55 M 675B55¤A55 M * 675B55¤A55 >
; 4 & 4 # 4 & — skull$H&
(# q ** vv! &$ & & )( & &"&$
jesteś tutaj 273
Pamięć dynamiczna
Hm… Do tej pory każdy element listy zapisywaliśmy w osobnej zmiennej. Skoro jednak nie wiemy, ile wysp zostało zapisanych w pliku, to skąd dowiemy się, ilu zmiennych nam potrzeba? Zastanawiam się, czy istnieje możliwość zarezerwowania nowego miejsca w pamięci wtedy, gdy pojawi się taka potrzeba.
Owszem, potrzebujesz jakiegoś sposobu dynamicznego rezerwowania pamięci. Wszystkie programy, które pisaliśmy do tej pory, korzystały z pamięci statycznej. Za każdym razem, kiedy musieliśmy przechować jakąś informację, dodawaliśmy do kodu odpowiednią zmienną. Zmienne te zazwyczaj były przechowywane na stosie. Pamiętaj: stos jest obszarem pamięci przeznaczonym do przechowywania zmiennych lokalnych. A zatem, kiedy miałeś utworzyć cztery wyspy, zrobiłeś to w następujący sposób: M~$M ? M«$$5B55$$A55$ªRR 6M~$M V $$5B55$$A55$ªRR xQ~$M 7$$5B55$$A55$ªRR 7~$M *$$5B55$$A55$ªRR
Do przechowania każdej struktury island potrzebna była odrębna zmienna. Ten fragment kodu zawsze utworzy dokładnie cztery struktury typu island. Gdybyś jednak chciał utworzyć więcej niż cztery wyspy, to potrzebowałbyś kolejnych zmiennych. Takie rozwiązanie jest w porządku, jeśli już w czasie kompilacji wiemy, ile danych będziemy musieli przechowywać; jednak całkiem często zdarza się, że program dopiero w momencie działania dowiaduje się, ile miejsca na dane potrzebuje. Gdybyś pisał na przykład przeglądarkę WWW, to nie wiedziałbyś, ile miejsca na stronę masz zarezerwować, aż do momentu jej odczytania. Właśnie z tego względu programy pisane w języku C muszą dysponować jakimś sposobem poinformowania systemu operacyjnego o tym, że potrzebują dodatkowego miejsca w pamięci, i to w momencie kiedy pojawi się taka potrzeba.
Programy potrzebują pamięci dynamicznej.
274
Rozdział 6.
Struktury danych i pamięć dynamiczna
Czyż nie byłoby cudownie, gdyby już w trakcie działania programu istniał sposób zarezerwowania obszaru pamięci o takiej wielkości, jaka jest nam potrzebna? Wiem jednak, że to tylko moje fantazje…
jesteś tutaj 275
malloc()
Pamięć dynamiczną rezerwuj na stercie Większość pamięci, jakiej używaliśmy do tej pory, była przechowywana na stosie. Stos jest obszarem pamięci przeznaczonym do przechowywania zmiennych lokalnych. Każda informacja jest przechowywana w zmiennej, a każda zmienna znika, gdy tylko zakończy się wykonywanie funkcji. Problem polega na tym, że przydzielanie dodatkowego miejsca na stosie w trakcie działania programu jest dosyć trudne. I właśnie w tym momencie na scenę wkracza sterta. Jest ona tym obszarem pamięci używanym przez programy do przechowywania danych, których będą używały przez dłuższy czas. Sterta nie zostanie automatycznie oczyszczona, zatem doskonale nadaje się do przechowywania struktur danych, takich jak nasza lista połączona. Możesz ją sobie wyobrazić jako bankowy sejf, w którym można wynajmować skrytki.
Rezerwowanie pamięci na stercie jest jak przechowywanie kosztowności w bankowej skrytce.
W pierwszej kolejności zarezerwuj pamięć, używając funkcji malloc() Wyobraź sobie, że Twój program w trakcie działania dowiaduje się, że musi przechować znaczne ilości danych. To trochę tak, jakby prosić o dużą skrytkę na dane, a w języku C robimy to, wywołując funkcję malloc(). Funkcji tej przekazujemy dokładną wielkość potrzebnego nam obszaru pamięci, a ona z kolei prosi system operacyjny o zarezerwowanie takiego obszaru na stercie. Funkcja malloc() zwraca wskaźnik na nowy obszar pamięci przydzielony na stercie, mniej więcej tak, jak gdyby wręczała nam klucz do skrytki bankowej. Wskaźnik ten zapewnia dostęp do przydzielonego obszaru pamięci i możemy go używać, by w przyszłości móc korzystać z naszej skrytki.
32 bajty danych na stercie w miejsc u o adresie 4204853
Funkcja malloc() zwraca wskaźnik na obszar przydzielony na stercie.
276
Rozdział 6.
Sterta
Struktury danych i pamięć dynamiczna
Zwróć pamięć, kiedy nie będzie już potrzebna Bardzo dobrą informacją dotyczącą pamięci przydzielanej na stercie jest ta, że możemy z niej korzystać przez bardzo długi czas. Niestety jest też nieco gorsza wiadomość… możemy jej używać przez bardzo długi czas. W przypadku korzystania ze stosu nie musimy się przejmować zwracaniem używanej pamięci — będzie to robione automatycznie. Zawsze gdy funkcja kończy działanie, wszystkie jej dane lokalne zostają usunięte ze stosu. Jednak sterta działa inaczej. Kiedy już poprosimy o miejsce na stercie, to nie będzie można z niego ponownie skorzystać aż do momentu, gdy jawnie poinformujemy standardową bibliotekę C, że skończyliśmy go używać. Ilość miejsca na stercie jest ograniczona, jeśli więc nasz program będzie o nie bezustannie prosił, to szybko zaczną powstawać tak zwane wycieki pamięci.
Ilość miejsca na stercie jest ograniczona, pamiętaj zatem, by korzystać z niego mądrze.
Wycieki pamięci powstają, gdy program bezustannie prosi o nowe obszary pamięci, nie zwalniając tych, które już mu nie są potrzebne. Wycieki należą do najczęstszych błędów występujących w programach pisanych w języku C, a wyśledzenie ich i poprawienie może być naprawdę bardzo trudne.
Zwalniaj pamięć, wywołując funkcję free() Funkcja malloc() przydziela obszar pamięci na stercie i zwraca wskaźnik do niego. Posługując się tym wskaźnikiem, możemy korzystać z pamięci, a później, kiedy już nie będzie nam ona potrzebna, możemy ją zwolnić, przekazując ten wskaźnik w wywołaniu funkcji free(). To tak jak byśmy oddawali klucz do skrytki pracownikowi banku, by mógł z niej skorzystać ktoś inny.
Dzięki za miejsce w pamięci. Nie jest mi już dłużej potrzebne.
Zawsze gdy jakiś fragment naszego programu prosi o przydzielenie miejsca na stercie, wywołując funkcję malloc(), to jakiś inny jego fragment powinien zwalniać to miejsce za pomocą funkcji free(). Kiedy program zakończy działanie, cała pamięć zajmowana przez jego stertę zostanie automatycznie zwolniona, niemniej jednak dobra praktyka programistyczna nakazuje, by każdy dynamicznie przydzielony fragment pamięci zwracać, wywołując funkcję free().
danych 32 bajty ew na sterci miejscu 4204853 o adresie
Przekonajmy się, jak funkcje malloc() i free() wyglądają w praktyce.
jesteś tutaj 277
free()
Proś o pamięć, wywołując malloc()… Funkcja, która prosi o przydzielenie pamięci, nosi nazwę malloc()— od angielskich słów memory allocation (przydzielanie pamięci). Funkcja ta ma tylko jeden parametr — określa on liczbę bajtów, jaką należy zarezerwować w pamięci. W większości przypadków nie wiemy dokładnie, ile bajtów pamięci nam potrzeba, dlatego też w wywołaniach funkcji malloc() zazwyczaj pojawia się operator sizeof: #include
Dodanie pliku nagłówkowego stdlib.h jest niezbędne, by móc korzystać z funkcji malloc() oraz free().
... malloc(sizeof(island));
To znaczy: „Przydzielcie mi tyle pamięci, by zmieściła się w niej struktur a typu island”.
Operator sizeof określa, ile bajtów zajmuje w pamięci — w używanym systemie operacyjnym — wartość konkretnego typu. Może to być typ struct bądź jakikolwiek inny typ danych, taki jak int czy double. Funkcja malloc() rezerwuje dla nas odpowiedni blok pamięci, a następnie zwraca wskaźnik zawierający jego adres. A jakiego typu będzie ten wskaźnik? Otóż funkcja malloc() zwraca wskaźniki ogólnego przeznaczenia typu void*. island *p = malloc(sizeof(island));
To znaczy: „Zarezerwuj obszar pamięci wystarczający, by zmieścić strukturę typu island, a jego adres zapisz w zmiennej p”.
…i zwalniaj ją, wywołując free() Po zarezerwowaniu pamięci na stercie możemy jej używać dowolnie długo. Jednak kiedy już nie będzie nam potrzebna, powinniśmy ją zwolnić, wywołując funkcję free(). W wywołaniu funkcji free() należy przekazać wskaźnik do obszaru pamięci zarezerwowanego przez funkcję malloc(). O ile tylko przekażemy bibliotece C informację o tym, gdzie się znajduje zwalniany obszar pamięci, to będzie ona w stanie sprawdzić swoje notatki i określić jego wielkość. Aby zatem zwolnić pamięć zarezerwowaną przez powyższą instrukcję, powinniśmy użyć wywołania: free(p);
To oznacza: „Zwolnij pamięć, którą przydzieliłeś na stercie dla zmiennej p”.
No dobrze; skoro już dowiedziałeś się nieco więcej o pamięci dynamicznej, możesz wykorzystać tę wiedzę do rozbudowy swojego programu.
278
Rozdział 6.
Pamiętaj: jeśli w jednej części programu rezerwujesz pamięć przy użyciu funkcji malloc(), to zawsze powinieneś ją później zwolnić, wywołując funkcję free().
Struktury danych i pamięć dynamiczna
O nie! To chwilowo bezrobotni aktorzy… Początkujący aktorzy chwilowo nie mają żadnego angażu, dlatego też znaleźli nieco wolnego czasu w swoich napiętych terminarzach, by pomóc Ci przy kodowaniu. Napisali funkcję narzędziową służącą do tworzenia nowych wysp — danych typu island — o podanych nazwach. Poniżej przedstawiliśmy kod tej funkcji: Nazwa wyspy jest przekazywana jako wskaźnik na łańcuch znaków.
To jest nowa funkcja.
island* create(char *name) To wywołanie utworzy na stercie nową strukturę typu island.
Do przydzielenia obszaru na stercie jest używana funkcja malloc().
{ island *i = malloc(sizeof(island)); ¤~
Te wiersze określają ¤ ~$5B55$ wartości pól nowej ¤6 ~$A55$ struktury.
Operator sizeof określa wielkość potrzebnego obszaru pamięci wyrażoną w bajtach.
¤8~ªRR return i; } Funkcja zwraca adres nowej struktury.
Ta funkcja wygląda naprawdę fajnie. Aktorzy zauważyli, że większość lotnisk na wyspach jest zamykana i otwierana o tych samych godzinach, i dlatego przypisują polom opens i closes odpowiednie wartości domyślne. Funkcja zwraca wskaźnik na nowo utworzoną strukturę typu island.
WYSIL
SZARE KOMÓRKI Uważnie przeanalizuj kod powyższej funkcji create(). Czy myślisz, że może ona przysparzać jakichś problemów? Kiedy już to dokładnie przemyślisz, odwróć kartkę.
jesteś tutaj 279
Rozpoczyna się gra
Dziennik kapitana. 11:00. Pogoda doskonała. Napisano funkcję create() korzystającą z dynamicznego przydzielania pamięci i zespół programistów zadecydował, że jest ona gotowa do lotów próbnych. island* create(char *name) { island *i = malloc(sizeof(island));
Zagadka na -
¤~ ¤ ~$5B55$ ¤6 ~$A55$ ¤8~ªRR return i; }
14:15. Pochmurno. Wieje od przodu z kierunku północno-zachodniego z siłą 15 węzłów. Lądujemy przy pierwszej sposobności. Zespół programistów na pokładzie zapewnia niezbędny kod. Nazwa wyspy została wpisana z poziomu wiersza poleceń. Tworzymy tablicę do zapisania nazwy wyspy.
679´5: ´5
nazwy wyspy. Prosimy użytkownika o podanie
'x 5~6 Plik Edycja Okno Pomoc
> ./test_flight Atlantyda
14:45. Startujemy. Pas startowy niebezpieczny, dużo kamieni naniesionych przez trzęsienia ziemi. Zespół programistów wciąż jest na pokładzie. Zapasy Red Bulla zaczynają się kończyć…
280
Rozdział 6.
Struktury danych i pamięć dynamiczna
15:35. Dolatujemy do drugiej wyspy. Pogoda dobra. Brak wiatru. Wprowadzam szczegółowe informacje do nowego programu. Prosimy użytkownika o podanie nazwy drugiej wyspy.
´5 'x A~6
wyspę Łączymy pierwszą z drugą.
Ta instrukcja tworzy drugą wyspę.
x 5¤8~x A Plik Edycja Okno Pomoc
Wyspa Liliputów
17:50. Z powrotem w kwaterze głównej; odwalam papierkową robotę. Dziwne. Dziennik lotu wygenerowany przez program testowy ma jakiś błąd. Po wyświetleniu szczegółów dzisiejszego lotu okazało się, że w tajemniczy sposób została zmieniona nazwa pierwszej wyspy, na której lądowaliśmy. Proszę zespół programistów o rozeznanie problemu. M x 5 W ten sposób, korzystając z napisanej wcześniej funkcji, wyświetlimy szczegółowe informacje zapisane na liście odwiedzonych wysp. ??? A co się stało z Atlantydą?
Plik Edycja Okno Pomoc
Nazwa: Wyspa Liliputów, lotnisko otwarte w godzinach: 09:00 — 17:00 Nazwa: Wyspa Liliputów,
Teraz pierwsza ma taką samą wyspa nazwę jak druga!!!
lotnisko otwarte w godzinach: 09:00 — 17:00
Co się stało z nazwą pierwszej wyspy? Czy w kodzie funkcji create() jest jakiś błąd? Czy wskazówką może być sposób jej wywoływania?
jesteś tutaj 281
Zagadka rozwiązana
Co się stało z nazwą pierwszej wyspy? Spójrzmy jeszcze raz na kod funkcji create(): island* create(char *name) { island *i = malloc(sizeof(island)); ¤~ ¤ ~$5B55$ ¤6 ~$A55$ ¤8~ªRR return i; }
Kiedy kod funkcji zapisuje nazwę wyspy, nie robi kopii przekazanego łańcucha znaków name — zapisuje jedynie wskaźnik do miejsca, jakie on zajmuje w pamięci. Czy to ma jakieś znaczenie? Gdzie jest przechowywany łańcuch znaków zawierający nazwę wyspy? Możemy to sprawdzić, analizując kod wywołujący funkcję create(): 679´5: ´5 'x 5~6 ´5 'x A~6
Program prosi użytkownika o podanie nazwy każdej z wysp, jednak za każdym razem nazwa ta jest zapisywana w tej samej lokalnej tablicy znaków o nazwie name. A to oznacza, że obie wyspy będą korzystały z tego samego łańcucha znaków zawierającego nazwę. Kiedy tylko zmienna lokalna name zostanie zaktualizowana — czyli gdy zapiszemy w niej nazwę drugiej wyspy — zmieni się także nazwa pierwszej wyspy.
282
Rozdział 6.
Zagadka na -
Struktury danych i pamięć dynamiczna
: 34$ $ # W języku C całkiem często trzeba robić kopie łańcuchów znaków. Można to zrobić, wywołując funkcję malloc(), by przydzielić nieznaczny obszar pamięci na stercie, a następnie ręcznie kopiując kolejne znaki z łańcucha do przydzielonego miejsca na stercie. Ale wiesz co? Programiści już przed nami wpadli na lepszy pomysł. Stworzyli funkcję strdup() i udostępnili ją w pliku nagłówkowym string.h. Załóżmy, że dysponujemy wskaźnikiem na tablicę znaków, którą chcemy skopiować: 67' ~$¬YR©V$
M
O
N
A
L
I
S
A
\0
Funkcja strdup() jest w stanie utworzyć kompletną kopię łańcucha znaków przechowywaną gdzieś na stercie: char *copy = strdup(s);
1
Z [\ 1 $ $*" #
$ % [\ + ! +
W łańcuchu s jest dziesięć znaków, zaczynając od samego początku i kończąc na ostatnim znaku \0. Wywołanie malloc(10) właśnie mnie poinformowało, że udało się zarezerwować potrzebne miejsce na stercie, zaczynając od adresu 2500000.
2
3
% $*"
W komórce 2500000 znajduje się M, w komórce 2500001 znajduje się O…
Oznacza to, że funkcja strdup() zawsze tworzy kopię łańcucha na stercie. Nie może tworzyć jej na stosie, gdyż ten obszar jest przeznaczony dla zmiennych lokalnych, a zmienne lokalne są usuwane zdecydowanie zbyt często. Niemniej fakt, że funkcja strdup() umieszcza kopie łańcuchów na stercie, oznacza, że zawsze trzeba zwalniać zarezerwowaną przez nią pamięć, wywołując funkcję free().
jesteś tutaj 283
Zastosowanie funkcji strdup()
Popraw kod, używając funkcji strdup() Teraz możesz poprawić kod programu, używając funkcji strdup(). Oto, jak należy to zrobić: island* create(char *name) { island *i = malloc(sizeof(island)); Nie istnieją
¤~strdup(name); ¤ ~$5B55$ ¤6 ~$A55$ ¤8~ªRR return i; }
Zauważ, że funkcji tej trzeba użyć wyłącznie do zapisania wartości pola name. Czy potrafisz powiedzieć, dlaczego tak się dzieje? Otóż wynika to z faktu, że w polach opens oraz closes są zapisywane literały łańcuchowe. Czy pamiętasz, jak dawno, dawno temu opisywaliśmy, w jakich obszarach pamięci są przechowywane różnego rodzaju dane? Literały łańcuchowe są przechowywane w obszarze przeznaczonym dla stałych, który jest obszarem tylko do odczytu. Ponieważ w obu polach są zapisywane wartości stałe, zatem nie musisz ich zabezpieczać, tworząc kopie ich zawartości, gdyż i tak nigdy nie będą się one zmieniać. Musisz jednak zrobić taką kopię bezpieczeństwa tablicy name, gdyż w przyszłości jej zawartość może zostać zmodyfikowana.
Czy to zatem rozwiązuje nasz problem?
głupie pytania
P
: Gdyby w strukturze island zamiast wskaźnika na znaki była używana tablica znaków, to czy także musiałbym korzystać z funkcji strdup()?
O: Nie. W takim przypadku każda struktura
typu island posiadałaby własną kopię tablicy, więc samodzielne tworzenie jeszcze jednej kopii łańcucha nie byłoby potrzebne.
P: Dlaczego zatem w strukturze
miałbym używać wskaźnika na znaki, a nie tablicy znaków?
O: Wskaźnik na znaki w żaden sposób nie
narzuca wielkości pamięci, jaką trzeba będzie zarezerwować w celu przechowania łańcucha znaków. Gdybyś natomiast użył tablicy znaków, to z góry musiałbyś dokładnie określić dopuszczalną długość łańcucha.
Aby sprawdzić, czy zmiany wprowadzone w funkcji create() rozwiązały problem znikającej nazwy wyspy, musisz ponownie uruchomić program: %&/3&%] ]
"& x7 Atlantyda M R/ M 675B55ÊA55 M R/ 675B55ÊA55
Teraz kod działa jak należy. Za każdym razem, gdy użytkownik wpisuje nazwę wyspy, funkcja create() zapisuje ją w zupełnie nowym łańcuchu znaków.
No dobrze, skoro już dysponujesz funkcją do tworzenia danych wysp, użyj jej do skonstruowania listy połączonej wysp zapisanych w pliku.
284
Rozdział 6.
Struktury danych i pamięć dynamiczna
/ " Katastrofa! Kod służący do tworzenia wycieczki po wyspach wpadł do basenu! Twoim zadaniem jest wyłowić poszczególne fragmenty kodu z basenu i wstawić je w odpowiednie, puste miejsca programu. Z kolei Twoim celem jest odtworzenie programu, by mógł on odczytywać listę nazw ze standardowego strumienia wejściowego, a następnie przekształcał te nazwy w listę wysp. Każdego fragmentu kodu możesz użyć tylko raz, a do odtworzenia programu nie będziesz potrzebował wszystkich fragmentów pływających w basenie.
' ~ªRR '~ªRR '8~ªRR 679´5: for(; ........... != ............; i = ...............) { next = create(name); ~~ªRR start = .................; ~ªRR i ...........
.......... = next;
} display(start); Uwaga: każdy fragment kodu z basenu może zostać użyty tylko raz!
fgets(name, 80, stdin) next NULL NULL
next
-> .
next
jesteś tutaj 285
Kod wyciągnięty z basenu
/ " Katastrofa! Kod służący do tworzenia wycieczki po wyspach wpadł do basenu! Twoim zadaniem było wyłowić poszczególne fragmenty kodu z basenu i wstawić je w odpowiednie, puste miejsca programu. Z kolei Twoim celem było odtworzenie programu, by mógł on odczytywać listę nazw ze standardowego strumienia wejściowego, a następnie przekształcał te nazwy w listę wysp.
' ~ªRR '~ªRR '8~ªRR
ze Wczytujemy łańcuch znakówwejściowego. standardowego strumienia
679´5: for(;
fgets(name, 80, stdin)
next = create(name); Ta instrukcja tworzy nową wyspę. ~~ªRR next
start =
!=
NULL
; i =
->
} display(start);
next
286
Rozdział 6.
= next;
Nie zapominaj: i jest wskaźnikiem, zatem należy tu użyć operatora ->.
Uwaga: każdy fragment kodu z basenu może zostać użyty tylko raz!
NULL
next
można Pętla będzie działać tak długo, jak będzie odczytywać kolejne łańcuchy znaków. Podczas pierwszej iteracji pętli zmienna start ma wartość NULL, należy w niej ; zatem zapisać pierwszą wyspę.
~ªRR i
Pod koniec działania każdej iteracji pętli w zmiennej i zapisujemy kolejną utworzoną wyspę.
.
)
{
Struktury danych i pamięć dynamiczna
Zaostrz ołówek Chwileczkę! Jeszcze nie skończyłeś. Nie zapominaj, że zawsze gdy rezerwujesz pamięć, używając funkcji malloc(), musisz ją także zwolnić za pomocą funkcji free(). Twój program tworzy listę wysp, korzystając przy tym z funkcji malloc(); nadszedł zatem właściwy moment, byś uzupełnił go o fragment, który będzie zwalniał tę pamięć, kiedy już nie będzie potrzebna. Poniżej przedstawiliśmy początek kodu funkcji o nazwie release(), służącej do zwalniania pamięci używanej przez listę połączoną i wymagającej przekazania wskaźnika na jej pierwszy element:
void release(island *start) { island *i = start; '8~ªRR ~ªRR~8 next = ..............; ................; ................; } }
Zastanów się bardzo uważnie. Kiedy będziesz zwalniał pamięć zajmowaną przez wyspę, to co tak naprawdę będziesz musiał zwolnić? Tylko wartość typu island? A może coś jeszcze? W jakiej kolejności powinieneś zwalniać te dane?
jesteś tutaj 287
Zaostrz ołówek
Zaostrz ołówek. Rozwiązanie
Chwileczkę! Jeszcze nie skończyłeś. Nie zapominaj, że zawsze gdy rezerwujesz pamięć, używając funkcji malloc(), musisz ją także zwolnić za pomocą funkcji free(). Twój program tworzy listę wysp, korzystając przy tym z funkcji malloc(); nadszedł zatem właściwy moment, byś uzupełnił go o fragment, który będzie zwalniał tę pamięć, kiedy już nie będzie potrzebna. Poniżej przedstawiliśmy początek kodu funkcji o nazwie release(), służącej do zwalniania pamięci używanej przez listę połączoną i wymagającej przekazania wskaźnika na jej pierwszy element:
void release(island *start) { island *i = start; '8~ªRR ~ªRR~8 W pierwszej kolejności musisz zwolnić łańcuch znaków zawierający nazwę, skopiowany przy użyciu funkcji strdup().
W zmiennej next musisz zapisać wskaźnik na następną wyspę.
i->next next = ................; free(i->name)
................; free(i) ................;
}
Dopiero po zwolnieniu pamięci zajmowanej przez nazwę możesz zwolnić samą strukturę island.
}
Gdybyś najpierw zwolnił strukturę, to nie miałbyś jak odwołać się i zwolnić pamięci zajmowanej przez nazwę.
Zastanów się bardzo uważnie. Kiedy będziesz zwalniał pamięć zajmowaną przez wyspę, to co tak naprawdę będziesz musiał zwolnić? Tylko wartość typu island? A może coś jeszcze? W jakiej kolejności powinieneś zwalniać te dane?
Zwalniaj pamięć, gdy jej nie potrzebujesz Skoro dysponujesz już funkcją do zwalniania pamięci, powinieneś ją wywołać, gdy lista wysp nie będzie Ci już potrzebna. Twój program musi jedynie wyświetlić zawartość listy, a zatem kiedy już to zrobi, będziesz mógł ją zwolnić: display(start); release(start);
Kiedy to zrobisz, możesz sprawdzić program.
288
Rozdział 6.
Struktury danych i pamięć dynamiczna
Jazda próbna Co się zatem stanie, kiedy skompilujesz kod i uruchomisz program? %&/3&% ; >, % )
"&A"8 M º/ 675B55ÊA55 M 675B55ÊA55 M º H 675B55ÊA55 M 675B55ÊA55 X+ 675B55ÊA55 X+` / 675B55ÊA55 M Q 67º/ 675B55ÊA55 M µ 675B55ÊA55 µ 675B55ÊA55 M ½6 675B55ÊA55 M Q/ 675B55ÊA55 M 675B55ÊA55 R`6M 675B55ÊA55
Zadziałało. Pamiętaj: nie mogłeś wiedzieć, ile wysp znajdzie się w pliku. W tym przypadku jedynie wyświetlasz nazwy pobierane z pliku, zatem mógłbyś to zrobić bez zapisywania ich w pamięci. Ponieważ jednak umieściłeś je w pamięci, możesz na nich dowolnie operować. Mógłbyś dodać do wycieczki nowe etapy bądź też usunąć któreś z wysp znajdujących się na liście. Mógłbyś też zmienić ich kolejność. Dzięki pamięci dynamicznej możesz rezerwować pamięć i korzystać z niej w odpowiedzi na potrzeby pojawiające się W TRAKCIE DZIAŁANIA PROGRAMU. Możliwość korzystania z takiej dynamicznej pamięci przydzielanej na stercie zapewniają dwie funkcje: malloc() oraz free().
jesteś tutaj 289
Stos i sterta
Pogawędki przy kominku Temat dzisiejszej dyskusji:
Sterto, jesteś tam? Wróciłem. Niezbyt często widuję cię o tej porze. Niewiele się dziś dzieje? Głęboka regresja. Ups… przepraszam… Tylko to posprzątam… Co robisz? Kod właśnie skończył wykonywać funkcję. Muszę więc posprzątać pamięć zajmowaną przez te zmienne lokalne. Powinieneś nieco luźniej podchodzić do życia. Trochę się odprężyć… Może masz rację. Nie masz nic przeciwko, bym sobie usiadł? Piwa? Nie przejmuj się tą czapką, połóż ją byle gdzie. To… czy ona jest twoja? Hej, znalazłeś pizzę! Super! Szukałam jej przez cały tydzień. Naprawdę powinnaś zastanowić się nad wynajęciem kogoś, kto zadba o to miejsce. Nie przejmuj się. To aplikacja do zamawiania przez internet ją tu zostawiła. Pewnie kiedyś po nią wróci. Skąd możesz to wiedzieć? Chodzi mi o to, skąd wiesz, że o niej nie zapomniała? Skontaktowałaby się. Wywołałaby free(). Hm? Jesteś pewna? Czy nie napisała jej ta sama kobieta, która jest autorką przerażającej gry Rozdepcz-króliczka? Wszędzie te wycieki pamięci. Ledwie co udało mi się dostać do struktur tych króliczków. Wszędzie te pozostałości. To było coś okropnego.
290
Rozdział 6.
Struktury danych i pamięć dynamiczna
Słuchaj, czyszczenie pamięci nie należy do zakresu moich obowiązków. Ktoś prosi mnie o pamięć, ja mu ją przydzielam. Potem ją zostawiam w spokoju, aż mnie poproszą o jej zwolnienie.
To dosyć nieodpowiedzialne podejście. Hm… Może. Ale za to jestem łatwa w obsłudze. Nie tak jak ty i to twoje… zawracanie głowy. Zawracanie głowy? Ja nie zawracam nikomu głowy! Może chcesz chusteczkę? Proszę? Chodzi mi o to, że trudno za tobą nadążyć. Ja po prostu uważam, że pamięcią należy odpowiednio zarządzać. Nieważne. Ja mam zasadę: żyj i daj żyć innym. Jeśli program lubi bałaganić, to sprzątanie po nim nie jest moim problemem. Bałaganiara. Jestem po prostu wyluzowana. Dlaczego nie zatrudniłaś mechanizmu oczyszczania pamięci? Aha… znowu się zaczyna… Chodzi mi tylko o… trochę porządku. Ty przecież nie robisz nic!!! Wyluzuj, stary. Przepraszam. Ale nie jestem w stanie poradzić sobie z takim stopniem nieuporządkowania. Hej… przepełniasz się. Weź to… Dziękuję. Chwila, a co to jest? To najlepszy wynik z gry Rozdepcz-króliczka. Nie przejmuj się; nie przypuszczam, żeby ten program go jeszcze potrzebował.
jesteś tutaj 291
Nie ma głupich pytań Nie istnieją
głupie pytania
P: Dlaczego sterta jest nazywana stertą?
O: Ponieważ komputer nie zarządza
automatycznie jej zawartością. To po prostu wielka sterta danych.
P: Rozumiem, dlaczego muszę
kopiować pole name struktury island w przedstawionym wcześniej przykładzie, ale dlaczego nie muszę kopiować wartości pól opens oraz closes?
P: Czym jest odzyskiwanie pamięci? O: Wartościami tych dwóch pól są literały znakowe. A ponieważ literałów O: Niektóre języki programowania łańcuchowych nie można zmieniać, zatem gromadzą informacje o danych zapisywanych na stercie, a następnie, kiedy dane te nie są już potrzebne, zwalniają zajmowaną przez nie pamięć.
P: Dlaczego mechanizm ten nie jest dostępny w języku C?
O: C jest dosyć starym językiem
programowania; w czasach gdy był on opracowywany, większość języków nie dysponowała możliwościami automatycznego odzyskiwania pamięci.
P: Czy muszę zwolnić wszystkie przydzielone dane przed zakończeniem programu?
O: Nie musisz; system operacyjny zwolni całą pamięć w momencie kończenia pracy programu. Niemniej zwalnianie całej pamięci, którą samodzielnie zarezerwowaliśmy, jest dobrą praktyką programistyczną.
nie ma znaczenia, czy kilka struktur będzie używać tego samego literału, czy nie.
P
: Czy funkcja strdup() korzysta z funkcji malloc()?
O: To zależy od implementacji
standardowej biblioteki C, jednak w większości przypadków tak właśnie się dzieje.
CELNE SPOSTRZEŻENIA
292
Q
Dynamiczne struktury danych pozwalają na przechowywanie różnej liczby elementów.
Q
Lista połączona jest strukturą danych pozwalającą na łatwe wstawianie nowych elementów.
Q
Dynamiczne struktury danych są zazwyczaj definiowane w języku C przy wykorzystaniu rekurencyjnych typów struct.
Q
Rekurencyjne typy struct zawierają jeden lub większą liczbę wskaźników do podobnych typów struct.
Rozdział 6.
Q
Stos jest używany do przechowywania zmiennych lokalnych i w całości zarządza nim komputer.
Q
Sterta służy do długotrwałego przechowywania danych. Obszar na stercie rezerwuje się przy użyciu funkcji malloc().
Q
Operator sizeof pozwala określić wielkość obszaru pamięci niezbędnego do zapisania danej typu struct.
Q
Dane będą przechowywane na stercie tak długo, aż zwolnimy je, wywołując funkcję free().
Struktury danych i pamięć dynamiczna :
?
7
JAKĄ STRUKTURĄ DANYCH JESTEM? 7
7
Dowiedziałeś się już, w jaki sposób można utworzyć w języku C listę połączoną. Jednak listy połączone nie są jedynymi strukturami danych, których możemy potrzebować. Poniżej przedstawiliśmy przykłady kilku innych. Przekonajmy się, czy będziesz potrafił dopasować te struktury danych do opisów sposobów ich wykorzystania.
Struktura danych
Opis
Można mnie używać do przechowywania sekwencji elementów, przy czym gwarantuję łatwość dodawania kolejnych. Moje elementy można przetwarzać tylko w jednym kierunku.
Każdy z moich elementów może się łączyć co najwyżej z dwoma innymi. Doskonale nadaję się do przechowywania informacji hierarchicznych.
Można mnie używać do kojarzenia ze sobą dwóch różnych typów danych. Na przykład mógłbyś mnie użyć do skojarzenia imion osób z ich numerami telefonów.
Każdy z moich elementów jest połączony co najwyżej z dwoma innymi. Moje elementy można przetwarzać w dwóch kierunkach.
jesteś tutaj 293
Taka jest moja struktura danych :
?
7
JAKĄ STRUKTURĄ DANYCH JESTEM? 7
7
Rozwiązanie
Dowiedziałeś się już, w jaki sposób można utworzyć w języku C listę połączoną. Jednak listy połączone nie są jedynymi strukturami danych, których możemy potrzebować. Poniżej przedstawiliśmy przykłady kilku innych. Miałeś dopasować te struktury danych do opisów sposobów ich wykorzystania.
Tablica skojarzeniowa lub mapa
Opis Ta struktura danych kojarzy klucze z wartościami.
Lista dwukierunkowa
Można mnie używać do przechowywania sekwencji elementów, przy czym gwarantuję łatwość dodawania kolejnych. Moje elementy można przetwarzać tylko w jednym kierunku.
Każdy z moich elementów może się łączyć co najwyżej z dwoma innymi. Doskonale nadaję się do przechowywania informacji hierarchicznych.
Ta struktura danych przypomina zwyczajną listę połączoną, jednak połączenia pomiędzy poszczególnymi elementami są dwukierunkowe.
Lista połączona
Można mnie używać do kojarzenia ze sobą dwóch różnych typów danych. Na przykład mógłbyś mnie użyć do skojarzenia imion osób z ich numerami telefonów.
Drzewo binarne Każdy z moich elementów jest połączony co najwyżej z dwoma innymi. Moje elementy można przetwarzać w dwóch kierunkach.
Struktury danych są użyteczne, jednak trzeba na nie uważać!
294
Rozdział 6.
Tworząc takie struktury danych w języku C, musimy zachować dużą ostrożność. Jeśli nie będziemy właściwie śledzić naszych danych, to istnieje ryzyko, że stare, niepotrzebne już informacje zostaną na stercie. Może to doprowadzić do stopniowego zajmowania pamięci komputera, a wraz z upływem czasu także do awarii programu spowodowanej błędami obsługi pamięci. Oznacza to, że naprawdę duże znaczenie ma to, byś nauczył się wykrywać i poprawiać pojawiające się w kodzie błędy związane z wyciekami pamięci…
Struktury danych i pamięć dynamiczna
¥FLĂOHWDMQH ! " # $% &
.#/ * )*0 #1 -'#23 '! ( #")*#+,- - #/(#)/.#1
5 6 , 4 4 2+
#8 9(.#: ( . 7 , = 7
5 ; 4? 4 4 ;
> ; . 7 &
5 4 & & & 7 75 &
6
@4 4
4
6 & 5 7& 6 & ;&& 5& AB ? &
& =
, 5 5 ? &C
4 &
&& (.# 5 .
4 ? & ; 4 5
< &
5 =
? &> >
? ;? 64 &
, ; ? * 6
jesteś tutaj 295
Ściśle tajne
Dowód rzeczowy A: kod źródłowy Poniżej został przedstawiony kod źródłowy systemu Suspicious Persons Identification Expert System (SPIES). System ten może być używany do rejestrowania i identyfikacji wybranych osób. Nie musisz analizować go dokładnie już teraz, zachowaj jednak jego kopię, byś mógł z niej skorzystać na dalszych etapach śledztwa. É6 "7 É6 Q"7 É6 "7 typedef struct node { char *question; struct node *no; struct node *yes; } node; int yes_no(char *question) { char answer[3]; $@ § &$¦ | 95:~~ËG } node* create(char *question) { node *n = malloc(sizeof(node)); ¤¦ ~ ¦ ¤~ªRR ¤M ~ªRR return n; } void release(node *n) { if (n) { ¤ ¤ ¤M ¤M ¤¦ ¤¦ free(n); } }
296
Rozdział 6.
Struktury danych i pamięć dynamiczna
int main() { 67¦ 9´5: 67 6945: ' x~6 $ M M` M$ x¤~6 $R* $ x¤M ~6 $º ^M26 $ node *current; do { current = start_node; 7 A M x 6¤¦ { 6¤M 6~6¤M } else { $?Yº¼*®*©º¼F®µ©HY®D$ break; } 6¤ 6~6¤ } else { &'* «$M $ '& $H M§$ 645 node *yes_node = create(suspect); 6¤M ~M x &'* «$$M'& 'x~6 6¤¦ 6¤~x &'* `MM'& $?M?º*©¼@ 6 ©¼?º*©¼@ §$ 66¤¦ ¦ ´5 6¤¦ ~ ¦ break; } } 7 M x $M$ release(start_node); 5 }
jesteś tutaj 297
Ściśle tajne
Przegląd systemu SPIES Program SPIES jest systemem eksperckim, który uczy się identyfikować osoby na podstawie ich cech szczególnych. Im więcej osób zostanie wpisanych do systemu, tym więcej wie i tym mądrzejszy się staje.
Program tworzy drzewo podejrzanych Program zapamiętuje informacje, wykorzystując strukturę danych nazywaną drzewem binarnym. Drzewo binarne pozwala, by każdy jego element był powiązany z dwoma innymi, tak jak pokazaliśmy na poniższym rysunku: e. To jest pierwsze pytani
D Tak, Denis ma wąsy.
Nie, Laura nie ma wą sów.
F *
E6&
Tak wyglądają dane zaraz po uruchomieniu programu. Pierwszy element (nazywany też węzłem) drzewa zawiera pytanie: „Czy podejrzany ma wąsy?”. Element ten jest połączony z dwoma innymi: pierwszym — reprezentującym odpowiedź twierdzącą (yes), oraz drugim — reprezentującym odpowiedź przeczącą (no). Węzły yes oraz no przechowują pseudonimy podejrzanych. Program korzysta z tego drzewa, by zadać użytkownikowi serię pytań w celu dokonania identyfikacji podejrzanego. Jeśli programowi nie uda się odnaleźć pasującej osoby, poprosi użytkownika o podanie jej pseudonimu oraz dodatkowych, szczegółowych informacji, które pomogą w jej identyfikacji. Program zapisze te informacje w drzewie, które stopniowo będzie rosło, odzwierciedlając coraz większą wiedzę systemu.
D
*> 4D
F& (
Program będzie gromadził nowe informacje w drzewie w taki sposób.
E6&
Zobaczmy, jak program prezentuje się w działaniu.
298
Rozdział 6.
D
$ & ;
F *
ych zawsze Pseudonimy podejrzan e drzewa. są umieszczane na dol
Struktury danych i pamięć dynamiczna
Jazda próbna Oto, co się stanie, gdy jakiś agent skompiluje program i go uruchomi: %&/3&%& L"
66 "6¤ ZZ"& M M` M§ & R* § & H M§±µ67 ?M?º*©¼±µ676 ©¼?º*©¼R* § ¬ M M§ & M M` M§ & ¬ M§ & ±µ67§ & ?Yº¼*®*©º¼F®µ©HY® M§ & >
Podczas pierwszego wykonania programowi nie udało się zidentyfikować podejrzanego Heńka Fachury. Jednak po podaniu dodatkowych informacji podczas drugiego wykonania program wiedział już wystarczająco dużo, by pana Fachurę zidentyfikować.
Chytre. Na czym zatem polega problem? Ktoś w laboratorium używał systemu przez kilka godzin i zauważył, że choć program zdawał się pracować prawidłowo, to jednak zużywał dwa razy więcej pamięci, niż powinien. To właśnie dlatego zadzwonili po Ciebie. Gdzieś głęboko w kodzie źródłowym jakiś fragment przydziela pamięć na stercie, lecz nigdy jej nie zwalnia. Teraz możesz już usiąść i przeanalizować cały kod, mając nadzieję, że uda Ci się dostrzec przyczynę problemów. Jednak wyśledzenie wycieków pamięci może być wyjątkowo trudne.
Może zatem warto zrobić sobie wycieczkę do laboratorium oprogramowania…
jesteś tutaj 299
valgrind
Detektywi oprogramowania: stosowanie valgrind Wyśledzenie błędów w dużym, złożonym programie, takim jak system SPIES, może zajmować boleśnie dużo czasu. Dlatego też specjaliści od języka C napisali narzędzie, które może nam w tym pomóc. Jednym z takich narzędzi, działającym w systemie operacyjnym Linux, jest program valgrind. Program ten potrafi monitorować dane zapisywane na stercie. Aby móc działać, valgrind tworzy swoją własną wersję funkcji malloc(). Kiedy nasz program chce zarezerwować jakiś fragment pamięci na stercie, valgrind przechwytuje wywołanie funkcji malloc() oraz free() i zamiast nich wywoła ich odpowiedniki. Wersja funkcji malloc() stosowana przez program valgrind zapamięta informację o tym, jaki fragment kodu wywołał tę funkcję oraz jaki fragment pamięci został mu przydzielony. Kiedy nasz program zakończy działanie, valgrind wyświetli informacje o wszelkich danych, które pozostały na stercie, oraz o tym, w których miejscach kodu została im przydzielona pamięć.
Przygotowanie kodu: dodanie informacji do debugowania Przed skorzystaniem z programu valgrind nie musimy nic robić. Nie musimy nawet ponownie kompilować naszego programu. Aby jednak naprawdę w pełnym stopniu skorzystać z możliwości programu valgrind, musimy się upewnić, że nasz program wykonywalny zawiera informacje do debugowania. Informacje do debugowania to dodatkowe dane dodawane do programu podczas jego kompilacji; między innymi należą do nich numery wierszy pliku źródłowego, w jakim był zapisany dany fragment kodu. Jeśli te informacje będą dostępne, valgrind będzie w stanie udzielać nam znacznie bardziej szczegółowych informacji o źródle wycieków pamięci.
9: valgrind gromadzi informacje o danych, dla których przydzielamy pamięć, lecz jej nie zwalniamy.
valgrind przechwytuje wywołania funkcji malloc() oraz free().
gcc –g spies.c –o spies Opcja -g informuje kompilator o tym, by zapisywał numery wierszy kompilowanego kodu źródłowego.
Aby dodać informacje do debugowania do pliku wykonywalnego, należy skompilować kod z opcją –g:
Takie są fakty: sprawdzaj swój kod Aby się przekonać, jak działa program valgrind, musisz go uruchomić na komputerze z systemem Linux, a następnie użyć do kilkukrotnego sprawdzenia programu SPIES. Za pierwszym razem użyjesz programu valgrind do sprawdzenia jednego z domyślnych podejrzanych: Denisa Łyżeczki. Musisz go uruchomić z poziomu wiersza poleceń, używając przy tym opcji ¤¤¤676~, a następnie określając program, który ma zostać uruchomiony:
Informacje o tym, czy program valgrind jest dostępny w wersji dla danego systemu operacyjnego oraz jak go zainstalować, można znaleźć na stronie http://valgrind.org.
%&/3&%x 6
> valgrind --leak-check=full ./spies ==1754== Copyright (C) 2002-2010, and GNU GPL’d, by Julian Seward et al. F7
F F3E ' ¸ ÇF! 3E ' PODEJRZANY ZIDENTYFIKOWANY F H 3E ' ==1754== All heap blocks were freed -- no leaks are possible
300
Rozdział 6.
Struktury danych i pamięć dynamiczna
Skorzystaj z programu valgrind kilkakrotnie, by zebrać więcej dowodów W poprzednim przypadku w momencie kończenia programu SPIES na jego stercie nie zostały żadne dane. Ale co się stanie, kiedy uruchomisz program drugi raz i spróbujesz nauczyć go rozpoznawania nowego podejrzanego — Heńka Fachury? %&/3&%x 6
> valgrind --leak-check=full ./spies ==1754== Copyright (C) 2002-2010, and GNU GPL’d, by Julian Seward et al. F7
F F3E ' 0 U 3E ' 7
F3Ì ³ !+
7 F
¸U,7 Ì ³ !+ !`,¸U,7 0 U 3 na twarzy Kod zarezerwował bloki F H 3E ' Na stercie pozostało 16 bajtów. pamięci na stercie 11 razy, 55%J[
Wyciek został zatamowany Wprowadziłeś do programu dokładnie te same dane, jednak tym razem program usunął ze sterty wszystkie zapisane na niej informacje. Jak to zrobiłeś? W jaki sposób poradziłeś sobie z tą sprawą? Nie przejmuj się, jeśli tym razem nie udało Ci się wykryć i poprawić błędu. Wycieki pamięci są jednymi z najtrudniejszych błędów występujących w programach pisanych w C. Prawda wygląda tak, że sporo dostępnych i używanych programów napisanych w C prawdopodobnie ma jakieś błędy tego typu; ale to właśnie z tego powodu programy takie jak valgrind są tak ważne.
Ì
-
Ì
1 "
Ì
) + ! $
jesteś tutaj 305
Nie ma głupich pytań Nie istnieją
głupie pytania
P
: valgrind stwierdził, że wyciek występuje w wierszu 46., ale został poprawiony zupełnie inny wiersz kodu. Jak to?
O: Łańcuch znaków „Laura…” został
P
: W jaki sposób valgrind przechwytuje wywołania funkcji malloc() i free()?
O: Obie funkcje, malloc() oraz free(),
zapisany na stercie w wierszu 46., jednak wyciek nastąpił, gdy zmieniła się wartość zmiennej (6¤¦ ), w której był przechowywany wskaźnik na ten łańcuch. Wycieki nie następują w momencie tworzenia danych, tylko wtedy, gdy program traci wszelkie odwołania do nich.
należą do standardowej biblioteki języka C. Jednak valgrind zawiera bibliotekę z własnymi wersjami obu tych funkcji. Kiedy uruchamiamy nasz program za pośrednictwem programu valgrind, to oba te programy będą korzystały z niestandardowych wersji funkcji malloc() oraz free().
P: Czy mogę korzystać z programu
P: Dlaczego kompilator, generując
valgrind na swoim systemie — Mac OS/Windows/FreeBSD?
O: Wszelkie informacje na temat
dostępnych wersji programu valgrind znajdziesz na stronie http://valgrind.org.
P: Skąd pochodzi nazwa valgrind? O: Valgrind to nazwa wejścia do Walhalli. valgrind (program) zapewnia dostęp do sterty programu w pamięci komputera.
kod, nie dodaje do niego informacji do debugowania zawsze, a jedynie wtedy, kiedy tego zażądamy?
O: Ponieważ dołączanie informacji do
debugowania powoduje powiększenie programu, a może także sprawić, że będzie on działał nieco wolniej.
CELNE SPOSTRZEŻENIA Q
Program valgrind sprawdza występowanie wycieków pamięci.
Q
Q
Q
306
Q
Działanie tego programu polega na przechwytywaniu wywołań funkcji malloc() oraz free().
Wykonując program kilka razy, możemy znacznie zawęzić obszar podejrzewany o występowanie przecieku.
Q
Kiedy sprawdzany program kończy działanie, valgrind wyświetla szczegółowe informacje o tym, co pozostało na stercie.
Program valgrind może nam powiedzieć, w jakim wierszu kodu źródłowego dana została umieszczona na stercie.
Q
Program valgrind może nam także powiedzieć, czy problem wycieków pamięci został rozwiązany.
Jeśli skompilujemy nasz program, dodając do niego informacje do debugowania, to valgrind będzie w stanie podać znacznie więcej informacji.
Rozdział 6.
Struktury danych i pamięć dynamiczna
I
Twój niezbędnik C Masz już za sobą rozdział 6., a do swojego niezbędnika dodałeś struktury danych i pamięć dynamiczną. Pełną listę podpowiedzi możesz znaleźć w dodatku B. Dynamiczne struktury danych używają rekurencyjnych typów struct.
Wstawianie nowych danych do listy połączonej jest bardzo łatwe.
a ołączon Lista p ziej d jest bar lna za rozszer a. lic b a t iż n
Lista jest połączona ą n z ic m a n dy strukturą danych.
Rekurencyjne typy struct zawierają jedno bądź więcej odwołań do danych tego samego typu.
Funkcja malloc() przydziela pamięć na stercie.
Funkcja free() zwalnia e z pamięć sterty. Funkcja strdup() tworzy kopię łańcucha znaków na stercie.
Wyciek pamięci to obszar pamięci, do którego nie możemy się już dostać.
W odróżnieniu od stosu, pamięć przydzielona na stercie nie jest automatycznie zwalniana. Stos jest używany do przechowyw ania zmiennych lokalnych.
W odnajdywaniu wycieków pamięci może nam pomóc program valgrind.
jesteś tutaj 307
308
Rozdział 6.
7. Zaawansowane funkcje
Odpicuj swoje funkcje na maksa!
Od kiedy odkryłam funkcje o zmiennej liczbie argumentów, moja funkcja go_on_date() stała się wprost niesamowita.
Proste funkcje są w porządku, jednak czasami możemy potrzebować czegoś więcej. Do tej pory koncentrowaliśmy się na sprawach podstawowych, ale co zrobić, kiedy do osiągnięcia celu będziemy potrzebowali większych możliwości i większej elastyczności? W tym rozdziale zobaczysz, jak podnieść IQ swojego kodu, przekazując funkcje jako parametry. Dowiesz się, jak można sortować, wykorzystując funkcje — komparatory. A na samym końcu nauczysz się, jak dzięki zastosowaniu zmiennej liczby argumentów tworzyć superelastyczne funkcje.
to jest nowy rozdział 309
Prawdziwa miłość
Szukając Pana Doskonałego… We wcześniejszych rozdziałach tej książki używałeś już całkiem sporej liczby funkcji; niemniej w rzeczywistości wciąż istnieje kilka sposobów, by nasze funkcje pisane w języku C były jeszcze potężniejsze. Dzięki znajomości odpowiednich sposobów korzystania z funkcji pisanych w C możemy robić znacznie więcej bez konieczności pisania znacznie bardziej rozbudowanego kodu. Aby się przekonać, jak to działa, przeanalizujmy przykład. Załóżmy, że dysponujesz tablicą łańcuchów znaków, które chcesz przefiltrować, wyświetlając tylko niektóre z nich, a pomijając pozostałe: int NUM_ADS = 7; char *ADS[] = { ”Wilhelm: SBM GSOH lubi sport, TV, dobre jedzenie”, $¬6V¬?Q M$ $¬ VR¬ºQ `2 $ $¬67+º¬ºVQ62/ Q$ $?V¬Q 67M $ $/ V¬Q M$ $º¬Q `2Q $ };
Chciałabym kogoś, kto lubi sport i definitywnie nie lubi Biebera…
Napiszmy zatem program, który przefiltruje tę tablicę, używając do tego celu funkcji operujących na łańcuchach znaków.
310
Rozdział 7.
Zaawansowane funkcje
Magnesiki z kodem 8 * ! ! 9 ; ( Uwaga:#% * , %# $* (
{ int i; $MM $ $¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤$
~5""""""""""""""""" """""""""" """"""""""""""""""""""""" """"""""""""""""""""""""""""""""" """"""""""""""""""""""" $@ D$ºV9: } } $¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤$ }
strcmp strstr
ADS[i]
ADS[i]
"sport"
NUM_ADS
b, to ta różnica będzie wartością większą od zera. Jeśli a < b, to będzie ona wartością mniejszą od zera. I wreszcie jeśli a będzie równe b, to różnica wyniesie 0.
A oto, w jaki sposób możesz posortować tablicę przy użyciu funkcji ¦ : qsort(scores, 7, sizeof(int), compare_scores);
jesteś tutaj 325
Ćwiczenie Długie ćwiczenie Teraz Twoja kolej. Przyjrzyj się przedstawionym poniżej opisom różnych sposobów sortowania. Sprawdź, czy będziesz potrafił dla każdego z nich napisać odpowiednią funkcję komparatora. Aby ułatwić Ci wykonanie ćwiczenia, pierwszą z nich napisaliśmy sami.
Posortuj tablicę liczb całkowitych, zaczynając do najmniejszych wartości.
6x 6 6 ' 6x6 ' 6xQ { ~' ' 6x Q~' ' 6xQ ¤Q }
Posortuj liczby całkowite, zaczynając od największych wartości.
6x 6 x 6 6 ' 6x6 ' 6xQ {
}
M 6
Posortuj ty prostoką i od śc o w kolejn zego najmniejs szego. k do najwię
7 77
To jest typ rectangle reprezentujący prostokąt.
6
6x 6 '6 'Q {
}
326
Rozdział 7.
Zaawansowane funkcje
Ostrzeżenie: to jest naprawdę trudne zadanie.
Posortuj alfabetycznie listę imion, uwzględniając przy tym wielkość liter.
6x 6 '6 'Q {
} Jeśli łańcuch znaków jest wskaźnikiem na znaki, to czym będzie wskaźnik na ten wskaźnik ?
Oto wskazówka: strcmp(“Abc”, “Def”) < 0
I ostatnie zadanie: jeśli już poradziłeś sobie z funkcjami 6x oraz 6x , to w jaki sposób napisałbyś dwie poniższe funkcje komparatorów?
Posortuj prostokąty pod względem pola powierzchni, zaczynając od największego.
Posortuj listę imion w odwrotnej kolejności alfabetycznej, uwzględniając wielkość liter.
6x x 6 6 ''Q {
}
6x x 6 6 '6 'Q {
}
jesteś tutaj 327
Rozwiązanie ćwiczenia Rozwiązanie długiego ćwiczenia Teraz Twoja kolej. Miałeś przyjrzeć się przedstawionym opisom różnych sposobów sortowania i dla każdego z nich napisać odpowiednią funkcję komparatora.
Posortuj tablicę liczb całkowitych, zaczynając do najmniejszych wartości.
6x 6 6 ' 6x6 ' 6xQ { ~' ' 6x
śniej. Tę funkcję napisaliśmy już wcze
Q~' ' 6xQ ¤Q }
Posortuj liczby całkowite, zaczynając od największych wartości.
6x 6 x 6 6 ' 6x6 ' 6xQ { int a = *(int*)score_a; int b = *(int*)score_b; return b – a; Jeśli odwrócisz kolejność, w jakiej liczby są od siebie odejmowane, to zmienisz w ten sposób kolejność sortowania.
}
M 6
Posortuj ty prostoką i od śc o w kolejn zego najmniejs szego. k do najwię
7 77
To jest typ rectangle reprezentujący prostokąt.
6
6x 6 '6 'Q { W pierwszej kolejności skonwertuj wskaźniki na odpowiedni typ.
rectangle* ra = (rectangle*)a; rectangle* rb = (rectangle*)b; int area_a = (ra->width * ra->height);
Następnie oblicz pole powierzchni.
I w końcu użyj sztuczki z odejmowaniem.
328
Rozdział 7.
int area_b = (rb->width * rb->height); return area_a – area_b; }
Zaawansowane funkcje
Posortuj alfabetycznie listę imion, uwzględniając przy tym wielkość liter.
6x 6 '6 'Q {
Łańcuch znaków jest wskaźnikiem na znaki, zatem do funkcji komparatora jest przekazywany wskaźnik na wskaźnik.
char** sa = (char**)a; char** sb = (char**)b; return strcmp(*sa, *sb); }
By dotrzeć do łańcuchów znaków, musimy użyć operatora *.
Oto wskazówka: strcmp(”Abc”, ”Def”) < 0
I ostatnie zadanie: jeśli już poradziłeś sobie z funkcjami 6x oraz 6x , to w jaki sposób napisałbyś dwie poniższe funkcje komparatorów?
Posortuj prostokąty pod względem pola powierzchni, zaczynając od największego.
Posortuj listę imion w odwrotnej kolejności alfabetycznej, uwzględniając wielkość liter.
6x x 6 6 ''Q { return compare_areas(b, a); }
Mógłbyś także użyć wywołania o postaci: -compare_areas(a, b).
6x x 6 6 '6 'Q { return compare_names(b, a); }
Mógłbyś także użyć wywołania o postaci: -compare_names(a, b).
Spokojnie
Nie przejmuj się, jeśli te ćwiczenia przysporzyły Ci pewnych problemów.
W końcu wymagały one od Ciebie umiejętności korzystania ze wskaźników, wskaźników do funkcji, a nawet pewnych operacji matematycznych. Jeśli były one dla Ciebie trudne, to zrób sobie przerwę, napij się trochę wody i za godzinę lub dwie spróbuj rozwiązać je jeszcze raz.
jesteś tutaj 329
Jazda próbna
Jazda próbna Niektóre z przedstawionych wcześniej funkcji komparatorów były naprawdę pokręcone; dobrze zatem byłoby sprawdzić, jak sobie dają radę w praktyce. Poniżej przedstawiliśmy kod, którego potrzebujesz, by skorzystać z tych funkcji.
É6 "7 É6 "7 É6 Q"7
Tutaj umieść funkcje komparatorów.
{ 6 9:~qU||4||4qqUAA|AA4 To jest wiersz, który sortuje tablicę scores.
int i; ¦ 6 6x 6 x 6 $Y 6 QM$ ~5
Ta pętla wyświetli tablicę, gdy już zostanie ona posortowana. To wywołanie sortuje tablicę imion. Pamiętaj: tablica imion to w rzeczywistości tablica wskaźników na znaki, a zatem wielkość każdego z jej elementów wynosi sizeof(char*).
330
Rozdział 7.
$M~@D$ 6 9:
Funkcja qsort() zmienia kolejność elementów tablicy.
} 67' 9:~$HM $$¬$$$$¬$ ¦ U 67'6x $Y $ ~5U $@ D$ 9: } 5 }
Ta instrukcja wyświetla posortowane imiona.
Zaawansowane funkcje
Oto wynik, jaki uzyskasz, kiedy skompilujesz i uruchomisz powyższy kod: Plik Edycja Okno Pomoc Posortowane
> ./test_drive Oto posortowane liczby: Wynik = 554 Wynik = 543 Wynik = 323 Wynik = 112 Wynik = 32 Wynik = 11 Wynik = 3 Oto posortowane imiona: Bartek Krysia Marek Mirka >
Super. Działa! A teraz spróbuj napisać swój własny kod. Funkcje sortujące mogą być niezwykle przydatne, jednak napisanie potrzebnych do ich działania funkcji komparatorów może być dosyć trudne. Na szczęście wraz ze zdobywaniem praktyki będzie to coraz łatwiejsze.
$" %
Nie istnieją
głupie pytania
P: Nie rozumiem funkcji komparatora P: No dobrze, ale kiedy korzystam do sortowania tablicy łańcuchów znaków. Co oznacza zapis char**?
O: Każdy element tablicy jest wskaźnikiem na znaki (char *). Kiedy ¦ wywołuje funkcję komparatora, przekazuje do niej wskaźniki na dwa elementy tablicy. Oznacza to, że do funkcji komparatora są przekazywane dwa wskaźniki na wskaźniki na znaki. W zapisie stosowanym w języku C każdy z tych wskaźników jest deklarowany jako char**.
z funkcji strcmp(), dlaczego używam wywołania w postaci strcmp(*a, *b)? Dlaczego nie strcmp(a, b)?
O: Parametry a i b są typu char**.
A do funkcji 6 należy przekazywać argumenty typu char*.
P: Czy funkcja qsort() tworzy posortowaną wersję tablicy?
O: ¦ nie tworzy kopii tablicy — modyfikuje oryginalną.
P: Dlaczego to wszystko przyprawia mnie o ból głowy?
O: Nie przejmuj się. Stosowanie
wskaźników bywa naprawdę trudne. Jeśli to wszystko nie wydaje Ci się trudne i złożone, to zapewne dlatego, że dostatecznie długo zastanawiałeś się nad wskaźnikami.
jesteś tutaj 331
drogi Janie
Automatyzacja generowania listów do Jana Wyobraźmy sobie, że piszesz program do rozsyłania korespondencji — będzie on wysyłał różne typy wiadomości do różnych osób. Jednym ze sposobów utworzenia danych dla takiego programu byłoby zastosowanie poniższego typu struct: xMDUMP, SECOND_CHANCE, MARRIAGE};
To są trzy typy wiadomości, które będą rozsyłane.
M 6 char *name; enum response_type type; } response;
We wszystkich danych o odpowiedzi będziesz zapisywał jej typ.
Typ wyliczeniowy daje Ci możliwość określenia nazwy każdego z trzech rodzajów odpowiedzi, które będą wysyłane i które możesz zapisać w strukturze każdej odpowiedzi. Struktur typu response będziesz używał, przekazując je w wywołaniach trzech poniższych funkcji przeznaczonych do generowania poszczególnych typów wiadomości: void dump(response r) { $º@ D$" $ M Q /` QM+6 + $ $ +QM 226 6"$ } void second_chance(response r) { $º@ D$" $MQ6 Q /` QM+6 +$ $ "V MQ6"$ } void marriage(response r) { $º@ D$" $MY Q /` QM+6 +QM $ $ M6+2¨ "$ }
A zatem skoro już wiesz, jak wyglądają dane, i dysponujesz funkcjami do generowania odpowiedzi, zobaczmy, jak skomplikowany będzie kod służący do wysyłania odpowiedzi na podstawie tablicy danych.
332
Rozdział 7.
Zaawansowane funkcje
/ " Wyciągnij fragmenty kodu z basenu i umieść je w odpowiednich, pustych miejscach poniższego programu. Twoim zadaniem jest uzupełnienie funkcji , tak by generowała zbiór odpowiedzi na podstawie tablicy danych typu response. Żadnego z fragmentów kodu z basenu nie możesz użyć więcej niż raz. { response r[] = { $¬67$ºª¬?$^ $V¼Yºx±¼ $¬6$V¼Yºx±¼$7$¬©X¼ }; int i; ~5U 67 """"""""""" 6 """""""""""""" """""""""""" break; 6 """""""""""""" 6x676 """"""""""""" break; """""""""""""" } } 5 }
Uwaga: każdy kawałeczek z basenu może zostać użyty tylko raz! r[i].type DUMP
r[i].name
r[i] r[i].name
r[i] dump
r[i].name r[i]
SECOND_CHANCE second_chance
jesteś tutaj 333
Wyciągnięte z basenu
/ " Wyciągnij fragmenty kodu z basenu i umieść je w odpowiednich, pustych miejscach poniższego programu. Twoim celem było uzupełnienie funkcji , tak by generowała zbiór odpowiedzi na podstawie tablicy danych typu response.
{ response r[] = { $¬67$ºª¬?$^ $V¼Yºx±¼ $¬6$V¼Yºx±¼$7$¬©X¼ }; int i; Wszystkie elementy tablicy są przetwarzane w pętli. ~5U r[i].type 67 """"""""""" DUMP 6 """""""""""""" Za każdym razem r[i] """""""""""" sprawdzasz pole type. Dla każdego break; typu wywołujesz SECOND CHANCE odpowiednią funkcję. 6 """"""""""""""""" r[i] 6x676 """"""""""""" break; r[i] """""""""""""" } } 5 }
Uwaga: każdy kawałeczek z basenu może zostać użyty tylko raz!
r[i].name r[i].name dump
334
Rozdział 7.
r[i].name
second_chance
Zaawansowane funkcje
Jazda próbna Kiedy uruchomisz program, wygeneruje on poprawne, bo jakżeby inaczej, odpowiedzi dla poszczególnych osób: %&/3&%8 %,&;
./send_dear_johns Drogi Michale,
F" "FYG
7! Y Y "F H ! !: ¸CÇ F7"G!" "FYG
7!Y C
C
:K F"! : Drogi Macieju, F7"G!" "FYG
7!Y C
C
:K F"! : Drogi Wilhelmie, C FV
" "FYG
7!Y "F H !F! Y : >
Cóż, fajnie, że program działa, jednak jest on całkiem długi, zważywszy, że jedynie wywołuje funkcję dla każdej ze struktur response. Za każdym razem gdy chcesz wywołać funkcję odpowiadającą typowi danej odpowiedzi, program musi wykonać poniższą instrukcję 67: 67 "M case DUMP: break; 6 V¼Yºx±¼ 6x676 break; }
Powiedzieli mi, że programista zapomniał wstawić instrukcji break, a przez to ja skończyłam z tym gościem…
A poza tym co by się stało, gdybyśmy wprowadzili czwarty typ odpowiedzi? Musiałbyś zmienić każdy fragment programu podobny do powyższej instrukcji. Szybko okazałoby się, że musisz zajmować się całkiem rozbudowanym kodem, a wtedy łatwo o błąd. Na szczęście w języku C jest pewna sztuczka, której można użyć w takich sytuacjach, i ma ona związek z tablicami…
jesteś tutaj 335
Tablice wskaźników do funkcji
Stwórz tablicę wskaźników do funkcji Cała sztuczka polega na stworzeniu tablicy wskaźników do funkcji, które będą odpowiadać poszczególnym typom odpowiedzi. Zanim zobaczymy, jak to działa, przekonajmy się, jak można utworzyć tablice wskaźników do funkcji. Gdybyś dysponował zmienną tablicową, w której mógłbyś umieścić wskaźniki do funkcji, to taką tablicę mógłbyś zdefiniować w następujący sposób: 9:~ 6x676
Jednak w języku C takiej składni nie możesz użyć. Musisz dokładnie wskazać kompilatorowi, jak będą wyglądały funkcje, które chcesz przechowywać w tablicy, czyli podać ich typ wartości wynikowej oraz typy parametrów. Oznacza to, że musisz zastosować znacznie bardziej złożoną składnię:
Każda funkcja w tablicy będzie funkcją typu void.
Zmienna będzie nosić nazwę „replies”.
I nie jest to zwykły wskaźnik do funkcji — to cała tablica takich wskaźników.
void (*replies[])(response)~ 6x676 Tylko jeden parametr typu ”response”.
(*
Deklarujesz wskaźnik do funkcji (tablicę).
)(
typy
)
Tu kończysz określanie zmiennej, teraz nadszedł czas na podanie, jak będą wyglądać parametry każdej z funkcji.
Ale w czym nam pomoże tablica? Przyjrzyj się tablicy. Zawiera ona grupę nazw funkcji, które zostały zapisane w dokładnie takiej samej kolejności, w jakiej są podane wartości typu wyliczeniowego: xMDUMP, SECOND_CHANCE, MARRIAGE};
To jest bardzo ważne, gdyż tworząc typ wyliczeniowy, kompilator C nadaje jego poszczególnym symbolom wartości całkowite (zaczyna od 0). A zatem DUMP to 5, V¼Yºx±¼ to A, a ¬©X¼ to 4. A to naprawdę bardzo korzystna okoliczność, gdyż oznacza ona, że możesz pobrać wskaźnik do odpowiedniej funkcji, używając wartości typu wyliczeniowego response_type: To twoja tablica funkcji „replies”.
replies[SECOND_CHANCE] == second_chance
Całe to wyrażenie to wskaźnik do funkcji second_chance.
SECOND_CHANCE ma wartość 1.
Przekonajmy się, czy potrafisz wykorzystać tę tablicę funkcji, by uprościć kod funkcji main().
336
Rozdział 7.
Zaawansowane funkcje
Zaostrz ołówek No dobrze, to będzie naprawdę trudne ćwiczenie. Nie spiesz się, a wszystko powinno się udać. Dysponujesz już wszystkimi informacjami, których potrzebujesz, by uzupełnić kod. W nowej wersji funkcji cała używana wcześniej instrukcja 67 została usunięta i masz ją zastąpić jednym wierszem kodu. Ten wiersz kodu będzie odnajdywał odpowiednią funkcję w tablicy replies i wywoływał ją. { response r[] = { $¬67$ºª¬?$^ $V¼Yºx±¼ $¬6$V¼Yºx±¼$7$¬©X¼ }; int i; ~5U """""""""""""""""""""""""""""""""""""""""""""""""""""""""" } 5 }
jesteś tutaj 337
Funkcja main() zaktualizowana
Zaostrz ołówek. Rozwiązanie
No dobrze, to było naprawdę trudne ćwiczenie. W nowej wersji funkcji cała używana wcześniej instrukcja 67 została usunięta, a Ty miałeś ją zastąpić jednym wierszem kodu. Ten wiersz kodu odnajduje odpowiednią funkcję w tablicy replies i wywołuje ją.
{ response r[] = { $¬67$ºª¬?$^ $V¼Yºx±¼ $¬6$V¼Yºx±¼$7$¬©X¼ }; int i; ~5U (replies[r[i].type])(r[i]); """""""""""""""""""""""""""""""""""""""""""""""""""""""""" Gdybyś chciał, mógłbyś dodać * za nawiasem otwierającym; w obu przypadkach wyrażenie działałoby jednak dokładnie w taki sam sposób.
} 5 }
Rozłóżmy to na czynniki pierwsze.
To całe wyrażenie reprezentuje funkcję, taką jak „dump” lub „marriage”.
(replies[r[i].type])(r[i]); To jest tablica nazw funkcji.
338
Rozdział 7.
To jest wartość, np. 0 dla symbolu DUMP lub 2 dla symbolu MARRIAGE.
Wywołujesz funkcję, przekazując do niej dane odpowiedzi — r[i].
Zaawansowane funkcje
Jazda próbna Jeśli teraz spróbujesz wykonać nową wersję programu, uzyskasz dokładnie takie same wyniki co wcześniej: %&/3&%>6 6
"& xx7 º¬67 M Q /` QM+6 + +QM 226 6" º^ MQ6 Q /` QM+6 + "V MQ6" º¬6 MQ6 Q /` QM+6 + "V MQ6" º7 MY Q /` QM+6 +QM 6 M6+2¨ " >
A na czym polega różnica? Zamiast całej instrukcji 67 mamy następujące wywołanie: (replies[r[i].type])(r[i]);
Jeśli chcesz wywołać funkcję generującą odpowiedź w kilku miejscach programu, to nie musisz kopiować całego rozbudowanego kodu. A jeśli w przyszłości zdecydujesz się dodać nowy typ odpowiedzi i nową funkcję generującą, to wystarczy dodać odpowiednie dane do tablicy:
ę Nowy typ oraz funkcj możesz dodać w ten sposób.
xMºª¬?V¼Yºx±¼¬©X¼LAW_SUIT}; ' 9: ~ 6x676law_suit};
Tablice wskaźników do funkcji mogą znacznie uprościć pielęgnację kodu. Stosuje się je, by zapewnić skalowalność kodu poprzez jego skrócenie i ułatwienie jego rozbudowy. Choć początkowo jest to rozwiązanie stosunkowo trudne do zrozumienia, to jednak tablice wskaźników do funkcji naprawdę mogą podnieść Twoje umiejętności programistyczne.
jesteś tutaj 339
Nie ma głupich pytań
CELNE SPOSTRZEŻENIA Q
Wskaźniki do funkcji przechowują adres funkcji.
Q
Nazwa każdej funkcji jest w rzeczywistości wskaźnikiem.
Q
Jeśli dysponujemy funkcją 7 , to zarówno shoot, jak i Z 7 są wskaźnikami do tej funkcji.
Q
Nowy wskaźnik do funkcji deklaruje się w następujący sposób: M¤M ' ¤ MM¤ /.
Q
Jeśli jest wskaźnikiem do funkcji, to funkcję tę można wywołać, używając wyrażenia M""";
Q
Również użycie wyrażenia ' M""" spowoduje wywołanie funkcji.
Q
Standardowa biblioteka C udostępnia funkcję sortującą o nazwie ¦ .
Q
Funkcja ¦ wymaga podania wskaźnika do funkcji komparatora sprawdzającej równość bądź relację pomiędzy danymi.
Q
Do funkcji komparatora są przekazywane wskaźniki do dwóch elementów sortowanej tablicy.
Q
Jeśli dysponujemy tablicą danych, to korzystając z tablicy wskaźników do funkcji, możemy skojarzyć z tymi danymi funkcje.
Nie istnieją
głupie pytania
P: Dlaczego składnia
deklarowania tablicy wskaźników do funkcji jest tak złożona?
P: To wygląda podobnie
do kodu obiektowego pisanego w innych językach programowania, prawda?
O: Ponieważ podczas deklarowania O: Owszem, całkiem podobnie. wskaźnika do funkcji konieczne jest określenie typu jej wartości wynikowej oraz typów parametrów. To właśnie dlatego używanych jest przy tym tyle nawiasów.
340
Rozdział 7.
Języki obiektowe kojarzą grupę funkcji (nazywanych metodami) z elementami danych. Dokładnie w taki sam sposób, w jaki można użyć wskaźników do funkcji, by skojarzyć funkcję z danymi.
P: Hm, czy to oznacza,
że C jest językiem obiektowym? Rany, ale super.
O: Nie. C nie jest językiem
obiektowym, jednak inne języki, takie jak Objective-C oraz C++, stworzone na bazie C, zapewniają wiele ze swych obiektowych możliwości, korzystając właśnie ze wskaźników do funkcji (choć robią to w sposób niezauważalny dla programistów).
Zaawansowane funkcje
Zapewnij swoim funkcjom elastyyyyyczność Czasami będziemy chcieć, by nasze funkcje zapewniały naprawdę duże możliwości, takie jak przedstawiona wcześniej funkcja , która dzięki wykorzystaniu wskaźników do funkcji pozwalała na filtrowanie łańcuchów znaków. Jednak w innych okolicznościach może nam zależeć na napisaniu funkcji, które będą łatwe w użyciu. W ramach przykładu przyjrzyjmy się funkcji . Dysponuje ona jedną naprawdę rewelacyjną cechą, z której już korzystaliśmy: posiada zmienną liczbę argumentów: $@26/@2Q¨ ` +QQD$A $6 +D$ $@ +` M+D$$/+$
żemy Do funkcji printf() mo ów, ent przekazać tyle argum ile chcemy wyświetlić.
Ale jak TY możesz coś takiego zrobić? Właśnie stanąłeś przed problemem, którego rozwiązanie wymaga takiej możliwości. Okazuje się bowiem, że personel Salonu Rusz Głową ma pewne problemy z rejestracją rachunków za drinki. Jedna osoba z personelu próbowała ułatwić sobie i innym życie i stworzyła typ wyliczeniowy z nazwami wszystkich dostępnych drinków oraz funkcję, która zwraca ich ceny: enum drink { ¬ªºVR©º¼µª**®xO¼R¬YH¼®xXRº*Y¬©¼ }; Q6 { 67 6 ¬ªºVR©º¼ "B 6 µª**®xO¼R q"|A 6 ¬YH¼®xXRº U"´4 6 *Y¬©¼ q"´B } 5 }
No i super, o ile tylko pracownikom Salonu Rusz Głową chodziłoby o określenie ceny pojedynczego drinka. Ale im chodzi o wyliczenie wartości całego zamówienia: Liczba drinków
To proste
6 *Y¬©¼ |*Y¬©¼¬YH¼®xXRºµª**®xO¼R
Pracownicy Salonu Rusz Głową chcieliby dysponować funkcją , do której byłaby przekazywana liczba wszystkich drinków oraz lista ich nazw.
To już nie jest takie proste
Lista zamówionych drinków
jesteś tutaj 341
Zmienna lista argumentów
* # " /$ # Język C udostępnia możliwość tworzenia funkcji o zmiennej liczbie argumentów. Standardowa biblioteka C zawiera kilka makr ułatwiających ich tworzenie. Aby się przekonać, jak one działają, napiszesz funkcję pozwalającą na wyświetlenie grupy liczb całkowitych:
Makro można sobie wyobrazić jako funkcję specjalnego typu, pozwalającą na modyfikowanie kodu źródłowego.
x |BA5A|4 Liczba liczb, jakie należy wyświetlić.
Liczby, która mają zostać wyświetlone.
A oto i kod:
To jest zwyczajny argument, który zawsze będzie przekazywany do funkcji.
Zmienna lista argumentów rozpoczyna się tutaj.
Zmienne argumenty rozpoczynają się za parametrem args.
É6 "7
x """ {
va_start informuje, gdzie rozpoczynają się zmienne argumenty. Ta pętla pobierze wszystkie pozostałe argumenty przekazane do funkcji.
args zawiera wartość całkowitą określającą liczbę przekazanych argumentów.
va_list ap; x int i; ~5 $@D$x } x }
Podzielmy ten kod na fragmenty i przeanalizujmy po kolei każdy z nich.
342
Rozdział 7.
Zaawansowane funkcje
1
@$ $# "
Cały kod potrzebny do obsługi funkcji o zmiennej liczbie argumentów jest dostępny w pliku nagłówkowym stdarg.h.
2
@ ! % + #4
Czy pamiętasz te wszystkie książki, w których bohaterka ciągnie ukochanego przez sypialnię, a rozdział kończy się „"""”? Te trzy kropki to tak zwany wielokropek; informuje ona, że pojawi się jakiś ciąg dalszy. W języku C te trzy kropki umieszczone po argumencie funkcji oznaczają, że w jej wywołaniu mogą się pojawić dodatkowe argumenty.
3
Nie, my też nie czytamy takich książek.
- fL
Zmienna tworzona przy użyciu makra va_list będzie zawierać dodatkowe argumenty przekazane w wywołaniu funkcji.
4
1
Język C musi znać nazwę ostatniego ustalonego argumentu. W przypadku naszej funkcji jest to parametr .
5
3
Teraz, gdy przekazane argumenty są zapisane w zmiennej va_list, możesz je odczytać, korzystając z makra x. Wymaga ono przekazania dwóch wartości: pierwszą jest va_list, a drugą typ kolejnego argumentu. W naszym przypadku wszystkie dodatkowe argumenty są liczbami całkowitymi typu int.
6
*
Po zakończeniu odczytywania argumentów należy poinformować o tym kompilator C. Do tego celu służy makro va_end.
7
$! %
Skoro funkcja jest już gotowa, pozostaje ją wywołać: x |BA5A|4 To wywołanie wyświetli liczby 79, 101 i 32.
jesteś tutaj 343
Nie ma głupich pytań
Porady maniaka
Funkcje kontra makra Makra służą do przepisania kodu źródłowego, zanim zostanie on skompilowany. Makra, których używałeś w tym rozdziale (va_start, x oraz va_end), mogą wyglądać jak funkcje, jednak w rzeczywistości ukrywają tajemne instrukcje informujące preprocesor, jak ma wygenerować całkiem rozbudowany i inteligentny kod, który przed kompilacją zostanie dodany do Twojego programu.
Nie istnieją
głupie pytania
P
: Chwila, dlaczego va_end oraz va_start nazywacie makrami? Czy to nie są zwyczajne funkcje?
O: Nie. Zostały one zaprojektowane
tak, by wyglądać jak zwyczajne funkcje, jednak w rzeczywistości preprocesor zastępuje je innym kodem.
P: A co to takiego ten preprocesor?
O: Preprocesor jest uruchamiany przed etapem kompilacji kodu źródłowego. Między innymi dodaje on do kodu zawartość plików nagłówkowych.
344
Rozdział 7.
P: Czy można stworzyć funkcję zawierającą wyłącznie zmienną listę argumentów, bez żadnych argumentów ustalonych?
O: Nie. Funkcja musi posiadać
przynajmniej jeden argument ustalony, którego nazwa zostanie przekazana do makra va_start.
P: A co się stanie, kiedy
spróbuję odczytać z va_arg więcej argumentów, niż zostało przekazanych do funkcji?
O: Mogą się pojawić losowe błędy.
P: To nie brzmi najlepiej. O: Niewątpliwie nie. P: A co by się stało, gdybym
spróbował odczytać argument typu int jako argument typu double lub jakiegoś innego?
O: Mogłyby się pojawić losowe błędy.
Zaawansowane funkcje
No dobrze, teraz Twoja kolej. Pracownicy Salonu Rusz Głową chcą, byś napisał im funkcję, która będzie zwracać sumaryczną kwotę zamawianej kolejki drinków, według poniższego przykładu:
Ćwiczenie
$M @"4 +D$ |¬YH¼®xXRº¬ªºVR©º¼µª**®xO¼R Ta instrukcja wyświetli tekst: „Cena wynosi: 16.92 zł”.
Korzystając z funkcji 6 przedstawionej kilka stron wcześniej, uzupełnij kod funkcji :
Q """ { Q~5 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" return total; }
jesteś tutaj 345
Kto płaci?
No dobrze, teraz Twoja kolej. Pracownicy Salonu Rusz Głową chcieli, byś napisał im funkcję, która będzie zwracać sumaryczną kwotę zamawianej kolejki drinków, według poniższego przykładu:
Rozwiązanie ćwiczenia
$M @"4 +D$ |¬YH¼®xXRº¬ªºVR©º¼µª**®xO¼R Ta instrukcja wyświetli tekst: ”Cena wynosi: 16.92 zł”.
Korzystając z funkcji 6 przedstawionej kilka stron wcześniej, miałeś uzupełnić kod funkcji :
Q """ { Q~5 Nie przejmuj się, jeśli Twój kod nie będzie wyglądał dokładnie tak jak ten. Można go zapisać na kilka różnych sposobów.
va_list ap; """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" va_start(ap, args); """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" int i; """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" for(i = 0; i < args; i++) { """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" enum drink d = va_arg(ap, enum drink); """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" total = total + price(d); """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" } """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" va_end(ap); """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" return total; }
346
Rozdział 7.
Zaawansowane funkcje
Jazda próbna Jeśli napiszesz prosty kod testowy służący do wywoływania funkcji z poprzedniej strony, to będziesz mógł skompilować program i zobaczyć, co się stanie: To jest kod testowy.
$M @"4 +D$ 4¬YH¼®xXRº¬ªºVR©º¼ $M @"4 +D$ |¬YH¼®xXRº¬ªºVR©º¼µª**®xO¼R $M @"4 +D$ A*Y¬©¼ 5 }
Plik Edycja Okno Pomoc Siemka
A oto i wyniki.
> ./price_drinks F &&:S&Y F &S:\%Y F [:]\Y >
Twój kod działa! Teraz już wiesz, jak tworzyć funkcje o zmiennej liście argumentów, które upraszczają kod i sprawiają, że staje się on bardziej intuicyjny.
Tak, dziecinko! Mógłbym to zapamiętać, nawet gdybym wypił o jednego Monkey Glanda za dużo…
CELNE SPOSTRZEŻENIA Q
Język C pozwala na tworzenie funkcji o zmiennej liczbie argumentów.
Q
Aby stworzyć funkcję o zmiennej liczbie argumentów, trzeba dołączyć plik nagłówkowy stdarg.h.
Q
Zmienne argumenty zostaną zapisane w zmiennej typu va_list.
Q
Dane zapisane w va_list można kontrolować przy użyciu makr: x , x oraz x .
Q
Konieczne jest zadeklarowanie przynajmniej jednego argumentu ustalonego.
Q
Uważaj, by nie odczytać większej liczby parametrów, niż faktycznie zostało przekazanych.
Q
Zawsze musisz znać typ każdego z odczytywanych parametrów.
jesteś tutaj 347
Niezbędnik C
K
Twój niezbędnik C Masz już za sobą rozdział 7., a do swojego niezbędnika dodałeś tworzenie zaawansowanych funkcji. Pełną listę porad i wskazówek możesz znaleźć w dodatku B. iki do Wskaźn o jedyne t funkcji i, które ik n ź wska agają nie wym ia n a w o stos rów * o t a r e p o oraz &…
Wskaźniki do funkcji pozwalają na przekazywanie funkcji, tak jak gdyby były one zwyczajnymi danymi.
ażdej Nazwa k t s je funkcji m do ie ik n ź a k ws . ji c k n tej fu
Funkcja qsort() sortuje tablicę.
Każda funkcja sortująca wymaga przekazania wskaźnika do funkcji komparatora. Funkcja komparat ora określa, w jakie j kolejności mają być umieszczone dw ie dane.
348
Rozdział 7.
…choć, jeśli tylko chcesz, możesz ich używać.
Tablice wskaźników do funkcji mogą nam pomóc w wykonywaniu różnych funkcji w zależności od typu danych.
Język C pozwala na cji tworzenie funk ie zb lic ej nn ie o zm argumentów. Aby tworzyć takie funkcje, należy dołączyć plik stdarg.h.
8. Biblioteki statyczne i dynamiczne
Wymienialny kod Kości śródstopia są statycznie połączone z kośćmi stopy, a te z kolei są statycznie połączone ze stawem skokowym…
Poznałeś już ogromne możliwości bibliotek standardowych. Nadszedł czas, byś użył mocy swojego własnego kodu. W tym rozdziale dowiesz się, jak tworzyć swoje własne biblioteki oraz jak wielokrotnie używać tego samego kodu w różnych programach. Co więcej, nauczysz się współużytkowania kodu w trakcie działania programu, co jest możliwe dzięki bibliotekom łączonym dynamicznie. Poznasz sekrety mistrzów kodowania. A pod koniec tego rozdziału będziesz już potrafił pisać kod, który w łatwy i efektywny sposób będzie można skalować oraz którym będzie można równie łatwo zarządzać.
to jest nowy rozdział 349
Biblioteka bezpieczeństwa
Kod, który możesz zabrać do banku Czy pamiętasz funkcję encrypt(), którą napisałeś już jakiś czas temu i która szyfrowała zawartość łańcucha znaków? Umieściłeś ją w osobnym pliku źródłowym, który można było wykorzystywać w różnych programach: #include “encrypt.h” void encrypt(char *message) { void encrypt(char *message);
while (*message) { *message = *message ^ 31; message++;
encrypt.h
} }
encrypt.c
Ktoś inny napisał już funkcję o nazwie checksum(), która potrafi sprawdzać, czy zawartość łańcucha znaków została zmodyfikowana. Zarówno szyfrowanie, jak i sprawdzanie, czy dane zostały zmodyfikowane, są ważnymi aspektami bezpieczeństwa. Obie te funkcje z osobna są całkiem przydatne, jednak wspólnie mogą się stać podstawą biblioteki bezpieczeństwa.
#include “checksum.h” int checksum(char *message)
Ta funkcja zwraca liczby określane na podstawie zawartości łańcucha znaków.
{ int c = 0;
int checksum(char *message);
while (*message) { c += c ^ (int)(*message); checksum.h
message++; } return c; }
checksum.c
350
Rozdział 8.
Biblioteki statyczne i dynamiczne Biblioteka bezpieczeństwa? Hej, właśnie czegoś takiego szukam! Poziom bezpieczeństwa w naszym banku jest… dość mizerny.
Zaostrz ołówek
Szef do spraw bezpieczeństwa Pierwszego Banku Rusz Głową. Dorabia sobie, czyszcząc baseny.
Pracownik banku napisał program testowy, by przekonać się, jak działają te dwie funkcje. Umieścił wszystkie pliki źródłowe w tym samym katalogu na swoim komputerze i zaczął je kompilować. Skompilował dwa pliki z funkcjami bezpieczeństwa do plików obiektowych, a następnie napisał program testowy:
#include #include #include
Plik Edycja Okno Pomoc
> gcc -c encrypt.c -o encrypt.o > gcc -c checksum.c -o checksum.o >
int main() { 67 9:~ ? M6«$ encrypt(s); printf(“Zakodowano do postaci ’%s’\n”, s); printf(“Suma kontrolna wynosi %u\n”, checksum(s));
Funkcja encrypt() zaszyfruje przekazany łańcuch znaków. Jej drugie wywołanie spowoduje jego odszyfrowanie.
encrypt(s); printf(“Odkodowano do postaci ’%s’\n”, s); printf(“Suma kontrolna wynosi %u\n”, checksum(s)); return 0; }
I właśnie wtedy zaczęły się problemy. Kiedy skompilował program, coś poszło koszmarnie źle… Plik Edycja Okno Pomoc
> gcc test_code.c encrypt.o checksum.o -o test_code test_code.c:2:21: error: encrypt.h: No such file or directory test_code.c:3:22: error: checksum.h: No such file or directory >
Weź ołówek i zakreśl polecenie lub kod, który sprawia, że kompilacja się nie powiodła.
jesteś tutaj 351
Stosuj dla standardowych plików nagłówkowych
Zaostrz ołówek Rozwiązanie
Problem tkwił w programie testowym. Wszystkie pliki źródłowe są umieszczone w tym samym katalogu, jednak program ten próbuje dołączyć pliki nagłówkowe encrypt.h oraz checksum.h, podając ich nazwy w nawiasach kątowych (< >).
#include #include #include int main() { 67 9:~ ? M6«$ encrypt(s); printf(“Zakodowano do postaci ’%s’\n”, s); printf(“Suma kontrolna wynosi %u\n”, checksum(s)); encrypt(s); printf(“Odkodowano do postaci ’%s’\n”, s); printf(“Suma kontrolna wynosi %u\n”, checksum(s)); return 0; }
Nawiasy kątowe dołączają standardowe pliki nagłówkowe Jeśli w dyrektywie #include użyjemy nawiasów kątowych, to kompilator nie będzie poszukiwał plików nagłówkowych w bieżącym katalogu, lecz w standardowych katalogach plików nagłówkowych. Aby nasz testowy program skompilował się i skorzystał z lokalnych plików nagłówkowych, musisz zamienić nawiasy kątowe na znaki cudzysłowu (“ “).
Plik stdio.h jest przechowywany w jednym ze standardowych katalogów plików nagłówkowych.
Teraz program się prawidłowo kompiluje. Szyfruje testowy łańcuch znaków do postaci, której nie da się odczytać.
#include #include “encrypt.h” #include “checksum.h”
Pliki encrypt.h oraz checksum.h są natomiast przechowywane w tym samym katalogu co program.
Plik Edycja Okno Pomoc
> gcc test_code.c encrypt.o checksum.o -o test_code > ./test_code Zakodowano do postaci ’Ophvz{e3?omefu~|vzsj3?v?hzu{ M Suma kontrolna wynosi 4294967253
77
7 !M7F ! 7M Suma kontrolna wynosi 4294967236 > Funkcja checksum() zwraca różne wartości dla różnych łańcuchów znaków.
352
Rozdział 8.
Powtórne wywołanie funkcji encrypt() powoduje przywrócenie oryginalnej postaci łańcucha znaków.
Biblioteki statyczne i dynamiczne
N - ""##$ /$4& A zatem gdzie kompilator będzie poszukiwał plików nagłówkowych, których nazwy zapisujesz w nawiasach kątowych? By się tego dowiedzieć, powinieneś sprawdzić dokumentację używanego kompilatora C, jednak zazwyczaj w systemach uniksowych, takich jak Linux lub komputery Mac, kompilator będzie ich poszukiwał w następujących katalogach: /usr/local/include /usr/include
Katalog /usr/local/include jest często używany do przechowywania plików nagłówkowych dodatkowych bibliotek.
A jeśli używasz wersji gcc dostępnej w MinGW, to kompilator będzie ich szukał w katalogu: C:\MinGW\include
W pierwszej kolejności kompilator sprawdzi katalog /usr/local/include.
Katalog /usr/include zaw zazwyczaj pliki nagłów iera systemu operacyjnego kowe .
A co zrobić, jeśli będziesz chciał współużytkować jakiś kod? Czasami będziesz chciał napisać kod przeznaczony do wykorzystania w wielu programach przechowywanych w różnych katalogach na całym dysku komputera. Co można zrobić w takiej sytuacji?
No pewnie. Mam zamiar dodać kod bezpieczeństwa do tych wszystkich programów. Nie chciałbym, żeby w każdym była używana osobna kopia tego samego kodu.
Istnieją dwie grupy plików, które będą użytkowane we wszystkich programach: pliki nagłówkowe .h oraz pliki obiektowe .o. Zobaczmy, w jaki sposób można współużytkować obie te grupy plików.
jesteś tutaj 353
Współużytkowanie plików nagłówkowych
Współużytkowanie plików nagłówkowych Istnieje kilka sposobów współdzielenia plików nagłówkowych pomiędzy kilkoma projektami:
1
& !
Jeśli skopiujesz swoje pliki nagłówkowe do jednego z katalogów standardowych, takich jak /usr/local/include, to będziesz mógł je dołączać, zapisując ich nazwy w nawiasach kątowych.
#include
2
Nawiasów kątowych możesz używać, jeśli Twoje pliki nagłówkowe są umieszczone w jednym z katalogów standardowych.
. h ! $ 1
Jeśli chcesz przechowywać swoje pliki nagłówkowe w innym miejscu, na przykład w katalogu /my_header_files, to możesz dodać nazwę katalogu do nazwy pliku nagłówkowego podanego w dyrektywie #include:
/
% &&
my_header_files
#include "/my_header_files/encrypt.h" encrypt.h
checksum.h
3
. ! !
Ostatnim rozwiązaniem jest poinformowanie kompilatora, gdzie może znaleźć Twoje pliki nagłówkowe. Można to zrobić, używając opcji -I kompilatora gcc: gcc -I/my_header_files test_code.c ... -o test_code
Opcja -I informuje kompilator gcc o tym, że istnieje jeszcze inne miejsce, w którym może szukać plików nagłówkowych. Kompilator wciąż będzie ich poszukiwał we wszystkich katalogach standardowych, jednak w pierwszej kolejności sprawdzi katalogi podane w opcji -I.
354
Rozdział 8.
pilator, że ma Ta opcja informuje komłówkowych nie tylko nag ów plik ć iwa zuk pos wych, lecz także w katalogach standardo _files. der hea y_ /m gu w katalo
Biblioteki statyczne i dynamiczne
Współużytkowanie plików .o poprzez określanie pełnej ścieżki dostępu Także pliki obiektowe — z rozszerzeniem .o — możesz umieszczać w podobnym wspólnym katalogu. Dzięki temu podczas kompilowania programu, który z nich korzysta, będziesz mógł podać pełną ścieżkę dostępu do nich:
/
% &&
my_object_files
gcc -I/my_header_files test_code.c /my_object_files/encrypt.o /my_object_files/checksum.o -o test_code Podawanie pełnej ścieżki dostępu do plików obiektowych oznacza, że nie będziesz musiał tworzyć ich odrębnych kopii dla poszczególnych projektów.
encrypt.o rolę Katalog /my_object_files pełni w plikó ch Twoi u azyn mag go centralne obiektowych.
checksum.o
Jeśli skompilujesz swój kod, podając pełne ścieżki dostępu do plików obiektowych, których chcesz używać, to wszystkie pisane programy będą mogły korzystać z tych samych plików encrypt.o oraz checksum.o.
Hm… Takie rozwiązanie może być w porządku, jeśli mam jeden lub dwa współużytkowane pliki obiektowe, ale co zrobić, jeśli będę ich miał znacznie więcej? Zastanawiam się, czy jest jakiś sposób, by przekazać kompilatorowi informacje o całej grupie takich plików…
Owszem, jeśli utworzysz archiwum plików obiektowych, to będziesz mógł powiedzieć kompilatorowi o nich wszystkich za jednym zamachem. Archiwum to grupa plików obiektowych zapisanych w jednym pliku. Tworząc pojedynczy plik archiwum zawierający wszystkie pliki bezpieczeństwa, znacznie ułatwisz współużytkowanie ich w wielu różnych projektach.
Zobaczmy, jak to zrobić…
jesteś tutaj 355
Archiwa
Archiwum zawiera pliki .o Czy kiedykolwiek używałeś plików .zip lub .tar? Zatem wiesz, jak łatwo można utworzyć plik zawierający inne pliki. I dokładnie tym samym jest archiwum .a — jest to plik zawierający inne pliki.
libl.a
Otwórz okno terminala lub wiersza poleceń i przejdź do jednego z katalogów library. Są to takie katalogi jak /usr/lib lub C:\MinGW\ lib; zawierają one kod bibliotek. W katalogach tych możesz znaleźć bardzo wiele archiwów .a. Dostępne jest także polecenie nm pozwalające na przeglądanie zawartości tych archiwów:
libmain.o
'
Być może na swoim komputerze nie będziesz miał biblioteki libl.a, jednak polecenie to możes z wypróbować na dowolnym pliku .a.
To jest archiwum o nazwie libl.a. libmain.o
libyywrap.o
%&/3&%% 3 !
> nm libl.a libl.a(libmain.o): 00000000000003a8 s EH_frame0 U _exit 0000000000000000 T _main 00000000000003c0 S _main.eh U _yylex libl.a(libyywrap.o): 0000000000000350 s EH_frame0 0000000000000000 T _yywrap 0000000000000368 S _yywrap.eh >
Polecenie nm generuje listę nazw dostępnych w podanym archiwum. Przedstawione na powyższym przykładzie archiwum libl.a zawiera dwa pliki obiektowe: libmain.o oraz libyywrap.o. To, do czego oba te pliki są używane, nie ma w tym przypadku większego znaczenia; najważniejsze jest to, że całą grupę plików obiektowych można przekształcić w jeden plik archiwalny i używać go podczas korzystania z kompilatora gcc. Zanim jednak zobaczysz, jak kompilować programy, korzystając przy tym z plików .a, zerknij, jak można umieścić pliki encrypt.o oraz checksum.o w archiwum.
356
Rozdział 8.
”T _main” oznacza, że libmain.o zawiera funkcję main().
Biblioteki statyczne i dynamiczne
Utwórz archiwum, używając polecenia ar… Polecenie archiwizowania (ar) pozwala umieszczać pliki obiektowe w archiwach: Przełącznik r oznacza, że jeśli plik .a istnieje, to zostanie on zaktualizowany.
Przełącznik s nakazuje utworzenie indeksu na samym początku pliku .a.
To są pliki obiektowe, które zost aną umieszczone w archiwum.
ar –rcs libhfsecurity.a encrypt.o checksum.o
Przełącznik c oznacza, archiwum zostanie utw że bez żadnych komunikat orzone ów.
To jest nazwa tworzonego pliku .a.
'" % Czy zauważyłeś, że wszystkie pliki .a mają nazwy podobne do lib.a? To standardowy sposób określania nazw archiwów. Nazwy zaczynające się od lib oznaczają, że są to biblioteki statyczne. W dalszej części rozdziału dowiesz się, co to oznacza.
Pamiętaj, by swoim archiwom zawsze nadawać nazwy w postaci lib.a.
Jeśli będziesz stosował inne nazwy, to kompilator może mieć problemy ze znajdowaniem Twoich archiwów.
…a następnie umieść plik .a w katalogu bibliotek Kiedy już utworzysz plik archiwum, powinieneś go umieścić w katalogu bibliotek. A który katalog to ma być? To już zależy od Ciebie, ale do wyboru masz dwie możliwości:
Ì
' 1! # q qq+
Niektórzy programiści, kiedy już wiedzą, że ich archiwa działają prawidłowo, lubią je umieszczać w standardowych katalogach. W przypadku systemu Linux, komputerów Mac lub środowiska Cygwin dobrym wyborem będzie katalog /usr/local/lib, gdyż jest on przeznaczony do przechowywania Twoich własnych niestandardowych bibliotek.
Ì
' 1! 1
Jeśli jeszcze pracujesz nad swoim kodem bądź też jeśli nie odpowiada Ci umieszczanie własnego kodu w katalogu systemowym, to możesz zapisać swoje archiwum w dowolnym innym katalogu, takim jak /my_lib.
W większości systemów umieszczanie plików w katalogu /usr/local/lib wymaga posiadania uprawnień administracyjnych.
jesteś tutaj 357
Kompiluj, używając przełącznika -l
I w końcu kompiluj inne programy Tworzenie archiwum biblioteki miało na celu zapewnienie Ci możliwości używania go podczas tworzenia innych programów. Jeśli zainstalowałeś swoje archiwum w bibliotece standardowej, to będziesz mógł kompilować kod, używając przy tym opcji -l: hfsecurity informuje komputer, że należy poszukać archiwum o nazwie libhfsecurity.a.
Pamiętaj, by nazwy plików podać przed nazwami bibliotek dołączanych przy użyciu opcji -l.
gcc test_code.c –lhfsecurity –o test_code hiwów, Jeśli korzystasz z kilku arc ji -l. opc u par ąc waj uży podaj je,
Czy teraz już rozumiesz, dlaczego tak ważne jest nadawanie swoim archiwom nazw typu lib.a? Nazwa podawana za opcją -l musi pasować do fragmentu nazwy pliku archiwum. A zatem jeśli archiwum nosi nazwę libawesome.a, to program będziesz mógł skompilować, używając opcji -lawesome.
Czy potrzebujesz opcji -I? Wszystko zależy od tego, gdzie umieściłeś swoje pliki nagłówkowe.
A zatem muszę poszukać pliku libhfsecurity.a w katalogu /my_lib.
Ale co zrobić, jeśli archiwa zostaną umieszczone w jakimś innym miejscu, takim jak katalog /my_lib? W takim przypadku należy posłużyć się opcją -L, umożliwiającą określenie katalogu, w którym znajdują się archiwa: gcc test_code.c –L/my_lib –lhfsecurity –o test_code
Dla maniaków Zawartość katalogów bibliotek na różnych komputerach może się bardzo różnić. A dlaczego? Ponieważ różne systemy operacyjne udostępniają różne usługi. Każdy z plików .a jest odrębną biblioteką. Będą istniały biblioteki służące do nawiązywania połączeń z siecią oraz do tworzenia aplikacji z graficznym interfejsem użytkownika. Spróbuj wykonać polecenie nm na kilku różnych plikach .a. Bardzo wiele z wyświetlonych w ten sposób nazw będzie odpowiadać skompilowanym funkcjom, których możesz używać: T oznacza „Text”, a to z kolei oznacza funkcję.
0000000000000000 T _yywrap
Funkcja ta ma nazwę yywrap().
Polecenie nm wyświetli nazwy wszystkich plików obiektowych (.o) oraz wszystkie nazwy dostępne wewnątrz tych plików. Jeśli obok nazwy ujrzysz literkę T, będzie to oznaczać, że dana nazwa jest funkcją dostępną w danym pliku obiektowym.
358
Rozdział 8.
Biblioteki statyczne i dynamiczne
Magnesiki make ) , - $ %$ - ( 6 5 = * encrypt checksum= ! ( ./elliptical
Mac
Waga: 52.18 kg Dystans: 18.18 km Spalono kalorii: 882.22 cal >
W systemie Linux To nie całkiem to samo, co faktycznie dzieje się w systemie Linux. W systemie Linux oraz większości wersji systemu UNIX kompilator zapisuje jedynie nazwę pliku libhfcal.so, bez dołączania pełnej ścieżki dostępu do niego. Oznacza to, że jeśli biblioteka nie będzie umieszczona w jednym ze standardowych katalogów (takich jak /usr/lib), to program nie będzie w stanie jej odszukać. Aby rozwiązać ten problem, system Linux sprawdza także dodatkowe katalogi, które zostały określone w zmiennej środowiskowej LD_LIBRARY_PATH. Jeśli zatem zadbasz o to, by nazwa używanego katalogu z bibliotekami została podana w tej zmiennej środowiskowej, i jeśli ją wyeksportujesz — to program elliptical odnajdzie bibliotekę libhfcal.so. W systemie Linux musisz odpowiednio określić zawartość zmiennej środowiskowej LD_LIBRARY_PATH, by program był w stanie odnaleźć bibliotekę. Nie trzeba tego robić, jeśli biblioteka zostanie umieszczona w jednym ze standardowych katalogów, takich jak /usr/lib.
374
Rozdział 8.
Nie możesz zapomnieć o wyeksportowaniu zmiennej. Plik Edycja Okno Pomoc
JestemLinux
> export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/libs > ./elliptical Waga: 52.18 kg Dystans: 18.18 km Spalono kalorii: 882.22 cal >
Linux
Biblioteki statyczne i dynamiczne
W systemie Windows Przyjrzyjmy się teraz, w jaki sposób możesz uruchomić kod skompilowany przez wersje kompilatora gcc dostępne w MinGW oraz Cygwin. Oba te kompilatory tworzą biblioteki DLL oraz programy wykonywalne dostosowane do systemu Windows. I podobnie jak w systemie Linux, także i w tym przypadku program wykonywalny zawiera wyłącznie nazwę biblioteki — hfcal — bez ścieżki dostępu do katalogu, w którym jest ona umieszczona. Jednak system Windows nie korzysta ze zmiennej środowiskowej LD_LIBRARY_PATH podczas poszukiwania bibliotek. Zamiast tego Windows szuka biblioteki w aktualnym katalogu, a jeśli jej tam nie znajdzie, to przeszuka katalogi podane w zmiennej środowiskowej PATH.
W przypadku korzystania z Cygwin Jeśli program skompilowałeś, używając Cygwin, to będziesz mógł go uruchomić z poziomu interpretera poleceń bash w następujący sposób: Plik Edycja Okno Pomoc
JestemCygwin
> PATH=”$PATH:/libs” > ./elliptical Waga: 52.18 kg Dystans: 18.18 km Spalono kalorii: 882.22 cal >
Cygwin w systemie Windows
W przypadku korzystania z MinGW Jeśli program skompilowałeś, używając kompilatora dostępnego w MinGW, to będziesz mógł go uruchomić z poziomu wiersza poleceń w następujący sposób: Plik Edycja Okno Pomoc
JestemMinGW
C:\code> PATH=”%PATH%;C:\libs” C:\code> ./elliptical Waga: 52.18 kg Dystans: 18.18 km Spalono kalorii: 882.22 cal C:\code>
Czy uważasz, że to wszystko jest dosyć złożone? Owszem, jest, i właśnie dlatego większość programów używających bibliotek dynamicznych przechowuje je w jednym ze standardowych katalogów. Oznacza to, że w systemie Linux oraz na komputerach Mac biblioteki będą się zwykle znajdować w takich katalogach jak /usr/lib lub /usr/local/lib; natomiast w systemie Windows programiści zazwyczaj umieszczają biblioteki DLL w tym samym katalogu, w którym znajduje się plik wykonywalny programu.
MinGW w systemie Windows
jesteś tutaj 375
Ćwiczenie Długie ćwiczenie Ekipa Siłowni Rusz Głową ma zamiar wysłać bieżnie treningowe do USA. Wbudowany serwer działa pod kontrolą systemu Linux i jest już na nim zainstalowana polska wersja kodu. Pracownicy techniczni umieścili bibliotekę w katalogu /usr/local/lib. lib. To jest katalog /usr/local/
/usr/local/lib common-lisp python2.7 python4.2 site_ruby libfluxcap.a libfluxcap.la To właśnie w nim została zainstalowana biblioteka hfcal.
libhfcal.so Znajduje się w nim tak że wiele innych plików.
Na tym samym komputerze, w katalogu /usr/local/include, został także umieszczony plik nagłówkowy biblioteki hfcal.
libmrfusion.so
/usr/local/include python2.7
. To jest katalog usr/local/include
python4.2 fluxcap.h hfcal.h
l.h. To jest plik nagłówkowy hfca
mrfusion.h Znajduje się w nim także wiele innych plików.
'(
Pracownicy techniczni preferują instalowanie bibliotek w tych katalogach, gdyż jest to rozwiązanie najbardziej zbliżone do przyjętych standardów. Komputer jest przygotowany do pracy w polskich warunkach, jednak teraz trzeba to zmienić.
376
Rozdział 8.
Biblioteki statyczne i dynamiczne
System należy zaktualizować, by mógł być używany w siłowni w kraju, do którego jest wysyłany, czyli w USA. Oznacza to, że kod obsługujący prezentację danych na wyświetlaczach bieżni musi zostać zmieniony na wersję z tekstami w języku angielskim oraz używającą innych jednostek — mil i funtów. To jest kod do zastosowania w siłowniach w USA.
#include #include
void display_calories(float weight, float distance, float coeff) { printf(“Weight: %3.2f lbs\n”, weight * 2.2046); printf(“Distance: %3.2f miles\n”, distance / 1.609344);
Ten kod wyświetla informacje w milach i funtach, a komunikaty są w języku angielskim.
printf(“Calories burned: %4.2f cal\n”, coeff * weight * distance); }
hfcal_UK.c
Ten plik jest umieszczony w katalogu /home/ebrown.
Oprogramowanie, które jest już zainstalowane na komputerze, musi używać tej nowej wersji kodu. Ponieważ aplikacja korzysta z tego kodu w formie biblioteki dynamicznej, jedyne, co musisz zrobić, to skompilować go w katalogu /usr/local/lib. Załóżmy też, że przeszedłeś do tego samego katalogu, w którym znajduje się plik hfcal_USA.c, oraz że dysponujesz uprawnieniami niezbędnymi do zapisu plików we wszystkich katalogach. Poniżej zapisz polecenia, które musisz wydać, by skompilować nową wersję biblioteki. .......................................................................................................................... .......................................................................................................................... .......................................................................................................................... Jeśli główna aplikacja do obsługi bieżni treningowych nosi nazwę /opt/apps/treadmill, to jakie polecenie musisz wydać, by ją uruchomić? .......................................................................................................................... .......................................................................................................................... ..........................................................................................................................
jesteś tutaj 377
Ćwiczenie rozwiązane Rozwiązanie długiego ćwiczenia Ekipa Siłowni Rusz Głową ma zamiar wysłać bieżnie treningowe do USA. Wbudowany serwer działa pod kontrolą systemu Linux i jest już na nim zainstalowana polska wersja kodu. Pracownicy techniczni umieścili bibliotekę w katalogu /usr/local/lib. lib. To jest katalog /usr/local/
/usr/local/lib common-lisp python2.7 python4.2 site_ruby libfluxcap.a libfluxcap.la To właśnie w nim została zainstalowana biblioteka hfcal.
libhfcal.so Znajduje się w nim tak że wiele innych plików.
Na tym samym komputerze, w katalogu /usr/local/include, został także umieszczony plik nagłówkowy biblioteki hfcal.
libmrfusion.so
/usr/local/include python2.7
. To jest katalog usr/local/include
python4.2 fluxcap.h hfcal.h
l.h. To jest plik nagłówkowy hfca
mrfusion.h Znajduje się w nim także wiele innych plików.
'(
Pracownicy techniczni preferują instalowanie bibliotek w tych katalogach, gdyż jest to rozwiązanie najbardziej zbliżone do przyjętych standardów. Komputer jest przygotowany do pracy w polskich warunkach, jednak teraz trzeba to zmienić.
378
Rozdział 8.
Biblioteki statyczne i dynamiczne
System należy zaktualizować, by mógł być używany w siłowni w kraju, do którego jest wysyłany, czyli w USA. Oznacza to, że kod obsługujący prezentację danych na wyświetlaczach bieżni musi zostać zmieniony na wersję z tekstami w języku angielskim oraz używającą innych jednostek — mil i funtów.
#include #include
void display_calories(float weight, float distance, float coeff) { printf(“Weight: %3.2f lbs\n”, weight * 2.2046); printf(“Distance: %3.2f miles\n”, distance / 1.609344); printf(“Calories burned: %4.2f cal\n”, coeff * weight * distance); }
hfcal_UK.c
Oprogramowanie, które już jest zainstalowane na komputerze, musi używać tej nowej wersji kodu. Ponieważ aplikacja korzysta z tego kodu w formie biblioteki dynamicznej, jedyne, co musisz zrobić, to skompilować go w katalogu /usr/local/lib. Załóżmy też, że przeszedłeś do tego samego katalogu, w którym znajduje się plik hfcal_USA.c, oraz że dysponujesz uprawnieniami niezbędnymi do zapisu plików we wszystkich katalogach. Poniżej zapisz polecenia, które musisz wydać, by skompilować nową wersję biblioteki. Musisz skompilować kod źródłowy do postaci kodu obiektowego. A następnie przekonwertować plik obiektowy na obiekt współużytkowany.
Nie musisz korzystać z opcji -I, gdyż plik nagłówkowy został umieszczony w standardowym katalogu.
gcc -c -fPIC hfcal_USA.c -o hfcal.o ................................................................................................................ gcc -shared hfcal.o -o /usr/local/lib/libhfcal.so ................................................................................................................
Jeśli główna aplikacja do obsługi bieżni treningowych nosi nazwę /opt/apps/treadmill, to jakie polecenie musisz wydać, by ją uruchomić?
Nie musisz określać wartości zmiennej środowiskowej LD_LIBRARY_PATH, ponieważ biblioteka jest umieszczona w katalogu standardowym.
/opt/apps/treadmill .......................................................................................................................... Czy zwróciłeś uwagę na to, że zarówno plik nagłówkowy, jak i biblioteka zostały zainstalowane w katalogach standardowych? Dzięki temu nie musiałeś korzystać z flagi -I podczas kompilowania programu ani określać wartości zmiennej środowiskowej LD_LIBRARY_PATH podczas jego uruchamiania.
jesteś tutaj 379
Jazda próbna
Jazda próbna Skoro już zaktualizowałeś bibliotekę przeznaczoną dla programu działającego na bieżniach treningowych używanych w siłowniach w USA, wypróbujmy ją na urządzeniu przeznaczonym do użytkowania w Polsce. To jest jedna ze zmodyfikowanych polskich bieżni, korzystająca z oryginalnej wersji biblioteki libhfcal.so: To jest polska wersja bieżni treningowej.
Aplikacja treadmill rozpoczyna działanie w momencie włączenia urządzenia, a zatem po pewnym czasie jej wyświetlacz może przedstawiać następujące wyniki:
Waga: 53.25 kg Dystans: 15.13 km Spalono kalorii: 749.27 cal
Program treadmill na polskiej bieżni treningowej dynamicznie łączy się z wersją biblioteki libhfcal.so skompilowanej na podstawie polskiej wersji programu hfcal.
A co z bieżniami przeznaczonymi do użytku w USA? 380
Rozdział 8.
Biblioteka 4) #7 (T
9 ##
Biblioteki statyczne i dynamiczne
W bieżniach przeznaczonych na rynek amerykański instalowany jest ten sam program treadmill, jednak w ich przypadku ponownie skompilowałeś bibliotekę libhfcal.so, używając przy tym kodu źródłowego umieszczonego w pliku hfcal_USA.c.
To jest bieżnia przeznaczona do użytku w siłowniach w USA.
To jest dokładnie ten sam program treadmill.
9 ##
Ta wersja programu jest łączona z amerykańską wersją biblioteki hfcal.
Biblioteka 4) #7 ;=V
W tym przypadku po przebiegnięciu podobnego dystansu na ekranie urządzenia będą widoczne następujące wyniki: Waga jest wyświetlana w funtach.
Dystans jest . wyświetlany w milach
Weight: 117.54 lbs Distance: 9.40 miles Calories burned: 749.27 cal
świetlane Komunikaty są wyim. lsk gie an u yk jęz w
Spalone kalorie są wciąż wyświetlane jako kalorie.
Udało się. Chociaż program treadmill nigdy nie był ponownie kompilowany, to jednak potrafił dynamicznie użyć kodu z nowej biblioteki. Biblioteki dynamiczne ułatwiają zmienianie kodu w trakcie działania programu. Możemy zatem aktualizować aplikację bez konieczności jej ponownego kompilowania. Gdybyśmy mieli kilka programów, wspólnie używających tego samego fragmentu kodu, to moglibyśmy zaktualizować je wszystkie za jednym zamachem. Teraz, kiedy już wiesz, jak tworzyć biblioteki dynamiczne, Twoje możliwości jako programisty C stały się zdecydowanie większe.
jesteś tutaj 381
Statyczne i dynamiczne
Pogawędki przy kominku Temat dzisiejszej dyskusji: Dwóch doskonale znanych zwolenników programowania modularnego
$
Cóż, jak sądzę, oboje zgodzimy się, że tworzenie kodu z mniejszych fragmentów jest dobrym rozwiązaniem. Oczywiście. To jest naprawdę sensowne rozwiązanie, prawda? Oczywiście. Sprawia, że łatwiej można zarządzać kodem. Tak. Miłe i duże programy. Duże? Tak. Miłe DUŻE programy ze ściśle określonymi zależnościami. To chyba nie jest najlepszy pomysł. Co masz na myśli, stary druhu? Uważam, że programy powinny się składać z wielu małych plików łączonych ze sobą wyłącznie podczas działania programu. Cóż… … to jest bardzo… nie, chyba nie mówisz poważnie? Mówię serio. Co? Dużo niezależnych plików? Łączonych ze sobą niezależnie od nas? Ja to nazywam łączeniem dynamicznym. Ale to przecież… przecież… to prosta droga do chaosu! To oznacza, że później można zmienić zdanie. Wszystko powinno być poprawnie określone od samego początku. Jednak to nie zawsze jest możliwe. Wszystkie duże programy powinny korzystać z łączenia dynamicznego.
382
Rozdział 8.
Biblioteki statyczne i dynamiczne
$
Wszystkie programy? Tak uważam. A co z jądrem systemu Linux? Czy ono jest wystarczająco duże? A przecież jest ono stworzone z użyciem… Łączenia statycznego. Tak, wiem o tym. W tym przypadku jesteś górą. Łączenie statyczne może nie jest takie luźne i swobodne, ale wiesz co? Programy łączone statycznie są przynajmniej łatwe w użyciu. To pojedyncze pliki. Chcesz taki program zainstalować? Wystarczy skopiować plik wykonywalny. Żadnego horroru związanego z zarządzaniem mnóstwem bibliotek DLL. Cóż, mamy nieco odmienne poglądy. Czy mogę jakoś wpłynąć na ciebie, byś zmienił zdanie? Nie. Hm… czyżbyś chciał mi powiedzieć, że twoja opinia w tym względnie jest statyczna?
CELNE SPOSTRZEŻENIA Q
Biblioteki dynamiczne są dołączane do programów podczas ich działania.
Q
Biblioteki dynamiczne są tworzone z jednego lub większej liczby plików obiektowych.
Q
Na niektórych komputerach tworzenie bibliotek dynamicznych wymaga zastosowania opcji -fPIC.
Q
Opcja -fPIC sprawia, że kompilator generuje kod przesuwny.
Q
W wielu systemach opcję –fPIC można pominąć.
Q
Także opcja kompilatora -shared powoduje utworzenie biblioteki dynamicznej.
Q
Biblioteki dynamiczne mają różne nazwy w różnych systemach operacyjnych.
Q
Możemy znacznie ułatwić sobie życie, umieszczając biblioteki dynamiczne w katalogach standardowych.
Q
W przeciwnym razie może się okazać, że konieczne będzie odpowiednie ustawienie zmiennej środowiskowej PATH lub LD_LIBRARY_PATH.
jesteś tutaj 383
Nie ma głupich pytań Nie istnieją
głupie pytania
P: Dlaczego występują tak znaczne różnice pomiędzy bibliotekami dynamicznymi w poszczególnych systemach operacyjnych?
O: Systemy operacyjne lubią
optymalizować wykorzystywane sposoby wczytywania bibliotek dynamicznych, dlatego też każdy z nich ma swoje własne wymagania dotyczące tych bibliotek.
P: Próbowałem zmienić nazwę
biblioteki, modyfikując nazwę jej pliku, ale kompilator nie był już w stanie jej odnaleźć. Dlaczego tak nie można?
O: Kiedy kompilator generuje bibliotekę
dynamiczną, umieszcza jej nazwę wewnątrz jej pliku. Jeśli zmienisz nazwę pliku biblioteki, to nazwa zapisana wewnątrz niego nie będzie pasowała do jego faktycznej nazwy, co zmyli kompilator. Jeśli chcesz zmienić nazwę biblioteki, musisz ją ponownie skompilować.
P: Dlaczego w Cygwin można
stosować tak wiele różnych konwencji nazewnictwa bibliotek dynamicznych?
O: Cygwin umożliwia kompilowanie
oprogramowania pisanego dla systemu UNIX na komputerach działających w systemie Windows. Ponieważ Cygwin tworzy środowisko uniksowe, wykorzystuje wiele konwencji stosowanych w tym systemie operacyjnym. Na przykład preferuje nadawanie bibliotekom rozszerzeń .a, nawet jeśli są to dynamiczne biblioteki DLL.
384
Rozdział 8.
P: Czy dynamiczne biblioteki
tworzone z użyciem Cygwin są prawdziwymi bibliotekami DLL?
O: Tak. Ponieważ jednak zależą one
od systemu Cygwin, umożliwienie zastosowania ich w kodzie, który z Cygwin nie korzysta, będzie wymagało nieco pracy.
P: Dlaczego kompilator MinGW
obsługuje ten sam format nazw bibliotek dynamicznych co Cygwin?
O: Ponieważ oba te projekty są ze sobą
ściśle powiązane i znaczna część ich kodu jest wspólna. Podstawowa różnica polega na tym, że programy tworzone przy użyciu MinGW mogą działać na komputerach, na których Cygwin nie jest zainstalowany.
P: Dlaczego w systemie Linux
w plikach wykonywalnych nie są zapisywane ścieżki dostępu do bibliotek? Dzięki temu nie trzeba by było korzystać ze zmiennej środowiskowej LD_LIBRARY_PATH.
O: To był wybór projektowy twórców
systemu. Rezygnacja z zapisywania ścieżki dostępu zapewnia bowiem znacznie większą kontrolę nad tym, z której wersji biblioteki program będzie korzystał — a to jest bardzo przydatne podczas pisania nowych bibliotek.
P: Dlaczego Cygwin nie korzysta ze zmiennej LD_LIBRARY_PATH podczas poszukiwania bibliotek?
O: Ponieważ musi korzystać z bibliotek
DLL systemu Windows. A te są poszukiwane z wykorzystaniem katalogów podanych w zmiennej środowiskowej PATH.
P: Które rozwiązanie jest lepsze?
Łączenie statyczne czy dynamiczne?
O: To zależy. Łączenie statyczne oznacza niewielkie, szybkie pliki wykonywalne programów, które łatwo można przenosić z komputera na komputer. Z kolei łączenie dynamiczne daje większe możliwości konfigurowania programu podczas jego działania.
P: Jeśli kilka programów korzysta z tej samej biblioteki dynamicznej, to czy jest ona wczytywana więcej niż jeden raz, czy też jest współużytkowana w pamięci?
O: To zależy od systemu operacyjnego.
Niektóre z systemów wczytują odrębne wersje bibliotek dla każdego wykonywanego procesu. Inne wczytują tylko jedną kopię biblioteki i współużytkują ją, by zaoszczędzić pamięć.
P: Czy biblioteki dynamiczne są
najlepszym sposobem konfigurowania aplikacji?
O: Zazwyczaj łatwiejszym rozwiązaniem
jest skorzystanie z plików konfiguracyjnych. Jeśli jednak mamy zamiar korzystać z jakichś urządzeń zewnętrznych, to zazwyczaj będziemy musieli używać przy tym bibliotek dynamicznych, spełniających rolę sterowników.
Biblioteki statyczne i dynamiczne
X
Twój niezbędnik C Masz już za sobą rozdział 8., a do swojego niezbędnika dodałeś biblioteki statyczne i dynamiczne. Kompletną listę porad można znaleźć w dodatku B.
wa Dyrekty e d lu c in w # je plikó poszuku wych o k nagłów ach g w katalo wych, do r a d n a t s k takich ja de. clu /usr/in
Polecenie ar tworzy archiwum biblioteki zawierające pliki obiektowe.
Opcja -I lo listy standa g do rdowych katalogów, w których są poszukiwan e pliki nagłów kowe.
Opcja -l konsoliduje program z plikami znajdującymi się w katalogach standardowych, takich jak /usr/lib. Opcja -L dodaje wskazany katalog do listy standardowych katalogów zawierających biblioteki.
Polecenie gcc -shared konwertuje plik obiektowy na bibliotekę dynamiczną.
Biblioteki dynamiczne są łączone w trakcie działania programu.
Archiwa są k bibliote wane o d li o s n o k ie. n z c y stat
Biblioteki dynamiczne mają rozszerzenia .so, .dylib, .dll lub .dll.a.
Biblioteki e dynamiczn e n ż ró mają różnych w y w z a n h c a m syste ych. operacyjn
Archiwa bibliotek mają nazwy podobne do libcostam.a.
jesteś tutaj 385
386
Rozdział 8.
Nazwisko:
Data:
2. laboratorium C
OpenCV ) %0% % ( % ( % %- ( "& ) %- ( ( &4 "% % * % % % &! 5 " % " " (% ('& Ciebie;
" %- ( &
C Lab
387
C# Lab
387
OpenCV Specyfikacja: zmień komputer w wykrywacz włamań Wyobraź sobie, że komputer mógłby pilnować Twojego domu, kiedy Cię nie ma, i informować Cię, kto się po nim kręcił. Okazuje się, że dzięki wbudowanej kamerze internetowej oraz bibliotece OpenCV jest to możliwe! Oto, co masz zamiar stworzyć.
Wykrywacz włamań Twój komputer będzie bezustannie kontrolował swoje otoczenie, korzystając przy tym z wbudowanej kamery internetowej. Kiedy wykryje jakiś ruch, zapisze na dysku aktualny obraz rejestrowany przez kamerę. A jeśli zapiszesz ten plik na dysku sieciowym bądź skorzystasz z usługi synchronizacji danych, takiej jak Dropbox, to będziesz dysponował rejestrem włamań z natychmiastowymi powiadomieniami.
Włamywacz
Kamera internetowa
O! Włamywacz ulatniający się z zapasami kawy! Muszę to zarejestrować…
Kiedy komputer za pośrednictwe m kamery internetowej wykryje ruch wbudowanej …
388
…zapisze w pliku to, co zobaczył.
OpenCV OpenCV OpenCV jest biblioteką o otwartym kodzie źródłowym, służącą do komputerowego przetwarzania obrazu. Pozwala ona pobierać dane z kamery podłączonej do komputera, przetwarzać je, analizować w czasie rzeczywistym i podejmować decyzje w zależności od tego, co komputer zobaczy. Co więcej, to wszystko można zrobić, pisząc kod w języku C. Biblioteka OpenCV jest dostępna w wersjach przeznaczonych dla systemów Windows, Linux oraz komputerów Mac. Stronę wiki biblioteki OpenCV możesz znaleźć na tej stronie: http://opencv.willowgarage.com/wiki/FullOpenCVWiki
Instalacja OpenCV Bibliotekę OpenCV można zainstalować w systemie Windows, Linux oraz na komputerach Mac. Instrukcję instalacji można znaleźć na stronie o podanym adresie wraz z odnośnikami do strony pozwalającej na pobranie najnowszych stabilnych wersji biblioteki: http://opencv.willowgarage.com/wiki/InstallGuide Po zainstalowaniu OpenCV na komputerze powinien się pojawić katalog o nazwie samples. Warto do niego zajrzeć. Dodatkowo można także znaleźć odnośniki do poradników umieszczonych na stronie wiki projektu OpenCV. W celu wykonania tego laboratorium będziesz musiał trochę poczytać i dokładniej zapoznać się z tą biblioteką. Jeśli chcesz dokładniej poznać bibliotekę OpenCV, polecamy, byś sięgnął po książkę Learning OpenCV, napisaną przez Gary’ego Bradskiego oraz Adriana Kaehlera (wydaną przez wydawnictwo O’Reilly).
Według nas książka Learning OpenCV jest inspirująca.
389
OpenCV Co powinien robić Twój kod Twój kod powinien realizować następujące zadania:
Pobierać dane wejściowe z kamery podłączonej do komputera Musisz operować na danych pobieranych w czasie rzeczywistym, przesyłanych do komputera z kamery. Dlatego pierwszą rzeczą, którą powinieneś się zająć, jest pobieranie tych danych. Biblioteka OpenCV udostępnia funkcję, która pomoże Ci wykonać to zadanie. Ta funkcja to cvCreateCameraCapture(0). Zwraca ona wskaźnik do struktury typu CvCapture. Wskaźnik ten stanowi Twoją „gorącą linię” z kamerą i będziesz go używał do pobierania obrazów. Pamiętaj, by sprawdzać błędy, na wypadek gdyby Twój komputer nie był w stanie odnaleźć podłączonej do niego kamery. Jeśli nawiązanie połączenia z kamerą nie będzie możliwe, wywołanie cvCreateCameraCapture(0) zwróci NULL.
Przechwyć obraz z kamery Ostatni obraz zarejestrowany przez kamerę możesz odczytać, używając funkcji cvQueryFrame(). Wymaga ona przekazania wskaźnika typu CvCapture. Funkcja ta zwraca wskaźnik na ostatni zarejestrowany obraz, zatem Twój kod będzie się zapewne zaczynał podobnie do tego przedstawionego poniżej:
CvCapture* webcam = cvCreateCameraCapture(0); if (!webcam)
To oznacza: „Nie mogłam znaleźć kamery internetowej”.
&'M67 M 6`6Q+`'& while (1) { Odczytujemy obraz z kamery.
Pętla ma być wykonywana w nieskończoność.
IplImage* image = cvQueryFrame(webcam); if (image) { Jeśli uda się odczytać obraz, to tutaj będziesz musiał go jakoś przetworzyć.
} } Jeśli uda Ci się ustalić, że na obrazie jest złodziej, to obraz możesz zapisać w pliku za pomocą poniższego wywołania: Nazwa pliku obrazka
Obraz pobrany z kamery internetowej
cvSaveImage("somefile.jpg", image, 0);
390
Jeśli nie chcesz, by zap obraz był czarno-biały isany , parametr ma mieć wa to ten rtość 0.
OpenCV Jeśli będę się ruszał baaarrrdzo pooowoooli, to mnie nie zauważą…
Wykryj włamywacza Teraz dochodzimy do naprawdę inteligentnej części kodu. W jaki sposób masz określić, czy na zdjęciu widać włamywacza? Jednym ze sposobów jest próba wykrycia ruchu na obrazie. Biblioteka OpenCV udostępnia funkcje pozwalające na tworzenie przepływu optycznego Farnebacka. Przepływ optyczny porównuje dwa obrazy i określa przesunięcia poszczególnych pikseli. Sposób wykonania tego zadania będziesz musiał opracować samemu. Najprawdopodobniej będziesz chciał skorzystać z funkcji cvCalcOpticalFlowFarneback(), by za jej pomocą porównać dwa obrazy pobrane z kamery i utworzyć przepływ optyczny. Następnie będziesz musiał napisać kod, który zmierzy wielkość ruchu pomiędzy obydwoma obrazami. Jeśli wielkość ta okaże się większa od jakiejś wartości progowej, to będziesz wiedział, że przed kamerą porusza się coś dużego.
Zadbaj o puste pole obserwacji Zapewne nie chcesz, by tuż po uruchomieniu programu kamera zarejestrowała, jak wychodzisz z domu. Dlatego warto, by program rozpoczynał analizę obrazu z pewnym opóźnieniem, które pozwoli Ci spokojnie opuścić obserwowane pomieszczenie.
Zadanie opcjonalne: prezentuj aktualny obraz z kamery Podczas testów przeprowadzanych w naszym laboratorium stwierdziliśmy, że bardzo przydatne jest sprawdzanie aktualnego obrazu rejestrowanego przez kamerę. W tym celu otworzyliśmy okno i wyświetliliśmy w nim obraz zarejestrowany przez kamerę. Korzystając z biblioteki OpenCV, w prosty sposób można utworzyć okno — wystarczy użyć poniższego wywołania: 6 *+ $A
Aby wyświetlić aktualny obrazek w oknie, użyj następującego wywołania: 6V7© *+ $
391
OpenCV Końcowy produkt Będziesz wiedział, że Twój program jest gotowy, kiedy komputer będzie w stanie automatycznie robić zdjęcia osób próbujących się koło niego prześlizgnąć.
No to klapa!
Dlaczego na tym poprzestawać? Jesteśmy pewni, że będziesz miał całą masę fascynujących pomysłów na zastosowanie biblioteki OpenCV.
392
processes and system calls
Czas, abyś także Ty był jak ninja C Ostatnia część książki opisuje zaawansowane zagadnienia. Ponieważ masz zamiar zajmować się niektórymi z bardziej zaawansowanych możliwości języka C, musisz się upewnić, że na swoim komputerze będziesz miał do nich dostęp. Jeśli używasz systemu operacyjnego Linux bądź masz komputer Mac, to wszystko w porządku. Jeśli jednak używasz systemu Windows, to musisz zainstalować Cygwin. Kiedy już się z tym uporasz, odwróć stronę i przejdź przez bramę…
jesteś tutaj 393
394
Rozdział 8.
9.(
Przekraczanie granic Dzięki, Tadku. Od kiedy nauczyłeś mnie, jak robić wywołania systemowe, nie oglądam się już za siebie. Tadku? Tadku, jesteś tam?
Czas, by zacząć myśleć kreatywnie. Dowiedziałeś się już, że można tworzyć złożone aplikacje, łącząc niewielkie programy narzędziowe wywoływane z poziomu wiersza poleceń. Ale co zrobić, gdy będziemy chcieli korzystać z innych programów we własnym kodzie? W tym rozdziale dowiesz się, jak korzystać z usług systemowych, by tworzyć i kontrolować działanie procesów. Dzięki temu Twoje programy uzyskają dostęp do poczty elektronicznej, WWW oraz wszelkich innych narzędzi zainstalowanych na komputerze. Po przeczytaniu tego rozdziału zyskasz moc pozwalającą wykraczać poza język C.
to jest nowy rozdział 395
system()
Wywołania systemowe są Twoją gorącą linią z systemem operacyjnym Programy pisane w języku C polegają na systemie operacyjnym podczas wykonywania praktycznie wszystkich operacji. Wykonują wywołania systemowe, kiedy chcą skomunikować się z komponentami sprzętowymi komputera. Wywołania systemowe to wywołania funkcji należących do jądra systemu operacyjnego. Z tych możliwości korzysta przeważająca większość kodu dostępnego w standardowej bibliotece języka C. Za każdym razem gdy wywołujesz funkcję printf(), by wyświetlić coś na ekranie, gdzieś za kulisami zostanie wykonane wywołanie systemowe nakazujące wysłanie łańcucha znaków na ekran komputera.
Oczywiście. Zaraz to wszystko zrobię.
Chcę wyświetlić to w wierszu poleceń, następnie odtworzyć ten utwór, a potem wysłać do sieci ten komunikat…
Przyjrzyjmy się przykładom wywołań systemowych. Zaczniemy od wywołania funkcji, która (całkiem adekwatnie) nosi nazwę system(). Funkcja system() wymaga przekazania jednego parametru będącego łańcuchem znaków i reprezentującego polecenie. Funkcja wykonuje to polecenie w taki sposób, jak gdyby zostało ono wpisane w wierszu poleceń: system(“dir D:”); system(“gedit”);
To polecenie wyświetli zawartość dysku D:. To polecenie, wydane w systemie Linux, spowoduje uruchomienie edytora.
system(“say ’Koniec wiersza’”);
To polecenie, wydane na komputerze Mac, spowoduje odczytanie łańcucha znaków.
Funkcja system() zapewnia prosty sposób wykonywania innych programów z poziomu naszego kodu — zwłaszcza jeśli tworzymy szybki prototyp programu i wolimy szybko wywołać program zewnętrzny, zamiast samemu mozolnie pisać niezbędny kod w C.
396
Rozdział 9.
Procesy i wywołania systemowe
Magnesiki z kodem ' - -$ ( ! #! * system() ( > * # -$ !
( #include #include #include Ta funkcja zwraca łańcuch znaków char* now() zawierający aktualną datę i godzinę. { time_t t; time (&t); return asctime(localtime (&t)); }
&'? M " M67M67 2M"'& int main() { char comment[80]; char cmd[120]; .............. ( .............. , .............. , ..............); ........................... ........................... , ........................... , ........................... , ...................... ); system(cmd); return 0; }
sprintf
comment
"echo '%s %s' >> reports.log" fgets
comment
stdin
80
cmd
printf 120
now()
scanf
stdout
jesteś tutaj 397
Magnesiki poprzesuwane
Magnesiki z kodem. Rozwiązanie ' - -$ ( ! #!
* system() ( . ! # * # -$ !
( #include #include #include char* now() { time_t t; time (&t); return asctime(localtime (&t)); } /* Master Control Program utility. 6 676¤ "'& int main() { char comment[80]; Funkcja fgets służy do pobrania dowolnego łańcucha znaków.
char cmd[120]; fgets
Funkcja sprintf zapisze znaki w łańcuchu znaków.
(
sprintf
Znajduje się w niej miejsce na 80 znaków.
Tekst ma być zapisany w tablicy comment.
comment
80
, (
);
matowany łańcuch znaków
"echo '%s %s' >> reports.log"
,
comment
now()
,
stdin
, Sfor zostanie zapisany w tablicy cmd.
cmd
To jest wzorzec poleceń. To wywołanie powoduje wykonanie polecenia zapisanego w łańcuchu cmd.
,
Dane będą pobierane ze standardowego strumienia wejściowego, czyli z klawiatury.
Polecenie dopisze łańcuch znaków do pliku.
);
system(cmd); return 0; }
W pierwszej kolejności zostanie zapisany tekst w zmiennej comment.
Za nim zostanie zapisany znacznik czasu.
printf 120
398
Rozdział 9.
scanf
stdout
Procesy i wywołania systemowe
Jazda próbna Skompiluj program i przekonaj się, jak działa: To polecenie skompiluje program. A to polecenie uruchomi program. Wykonujemy program po raz drugi.
%&/3&%>HL&
> gcc guard_log.c -o guard_log > ./guard_log 7 F¶C F!
! Y C: > ./guard_log `"CY FY C : >
To jest komentarz.
Kolejny komentarz.
Teraz, jeśli zajrzysz do tego samego katalogu, w którym jest umieszczony program, zauważysz, że został w nim utworzony nowy plik o nazwie reports.log. H MM6 6 +2" To są znaczniki czasu.
To jest plik reports.log utworzony przez program.
Thu Oct 29 11:25:53 2015 ?Q + M+ " Thu Oct 29 11:26:06 2015
Program zadziałał. Odczytuje on komentarze wpisywane w wierszu poleceń i wywołuje systemowe polecenie echo, by dodawać je na końcu pliku.
reports.log
Choć moglibyśmy napisać cały ten program w C, to jednak wywołując funkcję system(), udało się nam go uprościć i uruchomić bardzo małym nakładem pracy. Nie istnieją
głupie pytania
P: Czy funkcja system() zostaje wkompilowana w mój program?
O: Nie. Funkcja system() — podobnie jak wszystkie funkcje
systemowe — nie jest umieszczana w programie. Jej kod wchodzi w skład systemu operacyjnego.
P: A zatem, używając wywołania systemowego, wywołuję jakiś zewnętrzny fragment kodu, podobnie jak w przypadku bibliotek?
O: Coś w tym stylu. Jednak faktyczne szczegóły zależą od
używanego systemu operacyjnego. W niektórych systemach kod wywołań systemowych jest umieszczony w samym jądrze systemu, natomiast w innych — w jakiejś bibliotece dynamicznej.
jesteś tutaj 399
Zgrzyty
Wtem ktoś włamał się do systemu… Stosowanie funkcji system() ma pewną wadę. Korzystanie z niej jest szybkie i proste, lecz jednocześnie trochę niebezpieczne. Zanim dokładniej wyjaśnimy, na czym polegają problemy związane z tą funkcją, zobaczmy, co trzeba zrobić, by włamać się do programu.
UWAGA! UWAGA! Zabezpieczenia głównego systemu zostały przełamane!
Twój kod działa poprzez wygenerowanie łańcucha znaków zawierającego polecenia systemowe. Jego ogólna postać wygląda następująco: echo '
6 6
' >> reports.log
Ale co się stanie, kiedy ktoś użyje polecenia o następującej postaci?
echo '
' && ls / && echo '
6 6
' >> reports.log
Poprzez takie wstrzyknięcie kodu polecenia można sprawić, że program wykona całkowicie dowolny kod: Plik Edycja Okno Pomoc Rany
Użytkownik może użyć programu do wykonania dowolnego polecenia.
> ./guard_log ’ && ls / && echo ’ Applications Developer Library Network Space Paranoids Source >
System Users Volumes bin cores
Czy to duży problem? Jeśli użytkownik może uruchomić program guard_log, to równie łatwo może uruchomić jakiś inny program. Ale co zrobić, w przypadku gdy Twój program jest wywoływany przez serwer WWW? Albo jeśli przetwarza dane z pliku?
400
Rozdział 9.
dev etc home mach_kernel net
private sbin tmp usr var
To jest listing zawartości katalogu głównego.
Procesy i wywołania systemowe
Bezpieczeństwo nie jest jedynym problemem Poprzedni przykład wstrzykiwał kod wyświetlający zawartość katalogu głównego, jednak równie dobrze mógłby usuwać pliki lub uruchomić wirusa. Jednak Twoim zmartwieniem nie jest jedynie bezpieczeństwo.
Ì
1 + ! %^
To mogłoby uszkodzić polecenie.
Ì
1
1 5x % [\ $1 ^
Ì
1 $ 1
" 1 "^
Funkcja system() jest łatwa w użyciu, jednak w przeważającej większości przypadków będziemy potrzebowali rozwiązania o dokładnie określonej strukturze — jakiegoś sposobu wywoływania konkretnego programu z użyciem argumentów wiersza poleceń, a może nawet wymagającego ustawienia jakichś zmiennych środowiskowych.
Dla maniaków Czym jest jądro systemu? W większości systemów operacyjnych wywołania systemowe to funkcje umieszczone w jądrze systemu. Ale czym jest to jądro? Otóż nigdy nie zobaczymy go na ekranie, ale ono zawsze istnieje i zawsze kontroluje pracę komputera. To jądro jest najważniejszym z programów działających na komputerze, odpowiadającym za trzy zagadnienia:
Żaden program nie może zostać wykonany na komputerze, jeśli jądro systemu nie wczyta go do pamięci. Jądro tworzy procesy i dba o to, by otrzymały zasoby niezbędne do działania. Jądro poszukuje także procesów, które są zbyt zachłanne lub uległy awarii. !
Nasze komputery posiadają ograniczone zasoby pamięci, dlatego jądro systemu musi uważnie racjonować ilości pamięci przydzielane poszczególnym procesom. Jądro jest w stanie zwiększać wielkość pamięci wirtualnej poprzez niewidoczne dla użytkownika wczytywanie i zapisywanie bloków pamięci na dysku komputera. 2
Jądro systemu korzysta ze sterowników urządzeń, by komunikować się z urządzeniami podłączonymi do komputera. Nasze programy mogą korzystać z klawiatury, ekranu oraz procesora graficznego, nie wiedząc zbyt wiele na ich temat, ponieważ jądro systemu komunikuje się z nimi w naszym imieniu. Wywołania systemowe to funkcje, z których nasze programy korzystają, by komunikować się z jądrem systemu.
jesteś tutaj 401
exec()
Funkcja exec() zapewnia większą kontrolę Kiedy wywołujesz funkcję system(), system operacyjny musi zinterpretować łańcuch polecenia i określić, które programy należy uruchomić oraz jak należy to zrobić. I właśnie na tym polega problem: system operacyjny musi zinterpretować łańcuch polecenia, a samemu mogłeś się już przekonać, jak łatwo przy tym o jakąś pomyłkę i problemy. A zatem rozwiązanie polega na usunięciu niejednoznaczności i precyzyjnym określeniu, który program chcemy uruchomić. I właśnie do tego celu służy funkcja exec().
Funkcje exec() zastępują aktualny proces Proces jest jedynie programem działającym w pamięci komputera. Jeśli wpiszesz polecenie taskmgr w systemie Windows lub ps –ef w większości innych systemów, to ujrzysz listę procesów aktualnie działających w systemie operacyjnym. System operacyjny rejestruje wszystkie te procesy, nadając każdemu z nich unikalny numer, nazywany identyfikatorem procesu (ang. process identifier, określany w skrócie jako PID). Funkcje exec() zastępują aktualny proces, uruchamiając jakiś inny program. Możesz przy tym określić, które argumenty wiersza poleceń oraz zmienne środowiskowe mają zostać użyte, a kiedy nowy program zostanie uruchomiony, będzie miał dokładnie ten sam PID co program działający wcześniej. Można by to porównać do biegu sztafetowego, w którym Twój program przekazuje swój proces innemu programowi.
W porządku, sendmail, przekazuję ci pałeczkę. To są dane, których potrzebujesz. Nie zawiedź mnie.
402
Rozdział 9.
Proces jest programem działającym w pamięci komputera.
Już się nimi zajmuję.
Procesy i wywołania systemowe
Istnieje wiele funkcji exec() Wraz z upływem czasu programiści stworzyli kilka wersji funkcji exec(). Każda z nich ma nieco inną nazwę i unikalny zestaw parametrów. Choć wersji jest wiele, to w rzeczywistości istnieją dwie grupy funkcji exec(): funkcje z listą argumentów oraz funkcje z tablicą argumentów.
Funkcje exec() są
Funkcje z listą argumentów: execl(), execlp(), execle()
nagłówkowym unistd.h.
dostępne w pliku
Funkcje należące do tej grupy pobierają argumenty wiersza poleceń w formie listy parametrów. Oto jej postać:
Ì
To może być pełna ścieżka dostępu do programu (w funkcjach execl() oraz execle()) bądź jedynie nazwa polecenia, którego należy poszukać (w przypadku funkcji execlp()). Niemniej pierwszy parametr informuje funkcję, jaki program ma uruchomić.
Ì
5 *
Musisz podać kolejno wszystkie argumenty wiersza poleceń, których chcesz użyć. Pamiętaj: zawsze pierwszym argumentem jest nazwa programu. Oznacza to, że pierwsze dwa parametry przekazywane do tej grupy funkcji exec() zawsze muszą być tym samym łańcuchem znaków.
Ì
3-//
Właśnie tak. Po ostatnim argumencie wiersza wywołania musisz umieścić NULL. W ten sposób informujesz funkcję, że nie ma już więcej argumentów.
Ì
'" %
Odstępy na liście argumentów mogą zmylić MinGW.
Jeśli przekażesz dwa argumenty „on jest” oraz „żółwiem”, to programy uruchamiane w MinGW mogą rozpoznać trzy argumenty: „on”, „jest” oraz „żółwiem”.
&
1 [ \
Jeśli wywołujesz funkcję exec(), której nazwa kończy się na …e(), to możesz do niej przekazać także tablicę zmiennych środowiskowych. Jest to tablica łańcuchów znaków, takich jak: “MOC=4”, “SZYBKOSC=4”, “PORT=OPEN” itd. To są argumenty.
execL = LISTA argumentów.
execl("/home/flynn/clu", "/home/flynn/clu", "paranoids", "contract", NULL) Drugi parametr powinien być taki sam jak pierwszy.
execLP = LISTA argumentów H. oraz przeszukanie zmiennej PAT
To są argumenty.
Musisz zakończyć listę wartością NULL.
execlp("clu", "clu", "paranoids", "contract", NULL) To są argumenty.
execle("/home/flynn/clu", "/home/flynn/clu", "paranoids", "contract", NULL, env_vars) execLE = LISTA argumentów oraz zmienne ŚRODOWISKOWE.
env_vars to tablica łańcuchów znaków zawierających zmienne środowiskowe.
jesteś tutaj 403
Funkcje z tablicą argumentów
Funkcje z tablicą argumentów: execv(), execvp() oraz execve() Jeśli dysponujesz argumentami wiersza poleceń zapisanymi w tablicy, to być może uznasz, że łatwiejsze będzie skorzystanie z następujących funkcji: execV = tablica lub WEKTOR argumentów. execVP = tablica lub WEKTOR argumentów oraz przeszukanie zmiennej PATH.
execv(“/home/flynn/clu”, my_args); execvp(“clu”, my_args);
Argumenty muszą być zapisane w tablicy łańcuchów znaków — . w tym przypadku jest to my_args
Jedyną różnicą pomiędzy dwoma powyższymi funkcjami jest to, że execvp będzie poszukiwać programu w katalogach podanych w zmiennej środowiskowej PATH.
Jak zapamiętać funkcje exec() Możesz stwierdzić, której z funkcji exec() powinieneś użyć, określając jej nazwę. Do nazwy każdej z tych funkcji można dodać jedną lub dwie litery, którymi mogą być: l, v, p lub e. Litery te określają, których z możliwości funkcji chcesz używać. W ramach przykładu przeanalizujmy funkcję execle():
execle = exec + l + e = LISTA argumentów + zmienne ŚRODOWISKOWE Znak
Korzysta z Litery l oraz v zawsze są zapisywane przed literami p oraz e, a te dwie ostatnie występują opcjonalnie.
Użyj listy argumentów.
Listy argumentów
l
Tablicy (wektora) argumentów
v
p
e
Zastosuj zmienne środowiskowe.
l Wszystkie funkcje exec() zaczynają się od exec.
exec
Użyj tablicy lub wektora argumentów.
v
404
Rozdział 9.
W poszukiwaniu programu przejrzyj katalogi podane w zmiennej środowiskowej PATH.
p
Nie musisz dodawać ani litery p, ani e.
e
Procesy i wywołania systemowe
Przekazywanie zmiennych środowiskowych Każdy proces dysponuje grupą zmiennych środowiskowych. Są to te zmienne, które możesz wyświetlić, wpisując w wierszu poleceń set lub env, i które przekazują procesom użyteczne informacje, takie jak położenie katalogu domowego lub miejsce, w którym należy szukać poleceń. Programy pisane w C mogą odczytywać zmienne środowiskowe, korzystając z wywołania systemowego getenv(). Przykład zastosowania tej funkcji możesz zobaczyć w programie diner_info przedstawionym z prawej strony. Jeśli chcesz uruchomić program, korzystając przy tym z argumentów wiersza poleceń oraz ze zmiennych środowiskowych, to możesz to zrobić w następujący sposób:
Zbiór zmiennych środowiskowych możesz przygotować jako tablicę wskaźników na łańcuchy znaków.
Każda zmienna w tym zbiorze ma postać: nazwa=wartość.
#include #include int main(int argc, char *argv[]) { ? +@ D$9A: V@ D$getenv(“SOK”)); return 0; } Funkcja getenv() dostępna w stdlib.h pozwala odczytywać zmienne środowiskowe.
diner_info.c
Ostatnim elementem tablicy musi być NULL.
67'Mx9:~VYH~Q+¤MªRR 86 xxUªRRMx
Funkcja execle pozwala przekazać listę argumentów oraz tablicę zmiennych środowiskowych.
Zmienna my_env zawiera zmienne środowiskowe.
Funkcja execle() określi argumenty wiersza wywołania oraz zmienne środowiskowe, a następnie zastąpi aktualny proces programem diner_info. %&/3&%! 36
> ./my_exec_program YP K "YD F >
A co, jeśli pojawi się problem? Jeśli podczas wywoływania nowego programu pojawią się jakieś problemy, to będzie kontynuowane wykonywanie dotychczasowego procesu. Takie rozwiązanie jest przydatne, gdyż oznacza ono, że jeśli nie uda się nam uruchomić drugiego procesu, to będziemy mieli możliwość rozwiązania problemu i poinformowania użytkownika o tym, co poszło źle.
Jeśli przekazujesz zmienne środowiskowe w systemie Cygwin, to koniecznie przekaż także zmienną PATH.
'" %
Cygwin wymaga, by podczas uruchamiania programu była określona wartość zmiennej środowiskowej PATH. Jeśli zatem używasz Cygwin, pamiętaj o dodaniu do zmiennych środowiskowych łańcucha takiego jak: PATH=/usr/bin.
jesteś tutaj 405
errno
Większość wywołań systemowych zawodzi w taki sam sposób Ponieważ wywołania systemowe zależą od czegoś, co znajduje się poza naszym programem, mogą zawieść w sposób, którego nie jesteśmy w stanie kontrolować. Aby rozwiązać ten problem, większość wywołań systemowych zawodzi w taki sam sposób.
Gwarantowany Standard Niepowodzenia
W ramach przykładu przyjrzyjmy się wywołaniu funkcji execle(). Bardzo łatwo możemy zauważyć, kiedy jej wywołanie się nie powiodło. Jeśli się ono uda, aktualny program przestaje działać. A zatem jeśli po wywołaniu funkcji exec() program wykona jakikolwiek kod, to będzie to oznaczało, że pojawił się jakiś problem: Jeśli wywołanie funkcji execle() się powiedzie, to ten wiersz kodu nigdy nie zostanie wykonany.
86 x$ x$ U$ªRRMx VM x 6 `$
Jednak sama informacja, czy wywołanie się powiodło, nie wystarcza. Zazwyczaj będziemy chcieli wiedzieć, dlaczego wywołanie się nie udało. Właśnie z tego powodu większość wywołań systemowych stosuje się do złotej zasady niepowodzenia. Zmienna errno jest zmienną globalną zdefiniowaną w pliku nagłówkowym errno.h wraz z grupą standardowych wartości błędów, takich jak:
Ta wartość nie jest dostępna we wszystkich systemach.
Złota zasada niepowodzenia
*
Posprzątaj po sobie najlepiej, jak potrafisz.
*
Zapisz wartość błędu w zmiennej errno.
*
Zwróć –1.
EPERM=1
Operation not permitted (niedozwolona operacja)
ENOENT=2
No such file or directory (brak takiego pliku lub katalogu)
ESRCH=3
No such process (nie ma takiego procesu)
&'*++&,-./ 0!4"5#
Można by sprawdzać zmienną errno i porównywać ją z każdą z tych wartości bądź pobrać standardowy komunikat o błędzie, używając do tego funkcji strerror() dostępnej w pliku string.h: puts(strerror(errno));
Funkcja strerror() zamienia numer błędu na komunikat tekstowy.
A zatem jeśli system nie jest w stanie odnaleźć programu, który chcemy uruchomić, i przypisze zmiennej errno wartość ENOENT, to powyższe wywołanie wyświetli następujący komunikat: No such file or directory
406
Rozdział 9.
Procesy i wywołania systemowe
Ćwiczenie
W różnych systemach używane są różne polecenia zwracające informacje o konfiguracji sieci. W systemie Linux i na komputerach Mac służy do tego program /sbin/ifconfig, natomiast w systemie Windows dostępne jest polecenie ipconfig przechowywane gdzieś na ścieżce poleceń. Poniższy program próbuje uruchomić program /sbin/ifconfig, a jeśli mu się to nie uda, próbuje wykonać polecenie ipconfig. Żadne z wykonywanych poleceń nie wymaga przekazywania jakichkolwiek argumentów. Zastanów się dobrze. Której wersji funkcji exec() użyjesz?
kowych potrzebujesz? Których plików nagłów
#include ................................... ................................... ...................................
int main() {
To wywołanie ma wykonywać polecenie /sbin/ifconfig. Co powinniśmy sprawdzać?
To wywołanie ma wykonywać polecenie ipconfig i sprawdzać, czy to się udało.
if (................................................) if (execlp(..........................................) { 676@ $""""""""""""""" return 1; } return 0;
Jak sądzisz, co należy wpisać w tym miejscu?
}
jesteś tutaj 407
Ćwiczenie rozwiązane
Rozwiązanie ćwiczenia
W różnych systemach używane są różne polecenia zwracające informacje o konfiguracji sieci. W systemie Linux i na komputerach Mac służy do tego program /sbin/ifconfig, natomiast w systemie Windows dostępne jest polecenie ipconfig przechowywane gdzieś na ścieżce poleceń. Poniższy program próbuje uruchomić program /sbin/ifconfig, a jeśli mu się to nie uda, próbuje wykonać polecenie ipconfig. Żadne z wykonywanych poleceń nie wymaga przekazywania jakichkolwiek argumentów. Zastanów się dobrze. Której wersji funkcji exec() użyjesz?
#include
ystać Potrzebujesz tego pliku, by korz (). exec z funkcji
#include
................................... #include
................................... #include ...................................
int main() {
Potrzebujesz tego pliku, by korzystać ze zmiennej errno. Ten plik pozwoli Ci wyświetlać komunikaty o błędach za pomocą funkcji strerror().
Użyj funkcji execl(), gdyż dysponujesz ścieżką do pliku programu.
Jeśli wywołanie funkcji execl() zwróci –1, oznacza to, że wystąpił błąd; wygląda zatem na to, że powinniśmy poszukać programu ipconfig.
execl("/sbin/ifconfig", "/sbin/ifconfig", NULL) == -1 if (................................................) Funkcja execlp() "ipconfig", "ipconfig", NULL) == -1 if (execlp(..........................................) { pozwoli nam poszukać strerror(errno) 676@ $""""""""""""""" programu ipconfig return 1; Sprawdzamy, czy wywołanie w katalogach Wywołanie funkcji zwróciło wartość –1, na wypadek określonych strerror() wyświetli } gdyby się ono nie powiodło. w zmiennej ewentualne błędy. środowiskowej return 0; PATH.
}
408
Rozdział 9.
Procesy i wywołania systemowe Nie istnieją
głupie pytania
P: Czy funkcja system() nie jest łatwiejsza do użycia niż P: Czy zawsze muszę sprawdzać wartość wynikową funkcje exec()?
zwracaną przez wywołania systemowe?
O: Owszem, jest. Jednak ze względu na konieczność interpretacji
O: Jeśli będziesz korzystał z wywołań systemowych i pominiesz
przekazanego łańcucha znaków korzystanie z funkcji system() jest trochę problematyczne, zwłaszcza jeśli używane w niej łańcuchy poleceń są tworzone dynamicznie.
sprawdzanie wartości wynikowych, to kod programu będzie krótszy. Jednak najprawdopodobniej jednocześnie będzie w nim więcej błędów. O błędach najlepiej jest myśleć od razu podczas pisania kodu. Dzięki temu późniejsze poszukiwanie i obsługiwanie błędów jest znacznie łatwiejsze.
P: Dlaczego istnieje tak dużo funkcji exec()? O: Wraz z upływem lat programiści chcieli uruchamiać procesy na P: Czy po wywołaniu funkcji exec() mogę wykonywać różne sposoby. Różne wersje funkcji exec() zostały stworzone, by zapewnić większą elastyczność.
w programie jakiekolwiek inne operacje?
O: Nie. Jeśli wywołanie funkcji exec() się powiedzie, to nastąpi zmiana procesu — zamiast dotychczasowego programu zostanie wykonany nowy. Oznacza to, że program wywołujący funkcję exec() przestanie działać w momencie jej wykonania.
CELNE SPOSTRZEŻENIA Q
Także wywołania systemowe exec() pozwalają wykonywać programy, a jednocześnie dają większą kontrolę nad sposobem ich wywoływania.
Wykonując wywołanie systemowe, uruchamiasz kod spoza swojego programu.
Q
Istnieje kilka różnych wersji wywołania systemowego exec().
Q
Funkcja system() to wywołanie systemowe służące do wykonania polecenia podanego w formie łańcucha znaków.
Q
Kiedy pojawia się problem, wywołania systemowe zazwyczaj, choć nie zawsze, zwracają wartość ¤A.
Q
Funkcja system() jest łatwa w użyciu, jednak może być przyczyną występowania błędów.
Dodatkowo zapisują numer błędu w zmiennej errno.
Q
Q
Wywołania systemowe to funkcje udostępniane przez system operacyjny.
Q
jesteś tutaj 409
Pomieszane komunikaty
Chłopaki z kawiarni Gwiezdny Pył napisali nowy program do generowania zamówień, który nazwali coffee: #include
(
#include int main(int argc, char *argv[]) { char *w = getenv(“EXTRA”); if (!w) w = getenv(“FOOD”); if (!w) ~96¤A: char *c = getenv(“EXTRA”); if (!c) 6~96¤A: @ @ D$6 return 0; }
Aby go wypróbować, napisali także program testowy. Czy potrafiłbyś dopasować fragmenty kodu do generowanych przez nie wyników?
#include #include #include
Tutaj umieść odpowiednie fragmenty kodu.
int main(int argc, char *argv[]){
$ M /@ D$ return 1; } return 0; }
410
Rozdział 9.
Procesy i wywołania systemowe
Y ) / [
Dopasuj fragment kodu do generowanego przez niego wyniku.
J#[
char *my_env[] = {“FOOD=kawa”, NULL}; T!:E!#:E!#!#`00 F= 1'55D&'L 7#` C!+ H! Aby nawiązać połączenie z serwerem, będziesz potrzebował programu telnet. W większości systemów operacyjnych jest on instalowany domyślnie. Można to łatwo sprawdzić, wydając w wierszu poleceń komendę:
Zrób to sam!
Jeśli nie dysponujesz tym programem, możesz go zainstalować na kilka sposobów, zależnie od używanego systemu:
466
Rozdział 11.
Cygwin: Uruchom program setup.exe środowiska i odszukaj w nim hasło telnet. Linux: Poszukaj hasła telnet w menedżerze pakietów. W wielu wersjach Linuksa menedżer pakietów nosi nazwę Synaptic. Mac: Jeśli nie dysponujesz programem telnet, to możesz go pobrać i zainstalować ze strony http://www.macports.org lub http://www.finkproject.org.
Gniazda i komunikacja sieciowa
Prezentacja serwera puk-puk Twój serwer będzie w stanie komunikować się z kilkoma klientami jednocześnie. Zarówno klienty, jak i serwer będą prowadziły konwersację o ściśle określonej strukturze nazywanej protokołem. W internecie jest stosowanych wiele różnych protokołów. Niektóre z nich są protokołami niskiego poziomu, określającymi, w jaki sposób mają być przesyłane siecią zera i jedynki. Przykładem takich protokołów jest protokół internetowy (ang. internet protocol, w skrócie IP). Istnieją także protokoły wysokiego poziomu; ich przykładem może być protokół przesyłu hipertekstu (ang. hypertext transfer protocol, w skrócie HTTP) określający, w jaki sposób przeglądarki komunikują się z serwerami WWW. Nasz serwer dowcipów będzie korzystał z protokołu wysokiego poziomu, nazywanego Internetowym Protokołem Puk-Puk (w skrócie IPPP).
Protokół jest konwersacją o określonej strukturze.
Serwer Klient oraz serwer prowadzą konwersację o ściśle określonej strukturze, nazywanej protokołem.
Klient telnet
Serwer będzie rozmawiał z kilkoma klientami jednocześnie.
Klient telnet Klient telnet
Klient oraz serwer będą wymieniały komunikaty o następującej postaci:
Serwer:
Klient:
Puk! Puk!
&
Protokół wymaga, byś odpowiedział pytaniem: „Kto tam?”. Dlatego też natychmiast przerywam tę konwersację.
Oskar.
9 & 5(
Protokół zawsze określa zbiór reguł. Jeśli tylko zarówno klient, jak i serwer będą się do nich stosować, to wszystko będzie w porządku. Jeśli jednak któreś z nich złamie reguły, to konwersacja zapewne zakończy się bardzo szybko i gwałtownie.
jesteś tutaj 467
Bla, bla
PNAR — jak serwery komunikują się z internetem Kiedy programy pisane w języku C muszą się komunikować ze światem zewnętrznym, używają strumieni danych, by odczytywać i zapisywać bajty. Korzystałeś już ze strumieni danych podłączonych do plików oraz ze standardowych strumieni wejściowych i wyjściowych. Jeśli jednak masz zamiar napisać program komunikujący się z siecią, będziesz potrzebował nowego rodzaju strumienia nazywanego gniazdem. listener_d jest deskryptorem gniazda.
6 !79FE!:+;
Będziesz potrzebował tego pliku nagłówkowego.
... =75!³=`,K
=K, ”) != -1) { read_in(connect_d, buf, sizeof(buf)); Odczyt danych przesłanych przez klienta if (strncasecmp(“Kto tam?”, buf, 12)) say(connect_d, “Powinieneś zapytać: ’Kto tam?’!”); Sprawdzenie odpowiedzi else { podanej przez użytkownika. if (say(connect_d, “Oskar\r\n> ”) != –1) { read_in(connect_d, buf, sizeof(buf)); if (strncasecmp(“Jaki Oskar?”, buf, 10)) say(connect_d, “Powinieneś zapytać: ’Jaki Oskar?’!\r\n”); else say(connect_d, “Oskar, głupie pytanie, na które dostaniesz głupią odpowiedź.\r\n”); } } } Zamknięcie pomocniczego gniazdatem. close(connect_d); używanego do komunikacji z klien } return 0; }
jesteś tutaj 481
Jazda próbna
Jazda próbna Jeśli już napisałeś kod serwera, nadszedł czas, byś go skompilował i uruchomił. Plik Edycja Okno Pomoc JestemSerwerem
Konsola serwera
;C!!=1:!D=1 > ./ikkp_server Y!
Serwer czeka na połączenie, a zatem otwórz drugie okno konsoli i połącz się z serwerem przy użyciu programu telnet: Konsola klienta
Serwer może zrobić Ci kawał, ale co się stanie, kiedy złamiesz protokół i prześlesz do niego nieprawidłową odpowiedź? Konsola klienta
Plik Edycja Okno Pomoc JestemKlientem
; &%J:color, 2.0f); al_draw_line(-10, -20, 5, -20, a->color, 2.0f); al_draw_line(5, -20, 20, -10, a->color, 2.0f); al_draw_line(20, -10, 20, -5, a->color, 2.0f); al_draw_line(20, -5, 0, 0, a->color, 2.0f); al_draw_line(0, 0, 20, 10, a->color, 2.0f); al_draw_line(20, 10, 10, 20, a->color, 2.0f); al_draw_line(10, 20, 0, 15, a->color, 2.0f); al_draw_line(0, 15, -20, 20, a->color, 2.0f);
531
Blasteroidy Ruch asteroid Asteroidy poruszają się po ekranie wzdłuż linii prostych. Choć poruszają się po liniach prostych, to bezustannie obracają się wokół własnej osi. Jeśli asteroida wyleci poza jedną krawędź ekranu, natychmiast pojawia się z powrotem po jego drugiej stronie.
Kiedy asteroida zostanie trafiona strzałem Kiedy asteroida zostanie trafiona strzałem z działa naszego statku kosmicznego, natychmiast rozpada się na dwie części. Każda z nich będzie o połowę mniejsza od rozmiaru oryginalnej asteroidy. Kiedy asteroida zostanie trafiona i podzielona kilka razy, całkowicie znika z ekranu. Każde trafienie asteroidy powiększa wynik gracza o 100 punktów. Musisz zdecydować, w jaki sposób będziesz rejestrował dane o zbiorze wszystkich asteroid prezentowanych na ekranie. Może stworzysz jedną wielką tablicę? A może użyjesz listy połączonej?
Status gry Istnieje kilka informacji, które musisz wyświetlać na ekranie: liczba statków, jakimi gracz jeszcze dysponuje, oraz jego wynik. Kiedy wszystkie statki gracza zostaną zniszczone, wielkimi literami na samym środku ekranu masz wyświetlić napis „Gra skończona!”.
532
Blasteroidy Przekształcenia umożliwiające przesuwanie obiektów W trakcie gry będziesz musiał animować obiekty i przesuwać je po ekranie. Statek kosmiczny będzie musiał latać, a asteroidy obracać się i powoli przesuwać, a nawet zmieniać wielkość. Obroty, przesunięcia oraz skalowanie to operacje, których wykonanie wymaga przeprowadzenia całkiem dużej ilości operacji matematycznych. Na szczęście biblioteka Allegro jest wyposażona w całkiem sporą liczbę wbudowanych przekształceń. Podczas rysowania obiektu, takiego jak nasz statek kosmiczny, prawdopodobnie będziesz chciał się skoncentrować na narysowaniu go wokół środka układu współrzędnych. Środkiem układu współrzędnych jest lewy górny wierzchołek ekranu; ma on współrzędne (0, 0). Współrzędne x zmieniają się w poziomie, natomiast współrzędne y — w pionie. Korzystając z przekształceń, możesz przesunąć środek układu współrzędnych w miejsce, którego wymaga aktualnie rysowany obiekt, a następnie obrócić tak, by był skierowany w odpowiednią stronę. Kiedy już to zrobisz, wystarczy narysować obiekt wokół środka układu współrzędnych, a znajdzie się on w odpowiednim miejscu ekranu. Oto na przykład jeden ze sposobów narysowania na ekranie naszego statku kosmicznego:
void draw_ship(Spaceship* s) { RR¼XYxFVµY¬ xMx Z al_rotate_transform(&transform, DEGREES(s->heading)); x x Z ¤ 8 ¤ M al_use_transform(&transform); al_draw_line(-8, 9, 0, -11, s->color, 3.0f); al_draw_line(0, -11, 8, 9, s->color, 3.0f); al_draw_line(-6, 4, -1, 4, s->color, 3.0f); al_draw_line(6, 4, 1, 4, s->color, 3.0f); }
533
Blasteroidy Produkt końcowy Kiedy skończysz, nadejdzie czas, byś zagrał w swoje Blasteroidy!
Swoją grę możesz usprawniać i rozbudowywać na wiele sposobów. Na przykład niby dlaczego nie mógłbyś jej zintegrować z OpenCV?
534
Wyjeżdżamy z miasta…
Doskonale bawiliśmy się w Cewicach! Przykro nam, że już wyjeżdżasz, ale nie ma nic lepszego od praktycznego wykorzystania zdobytej wiedzy i umiejętności. Wciąż jednak w tej książce czeka na Ciebie kilka dodatkowych skarbów oraz, oczywiście, indeks, który też możesz przeczytać. Jednak później nadejdzie czas, byś zebrał wszystkie nowe pomysły i zastosował je w praktyce. Bardzo byśmy chcieli dowiedzieć się, jak sobie radzisz, więc koniecznie napisz kilka słów na naszej witrynie Head First Labs — http://www.headfirstlabs.com — i daj znać, jak CI idzie pisanie w C!
jesteś tutaj 535
536
Rozdział 12.
Dodatek A( ,
Dziesięć najważniejszych rzeczy (których nie opisaliśmy) O rany, spójrzcie tylko na tę smakowitą ucztę, którą zostawiliśmy…
Nawet po tym wszystkim, co już napisaliśmy, wciąż jeszcze pozostaje coś, czego nie wyjaśniliśmy. Jest jeszcze parę zagadnień, o których według nas powinieneś się dowiedzieć. Nie czulibyśmy się dobrze, gdybyśmy zupełnie je zignorowali. Wymagają one choć krótkiego wyjaśnienia, a my naprawdę nie chcemy oddawać Ci do rąk książki, której nie byłbyś w stanie podnieść bez solidnego treningu na lokalnej siłowni. A zatem zanim odłożysz tę książkę na półkę, przeczytaj zamieszczone tu informacje.
to jest dodatek 537
Operatory
1. Operatory W tej książce stosowaliśmy kilka operatorów, takich jak podstawowe operatory arytmetyczne — +, -, * oraz /. Jednak język C udostępnia także wiele innych operatorów, które mogą znacznie ułatwić nam życie.
Inkrementacja i dekrementacja Inkrementacja i dekrementacja to operacje powiększania i zmniejszania wartości o 1. Są to bardzo popularne operacje wykonywane w kodzie programów, zwłaszcza w przypadku gdy są w nim umieszczone pętle inkrementujące wartość pewnego licznika. Język C udostępnia cztery proste wyrażenia ułatwiające wykonywanie inkrementacji i dekrementacji: Powiększa wartość zmiennej i o a następnie zwraca nową wartość1, tej zmiennej.
++i
Powiększa wartość zm i o 1, a następnie zw iennej rac wartość tej zmiennej. a starą
i++
Zmniejsza wartość zmiennej i o 1, a następnie zwraca nową wartość tej zmiennej.
--i
iennej Zmniejsza wartość zm a rac i o 1, a następnie zw nej. starą wartość tej zmien
i--
Każde z tych wyrażeń zmienia wartość zmiennej i. Położenie operatorów ++ bądź -- określa, czy zostanie zwrócona oryginalna, czy też nowa wartość zmiennej. Oto przykład: int i = 3;
3, a i == 4. Po wykonaniu tej instrukcji j ==
int j = i++;
Operator trójargumentowy Co można zrobić, w przypadku gdy chcemy użyć jednej wartości, jeśli pewien warunek będzie spełniony, oraz drugiej — jeśli nie będzie? if (x == 1) return 2; else return 3;
Język C udostępnia operator trójargumentowy, który umożliwia skrócenie powyższego kodu do następującej postaci: return (x == 1) ? 2 : 3; W pierwszej kolejności jest zapisywany warunek.
538
Dodatek A
Następnie wartość, która zostanie użyta, jeśli warunek będzie spełniony.
I w końcu wartość, która zostanie użyta, jeśli warunek nie będzie spełniony.
Pozostałości
Zabawy bitami Język C może być używany do tworzenia programów operujących na bardzo niskim poziomie, dlatego też udostępnia kilka operatorów pozwalających wyznaczać nowe sekwencje bitów.
Operator Opis ~a
Wartości poszczególnych bitów zmiennej a zostaną zmienione na przeciwne.
a&b
Bitowa koniunkcja (operacja AND) wartości zmiennych a i b.
a | b
Bitowa alternatywa (operacja OR) wartości zmiennych a i b.
a^b
Bitowa alternatywa wykluczająca (XOR) wartości zmiennych a i b.
>
Przesunięcie bitów w prawo (zmniejszanie).
Operatora make fred !!7:!D7
zostało To polecenie kompilacji gram pro ez prz ne wygenerowa trukcji, make bez jawnych ins jak to ma zrobić.
To jest reguła niejawna.
Jest to możliwe dzięki temu, że program jest wyposażony w zbiór wbudowanych przepisów. Więcej informacji o tym programie możesz znaleźć na stronie: http://www.gnu.org/software/make/
jesteś tutaj 547
Narzędzia programistyczne
8. Narzędzia programistyczne Jeśli programujesz w C, to najprawdopodobniej bardzo zależy Ci na wydajności działania i stabilności aplikacji. Jeśli do kompilacji kodu używasz gcc, to zapewne będziesz się także chciał przyjrzeć innym dostępnym narzędziom programistycznym GNU.
gdb GNU Project Debugger (gdb) pozwala analizować skompilowany program podczas jego działania. Jest on nieoceniony, jeśli próbujesz wykryć i poprawić jakiś nieznośny błąd. gdb można używać zarówno z poziomu wiersza poleceń, jak i w zintegrowanych środowiskach programistycznych, takich jak Xcode lub Guile. http://sourceware.org/gdb/download/onlinedocs/gdb/index.html
gprof Jeśli Twój kod nie działa tak szybko, jak planowałeś, to być może warto go poddać profilowaniu. Narzędzie GNU Profiler (gprof) jest w stanie pokazać, które fragmenty kodu są najwolniejsze, dzięki czemu będziesz mógł je odpowiednio zmodyfikować. gprof pozwala skompilować zmodyfikowaną wersję kodu, która po zakończeniu działania programu generuje raport wydajności. Po jego utworzeniu program gprof wykonywany z poziomu wiersza poleceń pozwala przeanalizować taki raport i wskazać najwolniejsze fragmenty kodu. http://sourceware.org/binutils/docs-2.22/gprof/index.html
gcov Kolejnym narzędziem profilującym jest GNU Coverage (gcov). W odróżnieniu od programu gprof, który normalnie jest używany do sprawdzania wydajności działania kodu, gcov pozwala określać, które fragmenty kodu programu zostały wykonane, a które nie. Zagadnienie to jest ważne, gdy próbujemy zautomatyzować testy pisanego programu, gdyż w takim przypadku musimy mieć pewność, że obejmują one wszystkie fragmenty kodu, który planujemy testować. http://gcc.gnu.org/onlinedocs/gcc/Gcov.html
548
Dodatek A
Pozostałości
9. Tworzenie graficznego interfejsu użytkownika W żadnym z programów pisanych w głównych rozdziałach tej książki nie stworzyłeś żadnego graficznego interfejsu użytkownika. W laboratoriach korzystałeś natomiast z bibliotek Allegro oraz OpenCV do napisania programów, które mogły wyświetlać na ekranie bardzo proste okna. Problem polega na tym, że w każdym z dostępnych systemów operacyjnych graficzny interfejs użytkownika jest tworzony całkowicie inaczej.
Linux — GTK W systemie Linux jest dostępnych wiele bibliotek służących do tworzenia graficznego interfejsu użytkownika, a jedną z najbardziej popularnych jest GIMP toolkit (GTK+). http://www.gtk.org/ Biblioteka GTK+ jest dostępna w wersjach nie tylko dla Linuksa, lecz także dla systemów Windows oraz Mac OS, choć zdecydowanie najczęściej jest używana właśnie w systemie Linux.
Windows System Windows posiada wbudowane, bardzo zaawansowane biblioteki do obsługi graficznego interfejsu użytkownika. Programowanie aplikacji o graficznym interfejsie użytkownika w tym systemie jest bardzo wyspecjalizowanym zagadnieniem, dlatego zanim będziesz w stanie łatwo tworzyć takie aplikacje, będziesz musiał poświęcić nieco czasu na poznanie szczegółów działania interfejsów programowania aplikacji (API) systemu Windows. Coraz więcej aplikacji przeznaczonych dla systemu Windows jest pisanych w językach bazujących na C, takich jak C# oraz C++. Wprowadzenie do zagadnień pisania programów dla systemu Windows można znaleźć na stronie: http://www.winprog.org/tutorial/
Mac OS — Carbon Na komputerach Macintosh jest używany graficzny interfejs użytkownika (GUI) o nazwie Aqua. Na tych komputerach programy o graficznym interfejsie użytkownika pisane w C korzystają z bibliotek o nazwie Carbon. Jednak nowszy sposób tworzenia takich programów dla komputerów Mac bazuje na wykorzystaniu bibliotek Cocoa, przeznaczonych dla kolejnego języka bazującego na C — tak zwanego Objective-C. Teraz, kiedy już dotarłeś do końca tej książki, nadarza Ci się doskonała okazja, by zacząć naukę Objective-C. My, w Head First Labs, uwielbiamy książki i kursy programowania na komputery Mac dostępne na Big Nerd Ranch: http://www.bignerdranch.com/
jesteś tutaj 549
Materiały
10. Materiały Poniżej przedstawiliśmy listę niektórych popularnych książek i witryn poświęconych programowaniu w języku C: Brian W. Kernighan, Dennis M. Ritchie, Język ANSI C. Programowanie. Wydanie II, Helion, Gliwice 2010.
Ta książka zdefiniowała oryginalny język C, jej egzemplarz ma na półce każdy programista piszący w C. Samuel P. Harbison, Guy L. Steele Jr., C: A Reference Manual, Prentice Hall, New Jersey 2002.
To doskonały podręcznik, który na pewno będziesz chciał mieć pod ręką podczas pisania programów w C. Peter van der Linden, Expert C Programming, Prentice Hall, New Jersey 1994.
Jeśli interesują Cię bardziej zaawansowane zagadnienia związane z programowaniem w C, to warto sięgnąć po tę doskonałą książkę Petera van der Lindena. Steve Oualline, Practical C Programming, O’Reilly, Sebastopol 2003.
Ta książka przedstawia praktyczne szczegóły pisania oprogramowania w języku C.
Witryny WWW Informacje dotyczące standardów można znaleźć na stronie: http://pubs.opengroup.org/onlinepubs/9699919799/ Jeśli szukasz dodatkowych poradników poświęconych programowaniu w C, zajrzyj na witrynę: http://www.cprogramming.com/ Ogólne informacje encyklopedyczne na temat programowania w C można znaleźć na witrynie: http://www.cprogrammingreference.com/ Ogólne poradniki poświęcone programowaniu w C można znaleźć na stronie: http://www.crasseux.com/books/ctutorial/
550
Dodatek A
Dodatek B Zagadnienia programowania w C
Powtórka z całego materiału
Czy kiedykolwiek pomyślałeś, że fajnie by było, gdyby te wszystkie wspaniałe fakty dotyczące C zostały zebrane w jednym miejscu? W tym dodatku zostały zebrane wszystkie zagadnienia oraz zasady związane z pisaniem w języku C, które przedstawiliśmy w całej książce. Przejrzyj je wszystkie i przekonaj się, czy je zapamiętałeś. Przy każdym fakcie został podany numer rozdziału, z którego on pochodzi, a zatem nie będziesz miał problemów z odnalezieniem dodatkowych informacji, gdybyś ich potrzebował. Możesz nawet wyciąć te strony z książki i przykleić je sobie na ścianie.
to jest dodatek 551
Podstawy
552
Dyrektywa #include dołącza zewnętrzny kod wykonujący różne operacje, na przykład operacje wejścia-wyjścia.
Dodatek B
Do łączenia warunków można używać operatorów && oraz ||.
Instrukcje if wykonują kod, jeśli określony warunek jest spełniony.
Proste instrukcje można porównać do poleceń.
Podstawy Instrukcje blokowe są umieszczane pomiędzy nawiasami klamrowymi — { oraz }.
Instrukcja switch w wydajny sposób sprawdza różne wartości jednej zmiennej.
Każdy program musi zawierać funkcję main().
Pliki źródłowe powinny mieć rozszerzenie .c.
Pętla while wykonuje kod dopóty, dopóki jest spełniony określony warunek.
W wierszu poleceń możesz umieścić &&, by wykonać program wyłącznie wtedy, gdy uda się go skompilować.
count++ oznacza, że do wartości count należy dodać 1.
Przed uruchomieniem programu napisanego w C trzeba go skompilować.
Powtórka materiału
gcc jest najpopularniejszym kompilatorem języka C.
Parametr -o określa plik wynikowy.
count-- oznacza, że od wartości count należy odjąć 1.
Pętle do-while wykonują kod przynajmniej raz.
Pętle for są bardziej zwartym sposobem tworzenia pętli. jesteś tutaj 553
Wskaźniki
554
Zmienne lokalne są przechowywane na stosie.
Dodatek B
1
1
Zawartość pamięci określonej wskaźnikiem a można odczytać, używając wyrażenia *a.
&x jest nazywany wskaźnikiem na zmienną x.
1
&x zwraca adres zmiennej x.
1
1
Wystarczy użyć łańcucha znaków do zainicjowania tablicy, by łańcuch został skopiowany.
Zmienna x będąca wskaźnikiem na znaki jest deklarowana jako char *x.
Zmienne tablicowe mogą być używane jak wskaźniki.
1
Wywołanie scanf("%i", &x) pozwala użytkownikowi na bezpośrednie podanie wartości zmiennej x.
1
1 1
fgets(buf, rozmiar, stdin) jest prostszym sposobem wczytywania tekstów.
Powtórka materiału
178
178
Funkcja strcat() łączy ze sobą dwa łańcuchy znaków.
Funkcja strcpy() kopiuje jeden łańcuch znaków do drugiego.
Plik nagłówkowy string.h zawiera użyteczne funkcje do operacji na łańcuchach znaków.
Tablicę tablic tworzy się, używając zapisu char strings [...][...].
178
Funkcja strstr(a, b) zwraca adres łańcucha b wewnątrz łańcucha a.
178 178
Tablica łańcuchów znaków jest tablicą tablic.
178 178
Literały łańcuchowe są zapisywane w pamięci tylko do odczytu.
178
178 1
Funkcja strcmp() porównuje dwa łańcuchy znaków.
Funkcja strchr() określa położenie znaku wewnątrz łańcucha.
Funkcja strlen() określa długość łańcucha znaków.
jesteś tutaj 555
Strumienie danych
556
Standardowy strumień błędów jest oddzielnym strumieniem wyjściowym przeznaczonym do wyświetlania komunikatów o błędach.
Własne strumienie danych można tworzyć, używając wywołania fopen(“plik”, tryb).
Dodatek B
Domyślnie zawartość standardowego strumienia wyjściowego jest wyświetlana na ekranie.