Zadania

  1. Makefile - przygotuj plik w języku make:

    1. wykorzystujący zmienne CC, CFLAGS

    2. zawierający cele pozwalające na budowę zadań w repozytorium

    3. dostarczający następujący domyślny zbiór flag do kompilatora C: -Wall -Wextra -fsanitize=address,undefined

    4. umożliwiający budowę następującego programu c89.c, zgodnego z standardem C89:

      main() { puts("hello, world"); }
  2. guess_a_number.c: Plik random.a zawiera dwie funkcje: jedna z nich zwraca losową liczbę całkowitą od 0 do przekazanego do niej argumentu, druga zwraca liczbę podaną przez użytkownika na standardowe wejście. Przy pomocy narzędzia nm znajdź nazwy tych funkcji, a następnie stwórz program w języku C, który wykorzystuje funkcje z random.a do zagrania z użytkownikiem w grę zgadywania liczby:

    1. Użytkownik wybiera górną granicę przedziału w którym ma być wylosowana liczba (np. 100)

    2. Program losuje liczbę od 0 do wyznaczonej przez użytkownika granicy

    3. Program prosi użytkownika o podanie liczby. Jeśli podana liczba jest większa od wylosowanej wypisuje too big, jeśli mniejsza too small, jeśli równa you guessed correctly in <n> rounds, gdzie <n> to liczba prób odgadnięcia podjętych przez użytkownika.

  3. Pobierz wydanie biblioteki Raylib (plik raylib-5.5_linux_amd64.tar.gz) i wykorzystując poniższy kod źródłowy (window.c), stwórz program window, który wykorzystuje bibliotekę Raylib

  4. math.h, math.c - zdefiniuj plik nagłówkowy math.h oraz odpowiadający mu plik źródłowy math.c. Do pliku Makefile dodaj cele pozwalające na budowę biblioteki. Biblioteka powinna zawierać:

    1. funkcję silnia int factorial(int n);

    2. makro MAX(a, b) zwracający większą z dwóch przekazanych liczb, wykorzystując operator warunkowy.

/* window.c */
#include <raylib.h>

int main()
{
	InitWindow(800, 600, "hello from Raylib");

	while (!WindowShouldClose()) {
		BeginDrawing();
		ClearBackground((Color) { 255, 0, 255, 255 });
		EndDrawing();
	}

	CloseWindow();
	return 0;
}

Kod źródłowy należy umieścić w repozytorium GIT.

Make

Standardowe narzędzie systemów Unixowych, ułatwiający budowanie programów. make automatycznie rozpozna, które pliki wymagają budowy na podstawie zależności określonych w pliku Makefile, skracając czas kompilacji większych projektów.

Pliki Makefile są często generowane przez zewnętrzne narzedzia, takie jak cmake.

Prosty przykład

Stwórz plik Makefile w katalogu z programem w języku C program.c:

program: program.c
	cc -o program program.c

Powyższy program, określa zasadę (rule) stworzenia programu program (cel; target) na podstawie pliku program.c. Pod spodem jest zbiór instrukcji shellowych, który opisuje w jaki sposób Make może zbudować program (wcięcie jest obowiązkowe).

Aby zbudować program, wystarczy polecenie make:

$ make
cc -o program program.c
$ make
make: 'program' is up to date.

Jeśli chcemy zbudować konkretne cele, można wymienić je jako argumenty programu make:

$ make program

Zmienne, zmienne domyślne

Aby zdefiniować zmienną należy użyć konstrukcji NAZWA=wartość. Aby użyć wartości zmiennej: $(NAZWA).

Make posiada kilka wbudowanych zmiennych:

  • CC - kompilator języka C

  • CFLAGS - flagi przekazywane do kompilatora języka C

  • LDLIBS - flagi przekazywane do linkera, określające biblioteki

  • LDFLAGS - dodatkowe flagi przekazywane do linkera

program: program.c
	$(CC) $(CFLAGS) -o program program.c

Jeśli chcemy zmienić jaki kompilator używany jest przez make:

$ CC=clang make program
clang -o program program.c

.PHONY

Cele można określić jako „phony” (fałszywe) - są to cele, które nie tworzą pliku wykonywalnego, a przez to są wykorzystywane wyłącznie do tworzenia poleceń. Przykładem może być cel all, który nie buduje pliku all, ale pozwala na zbudowanie wszystkich celów określonych w Makefile; cel clean, który usuwa artefakty budowania, „sprzątając” repozytorium:

all: a b

a: a.c
	cc -o a a.c

b: b.c
	cc -o b b.c

clean:
	rm -vf a b

.PHONY: all clean

Proces budowy programu w C

