Tabloul trebuie declarat de minim 20 + 1 (liniuța) + 20 + 1 (pentru \0) caractere. Cuvintele nu trebuie citite în tablouri separate, ci direct acolo unde trebuie plasate: după citirea primului cuvânt calculăm lungimea lui, și plasăm o liniuță la sfârșit. Apoi, citim al doilea cuvânt la adresa caracterului următor în tablou. Pentru a citi un cuvânt trebuie să dăm funcției scanf adresa la care se va citi cuvăntul. Nu trebuie ca această adresă să fie neapărat începutul unui tablou (parametrii în C se transmit prin valoare, deci o funcție nu are cum "să-și dea seama" că adresa e început de tablou sau nu), ci trebuie doar ca de la acea adresă începând să rămână suficient de mult loc pentru a citi cuvântul.
#include <stdio.h> #include <string.h> int main(void) { char s[42]; printf("Introduceti doua cuvinte de max. 20 caractere: "); if (scanf("%20s", s) == 1) { unsigned len = strlen(s); // afla lungimea cuvantului citit s[len++] = '-'; // adauga liniuta, creste lungimea if (scanf("%20s", &s[len]) == 1) // citeste unde trebuie, pune si \0 printf("Cuvintele legate: %s\n", s); else printf("lipseste cuvantul 2\n"); } else printf("nu s-au introdus cuvinte\n"); return 0; }Este important să facem verificările la citire; altfel, dacă utilizatorul nu introduce cele solicitate, programul va afișa conținutul întâmplător al tabloului s (neinițializat!); sau programul poate fi abandonat forțat dacă apelul de tipărire sau de calcul al lungimii șirului nu întâlnește la timp caracterul nul și continuă accesul într-o zonă de memorie invalidă.
Exercițiul 1b Citiți o propoziție (șir de caractere care se termină cu '.') de maxim 80 de caractere (inclusiv punctul) și afișați în caz contrar un mesaj de eroare.
#include <stdio.h> #include <string.h> int main(void) { char prop[81]; int c, res; if ((res = scanf("%79[^.]", prop)) == EOF) printf("nu s-a introdus nimic!\n"); else if ((c = getchar()) == EOF) printf("s-a ajuns fara . la sfarsitul intrarii\n"); else if (c != '.') { printf("propozitie prea lunga, 80 de caractere fara punct!\n"); ungetc(c, stdin); // punem pe c inapoi in intrare } else if (res == 0) printf("punct fara nici un caracter inainte\n"); else { unsigned len = strlen(prop); prop[len++] = '.'; prop[len] = '\0'; } return 0; }Programul, deși foarte simplu, arată diferitele moduri în care poate să apară o eroare de la formatul cerut, și felul în care sunt semnalate de funcțiile de citire. Rezultatul apelului scanf din programul de mai sus poate fi:
Exercițiul 2 Recunoașterea unor tipare dintr-un text
O adresă IP (folosită în rețele de calculatoare) e formată din 4 octeți (numere între 0 și 255), și se reprezintă în text separând cele 4 numere prin câte un punct. Exemple: 127.0.0.1 , 255.255.255.255 , 193.226.12.13 . Scriem un program care identifică și tipărește toate adresele IP dintr-un text citit până la sfârșitul intrării.
Există multe variante de a aborda recunoașterea tiparului descris. Deși ar părea simplu să citim cele 4 părți direct ca numere, apare o problemă: citirea în format numeric (%d, %u, etc.) cu scanf permite spații albe inițiale, iar tiparul descris nu permite spații între punct și următorul număr. Soluții posibile ar fi:
char part[4][4]; // 4 cuvinte cu loc pentru 3 cifre si '\0' scanf("%3[0-9].%3[0-9].%3[0-9].%3[0-9]", part[0], part[1], part[2], part[3]);În secvența de mai sus, part e un tablou de tablouri. part[0] ... part[3] reprezintă adresele fiecărei linii din tablou și fiind adrese pot fi parametri la scanf. Dacă rezultatul apelului la scanf este 4, fiecare fragment e un șir valid de 1-3 cifre. Pentru a verifica dacă nu depășește 255, trebuie să-l transformăm în număr. Cea mai simplă funcție în acest scop este
int atoi(const char *s); // declarata in stdlib.hFuncția atoi nu permite tratarea erorilor (returnează zero, care nu poate fi distins de o valoare validă), dar în codul nostru, faptul că șirurile reprezintă numere s-a verificat deja la citire. Pentru stocarea celor 4 numere putem folosi un tablou. Deoarece numerele se pot reprezenta pe un octet, alegem tipul unsigned char (valoare intreagă pe un octet) pentru elementele tabloului. Rezultatul lui atoi (inainte de a-l verifica comparând cu 255) ar putea fi însa până la 999, deci alegem un tip întreg suficient de cuprinzător. Avem deci funcția:
// citeste o adresa IP in formatul numar.numar.numar.numar // pune cele 4 numere pe cate un octet in tabloul b; // returneaza 1 la succes si 0 la eroare int get_ip_addr(unsigned char b[]) { char part[PARTS][4]; // tablou de 4 siruri de cate 4 caractere short s; // numar intreg pe minim 2 octeti (suficient) scanf(" "); // elimina spatiile initiale if (scanf("%3[0-9].%3[0-9].%3[0-9].%3[0-9]", part[0], part[1], part[2], part[3]) != 4) return 0; for (int i = 0; i < PARTS; ++i) if ((s = atoi(part[i])) > 255) return 0; //eroare, numar prea mare else b[i] = s; // are loc pe un octet in tabloul b return 1; // succes }Funcția trebuie apelată repetat, până la parcurgerea întregului text de intrare. Eliminarea eventualelor spații inițiale e deja efectuată în funcție. Dacă secvența citită nu este însă o adresă validă, trebuie consumate eventualele caractere diferite de spații albe care îi urmează direct, pentru ca orice adresă validă care ar urma trebuie să fie un cuvânt separat (prin spații albe). Scriem ca punct de plecare secvența:
unsigned char b[4]; int c; if (get_ip_addr(b)) printf("%u.%u.%u.%u\n", b[0], b[1], b[2], b[3]); else do c = getchar(); while (c != EOF && !isspace(c));
Nu este suficient să includem această codul de mai sus într-un ciclu, deoarece la citirea cu succes nu am verificat dacă adresa citită e separată la sfârșit prin spații de textul care urmează (altfel am accepta eronat 4.3.2.1111, fără ultima cifră, ca fiind adresă validă). Completăm deci secvența cu un test la revenirea cu succes din funcție. Dacă urmează un spațiu sau s-a ajuns la sfârșitul intrării, adresa e corectă, o tipărim și trecem la următoarea iterație cu instrucțiunea continue. (Scriem ciclul cu condiția de terminare sfârșitul intrării). Altfel, fie că funcția a semnalat eroare, fie că adresa nu era separată la sfârșit, eliminăm restul caracterelor diferite de spații albe. (Aceasta s-ar putea face și cu secvența scanf("%*[^\t\n\v\f\r ]"); care citește (și ignoră, datorită modificatorului * din format) oricâte caractere diferite de cele 6 care sunt clasificate ca spații albe).
do { if (get_ip_addr(b)) { // adresa citita cu succes if (isspace(c = getchar()) || c == EOF) { // si separata la sfarsit printf("%u.%u.%u.%u\n", b[0], b[1], b[2], b[3]); continue; // treci la urmatoarea iteratie } } // elimina tot ce nu sunt spatii do c = getchar(); while (c != EOF && !isspace(c)); } while (c != EOF);Obținem astfel programul complet: ipaddr.c
Exercițiul 3 Prelucrarea unui tabel cu tipar dat
Dăm câteva exemple de citire și prelucrare a unor tabele, structurate întăi după un tipar fix, simplu, și apoi după un tipar variabil, mai complicat.
Considerăm întâi că la intrare se dă un tabel cu linii de forma
Nume nota1 nota2 nota3unde Nume e un singur cuvânt (fără spații), cu max. 32 caractere, iar notele sunt reale, separate prin spații albe. Trebuie să afișăm cele citite aliniate pe coloane, și în plus pe fiecare linie media celor 3 note, rotunjită la întreg.
Deoarece spațiile albe sunt separaratori impliciți pentru cuvinte și numere, citirea e simplă:
char nume[33]; double note[3]; scanf("%32s%lf%lf%lf", nume, ¬e[0], ¬e[1], ¬e[2]);Pentru float am fi folosit formatul %f. E obligatoriu să facem această distincție pentru scanf, deoarece float și double se reprezintă pe număr diferit de octeți; altfel obținem valori greșite și corupem memoria. În cazul lui printf lucrăm cu expresii și nu adrese, iar într-un apel de funcție, orice valoare float e convertită automat la double, formatul %f e comun pentru cele două tipuri.
Tipărim numele aliniat la stânga într-un cămp de 32 de caractere, completat cu spații, și folosim fanionul - pentru a-l alinia la stânga (implicit, alinierea e la dreapta și spațiile apar înainte)
printf("%-32s", nume);Tipărim numerele cu două zecimale, în total pe 8 caractere, completate la stânga cu spații cu formatul:
for (int i = 0; i < 3; ++i) printf("%8.2f", note[i]);Pentru a tipări media, calculăm suma ca real și forțăm tipărirea cu 0 zecimale (deci rotunjită la întreg).
printf("%8.0f\n", suma / 3);Există și funcția round (declarată în math.h) care rotunjește un număr real la cel mai apropiat întreg (și funcțiile trunc și ceil care rotunjesc în jos și respectiv în sus). Atenție, rezultatul acestora este tot de tip real (double), deși are zecimale nule! E incorect să scriem deci
Cu acestea, programul primind intrarea
Ionascovici 7 8.5 9va tipari la iesire
Ionascovici 7.00 8.50 9.00 8Să examinăm acum ce se întâmplă când trecem de la o linie la alta. După ultima notă a rămas necitit caracterul de linie nouă (și eventual alte caractere spații albe care s-ar putea afla la sfârșitul liniei). Fiind toate spații albe (inclusiv \n), acestea vor fi consumate automat la citirea numelui de pe linia următoare. Deci e suficient săstructurăm prelucrarea noastră într-un ciclu care se repetă cât timp citim toate cele 4 elemente.
while (scanf("%32s%lf%lf%lf", nume, ¬e[0], ¬e[1], ¬e[2]) == 4) { // prelucreaza si tipareste }Programele pe care le scriem trebuie în general să fie robuste la posibile erori în datele de intrare. Ciclul scris în acest fel ar abandona programul la prima citire incorectă. O variantă posibilă ar fi să semnalăm o eroare și să continuăm prelucrarea de la linia următoare, până la sfârșitul intrării. Pentru aceasta, eliminăm restul liniei după ce s-a detectat eroarea: fie
int c; do c = getchar(); while (c != EOF && c != '\n');fie cu scanf("%*[^\n]"); getchar(); (citim și ignorăm orice până la \n și apoi încă un caracter, chiar linia nouă). În acest caz nu vom mai ieși din ciclu când rezultatul e 4, ci doar atunci când e EOF, deci s-a ajuns la sfârșitul intrării. Obținem programul complet: tabel1.c.
Să presupunem acum că numerele au un separator explicit (de exemplu virgulă), o convenție des folosită atunci când salvăm un tabel în formă de text. Pentru numere, folosim atunci formatul: %lf ,%lf ,%lf . În format, punem spațiu înainte de virgulă, pentru a accepta eventualele spații albe înre număr și virgula separator din text: 3 , 4, 5. Nu e necesar spațiu în format după virgulă, deoarece formatele numerice acceptă oricum spații albe la început.
Dacă în tabel avem virgulă și între nume și primul număr, o primă soluție ar fi formatul %32s ,%lf ,%lf ,%lf dar acesta ridică o problemă: dacă virgula urmează imediat după nume, fără spații, aceasta va fi citită ca parte din nume (specificatorul s acceptă orice caractere care nu sunt spații albe). Ca soluție, putem înlocui %32s cu %32[^\t\n\v\f\r ,] care va citi șiruri formate din orice caractere în afară de spațiu alb și virgula. Ținând cont și de virgula care poate apare imediat după nume sau precedată de spații, obținem formatul "%32[^\t\n\v\f\r ,] ,%lf ,%lf ,%lf" .
Considerăm acum cazul în care avem în tabelul citit atât nume cât și prenume, cu un total de max. 48 caractere Putem folosi de două ori formatul s (cuvânt), dar această soluție nu mai e valabilă dacă putem avea mai multe prenume. O soluție e să acceptăm ca nume tot ce apare înainte de prima notă (sau înainte de virgulă, dacă aceasta e separator), presupunând că un nume valid nu conține cifre. Folosim atunci pentru formatul numelui %48[^,0-9]. Această versiune (prin excluderea caracterelor invalide) e mai puțin restrictivă decât una care cere nume formate doar din litere: %48[A-Za-z] deoarece admite de exemplu și caracterele naționale cu diacritice, care nu se încadează în alfabetul de 26 de litere din tabela ASCII. Va trebui să eliminăm potențialele spații albe de la sfârșit și eventual să normalizăm numele citit, lăsând câte un singur spațiu între componente.
Să presupunem acum că avem un număr variabil dar limitat de note separate prin spații pe fiecare rând (de exemplu maxim 8 discipline într-un semestru). Putem efectua citirea într-un ciclu, dar trebuie să detectăm sfârșitul de linie. Acesta trebuie testat după citirea fiecărui număr, deoarece citirea cu format numeric consumă spațiile albe inițiale (inclusiv \n) și ar putea astfel să treacă la rândul următor. Rezolvăm problema cu următoarea secvență de cod:
for (i = j = 0; i < NUM; ++i) { if (scanf("%lf%*[\t\v\f\r ]", ¬e[i]) != 1) break; ++j; // numar citit corect suma += note[i]; if ((c = getchar()) == '\n') break; // sfarsit de linie else ungetc(c, stdin); // pune inapoi }După fiecare număr citim și ignorăm orice spații albe în afară de \n (iar dacă nu s-a citit numărul ieșim din ciclu). Verificăm dacă următorul caracter e linie nouă, dacă nu, îl punem înapoi și citirea continuă. Ieșirea dintr-o linie corectă se face cu break la linie nouă, iar numărul j de note citite va fi cu 1 mai mare decăt indicele i care nu a mai ajuns să fie incrementat; putem testa aceasta dupa ciclu pentru a decide dacă afișăm linia sau eroare. Obținem programul tabel2.c .
Putem continua tratând cazul când în locul unei note inexistente trecem linie. Nu putem testa acest caz după ce am încercat citirea unui număr (dacă rezultatul citirii nu e 1) deoarece citirea va consuma caracterul - ca semn minus. Trebuie deci să testăm acest caracter înainte de citirea numărului, și să punem caracterul înapoi în intrare în caz contrar.
if ((c = getchar()) == '-') { note[i] = 0; scanf("%*[\t\v\f\r ]"); } else { ungetc(c, stdin); if (scanf("%lf%*[\t\v\f\r ]", ¬e[i]) != 1) break; }După orice astfel de modificare în program trebuie să ne punem problema ce efect are asupra celorlalte părți din program care până acum erau corecte. De exemplu, dacă în loc de prima notă avem - și am stabilit că numele (care poate conține spații, și de asemenea liniuțe) se oprește doar la prima cifră, semnul poate deveni parte din nume: pentru Radu-Ion Pop - 9 10 numele va fi considerat greșit Radu-Ion Pop - . Pentru a rezolva problema apărută, o soluție e să citim numele pe porțiuni (separate prin spații albe sau linie), caz în care linia de după nume va putea fi identificată corect. Concatenarea porțiunilor de nume citite (cu verificarea lungimii totale maxime!) se poate face printr-un ciclu de structură similară cu cel din programul de tipărire a unui text pe linii de lungime limitată exemplificat la curs.
Probleme propuse:
Extragerea unor porțiuni de text care corespund unor tipare. Exemple:
Verificarea unor texte structurate după anumite reguli:
a) Fișiere XML permit reprezentarea structurată a informației, asociind elementelor de date câte o etichetă care apare în forma <nume> la început, și în forma </nume> la sfârșitul valorii reprezentate, de exemplu <nota>9</nota> Entitățile de date pot conține la rândul lor alte elemente, de exemplu:
<student> <prenume>Ion</prenume> <nume>Iovanescovici</nume> <nota>9</nota> <nota>8</nota> <nota>10</nota> </student>O descriere bine structurată are etichete de început și de sfârțit pereche, și care sunt încuibate, fără a se încăleca (etichetele de sfârșit trebuie să apară în ordinea inversă a celor de început). Scrieți un program care citește de la intrare un text (cu etichete arbitrare), afișează doar informația (fără etichete) și semnalează dacă textul nu e bine structurat.
b) Fișiere LaTeX LaTeX e un limbaj pentru descrierea documentelor în vederea tipăririi. Printre multe facilități, el permite definirea de secțiuni care vor fi tipărite într-un anumit fel. Aceste secțiuni sunt delimitate prin comenzile \begin{nume} și \end{nume}, care pot fi la rândul lor încuibate. De exemplu, fragmentul
\begin{center} \begin{verbatim} int main(void) { return 0; } \end{verbatim} Cel mai simplu program C \end{center}are o astfel de secțiune (verbatim) împreună cu alt text obșnuit (dedesubt), ambele făcând parte dintr-o altă secțiune (center). Ca și exemplul de mai sus, secțiunile pot fi încadrate una în alta, dar nu se pot intercala (etichetele se închid în ordinea inversă în care au fost deschise). Scrieți un program care citește de la intrare un text cu această structură, afișează doar textul propriu-zis, și semnalează dacă apare erori de formatare.
Ambele exemple de mai sus se pot rezolva: