Zadania
-
Makefile
- przygotuj plik w językumake
:-
wykorzystujący zmienne
CC
,CFLAGS
-
zawierający cele pozwalające na budowę zadań w repozytorium
-
dostarczający następujący domyślny zbiór flag do kompilatora C:
-Wall -Wextra -fsanitize=address,undefined
-
umożliwiający budowę następującego programu
c89.c
, zgodnego z standardem C89:main() { puts("hello, world"); }
-
-
guess_a_number.c
: Plikrandom.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ędzianm
znajdź nazwy tych funkcji, a następnie stwórz program w języku C, który wykorzystuje funkcje zrandom.a
do zagrania z użytkownikiem w grę zgadywania liczby:-
Użytkownik wybiera górną granicę przedziału w którym ma być wylosowana liczba (np. 100)
-
Program losuje liczbę od 0 do wyznaczonej przez użytkownika granicy
-
Program prosi użytkownika o podanie liczby. Jeśli podana liczba jest większa od wylosowanej wypisuje
too big
, jeśli mniejszatoo small
, jeśli równayou guessed correctly in <n> rounds
, gdzie<n>
to liczba prób odgadnięcia podjętych przez użytkownika.
-
-
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 programwindow
, który wykorzystuje bibliotekę Raylib -
math.h
,math.c
- zdefiniuj plik nagłówkowymath.h
oraz odpowiadający mu plik źródłowymath.c
. Do plikuMakefile
dodaj cele pozwalające na budowę biblioteki. Biblioteka powinna zawierać:-
funkcję silnia
int factorial(int n);
-
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
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
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
, katalogudoc
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.