Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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).
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
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.
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 {
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; }
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; }
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.
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.
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
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.
CAPTULO 1 : INTRODUO AO C
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.
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'
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
-2.5
8 bytes
8 bytes
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 )
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 */
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
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*/
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 char B;
/* A = 102510 = 0000 0000 0000 0000 0000 0100 0000 00012 */ /* B = 0000 00012 = 110*/
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*/
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.
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
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 */
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.
18
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
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
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
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);
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) */
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 */
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)
else ABSX = X; */
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.
22
int X, Y, Z; ... X = Y = Z; X += Y -= Z;
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; }
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");
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++;
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 ;.
24
if {
(V1 > 0)
if (V2 > 0) V2++; } else V1++; if (V1 > 0) if (V2 > 0) V2++; else ; else V1++;
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.
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 }
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.
26
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; }
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 );
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.
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).
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.
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); }
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'; }
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.
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.
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.
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.
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')); ... }
*/ */
*/ */ */
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.
Figura 1.57 - Exemplo da leitura de uma cadeia de caracteres com o especificador de converso %s.
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]); */
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.
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);
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);
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 ... }
*/ */
*/ */ */ */ */
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.
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
*/ */ */ */
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.
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 */ */ */ */ */ */ */
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 <*/ */ */ */ */ */ */
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.<-
*/ */ */ */ */ */ */
46
47
CAPTULO 1 : INTRODUO AO 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 ..."); ... } ...
49
CAPTULO 1 : INTRODUO AO C
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
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.
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.
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.
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.
Funo
Resultado de Sada
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.
Funo Generalizada
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; }
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.
tipo de sada
nome da funo
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.
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
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
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; } ...
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 */
tipo base
Agregado bidimensional com 15 elementos do tipo base, com o elemento da 4 coluna da 3 linha marcado.
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.
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.
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.
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.
10
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
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.
12
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
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'
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.
14
15
y x w
b c d
alfabeto minsculo
...
Figura 2.24 - Deslocamento circular de um carcter trs posies para a frente.
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; }
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
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);
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" */
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 );
*/ */ */ */
19
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
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, "$#"); }
21
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 */ }
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.
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 */
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); */
23
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 ];
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
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
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
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.
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 */
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 */
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
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
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.
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.
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
*/ */ */ */
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 */
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
*/ */ */ */ */
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
29
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 */
30
menor
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]
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
TIPO_BASE (*PA)[4];
PA
B[1]
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.
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
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];
34
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; };
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;
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
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;
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;
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 };
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 ();
... /* 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; */
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.
37
da
estrutura
TDADOS_PESSOA,
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} };
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.
38
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.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
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.
Figura 2.76 - Exemplo de uma funo que processa um agregado de estruturas (1 verso).
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; }
41
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;
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;
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.
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");
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
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 */
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;
44
/* Varivel Local */
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
static int I, J; int main (void) { int I; ... FUNC (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 */ ... }
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; ... }
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.
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.
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.
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.
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+ * * * * * * * * *
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 ...
*/ */ */ */
*/ */
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.
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 */ ...
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 */
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.
#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 */
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 */
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.
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; }
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 */
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];
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.
14
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);
15
CAPTULO 3 : FICHEIROS EM C
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.
16
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.
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
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
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 */
/* 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 */
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 */ }
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.
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); } }
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
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.
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
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
*/
*/ */
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.
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); }
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, ®AUX); if ( strcmp (REG.NOME, REGAUX.NOME) < 0 ) break; R++; } while (R < NR); return R; }
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, ®AUX); if ( !strcmp (NTEL, REGAUX.NTELEF) ) break; R++; } while (R < NR); return R; }
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, ®AUX); ESCREVER_REGISTO_FICHEIRO (FP, R, REGAUX); } }
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, ®AUX); ESCREVER_REGISTO_FICHEIRO (FP, R, REGAUX); } }
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
NR NR+1
P
4 3 2
NR-1
1
NR
1 2 3 4
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); } }
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); } }
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.
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
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 */ }
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.
FACTORIAL(3) = 3 * FACTORIAL(2) 3 * 2
FACTORIAL(2) = 2 * FACTORIAL(1) 2 * 1
FACTORIAL(1) = 1 * FACTORIAL(0) 1 * 1
FACTORIAL(0) = 1
1n 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.
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.
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 */ }
FIBONACCI(1) FIB(1) = 1
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
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]; }
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; }
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.
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
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); }
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]; }
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.
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 } } */ */ */ */ */ */
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;
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) ...
11
CAPTULO 4 : RECURSIVIDADE
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.
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 -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 -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
Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
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.
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
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;
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); } }
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 ---------------------------------
17
CAPTULO 4 : RECURSIVIDADE
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 )
1n u x 2n n 0
N 1
2n
coseno( x,5 )
1
x2 x4 x6 x8 ... 2! 4! 6! 8!
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.
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)
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.
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.
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.
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.
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;
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
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.
/******************** 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) { ... } ...
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 *******/ ...
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
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
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 */
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; }
11
CAPTULO 5 : MEMRIAS
/* Mquina de calcular de nmeros complexos */ #include <stdio.h> #include "complexo.h" int main (void) { PtComplexo Comp1, Comp2, Resultado;
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; }
12
/********************* 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
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.
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
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
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
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.
18
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]
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.
FIFO[0]
FIFO[1]
FIFO[2]
...
FIFO[I]
...
FIFO[N-1]
FILA esttica
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.
20
cabea da fila
cauda da fila
FIFO[0]
FIFO[1]
FIFO[2]
...
FIFO[I]
...
FIFO[N-1]
FILA semiesttica
FIFO_Out
FIFO_In
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
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
STACK_Push STACK_Pop
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.
22
STACK_Push STACK_Pop
PtEle PtAnt
Elemento
PtEle PtAnt
Elemento
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.
23
CAPTULO 5 : MEMRIAS
CAM[0]
CHAVE 1
CAM[1]
CHAVE 2
CAM[2]
CHAVE 4
CAM[3]
CHAVE 6
...
CAM[N-1]
ltimo elemento
CAM esttica
CHAVE 1
CHAVE 2
CHAVE 4
CHAVE 6
CAM semiesttica
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.
24
cabea da CAM
CHAVE 1
CHAVE 2
CHAVE 4
CHAVE 6
Elemento
Elemento
pesquisa
Elemento
sequencial
Elemento
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
PtSeg PtEle
CHAVE 2
CAM semiesttica/dinmica
Elemento
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.
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.
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.
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 */
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.
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
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.
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.4 - Funo que determina o elemento do agregado com o primeiro valor que serve.
Figura 6.5 - Funo que determina o elemento do agregado com o melhor valor que serve.
Figura 6.6 - Funo que determina o elemento do agregado com o pior valor que serve.
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
Primeiro Valor<=40
Pior Valor<=40
Melhor Valor<=40
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
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
MIN
MED
MAX
3
0
7
1
MIN MED
MAX
3
0
7
1
MIN MAX
3
0
7
1
MED
MAX MIN
3
0
7
1
MED
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.
10
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
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; }
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
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
13
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 */
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).
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
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
15
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.
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
17
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); }
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
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
15
25
32
42
55
145
209
330
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
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 */ } }
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
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); }
21
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
330
25
15 I INICIAL
330
15
25
UT
15
25
UT
32 I
FINAL 2 8 15 25
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).
22
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
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
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
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.
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
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.
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
27
/* 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++];
/* 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 */
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).
29
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
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
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); }
31
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
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
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;
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
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
35
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]); }
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.
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 */
37
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.
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); }
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.
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.
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.
/*********** 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 *************/
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 ).
/******************* 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
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]
cauda da fila
estado inicial
cabea da fila
FIFO[0]
...
FIFO[N-1]
cabea da fila
cauda da fila
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]
cabea da fila
cauda da fila
colocao de um elemento
FIFO[0]
...
FIFO[N-1]
cabea da fila
cauda da 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]
colocao de um elemento
cabea da fila
cauda da fila
FIFO[0]
...
FIFO[N-1]
cauda da fila
cabea da fila
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]
cauda da fila
cabea da fila
FIFO[0]
...
FIFO[N-1]
FILA CHEIA
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]
cauda da fila
cabea da fila
remoo de um elemento
FIFO[0]
...
FIFO[N-1]
cauda da fila
cabea da fila
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]
remoo de um elemento
cauda da fila
cabea da fila
FIFO[0]
...
FIFO[N-1]
cabea da fila
cauda da fila
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]
cabea da fila
cauda da fila
FIFO[0]
...
FIFO[N-1]
FILA VAZIA
cabea da fila
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; }
/***************** 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
/* 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; }
10
/* 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
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
cauda da fila
estado inicial
PtSeg PtEle
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 2
colocao de um elemento
PtSeg PtEle
PtSeg PtEle
PtSeg PtEle
Elemento 2
Elemento 3
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.
12
cabea da fila
PtSeg PtEle
PtSeg PtEle
PtSeg PtEle
Elemento 2
Elemento 3
PtSeg PtEle
PtSeg PtEle
remoo de um elemento
Elemento 3
PtSeg PtEle
cabea da fila
cauda da fila
FILA VAZIA
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
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 */
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).
/* 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
15
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
. . .
STACK[1]
STACK[1]
Elemento
STACK[0]
Elemento
STACK[0]
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
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
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
. . .
STACK[1]
Elemento Elemento
STACK[1]
Elemento
STACK[0]
STACK[0]
Elemento
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
STACK[1]
Elemento
STACK[1]
Elemento Elemento
STACK[0]
Elemento
STACK[0]
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
/**************** 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; }
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
/* 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; }
19
/* 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
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.
20
estado inicial
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
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
21
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
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.
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; }
23
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
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
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;
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; }
26
27
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
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 */
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
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.
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
31
/**************** 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 */
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).
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
#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 */
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 */