Você está na página 1de 198

PROGRAMAÇÃO

DE COMPUTADORES

DISCIPLINAS REGULARES

1o Módulo:

Técnicas de
Programação
ÍNDICE

Capítulo 1 ...... Introdução à Linguagem C.........................2


Capítulo 2 ...... Saída de Dados .........................................8
Capítulo 3 ...... Tipos de Dados em C ................................14
Capítulo 4 ...... Operadores e Expressões .........................24
Capítulo 5 ...... Entrada de Dados ......................................34
Capítulo 6 ...... Desvio Condicional ....................................39
Capítulo 7 ...... Laços .........................................................49
Capítulo 8 ...... Funções .....................................................59
Capítulo 9 ...... Vetores.......................................................92
Capítulo 10 .... Ponteiros ....................................................122
Capítulo 11 .... Estruturas e Uniões....................................158
Capítulo 12 .... Operações com Arquivos ...........................183

1
CAPÍTULO 1 - INTRODUÇÃO À LINGUAGEM C

Um breve histórico das Linguagens de Programação

As linguagens de programação passaram por uma dramática evolução desde que os


primeiros computadores foram desenvolvidos para auxiliar os cálculos de telemetria durante
a segunda guerra mundial. Nos primórdios da computação os programadores usavam a
interface mais primitiva para lidar com a máquina: a linguagem de máquina, uma longa
seqüência de zeros e uns que controlavam diretamente o hardware da máquina. Um pouco
mais tarde foi desenvolvido o assembler para mapear instruções de máquina em uma forma
mais compreensível e de mais fácil memorização para humanos, tais como MOV e ADD.

Na seqüência do desenvolvimento surgiram as primeiras linguagens de alto nível tais


como BASIC e COBOL. Elas permitiram aos programadores trabalhar com instruções
próximas a palavras e frases tais como: faça I = 100. Estas instruções eram traduzidas
novamente para linguagem de máquina por interpretadores ou compiladores.

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.

Estas prioridades se alteraram à medida em que os computadores tornaram-se menores,


mais rápidos e mais baratos e o custo da memória caiu. Nos dias de hoje, o custo dos
programadores supera em muito o custo da maioria dos computadores usados na indústria
e no comércio. Nesse sentido, o conceito de programa “bem escrito” moveu-se para
programas fáceis de manter, isto é, programas fáceis de alterar ou expandir.

Resolvendo Problemas

Os problemas que os programadores de computadores vêm sendo chamados a


resolver vêm mudando com o tempo. Há 20 anos atrás os programas eram criados para
processar uma grande quantidade de dados numéricos. As pessoas que escreviam os
programas e os usuários eram todos profissionais de computação ou engenheiros. As
entradas de dados para os programas eram, freqüentemente, arquivos contendo a
descrição do problema e alguns comandos esotéricos que selecionavam o processamento a
ser aplicado aos dados. Nos dias de hoje, os programas de computador são usados por um
número muito maior de pessoas, algumas das quais com pouco ou nenhum entendimento

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.

Linguagens procedurais ou estruturadas

O principal conceito na programação estruturada é a técnica de “dividir para


conquistar”. Pode-se pensar em um programa de computador como sendo constituído por
uma série de tarefas. Qualquer tarefa que seja muito complexa para ser resolvida pode ser
quebrada em um conjunto de tarefas menores até que cada uma das tarefas seja pequena e
simples o suficiente para que ela possa ser compreendida e resolvida. Tomemos como
exemplo o cálculo do salário médio em uma companhia. Suponha que esta seja uma tarefa
por demais complexa para resolver. No entanto, ela pode ser quebrada nas seguintes sub-
tarefas:

1. Descubra quanto ganha cada pessoa


2. Conte quantos empregados você tem
3. Totalize os salários
4. Divida o total pelo número de empregados que você tem.

Totalizar os salários, por sua vez, pode ser dividido em:

1. Acesse o registro de cada empregado


2. Leia o campo salário
3. Some o salário ao total até o momento
4. Acesse o registro do próximo empregado

Acessar o registro de cada empregado pode, por sua vez, ser quebrado em:

1. Abra o arquivo de empregados


2. Vá para o registro correto
3. Leia os dados do disco.

A programação estruturada continua sendo um sucesso até os dias de hoje para


resolver problemas complexos. No entanto, no final da década de 80, algumas de suas
limitações tinham já tornado-se evidentes. Em primeiro lugar, a separação entre os dados e
as funções que manipulam os dados torna difícil a compreensão e a manutenção do
programa. Além disso, os programadores, freqüentemente, têm de inventar novas soluções
para velhos problemas. A busca de soluções para estas limitações levou ao

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.

Apesar de ser uma linguagem estruturada e modular, C é uma linguagem relativamente


de “baixo nível”, isto é, ela possibilita operações com os dados, normalmente só
disponíveis em assembler, permitindo assim que o programador “auxilie” o compilador na
tarefa de gerar códigos bastante otimizados.

As principais características da linguagem são: alto grau de portabilidade, expressões


compactas, um grande conjunto de operadores, poderosas estruturas de dados e
mecanismos de controle de fluxo bastante eficientes.

Um exemplo:

Seguindo a tradição. o seu primeiro programa em C será o “Alô mundo!” encontrado


na referência clássica de C “The C programming Language” de Kernighan and Ritchie.

Digite o seguinte programa:

#include <stdio.h>
main() /* primeiro programa */
{
printf("Alo mundo!");
}

Saída:

Alo mundo!

Análise:

Este programa é composto de uma única função chamada main( ). A função


main( ) é o módulo principal do programa e é a primeira a ser chamada no início da
execução do mesmo. Por este motivo, a função main( ) deve estar obrigatoriamente
presente em algum lugar do seu programa.
4
Os parênteses após o nome indicam que main( ) é uma função. Toda função em C
deve ser iniciada por uma chave de abertura ({) e finalizada por uma chave de fechamento
(}). As chaves em C são semelhantes ao par begin-end do Pascal. No interior das
chaves encontram-se as instruções da função.

As instruções são executadas na ordem em que as escrevemos. As instruções em C são


sempre encerradas por um ponto e vírgula (;). No nosso exemplo, a função main( )
contém somente uma instrução que é a chamada a uma outra função: printf( ).

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.

Os delimitadores /* e */ identificam começo e fim de comentário. Todo texto que


estiver entre eles é ignorado pelo compilador C. Este tipo de comentário é chamado de
comentário estilo C. A todo /* deve existir um */ correspondente que encerra o
comentário. Um outro tipo de comentário também usado em C é o comentário estilo C++
composto por uma barra dupla (//). A barra dupla instrui o compilador a ignorar tudo que
se segue até o final da linha. Os comentários estilo C++ são usados também em C, uma
vez que eles são aceitos pela maioria dos compiladores atuais. No entanto, eles não são
parte da definição oficial da linguagem.

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

//Meu primeiro programa


#include <stdio.h>
main()
{
printf("Alo mundo!");
5
Saída:

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!");
}

Com o tempo você desenvolverá o seu próprio estilo de programação.

Os elementos básicos em programação

O objetivo da maioria dos programas é resolver um problema. Programas resolvem


problemas manipulando informações ou dados. De alguma forma tem de ser possível:

• Trazer as informações a serem processadas para dentro do programa;

• Armazená-las em algum lugar;

• Dizer ao computador o que fazer com os dados;

• Apresentar os resultados em algum lugar para o usuário.

As suas instruções ao computador podem ser organizadas de forma que:

• Algumas são executadas somente quando uma condição é verdadeira;

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.

Nós acabamos de descrever os elementos básicos em programação: entrada de dados,


variáveis, operações, saída de dados, desvio condicional, laços e funções.

A maioria das linguagens de programação incorpora todas essas características. Algumas,


como C, têm outras características adicionais. No entanto, quando você deseja aprender
uma nova linguagem rapidamente, em geral, é suficiente aprender como a linguagem
implementa os elementos acima e depois evoluir a partir deste conjunto básico.

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( )

No capítulo anterior, você observou um exemplo de utilização da função printf( ).


O propósito de printf( ) é permitir que os programas construídos por você escrevam
resultados na tela do computador. Seu formato é ao mesmo tempo simples e flexível:

printf("expressão de controle", arg1, arg2, ...);

Um exemplo:

main() {
printf("Este e' o Capitulo %d (dois)", 2);
}

Saída:

Este e' o Capitulo 2 (dois)

A expressão de controle:

A expressão de controle consiste de uma cadeia de caracteres delimitada por aspas


("como agora"). O objetivo da função printf( ) é escrever a expressão de controle na
tela do computador. Antes porém, ela substitui os códigos de formatação (iniciados por %)
pelos parâmetros arg1, arg2, etc., na ordem em que eles aparecem. No exemplo
anterior, o código de formatação %d solicita à printf( ) imprimir o primeiro argumento
(o número dois) em formato decimal.

Deve haver exatamente um argumento para cada código de formatação existente na


expressão de controle. Os argumentos podem ser variáveis, constantes, expressões,
chamadas de funções, ou qualquer coisa que forneça um valor compatível com o código de
formatação correspondente.

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:

%u (inteiro decimal sem sinal)


%c (caracter simples)
%d (inteiro decimal com sinal)
%e (real em notação científica)
%f (real em ponto flutuante)
%s (cadeia de caracteres)
%x (inteiro em base hexadecimal)
%o (inteiro em base octal)

Imprimindo cadeia de caracteres

Vamos estudar mais um dos códigos de formatação através do seguinte exemplo:

main()
{
printf("%s | Nucleo\n", "NCE");
printf(" | de Computacao\n | Eletronica");
}

Saída:

NCE | Nucleo
| de Computacao
| Eletronica

Análise:

O \n não é um código de formatação. Ele é um caracter especial que informa à


printf( ) que o restante da impressão deve ser feito em nova linha.

Observe no exemplo anterior:

• Que se não houver nenhum código de formatação na expressão de controle, nenhum


argumento é passado à função printf( ), além da própria expressão de controle.

• Que a função printf( ) não muda de linha automaticamente ao final da impressão da


expressão de controle. Se você quiser mudar de linha deve inserir explicitamente um
caracter \n.

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:

\n nova linha (LF - 0x0a)


\r retorno de carro (CR - 0x0d)
\t tabulação (TAB - 0x09)
\b back space (BS - 0x08)
\f pula página (FF - 0x0c)
\0 caracter nulo
\xhh insere o caracter representado pelo código ASCII hh, onde hh representa o
código do caracter em notação hexadecimal
\nnn representação de um byte em base octal

Formatação dos resultados de saída

Muitas vezes desejamos apresentar os resultados de uma forma organizada em colunas,


com títulos e legendas. Para isto é necessário definir o tamanho dos campos onde os dados
serão escritos. Este parâmetro adicional pode ser usado juntamente com o código de
formatação de cada argumento a ser impresso, e diz quantos caracteres devem ser
reservados para a impressão deste dado.

No caso geral temos:

%[-][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.

No caso geral, os valores são escritos em um campo de largura mínima [tamanho]


alinhados pela direita, isto é, precedidos de um número suficiente de brancos.

Por exemplo, a seqüência de comandos

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);

deve gerar uma tela de saída com o seguinte formato:

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

O sinal de menos [-] precedendo a especificação do tamanho do campo justifica os campos


à esquerda, como mostra o próximo exemplo:

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

Além de especificar o tamanho do campo, podemos preencher o campo com zeros à


esquerda. Para isto, devemos especificar o parâmetro [tamanho] precedido de um zero.
Observe o exemplo a seguir:

11
printf("1234567890");
printf("\n%04d", 21);
printf("\n%06d", 21);

Saída:

1234567890
0021
000021

Outras Funções de Saída: puts( ) e putchar( )

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.

A função puts( ) <STDIO.H>

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:

int puts(const char *s);

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");

A função putchar( ) <STDIO.H>

putchar() é uma macro que escreve um caracter em stdout.

Declaração:

int putchar(int ch);

Valor de Retorno:

Em caso de sucesso,
putchar() retorna o caracter ch
Em caso de erro,
putchar() retorna EOF.

As duas instruções seguintes são equivalentes:

putchar('c');
printf("%c", 'c');

Você deve estar se perguntando porque usar puts( ) ou putchar( ) ao invés de


printf( ). Uma boa razão é que a rotina printf( ) é muito grande. Dessa forma, a
menos que você realmente precise de printf( ) (para saídas numéricas por exemplo),
você pode ter um programa muito menor e de execução muito mais rápida usando
puts( ) e putchar( ).

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")

Ponteiros não contêm informação propriamente dita. Eles contêm o endereço de


memória onde está armazenada a informação de interesse do programa.

As informações manipuladas pelo programa, também chamadas de dados, podem ser


constantes ou variáveis, como veremos a seguir.

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.

printf("Este e o Capitulo %d (dois)", 2);


printf("%s | Nucleo\n", "NCE");

Os tipos de constantes em C são:

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)

b) Constantes cadeias de caracteres


Além de caracteres isolados podemos representar cadeias de caracteres como uma
seqüência de zero ou mais caracteres entre aspas. Exemplos:

"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.

As declarações seguintes são equivalentes:

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

d) Constantes inteiras longas

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:

// Constantes decimais longas


10L
79L

// Constantes octais longas


012L
0115L

// Constantes hexadecimais longas


0xaL ou 0xAL
0x74fL ou 0x4FL

g) Constantes em ponto flutuante


Constantes com ponto decimal, precedidas ou não do sinal negativo, e seguidas ou não
da notação exponencial.

3.1415926
3.14e0
-10.0e-02
100.0F
100.0L

Uma constante em ponto flutuante sem os sufixos L, l, F ou f tem o tipo double (8


bytes). Se o sufixo for F ou f, a constante é do tipo float (4 bytes). Se o sufixo for L
ou l a constante é do tipo long double (10 bytes no compilador Borland C, 8 bytes
no compilador gcc em ambiente UNIX).

Constantes Enumeradas

As constantes enumeradas permitem a criação de novos tipos e a declaração de variáveis


pertencentes a estes tipos cujos valores estão restritos a um conjunto de valores possíveis.
Pode-se por exemplo declarar COR como uma enumeração e definir que COR pode
assumir 5 valores diferentes: VERMELHO, AZUL, VERDE, BRANCO e PRETO.

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:

enum COR {VERMELHO, AZUL, VERDE, BRANCO, PRETO}

A declaração acima tem dois efeitos:

• Ela cria um novo tipo, COR, que consiste em uma enumeração.

• 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:

enum COR {VERMELHO=100, AZUL, VERDE=500, BRANCO, PRETO=700


};

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;

