Exercícios de Programação II e respectivas resoluções


Observações

Note-se que:

  1. As soluções apresentadas não são as únicas, e possivelmente também não as "melhores".
  2. Estas soluções por vezes incluem algumas características do C que não tinham sido ainda apresentadas nas aulas teóricas correspondentes. No entanto, uma vez que as soluções são apresentadas como elemento de estudo, julgámos ser vantajoso apresentar código C tão completo quanto possível.
  3. Quando as soluções são semelhantes entre si, as versões vão sendo sucessivamente menos comentadas.
  4. Ao longo do texto usam-se sempre as expressões "abrir ficheiro", "fechar ficheiro", etc., que devem, em rigor, ser entendidas como "abrir canal de acesso ao ficheiro", etc.
  5. Note que as funções scanf() e fscanf() devolvem o número de argumentos lidos.

Exercício 1

Alínea a)

Mostrar no terminal o conteúdo de um ficheiro de texto.

Resolução

O programa deve, ao ser executado, mostrar no ecrã o conteúdo dum ficheiro de texto já existente algures no disco.

Alínea b)

Armazenar em ficheiro um texto lido do terminal.

Resolução

Entende-se por "texto" o que quer que seja escrito no teclado pelo utilizador do programa até que seja premida a combinação de teclas equivalente a um fim de ficheiro, que no MS-DOS é ctrl-z e em Unix é ctrl-d.

Alínea c)

Fazer uma cópia dum ficheiro de texto.

Resolução

Alínea d)

Concatenar dois ficheiros de texto.

Resolução

Neste caso optou-se por escrever o resultado da concatenação no ecrã, mas as alterações necessárias para que o resultado da concatenação seja escrito num terceiro ficheiro são triviais.

Alínea e)

Comparar dois ficheiros de texto linha a linha e mostrar a primeira linha onde eles diferem.

Resolução

Alínea f)

Contar os números de caracteres, palavras e linhas de um ficheiro de texto.

Resolução

São consideradas palavras sequências contíguas de caracteres que não contenham qualquer caractere "branco" (por exemplo ' ', tabulador '\t' ou fim-de-linha '\n').

Alínea g)

Mostrar as linhas de um ficheiro de texto onde ocorre uma dada sequência de caracteres.

Resolução

Alínea h)

Gerar um ficheiro de texto contendo uma pauta de resultados de avaliações lida do terminal. O ficheiro conterá um registo de aluno por linha. Cada registo é composto pelos seguintes campos (separados por um caractere espaço): número de aluno (4 posições), nome (25 posições), notas do teste e do trabalho (2 posições cada).

Resolução

A concretização deste programa é trivial, requerendo apenas algum conhecimento das especificações de conversão da função printf().

Alínea i)

Mostrar no terminal o conteúdo de ficheiros de texto gerados pelo programa da alínea anterior.

Resolução

A concretização deste programa é trivial, requerendo apenas algum conhecimento das especificações de conversão da função fscanf().

Alínea j)

Ler um conjunto de valores para uma matriz bidimensional de inteiros com dimensão 4x4. Escrever o conteúdo da matriz de duas formas diferentes: ficheiro de texto e ficheiro binário. Comparar o tamanho dos ficheiros obtidos.

Resolução

Alínea k)

Ler para matrizes os ficheiros do exercício 1.j) e afixar no ecrã o seu conteúdo.

Resolução


Exercício 2

Faça um programa para gerir uma base de dados associada aos utentes de um parque de estacionamento universitário. Admite-se um máximo de 50 utentes armazenados sob a forma de uma matriz. Cada utente é descrito pelos seguintes campos:

O programa deve permitir as seguintes operações, a cada uma das quais corresponde um função:

  1. Inserção de novos utentes: a função deve ter como único argumento a matriz de utentes. Os dados do utente são introduzidos pelo teclado. O utente será colocado na primeira posição livre da matriz.
  2. Remoção de utentes: a função tem como único argumento a matriz de utentes. Deve perguntar o nome do utente a remover e retirá-lo da matriz (marcando a posição como livre), caso ele exista.
  3. Armazenamento da base de dados num ficheiro de texto: a função tem como argumentos a matriz de utentes e o nome do ficheiro. Não se esqueça que o ficheiro de texto deve ser legível pela função do pr'oximo item.
  4. Leitura da base de dados de um ficheiro de texto: a função tem como argumentos a matriz de utentes e o nome do ficheiro. Deve ler o ficheiro e guardar os utentes na matriz. A leitura termina quando o número máximo de utentes for atingido ou quando o fim do ficheiro for atingido. Todos os utentes existentes anteriormente na matriz serão eliminados. Não esquecer que as posições que não forem ocupadas por utentes lidos do ficheiro devem ser marcadas como livres.
  5. Mostrar no ecrã o conteúdo das posições ocupadas da matriz: a função tem como único argumento a matriz de utentes.

A estrutura do programa principal, que faz uso das funções acima, fica ao critério do aluno, e deverá ser preparada antes das funções. Este assunto, tal como o resto do enunciado, deve ser pensado com antecedência.

Resolução


Exercício 3

Faça um programa que leia um ficheiro de utentes de um parque de estacionamento, contendo, em cada linha, o código, a categoria e o nome de cada utente, por esta ordem. Estes dados devem ser carregados numa matriz de estruturas. O programa deve escrever num ficheiro a base de dados de utentes ordenada por nome ou por código. A execução do programa é feita com passagem de três argumentos (usando argc e argv de main()):

programa entrada saida campo

em que campo indica o campo pelo qual a ordenação é feita, podendo tomar os valores nome ou codigo.

Durante a ordenação a matriz de utentes permanece inalterada, para o que se deve utilizar uma matriz adicional de ponteiros para a estrutura de utente (inspire-se no exemplo 13P13 da página 246 do livro). Exemplo 1:

ordena entrada.txt saida.txt codigo

Este comando lê o ficheiro entrada.txt, carrega-o para a matriz de utentes, ordena-a pelo código (indirectamente, não esquecer), e escreve o resultado no ficheiro saida.txt.

Exemplo 2:

ordena entrada.txt saida.txt nome

Este comando lê o ficheiro entrada.txt, carrega-o para a matriz de utentes, ordena-a pelo nome do utente, e escreve o resultado no ficheiro saida.txt.

Resolução


Exercício 4

Listas e recursividade:

Alínea a)

Refaça o programa do exercício 2 de modo a usar o conceito de lista simplesmente ligada. O programa deve, portanto, permitir:

  1. leitura de ficheiro
  2. escrita de ficheiro
  3. inserção de utentes
  4. remoção de utentes
  5. listagem dos utentes

Alínea b)

Escreva duas funções que gerem a sucessão de Fibonacci até um determinado um determinado valor de n: F(0) = F(1) = 1 e, para n > 1, F(n) = F(n-1) + F(n-2). A primeira função deverá ser recursiva e a segunda iterativa. Compare a velocidade das duas funções. Veja o que sucede quando o valor de n é muito grande.

Resolução

Alínea c)

Refaça o programa da alínea a) de modo a que as listas usadas sejam totalmente genéricas, não dependendo, portanto, dos dados a guardar. A estrutura de elemento da lista deverá conter um ponteiro generico (void *) para os dados a guardar. Deve também conter um campo que guardará uma chave de acesso aos dados (use uma chave inteira). As funções associadas às listas genéricas poderão, por exemplo, incluir:

Lista *Lcria()
cria lista vazia.
void *Linsere(Lista *l, void *dados, int chave)
insere dados na lista, devolvendo-o ou devolvendo NULL em caso de erro.
void *Lremove(Lista *l, int chave)
remove da lista os dados indexados por chave, devolvendo-os ou devolvendo NULL caso não existam ou em caso de erro.
void *Lbusca(Lista *l, int chave)
devolve os dados indexados chave, ou devolve NULL caso não existam ou em caso de erro.
void Ldestroi(Lista *l, int tudo)
desafecta toda a memória da lista; se tudo for verdadeira, liberta também a memória afectada aos dados.
void *LbuscaFunc(Lista *l, PFI função, void *oque)
procura dados contendo oque, usando uma função passada como argumento (a função devolve verdadeiro quando encontrou os dados). Assume-se que:
typedef int (*PFI)(void *dados, void *oque);

por exemplo:

int buscaNome(void *dadosgen, void *nomegen)
{
    Utente *dados = dadosgen;
    char *nome = nomegen;

    return strcmp(dados->nome, nome) == 0;
}

chamando-se a função, por exemplo, da seguinte forma

dados = LbuscaFunc(lista, buscaNome, "Zacarias Zagalo");

Notas: não esquecer, nas alíneas a) e c), que a memória afectada deve ser desafectada quando deixa de ser necessária.


