Wstęp do języka programowania C dla nie-informatyków

1/ Struktura programu w C

2/ Instrukcja skoku goto

3/ Sterowanie

4/ Punkty sekwencyjne

5/ Infix notation

6/ Operacje na plikach

7/ Wskaźniki

8/ Tablice

9/ Biblioteki

10/ System I/O i funkcje

1/ Struktura programu w C

Podstawy składni:

Składnia z dowolnym użyciem spacji lub tabulatorów (whitespace) w kodzie. Komentarze w /* i */ lub jak w C99 za // do końca linii (nie obowiązuje to w nowej linii).

Kod zawiera deklaracje funkcji (function declarations) i definicjefunkcji (function definitions). Definicjefunkcji zawierają z kolei deklaracje elementów funkcji np. zmienne i instrukcje (statements).

Deklaracje określające nowe typy używają słów kluczowych (keywords) i przydzielają typy danych i rezerwują pamięć dla nowych zmiennych, zazwyczaj przez zapisanie typu z nazwą zmiennej. Słowa kluczowe określają wbudowane typy.

Sekcje kodu są zamknięte w nawiasach sześciennych {} dla ograniczenia zakresu (scope) deklaracji i aby kod działał jako pojedyncza instrukcja dla struktur kontrolujących.

Instrukcje określają akcje. Najpopularniejszą instrukcją jest instrukcja wyrażenia (expression statement), składająca się z wyrażenia do przetworzenia zamkniętego średnikiem. Pobocznym skutkiem przetwarzania może być wywołanie funkcji i przydzielenia zmiennym nowych wartości. Aby modyfikować normalne sekwencyjne wykonanie tych instrukcji, mamy kilka instrukcji kontrolujących przepływ identyfikowanych za pomocą zarezerwowanych słów kluczowych. Programowanie strukturalne jest wsparte przez wykonanie warunkowe if(-else) i przez do-while, while, i dla pętli. Instrukcja for ma oddzielne instrukcje dla inicjalizacji, testowania i ponownej inicjalizacji, co może być ominięte. break i continue mogą być użyte do pozostawienia najbardziej wewnętrznej instrukcji pętli lub przeskoczenia pętli dla jej ponownej inicjalizacji.

Deklaracje

Najpierw dla kodu programu musimy zrobić listę nazw i typów wszystkich zmiennych, które będą użyte w programie i dostarczyć informacje o tym, gdzie mają być użyte. To jest zw. deklarowanie zmiennych. To ma dwa cele: daje informacje kompilatorowi o ostatecznej liście zmiennych, umożliwiając temu sprawdzić błędy i informować kompilator jak wiele przestrzenie musi być zarezerwowanej dla każdej zmiennej, kiedy program działa. C wspiera różne typy zmiennych (zawierają one różne rodzaje danych) i pozwala jednemu typowi zmiennej być zamienioną na inną zmienną. Typ zmiennej jest ważny dla kompilatora. Jeśli nie zadeklarujemy zmiennej, lub zadeklarujemy zły typ, będziemy mieli compilation error.

Zarezerwowane słowa

Programy C są konstruowane z zestawu zarezerwowanych słów, które dostarczają kontrolę i są też z bibliotek, które pełnią specjalne funkcje. Podstawowe instrukcje są zbudowane przy użyciu zarezerwowanych zestawów słów takich jak main, for, if, while, default, double, extern, for i int. Tylko dla poleceń i instrukcji. Nie używamy do naszych zmiennych.

Słowa używane w zawartych bibliotekach też stają się słowami zarezerwowanymi. Biblioteki dostarczają często używanych funkcjonalności i przynajmniej jedna biblioteka musi być zawarta w każdym programie. Np. biblioteka C ze standardowymi funkcjami. Np. biblioteka stdio, która jest częścią biblioteki C.

Większość programów użytkowych z C jest dostarczanych jako biblioteki, które są zawarte w programach jako (jednostki rozszerzenia standardu) plug-in expansion units. Podczas gdy cechy dane przez biblioteki nie są ściśle częścią samego języka C, one są ważne i zawsze są z wersjami C. Kiedy biblioteka została zawarta do programu, jej funkcje są określane i nie możemy używać ich nazw.

Typy danych

typy wbudowane dla liczb całkowitych (integers) o różnych rozmiarach, zarówno ze znakiem liczby (signed) lub bez (unsigned), liczby zmiennoprzecinkowe (floating-pointnumbers), znaki (characters) i typy wyliczane (enum, enumerated types). C99 dodaje typy booleanowskie (boolean). Są też typy pochodne: tablice, wskaźniki, rekordy, czyli struktury (records, struct) i niezłączone połączenia (untagged unions, union), czyli unie.

Zmiana typu przez rzutowanie typu (type cast) dla jednoznacznej konwersji wartości z jednego typu na inny lub przez użycie wskaźników (pointers) lub w unii (unions) dla ponownej interpretacji przedłożonych bitów wartości w inny sposób. Te same bity, ale inny sposób interpretacji.

Operatory

Są to symbole używane w wyrażeniach dla określenia manipulacji do wykonania podczas przetwarzania wyrażenia.

arithmetic (+, , *, /, %)

assignment (=) and augmented assignment (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=)

bitwise logic (~, &, |, ^)

bitwise shifts (<<, >>)

boolean logic (!, &&, ||)

conditional evaluation (? :)

equality testing (==, !=)

function argument collection (( ))

increment and decrement (++, –)

member selection (., ->)

object size (sizeof)

order relations (<, <=, >, >=)

reference and dereference (&, *, [ ])

sequencing (,)

subexpression grouping (( ))

type conversion ((typename))

Pierwszeństwo operatorów

wiązanie == mocniejsze niż & i | w wyrażeniach takich jak x & 1 == 0, dlatego piszemy (x & 1) == 0.

= oznacza wyznaczenie (assignement), a == to równość (equality), chociaż można stosować wymiennie)

Prosty program „Hello world” w C kiedyś mógł wyglądać tak:

main()

{

printf(“hello, world\n”);

}

Ale teraz zgodnie ze standardem program “hello, world” powinno być:

#include <stdio.h>

int main(void)

{

printf(“hello, world\n”);

return 0;

}

Pierwsza linia tego programu zawiera dyrektywę przedprzetworzeniową (preprocessing directive) wskazaną przez #include. To powoduje, ze preprocessor, czyli pierwsze narzędzie badające kod źródłowy podczas kompilowania, podstawia tę linię całym tekstem stdio.h. standard header, który zawiera deklaracje dla standardowych funkcji wejścia i wyjścia takich jak printf. angle brackets otaczające stdio.h wskazują, że to jest ulokowane przy użyciu strategii, która preferuje standard headers od innych headerów mających tę sama nazwę. Podwójny cudzysłów może też być użyty do zawarcia lokalnych lub specyficznych dla danego projektu header files.

#include “nazwa_pliku”
//wstawianie plików – w/w wiersz jest zastępowany plikiem o nazwie //nazwa_pliku
#include <nazwa_pliku>
//efekt zastosowania tej instrukcji jw. z tym, że dodatkowo

//zleca się kompilatorowi poszukiwanie pliku w
//pewnym wyróżnionym katalogu

//(w Unix jest to skorowidz/usr/include)

Są też inne dyrektywy, spośród których najczęstszą jest #define:
#define YES 1

#define NO 0

//zastąpienie nazwy przez ciąg znaków

Następna linia wskazuje, że jest definiowana funkcja zw. main. Ma ona specjalne znaczenie – środowisko uruchomieniowe wywołuje tę funkcję main, aby zacząć wykonanie programu. Określnik Typu (type specifier) int wskazuje, że zwracana wartość (return value), tj. ta wartość, która jest zwrócona do wywołujacego (invoker), czyli tutaj środowiska uruchomienieniowego, jest liczbą całkowitą (integer). Kluczowe słowo void jako lista parametrów (parameter list) wskazuje, że funkcja main nie bierze żadnych argumentów. Lista jest w nawiasach zwykłych funkcji ().

Następna linia wywołuje (call- kieruje wykonanie do) funkcję zw. printf, która została zadeklarowana w stdio.h i jest dostarczona z systemowej biblioteki. W tym wywołaniu funkcja printf przechodzi (passed- dostarczana z) z pojedynczym argumentem, adresem pierwszego znaku w ciągu literowego “hello, world\n”, czyli h. Ten ciąg literowy jest bezimienną tablicą (array) z elementami w typie char (poszczególne znaki), ustawianą automatycznie przez kompilator z ostatnim znakiem o wartości 0 dla oznaczenia końca tego tablicy (printf musi to wiedzieć, bo koniec ciągu musi być jasno zaznaczony: hello, world\n0). \n jest escape sequence, który tłumaczy na znak newline, co w danych wyjściowych oznacza koniec bieżącej linii. Wartością zwróconą (return value) funkcji printf jest w typie int, ale to jest odrzucane bez zawiadomienia, jeśli nie jest używane. Lepszy program mógłby przetestować wartość zwróconą, czy funkcja printf udała się czy nie. Średnik kończy każdą instrukcję.

Instrukcja return kończy wykonanie funkcji main i powoduje, że to zwraca liczbę całkowitą o wartości 0, co jest interpretowane przez uruchomiony system jako kod wyjściowy wskazujący na sukces wykonania. Wartość niezerowa to błąd.

Nie sprawdza liczby i typu argumentów, kiedy deklaracja funkcji ma pusty parametrlisty,czyli void.

void funkcja (void)

Zamykający nawias sześcienny wskazuje koniec kodu dla funkcji main.

main ()
{
…..
}

W programie deklaracja zmiennych wygląda następująco:

Int zmienna_całkowita 1, zmienna_całkowita2, …;
Float zmienna_zmiennoprzecinkowa1, zmienna_zmiennoprzecinkowa2, …;
Char zmienna_znakowa1, zmienna_znakowa2, …;

//komentarz

/* kom

mentarz */

Instrukcje podstawowe

a = 0;
//przypisanie wartosci 0 zmiennej a
I++;
// inkrementacja zmiennej I

Instrukcja złożona – blok

{
instrukcjal;
instrukcja2;

instrukcja3;
…..
}

if (wyrażenie)

instrukcja1;
//jeżeli spełniony jest warunek wykonywana jest instrukcja1
lub
if (wyrażenie)

instrukcja1;

else

instrukcja2;
//jeżeli spełniony jest warunek wykony wana jest instrukcja1 w przeciwnym wypadku wykonuje się instrukcja2

