This is an old revision of the document!


Laborator 01 - Asemănări C/C++

Autor: Răzvan Cristea

Obiective Specifice

Studentul va fi capabil la finalul acestui laborator să:

  • recunoască asemănările dintre limbajele C și C++
  • scrie un program simplu în limbajul C++
  • utilizeze comenzi elementare de git în terminal
  • creeze un repository pe contul personal de GitHub

Introducere

În acest laborator vom face o scurtă recapitulare a noțiunilor de bază învățate în anul întâi la disciplina Programarea Calculatoarelor și Limbaje de Programare (PCLP) și vom vedea care sunt asemănările în linii mari dintre limbajele C și C++. Noi vom scrie programe în C++ pe întreg parcursul acestui semestru, deoarece vom învăța o nouă paradigmă de programare și anume cea Orientată Obiect (OO).

Scurt Istoric

Limbajul C a fost dezvoltat între anii 1972-1973 de către Dennis Ritchie și a devenit extrem de popular datorită combinației sale unice de eficiență și flexibilitate. C a oferit programatorilor posibilitatea de a scrie cod mai lizibil și mai ușor de întreținut decât codul Assembly, fără a sacrifica performanța la nivel de execuție. Această eficiență a făcut ca limbajul C să fie preferat pentru dezvoltarea de sisteme de operare, compilatoare și alte aplicații care necesită performanțe ridicate, menținându-și popularitatea pe parcursul mai multor decenii, până la începutul anilor 2000. Chiar și în prezent, C continuă să fie un limbaj esențial în programarea hardware și în dezvoltarea sistemelor de operare. Kernel-ul Linux, de exemplu, este scris predominant în C, iar multe alte sisteme de operare moderne își bazează nucleele pe acest limbaj. Aceasta subliniază durabilitatea și relevanța limbajului C, chiar și în contextul avansurilor tehnologice din ultimele decenii. Pentru alte informații despre limbajul C puteți citi mai multe aici.

Limbajul C++ a fost creat de către Bjarne Stroustrup în anii '80, și este considerat o extensie a limbajului C, menită să aducă puterea paradigmei orientate obiect în programarea de sistem. C++ păstrează eficiența și flexibilitatea C-ului, adăugând în același timp suport pentru concepte precum clase, moștenire, polimorfism și încapsulare. Această extensie permite dezvoltarea de aplicații complexe, care sunt mai ușor de întreținut și de extins, datorită structurii sale modulare. C++ a jucat un rol esențial în evoluția programării moderne, fiind folosit pe scară largă în dezvoltarea de software pentru sisteme de operare, aplicații de înaltă performanță, jocuri video, și multe alte domenii critice. Suportul său atât pentru programarea procedurală, cât și pentru cea orientată obiect îl face un limbaj versatil, utilizat și în prezent pentru dezvoltarea de software la toate nivelurile. Puteți citi mai multe despre C++ aici.

Asemănări C/C++

În continuare, vom descoperi asemănările dintre cele două limbaje de programare, ilustrând similaritățile dintre acestea prin exemple de cod concrete.

Declararea și initializarea variabilelor

Modul de declarare și de inițializare a variabilelor în cele 2 limbaje este același, avem aceleași tipuri de date precum: int, float, char, double, unsigned int, long, unsigned long etc.

int main()
{
    int x = 3; // variabila de tip intreg
    float nr = 3.5f; // variabila de tip float
    double y = 10.6; // variabila de tip double
    char c = 'q'; // variabila de tip caracter
    long w = 1000000000; // variabila de tip long
 
    return 0;
}

Operații cu variabile

La fel ca în C, în C++ putem face aceleași operații cu tipurile de date existente precum: adunări, scăderi, înmulțiri, împărțiri etc. Operatorii existenți în C++ sunt identici cu cei din C.

int main()
{
    int a = 8;
    int b = 5;
 
    int suma = a + b;
    int produs = a * b;
    int diferenta = a - b;
 
    float impartire = (float)a / b; // va avea valoarea 1.6
    int rest = a % b; // calculam restul impartirii lui a la b
 
    return 0;
}

Se observă că pe linia unde valoarea variabilei a se împarte la cea a variabilei b este folosit un operator special numit operatorul de cast explicit care are menirea să convertească rezultatul împărțirii într-unul de tip float. Dacă variabila a era declarată ca float atunci nu mai era necesară folosirea operatorului de cast, rezultatul fiind calculat corect cu virgulă.

Instrucțiunile decizionale și cele repetitive

