Need to know

Fișiere header vs. Fișiere cod sursă în C

Nu există vreo diferență tehnică între cele două tipuri de fișiere; compilatorul îți permite să incluzi un fișier .c într-o altă sursă, sau să compilezi un fișier .h, dacă asta este ceea ce dorești să faci. Există însă o diferență culturală:

  • Declarările se găsesc în fișierele .h; acestea pot fi vizualizate ca o interfață către orice este implementat în fișierul .c corespondent.
  • Definițiile se găsesc în fișierele .c; ele implementează interfața specificată în fișierul .h.

Un fișier .h poate (și este de obicei) inclus în mai multe fișiere .c. În cazul în care o funcție este definită în cadrul acestuia, va ajunge în mai multe fișiere cod obiect, iar linker-ul se 'va plânge' de multiple definiții ale simbolului respectiv. Din acesta cauză definițiile nu trebuie să fie în fișierele header.

La calea ~/uso-lab/04-appdev/support/need-to-know avem 3 fișiere cod sursă C și 2 fișiere header.

student@uso:~/uso-lab/04-appdev/support/need-to-know$ ls
a.c     a.h     b.c     b.h     main.c

Fișierul main.c este cel care conține și funcția main, adică punctul de intrare în program.

student@uso:~/uso-lab/04-appdev/support/need-to-know$ cat main.c
#include <stdio.h>
#include <stdlib.h>
 
#include "a.h"
#include "b.h"
 
int main() {
    printf("globalVar = %d\n", globalVar);
    print1();
 
    double value = print2(0.5);
    printf("value = %f\n", value);
 
    return 0;
}

Se observă faptul că pe lângă bibliotecile standard stdio și stdlib, main.c mai are nevoie de câteva resurse externe și anume a.h și b.h. În aceste 2 fișiere găsim declarații de funcții și/sau variable pe care vrem să le folosim în main.c.

a.h

#ifndef A
#define A
 
extern int globalVar;
 
void print1();
 
#endif

b.h

#ifndef B
#define B
 
double print2(double angle);
 
#endif

Fiecare fișier header vine asociat cu un fișier cod sursă în care sunt definite toate funcțiile/variabilele declarate în headere. Fișierele cod sursă includ toate headerele asociate. Spre exemplu, a.c include fișierul a.h, la fel și pentru b.c. Trebuie reținut faptul că pentru a putea avea un program funcțional, trebuie să fie compilate toate fișierele cod sursă.

În această situație, vom compila toate cele 3 fișiere cod sursă într-un singur binar. Pentru asta folosim tot gcc.

student@uso:~/uso-lab/04-appdev/support/need-to-know$ gcc main.c a.c b.c -o main
student@uso:~/uso-lab/04-appdev/support/need-to-know$ ls
a.c     a.h     b.c     b.h     main    main.c

Observați că fișierele header nu au fost incluse în comanda de compilare deoarece ele sunt incluse în fișierele cod sursă și nu e nevoie să le includem și aici.

NU includeți niciodată fișiere cod sursă în alte fișiere cod sursă.

NU includeți niciodată definiții în fișiere header. Fișierele header sunt doar pentru declarații, fișierele cod sursă sunt cele care trebuie să conțină implementarea.

  1. Rulați programul main și asigurați-vă că rularea lui se termină cu succes.
  2. Urmăriți pașii de mai jos:
    1. Declarați o funcție cu numele bad fără parametri care nu întoarce nimic, în fișierul a.h (Atenție: doar declarația!).
    2. În fișierul main.c, înainte de linia return 0, apelați această funcție (bad()).
    3. Compilați din nou acest program (cu aceeași comandă pe care ați văzut-o mai sus). Ce s-a întâmplat? Care este motivul? Discutați cu asistentul.
    4. Rezolvați problema apărută fără a șterge apelul de funcție bad() din funcția main din fișierul main.c.

De fiecare dată când vreți să apelați o funcție declarată într-un fișier header, trebuie să existe o definiție / implementare a ei într-un fișier cod sursă, altfel programul nu este complet și nici corect!

Linkare cu o bibliotecă

Există câteva biblioteci care sunt linkate automat atunci când compilăm un program C. Una dintre acestea este libc. Majoritatea programelor C folosesc funcții din aceastea bibliotecă și s-a hotărât să fie linkată întotdeauna, însă aceasta nu este singura bibliotecă externă pe care putem să o folosim. Putem să creăm noi o bibliotecă și să o linkăm programului nostru sau să folosim o bibliotecă deja existentă cum ar fi biblioteca ce conține funcții matematice math.

Pentru a folosi biblioteca math în programul nostru, trebuie să facem 2 lucruri.

  1. Să includem headerul math.h în program, pentru a putea preciză că se vor folosi funcții declarate acolo.
  2. În momentul în care compilăm programul nostru, trebuie să-i precizăm compilatorului că noi folosim funcții din biblioteca math.h și că vrem ca binarul nostru să fie linkat cu biblioteca math.

Urmăriți tutorialul de mai jos.

În directorul ~/uso-lab/04-appdev/support/need-to-know creați un nou director cu numele using-math și intrați în el.

student@uso:~/uso-lab/04-appdev/support/need-to-know$ mkdir using-math
student@uso:~/uso-lab/04-appdev/support/need-to-know$ cd using-math
student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$ ls
student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$

Creați aici un fișier simplu numit main.c cu următorul conținut:

#include <stdio.h>
#include <math.h>

int main(void) {
	float x;
	printf("Give me a number: ");
	scanf("%f", &x);
	printf("The square root for it is: %f\n", sqrt(x));
	return 0;
}

Observați faptul că am inclus biblioteca math.h pentru a putea folosi funcția sqrt pentru calcularea radicalului unui număr.

Compilați programul folosind gcc astfel încât executabilul obținut să se numeasca main.