Instrukcja null (null statement)

Instrukcja null jest instrukcją pustą (blank) i nie robi nic, ale jest zakończona jak każda inna instrukcja, czyli średnikiem.

int x=5; // zwykła instrukcja

; //instrukcja null

Dostarcza działania null w sytuacji, gdzie gramatyka języka wymaga instrukcji, ale program nie chce żadnej akcji. Wtedy oznaczamy to tak:

;

Użyteczna jest z instrukcjami if, while, do i for. Operacje pętli, kiedy cała aktywność pętli jest wykonywana przez testową część pętli. Np. ta instrukcja znajduje pierwszy element tablicy, który ma wartość 0:

for (i=0; array[i] != 0; i++)

;

W tym wypadku instrukcja for jest wykonywana tylko dla swoich efektów ubocznych. Ciało pętli jest instrukcją null.

Instrukcja null jest też użyteczna, gdzie etykieta jest potrzebna tuż przed nawiasem, który kończy instrukcję złożoną. Etykieta nie może bezpośrednio poprzedzać prawego nawiasu. To musi być dołączone do instrukcji. Np.:

if (expression1)

{

goto label_1; /* Terminates this part of the if statement */

label_1: ;

}

else …

Każda instrukcja kończy się średnikiem!

2/ Instrukcja skoku goto

Jest też niestrukturalna instrukcja goto która idzie bezpośrednio do wyznaczonej etykiety (label) w danej funkcji. instrukcja w językach programowania, która powoduje przekazaniesterowania w inne miejsce, tzw. skok. Miejsce skoku identyfikuje się za pomocą numeru wiersza programu (zwykle w językach interpretowanych) bądź etykiety (najczęściej w językachkompilowanych).

Przykład

Niżej przedstawiono kod C z instrukcją skoku jest goto, po której następuje identyfikator etykiety (w kodzie etykieta zakończona jest dwukropkiem). Wynikiem działania poniższego programu jest wyświetlenie napisu „Mirek ma samochód”.

goto C;

A:

printf(“samochód\n”);

goto D;

B:

printf(“ma “);

goto A;

C:

printf(“Mirek “);

goto B;

D:

Wady używania goto w C:

Edsger Dijkstra powiedział, że nadmierne użycie i niestrukturalne użycie goto może:

– prowadzi do bardzo złego, nieczytelnego i niestrukturalnego kodu

– zwiększa skomplikowaność debugowania i analizy kodu

Inne tego typu ukryte goto:

– break

– continue

– return

Zasady użycia goto:

– skok do lokacji do przodu, a nie do tyłu – dobre w czynnościach czyszczenia

– skok do tyłu przez continue wewnątrz pętli. Goto do tyłu tylko w niewielkiej liczbie, bez komplikacji kodu

– zamiast goto używamy

do

{

…. if(xx)

break; …

}

while(0);

– nie używamy do skoku do innego bloku kodu/zasięgu. Zamiast tego struktura kodu ma być taka, że goto-labels są umieszczane na początku kodu bloku kodu/zasięgu, a nie wewnątrz

– goto jest szybkie

– goto używamy tylko kiedy trzeba i to jest uzasadnione, bo alternatywa goto może np. spowalniać kod

– użycie goto/break/continue/return nie może tworzyć kodu, do którego nie ma dostępu

– w pewnych standardach użycie goto jest niedopuszczalne (w.r.t.)

3/ Sterowanie

Do zaprogramowania każdego wykresu przepływu czy każdego automatu o skończonej liczbie stanów bez duplikacji kodu, a więc do sterowania przepływem kontroli w kodzie, którego żadna część się nie powtarza, wystarczą instrukcje skoku warunkowego (poprzedzone instrukcją porównania) i bezwarunkowego.

Pierwsze języki, przede wszystkim ze względu na nacisk na łatwość konstrukcji kompilatorównie odbiegały znacząco zasobem instrukcji od asemblera, np. w pierwszych wersjach Fortrana jedyną instrukcją warunkową był właśnie skok – nie można było warunkowo przypisać wartości czy wykonać grupy poleceń. Z czasem do popularnych języków (wyrosłych na podstawie Fortrana) zaczęto dodawać inne instrukcje kontroli przepływu: wykonania warunkowego (if, if-else), różne rodzaje pętli(while, for), rekurencyjność, instrukcje ponowienia (redo), następnejiteracji(next lub continue) lub zakończeniawykonywania (last lub break) pętli, te same instrukcje z wielopoziomowymi pętlami (foreach), wyjątki, iteratory,funkcje wyższego rzędu, wątkiitd., co uczyniło z instrukcji skoku instrukcję przestarzałą. A mimo to ciągle występuje w niektórych językach.

if (warunek)

{

polecenie1

}

else

{

polecenie2

}

Instrukcja switch wybiera case do wykonania w oparciu o wartość wyrażenia integer (i).

switch(i)

{

case -1:

n++;

break;

case 0 :

z++;

break;

case 1 :

p++;

break;

}

4/ Punkty sekwencyjne (Sequence points)