În C++ regăsim toate instrucțiunile de bază ale limbajului C. Ca instrucțiuni decizionale amintim if-else și switch, iar ca instrucțiuni repetitive menționăm buclele for, while și do while

Instrucțiuni decizionale

Forma generală a instrucțiunii if-else

if (conditie1)
{
    // bloc de cod
}
else if (conditie2)
{
    // bloc de cod
}
else if (conditie3)
{
    // bloc de cod
}
...
else
{
    // bloc de cod
}

Să luăm ca exemplu verificarea parității unui număr. Sțim că un număr este par dacă restul împărțirii lui la 2 este egal cu 0.

int main()
{
    int x = 13;
    int estePar = -1; // variabila care are valoarea 0 daca numarul este impar si 1 daca este par
 
    if (x % 2 == 0)
    {
        estePar = 1;
    }
    else
    {
        estePar = 0;
    }
 
    return 0;
}

Instrucțiunea switch poate fi o alternativă mult mai elegantă atunci când vrem să înlocuim o secvență de instrucțiuni de forma if → else if → … → else. Deși pare o instrucțiune “demodată” aceasta este extrem de eficientă și de utilă atunci când ne dorim un cod rapid și ușor de citit.

Forma generală a instrucțiunii swicth

switch (expresie) 
{
case x:
    // bloc de cod
    break;
case y:
    // bloc de cod
    break;
default:
    // bloc de cod
    break;
}

Ca și exemplu de cod încercați să vedeți care va fi valoarea lui n după execuția switch-ului de mai jos.

int main()
{
    int n = 4;
 
    switch (n)
    {
    case 0:
        n++;
        break;
 
    case 1:
        n = n * 5 + 1;
        break;
 
    case 2:
        n--;
        break;
 
    case 3:
        n *= 5;
        break;
 
    case 4:
        n = n - 2 + n++ + n * 2;
        break;
 
    default:
        n = 0;
        break;
    }
 
    return 0;
}
Instrucțiuni repetitive

Instrucțiunea for este prima instrucțiune repetitivă pe care o amintim. Ați folosit-o destul de mult la parcurgerea șirurilor de caractere, a vectorilor cât și a altor structuri de date învățate pe parcursul semestrului trecut la disciplina Proiectarea Algoritmilor.

Forma generală a instrucțiunii for

for (start; conditie de continuare; pas)
{
    // bloc de cod care se execută
}

Pasul indică la ce iterație ne aflăm în bucla for și dacă este satisfăcută condiția de continuare. Putem lua ca exemplu de cod incrementarea cu 5 a valorilor unui vector de numere întregi.

int main()
{
    int vector[10] = { 2, 4, 1, -4, 8, 10, 3, -5 };
    int nrElemente = 8;
 
    for (int index = 0; index < nrElemente; index++)
    {
        vector[index] += 5; // echivalent cu a scrie vector[index] = vector[index] + 5;
    }
 
    return 0;
}

Se poate observa că a fost declarat un vector care are capacitatea de 10 elemente din care am ocupat doar primele 8 poziții. Vom reaminti mai târziu de ce această variantă nu este chiar cea mai potrivită atunci când vom menționa despre alocarea dinamică a memoriei.

Instrucțiunea while oferă funcționalități similare cu cele ale instrucțiunii for, însă diferă prin faptul că utilizează un singur parametru, care reprezintă o expresie logică evaluată la fiecare iterație a buclei. Această expresie servește drept condiție de continuare a buclei, similar cu condiția de continuare din bucla for. Bucla while va continua să se execute atât timp cât această condiție este adevărată.

Forma generală a buclei while

while (expresie)
{
    // bloc de cod care se execută
}

Vom lua drept exemplu de utilizare pe cel de la bucla for pentru a vedea cum putem scrie același program, dar cu bucla while.

int main()
{
    int vector[10] = { 2, 4, 1, -4, 8, 10, 3, -5 };
    int nrElemente = 8;
 
    int index = 0;
 
    while (index != nrElemente)
    {
        vector[index] += 5;
        index++;
    }
 
    return 0;
}

Instrucțiunea do-while este o variantă a buclei while, cu o diferență esențială: în bucla do-while, condiția de verificare este evaluată după ce codul din interiorul buclei a fost executat, nu înainte. Aceasta înseamnă că blocul de cod din bucla do-while va fi executat cel puțin o dată, indiferent de valoare de adevăr a expresiei din clauza while.

Forma generală a instrucțiunii do-while

do 
{
    // bloc de cod care se execută
} while (expresie);

Iar ca și exemplu de cod îl vom rescrie pe cel de la bucla while.