Proces budowy programu w języku C, od początku pliku źródłowego po końcowy plik wykonywalny jest kilkuetapowym procesem, który często jest podzielony na kilka programów. Niektóre z tych etapów wynikają z historycznych uwarunkowań (preprocesor), inne są przyjętą konwencją wszystkich systemowych języków programowania (kompilacja i linkowanie).

Preprocessing

Preprocesor języka C jest zwyczajowo traktowane jako osobne od kompilatora języka C narzędziem (dzisiaj będąc zawsze jego integralną częścią). Uzupełnia on język C o możliwość dołączania plików w trakcie kompilacji, definiowanie makr (sekwencji znaków zastępowanych przez inne sekwencje w kodzie) oraz warunkową kompilację.

Działanie preprocesora, kontrolowane jest przez tzw. dyrektywy - są to polecenia preprocesora, rozpoczynające się zawsze od znaku #, znajdujące się na początku linii (lub poprzedzone wyłącznie białymi znakami).

Preprocesor może być użyty przy pomocy polecenia cpp (C PreProcesor).

#include

Dyrektywa #include powoduje zastąpienie jej zawartością wskazanego przez nią pliku.

$ cat a.c
#include "b.c"

int main()
{
	return value;
}
$ cat b.c
int value = 0;
$ cpp a.c
# 0 "a.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "a.c"
# 1 "b.c" 1
int value = 0;
# 2 "a.c" 2

int main()
{
 return value;
}

Dyrektywa #include posiada dwa warianty, które różnią się miejscem od którego zaczyna się poszukiwanie pliku źródłowego:

  • #include <plik> zaczyna szukać plik wg zdefiniowanej przez kompilator listy „search paths”

  • #include "plik" zaczyna szukać plik od katalogu w którym znajduje się aktualnie kompilowany plik, a następnie identycznie do #include <plik>

cpp pozwala na zdobycie listy ścieżek, w których wyszukuje pliki:

$ cpp -v
(..)
Using built-in specs.
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include
 /usr/local/include
 /usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include-fixed
 /usr/include
End of search list.

Jeśli chcemy dodać dodatkowe ścieżki pozwala na to parametr -I:

$ cat some/other/path/b.c
int value = 0;
$ cpp -Isome/other/path/ a.c
# 0 "a.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "a.c"
# 1 "some/other/path/b.c" 1
int value = 0;
# 2 "a.c" 2

int main()
{
 return value;
}
$ cc -o a a.c
a.c:1:10: fatal error: b.c: No such file or directory
    1 | #include "b.c"
      |          ^~~~~
compilation terminated.
$ cc -Isome/other/path -o a a.c
$ ./a

#define

Dyrektywa #define pozwala na zastępowanie znalezionego tekstu w kodzie źródłowym przez inny tekst, definiując makra. Makra zwyczajowo zapisuje się dużymi literami.

#include <stdio.h>
#define PI 3.14

int main()
{
	printf("PI = %f\n", PI);
}

Makra mogą przyjmować argumenty, które zostaną wklejone w produkowanym tekście:

/* multiply.c */
#define MULTIPLY_WRONG(A, B) A * B
#define MULTIPLY_CORRECT(A, B) (A) * (B)

int main()
{
	MULTIPLY_WRONG(10 + 20, 20);
	MULTIPLY_CORRECT(10 + 20, 20);
}

Błąd w w pierwszej definicji makra łatwo zauważyć po tym jak wypiszemy wynik preprocesingu:

$ cpp multiply.c
# 0 "multiply.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "multiply.c"




int main()
{
	10 + 20 * 20;
	(10 + 20) * (20);
}

#if, #ifdef

Preprocesor umożliwia warunkowe zawarcie kodu przy pomocy instrukcji #if, #elsif, #endif. Umożliwia to warunkową kompilację - zmianę tego jaki kod zostanie dołączony do programu w zależności od np. systemu operacyjnego albo kompilatora.

Warunki mogą wykorzystywać standardowe operacje logiczne oraz defined(), które zwraca prawdę jeśli jego argument został zdefiniowany.

#if defined(foo) /* false */
#endif
#define foo
#if defined(foo) /* true */
#endif

Sprawdzanie wersji, wykorzystując wbudowane makro __STDC_VERSION__:

#include <stdio.h>

int main()
{
#if !defined(__STDC_VERSION__)
        printf("C90\n");
#elif __STDC_VERSION__ == 199901L
        printf("C99\n");
#elif __STDC_VERSION__ == 201112L
        printf("C11\n");
#else
        printf("C17 or newer\n");
#endif
        return 0;
}
$ cc -o version version.c && ./version
C17 or newer
$ cc -o version version.c -std=c11 && ./version
C11
$ cc -o version version.c -std=c90 && ./version
C90
$ cc -o version version.c -std=c99 && ./version
C99

Konstrukcja #if defined(X) może być zapisana w formie skróconej #ifdef X, konstrukcja #if !defined(X) może być zapisana w formie skróconej #ifndef X.

