Monika Dekster

Programowanie w języku C: Spis treści

  1. Historia C
  2. Cechy C
  3. Struktura Programu
  4. Typy Danych
  5. Słowa kluczowe
  6. Operatory
  7. Instrukcje Sterujące
  8. Funkcje
  9. Wskaźniki i tablice
  10. Wejście / wyjście

Historia języka C

Wersje:

Cechy C

  1. Prostota, szybki mały kompilator
  2. Operacje niskopoziomowe, dające programiście dużą kontrolę nad działaniem programu (dlatego możliwe jest np. napisanie jądra systemu operacyjnego)
  3. Ze względów optymalizacyjnych autorzy nie wbudowali wielu mechanizmów kontroli (np. zakresu tablic, legalności odwołań wskaźnikowych, inicjalizacji zmiennych, itd.)
  4. Brak automatycznego zwalniania pamięci (może prowadzić do "memory leaks")
  5. Nie wymaga ścisłej zgodności typów między parametrem i argumentem.
  6. Dopuszcza prawie każdy typ argumentu, o ile da się go rozsądnie przekształcić do typu parametru. Konwersja następuje automatycznie.
  7. C nie wykonuje prawie żadnego sprawdzania błędów wykonania. Za obsługę błędów odpowiada programista.
  8. Język C umożliwia odczyt i zapis poza zasięgiem tablicy!
  9. Język dla programistów, którzy są świadomi tego co robią.

Kompilator gcc

Program gcc jest tylko interfejsem, sterownikiem. GCC (jako pakiet) składa się z wielu programów. Każda faza budowania ma przydzieloną osobną aplikację.

  1. Preprocesor : cpp (C PreProcessor), tworzy plik gotowy do kompilacji (-E)
  2. Kompilator : cc (lub cc1, C Compiler), kompiluje do assemblera (-S)
  3. Assembler : as, assembluje kod do kodu maszynowego platformy docelowej (-c)
  4. Konsolidator : ld – tworzy końcowy program z utworzonych obiektów.

Każdemu etapowi tworzenia obrazu procesu w pamięci towarzyszy odpowiednie przekształcanie adresów, począwszy od etykiet i innych symboli, a skończywszy na fizycznych adresach komórek pamięci. Jeśli w programie źródłowym występują adresy, to mają one najczęściej postać symboliczną (etykiety w asemblerze) lub abstrakcyjną (wskaźniki w C). Adresy związane z lokalizacją pojawiają się na etapie translacji i są odpowiednio przekształcane aż do uzyskania adresów fizycznych.

Skompilowany program tworzy 4 osobne obszary pamięci:

  1. Obszar kodu programu (instrukcje, stałe, zmienne statyczne i zewnętrzne)
  2. Obszar zmiennych globalnych (zdefiniowane poza funkcjami, widoczne dla wszystkich funkcji zdefiniowanych "pod" nimi)
  3. Stos – przechowywanie zmiennych lokalnych, zachowywanie stanu rejestrów przy wywoływaniu podprogramów itd. Wielkość stosu można regulować w ustawieniach linkera
  4. Sterta – obszar wolnej pamięci, którą program może dynamicznie zaalokować

Styl programowania

Styl programowania to sposób pisania kodu, który będzie czytelny zarówno dla jego twórcy, jak i innych programistów. Dobry styl jest podstawą dobrego programowania. Dobrego stylu programowania można nauczyć się wyłącznie poprzez zdobycie praktyki. Nie ma uniwersalnej reguły gwarantującej pisanie dobrych programów. Istnieje kilka prostych i przydatnych zasad.

  1. Należy używać nazw opisowych dla definicji globalnych, krótkich zaś dla lokalnych.
  2. Należy programować w sposób jednolity.
  3. Używać nazw czynności dla funkcji.
  4. Dbać o precyzję.
  5. Stosować wcięcia, aby uwidocznić strukturę.
  6. Używać naturalnej postaci wyrażeń.
  7. Używać nawiasów, aby rozwiać niejasności.
  8. Dzielić wyrażenia złożone.
  9. Należy uważać na efekty uboczne.
  10. Stosować jednolity sposób rozmieszczania wcięć i nawiasów klamrowych.
  11. Programując konstrukcje wielokrotnego wyboru, używać instrukcji else - if.
  12. Treść i argumenty makroinstrukcji ujmować w nawiasy.
  13. Nadawać nazwy liczbom magicznym.
  14. Definiować liczby jako stałe.
  15. Używać stałych znakowych zamiast liczb całkowitych (kodów znaków).
  16. Do obliczania rozmiarów instancji (zmiennych, typów) używać operatorów języka.
  17. Komentować funkcje i dane globalne.
  18. Nie komentować złego kodu, lecz go poprawiać.
  19. Komentarz nie może być sprzeczny z kodem.
  20. To samo robić wszędzie tak samo.
  21. Zwalniać zasoby w tej samej warstwie, w której były przydzielone.
  22. Trzymać się standardu.

Struktura programu


#include <stdio.h>
/*
	to jest komentarz
	kilkuwierszowy
*/
int main(void) {
	printf("Hello, world\n");	//	komentarz jednowierszowy
	return 0;
}

Typy danych

  1. typ wartości logicznych: bool, tylko w wersji C99 (po włączeniu nagłówka <stdbool.h>),
  2. typy całkowitoliczbowe:
    • char / unsigned char / signed char,
    • int / unsigned int,
    • short / unsigned short,
    • long / unsigned long,
    • long long / unsigned long long (C99),
  3. typy zmiennopozycyjne:
    • float,
    • double,
    • long double,

Rozmiary poszczególnych typów są zależne od implementacji (operator sizeof()).

Specyfikacja języka C zawiera również typy dodatkowe (zdefiniowane w <stddef.h>) size_t oraz ptrdiff_t. Ich rozmiar jest zdefiniowany w zależnosci od architektury procesora.

size_t jest typem całkowitym bez znaku używanym do reprezentacji rozmiaru dowolnego obiektu (włączając tablice) w danej implementacji. size_t jest typem zwracanym przez operator sizeof.

