Programarea Calculatoarelor: Laborator 10

Pointeri și alocare dinamică

Exercițiul 1 Utilizarea unui tablou de pointeri la caracter. Alocarea dinamică a unui șir

Scrieți o funcție care ia ca parametri un tablou de adrese de caracter (fiecare indicând un șir valid) și returnează, alocat dinamic, un șir de caractere care e concatenarea celor date, separate prin spații.

Pentru a clarifica folosirea unui tablou de adrese, scriem întâi următoarea funcție care tipărește tabloul dat:

void afiseaza(char *tab[], int num) // sau: char **tab
{
  for (int i = 0; i < num; ++i)
    printf("la adresa %p se afla sirul %s\n", tab[i], tab[i]);
}
În declarația funcției, tipul elementelor tabloului (char *) se scrie înaintea numelui tabloului, ca și pentru orice tablou: tip numetablou[], în acest caz tip fiind char *. După aceeași regulă, pentru că funcția primește de fapt ca parametru adresa tabloului (reprezentată prin numele său) se poate scrie tip *numetablou, deci char **tab.
Apelul la printf tipărește de două ori un element de tablou tab[i] (o adresă char *), interpretându-l în mod diferit: formatul %p tipărește adresa, în format numeric hexazecimal, formatul %s tipărește șirul de caractere care începe la acea adresă, și se continuă la adrese numeric succesive, până la sfârșitul său, indicat prin caracterul '\0' cu cod ASCII zero.

Pentru a rezolva problema propusă, parcurgem întâi tabloul și însumăm lungimile șirurilor pentru a afla care va fi dimensiunea șirului concatenat. E preferabil să facem alocarea dinamică o singură dată, cunoscând dimensiunea totală a memoriei necesare (chiar cu costul parcurgerii preliminare a șirurilor) decât să repetăm pentru fiecare șir efortul de creștere cu realloc a zonei de memorie alocate.

char *concat(char *tab[], int num) // sau: char **tab
{
  size_t len = num;	// dimensiunea zonei de alocat
  // pe langa siruri e nevoie de num-1 spatii + un caracter '\0'
  for (int i = 0; i < num; ++i)
    len += strlen(tab[i]);	// calculeaza lungimea totala
  char *p = malloc(len), *end = p;
  if (p) {	// alocare cu succes
    for (int i = 0; i < num; ++i) {
      char *s = tab[i];
      while (*s) *end++ = *s++); // copiaza sirul tab[i] pana la '\0'
      *end++ = ' ';              // pune spatiu și avansează
    }
    end[-1] = '\0';	// pune '\0' in loc de spatiu dupa ultimul cuvant
  }
  return p;
}
Dacă alocarea de memorie decurge cu succes (adresa returnată de malloc e nenulă), funcția continuă copiind rând pe rând șirurile din tablou în zona de memorie alocată. Se inițializează pointerul end cu adresa de început a zonei alocate (p) și se copiază pe rând fiecare șir, adăugând apoi câte un spațiu. După ultimul șir, în loc de spațiu trebuie pus terminatorul '\0'; pentru că prin *end++ = ' '; pointerul end a avansat deja la poziția următoare, caracterul trebuie scris cu o poziție înainte de adresa end, la end[-1] .

Copierea din tab[i] se putea face și cu funcția strcpy, dar în acest caz, șirul era parcurs de două ori: o dată la copiere, și încă o dată pentru a-i calcula lungimea (necesară pentru plasarea spațiului). Ciclul s-ar fi scris:

strcpy(end, tab[i]);
end += strlen(tab[i]);
*end++ = ' ';
Putem folosi funcția pentru a concatena cuvintele de pe linia de comandă a programului: argumentele primite de main sunt exact în forma cerută de funcția noastră. După ce șirul alocat dinamic a fost folosit și nu mai e nevoie de el, memoria trebuie eliberată prin apelarea funcției free.
int main(int argc, char *argv[])
{
  char *p = concat(argv, argc);
  if (p) {
    printf("%s\n", p);
    free(p);
  }
  return 0;
}
Exercițiul 2 Crearea unui tablou de pointeri
Scriem o funcție care face prelucrarea inversă celei din problema anterioară: funcția ia ca parametru un șir de caractere, îl desparte în cuvinte (subșiruri care nu conțin spații), marchează sfârșitul fiecărui cuvând cu '\0' și returnează un tablou alocat dinamic care are ca elemente adresele cuvintelor din șir.