student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$ ls
main.c
student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$ gcc main.c -o main
/tmp/ccRBgYrK.o: In function `main':
main.c:(.text+0x35): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status

Eroarea apare din cauză că gcc nu știe unde să localizeze funcția sqrt pe care noi o folosim în program. Pentru a rezolva această eroare, atunci când compilăm programul trebuie să dăm un argument în plus lui gcc care să specifice că vrem să linkăm executabilul nostru cu biblioteca math. Practic, îi promitem compilatorului că la rularea programului, funcția sqrt va exista pentru că îi spunem noi acum unde este.

student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$ ls
main.c
student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$ gcc main.c -o main -lm
student@uso:~/uso-lab/04-appdev/support/need-to-know/using-math$ ./main
Give me a number: 1234
The square root for it is: 35.128336

Acum programul a compilat cu succes și am putut să-l și rulăm.

Automatizarea procesului de compilare - Makefile

Până acum am compilat fișierele sursă folosind comanda gcc direct din terminal. Acest proces poate deveni anevoios în momentul în care proiectul conține mai multe fișiere sursă sau dacă codul depinde de niște variable externe. Nu vrem să scriem comanda de compilare de fiecare dată, vrem să automatizăm acest proces. Pentru asta folosim fișierele Makefile.

Dacă avem într-un directorul fișierul test.c și vrem să-l compilăm, pur și simplu rulăm comanda gcc test.c acolo și obținem un executabil cu numele a.out. Pot apărea situații când compilarea unui program este mai complexă. Spre exemplu, mai sus, existau 3 fișiere sursă pe care trebuia să le compilăm ca programul să funcționeze.

Un exemplu de fișier Makefile simplu care ar automatiza acest proces este următorul:

main:
	gcc main.c a.c b.c -o main
clean:
	rm main

Fișierul Makefile de mai sus conține 2 reguli: main și clean. Regula main va fi folosită pentru a compila programul, pe când regula clean va fi folosită pentru a șterge toate fișierele generate în urma compilării.

Atenție! Regulile în Makefile trebuie scrise la stânga de tot (fără spații la începutul rândului), iar comenzile aferente fiecărei reguli trebuie să se afle pe linia următoare, iar linia să înceapă cu <TAB>, nu spații.

  1. Creați fișierul Makefile de mai sus la calea ~/uso-lab/04-appdev/support/need-to-know/. Denumiți fișierul Makefile.

Pentru a rula regula main trebuie să scriem comanda make main.

student@uso:~$ cd ~/uso-lab/04-appdev/support/need-to-know
student@uso:~/uso-lab/04-appdev/support/need-to-know$ ls
a.c     a.h     b.c     b.h     main.c    Makefile
student@uso:~/uso-lab/04-appdev/support/need-to-know$ make main
gcc main.c a.c b.c -o main
student@uso:~/uso-lab/04-appdev/support/need-to-know$ ls
a.c     a.h     b.c     b.h     main		main.c    Makefile

Vedem acum că a fost creat executabilul main pe care putem să-l rulăm în continuare.

  1. Rulați executabilul main și asigurați-vă că funcționează.
  2. Adăugați o regulă numită run în Makefile care să aibă drept comandă asociată chiar comanda cu care rulăm programul main. Nu treceți mai departe până când partea aceasta nu este clară.
  3. Ștergeți executabilul main folosind una dintre regulile definite în Makefile.

Dacă se va rula în terminal doar comanda make (fără a fi urmată de vreun argument), atunci se va executa prima regulă găsită în Makefile, în cazul acesta tot regula main.

student@uso:~$ cd ~/uso-lab/04-appdev/support/need-to-know
student@uso:~/uso-lab/04-appdev/support/need-to-know$ ls
a.c     a.h     b.c     b.h     main.c    Makefile
student@uso:~/uso-lab/04-appdev/support/need-to-know$ make
gcc main.c a.c b.c -o main
student@uso:~/uso-lab/04-appdev/support/need-to-know$ ls
a.c     a.h     b.c     b.h     main		main.c    Makefile
  1. La calea ~/uso-lab/04-appdev/support/need-to-know/ creați un Makefile care să conțină regulile main, run și clean. Nu treceți mai departe până când asistentul confirmă că este în regulă.

Următorii pași în lumea git-ului

Înainte de a parcurge această parte a secțiunii, rulați comanda cd ~/my-awesome-project.

Vom crea acum 2 fișiere noi în acest director. Le vom numi tom.txt și jerry.txt. Scrieți câte un mesaj în fiecare dintre ele.

student@uso:~/my-awesome-project$ touch tom.txt
student@uso:~/my-awesome-project$ touch jerry.txt
(..) # aici editați fișierele

Vrem să vedem statusul repository-ul de git. Folosim comanda git status.

student@uso:~/my-awesome-project$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	jerry.txt
#	tom.txt
nothing added to commit but untracked files present (use "git add" to track)

Scopul nostru acum este de a crea 2 commit-uri, unul care să se refere la adăugarea fișierului tom.txt, iar celălalt la adăugarea lui jerry.txt

  1. Creați 2 commit-uri, unul cum mesajul added tom.txt file și celălalt cu added jerry.txt file. Verificați că cele 2 commit-uri s-au efectuat cu succes (hint: git log).

La rularea comenzii git log va trebui să primiți un output similar cu următorul:

commit 368bf97efe83109491a3f21874aa763a8f1fe682
Author: student <student@uso>
Date:   Wed Oct 9 14:30:34 2019 +0300

    added jerry.txt file

commit 55e8b77f46ae355d67e8ed072d2e87c367a078c4
Author: student <student@uso>
Date:   Wed Oct 9 14:30:23 2019 +0300

    added tom.txt file

commit a67a3fb85fea113b25e412f813382c4cbaf196f0
Author: student <student@uso>
Date:   Wed Oct 9 14:26:38 2019 +0300

    added main file

commit 64bccb03c2f3a8e76730582676bd944902a0c1eb
Author: student <student@uso>
Date:   Mon Oct 7 15:49:20 2019 +0300

    Initial commit
uso/laboratoare/new/04-appdev/need-to-know.txt · Last modified: 2019/10/24 11:23 by adrian.zatreanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0