int main()
{
    int vector[10] = { 2, 4, 1, -4, 8, 10, 3, -5 };
    int nrElemente = 8;
 
    int index = 0;
 
    do
    {
        vector[index] += 5;
        index++;
    } while (index != nrElemente);
 
 
    return 0;
}

Puteți folosi oricare din cele 3 instrucțiuni repetitive, important este ca efectul codului scris de voi să fie cel dorit și să fiți atenți la cum puneți condițiile.

Trebuie menționat faptul că șansele de a intra într-o buclă infinită sunt mai mari atunci când folosim instrucțiunea while, deoarece putem omite să modificăm pasul la fiecare iterație. Instrucțiunea for este mai sigură din acest punct de vedere, dar trebuie menționat că putem avea un infinite loop chiar și dacă folosim cum nu trebuie for-ul.

Pentru a avea un program care rulează la infinit utilizând bucla for putem să nu specificăm nimic între parantezele rotunde ale instrucțiunii.

int main()
{
    int x = 8;
 
    for (; ; )
    {
        x = x * 10;
    }
 
    return 0;
}

Astfel programul nu va mai ieși din bucla for și va multiplica valoarea lui x de 10 ori la infinit.

Utilizarea pointerilor

C++, la fel ca C-ul, are tipuri de date speciale denumite pointeri, care sunt utile atunci când vrem să modificăm o valoare de la o anumită adresă din memorie.

Un pointer este un tip de date care stochează o adresă din memorie la un anumit moment de timp în program. Să luam spre exemplu următoarea secvență de cod.

int main()
{
    int x = 10;
    int* ptr = &x;
 
    return 0;
}

Se poate observa că avem o varibilă de tip int care a fost inițializată cu valoarea 10 și un pointer la int care a fost inițializat cu adresa lui x. Într-o ilustrare simplificată putem înțelege efectiv ce s-a întâmplat pe lina a doua de cod.

Pe linia int* ptr = &x;, ceea ce se întâmplă este faptul că pointerul ptr este asociat cu adresa variabilei x (apariția săgeții de la ptr la adresa lui x din desen). Practic, ptr devine un “arătător” către locația din memorie unde este stocată variabila x. Este important să înțelegeți că un pointer nu este altceva decât o variabilă specială care conține adresa de memorie a unei alte variabile. Astfel, ptr indică către adresa lui x și poate fi folosit pentru a accesa sau modifica valoarea lui x prin intermediul acestei adrese.

Un pointer poate pointa doar către o singură adresă, situația de mai jos fiind taxată cu o eroare de compilare.

int main()
{
    int x = 10;
    int y = 2;
 
    int* ptr = &x, & y; // eroare de compilare un pointer nu poate arata catre 2 adrese simultan
 
    return 0;
}

Iar vizual lucrurile ar arăta ca în imaginea de mai jos.

Trebuie însă înțeles faptul că această situație nu este permisă și nu are sens. E ca și cum ați vrea să arătați simultan cu același deget spre două persoane diferite ceea ce este fizic imposibil.

În schimb situația următoare este permisă și complet validă.

int main()
{
    int x = 10;
    int y = 2;
 
    int* ptr = &x;  // Pointerul ptr pointeaza catre adresa lui x
    ptr = &y;       // Incercarea de a face ca ptr sa pointeze si catre y (in realitate, acum ptr pointeaza doar catre adresa lui y)
 
    return 0;
}

Dacă un pointer arată către adresa de memorie a unei variabile, cum putem accesa totuși valoarea de la acea adresă spre care pointează pointerul nostru? Răspunsul este unul foarte simplu și anume printr-o operație specifică pointerilor cunoscută sub numele de dereferențiere.

int main()
{
    int x = 10;
    int* ptr = &x; // declararea unei variabile de tip pointer la int si initializarea acesteia cu adresa lui x
    int y = *ptr; // dereferentierea pointerului ptr si stocarea valorii in variabila y (echivalent cu a scrie direct int y = x;)
 
    return 0;
}

Dereferențierea unui pointer în limbaj natural poate fi privită ca obținerea/vizualizarea valorii existente la o anumită adresă din memorie. Aceasta se realizează cu ajutorul operatorului de dereferențiere “*” plasat înaintea variabilei de tip pointer. A nu se confunda cu steluța de la declararea unui pointer sau cu operatorul aritmetic de înmulțire a două numere.

Orice modificare pe care o suferă valoarea lui x va fi vizibilă și prin intermediul dereferențierii pointerului ptr și invers. Adică orice modificare a valorii de la adresa de pointare a lui ptr se va răsfrânge asupra lui x. Ca și mic exercițiu aflați valoarea lui x de la finalul programului următor.

int main()
{
    int x = 10;
    int* ptr = &x;
 
    *ptr = 4;
    x -= 2;
    *ptr *= 2;
    *ptr += 10;
 
    // care este valoarea lui x dupa ce s-au executat toate liniile de mai sus
 
    return 0;
}

Pointerii au ca și valoare default o adresă alocată random de compilator. NULL este un pointer special care conține o adresă vidă formată doar din zerouri. Practic NULL e echivalentul lui nimic (o adresă care este mereu goală) și de obicei când declarăm un pointer pe care nu îl utilizăm imediat este bine să îl inițializăm cu NULL.

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

Vom folosi foarte mult pointerii în acest semestru, iar pentru cei mai puțini familiarizați puteți urmări tutorialul urmator: C++ Pointers

Definirea funcțiilor de către programator

C++, la fel ca C-ul, suportă paradigma procedurală care presupune organizarea codului sursă în subprograme denumite și funcții. Funcția după cum îi spune și numele trebuie să se ocupe de un anumit lucru în program.

Ca și regulă de bună practică o funcție nu trebuie să facă mai mult de un singur lucru pe care este menită să îl facă.

Rețeta cea mai simplă pentru declararea unei funcții este:

  1. tipul de date returnat
  2. numele funcției
  3. lista de parametri

A doua componentă împreună cu cea de a treia sunt cunoscute și sub denumirea de semnătură a funcției.

Dacă îmbinăm cele 3 componente putem constata ca forma generală a unei funcții în C/C++ va arăta în felul următor → return_type name(parameters).

În continuare vom vedea câteva exemple de declarări pentru funcții definite de către programator.

int suma(int a, int b);
 
float medieAritmetica(int a, int b);
 
void printeazaNumar(int numar);

Implementarea funcțiilor se face în corpul acestora care este reprezentat de parantezele acolade. Funcțiile pot fi folosite prin apelare fie în funcția main fie în alte funcții după nevoie. Să urmărim exemplul de cod următor.

int suma(int a, int b)
{
    return a + b;
}
 
float medieAritmetica(int a, int b)
{
    return (float)(a + b) / 2;
}
 
int main()
{
    int x = 5;
    int y = 21;
 
    int s = suma(x, y); // se apeleaza functia suma
    float ma = medieAritmetica(x, y); // apelam mediaAritmetica
 
    return 0;
}

Este recomandat să organizăm codul în funcții, deoarece acest lucru facilitează considerabil depanarea aplicației atunci când apar erori. În plus, codul devine mult mai ușor de citit și de înțeles. Această practică este cunoscută și sub denumirea de structurare modulară a codului, în care logica programului este împărțită în segmente mai mici și mai ușor de gestionat.

Moduri de transmitere a parametrilor unei funcții

Transmiterea parametrilor prin valoare

Acest mod de transmitere a parametrilor implică crearea unor copii ale valorilor originale ale parametrilor. Aceste copii sunt utilizate în interiorul funcției, iar orice modificare adusă acestora nu va afecta variabilele originale. După încheierea execuției funcției, aceste copii sunt eliminate din memorie, iar valorile inițiale rămân neschimbate.

Să analizăm exemplul de mai jos.

int diferenta(int a, int b)
{
    return a - b;
}
 
int main()
{
    int x = 5;
    int y = 21;
 
    int dif = diferenta(x, y);
    return 0;
}

Variabilelor x și y li se face la fiecare câte o copie atunci când se apelează funcția diferenta. Nu doar că aceste copii sunt pierdute la încheierea execuției acestei funcții ba chiar și rezultatul întors de această funcție este transmis prin valoare. Deci în total avem 3 copii care s-au făcut în momentul în care linia de cod int dif = diferenta(x, y); a fost executată.

Pentru a vedea efectiv dezavantajul transmiterii parametrilor prin valoare vom implementa o funcție care realizează interschimbarea valorilor a două variabile întregi.

void interschimbare(int a, int b)
{
    int auxiliar = a;
    a = b;
    b = auxiliar;
}
 
int main()
{
    int x = 5;
    int y = 21;
 
    interschimbare(x, y); // interschimbarea se realizeaza doar la nivelul functiei (x = 5, y = 21)
    return 0;
}

Deși parametrii a și b ai funcției interschimbare sunt copii ale variabilelor x și y din programul principal, aceștia sunt doar variabile locale care există doar în interiorul funcției. Astfel, valoarea lui a va deveni 21 și cea a lui b va fi 5, însă aceste modificări se aplică doar la nivel local. După terminarea execuției funcției, variabilele originale x și y nu vor fi afectate și vor păstra valorile inițiale, deoarece modificările din interiorul funcției nu se propagă în afara ei.