4. puts("Que dia voce quer sua folga (0-6)?");


5. puts("0 - Segunda");
6. puts("1 - Terca");
7. puts("2 - Quarta");
8. puts("3 - Quinta");
9. puts("4 - Sexta");
10. puts("5 - Sabado");
11. puts("6 - Domingo");
12. printf("Sua escolha: ");
13. scanf("%d", &Folga);

14. if (Folga==Sab || Folga==Dom)


15. puts("Voce nao trabalha nos fins de semana!");
16. else
17. puts("Ok, vou anotar sua folga.");
18.}

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:

Este e' o numero dois: 2


e este e' o numero tres: 3
18
Análise:

A declaração na linha 2 é um exemplo de declaração de variável. Ela instrui o compilador a


reservar espaço em memória, suficiente para armazenar uma variável do tipo inteiro. Além
disso, qualquer referência futura à variável cout, no corpo do programa, acessará esta
mesma posição de memória.
Em C todas as variáveis devem ser declaradas. Se você tiver mais de uma variável do
mesmo tipo, poderá declará-las de uma única vez, separando o seu nome por vírgulas.
Exemplo:

int bananas, laranjas, peras;

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.

Variáveis ponto flutuante

Observe o seguinte exemplo:

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.

Os números em ponto flutuante são representados internamente na forma:

[+/-] [mantissa] E [expoente],

e ocupam 4 bytes de memória no compilador Borland C: 1 para o expoente e 3 para o sinal


e mantissa. Existe ainda um tipo estendido de variáveis em ponto flutuante, o tipo double
que, como o nome indica, ocupa o dobro de bytes na memória. Isto significa que a mantissa
possui mais algarismos significativos do que no tipo float e que o expoente pode variar numa
faixa maior.

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)

Variáveis do tipo char

Ocupam 1 byte na memória. Assim como as constantes caracteres, as variáveis do tipo


char também podem participar normalmente de expressões aritméticas. O valor
armazenado no byte de memória corresponde ao código ASCII do caracter representado.
Assim, as atribuições abaixo são todas equivalentes:

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:

Na linha 4, quando o valor 65000 é atribuído à variável i, o compilador interpreta este


valor como um número negativo. (65000 é a representação em complemento a 2 de -536)

Ponteiros e Cadeia de caracteres

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];

Esta declaração aloca 80 bytes consecutivos de memória para armazenar a cadeia de


caracteres. Cada byte armazena o código ASCII de um caracter da cadeia.
A segunda forma é declarar um ponteiro para uma área de memória onde serão
armazenados os caracteres da string.

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
(_).

b) O restante do identificador pode conter letras, underscores ou dígitos.

c) Em C, letras maiúsculas e minúsculas são diferentes. Por exemplo, os identificadores


count, Count e COUNT, são tratadas como variáveis distintas pelo compilador.

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

As expressões combinam operandos e operadores para produzir um único resultado.

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

As expressões são avaliadas de acordo com o que se chama de precedência dos


operadores. A introdução de operadores com precedências distintas altera a ordem de
avaliação da expressão, sendo que os de maior precedência serão avaliados antes dos de
menor.

Exemplo:

A+ B
1 *3
2 C −D
1424 1
3
1442 244 3
3

É a seguinte a precedência dos operadores em C:

Os operadores unários (categoria #2), condicionais (categoria #14), e de atribuição


(categoria #15) associam os operandos da direita para a esquerda; todos os demais
operadores associam os operandos da esquerda para a direita.

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

2. Unários ! Negação Lógica (NOT)


~ Negação bit a bit
++ Pré-incremento ou Pós-incremento
-- Pré-decremento ou Pós-decremento
& Endereço
* Indireção
sizeof tamanho do operando em bytes

4. Multiplicativos * Multiplicação
/ Divisão
% Resto da divisão inteira

5. Aditivos + Soma binária


- Subtração binária

6. Deslocamento << Deslocamento à esquerda


>> Deslocamento à direita

7. Relacionais < Menor do que


<= Menor ou igual
> Maior do que
>= Maior ou igual

8. Igualdade == Igual a
!= Diferente

9. & AND bit a bit

10. ^ XOR bit a bit

11. | OR bit a bit

12. && AND lógico

25
13. || OR lógico

14. Condicional ?: a ? x : y (se a então x, senão y)

15. Atribuição = Atribuição simples


*= Produto e atribuição
/= Divisão e atribuição
%= Resto da divisão inteira e atribuição
+= Soma e atribuição
−= Subtração e atribuição
&= AND bit a bit e atribuição
∧= XOR bit a bit e atribuição
|= OR bit a bit e atribuição
<<= Deslocamento à esquerda e atribuição
>>= Deslocamento à direita e atribuição

16. Vírgula , Conectivo de expressões

Os operadores

O operador de atribuição:

O operador mais básico em C é o operador de atribuição:

soma = a + b;
fator = 3.0;

o valor da expressão à direita é atribuído à variável à esquerda.

Em C pode-se fazer várias atribuições em uma única linha de código:

a = b = c = 3.0;

após a atribuição, as três variáveis teriam o mesmo valor (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;

printf("Soma = %d\n", i+j);


}

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);

é uma construção válida, equivalente a:

b = 7/2;
a = 3+2*b;

Operadores de incremento e decremento:

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”.

Os operadores de incremento/decremento podem aparecer sozinhos em uma instrução:

i++;
++i;

Nesse caso é indiferente se o operador é colocado antes ou depois do operando. As


instruções anteriores são equivalentes a:

i = i + 1;

Operadores em bits:

Para operações ao nível de bits, C dispõe dos seguintes operadores:

& (AND lógico)


uso: op1 & op2;
descrição: é feito um AND lógico dos bits correspondentes de op1 e op2 usando a
seguinte tabela verdade:

op1 op2 &


0 0 0
0 1 0
1 0 0
1 1 1

exemplo: 0x32a & 0xa3b4 = 0x320

| (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

exemplo: 0x32a | 0xa3b4 = 0xa3be

∧ (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

exemplo: 0x32a ∧ 0xa3b4 = 0xa09e

∼ (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

exemplo: ∼0x32a = 0xfcd5

< < (deslocamento à esquerda)


uso: op1 << k;
descrição: os bits de op1 são deslocados k bits à esquerda.
exemplo: 0x0010 << 2 = 0x0040

> > (deslocamento à direita)


uso: op1 >> k;
descrição: os bits de op1 são deslocados k bits à direita.
29
exemplo: 0x0010 >> 2 = 0x0004

Observações:

a) Nos deslocamentos à direita em variáveis unsigned e nos deslocamentos à esquerda, os


últimos bits contrários ao deslocamento são preenchidos com zeros.

b) Nos deslocamentos à direita em variáveis signed, os últimos bits contrários ao


deslocamento são preenchidos com o valor do bit mais significativo da variável.

c) Um deslocamento de um bit à direita é equivalente a uma divisão por 2. Da mesma


forma, deslocar um bit à esquerda é equivalente a uma multiplicação por 2. Assim, as
instruções seguintes são equivalentes:

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.

d) Os operadores de deslocamento de bits não têm sentido se aplicados em operandos do


tipo float ou double.

Operadores de atribuição composta:

Em C, qualquer expressão da forma:

<variável> = <variável> <operando> <expressão>;

pode ser compactada na forma:

<variável> <operando>= <expressão>;

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;

Expressões usando operadores de atribuição composta são mais compactas e,


normalmente, geram um código mais eficiente.

Operadores de endereço:

C suporta dois operadores que lidam com endereços: o operador devolve endereço (&), e
o operador de acesso indireto (*).

A expressão &variável retorna o endereço do primeiro byte onde variável está


armazenada.

O programa seguinte imprime o conteúdo e o endereço da variável inteira x.

main() {
int x=2;

printf("valor = %d\nendereco = %u", x, &x);


}

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.

b) A conversão de tipos obedece à seguinte precedência entre os operandos:

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.

Conversão forçada de tipos

É possível em C forçar a mudança do tipo de uma expressão ou variável. Para isto, a


expressão (variável) é precedida pelo tipo desejado, escrito entre parênteses.

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

Até agora, em todos os nossos exemplos, as variáveis foram inicializadas no próprio


programa. Neste capítulo, iremos estudar algumas formas de entrada de dados a partir do
teclado.

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 é:

scanf("expressão de controle", endereço1, endereço2, ...);

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;

5. scanf("%d %d", &a, &b);


6. soma = a + b;
7. printf("Soma = %d\n", soma);
8. }

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:

scanf("%d,%d", &a, &b);

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:

scanf(“%10.5f”, &f); // errado

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.

Lendo strings com a função scanf( )

Vamos estudar a leitura de strings usando scanf( ) através de um exemplo:

main() {
char nome[30];

printf("Digite o seu nome : ");


scanf("%s", nome);
printf("Como vai %s?\n", nome);
}

Saída:

Digite o seu nome : Carlos da Silva


Como vai Carlos?

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( )

Vamos reescrever o exemplo anterior usando gets( ):

36
main() {
char nome[30];

printf("Digite o seu nome : ");


gets(nome);
printf("Como vai %s?\n", nome);
}

Saída:

Digite o seu nome : Carlos da Silva


Como vai Carlos da Silva?

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( )

A biblioteca C dispõe de funções dedicadas à leitura de caracteres. Estas funções, menores


e mais rápidas do que scanf( ) são também de mais fácil utilização. Uma das funções
pertencentes a esta classe é a função getchar( )2.
A seqüência de caracteres digitados pelo usuário é bufferizada (a entrada de dados
termina quando o caracter ENTER é pressionado) e, a cada chamada de getchar( ), o
próximo caracter no buffer é lido (incluindo o ENTER ou \n). Em caso de sucesso
getchar( ) retorna o caracter lido. Em caso de erro getchar( ) retorna EOF (caracter End
Of File).
A biblioteca C dispõe de outras funções (getch( ) e getche( ), por exemplo) que lêem um
caracter no instante em que ele é digitado, sem a necessidade do ENTER para concluir a
leitura 3. Estas funções por não estarem disponíveis em todas as versões dos compiladores
C, não serão vistas aqui.

Exemplo de utilização de getchar( ):

main() {
char ch;

printf("Digite algum caracter : ");


ch = getchar();

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:

Digite algum caracter : x


O caracter que voce digitou foi o x.

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

As instruções de desvio condicional permitem que o programa execute diferentes


procedimentos baseado em decisões do tipo: “se uma dada condição é verdadeira, faço
isso; senão faço aquilo”. Este tipo de decisão está presente no nosso dia a dia (“Se chover,
vou de carro; senão, vou de ônibus”, etc.) e, por isso, todas as linguagens de programação
incorporam instruções de desvio condicional.

Operadores relacionais e operadores lógicos

Existem duas classes de operadores sobre as quais ainda não falamos: os operadores
relacionais e os operadores lógicos.

Operadores relacionais

Os operadores relacionais em C são:

> maior do que


>= maior ou igual a
< menor do que
<= menor ou igual a
== igual a
!= diferente de

Operadores relacionais permitem que se compare dois valores e obtenha-se um resultado


que é dependente da comparação ser falsa ou verdadeira. Se a comparação é falsa, o
resultado obtido é 0. Se a comparação é verdadeira, o resultado obtido é 1.

O programa a seguir mostra expressões booleanas como argumentos da função printf( ):

1. main() {
2. int capitulo=6;

3. printf("Este e' o capitulo 5? %d\n", capitulo==5);


4. printf("Este e' o capitulo 6? %d\n", capitulo==6);
5. }

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( ).

Um dos erros mais freqüentes entre iniciantes em programação C é o de confundir o


operador de atribuição (=) com o operador relacional (==). Observe que, se tivéssemos
escrito:

printf("Este e' o capitulo 5? %d\n", capitulo=5);

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:

Este e' o capitulo 5? 5

Operadores lógicos

Os operadores lógicos em C são:

&& operador lógico E


|| operador lógico OU
! operador lógico NOT

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.

Se exp1 e exp2 são duas expressões simples, então:

exp1 && exp2 é verdadeira se ambas as expressões são verdadeiras.


exp1 || exp2 é verdadeira se pelo menos uma das expressões for verdadeira.
!exp1 é verdadeira se exp1 for falsa.

Exemplos:

Seja a == 3,
(a>0) && (a<=4) ⇒ Verdadeiro
(a>0) || (a==-3) ⇒ Verdadeiro

40
!(a==5) ⇒ Verdadeiro

Em C, uma expressão é verdadeira se, quando avaliada, produzir um resultado diferente de


zero. Uma expressão é falsa somente quando produzir um resultado igual a zero.

Exemplo: As seguintes construções são corretas e produzem os resultados assinalados.

1||2 verdadeiro (1)


!5 falso (0)

O COMANDO if

Um desvio condicional é usado quando se deseja escolher um dentre dois comandos ou


blocos de comandos para ser executado, dependendo de uma condição do programa.

V F
CONDIÇÃO

COMANDO 1 COMANDO 2
(BLOCO 1) (BLOCO 2)

Figura 6.1: Fluxograma de Desvio Condicional

Em C existem 2 formas para o desvio condicional, que são:

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;

A condição é avaliada. Se o seu valor é verdadeiro, comando1 é executado, senão


comando2 é executado. O processamento continua no comando seguinte ao if.

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;

printf("Entre com o valor de A : ");


scanf("%d", &A);
if (A>0)
printf("A e' maior que zero\n");
}

Caso mais de uma ação tenha que ser executada, deve-se utilizar o bloco de comandos
como mostra o seguinte exemplo:

if (salario > 10000) {


inps = salario * 0.10;
irenda = salario * 0.15;
}

O exemplo a seguir ilustra a forma do "if-else".


Pseudocódigo:

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;

printf("Entre com o valor de A : ");


scanf("%d", &a);
if (a>0)
printf("A e' maior que zero\n");
else
printf("A e' menor ou igual a zero\n");
}

Como vimos anteriormente, em C uma expressão é verdadeira quando o seu valor é


diferente de zero. Usando essa característica, podemos excluir em certos casos a
comparação lógica dentro do if. Por exemplo, as duas formas seguintes são equivalentes:

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;

Na primeira forma o comando só é executado se a for igual a 8. Na segunda forma o valor


8 é atribuído à variável a e o comando é sempre executado.

if aninhados
43
Um comando if pode estar dentro de um outro comando if. Dizemos então que o if interno
está aninhado.

Exemplo:

1. if ((ch=getchar()) >= 'a')


