Laborator 1 - Recapitulare PC. Vectori și matrice. Alocare dinamică

Responsabili

În cadrul acestui laborator ne propunem să recapitulăm cateva dintre conceptele de C invatate la cursul de Programarea Calculatoarelor.

Obiective

Ne dorim să:

  • Recapitulăm alocarea dinamică a memoriei și pointerii
  • Recapitulăm lucrul cu structuri
  • Recapitulăm directivele de preprocesare
  • Recapitulăm cum ne asigurăm ca nu avem memory leaks în programul nostru

Pass by value vs Pass by address

C este un limbaj pass-by-value. Asta înseamnă ca funcțiile își vor crea copii ale parametrilor și vor lucra cu ele. Dacă vrem să lucram direct pe variabilele trimise ca parametru, va trebui să trimitem adresa lor către funcție.

main.c
#include <stdio.h>
 
// Se va face câte o copie a variabilelor a și b. Aceste copii se vor distruge
// după ce funcția își va încheia execuția
void swap(int a, int b) {
     int temp = a;
     a = b;
     b = a;
}
 
// Se va face câte o copie a pointerilor dar vor pointa tot către variabilele
// a și b
void swap2(int* a, int* b) {
     int temp = *a;
     *a = *b;
     *b = temp;
}
 
int main() {
     int a = 5;
     int b = 10;
 
     printf("Before swap: %d %d\n", a, b); // 5 10
     swap(a, b);
     printf("After swap: %d %d\n", a, b); // 5 10
 
 
     printf("Before swap: %d %d\n", a, b); // 5 10
     swap2(&a, &b);
     printf("After swap: %d %d\n", a, b); // 10 5
 
     return 0;
}

Void pointer (void*)

Un pointer de tipul void* este un pointer care nu este asociat cu niciun tip de date, ceea ce îi permite să pointeze la adrese de memorie de orice tip. De asemenea, el poate fi specializat (cast) la orice tip de date.

Exemplu de folosire în contextul genericității

main.c
#include <stdio.h>
 
enum types {
   INT,
   DOUBLE,
   STRING
};
 
void print(void* var, enum types type) {
   if (type == INT) {
       printf("%d\n", *((int*)var));
   }
   if (type == DOUBLE) {
       printf("%lf\n", *((double*)var));
   }
   if (type == STRING) {
       printf("%s\n", (char*)var);
   }
}
 
int main() {
   int a = 123;
   double b = 2.67;
   char* c = "wubba lubba dub dub";
 
   print(&a, INT);
   print(&b, DOUBLE);
   print(c, STRING);
 
   return 0;
}

Alocarea dinamică

Vectori

Printr-un vector se înţelege o colecţie liniară şi omogenă de date. Un vector este liniar pentru că datele (elementele) pot fi accesate în mod unic printr-un index. Un vector este, de asemenea, omogen, pentru că toate elementele sunt de acelaşi tip. În limbajul C, indexul este un număr întreg pozitiv şi indexarea se face începând cu 0.

Declarare vector static:

int a[100]; // declarare statică: dimensiunea acestui vector trebuie să fie o constantă 
            // la compilare şi nu poate fi modificată în cursul execuţiei programului.

Declarare vector alocat dinamic:

int *a = malloc(n * sizeof(*a));free(a);

În limbajul C nu există la nivel de sintaxă nicio diferenţă între utilizarea unui vector cu dimensiune fixă şi utilizarea unui vector alocat dinamic.

Este posibil ca funcțiile ce alocă memorie să nu reușească să facă acest lucru, drept care întorc NULL. Din acest motiv, este obligatoriu să verificăm de fiecare dată pointerul întors de funcțiile malloc, calloc și realloc. Un mod universal acceptat de a face acest lucru este prin macro-ul DIE. Acesta primește ca “parametri” o condiție de verificat și o descriere a eventualei erori.