Vom soluționa problema de mai sus utilizând un alt mod de transmitere a parametrilor și anume cea prin intermediul pointerilor.

Transmiterea parametrilor prin pointer

Acest tip de transmitere a parametrilor unei funcții este utilizat atunci când intenționăm ca modificările pe care le facem în interiorul funcției să persiste în afara ei.

Să reluăm exemplul prezentat pentru interschimbarea valorilor a două numere întregi.

void interschimbare(int* a, int* b)
{
    if (a == NULL || b == NULL)
    {
        return;
    }
 
    int auxiliar = *a;
    *a = *b;
    *b = auxiliar;
}
 
int main()
{
    int x = 5;
    int y = 21;
 
    interschimbare(&x, &y); // modificarile se vor rasfrange asupra lui x si y (x = 21, y = 5)
    return 0;
}

Deși parametrii a și b sunt tot variabile locale în funcția interschimbare, faptul că aceștia sunt pointeri schimbă modul în care subrogramul funcționează. În loc să fie doar copii ale valorilor lui x și y, acești pointeri conțin adresele de memorie ale variabilelor originale. Astfel, orice modificare efectuată asupra variabilelor accesate prin intermediul pointerilor va afecta direct valorile lui x și y. Acest mecanism permite funcției să modifice variabilele din programul principal, deoarece nu lucrează cu copii ale acestora, ci direct cu locațiile lor din memorie.

Putem folosi transmiterea prin pointer atunci când vrem sa facem anumite modificări asupra variabilelor, dar se poate utiliza și atunci când ne dorim doar să evităm copierea inutilă a valorilor, spre exemplu afisarea valorilor unui vector de numere întregi.

#include <iostream>
 
void afisareVector(int* v, int* n)
{
    for (int index = 0; index < *n; index++)
    {
        std::cout << v[index] << " ";
    }
}
 
