Zadania
Dla zadania algorithm.h zdefiniowano pakiet testów: test_algorithm.c. Możesz go wykorzystać by zweryfikować tworzone przez siebie implementacje.
-
calc.c: program proszący użytkownika o dwie liczby zmiennoprzecinkowe oraz operację matematyczną jaką ma na nich wykonać. Wspierane operacje to: dodawanie (+), mnożenie (*), potęga (^), logarytm (l). Wykorzystaj bibliotekę matematyczną oraz funkcjęscanf. -
definitions.c: stwórz plik z programem, który zawiera poprawne definicje i inicjalizacje (wartościami różnymi odNULLjeśli nie powiedziano inaczej) zmiennych:-
tablica
integersbędąca wyzerowaną tablicą 256 liczb całkowitych -
tablica
bufferbędąca tablicą wyzerowanych 8 bitowych liczb bez znaku o rozmiarze 1MiB -
tablice
multiplicationbędąca dwuwymiarową tablicą zawierającą wynik mnożenia indeksów danego pola -
zmienna
constantbędąca wskaźnikiem na stałą 16 bitową wartość całkowitoliczbową
-
-
algorithm.h,algorithm.c: stwórz bibliotekę algorithm zawierającą:-
funkcję
void swap_ints(int *a, int *b);, która zamieni miejscamiaib -
funkcję
void swap(void *a, void *b, size_t size);, która zamieni miejscamiaib, gdzieaibmają rozmiarsize(tip: użyj funkcjimemcpy)
-
Typy
Typy podstawowe (zależne od platformy)
-
void, wykorzystywany głównie do oznaczenia wskaźników wskazujących na dowolny typ i funkcji, które nie zwracają wartości -
char- typ reprezentujący „znak” (co jest prawdziwe wyłącznie dla znaków ASCII). W zależności od implementacji może być równoważny zsigned charlubunsigned char -
signed char,short,int,long,long long- typy całkowitoliczbowe (ze znakiem), których rozmiar jest zależny od platformy. Zdefiniowane są przez następującą nierówność:1 <= sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). Jeśli chcemy wyrazić wprost ich całkowitoliczbowość można dodać prefixsigned(np.signed int). -
unsigned char,unsigned short,unsigned int,unsigned long,unsigned long long- odpowiedniki powyższych typów liczbowych, bez znaku. -
float,double,long double- wartości zmiennoprzecinkowe, zgodne z standardem IEEE-754.floatjest 32 bitową wartością pojedyńczej precyzji,double 64 bitową wartością pojedyńczej precyzji, a `long doublezależy od platformy (co najmniejdouble) -
bool(przed C23 w pliku<stdbool.h>), przechowujący wartości logiczne -
operator
sizeof(X), który zwraca wartość typusize_t(<stddef.h>) -
wynik różnicy wskaźników (
p - q, gdziep,qsą wskaźnikami) to wartość typuptrdiff_t(<stddef.h>)
| Typ | Linux/macOS 32bit | Windows 32bit | Windows 64 bit | Linux/macOS 64bit |
|---|---|---|---|---|
|
8 |
8 |
8 |
8 |
|
16 |
16 |
16 |
16 |
|
32 |
32 |
32 |
32 |
|
32 |
32 |
32 |
64 |
|
64 |
64 |
64 |
64 |
Wskaźnik |
32 |
32 |
64 |
64 |
|
32 |
32 |
64 |
64 |
|
32 |
32 |
64 |
64 |
Różnica pomiędzy int, a bool
Zwyczajowo w języku C typ int traktowany jest jako główny typ do przechowywania wartości logicznych (prawdy i fałszu). Może to prowadzić jednak do nieintuicyjnych konsekwencji:
$ cat bool.c
#include <stdbool.h>
#include <stdio.h>
int main()
{
printf("%d\n", (bool)0.5);
printf("%d\n", (int)0.5);
return 0;
}
$ cc bool.c -o bool
$ ./bool
1
0
Typy o znanym rozmiarze
Dostępne w pliku nagłówkowym <stdint.h>.
Grupą najczęściej wykorzystywanych typów są: int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t i są, w praktyce, standardem.
Modyfikator const
const pozwala na deklarację zmiennej jako stałej - takiej, której wartości nie będziemy modyfikować.
Główną siłą const jest informacja dla programisty - można przy pomocy const oznaczyć wskaźniki, które wyłącznie przekazują wartości do funkcji - a zatem gwarantujemy programiście, że nie będziemy ich modyfikować.
// const przed wskaźnikiem - to co jest stałe to `int` „pod” wskaźnikiem
void print_int(int const* x)
{
printf("%d\n", *x);
// *x += 1; <- to byłby błąd
x += 1; // legalne, w tym przypadku bez zastosowania
}
// const za wskaźnikiem - to co jest stałe to wskaźnik, zmienną możemy modifkować
void modify(int* const x)
{
// x += 1; <- to byłby błąd
*x += 1;
printf("%d\n", *x);
}
Wskaźniki
Wskaźnik jest to adres do wartości określanego przez wskaźnik typu w pamięci.
Wskaźniki są wytrychem pozwalającym na realizację wielu funkcjonalności bardziej złożonych języków programowania przy zachowaniu modelu operowania wyłącznie na wartościach liczbowych.
Przykładowe zastosowanie wskaźników:
#include <stdio.h>
int main()
{
int x = 0;
int *a, *b; // aby stworzyć zmiennę typu wskaźnikowego używamy *
a = &x; // &x pobiera adres zmiennej x
b = &x; // można mieć wiele wskaźników do tej samej zmiennej
printf("a = %p\n", a); // %p używamy do wypisania wskaźników
printf("b = %p\n", b);
// *a używamy do dereferencji - zamiany adresu w wartość znajdującą się pod danym adresem
printf("*a = %d\n", *a);
printf("*b = %d\n", *b);
printf("x = %d\n", x);
// możemy zmodyfikować zmienną przez którykolwiek z wskaźników
*a += 1;
*b += 1;
// a także przez oryginalną nazwę
x += 1;
printf("*a = %d\n", *a);
printf("*b = %d\n", *b);
printf("x = %d\n", x);
return 0;
}
L-wartość i r-wartość (ang. l-value, r-value)
Wskaźniki zazwyczaj odwołują się do l-wartości: wartości, których czas życia (ang. lifetime) wykracza poza wyrażenie w którym są użyte.
Zazwyczaj są to wartości, które są po lewej stronie znaku = - stąd l (ang. left) wartość.
W przeciwieństwie do nich są r-wartości, które mogą albo zostać użyte i zmienione w nową r-wartość, albo przypisane do zmiennej, stając się l-wartością.
-
Przykłady l-wartości: nazwa zmiennej
int x; x, dereferencja (*x), wynik preinkrementacji (++x), indeksowaniex[i] -
Przykłady r-wartości: literały liczbowe (
5,0.5), wartości zwracane przez funkcjęadd(5, 4), wynik operatorasizeof, pobranie adresu&x
Zastosowanie: przekazywanie wskaźników do funkcji
#include <stdio.h>
void increment(int *x)
{
*x += 1;
printf("in increment: %d\n", *x);
}
int main()
{
int x = 0;
printf("before increment: %d\n", x);
increment(&x);
printf("after increment: %d\n", x);
}
Arytmetyka
Wskaźniki są adresami, wskazującymi na zmienne określonego typu. Pozwala zdefiniować to arytmetykę wskaźników, która zachowuje własności oczekiwane od adresów do wartości danego typu.
Wskaźnik p przesunięty w prawo o 1 przez wyrażenie p+1 wskazuje na adres kolejnej wartości danego typu.
Wyrażenie x+1 jest tym samym równoważne (char*)x + sizeof(*x).
Podobnie można przesuwać wskaźnik w lewo, z wykorzystaniem operatora -.
Aby uzyskać wartość znajdującej się w oddalonej o i od x komórce pamięci można zastosować wyrażenie *(x+i).
Jest ona równoważna operacji indeksu: x[i].
Tablice statyczne
Tablice statyczne, są to tablice, których rozmiar jest zadeklarowany w programie - a przez to są alokowane przez kompilator w trakcie budowy pliku wykonywalnego.
Deklaracja tablicy
<typ> <nazwa-zmiennej>[<rozmiar>];
np. int nums[10];. Jeśli równocześnie inicjalizujemy tablicę, jej długość może być automatycznie wydedukowana z inicjalizacji:
// Wszystkie tablice xs mają te same długości i elementy
int xs1[5] = { 1, 2, 3, 0, 0 };
int xs2[] = { 1, 2, 3, 0, 0 }; // pominięto rozmiar - jest automatycznie wypełniony przez kompilator
int xs3[5] = { 1, 2, 3 }; // pominięto 2 ostatnie elementy - automatycznie mają wartość zero
Zarządzanie rozmiarem tablicy w programie
Rozmiar tablicy możemy manualnie wykorzystywać w programie:
int xs[10];
for (int i = 0; i < 10; ++i) {
xs[i] = i;
}
Nie jest to jednak wygodne i bezpieczne: po zmianie rozmiaru tablicy musimy manualnie zmodyfikować wszystkie miejsca użycia tablicy o nowy rozmiar.
Makro definiujące rozmiar tablicy
Pierwsze z dwóch popularnych rozwiązań: zdefiniowanie makra, które jest wykorzystywane jako rozmiar tablicy.
#define XS_LEN 10
int xs[XS_LEN];
for (int i = 0; i < XS_LEN; ++i) {
xs[i] = i;
}
Makro określające rozmiar tablicy statycznej
Alternatywnym rozwiązaniem jest zdefiniowanie makra, które może pobrać rozmiar tablicy.
Wykorzystywana jest mechanika operatora sizeof, który zwraca rozmiar całej tablicy.
Dzieląc rozmiar całej tablicy przez rozmiar pojedyńczego elementu, otrzymujemy liczbę elementów!
#define ARRAY_LENGTH(X) (sizeof(X) / sizeof(X[0]))
int xs[10];
for (int i = 0; i < ARRAY_LENGTH(xs); ++i) {
xs[i] = i;
}
Przekazywanie tablic do funkcji
C nie umożliwia przekazywanie (ani zwracanie) tablic statycznych do funkcji. Wymagane jest przekazywanie ich przez wskaźnik. Ponieważ wskaźnik nie zawiera informacji o długości tablicy, zazwyczaj przekazywany jest również jej rozmiar:
#include <stdio.h>
void print(int *xs, int n)
{
for (int i = 0; i < n; ++i) {
if (i == 0) {
printf("%d", xs[i]);
} else {
printf(", %d", xs[i]);
}
}
printf("\n");
}
int main()
{
int numbers[] = { 1, 2, 3, 4, 5, 6 };
print(numbers, 6);
return 0;
}
Alternatywnym sposobem przekazywania tablic jest para wskaźników, jeden wskazujący na początek, drugi na koniec tablicy. Zwyczajowo przekazywany jest wskaźnik do elementu za ostatnim elementem tablicy:
#include <stdio.h>
void print(int *begin, int *end)
{
for (int *it = begin; it != end; ++it) {
if (it == begin) {
printf("%d", *it);
} else {
printf(", %d", *it);
}
}
printf("\n");
}
/*
| 1 | 2 | 3 | 4 | 5 | 6 |
0 1 2 3 4 5 6
^ begin ^ end (begin + 6)
^ numbers
*/
int main()
{
int numbers[] = { 1, 2, 3, 4, 5, 6 };
print(numbers, numbers + 6);
return 0;
}
Funkcja scanf
Funkcja scanf pozwala na pobieranie wartości z standardowego wejścia.
Wykorzystuje te same oznaczenia co printf.
#include <stdio.h>
int main()
{
int a, b;
double c;
scanf("%d", &a); // wczytaj liczbę całokwitą
scanf("%f", &c); // wczytaj liczbę zmiennoprzecinkową
scanf("%d %d", &a, &b); // wczytaj dwie liczby całkowite rozdzielone spacją
printf("a=%d, b=%d, c=%f\n", a, b, c);
return 0;
}
Przykładowy program, który wczytuje liczby rozdzielone przecinkami zaprezentowany został poniżej.
Wykorzystuje on wartość zwracaną przez scanf - liczbę wczytanych elementów:
#include <stdio.h>
int main()
{
int total = 0;
int n;
scanf("%d", &n);
total += n;
while (scanf(",%d", &n) == 1) {
total += n;
}
printf("Total: %d\n", total);
return 0;
}