Pliki nagłówkowe

Wykorzystanie mark i warunków preprocesora języka C pozwala na stworzenie plików nagłówkowych - są to pliki języka C o specjalnym przeznaczeniu - bycia dołączonym przez inne pliki źródłowe. Rozszerzenie plików nagłówkowych to .h.

Pliki nagłówkowe zawierają deklaracje typów, funkcji i zmiennych. Przykładowy plik nagłówkowy:

/* math.h */
#ifndef MATH_H /* jeśli MATH_H nie zostało zdefiniowane */
#define MATH_H /* zdefiniuj MATH_H */

/* extern oznacza, że zmienna jest zdefiniowana w innym miejscu */
extern double pi;

double power(double x, int n);
#endif
Struktura #ifndef, #define, kod pliku nagłówkowego, #endif jest standardową konwencją plików nagłówkowych w języku C. Pozwala na dołączenie pliku wielokrotnie w ramach programu, rozwiązując problem diamentu.

Powiązany z nim plik źródłowy:

/* math.c */
/* dołączamy math.h by się upewnić, że deklaracje zgadzają się z definicjami */
#include "math.h"

double pi = 3.14;

double power(double x, int n)
{
	double res = 1;
	for (int i = 0; i < n; ++i) {
		res *= x;
	}
	return res;
}

Wykorzystanie:

/* usage.c */
#include <stdio.h>
#include "math.h"

int main()
{
	double radius = 1;
	printf("circle area is: %f\n", pi * power(radius, 2));
	return 0;
}

Kompilacja (należy przekazać oba pliki C - oryginalny program wyłącznie dołącza plik nagłówkowy, musimy sami dostarczyć plik źródłowy) i uruchomienie:

$ cc -o usage usage.c math.c
$ ./usage

Kompilacja

Proces zamiany kodu źródłowego w C na kod maszynowy. Wynikiem są tzw. pliki obiektowy (ang. object files, rozszerzenie .o) - pliki, które zawierają gotowy kod maszynowy funkcji, deklaracje zmiennych itp, ale nie są jeszcze pełnoprawnym plikiem wykonywalnym - posiadają nierozwiązane zależności zewnętrzne (takie jak np. funkcja printf).

Aby zbudować wyłącznie plik obiektowy należy użyć flagi -c (compile):

$ cc -c -o math.o math.c

Narzędzie nm

Systemy unixowe posiadają wbudowane narzędzie nm umożliwiające inspekcję plików wykonywalnych i obiektowych.

$ cc -c -o math.o math.c
$ nm math.o
0000000000000000 D pi
0000000000000000 T power

Szczegółowa dokumentacja dostępna jest w ramach man. Na powyższym przykładzie symbole oznaczają:

  • D - symbol zdefiniowany jest w sekcji zainicjalizowanych definicji (jest to zainicjalizowana zmienna)

  • T - symbol zdefiniowany jest w sekcji kodu (jest to funkcja)

Odnosi się to do budowy pliku wykonywalnego. Pliki wykonywalne (i obiektowe; w ramach systemów Unix mają ten sam format) składają się z sekcji, które są w różny sposób obsługiwane przez program inicjalizujący proces:

  • sekcja danych (.data) - sekcja zawierająca zdefiniowane zmienne globalne, które można modyfikować w trakcie wykonywania programów

  • sekcja danych do odczytu (.rodata) - sekcja zawierające zdefiniowane zmienne globalne wyłącznie do odczytu

  • sekcja wyzerowana (.bss) - niezainicjalizowane zmienne globalne

  • sekcja kodu (.text) - sekcja zawierająca kod maszynowy funkcji

Linkowanie

Proces łączenia plików obiektowych w plik wykonywalny. Rozwiązywane są zależności kodu z bibliotekami zewnętrznymi oraz z innymi plikami obiektowymi. Standardową nazwą programu linkującego jest ld:

$ ld /usr/lib/x86_64-linux-gnu/crt*.o -lc usage.o math.o -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o usage

Kompilator automatycznie znajduje i linkuje odpowiednie moduły, upraszczając komendę do:

$ cc usage.o math.o -o usage
Argumenty powyższej komendy są zależne od dystrybucji systemu Linux.

Linkowanie dynamiczne

Powyższy przykład wykorzystywał linkowanie dynamiczne - zakłada ono, że brakujący kod konieczny do uruchomienia programu (taki jak biblioteka standardowa C) zostanie dostarczony przy starcie programu przez program ładujący. Aby zobaczyć zależności pliku, który wykorzystuje linkowanie dynamiczne można wykorzystać polecenie ldd:

$ ldd usage
linux-vdso.so.1 (0x00007ffe8a490000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48a3b8d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f48a3db1000)

Dynamiczne biblioteki oznaczane są rozszerzeniem .so (w ramach systemów Windows .dll).