Exercício 5

Introdução

Sumário

Apresentam-se de seguida algumas notas sobre a linguagem C que não dispensam a leitura da bibliografia da cadeira, nomedamente do livro "The C Programming Language" (segunda edição), B. Kernighan e D. Ritchie, Prentice Hall, 1988.

Repare-se que a nomenclatura usada nestas notas não corresponde exactamente à usada, por exemplo, no livro referido acima. Os nomes aqui usados (e.g., local vs. global, interno vs. externo) foram escolhidos de modo a fazerem algum sentido em conjunto com os qualificadores do ANSI-C (e.g., extern).

Declaração vs. definição

A linguagem C diferencia os conceitos de declaração e de definição. Uma declaração informa o compilador da existência de algo (uma declaração de x é entendida pelo compilador como "existe x"). Uma definição, para além de declarar a existência, diz ao compilador em que é que consiste o objecto definido (que instruções compõem uma função, que campos compõem uma estrutura) e reserva memória para o objecto (no caso de variáveis).

Por declaração no sentido lato entende-se normalmente qualquer declaração, com ou sem definição. Por declaração no sentido estrito entende-se uma declaração sem definição. Uma definição é sempre também uma declaração.

O exemplo mais claro desta distinção encontra-se nas funções:

int func(int a);

que é equivalente a

int func(int);

é uma declaração no sentido estrito, pois não se especificam os pormenores de concretização da função, i.e., não se define a função. Já

int func(int a)
{
    return a/2;
}

é uma definição (e consequentemente também uma declaração) da função func().

No que diz respeito às variáveis, a distinção entre declaração e definição exige o recurso aos conceitos da próxima secção, nomeadamente aos conceitos de categoria e validade . Por exemplo, considerem-se os seguintes ficheiros:

/* Ficheiro a.c */
int i = 10;

e

/* Ficheiro b.c */
extern int i;

No ficheiro a.c define-se (e consequentemente declara-se) a variável (global e externa) i enquanto no ficheiro b.c se declara (no sentido estrito) uma variável i, que se supõe estar definida noutro ficheiro (neste caso seria provavelmente o ficheiro a.c...).

A mesma distinção entre declaração e definição se pode estabelecer para estruturas:

struct a;

é uma declaração, no sentido estrito, duma estrutura, enquanto que

struct a
{
    int b;
    ...
}

é uma definição duma estrutura.

Note-se que este último facto torna possível esconder totalmente, do utilizador de um determinado módulo de funções, os seus pormenores de realização. Suponha-se, por exemplo, um módulo de processamento de listas. Os respectivos ficheiros listas.h (ficheiro de cabeçalho, do inglês header file) e listas.c poderiam ser:

/* listas.h */
/*
 * Declaracao (sentido estrito) da estrutura struct ListaStr e 
 * definicao do tipo Lista!
 */
typedef struct ListaStr Lista;
/* Declaracoes, sentido estrito, de funcoes de interface... */
Lista *Lcria(void);
...

e

/* listas.c */
/*
 * A inclusao do ficheiro de cabecalho .h no ficheiro .c do 
 * proprio modulo  permite:
 * 1. Ter acesso `a definicao do tipo Lista.
 * 2. Garantir coerencia entre as declaracoes das funcoes no 
 *    ficheiro .h e a sua definicao no ficheiro .c.
 */
#include "listas.h"
/* Declaracao da estrutura ElementoStr e definicao do 
   tipo Elemento: */
typedef struct ElementoStr Elemento;
/* Definicao da estrutura ElementoStr: */
struct ElementoStr
{
    void *dados;
    Elemento *seguinte;
};
/* Definicao da estrutura struct ListaStr (declarada em 
   listas.h): */
struct ListaStr
{
    unsigned long numero;
    Elemento *inicio;
}
/* Definicao das funcoes: */
Lista *Lcria(void)
{
    ...
}
...

Categoria, âmbito, classe ou permanência e validade de objectos e nomes em C

Categoria de objectos

Em C existem duas categorias de objectos: objectos locais e objectos globais. São objectos locais aqueles que se definem dentro duma função. Os objectos definidos fora de qualquer função são objectos globais. 2

As funções, não se podendo nunca definir dentro de outras funções, são sempre globais. Quanto às variáveis, são locais as variáveis definidas dentro de funções e todos os parâmetros das funções. Por exemplo:

/* A funcao f() e' global: */
int f(int i)                /* O parametro i e' local. */
{
    int n;                  /* A variavel n e' local. */
    n = i*i;
    for(; n > 0; n--)
    {
        int aux = n;        /* A variavel aux e' local. */
	i += aux;
    }
    return n;
}
int n;                      /* A variavel n e' global. */
Âmbito dos nomes

Outro conceito importante diz respeito ao âmbito (ou skope em inglês) dos nomes declarados, i.e., em que zonas dum ficheiro em C são visíveis esses nomes. As regras são bastante simples:

  1. Os nomes declarados externamente às funções são visíveis desde o ponto de declaração até ao final do ficheiro.
  2. Os nomes dos parâmetros das funções são válidos ao longo de toda a função.
  3. Os nomes declarados dentro das funções são válidos desde a declaração até ao final do bloco em que foram declarados.
  4. Nomes declarados dentro de funções ocultam os mesmos nomes (desde que pertencentes ao mesmo espaço de nomenclatura) declarados fora das funções.
  5. Nomes declarados dentro de um bloco ocultam os mesmos nomes (desde que pertencentes ao mesmo espaço de nomenclatura) declarados em blocos que os envolvam.

Observe-se:

 1  int a;
 2  int f3(int);
 3  int f1(int b)
 4  {
 5      int c;
 6      int f2(int);
 7      for(c = 0; b > 0; b--)
 8      {
 9          static int d = 1;
10          c += f2(d);
11      }
12      return c;
13  }
14  int e;
15  int f2(int f)
16  {
17      return f3(f);
18  }
19  int f3(int g)
20  {
21      return 1;
22  }

Neste exemplo têm-se os seguintes nomes:

Classe ou permanência das variáveis

As variáveis em C têm diferentes tipos de permanência na memória consoante os qualificadores usados na sua definição e consoante sejam globais ou locais. As variáveis podem pertencer a uma de duas possíveis classes de permanência (ou storage class em inglês):

Automáticas
São variáveis locais (i.e., definidas dentro duma função) que têm existência (i.e., um espaço de memória afectado) apenas durante a execução da função em que foram definidas. O seu valor, como é óbvio, perde-se entre chamadas sucessivas à função.
Estáticas
São variáveis que têm existência desde o início até ao fim do programa.

As variáveis globais são sempre estáticas. As variáveis locais são automáticas por omissão e são estáticas caso a sua definição seja precedida do qualificador static.

As variáveis automáticas, quando não forem explicitamente inicializadas durante a sua definição, tomam valores indefinidos até à primeira atribuição que lhes seja feita.

As variáveis estáticas, quando não forem explicitamente inicializadas durante a definição, são inicializadas implicitamente com valores nulos (zeros). A inicialização (explícita ou implícita) de variáveis estáticas é feita apenas no início do programa.

Assim, no exemplo acima, apenas as variáveis a, d e e são estáticas.

Validade de funções e variáveis

De modo a facilitar a escrita de programas consistindo de diferentes módulos, existe na linguagem C o conceito de validade das funções e variáveis. As validades (ou linkage, em inglês) são:

Interna
Para variáveis e funções acessíveis apenas dentro do ficheiro onde são definidas.
Externa
Para variáveis e funções acessíveis em todo o programa (qualquer que seja o ficheiro).

As variáveis locais (ver definição acima) têm sempre validade apenas interna. As variáveis ou funções globais têm, por omissão, validade externa. No entanto, a sua validade pode ser interna desde que a sua definição seja precedida do qualificador static. 3

Para poder utilizar um objecto externo definido noutro ficheiro é necessário declará-lo com o qualificador extern. Este qualificador é obrigatório no caso das variáveis e opcional no caso das funções.

Considerem-se, por exemplo, os ficheiros: 4

/* Ficheiro a.c */
#include "a.h"
/* Definicao de variaveis globais: */
static int globalInterna = 2;
int globalExterna = 1;
/* Definicao de funcoes: */
static int funcaoInterna(int a)
{
    return globalExterna * globalInterna * a * a;
}
int funcaoExterna(int a)
{
    return funcaoInterna(a)-1;
}
/* Ficheiro a.h */
/* Declaracao de variaveis globais: */
extern int globalExterna;
/* Declaracao de funcoes: */
int funcaoExterna(int);

e

