Enrere Mòdul 7
Fonaments de programació. Llenguatge C/C++---
Pràctica  Exercicis
Pràctica d'ampliació

 
Resum Teòric

En aquest mòdul veurem una miscel·lània d'aspectes més avançats en C/C++. Aprendrem a treballar amb estructures de dades definides per l’ usuari. També farem una descripció de les unions, un altre mecanisme d’emmagatzematge de dades similar a les estructures però molt diferent en la seva utilització. Veurem com crear nous tipus de dades, com crear funcions amb un nombre variable d'arguments i  com podem assignar dinàmicament de memòria.

 

Concepte d'estructura

En C/C++ una estructura és una col·lecció de variables de diferents tipus bàsic referenciades sota el mateix nom. Per exemple,  un treballador d'una empresa pot estar caracteritzat pel seu nom, els cognoms, l’ edat...

El concepte d'estructura és molt important ja que és l'antecedent directe del concepte de classe en la Programació Orientada a Objectes.

Les estructures són, moltes vegades, anomenades registres o, en anglès, "records". De fet, les estructures són, en molts aspectes, similars als registres de les bases de dades. Seguint aquesta analogia, a les variables components de les estructures se solen anomenar camps o "fields".

Definició d'una estructura

Per definir una estructura es fa servir la següent sintaxi:

struct [<nom de l'estructura>] {
                     [<tipus> <nom de variable>[,<nom de variable>,...]];
                     ..........  
                     } [<variable de estructura>[,<variable de estructura>,...];

Sempre es comença amb la paraula reservada struct seguida d'un identificador per referir-se al nom de l'estructura. Aquest identificador és optatiu i, en alguns casos, no és necessari.

A continuació, i entre claus, es defineix l'estructura pròpiament dita. Aquesta consta d'una col·lecció de variables amb el seus tipus corresponents.

Després de la col·lecció de variables membres o camps, es pot declarar les variables d'estructura. Aquesta declaració es pot fer també separada de la definició de l'estructura. Veurem tot això amb un exemple:

struct treballador{
                char nom[50];
                char cognom1[30];
                char cognom2[30];
                int edat;
  } enginyer, administratiu, auxiliar;

D'aquesta forma s'ha definit l'estructura treballador i s'han declarat tres variables d'aquest "tipus". Per cada una de les variables es reserva 50+30+30+4=114 octets.

Aquest procés es pot seguir també per separat, en dues fases:

//fase de definició de l'estructura

struct treballador{
                char nom[50];
                char cognom1[30];
                char cognom2[30];
                int edat;
  }:

...

...

...

//fase de declaració de variables d'estructura

[struct] treballador enginyer, administratiu, auxiliar;

En la fase de definició de l'estructura no es reserva memòria per cap variable, només es fa un patró o motlle per poder declarar més tard variables amb aquesta estructura. 

Posteriorment, per declarar una variable amb aquesta estructura s'escriu el nom de l'estructura seguit del nom de la (o les) variable (s). 

En el estàndard ANSI C és necessari posar davant de l'identificador de l'estructura la paraula struct. Aquesta paraula es pot, optativament, ometre en C++, i d'aquesta forma, s'identifica el nom de l'estructura com un vertader tipus de variable.  

Si s'omet el nom de l'estructura en la seva definició es diu que s'ha creat una estructura anònima i no es pot, posteriorment a la definició de l'estructura, declarar cap variable amb aquesta estructura de forma directa.

Si una estructura té identificador, aquesta es pot passar com argument a una funció de la mateixa forma que qualsevol altre tipus de variable.

És una pràctica habitual definir les estructures globals, és a dir, fora de qualsevol funció, inclòs la funció main(). D'aquesta forma, totes les funcions podran declarar variables amb aquesta estructura.

Els camps d'una estructura poden ser altres estructures. Per exemple, és correcta aquesta definició:

struct data {
    int dia;
    int mes;
    int any;
};

 

struct persona {
    char nom [20] ;
    char cognoms[40] ;
    int edat;
    data naixement;
};

 

Inicialització de variables estructures

Per posar un valor inicial a les variables de tipus estructura es fa de la mateixa forma que amb els vectors. Els diferents valors dels camps són declarats un darrere de l'altre, separats per comes i tancats per claus. Per exemple:

treballador enginyer={"Joan","López","Puig", 35};

Referència a una estructura

Les variables d'estructura es poden referenciar completes o bé es pot accedir individualment als elements de l'interior. En aquest últim cas es fa servir l'operador punt (.). Aquest operador és el de màxima prioritat, exceptuant els parèntesis i claudàtors. 

Per exemple, per referir-nos al camp nom de la variable enginyer es fa servir l'expressió: enginyer.nom.

Assignació d'estructures

L'assignació d'estructures està permesa sempre que es faci entre variables del mateix tipus d'estructura. Per exemple, si definim dues variables del tipus treballador amb la sentència:

treballador enginyer1={"Joan", "López", "Puig", 35}, enginyer2;

una assignació com:

enginyer2=enginyer1;

equival a:

enginyer2.nom=enginyer1.nom;
enginyer2.cognom1=enginyer1.cognom1;
enginyer2.cognom2=enginyer1.cognom2;
enginyer2.edat=enginyer1.edat;

Vectors d'estructures:

L'ús simultani de vectors i estructures proporciona una poderosa eina per a l'emmagatzematge i manipulació de dades.

Per exemple, si l'empresa disposa de 10 enginyers, podem declarar una variable indexada com:

treballador enginyer[10];

En aquest cas, per referir-nos al nom de l'enginyer 0 escriurem:  enginyer[0].nom.

Pas d'estructures a funcions

Quan es passa una variable d'estructura a una funció, aquesta es passa per valor, és a dir, es passa una còpia de la informació de la variable que aquesta funció pot modificar sense modificar la variable original. A la pràctica 2 hi ha un exemple de com es passa una estructura sencera per valor a una funció.

Punters a estructures

De la mateixa forma que es pot declarar una variable punter que apunti a una variable de tipus bàsic, també és possible declarar un punter que apunti a una variable d'estructura. 

Per declarar una variable punter a una estructura se segueix la sintaxi normal de posar un * davant el nom de l'estructura. Per exemple:

treballador *persona;

Per trobar l'adreça d'una variable d'estructura s'escriu l'operador & davant el nom de la variable d'estructura. 

A l'exemple anterior podem assignar el punter persona a una variable d'estructura de la següent forma:

persona = &enginyer;

Per referir-nos al camp nom de la variable apuntada per persona es podria escriure: (*persona).nom (el parèntesi és necessari degut a la major prioritat de l'operador punt). Aquest és un sistema correcte però gairebé en desús. El segon mètode i el més utilitzat per referir-nos a un membre d'una variable d'estructura apuntada per un punter d'estructura és amb ajuda de l'operador (->). En el codi escrit a continuació, les dues últimes línies fan exactament el mateix, escriuen el nom de la variable enginyer on apunta el punter p:

treballador *p;
char cadena[110];
p=&enginyer;

printf("%s\n", (*p).nom);
printf("%s\n", p->nom);

 

Variables de tipus enumerat

Una enumeració és un conjunt de constants enteres amb un nom. Si una vegada definida una enumeració es declara una variable amb aquesta enumeració, la variable (entera) associarà aquestes constants amb els valors possibles de la variable. De fet, ens assegurem que aquesta variable no pot contenir un altre valor que alguna de les constants enteres definides.

Per definir una enumeració es procedeix de forma semblant a la definició d'una estructura, canviant la paraula clau struct per la paraula clau enum. La declaració de variables enum també es pot fer en el mateix moment de la definició de l'enumeració o bé després.

Exemple:

enum dia {dilluns, dimarts, dimecres, dijous, divendres, dissabte, diumenge} avui;

La declaració de la variable avui es fa en el mateix moment de la definició de l'enumeració dia.

enum dia {dilluns, dimarts, dimecres, dijous, divendres, dissabte, diumenge};

......

dia avui;

La declaració de la variable avui es fa després de la definició de l'enumeració. L'identificador de l'enumeració (dia) es pot fer servir com un autèntic nom de tipus. En ANSI C és necessari anteposar a l'identificador de l'enumeració la paraula clau enum, de la mateixa forma que es fa amb les estructures. En C++ no és necessària aquesta paraula clau en la declaració de variables.

Una variable del tipus enumeració és, de fet, una variable entera (4 octets). Internament associa cada constant de la llista de l'enumeració amb un enter que per defecte és 0 la primera constant, 1 la segona constant... Aquesta associació es podria canviar si en el moment de la definició de l'enumeració s'especifica el número enter associat a cada constant.

Encara que internament una variable de tipus enumeració és una variable entera, aquesta només pot contenir un dels possibles valors de la llista d'enumeració i no es pot assignar cap altre valor.

Els compiladors de C no diferencien entre els tipus int i enum, és per això que es poden fer assignacions d'aquest tipus:

avui=5;  //assignació no vàlida en C++, només en C

En C++ cal forçar la conversió com:

avui=(dia) 5;       //assignació vàlida

 

Unions

Una unió és una variable que pot contenir diferents tipus dades en la mateixa posició de memòria però, com és evidentment, mai simultàniament. 

Una unió es defineix de forma similar a les estructures, canviant la paraula clau struct per union

union [<nom de l'unió>] {
                  [<tipus> <nom de variable>[,<nom de variable>,...]];
                     ..........  
                     } [<variable d'unió>[,<variable d'unió>,...];

La similitud entre unions i estructures va encara més enllà.

  • De la mateixa forma que en les estructures, és possible definir unions anònimes.
  • És possible declarar variables d'unió en la mateixa definició de la unió o després.
  • En ANSI C és necessari posar la paraula clau union davant l'identificador de la unió en una declaració posterior d'una variable d'unió, i en C++ ja no és necessari aquesta paraula.
  • Per referir-se a un membre concret d'una variable d'unió es fa servir l'operador punt (.).
  • Per referir-se a un membre concret d'una variable d'unió on apunta un punter es fa servir l'operador fletxa (->).

Les diferències entre estructures i unions són les següents:

  • Quan es declara una variable d'estructura es reserva espai de memòria per tots els membres de l'estructura, en canvi, quan es declara una variable d'unió, es reserva espai de memòria per al membre més gran.

  • Quan s'assigna un valor a una variable d'unió, aquest valor s'assigna a un dels membres que té un tipus concret. En aquest moment només es pot referir a aquest membre de la variable i no a altre. Si s'assigna un altre valor a un altre membre, l'antic valor del primer membre es perd.

Veurem un exemple de com s'emmagatzema a la memòria una estructura i una unió:

 

struct st_dada{
    char c;
    float f;
    char s[8];
}dada;

union u_dada{
    char c;
    float f;
    char s[8];
}dada;

En aquest exemple, la variable d'estructura necessita 13 octets mentre que la variable d'unió només 8.

La paraula clau typedef

C/C++ permeten definir nous noms de tipus amb la paraula clau typedef. Realment no s'està creant nous tipus sinó que s'està definint un nou nom per un tipus ja existent. La sintaxi d'aquesta assignació és:

typedef tipus nou_nom;

tipus pot ser qualsevol tipus bàsic, amb o sense modificadors, estructures, unions, enumeracions, etc. De fet, en ANSI C era la forma que una estructura, enumeració o unió es considerés realment com un nou tipus. Aquesta utilitat ja no és necessària en C++, on aquestes estructures són ja, de fet, nous tipus.

Funcions amb un nombre d'arguments variable

És possible en C/C++ crear funcions amb un nombre indeterminat d'arguments. Els arguments coneguts es posen de la forma tradicional (n'ha d'existir almenys 1) i els desconeguts es substitueixen per tres punts (...). Per exemple:

int funcio (char c, float f, …);

A l'arxiu de capçalera stdarg.h es defineixen algunes macros útils per a la manipulació d'un nombre indeterminat d'arguments. Aquestes es descriuen a continuació:

  • Un nou nom de tipus: va_list. Serà necessari declarar una variable d'aquest tipus per tenir accés a la llista de paràmetres.

  • La  macro va_start() que té la següent sintaxi:

void va_start(va_list p, últim);

Aquesta macro ajusta el valor de p per tal que apunti al primer paràmetre de la llista. <últim> és l'identificador de l'últim paràmetre fix abans de començar la llista.

  • La macro va_arg() que té la següent sintaxi:

tipus va_arg(va_list p, tipus);

Aquesta macro torna el següent valor de la llista de paràmetres, p ha de ser la mateixa variable que es va inicialitzar amb la macro va_start i tipus és el tipus de paràmetre que es traurà de la llista.

  • La macro va_end() permet restaurar l'estat de la pila que és on s'emmagatzemen les dades de la llista de paràmetres. Això és necessari perquè el programa acabi normalment. La seva sintaxi és:

void va_end (va_list p);

Els arguments de la funció main()

És útil, moltes vegades, especificar opcions o valors en el moment d'executar un programa des de l'indicador d'ordres del sistema operatiu. Els comandaments de MSDOS com copy o rename poden ser exemples d'execució d'un "programa" amb arguments entrats junts al nom del programa. El programa reconeixerà aquests paràmetres a través dels arguments de la funció main().

La forma de declarar els arguments de la funció main() és:

tipus main(int argc, char *argv[]);

o bé, la forma equivalent:

tipus main(int argc, char **argv);

El primer paràmetre argc és el nombre d'arguments que s'han especificat a l'indicador d'ordres (el nom del programa executable compta com el primer argument). El segon paràmetre argv és un vector de cadenes de caràcters que conté els arguments especificats. Exemple, si a l'indicador d'ordres s'escriu:

programa arg1 arg2 arg 3        (sense comes)

El primer paràmetre, argc serà 4, i el segon paràmetre, argv[] contindrà la llista "arg1", "arg2", "arg3".

Si aquests paràmetres són nombres, s'han de convertir al tipus corresponent amb les funcions de la llibreria estàndard:

float atof(char*) Converteix una cadena en un nombre del tipus float.
int atoi(char*) Converteix una cadena en un nombre del tipus int.
double strtod(char*) Converteix una cadena en un nombre del tipus double.

La declaració d'aquestes funcions es troba a l'arxiu d'encapçalament stdlib.h.

Memòria dinàmica

En l'execució d'un programa, la memòria disponible de l'ordinador es divideix en quatre parts:

  • la que conté el codi del programa.

  • la que conté totes les variables globals.

  • la pila (on s'emmagatzemen les variables locals).

  • la zona de memòria dinàmica.

Quan es reserva espai de memòria per a les variables locals estàtiques, aquestes es van emmagatzemant a la pila, avançant el punter d'aquesta pila. Quan les variables surten del seu àmbit, s'allibera automàticament aquesta memòria desplaçant cap enrere el punter de la pila.

A més d'aquesta forma de reserva d'espai que es fa en temps de compilació hi ha una altra forma que es pot fer en temps d'execució. La memòria reservada en temps d'execució s'anomena memòria dinàmica i correspon a la quarta de les zones abans citades. Les variables emmagatzemades en aquesta zona no surten mai d'àmbit, és a dir, són sempre visibles fins que s'alliberi deliberadament aquesta memòria. És doncs, responsabilitat del programador alliberar aquesta memòria reservada.

Les funcions malloc() i free() i els operadors new i delete

En el llenguatge C hi ha dues funcions de la llibreria estàndard que s'encarreguen de reservar i alliberar memòria dinàmica, es tracta de les funcions malloc() i free(). Aquestes funcions necessiten els arxius <stdlib.h> i <malloc.h>.

La funció malloc() conté un únic argument enter que consisteix en el nombre d'octets a reservar. Si existeix espai suficient, aquesta funció tornarà un punter del tipus void que pot ser convertit a qualsevol tipus de punter, en cas contrari, és a dir, si no hi ha espai suficient de memòria es torna un punter NULL.

La funció free() conté com a únic argument el punter de la primera posició de memòria reservada i serveix per alliberar aquesta memòria per poder tornar-la a usar en una altra ocasió. És important assegurar-se de què es passa un punter vàlid en aquesta funció ja que, de no ser així, podria caure el sistema.

En C++, les característiques d'assignació dinàmica s'han considerat tan importants que es van incloure com a part del llenguatge i, els operadors equivalents a malloc() i free() en C++ són new i delete. Aquests dos operadors C++ són pràcticament equivalents a les funcions anteriorment tractades de C. Hi ha una diferència a destacar i és que new no necessita l'operador sizeof per calcular la mida en octets de la memòria a reservar quan es posa un tipus darrere de new.

Per exemple, en el següent codi, les tres últimes línies són equivalents:

int *p;
p=(*int) malloc(sizeof (int));
p=(*int) malloc(4);
p= new int;       

La sentència new int; retorna ja un punter a enter i, per tant, no és necessària la conversió, que sí és obligatòria si la funció torna un punter *void com és el cas de la funció malloc().

Les funcions calloc() i realloc()

A més de la funció malloc() vista anteriorment, C incorpora altres funcions que complementen l'assignació dinàmica de memòria. Veurem les funcions calloc() i realloc():

La funció calloc() admet dos arguments:  el nombre i la grandària de cada element,  i retorna un punter void a la primera posició reservada,  o NULL si la petició no ha estat possible. En aquest cas, i a diferència de malloc(),  l'espai s'inicialitza  amb zeros. La sintaxi d'aquesta funció és:

void *calloc( int nombre_elements, int mida );

Com en la funció malloc(), el punter retornat per la funció calloc() és del tipus void i és necessari fer una conversió forçada al tipus adient.

La funció realloc() permet ampliar un espai contigu ja existent, o proporciona una zona nova i més gran d’espai contigu. La funció té dos arguments:  un punter a la zona de memòria existent i una nova mida en octets de la nova zona reservada. Aquesta funció retorna també un punter void a la nova zona reservada. És important remarcar que el segon argument és el total d'octets que es desitja per a la zona de memòria, no el nombre d'octets a ampliar. La sintaxi de la funció és:

void *realloc ( void *p, int nova_mida);