2. if (ch <= 'z')
3. puts("Você digitou uma letra");
4. else
5. puts("Você não digitou uma letra");

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.

Problemas de ambigüidade em construções do tipo:

if ((ch=getchar()) >= 'a')


if (ch <= 'z')
puts("Voce digitou uma letra");
else
puts("Voce nao digitou uma letra");

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:

if ((ch=getchar()) >= 'a')


{
if (ch <= 'z')
puts("Voce digitou uma letra");
}
else
puts("Voce nao digitou uma letra");

O comando switch

Todas as seleções de comandos dentro de um programa podem ser escritas através de


comandos if ou if-else.

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;
}

A execução deste comando segue os seguintes passos:

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.

Vamos refazer o programa anterior usando 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;

• A expressão em switch (expressão) deve ter um valor compatível com um


inteiro, isto é, podem ser usadas expressões do tipo char e int com todas as suas
variações. Você não pode usar reais (float e double), ponteiros, strings, ou outras
estruturas de dados.

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");
}

O operador condicional ternário

Seja o seguinte programa para calcular o máximo de dois números:

main() {
int a;
int b;
int max;

printf("Digite dois numeros : ");


scanf("%d%d", &a, &b);
if (a > b)
max = a;
else
max = b;
printf("O maior deles e' o %d\n", 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:

expressão1? expressão2 : expressão3

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”.

O programa anterior poderia ser reescrito na forma:

main() {
int a;
int b;
int max;

printf("Digite dois numeros : ");


scanf("%d%d", &a, &b);
max = (a > b) ? a : b;
printf("O maior deles e' o %d\n", max);
}

ou, ainda melhor:

main() {
int a;
int b;

printf("Digite dois numeros : ");


scanf("%d%d", &a, &b);
printf("O maior deles e' o %d\n", (a>b)?a: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.

O formato do comando while é:

while (condição)
comando;

onde condição tem um valor igual a 0 (falso) ou diferente de 0 (verdadeiro), e


comando pode ser um único comando ou uma seqüência de comandos entre chaves
({...}).

A execução do while segue os seguintes passos:

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;

3. Volta para o passo 1;

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.

Vejamos como exemplo um programa que imprime na tela os números de 1 a 5:

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. }

O laço anterior poderia ser escrito de forma um pouco mais compacta:

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:

Esteja certo de que você entendeu as modificações feitas antes de prosseguir.


A variável de controle counter é inicializada com zero na linha 2. A instrução na linha 3
verifica se o valor de counter é menor do que 5 e em seguida incrementa a variável de
controle de modo que, na primeira passagem do loop, o primeiro valor a ser impresso é o
número 1. O programa prossegue até que counter atinge o valor 5, quando o laço é
encerrado.

Podemos utilizar ainda construções onde não existe nenhum comando associado ao while.
Exemplo:

while ((ch=getchar()) != 'a');

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:

1. #define MAXINT 0x7FFF


2. main() {
3. int small;
4. long large;

5. printf("Entre com um número pequeno: ");


6. scanf("%d", &small);
7. printf("Entre com um número grande: ");
8. scanf("%ld", &large);
9. while (small<large && small<MAXINT) {
10. small++;
11. large-=2;
12. }
13. printf("\nSmall: %d Large: %ld\n",
small, large);
14. }

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

O laço for é encontrado na maioria das linguagens de programação, incluindo C. No


entanto, como iremos ver, a versão C é mais flexível e dispõe de muito mais recursos do
que a implementação das outras linguagens.

A idéia básica do comando for é a de que você execute um conjunto de comandos um


número fixo de vezes enquanto que uma variável (chamada de variável de controle do for)
é incrementada ou decrementada a cada passagem pelo laço. Vamos, por exemplo,
modificar o programa do contador de 1 a 5 de forma a usar 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:

O coração do laço for encontra-se na linha 3. Ali a variável de controle é inicializada e


incrementada e a condição de parada é testada. Os detalhes de implementação do laço for
serão vistos a seguir.

Forma genérica do laço for:

for (expressão1; expressão2; expressão3)


comando;

Observe que dentro dos parênteses existem 3 expressões separadas por ponto e vírgula:

• expressão1 é, normalmente, a inicialização da variável de controle do laço.

• expressão2 é um teste que, enquanto resultar verdadeiro, causa a continuação do


laço.

• expressão3 é, normalmente, o incremento ou decremento da variável de controle do


for.

A forma genérica do laço for é equivalente ao seguinte código:

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;

for (; i <= 5; i++)


printf("%d\n", i);
}

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;

for (i = 1, j = 10; i <= 10; i++, j += 10)


printf("%d %d\n", i, 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.

Pode-se tornar as expressões do for arbitrariamente complexas incluindo, por exemplo, em


expressão1 instruções sem relação com as variáveis do laço ou ainda incluindo em
expressão3 todas as instruções do corpo do for. Isto é citado aqui apenas como
curiosidade e, absolutamente, não constitui boa prática de programação. Exemplo:

main() {
int i;

for (puts("comecou"), i=1; i<=10; printf("%d\n", i),


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;

for (i=1; i<10; i++)


for (j=1; j<10; j++)
printf("%d x %d = %d\n", i, j, i*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);

O comando do...while é semelhante ao comando while. A diferença está no momento de


avaliação da expressão, que sempre ocorre após a execução do comando. Isto faz com
que o comando do laço do...while sempre execute ao menos uma vez.

A execução do laço do...while segue os seguintes passos:

1. Executa comando;

2. Avalia expressão;

3. Se expressão for verdadeira então vai para passo 1;

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. }

que poderia ser reescrito como:

1. main() {
2. int i=1;

3. do
4. printf("%d\n", i);
5. while (++i<=5);
6. puts(acabou);
7. }

Na linha 2 a variável de controle do laço (i) é inicializada. O programa entra no corpo do


laço do...while antes do teste da condição, o que garante a execução dos comandos ao
menos uma vez. Na linha 4 o valor da variável de controle é impresso e, na linha 5, a
condição é testada. Se a condição for verdadeira, a execução do programa retorna para o
início do laço na linha 4, caso contrário o programa prossegue para a linha 6.

OS COMANDOS break, continue e goto

O comando break

O comando break, quando utilizado dentro do bloco de comandos associado a um for, a


um while ou a um do...while, faz com que o laço seja imediatamente interrompido,
transferindo o processamento para o primeiro comando seguinte ao laço. Observe o
programa a seguir:

1. void main() {
2. char ch;
3. int i;

4. for (i=0; i<10; i++) {


5. ch = getchar();
6. getchar();
7. if (ch=='#') break;
8. printf("%c\n", ch);
9. }
10. puts("Acabou");
11.}

55
Análise:

O programa acima lê e imprime um máximo de 10 caracteres digitados pelo teclado. Se um


dos caracteres digitados for o caracter ‘#’, o laço for é terminado e a execução pula para a
instrução seguinte ao for na linha 10. Observe a instrução na linha 6. Como foi visto no
Capítulo 5, a entrada de dados para a função getchar( ) é bufferizada e, por este
motivo, o ENTER digitado pelo usuário depois de cada caracter deve ser retirado do
buffer. Esta é a função da instrução na linha 6; o caracter de nova linha é retirado do buffer
mas não é atribuído a nenhuma variável do programa. Este é um artifício comum quando
fazemos leitura de caracteres via getchar( ) ou scanf( ).

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

O comando continue tem funcionamento semelhante ao comando break. A diferença


reside em que, ao invés de interromper a execução do laço, como no break, o comando
continue pula as instruções que estiverem abaixo, e força a próxima iteração do laço. Nos
laços while e do...while, a execução do programa vai diretamente para o teste condicional.
No caso do laço for, o computador primeiro executa expressão3 (normalmente o
incremento/decremento da variável de controle do laço) e então vai para o teste
condicional. Observe o seguinte exemplo:

void main() {
char ch;
int count=0;

while (count<10) {
ch=getchar();
getchar();
if(ch=='#') continue;
printf("%c\n", ch);
count++;
}
puts("Acabou!");
}

O programa lê e imprime 10 caracteres digitados pelo usuário. Se um dos caracteres


digitados for o ‘#’, ele é ignorado e o processamento continua com a próxima iteração do
laço.

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!");

A execução é desviada para a instrução seguinte ao rótulo.

Formalmente, o comando goto não é necessário e, na prática, sempre é possível escrever


programas sem ele. O seu uso é fortemente desaconselhado uma vez que, através dele,
pode-se criar programas sem qualquer estrutura: pode-se, entre outras coisas, desviar o
fluxo de execução para o interior de laços e para fora destes, usar goto como chamada de
função e chamadas de funções como goto, tornando os programas de difícil manutenção e
leitura. No entanto, em algumas poucas ocasiões o comando goto pode ser útil. Observe o
seguinte exemplo:

while (condição1) {
// comandos.
while (condição2) {
// comandos
while (condição3) {
// comandos
if (desastre)
goto ERRO;
}
}
}
ERRO:
// tratamento de erro

Compare com o mesmo trecho de código escrito sem o uso do goto:


5
57
while (condição1) {
// comandos
while (condição2) {
// comandos
while (condição3) {
// comandos
if (desastre) break;
}
if (desastre) break;
}
if (desastre) break;
}
if (desastre)
// 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?”

Talvez você já tenha tentado e tenha encontrado dificuldades em criar o programa.


Normalmente, a grande dificuldade encontra-se em como começar pois, mesmo para a
criação de um programa simples, existem muitas tarefas a serem executadas.

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.

A solução deste problema envolve três tarefas básicas:

1) “iniciar” o programa e pedir o intervalo onde se encontra o número;

2) “calcular” o valor do meio do intervalo;

3) “conferir” com o jogador se este é o valor correto, ou se está acima ou abaixo do


valor correto.

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

O diagrama de estrutura mostra a divisão de um programa em módulos. Cada módulo deve


executar uma função bem definida. No diagrama acima vemos que o programa
ADIVINHA é composto por 3 módulos (as folhas da árvore) e cada um deles irá executar
uma das tarefas descritas anteriormente.

O diagrama de estrutura, por sua característica gráfica, ajuda bastante na identificação e


visualização dos módulos componentes de um programa.

Após a confecção do diagrama de estrutura, podemos voltar ao nosso problema original já


com uma boa noção de como ele deve ser equacionado.

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);
}

No trecho de programa acima, podemos ver toda a estrutura de funcionamento deste


programa. Procedendo desta forma, podemos conferir se as linha gerais do programa se
encaixam naquilo que desejamos.

Ainda não sabemos como fazer Iniciar(), CalcularValor() e


ConferirValor(), nem como determinar o valor da variável acertou, porém esta é
uma questão para ser resolvida no próximo passo. Tratando cada um deles como um
programa menor, a solução fica muito mais fácil.

Funções

Ainda nos falta um mecanismo em C para definir Iniciar(), CalcularValor() e


ConferirValor().

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?

A solução está no uso de uma construção chamada subrotina, subprograma ou


procedimento. O termo subrotina é o nome que se dá a um módulo de programa que
permite o desenvolvimento do programa por partes.

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.

Na sua forma mais simples, uma função em C tem o seguinte formato:

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);
}

Dentro do corpo de Iniciar(), colocamos todas as tarefas que desejamos executar ao


iniciar o programa. Da mesma maneira, escrevemos os outros procedimentos e compomos
o programa global:

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;

printf("\nMeu chute e': %d\n", num);


printf("Este valor e' maior(>), menor(<) ou igual(=) ao seu? ");
ch = getchar(); getchar();
switch (ch) {
case '>':
sup = num - 1;
break;
case '<':
inf = num + 1;
break;
case '=':
acertou = 1;
}
}

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.

Do mesmo modo que chamamos uma função da biblioteca C - getchar(), scanf(),


printf(), etc. - chamamos nossas próprias funções: Iniciar(),
CalcularValor() e ConferirValor(). Ao encontrar a primeira sentença com o
nome de uma função, no nosso caso Iniciar(), passam a ser executados os comandos
que se encontram dentro da definição desta função.

Ao chegar ao final da função, a execução volta à função main().

A próxima sentença a ser executada é o do...while, onde as funções CalcularValor()


e ConferirValor() deverão ser chamadas até que a variável acertou tenha um valor
diferente de zero.

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;

• Um problema complexo = soma de problemas menos complexos;

• Programa = corpo principal que chama subrotinas;

• A utilização de procedimentos facilita a construção do programa e gera programas mais


simples de serem compreendidos;

• A chamada de uma subrotina ocasiona a execução de um trecho de programa;

• A utilização de procedimentos permite dividir o trabalho de elaboração de um programa


grande entre vários programadores ou grupos de programadores, trabalhando
independentemente sob a coordenação de um analista.

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);

teríamos um erro de compilação, pois main() não conhece a variável 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.

Vamos exemplificar o conceito de variáveis locais através de mais um exemplo. Seja o


seguinte programa que converte temperaturas em graus Fahrenheit para graus Celsius:

float Far;

Convert() {
float Cel;
Cel = ((Far - 32) * 5) / 9;
printf(“Em Convert. Temperatura em Celsius: %f\n”,
Cel);
}

main() {
float Cel=0.0;

printf("Temperatura em Fahrenheit: ");


scanf("%f", &Far);
Convert();
printf(“Em main. Temperatura em Celsius: %f\n”, Cel);
}

Saída:

Temperatura em Fahrenheit: 212


Em Convert. Temperatura em Celsius: 100.00000
Em main. Temperatura em Celsius: 0.000000

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:

Temperatura em Fahrenheit: 212


Em Convert. Temperatura em Celsius: 100.00000
Em main. Temperatura em Celsius: 100.000000

Análise:

Observe os valores impressos para a temperatura em graus Celsius. Eles correspondem ao


valor da variável Cel a qual, por ser uma variável global, é comum a ambas as funções:
main() e Convert().

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;

Cel = ((Far - 32) * 5) / 9;


printf(“Em Convert. Temperatura em Celsius: %f\n”,
Cel);
}

main() {
printf("Temperatura em Fahrenheit: ");
scanf("%f", &Far);
Convert();
printf(“Em main. Temperatura em Celsius: %f\n”, Cel);
}

Saída:

Temperatura em Fahrenheit: 212


Em Convert. Temperatura em Celsius: 100.00000
Em main. Temperatura em Celsius: 0.000000

Análise:

A declaração de uma variável local Cel:Convert torna impossível à função Convert()


qualquer referência à variável global Cel. A variável global foi mascarada pelo uso do
mesmo nome para variáveis locais e globais.

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.

Funções que retornam um valor

Nós já vimos funções da biblioteca C que retornavam um valor. Por exemplo:

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 tem duas ações:

• Se houver alguma expressão após o return, o valor da expressão é atribuído à função.


Ou, em outras palavras, a função passa a ter o valor da expressão.

• Em seguida a função é interrompida no ponto do return e o fluxo volta ao módulo que


chamou a função que estava sendo processada.

O comando return pode retornar somente um único valor à função que chama.

Exemplo: No programa de adivinhação, vamos modificar a rotina ConferirValor() de


modo que ela retorne o valor de acertou.

ConferirValor() {
char ch;

printf("\nMeu chute e': %d\n", num);


puts("Este valor e' maior(>), menor(<) ou igual(=) ao seu?");
ch = getchar();
switch (ch) {
case '>':
sup = num - 1;
return 0;
case '<':
inf = num + 1;;
return 0;
case '=':
return 1;
}
}

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().

Vamos transcrever novamente o programa adivinha, agora com as modificações acima:

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;

printf("\nMeu chute e': %d\n", num);


puts("Este valor e' maior(>), menor(<) ou igual(=) ao seu?");
ch = getchar(); getchar();
switch (ch) {
case '>':
sup = num - 1;
return 0;
case '<':
inf = num + 1;;
return 0;
case '=':
return 1;
}
}

main() {
int acertou;

Iniciar();
do {
CalcularValor();
acertou = ConferirValor();
}

69
while (!acertou);
}

Passando dados para a função chamada

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().

Vamos estudar a passagem de parâmetros, novamente através do programa de


adivinhação. Vamos supor que a função CalcularValor(), não mais acesse as
variáveis externas inf e sup, mas que agora as receba como parâmetros. A chamada à
função, em main(), ficaria:

CalcularValor(inf, sup);

e a definição da função passaria a:

CalcularValor(int baixo, int alto) {


num = (baixo + alto) >> 1; // divisao por 2. Lembra?
}

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.

As variáveis declaradas no cabeçalho da função (baixo e alto no nosso exemplo) são


chamadas de parâmetros formais da função, enquanto que as expressões usadas na
chamada à função (as variáveis inf e sup) são chamadas de parâmetros reais ou
argumentos.

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
}

compare com CalcularValor().

Mais modificações no programa exemplo

Vamos, a título de exercício, ver algumas outras modificações que poderiam ser
introduzidas no programa exemplo.

Em primeiro lugar, a função CalcularValor() poderia usar o comando return para


devolver o valor de num. A função main() ficaria então:

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.

Passagem de parâmetros por valor

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;

for (p=1; n>0; --n)


p *= x;
return p;
}

cuja chamada seria, por exemplo:

fator = potencia(base, expoente);

A passagem de parâmetros entre funções se dá através de uma estrutura de dados


conhecida por pilha, onde os dados são literalmente empilhados. O topo da pilha é
marcado por um apontador chamado de stack pointer (SP). Supondo, que no momento
da chamada base = 2 e expoente = 5, vamos acompanhar, de forma simplificada, a
evolução da pilha.

Antes da chamada da função potencia():

... SP

Na chamada da função potencia():

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
...

Ao término do processamento de potencia():

p:potencia 32
n:potencia 5
x:potencia 2
... SP

Observe:

a) que na chamada da função, são empilhados os VALORES de base e expoente, daí


o nome CHAMADA POR VALOR. A função potencia() trabalha sobre os dados
na pilha, e não sobre as variáveis base e expoente que estão armazenadas em algum
outro lugar na memória.

b) que o espaço para a variável automática p é alocado também na pilha.

c) que ao término do processamento de potência o stack pointer volta a posição original,


liberando com isso o espaço ocupado pelos parâmetros e pelas variáveis automáticas
para uso posterior na chamada a outras funções.

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.

A segunda conclusão, tirada das observações “b” e “c”, é a de que as variáveis


automáticas existem apenas durante a execução da função onde elas estão declaradas e são
destruídas na saída. Por esse motivo, elas não podem reter seus valores de uma ativação a
outra da função, e devem ser inicializadas a cada ativação da mesma.

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.

swap (int x, int y) {


int temp;

printf("Swap. Antes do swap. x : %d, y : %d\n", x,


y);
temp = x;
x = y;
y = temp;
printf("Swap. Depois do swap. x : %d, y : %d\n", x, y);
}

main() {
int x = 5, y = 10;

printf("Main. Antes do swap. x : %d, y : %d\n", x,


y);
swap(x,y);
printf("Main. Depois do swap. x : %d, y : %d\n", x,
y);
}

Saída:

Main. Antes do swap. x : 5, y : 10


Swap. Antes do swap. x : 5, y : 10
Swap. Depois do swap. x : 10, y : 5
Main. Depois do swap. x : 5, y : 10

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

e) Retorno da função swap

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.

Passagem de parâmetros por referência

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 (&).

Nós já lidamos com esse caso quando usamos a função scanf():

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.

Funções não inteiras

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.

A declaração de uma função é semelhante à declaração de uma variável e segue as mesmas


regras de escopo, isto é, uma declaração pode ser global ou local.

Vamos estudar um exemplo. Seja o programa que transforma temperaturas Fahrenheit para
Celsius.

float Convert(float Fer); // declaração da função

main() {
float Far, Cel;

printf("Temperature in Fahrenheit: ");


scanf("%f", &Far);
Cel=Convert(Far);
printf("Celsius: %f\n", Cel);
}

float Convert(float Fer) { // definição da função


float TCel;

TCel = ((Fer - 32) * 5) / 9;


return TCel;
}

A declaração de uma função é também chamada de protótipo da função. O protótipo da


função tem como objetivo informar ao compilador:

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.

Obs: área do triângulo = base * altura/2

1. float area(float, float); /* protótipo da função */

2. main() {
3. float s, b, h;

4. printf(“Entre com a base e a altura: ”);


5. scanf(“%f%f”, &b, &h);
6. s = area(b, h); // chamada da função
7. printf(“Area = %.2f\n”, s);
8. }

9. float area(float base, float altura) {


10. return base*altura/2.0;
11.}

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:

float area(float base, float altura);

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.

Podemos agora apresentar a forma genérica da definição de uma função:

tipo nome(parâmetros) {
variáveis internas da função; /* se houver alguma */

corpo da função;
}

compare com a função area().

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;

printf(“Entre com a base e a altura: ”);


scanf(“%f%f”, &b, &h);
area(b, h); // chamada da função
printf(“Area = %.2f\n”, s);
}

void area(float base, float altura) {


s = base*altura/2.0;
}

A vantagem de se declarar funções void é a de que o compilador acusa um erro se


tentarmos retornar um valor em funções desse tipo.

C não permite a construção de funções aninhadas, isto é, funções dentro de funções.

Classes de armazenamento

As variáveis em C dividem-se, quanto à classe de armazenamento em quatro grupos:

• variáveis automáticas;

80
• variáveis externas ou globais;

• variáveis estáticas;

• variáveis registrador;

Estudaremos cada um dos grupos com mais detalhes.

Variáveis automáticas

Já vimos variáveis automáticas em nossos exemplos anteriores. Suas características são:

• declaradas no interior de uma função(bloco);

• criadas na pilha;

• existem somente durante a execução da(o) função(bloco) onde elas foram declaradas;

• são visíveis, ou acessíveis, somente no interior da(o) função(bloco) onde 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;

printf("Entre com dois numeros: ");


scanf("%f%f", &a, &b);
if (a>b) {
float aux;
aux=a;
a=b;
b=aux;
}
printf("Valores ordenados: %g, %g\n", 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:

• declaradas fora de qualquer módulo do programa fonte;

• mantêm sua posição de memória alocada durante toda a execução do programa.

• o seu valor é acessível a todas as funções existentes desde o ponto de declaração da


variável, até o fim do programa.

Vimos que variáveis externas são uma alternativa para a troca de informações entre funções
num mesmo arquivo.

As funções e variáveis que compõem um programa C não precisam ser compiladas ao


mesmo tempo. O programa fonte pode estar dividido em vários arquivos e, assim, variáveis
externas servem também, para trocar informações entre funções definidas em arquivos
diferentes. Vejamos um exemplo:

Arquivo 1 (arq1.c)

float base, altura;


extern float area(void); // definida no Arquivo 2

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:

gcc -o area arq1.c arq2.c

82
b) Ambiente DOS usando o compilador Borland C++:

É necessário criar um arquivo project contendo os arquivos componentes do


programa.

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.

Variáveis locais estáticas:

• Declaradas no interior de um bloco;

• Não mais criadas na pilha. Mantêm a sua posição de memória alocada durante toda a
execução do programa.

• Retêm seus valores durante toda a execução do programa;

• São visíveis somente no interior do bloco onde foram declaradas;

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.

Somente podem ser declaradas com o prefixo register variáveis automáticas e


parâmetros formais do tipo int ou char.

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;

for (i=1; i<2000; i++)


for (j=1; j<2000; j++);
}

Observação: No compilador C da Borland o uso de variáveis do tipo registrador é


controlado através de uma opção de compilação: Options, Compiler, Optimization,
Register Variables: [None, Register keyword, Automatic]. O valor default é Automatic,
significando que as variáveis inteiras são alocadas nos registradores sempre que possível,
independente do uso ou não do prefixo register

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:

int vetor[5]={0, 1, 2, 3, 4};

Variáveis automáticas e registradores, quando não inicializadas explicitamente, têm o seu


valor indefinido. Lembre-se que o espaço para essas variáveis é alocado somente quando
da ativação do bloco onde elas foram declaradas, e que esse espaço (a pilha no caso de
variáveis automáticas e os registradores no outro caso) provavelmente já tinha sido usado
por funções chamadas anteriormente, que deixaram lá o conteúdo das suas próprias
variáveis.

A inicialização dessas variáveis, quando explicitamente feita no programa, se dá a cada vez


que o bloco onde elas foram declaradas é ativado. Por esse motivo, a inicialização pode ser
feita com constantes, variáveis ou expressões.

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);

A função que implementa o fatorial seria:

long fatorial(int n) {
long res;

if (n==0)
res=1L;
else
res = n*fatorial(n-1);
return res;
}

O que torna possível a recursividade em C é o fato de as variáveis automáticas e os


parâmetros formais serem criados na pilha.

Vamos estudar de forma simplificada a evolução da pilha para o cálculo de fatorial(2)


usando a função acima:

a) Primeira chamada da função: fatorial(2)

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

c) Terceira chamada da função: fatorial(0)

res fat(0)==1 SP

n 0
res 1*fat(0)
n 1
res 2*fat(1)

n 2

d) Primeiro retorno da função.

res 1
n 0
res fat(1)=1*1 SP

n 1
res 2*fat(1)

n 2

e) Segundo retorno da função.

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

O pré-processador é um programa que examina o programa fonte em C e executa neste


fonte certas modificações baseado em instruções chamadas de diretivas.

As modificações são feitas no texto do código fonte, antes do início do processo de


compilação.

Todas as diretivas ao pré-processador começam com o símbolo #

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);
}

Antes do início da compilação, o pré-processador troca todas as ocorrências de PI no


arquivo fonte por 3.14.

Constantes simbólicas são escritas usualmente em letras maiúsculas para diferenciá-las de


variáveis.

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 }

um programa em C poderia ser escrito como:

if (i>0) then
begin
a = 1;
b = 2;
end

Observe como o programa ficou parecido com um programa Pascal.

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.

#define area(base, altura) ((base)*(altura)/2.0)

89
cuja chamada seria:

s = area(b, h);

o pré-processador substituirá a instrução acima por:

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);

o pré-processador faria então a seguinte alteração:

s = b+1*h+1/2.0;

o que, obviamente, não seria o que esperávamos.

Por precaução, coloque parênteses envolvendo o texto todo de uma macro, bem como
cada um dos argumentos.

Vejamos outro exemplo. A macro abaixo calcula o valor absoluto de um número.

#define abs(x) ((x) > 0 ? (x) : -(x))

Macros versus funções

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);
}

Se quiséssemos calcular o valor absoluto de argumentos inteiros, teríamos de escrever uma


outra função para esse fim.

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

A diretiva #include já foi vista no Capítulo 1, quando falamos do arquivo stdio.h.

Observe que o uso do #include é diferente de mantermos funções em arquivos distintos


que são compilados separadamente e que são linkados se necessário.

O pré-processador ao encontrar o #include fisicamente copia um arquivo para dentro


do outro antes de iniciar a compilação. O código resultante é todo compilado junto.

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 estendem o conceito de variáveis contendo um item de informação, para variáveis


contendo vários itens de informação. Eles oferecem a possibilidade de tratar um grupo de
dados do mesmo tipo como um conjunto. Cada um dos elementos do conjunto pode ser
individualmente acessado através de um índice.

Nesse capítulo estudaremos vetores unidimensionais, matrizes, strings e dois algoritmos


freqüentemente aplicados a vetores: Busca e Ordenação.

Vetores unidimensionais

A utilização de um vetor em C deve ser precedido pela declaração do mesmo. A


declaração de um vetor é composta pelo tipo do vetor (o tipo de cada um dos elementos),
seguida pelo nome do vetor e pela dimensão do mesmo (o número de elementos que
compõem o vetor). A declaração de uma tabela de, por exemplo, 25 inteiros teria a forma:

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]

O primeiro elemento do vetor é referenciado como IntArray[0], o seguinte como


IntArray[1], e assim por diante até IntArray[24].

Observe, que o elemento referenciado por:

92
IntArray[2]
não é o segundo elemento do vetor, e sim o terceiro, uma vez que a numeração começa em
0.

Assim, a forma geral da declaração de um vetor é:

tipo nome[dimensão];

onde tipo é um tipo qualquer de dados, nome é o nome do vetor e dimensão é o


número de elementos do tipo tipo contidos no vetor. O primeiro elemento do vetor é
nome[0] e o último nome[dimensão-1].

O espaço total de memória alocado para o vetor nome é:

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];

6. for (i=0; i<DIM; i++) {


7. printf("vetor1[%d] = ", i);
8. scanf("%d", &vetor1[i]);
9. }

10. for (i=0; i<DIM; i++) {


11. printf("vetor2[%d] = ", i);
12. scanf("%d", &vetor2[i]);
13. }

14. for (i=0; i<DIM; i++)


15. printf("vetor1[%d] + vetor2[%d] = %d\n", i, i,
16. vetor1[i]+vetor2[i]);
17.}

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];

for (i=0; i<DIM; i++) {


printf("vetor1[%d] = ", i);
scanf("%f", &vetor1[i]);
}

