PROGRAMOWANIE STRUKTURAL N E
I . Algorytmy
Pojęcie algorytmu
Algorytm – abstrakcyjny przepis opisujący działanie, które może być wykonane przez człowieka, przez
komputer lub w inny sposób.
400BC-300BC – pierwszy niebanalny algorytm znajdowania największego wspólnego dzielnika dwóch
dodatnich liczb całkowitych wymyślony przez Euklidesa.
IX wiek – podanie reguł dodawania, odejmowania, mnożenia i dzielenia liczb dziesiętnych przypisuje się
perskiemu matematykowi Muhammedowi Alchwarizimi (łać. Algorismus).
1801r. - krosno tkackie wynalezione przez Francuza Josepha Jacquarda jest jedną z najwcześniejszych
maszyn wykonujących proces sterowany niebanalnym algorytmem.
Algorytmika – nauka o algorytmach.
W połowie lat trzydziestych XX wieku dokonano niektórych z najbardziej fundamentalnych prac nad teorią
algorytmów. Znanymi postaciami tego okresu byli: Alan Turning (Anglik), Kurt Goedel (Niemiec), Andriej
A. Markow (Rosjanin), Alonzo Church, Emil Post, Stephen Kleene (Amerykanie).
W latach sześćdziesiątych ponownie zainteresowano się pracami nad algorytmiką z lat trzydziestych i od
tego czasu ta dziedzina jest stale przedmiotem szeroko zakrojonych badań.
Poziom szczegółowości algorytmu
Przykłady:
a) Algorytm mnożenia dwóch dowolnych liczb naturalnych można podać w postaci wymagającej
znajomości tabliczki mnożenia lub nie. W tym drugim przypadku trzeba określić operacje wyszukiwania
iloczynów w tabliczce mnożenia.
b) Algorytm przejazdu samochodem z jednego miejsca w dużym mieście do innego można określić na
poziomie szczegółowości odpowiedniej dla osoby znającej miasto dobrze, nie znającej jego wcale, a
nawet dla osoby nie umiejącej jeździć samochodem.
Ogólna postać algorytmu zależy w krytyczny sposób od wyboru akcji podstawowych i ich stosowności w
danym przypadku.
Długość trwania procesów jednego algorytmu
Ten sam algorytm może opisywać długi lub krótki proces, w zależności od rodzaju danych wejściowych.
Przykładem może być tu sumowanie zarobków pracowników w przedsiębiorstwie. Dla danych wziętych z
dużego przedsiębiorstwa proces ten będzie trwał dłużej niż dla danych wziętych z przedsiębiorstwa małego.
Zadanie algorytmiczne
Każda z akcji podstawowych algorytmu powinna zostać wykonana w skończonym czasie.
Algorytmy i dane
Algorytmy zawierają starannie dobrane instrukcje elementarne, będące zleceniami akcji podstawowych.
W jaki sposób instrukcje są rozmieszczone w algorytmie, tak aby na jego podstawie można było dociec
Z
adanie
algorytmi
czne
Scharakteryzowanie
wszystkich poprawnych
danych wejściowych
Scharakteryzowanie
oczekiwanych wyników
jako funkcji danych
wejściowych
Rozwiązanie
alg
ory
tm
iczn
e
Oczekiwane wyniki
Dowolne, poprawne dane
Algorytm A
2
dokładnej kolejności wykonania akcji podstawowych?
Jakimi obiektami manipulują algorytmy?
Algorytm musi zawierać instruk
cje s
terujące
, które pozwolą sterować kolejnością wykonania akcji
podstawowych.
Struktury sterujące
Kolejnością wykonania steruje się za pomocą układów instrukcji zwanych strukturami przepływu
sterowania lub strukturami sterującymi.
Przykład takiego układu instrukcji:
–
bezpośrednie następstwo
–
wybór warunkowy (lub rozgałęzienie warunkowe)
–
iteracja ograniczona
–
iteracja warunkowa (lub nieograniczona)
Składanie struktur sterujących
Algorytm może zawierać wiele konstrukcji sterujących ułożonych w niebanalnych kombinacjach. Mającą
największe znaczenie kombinacją są iteracje zagnieżdżone (lub pętle zagnieżdżone). Składają się one z pętli
zewnętrznej i pętli wewnętrznej.
Sortowanie bąbelkowe jako przykład zastosowania struktur sterujących
I przebieg
II przebieg
Algorytm sortowania bąbelkowego dla listy N-elementowej:
(1) wykonaj co następuje N-1 razy:
(1.1) wskaż na pierwszy element;
(1.2) wykonaj co następuje N-1 razy:
(1.2.1) porównaj wskazany element z elementem następnym;
(1.2.2) jeśli porównywane elementy są w niewłaściwej kolejności, zamień je miejscami;
(1.2.3) wskaż na następny element.
Algorytm bardziej sprawny przechodziłby listę w kolejnym przebiegu jeden raz mniej niż w poprzednim.
Sortowania bąbelkowego nie należy używać ze względu na jego słabą sprawność czasową – O(N
2
).
Instrukcja skoku
Ma ona ogólną postać “skocz do G”, gdzie G oznacza pewne miejsce w tekście algorytmu. Konstrukcja ta
jest kontrowersyjna, ponieważ czyni program zagmatwanym. Wielu badaczy przeciwstawia się swobodnemu
używaniu instrukcji skoku w algorytmach ze względu na trudności w badaniu takich algorytmów.
Diagramy do algorytmów
Najczęściej instrukcje elementarne zapisuje się w ramkach prostokątnych, a testy w ramkach o kształcie
rombu. Strzałki wskazują na kolejną wykonywaną instrukcję.
Poniżej przedstawiony został schemat blokowy sumowania zarobków. Schemat ten zawiera jedną pętlę.
15
12
28
23
15
12
28
23
X
X
15
28
12
23
28
15
12
23
28
15
12
23
28
15
23
12
28
23
15
12
28
23
15
12
III przebieg
bez zmian
Start
Zanotuj 0. Wskaż na
pierwszą płacę.
Dodaj wskazywaną płacę
do zanotowanej liczby.
3
Podprogramy, czyli procedury
W algorytmie można wydzielić jego fragment, zapisać go w innym miejscu i wywołać przez skrót w
miejscu skąd został wzięty. Można również znaleźć w algorytmie dwa lub więcej podobnych fragmentów
różniących się wartością jednego lub więcej parametrów i zamiast nich umieścić tam skrót z wartościami
parametrów odpowiednich dla konkretnego fragmentu, a skrót rozwinąć w innym miejscu. Tak wydzielony
fragment algorytmu nazywamy podprogramem lub procedurą. Podprogramy można zagnieżdżać.
Zalety podprogramów:
–
skracają kod algorytmu
–
czynią algorytm bardziej czytelnym
–
mogą zostać wydzielone jako osobne części i stosowane w innych algorytmach
Projektant algorytmu dzieli go na podprogramy. Może on najpierw obmyślać algorytm wysokiego poziomu
z instrukcjami elementarnymi, nie należącymi do powszechnie uzgodnionych (podprogramami), a później
napisać podprogramy. Z kolei te podprogramy mogą same zawierać podprogramy, które zostaną napisane w
następnej kolejności. Taką metodę programowania od ogółu do szczegółu nazywamy analityczną (lub
zstępującą). Metoda programowania od szczegółu do ogółu nazywana jest syntetyczną (lub wstępującą).
Rekurencja
Rekurencja jest to zdolność podprogramu do wywoływania samego siebie.
Wieże Hanoi (łamigłówka): Mamy trzy kołki A, B i C. Na pierwszym kołku, A, znajduje się N krążków
nanizanych w porządku malejących wielkości, podczas gdy pozostałe kołki są puste. Należy przenieść krążki
z kołka A na B, być może używając do tego kołka C. Krążki można przenosić po jednym na raz. Krążek
większy nie może być umieszczony na wierzchu mniejszego.
Dla N=3 : A -› B; A -› C; B -› C; A -› B; C -› A; C -› B; A -› B.
Algorytm przeniesienia krążków z kołka A na kołek B można zapisać jako procedurę rekurencyjną:
Procedura przenieś N z X na Y używając Z
(1) jeśli N=1, to wypisz “X -› Y”;
(2) w przeciwnym razie (tj. jeśli N>1) wykonaj, co następuje:
(2.1) wywołaj przenieś N-1 z X na Z używając Y;
(2.2) wypisz “X -› Y”;
(2.3) wywołaj przenieś N-1 z Z na Y używając X;
(3) wróć.
Minimalna ilość przeniesień to 2
N
-1.
Minimalne zbiory struktur sterujących
Każdy algorytm można przekształcić do postaci, w której używane jest tylko kilka struktur sterujących.
Jeden dobrze znany minimalny zbiór struktur sterujących to: następstwo (a-potem), wybór warunkowy (jeśli-
to), iteracja warunkowa (np. dopóki-wykonaj).
Instrukcję skoku można zastąpić kosztem nieznacznego zwiększenia objętości algorytmu.
Stop
Czy koniec
listy?
Wypisz zanotowaną
liczbę.
TAK
NIE
Wskaż na
następną płacę.
A
B
C
4
Podprogramy i rekurencję da się zastąpić za pomocą prostych pętli, ale trzeba dodać oprzyrządowanie w
postaci nowych instrukcji elementarnych.
Typy danych
Dane są to obiekty, którymi manipuluje algorytm.
Dane mogą być różnych typów. Jeden z podziałów danych na typy danych określony ze względu na zbiory
operacji dozwolonych na nich to słowa i liczby. Inny podział danych uwzględniający ich reprezentację
maszynową to dane całkowite i zmiennoprzecinkowe.
Struktury danych
Struktury danych i operacje na nich organizują obiekty danych (dane). Jeśli myślimy o pojedynczym
elemencie struktury danych to najczęściej mamy na myśli obiekt zwany zmienną. W zmiennej można
zapamiętać obiekt danych.
Podstawowe struktury danych to: lista, zbiór i graf.
Lista to skończony ciąg elementów: q=[x
1
, x
2
, ..., x
n
]. Skrajne elementy listy x
1
i x
n
nazywają się końcami
listy (odpowiednio lewym i prawym), a wielkość |q|=n to długość (lub rozmiar listy). Szczególnym
przypadkiem listy jest lista pusta.
W przeciwieństwie do elementów listy elementy w zbiorze S={x
1
, x
2
, ..., x
n
} nie są podane w żadnym
ustalonym porządku. Liczbę n elementów w zbiorze S nazywamy rozmiarem zbioru: |S|=n.
Graf to system, który zapisujemy jako G=(V,E), gdzie V oznacza zbiór skończony, którego elementy są
nazywane wierzchołkami (jeżeli graf jest strukturą danych, to elementy te nazywa się węzłami), a E to zbiór
krawędzi, czyli par wierzchołków ze zbioru V, przy czym, dokładniej, albo E jest podzbiorem zbioru par
uporządkowanych {(x,y): x, y
∈ V ∧ x ≠ y} i wtedy graf nazywa się zorientowany, a krawędzie są
oznaczane strzałkami łączącymi wierzchołki, albo E jest podzbiorem zbioru wszystkich dwuelementowych
podzbiorów zbioru V i wtedy graf nazywa się niezorientowany, a krawędzie oznaczane są liniami. W obu
tych przypadkach krawędź łączącą wierzchołki x i y oznaczamy (x,y). Rozmiar grafu G=(V,E) jest równy
sumie dwóch liczb: n=|V| i m=|E|.
Struktury danych, które mają najszersze zastosowanie to:
–
wektory (czyli listy, czyli tablice jednowymiarowe)
Każdy element wektora jest dostępny bezpośrednio poprzez indeks.
–
tablice
Są to wektory, których elementami są wektory (tablice dwuwymiarowe, czyli tabele) lub tablice.
–
kolejki (czyli listy FIFO, ang. First-In-First-Out)
Elementy można dodawać na końcu listy i wyjmować je na początku.
–
stosy (czyli listy LIFO, ang. Last-In-First-Out)
Elementy można dodawać i wyjmować z jednego końca (wierzchołka) listy.
–
drzewa
Drzewo to dowolny niezorientowany graf spójny i acykliczny. Spójność oznacza, iż każde dwa wierzchołki
grafu są połączone ścieżką utworzoną z krawędzi grafu. Acykliczność oznacza brak cykli prostych
utworzonych z krawędzi grafu. Drzewo z korzeniem to drzewo z wyróżnionym jednym wierzchołkiem
nazywanym korzeniem. Prawie zawsze gdy mówi się o drzewie, to ma się na myśli drzewo z korzeniem.
Związki pomiędzy niektórymi strukturami danych, a strukturami sterującymi:
wektory <---> pętle
tablice <---> pętle zagnieżdżone
drzewa <---> procedury rekurencyjne
I I . J zyki programowania
ę
Każdy komputer może wykonać bezpośrednio jedynie niewielką liczbę skrajnie prostych operacji, takich
jak przerzucenie, wyzerowanie lub sprawdzenie bitu.
przerzuć bit 1
wyzeruj bit 1
jeśli bit 2 jest równy 1,
wtedy przerzuć bit 3
przerzuć bit 2
wyzeruj bit 2
jeśli bit 3 jest równy 1,
wtedy przerzuć bit 4
0 1 0 1 1
0 1 0 0 1
0 1 1 0 1
0 1 0 1 1
0 1 0 0 1
0 1 0 0 1
0 1 0 1 1
0 1 0 1 1
1 1 0 1 1
5
Przerzucenie
Wyzerowanie
Sprawdzenie
Aby opisać algorytm na użytek komputera, posługujemy się językiem programowania, w którym piszemy
programy.
Język programowania składa się z notacji i reguł, według których pisze się programy. Osoba pisząca
program zwie się programistą i nie musi być ona autorem algorytmu.
Algorytm sumowania liczb od 1 do N w typowym (hipotetycznym) języku programowania JP można by
zapisać:
wczytaj N;
X:=0;
dla Y od 1 do N wykonaj
X:=X+Y;
koniec; wypisz X.
Definicję składni języka można zapisać symbolicznie w notacji zwanej BNF (Backus-Naur Form, od
nazwisk wynalazców) w następujący sposób (“|” oznacza “lub”):
<instrukcja> : <instrukcja-dla> | <instrukcja-przypisania> | ...
<instrukcja-dla> : dla <nagłówek-dla> wykonaj <instrukcja> koniec
Opis klauzuli nagłówka mógłby brzmieć:
<nagłówek-dla> : <zmienna> od <wartość> do <wartość>
Język narzuca również wzorce definiowania struktur danych. Na przykład tablicę TA o wymiarach 50 na
100, której wartości mogą być liczbami całkowitymi, można by zdefiniować w hipotetycznym języku JP
instrukcją:
definiuj TA tablica [1..50,8..107] w niej liczby całkowite
a język mógłby dopuścić odwołania do elementów tablicy TA wyrażeniami postaci:
TA(wartość, wartość)
W rzeczywistości składnia wyznacza znacznie więcej niż tylko dostępne struktury sterujące i struktury
danych oraz operacje elementarne.
Język programowania wymaga nie tylko sztywnych reguł definiowania postaci poprawnego programu, ale
także równie sztywnych reguł definiowania jego znaczenia, czyli formalnej i jednoznacznej semantyki.
Od języków wysokiego poziomu do manipulowania bitami
Programista koduje program w języku wysokiego poziomu (program Ap). Program Ap przechodzi pewną
liczbę przekształceń, które sprowadzają go do poziomu, na którym komputer już może sobie poradzić.
Ostatecznym wynikiem tych przekształceń jest program Am na poziomie maszyny, o którym mówi się, że
jest napisany w języku maszyny. Liczba przekształceń bywa różna, w zależności od języka i komputera.
Języki adresów symbolicznych (asemblery) różnią się dla różnych maszyn, ale z reguły stosuje się w nich
dość proste struktury sterujące, przypominające instrukcje skocz oraz konstrukcje jeśli-to. Operują one nie
tylko bitami, lecz także liczbami całkowitymi i ciągami symboli, i mogą odwoływać się bezpośrednio do
adresów w pamięci komputera.
LDS 0,Y
(załaduj stałą 0 pod adres Y)
PETLA
POR N,Y
(porównaj wartości pod adresami N i Y)
SKR RESZTA
(jeśli równe, skocz do instrukcji o etykiecie RESZTA)
DDS 1,Y
(dodaj stałą 1 do wartości pod adresem Y)
<treść pętli>
Pomysł
algorytmu
algorytm
programowanie
program w języku
wysokiego poziomu
kompilacja
program
w języku adresów
symbolicznych
Kod maszynowy
Wykonanie
na komputerze
6
SKO PETLA
(skocz z powrotem do instrukcji o etykiecie PETLA)
RESZTA
(reszta programu)
Interpretatory
Program może być wykonywany w ten sposób, że każda z instrukcji wysokiego poziomu zostaje
przetłumaczona na instrukcje poziomu maszyny natychmiast po jej napotkaniu, a te z kolei od razu się
wykonuje. Mechanizm odpowiedzialny za to lokalne tłumaczenie i natychmiastowe wykonanie jest
fragmentem oprogramowania systemowego, nazywanym interpretatorem.
To, czy komputer będzie kompilował, czy interpretował dany program, zależy od samego komputera,
języka programowania i konkretnego pakietu oprogramowania systemowego, który jest używany. Niektóre
języki programowania nadają się do interpretowania bardziej niż inne, wszystkie jednak mogą w zasadzie
być kompilowane.
Przykłady języków interpretowanych (skryptowych): Lisp, APL, awk, Perl.
Wykonanie programu prowadzone przez interpreter pozwala na łatwiejsze do prześledzenia podsumowanie
tego, co się dzieje, szczególnie przy pracy konwersacyjnej za pośrednictwem końcówki z monitorem
ekranowym. Z tego powodu wielu użytkowników uważa, że języki interpretowane są łatwe do opanowania,
ponieważ można eksperymentować z poszczególnymi poleceniami, od razu obserwując wynik ich działania.
Języki skryptowe są mało wydajne, ponieważ polecenia muszą być interpretowane przy każdym
uruchomieniu. Nie nadają się one również do bezpośredniej obsługi pamięci i operacji wejścia-wyjścia.
Programowanie strukturalne a obiektowe
W języku proceduralnym (strukturalnym) określa się operacje, które mają być przeprowadzane na danych,
natomiast przy programowaniu obiektowym tworzy się obiekty, które zawierają dane i potrafią na nich
operować.
W języku strukturalnym tworzy się funkcje, które operują na strukturach danych. W języku obiektowym
funkcje wchodzą (dokładniej: mogą wchodzić) w skład struktur danych.
I I I . Opis j zyka C
ę
Historia i standardy języka C
Język C jest językiem programowania bardzo ściśle powiązanym z systemem operacyjnym UNIX. Od roku
1970 większa część systemu UNIX oraz jego aplikacji jest tworzona właśnie w języku C. Język ten nie
zależy bezpośrednio od żadnej architektury sprzętowej, dlatego system UNIX był jednym z pierwszych
przenośnych systemów operacyjnych. Cechy zależne od sprzętu zostały wyizolowane w kilku modułach
wchodzących w skład jądra systemu.
Język C został pierwotnie zaprojektowany przez Dennisa Ritchie'go dla komputerów DEC PDP-11 z
systemem UNIX. Język ten był następcą języka BCPL autorstwa Martina Richarda, którego wcześniejszą
formą był język B zaprojektowany przez Kena Thompsona dla komputerów DEC PDP-7. Pierwsza książka o
języku C - “The C Programming Language” (“Programowanie w języku C”) została wydana w roku 1978.
Jej autorami są Brian Kernigham i Dennis Ritchie. Wyznaczała ona pewien standard nazywany K&R C
(drugie wydanie tej książki obejmowało standard ANSI C). Ze względu na to, aby umożliwić przenoszenie
starszy programów, pewne cechy K&R C, z których zrezygnowano są akceptowalne przez kompilatory (i
dopuszczalne w ANSI C).
W roku 1989 język C został po raz pierwszy standaryzowany przez ANSI (Amerykański Narodowy
Instytut Standardów) w dokumencie ANSI X3.159 -1989 “Programming Language C”. Głównym celem
standaryzacji ANSI było stworzenie nadzbioru K&R C, jednak komitet ANSI włączył wiele cech, które były
“nieoficjalnie” używane przez kompilatory C. K&R C nie określał standardowej biblioteki języka, a język C
jest zależny od swojej biblioteki bardziej niż większość innych języków programowania, dlatego faktycznym
standardem stała się biblioteka dostarczana z implementacją C dla systemu UNIX. ANSI C uzupełnił ten
brak i określił zbiór funkcji standardowych. Standard ANSI C, z kilkoma drobnymi modyfikacjami, został
przyjęty przez ISO (Międzynarodową Organizację Standardów) jako standard ISO 9899. Pierwsze wydanie
dokumentu ISO pochodzi z 1990r. (ISO 9899:1990). Oba wymienione standardy są prawie identyczne i
nazw ANSI C i C89 można używać zamiennie.
Przez wiele lat specyfikacja języka C pozostawała prawie niezmieniona, jednak w późnych latach
dziewięćdziesiątych standard uległ zmianom, prowadzącym do ISO 9899:1999, który został opublikowany
w 1999r. Standard ten jest powszechnie cytowany jako C99.
7
Nowe cechy dodane do C99 zawierają:
–
funkcje inline
–
możliwość deklarowania zmiennych w dowolnym miejscu (w zgodzie z C++)
–
dodanie kilku nowych typów danych, m. in. long long int (pozwalający na łatwiejsze przejście z
architektury 32-bitowej na 64-bitową), typ logiczny boolean oraz typ reprezentujący liczby zespolone.
–
akceptację dawno używanego typu komentarza //, zgodnego z C++
–
snprintf()
gcc i wiele komercyjnych kompilatorów przyjęło większość cech C99, natomiast producenci kompilatorów
z firm Microsoft i Borland wydają się nie być zainteresowane tymi cechami.
Ogólna postać programu w C
~~~~~~~~~~~~~~~~~~~~~ najprostszy.c ~~~~~~~~~~~~~~~~~~~~~
void main(void){}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc najprostszy.c
- kompilacja programu
$a.out
- wykonanie programu
$
- wynik (powrót do powłoki)
Najprostszy, akceptowalny przez gcc, program to najprostszy.c bez słów void.
Kiedy włączy się standardową bibliotekę we/wy, wtedy można coś napisać na ekranie:
~~~~~~~~~~~~~~~~~~~~~~~ prosty.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{ printf(“Jak sie masz!\n”); return 0;}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc prosty.c -o prosty
- kompilacja programu
$prosty
- wykonanie programu
Jak sie masz!
- wynik
$
- powrót do powłoki
A oto przykład złożonego programu, napisanego w jednym pliku:
~~~~~~~~~~~~~~~~~~~~~~~ zlozony.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
- instrukcja preprocesora doł czaj ca plik
ą
ą
#define MNOZNIK 3
- instrukcja preprocesora zast puj ca ci gi
ę
ą
ą
znaków
int zmienna;
- deklaracja zmiennej globalnej
int funkcja1(int);
- deklaracja funkcji jako prototypu
funkcji
/*albo int funkcja1(int war);*/
- prototyp z atrap w komentarzu
ą
void funkcja2();
- deklaracja funkcji nie b d ca
ę ą
prototypem
// albo void funkcja2(void)
- prototyp funkcji w komentarzu
int funkcja1(int war)
- nagłówek funkcji
{
|
return war;
| ciało funkcji
}
|
int main(void)
- nagłówek specjalnej funkcji main()
{
int a;
- deklaracja zmiennej lokalnej typu
8
całkowitego
char haslo[]="Witaj!";
- deklaracja ła cucha z jego
ń
inicjalizacj
ą
zmienna=2;
- przypisanie do zmiennej warto ci
ś
a=1+zmienna;
- sumowanie i przypisanie wyniku
do zmiennej
printf("%d\n",funkcja1(MNOZNIK*a));
printf("%s\n",haslo);
- wywołanie funkcji bibliotecznej
printf()
funkcja2();
- wywołanie zdefiniowanej funkcji
return 0;
}
void funkcja2(void)
|
{
| definicja funkcji
printf("Juz szumia kasztany ");
|
printf("i pachnie juz wiosna\n");
|
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc zlozony.c -o zlozony
- kompilacja programu
$zlozony
- wykonanie programu
9
|
Witaj!
|
wynik
Juz szumia kasztany i pachnie juz wiosna
|
$
- powrót do powłoki
W programie tym zawarto najważniejsze elementy prostego programu i przykłady ich rozmieszczenia.
Większe programy umieszcza się w wielu plikach. Program “zlozony” umieszczony w wielu plikach
mógłby wyglądać następująco:
~~~~~~~~~~~~~~~~~~~~~ wieloplikowy.c ~~~~~~~~~~~~~~~~~~~~
#include "parametry_w.h"
int main(void)
{
int a;
char haslo[]="Witaj!";
zmienna=2;
a=1+zmienna;
printf("%d\n",funkcja1(MNOZNIK*a));
printf("%s\n",haslo);
funkcja2();
return 0;
}
void funkcja2()
{
printf("Juz szumia kasztany ");
printf("i pachnie juz wiosna\n");
}
~~~~~~~~~~~~~~~~~~~~~ parametry_w.h ~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
9
#include "prototypy_w.h"
#define MNOZNIK 3
int zmienna;
~~~~~~~~~~~~~~~~~~~~~ prototypy_w.h ~~~~~~~~~~~~~~~~~~~~~
int funkcja1(int);
void funkcja2(void);
~~~~~~~~~~~~~~~~~~~~~~~ funkcja1.c ~~~~~~~~~~~~~~~~~~~~~~
int funkcja1(int war)
{
return war;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc wieloplikowy.c funkcja1.c -o wieloplikowy
$wieloplikowy
9
Witaj!
Juz szumia kasztany i pachnie juz wiosna
$
Plik funkcja1.c nie musi być dołączany przez instrukcję preprocesora - #include do pliku głównego.
Wystarczy dołączyć odpowiedni prototyp i skompilować razem plik główny i plik z funkcją. Kompilator
rozpoznaje plik główny po tym, że zawiera on funkcję main(). W ten sposób można dołączać wiele funkcji,
wtedy przydatnym przy kompilacji jest program make.
Nagłówek to zbiór instrukcji u góry pliku. Są to wszystkie elementy poza definicjami funkcji. Jeżeli są one
zgrupowane w jednym pliku, wtedy taki plik nazywamy plikiem nagłówkowym. Pliki nagłówkowe mają
tradycyjnie rozszerzenie “.h”.
Standard ANSI C wymaga od kompilatorów uwzględniania co najmniej 31 znaków identyfikatorów, z
wyjątkiem identyfikatorów zewnętrznych, dla których wymagane jest rozpoznawanie co najmniej 6 znaków.
Zawsze można użyć większej ilości znaków. Język C rozróżnia między małymi i wielkimi literami. W
nazwach można używać liter, cyfr i znaku podkreślenia. Pierwszy znak każdej nazwy musi być literą lub
znakiem podkreślenia. W ANSI C (w odróżnieniu od C++ i C99) zmienne można deklarować jedynie na
początku bloku. Blokiem nazywamy ciąg instrukcji zawartych między nawiasami klamrowymi ({}).
Dane mają swój typ. Jeżeli są to zmienne, to jest on określony w deklaracji zmiennej. Jeżeli są to stałe,
wtedy typ jest domyślny lub określony w inny sposób.
Słowa kluczowe – nazwy będące częścią języka. Nie można ich używać jako identyfikatorów. Przykłady
słów kluczowych:
int, void, return
.
Operatory – wykonują działania na danych. Przykłady operatorów:
=, +, ?:, -
.
Operand – tym czym posługuje się operator. W instrukcji: z
mienna=2; zmienna
i
2
są operandami.
Wyrażenie – kombinacja operatorów i operandów. Przykłady wyrażeń:
zmienna=2
,
a=1+zmienna
.
Instrukcje – są głównymi elementami, z których zbudowane są programy. Instrukcja jest kompletnym
poleceniem wydanym komputerowi. W języku C instrukcje kończą się znakiem średnika. Przykłady
instrukcji:
zmienna=2;
,
funkcja2;
.
Dane proste
Dane ustalone przed uruchomieniem programu i pozostające niezmienione przez cały czas jego działania to
stałe. Dane, które mogą się zmieniać w trakcie działania programu to zmienne.
Słowa kluczowe typów danych:
int, long, short, unsigned, char, float, double,
signed, void, const, volatile
.
int
– definiuje klasę liczb całkowitych w C.
long, short, unsigned
– służą do tworzenia wariantów podstawowego typu int.
signed, unsigned
– precyzują reprezentację char, uniezależniając ją od kompilatora.
float, double
– tworzą klasę typów zmiennoprzecinkowych. Używa się tu też słowa long.
char
– typ używany w odniesieniu do liter alfabetu i innych znaków. Może przechowywać niewielkie
liczby całkowite.
Typy całkowite
Standard ANSI C określa, że minimalny zakres dla typu int powinien obejmować wartości od -32767 do
10
32767 (2 bajty), short int (skrót short) może być mniejszy niż int, long int (skrót long) może być większy niż
int (zalecany większy niż 4 bajty). Odpowiednie typy nieujemne to: unsigned int (skrót unsigned), unsigned
short int (skrót unsigned short), unsigned long int (skrót unsigned long). Typy long long int (skrót long long)
i unsigned long long int (unsigned long long) były używany jeszcze przed akceptacją ich w standardzie C99.
Stałe całkowite są standardowo przechowywane jako wartości typu int, a jeżeli są większe od tego typu to
jako typu long. Aby wymusić traktowanie stałej jako typu long dodaje się przyrostek L (lub l). Podobnie jest
dla stałych typu long long (LL lub ll), unsigned long (uL lub ul), unsigned long long (uLL lub ull).
Przykłady: #define mala_liczba 21234L
#define DUZA_UJEMNA 2845LL
#define DUZA_LICZBA 0x2AuL
Wartości szesnastkowe i ósemkowe
Wartości szesnastkowe poprzedzane są cyfrą 0 i literą x (lub X), a ósemkowe cyfrą 0. Reprezentacje te są
łatwo przetłumaczalne na reprezentację dwójkową (bitową) i vice versa:
nr bitu ->
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
= 256+128+8+4+1=397
1
8
D
= 0x18D
6
1
5
= 0615
Reprezentacja maszynowa liczb całkowitych
Dla ustalenia uwagi podano przykłady 8-bitowych liczb całkowitych.
- bez znaku
numer bitu ------------->
7 6 5 4 3 2 1 0
= 64+8+4+1=77. Zakres od 0 do 255.
wartość bitu ----------->
128 64 32 16 8 4 2 1
- ze znakiem (metoda dopełnienia dwójkowego)
Od 00000000 do 01111111 (od 0 do 127) obowiązuje reguła tak jak dla liczb bez znaku.
Jeżeli najbardziej znaczący bit jest włączony, wtedy liczba jest ujemna, a jej wartość bezwzględną
otrzymuje się przez jej odjęcie od 9-bitowej liczby 100000000 (256).
Przykłady: 10000000 to -128 (100000000-10000000)
11111111 to -1
11001101 to -51 (100000000-11001101=00110011)
Zakres 8-bitowych liczb całkowitych ze znakiem od -128 do 127.
Reprezentacja liczb zmiennoprzecinkowych jest bardziej skomplikowana.
Typ znakowy
W niektórych implementacjach języka C char jest typem posiadającym znak [-128; 127], a w innych nie [0;
255]. Aby uniezależnić się od tych różnic kompilatory zgodne z ANSI C pozwalają dodać do char słowo
kluczowe signed lub unsigned.
Pojedynczy znak zawarty między znakami apostrofu jest stałą znakową: 'A', '1', '!', '#'.
Przykłady deklaracji zmiennych: char klasa='B'; char dolar='$';.
Ponieważ znaki przechowywane są jako wartości liczbowe, więc dopuszczalne są przypisania:
char klasa=65; char dzwiek=7;
Ponieważ niektóre znaki nie są dostępne bezpośrednio z klawiatury lub są znakami specjalnymi do obsługi
znaków i łańcuchów (apostrof, cudzysłów), więc wymyślono sekwencje sterujące, których używa się
zazwyczaj w łańcuchach:
char nowa_linia='\n'; char dzwiek='\a'; char apostrof='\'';
char napis[]=''Don\'t worry.\nBe happy!\n\a'';
Można również używać kodu ósemkowego (\0oo) i szesnastkowego (\xhh):
char dzwiek='\007'; char A='\x41'; char pytajnik='\x3f';
Typy zmiennoprzecinkowe
Wartości zmiennoprzecinkowe udają liczby, które w matematyce nazywa się rzeczywistymi. Liczby te
można zapisywać używając kropek, które oddzielają część ułamkową:
double nu1=-8.5; float nu2=.24; long double nu3=3.0;
lub stosując notację wykładniczą:
0
|
1
|
0
|
0
|
1
|
1
|
0
|
1
0
|
0
|
0
|
0
|
0
|
0
|
0
|
1
|
1
|
0
|
0
|
0
|
1
|
1
|
0
|
1
11
double nw1=1.0e9; float nw2=-1.245E-16; long double nw3=.425E328;
W notacji tej po literze E (lub e) występuje wartość wykładnika potęgi liczby dziesięć.
Notacja naukowa
Notacja wykładnicza
1,5·10
8
-1,5·10
32
8,45·10
-18
9,4285·10
27
1.5E8
-1.5E32
8.45E-18
9.4285E27
Parametrem najbardziej istotnym dla typów zmiennoprzecinkowych jest ilość cyfr znaczących, która
wyznacza dokładność obliczeń. Drugim parametrem jest zakres wartości wykładników potęgi.
Standard ANSI C gwarantuje, że typ float potrafi wyrazić co najmniej sześć cyfr znaczących i dopuszcza
zakres wartości co najmniej od 10
-37
do 10
37
. Zakres wartości typu double musi być przynajmniej taki jak
float, a ilość cyfr znaczących większa lub równa 10. Typ long double powinien posiadać większą precyzję
od double.
Standardowo kompilator przyjmuje, że stałe zmiennoprzecinkowe są wartościami o podwójnej precyzji
(double). Aby stałe były innego typu trzeba dodać przyrostek F (lub f) dla typu float i L (lub l) dla typu long
double.
Dyrektywy preprocesora, pliki nagłówkowe
Preprocesor zajmuje się programem, zanim jeszcze rozpocznie się kompilacja. W oparciu o dyrektywy
zamienia on obecne w programie symboliczne skróty na ich definicje. Preprocesor potrafi także dołączać
wybrane pliki oraz określać, które części kodu będą widoczne dla kompilatora. Dyrektywy preprocesora
obejmują obszar od symbolu # do najbliższego znaku końca linii. Umieszczenie przez znakiem nowej linii
lewego ukośnika umożliwia kontynuację dyrektywy w następnej linii, kombinacja lewy ukośnik+znak nowej
linii nie występuje wtedy w dyrektywie. Przed i za znakiem # mogą występować tabulatory i odstępy.
Dyrektywa może znajdować się w dowolnym miejscu kodu, a zawarte w niej definicje obowiązują do końca
pliku.
Dyrektywa #include dołącza do treści programu zawartość innych plików:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include “prototypy.h”
#include “../gacek/moje_funkcje.c”
#include “/home/gacek/moje_funkcje.c”
Zwyczajowo pliki z rozszerzeniem “.h” zawierają elementy nagłówkowe, a pliki z rozszerzeniem “.c”
również definicje funkcji. Nawiasy trójkątne informują preprocesor, że ma szukać pliku w znanych sobie
miejscach (najczęściej w /usr/include), a treść ujęta w cudzysłowy traktowana jest jako ścieżka systemowa.
W plikach nagłówkowych można spotkać stałe symboliczne, które pozwalają uniezależnić program od
specyfikacji konkretnego systemu. Przykładem są tu pliki nagłówkowe limits.h i float.h, które dostarczają
szczegółowych informacji o zakresach typów całkowitych i zmiennoprzecinkowych.
Wybrane stałe symboliczne z pliku limits.h.
Stała
Znaczenie
CHAR_BIT
Rozmiar typu char w bitach
CHAR_MAX
Maksymalna wartość typu char
CHAR_MIN
Minimalna wartość typu char
UCHAR_MAX Maksymalna wartość typu unsigned char
SCHAR_MAX Maksymalna wartość typu signed char
SCHAR_MIN Minimalna wartość typu signed char
INT_MAX
Maksymalna wartość typu int
INT_MIN
Minimalna wartość typu int
UINT_MAX
Maksymalna wartość typu unsigned int
12
Odpowiednie stałe dla typów short i long są analogiczne jak dla int, z tym że zamiast ciągu INT występuje
odpowiednio SHRT i LONG.
Wybrane stałe symboliczne z pliku float.h.
Stała
Znaczenie
FLT_DIG
Minimalna liczba dziesiętnych cyfr znaczących dla typu float
FLT_MIN_10_EXP
Minimalny ujemny wykładnik typu float (podstawą potęgi jest 10)
FLT_MAX_10_EXP Maksymalny dodatni wykładnik typu float (podstawą potęgi jest 10)
FLT_MIN
Minimalna dodatnia wartość typu float
FLT_MAX
Maksymalna dodatnia wartość typu float
Odpowiednie stałe dla typów double i long double są analogiczne, z tym że zamiast ciągu FLT występuje
odpowiednio DBL i LDBL.
Dyrektywa #define składa się z trzech części: samej dyrektywy (#define), makra (skrótu) i treści. Nazwa
skrótu nie może zawierać odstępów i musi być zgodna z zasadami nazewnictwa zmiennych. Kiedy
preprocesor napotyka skrót, wtedy (w najprostszej wersji) zastępuje go treścią. Są pewne ograniczenia w
zastępowaniu wyrazów. Nie można zastąpić części nazwy identyfikatora lub słowa kluczowego. Rozpoczęty
łańcuch w treści dyrektywy musi być zakończony. Nie można zastąpić tekstu wewnątrz łańcucha.
#define begin int main(void){
#define end }
#define PR printf(“Drukuje znak: \
%c.\n”,znak)
~~~~~~~~~~~~~~~~~~~~~~ dyrektywy.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <limits.h>
#include <float.h>
#include "moje_naglowki.h"
#define CAL int
#define PR printf(
int main(void)
{
CAL max=SCHAR_MAX;
signed char zn;
float ziarenka, wynik;
printf(POLECENIE" (jego numer jest mniejszy od %d).\n",
SCHAR_MAX);
scanf("%c",&zn);
PR"Podaj liczbe ziarenek piasku na plazy.\n");
SC;
wynik=ziarenka*zn/SCHAR_MAX;
printf("Wybrales %f ziarenek piasku.\n", wynik);
PR"W wyniku tylko %d cyfr jest znaczaca.\n", FLT_DIG);
return 0;
}
~~~~~~~~~~~~~~~~~~~ moje_naglowki.h ~~~~~~~~~~~~~~~~~~~
#define SC scanf("%f", &ziarenka)
#define POLECENIE "Wymysl i wstukaj znak"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$dyrektywy
Wymysl i wstukaj znak (jego numer jest mniejszy od 127).
w
Podaj liczbe ziarenek piasku na plazy.
1.3E26
13
Wybrales 121811017678776805481250816.000000 ziarenek piasku.
W wyniku tylko 6 cyfr jest znaczaca.
$
Łańcuchy znakowe i formatowane wejście/wyjście
Łańcuchy znakowe
Łańcuch znakowy – ciąg składający się z jednego lub więcej znaków. Przykłady:
“Czulem sie jak
pies na lancuchu.”, “.”
.
Znaki cudzysłowu nie są częścią łańcucha. Język C nie posiada specjalnego typu łańcuchowego. Łańcuchy
przechowywane są w tablicach zbudowanych z elementów typu char, które tym różnią się od zwykłych
tablic, że po ostatnim elemencie łańcucha umieszczony jest znak zerowy ('\0', null character, NUL, ^@, znak
w kodzie ASCII o numerze 0).
każda komórka zajmuje jeden bajt
znak zerowy
Stałe łańcuchowe są to ciągi znaków otoczone znakami cudzysłowu. Zmienną łańcuchową deklaruje się,
deklarując tablicę znakową i pamiętając, że trzeba zarezerwować jedną komórkę dla znaku zerowego:
char pora_roku[7]; char kwiat[40];
Podczas deklaracji można zainicjować zmienną łańcuchową , jeśli nie określi się wtedy rozmiaru tablicy to
jej rozmiar będzie najmniejszym z możliwych:
char pora_roku[]=”jesien”;
- zostanie utworzona 7- elementowa tablica znakowa
char pora_roku[35]=”lato”;
- zostanie utworzona 35-elementowa tablica znakowa
Uwaga! Należy odróżniać znaki (np.
'a'
) od łańcuchów (np.
“a”
).
Funkcja strlen():
#include <string.h>
size_t strlen(const char *s);
zwraca długość łańcucha, na który wskazuje jej argument. size_t jest typem zwracanym przez operator
sizeof. Definicja języka C ogranicza się do stwierdzenia, że sizeof zwraca typ całkowity.
Słowo kluczowe const poprzedzające deklaracje zmiennej powoduję, że nie można zmieniać wartości tej
zmiennej. Gwiazdka (*) po nazwie typu oznacza, że zmienna jest wskaźnikiem do zmiennej danego typu.
Nazwa tablicy jest wskaźnikiem do jej pierwszego elementu. Stała łańcuchowa jest wskaźnikiem do
pierwszego elementu łańcucha.
Funkcje printf() i scanf()
Funkcje printf() i scanf() oraz ich pochodne: fprintf(), fscanf(), sprintf(), sscanf() umożliwiają wpisywanie
sformatowanych wartości zmiennych i stałych do strumienia lub przypisywanie zmiennym wartości
odczytanych ze strumienia. Rolę strumieni dla printf() i scanf() pełnią: standardowe wyjście (stdout, zwykle
ekran) i standardowe wejście (stdin, zwykle klawiatura). Funkcję printf() używa się w postaci:
#include <stdio.h>
int printf(const char *format, ...);
Powyższy prototyp jest tak zwanym prototypem częściowym. Używa się go dla funkcji o zmiennej liczbie
argumentów, gdzie liczba i typ części argumentów są nieznane. Łańcuch:
...
można czytać: nieznana
liczba argumentów o nieznanym typie.
Pierwszy argument funkcji printf() zwany jest łańcuchem sterującym. Składa się on ze znaków dosłownych
i specyfikatorów konwersji. Specyfikatory konwersji poprzedzone są znakiem
%
. Kolejnymi argumentami są
dane różnych typów. Każdej danej powinien odpowiadać jeden specyfikator z łańcucha sterującego
odpowiadający typowi danej. Wyjątkiem jest konstrukcja
%%
wyświetlająca znak
%
.
Specyfikatory konwersji (formatu) i odpowiadające im dane wyjściowe.
Specyfikator
konwersji
Dane wyjściowe
%c
Pojedynczy znak
%d, %i
Dziesiętna liczba całkowita (ze znakiem)
%u
Dziesiętna liczba całkowita (bez znaku)
%e, %E
Liczba zmiennoprzecinkowa w notacji e, E
C
|
z
|
u
|
l
|
e
|
m
|
|
s
|
i
|
e
|
|
j
|
a
|
k
|
|
p
|
i
|
e
|
s
|
|
n
|
a
|
|
l
|
a
|
n
|
c
|
u
|
c
|
h
|
u
|
.
|
\0
14
Specyfikator
konwersji
Dane wyjściowe
%f
Liczba zmiennoprzecinkowa w zapisie dziesiętnym
%g, %G
Równoważny
%e
(
%E
), jeśli wykładnik jest mniejszy niż -4 lub większy bądź
równy dokładności. W przeciwnym wypadku równoważny
%f
.
%o
Ósemkowa liczba całkowita (bez znaku)
%p
Wskaźnik
%s
Łańcuch znakowy
%x, %X
Szesnastkowa liczba całkowita, cyfry szesnastkowe pisane małą, wielką literą
Działanie podstawowych specyfikatorów konwersji można zmieniać, wstawiając pomiędzy znak
%,
a literę
określającą format tzw. modyfikator. W przypadku stosowania kilku modyfikatorów na raz, należy je
zapisać w tej samej kolejności, w jakiej przedstawiono je w tabeli poniżej, przy czym nie wszystkie
kombinacje modyfikatorów są możliwe.
Modyfikatory funkcji printf().
Modyfikator
Znaczenie
znacznik
Pięć dostępnych znaczników (-, +, odstęp, # i 0) jest opisane w kolejnej tabeli.
Można stosować kilka znaczników na raz.
liczba
Minimalna szerokość pola. Jeśli wyświetlana wartość lub łańcuch nie zmieści się
w polu o podanej szerokości, zostanie użyte większe pole.
.liczba
Dokładność. Dla
%e
,
%E
i
%f
jest to liczba miejsc po przecinku, dla
%g
i
%G
maksymalna liczba cyfr znaczących, dla s maksymalna liczba wyświetlanych
znaków, dla konwersji całkowitych minimalna liczba wyświetlanych cyfr (w razie
potrzeby uzupełniana zerami). “.” to tyle co “.0” (np.
%.f
to
%.0f
).
h
W połączeniu ze specyfikatorem całkowitym oznacza wartość short int lub
unsigned short int. Przykłady:
%hu, %5.3hd
.
l
W połączeniu ze specyfikatorem całkowitym oznacza wartość long int lub
unsigned long int. Przykłady:
%7ld, %lu
.
L
W połączeniu ze specyfikatorem zmiennoprzecinkowym oznacza wartość long
double. Przykłady:
%Lf, %9.4Le
.
W C99
ll
to modyfikator specyfikatora całkowitego oznaczający wartość typu long long (lub bez znaku).
Dla typu float nie jest wymagany modyfikator, ponieważ wartości tego typu są konwertowane do double.
Znaczniki funkcji printf().
Znacznik
Znaczenie
-
Wyrównuje wartość do lewej krawędzi pola. Przykład:
%-15s
.
+
Wyświetla wartość ze znakiem plus lub minus, w zależności od tego, czy jest
dodatnia czy ujemna. Przykład:
%+6.2f
.
odstęp
Wyświetla wartość z odstępem na początku (ale bez znaku plus), jeśli jest dodatnia, a
ze znakiem minus, jeśli jest ujemna. Przykład:
% 6.2f
.
#
Wyświetla wartość w postaci alternatywnej, zależnej od specyfikatora. Poprzedza
wartości typu
%o
zerem, a wartości typu
%x
i
%X
– odpowiednio symbolem 0x lub
0X. W przypadku wszystkich formatów zmiennoprzecinkowych, użycie znacznika #
zapewnia obecność kropki dziesiętnej, nawet jeśli nie następują po niej żadne cyfry.
W przypadku formatów
%g
i
%G
znacznik zapobiega usunięciu końcowych zer.
Przykłady:
%#o, %#9.0E, %+#7.0f
.
15
Znacznik
Znaczenie
0
W przypadku wartości liczbowych, wypełnia pole początkowymi zerami zamiast
odstępami. Znacznik jest ignorowany, jeśli występuje razem ze znacznikiem – lub
jeśli dla wartości całkowitej określona została dokładność. Przykłady:
%010d, %
08.3f
.
Funkcja printf() zwraca liczbę wyświetlonych znaków, a w przypadku wystąpienia błędu wartość ujemną.
Nie można podzielić na kilka wierszy stałej łańcuchowej. Aby było możliwe wyświetlenie długiego tekstu
w kilku linijkach za pomocą instrukcji printf() można albo podzielić łańcuch na dwa:
printf(“Jestem pierwszym lancuchem,”
“a ja drugim, ale naprawde jestesmy jednym.\n”);
lub (jak przy instrukcji #define) zastosować kombinację lewy ukośnik-znak nowego wiersza:
printf(“Jestem zbyt dlugim lancuchem i nie mieszcze sie na ekranie w\
jednej linijce, dlatego mnie podzielono\n”);
Funkcja scanf():
#include <stdio.h>
int scanf(const char *format, ...);
przetwarza tekst wejściowy na wartości różnych typów. Pierwszym jej argumentem, podobnie jak w printf
(), jest łańcuch sterujący, a następnymi wskaźniki do zmiennych. Adresy zmiennych prostych uzyskujemy
dopisując przed nazwą zmiennej jednoargumentowy operator &. Nazwa zmiennej łańcuchowej (ogólnie
tablicy) jest adresem tej zmiennej (ściśle: jest adresem jej pierwszego elementu) i dlatego nie stosuje się w
tym wypadku operatora &.
Aby podzielić łańcuch wejściowy na pojedyncze pola (wartości) funkcja scanf() wykorzystuje znaki
niedrukowalne (nowa linia, tabulator, odstęp). Dopasowuje ona kolejne specyfikatory konwersji do
kolejnych pól, pomijając wszystkie znaki niedrukowalne znajdujące się pomiędzy nimi. Wyjątkiem od tej
zasady jest specyfikator
%c
, który powoduje odczytanie jednego znaku, nawet jeśli jest to znak
niedrukowalny.
Specyfikatory konwersji dla funkcji scanf().
Specyfikator
Interpretuje daną wejściową jako:
%c
znak.
%d, %i, %u
dziesiętną liczbę całkowitą.
%f, %e, %
E, %g, %G
liczbę zmiennoprzecinkową.
%o
ósemkową liczbę całkowitą.
%p
wskaźnik (adres).
%s
łańcuch, rozpoczynający się pierwszym znakiem drukowanym i kończącym się
ostatnim takim znakiem.
%x, %X
szesnastkową liczbę całkowitą.
Między znakiem procentu a literą określającą rodzaj konwersji można umieścić modyfikatory. Kiedy
używa się kilku modyfikatorów, wtedy należy umieścić je w kolejności jak w poniższej tabeli.
Modyfikatory funkcji scanf().
Modyfikator
Znaczenie
*
Powstrzymuje odczytanie wartości. Przykład:
%*f
.
liczba
Maksymalna szerokość pola. Odczytywanie danych kończy się, gdy osiągnięta
zostanie maksymalna szerokość pola lub w momencie wystąpienia pierwszego
znaku niedrukowalnego. Przykład:
%9s
.
h
Powoduje zapisanie wartości w zmiennej typu short int (specyfikatory d, i) lub
unsigned short int (specyfikatory o, x, u). Przykłady:
%ho, %hd
16
Modyfikator
Znaczenie
l
Powoduje zapisanie wartości w zmiennej typu long (specyfikatory d, i), unsigned
long (specyfikatory o, x, u) lub double (specyfikatory e, f, g). Przykład:
%le
L
Powoduje zapisanie wartości w zmiennej typu long double (specyfikatory e, f, g).
Specyfikatory e, f i g bez modyfikatorów l lub L oznaczają typ float. W C99 L jest modyfikatorem, który
powoduje zapisanie wartości w zmiennej long long (specyfikatory d, i) lub unsigned long long
(specyfikatory o, x, u).
Funkcja scanf() zwraca liczbę poprawnie odczytanych pozycji. Jeśli wykryty zostanie koniec pliku scanf()
zwraca EOF (specjalną wartość zdefiniowaną w stdio.h).
~~~~~~~~~~~~~~~~~~~~~~~ lancuchy.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#define KIERUNEK "poludnie"
int main(void)
{
char baza[]="wsi";
char do_babci[8];
float waga_Kapturka=45.5;
int dzem;
unsigned jajka=11;
dzem=3;
baza[0]='A';
printf("Droga od %s idzie Czerwony Kapturek.\n", baza);
printf("Ide do babci, powiedzial Czerwony Kapturek"
" wilkowi.\nA w jakim to kierunku?\n");
scanf("%s", do_babci);
printf("Wygladasz na duza dziewczynke (jakies %6.2fkg).\n",
waga_Kapturka);
printf("Czy niesiesz %d sloik dzemu, %u jajek ...?\n",1,6);
scanf("%d %u",&dzem,&jajka);
printf("Niose %+.3d sloikow dzemu, %x jajek ...\n",dzem,jajka);
printf("Wilk idzie na %5.4s., a Kapturek na *%-6.4s.*\n",
do_babci, KIERUNEK);
printf("%dkm na %s i %dkm na %s.\n",
strlen(do_babci), do_babci, strlen(KIERUNEK), KIERUNEK);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$lancuchy
Droga od Asi idzie Czerwony Kapturek.
Ide do babci, powiedzial Czerwony Kapturek wilkowi.
A w jakim to kierunku?
zachod
- wprowadzanie danych
Wygladasz na duza dziewczynke (jakies 45.50kg).
Czy niesiesz 1 sloik dzemu, 6 jajek ...?
2 12
- wprowadzanie danych
Niose +002 sloikow dzemu, c jajek ...
Wilk idzie na zach., a Kapturek na *polu .*
6km na zachod i 8km na poludnie.
$
17
Operatory, wyrażenia, instrukcje
Operator przypisania: =
Obiekt danych – obszar pamięci, który może zostać użyty do przechowania wartości (np. zmiennej lub
tablicy).
l-wartość – (ang. lvalue) nazwa lub wyrażenie określające obiekt danych.
modyfikowalna l-wartość – (ang. modifiable lvalue) l-wartość, która określa obiekt danych mogący
zmieniać wartość.
r-wartość – (ang. rvalue) wartość, która może zostać przypisana modyfikowalnej l-wartości.
Po lewej stronie operatora przypisania musi znajdować się modyfikowalna l-wartość, a po prawej r-
wartość.
Skutkiem ubocznym (ang. side effect) nazywamy każdą modyfikację obiektu danych lub pliku. Głównym
zamiarem języka C jest obliczanie wyrażeń. Wartością wyrażenia składającego się z operatora przypisania
jest wartość jego prawego operandu. Na przykład skutkami ubocznymi instrukcji:
rok=year=2003;
są przypisania do zmiennych rok i year wartości 2003. Jednocześnie wartością wyrażenia year=2003 jest
2003, podobnie jak wyrażenia rok=year=2003.
Operatory dodawania i odejmowania: +, -
Operator dodawania (+) powoduje dodanie do siebie wartości znajdujących się po jego obydwu stronach.
Operator odejmowania (-) powoduje odjęcie wartości po prawej stronie operatora od wartości po lewej.
Operatory te należą do operatorów dwuargumentowych, co oznacza, że wymagają dwóch operandów.
Operatory znaku: +, -
Kiedy znaki + i – dotyczą jednego operandu, wtedy występują w roli operatorów jednoargumentowych
zmiany algebraicznego znaku wartości. Przykłady: zima=-15; wiosna=-zima; lato=+wiosna+(-zima);.
Operatory mnożenia i dzielenia: *, /
Operator mnożenia (*) powoduje pomnożenie wartości znajdujących się po jego obydwu stronach.
Operator dzielenia (/) powoduje podzielenie wartości po lewej stronie operatora przez wartość po prawej.
Kiedy przynajmniej jeden operand jest liczbą zmiennoprzecinkową, wtedy wynik dzielenia jest liczbą
zmiennoprzecinkową, a dzielenie przebiega zwyczajnie. W wypadku gdy obydwa operandy są liczbami
całkowitymi wynik jest liczbą całkowitą powstałą przez odrzucenie części ułamkowej ilorazu.
~~~~~~~~~~~~~~~~~~~~~~ dzielenie.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
printf("9/4 to %d, -9/4 to %d\n", 9/4, -9/4);
printf("5/2 to %d, 5/-2 to %d\n", 5/2, 5/-2);
printf("8/3 to %d, -8/3 to %d\n", 8/3, -8/3);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$dzielenie
9/4 to 2, -9/4 to -2
5/2 to 2, 5/-2 to -2
8/3 to 2, -8/3 to -2
$
Jest to najczęściej spotykana implementacja. Inna implementacja mówi, że rezultatem obcięcia ilorazu jest
największa liczba całkowita równa temu ilorazowi bądź od niego mniejsza. Różnica pojawia się, kiedy jeden
z operandów jest liczbą ujemną. Na przykład w drugiej implementacji: -9/4=-3, -5/2=-3, -8/3=-3, -8/4=-2.
Aby uniknąć różnic ANSI C wprowadził funkcję div(), która realizuje pierwszy sposób.
Operator modulo: %
Argumentami operatora modulo (%) mogą być wyłącznie liczby całkowite. Zwraca on resztę powstałą w
wyniku podzielenia wartości po jego lewej stronie przez wartość po jego prawej stronie. Przykłady: 9%4=1,
-9%4=-1, 8%3=2, -8%3=-2. W drugiej implementacji dzielenia liczb całkowitych -11%5=4, bo różnica
między -2.2 a -3 wynosi 4/5 (zwyczajnie -11%5=-1).
18
Priorytet i kolejność obliczeń
Kolejność wykonywania działań wpływa na wynik. Aby wynik był jednoznacznie określony wprowadza
się dwie cechy operatorów. Priorytet informuje o tym, które działanie jest wykonywane wcześniej, a które
później. Operacje o wyższym priorytecie są wykonywane wcześniej. Operatory, które mają ten sam priorytet
(także identyczne operatory) i wspólny operand działają w kolejności wyznaczanej przez kierunek wiązania.
Operatory języka C.
Operatory (od najwyższego priorytetu)
Kierunek wiązania
++ -- (przyrostki) ( ) [ ] . ->
L-P
++ -- (przedrostki) - + (jednoargumentowe) ~ !
P-L
sizeof * (dereferencja) & (adres)
P-L
(typ) (jednoargumentowy)
P-L
* / %
L-P
+ - (dwuargumentowe)
L-P
<< >>
L-P
< > <= >=
L-P
== !=
L-P
&
L-P
^
L-P
|
L-P
&&
L-P
||
L-P
? : (operator warunkowy)
P-L
= *= /= %= += -= <<= >>= &= |= ^=
P-L
, (przecinek)
L-P
W wyrażeniu 7*24+3*78 pierwsze zostaną obliczone iloczyny 7*24 i 3*78, a potem zostanie wykonane
dodawanie. Który z powyższych iloczynów zostanie obliczony pierwszy? Czyżby zgodnie z kierunkiem
wiązania 7*24, a potem 3*78? Nie, ponieważ kierunek wiązania dotyczy operatorów, które mają wspólny
operand. Twórcy kompilatorów mają w tym wypadku wolną rękę. Natomiast w wyrażeniu 14/7*2 kolejność
jest określona. Najpierw zostanie obliczony iloraz, a potem iloczyn (wynik wyniesie 4).
~~~~~~~~~~~~~~~~~~~~~~ wiazanie.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
unsigned a=2;
unsigned b,b_p_l;
unsigned c,c_p_l;
b=a*5%10;
b_p_l=a*(5%10);/* Gdyby wiazane bylo P-L */
c=3/2/2;
c_p_l=3/(2/2); /* Gdyby wiazane bylo P-L */
printf("%u %u\n", b, b_p_l);
printf("%u %u\n", c, c_p_l);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$wiazanie
19
0 10
0 3
$
Operator sizeof
Zwraca rozmiar operandu w bajtach. Operand może być konkretnym obiektem danych (np. zmienną) lub
typem. Kiedy operand jest typem, to musi znajdować się w nawiasach okrągłych. Operator zwraca wartość
typu size_t, którego definicja ogranicza się do stwierdzenia, że powinien być typem całkowitym, dlatego
niektóre kompilatory mogą wymagać specyfikatora %ld zamiast %d. sizeof jest słowem kluczowym.
Przykłady: sizeof(int), sizeof(long double), sizeof "Lancuch", sizeof 6.7, sizeof(6.7), sizeof zmienna.
Operatory relacyjne: <, <=, ==, >=, >, !=
Operator relacyjny porównuje wartości wyrażeń, które występują po obu jego stronach. Wartością
wyrażenia relacyjnego jest 1, kiedy porównanie jest prawdziwe, a 0, kiedy jest fałszywe. Operator <
sprawdza czy wyrażenie z lewej strony jest mniejsze od wyrażenia z prawej, <= czy jest mniejsze bądź
równe, == czy jest równe, >= czy jest większe bądź równe, > czy jest większe, != czy jest różne.
W języku C wszystkie wartości mogą mieć znaczenie logiczne. Fałszem jest tylko wartość równa 0.
Przykłady: jablka=4+5>gruszki++, 7==9, piernik!=wiatrak.
Operatory logiczne: &&, | | , !
Operator && (iloczyn logiczny) daje wartość iloczynu logicznego wyrażeń stojących po obu jego stronach.
Wartość ta jest prawdą wtedy i tylko wtedy, gdy oba wyrażenia są prawdziwe. Operator || (suma logiczna)
daje wartość sumy logicznej wyrażeń stojących po obu jego stronach. Wartość ta jest prawdą wtedy i tylko
wtedy, gdy przynajmniej jedno wyrażenie jest prawdziwe. Jednoargumentowy operator ! (zaprzeczenie
logiczne) zmienia na odwrotną wartość wyrażenia logicznego, przy którym występuje.
Przykłady: !(5<8) jest równoważne 5>=8, c<'\n' && 31!=++licz, akwarium || rybki.
Operatory inkrementacji i dekrementacji: ++ i --
Operator inkrementacji zwiększa wartość operandu o 1, a dekrementacji zmniejsza o tę samą wartość.
Operator inkrementacji występuje w dwóch formach: przedrostkowej i przyrostkowej, które różnią się
momentem dokonywania zwiększenia. Forma przedrostkowa najpierw zwiększa wartość operatora, który
następnie używany jest w wyrażeniu, a forma przyrostkowa najpierw umożliwia użycie operatora w
wyrażeniu, a potem zwiększa go. Podobnie jest dla operatora dekrementacji, tylko zamiast zwiększania
występuje zmniejszanie operatora. Reguły mnemotechniczne: ++zm – dodaj, użyj; zm++ - użyj, dodaj.
Przykłady: int ciastka=5; ciastka++<6; /*prawda*/ --ciastka; ++ciastka<6; /*fałsz*/, int poludnie,zegar=12;
poludnie=zegar++.
Konwersja typów
Instrukcje i wyrażenia powinny w zasadzie korzystać ze zmiennych i stałych należących do jednego typu,
ale często jest to niemożliwe. Kompilatory języka C dokonują automatycznych konwersji typów, aby móc
obliczyć wartości wyrażeń. Oto niektóre z zasad automatycznej konwersji typów (istotne lub nieistotne dla
programisty):
–
zmienne typów char i short użyte w wyrażeniu są przetwarzane na typ int lub unsigned int,
–
w operacji wykorzystującej dwa typy obie wartości są wyrównywane do typu o wyższej randze,
–
wyższe rangą są typy zmiennoprzecinkowe i o większej możliwej wartości,
–
w instrukcji przypisania wynik obliczeń jest przetwarzany na typ zmiennej, której nadawana jest wartość,
–
typy zmiennoprzecinkowe ulegające degradacji do typów całkowitych są zawsze obcinane, tzn.
wyrzucana jest część ułamkowa zarówno dla wartości dodatnich jak i ujemnych.
~~~~~~~~~~~~~~~~~~~~~~~ konwers.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
char ch;
int i;
float fl;
unsigned un;
ch=i=fl='C';
printf("%c %d %.2f\n", ch, i, fl);
i=fl+3.4+2*'%';
20
ch=fl/2+35;
fl='C'/2-(ch>i);
printf("%c %d %.2f\n", ch, i, fl);
un=-9; ch=51243.756;
printf("%d %u %c\n", un, un, ch);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$konwers
C 67 67.00
D 144 33.00
-9 4294967287 +
$
Operator rzutowania: (typ)
Operator rzutowania zamienia typ argumentu na typ wyszczególniony w operatorze.
Przykłady: (double)'B', (int) 4.6, (char)67.45, (unsigned)-9.
Inne operatory przypisania: +=, -=, *=, /=, %=
Operatory te służą do aktualizacji zmiennych. Zmienna znajdująca się z lewej strony operatora jest
modyfikowana wartością wyrażenia z prawej. Do zmiennej dodawane jest wyrażenie (+=), odejmowane jest
od niej wyrażenie (-=), mnożona jest przez wyrażenie (*=), dzielona przez wyrażenie (/=), do zmiennej
zapisywana jest wartość zmienna modulo wyrażenie (%=).
Przykłady: x*=y/5+12 jest równoważne x=x*(y/5+12), z%=23+i/5 jest równoważne z=z%(23+i/5), z-=12
jest równoważne z=z-12.
~~~~~~~~~~~~~~~~~~~~~~ operatory.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
int bs1=sizeof(long double), bs2=0;
int bs3=sizeof bs3, bs4=0;
int logika, roz;
double a,b,c;
printf("Przed przelaniem: %d %d %d %d\n",bs1,bs2,bs3,bs4);
while(--bs1) bs2++; //przelewanie
while(bs3--) ++bs4; //przelewanie
printf("Po przelaniu: %d %d %d %d\n",bs1,bs2,bs3,bs4);
logika=(roz=sizeof((long double)bs3)) < sizeof bs2 && 0 || !0;
/* && ma wyzszy priorytet niz || */
printf("logika=%d roz=%d\n", logika, roz);
a=5; b=4; c=3; a*=a; b/=4; c+=(int)c==((int)b+2);
printf("a=%.1lf b=%.1lf c=%.1lf\n",a,b,c);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$operatory
Przed przelaniem: 12 0 4 0
Po przelaniu: 0 11 -1 4
logika=1 roz=12
a=25.0 b=1.0 c=4.0
$
21
Wyrażenia i instrukcje
Wyrażenie jest kombinacją operatorów i operandów, a najprostsze składa się z samego operandu (np. 4). W
języku C każde wyrażenie ma wartość. Wartość wyrażeń zawierających operator przypisania jest taka sama
jak wartość, którą otrzymuje zmienna z lewej strony (wyjątek może się pojawić, kiedy obecny jest operator ,
(przecinek)). Wyrażenia relacyjne mają wartość 1, kiedy są prawdziwe, a 0 jeśli są fałszywe.
Instrukcje proste kończą się znakiem średnika. Instrukcją złożoną lub blokiem nazywamy przynajmniej
dwie instrukcje scalone ze sobą przez zawarcie ich w klamrach. Przykład bloku:
while(i++<5) { s=i*i*i; printf("%d do szescianu to %d\n", i, s); }
Punkt sekwencyjny jest momentem w trakcie działania programu, w którym zrealizowane zostały
wszystkie skutki uboczne. Punktem sekwencyjnym jest koniec pełnego wyrażenia, tzn. takiego które nie jest
częścią większego wyrażenia. Poza tym punkty sekwencyjne są częścią niektórych operatorów.
Instrukcje sterujące: Pętle
Pętla while
Pętla while jest pętlą z warunkiem wejścia, tzn. że warunek testujący zostanie sprawdzony jeszcze przed
wejściem do pętli. Budowa pętli while:
instrukcja_przed_pętlą;
while (warunek_testujący) instrukcja_pętli;
instrukcja_za_pętlą;
Po napotkaniu pętli while sprawdzany jest warunek_testujący. Kiedy jest on prawdziwy, wtedy
wykonywana jest instrukcja_pętli i program wraca na początek pętli, a kiedy jest fałszywy, wtedy
wykonywana jest instrukcja_za_pętlą.
Przykłady:
while(i++<10) printf("%d do kwadratu to %d\n", i, i*i);
while(i<10) { i++; printf("%d do kwadratu to %d\n", i, i*i);}
Operator przecinkowy: ,
Operator przecinkowy jest punktem sekwencyjnym rozdzielającym wyrażenia. Gwarantuje on, że
wyrażenia będą obliczane od lewej do prawej. Wartością całego wyrażenia zawierającego przecinek jest
wartość jego prawostronnej części. Operator ten jest najczęściej używany w pętlach (głównie w pętli for).
Przykłady:
while(i++, printf("%d do kwadratu to %d\n", i, i*i), i<10);
while(scanf("%c", &ch), ch!='#') printf("To nie ten znak\n");
x=(z+=4, (w=--y-10+z) +9);
pi=3,14;
pi=(3,14);
W przedostatnim przykładzie zmienna pi przyjmie wartość 3, a całe wyrażenie będzie miało wartość 14. W
ostatnim przykładzie zmienna pi przyjmie wartość 14 i całe wyrażenie będzie miało wartość 14. W
pierwszym przykładzie pokazano ponadto użycie instrukcji pustej.
W języku C przecinek jest również używany jako znak rozdzielający, a więc przecinki w instrukcjach:
int n, i;
printf("masa=%f predkosc=%f energia=%f\n", m, v, m*v*v/2);
są znakami rozdzielającymi, a nie operatorami.
Pętla for
Duża liczba pętli to pętle liczące, w których liczba powtórzeń jest z góry określona. Z działaniem takich
pętli związane są trzy czynności: inicjalizacja licznika, porównanie licznika z wartością ograniczającą,
zwiększanie licznika przy każdym powtórzeniu pętli. Pętla for skupia te trzy czynności w jednym miejscu.
Budowa pętli for:
instrukcja_przed_pętlą;
for (instrukcja_wstępna; warunek_testujący; instrukcja_powtarzana) instrukcja_pętli;
instrukcja_za_pętlą;
Po napotkaniu instrukcji for wykonywana jest instrukcja_wstępna, a następnie sprawdzany jest
warunek_testujący. Jeżeli warunek_testujący jest prawdziwy, to wykonywana jest instrukcja_pętli, a potem
instrukcja_powtarzana i ponownie sprawdzany jest warunek_testujący. Jeżeli warunek_testujący jest
22
fałszywy, to wykonywana jest instrukcja_za_pętlą.
Przykłady:
for(i=0; i<10; i++) printf("%d sekund!\n", i);
for(n=0, c='\0'; n<2000 && c!='#';scanf("%d", &k), c=k) {n+=c; printf("%d %c\n", n, c);}
for(n=32; n<127; n++) printf("%c ",(char)n);
Tablice (wstęp)
Tablica jest ciągiem wartości tego samego typu. Deklarując tablicę określa się liczbę jej elementów, dla
których zostanie zarezerwowana pamięć:
float temp[365];
/* zostanie zarezerwowane miejsce dla 365 wartości typu float */
char znaki[127];
/* zostanie zarezerwowane miejsce dla 127 wartości typu char */
unsigned long ziarenka[231]; /* zostanie zarezerwowane miejsce dla 231 wartości typu unsigned long */
Tablica ma jedną nazwę. Dostęp do poszczególnych elementów tablicy uzyskujemy przez podanie zaraz za
nazwą tablicy indeksu ujętego w nawiasy kwadratowe. Indeks jest zawsze liczbą naturalną. Pierwszy
element indeksowany jest liczbą 0. Przykłady:
int odl[4], odlx;
odl[0] – pierwszy element tablicy, odl[1] – drugi element tablicy,
odl[2] – trzeci element tablicy, odl[3] – czwarty (ostatni) element tablicy,
odl[2]=125; - trzeciemu elementowi tablicy przypisano wartość 125.
odl[3]=203; - ostatniemu elementowi tablicy przypisano wartość 203.
odlx=odl[3]; - zmiennej odlx przypisano wartość czwartego elementu tablicy odl.
~~~~~~~~~~~~~~~~~~~~~~~ tablice.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#define LICZBA_M 3
int main(void)
{
int i;
float miast[LICZBA_M];
float suma, srednia;
printf("Podaj ilosc mieszkancow %d miast.\n", LICZBA_M);
for(i=0;i<LICZBA_M;i++) scanf("%f", &miast[i]);
printf("Podales wartosci:\n");
for(suma=0,i=0;i<LICZBA_M;i++)
{printf("%g\n", miast[i]); suma+=miast[i];}
srednia=suma/LICZBA_M;
printf("Suma tych wartosci wynosi: %g,\n",suma);
printf("a ich srednia: %g\n",srednia);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$tablice
Podaj ilosc mieszkancow 3 miast.
234000
23900
1674000
Podales wartosci:
234000
23900
1.674e+06
Suma tych wartosci wynosi: 1.9319e+06,
a ich srednia: 643967
$
23
Pętla do while
Język C udostępnia pętlę z warunkiem wejścia, która gwarantuje, że zawartość pętli zostanie wykonana
przynajmniej jeden raz. Budowa pętli do while:
instrukcja_przed_pętlą;
do instrukcja_pętli; while (warunek_testujący);
instrukcja_za_pętlą;
Po napotkaniu pętli do while wykonywana jest instrukcja_pętli, a następnie sprawdzany jest
warunek_testujący. Jeżeli jest on prawdziwy, to następuje powrót na początek pętli, w przeciwnym wypadku
zostaje wykonana instrukcja_za_pętlą. Jeżeli instrukcja_pętli jest prosta, to kończy się średnikiem. Pętla do
while jest instrukcją, w związku z czym na jej końcu musi znajdować się średnik.
Przykłady:
do scanf("%d", &n); while(n!=5);
do {scanf("%c%c", &ch, &new_line); printf("%c\n", ch);} while(ch!='#');
~~~~~~~~~~~~~~~~~~~~~~~ do_while.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <string.h>
int main(void)
{
int h;
char st[50];
do {
scanf("%s", st); h=strlen(st);
while(--h>=0) printf("%c", st[h]); printf("\n");
}
while(st[0]!='*');
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$do_while
arbuz
zubra
kici koci
icik
icok
*koniec
ceinok*
$
Pętle zagnieżdżone
Pętlą zagnieżdżoną nazywamy pętlę znajdującą się w obrębie innej pętli. Pętlę taką nazywamy także pętlą
wewnętrzną, a pętlę w obrębie której się ona znajduje nazywa się pętlą zewnętrzną. Obie pętle mogą być od
siebie niezależne (tzn. że działanie jednej pętli nie wpływa na działanie drugiej) lub zależne.
~~~~~~~~~~~~~~~~~~~~~~ petle_zag.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
int n,h,i;
char lan[50];
printf("Wpisz lancuch: "); scanf("%s", lan);
24
n=strlen(lan); h=n;
/* Petle zagniezdzone niezalezne */
while(n--) {
for(i=h-1;i>-1;i--) printf("%c",lan[i]);
printf("\n");
}
printf("Dzialanie petli zaleznych:\n");
/* Petle zagniezdzone zalezne */
while(h--) {
for(i=h;i>-1;i--) printf("%c",lan[i]);
printf("\n");
}
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$petle_zag
Wpisz lancuch: sama
amas
amas
amas
amas
Dzialanie petli zaleznych:
amas
mas
as
s
$
Biblioteka matematyczna
Funkcje (wstęp)
Definicja funkcji składa się z nagłówka funkcji i ciała funkcji. Nagłówek funkcji zawiera (zaczynając od
lewej) typ wartości zwracanej przez funkcję, nazwę funkcji i ujętą w nawiasy okrągłe listę parametrów
formalnych. Parametry formalne w liście oddzielone są przecinkami. Każdy parametr formalny składa się z
nazwy typu argumentu i nazwy samego argumentu. Kiedy funkcja nie zwraca wartości lub lista parametrów
formalnych jest pusta, wtedy używa się słowa kluczowego void aby to zaznaczyć. W takim wypadku słowo
void zastępuje wartość zwracaną przez funkcję lub zastępuje listę parametrów formalnych. Używanie typu
wartości zwracanej przez funkcję jest zalecane przez ANSI C, jego brak oznacza, że funkcja zwraca wartość
typu int. Brak słowa void w nagłówku funkcji nie posiadającej argumentów nie jest błędem.
Definicje funkcji mogą występować wewnątrz definicji innych funkcji (i są wtedy widziane tylko w funkcji
macierzystej), ale zasadniczo nie jest to stosowane.
Ciało funkcji zawarte jest w nawiasach klamrowych. Może ono zawierać wszystkie elementy obecne w
funkcji main(). Argumenty funkcji widziane są tam przez swoje nazwy. Funkcja kończy prawidłowo
działanie po wykonaniu swojej ostatniej instrukcji lub po wywołaniu instrukcji return bez parametrów, lub
po wywołaniu instrukcji return, po której następuje wartość (zgodna z typem wartości zwracanej przez
funkcję, o ile nie występuje automatyczne rzutowanie typu) ujęta w nawiasy okrągłe, lub bez nich.
Wszystkie funkcje przed użyciem powinny zostać zadeklarowane. Jeżeli brakuje takiej deklaracji, to
kompilatory potrafią sobie poradzić z tą sytuacją, o ile widzą definicję funkcji. Występuje wtedy tzw.
deklaracja niejawna na podstawie postaci nagłówka funkcji. Starsza forma deklaracji funkcji, dopuszczalna
w standardzie ANSI C, składa się z typu wartości zwracanej przez funkcję, po którym następuje nazwa
funkcji, a za nią puste nawiasy okrągłe i średnik kończący instrukcję. Taka postać deklaracji może
powodować błędy, ponieważ w wypadku wystąpienia niezgodności ilości lub typów argumentów wywołania
funkcji z występującymi w jej definicji, kompilator tego nie zauważy. W konsekwencji program może
działać nieprawidłowo. W celu zaradzenia temu problemowi standard ANSI C wprowadził nową formę
deklaracji funkcji zwaną prototypem funkcji. Prototyp funkcji składa się z typu wartości zwracanej przez
funkcję, po którym następuje nazwa funkcji, a zaraz za nią lista typów argumentów funkcji (za każdą nazwą
25
typu można dopisać identyfikator, pełniący rolę atrapy) ujęta w nawiasy okrągłe oraz średnik kończący
instrukcję. Funkcja może zostać zadeklarowana na zewnątrz funkcji wywołującej lub w jej wnętrzu. W tym
drugim przypadku deklaracje należy umieszczać w miejscach dozwolonych na deklarację zmiennych.
W programie funkcja wywoływana jest przez swoją nazwę, po której następuje lista argumentów funkcji
ujęta w nawiasy okrągłe (lista ta może być pusta, jeśli funkcja nie posiada argumentów). W miejscu
wywołania funkcji podstawiana jest zwracana przez nią wartość.
~~~~~~~~~~~~~~~~~~~~~~~ funkcje.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
double normalna(double a, int p);
dozwolona();
double normalna(double a, int p)
{
double pom=1.0;
int h;
for(h=0;h<p;h++) pom*=a;
return pom;
}
dozwolona()
{
int h=15;
return h;
}
int main(void)
{
int i=3;
double a=4.5,b;
int fun1();
b=normalna(a,i)+dozwolona();
printf("b= %5.2f, normalna(5,6)= %5.2f\n",
b, normalna(5,6)+fun1());
fun1();
return 0;
}
int fun1()
{
void fun2(void);
printf("Hej! ");
fun2();
return 8;
}
void fun2(void)
{
printf("Ho!\n");
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26
$funkcje
Hej! Ho!
b= 106.12, normalna(5,6)= 15633.00
Hej! Ho!
$
Biblioteka matematyczna
Dołączenie pliku math.h pozwala na używanie etykiet zdefiniowanych w tym pliku. Poniżej podano
niektóre z nich:
# define M_E 2.7182818284590452354 /* e */
# define M_LOG2E 1.4426950408889634074 /* log_2 e */
# define M_LOG10E 0.43429448190325182765 /* log_10 e */
# define M_LN2 0.69314718055994530942 /* log_e 2 */
# define M_LN10 2.30258509299404568402 /* log_e 10 */
# define M_PI 3.14159265358979323846 /* pi */
# define M_PI_2 1.57079632679489661923 /* pi/2 */
# define M_PI_4 0.78539816339744830962 /* pi/4 */
# define M_1_PI 0.31830988618379067154 /* 1/pi */
# define M_2_PI 0.63661977236758134308 /* 2/pi */
# define M_2_SQRTPI 1.12837916709551257390 /* 2/sqrt(pi) */
# define M_SQRT2 1.41421356237309504880 /* sqrt(2) */
# define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */
Używanie powyższych etykiet może przyspieszyć działanie programu, ale nie dokładność obliczeń,
ponieważ stałe te traktowane są jako stałe typy double. Żeby zwiększyć dokładność powyższych stałych
najlepiej skopiować je, zmienić nazwę etykiety i dodać na końcu liczby literę L.
Aby móc używać funkcji ze standardowej biblioteki matematycznej:
Standardowe funkcje matematyczne ANSI C.
Prototyp
Opis
double acos(double x)
Zwraca arcus cosinus x (od 0 do
p radianów).
double asin(double x)
Zwraca arcus sinus x (od -
p/2 do p/2 radianów).
double atan(double x)
Zwraca arcus tangens x (od -
p/2 do p/2 radianów).
double atan2(double y, double x) Zwraca kąt (od -
p do p radianów), którego tangens wynosi y/x.
double cos( double x)
Zwraca cosinus x.
double sin(double x)
Zwraca sinus x.
double tan(double x)
Zwraca tangens x.
double exp(double x)
Zwraca eksponent x (e
x
).
double log(double x)
Zwraca logarytm naturalny z x.
double log10(double x)
Zwraca logarytm o podstawie 10 z x.
double pow(double x, double y) Zwraca x do potęgi y.
double sqrt(double x)
Zwraca pierwiastek kwadratowy z x.
double ceil(double x)
Zwraca najmniejszą wartość całkowitą nie mniejszą niż x.
double fabs(double x)
Zwraca wartość bezwzględną z x.
double floor(double x)
Zwraca największą wartość całkowitą nie większą niż x.
należy skompilować program z opcją -lm oznaczającą dołączenie biblioteki matematycznej.
Kąty, jako wartości zwracane i argumenty powyższych funkcji, mierzone są w radianach. Definicja funkcji
tangens pozwala określić jednoznacznie wartość kąta dawaną przez funkcję odwrotną w zakresie kąta
pełnego, co jest niekiedy istotne w obliczeniach, dlatego zdefiniowano funkcję atan2().
Uwaga! Odpowiednikiem fabs() dla liczb całkowitych jest funkcja abs():
#include <stdlib.h>
27
int abs(int j);
Biblioteka C99 zawiera wersje powyższych funkcji przeznaczone dla typów float i long double. Nazwy
tych funkcji są utworzone przez dodanie do nazwy funkcji podstawowej przyrostka f lub l.
Przykłady: float sinf(float), long double sinl(long double).
~~~~~~~~~~~~~~~~~~~~~~ matematyka.c ~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <math.h>
#include <float.h>
#define M_SQRT2_L "1.41421356237309504880"
#define M_SQRT2_D 1.41421356237309504880L
double fi_min(double gam, double n21);
int main(void)
{
double kat,przel,a=M_PI_4;
long double b=M_PI_4;
float c=M_PI_4;
printf("%.*f %d\n",DBL_DIG+3,tan(a), DBL_DIG);
printf("%.*Lf %d\n",LDBL_DIG,tanl(b), LDBL_DIG);
printf("%.*f %d\n",FLT_DIG+3,tanf(c), FLT_DIG);
printf("Dokladnie:\n");
printf("%.*f %d\n",DBL_DIG,tan(a), DBL_DIG);
printf("%.*f %d\n",FLT_DIG,tanf(c), FLT_DIG);
printf("Dokladnosc, M_SQRT2:\n");
printf("%22s - lancuch\n", M_SQRT2_L);
printf("%.20Lf - stala long double\n", M_SQRT2_D);
printf("%.20Lf - z funkcji sqrtl()\n", sqrtl(2.0));
printf("%.20f - stala double\n", M_SQRT2);
printf("T=20stC, dl. fali=550nm, gam=20st\n");
przel=180.0/M_PI;kat=20.0/przel;
printf("Diament: %12.1fst\n", przel*fi_min(kat,2.423));
printf("Kwarc topiony: %6.1fst\n", przel*fi_min(kat,1.460));
printf("Szklo kron Zn: %6.1fst\n", przel*fi_min(kat,1.515));
return 0;
}
/* Zwraca minimalny k?t odchylenia swiatla padajacego */
/* na pryzmat o kacie lamiacym gam. n21 - wzgledny */
/* wspolczynnik zalamania swiatla materialu pryzmatu. */
double fi_min(double gam, double n21)
{
double pom;
pom=2*asin(n21*sin(gam/2))-gam;
return pom;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc matematyka.c -lm -o matematyka
$matematyka
0.999999999999999889 15
0.999999999999999939 18
1.000000044 6
Dokladnie:
28
1.000000000000000 15
1.000000 6
Dokladnosc, M_SQRT2:
1.41421356237309504880 - lancuch
1.41421356237309504876 - stala long double
1.41421356237309504876 - z funkcji sqrtl()
1.41421356237309514547 - stala double
T=20stC, dl. fali=550nm, gam=20st
Diament: 29.8st
Kwarc topiony: 9.4st
Szklo kron Zn: 10.5st
$
Instrukcje sterujące: Rozgałęzienia i skoki
Instrukcja if
Instrukcję if nazywany instrukcją rozgałęzienia, ponieważ jest rodzajem skrzyżowania, na którym program
musi wybrać, którą z dwóch ścieżek dalej podążać. Budowa instrukcji if:
instrukcja_przed_if;
if (warunek_testujący) instrukcja_dla_if;
instrukcja_za_if;
Po napotkaniu instrukcji if sprawdzany jest warunek_testujący. Kiedy jest on prawdziwy, wtedy
wykonywana jest instrukcja_dla_if, a potem instrukcja_za_if. Gdy warunek_testujący jest fałszywy, wtedy
wykonywana jest instrukcja_za_if.
Przykłady:
if(temperatura<-15.0) printf("U! ha! ha! U! ha! ha! nasza zima zla.\n");
if(start==ODLICZANIE) {i=4; while(--i) printf("%d!\n", i); printf("Start!\n");}
Instrukcja if else
Instrukcja if umożliwia wybór pomiędzy wykonaniem instrukcji lub jej pominięciem. Konstrukcja if else
pozwala wybrać pomiędzy wykonaniem jednej z dwóch instrukcji. Budowa instrukcji if else:
instrukcja_przed_if_else;
if (warunek_testujący) instrukcja_dla_if;
else instrukcja_dla_else;
instrukcja_za_if_else;
Po napotkaniu instrukcji if else sprawdzany jest warunek_testujący. W wypadku gdy jest on prawdziwy
wykonywana jest instrukcja_dla_if, a potem instrukcja_za_if_else. Kiedy warunek_ testujący jest fałszywy,
wtedy wykonywana jest instrukcja_dla_else, a potem instrukcja_za_if_else.
Przykłady:
if(x>0) x++; else x--;
if(temperatura<0) jezioro=LOD; else {jezioro=WODA; printf("Mozna plywac.\n");}
if(scanf("%lf", &plus)==1) {suma+=plus; printf("Suma wynosi: %f\n", suma);} else printf("Zla dana\n");
Zagnieżdżanie instrukcji if i if else
Zagnieżdżanie instrukcji if i if else umożliwia wybór pomiędzy wykonaniem więcej niż dwóch instrukcji.
Pojawia się wtedy konstrukcja else if, która nie jest niczym nowym, ale umieszczeniem instrukcji if (if else)
w miejscu instrukcja_dla_else.
Przykłady:
if(godzina==7) printf("Podano sniadanie\n");
else if(godzina==13) printf("Podano obiad\n");
if(dochod<=PROG1) podatek=0.0;
else if(dochod<=PROG2) podatek=PROCENT2*dochod/100.0;
else if(dochod<=PROG3) podatek=PROCENT3*dochod/100.0;
29
else if(dochod<=PROG4) podatek=PROCENT4*dochod/100.0;
else podatek=PROCENT5*dochod/100.0;
if(temperatura>0) if(temperatura<100) H2O=WODA;
jest równoważna instrukcji:
if(temperatura>0 && temperatura<100) H2O=WODA;
Chcąc określić przedział wartości jako warunek nie należy pisać:
if(0<temperatura<100) H2O=WODA; /* Uwaga! Błąd semantyczny. */
Powyższa instrukcja nie zawiera błędu składniowego, ale niezależnie od wartości zmiennej temperatura
warunek instrukcji if będzie zawsze spełniony.
Słowo else należy do najbliższej instrukcji if (oczywiście poprzedzającej), chyba że klamry wskazują
inaczej.
Funkcje getchar() i putchar()
Funkcje getchar() i putchar() zazwyczaj są makrami a nie prawdziwymi funkcjami. Ich użycie:
#include <stdio.h>
int putchar(int c);
int getchar(void);
Funkcje te czytają jeden znak ze standardowego wejścia (getchar()) lub wpisują jeden znak do
standardowego wyjścia (putchar()). Porównując je z funkcjami printf() i scanf() można napisać że:
ch=getchar();
daje taki sam efekt jak
scanf("%c", &ch);
putchar(ch);
daje taki sam efekt jak
printf("%c", ch);
Rodzina funkcji znakowych z pliku ctype.h
ANSI C udostępnia standardowy zestaw funkcji do analizy znaków, których prototypy znajdują się w pliku
ctype.h. Wszystkie podane poniżej funkcje mają podobne prototypy, różniące się jedynie nazwą funkcji.
Posiadają one jeden argument typu int i zwracają wartość typu int. Przykładowo:
#include <ctype.h>
int islower(int c);
int tolower(int c);
Funkcje testujące znaki z pliku ctype.h
Nazwa
Zwraca prawdę, jeśli argument jest
isalnum() alfanumeryczny (jest literą lub cyfrą).
isalpha()
alfabetyczny (jest literą).
iscntrl()
znakiem sterującym, np. Ctrl-B.
isdigit()
cyfrą.
isgraph() jakimkolwiek znakiem drukowanym.
islower() małą literą.
isprint()
znakiem drukowanym lub odstępem.
ispunct() znakiem przestankowym (jakimkolwiek znakiem drukowanym niealfanumerycznym).
isspace() znakiem niedrukowalnym (whitespace): odstępem, znakiem nowej linii, znakiem
wysuwu strony, znakiem powrotu karetki, tabulatorem pionowym, tabulatorem
poziomym lub innym znakiem w zależności od implementacji.
isupper() wielką literą.
isxdigit() znakiem będącym cyfrą szesnastkową.
Funkcje odwzorowujące znaki z pliku ctype.h
Nazwa
Działanie
tolower() Jeśli argument jest wielką literą, zwraca jego mały odpowiednik; w przeciwnym
wypadku, zwraca pierwotny argument.
30
Nazwa
Działanie
toupper() Jeśli argument jest małą literą, zwraca jego wielki odpowiednik; w przeciwnym
wypadku, zwraca pierwotny argument.
Operator warunkowy: ?:
W celu skrócenia jednego z wariantów instrukcji if else język C udostępnia tzw. wyrażenie warunkowe.
Ogólna jego postać wygląda następująco:
wyrażenie1 ? wyrażenie2 : wyrażenie3
Jeżeli wyrażenie1 jest prawdziwe, to całe wyrażenie warunkowe ma wartość taką samą jak wyrażenie2, w
przeciwnym wypadku otrzymuje ono wartość taką jak wyrażenie3.
Przykłady:
abs_y=(y<0) ? -y : y;
jest równoważna instrukcji if(y<0) abs_y=-y; else abs_y=y;
min= (a<b) ? a : b;
jest równoważna instrukcji if(a<b) min=a; else min=b;
printf("Ptak na %c to %s\n", znak, znak=='b' ? "bazant" : "(brak odpowiedzi)");
~~~~~~~~~~~~~~~~~~~~ rozgalezienia.c ~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <ctype.h>
int main(void)
{
double a;
int h;
char ch;
scanf("%lf\n",&a);
h=(int)(a<0?a+1:a);
do {
ch=getchar();
if(ch==' ' || ch=='\n') putchar(ch);
else if(ch=='!') putchar('%');
else if(ch=='%') putchar('!');
else if(isdigit(ch)) putchar(ch=='0'?'$':ch-1);
else if(islower(ch)) putchar(toupper(ch));
else if(isupper(ch)) putchar(tolower(ch));
else putchar('+');
}
while(ch!='\n');
if(h>-15 && h<15) printf("h rowna sie teraz %d\n", h);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ rozgalezienia
12.6
AaBb !%0123456789 ****=+++++
aAbB %!$012345678 ++++++++++
h rowna sie teraz 12
$
Dodatki do pętli: continue i break
Po wejściu do wnętrza pętli program wykonuje wszystkie należące do niej instrukcje aż do kolejnego
sprawdzenia wyrażenia testowego. Instrukcje continue i break pozwalają wyjść poza ten schemat. Instrukcja
continue umożliwia pominięcie fragmentu pętli, a break jej zakończenie. Użycie tych instrukcji wewnątrz
pętli ma sens, kiedy wykonywane są w oparciu o wynik testu. Użycie instrukcji continue wewnątrz pętli:
31
instrukcja_przed_pętlą;
pętla_z_warunkiem_testującym { /* początek pętli */
instrukcje_przed_continue;
instrukcja_(zł/pr)_zawierająca_continue;
instrukcje_za_continue;
/* koniec pętli */ }
instrukcja_za_pętlą;
Po wejściu do pętli wykonywane są instrukcje_przed_continue. Instrukcja_(zł/pr)_zawierająca_continue
może być złożona lub prosta (samo continue). Jeżeli w tej instrukcji zostanie wykonana instrukcja continue,
wtedy program omija pozostałe instrukcje, aż do koniec_pętli i po sprawdzeniu warunku testującego (w pętli
for wykonywana jest wcześniej instrukcja_powtarzana) wraca do początek_pętli (jeżeli warunek przyjmie
wartość prawdziwą) lub wychodzi z pętli (jeżeli warunek przyjmie wartość fałszywą).
Użycie instrukcji break wewnątrz pętli:
instrukcja_przed_pętlą;
pętla_z_warunkiem_testującym {
instrukcje_przed_break;
instrukcja_(zł/pr)_zawierająca_break;
instrukcje_za_break;
}
instrukcja_za_pętlą;
Po wejściu do pętli wykonywane są instrukcje_przed_break. Instrukcja_(zł/pr)_zawierająca_break może
być złożona lub prosta (samo break). Jeżeli w tej instrukcji zostanie wykonana instrukcja break, wtedy
program wychodzi z pętli.
Instrukcję continue można często dość łatwo zastąpić przez rozszerzenie instrukcji rozgałęzienia np. z if do
if else lub przez zmianę warunku testującego w instrukcji rozgałęzienia. Nie należy jej stosować kiedy
alternatywy są bardziej przejrzyste. Instrukcję break rzadziej można zastąpić od continue, ale podobnie jak w
przypadku continue, jeżeli istnieją bardziej przejrzyste alternatywy, to powinny być stosowane. Instrukcje
continue i break dotyczą najbliższej pętli lub instrukcji switch (tylko break). Przykłady:
do { ch=getchar(); if(ch=='\t') continue; putchar(ch); } while(ch!='\n');
jest równoważne
do { ch=getchar(); if(ch!='\t') putchar(ch); } while(ch!='\n');
while(test>0.0) { scanf("%lf",&a); if(a<-1 || a>1) {printf("Zla dana\n"); continue;} test=asin(a); }
jest równoważne
while(test>0.0) { scanf("%lf",&a); if(a<-1 || a>1) printf("Zla dana\n"); else test=asin(a); }
nie jest równoważne
while(scanf("%lf",&a), test>0.0) if(a<-1 || a>1) printf("Zla dana\n"); else test=asin(a);
while((ch=getchar())!='\n') { ml=ch; if(!isalpha(ch)) break; ch=toupper(ch); printf("%c %c\n", ml, ch); }
nie jest równoważne
while(ch=getchar(), isalpha(ch)) { ml=ch; ch=toupper(ch); printf("%c %c\n", ml, ch); }
nie jest równoważne
while(ml=ch=getchar(), isalpha(ch)) { ch=toupper(ch); printf("%c %c\n", ml, ch); }
while((ch=getchar())!='\n') { if(ch=='\t') break; putchar(ch); }
jest równoważne
while((ch=getchar())!='\n' && ch!='\t') putchar(ch);
Wybór spośród wielu możliwości: switch i break
Operator warunkowy i konstrukcja if else są łatwymi sposobami realizacji wyboru jednej spośród dwóch
możliwości. Jeżeli możliwości jest więcej, wtedy można posłużyć się konstrukcją if ... else if ... else. Jednak
często można (i jest to wtedy wygodniejsze) posłużyć się instrukcją switch wraz z instrukcją break.
Budowa instrukcji switch:
instrukcja_przed_switch;
switch (wyrażenie_całkowite)
32
{
case stala1:
instrukcje; /*opcjonalnie*/
break; /*opcjonalnie*/
case stala2:
instrukcje; /*opcjonalnie*/
break; /*opcjonalnie*/
.
.
.
case stalaN:
instrukcje; /*opcjonalnie*/
break; /*opcjonalnie*/
default:
/*opcjonalnie*/
instrukcje; /*opcjonalnie*/
}
instrukcja_za_switch;
Kiedy program napotyka instrukcję switch, wtedy obliczana jest wartość wyrażenie_całkowite. Jeżeli
któraś ze stałych całkowitych za słowem case (stala1, stala2, ..., stalaN, stałe te muszą mieć różne wartości)
jest równa wyrażenie_całkowite, wtedy wykonywane są instrukcje następujące za tą stałą i dwukropkiem, aż
do napotkania instrukcji break, która powoduje zakończenie instrukcji switch, albo do napotkania końca
instrukcji switch. Przykłady:
switch(h) case 5: case 4: case -3: printf("Switch bez nawiasow.\n");
switch(h){case 3: a=3; b=5.7; break; case 4: a=4; b=6.8; break; case 5: a=5; b=2.4; }
switch(h){case 3: a=3; b=5.7; break; case 4: a=4; b=6.8; break; case -5: a=5; b=2.4; default: a=0; b=0.0;}
switch(h){case 3: a=3; b=5.7; case 4: a=4; b=6.8; break; case 5: a=5; b=2.4; break; default: a=0; b=0.0;}
jest równoważne
switch(h){case 3: case 4: a=4; b=6.8; break; case 5: a=5; b=2.4; break; default: a=0; b=0.0;}
switch(h){case 3: a=3; c=5.7; case 4: a=4; b=6.8; }
nie jest równoważne
switch(h){case 3: case 4: a=4; b=6.8; }
switch(getchar()){ case 'a': case 'A': printf("Albatros\n"); break; case 'b': case 'B': printf("Barbakan\n");
break; default: printf("Nic\n"); }
Instrukcja switch ma zastosowanie jedynie dla małej ilości wyborów spośród liczb całkowitych. Jeżeli
wybór następuje na podstawie wartości zmiennoprzecinkowych, wtedy instrukcja switch nie może być użyta.
Na przykład instrukcja if(a==9.7) printf("=9.7\n"); else printf("!=9.7\n"); nie może zostać zastąpiona przez
switch. Kiedy wybór następuję z dużego zakresu liczb całkowitych, wtedy użycie switch wymagałoby
użycia etykiety case dla każdej liczby całkowitej. Na przykład instrukcja if(liczba>-100 && liczba<100)
printf("Blisko!\n"); mogłaby być zastąpiona przez switch, ale wymagałoby to użycia 199 etykiet case.
Instrukcja goto
Instrukcja goto umożliwia przeskok z jednego miejsca programu do innego, dowolnie wybranego.
Wspólnie z instrukcjami continue i break tworzy ona grupę instrukcji skoku. Zasadniczo instrukcja goto nie
jest potrzeba w języku C. Kernigham i Ritchie określają ją mianem “pola nieograniczonych nadużyć” i
zalecają, aby “stosować ją z umiarem, jeśli wogóle”. W praktyce używanie goto ma uzasadnienie przy
wyjściu z wnętrza pętli zagnieżdżonych. Inne sposoby są w tym wypadku zbyt zawiłe. Użycie instrukcji
goto:
instrukcja1;
instrukcja2;
.
.
.
instrukcjaN-1;
etykietax:
instrukcjaN;
33
.
.
.
instrukcja_przed_goto;
goto etykietax;
instrukcja_po_goto;
.
.
.
instrukcjaM;
Kiedy program napotyka instrukcję goto, po której następuje etykieta (tu etykietax), wtedy przechodzi do
instrukcji (tu instrukcjaN), przed którą umieszczona jest etykieta (tu etykietax) wraz z dwukropkiem.
~~~~~~~~~~~~~~~~~~~~~~~~ skoki.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <math.h>
#include <ctype.h>
int main(void)
{
char zn, naz[]="Domek", kwiat[]="konwalia";
double dana, dx=0.1;
int h;
while(scanf("%c%d",&zn, &h)==2)
{
while(getchar()!='\n') continue;
if(h<0 || h>7)
{printf("Podaj znak i liczbe z zakresu [0;7].\n");continue;}
kwiat[h]=zn;
}
printf("Nowy gatunek kwiatu to %s\n", kwiat);
/* Instrukcja continue uzyta jako wypelniacz */
while(getchar()!='\n') continue;
switch(zn=getchar())
{
case 'a': case 'A': case '0': naz[0]='T'; printf("%s\n",naz);
break;
case 'd': case 'D': printf("%s\n",naz); break;
case 'r': case 'R': naz[0]='R'; printf("%s\n",naz);
case 'z': case 'Z': naz[0]='Z'; printf("%s\n",naz); break;
default: h=strlen(naz);
while(h--) if(h==0) putchar(tolower(naz[h]));
else putchar(naz[h]);
putchar('\n');
}
while(scanf("%lf", &dana)){
for(h=0; h<10; h++){
dana+=dx;
if(fabs(dana)<=1E-6)
{printf(" Dzielenie przez 0. Koniec.\n"); goto omin;}
printf("%5.2g ", 1.0/dana);
}
printf("\n");
}
omin:
34
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$skoki
t2
fg
Nowy gatunek kwiatu to kotwalia
z
Zomek
0.2
3.3 2.5 2 1.7 1.4 1.2 1.1 1 0.91 0.83
0.3
2.5 2 1.7 1.4 1.2 1.1 1 0.91 0.83 0.77
-0.4
-3.3 -5 -10 Dzielenie przez 0. Koniec.
$
Przekierowania w UNIX-ie
Standardowe funkcje we/wy printf(), scanf(), putchar(), getchar() przekazują dane pomiędzy programem a
otoczeniem. Czytają one dane ze standardowego wejścia i wypisują na standardowe wyjście. W UNIX-ie
istnieje mechanizm, który pozwala przekierować dane ze standardowego wejścia lub wyjścia do pliku lub
programu.
Przekierowanie wejścia: <
Przekierowanie to powoduje, że program nie będzie czytał ze standardowego wejścia, ale z pliku którego
nazwa znajduje się po prawej stronie operatora.
Przykłady: $bc -l < d.dat
$a.out < dane
Przekierowanie wyjścia: >
Przekierowanie to powoduje, że program nie będzie pisał do standardowego wyjścia, ale do pliku którego
nazwa znajduje się po prawej stronie operatora. Jeżeli plik istniał to jego pierwotna zawartość zostanie
skasowana.
Przykłady: $ls -l > kartoteka
$grep main * > programy_c
Dopisywanie do pliku: >>
Przekierowanie to powoduje, że program nie będzie pisał do standardowego wyjścia, ale do pliku którego
nazwa znajduje się po prawej stronie operatora. Dane będą dopisywane na końcu pliku, jeżeli plik istniał.
Przykłady: $ls -l /usr >> kartoteka
$find . -name "*.c" >> programy_c
Potok: |
Pozwala łączyć ze sobą działanie kilku programów. Standardowe wyjście działania bloku programu
znajdującego się po lewej stronie operatora zostanie przekierowane na standardowe wejście programu
znajdującego się po prawej stronie operatora. W bloku programu nazwa programu znajduje się po lewej
stronie.
Przykłady: $grep int * | more
$ls -s | sort -nr | head -4
Żeby program mógł obsługiwać pliki przez przekierowania, potrzebna jest mu w trakcie czytania pliku
informacja, kiedy nie można nic więcej przeczytać, bo program napotkał koniec pliku. Funkcje czytające
takie jak scanf() czy getchar() zwracają wtedy wartość EOF, która jest zwykle definiowana w pliku stdio.h
jako: #define EOF (-1). W większości systemów UNIX-owych do standardowego wejścia (z klawiatury)
można wprowadzić znak końca pliku naciskając Ctrl-D na początku wiersza.
~~~~~~~~~~~~~~~~~~~ przekierowania.c ~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
char zn, lan[80];
35
int h;
while(scanf("%s", lan)!=EOF)
{
h=strlen(lan);
if(h>1) lan[1]=toupper(lan[1]);
printf("%s",lan);
while(isspace((zn=getchar()))) putchar(zn);
/* Zapisuje znak z powrotem do wejscia. */
ungetc(zn, stdin);
}
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$cat tekst
Jesli nie chcesz mojej zguby
krokodyla kup mi luby.
Ala ma kota i kot ma Ale.
$przekierowania<tekst>tekst2
$cat tekst2
JEsli nIe cHcesz mOjej zGuby
kRokodyla kUp mI lUby.
ALa mA kOta i kOt mA ALe.
$
Buforowanie
Kiedy dane są dostarczane do programu z wejścia po jednym znaku, wtedy takie wejście nazywa się
niebuforowanym. Jeżeli dane dostarczane są w porcjach większych niż jeden znak, wtedy wejście programu
jest buforowane. Istnieją dwie odmiany buforowania: pełne i wierszowe. Buforowanie pełne stosuje się
zazwyczaj przy obsłudze plików. Bufor ma ustaloną wielkość np. 512 bajtów, 1024 bajty. Opróżnienie
bufora następuje w momencie, kiedy następny nadchodzący znak przepełniłby bufor. W buforowaniu
wierszowym opróżnienie następuje w momencie odczytania znaku nowej linii. Stosuje się je zwykle, kiedy
dane są odczytywane z klawiatury. Naciśnięcie klawisza Enter powoduje wtedy opróżnienie bufora.
Należy uważać kiedy używa się funkcji czytających z wejścia dane. Funkcja scanf() pomija wszystkie
znaki niedrukowalne i ostatnim znakiem, który jest niedostępny dla kolejnej funkcji wejścia jest ostatni znak
ostatniego łańcucha przyjętego przez scanf(). Funkcja getchar() czyta wszystkie znaki i działa tak jak scanf
("%c", zn);.
~~~~~~~~~~~~~~~~~~~~~~~ powtorz.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
char zn;
while((zn=getchar())!='#') putchar(zn);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$powtorz
ZZaacchhooddzzii sslloonnccee zzaa hhoorryyzzoonntt..
#$
$powtorz
Zachodzi slonce za horyzont.
Zachodzi slonce za horyzont.
36
#
$
Wynik pierwszego polecenie powtorz obrazuje uruchomienie programu na terminalu niebuforowanym.
Drugie polecenie powtorz uruchomione zostało na terminalu buforowanym wierszowo.
Funkcje
Prototypy ANSI C
~~~~~~~~~~~~~~~~~~~~~~~ prototyp.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
double dziel();
//double dziel(int a, int b);
int main(void)
{
double a=9,v=4;
int b=9,w=4;
printf("Poprawnie Niepoprawnie\n");
printf("%5.2f %5.2f\n", dziel(b,w), dziel(a,v));
printf("%5.2f %5.2f\n", dziel(b,w), dziel(b));
return 0;
}
double dziel(int a, int b)
{
return a/b;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$prototyp
Poprawnie Niepoprawnie
2.00 0.00
2.00 0.00
$
Powyższy program został poprawnie skompilowany. Wywołanie funkcji dziel() z wartościami argumentów
typu double nie byłoby błędem, kiedy deklaracja funkcji miałaby formę prototypu, ponieważ wartości te
zostałyby automatycznie zrzutowane do typu int. W wypadku kiedy nie podano w definicji typów
argumentów, rzutowanie takie nie następuje.
Przekazywanie argumentów do funkcji wywoływanej może przebiegać różnie w zależności od systemu. Na
komputerach PC odbywa się to następująco. Funkcja wywołująca umieszcza argumenty w tymczasowym
obszarze pamięci zwanym stosem, skąd pobiera je funkcja wywoływana. Oba procesy nie są ze sobą
skoordynowane. Funkcja wywołująca określa typ przekazywanej wartości w oparciu o typy argumentów
faktycznych, natomiast funkcja wywoływana odczytuje wartości kierując się typami argumentów
formalnych. W pierwszym przypadku program (funkcja main()) umieszcza na stosie dwie wartości typu
double, a dziel() odczytuje stamtąd dwie wartości typu int. W drugim przypadku program umieszcza na
stosie jedną wartość typu int, a dziel odczytuje ją oraz coś przypadkowego umieszczonego dalej na stosie.
Rekurencja
Język pozwala, aby funkcja wywoływała samą siebie. Proces ten nosi nazwę rekurencji. Rekurencja bywa
przydatna, ale niekiedy jest trudna w użyciu, ponieważ funkcja która wywołuje samą siebie, czyni to bez
końca, jeśli nie zawiera odpowiednio sformułowanej instrukcji warunkowej.
Działanie rekurencji najlepiej jest opisać na przykładzie:
~~~~~~~~~~~~~~~~~~~~~~ rekurencja.c ~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
37
int tam_i_nazad(int n);
int main(void)
{
tam_i_nazad(1);
return 0;
}
int tam_i_nazad(int n)
{
printf("Tam %d\n", n);
if(n<4) tam_i_nazad(n+1);
printf("Nazad %d\n", n);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$rekurencja
Tam 1
Tam 2
Tam 3
Tam 4
Nazad 4
Nazad 3
Nazad 2
Nazad 1
$
W powyższym przykładzie funkcja main() wywołuje funkcję rekurencyjną tam_i_nazad() przekazując jej
argument 1. Funkcja wyświetla komunikat i wywołuje tę samą funkcję, ale z argumentem równym 2 i czeka,
aż stos wywołań funkcji ponad nią opróżni się. Funkcje wywoływane są łańcuchowo, aż do momentu
wywołania funkcji z argumentem równym 4. Funkcja ta nie wywołuje już funkcji tam_i_nazad(), tylko
wyświetla dwa komunikaty i ściągana jest ze stosu wywołań. Pozwala to funkcji z argumentem równym 3 na
wyświetlenie drugiego komunikatu, zakończenie i zejście ze stosu. Na końcu ze stosu schodzi funkcja
wywoływana jako pierwsza i wykonywane są instrukcje funkcji main() (tutaj zakończenie programu).
Kod funkcji rekurencyjnej nie jest zwielokratniany przy każdym jej wywołaniu, ponieważ wywołanie
funkcji jest poleceniem nakazującym przejście do początku kodu funkcji. Jedynie każdy poziom wywołania
funkcji posiada swój własny zestaw zmiennych.
Wywołania rekurencyjne przypominają pętle i wielu przypadkach można je stosować zamiennie. W
najprostszej postaci rekurencji, wywołanie rekurencyjne znajduje się na końcu funkcji, tuż przed instrukcją
return. Rekurencję taką nazywamy rekurencją końcową, działa ona tak samo jak pętla. Gdyby usunąć drugie
printf() w funkcji tam_i_nazad(), otrzymalibyśmy przykład rekurencji końcowej.
Instrukcje w funkcji rekurencyjnej, znajdujące się po miejscu, w którym wywołuje ona samą siebie,
wykonywane są w kolejności odwrotnej do kolejności wywoływania funkcji rekurencyjnej. Ta cecha
rekurencji jest użyteczna w przypadku problemów programistycznych wymagających odwrócenia kolejności
działań.
Uzyskiwanie adresów: operator &
Jednoargumentowy operator & pozwala uzyskać adres, pod którym przechowywana jest zmienna. Adres
zmiennej określa miejsce w pamięci, w którym przechowywany jest pierwszy bajt jej wartości. Funkcja
printf() udostępnia specyfikator %p przeznaczony do drukowania wartości adresów. Sposób zapisu adresów
przez ten specyfikator zależy od implementacji. Wartość &zm (gdzie zm jest zmienną) jest stałą, ponieważ
nie zmienia się w trakcie działania programu.
~~~~~~~~~~~~~~~~~~~~~~~~ adresy.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
38
double a=426.71,b=182.82;
char zn='Z',ch=')';
int n=2,h=9;
char k[]="komorka",l[]="latarnia";
printf("%6.2f i %p | %6.2f i %p\n",a,&a,b,&b);
printf("%c i %p | %c i %p\n",zn,&zn,ch,&ch);
printf("%d i %p | %d i %p\n",n,&n,h,&h);
printf("%s i %p | %s i %p\n",k,k,l,l);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$adresy
426.71 i 0xbffffa38 | 182.82 i 0xbffffa30
Z i 0xbffffa2f | ) i 0xbffffa2e
2 i 0xbffffa28 | 9 i 0xbffffa24
komorka i 0xbffffa18 | latarnia i 0xbffffa00
$
Wskaźniki (wstęp), operator dereferencji: *
Wskaźnik (ang. pointer) jest zmienną, której wartość jest adresem. Tak jak wartością zmiennej typu char
jest znak, a wartością zmiennej typu int jest liczba całkowita, tak wartością zmiennej wskaźnikowej jest
adres w pamięci. Jeśli wsk jest zmienną wskaźnikową, a zma zmb zmiennymi, wtedy instrukcje: wsk=&zma;
wsk=&zmb; przypisują do zmiennej wsk adresy zmiennych zma i zmb. Mówimy wtedy, że zmienna wsk
“wskazuje na” zma lub zmb. W tych wyrażeniach wsk jest zmienną, a &zma i &zmb wartościami.
Dopóki nie zna się typu wartości, na którą wskazuje wskaźnik, nie można użyć w wyrażeniach wartości
przez niego wskazywanej, ponieważ istotne jest ile bajtów od miejsca wskazywanego przez wskaźnik trzeba
brać pod uwagę i jak traktować ten ciąg bajtów, czy jako wartość typu int, unsigned int, long czy może float.
Dlatego przy deklaracji zmiennej wskaźnikowej określa się typ zmiennej, na którą wskaźnik wskazuje. Żeby
było wiadomo, że jest to wskaźnik, a nie zmienna danego typu, poprzedza się go w deklaracji znakiem *.
Przykłady:
int * pn;
/* pn jest wskaźnikiem, wskazującym na wartość typu int */
char * pch, * lan;
/* pch i lan są wskaźnikami, wskazującymi na wartości typu char */
float * pf, *fl, *atp;
/* pf, fl, atp są wskaźnikami , wskazującymi na wartości typu float */
Odstęp pomiędzy symbolem * a nazwą wskaźnika można umieścić lub nie.
Nazwa wskaźnika przechowuje adres. Aby użyć w wyrażeniu wartości umieszczonej pod tym adresem, lub
ją zmienić, trzeba to jakoś zasygnalizować. Używa się w tym celu znaku *, umieszczonego przed nazwą
wskaźnika. Tak użyty znak *, nosi nazwę operatora dereferencji. Przykłady:
int *wsk, w1=8, w2=10;
wsk=&w1; w2=*wsk;
/* teraz w1==8 i w2==8 */
float *pf1, *pf2, f1=8.9, f2=7.4;
pf1=&f1; pf2=&f2; f1=*pf2; f2=*pf1; /* teraz f1==7.4 i f2==7.4, ostatnia instrukcja nic nie zmienia */
char c1='b', c2='2', *pzn=&c2;
/* Uwaga! Tu nie ma operatora dereferencji. */
*pzn=c1;
/* teraz c1=='b' i c2=='b' */
Komunikacja pomiędzy funkcjami
Przy wywołaniu funkcja tworzy swoje zmienne lokalne takie jak argumenty w liście parametrów
formalnych i kopiuje do nich wartości argumentów rzeczywistych wywołania funkcji. Jeżeli argumentami
formalnymi funkcji są zmienne nie będące wskaźnikami, wtedy jedyną możliwością zmiany wartości
zmiennych w funkcji wywołującej jest zwrócenie wartości przez funkcję wywoływaną. Jest to jednak sposób
pozwalający przekazać tylko jedną wartość. Dlatego kiedy chce się zmienić wartości kilku zmiennych w
funkcji wywołującej należy użyć wskaźników jako argumentów funkcji wywoływanej.
~~~~~~~~~~~~~~~~~~~~~ komunikacja.c ~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
void zamiana_zm(int u, int v);
void zamiana_ws(int *u, int *v);
39
// lub zamiana_ws(int *, int *);
int main(void)
{
int x=4, y=7;
char c1='a', c2='K', *pch=&c1, *pzn=&c1;
double d1=9.4, d2=4.7, *wd1, *wd2;
printf("Na poczatku x=%d , y=%d\n", x,y);
zamiana_zm(x,y);
printf("Po zamiana_zm x=%d , y=%d\n", x,y);
zamiana_ws(&x,&y);
printf("Po zamiana_ws x=%d , y=%d\n", x,y);
printf("---------------- Wskazniki ----------------\n");
*pzn=c2;
printf("pch=%p , *pch=%c, pzn=%p , *pzn=%c\n",
pch, *pch, pzn, *pzn);
wd1=&d1; wd2=&d2;
printf("wd1=%p , *wd1=%3.1f , wd2=%p , *wd2=%3.1f\n",
wd1, *wd1, wd2, *wd2);
wd1=wd2;
printf("wd1=%p , *wd1=%3.1f , wd2=%p , *wd2=%3.1f\n",
wd1, *wd1, wd2, *wd2);
return 0;
}
void zamiana_zm(int u, int v)
{
int pom;
pom=u; u=v; v=pom;
printf("Lokalnie u=%d , v=%d\n", u,v);
}
void zamiana_ws(int *u, int *v)
{
int pom;
pom=*u; *u=*v; *v=pom;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$komunikacja
Na poczatku x=4 , y=7
Lokalnie u=7 , v=4
Po zamiana_zm x=4 , y=7
Po zamiana_ws x=7 , y=4
---------------- Wskazniki ----------------
pch=0xbffffa3f , *pch=K, pzn=0xbffffa3f , *pzn=K
wd1=0xbffffa28 , *wd1=9.4 , wd2=0xbffffa20 , *wd2=4.7
wd1=0xbffffa20 , *wd1=4.7 , wd2=0xbffffa20 , *wd2=4.7
$
40
Tablice i wskaźniki
Inicjalizacja tablic
Tablicę można zainicjalizować w miejscu jej deklaracji dopisując za nawiasami kwadratowymi znak = i
ujętą w nawiasy klamrowe listę wartości początkowych oddzielonych przecinkami. Jeśli w deklaracji tablicy
nastąpiła jej inicjalizacja, to można użyć w niej pustych nawiasów kwadratowych. Ilość elementów tablicy
zostanie wtedy ustalona w oparciu o liczbę pozycji w liście wartości. Jeśli lista wartości inicjujących jest
krótsza od ilości elementów tablicy, wtedy pozostałe elementy są inicjalizowane wartością 0. Przykłady:
int dni[7]={0,1,2,3,4,5, 6}; short klasy[4]={1, 2, 3, 4}; double temp[4]={12.2, 14.8, 10.2, 16.1};
unsigned lata[100]={1905, 1927, 1957}; double pomiar[]={12.3, 14.5, 16.0, 10.1, 13.6, 17.2};
char kwiat[]={'b', 'r', 'a', 't', 'e', 'k', '\0'}; jest równoważne char kwiat[]="bratek"; i nie jest równoważne
char kwiat[]={'b', 'r', 'a', 't', 'e', 'k'};
Inicjalizacja automatyczna
Zmienna automatyczna lub tablica automatyczna jest zmienną lub tablicą zadeklarowaną wewnątrz funkcji
(dotyczy to także argumentów formalnych). Zmienne tego typu są prywatne dla danej funkcji i istnieją tylko
w czasie działania tej funkcji. Zmienne te nie są automatycznie inicjowane zerami, dlatego zawierają
wartości przypadkowe w wypadku braku ich inicjalizacji w programie.
Zmienna zewnętrzna lub tablica zewnętrzna jest zmienną lub tablicą zadeklarowaną poza jakąkolwiek
funkcją. Zmienne tego typu są znane wszystkim funkcjom, które następują po ich deklaracji w pliku
źródłowym i istnieją przez cały czas działania programu. Zmienne te są automatycznie inicjalizowane
wartościami 0 w wypadku braku ich inicjalizacji w programie.
Zmienne statyczne i tablice statyczne deklaruje się wewnątrz funkcji korzystając ze słowa kluczowego
static. Zmienne tego typu są prywatne dla danej funkcji i istnieją przez cały czas działania programu.
Zmienne te są automatycznie inicjalizowane wartościami 0 w wypadku braku ich inicjalizacji w programie.
Przypisanie wartości do tablic
Przypisanie wartości elementom tablicy odbywa się za pośrednictwem indeksu. W języku C nie można
przypisywać tablic w całości. Nie wolno również korzystać z listy wartości stosowanej przy inicjalizacji.
Przykłady:
int h, j[8];
for(h=0; h<8; h++) j[h]=3*h*h;
int h, trasy_o[3], trasy_p[3]={2,5,7};
trasy_o = trasy_p;
// Niedozwolone. Próba zmiany wartości stałej adresowej.
trasy_o[3]=trasy_p[3];
// Nieprawidłowe. Pisanie w pamięci niezarezerwowanej.
trasy_o[3]={2,5,7};
// Niedopuszczalne.
for(h=0; h<3; h++) trasy_o[h]=trasy_p[h];
// Prawidłowe.
Wskaźniki do tablic
Posługując się wskaźnikami można uzyskać łatwy i naturalny dostęp do tablic. Związek pomiędzy
tablicami i wskaźnikami jest bardzo ścisły. Nazwa tablicy jest adresem jej pierwszego elementu, stąd jeśli
box jest tablicą, prawdziwa jest równoważność: box==&box[0]. Oba wyrażenia są stałymi, tzn. że nie mogą
się zmieniać w trakcie działania programu, ale można je przypisać zmiennej wskaźnikowej, którą można już
zmieniać. Zasadniczo wskaźnik wskazujący na elementy tablicy powinien wskazywać na elementy takiego
samego typu jak elementy tablicy. Zwiększanie lub zmniejszanie wskaźnika powoduje wtedy, że wskazuje
on na kolejne elementy tablicy, np.:
int *wsk, tab[4]={5,7,2,8}; wsk=tab; wsk++; wsk+=2;wsk-=3;
Wskaźnik można zmniejszać lub zwiększać o wartości całkowite. Zwiększenie lub zmniejszenie wskaźnika
o 1 spowoduje, że będzie on wskazywał na adres przesunięty względem pierwotnego o wielkość równą
rozmiarowi elementu, na który wskazuje wskaźnik. Przykłady:
double *wd, *wk, tabd[5]={1.1,1.2,1.3,1.4,1.5};
wd=tabd;
// *wd==tabd[0]==1.1
wd++;
// *wd==tabd[1]==1.2
wk=tabd; wd+=2; wk+=3;
// *wd==*wk==tabd[3]==1.4
wk--; wd-=2;
// *wd==tabd[1]==1.2
*wk==tabd[2]==1.3
tabd[2]=*(wk+2); tabd[3]=*wk+2;
// tabd[2]==1.5
tabd[3]==3.3
Zapis wskaźnikowy można stosować do tablic, a notację tablicową do wskaźników. Należy jedynie
pamiętać, że nazwa tablicy jest stałą adresową. Przykłady:
double *sk, tk[5]={4.7, 6.8, 9.4, 1.9, 3.6};
sk=tk+1; sk[3]=0.6; sk[1]=3.3;
// Teraz tk[4]==0.6 , tk[2]==3.3.
41
sk=tk++;
// Niedozwolone. Próba zmiany wartości stałej (adresowej).
*(tk+2)=sk[2];
// Teraz tk[2]==1.9. Inaczej: tk[2]=*(sk+2);
*--sk=*(tk+1);
// Teraz tk[0]==6.8.
sk[1]=tk[4];
// Teraz tk[1]==0.6.
~~~~~~~~~~~~~~~~~~~~~~~ przypisz.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int statyczna(void);
char tzw[4];
int main(void)
{
double tn[5], tz[5]={1.2,5.8,6.8,9.3,8.8};
int h;
for(h=0; h<4; h++) printf("%d ", tzw[h]); printf("\n");
for(h=0; h<5; h++) printf("%6.2g ", tn[h]); printf("\n");
for(h=0; h<5; h++) printf("%6.2g ", tz[h]); printf("\n");
for(h=0; h<5; h++) tn[4-h]=tz[h];
for(h=0; h<5; h++) printf("%6.2g ", tn[h]); printf("\n");
statyczna();
statyczna();
return 0;
}
int statyczna(void)
{
static int st[6];
int h;
for(h=0; h<6; h++) printf("%d ",st[h]);
printf("\n");
st[0]+=1; st[2]+=2; st[4]+=3;
for(h=0; h<6; h++) printf("%d ",st[h]); printf("\n");
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$
0 0 0 0
4.9e-324 2.7e-314 -2 4.9e-270 2e+10
1.2 5.8 6.8 9.3 8.8
8.8 9.3 6.8 5.8 1.2
0 0 0 0 0 0
1 0 2 0 3 0
1 0 2 0 3 0
2 0 4 0 6 0
$
Funkcje, tablice i wskaźniki
Argumentami funkcji nie mogą być całe tablice. Uzasadnione jest to koniecznością oszczędzania zasobów
komputera (pamięci i czasu). Język C oferuje w zamian inny, lepszy mechanizm obsługi tablic jako
argumentów funkcji. Należy posłużyć się w tym celu wskaźnikami. Aby funkcja miała dostęp do tablicy dla
niej zewnętrznej, należy podać jako argument formalny wskaźnik do typu takiego samego jak typ elementu
tablicy. Argumentem rzeczywistym jest wtedy zazwyczaj wskaźnik wskazujący na pierwszy element tablicy
(może być to wskaźnik wskazujący na dowolnie inny element). Jeśli chce się kontrolować zapis i/lub odczyt
elementów tablicy, wtedy należy uzupełnić listę argumentów formalnych o liczbę całkowitą (zwykle liczbę
42
elementów tablicy) lub wskaźnik wskazujący na element tablicy (zwykle na adres za ostatnim elementem
tablicy). Deklarując wskaźniki w liście argumentów formalnych można posłużyć się alternatywnym
sposobem ich deklaracji (jedynie w liście argumentów formalnych), który sygnalizuje, że myśli się o
tablicach jako argumentach rzeczywistych. Polega on na tym, że wskaźniki deklaruje się wstawiając za
nazwą zmiennej puste nawiasy kwadratowe, zamiast gwiazdki przed nazwą zmiennej. Przykład:
int *wtab jest równoważne int wtab[] ale tylko w liście argumentów formalnych.
~~~~~~~~~~~~~~~~~~~~~~~ fun_tab.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#define ROZMIAR 5
int suma(int tab[], int roz);
// lub int suma(int [], int);
int sumap(int *p, int *k);
// lub int sumap(int *, int *);
int main(void)
{
int tab[ROZMIAR]={4,5,8,12,16};
printf("suma=%d sumap=%d\n", suma(tab, ROZMIAR),
sumap(tab, tab+ROZMIAR));
return 0;
}
int suma(int tab[], int roz)
{
int h, wyn=0;
for(h=0; h<roz; h++) wyn+=tab[h];
return wyn;
}
int sumap(int *p, int *k)
{
int wyn=0;
while(p!=k) wyn+=*p++;
return wyn;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$fun_tab
suma=45 sumap=45
$
Słowo kluczowe const
Stałe symboliczne dla danych typów prostych tworzy się zazwyczaj za pomocą dyrektywy kompilatora
#define. Żeby zaznaczyć, że wartości tablic lub wartości, na które wskazują wskaźniki nie powinny być
zmieniane poprzedza się ich deklaracje słowem kluczowym const. Słowo to może być stosowane do
wszystkich rodzajów zmiennych, ale do tablic i wskaźników jest używane najczęściej. Używanie tego słowa
pomaga jedynie uchronić się przed błędami i zaznaczyć np. w nagłówku funkcji, że dany parametr nie będzie
zmieniany. Przypisania do takich “stałych” powodują jedynie wypisanie ostrzeżeń podczas kompilacji
programu, w przeciwieństwie do próby przypisań do rzeczywistych stałych – zwykle łańcuchowych (adresy
innych stałych trudno byłoby uzyskać), które powodują w trakcie działania programu błąd naruszenia
ochrony pamięci.
~~~~~~~~~~~~~~~~~~~~~~~~ const.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
43
void stala(const char *tb);
int main(void)
{
const double a=6.7;
const char wyraz[]="kapturek";
double tb[3]={1.4,5.7,8.9};
const double *ws; /* Wskaznik do stalej. */
double *const sw=tb; /* Stala wskaznikowa. */
const char *wl="kapturek";
// *wl='K'; /* Przechodzi przez kompilacje z ostrzezeniem. */
/* Powoduje naruszenie ochrony pamieci. */
ws++; /* Dobrze */
*ws=2.5; /* Niedozwolone. Linia 17. */
*sw=3.6; /* Dobrze */
sw++; /* Niedozwolone. Linia 19. */
printf("%3.1f %3.1f %3.1f %3.1f\n",tb[0],tb[1],*ws,*sw);
scanf("%lf",&a);
a=4.5; /* Niedozwolone. Linia 22. */
printf("%.2f\n",a);
stala(wyraz);
stala("kapturek");
return 0;
}
void stala(const char *tb)
{
printf("Przed przypisaniem\n");
tb[0]=toupper(tb[0]); /* Niedozwolone. Linia 32.*/
printf("Po przypisaniu\n");
printf("%s\n",tb);
return;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc const.c -o const
const.c: In function `main':
const.c:17: warning: assignment of read-only location
const.c:19: warning: increment of read-only variable `sw'
const.c:22: warning: assignment of read-only variable `a'
const.c: In function `stala':
const.c:32: warning: assignment of read-only location
$const
3.6 5.7 2.5 5.7
8.0
4.50
Przed przypisaniem
Po przypisaniu
Kapturek
Przed przypisaniem
Naruszenie ochrony pami
ę
ci
$
Tablice wielowymiarowe
Elementami tablic mogą być tablice. Strukturę taką nazywa się wtedy tablicą wielowymiarową.
44
Najprostszą tablicą wielowymiarową jest tablica, której elementami są tablice jednowymiarowe (tablica
dwuwymiarowa). Tablice wielowymiarowe deklaruje się podobnie jak jednowymiarowe. Jedynie między
nazwą tablicy, a nawiasami kwadratowymi z liczbą określającą rozmiar wcześniejszego wymiaru, znajdują
się nawiasy kwadratowe z liczbą określającą rozmiar kolejnego wymiaru. Przykłady:
double temp[12][31];
// Tablica 12 tablic, zawierających 31 elementów typu double.
int posilki[7][3];
// Tablica 7 tablic, zawierających 3 elementy typu int.
char miesiace[4][3][10]; // Tablica 4 tablic, zawierających 3 tablice 10 elementów typu char.
Tablice wielowymiarowe są przechowywane w pamięci komputera liniowo w postaci następujących po
sobie tablic jednowymiarowych, te z kolei następują po sobie w kolejności określonych przez tablice
dwuwymiarowe, trójwymiarowe itd.
Inicjowanie tablic wielowymiarowych odbywa się podobnie jak inicjowanie tablic jednowymiarowych,
jedynie poszczególne elementy tablicy wielowymiarowej są inicjowane jak tablice. Przykłady:
double dane[4][3]={{2.5,78.5,34.2},{2.5,1.7,3},{3.6,9.4,1.5},{3.5,7.1,9.4}};
int sch[2][3][4]={{{2,5,7,8},{4,8,1,9},{1,7,4,8}},{{2,5,7,8},{3,4,2,2},{3,5,2,7}}};
char miesiace[4][3][5]={{"mar","kwi","maj"},{"cze","lip","sie"},{"wrz","paz","lis"},{"gru","sty","lut"}};
Korzystając z formy przechowywania tablic w pamięci komputera, można inicjować tablice
wielowymiarowe tak jak tablice o mniejszej liczbie wymiarów, w szczególności tak jak tablice
jednowymiarowe. Przykłady:
int sch[2][3][4]={{{2,5,7,8},{4,8,1,9},{1,7,4,8}},{{2,5,7,8},{3,4,2,2},{3,5,2,7}}}; jest równoważne
int sch[2][3][4]={{2,5,7,8,4,8,1,9,1,7,4,8},{2,5,7,8,3,4,2,2,3,5,2,7}}; jest równoważne
int sch[2][3][4]={2,5,7,8,4,8,1,9,1,7,4,8,2,5,7,8,3,4,2,2,3,5,2,7};
Jeśli podczas inicjalizacji podano zbyt mało danych, wtedy reszta danych będzie uzupełniana zerami.
Przykłady:
double pojem[3][2]={{2.4},{1.4,1.2}}; jest równoważne
double pojem[3][2]={{2.4,0.0},{1.4,1.2},{0.0,0.0}};
char tydzien[7][5]={"pon","wto","sro","czw","pia","sob","nie"}; jest równoważne
char tydzien[7][5]={'p','o','n','\0','\0','w','t','o','\0','\0','s','r','o','\0','\0','c','z','w','\0','\0','p','i','a','\0','\0','s','o','b','\0',
'\0','n','i','e','\0','\0'};
Podczas inicjalizacji tablicy, w deklaracji można pominąć liczbę w nawiasie kwadratowym zaraz za nazwą
tablicy. Rozmiar całej tablicy zostanie wtedy określony domyślnie. Przykłady:
char tydzien[][5]={"pon","wto","sro","czw","pia","sob","nie"}; jest równoważne
char tydzien[7][5]={"pon","wto","sro","czw","pia","sob","nie"};
int sch[][3][4]={{2,5,7,8,4,8,1,9,1,7,4,8},{2,5,7,8,3,4,2,2,3,5}}; jest równoważne
int sch[2][3][4]={{2,5,7,8,4,8,1,9,1,7,4,8},{2,5,7,8,3,4,2,2,3,5,0,0}};
Kopiowanie jednej tablicy wielowymiarowej do drugiej należy wykonywać, podobnie jak w przypadku
tablicy jednowymiarowej element po elemencie, używając najlepiej pętli zagnieżdżonych.
Wskaźniki a tablice wielowymiarowe
Nazwa tablicy wielowymiarowej jest adresem pierwszego jej elementu, lecz elementem tablicy o wymiarze
większym niż jeden jest tablica o wymiarze o jeden mniejszym od pierwotnej. Przykład:
int tabd[4][2];
tabd jest wskaźnikiem do dwóch elementów typu int. Oznacza to, że wartością wyrażenia tabd+1 jest adres
w pamięci przesunięty względem tabd o rozmiar równy rozmiarowi dwóch elementów typu int. tabd[0] jest
również wskaźnikiem i ma taką samą wartość jak tabd, ale wskazuje na elementy typu int. Oznacza to, że
tabd[0]+1 ma wartość adresu przesuniętego względem tabd (==tabd[0]) o rozmiar równy rozmiarowi
jednego elementu typu int.
tabd jest wskaźnikiem do wskaźnika, wskazującego na elementy typu int. Czyli *tabd jest wskaźnikiem
wskazującym na elementy typu int. Więcej przykładów pokazuje poniższy program.
~~~~~~~~~~~~~~~~~~~~~~~ wiel_wsk.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
int tabd[4][2]={{2,6},{5,8},{1,3},{9,4}};
printf("%p %p %p\n", tabd, tabd+1, *tabd+1);
printf("%p %p %p\n", &tabd[0], &tabd[0]+1, tabd[0]+1);
printf("%p %p %p\n", tabd[0], tabd[0]+1, tabd[0]+2);
printf("%p %p %p\n",&tabd[0][0],&tabd[0][0]+1,&tabd[0][0]+2);
45
printf("%d %d %d %d\n",**tabd,*tabd[0],*&tabd[0][0],tabd[0][0]);
printf("%d %d %d\n",**(tabd+1),*(*tabd+1),**tabd+1);
printf("%d %d %d\n",**(tabd+2),*(*tabd+2),**tabd+2);
printf("%d %d %d\n",*(tabd[0]+3),*(tabd[1]+1),*(tabd[2]-1));
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$wiel_wsk
0xbffffa20 0xbffffa28 0xbffffa24
0xbffffa20 0xbffffa28 0xbffffa24
0xbffffa20 0xbffffa24 0xbffffa28
0xbffffa20 0xbffffa24 0xbffffa28
2 2 2 2
5 6 3
1 5 4
8 8 8
$
Notacja tablicowa przekłada się na zapis wskaźnikowy zgodnie z poniższym wzorem:
tabd[n][m]==*(*(tabd+n)+m)
Podobnie jest w przypadku tablic o wyższych niż dwa wymiarach. Przykład:
double wwt[2][3][4];
wwt jest wskaźnikiem, który wskazuje na tablice składające się z trójelementowych tablic, które składają
się z czteroelementowych podtablic o elementach typu double. wwt+1 ma wartość adresu przesuniętego
względem wwt o rozmiar równy rozmiarowi dwunastu elementów typu double, wwt[0]+1 czterech
elementów typu double, wwt[0][0]+1 jednego elementu typu double. Wartościami wwt, *wwt, **wwt, wwt
[0], *wwt[0], wwt[0][0] są adresy (wszystkie takie same), a ***wwt, **wwt[0], *wwt[0][0], wwt[0][0][0]
wartości typu double (także takie same).
Aby utworzyć zmienną wskaźnikową, która wskazuje na tablicę, należy za nazwą zmiennej wskaźnikowej
dodać ujęte w nawiasy kwadratowe ilości zmiennych prostych w poszczególnych wymiarach, a nazwę
zmiennej wraz z gwiazdką ująć w nawiasy okrągłe. Przykład:
int tabd[4][2];
int (* wt)[2];
wt=tabd;
Wtedy wt+1==tabd+1 i **wt=**tabd. Zasadnicza różnica pomiędzy tabd i wt jest taka, że wt jest zmienna
a tabd stałą, czyli możliwa jest operacja wt++, co jest niemożliwe w przypadku tabd. Nawiasy w deklaracji
zmiennej wt są istotne, ponieważ modyfikator [ ] ma wyższy priorytet od *. Czyli deklaracja:
int * wt[2];
oznaczałaby dwuelementową tablicę wskaźników do int.
Funkcje a tablice wielowymiarowe
Istnieje kilka sposobów przetwarzania tablic wielowymiarowych przez funkcje. Pierwszym sposobem jest
przetwarzanie przez funkcje podtablic tablicy głównej. Sposób ten może mieć zastosowanie, kiedy pisze się
uniwersalną funkcję (która może mieć zastosowanie w innych programach), która pobiera adres tablicy o
ustalonym rozmiarze (np. tablicy jednowymiarowej), podczas gdy w programie mamy tablicę o większym
rozmiarze i (np. w pętli) wywołujemy funkcję dla każdej z podtablic. Drugim sposobem jest traktowanie
przez funkcję tablicy jako tablicy jednowymiarowej, podczas gdy w funkcji wywołującej jest ona
wielowymiarowa (należy wtedy użyć operatora dereferencji lub nawiasów kwadratowych z indeksem, które
spowodują, że wskaźniki argumentu formalnego i rzeczywistego będą tego samego typu). Może mieć to
zastosowanie dla przypadku, kiedy nie musi odróżniać się elementów z poszczególnych podtablic. Trzecim
sposobem jest przetwarzanie przez funkcję tablicy wielowymiarowej o wymiarze takim samym jak w funkcji
wywołującej. Ten sposób może mieć miejsce, kiedy pisze się funkcje na potrzeby konkretnego programu. Na
poziomie funkcji wywoływanej te trzy sposoby redukują się do dwóch: argumentem formalnym funkcji
wywoływanej jest wskaźnik do zmiennej typu prostego lub do tablicy.
~~~~~~~~~~~~~~~~~~~~~~~ chmurka.c ~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
double srednia(const double tb[],int n);
46
// lub double srednia(const double *tb,int n);
double sred_waz(const double tb[][3],int n);
// lub sred_waz(const double (*tb)[3],int n);
int main(void)
{
/* Tablica wartosci opadow deszczu w cm dla por roku */
/* i miesiecy: wiosna - marzec, kwiecien, maj, lato - */
/* czerwiec, lipiec, siepien, jesien - wrzesien, paz- */
/* dzernik, listopad, grudzien, zima - styczen, luty, */
/* marzec, w roku 1998 w Krainie Deszczowcow. */
const double opady[4][3]={{2.3,3.1,4.2},{4.6,0.2,1.3},
{2.0,3.7,5.6},{3.4,2.1,2.8}};
printf("Srednia opadow deszczu w cm/miesiac:\n");
printf("Wiosna - %3.1f Lato - %3.1f\n",
srednia(opady[0],3),srednia(opady[1],3));
printf("Jesien - %3.1f Zima - %3.1f\n",
srednia(opady[2],3),srednia(opady[3],3));
printf("Srednia w 1998r. - %.1f\n",srednia(*opady,12));
printf("Srednia wazona w 1998r. - %.1f\n",sred_waz(opady,4));
return 0;
}
double srednia(const double tb[],int n)
{
int h;
double licz=0.0;
for(h=0; h<n; h++) licz+=tb[h];
return licz/n;
}
/* Oblicza srednia wazona opadow. Wspolczynniki dla: */
/* wiosny - 3, lata - 6, jesieni - 3, zimy - 5. */
double sred_waz(const double tb[][3],int n)
{
int h,i;
double licz, suma=0.0;
for(i=0; i<n; i++){
for(licz=0,h=0; h<3; h++) licz+=tb[i][h];
switch(i) { case 0: case 2: licz*=3; break;
case 1: licz*=6; break;
case 3: licz*=5;
}
suma+=licz/3;
}
return suma/17;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$chmurka
Srednia opadow deszczu w cm/miesiac:
Wiosna - 3.2 Lato - 2.0
Jesien - 3.8 Zima - 2.8
Srednia w 1998r. - 2.9
47
Srednia wazona w 1998r. - 2.8
$
Kiedy argumentem funkcji jest wskaźnik do tablicy o wymiarze większym niż jeden, wtedy, podobnie jak
dla zwykłej deklaracji, tylko pierwszy nawias za nazwą wskaźnika może być pusty. Gdyby nawiasów
pustych było więcej, wtedy nie można byłoby określić na ile elementów wskazuje wskaźnik.
Przykłady:
int twi[][3][5]; jest równoważne int (*twi)[3][5];
double twd[][3][2][2]; jest równoważne double (*twd)[3][2][2];
int funw(float twf[][3][4][2], int n); jest równoważne int funw(float (*twf)[3][4][2], int n);
Funkcje łańcuchowe
Stałe łańcuchowe
Stała łańcuchowa jest ciągiem znaków zawartym pomiędzy znakami cudzysłowu. Wraz ze znakiem '\0'
dodawanym automatycznie przez kompilator jest przechowywana w pamięci jako łańcuch znakowy. Stałe
łańcuchowe należą do klasy statycznej. Oznacza to, że stała użyta w funkcji jest przechowywana w pamięci
przez cały czas działania programu i tylko w jednej kopii, nawet jeśli funkcja wywoływana jest wiele razy.
Cały ciąg znaków w cudzysłowie działa jak wskaźnik do miejsca, w którym zapisany jest łańcuch. Przykład:
printf("%s, %p, %c\n","Jedna","jaskolka",*"wiosny nie czyni");
spowodowało wyświetlenie napisu:
Jedna, 0x80483a0, w
Tablice znakowe, tablice łańcuchów, wskaźniki
Tablice łańcuchów są tablicami znakowymi, których ostatni znak jest znakiem zerowym. Ze względu na
szerokie zastosowanie tablic łańcuchów wydziela się je ze zbioru tablic znakowych. Dlatego jeśli mówimy o
tablicy znakowej, mamy na myśli tablicę, która nie zawiera znaku zerowego w spodziewanym miejscu.
Tablice łańcuchów inicjalizuje się zwykle, pisząc za nawiasami kwadratowymi zmiennej znak = i łańcuch
znakowy. Przykłady:
char mysl[]="Lubie myslec, powiedziala sowa.";
char domysl[50]="O gryzoniach, pomyslal ornitolog."
Niekiedy do obsługi łańcuchów używa się wskaźników do char. Wskaźniki takie można zainicjować
podczas deklaracji stałą łańcuchową. Przykład:
char tab[]="Mozna mnie zmienic."
deklaracja tablicy
char *wsk="Nie mozna mnie zmienic."
deklaracja wskaźnika
Istnieją dwie zasadnicze różnice w zastosowaniu powyższych zmiennych i przypisanych im danych. tab
jest stałą adresową i niepoprawna jest na przykład operacja tab++, ale poprawna jest operacja wsk++.
Deklaracja tablicy rezerwuje w pamięci miejsce, gdzie można zmieniać wartości, czyli poprawna jest
operacja tab[0]='R'. Podczas gdy wsk wskazuje na stałą łańcuchową i chociaż instrukcja *wsk='M'; powinna
przejść przez etap kompilacji to wykonanie jej spowoduje błąd naruszenia ochrony pamięci.
Kiedy pragnie skorzystać się z wielu stałych łańcuchowych, wtedy wygodnie jest zadeklarować i
zainicjować tablicę wskaźników do łańcuchów. Przykłady:
char *dni[7]={"Poniedzialek","Wtorek","Sroda","Czwartek","Piatek","Sobota","Niedziela"}
char *dni[]={"Poniedzialek","Wtorek","Sroda","Czwartek","Piatek","Sobota","Niedziela"}
Można wtedy łatwo posługiwać się w programie zadeklarowanymi nazwami, np. można napisać dni[1]
zamiast ”Wtorek”. Sposób ten oszczędza pamięć komputera, jest krótszy i łatwo jest operować tablicą
używając pętli i rozgałęzień. Oczywiście, jeśli chce się mieć możliwość zmiany łańcuchów, trzeba
zadeklarować tablicę dwuwymiarową:
char dni[7][13]={"Poniedzialek","Wtorek","Sroda","Czwartek","Piatek","Sobota","Niedziela"}
Argumenty wiersza poleceń
Funkcja main() może nie przyjmować żadnych argumentów lub przyjmować dokładnie dwa argumenty. W
tym drugim przypadku pierwszym argumentem jest liczba łańcuchów w wierszu poleceń. Argument ten
należy do typu int i zgodnie z tradycją nosi nazwę argc (ang. argument count). Granicami rozdzielającymi
kolejne łańcuchy są odstępy. UNIX pozwala skorzystać z cudzysłowów w celu połączenia kilku słów w
jeden łańcuch. Drugim argumentem jest tablica wskaźników do łańcuchów. Każdy łańcuch w wierszu
poleceń jest przechowywany w pamięci, a wskaźniki z tej tablicy na nie wskazują. Tradycja nakazuje, aby
tablica ta nosiła nazwę argv (ang. argument values). Jeśli jest to możliwe (mogą na to nie pozwalać niektóre
systemy operacyjne), argv[0] wskazuje na nazwę samego programu. Następnie kolejno wskaźnik argv[1]
wskazuje na pierwszy łańcuch, argv[2] na drugi, argv[3] na trzeci i tak dalej.
~~~~~~~~~~~~~~~~~~~~~~~~ wiersz.c ~~~~~~~~~~~~~~~~~~~~~~~
48
#include <stdio.h>
int main(int argc, char **argv)
{
int h;
char *dni[]={"Poniedzialek","Wtorek","Sroda","Czwartek",
"Piatek","Sobota","Niedziela"};
for(h=0;h<argc;h++)
printf("%s\n", argv[h]);
for(h=0;h<sizeof(dni)/sizeof(dni[0]);h++) {
printf("%s ", dni[h]);
if(!(h%3)) printf("\n");
}
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$wiersz 49.2 "Jesli jestes tu" Posejdonie opowiedz ...
wiersz
49.2
Jesli jestes tu
Posejdonie
opowiedz
...
Poniedzialek
Wtorek Sroda Czwartek
Piatek Sobota Niedziela
$
Wczytywanie łańcuchów
Przed wczytaniem łańcucha należy zarezerwować dla niego wystarczająco dużo miejsca w pamięci.
Najprościej jest zadeklarować tablicę np.: char miasto[30];. Można również skorzystać z funkcji
bibliotecznych przydzielających pamięć dynamicznie (malloc() i free()). Nie należy wykonywać operacji
typu:
char *kraj; scanf("%s", kraj);
lub rezerwować za mało pamięci, na przykład:
char kwiat; scanf("%s", &kwiat); - funkcja scanf() nie przyjmie łańcucha pustego
W pierwszym przypadku po deklaracja wskaźnika do char nie przypisano mu adresu zarezerwowanego
przez deklarację jakiejś tablicy znaków i chociaż wywołanie funkcji scanf() przejdzie przez etap kompilacji,
to jej wykonanie może wywołać niespodziewane skutki. W drugim przypadku można wczytać jedynie
łańcuch pusty, czego funkcja scanf() nie jest w stanie uczynić. W obu przypadkach może wystąpić błąd
polegający na zapisywaniu łańcucha w miejscu, gdzie znajduje się kod programu lub zmienne, których nie
chcemy w danym momencie zmieniać, tym bardziej w sposób zupełnie nieprzewidywalny.
Funkcja gets() jest przydatna w programach interaktywnych. Pobiera ona łańcuch ze standardowego
wejścia (zazwyczaj jest to klawiatura), aż do momentu napotkania znaku nowej linii (naciśnięcie klawisza
Enter). Funkcja gets() pobiera wszystkie znaki do napotkania znaku nowej linii (ale bez niego), dodaje znak
zerowy (\0) i przekazuje łańcuch do programu. Znak nowej linii jest porzucany, a więc kolejne wywołania
funkcji wejścia rozpoczną czytanie od następnego wiersza. Normalnie funkcja zwraca przekazany jej adres, a
w wypadku błędu lub napotkania końca pliku wartość NULL. NULL reprezentuje wartość tzw. wskaźnika
zerowego. Etykieta ta jest zdefiniowana w stdio.h i ma wartość 0.
W programach przeznaczonych do szerokiego użytku nie należy używać tej funkcji, ponieważ nie można z
góry określić jak długi będzie wczytany łańcuch. Właściwość ta była wykorzystana do stworzenia “robaka”
rozprzestrzeniającego się za pośrednictwem sieci. Zaowocowało to usunięciem wszystkich odwołań funkcji
gets() z kodów systemów UNIX. Zamiast niej powinno się używać funkcji fgets().
Funkcja gets():
#include <stdio.h>
char *gets(char *s);
49
Funkcja fgets() różni się od funkcji gets() trzema cechami.
–
pobiera drugi argument określający maksymalną liczbę znaków do odczytania. Jeśli argument ten ma
wartość n, fgets() odczyta do n-1 znaków (rzecz jasna, odczytywanie może zostać zatrzymane przez
wystąpienie znaku nowej linii).
–
umieszcza w łańcuchu odczytany znak nowej linii.
–
pobiera trzeci argument określający plik, z którego mają zostać pobrane dane.
Jeżeli funkcja fgets() ma czytać dane ze standardowego wejścia, należy podać jej trzeci argument równy
stdin (ang. standard input) zdefiniowany w <stdio.h>.
Funkcja fgets():
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
Funkcja scanf() była omawiana już wcześniej. Przy wspomnieniu o niej przy okazji omawiania innych
funkcji wczytujących łańcuchy trzeba dodać, że jeżeli przed specyfikatorem konwersji łańcucha pojawi się
liczba, wtedy jeżeli wczytywany łańcuch jest dłuższy od tej liczby, to pozostała część łańcucha nie zostanie
wczytana i pozostanie w buforze. Przykład:
scanf("%5s%7s",n1,n2); z łańcucha Nowy Targ przypisze do n1 Nowy, a do n2 Targ; z łańcucha Bielsko
Biala do n1 przypisze Biels, a do n2 ko; z łańcucha Rawa Mazowiecka do n1 przypisze Rawa, a do n2
Mazowie.
Wyświetlanie łańcuchów
Funkcja puts() wyświetla na standardowym wyjściu łańcuch, którego adres przekazywany jest jej jako
argument. Do wyświetlanego łańcucha dodaje ona automatycznie znak nowej linii. Funkcja zwraca dodatnie
liczby w wypadku sukcesu, a EOF po wystąpieniu błędu.
Funkcja puts():
#include <stdio.h>
int puts(char *s);
Funkcja fputs() jest wariantem puts() przystosowanym do współpracy z plikami. Pobiera ona drugi
argument określający plik, do którego należy zapisać dane. Aby wyprowadzić dane na standardowe wyjście,
należy użyć argumentu stdout, który jest identyfikatorem standardowego wyjścia zdefiniowanym w pliku
<stdio.h>. W przeciwieństwie do puts(), fputs() nie dodaje do danych wyjściowych znaku nowej linii.
Funkcja fputs():
#include <stdio.h>
int fputs(const char *s, FILE *stream);
Do wyświetlania łańcuchów służy także funkcja printf().
~~~~~~~~~~~~~~~~~~~~~~~~ we_wy.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#define MAX 10
int main(int argc, char **argv)
{
int h;
char miasto[12];
for(h=0;h<argc;h++) fputs(argv[h],stdout);
printf("\n----------\n");
for(h=0;h<argc;h++) puts(argv[h]);
printf("- Wypisano parametry wiersza polecen -\n");
while(gets(miasto)!=NULL) puts(miasto);
printf("- To bylo gets() i puts()-\n");
while(fgets(miasto,MAX,stdin)!=NULL) fputs(miasto,stdout);
printf("- To bylo fgets() i fputs() -\n");
while(gets(miasto)!=NULL) fputs(miasto,stdout);
printf("- To bylo gets() i fputs() -\n");
fgets(miasto,MAX,stdin);fputs(miasto,stdout);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$gcc we_wy.c -o we_wy
50
/tmp/ccqZsxag.o: In function `main':
/tmp/ccqZsxag.o(.text+0xa4): the `gets' function is dangerous and
should not be used.
$we_wy Ala ma kota
we_wyAlamakota
----------
we_wy
Ala
ma
kota
- Wypisano parametry wiersza polecen -
Wroclaw
Wroclaw
- To bylo gets() i puts()-
Warszawa Lublin Bialystok
Warszawa Lublin Bialystok
Chelm
Chelm
- To bylo fgets() i fputs() -
123
123abc
abc456
456def
def- To bylo gets() i fputs() -
Lancuch jest za dlugi
$we_wy
we_wy
----------
we_wy
- Wypisano parametry wiersza polecen -
Warszawa Lublin Wroclaw
Warszawa Lublin Wroclaw
- To bylo gets() i puts()-
Zielona Gora
Zielona Gora
- To bylo fgets() i fputs() -
123
123- To bylo gets() i fputs() -
Krotki
Krotki
$we_wy
we_wy
----------
we_wy
- Wypisano parametry wiersza polecen -
Warszawa Elblag Kielce Poznan Szczecin Zamosc Przemysl
Warszawa Elblag Kielce Poznan Szczecin Zamosc Przemysl
Krakow
Krakow
- To bylo gets() i puts()-
Torun
Torun
- To bylo fgets() i fputs() -
Katowice
Katowice- To bylo gets() i fputs() -
Koniec
51
Koniec
Naruszenie ochrony pami?ci
$
Funkcje przetwarzające łańcuchy
Biblioteka języka C udostępnia kilka funkcji przetwarzających łańcuchy. Ich prototypy (zgodnie z ANSI
C) znajdują się w pliku nagłówkowym string.h.
Funkcja strlen() zwraca długość łańcucha i była wcześniej omówiona.
Funkcje strcat() i strncat():
#include <string.h>
char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);
Funkcja strcat (ang. string concatenation – powiązanie łańcucha) pobiera jako argumenty dwa łańcuchy.
Kopia drugiego łańcucha zostaje przyłączona na końcu pierwszego. Drugi łańcuch nie ulega zmianie.
Funkcja zwraca wartość swojego pierwszego argumentu. Funkcja strcat() nie sprawdza, czy drugi łańcuch
zmieści się w pierwszej tablicy, dlatego podczas jej użycia może wystąpić błąd przelania nadmiarowych
znaków do miejsc w pamięci przylegających do tablicy. Funkcja strncat() pobiera dodatkowy argument,
określający maksymalną ilość znaków jakie mogą zostać dodane do pierwszego łańcucha. Tablica na którą
wskazuje pierwszy argument powinna być wystarczająco duża, aby pomieścić pierwszy łańcuch (bez znaku
zerowego) i drugi łańcuch (ze znakiem zerowym).
Funkcje strcmp() i strncmp():
#include <string.h>
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
Funkcja strcmp() (ang. string comparison) porównuje łańcuchy, na które wskazują jej argumenty. Zwraca
ona wartość 0, kiedy łańcuchy są identyczne (np. strcmp("Ala","Ala")), wartość ujemną, jeśli pierwszy
łańcuch jest alfabetycznie wcześniejszy od drugiego (np. strcmp("A","B"), strcmp("Ala","Ula")) lub wartość
dodatnią jeśli łańcuch pierwszy jest alfabetycznie późniejszy od drugiego (np. strcmp("b","a"), strcmp
("stonka","stokrotka")). Porównywane są ze sobą wszystkie znaki (nie tylko litery alfabetu), więc
porównywanie musi odbywać się według jakiegoś bardziej ogólnego porządku, którym jest porządek
sortowania komputera. Oznacza to, że znaki są porównywane w oparciu o ich reprezentację liczbową, czyli
najczęściej wartość ASCII. W kodzie ASCII małe litery następują po wielkich literach, stąd strcmp("Z","a")
zwraca wartość ujemną. Funkcja strcmp() przemierza łańcuchy, porównując ich kolejne znaki, do momentu
znalezienia pierwszej pary różniących się znaków lub napotkania pary znaków zerowych kończących
łańcuchy. Wynika z tego na przykład, że strcmp("buty","but") zwraca wartość dodatnią, ponieważ znak 'y'
jest późniejszy od znaku '\0'. Reguła mnemotechniczna dla porównywania łańcuchów może być następująca:
numer znaku z pierwszego łańcucha odejmij od numeru znaku z drugiego łańcucha i sprawdź znak wartości.
Z reguły strcmp() zwraca 1 dla wartości dodatnich, a -1 dla ujemnych. Funkcja strncmp() porównuje kolejne
znaki łańcuchów do momentu wykrycia różnicy lub napotkania pary znaków zerowych, lub do momentu
porównania ilości znaków określonej przez trzeci argument.
Funkcje strcpy() i strncpy():
#include <string.h>
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
Jeżeli wsk1 i wsk2 są wskaźnikami do łańcuchów, to instrukcja wsk2=wsk1; skopiuje jedynie adres
łańcucha wskazywanego przez wsk1, nie zaś sam łańcuch. W przypadku przechowywania łańcuchów tylko
w tablicach nie można przekopiować w ten sposób nawet adresów, ponieważ nazwy tablic są stałymi
adresowymi. W języku C do kopiowania łańcuchów służy funkcja strcpy(). Pierwszym argumentem tej
funkcji jest wskaźnik do miejsca, gdzie będzie skopiowany łańcuch wskazywany przez drugi argument. Żeby
zapamiętać kolejność argumentów wystarczy sobie uświadomić, że jest ona taka sama, jak w instrukcji
przypisania: Łańcuch docelowy znajduje się po lewej stronie. Funkcja strcpy() zwraca wartość swojego
pierwszego argumentu. Pierwszy argument nie musi wskazywać na początek tablicy, dlatego łańcuchy
można dołączać w różnych miejscach łańcucha wskazywanego przez pierwszy argument, czyli można
zachowywać początkowe jego znaki. Funkcja strcpy() posiada tę samą wadę co gets() - nie sprawdza czy
łańcuch źródłowy zmieści się w łańcuchu docelowym. Bezpieczniejszy sposób kopiowania łańcuchów
udostępnia funkcja strncpy(). Pobiera ona trzeci argument, który określa maksymalną liczbę znaków do
skopiowania. Z używaniem funkcji strncpy() należy także uważać, ponieważ kiedy ilość znaków w łańcuchu
źródłowym (licząc znak zerowy) jest większa od wartości trzeciego argumentu, wtedy znak zerowy nie
52
zostanie skopiowany. Własność tę można również wykorzystać, kiedy wiadomo że znak zerowy łańcucha
docelowego nie zostanie nadpisany przez łańcuch źródłowy.
Funkcja strstr():
#include <string.h>
char *strstr(const char *stog_siana, const char *igla);
Funkcja strstr() zwraca wskaźnik do pierwszego wystąpienia łańcucha igla w łańcuchu stog_siana. W
przypadku nie znalezienia łańcucha funkcja zwraca wskaźnik zerowy.
Funkcja sprintf():
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
Funkcja sprintf() działa podobnie do printf(), jednak zamiast wyświetlać dane na ekranie, zapisuje je w
łańcuchu, którego adres jest pierwszym argumentem wywołania funkcji. Funkcję tę można wykorzystywać
w rozmaity sposób, np. do łączenia łańcuchów, do wstawiania do łańcuchów danych liczbowych z
programu. Najbardziej niepowtarzalną jej cechą jest zdolność do przekształcania danych liczbowych w
łańcuch znakowy. Można to wykorzystać np. przy tworzeniu pliku, którego nazwa ma zawierać liczbę,
będącą wartością zmiennej całkowitej lub zmiennoprzecinkowej.
Powyżej została opisana tylko część z ponad 20 funkcji obsługujących łańcuchy, które zawiera biblioteka
ANSI C.
Konwersja łańcuchów do liczb
Niekiedy zdarza się potrzeba przekształcenia łańcucha na wartość liczbową, aby można było ją dalej
przetwarzać w programie. Przykładem może być tu chęć wczytanie z wiersza poleceń danej liczbowej.
Ponieważ z wiersza poleceń czyta się jedynie łańcuchy, więc potrzebne są funkcje przekształcające łańcuchy
na dane liczbowe. Zgodnie ze standardem ANSI prototypy tych funkcji znajdują się w pliku nagłówkowym
stdlib.h.
Funkcje atoi() (ang. alphanumeric to integer), atol(), atof():
#include <stdlib.h>
int atoi(const char *nptr);
long atol(const char *nptr);
double atof(const char *nptr);
Argumentem tych funkcji jest adres łańcucha. Funkcje te przetwarzają znaki łańcucha aż do momentu,
kiedy natrafią na coś, co nie należy do odpowiedniej wartości liczbowej. Zwracają one wartość liczbową,
która wystąpiła na początku łańcucha. Przykłady: atoi("54sadljk&k") zwróci 54, atol("3”) zwróci 3, atol
("23.3E2") zwróci 23, atof("23.3E2") zwróci 2.33E3 (inaczej 2330), atoi("eqe") przeważnie powinno
zwrócić 0.
ANSI nie stwierdza jaka powinna być wartość zwracana, jeżeli z łańcucha nie można przeczytać liczby.
Przeważnie wartością tą jest 0. Aby móc stwierdzić czy nie nastąpił błąd polegający na pobraniu łańcucha
nie zawierającego liczby, należałoby posłużyć się funkcjami bardziej ogólnymi: strtol(), strtoul(), strtod().
~~~~~~~~~~~~~~~~~~~~~~ przetw_lan.c ~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define ROZ 40
#define NIESPODZIANKA "A ku ku!"
#define STALA 15
int main(int argc, char **argv)
{
int h,n;
char gdzie[ROZ]="Za siedmioma gorami";
char co[ROZ]="Wiatr mieszka w ";
char odp[ROZ];
char *m="morzami";
char *t="dzikich topolach";
char *o="fiolek", *r="geologia";
double dana,wynik;
puts(co); strcat(co,t); puts(co);
53
puts(gdzie);
strcpy(gdzie+strlen(gdzie)-strlen("gorami"),m);
puts(gdzie);
strncpy(gdzie,NIESPODZIANKA,3); puts(gdzie);
strncpy(gdzie,NIESPODZIANKA,10); puts(gdzie);
do {
puts("Podaj gatunek kwiatu.");
if(gets(odp)==NULL) break;
for(h=0,n=strlen(odp);h<n;h++)
if(isupper(odp[h])) odp[h]=tolower(odp[h]);
if(!strcmp(o,odp)) { puts("Tak! To ten!"); break;}
else puts("Nie. To nie ten.");
} while(1);
do {
puts("Podaj dziedzine wiedzy.");
if(gets(odp)==NULL) break;
for(h=0,n=strlen(odp);h<n;h++)
if(isupper(odp[h])) odp[h]=tolower(odp[h]);
if(!strncmp(r,odp,3)) { puts("Tak! To z tych rzeczy!"); break;}
else puts("Nie. Zupelnie co innego.");
} while(1);
if(argc!=3) return 0;
n=atoi(argv[1]);
dana=atof(argv[2]);
wynik=STALA+n+dana;
sprintf(odp,"%d + %d + %3g = %4g",STALA,n,dana,wynik);
puts(odp);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$przetw_lan 3 5.6
Wiatr mieszka w
Wiatr mieszka w dzikich topolach
Za siedmioma gorami
Za siedmioma morzami
A ksiedmioma morzami
A ku ku!
Podaj gatunek kwiatu.
tulipan
Nie. To nie ten.
Podaj gatunek kwiatu.
FioleK
Tak! To ten!
Podaj dziedzine wiedzy.
muzyka
Nie. Zupelnie co innego.
Podaj dziedzine wiedzy.
GEOGRAFIA
Tak! To z tych rzeczy!
15 + 3 + 5.6 = 23.6
$
54
Obsługa plików
Widok tekstowy i widok binarny
Z punktu widzenia użytkownika systemu operacyjnego plik jest wydzielonym fragmentem pamięci
(najczęściej dyskowej) posiadającym nazwę. Natomiast w języku C plik jest ciągiem bajtów, z których
każdy może zostać oddzielnie odczytany. Taki model odpowiada strukturze pliku w systemie UNIX, kolebce
języka C. Ponieważ inne systemy mogą nie być zgodne z systemem UNIX, standard ANSI C udostępnia dwa
sposoby patrzenia na pliki.
W widoku binarnym każdy bajt pliku jest dostępny dla programu. W widoku tekstowym to, co “widzi”
program, może różnić się od tego, co faktycznie znajduje się w pliku. Wynika to z różnicy reprezentacji w
różnych systemach operacyjnych znaku końca wiersza i końca pliku. W plikach tekstowych systemu
Windows koniec wiersza jest oznaczany za pomocą kombinacji powrót karetki-wysuw wiersza: \r\n (^M^J w
kodzie ASCII), a na komputerach Macintosh tylko za pomocą znaku '\r'. W języku C, tak jak w systemie
UNIX, koniec wiersza jest reprezentowany przez znak '\n'. Stąd, jeśli program w języku C odczytuje plik
systemu Windows w widoku tekstowym, dokonuje on automatycznej zamiany kombinacji \r\n na znak '\n'.
Przy zapisie do pliku zamiana przebiega w odwrotnym kierunku. Kiedy używa się widoku binarnego taka
zamiana nie następuje i w systemie Windows język C widzi obydwa znaki składające się na koniec wiersza.
W systemie UNIX, gdzie konwersja nie następuje, obydwa widoki są równoważne.
Pliki standardowe
Program napisany w języku C otwiera automatycznie trzy pliki: standardowe wejście, standardowe wyjście
i standardowe wyjście dla błędów. Standardowe wejście jest domyślnie podstawowym urządzeniem
wprowadzania danych w komputerze, czyli zazwyczaj klawiaturą. Rolę standardowego wyjścia i
standardowego wyjścia dla błędów pełni zazwyczaj ekran monitora. Standardowe wejście jest plikiem
odczytywanym domyślnie przez getchar(), gets() i scanf(). Standardowe wyjście wykorzystują funkcje
putchar(), puts(), printf().
Plik standardowy
Wskaźnik plikowy
Zwykle
Standardowe wejście
stdin
klawiatura
Standardowe wyjście
stdout
ekran
Standardowe wyjście dla błędów stderr
ekran
Wskaźnik do pliku, funkcje fopen() i fclose()
Plik w języku C na poziomie standardowego wejścia/wyjścia wysokiego poziomu jest reprezentowany
przez wskaźnik do struktury typu FILE. Definicję tej struktury można znaleźć w pliku nagłówkowym stdio.h
i zmienia się ona w zależności od systemu, a programista rzadko bywa zainteresowany rzeczywistą jej
implementacją. Aby móc operować na pliku trzeba najpierw zadeklarować wskaźnik do struktury FILE, np.:
FILE *plik;
Następnym krokiem jest utworzenie i związanie struktury FILE z konkretnym plikiem na określony
sposób. Odbywa się to za pośrednictwem funkcji fopen() i mówimy wtedy, że plik jest otwierany.
Funkcja fopen():
#include <stdio.h>
FILE *fopen (const char *path, const char *mode);
Pierwszym argumentem funkcji fopen() jest nazwa systemowa pliku. W systemie UNIX może być to
ścieżka bezwzględna lub względna do pliku. Drugi argument określa tryb otwarcia pliku.
Łańcuchy określające tryb dla funkcji fopen().
Łańcuch
Znaczenie
"r"
Otwiera plik tekstowy do odczytu.
"w"
Otwiera plik tekstowy do zapisu, usuwając zawartość pliku, jeśli istnieje,
lub tworząc nowy plik, jeśli nie istnieje.
"a"
Otwiera plik tekstowy do zapisu, dopisując nowe dane na końcu
istniejącego pliku lub tworząc nowy plik, jeśli plik nie istnieje.
55
Łańcuch
Znaczenie
"r+"
Otwiera plik tekstowy do uaktualnienia, czyli zarówno do odczytywania,
jak zapisywania.
"w+"
Otwiera plik tekstowy do uaktualnienia (odczytu i zapisu), usuwając
zawartość pliku, jeśli istnieje, lub tworząc nowy plik, jeśli nie istnieje.
"a+"
Otwiera plik tekstowy do uaktualnienia (odczytu i zapisu), dopisując nowe
dane na końcu istniejącego pliku lub tworząc nowy plik, jeśli plik nie
istnieje; odczyt może obejmować cały plik, ale zapis może polegać tylko na
dodawaniu tekstu.
"rb", "wb", "ab"
"rb+", "r+b", "wb+"
"w+b", "ab+", "a+b"
Jak wyżej, ale otwiera plik w trybie binarnym zamiast tekstowego.
Po pomyślnym otwarciu pliku, funkcja fopen() zwraca wskaźnik do struktury typu FILE, który służy jako
identyfikator pliku dla innych funkcji wejścia/wyjścia. W sytuacji, kiedy otwarcie pliku nie powiedzie się,
funkcja fopen() zwraca wskaźnik zerowy. W takim przypadku (fopen()==NULL) program kończy działanie.
Kiedy plik nie będzie dalej używany w programie lub otwarto zbyt dużo plików i trzeba chwilowo jakieś
zamknąć, lub kiedy program ma zakończyć działanie, wskazanym jest zamknięcie pliku, czyli zwolnienie
zasobów przeznaczonych na jego kontakt z systemem. Do tego celu służy funkcja fclose().
Funkcja fclose():
#include <stdio.h>
int fclose(FILE *stream);
Argumentem funkcji fclose() jest wskaźnik do strumienia (zwykle pliku), który ma zostać zamknięty.
Przykład:
FILE *plik;
plik=fopen("wiadomosci","r");
// ... operacje na pliku.
fclose(plik);
Różnice pomiędzy exit() i return
Wynikiem wywołania funkcji exit() jest natychmiastowe zakończenie programu (funkcja nie powraca) po
uprzednim zamknięciu wszystkich otwartych plików. Argument funkcji exit() jest przekazywany do
niektórych systemów operacyjnych, włącznie z UNIX-em, gdzie może zostać wykorzystany przez inne
programy.
Funkcja exit():
#include <stdlib.h>
void exit(int status);
Ogólnie przyjęty zwyczaj każe przekazywać wartość 0 w przypadku prawidłowego zakończenia programu,
a wartość niezerową w przypadku zakończenia będącego wynikiem błędu. Zwykle też stosuje się różne
wartości dla różnych przyczyn niewłaściwego zakończenia. Ponieważ nie wszystkie systemy dopuszczają
ten sam zakres możliwych wartości zwracanych, dlatego zakres gwarantowany przez standard ANSI
obejmuje dwie wartości EXIT_SUCCESS i EXIT_FAILURE.
Wywołanie instrukcji return, w przeciwieństwie do wywołania funkcji exit(), powoduje jedynie powrót
funkcji. Funkcja zdjęta ze stosu umożliwia wznowienie działania funkcji, która ją wywołała. Nawet
wywołanie instrukcji return w funkcji main() nie musi oznaczać zakończenia programu. Można dać tu
przykład funkcji main() wywoływanej rekurencyjnie. Zgodnie ze standardem ANSI użycie słowa return w
funkcji main() na najwyższym poziomie daje ten sam efekt, co wywołanie funkcji exit().
Funkcje getc(), putc(), ungetc()
Funkcje getc() i putc() działają bardzo podobnie do funkcji getchar() i putchar().
Funkcje getc() i putc():
#include <stdlib.h>
int getc(FILE *stream);
int putc(int c, FILE *stream);
Różnica polega na tym, że funkcje getc() i putc() wymagają podania strumienia, na którym mają operować.
Przykłady:
56
FILE *plik; char ch; plik=fopen("komunikat.txt","r+");
ch=getc(plik); putc(ch, plik);
// Drugi znak w pliku zostanie zamieniony na pierwszy.
ch=getc(stdin);
jest równoważne
ch=getchar();
putc(ch,stdout);
jest równoważne
putchar(ch);
Funkcja ungetc():
#include <stdio.h>
int ungetc(int c, FILE *stream);
Funkcja ungetc() umieszcza znak określony przez swój pierwszy argument z powrotem w strumieniu, który
jest jej drugim argumentem. Oznacza to, że znak ten zostanie odczytany jako pierwszy przez najbliższe
wywołanie jakiejkolwiek standardowej funkcji wejścia. Standard ANSI C gwarantuje możliwość zwrócenia
jednego znaku. W przypadku powodzenia zwracana jest wartość pierwszego argumentu, a w przypadku
niepowodzenia EOF.
Plikowe wejście/wyjście
Dla każdej z funkcji we/wy operujących na stdin i stdout istnieje odpowiednia funkcja operująca na
strumieniu plikowym. Odpowiednikami getchar(), putchar(), gets() i puts() są odpowiednio getc(), putc(),
fgets() i fputs(), które zostały już omówione.
Funkcje fprintf() i fscanf() działają tak samo, jak printf() i scanf() z tą różnicą, że wymagają one podania
dodatkowego argumentu określającego plik, do którego mają pisać lub z którego mają czytać.
Funkcje fprintf() i fscanf():
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
Pierwszy argument tych funkcji określa plik (ogólnie strumień), na którym one operują.
Przykłady:
fprintf(stdout,"Dlugi cien.\n");
jest równoważne
printf("Dlugi cien.\n");
fscanf(stdin,"%d",n);
jest równoważne
scanf("%d",n);
Dostęp swobodny: fseek() i ftell()
Funkcja fseek() pozwala traktować plik tak, jak gdyby był tablicą i przejść bezpośrednio do dowolnego
przechowywanego w nim bajtu.
Funkcja fseek():
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
Pierwszym argumentem funkcji fseek() jest wskaźnik do pliku, po którym można się poruszać. Drugi
argument nosi nazwę przesunięcia. Określa on wielkość i kierunek przemieszczenia względem punktu
początkowego określonego przez trzeci argument. Wartość bezwzględna drugiego argumentu jest wielkością
przesunięcia wyrażoną w bajtach, a jego znak określa kierunek przesunięcia. Wartość dodatnia oznacza
przesunięcie w kierunku do końca pliku, a ujemna do początku. Standard ANSI C definiuje trzy stałe
symboliczne reprezentujące punkty początkowe funkcji fseek(): SEEK_SET – początek pliku, SEEK_CUR –
bieżąca pozycja, SEEK_END – koniec pliku. Funkcja zwraca 0 w przypadku powodzenia jak również w
przypadku przejścia za koniec pliku. W razie wystąpienia błędu oraz próby przejścia przed początek pliku
funkcja zwraca -1.
Funkcja ftell():
#include <stdio.h>
long ftell(FILE *stream);
Funkcja ftell() zwraca bieżące położenie wskaźnika pliku. Jeżeli wskaźnik znajduje się na początku pliku,
wtedy funkcja zwróci 0, jeżeli zaraz za pierwszym bajtem końcowym, wtedy zwróci wartość równą
rozmiarowi pliku.
Przykład:
FILE *plik; int roz; plik=fopen("rd.txt","r");
fseek(plik,0,SEEK_END); roz=ftell(plik); // Wartość roz równa jest wielkości pliku rd.txt w bajtach.
Binarne wejście/wyjście: fread() i fwrite()
Omawiane dotychczas funkcje we/wy przystosowane są do danych tekstowych – znaków i łańcuchów.
Jeśli nawet zapisuje się dane liczbowe np. za pomocą funkcji printf() i specyfikatora %f, to w pliku
zapisywane są one jako łańcuch. Taki sposób zapisu liczb zmiennoprzecinkowych z reguły prowadzi do
utraty dokładności danych. Zapisywanie danych jako tekstu oznacza w większości przypadków znaczne
zwiększenie objętości pliku. Na przykład zapis jednej cyfry dziesiętnej zajmuje 1 bajt, a nie powinien
zajmować więcej jak 4 bity. Żeby przezwyciężyć te ograniczenia język C dostarcza możliwość zapisu
57
danych w takiej formie w jakiej istnieją one w pamięci operacyjnej. Mówimy wtedy, że zapisywane są one w
postaci binarnej. Zapis w postaci tekstowej lub binarnej nie należy mylić z widokiem tekstowym lub
binarnym pliku. Plik w formacie tekstowym może zostać otwarty w trybie binarnym, a w pliku binarnym
można zapisać tekst. Generalnie jednak dane binarne zapisuje się w pliku binarnym korzystając z trybu
binarnego. Podobnie pliki tekstowe otwieramy zwykle w trybie tekstowym po to, aby pobrać z nich dane
tekstowe.
Funkcje fwrite() i fread():
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
Funkcja fwrite() zapisuje do pliku dane binarne. Typ size_t jest typem zwracanym przez operator sizeof.
Wskaźnik ptr wskazuje na początek danych, które mają zostać zapisane. size określa rozmiar w bajtach
pojedynczej porcji danych, a nmeb liczbę porcji. Dane zapisywane są do strumienia, który jest określony
przez ostatni argument. Wskaźnika do void, który jest pierwszym argumentem funkcji, jest czymś w rodzaju
uniwersalnego typu wskaźnikowego. Dodanie do niego wartości 1 powoduje, że wskazuje on miejsce w
pamięci przesunięte o 1 bajt od pierwotnego. Pierwszy argument faktyczny jest rzutowany do tego typu.
Funkcja fwrite() zwraca liczbę pomyślnie zapisanych porcji. Liczba ta powinna być równa nmemb, ale może
być mniejsza, jeśli wystąpił błąd zapisu.
Funkcja fread() pobiera ten sam zestaw argumentów co fwrite(). Tym razem jednak ptr wskazuje na
początek obszaru pamięci operacyjnej, do którego dane mają zostać wczytane, a stream określa strumień, z
którego dane mają zostać pobrane. Funkcja fread() zwraca liczbę pomyślnie zapisanych porcji. Liczba ta
powinna być równa nmemb, ale może być też mniejsza, jeśli wystąpił błąd odczytu lub w przypadku
osiągnięcia końca pliku.
~~~~~~~~~~~~~~~~~~~~~~~~ pliki.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <math.h>
#define NR 10
void stop(void);
int main(void)
{
FILE *plik;
long p1,p2;
void *ws, *wb;
char ch='!', km[]="By echem byla przebrzmialym";
double li=2.56,li2,tab[NR];
int h;
wb=ws+1;
/* ----- Pisanie do pliku tekstowego. ----- */
plik=fopen("tekst.dat","w+");
fprintf(plik,"%p %s %p %d %4.2f\n",ws,km,wb,2562,li);
strcpy(km+strlen(km)-strlen("przebrzmialym"),"gorskim");
fputs(km,plik); putc(ch,plik); putc('\n',plik);
/* ---------------------------------------- */
p1=ftell(plik); fseek(plik,0,SEEK_SET); p2=ftell(plik);
printf("%ld %ld\n",p1,p2);
km[5]='\0';
/* --- Czytanie z pliku tekstowego i pisanie na ekranie. --- */
while(fread(km,5,1,plik)==1) printf("%s",km); printf("\n");
printf("%s",km); printf("\n");
/* ----- Pisanie na ekranie pliku od konca. ----- */
for(h=1;fseek(plik,-h,SEEK_END)!=-1;h++)
{ch=getc(plik); putc(ch,stdout);} putc('\n',stdout);
fclose(plik);
58
/* ----- Pisanie do pliku binarnego. ----- */
plik=fopen("binar.dat","w+b");
for(h=0;h<NR;h++) { li=sqrt(h); fwrite(&li,sizeof(li),1,plik);}
fclose(plik);
/* ----- Czytanie z pliku binarnego. ----- */
plik=fopen("binar.dat","rb");
fread(tab,NR*sizeof(*tab),1,plik);
fclose(plik);
/* --------------------------------------- */
for(h=0;h<NR;h++) printf("%.2f ",tab[h]);
printf("\n");
stop();
printf("Ten lancuch nie zostanie wydrukowany\n");
return 0;
}
void stop(void)
{
printf("Tuz przed exit\n");
exit(0);
printf("Tuz po exit\n");
return;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$pliki
83 0
0x40012bd8 By echem byla przebrzmialym 0x40012bd9 2562 2.56
By echem byla gorski
m!
ki
!miksrog alyb mehce yB
65.2 2652 9db21004x0 mylaimzrbezrp alyb mehce yB 8db21004x0
0.00 1.00 1.41 1.73 2.00 2.24 2.45 2.65 2.83 3.00
Tuz przed exit
$cat tekst.dat
0xbffffa84 By echem byla przebrzmialym 0xbffffa85 2562 2.56
By echem byla gorskim!
$
Struktury i inne formy danych
W wielu przypadkach użycie prostych zmiennych lub tablic jest niewystarczające. Można na przykład
wyobrazić sobie obiekt opisywany przez wiele zmiennych prostych lub tablic. Jeśli zadeklarowane by były
nie powiązane ze sobą zmienne dla tego obiektu, wtedy łatwo by było pomylić je z innymi danymi. Kiedy
takich samych obiektów byłoby w programie kilka, wtedy co prawda można dla każdej zmiennej utworzyć
odpowiednią tablicę, ale jak przekazać taki obiekt lub ich zbiór do funkcji? W niektórych wypadkach lista
argumentów funkcji musiałaby być bardzo długa. Zmienne strukturalne w języku C rozwiązują ten problem,
podnosząc zdolność przedstawiania danych o kolejny poziom.
Deklaracja struktury
Deklaracja struktury jest planem, który opisuje budowę struktury. Wygląda on następująco:
struct etykieta {
deklaracja_zmiennej1;
deklaracja_zmiennej2;
.
.
59
.
deklaracja_zmiennejN;
};
Deklaracje zmiennych (deklaracja_zmiennej1, deklaracja_zmiennej2 ...) mogą być deklaracjami zmiennych
dowolnego typu. Konstrukcji struct etykieta używa się potem podczas deklarowania zmiennych
strukturalnych jako określenia typu zmiennej.
Przykłady:
struct urodz {
int dzien, miesiac, rok;
char miejsc[70];
};
struct osoba {
char imie[50];
char nazwisko[50];
char plec;
struct urodz urodziny;
float rozmiar_buta;
int kolor_wlosow;
};
struct samochod {
char marka[50];
int rocznik;
long przebieg;
float p_miasto, p_trasa;
};
Deklarowanie i inicjalizacja zmiennej strukturalnej
Żeby zadeklarować zmienną strukturalną należy przed nazwą zmiennej umieścić słowo struct i zaraz za
nim etykietę struktury. Można w ten sposób deklarować zwykłe zmienne strukturalne, wskaźniki do struktur
i tablice struktur.
Przykłady:
struct osoba Ala, Piotrek, Jola, *osoba_gotujaca, *osoba_sprzatajaca;
struct samochod Basi, Kasi, Lolka, firma_Niunia[20];
Zmienne strukturalne można deklarować wraz z definicją struktury, umieszczając za nią, ale przed
średnikiem kończącym instrukcję deklaracji struktury, etykiety zmiennych.
Przykłady:
struct dysk_twardy {
char firma[NAZ], nazwa[NAZ];
int rodzaj;
int pr_odczytu;
} C, D;
struct komputer {
char procesor[NAZ], plyta[NAZ], dysk_twardy[NAZ];
long pamiec, taktowanie_pam;
struct dysk_twardy twarde[3];
char DVD_CD[NAZ], nagrywarka[NAZ], obudowa[NAZ];
int FDD_y;
struct urz_zew peryferia;
} Mani, Loli, Benka, firma_BOBAS[30];
W przypadku wykorzystywania deklaracji struktury tylko raz do deklarowania zmiennych, można pominąć
etykietę struktury i deklarować zmienne wraz ze strukturą.
Przykłady:
struct {
char autor[DL], tytul[3*DL], wydawnictwo[DL];
int rok;
float cena;
} ksiegarnia[4000], biblioteczka[200];
struct {
float dlugosc, szerokosc, wysokosc, ciezar;
int gatunek;
60
} szafa, stol, sofa, krzeslo, taboret, *do_siedzenia, komplet[9];
Zmienne strukturalne można inicjalizować podczas deklaracji podobnie jak tablice. Za etykietą zmiennej
należy umieścić wtedy znak = i ujętą w nawiasy klamrowe listę wartości rozdzielonych przecinkami. Każda
wartość powinna należeć do tego samego typu, co odpowiadający jej składnik struktury.
Przykłady:
struct samochod Franka = {"Wolga", 1975, 384000, 20.3, 15.7};
struct dysk_twardy twardziel = {"Trybik", "Digital FD 45-9L", 3, 7200};
struct {
int n, h, tn[3];
char naz[10];
struct {
float lf, tf[2];
char ch;
} str;
} war={2,4,{23,7,5,},"Liscie",{3.2,{4.5,3.21E-6},'K'}};
Uzyskiwanie dostępu do składników struktury, operatory: ., ->
Elementy struktury mogą (i prawie zawsze są) być zmiennymi różnych typów: znakami, tablicami liczb
zmiennoprzecinkowych, całkowitych, znaków, liczbami całkowitymi, wskaźnikami do struktur,
wskaźnikami do liczb całkowitych, strukturami itd. Kiedy zmienna strukturalna jest zmienną prostą, wtedy
dostęp do jej składników uzyskuje się pisząc po nazwie zmiennej kropkę (“.” - operator przynależności do
struktury) i etykietę odpowiedniego pola. Jeżeli zmienna jest tablicą struktur, wtedy trzeba najpierw
wyodrębnić jedną strukturę za pomocą indeksu ujętego w nawiasy kwadratowe, który następuje po nazwie
zmiennej. Dostęp do elementów struktury wskazywanych przez wskaźnik do struktury jest inny. Można w
tym wypadku po nazwie wskaźnika wstawić operator -> (myślnik i symbol “większy niż”) i etykietę
odpowiedniego pola. Można również użyć operatora dereferencji przed etykietą wskaźnika. Jednak przed
zastosowaniem operatora “.” (kropka) trzeba ująć nazwę wskaźnika wraz z operatorem dereferencji w
nawiasy okrągłe, ponieważ operator przynależności do struktury ma wyższy priorytet od operatora
dereferencji. Tego drugiego sposobu nie można stosować wewnątrz struktury.
Przykłady:
struct {
int n, h, tbn[3];
double dana, *wsd;
char naz[10];
struct {
float lf, tf[17];
char znak;
} str, *wst;
} zwykla, tablica[15], *wskaznik, *tbws[5], (*wstb)[6];
zwykla.n, zwykla.h, zwykla.tbn[2]
// wartości typu int
zwykla.dana, *zwykla.wsd
// wartości typu double
zwykla.naz
// łańcuch znakowy
zwykla.naz[5], zwykla.str.znak, zwykla.wst->znak
// znaki
zwykla.str.lf, zwykla.str.tf[15], zwykla.wst->lf, zwykla.wst->tf[5] // wartości typu float
tablica[3].n, tablica[4].h, tablica[0].tbn[4]
// wartości typu int
tablica[1].tbn
// wskaźnik do int
tablica[9].str.tf[10], tablica[0].str.lf, tablica[5].wst->lf // wartości typu float
zwykla.wst, tablica[5].wst, wskaznik->wst
// wskaźniki do zdefiniowanej wewnątrz struktury
(*wskaznik).n jest równoważne wskaznik->n
// wartość typu int
wskaznik->dana, *wskaznik->wsd
// wartości typu double
(*wskaznik).wst->lf jest równoważne wskaznik->wst->lf
// wartość typu float
(*wskaznik).(*wst).lf
Uwaga!
// Źle. Nie przejdzie przez kompilacje.
wskaznik->naz[5], wskaznik->str.znak, wskaznik->wst->znak
// znaki
wskaznik->str.tf, wskaznik->wst->tf
// wskaźniki do float
tbws[3]->naz[5], tbws[0]->str.znak, tbws[4]->wst->znak
// znaki
tbws[3]->dana, (*tbws[1]).dana, *tbws[0]->wsd
// wartości typu double
(*wstb)[0].dana, *(*wstb)[1].wsd
// wartości typu double
(*wstb)[0].str.tf[10], (*wstb)[5].str.lf, (*wstb)[3].wst->lf
// wartości typu float
61
Struktury a funkcje
Wartość jednej struktury może zostać przypisana innej strukturze tego samego typu. Służy do tego celu
operator przypisania. Na przykład jeżeli n_ksiazka i s_ ksiazka są strukturami tego samego typu, to
wykonanie instrukcji n_ksiazka=s_ ksiazka; spowoduje przypisanie każdemu składnikowi struktury
n_ksiazka odpowiadający mu składnik struktury s_ ksiazka. Przypisanie takie może zostać wykonane także
przy inicjalizacji.
Przykłady:
struct samoloty transportowy={"FC-Kondor", 48.7, 32.5, 398, 120};
struct samoloty lot_ostatni,lot_biezacy=transportowy;
lot_ostatni=lot_biezacy;
Oczywiście zawsze można wyodrębnić pojedynczy element struktury, który zachowuje się jak zmienna
odpowiedniego, zadeklarowanego w strukturze typu.
Przykłady:
struct osoba Ziuta;
char zn='K'; float roz=25.5, dan;
strcpy(Ziuta.imie,"Zuzanna");
Ziuta.plec=zn;
Ziuta.rozmiar_buta=roz;
Ziuta.kolor_wlosow=4;
dan=Ziuta.rozmiar_buta/Ziuta.kolor_wlosow;
Inną cechą upodabniającą struktury do zmiennych prostych jest to, że mogą być zwracane przez funkcje.
Przykłady:
struct samolot lot_ostatni;
struct samochod Kacper;
lot_ostatni=loty(12, 'o', 12331, 21276);
Kacper=salon_samochodowy(data,osoba,firma,nr_marki,umowa);
Do funkcji można przekazać jako argument strukturę, adres struktury lub element struktury.
Przykład:
struct bukiet {
int ilk;
char kwiaty[MAX][NAZKW];
int nrk[MAX];
} peczek;
struct osoba Kasia, Wiesio, mama_Kasi;
struct bukiet imieniny(struct osoba solenizant, struct osoba *gosc, float but_rodzica)
{
int n,h=0;
int but_zrzut;
struct bukiet wia;
n=strlen(solenizant.imie);
if(n>8 || solenizant.imie[0]=='W')
{ strcpy(wia.kwiaty[h],"tulipany"); wia.nrk[h]=n/2; h++;}
if(solenizant.plec=='K' && gosc->plec=='M') {
if(solenizant.kolor_wlosow==3 && gosc->kolor_wlosow==1)
{ strcpy(wia.kwiaty[h],"gerbery"); wia.nrk[h]=2; h++;}
if(solenizant.kolor_wlosow==2 && gosc->kolor_wlosow==2)
{ strcpy(wia.kwiaty[h],"czerwone gozdziki"); wia.nrk[h]=3; h++;}
if(solenizant.kolor_wlosow==4 && gosc->kolor_wlosow==3)
{ strcpy(wia.kwiaty[h],"biale roze"); wia.nrk[h]=1; h++;}
}
but_zrzut=but_rodzica;
switch(but_zrzut) {
case 22: { strcpy(wia.kwiaty[h],"nasturcje"); wia.nrk[h]=3; h++;}
case 23: case 24: { strcpy(wia.kwiaty[h],"lilie"); wia.nrk[h]=2; h++;}break;
case 29: case 30: { strcpy(wia.kwiaty[h],"mieczyki"); wia.nrk[h]=2; h++;}break;
}
wia.ilk=h;
return wia;
}
62
peczek=imieniny(Kasia, &Wiesio, mama_Kasi.rozmiar_buta);
Unie
Unia (ang. union) jest typem danych, który pozwala przechowywać różne rodzaje danych w tym samym
obszarze pamięci (jednak nie równocześnie). Tworzenie unii przebiega podobnie, jak tworzenie struktury.
Jedynie słowo struct jest zastępowane przez union.
union etykieta {
deklaracja_zmiennej1;
deklaracja_zmiennej2;
.
.
.
deklaracja_zmiennejN;
};
Również inne właściwości unii są takie same jak struktury. Tym co odróżnia unię od struktury jest to, że
unia przechowuje wartość tylko jednego elementu spośród wyszczególnionych w liście pól unii. Na
przechowywanie tego elementu rezerwowany jest obszar pamięci o rozmiarze największego elementu
spośród listy elementów pól.
~~~~~~~~~~~~~~~~~~~~~~~~~ unie.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <string.h>
int main(void)
{
struct struktura {
char zn, kwiat[50];
double wr, twr[2];
int n;
} str;
union unia {
char zn, kwiat[50];
double wr, twr[2];
int n;
} uni;
str.zn='M'; strcpy(str.kwiat,"roza");
str.wr=4.3; str.n=5;
str.twr[0]=6.7; str.twr[1]=8.3;
uni.zn='M'; strcpy(uni.kwiat,"roza");
uni.wr=4.3; uni.n=5;
uni.twr[0]=6.7; uni.twr[1]=8.3;
printf("%c %s %6.2g\n",str.zn,str.kwiat,str.wr);
printf("%6.2g %6.2g %6d\n",str.twr[0],str.twr[1],str.n);
printf("----------- Wartosci unii ------------------\n");
printf("%c %s %6.2g\n",uni.zn,uni.kwiat,uni.wr);
printf("%6.2g %6.2g %6d\n",uni.twr[0],uni.twr[1],uni.n);
return 0;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$unie
M roza 4.3
6.7 8.3 5
----------- Wartosci unii ------------------
? ??????@?????? @d?Bi 6.7
6.7 8.3 -858993459
$
63
Nadawanie nazw typom, słowo kluczowe typedef
Za pomocą słowa kluczowego typedef można tworzyć nowe nazwy typów.
Przykłady:
typedef unsigned char BYTE;
Teraz
unsigned char za, zb[4];
jest równoważne
BYTE za, zb[4];
typedef char * STRING;
typedef char * string;
Teraz
char *naz;
jest równoważne STRING naz; jest równoważne string naz;
typedef struct samochod CAR;
Teraz
struct samochod Ani, *klient;
jest równoważne
CAR Ani, *klient;
typedef struct zespol {
float rzecz;
float uroj;
} ZESPOL;
Teraz
struct zespol zsa, *wzs, zst[4]; jest równoważne ZESPOL zsa, *wzs, zst[4];
Wskaźniki do funkcji
Wskaźnik do funkcji przechowuje adres, pod którym rozpoczyna się kod funkcji. W wypadku użycia
wskaźnika do funkcji trzeba dla wskazywanej funkcji określić prototyp, a nie zwykłą deklarację.
Zadeklarowanie takiej funkcji z nawiasami pustymi traktowane jest jako umieszczenie prototypu funkcji,
która nie posiada argumentów. Wskaźnik do funkcji określa jaki typ wartości zwraca funkcja, na którą może
on wskazywać oraz jaką posiada listę argumentów. Deklaracja wskaźnika do funkcji tym się różni od
prototypu funkcji, że zamiast etykiety funkcji umieszcza się etykietę wskaźnika poprzedzoną modyfikatorem
* i wraz z nim ujętą w nawiasy okrągłe. Nazwą wskaźnika do funkcji, można posługiwać się tak samo jak
nazwą funkcji. Należy jednak wcześniej przypisać mu adres zdefiniowanej funkcji, inaczej próba wywołania
funkcji pod złym adresem spowodowałaby błąd ochrony pamięci. Nazwa funkcji przechowuje adres danej
funkcji, dlatego przypisanie wskaźnikowi adresu polega na umieszczeniu po nazwie wskaźnika operatora
przypisania i nazwy wybranej funkcji (lub innego wskaźnika, który wskazuje na funkcję).
Przykłady:
char literki(int, char *);
// Prototyp funkcji.
char (*wlit)(int, char *); // Wskaźnik do funkcji, który może wskazywać na funkcję literki.
char *wlit(int, char *);
// Prototyp funkcji mającej argumenty typu int i char * i zwracającej wartość
// typu char *.
wlit=literki;
// wlit wskazuje teraz na funkcję literki
Teraz
literki(15,ws);
jest równoważne
wlit(15,ws);
literki=wlit;
// Źle. literki jest stałą adresową
int dzwony(char *, float);
int organy(char *, float);
int (*wdza)(char *, float); int (*wdzb)(char *, float); // Wskaźniki do funkcji tego samego typu
wdza=dzwony; wdzb=organy;
wdza(naz,masa); jest równoważne dzwony(naz,masa); ale nie wdzb(naz,masa);
wdzb=wdza;
Teraz
wdza(naz,masa); jest równoważne wdzb(naz,masa);
Wskaźniki do funkcji są najczęściej stosowane jako argumenty funkcji.
~~~~~~~~~~~~~~~~~~~~~~ wsk_do_fun.c ~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <ctype.h>
double sum(double *a, double *b);
double ilr(double *a, double *b);
double oblicz(double (*obl)(double *,double *),
double a,double b);
int main(void)
{
double (*wfun)(double *a, double *b);
double a,b;
char zn;
64
while(1) {
printf("Podaj litere: ");
zn=getchar();
while(getchar()!='\n') continue;
if(islower(zn)) wfun=sum; else wfun=ilr;
printf("Podaj liczby: ");
if(scanf("%lf%lf",&a,&b)!=2) break;
oblicz(wfun,a,b);
}
return 0;
}
double sum(double *a, double *b)
{
double pom;
pom=*a;
*a=*b;
*b=pom;
return *a+*b;
}
double ilr(double *a, double *b)
{
double pom;
pom=*a*(*b);
*a*=3;
*b*=3;
return pom;
}
double oblicz(double (*obl)(double *,double *),double a,double b)
{
double pom;
printf("Przed obl(): a=%g b=%g\n",a,b);
pom=obl(&a,&b);
printf("Po obl(): a=%g b=%g wynik=%g\n",a,b,pom);
printf("---------------------------------\n");
return(pom);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$wsk_do_fun
Podaj litere: s
Podaj liczby: 3.2 1.4
Przed obl(): a=3.2 b=1.4
Po obl(): a=1.4 b=3.2 wynik=4.6
---------------------------------
Podaj litere: 3
Podaj liczby: 5.2 3.1
Przed obl(): a=5.2 b=3.1
Po obl(): a=15.6 b=9.3 wynik=16.12
---------------------------------
Podaj litere: d
Podaj liczby: f
$
65
Funkcja qsort()
Algorytm quicksort jest jednym z najczęściej używanych algorytmów sortowania. Jest uważany za
najszybszy algorytm sortowania dla “losowych” danych wejściowych. Został on opracowany w 1962 r.
przez C. A. R. Hoare'a. W języku C algorytm ten jest realizowany przez funkcję qsort().
Funkcja qsort():
#include <stdlib.h>
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *))
Funkcja qsort() porządkuje tablicę dowolnych obiektów danych. Pierwszym jej argumentem jest wskaźnik
do początku tablicy przeznaczonej do uporządkowania. Standard ANSI C pozwala na rzutowanie dowolnego
typu wskaźnikowego do typu “wskaźnik do void”, a więc pierwszy argument faktyczny może wskazywać na
tablicę dowolnego rodzaju. Drugi argument to liczba pozycji do uporządkowania. Trzecim argumentem jest
rozmiar pojedynczego elementu (informacja ta tracona jest przy rzutowaniu wskaźnika do początku tablicy).
Ostatnim parametrem funkcji qsort() jest wskaźnik do funkcji, która zostanie wykorzystana do określenia
porządku sortowania.
Funkcja qsort() przekazuje wielokrotnie do funkcji porównującej comp() adresy dwóch elementów tablicy
(argument size pozwala obliczać położenie elementów tablicy) jako wskaźniki do void. Funkcja
porównująca musi operować na zmiennych takiego samego typu jak elementy tablicy, a najlepszym
sposobem na spełnienie tego wymagania jest zadeklarowanie wskaźników właściwego typu wewnątrz
funkcji porównującej i przypisanie im adresów przekazanych jako wskaźniki do void.
Zawartość tablicy jest sortowana w porządku rosnącym, zgodnie z funkcją porównującą. Funkcja
porównująca musi zwrócić liczbę całkowitą, która jest mniejsza, równa, lub większa od zera. Oznacza to
wtedy odpowiednio, że pierwszy argument jest mniejszy, równy, lub większy od drugiego. Jeśli dwa
elementy porównania są jednakowe, to ich kolejność w posortowanej tablicy jest nieokreślona (może być
na przykład taka sama jak w tablicy nieposortowanej).
~~~~~~~~~~~~~~~~~~~~~~ quicksort.c ~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct urodz {
unsigned d,m,r;
char miejsc[70];
};
struct osoba {
char imie[50];
char nazwisko[50];
char plec;
struct urodz ur;
int k_oczu;
int k_wlosow;
};
typedef struct osoba OSOBA;
int drukuj_osoby(OSOBA *os, int il);
int wedlug_nazwiska(const void *n1, const void *n2);
int wedlug_roku_ur(const void *n1, const void *n2);
int wedlug_daty_ur(const void *n1, const void *n2);
int wedlug_plcioczu(const void *n1, const void *n2);
int double_sort(const void *n1, const void *n2);
int main(void)
{
OSOBA os[]={{"Zenobia","Pierog",'K',{8,11,1985,"Bukowiec"},3,8},
{"Robert","Brusiek",'M',{27,3,1984,"Kartowice Wlk."},2,8},
66
{"Robert","Wawrylko",'M',{14,9,1986,"Szczekociny"},2,6},
{"Regina","Krajko-Decholska",'K',{23,8,1986,"Kolno"},5,2},
{"Maurycy","Granacki",'M',{2,6,1987,"Bukowiec"},4,11}};
double td[]={12.4,67.4,86.5,3.6,95.6};
int h,il=sizeof(td)/sizeof(double);
printf("------ Tablica nieposortowana ----------------------\n");
drukuj_osoby(os,sizeof(os)/sizeof(OSOBA));
printf("------ Tablica posortowana wedlug nazwiska ---------\n");
qsort(os,sizeof(os)/sizeof(OSOBA),sizeof(OSOBA),wedlug_nazwiska);
drukuj_osoby(os,sizeof(os)/sizeof(OSOBA));
printf("------ Tablica posortowana wedlug roku urodzenia ---\n");
qsort(os,sizeof(os)/sizeof(OSOBA),sizeof(OSOBA),wedlug_roku_ur);
drukuj_osoby(os,sizeof(os)/sizeof(OSOBA));
printf("------ Tablica posortowana wedlug daty urodzenia ---\n");
qsort(os,sizeof(os)/sizeof(OSOBA),sizeof(OSOBA),wedlug_daty_ur);
drukuj_osoby(os,sizeof(os)/sizeof(OSOBA));
printf("------ Tablica posortowana wedlug plci i koloru oczu\n");
qsort(os,sizeof(os)/sizeof(OSOBA),sizeof(OSOBA),wedlug_plcioczu);
drukuj_osoby(os,sizeof(os)/sizeof(OSOBA));
printf("------ Sortowanie tablicy elementow typu double ----\n");
for(h=0;h<il;h++) printf("%4.1f ",td[h]); printf("\n");
qsort(td,il,sizeof(td[0]),double_sort);
for(h=0;h<il;h++) printf("%4.1f ",td[h]); printf("\n");
return 0;
}
int drukuj_osoby(OSOBA *os, int il)
{
int h;
for(h=0;h<il;h++,os++)
printf("%7s %16s %c %02d %02d %4d %14s %d %02d\n",
os->imie,os->nazwisko,os->plec,os->ur.d,os->ur.m,os->ur.r,
os->ur.miejsc,os->k_oczu,os->k_wlosow);
return il;
}
int wedlug_nazwiska(const void *n1, const void *n2)
{
OSOBA *os1=n1, *os2=n2;
return strcmp(os1->nazwisko,os2->nazwisko);
}
int wedlug_roku_ur(const void *n1, const void *n2)
{
OSOBA *os1=n1, *os2=n2;
if(os1->ur.r<os2->ur.r) return -1;
else return 1;
}
int wedlug_daty_ur(const void *n1, const void *n2)
{
OSOBA *os1=n1, *os2=n2;
67
if(os1->ur.r<os2->ur.r) return -1;
if(os1->ur.r>os2->ur.r) return 1;
if(os1->ur.m<os2->ur.m) return -1;
if(os1->ur.m>os2->ur.m) return 1;
if(os1->ur.d<os2->ur.d) return -1;
if(os1->ur.d>os2->ur.d) return 1;
return 0;
}
int wedlug_plcioczu(const void *n1, const void *n2)
{
OSOBA *os1=n1, *os2=n2;
if(os1->plec<os2->plec) return -1;
if(os1->plec>os2->plec) return 1;
if(os1->k_oczu<os2->k_oczu) return -1;
if(os1->k_oczu>os2->k_oczu) return 1;
return 0;
}
int double_sort(const void *n1, const void *n2)
{
double *d1=n1, *d2=n2;
if(*d1<*d2) return -1;
else return 1;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$quicksort
------ Tablica nieposortowana ----------------------
Zenobia Pierog K 08 11 1985 Bukowiec 3 08
Robert Brusiek M 27 03 1984 Kartowice Wlk. 2 08
Robert Wawrylko M 14 09 1986 Szczekociny 2 06
Regina Krajko-Decholska K 23 08 1986 Kolno 5 02
Maurycy Granacki M 02 06 1987 Bukowiec 4 11
------ Tablica posortowana wedlug nazwiska ---------
Robert Brusiek M 27 03 1984 Kartowice Wlk. 2 08
Maurycy Granacki M 02 06 1987 Bukowiec 4 11
Regina Krajko-Decholska K 23 08 1986 Kolno 5 02
Zenobia Pierog K 08 11 1985 Bukowiec 3 08
Robert Wawrylko M 14 09 1986 Szczekociny 2 06
------ Tablica posortowana wedlug roku urodzenia ---
Robert Brusiek M 27 03 1984 Kartowice Wlk. 2 08
Zenobia Pierog K 08 11 1985 Bukowiec 3 08
Robert Wawrylko M 14 09 1986 Szczekociny 2 06
Regina Krajko-Decholska K 23 08 1986 Kolno 5 02
Maurycy Granacki M 02 06 1987 Bukowiec 4 11
------ Tablica posortowana wedlug daty urodzenia ---
Robert Brusiek M 27 03 1984 Kartowice Wlk. 2 08
Zenobia Pierog K 08 11 1985 Bukowiec 3 08
Regina Krajko-Decholska K 23 08 1986 Kolno 5 02
Robert Wawrylko M 14 09 1986 Szczekociny 2 06
Maurycy Granacki M 02 06 1987 Bukowiec 4 11
------ Tablica posortowana wedlug plci i koloru oczu
Zenobia Pierog K 08 11 1985 Bukowiec 3 08
68
Regina Krajko-Decholska K 23 08 1986 Kolno 5 02
Robert Brusiek M 27 03 1984 Kartowice Wlk. 2 08
Robert Wawrylko M 14 09 1986 Szczekociny 2 06
Maurycy Granacki M 02 06 1987 Bukowiec 4 11
------ Sortowanie tablicy elementow typu double ----
12.4 67.4 86.5 3.6 95.6
3.6 12.4 67.4 86.5 95.6
$
Udziwnione deklaracje
Język C pozwala deklarować bardzo zawiłe formy danych. Aby je prawidłowo zinterpretować trzeba
pamiętać o priorytecie i kierunku wiązania trzech modyfikatorów.
Modyfikator
Znaczenie
*
wskaźnik
( )
funkcja
[ ]
tablica
Modyfikatory [ ] oraz ( ) mają ten sam priorytet, który jest wyższy niż priorytet modyfikatora *. Nawiasy
okrągłe ( ) (nie modyfikator dotyczący funkcji) mają zawsze najwyższy priorytet. Modyfikatory [ ] i ( )
działają w kierunku od lewej do prawej.
Przykłady:
int pudelka[3][12];
// [3] działa pierwszy, dlatego jest to tablica 3 tablic 12 wartości typu int
short *pola[10];
// [10] działa pierwsze, więc jest to tablica 10 wskaźników do danych typu short
char (*wz)[80];
// (*wz) działa pierwsze, więc jest to wskaźnik do tablicy 80 wartości char
int (*bor)[5][6];
// wskaźnik do tablicy 5x6 wartości typu int
double (* mur[4])[2];
// 4-elementowa tablica wskaźników do 2-elementowych tablic typu double
char * tip(int);
// prototyp funkcji zwracającej wskaźnik do char
char (* top)(int);
// wskaźnik do funkcji zwracającej wartość typu char
char (* hop[3])(short);
// tablica 3 wskaźników do funkcji zwracających wartość typu char
Manipulowanie bitami
Wszystkie cztery bitowe operatory logiczne (~, &, |, ^) przetwarzają dane należące do typów całkowitych,
włącznie z typem char. Nazywa się je operatorami bitowymi, ponieważ działają one na każdy bit niezależnie
od bitów sąsiednich. Nie należy mylić ich ze zwykłymi operatorami logicznymi (&&, ||, !), które operują na
całych wartościach.
Bitowa negacja (dopełnienie jedynkowe): ~
Operator ~ zmienia każde zero na jedynkę i każdą jedynkę na zero, tak jak w poniższym przykładzie:
~(11000110) == (00111001)
// Uwaga! To nie jest zapis w języku C.
Przykład:
unsigned int bzn=2; int zn=2;
~bzn==4294967293
~4294967293==2
// Jeżeli zmienna typu int zajmuje 4 bajty.
~zn==-3
~(-3)==2
Bitowa koniunkcja (AND): &
Dwuargumentowy operator & tworzy nową wartość przez porównanie kolejnych bitów dwóch operandów.
Bit wynikowy jest równy 1 tylko wtedy, gdy oba bity na tej samej pozycji w operandach są równe 1, tak jak
w poniższym przykładzie:
(11001010) & (01001011) == (01001010)
Przykłady:
2&2==2
5&99==1
20&154==16 04&015==04 023&017==03
Bitowa alternatywa (OR): |
Dwuargumentowy operator | tworzy nową wartość przez porównanie kolejnych bitów dwóch operandów.
Bit wynikowy jest równy 1, gdy przynajmniej jeden z bitów na tej samej pozycji w operandach jest równy 1,
tak jak w poniższym przykładzie:
69
(11001010) | (01001011) == (11001011)
Przykłady:
2|2==2
5|99==103
20|154==158 04|015== 015 023|017==037
Bitowa alternatywa wyłączająca (XOR): ^
Dwuargumentowy operator ^ tworzy nową wartość przez porównanie kolejnych bitów dwóch operandów.
Bit wynikowy jest równy 1, gdy dokładnie jeden z bitów na tej samej pozycji w operandach jest równy 1, tak
jak w poniższym przykładzie:
(11001010) ^ (01001011) == (10000001)
Przykłady:
2^2==0
5^99==102 20^154==142 04^015==011 023^017=034
Stosowanie maski: &=maska
Operator bitowej koniunkcji jest często wykorzystywany w połączeniu z tzw. maską. Maska (ang. mask)
jest układem bitów, w którym niektóre bity są włączone (1), a niektóre wyłączone (0). Stosowanie maski do
zmiennej zm polega na przypisaniu zmiennej zm wyrażenia, które jest bitową koniunkcją zmiennej zm i
maski. Można sobie wyobrazić, że pola z zerami w masce są nieprzezroczyste, a z jedynkami przezroczyste.
Kiedy przykryje się taką maską zmienną, wtedy w nowej zmiennej nieprzezroczyste pola mają wartość 0, a
przezroczyste taką samą jak w starej zmiennej. Mówimy w tym wypadku, że stosujemy maskę do zmiennej.
Przykłady:
unsigned MASKA=02, MASKA2=0377, zm1=05, zm2=073, zm3=27383, zm4=73637;
zm1=zm1&MASKA; lub zm1&=MASKA;
// Teraz zm1==0
zm2=zm1&MASKA; lub zm2&=MASKA;
// Teraz zm1==02
zm3&=MASKA2 ;
// Teraz zm3==247
zm4&=MASKA2 ;
// Teraz zm4==165
Wartości zwanych maskami używa się nie tylko do zakrywania bitów, ale także do włączania, wyłączania,
czy odwracania bitów w zmiennych.
Włączanie bitów: |= maska
Czasami zachodzi potrzeba włączenia określonych bitów w wartości zmiennej, przy jednoczesnym
pozostawieniu reszty bitów bez zmian. Można to osiągnąć za pomocą bitowego operatora alternatywy.
Tworzy się najpierw maskę z bitami włączonymi na pozycjach, które mają być włączone w zmiennej, a na
innych pozycjach przyjmującymi wartość 0. Następnie do zmiennej przypisuje się wynik bitowej
alternatywy zmiennej i maski. Mówimy, że włączamy bity w zmiennej za pomocą maski.
Przykłady:
unsigned MASKA=02, MASKA2=042, zm1=0x80, zm2=012, zm3=27383, zm4=73637;
zm1=zm1|MASKA; lub zm1|=MASKA;
// Teraz zm1==0202
zm1|=MASKA2;
// Teraz zm1==0xa2
zm2|=MASKA2;
// Teraz zm2==052
zm3|=MASKA;
// Teraz zm3==27383
zm4|=MASKA2;
// Teraz zm4==73639
Wyłączanie bitów: &=~maska
Jeśli chce się wyłączyć określone bity w wartości zmiennej można uczynić to używając maski. Tworzy się
najpierw maskę z bitami włączonymi na pozycjach, które mają być wyłączone w zmiennej, a na innych
pozycjach przyjmującymi wartość 0. Następnie do zmiennej przypisuje się wynik bitowej koniunkcji
zmiennej i dopełnienia jedynkowego maski. Mówimy, że wyłączamy bity w zmiennej za pomocą maski.
Przykłady:
unsigned MASKA=02, MASKA2=042, zm1=0x80, zm2=012, zm3=27383, zm4=73637;
zm1=zm1&~MASKA; lub zm1&=~MASKA; // Teraz zm1==0200
zm1&=~MASKA2;
// Teraz zm1==0x80
zm2&=~MASKA2;
// Teraz zm2==010
zm3&=~MASKA;
// Teraz zm3==27381
zm4&=~MASKA2;
// Teraz zm4==73605
Odwracanie bitów: ^=maska
Odwrócenie bitu oznacza włączenie go, jeśli jest wyłączony, lub wyłączenie go, jeśli jest włączony. Żeby
odwrócić wybrane bity, tworzy się maskę z bitami włączonymi na pozycjach, które mają być odwrócone w
zmiennej, a na innych pozycjach przyjmującymi wartość 0. Następnie do zmiennej przypisuje się wynik
bitowej alternatywy wyłączającej zmiennej i maski. Mówimy, że odwracamy bity w zmiennej za pomocą
maski.
Przykłady:
70
unsigned MASKA=02, MASKA2=042, zm1=0x80, zm2=012, zm3=27383, zm4=73637;
zm1=zm1^MASKA; lub zm1^=MASKA;
// Teraz zm1==0202
zm1^=MASKA2;
// Teraz zm1==0xa2
zm2^=MASKA2;
// Teraz zm2==050
zm3^=MASKA;
// Teraz zm3==27381
zm4^=MASKA2;
// Teraz zm4==73607
Sprawdzenie wartości bitu
Żeby sprawdzić czy w wartości zmiennej jest włączony określony bit lub grupa bitów, można posłużyć się
maską. Bity w takiej masce powinny być włączone na pozycjach, które mają być sprawdzone, a wyłączone
na pozostałych. Odpowiedzią na pytanie, czy grupa bitów określonych przez maskę jest włączona w
zmiennej jest wartość wyrażenia, w którym z jednej strony operatora porównania znajduje się bitowa
koniunkcja zmiennej i maski, a z drugiej maska.
Przykłady:
unsigned MASKA=04, zm1=0253, zm2=0x465, zm3=247;
(zm1 & MASKA) == MASKA
// Fałsz
(zm2 & MASKA) == MASKA
// Prawda
(zm3 & MASKA) == MASKA
// Prawda
~~~~~~~~~~~~~~~~~~~~~~~~~ bity.c ~~~~~~~~~~~~~~~~~~~~~~~~
#include <stdio.h>
int main(void)
{
unsigned MASKA=04, za=0253, zb=0xa61, zc=973;
unsigned MASKB=020, zd=0157, ze=0x4e5, zf=1787;
unsigned MASKC=0101, zg=043, zh=0x16f, zi=521;
printf("%u %u %u %#x %#x %#x\n",~za,~zb,~zc,~za,~zb,~zc);
printf("%u %u %#o %#o\n",za&zb,zb&zc,za&zb,zb&zc);
printf("%u %u %#o %#o\n",za|zb,zb|zc,za|zb,zb|zc);
printf("%u %u %#o %#o\n" ,za^zb,zb^zc,za^zb,zb^zc);
printf("%o %o %#x\n",za&MASKA,zb&MASKA,zc&MASKA);
printf("%o %o %#x\n",zd|MASKB,ze|MASKB,zf|MASKB);
printf("%o %o %#x\n",zg&~MASKC,zh&~MASKC,zi&~MASKC);
printf("%o %o %#x\n",za^MASKA,zb^MASKA,zc^MASKA);
printf("%d %d %d\n",(za & MASKA)==MASKA,
(zb & MASKA)==MASKA, (zc & MASKA)==MASKA);
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$bity
4294967124 4294964638 4294966322 0xffffff54 0xfffff59e 0xfffffc32
33 577 041 01101
2795 3053 05353 05755
2762 2476 05312 04654
0 0 0x4
177 2365 0x6fb
42 456 0x208
257 5145 0x3c9
0 0 1
$