Você está na página 1de 241

PROGRAMAO ESTRUTURAS DE DADOS

ALGORITMOS
EM

Professor Doutor Antnio Manuel Adrego da Rocha Professor Doutor Antnio Rui Oliveira e Silva Borges
Departamento de Electrnica e Telecomunicaes Universidade de Aveiro

Prefcio
Este texto serve de suporte disciplina de Programao II, cujo objectivo o de fornecer uma familiarizao com o ambiente de programao fornecido pelo Unix, na sua variante mais popularizada Linux e o domnio da linguagem C, na sua norma ANSI, para o desenvolvimento de programas de mdia e elevada complexidade. Comeamos por apresentar os aspectos essenciais da linguagem C em dois captulos. Depois introduzirmos as construes mais complexas da linguagem de forma gradual, medida que so necessrias construo de estruturas de dados mais complexas, bem como para a optimizao e generalizao de algoritmos. Os aspectos fundamentais apresentados no texto so os seguintes: x A familiarizao progressiva com a linguagem de programao C e com as suas bibliotecas. x A apresentao de algoritmos recursivos e sua comparao com os algoritmos iterativos equivalentes. x A introduo da metodologia de decomposio modular das solues, ou seja, o paradigma da programao modular. x O estudo da organizao da Memria de Acesso Aleatrio (RAM), nas suas implementaes esttica e semiesttica, e, de um conjunto significativo de algoritmos de pesquisa e de ordenao. x O estudo da organizao de memrias mais complexas que a Memria de Acesso Aleatrio, como por exemplo, a Memria Fila de Espera (FIFO), a Memoria Pilha (Stack) e a Memria Associativa (CAM), nas suas implementaes esttica, semiesttica e dinmica, e, dos algoritmos associados para pesquisa, introduo e retirada de informao. Assume-se que os alunos frequentaram a disciplina de Programao I, e portanto, j esto familiarizados com a metodologia de decomposio hierrquica das solues, estabelecendo dependncias de informao e no encapsulamento da informao com a criao de novas instrues no mbito da linguagem Pascal, ou seja, com o paradigma da programao procedimental. Bem como, com a criao de estruturas de dados estticas com alguma complexidade que modelam correctamente a resoluo dos problemas. Pelo que, a apresentao da linguagem C feita por comparao com a linguagem Pascal. Pretende-se ainda, que os alunos se familiarizem com a terminologia informtica apresentada nos textos de referncia da rea das Cincias da Computao, pelo que, se tenha optado pela apresentao sistemtica, em itlico e entre parntesis, dos nomes dos algoritmos e das estruturas de dados em ingls.

Captulo 1
INTRODUO AO C

Sumrio
Este captulo dedicado introduo das primeiras noes sobre a gramtica da linguagem C. Comeamos por apresentar a estrutura de um programa e os seus elementos bsicos. Explicamos os tipos de dados bsicos existentes, a definio de constantes e de variveis. Apresentamos os vrios tipos de expresses e operadores existentes e a instruo de atribuio, que a instruo bsica de uma linguagem imperativa. Apresentamos de seguida as estruturas de controlo, que permitem alterar o fluxo da sequncia das instrues. Apresentamos ainda as instrues de leitura de dados do teclado scanf e de escrita de dados no monitor printf. Finalmente, apresentamos as bibliotecas que contm as funes mais usuais e que estendem a operacionalidade da linguagem.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

1.1 Introduo
Em 1972, Dennis M. Ritchie desenvolveu a linguagem C, nos Laboratrios Bell da companhia AT & T, que a principal empresa de telecomunicaes dos Estados Unidos da Amrica, como uma linguagem de programao concebida para a escrita de sistemas operativos, aquilo que se designa por Programao de Sistemas. Como a linguagem C era to flexvel e permitia que os compiladores produzissem cdigo em linguagem mquina muito eficiente, em 1973, Dennis M. Ritchie e Ken Thompson reescreveram quase totalmente o sistema operativo Unix em C. Devido a esta ligao ntima, medida que o Unix se tornou popular no meio acadmico, tambm a linguagem C se tornou a linguagem preferida para o desenvolvimento de aplicaes cientficas. Pelo que, apesar de ter sido concebida para a escrita de sistemas operativos, a linguagem C hoje encarada como uma linguagem de uso geral. A principal caracterstica da linguagem C que combina as vantagens de uma linguagem de alto nvel descendente do AlGOL 68, com a eficincia da linguagem assembly, uma vez que permite a execuo de operaes aritmticas sobre ponteiros e operaes sobre palavras binrias. A linguagem C tambm tem uma sintaxe muito compacta e permite que operadores de tipos diferentes possam ser combinados livremente. Esta liberdade e poder da linguagem C, permite aos programadores experientes escreverem cdigo compacto e eficiente que dificilmente poderiam ser escritos noutras linguagens de programao. Mas, como fracamente estruturada em termos semnticos, tambm permite que construes sem sentido aparente, escritas por programadores inexperientes, sejam aceites pelo compilador como vlidas. O facto da linguagem C ser muito poderosa, exige portanto, do programador muita disciplina e rigor na utilizao das construes da linguagem, para que o cdigo escrito seja legvel e facilmente altervel. Apesar da linguagem C ter sido desenvolvida no princpio da dcada de 1970, a norma ANSI (American National Standards Institute) foi apenas aprovada em 1989 (norma ISO/IEC 9899-1990).

1.2 A estrutura de um programa em C


Ao contrrio do que se passa em Pascal, em que um programa apresenta uma organizao hierrquica que reflecte directamente o algoritmo que lhe deu origem, na linguagem C, um programa organizado horizontalmente como um agrupamento de variveis e funes colocadas todas ao mesmo nvel, uma estrutura conhecida pelo nome de mar de funes. Neste contexto, a diferenciao entre o programa principal e os diversos subprogramas associados feita pelo facto de existir uma funo particular, de nome main, que sempre invocada em primeiro lugar aquando da execuo do programa. Assim, a funo main desempenha na prtica o papel do programa principal do Pascal. Segundo a norma ANSI, a funo main uma funo de tipo inteiro, em que o valor devolvido serve para informar sobre o estado de execuo do programa.

CAPTULO 1 : INTRODUO AO C

De facto, a noo de devolver um valor associado ao estado de execuo de um programa corresponde filosofia subjacente arquitectura de comandos do Unix, em que a linguagem de comandos (shell) , no fundo, uma verdadeira linguagem de programao, que permite construir comandos mais complexos por combinao de comandos mais simples (shell scripts), usando um conjunto de estruturas de controlo muito semelhantes aos encontrados na linguagem C ou Pascal. Neste contexto, os comandos mais simples so programas, de cujo sucesso de execuo vai eventualmente depender a continuao das operaes. Ora, como esta uma rea que no ser explorada no mbito desta disciplina, em muitos programas, sobretudo naqueles que sero desenvolvidos nesta disciplina, no se coloca a questo de devolver um valor. Pelo que, para evitar a mensagem de aviso do compilador, recomenda-se terminar o main com a instruo return 0. Um programa em C tem a estrutura apresentada na Figura 1.1.
aluso a funes e definies externas aluso a funes e definies locais int main ( void ) { declarao de variveis aluso a funes sequncia de instrues return 0; } definio de funes locais

Figura 1.1 - Estrutura de um programa em C.

Vamos analisar as diversas partes de um programa em C atravs do exemplo do programa de converso de distncias de milhas para quilmetros apresentado na Figura 1.2. O ficheiro fonte que contm o programa comea por mencionar as funes e estruturas de dados externas necessrias execuo do programa, que esto implementadas noutros ficheiros providenciados pela linguagem C, as chamadas bibliotecas da linguagem, ou que em alternativa so desenvolvidos pelo utilizador. A linguagem C foi concebida tendo em mente facilitar a construo descentralizada de aplicaes, atravs do fraccionamento do cdigo de um programa por diferentes ficheiros fonte. Por isso, necessrio e inevitvel, mesmo em programas muito simples, recorrer aluso a funes e definies feitas externamente, ou seja, noutros ficheiros. Para tornar mais rigorosa esta referncia, foi criado o conceito de ficheiro de interface, onde todas as aluses e definies associadas a um dado tipo de funcionalidade so colocadas. Estes ficheiros de interface distinguem-se dos ficheiros fonte, propriamente ditos, por terem a terminao .h em vez de .c. A norma ANSI fornece um conjunto muito variado de ficheiros de interface que descrevem as diferentes funcionalidades fornecidas pelas bibliotecas de execuo ANSI. Em Unix, por defeito, todos eles esto armazenados no directrio /usr/include. Da, no ser necessrio referenciar este caminho de um modo explcito. Quando o ficheiro em causa est neste directrio, basta colocar o seu nome entre os smbolos < e >. Em todos os outros casos, a especificao do caminho deve ser includa no nome do ficheiro e o conjunto ser colocado entre aspas duplas.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

A incluso de ficheiros de interface num dado ficheiro fonte feita usando a directiva do pr-processador #include numa das suas duas variantes: x No caso do ficheiro de interface pertencer linguagem C, ento ele est armazenado no directrio por defeito e usa-se a directiva #include <nome do ficheiro de interface>. x No caso do ficheiro de interface ter sido criado pelo utilizador e no estar armazenado no directrio por defeito, usa-se a directiva #include "nome do ficheiro de interface". No mnimo, todos os ficheiros que contenham cdigo que faa acesso aos dispositivos convencionais de entrada e de sada tm que incluir o ficheiro de interface stdio.h, que descreve as funes e que contm as definies associadas com o acesso aos dispositivos de entrada e de sada e aos ficheiros. Normalmente, o dispositivo de entrada o teclado e o dispositivo de sada o monitor. Portanto, qualquer programa interactivo tem pelo menos a aluso a este ficheiro de interface, tal como se apresenta na Figura 1.2. A seguir s definies de objectos externos segue-se a aluso s funes locais que vo ser usadas na funo main, bem como a definio de estruturas de dados e constantes locais. Locais para a aplicao, mas que para o ficheiro, se comportam como definies globais. Repare que a estruturao do programa, muito diferente do Pascal, sendo que as funes so primeiramente aludidas ou referidas, para se tornarem visveis em todo o ficheiro e s depois da funo main que so definidas. Neste exemplo, define-se apenas, atravs da directiva #define, um identificador constante MIL_QUI, que representa o factor de converso de milhas para quilmetros. Ele visvel para todo o cdigo do ficheiro, ou seja, uma constante global. A funo main implementada com instrues simples, e com recurso apenas s funes de entrada e de sada de dados da biblioteca stdio.
/* Programa de converso de distncias de milhas para quilmetros */ /* Instrues para o pr-processador */ #include <stdio.h> #define MIL_QUI /* interface com a biblioteca de entrada/sada */ /* factor de converso */ 1.609

/* Instrues em linguagem C propriamente ditas */ int main ( void ) { double MILHAS, QUILOMETROS; do {

/* distncia expressa em milhas */ /* distncia expressa em quilmetros */

/* Leitura com validao de uma distncia expressa em milhas */

printf ("Distncia em milhas? "); scanf ("%lf", &MILHAS); } while (MILHAS < 0.0); QUILOMETROS = MIL_QUI * MILHAS; /* Converso da distncia */ /* Impresso da distncia expressa em quilmetros */ printf ("Distncia em quilmetros %8.3f\n", QUILOMETROS); return 0; }

Figura 1.2 - Programa da converso de distncias.

Vamos agora analisar com detalhe na Figura 1.3 a definio da funo main, que tal como qualquer outra funo na linguagem C, supe a especificao do seu cabealho e do seu corpo. No cabealho, indica-se o tipo do valor devolvido, que como j foi referido anteriormente sempre do tipo inteiro, o nome, e entre parnteses curvos, a lista de

CAPTULO 1 : INTRODUO AO C

parmetros de comunicao. Neste caso a funo no comunica directamente com o exterior, pelo que, no existe lista de parmetros de comunicao. Quando tal acontece, utiliza-se o identificador void. O corpo da funo delimitado pelos separadores { e }, correspondentes, respectivamente, aos separadores begin e end do Pascal, e contm a declarao das variveis locais, a aluso a funes usadas na sequncia de instrues e a sequncia de instrues propriamente dita. Constitui aquilo que em linguagem C se designa por um bloco.
cabealho corpo { declarao de variveis locais double MILHAS, QUILOMETROS; sequncia de instrues do { printf ("Distncia em milhas? "); scanf ("%lf", &MILHAS); } while (MILHAS < 0.0); QUILOMETROS = MIL_QUI * MILHAS; printf ("Distncia em quilmetros %8.3f\n", QUILOMETROS); return 0; }

int main ( void )

Figura 1.3 - A funo main.

1.3 Elementos bsicos da linguagem C


Os identificadores so nomes que so usados para designar os diferentes objectos existentes no programa, como por exemplo, o nome do programa, os nomes das funes, os nomes das constantes, tipos de dados e variveis. Os identificadores obedecem regra de produo apresentada na Figura 1.4. identificador ::= letra do alfabeto | carcter underscore | identificador letra do alfabeto | identificador carcter underscore | identificador algarismo decimal
Figura 1.4 - Definio formal de um identificador.

Ou seja, so formados por uma sequncia de caracteres alfanumricos e o carcter underscore, em que o primeiro carcter obrigatoriamente uma letra do alfabeto ou o carcter underscore. Embora seja possvel comear um identificador pelo carcter underscore, tal deve ser evitado. Este tipo de notao , em princpio, reservado para o compilador. No h limite para o comprimento de um identificador. Na prtica, esse limite imposto pelo compilador. A norma ANSI exige um comprimento mnimo de 31 e 6 caracteres, respectivamente, para os identificadores internos e externos.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

Na linguagem C, os alfabetos maisculo e minsculo so distintos, ou seja, a linguagem sensvel ao tipo de letra (case sensitive). Assim sendo, o identificador conv_dist diferente do identificador CONV_DIST. Embora no seja obrigatrio, costume designar todos os identificadores da responsabilidade do programador com caracteres maisculos de maneira a distingui-los dos identificadores da linguagem C, que tm de ser escritos obrigatoriamente com caracteres minsculos. Em alternativa, h programadores que gostam de usar um carcter maisculo no primeiro carcter de cada palavra que compe o identificador e os restantes caracteres minsculos. Nesse caso, devem designar pelo menos os nomes das constantes em caracteres maisculos. Mas, o importante que cada programador defina o seu prprio estilo, e que exista uma certa coerncia nas regras que adopte. As palavras reservadas da linguagem C so: auto; break; case; char; const; continue; default; do; double; else; enum; extern; float; for; goto; if; int; long; register; return; short; signed; sizeof; static; struct; switch; typedef; union; unsigned; void; volatile; e while. As palavras reservadas aparecem a cheio ao longo do texto e no cdigo apresentado. Para melhorar a legibilidade do programa, devem ser introduzidos comentrios relevantes, que expliquem o significado dos diferentes objectos, ou que operao efectuada por grupos bem definidos de instrues. Um comentrio uma qualquer sequncia de smbolos inserida entre /* e */ e que no contenha */. Isto significa que no se pode nunca encapsular comentrios. A Figura 1.5 apresenta a definio formal do comentrio. comentrio ::= /* qualquer sequncia de smbolos que no contenha */ */
Figura 1.5 - Definio formal do comentrio.

O uso adequado de comentrios melhora extraordinariamente a legibilidade e a compreenso de um segmento de cdigo. Assim, devem introduzir-se comentrios, pelo menos, nas situaes seguintes: x Em ttulo, para explicar o que faz o segmento de cdigo e descrever, caso exista, o mecanismo de comunicao associado. x Sempre que se declarem constantes ou variveis, para explicar o seu significado, a menos que este seja trivial. x A encabear as pores de cdigo correspondentes decomposio algortmica que lhe deu origem.

1.4 Representao da informao


Em Pascal, os tipos de dados predefinidos esto directamente relacionados com o tipo de informao neles armazenado. Assim, para informao numrica, existem os tipos inteiro (integer) para a representao exacta e real (real) para a representao aproximada; para quantidades lgicas existe o tipo booleano (boolean); e para a representao de smbolos grficos existe o tipo carcter (char). Em linguagem C, pelo contrrio, os tipos de dados predefinidos reflectem apenas o formato de armazenamento. So sempre tipos numricos, embora em alguns casos possibilitem uma interpretao alternativa, funo do contexto em que so usados.

CAPTULO 1 : INTRODUO AO C

A Figura 1.6 apresenta os tipos de dados simples existentes na linguagem C. Estes tipos de dados tambm se designam por escalares, uma vez que, todos os seus valores esto distribudos ao longo de uma escala linear. Dentro dos tipos de dados simples, temos o tipo ponteiro (pointer), o tipo enumerado (enum) e os tipos aritmticos, que se dividem em tipos inteiros e tipos reais. Os tipos aritmticos e o tipo enumerado designam-se por tipos bsicos. Os tipos aritmticos inteiros podem armazenar valores negativos e positivos, que o estado por defeito ou usando o qualificativo signed, ou em alternativa, podem armazenar apenas valores positivos, usando para o efeito o qualificativo unsigned que lhe duplica a gama dinmica positiva. O tipo aritmtico int pode ainda ser qualificado como short, reduzindo-lhe a capacidade de armazenamento. O qualificativo long pode ser usado para aumentar a capacidade de armazenamento do tipo inteiro int e do tipo real double.
Tipos de Dados Simples Qualificativos Ponteiro Aritmticos

short long

pointer
Tipos Inteiros Tipos Reais

signed unsigned

int

char

float
Tipos Bsicos

double

Enumerado

enum

Figura 1.6 - Tipos de dados simples existentes na linguagem C.

1.4.1 Tipos de dados inteiros


Na linguagem C existem os seguintes tipos de dados inteiros: x O tipo char permite a representao de quantidades com sinal num byte e portanto, permite armazenar valores entre -128 a 127. x O tipo unsigned char permite a representao de quantidades sem sinal num byte e portanto, permite armazenar valores entre 0 e 255. x O tipo short [int] permite a representao de nmeros negativos e positivos em 2 bytes e portanto, permite armazenar valores entre -32768 e 32767. x O tipo unsigned short [int] permite a representao de nmeros positivos em 2 bytes e portanto, permite armazenar valores entre 0 e 65535. x O tipo int permite a representao de nmeros negativos e positivos em 2 bytes ou 4 bytes, consoante o computador. x O tipo unsigned int permite a representao de nmeros positivos em 2 bytes ou 4 bytes, consoante o computador. x O tipo long [int] permite a representao de nmeros negativos e positivos em 4 bytes ou 8 bytes, consoante o computador. x O tipo unsigned long [int] permite a representao de nmeros positivos em 4 bytes ou 8 bytes, consoante o computador.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

O tamanho dos tipos inteiros int e long e a sua gama dinmica dependem do compilador e do hardware utilizados. Esta informao indicada no ficheiro limits.h localizado no directrio include do ambiente de desenvolvimento. No caso do computador utilizado nesta disciplina, cujo processador de 32 bits, os tipos int e unsigned int so representados em 4 bytes, pelo que, permitem armazenar respectivamente valores entre -2147483648 e 2147483647 e valores entre 0 e 4294967295. Os tipos long e unsigned long so tambm representados em 4 bytes.

1.4.2 Tipos de dados reais


Na linguagem C existem os seguintes tipos de dados reais: float; double; e long double. O tamanho, a preciso e a gama dinmica dos tipos reais dependem do compilador e do hardware utilizados. Esta informao indicada no ficheiro float.h localizado no directrio include do ambiente de desenvolvimento. Para o caso do computador utilizado nesta disciplina, o tamanho, a preciso e a gama dinmica dos tipos reais so os seguintes: x O tipo float utiliza 4 bytes, o que permite armazenar valores entre 1.2x10-38 e 3.4x1038, com uma mantissa de 6-7 algarismos significativos. x O tipo double utiliza 8 bytes, o que permite armazenar valores entre 2.2x10-308 e 1.8x10308, com uma mantissa de 15-16 algarismos significativos. x O tipo long double utiliza 12 bytes, o que permite armazenar valores entre 3.4x10-4932 e 1.2x104932, com uma mantissa de 18-19 algarismos significativos.

1.4.3 Representao de caracteres e inteiros


A maioria das linguagens de programao, entre as quais se inclui o Pascal, faz a distino entre o tipo inteiro e o tipo carcter. Mesmo, apesar de nessas linguagens, os caracteres serem armazenados na memria em numrico, usando para o efeito o cdigo ASCII. Na linguagem C no existe tal distino. O tipo char que utiliza um byte permite armazenar quer um carcter quer um valor inteiro. A Figura 1.7 apresenta dois exemplos que exemplificam esta polivalncia. No primeiro caso a atribuio do valor 65, que o cdigo ASCII do carcter A, equivalente atribuio do prprio carcter 'A' varivel CAR. No segundo caso a atribuio do valor 3 varivel NUM diferente da atribuio varivel CAR do carcter '3', cujo cdigo ASCII 51. Devido a esta polivalncia de interpretao do valor inteiro armazenado na varivel, o valor escrito no monitor depende do especificador de formato que seja empregue na instruo de sada de dados. O formato %d representa o valor decimal, enquanto que o formato %c representa o carcter.
char CAR; /* declarao da varivel CAR de tipo char */ ... CAR = 'A'; /* ambas as atribuies armazenam */ CAR = 65; /* na posio de memria CAR o valor 65 */ char NUM, CAR; /* declarao das variveis NUM e CAR de tipo char */ ... NUM = 3; /* armazena na posio de memria NUM o valor 3 */ CAR = '3'; /* armazena na posio de memria CAR o valor 51 */

Figura 1.7 - Utilizao do tipo char.

CAPTULO 1 : INTRODUO AO C

1.5 Constantes e variveis


Uma constante um objecto, cujo valor se mantm invariante durante a execuo do programa. A utilizao de um valor constante num programa, no fornece qualquer indicao sobre o seu significado ou finalidade. Pelo que, a utilizao de uma mnemnica, ou seja, um nome associado a um valor constante, permite aumentar a legibilidade de um programa. Por outro lado, se um valor constante aparecer mais do que uma vez ao longo do programa, pode acontecer que o programador cometa algum lapso na repetio do valor e ter um erro algortmico que no detectvel pelo compilador e que pode ser muito difcil de detectar pelo prprio programador. A utilizao de uma constante permite assim parametrizar um programa, melhorar a legibilidade, j que os valores so substitudos por nomes com significado explcito e, a robustez, porque a alterao do valor realizada de um modo centralizado. Ao contrrio do que se passa em Pascal, a linguagem C no contempla a definio explcita de identificadores associados com constantes. Esta restrio pode ser ultrapassada, tal como se apresenta na Figura 1.2, usando a seguinte directiva do pr-processador. #define identificador de constante expresso O que o pr-processador faz, ao encontrar esta directiva no ficheiro fonte, efectuar a partir desse ponto a substituio de todas as ocorrncias do identificador de constante pela expresso, o que se designa por uma macro de substituio. Como se trata de um processo de substituio puro, e no de clculo do valor associado, torna-se necessrio, sempre que a expresso no for um literal, coloc-la entre parnteses curvos para garantir o clculo correcto do seu valor, independentemente do contexto em que est localizada. #define identificador de constante ( expresso ) Um dos erros mais frequentemente cometido, por programadores que se esto a iniciar na utilizao da linguagem C, quando utilizam esta directiva na definio de um identificador constante a sua terminao com o ;. Nesse caso o ; torna-se parte da substituio podendo gerar situaes de erro. As constantes numricas inteiras podem ser representadas no sistema decimal, no sistema octal, em que a constante precedida pelo dgito 0 ou no sistema hexadecimal, em que a constante precedida pelo dgito 0 e pelo carcter x. Quando as constantes esto representadas nos sistemas octal ou hexadecimal, o sinal normalmente expresso de uma maneira implcita, usando a representao em complemento verdadeiro. A Figura 1.8 apresenta alguns exemplos, considerando uma representao do tipo int em 32 bits.
Sistema decimal 54 -135 em complemento verdadeiro 0 Sistema octal 066 -0207 037777777571 00 Sistema hexadecimal 0x36 -0x87 0xFFFFFF79 0x0

Figura 1.8 - Exemplos de constantes inteiras.

O compilador atribui por defeito o tipo int a uma constante numrica inteira. Quando este tipo no tem capacidade de armazenamento suficiente, os tipos seguintes so sucessivamente atribudos. No caso de uma constante decimal, usa-se o tipo long, ou se ainda for insuficiente o tipo unsigned long. No caso de uma constante octal ou hexadecimal, usa-se pela seguinte ordem o tipo unsigned int, ou o tipo long, ou o tipo unsigned long.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

A atribuio do tipo pode ser forada pelos sufixos U para unsigned e L para long. Uma constante seguida do sufixo U do tipo unsigned int ou do tipo unsigned long. Uma constante seguida do sufixo L do tipo long ou do tipo unsigned long. Uma constante seguida do sufixo UL do tipo unsigned long. As constantes numricas reais so sempre expressas no sistema decimal, usando quer a representao em parte inteira e parte fraccionria, quer a chamada notao cientfica. O compilador atribui por defeito o tipo double a uma constante numrica real. A atribuio do tipo pode ser forada pelos sufixos F e L. Uma constante seguida do sufixo F do tipo float. Uma constante seguida do sufixo L do tipo long double. So exemplos de constantes reais os valores 0.0148, 1.48e-2 e 0.0. As constantes de tipo carcter podem ser expressas indiferentemente pelo respectivo smbolo grfico, colocado entre aspas simples, ou atravs do valor do seu cdigo de representao nos sistemas octal e hexadecimal, precedidas do carcter '\', tal como se mostra na Figura 1.9.
Sistema decimal 'B' Sistema octal '\102' Sistema hexadecimal '\x42'

Figura 1.9 - Exemplo de constante de tipo carcter.

Para alguns caracteres de controlo, pode ainda ser usada uma representao alternativa que consiste numa letra do alfabeto minsculo precedida do carcter '\'. Por exemplo o carcter de fim de linha o '\n', o carcter de backspace o '\b', o carcter de tabulao o '\t', o carcter aspas duplas o '\' e o carcter ponto de interrogao o '\?'. As constantes de tipo cadeia de caracteres so expressas como uma sequncia de caracteres, representados por qualquer dos mtodos anteriores, colocados entre aspas duplas. Por exemplo Ola malta!\n. Uma varivel um objecto, cujo valor se altera em princpio durante a execuo do programa, excepo eventualmente feita s variveis de entrada, cujo valor depois de lido do teclado , em princpio, mantido inalterado at ao fim da execuo do programa. Todas as variveis usadas num programa tm que ser previamente definidas ou declaradas. O objectivo da declarao simultaneamente a reserva de espao em memria para o armazenamento dos valores que as variveis vo sucessivamente tomar, e a associao de cada identificador com a rea de memria correspondente. A Figura 1.10 apresenta a definio formal da declarao de variveis na linguagem C. Para declarar variveis comea-se por identificar o tipo de dados seguido da varivel, ou lista de variveis que se pretendem declarar desse tipo, separadas por vrgulas, terminando a declarao com o separador ;. conveniente agrupar a declarao de variveis do mesmo tipo na mesma linha para aumentar a legibilidade do programa. A reserva de espao em memria no pressupe, em princpio, a atribuio de um valor inicial varivel. Em consequncia, nada deve ser presumido sobre o seu valor antes que uma primeira atribuio tenha sido efectivamente realizada. No entanto, a linguagem C permite combinar a definio de uma varivel com a atribuio de um valor inicial, usando para o efeito o operador de atribuio seguido da expresso de inicializao.

11

CAPTULO 1 : INTRODUO AO C

declarao de variveis ::= declarao de variveis de um tipo | declarao de variveis |declarao de variveis de um tipo declarao de variveis de um tipo ::= tipo de dados lista de variveis ; tipo de dados ::= qualquer tipo de dados vlido na linguagem C lista de variveis ::= identificador de varivel genrico | lista de variveis , identificador de varivel genrico identificador de varivel genrico ::= identificador de varivel | identificador de varivel = expresso de inicializao identificador de varivel ::= identificador vlido na linguagem C
Figura 1.10 - Definio formal da declarao de variveis.

Ao contrrio de outras linguagens de programao a linguagem C usa apenas um nico smbolo, o smbolo =, como operador de atribuio. A diferena do operador de atribuio em relao ao Pascal um dos erros mais frequentemente cometido por programadores que se esto a iniciar na utilizao da linguagem C. A regio de reserva de espao em memria principal no necessariamente contgua, pelo que, frequente surgirem buracos resultantes do alinhamento das variveis, em reas de endereos mltiplos de 4, para maximizar a taxa de transferncia de informao entre o processador e a memria principal. A Figura 1.11 apresenta alguns exemplos de declarao de variveis e sua colocao na memria.

A
char A, B = 'G'; unsigned int C = 1324; double D = -2.5, E;

'G'

4 bytes

rea no reservada

1324

4 bytes

rea reservada mas no inicializada

rea reservada e inicializada

-2.5

8 bytes

8 bytes

Figura 1.11 - Alguns exemplos de declarao de variveis e sua colocao na memria.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

1.6 Sequenciao
Tal como foi referido anteriormente, o corpo de uma funo delimitado pelos separadores { e }, correspondentes, respectivamente, aos separadores begin e end do Pascal, e contm a declarao das variveis locais, a aluso a funes usadas na sequncia de instrues e a sequncia de instrues propriamente dita. A Figura 1.12 apresenta a definio formal de uma sequncia de instrues. Ao contrrio do que se passa em Pascal, na linguagem C cada instruo simples obrigatoriamente terminada com o separador ;, a menos que o ltimo smbolo da instruo seja o separador }. sequncia de instrues simples ::= instruo simples | sequncia de instrues simples instruo simples instruo simples ::= instruo de atribuio | instruo decisria | instruo repetitiva | instruo de entrada-sada | invocao de uma funo
Figura 1.12 - Definio formal da sequncia de instrues.

Em Pascal, a invocao de uma funo, sendo uma expresso, no pode ser considerada uma instruo. Na linguagem C, contudo, o conceito de procedimento no tem uma existncia separada. Define-se como sendo uma funo de um tipo especial, o tipo void. Pelo que, a invocao de um procedimento , por isso, em tudo semelhante invocao de uma funo de qualquer outro tipo, quando o valor devolvido no tido em considerao. Logo, nestas circunstncias, na linguagem C, a pura e simples invocao de uma funo de qualquer tipo considerada uma instruo. A sequenciao de instrues a forma mais simples de controlo de fluxo num programa, em que as instrues so executadas pela ordem em que aparecem no programa. Dentro das instrues simples, apenas as instrues de atribuio, de entrada-sada e de invocao de uma funo, so verdadeiramente instrues de sequenciao, j que, as instrues decisrias e repetitivas permitem alterar a ordem do fluxo do programa. Tal como no Pascal, na linguagem C tambm existe o conceito de instruo composta, cuja definio formal se apresenta na Figura 1.13, e que composta por uma sequncia de instrues simples encapsuladas entre os separadores { e }. Uma instruo composta um bloco de instrues simples que se comporta como uma instruo nica e usada em instrues decisrias e repetitivas. instruo composta ::= { sequncia de instrues simples }
Figura 1.13 - Definio formal da instruo composta.

1.6.1 Expresses
A Figura 1.14 apresenta a definio formal de uma expresso. Uma expresso uma frmula que produz um valor. Pode assumir as seguintes formas: ser uma constante; ser uma varivel; ser o resultado da invocao de uma funo; ser uma expresso composta por operandos e operadores, sendo que existem operadores unrios e binrios; e ser uma expresso entre parnteses curvos.

13

CAPTULO 1 : INTRODUO AO C

expresso ::= constante | varivel | invocao de uma funo | operador unrio expresso | expresso operador unrio expresso operador binrio expresso | ( expresso )
Figura 1.14 - Definio formal de uma expresso.

Os primeiros trs tipos de expresses so a frmula mais simples de produzir um valor e resumem-se atribuio a uma varivel, do valor de uma constante, ou do valor de uma varivel ou do valor devolvido por uma funo. As expresses mais complexas envolvem operadores unrios ou binrios. Uma expresso pode ser composta por uma expresso colocada entre parnteses curvos. Este tipo de expresso usa-se para compor expresses mais complexas, ou para modificar a prioridade do clculo de expresses parcelares. Vamos agora analisar as expresses aritmticas. O clculo de uma expresso complexa supe um processo de decomposio prvio em expresses mais simples, que determinado pela precedncia e pela associatividade dos operadores presentes. Precedncia significa importncia relativa entre os operadores. Operadores de maior precedncia foram a ligao a si dos operandos, antes dos operadores de menor precedncia Por exemplo, como a multiplicao tem precedncia sobre a adio, ento a expresso a + b * c tem subjacente o agrupamento a + (b * c). Quando numa expresso todos os operadores tm a mesma precedncia, a associatividade permite determinar a ordem de ligao dos operandos aos operadores, da direita para a esquerda, ou da esquerda para a direita. Por exemplo, como a associatividade da adio da direita para a esquerda, ento a expresso a + b + c tem subjacente o agrupamento (a + b) + c. No entanto, qualquer que seja o agrupamento imposto pela precedncia e pela associatividade dos operadores presentes, ele pode ser sempre alterado pela introduo de parnteses curvos. No caso de aritmtica inteira, sempre que numa expresso surgem constantes, variveis, ou se invocam funes de tipo char ou short, os seus valores so automaticamente convertidos pelo compilador em quantidades de tipo int. Do mesmo modo, constantes, variveis, ou funes de tipo unsigned char ou unsigned short, so automaticamente convertidas pelo compilador em quantidades de tipo int, ou de tipo unsigned int, se o primeiro tipo no tiver capacidade de armazenamento suficiente. No caso de aritmtica real, sempre que numa expresso surgem constantes, variveis, ou se invocam funes de tipo float, os seus valores so automaticamente convertidos pelo compilador em quantidades de tipo double. Os operadores binrios supem normalmente operandos do mesmo tipo. Quando so de tipo diferente, a expresso do tipo com menor capacidade de armazenamento, automaticamente convertida no tipo da outra expresso. A hierarquia dos diferentes tipos de dados, expressa por ordem decrescente da sua capacidade de armazenamento, a que se apresenta a seguir.

long double o double o float o unsigned long o long o unsigned int o int
Alm das converses automticas que foram referidas, a converso de uma expresso num tipo especfico qualquer pode ser sempre forada atravs do operador cast. ( qualquer tipo de dados escalar vlido em C ) ( expresso numrica )

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

A Figura 1.15 apresenta um exemplo da utilizao do operador cast. A diviso de duas variveis inteiras d um resultado inteiro. Para forar a diviso real preciso forar um dos operandos a double, fazendo um cast de um dos operandos, neste caso da varivel A.
int A = 5, B = 2; double DIVISAO; ... DIVISAO = A / B; /* DIVISAO = 2.0 */ DIVISAO = (double) A / B; /* DIVISAO = 2.5 */

Figura 1.15 - Exemplo da utilizao do operador cast.

Se a converso se efectua no sentido crescente da hierarquia, no h risco de overflow ou de perda de preciso. Caso contrrio, estes problemas podem ocorrer. Concretamente, quando a converso se efectua no sentido decrescente da hierarquia, podemos ter as situaes apresentadas na Figura 1.16.
Tipos de dados de partida Tipos de dados de chegada long double double ou float existe arredondamento e portanto pode ocorrer overflow double float existe arredondamento e portanto pode ocorrer overflow qualquer tipo real qualquer tipo inteiro existe truncatura e portanto pode ocorrer overflow tipo inteiro signed (unsigned) mesmo tipo inteiro unsigned (signed) mudana na interpretao do valor tipo inteiro unsigned tipo hierarquicamente inferior unsigned resto do mdulo do registo de chegada tipo inteiro signed tipo hierarquicamente inferior signed pode ocorrer overflow

Figura 1.16 - Situaes possveis se a converso se efectuar no sentido decrescente da hierarquia.

Vamos agora apresentar alguns exemplos representativos destes problemas. A Figura 1.17 apresenta um exemplo da situao em que se atribui um valor negativo de uma varivel int a uma varivel unsigned int. O valor armazenado na memria em binrio vai ser interpretado como sendo positivo, pelo que, h uma mudana na interpretao do valor.
int A = -1024; ... B = A; unsigned int B;

/* A = -102410 = 1111 1111 1111 1111 1111 1100 0000 00002 */ /* B = 1111 1111 1111 1111 1111 1100 0000 00002 = 429496627210*/

Figura 1.17 - Mudana na interpretao do valor.

A Figura 1.18 apresenta um exemplo da situao em que se atribui uma varivel unsigned int a uma varivel unsigned char, que tem uma menor capacidade de armazenamento. O valor que vai ser armazenado em B constitudo pelos ltimos 8 bits de A, ou seja, o resto da diviso de A por 256, que o mximo valor que se pode armazenar num byte.

15

CAPTULO 1 : INTRODUO AO C

unsigned int A = 1025; ... B = A;

unsigned char B;

/* A = 102510 = 0000 0000 0000 0000 0000 0100 0000 00012 */ /* B = 0000 00012 = 110*/

Figura 1.18 - Resto do mdulo do registo de chegada.

A Figura 1.19 apresenta um exemplo da situao em que se atribui uma varivel int a uma varivel char, que tem uma menor capacidade de armazenamento. Como o valor de A excede a capacidade de armazenamento de B, ento temos uma situao de overflow. Esta situao facilmente detectada, uma vez que, o valor de chegada negativo, quando o valor de partida era positivo.
int A = 1152; ... B = A; char B;

/* A = 102510 = 0000 0000 0000 0000 0000 0100 1000 00002 */ /* B = 1000 00002 = -12810*/

Figura 1.19 - Ocorrncia de overflow.

No clculo de uma expresso complexa, uma vez fixado o agrupamento, segundo as regras da prioridade e da associatividade dos operadores envolvidos, modificadas ou no pela introduo de parnteses curvos, a ordem pela qual o compilador calcula as diferentes subexpresses em larga medida arbitrria. O compilador pode mesmo reorganizar a expresso, se isso no afectar o resultado final. Em geral, esta questo no acarreta consequncias graves. Contudo, sempre que o clculo de uma expresso envolva operadores com efeitos colaterais, ou seja, uma expresso em que o valor de uma ou mais variveis afectado pelo processo de clculo, normalmente devido utilizao dos operadores unrios incremento e decremento, o cdigo resultante pode deixar de ser portvel. Assim, de extrema importncia organizar cuidadosamente a formao das expresses para se evitar que tais situaes ocorram. Quando no clculo de uma expresso existe o risco de ocorrncia de overflow em resultado de uma possvel transformao da expresso numa equivalente, tal como se mostra na Figura 1.20, isso deve ser impedido por decomposio do clculo da expresso em duas ou mais expresses parcelares, seno o cdigo resultante pode deixar de ser portvel.
int X, K1 = 1024, K2 = 4096, K3 = 4094; ... X = K1 * K1 * (K2 K3); /* se o compilador transformar a expresso na expresso */ /* aparentemente equivalente X = K1 * K1 * K2 K1 * K1 * K3 */ /* vai ocorrer overflow para uma representao int em 32 bits */

Figura 1.20 - Exemplo de uma expresso onde existe o risco de ocorrncia de overflow.

A linguagem C permite ainda a construo de um tipo especial de expresso, cuja finalidade fornecer o tamanho em bytes do formato de um tipo de dados particular. Esse tipo pode ser representado, explicitamente, pelo seu identificador, ou, implicitamente, por qualquer expresso desse tipo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

sizeof ( qualquer tipo de dados vlido em C ) ou sizeof ( expresso ) No segundo caso, o valor da expresso nunca calculado, sendo unicamente determinado o seu tipo. A norma ANSI exige que o tipo do resultado do operador sizeof seja do tipo inteiro e unsigned, ou seja, do tipo unsigned int ou do tipo unsigned long. Este tipo o tipo size_t que est definido na biblioteca stdlib.

1.6.2 Operadores
A Figura 1.21 apresenta os operadores aritmticos disponveis na linguagem C.
Operadores Unrios Operador Simtrico Incremento de 1 Decremento de 1 Smbolo + ++ -Sintaxe +x -x ++x x++ --x x-Observaes

x tem que ser uma varivel x tem que ser uma varivel

Operadores Binrios Operador Smbolo Sintaxe Observaes Adio + x + y Subtraco x - y Multiplicao * x * y Diviso / x / y y 0 Resto da diviso inteira % x % y y 0 e x e y tm de ser expresses inteiras

Figura 1.21 - Operadores aritmticos.

Os operadores unrios incremento e decremento, s podem ser usados com variveis e, incrementam e decrementam o valor da varivel de uma unidade. Podem ser colocados antes da varivel, o que se designa por pr-incremento e pr-decremento. Nesse caso o valor da varivel alterado antes da sua utilizao na expresso em que a varivel est inserida. Ou, podem ser colocados depois da varivel, o que se designa por ps-incremento e ps-decremento. Nesse caso o valor da varivel s alterado depois da sua utilizao na expresso em que a varivel est inserida. A Figura 1.22 apresenta um exemplo da utilizao do operador unrio incremento. Na primeira expresso, uma vez que estamos perante o ps-incremento de X, ento no clculo de Y utilizado o valor de X antes deste ser incrementado, pelo que, Y fica com o valor 10 e s depois que o valor de X incrementado. Na segunda expresso, uma vez que estamos perante o pr-incremento de X, ento em primeiro lugar X incrementado e s depois calculado o valor de Y, pelo que, Y fica com o valor 15. Em ambas as expresses o valor final de X igual a 3.
int X = 2, Y; ... Y = 5 * X++; /* aps o clculo da expresso X = 3 e Y = 10 */ Y = 5 * ++X; /* aps o clculo da expresso X = 3 e Y = 15 */

Figura 1.22 - Exemplo da utilizao do operador unrio incremento.

17

CAPTULO 1 : INTRODUO AO C

A Figura 1.23 apresenta uma expresso que utiliza o operador ps-incremento incorrectamente, uma vez que, o cdigo resultante pode deixar de ser portvel. Neste tipo de expresso impossvel prever o valor final da expresso, uma vez que tal depende da ordem de clculo dos operandos. Pelo que, quando numa expresso existe um operador com efeito colateral, a varivel afectada s pode ser usada uma e uma nica vez.
int X, K = 3; ... X = K * K++; /* se a ordem de clculo for da esquerda para a direita, X = 9 */ /* se a ordem de clculo for da direita para a esquerda, X = 12 */

Figura 1.23 - Exemplo de uma expresso com utilizao incorrecta do operador ps-incremento.

O operador diviso usado simultaneamente para representar o quociente da diviso inteira e da diviso real. Tal como se mostra na Figura 1.15, tratar-se- de uma diviso inteira, sempre que os operandos forem inteiros e tratar-se- de uma diviso real, quando pelo menos um dos operandos for real. Quando as expresses do quociente e do resto da diviso inteira so quantidades negativas, o resultado no univocamente determinado e depende do compilador utilizado. No caso do quociente da diviso inteira, a norma ANSI possibilita dois tipos de aproximao, a truncatura ou a aproximao ao maior inteiro, menor ou igual ao correspondente quociente real. Se os operandos so ambos positivos ou negativos, o resultado obtido pelos dois mtodos idntico. Se um dos operandos negativo, tal no se passa. Por exemplo, 7/(-3) ou -7/3, tanto pode produzir um quociente de -2, como de -3. Para evitar esta ambiguidade e garantir-se a portabilidade, sempre que haja a possibilidade do quociente ser negativo, a operao x/y deve ser substituda, por exemplo, pela expresso seguinte, de maneira a forar a aproximao por truncatura. (tipo inteiro) ((double) x/y) Para o resto da diviso inteira a norma ANSI impe que se verifique a seguinte condio. x = x % y + (x / y) * y Isto significa que, conforme o tipo de aproximao usado em x/y e a localizao do operando negativo, diferentes restos so possveis quando um dos operandos negativo. 7 % (-3) = 1 -7 % 3 = -1 7 % (-3) = -2 -7 % 3 = 2 (truncatura) (aproximao ao maior inteiro menor ou igual)

Na prtica, o problema no to grave, porque a expresso x%y no faz matematicamente sentido para y negativo e, por isso, no deve nunca ser usada neste contexto. Alm disso, quando x negativo, fcil verificar que os dois resultados possveis so congruentes. Contudo, para se obter uma portabilidade completa, conveniente substituir a expresso x%y, pela construo condicional seguinte. if (x >= 0) r = x % y ; else r = y - (-x) % y ; A Figura 1.24 apresenta os operadores relacionais disponveis na linguagem C. Em relao ao Pascal existe apenas diferena nos operadores de igualdade e de desigualdade.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

Operador Igual Diferente Maior Menor Maior ou igual Menor ou igual

Smbolo == != > < >= <=

Sintaxe x == y x != y x > y x < y x >= y x <= y

Figura 1.24 - Operadores relacionais.

A expresso resultante de tipo int e assume o valor zero, se o resultado da comparao for falso, e o valor um, se o resultado for verdadeiro. Ao contrrio do que se passa geralmente, a precedncia dos diversos operadores no a mesma. Os operadores maior ou igual, menor ou igual, maior e menor tm maior precedncia do que os operadores igual e diferente. Dado que os tipos reais fornecem apenas uma representao aproximada das quantidades numricas, nunca deve ser usado o operador igualdade com operandos desse tipo. Mesmo os operadores maior ou igual e menor ou igual devem ser usados com extremo cuidado. A expresso booleana de terminao de um processo de contagem, nomeadamente, nunca deve ser formada com expresses de tipo real. A Figura 1.25 apresenta os operadores lgicos que se aplicam tanto a expresses numricas inteiras como a reais. Os operandos so interpretados como representando o valor falso, se forem iguais a zero, e como representando o valor verdadeiro, em todos os restantes casos. A expresso resultante de tipo int e assume o valor zero, se o resultado da comparao for falso, e o valor um, se o resultado for verdadeiro. Ao contrrio do Pascal, uma expresso formada por operadores lgicos nem sempre calculada at ao fim. O clculo procede da esquerda para a direita e o processo interrompido logo que o resultado esteja definido.
Operador Unrio Operador Negao (not) Smbolo ! Operadores Binrios Operador Conjuno (and) Disjuno inclusiva (or) Smbolo && || Sintaxe x && y x || y Sintaxe !x

Figura 1.25 - Operadores lgicos.

A Figura 1.26 apresenta os operadores para manipulao de bits que se aplicam apenas a expresses numricas inteiras.
Operador Unrio Operador Complemento (not) Smbolo ~ Operadores Binrios Operador Conjuno (and) Disjuno inclusiva (or) Disjuno exclusiva (xor) Deslocamento direita Deslocamento esquerda Smbolo & | ^ >> << Sintaxe x & y x | y x ^ y x >> y x << y Sintaxe ~x

Figura 1.26 - Operadores de manipulao de bits.

19

CAPTULO 1 : INTRODUO AO C

Os operadores lgicos actuam isoladamente sobre cada um dos bits dos operandos. Para os operadores lgicos binrios, as operaes so efectuadas em paralelo sobre os bits localizados em posies correspondentes de cada um dos operandos. O resultado da operao uma quantidade inteira do tipo do operando com maior capacidade de armazenamento, no caso dos operadores lgicos binrios, ou do tipo do operando x, no caso do operador complemento booleano ou dos operadores de deslocamento. Para os operadores de deslocamento, o operando y representa o nmero de posies a deslocar no sentido pretendido. O resultado da operao no est definido, quando y maior ou igual do que o comprimento em bits do operando x, ou negativo. A norma ANSI impe a realizao de um deslocamento lgico, quando x for de um tipo qualquer unsigned. Contudo, nada garantido quando x for de um tipo signed, embora normalmente o deslocamento seja ento aritmtico. Assim, para se obter uma portabilidade completa, deve fazer-se sempre um cast para tipos unsigned. (unsigned int ou unsigned long) x >> y (unsigned int ou unsigned long) x << y A Figura 1.27 apresenta a tabela de associatividade e de precedncia, por ordem decrescente, entre os operadores das expresses numricas.
Operadores na classe operadores unrios operador cast operador sizeof + ++ ! ~ operadores binrios * / % + >> << >= <= > =! == & ^ | && || esquerda esquerda esquerda esquerda esquerda esquerda esquerda esquerda esquerda esquerda o o o o o o o o o o direita direita direita direita direita direita direita direita direita direita direita direita direita direita o o o o esquerda esquerda esquerda esquerda maior Associatividade Precedncia

<

menor

Figura 1.27 - Precedncia e associatividade entre os operadores das expresses numricas.

1.6.3 Instrues de atribuio


Ao contrrio do Pascal que s tem uma forma da instruo de atribuio, na linguagem C existem as quatro variantes da instruo de atribuio que se apresentam na Figura 1.28. Regra geral, os tipos da varivel e da expresso podem ser quaisquer, desde que sejam tipos escalares. Aps o clculo da expresso e antes que a atribuio tenha lugar, o valor da expresso automaticamente convertido para o tipo da varivel.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

20

instruo de atribuio ::= identificador de varivel = expresso ; | identificador de varivel operador binrio= expresso ; | operador unrio inc_dec identificador de varivel ; | identificador de varivel operador unrio inc_dec ; | identificador de varivel = expresso-1 ? expresso_2 : expresso_3 ; operador unrio inc_dec ::= operador unrio ++ | operador unrio
Figura 1.28 - Definio formal da instruo de atribuio.

A primeira variante da instruo de atribuio a instruo de atribuio tpica das linguagens imperativas, em que se atribui o valor de uma expresso a uma varivel. semelhante encontrada em Pascal. A nica diferena que o operador de atribuio , neste caso o =, em vez de :=. A Figura 1.29 apresenta trs exemplos.
int ... Y = X = Z = X, Y, Z; X + 2 * Z; X + 1; pow (Y, 2);

/* pow a funo potncia, donde Z = Y2 */

Figura 1.29 - Exemplos da primeira variante da instruo de atribuio. A segunda variante uma construo tpica da sintaxe compacta da linguagem C, onde o operador de atribuio precedido por um qualquer operador binrio. Esta variante definida pela expresso geral operador binrio=, em que o operador binrio representa qualquer operador binrio aritmtico (*, /, %, +, ), ou de manipulao de bits (&, |, ^, >>, <<). A instruo equivalente operao binria da expresso com a prpria varivel como segundo operando, ou seja, equivalente seguinte instruo de atribuio. identificador de varivel = identificador de varivel operador binrio expresso A importncia desta notao tem a ver no s com uma maior clareza na indicao da operao a realizar, particularmente quando o identificador representa um campo de uma varivel de tipo complexo, como tambm com a possibilidade fornecida ao compilador de proceder mais facilmente a uma optimizao do cdigo gerado. A Figura 1.30 apresenta dois exemplos.
int X, Y, Z; ... Y += 5; Z *= 5 + X;

/* equivalente a Y = Y + 5 */ /* equivalente a Z = Z * (5 + X) */

Figura 1.30 - Exemplos da segunda variante da instruo de atribuio.

A terceira variante a utilizao dos operadores unrios incremento e decremento sobre uma varivel. Independentemente se serem utilizados na situao de pr ou ps actuao sobre a varivel, os efeitos colaterais apresentados por este tipo de operadores resultam no incremento ou decremento de uma unidade ao valor da varivel. As instrues ++identificador de varivel; e identificador de varivel ++; so equivalentes seguinte instruo. identificador de varivel = identificador de varivel + 1 ; E, as instrues identificador de varivel; e identificador de varivel; so equivalentes seguinte instruo. identificador de varivel = identificador de varivel 1 ;

21

CAPTULO 1 : INTRODUO AO C

A importncia desta notao tem a ver de novo com a compactao resultante e com a possibilidade fornecida ao compilador de proceder mais facilmente a uma optimizao do cdigo gerado. A Figura 1.31 apresenta dois exemplos.
int X, Y; ... Y++; --X;

/* equivalente a Y = Y + 1 */ /* equivalente a X = X - 1 */

Figura 1.31 - Exemplos da terceira variante da instruo de atribuio.

A quarta variante uma instruo de atribuio condicional, onde o valor da primeira expresso avaliada e caso seja verdadeira, ento atribudo o valor resultante do clculo da segunda expresso varivel, seno atribudo o valor resultante do clculo da terceira expresso varivel. Comporta-se assim como a instruo condicional binria if then else e equivalente ao cdigo em linguagem C que se apresenta a seguir. if (expresso_1) identificador de varivel = expresso_2 ; else identificador de varivel = expresso_3 ; Em termos formais, trata-se de um caso particular da primeira variante em que a expresso a indicada do tipo expresso_1 ? expresso_2 : expresso_3, ou seja, o agrupamento das trs expresses pelo operador ? :. Este operador constitui o nico exemplo de operador ternrio existente na linguagem C. Enquanto que a segunda e a terceira expresses podem ser de qualquer tipo vlido na linguagem C, desde que compatveis, segundo as regras de converso, com o tipo da varivel, a primeira expresso de um tipo escalar bsico. A precedncia deste operador surge imediatamente abaixo dos operadores descritos at ao momento e a sua associatividade da direita para a esquerda. A Figura 1.32 apresenta um exemplo.
int X, ABSX; ... ABSX = X < 0 ? X : X; /* equivalente a if (X < 0)

then ABSX = -X;

else ABSX = X; */

Figura 1.32 - Exemplo da quarta variante da instruo de atribuio.

Uma caracterstica notvel da Linguagem C que a instruo de atribuio tambm uma expresso. O seu valor o valor atribudo ao operando da esquerda, ou seja, ao identificador de varivel. O que faz com que os operadores = e operador binrio= apresentem uma precedncia imediatamente abaixo do operador condicional e uma associatividade igualmente da direita para a esquerda. Tornam-se por isso possveis instrues de atribuio mltipla, cuja sintaxe a que se apresenta a seguir. identificador de varivel_1 = ... = identificador de varivel_N = expresso ; Note-se que, devido associatividade e regra geral de converso automtica, relevante a ordem pela qual surgem na instruo as variveis, quando pertencentes a tipos escalares diferentes. Qualquer alterao a esta ordem pode conduzir a valores distintos atribudos a algumas delas. tambm perfeitamente possvel substituir, em qualquer ponto da cadeia de atribuies, o operador = pelo operador operador binrio=, numa das suas mltiplas formas. Isto, contudo, no deve nunca ser feito, porque a interpretao da instruo resultante torna-se muito difcil e, portanto, muito sujeita a erros. A Figura 1.33 apresenta dois exemplos de instrues de atribuio mltipla.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

22

int X, Y, Z; ... X = Y = Z; X += Y -= Z;

/* equivalente a X = (Y = Z); */ /* equivalente a X = (X + (Y = (Y-Z)); */

Figura 1.33 - Exemplos de instrues de atribuio mltipla.

A Figura 1.34 apresenta a precedncia e a associatividade entre os operadores de atribuio.


Operadores na classe operador condicional ? : = += -= *= /= %= >>= <<= &= ^= |= Associatividade direita o esquerda direita o esquerda Precedncia maior menor

Figura 1.34 - Precedncia e associatividade entre os operadores de atribuio.

1.7 Estruturas de controlo


1.7.1 Instrues decisrias
Na linguagem C existem dois tipos de instrues de tomada de deciso. A instruo decisria binria if e a instruo decisria mltipla switch.

1.7.1.1 A instruo decisria binria if


A instruo decisria binria if (se), cuja definio formal se apresenta na Figura 1.35, tem duas variantes que so fundamentalmente semelhantes s encontradas em Pascal. A nica diferena reside no facto do separador then no surgir aqui. Em consequncia, a expresso decisria obrigatoriamente colocada entre parnteses curvos para estabelecer a separao da instruo a executar. A expresso decisria deve ser de um tipo escalar bsico. A expresso falsa se for igual a zero e verdadeira se assumir qualquer outro valor. Nestas condies, o compilador aceita qualquer expresso numrica como expresso decisria vlida. Porm, por questes de clareza, isto deve ser evitado. de bom estilo que a expresso decisria represente sempre uma expresso booleana. instruo decisria binria ::= if ( expresso ) instruo simples ou composta | if ( expresso ) instruo simples ou composta else instruo simples ou composta expresso ::= expresso decisria de um tipo escalar bsico
Figura 1.35 - Definio formal da instruo if.

Uma questo muito importante para editar programas legveis o alinhamento das instrues. A Figura 1.36 apresenta como se deve alinhar a instruo if. No caso da variante mais simples e se existir apenas uma instruo simples curta, ento a instruo if pode ser toda escrita na mesma linha, mas se a instruo simples for longa deve ser escrita na linha seguinte mais alinhada para a direita. Caso a instruo seja composta, ento os separadores { e } devem ser alinhados com o if. No caso da variante completa, devemos alinhar o separador else com o if.

23

CAPTULO 1 : INTRODUO AO C

if ( expresso ) instruo simples; if ( expresso ) { instruo simples; ... instruo simples; } if ( expresso ) { instruo simples; ... instruo simples; } else { instruo simples; ... instruo simples; }

Figura 1.36 - Alinhamento da instruo if.

A Figura 1.37 apresenta um encadeamento de instrues if, o que se designa por instrues if encadeadas (nested if structures). Neste tipo de agrupamento, a regra de agrupamento semelhante de Pascal. O compilador associa sempre o separador else instruo if que ocorreu imediatamente antes.
if ((CAR >= 'A') && (CAR <= 'Z')) printf ("Carcter Maiusculo\n"); else if ((CAR >= 'a') && (CAR <= 'z')) printf ("Carcter Minusculo\n"); else if ((CAR >= '0') && (CAR <= '9')) printf ("Carcter Numerico\n"); else printf ("Outro Carcter\n");

Figura 1.37 - Construo de instrues if encadeadas.

Vamos agora considerar a situao apresentada na Figura 1.38, em que queremos ter o separador else para o primeiro if, mas em que o segundo if no o tem. Neste tipo de situao, que se designa por else desligado (dangling else), o separador else vai ser atribudo pelo compilador ao segundo if, independentemente de ter sido alinhado com o primeiro if.
if (V1 > 0) if (V2 > 0) else V1++;

V2++; /* situao do else desligado */

Figura 1.38 - Situao do else desligado.

Para resolver este problema existem as duas solues apresentadas na Figura 1.39. A primeira, consiste em usar uma instruo composta a abraar o segundo if, de maneira a informar o compilador onde acaba o segundo if. A segunda, consiste em usar um separador else com uma instruo nula, para emparelhar com o segundo if, e assim forar o emparelhamento do segundo separador else com o primeiro if. A instruo nula o ;.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

24

if {

(V1 > 0)

if (V2 > 0) V2++; } else V1++; if (V1 > 0) if (V2 > 0) V2++; else ; else V1++;

Figura 1.39 - Solues para a situao do else desligado.

Devido ao facto das instrues de atribuio constiturem tambm expresses, um erro muito frequentemente cometido por programadores que se esto a iniciar na utilizao da linguagem C e que no sinalizado pelo compilador, consiste em trocar o operador identidade pelo operador de atribuio na escrita da condio decisria, tal como se apresenta na Figura 1.40. O valor da expresso A = 5 5 e como um valor diferente de zero, ento a expresso verdadeira e vai ser executada a instruo B = 2. Pelo que, aps a execuo da instruo, as variveis A e B vo assumir respectivamente, os valores 5 e 2.
if (A = 5) B = 2; else B = 4; /* o que se pretendia era if (A == 5) B = 2; */

Figura 1.40 - Erro devido troca do operador identidade pelo operador de atribuio.

1.7.1.2 A instruo decisria mltipla switch


Muitas das situaes de estruturas if encadeadas podem ser resolvidas atravs da instruo decisria mltipla switch (comutador), cuja definio formal se apresenta na Figura 1.41. instruo decisria mltipla ::= switch ( expresso ) { bloco de execuo } bloco de execuo ::= bloco de execuo selectivo | bloco de execuo bloco de execuo terminal bloco de execuo selectivo ::= lista de constantes sequncia de instrues simples | bloco de execuo selectivo lista de constantes sequncia de instrues simples lista de constantes ::= case constante inteira : | lista de constantes case constante inteira : bloco de execuo terminal ::= default : sequncia de instrues simples
Figura 1.41 - Definio formal da instruo switch.

Embora as palavras reservadas na linguagem C sejam distintas das do Pascal, a estrutura sintctica da instruo de deciso mltipla essencialmente a mesma nas duas linguagens. Na norma ANSI, a expresso de deciso, bem como as constantes que formam a lista de constantes, so de qualquer tipo escalar bsico inteiro. A principal diferena decorre do modo como os vrios ramos de seleco esto organizados. Em Pascal, eles so mutuamente exclusivos, enquanto que, na linguagem C, a execuo sequencial a partir do

25

CAPTULO 1 : INTRODUO AO C

ponto de entrada. Para se conseguir o mesmo tipo de execuo encontrado em Pascal, a ltima instruo de cada sequncia de instrues ter que ser obrigatoriamente a instruo break, tal como se mostra na Figura 1.43. A Figura 1.42 apresenta como se deve alinhar a instruo. Para aumentar a legibilidade, o bloco de execuo selectivo e o bloco de execuo terminal devem ser alinhados mais direita. As sequncias de instrues simples devem ser todas alinhadas, permitindo uma melhor anlise das instrues que vo ser executadas.
switch ( expresso ) { case V1 : instruo case V2 : instruo break; case V3 : instruo instruo break; case V4 : instruo break; default : instruo }

simples; simples; simples; simples; simples; simples;

Figura 1.42 - Exemplo da utilizao e alinhamento da instruo switch.

A Figura 1.43 faz a comparao da instruo switch com a instruo case.


case CAR of /* na linguagem Pascal */ 'a', 'e', 'o' : writeln ('Vogais speras'); 'i', 'u' : writeln ('Vogais doces'); else writeln ('Outros smbolos grficos') end; switch (CAR) /* na linguagem C */ { case 'a' : case 'e' : case 'o' : printf ("Vogais speras\n"); break; case 'i' : case 'u' : printf ("Vogais doces\n"); break; default : printf ("Outros smbolos grficos\n"); }

Figura 1.43 - Comparao da instruo switch com a instruo case.

preciso ter em considerao que a instruo switch no to poderosa como a instruo case do Turbo Pascal, uma vez que a ltima permite que a lista de valores enumerada possa ser constituda por um literal, ou por um conjunto de literais separados pela vrgula, ou por um intervalo de valores, ou por combinaes de todas estas situaes.

1.7.2 Instrues repetitivas


Tal como na linguagem Pascal, na linguagem C existem dois tipos de instrues de repetio. As instrues while e do while, cujo nmero de iteraes previamente desconhecido, tm uma estrutura de controlo condicional. A instruo for, cujo nmero de iteraes previamente conhecido, tem normalmente uma estrutura de controlo contadora.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

26

1.7.2.1 As instrues repetitivas while e do while


A Figura 1.44 apresenta a definio formal das instrues while (enquanto fazer), e do while (fazer enquanto). instruo while ::= while ( expresso ) instruo simples ou composta

instruo do while ::= do { sequncia de instrues simples } while ( expresso ) ;


Figura 1.44 - Definio formal das instrues while e do while.

As instrues while e do while correspondem, respectivamente, s sintaxes while do e repeat until encontradas no Pascal. A expresso decisria deve ser de um tipo escalar bsico. A expresso falsa se for igual a zero e verdadeira se assumir qualquer outro valor. Nestas condies, o compilador aceita qualquer expresso numrica como expresso decisria vlida. Porm, por questes de clareza, isto deve ser evitado. de bom estilo que a expresso decisria represente sempre uma expresso booleana. Ao contrrio da instruo repeat until do Pascal, a sequncia de instrues simples da instruo do while colocada entre os separadores { e }. No entanto, existe uma diferena semntica importante entre a instruo repeat until do Pascal e a instruo do while da linguagem C. Em Pascal, o ciclo repetitivo executado at a expresso decisria de terminao ser verdadeira. Mas, na linguagem C, o ciclo repetitivo executado enquanto a expresso decisria de terminao for verdadeira. Ou seja, em situaes equivalentes, uma necessariamente a negao da outra. Tal como em Pascal, a utilizao correcta de qualquer das instrues supe que a expresso decisria de terminao possa ser modificada, durante a execuo do ciclo repetitivo, para que o seu valor passe eventualmente de verdadeiro a falso. Caso contrrio, o ciclo repetitivo seria infinito. preciso igualmente garantir que todas as variveis que constituem a expresso decisria so previamente inicializadas. A Figura 1.45 apresenta como se deve alinhar a instruo while. No caso da variante mais simples e se existir apenas uma instruo simples curta, ento a instruo while pode ser toda escrita na mesma linha, mas se a instruo simples for longa deve ser escrita na linha seguinte mais alinhada para a direita. No caso da variante, em que, o corpo do ciclo repetitivo constitudo por uma instruo composta, os separadores { e } que definem a instruo composta devem ser alinhadas pela palavra reservada while, e a sequncia de instrues simples so escritas, uma por linha, todas alinhadas mais direita de maneira a que seja legvel onde comea e acaba o ciclo repetitivo.
while ( expresso ) instruo simples; while ( expresso ) { instruo simples; ... instruo simples; }

Figura 1.45 - Alinhamento da instruo while.

27

CAPTULO 1 : INTRODUO AO C

A Figura 1.46 apresenta como se deve alinhar a instruo do while. As palavras reservadas do e while devem ser alinhadas e a condio booleana de terminao deve ser escrita frente do terminador while. As instrues que constituem o corpo do ciclo repetitivo so escritas uma por linha e todas alinhadas mais direita de maneira a que seja legvel onde comea e acaba o ciclo repetitivo.
do { instruo simples; ... instruo simples; } while ( expresso );

Figura 1.46 - Alinhamento da instruo do while.

A Figura 1.47 faz a comparao dos ciclos repetitivos do while e while, para calcular a mdia de um nmero indeterminado de nmeros lidos do teclado, sendo que, a leitura termina quando lido o valor zero. No caso do ciclo repetitivo do while, as instrues constituintes do corpo do ciclo repetitivo, contagem do nmero til de nmeros lidos e sua soma, necessitam de ser protegidas quando lido o valor de terminao, para no provocar um clculo errneo.
SOMA = 0.0; /* clculo da mdia com o ciclo do while */ do { printf ("Introduza um numero? "); scanf ("%lf", &NUMERO); if (NUMERO != 0.0) { SOMA += NUMERO; N++; } } while (NUMERO != 0.0); SOMA = 0.0; /* clculo da mdia com o ciclo while */ printf ("Introduza um numero? "); scanf ("%lf", &NUMERO); while (NUMERO != 0.0) { SOMA += NUMERO; N++; printf ("Introduza um numero? "); scanf ("%lf", &NUMERO); }

Figura 1.47 - Exemplo comparativo da utilizao dos ciclos repetitivos do while e while.

1.7.2.2 A instruo repetitiva for


A Figura 1.48 apresenta a definio formal da instruo repetitiva for (para fazer enquanto), cujo nmero de iteraes previamente conhecido. A instruo for da linguagem C constitui uma superinstruo que no tem correspondncia em mais nenhuma linguagem. A parte da inicializao executada em primeiro lugar e uma s vez. Em geral, a sua funo atribuir valores iniciais a uma ou mais variveis usadas no ciclo repetitivo. A parte da terminao uma expresso que calculada antes do incio de cada nova iterao e que determina a continuao, se for verdadeira, ou no, se for falsa, do processo repetitivo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

28

A expresso deve ser de um tipo escalar bsico. A expresso falsa se for igual a zero e verdadeira se assumir qualquer outro valor. Nestas condies, o compilador aceita qualquer expresso numrica como expresso decisria vlida. Porm, por questes de clareza, isto deve ser evitado. de bom estilo que a expresso decisria represente sempre uma expresso booleana. Finalmente, a parte de actualizao executada no fim de cada iterao. Em geral, a sua funo actualizar os valores de uma ou mais variveis usadas no ciclo repetitivo, entre as quais, sempre a varivel contadora. Normalmente, as expresses de actualizao das variveis recorrem aos operadores unrios incremento e decremento ou ao operador de atribuio precedido por um qualquer operador binrio. instruo for ::= for ( inicializao ; terminao ; actualizao ) instruo simples ou composta inicializao ::= identificador de varivel = expresso | inicializao , identificador de varivel = expresso terminao ::= expresso actualizao ::= identificador de varivel operador binrio= expresso | operador unrio inc_dec identificador de varivel | identificador de varivel operador unrio inc_dec | actualizao , identificador de varivel operador binrio= expresso | actualizao , operador unrio inc_dec identificador de varivel | actualizao , identificador de varivel operador unrio inc_dec operador unrio inc_dec ::= operador unrio ++ | operador unrio
Figura 1.48 - Definio formal da instruo for.

A instruo for deve ser alinhada da mesma maneira que a instruo while. A Figura 1.49 faz a comparao da instruo for do Pascal e do C usando como exemplo, o clculo das primeiras N potncias de 2.
POT := 1; /* na linguagem Pascal */ for I := 1 to N do begin writeln ('Potencia de 2 = ', POT:10); POT := POT * 2 end; for (POT = 1, I = 1 ; I <= N ; I++, POT *= 2) /* na linguagem C */ printf ("Potncia de 2 = %10d\n", POT);

Figura 1.49 - Exemplo do clculo das potncias de 2 com um ciclo repetitivo for.

Qualquer uma das trs partes componentes da instruo for, a inicializao, a terminao ou a actualizao pode ser omitida. Situaes especiais correspondem aos seguintes casos: x Quando todos os elementos so omitidos, ou seja, for ( ; ; ), temos um ciclo repetitivo infinito. x Quando a inicializao e a actualizao so omitidas, ou seja, for ( ; terminao ; ), temos um ciclo repetitivo funcionalmente equivalente ao ciclo repetitivo while. Ao contrrio do que se passa em Pascal, a varivel contadora tem um valor bem definido aps o esgotamento do ciclo repetitivo, que ser aquele que conduziu a um valor falso da expresso de terminao.

29

CAPTULO 1 : INTRODUO AO C

Devido sua grande versatilidade, a instruo for deve ser utilizada com extremo cuidado. de bom estilo us-la apenas como uma generalizao da instruo for de Pascal. O que significa, nomeadamente, que o valor da varivel contadora nunca deve ser modificado dentro do ciclo repetitivo. Um dos erros mais frequentemente cometido por programadores que se esto a iniciar na utilizao da linguagem C, consiste em esquecer que o ciclo repetitivo for actua enquanto a expresso de terminao for verdadeira. Se no exemplo anterior fosse utilizada a condio I == N, o ciclo repetitivo no seria executado uma nica vez, a no ser que N fosse 1, quando se pretende que o ciclo repetitivo seja executado N vezes. Os ciclos repetitivos podem ser encadeados, tal como a instruo condicional binria, dando origem a estruturas repetitivas em que, uma instruo do corpo do ciclo repetitivo ela mesmo um ciclo repetitivo. A este tipo de construo d-se o nome de ciclos imbricados (nested loops).

1.7.2.3 Instrues nula, break e continue


H ainda trs instrues que so muitas vezes usadas em conjunto com as instrues repetitivas. So elas a instruo nula ou muda, a instruo break e a instruo continue. A instruo nula, expressa pela colocao do separador ;, surge muitas vezes associada com as instrues while e for quando se pretende que o corpo do ciclo repetitivo no tenha qualquer instruo. A Figura 1.50 apresenta um exemplo que determina o comprimento de uma cadeia de caracteres. Uma vez que uma cadeia de caracteres um agregado de caracteres terminado obrigatoriamente com o carcter '\0', ento preciso detectar o ndice do agregado onde ele est armazenado. O que possvel fazer atravs de uma instruo repetitiva em que a expresso de teste, recorrendo ao operador unrio incremento, provoca o deslocamento dentro do agregado e, portanto, o ciclo repetitivo no carece de qualquer instruo.
COMP = -1; while (s[++comp] != '\0') ; /* ou em alternativa com o ciclo for */ for (comp = 0; s[comp] != '\0'; comp++) ;

Figura 1.50 - Clculo do comprimento de uma cadeia de caracteres.

A instruo break uma instruo que, genericamente, permite a sada intempestiva do bloco que est a ser executado. A sua utilizao no caso da instruo de deciso mltipla switch possibilitou, como se viu, transformar operacionalmente esta instruo e adequ-la ao tipo de funcionalidade encontrado na instruo case do Pascal. A sua aplicao em conjuno com as instrues while e do while tambm muito interessante, j que possibilita em muitas situaes prticas reduzir a complexidade da expresso de terminao, aumentando por isso a clareza e a legibilidade do cdigo correspondente. A Figura 1.51 apresenta um exemplo em que se pretende obter a soma de um mximo de 10 valores no negativos lidos do dispositivo de entrada, sendo que a leitura parar mais cedo se for lido um valor negativo. Compare as solues e repare na simplificao da condio de terminao da verso em linguagem C, devido utilizao da instruo break.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

30

N := 0; /* na linguagem Pascal */ SOMA := 0.0; readln (NUMERO); while (N < 10) and (NUMERO >= 0.0) do begin N := N + 1; SOMA := SOMA + NUMERO; readln (NUMERO) end; N = 0; /* na linguagem C */ SOMA = 0.0; scanf ("%lf", &NUMERO); while (N < 10) { if (NUMERO < 0.0) break; N++; SOMA += NUMERO; scanf ("%lf", &NUMERO); }

Figura 1.51 - Exemplo da utilizao da instruo break.

A instruo continue uma instruo que permite interromper a execuo do ciclo repetitivo, forando o fim da iterao presente. A sua aplicao em conjuno com a instruo for possibilita em muitas situaes prticas reduzir a complexidade do ciclo repetitivo, aumentando por isso tambm a clareza e a legibilidade do cdigo correspondente. A Figura 1.52 apresenta um exemplo em que se pretende contar o nmero de caracteres alfabticos de uma linha de texto e converter as vogais a existentes de minsculas para maisculas e vice-versa. O exemplo apresentado a seguir socorre-se das rotinas de manipulao de caracteres e de cadeias de caracteres das bibliotecas de execuo ANSI e supe, por isso, a incluso dos ficheiros de interface ctype.h e string.h.
N := 0; /* na linguagem Pascal */ for I := 1 to length (LINHA) do if LINHA[I] in ['A'..'Z','a'..'z'] then begin N := N + 1; if LINHA[I] in ['A','E','I','O','U','a','e','i','o','u'] then if LINHA[I] in ['A','E','I','O','U'] then LINHA[I] := chr(ord(LINHA[I])-ord('A')+ord('a')) else LINHA[I] := chr(ord(LINHA[I])-ord('a')+ord('A')) end; for (I = 0, N = 0; I < strlen (LINHA); I++) /* na linguagem C */ { if (!isalpha (LINHA[I])) continue; N++; if ((toupper(LINHA[I]) != 'A') && (toupper(LINHA[I]) != 'E') && (toupper(LINHA[I]) != 'I') && (toupper(LINHA[I]) != 'O') && (toupper(LINHA[I]) != 'U')) continue; if (isupper (LINHA[I])) LINHA[I] = LINHA[I]-'A'+'a'; else LINHA[I] = LINHA[I]-'a'+'A'; }

Figura 1.52 - Exemplo da utilizao da instruo continue.

31

CAPTULO 1 : INTRODUO AO C

Compare as solues e repare que as instrues condicionais na verso em linguagem C esto simplificadas, uma vez que no necessitam de ser encadeadas, para evitar as situaes em que os caracteres no precisam de ser convertidos. Se o carcter no um carcter alfabtico, ou se um carcter alfabtico, mas no uma vogal, ento fora-se a prxima iterao do ciclo repetitivo, usando para esse efeito a instruo continue.

1.7.2.4 Ciclos repetitivos infinitos


Um ciclo repetitivo infinito um ciclo repetitivo que no tem condio de terminao, ou cuja condio de terminao est mal implementada. A maior parte desses ciclos repetitivos so criados por engano. Mas, existem situaes em que se pretende de facto implementar um ciclo repetitivo infinito. Por exemplo, quando se pretende uma aplicao que repete sistematicamente uma determinada operao at que uma condio particular se verifica. Nessa situao mais fcil construir um ciclo repetitivo, que terminado atravs do recurso instruo break. A Figura 1.53 apresenta as duas formas mais simples de implementar ciclos repetitivos infinitos.
while ( 1 ) /* com o ciclo while */ instruo simples ou composta for ( ; ; ) /* com o ciclo for */ instruo simples ou composta

Figura 1.53 - Ciclos repetitivos infinitos.

1.8 Entrada e sada de dados


Tal como em qualquer outra linguagem, as instrues de entrada e de sada da linguagem C so instrues especiais que estabelecem uma interface com o sistema operativo na comunicao com os diferentes dispositivos do sistema computacional, em particular com os dispositivos convencionais de entrada e de sada. Normalmente, o dispositivo de entrada o teclado e mencionado pelo identificador stdin, e o dispositivo de sada o monitor e mencionado pelo identificador stdout. Os dispositivos de entrada e de sada funcionam da seguinte forma. Sempre que se prime uma tecla do teclado, gerada uma palavra binria, representando o valor atribudo ao carcter correspondente, em cdigo ASCII por exemplo, que armazenada num registo do controlador do teclado. O sistema operativo alertado para esse facto e, eventualmente, vai ler o contedo do registo e armazenar sequencialmente o valor lido numa regio da memria principal que se designa por armazenamento tampo de entrada (input buffer). O valor tambm quase sempre colocado no armazenamento tampo de sada do monitor, para posterior envio para o monitor, produzindo-se assim a interaco visual com o utilizador. Em termos da instruo de entrada, o armazenamento tampo de entrada visto como uma sequncia ordenada de caracteres, organizada em linhas que consistem em zero ou mais caracteres, com representao grfica, ou com funes de controlo, seguidos do carcter de fim de linha que o '\n'. A esta sequncia de caracteres d-se o nome de fluxo de caracteres (text stream). De um modo geral, s linhas completas presentes no fluxo de caracteres de entrada esto disponveis para leitura. medida que os caracteres vo sendo processados pela instruo de entrada, vo sendo retirados do fluxo de caracteres de entrada.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

32

Por outro lado, o fluxo de texto de sada implementado numa regio da memria principal, directamente acessvel pelo sistema operativo, que se designa por armazenamento tampo de sada (output buffer). Aps a escrita de uma sequncia de caracteres pela instruo de sada, o sistema operativo alertado para esse facto e, eventualmente, vai transferir essa sequncia, carcter a carcter, para um registo do controlador do monitor. Em resultado disso, a mensagem associada reproduzida no monitor a partir da posio actual do cursor e da esquerda para a direita. Alm de caracteres com representao grfica, as sequncias escritas podem conter caracteres de controlo diversos que possibilitam, entre outras aces mais ou menos especficas, o deslocamento do cursor para uma outra posio do monitor. Na linguagem C, as instrues de entrada e de sada so funes de tipo int que pertencem biblioteca de execuo ANSI e cuja descrio est contida no ficheiro de interface stdio.h.

1.8.1 A funo scanf


Na linguagem C, a entrada de dados implementada pela funo scanf cuja sintaxe se apresenta na Figura 1.54. A funo scanf l sequncias de caracteres do fluxo de caracteres de entrada (stdin) e processa-as segundo as regras impostas pelo formato de leitura, armazenando sucessivamente os valores convertidos nas variveis, cuja localizao indicada na lista de ponteiros de variveis. Salvo em duas situaes especiais adiante referidas, deve existir uma relao de um para um entre cada especificador de converso e cada varivel da lista de ponteiros de variveis. Se o nmero de variveis da lista de ponteiros de variveis for insuficiente, o resultado da operao no est definido. Se, ao contrrio, o nmero de variveis for demasiado grande, as variveis em excesso no so afectadas. O tipo da varivel e o especificador de converso devem ser compatveis, j que a finalidade deste ltimo indicar, em cada caso, que tipos de sequncias de caracteres so admissveis e como devem ser tratadas. Quando o especificador de converso no vlido, o resultado da operao no est definido. O processo de leitura s termina quando o formato de leitura se esgota, lido o carcter de fim de ficheiro, ou existe um conflito de tipo entre o que est indicado no formato de leitura e a correspondente quantidade a ser lida. Neste ltimo caso, o carcter que causou o conflito mantido no fluxo de caracteres de entrada. A funo devolve o nmero de valores lidos e armazenados, ou o valor fim de ficheiro (EOF), se o carcter de fim de ficheiro lido antes que qualquer converso tenha lugar. Se, entretanto, ocorreu um conflito, o valor devolvido corresponde ao nmero de valores lidos e armazenados at ocorrncia do conflito. Pondo de parte o facto de se tratar de uma instruo de leitura formatada, scanf semanticamente equivalente instruo read de Pascal. No existe, contudo, na linguagem C um equivalente directo para a instruo readln. O papel de um carcter separador no formato de leitura forar a eliminao do fluxo de caracteres de entrada de todos os caracteres deste tipo at primeira ocorrncia de um carcter distinto. O papel de um literal no formato de leitura estabelecer a correspondncia com sequncias idnticas de caracteres existentes no fluxo de caracteres de entrada, e, por consequncia, elimin-las do processo de leitura. A correspondncia com o carcter %, que no um literal, obtida atravs do especificador de converso %% e no h lugar a qualquer armazenamento. O supressor de atribuio, que o carcter *, possibilita que uma sequncia de caracteres seja lida e convertida, mas o seu valor no seja armazenado em qualquer varivel.

33

CAPTULO 1 : INTRODUO AO C

int scanf ( formato de leitura , lista de ponteiros de variveis ) formato de leitura ::= "cadeia de caracteres de definio" cadeia de caracteres de definio ::= especificador de converso | carcter separador | literal | cadeia de caracteres de definio especificador de converso | cadeia de caracteres de definio carcter separador | cadeia de caracteres de definio literal especificador de converso ::= %carcter de converso | %modificador de converso carcter de converso carcter de converso ::= i l uma quantidade inteira genrica d l uma quantidade inteira em decimal (signed) u l uma quantidade inteira em decimal (unsigned) o l uma quantidade inteira em octal (unsigned) x l uma quantidade inteira em hexadecimal (unsigned) f e g l uma quantidade real em decimal c l caracteres s l uma cadeia de caracteres [lista de caracteres] l uma cadeia de caracteres imposta pela lista de caracteres. Se o primeiro carcter for o ^ ento l uma cadeia de caracteres at encontrar um carcter da lista de caracteres p l o valor de um ponteiro para void n permite contar o nmero de caracteres lidos at ao seu aparecimento na cadeia de caracteres de definio modificador de converso ::= largura mxima de campo | especificador de dimenso | largura mxima de campo especificador de dimenso | supressor de atribuio | supressor de atribuio largura mxima de campo | supressor de atribuio especificador de dimenso | supressor de atribuio largura mxima de campo especificador de dimenso largura mxima de campo ::= valor decimal positivo que indica o nmero mximo de caracteres a serem lidos especificador de dimenso ::= h com i, d, u, o, x, indica tipo short l com i, d, u, o, x, indica tipo long com f, e, g, indica tipo double L com f, e, g, indica tipo long double supressor de atribuio ::= * carcter separador ::= carcter espao, carcter tabulador '\t' e carcter fim de linha '\n' literal ::= um ou mais caracteres diferentes do carcter separador ou do carcter % lista de ponteiros de variveis ::= ponteiro de varivel de tipo aritmtico | ponteiro de varivel de tipo void | lista de ponteiros de variveis , ponteiro de varivel de tipo aritmtico | lista de ponteiros de variveis , ponteiro de varivel de tipo void ponteiro de varivel de tipo aritmtico ::= & varivel de tipo aritmtico ponteiro de varivel de tipo void ::= & varivel de tipo void
Figura 1.54 - Definio formal da funo scanf.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

34

Na funo scanf as variveis so parmetros de entrada-sada, pelo que, tm de ser passadas funo por referncia, ou seja, por endereo. Para passar o endereo de uma varivel de tipo simples, temos que preceder a varivel pelo operador endereo que o carcter &. No caso das variveis de tipo agregado, o prprio nome da varivel representa o endereo do elemento inicial da varivel, e portanto, so usados no scanf normalmente.

1.8.1.1 Leitura de caracteres


Para a leitura de um carcter, o especificador de converso deve ser %c ou %1c. Para a leitura de mais do que um carcter, o nmero de caracteres a ser lido ser igual largura mxima de campo e ser necessrio garantir que o ponteiro da varivel referencia uma regio de memria com capacidade para o armazenamento dos caracteres lidos, tipicamente, um agregado de caracteres. A Figura 1.55 apresenta um exemplo de leitura de um carcter para a varivel CAR e de oito caracteres para um agregado de caracteres. A declarao char ARRAYCAR[8] define um agregado de 8 caracteres, sendo que o ndice do primeiro elemento do agregado o ndice zero. A invocao da funo, para uma linha de dados introduzida pelo teclado e constituda pelos caracteres a1234567890'\n', resulta no armazenamento do carcter 'a' na varivel CAR e dos caracteres '1' a '8' nos elementos sucessivos do agregado ARRAYCAR. Para passar o endereo da varivel CAR usa-se o operador &. Para indicar o incio do agregado de caracteres ARRAYCAR, ou se menciona apenas o nome do agregado, ou se indica o endereo do primeiro elemento do agregado &ARRAYCAR[0].
char CAR, ARRAYCAR[8]; ... scanf ("%c%8c", &CAR, ARRAYCAR); /* ou em alternativa scanf ("%c%8c", &CAR, &ARRAYCAR[0]); */

Figura 1.55 - Exemplo da leitura de um carcter.

A Figura 1.56 apresenta um excerto de cdigo em que se utiliza o scanf para ler uma varivel de tipo carcter, mas, de forma equivalente ao readln do Pascal.
#include <stdio.h> int main (void) { char CAR, /* escolha da opo do menu CARX; /* varivel auxiliar para teste de fim de linha ... do { printf ("1 - Opo_1\n"); printf ("2 - Opo_2\n"); printf ("3 - Opo_3\n"); printf ("Qual a sua escolha? "); scanf ("%c", &CAR); /* ler o carcter que representa a opo if (CAR != '\n') do /* descartar todos os eventuais restantes caracteres { /* da linha, incluindo o carcter fim de linha scanf ("%c", &CARX); } while (CARX != '\n'); } while ((CAR < '1') || (CAR > '3')); ... }

*/ */

*/ */ */

Figura 1.56 - Leitura de um carcter equivalente instruo readln do Pascal.

35

CAPTULO 1 : INTRODUO AO C

Depois da leitura da varivel de tipo carcter, caso tenha sido lido um carcter diferente de fim de linha ento vo ser lidos todos os caracteres que eventualmente existam no armazenamento tampo de entrada at leitura do carcter fim de linha inclusive, usando para o efeito uma varivel auxiliar de tipo carcter.

1.8.1.2 Leitura de uma cadeia de caracteres


Uma cadeia de caracteres (string) tipicamente um agregado de caracteres, terminado com o carcter especial '\0'. Este carcter especial serve de indicador de fim da cadeia de caracteres, e obviamente, ocupa uma posio de armazenamento do agregado. Pelo que, se necessitarmos de armazenar uma cadeia de caracteres com MAX_CAR caracteres, devemos definir um agregado de caracteres com o tamanho MAX_CAR+1. O subdimensionamento de uma cadeia de caracteres um dos erros mais frequentemente cometido por programadores que se esto a iniciar na utilizao da linguagem C. A leitura de mais do que um carcter usando o especificador de converso %c no muito prtica, pelo que, prefervel efectu-la como a leitura de uma cadeia de caracteres. Para a leitura de uma sequncia de caracteres que no contenha um carcter separador como elemento constituinte, no fundo, para a leitura de uma palavra, deve-se usar o seguinte especificador de converso. %nmero mximo de caracteres a lers O processo de leitura elimina todas as instanciaes do carcter separador que surjam no princpio e agrupa os caracteres seguintes at ao nmero mximo de caracteres indicados para a leitura, ou at ocorrncia de um carcter separador, o que significa que no possvel ler-se uma cadeia de caracteres nula. O ponteiro da varivel correspondente tem que referenciar uma regio de memria com capacidade para o armazenamento dos caracteres lidos e do carcter '\0', que automaticamente colocado no fim. Assim, fundamental incluir sempre no especificador de converso a largura mxima de campo, caso contrrio, corre-se o risco de, se a palavra for demasiado longa, ultrapassar-se a regio de armazenamento estabelecida e afectar indevidamente o valor de outras variveis. Note-se que o valor do nmero mximo de caracteres a ler tem que ser, pelo menos, inferior a uma unidade ao comprimento do agregado, para garantir que o carcter terminador da cadeia de caracteres sempre armazenado. A Figura 1.55 apresenta um exemplo de leitura de uma cadeia de caracteres, com capacidade para armazenar 7 caracteres teis. A invocao da funo para uma linha de dados introduzida pelo teclado e constituda pelos caracteres joana'\n', resulta no armazenamento dos caracteres 'j', 'o', ... , 'a', '\0' no agregado NOME. No caso de uma linha de dados constituda pelos caracteres universidade'\n', resulta no armazenamento dos caracteres 'u', 'n', ... , 's', '\0' no agregado NOME.
char NOME[8]; ... scanf ("%7s", NOME); /* ou em alternativa scanf ("%7s", &NOME[0]); */

Figura 1.57 - Exemplo da leitura de uma cadeia de caracteres com o especificador de converso %s.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

36

Alternativamente, a leitura de uma sequncia de caracteres pode ser realizada dividindo dicotomicamente os caracteres em duas classes, a classe de caracteres admissveis para a sequncia e a classe de caracteres no admissveis, e descrevendo uma delas no especificador de converso %[lista de caracteres], nas suas duas variantes que se apresentam a seguir. Entre parnteses rectos descreve-se extensivamente numa ordem qualquer os caracteres pertencentes classe de caracteres admissveis ou alternativamente os caracteres pertencentes classe de caracteres no admissveis precedidos pelo carcter ^. %nmero mximo de caracteres a ler [classe de caracteres admissveis] %nmero mximo de caracteres a ler [^classe de caracteres no admissveis] O processo de leitura termina quando for lido o nmero mximo de caracteres a ler ou ocorrer um conflito. Se o conflito acontecer logo no princpio, a varivel correspondente no afectada, o que significa que no possvel ler-se uma cadeia de caracteres nula. De novo, o ponteiro da varivel correspondente tem que referenciar uma regio de memria com capacidade para o armazenamento dos caracteres lidos e do carcter '\0', que automaticamente colocado no fim. Pelas razes apontadas anteriormente, a largura mxima de campo dever tambm ser sempre includa no especificador de converso. A Figura 1.58 apresenta dois exemplos da utilizao do especificador de converso %[ ] para a leitura de cadeias de caracteres. No primeiro caso pretende-se ler um nmero de telefone, que uma sequncia de 9 caracteres exclusivamente composta por caracteres numricos. No segundo exemplo, pretende-se efectuar a leitura de uma sequncia de caracteres quaisquer at ao aparecimento do carcter fim de linha, no mximo de 79 caracteres.
char NUMTEL[10]; ... scanf ("%9[0123456789]", NUMTEL); /* ou em alternativa scanf ("%9[0123456789]", &NUMTEL[0]); */ char FRASE[80]; ... scanf ("%79[^\n]", FRASE); /* ou em alternativa scanf ("%79[^\n]", &FRASE[0]); */

Figura 1.58 - Exemplos da leitura de cadeias de caracteres com o especificador de converso %[ ].

Um dos erros mais frequentes dos programadores que se esto a iniciar na utilizao da linguagem C, consiste em usar simultaneamente os dois especificadores de converso de cadeia de caracteres, inventando o especificador de converso %s[ ]. Estes dois especificadores de converso so alternativos, e, portanto, no podem ser combinados. A Figura 1.59 apresenta um excerto de cdigo em que se utiliza o scanf para ler uma varivel de tipo cadeia de caracteres, mas, de forma equivalente ao readln do Pascal. A funo invocada para ler uma sequncia de caracteres quaisquer, constituda no mximo por 79 caracteres. Depois descartam-se todos os caracteres que eventualmente existam no armazenamento tampo de entrada at leitura do carcter fim de linha. De seguida l-se e descarta-se o carcter fim de linha. Estas duas aces no podem ser combinadas numa s, porque caso no existam caracteres extras, ento a instruo de leitura terminaria abruptamente sem efectuar a leitura do carcter fim de linha, que ficaria no armazenamento tampo de entrada disponvel para posteriores invocaes da funo. Caso no tenha sido lido qualquer carcter para a varivel FRASE, o que pode ser indagado aferindo o resultado devolvido pelo scanf e armazenado na varivel T, que zero nesse caso, ento constri-se

37

CAPTULO 1 : INTRODUO AO C

uma cadeia de caracteres nula, colocando o carcter terminador na primeira posio da cadeia de caracteres.
#include <stdio.h>

int main (void) { char FRASE[80]; /* frase a ser lida int T; /* sinalizao do estado de leitura ... printf ("Escreva a frase: "); T = scanf ("%79[^\n]", FRASE); /* leitura da frase scanf ("%*[^\n]"); /* descartar todos os outros caracteres scanf ("%*c"); /* descartar o carcter fim de linha /* construir uma cadeia de caracteres nula if (T == 0) FRASE[0] = '\0'; ... }

*/ */ */ */ */ */

Figura 1.59 - Leitura de uma cadeia de caracteres equivalente instruo readln do Pascal.

1.8.1.3 Leitura de valores numricos


Na leitura de valores numricos, a introduo do especificador de dimenso no especificador de converso permite atribuir os valores lidos a variveis de tipo short ou de tipo long, em vez de tipo int, ou a variveis de tipo double ou de tipo long double, em vez de tipo float. O recurso largura mxima de campo s deve ser efectuado quando se pretende ler valores com um formato bem definido. Caso contrrio, no deve ser usado para possibilitar uma leitura em formato livre. Se os valores lidos forem demasiado grandes para poderem ser armazenados nas variveis que lhes esto associadas, vai ocorrer overflow. O processo de leitura termina quando for lido um nmero de caracteres vlidos igual largura mxima de campo, caso ela tenha sido especificada, surgir entretanto um carcter separador, ou qualquer outro carcter no vlido, que funciona neste caso tambm como carcter separador, ou ocorrer um conflito. Nesta ltima situao, a varivel correspondente no afectada. A Figura 1.60 apresenta as sequncias de caracteres admissveis para cada carcter de converso.
Carcter de converso i (tipo signed) d u x o (tipo (tipo (tipo (tipo signed) unsigned) unsigned) unsigned) Sequncia admissvel tipos inteiros [+/-]#...# [+/-]0x#...# [+/-]0#...# [+/-]#...# [+/-]#...# [+/-]0x#...# [+/-]0#...# tipos reais f,e,g [+/-]#...# # - algarismo decimal [+/-][#...#][.]#...# [+/-][#...#][.]#...#[e/E[+/-]#...#] # # # # # # # Observao algarismo algarismo algarismo algarismo algarismo algarismo algarismo decimal hexadecimal octal decimal decimal hexadecimal octal

Figura 1.60 - Tabela de sequncias admissveis em funo do carcter de converso.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

38

A Figura 1.61 apresenta alguns exemplos de leituras de valores numricos. A execuo da funo para a linha de dados introduzida pelo teclado e constituda pelos seguintes caracteres -123 0xF2'\t'-1.47e1'\n', resulta no armazenamento das quantidades -123 na varivel I, 242 na varivel S e -14.7 na varivel D. No caso de uma linha de dados constituda pelos caracteres '\t'12 -023 e-4'\n', resulta no armazenamento das quantidades 12 na varivel I, -19 na varivel S e nenhum valor na varivel D, porque ocorreu um conflito. O conflito deve-se ao facto de que a sequncia e-4 no ser uma sequncia de caracteres admissvel para representar uma quantidade real em notao cientfica. Para corrigir este conflito deve-se usar a sequncia de caracteres 1e-4.
int I; short S; double D; ... scanf ("%d%hi%lf", &I, &S, &D);

Figura 1.61 - Exemplo de leitura de valores numricos.

O uso do carcter separador no formato de leitura deve ser evitado porque, ou irrelevante, quando colocado no princpio, ou entre especificadores de converso, j que o carcter separador funciona como elemento de separao de sequncias numricas, ou tem efeitos colaterais nocivos, quando colocado no fim, j que obriga deteco de um carcter distinto no fluxo de texto de entrada, antes da concluso da instruo. Procure descobrir o que vai acontecer quando forem executadas as funes scanf apresentadas na Figura 1.62, para as duas linhas de dados introduzidas pelo teclado e constitudas pelos seguintes caracteres 1 2'\n' e 3'\n'.
int A, B, C; ... printf ("Valores 1 e 2? "); scanf ("%d%d\n", &A, &B); printf ("Valor 3? "); scanf ("%d", &C);

Figura 1.62 - Exemplo incorrecto de leitura de valores numricos.

Por causa do literal '\n' no fim da cadeia de caracteres de definio, a primeira invocao da funo scanf s termina quando se tecla o carcter 3, pelo que, a mensagem da segunda invocao da funo printf s escrita no monitor depois de introduzida a segunda linha de dados, quando deveria aparecer antes. A Figura 1.63 apresenta um excerto de cdigo em que se utiliza o scanf para ler uma varivel de tipo numrica, mas, de forma equivalente ao readln do Pascal. A funo invocada para ler uma varivel numrica, que neste caso inteira de tipo int. Depois descartam-se todos os caracteres que eventualmente existam no armazenamento tampo de entrada at leitura do carcter fim de linha. De seguida l-se e descarta-se o carcter fim de linha. Estas duas aces no podem ser combinadas numa s, porque caso no existam caracteres extras, ento a instruo de leitura terminaria abruptamente sem efectuar a leitura do carcter fim de linha, que ficaria no armazenamento tampo de entrada disponvel para posteriores invocaes da funo. Caso no tenha sido lido qualquer valor numrico para a varivel VAL, o que pode ser indagado aferindo o resultado devolvido pelo scanf e armazenado na varivel T, que zero nesse caso, ento repete-se o processo at que seja efectivamente lido um valor numrico.

39

CAPTULO 1 : INTRODUO AO C

#include

<stdio.h>

int main (void) { int VAL; /* valor a ser lido int T; /* sinalizao do estado de leitura ... printf ("Valor? "); do { T = scanf ("%d", &VAL); /* ler o valor scanf ("%*[^\n]"); /* descartar todos os outros caracteres scanf ("%*c"); /* descartar o carcter fim de linha } while (T == 0); /* repetir enquanto um valor numrico no tiver sido lido ... }

*/ */

*/ */ */ */

Figura 1.63 - Leitura de uma varivel de tipo numrica equivalente instruo readln do Pascal.

A Figura 1.64 apresenta o cdigo necessrio para ler uma informao horria vlida, de tipo HH:MM:SS. O processo de leitura tem que ler trs quantidades numricas positivas da o tipo de dados utilizado ser unsigned. Como os valores no excedem 23 para as horas e 60 para os minutos e segundos, ento podem ser de tipo short. A leitura repetida enquanto houver um erro de formato e enquanto houver valores fora dos limites.
#include <stdio.h>

int main (void) { unsigned short H, M, S; /* hora, minuto e segundo a serem lidos int T; /* sinalizao do estado de leitura ... do { do { /* leitura da informao horria printf ("Informao horria HH:MM:SS? "); T = scanf ("%2hu:%2hu:%2hu", &H, &M, &S); scanf ("%*[^\n]"); /* descartar todos os outros caracteres scanf ("%*c"); /* descartar o carcter fim de linha } while (T != 3); /* repetir enquanto houver um erro de formato } while ((H > 23) || (M > 59) || (S > 59)); /* repetir enquanto houver valores fora dos limites ... }

*/ */

*/ */ */ */ */

Figura 1.64 - Leitura de uma informao horria.

O especificador de converso %n permite contar a nmero de caracteres lidos at ao seu aparecimento na cadeia de caracteres de definio. utilizado quando queremos verificar se o formato dos dados est de acordo com a cadeia de caracteres de definio que se est a utilizar. A Figura 1.65 apresenta um exemplo da sua aplicao. Vamos considerar que queremos ler um valor numrico mas, que este se encontra a seguir a outro valor, e que este primeiro valor deve ser descartado porque no nos interessa. Se por algum motivo existir um erro de formato e existir apenas um valor para ser lido, esse valor descartado e no lido nem armazenado na varivel NUM. Para detectar se h ou no um erro no formato esperado, podemos determinar o nmero de caracteres lidos antes da leitura da varivel NUM. Se o valor de N for nulo sinal que no havia nenhum valor inicial, e, portanto, o formato previsto dos dados de entrada no se verifica.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

40

int NUM, /* valor a ser lido N; /* contador de caracteres lidos ... N = 0; /* inicializao do contador de caracteres lidos scanf ("Numero = %*d%n%d", &N, &NUM); /* leitura do valor e utilizao do contador

*/ */ */ */

Figura 1.65 - Exemplo da utilizao do especificador de converso %n.

1.8.2 A funo printf


Na linguagem C, a sada de dados implementada pela funo printf cuja sintaxe se apresenta na Figura 1.66. A funo printf escreve sucessivamente sequncias de caracteres no fluxo de texto de sada (stdout), representativas de texto e dos valores das expresses que formam a lista de expresses, segundo as regras impostas pelo formato de escrita. O texto especificado no formato de escrita pela introduo de literais, que so copiados sem modificao para o fluxo de texto de sada. O modo como o valor das expresses convertido, descrito pelos especificadores de converso. Deve, por isso, existir uma relao de um para um entre cada especificador de converso e cada expresso da lista de expresses. Se o nmero de expresses for insuficiente, o resultado da operao no est definido. Se, ao contrrio, esse nmero for demasiado grande, as expresses em excesso so ignoradas. O tipo da expresso e o especificador de converso devem ser compatveis, j que a finalidade deste ltimo indicar, em cada caso, o formato da sequncia convertida. Quando o especificador de converso no vlido, o resultado da operao no est definido. O processo de escrita s termina quando o formato de escrita se esgota, ou ocorreu um erro. A funo devolve o nmero de caracteres escritos no fluxo de texto de sada, ou o valor -1, se ocorreu um erro. Como o formato de escrita pode conter literais que so directamente copiados sem modificao para o fluxo de texto de sada, a instruo printf muito geral, podendo ser tornada semanticamente equivalente instruo write de Pascal, quando o formato de escrita no contm o carcter '\n', ou instruo writeln, quando este precisamente o ltimo carcter presente. De um modo geral, a incluso de literais no formato de escrita possibilita melhorar a compreenso dos dados impressos e organizar a sua apresentao de um modo mais regular. A impresso do carcter %, que no um literal, obtida atravs do especificador de converso %%. A largura mnima de campo indica o nmero mnimo de caracteres usados na representao do valor da expresso. Quando a expresso pode ser representada com menos caracteres, so introduzidos espaos ou zeros at perfazer o nmero indicado. Quando a largura mnima de campo insuficiente, o campo alargado para conter o valor convertido.

41

CAPTULO 1 : INTRODUO AO C

int printf ( formato de escrita , lista de expresses ) formato de escrita ::= "cadeia de caracteres de definio" cadeia de caracteres de definio ::= especificador de converso | literal | cadeia de caracteres de definio especificador de converso | cadeia de caracteres de definio literal especificador de converso ::= %carcter de converso | %modificador de converso carcter de converso carcter de converso ::= i d escreve uma quantidade inteira decimal (signed) u escreve uma quantidade inteira em decimal (unsigned) o escreve uma quantidade inteira em octal (unsigned) x escreve uma quantidade inteira em hexadecimal (unsigned) f escreve uma quantidade real em notao PI.PF e E escreve uma quantidade real em notao cientfica. O carcter indicador de expoente e ou E conforme se usa o carcter de converso e ou E g G escreve uma quantidade real no formato f ou e dependente do valor. O carcter indicador de expoente e ou E conforme se usa o carcter de converso g ou G c escreve um caracter s escreve uma cadeia de caracteres p escreve o valor de um ponteiro para void modificador de converso ::= modificador de formato | especificador de preciso | especificador de dimenso | modificador de formato especificador de preciso | modificador de formato especificador de dimenso | especificador de preciso especificador de dimenso | modificador de formato especificador de preciso especificador de dimenso modificador de formato ::= modificador de aspecto | largura mnima de campo | modificador de aspecto largura mnima de campo modificador de aspecto ::= qualquer combinao de espao, , +, #, 0 largura mnima de campo ::= valor decimal positivo que indica o nmero mnimo de caracteres a serem escritos especificador de preciso ::= . | . valor decimal positivo especificador de dimenso ::= h com i, d, u, o, x, indica tipo short l com i, d, u, o, x, indica tipo long L com f, e, g, indica tipo long double literal ::= carcter diferente de % | literal carcter diferente de % lista de expresses ::= expresso de tipo aritmtico | expresso de tipo ponteiro para void | lista de expresses , expresso de tipo aritmtico | lista de expresses , expresso de tipo ponteiro para void
Figura 1.66 - Definio formal da funo printf.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

42

O papel do modificador de aspecto controlar a apresentao dos valores convertidos. Assim, tem-se que: x O carcter , significa justificao da sequncia convertida esquerda no campo. Por defeito, ela justificada direita. x O carcter 0, significa que quando for necessrio introduzir caracteres extra para perfazer a largura mnima de campo na representao de valores numricos, so introduzidos zeros, em vez de espaos, para uma justificao direita. x O carcter +, significa que todos os valores numricos passam a ter sinal, os positivos, o sinal + e os negativos, o sinal . Por defeito, s os valores negativos so representados com sinal. x O carcter espao, significa que os valores numricos positivos passam a ser representados com um espao na posio do sinal. Por defeito, s os valores negativos so representados com sinal. x O carcter #, significa que os valores numricos no sistema hexadecimal ou octal so representados precedidos de '0x' ou '0', respectivamente. Os valores numricos reais so representados com o separador ., mesmo que no haja parte fraccionria. Alm disso, quando for usado o carcter de converso g, os zeros direita na parte fraccionria so representados. O papel do especificador de preciso est associado de alguma forma com a preciso com que os valores convertidos so expressos. Assim, tem-se que: x Para valores inteiros, indica o nmero mnimo de algarismos usados na representao do valor. Se surgir apenas o carcter ., significa um nmero zero de algarismos. x Para valores reais: x Com os caracteres de converso e ou f indica o nmero de algarismos usados na representao da parte fraccionria. Se surgir apenas o carcter ., significa um nmero zero de algarismos. Quando no est presente, esse nmero de 6. x Com o carcter de converso g indica o nmero mximo de algarismos significativos. Se surgir apenas o carcter ., significa um nmero zero de algarismos. x Quando o valor no puder ser expresso no nmero de algarismos indicado pelo especificador de preciso, ento o valor ser arredondado. x Para valores de tipo cadeia de caracteres, indica o nmero mximo de caracteres a imprimir. preciso ter em ateno que no existe uma total simetria nos formatos de converso das funes scanf e printf. Assim, a funo scanf exige o formato de converso %lf para ler um valor double, enquanto que a funo printf usa o formato de converso %f para escrever uma expresso de tipo double. Esta assimetria deve-se ao facto de a linguagem C converter automaticamente expresses reais para double e portanto, no existir de facto a impresso de expresses de tipo float. Esta assimetria um dos erros mais frequentemente cometido por programadores que se esto a iniciar na utilizao da linguagem C. A Figura 1.67 apresenta alguns exemplos da aplicao de vrios formatos de escrita de valores numricos e as respectivas sequncias de caracteres escritas no monitor.

43

CAPTULO 1 : INTRODUO AO C

printf ("->%12d\n", 675); printf ("->%012d\n", 675); printf ("->%12.4d\n", 675); printf ("->%12f\n", 675.95); printf ("->%12.4f\n", 675.95); printf ("->%12.1f\n", 675.95); printf printf printf printf printf printf printf ("->%12.6g\n", 675.0000); ("->%#12.6g\n", 675.0000); ("->%12.3g\n", 675.0); ("->%12.2g\n", 675.0); ("->%12.1g\n", 675.0); ("->%#12.1g\n", 675.0); ("->%#12.1G\n", 675.0);

/* -> 675 */ /* ->000000000675 */ /* -> 0675 */ /* -> /* -> /* -> /* /* /* /* /* /* /* -> -> -> -> -> -> -> 675.950000 */ 675.9500 */ 676.0 */ 675 675.000 675 6.8e+02 7e+02 7.e+02 7.E+02 */ */ */ */ */ */ */

Figura 1.67 - Exemplos de formatos de escrita.

1.8.2.1 Escrita de caracteres


Para a escrita de um carcter, deve-se utilizar o seguinte especificador de converso. %nmero de colunas de impressoc Sendo que o modificador de aspecto , usado quando se pretender a justificao do carcter esquerda dentro do nmero de colunas de impresso indicado. No possvel escrever-se mais do que um carcter de cada vez. A expresso argumento de tipo int convertida para tipo unsigned char e o valor obtido impresso. A Figura 1.68 apresenta alguns exemplos de escrita do carcter *. No primeiro caso no se especifica a largura mnima de campo, pelo que, o carcter escrito numa nica coluna de impresso. Nos segundo e terceiro casos, especifica-se cinco colunas de impresso, com justificao direita, que a situao por defeito, e com justificao esquerda usando o modificador de aspecto .
printf ("->%c<-\n", '*'); printf ("->%5c<-\n", '*'); printf ("->%-5c<-\n", '*'); /* ->*<- */ /* -> *<- */ /* ->* <- */

Figura 1.68 - Escrita de um carcter com vrios formatos.

1.8.2.2 Escrita de cadeias de caracteres


Para a escrita de uma cadeia de caracteres, deve-se utilizar o seguinte especificador de converso. % nmero mnimo de colunas de impresso.nmero mximo de colunas de impressos Sendo que o modificador de aspecto , usado quando se pretender a justificao da cadeia de caracteres esquerda dentro do nmero mnimo de colunas de impresso indicado. A expresso argumento deve referenciar a regio de memria onde a sequncia de caracteres, terminada necessariamente pelo carcter '\0', est armazenada, sendo tipicamente um agregado de caracteres. O nmero de caracteres a imprimir igual ao nmero de caracteres da cadeia de caracteres, se o especificador de preciso no tiver sido usado, ou se tiver um valor maior ou igual ao comprimento da cadeia de caracteres, ou, alternativamente, igual ao valor do especificador de preciso, em caso contrrio.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

44

A Figura 1.69 apresenta alguns exemplos de escrita de uma cadeia de caracteres. No primeiro caso no se usa qualquer formato, pelo que, a escrita ocupa o nmero de colunas de impresso igual ao comprimento da cadeia de caracteres. Nos segundo e terceiro casos como o nmero mnimo de colunas insuficiente, ento ignorado. No quarto caso apenas imprimido o nmero de caracteres indicado pelo especificador de preciso. No quinto caso no impresso nada, uma vez que o especificador de preciso nulo. Nos sexto e stimo casos usa-se um nmero mnimo de colunas de impresso igual ao nmero mximo de colunas de impresso, pelo que, a cadeia de caracteres impressa com esse nmero de caracteres. Como a cadeia de caracteres tem um nmero de colunas inferior ao pretendido, ento so acrescentados espaos direita ou esquerda, conforme a justificao pedida.
char FRASE[30] = "pim pam pum, cada bola ... printf ("->%s<-\n", FRASE); /* ->pim printf ("->%11s<-\n", FRASE); /* ->pim printf ("->%-11s<-\n", FRASE); /* ->pim printf ("->%.11s<-\n", FRASE); printf ("->%.s<-\n", FRASE); printf ("->%8.8s<-\n", "pim"); printf ("->%-8.8s<-\n", "pim"); ..."; pam pum, cada bola ...<pam pum, cada bola ...<pam pum, cada bola ...</* ->pim pam pum</* -></* -> pim</* ->pim <*/ */ */ */ */ */ */

Figura 1.69 - Escrita de uma cadeia de caracteres com vrios formatos.

1.8.2.3 Escrita de valores numricos


A introduo do especificador de dimenso no especificador de converso permite caracterizar o tipo do valor que vai ser convertido, o tipo short ou o tipo long em vez do tipo int, ou o tipo long double em vez do tipo double. Note-se o significado distinto atribudo ao especificador de preciso, tratando-se de valores inteiros ou de valores reais. No primeiro caso significa o nmero mnimo de algarismos da representao. No segundo caso significa o nmero de algarismos da parte fraccionria, quando so usados os caracteres de converso f ou e, ou o nmero mximo de algarismos significativos, que 6 por defeito, quando usado o carcter de converso g. Segundo a norma ANSI, a seleco do estilo de representao, quando usado o carcter de converso g, baseia-se na regra seguinte. A notao cientfica utilizada, quando o expoente resultante for inferior a -4 ou maior ou igual preciso estabelecida. Em todos os outros casos utilizada a representao em parte inteira, a vrgula, que nos computadores o . e a parte fraccionria. A Figura 1.70 apresenta alguns exemplos da aplicao de vrios formatos de escrita de valores numricos inteiros e as respectivas sequncias de caracteres escritas no monitor.
printf ("->%ld***%ld<-\n", 1095L, -1095L); /* ->1095***-1095<printf ("->%+li***%+li<-\n", 1095L, -1095L); /* ->+1095***-1095<printf ("->% .2ld***% .2ld<-\n", 1095L, -1095L); /* -> 1095***-1095<printf ("->%5.5hd***%5.hd<-\n", -13505, -13505); /* ->-13505***-13505<printf ("->%05u***%5u<-\n", 17, 17); /* ->00017*** 17<printf ("->%-5.2x***%#-5.2x<-\n", 17, 17); /* ->11 ***0x11<printf ("->%5.2ho***%#5.2o<-\n", 17, 17); /* -> 21*** 021<*/ */ */ */ */ */ */

Figura 1.70 - Escrita de valores numricos inteiros com vrios formatos.

45

CAPTULO 1 : INTRODUO AO C

A Figura 1.71 apresenta alguns exemplos da aplicao de vrios formatos de escrita de valores numricos reais e as respectivas sequncias de caracteres escritas no monitor.
printf ("->%f***%e<-\n", 13.595, 13.595); /* ->13.595000***1.359500e+01<printf ("->%.f***%.e<-\n", 13.595, 13.595); /* ->14***1e+01<printf ("->%#.f***%#.e<-\n", 13.595, 13.595); /* ->14.***1.e+01<printf ("->%g***%g<-\n", -13.595, -0.000011); /* ->-13.595***-1.1e-05<printf ("->%8.2f***%10.2e<-\n", -13.345, -13.345); /* -> -13.35*** -1.33e+01<printf ("->%08.2f***%010.2e<-\n", -13.345, -13.345); /* ->-0013.35***-01.33e+01<printf ("->%8.2g***%#08.2g<-\n", -13.345, -13.345); /* -> -13***-000013.<-

*/ */ */ */ */ */ */

Figura 1.71 - Escrita de valores numricos reais com vrios formatos.

1.9 Bibliotecas de execuo ANSI


J referimos a biblioteca de execuo ANSI stdio, que descreve as funes de acesso aos dispositivos de entrada e de sada. Agora vamos apresentar mais algumas das bibliotecas de execuo ANSI.

1.9.1 Biblioteca ctype


A biblioteca ctype contm um conjunto de funes para processamento de caracteres. Tem a particularidade de as rotinas poderem ser tornadas independentes do cdigo usado na representao dos caracteres. Para utilizar as suas funes preciso fazer a incluso do ficheiro de interface ctype.h com a seguinte directiva do pr-processador. #include <ctype.h> As funes dividem-se funcionalmente em dois grupos. O grupo das funes de teste de caracteres, que determinam se um dado carcter pertence ou no a uma dada classe. O grupo das funes de converso, que permitem a converso de um carcter do alfabeto maisculo num carcter do alfabeto minsculo e vice-versa. As funes de teste de caracteres tm o seguinte mecanismo de comunicao com o exterior. int nome da funo (int carcter); O valor devolvido no nulo, ou seja, verdadeiro, se o carcter em argumento pertencer classe associada e nulo, ou seja, falso, caso contrrio. Se o valor em argumento no puder ser representado como um unsigned int, o resultado indefinido. As funes de converso tm o seguinte mecanismo de comunicao com o exterior. int nome da funo (int carcter); O valor devolvido o do carcter em argumento, se o carcter j estiver convertido, ou no se tratar de um carcter alfabtico e o do carcter convertido, no caso contrrio.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

46

A Figura 1.72 apresenta as funes de teste de caracteres e as funes de converso.


funes de teste de caracteres Nome da funo isalnum isalpha iscntrl isdigit isgraph islower isprint Classe de pertena caracteres alfabticos e algarismos decimais caracteres alfabticos caracteres de controlo algarismos decimais todos os caracteres com representao grfica caracteres alfabticos minsculos todos os caracteres com representao grfica mais o carcter espao ispunct todos os caracteres com representao grfica menos os caracteres alfabticos e os algarismos decimais isspace espao, '\f', '\n', '\r', '\t' e '\v' isupper caracteres alfabticos maisculos isxdigit algarismos hexadecimais funes de converso Nome da funo tolower toupper Tipo de converso do alfabeto maisculo para o alfabeto minsculo do alfabeto minsculo para o alfabeto maisculo

Figura 1.72 - Funes da biblioteca ctype.

1.9.2 Biblioteca math


A biblioteca math contm um conjunto de funes matemticas. Para utilizar as suas funes preciso fazer a incluso do ficheiro de interface math.h com a seguinte directiva do pr-processador. #include <math.h> Quando da invocao destas funes podem ocorrer dois tipos de erros. Erros de domnio, quando o valor do argumento est fora da gama permitida. Erros de contradomnio (ERANGE), quando o valor devolvido no pode ser representado por uma varivel de tipo double. Quando tal acontece, a varivel global errno assume o valor EDOM ou ERANGE. A norma IEEE 754, usada na caracterizao dos tipos inteiros no computador utilizado na disciplina, permite a deteco destas situaes por inspeco do valor devolvido. Se o erro for EDOM e o valor devolvido for nan, ento isso significa que no um nmero. Se o erro for ERANGE podemos ter os seguintes valores devolvidos: inf que significa que um nmero positivo muito grande; -inf que significa que um nmero negativo muito grande; e 0 que significa que um nmero muito pequeno. Durante a compilao de um programa que referencia a biblioteca math.h, para que o processo de ligao funcione, a biblioteca tem que ser explicitamente referenciada no comando de compilao. Para tal, preciso acrescentar ao comando de compilao a opo de compilao -lm, da seguinte forma. cc programa.c -o programa ... -lm As funes da biblioteca matemtica dividem-se funcionalmente em trs grupos: funes trigonomtricas e hiperblicas; funes exponenciais e logartmicas; e funes diversas.

47

CAPTULO 1 : INTRODUO AO C

A Figura 1.73 apresenta as funes trigonomtricas e hiperblicas.


Nome da funo Significado double acos (double x); x [-1, 1] arco coseno(x) [0,S] double asin (double x); x [-1, 1] arco seno(x) [-S/2,S/2] double atan (double x); arco tangente(x) [-S/2,S/2] double atan2 (double y, double x); xz0 ou yz0 arco tangente(y/x) ]-S,S] double cos (double x); coseno(x) double sin (double x); seno(x) double tan (double x); tangente(x) double cosh (double x); (ex+e-x)/2 double sinh (double x); (ex-e-x)/2 x double tanh (double x); (e -e-x)/(ex+e-x)

Figura 1.73 - Funes trigonomtricas e hiperblicas.

A Figura 1.74 apresenta as funes exponenciais e logartmicas.


Nome da funo double exp (double x); double frexp (double x, int *y); double double double double double double Significado ex x = frexp (x,y) * 2*y, com frexp (x,y) ]0.5,1] ldexp (double x, int y); x * 2y log (double x); loge(x), com x>0 log10 (double x); log10(x), com x>0 modf (double x, double *y); x = modf (x,y) + *y, com |modf (x,y)| [0,1[ pow (double x, double y); xy, com xz0 ou y>0 e xt0 ou ento y no tem parte fraccionria sqrt (double x); x

Figura 1.74 - Funes exponenciais e logartmicas.

A Figura 1.75 apresenta as funes diversas.


Nome da funo Significado double ceil (double x); truncatura por excesso double fabs (double x); |x| double floor (double x); truncatura por defeito double fmod (double x, double y); resto da diviso de x por y (yz0)

Figura 1.75 - Funes diversas.

1.9.3 Biblioteca errno


Algumas funes da biblioteca de execuo ANSI, particularmente as funes matemticas, para alm de devolverem um valor especfico em situaes de erro, afectam uma varivel global de tipo int, designada por errno, com um cdigo de erro. A biblioteca errno contm a aluso varivel errno e a definio dos cdigos de erro. Para utilizar as suas funes preciso fazer a incluso do ficheiro de interface errno.h com a seguinte directiva do prprocessador. #include <errno.h>

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

48

A sua aplicao prtica vem muitas vezes associada com o envio da mensagem de erro correspondente para o dispositivo convencional de sada de erro, que normalmente o mesmo que o dispositivo convencional de sada, ou seja, o monitor. Isto feito usando a funo perror, descrita no ficheiro de interface stdio.h, que imprime no dispositivo convencional de sada de erro uma combinao de duas mensagens, separadas pelo carcter :. mensagem definida pelo programador : mensagem associada ao valor de errno A Figura 1.76 apresenta um exemplo da utilizao da funo perror.
#include <stdio.h> #include <errno.h> ... errno = 0; Z = log (X); if (errno != 0) /* ocorreu uma situao de erro */ { perror ("erro no clculo de um logaritmo na rotina ..."); ... } ...

Figura 1.76 - Exemplo da utilizao da funo perror.

1.9.4 Biblioteca stdlib


A biblioteca stdlib contm funes de utilidade geral e define quatro tipos de dados. Para utilizar as suas funes preciso fazer a incluso do ficheiro de interface stdlib.h com a seguinte directiva do pr-processador. #include <stdlib.h> As funes dividem-se funcionalmente em vrios grupos. As funes para gerao de sequncias pseudo-aleatrias, que permitem fazer uma aproximao gerao de uma sequncia de valores que segue uma funo densidade de probabilidade uniformemente distribuda. As funes para realizao de operaes inteiras elementares, como a obteno do valor absoluto e do quociente e do resto de divises inteiras. As funes para converso de cadeias de caracteres em quantidades numricas. A Figura 1.77 apresenta as funes para gerao de sequncias pseudo-aleatrias.
Nome da funo int rand (void); void srand (unsigned int s); sucessivas invocaes de rand() pseudo-aleatrios determinada por rand() aps a produzem s, sendo Significado {0, 1, ... , RAND_MAX} invocao desta funo, uma sequncia de valores s a semente de gerao

Figura 1.77 - Funes para gerao de sequncias pseudo-aleatrias.

49

CAPTULO 1 : INTRODUO AO C

A Figura 1.78 apresenta as funes para realizao de operaes inteiras elementares.


Nome da funo int abs (int x); long labs (long x); div_t idiv (int x, int y); ldiv_t ldiv (long x, long y); Significado |x| |x| quociente e resto da diviso inteira de x por y quociente e resto da diviso inteira de x por y

Figura 1.78 - Funes para realizao de operaes inteiras elementares.

Para as duas ltimas funes, os valores do quociente e do resto da diviso de operandos de tipo int ou de tipo long so devolvidos conjuntamente numa varivel de tipo complexo, div_t ou ldiv_t, assim definida para o primeiro caso.
typedef struct { int quot; int rem; } div_t;

/* quociente */ /* resto */

A definio do tipo ldiv_t formalmente semelhante, substitui-se apenas int por long em todas as ocorrncias. A Figura 1.79 apresenta as funes para converso de cadeias de caracteres em quantidades numricas.
Nome da funo Significado double atof (const char *str); converte a cadeia de caracteres apontada por str numa quantidade real de tipo double int atoi (const char *str); converte a cadeia de caracteres apontada por str numa quantidade inteira de tipo int

Figura 1.79 - Funes para converso de cadeias de caracteres em quantidades numricas.

Para alm dos tipos div_t e ldiv_t, a biblioteca stdlib define ainda o tipo size_t que o tipo do resultado do operador sizeof. Contm tambm dois identificadores que podem ser usados pela funo exit para indicar o estado de execuo de um programa. A constante EXIT_FAILURE, que vale 1, pode ser usada para indicar a finalizao do programa sem sucesso. A constante EXIT_SUCCESS, que vale 0, pode ser usada para indicar a finalizao do programa com sucesso. Esta biblioteca contm ainda as funes para gesto de memria, que permitem adjudicar e libertar memria dinmica, que sero apresentadas mais tarde.

1.10 Leituras recomendadas


x 3, 4, 5 e 6 captulos do livro C A Software Approach, 3 edio, de Peter A. Darnell e Philip E. Margolis, da editora Springer-Verlag, 1996.

Captulo 2
COMPLEMENTOS SOBRE C

Sumrio
Este captulo dedicado aos aspectos mais avanados da linguagem C. Comeamos por apresentar o conceito de funo generalizada, que o nico tipo de subprograma existente e como atravs dela se implementam funes e procedimentos. Explicamos a passagem de parmetros s funes e introduzimos os primeiros conceitos sobre ponteiros. De seguida, apresentamos o modo como se definem os tipos de dados estruturados, nomeadamente, os agregados unidimensionais, bidimensionais e tridimensionais, as cadeias de caracteres e a biblioteca string que providencia funes para a sua manipulao e os registos, que na linguagem C se designam por estruturas. Apresentamos tambm como se definem tipos de dados enumerados. Devido interaco entre os agregados e os ponteiros, o que se costuma designar por dualidade ponteiro agregado, e necessidade de implementar programas mais eficientes recorrendo a este novo tipo de dados, vamos expondo as suas caractersticas mais avanadas medida que explicamos os tipos de dados estruturados. Apresentamos, designadamente, os seus operadores especficos, a aritmtica de ponteiros, a dualidade ponteiro agregado, para agregados unidimensionais e multidimensionais e a construo de estruturas de dados versteis envolvendo agregados e ponteiros. Apresentamos tambm as classes de armazenamento das variveis e redefinimos os conceitos de objectos locais e globais, bem como dos nveis de visibilidade dos objectos aplicados ao facto da linguagem C permitir a construo de aplicaes distribudas por vrios ficheiros fonte.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

2.1 Funes
No estabelecimento de solues de problemas complexos essencial controlar o grau de complexidade da descrio. A descrio deve ser organizada de uma forma hierarquizada, tambm designada de decomposio do topo para a base (Top-Down Decomposition), de modo a que corresponda, a cada nvel de abstraco considerado, uma decomposio num nmero limitado de operaes, com recurso a um nmero tambm limitado de variveis. S assim possvel: x Minimizar a interaco entre as diferentes operaes, impondo regras estritas na formulao das dependncias de informao. x Desenvolver metodologias que, de uma forma simples, conduzam demonstrao da correco dos algoritmos estabelecidos. x Conceber um desenho da soluo que: x Promova a compactao atravs da definio de operaes reutilizveis em diferentes contextos. x Enquadre a mudana ao longo do tempo, possibilitando a alterao de especificaes com um mnimo de esforo. A transcrio da soluo numa linguagem de programao deve, depois, procurar reflectir a hierarquia de decomposio que foi explicitada na descrio. Isto consegue-se fazendo um uso intensivo dos mecanismos de encapsulamento de informao, presentes na linguagem utilizada, para implementar cada operao como uma seco autnoma de cdigo, ou seja, com um subprograma e promover, assim, uma visibilidade controlada dos detalhes de implementao. Desta forma, possvel: x Separar a especificao da operao, ou seja, a descrio da sua funcionalidade interna e do mecanismo de comunicao com o exterior, constituda pelas variveis de entrada e de sada, da sua implementao. x Introduzir caractersticas de robustez e de desenho para o teste no cdigo produzido. x Validar de uma maneira controlada e integrar de um modo progressivo os diferentes componentes da aplicao x Planear com eficcia a distribuio de tarefas pelos diversos membros da equipa de trabalho. Na linguagem Pascal existem dois tipos de subprogramas. O procedimento, que se apresenta na Figura 2.1, um mecanismo de encapsulamento de informao que permite a construo de operaes mais complexas a partir de uma combinao de operaes mais simples. Aps a sua definio, a nova operao identificada por um nome e por uma lista opcional de parmetros de comunicao. Os parmetros de entrada, que representam valores necessrios realizao da operao, e os parmetros de sada, que representam valores produzidos pela realizao da operao.

CAPTULO 2 : COMPLEMENTOS SOBRE C

Parmetros de Entrada (0)

Procedimento

Parmetros de Sada (0)

Figura 2.1 - Esquema de um procedimento.

A funo, que se apresenta na Figura 2.2, um mecanismo de encapsulamento de informao que permite a descrio de um processo de clculo complexo a partir de uma combinao de operaes mais simples. Aps a sua definio, a nova operao identificada por um nome e por uma lista opcional de parmetros de comunicao. Os parmetros de entrada, que representam valores necessrios realizao da operao e pela indicao do tipo do resultado de sada, ou seja, do valor calculado e devolvido pela funo.

Parmetros de Entrada (0)

Funo

Resultado de Sada

Figura 2.2 - Esquema de uma funo.

Na linguagem C s temos um tipo de subprograma que a funo generalizada, que se apresenta na Figura 2.3. A funo generalizada um mecanismo de encapsulamento de informao que combina as caractersticas apresentadas pelo procedimento e pela funo. Semanticamente, pode ser encarada como um procedimento que devolve o estado de realizao da operao. Pode ser visto como uma caixa preta que recebe informao entrada e que produz informao sada, sendo que os detalhes associados com a implementao da operao so invisveis externamente e no originam qualquer interaco com o exterior. A informao de entrada, quando necessria realizao da operao, fornecida funo generalizada atravs de um conjunto de parmetros de entrada. A informao de sada, quando produzida pela realizao da operao, fornecida atravs de um conjunto de parmetros de sada e/ou pela produo de um resultado de sada.

Parmetros de Entrada (0)

Funo Generalizada

Parmetros de Sada (0) Resultado de Sada

Figura 2.3 - Esquema de uma funo generalizada da linguagem C.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

Aps a sua definio, a nova operao identificada por um nome, por uma lista opcional de parmetros de comunicao, ou seja, os parmetros de entrada, os parmetros de sada, e pela indicao do tipo do valor devolvido. A Figura 2.4 apresenta a definio da funo de converso de distncias de milhas para quilmetros no Pascal e na linguagem C.
function CONVERTE_DISTANCIA (ML: real): real; (* no Pascal *) const MIL_QUI = 1.609; begin CONVERTE_DISTANCIA := MIL_QUI * ML end; #define MIL_QUI 1.609 /* na linguagem C */ ... double CONVERTE_DISTANCIA (double ML) /* definio da funo */ { return ML * MIL_QUI; }

Figura 2.4 - Exemplo da funo da converso de distncias de milhas para quilmetros.

Uma funo pode aparecer num programa, de trs formas diferentes: x A definio a declarao que define o que a funo faz, bem como o nmero e tipo de parmetros e o tipo de resultado de sada. Normalmente colocada depois da funo main, ou num ficheiro de implementao. x A invocao invoca a execuo da funo. x A aluso, ou prottipo a declarao que define a interface da funo, ou seja, o tipo, o nome e a lista de parmetros. Normalmente colocada no incio do ficheiro fonte logo aps as directivas de include, ou num ficheiro de interface, que tem a extenso .h.

2.1.1 Definio de uma funo


Tal como j foi mostrado para a funo main na Figura 1.3, qualquer funo na linguagem C, supe a especificao do seu cabealho e do seu corpo. A Figura 2.5 apresenta detalhadamente os vrios componentes da funo CONVERTE_DISTANCIA. No cabealho, indica-se o tipo do valor devolvido, o nome da funo, e entre parnteses curvos, a lista de parmetros de comunicao. Se a funo no tiver lista de parmetros utilizada a declarao de void. Se a funo no tiver resultado de sada utilizada a declarao de tipo void, como sendo o tipo do valor devolvido. O corpo da funo delimitado pelos separadores { e }. Para devolver o valor calculado pela funo, utiliza-se a palavra reservada return seguido da expresso a devolver. O valor de retorno pode ser de qualquer tipo, excepto um agregado ou uma funo. possvel devolver uma estrutura ou unio, mas no recomendado porque ineficiente. Uma funo pode conter qualquer nmero de instrues return. Se no houver instruo de return, a execuo da funo termina quando atingido o separador }. Nesse caso, o valor devolvido indefinido. O valor devolvido na instruo de return deve ser compatvel em termos de atribuio com o tipo de sada da funo. Para determinar se o valor devolvido aceitvel, o compilador usa as mesmas regras de compatibilidade da instruo de atribuio.

CAPTULO 2 : COMPLEMENTOS SOBRE C

tipo de sada

nome da funo

lista de parmetros cabealho

double CONVERTE_DISTANCIA (double ML) { return } ML * MIL_QUI; corpo

Figura 2.5 - A funo CONVERTE_DISTANCIA.

Normalmente os parmetros de uma funo so parmetros de entrada e como tal so passados por valor. Ou seja, a funo no altera o valor da varivel passada funo. A passagem por valor consiste em referir apenas a varivel, ou a expresso, na invocao da funo. Mas, por vezes necessrio usar parmetros de entrada-sada numa funo. Ou seja, o valor da varivel necessita de ser alterada durante a execuo da funo. Nesses casos estamos perante a passagem de parmetros por referncia, tambm designada por endereo. Nesta situao passado o endereo, ou seja, a localizao da varivel na memria. Isso feito usando um ponteiro para a varivel na invocao da funo.

2.1.2 Introduo aos ponteiros


Uma caracterstica saliente da linguagem C permitir referenciar de um modo muito simples a localizao de variveis em memria e, a partir dessa localizao, aceder ao seu contedo. Para isso existem os dois operadores unrios apresentados na Figura 2.6 que so aplicados directamente a uma varivel. localizao em memria da varivel ::= & identificador de varivel referncia indirecta varivel ::= * identificador de varivel de tipo ponteiro
Figura 2.6 - Definio formal dos operadores para manipulao de variveis atravs de ponteiros.

O operador endereo, cujo smbolo o &, d a localizao em memria da varivel. localizao de uma varivel chama-se habitualmente ponteiro para a varivel, j que o valor que lhe est associado aponta para, ou referencia, a regio de memria onde essa varivel est armazenada. Mais especificamente, o valor que est associado a um ponteiro representa o endereo do primeiro byte da regio de armazenamento da varivel. Assim, um ponteiro, embora assuma o valor de um endereo, no propriamente um endereo. O que est subjacente ao conceito no a localizao de um byte, que define a diviso fsica da memria, mas de uma varivel, ou seja, da diviso lgica da memria. Portanto, no faz sentido falar-se em ponteiros em abstracto, porque um ponteiro est sempre associado a um tipo de dados bem definido. Como nos processadores actuais, o barramento de endereos tem uma dimenso de 32 bits, o tamanho do formato do tipo ponteiro igual a 4 bytes. Por isso, tem-se que sizeof (ponteiro para um tipo de dados genrico) = 4. O operador apontado por, cujo smbolo o *, d acesso varivel, cuja localizao em memria est armazenada numa varivel de tipo ponteiro, ou seja, uma referncia indirecta varivel. Este operador tem outro significado quando usado na declarao de

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

uma varivel. Nessa situao indica que a varivel de tipo ponteiro. E quando utilizado na lista de parmetros, precedendo um parmetro, indica que o parmetro um parmetro de entrada-sada, ou seja, que uma passagem por referncia. A Figura 2.7 apresenta um exemplo da manipulao de uma varivel atravs de um ponteiro. A primeira linha de cdigo declara uma varivel A de tipo int e uma varivel PA de tipo ponteiro para int. A atribuio PA = &A, coloca na varivel PA o endereo da varivel A, da que, a varivel PA fica a apontar para a varivel A. A atribuio *PA = 23 coloca o valor 23 na varivel A, atravs do ponteiro PA.
int A, *PA; ... PA = &A; *PA = 23;

A PA

23

Figura 2.7 - Exemplo de manipulao de uma varivel atravs de um ponteiro.

2.1.3 Invocao de uma funo


Aquando da invocao de uma funo, o controlo do programa passa para a funo. A invocao de uma funo uma expresso e como tal pode aparecer em qualquer stio onde possa aparecer uma expresso. A funo devolve sempre um valor, a no ser quando o valor devolvido void. No entanto, possvel invocar uma funo sem utilizar o valor devolvido. A Figura 2.8 apresenta a invocao da funo CONVERTE_DISTANCIA. Podemos invocar a funo numa expresso de atribuio do seu valor varivel QUILOMETROS, ou em alternativa podemos invoc-la directamente na instruo de sada de dados printf. A definio da funo deve ser colocada aps a funo main.
#include <stdio.h> /* interface com a biblioteca de entrada/sada */ #define MIL_QUI 1.609 int main ( void ) { double MILHAS, QUILOMETROS; do { /* definio do factor de converso */ /* aluso funo */ double CONVERTE_DISTANCIA (double);

/* distncia expressa em milhas */ /* distncia expressa em quilmetros */

/* leitura com validao da distncia expressa em milhas */

printf ("Distncia em milhas? "); scanf ("%lf", &MILHAS); } while (MILHAS < 0.0); /* invocao da funo */ QUILOMETROS = CONVERTE_DISTANCIA (MILHAS); /* impresso da distncia expressa em quilmetros */ printf ("Distncia em quilmetros %8.3f\n", QUILOMETROS); return 0; } ...

Figura 2.8 - Programa da converso de distncias utilizando a funo CONVERTE_DISTANCIA.

CAPTULO 2 : COMPLEMENTOS SOBRE C

2.1.4 Aluso de uma funo


A aluso de uma funo, permite ao compilador assegurar que o nmero correcto de parmetros so passados na invocao. Probe a passagem de parmetros que no possam ser convertidos para o tipo correcto. Por outro lado, converte pacificamente os parmetros quando isso possvel. Permite tambm verificar se a atribuio do resultado da funo est de acordo com o tipo do valor devolvido pela funo. O cdigo apresentado na Figura 2.8 faz a aluso funo CONVERTE_DISTANCIA na terceira linha. Na lista de parmetros indica-se apenas o tipo dos parmetros e omite-se os seus nomes, uma vez que o compilador necessita apenas de saber o tipo dos parmetros, para verificar se a passagem de parmetros est correcta quando se invoca a funo.

2.1.5 Procedimentos na linguagem C


Na linguagem C um procedimento implementado por uma funo que no devolve qualquer valor, ou seja, cujo tipo do resultado de sada de tipo void. No corpo de uma funo que devolve o tipo void, no necessrio utilizar a instruo return. A no ser que, por razes de algoritmo haja a necessidade de abandonar a funo em stios diferentes. Nesse caso usa-se a instruo return sem expresso. A Figura 2.9 apresenta o procedimento TROCA em Pascal e a sua implementao com uma funo na linguagem C.
procedure TROCA (var X, Y: integer); (* no Pascal *) var TEMP : integer; begin TEMP := X; X := Y; Y := TEMP end; void TROCA (int *X, int *Y) /* na linguagem C */ { int TEMP; TEMP = *X; *X = *Y; *Y = TEMP; }

Figura 2.9 - Procedimento na linguagem C.

Na definio da funo os dois parmetros X e Y so parmetros de entrada-sada, pelo que, so passados por referncia atravs de ponteiros para as variveis. Para indicar que o parmetro um parmetro de entrada-sada, coloca-se o operador apontado por atrs do parmetro. Na invocao da funo necessrio passar a localizao em memria das variveis actuais, atravs de ponteiros para as variveis. Para passar a localizao da varivel, coloca-se o operador endereo atrs da varivel, tal como se mostra na Figura 2.10.
void TROCA (int *X, int *Y); ... int A, B; ... TROCA (&A, &B); /* aluso funo */ /* definio das variveis */ /* invocao da funo */

Figura 2.10 - Exemplo da invocao da funo TROCA.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

2.2 Agregados ( arrays )


O construtor agregado ( array ) um mecanismo usado por muitas linguagens de programao para criar tipos de dados estruturados, ou seja, tipos de dados mais complexos a partir de tipos de dados mais simples, designados de tipo base. O tipo agregado concebido como um conjunto ordenado de objectos do tipo base. Cada objecto do tipo base, designado genericamente por elemento, referenciado pela posio que ocupa no agregado. Estamos perante um acesso por indexao. A Figura 2.11 apresenta graficamente um agregado unidimensional com 11 elementos e um agregado bidimensional com 15 elementos.
Agregado unidimensional com 11 elementos do tipo base, com o 3 elemento marcado.

tipo base

Agregado bidimensional com 15 elementos do tipo base, com o elemento da 4 coluna da 3 linha marcado.

Figura 2.11 - Visualizao grfica de agregados unidimensionais e bidimensionais.

A declarao de variveis de tipo agregado segue a regra geral de declarao de variveis da linguagem C e a sua definio formal apresentada na Figura 2.12.

tipo de dados lista de variveis de tipo agregado ;


tipo de dados ::= qualquer tipo de dados vlido na linguagem C lista de variveis de tipo agregado ::= identificador de varivel de tipo agregado | lista de variveis de tipo agregado , identificador de varivel de tipo agregado identificador de varivel de tipo agregado ::= identificador de varivel descritor de dimenso | identificador de varivel descritor de dimenso = expresso de inicializao identificador de varivel ::= qualquer identificador vlido na linguagem C descritor de dimenso ::= [ ] | [ constante numrica inteira positiva ] | descritor de dimenso [ ] | descritor de dimenso [ constante numrica inteira positiva ] expresso de inicializao ::= { listas de constantes do tipo base }
Figura 2.12 - Definio formal da declarao de um tipo agregado.

A Figura 2.13, a Figura 2.41 e a Figura 2.42 apresentam respectivamente exemplos da declarao de agregados unidimensionais, bidimensionais e tridimensionais, bem como da sua colocao na memria. Uma varivel de tipo agregado distingue-se de uma varivel simples, devido aos descritores de dimenso, ou seja, aos parnteses rectos a seguir ao nome da varivel. A constante numrica inteira positiva, includa no descritor de dimenso, indica o tamanho da dimenso. Se se tratar de um agregado unidimensional, isto equivalente a indicar o nmero de elementos do agregado. Para um agregado multidimensional, o nmero de elementos do agregado calculado pelo produto dos tamanhos das diferentes dimenses.

CAPTULO 2 : COMPLEMENTOS SOBRE C

Quando o descritor de dimenso no inclui a constante numrica positiva, diz-se que se est perante uma definio incompleta, j que a reserva de espao de armazenamento no feita. O seu uso , por isso, bastante restrito e geralmente usado quando se faz a inicializao no momento da declarao. A inicializao feita atravs de listas de constantes do tipo base, separadas pela vrgula e inseridas entre chavetas. O nmero de listas de inicializao e de nveis de chavetas depende do nmero de dimenses do agregado. Um elemento particular do agregado referenciado atravs da indicao do nome da varivel, seguido do valor de um ou mais ndices, tantos quantas as dimenses do agregado, colocados individualmente entre parnteses rectos. Na linguagem C, os ndices so variveis numricas inteiras positivas e o ndice do elemento localizado mais esquerda em cada dimenso o ndice zero.

2.2.1 Agregados unidimensionais


A Figura 2.13 apresenta exemplos da declarao e inicializao de agregados unidimensionais, bem como, da sua colocao na memria.
int A[4] = {10, 21, -5, 13}; double B[3] = {10.2, 11.8}; int C[] = {22, 12}; A[0] A[1] A[2] A[3] B[0] B[1] B[2] C[0] C[1] 10 21 -5 13 10.2 11.8 0.0 22 12

Figura 2.13 - Declarao e inicializao de agregados unidimensionais.

A expresso de inicializao para variveis de tipo agregado unidimensional de um tipo base consiste numa lista de constantes do tipo base, separadas pela vrgula e colocadas entre chavetas. A atribuio das constantes aos elementos do agregado processa-se da esquerda para a direita e realizada at ao esgotamento da lista. Assim, a constante numrica positiva, includa no descritor de dimenso, ter que ser maior, ou quando muito igual, ao nmero de constantes da lista. Se for igual, como o caso do agregado A, todos os elementos do agregado sero inicializados com os valores indicados. Se for maior, como o caso do agregado B, s os K primeiros elementos, em que K igual ao nmero de constantes da lista, sero inicializados com os valores indicados, sendo que os restantes elementos so inicializados a zero. Alternativamente, como o caso do agregado C, o descritor de dimenso pode ser omitido e ento ser reservado espao, e sero inicializados, tantos elementos quanto o nmero de constantes da lista de inicializao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

2.2.2 Dualidade ponteiro agregado


Em geral, quando numa instruo se referencia o nome de uma varivel, pretende-se aludir como um todo regio de memria que est reservada para armazenamento dos seus valores. Tal no se passa, contudo, quando se referencia o nome de uma varivel de tipo agregado unidimensional. O significado atribudo , neste caso, o de um ponteiro para o primeiro elemento do agregado. TIPO_BASE A[N]; A { &A[0] Na linguagem C existe aquilo que se costuma designar por dualidade ponteiro agregado, j que, quer uma varivel de tipo agregado unidimensional de um dado tipo base, quer uma varivel de tipo ponteiro para o mesmo tipo base, so formas equivalentes de referenciar uma regio de memria formada por um conjunto contguo de variveis do tipo base. Consequentemente, possvel aceder a um elemento do agregado unidimensional, quer da forma usual em Pascal, quer atravs do operador apontado por. A+i { &A[i] *(A+i) { *&A[i] { A[i], com 0 d i < N Ou seja, a localizao do elemento de ndice i do agregado A pode ser alternativamente expressa por &A[i] ou por A+i, e o valor do mesmo elemento por A[i] ou por *(A+i). Por conseguinte, uma varivel de tipo ponteiro para um tipo base pode ser encarada como uma varivel de tipo agregado unidimensional do mesmo tipo base na referncia a uma regio de memria formada por um conjunto contguo de variveis do tipo base. Pelo que, a seguinte declarao do agregado A e do ponteiro para o agregado PA inicializado com o endereo inicial do agregado, resulta no mapa de reserva de espao em memria que se apresenta na Figura 2.14 e permite a utilizao do ponteiro PA para aceder aos elementos do agregado. TIPO_BASE A[N], *PA = A; PA+i { &PA[i] = &A[i] *(PA+i) { PA[i] = A[i], com 0 d i < N
TIPO_BASE A[8], *PA = A; A[0] A[1] A[2] A[3] A[4] A[5] A[6] A[7] PA A

Figura 2.14 - Declarao de um agregado unidimensional e de um ponteiro para o agregado.

Existe, porm, uma diferena subtil entre as variveis de tipo agregado unidimensional de um tipo base e as variveis de tipo ponteiro para o mesmo tipo base. Como se verifica do mapa de reserva de espao em memria apresentado na Figura 2.14, no existe espao directamente associado com a varivel A e, portanto, A um ponteiro constante, cujo valor no pode ser modificado. Assim, instrues do tipo A = expresso; so ilegais.

11

CAPTULO 2 : COMPLEMENTOS SOBRE C

2.2.3 Agregados como parmetros de funes


Na linguagem C, dentro de uma funo no possvel saber com quantos elementos foi declarado um agregado que foi passado como argumento para essa funo. Pelo que, para alm do agregado deve ser passado o nmero de elementos a processar. A Figura 2.15 apresenta o cdigo da definio de uma funo que inicializa a zero todos os elementos de um agregado. A funo tem um parmetro de entrada-sada que o agregado a inicializar e um parmetro de entrada que a dimenso do agregado. Para indicar que o parmetro de entrada-sada um agregado, usa-se o nome do parmetro formal seguido dos parnteses rectos. Quando da invocao da funo, o endereo inicial do agregado passado funo usando o nome do agregado.
void INICIALIZAR (int A[], int N) /* definio da funo */ { int I; for (I = 0; I < N; I++) A[I] = 0; } int X[10]; ... INICIALIZAR (X, 10); /* invocao da funo */

Figura 2.15 - Passagem de um agregado a uma funo.

possvel obter o nmero de elementos de um agregado dividindo o tamanho em bytes do agregado pelo tamanho em bytes de um dos seus elementos, ou seja, pelo tamanho do tipo base, utilizando para tal a seguinte expresso envolvendo o operador sizeof. sizeof ( agregado ) / sizeof ( elemento do agregado ) A expresso funciona independentemente do tipo de elementos do agregado. A Figura 2.16 apresenta uma aplicao desta expresso. O agregado C declarado sem descritor de dimenso, sendo no entanto inicializado. Na altura da invocao da funo FUNC usamos a expresso para passar o nmero de elementos do agregado ao parmetro de entrada N.
void FUNC (int A[], int N); /* aluso da funo */ ... int C[] = {20, 21, 22, 23}; ... /* invocao da funo FUNC para o agregado C */ FUNC (C, sizeof (C) / sizeof (C[0]));

Figura 2.16 - Utilizao do operador sizeof para calcular o nmero de elementos de um agregado.

Um dos erros mais frequentemente cometido, por programadores que se esto a iniciar na utilizao da linguagem C, ultrapassar a dimenso de um agregado, nomeadamente em ciclos repetitivos for. No exemplo apresentado na Figura 2.17, o for vai processar o agregado desde o elemento de ndice 0 at ao elemento de ndice 10, que no existe no agregado. Acontece que nesta posio de memria que est armazenada a varivel I que controla o for, pelo que, o seu valor vai ser reinicializado a zero e portanto o for de novo repetido. Estamos perante um ciclo repetitivo infinito criado por engano.
int I, AR[10]; ... for (I = 0; I <= 10; I++) AR[I] = 0;

/* situao de erro */

Figura 2.17 - Situao de erro na utilizao do ciclo for para processar um agregado.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

2.3 Cadeias de caracteres ( strings )


Na linguagem C, uma cadeia de caracteres ( string ) entendida como um agregado unidimensional de caracteres terminado obrigatoriamente pelo carcter nulo '\0', tambm reconhecido pelo identificador NUL. A expresso de inicializao para variveis de tipo cadeia de caracteres, pode por isso ser estabelecida alternativamente segundo as regras anteriores de inicializao de agregados unidimensionais, tendo o cuidado de garantir que a lista de constantes inclui em ltimo lugar o carcter nulo, ou atravs de uma constante de tipo cadeia de caracteres, ou seja, uma sequncia de caracteres delimitada por aspas duplas, onde o compilador coloca automaticamente no fim o carcter nulo. Quando se declara e inicializa uma cadeia de caracteres e o descritor de dimenso maior do que o nmero de caracteres de inicializao, ento todos os restantes caracteres da cadeia so inicializados com o carcter nulo. A Figura 2.18 apresenta alguns exemplos de declarao e inicializao de cadeias de caracteres. A varivel VOGAIS no uma cadeia de caracteres porque no foi terminada com o carcter nulo e foi declarada para armazenar apenas as cinco vogais. Pelo que, apenas um agregado de caracteres. A varivel CIDADE tambm est declarada como um agregado de caracteres, mas, como foi declarado com o tamanho de 10 caracteres, ento todos os caracteres no inicializados, que so quatro, so colocados a '\0'. Pelo que, de facto uma cadeia de caracteres. A varivel OLA uma cadeia de caracteres e est explicitamente declarada com o carcter nulo. A varivel CARTA no uma cadeia de caracteres, porque apesar de ser inicializada com uma constante de tipo cadeia de caracteres, foi declarada com um tamanho insuficiente para armazenar o carcter nulo. A varivel TEXTO est declarada sem especificao de tamanho e portanto, criado com a dimenso exacta para conter a cadeia de caracteres de inicializao mais o carcter nulo, ou seja, com a dimenso de 16 caracteres.
char VOGAIS[5] = {'a', 'e', 'i', 'o', 'u'}; /* VOGAIS no uma cadeia de caracteres */ char CIDADE[10] = {'A', 'v', 'e', 'i', 'r', 'o'}; /* CIDADE uma cadeia de caracteres */ char OLA[4] = {'o', 'l', 'a', '\0'}; /* OLA uma cadeia de caracteres */ char CARTA[4] = "tres"; /* CARTA no uma cadeia de caracteres */ char TEXTO[] = "era uma vez ..."; /* TEXTO uma cadeia de caracteres */

Figura 2.18 - Exemplos de declarao e inicializao de cadeias de caracteres.

A Figura 2.19 apresenta o mapa de reserva de espao em memria da declarao de duas cadeias de caracteres. A varivel OLA foi declarada e inicializada com quatro caracteres. A varivel TB inicializada com uma constante de 8 caracteres e o carcter nulo automaticamente colocado no fim.

13

CAPTULO 2 : COMPLEMENTOS SOBRE C

char OLA[4] = {'o', 'l', 'a', '\0'}; char TB[] = "tudo bem";

OLA[0] OLA[1] OLA[2] OLA[3] TB[0] TB[1] TB[2] TB[3] TB[4] TB[5] TB[6] TB[7] TB[8]

'o' 'l' 'a' '\0' 't' 'u' 'd' 'o' ' ' 'b' 'e' 'm' '\0'

Figura 2.19 - Mapa de reserva de espao em memria de duas cadeias de caracteres.

possvel criar um ponteiro para uma cadeia de caracteres e simultaneamente inici-lo com uma cadeia de caracteres constante, usando para esse efeito a seguinte instruo. char *ponteiro = "constante cadeia de caracteres" A Figura 2.20 apresenta o mapa de reserva de espao em memria de um exemplo deste tipo de declarao. O ponteiro PTS aponta para a cadeia de caracteres Aveiro, que uma cadeia de caracteres constante, pelo que, no pode ser alterada. O valor de PTS pode ser alterado, por exemplo, atribuindo-lhe outra constante, ou uma varivel, de tipo cadeia de caracteres, e caso o seja, perde-se o acesso cadeia de caracteres Aveiro.
char *PTS = "Aveiro"; 'A' 'v' 'e' 'i' 'r' 'o' '\0'

PTS

4 bytes

Figura 2.20 - Mapa de reserva de espao em memria de um ponteiro para uma cadeia de caracteres inicializado com uma cadeia de caracteres constante.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

2.3.1 Atribuio de cadeias de caracteres


Uma cadeia de caracteres um agregado de caracteres, portanto, uma constante de tipo cadeia de caracteres tambm um ponteiro para o seu primeiro carcter. Pelo que, quando uma varivel de tipo cadeia de caracteres declarada com o operador [ ], no possvel atribuir-lhe directamente uma constante de tipo cadeia de caracteres, uma vez que o nome da varivel cadeia de caracteres um ponteiro constante, logo no pode ser alterado. Mas, j possvel, atribuir uma constante de tipo carcter a um elemento da varivel, indicando para esse efeito o ndice do elemento entre parnteses rectos. Para atribuir a uma varivel de tipo cadeia de caracteres, uma constante de tipo cadeia de caracteres ou outra varivel de tipo cadeia de caracteres, existe a funo strcpy da biblioteca de execuo ANSI string. Mas j possvel atribuir a uma varivel de tipo ponteiro para char uma constante de tipo cadeia de caracteres ou outra varivel de tipo cadeia de caracteres. A Figura 2.21 apresenta um exemplo que mostra esta diferena.
char CIDADE[10] = "aveiro"; /* CIDADE igual a "aveiro" char *PTS = "Aveiro"; /* PTS aponta para "Aveiro" ... CIDADE = "Lisboa"; /* ERRO no se pode fazer esta atribuio CIDADE[0] = 'A'; /* agora CIDADE igual a "Aveiro" strcpy (CIDADE, "Lisboa"); /* agora CIDADE igual a "Lisboa" PTS = "Lisboa"; /* agora PTS aponta para "Lisboa" */ */ */ */ */ */

Figura 2.21 - Exemplo da atribuio de cadeias de caracteres.

2.3.2 Cadeias de caracteres versus caracteres


Uma constante de tipo carcter colocado entre pelicas, por exemplo 'a', enquanto que uma constante de tipo cadeia de caracteres colocada entre aspas, por exemplo "a". Existe uma diferena importante entre um carcter e uma cadeia de caracteres, mesmo quando esta apenas constituda por um nico carcter. Enquanto que um carcter ocupa apenas um byte, uma cadeia de caracteres ocupa tantos bytes quantos os caracteres, mais um byte para armazenar o carcter nulo '\0'. A Figura 2.22 apresenta alguns exemplos da utilizao de um ponteiro para carcter. Pode-se atribuir um carcter a um dos elementos de uma cadeia de caracteres atravs de um ponteiro para carcter, desde que a cadeia de caracteres no seja constante, como o caso do exemplo apresentado. Portanto, a instruo de atribuio *PTS = 'A' d erro de execuo, devido a violao de memria protegida. A instruo de atribuio *PTS = "A" est errada, uma vez que, no se pode atribuir um ponteiro para carcter a um carcter. preciso ter em ateno que *PTS do tipo char, enquanto que, "A" do tipo char *. Como j foi referido, possvel atribuir uma constante de tipo cadeia de caracteres a um ponteiro para carcter, mas no possvel atribuir-lhe um carcter, pelo que, a instruo de atribuio PTS = 'A' est errada.
char *PTS = "aveiro"; /* PTS aponta para "aveiro" */ ... *PTS = 'A'; /* ERRO "Aveiro" uma cadeia de caracteres constante */ *PTS = "A"; /* ERRO no se pode fazer esta atribuio */ PTS = 'A'; PTS = "A"; /* ERRO no se pode fazer esta atribuio */ /* agora PTS aponta para "A" */

Figura 2.22 - Exemplo da manipulao de um ponteiro para carcter.

15

CAPTULO 2 : COMPLEMENTOS SOBRE C

2.3.3 Cadeias de caracteres como parmetros de funes


A passagem de uma cadeia de caracteres a uma funo semelhante passagem de um agregado. Existem duas maneiras para indicar que um parmetro de uma funo uma cadeia de caracteres. A notao recomendada pela norma ANSI utiliza o operador [ ], declarando que o parmetro um agregado de caracteres. Em alternativa pode-se utilizar o operador *, declarando que o parmetro um ponteiro para carcter. Quando se passa uma cadeia de caracteres a uma funo, no necessrio indicar o comprimento da cadeia de caracteres, uma vez que este valor pode ser calculado recorrendo funo strlen da biblioteca de execuo ANSI string. A Figura 2.23 apresenta um exemplo da definio e invocao de uma funo que processa uma cadeia de caracteres.
int FUNC (char PST[]) /* definio da funo FUNC */ { /* ou em alternativa int FUNC (char *PST) */ ... return ...; } char ST[] = "bom dia"; ... /* invocao da funo FUNC para a cadeia de caracteres ST */ FUNC (ST);

Figura 2.23 - Funo com um parmetro de tipo cadeia de caracteres.

2.3.4 Codificao circular de caracteres


Na linguagem C, o tipo char permite armazenar quer um carcter quer um valor inteiro. Devido a esta polivalncia possvel misturar numa mesma expresso caracteres e valores numricos inteiros, sem ter a necessidade de recorrer a funes de converso de tipos. Pelo que, a expresso para implementar a codificao circular de um carcter alfabtico simplificada, quando comparada com o Pascal. A Figura 2.24 apresenta o deslocamento circular do carcter z trs posies para a frente, sendo transformado no carcter c. Para obtermos o efeito de deslocamento circular num conjunto linear, temos que utilizar o operador resto da diviso, que na linguagem C o smbolo %.
a + ( z - a + 3 ) % 26 = c

y x w

b c d

alfabeto minsculo

...
Figura 2.24 - Deslocamento circular de um carcter trs posies para a frente.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

A Figura 2.25 apresenta a funo que codifica um carcter minsculo. A funo no testa se o carcter ou no minsculo, pelo que, s deve ser invocada para caracteres minsculos. Se o valor do deslocamento circular, que representado pelo parmetro de entrada K, for positivo ento o carcter codificado K posies para a frente, se for negativo ento o carcter codificado K posies para a trs, e se K for zero o carcter sai inalterado. Apresentam-se as solues em Pascal e na linguagem C.
(* no Pascal *) function CODIFICAR_MINUSCULO (CAR: char; K: integer): char; var CAR_S : char; begin CAR_S := chr(ord('a') + (ord(CAR) - ord('a') + 26 + K) mod 26); CODIFICAR_MINUSCULO := CAR_S end; char CODIFICAR_MINUSCULO (char CAR, int K) /* na linguagem C */ { return 'a' + (CAR - 'a' + 26 + K ) % 26; }

Figura 2.25 - Funo que faz a codificao circular de um carcter minsculo.

2.3.5 Biblioteca string


A biblioteca string contm funes de manipulao de cadeias de caracteres. Para utilizar as suas funes preciso fazer a incluso do ficheiro de interface string.h com a seguinte directiva do pr-processador. #include <string.h> As funes dividem-se funcionalmente em cinco grupos: funes de cpia; funes de concatenao; funes de comparao; funes de busca; e funes diversas. A Figura 2.26 apresenta as funes de cpia.
void void char char *memcpy (void *zd, void *zp, size_t n); *memmove (void *zd, void *zp, size_t n); *strcpy (char *zd, char *zp); *strncpy (char *zd, char *zp, size_t n);

Figura 2.26 - Funes de cpia.

Todas as funes copiam byte a byte o contedo da regio de memria apontada por zp para a regio de memria apontada por zd e devolvem a localizao de zd. Est subjacente a qualquer das funes que zd referencia uma regio de memria, cuja reserva de espao de armazenamento foi previamente efectuada e que tem tamanho suficiente para que a transferncia seja efectivamente possvel. Para as funes memcpy e memmove, a transferncia realizada sem ser atribuda qualquer interpretao ao contedo dos bytes transferidos, enquanto que, para strcpy e strncpy, se supe que zp referencia uma cadeia de caracteres, ou seja, um agregado de caracteres terminado obrigatoriamente pelo carcter nulo.

17

CAPTULO 2 : COMPLEMENTOS SOBRE C

No caso de memcpy e de memmove, so transferidos n bytes da regio apontada por zp para a regio apontada por zd. A diferena entre elas est no facto de que, em memcpy, as duas regies tm que ser disjuntas, enquanto que, em memmove, o processo de cpia feito primeiramente para uma regio intermdia, o que possibilita a sobreposio parcelar das duas regies. No caso de strcpy e de strncpy, as regies apontadas por zp e zd tm que ser disjuntas. O nmero de caracteres transferidos por strcpy varivel e depende do tamanho da cadeia de caracteres, concretamente, so copiados todos os caracteres que formam a cadeia de caracteres referenciada por zp, incluindo o carcter nulo final. Por outro lado, strncpy copia um mximo de n caracteres, incluindo o carcter nulo final, da cadeia de caracteres referenciada por zp. Se o tamanho da cadeia de caracteres for menor do que n-1, as posies restantes da regio apontada por zd sero preenchidas com caracteres nulos. Se, pelo contrrio, o tamanho da cadeia de caracteres for maior do que n-1, o resultado da cpia para a regio de memria apontada por zd no constituir uma cadeia de caracteres, porque falta o carcter nulo final. A Figura 2.27 apresenta as funes de concatenao.
char *strcat (char *zd, char *zp); char *strncat (char *zd, char *zp, size_t n);

Figura 2.27 - Funes de concatenao.

Ambas as funes copiam a cadeia de caracteres referenciada por zp, incluindo um carcter nulo final, para o fim da cadeia de caracteres referenciada por zd e devolvem a localizao de zd. O ponto de juno a localizao do carcter nulo final da cadeia de caracteres referenciada por zd, que substitudo pelo primeiro carcter da cadeia de caracteres referenciada por zp. Est subjacente a qualquer das funes que zd aponta para uma regio de memria, cuja reserva de espao de armazenamento tem tamanho suficiente para conter a cadeia de caracteres resultante. No caso da funo strncat, so copiados no mximo n caracteres, excluindo o carcter nulo final, da cadeia de caracteres referenciada por zp. As regies de memria apontadas por zp e zd tm que ser disjuntas. A Figura 2.28 apresenta um exemplo da utilizao das funes strcpy e strcat.
char MENS1[] = "Ola", MENS2[] = "bom dia", MENS3[15]; ... strcpy (MENS3, MENS1); strcat (MENS3, " "); strcat (MENS3, MENS2); /* MENS3 igual a "Ola" */ /* agora MENS3 igual a "Ola " */ /* agora MENS3 igual a "Ola bom dia" */

Figura 2.28 - Exemplo da utilizao das funes strcpy e strcat.

A Figura 2.29 apresenta as funes de comparao.


int memcmp (void *z1, void *z2, size_t n); int strcmp (char *z1, char *z2); int strncmp (char *z1, char *z2, size_t n);

Figura 2.29 - Funes de comparao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

As funes comparam byte a byte o contedo das regies de memria referenciadas por z1 e z2. O processo de comparao termina logo que uma deciso possa ser univocamente tomada. O contedo da regio z1 diz-se menor do que o contedo da regio z2 quando, para o primeiro par de posies correspondentes que so distintas, se verifica que o contedo da posio de z1 menor do que o contedo da posio de z2, ambos os contedos interpretados como unsigned char. Adicionalmente, o contedo da regio z1 menor do que o contedo da regio z2 quando o tamanho da regio z1 menor do que o tamanho da regio z2 e o contedo de cada posio de z1 igual ao contedo da posio correspondente de z2. O contedo da regio z1 diz-se igual ao contedo da regio z2 quando as duas regies tm o mesmo tamanho e o contedo de cada posio de z1 igual ao contedo da posio correspondente de z2. O contedo da regio z1 diz-se maior do que o contedo da regio z2 quando se tem em alternativa que o contedo da regio z2 menor do que o contedo da regio z1. O valor devolvido ser positivo, quando o contedo da regio z1 for maior do que o contedo da regio z2, zero, quando o contedo da regio z1 for igual ao contedo da regio z2, e, negativo, quando o contedo da regio z1 for menor do que o contedo da regio z2. No caso da funo memcmp, comparam-se sempre n bytes sem ser atribuda qualquer interpretao ao seu contedo, enquanto que, para strcmp e strncmp, se supe que z1 e z2 referenciam cadeias de caracteres, ou seja, agregados de caracteres terminados obrigatoriamente pelo carcter nulo, e o processo de comparao decorre at tomada unvoca de uma deciso, no caso da funo strcmp, ou at um mximo de n caracteres terem sido comparados no caso da funo strncmp. A Figura 2.30 apresenta um exemplo que l e processa frases de texto, at um mximo de NMAX frases ou at ao aparecimento da frase FIM. Quando se detecta a frase de terminao, usando para esse efeito a funo strcmp, o ciclo repetitivo while interrompido com a instruo break, de forma a evitar o processamento da frase de terminao. Como a funo devolve 0 quando as duas cadeias de caracteres so iguais, ento usa-se o operador ! para que a expresso seja verdadeira. Ou seja, a expresso !strcmp(FRASE, FIM) equivalente expresso booleana strcmp(FRASE, FIM) == 0.
#define NMAX 10 ... char FRASE[80], FIM[] = "FIM"; int N = 0; ... do { N++; printf ("Escreva a frase -> "); scanf ("%79[^\n]", FRASE); /* leitura da frase scanf ("%*[^\n]"); /* descartar todos os outros caracteres scanf ("%*c"); /* descartar o carcter de fim de linha if ( !strcmp (FRASE, FIM) ) break; ... /* processar a frase lida } while ( N < NMAX );

*/ */ */ */

Figura 2.30 - Exemplo da utilizao da funo strcmp.

19

CAPTULO 2 : COMPLEMENTOS SOBRE C

A Figura 2.31 apresenta as funes de busca.


void *memchr (void *z, int c, size_t n); char *strchr (void *z, int c); char *strrchr (void *z, int c); size_t strspn (char *z, char *zx); size_t strcspn (char *z, char *zx); char *strpbrk (char *z, char *zx); char *strstr (char *z, char *zx); char *strtok (char *z, char *zx);

Figura 2.31 - Funes de busca.

A funo memchr localiza a primeira ocorrncia do valor c, previamente convertido para unsigned char, entre os n primeiros bytes da regio de memria referenciada por z. O valor devolvido a localizao do valor em z, ou o ponteiro nulo, caso o valor no tenha sido encontrado. O ponteiro nulo, que est definido no ficheiro de interface stddef.h, reconhecida pelo identificador NULL e representa o endereo da posio de memria 0. As funes strchr e strrchr localizam, respectivamente, a primeira e a ltima ocorrncia do valor c, previamente convertido para char, na cadeia de caracteres referenciada por z. Neste contexto, supe-se que o carcter nulo final faz parte da cadeia de caracteres. O valor devolvido a localizao do valor em z, ou o ponteiro nulo, caso o valor no tenha sido encontrado. As funes strspn e strcspn calculam e devolvem o comprimento do segmento inicial mximo da cadeia de caracteres referenciada por z que consiste inteiramente de caracteres, respectivamente, presentes e no presentes, na cadeia de caracteres referenciada por zx. A funo strpbrk localiza a primeira ocorrncia, na cadeia de caracteres referenciada por z, de qualquer carcter presente na cadeia de caracteres referenciada por zx. O valor devolvido a localizao do carcter, ou o ponteiro nulo, caso nenhum carcter tenha sido encontrado. A funo strstr localiza a primeira ocorrncia, na cadeia de caracteres referenciada por z, da sequncia de caracteres que constitui a cadeia de caracteres referenciada por zx. O valor devolvido a localizao da cadeia de caracteres, ou o ponteiro nulo, caso nenhum carcter tenha sido encontrado. Quando zx referencia uma cadeia de caracteres nula, ou seja, formado apenas pelo carcter nulo, o valor devolvido z. A funo strtok permite decompor por invocaes sucessivas a cadeia de caracteres referenciada por z num conjunto de subcadeias de caracteres. A decomposio baseia-se no princpio de que a cadeia de caracteres original formado por uma sequncia de palavras delimitadas por um ou mais caracteres separadores descritos na cadeia de caracteres referenciada por zx. O conjunto dos caracteres separadores pode variar de invocao para invocao. A primeira invocao de strtok procura na cadeia de caracteres referenciada por z a primeira ocorrncia de um carcter no contido na cadeia de caracteres referenciada por zx. Se tal carcter no existe, ento no h palavras na cadeia de caracteres e a funo devolve um ponteiro nulo. Se existir, esse carcter representa o incio da primeira palavra. A seguir, a funo procura na cadeia de caracteres a primeira ocorrncia de um carcter contido na cadeia de caracteres separadores. Se tal carcter no existe, ento a palavra actual estende-se at ao fim da cadeia de caracteres e as invocaes subsequentes da funo devolvem um

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

20

ponteiro nulo. Se existir, esse carcter substitudo pelo carcter nulo e constitui o fim da palavra actual, cuja localizao devolvida pela funo. Antes de terminar, porm, a funo strtok armazena internamente a localizao do carcter seguinte ao carcter nulo. Esta referncia vai constituir o ponto de partida da prxima pesquisa que, para ser feita, exige a invocao da funo com um ponteiro nulo em substituio de z. A Figura 2.32 apresenta um excerto de cdigo da utilizao da funo strtok para decompor uma cadeia de caracteres constituda por palavras separadas pelos caracteres $ e #, e a Figura 2.33 apresenta a sua visualizao grfica. A primeira invocao da funo detecta o incio da primeira palavra a seguir ocorrncia do carcter #. Enquanto a funo no devolver o ponteiro nulo, e o agregado de ponteiros para char tiver capacidade de armazenamento de informao, a funo invocada para encontrar o incio da prxima palavra a seguir a um dos caracteres separadores. A segunda palavra comea a seguir ao carcter #, a terceira palavra comea a seguir ao carcter $ e a quarta palavra comea a seguir ao carcter #. Quando a funo acaba de processar a frase, ela ficou decomposta em palavras, pelo que, ficou corrompida.
char FRASE[] = "#era$#uma$vez#um$"; char *PALAVRA[4], *PS; int N; ... N = 0; PS = strtok (FRASE, "$#"); while ( (PS != NULL) && (N < 4) ) { PALAVRA[N] = PS; N++; PS = strtok (NULL, "$#"); }

Figura 2.32 - Exemplo da utilizao da funo strtok.


FRASE INICIAL FRASE FINAL '#' '#' 'e' 'r' 'e' 'r' 'a' '$' '#' 'u' 'm' 'u' 'm' 'a' '$' 'v' 'e' 'e' 'z' '#' 'u' 'm' '$' '\0'

'a' '\0' '#'

'a' '\0' 'v'

'z' '\0' 'u'

'm' '\0' '\0'

PALAVRA[0] PALAVRA[1] PALAVRA[2] PALAVRA[3]

Figura 2.33 - Visualizao grfica do funcionamento da funo strtok.

A Figura 2.34 apresenta as funes diversas.


void *memset (void *z, int c, size_t n); char *strerror (int errnum); size_t strlen (char *z);

Figura 2.34 - Funes diversas.

21

CAPTULO 2 : COMPLEMENTOS SOBRE C

A funo memset copia o valor c, previamente convertido para unsigned char, para os n primeiros bytes da regio de memria referenciada por z. A funo strerror devolve a mensagem associada com a varivel global errno. A funo strlen devolve o comprimento da cadeia de caracteres, ou seja, o nmero de caracteres armazenados at ao carcter nulo. A Figura 2.35 apresenta um exemplo da utilizao da funo strerror. Esta funo devolve uma cadeia de caracteres que pode ser usada para escrever mensagens de erro associadas varivel global de erro. Neste exemplo, logo aps a invocao da funo matemtica raiz quadrada, a varivel global de erro testada para em caso de erro escrever a mensagem respectiva no monitor atravs do printf. No clculo de uma raiz quadrada, h erro se o valor de X for negativo, pelo que, nesse caso a mensagem indicar que o argumento X est fora da gama permitida para o clculo da raiz quadrada.
#include <stdio.h> #include <errno.h> #include <math.h> #include <string.h> ... errno = 0; Z = sqrt (X); if (errno != 0) /* se ocorreu uma situao de erro */ { printf("ERRO -> %s\n", strerror(errno)); ... -> Numerical argument out of domain */ }

Figura 2.35 - Exemplo da utilizao da funo strerror.

2.3.6 Converso de cadeias de caracteres


A biblioteca stdio contm funes de converso de cadeias de caracteres. Para utilizar essas funes preciso fazer a incluso do ficheiro de interface stdio.h com a seguinte directiva do pr-processador. #include <stdio.h> A Figura 2.36 apresenta as funes de converso, que permitem criar e decompor cadeias de caracteres.
int sscanf (char *z, const char *formato, lista de ponteiros); int sprintf (char *z, const char *formato, lista de expresses);

Figura 2.36 - Funes de converso de cadeias de caracteres.

A funo sscanf decompe uma cadeia de caracteres referenciada por z, segundo as regras impostas pelo formato de leitura indicado por formato, armazenando sucessivamente os valores convertidos nas variveis, cuja localizao indicada na lista de ponteiros de variveis. As definies do formato e da lista de ponteiros de variveis so as mesmas que para as funes scanf e fscanf. A funo sscanf fundamentalmente equivalente a fscanf. A diferena principal que a sequncia de caracteres a converter obtida da cadeia de caracteres referenciada por z, em vez de ser lida do ficheiro. Assim, a deteco do carcter nulo, que caracteriza a situao de se ter atingido o fim da cadeia de caracteres, vai corresponder situao de deteco do carcter de fim de ficheiro.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

22

A Figura 2.37 apresenta um exemplo da utilizao da funo sscanf para decompor uma cadeia de caracteres em subcadeias de caracteres. A cadeia de caracteres FRASE, que armazena uma data decomposta nas suas componentes, CIDADE, DIA, MES e ANO, para posterior impresso no monitor com outro formato.
char FRASE[] = "Aveiro, 25 de Fevereiro de 2003"; char CIDADE[10], MES[10]; int DIA, ANO; ... sscanf (FRASE, "%9[^,],%d de %9s de %d", CIDADE, &DIA, MES, &ANO); /* CIDADE igual a "Aveiro", DIA = 25, */ /* MES igual a Fevereiro e ANO = 2003 */ ... printf ("(%s) %2d/%s/%4d\n", CIDADE, DIA, MES, ANO); /* impresso no monitor (Aveiro) 25/Fevereiro/2003 */

Figura 2.37 - Exemplo da utilizao da funo sscanf.

A funo sprintf cria uma cadeia de caracteres referenciada por z, constituda por texto e pelos valores das expresses que formam a lista de expresses, segundo as regras impostas pelo formato de escrita, indicado por formato. As definies do formato e da lista de expresses so as mesmas que para as funes printf e fprintf. A funo sprintf fundamentalmente equivalente a fprintf. A diferena principal que a sequncia de caracteres convertida armazenada na regio de memria referenciada por z, em vez de ser escrita no ficheiro. Assim, torna-se necessrio garantir que foi reservado previamente espao de armazenamento suficiente para a sequncia de caracteres convertida e para o carcter nulo final. A Figura 2.38 apresenta um exemplo da utilizao da funo sprintf para a construo dinmica de um formato de leitura para a funo scanf. Se pretendermos fazer a leitura da cadeia de caracteres FRASE, que foi declarada com uma dimenso parametrizada pela constante MAX_CAR, no podemos usar o formato %MAX_CARs, uma vez que o formato de leitura da funo scanf s aceita literais. A soluo passa pela construo dinmica do formato de leitura. A cadeia de caracteres FORMATO constituda pela concatenao do carcter %, atravs do especificador de converso %%, com o valor de MAX_CAR, atravs do especificador de converso %d, e com o carcter s, ou seja, armazena o formato de leitura %40s.
#define MAX_CAR 40 ... char FRASE[MAX_CAR+1], FORMATO[20]; ... sprintf (FORMATO, "%%%ds", MAX_CAR); /* FORMATO igual a "%40s" */ ... scanf (FORMATO, FRASE); /* equivalente a scanf ("%40s", FRASE); */

Figura 2.38 - Exemplo da utilizao da funo sprintf.

23

CAPTULO 2 : COMPLEMENTOS SOBRE C

2.4 Agregados bidimensionais e tridimensionais


Um agregado unidimensional uma sequncia linear de elementos que so acedidos atravs de um ndice. No entanto, existem problemas em que a informao a ser processada melhor representada atravs de uma estrutura de dados com um formato bidimensional, como por exemplo, uma tabela com vrias colunas de informao, ou uma matriz no caso de aplicaes matemticas. Para esse tipo de aplicaes precisamos de um agregado bidimensional. Um agregado bidimensional pois um agregado de agregados. A sua definio respeita as mesmas regras de um agregado unidimensional, sendo a nica diferena o facto de ter dois descritores de dimenso em vez de um. Pode ser visto como uma estrutura composta por linhas, cujo nmero definido pelo primeiro descritor de dimenso, e por colunas, cujo nmero definido pelo segundo descritor de dimenso. Mas apesar desta viso bidimensional ele armazenado na memria de forma linear, em endereos de memria contguas, de maneira a simplificar o acesso aos seus elementos. O acesso a um elemento do agregado bidimensional feito atravs de dois ndices, o primeiro para a linha e o segundo para a coluna, onde se encontra o elemento a que se pretende aceder. preciso ter sempre em considerao que na linguagem C, os ndices dos agregados so variveis numricas inteiras positivas e o ndice do elemento localizado mais esquerda em cada dimenso o zero. Ao contrrio do Turbo Pascal onde o acesso pode ser feito indiscriminadamente por A[L][C] ou A[L,C], na linguagem C deve ser feito obrigatoriamente por A[L][C]. A Figura 2.39 apresenta um exemplo da declarao de dois agregados, um unidimensional com 3 elementos inteiros e outro bidimensional com 6 elementos inteiros, bem como, da sua colocao na memria.
int A[3], B[2][3]; A[0] A[1] A[2] B[0][0] B[0][1] B[0][2] B[1][0] B[1][1] B[1][2]

Figura 2.39 - Declarao de agregados unidimensionais e bidimensionais.

A Figura 2.40 apresenta um excerto de cdigo que atribui valores aos agregados A e B. Para aceder a todos os elementos do agregado bidimensional B necessitamos de um duplo ciclo repetitivo for. Procure descobrir quais os valores que so armazenados nos agregados.
int A[3], B[2][3]; unsigned int I, J; ... for (I = 0; I < 3; I++) A[I] = I; for (I = 0; I < 2; I++) for (J = 0; J < 3; J++) ... B[I][J] = A[ (I+J)%3 ];

Figura 2.40 - Manipulao dos agregados.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

24

A expresso de inicializao para variveis de tipo agregado com N dimenses de um tipo base baseia-se no pressuposto que um tal agregado pode ser entendido como um agregado unidimensional de objectos, que so por sua vez, agregados com N1 dimenses do tipo base. Assim, a aplicao sistemtica deste pressuposto e as regras de inicializao de agregados unidimensionais permitem a construo da expresso de inicializao adequada a cada caso. Cada linha de elementos de inicializao deve ser inserida entre chavetas, de maneira a aumentar a legibilidade da inicializao. A Figura 2.41 apresenta a declarao e inicializao de dois agregados bidimensionais, bem como da sua colocao na memria.
int B[2][3] = { {0, 1, 2}, {3, 4, 5} }; int C[3][3] = { {10}, {13, 14} }; B[0][0] B[0][1] B[0][2] B[1][0] B[1][1] B[1][2] C[0][0] C[0][1] C[0][2] C[1][0] C[1][1] C[1][2] C[2][0] C[2][1] C[2][2] 0 1 2 3 4 5 10 0 0 13 14 0 0 0 0

Figura 2.41 - Declarao e inicializao de agregados bidimensionais.

O agregado B, que tem 23 elementos, inicializado com duas listas de inicializao, cada uma delas constituda por trs constantes inteiras. Pelo que, todos os seus elementos so inicializados. O agregado C, que tem 33 elementos, inicializado com apenas duas listas de inicializao. A primeira lista de inicializao constituda apenas por uma constante, pelo que, o elemento C[0][0] inicializado a 10 e os elementos C[0][1] e C[0][2] so inicializados a 0. A segunda lista de inicializao constituda por duas constantes, pelo que, os elementos C[1][0] e C[1][1] so inicializados a 13 e a 14 respectivamente, e o elemento C[1][2] inicializado a 0. Como no existe a terceira lista de inicializao, os elementos C[2][0], C[2][1] e C[2][2] so inicializados a 0. Para alm dos agregados bidimensionais, por vezes existe a necessidade de utilizar agregados tridimensionais. Por exemplo, para a simulao de um campo electromagntico no espao. A Figura 2.42 apresenta a declarao e inicializao de dois agregados tridimensionais, bem como da sua colocao na memria. Tal como na declarao de agregados unidimensionais possvel omitir o descritor de dimenso, ou seja, fazer uma definio incompleta, mas apenas da primeira dimenso, tal como feito na declarao do agregado E. A primeira dimenso pode ser inferida pelo compilador a partir da expresso de inicializao, e neste exemplo 2.

25

CAPTULO 2 : COMPLEMENTOS SOBRE C

int D[2][2][2] = { { {0, {2, }, { {4, {6, } }; int E[ ][2][2] = { { {10}, {12, 13} }, { {14} } }; 1}, 3} 5}, 7}

D[0][0][0] D[0][0][1] D[0][1][0] D[0][1][1] D[1][0][0] D[1][0][1] D[1][1][0] D[1][1][1] E[0][0][0] E[0][0][1] E[0][1][0] E[0][1][1] E[1][0][0] E[1][0][1] E[1][1][0] E[1][1][1]

0 1 2 3 4 5 6 7 10 0 12 13 14 0 0 0

Figura 2.42 - Declarao e inicializao de agregados tridimensionais.

O agregado D, que tem 222 elementos, inicializado com quatro listas de inicializao, cada uma delas constituda por 2 constantes inteiras. Pelo que, todos os seus elementos so inicializados. O agregado E, que tem a mesma dimenso, inicializado com apenas trs listas de inicializao com um total de 4 constantes. A primeira lista de inicializao constituda apenas por uma constante, pelo que, o elemento E[0][0][0] inicializado a 10 e o elemento E[0][0][1] inicializado a 0. A segunda lista de inicializao constituda por duas constantes, pelo que, os elementos E[0][1][0] e E[0][1][1] so inicializados a 12 e a 13 respectivamente. A terceira lista de inicializao constituda por apenas uma constante, pelo que, o elemento E[1][0][0] inicializado a 14 e o elemento E[1][0][1] inicializado a 0. Como no existe a quarta lista de inicializao, os elementos E[1][1][0] e E[1][1][1] so inicializados a 0. Mas, apesar de normalmente no serem necessrios agregados com mais de duas ou trs dimenses, a linguagem C tal como o Pascal no limita o nmero de dimenses de um agregado. Segundo a norma ANSI, os compiladores devem suportar pelo menos seis dimenses. Na passagem de agregados multidimensionais a uma funo preciso passar um ponteiro para o incio do agregado, usando para o efeito o nome do agregado seguido dos parnteses rectos, tal como, na passagem de um agregado unidimensional. Mas, como o elemento inicial de um agregado multidimensional tambm um agregado, obrigatrio indicar o nmero de elementos de cada uma das N1 dimenses direita. Ou seja, apenas a dimenso mais esquerda pode ser omitida. A Figura 2.43 apresenta a definio de uma funo que inicializa a zero todos os elementos de um agregado tridimensional. A funo tem um parmetro de entrada-sada que o agregado a inicializar e um parmetro de entrada que a primeira dimenso do agregado.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

26

void INICIALIZAR (int AM[][M][N], int NE) /* definio da funo */ { int I, J, K; for (I = 0; I < NE; I++) for (J = 0; J < M; J++) for (K = 0; K < N; K++) AM[I][J][K] = 0; /* para aceder ao elemento AM[I][J][K] */ } int AMULT[L][M][N]; ... INICIALIZAR (AMULT, L); /* invocao da funo */

Figura 2.43 - Passagem de um agregado tridimensional a uma funo.

Uma forma alternativa de fazer a passagem do agregado, consiste em passar explicitamente um ponteiro para o primeiro elemento do agregado e indicar todas as dimenses do agregado como parmetros de entrada auxiliares. A Figura 2.44 apresenta esta forma alternativa. O parmetro de entrada-sada que representa o incio do agregado passado como sendo um ponteiro para ponteiro para ponteiro para inteiro, atravs da declarao int AM[ ][ ][ ], ou em alternativa int * * *AM.
void INICIALIZAR (int AM[][][], int X, int Y, int Z) { /* definio da funo */ int I, J, K; for (I = 0; I < X; I++) for (J = 0; J < Y; J++) for (K = 0; K < Z; K++) *((int *) AM + I*Y*Z + J*Z + K) = 0; } /* para aceder ao elemento AM[I][J][K] */ int AMULT[L][M][N]; ... INICIALIZAR (AMULT, L, M, N); /* invocao da funo */

Figura 2.44 - Passagem de um agregado tridimensional a uma funo (verso alternativa).

A vantagem desta soluo que a implementao no tem a necessidade de saber quais so as dimenses do agregado e, portanto, a funo genrica. Mas, tem a desvantagem de o programador necessitar de executar a aritmtica de ponteiros para aceder aos elementos do agregado. Uma vez que o parmetro passado funo do tipo int * * *, para aceder aos elementos do agregado usando o operador apontado por, o endereo calculado tem de ser convertido explicitamente num ponteiro para o tipo de elementos do agregado, neste caso o tipo inteiro, da o uso do operador cast (int *).

2.5 Ponteiros
Como j foi referido anteriormente, um ponteiro embora assuma o valor de um endereo, no propriamente um endereo, porque ele est sempre associado a um tipo de dados bem definido. Da que duas variveis de tipo ponteiro podem conter o mesmo valor, ou seja, o mesmo endereo, e no entanto constiturem de facto entidades distintas.

27

CAPTULO 2 : COMPLEMENTOS SOBRE C

Esta diferena apresentada no exemplo da Figura 2.45. Embora as variveis PI e PD contenham o mesmo valor aps a execuo das instrues de atribuio, elas constituem de facto entidades distintas. O ponteiro PI referencia a regio de memria de armazenamento de uma varivel de tipo int, representada na figura pela rea mais escura com o tamanho de 4 bytes, enquanto que o ponteiro PD referencia a regio de memria de armazenamento de uma varivel de tipo double, representada na figura pelas duas reas escura e clara com o tamanho de 8 bytes.
int *PI; double *PD, VD; ... PI = (int *) &VD; PD = &VD; PI PD VD

Figura 2.45 - Exemplo da diferena entre um ponteiro e um endereo.

A declarao de variveis de tipo ponteiro ( pointer ) segue a regra geral de declarao de variveis na linguagem C, cuja definio formal se apresenta na Figura 2.46.

tipo de dados genrico lista de variveis de tipo ponteiro ;


tipo de dados genrico ::= qualquer tipo de dados vlido na linguagem C lista de variveis de tipo ponteiro ::= identificador de varivel de tipo ponteiro | lista de variveis de tipo ponteiro , identificador de varivel de tipo ponteiro identificador de varivel de tipo ponteiro ::= * identificador de varivel genrico | *identificador de varivel tipo ponteiro identificador de varivel genrico ::= identificador de varivel | identificador de varivel = expresso de inicializao
Figura 2.46 - Definio formal da declarao de variveis de tipo ponteiro.

Podemos declarar um ponteiro de um qualquer tipo de dados genrico. O tipo de dados genrico inclui os tipos de dados nativos numricos anteriormente referidos e os tipos de dados definidos pelo utilizador que sero referidos adiante. Alm destes, inclui ainda um tipo de dados nativo no numrico, o tipo void, que tem um significado muito especial. Este tipo representa literalmente um tipo de dados que coisa nenhuma. Assim, no possvel definir-se variveis deste tipo, mas unicamente ponteiros para variveis deste tipo. Trata-se da forma encontrada pela linguagem C para caracterizar ponteiros genricos e, portanto, o seu uso bastante restrito. A Figura 2.47 apresenta um exemplo da utilizao de um ponteiro de tipo void. A primeira linha de cdigo declara uma varivel A de tipo int. A segunda linha declara uma varivel V de tipo void, o que ilegal e portanto, d erro de compilao. A terceira linha declara uma varivel de tipo ponteiro para void, ou seja, um ponteiro genrico. A atribuio *PV = 5 uma instruo ilegal, apesar de PV estar a apontar para A, porque o ponteiro PV de tipo void. Para colocar o valor 5 na varivel A que de tipo int, atravs do ponteiro PV, preciso fazer um cast do ponteiro PV, como se mostra na ltima linha.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

28

int A; void V; /* declarao ilegal porque a varivel V no tem sentido void *PV; /* a varivel PV representa um ponteiro genrico ... PV = &A; *PV = 5; /* instruo ilegal porque a varivel *PV no tem sentido *((int *) PV) = 5; /* agora A = 5

*/ */ */ */

Figura 2.47 - Exemplo da declarao e utilizao de um ponteiro de tipo void.

Como se mostra na Figura 2.48, a declarao de variveis de tipo ponteiro no precisa de ser feita numa linha separada, pelo que, a lista de variveis pode incluir conjuntamente variveis do tipo indicado, ponteiros para variveis do tipo indicado, ponteiros para ponteiros para variveis do tipo indicado, e assim sucessivamente. A expresso de inicializao para variveis de tipo ponteiro consiste alternativamente na localizao de uma varivel, previamente declarada, do tipo para o qual aponta a varivel de tipo ponteiro, usando o operador endereo, ou na constante NULL, que o ponteiro nulo e que representa o valor de um ponteiro que no localiza qualquer regio de memria.
int A, *PA = &A, **PPA = NULL; /* A uma varivel de tipo int, PA uma varivel de tipo ponteiro para int, inicializada com o endereo de A e PPA uma varivel de tipo ponteiro para ponteiro para int inicializada a NULL */

Figura 2.48 - Exemplo da declarao de variveis e de ponteiros de tipo int.

Dado que ponteiros em abstracto no fazem sentido, na atribuio de um valor a uma varivel de tipo ponteiro obrigatrio que o tipo de dados associado varivel e expresso seja o mesmo. As nicas excepes so a atribuio a uma varivel de tipo ponteiro para void de uma expresso que um ponteiro para um tipo qualquer, e a atribuio a uma varivel que um ponteiro para um tipo qualquer de uma expresso de tipo ponteiro para void. A Figura 2.49 apresenta um exemplo demonstrativo.
int A, *PA = &A, **PPA = &PA; double *PD; void *PV, **PPV; ... PD = PA; /* incorrecto porque so ponteiros distintos PD = (double *) PA; /* correcto aps converso forada PV = PA; /* correcto porque PV um ponteiro de tipo void PD = PV; /* correcto porque PV um ponteiro de tipo void PPV = PPA; /* incorrecto porque so ponteiros distintos

*/ */ */ */ */

Figura 2.49 - Exemplos da declarao e utilizao de ponteiros de tipos diferentes.

Na Figura 2.49 a varivel PPA foi declarada do tipo ponteiro para ponteiro para int. A Figura 2.50 apresenta a sua visualizao grfica e a atribuio do valor 45 varivel A, atravs de uma dupla referncia indirecta com a instruo de atribuio **PPA = 45.
int A, *PA = &A, **PPA = &PA; ... **PPA = 45; A PA PPA 45

Figura 2.50 - Visualizao grfica de um ponteiro para ponteiro para int.

29

CAPTULO 2 : COMPLEMENTOS SOBRE C

A Figura 2.51 apresenta um erro frequentemente cometido, por programadores que se esto a iniciar na utilizao da linguagem C. Nunca se deve fazer a atribuio de um valor a uma varivel apontado por um ponteiro, sem que este tenha sido previamente inicializado. Por uma questo de segurana deve-se sempre inicializar um ponteiro, quanto mais no seja com a constante NULL.
int A, *PA; ... *PA = 23; /* incorrecto porque PA no aponta para lado nenhum */ int A, *PA = &A; ... *PA = 23; /* correcto A = 23 */

Figura 2.51 - Exemplo de m utilizao de um ponteiro devido a falta de inicializao.

2.5.1 Aritmtica de ponteiros


Como um ponteiro armazena um valor inteiro, que representa o endereo do primeiro byte da regio de armazenamento da varivel para o qual ele aponta, sobre ele pode ser realizada a operao aritmtica da adio de um valor inteiro. O significado atribudo operao colocar o ponteiro a referenciar a regio de memria que se encontra localizada mais abaixo, mais frente, se a expresso inteira for positiva, ou mais acima, mais atrs, se a expresso inteira for negativa, e a uma distncia que medida em termos de unidades de armazenamento de variveis do tipo associado ao ponteiro. Logo, a realizao da operao s faz sentido para ponteiros para tipos de dados especficos, pelo que, no pode por isso ser aplicada a expresses de tipo ponteiro para void. O ponteiro avana ou recua o nmero de bytes equivalente ao sizeof (tipo de dados) por cada unidade adicionada. Sobre um ponteiro, tambm se pode aplicar a operao de subtraco de um valor inteiro, cujo significado obviamente o inverso da adio. Tambm possvel fazer a subtraco de dois ponteiros do mesmo tipo. O significado atribudo operao determinar a distncia entre os dois ponteiros, medida em termos de unidades de armazenamento de variveis do tipo associado. De novo, a realizao da operao s faz sentido para ponteiros para tipos de dados especficos, pelo que, no pode ser aplicada a expresses de tipo ponteiro para void. Sobre ponteiros tambm possvel fazer operaes de comparao, usando os operadores relacionais, sendo que, os dois ponteiros tm de ser do mesmo tipo. A Figura 2.52 apresenta alguns exemplos destas operaes.
int A[8], *PA = A, N; /* PA aponta para A[0] ... PA++; /* agora PA aponta para A[1], ou seja PA avana 4 bytes PA += 4; /* agora PA aponta para A[5], ou seja PA avana 16 bytes PA -= 2; /* agora PA aponta para A[3], ou seja PA recua 8 bytes ... N = &A[3] - &A[0]; /* N = 3 N = &A[0] - &A[3]; /* N = -3 ... if (PA > &A[2]) ... /* a expreso decisria verdadeira */ */ */ */ */ */ */

Figura 2.52 - Operaes aritmticas sobre ponteiros.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

30

2.5.2 Agregados de ponteiros e ponteiros para agregados


A linguagem C potencia a construo de tipos de dados muito versteis, onde h a combinao do construtor agregado [ ] com o operador apontado por * na sua definio. A legibilidade resultante por vezes, contudo, muito pobre e exige uma compreenso rigorosa da associatividade e da precedncia dos operadores envolvidos. A Figura 2.53 apresenta a precedncia e a associatividade dos novos operadores.
Operadores na classe operadores primrios ( ) [ ] referncia a campo -> acesso a campo . operadores unrios operador operador operador operador cast sizeof endereo & apontado por * direita direita direita direita o o o o esquerda esquerda esquerda esquerda esquerda esquerda esquerda esquerda o o o o direita direita direita direita maior Associatividade Precedncia

menor

Figura 2.53 - Precedncia e associatividade entre os operadores.

Os operadores parnteses curvos ( ) e parnteses rectos [ ] so designados de operadores primrios. Tm associatividade da esquerda para a direita e maior precedncia que os operadores unrios e binrios. Os operadores endereo & e apontado por * so operadores unrios, cuja associatividade da direita para a esquerda. Tm uma precedncia maior do que os operadores binrios, mas menor do que os operadores unrios anteriormente apresentados, ou seja, menor do que os operadores cast e sizeof. Os operadores referncia a campo -> e acesso a campo . sero explicados no item de acesso a campos de estruturas. Aplicando as regras de associatividade e precedncia dos operadores, temos que a declarao TIPO_BASE *AP[4]; declara um agregado de 4 ponteiros para TIPO_BASE, cujo mapa de reserva de espao em memria se apresenta na Figura 2.54.
TIPO_BASE *AP[4]; AP[0] AP[1] AP[2] AP[3]

Figura 2.54 - Visualizao grfica de um agregado de ponteiros.

Uma das aplicaes dos agregados de ponteiros a construo de agregados de cadeias de caracteres, que apresentado na Figura 2.57 e na Figura 2.58. Aplicando as regras de associatividade e precedncia dos operadores, temos que a declarao TIPO_BASE (*PA)[4]; declara um ponteiro para um agregado de 4 elementos de TIPO_BASE, cujo mapa de reserva de espao em memria se apresenta na Figura 2.55.

31

CAPTULO 2 : COMPLEMENTOS SOBRE C

TIPO_BASE (*PA)[4];

PA

(*PA)[0] (*PA)[1] (*PA)[2] (*PA)[3]

Figura 2.55 - Visualizao grfica de um ponteiro para um agregado.

2.5.3 Dualidade ponteiro agregado


J referimos que devido dualidade ponteiro agregado, quer uma varivel de tipo agregado unidimensional de um dado tipo base, quer uma varivel de tipo ponteiro para o mesmo tipo base, so formas equivalentes de referenciar uma regio de memria formada por um conjunto contguo de variveis do tipo base. Pelo que, a localizao do elemento de ndice i do agregado A pode ser alternativamente expressa por &A[i] ou por A+i, e o valor do mesmo elemento por A[i] ou por *(A+i). Se entendermos um agregado bidimensional do tipo base como um agregado unidimensional de objectos que so, por sua vez, agregados unidimensionais do tipo base, temos que. TIPO_BASE B[ N1 ][ N2 ]; B[i] { &B[i][0] , B { &B[0] Neste contexto, B[i] representa o agregado unidimensional, de ndice i, de N2 elementos do tipo base ou, o que equivalente, um ponteiro para o primeiro elemento desse agregado e, B representa um ponteiro para o primeiro agregado unidimensional de N2 elementos do tipo base. Ou seja, a localizao do elemento, colocado na linha de ndice i e na coluna de ndice j do agregado B, pode ser alternativamente expressa por &B[i][j] ou por B[i]+j, e o valor do mesmo elemento por B[i][j] ou por *(B[i]+j), ou ainda por *(*(B+i)+j). Ou seja, uma varivel de tipo ponteiro para um agregado unidimensional de N2 elementos do tipo base pode ser encarada como uma varivel de tipo agregado bidimensional do mesmo tipo base, com tamanho N2 na segunda dimenso, na referncia a uma regio de memria formada por um conjunto contguo de variveis do tipo base. Pelo que, a seguinte declarao do agregado B e do ponteiro para o agregado PB inicializado com o endereo inicial do agregado, resulta no mapa de reserva de espao em memria que se apresenta na Figura 2.56 e permite a utilizao do ponteiro PB para aceder aos elementos do agregado. TIPO_BASE B[N1][N2], (*PB)[N2] = B; PB+i { &PB[i] = &B[i] *(PB+i) { PB[i] = B[i] *(PB+i)+j { PB[i]+j = &B[i][j] *(*(PB+i)+j) { *(PB[i]+j) { (*(PB+i))[j] { PB[i][j] = B[i][j], com 0 d i < N1, 0 d j < N2
TIPO_BASE B[2][4], (*PB)[4] = B; B[0][0] B[0][1] B[0][2] B[0][3] B[1][0] B[1][1] B[1][2] B[1][3] PB B B[0]

B[1]

Figura 2.56 - Declarao de um agregado bidimensional e de um ponteiro para o agregado

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

32

Existe, porm, uma diferena subtil entre as variveis de tipo agregado bidimensional de um tipo base e as variveis de tipo ponteiro para um agregado unidimensional, de tamanho igual segunda dimenso do agregado bidimensional do mesmo tipo base. Como se verifica do mapa de reserva de espao em memria apresentado na Figura 2.56, no existe espao directamente associado com a varivel B e, portanto, B, B[0] e B[1] so ponteiros constantes, cujos valores no podem ser modificados. Assim, instrues do tipo B = expresso; ou B[i] = expresso; so ilegais e tem-se que *(B+i) = B[i]. Perante a declarao TIPO_BASE A[N1], B[N1][N2], C[N1][N2][N3]; tem-se que: x A um ponteiro constante para TIPO_BASE. x B um ponteiro constante para um agregado unidimensional de N2 elementos do TIPO_BASE e B[i] um ponteiro constante para TIPO_BASE, com 0 d i < N1. x C um ponteiro constante para um agregado bidimensional de N2N3 elementos do TIPO_BASE, C[i] um ponteiro constante para um agregado unidimensional de N3 elementos do TIPO_BASE, com 0 d i < N1, e, C[i][j] um ponteiro constante para TIPO_BASE, com 0 d i < N1 e 0 d j < N2.

2.5.4 Agregados de cadeias de caracteres ( arrays of strings )


Uma das aplicaes dos agregados de ponteiros a construo de agregados de cadeias de caracteres (array of strings) constantes. A Figura 2.57 apresenta a visualizao grfica do agregado FLORES, declarado da seguinte forma. char *FLORES[4] = { rosa, dahlia, cravo }; O agregado FLORES um agregado de ponteiros para char, em que cada elemento aponta para o primeiro carcter de uma cadeia de caracteres. Como o quarto elemento no foi inicializado, ento aponta para NULL.
'r' 'o' 's' FLORES[0] FLORES[1] FLORES[2] FLORES[3] 'a' '\0' 'd' 'a' 'h' 'l' 'i' 'a' '\0' representao grfica de ponteiro nulo 'c' 'r' 'a' 'v' 'o' '\0'

Figura 2.57 - Visualizao grfica de um agregado de cadeias de caracteres.

Uma aplicao de um agregado de cadeias de caracteres constantes a converso do ms em numrico para o ms por extenso, cuja funo se apresenta na Figura 2.58. Esta uma situao em que natural que o agregado comece no ndice um, de modo a poupar a operao de subtraco de uma unidade ao nmero do ms, quando se retira do agregado a cadeia de caracteres pretendida. Pelo que, o agregado tem treze cadeias de caracteres e utiliza-se o ndice zero para armazenar uma cadeia de caracteres que vai ser usada para assinalar situaes de erro.

33

CAPTULO 2 : COMPLEMENTOS SOBRE C

Quando a funo invocada para um ms incorrecto, a funo devolve a cadeia de caracteres de ndice zero, ou seja, MsErrado. Quando o nmero do ms est correcto, a funo devolve a cadeia de caracteres, cujo ndice o ms numrico.
type TMES = string[9]; (* no Pascal *) ... function MES_EXTENSO (MES: integer): TMES; begin case MES of 1: MES_EXTENSO := 'Janeiro'; 2: MES_EXTENSO := 'Fevereiro'; 3: MES_EXTENSO := 'Maro'; 4: MES_EXTENSO := 'Abril'; 5: MES_EXTENSO := 'Maio'; 6: MES_EXTENSO := 'Junho'; 7: MES_EXTENSO := 'Julho'; 8: MES_EXTENSO := 'Agosto'; 9: MES_EXTENSO := 'Setembro'; 10: MES_EXTENSO := 'Outubro'; 11: MES_EXTENSO := 'Novembro'; 12: MES_EXTENSO := 'Dezembro'; else MES_EXTENSO := 'MesErrado' end end; char *MES_EXTENSO (int MES) /* na linguagem C */ { char *MES_EXTENSO[13] = { "MsErrado", "Janeiro", "Fevereiro", "Maro", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" }; if ( (MES < 1) } || (MES > 12) ) return MES_EXTENSO[0]; return MES_EXTENSO[MES];

Figura 2.58 - Exemplo da funo da converso do ms em numrico para o ms por extenso.

2.6 Estruturas ( structs )


Apesar de um agregado ser uma estrutura de dados muito til, porque permite a manipulao de uma grande quantidade de dados atravs de um acesso indexado, tem no entanto a limitao de todos os seus elementos serem do mesmo tipo. Em muitas situaes precisamos de armazenar informao relacionada entre si, mas que so de tipos diferentes. Por exemplo, o registo de um aluno universitrio necessita de conter a informao relativa ao nome do aluno, a informao sobre o seu bilhete de identidade, a morada, a nacionalidade, a data de nascimento, o curso em que est inscrito, o nmero mecanogrfico, o ano que est a frequentar, a lista de disciplinas a que est inscrito, e para cada uma delas o cdigo, o nome e o nmero de crditos, e o histrico das disciplinas a que j esteve inscrito. Parte desta informao texto e portanto, pode ser armazenada em cadeias de caracteres, outra de tipo numrica e a lista de disciplinas e o histrico das disciplinas de tipo agregado. Para podermos armazenar toda esta informao numa nica estrutura de dados, precisamos de um tipo de dados que permita que os seus elementos possam ser de tipos diferentes. Para esse efeito a linguagem C providencia o tipo de dados estrutura (struct ), que equivalente ao registo ( record ) do Pascal. Uma estrutura distingue-se assim de um agregado pelo facto de permitir que os seus elementos, que se designam por campos, possam ser de tipos diferentes e porque o acesso a cada um dos campos no feito atravs da sua localizao na estrutura, mas sim atravs do nome do campo a que se pretende aceder. Estamos perante um acesso por nomeao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

34

2.6.1 Declarao, inicializao e atribuio de estruturas


A Figura 2.59 apresenta a definio formal da declarao de uma estrutura, que recorre ao construtor struct. A lista de campos inserida entre chavetas, e segue a regra geral de declarao de variveis da linguagem C. Cada lista de campos comea com a declarao do tipo de dados a que pertencem os campos, seguida do nome de um ou mais campos separados por vrgulas, e terminada com o separador ;. struct identificador da estrutura { lista de campos ; }; identificador da estrutura ::= identificador vlido na linguagem C lista de campos ::= elemento campo | lista de campos ; elemento campo elemento campo ::= tipo de dados lista de nomes de campos tipo de dados ::= qualquer tipo de dados vlido na linguagem C lista de nomes de campos ::= identificador de campo | lista de nomes de campos , identificador de campo identificador de campo ::= identificador vlido na linguagem C
Figura 2.59 - Definio formal da declarao de uma estrutura ( struct ).

A Figura 2.60 apresenta o exemplo da declarao da estrutura tdados_pessoa, que permite armazenar os dados pessoais de uma pessoa, composta pelo nome, sexo e data de nascimento, que por sua vez composta pelo dia, ms e ano.
struct tdados_pessoa { char NOME[60]; char SEXO; unsigned int DIA; unsigned int MES; unsigned int ANO; };

Figura 2.60 - Exemplo da declarao de uma estrutura.

A declarao da estrutura tdados_pessoa indica que de agora em diante o compilador reconhece um novo tipo de dados designado por struct tdados_pessoa. Pelo que, para declararmos variveis deste tipo, aplicam-se as mesmas regras da declarao de variveis dos tipos nativos da linguagem C. A Figura 2.61 apresenta a declarao de uma varivel, de um agregado e de um ponteiro deste novo tipo de dados.
struct tdados_pessoa PESSOA, GRUPO_PESSOAS[10], *PPESSOA;

Figura 2.61 - Exemplo da declarao de variveis e ponteiros do tipo struct tdados_pessoa.

Em alternativa possvel definir o tipo de dados struct tdados_pessoa e declarar variveis desse tipo na mesma instruo, tal como se apresenta na Figura 2.62.

35

CAPTULO 2 : COMPLEMENTOS SOBRE C

struct tdados_pessoa { char NOME[60]; char SEXO; unsigned int DIA; unsigned int MES; unsigned int ANO; } PESSOA, GRUPO PESSOAS[10], *PPESSOA;

Figura 2.62 - Exemplo da declarao de uma estrutura e da declarao de variveis desse tipo.

Existe uma outra forma de criar um novo tipo de dados em linguagem C usando para esse efeito a instruo typedef, que equivalente instruo type do Pascal. A Figura 2.63 apresenta esta forma alternativa.
typedef struct { char NOME[60]; char SEXO; unsigned int DIA; unsigned int MES; unsigned int ANO; } TDADOS_PESSOA;

Figura 2.63 - Exemplo da definio de uma estrutura usando o typedef.

Enquanto que o identificador tdados_pessoa definia apenas a estrutura, agora TDADOS_PESSOA um identificador de toda a declarao da estrutura incluindo a palavra reservada struct. Para distinguir o tipo de dados do identificador da estrutura, normalmente usa-se o mesmo identificador, mas em caracteres maisculos. Quando se define uma estrutura atravs do typedef possvel omitir o identificador da estrutura anteriormente usado, ou seja, o identificador tdados_pessoa. Na definio de uma estrutura usando o typedef, no podem ser declaradas, nem inicializadas, variveis. Estas tm de ser declaradas parte. A Figura 2.64 apresenta a declarao de uma varivel, de um agregado e de um ponteiro do tipo de dados TDADOS_PESSOA.
TDADOS_PESSOA PESSOA, GRUPO_PESSOAS[10], *PPESSOA;

Figura 2.64 - Exemplo da declarao de variveis e ponteiros do tipo TDADOS_PESSOA.

As definies de tipos de dados usando o typedef, so colocados no incio dos ficheiros fonte logo aps as directivas de include, de maneira a tornar o tipo de dados visvel por todo o programa, ou num ficheiro de interface, que tem a extenso .h, que depois aludido nos ficheiros fonte onde o tipo de dados necessrio. possvel inicializar uma estrutura da mesma forma que se inicializa um agregado. A Figura 2.65 apresenta a declarao e inicializao de uma estrutura de dados do tipo TDADOS_PESSOA.
TDADOS_PESSOA PESSOA = { "Vincent Van Gogh", 'M', 30, 3, 1853 };

Figura 2.65 - Exemplo da declarao e inicializao de uma varivel do tipo TDADOS_PESSOA.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

36

Na linguagem C possvel atribuir uma estrutura a outra estrutura, tal como no Pascal. A Figura 2.66 apresenta alguns exemplos de instrues de atribuio envolvendo estruturas.
TDADOS_PESSOA FUNC_PESSOA (void); /* aluso funo FUNC_PESSOA */ ... TDADOS_PESSOA PESSOA1 = { "Vincent Van Gogh", 'M', 30, 3, 1853 }; TDADOS_PESSOA PESSOA2, *PPESSOA; ... PESSOA2 = PESSOA1; ... PPESSOA = &PESSOA1; PESSOA2 = *PPESSOA; ... PESSOA2 = FUNC_PESSOA ();

Figura 2.66 - Exemplos da atribuio de estruturas.

2.6.2 Acesso aos campos de estruturas


Na linguagem C existem duas formas de aceder aos campos de uma estrutura. Se estivermos na presena de uma varivel do tipo struct, usa-se o nome da varivel e o nome do campo separados por um ponto, tal e qual como no Pascal. Vamos designar o operador . por operador acesso a campo. Se estivermos na presena de um ponteiro para uma varivel do tipo struct, ento usa-se o nome do ponteiro para a varivel e o nome do campo separados pelo operador ->. Vamos designar o operador -> por operador referncia a campo. Este operador uma abreviatura da linguagem C, que combina o operador apontado por * com o operador acesso a campo ., pelo que, a instruo PPESSOA->DIA equivalente a (*PPESSOA).DIA. A Figura 2.67 mostra a atribuio do valor 30 ao campo DIA, directamente atravs de uma varivel, usando o operador acesso a campo, e por referncia indirecta atravs de um ponteiro, usando o operador referncia a campo.
TDADOS_PESSOA PESSOA, *PPESSOA = &PESSOA;

... /* acesso ao campo DIA atravs da varivel PESSOA */ PESSOA.DIA = 30; ... /* acesso ao campo DIA atravs do ponteiro PPESSOA */ PPESSOA->DIA = 30; /* equivalente a (*PPESSOA).DIA = 30; */

Figura 2.67 - Acesso aos campos de uma estrutura.

Na formao de expresses de acesso aos campos de uma estrutura, preciso ter em conta que os operadores de acesso aos campos de estruturas so operadores primrios, com associatividade da esquerda para a direita e com menor precedncia do que os operadores primrios ( ) e [ ], tal como se mostra na Figura 2.53.

2.6.3 Estruturas hierrquicas


Tal como no Pascal, tambm a linguagem C permite que um campo de uma estrutura possa ser uma estrutura, o que se designa por estruturas hierrquicas (nested structures). Uma vez que a data constitui um tipo de informao muito frequente em programao, faz sentido que seja previamente declarada como uma estrutura autnoma e depois seja usada na declarao de outras estruturas.

37

CAPTULO 2 : COMPLEMENTOS SOBRE C

A Figura 2.68 apresenta a decomposio autonomizando a data na estrutura TDATA.


typedef struct { unsigned int DIA; unsigned int MES; unsigned int ANO; } TDATA; typedef struct { char NOME[60]; char SEXO; TDATA DATA_NASCIMENTO; } TDADOS_PESSOA;

da

estrutura

TDADOS_PESSOA,

Figura 2.68 - Exemplo da declarao de estruturas hierrquicas.

Esta declarao hierarquizada em dois nveis provoca algumas consequncias na inicializao da estrutura TDADOS_PESSOA, bem como, no acesso aos campos da data. Como se mostra na Figura 2.69, agora a inicializao da estrutura TDADOS_PESSOA, implica a inicializao da estrutura TDATA entre chavetas, num segundo nvel de inicializao, tal como se de um agregado bidimensional se tratasse.
TDADOS_PESSOA PESSOA = { "Vincent Van Gogh", 'M', {30, 3, 1853} };

Figura 2.69 - Exemplo da declarao e inicializao de uma estrutura hierrquica.

A Figura 2.70 apresenta as consequncias em termos de acesso aos campos da estrutura TDATA, que agora so campos de um campo da estrutura TDADOS_PESSOA.
TDADOS_PESSOA PESSOA, *PPESSOA = &PESSOA;

... /* acesso ao campo DIA da DATA atravs da varivel PESSOA */ PESSOA.DATA_NASCIMENTO.DIA = 30; ... /* acesso ao campo DIA da DATA atravs do ponteiro PPESSOA */ PPESSOA->DATA_NASCIMENTO.DIA = 30;

Figura 2.70 - Acesso aos campos de uma estrutura interna a outra estrutura.

2.6.4 Estruturas ligadas


Uma estrutura no pode conter uma instanciao da prpria estrutura, mas pode conter ponteiros para a estrutura, permitindo assim a criao de estruturas ligadas. No entanto, neste caso necessrio usar um identificador para designar a estrutura, que depois vai ser usado como identificador do tipo de dados dos campos de tipo ponteiro, tal como se mostra na Figura 2.71. O tipo TNODO para alm dos campos que vo conter a informao a armazenar na estrutura, tem um campo de tipo ponteiro que aponta para a prpria estrutura TNODO, atravs do identificador de tipo struct tnodo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

38

typedef struct tnodo { ...; ...; struct tnodo *PNODO; } TNODO;

Figura 2.71 - Definio de uma estrutura com referncia a si prpria.

Uma estrutura pode conter ponteiros para estruturas ainda no definidas. A Figura 2.72 apresenta duas estruturas que se referenciam mutuamente. Cada estrutura tem campos que vo conter a informao a armazenar na estrutura, e um campo de tipo ponteiro que aponta para a outra estrutura.
typedef struct ts1 { ...; ...; struct ts2 *PST; } TS1; typedef struct ts2 { ...; ...; struct ts1 *PST; } TS2;

Figura 2.72 - Definio de duas estruturas que se referenciam mutuamente.

2.6.5 Estruturas como parmetros de funes


Uma estrutura pode ser passada a uma funo por valor ou por referncia. Passar uma estrutura por referncia normalmente mais rpido, porque apenas feito a cpia do ponteiro para a estrutura. Enquanto que a passagem por valor implica a criao de uma cpia local da estrutura na funo. Portanto, uma estrutura passado por valor quando ela no necessita de ser alterada pela execuo da funo e se quer garantir que a funo no altera a informao nela armazenada, ou quando a estrutura muito pequena. Caso contrrio, a estrutura deve ser passada por referncia. A Figura 2.73 apresenta a funo de leitura da estrutura TDADOS_PESSOA. Como a estrutura um parmetro de entrada-sada da funo, tem de ser passada por referncia.
void LER_DADOS_PESSOA (TDADOS_PESSOA *PESSOA) { /* definio da funo */ ... } TDADOS_PESSOA PESSOA; ... LER DADOS PESSOA (&PESSOA); /* invocao da funo */

Figura 2.73 - Definio e invocao de uma funo com uma estrutura passada por referncia.

A Figura 2.74 apresenta a funo de escrita da estrutura TDADOS_PESSOA. Como a estrutura um parmetro de entrada da funo, pode ser passada por valor.

39

CAPTULO 2 : COMPLEMENTOS SOBRE C

void ESCREVER_DADOS_PESSOA (TDADOS_PESSOA PESSOA) { /* definio da funo */ ... } TDADOS_PESSOA PESSOA; ... ESCREVER DADOS PESSOA (PESSOA); /* invocao da funo */

Figura 2.74 - Definio e invocao de uma funo com uma estrutura passada por valor.

2.6.6 Estruturas como resultado de sada de funes


Uma funo pode devolver uma estrutura. No entanto, mais eficiente devolver um ponteiro para a estrutura. Mas, para isso, temos que declarar a estrutura como sendo de durao permanente, usando para o efeito o qualificativo static. Seno, a estrutura deixa de existir assim que a funo termina a sua execuo, uma vez que por defeito as variveis das funes so automticas, ou seja, so alocadas quando a funo invocada e destrudas quando a funo termina a execuo. A Figura 2.75 apresenta as duas solues para uma funo que devolve um resultado de tipo TDATA.
TDATA FUNC_DATA_E (void) { /* o resultado de sada da funo de tipo TDATA */ TDATA DATA; ... return DATA; /* retorno da estrutura */ } TDATA *FUNC_DATA_P (void) { /* o resultado de sada da funo de tipo ponteiro para TDATA */ static TDATA DATA; ... return &DATA; /* retorno do ponteiro para a estrutura */ }

Figura 2.75 - Devoluo de uma estrutura por uma funo.

2.6.7 Agregados de estruturas


Uma vez definida uma estrutura possvel declarar agregados de estruturas. A Figura 2.76 apresenta uma funo que processa um agregado de estruturas de tipo TDADOS_PESSOA e que calcula o nmero de pessoas do sexo masculino existentes no agregado. Nesta primeira verso, o acesso aos elementos do agregado feito atravs do ndice do elemento pretendido.
int NUMERO_HOMENS (TDADOS_PESSOA GRUPO_PESSOAS[ ], int N) { int I, NUM = 0; for (I = 0; I < N; I++) if (GRUPO_PESSOAS[I].SEXO == 'M') NUM++; return NUM; }

Figura 2.76 - Exemplo de uma funo que processa um agregado de estruturas (1 verso).

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

40

A Figura 2.77 apresenta uma segunda verso, em que o acesso aos elementos do agregado feito atravs de um ponteiro que aponta para o elemento pretendido.
int NUMERO_HOMENS (TDADOS_PESSOA GRUPO_PESSOAS[ ], int N) { int I, NUM = 0; TDADOS_PESSOA *P = GRUPO_PESSOAS; for (I = 0; I < N; P++, I++) if (P->SEXO == 'M') NUM++; return NUM; }

Figura 2.77 - Exemplo de uma funo que processas um agregado de estruturas (2 verso).

A Figura 2.78 apresenta o programa que invoca a funo NUMERO_HOMENS. O nmero de elementos do agregado determinado recorrendo ao operador sizeof. O tipo de dados TDADOS_PESSOA o da Figura 2.68.
int NUMERO_HOMENS (TDADOS_PESSOA [ ], int); /* aluso funo */ ... int main (void) { TDADOS_PESSOA PESSOAS[] = { {"Vincent Van Gogh",'M',{30, 3, 1853}}, {"Vieira da Silva",'F',{13, 6, 1908}} {"Amedeo Modigliani",'M',{12, 7, 1884}}, {"Claude Monet",'M',{14, 11, 1840}}, {"Georgia O'Keeffe",'F',{15, 11, 1887}} }; int NHOMENS, NEST; NEST = sizeof (PESSOAS) / sizeof (PESSOAS[0]); NHOMENS = NUMERO_HOMENS (PESSOAS, NEST); /* invocao da funo */ ... return 0; }

Figura 2.78 - Programa que invoca a funo NUMERO_HOMENS.

2.7 Tipo enumerado ( enum )


Na linguagem C tambm possvel declarar tipos de dados enumerados ( enum ), cuja definio formal se apresenta na Figura 2.79. um tipo de dados escalar, em que se enumera a lista de valores associados a uma varivel deste tipo. Cada valor definido como um identificador constante no bloco que contm a definio de tipo, bloco esse que inserido entre chavetas. Os identificadores constantes so numerados pelo compilador, sendo atribudo ao primeiro identificador da lista o valor 0, ao segundo o valor 1, e assim sucessivamente. Podemos no entanto, alterar esta numerao e indicar para cada identificador um valor especfico, de forma a utilizar valores mais explanatrios. Sempre que no se atribui um valor a um dos identificadores, o compilador atribui-lhe o valor do identificador anterior incrementado de uma unidade. Tal como no caso da definio de estruturas, a definio de enumerados tambm pode ser feita usando a instruo typedef.

41

CAPTULO 2 : COMPLEMENTOS SOBRE C

enum identificador do enumerado { lista de identificadores } ; identificador do enumerado ::= identificador vlido na linguagem C lista de identificadores ::= identificador genrico | lista de identificadores , identificador genrico identificador genrico ::= identificador definido pelo utilizador | identificador definido pelo utilizador = valor de inicializao identificador definido pelo utilizador ::= identificador vlido na linguagem C valor de inicializao ::= valor decimal positivo ou nulo
Figura 2.79 - Definio formal da declarao de um tipo enumerado.

A Figura 2.80 apresenta exemplos da declarao de tipos enumerados. O primeiro exemplo declara o enumerado t_cor e na mesma instruo a varivel COR. O segundo exemplo define o enumerado t_dia_util e numa declarao parte variveis desse tipo.
enum t_cor { BRANCO, AZUL, VERDE, ROSA, PRETO } COR; enum t_dia_util { SEGUNDA=2, TERCA=3, QUARTA=4, QUINTA=5, SEXTA=6 }; ... enum t_dia_util DIA_SEMANA, DIAS[5], *PDIA;

Figura 2.80 - Exemplos da declarao de variveis de tipos enumerados.

A Figura 2.81 apresenta os mesmos exemplos, mas recorrendo ao typedef para definir primeiro os tipos de dados enumerados T_COR e T_DIA_UTIL, ficando a declarao das variveis para depois. Como os valores atribudos aos identificadores da lista do dia da semana so seguidos, ento atribui-se apenas o valor inicial ao primeiro elemento da lista.
typedef enum { BRANCO, AZUL, VERDE, ROSA, PRETO } T_COR; ... T_COR COR; typedef enum { SEGUNDA=2, TERCA, QUARTA, QUINTA, SEXTA } T_DIA_UTIL; ... T_DIA_UTIL DIA_SEMANA, DIAS[5], *PDIA;

Figura 2.81 - Exemplos da definio de tipos enumerados usando o typedef.

Normalmente, as definies de tipos de dados enumerados usando o typedef, so colocados no incio dos ficheiros fonte logo aps as directivas de include, de maneira a tornar o tipo de dados visvel por todo o programa, ou num ficheiro de interface, que depois aludido nos ficheiros fonte onde o tipo de dados necessrio. O tipo de dados enumerado um tipo escalar, cujos identificadores so reconhecidos atravs do seu valor numrico. Pelo que, possvel ler variveis deste tipo directamente do teclado, usando o formato decimal e o valor numrico correspondente ao identificador. Tambm possvel escrever variveis deste tipo directamente no monitor, se bem que neste caso o valor impresso poder no ser muito esclarecedor. Tambm possvel executar operaes aritmticas sobre variveis enumeradas, bem como, operaes de comparao utilizando os operadores relacionais. A Figura 2.82 apresenta alguns exemplos.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

42

T_DIA_UTIL DIA; ... do { printf ("Dia da semana (SEGUNDA=2 ... SEXTA=6)? "); scanf ("%1d", &DIA); } while (DIA<2 || DIA>6); /* admitindo que foi introduzido o valor 2, ento DIA = SEGUNDA */ DIA += 4; /* agora DIA = SEXTA */ DIA--; /* agora DIA = QUINTA */ ... if (DIA > SEXTA) /* a expresso falsa */ printf ("*** Aleluia Fim de Semana ***\n");

Figura 2.82 - Exemplos de operaes sobre variveis de tipo enumerado.

2.8 Classes de armazenamento


Normalmente uma varivel declarada numa funo, designa-se por varivel automtica, porque a memria alocada automaticamente quando da invocao da funo e libertada quando termina a execuo da funo. Pelo que, o seu endereo pode ser diferente para cada invocao da funo. Como existe apenas durante a execuo da funo tambm designada de varivel com durao temporria. No entanto, possvel criar uma varivel com durao permanente. Ou seja, alocar um endereo de memria fixo para a varivel. Para tal declara-se a varivel dentro da funo com o qualificativo static. A memria, necessria para armazenar uma varivel static, alocada no incio do programa e o seu endereo fixo at ao fim do programa. Por outro lado, a varivel s inicializada da primeira vez que a funo invocada, permitindo assim que a varivel conserve o seu valor em invocaes sucessivas da funo. A Figura 2.83 mostra um exemplo que demonstra a diferena entre a varivel temporria TEMP e a varivel permanente PERM.
void INCREMENTAR (void) { int TEMP = 1; static int PERM = 1; TEMP++; PERM++; printf("Temporaria = %d <-> Permanente = %d\n", TEMP, PERM); } int main (void) { INCREMENTAR (); /* Temporaria = 2 <-> Permanente = 2 */ INCREMENTAR (); /* Temporaria = 2 <-> Permanente = 3 */ INCREMENTAR (); /* Temporaria = 2 <-> Permanente = 4 */ return 0; }

Figura 2.83 - Exemplo demonstrativo da diferena entre variveis temporrias e permanentes.

Quando a funo invocada pela primeira vez as variveis so inicializadas a 1 e depois so ambas incrementadas adquirindo o valor 2. Na segunda invocao da funo, a varivel TEMP de novo inicializada a 1 e toma o valor 2, enquanto que, a varivel PERM apenas incrementada e como conserva o valor anterior, agora toma o valor 3. Na terceira invocao da funo esta actuao repetida, pelo que, a varivel TEMP toma outra vez o valor 2, enquanto que, a varivel PERM toma o valor 4.

43

CAPTULO 2 : COMPLEMENTOS SOBRE C

Uma varivel de durao permanente s pode ser inicializada com uma expresso que contenha apenas literais. Se a varivel no for inicializada dentro de um funo, ento fica automaticamente inicializada a zero. A linguagem C permite que o programador pea ao compilador para colocar o contedo de uma varivel num registo do processador. As operaes envolvendo dados armazenados em registos so mais rpidas que as operaes envolvendo dados armazenados em posies de memria, porque, evitam a perda de tempo necessria a que que os dados sejam trazidos da memria para o processador e que o resultado da operao seja colocado de novo na memria. Para esse efeito declara-se a varivel com o qualificativo register. Infelizmente, o nmero de registos existentes no processador limitado, pelo que, no h qualquer garantia que o pedido seja respeitado. Porque as variveis declaradas com o qualificativo register, podem no existir na memria, o operador endereo no pode ser usado sobre essas variveis, ou seja, os registos no so endereveis. S podem ser declaradas com o qualificativo register variveis declaradas dentro de funes. Esta prorrogativa deve ser apenas usada para variveis que so acedidas frequentemente. A linguagem C tambm permite declarar variveis, cujo valor no pode ser alterado depois da sua inicializao. Nesse caso estamos perante uma constante que ao contrrio de uma constante definida com a directiva de define, que designada por constante simblica, ocupa espao em memria. Para declarar uma constante usa-se o qualificativo const. A Figura 2.84 apresenta uma nova verso da funo CONVERTE_DISTANCIA em que o factor de converso uma constante real local.
double CONVERTE_DISTANCIA (double ML) { const double MIL_QUI = 1.609; return ML * MIL_QUI; } /* definio da funo */

Figura 2.84 - Exemplo da utilizao de uma constante.

A grande vantagem de declarar uma varivel constante assegurar que a varivel est protegida contra escrita e portanto, no perde o seu valor original. Estas variveis so muitas vezes utilizadas como parmetros de entrada de funes, como por exemplo em funes da biblioteca de execuo ANSI string, de forma a assegurar que o parmetro no corrompido pela execuo da funo. Veja por exemplo o prottipo da funo strcpy. char *strcpy (char *zd, const char *zp); No caso da declarao de ponteiros, a palavra reservada const, pode aparecer de duas formas distintas, tendo por isso significados diferentes. A seguinte declarao, declara um ponteiro constante para um inteiro, ou seja, um ponteiro que aponta sempre para o mesmo endereo de memria. int *const PCONSTANTE; No entanto, a seguinte declarao, declara um ponteiro que aponta para um inteiro constante, ou seja, um ponteiro que pode apontar para qualquer endereo, desde que este seja o de uma varivel de tipo inteiro e constante. int const *PINTCONST;

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

44

2.9 Visibilidade dos objectos


Uma vez que a linguagem C permite a construo de aplicaes distribudas por vrios ficheiros fonte, muito importante a forma como possvel partilhar informao entre eles, bem como possibilitar a duplicao de identificadores. O nvel de visibilidade de uma varivel depende do stio onde esta declarada. Existem quatro nveis de visibilidade. As variveis declaradas antes da funo main so variveis globais que tm um alcance para todos os ficheiros fonte da aplicao. Vamos design-las por variveis globalmente globais ( Program Scope ). As variveis declaradas antes da funo main com o qualificativo static so variveis globais que tm um alcance apenas no ficheiro fonte onde esto declaradas. Vamos design-las por variveis localmente globais ( File Scope ). As variveis declaradas dentro das funes so variveis locais que tm o alcance limitado funo onde so declaradas. Vamos design-las por variveis locais ( Function Scope ). As variveis declaradas dentro de um bloco, ou seja, dentro de um conjunto de intrues inseridas entre chavetas so variveis que tm o alcance limitado ao bloco onde esto declaradas. Vamos design-las por variveis de bloco ( Block Scope ). Por uma questo de bom estilo de programao, estas variveis devem ser evitadas. A Figura 2.85 apresenta exemplos de declarao de variveis com diferentes nveis de visibilidade.
#include <stdio.h> TIPO1 VARIAVEL_1; static TIPO2 VARIAVEL_2; int main (void) { TIPO3 VARIAVEL_3; ... } /* Varivel Globalmente Global */ /* Varivel Localmente Global */

/* Varivel Local */

Figura 2.85 - Exemplos de declarao de variveis com diferentes nveis de visibilidade.

preciso ter em ateno ao duplo significado do qualificativo static. Quando uma varivel static declarada fora de uma funo, significa que a varivel global mas com alcance circunscrito ao ficheiro. Quando uma varivel static declarada dentro de uma funo, significa que a varivel local mas de durao permanente. As variveis globais so armazenadas na memria RAM, enquanto que as variveis locais so armazenadas na memria stack. Uma funo como uma caixa preta, em que os detalhes associados com a implementao da operao, nomeadamente as variveis locais, so invisveis externamente. Mas, por outro lado, todas as variveis declaradas globalmente so visveis dentro das funes. Em caso de conflito, a varivel instanciada aquela que est declarada mais perto. Numa funo no possvel declarar uma varivel local com o mesmo nome de um parmetro da funo. Mas, como as variveis locais so invisveis externamente, pode-se declarar variveis locais com o mesmo nome em funes diferentes. A Figura 2.86 apresenta exemplos de declarao e utilizao de variveis com diferente visibilidade.

45

CAPTULO 2 : COMPLEMENTOS SOBRE C

static int I, J; int main (void) { int I; ... FUNC (I); ... }

/* variveis localmente globais */

/* varivel local I do main */ /* varivel local I */

int FUNC (int N) { int N; /* ilegal, porque N um parmetro de entrada */ int I; /* varivel local I da funo FUNC */ ... I = N*J; /* varivel local I = parmetro N * varivel global J */ ... }

Figura 2.86 - Exemplos de declarao e utilizao de variveis com diferente visibilidade.

Quando temos uma aplicao distribuda por vrios ficheiros fonte, por vezes necessrio que uma funo de um ficheiro aceda a variveis globalmente globais que foram declaradas noutros ficheiros. At agora estivemos a considerar que cada declarao de uma varivel produz alocao de memria para a varivel. No entanto, a alocao de memria s feita quando h uma definio de uma varivel. Uma varivel global pode ser declarada sem produzir alocao de memria. Essa declarao chama-se aluso e usa-se para o efeito o qualificativo extern. A Figura 2.87 apresenta a aluso a uma varivel globalmente global. O objectivo de uma aluso permitir que o compilador faa a verificao de tipos. As aluses de variveis so normalmente colocadas em ficheiros cabealho, assegurando assim aluses consistentes. Para definir uma varivel global, a varivel deve ser declarada sem o qualificativo extern e deve incluir a inicializao da varivel. Para aludir uma varivel global ela deve ser declarada com o qualificativo extern e no deve incluir qualquer inicializao.
int main (void) { int I; extern int J; ... }

/* declarao da varivel local I */ /* aluso varivel globalmente global J */

Figura 2.87 - Exemplo da aluso a uma varivel global externa.

Por vezes tambm existe a necessidade de que uma funo seja apenas visvel no ficheiro onde est definida. Desta forma, pode-se definir funes com o mesmo nome em ficheiros diferentes. Para esse efeito, coloca-se o qualificativo static antes da definio da funo. normalmente aplicado a funes internas a outras funes, que portanto, no tm a necessidade de serem reconhecidas fora do ficheiro onde esto definidas.

2.10 Leituras recomendadas


x 7, 8, 9 e 10 captulos do livro C A Software Approach, 3 edio, de Peter A. Darnell e Philip E. Margolis, da editora Springer-Verlag, 1996.

Captulo 3
FICHEIROS EM C

Sumrio
Em muitas aplicaes prticas, como por exemplo na utilizao de bases de dados, a quantidade de informao de entrada tanta, que o simples facto de termos que a introduzir de novo sempre que executamos o programa torna-o pouco funcional. Por outro lado, neste tipo de aplicaes os resultados de sada precisam de ser salvaguardados para posterior utilizao. O tipo ficheiro um tipo estruturado que tem como suporte de armazenamento a memria de massa, e no a memria principal do computador, pelo que, um ficheiro no s armazena dados a ttulo definitivo, como tambm permite a comunicao de informao entre programas. Como um ficheiro tambm uma estrutura de dados dinmica, permite tambm ultrapassar a limitao das estruturas de dados estticas, como so os agregados de registos, pelo que, a estrutura de dados adequada para suportar o processamento de bases de dados. A norma ANSI da linguagem C cria um modelo uniforme de acesso aos diferentes dispositivos do sistema computacional. Quer se trate dos dispositivos de entrada e de sada, ou seja, dos perifricos, ou de ficheiros localizados na memria de massa, o acesso sempre efectuado associando um fluxo de comunicao ao dispositivo. Um fluxo de comunicao uma sequncia ordenada de bytes, armazenada numa dada regio da memria principal, que materializa o fluxo de dados entre o programa e o dispositivo. Portanto, ao contrrio da linguagem Pascal, em que um ficheiro visto como tendo uma estrutura interna ou registo associado, na linguagem C um ficheiro no mais do que uma sequncia ordenada de bytes. Ler de, ou escrever para, um dado ficheiro, significa portanto, ler do, ou escrever no, fluxo de comunicao associado ao ficheiro. Na linguagem C existem dois tipos de fluxos de comunicao, que so os fluxos de texto e os fluxos binrios. Vamos apresentar os dois atravs de exemplos de aplicao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

3.1 Fluxos de comunicao


A norma ANSI cria um modelo uniforme de acesso aos diferentes dispositivos de entrada e de sada do sistema computacional. Quer se trate dos dispositivos convencionais de entrada, que o teclado, de sada que o monitor, scanners, impressoras, ou de ficheiros localizados na memria de massa, o acesso sempre efectuado associando um fluxo de comunicao (stream) ao dispositivo. Um fluxo de comunicao visto como uma sequncia ordenada de bytes, armazenada numa dada regio da memria principal, que materializa o fluxo de dados entre o programa e o dispositivo. A transferncia de informao pode ser unidireccional ou bidireccional. Ler de, ou escrever para, um dado dispositivo, significa portanto, ler do, ou escrever no, fluxo de comunicao associado ao dispositivo. o sistema operativo que se encarrega da transferncia propriamente dita entre o fluxo de comunicao e o dispositivo, garantindo-se assim a portabilidade. Em transferncias unidireccionais, a leitura ou a escrita so sequenciais, iniciando-se, respectivamente, no princpio ou no fim da informao a residente, enquanto que, em transferncias bidireccionais, h por vezes a possibilidade de acesso aleatrio para leitura e/ou para escrita. A norma ANSI define no ficheiro de interface stdio.h uma estrutura de dados, de nome FILE, que mantm toda a informao necessria ao controlo de um fluxo de comunicao e que consiste, entre outros elementos: x Num indicador de posio de leitura ou de escrita. x Num ponteiro para a localizao do armazenamento tampo (buffer) associado ao fluxo de comunicao. x Num sinalizador de erro, que indica se ocorreu um erro de leitura ou de escrita. x Num sinalizador de fim de ficheiro, que indica que o fim da informao armazenada no dispositivo foi atingido. Assim, sempre que um programa pretender aceder a um dispositivo tem que declarar uma varivel de tipo ponteiro para FILE para armazenamento do identificador do fluxo de comunicao que devolvido pela operao de estabelecimento de comunicao. A partir da, esse identificador usado em todas as operaes de leitura e/ou de escrita no dispositivo, bem como no controlo e encerramento da comunicao. A norma ANSI distingue dois tipos de fluxos de comunicao. Os fluxos de texto e os fluxos binrios. x Nos fluxos de texto, a sequncia ordenada de bytes interpretada como uma sequncia de caracteres, organizada em linhas que consistem em zero ou mais caracteres, com representao grfica ou com funes de controlo, seguidos do carcter de fim de linha, que o carcter '\n'. x Nos fluxos binrios, a sequncia ordenada de bytes no sofre qualquer interpretao e exprime o modo de representao da informao em memria. Em geral, quando um programa posto em execuo, automaticamente estabelecida a comunicao com os dispositivos convencionais de entrada e de sada. No caso da linguagem C, a norma ANSI impe a inicializao automtica dos trs fluxos de texto seguintes:

CAPTULO 3 : FICHEIROS EM C

x O fluxo de texto stdin est associado com o dispositivo convencional de entrada, que o teclado. x O fluxo de texto stdout est associado com o dispositivo convencional de sada, que o monitor. x O fluxo de texto stderr est associado com o dispositivo convencional de sada de erro, que tambm o monitor. Para garantir a portabilidade, o ficheiro de interface stdio.h, define ainda duas constantes. A constante EOF (end of file), que sinaliza o fim de ficheiro e a constante NULL, que o ponteiro nulo, ou seja o valor de um ponteiro que no localiza qualquer regio de memria.

3.2 Abertura ou criao de um fluxo de comunicao


Como j foi referido, para que seja possvel ler ou escrever num dispositivo, necessrio associar-se-lhe um fluxo de comunicao. Isso feito invocando a funo fopen, que se apresenta na Figura 3.1, e cuja descrio est contida no ficheiro de interface stdio.h. FILE *fopen ( const char *nome do dispositivo , const char *modo de acesso ) ; nome do dispositivo ::= nome vlido para um dispositivo genrico modo de acesso ::= "r" | "rb" | "w" | "wb" | "a" | "ab" "r+" | "rb+" | "w+" | "wb+" | "a+" | "ab+"
Figura 3.1 - Funo fopen.

A funo fopen estabelece a associao de um fluxo de comunicao com o dispositivo pretendido, segundo as regras impostas pelo modo de acesso. A associao feita criando e inicializando em memria uma estrutura de dados de tipo FILE. A funo devolve, quando bem sucedida, a localizao em memria da estrutura definida, que vai funcionar na prtica como identificador do fluxo de comunicao correspondente, ou, quando falha, um ponteiro nulo. No caso de insucesso, a causa sinalizada na varivel global de erro errno. O nome do dispositivo essencialmente uma cadeia de caracteres que identifica um dispositivo especfico do sistema computacional ou um ficheiro do sistema de ficheiros. O formato concreto depende do sistema operativo presente. A diferena entre os modos de acesso sem e com o carcter b, que no primeiro caso o fluxo de comunicao associado de tipo fluxo de texto, enquanto que no segundo caso de tipo fluxo binrio. A funo tem os seguintes modos de acesso: x r Abertura de um dispositivo j existente para leitura. A leitura comea no princpio do contedo armazenado no ficheiro, ou da informao que for entretanto introduzida no dispositivo convencional de entrada. x r+ Abertura de um dispositivo j existente para leitura e escrita. A leitura ou a escrita comeam no princpio do contedo a armazenado, por cima do contedo anterior. Este modo usado tipicamente para ficheiros. x w Abertura ou criao de um dispositivo para escrita. Qualquer contedo previamente armazenado destrudo e a escrita comea numa situao de ficheiro vazio, ou de incio de comunicao no caso do dispositivo convencional de sada.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

x w+ Abertura ou criao de um dispositivo para leitura e escrita. Qualquer contedo previamente armazenado destrudo e a leitura ou a escrita comeam numa situao de dispositivo vazio. Este modo usado tipicamente para ficheiros. x a Abertura ou criao de um dispositivo para escrita no fim. A escrita s pode ser efectuada no fim do contedo j existente. Este modo usado tipicamente para ficheiros. x a+ Abertura ou criao de um dispositivo para escrita no fim e leitura. A escrita s pode ser efectuada no fim do contedo j existente, a leitura pode ocorrer em qualquer ponto. Este modo usado tipicamente para ficheiros. A Figura 3.2 sumaria as permisses dos modos de acesso.
Modo de acesso Existncia prvia do dispositivo Dispositivo aberto ou criado sem contedo Leitura do fluxo permitida Escrita no fluxo permitida Escrita permitida s no fim do fluxo r * * * * * w * a r+ w+ a+ * * * * * * * * *

Figura 3.2 - Permisses dos modos de acesso.

As condies em que se processa a comunicao com o dispositivo podem ser monitorizadas atravs de um conjunto de funes que testam os elementos, sinalizador de erro e sinalizador de fim de ficheiro da estrutura de dados FILE, e a varivel global de erro errno. Estas funes, cuja descrio est contida no ficheiro de interface stdio.h, so apresentadas na Figura 3.3. void clearerr ( FILE *fluxo ) ; int ferror ( FILE *fluxo ) ; int feof ( FILE *fluxo ) ; void perror ( const char *mensagem definida pelo programador ) ; fluxo ::= varivel de tipo ponteiro para FILE, inicializada pela invocao prvia de fopen, freopen, stdin, stdout ou stderr mensagem definida pelo programador :: = texto elucidativo da situao de ocorrncia de erro
Figura 3.3 - Funes que testam situaes de erro.

A funo clearerr limpa os indicadores de fim de ficheiro e de erro associados com o identificador do fluxo fornecido. A funo ferror testa o erro associado com o identificador do fluxo fornecido, ocorrido numa instruo de leitura ou de escrita anterior. A funo perror imprime no dispositivo convencional de sada de erro, a combinao de uma mensagem fornecida pelo programador, com a mensagem de erro associada varivel global de erro errno, separadas pelo carcter :. A funo feof testa o elemento sinalizador de fim de ficheiro da estrutura de dados FILE, correspondente ao identificador do fluxo fornecido, e devolve 0 (zero), se a sinalizao no foi activada, ou seja, se o fim de ficheiro no foi atingido, ou um valor diferente de 0, em caso contrrio. No entanto, esta funo s detecta o carcter de fim de ficheiro aps uma tentativa de leitura falhada, pelo que, em determinadas situaes produz resultados incorrectos, nomeadamente quando se tenta processar um ficheiro vazio.

CAPTULO 3 : FICHEIROS EM C

A Figura 3.4 apresenta um exemplo de abertura de um fluxo de comunicao para leitura de um ficheiro de texto. Comea-se por declarar uma varivel de tipo ponteiro para FILE e uma cadeia de caracteres para o nome do ficheiro. Deve-se assegurar que se l de facto um nome vlido para nome do ficheiro, para evitar situaes anormais na tentativa de abertura do ficheiro. Quando se abre um ficheiro, seja para leitura, seja para escrita deve-se verificar sempre se a abertura ou a criao do ficheiro foi bem sucedida e caso contrrio imprimir uma mensagem de erro. A mensagem de erro deve ser enviada para o dispositivo convencional de sada de erro, uma vez que, o dispositivo convencional de sada pode estar redireccionado para um ficheiro e ento perder-se-ia a mensagem de erro. Em caso de erro o programa deve ser terminado com indicao de erro, o que pode ser feito atravs da funo exit. Aps a leitura e processamento do contedo do ficheiro este deve ser fechado, usando a funo fclose que se apresenta a seguir.
... FILE *FPENT; char NOMEFICH[81]; int ST; ... do { printf ("Nome do ficheiro de entrada "); ST = scanf ("%80s", NOMEFICH); /* leitura do nome do ficheiro scanf ("%*[^\n]"); /* descartar todos os outros caracteres scanf ("%*c"); /* descartar o carcter de fim de linha } while (ST == 0); /* abertura do ficheiro if ( (FPENT = fopen (NOMEFICH, "r") ) == NULL ) { fprintf (stderr, "O Ficheiro %s no existe\n", NOMEFICH); exit (EXIT_FAILURE); } ... /* leitura do contedo do ficheiro e seu processamento fclose (FPENT); /* fecho do ficheiro ...

*/ */ */ */

*/ */

Figura 3.4 - Exemplo da abertura de um ficheiro de texto para leitura.

3.3 Fecho de um fluxo de comunicao


Aps a comunicao ter tido lugar, necessrio dissociar-se o fluxo de comunicao do dispositivo, para se garantir o encerramento da comunicao. Isso feito invocando a funo fclose, que se apresenta na Figura 3.5, e cuja descrio est contida no ficheiro de interface stdio.h. int fclose ( FILE *fluxo ) ; fluxo ::= varivel de tipo ponteiro para FILE, inicializada pela invocao prvia de fopen ou freopen, stdin, stdout ou stderr
Figura 3.5 - Funo fclose.

A funo fclose dissocia o fluxo de comunicao do dispositivo, encerra a comunicao e fecha o dispositivo. Se se tratar de um fluxo de comunicao, cujo modo de acesso de abertura pressupunha escrita, garantido que o sistema operativo transfere para o dispositivo, toda a informao nova existente no armazenamento tampo associado ao fluxo de comunicao. A funo devolve 0 (zero), quando bem sucedida, e EOF, em caso contrrio. No caso de insucesso, a causa sinalizada na varivel global de erro errno.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

3.4 Fluxos de texto


Os fluxos de texto esto organizados em linhas, cada linha contendo um nmero varivel de caracteres com representao grfica, ou com funes de controlo. Portanto, mesmo quando o dispositivo associado ao fluxo de comunicao supe armazenamento interno de informao, s possvel escrever-se no fim da informao l existente e ler-se a partir do princpio da informao l existente. Os fluxos de texto so, por isso, quase sempre abertos ou criados nos modos de leitura ou escrita apenas, com os modos de acesso r, w ou a, e supem um acesso puramente sequencial.

3.4.1 Leitura de um fluxo de texto


A operao bsica para leitura de fluxos de texto a funo fscanf, cuja definio formal se apresenta na Figura 3.6, e cuja descrio est contida no ficheiro de interface stdio.h. int fscanf ( identificador do fluxo de entrada , formato de leitura , lista de ponteiros de variveis ) ; identificador do fluxo de entrada ::= varivel de tipo ponteiro para FILE, inicializada pela invocao prvia de fopen, nos modos de acesso r, r+, w+ ou a+, ou stdin
Figura 3.6 - Definio formal da funo fscanf.

A funo fscanf l do fluxo de texto, cujo nome indicado pelo identificador do fluxo de entrada, sequncias de caracteres e processa-as segundo as regras impostas pelo formato de leitura, armazenando sucessivamente os valores convertidos nas variveis, cuja localizao indicada na lista de ponteiros de variveis. As definies de formato de leitura e de lista de ponteiros de variveis so as mesmas que para a funo scanf. Salvo em duas situaes especiais, deve existir uma relao de um para um entre cada especificador de converso e cada varivel da lista de ponteiros de variveis. Se o nmero de variveis da lista de ponteiros de variveis for insuficiente, o resultado da operao no est definido. Se, pelo contrrio, o nmero de variveis for demasiado grande, as variveis em excesso no so afectadas. O tipo da varivel e o especificador de converso devem ser compatveis, j que a finalidade deste ltimo indicar, em cada caso, que tipos de sequncias de caracteres so admissveis e como devem ser tratadas. Quando o especificador de converso no vlido, o resultado da operao no est definido. O processo de leitura s termina quando o formato de leitura se esgota, quando lido o carcter de fim de ficheiro, ou quando existe um conflito de tipo entre o que est indicado no formato de leitura e a correspondente quantidade a ser lida. Neste ltimo caso, o carcter que causou o conflito mantido no fluxo de texto. A funo devolve o nmero de valores lidos e armazenados, ou o valor EOF (end of file), se o carcter de fim de ficheiro lido antes que qualquer converso tenha lugar. Se, entretanto, ocorreu um conflito, o valor devolvido corresponde ao nmero de valores lidos e armazenados at ocorrncia do conflito. O maior problema na leitura de um ficheiro a correcta deteco do carcter de fim de ficheiro. Na linguagem Pascal a deteco do carcter de fim de ficheiro feito em avano quando se l o ltimo carcter til do ficheiro, da que se pode ler um ficheiro com um ciclo repetitivo repeat until. Como na linguagem C o carcter de fim de ficheiro s detectado aps uma tentativa frustrada de leitura, ento o ciclo de leitura no pode ser do tipo do while, tem que ser do tipo while. Por outro lado, a forma mais eficaz de o detectar, consiste em aproveitar o facto da funo fscanf devolver o valor EOF quando o carcter de fim de ficheiro lido antes que qualquer converso tenha lugar.

CAPTULO 3 : FICHEIROS EM C

Vamos apresentar um exemplo de leitura de um ficheiro de texto, que se supe ter um formato bem definido, composto por linhas com NITEMS de informao com formatos bem definidos. O ficheiro deve obviamente ser lido at que seja atingido o fim do ficheiro, pelo que, importante a sua correcta deteco. O processo de leitura tambm deve prever a situao de que uma linha possa eventualmente no estar de acordo com o formato esperado. O que fazer nesta situao? Temos duas solues que vamos apresentar de seguida. Vamos considerar que o ficheiro foi aberto com sucesso e que FPENT o ponteiro para FILE identificador do fluxo de texto. A Figura 3.7 apresenta a soluo mais simples, que consiste em terminar a leitura assim que se detecte uma linha desformatada, ou seja, uma linha que no contenha os NITEMS que era esperado ler. Nesse caso assinalada a situao de erro e fecha-se o ficheiro.
#define NITEMS 4 ... FILE *FPENT; int NLIDOS; ... /* leitura da linha completa de acordo com o formato esperado */ while ((NLIDOS = fscanf (FPENT, "formato\n", ponteiros)) == NITEMS) { ... ; /* processamento da informao lida */ } /* verificao da causa de paragem da leitura */ if ( NLIDOS != EOF ) /* EOF ou linha desformatada? */ fprintf (stderr, "Leitura parada devido a linha desformatada\n"); ... fclose (FPENT); /* fecho do ficheiro */ ...

Figura 3.7 - Leitura de um ficheiro de texto at deteco de uma linha desformatada.

A Figura 3.8 apresenta a segunda soluo, que consiste em assinalar com uma mensagem de erro sempre que se l uma linha desformatada, e tenta passar por cima dela de forma a continuar a ler, se possvel, o ficheiro at ao fim.
#define NITEMS 4 ... FILE *FPENT; int NLIDOS; ... /* leitura da linha completa de acordo com o formato esperado */ while ( (NLIDOS = fscanf (FPENT, "formato\n", ponteiros)) != EOF ) { /* no ocorreu EOF mas preciso aferir a consistncia da linha */ if ( NLIDOS != NITEMS ) /* lido o nmero esperado de items? */ fprintf (stderr, "Linha desformatada\n"); else ... ; ... } ... fclose (FPENT); ... /* processamento da informao lida */

/* fecho do ficheiro */

Figura 3.8 - Leitura de um ficheiro de texto ignorando linhas desformatadas.

A Figura 3.9 apresenta a soluo normalmente apresentada em livros de programao que recorre funo feof, e que em determinadas situaes produz resultados incorrectos, nomeadamente quando se tenta processar um ficheiro vazio. Pelo que, esta soluo nunca deve ser usada.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

#define NITEMS 4 ... FILE *FPENT; int NLIDOS; ... while ( !feof(FPENT) ) { /* leitura da linha completa de acordo com o formato esperado */ NLIDOS = fscanf (FPENT, "formato\n", ponteiros) /* no ocorreu EOF mas preciso aferir a consistncia da linha */ if ( NLIDOS != NITEMS ) /* lido o nmero esperado de items? */ fprintf (stderr, "Linha desformatada\n"); else ... ; } ... fclose (FPENT); ... /* processamento da informao lida */ /* fecho do ficheiro */

Figura 3.9 - Leitura de um ficheiro de texto recorrendo funo feof.

3.4.2 Escrita num fluxo de texto


A operao bsica para escrita em fluxos de texto a funo fprintf, cuja definio formal se apresenta na Figura 3.10, e cuja descrio est contida no ficheiro de interface stdio.h. int fprintf ( identificador do fluxo de sada , formato de escrita , lista de expresses ) ; identificador do fluxo de sada ::= varivel de tipo ponteiro para FILE, inicializada pela invocao prvia de fopen, nos modos de acesso r+, w, w+, a ou a+, ou stdout ou

stderr
Figura 3.10 - Definio formal da funo fprintf.

A funo fprintf escreve sucessivamente no fluxo de texto, cujo nome indicado pelo identificador do fluxo de sada, sequncias de caracteres, representativas de texto e dos valores das expresses que formam a lista de expresses, segundo as regras impostas pelo formato de escrita. As definies de formato de escrita e de lista de expresses so as mesmas que para a funo printf. O texto especificado no formato de escrita pela introduo de literais, que so copiados sem modificao para o fluxo de sada. O modo como o valor das expresses convertido, descrito pelos especificadores de converso. Salvo numa situao especial, deve existir uma relao de um para um entre cada especificador de converso e cada expresso da lista de expresses. Se o nmero de expresses for insuficiente, o resultado da operao no est definido. Se, pelo contrrio, esse nmero for demasiado grande, as expresses em excesso so ignoradas. O tipo da expresso e o especificador de converso devem ser compatveis, j que a finalidade deste ltimo indicar, em cada caso, o formato da sequncia convertida. Quando o especificador de converso no vlido, o resultado da operao no est definido. O processo de escrita s termina quando o formato de escrita se esgota, ou quando ocorre um erro. A funo devolve o nmero de caracteres escritos no fluxo de sada, ou o valor 1, se ocorreu um erro.

CAPTULO 3 : FICHEIROS EM C

A Figura 3.11 apresenta um exemplo em que vamos ler um ficheiro de texto formatado e escrever a informao lida noutro ficheiro de texto formatado, eventualmente com um formato diferente. Vamos considerar que o ficheiro de entrada foi aberto com sucesso, sendo FPENT o ponteiro para FILE identificador do fluxo de texto de entrada, e que o ficheiro para escrita foi aberto com sucesso, sendo FPSAI o ponteiro para FILE identificador do fluxo de texto de sada. S as linhas que esto de acordo com o formato de leitura esperado sero escritas no ficheiro de sada com o formato pretendido.
#define NITEMS 4 ... FILE *FPENT, *FPSAI; int NLIDOS; ... /* leitura da linha completa de acordo com o formato esperado */ while ( (NLIDOS = fscanf (FPENT, "formato\n", ponteiros)) != EOF ) { /* no ocorreu EOF mas preciso aferir a consistncia da linha */ if ( NLIDOS != NITEMS ) /* lido o nmero esperado de items? */ fprintf (stderr, "Linha desformatada\n"); else { ... ; /* processamento da informao lida */ /* escrita da linha completa de acordo com o formato pretendido */ fprintf (FPSAI, "formato\n", expresses); } ... } ... fclose (FPENT); fclose (FPSAI); ... /* fecho do ficheiro de entrada */ /* fecho do ficheiro de sada */

Figura 3.11 - Escrita num ficheiro de texto.

3.4.3 Consideraes finais sobre fluxos de texto


As instrues seguintes de leitura do fluxo de texto associado com o dispositivo convencional de entrada so equivalentes. int fscanf ( stdin , formato de leitura , lista de ponteiros de variveis ) ; int scanf ( formato de leitura , lista de ponteiros de variveis ) ; As instrues seguintes de escrita no fluxo de texto associado com o dispositivo convencional de sada so equivalentes. int fprintf ( stdout , formato de escrita , lista de expresses ) ; int printf ( formato de escrita , lista de expresses ) ; As instrues seguintes de escrita no fluxo de texto associado com o dispositivo convencional de sada de erro so aproximadamente equivalentes. int fprintf ( stderr , "mensagem definida pelo programador :%s\n" , strerror (errno) ) ; void perror ( mensagem definida pelo programador ) ; Existem ainda as duas funes, que se apresentam na Figura 3.12, para leitura e escrita de linhas de texto, e cuja descrio est contida no ficheiro de interface stdio.h.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

char *fgets ( char *s, int n, FILE *fluxo ) ; int fputs ( const char *s, FILE *fluxo ) ;
Figura 3.12 - Funes para leitura e escrita de linhas de texto.

A funo fgets l caracteres do fluxo de texto especificado por fluxo, para a cadeia de caracteres referenciada por s. O fluxo de texto foi inicializado pela invocao prvia de fopen, nos modos de acesso que permitam a leitura. Os caracteres so lidos at que seja detectado o carcter de fim de linha, ou o carcter de fim de ficheiro, ou at terem sido lidos n1 caracteres. A funo adiciona automaticamente o carcter nulo final. Se bem sucedida, a funo devolve um ponteiro para a cadeia de caracteres s. Se foi encontrado o carcter de fim de ficheiro antes da leitura de qualquer carcter, a cadeia de caracteres s fica inalterada e devolvido o ponteiro nulo. Se, entretanto, ocorreu um erro, devolvido o ponteiro nulo, mas o contedo da cadeia de caracteres imprevisvel. A funo fputs escreve a cadeia de caracteres referenciada por s, no fluxo de texto especificado por fluxo. A cadeia de caracteres tem de ser obrigatoriamente terminada com o carcter nulo final, carcter esse que no escrito no fluxo de texto. preciso ter em ateno que, a funo no escreve o carcter de fim de linha. Se bem sucedida, a funo devolve o valor 0 (zero), ou um valor diferente de 0, em caso contrrio.

3.5 Passagem de argumentos na linha de comando


Quando se coloca em execuo um programa desenvolvido na linguagem C, a funo main, que se comporta como o programa principal, a primeira funo a ser invocada e a partir da qual todas as restantes funes so invocadas. assim possvel, e desejvel em certas situaes, passar-lhe informao de entrada, tal como para qualquer outra funo. A responsabilidade de lhe passar essa informao do sistema operativo atravs da linha de comando, o que se designa por passagem de argumentos na linha de comando. Este mecanismo permite funo main comunicar directamente com o exterior e receber os argumentos que foram escritos na linha de comando. Para tal, acrescenta-se ao cabealho da funo, dois parmetros de entrada com a seguinte sintaxe. int main ( int argc, char *argv[ ] ) x O primeiro parmetro chama-se normalmente argc, de tipo int e indica o nmero de argumentos que foram escritos na linha de comando, nmero esse que inclui o nome do programa invocado. x O segundo parmetro chama-se normalmente argv, de tipo char * [ ], ou char ** , ou seja, um agregado de cadeias de caracteres. Cada cadeia de caracteres armazena um argumento da linha de comando, sendo que o primeiro argumento de ndice zero o nome do programa invocado. Este mecanismo de comunicao muito til, principalmente quando queremos desenvolver programas que vo manipular ficheiros, porque simplifica o cdigo necessrio obteno dos nomes dos ficheiros. Em vez do programa ter de ler o nome de cada ficheiro, necessitando para tal de uma varivel para o armazenar e de assegurar que no l uma cadeia de caracteres nula, pode-se passar a informao ao invocar o programa. Mas, a utilizao deste mecanismo implica alguns cuidados. Nomeadamente preciso assegurar

11

CAPTULO 3 : FICHEIROS EM C

que todos os argumentos necessrios execuo do programa foram de facto passados na linha de comando. Vamos considerar que queremos escrever um programa que utiliza dois ficheiros. Neste caso o programa tem de receber pelo menos trs argumentos, que so, o nome do programa e os nomes dos dois ficheiros. A Figura 3.13 apresenta o excerto de cdigo que valida os argumentos de entrada. Se o nmero de argumentos for inferior a trs, o programa escreve no dispositivo convencional de sada de erro, identificado por stderr, como que o programa deve ser invocado e termina a execuo do programa com indicao de finalizao sem sucesso.
... int main (int argc, char *argv[]) { if ( argc < 3 ) /* nmero de argumentos suficiente? */ { fprintf (stderr, "Uso: %s ficheiro ficheiro\n", argv[0]); exit (EXIT_FAILURE); } ... /* processamento dos ficheiros */ return EXIT_SUCESS; }

Figura 3.13 - Utilizao da passagem de argumentos na linha de comando.

3.6 Exemplos de processamento de ficheiros de texto


Pretende-se escrever um programa que leia um ficheiro de texto e que escreva noutro ficheiro de texto, todo o texto alfabtico em minsculas. Ou seja, que codifique todos os caracteres alfabticos maisculos do ficheiro de entrada em caracteres alfabticos minsculos no ficheiro de sada. Apesar de um ficheiro de texto estar organizado como um conjunto de linhas, o seu processamento pode ser feito como uma sequncia de caracteres, uma vez que o carcter de fim de linha pode ser tratado como um carcter normal. O que torna a organizao do ficheiro em linhas transparente para o programador. A Figura 3.14 apresenta o programa. Temos dois ficheiros de texto, logo necessitamos de dois ponteiros para FILE. Vamos designar o ponteiro para o ficheiro de entrada por FPENT e o ponteiro para o ficheiro de sada por FPSAI. Os nomes dos ficheiros vo ser passados na linha de comando. Vamos considerar que o primeiro argumento argv[1], o nome do ficheiro de entrada e o segundo argumento argv[2], o nome do ficheiro de sada. O nmero de argumentos argc, bem como, a abertura dos ficheiros validada e em qualquer situao de erro o programa termina com indicao de finalizao sem sucesso. No caso da abertura sem sucesso do ficheiro de sada, o programa deve fechar o ficheiro de entrada, que entretanto foi aberto, antes de terminar. A leitura do ficheiro de entrada feita carcter a carcter at ser atingido o fim do ficheiro. Cada carcter escrito no ficheiro de sada, transformado para minsculo atravs da funo tolower. Como a funo s converte o carcter se ele for um carcter alfabtico maisculo, ento no h a necessidade de testar previamente o carcter. Se o carcter for o carcter de fim de linha, o \n, ele escrito no ficheiro de sada provocando uma mudana de linha. Assim transporta-se o formato do ficheiro de entrada para o ficheiro de sada, com excepo dos caracteres alfabticos maisculos que passam para caracteres alfabticos minsculos. O carcter \ o carcter continuador de linha, que se utiliza para decompor linhas compridas.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

#include <stdio.h> #include <stdlib.h> #include <ctype.h> int main (int argc, char *argv[]) { FILE *FPENT, *FPSAI; char CAR; if ( argc < 3 ) /* o nmero de argumentos suficiente? */ { fprintf (stderr, "Uso: %s ficheiro ficheiro\n", argv[0]); exit (EXIT_FAILURE); } /* abertura do ficheiro de entrada cujo nome argv[1] */ if ( (FPENT = fopen (argv[1], "r") ) == NULL ) { fprintf (stderr, "No foi possvel abrir o ficheiro %s\n"\ , argv[1]); exit (EXIT_FAILURE); } /* criao do ficheiro de sada cujo nome argv[2] */ if ( (FPSAI = fopen (argv[2], "w") ) == NULL ) { fprintf (stderr, "No foi possvel criar o ficheiro %s\n"\ , argv[2]); fclose (FPENT); /* fecho do ficheiro de entrada */ exit (EXIT_FAILURE); } /* leitura do carcter do ficheiro de entrada e escrita do */ /* carcter convertido para minsculo no ficheiro de sada */ while ( fscanf (FPENT, "%c", &CAR) != EOF ) fprintf (FPSAI, "%c", tolower(CAR)); fclose (FPENT); fclose (FPSAI); return EXIT_SUCCESS; } /* fecho do ficheiro de entrada */ /* fecho do ficheiro de sada */

Figura 3.14 - Programa de codificao do ficheiro.

Pretende-se escrever um programa que l um ficheiro de texto que armazena informao relativa a um conjunto de pessoas e que determina as pessoas que nasceram no dia 29 de Fevereiro, escrevendo no monitor o nome da pessoa e o seu nmero de telefone. O ficheiro constitudo por linhas, sendo que cada linha contm a informao relativa a uma pessoa, com o seguinte formato. O nmero de registo da pessoa, que do tipo inteiro positivo, o nome que uma cadeia de caracteres com 40 caracteres no mximo, a data de nascimento constituda pelo dia, ms em numrico e ano, e o nmero de telefone que uma cadeia de 9 caracteres. Estes elementos de informao esto separados pelo carcter :. A Figura 3.15 apresenta o programa. Uma vez que o nome de uma pessoa constitudo por nomes separados pelo espao, no se pode usar o formato de leitura %s. Tem que se utilizar o formato alternativo para a leitura de cadeias de caracteres que o %[]. Como o nmero de registo da pessoa e o ano de nascimento no so precisos, a leitura pode descartar esses itens, no havendo a necessidade de declarar variveis para esses itens. A linha deve ser lida de forma a colocar o indicador de posio do ficheiro no incio da linha seguinte, pelo que, o carcter de fim de linha tambm deve ser lido. Portanto, o formato apropriado deve ser %*d:%40[^:]:%d:%d:%*d:%9s\n. O nome do ficheiro vai ser passado na linha de comando. O nmero de argumentos e a abertura do ficheiro so

13

CAPTULO 3 : FICHEIROS EM C

validados, e em qualquer situao de erro o programa termina com indicao de finalizao sem sucesso. A leitura do ficheiro de entrada feita linha a linha at ser atingido o fim do ficheiro e se for detectada uma linha desformatada, o programa ignora essa linha e continua a leitura do ficheiro.
#include <stdio.h> #include <stdlib.h> #define NITEMS 4 int main (int argc, char *argv[]) { FILE *FPENT; char NOME[41], TEL[10];

int NLIDOS, DIA, MES;

if ( argc < 2 ) /* o nmero de argumentos suficiente? */ { fprintf (stderr, "Uso: %s nome do ficheiro\n", argv[0]); exit (EXIT_FAILURE); } /* abertura do ficheiro de entrada cujo nome argv[1] */ if ( (FPENT = fopen (argv[1], "r") ) == NULL ) { fprintf (stderr, "No foi possvel abrir o ficheiro %s\n"\ , argv[1]); exit (EXIT_FAILURE); } printf ("Aniversariantes a 29 de Fevereiro\n"); /* leitura da linha do ficheiro de entrada */ while ( (NLIDOS = fscanf (FPENT, "%*d:%40[^:]:%d:%d:%*d:%9s\n"\ , NOME, &DIA, &MES, TEL)) != EOF ) if ( NLIDOS != NITEMS ) /* lido o nmero esperado de items? */ fprintf (stderr, "Linha desformatada\n"); /* escrita no monitor dos aniversariantes a 29/Fev */ else if ( DIA == 29 && MES == 2) printf ("NOME -> %-40.40s TELEFONE -> %s\n", NOME, TEL); fclose (FPENT); return EXIT_SUCCESS; } /* fecho do ficheiro */

Figura 3.15 - Processamento de um ficheiro constitudo por linhas com um formato especfico.

Como exerccio de treino, altere o programa para escrever a informao de sada num ficheiro de texto, cujo nome passado na linha de comando.

3.7 Fluxos binrios


Nos fluxos binrios, a transferncia de informao processa-se byte a byte segundo o formato usado para representao dos valores em memria. Quando o dispositivo associado ao fluxo de comunicao supe armazenamento interno da informao, comum ter-se uma organizao em duas partes, o cabealho e o corpo. O cabealho contm informao genrica relativa ao fluxo como um todo. O corpo concebido como uma sequncia de registos de um tipo previamente definido. A leitura e a escrita do cabealho ou de um registo particular podem ocorrer em qualquer ponto da sequncia de operaes, j que possvel determinar-se com rigor a sua localizao no ficheiro. Os fluxos binrios so, por isso, muitas vezes abertos ou criados nos modos de leitura ou escrita mistos, com os modos de acesso rb+, wb+ ou ab+, e supem um acesso aleatrio.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

3.7.1 Leitura e escrita de fluxos binrios


As operaes bsicas para leitura e escrita em fluxos binrios so respectivamente as funes fread e fwrite, cuja definio formal se apresenta na Figura 3.16, e cuja descrio est contida no ficheiro de interface stdio.h. int fread ( localizao , tamanho do elemento , nmero de elementos , identificador do fluxo ) ; int fwrite ( localizao , tamanho do elemento , nmero de elementos , identificador do fluxo ) ; localizao ::= varivel de tipo ponteiro para o tipo elemento que localiza o incio da regio de armazenamento em memria tamanho do elemento ::= tamanho em bytes de uma varivel de tipo elemento nmero de elementos ::= nmero de elementos a serem lidos ou escritos identificador do fluxo ::= varivel de tipo ponteiro para FILE, inicializada pela invocao prvia de fopen ou freopen
Figura 3.16 - Definio formal das funes fread e fwrite.

A funo fread l do fluxo binrio, cujo nome indicado pelo identificador do fluxo, o nmero de elementos indicado, todos com o mesmo tamanho em bytes, e armazena-os na regio de memria referenciada pela localizao indicada. A regio de memria referenciada tem que ter capacidade para o armazenamento da informao solicitada. O processo de leitura termina quando lido o nmero total de bytes pretendido, ou quando lido o carcter de fim de ficheiro, ou quando ocorreu um erro. A funo devolve o nmero de elementos que foram efectivamente lidos e armazenados, que pode ser menor do que o nmero de elementos pretendidos, se o carcter de fim de ficheiro lido antes, ou se ocorreu um erro. Se o nmero de elementos lidos for zero, ento o valor devolvido 0 (zero) e, quer a regio de memria referenciada, quer o fluxo binrio no so afectados. A funo fwrite escreve no fluxo binrio, cujo nome indicado pelo identificador do fluxo, o nmero de elementos indicado, todos com o mesmo tamanho em bytes, que esto armazenados na regio de memria referenciada pela localizao indicada. O processo de escrita termina quando escrito o nmero total de bytes pretendido, ou quando ocorre um erro. A funo devolve o nmero de elementos que foram efectivamente escritos no fluxo de sada, que pode ser menor do que o nmero de elementos pretendidos, se ocorreu um erro. Se o nmero de elementos escritos for zero, o valor devolvido 0 (zero) e o fluxo binrio no afectado. A Figura 3.17 exemplifica a utilizao das funes fread e fwrite para ler e escrever uma estrutura de tipo TDADOS_PESSOA em ficheiros binrios.
TDADOS_PESSOA PESSOA; ... /* leitura de uma estrutura */ fread (&PESSOA, sizeof (TDADOS_PESSOA), 1, FPENT); ... /* escrita de uma estrutura */ fwrite (&PESSOA, sizeof (TDADOS_PESSOA), 1, FPOUT);

Figura 3.17 - Utilizao das funes fread e fwrite.

15

CAPTULO 3 : FICHEIROS EM C

3.8 Funes para colocao do indicador de posio


Quando o dispositivo associado ao fluxo de comunicao supe o armazenamento interno de informao, ou seja, estamos perante um ficheiro, o ponto onde se efectua a prxima operao de leitura ou de escrita pode ser modificado atravs da manipulao do indicador de posio do ficheiro. As operaes bsicas para controlo e monitorizao do indicador de posio do ficheiro so as funes fseek, ftell e rewind, que se apresentam na Figura 3.18, e cuja descrio est contida no ficheiro de interface stdio.h. int fseek ( FILE *fluxo, long deslocamento, int referncia ) ; long ftell ( FILE *fluxo ) ; void rewind ( FILE *fluxo ) ; fluxo ::= varivel de tipo ponteiro para FILE, inicializada pela invocao prvia de fopen ou freopen deslocamento ::= nmero de bytes a deslocar o indicador de posio do ficheiro referncia ::= SEEK_SET | SEEK_CUR | SEEK_END
Figura 3.18 - Funes para colocao do indicador de posio do ficheiro.

A funo fseek posiciona o indicador de posio do ficheiro indicado, no byte referenciado pelo deslocamento pretendido a partir do ponto de referncia. O ponto de referncia pode ser o princpio da informao armazenada, reconhecido pelo identificador SEEK_SET, a posio actual do indicador, reconhecido pelo identificador SEEK_CUR ou o fim da informao armazenada, reconhecido pelo identificador SEEK_END. A funo devolve 0 (zero), em caso de sucesso, e 1, em caso de erro. No caso de insucesso, a causa sinalizada na varivel global de erro errno. Embora a sua aplicao possa ser feita com qualquer tipo de fluxo de comunicao, o seu uso com fluxos de texto muito restrito. A norma ANSI impe que, neste caso, o ponto de referncia obrigatoriamente SEEK_SET e o deslocamento ou 0 (zero), ou o valor devolvido por uma invocao prvia da funo ftell. A funo ftell devolve o valor do indicador de posio do ficheiro indicado. Para os fluxos binrios, o valor devolvido representa a distncia em bytes a partir do princpio da informao armazenada. Para os fluxos de texto, o valor devolvido contm um valor, que depende da implementao e que pode ser usado posteriormente por uma invocao de fseek para posicionamento do indicador de posio do ficheiro na mesma posio. A funo devolve o valor do indicador de posio do ficheiro, em caso de sucesso, e o valor 1L, em caso de erro. No caso de insucesso, a causa sinalizada na varivel global de erro errno. A funo rewind posiciona o indicador de posio do ficheiro no incio da informao armazenada. A funo equivalente a invocar a funo fseek para o incio do ficheiro, mas, com a vantagem de limpar os indicadores de erro e de fim de ficheiro, e sem devolver qualquer valor.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

3.9 Esvaziamento do armazenamento tampo


Por vezes necessrio garantir que a ltima informao produzida e colocada no armazenamento tampo associado ao fluxo de comunicao enviada para o dispositivo. Isso feito invocando a funo fflush, que se apresenta na Figura 3.19, e cuja descrio est contida no ficheiro de interface stdio.h. int fflush ( FILE *fluxo ) ;
Figura 3.19 - Funo fflush.

A funo fflush permite o esvaziamento do armazenamento tampo associado ao fluxo de comunicao indicado, transferindo toda a informao nova a existente para o dispositivo. Se bem sucedida, a funo devolve o valor 0 (zero), ou um valor diferente de 0, em caso contrrio.

3.10 Funes para operar sobre ficheiros


A Figura 3.20 apresenta as funes que a biblioteca stdio fornece para operar directamente sobre ficheiros. int remove ( const char *nome do ficheiro ) ; int rename ( const char *nome antigo, const char *nome novo ) ; FILE *tmpfile ( void ) ; char *tmpnam ( char *nome do ficheiro ) ;
Figura 3.20 - Funes para operar sobre ficheiros.

A funo remove remove do sistema de ficheiros o ficheiro cujo nome indicado por nome do ficheiro. A funo devolve 0 (zero), se tiver sucesso, e um valor no nulo, em caso contrrio. A funo rename altera o nome do ficheiro cujo nome indicado por nome antigo para o nome novo. A funo devolve 0 (zero), se tiver sucesso, e um valor no nulo, em caso contrrio. A funo tmpfile cria um ficheiro binrio temporrio que automaticamente apagado quando fechado, ou quando o programa termina. O ficheiro criado no modo de acesso wb+. A funo tmpnam gera um nome vlido de ficheiro que distinto de qualquer nome j existente. At TMP_MAX vezes, no mnimo 25, garantido que os nomes sucessivamente gerados so diferentes. Se o nome do ficheiro for um ponteiro nulo, a funo devolve a localizao de uma regio de memria interna onde est armazenado o nome, caso contrrio, dever apontar para uma regio de memria com capacidade de armazenamento de L_tmpnam caracteres, e ser esse o valor devolvido. O valor de L_tmpnam est definido no ficheiro de interface limits.h.

17

CAPTULO 3 : FICHEIROS EM C

3.11 Exemplo da manuteno de uma base de dados


Pretende-se desenvolver um programa que crie e mantenha uma agenda telefnica. A agenda deve ser concebida como uma sequncia de registos que contenham a seguinte informao. O nome de uma pessoa, com 40 caracteres no mximo, o seu nmero de telefone, com 15 caracteres no mximo e a sua data de aniversrio, constituda pelo dia, ms e ano. O programa deve funcionar de forma repetitiva, apresentando um menu que permita as seguintes opes: a introduo de um registo novo na agenda; a eliminao de um registo preexistente da agenda, sendo que o registo a eliminar identificado pelo nmero de telefone; a listagem do contedo de todos os registos da agenda, por ordem alfabtica do nome; e terminar a execuo do programa. A Figura 3.21 apresenta o algoritmo em pseudocdigo e linguagem natural da agenda telefnica.
nome: Base de dados da agenda telefnica manuseada num ficheiro begin Abrir ou criar o ficheiro da agenda telefnica; do Escrita do menu de operaes disponveis e leitura da operao; Processamento da operao escolhida; while no for seleccionada a operao para terminar; Fechar o ficheiro da agenda telefnica; end

Figura 3.21 - Algoritmo da agenda telefnica manuseada num ficheiro.

Uma vez que se pretende efectuar a listagem do ficheiro por ordem alfabtica do nome, ento temos que manter a agenda telefnica sempre ordenada inserindo cada novo registo no stio certo. O que implica pesquisar o ficheiro procura do local de insero do novo registo e depois deslocar todos os registos que se encontram abaixo do local de insero, um registo para baixo, de forma a abrir espao para escrever o novo registo. A Figura 3.22 apresenta a estrutura do ficheiro binrio, que constitudo por um cabealho que indica o nmero de registos presentes no ficheiro e o corpo que um conjunto de registos idnticos que esto ordenados por ordem alfabtica do nome, para facilitar a listagem do ficheiro.
cabealho nmero de registos armazenados

registo base corpo

nome nmero de telefone data de aniversrio

Figura 3.22 - Formato do ficheiro da agenda telefnica.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

A Figura 3.23 apresenta o programa da agenda telefnica, ou seja, a funo main. Para a sua implementao o programa tem como variveis de entrada, o nome do ficheiro e a opo pretendida pelo utilizador. Como o ficheiro binrio vai ser utilizado como estrutura de dados de suporte da agenda telefnica, ento o ficheiro, ou melhor o fluxo binrio associado com o ficheiro, uma varivel interna do programa.
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { unsigned int DIA, MES, ANO; } TDATA; #define #define NMX TMX 41 16 /* definio do tipo TDATA */

/* nmero mximo de caracteres do nome + 1 */ /* nmero mximo de caracteres do telefone + 1 */ /* definio do tipo TREG */ /* nome */ /* nmero de telefone */ /* data de aniversrio */ /* aluso a funes */

typedef struct { char NOME[NMX]; char NTELEF[TMX]; TDATA ANIVERSARIO; } TREG; void void void void void void void

ESCREVER_MENU_E_LER_OPCAO (int *); LER_NOME_FICHEIRO (char []); ABRIR_CRIAR_FICHEIRO (char [], FILE **); FECHAR_FICHEIRO (FILE *); INSERIR_REGISTO (FILE *); ELIMINAR_REGISTO (FILE *); LISTAR_FICHEIRO (FILE *);

int main (void) { char NOMEFICH[NMX]; int OPCAO; FILE *FP; LER_NOME_FICHEIRO (NOMEFICH);

/* nome do ficheiro */ /* escolha da opo */ /* ponteiro para o fluxo binrio */ /* ler o nome do ficheiro */ /* abrir/criar o ficheiro */ /* processamento */

ABRIR_CRIAR_FICHEIRO (NOMEFICH, &FP); do {

/* apresentao do menu e escolha da opo */ ESCREVER_MENU_E_LER_OPCAO (&OPCAO); switch (OPCAO) /* realizao da operao */ { case 1: INSERIR_REGISTO (FP); break; case 2: ELIMINAR_REGISTO (FP); break; case 3: LISTAR_FICHEIRO (FP); } } while (OPCAO != 4); FECHAR_FICHEIRO (FP); return EXIT_SUCCESS; } /* fechar o ficheiro */

Figura 3.23 - Programa da agenda telefnica.

19

CAPTULO 3 : FICHEIROS EM C

Vamos agora apresentar as funes necessrias para a implementao do programa. A funo ESCREVER_MENU_E_LER_OPCAO, que se apresenta na Figura 3.24, como o nome indica escreve o menu de operaes e l a escolha do utilizador, que o parmetro de sada da funo. A validao destina-se a assegurar que a opo escolhida uma das opes permitidas, pelo que, o programa principal no tem que se proteger contra dados de entrada incorrectos.
void ESCREVER_MENU_E_LER_OPCAO (int *OP) { do { fprintf (stdout, "\nAgenda de datas de aniversrio\n\n"); fprintf (stdout, "1 - Introduzir um registo novo\n"); fprintf (stdout, "2 - Eliminar um registo preexistente\n"); fprintf (stdout, "3 - Listar a agenda telefnica\n"); fprintf (stdout, "4 - Sada do programa\n"); fprintf (stdout, "\nQual a sua escolha? "); fscanf (stdin, fscanf (stdin, fscanf (stdin, } while (*OP < 1 } "%1d", OP); "%*[^\n]"); "%*c"); || *OP > 4); /* ler a opo */ /* ler e descartar outros dados */ /* ler e descartar o fim de linha */

Figura 3.24 - Funo para escrever o menu de operaes e ler a opo pretendida.

A funo LER_NOME_FICHEIRO, que se apresenta na Figura 3.25, um procedimento que tem como parmetro de sada o nome do ficheiro que lido do teclado.
void LER_NOME_FICHEIRO (char NOMEF[]) { fprintf (stdout, "\nNome do ficheiro? "); fscanf (stdin, "%40s", NOMEF); /* ler NMX-1 caracteres */ fscanf (stdin, "%*[^\n]"); /* ler e descartar outros caracteres */ fscanf (stdin, "%*c"); /* ler e descartar o fim de linha */ }

Figura 3.25 - Funo para ler o nome do ficheiro.

A funo ABRIR_CRIAR_FICHEIRO, que se apresenta na Figura 3.26, admite que o ficheiro, cujo nome o parmetro de entrada NOMEF, existe e tenta abri-lo. Caso ele no exista, pergunta ao utilizador se o pretende criar. No caso da criao de um ficheiro novo, o cabealho inicializado com zero registos. O ponteiro para o fluxo associado ao ficheiro binrio um parmetro de sada, pelo que, tem de ser passado por referncia. Mas, como ele um ponteiro para FILE, ento tem de ser passado um ponteiro para o ponteiro para FILE, da a declarao FILE **FP. Em caso de situao de erro na criao do ficheiro ou na escrita do cabealho so escritas mensagens de erro no dispositivo convencional de sada de erro. A funo FECHAR_FICHEIRO, que se apresenta na Figura 3.27, fecha o ficheiro, cujo ponteiro para o fluxo associado o parmetro de entrada FP. O fecho do ficheiro validado e em caso de situao de erro escrita uma mensagem de erro no dispositivo convencional de sada de erro. A vantagem da utilizao da funo perror, na escrita de mensagens de erro, deve-se ao facto da funo acrescentar mensagem do utilizador, a mensagem associada varivel global de erro errno, informando-nos da causa do erro.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

20

void ABRIR_CRIAR_FICHEIRO (char NOMEF[], FILE **FP) { char OPC; /* escolha da opo */ unsigned int ZERO = 0; /* sinalizao do nmero de registos */ if ( (*FP = fopen (NOMEF, "rb+")) != NULL ) return; do { fprintf (stdout, "\nO ficheiro no existe.\n"\ "Deseja cri-lo (s/n)? "); fscanf (stdin, "%c", &OPC); /* ler a opo */ fscanf (stdin, "%*[^\n]"); /* ler e descartar caracteres */ fscanf (stdin, "%*c"); /* ler e descartar o fim de linha */ } while ( OPC != 's' && OPC != 'n'); if (OPC == 'n') exit (EXIT_SUCCESS); if ( (*FP = fopen (NOMEF, "wb+")) == NULL ) { perror ("erro na criao do ficheiro"); exit (EXIT_FAILURE); } if (fwrite (&ZERO, sizeof (ZERO), 1, *FP) != 1) { fprintf (stderr, "erro na inicializao do ficheiro\n"); exit (EXIT_FAILURE); } }

Figura 3.26 - Funo para abrir ou criar o ficheiro da agenda telefnica.


void FECHAR_FICHEIRO (FILE *FP) { if ( fclose (FP) == EOF ) { perror ("erro no fecho do ficheiro"); exit (EXIT_FAILURE); } }

Figura 3.27 - Funo para fechar o ficheiro da agenda telefnica.

A Figura 3.28 apresenta o algoritmo em linguagem natural da funo INSERIR_REGISTO. Aps a leitura do contedo do novo registo digitado no teclado pelo utilizador recorrendo funo LER_REGISTO_TECLADO, preciso determinar o seu local de insero no ficheiro, em funo do nmero de registos actualmente armazenados no ficheiro. A funo LER_NUMREG determina o nmero de registos. A funo POS_INSERCAO implementa a operao de pesquisa e devolve o ndice do registo do ficheiro onde o novo registo deve ser colocado. O processo de comparao rudimentar, usando para o efeito a funo de comparao de cadeias de caracteres strcmp, e por isso, supe-se que o nome escrito em maisculas, com o nmero mnimo de espaos a separar as palavras. A insero do registo no ficheiro exige deslocar para baixo os registos do ficheiro a partir do elemento que foi detectado como local de insero do novo registo, o que feito pela funo DESLOCAR_PARA_BAIXO. A no ser que o local de insero do registo novo seja o fim do ficheiro. Finalmente preciso actualizar o nmero de registos armazenados no ficheiro, o que feito pela funo ESCREVER_NUMREG. A Figura 3.29 apresenta a implementao da funo INSERIR_REGISTO. As funes auxiliares sero posteriormente apresentadas.

21

CAPTULO 3 : FICHEIROS EM C

nome: Inserir um registo novo na agenda telefnica begin Leitura do contedo do novo registo do teclado; Obteno do nmero de registos armazenados no ficheiro; Determinao do ponto de insero do novo registo no ficheiro; Escrita do novo registo no ficheiro; Actualizao do nmero de registos armazenados no ficheiro; end

Figura 3.28 - Algoritmo da insero de um registo novo na agenda telefnica.


void INSERIR_REGISTO (FILE *FP) { TREG REGISTO; /* registo novo a ser lido do teclado */ unsigned int NREG, /* nmero de registos armazenados */ PREG; /* ponto de insero do registo novo */ /* aluso a funes */ void LER_REGISTO_TECLADO (TREG *); void LER_NUMREG (FILE *, unsigned int *); void ESCREVER_NUMREG (FILE *, unsigned int); unsigned int POS_INSERCAO (FILE *, unsigned int, TREG); void DESLOCAR_PARA_BAIXO (FILE *, unsigned int, unsigned int); void ESCREVER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG); /* leitura do contedo do novo registo do teclado LER_REGISTO_TECLADO (&REGISTO); /* obteno do nmero de registos actualmente armazenados LER_NUMREG (FP, &NREG); /* determinao do ponto de insero PREG = (NREG == 0) ? 0 : POS_INSERCAO (FP, NREG, REGISTO); /* escrita do novo registo no ficheiro if (PREG != NREG) DESLOCAR_PARA_BAIXO (FP, NREG, PREG); ESCREVER_REGISTO_FICHEIRO (FP, PREG, REGISTO); /* actualizao do nmero de registos armazenados ESCREVER_NUMREG (FP, NREG+1); } */ */ */ */ */

Figura 3.29 - Funo para inserir um registo novo na agenda telefnica.

A Figura 3.30 apresenta o algoritmo em linguagem natural da funo ELIMINAR_REGISTO. Para eliminar um registo preexistente no ficheiro, em primeiro lugar temos que verificar se existem de factos registos armazenados no ficheiro, o que feito pela funo LER_NUMREG. Depois preciso determinar se o registo pretendido existe no ficheiro e em que posio se encontra. Para pesquisar o registo, l-se do teclado a chave de pesquisa, que o nmero de telefone, com a funo LER_TELEFONE. A funo POS_ELIMINACAO implementa a operao de pesquisa e devolve o ndice do primeiro registo do ficheiro, cujo nmero de telefone igual ao nmero indicado. Caso no haja qualquer registo nestas condies, ser devolvido o nmero de registos do ficheiro. A eliminao do registo no ficheiro exige deslocar para cima os registos do ficheiro a partir do elemento que foi detectado como local de eliminao do novo registo, o que feito pela funo DESLOCAR_PARA_ACIMA. A no ser que o registo seja o ltimo registo do ficheiro. Finalmente preciso actualizar o nmero de registos armazenados no ficheiro. A Figura 3.31 apresenta a implementao da funo ELIMINAR_REGISTO. As funes auxiliares sero posteriormente apresentadas.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

22

nome: Eliminar um registo preexistente da agenda telefnica begin Obteno do nmero de registos armazenados no ficheiro; se o ficheiro est vazio, ento sair da funo; Leitura da chave de pesquisa; Determinao do ponto de eliminao do registo no ficheiro; Eliminao do registo do ficheiro; Actualizao do nmero de registos armazenados no ficheiro; end

Figura 3.30 - Algoritmo da eliminao de um registo preexistente da agenda telefnica.


void ELIMINAR_REGISTO (FILE *FP) { char NUMTEL[TMX]; /* nmero de telefone */ unsigned int NREG, /* nmero de registos armazenados */ PREG; /* ponto de eliminao do registo */ /* aluso a funes */ void LER_TELEFONE (char []); void LER_NUMREG (FILE *, unsigned int *); void ESCREVER_NUMREG (FILE *, unsigned int); unsigned int POS_ELIMINACAO (FILE *, unsigned int, char []); void DESLOCAR_PARA_CIMA (FILE *, unsigned int, unsigned int); /* obteno do nmero de registos actualmente armazenados */ LER_NUMREG (FP, &NREG); if (NREG == 0) { fprintf (stdout, "\nO ficheiro est vazio!\n"); return; } LER_TELEFONE (NUMTEL); /* leitura da chave de pesquisa */ /* determinao do ponto de eliminao */ PREG = POS_ELIMINACAO (FP, NREG, NUMTEL); if (PREG == NREG) { fprintf (stdout, "\nNo existe qualquer registo com esse "\ "nmero de telefone no ficheiro!\n"); return; } /* eliminao do registo */ if (PREG < NREG-1) DESLOCAR_PARA_CIMA (FP, NREG, PREG); /* actualizao do nmero de registos armazenados */ ESCREVER_NUMREG (FP, NREG-1); }

Figura 3.31 - Funo para eliminar um registo preexistente da agenda telefnica.

A Figura 3.32 apresenta o algoritmo em linguagem natural da funo LISTAR_FICHEIRO. Em primeiro lugar temos que verificar se existem de factos registos armazenados no ficheiro, o que feito pela funo LER_NUMREG. Depois preciso fazer a impresso um a um do contedo dos registos. Para isso temos a funo LER_REGISTO_FICHEIRO que l o registo do ficheiro e a funo ESCREVER_REGISTO_MONITOR que escreve o seu contedo no monitor. A Figura 3.33 apresenta a implementao da funo LISTAR_FICHEIRO. As funes auxiliares sero posteriormente apresentadas.

23

CAPTULO 3 : FICHEIROS EM C

nome: Listar a agenda telefnica begin Obteno do nmero de registos armazenados no ficheiro; se o ficheiro est vazio, ento sair da funo; para todos os registos existentes no ficheiro fazer begin Ler o registo do ficheiro; Escrever o contedo do registo no monitor; end end

Figura 3.32 - Algoritmo da listagem da agenda telefnica.


void LISTAR_FICHEIRO (FILE *FP) { TREG REGISTO; /* registo lido do ficheiro e escrito no monitor */ unsigned int NREG, /* nmero de registos armazenados */ REG; /* contador do ciclo for */ /* aluso a funes */ void LER_NUMREG (FILE *, unsigned int *); void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *); void ESCREVER_REGISTO_MONITOR (unsigned int, TREG); /* obteno do nmero de registos actualmente armazenados LER_NUMREG (FP, &NREG); if (NREG == 0) { fprintf (stdout, "\nO ficheiro est vazio!\n"); return; } /* impresso um a um do contedo dos registos fprintf (stdout, "\nImpresso do contedo dos registos\n"); for (REG = 0; REG < NREG; REG++) { /* leitura do contedo de um registo do ficheiro LER_REGISTO_FICHEIRO (FP, REG, &REGISTO); /* escrita no monitor do contedo de um registo ESCREVER_REGISTO_MONITOR (REG, REGISTO); } } */

*/

*/ */

Figura 3.33 - Funo para listar a agenda telefnica no monitor.

Vamos agora apresentar as funes auxiliares, que apareceram durante a descrio algortmica das trs operaes do programa principal. A Figura 3.34 apresenta a funo LER_REGISTO_TECLADO. Tem um parmetro de sada que o registo, cuja informao vai ser lida do teclado. A informao validada, de maneira a assegurar que o utilizador no se esquece de preencher qualquer campo do registo. Um nmero de telefone constitudo apenas por caracteres numricos, pelo que o formato de leitura impe essa limitao aos caracteres aceites. Por uma questo de simplificao da funo, a data no validada. A Figura 3.35 apresenta a funo ESCREVER_REGISTO_MONITOR. Tem dois parmetros de entrada que so o registo, cuja informao vai ser escrita no monitor, e o seu nmero de ordem no ficheiro. A Figura 3.36 apresenta a funo LER_TELEFONE. Tem um parmetro de sada que o nmero de telefone lido do teclado, que vai servir de chave de pesquisa para a eliminao do registo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

24

void LER_REGISTO_TECLADO (TREG *REG) { int ST; /* sinalizao do estado de leitura */ fprintf (stdout, "\nLeitura dos dados de um registo\n"); do { fprintf (stdout, "Nome? "); ST = fscanf (stdin, "%40[^\n]", REG->NOME); /* NMX-1 */ fscanf (stdin, "%*[^\n]"); fscanf (stdin, "%*c"); } while (ST != 1); do { fprintf (stdout, "Nmero de telefone? "); ST = fscanf (stdin, "%15[0123456789]", REG->NTELEF); /* TMX-1 */ fscanf (stdin, "%*[^\n]"); fscanf (stdin, "%*c"); } while (ST != 1); do { fprintf (stdout, "Data de aniversrio (DD-MM-AAAA)? "); ST = fscanf (stdin, "%2u-%2u-%4d", &(REG->ANIVERSARIO.DIA),\ &(REG->ANIVERSARIO.MES), &(REG->ANIVERSARIO.ANO)); fscanf (stdin, "%*[^\n]"); fscanf (stdin, "%*c"); } while (ST != 3); }

Figura 3.34 - Funo para ler os dados de um registo do teclado.


void ESCREVER_REGISTO_MONITOR (unsigned int NREG, TREG REG) { fprintf (stdout, "\nRegisto n%u\n", NREG); fprintf (stdout, "Nome: %s\n", REG.NOME); fprintf (stdout, "Nmero de telefone: %s\n", REG.NTELEF); fprintf (stdout, "Data de aniversrio: %02u-%02u-%04d\n",\ REG.ANIVERSARIO.DIA, REG.ANIVERSARIO.MES, REG.ANIVERSARIO.ANO); fprintf (stdout, "\nCarregue em Enter para continuar"); fscanf (stdin, "%*[^\n]"); fscanf (stdin, "%*c"); }

Figura 3.35 - Funo para escrever os dados de um registo no monitor.


void LER_TELEFONE (char NUMTELEF[]) { int ST; /* sinalizao do estado de leitura */ do { fprintf (stdout, "Nmero de telefone? "); ST = fscanf (stdin, "%15[0123456789]", NUMTELEF); fscanf (stdin, "%*[^\n]"); fscanf (stdin, "%*c"); } while (ST != 1); } /* TMX-1 */

Figura 3.36 - Funo para ler um nmero de telefone do teclado.

25

CAPTULO 3 : FICHEIROS EM C

A Figura 3.37 apresenta a funo POS_INSERCAO. Tem como parmetros de entrada o ficheiro, o nmero de registos armazenados e o registo. Tem como resultado de sada o ndice do registo do ficheiro onde o novo registo vai ser colocado. Para ler o registo do ficheiro, usa a funo LER_REGISTO_FICHEIRO e o processo de comparao usa a funo strcmp. A pesquisa termina quando se encontra um registo cujo nome seja alfabeticamente posterior ao nome do novo registo, o que implica que no caso de j existir um registo com esse nome, o novo registo ser colocado a seguir. Se o registo estiver alfabeticamente depois de todos os que j se encontram no ficheiro, ento a funo devolve o nmero de registos armazenados no ficheiro, ou seja, NR, como indicao que o novo registo dever ser colocado no fim do ficheiro.
unsigned int POS_INSERCAO (FILE *FP, unsigned int NR, TREG REG) { TREG REGAUX; /* registo auxiliar */ unsigned int R = 0; /* posio de insero determinada */ void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *); do { LER_REGISTO_FICHEIRO (FP, R, &REGAUX); if ( strcmp (REG.NOME, REGAUX.NOME) < 0 ) break; R++; } while (R < NR); return R; }

Figura 3.37 - Funo para determinar a posio de insero do registo.

A Figura 3.38 apresenta a funo POS_ELIMINACAO. Tem como parmetros de entrada o ficheiro, o nmero de registos armazenados e a chave de pesquisa. Tem como resultado de sada o ndice do registo do ficheiro onde se encontra o registo a ser eliminado. Mais uma vez o processo de comparao usa a funo strcmp. A pesquisa termina quando se encontra o primeiro registo cujo nmero de telefone igual ao nmero indicado. Se no existir, ento a funo devolve o nmero de registos armazenados no ficheiro, ou seja, NR, como indicao de registo inexistente no ficheiro.
unsigned int POS_ELIMINACAO (FILE *FP, unsigned int NR, char NTEL[]) { TREG REGAUX; /* registo auxiliar */ unsigned int R = 0; /* posio de eliminao determinada */ void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *); do { LER_REGISTO_FICHEIRO (FP, R, &REGAUX); if ( !strcmp (NTEL, REGAUX.NTELEF) ) break; R++; } while (R < NR); return R; }

Figura 3.38 - Funo para determinar a posio de eliminao do registo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

26

A insero do registo no ficheiro exige deslocar para baixo os registos do ficheiro a partir do ndice que foi detectado como local de insero do novo registo. A Figura 3.39 apresenta a funo DESLOCAR_PARA_BAIXO. Tem como parmetros de entrada o ficheiro, o nmero de registos armazenados e o ndice de insero do registo. Cada registo lido do ficheiro e escrito de novo no ficheiro mas uma posio mais abaixo. O deslocamento dos registos feito do fim do ficheiro at ao ndice de insero, como se exemplifica na parte esquerda da Figura 3.41.
void DESLOCAR_PARA_BAIXO (FILE *FP, unsigned int NR, unsigned int P) { TREG REGAUX; /* registo auxiliar */ unsigned int R; /* contador do ciclo for */ void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *); void ESCREVER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG); for (R = NR; R > P; R--) { LER_REGISTO_FICHEIRO (FP, R-1, &REGAUX); ESCREVER_REGISTO_FICHEIRO (FP, R, REGAUX); } }

Figura 3.39 - Funo para deslocar para baixo os registos do ficheiro.

A eliminao do registo no ficheiro exige deslocar para cima os registos do ficheiro a partir do ndice que foi detectado como local de eliminao do novo registo, a no ser que o registo seja o ltimo registo do ficheiro. A Figura 3.40 apresenta a funo DESLOCAR_PARA_ACIMA. Tem como parmetros de entrada o ficheiro, o nmero de registos armazenados e o ndice de eliminao do registo. Cada registo lido do ficheiro e escrito de novo no ficheiro mas uma posio mais acima. O deslocamento dos registos feito do ndice de eliminao at ao fim do ficheiro, como se exemplifica na parte direita da Figura 3.41.
void DESLOCAR_PARA_CIMA (FILE *FP, unsigned int NR, unsigned int P) { TREG REGAUX; /* registo auxiliar */ unsigned int R; /* contador do ciclo for */ void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *); void ESCREVER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG); for (R = P; R < NR-1; R++) { LER_REGISTO_FICHEIRO (FP, R+1, &REGAUX); ESCREVER_REGISTO_FICHEIRO (FP, R, REGAUX); } }

Figura 3.40 - Funo para deslocar para cima os registos do ficheiro.

O nmero de registos do ficheiro est armazenado no seu cabealho. Para determinar o nmero de registos, a funo LER_NUMREG, que se apresenta na Figura 3.42, limita-se a ler o cabealho. A funo tem como parmetro de entrada o ficheiro e como parmetro de sada o nmero de registos lido do cabealho do ficheiro. Para actualizar o nmero de registos, a funo ESCREVER_NUMREG, que se apresenta na Figura 3.43, escreve no cabealho o nmero actualizado de registos. A funo tem como parmetros de entrada o ficheiro e o nmero de registos a escrever no cabealho do ficheiro.

27

CAPTULO 3 : FICHEIROS EM C

deslocar para baixo

deslocar para cima

NR NR+1

ler escrever ler escrever ler escrever ler escrever

P
4 3 2

NR-1
1

NR

escrever ler escrever ler escrever ler escrever ler

1 2 3 4

Figura 3.41 - Visualizao grfica das operaes de deslocamento.

A funo fseek permite colocar o ponteiro de posio de leitura/escrita no incio do ficheiro. Aps a escrita do cabealho e uma vez que a sua actualizao feita aps a insero ou eliminao de um registo necessrio usar a funo fflush, para que o contedo do ficheiro seja imediatamente actualizado, ou seja, para assegurar que as alteraes acabadas de fazer esto visveis para a operao seguinte.
void LER_NUMREG (FILE *FP, unsigned int *NR) { if ( fseek (FP, 0, SEEK_SET) != 0 ) { perror ("erro no posicionamento do ficheiro - cabealho"); exit (EXIT_FAILURE); } if ( fread (NR, sizeof (unsigned int), 1, FP) != 1 ) { fprintf (stderr, "erro na leitura do cabealho do ficheiro\n"); exit (EXIT_FAILURE); } }

Figura 3.42 - Funo para ler o nmero de registos armazenado no cabealho do ficheiro.
void ESCREVER_NUMREG (FILE *FP, unsigned int NR) { if ( fseek (FP, 0, SEEK_SET) != 0 ) { perror ("erro no posicionamento do ficheiro - cabealho"); exit (EXIT_FAILURE); } if ( fwrite (&NR, sizeof (unsigned int), 1, FP) != 1 ) { fprintf (stderr, "erro na escrita do cabealho do ficheiro\n"); exit (EXIT_FAILURE); } if ( fflush (FP) != 0 ) { perror ("erro na escrita efectiva no ficheiro"); exit (EXIT_FAILURE); } }

Figura 3.43 - Funo para escrever o nmero de registos no cabealho do ficheiro.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

28

Para ler ou escrever um registo no ficheiro primeiro necessrio colocar o ponteiro de posio de leitura/escrita no registo com o ndice pretendido, usando a funo fseek. A Figura 3.44 apresenta a funo LER_REGISTO_FICHEIRO, que tem como parmetros de entrada o ficheiro e o nmero de ordem do registo a ler e como parmetro de sada o registo lido. A Figura 3.45 apresenta a funo ESCREVER_REGISTO_FICHEIRO, que tem como parmetros de entrada o ficheiro, o nmero de ordem do registo e o registo a escrever no ficheiro.
void LER_REGISTO_FICHEIRO (FILE *FP, unsigned int P, TREG *REG) { if (fseek (FP, sizeof(unsigned int)+P*sizeof(TREG), SEEK_SET)!=0) { perror ("erro no posicionamento do ficheiro - registo"); exit (EXIT_FAILURE); } if ( fread (REG, sizeof (TREG), 1, FP) != 1 ) { fprintf (stderr, "erro na leitura do registo do ficheiro\n"); exit (EXIT_FAILURE); } }

Figura 3.44 - Funo para ler um registo do ficheiro.


void ESCREVER_REGISTO_FICHEIRO (FILE *FP, unsigned int P, TREG REG) { if (fseek (FP, sizeof(unsigned int)+P*sizeof(TREG), SEEK_SET)!=0) { perror ("erro no posicionamento do ficheiro - registo"); exit (EXIT_FAILURE); } if (fwrite (&REG, sizeof (TREG), 1, FP) != 1) { fprintf (stderr, "erro na escrita do registo no ficheiro\n"); exit (EXIT_FAILURE); } }

Figura 3.45 - Funo para escrever um registo no ficheiro.

3.12 Leituras recomendadas


x 12 captulo do livro C A Software Approach, 3 edio, de Peter A. Darnell e Philip E. Margolis, da editora Springer-Verlag, 1996.

Captulo 4
RECURSIVIDADE

Sumrio
Neste captulo apresentamos exemplos da implementao de funes recursivas. Uma funo recursiva, ou recorrente, uma funo que se invoca a si prpria. Tal como a decomposio hierarquizada, a recursividade permite ao programador decompor um problema em problemas mais pequenos, que tm a particularidade de serem exactamente do mesmo tipo do problema original. O que no significa que os algoritmos recursivos sejam mais eficientes que os algoritmos iterativos equivalentes. Pelo contrrio, alguns algoritmos recursivos desencadeiam um nmero arbitrariamente grande de invocaes sucessivas da funo, pelo que, nestes casos temos uma ineficincia acrescida associada invocao de funes. Por outro lado, alguns algoritmos recursivos so computacionalmente ineficientes, devido ao facto de as mltiplas invocaes recursivas repetirem o clculo de valores. No entanto, os algoritmos recursivos so apropriados para resolver problemas que so por natureza recursivos. Por vezes, existem problemas que tm solues recursivas que so simples, concisas, elegantes e para os quais difcil esboar solues iterativas com a mesma simplicidade e clareza. Mas, como alguns algoritmos recursivos so menos eficientes que os algoritmos iterativos equivalentes, a verdadeira importncia da recursividade consiste em resolver esses problemas para os quais no existem solues iterativas simples. Vamos analisar alguns exemplos clssicos de algoritmos recursivos, com excepo de algoritmos de ordenao recursivos, que so algoritmos de ordenao muito eficientes, mas que sero tratados mais tarde no captulo de Pesquisa e Ordenao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

4.1 Funes recursivas


Tipicamente, a soluo de um problema repetitivo pode ser resolvido de forma iterativa utilizando explicitamente um ciclo repetitivo, ou de forma recursiva utilizando a invocao sucessiva da soluo. A linguagem C, tal como outras linguagens de programao de alto nvel, permite a definio de funes recursivas. Uma funo recursiva, ou recorrente, uma funo que definida em termos de si prprio, ou seja, a funo invoca-se a si prpria na parte executiva. Este tipo de recursividade, ou recorrncia, designa-se por recursividade directa. A grande vantagem da criao de uma funo recursiva, utilizando para o efeito um algoritmo recursivo, o de permitir desencadear um nmero arbitrariamente grande de repeties de instrues, sem contudo, usar explicitamente estruturas de controlo repetitivo. O que no significa que os algoritmos recursivos sejam mais eficientes que os algoritmos iterativos equivalentes. Tal como a decomposio hierarquizada, a recursividade permite ao programador decompor um problema em problemas mais pequenos, que tm a particularidade de serem exactamente do mesmo tipo do problema original. Portanto, um algoritmo recursivo resolve o problema resolvendo uma instncia menor do mesmo problema. Consequentemente, a complexidade do problema ser diminuda at que a soluo seja bvia ou conhecida. Os problemas que podem ser resolvidos recursivamente tm normalmente as seguintes caractersticas. Um ou mais casos de paragem, em que a soluo no recursiva e conhecida, e casos em que o problema pode ser diminudo recursivamente at se atingirem os casos de paragem. Portanto, quando se implementa uma funo recursiva temos que assegurar que existe uma instruo que para a invocao recursiva e que calcula efectivamente um valor, e por outro lado, que o valor das sucessivas invocaes alterado de maneira a atingir esse valor de paragem. Usualmente um algoritmo recursivo usa uma instruo condicional if com a forma que se apresenta na Figura 4.1.
nome: Algoritmo recursivo begin se o caso de paragem atingido? ento Resolver o problema seno Partir o problema em problemas mais pequenos, usando para esse efeito, uma ou mais invocaes recursivas end

Figura 4.1 - Soluo genrica de um algoritmo recursivo.

Devemos ter sempre presente, que a invocao de uma funo, produz um desperdcio de tempo e de memria devido criao de uma cpia local dos parmetros de entrada que so passados por valor, bem como na salvaguarda do estado do programa na altura da invocao, na memria de tipo pilha (stack), para que, o programa possa retomar a execuo na instruo seguinte invocao da funo, quando a execuo da funo terminar. Este problema ainda mais relevante no caso de funes recursivas, principalmente quando existem mltiplas invocaes recursivas. Geralmente, quando existem solues recursivas e iterativas para o mesmo problema, a soluo recursiva requer mais tempo de execuo e mais espao de memria devido s invocaes extras da funo.

CAPTULO 4 : RECURSIVIDADE

4.2 Clculo do factorial


Vamos considerar o problema do clculo do factorial. A soluo iterativa, cuja funo se apresenta na Figura 4.2, decorre directamente da definio n! = n (n1) (n2) ... 2 1, e calcula o produto acumulado de forma repetitiva para a varivel auxiliar FACT, utilizando explicitamente um ciclo repetitivo for.
unsigned int FACTORIAL (int N) { int I, FACT = 1; if (N < 0) return 0; return FACT; } /* invocao anormal (valor devolvido 0) */ /* clculo do produtrio */ /* devoluo do valor calculado */ for (I = 2; I <= N; I++) FACT *= I;

Figura 4.2 - Funo factorial implementada de forma iterativa.

Mas, se tivermos em conta que n! = n (n1)!, sendo que 0! = 1, ento a funo factorial pode ser implementada de forma recursiva, tal como se apresenta na Figura 4.3.
unsigned int FACTORIAL (int N) { if (N < 0) return 0; /* invocao anormal (valor devolvido 0) */ if (N == 0) return 1; /* condio de paragem */ return N * FACTORIAL (N-1); /* invocao recursiva */ }

Figura 4.3 - Funo factorial implementada de forma recursiva.

A primeira condio de teste evita que a funo entre num processo recursivo infinito, caso a funo seja invocada para um valor de N negativo. Uma das instrues da funo factorial recursiva a invocao recursiva, quando a funo se invoca a si prpria. Para cada nova invocao, o valor do factorial a calcular diminudo de uma unidade, at que acabar por atingir o valor zero. Nesse caso, a funo no se invoca mais e retorna o valor 1. Assim temos que N ser igual a zero, funciona como condio de paragem da recursividade. Quando a recursividade termina, ento calculado o valor final da funo, no retorno das sucessivas invocaes da funo. A Figura 4.4 representa a anlise grfica da invocao da funo factorial recursiva para o clculo de 3!. Cada caixa representa o espao de execuo de uma nova invocao da funo. Se considerarmos cada caixa como um n estamos perante o que se designa por rvore de recorrncia da funo. Temos que 3! = 3 2!, por sua vez 2! = 2 1!, por sua vez 1! = 1 0!, e finalmente a invocao recursiva termina com a condio de paragem, sendo 0! = 1. O valor de 3! calculado no retorno das sucessivas invocaes da funo recursiva, pelo que, 3! = 1 1 2 3 = 6. A implementao recursiva no tem qualquer vantagem sobre a implementao repetitiva. Mas, tem a desvantagem associada invocao sucessiva de funes, que o tempo gasto na invocao de uma funo e o eventual esgotamento da memria de tipo pilha. Apesar da implementao recursiva no usar variveis locais, no entanto, gasta mais memria porque a funo tem de fazer a cpia do parmetro de entrada que passado por valor. Pelo que, a verso iterativa mais eficiente e portanto, deve ser usada em detrimento da verso recursiva. O maior factorial que se consegue calcular em aritmtica inteira de 32 bits o 12! que igual a 479001600.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

printf ("%d", FACTORIAL(3)); 6

FACTORIAL(3) = 3 * FACTORIAL(2) 3 * 2

FACTORIAL(2) = 2 * FACTORIAL(1) 2 * 1

FACTORIAL(1) = 1 * FACTORIAL(0) 1 * 1

FACTORIAL(0) = 1

Figura 4.4 - Visualizao grfica da invocao da funo factorial recursiva.

4.3 Expanso em srie de Taylor


No computador as funes trigonomtricas e logartmicas so implementadas atravs da expanso em srie de Taylor. O termo geral da expanso em srie de Taylor da funo seno dado pela seguinte expresso esquerda, sendo apresentada direita a sua expanso para os primeiros cinco termos da srie.
seno( x, N )

 1 n u 2n  1 ! n 0

N 1

x 2 n 1

seno( x,5 )

x 

x3 x5 x7 x9 ...    3! 5! 7! 9!

Como bem sabido, a funo factorial explode rapidamente, assumindo valores de tal maneira elevados que s podem ser armazenados em variveis de tipo real, com a consequente perda de preciso. Pelo que, uma forma de resolver o problema, passa por eliminar a necessidade do clculo do factorial recorrendo a um processo recorrente. Uma vez que se trata de uma srie geomtrica, poderamos calcular cada elemento da srie a partir do elemento anterior e adicionar os elementos calculados. Mas, se aplicarmos de um modo sistemtico a propriedade distributiva da multiplicao relativamente adio, obtemos uma expresso, em que o clculo do factorial desaparece.
seno( x, N ) x2 x2 x2 x2 u 1  u 1  u 1  u ... x u 1  2u3 4u5 6 u 7 8 u 9

Esta expresso pode ser calculada de duas maneiras. Podemos calcul-la de baixo para cima (bottom-up), ou seja, do termo P4 para o termo P1, atravs de um processo iterativo, ou em alternativa podemos calcular a expresso de cima para baixo (top-down), ou seja, do termo P1 para o termo P4, atravs de um processo recursivo. Vamos comear por analisar a implementao iterativa. Neste caso vamos calcular de forma repetitiva a expresso entre parntesis da direita para a esquerda, ou seja, do termo P4 at ao termo P1, utilizando o passo iterativo dado pela seguinte expresso.

CAPTULO 4 : RECURSIVIDADE

Pi 1

1

x2 u Pi 2 u i u 2 u i  1

Como estamos perante o clculo de um produto acumulado, Pi tem de ser inicializado a 1.0, que o elemento neutro do produto. Depois do clculo repetitivo, temos que o valor final do seno seno( x, N ) x u P1 . A Figura 4.5 apresenta a funo iterativa.
double SENO_TAYLOR_ITERATIVO (double X, int N) { int I; double TAYLOR = 1.0; for (I = N; I > 0; I--) TAYLOR = 1 - X*X / (2*I * (2*I+1)) * TAYLOR; return X * TAYLOR; }

Figura 4.5 - Funo iterativa que calcula a expanso em srie de Taylor da funo seno.

Vamos agora analisar a implementao recursiva. Neste caso vamos calcular a expresso entre parntesis da esquerda para a direita, ou seja, do termo P1 at ao termo P4, sendo que, cada termo calculado em funo do termo seguinte ainda por calcular. Da a necessidade do processo ser recursivo. O passo recursivo dado pela seguinte expresso. Seno( x, N , i ) 1 x2 u Seno( x, N , i  1) 2 u i u 2 u i  1

O processo recursivo terminado quando o termo a calcular o ltimo termo desejado, ou seja, quando i igual a N, cujo valor dado pela seguinte expresso.

Seno( x, N , N )

1

x2 2 u N u 2 u N  1

Caso o nmero de termos pretendidos na expanso seja apenas um, o valor final do seno x e no h a necessidade de invocar o clculo recursivo. Caso, o nmero de termos seja maior do que um, ento o valor final do seno igual a x vezes o valor obtido pelo clculo recursivo do primeiro elemento, ou seja, seno( x, N ) x u Seno( x, N ,1) . Pelo que, para implementar o clculo de forma recursiva precisamos de decompor a soluo em duas funes, tal como se apresenta na Figura 4.6. Uma a funo recursiva propriamente dita que implementa a expanso em srie de Taylor, e a outra a funo que calcula o valor final e que invoca, caso seja necessrio, a funo recursiva.
double TAYLOR (double X, int N, int I) { /* condio de paragem */ if (I == N) return 1 - X*X / (2*N * (2*N+1)); return 1 - X*X / (2*I * (2*I+1)) * TAYLOR (X, N, I+1); } double SENO_TAYLOR_RECURSIVO (double X, int N) { if (N == 1) return X; return X * TAYLOR (X, N, 1); }

Figura 4.6 - Funo recursiva que calcula a expanso em srie de Taylor da funo seno.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

Neste exemplo, a verso recursiva para alm do desperdcio de tempo gasto com a invocao sucessiva da funo, tem uma implementao mais extensa em termos de cdigo, uma vez que a soluo tem de ser decomposta em duas funes. Pelo que, a verso iterativa mais eficiente e simples de programar.

4.4 Nmeros de Fibonacci


Um exemplo de clculo recursivo mais complexo do que o clculo do factorial o clculo dos nmeros de Fibonacci, que so definidos pela seguinte relao de recorrncia e cuja implementao recursiva se apresenta na Figura 4.7.
Fibonacci( n) 0, se n 0 1, se n 1 Fibonacci( n - 1)  Fibonacci( n - 2), se n t 2

unsigned int FIBONACCI (int N) { if (N <= 0) return 0; /* Fibonacci de 0 */ if (N <= 2) return 1; /* Fibonacci de 1 e de 2 */ return FIBONACCI (N-1) + FIBONACCI (N-2); /* Fibonacci de n > 2 */ }

Figura 4.7 - Funo que calcula os nmeros de Fibonacci de forma recursiva.

A Figura 4.8 apresenta a rvore de recorrncia do clculo do Fibonacci de 5.


FIBONACCI(5) FIB(5) = FIB(4)+FIB(3)

FIBONACCI(4) FIB(4) = FIB(3)+FIB(2)

FIBONACCI(3) FIB(3) = FIB(2)+FIB(1)

FIBONACCI(3) FIB(3) = FIB(2)+FIB(1)

FIBONACCI(2) FIB(2) = FIB(1)+FIB(0)

FIBONACCI(2) FIB(2) = FIB(1)+FIB(0)

FIBONACCI(1) FIB(1) = 1

FIBONACCI(2) FIB(2) = FIB(1)+FIB(0)

FIBONACCI(1) FIB(1) = 1

FIBONACCI(1) FIB(1) = 1

FIBONACCI(0) FIB(0) = 0

FIBONACCI(1) FIB(1) = 1

FIBONACCI(0) FIB(0) = 0

FIBONACCI(1) FIB(1) = 1

FIBONACCI(0) FIB(0) = 0

Figura 4.8 - Visualizao grfica da invocao da funo Fibonacci recursiva.

Como se pode ver, esta soluo computacionalmente ineficiente, porque para calcular o Fibonacci de 5, calcula repetidamente alguns valores intermdios. Mais concretamente, duas vezes o Fibonacci de 3, trs vezes o Fibonacci de 2, cinco vezes o Fibonacci de 1 e trs vezes o Fibonacci de 0. Por outro lado, devido dupla invocao recursiva o nmero de operaes explode rapidamente. Como se pode ver na Figura 4.8, o clculo do Fibonacci de 5 custa 7 adies. Para se calcular o Fibonacci de 6, temos que calcular o Fibonacci de 5 e o Fibonacci de 4, o que d um total de 12 adies. O Fibonacci de 7 custa 20 adies, o Fibonacci de 8 custa 33 adies. Ou seja, o nmero de adies quase que duplica quando se incrementa o argumento da funo de uma unidade. Pelo que, o tempo de execuo desta funo praticamente exponencial.

CAPTULO 4 : RECURSIVIDADE

Uma forma de resolver problemas recursivos de maneira a evitar o clculo repetido de valores consiste em calcular os valores de baixo para cima e utilizar um agregado para manter os valores entretanto calculados. Este mtodo designa-se por programao dinmica e reduz o tempo de clculo custa da utilizao de mais memria para armazenar valores internos. Esta soluo apresentada na Figura 4.9, sendo o clculo efectuado do Fibonacci de 0 at ao Fibonacci de N. Os trs primeiros valores, ou seja, o Fibonacci de 0, de 1 e de 2, so colocados na inicializao do agregado.
unsigned int FIBONACCI (int N) { int I; unsigned int FIB[50] = {0,1,1}; if (N <= 0) return 0; if (N <= 2) return 1; /* Fibonacci de 0 */ /* Fibonacci de 1 e de 2 */

for (I = 3; I <= N; I++) /* para calcular o Fibonacci de n > 2 */ FIB[I] = FIB[I-1] + FIB[I-2]; return FIB[N]; }

Figura 4.9 - Funo que calcula os nmeros de Fibonacci de forma dinmica.

Mas, se repararmos bem na soluo dinmica verificamos que de facto a clculo do Fibonacci de N s precisa dos valores do Fibonacci de N-1 e do Fibonacci de N-2, pelo que, podemos substituir a utilizao do agregado por apenas trs variveis simples. Uma para armazenar o valor a calcular, que designamos por PROXIMO, outra para armazenar o valor acabado de calcular, que designamos por ACTUAL e outra para armazenar o valor calculado anteriormente, que designamos por ANTERIOR. A Figura 4.10 apresenta esta verso iterativa. O valor inicial de ANTERIOR 0 e corresponde ao Fibonacci de 0, o valor inicial de ACTUAL 1 e corresponde ao Fibonacci de 1 e o valor de PROXIMO corresponde ao Fibonacci que se pretende calcular de forma iterada, e igual soma do ANTERIOR com o ACTUAL.
unsigned int FIBONACCI (int N) { int I; unsigned int ANTERIOR = 0, ACTUAL = 1, PROXIMO; if (N <= 0) if (N == 1) return 0; return 1; /* Fibonacci de 0 */ /* Fibonacci de 1 */ /* Fibonacci de n >= 2 */

for (I = 2; I <= N; I++) { PROXIMO = ACTUAL + ANTERIOR; ANTERIOR = ACTUAL; ACTUAL = PROXIMO; } return PROXIMO; }

Figura 4.10 - Funo que calcula os nmeros de Fibonacci de forma iterativa.

Esta soluo iterativa a melhor das trs solues. a mais rpida e a que gasta menos memria. O tempo de execuo desta soluo linear, ou seja, o clculo do Fibonacci de 2N custa aproximadamente o dobro do tempo do clculo do Fibonacci de N. O maior nmero de Fibonacci que se consegue calcular em aritmtica inteira de 32 bits o Fibonacci de 47 que igual a 2971215073.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

4.5 Clculo dos coeficientes binomiais


O coeficiente binomial C(n,k), que representa o nmero de combinaes de n elementos a k elementos dado pela seguinte expresso. C(n, k) n! (n - k)! k!

Em muitas situaes, mesmo para valores finais representveis em variveis inteiras, os valores parcelares podem ficar corrompidos, uma vez que o factorial rapidamente d overflow em aritmtica inteira. Por exemplo, duas situaes limites so o C(n,0) e o C(n,n), em que o nmero de combinaes igual a 1 (n!/n!). Pelo que, a utilizao da definio para calcular C(n,k) est normalmente fora de questo e neste caso nem sequer podemos recorrer a aritmtica real, devido consequente perda de preciso. Uma forma de resolver o problema, passa por eliminar a necessidade do clculo do factorial recorrendo a um processo iterativo, descoberto por Blaise Pascal e que conhecido pelo Tringulo de Pascal. Como se pode ver na Figura 4.11, cada elemento com excepo dos elementos terminais de cada linha, ou seja C(n,0) e C(n,n), calculado atravs da soma dos valores da linha anterior. Ou seja, C(n,k) = C(n-1,k) + C(n-1,k-1), com n>k>0. A Figura 4.11 representa de forma grfica o clculo de C(5,3) que igual a 10. Repare que existem valores que so usados duas vezes, o que significa que tambm vo ser calculados duas vezes.
1 1 1 1 1 1 5 4 10 3 6 10 2 3 4 5 1 1 1 1 1

Figura 4.11 - Tringulo de Pascal.

A funo recursiva para calcular o coeficiente binomial, que se apresenta na Figura 4.12, invoca-se recursivamente duas vezes e tem como condies de paragem os elementos terminais, cujo valor 1. A primeira condio de teste evita que a funo entre num processo recursivo infinito, caso a funo seja invocada para k menor do que n.
unsigned int COMBINACOES (int N, int K) { if (K > N) return 0; /* invocao anormal (valor devolvido 0) */ if ((K == 0) || (K == N)) return 1; /* condies de paragem */ return COMBINACOES (N-1, K) + COMBINACOES (N-1, K-1); }

Figura 4.12 - Funo recursiva para calcular os coeficientes binomiais.

CAPTULO 4 : RECURSIVIDADE

Esta implementao no entanto pouco prtica para valores de n e k elevados, antes de mais porque, tal como no caso dos nmeros de Fibonacci, repete o clculo de certos valores e portanto, computacionalmente ineficiente. Mas, o grande problema deste algoritmo que devido dupla invocao recursiva a invocao tambm explode rapidamente. A soluo dinmica, que se apresenta na Figura 4.13, implica a utilizao de um agregado bidimensional para armazenar os coeficientes binomiais, medida que estes so calculados.
unsigned int COMBINACOES (int N, int K) { int I, J, MIN; unsigned int BICOEF[36][36]; if (K > N) return 0; /* invocao anormal (valor devolvido 0) */ if ((K == 0) || (K == N)) return 1; /* C(n,0) = C(n,n) = 1 */ for (I = 0; I <= N; I++) { MIN = (I <= K) ? I : K; /* mnimo de I e K */ for (J = 0; J <= MIN; J++) if ( (J == 0) || (J == I) ) BICOEF[I][J] = 1; else BICOEF[I][J] = BICOEF[I-1][J-1] + BICOEF[I-1][J]; } return BICOEF[N][K]; }

Figura 4.13 - Funo dinmica para calcular os coeficientes binomiais.

Enquanto que a soluo recursiva calcula 2C(n,k)1 termos, a soluo dinmica calcula aproximadamente, por defeito, NK termos. Os maiores coeficientes binomiais que se conseguem calcular em aritmtica inteira de 32 bits so o C(35,16) e o C(35,19), que so iguais a 4059928950.

4.6 Clculo das permutaes


Consideremos agora que se pretende imprimir todas as permutaes de um conjunto de N caracteres. Por exemplo, se o conjunto de caracteres for {a,b,c}, ento o conjunto das permutaes { (a,b,c), (a,c,b), (b,a,c), (b,c,a), (c,a,b), (c,b,a) }, ou seja, existem N! permutaes. Podemos obter um algoritmo simples para gerar todas as permutaes de um conjunto de caracteres, se construirmos um algoritmo recursivo. Por exemplo, as permutaes do conjunto {a,b,c,d} so os quatro seguintes grupos de permutaes: a seguido das permutaes do conjunto {b,c,d}; b seguido das permutaes do conjunto {a,c,d}; c seguido das permutaes do conjunto {b,a,d}; e d seguido das permutaes do conjunto {b,c,a}. ento possvel resolver o problema para N caracteres, se tivermos um algoritmo recursivo que funcione para N1 caracteres. Em cada passo do processo coloca-se um carcter esquerda do conjunto e calculam-se recursivamente as permutaes dos N 1 restantes caracteres direita. Depois troca-se sucessivamente o carcter da esquerda com um dos caracteres da direita, de forma a ter experimentado todos os N grupos possveis de permutaes dos N1 caracteres. Durante o processo recursivo, quando se tiver atingido o carcter mais direita, imprime-se a sequncia de permutaes acabada de gerar.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

A Figura 4.14 apresenta a funo de clculo das permutaes. Assume-se que LISTA uma cadeia de N caracteres terminada com o carcter nulo, de maneira a simplificar a sua escrita no monitor. Para trocar os caracteres utiliza-se a funo TROCA.
void TROCA (char *CAR_I, char *CAR_J) { char TEMP; TEMP = *CAR_I; *CAR_I = *CAR_J; *CAR_J = TEMP; } void PERMUTACOES (char LISTA[], int I, int N) { int J; if (I == N) /* condio de paragem printf ("%s\n", LISTA[J]); /* imprimir a permutao gerada else for (J = I; J <= N; J++) /* para todos os caracteres { TROCA (&LISTA[I], &LISTA[J]); /* por o carcter direita PERMUTACOES (LISTA, I+1, N);/* permutar os n-1 caracteres TROCA (&LISTA[J], &LISTA[I]);/* repor o carcter no stio } } */ */ */ */ */ */

Figura 4.14 - Funo recursiva que gera as permutaes de um conjunto de caracteres.

A Figura 4.15 mostra a utilizao da funo. A invocao inicial da funo PERMUTACOES (LISTA, 0, N1);, para permutar todos os caracteres que se encontram na cadeia de caracteres LISTA, sendo que o primeiro carcter est na posio 0 e o ltimo carcter est na posio N1.
#include <stdio.h> #include <string.h> void PERMUTACOES (char [], unsigned int, unsigned int); int main (void) { char LISTA[11]; unsigned int K;

/* cadeia de caracteres LISTA */ /* nmero de caracteres de LISTA */

printf ("Caracteres a permutar -> "); scanf ("%10s", LISTA); /* leitura da cadeia de caracteres LISTA */ K = strlen (LISTA); /* determinao do nmero de caracteres */ printf ("Permutaes\n\n"); PERMUTACOES (LISTA, 0, K-1); return 0; } void TROCA (char *CAR_I, char *CAR_J) ... void PERMUTACOES (char LISTA[], unsigned int I, unsigned int N) ...

Figura 4.15 - Programa que utiliza a funo que gera as permutaes.

11

CAPTULO 4 : RECURSIVIDADE

4.7 Clculo do determinante de uma matriz quadrada


O clculo do determinante de uma matriz com dimenso elevada normalmente complexo, a no ser que a matriz seja uma matriz diagonal. Pelo que, uma maneira de simplificar o clculo consiste em transformar a matriz, numa matriz diagonal. Esta transformao da matriz pode ser feita recursivamente, atravs de operaes de adio e subtraco de colunas da matriz, operaes essas que no afectam o valor do seu determinante. A Figura 4.16 apresenta o algoritmo em pseudocdigo e linguagem natural do clculo recursivo do determinante de uma matriz quadrada de dimenso N. Quando o elemento da ltima linha e ltima coluna da matriz nulo a coluna tem de ser trocada com outra coluna, cujo ltimo elemento no seja nulo, de forma a colocar um valor no nulo na diagonal. Caso no haja nenhuma coluna nessa situao, ento sinal que todos os elementos da ltima linha so nulos, pelo que, o determinante nulo. Sempre que se trocam duas colunas de uma matriz o determinante tem de ser multiplicado por 1. Ao dividir-se a ltima coluna pelo ltimo elemento pe-se em evidncia esse elemento e depois para anular a ltima linha da matriz, basta subtrair todas as colunas menos a ltima, pela ltima coluna multiplicada pelo ltimo elemento da coluna a processar. Uma vez que, depois deste processamento a ltima linha da matriz constituda apenas por zeros, com excepo do ltimo elemento da linha, ou seja, o elemento que est na diagonal, agora o determinante desta matriz igual a este valor multiplicado pelo determinante da matriz de ordem N1. O processo invocado recursivamente at N ser igual a 1. Nessa altura o determinante o prprio valor. Em alternativa pode-se parar o processo recursivo quando N igual a 2, uma vez que fcil calcular o determinante dessa matriz.
nome: Calculo do determinante (MATRIZ, N) Funo begin if (N = 1) then Calculo do determinante := MATRIZ[1,1] else begin if (Matriz[N,N] = 0) then if existe coluna com ltimo elemento diferente de zero? then Trocar essa coluna com a ltima e multiplicar o determinante por -1 else Calculo do determinante := 0; if (Matriz[N,N] <> 0) then begin Dividir a ltima coluna pelo ltimo elemento pondo-o em evidncia; Subtrair todas as colunas menos a ltima, pela ltima coluna multiplicada pelo ltimo elemento da coluna a processar; Calculo do determinante := Matriz[N,N] * Calculo do determinante (MATRIZ, N-1); end end end

Figura 4.16 - Algoritmo do clculo recursivo do determinante.

A Figura 4.17 apresenta a funo e a Figura 4.18 apresenta a aplicao da funo sobre uma matriz quadrada de 44 e as sucessivas matrizes, equivalentes para efeito do clculo do determinante, que vo sendo obtidas aps cada invocao recursiva. Quando se atinge a matriz de um s elemento, a matriz original foi completamente transformada numa matriz diagonal. Agora o determinante calculado no retorno das sucessivas invocaes da funo recursiva e igual ao produto dos elementos na diagonal. Logo, o valor do determinante 2.0 -1.0 -1.5 5.0, ou seja, igual a 15.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

#define NMAX 10 ... double CALC_DETERMINANTE (double MATRIZ[][NMAX], unsigned int N) { int COL_AUX, NC, NL, UE = N-1; double ELEMENTO; if (N == 1) return MATRIZ[0][0]; /* condio de paragem */ else { COL_AUX = UE; /* procurar coluna com ltimo elemento 0 */ while ( (COL_AUX >= 0) && (MATRIZ[UE][COL_AUX] == 0) ) COL_AUX--; if (COL_AUX >= 0) /* se existir tal coluna */ { if (COL_AUX != UE) /* se no for a ltima coluna */ for (NL = 0; NL < N; NL++) /* trocar as colunas */ { ELEMENTO = MATRIZ[NL][UE]; MATRIZ[NL][UE] = MATRIZ[NL][COL_AUX]; MATRIZ[NL][COL_AUX] = -ELEMENTO; } /* dividir a coluna N-1 pelo ltimo elemento pondo-o em evidncia */ for (NL = 0; NL < UE; NL++) MATRIZ[NL][UE] /= MATRIZ[UE][UE]; /* subtrair todas as colunas menos a ltima pela ltima coluna */ /* multiplicada pelo ltimo elemento da coluna a processar */ for (NC = 0; NC < UE; NC++) for (NL = 0; NL < UE; NL++) MATRIZ[NL][NC] -= MATRIZ[NL][UE] * MATRIZ[UE][NC]; /* invocao recursiva para a matriz de dimenso N-1 */ return MATRIZ[UE][UE] * CALC_DETERMINANTE (MATRIZ, N-1); } else return 0; } }

Figura 4.17 - Funo recursiva que calcula o determinante de uma matriz quadrada.

3.0 4.0 1.0 0.0

4.0 2.0 3.0 5.0

2.0 2.0 2.0 3.0 0.0 0.0 0.0

5.0 1.0 2.0 2.0 0.0 0.0 0.0 2.0 =>

3.0 -8.5 -5.5 4.0 -0.5 0.5 1.0 -2.0 -1.0 0.0 0.0 0.0 5.0 0.0 0.0 -1.5 0.0 0.0 0.0 0.0 0.0 0.0

0.0 0.0 0.0 2.0 0.0 0.0 0.0 2.0

-2.5 2.5 4.5 -1.5 0.0 0.0 0.0

0.0 -1.0

=>

0.0 -1.0

Figura 4.18 - Execuo da funo recursiva que calcula o determinante de uma matriz quadrada.

13

CAPTULO 4 : RECURSIVIDADE

4.8 Torres de Hani


Vamos agora apresentar um problema cuja nica soluo conhecida a soluo recursiva. As Torres de Hani o puzzle que se apresenta na primeira linha da Figura 4.19. Temos um conjunto de discos todos de tamanho diferente enfiados na Torre A. Pretende-se mud-los para a Torre B, mas, s se pode mudar um disco de cada vez, s se pode mudar o disco de cima e nunca se pode colocar um disco sobre outro mais pequeno. Para efectuarmos a mudana podemos usar a Torre C como torre auxiliar.

Torre A

Torre B

Torre C

Torre A

Torre B

Torre C

Torre A

Torre B

Torre C

Torre A

Torre B

Torre C

Figura 4.19 - Torres de Hani.

A soluo para este problema trivial caso o nmero de discos seja um. Nesse caso basta mud-lo da Torre A para a Torre B. No entanto se tivermos mais do que um disco a soluo no bvia e at bastante dispendiosa medida que o nmero de discos aumenta. A soluo que se apresenta na Figura 4.20 a soluo recursiva que resolve o puzzle para o caso de N discos colocados na Torre A. Consiste em diminuir a complexidade do problema at situao em que temos apenas um disco e para o qual conhecemos a soluo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

Assim para o caso apresentado na Figura 4.19 em que temos quatro discos na Torre A, a soluo passa por movimentar os trs discos de cima da Torre A para a Torre C, depois mudar o quarto disco da Torre A para a Torre B e finalmente mudar os trs discos que se encontram na Torre C para a Torre B.
nome: Torres de Hanoi (N, TORRE_A, TORRE_B, TORRE_C) Procedimento begin if (N = 1) then Mover o disco da TORRE_A para a TORRE_B else begin Torres de Hanoi (N-1, TORRE_A, TORRE_C, TORRE_B); Torres de Hanoi (1, TORRE_A, TORRE_B, TORRE_C); Torres de Hanoi (N-1, TORRE_C, TORRE_B, TORRE_A); end end

Figura 4.20 - Algoritmo das Torres de Hani.

Obviamente que esta soluo funciona se for aplicada recursivamente, porque, resolver o problema de mudar os trs primeiros discos da Torre A para a Torre C, resolve-se da mesma forma, s que agora a torre de chegada a Torre C, a torre auxiliar a Torre B e o nmero de discos a mudar menos um do que no problema inicial. A soluo passa por mudar os dois discos de cima da Torre A para a Torre B, depois mudar o terceiro disco da Torre A para a Torre C e finalmente mudar os dois discos que se encontram na Torre B para a Torre C. Assim estamos perante o problema de mudar dois discos da Torre A para a Torre B, o que implica mudar o primeiro disco da Torre A para a Torre C, o segundo disco da Torre A para a Torre B e finalmente o primeiro disco da Torre C para a Torre B. Aps termos mudado os trs primeiros discos da Torre A para a Torre C e termos atingido a situao apresentada na terceira linha da Figura 4.19, ento para mudar os trs discos que se encontram na Torre C para a Torre B aplica-se de novo a soluo recursiva, mas agora a torre de partida a Torre C, a torre de chegada a Torre B e a torre auxiliar a Torre A. A Figura 4.21 e a Figura 4.22 apresentam o programa que simula as Torres de Hani. Para implementar as trs torres utilizam-se trs agregados, sendo os discos representados por nmeros inteiros de 1 a N. O programa principal l do teclado o nmero de discos, valida o seu valor, constri a situao inicial, imprime-a no monitor e depois invoca a funo para simular a mudana dos discos. A funo INICIALIZAR coloca os N discos na Torre A e nenhum disco na Torre B e na Torre C. A funo MUDAR_DISCOS implementa o algoritmo recursivo e aps cada mudana de um disco invoca a funo IMPRIMIR. A funo IMPRIMIR vai imprimir no monitor o estado das torres, no incio e aps cada mudana de um disco. A funo no tem parmetros, porque utiliza variveis globais. Uma vez que o algoritmo recursivo, em cada nova invocao as torres vo mudando de posio, pelo que, a nica forma de poder imprimir o estado das torres A, B e C, usando variveis globais para as torres e para os contadores do nmero de discos armazenados em cada torre. Para declarar variveis globais, elas tm de ser declaradas antes da funo main com o qualificativo static.

15

CAPTULO 4 : RECURSIVIDADE

#include <stdio.h> #define D_MAX 10 /* agregados para as torres */ static int TORREA[D_MAX], TORREB[D_MAX], TORREC[D_MAX]; static int NDA, NDB, NDC; /* nmero de discos de cada torre */ void INICIALIZAR (int, int [], int *, int [], int *, int [], int *); void IMPRIMIR (void); void MUDAR_DISCOS (int, int [], int *, int [], int *, int [], int *); int main (void) { int NDISCOS; do { printf ("Numero de discos = "); scanf ("%d", &NDISCOS); } while ( (NDISCOS <= 0) || (NDISCOS > DISCOS_MAX) ); INICIALIZAR (NDISCOS, TORREA, &NDA, TORREB, &NDB, TORREC, &NDC); printf ("---------------------------------\n"); printf ("| Torres de Hanoi |\n"); printf ("| Numero de discos = %2d |\n", NDISCOS); printf ("---------------------------------\n"); printf ("| TORRE A TORRE B TORRE C |\n"); printf ("---------------------------------\n"); IMPRIMIR (); MUDAR_DISCOS (NDISCOS, TORREA, &NDA, TORREB, &NDB, TORREC, &NDC); return 0; } void INICIALIZAR (int ND, int TA[], int *NDA, int TB[], int *NDB,\ int TC[], int *NDC); { int I; for (I = 0; I < D_MAX; I++) { TA[I] = 0; TB[I] = 0; TC[I] = 0; /* limpar os agregados */ } for (I = 0; I < ND; I++) TA[I] = ND - I; /* discos na Torre A */ *NDA = ND; } void IMPRIMIR (void) { int I, CMAX = NDA; if (NDB > CMAX) CMAX = NDB; if (NDC > CMAX) CMAX = NDC; for (I = CMAX; I > 0; I--) { if (NDA >= I) printf ("%10d", TORREA[I-1]); else printf ("%10c", ' '); if (NDB >= I) printf ("%10d", TORREB[I-1]); else printf ("%10c", ' '); if (NDC >= I) printf ("%10d", TORREC[I-1]); else printf ("%10c", ' '); printf ("\n"); } printf ("---------------------------------\n"); scanf ("%*c"); } *NDB = 0; *NDC = 0;

/* nmero de discos a colocar na Torre A */

Figura 4.21 - Programa das Torres de Hani (1 parte).

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

void MUDAR_DISCOS (int ND, int TA[], int *NDA, int TB[], int *NDB,\ int TC[], int *NDC); { if (ND == 1) /* condio de paragem */ { /* mudar o disco da Torre A para a Torre B */ (*NDB)++; TB[*NDB-1] = TA[*NDA-1]; (*NDA)--; IMPRIMIR (); } else { /* mudar os N-1 discos de cima da Torre A para a Torre C */ MUDAR_DISCOS (ND-1, TA, NDA, TC, NDC, TB, NDB); /* mudar o ltimo disco da Torre A para a Torre B */ (*NDB)++; TB[*NDB-1] = TA[*NDA-1]; (*NDA)--; IMPRIMIR (); /* mudar os N-1 discos da Torre C para a Torre B */ MUDAR_DISCOS (ND-1, TC, NDC, TB, NDB, TA, NDA); } }

Figura 4.22 - Programa das Torres de Hani (2 parte).

O nmero de movimentos necessrios para mudar N discos igual a 2N 1. Pelo que, o nmero de invocaes da funo recursiva aumenta exponencialmente com o aumento do nmero de discos. Por isso, no se deve invocar o programa para um nmero de discos maior do que 10. A Figura 4.23 apresenta a execuo do programa para trs discos.
--------------------------------| Torres de Hanoi | | Numero de discos = 3 | --------------------------------| TORRE A TORRE B TORRE C | --------------------------------1 2 3 --------------------------------2 3 1 --------------------------------3 1 2 --------------------------------1 3 2 --------------------------------1 3 2 --------------------------------1 3 2 --------------------------------2 1 3 --------------------------------1 2 3 ---------------------------------

Figura 4.23 - Execuo do programa Torres de Hani para 3 discos.

17

CAPTULO 4 : RECURSIVIDADE

4.9 Questes sobre a eficincia das solues recursivas


Alguns algoritmos recursivos desencadeiam um nmero arbitrariamente grande de invocaes sucessivas da funo, pelo que, nestes casos temos uma ineficincia acrescida associada invocao de funes. Por outro lado, existem algoritmos recursivos que so computacionalmente ineficientes, porque calculam certos valores repetidamente devido s mltiplas invocaes recursivas. No caso dos algoritmos em que a invocao recursiva explode rapidamente, existe ainda o perigo de a memria de tipo pilha esgotar e do programa terminar a sua execuo abruptamente. Portanto, o programador deve ter sempre em conta estas ineficincias e limitaes, antes de optar por uma soluo recursiva em detrimento de uma soluo iterativa, caso ela exista. Os algoritmos recursivos so apropriados para resolver problemas que so normalmente definidos de forma recursiva, ou seja, problemas que so por natureza recursivos. Por vezes, existem problemas que tm solues recursivas que so simples, concisas, elegantes e para os quais difcil esboar solues iterativas com a mesma simplicidade e clareza. Mas, tal como vimos nos exemplos apresentados, alguns algoritmos recursivos so menos eficientes que os algoritmos iterativos equivalentes. Pelo que, a verdadeira importncia da recursividade consiste em resolver esses problemas para os quais no existem solues iterativas simples.

4.10 Exerccios
1. Pretende-se escrever um programa que imprima no monitor, uma tabela em que se compara os valores da funo co-seno calculada pela expanso em srie de Taylor para 5, 10 e 15 termos e pela funo matemtica cos. Os valores inicial e final da tabela, bem como o nmero de elementos da tabela so lidos do teclado. O termo geral da expanso em srie de Taylor da funo co-seno dado pela seguinte expresso esquerda, sendo apresentada direita a sua expanso para os primeiros cinco termos da srie.
coseno( x, N )

 1 n u x 2n n 0

N 1

2n

coseno( x,5 )

1

x2 x4 x6 x8    ... 2! 4! 6! 8!

Desenhe a tabela com o formato que a seguir se apresenta.


---------------------------------------------------------------------| x | coseno(x,5) | coseno(x,10) | coseno(x,15) | cos(x) | ---------------------------------------------------------------------| ##.#### | #.######### | #.######### | #.######### | #.######### | ---------------------------------------------------------------------... ---------------------------------------------------------------------| ##.#### | #.######### | #.######### | #.######### | #.######### | ----------------------------------------------------------------------

Faa duas verses do programa. Na primeira verso, implemente a expanso da srie de Taylor da funo co-seno com uma funo iterativa, e na segunda verso com uma funo recursiva.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

2. Pretende-se escrever um programa que escreva no monitor as permutaes da cadeia de caracteres composta pelos caracteres numricos de 0 a 9. Altere o programa para escrever apenas as permutaes que so compostas alternadamente por caracteres numricos pares e mpares. 3. Pretende-se escrever um programa que l do teclado dois nmeros inteiros positivos, que calcula e imprime no monitor o seu mximo divisor comum. O mximo divisor comum de dois nmeros inteiros positivos pode ser calculado de forma repetitiva, utilizando para o efeito o mtodo de Euclides, cujo algoritmo dado pela seguinte expresso. mdc(m, n) m, se n 0 mdc(n, mod(m, n)), se n z 0

Faa duas verses do programa. Na primeira verso, implemente o clculo do mximo divisor comum com uma funo iterativa, e na segunda verso com uma funo recursiva. 4. Pretende-se escrever um programa que l do teclado dois nmeros inteiros positivos, que calcula e imprime no monitor o valor da funo de Ackermann, que se apresenta na seguinte expresso.

Ackermann(m, n)

n  1, se m 0 Ackermann(m - 1,1), se n 0 Ackermann(m - 1, Ackermann(m, n - 1)) , com n ! 0 e m ! 0

Faa duas verses do programa. Na primeira verso, implemente o clculo da funo de Ackermann com uma funo recursiva, e na segunda verso com uma funo iterativa dinmica, usando um agregado bidimensional.

4.11 Leituras recomendadas


x 3 captulo do livro Data Structures, Algorithms and Software Principles in C, de Thomas A. Standish, da editora Addison-Wesley Publishing Company, 1995.

Captulo 5
MEMRIAS

Sumrio
Neste captulo comeamos por introduzir o paradigma da programao modular e a construo de mdulos na linguagem C, apresentando as suas caractersticas e a necessidade de criao de mdulos genricos. A ttulo de exemplo, desenvolvemos um exemplo de um mdulo abstracto com mltipla instanciao para operaes sobre nmeros complexos. Seguidamente mostramos a organizao da Memria de Acesso Aleatrio ( RAM), da Memria Fila (Queue/FIFO), da Memria Pilha (Stack/LIFO) e da Memria Associativa (CAM) e os seus ficheiros de interface. Depois de descrevermos as particularidades e limitaes dos tipos de implementao de memrias, fazemos uma abordagem s estruturas de dados que servem de suporte implementao esttica e semiesttica da Memria de Acesso Aleatrio, e implementao esttica, semiesttica e dinmica da Memria Fila, da Memria Pilha e da Memria Associativa. Finalmente, apresentamos as funes da biblioteca de execuo ANSI stdlib que permitem a atribuio e libertao de memria, durante a execuo do programa.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

5.1 Programao modular


O paradigma de programao procedimental enunciado da seguinte forma decide os procedimentos que precisas e usa os melhores algoritmos possveis. Consiste na decomposio hierrquica da soluo do problema, tambm designada de decomposio do topo para a base (Top-Down Decomposition), ou seja, implementa a estratgia do dividir para conquistar. A nica maneira de lidar com um problema complexo consiste em decomp-lo num conjunto de problemas mais pequenos, cada um deles de resoluo mais fcil que o problema original. Outra tcnica que permite decompor a complexidade de um problema, em problemas mais pequenos que tm a particularidade de serem exactamente do mesmo tipo do problema original, a construo de solues recursivas. A estratgia da decomposio hierrquica implementada atravs da definio de novas operaes no mbito da linguagem, ou seja, atravs da criao de subprogramas. No caso da linguagem Pascal temos dois tipos de subprogramas, que so o procedimento e a funo. No caso da linguagem C, temos apenas a funo generalizada que um tipo de subprograma que combina as caractersticas apresentadas pelo procedimento e pela funo. O subprograma pode ser visto como uma caixa preta, que recebe informao entrada e que produz informao sada, escondendo no entanto os detalhes da implementao da operao, ou seja, as aces que dentro do subprograma transformam a informao de entrada na informao de sada. Estas aces so invisveis externamente e, portanto, no originam qualquer interaco com o exterior. Este conjunto de aces detalhadas, que representam a soluo do problema, designa-se por algoritmo. Ao encapsulamento do algoritmo dentro de um subprograma, designa-se por abstraco procedimental. A abstraco procedimental permite fazer a separao entre o objectivo de um subprograma da sua implementao. Aps a definio do subprograma, a nova operao identificada por um nome e por uma lista de parmetros de comunicao com o exterior. Do ponto de vista operacional, tudo o que o programador precisa de saber para utilizar o subprograma o seu nome e a sua interface com o exterior, que deve estar bem documentada. O programador no precisa de conhecer a sua implementao, ou seja, o algoritmo utilizado. Da que, as linguagens de alto nvel providenciam facilidades para a passagem de informao, de e para os subprogramas, de maneira a estabelecer o fluxo dos dados atravs das subprogramas e por conseguinte, ao longo do programa. Uma questo essencial a ter em conta durante o desenvolvimento de um subprograma, que ele deve ser implementado de forma a ser, se possvel, completamente autnomo do ambiente externo onde vai ser utilizado, tornando-o verstil. Assim assegura-se que ele pode ser testado separadamente e, mais importante, que pode ser reutilizado noutro contexto. portanto, uma nova operao que estende a operacionalidade da linguagem. Com a criao de subprogramas autnomos possvel criar bibliotecas de subprogramas que podero ser reutilizados mais tarde noutros programas. Neste paradigma de programao, os subprogramas podem ser vistos como blocos que podem ser interligados para construir programas mais complexos, fazendo uma montagem do programa da base para o topo (Bottom-Up Assembly). Permitindo assim, validar a soluo do problema de uma maneira controlada e integrar de um modo progressivo os diferentes subprogramas, avaliando possveis solues alternativas atravs de diferentes arranjos de subprogramas.

CAPTULO 5 : MEMRIAS

Assim, se potenciarmos ao mximo a reutilizao de subprogramas, o esforo que necessrio despender para criar novos programas menor do que implement-los de raiz. No entanto, a autonomia de um subprograma est limitada ao facto de a estrutura de dados que o seu algoritmo manipula ser exterior ao subprograma. Pelo que, a utilizao do subprograma tem que ter sempre em conta no s a lista de parmetros de comunicao com o exterior, mas tambm a prpria estrutura de dados para o qual foi desenvolvido. O que implica, que se um programa necessitar de uma estrutura de dados que mesmo sendo semelhante implementada de forma diferente, ento o subprograma tem de ser modificado antes de ser reutilizado. Assim a reutilizao do subprograma com um mnimo de esforo pode estar comprometida. Por outro lado, ao longo dos anos, a nfase na implementao do software dirigiu-se em direco organizao de estruturas de dados cada vez mais complexas. Para estruturas de dados complexas no faz sentido implementar subprogramas de uma forma isolada, mas sim providenciar um conjunto de subprogramas que as manipulam e que permitem que as estruturas de dados sejam encaradas como um novo tipo de dados da linguagem. Um tipo de dados permite a declarao de variveis desse tipo e tem associado um conjunto de operaes permitidas sobre essas variveis. Este paradigma de programao, a que se d o nome de programao modular, enunciado da seguinte forma decide os mdulos que precisas e decompe o programa para que as estruturas de dados sejam encapsuladas nos mdulos. Nesta filosofia de programao j no escondemos apenas os algoritmos, mas tambm as estruturas de dados que so processadas pelos algoritmos. Ou seja, abstraco procedimental acrescenta-se tambm a abstraco das estruturas de dados. Com a abstraco das estruturas de dados o programador concentra a sua ateno na operacionalidade das operaes que processam as estruturas de dados, ou seja, na aco das operaes sobre as estruturas de dados, em vez de se preocupar com os detalhes da implementao das operaes. Portanto, o paradigma de programao modular consiste na decomposio do programa em estruturas autnomas interactivas, onde existe uma separao clara entre a definio do mdulo, ou seja, a sua interface com o exterior e a respectiva implementao do mdulo. Do ponto de vista operacional, tudo o que o programador precisa de saber para utilizar o mdulo o seu nome e a sua interface com o exterior, que deve estar bem documentada. O programador no precisa de conhecer a sua implementao, ou seja, a organizao da estrutura de dados interna do mdulo e os algoritmos utilizados pelas operaes. Esta filosofia de programao permite a criao de estruturas de dados abstractas, uma vez que os detalhes da sua implementao esto escondidos, ou seja encapsulados no mdulo. Permite tambm a sua proteco uma vez que no esto acessveis a partir do exterior a no ser atravs das operaes de processamento disponibilizadas pelo mdulo. Permite ainda, a criao de operaes virtuais, uma vez que se podem experimentar vrios algoritmos alternativos de manipulao das estruturas de dados, atravs da criao de mdulos de implementao alternativos, para a mesma interface. Os mdulos so assim entidades mais autnomas do que os subprogramas e por conseguinte mais reutilizveis. So blocos construtivos mais poderosos, tanto mais poderosos quanto a sua estrutura de dados for reconfigurvel em funo das necessidades da aplicao, ou seja, quanto mais abstracta for a estrutura de dados interna do mdulo. A abstraco tem a vantagem de esconder completamente a implementao da estrutura de dados, protegendo-a assim de operaes que no so fornecidas pelo mdulo.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

Logo, uma questo essencial a ter em conta durante o desenvolvimento de um mdulo, que ele deve ser implementado da forma mais abstracta possvel, tornando-o assim genrico. Um mdulo genrico um mdulo cuja estrutura de dados pode ser do tipo de dados determinado pelo programador, em funo da aplicao onde o mdulo vai ser utilizado, sem que ele tenha que modificar a implementao do mdulo. Um mdulo genrico assegura uma maior reutilizao. Resumindo, os mdulos apresentam quatro caractersticas muito importantes: permitem agrupar estruturas de dados e as operaes de processamento associadas; apresentam interfaces bem definidas para os seus utilizadores, onde so reveladas as operaes de processamento disponveis; escondem os detalhes da implementao das mesmas focando a ateno do programador na sua funcionalidade; e podem ser compilados separadamente.

5.2 Mdulos na linguagem C


Antes de analisarmos a composio de um mdulo, devemos ter em considerao que devemos ento criar, sempre que possvel, um mdulo genrico, de modo a que seja o mais reutilizvel possvel. preciso tambm ter em ateno, que por vezes o programador no tem acesso implementao do mdulo, mas apenas sua interface. Como ento possvel criar um mdulo genrico? Existem duas maneiras. Uma consiste em criar um mdulo de dados abstractos, ou seja, um mdulo em que a estrutura de dados fica indefinida recorrendo declarao de ponteiros de tipo void. Aquando da utilizao de um mdulo abstracto preciso concretiz-lo para o tipo de dados pretendido atravs da disponibilizao de uma funo de criao do mdulo, que responsvel pela caracterizao do tamanho em bytes do elemento bsico da estrutura de dados. Uma vez que a biblioteca de execuo ANSI string providencia funes de cpia de memria, sem ser atribudo qualquer interpretao ao contedo dos bytes copiados, ento possvel manipular estruturas de dados sem que o cdigo C se tenha que preocupar com o tipo de dados que est a processar. A implementao abstracta tem a vantagem de esconder completamente a representao da informao, protegendo a estrutura de dados interna do mdulo. Se o programador no conhecer os pormenores da implementao da estrutura de dados, ento no pode, nomeadamente, desenvolver funes que actuam sobre ela. A alternativa criao de um mdulo abstracto, consiste na criao de um mdulo concreto, mas, em que o tipo de dados dos elementos da estrutura de dados definido parte num ficheiro de interface, que vamos designar por elemento.h, e que depois includo no ficheiro de interface do mdulo. Em certas aplicaes, este ficheiro para alm da definio do tipo de dados dos elementos, pode tambm definir as constantes que parametrizam a dimenso das estruturas de dados. Assim o utilizador do mdulo, pode concretizar o mdulo para uma estrutura de dados que corresponda s suas necessidades, quer em termos de dimenso quer em termos do tipo dos elementos, sem ter a necessidade de reprogramar o ficheiro de implementao do mdulo. Em relao criao de um mdulo abstracto, esta soluo exige a recompilao do mdulo, sempre que este ficheiro de interface modificado. A Figura 5.1 apresenta um exemplo do ficheiro de interface do elemento constituinte de uma estrutura de dados, considerando que os elementos so do tipo TIPO_ELEMENTO, bem como da definio da constante N_ELEMENTOS que dimensiona a estrutura de dados do mdulo.

CAPTULO 5 : MEMRIAS

/*********** Interface da Estrutura de Dados do Mdulo ***********/ /* Nome: elemento.h */ /* Definio da dimenso da estrutura de dados e do tipo de dados dos seus elementos. Este ficheiro deve ser modificado para adequar a definio a cada implementao especfica. */ #ifndef _ELEMENTO #define _ELEMENTO /****** Constantes de Parametrizao das Estruturas de Dados ******/ #define typedef #endif N_ELEMENTOS ... 100 /* nmero de elementos */ /* tipo de dados dos elementos */ /************* Definio do Tipo de Dados do Elemento *************/ TIPO_ELEMENTO;

Figura 5.1 - Ficheiro de interface do elemento constituinte da estrutura de dados.

Os mdulos na linguagem C so compostos por dois ficheiros. O ficheiro de interface, que se apresenta na Figura 5.2, tem a extenso .h. O ficheiro de implementao, que se apresenta na Figura 5.3, tem a extenso .c.
/*********************** Interface do Mdulo *********************/ /* Nome: modulo.h */ #ifndef _MODULO #define _MODULO /** Incluso do Ficheiro de Interface do Tipo de Dados do Mdulo **/ #include "elemento.h" /* caracterizao do tipo elemento do mdulo */ /******************** Definio de Constantes ********************/ #define OK 0 /* operao realizada com sucesso */ ... /************ Aluso s Funes Exportadas pelo Mdulo ************/ void INICIALIZAR (TIPO_ELEMENTO [], int); ... #endif

Figura 5.2 - Ficheiro de interface do mdulo.

O ficheiro de interface declara as entidades do mdulo que so visveis no exterior e que so utilizveis pelos utilizadores. Consiste na definio de constantes que representam identificadores de cdigos de erro devolvidos pelas funes do mdulo e que servem para assinalar o estado de execuo das funes. Consiste tambm nas aluses, ou prottipos, das funes exportadas pelo mdulo. Apenas estas funes podem ser utilizadas e atravs delas que as aplicaes manipulam as estruturas de dados internas do mdulo. Por vezes um mdulo includo noutro mdulo, que por sua vez includo noutro mdulo e assim sucessivamente at que um ou mais destes mdulos so includos nos ficheiros fonte da aplicao. De maneira a evitar uma possvel mltipla incluso de um mdulo na aplicao, ele deve ser includo condicionalmente. Para tal, existe a directiva do pr-processador #ifndef _MODULO #define _MODULO endif que assegura que o mdulo, cujo nome MODULO, includo apenas uma vez.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

/******************** Implementao do Mdulo ********************/ /* Nome: modulo.c */ /************ Incluso das Bibliotecas da Linguagem C ************/ #include <stdlib.h> ... /********** Incluso do Ficheiro de Interface do Mdulo **********/ #include "modulo.h" /***** Declarao das Estruturas de Dados Internas do Mdulo *****/ static TIPO_ELEMENTO ESTRUTURA_DADOS_MODULO[N_ELEMENTOS]; ... /******************* Implementao das Funes ********************/ static void TROCA (int *, int *) { ... } /* funo interna do mdulo */

/* funo exportada pelo mdulo */ void INICIALIZAR (TIPO_ELEMENTO [], int) { ... } ...

Figura 5.3 - Ficheiro de implementao do mdulo.

O ficheiro de implementao implementa a funcionalidade do mdulo. Comea por incluir as bibliotecas de execuo ANSI da linguagem C, que so necessrias e o ficheiro de interface do mdulo, de maneira a incluir as constantes que representam os cdigos de erro devolvidos pelas funes e do tipo de dados dos elementos da estrutura de dados. Declara as estruturas de dados internas do mdulo, que so o suporte ao armazenamento da informao interna do mdulo. A declarao das estruturas de dados no ficheiro de implementao, com o qualificativo static, torna-as globais no ficheiro de implementao mas invisveis no exterior. Pelo que, podem ser manipuladas pelas funes do mdulo sem terem de ser passadas pela lista de parmetros, mas, esto protegidas de serem manipuladas atravs de funes exteriores ao mdulo. No caso das estruturas de dados do mdulo serem estticas ou semiestticas elas so parametrizadas por constantes que definem a sua dimenso. Essas constantes encontram-se definidas no ficheiro de interface do tipo de dados dos elementos do mdulo, ou seja, no ficheiro elemento.h. Aps a definio das estruturas de dados, o ficheiro de implementao faz a definio das funes exportadas pelo mdulo, ou seja, das funes que foram declaradas no ficheiro de interface. Por vezes, para implementar uma funo necessrio recorrer a funes auxiliares de maneira a estruturar melhor a sua funcionalidade. Se estas funes auxiliares forem partilhadas por vrias funes, nesse caso devem ser aludidas logo a seguir declarao das estruturas de dados. Estas funes auxiliares, sendo funes internas do mdulo, devem ser declaradas com o qualificativo static. Desta forma so invisveis para o exterior, pelo que, os seus nomes podem ser utilizados noutros mdulos para implementar outras funes internas a esses mdulos. Os mdulos so compilados separadamente, com a opo c, para criar o ficheiro objecto, que tem a extenso .o, tal como se mostra na linha seguinte. cc c nome_do_mdulo.c Para que uma aplicao utilize um mdulo, o seu ficheiro de interface deve ser includo nos ficheiros da aplicao, tal como se apresenta na Figura 5.4.

CAPTULO 5 : MEMRIAS

/********************* Ficheiro da Aplicao *********************/ /************ Incluso das Bibliotecas da Linguagem C ************/ #include <stdio.h> ... /********** Incluso do Ficheiro de Interface do Mdulo **********/ #include "modulo.h" /******************** Definio de Constantes ********************/ #define NP 10 ... /*********** Aluso s Funes definidas neste Ficheiro ***********/ ... int main (void) { ... /* invocao das funes do mdulo e de outras funes */ return 0; } /******* Implementao das Funes definidas neste Ficheiro *******/ ...

Figura 5.4 - Utilizao de um mdulo.

O ficheiro objecto depois acrescentado no comando de compilao do programa que utiliza o mdulo, tal como se mostra na linha seguinte. cc nome_do_ficheiro.c nome_do_mdulo.o o nome_do_ficheiro_executvel

5.3 Exemplo de um mdulo


Vamos agora mostrar a implementao de um mdulo que implementa a criao de nmeros complexos e de operaes sobre nmeros complexos. A Figura 5.5 apresenta o seu ficheiro de interface. Como se pode ver, o programador apenas sabe que o mdulo define um tipo de dados PtComplexo que um ponteiro para uma estrutura, no sabendo no entanto, se a estrutura que armazena o nmero complexo utiliza uma representao em parte real e parte imaginria ou se utiliza a representao em coordenadas polares. Esta declarao prende-se com a necessidade do programador ter que declarar mais do que um nmero complexo na aplicao que vai desenvolver, pelo que, sem a exportao deste tipo de dados o programador no o poderia fazer. Portanto, estamos perante um mdulo abstracto com possibilidade de mltipla instanciao. O mdulo providencia um conjunto de funes necessrias para criar e operar nmeros complexos. Assim temos uma operao de inicializao que cria um nmero complexo com um valor especificado, uma operao de leitura que l o nmero complexo introduzido pelo teclado na forma R+jI e uma operao de escrita no monitor de um complexo na mesma forma. A operao de leitura s l informao do teclado para o nmero complexo, caso ele tenha sido criado previamente e a operao de escrita s escreve o nmero complexo se ele existir. Qualquer uma das quatros operaes bsicas, adio, subtraco, multiplicao e diviso, cria um novo nmero complexo para armazenar o resultado da operao. Quando uma funo cria um nmero complexo, faz a atribuio de memria para o seu armazenamento e devolve um ponteiro para a sua localizao na memria. A partir da o nmero complexo pode ser manipulado no programa atravs deste ponteiro.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

O mdulo tambm fornece uma funo que permite libertar a memria atribuda para um nmero complexo, quando ele no mais preciso, ou seja, uma funo para apagar o nmero complexo. Para evitar divises por zero, preciso uma funo que verifica se um nmero complexo o complexo nulo. Existem ainda funes para extrair a parte real e a parte imaginria do nmero complexo.
/* Ficheiro de interface do mdulo de nmeros complexos */ /* Nome: complexo.h */ #ifndef _COMPLEXO #define _COMPLEXO typedef struct complexo *PtComplexo;

PtComplexo Inicializar_Complexo (double R, double I); /* Funo que cria e inicializa um complexo na forma R+jI */ void Ler_Complexo (PtComplexo PC); /* Funo que cria e l do teclado um complexo na forma R+jI */ void Escrever_Complexo (PtComplexo PC); /* Funo que escreve no monitor um complexo na forma R+jI */ PtComplexo Somar_Complexos (PtComplexo PC1, PtComplexo PC2); /* Funo que soma dois nmeros complexos */ PtComplexo Subtrair_Complexos (PtComplexo PC1, PtComplexo PC2); /* Funo que subtrai dois nmeros complexos */ PtComplexo Multiplicar_Complexos (PtComplexo PC1, PtComplexo PC2); /* Funo que multiplica dois nmeros complexos */ PtComplexo Dividir_Complexos (PtComplexo PC1, PtComplexo PC2); /* Funo que divide dois nmeros complexos */ void Apagar_Complexo (PtComplexo *PC); /* Funo que apaga o nmero complexo e devolve o ponteiro a NULL */ int Complexo_Nulo (PtComplexo PC); /* Funo que testa se o nmero complexo nulo (0+j0). Devolve 0 em caso afirmativo e 1 em caso contrrio */ double Parte_Real (PtComplexo PC); /* Funo que devolve a parte real de um nmero complexo */ double Parte_Imaginaria (PtComplexo PC); /* Funo que devolve a parte imaginria de um nmero complexo */ #endif

Figura 5.5 - Ficheiro de interface do mdulo de nmeros complexos.

A Figura 5.6 e a Figura 5.7 apresentam o ficheiro de implementao do mdulo. A Figura 5.8 apresenta um exemplo da utilizao do mdulo para simular uma mquina de calcular de nmeros complexos. O programa comea por criar dois nmeros complexos, Comp1 e Comp2, com o valor nulo. Depois tem um funcionamento repetitivo, que consiste em escrever o menu de operaes no monitor e executar a operao escolhida pelo utilizador. A mquina de calcular permite ler do teclado valores para os dois operadores complexos Comp1 e Comp2 e executar as quatro operaes bsicas aritmticas. A operao de diviso s feita se o divisor que Comp2 no for nulo. Quando qualquer uma das quatro operaes aritmticas realizada, o resultado armazenado no nmero complexo Resultado. Este complexo depois impresso no monitor e apagado. Aps o utilizador visualizar o resultado da operao e premir uma tecla, o monitor limpo e o menu de operaes reescrito. Quando o utilizador escolher a opo de fim de execuo do programa, os dois nmeros complexos Comp1 e Comp2 so apagados.

CAPTULO 5 : MEMRIAS

/* Ficheiro de implementao do mdulo de nmeros complexos */ /* Nome: complexo.c - Primeira Parte */ #include <stdio.h> #include <stdlib.h> #include "complexo.h" struct complexo { double Real; double Imag; }; /* Funo que cria e inicializa um complexo na forma R+jI */ PtComplexo Inicializar_Complexo (double R, double I) { PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo)); PC->Real = R; return PC; } /* Funo que l do teclado um complexo na forma R+jI */ void Ler_Complexo (PtComplexo PC) { if (PC == NULL) return; printf ("Parte Real "); scanf ("%lf", &PC->Real); printf ("Parte Imaginria "); scanf ("%lf", &PC->Imag); } /* Funo que escreve no monitor um complexo na forma R+jI */ void Escrever_Complexo (PtComplexo PC) { if (PC != NULL) printf ("%f +j %f\n", PC->Real, PC->Imag); } /* Funo que soma dois nmeros complexos */ PtComplexo Somar_Complexos (PtComplexo PC1, PtComplexo PC2) { PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo)); PC->Real = PC1->Real + PC2->Real; PC->Imag = PC1->Imag + PC2->Imag; return PC; } /* Funo que subtrai dois nmeros complexos */ PtComplexo Subtrair_Complexos (PtComplexo PC1, PtComplexo PC2) { PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo)); PC->Real = PC1->Real - PC2->Real; PC->Imag = PC1->Imag - PC2->Imag; return PC; } PC->Imag = I; /* Ficheiro de interface do mdulo */

Figura 5.6 - Ficheiro de implementao do mdulo de nmeros complexos (1 parte).

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

/* Ficheiro de implementao do mdulo de nmeros complexos */ /* Nome: complexo.c - Segunda Parte */ /* Funo que multiplica dois nmeros complexos */ PtComplexo Multiplicar_Complexos (PtComplexo PC1, PtComplexo PC2) { PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo)); PC->Real = PC1->Real * PC2->Real - PC1->Imag * PC2->Imag; PC->Imag = PC1->Real * PC2->Imag + PC1->Imag * PC2->Real; return PC; } /* Funo que divide dois nmeros complexos */ PtComplexo Dividir_Complexos (PtComplexo PC1, PtComplexo PC2) { double QUO = PC2->Real * PC2->Real + PC2->Imag * PC2->Imag; PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo)); PC->Real = (PC1->Real * PC2->Real + PC1->Imag * PC2->Imag) / QUO; PC->Imag = (PC1->Imag * PC2->Real - PC1->Real * PC2->Imag) / QUO; return PC; } /* Funo que apaga o nmero complexo e devolve o ponteiro a NULL */ void Apagar_Complexo (PtComplexo *PC) { PtComplexo TPC = *PC; if (TPC == NULL) return; *PC = NULL; free (TPC); } /* Funo que devolve 0 se o nmero complexo nulo (0+j0) ou 1 no caso contrrio */ int Complexo_Nulo (PtComplexo PC) { if (PC->Real == 0 && PC->Imag == 0) return 0; else return 1; } /* Funo que devolve a parte real de um nmero complexo */ double Parte_Real (PtComplexo PC) { return PC->Real; }

/* Funo que devolve a parte imaginria de um nmero complexo */


double Parte_Imaginaria (PtComplexo PC) { return PC->Imag; }

Figura 5.7 - Ficheiro de implementao do mdulo de nmeros complexos (2 parte).

11

CAPTULO 5 : MEMRIAS

/* Mquina de calcular de nmeros complexos */ #include <stdio.h> #include "complexo.h" int main (void) { PtComplexo Comp1, Comp2, Resultado;

int Opcao, Oper;

Comp1 = Inicializar_Complexo (0.0, 0.0); Comp2 = Inicializar_Complexo (0.0, 0.0); do { system ("clear"); printf ("\t1 - Ler o primeiro complexo\n"); printf ("\t2 - Ler o segundo complexo\n"); printf ("\t3 - Somar os complexos\n"); printf ("\t4 - Subtrair os complexos\n"); printf ("\t5 - Multiplicar os complexos\n"); printf ("\t6 - Dividir os complexos\n"); printf ("\t7 - Sair do programa\n"); do { printf ("\n\tOpo -> "); scanf ("%d", &Opcao); scanf ("%*[^\n]"); scanf ("%*c"); } while (Opcao<1 && Opcao>7); Oper = 0; switch (Opcao) { case 1 : printf("\n\n"); Comp1 = Ler_Complexo(); break; case 2 : printf("\n\n"); Comp2 = Ler_Complexo(); break; case 3 : Resultado = Somar_Complexos(Comp1, Comp2); printf ("Adio dos complexos -> "); Escrever_Complexo (Resultado); Oper = 1; break; case 4 : Resultado = Subtrair_Complexos(Comp1, Comp2); printf ("Subtraco dos complexos -> "); Escrever_Complexo (Resultado); Oper = 1; break; case 5 : Resultado = Multiplicar_Complexos(Comp1, Comp2); printf ("Multiplicao dos complexos -> "); Escrever_Complexo (Resultado); Oper = 1; break; case 6 : if ( Complexo_Nulo (Comp2) ) { Resultado = Dividir_Complexos(Comp1, Comp2); printf ("Diviso dos complexos -> "); Escrever_Complexo (Resultado); Oper = 1; } else printf ("O divisor o complexo nulo!!!\n"); break; } if (Opcao != 7) { printf ("\nPrima uma tecla para continuar\n"); scanf ("%*c"); } if (Oper) Apagar_Complexo (&Resultado); } while (Opcao != 7); Apagar_Complexo (&Comp1); Apagar_Complexo (&Comp2); return 0; }

Figura 5.8 - Mquina de calcular de nmeros complexos.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

5.4 Tipos de memrias e suas caractersticas


Vamos apresentar as caractersticas dos seguintes quatro tipos de memrias: a Memria de Acesso Aleatrio (RAM ); a Memria Fila (Queue/FIFO ); a Memria Pilha (Stack/LIFO ); e a Memria Associativa (CAM ).

5.4.1 Memria de acesso aleatrio (RAM )


Uma memria de acesso aleatrio (Random Access Memory) uma memria em que no existe nenhuma organizao especial de acesso aos elementos armazenados, antes pelo contrrio, possvel aceder em qualquer instante a qualquer posio da memria, indicando para o efeito o endereo, ou seja, o ndice da posio de memria a que se pretende aceder. Estamos perante um acesso indexado, pelo que, uma memria de acesso aleatrio s pode ser implementada por uma estrutura de dados que permita este tipo de acesso. A Figura 5.9 apresenta o ficheiro de interface de uma memria de acesso aleatrio abstracta.
/******************** Interface do Mdulo RAM ********************/ #ifndef _RAM #define _RAM /******************** Definio de Constantes ********************/ #define #define #define #define #define #define #define #define OK NULL_PTR NULL_SIZE NO_MEM RAM_EMPTY RAM_FULL RAM_EXISTS NO_RAM 0 1 2 3 4 5 6 7 /* /* /* /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ tamanho nulo */ memria esgotada */ RAM vazia */ RAM cheia */ j foi instanciada uma RAM */ ainda no foi instanciada qualquer RAM */

/********************* Prottipos das Funes *********************/ int RAM_Create (unsigned int sz); /* Concretiza a RAM para elementos de sz bytes. Valores de retorno: OK, NULL_SIZE ou RAM_EXISTS. */ int RAM_Destroy (void); /* Destri a RAM. Valores de retorno: OK ou NO_RAM. */ int RAM_Write (void *pelemento, unsigned int pos); /* Escreve o contedo do elemento apontado por pelemento na posio pos da RAM. Valores de retorno: OK, NO_RAM, NULL_PTR, RAM_FULL ou NO_MEM. */ int RAM_Read (void *pelemento, unsigned int pos); /* L o contedo do elemento da posio pos da RAM para o elemento apontado por pelemento. Valores de retorno: OK, NO_RAM, NULL_PTR ou RAM_EMPTY. */ int RAM_Search (void *pelemento, int *pos); /* Procura o primeiro elemento da RAM com contedo igual ao elemento apontado por pelemento. Coloca em pos o ndice do elemento encontrado ou -1 caso no exista tal elemento. Valores de retorno: OK, NO_RAM, NULL_PTR ou RAM_EMPTY. */ int RAM_Sort (void); /* Ordena a RAM. Valores de retorno: OK, NO_RAM, ou RAM_EMPTY. */ #endif

Figura 5.9 - Ficheiro de interface da memria de acesso aleatrio abstracta.

13

CAPTULO 5 : MEMRIAS

Sendo a caracterstica principal da memria de acesso aleatrio o acesso indexado, o posicionamento para a operao de leitura, que vamos designar por RAM_Read, ou para a operao de escrita, que vamos designar por RAM_Write, feito atravs do ndice do elemento de memria onde se pretende ler ou escrever. Para alm das operaes de leitura e de escrita, normalmente tambm necessrio procurar um elemento com um determinado valor, que vamos designar por RAM_Search e ordenar a memria, que vamos designar por RAM_Sort. A ordenao da memria facilita a pesquisa de informao, permitindo por exemplo, utilizar a pesquisa binria em alternativa pesquisa sequencial. As operaes de leitura e de escrita neste tipo de memria no afectam o seu tamanho. A memria de acesso aleatrio tem sempre o tamanho que foi decidido na altura da sua definio, o que permite aceder a qualquer um dos seus elementos para ler a informao armazenada ou para escrever nova informao. No entanto, conveniente no tentar ler informao de elementos onde ainda no foi feita qualquer operao de escrita. Logo, aconselhvel implementar uma poltica de escrita em elementos sucessivos da memria e manter um indicador de posio que indique qual o ndice do ltimo elemento da memria que contem informao til. As operaes de criao RAM_Create e de destruio RAM_Destroy s existem para implementaes abstractas e tm como funo respectivamente, concretizar a memria para o tipo de elementos pretendidos e repor a situao inicial de memria ainda por concretizar de maneira a poder ser reutilizada, eventualmente para um novo tipo de dados.

5.4.2 Memria fila (Queue/FIFO )


Uma memria fila, do ingls queue, uma memria em que s possvel processar a informao pela ordem de chegada. Da que, tambm seja apelidada de memria do primeiro a chegar primeiro a sair, do ingls First In First Out. A Figura 5.10 apresenta o ficheiro de interface de uma memria fila abstracta. Numa fila, o posicionamento para a colocao de um novo elemento, que vamos designar por FIFO_In, a cauda da fila (fifo tail ), e o posicionamento para a remoo de um elemento, que vamos designar por FIFO_Out, a cabea da fila (fifo head ). A colocao de um novo elemento consiste na adio de um novo elemento no fim da fila, ficando este novo elemento a ser a cauda da fila, e na escrita da informao nesse elemento. A remoo de um elemento consiste na leitura da informao armazenada no elemento que se encontra na cabea da fila e da eliminao desse elemento da fila. Consequentemente, o elemento seguinte, caso o haja, passa a ser a cabea da fila. Quando retirado o ltimo elemento, a fila fica vazia, sendo apenas possvel efectuar a operao de colocao de elementos na fila. A situao inversa de fila cheia tambm pode acontecer para certo tipos de implementaes, mais concretamente para implementaes estticas e semiestticas. Como o acesso memria fila est limitado aos elementos posicionados nos extremos da memria, a fila mantm dois indicadores de posio para a cabea e a cauda da fila. Ao contrrio da memria de acesso aleatrio, o tamanho da fila dinmico e depende do nmero de elementos colocados e do nmero de elementos retirados da fila. As operaes de criao FIFO_Create e de destruio FIFO_Destroy s existem para implementaes abstractas e tm como funo respectivamente, concretizar a fila para o tipo de elementos pretendidos e repor a situao inicial de fila ainda por concretizar de maneira a poder ser reutilizada, eventualmente para um novo tipo de dados.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

/******************** Interface do Mdulo FIFO ********************/ #ifndef _FIFO #define _FIFO /******************** Definio de Constantes ********************/ #define #define #define #define #define #define #define #define OK NULL_PTR NULL_SIZE NO_MEM FIFO_EMPTY FIFO_FULL FIFO_EXISTS NO_FIFO 0 1 2 3 4 5 6 7 /* /* /* /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ tamanho nulo */ memria esgotada */ fila vazia */ fila cheia */ j foi instanciada uma fila */ ainda no foi instanciada qualquer fila */

/********************* Prottipos das Funes *********************/ int FIFO_Create (unsigned int sz); /* Concretiza a fila para elementos de sz bytes. Valores de retorno: OK, NULL_SIZE ou FIFO_EXISTS. */ int FIFO_Destroy (void); /* Destri a fila. Valores de retorno: OK ou NO_FIFO. */ int FIFO_In (void *pelemento); /* Coloca o elemento apontado por pelemento na cauda da fila. Valores de retorno: OK, NO_FIFO, NULL_PTR, FIFO_FULL ou NO_MEM. */ int FIFO_Out (void *pelemento); /* Retira o elemento da cabea da fila para o elemento apontado por pelemento. Valores de retorno: OK, NO_FIFO, NULL_PTR ou FIFO_EMPTY. */ #endif

Figura 5.10 - Ficheiro de interface da memria fila abstracta.

5.4.3 Memria pilha (Stack/LIFO )


Uma memria pilha, do ingls stack, uma memria em que s possvel processar a informao pela ordem inversa ordem de chegada. Da que, tambm seja apelidada de memria do ltimo a chegar primeiro a sair, do ingls Last In First Out. A Figura 5.11 apresenta o ficheiro de interface de uma memria pilha abstracta. Numa pilha, o posicionamento para a colocao de um novo elemento, que se designa por

STACK_Push, e o posicionamento para a remoo de um elemento, que se designa por STACK_Pop, o topo da pilha (top of the stack). A colocao de um novo elemento
consiste na adio de um novo elemento, em cima do topo da pilha e na escrita da informao nesse elemento, ficando este novo elemento a ser o topo da pilha. A remoo de um elemento consiste na leitura da informao armazenada no elemento que se encontra no topo da pilha e da eliminao desse elemento da pilha, ficando o elemento anterior, caso o haja, a ser o topo da pilha. Quando retirado o ltimo elemento, a pilha fica vazia, sendo apenas possvel efectuar a operao de colocao de elementos na pilha. A situao inversa de pilha cheia tambm pode acontecer para certo tipos de implementaes, mais concretamente para implementaes estticas e semiestticas. Como o acesso memria pilha est limitado a este elemento posicionado no extremo da memria, a pilha mantm um indicador de posio para o topo da pilha. Tal como na fila, o tamanho da pilha tambm dinmico.

15

CAPTULO 5 : MEMRIAS

As operaes de criao STACK_Create e de destruio STACK_Destroy s existem para implementaes abstractas e tm como funo respectivamente, concretizar a pilha para o tipo de elementos pretendidos e repor a situao inicial de pilha ainda por concretizar de maneira a poder ser reutilizada, eventualmente para um novo tipo de dados.
/******************* Interface do Mdulo STACK *******************/ #ifndef _STACK #define _STACK /******************** Definio de Constantes ********************/ #define #define #define #define #define #define #define #define OK NULL_PTR NULL_SIZE NO_MEM STACK_EMPTY STACK_FULL STACK_EXISTS NO_STACK 0 1 2 3 4 5 6 7 /* /* /* /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ tamanho nulo */ memria esgotada */ pilha vazia */ pilha cheia */ j foi instanciada uma pilha */ ainda no foi instanciada qualquer pilha */

/********************* Prottipos das Funes *********************/ int STACK_Create (unsigned int sz); /* Concretiza a pilha para elementos de retorno: OK, NULL_SIZE ou STACK_EXISTS. */ sz bytes. Valores de

int STACK_Destroy (void); /* Destri a pilha. Valores de retorno: OK ou NO_STACK. */ int STACK_Push (void *pelemento); /* Coloca o elemento apontado por pelemento no topo da pilha. Valores de retorno: OK, NO_STACK, NULL_PTR, STACK_FULL ou NO_MEM. */ int STACK_Pop (void *pelemento); /* Retira o elemento do topo da pilha para o elemento apontado por NULL_PTR ou pelemento. Valores de retorno: OK, NO_STACK, STACK_EMPTY. */ #endif

Figura 5.11 - Ficheiro de interface da memria pilha abstracta.

5.4.4 Memria associativa (CAM )


Uma memria associativa (Content Access Memory) uma memria em que o acesso aos seus elementos, feita pelo contedo a que se pretende aceder, indicando para o efeito a chave do elemento a que se pretende aceder. Estamos perante um acesso por chave, pelo que, uma memria associativa s pode ser implementada por uma estrutura de dados em que a informao armazenada est sempre ordenada. A Figura 5.12 apresenta o ficheiro de interface de uma memria associativa abstracta. Numa memria associativa, o posicionamento para a colocao de um novo elemento, que vamos designar por CAM_In, e o posicionamento para a remoo de um elemento, que vamos designar por CAM_Out, feito atravs da chave do elemento que se pretende processar. A colocao de um novo elemento consiste na adio de um novo elemento na posio correspondente chave de acesso do novo elemento, de maneira a manter a memria ordenada e na escrita da informao nesse elemento. A remoo de um elemento consiste na leitura da informao armazenada no primeiro elemento com a chave pretendida e da eliminao desse elemento da memria. Tal como nas memrias fila e pilha, o tamanho da memria associativa tambm dinmico.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

Como a memria tem que estar sempre ordenada pela chave de pesquisa, as operaes de colocao e de remoo de elementos podem implicar deslocamentos dos elementos da memria para permitir a colocao do elemento na memria ou para compensar a eliminao do elemento da memria. O facto da memria estar sempre ordenada, permite tambm implementar de forma eficiente operaes de leitura de informao, que so normalmente muito importantes neste tipo de memria. Essas operaes so a leitura do primeiro elemento da memria que tem uma determinada chave, que vamos designar por CAM_Read_First, e a leitura de elementos sucessivos com a mesma chave, que vamos designar por CAM_Read_Next.
/******************** Interface do Mdulo CAM ********************/ #ifndef _CAM #define _CAM /******************** Definio de Constantes ********************/ #define #define #define #define #define #define #define #define #define #define OK NULL_PTR NULL_SIZE NO_MEM CAM_EMPTY CAM_FULL CAM_EXISTS NO_CAM NO_KEY NO_FUNC 0 1 2 3 4 5 6 7 8 9 /* /* /* /* /* /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ tamanho nulo */ memria esgotada */ CAM vazia */ CAM cheia */ j foi instanciada uma CAM */ ainda no foi instanciada qualquer CAM */ no existe elemento com a chave indicada */ no foi comunicada a funo de comparao */

/********************* Prottipos das Funes *********************/ int CAM_Create (unsigned int sz, CompFunc cmpf); /* Concretiza a CAM para elementos de sz bytes e indica a funo de comparao cmpf, que permite manter a memria ordenada. Valores de retorno: OK, NULL_SIZE, CAM_EXISTS ou NULL_PTR. */ int CAM_Destroy (void); /* Destri a CAM. Valores de retorno: OK ou NO_CAM. */ int CAM_In (void *pelemento); /* Coloca o elemento apontado por pelemento na posio da CAM com a chave indicada. Permite-se a existncia na CAM de elementos distintos com a mesma chave. Elementos com a mesma chave so armazenados por ordem cronolgica da colocao na CAM. Valores de retorno: OK, NO_CAM, NULL_PTR, CAM_FULL, NO_MEM ou NO_FUNC. */ int CAM_Out (void *pelemento); /* Retira o elemento da CAM com a chave indicada para o elemento apontado por pelemento. Havendo mais do que um elemento com a chave indicada ser retirado o primeiro que foi introduzido. Valores de retorno: OK, NO_CAM, NULL_PTR, CAM_EMPTY, NO_KEY ou NO_FUNC. */ int CAM_Read_First (void *pelemento); /* L o contedo do primeiro elemento da CAM que contm a chave indicada para o elemento apontado por pelemento. Valores de retorno: OK, NO_CAM, NULL_PTR, CAM_EMPTY, NO_KEY ou NO_FUNC. */ int CAM_Read_Next (void *pelemento); /* L o contedo do elemento seguinte da CAM que contm a chave indicada para o elemento apontado por pelemento. Os elementos so lidos sucessivamente a partir do primeiro. Valores de retorno: OK, NO_CAM, NULL_PTR, CAM_EMPTY, NO_KEY ou NO_FUNC. */ #endif

Figura 5.12 - Ficheiro de interface da memria associativa abstracta.

17

CAPTULO 5 : MEMRIAS

As operaes de criao CAM_Create e de destruio CAM_Destroy s existem para implementaes abstractas e tm como funo respectivamente, concretizar a memria para o tipo de elementos pretendidos e indicar a funo de comparao dos elementos, e repor a situao inicial de memria ainda por concretizar de maneira a poder ser reutilizada, eventualmente para um novo tipo de dados.

5.5 Tipos de implementao


Existem trs tipos de implementao de memrias: x Numa implementao esttica a memria definida partida e fixa. A sua dimenso, ou seja o nmero de elementos que pode armazenar, previamente conhecida e no pode ser alterada. As implementaes estticas so baseadas em agregados de elementos. Os elementos so de tipos de dados simples ou estruturas, e so definidos partida, pelo que, s podem armazenar dados desse tipo. Independentemente da utilizao da memria, ou seja, do nmero de elementos que contm de facto informao, a ocupao de memria do computador sempre a mesma. A implementao esttica a mais simples, mas, a menos verstil, uma vez que a memria tem uma dimenso inaltervel e no pode ser reconfigurada em termos do tipo dos elementos que armazena, durante a execuo do programa. x Numa implementao semiesttica a dimenso da memria definida partida e fixa. Tal como na implementao esttica, a sua dimenso previamente conhecida e no pode ser alterada. As implementaes semiestticas so baseadas em agregados de ponteiros para elementos, que so atribudos dinamicamente. Os elementos so criados por atribuio de memria dinmica quando a informao colocada na memria, e so eliminados quando a informao retirada da memria, por libertao da memria dinmica. A ocupao de memria do computador tem uma componente constante, que o agregado de ponteiros que serve de estrutura bsica de suporte e, uma componente varivel que depende do nmero de elementos que contm de facto informao. A memria tem uma organizao mais complexa que a implementao esttica. Mas mais verstil, porque com a utilizao de ponteiros para void possvel criar memrias de tipos de dados indefinidos, ou seja, tipos de dados que podem ser concretizados na altura da criao da memria. Este tipo de realizao designa-se por memria de dados abstractos. Mas, a memria continua a ter uma dimenso inaltervel, devido estrutura de dados de suporte ser esttica. x Numa implementao dinmica, a dimenso da memria inicialmente nula e depois vai crescendo medida das necessidades. As implementaes dinmicas so baseadas em estruturas ligadas, de tipo lista ligada, lista biligada ou rvore binria. Estas estruturas so constitudas por ns ou elos interligados entre si, que so criados ou eliminados na memria dinmica medida que a memria vai respectivamente crescendo ou decrescendo. Os elementos que contm a informao a armazenar na memria, esto dependurados nos ns, atravs de um ponteiro para o elemento. Os elementos tambm so criados ou eliminados dinamicamente quando respectivamente se coloca ou retira a informao da memria. A ocupao de memria do computador depende do nmero de elementos efectivamente armazenados na memria. A memria tem uma organizao mais complexa que a implementao semiesttica, mas, a implementao mais verstil, porque permite a realizao de uma memria de dados abstractos e mais importante ainda, porque a sua dimenso no est limitada.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

5.6 Memria de acesso aleatrio (RAM )


Como a memria de acesso aleatrio uma memria de acesso indexado, ento implementada atravs de um estrutura de dados de tipo agregado. A implementao esttica baseada num agregado de elementos do tipo que se pretende armazenar, como se apresenta na Figura 5.13. As funes de leitura e de escrita tm um parmetro de entrada que o ndice do elemento de memria que vai ser processado. Uma vez que a memria permite o acesso a qualquer um dos seus elementos, para procurar um elemento especfico, a funo de pesquisa tanto pode usar a pesquisa sequencial como a pesquisa binria, caso a memria esteja ordenada. Para ordenar a memria pode ser usado qualquer algoritmo de ordenao.
RAM[0] RAM[1] RAM[2] ... RAM[I] ... RAM[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

ltimo elemento RAM esttica

RAM_Read (I) RAM_Write (I)

Figura 5.13 - Implementao esttica da memria de acesso aleatrio.

A implementao semiesttica baseada num agregado de ponteiros, como se apresenta na Figura 5.14. A operao de escrita de informao responsvel pela atribuio de memria para o elemento. Portanto, as operaes de leitura, de pesquisa e de ordenao s podem aceder aos elementos existentes de facto na memria, pelo que, muito importante disponibilizar um contador que indica o nmero de elementos teis existentes na memria. Este contador comporta-se assim como um ponteiro indicador do ltimo elemento da memria onde foi escrita informao. A actualizao deste contador fica responsabilidade da operao de escrita, que deve utilizar uma poltica de escrita em elementos sucessivos do agregado para que no haja elementos sem informao na parte utilizada do agregado.
ltimo elemento RAM[0] RAM[1] RAM[2] ... RAM[I] RAM semiesttica ... RAM[N-1]

Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro

Elemento Elemento Elemento

Figura 5.14 - Implementao semiesttica da memria de acesso aleatrio.

No caso da implementao semiesttica possvel criar uma memria de acesso aleatrio abstracta. Para isso, necessrio providenciar a funo de criao da memria RAM_Create que concretiza o tipo de elementos, atravs da especificao do seu tamanho em bytes, e a funo de destruio da memria RAM_Destroy que alm de libertar a memria dinmica atribuda para os elementos, coloca o indicador de tamanho dos elementos de novo a zero e o contador de elementos teis a zero. Ou seja, coloca o indicador do ltimo elemento til da memria no incio do agregado.

19

CAPTULO 5 : MEMRIAS

Para implementar uma memria de acesso aleatrio dinmica seria preciso recorrer a uma lista ligada de elementos. S que depois a memria s poderia ser acedida sequencialmente a partir do elemento inicial e perder-se-ia o acesso indexado, pelo que, a memria deixaria ento de ter as caractersticas de acesso aleatrio. Portanto, no existe implementao dinmica da memria de acesso aleatrio.

5.7 Memria fila (Queue/FIFO )


A Figura 5.15 apresenta a implementao esttica da memria fila, que baseada num agregado de elementos do tipo que se pretende armazenar, usado de forma circular. A fila tem dois indicadores numricos que indicam os ndices dos elementos que so, em cada instante, a cabea da fila e a cauda da fila. Por uma questo de implementao, a cauda da fila indica a primeira posio livre para a prxima operao de colocao de um elemento. Sempre que se coloca um elemento na fila o indicador de cauda da fila deslocado para o elemento seguinte do agregado. Sempre que se retira um elemento da fila o indicador de cabea da fila deslocado para o elemento seguinte do agregado. Esta implementao no a forma habitual de funcionamento de um fila, onde sempre que o elemento da cabea da fila sai da fila, toda a fila deslocada para a frente. Mas, para evitar deslocamentos de elementos no agregado, a cabea da fila que se desloca para o elemento seguinte. Quando ao colocar um novo elemento na fila, a cauda da fila se sobrepor cabea da fila, ento sinal que a fila est cheia. Quando ao retirar um elemento da fila, a cabea da fila se sobrepor cauda da fila, ento sinal que a fila ficou vazia.

FIFO[0]

FIFO[1]

FIFO[2]

...

FIFO[I]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila FIFO_Out

cauda da fila FIFO_In

FILA esttica

Figura 5.15 - Implementao esttica da memria fila.

A implementao semiesttica, que se apresenta na Figura 5.16, baseada num agregado de ponteiros usado de forma circular e comporta-se da mesma forma que a implementao esttica, mas, com as seguintes diferenas. A operao de colocao de um elemento na fila responsvel pela atribuio de memria para o elemento e a operao de remoo de um elemento da fila responsvel pela libertao da memria ocupada pelo elemento.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

20

cabea da fila

cauda da fila

FIFO[0]

FIFO[1]

FIFO[2]

...

FIFO[I]

...

FIFO[N-1]

Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro

Elemento Elemento Elemento

FILA semiesttica

FIFO_Out

FIFO_In

Figura 5.16 - Implementao semiesttica da memria fila.

A implementao dinmica, que se apresenta na Figura 5.17, baseada numa lista ligada de elementos. Uma lista ligada uma estrutura constituda por elementos, a que vamos chamar ns, ligados atravs de ponteiros. Cada n da lista ligada constitudo por dois ponteiros. Uma para o elemento que armazena a informao e outro para o n seguinte da lista. Repare que o ltimo n da lista aponta para NULL, para servir de indicador de finalizao da fila. A memria para os ns e para os elementos da lista atribuda, quando um elemento colocado na fila e libertada quando um elemento retirado da fila. Os indicadores de cabea e cauda da fila so ponteiros. A cabea da fila aponta sempre para o elemento mais antigo que se encontra na fila e que o primeiro a ser retirado. A cauda da fila aponta sempre para o elemento mais recente que se encontra na fila e frente do qual um novo elemento colocado. Quando so ambos ponteiros nulos, sinal que a fila est vazia. Uma fila dinmica nunca est cheia. Quando muito, pode no existir memria para continuar a acrescentar-lhe mais elementos.
cabea da fila

PtSeg PtEle

PtSeg PtEle

PtSeg PtEle

PtSeg PtEle

Elemento

Elemento

Elemento

Elemento

FIFO_Out

FILA dinmica

cauda da fila

FIFO_In

Figura 5.17 - Implementao dinmica da memria fila.

No caso das implementaes semiesttica e dinmica possvel criar uma fila abstracta. Para isso, necessrio providenciar a funo de criao da fila FIFO_Create que concretiza o tipo de elementos, atravs da especificao do seu tamanho em bytes, e a funo de destruio da fila FIFO_Destroy que alm de libertar toda a memria dinmica atribuda, coloca o indicador de tamanho dos elementos de novo a zero e recoloca os indicadores de cabea e cauda da fila nas condies iniciais.

21

CAPTULO 5 : MEMRIAS

5.8 Memria pilha (Stack/LIFO )


Tal como a memria fila, a memria pilha tem as implementaes esttica e semiesttica, que se apresentam na Figura 5.18, baseadas em agregados. A pilha tem um indicador numrico que indica o ndice do elemento que o topo da pilha. Por uma questo de implementao, o topo da pilha indica a primeira posio livre para a prxima operao de colocao de um elemento. Sempre que se coloca um elemento na pilha o indicador de topo da pilha deslocado para o elemento seguinte do agregado. Quando o topo da pilha atinge o elemento a seguir ao fim do agregado, ou seja, o ndice N, ento sinal que a pilha est cheia. Sempre que se retira um elemento da pilha, primeiro o indicador de topo da pilha deslocado para o elemento anterior do agregado e depois o elemento retirado da pilha. Quando o indicador de topo da pilha atinge o elemento inicial do agregado, ou seja, o ndice 0, ento sinal que a pilha ficou vazia. A implementao semiesttica comporta-se da mesma forma que a implementao esttica, mas, com as seguintes diferenas. A operao de colocao de um elemento na pilha responsvel pela atribuio de memria para o elemento e a operao de remoo de um elemento da pilha responsvel pela libertao da memria ocupada pelo elemento.
PILHA esttica Ponteiro Elemento STACK[N-1] Elemento Elemento STACK_Push STACK_Pop topo da pilha Elemento Elemento Elemento Elemento ... STACK[I] ... STACK[2] STACK[1] STACK[0] Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro topo da pilha Elemento Elemento Elemento PILHA semiesttica

STACK_Push STACK_Pop

Figura 5.18 - Implementaes esttica e semiesttica da memria pilha.

A implementao dinmica de uma pilha, que se apresenta na Figura 5.19, baseada numa lista ligada de elementos. Mas, enquanto que na fila cada n da lista ligada aponta para o n seguinte, na pilha cada n aponta para o n anterior, sendo que o primeiro n da lista aponta para NULL, para servir de indicador de finalizao da pilha. A memria para os ns e para os elementos da lista atribuda, quando um elemento colocado na pilha e libertada quando um elemento retirado da pilha. O indicador de topo da pilha um ponteiro. O topo da pilha aponta sempre para o elemento mais recente que se encontra na pilha, que o primeiro elemento a ser retirado e frente do qual um novo elemento colocado. Quando o topo da pilha um ponteiro nulo, sinal que a pilha est vazia. Uma pilha dinmica nunca est cheia. Quando muito, pode no existir memria para continuar a acrescentar-lhe mais elementos.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

22

PILHA dinmica topo da pilha PtEle PtAnt Elemento

STACK_Push STACK_Pop

PtEle PtAnt

Elemento

PtEle PtAnt

Elemento

Figura 5.19- Implementao dinmica da memria pilha.

No caso das implementaes semiesttica e dinmica possvel criar uma pilha abstracta. Para isso, necessrio providenciar a funo de criao da pilha STACK_Create que concretiza o tipo de elementos, atravs da especificao do seu tamanho em bytes, e a funo de destruio da pilha STACK_Destroy que alm de libertar a memria dinmica atribuda, coloca o indicador de tamanho dos elementos de novo a zero e recoloca o indicador de topo da pilha na condio inicial.

5.9 Memria associativa (CAM )


As implementaes esttica e semiesttica da memria associativa, que se apresentam na Figura 5.20, so baseadas em agregados. Quando se pretende colocar um elemento na memria necessrio pesquisar a memria para encontrar a posio de colocao do elemento, que depende da sua chave e dos elementos j existentes na memria. Quando se pretende retirar um elemento da memria tambm necessrio pesquisar a memria para encontrar a posio onde o elemento se encontra. Para implementar a pesquisa da memria necessrio que a memria mantenha um contador que indica o nmero de elementos teis armazenados, ou seja, um indicador do ltimo elemento da memria onde foi escrita informao. Este contador actualizado pelas operaes de colocao e remoo de elementos na memria. Por questes de eficincia, tambm conveniente utilizar a pesquisa binria. Para manter a memria sempre ordenada, colocar ou retirar um elemento da memria pode implicar deslocar os elementos no agregado. No caso da implementao semiesttica, estes deslocamentos so deslocamentos de ponteiros, ou seja, de entidades de 4 bytes, pelo que, so operaes menos custosas que na implementao esttica. Na implementao semiesttica, as operaes de colocao e remoo de elementos so ainda responsveis respectivamente, pela atribuio de memria para o elemento e pela libertao da memria ocupada pelo elemento.

23

CAPTULO 5 : MEMRIAS

CAM[0]
CHAVE 1

CAM[1]
CHAVE 2

CAM[2]
CHAVE 4

CAM[3]
CHAVE 6

...

CAM[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento


pesquisa binria

CAM_In (CHAVE) CAM_Out (CHAVE)

ltimo elemento

CAM esttica

ltimo elemento CAM[0] CAM[1] CAM[2] CAM[3] ... CAM[N-1]

Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro Ponteiro

CHAVE 1

CHAVE 2

CHAVE 4

CHAVE 6

Elemento Elemento Elemento Elemento


pesquisa binria

CAM_In (CHAVE) CAM_Out (CHAVE)

CAM semiesttica

Figura 5.20 - Implementaes esttica e semiesttica da memria associativa.

No caso da implementao semiesttica possvel criar uma memria associativa abstracta. Para isso, necessrio providenciar a funo de criao da memria associativa CAM_Create que concretiza o tipo de elementos, atravs da especificao do seu tamanho em bytes, e a funo de destruio da memria associativa CAM_Destroy que alm de libertar a memria dinmica atribuda para os elementos, coloca o indicador de tamanho dos elementos de novo a zero e o contador de elementos teis a zero. Ou seja, coloca o indicador do ltimo elemento til da memria associativa no incio do agregado. Para obter uma implementao mais eficiente da memria associativa, essencial utilizar uma estrutura de dados que evite a necessidade de fazer deslocamentos dos elementos, quando se pretende colocar ou retirar um elemento da memria. Para isso precisamos de uma estrutura de dados dinmica em que os elementos podem ser colocados e retirados por ajustamento de ligaes entre os elementos. Mas, no pode ser uma lista ligada, porque para pesquisar a memria procura de um elemento com uma determinada chave, a memria tem de ser pesquisada em ambos os sentidos. Pelo que, a lista tem de ser biligada. Uma lista biligada uma lista em que cada n tem um ponteiro para o n seguinte e um ponteiro para o n anterior. Sendo que, o n inicial aponta para trs para NULL e o n final aponta para a frente para NULL, para servirem de indicadores de finalizao da lista. Colocar ou retirar um elemento numa lista biligada, implica fazer ou desfazer mais ligaes. Mas, em contrapartida todas as operaes de ligao ou desligao ao n de atrs e ao n da frente so possveis de fazer, tendo apenas um ponteiro a indicar, o n atrs ou frente do qual se vai colocar o novo elemento, ou o n do elemento que vai ser eliminado. A Figura 5.21 apresenta esta implementao dinmica linear, que mantm um ponteiro para o primeiro n da lista, que se designa por cabea da memria. O inconveniente desta implementao tem a ver com a eficincia da pesquisa de informao. Numa lista, seja ela ligada ou biligada, s se pode implementar a pesquisa sequencial, a partir da cabea da lista.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

24

cabea da CAM

PtSeg PtAnt PtEle

PtSeg PtAnt PtEle

PtSeg PtAnt PtEle

PtSeg PtAnt PtEle

CHAVE 1

CHAVE 2

CHAVE 4

CHAVE 6

Elemento

Elemento
pesquisa

Elemento
sequencial

Elemento

CAM dinmica linear

CAM_In (CHAVE) CAM_Out (CHAVE)

Figura 5.21 - Implementao dinmica linear da memria associativa.

Para optimizarmos a pesquisa pode-se optar por outra estrutura de dados que implemente uma pesquisa ainda mais eficiente que a pesquisa binria, mas, que seja ainda uma estrutura de dados em que os elementos possam ser colocados e retirados sem custos de deslocamentos de elementos. Tal estrutura de dados, que se apresenta na Figura 5.22, uma tabela de disperso (hashing table ).
CAM_In (CHAVE) CAM_Out (CHAVE) CAM[0] Ponteiro CAM[1] Ponteiro
1 pesquisa por disperso

PtSeg PtEle

PtSeg PtEle

. . .

. . .

CHAVE 1

CHAVE 41

Elemento

Elemento

2 pesquisa sequencial

CAM[N-2] Ponteiro CAM[N-1] Ponteiro

PtSeg PtEle

CHAVE 2

CAM semiesttica/dinmica

Elemento

Figura 5.22 - Implementao da memria associativa com tabela de disperso.

Uma tabela de disperso um agregado, onde os elementos so colocados em posies determinadas por uma funo de disperso (hashing function ). Uma funo de disperso uma funo aritmtica que determina a posio de colocao do elemento, usando a chave do elemento. Portanto, os elementos no so colocados em posies seguidas do agregado, mas sim em posies que dependem da chave do elemento. Uma vez que o nmero de posies de armazenamento, ou seja, a dimenso do agregado tipicamente vrias ordens de grandeza menor do que o nmero total de chaves possveis, podem ocorrer as situaes de overflow e coliso. A situao de overflow acontece quando a tabela fica completamente preenchida e como o agregado uma estrutura esttica no tem soluo, a no ser com um bom dimensionamento do agregado.

25

CAPTULO 5 : MEMRIAS

A situao de coliso acontece quando dadas duas chaves diferentes, a funo de disperso calcula a mesma a posio de colocao. O que compromete a implementao da memria, uma vez que, no se pode colocar dois elementos na mesma posio da tabela. Portanto, tem que se resolver o problema das colises. Uma maneira passa por reduzir o seu nmero, utilizando uma funo de disperso que assegure uma boa disperso. Mas, independentemente da funo usada vo existir sempre colises. Portanto, tem de ser criada uma estrutura de dados que permita colocar mais do que um elemento na mesma posio da tabela. Uma soluo possvel consiste em criar uma estrutura semiesttica em que cada elemento do agregado um ponteiro que aponta para uma lista ligada de elementos. Temos assim uma tabela de listas ligadas. Esta implementao semiesttica, sob o ponto de vista da estrutura de suporte, mas dinmica sob o ponto de vista da colocao e remoo dos elementos. Permite resolver o problema das colises e permite ainda a existncia na memria associativa de elementos distintos com a mesma chave. Elementos com a mesma chave vo ficar na mesma posio da tabela e podem ser colocados na lista ligada por ordem cronolgica da sua colocao na memria associativa, se por exemplo, a lista for implementada como uma fila. Para pesquisar a existncia de um elemento nesta implementao da memria associativa, seja para colocar um novo elemento, seja para retirar um elemento j existente, aplica-se a funo de disperso para a chave do elemento e obtm-se a posio da tabela onde o elemento deve estar colocado. Depois utiliza-se a pesquisa sequencial para analisar a lista ligada de elementos at detectar a posio de colocao ou remoo do elemento com a chave pretendida. Como o nmero de elementos existente na lista ligada pequena, a pesquisa sequencial aceitvel. Por outro lado, nesta implementao a leitura sucessiva de elementos com a mesma chave aplicada facilmente. No entanto, a grande limitao da tabela de disperso tem a ver com o facto da estrutura de suporte ser um agregado, que uma estrutura de dados esttica. Se a memria associativa necessitar de crescer esta soluo no a mais adequada. Portanto, necessria uma estrutura de dados dinmica que permita colocar e retirar elementos de forma eficiente, como na lista biligada, mas que suporte tambm uma pesquisa eficiente, como o caso da pesquisa binria. Tal estrutura de dados a rvore binria de pesquisa. A Figura 5.23 apresenta esta implementao dinmica hierrquica. Uma rvore uma estrutura de dados constituda por uma coleco de ns. Esta coleco pode ser nula, ou constituda por um n inicial, que se designa por raiz da rvore, e zero ou mais subrvores. Portanto, uma estrutura de dados com uma organizao recursiva, em que cada n tambm uma rvore. Uma rvore diz-se binria se cada n no tiver mais do que duas subrvores, a subrvore da esquerda e a subrvore da direita. Numa rvore binria de pesquisa, um elemento colocado na rvore de maneira que, os elementos da sua subrvore da esquerda tm uma chave menor do que a sua chave e os elementos da sua subrvore da direita tm uma chave maior do que a sua chave. Para implementar uma rvore binria, cada n da rvore constitudo por trs ponteiros. Um para o elemento que armazena a informao e os outros dois para os ns seguintes da rvore, ou seja, para as subrvores da esquerda e da direita. Quando o n seguinte no existe, ento o respectivo ponteiro aponta para NULL, para servir de indicador de inexistncia da subrvore.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

Figura 5.23 - Implementao dinmica hierrquica da memria associativa.

CAM dinmica hierrquica

raiz da CAM PtEle

CAM_In (CHAVE) CAM_Out (CHAVE)


pesquisa binria

PtEsq

PtDir

CHAVE 4

PtEsq

PtEle

PtDir

Elemento

PtEsq

PtEle

PtDir

CHAVE 2

CHAVE 6

PtEsq

PtEle

PtDir

Elemento

PtEsq

PtEle

PtDir

Elemento

PtEsq

PtEle

PtDir

CHAVE 1

CHAVE 3

CHAVE 8

Elemento

Elemento

Elemento

26

27

CAPTULO 5 : MEMRIAS

Uma rvore binria diz-se completa, ou completamente balanceada, quando, para um nmero de nveis previamente fixado, contm o nmero mximo de ns, pelo que, se verifica que N = 2K1, em que N o nmero de ns e K o nmero de nveis da rvore. Se uma rvore estiver balanceada, ento tambm as suas subrvores da esquerda e da direita o estaro. Por definio, considera-se que uma rvore vazia, ou seja, uma rvore sem qualquer n, est balanceada. Numa rvore binria de pesquisa pode ser aplicada a pesquisa binria de forma recursiva, mas, s em rvores completamente balanceadas que se consegue obter uma pesquisa proporcional a log2N. Portanto, fundamental garantir-se em cada instante que a rvore se encontra organizada numa estrutura to prxima quanto possvel do balanceamento completo. Daqui resulta que, para se garantir a operacionalidade mxima de uma organizao hierrquica em rvore binria, se deve conceber as operaes para colocar e retirar elementos, como operaes invariantes em termos de balanceamento.

5.10 Atribuio dinmica de memria


Para permitir a construo de estruturas de dados semiestticas e dinmicas, a linguagem C permite a atribuio dinmica de memria durante a execuo do programa. A biblioteca de execuo ANSI stdlib providencia as quatro funes que se apresentam na Figura 5.24.
Nome da funo Significado void *malloc (size_t sz); atribui sz bytes na memria. Devolve um ponteiro para o incio do bloco atribudo ou NULL no caso contrrio void *calloc (size_t nelem, size_t sz); atribui um espao de memria contguo de nelem objectos de sz bytes cada. Todos os bits do espao de memria atribudo so inicializados a zero. Devolve um ponteiro para o incio do bloco atribudo ou NULL no caso contrrio void *realloc (void *ptr, size_t nsz); altera o tamanho do espao de memria atribudo anteriormente e apontado por ptr para nsz bytes. Devolve um ponteiro para o incio do bloco atribudo ou NULL no caso contrrio void free (void *ptr); liberta o espao de memria previamente atribudo pelas funes anteriores e apontado por ptr

Figura 5.24 - Funes para gesto da memria dinmica.

A Figura 5.25 apresenta um exemplo de atribuio dinmica de memria. Pretende-se escrever um programa que leia um ficheiro de texto constitudo por nmeros inteiros, um por linha, para os ordenar e imprimir no monitor. Vamos admitir, que o nmero de nmeros inteiros armazenados no ficheiro depende de ficheiro para ficheiro e se encontra armazenado na primeira linha do ficheiro, que se comporta como cabealho do ficheiro. Uma vez que, a dimenso do agregado dependente do tamanho do ficheiro, no podemos declarar o agregado de forma esttica, sob pena de o dimensionarmos com tamanho insuficiente. A no ser que se dimensione o agregado por excesso, prevendo o pior caso. Normalmente, no possvel implementar esta estratgia, uma vez que, na maioria das aplicaes praticamente impossvel prever o pior caso. Nem to pouco desejvel, porque gera um desperdcio de memria na maioria dos casos.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

28

A soluo correcta passa por criar o agregado dinamicamente, recorrendo funo calloc, assim que se saiba o nmero de nmeros contido no ficheiro. Para tal declara-se o ponteiro PARINT que vai receber o endereo devolvido pela funo, que o endereo inicial do bloco de memria atribudo para o agregado. Devido dualidade ponteiro agregado, o ponteiro PARINT utilizado para aceder aos elementos do agregado, tal como se fosse um agregado. O programa antes de terminar, deve libertar a memria atribuda dinamicamente, usando para o efeito a funo free.
#include <stdio.h> #include <stdlib.h> int main (int argc, char *argv[]) { FILE *FP; int *PARINT, N, I; if ( argc < 2 ) /* o nmero de argumentos suficiente? */ { fprintf (stderr, "Uso: %s nome do ficheiro\n", argv[0]); exit (EXIT_FAILURE); } /* abertura do ficheiro de entrada cujo nome argv[1] */ if ( (FP = fopen (argv[1], "r")) == NULL ) { fprintf (stderr, "No foi possvel abrir o ficheiro %s\n"\ , argv[1]); exit (EXIT_FAILURE); } /* leitura do nmero de nmeros inteiros armazenado no ficheiro */ fscanf (FP, "%d", &N); /* atribuio de memria para um agregado de N inteiros */ if ( (PARINT = (int *) calloc (N, sizeof (int))) == NULL ) { fprintf (stderr, "No foi possvel atribuir o agregado\n"); fclose (FP); /* fecho do ficheiro de entrada */ exit (EXIT_FAILURE); } for (I = 0; I < N; I++) /* leitura do ficheiro para o agregado */ fscanf (FP, "%d", &PARINT[I]); fclose (FP); ... return EXIT_SUCCESS; } /* fecho do ficheiro de entrada */ /* processamento do agregado */

free (PARINT);/* libertao da memria atribuda para o agregado */

Figura 5.25 - Exemplo de aplicao da funo calloc.

5.11 Leituras recomendadas


x 4 captulo do livro Data Structures, Algorithms and Software Principles in C, de Thomas A. Standish, da editora Addison-Wesley Publishing Company, 1995. x 8 captulo do livro C A Software Approach, 3 edio, de Peter A. Darnell e Philip E. Margolis, da editora Springer-Verlag, 1996.

Captulo 6
PESQUISA E ORDENAO

Sumrio
Uma das tarefas mais habituais na programao a pesquisa de informao, o que exige o desenvolvimento de algoritmos eficientes de pesquisa. Neste captulo introduzimos os mtodos de pesquisa sequencial, de pesquisa binria e de pesquisa por tabela. Apresentamos vrios algoritmos que utilizam o mtodo de pesquisa sequencial e o algoritmo de pesquisa binria, nas verses iterativa e recursiva. Por outro lado a pesquisa fortemente condicionada pela organizao da informao, e por conseguinte, a ordenao uma tarefa ainda mais frequente. Apresentamos as classes mais simples dos algoritmos de ordenao, que so, a ordenao por seleco, a ordenao por troca e a ordenao por insero. Para cada uma delas expomos os algoritmos mais simples, que apesar de no serem os mais eficientes, so contudo suficientemente rpidos para pequenos agregados e apropriados para mostrar as caractersticas dos princpios de ordenao. Explicamos os algoritmos de ordenao Sequencial ( Sequential ), Seleco ( Selection ), Bolha ( Bubble ), Concha ( Shell ), Crivo ( Shaker ) e Insero ( Insertion ), mostramos a sua execuo com um pequeno agregado e fazemos a comparao do desempenho de cada um deles. Apresentamos tambm o algoritmo de ordenao Fuso de Listas ( Merge List ) que muito verstil, porque pode ser aplicado, quer na ordenao de agregados, quer na ordenao de ficheiros. Apresentamos ainda os algoritmos de ordenao recursivos Fuso ( Merge ) e Rpido ( Quick ), que so muito eficientes. Finalmente, explicamos como generalizar os algoritmos de ordenao e como contabilizar as instrues de comparao e de atribuies de elementos, de modo a poder fazer testes de desempenho dos algoritmos.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

6.1 Pesquisa
Uma das tarefas mais comuns no dia a dia a pesquisa de informao. Mas, a pesquisa depende muito da forma como a informao est organizada. Se a informao estiver completamente desordenada no temos outra alternativa que no seja analisar toda a informao por ordem, seja ela do princpio para o fim ou vice-versa, at encontrar o que pretendemos. Este processo de pesquisa normalmente lento. Imagine, por exemplo, procurar a nota de um teste numa pauta no ordenada, no caso de uma disciplina com algumas centenas de alunos. Mas, se a informao estiver ordenada por uma ordem, seja ela crescente ou decrescente no caso de informao numrica, ascendente ou descendente no caso de informao textual, como por exemplo a mesma pauta listada por ordem do nmero mecanogrfico, ento possvel fazer uma procura mais ou menos selectiva, partindo a informao a procurar em intervalos sucessivos cada vez menores, evitando assim analisar informao irrelevante e acelerar o processo de pesquisa. E, se a informao alm de estar ordenada por ordem, estiver ainda composta com entradas especficas, como por exemplo, uma pauta de uma disciplina com alunos de vrios cursos, que se apresenta dividida em pautas por cursos, cada uma delas ordenada por nmero mecanogrfico, ento possvel fazer uma procura dirigida pauta do respectivo curso e depois fazer uma procura em funo do nmero mecanogrfico, optimizando ainda mais o processo de pesquisa. Portanto, o mtodo de pesquisa inevitavelmente dependente da forma como a informao est organizada e apresentada. Quanto mais ordenada estiver a informao, mais eficiente poder ser o mtodo de pesquisa. Tal como no dia a dia, a pesquisa de informao tambm uma tarefa trivial em programao. Pesquisar um agregado procura da localizao de um determinado valor, ou de uma determinada caracterstica acerca dos seus valores, uma tarefa muito frequente e simples. Mas, computacionalmente dispendiosa porque, o agregado pode ser constitudo por centenas ou mesmo milhares de elementos. Pelo que, exige o desenvolvimento de algoritmos eficientes. A maneira mais simples de pesquisar um agregado a pesquisa sequencial ( sequencial search ), tambm chamada de pesquisa linear. Consiste em analisar todos os elementos do agregado de maneira metdica. A pesquisa comea no elemento inicial do agregado e avana elemento a elemento at encontrar o valor procurado, ou at atingir o elemento final do agregado. Este mtodo de pesquisa normalmente demorado, dependente do tamanho do agregado, mas, no depende do arranjo interno dos elementos no agregado. Independentemente da forma como a informao est armazenada no agregado, o valor procurado ser encontrado, caso exista no agregado. No entanto, se tivermos informao priori sobre os elementos do agregado possvel acelerar a pesquisa. Se por exemplo, os elementos estiverem ordenados por ordem crescente ou decrescente, ento possvel fazer uma pesquisa binria ( binary search ), tambm chamada de pesquisa logartmica. Este tipo de pesquisa comea por seleccionar o elemento central do agregado e compara-o com o valor procurado. Se o elemento escolhido for menor ento podemos excluir a primeira metade do agregado e analisamos apenas a segunda metade. Caso contrrio, ento podemos excluir a segunda metade do agregado e analisamos apenas a primeira metade. Em cada passo da pesquisa, o nmero de elementos do agregado que tm de ser analisados reduzido a metade, pelo que, este mtodo de pesquisa mais eficiente. O processo repetido at que o elemento seleccionado seja o valor procurado, ou at que o nmero de elementos que tm de ser

CAPTULO 6 : PESQUISA E ORDENAO

analisados seja reduzido a zero, o que significa, que o valor procurado no existe no agregado. Se os elementos do agregado, em vez de estarem colocados em posies sucessivas do agregado, estiverem colocados em posies predeterminadas do agregado, posies essas que so determinadas por uma funo de disperso ( hashing function ), ento possvel fazer uma pesquisa por tabela ( table search ), tambm chamada de pesquisa por disperso ( hashing ). Uma funo de disperso uma funo aritmtica que determina a posio de colocao do elemento no agregado, tendo em considerao o valor do elemento, ou no caso de um elemento estruturado, usando um campo do elemento como chave de colocao no agregado. No entanto, normalmente uma funo de disperso produz situaes de coliso. A situao de coliso acontece quando dados dois valores diferentes, a funo de disperso calcula a mesma a posio de colocao. Portanto, o agregado tem de permitir colocar mais do que um elemento numa mesma posio. Uma soluo possvel consiste em criar uma estrutura semiesttica em que cada elemento do agregado um ponteiro que aponta para uma lista ligada de elementos. Temos assim um agregado de listas ligadas. Este modelo de estrutura de dados, que se designa por uma tabela de disperso com encadeamento de elementos, pesquisado em dois tempos. Para procurar um elemento, primeiro usada a funo de disperso para, dado o valor do elemento desejado, calcular a sua posio de colocao no agregado e aceder directamente a essa posio. Depois utiliza-se a pesquisa sequencial para analisar a lista ligada de elementos at detectar o elemento com o valor pretendido. Este mtodo de pesquisa ser abordado mais tarde a propsito da implementao de memrias associativas. Para os algoritmos apresentados neste captulo vamos considerar que a estrutura de dados a pesquisar ou ordenar, um agregado de elementos inteiros de nome seq, com capacidade para armazenar NMAX elementos, tendo no entanto apenas nelem elementos teis. Tendo em considerao este agregado de elementos inteiros, vamos analisar vrios algoritmos que utilizam a pesquisa sequencial e o algoritmo de pesquisa binria nas suas verses iterativa e recursiva. Vamos considerar que as funes s so invocadas para agregados que contm de facto elementos com informao, ou seja, nelem maior do que zero, pelo que, os algoritmos podem ser simplificados, uma vez que no tm que se preocupar com esta situao anmala.

6.1.1 Pesquisa Sequencial


Com o objectivo de tornar as funes, o mais versteis possveis, elas tm uma varivel de entrada que indica o ndice do agregado onde deve comear a pesquisa. As funes calculam e devolvem o ndice do elemento onde est armazenado o valor procurado.

6.1.1.1 Procurar o maior valor de um agregado


A Figura 6.1 apresenta a funo que determina, a partir de um ndice inicial de pesquisa, o ndice do elemento com o maior valor armazenado na parte restante do agregado. No caso de existir mais do que um elemento com o mesmo valor a funo devolve o ndice do primeiro elemento encontrado. No incio assume-se que o maior valor o elemento de ndice inicial e depois o agregado analisado at ao ltimo elemento, procura de um valor ainda maior. Se pretendermos procurar o maior valor armazenado no agregado, basta invocar a funo a partir do primeiro elemento do agregado.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

unsigned int ProcurarMaior (int seq[], unsigned int nelem,\ unsigned int inicio) { unsigned int indmaior = inicio, indactual; for (indactual = inicio+1; indactual < nelem; indactual++) if (seq[indactual] > seq[indmaior]) indmaior = indactual; return indmaior; }

Figura 6.1 - Funo que determina o elemento de maior valor do agregado.

6.1.1.2 Procurar o menor valor de um agregado


A Figura 6.2 apresenta a funo que determina, a partir de um ndice inicial de pesquisa, o ndice do elemento com o menor valor armazenado na parte restante do agregado. No caso de existir mais do que um elemento com o mesmo valor a funo devolve o ndice do primeiro elemento encontrado. No incio assume-se que o menor valor o elemento de ndice inicial e depois o agregado analisado at ao ltimo elemento, procura de um valor ainda menor. Se pretendermos procurar o menor valor armazenado no agregado, basta invocar a funo a partir do primeiro elemento do agregado.
unsigned int ProcurarMenor (int seq[], unsigned int nelem,\ unsigned int inicio) { unsigned int indmenor = inicio, indactual; for (indactual = inicio+1; indactual < nelem; indactual++) if (seq[indactual] < seq[indmenor]) indmenor = indactual; return indmenor; }

Figura 6.2 - Funo que determina o elemento de menor valor do agregado.

6.1.1.3 Procurar um valor no agregado


A Figura 6.3 apresenta a funo que determina, a partir de um ndice inicial de pesquisa, o ndice do elemento que armazena o valor que se pretende procurar na parte restante do agregado. Como no temos a certeza de que o valor existe de facto no agregado, se a pesquisa ultrapassar o ltimo elemento do agregado sem o ter encontrado, ento a funo devolve o ndice 1 como sinal de pesquisa falhada. A pesquisa deve acabar assim que a primeira ocorrncia do valor pretendido seja encontrado, pelo que, se usa a instruo de return dentro do ciclo for, para terminar a pesquisa assim que se encontrar um elemento com o valor pretendido. Se pretendermos pesquisar todo o agregado, basta invocar a funo a partir do primeiro elemento do agregado.
int ProcurarValor (int seq[], unsigned int nelem,\ unsigned int inicio, int valor) { unsigned int indactual; for (indactual = inicio; indactual < nelem; indactual++) if (seq[indactual] == valor) return indactual; return -1; }

Figura 6.3 - Funo de pesquisa sequencial que procura um valor no agregado.

CAPTULO 6 : PESQUISA E ORDENAO

6.1.1.4 Procurar o primeiro que serve


Existem alguns problemas concretos do dia a dia, cuja soluo optimizada requer que se procure um determinado valor num agregado, mas, se este valor no existir podemos em alternativa utilizar um valor prximo do valor pretendido. Nesta situao podemos aplicar uma de trs estratgias. A primeira e mais simples consiste em procurar no agregado, o primeiro valor que no excede o valor procurado. Este algoritmo designa-se por o primeiro que serve ( first fit ). A Figura 6.4 apresenta a funo que determina, a partir de um ndice inicial de pesquisa, o ndice do primeiro elemento do agregado com um valor que no excede o valor procurado. Como nesta pesquisa, no temos a certeza de que existe de facto tal valor, se a pesquisa ultrapassar o ltimo elemento do agregado sem ter encontrado um valor, ento a funo devolve o ndice 1 como sinal de pesquisa falhada. Como o nome indica, a pesquisa deve acabar assim que seja encontrado o primeiro valor que sirva, pelo que, se usa a instruo de return dentro do ciclo for, para terminar a pesquisa assim que se encontrar um elemento com o valor pretendido. Se pretendermos pesquisar todo o agregado, basta invocar a funo a partir do primeiro elemento do agregado.
int ProcurarPrimeiro (int seq[], unsigned int nelem,\ unsigned int inicio, int valor) { unsigned int indactual; for (indactual = inicio; indactual < nelem; indactual++) if (seq[indactual] <= valor) return indactual; return -1; }

Figura 6.4 - Funo que determina o elemento do agregado com o primeiro valor que serve.

6.1.1.5 Procurar o melhor que serve


A segunda estratgia de optimizao consiste em procurar no agregado, o valor mais prximo do valor pretendido, ou seja, o melhor valor. Este algoritmo designa-se por o melhor que serve ( best fit ). A Figura 6.5 apresenta a funo que determina, a partir de um ndice inicial de pesquisa, o ndice do elemento do agregado com o melhor valor que no excede o valor pretendido. Comea-se por procurar o primeiro valor que serve. Caso ele no exista, a funo devolve o ndice 1 como sinal de pesquisa falhada. Caso contrrio procura-se at ao ltimo elemento do agregado um elemento com um valor melhor, ou seja, um elemento com um valor que seja ainda maior do que o j encontrado e que no exceda o valor procurado. Se pretendermos pesquisar todo o agregado, basta invocar a funo a partir do primeiro elemento do agregado.
int ProcurarMelhor (int seq[], unsigned int nelem,\ unsigned int inicio, int valor) { int indmelhor; unsigned int indactual; indmelhor = ProcurarPrimeiro (seq, nelem, inicio, valor); if (indmelhor == -1 ) return -1; for (indactual = indmelhor+1; indactual < nelem; indactual++) if (seq[indactual] > seq[indmelhor] && seq[indactual] <= valor) indmelhor = indactual; return indmelhor; }

Figura 6.5 - Funo que determina o elemento do agregado com o melhor valor que serve.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

6.1.1.6 Procurar o pior que serve


A terceira estratgia de optimizao consiste em procurar no agregado, o valor menos prximo do valor pretendido, ou seja, o pior valor. Este algoritmo designa-se por o pior que serve ( worst fit ). Por vezes, uma estratgia pela negativa melhor do que pela positiva. Imagine-se por exemplo, que se pretende aproveitar uma sobra de 100 cm de uma calha de alumnio e que temos a necessidade de cortar seces de 80 cm, 50 cm e 40 cm. Uma estratgia o melhor que serve escolher o valor 80 cm, desperdiando 20 cm, enquanto que uma estratgia o pior que serve escolher sucessivamente os valores 40 cm e 50 cm, desperdiando apenas 10 cm. A Figura 6.6 apresenta a funo que determina, a partir de um ndice inicial de pesquisa, o ndice do elemento do agregado com o pior valor que no excede o valor pretendido. Comea-se por procurar o primeiro valor que serve. Caso ele no exista, a funo devolve o ndice 1 como sinal de pesquisa falhada. Caso contrrio procura-se at ao ltimo elemento do agregado um elemento com um valor pior, ou seja, um elemento com um valor que seja ainda menor do que o j encontrado. Se pretendermos pesquisar todo o agregado, basta invocar a funo a partir do primeiro elemento do agregado.
int ProcurarPior (int seq[], unsigned int nelem,\ unsigned int inicio, int valor) { int indpior; unsigned int indactual; indpior = ProcurarPrimeiro (seq, nelem, inicio, valor); if (indpior == -1 ) return -1; for (indactual = indpior+1; indactual < nelem; indactual++) if (seq[indactual] < seq[indpior]) indpior = indactual; return indpior; }

Figura 6.6 - Funo que determina o elemento do agregado com o pior valor que serve.

6.1.1.7 Exemplificao dos algoritmos de pesquisa sequencial


A Figura 6.7 apresenta a aplicao destes algoritmos num agregado com vinte elementos teis. Se pesquisarmos o agregado a comear no elemento de ndice 7, temos que, o maior valor o elemento com ndice 19, cujo valor 250, e o menor valor o elemento com ndice 11, cujo valor 1. Se procurarmos um elemento no agregado com valor 55, a comear no elemento de ndice 7, encontramos o elemento com ndice 16. Mas, se procurarmos um elemento com valor 40, no conseguimos encontrar nenhum valor, pelo que, a funo devolve o ndice 1. O primeiro elemento com valor que no excede 40, a comear no elemento de ndice 7, o elemento com ndice 7, cujo valor 32. Nas mesmas circunstncias, o elemento com o melhor valor o elemento com ndice 15, cujo valor 39, e o elemento com o pior valor o elemento com ndice 11, cujo valor 1. No entanto, se comearmos a pesquisa no elemento de ndice 17 do agregado, no encontraremos nenhum valor que no exceda 40, pelo que, as funes, o primeiro que serve, o melhor que serve e o pior que serve devolvem o ndice 1.

CAPTULO 6 : PESQUISA E ORDENAO

Posio Inicial de Pesquisa

Menor Valor

Valor 55

Maior Valor

20
0

3 330 25 22 24 15 32 42
1 2 3 4 5 6 7 8

2
9

5
10

1
11

8
12

7
13

6
14

39 55 145 209 250


15 16 17 18 19

Primeiro Valor<=40

Pior Valor<=40

Melhor Valor<=40

Figura 6.7 - Resultados da utilizao dos algoritmos de pesquisa sequencial.

6.1.2 Pesquisa Binria


Uma forma de acelerar a pesquisa consiste em utilizar uma estratgia de partio sucessiva do agregado ao meio para diminuir o nmero de elementos a analisar. Mas, este mtodo de pesquisa s funciona se os elementos estiverem ordenados. A Figura 6.8 apresenta a funo de pesquisa binria, na sua verso iterativa, que calcula o ndice do elemento com o valor procurado. Como no temos a certeza que o valor procurado existe de facto no agregado, a funo devolve o ndice 1 como sinal de pesquisa falhada. Com o objectivo de tornar a funo o mais verstil possvel, a funo tem duas variveis de entrada que indicam os ndices dos elementos onde deve comear e acabar a pesquisa.
int ProcuraBinariaIterativa (int seq[], unsigned int inicio,\ unsigned int fim, int valor) { unsigned int minimo = inicio, maximo = fim, medio; while (minimo <= maximo) { medio = (minimo + maximo) / 2;

/* clculo da posio mdia */

if (seq[medio] == valor) return medio; /* pesquisa com sucesso */ /* actualizao dos limites do intervalo de pesquisa */ if (seq[medio] > valor) maximo = medio - 1; else minimo = medio + 1; return -1; } /* pesquisa falhada */

Figura 6.8 - Funo de pesquisa binria que procura um valor no agregado (verso iterativa).

Vamos mostrar na Figura 6.9, o funcionamento deste algoritmo, para o caso de um agregado com vinte elementos ordenado por ordem crescente. Pretendemos procurar o valor 34. Vamos invocar a funo para todos os elementos teis do agregado, ou seja, do elemento de ndice 0 at ao elemento nelem1, que neste caso o elemento de ndice 19. Calcula-se o elemento mdio do agregado, atravs da diviso inteira da soma das posies mnima e mxima, e que neste caso o elemento de ndice 9. Como o valor procurado, que 34, menor do que valor armazenado no elemento mdio, neste caso 44, ento isso significa que ele se encontra na primeira metade do agregado, pelo que, a posio mxima passa para o elemento esquerda da posio mdia, ou seja, a nova posio mxima agora o elemento de ndice 8. Se pelo contrrio, o valor procurado fosse maior do que 44 ento isso significava que ele se encontrava na segunda metade do agregado, pelo que, a nova posio mnima passaria para o elemento direita da posio mdia, ou seja, a nova

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

posio mnima seria o elemento de ndice 10. Agora que estamos a analisar a primeira metade do agregado, calcula-se a nova posio mdia, que o elemento de ndice 4. Como o valor procurado maior do que valor armazenado no elemento mdio, que 26, ento isso significa que ele se encontra na segunda metade do intervalo em anlise, pelo que, a posio mnima passa para o elemento direita da posio mdia, ou seja, para o elemento de ndice 5. Agora, a nova posio mdia passa a ser o elemento de ndice 6. Como o valor procurado ainda maior do que valor armazenado no elemento mdio, que 30, ento isso significa que ele se encontra na segunda metade do intervalo em anlise, pelo que, a posio mnima passa para o elemento direita da posio mdia, ou seja, para o elemento de ndice 7. A nova posio mdia passa agora a ser o elemento de ndice 7, cujo valor o valor procurado, pelo que, a pesquisa termina com sucesso e devolve o ndice 7. Para obtermos este resultado, foi preciso analisar quatro elementos do agregado enquanto que a pesquisa sequencial necessitaria de analisar oito elementos.
MIN MED MAX

3
0

7
1

20 25 26 28 30 34 42 44 50 60 68 75 86 99 125 145 209 250


2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

MIN

MED

MAX

3
0

7
1

20 25 26 28 30 34 42 44 50 60 68 75 86 99 125 145 209 250


2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

MIN MED

MAX

3
0

7
1

20 25 26 28 30 34 42 44 50 60 68 75 86 99 125 145 209 250


2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

MIN MAX

3
0

7
1

20 25 26 28 30 34 42 44 50 60 68 75 86 99 125 145 209 250


2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

MED

o valor 34 foi encontrado ao fim de 4 tentativas

MAX MIN

3
0

7
1

20 25 26 28 30 34 42 44 50 60 68 75 86 99 125 145 209 250


2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

MED

o valor 40 no foi encontrado ao fim de 5 tentativas

Figura 6.9 - Utilizao do algoritmo de pesquisa binria num agregado ordenado.

CAPTULO 6 : PESQUISA E ORDENAO

Se por exemplo, o valor procurado fosse 40, que maior do que 34, ento a nova posio mnima passaria para o elemento direita da posio mdia, ou seja, para o elemento de ndice 8, exactamente igual ao valor da posio mxima. Consequentemente, a nova posio mdia passaria a ser o elemento de ndice 8, cujo valor 42, pelo que, a posio mxima passaria para o elemento esquerda da posio mdia, ou seja, para o elemento de ndice 7 e portanto, as posies mnima e mxima trocavam de posio parando o ciclo de pesquisa. Como o valor armazenado no elemento cujo ndice a posio mdia final, que o elemento de ndice 8, diferente do valor procurado, ento a pesquisa terminaria sem sucesso e devolveria o ndice 1 como sinal de pesquisa falhada. Neste caso a funo analisava cinco elementos do agregado enquanto que a pesquisa sequencial necessitaria de analisar os vinte elementos do agregado para chegar ao mesmo resultado. Esta estratgia de pesquisa tambm pode ser implementada de forma recursiva, sendo que cada nova pesquisa analisa uma das metades do agregado anteriormente analisado at que o valor seja encontrado ou at no existirem mais elementos para analisar. A verso recursiva apresentada na Figura 6.10. Compare-a com a verso iterativa que foi apresentada na Figura 6.8. A funo comea por testar a situao de paragem no caso da pesquisa sem sucesso, o que acontece quando as posies inicial e final trocam de posio. Nesse caso, a funo devolve o ndice 1 como sinal de pesquisa falhada. Caso tal no se verifique, calcula o ndice do elemento central do agregado em anlise. Se o elemento for o valor procurado, estamos perante a situao de paragem com sucesso e a funo devolve o ndice do elemento central do agregado. Caso contrrio, em funo da comparao do valor procurado com o valor do elemento central, a funo invoca-se recursivamente de forma alternativa para a primeira metade ou para a segunda metade do agregado.
int ProcuraBinariaRecursiva (int seq[], unsigned int inicio,\ unsigned int fim, int valor) { unsigned int medio; /* condio de paragem no caso de pesquisa sem sucesso */ if (inicio > fim) return -1; medio = (inicio + fim) / 2; /* clculo da posio mdia */ /* condio de paragem no caso de pesquisa com sucesso */ if (seq[medio] == valor) return medio; if (seq[medio] > valor) ProcuraBinariaRecursiva (seq, inicio, medio-1, valor); /* invocao recursiva para a primeira metade do agregado */ else ProcuraBinariaRecursiva (seq, medio+1, fim, valor); /* invocao recursiva para a segunda metade do agregado */ }

Figura 6.10 - Funo de pesquisa binria que procura um valor no agregado (verso recursiva).

A implementao recursiva no tem qualquer vantagem sobre a implementao repetitiva, quando aplicada a agregados. No entanto normalmente aplicada para pesquisar estruturas de dados dinmicas organizadas de forma binria, como o caso das rvores binrias de pesquisa, permitindo combinar a eficincia da pesquisa binria com a flexibilidade destas estruturas de dados dinmicas.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

6.1.3 Comparao entre as pesquisas sequencial e binria


Dada a dificuldade em calcular com exactido a eficincia de um algoritmo, para exprimir a sua eficincia em funo do nmero de elementos processados, utiliza-se a notao matemtica conhecida por ordem de magnitude ou notao O maisculo (Big O Notation). A ordem de magnitude de uma funo igual ordem do seu termo que cresce mais rapidamente. Por exemplo, a funo f(n) = n2+n de ordem de magnitude O(n2), uma vez que para grandes valores de n, o termo n2 cresce mais rapidamente que o termo n e portanto, domina a funo. Para um agregado com N elementos, o pior caso da pesquisa sequencial quando o valor procurado est no ltimo elemento do agregado ou no existe no agregado, o que exige a anlise dos N elementos do agregado. Se considerarmos ainda, que a probabilidade do valor procurado estar em qualquer um dos elementos do agregado igual, ento a pesquisa sequencial analisa em mdia (N+1)/2 elementos. Se considerarmos que a probabilidade do valor no existir no agregado igual a ser qualquer um dos elementos do agregado, ento a pesquisa sequencial analisa em mdia (N+2)/2 elementos. Portanto, a eficincia do algoritmo de pesquisa sequencial de ordem O(N). Para o mesmo agregado, o pior caso da pesquisa binria quando se reduz o intervalo em anlise a apenas um elemento do agregado, o que exige a anlise de log2(N+1) elementos do agregado. Se considerarmos ainda, que a probabilidade do valor estar em qualquer um dos elementos do agregado igual, ento a pesquisa binria analisa em mdia log2(N+1) 1 elementos. Se considerarmos que a probabilidade do valor no existir no agregado igual a ser qualquer um dos elementos do agregado, ento a pesquisa binria analisa em mdia log2(N+1) elementos. Portanto, a eficincia do algoritmo de pesquisa binria de ordem O(log2 N).

6.2 Ordenao
A ordenao o processo de organizar um conjunto de objectos segundo uma determinada ordem. Como j foi dito anteriormente, se a informao estiver ordenada possvel utilizar algoritmos de pesquisa mais eficientes, como por exemplo a pesquisa binria ou a pesquisa por tabela. Pelo que, a ordenao uma tarefa muito importante no processamento de dados e feita para facilitar a pesquisa. Os algoritmos de ordenao so classificados em dois tipos. A ordenao de informao armazenada em agregados designa-se por ordenao interna. Enquanto que, a ordenao de informao armazenada em ficheiros, designa-se por ordenao externa. Neste captulo vamos apresentar alguns algoritmos de ordenao interna, se bem que alguns tambm possam ser utilizados para ordenao externa. Existem algoritmos de ordenao muito eficientes para ordenar agregados de grandes dimenses, mas so normalmente complexos. Pelo contrrio, os algoritmos de ordenao que vamos apresentar so simples, mas apropriados para mostrar as caractersticas dos princpios de ordenao. So normalmente pouco eficientes, mas so suficientemente rpidos para agregados de pequenas dimenses. Os algoritmos de ordenao que ordenam os elementos do agregado, no prprio agregado, fazendo para o efeito um rearranjo interno dos seus elementos, enquadram-se numa das trs seguintes categorias: ordenao por seleco; ordenao por troca; e ordenao por insero.

11

CAPTULO 6 : PESQUISA E ORDENAO

Para implementar os algoritmos de ordenao baseados em trocas de elementos vamos utilizar a funo Swap que se apresenta na Figura 6.11. A troca de dois elementos do agregado exige uma varivel temporria do mesmo tipo dos elementos do agregado e custa trs instrues de atribuio. Os elementos a trocar so parmetros de entrada-sada, pelo que, so passados por referncia.
void Swap (int *x, int *y) { int temp; temp = *x; *x = *y; *y = temp; }

Figura 6.11 - Funo para trocar dois elementos de um agregado de inteiros.

6.2.1 Ordenao por seleco


Os algoritmos de ordenao por seleco utilizam uma pesquisa sequencial. Fazem passagens sucessivas sobre todos os elementos de uma parte do agregado e em cada passagem seleccionam o elemento de menor valor de todos os elementos analisados, no caso de se pretender uma ordenao crescente, ou em alternativa, o elemento de maior valor de todos os elementos analisados, no caso de se pretender uma ordenao decrescente, colocando esse valor nos sucessivos elementos iniciais do agregado. Em cada passagem, um elemento, de entre todos os restantes elementos do agregado ainda no ordenados, colocado no stio certo. Mais concretamente, no caso de uma ordenao crescente, na primeira passagem, o menor valor de todos fica colocado no primeiro elemento do agregado, na segunda passagem, o segundo menor valor fica colocado no segundo elemento do agregado, e assim sucessivamente at que na ltima passagem o penltimo menor valor, ou seja, o segundo maior valor, fica colocado no penltimo elemento do agregado, e, consequentemente, o maior valor fica automaticamente colocado no ltimo elemento do agregado. Vamos agora apresentar dois algoritmos que aplicam este mtodo de ordenao. A Figura 6.12 apresenta o algoritmo de Ordenao Sequencial ( Sequencial Sort ) para a ordenao crescente do agregado. Cada elemento do agregado comparado com os restantes elementos do agregado abaixo dele. Se o elemento de cima, cujo ndice indi for menor que o elemento de baixo, cujo ndice indj, ento trocam-se os elementos. Para efectuar a troca dos elementos do agregado utilizada a funo Swap. S so analisados os primeiros nelem1 elementos, porque, quando se ordena o penltimo elemento do agregado, o ltimo elemento fica automaticamente ordenado.
void Sequential_Sort (int seq[], unsigned int nelem) { unsigned int indi, indj; for (indi = 0; indi < nelem-1; indi++) for (indj = indi+1; indj < nelem; indj++) if (seq[indi] > seq[indj]) Swap (&seq[indi], &seq[indj]); /* trocar os elementos */ }

Figura 6.12 - Algoritmo de ordenao Sequencial.

A Figura 6.13 apresenta a execuo do algoritmo para um agregado com 10 elementos. Em cada passagem o elemento ordenado fica a sombreado para melhor se observar a parte ordenada do agregado. Na nona passagem para alm do nono elemento, tambm o dcimo

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

elemento fica ordenado. O algoritmo executa nove passagens e faz um total de 45 comparaes e de 25 trocas para ordenar o agregado.
209 depois da primeira passagem NC=9 NT=3 depois da segunda passagem NC=8 NT=4 depois da terceira passagem NC=7 NT=4 depois da quarta passagem NC=6 NT=4 depois da quinta passagem NC=5 NT=3 depois da sexta passagem NC=4 NT=2 depois da stima passagem NC=3 NT=2 depois da oitava passagem NC=2 NT=2 depois da nona passagem NC=1 NT=1 TOTAL NC = 45 NT = 25 I 2 330 J I 2 8 330 J I 2 8 15 330 J I 2 8 15 25 330 J I 2 8 15 25 32 330 J I 2 8 15 25 32 42 330 J I 2 8 15 25 32 42 55 330 J I 2 8 15 25 32 42 55 145 330 J I 2 8 15 25 32 42 55 145 209 330 J 209 J 209 145 J 209 55 145 J 209 42 55 145 J 209 42 32 55 145 J 209 42 32 25 55 145 J 209 42 25 32 15 55 145 J 209 25 42 15 32 8 55 145 J 330 25 15 42 2 32 8 55 145

15

25

32

42

55

145

209

330

Figura 6.13 - Execuo do algoritmo de ordenao Sequencial.

13

CAPTULO 6 : PESQUISA E ORDENAO

Para um agregado com N elementos, todos os elementos do agregado so comparados com todos os outros elementos, pelo que, este algoritmo faz (N 2N)/2 comparaes e o nmero de trocas se bem que dependente do grau de desordenao dos elementos, no pior caso pode atingir tambm as (N2N)/2 trocas. Portanto, este algoritmo tem uma eficincia de comparao de ordem O(N2) e uma eficincia de trocas de ordem O(N2), pelo que, pertence classe O(N2). Este algoritmo tem a desvantagem de eventualmente fazer trocas desnecessrias de elementos. Pelo que, para reduzir o nmero de trocas, em cada passagem deve-se detectar o elemento de menor valor, memorizando-se para o efeito o ndice onde ele se encontra armazenado e s depois de comparar o elemento com os restantes elementos do agregado, que se faz a troca do elemento analisado com o elemento de menor valor. A Figura 6.14 apresenta o algoritmo de Ordenao Seleco ( Selection Sort ), que implementa esta optimizao das trocas, para a ordenao crescente do agregado. Existe a varivel auxiliar indmin que no incio de cada passagem inicializada a indi e que representa o ndice do elemento de menor valor detectado durante a passagem. Cada elemento do agregado comparado com os restantes elementos do agregado abaixo dele. Se o elemento de menor valor, cujo ndice indmin for menor que o elemento, cujo ndice indj, ento este elemento com ndice indj o novo menor valor at ento detectado, pelo que, o ndice indmin actualizado a indj. No final da passagem pelo agregado, se indmin for diferente de indi, ento sinal que o elemento de menor valor no est na posio inicial indi e, portanto, preciso trocar os dois elementos.
void Selection_Sort (int seq[], unsigned int nelem) { unsigned int indi, indj, indmin; for (indi = 0; indi < nelem-1; indi++) { indmin = indi; /* o menor valor est na posio i */ for (indj = indi+1; indj < nelem; indj++) if (seq[indmin] > seq[indj]) indmin = indj; /* o menor valor est na posio j */ if (indmin != indi) Swap (&seq[indi], &seq[indmin]); } } /* trocar os elementos */

Figura 6.14 - Algoritmo de ordenao Seleco.

A Figura 6.15 apresenta a execuo do algoritmo para um agregado com 10 elementos e o algoritmo executa nove passagens para ordenar o agregado, sendo o nmero total de comparaes igual ao do algoritmo anterior. Na quarta e na nona passagens no efectuada qualquer troca e nas restantes passagens efectuada apenas uma troca, pelo que, o nmero de trocas de apenas 7 contra as 25 do algoritmo anterior. Para um agregado com N elementos, este algoritmo faz tantas comparaes como o algoritmo Sequencial. No entanto, como faz no mximo uma troca por passagem, o nmero de trocas no pior caso pode atingir as N1 trocas. Este algoritmo tem uma eficincia de comparao de ordem O(N2) e uma eficincia de trocas de ordem O(N). Apesar de ser mais eficiente no nmero de trocas efectuadas, no entanto, um algoritmo que pertence classe O(N2).

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

209 depois da primeira passagem NC=9 NT=1 depois da segunda passagem NC=8 NT=1 depois da terceira passagem NC=7 NT=1 depois da quarta passagem NC=6 NT=0 depois da quinta passagem NC=5 NT=1 depois da sexta passagem NC=4 NT=1 depois da stima passagem NC=3 NT=1 depois da oitava passagem NC=2 NT=1 depois da nona passagem NC=1 NT=0 TOTAL NC = 45 NT = 7 I 2

330

25

15

42

2 MIN

32

55

145

330 J I

25

15

42

209

32

55

145 J

MIN 25 J I MIN 25 J I 42 209 32 330 55 145 J 15 42 209 32 330 55 145 J

15

15

25 MIN

42 J I

209

32

330

55

145 J

MIN 209 J I MIN 209 J I MIN 330 J I 209 145 J MIN 209 J I 330 J 330 55 145 J 42 330 55 145 J

15

25

32

15

25

32

42

15

25

32

42

55

15

25

32

42

55

145

15

25

32

42

55

145

209 MIN

330 J

15

25

32

42

55

145

209

330

Figura 6.15 - Execuo do algoritmo de ordenao Seleco.

15

CAPTULO 6 : PESQUISA E ORDENAO

6.2.2 Ordenao por troca


Os algoritmos de ordenao por troca, fazem passagens sucessivas sobre o agregado, comparando elementos que se encontram a uma distncia fixa, que caso estejam fora de ordem so trocados. Quando durante uma passagem no se tiver efectuado qualquer troca, ento sinal de que os elementos que se encontram distncia de comparao j esto ordenados, pelo que, a ordenao termina. Vamos agora apresentar trs algoritmos que aplicam este mtodo de ordenao. A Figura 6.16 apresenta o algoritmo de Ordenao Bolha ( Bubble Sort ) para a ordenao crescente do agregado. Existe a varivel auxiliar indinicial que inicializada com o ndice do segundo elemento do agregado e que representa o ndice do elemento da parte inicial do agregado onde termina a comparao de elementos em cada passagem. Para todos os elementos finais do agregado, desde o ltimo elemento at ao elemento de ndice indinicial, cada elemento comparado com o elemento atrs dele, ou seja, compara-se o elemento de ndice indi com o elemento de ndice indi1, e caso o valor seja menor trocam-se os elementos. Deste modo os elementos de menor valor vo sendo deslocados em direco parte inicial do agregado. Em cada passagem, contabiliza-se o nmero de trocas efectuadas, e quando uma passagem no tiver efectuado qualquer troca, isso sinal de que o agregado j est ordenado. Em cada passagem, pelo menos um novo elemento fica ordenado, pelo que, a varivel indinicial incrementada de uma unidade, de maneira a evitar fazer-se comparaes desnecessrias com os elementos que j esto ordenados na parte inicial do agregado. Portanto, a ordenao tambm acaba quando a varivel indinicial excede o fim do agregado, ou seja, ao fim de nelem1 passagens.
void Bubble_Sort (int seq[], unsigned int nelem) { unsigned int indi, indinicial, ntrocas; indinicial = 1; do { ntrocas = 0; /* inicializar o contador de trocas */ for (indi = nelem-1; indi >= indinicial; indi--) if (seq[indi-1] > seq[indi]) { Swap (&seq[indi], &seq[indi-1]); /* trocar os elementos */ ntrocas++; } indinicial++; /* actualizar o limite superior de ordenao */ } while (ntrocas && indinicial < nelem); } /* actualizar o nmero de trocas efectuadas */ /* inicializar o limite superior de ordenao */

Figura 6.16 - Algoritmo de ordenao Bolha.

A Figura 6.17 apresenta a execuo do algoritmo para um agregado com 10 elementos. Uma vez que o agregado est bastante desordenado, o algoritmo executa nove passagens, se bem que na ltima no efectua qualquer troca. No entanto mesmo que efectuasse trocas na nona passagem, o algoritmo terminaria na mesma, uma vez que, o indicador de posio inicial excedeu o fim do agregado. O algoritmo faz um total de 45 comparaes e de 25 trocas para ordenar o agregado.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

209 depois da primeira passagem NC=9 NT=6 depois da segunda passagem NC=8 NT=5 depois da terceira passagem NC=7 NT=4 depois da quarta passagem NC=6 NT=2 depois da quinta passagem NC=5 NT=2 depois da sexta passagem NC=4 NT=2 depois da stima passagem NC=3 NT=2 depois da oitava passagem NC=2 NT=2 depois da nona passagem NC=1 NT=0 TOTAL NC = 45 NT = 25

330

25

15

42

32

55

145

INICIAL 2 209 I INICIAL 2 8 209 I INICIAL 2 8 15 209 I INICIAL 2 8 15 25 209 I INICIAL 2 8 15 25 32 209 I INICIAL 2 8 15 25 32 42 209 I INICIAL 2 8 15 25 32 42 55 209 I INICIAL 2 8 15 25 32 42 55 145 209 I 330 I INICIAL 2 8 15 25 32 42 55 145 209 330 I INICIAL 2 8 15 25 32 42 55 145 209 330 330 145 I 330 55 145 I 330 42 55 145 I 330 32 42 55 145 I 330 25 32 42 55 145 I 330 25 15 42 32 55 145 I 330 25 15 42 8 32 55 145 I

Figura 6.17 - Execuo do algoritmo de ordenao Bolha.

17

CAPTULO 6 : PESQUISA E ORDENAO

Para um agregado com N elementos, este algoritmo faz no melhor caso apenas uma passagem, fazendo N1 comparaes e faz no pior caso N1 passagens, fazendo (N2N)/2 comparaes. Em mdia faz aproximadamente N2/3 comparaes. O nmero de trocas se bem que dependente do grau de desordenao dos elementos, no pior caso pode atingir as (N2N)/2 trocas. Portanto, este algoritmo tem uma eficincia de comparao de ordem O(N2), uma eficincia de trocas de ordem O(N2), pelo que, pertence classe O(N2). Como este algoritmo tem a capacidade de aps cada passagem determinar se o agregado est ou no ordenado, ento indicado para ordenar agregados que estejam parcialmente desordenados. No entanto, caso o agregado esteja muito desordenado, ele o pior dos algoritmos de ordenao, uma vez que faz muitas trocas. De maneira a diminuir o nmero de trocas, Donald L. Shell criou uma variante deste algoritmo, que em vez de comparar elementos adjacentes, compara elementos distanciados de um incremento que vai sendo progressivamente diminudo, at que nas ltimas passagens compara elementos adjacentes. A Figura 6.18 apresenta o algoritmo de Ordenao Concha ( Shell Sort ) para a ordenao crescente do agregado. Existe a varivel auxiliar incremento que representa a distncia de comparao e cujo valor inicial metade do comprimento do agregado. Para os ltimos elementos do agregado, mais concretamente para os nelemincremento ltimos elementos, compara-se o elemento de ndice indi, com o elemento distanciado incremento elementos, ou seja com o elemento de ndice indiincremento, e caso o valor seja menor trocam-se os elementos. Deste modo os elementos de menor valor vo sendo deslocados em direco parte inicial do agregado. Em cada passagem, contabiliza-se o nmero de trocas efectuadas, e quando uma passagem no tiver efectuado qualquer troca, isso sinal de que os elementos que esto separados da distncia de comparao, j esto ordenados e a distncia de comparao reduzida, sendo dividida ao meio. O algoritmo repetido at que a ltima distncia de comparao usada seja igual a um. Quando numa passagem com distncia de comparao unitria, no se efectuar qualquer troca de elementos, ento sinal que o agregado est ordenado. Ao contrrio dos outros algoritmos de ordenao, at esta passagem final no h garantia que algum elemento do agregado j esteja ordenado. A srie de incrementos utilizada a que foi proposta pelo Shell.
void Shell_Sort (int seq[], unsigned int nelem) { unsigned int indi, ntrocas, incremento; /* 1 verso */

for (incremento = nelem/2; incremento > 0; incremento /= 2) do { ntrocas = 0; /* inicializar o contador de trocas */ for (indi = incremento; indi < nelem; indi++) if (seq[indi-incremento] > seq[indi]) { /* trocar os elementos */ Swap (&seq[indi], &seq[indi-incremento]); ntrocas++; /* actualizar o nmero de trocas efectuadas */ } } while (ntrocas); }

Figura 6.18 - Algoritmo de ordenao Concha (1 verso).

A Figura 6.19 apresenta a execuo do algoritmo para um agregado com 10 elementos. O algoritmo executa 7 passagens. Nas primeiras duas passagens, o incremento de 5 elementos, nas trs passagens seguintes o incremento de 2 elementos e nas duas

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

passagens finais o incremento de 1 elemento. Faz um total de 52 comparaes e de 11 trocas para ordenar o agregado. Comparando com a ordenao Bolha, temos menos 2 passagens, mais algumas comparaes, mas, menos de metade das trocas.
209 depois da primeira passagem NC=5 NT=3 depois da segunda passagem NC=5 NT=0 depois da terceira passagem NC=8 NT=4 depois da quarta passagem NC=8 NT=1 depois da quinta passagem NC=8 NT=0 depois da sexta passagem NC=9 NT=3 depois da stima passagem NC=9 NT=0 TOTAL NC = 52 NT = 11 INCREMENTO=2 2 15 8 I INCREMENTO=2 2 15 8 I INCREMENTO=2 2 15 8 I INCREMENTO=1 2 8 I INCREMENTO=1 2 8 I 15 25 32 42 55 145 209 330 I 15 25 32 42 55 145 209 330 I 25 42 32 55 145 330 209 I 25 42 32 55 145 330 209 I 32 42 25 55 145 330 209 I INCREMENTO=5 2 32 8 15 42 209 I 330 25 55 145 I 330 25 15 42 2 32 8 55 145

INCREMENTO=5 2 32 8 15 42 209 I 330 25 55 145 I

15

25

32

42

55

145

209

330

Figura 6.19 - Execuo do algoritmo de ordenao Concha.

Normalmente, o algoritmo Concha mais eficiente do que o algoritmo Bolha, uma vez que, as primeiras passagens analisam apenas parte dos elementos do agregado e portanto, fazem poucas trocas, mas trocam elementos que esto muito fora de stio. Quando as ltimas passagens, que analisam os elementos adjacentes, so efectuadas, ento o agregado j se encontra parcialmente ordenado, pelo que, so necessrias poucas passagens e poucas trocas para acabar a ordenao. No entanto, para passar de incremento em incremento esta verso do algoritmo exige uma passagem sem trocas.

19

CAPTULO 6 : PESQUISA E ORDENAO

Existe uma implementao alternativa deste algoritmo, que se apresenta na Figura 6.20, que no necessita de fazer esta passagem extra, para determinar a passagem ao incremento seguinte. Utiliza uma tcnica de insero de elementos em vez de troca de elementos. Pega no agregado constitudo pelos elementos que esto distncia de comparao e para cada um desses elementos faz a sua insero na posio correcta, de maneira que este agregado fique ordenado. Depois passa distncia de comparao seguinte. Quando a distncia de comparao unitria, ento estamos perante o algoritmo de Insero, que se apresenta na Figura 6.23. Para ordenar o mesmo agregado, esta verso faz um total de 29 comparaes e de 55 instrues de atribuio, ou seja, cpias de elementos, que correspondem a aproximadamente 18 trocas. Se por um lado esta verso mais eficiente nas comparaes, menos 23 o que d uma eficincia na ordem dos 50%. Por outro lado, ela menos eficiente nas trocas, com aproximadamente mais 7 trocas para trocar os 11 elementos do agregado que esto fora do stio, ou seja, existe uma ineficincia na ordem dos 70%. Esta ineficincia caracterstica da tcnica de insero.
void Shell_Sort (int seq[], unsigned int nelem) { unsigned int indi, indj, incremento; int temp; /* 2 verso */

for (incremento = nelem/2; incremento > 0; incremento /= 2) for (indi = incremento; indi < nelem; indi++) { temp = seq[indi]; /* copiar o elemento a ordenar */ for (indj = indi; indj >= incremento; indj -= incremento) if (temp < seq[indj-incremento]) seq[indj] = seq[indj-incremento]; /* deslocar elementos */ else break; seq[indj] = temp; /* inserir o elemento a ordenar na posio */ } }

Figura 6.20 - Algoritmo de ordenao Concha (2 verso).

O desempenho deste algoritmo depende da srie de incrementos. Nesta verso e para um agregado com N elementos, em relao ao nmero de comparaes, este algoritmo no pior caso e usando a srie de incrementos propostos por Donald Shell, pertence classe O(N2). Com a srie de incrementos propostos por Hibbard pertence classe O(N3/2) e com a srie de incrementos propostos por Sedgewick pertence classe O(N4/3). Voltando ao algoritmo de ordenao Bolha, foi dito que em cada passagem pelo agregado, pelo menos um novo elemento fica ordenado. De facto todos os elementos atrs do elemento onde foi feita a ltima troca, j esto ordenados, pelo que, podemos colocar a varivel indinicial, que representa o ndice do elemento da parte inicial do agregado onde termina a comparao de elementos em cada passagem ascendente, na posio seguinte posio onde foi feita a ltima troca. Desta forma evitam-se fazer ainda mais comparaes desnecessrias com os elementos que j esto ordenados na parte inicial do agregado. Se aplicarmos esta tcnica fazendo tambm passagens descendentes no agregado, alternadas com as passagens ascendentes, usando uma varivel auxiliar indfinal que representa o ndice do elemento da parte final do agregado onde termina a comparao de elementos em cada passagem descendente, podemos assim ordenar o agregado com menos comparaes de elementos. Estas variveis auxiliares podem ainda ser usadas para determinar quando o agregado est ordenado, pelo que, no necessrio contar o nmero de trocas efectuadas. A Figura 6.21 apresenta o algoritmo de Ordenao Crivo ( Shaker Sort ) para a ordenao crescente do agregado. Existe a varivel auxiliar indinicial que inicializada com o ndice do

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

20

segundo elemento do agregado e que representa o ndice do elemento da parte inicial do agregado onde termina a comparao de elementos em cada passagem ascendente. Existe tambm a varivel auxiliar indfinal que inicializada com o ndice do ltimo elemento do agregado e que representa o ndice do elemento da parte final do agregado onde termina a comparao de elementos em cada passagem descendente. Na passagem ascendente, cada elemento final do agregado, desde o elemento de ndice indfinal at ao elemento de ndice indinicial, comparado com o elemento atrs dele, ou seja, compara-se o elemento de ndice indi com o elemento de ndice indi1, e caso o valor seja menor trocam-se os elementos. O ndice do elemento onde foi feita a ltima troca armazenado e aps ter sido completada a passagem ascendente, o ndice indinicial colocado na posio seguinte posio da ltima troca. Deste modo os elementos de menor valor vo sendo deslocados em direco parte inicial do agregado. Depois na passagem descendente, cada elemento inicial do agregado, desde o elemento de ndice indinicial at ao elemento de ndice indfinal, comparado com o elemento atrs dele, ou seja, compara-se o elemento de ndice indi com o elemento de ndice indi1, e caso o valor seja menor trocam-se os elementos. O ndice do elemento onde foi feita a ltima troca armazenado e aps ter sido completada a passagem descendente, o ndice indfinal colocado na posio anterior posio da ltima troca. Deste modo os elementos de maior valor vo sendo deslocados em direco parte final do agregado. Em cada passagem ascendente e descendente, ordena-se pelo menos dois elementos, um na parte inicial do agregado e outro na parte final do agregado. Quando no feita qualquer troca durante uma passagem ascendente, o ndice indinicial ser colocado depois do ndice indfinal. Por outro lado, quando no feita qualquer troca durante uma passagem descendente, o ndice indfinal ser colocado antes do ndice indinicial. Qualquer destas situaes sinal de que os elementos entre o ndice indinicial e o ndice indfinal tambm esto ordenados e a ordenao termina, porque o agregado est todo ordenado.
void Shaker_Sort (int seq[], unsigned int nelem) { unsigned int indi, indinicial = 1, indfinal = nelem-1, utroca; do { /* passagem ascendente */ for (indi = indfinal; indi >= indinicial; indi--) if (seq[indi-1] > seq[indi]) { Swap (&seq[indi], &seq[indi-1]); /* trocar os elementos */ utroca = indi; /* actualizar a posio da ltima troca */ } indinicial = utroca+1; /* actualizar o limite superior */ /* passagem descendente */ for (indi = indinicial; indi <= indfinal; indi++) if (seq[indi-1] > seq[indi]) { Swap (&seq[indi], &seq[indi-1]); /* trocar os elementos */ utroca = indi; /* actualizar a posio da ltima troca */ } indfinal = utroca-1; /* actualizar o limite inferior */ } while (indinicial < indfinal); }

Figura 6.21 - Algoritmo de ordenao Crivo.

21

CAPTULO 6 : PESQUISA E ORDENAO

A Figura 6.22 apresenta a execuo do algoritmo para um agregado com 10 elementos. O algoritmo executa 6 passagens, 3 em cada sentido. Na terceira passagem descendente no efectuada qualquer troca, pelo que, o ndice de posio final passa para trs do ndice de posio inicial parando o processo de ordenao. O algoritmo faz um total de 39 comparaes e de 25 trocas para ordenar o agregado. Comparando com a ordenao Bolha, temos menos 3 passagens e menos 6 comparaes, e o mesmo nmero de trocas. Os elementos do agregado que vo ficando ordenados, esto a sombreado para melhor se observar as duas extremidades ordenadas do agregado.
209 depois da primeira passagem NC=9 NT=6 depois da segunda passagem NC=8 NT=7 depois da terceira passagem NC=7 NT=4 depois da quarta passagem NC=6 NT=6 depois da quinta passagem NC=5 NT=2 depois da sexta passagem NC=4 NT=0 TOTAL NC = 39 NT = 25 330 25 15 42 2 32 8 55 145 FINAL 25 15 42 8 32 55 145 I INICIAL 2 209 25 I INICIAL 2 8 209
UT

INICIAL 2 209
UT

330

FINAL 15 42 8 32 55 145 330


UT

I FINAL 25 15 42 32 55 145 I INICIAL FINAL 42 32 55 145 209


UT

330

25

15 I INICIAL

330

I FINAL 32 42 55 145 I INICIAL FINAL 42 55 145 I 209 330 209 330

15

25
UT

15

25
UT

32 I

FINAL 2 8 15 25

INICIAL 32 42 55 145 209 330

Figura 6.22 - Execuo do algoritmo de ordenao Crivo.

O desempenho deste algoritmo complexo de analisar. Em comparao com o algoritmo Bolha, tem o mesmo desempenho em relao ao nmero de trocas e mais eficiente em relao ao nmero de comparaes. No entanto, tambm pertence classe O(N2).

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

22

6.2.3 Ordenao por insero


A ordenao por insero a tcnica de ordenao que normalmente usamos, nomeadamente, para ordenar uma mo de cartas. Consiste em determinar para cada elemento do agregado, a posio de insero para que ele fique ordenado e inseri-lo nessa posio, deslocando para o efeito os restantes elementos. A Figura 6.23 apresenta o algoritmo de Ordenao Insero ( Insertion Sort ) para a ordenao crescente do agregado. Durante o processo de ordenao, o agregado est dividido em duas partes. A parte inicial do agregado est ordenada e a parte final est desordenada. O algoritmo comea por considerar que o primeiro elemento do agregado j est ordenado e depois ordena os restantes elementos do agregado, ou seja, desde o segundo ao ltimo elemento. Cada elemento a ordenar previamente copiado para uma varivel temporria, de forma a abrir uma posio livre que permita o deslocamento dos elementos que esto atrs dele e que so maiores do que ele. Enquanto houver elementos na parte ordenada do agregado, ou seja, elementos que esto atrs do elemento a ordenar que sejam maiores do que ele, estes elementos so deslocados uma posio para a frente. Quando o deslocamento dos elementos terminar, o elemento a ordenar colocado na posio que foi aberta pelo deslocamento dos elementos. Se o elemento j estiver na posio correcta, porque maior do que todos os que esto atrs de si, ento no h deslocamento de elementos e o elemento colocado sobre ele prprio. Nesta situao, so feitas duas instrues de cpia inutilmente. Esta ineficincia caracterstica desta implementao do algoritmo de insero. O algoritmo faz praticamente tantas comparaes como deslocamentos. Mais concretamente, para ordenar cada elemento, faz mais uma comparao, para poder detectar o fim dos deslocamentos.
void Insertion_Sort (int seq[], unsigned int nelem) { unsigned int indi, indd; int temp; for (indi = 1; indi < nelem; indi++) { temp = seq[indi]; /* copiar o elemento a ordenar */ /* deslocar os elementos atrs dele que lhe so maiores */ for (indd = indi; indd > 0 && seq[indd-1] > temp; indd--) seq[indd] = seq[indd-1]; seq[indd] = temp; } } /* inserir o elemento a ordenar na posio */

Figura 6.23 - Algoritmo de ordenao Insero.

Para comparar este algoritmo com os anteriores, temos que ter em considerao, que para ordenar cada elemento, so precisas duas instrues de atribuio. Uma para copiar o elemento para uma varivel temporria e outra para o inserir na posio definitiva. E o deslocamento de cada elemento custa ainda uma instruo de atribuio. Pelo que, o nmero de instrues de atribuio NA = ND + 2 * (N1). Como cada instruo de atribuio equivalente, a um tero das instrues de atribuio que so necessrias para efectuar a troca de dois elementos do agregado, nos algoritmos anteriores baseados em trocas, ento temos que NT = ( ND + 2 * (N1) )/3. A Figura 6.24 apresenta a execuo do algoritmo para um agregado com 10 elementos. O algoritmo faz um total de 34 comparaes e de 43 instrues de atribuio para efectuar os

23

CAPTULO 6 : PESQUISA E ORDENAO

25 deslocamentos de elementos que so necessrios para ordenar o agregado, o que equivalente a aproximadamente 14 trocas.
209 ordenar o segundo elemento NC=1 ND=0 ordenar o terceiro elemento NC=3 ND=2 ordenar o quarto elemento NC=4 ND=3 ordenar o quinto elemento NC=3 ND=2 ordenar o sexto elemento NC=6 ND=5 ordenar o stimo elemento NC=4 ND=3 ordenar o oitavo elemento NC=7 ND=6 ordenar o nono elemento NC=3 ND=2 ordenar o dcimo elemento NC=3 ND=2 TOTAL NC = 34 NA = 43 PI 209 25 PI 25 15 209 25 330 330 209 330 I 209 330 PI 25 15 42 2 32 8 55 145 25 15 42 2 32 8 55 145

o segundo elemento j est no stio I 25 330 15 15 I 15 42 42 I 42 330 2 2 I 32 32 8 8 55 55 145 145 2 2 32 32 8 8 55 55 145 145 42 42 2 2 32 32 8 8 55 55 145 145

209 330 PI

15 15 PI 15 2

25 25

209 330 42 209

25 15

42 25

209 42 PI

330 209

2 330

32 32 I

8 8

55 55

145 145

2 2

15 15 PI

25 25

42 32

209 42

330 209

32 330

8 8 I

55 55

145 145

2 2

15 8

25 15

32 25

42 32

209 42

330 209 PI

8 330

55 55 I

145 145

2 2

8 8

15 15

25 25

32 32

42 42

209 55

330 209 PI

55 330

145 145 I

2 2

8 8

15 15

25 25

32 32

42 42

55 55

209 145

330 209

145 330

15

25

32

42

55

145

209

330

Figura 6.24 - Execuo do algoritmo de ordenao Insero.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

24

Para um agregado com N elementos, este algoritmo faz no pior caso (N2N)/2 comparaes e deslocamentos, mas, em termos mdios faz aproximadamente N 2/4 comparaes e deslocamentos, pelo que, pertence classe O(N 2). Se o agregado estiver parcialmente desordenado este o algoritmo de ordenao mais indicado.

6.2.4 Comparao dos algoritmos de ordenao


A Tabela 6.1 apresenta os resultados dos algoritmos de ordenao apresentados. O pior algoritmo o Sequencial, j que faz o nmero mximo de comparaes necessrias para ordenar um agregado, e faz trocas s cegas, fazendo por vezes trocas que so feitas e refeitas vezes sem conta. O algoritmo Seleco permite minimizar o nmero de trocas e neste aspecto o melhor de todos, compensando desta forma o excesso de comparaes. O algoritmo Bolha tem um desempenho mau, a no ser quando o agregado est parcialmente desordenado, o que no o caso do exemplo apresentado. A primeira verso do algoritmo Concha tem, para o exemplo utilizado, um nmero de comparaes maior do que os outros algoritmos, se bem que com um nmero de trocas baixo. Mas, no caso da segunda verso, ele o melhor algoritmo em nmero de comparaes. Como o resultado deste algoritmo depende muito da srie de incrementos utilizado, estes valores podem eventualmente ser melhorados com outra srie de incrementos. O nmero de comparaes s desce abaixo de N2/2, para os algoritmos Concha (2 verso), Crivo e Insero. O primeiro tem um nmero de trocas aceitvel, o segundo tem um nmero de trocas elevado, enquanto que o terceiro tem um nmero de trocas pior do que o Concha (1 verso), mas que aceitvel se tivermos em conta a reduo do nmero de comparaes. Portanto, os algoritmos Insero e Concha (2 verso) so os melhores algoritmos analisados. Algoritmo N de Comparaes N de Trocas Sequencial 45 25 Seleco 45 7 Bolha 45 25 Concha (1 verso) 52 11 Concha (2 verso) 29 18 Crivo 39 25 Insero 34 14
Tabela 6.1 - Comparao dos algoritmos de ordenao.

Para a escolha da verso do algoritmo Concha, devemos optar pela primeira verso se a funo de comparao dos elementos do agregado for simples, por exemplo uma comparao numrica e se os elementos do agregado forem estruturas pesadas, ou seja, com muitos bytes, com vista a minorar o nmero de trocas. Se pelo contrrio, a funo de comparao dos elementos do agregado for pesada, por exemplo uma comparao alfanumrica com muitos caracteres e se os elementos do agregado forem tipos simples, estruturas pequenas, ou ponteiros, ento devemos optar pela segunda verso com vista a minorar o nmero de comparaes. Estas consideraes aplicam-se igualmente escolha de qualquer algoritmo de ordenao. Para a anlise dos algoritmos devemos ter em conta que o pior caso na ordenao de um agregado acontece quando o agregado est invertido em relao ordenao pretendida, enquanto que o melhor caso acontece quando o agregado est ordenado de acordo com a ordenao pretendida. Para avaliar o caso mdio deve-se utilizar agregados gerados aleatoriamente e fazer uma estimativa mdia.

25

CAPTULO 6 : PESQUISA E ORDENAO

6.3 Ordenao por fuso


Uma forma de optimizar a ordenao de um agregado com muitos elementos, consiste em partir o agregado em vrios agregados mais pequenos, orden-los separadamente e depois fundi-los num agregado nico. Para fundir dois ou mais agregados j ordenados, usa-se um algoritmo de ordenao por fuso. A Figura 6.25 apresenta o algoritmo de Ordenao Fuso de Listas ( Merge List Sort ) para a fuso de dois agregados ordenados por ordem crescente, considerando que eles no tm elementos repetidos. O agregado de sada tem que ter capacidade suficiente para armazenar os elementos dos dois agregados de entrada. Comea-se por apontar para o primeiro elemento dos agregados a fundir, ou seja, para o ndice 0. O ndice do agregado de sada indk aponta sempre para a primeira posio livre do agregado, que inicialmente a posio zero. Enquanto existirem elementos nos dois agregados, compara-se o elemento do agregado seqa, de ndice indi, com o elemento do agregado seqb, de ndice indj. O menor dos valores copiado para o agregado de sada seqc, cujo ndice indk previamente incrementado para contabilizar mais um elemento. O ndice do agregado, cujo elemento copiado para o agregado de sada tambm incrementado de uma posio, para sinalizar que o elemento j foi ordenado. Quando um dos agregados de entrada estiver esgotado, copiam-se os restantes elementos do outro agregado para o agregado de sada. No final da fuso, o indicador de elementos do agregado de sada nelemc, que passado por referncia, deve conter o nmero de elementos armazenados no agregado, que igual a nelema mais nelemb, uma vez que estamos a considerar que no existem elementos repetidos nos agregados de entrada.
void Merge_List_Sort (int seqa[], unsigned int nelema,\ int seqb[], unsigned int nelemb,\ int seqc[], unsigned int *nelemc) { unsigned int indi = 0, indj = 0, indk = 0, indc; /* copiar o elemento do agregado A ou o elemento do agregado B */ while (indi < nelema && indj < nelemb) if (seqa[indi] < seqb[indj]) seqc[indk++] = seqa[indi++]; else seqc[indk++] = seqb[indj++]; if (indi < nelema) for (indc = indi; indc < nelema; indc++) seqc[indk++] = seqa[indc]; /* copiar os restantes elementos do agregado A */ else for (indc = indj; indc < nelemb; indc++) seqc[indk++] = seqb[indc]; /* copiar os restantes elementos do agregado B */ *nelemc = indk; /* armazenar o nmero de elementos do agregado C */ }

Figura 6.25 - Algoritmo de ordenao Fuso de Listas.

Este algoritmo muito verstil e eficiente, uma vez que, pode ser generalizado para fazer a fuso de mais do que dois agregados. No caso da existncia de elementos repetidos nos agregados de entrada, podemos considerar solues alternativas, em que a ocorrncia de elementos repetidos eliminada ou mantida. Tambm pode ser utilizado como algoritmo de ordenao externa para fundir dois ou mais ficheiros. Para executar a fuso de dois agregados j ordenados, com N/2 elementos cada, este algoritmo faz no melhor caso N/2 comparaes e no pior caso N1 comparaes.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

26

A Figura 6.26 apresenta a execuo do algoritmo na fuso de dois agregados previamente ordenados, com 5 elementos cada. O algoritmo faz 8 comparaes, at que o agregado B fica esgotado, aps o qual, os restantes elementos do agregado A so copiados para o agregado C.
I antes da ordenao comear A C K depois de copiar o primeiro elemento do agregado B I A C 8 2 K I A C 8 2 32 8 K I A 8 2 32 8 42 15 209 25 K I A C 8 2 32 8 42 15 209 25 330 32 B 42 K I A C 8 2 32 8 42 15 209 25 330 32 B 42 2 55 15 145 K I A C 8 2 32 8 42 15 209 25 330 32 B 42 2 55 15 145 25 209 55 330 K 145 J 25 55 145 J 2 15 25 J 55 145 330 B 2 15 25 J 55 145 42 209 330 B 2 J 15 25 55 145 32 42 209 330 B 2 J 15 25 55 145 8 32 42 209 330 B J 2 15 25 55 145

depois de copiar o primeiro elemento do agregado A

depois de copiar mais dois elementos do agregado B

depois de copiar mais dois elementos do agregado A

depois de esgotar os elementos do agregado B depois de copiar os restantes elementos do agregado A

Figura 6.26 - Execuo do algoritmo de ordenao Fuso de Listas.

27

CAPTULO 6 : PESQUISA E ORDENAO

6.4 Algoritmos de ordenao recursivos


Os algoritmos de ordenao recursivos so algoritmos de ordenao que aplicam o princpio do dividir para conquistar. So algoritmos complexos, mas muito eficientes e pertencem classe O(N log2 N). Vamos apresentar dois algoritmos.

6.4.1 Algoritmo de ordenao Fuso


O algoritmo de Ordenao Fuso ( Merge Sort ) pode ser utilizado de forma recursiva para ordenar um agregado, partindo-o sucessivamente ao meio e fazendo a fuso dos agregados parcelares j ordenados. Este princpio quando aplicado recursivamente, at que os agregados obtidos so apenas compostos por um nico elemento, acaba por ordenar o agregado. Esta verso recursiva, para a ordenao crescente do agregado, apresentada na Figura 6.27.
void Merge_Sort (int seq[], unsigned int inicio, unsigned int fim) { unsigned int medio; if (inicio < fim) { medio = (inicio + fim) / 2; /* condio de paragem */ /* partio do agregado */

/* invocao recursiva para ordenar a primeira metade do agregado */ Merge_Sort (seq, inicio, medio); /* invocao recursiva para ordenar a segunda metade do agregado */ Merge_Sort (seq, medio+1, fim); /* fuso das duas metades ordenadas do agregado */ Merge_List_Sort (seq, inicio, medio, fim); } } void Merge_List_Sort (int seq[], unsigned int inicio,\ unsigned int medio, unsigned int fim) { unsigned int inica = inicio, inicb = medio+1, indi = 0, indc; /* atribuio de memria para o agregado local */ int *seqtemp = (int *) calloc (fim-inicio+1, sizeof (int)); while (inica <= medio && inicb <= fim) if (seq[inica] < seq[inicb]) seqtemp[indi++] = seq[inica++]; else seqtemp[indi++] = seq[inicb++];

/* elemento da 1 parte */ /* elemento da 2 parte */

/* copiar os restantes elementos da primeira parte do agregado */ while (inica <= medio) seqtemp[indi++] = seq[inica++]; /* copiar os restantes elementos da segunda parte do agregado */ while (inicb <= fim) seqtemp[indi++] = seq[inicb++]; /* copiar o resultado para o agregado a ordenar */ for (indc = 0, inica = inicio; indc < indi; indc++, inica++) seq[inica] = seqtemp[indc]; free (seqtemp); } /* libertao da memria do agregado local */

Figura 6.27 - Algoritmo de ordenao Fuso.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

28

A invocao inicial da funo de ordenao Merge_Sort (seq, 0, nelem-1);. Cada invocao da funo Merge_Sort precisa de saber o primeiro e o ltimo elementos do agregado parcelar que est a ser ordenado, pelo que, o procedimento tem como parmetros de entrada o incio e o fim do agregado. A funo Merge_List_Sort que constitui a parte no recursiva do algoritmo faz a fuso das duas partes j ordenadas para um agregado local e no fim copia-o de volta para o agregado a ordenar. uma verso ligeiramente diferente do algoritmo apresentado na Figura 6.25, uma vez que faz a fuso de duas partes do mesmo agregado em vez de dois agregados distintos. Da que tem como parmetros de entrada os limites das duas partes do agregado, definidos pelos elementos inicial, mdio e final do agregado. O agregado local atribudo dinamicamente com o tamanho necessrio para fazer a fuso das duas metades do agregado e libertado quando no mais necessrio. Para simplificar no feita a validao da atribuio de memria. Para comparar este algoritmo com os algoritmos que ordenam fazendo trocas dos elementos do agregado a ordenar, temos que ter em considerao, que para ordenar cada elemento preciso copi-lo de e para o agregado. Cada cpia custa uma instruo de atribuio, o que equivalente, a um tero das instrues de atribuio que so necessrias para efectuar a troca de dois elementos do agregado. Ou seja, NT = NA/3. A Figura 6.28 apresenta a execuo do algoritmo para um agregado com 10 elementos. O algoritmo faz um total de 21 comparaes para fundir os agregados parcelares e executa 68 instrues de atribuio, ou seja, cpias de valores entre o agregado a ordenar e o agregado local da funo de fuso de listas, metade das quais se devem ao facto da necessidade de usar um agregado extra para fazer a fuso. Este nmero de cpias equivalente a aproximadamente 23 trocas. Quando comparado com os algoritmos, cujos resultados esto coligidos na Tabela 6.1, ele o melhor algoritmo de ordenao no que diz respeito ao nmero de comparaes. Tem, no entanto, um nmero elevado de trocas quando comparado com os algoritmos Insero e Concha (2 verso), que so os algoritmos com o melhor desempenho global de todos os algoritmos apresentados anteriormente. Para executar a ordenao de um agregado com N elementos, sendo N mltiplo de 2, este algoritmo faz no melhor caso (N log2 N)/2 comparaes e no pior caso N log2 N N + 1 comparaes, pelo que, pertence classe O(N log2 N).

6.4.2 Algoritmo de ordenao Rpido


O algoritmo de Ordenao Rpido ( Quick Sort ) foi criado em 1960 por C. A. R. Hoare e utiliza um principio muito simples que quando aplicado recursivamente, acaba por ordenar o agregado. O princpio o seguinte. Vamos escolher um elemento do agregado, que vamos designar por pivot e dividir o agregado em duas partes. Na parte da esquerda colocam-se os valores menores do que o pivot e na parte direita colocam-se os valores maiores do que o pivot. Se cada parte do agregado for sucessivamente dividida ao meio e for aplicado este princpio de separao dos elementos, ento quando o processo recursivo terminar ao se atingir agregados com trs ou menos elementos, o agregado est ordenado por ordem crescente. A Figura 6.29 apresenta o algoritmo para a ordenao crescente do agregado.

29

CAPTULO 6 : PESQUISA E ORDENAO

209

330

25

15

42

32

55

145 i r e c u r s i v a s

209

330

25

15

42

32

55

145

n v o

209

330

25

15

42

32

55

145

c a

209

330

25

15

42

32

55

145

e s

209

330

32

209

330

32 f u a g r e g a d o s d o s

25

209

330

15

42

32

55

145

s o

15

25

42

209

330

32

55

145

TOTAL NC = 21 NA = 68

15

25

32

42

55

145

209 330

Figura 6.28 - Execuo do algoritmo de ordenao Fuso.

Se o nmero de elementos do agregado for menor do que dois, ento o agregado est automaticamente ordenado. Se existirem apenas dois elementos, eles so ordenados atravs de uma simples comparao e eventual troca dos dois elementos. Quando o agregado tem mais do que dois elementos, escolhido o elemento de ndice mdio para servir de pivot. Os elementos das extremidades e o pivot so trocados para ficarem por ordem crescente. Se s existirem trs elementos, ento esta operao constituda por trs comparaes e eventuais trs trocas, ordena o agregado. Seno, h que assegurar que todos os elementos esquerda do pivot so menores do que ele e que todos os elementos sua direita so maiores do que ele. Os elementos que se encontrem fora do stio so trocados da parte direita para a parte esquerda e vice-versa, ou ento so trocados com o pivot provocando que ele se desloque mais para a esquerda ou mais para a direita do que a posio mdia inicialmente calculada. Pelo que, quando o processamento se separao terminar no h garantia que o agregado est partido em duas metades com um nmero semelhante de elementos. Aps este processamento, o algoritmo invocado recursivamente para o agregado constitudo pelos elementos esquerda do pivot e para o agregado constitudo

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

30

pelos elementos direita do pivot. A invocao inicial da funo de ordenao Quick_Sort (seq, 0, nelem-1);.
void Quick_Sort (int seq[], unsigned int inicio, unsigned int fim) { unsigned int medio, nelem = fim-inicio+1, indi, indj; if (nelem <= 1) return; /* o agregado tem no mximo 1 elemento */ if (nelem == 2) /* o agregado s tem 2 elementos */ { if (seq[inicio] > seq[fim]) Swap (&seq[inicio], &seq[fim]); return; } medio = (inicio + fim) / 2; /* clculo do ndice do pivot */

/* colocar os extremos e o pivot por ordem crescente */ if (seq[inicio] > seq[medio]) Swap (&seq[inicio], &seq[medio]); if (seq[inicio] > seq[fim]) Swap (&seq[inicio], &seq[fim]); if (seq[medio] > seq[fim]) Swap (&seq[medio], &seq[fim]); /* se o agregado s tem 3 elementos, ento j est ordenado */ if (nelem == 3) return; indi = inicio+1; indj = fim-1; while (indi < indj) { /* procurar elementos na parte esquerda maiores do que pivot */ while (indi < medio) if (seq[indi] > seq[medio]) break; else indi++; /* procurar elementos na parte direita menores do que pivot */ while (indj > medio) if (seq[indj] < seq[medio]) break; else indj--; if (indi != medio && indj != medio) Swap (&seq[indi], &seq[indj]); /* trocar os elementos */

else if (indi == medio && indj != medio) { /* trocar o elemento de ndice indj com o pivot */ /* e deslocar o pivot uma posio para a direita */ Swap (&seq[medio++], &seq[indj]); Swap (&seq[medio], &seq[indj]); indi = medio; } else if (indi != medio && indj == medio) { /* trocar o elemento de ndice indi com o pivot */ /* e deslocar o pivot uma posio para a esquerda */ Swap (&seq[medio--], &seq[indi]); Swap (&seq[medio], &seq[indi]); indj = medio; } } /* invocao recursiva para a parte esquerda do agregado */ Quick_Sort (seq, inicio, medio-1); /* invocao recursiva para a parte direita do agregado */ Quick_Sort (seq, medio+1, fim); }

Figura 6.29 - Algoritmo de ordenao Rpido (1 verso).

31

CAPTULO 6 : PESQUISA E ORDENAO

A Figura 6.30 apresenta a execuo do algoritmo para um agregado com 10 elementos. Os elementos que servem de pivot, aparecem a cheio para se distinguirem dos restantes elementos e as invocaes recursivas so identificadas pela sigla IR. O algoritmo faz um total de 28 comparaes e de 19 trocas de elementos. Como exemplo, vamos ver como se comporta o algoritmo quando a funo invocada. Uma vez que o agregado inicial tem dez elementos, de ndices 0 a 9, o elemento de ndice 4, cujo valor 42, escolhido para pivot. Ao ordenar os valores 209, 42 e 145, o valor 145 passa a ser o novo pivot, porque o valor mediano. Todos os valores menores do que 145 so colocados sua esquerda, enquanto que, todos os valores maiores do que 145 so colocados sua direita. Pelo que, o pivot vai ser deslocado para a posio de ndice 7. Depois so feitas as invocaes recursivas dos agregados formados pelos elementos 0 a 6 e pelos elementos 8 a 9. Quando comparado com os algoritmos anteriores, constatamos que ele tem um desempenho praticamente semelhante ao Concha (2 verso) e globalmente equivalente ao Fuso, com a vantagem de no necessitar de um agregado auxiliar. Quando comparado com o algoritmo de Insero, ele tem um maior nmero de trocas compensado com um menor nmero de comparaes, tendo no entanto uma implementao mais complexa.
209 209 330 330 25 25 15 15 42 42 2 2 32 32 8 8 55 55 145 145

aps processamento 42 55 25 I 42 55 25 15 R 8 2 32 8 2 32 145 330 I 209 209 R 330

15

aps processamento 15 2 I 15 2 25 R 25 8 8 32 55 I 42 42 R 55

aps processamento 2 I 2 R 8 25 I 15 15 R 25

15

25

2 TOTAL NC = 28 NT = 19

15

25

32

42

55

15

25

32

42

55

145

209

330

Figura 6.30 - Execuo do algoritmo de ordenao Rpido.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

32

Com o objectivo de diminuir o nmero de trocas, existe uma verso optimizada, que se apresenta na Figura 6.31 e que frequentemente apresentada na literatura. A optimizao consiste em evitar as trocas que envolvem o pivot e o deslocam da posio inicial. Aps a escolha do pivot ele escondido na penltima posio do agregado. Os elementos so analisados e trocados at serem todos comparados com o pivot. Quando a anlise do agregado terminar, ou seja, quando indi for maior ou igual do que indj, o pivot colocado no stio, por troca com o elemento que est na posio mais esquerda do agregado que maior do que ele, ou seja, com o elemento que est na posio indi. Esta verso do algoritmo faz um total de 30 comparaes e de 17 trocas de elementos. Quando comparado com a primeira verso temos mais 2 comparaes, mas menos 2 trocas.
void Quick_Sort (int seq[], unsigned int inicio, unsigned int fim) { unsigned int medio, nelem = fim-inicio+1, indi, indj; if (nelem <= 1) return; /* o agregado tem no mximo 1 elemento */ if (nelem == 2) /* o agregado s tem 2 elementos */ { if (seq[inicio] > seq[fim]) Swap (&seq[inicio], &seq[fim]); return; } medio = (inicio + fim) / 2; /* clculo do ndice do pivot */

/* colocar os extremos e o pivot por ordem crescente */ if (seq[inicio] > seq[medio]) Swap (&seq[inicio], &seq[medio]); if (seq[inicio] > seq[fim]) Swap (&seq[inicio], &seq[fim]); if (seq[medio] > seq[fim]) Swap (&seq[medio], &seq[fim]); /* se o agregado s tem 3 elementos, ento j est ordenado */ if (nelem == 3) return; /* esconder o pivot na penltima posio do agregado */ Swap (&seq[medio], &seq[fim-1]); indi = inicio; medio = indj = fim-1; for ( ; ; ) { /* procurar elementos na parte esquerda maiores do que pivot */ while (seq[++indi] < seq[medio]) ; /* procurar elementos na parte direita menores do que pivot */ while (seq[--indj] > seq[medio]) ; if (indi < indj) Swap (&seq[indi], &seq[indj]); } /* recuperar o pivot para a posio mdia do agregado */ medio = indi; Swap (&seq[medio], &seq[fim-1]); /* invocao recursiva para a parte esquerda do agregado */ Quick_Sort (seq, inicio, medio-1); /* invocao recursiva para a parte direita do agregado */ Quick_Sort (seq, medio+1, fim); } else break;

Figura 6.31 - Algoritmo de ordenao Rpido (2 verso).

Para executar a ordenao de um agregado com N elementos, este algoritmo faz no pior caso (N2N)/2 comparaes. No entanto, no caso mdio faz aproximadamente 1.4(N+1) log2 N comparaes, pelo que, considera-se que pertence classe O(N log2 N).

33

CAPTULO 6 : PESQUISA E ORDENAO

6.5 Generalizao dos algoritmos de ordenao


Os algoritmos de ordenao foram apresentados e aplicados sobre agregados de nmeros inteiros, com vista a melhor visualizar o seu funcionamento. No entanto, em muitas aplicaes h a necessidade de armazenar bases de dados em agregados e de as ordenar de acordo com as diferentes caractersticas dos seus elementos. Por vezes existe mesmo a necessidade de ordenar sucessivamente um agregado de registos por diversas chaves de ordenao diferente, como por exemplo, fazer ordenaes alfabticas, cronolgicas e numricas e fazer ordenaes crescentes ou ascendentes e decrescentes ou descendentes. Como podemos ento implementar estas ordenaes recorrendo ao mesmo algoritmo de ordenao? E de preferncia com o mnimo de esforo. Ou seja, como podemos generalizar um algoritmo de ordenao para que ele possa ordenar um agregado com elementos de um qualquer tipo de dados, ordenar por diferentes critrios e ordenar por ordem crescente ou decrescente. Para poder generalizar um algoritmo de ordenao e de modo a estruturar melhor a soluo, precisamos de um ficheiro de interface que defina o tipo de elementos constituintes do agregado e que providencie as funes de comparao de acordo com os diferentes critrios de ordenao que se pretendem efectuar. Vamos utilizar o exemplo concreto que se apresenta na Figura 6.32. Vamos considerar que se pretende gerir uma base de dados constituda por elementos com a seguinte informao. O nmero de registo de uma pessoa na base de dados que do tipo inteiro, um nome que uma cadeia de caracteres e uma data constituda por dia, ms e ano. Vamos considerar ainda que pretendemos ordenar a base de dados por ordem numrica do nmero de registo, por ordem alfabtica do nome e por ordem cronolgica da data. Assim sendo, precisamos de implementar as trs funes de comparao CompNRegisto, CompNome e CompData. Para implementar no mesmo algoritmo de ordenao, a ordenao crescente ou ascendente e decrescente ou descendente, vamos passar funo de ordenao o parmetro de entrada adicional inteiro tord que representa o tipo de ordenao a efectuar. Vamos definir que este parmetro indica que se pretende a ordenao crescente com o valor 1 e indica que se pretende a ordenao decrescente com o valor 1. Pelo que, as funes de comparao tm a particularidade de devolver um valor inteiro que um dos seguintes trs valores: o valor 0 no caso dos elementos serem iguais conforme o item comparado; o valor 1 no caso do primeiro elemento ser maior do que segundo elemento conforme o item comparado, ou seja, um maior nmero de registo ou um nome alfabeticamente posterior ou uma data maior, o que significa uma data mais recente; e o valor 1 no caso do primeiro elemento ser menor do que segundo elemento conforme o item comparado, ou seja, um menor nmero de registo ou um nome alfabeticamente anterior ou uma data menor o que significa uma data mais antiga. Finalmente, para poder generalizar os algoritmos de ordenao precisamos de um mecanismo que permita utilizar uma qualquer funo de comparao dentro da funo de ordenao, de acordo com a ordenao pretendida num dado momento do programa. Para tal, a linguagem C providencia os ponteiros para funes. Um ponteiro para uma funo permite invocar de uma forma simples e elegante diferentes funes. A seguinte instruo define um ponteiro para uma funo inteira com dois parmetros de entrada do tipo TElem, que reconhecido pelo identificador PtFComp.
typedef int (*PtFComp) (TElem, TElem);

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

34

/********** Interface da Estrutura de Dados do Agregado **********/ /* Nome: elemento.h */ /* Definio do tipo dos elementos do agregado e das funes de comparao necessrias. Este ficheiro deve ser modificado para adequar a definio e as funes a cada implementao especfica. */ #include <string.h> #ifndef _ELEMENTO #define _ELEMENTO /************* Definio do Tipo de Dados do Elemento *************/ typedef struct { unsigned int dia; unsigned int mes; unsigned int ano; } TData; typedef struct { unsigned int nreg; char nome[60]; TData data; } TElem; /***** Definio do Tipo Ponteiro para a Funo de Comparao *****/ typedef int (*PtFComp) (TElem, TElem); /******* Definio das Funes de Comparao dos Elementos *******/ int CompNRegisto (TElem a, TElem b) { if ( a.nreg > b.nreg ) return 1; else if ( a.nreg < b.nreg ) return -1; else return 0; } int CompNome (TElem a, TElem b) { int comp = strcmp (a.nome, b.nome); if ( comp > 0 ) return 1; else if ( comp < 0 ) return -1; else return 0; } int CompData (TElem a, TElem b) { if ( a.data.ano > b.data.ano ) return 1; else if ( a.data.ano < b.data.ano) return -1; else if ( a.data.mes > b.data.mes ) return 1; else if (a.data.mes < b.data.mes ) return -1; else if ( a.data.dia > b.data.dia ) return 1; else if ( a.data.dia < b.data.dia ) return -1; else return 0; } #endif

Figura 6.32 - Ficheiro de interface do elemento constituinte do agregado a ordenar.

35

CAPTULO 6 : PESQUISA E ORDENAO

A partir desta definio podem-se declarar variveis do tipo PtFComp e atribuir-lhe uma qualquer funo, desde que seja uma funo inteira com dois parmetros de entrada do tipo TElem. A Figura 6.34 apresenta um exemplo da passagem de um parmetro de entrada deste tipo para uma funo de ordenao. A Figura 6.35 apresenta a utilizao de uma varivel deste tipo para ser colocada a apontar para diferentes funes ao longo da execuo do programa, de forma a parametrizar a ordenao de um agregado. Tal como nos agregados, o nome de uma funo tambm um ponteiro para a funo, pelo que, a instruo de atribuio fcomp = CompNome; coloca o ponteiro para funo fcomp a apontar para a funo CompNome. A funo de troca de elementos do agregado que foi apresentada na Figura 6.11, tem de ser alterada de maneira a poder trocar dois elementos do tipo TElem. A Figura 6.33 apresenta a nova verso que vamos designar por SwapElementos.
void SwapElementos (TElem *x, TElem *y) { TElem temp; temp = *x; *x = *y; *y = temp; }

Figura 6.33 - Funo para trocar dois elementos de um agregado de elementos do tipo TElem.

A Figura 6.34 apresenta a verso generalizada do algoritmo de ordenao Sequencial. Escolhemos este algoritmo, apenas porque o mais simples em termos de cdigo. O primeiro parmetro da funo o parmetro de entrada-sada que representa o agregado a ordenar, o segundo parmetro o parmetro de entrada que representa o nmero de elementos do agregado, o terceiro parmetro o parmetro de entrada que representa a funo de comparao e o quarto parmetro o parmetro de entrada que representa o tipo de ordenao a efectuar. Estes dois ltimos parmetros de entrada configuram o algoritmo de ordenao, tornando-o assim genrico e reutilizvel.
void Sequential_Sort (TElem seq[], unsigned int nelem,\ PtFComp fcomp, int tord) { unsigned int indi, indj; for (indi = 0; indi < nelem-1; indi++) for (indj = indi+1; indj < nelem; indj++) if ( fcomp (seq[indi], seq[indj]) == tord ) SwapElementos (&seq[indi], &seq[indj]); }

Figura 6.34 - Algoritmo de ordenao Sequencial generalizado.

A Figura 6.35 apresenta a utilizao sucessiva deste algoritmo de ordenao, para ordenar um agregado de elementos do tipo TElem. Antes de cada invocao, atribudo ao ponteiro fcomp a funo de comparao necessria para obter a ordenao desejada e depois a funo de ordenao invocada indicando tambm o tipo de ordenao pretendido. Para aumentar a legibilidade do programa, definimos as constantes simblicas CRESCENTE e DECRESCENTE. Como exerccio de treino escreva a funo Display, cuja funcionalidade imprimir no monitor a informao relativa a um agregado de estruturas do tipo TElem, com o prottipo Display (TElem seq[], unsigned int nelem); e teste o programa.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

36

#include <stdio.h> #include <stdlib.h> #include "elemento.h" #define CRESCENTE 1 #define DECRESCENTE -1 void SwapElementos (TElem *, TElem *); void Sequential_Sort (TElem [], unsigned int, PtFComp, int); void Display (TElem [], unsigned int); int main (void) { TElem pintores[] = { { { { { { }; int nelem = sizeof (pintores) / sizeof (pintores[0]); /* ponteiro para a funo de comparao inicializado a NULL */ PtFComp fcomp = NULL; fcomp = CompNome; /* ordenao alfabtica ascendente */ Sequential_Sort (pintores, nelem, fcomp, CRESCENTE); printf ("Ordenao Alfabtica Ascendente\n"); Display (pintores, nelem); fcomp = CompData; /* ordenao cronolgica decrescente */ Sequential_Sort (pintores, nelem, fcomp, DECRESCENTE); printf ("Ordenao Cronolgica Decrescente\n"); Display (pintores, nelem); fcomp = CompNRegisto; /* ordenao crescente por registo */ Sequential_Sort (pintores, nelem, fcomp, CRESCENTE); printf ("Ordenao Numrica Crescente\n"); Display (pintores, nelem); return EXIT_SUCCESS; } ... 1, 2, 3, 4, 5, "Vincent Van Gogh", {30, 3, 1853} }, "Vieira da Silva", {13, 6, 1908} }, "Amedeo Modigliani", {12, 7, 1884} }, "Claude Monet", {14, 11, 1840} }, "Georgia O'Keeffe", {15, 11, 1887} } /* caracterizao do tipo elemento */

/* Definio das funes */

Figura 6.35 - Exemplo da utilizao sucessiva do algoritmo de ordenao Sequencial.

6.6 Avaliao do desempenho dos algoritmos


Para podermos avaliar o desempenho dos algoritmos de ordenao necessrio contabilizar o nmero de operaes de comparao e o nmero de operaes de trocas de elementos. Ou, uma vez que existem algoritmos de ordenao que em vez de trocas fazem cpias de elementos, contabilizar em alternativa o nmero de instrues de atribuio, de maneira a ter uma mtrica uniforme. Para calcular o nmero de operaes efectuadas em invocaes sucessivas das funes de troca e de comparao de elementos, utilizam-se variveis contadoras de durao permanente, ou seja, variveis que so declaradas com o qualificativo static. Como a funo de ordenao pode ser repetidamente invocada, ento conveniente poder reiniciar a contagem, assim como necessrio reportar o resultado final da contagem. Pelo que, as

37

CAPTULO 6 : PESQUISA E ORDENAO

funes tm o parmetro de entrada modo de tipo inteiro, que indica o modo de actuao sobre a varivel contadora. De maneira a aumentar a legibilidade das funes aconselhvel utilizar constantes simblicas que representam o modo de actuao sobre a varivel contadora. Temos as trs constantes simblicas seguintes: REP para reportar o valor da varivel contadora; INIC para inicializar a varivel contadora; e NORM para realizar a operao da funo e para incrementar o valor da varivel contadora. Para calcular o nmero de instrues de atribuio utiliza-se uma varivel de durao permanente na funo SwapCount, tal como se mostra na Figura 6.36. No modo NORM a funo troca os dois elementos e incrementa o valor da varivel contadora de trs unidades, para contabilizar as trs instrues de atribuio.
unsigned int SwapCount (TElem *x, TElem *y, int modo) { static unsigned int cont; /* varivel contadora */ TElem temp; if (modo == REP) return cont; else if (modo == INIC) cont = 0; else if (modo == NORM) { temp = *x; *x = *y; *y = temp; /* efectuar a troca */ cont += 3; /* contagem das 3 instrues de atribuio */ } return 0; }

Figura 6.36 - Funo para trocar elementos do agregado com contabilizao de atribuies.

Para calcular o nmero de comparaes utiliza-se uma varivel de durao permanente em conjuno com a funo de comparao pretendida. Assim encapsula-se a funo de comparao dentro de uma nova funo CCount, tal como se mostra na Figura 6.37, que alm de efectuar a comparao pretendida, usando para o efeito o ponteiro para a funo de comparao, tambm contabiliza o nmero de vezes que invocada. Os elementos a comparar so passados funo por referncia, ou seja, atravs de ponteiros. No modo NORM a funo compara os dois elementos e incrementa a varivel contadora uma unidade, para contabilizar mais uma comparao.
int CCount (TElem *x, TElem *y, PtFComp fcomp, int modo) { static unsigned int cont; /* varivel contadora */ if (modo == REP) return cont; else if (modo == INIC) { cont = 0; return 0; } else if (modo == NORM) { cont++; /* contagem de 1 instruo de comparao */ return fcomp (*x, *y); /* efectuar a comparao */ } }

Figura 6.37 - Funo para comparar elementos do agregado com contabilizao de comparaes.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

38

A Figura 6.38 apresenta a verso generalizada e com contabilizao de instrues do algoritmo de ordenao Sequencial. Antes da ordenao comear, as funes SwapCount e CCount so invocadas para inicializar as variveis contadoras. Durante a ordenao as funes so invocadas no modo normal para desempenharem a sua tarefa. Depois da ordenao terminar, as funes so invocadas para reportar o nmero de comparaes e de instrues de atribuio efectuadas durante a ordenao, que so armazenados nos parmetros de sada nc e na respectivamente. Quando as funes so invocadas para inicializar ou reportar a varivel contadora, os restantes parmetros so passados como sendo ponteiros nulos.
void Sequential_Sort (TElem seq[], unsigned int nelem,\ PtFComp fcomp, int tord, int *nc, int *na) { unsigned int indi, indj; /* inicializao das variveis contadoras */ CCount ((TElem *) NULL, (TElem *) NULL, (PtFComp) NULL, INIC); SwapCount ((TElem *) NULL, (TElem *) NULL, INIC); /* execuo da ordenao com contabilizao das operaes */ for (indi = 0; indi < nelem-1; indi++) for (indj = indi+1; indj < nelem; indj++) if (CCount (&seq[indi], &seq[indj], fcomp, NORM) == tord ) SwapCount (&seq[indi], &seq[indj], NORM); /* relatrio das variveis contadoras */ *nc = CCount ((TElem *) NULL, (TElem *) NULL, (PtFComp) NULL, REP); *na = SwapCount ((TElem *) NULL, (TElem *) NULL, REP); }

Figura 6.38 - Algoritmo de ordenao Sequencial generalizado e com contabilizao de operaes.

Como exerccio de treino altere o programa apresentado na Figura 6.35 de modo a utilizar estas novas verses das funes SwapCount, CCount e Sequential_Sort. Acrescente ainda ao programa a impresso no monitor do nmero de comparaes e do nmero de trocas, que so um tero das instrues de atribuio, depois de cada ordenao.

6.7 Exerccios
1. Pretende-se escrever uma funo de ordenao Fuso de Listas que contemple a situao da existncia de elementos repetidos nos agregados de entrada, e que nessa situao copie apenas um dos elementos repetidos para o agregado de sada.

6.8 Leituras recomendadas


x 13 captulo do livro Data Structures, Algorithms and Software Principles in C, de Thomas A: Standish, da editora Addison-Wesley Publishing Company, 1995. x 7 captulo do livro Data Structures and Algorithm Analysis in C, 2 edio, de Mark Allen Weiss, da editora Addison-Wesley Publishing Company, 1997.

Captulo 7
FILAS E PILHAS

Sumrio
Este captulo dedicado s estruturas de dados lineares que so as filas e as pilhas. Apresentamos as implementaes esttica e semiesttica baseadas em agregados e a implementao dinmica baseada em listas ligadas. Mostramos exemplos de aplicao que utilizam filas e pilhas como elementos de armazenamento e que tiram partido da sua organizao interna para a resoluo de problemas. Finalmente apresentamos uma implementao abstracta, dinmica e com capacidade de mltipla instanciao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

7.1 Introduo
As filas e as pilhas so estruturas de dados lineares que tm implementaes em tudo semelhantes, sendo que diferem apenas na prioridade da retirada de informao. Enquanto que uma fila implementa a poltica do primeiro a chegar primeiro a sair ( first in first out ), a pilha implementa a poltica do ltimo a chegar primeiro a sair ( last in first out ). As filas so muito usadas nos sistemas operativos para funcionarem como reas de armazenamento de informao que deve ser processada por ordem de chegada, como por exemplo, o atendimento de pedidos de utilizao de uma impressora de rede que recebe ficheiros para imprimir, enviados por diferentes computadores instalados na rede. Para sincronizar a interaco entre processos concorrentes que executam a diferentes velocidades de processamento. So tambm utilizadas para simular modelos que descrevem o comportamento de situaes reais de atendimento de pedidos que so processados por ordem de chegada, como por exemplo, simular o atendimento de uma fila de espera num qualquer servio do dia a dia. As pilhas so usadas para gerir algoritmos em que existem processos que invocam subprocessos do mesmo tipo, como o caso dos algoritmos recursivos. Para processar estruturas imbricadas, ou seja, estruturas que contm outras estruturas do mesmo tipo dentro delas, como por exemplo, expresses aritmticas que so compostas por subexpresses aritmticas do mesmo tipo. Assim uma pilha pode ser usada para verificar o balanceamento dos parnteses numa expresso aritmtica, que pode conter vrios nveis de parnteses, que podem ser de tipos diferentes como parnteses curvos, rectos e chavetas. Podem ser usadas para fazer a avaliao de expresses, que contm expresses internas que tm de ser avaliadas previamente, antes do clculo da expresso final. Por exemplo, para calcular uma expresso em notao polaca, que utiliza os smbolos das operaes depois dos operadores. Para fazer a anlise sintctica durante a compilao de um programa, onde existem ciclos repetitivos dentro de ciclos repetitivos, blocos dentro de blocos e portanto, existem estruturas imbricadas que tm de estar balanceadas e correctamente imbricadas. Vamos apresentar as implementaes esttica, semiesttica e dinmica de filas e de pilhas, considerando que estamos perante estruturas de dados concretas, cujo tipo dos elementos de armazenamento concretizado pelo utilizador atravs de um ficheiro de interface, que vamos designar por elemento.h. Este ficheiro de interface, que se apresenta na Figura 7.1, define a constante que parametriza a dimenso da estrutura de dados de suporte, necessrio apenas no caso das implementaes esttica e semiesttica, bem como o tipo de dados do elemento constituinte da memria. No caso da implementao dinmica, este ficheiro precisa apenas de definir o tipo de dados do elemento constituinte da memria. Assim o utilizador do mdulo, pode concretiz-lo para uma estrutura de dados que corresponda s suas necessidades, sem ter a necessidade de reprogramar o ficheiro de implementao do mdulo. Em relao criao de um mdulo abstracto, esta soluo exige a recompilao do mdulo, sempre que este ficheiro modificado.

CAPTULO 7 : FILAS E PILHAS

/*********** Interface da Estrutura de Dados do Mdulo ***********/ /* Nome: elemento.h */ /* Definio da dimenso da estrutura de dados para o caso das implementaes esttica e semiesttica e definio do tipo de dados dos seus elementos. Este ficheiro deve ser modificado para adequar a definio a cada implementao especfica. */ #ifndef _ELEMENTO #define _ELEMENTO /****** Constantes de Parametrizao das Estruturas de Dados ******/ #define typedef #endif N_ELEMENTOS ... TElem; 100 /* nmero de elementos do agregado */ /* tipo de dados dos elementos */ /************* Definio do Tipo de Dados do Elemento *************/

Figura 7.1 - Ficheiro de interface do elemento constituinte da memria.

7.2 Filas
Uma memria fila ( queue/FIFO ) uma memria em que s possvel processar a informao pela ordem de chegada, da que, tambm seja apelidada de memria do primeiro a chegar primeiro a sair. Numa memria fila, o posicionamento para a colocao de um novo elemento na fila, que vamos designar por Fifo_In, a cauda da fila (fifo tail ), e o posicionamento para a remoo de um elemento da fila, que vamos designar por Fifo_Out, a cabea da fila (fifo head ).

7.2.1 Implementao esttica


A Figura 7.2 apresenta o ficheiro de interface da implementao esttica de uma fila, que baseada num agregado de elementos usado de forma circular. Os indicadores de cabea e cauda da fila so variveis de tipo inteiro positivo. Como um agregado tem uma dimenso fixa, antes de se colocar um elemento na fila necessrio verificar se ela est cheia. Em caso afirmativo, mais nenhum elemento pode ser colocado na fila e assinalada a situao de erro, usando o cdigo de erro FIFO_FULL. O elemento que se pretende copiar para a fila passado funo Fifo_In por valor. Por outro lado, o elemento que vai receber a cpia do elemento que se pretende retirar da fila passado funo Fifo_Out por referncia. Pode acontecer que o ponteiro passado funo seja um ponteiro nulo. Nestas circunstncias a funo no pode retirar o elemento da fila, pelo que, no faz nada e assinala esta anomalia, usando o cdigo de erro NULL_PTR. Antes de se retirar um elemento da fila preciso detectar se ela est vazia. Em caso afirmativo, nenhum elemento pode ser retirado da fila e assinalada a situao de erro, usando o cdigo de erro FIFO_EMPTY. Sempre que colocado ou retirado um elemento da fila devolvido o cdigo OK sinalizando que a operao foi realizada com sucesso. De seguida vamos apresentar graficamente o comportamento das operaes de colocao de um elemento na fila e de remoo de um elemento da fila.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

/******************* Interface da FILA Esttica *******************/ /* Nome : fila_est.h */ #ifndef _FILA_ESTATICA #define _FILA_ESTATICA #include "elemento.h" #define #define #define #define OK NULL_PTR FIFO_EMPTY FIFO_FULL 0 1 4 5 /* caracterizao do tipo elemento da fila */ /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ fila vazia */ fila cheia */ /* Definio de Constantes */

/* Aluso s Funes Exportadas pelo Mdulo */ int Fifo_In (TElem elemento); /* Coloca o elemento elemento na cauda da fila. Valores de retorno: OK ou FIFO_FULL. */ int Fifo_Out (TElem *pelemento); /* Retira o elemento da cabea da fila para o elemento apontado por pelemento. Valores de retorno: OK, NULL_PTR ou FIFO_EMPTY. */ #endif

Figura 7.2 - Ficheiro de interface da fila esttica.

A Figura 7.3 mostra o estado inicial da fila. Por uma questo de implementao, vamos considerar que a cauda da fila indica sempre a primeira posio livre para a prxima operao de colocao de um elemento na fila, pelo que, inicialmente aponta para a posio 0 do agregado, enquanto que a cabea da fila indica sempre a posio da prxima operao de remoo de um elemento da fila, pelo que, inicialmente aponta para a posio N do agregado, ou seja, a posio depois do fim do agregado, como indicao de fila vazia. A Figura 7.3 mostra tambm a colocao do primeiro elemento na fila e o estado aps a operao. A cabea da fila vai ficar a apontar para o elemento acabado de colocar, que a posio 0 do agregado, e, fica nesta posio enquanto este elemento no for retirado da fila. A cauda da fila deslocada para a posio 1 do agregado, que a primeira posio livre para colocar o prximo elemento.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cauda da fila

estado inicial

cabea da fila

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila

cauda da fila

colocao do primeiro elemento

Figura 7.3 - Situao inicial da fila e aps a colocao do primeiro elemento.

CAPTULO 7 : FILAS E PILHAS

Sempre que se coloca um elemento na fila, a cauda da fila deslocada para o elemento seguinte da fila. Existem duas situaes distintas. Na primeira situao, que se apresenta na Figura 7.4, a posio seguinte da fila est ainda dentro da dimenso do agregado, pelo que, a cauda continua atrs da cabea.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila

cauda da fila

colocao de um elemento

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila

cauda da fila

Figura 7.4 - Colocao de um elemento na fila.

Na segunda situao, que se apresenta na Figura 7.5, a posio seguinte da fila est fora da dimenso do agregado, pelo que, a cauda da fila passa para o incio do agregado e fica frente da cabea. O que parece uma situao anmala quando comparada com o funcionamento de uma fila de espera no dia a dia, em que a cabea est sempre frente da cauda. Mas, preciso ter em considerao que estamos perante uma fila circular. Para conseguir este efeito circular da cauda da fila utiliza-se o operador mdulo, para calcular sempre um valor que est entre 0 e N_ELEMENTOS1. Em alternativa utilizao circular do agregado, a fila poderia ser implementada de forma linear, o que implicaria deslocar todos os elementos da fila, para o incio do agregado, sempre que se retirasse um elemento da fila. Mas, tal implementao seria muito ineficiente para filas grandes.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

colocao de um elemento

cabea da fila

cauda da fila

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cauda da fila

cabea da fila

Figura 7.5 - Colocao de um elemento na fila com movimentao circular.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

A Figura 7.6 apresenta a situao de colocao do ltimo elemento na fila. Nesta situao a cauda da fila fica a apontar para o mesmo elemento que a cabea da fila, o que significa que a fila ficou cheia. Enquanto este estado durar, no possvel colocar mais elementos na fila.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cauda da fila

cabea da fila

colocao do ltimo elemento

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila cauda da fila

FILA CHEIA

Figura 7.6 - Colocao do ltimo elemento na fila.

Sempre que um elemento retirado da fila, o indicador de cabea da fila deslocado para o elemento seguinte. Como j foi referido, esta implementao no a forma habitual de funcionamento de um fila, onde sempre que o elemento da cabea da fila sai da fila, toda a fila deslocada para a frente. Existem duas situaes distintas quando se retira um elemento da fila. Na primeira situao, que se apresenta na Figura 7.7, a posio seguinte da fila est ainda dentro da dimenso do agregado, pelo que, a cabea ainda no deu a volta ao agregado. Neste caso ainda est atrs da cauda, porque a cauda j excedeu o fim do agregado.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cauda da fila

cabea da fila

remoo de um elemento

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cauda da fila

cabea da fila

Figura 7.7 - Remoo de um elemento da fila.

CAPTULO 7 : FILAS E PILHAS

Na segunda situao, que se apresenta na Figura 7.8, a posio seguinte da fila est fora da dimenso do agregado, pelo que, a cabea passa para o incio do agregado e neste caso fica de novo frente da cauda. Para conseguir este efeito circular da cabea da fila utiliza-se o operador mdulo, para calcular sempre um valor que est entre 0 e N_ELEMENTOS1.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

remoo de um elemento

cauda da fila

cabea da fila

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila

cauda da fila

Figura 7.8 - Remoo de um elemento da fila com movimentao circular.

A Figura 7.9 apresenta a situao de remoo do ltimo elemento da fila. Nesta situao a cabea da fila fica a apontar para o mesmo elemento que a cauda da fila, o que significa que a fila ficou vazia. Enquanto este estado durar, no possvel retirar mais elementos da fila. Nesta situao, a cabea da fila deve ficar a apontar para a posio N do agregado, ou seja, a posio depois do fim do agregado, como indicao de fila vazia. Assim, fica reposta o estado inicial da fila, com a diferena que neste caso, a cauda da fila no est a apontar para a posio 0 do agregado. Quando se colocar um elemento na fila, na posio apontada pela cauda da fila, a cabea da fila ser colocada de novo a apontar para o elemento acabado de colocar.

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

cabea da fila

cauda da fila

remoo do ltimo elemento

FIFO[0]

...

FIFO[N-1]

Elemento Elemento Elemento Elemento Elemento Elemento Elemento

FILA VAZIA

cauda da fila cabea da fila

cabea da fila

Figura 7.9 - Remoo do ltimo elemento da fila.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

A Figura 7.10 apresenta o ficheiro de implementao da fila esttica. A estrutura de dados do mdulo constituda pelo agregado FILA, que um agregado de elementos do tipo TElem definido no ficheiro de interface elemento.h, e pelas variveis inteiras positivas head para a cabea da fila e tail para a cauda da fila. A estrutura de dados declarada com o qualificativo static, de modo a estar protegida do exterior.
/***************** Implementao da FILA Esttica *****************/ /* Nome : fila_est.c */ #include <stdio.h> #include "fila_est.h" /* Ficheiro de interface do mdulo */ /* rea de armazenamento */ /* cabea da fila */ /* cauda da fila */ /* Definio da Estrutura de Dados Interna da FILA */ static TElem FILA[N_ELEMENTOS]; static unsigned int head = N_ELEMENTOS; static unsigned int tail = 0; int Fifo_In (TElem elemento) { if (head == tail) return FIFO_FULL; FILA[tail] = elemento; if (head == N_ELEMENTOS) head = tail; tail = ++tail % N_ELEMENTOS; return OK; } int Fifo_Out (TElem *pelemento) { if (pelemento == NULL) return NULL_PTR; if (head == N_ELEMENTOS) return FIFO_EMPTY; *pelemento = FILA[head]; head = ++head % N_ELEMENTOS; if (head == tail) head = N_ELEMENTOS; return OK; }

/* Definio das Funes */

Figura 7.10 - Ficheiro de implementao da fila esttica.

7.2.2 Implementao semiesttica


A Figura 7.11 e a Figura 7.12 apresentam respectivamente o ficheiro de interface e o ficheiro de implementao da fila semiesttica. A implementao semiesttica da fila baseada no agregado FILA, que um agregado de ponteiros para elementos do tipo de dados TElem, que est definido no ficheiro de interface elemento.h, e pelas variveis inteiras positivas head para a cabea da fila e tail para a cauda da fila. Tal como na implementao esttica, o agregado usado de forma circular. A implementao em tudo semelhante da implementao esttica. A nica diferena que quando se coloca um elemento na fila necessrio atribuir memria para o armazenamento do elemento. A atribuio da memria feita recorrendo funo malloc, uma vez que, a atribuio feita elemento a elemento. Se no existir memria para essa atribuio, o elemento no pode ser colocado na fila, pelo que, assinalada a situao de inexistncia de memria, usando o cdigo de erro NO_MEM. Por outro lado, quando se retira um elemento da fila necessrio libertar a memria ocupada com o armazenamento do elemento, recorrendo funo free e colocar o elemento do agregado a apontar para NULL.

CAPTULO 7 : FILAS E PILHAS

/***************** Interface da FILA Semiesttica *****************/ /* Nome : fila_semiest.h */ #ifndef _FILA_SEMIESTATICA #define _FILA_SEMIESTATICA #include "elemento.h" #define #define #define #define #define OK NULL_PTR NO_MEM FIFO_EMPTY FIFO_FULL 0 1 3 4 5 /* caracterizao do tipo elemento da fila */ /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ memria esgotada */ fila vazia */ fila cheia */ /* Definio de Constantes */

/* Aluso s Funes Exportadas pelo Mdulo */ int Fifo_In (TElem elemento); /* Coloca o elemento elemento na cauda da fila. Valores de retorno: OK, FIFO_FULL ou NO_MEM. */ int Fifo_Out (TElem *pelemento); /* Retira o elemento da cabea da fila para o elemento apontado por pelemento. Valores de retorno: OK, NULL_PTR ou FIFO_EMPTY. */ #endif

Figura 7.11 - Ficheiro de interface da fila semiesttica.


/*************** Implementao da FILA Semiesttica ***************/ /* Nome : fila_semiest.c */ #include <stdio.h> #include <stdlib.h> #include "fila_semiest.h" /* Ficheiro de interface do mdulo */ /* rea de armazenamento */ /* cabea da fila */ /* cauda da fila */ /* Definio da Estrutura de Dados Interna da FILA */ static TElem *FILA[N_ELEMENTOS]; static unsigned int head = N_ELEMENTOS; static unsigned int tail = 0;

/* Definio das Funes */ int Fifo_In (TElem elemento) { if (head == tail) return FIFO_FULL; if ((FILA[tail] = (TElem *) malloc (sizeof (TElem))) == NULL) return NO_MEM; *FILA[tail] = elemento; if (head == N_ELEMENTOS) head = tail; tail = ++tail % N_ELEMENTOS; return OK; } int Fifo_Out (TElem *pelemento) { if (pelemento == NULL) return NULL_PTR; if (head == N_ELEMENTOS) return FIFO_EMPTY; *pelemento = *FILA[head]; free (FILA[head]); FILA[head] = NULL; head = ++head % N_ELEMENTOS; if (head == tail) head = N_ELEMENTOS; return OK; }

Figura 7.12 - Ficheiro de implementao da fila semiesttica.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

10

7.2.3 Implementao dinmica


A Figura 7.13 apresenta o ficheiro de interface da implementao dinmica de uma fila, que baseada numa lista ligada de elementos. Uma lista ligada uma estrutura constituda por elementos, a que vamos chamar ns, ligados atravs de ponteiros. Cada n da lista ligada constitudo por dois ponteiros, um para o elemento que armazena a informao e outro para o n seguinte da lista. O ltimo n da lista aponta para NULL, para servir de indicador de finalizao da fila. A memria para os ns e para os elementos atribuda, quando um elemento colocado na fila e libertada quando um elemento retirado da fila. Os indicadores de cabea e cauda da fila so ponteiros. A cabea da fila aponta para o elemento mais antigo que se encontra na fila e que o primeiro a ser retirado. A cauda da fila aponta sempre para o elemento mais recente que se encontra na fila e frente do qual se insere um novo elemento. Quando so ambos ponteiros nulos, sinal que a fila est vazia. Uma fila dinmica nunca est cheia. Quando muito, pode no existir memria para continuar a acrescentar-lhe mais elementos. Portanto, as situaes de erro so as mesmas da implementao semiesttica, com excepo que no existe o cdigo de erro FIFO_FULL.
/******************* Interface da FILA Dinmica *******************/ /* Nome : fila_din.h */ #ifndef _FILA_DINAMICA #define _FILA_DINAMICA #include "elemento.h" #define #define #define #define OK NULL_PTR NO_MEM FIFO_EMPTY 0 1 3 4 /* caracterizao do tipo elemento da fila */ /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ memria esgotada */ fila vazia */ /* Definio de Constantes */

/* Aluso s Funes Exportadas pelo Mdulo */ int Fifo_In (TElem elemento); /* Coloca o elemento apontado por elemento na cauda da fila. Valores de retorno: OK ou NO_MEM. */ int Fifo_Out (TElem *pelemento); /* Retira o elemento da cabea da fila para o elemento apontado por pelemento. Valores de retorno: OK, NULL_PTR ou FIFO_EMPTY. */ #endif

Figura 7.13 - Ficheiro de interface da fila dinmica.

Vamos apresentar de forma grfica as operaes de colocao e remoo de elementos numa fila dinmica. A Figura 7.14 apresenta o estado inicial da fila e o estado aps a colocao do primeiro elemento. Para colocar um elemento na fila primeiro necessrio atribuir memria para o armazenamento do n da lista e depois para o armazenamento do elemento. Se no existir memria para essas atribuies, ento o elemento no pode ser colocado na fila e assinalada esta situao de erro. Aps a atribuio da memria, o elemento ligado ao n e o n ligado fila. O n fica a apontar para NULL, uma vez que o ltimo n da fila, ou seja, ele o elemento finalizador da fila. A cauda da fila posta a apontar para ele e finalmente, a informao a armazenar copiada para o elemento. No caso da colocao do primeiro elemento na fila, ele no precisa de ser ligado fila, mas, em contrapartida a cabea da fila que estava a apontar para NULL colocada a apontar para o n deste elemento. Ele simultaneamente o primeiro e ltimo elemento da fila.

11

CAPTULO 7 : FILAS E PILHAS

cabea da fila cabea da fila

cauda da fila

estado inicial

PtSeg PtEle

cauda da fila Elemento 1

colocao do primeiro elemento

Figura 7.14 - Situao inicial da fila e aps a colocao do primeiro elemento.

A Figura 7.15 apresenta a colocao de mais um elemento na fila. Neste caso, este elemento tem de ser ligado fila, pelo que, o n do elemento que est apontado pela cauda e que aponta para NULL, posto a apontar para o novo n, que passa agora a ser o ltimo elemento da fila. A cauda da fila actualizada e a cabea da fila continua inalterada.
cabea da fila

PtSeg PtEle

PtSeg PtEle

Elemento 1 cauda da fila cabea da fila

Elemento 2

colocao de um elemento

PtSeg PtEle

PtSeg PtEle

PtSeg PtEle

Elemento 1 cauda da fila

Elemento 2

Elemento 3

Figura 7.15 - Colocao de mais um elemento na fila.

A Figura 7.16 apresenta a remoo de um elemento da fila. A informao armazenada no elemento copiada e depois a cabea da fila colocada a apontar para o elemento seguinte que vai passar agora a ser a nova cabea da fila. Esta operao feita atribuindo cabea da fila o valor apontado pelo ponteiro PtSeg, que aponta para o n seguinte da fila. Toda a memria ocupada pelo elemento e pelo n libertada. A Figura 7.17 mostra a remoo do ltimo elemento da fila. Como este ltimo n aponta para NULL, ao ser atribudo o valor NULL cabea da fila isso sinal de que a fila ficou vazia, pelo que, a cauda da fila tambm colocada a apontar para NULL. Aps esta operao, a fila fica num estado igual ao estado inicial.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

12

cabea da fila

PtSeg PtEle

PtSeg PtEle

PtSeg PtEle

Elemento 1 cauda da fila cabea da fila

Elemento 2

Elemento 3

PtSeg PtEle

PtSeg PtEle

remoo de um elemento

Elemento 2 cauda da fila

Elemento 3

Figura 7.16 - Remoo de um elemento da fila.


cabea da fila

PtSeg PtEle

cauda da fila Elemento 3

remoo do ltimo elemento

cabea da fila

cauda da fila

FILA VAZIA

Figura 7.17 - Remoo do ltimo elemento da fila.

A Figura 7.18 apresenta o ficheiro de implementao da fila dinmica. A estrutura de dados do mdulo constituda por uma lista ligada de ns do tipo struct no, sendo cada n constitudo pelo ponteiro pelemento para um elemento do tipo TElem, que est definido no ficheiro de interface elemento.h, e pelo ponteiro pseg para fazer a ligao do n ao n seguinte da fila, caso ele exista. Para controlar a fila existem os ponteiros, head e tail respectivamente para a cabea e para a cauda da fila, que so declarados com o qualificativo static, de modo a estarem protegidas do exterior. Como a fila inicialmente est vazia, estes ponteiros so inicializadas a NULL. Quando um elemento colocado na fila, a atribuio de memria para o seu crescimento, comea pela atribuio de memria para o n, que caso seja bem sucedida, prossegue com a atribuio de memria para o elemento. Caso no exista memria para o elemento, a colocao de mais um elemento na fila cancelada e a memria anteriormente atribuda para o n libertada, antes da funo terminar a sua execuo com o cdigo de erro NO_MEM. No faz sentido ter um n na fila, seno podemos colocar tambm o elemento que vai armazenar a informao. O n do novo elemento aponta para NULL, uma vez que o ltimo elemento da fila. A funo prossegue com a ligao do ltimo elemento que estava na fila ao novo elemento e com a actualizao do ponteiro cauda da fila que fica a apontar para o novo elemento. Finalmente feita a

13

CAPTULO 7 : FILAS E PILHAS

cpia da informao para o elemento. Quando um elemento retirado da fila, primeiro verificado se o ponteiro passado no nulo e s depois que a informao armazenada no elemento retirada da fila. Caso a fila no esteja vazia, o ponteiro cabea da fila colocado a apontar para o elemento seguinte. Quando o ltimo elemento retirado da fila, ento a cabea da fila fica a apontar para NULL, o que significa que a fila ficou vazia, pelo que, a cauda da fila tambm colocada a apontar para NULL. Depois a memria ocupada pelo elemento libertada e s depois que memria ocupada pelo n libertada.
/***************** Implementao da FILA Dinmica *****************/ /* Nome : fila_din.c */ #include <stdio.h> #include <stdlib.h> #include "fila_din.h" /* Ficheiro de interface do mdulo */ /* Definio da Estrutura de Dados Interna da FILA */ typedef struct no *PtNo; struct no { TElem *pelemento; PtNo pseg; }; static PtNo head = NULL; static PtNo tail = NULL; /* Definio das Funes */ int Fifo_In (TElem elemento) { PtNo tmp; if ((tmp = (PtNo) malloc (sizeof (struct no))) == NULL) return NO_MEM; if ((tmp->pelemento = (TElem *) malloc (sizeof(TElem))) == NULL) { free (tmp); return NO_MEM; } tmp->pseg = NULL; if (tail == NULL) head = tmp; else tail->pseg = tmp; tail = tmp; *tail->pelemento = elemento; return OK; } int Fifo_Out (TElem *pelemento) { PtNo tmp; if (pelemento == NULL) return NULL_PTR; if (head == NULL) return FIFO_EMPTY; *pelemento = *head->pelemento; tmp = head; head = head->pseg; if (head == NULL) tail = NULL; free (tmp->pelemento); free (tmp); return OK; }

/* ponteiro para o elemento */ /* ponteiro para o n seguinte */ /* cabea da fila */ /* cauda da fila */

Figura 7.18 - Ficheiro de implementao da fila dinmica.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

14

7.3 Pilhas
Uma memria pilha ( stack/LIFO ) uma memria em que s possvel processar a informao pela ordem inversa ordem de chegada. Da que, tambm seja apelidada de memria do ltimo a chegar primeiro a sair. Numa memria pilha, o posicionamento para a colocao de um elemento na pilha, que vamos designar por Stack_Push, e o posicionamento para a remoo de um elemento da pilha, que vamos designar por Stack_Pop, o topo da pilha (top of the stack).

7.3.1 Implementao esttica


A Figura 7.19 apresenta o ficheiro de interface da implementao esttica de uma pilha, que baseada num agregado de elementos. O indicador de topo da pilha uma varivel de tipo inteiro positivo. Como um agregado tem uma dimenso fixa, antes de se colocar um elemento na pilha necessrio verificar se a pilha est cheia. Em caso afirmativo, mais nenhum elemento pode ser colocado na pilha e assinalada a situao de erro, usando o cdigo de erro STACK_FULL. O elemento que se pretende copiar para a pilha passado funo Stack_Push por valor. Por outro lado, o elemento que vai receber a cpia do elemento que se pretende retirar da pilha passado funo Stack_Pop por referncia. Pode acontecer que o ponteiro passado funo seja um ponteiro nulo. Nestas circunstncias a funo no pode retirar o elemento da pilha, pelo que, no faz nada e assinala esta anomalia, usando o cdigo de erro NULL_PTR. Antes de se retirar um elemento da pilha preciso detectar se a pilha est vazia. Em caso afirmativo, nenhum elemento pode ser retirado da pilha e assinalada a situao de erro, usando o cdigo de erro STACK_EMPTY. Sempre que colocado ou retirado um elemento da pilha devolvido o cdigo OK sinalizando que a operao foi realizada com sucesso.
/****************** Interface do PILHA Esttica ******************/ /* Nome : pilha_est.h */ #ifndef _PILHA_ESTATICA #define _PILHA_ESTATICA #include "elemento.h" #define #define #define #define OK NULL_PTR STACK_EMPTY STACK_FULL 0 1 4 5 /* caracterizao do tipo elemento da pilha */ /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ pilha vazia */ pilha cheia */ /* Definio de Constantes */

/* Aluso s Funes Exportadas pelo Mdulo */ int Stack_Push (TElem elemento); /* Coloca o elemento apontado por elemento no topo da pilha. Valores de retorno: OK ou STACK_FULL. */ int Stack_Pop (TElem *pelemento); /* Retira o elemento do topo da pilha para o elemento apontado por pelemento. Valores de retorno: OK, NULL_PTR ou STACK_EMPTY. */ #endif

Figura 7.19 - Ficheiro de interface da pilha esttica.

15

CAPTULO 7 : FILAS E PILHAS

De seguida, vamos apresentar graficamente o comportamento das operaes de colocao de um elemento na pilha e de remoo de um elemento da pilha. A Figura 7.20 mostra o estado inicial da pilha. Por uma questo de implementao, vamos considerar que o topo da pilha indica sempre a primeira posio livre para a prxima operao de colocao de um elemento na pilha, pelo que, inicialmente aponta para a posio 0 do agregado. A Figura 7.20 mostra tambm a colocao do primeiro elemento na pilha e o estado da pilha aps a operao. O topo da pilha deslocado para a posio 1 do agregado, que a primeira posio livre para colocar o prximo elemento.
estado inicial colocao do primeiro elemento STACK[N-1] Elemento

STACK[N-1] Elemento

. . .

Elemento

Elemento Elemento topo da pilha

. . .

Elemento Elemento topo da pilha

STACK[1]

STACK[1]

Elemento

STACK[0]

Elemento

STACK[0]

Elemento

Figura 7.20 - Situao inicial da pilha e aps a colocao do primeiro elemento.

Sempre que se coloca um elemento na pilha, o topo da pilha deslocada para a posio seguinte da pilha, depois da cpia do elemento para a pilha. A Figura 7.21 apresenta a situao limite de utilizao da pilha, quando se coloca o ltimo elemento na pilha. Nesta situao o topo da pilha fica a apontar para a posio do agregado de ndice N, o que significa que a pilha ficou cheia. Enquanto este estado durar, no possvel colocar mais elementos na pilha.
colocao do ltimo elemento

PILHA CHEIA STACK[N-1] Elemento

topo da pilha

STACK[N-1] Elemento

topo da pilha

. . .

Elemento

Elemento Elemento

. . .

Elemento Elemento

STACK[1]

STACK[1]

Elemento

STACK[0]

Elemento

STACK[0]

Elemento

Figura 7.21 - Colocao do ltimo elemento na pilha.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

16

Sempre que um elemento retirado da pilha, primeiro preciso deslocar o topo da pilha para a posio anterior da pilha, onde est armazenado o ltimo elemento da pilha. S depois que o elemento copiado para fora da pilha. A Figura 7.22 apresenta a situao de remoo de um elemento, quando existe mais do que um elemento na pilha. O topo da pilha deslocado para a posio anterior e a informao armazenada no elemento retirado da pilha.
remoo de um elemento STACK[N-1] Elemento topo da pilha STACK[N-1] Elemento

. . .

Elemento

Elemento

. . .

Elemento Elemento topo da pilha

STACK[1]

Elemento Elemento

STACK[1]

Elemento

STACK[0]

STACK[0]

Elemento

Figura 7.22 - Remoo de um elemento da pilha.

A Figura 7.23 apresenta a situao de remoo do ltimo elemento da pilha. Aps a operao, o topo da pilha fica a apontar para a posio do agregado de ndice 0, o que significa que ficou reposta o estado inicial da pilha, ou seja, a pilha ficou vazia. Enquanto este estado durar, no possvel retirar mais elementos da pilha.
remoo do ltimo elemento STACK[N-1] Elemento STACK[N-1] Elemento

. . .

Elemento Elemento topo da pilha

. . .

Elemento

Elemento PILHA VAZIA topo da pilha

STACK[1]

Elemento

STACK[1]

Elemento Elemento

STACK[0]

Elemento

STACK[0]

Figura 7.23 - Remoo do ltimo elemento da fila.

A Figura 7.24 apresenta o ficheiro de implementao da pilha esttica. A estrutura de dados do mdulo constituda pelo agregado PILHA, que um agregado de elementos do tipo TElem, que est definido no ficheiro de interface elemento.h, e pela varivel inteira positiva top para o topo da pilha. A estrutura de dados declarada com o qualificativo static, de modo a estar protegida do exterior.

17

CAPTULO 7 : FILAS E PILHAS

/**************** Implementao da PILHA Esttica ****************/ /* Nome : pilha_est.c */ #include <stdio.h> #include "pilha_est.h" static TElem PILHA[N_ELEMENTOS]; static unsigned int top = 0; /* Ficheiro de interface do mdulo */ /* rea de armazenamento */ /* topo da pilha */ /* Definio da Estrutura de Dados Interna da PILHA */

/* Definio das Funes */ int Stack_Push (TElem elemento) { if (top == N_ELEMENTOS) return STACK_FULL; PILHA[top++] = elemento; return OK; } int Stack_Pop (TElem *pelemento) { if (pelemento == NULL) return NULL_PTR; if (top == 0) return STACK_EMPTY; *pelemento = PILHA[--top]; return OK; }

Figura 7.24 - Ficheiro de implementao da pilha esttica.

7.3.2 Implementao semiesttica


A Figura 7.25 e a Figura 7.26 apresentam respectivamente o ficheiro de interface e o ficheiro de implementao da pilha semiesttica. A implementao semiesttica da pilha baseada no agregado PILHA, que um agregado de ponteiros para elementos do tipo de dados TElem, que est definido no ficheiro de interface elemento.h, e pela varivel inteira positiva top para o topo da pilha. O agregado usado tal como na implementao esttica. A implementao em tudo semelhante da implementao esttica. A nica diferena que quando se coloca um elemento na pilha necessrio atribuir memria para o armazenamento do elemento. A atribuio da memria feita recorrendo funo malloc, uma vez que, a atribuio feita elemento a elemento. Se no existir memria para essa atribuio, o elemento no pode ser colocado na pilha e assinalada a situao de inexistncia de memria, usando o cdigo de erro NO_MEM. Por outro lado, quando se retira um elemento da pilha necessrio libertar a memria ocupada com o armazenamento do elemento, recorrendo funo free e colocar o elemento do agregado a apontar para NULL.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

18

/**************** Interface do PILHA Semiesttica ****************/ /* Nome : pilha_semiest.h */ #ifndef _PILHA_SEMIESTATICA #define _PILHA_SEMIESTATICA #include "elemento.h" #define #define #define #define #define OK NULL_PTR NO_MEM STACK_EMPTY STACK_FULL 0 1 3 4 5 /* caracterizao do tipo elemento da pilha */ /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ memria esgotada */ pilha vazia */ pilha cheia */ /* Definio de Constantes */

/* Aluso s Funes Exportadas pelo Mdulo */ int Stack_Push (TElem elemento); /* Coloca o elemento apontado por elemento no topo da pilha. Valores de retorno: OK, STACK_FULL ou NO_MEM. */ int Stack_Pop (TElem *pelemento); /* Retira o elemento do topo da pilha para o elemento apontado por pelemento. Valores de retorno: OK, NULL_PTR ou STACK_EMPTY. */ #endif

Figura 7.25 - Ficheiro de interface da pilha semiesttica.


/************** Implementao da PILHA Semiesttica **************/ /* Nome : pilha_semiest.c */ #include <stdio.h> #include <stdlib.h> #include "pilha_semiest.h" /* Ficheiro de interface do mdulo */ /* rea de armazenamento */ /* topo da pilha */ /* Definio da Estrutura de Dados Interna da PILHA */ static TElem *PILHA[N_ELEMENTOS]; static unsigned int top = 0;

/* Definio das Funes */ int Stack_Push (TElem elemento) { if (top == N_ELEMENTOS) return STACK_FULL; if ((PILHA [top] = (TElem *) malloc (sizeof (TElem))) == NULL) return NO_MEM; *PILHA[top++] = elemento; return OK; } int Stack_Pop (TElem *pelemento) { if (pelemento == NULL) return NULL_PTR; if (top == 0) return STACK_EMPTY; *pelemento = *PILHA[--top]; free (PILHA[top]); PILHA[top] = NULL; return OK; }

Figura 7.26 - Ficheiro de implementao da pilha semiesttica.

19

CAPTULO 7 : FILAS E PILHAS

7.3.3 Implementao dinmica


A Figura 7.27 apresenta o ficheiro de interface da implementao dinmica, que baseada numa lista ligada de elementos. Uma lista ligada uma estrutura constituda por elementos, a que vamos chamar ns, ligados atravs de ponteiros. Cada n da lista ligada constitudo por dois ponteiros, um para o elemento que armazena a informao e outro para o n anterior da lista. O primeiro n da lista aponta para NULL, para servir de indicador de finalizao da pilha. A memria para os ns e para os elementos da pilha atribuda, quando um elemento colocado na pilha e libertada quando um elemento retirado da pilha. O indicador de topo da pilha um ponteiro, que aponta para o elemento mais recentemente colocado na pilha e que o primeiro a ser retirado. Quando o topo da pilha um ponteiro nulo, sinal que a pilha est vazia. Uma pilha dinmica nunca est cheia. Quando muito, pode no existir memria para continuar a acrescentar-lhe mais elementos. Portanto, as situaes de erro so as mesmas da implementao semiesttica, com excepo que no existe o cdigo de erro STACK_FULL.
/****************** Interface da PILHA Dinmica ******************/ /* Nome : pilha_din.h */ #ifndef _PILHA_DINAMICA #define _PILHA_DINAMICA #include "elemento.h" #define #define #define #define OK NULL_PTR NO_MEM STACK_EMPTY 0 1 3 4 /* caracterizao do tipo elemento da pilha */ /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ memria esgotada */ pilha vazia */ /* Definio de Constantes */

/* Aluso s Funes Exportadas pelo Mdulo */ int Stack_Push (TElem elemento); /* Coloca o elemento apontado por elemento no topo da pilha. Valores de retorno: OK ou NO_MEM. */ int Stack_Pop (TElem *pelemento); /* Retira o elemento do topo da pilha para o elemento apontado por pelemento. Valores de retorno: OK, NULL_PTR ou STACK_EMPTY. */ #endif

Figura 7.27 - Ficheiro de interface da pilha dinmica.

Vamos apresentar de forma grfica as operaes de colocao e remoo de elementos numa pilha dinmica. A Figura 7.28 apresenta o estado inicial da pilha e o estado aps a colocao do primeiro elemento. Para colocar um elemento na pilha necessrio atribuir memria, primeiro para o armazenamento do n da lista e depois para o armazenamento do elemento. Se no existir memria para essas atribuies, ento o elemento no pode ser colocado na pilha e assinalada esta situao de erro. Aps a atribuio da memria, o elemento ligado ao n e o n ligado pilha. O n fica a apontar para o n anterior que o topo da pilha, que depois actualizado, sendo colocado a apontar para este novo n. Finalmente, a informao a armazenar copiada para o elemento. No caso da colocao do primeiro elemento na pilha, ele no precisa de ser ligado pilha, mas, em contrapartida colocado a apontar para NULL, indicando que ele o primeiro elemento da pilha. Ele simultaneamente o primeiro e o ltimo elemento da pilha.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

20

estado inicial

colocao do primeiro elemento topo da pilha

topo da pilha

PtEle PtAnt

Elemento 1

Figura 7.28 - Situao inicial da pilha e aps a colocao do primeiro elemento na pilha.

A Figura 7.29 apresenta a colocao de mais um elemento na pilha. O n do novo elemento posto a apontar para o antigo topo da pilha, atravs do ponteiro PtAnt, e depois passa a ser o novo topo da pilha. Ou seja, o topo da pilha actualizado, ficando a apontar para este novo n.
colocao de um elemento topo da pilha

PtEle PtAnt

Elemento 2

topo da pilha

PtEle PtAnt

Elemento 1

PtEle PtAnt

Elemento 1

Figura 7.29 - Colocao de mais um elemento na pilha.

A Figura 7.30 apresenta a remoo de um elemento da pilha. A informao armazenada no elemento copiada. Depois o topo da pilha colocada a apontar para o elemento anterior que vai passar agora a ser o novo topo da pilha. Esta operao feita atribuindo ao topo da pilha o valor apontado pelo ponteiro PtAnt, que aponta para o n anterior da pilha. Depois toda a memria ocupada pelo elemento e pelo n libertada.
remoo de um elemento topo da pilha

PtEle PtAnt

Elemento 2

PtEle PtAnt

Elemento 1

topo da pilha

PtEle PtAnt

Elemento 1

Figura 7.30 - Remoo de um elemento da pilha.

21

CAPTULO 7 : FILAS E PILHAS

A Figura 7.31 mostra a remoo do ltimo elemento da pilha. Como este ltimo n aponta para NULL, ao ser atribudo o valor NULL ao topo da pilha isso sinal de que a pilha ficou vazia, e a pilha fica num estado igual ao estado inicial.
remoo do ltimo elemento topo da pilha topo da pilha PILHA VAZIA

PtEle PtAnt

Elemento 1

Figura 7.31 - Remoo do ltimo elemento da pilha.

A Figura 7.32 apresenta o ficheiro de implementao da pilha dinmica. A estrutura de dados do mdulo constituda por uma lista ligada de ns do tipo struct no, sendo cada n constitudo pelo ponteiro pelemento para um elemento do tipo TElem, que est definido no ficheiro de interface elemento.h, e pelo ponteiro pant para fazer a ligao do n ao n anterior da pilha, caso ele exista. Para controlar a pilha existe o ponteiro top para o topo da pilha, que declarado com o qualificativo static, de modo a estar protegido do exterior. Como a pilha inicialmente est vazia, este ponteiro inicializado a NULL. Quando um elemento colocado na pilha, a atribuio de memria para o seu crescimento, comea pela atribuio de memria para o n, que caso seja bem sucedida, prossegue com a atribuio de memria para o elemento. Caso no exista memria para o elemento, a colocao de mais um elemento na pilha cancelada e a memria anteriormente atribuda para o n libertada, antes da funo terminar a sua execuo com o cdigo de erro NO_MEM. No faz sentido ter um n na pilha, seno podemos colocar tambm o elemento que vai armazenar a informao. A funo continua com a ligao do novo elemento ao ltimo elemento que estava na pilha, pelo que, o n do primeiro elemento da pilha fica a apontar para NULL. Depois feita a actualizao do ponteiro topo da pilha que fica a apontar para o novo elemento e finalmente feita a cpia da informao para o elemento. Quando um elemento retirado da pilha, primeiro verificado se o ponteiro passado no nulo e s depois que a informao armazenada no elemento retirada da pilha. O ponteiro topo da pilha colocado a apontar para o elemento anterior. Quando o ltimo elemento retirado da pilha, ento o topo da pilha fica a apontar para NULL, o que significa que a pilha ficou vazia. Depois a memria ocupada pelo elemento libertada e s depois que a memria ocupada pelo n da pilha libertada.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

22

/**************** Implementao da PILHA Dinmica ****************/ /* Nome : pilha_din.c */ #include <stdio.h> #include <stdlib.h> #include "pilha_din.h" typedef struct no *PtNo; struct no { TElem *pelemento; PtNo pant; }; static PtNo top = NULL; /* Definio das Funes */ int Stack_Push (TElem elemento) { PtNo tmp; if ((tmp = (PtNo) malloc (sizeof (struct no))) == NULL) return NO_MEM; if ((tmp->pelemento = (TElem *) malloc (sizeof(TElem))) == NULL) { free (tmp); return NO_MEM; } tmp->pant = top; top = tmp; *top->pelemento = elemento; return OK; } int Stack_Pop (TElem *pelemento) { PtNo tmp; if (pelemento == NULL) return NULL_PTR; if (top == NULL) return STACK_EMPTY; *pelemento = *top->pelemento; tmp = top; top = top->pant; free (tmp->pelemento); free (tmp); return OK; }

/* Ficheiro de interface do mdulo */

/* Definio da Estrutura de Dados Interna da PILHA */

/* ponteiro para o elemento */ /* ponteiro para o n anterior */ /* topo da pilha */

Figura 7.32 - Ficheiro de implementao da pilha dinmica.

7.4 Exemplos de aplicao de filas e pilhas


Uma das aplicaes das pilhas a anlise e processamento de estruturas imbricadas. Estruturas imbricadas so estruturas que so compostas internamente por outras estruturas do mesmo tipo, onde existe a necessidade de garantir que uma estrutura interna finalizada antes da estrutura externa. Um exemplo tpico de uma estrutura imbricada uma expresso aritmtica que normalmente composta por subexpresses aritmticas entre parnteses

23

CAPTULO 7 : FILAS E PILHAS

curvos. Por vezes para melhor identificar os vrios nveis de subexpresses, utilizam-se parnteses curvos, rectos e chavetas, tal se mostra na seguinte expresso. { A + B * [ C / (F+A) + (C+D) * (EF) ] } / [ (A+B) * C ] Para que uma expresso aritmtica possa ser correctamente calculada necessrio assegurar o correcto balanceamento dos parnteses, como o caso da expresso anterior. Uma expresso com um balanceamento correcto de parnteses, tem tantos parnteses a abrir, ou seja, parnteses esquerdos, como parnteses a fechar, ou seja, parnteses direitos. Mas tambm preciso assegurar que um nvel interno de parnteses fechado antes que um nvel externo e com um parntese direito equivalente ao parntese esquerdo. Pelo que, a anlise no pode apenas limitar-se a contar o nmero de parnteses direitos e esquerdos de cada tipo. Por exemplo, a expresso seguinte tem o mesmo nmero de parnteses direitos e esquerdos e no est balanceada, porque a chaveta fechada antes do parntese recto. { A + B * [ C / (F+A) + (C+D) * (EF) } ] / [ (A+B) * C ] Uma vez que necessrio assegurar que cada parntese direito equivalente ao ltimo parntese esquerdo, esta anlise pode ser feita usando uma pilha O algoritmo o seguinte. Sempre que aparece um parntese esquerdo na expresso, este colocado na pilha. Quando aparece um parntese direito, tira-se da pilha o ltimo parntese esquerdo l colocado e verifica-se se so equivalentes. Caso o teste seja positivo ento este nvel interno de parnteses est balanceado e ambos os parnteses so descartados. Caso os parnteses no sejam equivalentes, ento o processo interrompido porque este nvel interno de parnteses no est balanceado. Se por acaso no existir um parntese esquerdo na pilha, ento sinal que existem mais parnteses direitos que esquerdos at esta posio da expresso, pelo que, a expresso tambm no est balanceada. Quando a anlise da expresso terminar preciso verificar se a pilha est vazia. Porque, caso no esteja vazia, ento sinal que existem mais parnteses esquerdos que direitos na expresso, pelo que, a expresso tambm no est balanceada. A Figura 7.34 apresenta o programa que implementa este algoritmo. De maneira a estruturar melhor o programa, foi implementada uma funo inteira que compara o parntese esquerdo com o parntese direito e que devolve 1, caso eles sejam equivalentes e 0 no caso contrrio. Neste caso a pilha utilizada a implementao esttica, mas podia ser qualquer outra implementao. Para utilizar a pilha, primeiro preciso editar o ficheiro de interface elemento.h, e definir o nmero de elementos da estrutura de dados da pilha com um valor suficiente para armazenar os parnteses da expresso, que no pior caso podem ser tantos quantos o nmero de caracteres da expresso e definir tambm o tipo dos elementos da pilha como sendo do tipo char. Depois o mdulo compilado, com a opo c, para ficar concretizado para uma pilha de caracteres. Finalmente, o programa compilado, indicando no comando de compilao o ficheiro objecto do mdulo pilha_est.o. A Figura 7.33 apresenta o resultado de execuo do programa para vrias expresses.
Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)]}/[(A+B)*C] Expresso com parnteses balanceados Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)}]/[(A+B)*C] Parnteses [ e } discordantes Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)]}/[(A+B)*C]} Mais parnteses direitos do que esquerdos Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)]}/[(A+B)*C Mais parnteses esquerdos do que direitos

Figura 7.33 - Exemplos de utilizao do programa.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

24

#include <stdio.h> #include <stdlib.h> #include <string.h> #include "pilha_est.h" /* ficheiro de interface do mdulo da pilha */ int parenteses_equivalentes (char, char); int main (void) { char exp[81], cpilha; int nc, c, st;

/* expresso lida do teclado para analisar /* parntese esquerdo armazenado na pilha /* nmero de smbolos da expresso /* contador do ciclo for /* estado de realizao da operao da pilha

*/ */ */ */ */

printf ("Expresso algbrica -> "); scanf ("%80s", exp); nc = strlen (exp); for (c = 0; c < nc; c++) if ( exp[c] == '(' || exp[c] == '[' || exp[c] == '{' ) Stack_Push (exp[c]); else if ( exp[c] == ')' || exp[c] == ']' || exp[c] == '}' ) { st = Stack_Pop (&cpilha); if (st == STACK_EMPTY) { printf ("Mais parnteses direitos do que esquerdos\n"); return EXIT_SUCCESS; } else if ( !parenteses_equivalentes (cpilha, exp[c]) ) { printf ("Parnteses %c e %c discordantes\n",\ cpilha, exp[c]); return EXIT_SUCCESS; } } st = Stack_Pop (&cpilha); if (st == STACK_EMPTY) printf ("Expresso com parnteses balanceados\n"); else printf ("Mais parnteses esquerdos do que direitos\n"); return EXIT_SUCCESS; } int parenteses_equivalentes (char pesquerdo, char pdireito) { switch (pesquerdo) { case '(' : return pdireito == ')'; break; case '[' : return pdireito == ']'; break; case '{' : return pdireito == '}'; break; default : return 0; } }

Figura 7.34 - Programa que verifica o balanceamento dos parnteses numa expresso.

25

CAPTULO 7 : FILAS E PILHAS

Pretende-se determinar se uma palavra um palndromo, que uma palavra que se l da mesma maneira da esquerda para a direita e da direita para esquerda. Ou seja, aquilo que normalmente se designa por capicua. Para que uma palavra seja uma capicua, os seus caracteres esto reflectidos em relao ao carcter central. Uma forma de detectar esta caracterstica, consiste em comparar cada carcter com o seu simtrico. Por outras palavras, se a palavra for comparada com a palavra invertida, carcter a carcter, temos que os caracteres so coincidentes. Podemos fazer esta deteco utilizando uma fila e uma pilha conjuntamente. O algoritmo o seguinte. Coloca-se todos os caracteres da palavra na fila e na pilha. Depois retira-se o carcter da cabea da fila e o carcter do topo da pilha e comparam-se. Desta maneira, compara-se o primeiro carcter da palavra, ou seja, o carcter que se encontra na fila com o ltimo carcter da palavra, ou seja, o carcter que se encontra na pilha. Se os caracteres forem iguais at se esgotar os caracteres da fila e da pilha, ento a palavra uma capicua. Caso contrrio, a palavra no uma capicua. A Figura 7.35 apresenta o programa que implementa este algoritmo. A pilha e a fila utilizadas so a implementao esttica. Para utilizar a fila e a pilha, primeiro preciso editar o ficheiro de interface elemento.h, e definir o nmero de elementos das estruturas de dados da fila e da pilha, com um valor suficiente para armazenar o nmero de caracteres da palavra e definir tambm o tipo dos elementos da fila e da pilha como sendo do tipo char. Depois os mdulos so compilados, com a opo c, para ficarem concretizados para uma fila e uma pilha de caracteres. Finalmente, o programa compilado, indicando no comando de compilao os ficheiros objectos dos mdulos fila_est.o e pilha_est.o.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "fila_est.h" /* ficheiro de interface do mdulo da fila */ #include "pilha_est.h" /* ficheiro de interface do mdulo da pilha */ int main (void) { char palavra[81], cfila, cpilha; int nc, c;

/* palavra lida do teclado */ /* carcter armazenado na fila */ /* carcter armazenado na pilha */

printf ("Palavra -> "); scanf ("%80s", palavra); nc = strlen (palavra); for (c = 0; c < nc; c++) { Fifo_In (palavra[c]); Stack_Push (palavra[c]); } for (c = 0; c < nc; c++) { Fifo_Out (&cfila); Stack_Pop (&cpilha); if (cfila != cpilha) { printf ("A palavra no uma capicua\n"); return EXIT_SUCCESS; } } printf ("A palavra uma capicua\n"); return EXIT_SUCCESS; }

Figura 7.35 - Programa que verifica se uma palavra uma capicua.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

26

7.5 Implementaes abstractas


Uma alternativa para poder reutilizar um mdulo de memria sem a necessidade de o recompilar sempre que o utilizador precisa de mudar o tipo dos elementos da memria, consiste na criao de um mdulo abstracto. Um mdulo abstracto pode ser concretizado durante a execuo do programa, permitindo assim a sua reutilizao para diferentes tipos de dados. Para que um mdulo de memria seja reutilizvel sem limitaes, ele deve tambm ter uma implementao dinmica para que a sua capacidade de armazenamento seja ilimitada. Por outro lado, para implementar certos algoritmos, por vezes existe a necessidade de utilizar mais do que uma memria do mesmo tipo, pelo que, o mdulo de memria tambm deve ter a capacidade de mltipla instanciao. Portanto, um mdulo de memria abstracto, com uma implementao dinmica e com capacidade de mltipla instanciao um mdulo muito verstil e com uma reutilizao praticamente ilimitada. Com a linguagem C possvel criar um mdulo de memria usando ponteiros de tipo void, uma vez que ele pode ser colocado a apontar para qualquer tipo de dados. Portanto, estamos perante um ponteiro genrico que possibilita a construo de um mdulo de memria, em que o seu elemento de armazenamento pode ser de qualquer tipo de dados. Aquando da utilizao do mdulo preciso concretiz-lo para o tipo de dados necessrio aplicao. Esta operao feita atravs de uma funo de criao do mdulo, que responsvel pela caracterizao do tamanho em bytes do elemento de armazenamento da memria. Para permitir manipular estruturas de dados, sem conhecimento do seu tipo concreto, a biblioteca de execuo ANSI string providencia a funo memcpy, que copia blocos de memria, sem atribuir qualquer interpretao ao contedo dos bytes copiados. A Figura 7.36 apresenta o ficheiro de interface da fila dinmica, abstracta e com capacidade de mltipla instanciao, cujo ficheiro de implementao se apresenta na Figura 7.37 e na Figura 7.38. Para que o mdulo possa criar e manipular mais do que uma fila, necessrio que exista uma referncia para cada fila criada de forma a identificar de forma inequvoca a fila onde se pretende colocar ou retirar elementos. Para isso preciso uma estrutura de suporte que possa armazenar os elementos que controlam a fila e que so os ponteiros para a cabea e para a cauda da fila e um indicador do tamanho em nmero de bytes do elemento da fila. Esta estrutura vai ser criada na memria na altura em que pedido a criao de uma fila e o seu endereo devolvido de forma a permitir o posterior acesso fila criada, quer para a colocao e remoo de elementos, quer para a destruio da fila, quando ela deixa de ser necessria. Pelo que, o ficheiro de interface define o tipo PtFifo, que um ponteiro para struct fifo, que permite ao utilizador do mdulo manipular as filas criadas, sem no entanto ter acesso estrutura de dados da fila. A funo de criao da fila Fifo_Create, cria a estrutura de suporte da fila, coloca os ponteiros para a cabea e a cauda da fila a apontar para NULL, ou seja, cria uma fila sem elementos, concretiza o tipo de elementos atravs da especificao do seu tamanho em bytes e devolve a referncia da estrutura de suporte da fila criada. Por sua vez, a funo de destruio da fila Fifo_Destroy, liberta toda a memria ocupada pelos elementos da fila e pela estrutura de suporte da fila e coloca a referncia da fila a NULL, de maneira a no ser mais possvel aceder fila.

27

CAPTULO 7 : FILAS E PILHAS

A funo de colocao de elementos na fila Fifo_In comea por assegurar que a fila existe e depois comporta-se tal como na implementao dinmica. Mas, a cpia do elemento para a fila feita pela funo memcpy indicando o ponteiro para o elemento a colocar na fila, o ponteiro para a cauda da fila e o nmero de bytes a copiar, que est armazenado no campo que especifica o tamanho dos elementos da fila. Desta maneira possvel manipular os elementos da fila sem que se saiba de que tipo eles so. A funo de remoo de elementos da fila Fifo_Out comea por assegurar que a fila existe e depois comporta-se tal como na implementao dinmica. Tal como na funo Fifo_In, a cpia do elemento da fila feita pela funo memcpy.
/****************** Interface da FILA Abstracta ******************/ /* Nome : fila_abs.h */ #ifndef _FILA_ABSTRACTA #define _FILA_ABSTRACTA typedef struct fifo *PtFifo; /* Definio de Constantes */ #define #define #define #define #define #define OK NULL_PTR NULL_SIZE NO_MEM FIFO_EMPTY NO_FIFO 0 1 2 3 4 7 /* /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ tamanho nulo */ memria esgotada */ fila vazia */ no foi instanciada qualquer fila */

/* Aluso s Funes Exportadas pelo Mdulo */ PtFifo Fifo_Create (unsigned int sz); /* Concretiza a fila para elementos de sz bytes. referncia da fila criada ou NULL em caso de erro. */ Devolve a

int Fifo_Destroy (PtFifo *fila); /* Destri a fila referenciada por fila e coloca a referncia a NULL. Valores de retorno: OK ou NO_FIFO. */ int Fifo_In (PtFifo fila, void *pelemento); /* Coloca o elemento apontado por pelemento na cauda da fila referenciada por fila. Valores de retorno: OK, NO_FIFO, NULL_PTR ou NO_MEM. */ int Fifo_Out (PtFifo fila, void *pelemento); /* Retira o elemento da cabea da fila referenciada por fila, para o elemento apontado por pelemento. Valores de retorno: OK, NO_FIFO, NULL_PTR ou FIFO_EMPTY. */ #endif

Figura 7.36 - Ficheiro de interface da fila abstracta com mltipla instanciao.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

28

/**************** Implementao da FILA Abstracta ****************/ /* Nome : fila_abs.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "fila_abs.h" typedef struct no *PtNo; struct no { void *pelemento; PtNo pseg; }; /* Ficheiro de interface do mdulo */ /* Definio da Estrutura de Dados Interna da FILA */

/* ponteiro para o elemento */ /* ponteiro para o n seguinte */

struct fifo { unsigned int size;/* tamanho em nmero de bytes de cada elemento */ PtNo head; /* cabea da fila */ PtNo tail; /* cauda da fila */ }; /* Definio das Funes */ PtFifo Fifo_Create (unsigned int sz) { PtFifo fifo; if (sz == 0) return NULL; if ((fifo = (PtFifo) malloc (sizeof (struct fifo))) == NULL) return NULL; fifo->size = sz; fifo->head = NULL; fifo->tail = NULL; return fifo; } int Fifo_Destroy (PtFifo *fila) { PtFifo fifo = *fila; PtNo tmp; if (fifo == NULL) return NO_FIFO; while ( fifo->head != NULL ) { tmp = fifo->head; fifo->head = fifo->head->pseg; free (tmp->pelemento); free (tmp); } free (fifo); *fila = NULL; return OK; }

Figura 7.37 - Ficheiro de implementao da fila abstracta com mltipla instanciao (1 parte).

29

CAPTULO 7 : FILAS E PILHAS

int Fifo_In (PtFifo fila, void *pelemento) { PtFifo fifo = fila; PtNo tmp; if (fifo == NULL) return NO_FIFO; if (pelemento == NULL) return NULL_PTR; if ((tmp = (PtNo) malloc (sizeof (struct no))) == NULL) return NO_MEM; if ((tmp->pelemento = (void *) malloc (fifo->size)) == NULL) { free (tmp); return NO_MEM; } tmp->pseg = NULL; if (fifo->tail == NULL) fifo->head = tmp; else fifo->tail->pseg = tmp; fifo->tail = tmp; memcpy (fifo->tail->pelemento, pelemento, fifo->size); return OK; } int Fifo_Out (PtFifo fila, void *pelemento) { PtFifo fifo = fila; PtNo tmp; if (fifo == NULL) return NO_FIFO; if (pelemento == NULL) return NULL_PTR; if (fifo->head == NULL) return FIFO_EMPTY; memcpy (pelemento, fifo->head->pelemento, fifo->size); tmp = fifo->head; fifo->head = fifo->head->pseg; if (fifo->head == NULL) fifo->tail = NULL; free (tmp->pelemento); free (tmp); return OK; }

Figura 7.38 - Ficheiro de implementao da fila abstracta com mltipla instanciao (2 parte).

A Figura 7.39 apresenta o ficheiro de interface da pilha dinmica, abstracta e com capacidade de mltipla instanciao, cujo ficheiro de implementao se apresentam na Figura 7.40 e na Figura 7.41. Para que o mdulo possa criar e manipular mais do que uma pila, necessrio que exista uma referncia para cada pilha criada de forma a identificar de forma inequvoca a pilha onde se pretende colocar ou retirar elementos. Para isso preciso uma estrutura de suporte que possa armazenar os elementos que controlam a pilha e que so o ponteiro para o topo da pilha e um indicador do tamanho em nmero de bytes do elemento da pilha. Esta estrutura vai ser criada na memria na altura em que pedido a criao de uma pilha e o seu endereo devolvido de forma a permitir o posterior acesso pilha criada, quer para a colocao e remoo de elementos, quer para a destruio da pilha, quando ela deixa de ser necessria. Pelo que, o ficheiro de interface define o tipo PtStack, que um ponteiro para struct stack, que permite ao utilizador do mdulo manipular as filas criadas, sem no entanto ter acesso estrutura de suporte da fila.

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

30

A funo de criao da pilha Stack_Create, cria a estrutura de suporte da pilha, coloca o ponteiro para o topo da pilha a apontar para NULL, ou seja, cria uma pilha sem elementos, concretiza o tipo de elementos atravs da especificao do seu tamanho em bytes e devolve a referncia da estrutura de suporte da pilha criada. Por sua vez, a funo de destruio da pilha Stack_Destroy, liberta toda a memria ocupada pelos elementos da pilha e pela estrutura de suporte da pilha e coloca a referncia da pilha a NULL, de maneira a no ser mais possvel aceder pilha. A funo de colocao de elementos na pilha Stack_Push comea por assegurar que a pilha existe e depois comporta-se tal como na implementao dinmica. Mas, a cpia do elemento para a pilha feita pela funo memcpy indicando o ponteiro para o elemento a colocar na pilha, o ponteiro para o topo da pilha e o nmero de bytes a copiar, que est armazenado no campo que especifica o tamanho dos elementos da pilha. Desta maneira possvel manipular os elementos da pilha sem que se saiba de que tipo eles so. A funo de remoo de elementos da pilha Stack_Pop comea por assegurar que a pilha existe e depois comporta-se tal como na implementao dinmica. Tal como na funo Stack_Push, a cpia do elemento da pilha feita pela funo memcpy.
/****************** Interface da PILHA Abstracta ******************/ /* Nome : pilha_abs.h */ #ifndef _PILHA_ABSTRACTA #define _PILHA_ABSTRACTA typedef struct stack *PtStack; /* Definio de Constantes */ #define #define #define #define #define #define OK NULL_PTR NULL_SIZE NO_MEM STACK_EMPTY NO_STACK 0 1 2 3 4 7 /* /* /* /* /* /* operao realizada com sucesso */ ponteiro nulo */ tamanho nulo */ memria esgotada */ pilha vazia */ no foi instanciada qualquer pilha */

/* Aluso s Funes Exportadas pelo Mdulo */ PtStack Stack_Create (unsigned int sz); /* Concretiza a pilha para elementos de sz bytes. referncia da pilha criada ou NULL em caso de erro. */ Devolve a

int Stack_Destroy (PtStack *pilha); /* Destri a pilha referenciada por pilha e coloca a referncia a NULL. Valores de retorno: OK ou NO_STACK. */ int Stack_Push (PtStack pilha, void *pelemento); /* Coloca o elemento apontado por pelemento no topo da pilha referenciada por pilha. Valores de retorno: OK, NO_STACK, NULL_PTR ou NO_MEM. */ int Stack_Pop (PtStack pilha, void *pelemento); /* Retira o elemento do topo da pilha referenciada por pilha, para o elemento apontado por pelemento. Valores de retorno: OK, NO_STACK, NULL_PTR ou STACK_EMPTY. */ #endif

Figura 7.39 - Ficheiro de interface da pilha abstracta com mltipla instanciao.

31

CAPTULO 7 : FILAS E PILHAS

/**************** Implementao da PILHA Abstracta ****************/ /* Nome : pilha_abs.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "pilha_abs.h" typedef struct no *PtNo; struct no { void *pelemento; PtNo pant; }; /* Ficheiro de interface do mdulo */ /* Definio da Estrutura de Dados Interna da PILHA */

/* ponteiro para o elemento */ /* ponteiro para o n anterior */

struct stack { unsigned int size;/* tamanho em nmero de bytes de cada elemento */ PtNo top; /* topo da pilha */ }; /* Definio das Funes */ PtStack Stack_Create (unsigned int sz) { PtStack stack; if (sz == 0) return NULL; if ((stack = (PtStack) malloc (sizeof (struct stack))) == NULL) return NULL; stack->size = sz; stack->top = NULL; return stack; } int Stack_Destroy (PtStack *pilha) { PtStack stack = *pilha; PtNo tmp; if (stack == NULL) return NO_STACK; while (stack->top != NULL) { tmp = stack->top; stack->top = stack->top->pant; free (tmp->pelemento); free (tmp); } free (stack); *pilha = NULL; return OK; }

Figura 7.40 - Ficheiro de implementao da pilha abstracta com mltipla instanciao (1 parte).

PROGRAMAO ESTRUTURAS DE DADOS E ALGORITMOS EM C

32

int Stack_Push (PtStack pilha, void *pelemento) { PtStack stack = pilha; PtNo tmp; if (stack == NULL) return NO_STACK; if (pelemento == NULL) return NULL_PTR; if ((tmp = (PtNo) malloc (sizeof (struct no))) == NULL) return NO_MEM; if ((tmp->pelemento = (void *) malloc (stack->size)) == NULL) { free (tmp); return NO_MEM; } tmp->pant = stack->top; stack->top = tmp; memcpy (stack->top->pelemento, pelemento, stack->size); return OK; } int Stack_Pop (PtStack pilha, void *pelemento) { PtStack stack = pilha; PtNo tmp; if (stack == NULL) return NO_STACK; if (pelemento == NULL) return NULL_PTR; if (stack->top == NULL) return STACK_EMPTY; memcpy (pelemento, stack->top->pelemento, stack->size); tmp = stack->top; stack->top = stack->top->pant; free (tmp->pelemento); free (tmp); return OK; }

Figura 7.41 - Ficheiro de implementao da pilha abstracta com mltipla instanciao (2 parte).

A Figura 7.42 apresenta a utilizao da pilha abstracta para a resoluo do problema da dupla inverso de uma linha de texto. um exemplo meramente acadmico, uma vez que para inverter uma cadeia de caracteres no necessrio uma pilha, mas serve para mostrar a utilizao de duas pilhas em simultneo no mesmo programa. Os caracteres so colocados numa pilha e depois ao serem retirados so escritos no monitor por ordem inversa. Mas, se forem de novo colocados noutra pilha, depois ao serem retirados da segunda pilha so escritos no monitor pela ordem inicial. Temos assim uma dupla inverso. Para podermos usar duas pilhas, primeiro preciso declarar duas variveis de tipo PtStack, que vo apontar para as pilhas criadas atravs da invocao da funo Stack_Create. Na invocao desta funo indicado que os elementos da pilha devem ter o tamanho de um char, pelo que estamos a criar pilhas para caracteres. Para colocar e retirar caracteres numa pilha obrigatrio passar s funes Stack_Push e Stack_Pop, como parmetro de entrada, a pilha que se est a processar usando o respectivo ponteiro. Assim que as pilhas no so mais precisas, elas devem ser destrudas invocando a funo Stack_Destroy para cada uma das pilhas. Para simplificar o programa, o resultado de sada das operaes de colocao e remoo de caracteres nas pilhas no testado para conferir se so bem sucedidas ou no.

33

CAPTULO 7 : FILAS E PILHAS

#include <stdio.h> #include <stdlib.h> #include <string.h> #include "pilha_abs.h" /* ficheiro de interface do mdulo da pilha */ int main (void) { char exp[81], cpilha; int nc, c, st; PtStack pilha1 = NULL, pilha2 = NULL; pilha1 = Stack_Create (sizeof (char)); pilha2 = Stack_Create (sizeof (char)); printf ("Texto de entrada -> "); nc = strlen (exp); /* Criao da pilha 1 */ /* Criao da pilha 2 */

scanf ("%80s", exp);

for (c = 0; c < nc; c++) st = Stack_Push (pilha1, &exp[c]); /* Colocar texto na pilha 1 */ printf ("Texto de sada depois de colocado na pilha 1 -> "); for (c = 0; c < nc; c++) { st = Stack_Pop (pilha1, &cpilha); /* Retirar texto da pilha 1 */ printf ("%c", cpilha); st = Stack_Push (pilha2, &cpilha); /* Colocar texto na pilha 2 */ } printf ("\n"); printf ("Texto de sada depois de colocado na pilha 2 -> "); for (c = 0; c < nc; c++) { st = Stack_Pop (pilha2, &cpilha); /* Retirar texto da pilha 2 */ printf ("%c", cpilha); } printf ("\n"); Stack_Destroy (&pilha1); Stack_Destroy (&pilha2); return EXIT_SUCCESS; } /* Destruio da pilha 1 */ /* Destruio da pilha 2 */

Figura 7.42 - Exemplo de utilizao da pilha abstracta com mltipla instanciao.

7.6 Leituras recomendadas


x 7 captulo do livro Data Structures, Algorithms and Software Principles in C, de Thomas A. Standish, da editora Addison-Wesley Publishing Company, 1995.

Você também pode gostar