Wyrażenia mogą używać wielu wbudowanych operatorów (operators) i mogą zawierać wywołania funkcji (function calls). Nieokreślony jest porządek, w jakim są przetwarzane argumenty (arguments) dla funkcji i operandy (operands) dla większości operatorów (operators). Przetwarzanie może być przekładane. Jednak wszystkie efekty uboczne (w tym przechowywanie w zmiennych) dzieją się przed następnym punktemsekwencyjnym (sequence point). Stanowią one koniec każdej instrukcji wyrażenia (expression statement) oraz wynik tego zostaje wypisany. I tak jest ze zwrotem z każdego wywołania funkcji (function call). Punkty sekwencyjne też zdarzają się podczas przetwarzania wyrażeń zawierających pewne operatory (&&, ||, ?:oraz operator kropka .). To pozwala na wyższy poziom optymalizacji kodu obiektu przez kompilator.

Punkt sekwencyjny w programowaniu używającym instrukcji (imperative programming) określa jakikolwiek punkt w wykonaniu komputerowego programu, przy którym jest gwarantowane, że wszystkie efekty uboczne poprzednich obliczeń (evaluation) zostały wykonane, a żadne efekty uboczne z następujących obliczeń nie zostały jeszcze wykonane.

Obliczenia wykonane |SEQUENCE POINT| Obliczenia będą wykonane

Ostateczny wynik Ostateczny wynik

Jest to częste w C i C++, bo wynik pewnych wyrażeń może zależeć od porządku obliczeń ich podwyrażeń. Dodając jeden lub więcej takich punktów sekwencyjnych daje nam zwięzłość wykonania, bo to ogranicza możliwe porządki obliczeń. Wynik jest bardziej przewidywalny.

Mamy dwie funkcje l() i k(). W C i C++ operator + nie jest kojarzony z jakimkolwiek punktem sekwencyjnym i dlatego w wyrażeniu

l() + k()

może być wykonane najpierw albo l() albo k(). Tutaj operator przecinka (,) wprowadza punkt sekwencyjny i dlatego kod

k(),k()

porządek obliczeń jest z góry określony: najpierw l(), a potem k() jest wywoływany.

Punkty sekwencyjne są też przydatne, kiedy ta sam zmienna jest modyfikowana więcej niż raz w pojedynczym wyrażeniu. Np. Wyrażenie C

i = i++

które albo przydziela i jej poprzednią wartość, albo zwiększa i o 1. Ostateczna wartość i jest niewiadoma, bo może mieć dwie wartości, zależnie od porządku obliczania wyrażenia — zwiększenie może być przed, po lub w trakcie przydzielenia. Może być to określone w języku lub po prostu nieokreślone (undefined). W C i C++ obliczanie takiego wyrażenia daje zachowanie nieokreślone.

Punkty sekwencyjne w C i C++:

W C++ przeładowane operatory działają jak funkcje i dlatego operatory, które zostały przeładowane, wprowadzają punkty sekwencyjne w ten sam sposób co wywołania funkcji.

Punkty sekwencyjne w C/C++ występują:

– Między obliczaniem lewych i prawych operandów && (AND), || (OR) i operatorów przecinków. Np. w wyrażeniu

*p++ != 0 && *q++ != 0

wszystkie efekty uboczne podwyrażenia

*p++ != 0

są kończone przed jakąkolwiek próbą dostępu do q.

– Między obliczeniem pierwszego operandu ternerego znaku zapytania (?) a operandem drugim lub trzecim. Np. W wyrażeniu

d = (*p++) ? (*p++) : 0

Jest punkt sekwencyjny po pierwszym:

*p++

czyli zanim ta druga instancja *p++ jest wykonana, ta pierwsza musi zostać zwiększona.

– na końcu pełnego wyrażenia. Ta kategoria zawiera instrukcje wyrażeń (takich jak przydzielenie a = b;), instrukcje return, instrukcje kontrolne if, switch, while,do-while oraz wszystkie trzy wyrażenia w instrukcji for.

– Zanim funkcja jest wprowadzona w wywołaniu funkcji. Porządek, w którym argumenty są obliczane nie jest określony, ale ten punkt sekwencyjny znaczy, że wszystkie ich efekty uboczne (tutaj: zwiększenie parametru o 1) są zakończone zanim funkcja jest wprowadzana przy jej wywołaniu. W wyrażeniu

f(i++) + g(j++) + h(k++), f

funkcja jest wywoływana z parametrem oryginalnej wartości i, ale i jest zwiększany przed wprowadzeniem ciała f przy wywołaniu.

int i = 1;

a = f(i++)

//wywołanie funkcji z parametrem o wartości 1 (bez //zwiększenia)

printf (“%d”,a);

//zmienna a ma wartość 2 (zwiększenie i o 1, czyli 2)

Podobnie j i k są uaktualniane przed wprowadzeniem ciał g i h odpowiednio przy wywołaniu. Jednak to nie jest okeślone w jakiej kolejności wykonywane są f(), g() i h(), ani w jakim porządku zwiększane są i, j oraz k. Zmienne j oraz k w ciele f unkcji może lub może jeszcze nie być zwiększone. Tego nie wiemy, ale po zakończeniu wywołania funkcji wiemy, że i jest na pewno zwiększone. Wywołanie funkcji f(a,b,c) nie jest użyciem operatora przecinka i porządek obliczeń dla a, b oraz c nie jest określony.

Przy zwrocie funkcji (return) wartość zwracana jest kopiowana do wywołującego kontekstu. Ten punkt sekwencyjny jest tylko określony w standardzie C++, nieoficjalnie w C.