/* Ficheiro b.c */
#include <stdio.h>
#include <stdlib.h>
#include "a.h"
int main(void)
{
    globalExterna = -1;
    printf("funcaoExterna(3) = %d\n", funcaoExterna(3));
    return EXIT_SUCCESS;
}

Neste programa existem duas funções (para além de main()). A primeira, funcaoInterna() apenas pode ser utilizada dentro do ficheiro a.c, enquanto a segunda, funcaoExterna(), pode ser utilizada ao longo de todo o programa.

Relativamente às duas variáveis globais passa-se o mesmo: a primeira, globalInterna, só pode ser usada dentro de a.c enquanto a segunda, globalExterna, pode ser usada em todo o programa.

Como é óbvio, a utilização dos objectos externos fora do ficheiro onde são definidos implica a sua declaração, o que é conseguido, neste caso, por intermédio da inclusão do ficheiro de cabeçalho a.h. Note-se que se utilizou o qualificador extern apenas para a declaração das variáveis, tendo-se omitido, por desnecessário, esse qualificador na declaração das funções.

Finalmente, é de frisar neste ponto que, em geral, a utilização de variáveis globais é desaconselhável, por reduzir consideravelmente a legibilidade dos programas ao permitir que as funções afectem mais do que os seus argumentos. Os exemplos apresentados destinam-se apenas a demonstrar conceitos, e não devem ser tomados como exemplos de boa programação.

Alínea a)

Altere o programa produzido no exercício 4.b) de modo a que o número de chamadas da versão recursiva da função possa ser contabilizado. Para isso, utilize uma variável local à função (que terá de ser, obviamente, estática). Essa variável deve ser anulada sempre que se chamar a função com o argumento -1. O número de chamadas à função deverá ser devolvido pela função sempre que for chamada com o argumento -2.

Resolução

Alínea b)

Envolva todo o código relativo à contabilização de chamadas introduzido na alínea anterior em directivas do pré-processador de modo a ser compilado apenas se estiver definida a macro CONTABILIZA.

Recorra às seguintes directivas do pré-processador:

#define nome oque (em que oque é opcional)
define a macro nome, que é expandida para oque
#ifdef nome
o código entre esta directiva e a próxima directiva #else ou #endif é compilado apenas se a macro nome estiver definida.

Resolução

Alínea c)

Coloque as funções de cálculo da sucessão de Fibonacci num módulo separado, de nome sucessao.c, com o respectivo ficheiro de cabeçalho sucessao.h. Utilize nomes e tipos apropriados para as funções e seus parâmetros, por exemplo unsigned long SUCfibonacci(int n) e unsigned long SUCfibonacciRec(int n).

Resolução

Alínea d)

Tente calcular analiticamente o número de chamadas da função SUCfibonacciRec() quando lhe é passado o argumento n. Verifique que o número de chamadas pode ser expresso duma forma recursiva muito semelhante à da própria sucessão de Fibonacci! Escreva uma nova função, unsigned long SUCnfibbonaci(int n) (na sua forma iterativa!) que devolva o número de chamadas à função SUCfibbonaciRec() quando lhe é passado o argumento n.

Verifique que o valor calculado está correcto comparando-o com o valor contabilizado pela própria função SUCfibonacciRec().

Resolução

A dedução é simples. Seja a definição recursiva da sucessão: F(0) = F(1) = 1 e, para n > 1, F(n) = F(n-1) + F(n-2). Seja N(n) o número de chamadas à função de Fibonacci recursiva. Claramente N(0)=N(1)=1. Por outro lado, para calcular F(n) é necessário chamar a função uma vez, que por sua vez chamará F(n-1) e F(n-2). Então, a função N() define-se recursivamente, tal como F()! Em particular, N(0) = N(1) = 1 e, para n > 1, N(n) = 1 + N(n-1) + N(n-2).

Alínea e)

Um problema que surge frequentemente quando se trabalha com módulos é o da inclusão múltipla do mesmo ficheiro de cabeçalho. Por exemplo, considere o módulo de listas genéricas que foi programado no exercício 4.c), e admita que o ficheiro de cabeçalho respectivo se chama listas.h. Suponha que o (os) tipos definidos no ficheiro listas.h é (são) necessário(s) para a declaração das funções no ficheiro de cabeçalho de outro módulo, por exemplo utentes.h:

/* utentes.h */
#include "listas.h"
...

Se o módulo principal (aquele que contém a função main()) necessitar simultâneamente dos módulos listas e utentes, conterá, provavelmente, as directivas:

#include "listas.h"
#include "utentes.h"

o que resultará na inclusão do ficheiro listas.h duas vezes. Esta dupla inclusão acabará por gerar erros de compilação, porque o mesmo tipo não pode ser definido duas vezes (com typedef). Mas, mesmo que não ocorra qualquer erro de compilação, a dupla inclusão de um ficheiro de cabeçalho é inútil e desperdiça tempo de compilação.

Tente evitar a dupla inclusão, tal como descrita atrás, do ficheiro sucessao.h, recorrendo para isso às seguintes directivas do pré-processador:

#define nome oque (em que oque é opcional)
define a macro nome, que é expandida para oque
#ifndef nome
o código entre esta directiva e a próxima directiva #else ou #endif é compilado apenas se a macro nome não estiver definida.

Resolução

Alínea f)

Escreva um ficheiro de cabeçalho uteis.h, não correspondendo a qualquer módulo, que defina as seguintes macros úteis:

ABS(x)
Calcula o valor absoluto de x.
ABSDIF(x, y)
Calcula o valor absoluto da diferença entre x e y.
ABSMAIOR(x, y)
Devolve verdadeiro se |x| > y (com y positivo).
MAX(x, y)
Devolve o maior dos argumentos.
MIN(x, y)
Devolve o menor dos argumentos.
LIM(x, y, z)
Devolve o valor de y limitado ao intervalo [x z].
SINAL(x)
Devolve o sinal de x (-1 para negativo, 0 para nulo e 1 para positivo).
TROCA(x, y, aux)
Troca a variável x com y usando a variável auxiliar aux.

Comente claramente os possíveis efeitos secundários das macros, nomedamente indicando o número de vezes que cada argumento da macro é utilizado no texto de expansão da mesma.

Explique porque é que não se devem, em geral, chamar macros com argumentos envolvendo expressões complexas. Por exemplo:

x = ABS(sin(cos(x*x/y)));

Não se esqueça de usar parênteses generosamente para evitar os problemas de precedências, tão frequentes quando se utilizam macros!

Resolução

Repare que a chamada

x = ABS(sin(cos(x*x/y)));

é traduzida pelo compilador para

x = ((sin(cos(x*x/y))) >= 0 ? (sin(cos(x*x/y))) : 
                             -(sin(cos(x*x/y))));

o que implica que o cálculo completo é efectuado duas vezes, o que é extremamente ineficiente. Por outro lado

x = ABS(i++);

é traduzida pelo compilador para

x = ((i++) >= 0 ? (i++) : -(i++));

que não produzirá os resultados esperados nem em i nem em x!


Exercício 6

Refaça o programa do exercício 2 de modo a utilizar um ficheiro de registos binário para guardar a base de dados, em vez duma matriz. Assim, deixa de haver limitação quanto ao número de utentes. Não são permitidas repetições de nomes ou códigos.

Utilize as funções:

fopen() com modo "r+b" ou "w+b"
para criar um abrir ou criar um ficheiro no modo de actualização.
ftell()
para obter a posição de leitura/escrita corrente.
fseek()
para colocar a posição de leitura/escrita num local determinado.
fread()
para efectuar leituras de registos.
fwrite()
para efectuar escritas de registos.
feof()
devolve verdadeiro se e só se se tiver já tentado ler para além do fim do ficheiro (isto é, se o indicador de fim-de-ficheiro estiver activo).

Valores negativos no código dos utentes continuam a indicar que a posição, neste caso no ficheiro de registos, está livre.

Não esquecer que, nesta versão do programa, a base de dados é guardada, entre chamadas ao programa, no próprio ficheiro de registos binário (e não num ficheiro de texto como no exercício 2). 5

Note-se que é muito importante pensar nas funções a realizar antes de as começar a programar em C. Em particular, a utilização de um diagrama para representar o estado do ficheiro de registos e a evolução da posição de leitura/escrita revelar-se-á muito útil.

Finalmente, lembre-se de que a norma ANSI-C obriga a que entre passagens de leitura para escrita e vice versa sejam intercaladas chamadas a fflush(), fseek(), fsetpos() ou frewind(). Entre operações de leitura e escrita (neste sentido, de leitura para escrita), não é necessário intercalar estas funções se as funções de leitura terminaram colocando o indicador de fim-de-ficheiro no valor verdadeiro.

Resolução