ptrdiff_t jest typem całkowitym ze znakiem używanym do reprezentowania różnicy wskaźników (tego samego typu, różnica wskaźników różnych typów nie jest zdefiniowana).

Stałe

  1. stałe całkowite
    • int: 536230
    • unsigned int: 536230u
    • int: 012300 wartość oktalna
    • int: 0xff, 0xab12 wartość heksadecymalna
    • long: 536230l
    • unsigned long: 536230ul
    • long long: 536230ll
    • unsigned long long: 536230ull
  2. stałe zmiennoprzecinkowe
    • float: 1e1f, 2.f, .3f
    • double: 1e1, .3, 0.0, 2.d
    • long double: .3l, 0.0l, 2.e5l
  3. stałe logiczne: true, false
  4. stałe znakowe:
    • 'a'
    • '\t' tab
    • '\n' linefeed
    • '\f' form feed
    • '\r' carriage return
    • '\"' double quote
    • '\\' backslash
    • '\'' single quote
    • '\xff' kod heksadecymalny znaku
    • '\177' kod oktalny znaku
  5. "to jest tekst\n" literał tekstowy (tablica znakowa)

Zmienne

Zmienne są (zwykle) nazwanymi pojemnikami na pojedyncze wartości typu z jakim zostały zadeklarowane.

Wyróżniamy następujące rodzaje zmiennych:

  1. zmienne lokalne (local variables),
  2. zmienne globalne (global variables),
  3. zmienne statyczne lokalne (static local variables),
  4. zmienne statyczne globalne (static global variables),
  5. zmienne rejestrowe (register variables),

Przy deklaracji zmiennych można jawnie podawać ich wartości początkowe. To bardzo dobra praktyka. Jeśli w deklaracji zmiennych statycznych lub globalnych nie podano ich wartość początkowej, to taka zmienna zostanie automatycznie zainicjalizowana wartością 0 odpowiedniego typu. W przypadku zmiennych lokalnych programista musi sam zadbać o zainicjalizowanie zmiennej przed jej odczytaniem.

Słowa kluczowe

auto break case char
const continue default do
double else enum extern
float for goto if
int long register return
short signed sizeof static
struct switch typeset union
unsigned void volatile while

Operatory

  1. operatory numeryczne:
    • +, − unarny
    • *, ⁄, % dwuargumentowe
    • +, − dwuargumentowe
    • ++, −− inkrementacja i dekrementacja (pre i postfiksowa)
  2. operatory relacyjne: <, <=, >, >=, ==, !=
  3. logiczne operatory warunkowe: &&, ||
  4. operator zmiany typu (cast)
  5. <<, >> operatory przesunięcia (tylko dla typów całkowitych),
  6. logiczne operatory bitowe (tylko dla typów całkowitych)
    • ~ operator dopełnienia bitowego
    • &, |, ^ (and, or, xor)
  7. +=, −=, *=, ⁄=, &=, |=, ^=, %=, <<=, >>= operatory modyfikacji
  8. ?: operator warunkowy

Uwagi dotyczące operatorów

Operator dzielenia   ⁄

Dzielenie całkowite daje wynik całkowity, np. 5 ⁄ 2 daje w wyniku 2 (a nie 2.5)

Operator %

dla operandów całkowitych daje wynik taki, że (a ⁄ b) * b + (a % b) jest równe a

Operator warunkowy

E1 ? E2 : E3

wartością wyrażenia jest E2 jeżeli E1 jest true i E3 jeżeli E1 jest false

np. c = (a > b) ? a : b oblicza maximum liczb a, b

Operatory zwiększania i zmniejszania

n++, n−− postfixowy

++n, −−n prefixowy

n = 5; m = n++; daje m = 5

n = 5; m = ++n; daje m = 6

Złożone operatory przypisania

Postać: E1 op= E2 jest równoważne:

E1 = (T)((E1) op (E2))

T jest typem E1, op jest jednym z:

+, −, *, ⁄, %, &, |, ^, <<, >>

Priorytety i łączność operatorów

Precedence Operator Description Associativity
1 ++ -- Suffix/postfix increment and decrement Left-to-right
() Function call
[] Array subscripting
. Structure and union member access
-> Structure and union member access through pointer
(type){list} Compound literal(C99)
2 ++ -- Prefix increment and decrement Right-to-left
+ - Unary plus and minus
! ~ Logical NOT and bitwise NOT
(type) Type cast
* Indirection (dereference)
& Address-of
sizeof Size-of
3 * / % Multiplication, division, and remainder Left-to-right
4 + - Addition and subtraction
5 << >> Bitwise left shift and right shift
6 < <= For relational operators < and ≤ respectively
> >= For relational operators > and ≥ respectively
7 == != For relational = and ≠ respectively
8 & Bitwise AND
9 ^ Bitwise XOR (exclusive or)
10 | Bitwise OR (inclusive or)
11 && Logical AND
12 || Logical OR
13 ?: Ternary conditional Right-to-Left
14 = Simple assignment
+= -= Assignment by sum and difference
*= /= %= Assignment by product, quotient, and remainder
<<= >>= Assignment by bitwise left shift and right shift
&= ^= |= Assignment by bitwise AND, XOR, and OR
15 , Comma Left-to-right

Uwagi:

  1. Wyrażenie wewnątrz operatora warunkowego(między ? a :) jest traktowane jak ujęte w nawiasy: jego priorytet względem ?: jest ignorowany.
  2. Operatory znajdujące się w tej samej komórce (w jednej komórce może znajdować się kilka wierszy) są ewaluowane z tym samym priorytetem, w kolejności zależnej od łączności tej grupy operatorów. Na przykład wyrażenie a = b = c jest parsowane jak a = (b = c) a nie (a = b) = c ponieważ operator = jest prawostronnie łączny, ale a + b - c jest równoważne (a + b) - c a nie a + (b - c) z powodu lewostronnej łączności operatorów dodawania i odejmowania.
  3. Specyfikacja łączności dla operatorów unarnych (jednoargumentowych) jest redundantna: unarne operatory prefiksowe są zawsze prawostronnie łączne: ++*p oznacza (++(*p)), natomiast unarne operatory postfiksowe są zawsze lewostronnie łączne: a[1][2]++ oznacza ((a[1])[2])++. Łączność jest jednak istotna dla operatorów dostępu do składowych: a.b++ jest parsowane jako (a.b)++.

Punkty sekwencyjne

Łączność operatorów jest wykorzystywana kiedy w wyrażeniu występują co najmniej dwa operatory o tym samym priorytecie. Istotne jest aby zauważyć, że łączność nie definiuje kolejności, w jakiej wyliczane są operandy pojedynczego operatora. Na przykład w następującym fragmencie programu


int x = 0; 
int f1() {
	x = 5;
	return x;
} 
int f2() {
	x = 10;
	return x;
}
int main(void) {
	int p = f1() + f2();
	printf("%d ", x);
	return 0;
}

nie ma gwarancji, że f1() zostanie obliczone przed f2() (mimo, że operator + jest lewostronnie łączny). Wynik wykonania tego programu jest zależny od kompilatora.


x = 2*6 + 6*9;

Mnożenie wykonuje się przed dodawaniem, ale które jako pierwsze zależy od kompilatora


a[i] = i++; // Nie wiemy czy zostanie zmodyfikowana komórka a[i] czy a[i+1].
...
int i = 7;
printf("%d\n", i * i++); // Wypisuje 56, a wydaje się, że powinno być 49

Pojedyncze wyrażenie nie powinno modyfikować dwa razy tego samego obiektu, ani jednocześnie modyfikować i pobierać wartości.

Punkt sekwencyjny to miejsce, w którym wszystkie efekty uboczne danego wyrażenia zostały zrealizowane. Punkty sekwencyjne są ustawiane:

  1. Na końcu wyrażenia będącego samodzielną instrukcją, czyli:
    • dowolne pełne wyrażenie zakończone średnikiem
    • instrukcja return
    • wyrażenia sterujące instrukcji if, switch, while, do-while
    • wszystkie trzy wyrażenia instrukcji for
  2. "Wewnątrz" operatorów: || , && , ? :, przecinek
  3. Przy wywołaniu funkcji (po obliczeniu argumentów , tuż przed skokiem do funkcji).

Zasady:

  1. Pomiędzy poprzednim a następnym punktem sekwencyjnym wartość, przechowywana przez dany obiekt może zostać zmodyfikowana najwyżej raz
  2. Poprzednia wartość obiektu może zostać użyta jedynie do określenia nowej wartości.

Antyprzykłady, czyli jak nie postępować


z = i++ * i++; // 1
i = i++;       // 1
a[i] = i++;    // 2

Ale


z = i++ && i++;

Jest jednoznaczne, ponieważ operator && jest punktem sekwencyjnym.

Konwersja typów

Binarne promocje numeryczne

Dokonywane dla operandów następujących operatorów dwuargumentowych:

Zasady konwersji

Konwersja typu jest zawsze możliwa dla dowolnych dwóch typów numerycznych. Kompilator automatycznie dokonuje takiej konwersji w razie potrzeby. Czasami taka konwersja może doprowadzić do zmiany wartości zmiennej.

Hierarchia typów

Gdy operandy mają różne typy, niejawna konwersja zależy od ich hierarchii.

Zasady hierarchii:

  1. Dwa typy całkowite bez znaku mają różną pozycję. Wyższą pozycję ma zawsze typ o większym rozmiarze
  2. Każdy typ całkowity ze znakiem ma tę samą pozycję co odpowiadający mu typ bez znaku. Typ char na tę samą pozycję co signed char i unsigned char.
  3. Hierarchia typów całkowitych jest następująca:

    char < short < int < long < long long

  4. Każdy typ wyliczeniowy ma tę samą pozycję jak odpowiadający mu typ całkowity.
  5. Typy zmiennoprzecinkowe są uszeregowane następująco:

    float < double < long double

  6. Każdy typ rzeczywisty ma wyższą pozycję niż dowolny typ całkowity.

Promocje całkowite

W każdym wyrażeniu można użyć wartości niżej w hierarchii niż int w miejsce wartości int lub unsigned int. W takich przypadkach kompilator stosuje promocje całkowite: każdy operand o pozycji niższej niż int jest automatycznie konwertowany do typu int (gdy int może pomieścić wszystkie wartości oryginalnego typu). Gdy typ int jest zbyt mały to wartość jest konwertowana do unsigned int.

Promocje całkowite zawsze zachowują wartość operandu.

Standardowe konwersje arytmetyczne

Zwykłe konwersje arytmetyczne to konwersje niejawne, które są automatycznie stosowane do operandów o różnych typach. Celem tych konwersji jest:

  1. doprowadzenie do tego, aby typy argumentów były takie same (funkcje realizujące działanie operatorów są zdefiniowane tylko dla argumentów takiego samego typu, przy czym nie są zdefiniowane dla wszystkich możliwych typów);
  2. zapewnienie, aby ten wspólny typ był "obsługiwany" przez istniejącą funkcję realizującą działanie danego operatora;
  3. zachowanie, w miarę możności, precyzji wyniku (typem wyniku arytmetycznej operacji dwuargumentowej jest wspólny typ argumentów po dokonaniu konwersji).

Zasady konwersji

  1. Jeżeli co najmniej jeden z operandów jest typu rzeczywistego, to operand o niższej pozycji jest konwertowany do typu o tej samej pozycji jak drugi operand .
  2. Jeżeli oba operandy są całkowite, najpierw wykonywana jest promocja całkowita. Jeżeli po promocji operandy w dalszym ciągu są różnych typów, dalsza konwersja wygląda następująco:
    1. Jeżeli jeden z operandów ma typ bez znaku, T, którego pozycja jest nie mniejsza niż pozycja typu drugiego operandu, to drugi operand jest konwertowany do typu T.
    2. W przeciwnym przypadku jeden z operandów ma typ ze znakiem, T, którego pozycja jest wyższa niż pozycja drugiego operandu. Drugi operand jest konwertowany do typu T tylko wtedy gdy typ T może reprezentować wszystkie wartości swojego poprzedniego typu. Jeżeli nie, oba operandy są konwertowane do typu bez znaku, który odpowiada typowi T