Observați în codul de mai jos macro-urile __FILE__ și __LINE__. Acestea se expandează în cadrul preprocesării la numele fișierului și, respectiv, linia curentă din acesta, pentru a ajuta la identificarea erorii. Funcția perror afișeaza la consolă mesajul primit ca parametru, împreună cu o descriere a ultimei erori produse în sistem. Aceasta este indicată de variabila errno, care este un număr întreg, declarat în headerul errno.h, și care indică ultima eroare aparută în sistem. Funcția perror se folosește, deci, de errno.

#include <errno.h>
#define DIE(assertion, call_description)				\
        do {								\
                if (assertion) {					\
                        fprintf(stderr, "(%s, %d): ",			\
                                        __FILE__, __LINE__);		\
                        perror(call_description);			\
                        exit(errno);					\
                }							\
        } while (0)

Pentru mai multe detalii despre DIE, puteți consulta și descrierea acestuia de pe pagina cursului de Sisteme de Operare, din anul 3.

Matrice

Matricea este o colecţie omogenă şi bidimensională de elemente. Acestea pot fi accesate prin intermediul a doi indici, numerotaţi, ca şi în cazul vectorilor, începând de la 0.

Declarare matrice statica:

int mat[5][10];

Declarare matrice dinamică:

int **a;
...
a = malloc(nl * sizeof(int *));   // Alocare pentru vector de pointeri
for (i = 0; i < nl; ++i) {
   a[i] = malloc(nc * sizeof(int));  // Alocare pentru o linie
}
...
for (i = 0; i < nl; ++i) {
     free(a[i]);
}
free(a);

Dacă se cunoaște la compilare prima dimensiune a matricei (numărul de linii), un alt mod de a declara o matrice dinamic este următorul:

int (*mat)[10] = malloc(sizeof(*mat) * 5);
...
free(mat);

În acest caz, toată matricea va fi alocată într-o zonă continuă de memorie. Pentru a explicita codul de mai sus, trebuie să ințelegem de ce sizeof(*mat) == 40. Acest lucru este datorat faptului că mat desemnează un array de vectori de câte 10 int-uri, adică 40 de octeți. Va sa zica, pentru oricare i, mat[i] este un vector de 10 int-uri.

Pentru a aloca dinamic un vector putem folosi și funcția calloc. Există două mari diferențe între calloc si malloc:

  • Semnătura funcției: calloc primeşte două argumente, mai exact o lungime de vector şi o dimensiune a fiecărui element.
  • calloc initializeaza elementele cu 0, pe când malloc nu face niciun fel de inițializare.

Realocarea

Redimensionarea unui vector care crește (sau scade) față de dimensiunea alocată inițial se poate face cu funcţia realloc, care primeşte adresa veche şi noua dimensiune şi întoarce noua adresă.

În cazul în care realocarea a eșuat, la fel ca celelalte funcții de alocare, realloc va întoarce NULL. Orice funcție din “familia” *alloc manifestă acest comportament atunci când alocarea de memorie eșuează. Din acest motiv, trebuie sa verificați intotdeauna că pointerii întoarși de acestea nu sunt NULL.

Static

Variabile statice

Variabilele statice reprezintă un tip special de variabile care sunt stocate într-o parte separată a memoriei, astfel încât să fie vizibile oriunde în program. Acestea au avantajul că dacă sunt declarate într-o funcție și apoi modificate, acele modificări vor fi vizibile și la următorul apel al funcției, spre deosebire de variabilele locale obișnuite.

#include <stdio.h>
void f() {
    static int i = 0;
    ++i;
    printf("%d ", i);
}
 
int main() {
    for (int i = 0; i < 3; ++i) {
        f();
    }
 
    return 0;
}

Funcții statice

Declararea unei funcții ca fiind statică îi va restricționa acesteia accesul la fișierul în care este declarată. Astfel, dacă fișierul sursă al acesteia este inclus în altul iar funcția respectivă este apelată din el, vom primi o eroare de compilare, funcția statică nefiind inclusă.

Este recomandat, deci, ca atunci când vă definiți o funcție auxiliară sau care pur și simplu nu are sens să fie vizibilă din alte fișiere ce ar fi linkate cu fișierul in care e definită funcția, aceasta să fie declarată folosind keywordul static.

O explicație mai detaliată și mai tehnică găsiți aici.