Funcția trebuie să returneze adresa unui tablou cu elemente adrese de caracter (char *), deci rezultatul e de tipul char **. Similar cu problema anterioară, efectuăm întâi o parcurgere în care numărăm cuvintele din șir (primul ciclu for).

char **splitwords(char *s)
{
  while (isspace(*s)) ++s;	// sare peste spațiile inițiale
  int nw = 0;	// numarul de cuvinte
  for (char *p = s; *p; ++nw) {	// fiecare ciclu numara un cuvant
    while (!isspace(*++p) && *p);  // sare peste cuvânt
    while (isspace(*p)) ++p;	   // sare peste spații
  }
  char **t = malloc((nw + 1) * sizeof(char *));	 // un element in plus
  if (t) {
    char **t1 = t;	// intrarea curentă în tablou
    while (*s) {	
      *t1++ = s;	// memorează începutul cuvântului
      while (!isspace(*++s) && *s);  // sare peste cuvânt
      if (*s == '\0') break;   // s-a terminat tot șirul
      *s = '\0';	// marcheaza sfârșitul cuvântului
      while (isspace(*++s));	// sare peste eventuale alte spații
    }
    *t1 = NULL;	// marchează sfârșitul tabloului
  }
  return t; 
}
Pentru a folosi ulterior tabloul de adrese creat trebuie să știm câte elemente are. Încheiem prin convenție șirul de elemente din tablou (în aceste caz adrese) printr-o valoare care nu poate reprezenta un element valid (în acest caz, valoarea NULL pentru adresă invalidă). Convenția e folosită și la argumentele lui main: tabloul argv conține argc adrese valide, urmate de adresa NULL. Astfel, tabloul poate fi parcurs și fără a fi nevoie să specificăm explicit numărul de elemente, pănă la întâlnirea valorii NULL care semnalează sfârșitul tabloului.

După ce a numărat cuvintele (nw), funcția alocă un tablou de nw+1 elemente char * unde vor fi memorate adresele de început ale cuvintelor din șir. Tabloul este parcurs cu un pointer t1 care indică elementul curent; la fiecare iterație, la adresa indicată de acesta (adică în elementul curent) se trece adresa de început al cuvântului. Parcurgerea șirului este similară cu cea făcută prima dată; cu deosebire că primul spațiu de după un cuvânt e transformat în caracterul '\0' pentru a indica sfârșitul cuvântului, și apoi se sare peste eventualele spații suplimentare.

int main(void)
{
  char buf[81];
  if (fgets(buf, 81, stdin)) {
    char **t = splitwords(buf);
    if (t) {
      for (char **t1 = t; *t1; t1++)
	printf("la adresa %p cuvântul %s\n", *t1, *t1);
      free(t);
    }
  }
  return 0;
}
În programul principal, apelâm funcția scrisă pentru a separa în cuvinte o linie de text citită de la intrare. Parcurgem tabloul obținut cu un pointer și tipărim cuvintele. Desigur, ciclul se putea scrie și cu un indice:
for (int i = 0; t[i]; ++i)
  printf("la adresa %p cuvântul %s\n", t[i], t[i]);
Pentru ilustrare, am tipărit și adresa fiecărui cuvânt (elementul propriu-zis al tabloului): remarcăm că adresele sunt crescătoare și diferă între ele prin lungimea fiecărui cuvânt, plus spațiile. Funcția nu a realizat copii ale cuvintelor, ci a memorat adresele lor așa cum au apărut în linia de text.

Exercițiul 3 Crearea unui tablou care poate fi redimensionat
Scriem un program care citește în memorie un text de la intrare păna la sfârșitul intrării, și îl memorează pe linii, alocând un tablou de adrese, câte una pentru fiecare linie.

Folosim pentru citirea unei linii funcția prezentată la curs care alocă dinamic memorie pentru a citi o linie de text oricât de lungă și returnează adresa unde a fost citită linia:

char *makeline(void)
{
  char *p = NULL, *p1;
  int c, i = 0, lim = -1;

  while ((c = getchar()) != EOF) {
    if (i >= lim)
      if (p1 = realloc(p, (lim += 16)+1)) p = p1;
      else break;
    p[i++] = c;
    if (c == '\n') break;
  }
  if (p) {
    p[i++] = '\0';
    return realloc(p, i);
  } else return NULL;
}
Structura programului este similară din punct de vedere al alocării memoriei: nu știm dinainte câte linii conține intrarea, deci vom redimensiona tabloul de adrese atunci cănd devine insuficient. Dacă tabloul ar fi alocat static, am scrie de exemplu:
int i = 0, size = 10;	// 10 linii 
char *p, *tab[size];
while (p = makeline()) { // p e nenul, s-a citit o linie
  if (i >= size) break;	// tabloul e plin
  tab[i++] = p;	// stocheaza adresa
}
În cazul nostru, vom folosi alocarea dinamică, și dacă dimensiunea folosită (i) a tabloului depășește dimensiunea alocată (size), vom mări tabloul cu un număr de adrese (de exemplu, 10). Programul devine:
int main(void)
{
  int i = 0, size = 0;
  char *p, **tab = NULL, **t1;
  while (p = makeline()) { // p e nenul, s-a citit o linie
    if (i >= size) {
      if (!(t1 = realloc(tab, (size += 10)*sizeof(char *)))) break; // nu e loc
      tab = t1; // folosim adresa la care a fost realocat tabloul
    }
    tab[i++] = p;	// stocheaza adresa
  }
  if (tab) {		// s-a citit macar o linie
    tab = realloc(tab, (size = i) * sizeof(char *));	// realoca cat e nevoie
    for (i = 0; i < size; ++i) // foloseste tabloul
      printf("%s", tab[i]);
    for (i = 0; i < size; ++i) 
      free(tab[i]);	// elibereaza fiecare sir
    free(tab);	// elibereaza tabloul
  }  
  return 0;
}
De remarcat că in acest program am alocat dinamic (și trebuie să eliberăm) două tipuri de structuri: șiruri de caractere (alocate în funcția makeline) și un tablou de adrese (alocat în programul principal).

Exercițiul 4 Sortarea unor șiruri de caractere
Continuăm programul anterior efectuând și o prelucrare asupra textului citit: afișăm liniile acestuia în ordine alfabetică (lexicografică) crescătoare (ca numele din cartea de telefon). Pentru aceasta folosim funcția standard qsort. Aceasta are nevoie de o funcție de comparație care primește adresele a două elemente de tablou. Întrucât elementele tabloului sunt ele însele adrese (char *), funcția va avea parametri de tipul char ** :

int compstr(const char **ps1, const char **ps2)
{
  return strcmp(*ps1, *ps2);
}
Pentru compararea propriu-zisă putem folosi funcția standard strcmp; deoarece ea cere adrese de caracter, iar funcția de comparare primește adrese de adrese, e necesară dereferențierea acestora. Definim de asemenea un tip pentru funcția generică de comparare necesitată de qsort, care are doi parametri adrese generice (void *):
typedef int (*compar_t)(const void *, const void *);

Pentru sortare, e suficient să mai adăugăm la programul anterior o linie înainte de tipărirea tabloului:

qsort(tab, size, sizeof(char *), (compar_t)compstr);
unde am folosit conversia explicită de tip (typecast) pentru a converti adresa de funcție compstr la tipul precizat de declarația funcției qsort.

Spre comparație, dacă am fi lucrat cu tablouri bidimensionale de caractere, funcția de comparare ar fi primit direct adresele șirurilor si nu mai era nevoie de o dereferențiere suplimentară:

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

#define N	4
#define LEN	10

typedef int (*compar_t)(const void *, const void *);

int comptab(const char *s1, const char *s2)
{
  return strcmp(s1, s2);
}

int main(void)
{
  char t[N][LEN] = { "primavara", "vara", "toamna", "iarna" };
  qsort(t, N, LEN, (compar_t)comptab);
  for (int i = 0; i < N; ++i)
    printf("%s\n", t[i]);
  return 0;
}

Exerciții propuse:


Marius Minea
Last modified: Sat Apr 5 17:38:25 EET 2008