for (i=0; i<DIM; i++) {


printf("vetor2[%d] = ", i);
scanf("%f", &vetor2[i]);
}

for (i=0; i<DIM; i++)


printf("vetor1[%d] + vetor2[%d] = %.2f\n", i, i,
vetor1[i]+vetor2[i]);
}

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.

Seja, por exemplo, a seguinte declaração:

int sentinela=0;
int Array[3];

A alocação em memória, numa máquina do tipo PC, seria:

Array[0] 32 SP

Array[1] 5
Array[2] 2
sentinela 0

Escrevendo além do fim de um vetor

Quando você atribui um valor a um elemento de um vetor, o compilador calcula onde


armazenar o valor baseado no tamanho de cada elemento e no índice deste elemento.
Suponha que você queira atribuir um valor a Array[2] que é o terceiro elemento do
vetor. O compilador multiplica o índice 2 pelo tamanho de cada elemento do vetor, no
caso, 4 bytes. O valor resultante (8) é somado ao endereço da primeira posição do vetor
para obter o endereço de Array[2].
Em C não existe crítica quanto ao limite dos vetores. Se você pedir ao compilador para
acessar o elemento Array[3], o compilador ignora o fato que não há tal elemento. Ele
calcula a que distância (em bytes) tal elemento deveria estar do primeiro elemento do vetor
e escreve por cima do dado que se encontrava naquela localização de memória. Este pode
ser virtualmente qualquer dado e, escrever um novo valor nesta posição, pode levar a
resultados imprevisíveis. Se você tiver sorte, seu programa causará algum erro fatal e será
imediatamente interrompido. Caso contrário, o programa começará a gerar resultados
estranhos muito depois do acesso inválido e você terá dificuldades para entender o que saiu
errado.

Observe o seguinte exemplo:

95
1. void main() {
2. int i;
3. int sentinela=0;
4. int Array[25];

5. printf("Sentinela = %d\n", sentinela);


6. for (i = 0; i<=25; i++)
7. Array[i] = 20;
8. printf("Sentinela = %d\n", sentinela);
9. }

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:

int vetor[5]={0, 1, 2, 3, 4};

Os valores são atribuídos em seqüência, isto é,

vetor[0] = 0;
vetor[1] = 1;

e assim por diante.

A declaração acima poderia ser substituída por:

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.

Vetores com mais de uma dimensão

Os elementos de um vetor podem ser de qualquer tipo, simples ou estruturado. Na prática,


é muito comum a ocorrência em que o tipo do elemento é também um vetor. A estrutura
assim definida é chamada de MATRIZ. Uma matriz é simplesmente uma tabela com 2 ou
mais dimensões (ou índices).

Forma geral:

tipo nome [tamanho1][tamanho2] ... [tamanhoN]

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.

ALUNO PROVA 1 PROVA 2 PROVA 3 PROVA 4 PROVA 5


1 9.5 9.0 6.5 5.0 7.0
2 5.0 4.0 5.5 6.5 3.5
3 7.0 6.5 8.0 10.0 8.5
4 6.5 7.0 5.5 4.5 6.5
5 4.5 5.0 6.0 5.5 5.0
6 3.0 3.5 6.0 5.5 5.0

Como acessar cada nota?

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).

Como fica então a declaração desta tabela?

#define NumeroDeProvas 5
#define MaximoDeAlunos 50

float
boletim[MaximoDeAlunos][NumeroDeProvas];

Como podemos referenciar a 3ª nota do 15º aluno?

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.

CONSULTOR SEG TER QUA QUI SEX


1 6 8 7 4 10
2 8 8 9 9 7
. . . . . .
. . . . . .
30 6 10 4 3 2

A declaração dessa tabela em C ficaria:

#define maxconsult 30

int tabela_horas[maxconsult][5];

Quantas horas o consultor número 2 trabalhou na quinta-feira?

Vamos criar um tipo enumerado para os dias da semana:

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;

for (num_horas=0, dia=SEG; dia<=SEX; dia++)


num_horas += tabela_horas[4][dia];

printf("O consultor 5 trabalhou %d horas\n", num_horas);

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

que em C, seria definida como:

enum cliente {novo, antigo};


enum mercadoria {alimento, limpeza, papelaria, ferragem,
eletrodomestico};

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:

Já vimos como construir tabelas de 1 dimensão (vetores) e duas dimensões (matrizes). A


Linguagem C permite a construção de tabelas com qualquer número de dimensões, o que
propicia a solução de inúmeros problemas em vários campos de aplicação.

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.

#define MaxFornecedor 100


#define MaxObra 20
#define MaxMaterial 500

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:

compras [11] [13] [150]

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];

for (i=0; i<3; i++)


for (j=0; j<2; j++)
{
for(c[i][j]=0, k=0; k<4; k++)
c[i][j] += a[i][k]*b[k][j];
printf("c[%d][%d] = %d\n", i, j, c[i][j]);
}
}

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.

A matriz C no exemplo anterior seria armazenada na seguinte ordem:

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.

Inicializando vetores de 3 dimensões

Vetores de 3 dimensões podem ser vistos como vetores em que os elementos são matrizes.

Exemplo:

int vet3d[3][4][2]={ { {6, 3},


{9, 7},
{3, 9},
{4, 2} },

{ {5, 1},
{6, 2},
{7, 8},
{9, 1} },

{ {4, 5},
{8, 1},
{2, 3},
{4, 5} } };

Como faríamos para acessar o primeiro 8?

vet3d[1][2][1]

Vetores como argumento de funções

Vimos no capítulo anterior que vetores constituíam em uma exceção à passagem de


parâmetros por valor. 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. Observe o programa abaixo que retorna o índice do maior elemento em um vetor
de inteiros.

102
#define DIM 5

int maior(int vet[]);

void main() {
int vetor[DIM]={1, 10, 7, 35, 4};

printf("Maior = %d\n", maior(vetor));


}

int maior(int vet[]) {


int i, M;

for(M=vet[0], i=1; i<DIM; i++)


if(vet[i]>M) M=vet[i];
return M;
}

Observe a declaração do vetor vet na função maior():

int maior(int vet[]);

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.

E se quisermos passar uma matriz como argumento?

Seja o seguinte programa que preenche uma matriz quadrada com 0's.

#define DIMX 5
#define DIMY 5

void preenche(int mat[][DIMY]);

void main() {
int mat[DIMX][DIMY];

preenche(mat);
}

void preenche(int mat[][DIMY]) {


int i, j;

for(i=0; i<DIMX; i++)


for(j=0; j<DIMY; j++)
mat[i][j]=0;

103
}

Observe o protótipo da função preenche():

void preenche(int mat[][DIMY]);

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

Vamos ver como o compilador calcula o endereço de um elemento genérico c[i][j]:

&c[i][j] == &c[0][0] + (i*DIMC + j) * sizeof(int);

Por exemplo, o endereço do elemento c[2][1] seria:

&c[2][1] == 1000 + (2*2 + 1) * 2 == 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').

Como em qualquer vetor, os caracteres da string podem ser individualmente acessados.

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);
}

A função printf() imprime os caracteres armazenados a partir do endereço nome


(Lembre-se: nome == &nome[0]) até que seja encontrado o caracter null.

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 caracteres podem ainda ser inicializados com strings constantes:


105
char nome[]="NCE";

que é equivalente à inicialização anterior.

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 nomes[][10]={ "Eduardo",


"Andre",
"Alexandre",
"Debora",
"Cecilia" };

Vamos reforçar o conceito de que, em C, o nome de um vetor é o endereço da primeira


posição do mesmo.

char pessoa[]="Eduardo";

Lembre-se: pessoa == &pessoa[0].

De modo equivalente, em matrizes como a do exemplo acima, temos:

nomes[0] == &nomes[0][0].

Pode-se entender nomes[0] como o nome do vetor representando a primeira linha da


matriz. Assim, a instrução:

printf("O mais bonito e': %s.\n", nomes[0]);

imprimiria:

O mais bonito e' Eduardo.

Referências a uma matriz usando apenas um índice são equivalentes


ao endereço das linhas da matriz.
106
Funções que manipulam strings

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);

printf("String1: %s\n", String1);


printf("String2: %s\n", String2);
}

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';

ptr = strchr(string, c);


if (ptr)
printf("The character %c is at position: %d\n",
c, ptr-string);
else
printf("The character was not found\n");
}

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;

ptr = strstr(str1, str2);


printf("The substring is: %s\n", ptr);
}
Algoritmos de Busca

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.

Em processamento de dados, a tarefa de procurar é, como se pode imaginar, uma das


funções mais utilizadas. Por exemplo, consultar um terminal automático para saber o valor
de seu saldo, ou um sistema de registro acadêmico para saber o seu histórico escolar.

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.

O objetivo primordial desta seção é apresentar um conjunto de rotinas para busca de


informações em tabelas. Além disto, queremos mostrar uma maneira de fazer avaliações
sobre a eficiência destas rotinas. Deste modo, ao defrontar-se com um problema prático,
você poderá julgar se os algoritmos apresentados prestam-se ou não a esta dada situação.

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 */

puts("Entre com os valores da tabela");


for (ind=0; ind<N; ind++)
scanf("%d", &tab[ind]);
printf("Entre com o valor a ser procurado...");
scanf("%d", &chave);

for (ind=0; (ind < N) && (tab[ind] != chave); ind++);

if (ind < N)
printf("Chave encontrada na posicao %d\n", ind);
else
printf("Chave nao encontrada\n");
}

Análise:

Quantas comparações serão executadas para encontrar a chave? Depende de como os


elementos são distribuídos na tabela. Se esta distribuição for aleatória, podemos esperar
desde encontrar a chave na primeira posição, até ter de percorrer toda a tabela. Dessa
forma, em média, o algoritmo executa N/2 repetições do for. Diz-se então, que o tempo
de execução do algoritmo é da ordem de N/2 e escreve-se O(N/2).

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

A otimização do algoritmo anterior dá-se pela inserção da chave procurada ao final da


tabela (que é, portanto, acrescida de um elemento). A busca termina quando a chave for
encontrada, o que certamente ocorrerá. Se isto ocorrer na última posição da tabela, a
chave procurada não pertence à tabela.

#define N 10

void main() {
int ind;
int chave;
int tab[N+1];

puts("Entre com os valores da tabela");


for (ind=0; ind<N; ind++)
scanf("%d", &tab[ind]);
printf("Entre com o valor a ser procurado...");
scanf("%d", &chave);

for (ind=0, tab[N]=chave; (tab[ind] != chave); ind++);

if (ind < N)
printf("Chave encontrada na posicao %d\n", ind);
else
printf("Chave nao encontrada\n");
}

Uma outra forma de otimizar o algoritmo de busca seqüencial, é ordenar os elementos do


vetor em ordem crescente. Nesse caso, ao atingir-se um elemento da tabela maior do que a
chave, a busca termina, indicando que a chave procurada não encontra-se na tabela. (Uma
vez que todos os elementos até o fim da tabela são maiores do que a chave)

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.

Busca Binária em Tabela Ordenada

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:

• Abre-se o dicionário aproximadamente ao meio e notamos que a entrada está na letra J


- pág. 798. Como a letra T vem depois da letra J, podemos abandonar a primeira
metade e procurar somente na parte final;

• Tomamos a metade final, dividimos ao meio novamente e encontramos a letra P (pág.


1106);

• Tomamos novamente a parte final e dividimos ao meio, caindo na letra R (pág. 1204);

• Dividimos ao meio e chegamos à letra S (pág. 1318);

• Ao dividirmos novamente, chegamos à entrada Tomo (pág. 1368). Portanto a palavra


deve estar entre as páginas 1318 e 1386;

• Se continuarmos teremos sucessivamente as páginas 1353, 1343, 1347 e 1345.

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).

O logaritmo de um número tem a propriedade de crescer muito menos rapidamente do que


o número. Por exemplo, uma pesquisa em tabela com 32.000 itens, precisa de somente 15
comparações. A cada vez que dobramos o tamanho da tabela, necessitamos de apenas
mais uma pesquisa.
No programa abaixo, início, meio e fim são os marcadores da área de pesquisa.
Caso o elemento pesquisado seja menor do que o elemento procurado, fazemos com que o
marcador início seja igual a meio + 1, caso contrário, fazemos com que fim seja
igual a meio - 1.

#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 */

puts("Entre com os valores da tabela em ordem crescente");


for (ind=0; ind<N; ind++)
scanf("%d", &tab[ind]);
printf("Entre com o valor a ser procurado...");
scanf("%d", &chave);

while ((inicio <= fim) && (tab[meio] != chave)) {


if (tab[meio] < chave)
inicio = meio + 1;
else
fim = meio - 1;
meio = (inicio+fim)>>1; /* divisao por 2 */

115
}

if (tab[meio] == chave)
printf("Chave encontrada na posicao %d\n", meio);
else
printf("Chave nao encontrada\n");
}

O algoritmo de busca binária é da ordem de O(log 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:

tab[i] >= tab[j] se i > j

Ordem decrescente:

tab[i] < = tab[j] se i > j

Os algoritmos de ordenação podem ser classificados de acordo com sua eficiência em


elementares ou avançados. Isto se deve ao fato de que existe uma grande disparidade entre
os diversos métodos de ordenação. Alguns algoritmos apresentam um desempenho
excepcional quando comparados com os métodos mais simples.

Vamos começar examinando a Ordenação pelo Método da Seleção.

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.

Para melhor acompanhar o algoritmo, vamos examinar um exemplo com um vetor de 8


elementos.

tab = [46 15 91 59 62 76 10 93]

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.

tab = [10 15 91 59 62 76 46 93]

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.

Repete-se o processo até a penúltima posição do vetor, pois a última ficará


automaticamente posicionada.

O processo, na sua totalidade, é apresentado na seqüência abaixo.

Início - [46 15 91 59 62 76 10 93]


passo 1 - [10 15 91 59 62 76 46 93]
passo 2 - [10 15 46 59 62 76 91 93]
passo 3 - [10 15 46 59 62 76 91 93]
passo 4 - [10 15 46 59 62 76 91 93]
passo 5 - [10 15 46 59 62 76 91 93]
passo 6 - [10 15 46 59 62 76 91 93]
passo 7 - [10 15 46 59 62 76 91 93]
passo 8 - [10 15 46 59 62 76 91 93]

O algoritmo, numa primeira versão, pode ser escrito na forma:

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 */

7. puts("Entre com os valores da tabela");


8. for (ind1=0; ind1<N; ind1++)
9. scanf("%d", &tab[ind1]);

10. for (ind1=0; ind1<N-1; ind1++)


117
11. for (ind2=ind1+1; ind2<N; ind2++)
12. if (tab[ind1] > tab[ind2]) {
13. aux = tab[ind2];
14. tab[ind2] = tab[ind1];
15. tab[ind1] = aux;
16. }

17. puts("\nO vetor ordenado e':");


18. for (ind1=0; ind1<N; ind1++)
19. printf("%d ", tab[ind1]);
20. printf("\n");
21.}

Saída:

Entre com os valores da tabela


9 2 3 8 4 7 5 10 6 1

O valor ordenado é:
1 2 3 4 5 6 7 8 9 10

Análise:

O coração do algoritmo encontra-se nas linhas 10-15. Em cada passo, o elemento na


posição ind1 é preenchido com o valor correto. Cada vez que o elemento em ind2 for
menor do que o elemento na posição ind1, estes dois elementos trocam de posição.

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 */

6. puts("Entre com os valores da tabela");


7. for (ind1=0; ind1<N; ind1++)
8. scanf("%d", &tab[ind1]);

9. for (ind1=0; ind1<N-1; ind1++) {


10. int
11. aux, /* variavel auxiliar para a troca */
12. indmin; /* posicao do menor elemento */

13. for (indmin=ind1, ind2=ind1+1; ind2<N; ind2++)


14. if (tab[indmin] > tab[ind2])
15. indmin = ind2;
16. aux = tab[indmin];
118
17. tab[indmin] = tab[ind1];
18. tab[ind1] = aux;
19. }

20. puts("\nO vetor ordenado e':");


21. for (ind1=0; ind1<N; ind1++)
22. printf("%d\n", tab[ind1]);
23.}

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.

Podemos analisar a eficiência deste algoritmo da seguinte maneira: o esforço computacional


gasto num processo de ordenação está nas suas operações básicas que são as
comparações e as trocas de posições de elementos.

A cada passagem do for mais externo é feita uma troca, num total de (N-1) trocas.

O número de comparações depende do passo que estamos executando. No primeiro passo


temos 9 comparações, no segundo 8, no terceiro 7 até que, no último passo, temos
somente 1 comparação. O número de comparações é então a soma dos números inteiros
de 1 até 9. Para o caso geral, num vetor com N elementos, o número de comparações é
igual a soma dos N-1 primeiros inteiros. Isto pode ser expresso pela fórmula:

Nc = ( N − 1) * N 2

Da fórmula acima podemos notar que o número de comparações domina o número de


trocas, e é proporcional ao quadrado de N. Dizemos então que a eficiência do algoritmo é
quadrática (O(n2)). Isto significa que se N for dobrado ou triplicado, o tempo de
processamento é multiplicado por um fator de 4 ou 9 respectivamente. Obviamente,
necessitamos de algoritmos mais rápidos para valores grandes de N (talvez até para valores
pequenos). O algoritmo da próxima seção apresenta uma evolução nesse sentido.

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

enum BOOL {FALSE, TRUE};

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 */

puts("Entre com os valores da tabela");


for (ind1=0; ind1<N; ind1++)
scanf("%d", &tab[ind1]);

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);

puts("\nO vetor ordenado e':");


for (ind1=0; ind1<N; ind1++)
printf("%d ", tab[ind1]);
printf("\n");
}

Se o algoritmo for aplicado a uma tabela ordenada são necessárias N comparações e


nenhuma troca para chegar ao fim do procedimento. Este é o melhor caso e o tempo de
processamento é proporcional a N.

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 armazena o código e as variáveis dos programas escritos por você na


memória. A memória é constituída, no seu nível mais baixo, por bits , circuitos eletrônicos
capazes de armazenar dois valores normalmente associados aos níveis lógicos 0 e 1.

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

Observe que o conteúdo da variável ptr é o endereço da variável a.

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.

elemento 1 elemento 2 elemento 3

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;

O operador (*) é chamado em C de operador indireto e indica ao compilador que a


variável que o segue é um ponteiro. A declaração acima deve ser lida da seguinte forma:
“ptipo é um ponteiro para uma variável do tipo tipo”.

Exemplo:

int *pint;

pint é um ponteiro para uma variável do tipo inteiro.

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. }

Para entender o funcionamento do programa, vamos acompanhar a evolução da pilha


instrução a instrução. Suponha que as variáveis do programa são carregadas a partir do
endereço 1000.

• 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

Pode-se alocar memória para os dados do programa dinamicamente, em tempo de


execução. Esta memória é alocada pelo sistema operacional em uma região de memória
conhecida como heap. Assim, através de funções específicas, o programa pede ao sistema
operacional a quantidade de memória desejada no heap. O sistema operacional reserva
esta quantidade de memória para a aplicação (se houver tal quantidade de memória
disponível) e marca os bytes alocados a fim de que, em chamadas futuras, o seu endereço
não seja atribuído a outros ponteiros.

Vamos estudar as funções para alocação dinâmica de memória.

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.

calloc() aloca um bloco de memória de tamanho (nitems*size) bytes e


preenche o seu conteúdo com zeros.

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;

ptr=(int *)calloc(1, sizeof(int));


*ptr = 3;
printf("%d\n", *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()

Uso: sizeof(tipo) ou sizeof(expressão)

Devolve o tamanho em bytes do tipo ou da expressão entre parênteses.

Exemplos:

sizeof(int) == 2
sizeof(float) == 4

• A conversão forçada de tipos

Uso: (tipo)nome

Converte a variável nome para o tipo entre parênteses.

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.

A inicialização de um ponteiro é realmente necessária? Sim. Lembre-se que a declaração de


um ponteiro não inicializa o conteúdo do mesmo. Desta forma, o efeito de atribuir um valor
a *ptr sem antes inicializar o ponteiro é imprevisível uma vez que podemos alterar o
conteúdo de alguma outra variável do programa ou mesmo “sujar” o código do programa.
A regra para usar ponteiros é simples: nunca acesse o conteúdo de uma posição de
memória referenciada por um ponteiro sem antes inicializar o mesmo, ou, em outras
palavras, atribua sempre um endereço a um ponteiro antes de usá-lo.

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( )

O exemplo a seguir mostra a utilização das funções malloc(), calloc() e


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

Na linha 2 a variável Local é declarada e inicializada com o valor 5.


Na linha 3 é declarado o ponteiro pLocal e o seu conteúdo inicializado com o endereço
da variável Local.
Na linha 4 é declarado o ponteiro pHeap e o seu conteúdo inicializado com o endereço de
um bloco de memória no heap obtido através de uma chamada à função calloc(). Este
bloco de memória tem tamanho suficiente para armazenar um único número inteiro5.
Na linha 5 é realizado um teste para verificar se o bloco de memória no heap foi alocado
com sucesso. Uma vez que existe a possibilidade de erro nas chamadas às funções
calloc() e malloc() (por exemplo, se o bloco de memória requisitado for muito
grande), qualquer aplicação séria deveria verificar o valor retornado por tais funções.
Na linha 9 o bloco de memória no heap é inicializado com o valor 7. Observe que a variável
Local e o ponteiro pLocal referem-se à mesma posição de memória, o que pode ser
constatado pela seqüência de printf() nas linhas 10-12.
Na linha 13 o bloco de memória no heap é liberado.
Na linha 14 o ponteiro pHeap é usado para alocar um novo bloco no heap. O endereço
atribuído a pHeap através da chamada à função malloc() não é necessariamente o
endereço do bloco liberado anteriormente pela chamada à função free().
Observe a diferença entre o acesso à memória usando variáveis e ponteiros. Como vimos
anteriormente, o nome de uma variável corresponde a um apelido de uma posição de
memória. No entanto, esta associação entre o nome da variável e o endereço de memória
permanece fixa durante toda a execução do programa 6. O endereço de memória
referenciado por um ponteiro, ao contrário, pode variar durante a execução do programa
podendo “apontar” para diferentes regiões de memória.

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);
.
.

void troca(int a, int b) { /* ERRADO */


int
aux = a;
a = b;
b = aux;
}

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).

• antes da chamada de troca()

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

• No retorno ao programa principal:

aux:troca 2 994
b:troca 2 998
a:troca 3 1002

... 1006 SP

...
tab[ind1] 2 1026
tab[ind1+1] 3 1030

Observe que a função troca() não altera os valores de tab[ind1] e tab[ind1+1].


Isto se dá porque a função trabalha sobre o conteúdo das suas próprias variáveis, criadas
na pilha.

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.

void troca(int *a, int *b) {


int
aux = *a;
*a = *b;
*b = aux;
}

Vamos estudar novamente a evolução da pilha:

• antes da chamada de troca():

994
998
1002

... 1006 SP

...
tab[ind1] 2 1026
tab[ind1+1] 3 1030

• Na chamada à função troca()

aux:troca 994 SP

b:troca 1030 998


a:troca 1026 1002

... 1006

...
tab[ind1] 2 1026
tab[ind1+1] 3 1030

133
• aux = *a;

aux:troca 2 994 SP

b:troca 1030 998


a:troca 1026 1002

... 1006

...
tab[ind1] 2 1026
tab[ind1+1] 3 1030

• *a = *b;

aux:troca 2 994 SP

b:troca 1030 998


a:troca 1026 1002

... 1006

...
tab[ind1] 3 1026
tab[ind1+1] 3 1030

• *b = aux;

aux:troca 2 994 SP

b:troca 1030 998


a:troca 1026 1002

... 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.

Aritmética com Ponteiros

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

Observe as duas primeiras instruções:

p1 = vetor;
p2 = &vetor[2];

Elas já são nossas conhecidas. Consistem na atribuição de endereços, ou inicialização, dos


ponteiros p1 e p2. O endereço atribuído é, normalmente, o endereço de alguma variável
do programa ou é obtido dinamicamente através de chamadas às funções malloc() e
calloc().

• Acessando os endereços apontados por ponteiros

*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

É importante entender a instrução seguinte:

*(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

À primeira vista você poderia imaginar que a instrução

*(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.

Felizmente, o compilador C é esperto o suficiente para perceber que p1 é um ponteiro.


Assim, quando incrementamos um ponteiro, o valor efetivamente acrescentado ao conteúdo
do ponteiro não é a unidade, mas sim, o tamanho do tipo apontado por este ponteiro. Desta
forma, a expressão (p1+1) refere-se ao endereço:

p1 + 1 * sizeof(int) == 1012 + 1*4 == 1016


// endereço de vetor[1]

De maneira semelhante, (p1+2) refere-se ao endereço

p1 + 2 * sizeof(int) == 1012 + 2*4 == 1020


// endereço de vetor[2]

Sendo assim, a instrução:

*(p1+2) = 2;

armazena o inteiro 2 na posição ocupada por vetor[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

O programa anterior mostra ainda a possibilidade de comparar ponteiros numa expressão:

if (p2 > p1)

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,

printf("Posicoes: %d\n", p2 - p1);

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.

Sejam, por exemplo, as seguintes declarações:

int list[10];

ou,

int *list=(int *)malloc(10*sizeof(int));

Em ambos os casos é possível escrever:

a) Referência a endereço:

(list+i) // equivalente a &list[i]


&list[i] // equivalente a (list+i)

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.

• Usando notação de vetor:

main() {
int list[]={1, 2, 3, 4, 5};
int ind;

for (ind=0; ind<5; ind++)


printf("%d\n", list[ind]);
}

• Usando ponteiros:

main() {
int list[]={1, 2, 3, 4, 5};
int ind;

for (ind=0; ind<5; ind++)


printf("%d\n", *(list+ind));
}

Existem, no entanto, diferenças significativas entre declarar list como um ponteiro ou


como um vetor. A primeira delas consiste em que, se você declarar list como um vetor,
o compilador automaticamente reserva o espaço em memória necessário para armazená-lo.
Ao contrário, se você declarar list como um ponteiro, será necessário obter o espaço
em memória (através, por exemplo, de uma chamada à função malloc()) ou então,
atribuir a list o endereço de alguma variável ou estrutura do programa.

No exemplo anterior, se list fosse declarado como um ponteiro teríamos:

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.

Outra importante diferença entre vetores e ponteiros é que o nome de um vetor é um


ponteiro constante e, portanto, não pode ter o seu valor alterado. Baseado nisso, as formas
abaixo não são válidas:

main() {
int list[5];
int i;

list = &i; // ERRADO


list++; // ERRADO
}

Ponteiros variáveis fornecem ainda uma maneira eficiente de percorrer seqüencialmente os


elementos de um vetor. Seja o seguinte exemplo:

main() {
int list[]={1, 2, 3, 4, 5};
int *pint;
int ind;

for (pint=list, ind=0; ind<5; ind++, pint++)


printf("%d\n", *pint);
}

Observe que para acessar o elemento list[ind] usando a notação de vetores, o


compilador teria de gerar código para calcular o endereço:

&list[ind] == list + ind * sizeof(int)

No exemplo anterior tira-se proveito do fato de que o vetor é percorrido seqüencialmente,


de modo que:

&list[ind] == pint == pint + sizeof(int))

Observe que deixamos de fazer uma multiplicação a cada acesso7.

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:

void imprime(int vetor[], int dim);

ou:

void imprime(int *vetor, int dim);

A única diferença consiste em que, no primeiro caso, vetor é um ponteiro constante e,


portanto, não pode ter o seu valor alterado, enquanto que, no segundo caso, poderíamos
escrever:

void imprime(int *vetor, int dim) {


int i;

for (i=0; i<dim; i++, vetor++)


printf("%d\n", *vetor);
}

Vetores de ponteiros versus matrizes

Observe as declarações:

1. int vetor1[500]

2. int *vetor2[500];

3. int *vetor3=(int *)malloc(500*sizeof(int));

142
vetor3 é uma variação de vetor1 mas é muito diferente de vetor2.

vetor1 é um vetor estático de 500 números inteiros. O espaço de memória reservado


para o vetor é alocado em tempo de compilação. De modo semelhante, vetor3 é também
um vetor de 500 números inteiros. No entanto, o espaço de memória ocupado pelo vetor é
alocado de forma dinâmica, em tempo de execução do programa.
vetor2 é também um vetor de 500 posições alocadas em tempo de compilação. No
entanto, cada um dos elementos do vetor é um ponteiro para um número inteiro. Ou, em
outras palavras, cada uma das posições do vetor vai armazenar o endereço de um número
inteiro. Observe que a simples declaração do vetor de ponteiros não inicializa o conteúdo
das posições do vetor (os endereços dos números inteiros). Desta forma, durante a
execução do programa, cada uma das posições do vetor precisa ser explicitamente
inicializada8.

Vetores de ponteiros oferecem uma alternativa à implementação de matrizes, como veremos


no exemplo a seguir.

/* soma 10 a cada um dos elementos da matriz */


main() {
int matriz[3][3]={{1, 2, 3},
{4, 5, 6},
{7, 8, 9}};
int *p[3]={matriz[0], matriz[1], matriz[2]};
int i, j;

for (i=0; i<3; i++)


for (j=0; j<3; j++)
p[i][j] += 10;
}

Supondo que matriz é armazenada a partir da posição 1000, a representação simbólica da


alocação em memória é dada por:

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.

Existem, no entanto, importantes diferenças entre as duas formas. A declaração,

int matriz[3][3];

aloca 9 posições de memória para armazenar números inteiros. Já a declaração:

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:

/* permuta as duas primeiras linhas da matriz */


void main() {
int matriz[3][3]={{1, 2, 3},
{4, 5, 6},
{7, 8, 9}};
int i, aux;

for (i=0; i<3; i++) {


aux = matriz[1][i];
matriz[1][i] = matriz[2][i];
matriz[2][i] = aux;
}
}

• Usando ponteiros:

/* permuta as duas primeiras linhas da matriz */


void main() {
int matriz[3][3]={{1, 2, 3},
{4, 5, 6},
{7, 8, 9}};

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

Alocação dinâmica de matrizes

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;

6. for(i=0; i<DIML; i++)


7. pMatriz[i]=(int *)calloc(DIMC,sizeof(int));

8. for(i=0; i<DIML; i++)


9. for(j=0; j<DIMC; j++)
10. printf("Matriz[%d][%d] = %d\n", i, j,
11. pMatriz[i][j]);
12.}

Análise:

Na linha 4 o vetor pMatriz[] é declarado. Observe que a dimensão do vetor,


correspondente ao número de linhas da matriz (DIML), é fixa e determinada em tempo de
compilação. Na linha 7, cada uma das posições de pMatriz[] é inicializada com o
endereço de um buffer no heap suficiente para armazenar um vetor de DIMC posições.
Dessa forma, cada chamada à função calloc() aloca espaço no heap para uma das
linhas da matriz. No caso do exemplo, todas as linhas têm o mesmo número de elementos
ou colunas (DIMC). A figura a seguir mostra o esquema de alocação da matriz com a
dimensão das linhas fixa

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

b) Dimensão das linhas e das colunas determinada em tempo de execução.

1. #define DIML 10
2. #define DIMC 10

3. void main() {
4. int **pMatriz;
5. int i, j;

6. pMatriz=(int **)calloc(DIML,sizeof(int *));


7. for(i=0; i<DIML; i++)
8. pMatriz[i]=(int *)calloc(DIMC,sizeof(int));

9. for(i=0; i<DIML; i++)


10. for(j=0; j<DIMC; j++)
11. printf("Matriz[%d][%d] = %d\n", i, j,
12. pMatriz[i][j]);
13.}

Análise:

Observe a declaração na linha 4. pMatriz é um ponteiro para um ponteiro, ou, se for


usada uma dimensão conveniente na linha 6, pMatriz é um ponteiro para um vetor de
ponteiros, alocado dinamicamente. Na linha 6 é criado no heap o vetor de ponteiros
apontado por pMatriz. Na linha 8 cada uma das posições de pMatriz[] é inicializada

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.

• Usando vetores de caracteres

Vamos rever o uso de vetores de caracteres através de um exemplo:

1. void main() {
2. char nome[10]="antigo";

3. printf("nome = %s\n", nome);


4. strcpy(nome, "novo");
5. printf("nome = %s\n", nome);

148
6. }

Saída:
nome = antigo
nome = novo

Análise:

Na linha 2 o vetor de caracteres nome[] é inicializado com a string “antigo”. Vimos no


Capítulo anterior que esta forma de inicialização é equivalente a:

char nome[10]={‘a’,‘n’,‘t’,‘i’,‘g’,‘o’,‘\0’);

Na linha 4 a string contida no vetor nome[] é alterada para “novo”. O compilador, ao


encontrar a declaração strcpy(nome, “novo”), cria em algum lugar da área de
código a string "novo" seguida pelo caracter null . Em seguida, ele chama a função
strcpy() passando como argumentos o endereço do vetor nome[] e o endereço da
string constante.
Por que não fizemos simplesmente (nome = "novo")? Porque o nome de um vetor é um
ponteiro constante e, nesta instrução, estaríamos tentando atribuir o endereço da string
"novo" a nome .

• Usando ponteiros para caracteres

Seja o seguinte exemplo:

1. void main() {
2. char *nome="antigo";

3. printf("nome = %s\n", nome);


4. nome = "novo";
5. printf("nome = %s\n", nome);
6. }

Saída:

nome = antigo
nome = novo

Análise:

Na inicialização, o compilador cria uma string constante ("antigo") em algum lugar da


área de código e atribui o endereço dessa string ao ponteiro nome .
Na linha 4 o compilador atribui o endereço da string "novo" ao ponteiro nome. Observe
que agora não foi necessário usar a função strcpy(). Isso se dá porque nome é um

149
ponteiro variável ao qual pode-se, portanto, atribuir qualquer endereço durante a execução
do programa.

Ponteiros e Vetores de Strings

Como vimos anteriormente, vetores de ponteiros oferecem uma alternativa à implementação


de matrizes. Assim, vetores de ponteiros para o tipo char são equivalentes a vetores de
strings .

Vamos rever o exemplo do Capítulo anterior de um vetor de strings:

char nomes[][10]={“Eduardo”,
“Ricardo”,
“Andre”,
“Alexandre”,
“Debora”,
“Cecilia”
“Marina”};

Supondo que nomes é armazenada a partir da posição 1000, a representação simbólica da


alocação em memória é dada por:

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.

Vamos ver agora a versão usando ponteiros:

char *nomes[]={"Eduardo",
“Ricardo”,

150
"Andre",
"Alexandre",
"Debora",
"Cecilia",
“Marina”};

A representação simbólica da alocação em memória é dada por:

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.

Argumentos em linha de comandos

152
Você certamente já utilizou algum programa cujos argumentos eram passados na chamada
ao mesmo, a partir do sistema operacional. Exemplo:

$ gcc -o teste teste.c

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:

void main(int argc, char *argv[]);

Como exemplo, vejamos o programa echo que ecoa os seus argumentos na saída padrão.
Assim, a linha de comando:

echo NCE Nucleo de Computacao Eletronica

produziria na saída:

NCE Nucleo de Computacao Eletronica

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"

Finalmente, o programa seria escrito na forma:

void main(int argc, char *argv[]) {


int i;

for (i=1; i<argc; i++)


153
printf("%s ", argv[i]);
}
Ponteiros para funções

Funções também possuem endereços que podem ser referenciados indiretamente através
de ponteiros.

A forma geral da declaração de um ponteiro para função é a seguinte:

tipo (*ptr)();

Na declaração acima, ptr é um ponteiro para uma função que retorna um tipo.

A utilização dos parênteses em (*ptr) é necessária, uma vez que,

tipo *ptr();

é a declaração de uma função ptr() que devolve um ponteiro para tipo.

Vejamos um exemplo prático. Vamos estudar duas alternativas de implementação de um


programa que solicita um número ao usuário e, em seguida, apresenta um menu de
alternativas de operações que podem ser feitas com o número. A primeira versão usa o
comando switch e a segunda, ponteiros para funções.

• Versão com switch

void main() {
int op, result, num;
int dobro(int);
int triplo(int);
int quadruplo(int);

printf("Entre com um numero : ");


scanf("%d", &num); getchar();
puts("2 - Multiplicar por 2");
puts("3 - Multiplicar por 3");
puts("4 - Multiplicar por 4");
printf("\nEntre com a operacao desejada : ");
op = getchar();

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
}

printf("\n\nResultado = %d\n", result);


}

int dobro(int num) {


return 2*num;
}

int triplo(int num) {


return(3*num);
}

int quadruplo(int num) {


return(4*num);
}

• Versão com ponteiros

typedef int (*VectPontFunc[3])(int);

void main() {
int op, num;
int dobro(int);
int triplo(int);
int quadruplo(int);
VectPontFunc ptr={dobro, triplo, quadruplo};

printf("Entre com um numero : ");


scanf("%d", &num); getchar();
puts("2 - Multiplicar por 2");
puts("3 - Multiplicar por 3");
puts("4 - Multiplicar por 4");
printf("\nEntre com a operacao desejada : ");
op = getchar();

printf("\n\nResultado = %d\n", (*ptr[op-'2'])(num));


}

int dobro(int num) {


return(2*num);
}

int triplo(int num) {


return(3*num);
}

int quadruplo(int num) {

return(4*num);
}

155
Observe a declaração,

typedef int (*VectPontFunc[3])(int);

Trata-se da declaração de um tipo, VectPontFunc, um vetor de 3 posições onde cada


um dos elementos é um ponteiro para uma função que recebe um número inteiro como
argumento e retorna um valor inteiro. O prefixo typedef cria tipos de dados definidos
pelo usuário os quais têm propriedades idênticas as dos tipos pré existentes na linguagem
(int, char, float, etc.). Assim, a partir da declaração de um tipo é possível usá-lo para
a declaração de variáveis, protótipos de funções, argumento do operador sizeof(), etc.

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

typedef unsigned int uint;


typedef char str40[41];

Vamos retornar ao exemplo anterior. A declaração:

VectPontFunc ptr={dobro, triplo, quadruplo};

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().

O nome de uma função desacompanhado de parênteses corresponde ao endereço da


função.

Observe agora a instrução,

(*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.

Os tipos estruturados struct e union, possibilitam a criação de estruturas de dados


complexas e, por conseguinte, são bastante utilizados na elaboração de programas
aplicativos.

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).

NOME : PAULO DA SILVA

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

Cada um dos componentes de uma estrutura é chamado de CAMPO.

A declaração em C de um registro do cadastro de funcionários ficaria:

struct { // declaração do tipo


char nome[20];
int código;
float salário;
}
funcionário; // declaração da variável

A definição do registro começa com a palavra reservada struct e termina no fecha


chaves (}). Entre as chaves encontramos a definição de cada um dos campos componentes
do registro. A definição de cada campo é exatamente igual à definição de qualquer variável
em C. Além disso, os campos podem ser de qualquer tipo válido em C, incluindo vetores,
ou até mesmo outros registros.

A variável funcionário segue a declaração do tipo, indicando ao compilador que


funcionário é uma variável do tipo struct.

O tamanho ocupado em memória pela variável funcionário é igual ao somatório dos


tamanhos dos campos, isto é:
159
sizeof(funcionário)==sizeof(funcionário.nome)+
sizeof(funcionário.código)+
sizeof(funcionário.salário9)

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:

struct RegFunc { // declaração do tipo


char nome[20];
int código;
float salário;
}
funcionário; // declaração da variável

em algum outro ponto do programa poderíamos declarar a variável gerente na forma:

struct RegFunc gerente;

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:

struct RegFunc { // declaração do tipo


char nome[20];
int código;
float salário;
};

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.

Para obter-se o conteúdo do sexto elemento do vetor x[] mencionado anteriormente,


devemos escrever:

x [5]

variável seletor

De forma análoga, para acessarmos o campo salário da estrutura funcionário,


temos:

funcionário.salário

variável seletor

A diferença fundamental no acesso a elementos de registros e de vetores é que o seletor de


um vetor pode ser uma expressão, enquanto o de um registro tem que ser obrigatoriamente
um campo, não podendo portanto, ser calculado em tempo de execução do programa.

Para preenchermos os campos da estrutura funcionário a partir do teclado, teríamos:

scanf("%s", funcionário.nome);
scanf("%d", &funcionário.código);
scanf("%f", &funcionário.salário);

Em resumo, um registro é uma estrutura com as seguintes características:

• contém um número fixo de elementos chamados campos;

161
• os campos podem ser de qualquer tipo;

• os campos podem ser de tipos diferentes;

• cada campo é referenciado por um nome chamado de identificador do campo.

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.

No exemplo anterior, as variáveis funcionário e gerente do tipo RegFunc


poderiam ser inicializadas como:

struct RegFunc { // declaração do tipo


char nome[20];
int código;
float salário;
};

void main() {
struct RegFunc
funcionário={"Márcio Nascimento", 1234, 1000.00},
gerente={"Roberto Diniz", 24, 2000.00};
}

Atribuições entre estruturas

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;

Na instrução acima, todos os campos do registro funcionário são atribuídos aos


campos correspondentes do registro gerente.

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:

typedef struct { // declaração da estrutura mais interna


int dia;
char mês[10];
int ano;
} data;

typedef struct { // declaração da estrutura externa


char nome[20];
int código;
float salário;
data nascimento;
} RegFunc;

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.

O acesso a um campo de uma estrutura aninhada é feito na forma:

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 (&).

Em compiladores mais recentes, a responsabilidade da decisão é deixada para o


programador. Assim, uma função pode passar ou retornar uma estrutura para outra função.
Voltemos ao exemplo: suponhamos que o programa principal chama uma função
NovoFuncionario() que devolve os dados de um novo funcionário. Em seguida
chama-se a função list() que imprime os dados do novo registro.

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;

10. funcionário = NovoFuncionario();


11. list(funcionário);
12. .
13. .
14. }

15. RegFunc NovoFuncionario(void) {


16. RegFunc func;

17. printf("Nome : "); gets(func.nome);


18. printf("Código : "); scanf("%d", &func.codigo);
19. printf("Salário: "); scanf("%f", &func.salario);
20. return func;
21. }
22.
23. void list(RegFunc func) {
24. printf("\nNome : %s\n", func.nome);
25. printf("Código : %d\n", func.codigo);
26. printf("Salário: %g\n", func.salario);
27. }

Comentários:

(linha 6) Protótipo da função NovoFuncionario(). NovoFuncionario() é uma


função que retorna uma estrutura do tipo RegFunc.
164
(linha 7) Protótipo da função list(). list() é uma função que recebe como
argumento uma estrutura do tipo RegFunc. A estrutura é passada por valor.

(linha 10) Atribuição entre estruturas. A estrutura funcionário recebe os valores dos
campos da estrutura retornada pela função NovoFuncionario().

(linha 16) func é uma variável local à função NovoFuncionario().


Deve-se ter em mente que se a estrutura a ser passada ou retornada for muito grande, e se
o tempo de execução do programa for um dado crítico, a solução usando ponteiros
(passagem por referência) pode ser mais apropriada.

Ponteiros para estruturas

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.

