_curso de programacion en c c plus plus(Català)

141
El llenguatge de programació C Ernest Valveny Robert Benavente Àgata Lapedriza Miquel Ferrer Jaume Garcia Gemma Sánchez

Transcript of _curso de programacion en c c plus plus(Català)

Page 1: _curso de programacion en c c plus plus(Català)

El llenguatge de programació C

Ernest Valveny Robert Benavente Àgata Lapedriza

Miquel Ferrer Jaume Garcia

Gemma Sánchez

Page 2: _curso de programacion en c c plus plus(Català)

Índex Tema 1: Introducció.......................................................................................3

1. Història del llenguatge C .................................................................................. 3 2. Característiques del Llenguatge C .................................................................. 4 3. Estructura d’un programa en C ....................................................................... 6 4. Elements sintàctics d’un programa en C........................................................ 8 5. Compilació d’un programa en C .................................................................... 10 6. Entorns de Desenvolupament Integrat (IDE) ................................................ 12

Tema 2: Tipus de dades simples ................................................................24 1. Variables .......................................................................................................... 24 2. Constants......................................................................................................... 28 3. Identificadors de variables i constants ......................................................... 29 4. Entrada / Sortida bàsica ................................................................................. 30 5. Operadors i expressions ................................................................................ 35

Tema 3: Estructures de control ..................................................................43 1. Estructures condicionals ............................................................................... 43 2. Composició iterativa ....................................................................................... 50

Tema 4: Estructures de dades compostes ................................................57 1. Taules............................................................................................................... 57 2. Cadenes de caràcters ..................................................................................... 62 3. Taules bidimensionals.................................................................................... 67 4. Registres .......................................................................................................... 71 5. Unions .............................................................................................................. 78 6. Enumeracions ................................................................................................. 80 7. Definició de tipus (typedef) ............................................................................ 83

Tema 5: Funcions ........................................................................................84 1. Funcions i procediments................................................................................ 84 2. Declaració i crida de funcions ....................................................................... 85 3. Declaració i crida de procediments............................................................... 87 4. Pas de paràmetres .......................................................................................... 89 5. Àmbit de les variables .................................................................................... 90 6. Prototipus de les funcions. Fitxers de capçalera ........................................ 91 7. Funcions de llibreria estàndard ..................................................................... 94

Tema 6: Apuntadors ....................................................................................96 1. Conceptes bàsics............................................................................................ 96 2. Pas de paràmetres per referència utilitzant apuntadors ........................... 101 3. Apuntadors i arrays ...................................................................................... 102 4. Apuntadors a registres ................................................................................. 111

Tema 7: Memòria dinàmica .......................................................................113 1. Objectes dinàmics ........................................................................................ 113 2. Arrays dinàmics ............................................................................................ 116 3. Registres dinàmics ....................................................................................... 123 4. Estructures dinàmiques: llistes ................................................................... 127

Tema 8: Fitxers...........................................................................................130 1. Conceptes bàsics.......................................................................................... 130 2. El tipus fitxer (FILE) ..................................................................................... 130

Page 3: _curso de programacion en c c plus plus(Català)

Tema 1: Introducció 1. Història del llenguatge C La història del llenguatge C és certament curiosa, ja que, a diferència d'altres llenguatges de programació, el C no va sorgir d'un procés de disseny deliberat, sinó que la seva aparició està invariablement lligada al disseny del sistema operatiu UNIX. Moltes de les característiques del llenguatge C, que veurem més endavant en aquest capítol, responen a necessitats de disseny de sistemes operatius. Per tant és important contextualitzar el llenguatge C dins d'aquest marc. A finals de la dècada dels anys 60, el Massachusetts Institute of Technology (MIT), els laboratoris Bell d'AT&T i l'empresa General Electric estaven treballant en un sistema operatiu experimental anomenat MULTICS (Multiplexed Information and Computing Service), dissenyat per ser executat en grans Mainframes. Desgraciadament, ni la tecnologia, ni les tècniques de programació estaven prou madures en aquell moment per poder desenvolupar amb garanties un projecte de tal magnitud. Així que el projecte va ser un fracàs absolut. Dins de l'equip de programadors de MULTICS hi havia Ken Thompson i Dennis Ritchie, que pertanyien als laboratoris Bell. Tot i el fracàs del projecte MULTICS, alguns dels programadors de Bell (entre els quals Thompson i Ritchie), van creure que encara hi havia algun esperança de crear un sistema operatiu. Així durant el 1969 van començar a pensar en alguna alternativa a MULTICS. En aquell moment l'equip dirigit per Thompson encara treballava amb el Mainframe on havien estat desenvolupant MULTICS. Veient que el desenvolupament del sofware sobre el Mainframe es tornava impracticable degut a que aquest també era usat per a tasques de l'empresa, Thompson va decidir seguir desenvolupant el nou sistema operatiu en una màquina més petita, un PDP-7 de Digital Equipment. Així que a finals del 1969 l'equip de Thompson va aconseguir executar una primera versió de UNIX (que encara no es deia així) en el PDP-7. La programació havia estat feta íntegrament en llenguatge assemblador. Veient que el nou sistema operatiu començava a funcionar, Thompson va creure convenient crear un nou llenguatge de programació que fos de més alt nivell que l’assemblador. El nou llenguatge que va crear en Thompson es va dir B, en referència al llenguatge en el que estava inspirat, el BCPL. Durant el 1970, veient que el sistema operatiu avançava, l'equip d'en Thompson va decidir adquirir una màquina més potent, un PDP-11. En aquelles mateixes dates, un altre programador de l'equip d'en Thompson, anomenat Brian Kernighan, proposava anomenar UNICS (en contraposició a MULTICS) al nou sistema operatiu d'en Thompson. Probablement, la sonoritat del nou nom del sistema operatiu va fer que amb el temps aquest derivés a UNIX. Tanmateix, el llenguatge B que havia desenvolupat Thompson, era poc adequat per a ser portat a altres màquines. Així que el 1971, Ritchie va decidir crear una extensió del llenguatge B, que va anomenar C. A partir d'aquest moment, Dennis Ritchie i Brian Kernighan van anar desenvolupant el llenguatge C, fins que a principis del 1973, l'essència del llenguatge C que coneixem estava acabada. L'estiu del mateix any, utilitzant el llenguatge i el compilador de C que havien desenvolupat Kernighan i Ritchie, l'equip de Thompson va crear el primer UNIX escrit en C. A partir d’aquí els creadors del C van seguir treballant, fins que el

Page 4: _curso de programacion en c c plus plus(Català)

Figura 1.1 Treballant en un PDP-7 1978 van publicar el que seria la primera guia sobre el llenguartge C, "The C Programming Language". La popularitat del nou llenguatge es va estendre molt ràpidament, i molt aviat, molts fabricants d'ordinadors i universitats posaren a disposició els seus propis compiladors i traductors de llenguatge C. La seva gran popularitat va venir donada gràcies a que, tot i ser un llenguatge pensat per a programar sistemes (recordem que originalment va ser creat per programar el sistema operatiu UNIX), s'adaptava molt bé a qualsevol tipus d'aplicació, tot i tenir una sintaxi senzilla (pels llenguatges del moment). La ràpida profusió de compiladors, en els quals cada fabricant incloïa les seves pròpies instruccions, va conduir a una certa falta de portabilitat dels programes que s'escrivien en C. Així que a mitjans de la dècada dels anys 80, la ANSI (American National Standards Institute) va crear el comitè WG14 amb l'objectiu d’estandarditzar el C. D'aquest comitè va sorgir l'ANSI C, norma del llenguatge que segueixen quasi tots els compiladors actuals i que també seguirem en aquest llibre. 2. Característiques del Llenguatge C Les principals característiques del llenguatge C són: Eficiència El C és un llenguatge eficient. Presenta característiques de baix nivell. Això significa que el seu disseny aprofita al màxim les possibilitats de la màquina sobre la qual s'executaran els programes. De fet, el llenguatge C incorpora alguns mecanismes que s'associen tradicionalment al llenguatge assemblador. Permetent, fins i tot, incrustar parts d’aquest llenguatge dins del mateix codi C. Així, els programes fets amb C tendeixen a ser compactes i a executar-se amb rapidesa.

Page 5: _curso de programacion en c c plus plus(Català)

Portabilitat Una de les característiques que el fan més interessant és l'elevat grau de portabilitat. Això significa que un programa escrit en C en un sistema determinat, pot executar-se en un altre sistema sense haver de fer canvis en el codi o introduint molt poques modificacions, sovint només en les capçaleres dels fitxers. Potència i flexibilitat Per una banda el C és considerat com un llenguatge potent. De fet com ja s'ha comentat, la major part del sistema operatiu UNIX està escrit en llenguatge C. Fins i tot, compiladors per altres llenguatges com FORTRAN, BASIC, LISP o Pascal també han esta escrits el llenguatge C. Per altra banda permet escriure qualsevol altre tipus d'aplicació, és a dir que no està únicament restringit a aplicacions de sistemes. Orientat al programador El C és un llenguatge que està pensat per adaptar-se a les necessitats del programador. Facilita l'accés al harwdare i dóna al programador un control enorme sobre els recursos de la màquina. En general el C és un llenguatge petit. Compta amb sentències de control senzilles i dóna suport a funcions. També posseeix una enorme selecció d'operadors, cosa que facilita la implementació d'una infinitat de tasques de forma senzilla. Totes aquestes eines donen molt de control al programador però també molta responsabilitat, ja que pot induir a errors que en altres llenguatges no es podrien donar. Així, malgrat la fama de llenguatge potent, també és veritat que se li ha atribuït una depuració d'errors difícil i que els programes poden ser molt complicats de llegir (de fet existeix un concurs de programació obtusa en C, www.ioccc.org, on es premia el codi més difícil d’interpretar. Vegeu els exemples més avall). Limitacions D'entre les limitacions del llenguatge es podrien destacar les següents. El C no és un llenguatge fortament tipat. Això pot induir a conversions entre tipus de dades bastant permissius que podrien donar lloc a errors en els resultats. Sense una programació metòdica els programes tendeixen a fer-se difícils de llegir i per tant es poden donar errors complicats de trobar i resoldre. Vegeu el codi del següent exemple:

Codi exemple 1 #define _ -F<00||--F-0--; int F=00,00=00; main(){F_00();printf("%1.3f\n",4.+-F/00/00);}F_00() { _-_-_-_-_ _-_-_-_-_ _-_-_-_-_ _-_-_-_-_ }

Codi exemple 2 #include <stdio.h> #define _ main(

Page 6: _curso de programacion en c c plus plus(Català)

#define _l ___l ___l ___l ___l ___) #define __l int #define ___l ___)*( #define ____l (_l], #define ____ 1 #define ___ __+_____ #define __ ____+____ #define _____ __+____ #define ______1 *(l__ #define _____1 *__1%(__ #define ____1 )?( #define ___1 _1&(__ __l __I[____l _I[____l*l__=_I,*l_=__I;__l _ __l _1,__l*__1){__l _l_; return ___1+ __ ____1 ___1 ____1*__1 = getchar()):__ ____1*__1<____? ____:_____1+___ ____1 _____1+_____ ____1 _____1+____)____1 ___1+___+ ____ ____1 _____1 +__)____1 _____1 )____1 *l__)++:_____1+____)-____? ______1 ++) :_____1+_____) -____?__:printf("%d\n",*l__):_____1+____+ ___ ____1 *l__) =*(l_++ ):__:___l ____- ____):_____1 +_____)-3?__-2: ____:(___1+____+___ ____1 _____1)____1*l__)-- :_____1+__)?__:______1 --):___l 0))?__:_ _1,__1+____)+____:(___1+____+___ )) &&* l__?_ ___1 +___+___+__),__1+____)?_ ___1+11 )|(___1)?____:__- 2),__1):____-1 :( _l_=_ ___1 -____+___l ____),__1+____ )) ?_l_+_ ___1 )?_1 :___1+10)|( ___1-____ ____1 __):0),__1+_l_):0):__:_1%(__ ____1 _1/(__)) ?_ scanf ("%i",__1 ____1 _____):(___l _____)-____,__1 +____):_ _____,l_):__;} És obvi que aquest estil de programació no és el més adequat per a llegir de forma clara el contingut del programa. A continuació es presenta una estructura més clara d'un programa en C. 3. Estructura d’un programa en C L’estructura bàsica d’un programa en C es podria resumir en els següents blocs:

Inclusió de fitxers de capçalera

Definició de constants

Definició de tipus de dades

Definició de funcions

Programa principal

Page 7: _curso de programacion en c c plus plus(Català)

En el següent exemple podem veure un programa senzill escrit en C. En aquest exemple només hi apareixen els blocs de: Inclusió de fitxers de capçalera, Definició de funcions i Programa principal.

/* Maxim.c Programa d’exemple: Calcula el màxim de dos nºs introduïts per pantalla */ #include <stdio.h> /* maxim: funció que retorna el més gran dels dos nºs donats */ int maxim (int x, int y) { int max; if (x > y) max = x; else max = y;

return max; } void main() {

int n, m, max;

printf (“Introdueix dos nºs:\n“); scanf (“%d”, &n);

scanf (“%d”, &m); max = maxim (n,m); printf (“El més gran dels dos nºs és: %d”, max);

}

En aquest programa podem veure els principals elements que trobarem en qualsevol programa en C: 1. Programa principal: marca el punt on comença a executar-se el programa.

L’execució del programa sempre comença amb la primera instrucció del bloc del programa principal (en l’exemple, la instrucció printf (“Introdueix dos nºs:\n“)). El programa principal ve identificat per la capçalera void main(), i sempre ha d’estar present en qualsevol programa en C. Dins del programa principal podem trobar declaracions de variables i instruccions executables.

2. Funcions: són blocs de programa independents que serveixen per fer una tasca concreta i específica (en l’exemple, calcular el màxim de dos nombres). Poden ser utilitzats des de qualsevol altre punt del programa (en l’exemple, la funció maxim es cridada des del programa principal, mitjançant la instrucció max = maxim (n,m)). En el tema 5 s’estudiaran tots els aspectes relacionats amb funcions. Fins aleshores, el programes que fem no tindran funcions, és a dir, només tindran el programa principal.

3. Inclusió de fitxers de capçalera: els fitxers de capçalera acostumen a ser fitxers que contenen declaracions i definicions externes, que són necessàries per executar el nostre programa. Normalment són fitxers que tenen extensió .h, i per poder-los utilitzar dins del programa els hem d’incloure amb la instrucció #include.

Comentaris

Inclusió de fitxers de capçalera

Definició de funcions

Programa principal

Declaració de variables

Instruccions

Page 8: _curso de programacion en c c plus plus(Català)

En l’exemple, el programa inclou les definicions que hi ha en el fitxer stdio.h. Aquest fitxer està present en qualsevol entorn de compilació en C i conté les definicions necessàries per poder utilitzar les funcions que permeten llegir i escriure valors per pantalla (scanf i printf). O sigui, que per poder utilitzar les funcions scanf i printf, hem d’incloure el fitxer de capçalera stdio.h. Al llarg d’aquest manual, anirem introduint diverses funcions que estan definides en fitxers de capçalera i ja indicarem en cada cas quin fitxer de capçalera s’ha d’incloure per poder-les utilitzar.

4. Comentaris: són qualsevol bloc de programa inclòs entre els delimitadors /* i */, o després de // si es tracta només d’una sola línia de comentari. Els comentaris són parts del programa que no s’executen. En un comentari pot haver-hi qualsevol text i serveixen per afegir explicacions i aclariments sobre el funcionament del programa, com per exemple per què serveix una funció o una variable, o explicar trossos de codi complicats. És molt important acostumar-se a posar comentaris als programes per facilitar la seva revisió i correcció.

4. Elements sintàctics d’un programa en C

Com podem veure en l’exemple anterior, en tot programa en C hi ha alguns símbols i paraules que tenen un significat especial, i són els elements propis del llenguatge C.

Símbols especials del llenguatge C La taula següent resumeix els principals caràcters i símbols que tenen algun significat particular dins d’un programa en C:

Símbol Significat # Indicador de directives de pre-compilació (#define, #include,

...). A l’apartat dedicat a la compilació s’expliquen què són les directives de pre-compilació.

/* ...... */ Delimitadors dels comentaris. Són ignorats pel compilador. ; Final de sentència. Totes les sentències han d'acabar en ; { ..... } Delimitadors d'un bloc d'instruccions que formen una unitat, com

per exemple el programa principal o les funcions.

Paraules reservades Les paraules reservades són totes aquelles paraules que tenen algun significat particular dins d’un programa en C, ja sigui com a instruccions, com a part de declaracions o com a directives de pre-compilació. A l’exemple, totes les paraules reservades apareixen en negreta. A continuació hi ha la llista de totes les paraules reservades de C:

auto do goto signed unsigned break double if sizeof void case else int static volatile char enum long struct while const extern register switch continue float return typedef default for short union Al llarg d’aquest manual s’anirà explicant el significat de la majoria d’aquestes paraules reservades.

Page 9: _curso de programacion en c c plus plus(Català)

Identificadors Els identificadors són seqüències de caracters que identifiquen coses, per exemple variables o constants. En C es poden usar caràcters alfabètics en majúscules i minúscules, així com els deu dígits decimals i el caracter subratllat “_”. Els identificadors no poden començar amb un número. Per exemple as_DE_23 és un identificador vàlid però no ho són as_%DE (ja que el caràcter “%” no està permès) ni tampoc 23_as_DE (ja que comença amb un número). És important saber que el C és sensible a majúscules i minúscules. Per tant els identificadors data_limit i Data_Limit són diferents. Sentències (Instruccions) Les sentències en C estan formades per un conjunt d’identificadors i paraules reservades separades per un espai en blanc i acabades en punt i coma. Diferents sentències es troben separades per punt i coma. Tipus de dades El llenguatge C inclou diferents tipus bàsics de dades, com poden ser int per definir numeros enters o char per definir caràcters. A partir d’aquests tipus bàsics es poden definir tipus més complexes com arrays o estructures. Constants El llenguatge C permet definir constants simbòliques que representen un determinat valor que no canvia al llarg del programa. Es defineixen mitjançant la directiva #define. Per exemple per definir la constant PI, la sentència en C seria #define PI 3.141592. Variables El llenguatge C també permet definir entitats simbòliques, anomenades variables, que si que poden canviar el valor al llarg del programa. Una variable ha d’estar sempre lligada a un tipus de dades. Per exemple si volem definir una variable de tipus enter anomenada num, la sentència en C seria int num; Operadors Els operadors de C permeten efectuar operacions amb constants i variables. Existeixen tres tipus d’operadors en C: matemàtics, lògics i relacionals. 4.1. Estil d’escriptura d’un programa en C

A l’hora d’escriure un programa en C, és molt important l’estil de codificació que utilitzem. Els programes han de ser clars i entenedors per facilitar la seva modificació i correcció. Un programador es passa moltes més hores modificant i corregint programes ja fets, que escrivint programes nous.

Page 10: _curso de programacion en c c plus plus(Català)

Com a contra-exemple agafem el següent programa (com a cas extrem podeu prendre els exemples de la pàgina XX):

#include <stdio.h> int funcioA (int x, int y){int m; if (x > y) m = x; else m = y; return m;} void main(){int n, m, a;printf (“Introdueix dos nºs:\n“);scanf (“%d”, &n); scanf (“%d”, &m);a = funcioA (n,m); printf (“El més gran dels dos nºs és: %d”, a);}

Aquest programa és sintàcticament correcte, es pot compilar i executar i fa exactament el mateix que el primer exemple. Evidentment, a simple vista i sense necessitar cap noció de programació, podem veure que és molt més difícil d’entendre i saber què fa el programa, tal com està escrit en aquesta segona versió que en la primera. Algunes regles que cal seguir a l’hora d’escriure un programa són les següents: • Separar de forma clara els diferents blocs del programa (funcions, programa

principal, declaracions de variables, etc.).

• Utilitzar comentaris per explicar les funcions, variables o parts del programa que puguin ser confoses. Per l’altre extrem, hem d’evitar omplir el programa de comentaris superflus, com per exemple el següent: /* si x és més gran que y assignem a max el valor de x. Si no, assignem a max el valor de y. */ if (x > y) max = x; else max = y;

• Utilitzar les tabulacions i alineacions per fer més clar el programa. Totes les instruccions d’un mateix bloc haurien d’estar alineades, i els símbols { i } que es corresponguin haurien d’estar també alineats.

• Utilitzar noms de variables i funcions que tinguin a veure amb la utilització que se’n fa. Per exemple, si una funció calcula el màxim de dos nombres, és molt millor anomenar-la maxim que funcioA.

Els exemples que hi ha al llarg d’aquest manual poden servir com a referència de les regles d’estil que s’han de seguir al escriure programes en C. 5. Compilació d’un programa en C El llenguatge C és un llenguatge compilat. Això vol dir que per poder executar un programa escrit en C, primer l’hem de traduir a llenguatge màquina, que és l’únic llenguatge que l’ordinador pot entendre directament. El procés de traduir un programa escrit en llenguatge C a codi màquina executable per l’ordinador és el que s’anomena com a compilació. A la figura següent s’il·lustra el procés de compilació d’un programa en C en el cas general que el programa (com sol ser habitual) estigui dividit en diversos fitxers de codi font (fitxers amb extensió .c). En el cas de

Page 11: _curso de programacion en c c plus plus(Català)

programes senzills, tot el programa estarà en un sol fitxer de codi font, i el procés serà exactament el mateix, però només amb un fitxer involucrat en tot el procés.

Figura xx. Exemple .....

La figura mostra que el procés de compilació en realitat consta de tres passos diferents: pre-compilació, compilació i linkatge: • Pre-compilació: el pre-compilador s’encarrega de processar tot el que són

instruccions (directives) de pre-compilació, que com s’ha explicat anteriorment, són totes aquelles instruccions precedides pel símbol # (#define, #include). La directiva de pre-compilació més utilitzada és la instrucció #include, que com ja s’ha explicat, serveix per incorporar dins del nostre programa declaracions de funcions que són necessàries pel programa i que estan en fitxers de capçalera amb extensió .h (com per exemple scanf i printf). El resultat de la pre-compilació és el mateix fitxer de codi font (amb extensió .c), modificat per incloure les definicions que hi ha en els fitxers de capçalera.

• Compilació: aquest és el pas de traducció pròpiament. El compilador analitza el codi font escrit en C i el tradueix al codi màquina que pot entendre l’ordinador. En

.c.h.c.c .h

PRE-COMPILADOR

COMPILADOR

.o.o.o

.lib

.lib ENLLAÇADOR (Linker)

.exe

.c.c.c

Page 12: _curso de programacion en c c plus plus(Català)

el procés de traducció comprova que el programa en C sigui sintàcticament correcte, és a dir que segueixi totes les regles del llenguatge C. En el cas que trobi errors, para el procés de traducció i avisa de quins errors ha trobat i en quina línia de codi estan, perquè es puguin corregir. Més endavant es comenten amb detall els possibles tipus d’errors que es poden produir en un programa en C. El resultat d’aquest pas de compilació és un fitxer de codi objecte (extensió .o ó .obj) per cada fitxer de codi font que es processa.

• Linkatge: aquest últim pas s’encarrega d’ajuntar tots els fitxers de codi objecte que s’han generat al pas de compilació, en un únic fitxer executable amb extensió .exe. A més a més, el procés de linkatge també incorpora tot el codi necessari de les llibreries del sistema. Les llibreries del sistema són fitxers que existeixen en tot entorn C, que contenen el codi objecte ja compilat de les funcions pre-definides en els fitxers de capçalera (com per exemple, les funcions printf i scanf). Durant el procés de linkatge també es poden produir errors, que estan descrits més endavant.

Degut a que aquestes tres operacions es repeteixen diverses vegades durant el desenvolupament d’un programa, molts fabricants de software proporcionen entorns de desenvolupament en els quals es pot realitzar tot el cicle complert, des de l’edició del programa font fins al seu linkatge passant per la compilació. Aquests sistemes es denominen Entorns de Desenvolupament Integrat (IDE, Integrated Development Environment). Existeixen diferents IDE per al desenvolupament de programes en C. 6. Entorns de Desenvolupament Integrat (IDE) Els entorns de desenvolupament integrat, també anomenats IDEs (segons les seves sigles en anglès), són aplicacions que contenen tota una sèrie d'eines de programació agrupades dins una interfície gràfica (GUI: Graphical User Interface). Algunes d'aquestes eines són: un editor de codi, un compilador, un constructor i un depurador entre d'altres. Els IDEs, per tant, constitueixen un marc de treball amigable per a que el programador pugui dur a terme els seus projectes. Hi ha gran quantitat d'IDEs disponibles. En aquest llibre s'introdueixen dos dels més comuns: • Visual Studio 2005. És un IDE desenvolupat per Microsoft, que requereix

llicència i que està especialment enfocat a sistemes Windows.

• Dev-C++ 4.9.8.0. Software lliure (GNU) desenvolupat per Bloodshed. Es pot

descarregar de la pàgina: http://www.bloodshed.net/devcpp.html

Page 13: _curso de programacion en c c plus plus(Català)

Les característiques d'aquests IDEs s'aniran comentant de forma simultània per poder veure les diferències entre ells. Els IDEs donen la oportunitat de gestionar, de forma còmode, diversos elements que combinats constitueixen els programes. Els principals són: • Fitxer: És l'element més bàsic. Conté dades i instruccions escrites en un cert

llenguatge pel programador. Aquest llenguatge serà C o C++ (C és un subconjunt de C++) per al Dev-C++, però en el cas de Visual Studio 2005, a més a més se'n poden considerar d'altres com ara Java, ASP.NET i Basic .NET.

• Projecte: Tots els fitxers que formen part d'un programa o aplicació que porta a

terme una tasca de certa complexitat, s'agrupen en el que es coneix com a projecte. Els projectes tenen sentit per sí mateixos, és a dir, poden ser executats.

El Visual Studio 2005 permet, a més a més, considerar un element jeràrquicament superior: • Solució: Pot haver-hi ocasions en què es requereix unir varis projectes

possiblement desenvolupats paral·lelament, dins una entitat superior anomenada solució. Això permet resoldre problemes de gran complexitat.

La següent figura mostra la jerarquia dels diversos elements en Visual Studio 2005 i Dev-C++:

a)

b)

Figura 1. a) Jerarquia d'elements dins el Visual Studio 2005. Dins una solució poden haver-hi varis projectes que, al seu torn, contenen varis fitxers. b) El Dev-C++, per altra banda, tan sols dóna la oportunitat de considerar projectes formats per varis fitxers. En aquest llibre es deixaran de banda les solucions considerant, tan sols, projectes i fitxers. Tant per al Visual Studio 2005 (VS) com per al Dev C++ (DC), tots els fitxers d'un determinat projecte es guarden en un directori específic i tota la informació relativa al projecte, es guarda en un determinat fitxer. Obrint aquest fitxer s'obté, de forma automàtica, accés a tot el projecte, és a dir, a tots els fitxers que el formen. En el cas del VS, aquest fitxer té extensió .sln, mentre que en DC, és .dev.

Page 14: _curso de programacion en c c plus plus(Català)

Dins els projectes poden haver-hi fitxers de varis tipus però, de tots ells, els únics sobre els quals treballarem seran: • Fitxers de codi font (.c o .cpp). Contenen instruccions escrites en lleguatge C. • Fitxers de capcelera (.h). Contenen declaracions de funcions pre-definides que

ha d’utilitzar el programa, així com la definició de constants i /o nous tipus de dades etc.

6.1. Creació de projectes Cada vegada que s'hagi de començar un nou programa (per petit que sigui), el primer que s'ha de fer és crear un nou projecte que inicialment estarà buit de contingut. A mesura que es va desenvolupant el projecte, s'aniran afegint tots els fitxers de codi font i de capçalera que siguin necessaris. La metodologia per crear nous projectes varia d'una IDE a una altra: 6.1.1. Creació d’un projecte en Visual Studio 2005 Per crear un projecte en VC, s'han de seguir els següents passos: • Dins el menú principal triem la opció Archivo → Nuevo→ Proyecto. • Triem el tipus de projecte: Win32 i el tipus de plantilla: Aplicación de Consola

Win32 (Fig. 2 ) • Especifiquem el nom i la ubicació del projecte. • Dins el diàleg de Asistente para aplicaciones Win32, triem la opció Configuración

de la aplicación i, dins Opciones adicionales, marquem el checkbox Preyecto vacío (Fig. 3).

Figura 2

Page 15: _curso de programacion en c c plus plus(Català)

Figura 3 Un cop realitzats aquests passos, s’haurà creat un projecte buit en el directori especificat. En aquest punt la GUI mostrarà una aparença similar a la de la figura 4. Aquest és l’entorn habitual amb el que es treballa per editar, compilar i executar els programes en VS. La part central és l'area de treball, és a dir, on s'edita el codi. Atès que el projecte és buit, en aquesta part encara no hi apareix res. A la part inferior apareix una àrea amb diverses pestanyes, de les quals tan sols usarem la de Resultados. Aquí es mostren els missatges i/o errors relacionats amb el procés de compilació i execució.

Figura 4 A l'esquerra s'observa una finestra vertical que conté 3 pestanyes a la part inferior. Seleccionarem la que posa: Explorador de Soluciones. Fixeu-nos que per defecte el projecte forma part d'una solució que porta el mateix nom. De la carpeta del projecte,

Page 16: _curso de programacion en c c plus plus(Català)

en surten 3 carpetes més: Archivos de código fuente, Archivos de encabezado i Archivos de recursos. A la primera carpeta s'hi afegeixen arxius amb extensió .c o .cpp (almenys un), mentre que a la segona hi aniran arxius amb extensió .h (si el programa ho requereix). La darrera carpeta, simplement la ignorarem. Tant a la carpeta de fitxers de codi font, com a la de fitxers de capçalera, s'hi poden afegir fitxers ja existents o nous (buits). En el primer cas se suposa que aprofitem fitxers escrits amb anterioritat (per nosaltres o algú altre), mentre que en el segon cas s'ha de començar de zero. Per afegir fitxers tenim dues formes de fer-ho: • Des del menú principal mitjançant l'opció Proyecto. • Prement el botó contextual (dret) del ratolí sobre la carpeta a la qual es vol

incloure el fitxer, i triant la opció Agregar (Fig. 5).

Figura 5. Qualsevol d'aquestes alternatives permetrà accedir a les opcions Nuevo elemento i Elemento existente. En el primer cas apareixerà el diàleg Agregar nuevo elemento (Fig.6), que permetrà especificar el nom i el tipus del fitxer que es vol crear, mentre que en el segon cas apareixerà la finestra Agregar elemento existente per poder seleccionar el fitxer existent.

a)

b) Figura 6

Page 17: _curso de programacion en c c plus plus(Català)

6.1.2. Creació d’un projecte en Dev C++ Per crear un projecte en DC, s'han de seguir els passos següents: • Dins el menú principal triem la opció Archivo → Nuevo→ Proyecto. Sortirà un

diàleg anomenat Nuevo Proyecto. • A la pestanya Basic, triarem el tipus de projecte: Empty Project. • Dins Opciones de Proyecto, triem C com a tipus de llenguatge i nombrem el

projecte. (Fig. 7). • Apareixerà un diàleg anomenat Create new project mitjançant el qual triarem la

carpeta on volem guardar el projecte. Si no existeix, l'haurem de crear.

Figura 7 Un cop realitzats aquests passos, s’haurà creat un projecte buit en el directori amb el nom que haguem especificat, i la GUI mostrarà una aparença similar a la de la figura 8. Aquest és l’entorn habitual amb el que es treballa per editar, compilar i executar els programes en DC. La part central és l’àrea de treball que, atès que el projecte és buit, no hi apareix res. A l'esquerra s'observa una finestra vertical que conté 3 pestanyes a la part superior. Ens centrem en la que posa: Proyecto, dins la qual hi ha una icona representant el projecte. A la part inferior apareixen diverses pestanyes. La que posa Compilador mostrarà missatges sobre l'estat de la compilació quan es porta a terme.

Page 18: _curso de programacion en c c plus plus(Català)

Figura 8 Per afegir fitxers de codi font o de capçalera, ho podem fer mitjançant una de les següents opcions: • Des del menú principal, mitjançant la opció Proyecto • Prement el botó contextual del ratolí sobre la carpeta que representa el projecte

a la finestra de l'esquerra. s'accedeix a: • Nuevo Código Fuente, que us crea un nou fitxer que haurem de renombrar (botó

esquerre sobre la nova icona) posant un nom que acabi en .c si es tracta d'un fitxer de codi font o en .h si es tracta d'un fitxer de capçalera.

• Añadir a Proyecto, que permet navegar pel disc dur i triar fitxers de codi font o de

capçalera ja existents.

Page 19: _curso de programacion en c c plus plus(Català)

6.2. Compilació i execució de projectes 6.2.1. Compilació i execució de projectes en Visual Studio 2005 Totes les opcions de compilació dels programes estan dins del menú Generar. Desplegant aquest menú apareixen les següents opcions (Fig. 9):

Figura 9 • Compilació del fitxer actual (Compilar): compila únicament el fitxer que

tinguem actiu a la finestra d’edició. • Compilació i linkatge de tot el projecte (Generar): compila tots els fitxers

associats al projecte i fa el linkatge per obtenir el fitxer executable final. Només compila els fitxers que s’hagin modificat des de l’última compilació.

• Re-compilació i linkatge de tot el projecte (Volver a generar): fa el mateix que

