Prévia do material em texto
#include<math.h>
#include<stdio.h>
#include<stdlib.h>
float max(float x,
float y){
if (x > y)
return x;
return y;
}
float mediaHarmPond(float amostra1,
float peso1,
float amostra2,
float peso2,
float amostra3,
float peso3) {
return (peso1 + peso2 +peso3)/(peso1/max(amostra1,0.0001)+peso2/max(amostra2,0.0001)+peso3/max(amostra3,0.0001));
}
void main(){
unsigned long int cod;
int i, n=0;
float p1, p2, p3, mh[60], mh_media=0, somatorio=0, s;
FILE *saida;
if ((saida = fopen("l:medias.DAT", "w"))== NULL){ // Cria um arquivo para escrita
printf("Nao conseguiu criar arquivo de saida.\n");
exit(0); // Aborta execucao
}
while(1){
printf("Forneca o codigo do aluno\n");
scanf("%ld",&cod);
if (cod==0) break; // Sai do laco
printf("Forneca as notas das provas do aluno\n");
scanf("%f %f %f",&p1, &p2, &p3);
if (p1>=0 && p1<=10 && p2>=0 && p2<=10 && p3>=0 && p3<=10){
mh[n] = mediaHarmPond(p1,1,p2,2,p3,3);
mh_media += mh[n];
fprintf(saida,"Codigo: %8ld Prova 1: %2.2f Prova 2: %2.2f Prova 3: %2.2f Media harmonica ponderada: %2.2f\n", cod, p1, p2, p3, mh[n]);
n++;
} else
printf("Intervalo de notas invalido\n");
}
mh_media = mh_media/n;
for(i=0;i<n;i++)
somatorio+=(mh[i]-mh_media)*(mh[i]-mh_media);
s = sqrt(somatorio/(n-1));
printf("O desvio padrao eh %.4f\n",s);
}
Programação
em C
Módulo Avançado
Sumário
Prefácio 4
1 Tipos de Dados 5
1.1 Enumeração 7
1.2 Modificadores de Tipo de Acesso 7
1.2.1 Const 7
1.2.2 Volatile 8
1.3 Constantes 8
1.3.1 Constantes pré-definidas 9
2 Operadores 10
2.1 Operadores Bit a Bit 10
2.1.1 Operadores bit a bit de Atribuição 12
2.2 Operador ? : 12
2.3 Operadores de Ponteiros & e * 13
2.4 Precedência dos operadores 13
2.5 Exercícios 14
3 Funções 15
3.1 Localização das funções 15
3.1.1 Corpo da função antes do programa principal (no mesmo arquivo) 16
3.1.2 Corpo da função depois do programa principal (no mesmo arquivo) 16
3.1.3 Corpo da função escrito em arquivo separado 16
3.2 Argumentos para função main() 17
3.3 Protótipo de funções 17
3.4 Retorno de Ponteiros 18
3.5 Classes de Armazenamento 18
3.5.1 auto 18
3.5.2 static 18
3.5.3 extern 19
3.5.4 register 20
3.6 Diretiva #define 20
3.7 Funções Recursivas 20
3.8 Exercícios 21
4 Ponteiros 24
4.1 Expressões com Ponteiros 24
4.1.1 Atribuição de Ponteiros 24
4.1.2 Aritmética de Ponteiros 24
4.2 Inicialização de Ponteiros 25
4.2.1 Comparação de Ponteiros 26
4.3 Ponteiros e Matrizes 26
4.3.1 Matrizes de Ponteiros 27
4.3.2 Acessando partes de Matrizes como vetores 27
4.4 Indireção Múltipla 28
4.5 Ponteiros para Funções 28
4.6 Mais Sobre declarações de Ponteiros 29
4.7 Exercícios 31
5 Estruturas e Uniões 32
5.1 Estruturas 32
5.1.1 Inicializando Estruturas 33
5.1.2 Estruturas Aninhadas 33
5.1.3 Estruturas e funções 34
5.1.4 Vetor de Estruturas 34
5.1.5 Ponteiros para Estruturas 35
5.2 Campos de Bits 35
5.3 Uniões 37
5.4 Sizeof() 38
5.5 Typedef 39
5.6 Exercícios 40
6 Alocação Dinâmica 41
6.1 Mapa de memória 41
6.2 Funções de Alocação dinâmica em C 41
6.2.1 malloc() 42
6.2.2 calloc() 43
6.2.3 free() 43
6.2.4 realloc() 44
6.3 Matrizes Dinamicamente Alocadas 44
6.4 Listas Encadeadas 45
6.4.1 Listas Singularmente Encadeadas 45
6.4.2 Listas Duplamente Encadeadas 46
6.5 Árvores Binárias 48
6.6 Exercícios 50
7 E/S com Arquivo 51
7.1 E/S ANSI x E/S UNIX 51
7.2 Streams 51
7.3 Arquivos 51
7.4 Sistema de Arquivos 51
7.5 Estrutura FILE 52
7.6 Abertura de Arquivos 52
7.7 Fechamento de Arquivo 53
7.8 Verificando fim de arquivo 53
7.9 Condições de erro 53
7.10 Streams Padrão 54
7.11 Leitura e Gravação de caracteres 54
7.12 Trabalhando com Strings 55
7.13 Funções de tratamento de arquivos 56
7.13.1 rewind() 56
7.13.2 ferror() 56
7.13.3 remove() 57
7.13.4 fflush() 57
7.13.5 Acesso aleatório: fseek() 57
7.13.6 ftell() 58
7.14 Comando de gravação em modo texto formatado 58
7.15 Lendo e gravando registros 59
7.15.1 Escrita de um bloco de dados 59
7.15.2 Leitura de um bloco de dados 59
7.15.3 Utilizando os comandos de leitura e gravação de registros 60
7.16 Funções para manipulação de buffers 60
7.17 Exercícios 62
A. Tabela Ascii 63
8 Bibliografia 64
Prefácio
Este texto tem o objetivo de fornecer os subsídios para o desenvolvimento de programas avançados na linguagem C. Os tópicos estudados neste texto são estruturas, uniões, campos de bits, alocação dinâmica e arquivos.
Tipos de Dados
Em C existem 5 tipos de variáveis básicas. Nos computadores da linha IBM-PC (plataforma 32 bits), a Tabela 1.1 é válida. Estes tipos de dados definem a quantidade de memória que ocupa e o intervalo de valores que consegue representar.
Tabela 1.1 - Tipos de dados básicos para plataformas 32 bits
Tipo
Bits
Faixa Mínima
char
8
-128 a 127
int
32
-2,147,483,648 a 2,147,483,647
float
32
3.4E-38 a 3.4E+38
double
64
1.7E-308 a 1.7E+308
void
0
sem valor
Os tipos char e int armazenam números inteiros, enquanto que os tipos float e Double armazenam números de ponto flutuante (é um formato de representação digital de números reais).
Com exceção de void, os tipos de dados básicos podem estar acompanhados por modificadores na declaração de variáveis. Os modificadores de tipos da linguagem C são:
Linguagem de Programação C
5
· signed;
· long;
· unsigned;
· short.
Os modificadores signed, short, long e unsigned podem ser aplicados aos tipos básicos char e int. Contudo, long também pode ser aplicado à double.
A função printf() possui especificadores de formato que permitem mostrar inteiros short e long. O %ld, %li, %lo, %lu, %lx especificam que o tipo de dado é long. O %hd, %hi, %ho, %hu, %hx especificam que o tipo de dado é short.
O especificador de formato long pode ser ainda utilizado para tipo ponto flutuante (indicando que segue um double): %le, %l E, %lf, %lg, e %lG. Outro especificado de formato é o L, o qual é utilizado para associar um long double: %Le, %LE, %Lf, %Lg, e %LG.
A Tabela 1.2 mostra todas as combinações de tipos de dados e as informações sobre tamanho, formatação e intervalo.
Tabela 1.2 - Utilização dos tipos de dados (plataforma 32 bits)
Tipo
Bits
Formatação printf()
Intervalo
Inicio
Fim
Inteiros
char
8
%c
-128
127
unsigned char
8
%c
0
255
signed char
8
%c
-128
127
short int
16
%hi
-32.768
32.767
signed short int
16
%hi
-32.768
32.767
unsigned short int
16
%hu
0
65.535
int
32
%i
-2.147.483.648
2.147.483.647
signed int
32
%i
-2.147.483.648
2.147.483.647
unsigned int
32
%u
0
4.294.967.295
long int
32
%li
-2.147.483.648
2.147.483.647
signed long int
32
%li
-2.147.483.648
2.147.483.647
unsigned long int
32
%lu
0
4.294.967.295
Ponto Flutuante
float
32
%f
3,4E-38
3.4E+38
double
64
%lf
1,7E-308
1,7E+308
long double
80
%Lf
3,4E-4932
3,4E+4932
O uso de signed com inteiros é redundante. No entanto, ele é permitido porque a declaração default de inteiros assume um número com sinal. O uso mais importante de signed é modificar char em implementações em que esse tipo, por padrão, não tem sinal. Algumas implementações podem permitir que unsigned seja aplicado aos tipos de ponto flutuante (como em unsigned double). Porém, isso reduz a portabilidade de seu código e geralmente não é recomendado. O modificador unsigned altera o valor da faixa mínima do tipo através do uso do bit mais significativo (indicador de sinal).
O tamanho, e conseqüentemente o intervalo de valores, pode variar de plataforma para plataforma. Por exemplo, o long double em algumas plataformas possui 10 bytes de tamanho. O char já é um tipo que não varia de plataforma.
Exemplo 1.1
#include <stdio.h>
int main() {
int qtde;
char tam;
float total;
qtde = 2; tam = ‘G’;
total = 20.70;
printf(“Comprei %d camisas de tamanho %c.”, qtde, tam);
printf(“\nNo total, paguei R$ %f.”, custo);
return 0;
}
Execução:
Comprei 2 camisas de tamanho G.
No total, paguei R$ 20.70.
As variáveis podem ser inicializadas no momentoem que se faz a declaração das mesmas. Pode-se ver isto usando o programa anterior, que a execução será a mesma da versão anterior.
Exemplo 1.2
#include <stdio.h>
int main() {
int qtde=2;
char tam=‘G’;
float total=20.70;
printf(“Comprei %d camisas de tamanho %c.”, qtde, tam);
printf(“\nNo total, paguei R$ %f.”, custo);
return 0;
}
Devido às diferenças de tipos em diferentes máquinas e plataformas, pode-se utilizar a função sizeof() para descobrir o tamanho real da variável ou tipo.
Exemplo 1.3
#include <stdio.h>
int main() {
printf("Tipo\t\tTamanho\n");
printf("char\t\t%d\n",sizeof(char));
printf("int\t\t%d\n",sizeof(int));
printf("float\t\t%d\n",sizeof(float));
printf("double\t\t%d\n",sizeof(double));
printf("long int\t%d\n",sizeof(long int));
return 0;
}
Execução:
Tipo Tamanho
char 1
int 4
float 4
double 8
long int 4
Enumeração
Enumeração é um conjunto de constantes inteiras que especifica todos os valores legais de uma variável desse tipo pode ser. A forma geral para enumeração é:
Sintaxe:
enum nome { lista_de_enumeração } lista_de_variáveis;
Aqui, tanto o nome da enumeração quanto a lista de variáveis são opcionais. O nome da enumeração é usado para declarar variáveis daquele tipo. Com isso pode-se declarar as cores
Exemplo 1.4
enum cores {amarelo, verde, vermelho};
enum cores semaforo;
Dada essa definição e declaração, os tipos de comandos seguintes são perfeitamente válidos:
semaforo = verde;
if (semaforo==verde) printf(“Passagem permitida \n”);
Para melhor compreensão da enumeração entende-se que cada símbolo representa um valor inteiro. O valor do primeiro símbolo da enumeração é 0. Assim,
printf (“%d %d”, verde, vermelho);
mostra 1 2 na tela.
Como extensão, pode-se inicializar os símbolos de forma alternada para algum problema específico.
Exemplo 1.5
enum cores { amarelo, verde=10, vermelho };
Agora os valores destes símbolos são
amarelo 0
verde 10
vermelho 11
Modificadores de Tipo de Acesso
O padrão ANSI introduziu dois novos modificadores de tipo que controlam a maneira como a variáveis podem ser acessadas ou modificadas. Esses modificadores são const e volatile. Devem preceder os modificadores de tipo e os nomes que eles modificam.
Const
Variáveis do tipo const não podem ser modificadas por seu programa (por isso ela recebe um valor inicial).
Exemplo 1.6
const int a=10;
O Exemplo 1.6 cria uma variável inteira chamada a, com um valor inicial 10, que seu programa não pode modificar.
Um exemplo do uso do const é para verificar se uma variável em particular é modificada pelo seu programa.
Volatile
O modificador volatile é usado para informar ao compilador que o valor de uma variável pode ser alterado de maneira não explicitamente especificada pelo programa. Por exemplo, um endereço de uma variável global pode ser passado para a rotina de relógio do sistema operacional e usado para guardar o tempo real do sistema. Nessa situação, o conteúdo de uma variável é alterado sem nenhum comando de atribuição explicito no programa. Isso ajuda o programa no sentido de avisar ao compilador que o conteúdo de uma variável é mutável, mesmo que sua referência não aparecer no lado esquerdo da expressão.
É possível usar const e volatile juntos. Por exemplo, se 0x30 é assumido como sendo o valor de uma porta que é mudado por condições externas. Para evitar efeitos colaterais deve-se declarar da seguinte forma:
const volatile unsigned char *port = 0x30;
Constantes
Uma constante tem valor fixo e inalterável durante a execução do programa. Isto pode ser exemplificado pelos Exemplos 3.1 e 3.2 da função printf().
Em uma constante caractere é escrita entre aspas simples (‘’), uma constante cadeia de caracteres entre aspas duplas (“”) e constantes numéricas como o número propriamente dito.
Exemplo 1.7
‘C’
“programa”
8
465.67
Constantes em C podem ser de qualquer um dos cinco tipos de dados básicos. A maneira como cada constante é representada depende do seu tipo. Pode-se especificar precisamente o tipo da constante numérica através da utilização de um sufixo. Para tipos em ponto flutuante coloca-se um F após o número, ele será tratado como float. Se for colocado um L, ele tornar-se-á um long double. Para tipos inteiros, o sufixo U representa unsigned e o L representa long. A Tabela 1.3 mostra alguns exemplos de constantes.
Tabela 1.3 - Exemplo de constantes
Tipo de Dado
Exemplo de Constantes
int
1 123 21000 -234
long int
35000L -34L
short int
10 -12 90
unsigned int
10000U 987U 40000“
float
123.23F 2.34e-3F
double
123.23 12312333 -0.9876324
long double
1001.2L
Além deste tem-se as constantes Hexadecimais e Octais. Usam-se tais sistemas numéricos para facilitar a programação. Uma constante hexadecimal deve consistir em um 0x seguido por uma constante na forma hexadecimal. Uma constante octal começa com 0.
Exemplo 1.8
int hex = 0x80; /* 128 em decimal */
int oct = 012; /* 10 em decimal */
Constantes pré-definidas
Em alguns compiladores C, algumas constantes simbólicas já estão pré-definidas. Estas constantes em geral definam alguns valores matemáticos (, /2, e, etc.), limites de tipos etc. A seguir são apresentadas algumas (existem muitas outras) constantes simbólicas pré-definidas no compilador Turbo C++ da Borland.
Biblioteca Constante Valor Significado
math.h M_PI 3.14159...
math.h M_PI_2 1.57079... /2
math.h M_PI_4 0,78539... /4
math.h M_1_PI 0,31830... 1/
math.h M_SQRT2 1,41421... 2
Operadores
A linguagem C é muito rica em operadores internos. C define quatro classes de operadores: aritméticos, relacionais, lógicos e bit a bit. Além disso, C tem alguns operadores especiais para tarefas particulares.
Operadores Bit a Bit
Os operadores bit a bit são comumente utilizados para trabalhar com dispositivos (pois os mesmos utilizam bytes ou palavras codificadas para comunicação), modo de armazenamento (um byte pode representar 8 informações binárias), e até compactação de dados (utilização de bits ociosos). A Tabela 2.1 mostra os operadores bit a bit suportados pela linguagem.
Tabela 2.1 - Operadores bit-a-bit
Operador
Ação
&
E (AND)
|
OU (OR)
^
OU exclusivo (XOR)
~
Complemento de um
>>
Deslocamento à esquerda
<<
Deslocamento à direita
Os operadores bit a bit só podem ser utilizados sobre um byte ou uma palavra, isto é, aos tipos de dados char e int e variantes do padrão C. Operações bit não podem ser usadas em float, double, long double, void ou outros tipos mais complexos.
O operador bit a bit & executa um e lógico para cada par de bits, produzindo um novo byte ou palavra. Para cada bit dos operandos, o operador & retorna o bit em 1 se ambos os bits dos operandos é 1. Caso algum bit dos operandos for 0, o operador retorna o bit 0. Este operador é mais utilizado para desligar bits (realizando a operação com um operando - também chamado de máscara - cujos bits que devam ser desligados estejam com valor 0, enquanto que os outros estão em 1) ou verificar se um determinado bit está ligado ou desligado (realizando a operação com um operando cujo bit que deva ser checado esteja com valor 1, enquanto que os outros estão em 1).
Exemplo 2.1
unsigned char x = 7; /* 0000 0111 */
unsigned char y = 4; /* 0000 1010 */
unsigned char mascara = 252; /* 1111 1100 */
unsigned char res;
res = x & y; /* res = 0000 0010 */
res = y & mascara; /* res = 0000 1000 – desligar os bits 0 e 1 */
res = y & 2 /* res = 0000 0010 – bit ligado qdo res > 0 */
res = y & 4 /* res = 0000 0000 – bit desligado qdo res == 0 */
O operador bit a bit | executa um ou lógico para cada par de bits, produzindo um novo byte ou palavra. Para cada bit dos operandos, o operador | retorna o bit em 1 se algum dos bits dos operandos é 1. Caso ambos os bits dos operandos for 0, o operador retorna o bit 0. Este operador é mais utilizado para ligar (realizando a operaçãocom um operando cujos bits que devam ser ligados estejam com valor 1, enquanto que os outros que não devem ser alterados estão em 0).
Exemplo 2.2
unsigned char x = 7; /* 0000 0111 */
unsigned char y = 4; /* 0000 1010 */
unsigned char mascara = 1; /* 0000 0001 */
unsigned char res;
res = x | y; /* res = 0000 1111 */
res = y | mascara; /* res = 0000 1011 – ligar o bit 0 */
res = x | 8; /* res = 0000 1111 – ligar o bit 3 */
O operador bit a bit ^ executa um ou-exclusivo (XOR) lógico para cada par de bits, produzindo um novo byte ou palavra. Para cada bit dos operandos, o operador ^ retorna o bit em 1 se somente um dos bits dos operandos é 1. Caso os bits dos operandos forem iguais, o operador retorna o bit 0. Este operador é mais utilizado para inverter bits (realizando a operação com um operando cujos bits que devam ser invertidos estejam com valor 1, enquanto que os outros estão em 0).
Exemplo 2.3
unsigned char x = 7; /* 0000 0111 */
unsigned char y = 4; /* 0000 1010 */
unsigned char mascara = 3; /* 0000 0011 */
unsigned char res;
res = x ^ y; /* res = 0000 1101 */
res = y ^ mascara; /* res = 0000 1001 – inverter os bits 0 e 1 */
res = y ^ 8; /* res = 0000 0010 – inverter o bit 3 */
O operador bit a bit ~ executa um não lógico (complemento de 1) no valor a sua direita (operador unário), produzindo um novo byte ou palavra com os bits invertidos.
Exemplo 2.4
unsigned char x = 7; /* 0000 0111 */
unsigned char res;
res = ~x; /* res = 1111 1000 */
res = ~127; /* res = 1000 0000 */
Os operadores de deslocamento, ≪ e ≫, movem todos os bits de um operando para a esquerda ou direita, respectivamente. A forma geral do comando de deslocamento é:
Sintaxe:
operando << número de bits
operando >> número de bits
Conforme os bits são deslocados para um direção, zeros são utilizados para preencher a extremidade contrária da direção (isto é, deslocamento para a direita coloca zeros os bits mais significativos). Estes operadores são utilizados para recebimento e envio de dados bit a bit (conversores analógico/digitais, portas seriais), e multiplicação (deslocamento para a esquerda) e divisão inteira (deslocamento para a direita) por 2.
Exemplo 2.5
#include <stdio.h>
int main() {
unsigned char x=7;
printf("1. %2i\n",x >> 1);
printf("2. %2i\n",x << 1);
printf("3. %2i\n",x >> 2);
printf("4. %2i\n",x << 2);
printf("5. %2i\n",8 >> 2);
printf("6. %2i\n",8 << 2);
printf("7. %2i\n",x+3 >> 2);
printf("8. %2i\n",x+3 << 2);
return 0;
}
Execução:
1. 3
2. 14
3. 1
4. 28
5. 2
6. 32
7. 2
8. 40
O Exemplo 2.5 mostra que os operadores de deslocamento podem ser utilizados com variáveis, constantes e até mesmo expressões. Entretanto, deve-se verificar a precedência de operadores quando trabalhando com expressões.
Exemplo 2.6
unsigned char x = 7; /* 0000 0111 */
unsigned char res;
res = x << 1; /* res = 00001110 res = 14 */
res = x << 3; /* res = 01110000 res = 112 */
res = x << 2; /* res = 11000000 res = 192 */
res = x >> 1; /* res = 01100000 res = 96 */
res = x >> 2; /* res = 00011000 res = 24 */
Não confunda os operadores relacionais && e || com & e |, respectivamente. Os operadores relacionais trabalham com os operandos como um único valor lógico (verdadeiro ou falso), e eles produzem somente dois valores 0 ou 1. Os operadores bit a bit podem produzir valores arbitrários pois a operação é realizada em nível de bit.
Operadores bit a bit de Atribuição
A Tabela 2.2 mostra os operadores bit a bit de atribuição suportados pela linguagem.
Tabela 2.2 - Operadores aritméticos de atribuição
Operador
Ação
x &= y
x = x & y
x |= y
x = x | y
x ^= y
x = x ^ y
x ~= y
x = x ~ y
x >>= y
x = x >> y
x <<= y
x = x << y
As expressões com este operadores são mais compactas e normalmente produzem um código de máquina mais eficiente.
A execução da operação bit a bit ocorre por último após a avaliação da expressão à direita do sinal igual.
Operador ? :
O operador ? substitui sentenças da forma Se-então-senão.
Sintaxe:
Exp1 ? Exp2 : Exp3;
Onde Exp1, Exp2 e Exp3 são expressões. Onde Exp1 é avaliada e se a mesma for verdadeira, então Exp2 é avaliada e se torna o valor da expressão. Se Exp1 é falso, então Exp3 é avaliada e se torna o valor da expressão.
Exemplo 2.7
x = 10;
y = x > 9 ? 100 : 200;
No Exemplo 2.7, y recebe o valor 100, porque x (valor é 10) é maior que 9. Uma expressão equivalente seria
x = 10;
if (x > 9) y = 100;
else y = 200;
Operadores de Ponteiros & e *
Um ponteiro é um endereço na memória de uma variável. Uma variável de ponteiro é uma variável especialmente declarada para guardar um ponteiro para seu tipo especificado.
O primeiro operador de ponteiro é &. Ele é um operador unário que devolve o endereço na memória de seu operando. Por exemplo,
m = &cont;
atribui o endereço de memória da variável cont em m.
Este tipo de operando não pode ser utilizado em três casos:
1. &(cont + 4) - sempre se associa a uma variável e não expressão;
2. &3 - constantes não são válidas;
3. variáveis declaradas com classe de armazenamento register (não existe endereço para registrador).
O segundo operador é *. Ele é um operador unário que devolve o valor da variável localizada no endereço que o segue. Por exemplo, se m contém o endereço da variável cont,
q = *m;
coloca o valor de cont em q.
Os seguintes operadores * e & colocam o valor 10 na variável chamada target. O resultado (o valor 10) deste programa é mostrado na tela.
Exemplo 2.8
#include <stdio.h>
int main() {
int target, source;
int *m;
source = 10;
m = &source;
target = *m;
printf(“%d”,target);
return 0;
}
Precedência dos operadores
A Tabela 2.3 mostra a precedência dos operadores da linguagem C.
Tabela 2.3: Precedência dos operadores
Precedência
Operador
1º
() [] ->
2º
- (menos unário) ++ -- ! ~ & (endereço) * (ponteiro)
3º
* / %
4º
+ -
4º
<< ≫
5º
< <= > >=
6º
== !=
7º
&
8º
^
9º
!
10º
&&
11º
||
12º
?:
13º
= *= /= %= += -= &= |= ^= ~= <<= >>=
14º
,
Exercícios
1. Faça um programa que leia um número binário de 16 bits, armazene-o, e mostre o valor em hexadecimal, decimal e octal.
2. Faça uma função que receba um valor do tipo int como parâmetro e escreva na tela os valores do bits do valor
3. Faça uma função que receba um valor do tipo int como parâmetro e devolva um novo valor int com a ordem dos bits invertidos.
4. Faça uma função que receba um valor do tipo char como parâmetro e devolva quantos bits estão ligados.
5. Faça uma função crossover(n, m, pontoDeCorte) que retorna um inteiro que representa os bits mais significativos de n e os bits menos significativos de m, de acordo com o ponto de corte, que é a posição onde o número será partido. Por exemplo:
int main(){
crossover(10,69,2); // retorna 13 = 0000 1101
// 10 = 0000 1010, teremos que pegar desse nº, os bits + significativos, ou seja: 0000 1???
// 69 = 0100 0101, teremos que pegar desse nº, os bits menos significativos, ou seja: ???? ?101
crossover(10,69,3); // retorna 5 = 0000 0101, ou seja, metade de um + metade do outro
}
6. Faça a função rodaEsquerda(int n, int nBits) que retorna o n com nBits rotações à esquerda. Perceba que uma rotação não deve perder bits, ao contrário do operador de deslocamento. Trabalhe pensando apenas nos 8 bits para n.
int main(){
unsigned char x;
x = rodaEsquerda (4, 2); // se 4 = 0000 0100,
// então rodaEsquerda (4,2)== 0000 1000
}
7. Escreva uma função criptografa(int n) que recebe um inteiro n com 8 bits (índices: 7,6,5,4,3,2,1,0) e que retorna esse inteiro embaralhando esses bits para a seguinte seqüência (7,5,3,1,6,4,2,0)
int main(){
criptografa(73); // se 73 = 0100 1001,
// então criptografa(73) == 0010 1001 == 41
}
8. Faça a função descriptografa(int n) que faz o processo invertido da questão 7.
9. Faça uma função que receba um vetor de 32 posições de inteiros (valores 0 e 1) e que retorne um valor com os bits ligados ou desligados conforme o conteúdode cada posição do vetor.
Funções
A forma geral de uma função é:
Sintaxe:
tipo_função nome_função (declaração_parâmetros) {
corpo_função;
}
Exemplo 3.1
int soma(int x, int y) {
...
}
As funções retornam um valor (do tipo indicado em tipo_função). O valor retornado pela função é dado pelo comando return (o valor retornado pode ou não ser utilizado).
Existem dois tipos de passagem de argumentos: por valor e por referência. A segunda é realizada através de apontadores.
Exemplo 3.2
int pot(int x, int n) { /* x elevado na n potência */
int p;
for(p=1;n>0;n--)
p *= x;
return p;
}
No Exemplo 3.2, os argumentos foram passados por valor e a função retorna um valor do tipo inteiro. A chamada seria:
a = pot(10,2);
No Exemplo 3.3, nenhum valor é retornado (por isso usa-se o tipo void) mas é realizado uma troca dos valores das variáveis, necessitando de uma passagem de parâmetros por referência.
Exemplo 3.3
/* troca os valores de duas variáveis*/
void troca(int *a, *b) {
int aux;
aux = *a;
*a = *b;
*b = aux;
}
A chamada para esta função seria:
int x=1,y=2;
troca(&x,&y);
Na passagem de parâmetros por referência é passado explicitamente o endereço da variável com o uso do operador &. Quando o argumento for uma matriz automaticamente será passado o endereço da matriz para a função.
A linguagem C aceita chamadas recursivas de funções.
Localização das funções
Existem basicamente duas posições possíveis para escrevermos o corpo de uma função: ou antes ou depois do programa principal. Podemos ainda escrever uma função no mesmo arquivo do programa principal ou em arquivo separado.
Corpo da função antes do programa principal (no mesmo arquivo)
Quando escrevemos a definição de uma função antes do programa principal e no mesmo arquivo deste, nenhuma outra instrução é necessária.
Exemplo 3.4
float media2(float a, float b) { // função
float med;
med = (a + b) / 2.0;
return(med);
}
int main() { // programa principal
float num_1, num_2, med;
puts(”Digite dois números:”);
scanf(”%f %f”, &num_1, &num_2);
med = media2(num_1, num_2); // chamada da função
printf(”\nA media destes números é %f”, med);
return 0;
}
Corpo da função depois do programa principal (no mesmo arquivo)
Quando escrevemos a definição de uma função depois do programa principal e no mesmo arquivo deste, devemos incluir um protótipo da função chamada. Um protótipo é uma instrução que define o nome da função, seu tipo de retorno e a quantidade e o tipo dos argumentos da função. O protótipo de uma função indica ao compilador quais são as funções usadas no programa principal os tipo. A sintaxe geral para isto é a seguinte:
Sintaxe:
int main() { // programa principal
tipo nomef(...); // protótipo da função
...
var = nomef(...) // chamada a função
...
}
tipo nomef(...){ // definição da função
[corpo de função]
}
Exemplo 3.5
#include <stdio.h>
int main() { // programa principal
float media2(float,float); // protótipo de media2()
float num_1, num_2, med;
puts(”Digite dois números:”);
scanf(”%f %f”, &num_1, &num_2);
med = media2(num_1, num_2); // chamada a função
printf(”\nA media destes números é %f”, med);
}
float media2(float a, float b){ // função media2()
float med;
med = (a + b) / 2.0;
return(med);
}
Protótipo de uma função nada mais é que a declaração da função sem o seu corpo. Por isso, a lista de argumentos do protótipo podem ser escritas apenas com os tipos dos argumentos.
Corpo da função escrito em arquivo separado
Em C, como em muitas outras linguagens, é permitido que o usuário crie uma função em um arquivo e um programa que a chame em outro arquivo distinto. Esta facilidade permite a criação de bibliotecas de usuário: um conjunto de arquivos contendo funções escritas pelo usuário. Esta possibilidade é uma grande vantagem utilizada em larga escala por programadores profissionais.
Quando escrevemos a definição de uma função em arquivo separado do programa principal devemos incluir este arquivo no conjunto de arquivos de compilação do programa principal. Esta inclusão é feita com a diretiva #include. Esta diretiva, vista nas seções 2.4.2 e 3.7.1, instrui o compilador para incluir na compilação do programa outros arquivos que contem a definição das funções de usuário e de biblioteca.
Sintaxe:
#include ”path” // inclusão da função
int main() { // programa principal
...
var = nomef(...) // chamada a função
...
}
Na diretiva #include, indicamos entre aspas duplas o caminho de localização do arquivo onde está definida a função chamada.
Exemplo 3.6
#include ”c:\tc\userbib\stat.h” // inclusão da função
int main() { // programa principal
float num_1, num_2, med;
puts(”Digite dois números:”);
scanf(”%f %f”, &num_1, &num_2);
med = media2(num_1, num_2); // chamada a função
printf(”\nA media destes números é %f”, med);
return 0;
}
Argumentos para função main()
A função main() aceita argumentos para a passagem de parâmetros realizada através da chamada do programa. Os dois argumentos são:
argc: contador de argumentos;
argv: vetor de argumentos (vetor de apontadores para strings).
Sintaxe:
main(int argc, char *argv[])
É importante lembrar que
Exemplo 3.7
#include <stdio.h>
int main(int argc, char *argv[]) {
int cont;
printf(“Foram encontrados %d argumentos \n”,argc -1);
for (cont=1;cont < argc;cont++)
printf(“Argumento %d: %s \n”, cont, argv[cont]);
return 0;
}
O primeiro argumento (argv[0]) é o nome do programa.
Protótipo de funções
O padrão ANSI C expandiu a declaração tradicional de função, permitindo que a quantidade e os tipos dos argumentos das funções sejam declarados. A definição expandida é chamada protótipo de função. Protótipos de funções não faziam parte da linguagem C original.
Protótipos permitem que C forneça uma verificação mais forte dos tipos. Protótipos de funções ajudam a detectar erros antes que eles ocorram. É verificado número de parâmetros, compatibilidade de tipos, entre outras.
Existem três tipos de declaração de protótipos:
Sintaxe Exemplo
tipo_função nome_função (); int pot();
tipo_função nome_função (lista_tipo_argumentos); int pot(int,int);
tipo_função nome_função (lista_tipo_nome_argumentos); int pot(int x, int y);
Retorno de Ponteiros
Ponteiros para variáveis não são variáveis e tampouco inteiros sem sinal. São endereços na memória. A forma geral é:
Sintaxe:
tipo_função *nome_função(lista_de_argumentos);
Classes de Armazenamento
A linguagem C possui quatro classes de armazenamento de variáveis:
· auto (automáticas)
· extern (externas)
· static (estáticas)
· register (em registradores)
auto
As variáveis declaradas nos exemplos anteriores só podem ser acessadas somente às funções onde estão declaradas. Tais variáveis são chamadas locais ou automáticas e elas são criadas quando a função é chamada e destruídas quando a função ou o bloco de código termina a sua execução.
As variáveis declaradas dentro de uma função são automáticas por padrão. A classe de variáveis automáticas pode ser explicitada usando-se a palavra auto. O código
Linguagem de Programação C
35
int main() {
auto int x;
...
}
é equivalente a
int main() {
int x;
...
}
static
Dentro de sua própria função ou arquivo, variáveis static são variáveis permanentes. Ao contrário das variáveis globais, elas não são reconhecidas fora de sua função ou arquivo, mas mantêm seus valores entre chamadas. O especificador static tem efeitos diferentes em variáveis locais e em variáveis globais.
Uma variável estática é inicializada uma única vez, no momento em que é criada.
O Exemplo 3.7 mostra o uso de uma variável estática. Note que a variável i foi declarada como static enquanto que j foi declarado normalmente (ou auto). A cada chamada, a variável j é inicializada com zero enquantoque a variável i mantém o último valor. Além disso, apesar de i ser inicializada, isto ocorre somente na primeira vez.
Exemplo 3.7
#include <stdio.h>
void imprimeValor() {
static int i = 10;
int j =0;
for (; j<5; j++)
printf("%3d\t",i++);
printf("\n");
}
int main() {
imprimeValor();
imprimeValor();
imprimeValor();
return 0;
}
Execução
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
Variáveis estáticas podem manter a contagem de quantas vezes uma função foi chamada.
Variáveis Locais static
Quando o modificador static é aplicado a uma variável local, o compilador cria armazenamento permanente para ela quase da mesma forma como cria armazenamento para uma variável global. Em termos simples, uma variável local static é uma variável local que retém seu valor entre chamadas. Mas ela só é reconhecida apenas no bloco em que está declarada.
Variáveis Globais static
Quando o modificador static é aplicado a uma variável global, o compilador cria uma variável que é reconhecida apenas no arquivo na qual a mesma foi declarada.
extern
Toda variável declarada fora de qualquer função têm a classe de armazenamento extern. Como pode-se somente uma única vez declarar uma variável global, ao usar diversos arquivos para um mesmo programa (por ser grande, por exemplo) deve-se utilizar a declaração extern nos outros arquivos onde a variável é utilizada. Se não proceder assim, o compilador acusará um erro de duplicidade de variável.
Exemplo 3.8
Arquivo 1
int x,y;
char ch;
int main() {
...
}
void func1() {
x = 123;
}
Arquivo 2
extern int x,y;
extern char ch;
void func22() {
x = y / 10;
}
void func23() {
y = 10;
}
No arquivo 2, a lista de variáveis globais foi copiada do arquivo 1 e o especificador extern foi adicionado às declarações. O especificador extern diz ao compilador que os tipos de variável e nomes que o seguem foram declarados em outro lugar. Isto é, o compilador não reserva um espaço de memória para essas variáveis declaradas com o especificador extern na certeza de estarem declaradas em outro módulo.
register
A classe de armazenamento register indica que a variável associada deve ser guardada fisicamente numa memória de acesso muito mais rápido, chamada registrador. No caso do IBM-PC pode ser colocado o tipo int e char associado a register pois o registrador tem apenas 16 bits.
Basicamente, variáveis register são usadas para aumentar a velocidade de processamento. Por exemplo, variáveis de uso freqüente como variáveis de laços e argumentos de funções.
Diretiva #define
A diretiva #define pode ser usada para definir constantes simbólicas com nomes apropriados. Por exemplo, a constante PI pode ser definida com o valor 3.14159.
#define PI 3.14159
Só pode ser escrito um comando destes por linha, e não há ponto-e-vírgula após qualquer diretiva do pré-processador.
Esta diretiva é usada para definir macros com argumentos.
#define AREA(x) (4*PI*x*x)
A declaração acima define a função AREA() a qual calcula a área de uma esfera. A vantagem desta declaração é a não tipagem do argumento x. Não deve haver espaços entre o nome da macro e seus identificadores.
Funções Recursivas
Uma função é dita recursiva quando se é definida dentro dela mesma. Isto é, uma função é recursiva quando dentro dela está presente uma instrução de chamada a ela própria.
Exemplo 10.9
// imprime uma frase invertida utilizando recursão
#include <stdio.h>
#include <conio.h>
void inverte()
int main() {
clrscr( );
inverte( );
return 0;
}
void inverte() {
char ch ;
if ((ch=getche( )) != ‘\r’ ) inverte();
scanf(“%c”,ch)
}
Toda função recursiva deve possuir uma condição que termina a recursividade, senão ela pode causar uma parada inesperada do sistema
Exercícios
1. Escreva um programa que receba como parâmetro um índice (float). Após, ler uma seqüência de números (a qual termina por 0) e exibir o seu valor multiplicado pelo índice. A função que transforma uma string em um float é atof(char *x).
2. Escreva uma função que receba um caractere como argumento e que retorne a letra maiúscula se a mesma for minúscula. funções: islower(int ch), toupper(int ch).
3. Existe um algoritmo interessante para se obter a raiz quadrada de um número quando ela é exata. Para isso, basta subtrair números ímpares consecutivos do número do qual se deseja retirar a raiz quadrada. O número de vezes será a raiz do número. Por exemplo:
No exemplo, subtraíram-se de 25 os 5 primeiros números ímpares consecutivos até que se chegasse 0. Assim, a raiz quadrada de 25 é 5. Escreva uma função que receba um inteiro n e retorne a raiz quadrada de n. Por exemplo, se a função receber 49, ele retornará 7. O calculo da raiz quadrada deverá ser feito usando o algoritmo acima, sem usar qualquer função pré-existente de alguma biblioteca C.
4. Seja o seguinte programa:
#include<stdio.h>
void x(int n){
int i, resto;
i = n;
do{
resto = i%16;
i=i/16;
switch(resto){
case 10: printf("A"); break;
case 11: printf("B"); break;
case 12: printf("C"); break;
case 13: printf("D"); break;
case 14: printf("E"); break;
case 15: printf("F"); break;
default: printf("%d", resto);
}
}while(i>0);
printf("\n");
}
void main(){
int N;
scanf("%d",&N);
x(N);
}
O que será escrito na tela, supondo que o valor fornecido para N seja 10846? Mostre o teste de mesa completo utilizado para determinar a saída.
5. Simule a execução do programa abaixo mostrando todas as mudanças de valores de variáveis e o resultado da impressão.
#include<stdio.h>
int perf(long int N){
long int i, divs=0;
for(i=1; i<= N/2; i++)
if (N%i == 0) divs = divs + i;
if (divs == N) return 1;
else return 0;
}
void main(){
long int x=14;
if (perf(x)==1) printf("%d é perfeito",x);
else printf("%d não é perfeito",x);
}
6. Escreva uma função em C que receba como argumentos a altura (alt) e o sexo de uma pessoa e retorne o seu peso ideal. Para homens, calcular o peso ideal usando a fórmula
e , para mulheres,
.
7. Escreva uma função em C com o seguinte protótipo
long int multiplicatorio(int i, int n)
A função deve retornar o multiplicatório de i a n. Por exemplo, a chamada
multiplicatorio(3,10)
retorna 1814400 (3×4×5×6×7×8×9×10).
8. Escreva uma função em C com o seguinte protótipo
long int somatório(int i, int n)
A função deve retornar o somatório de i a n. Por exemplo, a chamada
somatório(3,10)
retorna 52 (3+4+5+6+7+8+9+10).
9. Escreva uma função em C que receba dois números e retorne o maior deles.
10. A aceleração é a taxa de variação da velocidade em relação ao tempo, isto é, a razão entre a variação da velocidade e o intervalo de tempo. Matematicamente,
onde
é a variação da velocidade ou a velocidade final menos a velocidade inicial. Escreva uma função em C que receba como parâmetros a velocidade inicial, a velocidade final e o intervalo de tempo correspondente e retorne a aceleração. Mostre, também, uma função main() que chame essa função.
11. O valor de π/2 pode ser calculado pela seguinte série de produtos:
Escreva uma função em C que receba como argumento um número inteiro n e retorne o valor de π calculado através da série acima com n termos.
12. Um aço é classificado de acordo com o resultado de três testes, que devem verificar se ele satisfaz às seguintes especificações:
· Teste 1: conteúdo de carbono abaixo de 7%;
· Teste 2: dureza Rokwell maior que 50;
· Teste 3: resistência à tração maior do que 80.000 psi.
O aço recebe grau 10 se passar pelos três testes; 9, se passar apenas nos testes 1 e 2; 8, se passar no teste 1; e 7, se não passou nos três testes. Escreva uma função em C que o conteúdo de carbono (em %), a dureza Rokwell e a resistência à tração (em psi) de uma amostra de aço e retorne o grau obtido.
13. Escreva uma função em C que receba um número n e retorne 1 se a soma dos dígitos formantes de n for 10; 0 caso contrário. Por exemplo, se o valor de n recebido for 145 a função retorna 1.
14. Escreva uma função em C que recebaum numero e retorne seu fatorial. O fatorial de n é representado por n! sendo que
0! = 1
1! = 1
2! = 1×2 = 2
3! = 1×2×3 = 6
4! = 1×2×3×4 = 24
5! = 1×2×3×4×5 = 120
Ponteiros
Para uma boa utilização dos ponteiros deve-se compreender corretamente o seu uso. Existem três razões para isso: primeiro, ponteiros fornecem os meios pelos quais as funções podem modificar seus argumentos; segundo, eles são usados para suportar as rotinas de alocação dinâmica de C, e terceiro, o uso de ponteiros para aumentar a eficiência de certas rotinas.
Por ser um dos aspectos mais poderosos da linguagem também são os mais perigosos. Por erros no uso de ponteiros (como a não inicialização de ponteiros - ponteiros selvagens) podem provocar quebra do sistema.
Por definição, um ponteiro é uma variável que contém um endereço de memória. Esse endereço é normalmente uma posição de outra variável na memória.
Uma declaração de ponteiros consiste no tipo base, um "*" e o nome da variável. A forma geral é:
tipo *nome;
onde tipo é qualquer tipo válido em C e nome é o nome da variável ponteiro.
O tipo base do ponteiro define que tipo de variáveis o ponteiro pode apontar. Basicamente, qualquer tipo ponteiro pode apontar para qualquer lugar, na memória. Mas, para a aritmética de ponteiros é feita através do tipo base.
Os operadores utilizados são * e &, como já foi explicado na seção 6.6.
Expressões com Ponteiros
Nesta seção serão analisados alguns aspectos especiais de expressões com ponteiros.
Atribuição de Ponteiros
Como é o caso com qualquer variável, um ponteiro pode ser usado no lado direito de um comando de atribuição para passar seu valor para outro ponteiro.
Exemplo 4.1
#include <stdio.h>
void main() {
int x;
int *p1,*p2; /* declaração do ptr p1 e p2 com o tipo base int. */
p1 = &x;
p2 = p1;
printf(“%p”,p2); /* escreve o endereço de x, não seu valor */
return 0;
}
Agora, tanto p1 quanto p2 apontam para x. O endereço de x é mostrado usando o modificador de formato de printf() %p, que faz com que printf() apresente um endereço no formato usado pelo computador hospedeiro.
Aritmética de Ponteiros
Existem apenas duas operações aritméticas que podem ser usadas com ponteiros: adição e subtração. Os operadores permitidos no caso seriam: +, -, ++, --.
O incremento é sempre realizado do tamanho básico de armazenamento do tipo base. Isto é, se o tipo base for um inteiro e incrementarmos em uma unidade, o apontador apontará para o próximo inteiro (no caso do inteiro ocupar 2 bytes o incremento será de dois bytes), no caso de um caractere (char) será de um byte.
Exemplo 4.2
int *ptri=3000;
char *ptrc=4000;
float *ptrf=4000;
ptri++; /* ptri apontará para o endereço 3002 */
ptrc++; /* ptrc apontará para o endereço 4001 */
ptrf++; /* ptrf apontará para o endereço 5004 */
ptri = ptri - 10; /* ptri apontará para o endereço 2982 */
ptrc = ptrc - 10; /* ptrc apontará para o endereço 3991 */
ptrf = ptrf - 10; /* ptrf apontará para o endereço 4964 */
Além de adição e subtração entre um ponteiro e um inteiro, nenhuma outra operação aritmética pode ser efetuada com ponteiros. Isto é, não pode multiplicar ou dividir ponteiros; não pode aplicar os operadores de deslocamento e de mascaramento bit a bit com ponteiros e não pode adicionar ou subtrair o tipo float ou o tipo double a ponteiros.
Não se altera o valor de um ponteiro constante (ponteiro para um tipo de dado básico - int, float double, …), somente de um ponteiro variável (ponteiro de estruturas complexas - vetores, matrizes, strings, …).
Inicialização de Ponteiros
Após um ponteiro ser declarado, mas antes que lhe seja atribuído um valor, ele contém um valor desconhecido. Ao usar este ponteiro antes de inicializar, provavelmente provocará uma falha do programa ou até do sistema operacional.
Como um ponteiro nulo é assumido como sendo não usado, pode-se utilizar o ponteiro nulo para fazer rotinas fáceis de codificar e mais eficientes. Por exemplo, pode-se utilizar um ponteiro nulo para marcar o fim de uma matriz de ponteiros. Uma rotina que acessa essa matriz sabe que chegará ao final ao encontrar o valor nulo. A função search(), mostrada no Exemplo 4.3, ilustra esse tipo de abordagem.
Exemplo 4.3
/* procura um nome */
search(char *p[], char *name)
{
register int t;
for (t=0;p[t];++t)
if(!strcmp(p[t],name)) return t;
return -1; /* não encontrado */
}
O laço for dentro de search() é executado até que seja encontrada uma coincidência ou um ponteiro nulo. Como o final da matriz é marcado com um ponteiro nulo, a condição de controle do laço falha quando ele é atingido.
Outra utilização de inicialização de ponteiros é a inicialização de strings. Isto pode ser levado como uma variação no tema de inicialização usado na variável argv.
Exemplo 4.4
char *p= “alo mundo \n”;
O ponteiro p (Exemplo 4.4) não é uma matriz, mas como o compilador C cria uma tabela de strings, a constante string é colocada em tal tabela sendo que a mesma pode ser utilizada em todo o programa como se fosse uma string comum. Por isso, inicializar uma matriz de strings usando ponteiros aloca menos memória que a inicialização através de matriz.
Exemplo 4.5
#include <stdio.h>
#include <string.h>
char *p=“alo mundo”;
int main() {
register int t;
printf(p);
for (t=strlen(p) - 1; t > -1; t--) printf(“%c”,p[t]);
return 0;
}
Comparação de Ponteiros
É possível comparar dois ponteiros em uma expressão relacional.
Exemplo 4.6
if (p<q) printf(“p aponta para uma memória mais baixa que q /n”);
Geralmente, a utilização de comparação entre ponteiros é quando os mesmos apontam para um objeto comum. Exemplo disto é a pilha, onde é verificado se os ponteiros de início e fim da pilha estão apontando para a mesma posição de memória, significando pilha vazia.
Ponteiros e Matrizes
Existe uma estreita relação entre matrizes e ponteiros. Pois C fornece dois métodos para acessar elementos de matrizes: aritmética de ponteiros e indexação de matrizes. Aritmética de ponteiros pode ser mais rápida que indexação de matrizes. Normalmente utilizam-se ponteiros para acessar elementos de matrizes devido a velocidade de acesso.
Exemplo 4.7
char str[80], *p1;
p1 = str;
Para acessar a string str pode-se utilizar estes dois mecanismos
str[4] /* indexação de matrizes */
ou
*(p1 + 4) /* aritmética de ponteiros */
Os dois comandos devolvem o quinto elemento.
*(matriz + índice) é o mesmo que matriz[índice].
Para uma melhor compreensão ou facilidade de programação as funções de indexação trabalham com ponteiros (como mostra o Exemplo 4.8 a implementação da função puts()).
Exemplo 4.8
/* Indexa s como uma matriz */
void put(char *s) {
register int t;
for (t=0;s[t]; ++t) putchar(s[t]);
}
/* Acessa s como um ponteiro */
void put(char *s) {
while (*s) putchar(*s++);
}
No caso da passagem de parâmetros é possível tratar uma matriz como se fosse um ponteiro.
Exemplo 4.9
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void le_tab(int *p) {
register int i;
for(i=0; i<20; i++)
scanf(“%d”,(p+i));
}
void mostra_tab(int *p) {
register int i;
for(i=0; i<20; i++)
printf(“%d”,*(p+i));
}
void main(void) {
int mat[20];
le_tab(mat);
mostra_tab(mat);
}
Matrizes de Ponteiros
Ponteiros podem ser organizados em matrizes como qualquer outro tipo de dado. A declaração de uma matriz de ponteiros int, de tamanho 10, é
int *x[10];
Para atribuir o endereço de uma variável inteira, chamada var, ao terceiro elemento da matriz de ponteiros, deve-se escrever
x[2] = &var;
para encontrar o valor de var, escreve-se
*x[2];
Se for necessário passar uma matriz de ponteiros para uma função, pode ser usado o mesmo método que é utilizado para passar outras matrizes - simplesmente chama-se a função com o nome da matriz sem qualquer índice.
Exemplo 4.10
void display_array(int *q[]) {
register int t;
for (t=0; t<10; t++)
printf(“%d”,*q[t]);
}
Lembre-se de que q não é um ponteiro para inteiros; q é um ponteiro para uma matriz de ponteiros para inteiros. Portanto, é necessário declarar o parâmetroq como uma matriz de ponteiros para inteiros, como é mostrado no Exemplo 4.10. Isto é, não é uma passagem de parâmetros por referência por dois motivos: primeiro, matriz como argumento de função é automaticamente passada por referência por questão da implementação da linguagem, e segundo, é uma matriz de ponteiros e conseqüentemente sua declaração é caracterizada pelo asterisco na frente do nome da variável.
Matrizes de ponteiros são usadas normalmente como ponteiros de strings como, por exemplo, o argumento da linha de comandos argv.
Acessando partes de Matrizes como vetores
A linguagem C trata partes de matrizes como matrizes. Mais especificamente, cada linha de uma matriz de duas dimensões pode ser considerada como uma matriz de uma dimensão. Isto pode ser muito útil no tratamento de matrizes. O Exemplo 4.11 mostra a atribuição de uma linha da matriz nome para um ponteiro.
Exemplo 4.11
int main() {
static char nome[10][10];
char *p[10];
for (i = 0;i<10;i++)
p[i] = nome[i];
ordena(p);
return 0;
}
Indireção Múltipla
Indireção múltipla é uma situação onde o ponteiro aponta para outro ponteiro e que o mesmo aponta para um valor final. A Figura 4.1 mostra o conceito de indireção múltipla.
Ponteiro Variável
endereço valor
Indireção Simples
Ponteiro Ponteiro Variável
endereço endereço valor
Indireção Múltipla
Figura 4.1 Indireção simples e múltipla
A indireção múltipla pode ser levada a qualquer dimensão desejada, mas raramente é necessário mais de um ponteiro para um ponteiro.
Não confunda indireção múltipla com listas encadeadas.
A declaração deste tipo de variável é feita colocando-se um * adicional em frente ao nome da variável, como mostra o Exemplo 4.12. Tal exemplo mostra a declaração da variável ptrptrint como um ponteiro para um ponteiro do tipo int.
Exemplo 4.12
int **ptrptrint;
Para acessar o valor final apontado indiretamente por um ponteiro a um ponteiro, você deve utilizar o operador asterisco duas vezes, como no Exemplo 4.13:
Exemplo 4.13
#include <stdio.h>
int main() {
int x, *p, **q;
x = 10;
p = &x;
q = &p;
printf(“%d”, **q); /* imprime o valor de x */
return 0;
}
Ponteiros para Funções
A linguagem C permite apontadores para funções. Isto é permitido pois toda função tem uma posição física na memória que pode ser atribuída a um ponteiro. Portanto, um ponteiro de função pode ser usado para chamar uma função.
O endereço de uma função é obtido usando o nome da função sem parênteses ou argumentos. Mas para declarar este tipo de apontador tem que se seguir uma sintaxe especial como mostra o Exemplo 4.14.
Exemplo 4.14
#include <stdio.h>
#include <string.h>
void check(char *a, char *b, int (*cmp)());
int main() {
char s1[80], s2[80];
int (*p)();
p = strcmp;
gets(s1);
gets(s2);
check(s1,s2,p);
return 0;
}
void check(char *a, char *b, int (*cmp)()) {
if (!(*cmp) (a, b)) printf(“igual”);
else printf(“diferente”);
}
Quando a função check() é chamada, dois ponteiros para caractere e um ponteiro para uma função são passados como parâmetros. Dentro da função check(), note como o ponteiro para função é declarado, pois esta é a forma correta de se declarar este tipo de ponteiro. Os parênteses ao redor de *cmp são necessários para que o compilador interprete o comando corretamente.
Outra forma de fazer a chamada é mostrada no Exemplo 4.14 o qual dispensa o uso de um ponteiro adicional.
check(s1, s2, strcmp);
Uma das grandes utilidades é o uso de drivers de dispositivos (placas de som, placas de vídeo, modems, entre outros) que fornecem rotinas de tratamento para aquele hardware específico. Onde o programador lê o arquivo do driver para a memória e o executa de acordo com as especificações do fabricante.
Outra utilidade é o programador poder enviar a função que se apropria para a comparação por exemplo. Isto é, no caso de strings pode-se pensar em um comparador de strings genérico onde como terceiro parâmetro é enviado a função que vai realizar a comparação. Antes da chamada da função genérica pode verificar se a string é composta por caracteres alfanuméricos (através da função isalpha()) e enviar a função strcmp(), caso contrário uma função que realize uma comparação de números inteiros (nesta função conterá a conversão das strings em um inteiro (função atoi()).
Mais Sobre declarações de Ponteiros
As declarações de ponteiros podem ser complicadas e é necessário algum cuidado na sua interpretação. principalmente em declarações que envolvem funções e matrizes. Assim, a declaração
int *p(int a);
indica uma função que aceita um argumento inteiro e retorna um ponteiro para um inteiro. Por outro lado, a declaração
int (*p)(int a);
indica um ponteiro para uma função que aceita um argumento inteiro e retorna um inteiro. Nessa última declaração, o primeiro par de parênteses é usado para o aninhamento e o segundo par, para indicar uma função.
A interpretação de declarações mais complexas pode ser extremamente mais trabalhosa. Por exemplo, considere a declaração
int *(*p)(int (*a)[]);
Nessa declaração, (*p)(..) indica um ponteiro para uma função. Por isso, int *(*p)(...) indica um ponteiro para uma função que retorna um ponteiro para um inteiro. Dentro do último par de parênteses (a especificação dos argumentos da função), (*a)[] indica um ponteiro para um vetor. Assim int (*a)[] representa um ponteiro para um vetor de inteiros. Juntando todas as peças, (*p)(int (*a)[]) representa um ponteiro para uma função cujo argumento é um ponteiro para um vetor de inteiros. E, finalmente, a declaração original representa um ponteiro para uma função que aceita um ponteiro para um vetor de inteiros como argumento e devolve um ponteiro para um inteiro.
Se logo após um identificador existir um “abre parênteses” indica que o identificador representa uma função e os colchetes representam uma matriz. Os parênteses e colchetes têm maior precedência do que qualquer operador.
A seguir será mostrado várias declarações envolvendo ponteiros e seu significado.
int *p;
p é um ponteiro para um valor inteiro
int *p[10];
p é uma matriz de ponteiros com 10 elementos para valores inteiros
int (*p)[10];
p é um ponteiro para uma matriz de inteiros com 10 elementos
int *p(void);
p é uma função que retorna um ponteiro para um valor inteiro
int *p(char *a);
p é uma função que aceita um argumento que é um ponteiro para um caractere e retorna um ponteiro para um valor inteiro
int (*p)(char *a);
p é m ponteiro para uma função que aceita um argumento que é um ponteiro para um caractere e retorna um valor inteiro
int (*p(char *a))[10];
p é uma função que aceita um argumento que é um ponteiro para um caractere e retorna um ponteiro para uma matriz inteira de 10 elementos
int p(char (*a)[]);
p é uma função que aceita um argumento que é um ponteiro para uma matriz de caractere e retorna um valor inteiro
int p(char *a[]);
p é uma função que aceita um argumento que é uma matriz de ponteiros para caractere e retorna um valor inteiro
int *p(char a[]);
p é uma função que aceita um argumento que é uma matriz de caractere e retorna um ponteiro para um valor inteiro
int *p(char (*a)[]);
p é uma função que aceita um argumento que é um ponteiro para uma matriz de caractere e retorna um ponteiro para um valor inteiro
int *p(char *a[]);
p é uma função que aceita um argumento que é uma matriz de ponteiros para caracteres e retorna um ponteiro para um valor inteiro
int (*p)(char (*a)[]);
p é um ponteiro para uma função que aceita um argumento que é um ponteiro para uma matriz de caractere e retorna um valor inteiro
int *(*p)(char (*a)[]);
p é um ponteiro para uma função que aceita um argumento que é um ponteiro para uma matriz de caractere e retorna um ponteiro para um valor inteiro
int *(*p)(char *a[]);
p é um ponteiro para uma função que aceita um argumento que é uma matriz de ponteiros para caracteres e retorna um ponteiro para um valor inteiro
int (*p[10])(char a);
p é uma matriz de ponteiroscom 10 elementos para funções; cada função aceita um argumento que é um caractere e retorna um valor inteiro
int *(*p[10])(char a);
p é uma matriz de ponteiros com 10 elementos para funções; cada função aceita um argumento que é um caractere e retorna um ponteiro para um valor inteiro
int *(*p[10])(char *a);
p é uma matriz de ponteiros com 10 elementos para funções; cada função aceita um argumento que é um ponteiro para um caractere e retorna um ponteiro para um valor inteiro
Exercícios
1. Como referenciar mat[x][y] em notação de ponteiros.
2. Qual será a saída deste programa?
int main() {
int i=5;
int *p;
p = &i;
printf(“%u %d %d %d %d \n”, p, *p+2,**&p,3**p,**&p+4);
return 0;
}
3. Escreva uma função que inverta a ordem dos caracteres de uma string.
4. Crie uma função que receba como parâmetro uma matriz de ponteiros para strings e devolve a matriz ordenada.
5. Faça uma função que receba um ponteiro para uma matriz e que realize a ordenação da mesma.
6. Faça a declaração de uma função (nome: teste) que receba um ponteiro para uma função que possui dois argumentos (int e char) e retorne um ponteiro para um float.
7. Faça a declaração e inicialização de uma matriz de ponteiros para os dias da semana.
8. Faça uma função que receba uma matriz de ponteiros para caracteres e realize a ordenação alfabética da mesma.
Estruturas e Uniões
A linguagem C permite criar tipos de dados definíveis pelo usuário de cinco formas diferentes. A primeira é estrutura, que é um agrupamento de variáveis sobre um nome e, algumas vezes, é chamada de tipo de dado conglomerado. O segundo tipo definido pelo usuário é o campo de bit, que é uma variação da estrutura que permite o fácil acesso aos bits dentro de uma palavra. O terceiro é a união, a qual permite que a mesma porção da memória seja definida por dois ou mais tipos diferentes de variáveis. Um quarto tipo de dado é a enumeração, que é uma lista de símbolos, como foi visto na seção 1.5. O último tipo definido pelo usuário é criado através do uso de typedef e define um novo nome para um tipo existente.
Estruturas
O tipo estruturado struct possibilita a criação de estruturas de dados complexas, isto é, pode-se obter estruturas que contenham mais de um tipo de dado. Tal estrutura é conhecida em outras linguagens como registros.
Cada elemento que compõe a estrutura (chamado campo) pode ser acessado individualmente, assim como a estrutura pode ser acessada como um todo. Em C, a declaração de uma estrutura é feita da seguinte forma:
struct [nome_struct] {
tipo var1;
tipo var2;
…
tipo varN;} [nome_var];
Deve-se encerrar com um ponto-e-vírgula a declaração porque a definição de estrutura é na realidade uma instrução C.
A declaração de estruturas pode se apresentar de diversas formas. Tais como:
struct {
char nome[30];
int idade;
int codigo;
float saldo;
} conta1, conta2;
Na declaração acima, o nome_struct não é utilizado, pois esta estrutura será utilizada pelas variáveis de estrutura conta1 e conta2. Para utilizar esta estrutura na definição de outras variáveis tem-se que declará-las juntas com a definição da estrutura. No caso de um programa que utilize esta estrutura para passar parâmetros, declarar variáveis locais, entre outros, a linguagem permite a criação de rótulos de estruturas (nome_struct).
Exemplo 5.1
struct cad_conta {
char nome[30];
int idade;
int codigo;
float saldo;
} conta1, conta2;
Como mostra Exemplo 5.1, foram declaradas as variáveis conta1 e conta2 como sendo uma estrutura do tipo cad_conta. Quando rotula-se a estrutura pode-se omitir a declaração das variáveis, como é mostrado no Exemplo 5.2.
Exemplo 5.2
struct cad_conta {
char nome[30];
int idade;
int codigo;
float saldo;
};
Para usar esta estrutura em outras declarações deve-se especificar desta forma:
struct cad_conta conta1, conta2;
As estruturas seguem o padrão do escopo de variáveis, isto é, se a declaração estiver contida numa função, a estrutura tem escopo local para aquela função; se a declaração estiver fora de todas as funções, ela terá um escopo global.
Para acessar um campo específico de uma struct utiliza-se o operador . (ponto).
Exemplo 5.3
conta1.saldo = 0;
conta1.codigo = 0;
strcpy(conta1.nome,”Joao”);
conta1.idade = 21;
É permitida a atribuição entre struct. Neste caso todos os campos são copiados.
Exemplo 5.4
conta2 = conta1;
Inicializando Estruturas
Uma estrutura só pode ser inicializada se pertencer às classes static ou extern. Observe que a classe de uma estrutura é dada pelo ponto em que as variáveis foram declaradas e não pelo ponto onde a estrutura foi definida.
Da mesma forma que os vetores, as estruturas são inicializadas com uma lista de valores (cada um correspondente a um campo de estrutura) entre chaves e separados por vírgulas.
Exemplo 5.5
struct cad_conta {
char nome[30];
int idade;
int codigo;
float saldo;
};
int main() {
static struct cad_conta
conta1 = {“Andre”, 23, 9507, 1567.89},
conta2 = {“Carlos”, 33, 9678, 1000.59};
…
}
Estruturas Aninhadas
Como os campos da estrutura podem ser de qualquer tipo, também é permitido o uso de estruturas na declaração.
Exemplo 5.6
struct data {
int dia;
char mes[10];
int ano;
};
struct func {
char nome[20];
int codigo;
float salario;
struct data nascimento;
};
int main() {
static struct func
funcionario = {“Marcio”, 1234, 3743.44, {10, “Janeiro”, 1967}},
gerente = {“Jose”, 456, 5634.28, {18, “Marco”, 1950}};
return 0;
}
Observe a inicialização das variáveis. A estrutura é inicializada também com uma lista de valores entre chaves e separados por vírgulas. O acesso a um campo de uma estrutura aninhada é feito na forma:
funcionário.nascimento.dia = 10;
strcpy(gerente.nascimento.mes,”Abril”);
Estruturas e funções
Em versões mais antigas de compiladores C, as estruturas não podiam ser usadas em passagem de parâmetros por valor para funções. Isto se devia a razões de eficiência, uma vez que uma estrutura pode ser muito grande e a cópia de todos os seus campos para a pilha poderia consumir um tempo exagerado. Desta forma, as estruturas eram obrigatoriamente passadas por referência, usando-se o operador de endereço (&).
No Turbo C e outros compiladores mais recentes, a responsabilidade da decisão fica a cargo do programador. Assim, uma função pode passar ou retornar uma estrutura.
Exemplo 5.7
struct cad_conta {
char nome[30];
int idade;
int codigo;
float saldo;
};
int main() {
static struct cad_conta conta1;
conta1 = ins_conta();
lista(conta1);
…
}
struct cad_conta ins_conta() {
struct cad_conta aux;
gets(aux.nome);
scanf(“%d”, &aux.idade);
scanf(“%d”, &aux.codigo);
scanf(“%f”, &aux.saldo);
return(aux);
}
void lista(struct cad_conta aux) {
printf(“Nome : %s\n”,aux.nome);
printf(“Idade : %d\n”, aux.idade);
printf(“Codigo : %d\n”, aux.codigo);
printf(“Saldo : %.2f\n”, aux.saldo);
}
Vetor de Estruturas
A criação de tabela de estruturas mantém a sintaxe normal de definição de matrizes, como é mostrada no Exemplo 5.8:
Exemplo 5.8
struct cad_conta {
char nome[30];
int idade;
int codigo;
float saldo;
};
int main() {
int i
static struct cad_conta conta[10]=
{ {“Andre”, 23, 9507, 1567.89},
{“Carlos”, 33, 9678, 1000.59},
...
};
for (i=0;i<10;i++) {
printf(“Nome : %s\n”,conta[i].nome);
printf(“Idade : %d\n”, conta[i].idade);
printf(“Codigo : %d\n”, conta[i].codigo);
printf(“Saldo : %.2f\n”, conta[i].saldo);
}
…
}
Ponteiros para Estruturas
C permite ponteiros para estruturas exatamente como permite ponteiros para outros tipos de variáveis. No entanto, há alguns aspectos especiais de ponteiros de estruturas.
Como outros ponteiros, declara-se colocando um * na frente do nome da estrutura. No Exemplo 5.9 declara-se ptr_cta como um apontador da estrutura previamente definida cad_conta.
Exemplo 5.9
struct cad_conta *ptr_cta;
Há dois usos primários para ponteiros de estrutura:gerar uma chamada por referência para uma função e criar estruturas de dados dinâmicas (listas, pilhas, filas, entre outras) utilizando-se do sistema de alocação de C.
Na forma de acessar os elementos ou campos de uma estrutura usando um ponteiro para a estrutura, deve-se utilizar o operador -> (seta). A seta é usada sempre no caso de apontador de estruturas. No Exemplo 5.10 é mostrada a declaração, atribuição e utilização de ponteiros de estruturas.
Exemplo 5.10
struct cad_conta {
char nome[30];
int idade;
int codigo;
float saldo;
} conta;
int main() {
struct cad_conta *ptr;
ptr = &conta; /* o ptr recebe o end. da estrutura */
ptr->idade = 23;
ptr->codigo = 1000;
return 0;
}
Campos de Bits
Ao contrário das linguagens de computador, C tem um método intrínseco para acessar um único bit dentro de um byte. Isso pode ser útil por diversas razões:
· Se o armazenamento é limitado, você pode armazenar diversas variáveis Booleanas (verdadeiro/falso) em um byte.
· Certos dispositivos transmitem informações codificadas nos bits dentro de um byte.
· Certas rotinas de criptografia precisam acessar os bits dentro de um byte.
Para acessar os bits, C usa um método baseado na estrutura. Um campo de bits é, na verdade, apenas um tipo de elemento de estrutura que define o comprimento, em bits, do campo. A forma geral de uma definição de campo de bit é:
struct nome {
tipo var1 : comprimento;
tipo var2 : comprimento;
…
tipo varN : comprimento;
} [lista_de_variaveis];
Um campo de bit deve ser declarado como int, unsigned ou signed. Campos de bit de comprimento 1 devem ser declarados como unsigned, porque um único bit não pode ter sinal. (Alguns compiladores só permitem campos do tipo unsigned).
Um exemplo de campos de bits é a comunicação via serial que devolve um byte de estado organizado como mostra a Tabela 5.1.
Tabela 5.1: Estado da comunicação serial.
Bit
Significado quando ligado
0
alteração na linha clear-to-send
1
alteração em data-set-ready
2
borda de subida da portadora detectada
3
alteração na linha de recepção
4
clear-to-send
5
data-set-ready
6
chamada do telefone
7
sinal recebido
Pode-se representar a informação em um byte de estado utilizando o seguinte campo de bits:
Exemplo 5.11
struct status_type {
unsigned delta_cts : 1;
unsigned delta_dsr : 1;
unsigned tr_edge : 1;
unsigned delta_rec : 1;
unsigned cts : 1;
unsigned dsr : 1;
unsigned ring : 1;
unsigned rec_line : 1;
} status;
Para atribuir um valor a um campo de bit, simplesmente utiliza-se a forma para atribuição de outro tipo de elemento de estrutura.
status.ring = 0;
Não é necessário dar um nome a todo campo de bit. Isto torna fácil alcançar o bit que se deseja acessar, contornando os não usados. Por exemplo, se apenas cts e dtr importam, pode-se declarar a estrutura status_type desta forma:
struct status_type {
unsigned : 4;
unsigned cts : 1;
unsigned dsr : 1;
} status;
Além disso, nota-se que os bits após dsr não precisam ser especificados se não são usados.
Variáveis de campo de bit têm certas restrições:
· Não pode obter o endereço de uma variável de campo de bit.
· Variáveis de campo de bit não podem ser organizadas em matrizes.
· Não pode ultrapassar os limites de um inteiro.
· Não pode saber, de máquina para máquina, se os campos estarão dispostos da esquerda para a direita ou da direita para a esquerda.
· Em outras palavras, qualquer código que use campos de bits pode ter algumas dependências da máquina.
Finalmente, é válido misturar elementos normais de estrutura com elementos de campos de bit. O Exemplo 5.12 define um registro de um empregado que usa apenas um byte para conter três informações: o estado do empregado, se o empregado é assalariado e o número de deduções. Sem o campo de bits, essa variável ocuparia três bytes.
Exemplo 5.12
struct emp {
struct addr endereco;
float salario;
unsigned ativo : 1; /* ocioso ou ativo */
unsigned horas : 1; /* pagamento por horas */
unsigned deducao : 3; /* deduções de imposto */
};
Uniões
Uma união é um tipo de dado que pode ser usado de muitas maneiras diferentes. Por exemplo, uma união pode ser interpretada como sendo um inteiro numa operação e um float ou double em outra. Embora, as uniões possam tomar a aparência de uma estrutura, elas são muito diferentes.
Uma união pode conter um grupo de muitos tipos de dados, todos eles compartilhando a mesma localização na memória. No entanto, uma união só pode conter informações de um tipo de dados de cada vez. Para criar uma união utiliza-se a seguinte sintaxe:
union [nome_union] {
tipo var1;
tipo var2;
…
tipo varN;
} [nome_var];
Deve-se encerrar com um ponto-e-vírgula a declaração porque a definição de união é na realidade uma instrução C. A declaração de uniões pode se apresentar de diversas formas. Tais como:
Exemplo 5.13
union {
char c;
int i;
double d;
float f;
} data;
Na declaração acima, o nome_union não é utilizado pois esta união será utilizada pela variável data. Para utilizar esta união na definição de outras variáveis tem-se que declará-las juntas com a definição da união. No caso de um programa que utilize esta união em várias partes do programa a linguagem C permite a criação de rótulos de estruturas (nome_union).
Exemplo 5.14
union tipos {
char c;
int i;
double d;
float f;
} data;
Como mostra o Exemplo 5.14, foi declarada a variável data como sendo uma união do tipo tipos. Quando rotula-se a união pode-se omitir a declaração das variáveis, como é mostrado no Exemplo 5.15:
Exemplo 5.15
union tipos {
char c;
int i;
double d;
float f;
};
Para usar esta união em outras declarações deve-se especificar desta forma:
union tipos data1, data2;
As estruturas seguem o padrão do escopo de variáveis, isto é, se a declaração estiver contida numa função, a estrutura tem escopo local para aquela função; se a declaração estiver fora de todas as funções, ela terá um escopo global.
Para acessar um campo específico de uma union utiliza-se o operador . (ponto). Pode-se declarar estruturas dentro de uniões.
Exemplo 5.16
struct so_int {
int i1,i2;
};
struct so_float {
float f1,f2;
};
union {
struct so_int i;
struct so_float f;
} teste;
int main() {
teste.i.i1 = 2;
teste.i.i2 = 3;
printf(“i1 = %-3d i2 = %-3d\n”,teste.i.i1,teste.i.i2);
teste.f.f1 = 2.5;
teste.f.f2 = 3.5;
printf(“f1 = %.1f f2 = %.1f\n”,teste.f.f1,teste.f.f2);
return 0;
}
Sizeof()
Com uso de estruturas, uniões e enumerações pode-se utilizá-las para a criação de variáveis de diferentes tamanhos e que o tamanho real dessas variáveis pode mudar de máquina para máquina. O operador unário sizeof() calcula o tamanho de qualquer variável ou tipo e pode ajudar a eliminar códigos dependentes da máquina de seus programas.
Exemplo 5.17
union tipos {
char c;
int i;
double d;
float f;
} data;
O sizeof(data) é 8. No tempo de execução, não importa o que a união data está realmente guardando. Tudo o que importa é o tamanho da maior variável que pode ser armazenada porque a união tem de ser do tamanho do seu maior elemento.
Typedef
A linguagem C permite que defina-se explicitamente novos nomes aos tipos de dados, utilizando a palavra-chave typedef. Não há criação de uma nova variável, mas sim, definindo-se um novo nome para um tipo já existente. Serve para uma boa documentação ou até tornar os programas dependentes de máquina um pouco mais portáteis. A forma geral de um comando typedef é
typedef tipo nome;
Por exemplo, poderia ser criado um novo nome para char utilizando
typedef char boolean;
Esse comando diz ao compilador para reconhecer boolean como outro nome para char. Assim, para se criar uma variável char, usando boolean
boolean ok;
Também é válida a redefinição, isto é, utilizar um novo nome para um nome atribuído a um dado previamente estabelecido.
Exemplo 5.18
#include <stdio.h>
typedef char boolean;
typedef boolean bool;
int main() {
boolean a;
bool b;
a = 1;
b = 2;
printf("%d%d",a,b);
return 0;
}
A declaração typedef é usado também para definir tipos estruturados (struct e union) para facilitar a nomenclatura dos tipos na declaração de variáveis.
Exemplo 5.19
typedef struct conta {
char nome[30];
int idade;
int codigo;
float saldo;
} cad_conta;
int main() {
cad_conta *ptr;
ptr = &conta;
ptr->idade = 23;
ptr->codigo = 1000;
return 0;
}
ou
struct conta {
char nome[30];
int idade;
int codigo;
float saldo;
};
typedef struct conta cad_conta;
int main() {
cad_conta *ptr;
ptr = &conta;
ptr->idade = 23;
ptr->codigo = 1000;
return 0;
}
Exercícios
1. Faça um programa que leia os dados de 10 clientes de um banco e após leia 100 conjuntos de 3 valores:
· código de operação - 0 depósito, 1 - retirada,
· valor da operação
· código do cliente.
Realize as movimentações nas contas correspondentes e ao final escreva o nome e saldo de cada cliente.
2. Faça um programa de cadastro de clientes que contenham as seguintes opções: incluir, alteração, excluir e consultar por código ou por nome. O cadastro deve ser da seguinte forma:
· nome (30 caracteres);
· código (0 a 255);
· idade(char);
Alocação Dinâmica
Programas consistem em duas coisas: algoritmos e estruturas de dados. Um bom programa é uma combinação de ambos. A escolha e a implementação de uma estrutura de dados são tão importantes quanto as rotinas que manipulam os dados.
Para a manipulação de dados é utilizado mecanismos que auxiliam tanto na forma de como é armazenado ou recuperado. Existem vários mecanismos que realizam este tipo de processamento. Abaixo estão listados alguns mecanismos básicos:
·
· Listas
· Pilhas
· Filas
· Árvores
Cada um destes mecanismos pode ter variações de acordo com a política de processamento (armazenamento/recuperação). Neste capítulo será abordado com mais ênfase as listas encadeadas, porque serão como base para a construção dos demais.
Mapa de memória
Quando um programa em C é executado, quatro regiões de memória são criadas: código do programa, variáveis globais, heap e pilha, como ilustrado Figura 6.1. A região da pilha é a porção de memória reservada para armazenar o endereço de retorno das chamadas de funções, argumentos de funções e variáveis locais, o estado atual da CPU. A pilha cresce de cima para baixo, isto é, do endereço mais alto para o mais baixo. O heap é a região utilizada para uso do programador através das funções de alocação dinâmica. Como o heap cresce de baixo para cima (endereço mais baixo par o mais alto), pode haver uma colisão entre as duas regiões, o que causa uma falha no programa. Mas isto pode ocorrer somente quando determinados modelos de memória são utilizados para executar o programa.
Figura 6.1: Mapa de memória de um programa em C
Funções de Alocação dinâmica em C
O padrão C ANSI define apenas quatro funções para o sistema de alocação dinâmica: calloc(), malloc(), free(), realloc(). No entanto, serão estudadas, além das funções descritas, algumas funções que estão sendo largamente utilizadas.
O padrão C ANSI especifica que os protótipos para as funções de alocação dinâmica definidas pelo padrão estão em STDLIB.H. Entretanto, tais funções estão especificadas na biblioteca ALLOC.H, onde encontram-se mais funções de alocação dinâmica.
O padrão C ANSI especifica que o sistema de alocação dinâmica devolve ponteiros void, que são ponteiros genéricos, podendo apontar para qualquer objeto. Porém, alguns compiladores mais antigos devolvem ponteiros para char. Nesse caso, deve-se usar um cast quando atribuir a ponteiros de tipos diferentes.
malloc()
Esta função devolve um ponteiro para o primeiro byte de uma região de memória de tamanho size que foi alocada do heap. No caso em que não houver memória suficiente, a função devolve um ponteiro nulo.
Cuidado! Ao usar um ponteiro nulo, pode causar uma falha no programa.
Sintaxe.:
void *malloc(size_t size);
Onde size_t pode ser considerado um inteiro sem sinal e size é o número de bytes de memória que se quer alocar. Essa função devolve um ponteiro void, como mostra a sintaxe, portanto pode-se atribuir a qualquer tipo de ponteiro.
Para assegurar a portabilidade de um programa que utilize a alocação dinâmica, faz-se necessário a utilização da função sizeof().
Exemplo 6.1:
/* Esta função aloca memória suficiente para conter uma
estrutura do tipo addr */
struct addr {
char nome[40];
char rua[40];
char cidade[40];
char estado[3];
char cep[10];
};
struct addr *get_struct(void) {
struct addr *p;
if ((p=(struct addr*)malloc(sizeof(addr)))==NULL)
{
printf(“ erro de alocação - abortando”);
exit(1);
}
return p;
}
O fragmento do código mostra a alocação de 1000 bytes de memória.
char *p;
p = (char*)malloc(1000);
No fragmento abaixo é alocado memória suficiente para 50 inteiros.
int *p;
p = (int*)malloc(50 * sizeof(int));
O compilador deve conhecer duas informações sobre qualquer ponteiro: o endereço da variável apontada e seu tipo. Por isso, precisa-se fazer uma conversão de tipo (cast) do valor retornado por malloc(), já que o mesmo retorna um void. Portanto, no Exemplo 6.1 deve-se indicar ao compilador que o valor retornado por malloc() é do tipo ponteiro para struct addr.
p=(struct addr*) malloc(sizeof(addr))
Este tipo de conversão deve ser realizado em todas as funções de alocação como calloc(), realloc() e malloc().
calloc()
Esta função devolve um ponteiro para o primeiro byte de uma região de memória de tamanho size * num que foi alocada do heap. No caso em que não houver memória suficiente, a função devolve um ponteiro nulo.
Sintaxe.:
void *calloc(size_t num, size_t size);
Onde size_t pode ser considerado um inteiro sem sinal e size é o número de bytes de memória que se quer alocar. Essa função devolve um ponteiro void, como mostra a sintaxe, portanto pode-se atribuir a qualquer tipo de ponteiro.
Para assegurar a portabilidade de um programa que utilize a alocação dinâmica, faz-se necessário a utilização da função sizeof().
A diferença entre calloc() e malloc() é que a primeira aloca a memória e inicializa-a com zeros.
Exemplo 6.2
/* Esta função aloca memória suficiente para conter um
vetor de 100 elementos */
#include <stdlib.h>
#include <stdio.h>
float *get_mem() {
float *p;
p=(float*)calloc(100, sizeof(float));
if (!p) {
printf(“ erro de alocação - abortando”);
exit(1);
}
return p;
}
No fragmento abaixo é alocado memória suficiente para 50 inteiros.
int *p;
p = (int*)calloc(50,sizeof(int));
free()
Esta função devolve ao heap a memória apontada por ptr, tornando a memória disponível para alocação futura.
A função free() deve ser chamada somente com um ponteiro que foi previamente alocado com uma das funções do sistema de alocação dinâmica. A utilização de um ponteiro inválido na chamada provavelmente destruirá o mecanismo de gerenciamento de memória e provocará uma quebra do sistema.
Exemplo 6.3
#include <string.h>
#include <stdio.h>
#include <alloc.h>
int main() {
char *str;
/* aloca memória para uma string */
str = (char*)malloc(10);
/* copia "Hello" para a string */
strcpy(str, "Hello");
/* mostra a string */
printf("String: %s\n", str);
/* libera a memória */
free(str);
return 0;
}
realloc()
Esta função modifica o tamanho da memória previamente alocada apontada por ptr para aquele especificado por size. O valor de size pode ser maior ou menor que o original. Um ponteiro para o bloco de memória é devolvido porque realloc() pode precisar mover o bloco para aumentar seu tamanho. Se isso ocorre, o conteúdo do bloco antigo é copiado no novo bloco; nenhuma informação é perdida.
Sintaxe.:
void *realloc(void *ptr, size_t size);
Se ptr é um nulo, realloc() simplesmente aloca size bytes de memória e devolve um ponteiro para a memória alocada. Se size é zero, a memória apontada por ptr é liberada.
Se não há memória livre suficiente no heap para alocar size bytes, é devolvido um ponteiro nulo e o bloco original é deixado inalterado.
Exemplo 6.4:
/* Esta programa primeiroaloca 23 caracteres, copia a string “isso são 22 caracteres” neles e, em seguida, usa realloc() para aumentar o tamanho para 24 e, assim, pôr um ponto no final. */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
char *p;
p=(char*)malloc(23);
if (!p) {
printf(“ erro de alocação - abortando”);
exit(1);
}
strcpy(p,”isso são 22 caracteres”);
p = (char*)realloc(p,24);
if (!p) {
printf(“ erro de alocação - abortando”);
exit(1);
}
strcat(p,”.”);
printf(p);
free(p);
return 0;
}
Matrizes Dinamicamente Alocadas
Qualquer ponteiro pode ser indexado como se fosse uma matriz unidimensional, portanto não haverá nenhum problema para utilizar.
Exemplo 6.5:
/* Aloca memória para uma string dinamicamente, solicita */
/* a entrada do usuário e, em seguida, imprime a string */
/* de trás para frente. */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
char *s;
register int t;
s=(char*)malloc(80);
if (!s) {
printf(“ erro de alocação - abortando”);
exit(1);
}
gets(s);
for (t=strlen(s)-1; t>=0; t--) putchar(s[t]);
free(s);
return 0;
}
Para acessar uma matriz unidimensional é simples, mas para mais de uma dimensão levam alguns problemas com a indexação.
Para conseguir uma matriz alocada dinamicamente, deve-se utilizar um truque: passar um ponteiro como um parâmetro a uma função. Dessa forma, a função pode definir as dimensões do parâmetro que recebe o ponteiro, permitindo, assim, a indexação normal da matriz. Isto é mostrado no Exemplo 6.6.
Exemplo 6.6:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void le_tab(int mat[20][5]) {
register int i,j;
for(i=0; i<20; i++)
for(j=0; j<5; j++)
scanf(“%d”,&mat[i][j]);
}
void mostra_tab(int mat[20][5]) {
register int i,j;
for(i=0; i<20; i++)
for(j=0; j<5; j++)
printf(“%d”,mat[i][j]);
}
int main() {
char *p;
register int t;
s=(int*)calloc(100, sizeof(int));
if (!p) {
printf(“ erro de alocação - abortando”);
exit(1);
}
le_tab(p);
mostra_tab(p);
return 0;
}
Listas Encadeadas
Listas encadeadas são usadas para dois propósitos fundamentais. O primeiro é criar matrizes de tamanho desconhecido na memória. Listas encadeadas também são usadas em armazenamento de banco de dados em arquivos em disco.
Listas encadeadas podem ser singularmente (simplesmente) - um elo para o próximo item - ou duplamente - elos para o anterior e próximo elemento da lista - encadeadas.
Listas Singularmente Encadeadas
Uma lista singularmente encadeada requer que cada item de informação contenha um elo como o próximo da lista. Cada item de dado geralmente consiste em uma estrutura que inclui campos de informação e ponteiro de enlace (ou de ligação).
Antes, precisa-se definir a estrutura de dados que contenha a informação e os elos. Considere um exemplo de armazenamento de coordenadas cartesianas (x,y,z) para representação de uma figura geométrica. A estrutura de dados para cada elemento é definido aqui:
struct ponto {
int x,y,z;
struct ponto *prox;
} figura1;
typedef struct ponto figura;
Na estrutura acima, é declarado um apontador para a próxima estrutura, por isso declara-se um apontador para a própria estrutura (auto-referência). Após a declaração, foi definido que figura é um tipo, que representa uma struct ponto, o qual pode ser utilizado em todo o programa.
A função inclui() constrói uma lista singularmente encadeada colocando cada novo item no final da lista. Deve ser passado um ponteiro para uma estrutura do tipo ponto, ponteiro para o primeiro elemento e ponteiro para o último elemento.
void inclui(figura *i, figura **inicio, figura **fim) {
if (!*fim) {/* Primeiro elemento da lista */
*fim = i;
*inicio = i;
} else {
(*fim)->prox = i;
*fim = i;
}
}
Os parâmetros início e fim da função têm dois asteriscos porque representam uma indireção múltipla. Isto é, são apontadores para apontadores da estrutura figura. Isto é necessário para poder implementar a passagem de parâmetros por referência.
Apagar um item de uma lista singularmente encadeada pode ocorrer em três situações: apagar o primeiro item, apagar um item intermediário e apagar o último item. A função a seguir excluirá um item de uma lista de estruturas do tipo ponto:
void exclui(
figura *p, /* item anterior */
figura *i, /* item a apagar */
figura **inicio, /* início da lista */
figura **ultimo) { /* final da lista */
if (p)
p->next = i->next;
else
*start = i->next;
if (i==*last && p)
*last = p;
}
Listas singularmente encadeadas têm uma desvantagem é que a lista não pode ser lida em ordem inversa.
Listas Duplamente Encadeadas
Consistem em dados e elos para o próximo item e para o item precedente. Um novo elemento pode ser inserido em uma lista duplamente encadeada de três maneiras: inserir um novo primeiro elemento, inserir um elemento intermediário ou inserir um novo último elemento.
A construção de uma lista duplamente encadeada é semelhante à de uma lista singularmente encadeada, exceto pelo fato de que dois elos devem ser mantidos. Utilizando a estrutura ponto, será mostrado a declaração de um nodo de lista duplamente encadeada.
struct ponto {
int x,y,;
struct ponto *prox;
struct ponto *ant;
};
typedef struct ponto figura;
Usando figura como o item de dado básico, a função seguinte constrói uma lista duplamente encadeada. Esta função inclui um novo dado no fim da lista:
void incfim(figura *i, figura **fim) {
if (!*fim)
*fim = i; /* é o primeiro item da lista */
else
(*fim)->prox = i;
i->prox = NULL;
i->ant = *fim;
*fim = i;
}
Para armazenagem de um dados em uma posição específica a função abaixo realiza a inclusão em ordem crescente pelo eixo x de uma lista duplamente encadeada.
void incord(figura *i, /* novo elemento */
figura **inicio, /* primeiro elemento da lista */
figura **fim) { /* ultimo elemento da lista */
if (!*fim) { /* é o primeiro item da lista */
i->prox = NULL;
i->ant = NULL;
*inicio = i;
*fim = i;
} else {
figura *old, *p;
p = *inicio;
old = NULL;
while (p && (p->x < i->x)) {
old = p;
p = p->prox;
}
if (!old) {
i->prox = p; /* inserir no inicio da lista */
i->ant = NULL;
p->ant = i;
*inicio = i;
} else {
if (p->ant) { /* inserir em uma posição */
p->ant->prox = i; /* intermediária da lista */
i->prox = p;
i->ant = p->ant;
p->ant = i;
} else {
old->prox = i; /* inserir no fim da lista */
i->prox = NULL;
i->ant = old;
*fim = i;
}
}
}
Como o ponteiro de início e fim de lista podem ser alterados, a função incord() atualiza automaticamente estes ponteiros através das variáveis inicio e fim.
Há três casos a considerar ao excluir um elemento de uma lista duplamente encadeada: excluir o primeiro item, excluir um item intermediário ou excluir o último item.
void delord(figura *i, /* item a apagar */
figura **inicio, /* primeiro elemento da lista */
figura **fim) { /* ultimo elemento da lista */
figura *old, *p;
p = *inicio;
old = NULL;
while (p && (p->x != i->x) &&(p->y != i->y)) {
old = p;
p = p->prox;
}
if ((p->x = i->x) &&(p->y = i->y)) {
if (!old) /* exclusão único elemento da lista */
*inicio=*fim=NULL;
else {
if (!p->prox) { /* exclusão do ultimo elemento da lista */
old->prox = NULL;
*fim = old;
} else { /* excluir item de uma posição */
old->prox = p->prox; /* intermediária da lista */
p->prox->ant = old;
}
}
free(p);
}
}
Árvores Binárias
A estrutura utilizada para a construção de árvores binárias é semelhante a listas duplamente encadeadas. A diferença está na política de organização das mesmas. O Exemplo 6.7 mostra uma função que constrói uma árvore binária ordenada recursivamente.
Exemplo 6.7
struct tree {
char info;struct tree *esq;
struct tree *dir;
};
typedef struct tree arvore;
arvore *stree (
arvore *raiz;
arvore *r;
char info) {
if (!r) {
r = (arvore *) malloc(sizeof(arvore));
if (!r) {
printf(“Sem memória \n”);
exit(0);
}
r->esq = NULL;
r->dir = NULL;
r->info = info;
if (!raiz)
return r; /* primeira entrada */
if (info < raiz->info)
raiz->esq = r;
else
raiz->dir = r;
return r;
}
if (info < r->info)
stree(r,r->esq, info);
else
stree(r,r->dir, info);
}
A chamada desta função é realizada desta forma:
if (!rt)
rt = stree(rt, rt, info);
else
stree (rt, rt, info);
Dessa forma, tanto o primeiro quanto os elementos subseqüentes podem ser inseridos corretamente. A função stree() é um algoritmo recursivo.
Existem três formas de acessar os dados de uma árvore: ordenada, preordenada e pós-ordenada. Onde a ordenada, é visitado a subárvore da esquerda, a raiz e em seguida a subárvore da direita. Na preordenada, visita-se a raiz, subárvore da esquerda e, em seguida, a subárvore da direita. Na pós-ordenada, visita-se a subárvore da esquerda, subárvore da direita e, depois, a raiz.
Figura 6.2 Exemplo de uma árvore binária
Utilizando a figura 14.1, a ordem de acesso á árvore usando cada método é
ordenada
a
b
c
d
e
f
g
preordenada
d
b
a
c
f
e
g
pós-ordenada
a
c
b
e
g
f
d
Para o acesso de forma ordenada, pelas formas descritas anteriormente, pode-se utilizar as funções descritas abaixo:
void inorder(arvore *raiz) {
if(!raiz)
return;
inorder(raiz->esq);
printf(“%c”, raiz->info);
inorder(raiz->dir);
}
void preorder(arvore *raiz) {
if(!raiz)
return;
printf(“%c”, raiz->info);
preorder(raiz->esq);
preorder(raiz->dir);
}
void postorder(arvore *raiz) {
if(!raiz)
return;
postorder(raiz->esq);
postorder(raiz->dir);
printf(“%c”, raiz->info);
}
Para exclusão de um nó de uma árvore tem que ser verificado se o nó é a raiz, um nodo esquerdo ou direito e que os mesmos podem ter subárvores ligadas a ele. Na função a seguir é realizada uma exclusão recursiva observando as restrições delineadas anteriormente.
arvore *dtree(arvore *raiz, char key) {
arvore *p, *p2;
if (raiz->info==key) { /* apagar a raiz */
if (raiz->esq== raiz->dir) { /*não tem filhos */
free(raiz);
return NULL;
} else
if (raiz->esq == NULL) {
p = raiz->dir;
free(raiz);
return p;
} else
if (raiz->dir == NULL) {
p = raiz->esq;
free(raiz);
return p;
} else {
p2 = raiz->dir;
p = raiz->dir;
while (p->esq)
p = p->esq;
p->esq = raiz->esq;
free(raiz);
return p2;
}
}
if (raiz->info < key)
raiz->dir = dtree(raiz->dir, key);
else
raiz->esq = dtree(raiz->esq, key);
return raiz;
}
Árvores binárias oferecem grande poder, flexibilidade e eficiência quando usadas em programas de gerenciamento de banco de dados. Principalmente pela sua política de organização e a não limitação do tamanho (exceto aquela imposta pela memória).
Exercícios
1. Escreva um programa que leia vários nomes e endereços, rearranje os nomes em ordem alfabética e, depois, imprima a lista em ordem alfabética. Utilize várias estruturas.
2. Escreva um programa que gerencie uma pilha. O mesmo deve conter a função de empilhar e desempilhar para o usuário os quatro tipos de dados básicos da linguagem C (char, float, int, double).
3. Escreva um programa que gerencie uma fila circular do tipo FIFO (Primeiro que entra é o primeiro que sai).
9. Faça um programa que leia o número de alunos; construa uma matriz dinamicamente alocada de tamanho N X 4, onde N é o número de alunos e 4 as respectivas notas de cada aluno. Calcule a média e mostre na tela conforme descrição a seguir:
ALUNO N1 N2 N3 N4 MEDIA
1 8.5 7.0 9.5 7.0 8.0
2 7.5 7.0 6.5 7.0 7.0
E/S com Arquivo
São grupos de dados armazenados em meio não volátil (disco, fita, entre outros). São utilizados para armazenar dados de forma permanente.
A linguagem C não contém nenhum comando de E/S. Ao contrário, todas as operações de E/S ocorrem através de chamadas a funções da biblioteca C padrão. Essa abordagem faz o sistema de arquivos de C extremamente poderoso e flexível. O sistema de E/S de C é único, porque dados podem ser transferidos na sua representação binária interna ou em um formato de texto.
E/S ANSI x E/S UNIX
O padrão ANSI define um conjunto completo de funções de E/S que pode ser utilizado para ler e escrever qualquer tipo de dado. Em contraste, o antigo padrão C UNIX contém dois sistemas distintos de rotinas que realizam operações de E/S. O primeiro método é denominado de sistema de arquivo com buffer (algumas vezes os termos formatado ou alto nível são utilizados para referenciá-lo). O segundo é o sistema de arquivo tipo UNIX (algumas vezes chamado de não formatado ou sem buffer) definido apenas sob o antigo padrão UNIX.
Streams
A linguagem C oferece uma abstração da interface para controle de E/S, independente do dispositivo real (terminais, acionadores de disco, acionadores de fita, entre outros) que é acessado. Sendo que o dispositivo real é chamado de arquivo.
Existem dois tipos de streams: texto e binária. A primeira é uma seqüência de caracteres organizados em linhas e terminadas por um caractere de nova linha (depende da implementação). A segunda é uma seqüência de bytes com uma correspondência de um para um com aqueles encontrados no dispositivo externo - isto é, não ocorre nenhuma tradução de caracteres.
Arquivos
Um arquivo pode ser qualquer coisa, desde um arquivo em disco até um terminal ou uma impressora. Associa-se uma stream com um arquivo específico realizando uma operação de abertura. Todos as streams são iguais, mas não todos os arquivos. Isto é, um arquivo disco pode suportar acesso aleatório enquanto um teclado não pode.
Cada stream associada a um arquivo tem uma estrutura de controle de arquivo do tipo FILE. Essa estrutura é definida no cabeçalho STDIO.H.
Todos os arquivos são fechados automaticamente quando o programa termina, normalmente com main() retornando ao sistema operacional ou uma chamada à exit(). Os arquivos não são fechados quando um programa quebra (crash).
Sistema de Arquivos
O sistema de arquivos ANSI é composto de diversas funções inter-relacionadas. As mais comuns são mostradas na Tabela 7.1. Essas funções exigem que o cabeçalho STDIO.H seja incluído em qualquer programa em que são utilizadas.
Tabela 7.1 - Funções mais comuns do sistema de arquivo com buffer
Nome
Função
fopen()
Abre um arquivo
fclose()
Fecha um arquivo
putc()
Escreve um caractere em um arquivo
fputc()
O mesmo que putc()
getc()
Lê um caractere de um arquivo
fgetc()
O mesmo que getc()
fseek()
Posiciona o arquivo em um byte específico
fprintf()
É para um arquivo o que printf() é para o console
fscanf()
É para um arquivo o que scanf() é para o console
feof()
Devolve verdadeiro se o fim de arquivo for atingido
ferror()
Devolve verdadeiro se ocorreu um erro
rewind()
Repõe o indicador de posição de arquivo no início do arquivo
remove()
Apaga um arquivo
fflush()
Descarrega um arquivo
O arquivo cabeçalho STDIO.H fornece os protótipos para as funções de E/S e define estes três tipos: size_t, fpos_t e FILE. O tipo size_t é essencialmente o mesmo que um unsigned, assim como o fpos_t. O tipo FILE é discutido na próxima seção.
STDIO.H define várias macros como: EOF, SEEK_SET, SEEK_CUR e SEEK_END. A macro EOF é geralmente definida como -1 e é o valor quando uma função de entrada tenta ler além do final do arquivo. As outras macros são usadas com fseek(), que é uma função que executa acesso aleatório em um arquivo.
Estrutura FILE
Para a manipulação de arquivos é utilizado a declaração de ponteiro (ponteiro de arquivo). Isto é, um ponteiro para informações que definem vários dados sobreo arquivo, como o seu nome, status, e a posição atual do arquivo. Um ponteiro de arquivo é uma variável ponteiro do tipo FILE . Todas as funções são realizadas utilizando o ponteiro. Para a declaração de um ponteiro de arquivo utiliza-se a seguinte sintaxe:
Sintaxe:
FILE *<var>
Exemplo 7.1
FILE *fp;
Abertura de Arquivos
A função fopen() abre uma stream para uso e associa um arquivo a ela. Retorna o ponteiro de arquivo associado a esse arquivo.
Sintaxe:
FILE *fopen(const char * <nome_arquivo>, const char * <modo_abertura>);
O modo de abertura define a forma como é feito o acesso aos dados (somente leitura, leitura e escrita, etc). As forma principais são apresentadas na Tabela 7.2.
Tabela 7.2 - Os modos de abertura válidos
Modo
Significado
r
Abre um arquivo texto para leitura
w
Cria um arquivo texto para escrita
a
Anexa a um arquivo texto
rb
Abre um arquivo binário para leitura
wb
Cria um arquivo binário para escrita
ab
Anexa a um arquivo binário
r+
Abre um arquivo texto para leitura/escrita
w+
Cria um arquivo texto para leitura/escrita
a+
Anexa ou cria um arquivo texto para leitura/escrita
r+b ou rb+
Abre um arquivo binário para leitura/escrita
w+b ou wb+
Cria um arquivo binário para leitura/escrita
a+b ou ab+
Anexa a um arquivo binário para leitura/escrita
Exemplo 7.2
FILE *arq; /* ponteiro de arquivo */
arq = fopen(“dados.dat”,”wb”);
Se ao abrir um arquivo para leitura o mesmo não existir a função fopen retorna um ponteiro nulo (NULL).
arq = fopen(“dados.dat”,”rb”);
if (arq == NULL)
arq=fopen(“dados.dat”,”wb”);
Fechamento de Arquivo
A função fclose() fecha uma stream que foi aberta através de uma chamada à fopen(). Esta função tem a seguinte sintaxe:
Sintaxe:
int fclose(FILE *fp);
onde fp é o ponteiro de arquivo devolvido pela chamada à fopen(). O valor de retorno 0 significa uma operação de fechamento bem-sucedida.
Verificando fim de arquivo
A função feof()devolve verdadeiro quando for encontrado o fim de arquivo. O protótipo desta função está declarado abaixo:
Sintaxe:
int feof(FILE *fp);
Esta função pode ser aplicada tanto para arquivo texto como para arquivos binários.
Condições de erro
Para determinar se um erro ocorreu utiliza-se a função ferror(), mas esta informação não basta para a solução por parte do usuário. Para isso utiliza-se a função perror() em conjunto com a função ferror(). O argumento de perror() é uma string fornecida pelo programa que normalmente é uma mensagem de erro que indica em que parte do programa ocorreu erro.
Sintaxe:
void perror (const char *str);
Se for detectado um erro de disco, ferror() retornará um valor verdadeiro (não zero) e perror() imprimirá a seguinte mensagem
Erro de Busca: Bad data
A primeira parte da mensagem é fornecida pelo programa, e a segunda parte, pelo sistema operacional.
Streams Padrão
Sempre que um programa é iniciado três streams padrões são abertas automaticamente:
· stdin (entrada padrão - teclado);
· stdout (saída padrão - tela);
· stderr (saída de erro padrão - tela);
· stdaux (saída auxiliar padrão - porta serial);
· stdprn (impressora padrão - impressora paralela).
Essas streams podem ser utilizadas normalmente para executar operações de E/S bufferizada.
Leitura e Gravação de caracteres
Para as operações de leitura e gravação são utilizadas duas funções padrões: getc() e putc() (consideradas tecnicamente macros) . Para cada uma destas funções existem duas equivalentes: fgetc() e fputc(). Nesta seção são apresentadas as funções declaradas padrão ANSI (as duas primeiras). As outras funções têm a mesma sintaxe que suas equivalentes.
Para a escrita de caracteres em um arquivo utilizam-se as funções putc() e fputc(), as quais são equivalentes (putc() é uma macro). O protótipo para essa função é
Sintaxe:
int putc(int ch, FILE *fp);
onde fp é um ponteiro de arquivo devolvido por fopen() e ch é o caractere a ser escrito.
Se a operação putc() foi bem-sucedida, ela devolve o caractere escrito. Caso contrário, ela devolve EOF.
Para leitura de caracteres em um arquivo utilizam-se as funções getc() e fgetc(), as quais são equivalentes (getc() é uma macro). O protótipo para essa função é
Sintaxe:
int getc(FILE *fp);
onde fp é um ponteiro de arquivo devolvido por fopen().
Se a operação getc() foi bem-sucedida, ela devolve o caractere lido. Caso contrário, ela devolve EOF. O Exemplo 7.3 mostra um laço que realiza uma leitura de um arquivo texto até que a marca de final de arquivo seja lida.
Exemplo 7.3
do {
ch = getc(fp);
} while (ch!=EOF);
As funções fopen(), getc(), putc() e fclose() constituem o conjunto mínimo de rotinas de arquivos. O programa a seguir lê caracteres do teclado e os escreve em um arquivo em disco até que o usuário digite um cifrão ($). O nome do arquivo é passado pela linha de comando.
Exemplo 7.4
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE *fp;
char ch;
if(argc !=2) {
printf(“Você esqueceu de entrar o nome do arquivo \n”);
exit(1);
}
if((fp=fopen(argv[1],”w”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
do {
ch = getchar();
putc(ch,fp);
} while(ch!=‘$’);
fclose(fp);
return 0;
}
O programa complementar descrito a seguir lê qualquer arquivo ASCII e mostra o conteúdo na tela.
Exemplo 7.5
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE *fp;
char ch;
if (argc !=2) {
printf(“Você esqueceu de entrar o nome do arquivo \n”);
exit(1);
}
if ((fp=fopen(argv[1],”r”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
ch = getc(fp);
while (ch!=EOF) {
putchar(ch);
ch = getc(fp);
}
fclose(fp);
return 0;
}
Trabalhando com Strings
Para a gravação e leitura de strings de caractere para e de um arquivo em disco são utilizadas as funções fgets() e fputs(), respectivamente. São os seguintes os seus protótipos:
Sintaxe:
int fputs(const char *str, FILE *fp);
char *fgets(char *str, int length, FILE *fp);
A função fputs() opera como puts(), mas escreve a string na stream especificada. EOF é devolvido se ocorre um erro.
A função fgets() lê uma string da stream especificada até que um caractere de nova linha seja lido ou que length-1 caracteres tenham sido lidos. Se uma nova linha é lida, ela será parte da string (diferente de gets()). A string resultante será terminada por um nulo. A função devolve um ponteiro para str se bem-sucedida ou um ponteiro nulo se ocorre um erro.
O programa a seguir lê strings do teclado e escreve-as no arquivo chamado frase.dat. A condição de saída é uma linha em branco. Como gets() não armazena o caractere de nova linha, é adicionado um antes que a string seja escrita no arquivo para que o arquivo possa ser lido mais facilmente.
Exemplo 7.6
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char str[80];
FILE *fp;
if ((fp=fopen(“frase.dat”,”w”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
do {
printf(“entre uma string (CR para sair): \n”);
gets(str);
strcat(str,”\n”);
fputs(str,fp);
} while (*str != ‘\n”);
fclose(fp);
return 0;
}
Funções de tratamento de arquivos
Nas próximas seções serão vistas algumas funções utilizadas em operações com arquivos com buffer.
rewind()
Esta função reposiciona o indicador de posição de arquivo no início do arquivo especificado como seu argumento. Seu protótipo é:
Sintaxe:
void rewind(FILE *fp);
O Exemplo 7.7 reposiciona o indicador de posição do arquivo do programa anterior e mostra o conteúdo do mesmo.
Exemplo 7.7
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char str[80];
FILE *fp;
if((fp=fopen(“frase.dat”,”w”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
do {
printf(“entre uma string (CR para sair): \n”);
gets(str);
strcat(str,”\n”);
fputs(str,fp);
} while (*str != ‘\n”);
rewind(fp); /* reinicializa o file pointer */while(!feof(fp)) {
fgets(str, 79, fp);
printf(str);
}
fclose(fp);
return 0;
}
ferror()
A função ferror() determina se uma operação com arquivo produziu um erro. A função ferror() tem esse protótipo:
Sintaxe:
int ferror(FILE *fp);
Ela retorna verdadeiro se ocorreu um erro durante a última operação no arquivo; caso contrário, retorna falso.
remove()
A função remove() apaga o arquivo especificado. Seu protótipo é:
Sintaxe:
int remove(char *nome_arq)
Ela devolve zero caso seja bem-sucedido e um valor diferente de zero caso contrário. O Exemplo 7.8 apresenta um trecho de programa que apaga o arquivo dados.dat.
Exemplo 7.8
if (remove(“dados.dat”)) {
printf(“Arquivo não pode ser apagado\n”);
exit(1);
}
fflush()
Para se esvaziar o conteúdo de uma stream de saída, deve-se utilizar a função fflush(), cujo protótipo é mostrado a seguir:
Sintaxe:
int fflush(FILE *fp);
Essa função escreve o conteúdo de qualquer dado existente no buffer arquivo associado a fp. Se fflush() devolve 0 para indicar sucesso; caso contrário, devolve EOF.
Acesso aleatório: fseek()
Operações de leitura e escrita aleatórias podem ser executadas utilizando o sistema de E/S bufferizado com a ajuda de fseek(), que modifica o indicador de posição de arquivo. Seu protótipo é mostrado aqui:
Sintaxe:
int fseek (FILE *fp, long numbytes, int origem);
Onde, numbytes, um inteiro longo, é o número de bytes a partir de origem, que se tornará a nova posição corrente, e origem é uma das seguintes macros definidas em STDIO.H.
Tabela 7.3 – Macros definidas em STDIO.H para as posições permitidas na função fseek().
Origem
Nome da Macro
Início do arquivo
SEEK_SET
Posição atual
SEEK_CUR
Final do arquivo
SEEK_END
A função fseek() devolve 0 se a operação for bem-sucedida e um valor diferente de zero se ocorre um erro.
O Exemplo 7.9 mostra a utilização da função fseek() num programa que recebe pela linha de comando o deslocamento a ser realizado a partir do início do arquivo (SEEK_SET).
Exemplo 7.9
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE *fp;
char ch;
if(argc !=3) {
printf(“Uso: SEEK nomearq byte \n”);
exit(1);
}
if((fp=fopen(argv[1],”r”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
if(fseek(fp, atol(argv[2]), SEEK_SET)) {
printf(“erro na busca\n”);
exit(1);
}
printf(“O byte em %ld é %c\n”, atol(argv[2]), getc(fp));
fclose(fp);
return 0;
}
No Exemplo 7.9 verifica-se a utilização da função atol(), que tem por função converter uma string em um inteiro longo. No printf() utiliza-se o modo de formatação %ld que é para apresentação de um decimal longo ou um inteiro longo.
ftell()
Esta função retorna a posição do ponteiro de um arquivo binário em relação ao seu começo. Esta função aceita um único argumento, que é o ponteiro para a estrutura FILE do arquivo. Seu protótipo é mostrado aqui:
Sintaxe:
long ftell (FILE *fp);
Retorna um valor do tipo long, que representa o número de bytes do começo do arquivo até a posição atual.
A função ftell() pode não retornar o número exato de bytes se for usada com arquivos em modo texto, devido à combinação CR/LF que é representada por um único caractere em C.
Comando de gravação em modo texto formatado
Como extensão das funções básicas de E/S já discutidas, o sistema de E/S com buffer inclui fprintf() e fscanf(). Essas funções se comportam exatamente como printf() e scanf() exceto por operarem em arquivos. Os protótipos de fprintf() e fscanf() são
Sintaxe:
int fprintf(FILE *fp, const char *control_string, ...);
int fscanf(FILE *fp, const char *control_string, ...);
onde fp é um ponteiro de arquivo devolvido por uma chamada à fopen(). fprintf() e fscanf() direcionam suas operações de E/S para o arquivo apontado por fp.
A fscanf() devolve o número de elementos lidos, convertidos e armazenados. No caso de fim de arquivo o fscanf() devolve o valor EOF.
O Exemplo 7.10 mostra um programa que lê uma string e um inteiro do teclado, escreve-os em um arquivo em disco e em seguida lê e mostra a informação na tela.
Exemplo 7.10
#include <stdio.h>
#include <stdlib.h>
#include <io.h>
int main() {
FILE *fp;
char s[80];
int t;
if((fp=fopen(“teste”,”w”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
printf(“entre com uma string e um número: “ );
fscanf(stdin, “%s%d”, s, &t); /* le do teclado */
fprintf(fp, “%s %d”, s, t); /* escreve no arquivo */
fclose(fp);
if((fp=fopen(“teste”,”w”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
fscanf(fp, “%s%d”, s, &t); /* le do arquivo */
fprintf(stdout, “%s %d”, s, t); /* imprime na tela */
return 0;
}
A formatação realizada para leitura e escrita de dados pode comprometer a velocidade de processamento do programa.
Lendo e gravando registros
As funções fread() e fwrite() possibilitam uma maneira de transferir blocos de dados do disco para a memória do computador e vice-versa. A grande vantagem destes comandos é poder ler e gravar dados maiores que um byte e que formem estruturas complexas (vetor, matriz, ou um registro, ou até um vetor de registros).
Escrita de um bloco de dados
Para se gravar um bloco de dados maiores que um byte o sistema de arquivo ANSI C fornece a função fwrite(). Seu protótipo está definido a seguir:
Sintaxe:
size_t fwrite(void *buffer, size_t num_bytes, size_t count, FILE *fp);
onde buffer é um ponteiro para uma região de memória que contém as informações que serão escritas no arquivo. O número de bytes para gravar é especificado por num_bytes. O argumento count determina quantos itens serão gravados. E, finalmente, fp é um ponteiro de arquivo para uma stream aberta anteriormente.
Esta função devolve o número de itens escritos. O número retornado pode ser menor que count quando o final de arquivo for atingido ou ocorrer um erro de gravação.
Exemplo 7.11
int var_int;
FILE *arq;
arq = fopen(“dados.dat”,”wb”);
var_int = 5;
fwrite(&var_int,sizeof(var_int),10,arq);
Leitura de um bloco de dados
Para se ler um bloco de dados maiores que um byte o sistema de arquivo ANSI C fornece a função fread(). Seu protótipo está definido a seguir:
Sintaxe:
size_t fread(void *buffer, size_t num_bytes, size_t count, FILE *fp);
onde buffer é um ponteiro para uma região de memória que receberá os dados do arquivo. O número de bytes para ler é especificado por num_bytes. O argumento count determina quantos itens serão lidos. E, finalmente, fp é um ponteiro de arquivo para uma stream aberta anteriormente.
Esta função devolve o número de itens lidos. O número retornado pode ser menor que count quando o final de arquivo for atingido ou ocorrer um erro de leitura.
Exemplo 7.12
int var_int;
FILE *arq;
arq = fopen(“dados.dat”,”rb”);
fread(&var_int,sizeof(var_int),1,arq);
Utilizando os comandos de leitura e gravação de registros
Uma das mais úteis aplicações de fread() e fwrite() envolve ler e escrever tipos de dados definidos pelo usuário, especialmente estruturas.
Exemplo 7.13
#include <stdio.h>
struct pto {
int x,y;
};
typedef struct pto ponto;
int main() {
FILE *fp;
int i;
ponto coord[10];
for (i=0; i < 10; i++) {
printf(“Coordenada x:”);
scanf(“%d \n”,&coord[i].x);
printf(“Coordenada y:”);
scanf(“%d \n”,&coord[i].y);
}
if ((fp=fopen(“figura.dat”,”w”))==NULL) {
printf(“Arquivo não pode ser aberto\n”);
exit(1);
}
for (i=0; i < 10; i++)
fwrite(&coord[i], sizeof(ponto),1, fp);
rewind(fp);
i = 0;
while(!feof(fp)) {
fread(&coord[i], sizeof(ponto), 1, fp);
printf(“Coordenadas (x,y) = (%d,%d),coord[i].x,coord[i].y);
}
fclose(fp);
return 0;
}
Funções para manipulação de buffers
Para trabalhar com buffers utilizam-se algumas funções especializadas que são independentes de tipo.
A função memchr() procura, no vetor apontado por buffer, pela primeira ocorrência de ch nos primeiros count caracteres. Devolve um ponteiro para a primeira ocorrência de ch em buffer ou um ponteiro nulo se ch não forencontrado. O protótipo da função é
Sintaxe:
void *memchr(const void*buffer, int ch, size_t count);
Exemplo 7.14
#include <stdio.h>
#include <string.h>
int main() {
char *p;
p = memchr(“isto e um teste”. ´ ´,14);
printf(p);
return 0;
}
A função memcmp() compara os primeiros count caracteres das matrizes apontadas por buf1 e buf2. O valor devolvido segue os valores da função strcmp(). O protótipo da função é
Sintaxe:
int memcmp(const void*buf1, const void*buf2, size_t count);
A função memcpy() copia os primeiros count caracteres do vetor origem para o vetor apontado por destino. Ela devolve um ponteiro para destino. O protótipo da função é
Sintaxe:
void *memcpy(void*destino, const void*origem, size_t count);
Exemplo 7.15
#include <stdio.h>
#include <string.h>
#define SIZE 80
int main() {
char buf1[SIZE], buf2[SIZE];
strcpy(buf1, “Quando, no curso do ...”);
memcpy(buf2, buf1, SIZE);
printf(buf2);
return 0;
}
A função memmove() copia count caracteres do vetor apontado por origem para o vetor apontado por destino. Se as matrizes se sobrepõem, a cópia ocorrerá corretamente, colocando o conteúdo correto em destino, porém origem será modificado. Ela devolve um ponteiro para destino. O protótipo da função é
Sintaxe:
void *memmove(void*destino, const void*origem, size_t count);
Exemplo 7.16
#include <stdio.h>
#include <string.h>
#define SIZE 80
int main() {
char buf1[SIZE], buf2[SIZE];
strcpy(buf1, “Quando, no curso do ...”);
memmove(buf2, buf1, SIZE);
printf(buf2);
}
A função memset() copia o byte menos significativo de ch nos primeiros count caracteres do vetor apontado por buf. Ela devolve buf. é muito utilizado na inicialização de uma região de memória com algum valor conhecido. O protótipo da função é
Sintaxe:
void *memset(void*buf, int ch, size_t count);
Exemplo 7.17
/* Inicializa com nulo os 100 primeiros bytes */
memset(buf, ’\0’, 100); /*do vetor apontado por buf */
/* Inicializa com X os 10 primeiros bytes */
memset(buf, ’X’, 10);
printf(buf);
Exercícios
1. Faça um programa que escreva os números de 0 a 10 em um arquivo.
2. Faça um programa que leia 11 números de um arquivo.
3. Escrever um programa em C que leia um número indeterminado de códigos (inteiro) e taxas de consumo (real), em Kw, dos consumidores de uma cidade e grave os dados em um arquivo chamado medidas.txt. O programa pára de ler quando o código fornecido for zero.
4. Escrever um programa em C que leia o arquivo gerado no exercício 1 e mostre quantas taxas existem e o maior consumo.
5. Escrever um programa em C que leia um arquivo texto (temp.txt) e grave em outro arquivo (tempMax.txt). O arquivo origem possuiu um número indeterminado de linhas, onde cada linha possui 5 colunas: dia, mês, ano, temperatura mínima (real), temperatura máxima (real). O programa deve escrever no arquivo tempMax.txt o dia, mês, ano, e a temperatura máxima somente quando ela for maior que a temperatura máxima do dia anterior. Por exemplo, assuma que o arquivo temp.txt possua o seguinte conteúdo:
10 10 2007 16.5 32.2
11 10 2007 15.4 31.0
12 10 2007 17.5 32.7
13 10 2007 18.6 32.4
14 10 2007 17.5 30.3
15 10 2007 18.3 31.2
16 10 2007 16.8 30.2
17 10 2007 17.6 31.3
O programa deve gerar o arquivo tempMax.txt com o seguinte conteúdo:
12 10 2007 32.7
15 10 2007 31.2
17 10 2007 31.3
A. Tabela Ascii
Cód
Car
Cód
Car
Cód
Car
Cód
Car
Cód
Car
Cód
Car
Cód
Car
Cód
Car
0
32
64
@
96
‘
128
Ç
160
á
192
└
224
Ó
1
☺
33
!
65
A
97
a
129
ű
161
í
193
┴
225
ß
2
☻
34
”
66
B
98
b
130
é
162
ó
194
┬
226
Ô
3
35
#
67
C
99
c
131
â
163
ú
195
├
227
Ò
4
36
$
68
D
100
d
132
ä
164
ñ
196
─
228
õ
5
37
%
69
E
101
e
133
à
165
Ñ
197
┼
229
Õ
6
38
&
70
F
102
f
134
å
166
ª
198
ã
230
µ
7
39
’
71
G
103
g
135
ç
167
º
199
Ã
231
þ
8
40
(
72
H
104
h
136
ê
168
¿
200
╚
232
Þ
9
41
)
73
I
105
i
137
ë
169
®
201
╔
233
Ú
10
42
*
74
J
106
j
138
è
170
¬
202
╩
234
Û
11
♂
43
+
75
K
107
k
139
ï
171
½
203
╦
235
Ù
12
♀
44
,
76
L
108
l
140
î
172
¼
204
╠
236
ý
13
45
-
77
M
109
m
141
ì
173
¡
205
═
237
Ý
14
♫
46
.
78
N
110
n
142
Ä
174
«
206
╬
238
¯
15
☼
47
/
79
O
111
o
143
Å
175
»
207
¤
239
´
16
48
0
80
P
112
p
144
É
176
░
208
ð
240
17
49
1
81
Q
113
q
145
æ
177
▒
209
Ð
241
±
18
↕
50
2
82
R
114
r
146
Æ
178
▓
210
Ê
242
_
19
!!
51
3
83
S
115
s
147
ô
179
│
211
Ë
243
¾
20
¶
52
4
84
T
116
t
148
ö
180
┤
212
È
244
¶
21
§
53
5
85
U
117
u
149
ò
181
Á
213
245
§
22
▬
54
6
86
V
118
v
150
û
182
Â
214
Í
246
÷
23
↕
55
7
87
W
119
w
151
ù
183
À
215
Î
247
¸
24
↑
56
8
88
X
120
x
152
ÿ
184
©
216
Ï
248
°
25
↓
57
9
89
Y
121
y
153
ö
185
╣
217
┘
249
¨
26
58
:
90
Z
122
z
154
Ü
186
║
218
┌
250
·
27
59
;
91
[
123
{
155
ø
187
╗
219
█
251
¹
28
60
<
92
\
124
|
156
£
188
╝
220
▄
252
³
29
61
=
93
]
125
}
157
Ø
189
¢
221
¦
253
²
30
62
>
94
^
126
~
158
×
190
¥
222
Ì
254
31
63
?
95
_
127
159
ƒ
191
┐
223
▀
255
Bibliografia
BERRY, J. Programando em C++. São Paulo: Makron Books, 1991.
ECKEL, B. C++. São Paulo: McGraw-Hill, 1991.
ELLIS, M. A. et alli C++: Manual de referência completo. Rio de Janeiro: Campus, 1993.
IBPI. Dominando a Linguagem C. Rio de Janeiro: IBPI, 1993.
MIZRAHI, V. V. Treinamento em Linguagem C. São Paulo: McGraw-Hill, 1990.
PAPPAS, C. H.; MURRAY, W. Turbo C++ completo e total. São Paulo: McGraw-Hill, 1991.
SCHILDT, H. C Completo e total. São Paulo: McGraw-Hill, 1991.
0
9
7
5
3
1
25
25
=
-
-
-
-
-
=
PI
h
=72.7×alt−58
PI
m
=62.1×alt−44.7
t
v
a
D
D
=
i
f
v
v
v
-
=
D
L
8
8
7
8
7
6
5
6
5
4
3
4
3
2
1
2
2
=
p
d
b
f
a
c
e
g