– Na końcu inicjalizatora np. Po obliczaniu 5 w deklaracji:

int a = 5;

Inne obliczenia nastąpią, jeśli a otrzyma wartość 5.

– Między każdym deklaratorem w sekwencji każdego deklaratora – np. Między tymi dwoma obliczeniami a++ w:

int x = a++, y = a++;

Jeśli a wynosi 5, to x wyniesie 6 i dopiero potem to drugie wyniesie 6.

5/ Infix notation

W C brak operatorów infix dla skomplikowanych obiektów, zwł. dla operacji na ciągach.

Infix notation jest powszechną logiczną formułą, w której opeartory są zapisane stylem infix między operandami, na których one działają (np. 2+2). Nie jest proste parsować przez komputery jak prefix notation (np. +22) lub postfix notation (np. 22+), ale wiele języków używa tego.

W infix notation, nie tak jak w prefix notation lub postfix notation, nawiasy otaczające grupy operandów i operatorów są potrzebne dla wskazania odpowiedniego porządku, w którym operacje będą wykonywane. Jeśli nie ma nawiasów, jest pewien priorytet porządku operacji.

Infix notation może być też odróżniona od function notation, gdzie nazwa funkcji sugeruje konkretną operację i jej argumenty są składnikami. Np. Dodaj(1,3) gdzie funkcja Dodaj oznacza dodawanie:

Dodaj(1,2) = 1+2 = 3.

Przykłady Infix Notation

(1 × 23) – 3 + 4 × 5 byłoby obliczane jako:

(1 × 2 ^ 3) – 3 + 4 × 5

= (1 × 8) – 3 + 4 × 5

= 8 – 3 + 4 × 5

= 8 – 3 + 20

= 5 + 20

= 25

Składnia deklaracji nie jest zbyt intuicyjna, zwł. dla wskaźników do funkcji (function pointers).


6/ Operacje na plikach


Aby otworzyć plik i związać z nim strumień, należy użyć funkcji
fopen(). Wygląda ona następująco:

FILE *fopen(char *nazwa_pliku,char *tryb);

FILE jest typem danych w rodzaju struktury. Funkcja fopen() korzysta z pliku nagłówkowego stdio.h. Nazwa otwieranego pliku jest wskazywana przez wskaźnik nazwa_pliku. Musi to być nazwa poprawna w używanym systemie plików. Możliwe wartości trybu znajdują się w poniższym zestawieniu:

Tryb

Opis

r

Otwieranie pliki tekstowego do odczytu

w

Tworzenie pliku tekstowego do zapisu

a

Dołączanie do pliku tekstowego

rb

Otwieranie pliku binarnego do odczytu

wb

Tworzenie pliku binarnego do zapisu

ab

Dołączanie do pliku binarnego

r+

Otwieranie pliku tekstowego do odczytu-zapisu

w+

Tworzenie pliku tekstowego do odczytu-zapisu

a+

Dołączanie do pliku tekstowego do odczytu-zapisu lub jego tworzenie

r+b

Otwieranie pliku binarnego do odczytu-zapisu; można także użyć rb+

w+b

Tworzenie pliku binarnego do odczytu-zapisu; można także użyć wb+

a+b

Dołączanie pliku binarnego do odczytu-zapisu lub jego tworzenie; można także użyć ab+

Poprawnie wykonana funkcja fopen() zwraca wskaźnik null.

Aby zamknąć plik należy skorzystać z funkcji fclose(). Wygląda ona następująco:

int fclose(FILE *fp);

Funkcja fclose() zamyka plik związany ze wskaźnikiem fp, który musi być poprawnym wskaźnikiem otrzymanym w wyniku użycia funkcji fopen(), oraz odłącza strumień od pliku. Wskaźnik *fp jest symbolem pliku (występującego pod nazwą pliku). A FILE jest typem danych tego wskaźnika.
Nie wolno wywoływać funkcji 
fclose() z niepoprawnym argumentem. Może to spowodować uszkodzenie systemu plików i nieodwracalną utratę danych. Po prawidłowym wykonaniu funkcji fclose() zwraca ona wartość zero. Jeśli wystąpi błąd zwraca ona EOF.

Parametry programu

Jako parametru używa się głównie nazwy pliku, którego będzie wykorzystywał program. Można również w sekcji funkcji main podać parametry programu.

7/ Wskaźniki

To prosty typ odnośników (references), który zapisuje adres i lokację obiektu lub funkcji w pamięci. Wskaźniki mogą być manipulowane przez użycie przydzielania (assignment) i też arytmetycznegowskaźnika (pointer arithmetic). Uruchomieniową reprezentację wartości wskaźnika jest typowo surowy adres surowej pamięci (raw memory address), ale skoro typ wskaźnika zawiera typ danej rzeczy, do której wskazuje, wyrażenia zawierające wskaźniki mogą być sprawdzane pod względem typu podczas kompilacji.

Wskaźniki arytmetyczne są skalowane automatycznie do typu wskazywanych danych. Czyli wskaźnik wskazuje na pierwszy blok pamięci i wie, gdzie jest jego koniec przez wielkość danego typu np. int ma 4 bajty i wskaźnik wskazuje na adres pierwszego bajtu i wie, że koniec jest na czwartym bajcie.