Przykładowo:


int si = -20;
long sl = -20;
unsigned int ui = 10;
printf("si < ui = %d\n", si < ui);	//	(1)
printf("si + ui = %u\n", si + ui);
printf("sl < ui = %d\n", sl < ui);	//	(2)
printf("sl + ui = %ld\n", sl + ui);

drukuje (przyjmując, że sizeof(int) == 4 i sizeof(long) == 8):

si < ui = 0
si + ui = 4294967286
sl < ui = 1
sl + ui = -10

Aby wyznaczyć wartość wyrażenia warunkowego (1) si < ui, wartość si (-20) jest konwertowana na typ unsigned int (reguła 2a). Wynikiem jest duża liczba dodatnia (większa niż wartość zmiennej ui). W zwiazku z tym warunek będzie fałszywy i program konsekwentnie wyprowadzi wartość 0. Podobna konwersja ma miejsce w przypadku sumy si + ui.

W przypadku wyrażeń (2), stosujemy regułę 2b. Wartość zmiennej ui jest konwertowana do typu zmiennej sl (czyli long) jeżeli zakres wartości typu long zawiera cały zakres wartości typu unsigned int (jak w prezentowanym powyżej przykładzie). Jeżeli nie (np. oba typy są tej samej długości), to oba czynniki są konwertowane do typu unsigned long i program wyprowadzi wartości podobne do przypadku (1).

Sterowanie przebiegiem programu

instrukcja - wyrażenie zakończone średnikiem;

blok - grupa instrukcji ujęta w nawiasy klamrowe {} (składniowo równoważna jednej instrukcji)

Instrukcja warunkowa (if)


if (wyrażenie)
	instrukcja-1
else
	instrukcja-2

Instrukcja rozgałęzienia (switch)


switch (wyrażenie) {
	case stała-1: instrukcja-1
	case stała-2: instrukcja-2
	... 
	case stała-n: instrukcja-n
	default: instrukcja
}

Pętle


while (wyrażenie) instrukcja;

do {
	instrukcja;
} while (wyrażenie);

for (wyrażenie1; wyrażenie2; wyrażenie3) 
	instrukcja;

Równoważne:


wyrażenie1;
while (wyrażenie2) { 
	instrukcja; 
	wyrażenie3;
}

Funkcje

Definicja funkcji


<type> name(<type> arg1, <type> arg2, ...) {
	definitions and instructions;
	...
}

Deklaracja funkcji


<type> name(<type> arg1, <type> arg2, ...);

Przykłady:


double sqr(double x);

double sqr(double x) {
	return x*x;
}

int add(int a, int b);

int add(int a, int b) {
	return a + b;
}

void print (char* s, double x);

void print (char* s, double x) {
	printf("%s %f\n", s, x);
}

void message(void);

void message(void) {
	printf("Function with empty parameter list\n");
}

Funkcja o typie różnym od void musi mieć przynajmniej jedną instrukcję return wyr;, gdzie wyr jest wyrażeniem zgodnym z typem funkcji. Funkcja typu void może zawierać instrukcję return; (bez wyrażenia). Jeżeli tej instrukcji nie ma to funkcja kończy się po wykonaniu wszystkich jej instrukcji.

Parametry do funkcji są przekazywane przez wartość. Oznacza to, że aktualne wartości parametrów są kopiowane do obszaru roboczego funkcji, a sama funkcja nie ma dostępu do oryginału. Wniosek: Funkcja nie może zmienić oryginalnych wartości parametrów. Inne sposoby przekazywania parametrów są opisane w rozdziale o wskaźnikach.

According to the C Standard, subclause 6.7.6.3, paragraph 14 [ISO/IEC 9899:2011]

An identifier list declares only the identifiers of the parameters of the function. An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. The empty list in a function declarator that is not part of a definition of that function specifies that no information about the number or types of the parameters is supplied.

Subclause 6.11.6 states that

The use of function declarators with empty parentheses (not prototype-format parameter type declarators) is an obsolescent feature.

Consequently, functions that accept no arguments should explicitly declare a void parameter in their parameter list. This holds true in both the declaration and definition sections (which should match). Defining a function with a void argument list differs from declaring it with no arguments because, in the latter case, the compiler will not check whether the function is called with parameters at all [TIGCC, void usage]. Consequently, function calling with arbitrary parameters will be accepted without a warning at compile time.

Wskaźniki i tablice

Wskaźniki

Pointers



Pointers



Pointers


Tablice

Deklaracja tablicy


int ia[10];

#define DIM 10
char ca[DIM];

Tworzenie tablicy:

Inicjalizacja


int silnia[] = { 1, 1, 2, 6, 24};
char as[] = "Tablica";

Tablice muszą być indeksowane wyrażeniami całkowitymi. Tablica o długości n może być indeksowana od 0 do n-1.

Pointers

Przekazywanie tablicy jednowymiarowej do funkcji

W C mamy do dyspozycji kilka form zapisu przekazywania tablic do funkcji. Zapisy te są względem siebie równoważne, a więc można ich używać zamiennie. Zapisy te wyglądają następująco:


void sposob_1(int tablica[123]);
void sposob_2(int tablica[]);
void sposob_3(int* tablica);

Z punktu widzenia funkcji znajomość liczby elementów dla tablic jednowymiarowych jest zbędna. Wspomniany zapis zadziała więc tak samo jak zapis int tablica[], który informuje kompilator o przekazywaniu tablicy do funkcji. Ostatni zapis natomiast mówi, że jest to wskaźnik. Kompilator wszystkie omówione zapisy zinterpretuje jako wskaźnik.

Zwracanie tablicy jednowymiarowej z funkcji

Pierwsza możliwość


char *itoa(int n) {
	static char retbuf[25];	//	użycie 'static' jest ważne!!!
	sprintf(retbuf, "%d", n);
	return retbuf;
}
...............
printf("i = %s, j = %s\n", itoa(i), itoa(j));