int main()
{
    int nrElemente = 10;
    int vec[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 
    afisareVector(vec, &nrElemente); // sau un apel echivalent afisareVector(&vec[0], &nrElemente);
    return 0;
}

Deși în acest caz nu modificăm numărul de elemente sau valorile din vector, am reușit să evităm două copieri inutile care ar fi avut loc dacă am fi transmis vectorul și numărul de elemente prin valoare. Atunci când un vector este transmis ca parametru, numele său este echivalent cu adresa primului element din memorie, ceea ce permite funcției să lucreze direct cu datele din vectorul original. Din acest motiv, nu este necesar să folosim operatorul ”&“ pentru a obține adresa vectorului, deoarece aceasta este implicit furnizată atunci când transmitem variabila vec.

Pointeri la funcții

Pointerii la funcții (Function Pointers) sunt o caracteristică avansată a limbajelor de programare C și C++, care permit stocarea adreselor funcțiilor în variabile. Un pointer la funcție nu stochează o valoare, ci adresa din memorie unde este definită o funcție, oferind posibilitatea de a apela acea funcție indirect, prin intermediul pointerului.

Cum ne putem da seama dacă o funcție are asociată o adresă din memorie? Răspunsul este unul simplu și anume o apelăm fără parametri.

#include <iostream>
 
void func()
{
    // bloc de cod
}
 
int main()
{
    std::cout << func << '\n';
    return 0;
}

Practic, numele funcției reprezintă adresa din memorie unde este stocată acea funcție. Cu alte cuvinte, atunci când folosim numele funcției, accesăm automat locația sa din memorie, ceea ce permite atribuirea adresei acesteia unui pointer la funcție.

Rețeta cea mai simplă pentru declararea corectă a unui function pointer în C/C++ este următoarea → return_type (*name)(parameters).

În continuare vom construi un function pointer pentru funcția de la exemplul anterior.

#include <iostream>
 
void func()
{
    // bloc de cod
}
 
int main()
{
    void (*functionPtr)() = func; // declararea si initializarea unui function pointer cu adresa functiei func
 
    std::cout << func << '\n';
    std::cout << functionPtr << '\n'; // afiseaza aceeasi adresa cu cea de la linia anterioara
 
    return 0;
}

Atât tipul returnat de function pointer cât și lista parametrilor acestuia trebuie să fie identice cu cele ale funcției spre care pointează pentru a nu avea eroare de compilare.

Utilitatea pointerilor la funcții devine evidentă atunci când dorim să evităm duplicarea codului și să facem programul mai flexibil și mai modular. În loc să scriem cod redundant pentru a apela funcții similare în contexte diferite, putem utiliza pointeri la funcții pentru a selecta dinamic funcția corespunzătoare în timpul execuției.

Să luăm ca exemplu sortarea unui vector de numere întregi mai întâi crescător și apoi descrescător. Vom utiliza Bubble Sort pentru a păstra simplitatea programului.

#include <iostream>
 
void interschimbare(int* a, int* b)
{
    if (a == NULL || b == NULL)
    {
        return;
    }
 
    int auxiliar = *a;
    *a = *b;
    *b = auxiliar;
}
 
void sortareVectorCrescator(int* vector, int* nrElemente)
{
    for (int i = 0; i < *nrElemente - 1; i++)
    {
        for (int j = i + 1; j < *nrElemente; j++)
        {
            if (vector[i] > vector[j])
            {
                interschimbare(&vector[i], &vector[j]);
            }
        }
    }
}
 
void sortareVectorDescrescator(int* vector, int* nrElemente)
{
    for (int i = 0; i < *nrElemente - 1; i++)
    {
        for (int j = i + 1; j < *nrElemente; j++)
        {
            if (vector[i] < vector[j])
            {
                interschimbare(&vector[i], &vector[j]);
            }
        }
    }
}
 
void afisareVector(int* vector, int* nrElemente)
{
    for (int index = 0; index < *nrElemente; index++)
    {
        std::cout << vector[index] << ' ';
    }
 
    std::cout << '\n';
}
 
int main()
{
    int vector[10] = { 2, 1, 4, -3, 0, 5, -1, 8, 10, 9 };
    int nrElemente = 10;
 
    std::cout << "Vectorul sortat crescator este: ";
 
    sortareVectorCrescator(vector, &nrElemente);
    afisareVector(vector, &nrElemente);
 
    std::cout << "\nVectorul sortat descrescator este: ";
 
    sortareVectorDescrescator(vector, &nrElemente);
    afisareVector(vector, &nrElemente);
 
    return 0;
}

Se poate observa că singura diferență, între cele două funcții de sortare, apare la condiția din if pentru a stabili ordinea în care vor fi ordonate elementele vectorului. Pentru a putea simplifica masiv codul am putea utiliza pointeri la funcții.

#include <iostream>
 
void interschimbare(int* a, int* b)
{
    if (a == NULL || b == NULL)
    {
        return;
    }
 
    int auxiliar = *a;
    *a = *b;
    *b = auxiliar;
}
 
bool ordonareCrescatoare(int a, int b)
{
    return a < b;
}
 
bool ordonareDescrescatoare(int a, int b)
{
    return a > b;
}
 
void sortareVector(int* vector, int* nrElemente, bool (*comparator)(int, int))
{
    for (int i = 0; i < *nrElemente - 1; i++)
    {
        for (int j = i + 1; j < *nrElemente; j++)
        {
            if (comparator(vector[i], vector[j]) == false)
            {
                interschimbare(&vector[i], &vector[j]);
            }
        }
    }
}
 
void afisareVector(int* vector, int* nrElemente)
{
    for (int index = 0; index < *nrElemente; index++)
    {
        std::cout << vector[index] << ' ';
    }
 
    std::cout << '\n';
}
 
int main()
{
    int vector[10] = { 2, 1, 4, -3, 0, 5, -1, 8, 10, 9 };
    int nrElemente = 10;
 
    std::cout << "Vectorul sortat crescator este: ";
 
    sortareVector(vector, &nrElemente, ordonareCrescatoare);
    afisareVector(vector, &nrElemente);
 
    std::cout << "\nVectorul sortat descrescator este: ";
 
    sortareVector(vector, &nrElemente, ordonareDescrescatoare);
    afisareVector(vector, &nrElemente);
 
    return 0;
}

Se poate observa cât de elegant am evitat acum codul duplicat prin construirea a doi comparatori (cele două funcții care întorc o valoare booleană) pentru a decide ordinea de sortare și trimiterea adreselor acestora ca al treilea parametru de la funcția de sortare. Așadar prin utilizarea function pointer-ului ca parametru beneficiem de flexibilitate și eleganță în cod chiar dacă sintaxa nu este cea mai prietenoasă. Acesta este doar un simplu exemplu demonstrativ de utilizare a pointerilor la funcții. Pentru mai multe exemple de cum pot fi folosți pointerii la funcții puteți citi de aici.

Tipul de date bool în C++ este un tip de bază, care simplifică exprimarea condițiilor în programare. O variabilă booleană poate avea doar două valori: true sau false, făcând astfel codul mai clar și mai ușor de înțeles. Se recomandă utilizarea tipului bool în locul tipului int atunci când rezultatul unei comparații este strict adevărat sau fals. În C, pentru a folosi acest tip de date, este necesară includerea antetului stdbool.h prin directiva #include <stdbool.h> la începutul programului.

Variabile constante

La fel ca în C, în C++ putem declara variabile constante folosind cuvântul cheie const. Așa cum le spune și numele variabilele constante sunt acea categorie de variabile care odată ce au fost inițializate cu o valoare nu mai pot fi modificate ulterior.

O variabilă constantă se ințializează pe linia unde a fost declarată altfel va genera o eroare de compilare.

Utilizarea keyword-ului const pe variabile obișnuite

Pentru a înțelege cum putem declara variabile constante în C/C++ vom urmări exemplul de cod de mai jos.

int main()
{
    const float pi = 3.14f; // variabila constanta corect declarata
    const double k; // eroare de compilare variabila constanta este declarata dar neinitializata
 
    int a = 2;
    int b = 3;
 
    const int suma = a + b; // corect
    suma = 20; // incorect, o variabila constanta nu mai poate fi modificata dupa ce a fost declarata si initializata
 
    return 0;
}
Utilizarea keyword-ului const pe variabile de tip pointer

Sunt 3 situații pe care le vom observa în exemplul de cod de mai jos.

int main()
{
    int a = 2;
    int b = 3;
 
    // Situatia 1
 
    // Pointer la un int, dar pointerul nu poate modifica valoarea la care pointeaza
    // P1 este un pointer la un int constant (valoarea la care pointeaza nu poate fi modificata prin intermediul lui p1)
    const int* p1 = &a;
 
    // Pointer la un int constant, echivalent cu const int* p1
    // P2 este un pointer la un int constant (valoarea la care pointeaza nu poate fi modificata prin intermediul lui p2)
    int const* p2 = &b;
 
    // Situatia 2
 
    // Pointer constant la un int
    // P3 este un pointer constant la un int (pointerul in sine nu poate fi schimbat, dar valoarea la care pointeaza poate fi modificata)
    int* const p3 = &a;
 
    // Situatia 3
 
    // Pointer constant la un int constant
    // P3 este un pointer constant la un int constant si pointerul in sine nu poate fi schimbat (nu poate pointa catre alta adresa)
    const int* const p4 = &a;
 
    // Efectiv aceeași declaratie ca p3, dar cu sintaxa diferita
    // P4 este un pointer constant la un int constant si pointerul in sine nu poate fi schimbat
    int const* const p5 = &a;
 
    *p1 = 5; // eroare de compilare
    p1 = &b; // valid
 
    *p2 = *p1; // eroare de compilare
    p2 = p1; // valid
 
    *p3 = *p2; // valid
    p3 = &b; // eroare de compilare
 
    *p4 = 50; // eroare de compilare
    p4 = p2; // eroare de compilare
 
    p5 = p1; // eroare de compilare
    *p5 = 10; // eroare de compilare
 
    return 0;
}
Utilizarea keyword-ului const pe parametrii funcțiilor

Este similar cu ceea ce ați văzut în cele 2 situații pentru variabile și pentru pointeri. Reluați exemplele de cod de mai sus care conțin funcții și încercați să vă dați seama unde ar fi necesar ca subprogramele să aibă parametrii constanți.

GIT și GitHub

GIT este un sistem de control al versiunilor distribuit, dezvoltat de Linus Torvalds în 2005 pentru a gestiona kernel-ul Linux. În prezent, este cel mai folosit sistem de versionare, iar împreună cu platforma GitHub oferă un cadru complet pentru dezvoltare colaborativă, versionare și backup al proiectelor software.

De ce să folosim Git și GitHub?

  • Lucru colaborativ – mai mulți programatori pot contribui la același proiect.
  • Backup pentru cod – fiecare clonă locală conține întregul istoric.
  • Istoricul modificărilor – putem reveni oricând la o versiune anterioară.
  • Gestionarea branch-urilor – dezvoltare paralelă și integrare prin merge.
  • Integrare cu unelte moderne – paltforma GitHub permite code review, issue tracking și CI/CD.

Termeni specifici

  • Repository – componenta ce conține ierarhia de fișiere și istoricul versiunilor.
  • Checkout – descărcarea unei versiuni în mediul local.
  • Working copy – copia locală a proiectului în care lucrăm.
  • Commit – publicarea în repository-ul local a modificărilor din working copy.
  • Push – trimiterea modificărilor locale către repository-ul central (GitHub).
  • Pull – actualizarea versiunii locale cu modificările de pe server și integrarea lor în branch-ul curent.
  • Fetch – aducerea ultimelor modificări de pe server (remote) dar fără a le integra automat în branch-ul local.
  • Branch – linie paralelă de dezvoltare.
  • Merge – unirea a două sau mai multe ramuri de dezvoltare.
  • Conflict – apare când mai mulți utilizatori modifică același fișier și Git nu poate decide singur ce variantă să păstreze.
  • Stash – arhivează temporar modificările locale care nu sunt încă pregătite pentru commit, permițând schimbarea branch-ului sau actualizarea codului fără a pierde progresul.
  • Cherry-pick – preluarea unui anumit commit dintr-un alt branch și aplicarea lui în branch-ul curent, fără a face un merge complet; util pentru a integra doar modificări specifice.

Comenzi uzuale

Inițializarea unui repository
 git init 
Verificarea statusului
 git status 
Vizualizarea istoricului commit-urilor
 git log 
Adăugarea și commit-ul modificărilor
git add . # adaugă toate fișierele modificate
git add nume_fisier  # adaugă un singur fișier
git commit -m "Mesaj pentru modificare"
Trimiterea modificărilor pe server (GitHub)
git push # trimite modificările locale către branch-ul curent de pe remote
git push origin main # trimite explicit către branch-ul 'main' de pe remote
Actualizarea repository-ului local
git fetch # aduce modificările de pe server, fără a le integra automat
git pull # aduce și integrează modificările din branch-ul curent de pe server
git pull origin main # aduce și integrează modificările din branch-ul main
Lucrul cu branch-uri
git branch # afișează branch-urile locale
git branch -a # afișează toate branch-urile (locale și remote)
git checkout nume_branch # schimbă branch-ul curent
git branch -b nou_branch # creează un branch nou și trece pe el
git merge branch_modificari # unirea unui branch în branch-ul curent
Cherry-pick
 git cherry-pick <commit_id> # aplică un commit specific din alt branch în branch-ul curent 
Stash
git stash # salvează temporar modificările locale
git stash apply # reaplică ultimele modificări stashed
git stash list # afișează lista modificărilor stashed
git stash drop # șterge un stash specific 

Pașii pe care îi urmăm pentru un proiect colaborativ ca și developeri

  • Inițializăm repository-ul local cu git init sau clonăm unul existent cu git clone link_repo.
  • Lucrăm în working copy pentru a efectua modificări locale.
  • Adăugăm modificările pregătite pentru commit cu git add.
  • Facem commit cu modificările adăugate: git commit -m “Mesaj”.
  • Dacă avem modificări locale care nu sunt gata pentru commit, le putem salva temporar cu git stash.
  • Transmitem modificările pe server cu git push.
  • Actualizăm repository-ul local cu ultimele schimbări: git fetch, git pull.
  • Lucrăm pe branch-uri pentru dezvoltare paralelă și izolare modificări.
  • Pentru a integra modificări specifice din alt branch, putem folosi git cherry-pick <commit_id>.
  • Rezolvăm eventualele conflicte apărute la merge sau pull.
  • La final, facem merge pentru a integra branch-urile de dezvoltare în branch-ul principal.
  • Verificăm istoricul și starea proiectului cu git log și git status pentru claritate și control.

Ce ar trebui să facem?

  1. Să instalăm Git pe mașinile noastre
  2. Să avem sau să ne facem un cont pe platforma GitHub
  3. La prima utilizare după ce am instalat Git-ul trebuie să rulăm comenzile git config --global user_name și git config --global user_email
  4. Să ne setăm o cheie ssh conform instrucțiunilor de aici.

  • Pentru a descărca git-ul accesați site-ul următor: Download Git
  • Pentru instalare git urmăriți acest tutorial: Tutorial instalare Git
  • Pentru a vă crea un cont pe GitHub puteți urmări indicațiile de aici

Concluzii

În cadrul acestui laborator am explorat asemănările dintre C și C++ și am observat că ambele limbaje folosesc aceeași sintaxă de bază pentru variabile, funcții și instrucțiuni. De asemenea, am înțeles în mare ce sunt Git și GitHub și am învățat să inițializăm și să clonăm repository-uri, să lucrăm cu branch-uri, să facem commit-uri și să folosim comenzi precum push sau pull. În esență laboratorul și-a propus trecerea lină de la C la C++ cât și pregătirea mediului de lucru pe partea de Git și Github.

poo-is-ab/laboratoare/01.1759220097.txt.gz · Last modified: 2025/09/30 11:14 by razvan.cristea0106
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