l’opció anterior, amb la diferència que compila tots els fitxers del projecte, independentment que s’hagin modificat o no des de l’última compilació.

Per altra banda, l'opció d'execució del programa, es troba dins el menú Depurar juntament amb les opcions de depuració que especificarem més endavant.

Figura 10

Page 20: _curso de programacion en c c plus plus(Català)

• Execució del programa (Iniciar sin depurar): executa el fitxer executable que

s'ha generat després de compilar i linkar. 6.2.2. Compilació i execució de projectes en Dev-C++ Totes les opcions de compilació i execució dels programes estan dins del menú Ejecutar. Desplegant aquest menú apareixen les següents opcions (Fig. 11):

Figura 11 • Compilació de tot el projecte (Compilar): compila i fa el linkatge de tots els

fitxers associats al projecte. • Compilació del fitxer actual (Compilar el archivo actual): compila i fa el linkatge

únicament el fitxer que tinguem actiu a la finestra d’edició. • Execució de tot el projecte (Ejecutar): executa el fitxer executable obtingut de

la última compilació i linkatge. • Compilació i Execució de tot el projecte (Compilar y ejecutar): compila, fa el

linkatge i executa tot el projecte. 6.3. Detecció i depuració d'errors Un programa mai funcionarà correctament a la primera. Quan s’escriu el codi d’un programa és fàcil que s’introdueixin errors, que podem dividir en tres classes: • Errors de compilació. • Errors de linkatge. • Errors d’execució.

Page 21: _curso de programacion en c c plus plus(Català)

6.3.1. Errors de compilació Els errors de compilació es produeixen quan apareix un error sintàctic, és a dir, el programa que s’ha escrit no s’ajusta estrictament a les regles del llenguatge C. Alguns exemples d’aquests tipus d’error podrien ser: escriure una instrucció de forma incorrecta, deixar-nos de posar un punt i coma al final d’una instrucció, no tancar un parèntesi, no declarar una variable, etc. Aquest tipus d’errors es detecta a la fase de compilació. Si el compilador detecta errors de compilació, ja no passa a la fase de linkatge i per tant, no genera cap fitxer executable. En la majoria d'IDEs, si durant la fase de compilació s'han detectat errors, aquests es mostren en un llistat que descriu aproximadament la seva naturalesa. A més a més, fent clic sobre els elements d'aquesta llista, l'IDE dóna una idea aproximada de la seva localització mitjançant una marca al començament de la línia on probablement s'hagi produït. S’ha de tenir en compte que no sempre el missatge serà suficientment explicatiu de l’error que hi ha al programa. A més a més pot ser que encara que ens indiqui una certa línia de codi, l'error s’hagi produït realment en alguna línia anterior o posterior. Amb la pràctica es va guanyant experiència per interpretar correctament els missatges d’error del compilador. En el cas del VS i el DC, aquesta llista d'errors es mostra a la part inferior de la GUI dins les pestanyes Resultados i Compilador respectivament (Fig. 12). Hi ha dos tipus de missatges de compilació: les advertències (o warnings) i els errors: • Les advertències (warnings) són "errors" que no impedeixen la compilació

complerta del programa. En aquest tipus d’error el compilador mostra un missatge, però continua el procés de compilació i de linkatge i pot generar el fitxer executable. Simplement ens avisa que hi ha quelcom estrany que potencialment pot ser font d’algun error, encara que no necessàriament ho ha de ser. Aquests errors s’han d’analitzar i decidir si s’han de corregir o no.

• Els errors són els que impedeixen que es passi a la fase de linkatge i es pugui generar el fitxer executable. Els errors s’han de corregir sempre.

a)

b)

Figura12. Finestres que mostren la llista d'error sintàctics detectats pel compilador per: a) Visual Studio 2005 i b) Dev-C++.

Page 22: _curso de programacion en c c plus plus(Català)

6.3.2. Errors de linkatge Són menys habituals que els errors de compilació i típicament es produeixen quan no es pot accedir a algun fitxer de codi font o llibreria que conté dades i/o funcions que utilitzem en el nostre programa. 6.3.3. Errors d’execució Són els errors que es produeixen mentre s’està executant el programa, és a dir, el programa s’ha compilat i linkat sense errors, però no funciona correctament, és a dir, no fa el que pretenem que faci. Aquests són els errors més difícils de detectar i corregir ja que no apareix cap missatge d'error ni cap indici d'on pot estar localitzat. L’única forma d’assegurar-nos que el programa no té errors és provant totes les seves opcions amb tots els casos possibles d’entrada de dades i comprovar si el resultat obtingut és l’esperat. És a dir, s’ha de portar a terme un testeig exhaustiu que consti d’un bon joc de proves. Per tant no n’hi ha prou en executar el programa un sol cop per assegurar-ne el bon funcionament. Una vegada s'ha detectat la presència d'errors, el següent pas és corregir-los. La correcció d’aquests errors no és fàcil perquè tampoc tenim cap indicació directa de la seva localització. Per trobar-los i corregir-los s’ha de fer un seguiment detallat del programa tot executant les instruccions pas a pas i observant el valor que van prenent les diverses variables per tal d’arribar a deduir què és el que està malament. Aquest procés és el què es coneix com a depuració (debugging) del programa. 6.3.4. Depuració d’errors en Visual Studio 2005 Les següents són les principals opcions de depuració que farem servir en el VC: • Execució d'instruccions pas a pas ingorant funcions. Es pot accedir a

aquesta opció mitjançant la tecla de funció F10 o el menú Depurar→ Paso a paso por procedimientos. Apareixerà una fletxa groga al cantó esquerra del codi, indicant la instrucció que està a punt d'executar-se. Prement F10 s'executa i s'accedeix a la següent instrucció. Si la instrucció és una crida a una altra funció, s'executa la crida sense entrar-hi.

• Execució d'instruccions pas a pas entrant dins les funcions. Es pot accedir

a aquesta opció mitjançant la tecla de funció F11 o el menú Depurar→ Paso a paso por instrucciones. Essencialment fa el mateix que l'opció anterior, però quan s'executa sobre una instrucció que és una crida a una altra funció, el flux d'execució salta a l'interior de la funció en qüestió. Allà es pot continuar l'execució pas a pas de forma normal i, una vegada s'arriba al final de la funció, el flux es restaura al punt on s'havia fet la crida.

Cadascuna d'aquestes opcions es pot dur a terme des de la primera línia del programa, o a partir d'un cert punt que el programador consideri oportú. En aquest últim cas, s'haurà de "marcar" la línia de codi d'interès. La manera de fer això és amb els anomenats: • Punts d'interrupció (Breakpoints). Per posar-los en una línia concreta del codi,

primer ens hem de posicionar sobre la línia amb el cursor i després amb la opció

Page 23: _curso de programacion en c c plus plus(Català)

de menú Depurar->Alternar puntos de interrupción o la tecla de funció F9, activar-lo. De punts d'interrupció, se’n poden posar tants com es vulgui d'igual forma. Si es fa aquest procés sobre una línia on ja hi havia un punt de control, s'elimina. Si es volen eliminar tots els punts de control, es pot fer mitjançant Depurar→ Eliminar todos los puntos de interrupción o bé Ctrl.+Mayús.+F9.

Per poder executar el programa de forma automàtica i fer que es pari en el primer breakpoint, hem d'emprendre l'execució mitjançant Depurar→ Iniciar depuración o bé prement F5. Una vegada el flux d'execució es para sobre el breakpoint, podem continuar la depuració mitjançant una de les opcions de depuració pas a pas descrites anteriorment (F10 o F11). Tornant a prémer F5, el programa s'executarà de forma automàtica fins que trobi el següent breakpoint, o bé fins al final en cas que no n'hi hagi cap altre. Tant si hem optat per debugar des del principi del programa, com si ho fem a partir d'un cert punt, sempre podem accedir i fer ús de les següents opcions: • Visualització del valor que prenen les diverses variables. Quan entrem en

mode debug, a la part inferior apareixen noves finestres entre les qual hi ha les d'inspecció. Aquestes permeten introduir noms de variables per poder fer el seguiment dels valors que van adquirint al llarg de la execució. En cas de no veure's, aquesta finestra es pot activar a través de la opció de menú Depurar→ Ventanas→ Inspección.

• Sortir del mode de depuració. En qualsevol moment podem aturar el mode de

depuració mitjançant l'opció de menú Depurar→ Detener depuración, o bé amb amb la combinació de tecles Mayús.+F5.

Page 24: _curso de programacion en c c plus plus(Català)

Tema 2: Tipus de dades simples 1. Variables 1.1. Concepte de variable Una variable es pot definir com una posició de memòria on es pot guardar un valor d’un tipus determinat, que pot ser modificat des del codi del programa. Una variable queda identificada per un nom, que ens permet referenciar-la i utilitzar-la dins del programa, i per un tipus de dades, que ens indica quins valors podem guardar a la variable i quines operacions podem fer amb ella. 1.2. Tipus de dades bàsics en C En C hi ha 4 tipus de dades bàsics: enter, real, caràcter i lògic. Cal comentar, però, que el tipus de dades lògic va ser introduït a l’estàndard C99 i no tots els compiladors de C el suporten. El tipus de dades determina el tamany de memòria que ocupa una variable i, per tant, el rang de valors que s’hi pot guardar. També determina quins operadors es poden aplicar a les variables. El tipus enter: int Serveix per guardar valors numèrics de tipus enter. Hi ha diferents variants del tipus enter. Per una banda podem indicar si volem guardar nombres amb signe (positius i negatius) o sense signe (només positius). En el primer cas el tipus s’especifica com a signed int, mentre que en el segon cas com a unsigned int. Per defecte, el tipus int representa nombre amb signe. Per altra banda podem també especificar el tamany de memòria que volem reservar per guardar el nombre enter (i per tant, el rang de valors de la variable). Si volem treballar amb nombre enters més petits utilitzarem el tipus short int i si necessitem nombres enters més grans utilitzarem long int o long long int, depenent del rang de valors. El tamany exacte de memòria que s’utilitza en cada cas depèn de la versió concreta del llenguatge. Totes aquestes variants es poden utilitzar conjuntament per formar qualsevol combinació possible de nombres amb signe o sense signe, petits o grans. La taula següent resumeix totes les variants de tipus enter amb el tamany de memòria que ocupen i el rang de valors possibles1.

1 Els valors de tamany i rang de valors s’han obtingut amb el compilador de Visual Visual Studio .NET 2003 en un entorn Windows XP

Page 25: _curso de programacion en c c plus plus(Català)

Resum del tipus de dades enter Identificador de tipus Tamany Rang de valors short / short int 2 bytes Min -32.768 Max 32.767 int 4 bytes Min -2.147.483.648 Max 2.147.483.647 long / long int 4 bytes Min -2.147.483.648 Max 2.147.483.647 long long / long long int 8 bytes Min -9.223.372.036.854.775.808 Max 9.223.372.036.854.775.807 unsigned short / 2 bytes Min 0 unsigned short int Max 65535 unsigned / unsigned int 4 bytes Min 0 Max 4.294.967.295 unsigned long / 4 bytes Min 0 unsigned long int Max 4.294.967.295 unsigned long long / 8 bytes Min 0 unsigned long long int Max 18.446.744.073.709.551.615

El tipus real: float / double S’utilitza per guardar valors numèrics de tipus real. Igual que passa amb el tipus enter hi ha diferents variants del tipus real (float, double, long double) que es diferencien en el tamany de memòria que ocupen (i per tant, en el rang de valors possibles que poden representar), en la precisió i en el nº de decimals que s’utilitzen per representar el nombre real. El tipus real es pot utilitzar també per guardar nombres enters que siguin més grans que el valor màxim que permet representar el tipus long long int. El format en què normalment s’expressen els nombres reals és una forma especial de la notació científica habitual. Així per exemple, 3.5·108 s’expressa com 3.5E+08. La taula següent resumeix la precisió, tamany i rang dels diferents tipus de dades reals2. Resum del tipus de dades real Tipus Tamany Precisió Nº decimals Rang de valors float 4 bytes 1.19209E-007 6 Min 1.175494E-038 Max 3.402823E+038double 8 bytes 2.22045E-016 15 Min 2.225074E-308 Max 1.797693E+308long double 8 bytes 2.22045E-016 15 Min 2.225074E-308 Max 1.797693E+308

2 Els valors de tamany, precisió i rang de valors s’han obtingut amb el compilador de Visual Visual Studio .NET 2003 en un entorn Windows XP

Page 26: _curso de programacion en c c plus plus(Català)

El tipus caràcter: char S’utilitza per guardar caràcters alfanumèrics, és a dir lletres, dígits, signes de puntuació, etc. Ocupa sempre 1 byte de memòria. Els caràcters es codifiquen amb un valor numèric segons la codificació ASCII que mostra la taula que hi ha a continuació. Aquest valor numèric és el que es guarda a la posició de memòria de la variable. El C aprofita aquesta codificació numèrica i permet tractar les variables de tipus char també com si fóssin nombres enters d’un byte de tamany. D’aquesta manera una variable de tipus char també es pot utilitzar per representar un nombre enter en el rang de -128 a 127 en el cas de nombres amb signe i de 0 a 255 en el cas de nombres sense signe (unsigned char). D’aquesta manera una variable de tipus char es pot tractar com un caràcter o com un nombre enter. Codificació ASCII

