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.
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
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: