Professional Documents
Culture Documents
DE COMPUTADORES
DISCIPLINAS REGULARES
1o Módulo:
Técnicas de
Programação
ÍNDICE
1
CAPÍTULO 1 - INTRODUÇÃO À LINGUAGEM C
Por muitos anos, o principal objetivo dos programadores foi o de escrever programas
pequenos e rápidos. Os programas precisavam ser pequenos porque memória era um
recurso caro e, por este motivo, limitado. Além disso, o poder de processamento das
máquinas então disponíveis era, provavelmente, muito menor do que o de uma simples
calculadora de bolso com a qual estamos acostumados nos dias de hoje. Não raras eram as
aplicações (que hoje seriam consideradas pequenas e simples) em que o computador
processava por dias até gerar resultados úteis. Por este motivo, os programadores tinham
de preocupar-se com a otimização do código gerado a fim de que a aplicação executasse
no menor tempo possível.
Resolvendo Problemas
2
sobre o funcionamento de computadores. Estes usuários estão mais interessados em
resolver os seus problemas do que em entender o funcionamento da máquina. Ironicamente,
para facilitar o uso dos programas por parte deste novo público, estes se tornaram muito
mais complexos e sofisticados. Os usuários hoje em dia estão familiarizados com janelas,
menus, caixas de diálogo, botões e uma série de mecanismos que visam tornar mais
amigável a interface com o computador. Os programas escritos para suportar estas
facilidades são muito mais complexos do que os programas escritos há 10 ou 20 anos atrás.
Na medida em que as exigências mudaram, mudaram também as linguagens e as técnicas
usadas para escrever programas.
Acessar o registro de cada empregado pode, por sua vez, ser quebrado em:
3
desenvolvimento das chamadas linguagens orientadas a objeto que não são, no entanto,
escopo deste livro.
A Linguagem C
A linguagem C tem sido fortemente associada ao sistema operacional UNIX por ter
sido desenvolvida nesse sistema e por ser o próprio UNIX escrito em C. No entanto, a
linguagem não é amarrada a nenhuma máquina ou sistema operacional em particular. Do
mesmo modo, C não é dedicada a nenhuma área de aplicação específica, tendo sido usada
com sucesso em aplicações numéricas, processamento de texto, software básico e banco
de dados.
Um exemplo:
#include <stdio.h>
main() /* primeiro programa */
{
printf("Alo mundo!");
}
Saída:
Alo mundo!
Análise:
Sabemos que printf( ) é uma função por causa dos parênteses que a seguem.
printf( ) é uma das funções de entrada/saída que podem ser usadas em C. Ela
simplesmente imprime na saída padrão (o terminal de vídeo, no nosso caso) os caracteres
entre aspas. Vale salientar que a entrada/saída em C, ao contrário do que acontece na
maioria das outras linguagens, não é efetuada através de comandos, mas sim processada na
forma de funções que, juntas, compõem a biblioteca padrão. Isso se deve à filosofia de
portabilidade da linguagem, uma vez que as funções da biblioteca padrão são dependentes
da máquina e, por esse motivo, podem ser facilmente reprogramadas quando necessário.
Por este motivo, escrevemos no início do programa a diretiva #include <stdio.h>
que instrui o compilador a incluir as funções da biblioteca padrão (entre as quais printf( )) no
nosso programa. Nos exemplos futuros , omitiremos a diretiva #include <stdio.h>.
No entanto, ela será sempre necessária quando usarmos funções da biblioteca padrão.
Erros de Compilação
Erros de compilação podem acontecer por uma série de razões. Normalmente eles são
resultado de erros de datilografia ou outros pequenos erros. Os bons compiladores indicam
não somente a existência de erros no programa como também o lugar onde eles ocorreram.
Alguns chegam mesmo a sugerir o procedimento para corrigir o erro. Vamos verificar as
mensagens de erro do compilador introduzindo intencionalmente alguns erros no programa
“Alô mundo!”. Remova, por exemplo, a última chave do programa anterior e o recompile
Compiling \MUNDO.C:
Error \MUNDO.C 5: Compound statement missing }
Estilos de Programação
Em C não há um estilo obrigatório. Desse modo, você pode inserir espaços, caracteres
de tabulação e pular linhas à vontade pois o compilador ignora esses caracteres. Assim,
nosso programa poderia ser escrito como:
main() {
printf("Alo mundo!"); }
ou,
main () {
printf ("Alo mundo!");
}
6
• Outras são repetidas um determinado número de vezes;
• Outras podem ser separadas em módulos que podem ser executados em diferentes
localizações do seu programa.
7
CAPÍTULO 2 - SAÍDA DE DADOS
Pode parecer curioso começar os nossos estudos pela saída de dados, mas um
programa que, de alguma forma, não externa resultados não é muito útil. Essa saída de
dados normalmente se dá através da tela do computador, ou através de um dispositivo de
armazenamento de dados (discos rígidos ou flexíveis) ou ainda, através de uma porta de
entrada/saída (saídas seriais, impressoras, etc.).
A função printf( )
Um exemplo:
main() {
printf("Este e' o Capitulo %d (dois)", 2);
}
Saída:
A expressão de controle:
8
O código %d no exemplo anterior diz que o argumento correspondente deve ser um inteiro
decimal. A seguir, encontram-se alguns outros códigos de formatação habitualmente usados:
main()
{
printf("%s | Nucleo\n", "NCE");
printf(" | de Computacao\n | Eletronica");
}
Saída:
NCE | Nucleo
| de Computacao
| Eletronica
Análise:
9
Os caracteres, tais como o \n (caracter de mudança de linha), que não podem ser obtidos
diretamente do teclado, são escritos em C como a combinação do sinal \ (barra invertida)
com outros caracteres. A tabela seguinte mostra outros códigos de C para tais caracteres:
%[-][tamanho][.precisão]{d,o,x,u,c,s,e,f}
os itens entre [ ] são opcionais e as letras entre { } representam o tipo do dado sendo
impresso (decimal, octal, etc.) e apenas uma deve ser escolhida.
printf("123456789012345678901234567890\n");
printf("%10s%10c%10s\n", "Ano", ' ', "Valor");
printf("%9d%11c%10d\n", 1, ' ', 1000);
printf("%9d%11c%10d\n", 2, ' ', 2560);
printf("%9d%11c%10d\n", 3, ' ', 6553);
10
123456789012345678901234567890
Ano Valor
1 1000
2 2560
3 6553
Nos casos em que a expressão é do tipo real, o parâmetro [.precisão] define com
quantas casas decimais o número deve ser escrito.
Exemplo:
printf("1234567890\n");
printf("%4.2f\n", 3456.78);
printf("%3.2f\n", 3456.78);
printf("%3.1f\n", 3456.78);
printf("%10.3f\n", 3456.78);
Saída:
1234567890
3456.78
3456.78
3456.8
3456.780
printf("123456789012345678901234567890\n");
printf("%-10s%10c%-10s\n", "Ano", ' ', "Valor");
printf("%-9d%11c%-10d\n", 1, ' ', 1000);
printf("%-9d%11c%-10d\n", 2, ' ', 2560);
printf("%-9d%11c%-10d\n", 3, ' ', 6553);
Saída:
123456789012345678901234567890
Ano Valor
1 1000
2 2560
3 6553
11
printf("1234567890");
printf("\n%04d", 21);
printf("\n%06d", 21);
Saída:
1234567890
0021
000021
Existem duas outras funções para a saída de dados que podem ser úteis na construção de
programas: a função puts( ) que imprime uma string na tela do computador e a função
putchar( ) que imprime um único caracter.
puts imprime uma string em stdout (e insere um caracter de nova linha ao final). O
endereço da string deve ser passado para puts() como argumento
Declaração:
Valor de Retorno:
Em caso de sucesso,
puts() retorna um valor não negativo.
Em caso de erro,
puts() retorna o valor de EOF.
Exemplo:
puts("NCE | Nucleo");
puts(" | de Computacao");
puts(" | Eletronica");
Saída:
NCE | Nucleo
| de Computacao
| Eletrônica
12
Observe que não foi acrescentado o caracter de nova linha (\n) ao final de cada string a ser
impressa. Isto não é necessário uma vez que puts( ) automaticamente muda de linha ao
final da impressão. Assim, as duas instruções seguintes são equivalentes:
puts("string");
printf("%s\n", "string");
Declaração:
Valor de Retorno:
Em caso de sucesso,
putchar() retorna o caracter ch
Em caso de erro,
putchar() retorna EOF.
putchar('c');
printf("%c", 'c');
13
CAPÍTULO 3 - Tipos de dados em C
Quando você escreve um programa, você está lidando com algum tipo de informação.
Esta informação pode ser classificada, na maioria dos casos, em 4 grandes grupos: inteiros,
números em ponto flutuante, texto e ponteiros.
Inteiros são os números que usamos para contar ou enumerar um conjunto (1, 2, -56,
735, por exemplo)
Números em ponto flutuante são números que têm uma parte fracionária (3.1415)
e/ou um expoente (1.0E+24). São também chamados de números reais.
Texto é composto de caracteres ('a', 'f', '%') e cadeia de caracteres ("Isto e'
uma cadeia de caracteres")
Constantes e Variáveis
Constantes
As constantes têm valor fixo e inalterável. Nos exemplos do Capítulo anterior,
mostramos o uso de constantes numéricas e constantes cadeia de caracteres.
a) Constantes caracteres
Uma constante caracter é um único caracter escrito entre plicas (') o qual pode
participar normalmente de expressões aritméticas. O seu valor é o correspondente ao seu
código ASCII. Como exemplo temos:
14
'a' (caracter a)
'A' (caracter A)
'\'' (caracter ')
'\\' (caracter \)
'\n' (caracter mudança de linha)
'\0' (caracter nulo)
'\033' (caracter escape em octal)
"NCE"
"Este e' o Capítulo 3"
c) Constantes inteiras
São valores numéricos sem ponto decimal, precedidos ou não de sinal. Se uma
constante inteira começa com 0x ou 0X ela é hexadecimal. Se ela começa com o dígito 0,
ela é octal. Em caso contrário ela é uma constante decimal.
28 // decimal 28
0x1C // representação hexadecimal do decimal 28
034 // representação octal do decimal 28
1982
-76
Outros exemplos:
// Constantes Decimais
10
132
32179
// Constantes Octais
077
010
03777
// Constantes Hexadecimais
0xFFFF
0x7db3
0x10
15
São constantes armazenadas com o dobro do número de bytes das constantes inteiras1
capazes, por este motivo, de armazenar valores maiores. Para diferenciá-las das constantes
inteiras, os valores longos têm como sufixo a letra L ou l. Alguns exemplos de constantes
inteiras longas:
3.1415926
3.14e0
-10.0e-02
100.0F
100.0L
Constantes Enumeradas
1
Esta informação é verdadeira para o compilador Borland C rodando em PCs. Para outros compiladores a
afirmação pode não ser correta. Para o compilador gcc, por exemplo, rodando em ambientes UNIX, as
constantes inteiras e as constantes longas têm o mesmo tamanho em bytes.
16
A sintaxe para constantes enumeradas consiste na palavra reservada enum seguida pelo
nome do tipo e, em seguida, a lista dos valores possíveis, entre chaves e separados por
vírgulas.
Exemplo:
• Ela cria uma constante simbólica VERMELHO com o valor 0, uma constante simbólica
AZUL com o valor 1, VERDE com o valor 2 e assim por diante.
Toda constante enumerada tem um valor inteiro. Se nada for especificado, a primeira
constante tem o valor 0, a segunda tem o valor 1 e as demais seguem seqüencialmente a
partir daí. No entanto, qualquer das constantes pode ser inicializada com um valor em
particular, e aquelas que não forem inicializadas têm o seu valor obtido pelo incremento das
constantes anteriores.
Exemplo:
VERMELHO tem o valor 100, AZUL o valor 101, VERDE o valor 500, BRANCO o
valor 501 e PRETO o valor 700.
Você pode declarar variáveis do tipo COR, e estas variáveis deveriam assumir somente
valores correspondentes a uma das constantes enumeradas (no caso do exemplo,
VERMELHO, AZUL, VERDE, BRANCO ou PRETO). Você pode atribuir qualquer uma
das cores a uma variável do tipo COR. Na verdade, você pode atribuir qualquer número
inteiro a uma variável do tipo COR, ainda que este número não corresponda a uma cor
válida (ainda que um bom compilador deva emitir uma mensagem de advertência se você
fizer esta atribuição). É importante entender que as variáveis enumeradas são, de fato,
variáveis unsigned int e que as constantes enumeradas correspondem a constantes
inteiras. É, no entanto, muito conveniente poder dar nomes a estas constantes quando se
está trabalhando com cores, dias da semana ou conjuntos similares de valores. O programa
abaixo mostra um exemplo do uso de constantes enumeradas.
17
1. enum Dias {Seg, Ter, Qua, Qui, Sex, Sab, Dom};
2. void main() {
3. Dias Folga;
Análise
A constante enumerada Dias é definida na linha 1. O usuário escolhe um dia de folga na
linha 13. O valor escolhido, um número entre 0 e 6, é comparado na linha 14 com as
constantes enumeradas para Sábado e Domingo e a ação apropriada é tomada.
Você não pode digitar a palavra “Sab” quando o programa pede o dia de folga. O
programa não sabe como associar os caracteres em “Sab” a uma das constantes
enumeradas.
Variáveis
As variáveis são o aspecto fundamental de qualquer linguagem de programação. Uma
variável é um espaço de memória reservado para armazenar um certo tipo de dado e tendo
um nome para referenciar o seu conteúdo, o qual, ao contrário das constantes, pode variar
ao longo da execução do programa. Vamos estudar o seguinte exemplo:
1. main() {
2. int count;
3. count = 2;
4. printf("Este e' o numero dois: %d\n", count);
5. count = 3;
6. printf("e este e' o numero tres: %d", count);
7. }
Saída:
A instrução na linha 3 do nosso programa exemplo atribui o valor 2 à variável count, isto é,
em tempo de execução, o inteiro 2 é armazenado na posição de memória reservada pelo
compilador para a variável count.
A instrução seguinte printf( ) já é nossa conhecida. Ela imprime o conteúdo da
variável count na tela do computador.
A instrução na linha 5 altera o conteúdo da variável count, a qual é novamente impressa
pela instrução printf( ) seguinte.
Tipos de Variáveis
Como foi visto anteriormente, os dados manipulados por um programa podem ser
classificados em quatro grandes grupos: inteiros, números em ponto flutuante, texto
(caracteres e cadeia de caracteres) e ponteiros. Cada um desses tipos será visto com mais
detalhes a seguir.
main() {
int a, b;
float ratio;
a = 10;
b = 3;
ratio = a/b;
printf("O resultado da divisao e': %f", ratio);
}
Saída:
3.000000
19
Você provavelmente esperava que o resultado da divisão desse 3.333333. Por que o
resultado encontrado? Porque a e b são inteiros. A divisão é efetuada entre dois números
inteiros obtendo um resultado inteiro (3) o qual é posteriormente convertido para float e
atribuído à variável ratio. Rode o programa no seu computador trocando o tipo das
variáveis a e b para float e observe que agora obtém-se o resultado esperado.
Variáveis inteiras
Em adição ao tipo int, C suporta também os tipos short int e long int, geralmente
abreviados como short e long. O número de bytes ocupados por cada um desses tipos
depende do compilador e da máquina usados, mas, no Compilador da Borland para PCs,
eles ocupam 2 bytes (short), 2 bytes (int) e 4 bytes (long)
char ch;
ch = 'a'; /* caracter a */
ch = 97; /* codigo ASCII em decimal */
ch = 0x61; /* codigo ASCII em hexa */
ch = 0141; /* codigo ASCII em octal */
ch = '\x61'; /* caracter cujo codigo ASCII e' 0x61 */
Variáveis sem sinal
C permite que você declare certos tipos de variáveis (char, short, int, long) com o
modificador unsigned. Isto quer dizer que as variáveis declaradas com este modificador
podem assumir somente valores não negativos.
20
Variáveis sem sinal podem assumir valores maiores do que variáveis com sinal. Por
exemplo, uma variável do tipo short pode assumir, no compilador Borland C, valores entre
-32768 e 32767 enquanto que uma variável unsigned short pode conter valores entre 0 e
65535. Ambas ocupam o mesmo espaço de memória. Elas apenas o usam de maneira
diferente. Observe o seguinte exemplo:
1. main() {
2. unsigned short j;
3. short i;
4. j=65000;
5. i=j;
6. printf("i = %d\nj = %u", i, j);
7. }
Saída:
i = -536
j = 65000
Análise:
Em C não existe um tipo string como o do Turbo Pascal para manipular cadeias de
caracteres. No entanto, a linguagem oferece duas maneiras diferentes de lidar com strings.
A primeira delas é declarar a cadeia de caracteres como um vetor de caracteres:
char cadeia[80];
char *ptr;
O asterisco (*) na frente de ptr informa ao compilador que ptr é um ponteiro para um
caracter, ou, em outras palavras, ptr contém um endereço de memória, onde está
armazenado um caracter. O compilador, no entanto, não aloca espaço para a cadeia de
caracteres, nem inicializa ptr com nenhum valor em particular.
21
O estudo mais detalhado de cadeias de caracteres será visto mais adiante, quando
estudarmos ponteiros e vetores.
Inicializando Variáveis
É possível combinar uma declaração de variável com o operador de atribuição (=) para
que a variável seja criada em memória e o seu conteúdo inicializado antes do início da
execução do programa propriamente dito. Observe o programa abaixo:
1. main() {
2. short count=5,
3. short sx=-8;
4. unsigned short ux=-8;
5. char ch='5';
6. printf("%d\n", count);
7. printf("%d\n", ch);
8. printf("%o\n", sx);
9. printf("%o\n", ux);
10. }
Saída:
5
53
177770
177770
Análise:
Nas linhas 2 e 3 as variáveis count e sx são inicializadas durante a declaração das mesmas
com os valores 5 e -8 (0xFFF8) respectivamente. Na linha 4 a variável unsigned ux é
inicializada com o valor -8, o qual é interpretado pelo compilador como o inteiro positivo
65528 (0xFFF8). Na linha 5 a variável do tipo char ch é inicializada com o código ASCII
do caracter ‘5’ (53). Observe que o conteúdo das posições de memória ocupadas por sx e
ux é o mesmo. A diferença consiste em que, possivelmente, este conteúdo será
interpretado de maneira diferente pelo compilador quando as variáveis forem acessadas
uma vez que sx é uma variável com sinal e ux uma variável unsigned. Nas linhas 6-9 os
conteúdos das variáveis são impressos. Observe que a saída ocasionada pelas linhas 8 e 9
é a mesma uma vez que em ambas as instruções as variáveis são interpretadas como um
número na base octal.
Nomes de Variáveis
22
Existem algumas regras em C que devem ser seguidas para dar nome às variáveis:
a) Todos os identificadores devem começar com uma letra (a..z, A..Z) ou um underscore
(_).
Além das regras de sintaxe, devemos escolher nomes para as variáveis que indiquem o
conteúdo armazenado por estas variáveis. Assim, por exemplo, uma variável que armazene
o salário de um empregado deve se chamar salário, e não xpto.
23
CAPÍTULO 4 - Operadores e expressões
Uma vez que as variáveis do seu programa contêm os dados a serem processados, o
que fazer com elas? Provavelmente combiná-las em expressões envolvendo operadores.
Esse será o assunto desse capítulo.
Operadores e Expressões
Os operandos podem ser constantes, variáveis ou valores fornecidos por funções. Por
exemplo, se A = 3 e B = 8 então a expressão (A + B) dá como resultado o valor 11.
O resultado de uma expressão também constitui um tipo que, em geral, é o mesmo dos
operandos envolvidos nesta expressão.
Precedência de operadores
Exemplo:
A+ B
1 *3
2 C −D
1424 1
3
1442 244 3
3
24
Categoria Operador O que ele é (ou faz)
1. Prioridade mais alta ( ) Chamada de função
[ ] Índice de vetor
→ Acesso indireto a membro de estrutura
. Acesso direto a membro de estrutura
4. Multiplicativos * Multiplicação
/ Divisão
% Resto da divisão inteira
8. Igualdade == Igual a
!= Diferente
25
13. || OR lógico
Os operadores
O operador de atribuição:
soma = a + b;
fator = 3.0;
a = b = c = 3.0;
Os operadores aritméticos:
26
C suporta o conjunto usual dos operadores aritméticos:
* (multiplicação)
/ (divisão)
% (módulo)
+ (adição)
− (subtração)
O operador módulo (resto da divisão inteira) não pode ser aplicado a variáveis do tipo
float nem double.
É possível incluir expressões envolvendo operadores aritméticos (ou qualquer outro tipo de
operadores) diretamente em printf( ). Exemplo:
main() {
int i=3;
int j=4;
Em C, qualquer atribuição entre parênteses é considerada como uma expressão que tem o
valor da atribuição sendo feita. Exemplo:
a = 3+2*(b=7/2);
b = 7/2;
a = 3+2*b;
A linguagem C possui alguns operadores que permitem escrever uma instrução de forma
muito compacta, gerando, assim, um código bastante otimizado. Um exemplo de tais
operadores são os operadores unários de incremento (++) e decremento (−−). Tais
operadores incrementam ou decrementam os seus operandos de 1 unidade. A adição ou
subtração pode ser efetuada no meio de uma expressão e pode-se mesmo escolher se o
27
incremento/decremento será feito antes ou depois do cálculo da expressão. Sejam os
seguintes exemplos:
m = 3.0 * n++;
m = 3.0 * ++n;
A primeira instrução diz: “Multiplique n por 3.0; atribua o resultado a m e então incremente
n de 1 unidade”. A segunda diz: “Incremente n de 1 unidade; multiplique n por 3.0 e atribua
o resultado a m”.
i++;
++i;
i = i + 1;
Operadores em bits:
| (ou lógico)
uso: op1 | op2;
descrição: é feito um ou lógico dos bits correspondentes de op1 e op2 usando a seguinte
tabela verdade:
28
op1 op2 |
0 0 0
0 1 1
1 0 1
1 1 1
∧ (ou exclusivo)
uso: op1 ∧ op2;
descrição: é feito um ou exclusivo entre os bits correspondentes de op1 e op2 usando a
seguinte tabela verdade:
op1 op2 ∧
0 0 0
0 1 1
1 0 1
1 1 0
∼ (negação ou complemento)
uso: ∼op1;
descrição: todos os bits de op1 são invertidos segundo a seguinte tabela verdade:
op1 ∼
0 1
1 0
Observações:
a = a * 16;
a = a << 4;
Você deve estar se perguntando porque então usar os operadores de deslocamento. Isto
se deve ao fato de que um deslocamento à direita ou à esquerda é uma operação
rapidíssima existente, em geral, no assembly da máquina. Por outro lado, multiplicações
ou divisões de inteiros são instruções complexas que demandam vários ciclos de
máquina para serem executadas.
A seguir, encontram-se alguns exemplos de tais expressões e como elas podem ser
compactadas:
a = a + b; ⇒ a += b;
a = a - b; ⇒ a -= b;
a = a * b; ⇒ a *= b;
30
a = a / b; ⇒ a /= b;
a = a % b; ⇒ a %= b;
a = a << b; ⇒ a <<= b;
a = a >> b; ⇒ a >>= b;
a = a & b; ⇒ a &= b;
a = a | b; ⇒ a |= b;
a = a ^ b; ⇒ a ^= b;
Operadores de endereço:
C suporta dois operadores que lidam com endereços: o operador devolve endereço (&), e
o operador de acesso indireto (*).
main() {
int x=2;
Um endereço de memória é tratado como um inteiro sem sinal. A saída desse programa
varia conforme a máquina e o endereço de memória onde o programa é carregado. Uma
saída possível é:
valor = 2
endereco = 1370
A expressão *ptr devolve o conteúdo da posição de memória apontada por ptr. Este
operador será visto com mais detalhes quando estudarmos ponteiros.
Conversão de tipos
31
Além da prioridade dos operadores, quando avaliarmos uma expressão devemos levar em
conta também a conversão de tipos que se dá quando os operadores são de tipos
diferentes. Esta conversão segue algumas regras básicas:
a) Expressões envolvendo variáveis char e short são sempre convertidas para int.
double
float
long
unsigned
int, char ou short
Isto é, se, por exemplo, um dos operandos for do tipo double, toda a expressão será
convertida e o resultado será do tipo double.
Exemplo:
1. main() {
2. int a=10;
3. int b=3;
4. float c;
5. c=a/b;
6. printf(“Resultado = %f\n”, c);
7. c=(float)a/b;
8. printf(“Resultado = %f\n”, c);
9. }
Saída:
Resultado = 3.000000
Resultado = 3.333333
Análise:
Na linha 5 a expressão inteira a/b é avaliada e produz um resultado inteiro (3) o qual é,
posteriormente, convertido para float e armazenado na variável c. Na linha 7, através da
conversão forçada de tipo, a expressão é convertida para uma expressão em ponto
32
flutuante a qual é avaliada e produz o resultado correto (3.333333) que é armazenado na
variável c.
33
CAPÍTULO 5 - Entrada de dados
A função scanf( )
Para entrada de dados a partir do teclado você, na maior parte das vezes, utilizará a função
scanf( ). A função scanf( ) é equivalente à função printf( ). Seu formato é:
A função scanf( ) lê uma seqüência de campos de entrada (um caracter por vez), formata
cada campo de acordo com um padrão de formatação passado na string “expressão de
controle” e armazena a entrada formatada em um endereço passado como argumento,
seguindo a string “expressão de controle”.
scanf( ) usa a maioria dos códigos de formatação utilizados por printf( ). Dessa forma,
usa-se %d quando se quer ler um inteiro, %f para números em ponto flutuante, %c para
caracteres, etc. Espaços em branco na expressão de controle são ignorados.
Existe uma diferença fundamental, no entanto, entre scanf( ) e printf( ): os itens seguindo a
expressão de controle são os endereços das variáveis que vão receber os valores lidos e
não, como em printf( ), as próprias variáveis. A explicação para esta distinção ficará clara
posteriormente, quando estudarmos ponteiros, funções e passagem de parâmetros por
referência. Por ora, é suficiente ter em mente esta distinção. A falta do operador de
endereço (&) antes do nome das variáveis a serem lidas não acarreta em erro de
compilação. A execução de tal programa, no entanto, terá resultados imprevisíveis.
A função scanf( ) retorna o número de parâmetros lidos, convertidos e armazenados com
sucesso.
Exemplo:
1. main() {
2. int a;
3. int b;
4. int soma;
34
Análise:
A instrução na linha 5 faz com que o programa pare a execução e espere você digitar dois
números inteiros. Os números podem ser separados por um ou mais espaços em branco,
tabs, ou enters . O primeiro número lido é atribuído à variável a, e o segundo à variável b.
Mas, e se quisermos que os valores de a e b sejam digitados separados por vírgulas?
Nesse caso teríamos de modificar a chamada à scanf( ) que ficaria:
Deve existir uma exata correspondência entre os caracteres diferentes de branco existentes
na expressão de controle e a seqüência digitada via teclado. Por exemplo, se quisermos
entrar com os valores a = 3 e b = 5:
3,5 ⇒ correto
3.5 ⇒ errado
3, 5 ⇒ correto
3 ,5 ⇒ errado
Outro erro freqüente consiste na tentativa de se limitar o tamanho dos campos de entrada
quando da leitura das variáveis:
A limitação do tamanho dos campos dos dados só tem sentido quando do uso de funções
de saída de dados. Assim, a forma correta da instrução acima seria, por exemplo:
scanf(“%f”, &f);
printf(“%10.5f”, f);
A função scanf( ) interrompe sua execução quando todos os valores são lidos, ou quando a
entrada não combina com a expressão de controle.
Lendo Strings
Vamos nos antecipar um pouco ao estudo de vetores (como foi visto no Capítulo 3, uma
string em C é um vetor de caracteres) e ver como podemos ler uma string do teclado.
Ler uma string consiste de dois passos: reservar espaço de memória para armazená-la e
usar alguma função que permita a sua leitura.
O primeiro passo é declarar a string especificando o seu tamanho. Por exemplo:
35
char
nome[30];
Esta declaração aloca 30 bytes consecutivos de memória nos quais as letras do nome serão
armazenadas, uma por byte.
Uma vez reservado o espaço necessário você pode usar as funções scanf( ) ou gets( ) da
biblioteca C para ler a string.
main() {
char nome[30];
Saída:
Análise:
Observe que não foi usado o operador de endereço (&) antes de nome. Isto ocorreu
porque em C o nome de um vetor corresponde ao endereço do primeiro byte do vetor. Se
você não quiser usar essa elegante característica da linguagem C, poderá escrever (o que
absolutamente não recomendamos):
scanf("%s", &nome[0]);
Observe que o programa só usou o primeiro nome na sua saudação. Isto ocorre por que a
função scanf( ) interpreta o branco após o Carlos, como um sinalizador de final de string.
Mas, e se quiséssemos ler uma string com brancos entre os nomes? Nesse caso, a solução
mais recomendada é usar a função gets( ) que será vista a seguir.
A função gets( )
36
main() {
char nome[30];
Saída:
Análise:
A função gets( ) lê tudo o que você digita até que você pressione Enter. O caracter Enter
não é acrescentado à string.
A função getchar( )
main() {
char ch;
2
Em verdade, getchar( ) é uma macro declarada em <stdio.h>. A distinção entre macros e funções será
vista mais adiante.
3
Em verdade, estas funções estão disponíveis apenas em compiladores C para máquinas do tipo PC tais
como o compilador da Borland e o Microsoft C. Em máquinas do tipo RISC, para que os caracteres
digitados pelo usuário sejam imediatamente disponíveis para o programa, é necessário combinar a
utilização de getchar( ) com funções da biblioteca “curses” (libcurses.a).
37
printf("O caracter que voce digitou foi o %c.", ch);
}
Saída:
Análise:
Observe que a função getchar( ) não aceita argumentos. Ela devolve o caracter lido para a
função que a chamou. No nosso exemplo, esse caracter foi atribuído à variável ch.
38
CAPÍTULO 6 - Desvio condicional
Existem duas classes de operadores sobre as quais ainda não falamos: os operadores
relacionais e os operadores lógicos.
Operadores relacionais
1. main() {
2. int capitulo=6;
Saída:
Este e' o capitulo 5? 0
Este e' o capitulo 6? 1
Análise:
39
Na linha 3, a expressão booleana (capitulo==5) é avaliada e o resultado da
comparação (Falso ou 0) é impresso por printf( ). De modo análogo, na linha 4, o
resultado da comparação (capitulo==6) é impresso por printf( ).
o compilador não retornaria nenhuma mensagem de erro, uma vez que não existe nenhum
erro de sintaxe e sim um erro de lógica. A saída gerada por essa instrução seria:
Operadores lógicos
Esses operadores não devem ser confundidos com os operadores em bits (&, |, ~)
descritos anteriormente. Os operadores lógicos trabalham com valores lógicos (verdadeiro
ou falso), permitindo que você combine expressões relacionais.
Exemplos:
Seja a == 3,
(a>0) && (a<=4) ⇒ Verdadeiro
(a>0) || (a==-3) ⇒ Verdadeiro
40
!(a==5) ⇒ Verdadeiro
O COMANDO if
V F
CONDIÇÃO
COMANDO 1 COMANDO 2
(BLOCO 1) (BLOCO 2)
1) if (condição)
comando;
41
A condição é avaliada. Se o seu valor é verdadeiro, comando é executado. O
processamento continua no comando seguinte ao if.
2) if (condição)
comando1;
else
comando2;
Nas duas formas acima, comando pode ser tanto um comando simples quanto um bloco de
comandos.
Vejamos um exemplo:
Pseudocódigo:
LEIA A
SE A > 0 ENTÃO
IMPRIMA "A é maior que zero"
em C:
main() {
int A;
Caso mais de uma ação tenha que ser executada, deve-se utilizar o bloco de comandos
como mostra o seguinte exemplo:
LEIA A
42
Se A > 0
IMPRIMA "A E’ MAIOR QUE ZERO"
Senão
IMPRIMA "A E’ MENOR OU IGUAL A ZERO"
Fim Se
Em C:
main() {
int a;
if (a!=0)
e
if (a)
Deve-se tomar muito cuidado também aqui, para não confundir o operador de atribuição
(=) com o operador relacional (==). Por exemplo, sejam as seguintes instruções.
if (a==8)
comando;
if (a=8)
comando;
if aninhados
43
Um comando if pode estar dentro de um outro comando if. Dizemos então que o if interno
está aninhado.
Exemplo:
Análise:
Observe a construção na linha 1. Não há nela nenhuma novidade. Como vimos antes, em
C, qualquer atribuição entre parênteses é considerada como uma expressão que tem o valor
da atribuição sendo feita.
onde poderíamos ficar em dúvida sobre a qual if corresponde o else, são resolvidos
assumindo-se que cada else é sempre associado ao mais recente if ainda sem else.
Mas, e se quiséssemos que o else no exemplo anterior pertencesse ao primeiro if? Nesse
caso, você tem de usar chaves:
O comando switch
44
Isto, porém, pode tornar-se muito trabalhoso e de difícil compreensão quando tivermos
muitas alternativas, como mostra o exemplo a seguir.
main() {
char ch;
ch = getchar();
if (ch=='a') {
// entre com os dados
.
.
}
else
if (ch=='b') {
// processe dados
.
.
}
else
if (ch=='c') {
// imprima relatório
.
.
}
else
puts("Opcao invalida");
}
Para reescrever este mesmo programa de forma mais simples, C fornece o comando
switch, que tem o seguinte formato:
switch (expressão)
{
case Constante1:
lista de comandos 1;
[break;]
case Constante2:
lista de comandos 2;
[break;]
.
.
.
case ConstanteN:
lista de comandos N;
[break;]
default: /* opcional */
lista de comandos default;
}
45
1. A expressão é avaliada;
2. Se o resultado da expressão for igual a uma das constantes, então a execução começa a
partir do comando associado à essa constante e prossegue com a execução de todos os
comandos até o fim do switch, ou até que se encontre uma instrução break;
3. Se o resultado da expressão não for igual a nenhuma das constantes e você tiver incluído
no comando switch a opção default, o comando associado ao default é executado. Caso
contrário, isto é, se a opção default não estiver presente, o processamento continua a
partir do comando seguinte ao switch.
main() {
char ch;
ch = getchar();
switch (ch) {
case 'a':
/* entre com os dados */
.
.
break;
case 'b':
/* processe dados */
.
.
break;
case 'b':
/* imprima relatório */
.
.
break;
default:
puts("Opcao invalida");
}
}
Observações:
• Pode haver uma ou mais instruções seguindo cada case. Estas instruções não precisam
estar entre chaves;
46
• O comando break causa uma saída imediata do switch. Se não existir um break
seguindo as instruções associadas a um case, o programa segue executando todas as
instruções associadas aos case abaixo. Esta característica pode ser interessante em
algumas circunstâncias. Uma delas é quando temos o mesmo procedimento associado a
mais de uma constante diferente. Por exemplo, no programa anterior poderíamos querer
fazer a seleção de procedimentos usando também letras maiúsculas:
switch (ch) {
case 'A':
case 'a':
/* entre com os dados */
.
.
break;
case 'B':
case 'b':
/* processe dados */
.
.
break;
case 'C':
case 'c':
/* imprima relatório */
.
.
break;
default:
puts("Opcao invalida");
}
main() {
int a;
int b;
int max;
47
Observe que o programa teve de escolher entre duas sentenças (max = a ou max = b
baseado em uma condição (a > b ou a <= b). Esta é uma situação tão comum em
programação de computadores, que em C existe uma construção especial para fazer essa
seleção: o operador condicional.
Forma geral:
O operador condicional pode ser visto como uma única expressão. Sua interpretação é a
seguinte: “Se expressão1 é verdadeira, a expressão toda toma o valor de expressão 2;
senão a expressão toma o valor de expressão3”.
main() {
int a;
int b;
int max;
main() {
int a;
int b;
Expressões com o operador condicional não são necessárias, visto que o comando if-else
pode substituí-las. São, entretanto, mais compactas e, em geral, geram um código de
máquina menor.
48
CAPÍTULO 7 - Laços
Assim como existem comandos que você deseja executar condicionalmente, podem existir
outros comandos que você deseja executar repetidamente. Laços são usados, tipicamente,
em situações onde você deseja repetir um dado procedimento até que aconteça alguma
coisa (uma tecla em particular seja pressionada ou uma variável contenha um dado valor).
Existem três tipos de laços em C: o laço while, o laço for e o laço do...while. Nós
estudaremos os três nessa ordem.
O laço while
O laço while é o mais genérico dos três e pode ser usado para substituir os outros dois, ou,
em outras palavras, o laço while é tudo o que você precisa. Os outros dois existem somente
para a sua conveniência.
while (condição)
comando;
1. A condição é avaliada:
2. Se condição for verdadeira então o comando é executado. Senão, a execução vai para
o passo 4;
4. Fim do comando.
Pela ordem de execução vista acima, percebemos que, se a expressão tiver valor falso na
primeira vez em que for avaliada, o comando associado não será executado nem mesmo
uma vez.
49
1. main() {
2. int counter = 0;
3. while(counter < 5) {
4. counter++;
5. printf(“%d\n”, counter);
6. }
7. puts(“Fim do Programa”);
8. }
1. main() {
2. int counter = 0;
3. while(counter++ < 5)
4. printf(“%d\n”, counter);
5. puts(“Fim do Programa”);
6. }
Saída:
1
2
3
4
5
Fim do Programa
Análise:
Podemos utilizar ainda construções onde não existe nenhum comando associado ao while.
Exemplo:
O laço anterior lê um caracter do teclado até que ele seja o caracter 'a'. Observe que
tivemos de colocar o ponto e vírgula (;) após o while para indicar a ausência de comandos.
50
Laços while mais complexos
Uma ou mais expressões lógicas podem ser combinadas para gerar a condição do laço
while. Observe o seguinte exemplo:
Análise:
O programa consiste em uma espécie de jogo. O objetivo é determinar em que ponto uma
variável inteira, small, se encontra com a variável large. O valor inicial das variáveis é
fornecido pelo usuário. Em cada passagem do laço (linhas 9-12) o valor de small é
incrementado de uma unidade e o valor de large é decrementado de 2 unidades.
Observe a condição composta de parada do laço while na linha 9. O programa termina
quando (small < large) mas, além disso, o valor atribuído à variável small não
pode ser maior do que MAXINT, uma constante definida na linha 1 contendo o maior valor
permissível para uma variável do tipo int.
O LAÇO for
1. main() {
2. int i;
51
3. for (i = 1; i <= 5; i++)
4. printf("%d\n", i);
5. }
Análise:
Observe que dentro dos parênteses existem 3 expressões separadas por ponto e vírgula:
expressão1;
while (expressão2) {
comando;
expressão3;
}
Qualquer uma das três expressões pode ser omitida mas os ponto e vírgulas devem
permanecer. Exemplo:
main() {
int i=1;
52
Se você omitir expressão2, o compilador assume que ela é verdadeira, o que resulta em
um laço que nunca termina, ou, um laço eterno. Laços eternos são construções
freqüentemente encontradas em programas de computadores. Eles não terminam devido ao
teste de uma condição. Ao invés disso, o programador confia que a ocorrência de algum
evento durante a execução do laço force o desvio do fluxo para fora do mesmo. Assim,
visando a clareza do código escrito, muitos programadores C costumam usar a seguinte
construção para a criação de laços eternos:
1. #define EVER ;;
2. for(EVER) {
3. // comandos...
4. }
O define na linha 1 cria uma constante simbólica EVER que faz com que as três
expressões no for da linha 2 sejam omitidas, resultando em um laço eterno. A diretiva
define será vista com mais detalhes posteriormente, quando estudarmos o pré-
processador C.
Por outro lado, qualquer uma das expressões de um laço for podem conter várias
instruções separadas por vírgulas. Vejamos através de um exemplo:
main() {
int i;
int j;
Observe que a primeira e a última expressões do for são constituídas de 2 instruções cada
uma, inicializando e modificando as variáveis i e j.
main() {
int i;
53
Laços for aninhados
Pode-se ter um for dentro de outro for. Seja, por exemplo, o seguinte programa que
imprime a tabuada:
main() {
int i;
int j;
O laço externo executa 10 vezes, enquanto que o laço interno executa 10 vezes para cada
passagem do laço externo, totalizando 10 * 10 = 100 passagens pela função printf( ).
O LAÇO do...while
Forma genérica:
do
comando
while (expressão);
1. Executa comando;
2. Avalia expressão;
4. Fim do comando.
Exemplo:
54
1. main() {
2. int i=1;
3. do {
4. printf("%d\n", i);
5. i++;
6. }
7. while (i<=5);
8. }
1. main() {
2. int i=1;
3. do
4. printf("%d\n", i);
5. while (++i<=5);
6. puts(acabou);
7. }
O comando break
1. void main() {
2. char ch;
3. int i;
55
Análise:
Você deve estar lembrado que nós já vimos o comando break quando estudamos a
instrução de desvio condicional switch. Ele fazia com que o processamento fosse desviado
para a instrução seguinte ao switch. Além do uso com o switch, o comando break pode
ser usado com qualquer uma das formas de laço (while, for e do...while).
O comando continue
void main() {
char ch;
int count=0;
while (count<10) {
ch=getchar();
getchar();
if(ch=='#') continue;
printf("%c\n", ch);
count++;
}
puts("Acabou!");
}
56
O comando goto
O comando goto causa o desvio da execução do programa para algum outro ponto dentro
do código.
Forma genérica:
goto rótulo;
Exemplo:
goto erro;
Para que esta instrução opere, deve haver um rótulo em algum outro ponto do programa.
Um rótulo é um nome seguido por dois pontos.
Exemplo:
erro:
puts("Deu pau!");
while (condição1) {
// comandos.
while (condição2) {
// comandos
while (condição3) {
// comandos
if (desastre)
goto ERRO;
}
}
}
ERRO:
// tratamento de erro
58
CAPÍTULO 8 - Funções
Nós já vimos como executar um trecho de código condicionalmente (if, switch, etc.) ou
iterativamente (while, for e do...while). Neste capítulo, consideraremos como executar o
mesmo trecho de código em diversos pontos do seu programa ou, o mesmo procedimento
para conjuntos de dados diferentes.
Introdução
Agora que você já conhece os comandos mais simples da linguagem e já escreveu e testou
vários programas pequenos, deve estar pensando: “Mas como eu faço para escrever um
programa maior?”
Uma das respostas para essa questão, está na essência do método usado na programação
estruturada: a redução da complexidade do problema que está sendo tratado. Isto pode ser
conseguido quebrando-se o problema num conjunto de subproblemas menores, até que
cada um dos subproblemas seja de solução imediata.
Podemos explicar melhor essa idéia por meio de um exemplo: Vamos supor que você
deseje construir um jogo de adivinhação de números. O jogador deve pensar um número e
dizer ao programa o intervalo onde o número se encontra, e o computador deve adivinhar o
número que o jogador pensou em um número razoável de tentativas. Por exemplo, se o
intervalo está entre 1 e 1000, o programa deve adivinhar o número, no pior caso, em 10
vezes.
A procura do número a ser adivinhado será feita pelo método de BUSCA BINÁRIA.
Tenta-se primeiro, o número no meio do intervalo. Se for igual ao número pensado pelo
jogador, a procura termina. Se o número do meio do intervalo for maior (menor) do que o
número do jogador, repete-se o processo para a metade inferior (superior) do intervalo.
59
Sabemos ainda, que as tarefas (2) e (3) têm que ser repetidas até que o resultado correto
seja encontrado.
Este programa pode ser representado pelo seguinte diagrama gráfico, também conhecido
por DIAGRAMA DE ESTRUTURAS:
PROGRAMA
ADIVINHA
PROCURAR
INICIAR VALOR
ATÉ ACERTAR
CALCULAR CONFERIR
VALOR VALOR
Vamos atacar o problema por partes. Primeiro, tratemos o programa principal, que pode
ser descrito informalmente da seguinte maneira:
60
começo
Iniciar
faça
CalcularValor
ConferirValor
enquanto não acertou
fim
Esta estrutura pode ser reproduzida fielmente por um trecho de programa C da seguinte
maneira:
main() {
Iniciar();
do {
CalcularValor();
ConferirValor();
}
while (!acertou);
}
Funções
Sabemos, com certeza, que cada um destes nomes corresponde a um módulo de programa.
Sabemos também, quais as tarefas que devem ser feitas em cada um deles. A questão, é
como definir um módulo em C?
61
Em C, todas as subrotinas são chamadas de funções. Teoricamente, uma função é um
procedimento que retorna um valor para o programa que a chamou. Esta distinção é bem
marcada em PASCAL, onde as subrotinas são divididas em procedures (que não
retornam valor) e functions (que retornam valor). A definição de função em C é mais
flexível: além do uso convencional, funções podem retornar valores que não são usados
pelo programa que as chamou, bem como não retornar valor nenhum.
A definição de uma função deve ser associada a um identificador, para que a mesma possa
ser ativada por uma chamada do programa.
nome() {
variáveis internas da função; /* se houver alguma */
corpo do procedimento;
}
Assim, por exemplo, a função que inicia o programa pode ser definida da seguinte maneira:
Iniciar() {
puts(" Programa Adivinha\n");
puts("Pense num numero dentro de um intervalo");
printf("Entre com o valor inferior do intervalo: ");
scanf("%d", &inf);
printf("Entre com o valor superior do intervalo: ");
scanf("%d", &sup);
}
int inf;
int sup;
int num;
int acertou=0;
Iniciar() {
puts(" Programa Adivinha\n");
puts("Pense num numero dentro de um intervalo");
printf("Entre com o valor inferior do intervalo: ");
scanf("%d", &inf);
printf("Entre com o valor superior do intervalo: ");
scanf("%d", &sup); getchar();
}
CalcularValor() {
num = (inf + sup) >> 1; /* divisao por 2. Lembra? */
62
}
ConferirValor() {
char ch;
main() {
Iniciar();
do {
CalcularValor();
ConferirValor();
}
while (!acertou);
}
Análise:
Como você pode observar, a estrutura de uma função C é bastante semelhante à da função
main(). Na verdade, a única diferença é que main() é uma função privilegiada. Todo
programa em C tem uma função chamada main(); quando se inicia a execução do
programa, o fluxo é desviado para o início de main() e o processamento continua a partir
daí. O programa termina quando todas as instruções de main() tiverem sido executadas.
63
Iniciar( ){
---;
---;
---;
}
main() {
Iniciar();
do {
CalcularValor( ){
CalcularValor(); ---;
}
ConferirValor();
}
ConferirValor( ){
---;
---;
---;
{
Você deve ter notado, que o simples fato de dividir o programa em módulos simplificou em
muito, não somente a tarefa de escrita, como também a sua leitura, pois, durante a
execução, tudo se passa como se o corpo das funções fosse incluído dentro da função
main().
Resumindo:
• Um problema é sempre mais difícil de ser resolvido quando consideramos todos os seus
aspectos simultaneamente;
Variáveis locais
64
As variáveis declaradas dentro de uma função são chamadas variáveis locais, e são
conhecidas somente pela função onde elas são declaradas. Por exemplo, a variável ch em
ConferirValor(), é conhecida somente por ConferirValor() e é invisível às
demais funções, incluindo main(). Se incluíssemos em main() a instrução:
printf("%c", ch);
Uma variável local é conhecida em C como variável automática, pois ela é automaticamente
criada quando a função é ativada e é destruída na saída da função. Veremos isso com mais
detalhes adiante.
float Far;
Convert() {
float Cel;
Cel = ((Far - 32) * 5) / 9;
printf(“Em Convert. Temperatura em Celsius: %f\n”,
Cel);
}
main() {
float Cel=0.0;
Saída:
Análise:
Observe o valor impresso para a temperatura em graus Celsius na função main(). Ele
corresponde ao valor da variável Cel em main() (deste ponto em diante denotada por
Cel:main). Esta variável, inicializada com 0.0, nada tem a ver com a variável
Cel:Convert. O conteúdo de Cel:main permanece inalterado durante toda a
execução do programa. Apesar de as variáveis terem o mesmo nome, elas ocupam
endereços diferentes de memória. A variável Cel:Convert é destruída ao final da
execução da função Convert() e o seu conteúdo é perdido.
65
Variáveis globais
Variáveis globais são conhecidas em C como variáveis externas. Uma variável é dita global
quando for declarada fora de qualquer módulo do programa fonte. Com isto, o seu valor
torna-se acessível a todas as funções, desde o ponto de declaração da variável até o fim do
programa.
Como as variáveis globais são acessíveis a todos os módulos do programa fonte, elas
constituem uma boa maneira de trocar informações entre funções que precisam ter acesso
aos mesmos dados. Observe o programa da adivinhação de um número: as variáveis inf e
sup, são lidas por Iniciar(), mas são acessadas por CalcularValor() e
ConferirValor(). A variável num tem o seu valor determinado por
CalcularValor(), mas é utilizada por ConferirValor(). Finalmente, acertou é
determinada por ConferirValor() e utilizada pela função main(). Por esse motivo,
todas essas variáveis foram declaradas no início do programa, fora de qualquer uma das
funções do programa.
int inf;
int sup;
int num;
int acertou=0;
Iniciar() {
puts(" Programa Adivinha\n");
.
.
.
Vamos rever o programa para conversão de temperaturas dessa vez com a utilização de
variáveis globais.
float Far;
float Cel;
Convert() {
Cel = ((Far - 32) * 5) / 9;
printf(“Em Convert. Temperatura em Celsius: %f\n”,
Cel);
}
void main() {
printf("Temperatura em Fahrenheit: ");
scanf("%f", &Far);
Convert();
printf(“Em main. Temperatura em Celsius: %f\n”, Cel);
66
}
Saída:
Análise:
Você deve prestar especial atenção ao nome das variáveis. Variáveis locais com o mesmo
nome de variáveis globais impossibilitam o acesso a estas. Observe o seguinte exemplo:
float Far;
float Cel=0.0;
Convert() {
float Cel;
main() {
printf("Temperatura em Fahrenheit: ");
scanf("%f", &Far);
Convert();
printf(“Em main. Temperatura em Celsius: %f\n”, Cel);
}
Saída:
Análise:
Variáveis globais devem ser usadas com cautela, pois, por elas serem acessíveis a todos os
módulos componentes do programa, o seu conteúdo pode ser inadvertidamente alterado
67
por algum desses módulos. Existem outros meios de trocar informações entre funções, os
quais serão vistos adiante.
ch = getchar();
Vamos ver agora como escrever nossas próprias funções de modo que elas retornem
valores. Para isso, usaremos o comando return.
O comando return pode retornar somente um único valor à função que chama.
ConferirValor() {
char ch;
Observe que não são necessários os comandos break após cada caso do switch. Por
que? Porque o return causa o imediato retorno do processamento à função main().
68
A chamada à função passa a ter a seguinte forma:
acertou = ConferirValor();
observe ainda, que agora a variável acertou não precisa mais ser global, podendo ser local à
função main().
int inf;
int sup;
int num; /* variáveis externas */
Iniciar() {
puts("Programa Adivinha\n");
puts("Pense num numero dentro de um intervalo");
printf("Entre com o valor inferior do intervalo: ");
scanf("%d", &inf);
printf("Entre com o valor superior do intervalo: ");
scanf("%d", &sup); getchar();
}
CalcularValor() {
num = (inf + sup) >> 1; /* divisao por 2. Lembra? */
}
ConferirValor() {
char ch;
main() {
int acertou;
Iniciar();
do {
CalcularValor();
acertou = ConferirValor();
}
69
while (!acertou);
}
Até agora, vimos duas maneiras de funções trocarem informações: através de variáveis
externas e através do comando return. Vamos estudar agora uma outra maneira que é
através do uso de parâmetros ou argumentos. Você já usou argumentos nas funções
printf() e scanf().
CalcularValor(inf, sup);
Observe a declaração dos parâmetros no interior dos parênteses que seguem o nome da
função. Os parâmetros baixo e alto são, na verdade, novas variáveis, funcionando
exatamente como variáveis automáticas da função CalcularValor().
Embora o nome dos argumentos não seja o mesmo, o compilador entende que a variável
baixo em CalcularValor() receberá o valor armazenado em inf. Da mesma forma,
alto recebe o valor de sup.
Note que o tipo dos parâmetros formais e dos argumentos são idênticos, condição
fundamental para que o programa funcione corretamente.
Podemos agora apresentar a sintaxe da definição de uma função em uma forma mais
genérica:
nome(parâmetros) {
variáveis internas da função; /* se houver alguma */
corpo da função;
70
}
Vamos, a título de exercício, ver algumas outras modificações que poderiam ser
introduzidas no programa exemplo.
main() {
int acertou;
Iniciar();
do {
num = CalcularValor(inf, sup);
acertou = ConferirValor();
}
while (!acertou);
}
ConferirValor() poderia receber como parâmetro num, de modo que num não mais
precisaria ser uma variável global, podendo ser local à main().
main() {
int num;
int acertou;
Iniciar();
do {
num = CalcularValor(inf, sup);
acertou = ConferirValor(num);
}
while (!acertou);
}
Observe que, baseado no trecho de código anterior, a seguinte construção seria válida em
C:
main() {
Iniciar();
while (!ConferirValor(CalcularValor(inf, sup)));
}
71
Perderíamos, no entanto, a legibilidade que a forma original nos proporcionava.
Para entender melhor o conceito de passagem de parâmetros por valor, vamos estudar com
um pouco mais de detalhes a troca de dados entre funções usando argumentos. Seja, por
exemplo, a seguinte função que calcula a potência n de x:
potencia(int x, int n) {
int p;
... SP
n:potência 5 SP
x:potência 2
... 72
Durante a execução de potencia():
p:potencia 32 SP
n:potencia 5
x:potencia 2
...
p:potencia 32
n:potencia 5
x:potencia 2
... SP
Observe:
73
A partir da observação “a” concluímos que, em C, uma função chamada não pode alterar o
valor de uma variável da função que a chamou; ela só pode alterar a sua cópia temporária,
que é criada na pilha.
Uma exceção a essa regra é a passagem de vetores como parâmetros de funções. Quando
passamos como argumento o nome de um vetor, a função chamada não recebe uma cópia
do vetor, mas sim o endereço da primeira posição do mesmo. Isto se dá porque C é, em
tudo, uma linguagem voltada para a eficiência do código gerado e seria muito dispendioso
empilhar uma cópia de cada uma das posições de um vetor de, digamos, 1000 posições.
Vejamos um outro exemplo de passagem de parâmetros por valor: A função swap troca o
valor dos seus argumentos inteiros x e y.
main() {
int x = 5, y = 10;
Saída:
Análise:
74
Para entendermos o funcionamento do programa vamos, mais uma vez, observar a
evolução da pilha:
a) int x = 5, y = 10;
y:main 10 SP
x:main 5
75
b) swap(x,y);
temp:swap ? SP
y:swap 10
x:swap 5
y:main 10
x:main 5
c) temp = x;
temp:swap 5 SP
y:swap 10
x:swap 5
y:main 10
x:main 5
d) x = y;
temp:swap 5 SP
y:swap 10
x:swap 10
y:main 10
x:main 5
76
e) y = temp;
temp:swap 5 SP
y:swap 5
x:swap 10
y:main 10
x:main 5
temp:swap 5
y:swap 5
x:swap 10
y:main 10 SP
x:main 5
Observe que a função swap opera sobre cópias dos valores das variáveis da função
main(). As variáveis x:main e y:main permanecem inalteradas durante toda a
execução do programa. Ao término do processamento da função swap o valor do stack
pointer é atualizado e as variáveis e os parâmetros locais da função deixam de existir.
Mas e se quisermos que uma função altere o valor das variáveis do módulo que a chamou?
A solução, nesse caso, é passarmos como parâmetro na chamada da função o endereço
das variáveis e não o seu conteúdo. Isso é feito com o operador de endereço (&).
scanf("%d", &inf);
77
Neste exemplo, queremos que scanf() efetivamente altere o valor de inf, e por esse
motivo, usamos o operador de endereço.
A função chamada deve lidar com o parâmetro recebido como um endereço, e não como
um valor. Veremos a passagem de parâmetros por referência com mais detalhes quando
estudarmos ponteiros.
O tipo de uma função é determinado pelo tipo de valor que ela retorna e não pelo tipo de
seus argumentos.
Até agora, temos trabalhado somente com funções inteiras. Se uma função for do tipo não
inteira ela deve ser declarada.
Em C, existe uma distinção entre declarar uma função e definir uma função.
Vamos estudar um exemplo. Seja o programa que transforma temperaturas Fahrenheit para
Celsius.
main() {
float Far, Cel;
78
a) O tipo de valor retornado pela função.
b) O tipo e o número de parâmetros passados para a função.
Estas informações são necessárias para que o compilador aloque o espaço necessário na
pilha para a passagem de parâmetros e para o retorno do valor calculado pela função.
A declaração de uma função não é necessária se o programador puder garantir que toda
função é definida antes de ser chamada. Ou, em outras palavras, o protótipo de uma função
não é necessário se a definição da função for escrita no arquivo fonte antes de qualquer
chamada à função. Criar o protótipo das funções, no entanto, constitui em boa prática de
programação uma vez que torna explicita a interface com as funções além de livrar o
programador da preocupação de ordenar as funções no arquivo.
Vejamos mais um exemplo. O programa abaixo chama uma função que, dada a base e a
altura de um triângulo, calcula a sua área.
2. main() {
3. float s, b, h;
Olhe com atenção o protótipo da função na linha 1. Observe que o nome dos parâmetros
não foi especificado mas apenas o seu tipo. Como foi dito anteriormente, o compilador
precisa conhecer o número e o tipo dos parâmetros passados para a função a fim de alocar
espaço na pilha. O nome dos parâmetros é uma informação desnecessária para o
compilador. Constitui em boa prática de programação, no entanto, dar nomes aos
parâmetros quando da construção do protótipo das funções. Lembre-se que o protótipo de
uma função é a especificação da interface com a mesma. Assim, tornar-se-ia muito mais
claro para um programador entender o que a função area faz se o seu protótipo fosse
escrito como:
79
O nome dado aos parâmetros no protótipo de uma função é uma informação útil ao
programador e não ao compilador.
tipo nome(parâmetros) {
variáveis internas da função; /* se houver alguma */
corpo da função;
}
Em C, pode-se declarar funções que não retornam nada com o tipo void. No exemplo
anterior, se a variável s fosse externa, teríamos:
float s;
void area(float, float); /* protótipo da função */
void main() {
float b, h;
Classes de armazenamento
• variáveis automáticas;
80
• variáveis externas ou globais;
• variáveis estáticas;
• variáveis registrador;
Variáveis automáticas
• criadas na pilha;
• existem somente durante a execução da(o) função(bloco) onde elas foram declaradas;
As características acima usando bloco no lugar de função seriam mais precisas, uma vez
que C permite construções do tipo:
void main() {
float a, b;
A variável aux existe somente durante a execução do if, e é conhecida apenas no bloco
onde ela foi declarada.
Variáveis externas
81
As variáveis externas ou globais foram também vistas em nossos exemplos anteriores. Suas
principais características são:
Vimos que variáveis externas são uma alternativa para a troca de informações entre funções
num mesmo arquivo.
Arquivo 1 (arq1.c)
void main() {
base=10.0;
altura=2.0;
printf("Area = %g\n", area());
}
Arquivo 2 (arq2.c)
float area(void) {
extern float base, altura; // declaradas no Arquivo 1
return base*altura/2.0;
}
Observe que as variáveis compartilhadas devem ser globais no arquivo onde elas forem
declaradas. Nos demais arquivos elas devem ser declararadas com o prefixo extern. O
mesmo se aplica às funções.
Compilação:
a) Ambiente UNIX rodando o compilador gcc:
82
b) Ambiente DOS usando o compilador Borland C++:
Variáveis estáticas
Variáveis estáticas são declaradas com o prefixo static o qual modifica algumas das
características de variáveis locais e globais.
• Não mais criadas na pilha. Mantêm a sua posição de memória alocada durante toda a
execução do programa.
Exemplo:
void soma(void);
void main() {
soma();
soma();
soma();
}
void soma(void) {
static int i=0;
printf("%d\n", ++i);
}
Saída
1
2
3
Observe que a variável i retém o seu valor entre as chamadas de soma().
Variáveis externas declaradas com o prefixo static têm as mesmas características das
variáveis externas comuns, com a exceção de que elas agora são visíveis apenas pelas
83
funções definidas no mesmo arquivo da declaração. Em outras palavras, variáveis externas
declaradas com o prefixo static não podem ser acessadas por funções definidas em
outros arquivos. Esta característica constitui-se em um mecanismo de privacidade
importante à programação modular.
Variáveis registrador
O prefixo register, quando usado, indica que a variável deve ser armazenada em uma
memória de acesso muito rápido chamada de registrador. Os registradores localizam-se
fisicamente dentro da CPU do computador e são em número limitado.
O prefixo register deve ser usado para variáveis que são muito acessadas no programa.
(tais como as variáveis de controle de laços, por exemplo)
Nada garante que variáveis declaradas com o prefixo register sejam realmente
alocadas em registradores. O compilador faz um esforço nesse sentido, mas, se os
registradores estiverem ocupados, o prefixo register é ignorado e a variável é alocada
em memória.
Exemplo: Rode o programa abaixo com e sem o prefixo register e tente observar a
diferença no tempo de processamento.
main() {
register
unsigned int i, j;
Inicialização de variáveis
Veremos agora como as variáveis nas diversas classes de armazenamento podem ser
inicializadas.
84
Na falta de inicialização explícita, variáveis externas e variáveis estáticas são inicializadas
com zero. Essas variáveis são inicializadas uma única vez, em tempo de compilação, e,
portanto, devem ser inicializadas com constantes.
Exemplo:
int i=1;
main() {
static int j=3;
.
.
.
}
Vetores podem ser inicializados com uma lista de valores entre chaves e separados por
vírgulas:
Exemplo:
main {
int n=3;
// comandos;
if (n>0) {
int i=n; // variável automática inicializada com expressão
for (; i>0; i--) {
// bloco de comandos
}
}
// comandos;
}
85
Funções recursivas
Uma função é dita recursiva quando existe dentro da função uma chamada a ela mesma.
Como exemplo, vamos escrever o fatorial de um número de forma recursiva:
fatorial(n) = n * fatorial(n-1);
long fatorial(int n) {
long res;
if (n==0)
res=1L;
else
res = n*fatorial(n-1);
return res;
}
res 2*fat(1) SP
n 2
86
b) Segunda chamada da função: fatorial(1)
res 1*fat(0) SP
n 1
res 2*fat(1)
n 2
res fat(0)==1 SP
n 0
res 1*fat(0)
n 1
res 2*fat(1)
n 2
res 1
n 0
res fat(1)=1*1 SP
n 1
res 2*fat(1)
n 2
87
res 1
n 0
res 1
n 1
res fat(2)=2*1 SP
n 2
Observe que a cada chamada da função fatorial() são criadas novas instâncias das
variáveis n e res as quais, ainda que tenham o mesmo nome, não confundem seus valores.
É claro que variáveis locais a funções recursivas não podem ser estáticas. Por quê?
Ao usarmos funções recursivas devemos ter em mente que a dimensão da pilha é finita e,
portanto, se o número de chamadas exceder um limite máximo, pode esgotar-se a
quantidade de memória disponível para a pilha.
O Pré-Processador C
A diretiva #define
A diretiva define pode ser usada para definir constantes simbólicas com nomes
apropriados.
Exemplo: Vamos escrever uma função que calcule o volume de uma esfera.
#define PI 3.14
88
float CalculaVolume(float raio) {
return(4.0/3.0*PI*raio*raio*raio);
}
Por quê deve-se usar constantes simbólicas? Em primeiro lugar para tornar o seu programa
mais legível. Em segundo lugar, suponha que no exemplo anterior a constante PI aparecesse
em diversos pontos do seu programa e que, um dia, você resolvesse aumentar a precisão e
usar 3.1415926 no lugar do 3.14. Com o uso de constantes simbólicas a alteração é feita
em apenas um ponto do programa: na diretiva define. Se, ao contrário, você tiver
digitado 3.14 ao longo do programa, você terá de procurar cada ocorrência da constante e
alterar seu valor.
A diretiva define pode ser usada para definir não apenas constantes numéricas mas
também constantes simbólicas.
Exemplo: Se a diretiva define fosse usada para definir as constantes simbólicas:
#define then
#define begin {
#define end }
if (i>0) then
begin
a = 1;
b = 2;
end
Macros
A diretiva define pode ser usada com argumentos, quando então é chamada de macro.
O uso de macros é semelhante ao uso de funções. Vamos, por exemplo, escrever uma
macro que calcula a área de um triângulo.
89
cuja chamada seria:
s = area(b, h);
s = ((b)*(h)/2.0)
Por quê são usados os parênteses? Imagine que não tivéssemos usado parênteses e feito a
seguinte chamada à macro:
s = area(b+1, h+1);
s = b+1*h+1/2.0;
Por precaução, coloque parênteses envolvendo o texto todo de uma macro, bem como
cada um dos argumentos.
Uma macro é uma solução eficiente quando argumentos de tipos diferentes devem ser
usados em um mesmo programa.
Seja, por exemplo, a macro abs() acima. Podemos chamá-la com argumentos inteiros ou
reais, ao passo que se fossemos escrever uma função para isso, teríamos de fazer:
float abs(float x) {
return (x>0 ? x : -x);
}
O uso de macros torna ainda a execução do programa mais rápida, uma vez que ele elimina
o desvio de fluxo (o jump para o início da função chamada e o return) e a passagem de
parâmetros entre funções.
90
Por outro lado, o pré-processador substitui cada chamada de uma macro pelo seu código,
o que aumenta o tamanho do programa fonte.
A diretiva #include
91
CAPÍTULO 9 - Vetores
Chama-se vetor a um conjunto de posições contíguas de memória onde cada uma destas
posições armazena elementos de um mesmo tipo. Cada posição de memória é chamada de
um elemento do vetor.
Vetores unidimensionais
int IntArray[25];
Quando o compilador encontra a declaração acima, ele reserva espaço em memória para
armazenar, exatamente, 25 números inteiros. Supondo que os números inteiros sejam
representados em 4 bytes, a declaração do vetor IntArray aloca em memória 100 bytes
contínuos como mostrado na figura a seguir.
IntArray[24]
4 bytes
IntArray[0]
92
IntArray[2]
não é o segundo elemento do vetor, e sim o terceiro, uma vez que a numeração começa em
0.
tipo nome[dimensão];
dimensão*sizeof(tipo)
Vamos estudar um exemplo de aplicação de vetores. Seja o seguinte programa que soma
as posições correspondentes de dois vetores: vetor1 e vetor2, cujos elementos são
fornecidos pelo usuário.
1. #define DIM 3
2. main() {
3. int i;
4. int vetor1[DIM];
5. int vetor2[DIM];
Saída:
93
vetor1[0] = 0
vetor1[1] = 1
vetor1[2] = 2
vetor2[0] = 0
vetor2[1] = 1
vetor2[2] = 2
vetor1[0] + vetor2[0] = 0
vetor1[1] + vetor2[1] = 2
vetor1[2] + vetor2[2] = 4
Análise:
Observe o uso da diretiva #define para dimensionar o vetor. Se, posteriormente,
quisermos trocar a dimensão do vetor, basta alterar um único ponto do programa, ao invés
de procurarmos cada ocorrência de DIM.
O programa consiste de 3 laços for. No primeiro, na linha 6, o usuário entra com os
dados para vetor[1]. Observe que na instrução scanf( ) foi usado o operador de
endereço (&) precedendo cada elemento do vetor. Isso é possível porquê vetor1[i] é
uma variável como outra qualquer e, portanto, tem endereço univocamente determinado.
O programa acima poderia ser facilmente alterado para que os elementos dos vetores
fossem, por exemplo, números reais:
#define DIM 3
void main() {
int i;
float vetor1[DIM];
float vetor2[DIM];
Alocação de vetores
94
Os elementos de um vetor são alocados em posições contínuas de memória. Deste modo, o
elemento de índice 0 é alocado primeiro, o elemento de índice 1 imediatamente abaixo e
assim sucessivamente, até o último elemento do vetor.
int sentinela=0;
int Array[3];
Array[0] 32 SP
Array[1] 5
Array[2] 2
sentinela 0
95
1. void main() {
2. int i;
3. int sentinela=0;
4. int Array[25];
Saída:
Sentinela = 0;
Sentinela = 20;
Análise:
Na linha 3 a variável sentinela é alocada logo abaixo do vetor Array[] e é inicializada
com 0. O for da linha 6 preenche os elementos do vetor com o inteiro 20. Observe que a
variável de controle do for assume os valores 0..25. O elemento de índice 25 não foi
originalmente alocado para o vetor. O seu endereço corresponde ao endereço da variável
Sentinela. Desta forma, a atribuição Array[25] = 20, em verdade, atribui o inteiro
20 à variável Sentinela, o que pode verificar-se pelo valor impresso na linha 8.
Inicialização de vetores
Vetores podem ser inicializados com uma lista de valores entre chaves e separados por
vírgulas.
Exemplo:
vetor[0] = 0;
vetor[1] = 1;
96
int vetor[]={0, 1, 2, 3, 4};
Observe os colchetes vazios. Se nenhum número for fornecido para dimensionar o vetor, o
compilador conta o número de itens na lista de inicialização e atribui esse número à
dimensão do vetor.
Forma geral:
Exemplo 1:
Vamos analisar uma situação, onde desejamos construir uma estrutura capaz de armazenar
as 5 notas obtidas pelos alunos de um determinado curso. Cada aluno é identificado pelo
seu número, que corresponde à sua posição na lista de chamada da turma.
Utilizamos dois índices para obter o valor de uma nota: o número do aluno e o número da
prova.
A figura acima mostra claramente que cada uma das entradas da tabela é na verdade uma
segunda tabela contendo as notas das provas de um aluno. Além disso, cada um dos
elementos da tabela de notas pode ser um número fracionário e portanto do tipo float.
97
As dimensões da tabela são o número máximo de alunos (nº de linhas) e o número de
provas naquele curso (nº de colunas).
#define NumeroDeProvas 5
#define MaximoDeAlunos 50
float
boletim[MaximoDeAlunos][NumeroDeProvas];
boletim [14][2]
Em C o índice de um vetor tem de ser do tipo inteiro. Em certas aplicações pode ser
interessante trabalhar com índices não numéricos tais como dias da semana, cores, meses
do ano, etc. Em tais ocasiões usa-se as constantes enumeradas vistas no Capítulo 3.
Vejamos alguns exemplos de declaração de tabelas em C usando constantes enumeradas.
Exemplo 2:
Suponha que se deseje armazenar o número de horas trabalhadas, em cada dia da semana,
pelos consultores de uma empresa.
#define maxconsult 30
int tabela_horas[maxconsult][5];
98
enum dia_util {SEG, TER, QUA, QUI, SEX};
Assim, o número de horas que o consultor 2 trabalhou na quinta-feira seria acessada como:
tabela_horas[1][QUI]
Para sabermos quantas horas o consultor 5 trabalhou naquela semana, teríamos o seguinte
trecho do programa:
int num_horas;
dia_util dia;
Exemplo 3:
Uma loja de departamentos está oferecendo uma promoção fornecendo descontos nas suas
várias linhas de mercadorias. Esse desconto dependerá do tipo da mercadoria adquirida e
do tipo do cliente (novo ou antigo). Como o preço final é obtido diretamente nos terminais
do computador central, o gerente da loja pediu ao programador que alterasse o programa
de cálculo de preço de mercadorias, com o objetivo de implantar a atual política de
descontos. Para tal, o programador organizou a seguinte tabela:
CLIENTE
MERCADORIA novo antigo
alimento 0 5
limpeza 0 5
papelaria 5 10
ferragem 10 15
eletrodoméstico 15 20
float tab_desconto[5][2];
Observe que a tabela de descontos deve ser preenchida com os comandos de atribuição:
99
tab_desconto[alimento][novo]=0;
tab_desconto[limpeza][novo]=0;
.
.
tab_desconto[eletrodomestico][antigo]=20;
Exemplo 4:
Imagine o caso de uma firma construtora que deseja controlar a quantidade de materiais
comprados para suas diversas obras. Os materiais podem ser comprados de vários
fornecedores cadastrados na firma e usados em qualquer uma das obras. A estrutura
abaixo, define uma tabela de 3 dimensões (fornecedor, obra e material) que representa a
quantidade de todos os materiais, comprados de todos os fornecedores cadastrados, para
todas as obras da firma.
int compras[MaxFornecedor][MaxObra][MaxMaterial];
Para sabermos a quantidade do material número 150 (cimento, por exemplo) comprado do
fornecedor cujo código é 11 para a obra em Madureira, cujo código é 13, devemos usar:
fornecedor material
obra
Inicialização de matrizes
Lembre-se que uma matriz consiste de um vetor cujos elementos são vetores. Sendo assim,
a inicialização de matrizes é semelhante à inicialização de vetores: uma lista de elementos
(vetores) entre chaves e separados por vírgulas.
Exemplo: Considere o seguinte programa que inicializa duas matrizes e então as multiplica.
100
main() {
short int
a[3][4] = { {-14, -36, -62, 78},
{-77, 14, -92, 17},
{ 67, -51, 18, -60} },
b[4][2] = { { 60, -65},
{ 7, 34},
{-23, 69},
{ 32, -1} };
short int
i, j, k,
c[3][2];
Saída:
c[0][0] = 2830
c[0][1] = -4670
c[1][0] = -1862
c[1][1] = -884
c[2][0] = 1329
c[2][1] = -4787
Matrizes são armazenadas na memória por linha, isto é, o índice que varia mais rápido é o
último (o das colunas). Essa mesma regra se aplica no caso de vetores de mais de duas
dimensões.
C[0][0]
C[0][1]
C[1][0]
C[1][1]
C[2][0]
C[2][1]
101
Cada posição de memória representa 2 bytes.
Vetores de 3 dimensões podem ser vistos como vetores em que os elementos são matrizes.
Exemplo:
{ {5, 1},
{6, 2},
{7, 8},
{9, 1} },
{ {4, 5},
{8, 1},
{2, 3},
{4, 5} } };
vet3d[1][2][1]
102
#define DIM 5
void main() {
int vetor[DIM]={1, 10, 7, 35, 4};
Você notou os colchetes vazios? Reside aí uma das grandes forças da linguagem C: não é
necessário conhecer a dimensão do vetor vet em tempo de compilação. Por quê? Porque
o compilador se satisfaz em saber que vet[] é o endereço da primeira posição do vetor,
uma vez que é responsabilidade do programador não ultrapassar a dimensão do mesmo.
Seja o seguinte programa que preenche uma matriz quadrada com 0's.
#define DIMX 5
#define DIMY 5
void main() {
int mat[DIMX][DIMY];
preenche(mat);
}
103
}
Ela parece um pouco misteriosa, não é? Para entendê-la vamos ver novamente a alocação
em memória de uma matriz C de inteiros (short) de dimensão DIML x DIMC, onde
DIML=3 e DIMC=2. Vamos supor que a matriz é alocada a partir do endereço 1000.
1000 C[0][0]
1002 C[0][1]
1004 C[1][0]
1006 C[1][1]
1008 C[2][0]
C[2][1]
1010
Pela expressão acima fica claro que o compilador precisa conhecer o número de colunas da
matriz. O número de linhas, assim como em vetores unidimensionais, não é necessário.
STRINGS
Em C não existe um tipo de dados string como no PASCAL. Ao contrário, strings são
implementadas como vetores de caracteres, terminados pelo caracter null ('\0').
104
O caracter null serve como uma marca de fim de string para as funções que lidam com
strings. Por exemplo:
main() {
char nome[10];
nome[0] = 'N';
nome[1] = 'C';
nome[2] = 'E';
nome[3] = '\0';
puts(nome);
}
O caracter null ou '\0' tem valor 0 decimal. Não confundir com o caracter '0' que
tem valor 48 decimal.
Strings constantes
Strings constantes são uma seqüência de caracteres entre aspas. Já vimos vários exemplos
de strings constantes ao longo dessa apostila. Por exemplo:
printf("%s", "NCE");
Observe que você não deve incluir o caracter null ao fim de uma string constante. O
compilador faz isso por você.
Observe ainda a diferença entre 'c' e "c". No primeiro caso é armazenado na memória
apenas 1 byte correspondente ao caracter c. No segundo caso são armazenados dois
bytes: o caracter c seguido do caracter null.
Inicializando strings
Strings, assim como qualquer outro tipo de vetor, podem ser inicializadas em tempo de
compilação com uma lista de valores entre chaves e separados por vírgulas.
char nome[]={'N','C','E','\0');
Vetores de strings
Como strings em C são vetores de caracteres, vetores de strings são, na verdade, vetores
de vetores de caracteres ou, matrizes de caracteres.
Exemplo:
char pessoa[]="Eduardo";
nomes[0] == &nomes[0][0].
imprimiria:
As bibliotecas de funções C incorporam uma série de funções para lidar com strings.
Veremos a seguir algumas das mais usuais:
strcpy() <STRING.H>
Copia a string no endereço de origem para o endereço destino
Declaração
char *strcpy(char *dest, const char *src);
Comentários
Copia a string src para a string dest caracter a caracter. A cópia termina quando o
caracter nulo em src tiver sido copiado.
Valor de Retorno
O endereço apontado por dest.
Exemplo
void main() {
char String1[] = "No man is an island";
char String2[80];
strcpy(String2,String1);
Saída
String1: No man is an island
String2: No man is an island
strlen() <STRING.H>
Calcula o comprimento de uma string.
Declaração
107
size_t strlen(const char *s);
Comentários
strlen() calcula o comprimento (número de caracteres) da string s.
Valor de Retorno
Retorna o número de caracteres em s. O caracter nulo ao final de s não é incluído na
contagem.
Exemplo
#include <stdio.h>
#include <string.h>
void main() {
char *string = "Borland International";
printf("%d\n", strlen(string));
}
strcat() <STRING.H>
Concatena duas strings
Declaração
char *strcat(char *dest, const char *src);
Comentários
strcat() escreve uma cópia da string src no final da string dest. O tamanho
da string resultante é strlen(dest) + strlen(src).
Valor de Retorno
strcat() retorna um ponteiro para as strings concatenadas.
Exemplo
void main() {
char destination[25];
char *blank = " ", *c = "C++", *turbo = "Turbo";
strcpy(destination, turbo);
strcat(destination, blank);
108
strcat(destination, c);
printf("%s\n", destination);
}
strcmp() <STRING.H>
Compara duas strings
Declaração
int strcmp(const char *s1, const char*s2);
Comentários
strcmp() realiza uma comparação entre as strings s1 e s2. A comparação
começa com o primeiro caracter em cada string e continua com os caracteres
subseqüentes até que os caracteres correspondentes difiram ou o final de uma das
strings seja alcançado.
Valor de Retorno
strcmp() retorna um valor inteiro que é:
< 0 se s1 < s2
== 0 se s1 == s2
> 0 se s1 > s2
Exemplo
void main() {
char *buf1 = "aaa", *buf2 = "bbb";
int ptr;
if ((ptr=strcmp(buf2, buf1))>0)
printf("buf2 é maior do que buf 1\n");
else
printf("buf2 é menor do que buf1\n");
}
strchr() <STRING.H>
Procura a primeira ocorrência de um dado caracter numa string.
Declaração
char *strchr(const char *s, int c);
109
Comentários
strchr() percorre uma string procurando pela primeira ocorrência de um dado
caracter. O caracter nulo é considerado como parte da string; por exemplo,
strchr(strs, 0) retorna um ponteiro para o caracter nulo que marca o fim da
string strs.
Valor de Retorno
Em caso de sucesso retorna um ponteiro para a primeira ocorrência do caracter c na
string s.
Em caso de erro (a string s não contém o caracter c) retorna null.
Exemplo
void main() {
char string[15]= "This is a string";
char *ptr, c = 'r';
strstr() <STRING.H>
Encontra a primeira ocorrência de uma substring em outra string
Declaração
char *strstr(const char *s1, const char *s2);
Comentários
strstr() percorre s1 procurando pela primeira ocorrência da substring s2.
Valor de Retorno
Em caso de sucesso, strstr() retorna um ponteiro para o caracter em s1 onde
começa s2 (um ponteiro para s2 em s1).
Em caso de erro (s2 não ocorre em s1), strstr() retorna null.
110
Exemplo
void main() {
char *str1 = "Borland International";
char *str2 = "nation", *ptr;
A necessidade de procurar uma informação numa tabela ou num catálogo é muito comum.
Por exemplo: procurar o telefone de uma pessoa no catálogo, ou o valor do IPVA de um
automóvel em uma tabela que dá o valor do imposto em função do modelo e do ano de
fabricação do carro.
Como esta função é muito utilizada, é importante desenvolver rotinas que a executem de
forma eficiente.
Por eficiente deve-se entender uma rotina que faça a busca no menor tempo possível.
Como veremos adiante, é possível detalhar com um pouco mais de precisão o conceito de
eficiência porém, por enquanto, ficaremos com esta definição intuitiva.
Sabemos que o tempo gasto procurando dados em tabelas depende, em primeiro lugar, do
tamanho da tabela. Veja por exemplo o tempo gasto para se descobrir o telefone de um
assinante na lista de São Paulo e faça a comparação com uma cidade do interior com 1.000
telefones. Além disso, a eficiência do processo de busca depende fortemente do algoritmo
empregado.
Para visualizar melhor os algoritmos envolvidos, vamos considerar apenas o caso de tabelas
de números inteiros. Nos casos práticos, em geral, cada elemento da tabela é um registro
(record), e procuramos um elemento desta tabela cujo campo chave tenha correspondência
com uma dada chave de busca.
Imagine, por exemplo, uma tabela que contém o nome e as notas dos alunos de uma turma.
O campo pesquisado pode ser o nome do aluno e a informação que procuramos sua nota.
111
A generalização do processo para este tipo de estrutura pode ser feita facilmente depois
que os princípios aqui expostos forem conhecidos.
Busca Seqüencial
Neste método, o processo de busca pesquisa a tabela seqüencialmente, desde o seu início.
Cada elemento da tabela é comparado com a chave. Se eles forem iguais, o índice do
elemento é retornado e a busca termina. Se o algoritmo atingir o fim da tabela e a chave
ainda não tiver sido encontrada, isto sinaliza que a busca falhou e que nenhum elemento da
tabela tinha o valor da chave.
#define N 10
void main() {
int ind; /* retorna a posicao do elemento */
int chave; /* valor a ser procurado */
int tab[N]; /* tabela a ser pesquisada */
if (ind < N)
printf("Chave encontrada na posicao %d\n", ind);
else
printf("Chave nao encontrada\n");
}
Análise:
Este algoritmo pode ainda ser ligeiramente melhorado. Observe que para cada valor de
ind, a rotina tem de fazer duas comparações: a primeira para saber se (ind < N) e a
segunda para testar se (tab[ind] != chave). Na versão seguinte do algoritmo,
eliminaremos uma das comparações.
112
Busca com Sentinela
#define N 10
void main() {
int ind;
int chave;
int tab[N+1];
if (ind < N)
printf("Chave encontrada na posicao %d\n", ind);
else
printf("Chave nao encontrada\n");
}
Também é possível, se for conhecida a freqüência com que cada um dos elementos da
tabela será procurado, armazenar os elementos em ordem decrescente da freqüência de
busca. Garante-se assim que os elementos mais freqüentemente acessados encontram-se
nas primeiras posições do vetor e que, portanto, serão rapidamente localizados.
Obviamente, ninguém pensaria em fazer uma busca seqüencial em uma lista de assinantes
em cidades como o Rio de Janeiro ou São Paulo. No entanto, consegue-se localizar um
113
nome em alguns poucos segundos. A chave para a eficiência do algoritmo empregado vem
do fato de que os nomes são listados em ordem alfabética. O método que, normalmente, as
pessoas usam para procurar um nome no catálogo é abrir o mesmo mais para o início ou
mais para o fim dependendo da inicial do assinante procurado. Se a página aberta contiver
nomes que, alfabeticamente, vêm depois do nome procurado, a "metade" direita é
descartada e a busca se limita à "metade" esquerda.
Esse processo de eliminação é a base da busca binária. O nome binária vem do fato de
que, a cada comparação, metade da tabela é descartada.
Imagine, por exemplo, que você está procurando a palavra tarol (uma espécie de tambor)
num dicionário de 1500 páginas. Você poderia aplicar a seguinte rotina:
• Tomamos novamente a parte final e dividimos ao meio, caindo na letra R (pág. 1204);
Desta forma, dividimos a área de pesquisa ao meio em cada passo. Caso o dicionário tenha
1500 páginas e a palavra procurada nos leve ao pior caso, onde a área de pesquisa é
reduzida a cada comparação até conter uma única página, o número de comparações
necessárias seria:
114
Comparação Páginas Restantes
1ª 1500/2 → 750
2ª 750/2 → 375
3ª 376/2 → 188
4ª 188/2 → 94
5ª 94/2 → 47
6ª 47/2 → 23
7ª 24/2 → 12
8ª 12/2 → 6
9ª 6/2 → 3
10ª 3/2 → 1
11ª 2/2 → 1
Vemos que, no pior caso, 11 pesquisas serão necessárias. Na verdade este número pode
ser expresso pela equação log2 N (logaritmo na base 2 de N).
#define N 10
void main() {
int ind;
int chave; /* valor a ser procurado */
int tab[N]; /* tabela a ser pesquisada */
int inicio=0; /* inicio da area de pesquisa */
int fim=N-1; /* fim da area de pesquisa */
int meio=(inicio+fim)/2; /* indice de pesquisa */
115
}
if (tab[meio] == chave)
printf("Chave encontrada na posicao %d\n", meio);
else
printf("Chave nao encontrada\n");
}
Algoritmos de Ordenação
A ordenação, da mesma forma que a busca, é uma das tarefas básicas em processamento
de dados.
Ordenar uma tabela consiste em fazer com que os elementos desta tabela sejam
armazenados de acordo com um critério de ordenação. Este critério pode ser muito variado
dependendo do tipo dos elementos que estejam sendo ordenados.
Nos casos mais simples, como os que serão examinados aqui, os elementos da tabela são
números inteiros, e os critérios de ordenação se resumem à ordem crescente ou decrescente
dos valores da tabela.
Ordem crescente:
Ordem decrescente:
Método da Seleção
Talvez, o método mais intuitivo de ordenar uma tabela de números inteiros em ordem
crescente, seja procurar o menor número e colocá-lo na primeira posição. Em seguida
116
procuramos novamente o menor entre os números restantes e o armazenamos na segunda
posição e assim por diante. Nisto consiste a essência do método da seleção.
Desejamos colocar este vetor em ordem crescente. Para saber quem fica na posição 1
podemos varrer o vetor da posição 2 até a 8, descobrir o menor elemento (no caso, 10 na
posição 7) e comparar com o elemento que encontra-se na primeira posição. Se aquele for
menor do que este trocamos as posições dos dois elementos. Desta forma, ao final do
primeiro passo, o primeiro elemento estará posicionado.
Para posicionar o segundo elemento procuramos pelo menor elemento entre a terceira
posição e o final do vetor. Encontramos o número 46 e comparamos com o elemento da
segunda posição (15), não efetuando-se a troca.
1. #define N 10
2. void main() {
3. int
4. ind1, ind2, /* marcadores */
5. aux, /* variável auxiliar */
6. tab[N]; /* tabela a ser pesquisada */
Saída:
O valor ordenado é:
1 2 3 4 5 6 7 8 9 10
Análise:
O algoritmo acima pode ser refinado para chegarmos ao programa final apresentado a
seguir.
1. #define N 10
2. main() {
3. int
4. ind1, ind2, /* marcadores */
5. tab[N]; /* tabela a ser pesquisada */
Análise:
A otimização introduzida neste algoritmo consiste em não efetuar a troca de posições toda
vez que o elemento em ind2 for menor do que o elemento em ind1. Ao invés disso,
armazena-se a posição do menor elemento encontrado em cada passo e procede-se a troca
de posições apenas uma vez, ao final do passo.
A cada passagem do for mais externo é feita uma troca, num total de (N-1) trocas.
Nc = ( N − 1) * N 2
Método da bolha
O método da bolha deriva o seu nome da maneira com que os maiores valores “afundam”
em direção ao fim do vetor enquanto que os menores valores “borbulham” em direção à
“superfície” ou topo da tabela.
119
Este método consiste em comparar-se cada elemento com o seguinte, começando com os
dois primeiros e terminando nos dois últimos. Se o elemento tab[ind1+1] for menor do
que o elemento tab[ind1], os dois elementos são trocados de posição. Se, ao final de
uma varredura, alguma troca houver sido efetuada, o processo se repete.
No programa a seguir usa-se a variável troquei para indicar se durante um passo foi
executada alguma troca.
#define N 10
void main() {
int ind1, ind2; /* marcadores */
int tab[N]; /* tabela a ser pesquisada */
int aux; /* variavel auxiliar para a troca */
BOOL troquei; /* flag de parada */
ind2=N-1;
do {
troquei=FALSE;
for (ind1=0; ind1<ind2; ind1++)
if (tab[ind1] > tab[ind1+1]) {
aux=tab[ind1];
tab[ind1]=tab[ind1+1];
tab[ind1+1]=aux;
troquei=TRUE;
}
ind2--; }
while (troquei);
Se, no pior caso, o algoritmo for aplicado a uma tabela com os elementos em ordem
decrescente, seriam necessárias (N-1) comparações e (N-1) trocas no primeiro passo, (N-
2) comparações e trocas no segundo passo, e assim por diante. O número total de
comparações e trocas seria:
120
( N − 1) + ( N − 2 )+K+1 = N * ( N − 1) 2
Logo, no pior caso, o método da bolha é da ordem de O(N2) da mesma forma que o
método da seleção. No entanto, o método da seleção é sempre da ordem de O(N 2)
enquanto que o método da bolha varia de O(N) para tabelas já ordenadas até O(N 2) para
tabelas em ordem decrescente.
121
CAPÍTULO 10 - Ponteiros
A maioria das variáveis vistas até agora armazenavam dados, isto é, a informação
manipulada pelo programa na sua forma final. Algumas vezes, no entanto, necessitamos
saber onde uma variável foi armazenada ao invés do seu conteúdo. Para isso, precisaremos
de ponteiros.
Introdução
O computador enxerga a memória como uma seqüência de bytes (grupos de 8 bits), cada
um dos quais com um endereço único.
Os dados são dispostos seqüencialmente na memória de modo que, se, por exemplo, o
primeiro byte de uma variável inteira for armazenado no endereço N, o byte seguinte será
armazenado no endereço N+1 e assim por diante.
Um ponteiro é uma variável que, no seu espaço de memória, armazena o endereço de uma
segunda variável, essa sim, normalmente, contendo o dado a ser manipulado pelo
programa.
Seja, por exemplo, um programa que contenha duas variáveis inicializadas de alguma forma
e dispostas seqüencialmente na memória: a, uma variável inteira contendo o valor 5 e ptr,
um ponteiro para a variável a. Suponha que as variáveis sejam criadas a partir do endereço
1000. O conteúdo da memória será:
1000 a==5
1004 ptr==1000
1008
1012
122
Por que usar ponteiros? Em primeiro lugar porque um único ponteiro permite que sejam
acessados diferentes dados em diferentes posições de memória bastando, para isso, trocar
o endereço armazenado na variável do tipo ponteiro.
Além disso, o uso de ponteiros permite que sejam criadas variáveis enquanto o programa
está executando. Existem em C funções que retornam o endereço de uma área de memória
ainda não utilizada pelo computador4. Esse endereço pode então ser atribuído a um
ponteiro que, dessa forma, passa a apontar para uma variável criada dinamicamente ou em
tempo de execução. O tamanho do bloco de memória requisitado é especificado na
chamada à função. Assim, um programa pode, por exemplo, criar um vetor com a dimensão
exata da necessidade da aplicação, evitando-se assim o risco de super ou subdimensionar o
problema.
Ponteiros fornecem ainda um mecanismo pelo qual as funções podem retornar mais de um
valor, permitem um acesso mais rápido aos elementos de vetores e matrizes e possibilitam a
criação de estruturas de dados complexas tais como listas encadeadas e árvores binárias,
onde cada elemento da estrutura deve “apontar” para outros elementos da mesma
estrutura.
Usando Ponteiros
Agora você já deve estar convencido da utilidade de ponteiros. Como usá-los em C? Antes
de mais nada, assim como qualquer outra variável, eles precisam ser declarados. A forma
geral da declaração de um ponteiro é:
tipo *ptipo;
Exemplo:
int *pint;
4
Esta área de memória recebe o nome de heap
123
Considere o seguinte programa:
1. main() {
2. int i;
3. int *ptr;
4. ptr = &i;
5. *ptr = 3;
6. }
• Declaração:
int i;
int *ptr;
?
ptr:main ? 996
i:main ? 1000
...
Observe que a simples declaração do ponteiro não inicializa ptr com nenhum valor em
particular. O ponteiro precisa ser explicitamente inicializado antes de ser utilizado.
• 1ª instrução:
ptr = &i;
?
ptr:main 1000 996
i:main ? 1000
...
124
O operador (&) é usado para atribuir o endereço de i a ptr. Ou, em outras palavras,
ptr passa a apontar para i.
• 2ª instrução:
*ptr = 3;
?
ptr:main 1000 996
i:main 3 1000
...
Observe que o operador de indireção tem duas leituras diferentes: quando usado na
declaração de uma variável do tipo ponteiro e, no corpo do programa, quando usado para
referenciar o conteúdo de uma posição de memória. Assim, a declaração na linha 3 deve
ser lida como: “ptr é um ponteiro para o tipo inteiro”. Já a instrução na linha 5 deve ser
lida como: “A posição de memória apontada por ptr recebe o inteiro 3”. A instrução na
linha 5 seria equivalente a:
i = 3;
Por quê? Porque i e *ptr referem-se ao mesmo endereço de memória de modo que
ambas as instruções atribuem o valor 3 ao endereço 1000.
É importante entender que ponteiros são variáveis como quaisquer outras variáveis.
Algumas variáveis são apropriadas para armazenar no seu espaço de memória números
inteiros (variáveis inteiras). Outras são próprias para armazenar números em ponto flutuante
(variáveis float) ou caracteres (variáveis char). De forma semelhante, existem variáveis
próprias para armazenar endereços: as variáveis do tipo ponteiro. Assim como qualquer
outra variável, as variáveis do tipo ponteiro tem também um conteúdo e um endereço.
Observe o programa abaixo. Esteja certo de não prosseguir antes de compreender as
diferenças entre os valores impressos pelos 3 printf().
1. main() {
2. int i;
3. int *ptr;
125
4. ptr = &i;
5. *ptr = 3;
6. printf(“ ptr = %u\n”, ptr);
7. printf(“&ptr = %u\n”, &ptr);
8. printf(“*ptr = %d\n”, *ptr);
9. }
Saída:
ptr = 1000
&ptr = 996
*ptr = 3
Alocação Dinâmica
calloc() <STDLIB.H>
Aloca memória no heap
Declaração
void *calloc(unsigned nitems, unsigned size);
Comentários
calloc() provê acesso à memória heap. O heap é usado para a alocação dinâmica
de blocos de memória de tamanho variável.
Diversas estruturas de dados tais como árvores e listas encadeadas fazem uso de áreas
de memórias alocadas dinamicamente no heap.
Valor de Retorno
Em caso de sucesso, calloc() retorna um ponteiro para a área recém alocada.
126
Em caso de falha (não existe espaço suficiente para o bloco de memória requisitado ou
nitems ou size é igual a 0), retorna null.
Exemplo
#include <stdio.h>
#include <stdlib.h>
void main() {
int *ptr;
Neste exemplo a função calloc() é chamada para criar dinamicamente uma área de
memória no heap a qual é acessada através do ponteiro ptr. Vamos estudar em detalhes
cada uma das partes componentes da chamada à função calloc().
• O operador sizeof()
Exemplos:
sizeof(int) == 2
sizeof(float) == 4
Uso: (tipo)nome
Exemplos:
(float)i
//converte a variável i para o tipo float
(int *)calloc(2)
//converte o ponteiro retornado pela função calloc()
//em um ponteiro para inteiro.
127
A conversão forçada de tipo é necessária porque o ponteiro retornado por calloc() é
de tipo indefinido ou void.
malloc() <STDLIB.H>
Aloca memória no heap
Declaração
void *malloc(unsigned size);
Comentários
malloc() aloca um bloco de tamanho size bytes no heap. Ela permite que uma
aplicação aloque memória em tempo de execução, na medida exata da necessidade da
aplicação.
O heap é usado para a alocação dinâmica de blocos de memória de tamanho variável.
Diversas estruturas de dados tais como árvores e listas encadeadas fazem uso de áreas
de memórias alocadas dinamicamente no heap.
Valor de Retorno
Em caso de sucesso, malloc() retorna um ponteiro para a área recém alocada.
Em caso de erro (não existe espaço suficiente para o bloco de memória requisitado)
malloc() retorna null.
Exemplo
void main() {
int *pAge;
pAge=(int *)malloc(sizeof(int));
*pAge = 5;
}
128
free() <STDLIB.H>
free() libera os blocos de memória alocados no heap.
Declaração
void free(void *block);
Comentários
free() libera os blocos de memória alocados no heap através de chamadas prévias
às funções calloc() e malloc().
Valor de Retorno
Nenhum
Quando uma área de memória alocada dinamicamente deixa de ser necessária ela
deve ser liberada através de uma chamada à função free( )
1. void main() {
2. int Local = 5;
3. int *pLocal= &Local;
4. int *pHeap=(int *)calloc(1, sizeof(int));
5. if (pHeap == NULL) {
6. puts("No memory for pHeap!!");
7. return;
8. }
9. *pHeap = 7;
10. printf("Local: %d\n", Local);
11. printf("*pLocal: %d\n", *pLocal);
12. printf("*pHeap: %d\n", *pHeap);
13. free(pHeap);
14. pHeap = (int *)malloc(sizeof(int));
15. if (pHeap == NULL) {
16. puts("No memory for pHeap!!");
17. return;
18. }
19. *pHeap = 9;
129
20. printf("*pHeap: %d\n", *pHeap);
21. free(pHeap);
22.}
Saída
Local: 5
*pLocal: 5
*pHeap: 7
*pHeap: 9
Análise
Ponteiros e Funções
5
Veremos posteriormente que o programador poderia referir-se ao bloco recém alocado como pHeap[0].
Nada impediria que ele referenciasse, por exemplo, a posição pHeap[1]. O resultado deste acesso, no
entanto, traria conseqüências imprevisíveis uma vez que o bloco de memória alocado tem tamanho
suficiente para armazenar um único número inteiro.
6
Pode-se pensar, portanto, em uma variável como um ponteiro constante para um dado endereço de
memória.
130
Quando estudamos funções, vimos que o mecanismo pelo qual uma função retorna um valor
é através do uso do comando return. Mas, e se quisermos que a função chamada
retorne mais de um valor? Essa é uma situação que ocorre com muita freqüência na prática.
Suponha, por exemplo, que a troca de posição entre dois elementos no método de
ordenação da bolha fosse efetuada por uma função troca(). Seja a seguinte
implementação (ERRADA) onde os parâmetros são passados por valor.
do {
troquei = FALSE;
for (ind1=0; ind1<ind2; ind1++)
if (tab[ind1] > tab[ind1+1]) {
troca(tab[ind1], tab[ind1+1]);
troquei = TRUE;
}
ind2--;
}
while (troquei);
.
.
Para entender porque o programa acima não funciona vamos estudar a evolução da pilha
durante a execução do mesmo. Suponha por exemplo que (tab[ind1]==2) e
(tab[ind1+1]==3).
994
998
1002
... 1006 SP
...
tab[ind1] 2 1026
tab[ind1+1] 3 1030
131
• Na chamada à função troca()
aux:troca 994 SP
b:troca 3 998
a:troca 2 1002
... 1006
...
tab[ind1] 2 1026
tab[ind1+1] 3 1030
aux:troca 2 994
b:troca 2 998
a:troca 3 1002
... 1006 SP
...
tab[ind1] 2 1026
tab[ind1+1] 3 1030
A solução para esse problema consiste em o programa principal passar os endereços das
variáveis cujo conteúdo deve ser permutado.
troca(&tab[ind1], &tab[ind1+1]);
132
Lembre-se que se a função troca() vai receber os endereços dos argumentos a serem
trocados, ela deve armazenar estes endereços em variáveis próprias para armazenar
endereços, ou seja, ponteiros. As variáveis a serem trocadas são então acessadas através
dos ponteiros para as mesmas.
994
998
1002
... 1006 SP
...
tab[ind1] 2 1026
tab[ind1+1] 3 1030
aux:troca 994 SP
... 1006
...
tab[ind1] 2 1026
tab[ind1+1] 3 1030
133
• aux = *a;
aux:troca 2 994 SP
... 1006
...
tab[ind1] 2 1026
tab[ind1+1] 3 1030
• *a = *b;
aux:troca 2 994 SP
... 1006
...
tab[ind1] 3 1026
tab[ind1+1] 3 1030
• *b = aux;
aux:troca 2 994 SP
... 1006
...
tab[ind1] 3 1026
134
tab[ind1+1] 2 1030
• No retorno ao programa principal:
aux:troca 2 994
b:troca 1030 998
a:troca 1026 1002
... 1006 SP
...
tab[ind1] 3 1026
tab[ind1+1] 2 1030
Observe que a função troca() efetivamente alterou o valor das variáveis tab[ind1] e
tab[ind1+1]. Isto foi possível devido à passagem dos endereços dos argumentos para a
função. A esse mecanismo de passagem de parâmetros, usando ponteiros, dá-se o nome de
passagem de parâmetros por referência. Já tínhamos visto antes passagem de parâmetros
por referência quando estudamos a função scanf(). Os argumentos para a função
scanf() são os endereços das variáveis a serem lidas. Desta forma, os valores são lidos
de standard input e armazenados nos endereços das variáveis passadas como argumentos.
De modo a possibilitar um acesso rápido aos dados do programa, ponteiros podem ser
incrementados e decrementados. Seja o seguinte exemplo:
1. void main() {
2. int vetor[3];
3. int *p1, *p2;
4. int i;
5. p1 = vetor;
6. p2 = &vetor[2];
7. *p1 = 0;
135
8. *(p1+1) = 1;
9. *(p1+2) = 2;
10. for(i=0; i<3; i++)
11. printf("vetor[%d] = %d\n", i , vetor[i]);
12. if (p2 > p1)
13. printf("Posicoes: %d\n", p2-p1);
14.}
Saída:
vetor[0] = 0
vetor[1] = 1
vetor[2] = 2
Posicoes: 2
Análise:
Este programa contém as operações básicas que podem ser efetuadas em ponteiros:
• Atribuição
p1 = vetor;
p2 = &vetor[2];
*p1 = 0;
Esta instrução também é familiar. Ela simplesmente diz: “armazene o inteiro 0 na posição de
memória apontada por p1”.
• Incrementando ponteiros
*(p1+1) = 1;
136
Vamos observar o conteúdo da memória até este instante. Suponha que as variáveis foram
carregadas a partir do endereço 1000.
137
i ? 1000 SP
p2 1020 1004
p1 1012 1008
vetor[0] 0 1012
vetor[1] ? 1016
vetor[2] ? 1020
*(p1+1) = 1
armazena o inteiro 1 no byte seguinte ao endereço apontado por p1 (1012). Isso causaria
uma grande confusão uma vez que vetor[0] é um inteiro (o qual ocupa 4 bytes) e o seu
conteúdo seria assim destruído.
*(p1+2) = 2;
De maneira geral, se ptr é um ponteiro para tipo, então a expressão (ptr + i) refere-
se ao endereço:
ptr + i * sizeof(tipo)
138
• Comparações entre ponteiros
Testes relacionais são aceitos somente quando os operandos são ponteiros do mesmo tipo.
• Subtração de ponteiros
No caso da subtração de dois ponteiros, o resultado será dado pelo número de elementos
do tipo apontado existentes entre os ponteiros. Assim,
imprime o decimal 2, uma vez que entre o endereço apontado por p2 (1020) e o endereço
apontado por p1 (1012) existem 2 inteiros (vetor[0] e vetor[1]).
Ponteiros e Vetores
Existe uma forte relação em C entre ponteiros e vetores. Como já vimos, o nome de um
vetor é o endereço da primeira posição desse vetor ou, em outras palavras, o nome de um
vetor é um ponteiro para esse vetor. De fato, você pode usar o nome de um vetor como se
ele fosse um ponteiro, da mesma forma que você pode usar um ponteiro como se ele fosse
um vetor.
int list[10];
ou,
a) Referência a endereço:
b) Referência a conteúdo:
139
*(list+i) // equivalente a list[i]
list[i] // equivalente a *(list+i)
Os grupos de expressões acima são equivalentes, isto é, você pode usar uma no lugar da
outra, independente de list ter sido declarado como um ponteiro ou como um vetor.
Para ilustrar a relação entre ponteiros e vetores, vamos examinar duas versões de um
programa para imprimir o conteúdo de um vetor. A primeira delas usa notação de vetores
para percorrer os elementos do mesmo, enquanto que a segunda usa o nome do vetor
como um ponteiro.
main() {
int list[]={1, 2, 3, 4, 5};
int ind;
• Usando ponteiros:
main() {
int list[]={1, 2, 3, 4, 5};
int ind;
main() {
int *list=(int *)calloc(5, sizeof(int));
int ind;
list[0] = 1;
list[1] = 2;
list[2] = 3;
list[3] = 4;
list[4] = 5;
for (ind=0; ind<5; ind++)
140
printf("%d\n", list[ind]);
}
Esta rotina usa a função calloc() para reservar espaço no heap. Assim, depois da
atribuição, list aponta para um espaço de memória de 20 bytes (5*sizeof(int)),
suficiente para armazenar 5 números inteiros.
main() {
int list[5];
int i;
main() {
int list[]={1, 2, 3, 4, 5};
int *pint;
int ind;
7
Ainda que, em geral, estas multiplicações sejam implementadas com deslocamentos da palavra à
esquerda (shift left), uma operação executada de forma bastante rápida em qualquer CPU.
141
Vetores passados como argumentos
Vimos anteriormente que, quando um vetor é passado como um argumento para uma
função, somente o endereço do mesmo é efetivamente passado. Isso se deve a questões de
eficiência, uma vez que a passagem de um vetor por valor implicaria na cópia dos elementos
do mesmo na pilha, a cada chamada à função.
Vamos reescrever o exemplo anterior, dessa vez usando uma função para imprimir os
elementos do vetor.
main() {
int list[]={1, 2, 3, 4, 5};
imprime(list, 5);
}
Uma vez que o argumento passado para a função imprime() é o endereço do vetor
list, os dois protótipos abaixo são equivalentes:
ou:
Observe as declarações:
1. int vetor1[500]
2. int *vetor2[500];
142
vetor3 é uma variação de vetor1 mas é muito diferente de vetor2.
matriz
p[0]==matriz[0] 1000 1 2 3
p[1]==matriz[1] 1012 4 5 6
p[2]==matriz[2] 1024 7 8 9
8
Com o endereço de alguma outra variável do programa ou através de chamadas às funções calloc() ou
malloc().
143
Observe que cada um dos elementos de p é inicializado com o endereço de uma das linhas
de matriz. Observe ainda que o uso de matrizes e de vetores de ponteiros é similar, no
sentido de que tanto matriz[2][2] como p[2][2], por exemplo, são referências ao
mesmo elemento da matriz.
int matriz[3][3];
int *p[3];
aloca somente 3 posições de memória onde serão armazenados os ponteiros para inteiros.
Os elementos do vetor de ponteiros precisam ser inicializados com o endereço de regiões
de memória grandes o suficiente para armazenarem cada uma das linhas da matriz. Essas
regiões de memória podem ser obtidas através do uso das funções calloc() ou
malloc(), ou, como no exemplo, aproveitando-se o espaço de memória alocado pelo
compilador para outras estruturas do programa. Em contrapartida, as linhas da matriz
podem ser de tamanhos diferentes.
Ponteiros fornecem ainda uma maneira eficiente de lidar com os elementos de uma matriz.
Suponha que, no exemplo anterior, queremos permutar as duas primeiras linhas da matriz.
Vamos comparar as soluções usando matrizes e ponteiros.
• Usando matrizes:
• Usando ponteiros:
144
int *p[3]={matriz[0], matriz[1], matriz[2]};
int *aux;
aux = p[0];
p[0] = p[1];
p[1] = aux;
}
Observe que não trocamos de posição as linhas da matriz, apenas os ponteiros para elas.
Note a otimização do código que foi possível com o uso de ponteiros.
145
matriz
p[1]==matriz[0] 1000 1 2 3
p[0]==matriz[1] 1012 4 5 6
p[2]==matriz[2] 1024 7 8 9
Nos exemplos anteriores o vetor de ponteiros foi inicializado com os endereços das linhas
de uma variável automática matriz dimensionada em tempo de compilação. Veremos a
seguir como determinar as dimensões da matriz dinamicamente, em tempo de execução.
a) Dimensão das linhas fixa; dimensão das colunas determinada em tempo de execução.
1. #define DIML 10
2. #define DIMC 10
3. void main() {
4. int *pMatriz[DIML];
5. int i, j;
Análise:
146
PMatriz[0]
PMatriz[1]
PMatriz[2]
PMatriz[3]
PMatriz[4]
PMatriz[5]
PMatriz[6]
PMatriz[7] Alocados no heap
PMatriz[8]
PMatriz[9]
Alocado na pilha
1. #define DIML 10
2. #define DIMC 10
3. void main() {
4. int **pMatriz;
5. int i, j;
Análise:
147
com o endereço de um buffer no heap de tamanho suficiente para armazenar as linhas da
matriz. Observe que a chamada à função calloc() na linha 6 determina, em tempo de
execução, a dimensão das linhas da matriz. De forma semelhante, as chamadas à função
calloc() na linha8 determinam, em tempo de execução, as dimensões das linhas da
matriz. A figura abaixo ilustra o esquema de alocação dinâmica da matriz.
pMatriz
Alocados no heap
Ponteiros e strings
A Linguagem C oferece duas maneiras de lidar com strings . A primeira, vista no Capítulo
anterior, é através de um vetor de caracteres. A segunda maneira é através de um ponteiro
para caracteres.
1. void main() {
2. char nome[10]="antigo";
148
6. }
Saída:
nome = antigo
nome = novo
Análise:
char nome[10]={‘a’,‘n’,‘t’,‘i’,‘g’,‘o’,‘\0’);
1. void main() {
2. char *nome="antigo";
Saída:
nome = antigo
nome = novo
Análise:
149
ponteiro variável ao qual pode-se, portanto, atribuir qualquer endereço durante a execução
do programa.
char nomes[][10]={“Eduardo”,
“Ricardo”,
“Andre”,
“Alexandre”,
“Debora”,
“Cecilia”
“Marina”};
nomes
1000 E d u a r d o \0
1010 R i c a r d o \0
1020 A n d r e \0
1030 A l e x a n d r e \0
1040 D e b o r a \0
1050 C e c i l i a \0
1060 M a r i n a \0
Observe que as strings constantes são armazenadas na pilha, nas posições de memória
alocadas para o vetor nomes[]. Observe ainda que para cada linha são alocadas 10
posições e que, algumas delas, permanecem sem uso.
char *nomes[]={"Eduardo",
“Ricardo”,
150
"Andre",
"Alexandre",
"Debora",
"Cecilia",
“Marina”};
151
E d u a r d o \0
R i c a r d o \0
nomes[0]
nomes[1] A n d r e \0
nomes[2]
nomes[3] A l e x a n d r e \0
nomes[4]
nomes[5] D e b o r a \0
nomes[6]
C e c i l i a \0
M a r i n a \0
As strings constantes são criadas na área de código pelo compilador e os seus endereços
são atribuídos aos elementos de nomes[]. Observe que agora, não existem bytes sem uso
ao final de cada string.
A versão com ponteiros possibilita implementar de forma eficiente trocas de posições entre
as strings. Suponha, por exemplo, que desejamos ordenar os nomes acima alfabeticamente.
Isto pode ser feito permutando-se os endereços do vetor de ponteiros, ao invés de,
fisicamente, trocar as strings de posição.
E d u a r d o \0
R i c a r d o \0
nomes[0]
nomes[1] A n d r e \0
nomes[2]
nomes[3] A l e x a n d r e \0
nomes[4]
nomes[5] D e b o r a \0
nomes[6]
C e c i l i a \0
M a r i n a \0
Desta forma, nomes[0] aponta para o primeiro nome, nomes[1] para o nome seguinte,
e assim por diante.
152
Você certamente já utilizou algum programa cujos argumentos eram passados na chamada
ao mesmo, a partir do sistema operacional. Exemplo:
Esta instrução chama o compilador gcc passando como argumentos uma diretiva de
compilação (-o), o nome do programa executável (teste) e o nome do programa fonte
(teste.c).
Como fazer isso em C? Quando a função main() é ativada, a ela são passados dois
argumentos (que podem ou não ser utilizados): um contador do número de argumentos
passados na linha de comando e um vetor de ponteiros para os argumentos propriamente
ditos. O protótipo da função main() poderia então ser escrito como:
Como exemplo, vejamos o programa echo que ecoa os seus argumentos na saída padrão.
Assim, a linha de comando:
produziria na saída:
Por convenção, argv[0] contém o nome pelo qual o programa foi ativado, de modo que
argc é sempre igual ou maior do que 1. No exemplo acima teríamos:
argc == 6
e,
argv[0] → "ECHO"
argv[1] → "NCE"
argv[2] → "Nucleo"
argv[3] → "de"
argv[4] → "Computacao"
argv[5] → "Eletronica"
Funções também possuem endereços que podem ser referenciados indiretamente através
de ponteiros.
tipo (*ptr)();
Na declaração acima, ptr é um ponteiro para uma função que retorna um tipo.
tipo *ptr();
void main() {
int op, result, num;
int dobro(int);
int triplo(int);
int quadruplo(int);
switch (op) {
case '2': /* Multiplicar por 2 */
result = dobro(num);
break;
case '3': /* Multiplicar por 3 */
result = triplo(num);
break;
case '4': /* Multiplicar por 4 */
result = quadruplo(num);
154
}
void main() {
int op, num;
int dobro(int);
int triplo(int);
int quadruplo(int);
VectPontFunc ptr={dobro, triplo, quadruplo};
return(4*num);
}
155
Observe a declaração,
Você pode pensar na sintaxe da declaração de um tipo como a declaração de uma variável
precedida do prefixo typedef. O identificador correspondente ao nome da variável passa
a designar o tipo recém criado.
Exemplo
cria uma variável ptr do tipo VectPontFunc. Isto é, ptr é um vetor de 3 posições
onde cada um dos elementos é um ponteiro para uma função que recebe um argumento
inteiro e retorna um valor inteiro. Os elementos do vetor são inicializados com os endereços
das funções dobro(), triplo() e quadruplo().
(*ptr[i])(num);
ela chama a função apontada pelo i-ésimo elemento do vetor ptr, passando como
parâmetro o inteiro num. Por exemplo, a instrução:
(*ptr[0])(num);
é equivalente a:
dobro(num)
156
Comparando as duas versões do programa acima, percebe-se que vetores de ponteiros
para funções possibilitam a construção de um código mais eficiente, ainda que de mais difícil
compreensão.
157
CAPÍTULO 11 - Estruturas e Uniões
A forma mais geral de estruturação de dados consiste na junção de tipos em um tipo
composto.
Estruturas
Podemos fazer a analogia entre uma estrutura de dados do tipo vetor, onde todos os
elementos devem ser do mesmo tipo, e um gaveteiro, onde todas as gavetas são do mesmo
tamanho. Por exemplo:
int x[7];
corresponderia a:
x[0]
x[1]
x[2]
x[3]
x[4]
x[5]
x[6]
O que aconteceria se desejássemos criar uma nova estrutura em que cada um dos seus
elementos fosse de um tipo diferente?
Este é um caso que ocorre com muita freqüência em aplicações práticas como por
exemplo, quando temos que armazenar as informações relativas aos empregados de uma
158
firma. Estas informações podem constar, por exemplo, de: nome (cadeia de caracteres),
código (inteiro) e salário (real).
CÓDIGO : 1234
SALÁRIO : 1000,00
Esta forma de organizar uma estrutura, com tipos diferentes, é muito comum em
processamento de dados e é conhecida como REGISTRO. Registros em C são
designados pela palavra reservada struct.
NOME
CÓDIGO
SALÁRIO
sizeof(funcionário)==20*sizeof(char)+sizeof(int)+sizeof(float
)
Quando a mesma estrutura for utilizada em diversos pontos do programa, pode-se dar um
nome à mesma de modo que ela não tenha de ser redefinida cada vez que declararmos uma
variável do tipo da estrutura. Vejamos um exemplo:
Observe que RegFunc não é o nome de uma variável e sim, o nome da estrutura. A
declaração de outras variáveis do tipo RegFunc dentro do programa, podem ser feitas
utilizando-se apenas o nome da estrutura, não sendo necessária a definição dos campos.
A definição de uma estrutura pode ser feita também, sem a lista de variáveis ao seu final.
Suponha, por exemplo, que você desejasse que a definição de RegFunc fosse global a
todo o programa, mas que nenhuma variável fosse global. A solução seria fazer:
main() {
struct RegFunc funcionario;
struct RegFunc gerente; // declaração das variáveis
.
.
}
9
Observe a forma de referenciar os campos da estrutura. Isto será visto com mais detalhes adiante neste
Capítulo.
160
Repare o ponto e vírgula ao final da definição da estrutura. Ele agora é necessário.
Temos agora que descobrir como ter acesso às informações de uma estrutura.
Existe alguma semelhança entre a maneira utilizada para se ter acesso aos elementos de um
vetor e aquela utilizada para os campos de um registro. Ambas são compostas pelo nome
da variável seguida por um seletor.
x [5]
variável seletor
funcionário.salário
variável seletor
scanf("%s", funcionário.nome);
scanf("%d", &funcionário.código);
scanf("%f", &funcionário.salário);
161
• os campos podem ser de qualquer tipo;
Inicializando estruturas
Da mesma forma que vetores, estruturas são inicializadas com uma lista de valores (cada um
correspondente a um campo da estrutura) entre chaves e separados por vírgulas.
void main() {
struct RegFunc
funcionário={"Márcio Nascimento", 1234, 1000.00},
gerente={"Roberto Diniz", 24, 2000.00};
}
Em C clássico, atribuições entre estruturas tinham de ser feitas campo a campo, como no
exemplo:
strcpy(gerente.nome, funcionário.nome);
gerente.código = funcionário.código;
gerente.salário = funcionário.salário;
Versões mais modernas do compilador C permitem que se faça uma atribuição direta entre
estruturas do mesmo tipo:
gerente = funcionário;
162
Estruturas aninhadas
Como visto anteriormente, os campos de uma estrutura podem ser de qualquer tipo,
inclusive outras estruturas. Suponha por exemplo, que desejamos incluir no registro de
funcionários a data de aniversário composta de dia, mês e ano. A solução seria:
void main() {
RegFunc // declaração das variáveis
funcionário = {"Márcio Nascimento",
1234,
1000.00,
{10, "Janeiro", 1962}},
gerente = {"Roberto Diniz",
24,
2000.00,
{9, "Marco", 1959}};
.
.
}
Observe a inicialização das variáveis. A estrutura aninhada é inicializada também com uma
lista de valores entre chaves e separados por vírgulas. Observe ainda a declaração das
estruturas. Foi usado o prefixo typedef de modo a tornar data e RegFunc os
identificadores dos tipos criados.
funcionário.nascimento.dia = 11;
strcpy(gerente.nascimento.mês, "Abril");
Estruturas e funções
163
Em versões mais antigas de compiladores C, 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 campos da estrutura
para a pilha poderia consumir um tempo exagerado. Dessa forma, estruturas eram
obrigatoriamente passadas por referência, usando-se o operador de endereço (&).
1. typedef struct {
2. char nome[20];
3. int código;
4. float salário;
5. } RegFunc;
6. RegFunc NovoFuncionario(void);
7. void list(RegFunc);
8. void main() {
9. RegFunc funcionário;
Comentários:
(linha 10) Atribuição entre estruturas. A estrutura funcionário recebe os valores dos
campos da estrutura retornada pela função NovoFuncionario().
Pode-se declarar ponteiros para estruturas da mesma forma que declaramos ponteiros para
outros tipos de dados. Ponteiros para estruturas são essenciais à criação de estruturas de
dados dinâmicas tais como listas encadeadas e árvores binárias. De fato, ponteiros para
estruturas são usados tão freqüentemente em C que existe um símbolo especial para
acessar um campo de uma estrutura apontada por um ponteiro.
1. typedef struct {
2. char nome[20];
3. int codigo;
4. float salario;
5. } RegFunc;
8. void main() {
9. RegFunc funcionario;
10. NovoFuncionario(&funcionario);
11. list(&funcionario);
12. // .
13. // .
14. }
Comentários:
(linha 10) De maneira idêntica à passagem por referência de qualquer outro tipo de
variável, o endereço da estrutura é obtido com o operador de endereço (&).
(linha 16) Acesso aos campos de uma estrutura apontada por um ponteiro. Observe que
os parênteses são obrigatórios uma vez que o operador (.) tem precedência
sobre o operador de indireção (*). No caso do exemplo, uma vez que
(pFunc==&funcionario), é verdadeira a relação:
(*pFunc).nome==funcionario.nome
(linha 21) De fato, a referência a campos de estruturas apontadas por ponteiros é uma
construção tão freqüente em C, que um operador foi criado especialmente para
lidar com esta situação. Este operador é formado pelo sinal de menos (-)
seguido pelo símbolo de maior (>).
pFunc->nome
pFunc->nome == funcionario.nome
pFunc->nome == (*pFunc).nome ==
funcionario.nome
Vetor de estruturas
166
Quando definimos a estrutura funcionário, criamos uma variável capaz de armazenar apenas
um empregado da empresa. Como fazer para armazenar os dados de 1000 funcionários de
uma empresa?
void main() {
RegFunc funcionario[MAXFUNC];
.
.
}
Observe que foi criada uma tabela de funcionários (um vetor) onde cada elemento da tabela
é uma estrutura contendo os dados de um funcionário.
funcionário[25].salario
167
6. void main() {
7. int i;
8. RegFunc funcionario[]={{"Alice Falcao", 4650, 1231.00},
9. {"Ronaldo Jesus", 7587, 670.00},
10. {"Almir Cardoso", 3640, 247.00},
11. {"Jose Lima", 6194, 1118.00},
12. {"Joao Prado", 2435, 889.00},
13. .
14. .
15. };
Comentários:
sizeof(funcionario)/sizeof(RegFunc)
Ela é usada para determinar o número de elementos no vetor funcionario[] uma vez
que a dimensão do vetor não foi explicitamente definida na declaração do mesmo.
Listas encadeadas
Suponha que o programa anterior fosse usado em uma empresa com 5 funcionários.
Haveria um desperdício muito grande de memória uma vez que foram alocadas posições
para o registro de 1000 funcionários.
Suponha agora, que o número de empregados na sua empresa fosse crescendo até o dia
em que excedesse o valor de MAXFUNC. Nesse dia, seria necessário alterar o valor de
MAXFUNC e recompilar o programa, o qual seria utilizável até o dia em que se fizesse
necessária nova alteração.
168
Uma solução para esse problema é a utilização de uma estrutura chamada lista encadeada
onde os elementos da lista (no nosso exemplo, os registros de funcionários) são criados
dinamicamente, à medida em que se façam necessários.
Uma lista encadeada é uma estrutura em que cada elemento contém um ponteiro para o
próximo elemento na lista.
Pode-se fazer uma analogia entre uma lista encadeada e a sala de espera de um consultório.
Muito embora os pacientes não entrem em fila, cada um deles conhece o paciente que
chegou imediatamente após a ele, ou seja, cada paciente é capaz de apontar o paciente
seguinte, o que garante que eles sejam atendidos na ordem de chegada.
Vamos modificar os registros de funcionários, de modo a que eles possam ser estruturados
na forma de uma lista encadeada.
Observe com atenção a declaração acima: a estrutura RegFunc contém agora um campo
(pprox) que é um ponteiro para uma estrutura do tipo RegFunc. Observe a
correspondência com a figura da lista encadeada.
169
O algoritmo para percorrer listas encadeadas pode ser visto no exemplo abaixo. A fim de
simplificar a análise, vamos construir uma lista encadeada usando os elementos de um vetor.
Esta não é a construção mais usual uma vez que, normalmente, os elementos de uma lista
são obtidos em tempo de execução com as funções calloc() ou malloc(). Mais
adiante veremos exemplos de construção de listas encadeadas usando alocação dinâmica
de memória.
7. void main() {
8. RegFunc func[3]={{"Jorge", 3554, 750.0, &func[1]},
9. {"Paulo", 1234, 500.0, &func[2]},
10. {"Carlos", 755, 1200.0, NULL}};
11. RegFunc *Head=&func[0];
12. RegFunc *pFunc;
13. pFunc=Head;
14. while(pFunc!=NULL) {
15. printf("Nome : %s\n", pFunc->nome);
16. pFunc=pFunc->pProx;
17. }
18. }
Análise:
Observe a construção da lista encadeada nas linhas 8-10: o primeiro elemento do vetor
(func[0]) aponta para o segundo elemento (func[1]). Este, por sua vez aponta para o
elemento seguinte (func[2]). O primeiro elemento da lista é apontado pela variável
Head. O último elemento da lista aponta para NULL. A lista é percorrida usando-se um
ponteiro auxiliar pFunc.
O procedimento para percorrer a lista encadeada pode ser visto nas linhas 13-17. Observe
com atenção a linha 16. Nela o ponteiro pFunc é atualizado e passa a apontar o elemento
seguinte na lista. O procedimento é repetido até que pFunc==NULL.
O laço das linhas 13-17 no exemplo anterior poderia ser reescrito como:
pFunc=Head;
while(pFunc) {
printf("Nome : %s\n", pFunc->nome);
pFunc=pFunc->pProx;
}
ou ainda:
170
for(pFunc=Head; pFunc; pFunc=pFunc->pProx)
printf("Nome : %s\n", pFunc->nome);
Vamos ver agora como adicionar novos elementos à lista. Em primeiro lugar é preciso
alocar espaço em memória para o novo elemento. Isto pode ser feito com as funções
calloc() ou malloc(). Vejamos como seria a inserção de um novo funcionário na lista
do exemplo anterior:
Head 0
pNew
strcpy(pNew->nome, "Mauro");
pNew->codigo=345;
pNew->salario=780.0;
O próximo passo é inserir o elemento recém criado na lista. Suponha que os registros de
funcionários não são armazenados em ordem. Nesse caso, a inserção pode ser feita perto
da cabeça, o que nos livra de percorrer toda a lista procurando o lugar certo para o novo
registro. A inserção se dá através de duas instruções:
1. pNew->pProx=Head;
Head 0
171
pNew
2. Head=pNew;
Head 0
pNew
Vamos imaginar o caso de uma empresa que tenha um certo número de funcionários e
deseja implementar um cadastro em computador com informações de todo o seu pessoal.
Este cadastro deve ser constantemente atualizado com as alterações no quadro de
funcionários da empresa e, portanto, requer um programa para manutenção e visualização
do mesmo.
1. Inclusão de funcionários;
2. Exclusão de funcionários;
3. Alteração dos dados de funcionários;
4. Consulta aos dados de funcionários;
5. Listagem dos dados de todos os funcionários;
SISTEMA
CADASTRO
172
Os módulos LER ARQUIVO e GRAVAR ARQUIVO já podem ser codificados,
enquanto PROCESSAR CADASTRO precisa ainda ser refinado através de um outro
diagrama.
PROCESSAR
CADASTRO
Os módulos MOSTRAR MENU e LER OPÇÃO já podem ser codificados, mas o que
significa EXECUTAR OPÇÃO?
EXECUTAR
OPÇÃO
0 1 2 3 4 5
INCLUIR ALTERAR CONSULTAR EXCLUIR LISTAR FIM
Para manipular o cadastro na memória do computador vamos usar uma lista encadeada
contendo os registros dos funcionários.
Em geral, devido às suas dimensões, o cadastro deve ficar armazenado em disco. Para isto
precisamos de rotinas para buscar e guardar em disco as informações do cadastro. Estas
rotinas serão objeto de estudo do próximo capítulo, quando trataremos da manipulação de
arquivos.
struct RegFunc {
char nome[20];
int codigo;
173
float salario;
struct RegFunc *proximo;
};
174
struct RegFunc *cabeca=NULL;
struct RegFunc *ptr;
BOOL fim=FALSE;
int LerOpcao(void);
void ExecutarOpcao(int opcao);
void LerArquivo(void);
void MostrarMenu(void);
void ProcessarCadastro(void);
void GravarArquivo(void);
void main() {
LerArquivo();
ProcessarCadastro();
GravarArquivo();
}
void LerArquivo() {
// le os registros de funcionarios de um
// arquivo de entrada e os coloca em uma
// lista encadeada.
}
void ProcessarCadastro() {
int opcao;
do {
MostrarMenu();
opcao=LerOpcao();
ExecutarOpcao(opcao);
}
while (!fim);
}
void MostrarMenu() {
// por fazer
}
int LerOpcao() {
// por fazer
}
(*matriz[opcao])();
}
175
void Incluir() {
// por fazer
}
void Alterar() {
// por fazer
}
void Consultar() {
// por fazer
}
void Excluir() {
// por fazer
}
void Listar() {
// por fazer
}
void Fim() {
fim=TRUE;
}
void GravarArquivo() {
// por fazer
}
Uniões
A sintaxe da definição e uso de uma união é a mesma de uma estrutura. Estruturas e Uniões
são ambas usadas para armazenar elementos de tipos diferentes em uma mesma variável.
A diferença reside no fato de que, quando declaramos uma variável do tipo estrutura, o
compilador aloca espaço suficiente para armazenar todos os elementos ao mesmo tempo
enquanto que, para variáveis do tipo união, apenas um dos elementos é armazenado por
vez.
Exemplo:
typedef struct {
long i1;
long i2;
float f1;
float f2;
} RegStruct;
void main() {
RegStruct estrutura;
estrutura.i1 = 2;
176
estrutura.i2 = 3;
printf ("i1 = %-3d i2 = %-3d\n", estrutura.i1, estrutura.i2);
estrutura.f1 = 2.5;
estrutura.f2 = 3.5;
printf ("f1 = %.1f f2 = %.1f\n", estrutura.f1, estrutura.f2);
}
Saída:
i1 = 2 i2 = 3
f1 = 2.5 f2 = 3.5
Observe que os dados não se confundem, uma vez que o compilador aloca memória
suficiente para armazenar estrutura.i1, estrutura.i2, estrutura.f1, e
estrutura.f2 ao mesmo tempo.
estrutura.i1
estrutura.i2
estrutura.f1
estrutura.f2
sizeof(estrutura)==sizeof(estrutura.i1)+sizeof(estrutura.i2)+
sizeof(estrutura.f1)+sizeof(estrutura.f2)
typedef union {
long i1;
long i2;
float f1;
float f2;
} RegUnion;
void main() {
RegUnion uniao;
uniao.i1 = 2;
uniao.i2 = 3;
printf ("i1 = %-3ld i2 = %-3ld\n", uniao.i1, uniao.i2);
uniao.f1 = 2.5;
uniao.f2 = 3.5;
177
printf ("f1 = %.1f f2 = %.1f\n", uniao.f1, uniao.f2);
}
Saída:
i1 = 3 i2 = 3
f1 = 3.5 f2 = 3.5
uniao.i1==uniao.i2==uniao.f1==uniao.f2
Desta forma, quando fizemos (uniao.i2 = 3), o inteiro 3 foi escrito na mesma posição
de memória onde anteriormente havíamos feito: (uniao.i1 = 2). Esse mesmo espaço
de memória foi usado nas instruções seguintes para fazer as atribuições dos reais.
O tamanho do bloco de memória alocado para uma união é o tamanho do maior dos seus
elementos. No exemplo anterior, as variáveis têm todas o mesmo tamanho, logo:
sizeof(uniao)==sizeof(uniao.i1)==sizeof(uniao.i2)
==sizeof(uniao.f1)==sizeof(uniao.f1)
178
union {
long i1;
long i2;
float f1;
float f2;
} uniao;
Da mesma forma que em estruturas, aqui também podemos dar um nome ao tipo da
estrutura de forma que ele não precise ser redefinido sempre que referenciado.
union RegUnion {
long i1;
long i2;
float f1;
float f2;
};
void main() {
RegUnion uniao;
.
.
}
Pode-se ainda criar um novo tipo usando o prefixo typedef de modo a simplificar a
declaração de uniões.
typedef union {
long i1;
long i2;
float f1;
float f2;
} RegUnion;
void main() {
RegUnion uniao;
.
.
}
Da mesma forma que em estruturas, o operador ponto (.) é usado para acessar os
membros de uma união. Deve-se ter em mente que, como todos os elementos compartilham
a mesma posição de memória, o tipo da variável armazenada é o mesmo da última
atribuição de valor feita à união.
179
Por exemplo, no programa anterior, se tentarmos referenciar uniao.i1 após atribuir um
valor a uniao.f1 o resultado será imprevisível, uma vez que o compilador interpretará o
conteúdo da variável real uniao.f1 como um número inteiro.
180
Estruturas como membros de uniões
Da mesma forma que uma estrutura pode ser membro de uma outra estrutura, uniões
podem ser membros de outras uniões, uniões podem ser membros de estruturas e estruturas
podem ser membros de uniões. Vejamos um exemplo desse último caso:
typedef struct {
long i1;
long i2;
} DoisInt;
typedef struct {
float f1;
float f2;
} DoisFloat;
union {
DoisInt i;
DoisFloat f;
} teste;
void main() {
teste.i.i1 = 2;
teste.i.i2 = 3;
printf("i1 = %-3ld i2 = %-3ld\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);
}
Saída:
i1 = 2 i2 = 3
f1 = 2.5 f2 = 3.5
Observe que os campos das estruturas internas à união são acessados usando-se o
operador ponto duas vezes:
teste.i.i1
181
Por que usar uniões?
A união fornece uma forma de ver o mesmo dado de várias maneiras diferentes. Seja o
seguinte exemplo:
union {
uint word;
RegByte byte;
} numero;
void main() {
numero.word=0xABCD;
printf("numero : %X\n", numero.word);
printf("byte baixo : %X\n", numero.byte.baixo);
printf("byte alto : %X\n", numero.byte.alto);
}
Saída:
numero : ABCD
byte baixo : FFCD
byte alto : FFAB
numero.word
numero.byte.alto
numero.byte.baixo
182
CAPÍTULO 12 - Operações com arquivos
Chamamos de arquivo a uma coleção de bytes armazenados em memória secundária
(disquete, disco rígido, CD-ROM, etc.), e referenciados por um nome comum.
A estrutura FILE
Todo arquivo aberto tem a ele associado uma estrutura do tipo FILE. Toda e qualquer
referência ao arquivo se dá através de um ponteiro para tal estrutura.
O tipo FILE é uma estrutura declarada em stdio.h cujos campos são informações sobre
o arquivo sendo acessado. Estas informações são úteis ao sistema operacional a fim de
gerenciar o acesso ao arquivo aberto. Algumas dessas informações são: status do arquivo,
endereço do buffer para transferência de dados, posição corrente do ponteiro, etc.
183
A estrutura FILE <STDIO.H>
typedef struct{
short level;
unsigned flags;
char fd;
unsigned char hold;
short bsize;
unsigned char *buffer, *curp;
unsigned istemp;
short token;
} FILE;
FILE *pFILE;
Abrindo arquivos
onde, filename é uma string contendo o nome do arquivo e mode é o modo de abertura
do arquivo.
Exemplo:
184
r Abre um arquivo apenas para leitura. O arquivo deve existir no disco,
w Abre um arquivo para escrita. Se já existir um arquivo com o mesmo nome, este
arquivo será sobrescrito.
a Abre um arquivo para gravação. Se o arquivo existir os dados são adicionados ao
seu final. Se ele não existir será criado.
r+ Abre um arquivo para atualização (leitura e escrita). O arquivo deve estar presente
no disco.
w+ Abre um arquivo para atualização (leitura e escrita). Se o arquivo já existir no
disco ele será destruído e reinicializado. Se ele não existir será criado.
a+ Abre um arquivo para atualização (leitura e escrita). Se o arquivo existir os dados
são adicionados ao seu final. Se ele não existir será criado.
Podem ocorrer erros ao abrir-se arquivos. Pode-se, por exemplo, abrir um arquivo não
existente para leitura ou abrir um arquivo para escrita em um meio magnético protegido
contra escrita ou sem nenhum espaço livre.
Exemplo:
if((pFILE=fopen("saida.dat", "r"))==NULL) {
puts("Nao pude abrir o arquivo");
return;
}
Fechando Arquivos
Ainda que todos os arquivos abertos sejam fechados pelo sistema operacional ao término
da aplicação, consiste em boa regra de programação fechar, explicitamente, qualquer
arquivo que tenha sido aberto durante a execução do programa
185
Leitura e gravação de caracteres
A leitura e gravação de caracteres se dá através do uso de funções semelhantes às usadas
para entrada e saída de caracteres de standard input/standard output. Estas funções são
apresentadas a seguir:
Declaração
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
Comentários
fgetc() retorna o próximo caracter no arquivo especificado.
Valor de Retorno
Em caso de sucesso,
fgetc() retorna o caracter lido (convertido para um número inteiro sem sinal)
fputc() retorna o caracter c.
Exemplos:
Escrevendo um arquivo caracter a caracter: O programa abaixo lê uma seqüência de
caracteres do teclado e os grava em um arquivo. O programa termina quando o usuário
pressionar o Return.
void main() {
FILE *pFILE;
char ch;
pFILE=fopen("saida.dat", "w");
while((ch=getchar())!='\n')
fputc(ch, pFILE);
fclose(pFILE);
}
186
Lendo um arquivo caracter a caracter: O programa a seguir escreve na tela do computador
caracteres lidos de um arquivo cujo nome é passado como argumento na linha de comando.
pFILE=fopen(argv[1], "r");
while ((ch=fgetc(pFILE))!=EOF)
putchar(ch);
fclose(pFILE);
}
É importante entender que EOF não é um caracter e sim um número inteiro, definido em
stdio.h com o valor -1. Observe que a função fgetc() retorna um inteiro, a fim de
que o caracter de código ASCII 255, se porventura presente ao arquivo, não seja
interpretado como o EOF.
Declaração
char *fgets(char *s, int n, FILE *stream);
int fputs(const char *s, FILE *stream);
Comentários
187
fgets() lê uma seqüência de caracteres do arquivo e os armazena na string s. A
leitura é interrompida quando são lidos (n-1) caracteres ou quando for lido um caracter
de nova linha (‘\n’), o que ocorrer primeiro. fgets() mantém o caracter de nova
linha no final de s e insere o caracter nulo, para marcar o final da string.
fputs() copia a string s (terminada por um ‘\0’) para o arquivo especificado. Ela
não acrescenta o caracter de nova linha à string e o caracter nulo não é copiado.
Valor de Retorno
Em caso de sucesso,
fgets() retorna a string apontada por s.
fputs() retorna o último caracter escrito.
A função fgets() é semelhante à função gets() mas há alguns pontos onde elas são
distintas. A tabela abaixo apresenta a comparação das duas funções:
gets() fgets()
entrada teclado arquivo
término de leitura ‘\n’ ‘\n’ ou EOF
‘\n’ ao final não é incluído incluído
‘\0’ ao final incluído incluído
Exemplos:
if((pFILE=fopen(argv[1],"r"))==NULL) {
puts("Nao pude abrir o arquivo");
return;
}
while(fgets(linha, 80, pFILE)!=NULL)
printf("%s", linha);
fclose(pFILE);
}
188
Gravando um arquivo linha a linha: O programa abaixo lê uma seqüência de linhas de stdin
e as grava no arquivo de saída.
while(strlen(gets(linha))>0) {
fputs(linha, pFILE);
fputs("\n", pFILE);
}
fclose(pFILE);
}
O programa termina quando o usuário entra com uma linha vazia. Observe que a função
fputs() não coloca o caracter de nova linha ('\n') ao final da linha sendo impressa, o
que nós tivemos de fazer explicitamente através de outra chamada à fputs().
• Saída formatada
fprintf() <STDIO.H>
Envia saída formatada para um arquivo.
Declaração
int fprintf (FILE *stream, char *format [, argument,
...]);
Comentários
A função fprintf() recebe uma série de argumentos, aplica a cada um deles um
padrão de formatação especificado na string *format e escreve o dado formatado em
um arquivo.
A função aplica o primeiro padrão de formatação ao primeiro argumento, o segundo
padrão ao segundo argumento, o terceiro ao terceiro, e assim por diante, até o último
dos argumentos.
Observação: Devem existir suficientes argumentos para serem formatados. Se houver
menos argumentos do que padrões de formatação os resultados serão imprevisíveis e,
189
provavelmente, desastrosos. Argumentos em excesso (em maior número do que os
requeridos pela string de formatação) são simplesmente ignorados.
Valor de Retorno
Em caso de sucesso a função fprintf() retorna o número de bytes escritos no
arquivo de saída.
Em caso de erro a função retorna EOF
Exemplo:
#define DIM 3
typedef struct {
char nome[20];
unsigned codigo;
float salario;
} RegFunc;
void main() {
RegFunc func[DIM]={{"Jorge", 3554, 1000.0},
{"Paulo", 1234, 500.0},
{"Carlos", 755, 720.0}};
FILE *pFILE=fopen("saida.dat", "w");
int i;
Entrada formatada
Entrada formatada é efetuada com a função fscanf(). Esta função é similar à scanf(),
exceto que, assim como em fprintf(), um ponteiro para FILE é tomado como
primeiro argumento.
fscanf() <STDIO.H>
fscanf() lê e formata dados lidos de um arquivo de entrada.
190
Declaração
int fscanf (FILE *stream, char *format [, address, ...]);
Comentários
A função fscanf() lê uma série de campos de entrada (um caracter por vez),
formata cada campo de acordo com um especificador de formato (passado na string de
formatação *format) e armazena a entrada formatada no endereço passado como
argumento após a string *format.
Deve haver um especificador de formato e um endereço para cada campo lido.
fscanf(), em geral, leva a resultados inesperados se o especificador de formato
diverge do dado lido.
A combinação de fgets() seguida por sscanf() é mais segura e mais simples e,
portanto, recomendada sobre fscanf().
Valor de Retorno
Em caso de sucesso, fscanf() retorna o número de campos de entrada lidos,
formatados e armazenados com sucesso.
fscanf() retorna EOF se for feita uma tentativa de leitura além do final do arquivo.
Exemplo:
Vamos usar como entrada o programa saida.dat gerado no exemplo anterior.
#define DIM 3
typedef struct {
char nome[20];
unsigned codigo;
float salario;
} RegFunc;
void main() {
int i;
RegFunc func[DIM];
FILE *pFILE=fopen("saida.dat", "r");
func[0].nome==Jorge
func[0].codigo==3554
func[0].salario==1000
func[1].nome==Paulo
191
func[1].codigo==1234
func[1].salario==500
func[2].nome==Carlos
func[2].codigo==755
func[2].salario==720
fwrite() <STDIO.H>
Escreve um bloco de dados em um arquivo.
Declaração
size_t fwrite(void *ptr, size_t size, size_t n, FILE* stream);
Comentários
fwrite() escreve um número especificado de blocos de dados, de mesmo tamanho,
em um arquivo de saída.
Argumento Significado
ptr Ponteiro para o bloco de dados; os dados a serem escritos
começam em ptr.
size Tamanho de cada bloco de dados (em bytes)
n Número de blocos a serem escritos
stream Ponteiro para o arquivo de saída.
192
Valor de Retorno
fwrite() retorna o número de blocos (de blocos, não de bytes) de fato escritos.
Exemplo:
Vamos estudar o mesmo programa usado para exemplificar o uso de saída de dados
formatada.
#define DIM 3
typedef struct {
char nome[20];
unsigned codigo;
float salario;
} RegFunc;
void main() {
RegFunc func[DIM]={{"Jorge", 3554, 1000.0},
{"Paulo", 1234, 500.0},
{"Carlos", 755, 720.0}};
FILE *pFILE=fopen("saida.dat", "w");
4A 6F 72 67 65 00 00 00 00 00 00 00 00 00 00 00 Jorge···········
00 00 00 00 E2 0D 00 00 7A 44 50 61 75 6C 6F 00 ········zDPaulo·
00 00 00 00 00 00 00 00 00 00 00 00 00 00 D2 04 ················
00 00 FA 43 43 61 72 6C 6F 73 00 00 00 00 00 00 ···CCarlos······
00 00 00 00 00 00 00 00 F3 02 00 00 34 44 ············4D
Observe que todo o vetor foi gravado com uma única chamada à função fwrite().
Compare com o código que necessário para gravar o vetor usando a função fprintf().
Observe ainda que a função fwrite() efetua um dump de memória, isto é, os dados são
gravados em disco na mesma forma em que eles eram armazenados na memória do
computador. Por este motivo, o arquivo resultante é dito um arquivo binário (em oposição
ao arquivo texto obtido quando da gravação dos dados usando fprintf()).
Observe por exemplo o código do funcionário Jorge. O número inteiro sem sinal 3554 é
armazenado como 0DE2 (sua representação binária). Da mesma forma o salário de Jorge
(1000.0) é armazenado como 00 00 7A 44.
Um arquivo binário não pode ser imediatamente lido a partir de um comando type ou
cat. Os pontos na representação ASCII no exemplo anterior (coluna à direita)
correspondem a caracteres não imprimíveis no terminal ou impressora.
193
Transferindo blocos do disco para a memória.
fread() <STDIO.H>
Lê um bloco de dados de um arquivo.
Declaração
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
Comentários
fread() lê um número especificado de blocos de dados, de mesmo tamanho, de um
arquivo de entrada.
Argumento Significado
ptr Início da área de memória que receberá os dados lidos.
size Tamanho de cada bloco lido (em bytes)
n Número de blocos a serem lidos
stream Ponteiro para o arquivo de entrada.
Valor de Retorno
fread() retorna o número de blocos (não de bytes) de fato lidos.
Exemplo
Vamos usar o arquivo saida.dat obtido no exemplo anterior.
#define DIM 3
typedef struct {
char nome[20];
unsigned codigo;
float salario;
} RegFunc;
void main() {
int i;
RegFunc func[DIM];
FILE *pFILE=fopen("saida.dat", "rb");
func[0].nome==Jorge
func[0].codigo==3554
func[0].salario==1000
func[1].nome==Paulo
func[1].codigo==1234
func[1].salario==500
func[2].nome==Carlos
func[2].codigo==755
func[2].salario==720
Observe o comando de abertura do arquivo:
pFILE=fopen("saida.dat", "rb");
A string "rb" indica ao sistema operacional que o arquivo deve ser aberto no modo de
leitura ("r") e que trata-se de um arquivo binário ("b"). Esta última informação é
necessária para que o caracter 0x0D, se presente no arquivo, não seja interpretado como o
caracter de nova linha.
Acesso aleatório
Todo arquivo aberto tem a ele associado um ponteiro para a posição corrente no arquivo.
Esse ponteiro fornece a localização do próximo byte a ser lido ou escrito.
O acesso aleatório ao arquivo, por sua vez, permite que a transferência de dados seja feita
de/para qualquer posição do arquivo sem que as informações anteriores precisem ser
acessadas. Isto é conseguido com o uso da função fseek() que altera o ponteiro de
posição corrente para a localização desejada. Segue-se então uma operação elementar de
leitura ou gravação seqüencial dos dados.
fseek() <STDIO.H>
Reposiciona o ponteiro de posição corrente em um arquivo
Declaração
int fseek(FILE *stream, long offset, int origem);
195
Comentários
fseek() altera o ponteiro associado a um arquivo para uma nova posição.
Argumento Significado
stream Arquivo cujo ponteiro é alterado por fseek()
offset Diferença em bytes entre origem (uma posição relativa no arquivo) e a
nova posição. Para arquivos texto, offset deve ser 0.
origem Três valores possíveis:
0. começo do arquivo
1. posição corrente do ponteiro
2. fim do arquivo
Observe que o offset, contado a partir do fim do arquivo, deve ser negativo. Da mesma
forma, o offset contado a partir do início do arquivo deve ser positivo.
Valor de Retorno
Em caso de sucesso (o ponteiro é movido com sucesso), fseek() retorna 0.
Em caso de erro, fseek() retorna um valor não nulo.
Exemplo:
Vamos rever o programa exemplo anterior. Suponha, que queremos ler apenas o último
registro do vetor de funcionários. A solução seria:
#define DIM 3
typedef struct {
char nome[20];
unsigned codigo;
float salario;
} RegFunc;
void main() {
RegFunc func[DIM];
FILE *pFILE=fopen("saida.dat", "rb");
nome : Carlos
196
codigo : 755
salario : 720
Arquivos padrão
Nome Significado
stdin Dispositivo padrão de entrada
stdout Dispositivo padrão de saída
stderr Dispositivo padrão para mensagens de erro
stdaux Dispositivo auxiliar padrão
stdprn Impressora padrão
Cada um desses nomes pode ser usado como um ponteiro para uma estrutura FILE
associada ao periférico correspondente.
fputc(ch, stdout);
putchar(ch)
ch = fgetc(stdin);
ch = getchar();
197