Ciągi tekstowe są manipulowane przez wskaźniki jako tablice znaków.

Wskaźnik null (null pointer) jest wskaźnikiem, który nie wskazuje na żadną ważną lokację przez posiadanie wartości 0. Jest ważny dla wskazania specjalnego przypadku, takiego jak nieobecność żadnego następnego wskaźnika w ostatnim węźle listy połączonej linkami lub jako wskazanie błędu z funkcji zwracających wskaźniki. W kodzie wskaźniki null są zazwyczaj reprezentowane przez 0 lub NULL.

int *p = NULL

Aby mieć pewność, że wskaźnik nie jest zainicjowany na złe miejsce w pamięci.

Wskaźniki void (Void pointers (void *)) wskazują na obiekty o nieznanym typie i mogą dlatego być użyte jako ogólne wskaźniki danych. Skoro rozmiar i typ wskazywanego obiektu nie jest znany, wskaźniki void nie mogą mieć usuniętej pośredniości, ani nie ma dozwolonego wskaźnika arytmetycznego do nich, chociaż one mogą być łatwo konwertowane do lub z innego typu wskaźnika obiektu.

Wskaźniki nie są sprawdzane i dlatego są niebezpieczne. Wskaźnik do jakiejś lokacji może spowodować nieoczekiwany efekt. Np. wskaźniki arytmetyczne mogą sprawiać, że obiekty, na które wskazują, mogą być zwalniane i jeszcze raz używane (wskaźnikizawieszone, dangling pointers). Mogą też być używane bez inicjalizacji (obłędne wskaźniki, wild pointers) lub mogą być bezpośrednio przydzielane do niebezpiecznych wartości używając cast, union lub przez inny mogą zniszczyć wskaźnik. Można zmieniać typy wskaźników.

8/ Tablice

W języku C tablica jest listą (ciągiem) zmiennych, które są tego samego typu i do których można się odwołać za pomocą wspólnej nazwy. Pojedyncza zmienna w tablicy jest nazywana elementem tablicy. Tablice są prostym sposobem obsługiwania grup powiązanych danych.

Aby zdeklarować tablicę, należy użyć następującego schematu:

typ nazwa-tab[wielkość];

Typ musi być poprawnym typem języka C, nazwa-tab definiuje nazwę tablicy, a wielkość – liczbę jej elementów (rozmiar). Deklaracja 20-sto elementowej tablicy liczb całkowitych o nazwie mojatablica, będzie wyglądać następująco:

int mojatablica[20];

Element tablicy jest dostępny za pomocą indeksowania tablicy numerami elementów. W języku C tablica zaczyna się od indeksu zerowego. Oznacza to, że aby osiągnąć pierwszy element tablicy, należy użyć zera jako indeksu. Indeksowanie tablicy polega na podaniu numeru szukanego elementu w nawiasach kwadratowych.

Poniższy zapis odwołuje się do drugiego elementu tablicy:

mojatablica[1] 

Pamiętaj o tym, że tablice rozpoczynają się od numeru 0, zatem indeks 1oznacza drugi element tablicy!

Aby nadać elementowi tablicy wartość, należy ją zapisać w instrukcji przypisania:

mojatablica[0] = 100

Powyższa instrukcja nadaje pierwszemu elementowi tablicy (o nazwie: mojatablica) wartość 100.

Jest to typ ustalony o rozmiarze statycznym określonym podczas kompilacji. C99 też daje tablice o długości zmiennej, niestatycznej. Można też alokować blok pamięci (o rozmiarze dowolnym) w czasie uruchomienia, używając funkcji malloc standardowej biblioteki i traktować to jako tablicę. Unifikacja w C tablic i wskaźników znaczy, że prawdziwe tablice i te dynamicznie alokowane, symulowane tablice, są wirtualnie wymienne. Skoro tablice są zawsze dostępne przez wskaźniki, dostępy do tablic nie są typowo sprawdzane wg. przedłożonych rozmiarów tablic, chociaż kompilator może nadać granice sprawdzania jako opcję. Przekroczenie ich może zniszczyć dane.

Nie ma możliwości deklaracji tablic o wielu rozmiarach, ale polegamy na rekursywności typu SO dla deklaracji tablic tablic, które robią to samo.

Możemy napisać do szóstego elementu tablicy tablicę z pięcioma elementami, ale będziemy mieli niechciane rezultaty), dlatego tablica musi być statyczna. Ten błąd to przepełnieniebufora (buffer overflow lub buffer overrun).

Tablice wielowymiarowe są w algorytmach numerycznych (głównie z linear algebra) dla przechowywania macierzy. Tablica C jest dobra dla tego zadania. Jednak skoro tablice są przekazywane tylko jako wskaźniki, granice tablicy muszą być znanymi ustalonymi wartościami lub jasno i jednoznacznie przekazane do jakiejkolwiek funkcji, podprocedury (subroutine), która ich wymaga i dynamicznie rozmiarowane tablice nie mogą być dostępne przy użyciu podwójnego indeksowana. Aby to obejść trzeba alokować tablicę z dodatkowym rzędowym wektorem (row vector) dla wskaźników do kolumn.

C99 ma “variable-length arrays”, które adresują pewne zagadnienia z zwykłych tablic C.

Wymienialność tablica-wskaźnik

