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 odNULL
jeśli nie powiedziano inaczej) zmiennych:-
tablica
integers
będąca wyzerowaną tablicą 256 liczb całkowitych -
tablica
buffer
będąca tablicą wyzerowanych 8 bitowych liczb bez znaku o rozmiarze 1MiB -
tablice
multiplication
będąca dwuwymiarową tablicą zawierającą wynik mnożenia indeksów danego pola -
zmienna
constant
bę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 miejscamia
ib
-
funkcję
void swap(void *a, void *b, size_t size);
, która zamieni miejscamia
ib
, gdziea
ib
mają 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 char
lubunsigned 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.float
jest 32 bitową wartością pojedyńczej precyzji,double 64 bitową wartością pojedyńczej precyzji, a `long double
zależ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
,q
są 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;
}