Note-se que:
Mostrar no terminal o conteúdo de um ficheiro de texto.
O programa deve, ao ser executado, mostrar no ecrã o conteúdo dum ficheiro de texto já existente algures no disco.
meutype texto.txt
mostra no ecrã o conteúdo do ficheiro texto.txt.
Armazenar em ficheiro um texto lido do terminal.
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.
Fazer uma cópia dum ficheiro de texto.
Concatenar dois ficheiros de texto.
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.
Comparar dois ficheiros de texto linha a linha e mostrar a primeira linha onde eles diferem.
while(fgets(s1, n, entrada1) != NULL &&
fgets(s2, n, entrada2) != NULL)
tem a desvantagem de, quando a primeira condição do E falha, a segunda não chegar a ser calculada, ou seja, o segundo fgets() não chega a ser chamado (lembre-se que os operadores E (&&) e OU (||) são especiais, pois o operando esquerdo é calculado em primeiro lugar e, se determinar o resultado da expressão lógica, o operando direito não chega a ser calculado!). Isto leva a que, se ambos os ficheiros forem iguais, o ciclo termina com condição de fim-de-ficheiro no ficheiro correspondente a entrada1 mas não no entrada2 ! Isso tornaria mais dificil avaliar se os dois seriam de facto iguais ou não.
Contar os números de caracteres, palavras e linhas de um ficheiro de texto.
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').
Mostrar as linhas de um ficheiro de texto onde ocorre uma dada sequência de caracteres.
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).
A concretização deste programa é trivial, requerendo apenas algum conhecimento das especificações de conversão da função printf().
Mostrar no terminal o conteúdo de ficheiros de texto gerados pelo programa da alínea anterior.
A concretização deste programa é trivial, requerendo apenas algum conhecimento das especificações de conversão da função fscanf().
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.
Ler para matrizes os ficheiros do exercício 1.j) e afixar no ecrã o seu conteúdo.
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:
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.
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.
Listas e recursividade:
Refaça o programa do exercício 2 de modo a usar o conceito de lista simplesmente ligada. O programa deve, portanto, permitir:
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.
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:
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.
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).
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)
{
...
}
...
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. */
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:
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:
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):
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.
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:
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.
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.
printf("F(%d) = %lu\n", n, fibonacciRec(n));
printf("chamadas = %lu\n", fibonacciRec(-2));
e não
printf("F(%d) = %lu\nchamadas = %lu\n", n,
fibonacciRec(n), fibonacciRec(-2));
pois o ANSI-C não garante a ordem de cálculo dos argumentos duma função, e portanto fibonacciRec(-2) poderia ser calculada em primeiro lugar!
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:
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).
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().
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).
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:
#endif /* _SUCESSAO_H_ */
uma vez que o ANSI-C não permite argumentos nas directivas #endif e #else.
Escreva um ficheiro de cabeçalho uteis.h, não correspondendo a qualquer módulo, que defina as seguintes macros úteis:
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!
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!
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:
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.
Fica como exercício expurgar o ficheiro de registo de todos os registos livres antes de sair do programa.
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"):
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á
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.
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"):
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".
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á
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".
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.
| Página
concebida e mantida por Eng. Manuel Menezes de Sequeira (última actualização 2006/07/07) Copyright © 1996-2001 ISCTE |
||||