Charakterystyczne jest traktowanie tablic i wskaźników. Notacja tablica-indeks x[i] może tylko być użyta, kiedy x jest wskaźnikiem. Interpretacja (używająca wskaźnika arytmetycznego) ma dostęp do obiektu (i + 1)th kilku sąsiadujących obiektów danych wskazanych przez x, wliczających obiekt, którego wskazuje X (który jest x[0]) jako pierwszy element tablicy.

x[i]

x – nazwa tablicy i adres jej pierwszego elementu o indeksie 0

i – numer indeksu, czyli rozmiar elementu tablicy

*(x + i) – adres miejsca x[0] + rozmiar elementu tablicy

x[3] – dostęp do czwartego elementu tablicy x[0] + x[1] + x[2] + x[3] = x[4]

*(x + 3) – dostęp do adresu czwartego elementu tablicy *(0 + 3) = 4

Formalnie x[i] jest ekwiwalentem dla *(x + i). Skoro typ tego wskaźnika jest znany dla kompilatora w czasie kompilacji, adres, który wskazuje x + i nie jest adresem wskazywanym przez x zwiększone przez i bajtów, ale raczej zwiększone przez i pomnożone przez rozmiar elementu, na który wskazuje x. Rozmiar tych elementów może być określony przez operator sizeof przez zastosowanie tego do jakiegokolwiek elementu x z usuniętą pośredniością, jak w

n = sizeof *x

lub

n = sizeof x[0]

sizeof *x lub sizeof x[0] – wielkość elementu o indeksie 0, bo *x = x[0]

Co więcej w większości kontekstów wyrażenia (wyjątkiem jest właśnie operand dla sizeof), nazwa tablicy jest automatycznie konwertowana na wskaźnik do pierwszego elementu tablicy. To sprawia, że tablica nie jest nigdy kopiowana jako całość i pracuje się na jej oryginale, kiedy jej nazwa występuje jako argument dla funkcji. Tutaj raczej jest to adres jej pierwszego elementu, który jest tylko przekazywany do funkcji. Dlatego chociaż wywołania funkcji używają semantyki przekazywania przez wartość (pass-by-value), tablice są w rzeczywistości przekazywane przez odniesienie. Bo mamy tutaj adres jej pierwszego elementu. Dzięki temu używając wskaźnika arytmetycznego możemy mieć dostęp do pozostałych jej elementów dodając lub odejmując jednostkę (wielkość pojedynczego elementu tablicy w bajtach).

Liczba elementów w zadeklarowanej tablicy x może być ustalona jako

sizeof x / sizeof x[0] lub sizeof x / sizeof *x

rozmiar tablicy / rozmiar jej pierwszego elementu

Wymienialność wskaźników i tablic. Te cztery wyznaczenia są równe i każdy jest ważnym kodem C:

/* x jest tablicą lub wskaźnikiem. i jest liczbą całkowitą.*/

x[i] = 1; /* equivalent to *(x + i) */

*(x + i) = 1;

*(i + x) = 1;

i[x] = 1; /* equivalent to *(i + x) */

Chociaż wszystkie wyznaczenia są równe, tylko pierwsze reprezentuje dobry styl kodu.

i[x] = i indeks (i) + 0 indeks (x)

5[x] = 5 + 0 = 6 indeks, czyli odwołanie do 6 elementu tablicy

x[i] = 0 indeks (x) + i indeks (i)

x[5] = 0 + 5 = 6 indeks, czyli odwołanie do 6 elementu tablicy

0 + 1 2 3 4 5 indeks

1 2 3 4 5 6 element

Mimo równości między zmiennymi tablicy i wskaźnika, jest różnica między nimi. Chociaż w większości kontekstach wyrażenia nazwa tablicy jest konwertowana na wskaźnik (do jego pierwszego elementu), ten wskaźnik sam nie zajmuje pamięci, w przeciwieństwie do wskaźnika zmiennej. Dlatego to, na co tablica wskazuje nie może być zmienione (zawsze jest to pierwszy element tablicy o indeksie 0) i jest niemożliwe wyznaczyć wartość do zmiennej tablicy przez ten swoisty wskaźnik. Wartości tablicy mogą być kopiowane np. przez funkcję memcpy.

9/ Biblioteki

Są to podstawowe rozszerzenia dla języka. Biblioteka to zestaw funkcji zawartych w pojedynczym pliku archiwalnym. Każda biblioteka ma plik header (header file), który zawiera wzorce funkcji zawartych w danej bibliotece, które mogą być użyte przez program i ma też deklaracje typowo specjalnych danych i symbole makro używane z tymi funkcjami. Aby program użył biblioteki, musimy zawrzeć w niej plik header i ta biblioteka musi być linkowana z tym programem, który w wielu wypadkach wymaga flag kompilatora (np. –lm skrót dla “math library”).

Biblioteka

————————————————————————————————–

funkcja funkcja funkcja header symbole makro

Najpowszechniejszą biblioteką C jest standardowa biblioteka C, która jest określona przez standardy ISO i ANSI C i jest ona z każdą implementacją C. Ta biblioteka wspiera strumień danych wejściowych i wyjściowych, alokację pamięci, matematykę, ciągi znaków i wartości czasu.

kod maszynowy- strumień liczb (o charakterze binarnym) przekazywany do komputera