Fica como exercício expurgar o ficheiro de registo de todos os registos livres antes de sair do programa.


Exercício 7

Alínea a)

Realize um módulo de pilhas genéricas (o último a entrar é o primeiro a sair). Faça também o respectivo programa de teste. Dever-se-ão utilizar matrizes para guardar os ponteiros genéricos (void *) para os dados do utilizador do módulo. Assim, sendo, existe um número máximo de elementos na pilha. O número máximo de elementos deverá poder ser especificado pelo utilizador aquando da criação duma nova pilha (i.e., deverá usar memória dinâmica para a matriz, que será afectada pela função calloc()).

Apresentam-se sugestões para as funções de interface do módulo (PM significa "pilhas com matrizes"):

PilhaM *PMcria(size_t n)
cria uma nova pilha cuja dimensão (número máximo de elementos guardados) é n.
void PMdestroi(PilhaM *p)
desafecta toda a memória associada à pilha p.
PilhaM *PMesvazia(PilhaM *p)
esvazia a pilha p.
size_t PMquantos(PilhaM *p)
devolve o número de elementos guardados na pilha p.
int PMcheia(PilhaM *p)
devolve verdadeiro se a pilha p estiver cheia.
PilhaM *PMpoe(PilhaM *p, void *d)
insere os dados d na pilha p; devolve zero se a pilha estiver cheia, caso contrário devolve o ponteiro para a pilha.
void *PMtira(PilhaM *p)
retira o elemento que se encontra há menos tempo na pilha p e devolve os seus dados; devolve zero se a pilha estiver vazia.

Resolução

Alínea b)

Semelhante à alínea a), mas agora serão filas (o primeiro a entrar é o primeiro a sair), e não pilhas.

As funções de exemplo são semelhantes, substituindo-se PM por FM.

Note que a função void *FMtira(FilaM *f) retira o elemento que se encontra há mais tempo na fila f!

Repare que neste caso poderá usar os indíces de modo a que "dêem a volta", evitando-se assim a renormalização da matriz sempre que se retira um elemento.

Finalmente, note que a condição de "fila cheia" se pode confundir com a condição de "fila vazia"! Pode evitar o problema reservando n+1 posições na matriz e nunca deixando introduzir mais do que n elementos.

Resolução

Alínea c)

Semelhante à alínea a), mas agora utilizando o módulo de listas genéricas, desenvolvido no exercício 4.c), e não matrizes. Exemplo de funções de interface do módulo (P significa "pilhas"):

Pilha *Pcria(void)
cria uma nova pilha.
void Pdestroi(Pilha *p)
desafecta toda a memória associada à pilha p.
Pilha *Pesvazia(Pilha *p)
esvazia a pilha p.
unsigned long Pquantos(Pilha *p)
devolve o número de elementos guardados na pilha p.
Pilha *Ppoe(Pilha *p, void *d)
insere os dados d na pilha p; devolve o ponteiro para a pilha.
void *Ptira(Pilha *p)
devolve os dados que se encontram há menos tempo na pilha p; devolve zero se a pilha estiver vazia.

Repare que neste caso não se especifica qualquer dimensão máxima para a pilha aquando da sua criação. Note-se ainda que não existe, como é óbvio, qualquer função para verificar se a pilha está cheia.

Note ainda que poderá necessitar de fazer algumas alterações ão módulo de listas genéricas, pois poderá faltar uma função que "retire o elemento inicial".

Resolução

Alínea d)

Semelhante à alínea c), mas agora serão filas, e não pilhas.

As funções de exemplo são semelhantes, substituindo-se P por F.

Note que a função void *Ftira(Fila *f) retira o elemento que se encontra há mais tempo na fila f!

Note ainda que poderá necessitar de fazer algumas alterações ão módulo de listas genéricas, pois poderá faltar uma função que "retire o elemento final".

Resolução


Exercício 8

Faça um programa que permita realizar as seguintes operações elementares com valores double:

As variáveis, que consistirão em pares (nome,valor) (i.e., estruturas), devem ser guardadas numa lista (use o módulo de listas genéricas desenvolvido no exercício 4.c)).

Note que as variáveis a que este enunciado se refere não são variáveis da linguagem C, mas sim objectos guardados pelo programa e definidos pelo seu utilizador.

Resolução


Página concebida e mantida por Eng. Manuel Menezes de Sequeira (última actualização 2006/07/07)
Copyright © 1996-2001 ISCTE