codi char codi char codi char codi char codi char codi char codi char32 64 @ 96 ` 128 € 160 192 À 224 à 33 ! 65 A 97 a 129 � 161 ¡ 193 Á 225 á 34 " 66 B 98 b 130 ‚ 162 ¢ 194  226 â 35 # 67 C 99 c 131 ƒ 163 £ 195 à 227 ã 36 $ 68 D 100 d 132 „ 164 ¤ 196 Ä 228 ä 37 % 69 E 101 e 133 … 165 ¥ 197 Å 229 å 38 & 70 F 102 f 134 † 166 ¦ 198 Æ 230 æ 39 ' 71 G 103 g 135 ‡ 167 § 199 Ç 231 ç 40 ( 72 H 104 h 136 ˆ 168 ¨ 200 È 232 è 41 ) 73 I 105 i 137 ‰ 169 © 201 É 233 é 42 * 74 J 106 j 138 Š 170 ª 202 Ê 234 ê 43 + 75 K 107 k 139 ‹ 171 « 203 Ë 235 ë 44 , 76 L 108 l 140 Œ 172 ¬ 204 Ì 236 ì 45 - 77 M 109 m 141 � 173 205 Í 237 í 46 . 78 N 110 n 142 Ž 174 ® 206 Î 238 î 47 / 79 O 111 o 143 � 175 ¯ 207 Ï 239 ï 48 0 80 P 112 p 144 � 176 ° 208 Ð 240 ð 49 1 81 Q 113 q 145 ‘ 177 ± 209 Ñ 241 ñ 50 2 82 R 114 r 146 ’ 178 ² 210 Ò 242 ò 51 3 83 S 115 s 147 " 179 ³ 211 Ó 243 ó 52 4 84 T 116 t 148 " 180 ´ 212 Ô 244 ô 53 5 85 U 117 u 149 • 181 µ 213 Õ 245 õ 54 6 86 V 118 v 150 – 182 ¶ 214 Ö 246 ö 55 7 87 W 119 w 151 — 183 · 215 × 247 ÷ 56 8 88 X 120 x 152 ˜ 184 ¸ 216 Ø 248 ø 57 9 89 Y 121 y 153 Ö 185 ¹ 217 Ù 249 ù 58 : 90 Z 122 z 154 š 186 º 218 Ú 250 ú 59 ; 91 [ 123 { 155 › 187 » 219 Û 251 û 60 < 92 \ 124 | 156 œ 188 ¼ 220 Ü 252 ü 61 = 93 ] 125 } 157 � 189 ½ 221 Ý 253 ý 62 > 94 ^ 126 ~ 158 ž 190 ¾ 222 Þ 254 þ 63 ? 95 _ 127 � 159 Ÿ 191 ¿ 223 ß 255 ÿ

Page 27: _curso de programacion en c c plus plus(Català)

El tipus lògic: _Bool Permet guardar valors lògics, és a dir cert (true) o fals (false). S’utilitza bàsicament per saber si una determinada condició es compleix o no. Aquest tipus va ser introduït en l’estàndard C99 i no tots els compiladors els suporten. En concret, el compilador de DevC++ sí que l’admet, però el compilador de Visual Studio, no. En els entorns de compilació on el tipus lògic no existeix, es pot representar utilitzant una variable de tipus enter o caràcter. En aquest cas el valor 0 representa fals i qualsevol valor diferent de 0 representa cert. 1.3. Declaració de variables Totes les variables que s’utilitzen en un programa s’han de declarar abans de ser utilitzades. En la declaració d’una variable especifiquem el nom i el tipus, de manera que a partir d’aquell punt, el compilador coneix l’existència de la variable i sap com es pot utilitzar (valors i operacions permeses). La sintaxi per declarar variables és la següent: Declaració de variables

<tipus_dades> <nom_variable>; <tipus_dades> <nom_variable>, <nom_variable>, ... ;

El tipus de dades pot ser qualsevol dels que s’ha explicat a la secció anterior3. El nom de la variable haurà de ser un identificador vàlid segons les regles que s’expliquen a la secció 3. En una sola instrucció podem declarar una sola variable o diferents variables del mateix tipus. Exemple /* Declaració d’una variable de tipus enter per guardar el nombre d’alumnes d’una classe */ unsigned int nAlumnes; /* Declaració de tres variables de tipus real per guardar numerador, denominador i resultat d’una divisió */ float numerador, denominador, resultat;

/* Declaració d’una variable de tipus caràcter per guardar una lletra */ char lletra;

1.4. Inicialització de variables. La declaració d’una variable permet reservar un espai de memòria suficient per guardar la variable associar-la amb un tipus de dades determinat, però no li assigna cap valor. El valor inicial que té la variable és el valor que hi ha guardat en aquest espai de memòria, que és totalment indeterminat. Per tant, el primer que s’ha de fer sempre és inicialitzar la variable per assignar-li un valor concret. És imprescindible inicialitzar les variables amb un valor controlat abans d’utilitzar-les. Si no, el resultat de l’execució del programa pot ser totalment imprevisible. 3 En capítols posteriors s’introduiran nous tipus de dades que també podran ser utilitzats per declarar variables.

Page 28: _curso de programacion en c c plus plus(Català)

Hi ha tres maneres d’inicialitzar una variable: 1. Assignar un valor a la variable després de la declaració. Per exemple:

char c; unsigned short i; long n, m; c = ‘a’; /* Inicialització amb constant de tipus caràcter */ i = 10; /* Inicialització amb constant de tipus enter*/ n = -1000000; /* Inicialització amb constant de tipus enter negativa */ m = n*2; /* Inicialització amb expressió que calcula un enter utilitzant una variable prèviament inicialitzada */

2. Assignar un valor a la variable en el mateix moment de la declaració. Per

exemple:

float x = 2.5; /* Declaració i inicialització d'una variable real */ int n = 10; /* Declaració i inicialització d’una variable de tipus enter */ char c = ‘a’; /* Declaració i inicialització d’una variable de tipus caràcter */

Fixem-nos que aquest tipus d’inicialització només es pot utilitzar si sabem el valor que li hem de donar a la variable quan estem escrivint el programa. Si el valor inicial de la variable només es pot saber mentre s’executa el programa haurem de fer servir obligatòriament la primera forma d’inicialització.

3. Assignar a la variable un valor introduït per l’usuari (veure les instruccions d’entrada / sortida, explicades més endavant). Per exemple:

char c; int n; scanf (“%c”, &c); /* Inicialització amb un caràcter que introdueix l’usuari per teclat */ scanf (“%d”, &n); /* Inicialització demanant a l’usuari que introdueixi un nombre enter */

Com es pot veure en tots aquests exemples les variables es poden inicialitzar utilitzant constants del tipus de dades corresponent a la variable o utilitzant qualsevol dels operadors i expressions que s’expliquen més endavant a la secció 5. 2. Constants 2.1. Concepte de constant Una constant també es pot definir com una posició de memòria on podem guardar un valor d’un tipus determinat, però a diferència de les variables, aquest valor ja no es pot modificar un cop inicialitzat. Les constants serveixen doncs, per guardar valors que no volem que siguin modificats durant l’execució del programa. Definint-los com a constants evitem que per error siguin modificats en alguna part del

Page 29: _curso de programacion en c c plus plus(Català)

programa. Si intentem posar una instrucció que modifica el valor d’una constant, el compilador ens donarà un missatge d’error. 2.2. Declaració de constants La declaració de constants es fa igual que la declaració i inicialització d’una variable, però afegint la paraula reservada const al davant del tipus de dades: Declaració de constants

const <tipus_dades> <nom_variable> = <valor>;

Exemples de valors que poden ser declarats com a constants podrien ser els que es mostren en l’exemple següent: el nombre π (en un programa que faci càlculs matemàtics); el nombre màxim d’alumnes que es poden matricular a una assignatura o la tecla que s’ha de prémer per sortir d’un programa. La declaració d’aquestes constants seria de la següent forma: Exemple de declaració de constants

const float pi = 3.141592; const int maxAlumnes = 100; const char teclaSortida = ‘S’;

Fixem-nos com s’especifiquen els valors constants de tipus caràcter: estan sempre delimitats pel caràcter ‘. 3. Identificadors de variables i constants No qualsevol combinació de caràcters (lletres, dígits i signes de puntuació) és vàlida per construir noms de constants, variables i funcions. Hi ha un conjunt de regles, que s’especifiquen a continuació, que s’han de seguir per formar tots els identificadors. Si no es compleixen aquestes regles, el compilador donarà un error: 1. Els identificadors poden estar formats per qualsevol combinació de lletres

(majúscules i minúscules), dígits (0-9) i el caràcter del subratllat ( _ ). 2. Qualsevol altre caràcter està prohibit. 3. El primer caràcter de l’identificador ha de ser una lletra o el carácter del

subratllat. No pot ser un dígit. 4. No es poden utilitzar com a identificadors les paraules reservades de C. 5. Els identificadors són sensibles a majúscules i minúscules: els mateixos

caràcters en majúscules i minúscules formen identificadors diferents. Per exemple, area_cercle, Area_Cercle i AREA_CERCLE són identificadors diferents.

Exemples d’identificadors

Identificador vàlids Identificadors invàlids area_cercle 1x _area_cercle Area Cercle AreaCercle_1 Area#Cercle x1 x:

Page 30: _curso de programacion en c c plus plus(Català)

4. Entrada / Sortida bàsica Els programes en C es poden comunicar amb l’usuari escrivint dades per pantalla o llegint dades introduïdes des del teclat. La forma més simple de fer-ho és utilitzar un conjunt de funcions de la llibreria estàndard definides dins del fitxer de capçalera stdio.h. Per tant, per poder utilitzar aquestes funcions, haurem de posar sempre en els nostres programes la línia #include <stdio.h>. A continuació veurem primer com utilitzar la funció printf que ens permet escriure dades per pantalla i després explicarem la funció scanf que permet que l’usuari del programa introdueixi valors per teclat i guardar-los en alguna de les variables del programa. 4.1. Sortida bàsica: funció printf La funció printf és la que ens permet imprimir qualsevol cosa en pantalla, ja sigui un missatge de text, o el valor d’una variable. La sortida que obtenim és en mode text i seqüencial, és a dir, s’imprimeix sempre a continuació de l’últim missatge que s’hagi escrit prèviament. Escriptura de missatges de text La utilització més simple de la funció printf és per imprimir missatges de text. En aquest cas la forma d’utilitzar aquesta funció és la següent: Utilització de la funció printf printf (“missatge_de_text”); En el text a imprimir amb la funció printf podem incloure el que s’anomenen seqüències de “escape”, que serveixin per imprimir alguns caràcters especials o indicar certes seqüències de control com per exemple, saltar a la línia següent. Les seqüències de “escape” comencen pel caràcter “backslash” (\). Les principals són les següents: Seqüències de escape Seqüència Significat \a \b \f \n \r \t \\ \”

Emet un so per l’altaveu Retrocedir una posició a l’esquerra Avançar una pàgina Avançar al principi de la següent línia Tornar al principi de la línia Tabulació Imprimir el caràcter \ Imprimir el caràcter “

L’exemple següent mostra com es poden utilitzar les seqüències de “escape” amb la instrucció printf.

Page 31: _curso de programacion en c c plus plus(Català)

Exemple printf (“La instrucció \”printf\” serveix\nper imprimir per pantalla.”); /* Resultat per pantalla: La instrucció “printf” serveix per imprimir per pantalla. */

Escriptura del valor de les variables Podem utilitzar també la funció printf per imprimir per pantalla el valor d’una variable. En aquest cas hem d’especificar el tipus i el nom de la variable que volem imprimir utilitzant el format següent: Utilització de la funció printf printf (“%<identificador_del_tipus>”, <nom_de_la_variable>);

Per exemple, si tenim definida una variable de tipus enter n i la volem imprimir per pantalla, hauríem de posar la següent instrucció: Exemple

int n=10; printf("%d",n);

L’identificador de tipus ha de coincidir amb el tipus de la variable que volem imprimir. Si no coincideix, la sortida per pantalla pot ser incorrecta i no correspondre’s amb el valor real de la variable. A la taula següent tenim els identificadors de tipus que es poden utilitzar dins de la funció printf i el seu significat: Identificadors de format per la funció printf Identificador Tipus Sortida per pantalla %d, %i %u %o %ou %x, %X %f %e, %E %g, %G %c %s %p

int int int int int double double double char char * apuntador

Enter amb signe en base decimal Enter sense signe en base decimal Enter amb signe en base octal Enter sense signe en base octal Enter en base hexadecimal (%x minúscules, %X majúscules) Real en notació decimal Real en notació exponencial (%e minúscules, %E majúscules) Real en notació decimal o exponencial depenent de l’exponent. Un caràcter individual Una cadena de caràcters El valor de l’apuntador

format: identificador de tipus enter

argument: variable a imprimir

Page 32: _curso de programacion en c c plus plus(Català)

Aquests identificadors de tipus es poden complementar amb altres especificadors per indicar com s’ha d’imprimir el valor: el nombre de dígits a imprimir, la precisió dels nombres reals, etc. D’aquesta manera, el format complet dels especificadors de tipus per la funció printf és el següent: %[flags][amplada][.precisió][longitud]identificador_de_tipus El significat i utilització de cadascun d’aquests camps poden variar depenent del tipus de la variable a imprimir. A la taula següent es mostren totes les possibilitats: Especificadors de format per la funció printf Especificador Valor Tipus Significat

h Tots tipus enters Enter de tipus short l Tots tipus enters Enter de tipus long ll Tots tipus enters Enter de tipus long long

Longitud

L Tots tipus reals Real de tipus long double

Tots tipus enters Nº mínim dígits a escriure. Omple amb zeros a l’esquerra si és necessari

%f,%e Nº dígits decimals. %g Nº màxim de dígits significatius

Precisió Nombre enter

%s Nº màxim de caràcters a imprimir

Amplada Nombre enter Tots els tipus Nº mínim de caràcters a imprimir

- Tots els tipus Alineació a l’esquerra + Tots tipus numèrics Mostra el signe (+ ó -) davant del número

0 Tots tipus reals Afegeix zeros fins que s’arriba a l’amplada mínima

# %o,%x,%X Mostra o,x ó X davant del número

Flags

# Tots tipus reals Força que s’imprimeixi el punt decimal En l’exemple següent es mostra com es poden utilitzar aquests especificadors per imprimir variables amb la funció printf. Fixem-nos que es pot utilitzar per imprimir el valor de més d’una variable en una mateixa instrucció, simplement posant tants especificadors de tipus com variables vulguem imprimir, i a continuació el nom de totes les variables: Exemple int n=10000; float x = 3.5; printf ("%d %#x %8d %-8d %.8d ", n, n, n, n, n); /* Resultat per pantalla: 10000 0x2710 10000 10000 00010000 */ printf ("%f %g %.3f %8.2f %08.2f", x, x, x, x, x); /* Resultat per pantalla: 3.500000 3.5 3.500 3.50 00003.50 */

Page 33: _curso de programacion en c c plus plus(Català)

Escriptura de valors de variables combinats amb missatges de text Es poden combinar les dues formes d’utilització de la funció printf per imprimir a la vegada un missatge de text i el valor d’una o més variables. Per aconseguir-ho, hem de posar dins del missatge de text els especificadors de tipus que calguin segons el tipus de la variable que volem imprimir. Després del missatge haurem d’indicar el nom de totes les variables que s’hagin d’imprimir, que hauran de correspondre en nombre i tipus amb els especificadors de tipus que s’hagin posat dins del missatge de text. L’exemple següent il·lustra aquesta forma d’utilitzar la funció printf: Exemple int n=10, m=20; printf (“La suma de %d i %d és %d”, n, m, n+m); /* Resultat per pantalla: La suma de 10 i 20 és 30 */ 4.2. Entrada bàsica: funció scanf La funció scanf és la que ens permet que l’usuari del programa introdueixi valors per teclat que es guardaran en variables del programa. Quan s’executa la funció scanf, el programa s’atura i queda esperant a que l’usuari introdueixi un o més valors amb el teclat. Quan l’usuari acaba d’introduir tots els valors i prem la tecla intro, el programa continua executant la instrucció següent a la funció scanf. La forma d’utilitzar la funció scanf és la següent: Utilització de la funció scanf scanf (“%<identificador_del_tipus>”, &<nom_de_la_variable>);

L’identificador de tipus té el mateix sentit, i pot prendre els mateixos valors que per la funció printf. És a dir, indica com s’ha d’interpretar el valor que introdueix l’usuari. Igual que amb la funció printf, l’identificador de tipus ha de coincidir amb el tipus de la variable. Si no coincideix, el compilador no donarà cap error, però en el moment d’executar el programa, el valor que es guarda a la variable no serà correcte. Igualment, si l’usuari introdueix un valor que no coincideix amb el tipus especificat, el programa tampoc donarà cap error d’execució però el valor guardat a la variable serà incorrecte. En el cas de la funció scanf l’únic especificador de format que es pot aplicar és l’especificador d’amplada, que indica el nombre màxim de caràcters que es tindran

Substitució de valors

Especificadors de tipus

Page 34: _curso de programacion en c c plus plus(Català)

en compte al llegir el valor de la variable. Si se n’introdueixen de més, simplement s’ignoraran. Exemple int n; float x; printf (“Introdueix primer un nombre enter: “); scanf (“%d”, &n); /* El programa s’atura fins que s’introdueix un valor i es guarda a la variable n com a nombre enter */ printf (“\nIntrodueix un nombre real: “); /* \n per saltar a la línia següent */ scanf (“%f”, &x); /* El programa s’atura fins que s’introdueix un valor i es guarda a la variable x com a nombre real */

Fixem-nos que totes les variables van precedides pel caràcter “&”. Això és degut a que els paràmetres que es passen a la funció scanf s’han de passar per referència tal com s’explicarà més endavant al capítol 5 Igual que es pot fer amb la funció printf podem utilitzar una sola crida a la funció scanf per llegir més d’un valor. En aquest cas hem d’especificar tots els identificadors de tipus separats per un espai en blanc i indicar el nom de totes les variables, com es pot veure en aquest exemple: Exemple int n; float x; printf (“Introdueix un nombre enter i un nombre real: “); scanf (“%d %f”, &n, &x); /* El programa s’atura fins que s’introdueix un valor enter i un valor real que es guarden a les variable n i x respectivament */

En aquest exemple, els dos valors que s’han d’introduir poden estar separats per espais en blanc, tabuladors o salts de línia (tants com vulguem). Quan acabem d’introduir el segon valor s’ha de prémer la tecla intro per acabar la crida a la funció scanf. 4.3. Entrada de caràcters: funcions getchar, getche i getch Si volem llegir un sol caràcter de teclat, ho podríem fer utilitzant la funció scanf que hem explicat abans. Però si el que volem fer és simplement detectar si s’ha pressionat una determinada tecla, la utilització de la funció scanf no és massa adequada perquè obligatòriament s’ha de prémer també la tecla intro després de la tecla per continuar. Com a alternativa tenim les funcions getchar, getche i getch, que estan definides específicament per llegir caràcters de teclat. A l’exemple següent es pot veure com es poden utilitzar aquestes funcions:

Page 35: _curso de programacion en c c plus plus(Català)

Utilització de les funcions getchar, getche i getch char c; c=getchar(); char c; c=getche(); char c; c=getch();

La diferència entre les tres funcions està en el que imprimeixen per pantalla i en la necessitat o no de prémer la tecla intro després de llegir el caràcter: • getchar: S’ha de prémer intro després de llegir el caràcter. El caràcter llegit es

mostra per pantalla. • getche: No s’ha de prémer intro després de llegir el caràcter. El caràcter llegit

es mostra per pantalla. • getch: No s’ha de prémer intro després de llegir el caràcter. El caràcter llegit no

es mostra per pantalla. D’aquesta manera la funció getch es pot utilitzar per aturar el programa fins que l’usuari premi qualsevol tecla. Això ho podem fer simplement posant en el programa: getch(); Si no ens interessa saber quina és la tecla que s’ha pressionat no cal assignar el resultat a cap variable. El programa esperarà que es premi qualsevol tecla i després continuarà. 5. Operadors i expressions 5.1. Expressions Una expressió és una construcció del llenguatge que combina variables, constants i operadors i que permet obtenir un valor d’un tipus determinat. Les expressions es poden utilitzar, per exemple, per assignar valor a una variable, per proporcionar els paràmetres a una funció o per calcular un resultat que s’ha d’imprimir per pantalla. Ja hem explicat en les seccions prèvies que per assignar un valor a una variable fem servir aquest tipus d’instrucció: <nom_variable> = <expressió>; L’expressió pot ser tan complexa com sigui necessari (seguint les regles que explicarem a continuació) per calcular el valor que volem guardar a la variable. Les expressions més simples són les constants o noms d’altres variables, però també poden ser la combinació de qualsevol dels operadors que estan definits al llenguatge C i que explicarem en aquesta secció o el resultat de crides a funcions que retornen un valor. En general, els operadors es poden dividir en operadors binaris (si necessiten dos operands, com per exemple la suma) o operadors unaris (si només necessiten un operand, com per exemple, el canvi de signe). Per altra banda, segons el tipus

Page 36: _curso de programacion en c c plus plus(Català)

d’operació que permeten fer, els classificarem en operadors aritmètics, relacionals, lògics i operadors de bits. 5.2. Operadors aritmètics Els operadors aritmètics serveixen per fer les operacions aritmètiques bàsiques (suma, resta, multiplicació i divisió). La taula següent resumeix a quins tipus de dades es poden aplicar i com s’utilitzen: Operadors aritmètics Operació Símbol Tipus operands Exemple Resultat Suma + reals, enters 5 + 3 8 Resta - reals, enters 5 – 3 2 Multiplicació * reals, enters 5 * 3 15 Divisió real / reals 5.0 / 3.0 1.666666 Divisió entera / enters 5 / 3 1 Mòdul % enters 5 % 3 2

Fixem-nos que la divisió es comporta diferent segons si s’aplica a valors de tipus real o enter. Si s’aplica a nombres reals, el resultat també és un nombre real amb tots els decimals. En canvi, si s’aplica a valors de tipus enter, el resultat també ha de ser de tipus enter, i retorna només la part entera de la divisió, sense els decimals (fixem-nos que no s’arrodoneix el resultat). A més a més tenim l’operador mòdul, que s’aplica només a valors de tipus enter i que retorna el residu de la divisió entera dels dos valors. Per exemple, si dividim 5 entre 3, el quocient (5/3) és 1 i el residu (5%3) és 2. 5.2.1. Operadors d’assignació compostos El llenguatge C proporciona 5 operadors que permeten combinar els operadors aritmètics anteriors i l’operador d’assignació bàsic. Amb aquests operadors el resultat de l’operació aritmètica es torna a guardar en una de les variables que intervenen en l’expressió. A la taula següent tenim quins són aquests operadors d’assignació compostos, l’explicació del seu significat i un exemple d’utilització. (suposant que el valor inicial de x en tots els casos és 5): Operadors d’assignació compostos Operador Sintaxi Significat Exemple Valor inicial de x Valor final de x

+= x += y; x = x + y; x+=3 5 8 -= x -= y; x = x – y; x-=3 5 2 *= x *= y; x = x * y; x*=3 5 15 /= x /= y; x = x / y; x/=3 5 1 %= x %= y; x = x % y; x%=3 5 2

Fixem-nos que, simplement, és una manera més curta d’escriure les dues operacions (per exemple, x=x+y ho podem escriure x+=y). 5.2.2. Operadors d’increment i decrement

Page 37: _curso de programacion en c c plus plus(Català)

Els operadors d’increment i decrement serveixen per sumar o restar 1, respectivament, a una variable. Són operadors que s’utilitzen molt, sobretot en les sentències iteratives. Per cada operador hi ha dues versions, operador de pre-increment i post-increment, i operador de pre-decrement i post-decrement. La diferència està en el moment en què es fa l’increment o el decrement dins de l’expressió. La taula següent resumeix el seu funcionament: Operadors d’increment i decrement Operador Sintaxi Significat Post-increment x++ Utilitza x. Després incrementa el seu valor: x=x+1 Pre-increment ++x Incrementa el valor de x: x=x+1. Després utilitza el nou valor Post-decrement x-- Utilitza x. Després decrementa el seu valor: x=x-1 Pre-decrement --x Decrementa el valor de x: x=x–1. Després utilitza el nou valor Fixem-nos que si utilitzem aquests operadors de forma aïllada, com una sola instrucció, no hi ha diferència entre l’operador de pre-increment / pre-decrement i el de post-increment / post-decrement. Els dos operadors simplement incrementen / decrementen el valor de la variable. Per exemple, si x val 3, aquestes dues instruccions: x++; ++x; produeixen el mateix resultat. Després d’executar qualsevol de les dues instruccions el valor de x s’ha incrementat i passa a ser 4. En canvi, el resultat és diferent si aquests operadors s’utilitzen dins d’alguna expressió més complexa o dins d’una assignació. En aquests casos, l’operador indica si es fa l’increment / decrement abans de l’assignació (pre-increment / pre-decrement) o després (post-increment / post-decrement). L’exemple següent il·lustra la diferència entre els operadors de pre-increment i post-increment (pels operadors de decrement, el funcionament és similar): Exemple int m = 5, n= 0; m = n++; /* Post-increment: 1.Assignació: m=n, 2.Increment: n=n+1 */ printf (“Valor de m: %d, Valor de n: %d); /* Resultat per pantalla: Valor de m: 0, Valor de n: 1 */ int m = 5, n= 0; m = ++n; /* Pre-increment. 1.Increment: n=n+1, 2.Assignació: m=n */ printf (“Valor de m: %d, Valor de n: %d); /* Resultat per pantalla: Valor de m: 1, Valor de n: 1 */

Page 38: _curso de programacion en c c plus plus(Català)

5.3. Operadors relacionals Els operadors relacionals serveixen per comparar dos valors, és a dir, per saber si són iguals o diferents, o si un és més gran o més petit que l’altre. Sempre retornen cert o fals. Normalment s’utilitzen en sentències condicionals o iteratives que s’expliquen més endavant per decidir si una acció s’executa o no en funció del resultat de la comparació. La taula següent resumeix la sintaxi i significat dels operadors relacionals (en tots els exemples suposem que x és una variable de tipus enter que té el valor 3): Operadors relacionals Operació Símbol Exemple Resultat Igual == x == 3 true Diferent != x != 3 False Més gran o igual >= x >= 3 Trae Més petit o igual <= x <= 3 Trae Més gran > x > 3 False Més petit < x < 3 False

Normalment, el resultat d’una comparació ha de ser un valor lògic: cert, si la comparació es compleix, o fals, si la comparació no es compleix. Com ja s’ha explicat, no tots els compiladors admeten el tipus lògic (_Bool). En aquests casos, el tipus lògic es substitueix per un valor enter, de manera que el resultat és 0 si la comparació és falsa, i 1 si la comparació és certa. S’ha d’anar en compte amb la utilització de l’operador d’igualtat (==). Fixem-nos que és molt similar a l’operador d’assignació (=) i és relativament fàcil confondre’ls. En aquest cas, el compilador no donarà cap missatge d’error però el programa no funcionarà correctament. Els operadors relacionals es poden aplicar a qualsevol tipus de valor. En el cas de valors de tipus enter o real, la comparació es fa segons l’ordenació natural dels nombres enters o reals. En canvi, si la comparació s’aplica a valors de tipus caràcter, l’ordre que s’utilitza és el que defineix el codi ASCII de cada caràcter, que coincideix amb l’ordre alfabètic. 5.4. Operadors lògics Els operadors lògics es corresponen amb els operadors lògics habituals de la lògica matemàtica (and, or i negació). S’utilitzen bàsicament per combinar vàries comparacions en una única expressió i obtenir un únic valor lògic final. La taules següents resumeixen la sintaxi i el funcionament dels operadors lògics (en els exemples, suposem x = 5 i y= 10): Operadors lògics Operació Símbol Exemple Resultat AND && (x < 3) && (y < 20) false OR || (x < 3) || (y < 20) true Negació ! !(x < 3) true

Page 39: _curso de programacion en c c plus plus(Català)

a b a && b a || b !a

true true true true falsetrue false false true false true false true cert false false false false 5.5. Operadors de bits Els operadors de bits serveixen per manipular nombres enters en format binari com a seqüències de bits. Permeten modificar individualment els bits dels nombres enters. A les taules següents es mostren quins són els operadors de bits, el seu significat i un exemple del seu funcionament. Operadors de bits Operador Símbol Significat Exemple

Complement a 1 ~ Inverteix els bits. 1->0. 0->1

~0101 = 1010

AND binari & Fa un AND binari bit a bit 0110 & 1010 = 0010 OR binari | Fa un OR binari bit a bit 0110 | 1010 = 1110 OR exclusiu binari (XOR) ^ Fa un XOR binari bit a bit 0110 ^ 1010 = 1100

Desplaçament esquerra << Desplaça tots els bits n posicions a l’esquerra.

0101 << 2 = 0100

Desplaçament dreta >> Desplaça tots els bits n posicions a la dreta.

0101 >> 2 = 0001

a b a & b a | b a ^ b1 1 1 1 0 1 0 0 1 1 0 1 0 1 1 0 0 0 0 0

En els operadors de desplaçament els bits més a l’esquerra (dreta) es perden mentre que els bits nous que apareixen per la dreta (esquerra) s’omplen amb zeros. Fixem-nos també en la similitud en la forma d’expressar els operadors AND i OR binaris (&, |) i els operadors AND i OR lògics (&&, ||). Igual que passa amb l’operador de comparació i el d’assignació, si els confonem normalment el compilador no donarà cap missatge d’error però el programa no funcionarà correctament. 5.6. Prioritat i associativitat dels operadors En expressions complexes, amb moltes operacions, hem de definir algun mecanisme que especifiqui l’ordre d’avaluació de les diferents operacions i poder determinar el resultat final. En C aquest ordre el marquen les regles de prioritat i associativitat dels operadors. Cada operador té un ordre de prioritat de manera que primer s’executen les operacions més prioritàries. En cas de tenir varis operadors del mateix tipus o varis operadors amb la mateixa prioritat, llavors s’apliquen les regles d’associativitat.

Page 40: _curso de programacion en c c plus plus(Català)

Les regles d’associativitat són dues: associativitat esquerra-dreta, que vol dir que les operacions d’igual prioritat s’executen d’esquerra a dreta, i associativitat dreta-esquerra, que vol dir que les operacions d’igual prioritat s’executen de dreta a esquerra. La taula següent resumeix la prioritat i associativitat dels operadors: Regles de prioritat i associativitat Prioritat Operadors Associativitat 1 Parèntesi Esquerra – Dreta2 ++, -- (post-increment i post-decrement) Esquerra – Dreta3 ++, --, (pre-increment i pre-decrement), !, ~, - (canvi de signe) Dreta – Esquerra4 *, /, % Esquerra – Dreta5 +, - Esquerra – Dreta6 <<, >> Esquerra – Dreta7 <, <=, >=, > Esquerra – Dreta8 ==, != Esquerra – Dreta9 & Esquerra – Dreta10 ^ Esquerra – Dreta11 | Esquerra – Dreta12 && Esquerra – Dreta13 || Esquerra – Dreta14 =, *=, /=, %=, +=, -=, <<=, >>=, &=, |=, ^= Dreta – Esquerra Amb els parèntesis podem canviar l’ordre d’execució que marquen la prioritat i l’associativitat. Els parèntesis sempre són més prioritaris, de manera que sempre s’executa primer el que hi ha dins dels parèntesis. Si hi ha varis parèntesis s’executen d’esquerra a dreta. Exemple int a = 6, b = 12, c = 2, d = 1; resultat = a + b/a * c – d; /* Ordre: divisió, multiplicació, suma, resta. Resultat: 9 */ resultat = (a + b)/a * c – d; /* Ordre: suma, divisio, multiplicació, resta. Resultat: 5 */ resultat = a + b/(a * (c – d)); /* Ordre: resta, multiplicació, divisió, suma. Resultat: 8 */ resultat = (a + b)/(a * c) – d; /* Ordre: suma, multiplicació, divisió, resta. Resultat: 0 */

5.7. Conversions de tipus En general, en una mateixa operació es poden combinar operands que tinguin diferents tipus de dades (per exemple un enter amb un real). En aquests casos, s’ha convertir un dels operands al tipus de l’altre per poder realitzar correctament l’operació. Les conversions poden ser explícites si és el programador el que indica quina conversió s’ha d’aplicar, o implícites si és el compilador el que aplica

Page 41: _curso de programacion en c c plus plus(Català)

automàticament una conversió seguint les regles que s’expliquen a la secció següent. 5.7.1. Conversions implícites Les conversions implícites les realitza el compilador seguint dues regles de conversió: 1. En assignacions, el tipus del resultat es converteix automàticament al tipus de la

variable d’assignació. 2. En qualsevol altra operació binària, el tipus de menor rang es converteix

automàticament al tipus de rang més gran, segons la següent ordenació:

Conversions implícites

char < int < unsigned int < long < unsigned long < float < double

S’ha de tenir present que en les assignacions, si el tipus de la variable és de rang inferior al de l’expressió resultat, podem estar perdent informació perquè l’espai de memòria ocupen és diferent. En concret, ens podem trobar amb 3 situacions: - La variable és de tipus enter i el resultat és de tipus real: el valor real es trunca,

és a dir, s’assigna la part entera del valor real i es perden tots els dígits decimals. - La variable és de tipus float i el resultat és de tipus double. El valor

s’arrodoneix a la precisió del tipus float. - La variable i el resultat són de tipus enter, però la variable és de rang inferior. En

aquest cas, el valor que s’assigna a la variable pot ser diferent al resultat degut a la diferència de bytes en memòria que ocupen els dos tipus.

Exemple int n=10, m=5; char c=20; float r=3.5; r = n + r; /* Conversió implícita int-float sense pèrdua d’informació. r=13.5 */ r = n + m; /* Conversió implícita int-float sense pèrdua d’informació. r=15.0 */ c = m; /* Conversió implícita int-char amb pèrdua d’informació. r=? */ n = r; /* Conversió implícita float-int amb pèrdua d’informació. r=3 */

5.7.2. Conversions explícites Les conversions explícites són les que força el programador per obligar al programa a fer les conversions de tipus desitjades. S’especifiquen de la forma següent: Conversions explícites (<tipus_dades>) <expressió>

Page 42: _curso de programacion en c c plus plus(Català)

El resultat és que l’expressió indicada es converteix al tipus especificat. Igual que amb les conversions implícites si el tipus de conversió té rang inferior al tipus original, es pot perdre informació. Exemple

int n=10, m=5; char c=20; float r=3.5; r = (float) (n+m); /* Conversió explícita int-float sense pèrdua. r=15.0*/ c = (char) n; /* Conversió explícita amb possible pèrdua. c=? */ n = (float) m * 2.5 /* Conversió explícita sense pèrdua, combinada amb conversió implícita (assignació) amb pèrdua. n=12 */

Page 43: _curso de programacion en c c plus plus(Català)

Tema 3: Estructures de control 1. Estructures condicionals Les estructures condicionals permeten vincular l’execució d’una acció o conjunt d’accions al compliment d’una condició determinada. Primer s’avalua la condició i en funció del resultat s’executen unes accions o unes altres. D’aquesta manera, el resultat final del programa pot ser diferent cada cop que s’executi depenent del valor de les variables que intervenen a la condició. El llenguatge C incorpora tres tipus d’estructures condicionals (l’alternativa simple, l’alternativa doble i l’alternativa múltiple) que es diferencien en el tipus i el nombre de condicions que es poden indicar i en el comportament que tenen en funció de si es compleix o no la condició. 1.1. Alternativa simple: if L’alternativa simple permet indicar una condició i un conjunt d’accions que s’executaran només si la condició és certa. Si la condició és falsa aquest conjunt d’instruccions no s’executaran. La sintaxi en C de l’alternativa simple i la seva equivalència en pseudocodi i diagrama de flux és la següent: Alternativa simple Fixem-nos que és obligatori que la condició estigui sempre entre parèntesi. La diferència entre les dues estructures en la sintaxi en C està en què si només volem executar una sola acció en cas que es compleixi la condició, no cal posar els delimitadors { i } al principi i al final. En canvi si s’han d’executar vàries accions és obligatori posar els delimitadors per marcar el principi i el final de les instruccions afectades per la condició. L’exemple següent mostra la utilització de l’alternativa simple. El programa calcula l’àrea i el perímetre d’un cercle a partir del valor del radi que introdueix l’usuari per teclat, però el càlcul només es fa si el valor del radi és més gran que zero. En cas contrari el programa no fa res. Exemple /* Programa que calcula àrea i perímetre d'un cercle */ #include <stdio.h> void main() {

Diagrama de flux

si <condició> aleshores <accions> fisi

if (<condició>) { <accions> }

Pseudocodi Sintaxi C

if (<condició>) <acció>;

No Sí

Condidició?

Accions

Page 44: _curso de programacion en c c plus plus(Català)

float radi, area, perimetre; const float PI=3.141592; /* Definició de la constant PI */ printf ("Introdueix el radi del cercle:"); scanf("%f",&radi); if (radi > 0) /* Comprovació que el radi sigui positiu */ { area = PI * radi * radi; perimetre = 2 * PI * radi; printf("L'area del cercle és: %f\n", area); printf("El perimetre de la circumferencia és: %f\n",perimetre); } }

1.2. Alternativa doble: if/else En l’alternativa doble indiquem una condició i dos blocs d’accions alternatius. Només s’executarà un dels dos blocs d’accions en funció del resultat de la condició: si la condició és certa s’executarà el primer bloc d’accions mentre que si la condició és falsa s’executarà el segon. La sintaxi en C de l’alternativa doble i la seva correspondència en pseudocodi i diagrama de flux és la següent: Alternativa doble Igual que en el cas de l’alternativa simple, si només volem executar una acció (en qualsevol de les dues alternatives), els delimitadors, { i }, són opcionals. Com a exemple de l’alternativa doble mostrem una modificació de l’exemple anterior en què hi afegim que es mostri un missatge d’error si el radi no és més gran que zero, enlloc d’acabar sense fer res. Fixem-nos que en el bloc d’accions que imprimeix el missatge d’error, com que només executem una acció, no cal posar els delimitadors. Exemple /* Programa que calcula àrea i perímetre d'un cercle */ #include <stdio.h> void main() {

si <condició> aleshores <accions sí> sino <accions no> fisi

if (<condició>){ <accions sí>} else { <accions no>}

Diagrama de flux Pseudocodi Sintaxi C

No Sí Condidició?

Accions_Sí Accions_No

Page 45: _curso de programacion en c c plus plus(Català)

float radi, area, perimetre; const float PI=3.141592; /* Definició de la constant PI */ printf ("Introdueix el radi del cercle:"); scanf("%f",&radi); if (radi > 0) /* Comprovació que el radi sigui positiu */ { area = PI * radi * radi; perimetre = 2 * PI * radi; printf("L'area del cercle és: %f\n", area); printf("El perimetre de la circumferencia és: %f\n",perimetre); } else printf ("Error. El valor del radi ha de ser positiu\n"); }

1.2.1. Estructures if/else encadenades Tant l’alternativa simple com l’alternativa doble permeten decidir si s’executa o no un bloc d’accions en funció del resultat d’una única condició. Hi ha casos, però, en què és necessari comprovar diverses condicions a la vegada i executar una acció diferent en funció de quina sigui la condició que es compleix. Imaginem, per exemple, un programa per convertir la nota numèrica d’un examen en la nota final que s’ha de posar a l’acta d’una assignatura de la forma següent: si la nota numèrica està entre 0 i 5 la nota de l’acta ha de ser “suspès”, si la nota està entre 5 i 6.5, “aprovat”, si està entre 6.5 i 8.5, “notable”, entre 8.5 i 9.5, “excel·lent” i “matrícula” si la nota està entre 9.5 i 10. En aquest cas, cada condició té associada una acció diferent. Hem de comprovar totes les condicions i executar només l’acció (o accions) que corresponen a la condició que sigui certa. Tot i que no tenim una estructura condicional específica per aquest tipus de situacions, es pot implementar utilitzant una seqüència d’alternatives dobles encadenades de la forma següent: Estructures if/else encadenades if (<condició 1>) { <accions 1> } else if (<condició 2>) { <accions 2> } . . . else if (<condició n>) { <accions n> } else { <accions n+1> }

Page 46: _curso de programacion en c c plus plus(Català)

Amb aquest tipus d’estructura podem especificar diverses condicions i un bloc d’accions associat a cadascuna d’elles que només s’executarà si la condició és certa. Es comença comprovant la primera condició. Si és certa s’executa el primer bloc d’accions i ja no es fa res més. En canvi si és falsa, passarà a comprovar-se la següent condició. D’aquesta forma s’aniran comprovant totes les condicions fins que una d’elles sigui certa. Llavors, s’executarà el bloc d’accions associat a la condició i s’acabarà l’execució de tota l’estructura condicional. Si cap de les condicions és certa, s’executarà l’últim bloc d’accions. Fixem-nos també que si vàries condicions són certes només s’executarà el bloc d’accions associat a la primera d’elles. Utilitzant aquest tipus d’estructura l’exemple anterior de la conversió de notes es podria implementar de la forma següent: Exemple /* Programa que converteix una nota numèrica en la nota final de l'assignatura */ #include <stdio.h> void main() { float nota; printf ("Introdueix la nota numerica (entre 0 i 10):"); scanf("%f",&nota); /* Criteris de conversió: nota negativa o més gran que 10--> error, 0<=nota<5 --> suspès, 5<=nota<6.5 --> aprovat, 6.5<=nota<8.5 --> notable, 8.5<=nota<9.5 --> excel·lent, 9.5<=nota<=10 --> matrícula */ if ((nota < 0) || (nota > 10)) printf ("La nota es incorrecta\n"); else if (nota < 5) printf ("La nota final es suspes\n"); else if (nota < 6.5) printf ("La nota final es aprovat\n"); else if (nota < 8.5) printf ("La nota final es notable\n"); else if (nota < 9.5) printf ("La nota final es execlent\n"); else printf ("La nota final es matricula\n"); }

Fixem-nos que a l’hora d’expressar les condicions utilitzem el fet que es comproven de forma seqüencial. Així per exemple, per comprovar si la nota correspon a un suspès només cal que comprovem si la nota és inferior a 5 perquè si arribem a aquesta condició ja sabem segur que serà més gran que zero perquè la primera condició és falsa. 1.3. Alternativa múltiple: switch/case L’alternativa múltiple és similar a l’estructura d’alternatives dobles encadenades. És a dir, també permet especificar vàries condicions i un bloc d’accions associat a cadascuna d’elles que només s’executarà si la condició és certa. La diferència està

Page 47: _curso de programacion en c c plus plus(Català)

en el tipus de condicions que podem posar. En aquest cas, les condicions seran sempre del tipus expressió == valor. Per tant, aquesta construcció és útil quan hem d’executar accions diferents segons el valor d’una variable o expressió. La sintaxi i la seva equivalència en pseudocodi i diagrama de flux és la següent: Alternativa Múltiple Fixem-nos que també és obligatori que l’expressió estigui entre parèntesi. El funcionament és el següent: s’avalua l’expressió que s’indica a la clàusula switch i es compara de forma seqüencial amb cadascun dels valors que s’especifiquen a les clàusules case. Quan es troba el primer valor que coincideix amb el resultat de l’expressió s’executa el bloc d’accions associat al valor i s’acaba l’execució. Si no es troba cap valor igual al resultat de l’expressió s’executa el bloc d’accions associat a la clàusula default. Aquesta clàusula, però, és opcional. Llavors, si no s’especifica i cap valor coincideix amb el resultat de l’expressió, no s’executarà res. Un exemple simple de la utilització d’aquesta estructura podria ser un programa que simulés el funcionament d’una calculadora. El programa ens demanarà els dos números que volem operar i a continuació, quina operació volem fer (suma, resta, multiplicació o divisió). En funció de l’opció escollida es farà una operació o una altra. Per tant, podem utilitzar l’estructura switch/case per comprovar quina operació s’ha indicat i executar l’acció corresponent. També podem utilitzar la clàusula default per indicar què s’ha de fer si el caràcter que s’introdueix no correspon amb cap de les 4 operacions vàlides.

Sintaxi C

switch (<expressió>) { case valor 1: <accions 1>;

break; case valor 2: <accions 2>;

break; . . . case valor N: <accions N>;

break; default: <accions resta casos>;

break; }

Pseudocodi

...Valor 2 Valor N Valor 1

expressió

Accions 1 Accions 2 Accions N

Diagrama de flux

cas <expressió> <valor 1> : <accions 1> <valor 2> : <accions 2> . . . <valor N> : <accions N> en altre cas : <accions resta casos> fi cas

Page 48: _curso de programacion en c c plus plus(Català)

Exemple /* Programa que simula el funcionament d'una calculadora simple */ #include <stdio.h> void main() { char operacio; /* Operació que s'ha de realitzar. */ float x,y; /* Valors d'entrada. */ printf("Introdueix dos numeros:\n"); scanf("%f",&x); scanf("%f",&y); printf("Introdueix una operacio valida:\n"); fflush(stdin); operacio=getchar(); switch (operacio) { case '+': printf ("%f\n", x+y); break; case '-': printf ("%f\n", x-y); break; case '*': printf ("%f\n", x*y); break; case '/': if (y != 0) printf ("%f\n", x/y); else printf ("Error. Divisio per zero.\n"); break; default: printf("Operacio incorrecta.\n"); break; } }

En la utilització de l’estructura switch/case hem d’anar en compte amb tres aspectes importants que poden ser la causa d’errors bastant habituals: • Les opcions que podem posar a les etiquetes de cada clàusula case només

poden ser constants de tipus char o de tipus int (o de qualsevol de les variants del tipus enter). No poden ser ni constants de tipus real (float, double) ni comparacions de cap tipus (no podem posar per exemple, case >0: ...).

• Només podem especificar un únic valor a cada clàusula case. No podem indicar de cap manera una llista o un rang de valors (no podem posar, per exemple, case 0, 2, 4, 6, 8: ....).

• És important no oblidar-se la instrucció break al final de cada bloc d’accions. Aquesta instrucció marca el final del bloc d’ accions que s’han d’executar en aquest cas particular. Si no es posa la instrucció break, el programa no para i continua executant les instruccions que venen a continuació. D’aquesta forma, enlloc d’executar només les instruccions que corresponen a la clàusula case correcta, s’executen també les instruccions de la clàusula case següent. En

Page 49: _curso de programacion en c c plus plus(Català)

l’exemple anterior si no poséssim cap instrucció break, si introduïm el caràcter ‘+’ com a operació, el programa ens mostraria el resultat de la suma, però també de la resta, la multiplicació i la divisió.

En el segon punt comentem que no es pot especificar més d’un valor en una clàusula case. Per tant, si volem compartir el mateix bloc d’accions per més d’un valor hauríem de repetir el mateix codi en clàusules case diferents, una per cada valor. De totes maneres, el tercer punt ens dóna una alternativa per poder executar el mateix conjunt d’accions associat a més d’un valor diferent. Imaginem un programa com el que hem vist a l’exemple per convertir una nota numèrica en la nota final de l’assignatura, però suposem ara que els valors de la nota numèrica només poden ser valors de tipus enter. En aquest cas, només tenim 11 valors possibles de nota (del 0 al 10) de manera que podem utilitzar l’estructura switch/case. El problema és que hi ha diferents valors que comparteixen la mateixa nota final (per exemple, totes les notes de 0 a 4 corresponen a la nota final de suspès). Per tractar aquest cas el que fem és posar totes les clàusules case que han de compartir el mateix bloc d’accions (per exemple, nota final suspès) seguides i sense posar cap acció ni la instrucció break en cap d’elles menys a l’última (en aquest cas case 4: ...). D’aquesta manera si la nota és, per exemple un 2, detectem que l’opció vàlida és case 2: ...., però com que no hem especificat cap acció ni la instrucció break, el programa continua i passa a la següent opció (case 3: ....). Com que tampoc té cap acció associada ni la instrucció break també passem a l’opció següent (case 4: ....) que sí que té acció associada i instrucció break. Per tant, s’executa el tros de codi que ens diu que la nota final és suspès. El mateix passa pels casos de notes que corresponen a aprovat o notable. Exemple /* Programa que converteix una nota numèrica en la nota final de l'assignatura */ #include <stdio.h> void main() { int nota; printf ("Introdueix la nota numerica (entre 0 i 10):"); scanf("%d",&nota); /* Criteris de conversió: nota negativa o més gran que 10--> error, 0<=nota<5 --> suspès, 5,6 --> aprovat, 7,8 --> notable, 9 --> excel·lent, 10 --> matrícula */ switch (nota) { case 0: case 1: case 2: case 3: case 4: printf ("La nota final es suspes\n"); break; case 5: case 6:

Page 50: _curso de programacion en c c plus plus(Català)

printf ("La nota final es aprovat\n"); break; case 7: case 8: printf ("La nota final es notable\n"); break; case 9: printf ("La nota final es execlent\n"); break; case 10: printf ("La nota final es matricula\n"); break; default: printf ("La nota es incorrecta\n"); break; } }

2. Composició iterativa Les estructures iteratives (o bucles) permeten repetir un conjunt d’accions tantes vegades com faci falta. El nombre de repeticions es controla amb una condició que permet decidir si s’ha de continuar executant el bloc d’accions o s’ha de parar. En C hi ha tres tipus d’estructures iteratives: while, do/while i for. Es diferencien en el tipus de condició que es pot posar per controlar el bucle i en l’ordre i la forma en que s’executa la condició i les accions que es repeteixen. Independentment del tipus concret d’estructura iterativa, sempre podem distingir aquests els tres elements bàsics següents: 1. Inicialització del bucle: abans de començar l’estructura iterativa normalment

necessitem inicialitzar correctament el valor de les variables que s’utilitzen en la condició d’acabament del bucle.

2. Condició de finalització: és la condició que permet indicar quan acaba l’execució del bucle.

3. Accions del bucle: és el cos principal del bucle, les accions que s’han de repetir.

Aquests tres elements s’han de definir de forma coordinada per assegurar que les accions del bucle s’executen exactament el nombre vegades desitjades. Sobretot, és important no oblidar-se la inicialització i assegurar que modifiquem les variables dins de les accions del bucle de forma que la repetició s’acaba (arribem a la condició de finalització) en el moment adequat. Si no, correm el risc d’entrar en un bucle infinit en el qual les accions s’executin de forma indefinida. A les seccions següents veurem els tres tipus d’estructures iteratives (while, do/while i for) i veurem les diferències entre elles a l’hora d’expressar la condició de finalització i de coordinar l’execució de la condició i de les accions del bucle. 2.1. Estructura while En l’estructura while, la condició de finalització es comprova al principi del bucle i després s’executen les accions del cos del bucle. D’aquesta forma, si quan s’arriba al bucle, la condició no es compleix, les accions no s’executaran cap vegada. Equival a l’estructura mentre en pseudocodi. La sintaxi d’aquesta estructura i la seva correspondència en pseudocodi i diagrama de flux és la següent:

Page 51: _curso de programacion en c c plus plus(Català)

Estructura iterativa while Igual que en l’estructura condicional, els delimitadors { i } són opcionals si hi ha una sola acció per repetir. Fixem-nos que la condició també s’ha de posar entre parèntesis. Les inicialitzacions de les variables que controlen la condició s’han de posar explícitament abans del bucle. El funcionament és el següent: primer de tot es comprova la condició. Si la condició és certa passem a executar les accions i tornem a comprovar la condició. En el moment que la condició sigui falsa s’acaba l’execució. Com exemple, veurem un programa que calcula el factorial d’un nombre enter introduït per teclat. Exemple /* Programa que calcula el factorial d'un nombre enter */ #include <stdio.h> void main() { int n, factorial, i; printf ("Introdueix un nombre positiu: "); scanf("%d",&n); factorial = 1; /* Inicialització variables bucle */ i = 2; while (i <= n) /* Condició de finalització */ { factorial = factorial * i; /* Accions del bucle */ i++; } printf ("El factorial es: %d.\n",factorial); }

El factorial d’un nombre enter n es calcula multiplicant tots els nombres enters des de 1 fins a n. Per tant, l’acció a repetir és una multiplicació. Aquesta és l’acció que posem dins del cos del bucle. Hem de tenir en compte però, que cada vegada hem de multiplicar per un nombre diferent. Per tant, després de la multiplicació incrementem la variable i perquè a la següent repetició es multipliqui pel següent nombre. A més a més, el resultat és acumulatiu, de manera que el resultat de la multiplicació el guardem a la mateixa variable factorial que utilitzem com a base

Condició

Accions

sí no

mentre <condició> <accions> fi mentre

while (<condició>) { <accions> }

Diagrama de flux Pseudocodi Sintaxi C

Page 52: _curso de programacion en c c plus plus(Català)

per multiplicar. Pel que fa a la condició de finalització l’última multiplicació que hem de fer correspon al nombre n. Per tant, la condició és i<=n, de forma que quan ja hem multiplicat per n i incrementem la variable i, la condició deixa de complir-se i parem l’execució del bucle. Finalment, perquè tot el bucle funcioni correctament hem d’inicialitzar la variable factorial (que va acumulant el resultat) amb el valor 1 i la variable i amb el valor 2 (la primera multiplicació que hem de fer és directament amb i = 2). 2.2. Estructura do/while L’estructura do/while és molt similar a l’estructura while, amb la diferència que la condició de finalització es comprova al final del bucle, després d’executar les accions del bucle. La principal conseqüència és que les accions del bucle s’executen sempre almenys un cop. Equival a l’estructura repetir del pseudocodi. La sintaxi i la seva equivalència en pseudocodi i diagrama són les següents Estructura iterativa while Fixem-nos que al final de la condició s’ha de posar obligatòriament el delimitador “;” per marcar el final de l’estructura. Les inicialitzacions de les variables de conformen la condició també s’han de posar explícitament abans del bucle. El funcionament és el següent: primer de tot s’executen les accions del bucle. Després es comprova la condició. Si la condició és certa tornem a executar les accions i a comprovar la condició. En el moment que la condició sigui falsa s’acaba l’execució. L’exemple del càlcul del factorial utilitzant l’estructura do-while quedaria de la forma següent: Exemple /* Programa que calcula el factorial d'un nombre enter */ #include <stdio.h> void main() { int n, factorial, i; printf ("Introdueix un nombre positiu:"); scanf("%d",&n); factorial = 1; i = 1; /* Inicialització variables bucle */ do {

Condició

Accions sí

no

do{ <accions> } while (<condició>);

Sintaxi C Diagrama de flux

repetir <accions> mentre <condició>

Pseudocodi

Page 53: _curso de programacion en c c plus plus(Català)

factorial = factorial * i; /* Accions del bucle */ i++; } while (i <= n); /* Condició de finalització */ printf ("El factorial es: %d\n",factorial); }

Fixem-nos que en aquesta versió del càlcul del factorial hem hagut de canviar la inicialització de la variable i de i=2 per i=1. Això és degut al fet que l’estructura do/while comprova la condició després d’executar les accions. Si deixem la mateixa inicialització que per l’estructura while (i=2) el programa no funcionaria correctament si n té el valor 1. Per tant, en aquest cas hem de fer una repetició addicional que, a més a més, no modifica el resultat final perquè simplement fa una multiplicació per 1. Aquest exemple ens mostra que, tot i que pràcticament qualsevol bucle es pot implementar amb qualsevol de les 3 estructures iteratives, pot haver-hi casos en què una d’elles sigui més eficient i òptima. 2.3. Estructura for L’estructura for s’utilitza bàsicament quan volem executar un conjunt d’accions que s’han de repetir un nombre de cops que pot ser determinat pel valor d’una variable o expressió. En aquest tipus d’estructura tenim un índex que s’inicialitza al principi del bucle i es va actualitzant a cada pas del bucle fins que es compleix la condició final. Tant la inicialització i l’actualització de l’índex com la condició de final s’inclouen dins de la instrucció de control del bucle. La comprovació de la condició es fa al principi del bucle (abans d’executar les accions) igual que a l’estructura while. L’actualització de l’índex es fa després d’executar les accions. En aquest sentit, l’estructura for es pot considerar equivalent a l’estructura per del pseudocodi, però amb algunes diferències que la fan més flexible: 1. La condició de final del bucle pot ser qualsevol condició vàlida. En canvi, a

l’estructura per la condició final sempre comprova si l’índex arriba al valor final. 2. L’actualització de la variable índex també pot ser qualsevol expressió valida i no

s’ha de limitar, com a l’estructura per, a sumar o restar una quantitat fixa. A continuació, tenim la sintaxi de l’estructura for i la seva equivalència en diagrama de flux i pseudocodi: Estructura iterativa for

Condició

Inicialització índex

No Accions

Actualització índex

for (var=inici; condicio; expressió) {

<accions> }

per index ∈ [inici .. final] pas n <accions> fi per

Sintaxi C

Diagrama de flux Pseudocodi

Page 54: _curso de programacion en c c plus plus(Català)

El funcionament és el següent. Primer es fa la inicialització (var=inici) i es comprova la condició. Si la condició és certa s’executen les accions del bucle i s’executa l’expressió que actualitza l’índex. Llavors es torna a comprovar la condició. Si la condició és certa es tornen a executar les accions i l’expressió d’actualització fins que la condició sigui falsa. L’exemple del factorial utilitzant l’estructura for seria de la següent manera: Exemple /* Programa que calcula el factorial d'un nombre enter */ #include <stdio.h> void main() { int n, factorial, i; printf ("Introdueix un numero positiu:"); scanf("%d",&n); factorial = 1; /* Inicialització, actualització i condició final dins del for*/ for (i=2; i <= n; i++) factorial = factorial * i; /* Accions del bucle */ printf ("El factorial es: %d\n",factorial); }

Fixem-nos que tant la inicialització (i=2) i l’actualització (i++) de la variable i com la condició de final (i<=n) es fan dins de la capçalera de la instrucció for. Dins del cos del bucle només queda l’acció de la multiplicació que permet fer el càlcul final. 2.4. Comparació de les estructures iteratives A les seccions anteriors hem implementat el mateix exemple implementat amb les tres estructures iteratives. En general, gairebé tots els problemes es poden resoldre utilitzant qualsevol de les tres estructures. De totes maneres ja hem explicat que la implementació del càlcul del factorial amb l’estructura do/while requereix una repetició més que la implementació amb les altres dues estructures i per tant, és més ineficient. Això ens mostra també que, segons el tipus de problema, una de les tres estructures pot ser més adequada i eficient que les altres. L’estructura for és especialment indicada quan podem determinar a priori el nombre de vegades que s’ha d’executar el bucle, de manera que es podem controlar el nombre de repeticions amb la inicialització i l’actualització de la variable de control. Les estructures while i do/while seran més indicades quan a priori no puguem determinar quantes vegades s’haurà d’executar el bucle. És a dir, quan la finalització del bucle depengui d’algun càlcul que s’hagi de fer dins del propi bucle, o d’alguna entrada de dades per part l’usuari que es produeix també dins del bucle. La diferència entre utilitzar l’estructura while o do/while estarà en si les accions del bucle s’han d’executar almenys una vegada o no. Si és possible que les accions del bucle no s’hagin d’executar caldrà utilitzar l’estructura while perquè comprova la condició abans de començar. En canvi, si sabem segur que s’executaran almenys un cop, serà millor utilitzar l’estructura do/while que comprova la condició al final del bucle.

Page 55: _curso de programacion en c c plus plus(Català)

Els tres exemples següents ens il·lustren en quins casos és millor utilitzar cadascuna de les tres estructures. En el primer exemple, volem calcular la suma dels n primers nombres naturals parells, on n és un valor que introdueix l’usuari al començar el programa. En aquest cas, un cop llegit el valor de n, sabem exactament quants nombres hem de sumar. Sabem també que el primer nº a sumar és 2, l’últim és 2n i que a cada pas del bucle hem d’incrementar en 2 el nº a sumar. Per tant, és millor utilitzar l’estructura for: Exemple /* Programa que suma els n primers nombres parells */ #include <stdio.h> void main() { int n, i, suma; printf ("Introdueix un numero positiu:"); scanf("%d",&n); suma = 0; /* Primer nº a sumar: 2, Últim nº a sumar: 2n, Actualització de 2 en 2*/ for (i=2; i<=2*n; i+=2) suma += i; /* Accions del bucle */ printf ("La suma es: %d\n",suma); }

En el segon exemple volem sumar una sèrie de nombres que s’introdueix per teclat. La sèrie s’acaba quan s’introdueix un 0. Per tant, no podem saber a priori quants nombres haurem de sumar, sinó que depèn de l’entrada que fa l’usuari a cada pas del bucle. Per tant, no podem utilitzar l’estructura for. A més a més, hem de preveure la possibilitat que el primer nombre introduït ja sigui un 0. En aquest cas no s’haurà de fer cap suma, és a dir, cap repetició del bucle. Per tant, només podem utilitzar l’estructura while. L’estructura do/while no és adequada perquè ens obligaria a fer la suma encara que el nº introduït sigui un 0. Fixem-nos que abans de començar el bucle hem de demanar el primer nº i després a cada pas del bucle hem de tornar a demanar el següent nº de la sèrie. Exemple /* Programa que suma una sèrie de nºs entrats per l'usuari fins que decideixi acabar, entrant el valor 0.*/ #include <stdio.h> void main() { int n, suma; printf ("Introdueix un numero:"); scanf("%d",&n); /* Demanar el primer nº */ suma = 0; /* Inicialització de la suma */ while (n != 0) /* Acabem si n==0 */ { suma = suma + n; /* Accions del bucle */ printf ("Introdueix un numero:"); scanf("%d",&n); /* Demanar el següent nº */

Page 56: _curso de programacion en c c plus plus(Català)

} printf ("La suma es: %d\n",suma); }

Finalment, en el tercer exemple també volem sumar una sèrie de nombres que s’introdueixen per teclat, però en aquest cas, la forma de controlar quan s’acaba la sèrie serà diferent. Després de llegir i sumar cada nº de la sèrie, preguntarem a l’usuari si vol seguir introduir més nombres i pararem quan contesti que no. La situació és similar al cas anterior en què no podem saber a priori quants nºs s’han de sumar, sinó que depèn de la resposta de l’usuari que forma part de les accions del bucle. La diferència està en què en aquest cas, com a mínim, sempre s’introduirà almenys un nº a sumar. Per tant, l’estructura do/while és la més indicada: Exemple /* Programa que suma una sèrie de nºs */ #include <stdio.h> void main() { int n, suma; char resposta; suma = 0; do { /* Accions del bucle */ printf ("Introdueix un numero:"); scanf("%d",&n); suma += n; printf ("Vols continuar?:"); /* Demanar si continuar */ fflush(stdin); resposta=getchar(); } while ((resposta=='s')||(resposta=='S')); /* Continuar mentre contestem sí */ printf ("La suma es: %d\n",suma); }

Page 57: _curso de programacion en c c plus plus(Català)

Tema 4: Estructures de dades compostes 1. Taules 1.1. Definició Una taula4 es pot veure com una estructura que serveix per guardar un conjunt de valors, tots del mateix tipus. Cada valor queda identificat per la posició que ocupa dins de la taula de manera que per recuperar o modificar un valor de la taula hem d’indicar sempre la seva posició (o índex). Els valors de la taula sempre s’han d’accedir individualment utilitzant el seu índex. Així per exemple, podríem definir una taula per guardar les notes de tots els alumnes d’una classe, tal com es mostra a la figura següent. D’aquesta manera, podríem accedir a la nota de cada alumne especificant la posició que ocupa dins de la taula: Exemple

0 1 2 N-1 5 10 8 ............. 2

Hem de tenir en compte, però, que hi ha algunes diferències entre les taules en C i el concepte genèric de taula que podem trobar en pseudocodi o en altres llenguatges de programació. La diferència principal és que en C, la primera posició de la taula sempre correspon a l’índex 0. Per tant, l’índex de l’última posició és el nombre de valors que volem guardar dins la taula menys 1. Això és diferent del que es pot fer amb una taula genèrica, en què l’índex de la primera i última posició es poden fixar a qualsevol valor que ens interessi. En l’exemple podem veure que si volem guardar les notes d’N alumnes, l’índex de la primera posició és 0 i l’índex de l’última posició és N-1. 1.2. Declaració La sintaxi per declarar una taula en C i la seva relació amb la declaració de taules en pseudocodi és la següent: Declaració de taules

4 En C és habitual referir-se a les taules utilitzant el terme array.

notes:

N alumnes

tipus <nom_tipus_taula> : taula [MINIM .. MAXIM] de <tipus> fitipus var <nom_taula> : <nom_tipus_taula> fivar

Pseudocodi

Page 58: _curso de programacion en c c plus plus(Català)

Utilitzant aquesta sintaxi la declaració d’una taula per guardar les notes d’una classe de 100 alumnes es faria de la forma següent: Exemple

0 1 2 99 5 10 8 ............. 2

Fixem-nos que en la declaració d’una taula en C hem d’especificar tres coses: 1. El tipus base de la taula, és a dir, el tipus dels valors que guardem a la taula (a

l’exemple, el tipus int). 2. El nom de la taula (notes), és a dir, el nom de la variable que utilitzarem per

accedir als valors de la taula. 3. El nº posicions de la taula, és a dir, el nº de valors que guardem a la taula.

Fixem-nos que aquest número ha de venir definit obligatòriament per un valor constant (a l’exemple, la constant MAX_ALUMNES). D’aquesta forma, el tamany de la taula queda determinat quan escrivim el programa i no pot canviar-se en temps d’execució.

Resumint, amb la declaració de l’exemple anterior, estem definint una variable notes, on podem guardar 100 valors de tipus enter, indexats del 0 al 99. Podem veure també que les diferències entre la declaració de taules en C i la declaració genèrica de taules que s’utilitza en pseudocodi són bàsicament dues: 1. En C l´index de la primera posició d’una taula serà sempre 0. Per tant, en la

declaració no fa falta especificar quina és la primera posició i només cal indicar quants elements té la taula. D’aquesta manera, les posicions vàlides de la taula seran des de la posició 0 fins arribar al nombre total d’elements menys 1.

2. En C no cal declarar primer un nou tipus abans de declarar la variable5. Directament es pot declarar la variable de tipus taula, especificant el tipus base i el nº d’elements.

5 Utilitzant la instrucció typedef es pot declarar un nou tipus per la taula i declarar les variables utilitzant aquest nou tipus de dades

Sintaxi en C

<tipus> <nom_taula>[<nº d’elements>];

#define MAX_ALUMNES 100 /* Constant pel màxim d’alumnes */ int notes[MAX_ALUMNES]; /* Declaració taula notes de 100 elements */

notes:

100 alumnes

Page 59: _curso de programacion en c c plus plus(Català)

1.3. Accés als elements de la taula L’accés a un element de la taula (ja sigui per consultar o modificar el valor) es fa especificant el nom de la taula i l’index de l’element que volem accedir d’aquesta forma: <nom_taula>[<índex>] Aquesta és l’única forma de poder accedir als elements d’una taula. L’índex pot ser una variable o una expressió, sempre de tipus enter. Cada element de la taula pot ser considerat i tractat de la mateixa forma que qualsevol altra variable del tipus bàsic que hem utilitzat al declarar la taula. Fixem-nos que hem d’accedir individualment a cada valor. No tenim cap forma de recuperar o modificar a la vegada tots els valors de la taula. Tampoc es pot fer una còpia ni una assignació directa entre dues taules de forma global. S’ha de fer també element a element. Quan accedim als elements d’una taula hem d’anar molt en compte per utilitzar només índexs de les posicions vàlides dins de la taula, és a dir, índexs entre 0 i N-1, on N és el nombre d’elements que hem indicat al declarar la taula. Si per error utilitzem algun índex fora d’aquests límits podem provocar errors d’execució imprevisibles i, de vegades, difícils de detectar. Penseu que aquest tipus d’errors no són detectats pel compilador ni provoquen que surti cap missatge d’error quan s’executa el programa. Simplement el programa no funcionarà correctament. A l’exemple següent mostrem com declarar i utilitzar una taula per fer un programa que llegeixi totes les notes d’un grup d’alumnes i calculi quants alumnes han aprovat l’assignatura. Exemple /* Programa que llegeix les notes dels alumnes i diu quants alumnes han aprovat */ #include <stdio.h> #define N_ALUMNES 100 /* Definició del màxim nº d'alumnes possible */ void main() { int notes[N_ALUMNES]; /* Taula de 100 posicions per notes */ int i; /* Índex per recórrer la taula */ int n_aprovats; /* Lectura de les notes dels alumnes */ printf ("Introdueix les notes dels alumnes: \n"); /* Bucle per accedir a tots els elements de la taula */ for (i=0; i< N_ALUMNES; i++) { printf ("Introdueix nota de l'alumne %d: ", i+1); scanf ("%d", &notes[i]); /*Accés posició i de la taula */ } n_aprovats = 0; /* Comptar quants alumnes han aprovat */ for (i=0; i< N_ALUMNES; i++)

Page 60: _curso de programacion en c c plus plus(Català)

{ if (notes[i] >= 5) n_aprovats++; } printf ("Nº d'alumnes aprovats: %d", n_aprovats); }

En aquest exemple, primer llegim el valor de les notes de tots els alumnes i les guardem en una taula que hem declarat prèviament. Després, utilitzem la informació que hem guardat a la taula per comptar quants alumnes han aprovat. Per declarar la taula, primer definim la constant N_ALUMNES i després, utilitzem aquesta constant per declarar la taula notes de tipus int. Per accedir als valors de la taula, com que hem d’accedir individualment a cadascun dels elements, és habitual la utilització de bucles que permeten anar recuperant tots els valors, des del primer a l’últim amb l’ajuda d’una variable índex que es va incrementant a cada pas del bucle. En aquest exemple utilitzem dos bucles for per poder accedir a tots els elements de la taula. En el primer bucle anem llegint valors enters i els anem guardant a la taula mentre que en el segon bucle consultem aquests valors per anar comptant quantes notes corresponen a un aprovat. Dins dels bucles utilitzem una variable índex i, que serveix per indicar a quina posició de la taula volem accedir (notes[i]). Com que aquesta variable i s’incrementa a cada repetició del bucle, permet accedir cada vegada a un element diferent. Fixem-nos també que aquesta variable índex està inicialitzada dins del bucle for amb el valor i=0 i la condició de final és i<N_ALUMNES. D’aquesta manera pren valors que van des de 0 fins a 99 que són les posicions vàlides de la taula. És molt important que tinguem en compte de posar la condició de finalització del bucle de manera que mai superem el límit superior de l’array. Imaginem que en l’exemple anterior, en comptes de posar la condició i<N_ALUMNES, utilitzem la condició i<=N_ALUMNES. En aquest cas, el bucle s’executaria també pel valor i=100, cosa que faria que accedíssim fora de les posicions vàlides de la taula i provocaríem que el programa no funcionés correctament. 1.4. Inicialització de les taules Igual que passa amb les variables de tipus bàsics, és necessari inicialitzar les taules abans de la seva utilització dins del programa. Per inicialitzar una taula haurem d’indicar un valor per cadascuna de les posicions de la taula. Hi ha dues maneres de fer-ho: en el moment de la declaració de la taula, o després de la declaració, dins del codi del programa: 1.4.1. Inicialització en el moment de la declaració Quan declarem una taula podem especificar un valor per cadascun dels seus elements, simplement posant una llista de valors separats per comes, i delimitada per corxets de la forma següent: Inicialització de taules Exemple

<tipus> <nom>[<nºelements>] = {<valor1>,<valor2>,…,<valorN>};

int nombres_parells[10] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};

Page 61: _curso de programacion en c c plus plus(Català)

0 1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 20

L’exemple il·lustra la declaració i incialització d’una taula que s’utilitza per guardar els 10 primers nombres naturals parells. Aquest tipus d’inicialització només serà pràctica quan el nombre d’elements de la taula sigui reduït. 1.4.2. Inicialització element a element L’altra manera d’inicialitzar una taula és donant valor un a un a tots els elements de la taula des del codi del programa. No es pot inicialitzar tota la taula amb una única instrucció; s’ha de fer sempre element a element. Moltes vegades la inicialització es fan utilitzant un bucle per recórrer tots els valors de la taula. La mateixa taula de l’exemple anterior es podria inicialitzar des del codi del programa de qualsevol de les dues formes que es mostren en aquest exemple: la primera utilitza un instrucció per cada element mentre que la segona utilitza un bucle. Exemple /* Inicialització de l’array element a element individualment */ int nombres_parells[10]; ......... nombres_parells[0] = 2; nombres_parells[1] = 4; nombres_parells[2] = 6; ..... nombres_parells[9] = 20; /* Inicialització de l’array element a element amb un bucle */ int nombres_parells[10]; int i; ..... for (i=0; i<10; i++) nombres_parells[i] = (i+1) * 2; 1.5. Representació en memòria Els arrays es guarden en memòria en blocs consecutius, un valor a continuació de l’anterior, tal com il·lustra la figura següent:

Nombres_parells:

Page 62: _curso de programacion en c c plus plus(Català)

0 1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 20

! ! 2 4 6 8

10 12 14 16 18 20 ! !

Aquesta manera de representar els arrays tindrà força importància quan en el tema 6 tractem els apuntadors. De moment, no cal tenir-ho en compte. 2. Cadenes de caràcters 2.1. Definició i declaració Les cadenes de caràcters són taules que ens serveixen per guardar qualsevol informació de tipus text com per exemple, noms de persones o de productes, paraules, frases, etc. Una cadena de caràcters és una taula (amb totes les propietats i operacions que hem definit a la secció anterior) en què el tipus bàsic és el tipus caràcter. Per tant, una cadena es pot utilitzar exactament igual que qualsevol altra taula. La particularitat de les cadenes resideix en què tenen algunes propietats i operacions específiques que faciliten la manipulació i el tractament de la informació de tipus text. La principal característica que diferencia les cadenes de la resta de les taules és que inclouen un caràcter especial que permet indicar el final de la cadena. A més a més, disposem d’un conjunt d’operacions i funcions específiques per inicialitzar, accedir i manipular les cadenes. La figura següent il·lustra la definició d’una cadena com a taula de caràcters i la seva utilització per a guardar informació de tipus text, amb el caràcter especial de final de cadena: Exemple de cadena char cadena[10] = “HOLA”;

0 1 2 3 4 5 6 7 8 9 H O L A \0

Nombres_parells:

Posició del primer element

Memòria

cadena:

Nº d’elements de la taula: 10

Longitud de la cadena: 4 Marcador de final de cadena

Page 63: _curso de programacion en c c plus plus(Català)

Fixem-nos que la declaració de la cadena (char cadena[10]) és igual que la declaració d’una taula de tipus base caràcter. Al declarar la variable la inicialitzem amb la paraula HOLA (a la secció següent expliquem les diferents formes d’inicialitzar una cadena). Si observem els valors que s’han guardat a la taula, veurem que els caràcters de la paraula ocupen de forma consecutiva les primeres posicions de la taula, però veurem també que al final hi trobem un caràcter especial, representat com a ‘\0’, que és el que anomenem caràcter nul i que es correspon amb el codi ASCII 0. Aquest caràcter especial s’afegeix automàticament quan inicialitzem la cadena, i indica on acaben els caràcters vàlids de la cadena dins de la taula. Fixem-nos que quan hem declarat la taula hem especificat un tamany de 10 elements, però la paraula que hi guardem té menys caràcters. Per això necessitem el caràcter nul per marcar el final de la cadena. Aquesta serà una situació habitual quan treballem amb cadenes perquè quan declarem una cadena, normalment no sabem encara què hi guardarem. 2.2. Inicialització Igual que les taules, les cadenes es poden inicialitzar de forma global al moment de fer la declaració, o de forma individual, element a element dins del codi del programa. Hi ha però, algunes diferències en la forma d’inicialitzar les cadenes pel fet de guardar informació de tipus text. A continuació, primer comentem com s’especifiquen les constants de tipus cadena i després expliquem les tres formes diferents d’inicialitzar les cadenes. 2.2.1. Constants de tipus cadena Les constants de tipus cadena s’especifiquen com una seqüència de caràcters delimitats al principi i al final pel caràcter “. Per exemple, “HOLA” seria una constant de tipus cadena vàlida. Apart dels caràcters alfanumèrics (lletres, dígits i signes de puntuació) dins d’una cadena es poden posar caràcters especials amb un significat particular. Aquests caràcters especials són equivalents a les seqüències de “escape” per la funció printf. Per tant, són seqüències que consisteixen del caràcter “backslash” (\) seguit d’algun d’aquests caràcters. Per poder utilitzar caràcters no imprimibles (dels codis ASCII 128 al 256) podem utilitzar el caràcter “backslash” (\) seguit del codi ASCII que volem mostrar. Per exemple, per poder posar en una cadena la paraula “adéu”, hauríem d’especificar la constant d’aquesta manera: “ad\233u”, per poder imprimir el caràcter “é” que correspon al codi ASCII 233. 2.2.2. Inicialització en el moment de la declaració La idea és la mateixa que en el cas general de les taules, però enlloc d’especificar els valors per cada posició de la taula separats per comes, utilitzarem les constants de tipus cadena que hem introduït a la secció anterior. Amb aquest tipus d’inicialització, es posa automàticament el caràcter nul (‘\0’) per marcar el final de la cadena.

Page 64: _curso de programacion en c c plus plus(Català)

Exemple char cadena[5] = “HOLA”;

0 1 2 3 4 H O L A \0

Fixem-nos que, com que es guarda el caràcter nul al final, al declarar la cadena hem d’especificar un nº d’elements suficientment gran perquè contingui, com a mínim, el nº de caràcters del text que hi guardarem més el caràcter nul. En aquest cas, la paraula HOLA té només quatre lletres, però hem hagut de declarar la taula amb 5 elements per poder-hi posar el caràcter nul. Si utilitzem aquest tipus d’inicialització, també podem no especificar el nº d’elements de la taula. Automàticament es crearà una taula amb un nº d’elements suficient per guardar tota la cadena que s’hi vol guardar. Així, evitem el problema del tamany de la taula. D’aquesta forma, l’exemple anterior també es podria escriure de la manera següent: Exemple char cadena[] = “HOLA”;

0 1 2 3 4 H O L A \0

2.2.3. Inicialització element a element Si no inicialitzem la cadena en el moment de declarar-la s’ha d’inicialitzar després des del codi del programa, element a element, igual que es fa amb les taules. En aquest cas, s’ha d’afegir explícitament el caràcter nul a l’última posició de la cadena. Per exemple: Exemple char cadena[10]; ...... cadena[0] = ‘H’; cadena[1] = ‘O’; cadena[2] = ‘L’; cadena[3] = ‘A’; cadena[4] = ‘\0’;

0 1 2 3 4 5 6 7 8 9 H O L A \0

2.2.4. Inicialització utilitzant la funció strcpy El llenguatge C té definides una sèrie de funcions estàndard de llibreria que permeten treballar amb les cadenes. Una d’aquestes funcions és la funció strcpy,

Cadena:

Cadena:

Cadena:

Page 65: _curso de programacion en c c plus plus(Català)

que permet copiar el valor d’una cadena en una altra. Per tant, també podem utilitzar aquesta funció per inicialitzar una cadena, copiant el valor que interessi. La sintaxi d’utilització de la funció strcpy és: strcpy (cadena_destí, cadena_origen); La funció strcpy copia el contingut que hi ha a cadena_origen a cadena_destí. La cadena d’origen pot ser una altra variable de tipus cadena o bé una constant de tipus cadena i ha de contenir obligatòriament el caràcter nul al final. Al fer la còpia, automàticament es posa també el caràcter nul al final de la cadena de destí. Per exemple: Exemple char cadena_1[10]; char cadena_2[10]; strcpy (cadena_1, “HOLA”); strcpy (cadena_2, cadena_1); 2.3. Accés als elements de la cadena Les cadenes de caràcters es poden tractar com qualsevol altra taula. Això vol dir que es pot accedir als elements de la cadena de forma individual utilitzant l’operador []. El caràcter nul serveix per identificar on s’acaba la cadena. Això ho podem veure en el següent exemple, que llegeix una cadena de caràcters i ens diu quantes vocals minúscules té: Exemple #define MAX_CADENA 100 void main() { char cadena[MAX_CADENA]; int n_vocals=0; int i; printf (“Introdueix una cadena de caràcters\n”); gets (cadena); i = 0; /* Parem quan arribem al caràcter nul */ while (cadena[i] != ‘\0’) { if ((cadena[i] == ‘a’) || (cadena[i] == ‘e’) || (cadena[i] == ‘i’) || (cadena[i] == ‘o’) || (cadena[i] == ‘u’)) n_vocals++; i++; } printf (“El nombre de vocals a la cadena és: %d”, n_vocals); }

0 1 2 3 4 5 6 7 8 9H O L A \0 cadena_1:

0 1 2 3 4 5 6 7 8 9H O L A \0 cadena_2:

Page 66: _curso de programacion en c c plus plus(Català)

2.4. Entrada / Sortida Per llegir i escriure cadenes de caràcters podem utilitzar les funcions scanf i printf que s’han explicat al capítol 2, indicant l’especificador %s, que correspon a les cadenes. El problema és que la funció scanf para de llegir caràcters quan introduïm un espai en blanc. Per tant, no permet llegir cadenes que continguin espais en blanc. Per solucionar-ho, podem utilitzar les funcions gets i puts, que estan definides al fitxer de capçalera string.h, i que permeten llegir i escriure directament cadenes de caràcters. • Funció gets (<nom_de_cadena>): aquesta funció llegeix una sèrie de

caràcters que s’introdueixen per teclat i els guarda a la variable que se li passa com a paràmetre, afegint al final el caràcter nul de final de cadena. Es llegeixen tots els caràcters fins que s’introdueix un salt de línia. El caràcter de salt de línia no es guarda a la cadena. S’ha d’anar en compte perquè la funció gets no fa cap tipus de comprovació respecte al nº de caràcters que es llegeixen. Per tant, ens hem d’assegurar que no s’introduiran més caràcters que la longitud de la cadena.

• Funció puts(<nom_de_cadena>): aquesta funció escriu tots els caràcters de la cadena per pantalla (fins que es troba el caràcter nul) i després provoca un salt de línia per pantalla.

2.5. Funcions de tractament de cadenes La llibreria estàndard de C té definides un conjunt de funcions que permeten manipular i treballar amb cadenes de caràcters. La definició d’aquestes funcions està en el fitxer de capçalera string.h. Per tant, haurem d’incloure aquest fitxer sempre que vulguem utilitzar qualsevol d’aquestes funcions. A la taula següent tenim l’explicació d’alguna d’aquestes funcions. A continuació de la taula podem trobar un exemple de la utilització d’aquestes funcions: Funció Significat strcpy(<cadena_destí>,<cadena_origen>) Fa la cópia d'una cadena sobre

una altra. int strlen(<cadena>) Retorna la longitud de la cadena. strcat(<cadena_destí>,<cadena_origen>) Concatena la segona cadena al

final de la primera. int strcmp(<cadena_1>,<cadena_2>) Compara les dues cadenes.

Retorna 0 si són iguals. char *strchr(<cadena>, <carácter>) Busca un caracter dins d'una

cadena. char *strrchr(<cadena>, <carácter>) Busca un caracter dins d'una

cadena (comença per final). char *strstr(<cadena>,<subcadena>) Busca una subcadena dins d'una

altra cadena. char *strpbrk(<cadena>,<subcadena>) Busca la primera aparició de

qualsevol carácter d'una subcadena dins d'una altra cadena.

double strtod(<cadena>) Converteix una cadena de caracters a un real.

long strtol(<cadena>) Converteix una cadena de caracters a un long.

Page 67: _curso de programacion en c c plus plus(Català)

unsigned long strtoul(<cadena>) Converteix una cadena de caracters a un unsigned long.

Exemple #include <stdio.h> #include <string.h> #define MAX 80 /* Tamany màxim de les cadenes */ void main() { char cadena_1[MAX+1], cadena_2[MAX+1]; printf ("Introdueix la primera cadena (màxim %d cars.): ", MAX); gets(cadena_1); printf ("La longitud de la primera cadena és: %d\n", strlen(cadena_1)); printf ("Introdueix la segona cadena (màxim %d cars.): ", MAX); gets(cadena_2); printf ("La longitud de la segona cadena és: %d", strlen(cadena_2)); if (strcmp (cadena_1, cadena_2) == 0) printf ("Les dues cadenes són iguals"); else printf ("Les dues cadenes són diferents"); strcat (cadena_1, cadena_2); printf ("La concatenació de les dues cadenes és:"); puts (cadena_1); } 3. Taules bidimensionals 3.1. Definició i declaració Les taules bidimensionals són l’extensió de les taules a dues dimensions. És a dir, la informació s’organitza en files i en columnes. Per tant, per accedir a cada element de la taula bidimensional necessitarem dos índexos, un per les fila i un altre per la columna. Igual que en el cas de les taules unidimensionals, hem de tenir en compte que en C, l’índex de la primera fila i de la primera columna sempre serà 0 i, per tant, l’índex de l’última fila i columna vàlides serà el nº de files o de columnes menys 1. En la declaració de les taules bidimensionals hem d’especificar el nº de files i de columnes que volem que tingui que la taula. La sintaxi de la declaració i la seva equivalència amb la declaració en pseudocodi de taules bidimensionals és la següent:

Page 68: _curso de programacion en c c plus plus(Català)

L’exemple anterior defineix la següent matriu de dues dimensions:

0 1 2 0 1 2 3

Fixem-nos que el primer índex correspon sempre a les files i el segon a les columnes. Els índexs comencen per zero i van fins al nº de files o de columnes menys 1. Igual que amb les taules unidimensionals, haurem de vigilar de no accedir mai a un índex no permès perquè pot provocar errors d’execució imprevisibles i difícils de trobar.

matriu:

tipus <nom_tipus_taula>: taula [min1..max1, min2..max2] de <tipus> fitipus var <nom_taula> : <nom_tipus_taula> fivar

Pseudocodi

Sintaxi en C

<tipus> <nom_taula>[<nº de files>][<nº de columnes>];

Exemple

#define N_FILES 4 /* Constant pel nombre de files */ #define N_COLUMNES 3 /* Constant pel nombre de columnes */ int matriu[N_FILES][N_COLUMNES]; /* Declaració d’una matriu amb 4

files i 3 columnes */

matriu[0][0] matriu[0][2]

matriu[2][1]

matriu[3][0] matriu[3][2]

Page 69: _curso de programacion en c c plus plus(Català)

3.2. Inicialització Igual que passa amb la resta de variables, és necessari inicialitzar els valors d’una taula bidimensional abans d’utilitzar-la. De forma similar a les taules unidimensionals, podem fer la inicialització en el moment de la declaració, de forma global, o dins del codi del programa, element a element. Per inicialitzar una taula bidimensional en el moment de la declaració, hem d’especificar valor per tots els elements, començant per la primera fila, fins arribar a l’última. La forma de fer-ho és la següent: Inicialització de taules bidimensionals Exemple

0 1 2 0 2 4 6 1 8 10 122 14 16 183 20 22 24

L’exemple il·lustra la declaració i incialització de la matriu que hem definit a l’exemple anterior. Fixem-nos que especifiquem les files consecutivament començant per la primera, i per cada fila, donem un valor per cada columna. 3.3. Accés als elements L’accés als elements d’una taula bidimensional es fa generalitzant l’accés a una taula unidimensional, especificant primer la fila i després la columna: <nom_array>[<posició_fila>][<posició_columna>] Com sempre, hem de tenir molt present d’accedir només a les posicions vàlides de la taula, és a dir, entre 0 i el nº de files menys 1, i entre 0 i el nº de columnes menys 1. L’accés a les taules bidimensionals s’acostuma a fer també amb l’ajuda de bucles per recórrer tots els elements de la taula. Normalment són necessaris dos bucles, un per accedir a cadascuna de les files (o columnes) i un altre més intern, per accedir a cada element dins de la fila (o columna). En cada cas s’haurà de decidir si el bucle extern permet recórrer primer les files i després el bucle intern permet recórrer tots

<tipus> <nom_taula>[NFils][NCols] = {{<valor_f_0_c_0>, … , <valor_f_0_c_NCols-1>}, {<valor_f_1_c_0>, … , <valor_f_1_c_NCols-1>}, ……. {<valor_f_NFils-1_c_0>, …, <valor_f_NFils-1_c_NCols-1>}};

int matriu[4][3] = {{2,4,6}, {8,10,12}, {14,16,18}, {20,22,24}};

matriu:

Page 70: _curso de programacion en c c plus plus(Català)

els elements dins de cada fila (és el que s’anomena un recorregut per files de la taula) o si ens interessa més que el bucle extern serveixi per recórrer primer les columnes i en el bucle intern recórrer tots els elements dins de cada columna (és el que s’anomena recorregut per columnes de la taula). En el següent programa veiem un exemple dels dos tipus de recorreguts. El programa calcula la suma dels elements de cada fila (recorregut per files) i la suma dels elements de cada columna (recorregut per columnes) d’una matriu: Exemple /* Suma de files i columnes d'una matriu*/ #include <stdio.h> #define N_FILES 4 /* Constant pel nombre de files */ #define N_COLUMNES 3 /* Constant pel nombre de files */ void main() { /* Declaració i inicialització matriu: 4 files i 3 columnes */ int matriu[N_FILES][N_COLUMNES] = {{2, 4, 6}, {8, 10, 12},

{14, 16, 18}, {20, 22, 24}};

int i,j, suma_fila, suma_columna; /* Recorregut de les files (i=0 -> fila 1, i=3-> fila 4) */ for (i=0; i<N_FILES; i++)

{ suma_fila = 0; /* Inicialització suma fila i */ /* Recorregut de les columnes de la fila i*/ for (j=0; j<N_COLUMNES; j++) suma_fila += matriu[i][j]; /* Suma elements fila i */ printf ("La suma de la fila %d és: %d\n", i, suma_fila); } /* Recorregut de les cols. (j=0-> col 1, j=1->col 2) */ for (j=0; j<N_COLUMNES; j++) { suma_columna = 0; /* Inicialització suma columna j */ /* Recorregut de les files de la col. j*/ for (i=0; i<N_FILES; i++) suma_columna += matriu[i][j];/* Suma elements col.j*/ printf ("La suma de la columna %d és: %d\n", j, suma_columna); } }

Amb el primer bucle volem calcular la suma de tots els elements de cada fila. Per tant, fem un bucle extern que ens permet recórrer totes les files. A cada iteració d’aquest bucle, per cada fila, incialitzem la suma de la fila i fem un altre bucle intern que recorre tots els elements de la fila i acumula la suma. Amb el segon bucle volem calcular la suma de tots els elements de cada columna i, per tant, s’ha de fer el mateix que al primer, però invertint el paper de les files i les columnes.

Page 71: _curso de programacion en c c plus plus(Català)

3.4. Representació en memòria Les taules bidimensionals també es guarden en blocs consecutius de memòria, un valor a continuació de l’anterior. En aquest cas, l’ordre en què es guarden els elements de la taula és el de les files. És a dir, primer es guarden tots els elements de la primera fila, a continuació tots els de la segona, i així fins arribar a l’última fila. La figura següent ho il·lustra:

0 1 2 0 2 4 6 1 8 10 12 2 14 16 18 3 20 22 24

4. Registres 4.1. Definició Hem explicat que les taules són estructures de dades que es caracteritzen perquè permeten guardar un conjunt de valors amb dues propietats: 1) tots els valors han de ser del mateix tipus; 2) els valors es poden accedir mitjançant un índex numèric que indica la seva posició dins de la taula. De forma alternativa, els registres són estructures de dades que també permeten guardar un conjunt de valors però amb dues propietats diferents a les taules: 1. Els valors que es guarden en un registre poden ser de tipus diferent (no és

obligatori, però que ho siguin; també poden ser valors del mateix tipus). Cadascun dels elements d’un registre s’anomena camp.

2. L’accés als elements d’un registre es fa utilitzant l’etiqueta o nom del camp.

! ! 2 4 6 8

10 12 14 16 18 20 22 24 ! !

Posició del primer element de primera fila

Memòria

matriu:

1ª fila

2ª fila

3ª fila

4ª fila

Page 72: _curso de programacion en c c plus plus(Català)

Per tant, els registres seran útils quan necessitem guardar en una mateixa variable informacions de tipus diferent o quan l’accés a cadascun dels valors s’ha de fer utilitzant el seu nom i no la seva posició. Per exemple, imaginem que volem guardar les dades d’un alumne d’una assignatura qualsevol i que, per cada alumne, ens interessa tenir el nom, que ha de ser de tipus cadena, el grup en què està matriculat, que ha de ser de tipus enter, i la nota que aconsegueix, que ha de ser de tipus real. És a dir, per cada alumne, tenim tres valors de tipus diferent (cadena, enter i real) i cadascun d’ells pot ser identificat utilitzant una etiqueta o nom (nom_alumne, grup, nota). Per tant, en aquest cas la millor estructura per guardar la informació d’un alumne és un registre. La següent figura ens il·lustra com seria aquest registre: Exemple de registre

nom_alumne “Joan Vila” grup 1 nota 6.5

4.2. Declaració La declaració de registres en C segueix dos passos: primer, la definició de l’estructura del registre; després, la declaració de les variables utilitzant la definició prèvia de l’estructura: 4.2.1. Definició de l’estructura de registre Primer s’han d’especificar els camps que tindrà el registre, s’ha de donar un nom i un tipus a cada camp, i s’ha donar un nom genèric a l’estructura de registre. La sintaxi és la següent: Sintaxi

Exemple

En la definició de l’estructura del registre hem d’especificar una etiqueta per referir-nos a aquest tipus de registre, i per cada camp del registre, especificar el nom i el

alumne

struct <etiqueta_registre> {

<tipus_1> <nom_camp1>; <tipus_2> <nom_camp2>; ... <tipus_N> <nom_campN>;

};

struct registre_alumne {

char nom_alumne[20]; int grup; float nota;

};

Page 73: _curso de programacion en c c plus plus(Català)

tipus associat. En l’exemple, estem definint un registre que es dirà registre_alumne, i que té tres camps: nom_alumne, de tipus cadena, grup, de tipus enter i nota_final de tipus real, que serveixen respectivament per guardar el nom, el nº de grup i la i la nota final d’un alumne. Fixem-nos que fins aquí no hem declarat cap variable que correspongui a aquesta estructura de registre; només hem fet la definició genèrica del tipus de registre. 4.2.2. Declaració de variables registre Un cop definida l’estructura del registre, podem utilitzar-la per declarar tantes variables d’aquest tipus com ens interessi. La sintaxi és la següent: Sintaxi

Exemple

És a dir, cal posar l’etiqueta que hem donat al registre i a continuació el nom de totes les variables que vulguem declarar. En l’exemple anterior declarem dues variables utilitzant l’estructura de registre d’alumne que hem definit abans. Aquests dos passos, la definició de l’estructura i la declaració de les variables també es poden combinar en una sola instrucció. De totes maneres, acostuma a ser més clar i convenient separar-ho en els dos passos que hem indicat abans. Pot ser útil combinar-ho en una sola instrucció quan només s’han de declarar variables d’aquest tipus de registre en un únic punt del programa. Si es vol fer en sol pas, la forma de fer-ho és la següent: Sintaxi

Exemple

struct <etiqueta_registre> <nom_variable_1>, …. , <nom_variable_N>;

struct registre_alumne alumne_1, alumne_2;

struct <etiqueta_registre> {

<tipus_1> <nom_camp1>; <tipus_2> <nom_camp2>; ... <tipus_N> <nom_campN>;

} <nom_variable_1>, ….. , <nom_variable_N>;

struct registre_alumne {

char nom_alumne[20]; int grup; float nota;

} alumne_1, alumne_2;

Page 74: _curso de programacion en c c plus plus(Català)

4.3. Accés als elements d’un registre L’accés al valor de cadascun dels camps d’un registre es fa especificant el nom de la variable del registre i el nom o etiqueta del camp utilitzant la sintaxi següent: <nom_variable_registre>.<etiqueta_del_camp> Amb aquesta expressió podem utilitzar el valor del camp de la mateixa manera que qualsevol altra variable del mateix tipus que el camp. L’exemple següent mostra l’accés als camps d’un registre. El programa llegeix per teclat les dades d’un registre de tipus alumne com el que s’ha declarat abans i ens diu si l’alumne està aprovat o no: Exemple /* Utilització de registres */ #include <stdio.h> /* Declaració del registre */ struct registre_alumne { char nom[20]; int grup; int nota; } alumne; void main() { printf ("Introdueix el nom de l'alumne: "); gets (alumne.nom); printf ("Introdueix nº de grup de l'alumne: "); scanf ("%d", &alumne.grup); printf ("Introdueix la nota de l'alumne: "); scanf ("%d", &alumne.nota); if (alumne.nota >= 5) printf ("L'alumnne %s, del grup %d està aprovat", alumne.nom, alumne.grup); else printf ("L'alumnne %s, del grup %d està suspès", alumne.nom, alumne.grup); }

Fixem-nos que els valors dels camps del registre (alumne.nom, alumne.grup i alumne.nota) es tracten igualment que qualsevol variable del seu tipus respectiu (cadena, enter i real), tant al llegir o escriure per pantalla (funcions gets, scanf i printf) com en les altres instruccions del programa.

Page 75: _curso de programacion en c c plus plus(Català)

4.4. Inicialització Igual que amb la resta de variables també és necessari inicialitzar correctament els valors dels registres. Hi ha tres possibilitats d’inicialitzar un registre: en el moment de la declaració de la variable, després de la declaració omplint individualment cadascun dels camps, o després de la declaració fent la còpia d’un altre registre prèviament inicialitzat. 4.4.1. Inicialització en el moment de la declaració Igual que amb les taules, quan declarem una variable de tipus registre podem especificar un valor per cadascun dels camps posant una llista de valors, un per cada camp, separats per comes i delimitats per { i }. Aquí en tenim un exemple: Exemple struct registre_alumne { char nom[20]; int grup; float nota; }; struct registre_alumne alumne ={"Joan", 1, 7.5};

Declarem la variable alumne i assignem “Joan” al camp nom, el nº de grup és 1 i la nota final és 7.5. 4.4.2. Inicialització camp a camp Després de la declaració d’una variable de tipus registre ja no podem utilitzar el tipus d’inicialització anterior. Una forma d’inicialitzar el registre és donant valor a cada camp de forma individual, com es mostra en aquest exemple: Exemple struct registre_alumne { char nom[20]; int grup; float nota; }; struct registre_alumne alumne; strcpy (alumne.nom, "Joan"); alumne.grup = 1; alumne.nota = 7.5;

En aquest cas, cada valor del registre s’ha considerat com una variable del tipus corresponent (cadena, enter o real) que s’ha inicialitzat com qualsevol altra variable del mateix tipus.

Page 76: _curso de programacion en c c plus plus(Català)

4.4.3. Inicialització per còpia d’un altre registre L’altra forma d’inicialitzar un registre des del codi del programa, després de la declaració, és fent una còpia del contingut d’un altre registre que s’hagi inicialitzat prèviament. Els registres, a diferència de les taules, sí que es poden assignar de forma global, com es pot veure en aquest exemple: Exemple struct registre_alumne { char nom[20]; int grup; float nota; }; struct registre_alumne alumne_1={"Joan",1,7.5}; struct registre_alumne alumne_2; alumne_2 = alumne_1;

L’assignació directa entre dos variables de tipus registre produeix una còpia camp a camp del contingut d’un registre a l’altre (en aquest exemple de alumne_1 a alumne_2). Si algun dels camps és una taula (com en aquest cas, el camp nom) es fa una còpia valor a valor de tots els elements de la taula. 4.5. Taules de registres Evidentment, un cop creat una estructura de registre, es pot utilitzar dins d’un array per guardar tants registres com ens interessi. Aquesta és una de les utilitats més interessants dels registres. Per definir un array de registres, la sintaxi serà: struct <etiqueta_registre> <nom_taula>[<nº elements>]; En el següent exemple declarem un array per guardar les dades de tots els alumnes matriculats a una assignatura: Exemple #define MAX_ALUMNES = 100 struct registre_alumne { char nom[20]; int grup; int nota_final; }; struct registre_alumne alumnes[MAX_ALUMNES];

0 1 2 ······· ·······

alumnes:MAX_ALUMNES-1

nom grup

nota_final

alumnes[0] alumnes[0].nom

Page 77: _curso de programacion en c c plus plus(Català)

L’accés als valors d’aquesta estructura segueix les regles d’accés a taules i registres que s’han explicat anteriorment. La variable alumnes és una taula. Per tant, en un primer nivell haurem d’indicar a quin element de la taula volem accedir de la forma habitual, per exemple, alumnes[i]. Igualment, cada element de la taula és un registre. Per tant, en un segon nivell haurem d’especificar a quin camp del registre ens volem referir utilizant la forma habitual d’accedir als registres, és a dir, alumnes[i].nom, alumnes[i].grup o alumnes[i].nota_final. L’exemple següent agafa la definició anterior i l’utilitza en un programa complet que llegeix les dades de tots els alumnes de l’array i després ens diu quants alumnes han aprovat en total: Exemple #include <stdio.h> #include <string.h> #define MAX_ALUMNES 100 /* Definició de l'estructura de registre */ struct registre_alumne { char nom[20]; int grup; int nota_final; }; void main() { /* Declaració de l'array d'alumnes */ struct registre_alumne alumnes[MAX_ALUMNES]; int i, n_aprovats; /* Lectura de les dades dels alumnes */ for (i=0; i<MAX_ALUMNES; i++) { printf ("Introdueix el nom de l'alumne: "); fflush(stdin); gets (alumnes[i].nom); printf ("Introdueix nº de grup de l'alumne: "); scanf ("%d", &alumnes[i].grup); printf ("Introdueix la nota de l'alumne: "); scanf ("%d", &alumnes[i].nota_final); } /* Comptar quants alumnes han aprovat */ n_aprovats = 0; for (i=0; i<MAX_ALUMNES; i++) { if (alumnes[i].nota_final >= 5) n_aprovats++; } printf ("Nº aprovats: %d", n_aprovats); }

Page 78: _curso de programacion en c c plus plus(Català)

5. Unions 5.1. Definició i declaració Les unions són estructures de dades similars als registres: poden tenir diferents camps d’informació, cadascun d’ells d’un tipus diferent i identificat per una etiqueta o nom de camp. La diferència respecte als registres resideix en què només un d’aquests camps pot estar actiu en un moment determinat. D’aquesta en una variable de tipus unió hi podem guardar un únic valor (i no molts, com en els registres), que pot ser de tipus diferents segons el camp que utilitzem. L’explicació per aquest comportament de les variables de tipus unió està en la forma de guardar la informació dels camps en memòria. En un registre, la informació de cada camp es guarda en posicions de memòria consecutives i diferents. En canvi, en una unió, la informació de tots els camps es guarda a la mateixa posició de memòria, de forma solapada. Per tant, només pot ser vàlid el valor de l’últim camp que s’hagi modificat. El tamany d’una variable de tipus unió és el més gran d’entre tots els tipus de dades utilitzats per declarar els camps. Les unions poden ser útils quan necessitem variables que hagin de tenir valors de tipus diferents en funció d’alguna condició del programa. La declaració de variables de tipus unió és molt similar a la declaració dels registres. Primer hem de declarar l’estructura genèrica de la unió, especificant el nom i tipus de tots els camps i el nom de l’estructura de tipus unió. Després, utilitzant aquesta estructura genèrica, podem declarar les variables. Sintaxi

Exemple

En aquest exemple declarem una variable de tipus unió per guardar en una mateixa variable nota les diferents versions que podem tenir de la nota final d’una assignatura: la nota numèrica exacta (un valor real entre 0 i 10), la nota que apareix a l’acta de l’assignatura (suspès, aprovat, etc.) i la nota que s’utilitza per calcular la mitja de l’expedient acadèmic (un valor enter entre 0 i 4). Només una de les tres versions de la nota serà vàlida en cada moment del programa.

union <etiqueta_unió> {

<tipus_1> <nom_camp1>; <tipus_2> <nom_camp2>; ... <tipus_N> <nom_campN>;

}; union <etiqueta_unió> <nom_variable_1>, …. , <nom_variable_N>;

union TipusNota {

float nota_numerica; char nota_acta[4]; int nota_expedient;

}; union TipusNota nota;

Page 79: _curso de programacion en c c plus plus(Català)

5.2. Utilització L’accés als camps d’una variable de tipus unió es fa exacament igual que a les variables de tipus registre: nom_variable.nom_camp L’exemple següent mostra un programa que utilitza l’estructura de tipus unió declarada a l’exemple anterior per guardar correctament la nota final de l’assignatura segons el tipus d’aplicació. Exemple #define NOTA_NUMERICA 1 #define NOTA_ACTA 2 #define NOTA_EXPEDIENT 3 union TipusNota {

float nota_numerica; char nota_acta[4]; int nota_expedient;

}; void main () { union TipusNota nota; float valor_nota; int tipus_nota; printf (“Introdueix la nota final (entre 0 i 10)”); scanf (“%d”, &valor_nota); printf (“Quin tipus de nota vols guardar? (entre 1 i 3)”); scanf (“%d”, &tipus_nota); switch (tipus_nota) { case NOTA_NUMERICA:

nota.nota_numerica = valor_nota; printf (“Valor de nota: %f”, nota.nota_numerica);

break; case NOTA_ACTA:

ConvertirNotaActa (valor_nota, nota.nota_acta); printf (“Valor de nota: %s”, nota.nota_acta);

break; case NOTA_EXPEDIENT:

nota.nota_expedient = ConvertirNotaExp (valor_nota);

printf (“Valor de nota: %d”, nota.nota_expedient); break; } }

En aquest exemple es demana el valor de la nota (numèric) i quin tipus de nota es vol guardar (numèrica, nota de l’acta o de l’expedient). En funció del tipus de nota el programa guarda el valor de la nota en un camp diferent de la variable de tipus unió (només aquest camp que es modifica és vàlid). Fixem-nos que per guardar la nota en format d’acta o d’expedient acadèmic s’utilitzen dues funcions, ConvertirNotaActa i ConvertirNotaExp que no estan implementades dins d’aquest exemple (veure el capítol 5 per una explicació del tema de funcions). Aquestes funcions d’encarreguen d’obtenir el valor de la nota de l’acta o de l’expedient, respectivament, a partir de la nota numèrica.

Page 80: _curso de programacion en c c plus plus(Català)

6. Enumeracions 6.1. Definició i declaració Les enumeracions permeten definir, com en el cas genèric dels enumerats en pseudocodi, un conjunt fix d’etiquetes. En C, cada etiqueta s’associa a un valor enter constant. Per tant, en C una variable de tipus enumerat és igual que una variable de tipus enter amb l’única diferència que podem utilitzar les etiquetes per referir-nos als valors enters. La sintaxi de la declaració de variables de tipus enumerat en C és la següent:

Igual que passa amb els registres, primer hem de definir el tipus de l’enumeració i després ja podem declarar variables utilitzant aquest tipus d’enumeració. Per definir l’enumeració només hem d’especificar la llista d’etiquetes (en l’exemple NotaNumerica, NotaActa, NotaExpedient). Com ja hem dit prèviament, en C cada etiqueta del tipus enumerat queda associada a un valor enter. Si no s’especifica explícitament cap valor enter, s’assignen automàticament a partir de 0 per la primera etiqueta. En l’exemple hem associat a la primera etiqueta el valor 1. A partir d’aquí s’associen automàticament els valors per la resta de valors de forma consecutiva, és a dir NotaActa queda associat amb el 2, NotaExpedient amb el 3, i així successivament. Aquest exemple d’enumeració pot servir per agrupar les

tipus <nom_tipus> : (valor_1, valor_2, ...... , valor_N>) fitipus var <nom_variable> : <nom_tipus> fivar

Pseudocodi

Sintaxi en C enum <etiqueta_enumerat> {

<etiqueta_valor_1> [= <valor_enter_1>], <etiqueta_valor_2> [= <valor_enter_2>], ... <etiqueta_valor_N> [= <valor_enter_N>],

}; enum <etiqueta_enumerat> <nom_variable> ;

Exemple enum TipusNotes {

NotaNumerica = 1, NotaActa, NotaExpedient

}; enum TipusNotes tipus_nota ;

Page 81: _curso de programacion en c c plus plus(Català)

constants que hem definit a l’exemple anterior en una estructura de tipus enumerat. D’aquesta forma queda més clar quins són els valors que pot prendre la variable del tipus_nota, com es pot veure a l’exemple de la secció següent. 6.2. Utilització Els tipus enumerat serà útil quan vulguem que una variable només pugui prendre valors enters entre un conjunt de constants predefinades. Internament, una variable de tipus enumerat es representa com un valor enter. Per tant, podrem utilitzar tant l’etiqueta com el valor enter per assignar valor a la variable. En l’exemple següent podem veure aquesta dualitat de les variables de tipus enumerat. L’exemple és el mateix que el que s’ha presentat a la secció anterior substituïnt la definició de les constants pel tipus enumerat TipusNotes. Utilitzem el tipus enumerat per representar el tipus de nota que volem guardar. Fixem-nos que al demanar el valor del tipus de la nota utilitzem directament el valor enter. En canvi, a l’estructura switch-case per decidir a quin tipus de nota correspon utilitzem les etiquetes del tipus enumerat. Exemple #include <stdio.h> enum TipusNotes {

NotaNumerica = 1, NotaActa, NotaExpedient

}; union TipusNota {

float nota_numerica; char nota_acta[4]; int nota_expedient;

}; void main () { union TipusNota nota; float valor_nota; enum TipusNotes tipus_nota; printf (“Introdueix la nota final (entre 0 i 10)”); scanf (“%d”, &valor_nota); printf (“Quin tipus de nota vols guardar? (entre 1 i 3)”); scanf (“%d”, &tipus_nota); switch (tipus_nota) { case NotaNumerica:

nota.nota_numerica = valor_nota; printf (“Valor de nota: %f”, nota.nota_numerica);

break; case NotaActa:

ConvertirNotaActa (valor_nota, nota.nota_acta); printf (“Valor de nota: %s”, nota.nota_acta);

break; case NotaExpedient:

Page 82: _curso de programacion en c c plus plus(Català)

nota.nota_expedient = ConvertirNotaExp (valor_nota);

printf (“Valor de nota: %d”, nota.nota_expedient); break; } }

Una altra possible utilitat del tipus enumerat és simular el tipus booleà en aquelles implementacions del llenguatge C que no la suporten. Es podria utilitzar la definició següent: Exemple enum _bool { false, true };

D’aquesta forma, l’etiqueta false queda associada al valor 0 i l’etiqueta true al valor 1, com és habitual en el tipus lògic. Per tant, utilitzant aquest enumerat podem definir variables booleanes i tractar-les de la forma habitual, com podem veure en aquest exemple: Exemple enum _bool { false, true }; void main { _bool encertat; int n,x; const int N_INTENTS=10; n = rand()*100; i = 0; encertat = false; while (!(encertat) && (i < N_INTENTS)) { printf (“Introdueix un número entre 0 i 100\n”); scanf (“%d”, &x); if (x == n) encertat = true; else i++; } if (encertat) printf (“Enhorabona. Has encertat el número\n”); else printf (“Ho sento. Has fallat\n”); }

Page 83: _curso de programacion en c c plus plus(Català)

7. Definició de tipus (typedef) En C no existeix la possibilitat real de crear nous tipus de dades. De totes formes es pot simular utilitzant la paraula reservada typedef, que serveix per renombrar un tipus compost (bàsicament una taula, un registre, una unió o una enumeració) de manera que sigui més fàcil d’utilitzar en els programes, tal com veurem en els exemples següents. La forma d’utilitzar-ho és la següent: Sintaxi typedef <tipus_compost_existent> <nom_nou_tipus>; El que fem és donar un nom nou a un tipus que ja existia. A partir d’aquesta definició podem utilitzar el nom del nou tipus per declarar variables de la mateixa forma que es fa amb els tipus predefinits. La principal aplicació que s’acostuma a donar a typedef és per associar un nou tipus de dades a les estructures de registres, unions o enumeracions. D’aquesta manera, la declaració de les variables es simplifica. Vegem-ho amb l’exemple del registre dels alumnes: Exemple /* Definició del tipus TipusAlumne */ typedef struct registre_alumne { char nom[20]; int grup; int nota_final; } TipusAlumne; /* Declaració d'una variable del tipus TipusAlumne */ TipusAlumne alumne;

En aquesta declaració, al mateix temps que declarem el registre registre_alumne utilitzem typedef per renombrar-lo com un nou tipus de dades que anomenem TipusAlumne. D’aquesta forma es simplifica la declaració de les variables ja que podem utilitzar simplement el nom d’aquest nou tipus de dades enlloc d’haver de posar struct registre_alumne. Una altra aplicació de typedef podria ser per definir nous tipus de dades per simplificar les declaracions de les taules. A l’exemple següent mostrem com es podria definir un tipus genèric cadena, com el que utilitzem en pseudocodi. El tipus cadena el definirem com una taula de 100 caràcters de longitud. Per tant, la seva definició en C seria d’aquesta forma: Exemple /* Declaració del nou tipus cadena com un array de 100 caràcters */ #define MAX_CADENA 100 typedef char cadena[MAX_CADENA]; /* Declaració de la variable c de tipus cadena */ cadena c;

Page 84: _curso de programacion en c c plus plus(Català)

Tema 5: Funcions 1. Funcions i procediments Les funcions i procediments són trossos de codi independents que permeten realitzar una tasca determinada dins del programa. La seva utilitat principal és dividir el programa en un conjunt de mòduls més petits i més fàcilment manipulables. És el que es coneix com a descomposició modular del programa. Cada mòdul (funció o procediment) pot ser utilitzat (cridat) des de qualsevol punt del programa tantes vegades com sigui necessari. Els avantatges principals de la utilització de funcions i procediments són els següents: 1. Simplificació del codi: cada funció o procediment pot realitzar una tasca

concreta (per exemple, calcular el factorial d’un número, multiplicar dues matrius, llegir dades, etc.), que pot ser realitzada molts cops en un mateix programa. Utilitzar funcions i procediments fa que només hàgim d’escriure el codi una sola vegada i, per tant, el nº total de línies de codi disminueix i el codi del programa principal queda simplificat.

2. Reducció d’errors: haver d’escriure només un cop el codi de les funcions independentment del nº de vegades que s’utilitza redueix el nº total d’errors del programa ja que evitem repetir el mateix error cada vegada que s’hauria d’escriure el mateix codi de la funció. A més a més assegurem que cada vegada que s’executa la funció s’està executant exactament el mateix codi de programa. Sense funcions podríem introduir per error diferències en el codi que s’ha d’executar.

3. Estructuració i claredat del codi: amb la utilització de funcions no hem d’escriure el codi tot d’una peça, sinó que el podem dividir i estructurar segons les diferents tasques que s’han de fer. D’aquesta manera, el codi serà més fàcil de llegir i d’entendre i per tant, de corregir.

4. Localització d’errors: cada funció pot ser escrita i provada independentment de la resta del programa. D’aquesta manera, és més fàcil localitzar i corregir els errors del programa, ja que un cop hem identificat la funció que conté l’error, només ens hem de centrar en el codi d’aquesta funció i no en el codi de tot el programa.

Com ja hem comentat, les funcions i procediments són fragments de codi independents, però que a la vegada poden ser utilitzats des de qualsevol punt del programa. Per tant, necessitem algun mecanisme que ens permeti intercanviar dades entre les funcions o procediments i la resta del programa. Els paràmetres i els valors de retorn de les funcions permeten implementar aquesta comunicació. La diferència entre procediments i funcions està en el resultat final del mòdul: les funcions calculen un valor de sortida que es retorna al programa principal quan s’acaba la funció; els procediments, en canvi, no retornen cap valor al punt del programa des d’on són cridats. El llenguatge C, però, no distingeix entre funcions i procediments. Només té el concepte de funció. De totes maneres, com veurem a la secció 3, hi ha una manera d’utilitzar les funcions per simular la definició de procediments en C. En C tot el codi del programa ha d’estar dins d’alguna funció o procediment. De fet, la forma que hem utilitzat fins ara per escriure el programa principal (main) és una definició de procediment. El nom d’aquest procediment indica que és el programa

Page 85: _curso de programacion en c c plus plus(Català)

principal i que, per tant, l’execució del programa comença i acaba en aquest procediment. 2. Declaració i crida de funcions 2.1. Declaració de funcions Igual que passa amb les variables, totes les funcions s’han de declarar abans de la seva utilització. Això vol dir que, normalment, les funcions les declararem i definirem abans del programa principal (main). Per declarar una funció, necessitem especificar el nom (que serveix per identificar-la), els paràmetres (que permeten la comunicació amb el programa principal) i el tipus del valor de retorn (resultat final de la funció). Els paràmetres són els valors que es proporcionen a la funció en el moment de la crida i que són necessaris perquè la funció pugui treballar. Els paràmetres es defineixen com a variables a les que se’ls hi assigna un valor en el moment de la crida a la funció. El valor de retorn és el valor que la funció produeix al final de la seva execució i que es retorna al programa principal com a resultat de la funció. La sintaxi per definir una funció en C és la següent: Sintaxi

Exemple

Els paràmetres es declaren igual que variables: s’ha d’especificar el tipus de dades i el nom que utilitzem dins de la funció per referir-nos-hi. Fixem-nos que, a més a més, la funció té la seva pròpia declaració de variables. És el que anomenem variables locals: variables que s’utilitzen dins de la funció, però que no són paràmetres, és a dir, no serveixen per comunicar-se amb la resta del programa, són internes al codi de la funció. Les funcions ha d’acabar amb la instrucció return. Aquesta instrucció marca el final de la funció i especifica el valor que es retorna al programa principal. El tipus

<tipus_retorn> <nom_funció> (<tipus_paràmetre> <nom_paràmetre>,...) { <Declaració_variables_locals_funció> <Instruccions_funció> return (<valor_retorn>); }

/* Declaració i definició de la funció: nom + tipus valor de retorn + paràmetres */ long factorial (long n) { long fact, i; /* Declaració variables locals de la funció */ long i; fact = 1; for (i=2; i<=n; i++) fact = fact*i; return (fact); /* Retorn del resultat de la funció */ }

Page 86: _curso de programacion en c c plus plus(Català)

d’aquest valor (pot ser una constant, una variable o una expressió qualsevol) ha de coincidir amb el tipus de retorn que s’ha indicat per la funció. A l’exemple es pot veure la definició d’una funció que calcula el factorial d’un número qualsevol. La funció necessita com a paràmetre que se li proporcioni el número del qual volem calcular el seu factorial (paràmetre long n). El valor d’aquest paràmetre es proporcionarà des del programa principal quan es cridi a la funció. Al final de l’execució s’ha calculat el valor del factorial i s’ha guardat a la variable fact. Aquest valor es retorna com a resultat final de la funció (return (fact)). El tipus d’aquesta variable coincideix amb el tipus de retorn especificat per la funció (long). Internament, la funció declara totes les variables locals necessàries per fer aquest càlcul: la variable auxiliar i i la variable fact per guardar el resultat. 2.2. Crida a funcions Després de la seva declaració les funcions poden cridar ser cridades (utilitzades) des de qualsevol punt del programa. Per cridar una funció simplement s’ha de posar el nom de la funció, proporcionar un valor per cada paràmetre i assignar el valor de retorn de la funció a una variable del programa. La crida fa que passi a executar-se el codi de la funció, assignant als paràmetres el valor que s’especifiqui. D’aquesta forma, el mateix codi de la funció pot utilitzar-se per fer càlculs diferents proporcionant cada vegada valors diferents als paràmetres. Si la funció té més d’un paràmetre s’ha d’indicar un valor per cadascun dels paràmetres en el mateix ordre en què han estat declarats a la funció. El tipus dels valors ha de coincidir amb el tipus especificat a la declaració. La sintaxi de la crida a una funció és la següent: Sintaxi <nom_variable> = <nom_funció> (<valor_paràmetre_1>, ...);

Un exemple complet de definició i crida a una funció que permet calcular el factorial d’un número qualsevol és el següent: Exemple /* Factorial.c Programa que calcula el factorial d'un número */ #include <stdio.h> /* Declaració i definició de la funció: nom + tipus valor de retorn + paràmetres */ long factorial (long n) { long fact, i; /* Declaració variables locals de la funció */ fact = 1; for (i=2; i<=n; i++) fact = fact*i; return (fact); /* Retorn del resultat de la funció */ } void main () { long num, res; printf ("Introdueix un numero: "); scanf ("%d", &num);

Page 87: _curso de programacion en c c plus plus(Català)

res = factorial (num); /* Crida a la funció */ printf ("El factorial del numero %d es: %ld\n", num, res); }

Fixem-nos que quan en el programa principal volem calcular el factorial del número que tenim guardat a la variable num posem la crida a la funció factorial, passant el valor de num com a paràmetre. El resultat de l’execució de la funció queda assignat a la variable res, que és la variable que s’utilitza després per mostrar el resultat per pantalla. L’esquema de l’execució d’aquest programa és el següent: void main () { long num, res; printf ("Introdueix un numero: "); scanf ("%d", &num); res = factorial (num); printf ("El factorial del numero %d es: %ld\n", num, res); } En el moment de la crida, es passa el valor del paràmetre num del programa principal a la funció, és a dir s’assigna al paràmetre n el valor que tingui la variable del programa principal num. S’executa el codi de la funció amb aquests valors i al final es passa el valor de retorn fact, de la funció al programa principal, és a dir s’assigna a la variable del programa principal res el valor final de la variable local de la funció fact. 3. Declaració i crida de procediments En C el concepte de procediment no existeix però es pot simular utilitzant funcions. Existeix un tipus de dades, el tipus void, que representa el tipus buit. Serveix per indicar que un objecte (variable o funció) no té un tipus definit. Si en una una funció declarem el valor de retorn de tipus void, estem declarant que la funció no té cap tipus definit i per tant, no pot retornar cap valor. D’aquesta manera convertim la funció en un procediment. L’única diferència entre la declaració de funcions i procediments és que en aquests últims, al no retornar cap valor al final de l’execució, no s’ha d’incloure la instrucció return al final del procediment. Per la resta, la declaració de procediments es fa igual que la declaració de funcions:

res = fact

n = num

long factorial (long n) { long fact; long i; fact = 1;

for (i=2; i<=n; i++) fact = fact*i; return (fact); }

Page 88: _curso de programacion en c c plus plus(Català)

Sintaxi

L’exemple següent mostra un procediment que imprimeix tots els divisors exactes d’un número donat com a paràmetre. Exemple En aquest cas el procediment divisors no ha de calcular cap valor final; simplement ha de mostrar per pantalla la seqüència de divisors exactes del número n. Per tant, no fa falta que retorni cap valor i per això especifiquem el tipus void com a tipus de la funció. A la crida tampoc fa falta recollir cap resultat i per tant, simplement posem el nom del procediment i la llista de paràmetres, sense cap variable per assignar el resultat de la crida. Fixem-nos també que la variable del programa principal i el paràmetre del procediment divisors tenen el mateix nom. No hi ha cap problema en el fet que tinguin el mateix nom perquè són variables diferents, totalment independents, amb valors diferents. Cada funció té les seves pròpies variables, que poden tenir el mateix nom que les variables d’altres funcions.

void <nom_funció> (<tipus_paràmetre> <nom_paràmetre> , ...) { <Declaració_variables_locals_funció> <Instruccions_funció> }

/* Divisors.c Programa que imprimeix els divisors d'un número */ #include <stdio.h> void divisors (int n) /* Declaració i definició del procediment */ { int i; /* Declaració de variables locals del procediment */ for (i=2; i<=n/2; i++) { if (n%i == 0) printf ("%d\n", i); } } void main () { int n; printf ("Introdueix un numero: "); scanf ("%d", &n); divisors (n); /* Crida al procediment */ }

Page 89: _curso de programacion en c c plus plus(Català)

4. Pas de paràmetres Hem vist que els paràmetres són variables que permeten la comunicació entre el programa principal i les funcions. En general, les llenguatges de programació permeten dues formes diferents d’establir aquesta comunicació, el que s’anomena pas de paràmetres per valor i pas de paràmetres per referència: 1. Pas de paràmetres per valor: es caracteritza perquè qualsevol canvi que es faci

al paràmetre dins de la funció, no afecta al valor original de la variable del programa principal utilitzada al fer la crida. És a dir, quan es torna de la funció la variable del programa principal segueix tenint el mateix valor que abans de fer la crida. La comunicació entre el programa principal i la funció és unidireccional: el valor es passa de la variable del programa principal al paràmetre de la funció, però quan la funció acaba, no es torna a passar el valor final de la funció al programa principal.

2. Pas de paràmetres per referència: es caracteritza perquè qualsevol canvi que es faci al paràmetre dins de la funció queda reflectit a la variable del programa principal que s’utilitza al fer la crida. Per tant, aquesta variable pot quedar modificada per l’execució de la funció. La comunicació entre el programa principal i la funció és bidireccional: el valor es passa de la variable del programa principal al paràmetre de la funció en el moment de fer la crida, i quan la funció acaba, es torna a passar el valor final del paràmetre de la funció a la variable del programa principal.

En C, i com a norma general, el pas de paràmetres es fa sempre per valor. El pas de paràmetres per referència no existeix, però es pot simular amb la utilització d’apuntadors tal com s’explica al capítol següent. El fet que el pas de paràmetres sigui sempre per valor implica, com ja s’ha dit, que qualsevol canvi que la funció faci al valor d’un paràmetre no té efecte a la variable que s’utilitza dins el programa principal al fer la crida. Vegem-ho en aquesta nova versió del càlcul del factorial: Exemple /* Factorial.c Programa que calcula el factorial d'un número */ #include <stdio.h> /* Declaració i definició de la funció: nom + tipus valor de retorn + paràmetres */ long factorial (long n) { long fact; /* Declaració de variables locals de la funció */ fact = 1; while (n > 1) { fact = fact*n; n--; /* Modificació del paràmetre n*/ } return (fact); /* Retorn del resultat de la funció */ } void main () { long num, res; printf ("Introdueix un numero: ");

Page 90: _curso de programacion en c c plus plus(Català)

scanf ("%d", &num); /* Crida a la funció. La variable n no queda modificada */ res = factorial (num);

printf ("El factorial del numero %d es: %ld\n", num, res); }

Fixem-nos que la funció factorial modifica el valor del paràmetre n, de manera que quan acaba la funció, sempre té valor 1. Tot i això, el valor de la variable num quan tornem de la crida manté el mateix valor original que tenia abans de la crida a la funció factorial. 5. Àmbit de les variables L’àmbit d’una variable defineix en quines parts del programa pot ser utilitzada. En aquest sentit les variables d’un programa en C es divideixen en dos tipus: variables locals i variables globals. Les variables locals són les variables definides dins de cada funció, incloent en aquesta categoria els paràmetres de les funcions. Per tant, cada funció té les seves pròpies variables locals. Diferents funcions poden tenir variables locals amb el mateix nom. Tot i que tinguin el mateix nom són variables diferents i independents. Les variables locals només poden ser utilitzades dins de la funció on han estat declarades. És a dir, l’àmbit de les variables locals és la funció on estan declarades. Per altra banda, les variables globals són les variables definides fora de qualsevol bloc de funció, normalment al principi del programa. Poden ser utilitzades dins de a qualsevol de les funcions del programa . Hem de tenir present que, en C, el programa principal, main, es defineix com una funció qualsevol. Per tant, les variables definides dins de la funció main són locals a la funció main i només es poden utilitzar dins del main i no a les altres funcions. Per definir variables globals ho hem de fer fora de qualsevol bloc funció (incloent ell main). Regla general: com a regla general, evitarem la utilització de variables globals i només utilitzarem variables locals, sempre que sigui possible. La utilització de variables globals dificulta la comprensió del programa i és una font potencial d’errors. Només en casos molt justificats i on sigui imprescindible, utilitzarem variables i definicions globals, com per exemple, constants, definicions de tipus (typedef) o declaració de registres (struct) que han de ser utilitzats per més d’una funció dins del programa. 5.1. Variables estàtiques En principi, totes les variables locals només existeixen en memòria mentre s’està executant la funció on estan declarades. És a dir, quan es crida a una funció es reserva memòria per guardar totes les seves variables locals i quan la funció acaba s’allibera tot aquest espai de memòria. Això vol dir que al final de l’execució de la funció es perd el valor final de les variables locals; si la funció es torna a executar el valor de la variable es torna a inicialitzar. Les variables estàtiques permenten aconseguir que el valor d’una variable local es mantingui en execucions consecutives de la mateixa funció. Les variables estàtiques són variables que existeixen en memòria durant tota l’execució del programa i no només mentre s’executa la funció on estan declarades. Es reserva espai de

Page 91: _curso de programacion en c c plus plus(Català)

memòria per guardar-les al principi de tot i aquest espai no s’alllibera fins que el programa acaba. D’aquesta manera, el valor final que té una variable al finalitzar una funció es manté a memòria i pot ser utilitzat la següent vegada que cridem a la mateixa funció. Per declarar una variable estàtica només cal que posem la paraula reservada static al davant de la declaració, tal com es mostra a l’exemple següent. En aquest exemple implementem una funció que permet generar una seqüència de nombres aleatoris de forma similar a com ho fa la funció estàndard de la llibreria de C rand(). El mètode es basa en generar una seqüència de nombres a partir d’un nombre inicial, de forma que a cada crida a la funció es genera un nou nombre aleatori de la seqüència a partir del valor de l’últim nombre aleatori que s’ha generat. Per tant, necessitem mantenir el valor de l’últim nombre aleatori generat fins a la següent crida a la funció. La utilització de la variable estàtica num ens permet fer això. Fixem-nos que per fer la declaració de la variable estàtica simplement hem posat la paraula reservada static al davant de la declaració de la variable. D’aquesta manera, la variable no desapareix quan s’acaba la crida a la funció, sinó que es manté a memòria amb el seu valor final. Així, quan tornem a cridar a la funció per generar el següent nombre aleatori la variable num conserva el valor de l’últim nombre generat. La inicialització de la variable amb la constant num_inicial només es fa a la primera crida a la funció. Exemple const long num_inicial = 1; const long a = 214013; const long c = 2531011; const unsigned long m = 2^32-1; unsigned long NumAleatori() { static unsigned long num = num_inicial; num = (a*num + c)%m; return num; }

6. Prototipus de les funcions. Fitxers de capçalera 6.1. Declaració i definició de funcions Tot i que fins ara ho hem considerat sempre conjuntament, en C es distingeix entre la declaració i la definició d’una funció. En la declaració d’una funció només s’indica el nom de la funció, el tipus del valor de retorn i els paràmetres que ha de tenir la funció (el que s’anomena prototipus o capçalera de la funció). En la definició de la funció s’especifica el codi de la funció. En el següent exemple, podem veure el mateix programa de la secció 4.1, però separant la declaració i la definició de les funcions:

Page 92: _curso de programacion en c c plus plus(Català)

Exemple #include <stdio.h> const int MAX = 5; /* Declaració (prototipus) de les funcions */ void LlegirVector(int v[MAX]); void SumaVectors (int v1[MAX], int v2[MAX], int suma[MAX]); void main() { int v1[MAX], v2[MAX], suma[MAX], i; LlegirVector(v1); LlegirVector(v2); SumaVectors(v1,v2,suma); printf ("Suma dels vectors: "); for (i=0; i<MAX; i++) printf ("%d ", suma[i]); } /* Definició de les funcions */ void LlegirVector (int v[MAX]) { int i; for (i=0; i<MAX; i++) { printf ("Introdueix valor %d del vector: ", i); scanf ("%d", &v[i]); } } void SumaVectors (int v1[MAX], int v2[MAX], int suma[MAX]) { int i; for (i=0; i<MAX; i++) suma[i] = v1[i] + v2[i]; }

Veiem que en la declaració de les funcions, només hem posat el nom de la funció, el tipus de retorn i els paràmetres de la funció, tot acabat en un ;, i que les hem posat al principi de tot del programa. L’avantatge de distingir entre declaració i definició està en què només cal que posem la declaració de la funció abans de la seva utilització. La definició de la funció pot estar en qualsevol lloc del programa, com podem veure en l’exemple, que hem posat la definició de les funcions després del programa principal, que és on s’utilitzen. 6.2. Divisió del programa en fitxers. Fitxers de capçalera La distinció entre declaració i definició de funcions permet dividir un programa gran en més d’un fitxer de codi per estructurar-lo millor i simplificar l’escriptura del codi i la seva correcció. Aquesta sol ser una tècnica habitual quan els programes tenen un cert tamany, tot i que en els primers programes que feu potser serà necessari. En l’exemple anterior, podríem dividir el codi en fitxers, posant en un fitxer el programa principal (main) i en un altre fitxer la definició de la resta de funcions. Com el programa principal necessita utilitzar aquestes funcions, necessita conèixer la seva declaració. Normalment, la declaració de les funcions que es defineixen en un fitxer apart es posa en un altre fitxer que conté només declaracions de constants i de funcions, amb extensió .h, i que s’anomena fitxer de capçalera.

Page 93: _curso de programacion en c c plus plus(Català)

Aquest fitxer es pot incloure (#include) en el programa principal, com feu amb els fitxers de llibreria estàndard (stdio.h, string.h, etc.). Tot això quedaria de la manera següent: Exemple

/* Fitxer funcions.h */ /* Fitxer que conté la declaració de constants i funcions que s’han d’utilitzar al programa. Aquest fitxer pot ser inclòs (#include) per qualsevol altre fitxer que necessiti utilitzar la constant o les funcions */ #define MAX 5 /* Declaració (prototipus) de les funcions */ void LlegirVector(int v[MAX]); void SumaVectors (int v1[MAX], int v2[MAX], int suma[MAX]);

/* Fitxer funcions.c */ /* Fitxer que conté la definició de les funcions declarades a funcions.h */ #include <stdio.h> #include "funcions.h" /* Definició de les funcions */ void LlegirVector (int v[MAX]) { int i; for (i=0; i<MAX; i++) { printf ("Introdueix valor %d del vector: ", i); scanf ("%d", &v[i]); } } void SumaVectors (int v1[MAX], int v2[MAX], int suma[MAX]) { int i; for (i=0; i<MAX; i++) suma[i] = v1[i] + v2[i]; }

Page 94: _curso de programacion en c c plus plus(Català)

7. Funcions de llibreria estàndard El llenguatge C té predefinides un conjunt de funcions (que en diem funcions de la llibreria estàndard) que permeten fer operacions bàsiques i comuns, com per exemple, llegir i escriure valors, calcular funcions matemàtiques o operar amb cadenes. Fins ara ja hem utilitzat algunes d’aquestes funcions (scanf, printf, strcpy, strcmp). Per poder utilitzar aquestes funcions, es fa servir el mecanisme de declaració i definició de funcions que hem explicat a l’apartat anterior. Totes aquestes funcions estan declarades en diferents fitxers de capçalera. És per això que quan en un programa les volem utilitzar, hem d’incloure (#include) el fitxer on estan declarades, com hem fet fins ara, per exemple, amb el fitxer stdio.h sempre que volíem llegir o escriure dades. A continuació teniu una sèrie de taules corresponent a diferents fitxers de capçalera de la llibreria estàndard, indicant algunes de les funcions més comunes que estan declarades en cadascun d’aquests fitxers, i que us poden ser d’utilitat. En qualsevol llibre o manual de C podeu trobar el llistat complet de funcions que estan declarades a cada fitxer de capçalera: Funcions d’entrada/sortida: fitxer <stdio.h> int printf (char *format, ...) Escriure per pantalla int scanf (char *format, ...) Llegir dades per teclat int getchar() Llegir un caràcter per teclat char *gets(char *s) Llegir una cadena de caràcters per teclat int puts(char *s) Escriu una cadena de caràcters per pantalla Funcions de tractament de caràcters: fitxer <ctype.h> int isalpha(int c) Retorna cert si c és una lletra majúscula o minúscula. int isdigit(int c) Retorna cert si c és un dígit. int isupper(int c) Retorna cert si c és una lletra majúscula. int islower(int c) Retorna cert si c és una lletra minúscula. int isalnum(int c) Retorna cert si c és una lletra (majúscula o minúscula) o un

/* Fitxer principal.c *//* Fitxer que conté el codi del programa principal. Com aquest fitxer necessita utilitzar les funcions declarades a funcions.h necessita incloure aquest fitxer */ #include <stdio.h> /* Inclusió de la declaració de les funcions declarades a funcions.h */ #include "funcions.h" void main() { int v1[MAX], v2[MAX], suma[MAX], i; LlegirVector(v1); LlegirVector(v2); SumaVectors(v1,v2,suma); printf ("Suma dels vectors: "); for (i=0; i<MAX; i++) printf ("%d ", suma[i]); }

Page 95: _curso de programacion en c c plus plus(Català)

dígit. int tolower(int c) Converteix c a una lletra minúscula. int toupper(int c) Converteix c a una lletra majúscula. Funcions numèriques: fitxer <math.h> double ceil(double x) Retorna l’enter més proper i més gran que x. double floor(double x) Retorna l’enter més proper i més petit que x. double fabs(double x) Retorna el valor absolut de x. double pow (double x, double y) Retorna yx . double sqrt(double x) Retorna l’arrel quadrada de x. double cos(double x) Retorna el cosinus de x, expressat en radians. double sin(double x) Retorna el sinus de x, expressat en radians. double tan(double x) Retorna la tangent de x, expressat en radians. double asin(double x) Retorna el arc sinus de x, expressat en radians. double acos(double x) Retorna el arc cosinus de x, expressat en radians. double atan(double x) Retorna el arc tangent de x, expressat en radians. double exp(double x) Retorna xe . double log(double x) Retorna el logaritme natural de x. Funcions generals: fitxer <stdlib.h> int rand() Retorna un nombre aleatori en el rang 0-RAND_MAX. void srand(int n) Inicialitza el generador de nºs aleatoris amb el valor n. int abs(int n) Retorna el valor absolut de n.

Page 96: _curso de programacion en c c plus plus(Català)

Tema 6: Apuntadors 1. Conceptes bàsics Un apuntador és una variable que guarda el valor d’una adreça de memòria. Aquesta adreça de memòria pot correspondre a la posició que ocupa una altra variable del programa o bé a la posició del que anomenem un objecte dinàmic. Els objectes dinàmics els explicarem més endavant, així que de moment, suposarem que l’adreça de memòria que tenim guardada en un apuntador correspon a una de les variables del programa. L’esquema següent ens mostra una variable de tipus apuntador, p, que conté l’adreça de memòria d’una altra variable del programa, x, de tipus enter. Concepte d’apuntador

Com es pot veure a la figura, l’apuntador p es converteix en una referència a la variable x, ja que amb l’adreça de memòria guardada a p podem localitzar on està guardada la variable x i accedir al seu valor. Per això diem que l’apuntador p “apunta” a la variable x. Anem a veure ara com podem declarar una variable de tipus apuntador i com podem fer que apunti a una determinada variable. 1.1. Declaració d’apuntadors Els apuntadors s’han de declarar igual que fem amb qualsevol altra variable del programa. La declaració d’un apuntador es fa de la manera següent: Sintaxi <tipus_de_dades> *<nom_variable>; Exemple int *p;

@ x

5

x:

p:

@ x

Memòria

Page 97: _curso de programacion en c c plus plus(Català)

És a dir, per declarar una variable com un apuntador, posem el símbol “*” al davant del nom de la variable i especifiquem el tipus de dades de l’objecte apuntat per l’apuntador. Amb la declaració de l’exemple (int *p) estem declarant la variable p com un apuntador que contindrà l’adreça d’un objecte de tipus enter i, per tant, la variable apuntada per p haurà de ser de tipus enter. 1.2. Operadors Per treballar amb variables de tipus apuntador, tindrem dos nous operadors (operador & i operador *) que ens permetran assignar una adreça de memòria a un apuntador i accedir al contingut de la variable apuntada per un apuntador. 1.2.1. Operador & Aquest operador retorna l’adreça de memòria d’una variable del programa. Normalment s’utilitza per assignar l’adreça de la variable a un apuntador. El tipus de la variable s’ha de correspondre amb el tipus de l’apuntador. Sintaxi <apuntador> = &<nom_variable>; Exemple int x; int *p; p = &x;

Amb la instrucció p=&x; el que fem és assignar a la variable p (que és un apuntador), l’adreça de memòria de la variable x, @x. A partir del moment que assignem l’adreça de memòria de la variable x a l’apuntador p, podem dir que “l’apuntador p apunta a la variable x” i, per tant, es converteix en una referència alternativa al valor de la variable. L’operador * és el que ens permetrà accedir al valor de la variable x utilitzant l’apuntador p. 1.2.2. Operador * Aquest operador retorna el contingut de la variable apuntada (o referenciada) per un apuntador. D’aquesta manera podem consultar i modificar el valor de la variable utilitzant l’apuntador. Sintaxi *<apuntador>

@ x

x:

p:

@ x

Memòria

p: x:

Page 98: _curso de programacion en c c plus plus(Català)

Exemple int x, y; int *p; p = &x; x = 10; y = *p; *p = 5; Fixem-nos que a partir del moment en què fem l’assignació p=&x, és equivalent utilitzar la variable x o el contingut de l’apuntador (*p). És a dir, *p ho podem utilitzar com una forma alternativa de consultar o modificar el valor de la variable x. Veiem un altre exemple d’utilització d’apuntadors per accedir al contingut de variables del programa. Els dos programes que posem a continuació són equivalents i serveixen per calcular la suma dels n primers nombres naturals. El primer programa no utilitza apuntadors, mentre que el segon utilitza apuntadors per accedir al valor de les variables: Exemple En aquest exemple podem veure que quan definim els tres apuntadors, pi, psuma i pn, i els hi assignem les adreces de memòria de les variables i, suma i n, respectivament, podem utilitzar les expressions *pi, *psuma i *pn per accedir al valor de les variables, sense cap canvi en el resultat del programa. De totes maneres, en aquest cas concret, la utilització d’apuntadors no és de massa utilitat, perquè el mateix que fem amb apuntadors ho podem fer sense. Com veurem més endavant, la utilitat real dels apuntadors estarà en el pas de paràmetres per referència i en la manipulació d’objectes dinàmics.

p: x: 10 y:

p: x: 10 y: 10

p: x: 5 y: 10

/* Programa que suma dels n primers nombres naturals */ #include <stdio.h> void main () { int i, suma, n; printf ("Introdueix un nombre enter positiu\n"); scanf ("%d", &n); suma = 0; for (i=1; i<=n; i++) suma = suma + i; printf ("El resultat de la suma és %d\n", suma); }

Page 99: _curso de programacion en c c plus plus(Català)

1.3. Precaucions amb la utilització d’apuntadors Els apuntadors són una eina molt potent i molt utilitzada en C, com anirem veient en les properes seccions. De totes maneres, hem de prestar molta atenció a utilitzar correctament els apuntadors, ja que una utilització incorrecta d’aquests pot donar lloc a errors d’execució greus difícils de detectar. Un dels principals problemes dels apuntadors és utilitzar-los sense haver-los inicialitzat, és a dir que conté una adreça de memòria no controlada. Fixem-nos en el següent exemple, que és una modificació de l’exemple anterior: Exemple /* Programa que il·lustra la utilització d’apuntadors Suma dels n primers nombres naturals */ #include <stdio.h> void main () { int i, suma, n, *pi, *psuma, *pn; printf ("Introdueix un nombre enter positiu\n"); scanf ("%d", &n); suma = 0; /* ERROR: accés a apuntadors no incialitzats */ for (i=1; i<=*pn; i++) *psuma = suma + *pi; printf ("El resultat de la suma és %d\n", suma); }

Respecte a la primera versió, hem suprimit les instruccions que incialitzaven els apuntadors pn, psuma i pi amb les adreces de memòria de les variables n, suma i i. D’aquesta manera, quan utilitzem els apuntadors en el bucle que calcula la suma, no tenim controlat on apunten perquè no estan incialitzats. De fet, és el mateix problema que utilitzar una variable qualsevol no inicialitzada. Amb els apuntadors, però, el problema és més greu, ja que al ser adreces de memòria podem estar accedint a qualsevol adreça de memòria, que pot ser una adreça vàlida o no. Si l’adreça no és vàlida el programa s’aturarà i es mostrarà un missatge d’error en l’accés a memòria. Si, pel contrari, l’adreça és vàlida, correspondrà a la posició de memòria d’alguna altra variable del programa. En aquest cas, estaríem modificant el valor d’aquesta variable, que no podem saber

/* Programa que il·lustra la utilització d’apuntadors. Suma dels n primers nombres naturals */ #include <stdio.h> void main () { int i, suma, n, *pi, *psuma, *pn; printf ("Introdueix un nombre enter positiu\n"); scanf ("%d", &n); pn = &n; suma = 0; psuma = &suma; pi = &i; for (i=1; i<=*pn; i++) *psuma = suma + *pi; printf ("El resultat de la suma és %d\n", suma); }

Page 100: _curso de programacion en c c plus plus(Català)

quina és, i per tant, podríem estar provocant comportaments “estranys” en el nostre programa, difícils de detectar i corregir. Tinguem present que aquest tipus d’errors no els detecta el compilador, només es poden detectar executant el programa. Per tant, és molt important inicialitzar correctament tots els apuntadors a una adreça de memòria vàlida i controlada. 1.4. Valor NULL Com acabem de dir, l’accés amb apuntadors a adreces de memòria no vàlides és una de les principals fonts d’errors en els programes en C. Per això, s’introdueix un valor especial (el valor NULL) que serveix per indicar que un apuntador no conté una adreça de memòria vàlida. El valor NULL és una constant predefinida al fitxer de capçalera stdlib.h, associada amb l’adreça de memòria 0. Intentar accedir al contingut d’un apuntador que tingui el valor NULL sempre provocarà que el programa s’aturi amb un missatge d’error d’execució Un bon hàbit de programació consisteix en inicialitzar tots els apuntadors amb el valor NULL. D’aquesta manera, sempre podem comprovar, abans d’utilitzar un apuntador, si té una adreça vàlida diferent de NULL, i evitar accessos no permesos. I, per altra banda, si es dóna el cas que accedim a un apuntador inicialitzat a NULL, ens assegurem que el programa s’aturarà amb un missatge d’error i que no modificarà el valor de cap altra variable del programa. D’aquesta forma, serà més fàcil detectar i corregir els errors. Tot i que la utilització principal del valor NULL està relacionada amb l’ús de memòria dinàmica, el següent exemple mostra com inicialitzar un apuntador a NULL i com comprovar si l’apuntador té un valor vàlid abans d’utilitzar-lo: Exemple /* Exemple d’utilització del valor NULL en apuntadors */ #include <stdio.h> void main () { float x,invers; float *p_x=NULL,*p_invers=NULL; printf ("Introdueix un nombre\n"); scanf ("%f", &x); if (x!=0) { p_x = &x; p_invers = &invers; *p_invers = 1/(*p_x); } if (p_x != NULL) printf ("L'invers de %f és %f", *p_x, *p_invers); }

Fixem-nos que només imprimirem el contingut de p_x i de p_invers si els hem inicialitzat correctament, és a dir si x!=0, que és quan els hi assignem l’adreça de x i de invers. Si x==0, els dos apuntadors seguiran tenint el valor NULL i no s’imprimirà res. Com el valor NULL indica una adreça no vàlida, molts cops s’utilitza en funcions que han de retornar un apuntador per indicar algun tipus d’error en l’execució de la funció que fa que no es pugui assignar un valor vàlid a l’apuntador.

Page 101: _curso de programacion en c c plus plus(Català)

2. Pas de paràmetres per referència utilitzant apuntadors En el tema anterior es va comentar que en C el pas de paràmetres a les funcions sempre es fa per valor, i que el pas de paràmetres per referència s’havia de simular utilitzant apuntadors. Ara que ja sabem què són i com s’utilitzen els apuntadors, anem a veure com es poden aprofitar per simular el pas de paràmetres per referència. Recordem primer que el pas de paràmetres per referència vol dir que qualsevol canvi que es faci en un paràmetre dins d’una funció, automàticament queda reflectit en el valor de la variable que s’ha utilitzat en el moment de fer la crida. En C, el pas de paràmetres per referència es farà definint el paràmetre com un apuntador. Per tant, en el moment de la crida a la funció no li haurem de passar el valor de la variable, sinó l’adreça de memòria de la variable. D’aquesta manera, l’apuntador que actua de paràmetre es converteix en una referència a la variable utilitzada a la crida i qualsevol canvi que fem al contingut de l’apuntador queda automàticament reflectit al valor de la variable. El següent exemple ens mostra el funcionament del pas de paràmetres per referència en una funció que serveix per ordenar tres valors de tipus enter: Exemple /* Exemple de pas de paràmetres per referència */ #include <stdio.h> void ordenar (int *primer, int *segon, int *tercer) { int tmp; if (*primer > *segon) { tmp = *primer; *primer = *segon; *segon = tmp; } if (*primer > *tercer) { tmp = *primer; *primer = *tercer; *tercer = tmp; } if (*segon > *tercer)

primer=&n1 segon=&n2 tercer=&n3

n1:

n2:

n3:

ordenar (&n1, &n2, &n3)

ordenar (int *primer, int *segon, int *tercer)

primer

segon

tercer

main

Page 102: _curso de programacion en c c plus plus(Català)

{ tmp = *segon; *segon = *tercer; *tercer = tmp; } } void main () { int n1, n2, n3; printf ("Introdueix tres nombres enters: \n"); scanf ("%d %d %d", &n1, &n2, &n3); ordenar (&n1, &n2, &n3); printf ("Els tres nombres ordenats són: %d %d %d\n", n1, n2, n3); }

La funció ordenar ha de modificar els valors que se li passen, de manera que quan acabi estiguin ordenats de més petit a més gran. Per tant, el pas de paràmetres ha de ser per referència. Fixem-nos que els paràmetres es declaren com apuntadors i que, per tant, en el moment de la crida es passa l’adreça de memòria de les variables (&n1, &n2, &n3), i no el seu valor (n1, n2, n3). D’aquesta forma, s’aconsegueix l’enllaç entre els apuntadors de la funció ordenar (primer, segon i tercer) i les variables del programa principal (n1, n2 i n3), tal com es mostra en el diagrama. Per tant, quan a la funció ordenar es modifica el contingut de primer, segon i tercer (*primer, *segon i *tercer), es modifica també el valor de les variables n1, n2 i n3. 3. Apuntadors i arrays De la mateixa manera que hem vist la utilització d’apuntadors per referenciar variables dels tipus bàsics, podem utilitzar els apuntadors per referenciar elements d’un array. De fet, els elements d’un array són com variables de tipus bàsic, que també tenen una posició de memòria que pot ser referenciada per un apuntador. Així, per exemple, podem definir un array d’enters i un apuntador a enter, i utilitzar aquest apuntador per referenciar la posició de memòria d’un dels elements de l’array de la següent manera: Exemple int v[10]; int *p; p = &v[0]; *p = 5;

Amb p = &v[0] estem fent que l’apuntador p contingui l’adreça del primer element de l’array, de manera que *p permet accedir al primer element de l’array. Per tant, com a resultat del codi anterior la primera posició de l’array conté el valor 5. Però de fet, en C la relació entre apuntadors i arrays és molt més estreta, de manera que podem utilitzar apuntadors per recórrer fàcilment tots els elements d’un array. Aquest fet serà molt útil pel pas d’arrays com a paràmetres a les funcions i per la gestió d’arrays dinàmics.

Page 103: _curso de programacion en c c plus plus(Català)

En primer lloc, per fer que un apuntador contingui l’adreça del primer element de l’array podem simplificar l’expressió que hem utilitzat anteriorment per p = v, de manera que el següent codi és equivalent a l’anterior: Exemple int v[10]; int *p; p = v; /* equivalent a p=&v[0] */ *p = 5; A partir d’aquest moment l’apuntador p es pot utilitzar per accedir a tots els elements de l’array. Això ho podrem fer gràcies a que s’introdueixen nous operadors que es poden aplicar a un apuntador que s’utilitza per referenciar els elements d’un array. Aquests operadors són els següents: − Operadors aritmètics: podem sumar o restar a l’apuntador un determinat valor

numèric (p+n), i això farà que l’apuntador avanci o retrocedeixi n posicions dins de l’array. D’aquesta manera ens podem desplaçar per tots els elements de l’array.

− Operadors d’autoincrement i autodedrecrement: podem utilitzar amb l’apuntador els operadors d’autoincrement (p++) i autodecrement (p--). Això fa que l’apuntador passi a apuntar a l’element següent o anterior dins de l’array.

− Operador d’indexació []: podem utilitzar també l’apuntador p com si fos un array i utilitzar l’expressió p[n]. D’aquesta manera estem accedint a la posició n, dins de l’array. És equivalent a utilitzar l’expressió *(p+n).

La figura següent mostra com es poden utilitzar aquests operadors per accedir als elements de l’array utilitzant l’apuntador p. És a dir, si p és un apuntador que apunta al primer element d’un array v, les expressions v[n], p[n] i *(p+n) són equivalents. Per altra banda, l’expressió p++,

p++; *p = 10;

0 5 1 2 3 4 5 6 7 8 9

vp:

v[0] → 0 5 ← p[0] ↔ *p v[1] → 1 ← p[1] ↔ *(p+1) v[2] → 2 ← p[2] ↔ *(p+2) v[3] → 3 ← p[3] ↔ *(p+3) v[4] → 4 ← p[4] ↔ *(p+4) v[5] → 5 ← p[5] ↔ *(p+5) v[6] → 6 ← p[6] ↔ *(p+6) v[7] → 7 ← p[7] ↔ *(p+7) v[8] → 8 ← p[8] ↔ *(p+8) v[9] → 9 ← p[9] ↔ *(p+9)

v: p:

0 5 1 10 2 3 4 5 6 7 8 9

v p:

Page 104: _curso de programacion en c c plus plus(Català)

fa que l’apuntador p passi a apuntar al següent element de l’array. Això és vàlid per qualsevol tipus d’array. Llavors, tenint en compte aquestes equivalències, un programa que llegeixi les dades d’un vector d’enters i calculi el màxim es pot expressar de qualsevol d’aquestes quatre formes: Exemple

/* Exemple d’accés a arrays mitjançant punters */ #include <stdio.h> void main () { int v[10]; int i, maxim; for (i=0; i<10; i++) scanf ("%d", &v[i]); maxim = v[0]; for (i=1; i<10; i++) if (v[i] > maxim) maxim = v[i]; printf ("Maxim: %d\n", maxim);

#include <stdio.h> void main () { int v[10]; int i, maxim; int *p; p = v; for (i=0; i<10; i++) scanf ("%d", &p[i]); maxim = p[0]; for (i=1; i<10; i++) if (p[i] > maxim) maxim = p[i]; printf ("Maxim: %d\n", maxim); }

Page 105: _curso de programacion en c c plus plus(Català)

En la primera versió fem servir la notació tradicional d’accés a arrays que havíem utilitzat fins ara. A la segona utilitzem l’operador d’indexació aplicat sobre l’apuntador (p[i]). A la tercera utilitzem el contingut de l’apuntador (*p) i l’operador d’autoincrement per anar avançant per tots els elements de l’array. Finalment, a la quarta versió accedim als elements de l’array fent servir l’operador aritmètic (p+i) amb una variable índex que permet recórrer tots els elements de l’array. 3.1. Precaucions en la utilització d’arrays i apuntadors Com hem vist, en C hi ha molta similitud en la forma de gestionar arrays i apuntadors a arrays. Tot i aquestes similituds, s’han de tenir molt clares també les diferències entre arrays i apuntadors a arrays. Bàsicament, un array té assignat

#include <stdio.h> void main () { int v[10]; int i, maxim; int *p; p = v; for (i=0; i<10; i++) scanf ("%d", p+i); maxim = *p; for (i=1; i<10; i++) if (*(p+i) > maxim) maxim = *(p+i); printf ("Maxim: %d\n", maxim); }

#include <stdio.h> void main () { int v[10]; int i, maxim; int *p; p = v; for (i=0; i<10; i++) { scanf ("%d", p); p++; } p = v; maxim = *p; for (i=1; i<10; i++) { if (*p > maxim) maxim = *p; p++; } printf ("Maxim: %d\n", maxim); }

Page 106: _curso de programacion en c c plus plus(Català)

un espai de memòria i per tant, l’adreça d’un array no es pot modificar, mentre que un apuntador necessita inicialitzar-se amb una adreça de memòria vàlida abans d’utilitzar-se. Els següents exemples il·lustren els errors més freqüents que es poden cometre si no es tenen clares les diferències entre arrays i apuntadors: Exemple /* Exemple d’error de punter no inicialitzat */ #include <stdio.h> void main () { int *p; int i, maxim; /* ERROR: Accés a un apuntador no incialitzat */ for (i=0; i<10; i++) scanf ("%d", &p[i]); maxim = p[0]; for (i=1; i<10; i++) { if (p[i] > maxim) maxim = p[i]; } printf ("Maxim: %d\n", maxim); }

Aquest programa pretén fer el mateix que els exemples anteriors, calcular el màxim en un vector d’enters. L’error ve de confondre l’apuntador p amb un array. Fixeu-vos que en aquest cas no inicialitzem l’apuntador p i, per tant, quan llegim els valors, l’apuntador no conté cap adreça vàlida i el programa genera un error d’execució. La solució passa, com ja hem vist abans, per declarar un array de 10 posicions i assignar l’adreça del primer element de l’array a l’apuntador abans d’utilitzar-lo. Exemple /* Exemple d’error d’utilització de punters */ #include <stdio.h> void main () { int v[10]; int i, maxim; for (i=0; i<10; i++) scanf ("%d", &v[i]); maxim = *v; for (i=1; i<10; i++) { if (*v > maxim) maxim = *v; v++; } printf ("Maxim: %d\n", maxim); }

En aquest exemple, la confusió ve de considerar l’array v com un apuntador i intentar utilitzar l’operador d’autoincrement v++. Amb aquesta expressió estem intentar modificar l’adreça de memòria de l’array i això no es pot fer perquè un array

Page 107: _curso de programacion en c c plus plus(Català)

té assignat un espai de memòria fix i inamovible. En aquest cas, es produeix un error de compilació. 3.2. Pas per referència d’arrays unidimensionals L’exemple següent serveix per il·lustrar els pas d’arrays com a paràmetres: Exemple /* Programa que llegeix i suma dos vectors */ #include <stdio.h> const int MAX = 5; void LlegirVector (int v[MAX]) { int i; for (i=0; i<MAX; i++) { printf ("Introdueix valor %d del vector: ", i); scanf ("%d", &v[i]); } } void SumaVectors (int v1[MAX], int v2[MAX], int suma[MAX]) { int i; for (i=0; i<MAX; i++) suma[i] = v1[i] + v2[i]; } void main() { int v1[MAX], v2[MAX], suma[MAX]; int i; LlegirVector(v1); LlegirVector(v2); SumaVectors(v1,v2,suma); printf ("Suma dels vectors: "); for (i=0; i<MAX; i++) printf ("%d ", suma[i]); } En aquest exemple tenim un programa que llegeix valor deper dos vectors d’enters i en calcula la seva la suma dels dos vectors, utilitzant procediments tan per llegir les dades del vector i calcular la suma. Podem veure com la declaració dels arrays com a paràmetres es fa igual que amb paràmetres dels altres tipus bàsics, declarant cada paràmetre de la manera habitual com hem declarat les variables de tipus array. La diferència està en què, en aquest cas, les modificacions que el procediment LlegirVector fa al paràmetre v, quan tornem al programa principal queden reflectides en els valors de les variables v1 i v2, utilitzades al fer la crida, i que també veuen modificats els seus valors. Igualment, els canvis que el procediment

Page 108: _curso de programacion en c c plus plus(Català)

SumaVectors fa al paràmetre suma també es traspassen, després de la crida, a la variable suma del programa principal, utilitzada al fer la crida. En el tema anterior hem explicat que, tot i que el pas de paràmetres en C sempre es fa per valor, els arrays es passen per referència. El motiu és l’equivalència entre arrays i apuntadors que hem introduït a les seccions anteriors. En realitat, quan en una crida a una funció, utilitzem un array com a paràmetre, el que es passa a la funció no és una còpia de tot l’array, sinó simplement l’adreça del primer element de l’array. D’aquesta manera, dins de la funció, l’array es pot tractar com un apuntador utilitzant els operadors que hem introduït a la secció anterior i, per tant, tots els canvis que es facin dins de la funció es reflecteixen directament a l’array original. Des d’aquest punt de vista, podríem reescriure l’exemple de la secció 4.1 del tema anterior que il·lustrava el pas d’arrays com a paràmetres, de la següent manera, utilitzant apuntadors per definir els paràmetres de les funcions: Exemple /* Programa que llegeix i suma dos vectors */ #include <stdio.h> #define MAX 5 void LlegirVector (int *v, int n_elements) { int i; for (i=0; i<n_elements; i++) { printf ("Introdueix valor %d del vector: ", i); scanf ("%d", &v[i]); } } void SumaVectors (int *v1, int *v2, int *suma, int n_elements) { int i; for (i=0; i<n_elements; i++) suma[i] = v1[i] + v2[i]; } void main() { int v1[MAX], v2[MAX], suma[MAX]; int i; LlegirVector(v1,MAX); LlegirVector(v2,MAX); SumaVectors(v1,v2,suma,MAX); printf ("Suma dels vectors: "); for (i=0; i<MAX; i++) printf ("%d ", suma[i]); }

Page 109: _curso de programacion en c c plus plus(Català)

Fixem-nos que el que hem canviat ha estat la definició dels paràmetres dins de la funció. Enlloc de definir-los com si fossin un array, els hem definit com apuntadors (int v[MAX] per int *v). Fixem-nos, però, que tant la crida a la funció com la manera d’utilitzar l’array dins de la funció no fa falta canviar-los degut a l’equivalència entre apuntadors i arrays i al fet que l’apuntador agafa l’adreça del primer element de l’array, tal com il·lustra la següent figura:

Fixem-nos també en un altre canvi que hem introduït: hem afegit un nou paràmetre a les funcions que indica el nº d’elements que hi ha a l’array. Aquesta és una tècnica habitual quan es passen arrays com a paràmetres a les funcions. En aquest cas, simplement ens evita utilitzar la constant MAX dins de la funció, de manera que tots els valors que s’utilitzen dins de la funció són locals. Quan treballem amb arrays dinàmics (explicats més endavant) serà imprescindible introduir aquest paràmetre per poder saber el nº d’elements de l’array dins de la funció. Finalment, hi ha una tercera notació que en C podem utilitzar per passar un array a una funció, que ens permet marcar l’equivalència entre arrays i apuntadors. Per exemple, la funció anterior la podríem declarar de la forma següent: LlegirVector (int v[], int n_elements) És a dir, definim el paràmetre com un array sense dimensió. Això és equivalent a definir el paràmetre com un apuntador, igual que hem fet abans. Per tant, el codi de la funció i la crida a la funció no s’haurien de canviar. 3.3. Pas per referència d’arrays multidimensionals En el cas dels arrays multidimensionals, la idea és similar. Si passem un array multidimensional a una funció, no fem una còpia de tot l’array, sinó que passem l’adreça del primer element de l’array, que correspon a l’element de la primera fila i primera columna. En aquest cas, però si volem tractar el paràmetre dins de la funció com un array multidimensional, no podem definir el paràmetre simplement com un apuntador degut a la forma en què els arrays multidimensionals es guarden en memòria. Com que es guarden tots els elements per files, per accedir a un determinat element de l’array, el compilador necessita saber quantes columnes hi ha a l’array i, per tant, aquesta informació s’ha de proporcionar quan definim el paràmetre a la funció. Per això la definició de paràmetres que corresponen a arrays multidimensionals segueix l’última notació explicada a la secció anterior, però ens obliga a especificar el nº de columnes de l’array, com podem veure en aquest exemple, que permet

v = v1 (v = &v1[0])

v10 1 2 3 4

LlegirVector(v1,MAX);

void LlegirVector(int *v,int n elements);

v:

main

Page 110: _curso de programacion en c c plus plus(Català)

llegir dues matrius i sumar-les. Fixem-nos que els paràmetres es defineixen com arrays en què només s’especifica la segona dimensió, és a dir, el nº de columnes (m1[][N_COLUMNES]). Exemple /* Programa que llegeix i suma dues matrius */ #include <stdio.h> #define N_FILES 2 #define N_COLUMNES 3 void LlegirMatriu (int m[][N_COLUMNES]) { int i, j; for (i=0; i<N_FILES; i++) for (j=0; j<N_COLUMNES; j++) { printf ("Introdueix valor de la fila %d i columna %d: ", i, j); scanf ("%d", &m[i][j]); } } void SumaMatrius (int m1[][N_COLUMNES], int m2[][N_COLUMNES], int suma[][N_COLUMNES]) { int i, j; for (i=0; i<N_FILES; i++) for (j=0; j<N_COLUMNES; j++) suma[i][j] = m1[i][j] + m2[i][j]; } void main() { int m1[N_FILES][N_COLUMNES], m2[N_FILES][N_COLUMNES]; int suma[N_FILES][N_COLUMNES]; int i,j; LlegirMatriu(m1); LlegirMatriu(m2); SumaMatrius(m1,m2,suma); printf ("Suma de les matrius:\n"); for (i=0; i<N_FILES; i++) { for (j=0; j<N_COLUMNES; j++) printf ("%d ", suma[i][j]); printf ("\n"); } }

Page 111: _curso de programacion en c c plus plus(Català)

4. Apuntadors a registres Tot el que hem explicat fins ara d’apuntadors es pot aplicar igualment a registres. En aquest cas, si tenim un apuntador p que apunta a un objecte de tipus registre, l’expressió *p fa referència a tot el registre i, per tant, per accedir a un dels camps del registre, hauríem de posar l’expressió (*p).nom_camp, De totes maneres, en C, aquesta notació gairebé no s’utilitza i sempre s’utilitza una notació simplificada per accedir als camps d’un registre a partir d’un apuntador. El canvi de notació és el següent: Accés als camps d’un registre amb apuntadors

L’exemple següent mostra la utilització d’aquesta notació per accedir als camps d’un registre amb apuntadors, en un programa simple que llegeix les dades d’un alumne i les guarda en un registre: Exemple /* Utilització d'apuntadors a registres */ #include <stdio.h> typedef struct { char nom[20]; int grup; int nota_final; } reg_alumne; void main() { reg_alumne alumne, *p_alumne; p_alumne = &alumne; printf ("introdueix el nom de l'alumne: "); gets (p_alumne->nom); printf ("Introdueix el grup de l'alumne: "); scanf ("%d", &p_alumne->grup); printf ("Introdueix la nota de l'alumne: "); scanf ("%d", &p_alumne->nota_final); }

4.1. Pas de registres per referència Tot i que els registres es poden passar com a paràmetres per valor a les funcions, normalment s’utilitza el pas per referència amb apuntadors per definir els paràmetres de tipus registre a les funcions. El motiu és que el pas per valor implica una còpia dels valors de la variable del programa principal a la variable paràmetre de la funció. Com que els registres acostumen a ser variables que ocupen una quantitat significativa de memòria, aquesta còpia suposa un cost en temps i espai de memòria que ens estalviem si fem el pas per referència.

p->nom_camp (*p).nom_camp

Page 112: _curso de programacion en c c plus plus(Català)

L’exemple següent ens mostra com s’utilitza el pas de paràmetres per referència en una funció (EscriureAlumne), encara que les dades del registre no es modifiquen dins de la funció. L’exemple permet llegir les dades de tots els alumnes i guardar-les en un array. Per llegir les dades d’un alumne també s’utilitza una funció (LlegirAlumne) amb pas de registres per referència. Després de llegir les dades dels alumnes, es demana el nom d’un alumne, es busca dins de l’array i s’escriuen les dades de l’alumne utilitzant la funció EscriureAlumne. Exemple /* Utilització d'apuntadors a registres. Pas per referència */ #include <stdio.h> #include <string.h> #define MAX 100 typedef struct { char nom[20]; int grup; int nota_final; } reg_alumne; void LlegirAlumne (reg_alumne *p_alumne) { fflush (stdin); printf ("Introdueix el nom de l'alumne: "); gets (p_alumne->nom); printf ("Introdueix el grup de l'alumne: "); scanf ("%d", &p_alumne->grup); printf ("Introdueix la nota de l'alumne: "); scanf ("%d", &p_alumne->nota_final); } void EscriureAlumne (reg_alumne *p_alumne) { printf ("Nom de l'alumne: %s\n", p_alumne->nom); printf ("Grup de l'alumne: %d\n", p_alumne->grup); printf ("Nota de l'alumne: %d\n", p_alumne->nota_final); } void main() { reg_alumne alumnes[MAX]; char nom_alumne[20]; int i; for (i=0; i<MAX; i++) LlegirAlumne (&alumnes[i]); printf ("Introdueix el nom d'un alumne: "); fflush(stdin); gets (nom_alumne); i=0; while ((i < MAX) && (strcmp(alumnes[i].nom,nom_alumne) != 0)) i++; if (i<MAX) EscriureAlumne (&alumnes[i]); }

Page 113: _curso de programacion en c c plus plus(Català)

Tema 7: Memòria dinàmica 1. Objectes dinàmics En C, l’assignació i alliberament de memòria dinàmica es fa amb les funcions malloc() i free() que estan definides en el fitxer de llibreria stdlib.h. La funció malloc() permet reservar un determinat espai de memòria lliure per a que pugui ser utilitzat pel nostre programa. Aquest espai de memòria actuarà com un objecte dinàmic, és a dir, com una nova variable creada dinàmicament en temps d’execució, que no existia abans de la crida a la funció. La funció malloc() retorna l’adreça d’aquest espai de memòria, de manera que assignant aquesta adreça a un apuntador, podem accedir al valor de l’objecte dinàmic utilitzant l’apuntador. La funció free() serveix per indicar que ja no volem utilitzar més un determinat objecte dinàmic. La memòria que s’havia reservat amb la funció malloc() retorna a l’espai de memòria lliure i, per tant, ja no podrà ser utilitzada pel nostre programa. 1.1. Creació d’objectes dinàmics: funció malloc() La funció malloc() seria equivalent a la instrucció assignar que hem vist en pseudocodi. La sintaxi general d’utilització és la següent: Sintaxi <apuntador> = (<tipus> *) malloc (<tamany de memòria>) Exemple int *p; p = (int *) malloc (sizeof (int));

De la sintaxi d’utilització de la funció malloc() en podem remarcar diverses coses: 1. La funció retorna l’adreça de l’espai de memòria que s’ha reservat (l’adreça

del nou objecte dinàmic que s’ha creat). Per poder accedir a aquest nou objecte dinàmic, aquesta adreça s’ha de guardar en un apuntador.

2. La funció necessita que se li especifiqui la quantitat de memòria que s’ha de reservar, depenent del tipus d’objecte dinàmic que es vulgui crear. Hem de tenir en compte que no es requereix la mateixa quantitat de memòria per guardar un objecte de tipus char, que per guardar un objecte de tipus int, que per guardar un objecte de tipus struct que hàgim definit nosaltres, etc. Per això, la funció malloc() té un paràmetre que és el tamany de memòria (nº de bytes) que volem reservar. Per evitar haver de saber el nº de bytes que necessita cada objecte, tenim l’operador sizeof(), que expliquem més endavant, que ens permet saber el tamany que ocupa cada tipus d’objecte.

3. Com que la funció malloc() ha de servir per crear qualsevol tipus d’objecte dinàmic, està declarada per retornar un apuntador genèric de tipus void. Per tant, per assignar l’adreça que retorna a l’apuntador, hem de convertir-lo al

1. Assignació adreça a l’apuntador

2. Quantitat de memoria a reservar depenent del tipus d’objecte

3. Conversió al tipus de l’objecte

Page 114: _curso de programacion en c c plus plus(Català)

? p:

5 p:

tipus de l’objecte dinàmic que estem creant (en l’exemple (int *)). D’aquesta manera, evitem errors de compilació.

El següent diagrama il·lustra el funcionament de la funció malloc() i el seu efecte sobre la memòria de l’ordinador. Fixem-nos que és equivalent a l’efecte que té la instrucció assignar que hem explicat en pseudocodi: inicialment tenim un apuntador p sense inicialitzar. Després de cridar a la funció malloc(), aquest apuntador conté l’adreça d’un nou espai de memòria que s’ha reservat dins de l’espai de memòria lliure. Aquest espai de memòria es converteix en un objecte dinàmic que pot ser accedit amb l’apuntador i l’operador *, com es veu en l’exemple (*p = 5). Assignació de memòria int *p; p = (int *) malloc (sizeof (int)); *p = 5;

p:

programa

memòria lliure

variables ???

p: ?

???

p:

programa

memòria lliure

variables

5

p:

programa

memòria lliure

variables

Page 115: _curso de programacion en c c plus plus(Català)

Operador sizeof() Aquest operador serveix per saber la quantitat de memòria que ocupa un determinat tipus d’objecte. La sintaxi d’utilització és la següent: Sintaxi sizeof (<tipus de dades>) Exemple #include <stdio.h> void main() { printf ("El tamany d'un int són %d bytes\n", sizeof(int)); printf ("El tamany d'un float són %d bytes\n", sizeof(float)); printf ("El tamany d'un char són %d bytes\n", sizeof(char)); }

Aquest programa ens mostrarà per pantalla la quantitat de memòria que ocupa un objecte de tipus int, un objecte de tipus float i un objecte de tipus char. Aquests valors poden variar segons el tipus d’ordinador, el sistema operatiu i el compilador del llenguatge. Com hem vist en l’exemple de l’apartat anterior, l’operador sizeof() es pot utilitzar en les crides a la funció malloc() per especificar la quantitat de memòria que hem de reservar per crear un determinat tipus d’objecte dinàmic. L’operador sizeof() es pot utilitzar amb els tipus bàsics, però també amb tipus compostos (registres, arrays, etc.) com veurem més endavant. 1.2. Destrucció d’objectes dinàmics: funció free() Quan un objecte dinàmic ja no és necessari, s’ha de destruir amb la funció free(). La funció free() seria equivalent a la instrucció alliberar que hem definit en pseudocodi. Una crida a la funció free() provoca que s’alliberi l’espai de memòria associat amb l’apuntador, és a dir que el retorni a l’espai de memòria lliure i ja no es pugui utilitzar. Això ho podem veure il·lustrat en el següent diagrama: tenim un apuntador que conté l’adreça d’un objecte dinàmic creat prèviament amb la funció malloc(). Després de cridar a la funció free(), aquest espai de memòria torna a l’espai de memòria lliure i l’apuntador queda amb un valor indefinit i, per tant, ja no es pot utilitzar. L’efecte és que hem tornat a la situació inicial que teníem abans de cridar a la funció malloc() i que l’objecte dinàmic ha desparegut.

Page 116: _curso de programacion en c c plus plus(Català)

5 p:

Sintaxi free (<apuntador>); Exemple free (p);

2. Arrays dinàmics Una de les possibilitats que ens dóna la utilització d’objectes dinàmics és la creació d’arrays dinàmics, és a dir, arrays que es creen en temps d’execució amb una crida a la funció malloc() i que, per tant, poden tenir un tamany variable, ajustat a les necessitats reals del programa. Fins ara, quan definíem un array, s’havia d’especificar la longitud d’aquest amb una constant que ja no es podia canviar en tota l’execució del programa. El problema és que moltes vegades no és fàcil saber el tamany exacte que ha de tenir l’array quan escrivim el programa. Per tant, podem definir-lo molt més gran del que realment es necessita o, a l’inrevés, definir-lo massa petit i no tenir espai suficient per guardar totes les dades que ens interessen. Els arrays dinàmics ens permeten solucionar aquest problema perquè podem especificar el tamany de l’array en temps d’execució quan ja sabem el nombre exacte d’elements que ha de tenir. Els arrays dinàmics es basen en el fet que a la funció malloc() li podem especificar un tamany de memòria qualsevol. Per tant, podem indicar un tamany de memòria suficient per guardar tots els elements d’un array. A més a més, com que a la secció 3 ja hem parlat de l’equivalència entre arrays i apuntadors, ja sabem com utilitzar un apuntador per accedir als elements d’un array. Per tant, la funció malloc() ens reservarà memòria suficient per guardar tots els elements de l’array i ens permetrà utilitzar l’apuntador que retorna per accedir a aquest espai de memòria com si fóssin els elements d’un array. Anem a veure com es concreta tot això en un exemple que defineix un array dinàmic de 5 posicions i accedeix als elements de l’array utilitzant l’apuntador que retorna la funció malloc()

5

p:

programa

memòria lliure

variables

p:

programa

memòria lliure

variables ???

p: ?

Page 117: _curso de programacion en c c plus plus(Català)

Sintaxi <apuntador> = (<tipus> *) malloc (<nº elements > * sizeof (<tipus>)); Exemple /* Creació de l’array dinàmic de 5 elements de tipus enter */ p = (int *) malloc (5 * sizeof (int));

És a dir, per crear un array dinàmic s’utilitza també la funció malloc() especificant el tipus dels objectes que volem guardar dins de l’array. La diferència respecte a la creació d’un únic objecte dinàmic és que hem d’especificar el nombre d’elements de l’array. Això ho fem quan indiquem el tamany de memòria a reservar, multiplicant el tamany de l’objecte (sizeof (int) en l’exemple anterior) pel nombre d’elements de l’array (5 en l’exemple). D’aquesta manera, reservem memòria suficient per guardar tots els elements de l’array. Fixem-nos que un cop reservat l’espai de memòria, podem accedir a l’array amb l’apuntador p utilitzant l’equivalència entre apuntadors i arrays, utilitzant qualsevol de les notacions que hem explicat a la secció 3 (p[i] ó *(p+i)). Finalment, quan ja no necessitem seguir utilitzant l’array, aquest s’ha d’alliberar de la mateixa manera que ho fem amb qualsevol altre objecte dinàmic, amb la funció free(). Quan es crida a la funció free() s’allibera tota la memòria reservada per l’array. Els arrays dinàmics es poden utilitzar per crear un array en temps d’execució just en el moment que sabem el nº d’elements exactes que haurà de contenir l’array. D’aquesta manera ocupem només la memòria que necessitem, com podem veure en aquest exemple:

tipus apuntador i de l’array

nº d’elements de l’array

tipus de cada element de l’array

? p: ? ? ? ?/* Accés als elements de l’array utilitzant l’apuntador que retorna malloc */ for (i=0; i< 5; i++) *(p+i) = i;

0p: 1 2 3 4

/* Alliberament de l’array dinamic */ free (p);

?? p:

for (i=0; i< 5; i++) p[i] = i;

Page 118: _curso de programacion en c c plus plus(Català)

Exemple /* Assignació dinàmica de memòria per calcular la mitja d'una sèrie de nombres reals */ #include <stdio.h> #include <stdlib.h> float Mitja (float *vector, int n_elements) { int i; float suma; suma = 0; for (i=0; i<n_elements; i++) suma += *vector++; /* Equivalent a suma+= *vector; vector++ */ return suma / n_elements; } void main () { float *vector, mitja_vector; int n_elements, i; printf (“Quants nombres vols introduir?: “); scanf (“%d”, &n_elements); vector = (float * ) malloc (n_elements * sizeof (float)); for (i=0; i<n_elements; i++) { printf(“Introdueix un nombre real: “); scanf (“%f”, &vector[i]); } mitja_vector = Mitja (vector, n_elements); printf (“La mitja dels nombres és: %f”, mitja_vector); free (vector); }

En aquest exemple volem calcular la mitja d’una sèrie de nombres reals, però a priori no sabem quants nombres s’introduiran. Per evitar haver de definir un array suficientment gran per encabir qualsevol número d’elements, decidim utilitzar un array dinàmic. L’array dinàmic es defineix com un apuntador (float *vector). Llavors, el primer que fem és averiguar quants nombres s’introduiran i després es crea l’array dinàmic amb la crida a la funció malloc() (vector = (float * ) malloc (n_elements * sizeof (float))) utilitzant aquest nombre d’elements. A partir d’aquest moment l’apuntador vector es converteix en una referència a l’array dinàmic i el podem utilitzar per llegir els elements de l’array (scanf (“%f”, &vector[i])) o per passar-lo com a paràmetre a la funció que calcula la mitja de tots els elements. Fixem-nos que a la funció que calcula la mitja, a més a més de l’array (que es passa com apuntador), necessitem passar com a paràmetre també el nº d’elements de l’array. Això serà una tècnica habitual en totes les funcions que hagin de treballar amb arrays dinàmics. Com que els arrays dinàmics poden tenir qualsevol longitud, necessitarem especificar sempre un paràmetre addicional que ens indiqui el nombre d’elements perquè dins de la funció puguem recórrer l’array correctament.

Page 119: _curso de programacion en c c plus plus(Català)

2.1. Retorn d’arrays Una altra de les utilitats dels arrays dinàmics és la possibilitat d’utilitzar-los com a valors de retorn de les funcions. Els arrays estàtics, declarats com a variables dins d’una funció no poden ser utilitzats com a valor de retorn de la funció perquè la memòria que ocupen desapareix quan s’acaba la funció. Per tant, quan necessitem que una funció retorni un array la solució passa per utilitzar arrays dinàmics: dins de la funció podem crear un array dinàmic i retornar la seva adreça amb un apuntador, que podrà ser utilitzat al programa principal per accedir a l’array. Quan ja no sigui necessari seguir utilitzant l’array dinàmic, s’haurà d’alliberar la memòria des del programa principal. En el següent exemple podem veure com es fa: volem llegir dues cadenes de caràcters i concatenar-les per obtenir una nova cadena. La concatenació la volem fer amb una funció que agafi com a paràmetres les dues cadenes originals i retorni la nova cadena resultat de la concatenació. Fixem-nos que a la funció concatena, definim que el valor de retorn és un char * que, en aquest cas, correspon a un apuntador a la cadena resultat. Dins de la funció utilitzem un apuntador (desti) per crear dinàmicament una cadena que tingui longitud suficient per encabir tots els caràcters de les dues cadenes originals. En aquesta cadena hi copiem el contingut de les dues cadenes originals. Quan la funció acaba, retornem l’apuntador desti, que conté l’adreça de la cadena dinàmica resultat de la concatenació. Per tant, dins del programa principal, podem utilitzar l’apuntador retornat (cadena_desti) per accedir a la cadena que conté la concatenació. Al final del programa s’ha d’alliberar la memòria dinàmica utilitzada per la nova cadena. Fixem-nos també que sempre que volem declarar o reservar espai per una cadena, hem d’especificar una longitud igual al nombre màxim de caràcters + 1. Això és degut a que en C, totes les cadenes acaben amb el caràcter ‘\0’ (que utilitza una posició de la cadena). Exemple #include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX 100 char *concatena (char *origen1, char *origen2) { char *desti; int i; /* Creació de la nova cadena dinàmica */ /* Longitud = longitud origen + 1, per guardar el \0 al final de la cadena */ desti = (char *) malloc ((strlen(origen1) + strlen(origen2) + 1) * sizeof (char)); i = 0; while (*origen1 != '\0') { desti[i] = *origen1++; i++; } while (*origen2 != '\0')

Page 120: _curso de programacion en c c plus plus(Català)

{ desti[i] = *origen2++; i++; } desti[i] = '\0'; return desti; /* Retornem la nova cadena dinàmica */ } void main () { /* Definim les dues cadenes origen. */ /* Reservem espai MAX+1 per poder guardar \0 al final */ char cadena_origen1[MAX+1], cadena_origen2[MAX+1]; /* Definim la cadena destí. Apuntador per guardar cadena dinàmica */ char *cadena_desti; printf ("Introdueix la primera cadena (max. 100 caracters)"); gets (cadena_origen1); printf ("Introdueix la segona cadena (max. 100 caracters)"); gets (cadena_origen2); cadena_desti = concatena (cadena_origen1, cadena_origen2); printf ("La concatenació de les dues cadenes és: %s", cadena_desti); free (cadena_desti); /* Alliberament de la cadena dinàmica */ }

La creació i destrucció de la cadena dinàmica, així com el retorn de la funció al programa principal queden il·lustrats en aquesta figura: void main()

cadena_desti = desti

?? cadena_dest

cadena_desti = concatena ( )

char *concatena (….)

?? dest

desti = (char *) malloc (…)

·····

dest

return desti

cadena_dest

free (cadena_desti)

?? cadena_dest

Page 121: _curso de programacion en c c plus plus(Català)

2.2. Arrays d’apuntadors Anem a veure una última aplicació dels arrays dinàmics: els arrays d’apuntadors. Imaginem una aplicació en què volem simular un editor de text en què les línies de text poden tenir longitud variable. Lògicament, si les línies de text poden tenir longitud variable, una opció és definir-les com a cadenes dinàmiques. Però si podem tenir vàries línies i les volem guardar totes necessitarem definir un array de cadenes dinàmiques (array d’apuntadors). Això ho farem de la següent manera: Exemple char *linies_text[10]; 0 1 2 3 4 5 6 7 8 9

Amb aquesta definició linies_text és un array de 10 posicions. Cada posició d’aquest array és de tipus char *, o sigui que cada posició de l’array és un apuntador a caràcter. Cadascun d’aquests apuntadors a caràcter es pot utilitzar per crear i gestionar una cadena dinàmica, per exemple de la següent forma: Exemple for (i=0; i<10; i++) { printf ("Nº de caracters de la linia %d: ", i); scanf ("%d", &n); linies_text[i] = (char *) malloc ((n+1) * sizeof(char)); } 0 1 2 3 4 5 6 7 8 9

Amb aquest codi estem utilitzant cada apuntador de l’array per crear una cadena dinàmica, cadascuna d’una longitud diferent. D’aquesta manera tenim un conjunt de cadenes dinàmiques, cadascuna representant una línia de text. Aquesta idea la

?? ?? ?? ?? ?? ?? ?? ?? ?? ??

linia 1 linia 2 linia 3 linia 4linia 5

linia 10 linia 9 linia 8

linia 6linia 7

Page 122: _curso de programacion en c c plus plus(Català)

podem utilitzar en el següent exemple per definir una estructura que permeti llegir totes les línies de text i comptar el nombre de paraules de cada línia de text: Exemple #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX 100 int NParaules (char *cadena) { int i, n; i = 0; n = 0; while (cadena[i] != '\0') { while ((cadena[i] == ' ') && (cadena[i] != '\0')) i++; if (cadena[i] != '\0') n++; while ((cadena[i] != ' ') && (cadena[i] != '\0')) i++; } return n; } void main() { char *linies_text[10]; char aux[MAX+1]; int i, n; for (i=0; i<10; i++) { printf ("Introdueix la línia %d (max. 100 caracters): ", i); gets(aux); linies_text[i] = (char *) malloc ((strlen(aux)+1) * sizeof(char)); strcpy (linies_text[i], aux); } for (i=0; i<10; i++) { n = NParaules (linies_text[i]); printf ("Linia %d: %s\n", i, linies_text[i]); printf ("Nº de paraules: %d\n", n); } for (i=0; i<10; i++) free (linies_text[i]); }

Fixem-nos que per cada línia, primer es llegeix en una cadena estàtica auxiliar (aux) i un cop s’ha llegit, es reserva l’espai necessari per guardar la línia utilitzant una cadena dinàmica i s’associa a una posició de l’array d’apuntadors (linies_text[i] = (char *) malloc ((strlen(aux)+1) * sizeof(char))). Després, per comptar i imprimir el nombre de paraules de cada línia, cada posició de l’array es tracta igual que hem tractat fins ara les cadenes dinàmiques, per passar-les com a paràmetres a la funció NParaules (n = NParaules

Page 123: _curso de programacion en c c plus plus(Català)

(linies_text[i])). Dins de la funció el paràmetre es defineix com a char * i es tracta com a cadena dinàmica. Finalment, quan s’acaba el programa, hem d’alliberar l’espai que ocupen totes les cadenes dinàmiques associades a cada posició de l’array (free (linies_text[i])). 3. Registres dinàmics La memòria dinàmica també pot utilitzar-se per crear objectes dinàmics de tipus registre. El funcionament serà el mateix que per objectes de tipus bàsic, com podem veure amb aquest exemple: Exemple typedef struct registre_alumne { char nom[20]; int grup; int nota_final; } TipusAlumne; TipusAlumne *alumne; alumne = (TipusAlumne *) malloc (sizeof (TipusAlumne)); strcpy (alumne->nom,"Joan"); alumne->grup = 1; alumne->nota_final = 7; free (alumne);

Amb la funció malloc() es crea un objecte dinàmic de tipus TipusAlumne, que correspon a un registre. L’adreça d’aquest objecte dinàmic s’assigna a l’apuntador alumne, que pot ser utilitzat per accedir al nou registre dinàmic amb la notació que hem explicat al capítol anterior (alumne->nom, alumne->grup, alumne->nota_final). Finalment, el registre dinàmic es destrueix utilitzant la funció free() de la forma habitual que ja hem vist. 3.1. Retorn de registres Igual que en el cas dels arrays, una de les utilitats dels registres dinàmics és el retorn de registres en les funcions. De la mateixa manera que amb els arrays, quan

??

alumne

nom Joan grup 1

nota_final 7

alumne

nom grup

nota_final

alumne

Page 124: _curso de programacion en c c plus plus(Català)

volem retornar un registre des d’una funció, el millor és retornar un registre dinàmic. Per tant, haurem de declarar que la funció retorna un apuntador a registre i, dins de la funció, haurem d’assignar memòria dinàmica pel registre i retornar l’adreça d’aquest objecte dinàmic com a resultat de la funció. Dins del programa principal, quan ja no necessitem més el registre, haurem d’alliberar la memòria dinàmica reservada pel registre. Tot això ho podem veure en el següent exemple on definim una funció per llegir i retornar les dades d’un registre de tipus alumne: Exemple #include <stdlib.h> #include <string.h> #include <stdio.h> #define MAX 20 typedef struct registre_alumne { char *nom; int grup; int nota_final; } TipusAlumne; /* Retorn d'un apuntador (objecte dinàmic) al registre */ TipusAlumne *DemanarAlumne () { TipusAlumne *alumne; char cadena[MAX+1]; /* Creació de l'objecte dinàmic */ alumne = (TipusAlumne *) malloc (sizeof (TipusAlumne)); printf ("Introdueix el nom de l'alumne (max. 20 caracters): "); gets (cadena); /* Creació de la cadena dinàmica dins del registre */ alumne->nom = (char *) malloc ((strlen (cadena) + 1) * sizeof (char)); strcpy (alumne->nom, cadena); printf ("Introdueix el grup de l'alumne: "); scanf ("%d", &alumne->grup); printf ("Introdueix la nota de l'alumne: "); scanf ("%d", &alumne->nota_final); return alumne; /* Retorn del registre dinàmic */ } void main() { TipusAlumne *alumne; alumne = DemanarAlumne(); printf("Nom de l'alumne: %s\n", alumne->nom); printf("Grup de l'alumne: %d\n", alumne->grup); printf("Nota de l'alumne: %d\n", alumne->nota_final); free (alumne->nom); /* Alliberament de la cadena dinàmica dins del registre */ free (alumne); /* alliberament del registre dinàmic */ }

Page 125: _curso de programacion en c c plus plus(Català)

Fixem-nos en un detall particular d’aquest exemple. La cadena nom l’hem declarada, no com una cadena estàtica dins del registre, sinó com una cadena dinàmica. Per tant, quan creem el registre dinàmic, només es reserva memòria per guardar l’apuntador que servirà per accedir a la cadena. Per crear aquesta cadena dinàmica hem de tornar a cridar a la funció malloc() per assignar-li l’espai adequat. Igualment quan alliberem la memòria primer hem d’alliberar la cadena dinàmica i després el registre dinàmic. El següent gràfic il·lustra aquesta seqüència de passos: alumne = (TipusAlumne *) malloc (sizeof (TipusAlumne)); alumne->nom = (char *) malloc ((strlen (cadena) + 1) * sizeof (char)); free (alumne->nom); free (alumne); 3.2. Arrays dinàmics de registres L’altra utilitat dels registres dinàmics, igual que amb objectes de tipus bàsics és la creació d’arrays dinàmics de registres. La forma de fer-ho és la mateixa que amb els arrays dinàmics de tipus bàsics: a la crida a la funció malloc() especifiquem el nombre d’elements que volem que tingui l’array, com veiem en aquest exemple: Exemple typedef struct registre_alumne { char nom[20]; int grup; int nota_final; } TipusAlumne; TipusAlumne *alumnes; alumnes = (TipusAlumne *) malloc (5 * sizeof (TipusAlumne));

??

alumne

nom grup

nota_final

??

alumne

nom grup

nota_final

alumne

nom grup

nota_final

alumne

Page 126: _curso de programacion en c c plus plus(Català)

strcpy (alumnes[0].nom,"Joan"); alumnes[0].grup = 1; alumnes[0].nota_final = 7; free (alumnes); En aquest cas, alumnes és un apuntador a registre que utilitzem per crear un array dinàmic de 5 posicions. Per tant, un cop creat amb la funció malloc() podem accedir als elements de l’array de la mateixa forma que accedim als elements d’un array de registres creat de forma estàtica (alumnes[0].nom, alumnes[0].grup, alumnes[0].nota_final). El següent exemple reprodueix l’exemple de la secció 4.5 del tema 4 del manual en què es definia un array de registres per guardar les notes de tots els alumnes. La diferència està en que aquí no es defineix un nombre fix d’alumnes sinó que és variable i, per tant, es deixa que al principi l’usuari digui quants alumnes voldrà introduir. Llavors, es crea un array dinàmic (amb una crida a la funció malloc()) que permet guardar exactament el número d’alumnes que ha especificat l’usuari. A partir d’aquest moment, l’accés a l’array de registres dinàmic es fa exactament igual que es feia en l’exemple original utilitzant l’array estàtic. Al final del programa s’allibera l’espai ocupat per l’array dinàmic. Com que en aquest cas, el nom s’ha definit també com una cadena dinàmica, per cada registre cal reservar explícitament memòria dinàmica per crear-la i al final del programa, també s’ha d’alliberar explícitament. Exemple #include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX 20 void main() { /* Definició de l'estructura de registre */ typedef struct registre_alumne { char *nom;

alumnes

0 1 2 3 4 nom Joan grup 1

nota_final 7

alumnes

0 1 2 3 4 nom grup

nota_final

?? alumnes

Page 127: _curso de programacion en c c plus plus(Català)

int grup; int nota_final; } TipusAlumne; /* Declaració de l'array dinàmic d'alumnes */ TipusAlumne *alumnes; int i, n_aprovats, n_alumnes; char cadena[MAX+1]; printf ("Quants alumnes vols introduir?: "); scanf ("%d", &n_alumnes); /* Creació de l'array dinàmic d'alumnes */ alumnes = (TipusAlumne *) malloc (n_alumnes * sizeof (TipusAlumne)); /* Lectura de les dades dels alumnes */ for (i=0; i<n_alumnes; i++) { printf ("Introdueix el nom de l'alumne (max. 20 caracters): "); fflush (stdin); gets (cadena); /* Creació cadena dinàmica del registre alumne */ alumnes[i].nom = (char *) malloc ((strlen(cadena)+1) * sizeof(char)); strcpy (alumnes[i].nom, cadena); printf ("Introdueix nº de grup de l'alumne: "); scanf ("%d", &alumnes[i].grup); printf ("Introdueix la nota de l'alumne: "); scanf ("%d", &alumnes[i].nota_final); } /* Comptar quants alumnes han aprovat */ n_aprovats = 0; for (i=0; i<n_alumnes; i++) { if (alumnes[i].nota_final >= 5) n_aprovats++; } printf ("Nº aprovats: %d", n_aprovats); /* Alliberament cadenes dinàmiques dels registres */ for (i=0; i<n_alumnes; i++) free (alumnes[i].nom); /* Alliberament de l'array dinàmic d'alumnes */ free (alumnes); }

4. Estructures dinàmiques: llistes Finalment, introduirem com podem definir en C el tipus llista simplement enllaçada que hem vist en pseudocodi, utilitzant com exemple una llista per guardar dades d’alumnes a partir de la definició de registre que hem estat fent servir fins ara. La definició dels tipus necessaris per gestionar la llista seria la següent:

Page 128: _curso de programacion en c c plus plus(Català)

Definició de llista typedef struct registre_alumne { char nom[20]; int grup; int nota_final; struct registre_alumne *seguent; } NodeAlumne; NodeAlumne *primer;

A partir d’aquestes definicions es podrien implementar en C totes les funcions de manipulació de llistes que hem vist en pseudocodi. Com a exemple, només posarem aquí les funcions que permeten inserir i eliminar un element al principi de la llista. La implementació d’aquestes funcions és la traducció a C del pseudocodi que teniu als apunts de teoria. Funcions de la llista NodeAlumne *AfegirPrimer (NodeAlumne *primer, char *nom, int grup, int nota_final, int *correcte) { NodeAlumne *NouElement; NouElement = (NodeAlumne *) malloc (sizeof (NodeAlumne)); if (NouElement == NULL) *correcte = 0; /* correcte = 0 si no es pot crear el node */ else { strcpy (NouElement->nom, nom); NouElement->grup = grup; NouElement->nota_final = nota_final; NouElement->seguent = primer; primer = NouElement; *correcte = 1; /* correcte = 1 si es pot crear el node */ } return primer; /* Retornem la modificació de l'apuntador primer */ } NodeAlumne *EliminarPrimer (NodeAlumne *primer, int *correcte) { NodeAlumne *aux; if (primer == NULL) *correcte = 0; /* correcte = 0 si no hi ha cap element a la llista */ else { aux = primer; primer = primer->seguent; /* Actualitza el primer element */ free (aux); /* Allibera l’element eliminat */ *correcte = 1;

Page 129: _curso de programacion en c c plus plus(Català)

/* correcte = 1 si s’ha pogut eliminar el primer element */ } return primer; /* Retorna la modificació de l'apuntador primer */ }

Page 130: _curso de programacion en c c plus plus(Català)

Tema 8: Fitxers 1. Conceptes bàsics Ja sabem que les variables es guarden a la memòria de l’ordinador, i que per tant, la informació que contenen es perd quan apaguem l’ordinador o, fins i tot, quan sortim de l’aplicació que estem executant. Per aconseguir que la informació es guardi de forma permanent hem d’utilitzar dispositius de memòria externa o secundària, com per exemple els discs durs. En aquests dispositius, la informació s’organitza en forma de fitxers, i cadascun d’ells està identificat per un nom. En C, per poder accedir a la informació que hi ha en un fitxer, necessitem utilitzar un tipus especial de variable i un conjunt de funcions estàndard que estan definides al fitxer de llibreria stdio.h. Aquestes funcions permetran associar una variable del programa a un determinat fitxer, de manera que les operacions de lectura i escriptura que es facin amb aquesta variable hi quedin automàticament reflectides. Des del programa en C, un fitxer es veurà com una seqüència de caràcters (fitxers de text que podeu crear i/o modificar amb qualsevol editor de text) o com una seqüència de bytes (fitxers binaris). La majoria dels fitxers que utilitzarem seran els de text i la manera en què hi accedirem, serà sempre seqüencial (per accedir a la posició X del fitxer, s’han de llegir les X-1 posicions anteriors). Malgrat que en aquest curs no ho tractarem, val la pena mencionar que en C es poden utilitzar fitxers d’accés directe que permeten accedir directament a una posició determinada dins del fitxer. Per tant, per llegir i escriure informació en un fitxer ho farem sempre element a element, És a dir, de forma consecutiva. Sempre haurem de començar per la primera posició del fitxer i anar llegint element a element fins arribar al que estem buscant. 2. El tipus fitxer (FILE) Dins la llibreria stdio.h hi ha definit un tipus de dada (FILE *) que serveix per guardar tota la informació necessària per accedir a qualsevol fitxer del disc. Per tant, quan vulguem accedir a un fitxer, haurem de definir primer una variable d’aquest tipus de la següent manera: Sintaxi FILE *<nom_variable>; Exemple FILE *f;

La variable f pot ser utilitzada per accedir a qualsevol fitxer del disc. La utilització d’un fitxer normalment es fa en tres passos: 1. Obertura del fitxer 2. Operació amb el fitxer: lectura o escriptura. La lectura i escriptura en un fitxer

es podrà fer a diferents nivells: • Caràcter a caràcter (funcions fgetc i fputc)

Page 131: _curso de programacion en c c plus plus(Català)

• De línia en línia de text (funcions fgets i fputs)

• Utilitzant format per diferents tipus de dades (fscanf i fprinf)

• En blocs de bytes de memòria en fitxers binaris (fread i fwrite).

3. Tancament del fitxer

A continuació descriurem les funcions que hi ha definides dins stdio.h per fer cadascun d’aquests passos. 2.1. Obertura de fitxers: fopen() El primer que s’ha de fer sempre que volem treballar amb un fitxer del disc és obrir-lo. Obrir un fitxer vol dir associar la variable del programa en C (del tipus FILE*) a un fitxer concret de disc, de forma que totes les operacions que es facin amb la variable quedin reflectides en el fitxer de disc. La funció per obrir un fitxer és fopen() i necessita que se li especifiqui com a paràmetres la variable de tipus FILE*, el nom del fitxer de disc i el mode d’obertura (lectura, escriptura, ...). Si el fitxer està situat en un directori diferent al del projecte que s’està executant s’ha d’afegir, a més, el path o camí fins al fitxer. La forma d’utilitzar la funció fopen és la següent: Sintaxi <variable_fitxer>=fopen(<nom_fitxer>, <mode_obertura>); Exemple FILE *fitxer1, *fitxer2; Fitxer1=fopen(“fitxer_exemple.txt”, “r”); Fitxer2=fopen(“c:\Dades\altre_fitxer_exemple.txt”, “w”);

En aquest exemple estem obrint un parell de fitxers que es diuen fitxer_exemple.txt i altre_fitxer_exemple.txt en mode de lectura i escriptura respectivament (identificat pel segon paràmetre “r” i “w” respectivament). A més els estem vinculant a les variables fitxer1 i fitxer2. Aquestes són les variables que farem servir dins del programa en C per llegir i modificar els fitxers. En el moment d’obrir un fitxer hem d’especificar quin ús volem fer del fitxer, indicant un dels possibles modes d’obertura dels fitxers, que tenim resumits a la taula següent:

Mode Significat "r" Lectura. El fitxer ha d'existir prèviament. Si no existeix retorna un error.

Comença a llegir per la primera posició del fitxer.

"w" Escriptura. Les dades anteriors es perden i es comença a escriure per la primera posició del fitxer. Si el fitxer no existeix es crea. Si no es pot crear retorna un error.

"a" Afegir dades pel final del fitxer. Les dades anteriors es conserven i es comença a escriure pel final del fitxer. Si el fitxer no existeix es crea.

"r+" Actualització. Lectura i escriptura. El fitxer ha d'existir. Les dades anteriors

Page 132: _curso de programacion en c c plus plus(Català)

es conserven Es comença a llegir/escriure pel principi del fitxer.

"w+" Actualització. Lectura i escriptura. Les dades anteriors es perden. Es comença a llegir/escriure pel principi del fitxer. Si el fitxer no existeix es crea.

"a+" Actualització. Lectura i escriptura pel final del fitxer. Les dades anteriors es conserven. Es comença a llegir/escriure pel final del fitxer. Si el fitxer no existeix es crea.

Fixeu-vos que la variable f és de tipus punter. Així, per qualsevol d’aquests modes d’obertura, si es produeix qualsevol error (el fitxer no existeix o no es pot crear o no s’hi pot accedir), la funció fopen() retorna un apuntador NULL. 2.2. Tancament de fitxers: fclose() Els fitxers s’han de tancar quan ja no s’han d’utilitzar més. Tancar un fitxer vol dir desfer la vinculació entre la variable del programa i el fitxer de disc, i es fa cridant a la funció fclose(), passant-li com a paràmetre la variable associada amb el fitxer, d’aquesta manera: Sintaxi fclose (<variable_fitxer>); Exemple FILE *fitxer; fitxer = fopen (“fitxer_exemple.txt”, “r”); ........ fclose (fitxer); /* Ja no el necessitem més */

A partir d’aquest moment, ja no podem utilitzar la variable fitxer per accedir al fitxer de disc. Si el volem tornar a utilitzar l’haurem de tornar a obrir. 2.3. Lectura i escriptura de caràcters: fgetc() i fputc() Aquestes dues funcions permeten fer la lectura i escriptura en fitxers de text caràcter a caràcter. Cada cop que cridem a la funció fgetc(), llegirem el següent caràcter del fitxer, i cada cop que cridem a la funció fputc() afegirem un caràcter al fitxer. La utilització d’aquestes funcions es fa com es mostra a continuació: Lectura de caràcters <var_char> = fgetc (<var_fitxer>); Escriptura de caràcters int fputc (<var_char>, <var_fitxer>);

La funció fgetc() retorna el següent caràcter del fitxer en una variable de tipus char. Si s'arriba al final del fitxer o es produeix qualsevol error retorna EOF. La funció fputc() escriu el caràcter a la següent posició del fitxer. Si es produeix qualsevol error retorna EOF.

Page 133: _curso de programacion en c c plus plus(Català)

El següent exemple mostra la utilització d’aquestes dues funcions en un programa que permet fer una còpia d’un fitxer caràcter a caràcter. Primer, el programa obre el fitxer original (fitxer1) en mode lectura. Després, si el fitxer s’ha pogut obrir correctament, obre el fitxer que tindrà la còpia (fitxer2) en mode escriptura, de forma que es crea de nou (si ja existia el seu contingut es perd). Si també el podem obrir correctament, anem llegint els caràcters del fitxer original (fitxer1) de forma seqëncial amb la funció fgetc i els va escrivint al fitxer còpia (fitxer2) també de manera seqëncial amb la funció fputc(). Quan es llegeix el caràcter EOF (final del fitxer original), el programa para i tanca els dos fitxers. També es para la còpia si es detecta qualsevol error en l’escriptura dels caràcters al fitxer còpia amb la funció fputc(). Exemple /* Còpia de fitxers caràcter a caràcter */ #include <stdio.h> void main () { int error = 0; FILE *fitxer1, *fitxer2; char c; /* Obertura del 1er fitxer de lectura */ fitxer1 = fopen ("fitxer1.txt", "r"); if (fitxer1 == NULL) printf ("Error obrint el primer fitxer\n"); else { /* Obertura 2n fitxer d’escriptura */ fitxer2 = fopen ("fitxer2.txt", "w"); if (fitxer2 == NULL) printf ("Error obrint el segon fitxer\n"); else { /* Lectura del primer fitxer */ c = fgetc (fitxer1); while (c != EOF) { /* Escriptura al 2n fitxer */ error = fputc (c, fitxer2); if (error == EOF) { printf ("Error d’escriptura\n"); /* Parem lectura si error escrivint */ c = EOF; } /* Lectura del 1er fitxer */ else c = fgetc (fitxer1); } fclose (fitxer2); /* Tancament del segon fitxer */ } fclose (fitxer1); /* Tancament del primer fitxer */ } }

Page 134: _curso de programacion en c c plus plus(Català)

2.4. Lectura i escriptura de línies de text: fgets() i fputs() En els fitxers de text, la informació està normalment estructurada en línies de text. Aquestes dues funcions fgets() i fputs() permeten anar llegint i escrivint línies senceres de text en un fitxer. Serien equivalents a les funcions gets() i puts() que ja hem utilitzat per llegir i escriure cadenes de text per pantalla. Podeu pensar doncs, que els fitxers són com “una pantalla que no es veu”. La utilització de la funció fgets es fa de la següent forma: Lectura de línies de text char *fgets (<cadena_text>, <Número_caràcters>, <var_fitxer>);

La funció fgets llegeix una sèrie de caràcters a partir de la posició actual del fitxer i els va guardant a la cadena que se li passa com a paràmetre. Para de llegir caràcters quan es compleix una d’aquestes dues condicions: 1. Es llegeix un caràcter de salt de línia (‘\n’).

2. S’han llegit el nº de caràcters especificat com a paràmetre a la crida de la

funció menys 1.

Al final de la sèrie de caràcters llegits s’afegeix el caràcter ‘\0’ per indicar el final de la cadena. La figura següent mostra el comportament de la funció fgets en les dues possibilitats de finalització de lectura:

Page 135: _curso de programacion en c c plus plus(Català)

En qualsevol cas, la funció fgets retorna NULL si es produeix algun error de lectura o s’arriba al final de fitxer. Per la funció fputs, la forma d’utilitzar-la és la següent: Escriptura de línies de text int fputs (<cadena_text>, <var_fitxer>);

La funció fputs senzillament escriu tots els caràcters de la cadena que es passa com a paràmetre al fitxer a partir de la posició actual. Si es produeix algun error escrivint els caràcters al fitxer retorna EOF. El següent exemple mostra la utilització de les funcions fgets i fputs per fer la còpia d’un fitxer de text, igual que en l’exemple anterior. En aquest cas, parem de llegir quan la funció fgets retorna NULL o quan fputs retona EOF. Per anar llegint les línies del fitxer utilitzem una cadena de fins a 100 caràcters (suposem que les línies del fitxer tenen una longitud màxima de 100 caràcters). Exemple /* CòpiaFitxers.c */ #include <stdio.h> #define MAX_LINIA 100 void main () { int error = 0; FILE *fitxer1, *fitxer2; char cadena[MAX_LINIA]; /* Obertura del 1er fitxer de lectura */ fitxer1 = fopen ("fitxer1.txt", "r"); if (fitxer1 == NULL) printf ("Error obrint el primer fitxer\n"); else { /* Obertura 2n fitxer d'escriptura */ fitxer2 = fopen ("fitxer2.txt", "w"); if (fitxer2 == NULL) printf ("Error obrint el segon fitxer\n"); else { /* Lectura del primer fitxer */ while ((fgets (cadena,MAX_LINIA,fitxer1) != NULL) && (error != EOF)) { /* Escriptura 2n fitxer */ error = fputs (cadena, fitxer2); } if (error == EOF) printf ("Error escriptura\n"); fclose (fitxer2); /* Tancament del segon fitxer */ } fclose (fitxer1); /* Tancament del primer fitxer */ } }

Page 136: _curso de programacion en c c plus plus(Català)

2.5. Lectura i escriptura amb format: fscanf i fprintf Amb les funcions anteriors (fgetc, fputc, fgets i fputs) només som capaços de llegir i escriure caràcters en un fitxer de text. Per poder llegir i escriure altre tipus d’informació (per exemple, nombres enters i reals) tenim les funcions fscanf i fprintf, que tenen un funcionament similar a les funcions scanf i printf. És a dir, són funcions que ens permeten llegir i escriure qualsevol tipus d’informació en un fitxer de text i, a més, establir-ne el format. La utilització de la funció fscanf es fa de la següent forma: Lectura amb format int fscanf (<var_fitxer>, <especificadors_format>, <variables>);

Fixem-nos que el funcionament de la funció fscanf és exactament igual que el de la funció scanf, però afegint com a primer paràmetre la variable del fitxer d’on s’han de llegir les dades. La funció fscanf llegeix del fitxer els valors indicats pels especificadors de format i els guarda a les variables que es passen com a paràmetres. Si es produeix algun error de lectura o s’arriba al final del fitxer fscanf retorna EOF. Per la funció fprintf, la utilització és la següent: Escriptura amb format int fprintf (<var_fitxer>, <especificadors_format>, <variables>);

Page 137: _curso de programacion en c c plus plus(Català)

Aquí també el funcionament de la funció fprintf és exactament igual al de la funció printf, però afegint com a primer paràmetre la variable del fitxer on s’han d’escriure les dades. La funció fprintf escriu al fitxer els valors guardats a les variables passades com a paràmetres utilitzant els especificadors de format indicats. Si es produeix algun error d’escriptura al fitxer es retorna un nº negatiu. El següent exemple mostra el funcionament de les funcions fscanf i fprintf per fer la còpia, amb un nou format, d’un fitxer que suposem que conté dades d’un conjunt d’alumnes i per cada alumne es guarda el nom, el grup al que està matriculat i la nota que ha tret. Per tant, definim variables per guardar el nom, el grup i la nota i les utilitzem per llegir (fscanf) i per escriure (fprintf) del fitxer. Fixem-nos que per escriure afegim el caràcter de salt de línia (‘\n’) al final perquè les dades dels alumnes quedin en línies de text separades. Exemple /* CòpiaFitxers.c */ #include <stdio.h> #define MAX_NOM 20 void main () { int ErrLec = 0, ErrEsc= 0; FILE *F1, *F2; char nom[MAX_NOM]; int grup; float nota; /* Obertura del 1er fitxer de lectura */ F1 = fopen ("fitxer1.txt", "r"); if (ErrEsc == NULL) printf ("Error obrint el primer fitxer\n"); else { /* Obertura 2n fitxer d'escriptura */ F2 = fopen ("fitxer2.txt", "w"); if (F2 == NULL) printf ("Error obrint el segon fitxer\n"); else {

Page 138: _curso de programacion en c c plus plus(Català)

/* Lectura del primer fitxer */ ErrLec=fscanf(ErrEsc,"%s %d %f",nom,&grup,&nota); while ((ErrLec != EOF) && (ErrEsc>= 0)) { /* Escriptura al segon fitxer */ errorEscriptura=...

fprintf(F2,"Nom:%s Grup:%d Nota:%f\n",... nom, grup, nota); if (ErrEsc< 0) printf ("Error escriptura\n"); else ErrLec = ...

fscanf(ErrEsc,"%s %d %f", nom, &grup, &nota); } fclose (F2); /* Tancament del segon fitxer */ } fclose (ErrEsc); /* Tancament del primer fitxer */ } } NOTA: El símbol “...” NO és propi de C. Simplement indica que la línia de codi continua.

2.6. Lectura i escriptura en fitxers binaris: fread i fwrite Totes les funcions que hem vist fins ara treballen amb fitxers de text, és a dir, fitxers que podem llegir i editar amb qualsevol editor de text. Les funcions fread i fwrite treballen amb el que anomenem fitxers binaris, és a dir, fitxers que no es poden llegir amb un editor de text perquè contenen caràcters no imprimibles. Les funcions fread i fwrite permeten llegir i escriure informació de qualsevol tipus (enter, real, caràcter), però, a diferència de les funcions fscanf i fprintf, que formategen la informació de manera que quedi en format de text llegible, simplement guarden al fitxer una còpia del contingut de la memòria ocupada per les variables utilitzades per escriure. El primer aspecte que s’ha de tenir en compte per utilitzar fitxers binaris és la forma d’obrir el fitxer. En el mode d’obertura del fitxer s’ha d’indicar que el fitxer és binari. Això es fa afegint una ‘b’ al mode d’obertura, de manera que els sis modes d’obertura que havíem vist s’han d’especificar així per fitxers binaris: “rb”, “wb”, “ab”, “r+b”, “w+b”, “a+b”. Un cop s’ha obert el fitxer d’aquesta forma ja podem utilitzar les funcions fread i fwrite. Aquestes dues funcions ens serviran per llegir i escriure al fitxer directament el contingut de les variables del programa, ja siguin variables de tipus bàsics, registres o arrays. La forma d’utilitzar la funció fread és la següent: Lectura binària int fread (&<variable>, <tamany_objecte>, 1, <var_fitxer>); int fread (<array>,<tamany_objecte>,<numero_objectes>, <var_fitxer>); Exemple FILE *fitxer; int x, a[5];

Page 139: _curso de programacion en c c plus plus(Català)

//aqui obririem el fitxer per lectura fread (&x, sizeof(int), 1, fitxer); /* Llegeix un enter */ fread (a, sizeof(int), 5, fitxer); /* Llegeix cinc enters */

Com veiem, la funció fread serveix tant per llegir una variable (en aquest cas de tipus enter) com un array. En el cas de voler llegir una sola variable, li hem de passar com a paràmetre l’adreça de la variable i el tamany de l’objecte que correspon al tipus de la variable. En aquest cas, el nombre d’objectes que es llegeixen és 1. El tamany de l’objecte el podem saber, igual que fem amb les crides a la funció malloc, utilitzant l’operador sizeof. En el cas de voler llegir un array, li hem de passar l’adreça de l’array, el tamany dels objectes que es guarden a l’array i el nombre d’elements de l’array. En qualsevol dels dos casos retorna el nombre d’objectes que s’han pogut llegir realment. Per escriure, la funció fwrite funciona de la següent manera: Escriptura binària int fwrite (&<variable>, <tamany_objecte>, 1, <var_fitxer>); int fwrite (<array>,<tamany_objecte>,<numero_objectes>,<var_fitxer>); Exemple FILE *fitxer; int x, a[5]; //aqui obririem el fitxer per escriptura fwrite (&x, sizeof(int), 1, fitxer); /* Escriu un enter */ fwrite (a, sizeof(int), 5, fitxer); /* Escriu cinc enters */

Igual que la funció fread, fwrite serveix tant per escriure una variable com un array. En el cas de voler escriure una sola variable, li hem de passar com a paràmetre l’adreça de la variable i el tamany de l’objecte que correspon al tipus de la variable. En el cas de voler escriure un array, li hem de passar l’adreça de l’array, el tamany dels objectes que es guarden a l’array i el nº d’elements de l’array. En qualsevol dels dos casos retorna el nº d’objectes que s’han pogut escriure realment. En el cas dels fitxers binaris, tenim una altra funció, la funció feof(), que serveix per saber si hem arribat al final del fitxer o no. Aquesta funció retorna un valor diferent de zero si s’ha arribat al final del fitxer. La utilització de fitxers binaris la podem veure en el següent exemple. En aquest programa utilitzem fitxers binaris i les funcions fread i fwrite per fer la còpia d’un fitxer binari que conté les dades d’alumnes (nom, grup i nota) definides com a registres. Per tant, utilitzem una variable de tipus registre per llegir i escriure les dades als fitxers. Fixem-nos que en el mode d’obertura dels fitxers especifquem que han de ser fitxers binaris (“rb” i “wb”). Per llegir i escriure passem com a paràmetre a les funcions fread i fwrite l’adreça de la variable alumne. Com a tamany de l’objecte li passem el tamany de memòria que ocupa el registre que obtenim amb l’operador sizeof. I com a nombre d’objectes especifiquem 1 perquè en aquest cas llegim i escrivim els registres un a un.

Page 140: _curso de programacion en c c plus plus(Català)

Exemple /* Fitxers Binaris */ #include <stdio.h> #define MAX_NOM 20 typedef struct { char nom[MAX_NOM]; int grup; float nota; } dades_alumne; void main () { int error = 0; int n_elements; FILE *fitxer1, *fitxer2; dades_alumne alumne; /* Obertura del 1er fitxer binari */ fitxer1 = fopen ("fitxer1.dat", "rb"); if (fitxer1 == NULL) printf ("Error obrint el primer fitxer\n"); else { /* Obertura del 2n fitxer binari */ fitxer2 = fopen ("fitxer2.dat", "wb"); if (fitxer2 == NULL) printf ("Error obrint el segon fitxer\n"); else {

/* Parar si final de fitxer o error de lectura / escriptura */

while ((feof (fitxer1) == 0) && (error == 0)) { /* Lectura d’un registre del primer fitxer */ n_elements = fread...

(&alumne, sizeof(dades_alumne), 1, fitxer1); if (n_elements < 1) /* Error si no s’ha llegit un

registre */ error = 1; else {

/* Escriptura d’un registre al segon fitxer */

n_elements=fwrite(&alumne,... sizeof(dades_alumne),1,fitxer2);

if (n_elements < 1) /* Error si no es pot esciure */ error = 1; } } if (error == 1)

printf("Error llegint / escrivint... al fitxer\n");

fclose (fitxer2); /* Tancament del segon fitxer */

Page 141: _curso de programacion en c c plus plus(Català)

} fclose (fitxer1); /* Tancament del primer fitxer */ } } NOTA: El símbol “...” NO és propi de C. Simplement indica que la línia de codi continua.