Diferență între revizuiri ale paginii „C++ POO Lab Lucrarea 1”

De la WikiLabs
Jump to navigationJump to search
(Intro CLion)
 
(Nu s-au afișat 26 de versiuni intermediare efectuate de alți 2 utilizatori)
Linia 1: Linia 1:
În acest laborator ne vom reaminti sintaxa limbajului C, vom exersa utilizarea mediului de dezvoltare CLion și a ''debugger''-ului.
+
= Obiective : recapitulare structuri si pointeri =
  
== Tool-ul de depanare GDB ==
+
La sfârșitul acestei recapitulări studenții vor fi capabili:
Depanatorul GNU, cunoscut drept GDB (GNU debugger), este depanatorul standard pentru sistemul de software GNU.<br>
+
* să definească tipuri de date de tip struct;
Scopul unui depanator precum GDB este de a permite utilizatorului să vadă ce se întâmplă în interiorul unui alt program în timp ce acesta se execută (pentru a determina motivul rezultatelor necorespunzătoare) sau ce s-a întâmplat cu programul în momentul în care a fost întreruptă execuţia în mod neaşteptat.<br><br>
+
* să declare și să utilizele variabile de tip struct în programe;
  
GDB este capabil de a îndeplini 4 categorii de operații (și alte operaţii intermediare lor):
+
* să definească și să utilizeze tipuri de date pointer;
* să pornească programul, specificând orice ar putea interveni în buna funcționare a acestuia;
+
* să folosească pointeri pentru a putea modifica variabilele trimise ca argumente unor funcții;
* să determine o pauză in execuţia programului în anumite condiții specificate de utilizator;
+
* să aloce, să folosească și elibereze memorie HEAP, în mod dinamic (in C si C++);
* examineze ce s-a întâmplat în momentul opririi programului;
+
* să utilizeze aritmetica pointerilor pentru a itera peste elemente de la adrese consecutive de memorie;
* să modifice anumite valori în program pentru a putea corecta efectele unui bug și a investiga urmările altuia.<br>
 
Odată pornit, GDB citește comenzi din terminal până la întâlnirea comenzii de ieșire "quit".<br>
 