Linkowanie statyczne

Linkowanie statyczne łączy kod użytkownika z kodem wykorzystywanych bibliotek do pojedynczego pliku wykonywalnego, bez dodatkowych zależności.

$ cc -o usage usage.o math.o -static
$ ldd usage
not a dynamic executable

Biblioteki

Język C nie posiada centralnego systemu zarządzania bibliotekami lub formatu, w jakim są one przechowywane i dystrybuowane. W zależności od systemu operacyjnego konwencja katalogów, nazw i przekazywanych plików może być różna.

Biblioteki zazwyczaj są dystrybuowane w formie archiwum (.tar.gz na Unixach, .zip na Windowsach), zawierające:

  • Pliki nagłówkowe, zazwyczaj w podkatalogu include

  • Biblioteki dynamiczne i statyczne, zazwyczaj w podkatalogu lib

  • Kod źródłowy (jeśli otwartoźródłowa) w podkaalogu src

  • Dokumentacja w plikach README, katalogu doc

Tworzenie bibliotek

Tworzymy pliki źródłowe i nagłówkowe:

/* hello.h */
#ifndef HELLO_H
#define HELLO_H

void hello();

#endif
/* hello.c */
#include <stdio.h>
#include "hello.h"

void hello()
{
	printf("hello, world\n");
}

Przygotowujemy plik obiektowy:

$ cc -c -o hello.o hello.c

I zmieniamy go w bibliotekę:

$ ar rc libhello.a hello.o

Następnie tworzymy odpowiednią strukturę katalogów i tworzymy archiwum:

$ mkdir -p hello/include hello/lib
$ cp hello.h hello/include/
$ cp libhello.a hello/lib/
$ tar cvf hello.tar.gz hello

Wynikowe archiwum hello.tar.gz jest gotową biblioteką, którą możemy opublikować np. w ramach wydań naszego projektu w serwisie Github.

Biblioteki systemowe

Poza biblioteką standardową i matematyczną C posiada dostęp do bibliotek systemowych. Są to biblioteki oferowane przez system operacyjny lub biblioteki zainstalowane przez menadżera paczek.

Narzędziem, pozwalającym na inspekcję zainstalowanych paczek jest pkg-config.

Listowanie bibliotek:

$ pkg-config --list-package-names

Narzędzie pozwala na zdobycie informacji potrzebnych do kompilacji z użyciem danej biblioteki:

$ pkg-config sdl3 --cflags

$ pkg-config sdl3 --libs
-lSDL3
$ cat test.c
#include <SDL3/SDL.h>
#include <stdio.h>

int main()
{
        printf("%d\n", SDL_VERSION);
        return 0;
}
$ cc test.c -o test $(pkg-config sdl3 --libs) $(pkg-config sdl3 --cflags)
$ ./test
3002004

Biblioteki zewnętrzne

Przykładowe wykorzystanie biblioteki zewnętrznej, będącej interpreterem języka Lua.

Po pobraniu źródeł z oficjalnej strony języka, należy je rozpakować i przejść do wypakowanego katalogu:

$ tar xvf lua-5.4.7.tar.gz
$ cd lua-5.4.7

Dokumentacja zawarta wraz z biblioteką w katalogu doc wskazuje w jaki sposób należy zbudować bibliotekę. Wystarczy użyć polecenia make, a następnie make local by stworzyć klasyczną strukturę biblioteki w podkatalogu install

$ make
$ make local

Wynikowymi plikami skryptów są:

install
├── bin
│   ├── lua       # interpreter Lua
│   └── luac      # kompilator Lua
├── include       # katalog z plikami nagłówkowymi
│   ├── lauxlib.h
│   ├── luaconf.h
│   ├── lua.h
│   ├── lua.hpp
│   └── lualib.h
├── lib
│   ├── liblua.a  # biblioteka statyczna
│   └── lua
│       └── 5.4
├── man
│   └── man1
│       ├── lua.1
│       └── luac.1
└── share
    └── lua
        └── 5.4

Następnie możemy stworzyć przykładowy program, wykorzystujący bibliotekę Lua:

/* hello.c */
#include <lauxlib.h>
#include <lualib.h>

int main()
{
	lua_State *L = luaL_newstate();
	luaL_openlibs(L);
	luaL_dostring(L, "print(\"hello, world\")");
	lua_close(L);
	return 0;
}

Kompilacja wygląda następująco (zakładając, że hello.c jest w katalogu, w którym jest katalog install):

$ cc hello.c -o hello -Iinstall/include -Linstall/lib -llua -lm
$ ./hello
hello, world

Wykorzystano parametry -I do wskazania katalogu w którym znajdują się pliki nagłówkowe; -L do wskazania katalogu w którym są biblioteki; -llua w celu dołączenia biblioteki Lua.