Vamos reescrever o exemplo de passagem de estruturas como parâmetro entre funções,


dessa vez usando ponteiros:

1. typedef struct {
2. char nome[20];
3. int codigo;
4. float salario;
5. } RegFunc;

6. void NovoFuncionario(RegFunc *pFunc);


7. void list(RegFunc *pFunc);

8. void main() {
9. RegFunc funcionario;

10. NovoFuncionario(&funcionario);
11. list(&funcionario);
12. // .
13. // .
14. }

15. void NovoFuncionario(RegFunc *pFunc) {


16. printf("Nome : "); gets((*pFunc).nome);
17. printf("Codigo : "); scanf("%d", &(*pFunc).codigo);
18. printf("Salario: "); scanf("%f", &(*pFunc).salario);
19. }

20. void list(RegFunc *pFunc) {


165
21. printf("\nNome : %s\n", pFunc->nome);
22. printf("Codigo : %d\n", pFunc->codigo);
23. printf("Salario: %g\n", pFunc->salario);
24. }

Comentários:

(linha 6) - Observe o protótipo da função NovoFuncionario().


NovoFuncionario() recebe como argumento um ponteiro para uma
estrutura do tipo RegFunc. A sintaxe da declaração de um ponteiro para
estruturas é a mesma de qualquer outra declaração de ponteiros que já
tenhamos visto.

(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

Por que não podemos fazer gets(pFunc.nome)? Porque pFunc não é


uma estrutura e sim um ponteiro.

(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

Aqui também, como (pFunc == &funcionario), é verdadeira a relação:

pFunc->nome == funcionario.nome

Comparando as duas notações anteriores, vemos que:

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?

A solução está em usar um vetor de funcionários:

#define MAXFUNC 1000

typedef struct { // declaração do tipo


char nome[20];
int codigo;
float salario;
} RegFunc;

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.

Para referenciar o salário do funcionário 25, deve-se escrever:

funcionário[25].salario

Vetores de estruturas podem ser inicializados como se segue:

#define MAXFUNC 1000


RegFunc funcionario[MAXFUNC]=
{{"Alice Falcao", 4650, 1231.00},
{"Ronaldo Jesus", 7587, 670.00},
{"Almir Cardoso", 3640, 247.00},
{"Jose Lima", 6194, 1118.00},
{"Joao Prado", 2435, 889.00},
.
.
.
};

Vejamos um programa simples para listar o cadastro de funcionários de uma empresa:

1. typedef struct { // declaração do tipo


2. char nome[20];
3. int codigo;
4. float salario;
5. } RegFunc;

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. };

16. printf("%s %22s %20s\n", "nome", "codigo", "salario");


17. for (i=0; i<sizeof(funcionario)/sizeof(RegFunc); i++)
18. printf("%-21s %4d %21.2f\n",
19. funcionario[i].nome,
20. funcionario[i].codigo,
21. funcionario[i].salario);
22. }
Saída:

nome codigo salario


Alice Falcao 4650 1231.00
Ronaldo Jesus 7587 670.00
Almir Cardoso 3640 247.00
Jose Lima 6194 1118.00
Joao Prado 2435 889.00

Comentários:

Observe a construção na linha 17,

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.

elemento 1 elemento 2 elemento 3


cabeça

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.

Uma lista encadeada é acessada através de um ponteiro (cabeça) para o primeiro


elemento da lista. A partir daí, os elementos são acessados em seqüência: o próximo
elemento é aquele apontado pelo elemento atual.

Vamos modificar os registros de funcionários, de modo a que eles possam ser estruturados
na forma de uma lista encadeada.

struct RegFunc { // declaração do tipo


char nome[20];
int codigo;
float salario;
struct RegFunc *pprox;
}

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.

Percorrendo listas encadeadas.

Percorre-se a lista a partir da cabeça, acessando-se os elementos seqüencialmente até que


o ponteiro para o próximo tenha o valor NULL. NULL é uma constante definida no arquivo
stdio.h como 0. Em C, nenhum objeto válido é alocado na posição 0, de modo que este
endereço pode ser usado para sinalizar o fim da lista.

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.

1. typedef struct Func {


2. char nome[20];
3. unsigned codigo;
4. float salario;
5. struct Func *pProx;
6. } RegFunc;

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);

Inserindo novos elementos em listas encadeadas

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:

pNew = (RegFunc *)malloc(sizeof(RegFunc));

pNew aponta para um espaço de memória, alocado no heap, de tamanho


sizeof(RegFunc).

Head 0

pNew

Em seguida é preciso preencher os campos da estrutura recém criada com os dados do


novo funcionário:

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

Desenvolvimento de uma aplicação

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.

O programa deverá executar as seguintes funções:

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;

Podemos visualizar o sistema na forma de um diagrama de estruturas que mostra como o


programa principal está ligado às diversas tarefas que devem ser executadas. Cada módulo
corresponde a um procedimento do programa, exceto o módulo SISTEMA CADASTRO
que corresponde ao programa principal.

SISTEMA
CADASTRO

LER PROCESSAR GRAVAR


ARQUIVO CADASTRO 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

OPÇÃO == FIM DO PROGRAMA

MOSTRAR LER EXECUTAR


MENU OPÇÃO OPÇÃO

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.

Apresentamos a seguir um esboço do programa de processamento do cadastro de


funcionários que acabamos de desenvolver por meio dos diagramas.

enum BOOL {FALSE, TRUE};

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
}

void ExecutarOpcao(int opcao) {


void Incluir(void);
void Alterar(void);
void Consultar(void);
void Excluir(void);
void Listar(void);
void Fim(void);
void (*matriz[6])()={Incluir, Alterar, Consultar,
Excluir, Listar, Fim};

(*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)

Vamos estudar o mesmo programa usando uniões:

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

Observe a sintaxe de declaração de uma union. Ela é idêntica à declaração de uma


struct.
Por que foi impresso o valor de i1 == 3 se, explicitamente, atribuímos
(uniao.i1 = 2)? Porque todos os elementos de uma união compartilham o mesmo
espaço de memória, o qual pode ser referenciado pelo nome de qualquer um dos
elementos. Dessa forma:

&uniao.i1 == &uniao.i2 == &uniao.f1 == &uniao.f2

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)

Definindo e declarando uniões

A sintaxe da definição e declaração de uniões é idêntica à de estruturas, exceto pelo uso da


palavra reservada union. Por exemplo:

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;
.
.
}

Acessando membros da união

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.

Na utilização de uniões, é responsabilidade do programador saber qual foi o tipo mais


recentemente referenciado.

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

A organização da união em memória seria:

teste.i &teste.i.i1==&teste.f.f1 teste.f


&teste.i.i2==&teste.f.f2

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:

typedef unsigned short int uint;

typedef struct byte {


char baixo ;
char alto;
} RegByte;

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

Através da construção acima, podemos referenciar o conteúdo da variável numero como


uma palavra de 16 bits,

numero.word

ou podemos referenciar os bytes alto e baixo individualmente:

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 linguagem C oferece um pacote de funções da biblioteca que nos permite acessar


arquivos de quatro modos diferentes:

• Entrada e saída de caracteres


Os dados são lidos e escritos, um caracter por vez. As funções são análogas às funções
putchar() e getchar()

• Entrada e saída de linhas


Os dados são lidos e escritos como linhas de texto. Análogo ao uso das funções
gets() e puts().

• Entrada e saída formatada


Os dados são lidos e escritos formatados. Análogo ao uso das funções printf() e
scanf().

• Entrada e saída por blocos


Os dados são lidos ou escritos em blocos de bytes cujo tamanho é especificado nas
chamadas às funções. Usadas para ler ou armazenar estruturas, vetores e matrizes.

Estudaremos a seguir cada uma dessas funções.

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;

As informações contidas na estrutura FILE não são de interesse imediato do programador.


A este cabe apenas assegurar a associação (através de um ponteiro) entre qualquer arquivo
aberto e uma estrutura do tipo FILE.

FILE *pFILE;

Abrindo arquivos

Uma vez declarado um ponteiro para um arquivo, é necessário “abrir o arquivo”. A


expressão “abrir o arquivo” significa prepará-lo para operações subseqüentes de leitura
e/ou escrita.

Em C, abrimos um arquivo com a função fopen() a qual devolve um ponteiro para a


estrutura FILE associada ao arquivo recém aberto.

FILE *fopen(char *filename, char *mode);

onde, filename é uma string contendo o nome do arquivo e mode é o modo de abertura
do arquivo.

Exemplo:

FILE *pFILE = fopen("teste.c", "w");

A tabela a seguir apresenta os possíveis modos de abertura de um arquivo.

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.

Erros ao abrir arquivos

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.

Quando da ocorrência de um erro a função fopen() retorna o endereço NULL.

Exemplo:

if((pFILE=fopen("saida.dat", "r"))==NULL) {
puts("Nao pude abrir o arquivo");
return;
}

A fim de simplificar a codificação, não testaremos a condição de erro nos programas


futuros neste Capítulo. No entanto, em qualquer aplicação séria, esse teste deveria ser
incluído.

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

int fclose(FILE *stream);

Todos os buffers associados com o arquivo são esvaziados antes do fechamento do


arquivo.

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:

fgetc(), fputc() <STDIO.H>


fgetc() lê um caracter de um arquivo
fputc() escreve um caracter em um arquivo

Declaração
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);

Comentários
fgetc() retorna o próximo caracter no arquivo especificado.

fputc() escreve o caracter c 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.

Em caso de erro, ou ao atingir o final do arquivo, fgetc() retorna EOF


Em caso de erro, fputc() retorna EOF

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.

void main(int argc, char *argv[]) {


FILE *pFILE;
int ch;

pFILE=fopen(argv[1], "r");
while ((ch=fgetc(pFILE))!=EOF)
putchar(ch);
fclose(pFILE);
}

Fim de arquivo (EOF)

No exemplo anterior, os caracteres do arquivo de entrada foram lidos até que


(ch = EOF). EOF (End Of File) é enviado ao programa pelo sistema operacional quando
este detecta o final do arquivo.

É 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.

Leitura e gravação de linhas

De maneira análoga a leitura e escrita de strings em standard input / standard output


existem na biblioteca C funções para a entrada e saída de “linhas” em arquivos. Chamamos
de linha a uma seqüência de caracteres terminada pelo caracter de nova linha (‘\n’). As
funções para a entrada e saída de linhas são fgets() e fputs().

fgets(), fputs() <STDIO.H>


fgets() lê uma linha do arquivo
fputs() escreve uma linha no arquivo

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.

Em caso de erro, ou ao atingir o final do arquivo, fgets() retorna NULL.


Em caso de erro, fputs() retorna EOF.

Comparação gets() x fgets()

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:

Lendo um arquivo linha a linha: O programa abaixo lê uma seqüência de linhas de um


arquivo dado e as ecoa em stdout.

void main(int argc, char *argv[]) {


FILE *pFILE;
char linha[80];

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.

void main(int argc, char *argv[]) {


FILE *pFILE=fopen(argv[1],"w");
char linha[80];

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().

Entrada e saída formatada

• Saída formatada

Saída formatada é efetuada com a função fprintf(). Esta função é similar a


printf(), exceto que um ponteiro para FILE é tomado como primeiro argumento.

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;

for (i=0; i<DIM; i++)


fprintf(pFILE, "%s %u %g\n",
func[i].nome, func[i].codigo, func[i].salario);
fclose(pFILE);
}

Após a execução do programa, o conteúdo do arquivo saida.dat será:

Jorge 3554 1000


Paulo 1234 500
Carlos 755 720

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");

for(i=0; i<DIM; i++)


fscanf(pFILE, "%s%u%g",
func[i].nome, &func[i].codigo,
&func[i].salario);
fclose(pFILE);
}

Após a execução do programa, o conteúdo das variáveis será:

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

Lendo e gravando registros

As funções fread() e fwrite() possibilitam uma maneira de transferir blocos de


dados do disco para a memória do computador e vice-versa. Elas se mostram como uma
excelente alternativa para armazenar uma grande quantidade de dados numéricos. Isto se
deve ao fato de que, em primeiro lugar, os dados são armazenados em modo binário,
ocupando portanto menos espaço em disco. Em segundo lugar, é possível com uma única
instrução gravar ou ler todo um vetor, ou uma matriz, um registro ou, até mesmo, um vetor
de registros.

Vamos estudar as funções fread() e fwrite() com mais detalhes.

Transferindo blocos de dados para o disco.

A transferência de blocos de dados da memória principal do computador para o disco será


feita com a função fwrite().

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.

O número total de bytes escritos é (n * size)

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");

fwrite(func, sizeof(RegFunc), DIM, pFILE);


fclose(pFILE);
}

Após a execução do programa, o conteúdo do arquivo saida.dat (em hexadecimal e


em ASCII) será:

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.

A transferência de blocos do disco para a memória é feita com a função fread(),


complementar à fwrite().

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.

O número total de bytes lidos é (n * size)

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");

fread(func, sizeof(RegFunc), DIM, pFILE);


fclose(pFILE);
}
194
Após a execução do programa acima, as variáveis do programa valem:

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.

Em todos os exemplos apresentados até agora, os arquivos eram acessados de forma


seqüencial, isto é, em operações de escrita os dados eram escritos em posições contíguas
de memória . Da mesma forma para ler um dado, digamos, no meio de um arquivo, era
necessário ler a primeira posição do arquivo e prosseguir até encontrar o dado desejado.

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");

fseek(pFILE, (DIM-1)*sizeof(RegFunc), 0);


fread(&func[DIM-1], sizeof(RegFunc), 1, pFILE);
printf("nome : %s\n", func[DIM-1].nome);
printf("codigo : %u\n", func[DIM-1].codigo);
printf("salario : %g\n", func[DIM-1].salario);
fclose(pFILE);
}

O valor impresso pelo programa será:

nome : Carlos

196
codigo : 755
salario : 720

o que confirma a correção do programa.

Arquivos padrão

Uma característica interessante da linguagem C é a de que os periféricos são tratados


exatamente como arquivos. Assim, quando um programa inicia a execução, o sistema
operacional abre automaticamente alguns arquivos predefinidos. Estes arquivos, definidos
em <STDIO.H>, sã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.

Seja, por exemplo, a instrução:

fputc(ch, stdout);

Essa instrução imprime um caracter no vídeo e, portanto, é equivalente a:

putchar(ch)

De maneira análoga, a instrução:

ch = fgetc(stdin);

lê um caracter do teclado e é equivalente a:

ch = getchar();

Os arquivos “standard” são automaticamente fechados quando o programa encerra a


execução.

197