Programy wysokiego poziomu nie tylko wyrażają instrukcje dla komputera, też służą do komunikacji między ludźmi. C pozwala na programy zorganizowane w łatwy i logiczny sposób. To jest ważne dla pisania długich programów, bo skomplikowane problemy są obsługiwane przez jasną organizację i strukturę programu.

C to wiele plików tekstowych używających ekranowego edytora. Ta forma programu jest zw. program źródłowy (source program). Nie można wykonać tego pliku bezpośrednio, bo maszyna go nie zrozumie.

Kompletny plik źródłowy jest przekazywany do kompilatora, który generuje nowy plik, zawierający kod maszynowy tłumaczony z tekstu źródłowego. Ten plik to plik obiektu lub plik wykonywalny

10/ System I/O i funkcje

SOs i środowiska

SO ma dwie warstwy: interfejs użytkownika, czyli user interface (shell) i system plików, czyli filing system. SO jest trasą dla wszystkich danych wejściowych i wyjściowych. Język programowania musi dotrzeć do danych wejściowych i wyjściowych łatwo, aby programy mogły otrzymywać i wysyłać wiadomości od użytkownika i musi mieć kontakt z SO, aby tego dokonać. Ma zestaw funkcji, które każdy SO musi wdrożyć, ale są specjalnie adaptowane do naszego SO.

Pliki i urządzenia

Filing system jest też częścią input/output. W wielu SOs wszystkie trasy do i z komputera są trakotwane przez SO, jak gdyby były one plikami lub strumieniami danych. Zarówno stdin i stdout są częścią stdio lub standard input output. Klawiatura i monitor nie są prawdziwymi plikami, a urządzeniami (nie jest możliwe ponownie odczytać, co wysłano do monitora lub zapisać do klawiatury), ale urządzenia są reprezentowane przez pliki, aby klawiaturę traktować jako plik tylko do odczytu i monitor jako plik tylko do zapisu.

Podstawowymi funkcjami umożliwiającymi komunikację programu z otoczeniem są funkcje Printf oraz Scanf. Ich ogólna struktura wygląda następująco:

Printf(control,arg1,arg2,…)
Scanf(control,arg1,arg2,…)

Funkcja printf pod nadzorem argumentu tekstowego control przekształca, formatuje i wypisuje swoje argumenty do standardowego wyjścia.
Tekst sterujący ograniczony jest znakami cudzysłowia i może zawierać zwykłe znaki, które są kopiowane do strumienia wyjściowego oraz specyfikację przekształceń, z których każda wskazuje sposób przekształcenia i wypisania kolejnego argumentu funkcji printf.
Wszystkie specyfikacje rozpoczynają, się znakiem
% i kończą znakiem przekształcenia. Miedzy nimi mogą się znajdować:

Funkcja printf()

Funkcja printf lub `print-formatted’ daje drukowanie tekstu np. ciągu:

printf (“..some string…”);

Może drukować też zawartość zmiennych. One mogą być wstawianie do ciągu tekstu przez użycie `control sequence’ w cudzysłów i listowanie zmiennych po tym ciągu, które zostały wstawione do ciągu w miejsce control sequence. Aby wydrukować liczbę całkowitą (integer) jest używana control sequence w formie %d:

printf (“Integer = %d”,jakaś_liczba_całkowita np. 5);

Zmienna jakaś_liczba_całkowita, czyli 5 jest drukowana zamiast %d. Funkcja printf:

– (minus) – zlecenie dosunięcia przekształconego argumentu do lewego krańca jego pola

ciąg cyfr – określa minimalny rozmiar pola

. (kropka) – oddziela specyfikację rozmiaru pola od następnego ciągu cyfr.

Lista znakow przekształceń:

d – argument będzie przekształcony do postaci dziesiętnej

o – argument będzie przekształcony do postaci ósemkowej

x – argument będzie przekształcony do postaci szesnastkowej

c – argument będzie traktowany jako jeden znak

s – argument jest tekstem znakowym

e – argument będzie traktowany jako liczba typu float lub double ([-]m.nnnnnE[+-]xx)

f – argument będzie traktowany jako liczba typu float lub double ([-]mmm.nnn)

Znaki niegraficzne:

\n – nowy wiersz

\t – tabulacja

\b – cofanie

\f – nowa strona

– znak NULL

Funkcja scanf wczytuje znaki ze standardowego wejścia, interpretuje je zgodnie z zadanym formatem i przypisuje kolejnym argumentom. W funkcji scanf wszystkie argumenty muszą być wskaźnikami! np. scanf(,,%d”,&x) gdzie x – zmienna całkowita dziesiętna, &x – wskaźnik (adres) do zmiennej x.

3 thoughts on “Wstęp do języka programowania C dla nie-informatyków

  1. Pilar

    A fascinating discussion is worth comment. There’s no doubt that that you ought to publish more about this issue, it may not be a taboo subject but typically folks don’t talk about these issues.

    To the next! Kind regards!!

    Reply
  2. Gilbert

    Hey I am so happy I found your blog page, I really found you by accident, while I was searching on Bing for something else,
    Regardless I am here now and would just like to
    say kudos for a marvelous post and a all round interesting
    blog (I also love the theme/design), I don’t have time to browse it all at the minute but I have book-marked it and also added in your RSS feeds, so when I have time I will be back to read much more, Please do keep up the awesome work.

    Reply

Leave a comment