<div class="regula">'''<font color="red"> Atenție:</font>''' Pentru a putea folosi debugger-ul, la generarea fișierului executabil trebuie folosită opțiunea '''-g''', care adaugă simboluri de debug în executabil, fără de care depanarea nu este posibilă. (vezi [http://wiki.dcae.pub.ro/index.php/PC_Laborator_1 Laboratorul 1])</div>
 
Exemplul unei comenzi de compilare în vederea depanării:<br>
 
  
  '''<span style="color: green">student@pracsis01</span> <span style="color: blue">~/Desktop $</span> gcc -g hello.c -o hello'''
+
= Structura în C =
  
=== Comenzi specifice GDB ===
+
Tipurile de date <code>struct</code> sunt utilizate pentru a agrega mai multe multe varibile care au sens împreună. De exemplu, dorim să stocăm informații despre o mașină, prin urmare avem nevoie să stocăm marca, modelul, anul de fabricație, numărul de înmatriculare, culoarea, etc. Putem în acest caz să definim o structură numită ''Masina'' care să stocheze aceste valori. Variabilele care aparțin unei structuri se numesc '''câmpuri''' ale structurii. Un exemplu:
Cea mai folosită modalitate de a porni tool-ul de depanare este de a scrie '''gdb''' în terminal, urmat de numele executabilului ce se dorește a fi depanat.
 
  
  '''<span style="color: green">student@pracsis01</span> <span style="color: blue">~/Desktop $</span> gdb hello'''
+
<syntaxhighlight lang="c">
 +
struct Masina {
 +
    char marca[100];
 +
    char model[50];
 +
    unsigned short anFabricatie;
 +
    char numarInmatriculare[8];
 +
    char culoare[10];
 +
};
 +
</syntaxhighlight>
 +
 
 +
Câmpurile structurii ''Masina'' sunt: '''marca''', '''model''', '''anFabricatie''', '''numarInmatriculare''' și '''culoare'''.
 +
 
 +
<span style="color: red; font-weight: bold">Atenție</span>: Definiția unei structuri nu implică automat și existența unei variabile de tipul respectiv, așa cum definirea tipului de date <code>int</code> nu implică existența unei varibile de tip <code>int</code>.
 +
 
 +
Declararea unei variabile de tipul ''Masina'' se face exact ca declararea oricărei alte variabile, sub forma: <tip_data> <nume_variabila>, cu observația că tipul de dată va conține și cuvântul cheie <code>struct</code>, deci acesta va fi <code>struct Masina</code>:
 +
 
 +
<syntaxhighlight lang="c">
 +
struct Masina {
 +
    char marca[100];
 +
    char model[50];
 +
    unsigned short anFabricatie;
 +
    char numarInmatriculare[8];
 +
    char culoare[10];
 +
};
 +
 
 +
int main() {
 +
    struct Masina masina;
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
Odată definită o variabilă de tip <code>struct</code>, operatorul folosit pentru a accesa câmpurile structurii este <code>.</code>:
 +
 
 +
<syntaxhighlight lang="c">
 +
#include <stdio.h>
 +
 
 +
struct Masina {
 +
    char marca[100];
 +
    char model[50];
 +
    unsigned short anFabricatie;
 +
    char numarInmatriculare[8];
 +
    char culoare[10];
 +
};
 +
 
 +
int main() {
 +
    struct Masina masina_mea;
 +
    printf("Care este marca masinii? ");
 +
    fgets(masina_mea.marca, 100, stdin);
 +
    printf("Care este modelul masinii? ");
 +
    fgets(masina_mea.model, 50, stdin);
 +
    printf("Care este numarul de inmatriculare al masinii? ");
 +
    fgets(masina_mea.numarInmatriculare, 8, stdin);
 +
    printf("Care este culoarea masinii? ");
 +
    fgets(masina_mea.culoare, 10, stdin);
 +
    printf("Care este anul de fabricatie a masinii? ");
 +
    scanf("%hu", &masina_mea.anFabricatie);
 +
 
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
  <span style="color: blue; font-weight: bold">Observație</span>: O structură poate avea câmpuri de orice tip, inclusiv de tipul altor structuri.
 +
 
 +
Un tip de dată de tip structură poate fi folosit ca orice alt tip de dată, spre exemplu pentru a crea vectori de acel tip, dar și pentru a defini funcții care au argumente sau întorc valori de acel tip:
 +
 
 +
<syntaxhighlight lang="c">
 +
#include <stdio.h>
 +
 
 +
struct Masina {
 +
    char marca[100];
 +
    char model[50];
 +
    unsigned short anFabricatie;
 +
    char numarInmatriculare[8];
 +
    char culoare[10];
 +
};
 +
 
 +
struct Masina citesteMasina() {
 +
    struct Masina masina_mea;
 +
    printf("Care este marca masinii? ");
 +
    fgets(masina_mea.marca, 100, stdin);
 +
    printf("Care este modelul masinii? ");
 +
    fgets(masina_mea.model, 50, stdin);
 +
    printf("Care este numarul de inmatriculare al masinii? ");
 +
    fgets(masina_mea.numarInmatriculare, 8, stdin);
 +
    printf("Care este culoarea masinii? ");
 +
    fgets(masina_mea.culoare, 10, stdin);
 +
    printf("Care este anul de fabricatie a masinii? ");
 +
    scanf("%hu", &masina_mea.anFabricatie);
 +
    return masina_mea;
 +
}
 +
 
 +
void afiseazaMasina(struct Masina masina){
 +
    printf("Masina marca %s si modelul %s are numarul de inmatriculare "
 +
            "%s, culoarea %s si a fost fabricata in anul %hu!\n",
 +
            masina.marca, masina.model, masina.numarInmatriculare,
 +
            masina.culoare, masina.anFabricatie);
 +
}
 +
 
 +
int main() {
 +
    struct Masina parcAuto[10];
 +
    parcAuto[0] = citesteMasina();
 +
 
 +
    afiseazaMasina(parcAuto[0]);
 +
 
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
 
 +
Plecând de la limbajul C, ne amintim că acesta pune la dispoziție o serie de tipuri de date primitive, incluse în standardul limbajului. Ca exemple, avem: ''int'', ''long'', ''char'', ''double'', etc. În plus, există posibilitatea de a crea structuri compuse folosind cuvântul cheie ''[http://en.wikipedia.org/wiki/Struct_(C_programming_language) struct]''. O structură în C este compusă din una sau mai multe variabile care pot fi ori de tip primitiv, ori alte structuri, ori o combinație din cele două.
 +
 
 +
 
 +
În continuare avem un exemplu de definiție a unor structuri în C.
 +
<syntaxhighlight lang="C">
 +
struct mystring{
 +
    char* str;
 +
    unsigned length;
 +
};
 +
 
 +
struct person{
 +
    struct mystring *first_name;
 +
    struct mystring *last_name;
 +
    unsigned age;
 +
    float height;
 +
    float weight;
 +
};
 +
</syntaxhighlight>
  
O parte din comenzile cele mai utilizate ale GDB sunt următoarele (pentru lista completă studiați pagina de manual GDB - man gdb):
+
Se vede că structura ''person'' conține pointeri la două structuri de tip ''mystring''. Relația este descrisă de schema bloc următoare (cu exemple de valori pentru variabilele primitive):
{| class="wikitable"
 
! Opțiune !! Efect
 
|-
 
| <code style="color: green">break</code> [nume_fișier:] nume_funcție <br> <code style="color: green">break</code> [nume_fișier:] număr_linie || Setează un breakpoint (punct de întrerupere) la începutul funcției ''nume_funcție'' sau la linia specificată prin ''număr_linie'' din fișierul specificat prin ''nume_fișier''.
 
|-
 
| <code style="color: green">run</code> [listă_argumente] || Pornește programul în execuție (cu lista de argumente, dacă au fost specificate).
 
|-
 
| <code style="color: green">bt</code> || Backtrace: afișează stiva de program.
 
|-
 
| <code style="color: green">print</code> expresie || Afișează valoarea unei expresii (valoarea stocată într-o variabilă, valoarea returnată de o funcție etc.).
 
|-
 
| <code style="color: green">c</code> || Continuă execuția programului (după ce a fost oprit, spre exemplu după un breakpoint).
 
|-
 
| <code style="color: green">next</code> || Execută următoarea linie de program (după oprire), sărind peste orice apel de funcție care se găsește în această linie.
 
|-
 
| <code style="color: green">list</code> [nume_fișier:] nume_funcție || Afișează liniile de cod ale programului din vecinătatea locului unde este oprit acum.
 
|-
 
| <code style="color: green">step</code> || Execută următoarea linie de program (după oprire), intrând în orice funcție apelată de acea linie.
 
|-
 
| <code style="color: green">help</code> [nume] || Afișează informații despre comanda GDB ''nume'' sau informații generale despre utilizarea GDB.
 
|-
 
| <code style="color: green">quit</code> || Iese din GDB.
 
|}
 
  
<div class="regula"> ''' Observație: ''' Cele mai multe dintre aceste comenzi pot fi apelate doar prin prima literă a numelui lor. </div>
+
<syntaxhighlight lang="text">
Spre exemplu, următoarea comandă:
+
    struct person
   '''(gdb) next'''
+
-------------------------                                          char char ...                    
este identică cu:
+
| struct mystring*    ● |------>       struct mystring            --------------------------------------
  '''(gdb) n'''
+
-------------------------          -------------------------      | G | h | e | o | r | g | h | e | \0 |
<div class="regula"> ''' Observație: ''' Dacă se apasă pe enter fără a scrie o comandă, se repetă ultima comandă specificată.</div>
+
| struct mystring*   ● |---      | char*              ● | ----> --------------------------------------
 +
-------------------------  |      -------------------------
 +
| unsigned          29 |  |      | unsigned          10 |
 +
-------------------------  |      -------------------------
 +
| float            1.7 |  |
 +
-------------------------  |                                      char char ...
 +
| float            68.9 |  ---->     struct mystring              ------------------------------
 +
-------------------------          -------------------------      | V | a | s | i | l | e | \0 |
 +
                                  | char*              ● |---->  ------------------------------
 +
                                  -------------------------
 +
                                  | unsigned          10 |
 +
                                  -------------------------
 +
</syntaxhighlight>
  
=== Exemplu de depanare ===
+
În continuare, vom da un exemplu de utilizare a acestor structuri:
Se dă fișierul sursă ''suma_fact.c'', care conține următoarele instrucțiuni:
 
 
<syntaxhighlight lang="C">
 
<syntaxhighlight lang="C">
 
#include<stdio.h>
 
#include<stdio.h>
 +
#include<stdlib.h>
 +
#include<string.h>
 +
 +
int main(){
 +
    //make a default mystring with no content and length = 30
 +
    struct mystring * some_string = (struct mystring*)malloc(sizeof(struct mystring));
 +
    unsigned default_length = 30;
 +
    some_string->length = default_length;
 +
    some_string->str = (char*)malloc(default_length * sizeof(char));
 +
    strcpy(some_string->str, "Vasile");
 +
 +
    //make a person structure in which all strings refer to the default empty mystring
 +
    struct person* new_person = (struct person*)malloc(sizeof(struct person));
 +
    new_person->first_name = some_string;
 +
    new_person->last_name = some_string;
 +
    new_person->age = 29;
 +
    new_person->height = (float)1.7;
 +
    new_person->weight = 68.9F;
 +
 +
    printf("Persoana se numeste %s %s, are varsta de %d ani, inaltimea %f si greutatea %f\n", new_person->first_name->str,
 +
        new_person->last_name->str,
 +
        new_person->age,
 +
        new_person->height,
 +
        new_person->weight);
  
unsigned int factorial(int n) {
+
     return 0;
     if (n == 0) {
 
        return 1;
 
    }
 
    int i;
 
    unsigned int fact = 0;
 
    for (i = 1; i <= n; i++){
 
        fact *= i;
 
    }
 
    return fact;
 
 
}
 
}
 +
</syntaxhighlight>
 +
 +
În acest exemplu, schema bloc este diferită dintr-un punct de vedere esențial: ambii pointeri de tip mystring sunt referință la aceeași adresă, respectiv la același mystring. Astfel, dacă se modifică ''new_person->first_name'', atunci implicit se modifică și ''new_person->last_name'' (de fapt este aceeași structură):
 +
 +
<syntaxhighlight lang="text">
 +
-------------------------
 +
| struct person        |
 +
-------------------------        -------------------------      char char...            char ...
 +
| struct mystring*    ● |------>  | struct mystring      |      ------------------------------------------------
 +
-------------------------    /    -------------------------      | V | a | s | i | l | e | \0 |    |  ...  |  |
 +
| struct mystring*    ● |---/    | char*              ● |-----> -----------------------------------------------
 +
-------------------------        -------------------------
 +
| unsigned          29 |        | unsigned          30 |
 +
-------------------------        -------------------------
 +
| float            1.7 | 
 +
------------------------- 
 +
| float            68.9 |   
 +
-------------------------
 +
 +
</syntaxhighlight>
 +
 +
Pentru a face un rezumat, structura, in C, este un '''tip de dată''' compusă din tipuri primitive, sau alte structuri. Analog cu orice alt tip de dată, se pot defini variabile de tipul structurii, așa cum se pot defini variabile de tip primitiv. Limitarea fundamentală a structurilor este că acestea nu pot conține decât '''date''', nu și '''funcții'''. În laboratorul următor vom vedea aceasta limitare rezolvată prin noțiunea de '''clasă'''.
 +
 +
= Tipuri de date pointer =
 +
 +
Un '''pointer''' reprezintă o variabilă care stochează o adresă în memoria dedicată aplicației. Tipul variabilei de tip pointer specifică tipul datei care poate fi citit de la adresa respectivă.
 +
 +
O variabilă de tip pointer se definește în felul următor:
 +
 +
<tip_data> * <nume_variabila>;
 +
 +
Spre exemplu:
 +
 +
<syntaxhighlight lang="c">
 +
int * pa;
 +
</syntaxhighlight>
 +
 +
Variabila de tip pointer <code>pa</code> nu memorează un întreg, ci o adresă în memorie, iar de la adresa respectivă se poate citi un întreg. Acest lucru se numește indirectare simplă. În plus, deoarece tipul pointer este în sine un tip de dată, și în definiția unui pointer <tip_data> poate fi un pointer, aceasta permite următoarele construcții:
 +
* <code>int * pa;</code> - variabilă ce stochează o adresă de unde se poate citi un întreg (indirectare simplă);
 +
* <code>int ** pa;</code> - variabilă ce stochează o adresă de unde se poate citi o adresă de unde se poate citi un întreg (dublă indirectare);
 +
* <code>int *** pa;</code> - variabilă ce stochează o adresă de unde se poate citi o adresă de unde se poate citi o adresă de unde se poate citi un întreg (triplă indirectare);
 +
* etc.
 +
 +
<div class="regula"><span style="color: red; font-weight: bold">Regulă:</span> Orice variabilă este stocată în memorie și deci are o adresă în memorie.</div>
 +
 +
În C există un tip de dată pointer care poate memora o adresă fără a ști ce date se află la adresa respectivă. Acest tip de pointer este <code>void*</code>.
 +
 +
== Dimensiunea tipului de date pointer ==
 +
 +
Conform regulii de mai sus, și variabilele de tip pointer ocupă loc în memorie, deci au dimensiune, în octeți. Un pointer nu oferă informații legate de dimensiunea ocupată de datele de la adresa respectivă, ci doar adresa de unde începe zona ocupată. Din moment ce un pointer memorează doar o adresă, dimensiunea variabilelor de tip pointer nu depinde decât de dimensiunea spațiului de memorie. Astfel, pentru procesoare și sisteme de operare pe 32 de biți, o variabilă de tip pointer va avea 32 de biți, iar pe procesoare și sisteme de operare pe 64 de biți, o variabilă de tip pointer va avea 64 de biți.
 +
 +
== Adresa unei variabile ==
 +
 +
Operatorul care permite aflarea adresei unde este stocată o variabilă este ampersand (&). Acesta este un operator unar ce se plasează înaintea unei variabile iar rezultatul evaluării sale este adresa unde este stocată variabila respectivă. Această adresă poate fi stocată într-o altă variabilă de tipul corespunzător. Altfel spus, pentru o variabilă de tip ''tip_data'', adresa acesteia se poate stoca într-o variabilă de tip ''tip_data *'':
 +
<syntaxhighlight lang="c">
 +
float floatValue;
 +
float * floatAddress = &floatValue;
 +
 +
int intValue;
 +
int * intAddress = &intValue;
 +
int ** intPointerAddress = &intAddress;
 +
</syntaxhighlight>
  
 +
== Adresa NULL ==
 +
 +
Pentru orice aplicație, adresa 0 din spațiul ei de memorie este rezervată. Aceasta nu poate fi nici scrisă și nici citită. Această adresa poartă numele de '''NULL'''. Orice variabilă de tip pointer poate lua valoarea '''NULL''', lucru care de obicei specifică faptul că de fapt variabila pointer nu stochează o adresă validă.
 +
 +
Constanta '''NULL''' este definită în fișierul header <code style="color: green">stdlib.h</code> (Standard Library).
 +
 +
<syntaxhighlight lang="C">
 +
#include <stdlib.h>
  
 
int main() {
 
int main() {
     int n, i;
+
     char * charPointer = NULL;
     do {
+
     return 0;
        printf("Introduceti un numar natural mai mic sau egal cu 8: ");
+
}
        scanf("%d", &n);
+
</syntaxhighlight>
     } while (n < 0 || n > 8);
+
 
     unsigned int suma = 0;
+
== Valoarea de la o adresă ==
     for (i = 0; i <= n; i++) {
+
 
        suma += factorial(i);
+
Având o variabilă de tip pointer, valoarea stocată în memorie la adresa respectivă se poate afla folosind caracterul steluță (*), numit și operator de indirectare. Acesta este un operator unar ce se plasează înaintea unei variabile de tip pointer iar rezultatul evaluării sale este valoarea din memorie de la adresa stocată în variabila respectivă. Această valoare poate fi stocată într-o altă variabilă de tipul corespunzător. Altfel spus, pentru o variabilă pointer de tip ''tip_data *'', valoarea de la adresa stocată de pointerul respectiv se poate memora într-o altă variabilă de tip ''tip_data'':
     }
+
 
     printf("%u\n", suma);
+
<syntaxhighlight lang="c">
 +
float floatValue;
 +
float * floatAddress = &floatValue;
 +
float anotherFloatValue = *floatAddress;
 +
 
 +
int intValue;
 +
int * intAddress = &intValue;
 +
int ** intPointerAddress = &intAddress;
 +
int * anotherintAddress = *intPointerAddress;
 +
</syntaxhighlight>
 +
 
 +
 
 +
<div class="regula"><span style="color: red; font-weight: bold">Atenție:</span> Accesarea valorii de la o adresă se numește dereferențiere. Dereferențierea unei adrese care nu face parte din spațiul de memorie al aplicației sau dereferențierea lui '''NULL''' va avea ca efect oprirea imediată a programului printr-un ''Segmentation fault.''</div>
 +
 
 +
<div class="regula"><span style="color: red; font-weight: bold">Atenție:</span> Nu confundați steluța utilizată la definirea unui pointer cu operatorul de indirectare/ dereferențiere. Aceștia sunt la fel de diferiți cum este și operatorul de inmulțire față de cel de indirectare.</div>
 +
 
 +
= Utilitatea variabilelor de tip pointer =
 +
 
 +
Pointerii în C au două roluri foarte importante:
 +
# Alocarea dinamică de memorie în HEAP
 +
# Modificarea variabilelor parametri ale unor funcții astfel încât modificarea să se păstreze în afara funcției
 +
 
 +
== Alocarea dinamică de memorie ==
 +
 
 +
Memoria alocată unei aplicații de către sistemul de operare este împărțită în mai multe secțiuni, dintre care importante pentru stocarea de date sunt:
 +
 
 +
* Segmentele BSS și Data - reprezintă memoria alocată pentru variabilele statice (globale), care există de la începutul până la încheierea programului, fără posibilitate de eliberare;
 +
* Stiva (Stack) - zona de memorie în care se alocă contextele funcțiilor apelate în timpul execuției programului și în care se alocă argumentele și variabilele locale are funcțiilor; această zonă este alocată la intrarea în funcție și este eliberată la ieșirea din funcție;
 +
* HEAP - zonă de memorie în care se pot aloca dinamic, de către programator, blocuri de memorie ce pot fi folosite în program până la eliberarea acestora de către programator.
 +
 
 +
Alocarea și dezalocarea memoriei în heap se fac folosind următoarele funcții:
 +
* <code>void * malloc(unsigned size)</code> - alocă ''size'' octeți într-o zonă continuă din HEAP și întoarce adresa de memorie unde începe zona respectivă; dacă alocarea eșuează (nu exită suficientă memorie într-o zonă continuă în HEAP), funcția va întoarce '''NULL'''; alocarea nu șterge conținutul memoriei respective;
 +
* <code>void * calloc(unsigned elements, unsigned elementSize)</code> - alocă ''elements'' elemente de ''elementSize'' octeți fiecare într-o zonă continuă din HEAP și întoarce adresa de memorie unde începe zona respectivă; dacă alocarea eșuează (nu exită suficientă memorie într-o zonă continuă în HEAP), funcția va întoarce '''NULL'''; alocarea șterge tot conținutul memoriei respective, scriind 0 la fiecare locație;
 +
* <code>void free (void *)</code> - dezalocă o zonă de memorie alocată în prealabil cu <code>malloc</code> sau <code>calloc</code>; apelul succesiv de două sau mai multe ori a funcției <code>free</code> pentru aceeași zonă de memorie sau apelul ei pentru o adresă care nu a fost alocată în prealabil va avea ca efect oprirea imediată a programului cu eroare (''double free or corruption'').
 +
 
 +
Toate aceste funcții sunt definite în fișierul header <code style="color: green">stdlib.h</code>.
 +
 
 +
<syntaxhighlight lang="c">
 +
#include <stdlib.h>
 +
#include <stdio.h>
 +
 
 +
int main(){
 +
    int * intPointer;
 +
    short * shortPointer;
 +
 
 +
    intPointer = (int*) malloc(sizeof(int));
 +
    shortPointer = (short*) calloc(1, sizeof(short));
 +
 
 +
    printf("Valoarea din zona alocata pentru int este: %d\n", *intPointer);
 +
    printf("Valoarea din zona alocata pentru short este: %hd\n", *shortPointer);
 +
 
 +
    free(intPointer);
 +
    free(shortPointer);
 +
 
 +
     return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
<div class="sfat"><span style="color: red; font-weight: bold">Atenție:</span> Deoarece funcțiile <code>malloc</code> și <code>calloc</code> au doar rolul de a aloca memorie, fără să știe care este scopul utilizării acestei memorii, ele întorc un pointer de tip <code>void *</code>. Astfel, pentru a putea stoca adresa într-un alt tip de pointer (de exemplu <code>int *</code>), ea trebuie convertită la tipul de date corect. Aceasta este explicația prezenței operatorului de cast din fața apelului funcțiilor <code>malloc</code> și <code>calloc</code> din codul de mai sus.</div>
 +
 
 +
== Aritmetica pointerilor ==
 +
 
 +
Când se alocă memorie in HEAP, rareori se alocă pentru un singur element, de cele mai multe ori se alocă pentru un număr mare de elemente de același fel. Ca exemplu, dacă vrem să stocăm o imagine High Definition, ne trebuie o zonă de memorie care să poată memora informație de culoare pentru 1920 * 1080 de pixeli, fiecare pixel având informație de culoare pentru roșu, verde și albastru (RGB). Fiecare din aceste componente de culoare se stochează pe un octet ca valoare întreagă fără semn (unsigned char). Prin urmare, pentru un frame se vor aloca 1920 * 1080 * 3 octeți = 6220800, aproape 6 MB. Această memorie se alocă întotdeauna într-o zonă continuă de către funcțiile <code>malloc</code> și <code>calloc</code>:
 +
 
 +
<syntaxhighlight lang="c">
 +
#include <stdlib.h>
 +
int main(){
 +
    unsigned char * frame;
 +
     frame = (unsigned char*) malloc(1920 * 1080 * 3 * sizeof(unsigned char));
 +
     //... use frame
 +
 
 +
    free (frame);
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
Deoarece pointer-ul nu memorează decât adresa de început a zonei de memorie, există posibilitatea de a modifica adresa pentru a accesa elementele ulterioare. În acest scop, variabilele de tip pointer suportă doar operații aritmetice de adunare sau scădere, nu și de înmulțire sau împărțire.
 +
 
 +
<div class="regula"><span style="color: red; font-weight: bold">Atenție:</span> Incrementarea unui pointer nu crește adresa cu 1, ci cu dimensiunea tipului de dată al pointer-ului. Deci pentru un <code>int * p;</code>, linia <code>p++;</code> va incrementa adresa cu 4 (sizeof(int)):</div>
 +
 
 +
<syntaxhighlight lang="c">
 +
#include <stdio.h>
 +
#include <stdlib.h>
 +
 
 +
int main(){
 +
    int * pointer;
 +
    pointer = (int*) malloc(10 * sizeof(int));
 +
    printf("The pointer address is: %p\n", pointer);
 +
    printf("The pointer address + 1 is: %p\n", pointer + 1);
 +
    free (pointer);
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
<div class="regula"><span style="color: red; font-weight: bold">Atenție </span> ca prin operații pe pointeri să nu depășiți zona de memorie alocată.</div>
 +
 
 +
== Pointerii și vectorii ==
 +
 
 +
Memoria alocată pentru mai multe elemente de același fel reprezintă de fapt un vector de elemente de acel tip. Astfel aflăm că de fapt vectorii și pointerii, în multe situații se pot folosi interschimbabil.
 +
 
 +
Când se definește un vector, numele vectorului reprezintă un pointer la adresa de unde începe zona lui de memorie, adică adresa unde este memorat elementul de la indexul 0:
 +
 
 +
<syntaxhighlight lang="c">
 +
#include <stdio.h>
 +
#include <stdlib.h>
 +
 
 +
int main(){
 +
    int array[10];
 +
     array[0] = 13;
 +
    printf("The array pointer address is: %p\n", array);
 +
     printf("The value at address %p is %d\n", array, *array);
 
     return 0;
 
     return 0;
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Programul își propune să calculeze suma factorialelor de la 0 la n, unde n este introdus de la tastatură. Dacă rulăm programul vom obține ca rezultat numărul '''1''', indiferent de ce a fost introdus de la tastatură. Ne propunem să descoperim bug-ul folosind GDB.<br>
 
Compilăm sursa, adăugând simboluri de debug în executabil, după care lansăm în execuție debugger-ul:
 
  
  '''<span style="color: green">student@pracsis01</span> <span style="color: blue">~/Desktop $</span> gcc -g suma_fact.c -o suma_fact'''
+
Dereferențierea unui pointer indexat cu o valoare este echivalentă cu folosirea operatorului de acces la vector: <code>*(v + k)</code> == <code>v[k]</code>, unde '''v''' este un pointer (sau vector) iar '''k''' este un întreg.
  '''<span style="color: green">student@pracsis01</span> <span style="color: blue">~/Desktop $</span> gdb suma_fact'''
 
  
<div class="regula">'''<font color="red"> Atenție:</font>''' Comanda de mai sus lansează în execuție numai debugger-ul, nu și programul ''suma_fact''.</div>
+
<syntaxhighlight lang="c" highlight="14,18">
<div class="regula"> ''' Observație: ''' Putem observa că am pornit debugger-ul prin faptul că fiecare rând nou din linia de comandă începe acum astfel:</div>
+
#include <stdlib.h>
  '''(gdb)'''
+
#include <stdio.h>
Punem un ''breakpoint'' la linia 20, unde se citește valoarea lui n:
+
int main(){
  '''(gdb) b 20'''
+
    int arraySize;
  '''Breakpoint 1 at 0x80484c2: file suma_fact.c, line 20.'''
+
    printf("size = ");
  '''(gdb) '''
+
    scanf("%d", &arraySize);
  
Pornim programul în interiorul debugger-ului:
+
    int * heapArray;
  '''(gdb) run'''
+
     heapArray = (int*) malloc(arraySize * sizeof(int));
  '''Starting program: /home/student/Desktop/suma_fact'''
 
  ''' '''
 
  '''Breakpoint 1, main () at suma_fact.c:20'''
 
  '''20          scanf("%d", &n);'''
 
  '''(gdb) '''
 
Trecem mai departe și introducem o valoare pentru n:
 
  '''(gdb) next'''
 
  '''Introduceti un numar natural mai mic sau egal cu 8: 6 '''
 
  '''21      }while(n<0 || n>8);'''
 
  '''(gdb) '''
 
Vom parcurge programul pas cu pas până la apelarea funcției ''factorial'':
 
  '''(gdb) n'''
 
  '''22      unsigned int suma = 0;'''
 
  '''(gdb) n'''
 
  '''23      for(i=0; i<=n; i++){'''
 
  '''(gdb) n'''
 
  '''24          suma += factorial(i);'''
 
  '''(gdb) s'''
 
  '''factorial (n=0) at suma_fact.c:4'''
 
  '''4     if(n==0){'''
 
  '''(gdb) s'''
 
  '''5        return 1;'''
 
  '''(gdb) s'''
 
  '''13  }'''
 
  '''(gdb) s'''
 
  '''main () at suma_fact.c:23'''
 
  '''23      for(i=0; i<=n; i++){'''
 
  '''(gdb) s'''
 
  '''24          suma += factorial(i);'''
 
  '''(gdb) s'''
 
  '''factorial (n=1) at suma_fact.c:4'''
 
  '''4    if(n==0){'''
 
  '''(gdb) s'''
 
  '''8    unsigned int fact=0;'''
 
  '''(gdb) s'''
 
  '''9    for(i=1; i<=n; i++){'''
 
  '''(gdb) s'''
 
  '''10          fact *= i;'''
 
  '''(gdb) s'''
 
  '''9    for(i=1; i<=n; i++){'''
 
  '''(gdb) s'''
 
  '''12      return fact;'''
 
  '''(gdb) print fact'''
 
  '''$1 = 0'''
 
  '''(gdb) '''
 
Observăm că valoarea returnată de funcția ''factorial'' pentru orice număr nenul este '''0'''. Acest lucru se datorează inițializării variabilei ''fact'' cu valoarea '''0''' la începutul funcției ''factorial''. Putem ieși din debugger pentru a modifica linia 8 din fișierul sursă suma_fact.c:
 
  '''(gdb) q'''
 
  '''A debugging session is active.'''
 
  ''' '''
 
  '''    Inferior 1 [process 4279] will be killed.'''
 
  ''' '''
 
  '''Quit anyway? (y or n) y '''
 
  '''<span style="color: green">student@pracsis01</span> <span style="color: blue">~/Desktop $</span>'''
 
<div class="regula"> ''' Observație: ''' Pentru a putea intra în interiorul funcției ''factorial'' este absolut necesar să se folosească comanda '''step''' și nu comanda '''next''' care ar trece peste acea linie 24 fără a intra în corpul funcției ''factorial''.</div>
 
  
După acest laborator veți putea folosi IDE-ul CLion pentru a scrie și depana programe în C. Totodată veți relua și recapitula noțiunile legate de pointeri în C.
+
    int i;
 +
    for(i = 0; i < arraySize; i++) {
 +
        printf("heapArray[%d] = ", i);
 +
        scanf("%d", &heapArray[i]);
 +
    }
  
= Utilizarea IDE-ului CLion =
+
    for(i = 0; i < arraySize; i++) {
 +
        printf("heapArray[%d] = %d\n", i, *(heapArray + i));
 +
    }
  
[https://www.jetbrains.com/clion CLion] este un mediu integrat de dezvoltare (IDE) care permite dezvoltarea de programe în limbajele C și C++. CLion nu instalează și compilator pentru C/C++, acesta trebuie instalat manual, în prealabil.
 
  
<div class="regula"><span style="color: red; font-weight: bold">Atenție:</span> Imaginea de Linux folosită la Programarea Calculatoarelor are deja instalat compilatorul de C.</div>
+
    free (heapArray);
 +
    return 0;
 +
}
 +
</syntaxhighlight>
  
== Instalarea GCC ==
+
== Pointerii și structurile ==
  
=== Instalarea GCC în Linux ===
+
Ca orice tip de dată, și variabilele de tip structură ocupă loc în memorie și deci pot exista pointeri de tip structură:
  
Pentru instalarea compilatorului de C (GCC) în distribuțiile de Linux provenite din Ubuntu (Ubuntu, Kubuntu, Xubuntu, Mint, LMDE, etc.) se poate folosi comanda:
+
<syntaxhighlight lang="c">
 +
struct Test {
 +
    int intField;
 +
    float floatField;
 +
    char charField;
 +
    char charArrayField[100];
 +
};
  
apt-get install -y build-essential
+
int main() {
 +
    struct Test testStructVar;
 +
    struct Test * structPointer = &testStructVar;
 +
    return 0;
 +
}
 +
</syntaxhighlight>
  
=== Instalarea GCC în Windows ===
+
Având un pointer la o structură, accesul la câmpuri se poate face în două moduri:
 +
<ol>
 +
<li>Dereferențierea pointerului pentru a obține "valoarea" structurii și apoi utilizarea operatorului "." pentru accesul la membri:
 +
<syntaxhighlight lang="c" highlight="12">
 +
struct Test {
 +
    int intField;
 +
    float floatField;
 +
    char charField;
 +
    char charArrayField[100];
 +
};
  
Pentru compilarea de programe cu GCC în Windows, aveți nevoie de instalarea acestui compilator care poate fi făcută prin instalarea unuia din următoarele două suite de programe:
+
int main() {
* [https://cygwin.com Cygwin] - Tutorial de instalare [https://cygwin.com/install.html aici].
+
    struct Test testStructVar;
* [http://www.mingw.org/ MinGW] - Tutorial de instalare [http://www.mingw.org/wiki/InstallationHOWTOforMinGW aici].
+
    struct Test * structPointer = &testStructVar;
  
<div class="regula"><span style="color: red; font-weight: bold">Atenție:</span> Nu uitați ca la instalare să bifați în lista de pachete și compilatorul de C (<code>gcc</code>), <code>make</code> și <code>git</code>.</div>
+
    *structPointer.intField = 10;
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
</li>
 +
<li>Accesul la membrii unei structuri accesate printr-un pointer este atât de popular și des întâlnit în C încât există un operator special care permite accesul la câmpuri fără dereferențiere. Acest operator este săgeata "->":
 +
<syntaxhighlight lang="c" highlight="13,14">
 +
struct Test {
 +
    int intField;
 +
    float floatField;
 +
    char charField;
 +
    char charArrayField[100];
 +
};
  
== Instalarea CLion ==
+
int main() {
 +
    struct Test testStructVar;
 +
    struct Test * structPointer = &testStructVar;
  
De la adresa https://www.jetbrains.com/clion/download/#section=linux puteți descărca kitul de instalare pentru sistemul vostru de operare (imaginea de Virtualbox cu Linux folosită la Programarea Calculatoarelor ruelază un Linux pe 32 de biți).
+
    *structPointer.intField = 10;
 +
    structPointer->floatField = 4.3;
 +
    structPointer->charField = 'B';
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
</li>
 +
</ol>
  
Aveți [https://blog.jetbrains.com/clion/2015/05/debug-clion/ aici] un tutorial pentru modalitatea de utilizarea a IDE-ului pentru depanarea codului.
+
Alocarea memoriei în HEAP pentru structuri se face identic cu alocarea pentru tipurile primitive de date:
 +
<syntaxhighlight lang="c" highlight="10, 14">
 +
#include <stdlib.h>
 +
struct Test {
 +
    int intField;
 +
    float floatField;
 +
    char charField;
 +
    char charArrayField[100];
 +
};
  
Nu uitați de regulile următoare: [[Convenții de cod - C]]
+
int main() {
 +
    struct Test * structPointer = (struct Test*) malloc(sizeof(struct Test));
  
== Realizarea unui proiect ==
+
    // use structPointer
  
Odată pornit, CLion oferă posibilitatea de a deschide un proiect existent, sau de a crea unul nou. Vrem să realizăm un proiect nou, aşadar se va alege '''New Project'''.
+
    free(structPointer);
 +
    return 0;
 +
}
 +
</syntaxhighlight>
  
<div class="regula"> ''' Observație: ''' Se poate realiza un nou proiect atunci când altul este deja deschis folosind meniul <code>File - New Project </code> .</div>
+
== Adrese ca argumente de funcție ==
  
[[Fișier:Welcome_to_clion.png | 600px]]
+
Știm deja că în C argumentele funcțiilor sunt pass-by-value, asta înseamnă că funcției i se transmite valoarea dintr-o variabilă, nu variabila în sine. Astfel, dacă o variabilă este folosită ca argument la apelul unei funcții, modificarea argumentului în funcție nu se propagă și spre variabila sursă:
  
<br> Mai departe se va selecta categoria '''C++ Executable''' şi se va introduce locaţia proiectului.
+
<syntaxhighlight lang="c">
 +
#include <stdio.h>
  
<div class="regula"><span style="color: red; font-weight: bold">Atenție:</span> În imagine se observă că locaţia introdusă este '''/home/student/projects/NewProject'''. Se recomandă ca directorul în care se realizează proiectul să aibă numele proiectului, sau un nume sugestiv (în imagine '''NewProject''')</div>
+
void divideByTwo(int arg) {
 +
    printf("Argument before division = %d\n", arg);
 +
    arg /= 2;
 +
    printf("Argument after division = %d\n", arg);
 +
}
  
[[Fișier:New_project_location_clion.png | 600px]]
+
int main() {
 +
    int var = 10;
 +
    divideByTwo(var);
 +
    printf("Variabile after function call = %d\n", var);
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 +
 
 +
Există o soluție pentru a permite propagarea modificării spre variabila sursă, și anume în loc de a trimite ca argument funcției variabila, se transmite adresa variabilei. Asta permite funcției să scrie direct în memoria alocată pentru variabilă și deci să-i modifice valoarea:
 +
<syntaxhighlight lang="c">
 +
#include <stdio.h>
 +
 
 +
void divideByTwo(int *arg) {
 +
    printf("Argument before division = %d\n", *arg);
 +
    *arg /= 2;
 +
    printf("Argument after division = %d\n", *arg);
 +
}
 +
 
 +
int main() {
 +
    int var = 10;
 +
    divideByTwo(&var);
 +
    printf("Variabile after function call = %d\n", var);
 +
    return 0;
 +
}
 +
</syntaxhighlight>
  
== Componentele IDE-ului CLion ==
 
  
[[Fișier:CLion_env.png | 1000px]]
+
= Exerciții =
  
La crearea unui nou proiect, IDE-ul CLion generează automat fişierul '''main.cpp'''. Acesta conţine un exemplu program ce va fi rescris de către utilizator. <br>
+
<ol>
 +
<li>Definiți o structură numită '''Dog''' ce trebuie să conțină următoarele informații:
 +
<ul>
 +
<li> nume </li>
 +
<li> vârstă </li>
 +
<li> culoare </li>
 +
<li> rasă </li>
 +
</ul>
 +
Realizați apoi o funcție care să citească date de la tastatură și să întoarcă o variabilă de tip <code>struct Dog</code> și o altă funcție care să ia ca argument un <code>struct Dog</code> și să afișeze informațiile pe ecran. Rasa se va citi ca o structură mystring. Nu uitați că după un apel de '''scanf''', în stream-ul standard de intrare va rămâne întotdeauna un caracter newline ('\n'). Dacă după un apel de '''scanf''' doriți să citiți un șir de caractere cu '''fgets''', va trebui ca înainte de acest apel să apelați o dată '''getchar()''' care va citi și elimina din stream caracterul ('\n').
 +
</li>
 +
<li>Definiți o variabilă globală, de tip vector de <code>struct Dog</code> numită '''gCrazyDogLady''', de 100 câini. În funcția '''main''', citiți apoi un număr de câini '''n''' și apoi citiți informații legate de respectivii câini, folosind funcția de mai sus, și apoi stocând informațiile în vector. Dacă jumătate dintre câini au culoarea "black" si jumătate "white", atunci se va afișa "The crazy dog lady is a Cruella wannabe!"</li>
  
Secţiunile marcate în imagine reprezintă:
+
<li> În funcția '''main''' apoi, creați un pointer de tip corespunzator spre începutul vectorului '''gCrazyDogLady''', numit '''pTowardsADog'''. Iterați prin acest vector, folosind aritmetica pointerilor pe variabila '''pTowardsADog''' (incrementând-o de n ori) : </li>
# Zona <span style="color: green">verde</span> - '''Project view''' indică toate fişierele şi directoarele ce alcătuiesc proiectul.
+
<ul>
# Zona <span style="color: blue">albastră</span> - '''Editor''' este fereastra de vizualizare şi editare a textului
+
<li> pentru jumătate dintre câini, dereferențiați acel pointer și afișați informațiile câinilor - câte un câine pe rând (nume, varstă, culoare, rasă) </li>
# Zona <span style="color: red">roşie</span> - '''Toolbar''' oferă acces rapid pentru operaţiile uzuale. Dintre acestea, cel mai des vom folosi:
+
<li> pentru jumătate dintre câini, fără să dereferențiați acel pointer și afișați informațiile câinilor - câte un câine pe rând (nume, varstă, culoare, rasă) </li>
#*'''Build''' [[Fișier:CLion_build_button.png | 16px]]
+
</ul>
#*'''Run''' [[Fișier:CLion_run_button.png | 16px]]
+
<li> Folosind operatorul '''sizeof''' afisați numarul de bytes (și in paranteză rotundă, numărul de biți) pentru fiecare dintre tipurile de date: char, int, long, float, struct Dog. Ce observați? </li>
#*'''Debug''' [[Fișier:CLion_debug_button.png | 16px]]
+
<li> Folosind operatorul '''sizeof''' afisați numarul de bytes (și in paranteză rotundă, numărul de biți) pentru fiecare dintre tipurile de date: char*, int*, long*, float*, struct Dog*, void*. Ce observați? </li>
# Zona <span style="color: gold">galbenă</span> - '''Run''' este zona în care se introduc datele de intrare şi în care vor fi afişate datele de ieşire.
+
</ol>

Versiunea curentă din 20 februarie 2026 09:17

Obiective : recapitulare structuri si pointeri

La sfârșitul acestei recapitulări studenții vor fi capabili:

  • să definească tipuri de date de tip struct;
  • să declare și să utilizele variabile de tip struct în programe;
  • să definească și să utilizeze tipuri de date pointer;
  • să folosească pointeri pentru a putea modifica variabilele trimise ca argumente unor funcții;
  • să aloce, să folosească și să elibereze memorie HEAP, în mod dinamic (in C si C++);
  • să utilizeze aritmetica pointerilor pentru a itera peste elemente de la adrese consecutive de memorie;

Structura în C

Tipurile de date struct sunt utilizate pentru a agrega mai multe multe varibile care au sens împreună. De exemplu, dorim să stocăm informații despre o mașină, prin urmare avem nevoie să stocăm marca, modelul, anul de fabricație, numărul de înmatriculare, culoarea, etc. Putem în acest caz să definim o structură numită Masina care să stocheze aceste valori. Variabilele care aparțin unei structuri se numesc câmpuri ale structurii. Un exemplu:

struct Masina {
    char marca[100];
    char model[50];
    unsigned short anFabricatie;
    char numarInmatriculare[8];
    char culoare[10];
};

Câmpurile structurii Masina sunt: marca, model, anFabricatie, numarInmatriculare și culoare.

Atenție: Definiția unei structuri nu implică automat și existența unei variabile de tipul respectiv, așa cum definirea tipului de date int nu implică existența unei varibile de tip int.

Declararea unei variabile de tipul Masina se face exact ca declararea oricărei alte variabile, sub forma: <tip_data> <nume_variabila>, cu observația că tipul de dată va conține și cuvântul cheie struct, deci acesta va fi struct Masina:

struct Masina {
    char marca[100];
    char model[50];
    unsigned short anFabricatie;
    char numarInmatriculare[8];
    char culoare[10];
};

int main() {
    struct Masina masina;
    return 0;
}

Odată definită o variabilă de tip struct, operatorul folosit pentru a accesa câmpurile structurii este .:

#include <stdio.h>

struct Masina {
    char marca[100];
    char model[50];
    unsigned short anFabricatie;
    char numarInmatriculare[8];
    char culoare[10];
};

int main() {
    struct Masina masina_mea;
    printf("Care este marca masinii? ");
    fgets(masina_mea.marca, 100, stdin);
    printf("Care este modelul masinii? ");
    fgets(masina_mea.model, 50, stdin);
    printf("Care este numarul de inmatriculare al masinii? ");
    fgets(masina_mea.numarInmatriculare, 8, stdin);
    printf("Care este culoarea masinii? ");
    fgets(masina_mea.culoare, 10, stdin);
    printf("Care este anul de fabricatie a masinii? ");
    scanf("%hu", &masina_mea.anFabricatie);

    return 0;
}
 Observație: O structură poate avea câmpuri de orice tip, inclusiv de tipul altor structuri.

Un tip de dată de tip structură poate fi folosit ca orice alt tip de dată, spre exemplu pentru a crea vectori de acel tip, dar și pentru a defini funcții care au argumente sau întorc valori de acel tip:

#include <stdio.h>

struct Masina {
    char marca[100];
    char model[50];
    unsigned short anFabricatie;
    char numarInmatriculare[8];
    char culoare[10];
};

struct Masina citesteMasina() {
    struct Masina masina_mea;
    printf("Care este marca masinii? ");
    fgets(masina_mea.marca, 100, stdin);
    printf("Care este modelul masinii? ");
    fgets(masina_mea.model, 50, stdin);
    printf("Care este numarul de inmatriculare al masinii? ");
    fgets(masina_mea.numarInmatriculare, 8, stdin);
    printf("Care este culoarea masinii? ");
    fgets(masina_mea.culoare, 10, stdin);
    printf("Care este anul de fabricatie a masinii? ");
    scanf("%hu", &masina_mea.anFabricatie);
    return masina_mea;
}

void afiseazaMasina(struct Masina masina){
    printf("Masina marca %s si modelul %s are numarul de inmatriculare "
            "%s, culoarea %s si a fost fabricata in anul %hu!\n",
            masina.marca, masina.model, masina.numarInmatriculare,
            masina.culoare, masina.anFabricatie);
}

int main() {
    struct Masina parcAuto[10];
    parcAuto[0] = citesteMasina();

    afiseazaMasina(parcAuto[0]);

    return 0;
}


Plecând de la limbajul C, ne amintim că acesta pune la dispoziție o serie de tipuri de date primitive, incluse în standardul limbajului. Ca exemple, avem: int, long, char, double, etc. În plus, există posibilitatea de a crea structuri compuse folosind cuvântul cheie struct. O structură în C este compusă din una sau mai multe variabile care pot fi ori de tip primitiv, ori alte structuri, ori o combinație din cele două.


În continuare avem un exemplu de definiție a unor structuri în C.

struct mystring{
    char* str;
    unsigned length;
};

struct person{
    struct mystring *first_name;
    struct mystring *last_name;
    unsigned age;
    float height;
    float weight;
};

Se vede că structura person conține pointeri la două structuri de tip mystring. Relația este descrisă de schema bloc următoare (cu exemple de valori pentru variabilele primitive):

     struct person
-------------------------                                          char char ...                      
| struct mystring*    ● |------>       struct mystring             --------------------------------------
-------------------------          -------------------------       | G | h | e | o | r | g | h | e | \0 |
| struct mystring*    ● |---       | char*               ● | ----> --------------------------------------
-------------------------  |       -------------------------
| unsigned           29 |  |       | unsigned           10 |
-------------------------  |       -------------------------
| float             1.7 |  |
-------------------------  |                                       char char ...
| float            68.9 |  ---->      struct mystring              ------------------------------
-------------------------          -------------------------       | V | a | s | i | l | e | \0 |
                                   | char*               ● |---->  ------------------------------
                                   -------------------------
                                   | unsigned           10 |
                                   -------------------------

În continuare, vom da un exemplu de utilizare a acestor structuri:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(){
    //make a default mystring with no content and length = 30
    struct mystring * some_string = (struct mystring*)malloc(sizeof(struct mystring));
    unsigned default_length = 30;
    some_string->length = default_length;
    some_string->str = (char*)malloc(default_length * sizeof(char));
    strcpy(some_string->str, "Vasile");

    //make a person structure in which all strings refer to the default empty mystring
    struct person* new_person = (struct person*)malloc(sizeof(struct person));
    new_person->first_name = some_string;
    new_person->last_name = some_string;
    new_person->age = 29;
    new_person->height = (float)1.7;
    new_person->weight = 68.9F;

    printf("Persoana se numeste %s %s, are varsta de %d ani, inaltimea %f si greutatea %f\n", new_person->first_name->str, 
        new_person->last_name->str,
        new_person->age,
        new_person->height,
        new_person->weight);

    return 0;
}

În acest exemplu, schema bloc este diferită dintr-un punct de vedere esențial: ambii pointeri de tip mystring sunt referință la aceeași adresă, respectiv la același mystring. Astfel, dacă se modifică new_person->first_name, atunci implicit se modifică și new_person->last_name (de fapt este aceeași structură):

-------------------------
| struct person         |
-------------------------         -------------------------       char char...             char ...
| struct mystring*    ● |------>  | struct mystring       |       ------------------------------------------------
-------------------------    /    -------------------------       | V | a | s | i | l | e | \0 |    |  ...   |   |
| struct mystring*    ● |---/     | char*               ● |-----> -----------------------------------------------
-------------------------         -------------------------
| unsigned           29 |         | unsigned           30 |
-------------------------         -------------------------
| float             1.7 |  
-------------------------  
| float            68.9 |    
-------------------------

Pentru a face un rezumat, structura, in C, este un tip de dată compusă din tipuri primitive, sau alte structuri. Analog cu orice alt tip de dată, se pot defini variabile de tipul structurii, așa cum se pot defini variabile de tip primitiv. Limitarea fundamentală a structurilor este că acestea nu pot conține decât date, nu și funcții. În laboratorul următor vom vedea aceasta limitare rezolvată prin noțiunea de clasă.

Tipuri de date pointer

Un pointer reprezintă o variabilă care stochează o adresă în memoria dedicată aplicației. Tipul variabilei de tip pointer specifică tipul datei care poate fi citit de la adresa respectivă.

O variabilă de tip pointer se definește în felul următor:

<tip_data> * <nume_variabila>;

Spre exemplu:

int * pa;

Variabila de tip pointer pa nu memorează un întreg, ci o adresă în memorie, iar de la adresa respectivă se poate citi un întreg. Acest lucru se numește indirectare simplă. În plus, deoarece tipul pointer este în sine un tip de dată, și în definiția unui pointer <tip_data> poate fi un pointer, aceasta permite următoarele construcții:

  • int * pa; - variabilă ce stochează o adresă de unde se poate citi un întreg (indirectare simplă);
  • int ** pa; - variabilă ce stochează o adresă de unde se poate citi o adresă de unde se poate citi un întreg (dublă indirectare);
  • int *** pa; - variabilă ce stochează o adresă de unde se poate citi o adresă de unde se poate citi o adresă de unde se poate citi un întreg (triplă indirectare);
  • etc.
Regulă: Orice variabilă este stocată în memorie și deci are o adresă în memorie.

În C există un tip de dată pointer care poate memora o adresă fără a ști ce date se află la adresa respectivă. Acest tip de pointer este void*.

Dimensiunea tipului de date pointer

Conform regulii de mai sus, și variabilele de tip pointer ocupă loc în memorie, deci au dimensiune, în octeți. Un pointer nu oferă informații legate de dimensiunea ocupată de datele de la adresa respectivă, ci doar adresa de unde începe zona ocupată. Din moment ce un pointer memorează doar o adresă, dimensiunea variabilelor de tip pointer nu depinde decât de dimensiunea spațiului de memorie. Astfel, pentru procesoare și sisteme de operare pe 32 de biți, o variabilă de tip pointer va avea 32 de biți, iar pe procesoare și sisteme de operare pe 64 de biți, o variabilă de tip pointer va avea 64 de biți.

Adresa unei variabile

Operatorul care permite aflarea adresei unde este stocată o variabilă este ampersand (&). Acesta este un operator unar ce se plasează înaintea unei variabile iar rezultatul evaluării sale este adresa unde este stocată variabila respectivă. Această adresă poate fi stocată într-o altă variabilă de tipul corespunzător. Altfel spus, pentru o variabilă de tip tip_data, adresa acesteia se poate stoca într-o variabilă de tip tip_data *:

float floatValue;
float * floatAddress = &floatValue;

int intValue;
int * intAddress = &intValue;
int ** intPointerAddress = &intAddress;

Adresa NULL

Pentru orice aplicație, adresa 0 din spațiul ei de memorie este rezervată. Aceasta nu poate fi nici scrisă și nici citită. Această adresa poartă numele de NULL. Orice variabilă de tip pointer poate lua valoarea NULL, lucru care de obicei specifică faptul că de fapt variabila pointer nu stochează o adresă validă.

Constanta NULL este definită în fișierul header stdlib.h (Standard Library).
#include <stdlib.h>

int main() {
    char * charPointer = NULL;
    return 0;
}

Valoarea de la o adresă

Având o variabilă de tip pointer, valoarea stocată în memorie la adresa respectivă se poate afla folosind caracterul steluță (*), numit și operator de indirectare. Acesta este un operator unar ce se plasează înaintea unei variabile de tip pointer iar rezultatul evaluării sale este valoarea din memorie de la adresa stocată în variabila respectivă. Această valoare poate fi stocată într-o altă variabilă de tipul corespunzător. Altfel spus, pentru o variabilă pointer de tip tip_data *, valoarea de la adresa stocată de pointerul respectiv se poate memora într-o altă variabilă de tip tip_data:

float floatValue;
float * floatAddress = &floatValue;
float anotherFloatValue = *floatAddress;

int intValue;
int * intAddress = &intValue;
int ** intPointerAddress = &intAddress;
int * anotherintAddress = *intPointerAddress;


Atenție: Accesarea valorii de la o adresă se numește dereferențiere. Dereferențierea unei adrese care nu face parte din spațiul de memorie al aplicației sau dereferențierea lui NULL va avea ca efect oprirea imediată a programului printr-un Segmentation fault.
Atenție: Nu confundați steluța utilizată la definirea unui pointer cu operatorul de indirectare/ dereferențiere. Aceștia sunt la fel de diferiți cum este și operatorul de inmulțire față de cel de indirectare.

Utilitatea variabilelor de tip pointer

Pointerii în C au două roluri foarte importante:

  1. Alocarea dinamică de memorie în HEAP
  2. Modificarea variabilelor parametri ale unor funcții astfel încât modificarea să se păstreze în afara funcției

Alocarea dinamică de memorie

Memoria alocată unei aplicații de către sistemul de operare este împărțită în mai multe secțiuni, dintre care importante pentru stocarea de date sunt:

  • Segmentele BSS și Data - reprezintă memoria alocată pentru variabilele statice (globale), care există de la începutul până la încheierea programului, fără posibilitate de eliberare;
  • Stiva (Stack) - zona de memorie în care se alocă contextele funcțiilor apelate în timpul execuției programului și în care se alocă argumentele și variabilele locale are funcțiilor; această zonă este alocată la intrarea în funcție și este eliberată la ieșirea din funcție;
  • HEAP - zonă de memorie în care se pot aloca dinamic, de către programator, blocuri de memorie ce pot fi folosite în program până la eliberarea acestora de către programator.

Alocarea și dezalocarea memoriei în heap se fac folosind următoarele funcții:

  • void * malloc(unsigned size) - alocă size octeți într-o zonă continuă din HEAP și întoarce adresa de memorie unde începe zona respectivă; dacă alocarea eșuează (nu exită suficientă memorie într-o zonă continuă în HEAP), funcția va întoarce NULL; alocarea nu șterge conținutul memoriei respective;
  • void * calloc(unsigned elements, unsigned elementSize) - alocă elements elemente de elementSize octeți fiecare într-o zonă continuă din HEAP și întoarce adresa de memorie unde începe zona respectivă; dacă alocarea eșuează (nu exită suficientă memorie într-o zonă continuă în HEAP), funcția va întoarce NULL; alocarea șterge tot conținutul memoriei respective, scriind 0 la fiecare locație;
  • void free (void *) - dezalocă o zonă de memorie alocată în prealabil cu malloc sau calloc; apelul succesiv de două sau mai multe ori a funcției free pentru aceeași zonă de memorie sau apelul ei pentru o adresă care nu a fost alocată în prealabil va avea ca efect oprirea imediată a programului cu eroare (double free or corruption).

Toate aceste funcții sunt definite în fișierul header stdlib.h.

#include <stdlib.h>
#include <stdio.h>

int main(){
    int * intPointer;
    short * shortPointer;

    intPointer = (int*) malloc(sizeof(int));
    shortPointer = (short*) calloc(1, sizeof(short));

    printf("Valoarea din zona alocata pentru int este: %d\n", *intPointer);
    printf("Valoarea din zona alocata pentru short este: %hd\n", *shortPointer);

    free(intPointer);
    free(shortPointer);

    return 0;
}
Atenție: Deoarece funcțiile malloc și calloc au doar rolul de a aloca memorie, fără să știe care este scopul utilizării acestei memorii, ele întorc un pointer de tip void *. Astfel, pentru a putea stoca adresa într-un alt tip de pointer (de exemplu int *), ea trebuie convertită la tipul de date corect. Aceasta este explicația prezenței operatorului de cast din fața apelului funcțiilor malloc și calloc din codul de mai sus.

Aritmetica pointerilor

Când se alocă memorie in HEAP, rareori se alocă pentru un singur element, de cele mai multe ori se alocă pentru un număr mare de elemente de același fel. Ca exemplu, dacă vrem să stocăm o imagine High Definition, ne trebuie o zonă de memorie care să poată memora informație de culoare pentru 1920 * 1080 de pixeli, fiecare pixel având informație de culoare pentru roșu, verde și albastru (RGB). Fiecare din aceste componente de culoare se stochează pe un octet ca valoare întreagă fără semn (unsigned char). Prin urmare, pentru un frame se vor aloca 1920 * 1080 * 3 octeți = 6220800, aproape 6 MB. Această memorie se alocă întotdeauna într-o zonă continuă de către funcțiile malloc și calloc:

#include <stdlib.h>
int main(){
    unsigned char * frame;
    frame = (unsigned char*) malloc(1920 * 1080 * 3 * sizeof(unsigned char));
    //... use frame

    free (frame);
    return 0;
}

Deoarece pointer-ul nu memorează decât adresa de început a zonei de memorie, există posibilitatea de a modifica adresa pentru a accesa elementele ulterioare. În acest scop, variabilele de tip pointer suportă doar operații aritmetice de adunare sau scădere, nu și de înmulțire sau împărțire.

Atenție: Incrementarea unui pointer nu crește adresa cu 1, ci cu dimensiunea tipului de dată al pointer-ului. Deci pentru un int * p;, linia p++; va incrementa adresa cu 4 (sizeof(int)):
#include <stdio.h>
#include <stdlib.h>

int main(){
    int * pointer;
    pointer = (int*) malloc(10 * sizeof(int));
    printf("The pointer address is: %p\n", pointer);
    printf("The pointer address + 1 is: %p\n", pointer + 1);
    free (pointer);
    return 0;
}
Atenție ca prin operații pe pointeri să nu depășiți zona de memorie alocată.

Pointerii și vectorii

Memoria alocată pentru mai multe elemente de același fel reprezintă de fapt un vector de elemente de acel tip. Astfel aflăm că de fapt vectorii și pointerii, în multe situații se pot folosi interschimbabil.

Când se definește un vector, numele vectorului reprezintă un pointer la adresa de unde începe zona lui de memorie, adică adresa unde este memorat elementul de la indexul 0:
#include <stdio.h>
#include <stdlib.h>

int main(){
    int array[10];
    array[0] = 13;
    printf("The array pointer address is: %p\n", array);
    printf("The value at address %p is %d\n", array, *array);
    return 0;
}
Dereferențierea unui pointer indexat cu o valoare este echivalentă cu folosirea operatorului de acces la vector: *(v + k) == v[k], unde v este un pointer (sau vector) iar k este un întreg.
#include <stdlib.h>
#include <stdio.h>
int main(){
    int arraySize;
    printf("size = ");
    scanf("%d", &arraySize);

    int * heapArray;
    heapArray = (int*) malloc(arraySize * sizeof(int));

    int i;
    for(i = 0; i < arraySize; i++) {
        printf("heapArray[%d] = ", i);
        scanf("%d", &heapArray[i]);
    }

    for(i = 0; i < arraySize; i++) {
        printf("heapArray[%d] = %d\n", i, *(heapArray + i));
    }


    free (heapArray);
    return 0;
}

Pointerii și structurile

Ca orice tip de dată, și variabilele de tip structură ocupă loc în memorie și deci pot exista pointeri de tip structură:

struct Test {
    int intField;
    float floatField;
    char charField;
    char charArrayField[100];
};

int main() {
    struct Test testStructVar;
    struct Test * structPointer = &testStructVar;
    return 0;
}

Având un pointer la o structură, accesul la câmpuri se poate face în două moduri:

  1. Dereferențierea pointerului pentru a obține "valoarea" structurii și apoi utilizarea operatorului "." pentru accesul la membri:
    struct Test {
        int intField;
        float floatField;
        char charField;
        char charArrayField[100];
    };
    
    int main() {
        struct Test testStructVar;
        struct Test * structPointer = &testStructVar;
    
        *structPointer.intField = 10;
        return 0;
    }
    
  2. Accesul la membrii unei structuri accesate printr-un pointer este atât de popular și des întâlnit în C încât există un operator special care permite accesul la câmpuri fără dereferențiere. Acest operator este săgeata "->":
    struct Test {
        int intField;
        float floatField;
        char charField;
        char charArrayField[100];
    };
    
    int main() {
        struct Test testStructVar;
        struct Test * structPointer = &testStructVar;
    
        *structPointer.intField = 10;
        structPointer->floatField = 4.3;
        structPointer->charField = 'B';
        return 0;
    }
    

Alocarea memoriei în HEAP pentru structuri se face identic cu alocarea pentru tipurile primitive de date:

#include <stdlib.h>
struct Test {
    int intField;
    float floatField;
    char charField;
    char charArrayField[100];
};

int main() {
    struct Test * structPointer = (struct Test*) malloc(sizeof(struct Test));

    // use structPointer

    free(structPointer);
    return 0;
}

Adrese ca argumente de funcție

Știm deja că în C argumentele funcțiilor sunt pass-by-value, asta înseamnă că funcției i se transmite valoarea dintr-o variabilă, nu variabila în sine. Astfel, dacă o variabilă este folosită ca argument la apelul unei funcții, modificarea argumentului în funcție nu se propagă și spre variabila sursă:

#include <stdio.h>

void divideByTwo(int arg) {
    printf("Argument before division = %d\n", arg);
    arg /= 2;
    printf("Argument after division = %d\n", arg);
}

int main() {
    int var = 10;
    divideByTwo(var);
    printf("Variabile after function call = %d\n", var);
    return 0;
}

Există o soluție pentru a permite propagarea modificării spre variabila sursă, și anume în loc de a trimite ca argument funcției variabila, se transmite adresa variabilei. Asta permite funcției să scrie direct în memoria alocată pentru variabilă și deci să-i modifice valoarea:

#include <stdio.h>

void divideByTwo(int *arg) {
    printf("Argument before division = %d\n", *arg);
    *arg /= 2;
    printf("Argument after division = %d\n", *arg);
}

int main() {
    int var = 10;
    divideByTwo(&var);
    printf("Variabile after function call = %d\n", var);
    return 0;
}


Exerciții

  1. Definiți o structură numită Dog ce trebuie să conțină următoarele informații:
    • nume
    • vârstă
    • culoare
    • rasă

    Realizați apoi o funcție care să citească date de la tastatură și să întoarcă o variabilă de tip struct Dog și o altă funcție care să ia ca argument un struct Dog și să afișeze informațiile pe ecran. Rasa se va citi ca o structură mystring. Nu uitați că după un apel de scanf, în stream-ul standard de intrare va rămâne întotdeauna un caracter newline ('\n'). Dacă după un apel de scanf doriți să citiți un șir de caractere cu fgets, va trebui ca înainte de acest apel să apelați o dată getchar() care va citi și elimina din stream caracterul ('\n').

  2. Definiți o variabilă globală, de tip vector de struct Dog numită gCrazyDogLady, de 100 câini. În funcția main, citiți apoi un număr de câini n și apoi citiți informații legate de respectivii câini, folosind funcția de mai sus, și apoi stocând informațiile în vector. Dacă jumătate dintre câini au culoarea "black" si jumătate "white", atunci se va afișa "The crazy dog lady is a Cruella wannabe!"
  3. În funcția main apoi, creați un pointer de tip corespunzator spre începutul vectorului gCrazyDogLady, numit pTowardsADog. Iterați prin acest vector, folosind aritmetica pointerilor pe variabila pTowardsADog (incrementând-o de n ori) :
    • pentru jumătate dintre câini, dereferențiați acel pointer și afișați informațiile câinilor - câte un câine pe rând (nume, varstă, culoare, rasă)
    • pentru jumătate dintre câini, fără să dereferențiați acel pointer și afișați informațiile câinilor - câte un câine pe rând (nume, varstă, culoare, rasă)
  4. Folosind operatorul sizeof afisați numarul de bytes (și in paranteză rotundă, numărul de biți) pentru fiecare dintre tipurile de date: char, int, long, float, struct Dog. Ce observați?
  5. Folosind operatorul sizeof afisați numarul de bytes (și in paranteză rotundă, numărul de biți) pentru fiecare dintre tipurile de date: char*, int*, long*, float*, struct Dog*, void*. Ce observați?