f1.c
static void f() {
    printf(“imported”);
}
f2.c
#include “f1.c”;
 
int main() {
    f(); // Se va genera o eroare de tip “undefinied reference to f” deoarece funcția statică nu a fost importata.
    return 0;
}

Const

Keyword-ul const desemnează o variabilă constantă, read-only, a cărei valoare nu se va mai schimba după actualizare. Cand se incearca schimbarea valorii, se va genera o eroare de compilare. În cazul pointerilor fiți atenți dacă adresa de memorie este cea constantă sau valoarea spre care pointeaza!

Deducerea acestor declarări se poate face prin clockwise/spiral rule.

Exemple:

int* a; // pointer de tip int
int* const a; // pointer constant catre un int variabil
int const *a; // pointer variabil catre un int constant
int const * const a; // pointer constant catre un int constant

Pentru syntactic sugar putem muta primul const la începutul declarației:

const int* a == int const* a (veți vedea de multe ori acest tip de declarare)

Directive de preprocesor

Definirea de macro-uri

#define MAX 50

Putem defini constante ce vor fi înlocuite peste tot în program în etapa de preprocesare. Este recomandat să folosim aceasta optiune în defavoarea scrierii efective a constantei deoarece suntem mai predispuși la buguri putând uita sa modificăm constanta în unele părți ale programului.

Garzi

Folosim gărzi de preprocesare pentru a evita incluziunea multiplă și redefinirea de variabile. Astfel, chiar dacă includem de mai multe ori același header, textual, vom avea o singura înlocuire a variabilelor/funcțiilor.

engine.h
struct A{};
car.h
car.h
#include “engine.h” // se va înlocui cu struct A la preprocesare
main.c
#include “engine.h”
#include “car.h”
 
...// Vom avea inclusă de două ori struct A, ceea ce va duce la o eroare de compilare
   // deoarece se încearcă redefinirea lui A.

Solutie: Adăugăm gărzi în fiecare fișier

engine.h
#ifndef __ENGINE_H__
#define __ENGINE_H__
 
struct A {};
 
#endif
car.h
#ifndef __CAR_H__
#define __CAR_H__
 
#include "engine.h"
 
#endif
main.c
#include "engine.h"
#include "car.h"
 
int main() {
    return 0;
}

După expandare vom avea:

main.c
#ifndef __ENGINE_H__ // Adăugăm o intrare pentru "engine" în tabela de simboluri
#define __ENGINE_H__
 
struct A {};
 
#endif
 
#ifndef __CAR_H__ // Adăugăm o intrare pentru "car" in tabela de simboluri
#define __CAR_H__
 
#ifndef __ENGINE_H__ // "engine" este definit deci sărim peste acest branch și nu includem struct A din nou
#define __ENGINE_H__
 
struct A {};
 
#endif
 
#endif
 
int main() {
    return 0;
}

Exerciții

Scheletul de laborator

1) Se citesc de la tastatura N cercuri definite prin coordonatele centrului si raza (toate, numere întregi). Sa se numere cate perechi de cercuri se intersectează. Se consideră că două cercuri se intersectează și dacă acestea sunt doar tangente.

Ex:
6
25 25 15
10 10 12
25 20 7
40 40 5
48 40 5
0 30 5

Output: 4 (se intersectează perechile de cercuri: (1, 2), (1, 3), (2, 3), (4, 5))

2) Se citește o matrice de dimensiune n x m. Fiecare linie va forma un număr. Exemplu: linia [3, 0, 2] va forma numărul 302. Afișați suma liniilor.

  • Alocați dinamic matricea.
  • Implementați logica problemei.
  • Verificați cu Valgrind că nu aveți memory leaks.

Interviu

Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului.

  • Care este diferența dintre pass by value si pass by address?
  • Ce întoarce funcția malloc? De ce?
  • Care sunt diferențele dintre alocarea dinamică și alocarea statică?
  • Ce se întâmplă dacă un header este inclus de două ori?

Și multe altele…

Bibliografie obligatorie

Bibliografie recomandată

sd-ca/laboratoare/lab-01.txt · Last modified: 2023/03/01 22:04 by iulia.corici
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