Niezależnie od tego, które z wywołań funkcji itoa() zostanie wykonane wcześniej (standard tego nie specyfikuje), printf wydrukuje dwie takie same wartości (albo i albo j). W tablicy retbuf znajdzie sie wartość z drugiego wywołania.

Druga możliwość

Jeżeli funkcja nie może użyć lokalnej tablicy statycznej (bo np. przewidujemy możliwość wielokrotnych wywołań, j.w.), kolejną możliwością jest użycie tablicy uprzednio zaalokowanej przez funkcję wołającą.Wtedy nasza funkcja przyjmuje kolejny argument: wskaźnik do obszaru, gdzie należy umieścić wartości zwracanej tablicy.


char *itoa(int n, char buf[]) {
        sprintf(buf, "%d", n);
        return buf;
}
//	wywołanie ...
int i = 23;
char buf[25];
char *str = itoa(i, buf);

Wadą powyższego rozwiązania jest fakt, że funkcja atoi() nie posiada informacji o aktualnym rozmiarze przekazanej jej tablicy. W związku z tym nie może ona sprawdzić, czy zakres tej tablicy nie zostanie przekroczony.

Trzecia możliwość

Trzecia możliwością jest dynamiczna alokacja pamięci na zwracaną tablicę przez nasza funkcję.


char *itoa(int n) {
	char *retbuf = malloc(25);
	if(retbuf == NULL) return NULL;
	sprintf(retbuf, "%d", n);
	return retbuf;
}

Metoda ta również posiada wady:

  1. Funkcja zwraca wskaźnik zerowy (NULL) w przypadku gdy malloc() zawiedzie. Funkcja wołająca powinna zatem sprawdzać (zawsze przed użyciem zwróconego wskaźnika), czy nie jest on zerowy.
  2. Funkcja przy każdym wywołaniu alokuje nową pamięć. Nie może natomiast jej zwolnić, ponieważ nie ma informacji jak długo będzie potrzebna. W związku z tym jedynym podmiotem odpowiedzialnym za zwolnienie pamięci po każdym wywołaniu jest funkcja wołająca. Jeżeli tego nie zrobi, doprowadzi do wycieku pamięci (memory leaks).

Tablice wielowymiarowe statyczne

Tablica 2D w języku C jest tablicą 1D, której elementami są tablice 1D (wiersze). Na przyklad tablica T a[4][3] (T jest jakimś typem) może być przedstawiona następująco:

Pointers

a[0] ---> a[0][0] a[0][1] a[0][2]
a[1]---> a[1][0] a[1][1] a[1][2]
a[2]---> a[2][0] a[2][1] a[2][2]
a[3]---> a[3][0] a[3][1] a[3][2]

Elementy tablicy są przechowywane w pamięci wierszami, tak więc adres elementu T a[i][j] tablicy T a[m][n] typu T jest wyznaczany następująco:


address(a[i][j]) = address(a[0][0]) + (i * n + j) * size(T)
  1. Powyższe równanie jest ważne. Stanowi ono połączenie między abstrakcyjnym typem danych a jego implementacją. Z punktu widzenia programisty jest ono niewidoczne; kompilator automatycznie generuje odpowiedni kod gdy w programie pojawi się odniesienie do tablicy.
  2. Dla tablic 3 i więcej wymiarowych równanie to staje się coraz bardziej skomplikowane.
  3. Liczba wierszy (ogólnie pierwszy wymiar tablicy) nie pojawia się w równaniu - nie jest potrzebna by obliczyć adres danego elementu. Dlatego przy przekazywaniu tablic do funkcji nie jest konieczne podawanie pierwszego wymiaru.

Metoda K&R redukcji tablic do wskaźników

