Programarea Calculatoarelor: Laborator 8

Funcții de intrare/ieșire

Exerciții pregătitoare

Exercițiul 1a Citiți într-un tablou de caractere două cuvinte de maxim 20 caractere (separate prin spații albe în intrare), și plasați-le în tablou unite printr-o liniuță. Exemplu: din Ana Maria în intrare contruim șirul Ana-Maria în tablou.

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: Dacă citirea a avut succes, nu putem afla direct din scanf dacă aceasta s-a oprit pentru că a ajuns la caracterul '.' sau la sfârșitul intrării sau din cauza limitei de caractere impuse. Putem afla acest lucru doar citind și testând următorul caracter. În general, când un caracter citit nu corespunde formatului dorit, e bine să-l punem înapoi în intrare, pentru a putea fi eventual tratat de restul programului (sau dimpotrivă, în funcție de cerințe consumăm caractere până ajungem la un caracter dorit). Problema se poate soluționa desigur și fără utilizarea funcției scanf, citind caracter cu caracter. Ea e foarte similară cu implementarea funcției de citire a unei linii de text prezentată la curs, cu deosebirea că nu se citește până la linie nouă, ci până la punct.

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:

Cunoscând opțiunile de format pentru scanf putem să ne ușurăm sarcina efectuând împărțirea și parte din verificări deja prin șirul de format. Privim numerele ca șiruri de maxim 3 cifre: alegem formatul %3[0-9], repetat de 4 ori și separat prin punct, la fel ca in intrare. Cum cele 4 șiruri de cifre trebuie tratate uniform, e avantajos să nu declarăm 4 variabile separate, ci un tablou de 4 liniii (cuvinte):
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.h
Funcț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  nota3
unde 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, &note[0], &note[1], &note[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 printf("%d", round(suma / 3)) , compilatorul ne va avertiza că am specificat tipărirea unui întreg dar am dat ca parametru un real. Putem scrie însă o conversie explicită de tip, cu sintaxa: (tip)expresie : printf("%d", (int)round(suma / 3)) . Pentru rotunjirea în direcția lui zero (în jos pentru numere pozitive) a unui real x putem scrie și direct (int)x fără a mai fi nevoie de o funcție.

Cu acestea, programul primind intrarea

Ionascovici 7 8.5 9
va tipari la iesire
Ionascovici                         7.00    8.50    9.00       8
Să 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, &note[0], &note[1], &note[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 ]", &note[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 ]", &note[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:


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