K&R stworzyli zunifikowane pojęcie tablicy i wskaźnika. Ich rozwiązanie można przedstawić w postaci pięciu reguł:

  1. Tablica N-wymiarowa jest tablicą 1D elementów, które są tablicami N-1-wymiarowymi.
  2. Dodawanie "wskaźnikowe" jest zdefiniowane następująco:
    
    ptr # n = ptr + n * size(type-pointed-into)
    
    "#" oznacza dodawanie "wskaźnikowe" (wskaźnik + int), by odróżnić go od zwykłego dodawania.
  3. Tablica jest traktowana jako wskaźnik do swojego pierwszego elementu (decay convention). Decay convention nie powinna być używana więcej niż raz do tego samego obiektu.
  4. Pobranie elementu o indeksie i jest równoważne operacji "dodaj wskaźnikowo indeks do adresu początku tablicy i pobierz element wskazany przez tak otrzymaną sumę"
    
    tab[i] == *(tab # i)
    
  5. Dla tablic wielowymiarowych reguły 3 i 4 są stosowane rekursyjnie, ale wyłuskiwany jest tylko typ danych, a nie wskaźnik (z wyjątkiem ostatniego kroku).

Powyższe reguły prowadzą do równania tablic. Dla tablicy 2D elementów typu T mamy:


T a[m][n];
a[i] = a # i = a + i * sizeof(row) // no '*' since it is an address (rule 5.)
a[i][j] = *(a[i] # j)
a[i][j] = *((a + i * sizeof(row)) # j)
a[i][j] = *(a + i * sizeof(row) + j * sizeof(T))
a[i][j] = *(a + (i * n + j) * size(T)) // since sizeof(row) = n * sizeof(T)
address(a[i][j]) = a + (i * n + j) * size(T)

Uzyskaliśmy więc równanie jak wyżej.

"Podwójny" wskaźnik NIE powinien być używany jako tablica 2D.

"Wskaźnik do wskaźnika typu T" nie jest "tablicą 2D typu T". Tablica 2D jest równoważna "wskaźnikowi do wiersza T", a to jest zupełnie różne od "wskaźnika do wskaźnika typu T". Gdy podwójny wskaźnik do pierwszego elementu tablicy jest używany jako ptr[0][0]", jest on dwukrotnie wyłuskiwany. Po tej operacji element wynikowy będzie miał adres równy zawartości pierwszego elementu tablicy. Element ten zawiera jednak dane a nie adres!!!

Sposoby dostępu do tablicy 2D.

  1. Tablica 2D (może być bez pierwszego wymiaru)
  2. Wskaźnik do tablicy; drugi wymiar podany jawnie
  3. Pojedynczy wskaźnik; "spłaszczenie" tablicy do 1D. W ten sposób można tworzyć funkcje ogólnego zastosowania; rzeczywiste wymiary nie pojawiają się nigdzie i mogą być przekazane przez listę parametrów.
  4. Podwójny wskaźnik; użycie pomocniczej tablicy wskaźników.
  5. Pojedynczy wskaźnik; użycie pomocniczej tablicy wskaźników.

Podwójne (potrójne ...) wskaźniki

Deklaracja "wskaźnika-do-wskaźnika" wygląda następująco:


int **ipp;

gdzie '**' oznacza dwa "poziomy" wskaźników.

Proste przykłady:


int i = 5, j = 6; k = 7;
int *ip1 = &i, *ip2 = &j;

Przypisanie


ipp = &ip1;

powoduje, że ipp wskazuje na ip1, który wskazuje na i. (*ipp jest równy ip1 a **ipp jest równy i, czyli 5).

Pointers

Jeżeli napiszemy:


*ipp = ip2;

to zmienimy wskaźnik wskazywany przez ipp (ip1) na ip2, tak, że ip1 wskazuje teraz na j.

Pointers

Przypisanie:


*ipp = &k;

zmieni wskaźnik wskazywany przez ipp (ip1) ponownie, tym razem na k.

Pointers

Zastosowanie: Przekazywanie wskaźników do funkcji "przez wskaźnik", gdy np. chcemy, aby funkcja mogła zmienić wartość wskaźnika

Zarządzanie pamięcią

Stos (stack)

Stos to obszar pamięci w przestrzeni adresowej programu wykorzystywany do specyficznych celów. Stos obsługiwany jest w sposób automatyczny, obsługa ta nie wymaga ingerencji programisty. Na stosie dostępny jest wyłącznie element położony na jego wierzchołku, a elementy zdejmowane są ze stosu w odwrotnej kolejności niż były na nim umieszczane (rejestr LIFO ).

Na stosie przechowywane są:

Sterta (heap)

Sterta to obszar pamięci udostępniany przez system operacyjny wszystkim działającym programom (procesom). Na stercie przechowywane są dynamicznie przydzielane obszary pamięci

Pamięć statyczna

Wszystkie zmienne globalne oraz zmienne statyczne. Dodatkowo, pamięć w której przechowywany jest binarny kod wykonywalny programu również jest statycznie zaalokowana. Pamięć statyczna jest alokowana w momencie uruchomienia procesu (zawsze w tym samym miejscu).

Wskaźniki do funkcji

Składnia:


int (*foo)(int);

foo jest wskaźnikiem do funkcji posiadającej jeden argument typu int i zwracającej wartość typu int.

Deklarując wskaźniki funkcyjne postepujemy jak przy deklaracji funkcji, zamieniając nazwę funkcji (np. foo) na (*foo) (pamiętając o nawiasach).

Inicjalizacja wskaźników funkcyjnych:

Aby zainicjalizować wskaźnik funkcyjny musimy podać adres funkcji o parametrach zgodnych z deklaracją


#include <stdio.h>
void my_int_func(int x) {
	printf( "%d\n", x );
}

int main(void) {
	void (*foo)(int);
	foo = &my_int_func;	// the ampersand is optional
	foo(2);	// call my_int_func; you do not need to write (*foo)(2)
	(*foo)(2);	// but if you want to, you may
	return 0;
}

powyższego przykładu wynika, że składnia wskaźników funkcyjnych jest elastyczna: można stosować konwencję 'wskaźnikową', z '*' i '&', albo ominąć tę część.

The "right-left" rule

Reguła "right-left" służu do analizy deklaracji C; może także być pomocna w ich tworzeniu.

Symbole tłumacz następująco:

Algorytm postępowania:

  1. Znajdź identyfikator. Od tego punktu zaczynamy. Zapisz "identyfikator jest".
  2. Popatrz na symbole na prawo od identyfikatora. Jeżeli jest to np. "()" to wiemy, że jest to deklaracja funkcji. Wtedy zapisz "identyfikator jest funkcją zwracającą ...". Jeżeli jest to "[]", zapisujemy "identyfikator jest tablicą ...". Następnie idziemy dalej na prawo do momentu, gdy skończą się symbole LUB natrafimy na prawy nawias ")".
  3. Popatrz na symbole na lewo od identyfikatora. Jeżeli nie jest to żaden z symboli wymienionych powyżej (np. int), zapisz ten symbol. W przeciwnym przypadku zapisz "tłumaczenie" symbolu jak w tabeli. Następnie idziemy dalej na lewo do momentu, gdy skończą się symbole LUB natrafimy na lewy nawias "(".

Powtarzaj kroki 2 i 3 do wyczerpania symboli.

Przykład 1:

int *p[];

  1. Znajdź identyfikator: int *p[];
    Zapisz: "p jest"
  2. Idziemy na prawo do momentu, gdy skończą się symbole LUB natrafimy na prawy nawias int *p[];
    Zapisz: "p jest tablicą"
  3. Nie możemy iść dalej na prawo (nie ma symboli), więc idziemy na lewo: int *p[];
    Zapisz: "p jest tablicą wskaźników do "
  4. Idziemy dalej na lewo: int*p[];
    Zapisz: "p jest tablicą wskaźników do int"

Przykład 2:

int *(*f())();

  1. Znajdź identyfikator: int *(*f())();
    Zapisz: "f jest"
  2. Idziemy na prawo: int *(*f())();
    Zapisz: "f jest funkcją zwracającą"
  3. Nie możemy iść dalej na prawo (prawy nawias), więc idziemy na lewo: int *(*f())();
    Zapisz: "f jest funkcją zwracającą wskaźnik do"
  4. Nie możemy iść dalej na lewo (lewy nawias), więc idziemy znów na prawo: int *(*f())();
    Zapisz: "f jest funkcją zwracającą wskaźnik do funkcji zwracającej"
  5. Nie możemy iść dalej na prawo (koniec symboli), więc idziemy znów na lewo: int *(*f())();
    Zapisz: "f jest funkcją zwracającą wskaźnik do funkcji zwracającej wskaźnik do"
  6. Idziemy dalej na lewo (bo nie ma nic na prawo): int *(*f())();
    Zapisz: "f jest funkcją zwracającą wskaźnik do funkcji zwracającej wskaźnik do int"

Niektóre deklaracje wyglądają na bardziej skomplikowane, ponieważ zawierają rozmiary tablic i listy argumentów funkcji. W tym przypadku "[3]" należy zapisać jako: "tablica (o rozmiarze 3) elementów typu". Natomiast "(char*, int)" oznacza "funkcja o parametrach (char*, int) zwracająca...".

Przykład 3:

void *(*f)(int *);

  1. Znajdź identyfikator: void *(*f)(int *);
    Zapisz: "f jest"
  2. Nie możemy iść na prawo (prawy nawias), więc idziemy na lewo: void *(*f)(int *);
    Zapisz: "f jest wskaźnikiem do"
  3. Idziemy na prawo (lewy nawias): void *(*f)(int *);
    Zapisz: "f jest wskaźnikiem do funkcji przyjmującej parametr (int*) i zwracającej "
  4. Idziemy na lewo (koniec symboli na prawo): void *(*f)(int *);
    Zapisz: "f jest wskaźnikiem do funkcji przyjmującej parametr (int*) i zwracającej wskaźnik do"
  5. Idziemy na lewo: void *(*f)(int *);
    Zapisz: "f jest wskaźnikiem do funkcji przyjmującej parametr (int*) i zwracającej wskaźnik do void" (void*)

Przykład 4:

int (*(*f)(char*,double))[9][20];

f jest wskaźnikiem do funkcji o parametrach (char*, double) i zwracającej wskaźnik do tablicy (rozmiar 9) tablic (rozmiar 20) wartości typu int

Wejście / wyjście

printf


int printf (const char* format, ...);

Funkcja formatuje tekst zgodnie z podanym formatem opisanym poniżej i wypisuje tekst na standardowe wyjście (tj. do stdout). Jeżeli format zawiera specyfikacje formatu, dodatkowe argumenty są formatowane i wstawiane do stringu, zastępując odpowiadające im specyfikacje.

Specyfikator formatu ma następującą postać:


%[flags][width][.precision][length]specifier

specifier jest obowiązkowy i definiuje typ i interpretację odpowiadającego mu argumentu.

specifierOutputExample
d or iSigned decimal integer392
uUnsigned decimal integer7235
oUnsigned octal610
xUnsigned hexadecimal integer7fa
XUnsigned hexadecimal integer (uppercase)7FA
fDecimal floating point, lowercase392.65
FDecimal floating point, uppercase392.65
eScientific notation (mantissa/exponent), lowercase3.9265e+2
EScientific notation (mantissa/exponent), uppercase3.9265E+2
gUse the shortest representation: %e or %f392.65
GUse the shortest representation: %E or %F392.65
aHexadecimal floating point, lowercase-0xc.90fep-2
AHexadecimal floating point, uppercase-0XC.90FEP-2
cCharactera
sString of characterssample
pPointer addressb8000000
n Nothing printed.
The corresponding argument must be a pointer to a signed int.
The number of characters written so far is stored in the pointed location.
% A % followed by another % character will write a single % to the stream.%


flagsdescription
- Left-justify within the given field width; Right justification is the default (see width sub-specifier).
+ Forces to preceed the result with a plus or minus sign (+ or -) even for positive numbers. By default, only negative numbers are preceded with a - sign.
(space) If no sign is going to be written, a blank space is inserted before the value.
# Used with o, x or X specifiers the value is preceeded with 0, 0x or 0X respectively for values different than zero.
Used with a, A, e, E, f, F, g or G it forces the written output to contain a decimal point even if no more digits follow. By default, if no digits follow, no decimal point is written.
0 Left-pads the number with zeroes (0) instead of spaces when padding is specified (see width sub-specifier).


widthdescription
(number) Minimum number of characters to be printed. If the value to be printed is shorter than this number, the result is padded with blank spaces. The value is not truncated even if the result is larger.
* The width is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted.


.precisiondescription
.number For integer specifiers (d, i, o, u, x, X): precision specifies the minimum number of digits to be written. If the value to be written is shorter than this number, the result is padded with leading zeros. The value is not truncated even if the result is longer. A precision of 0 means that no character is written for the value 0.
For a, A, e, E, f and F specifiers: this is the number of digits to be printed after the decimal point (by default, this is 6).
For g and G specifiers: This is the maximum number of significant digits to be printed.
For s: this is the maximum number of characters to be printed. By default all characters are printed until the ending null character is encountered.
If the period is specified without an explicit value for precision, 0 is assumed.
.* The precision is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted.

Specyfikator length modyfikuje długość danego typu. Tabela przedstawia typy argumentów oczekiwane przez funkcję. W przypadku niezgodności typów, dokonywana jest konwersja (w dopuszczalnych sytuacjach).

specifiers
length d i u o x X f F e E g G a A cs p n
(none) int unsigned int doubleint char*void* int*
hhsigned char unsigned char signed char*
hshort int unsigned short int short int*
llong int unsigned long int wint_t wchar_t* long int*
lllong long int unsigned long long int long long int*
jintmax_t intmax_t intmax_t*
zsize_t size_t size_t*
tptrdiff_t ptrdiff_t ptrdiff_t*
L long double

Uwaga: Żółte wiersze oznaczają specyfikatory wprowadzone w standardzie C99.

Wartość zwracana

Funkcja zwraca liczbę wyprowadzonych znaków.

scanf


int scanf ( const char * format, ... );

Funkcja odczytuje dane ze standardowego wejścia (stdin) zgodnie z podanym formatem opisanym niżej. Dodatkowe argumenty powinny wskazywać na istniejące obiekty o typie określonym przez odpowiedni specyfikator formatu zawarty w parametrze format.

Format

Format składa się ze zwykłych znaków (innych niż znak '%') oraz sekwencji sterujących, zaczynających się od symbolu procenta, po którym następuje:

Wystąpienie w formacie białego znaku powoduje, że funkcje z rodziny scanf będą odczytywać i odrzucać znaki, aż do napotkania pierwszego znaku nie będącego białym znakiem.

Wszystkie inne znaki (tj. nie białe znaki oraz nie sekwencje sterujące) muszą dokładnie pasować do danych wejściowych.

Wszystkie białe znaki z wejścia są ignorowane, chyba że sekwencja sterująca określa format [], c lub n.

Jeżeli w sekwencji sterującej występuje gwiazdka to dane z wejścia zostaną pobrane zgodnie z formatem, ale wynik konwersji nie zostanie nigdzie zapisany. W ten sposób można pomijać część danych.

Maksymalna szerokość pola przyjmuje postać dodatniej liczby całkowitej zaczynającej się od cyfry różnej od zera. Określa ona ile maksymalnie znaków dany format może odczytać. Jest to szczególnie przydatne przy odczytywaniu ciągu znaków, gdyż dzięki temu można podać wielkość tablicy (minus jeden) i tym samym uniknąć błędów przepełnienia bufora.

specifierDescriptionCharacters extracted
iInteger Any number of digits, optionally preceded by a sign (+ or -).
Decimal digits assumed by default (0-9), but a 0 prefix introduces octal digits (0-7), and 0x hexadecimal digits (0-f).
Signed argument.
d or uDecimal integer Any number of decimal digits (0-9), optionally preceded by a sign (+ or -).
d is for a signed argument, and u for an unsigned.
oOctal integer Any number of octal digits (0-7), optionally preceded by a sign (+ or -).
Unsigned argument.
xHexadecimal integer Any number of hexadecimal digits (0-9, a-f, A-F), optionally preceded by 0x or 0X, and all optionally preceded by a sign (+ or -).
Unsigned argument.
f, e, gFloating point number A series of decimal digits, optionally containing a decimal point, optionally preceeded by a sign (+ or -) and optionally followed by the e or E character and a decimal integer (or some of the other sequences supported by strtod).
Implementations complying with C99 also support hexadecimal floating-point format when preceded by 0x or 0X.
a
c Character The next character. If a width other than 1 is specified, the function reads exactly width characters and stores them in the successive locations of the array passed as argument. No null character is appended at the end.
s String of characters Any number of non-whitespace characters, stopping at the first whitespace character found. A terminating null character is automatically added at the end of the stored sequence.
p Pointer address A sequence of characters representing a pointer. The particular format used depends on the system and library implementation, but it is the same as the one used to format %p in fprintf.
[characters] Scanset Any number of the characters specified between the brackets.
A dash (-) that is not the first character may produce non-portable behavior in some library implementations.
[^characters] Negated scanset Any number of characters none of them specified as characters between the brackets.
nCount No input is consumed.
The number of characters read so far from stdin is stored in the pointed location.
%% A % followed by another % matches a single %.

Z wyjątkiem n, przynajmniej jeden znak powinien zostać pobrany przez dany specyfikator, w przeciwnym przypadku dopasowanie zawodzi i skanowanie wejścia się kończy.

Specyfikator może także zawierać podspecyfikatory *, width and length (w tej kolejności), które są opcjonalne.

Specyfikator '%[' pozwala na podanie zbioru znaków, które powinny znaleźć się na wejściu. Konwersja (pobieranie znaków) kończy się po natrafieniu na pierwszy znak różny od opisanego.

Na przykład %[0-9] oznacza "pobierz wszystkie cyfry od 0 do 9", a %[AD-G34] "pobierz A, D do G, 3, lub 4".

Można też wypisać znaki, które NIE powinny znaleźć się na wejściu, poprzez użycie znaku '^' bezpośrednio po '%['. Na przykład %[^A-K] oznacz wszystkie znaki poza literami z zakresu A-K.

Aby umieścić na liście nawias zamykający (]), wpisujemy go jako pierwszy, np. %[]A-C] lub %[^]A-C]. Z kolei myślnik powinien być ostatni, np. %[A-C-]. Czyli jeżeli chcemy by na wejściu mogły się pojawiać wszystkie litery z wyjątkiem "%", "^", "]", "B", "C", "D", "E", i "-" można użyć następującego formatu: %[^]%^B-E-].

sub-specifierdescription
* An optional starting asterisk indicates that the data is to be read from the stream but ignored (i.e. it is not stored in the location pointed by an argument).
width Specifies the maximum number of characters to be read in the current reading operation (optional).
length One of hh, h, l, ll, j, z, t, L (optional).
This alters the expected type of the storage pointed by the corresponding argument (see below).

Tabela przedstawia typy argumentów oczekiwane przez funkcję

specifiers
length d i u o x f e g a c s [] [^] p n
(none) int* unsigned int* float* char* void** int*
hh signed char* unsigned char* signed char*
h short int* unsigned short int* short int*
l long int* unsigned long int* double* wchar_t* long int*
ll long long int* unsigned long long int* long long int*
j intmax_t* uintmax_t* intmax_t*
z size_t* size_t* size_t*
t ptrdiff_t* ptrdiff_t* ptrdiff_t*
L long double*

Uwaga: Żółte wiersze oznaczają specyfikatory wprowadzone w standardzie C99.

W zależności od stringu formatującego, funkcja oczekuje sekwencji dodatkowych argumentów, każdy będący wskaźnikiem do uprzednio zaalokowanej pamięci, gdzie wartości odpowiadające polom wejściowym zostaną zapamiętane.

Funkcja powinna mieć co najmniej tyle dodatkowych argumentów ile wynika i liczby specyfikatorów formatu. Dodatkowe argumenty są ignorowane.

Wartość zwracana

W przypadku sukcesu, funkcja zwraca liczbę poprawnie wczytanych pozycji. Mniejsza wartość zwykle oznacza błąd dopasowania, błąd odczytu lub koniec pliku wejściowego.