Programação de Computadores em C - Carlos Camarão UFMG
Programação de Computadores em C - Carlos Camarão UFMG
Programação de Computadores em C - Carlos Camarão UFMG
Carlos Camarao
Universidade Federal de Minas Gerais
Doutor em Ciencia da Computacao pela Universidade de Manchester, Inglaterra
Anolan Milanes
Centro Federal de Educacao Tecnologica de Minas Gerais
Doutora em Ciencia da Computacao pela PUC-Rio
Luclia Figueiredo
Universidade Federal de Ouro Preto
Doutora em Ciencia da Computacao pela UFMG
12 de Marco de 2016
i
Direitos exclusivos
Copyright
c 2009 by Carlos Camarao, Anolan Milanes e Luclia Figueiredo
E permitida a duplicacao ou reproducao, no todo ou em parte, sob quaisquer formas ou por
quaisquer meios (eletronico, mecanico, gravacao, fotocopia, distribuicao na Web ou outros), desde
que seja para fins nao comerciais.
ii
Conteudo
Prefacio v
1 Programas 3
1.1 Computadores: Maquinas Programaveis . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Algoritmo e Programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3 Funcionamento e Organizacao de Computadores . . . . . . . . . . . . . . . . . . . 4
1.3.1 Linguagem de maquina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3.2 Linguagem de montagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.3 Linguagem de alto nvel, compilacao e interpretacao . . . . . . . . . . . . . 6
1.4 Exerccios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Exerccios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.6 Notas Bibliograficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2 Paradigmas de Programacao 15
2.1 Variavel e Atribuicao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.2 Composicao Sequencial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3 Selecao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.4 Repeticao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.5 Funcoes e Procedimentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.5.1 Blocos, Escopo e Tempo de Vida de Variaveis . . . . . . . . . . . . . . . . . 20
2.6 Outros Paradigmas de Programacao . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7 Exerccios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.8 Notas Bibliograficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3 Primeiros Problemas 25
3.1 Funcoes sobre Inteiros e Selecao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2 Entrada e Sada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.2.1 Entrada e Sada em Arquivos via Redirecionamento . . . . . . . . . . . . . 30
3.2.2 Especificacoes de Formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.3 Numeros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.3.1 Consequencias de uma representacao finita . . . . . . . . . . . . . . . . . . 34
3.4 Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.5 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.6 Enumeracoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.7 Ordem de Avaliacao de Expressoes . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.8 Operacoes logicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.9 Programas e Bibliotecas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.10 Conversao de Tipo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.11 Exerccios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.12 Exerccios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4 Recursao e Iteracao 47
4.1 Multiplicacao e Exponenciacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.2 Fatorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.3 Obtendo Valores com Processos Iterativos . . . . . . . . . . . . . . . . . . . . . . . 53
4.3.1 Nao-terminacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
iii
iv CONTEUDO
5 Arranjos 77
5.1 Declaracao e Criacao de Arranjos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.2 Arranjos criados dinamicamente . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.3 Exemplo de Uso de Arranjo Criado Dinamicamente . . . . . . . . . . . . . . . . . . 79
5.4 Operacoes Comuns em Arranjos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.5 Cadeias de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.5.1 Conversao de cadeia de caracteres para valor numerico . . . . . . . . . . . . 82
5.5.2 Conversao para cadeia de caracteres . . . . . . . . . . . . . . . . . . . . . . 83
5.5.3 Passando valores para a funcao main . . . . . . . . . . . . . . . . . . . . . . 83
5.5.4 Exerccios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.5.5 Exerccios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.6 Arranjo de arranjos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.6.1 Passagem de Arranjos Multidimensionais como Parametros . . . . . . . . . 90
5.7 Inicializacao de Arranjos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
5.8 Exerccios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.9 Exerccios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.10 Notas Bibliograficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6 Ponteiros 97
6.1 Ponteiros e inteiros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
6.2 Ponteiros e arranjos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7 Registros 101
7.1 Declaracoes de tipos com typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
7.2 Ponteiros para registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
7.3 Estruturas de dados encadeadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
7.4 Exerccios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
7.5 Exerccios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
7.6 Notas Bibliograficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
8 Exerccios 109
8.1 ENCOTEL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
8.2 PAPRIMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
Recursos Adicionais
Uma pagina na Internet associada a este livro pode ser encontrada no endereco:
http://www.dcc.ufmg.br/~camarao/ipcc
O texto desse livro e os codigos da maioria dos programas apresentados no livro como exemplos
encontram-se disponveis nesta pagina. Outros recursos disponveis incluem sugestoes de exerccios
adicionais e projetos de programacao, transparencias para uso em cursos baseados neste livro e
referencias para outras paginas da Internet que contem informacoes sobre a linguagem C ou sobre
ferramentas para programacao nessa linguagem.
Ha diversas paginas na Web com informacoes sobre a linguagem C disponveis na Web, assim
como cursos sobre introducao a prgramacao em C, dentre os quais citamos:
v
http://publications.gbdirect.co.uk/c book/: Pagina com a versao gratuita da segunda
edicao do livro The C Book , de Mike Banahan, Declan Brady e Mark Doran, publicado pela
Addison Wesley em 1991.
http://www.cyberdiem.com/vin/learn.html: Learn C/C++ today, de V. Carpenter. Uma
colecao de referencias e tutoriais sobre as linguagens C e C++ disponveis na Internet.
http://c-faq.com/index.html: Perguntas frequentes sobre a linguagem C, e suas respostas
(em ingles).
http://cm.bell-labs.com/cm/cs/who/dmr/chist.html: The Development of the C Lan-
guage, Dennis M. Ritchie (Janeiro de 1993).
http://www.livinginternet.com/i/iw unix c.htm: History of the C Programming Lan-
guage, Bill Stewart (Janeiro de 2000).
http://www.cs.ucr.edu/ nxiao/cs10/errors.htm: 10 Common Programming Mistakes
in C.
A linguagem de programacao padrao ANSI C . B. Kernighan & D.C. Ritchie. Editora Cam-
pus, 1990.
C completo e total. H. Schildt. Editora McGraw-Hill, 1990.a
C: A Reference Manual , Samuel P. Harbison & Guy L. Steele, 5a edicao, Prentice Hall, 2002.
E divirta-se assistindo:
http://www.youtube.com/watch?v=XHosLhPEN3k
1
2
Captulo 1
Programas
Embora seja capaz de executar apenas um pequeno numero de operacoes basicas bastante
simples, um computador pode ser usado, em princpio, para resolver qualquer problema cuja solucao
possa ser obtida por meio de um algoritmo. O segredo e que um computador prove tambem
um conjunto de instrucoes para a combinacao dessas operacoes que, embora tambem reduzido, e
suficiente para expressar qualquer algoritmo. Essa tese, de que o conjunto de operacoes basicas
e instrucoes de um computador e suficiente para expressar qualquer algoritmo, constitui uma
3
CPU
Dispositivos de E/S
ULA
Vdeo
Registradores Fita
Disco
UC
Teclado Impressora
Barramento do sistema
Memria
tese resultante de trabalhos com modelos computacionais distintos usados nas primeiras pesquisas
teoricas em computacao, que se mostraram equivalentes, em termos de o que se poderia computar
com eles. Essas pesquisas lancaram as bases para a ciencia da computacao e para a construcao
dos primeiros computadores (ver Notas Bibliograficas (1.6)).
Como construir algoritmos para a solucao de problemas e como expressar tais algoritmos de
modo adequado usando uma linguagem de programacao constituem os temas centrais deste livro.
Entretanto, faremos agora um pequeno preambulo para estudar brevemente as bases da organizacao
e o funcionamento de um sistema de computacao. Esse conhecimento nos permitira entender como
o computador executa os nossos programas.
4
A memoria do computador consiste em uma sequencia finita de unidades de armazenamento
de dados, cada qual identificada pelo seu endereco, isto e, por um numero inteiro nao-negativo que
corresponde a sua posicao nessa sequencia. Cada unidade de armazenamento de dados da memoria
e comumente chamada de uma palavra. Uma palavra de memoria usualmente e composta de um
pequeno numero de bytes (em geral, 4 ou 8). Cada byte armazena uma sequencia de 8 bits.
O outro grupo de componentes do computador e constitudo pelos seus dispositivos de entrada
e sada, tambem chamados de dispositivos perifericos (ou apenas de perifericos). Os perifericos
sao usados para a comunicacao de dados entre o computador e o mundo externo. O teclado e o
mouse sao exemplos de dispositivos de entrada. A tela, ou monitor, e a impressora sao exemplos de
dispositivos de sada. Alguns dispositivos, como discos e pendrives, constituem dispositivos tanto
de entrada quanto de sada de dados.
A linguagem constituda pelas instrucoes que podem ser diretamente executadas por um com-
putador, representadas na forma de sequencias de bits, e chamada de linguagem de maquina.
5
O codigo de cada uma dessas instrucoes e mostrado, na segunda coluna, a seguir.
Operacao Codigo
CARREGAR valor inteiro em registrador 0000
CARREGAR valor em memoria em registrador 0001
SOMAR valor em registrador com valor em 0010
registrador e guardar resultado no primeiro registrador
DESVIAR execucao se resultado de 0011
instrucao anterior for maior ou igual a zero
Essa sequencia de bits poderia ser um pequeno trecho de um programa escrito em linguagem
de maquina. Como ja mencionamos, tais programas consistem em instrucoes muito basicas, tais
como somar dois numeros, ou comparar se dois numeros sao iguais, ou transferir dados entre a
memoria e um registrador, ou entre a memoria e um dispositivo de entrada e sada, e controlar o
fluxo de execucao das instrucoes de um programa.
Na epoca em que foram construdos os primeiros computadores, os programadores usavam
instrucoes como essas, cada qual formada por aproximadamente uma dezena de bits, para descrever
seus algoritmos. Programar nessa linguagem era uma tarefa trabalhosa e extremamente sujeita
a erros, difceis de detectar e corrigir, que geravam programas difceis de serem estendidos e mo-
dificados. Por esses motivos, os programadores logo passaram a usar nomes para as operacoes e
dados, como mostrado na secao seguinte.
MOV R1 , #5
MOV R2 , x
ADD R1 , R2
JGE L
Um programa escrito nessa forma era entao manualmente traduzido para linguagem de maquina,
e depois carregado na memoria do computador para ser executado.
O passo seguinte foi transferir para o proprio computador essa tarefa de montar o programa
em linguagem de maquina, desenvolvendo um programa para realizar essa tarefa. Esse programa e
chamado de montador (em ingles, assembler ), e a notacao simbolica dos programas que ele traduz
e chamada de linguagem de montagem (em ingles, assembly language).
6
de maquina, tem pouca relacao com as abstracoes usualmente empregadas pelo programador na
construcao de algoritmos para a solucao de problemas.
Para facilitar a tarefa de programacao, e torna-la mais produtiva, foram entao desenvolvidas
novas linguagens de programacao. Essas novas linguagens foram chamadas linguagens de alto nvel ,
por oferecer um conjunto muito mais rico de operacoes e construcoes sintaticas adequadas para
expressar, de maneira mais natural, algoritmos usados na solucao de problemas. Linguagens de
maquina e linguagens de montagem sao chamadas, em contraposicao, linguagens de baixo nvel .
Para que um programa escrito em uma linguagem de alto nvel possa ser executado pelo compu-
tador, ele precisa ser primeiro traduzido para um programa equivalente em linguagem de maquina.
Esse processo de traducao e chamado de compilacao; o programa que faz essa traducao e chamado
de compilador . Um compilador e, portanto, simplesmente um programa tradutor, de programas
escritos em uma determinada linguagem, chamada de linguagem fonte, para programas em outra
linguagem, chamada de linguagem objeto. Os programas fornecidos como entrada e obtidos como
sada de um compilador sao tambem comumente chamados, respectivamente, de programa fonte
(ou codigo fonte) e programa objeto (ou codigo objeto).
Um compilador analisa o texto de um programa fonte para determinar se ele esta sintaticamente
correto, isto e, em conformidade com as regras da gramatica da linguagem e, em caso afirmativo,
gera um codigo objeto equivalente. Caso o programa fonte contenha algum erro, o compilador entao
emite mensagens que auxiliam o programador na identificacao e correcao dos erros existentes.
Outro processo para execucao de um programa em linguagem de alto nvel, em vez da com-
pilacao desse programa seguida pela execucao do codigo objeto correspondente, e a interpretacao
do programa fonte diretamente. Um interpretador e, como o nome indica, um programa que in-
terpreta diretamente as frases do programa fonte, isto e, simula a execucao dos comandos desse
programa sobre um conjunto de dados, tambem fornecidos como entrada para o interpretador. A
interpretacao de programas escritos em uma determinada linguagem define uma maquina virtual,
na qual e realizada a execucao de instrucoes dessa linguagem.
A interpretacao de um programa em linguagem de alto nvel pode ser centenas de vezes mais
lenta do que a execucao do codigo objeto gerado para esse programa pelo compilador. A razao disso
e que o processo de interpretacao envolve simultaneamente a analise e simulacao da execucao de
cada instrucao do programa, ao passo que essa analise e feita previamente, durante a compilacao,
no segundo caso. Apesar de ser menos eficiente, o uso de interpretadores muitas vezes e util,
principalmente devido ao fato de que, em geral, e mais facil desenvolver um interpretador do que
um compilador para uma determinada linguagem.
Esse aspecto foi explorado pelos projetistas de linguagens tais como Java, no desenvolvimento
de sistemas (ou ambientes) para programacao e execucao de programas nessa linguagem: esses
ambientes sao baseados em uma combinacao dos processos de compilacao e interpretacao. Um
ambiente de programacao Java e constitudo de um compilador Java, que gera um codigo de
mais baixo nvel, chamado de bytecodes, que e entao interpretado. Um interpretador de bytecodes
interpreta instrucoes da chamada Maquina Virtual Java (Em ingles JVM Java Virtual Machine)
Esse esquema usado no ambiente de programacao Java nao apenas contribuiu para facilitar a
implementacao da linguagem em grande numero de computadores diferentes, mas constitui uma
caracterstica essencial no desenvolvimento de aplicacoes voltadas para a Internet, pois possibilita
que um programa compilado em um determinado computador possa ser transferido atraves da
rede e executado em qualquer outro computador que disponha de um interpretador de bytecodes.
Outras linguagens interpretadas muito conhecidas sao Phyton e Lua. Lua e uma linguagem de
programacao projetada na PUC-Rio e desenvolvida principalmente no Brasil, formando atualmente
parte do Ginga, o padrao brasileiro de televisao digital.
Ambientes de Programacao
Alem de compiladores e interpretadores, um ambiente de programacao de uma determinada
linguagem de alto nvel oferece, em geral, um conjunto de bibliotecas de componentes ou modulos de
programas, usados comumente no desenvolvimento de programas para diversas aplicacoes. Alem
disso, ambientes de programacao incluem outras ferramentas para uso no desenvolvimento de
programas, como editores de texto e depuradores de programas.
Um editor e um programa usado para criar um arquivo de dados e modificar ou armazenar
dados nesse arquivo. O tipo mais simples de editor e um editor de texto, que permite editar
7
(i.e. criar ou modificar) qualquer documento textual (um texto significando uma sequencia de
caracteres, separados linha por linha). Alguns editores usam caracteres especiais, chamados de
caracteres de controle, para facilitar a visualizacao do texto editado, por exemplo colocando em
destaque (em negrito ou com uma cor diferente) palavras-chave da linguagem.
Um depurador e um programa que oferece funcoes especficas para acompanhamento da execucao
de um programa, com o objetivo de auxiliar o programador na deteccao e identificacao da origem
de erros que possam existir em um programa.
Em um ambiente de programacao convencional, editores, compiladores, interpretadores e depu-
radores sao programas independentes. Em um ambiente integrado de programacao, ao contrario,
as tarefas de edicao, compilacao, interpretacao e depuracao sao oferecidas como opcoes disponveis
em um mesmo programa.
A execucao de programas em um computador e iniciada e controlada por um programa deno-
minado sistema operacional. O sistema operacional controla a operacao em conjunto dos diversos
componentes do computador processador, memoria e dispositivos de entrada e sada assim
como a execucao simultanea de diversos programas pelo computador. A execucao do nucleo do
sistema operacional e iniciada no momento em que o computador e ligado, quando esse nucleo e
transferido do disco para a memoria do computador, permanecendo residente na memoria enquanto
o computador estiver ligado. O nucleo do sistema operacional prove uma interface adequada entre
a maquina e os demais programas do sistema operacional que, por sua vez, oferecem uma interface
adequada entre os diversos componentes do computador e os usuarios e seus programas, em um
ambiente de programacao.
8
13 2
1 6 2
0 3 2
1 1 2
1 0
p = 2q0 + d0
= 2(2q1 + d1 ) + d0 = 22 q1 + 2d1 + d0
..
.
= 2(. . . 2((2qn + dn ) + dn1 ) . . . + d1 ) + d0
= 2n+1 qn + 2n dn + 2n1 dn1 + . . . + 2d1 + d0
= 2n dn + 2n1 dn1 + . . . + 2d1 + d0
2. Uma das operacoes basicas que um computador e capaz de realizar e a operacao de somar
dois numeros inteiros. Como essa operacao e executada em um computador?
Os componentes basicos dos circuitos eletronicos de um computador moderno sao chamados
de portas logicas. Uma porta logica e simplesmente um circuito eletronico que produz um
sinal de sada, representado como 1 ou 0 e interpretado como verdadeiro (V) ou falso (F),
respectivamente, que e o resultado de uma operacao logica sobre os seus sinais de entrada.
Essas operacoes logicas nao, e, ou, ou exclusivo, representadas pelos smbolos
(ou conectivos logicos) , , , , respectivamente sao definidas na Tabela 1.1.
Operacao Resultado
(e) (ou) (ou exclusivo)
Operacao Resultado
op = op = op =
nao
V op V V V F
V F
V op F F V V
F V
F op V F V V
F op F F F F
9
m
v
E
n
NO
E r
OU
booleanos e as operacoes logicas definidas sobre esse conjunto sao chamadas de operacoes
booleanas, ou operacoes da Logica Booleana (ou Logica Proposicional).
A operacao logica e tem resultado verdadeiro se ambos os operandos sao verdadeiros, e
falso em caso contrario. A operacao logica ou tem resultado falso se ambos os operando
sao falsos, e verdadeiro em caso contrario. A operacao logica ou exclusivo tem resultado
verdadeiro se um dos operandos, mas nao ambos, e verdadeiro, e falso caso contrario.
Para entender como portas logicas podem ser usadas para implementar a soma de numeros
inteiros positivos em um computador, considere primeiramente a soma de dois numeros n e
m, representados na base binaria, cada qual com apenas 1 bit, ilustrada a seguir:
n m vai um r
1 + 1 = 10
1 1 1 0
1 + 0 = 01
1 0 0 1
0 + 1 = 01 ou
0 1 0 1
0 + 0 = 00
0 0 0 0
Ao comparar a tabela acima com as operacoes logicas definidas na Tabela 1.1, e facil perceber
que a operacao logica ou exclusivo fornece o bit r do numeral que representa o resultado
da soma n + m: o bit r e igual a 1 se n ou m for igual a 1, mas nao ambos, e e igual a 0,
caso contrario. O bit vai um desse numeral e obtido pela operacao logica e: o bit vai
um e igual a 1 quando n e m sao iguais a 1, e e igual a 0 em caso contrario.
O resultado da soma n + m pode ser, portanto, representado pelo par (n m, n m), em
que o primeiro componente e o bit vai um e o segundo e o bit r do resultado. Note que
m n = (m n) (m n).
Com base nessas observacoes, fica facil construir um circuito para somar dois numeros binarios
n e m, cada qual representado com apenas 1 bit. Esse circuito, chamado de meio-somador ,
e apresentado na Figura 1.3. Smbolos usuais sao empregados, nessa figura, para representar
as portas logicas que implementam as operacoes e, ou e nao.
O meio-somador pode ser usado para construir um circuito que implementa a soma de tres
numeros binarios n, m e p, cada qual representado com apenas 1 bit, usando o fato de que
a operacao de adicao e associativa: n + m + p = (n + m) + p. Sendo n + m = (v1 , r1 ) e
r1 + p = (v2 , r), temos que n + m + p = (v1 v2 , r), uma vez que v1 e v2 nao podem ser
ambos iguais a 1. O circuito logico que implementa a soma n + m + p, chamado de somador
completo, pode ser construdo como mostra a Figura 1.4.
Podemos agora facilmente construir o chamado somador paralelo, para somar numeros intei-
ros, representados no sistema de numeracao binario, com qualquer numero fixo de bits.
A Figura 1.5 ilustra um circuito somador paralelo para somar numeros binarios n e m, cada
qual representado com 3 bits, ABC e DEF, respectivamente.
10
v1
m Meio-
OU v
n r1
Somador
Meio- v2
p Somador r
F r1 = Q
Meio-
C Somador v1
Somador r2 = P
E
B Completo
v2
r3 = N
D Somador
A Completo v3 = M
3. Sabemos que um computador e capaz de operar com numeros inteiros, positivos ou negativos.
Como numeros inteiros negativos sao representados no computador?
Para maior facilidade de armazenamento e de operacao, todo numero e representado, em um
computador, por uma sequencia de bits de tamanho fixo. No caso de numeros inteiros, esse
tamanho e igual ao numero de bits que podem ser armazenados na palavra do computador.
Em grande parte dos computadores modernos, esse tamanho e de 32 bits ou, em computadores
ainda mais modernos, de 64 bits.
Para representar tanto numeros inteiros nao-negativos quanto negativos, um determinado
bit dessa sequencia poderia ser usado para indicar o sinal do numero essa abordagem e
chamada de sinal-magnitude.
A abordagem de sinal-magnitude nao e muito adequada, pois existem nesse caso duas
possveis representacoes para o zero e as operacoes de somar numeros nao e tao simples
quanto no caso da representacao em complemento de dois, usada em todos os computadores
11
modernos.
A caracterstica fundamental dessa representacao e a de que a operacao de somar 1 ao
maior inteiro positivo fornece o menor inteiro negativo. Desse modo existe apenas uma
representacao para o zero e pode-se realizar operacoes aritmeticas de modo bastante simples.
Ilustramos, na Tabela 1.2, a representacao de numeros na notacao de complemento de 2
em um computador com uma palavra de apenas 4 bits. Note que existem 2n combinacoes
distintas em uma palavra de n bits, e portanto e possvel representar 2n numeros inteiros,
que na representacao de complemento de dois compreendem os inteiros na faixa de 2(n1)
a 2(n1) 1.
O complemento de 2 de um numero inteiro positivo p, 0 < p 2n1 , com relacao a n bits,
denotado por cn2 (p), e definido como sendo a representacao na base binaria, com n bits, do
numero positivo 2n p. Na notacao de complemento de 2, um numero inteiro p 0 e
representado na base binaria, com n bits, da maneira usual, e um numero inteiro p < 0 e
representado pelo complemento de 2 do valor absoluto de p, cn2 (|p|).
0 0000 -8 1000
1 0001 -1 1111
2 0010 -2 1110
3 0011 -3 1101
4 0100 -4 1100
5 0101 -5 1011
6 0110 -6 1010
7 0111 -7 1001
Dado um numero inteiro positivo p, uma maneira eficiente para calcular cn2 (p), a partir da
representacao de p na base binaria, pode ser obtida pela observacao de que cn2 (p) = 2n p =
(2n 1) p + 1. Como a representacao de 2n 1 na base binaria consiste de uma sequencia
de n 1s, e facil ver que, para obter o resultado da subtracao (2n 1) p, ou seja, cn2 (p) 1,
tambem chamado de complemento de 1 de p, basta tomar a representacao de p na base
binaria e trocar, nessa representacao, os 1s por 0s e vice-versa. Para obter cn2 (p), precisamos
entao apenas somar 1 ao resultado obtido.
Por exemplo:
12
Usando essa representacao, a adicao de dois numeros inteiros n e m pode ser feita da maneira
usual, sendo descartado o bit vai um obtido mais a esquerda. Por exemplo:
01102 11102
+ 10012 + 11012
= 11112 = 10112
1.5 Exerccios
1. Determine a representacao, no sistema de numeracao binario, de cada um dos seguintes
numeros, escritos na base decimal:
(a) 19 (b) 458
6. Indique como seria feito o calculo das seguintes operacoes, em um computador que utiliza
notacao de complemento de 2 para representacao de numeros e tem palavra com tamanho
de 8 bits:
(a) 57 + (118) (b) (15) + (46)
13
14
Captulo 2
Paradigmas de Programacao
15
como um todo, alem de facilitar o aprendizado de como aplicar esses conceitos no desenvolvimento
de programas.
O nome de uma variavel ou funcao pode ser escolhido livremente pelo programador,
mas e importante que sejam escolhidos nomes apropriados ou seja, mnemonicos, que
lembrem o proposito ou significado da entidade (variavel ou funcao) representada.
Por exemplo, procure usar nomes como soma, media e pi para armazenar, respec-
tivamente, a soma e a media de determinados valores, e uma aproximacao do numero
, em vez de simplesmente, digamos, s, m, p (outras letras que nao s, m e p sao ainda
menos mnemonicas, por nao ter relacao com soma, media e pi). No entanto, e util
procurar usar nomes pequenos, a fim de tornar o programa mais conciso. Por isso, mui-
tas vezes e comum usar nomes como, por exemplo, i e j como contadores de comandos
de repeticao, e n como um numero natural qualquer, arbitrario de modo semelhante
ao uso de letras gregas em matematica. Evite usar nomes como, por exemplo, aux,
que sao pouco significativos e poderiam ser substitudos por nomes mais concisos, caso
nao haja um nome que represente de forma concisa e mnemonica o proposito de uso de
uma variavel ou funcao.
Uma declaracao de variavel pode, opcionalmente, especificar tambem o valor a ser armazenado
na variavel, quando ela e criada isto e, quando uma area de memoria e alocada para essa
variavel.
Em C, a declaracao:
char x ; i n t y = 10; i n t z ;
especifica que x e uma variavel de tipo char, ou seja, que pode armazenar um caractere, e que
y e z sao variaveis de tipo i n t (variaveis inteiras, ou de tipo inteiro). A declaracao da variavel y
especifica que o valor 10 deve ser armazenado nessa variavel, quando ela e criada. Nao sao especi-
ficados valores iniciais para as variaveis x e z; em C, isso significa que um valor inicial indefinido
(determinado de acordo com a configuracao da memoria no instante da execucao do comando de
16
declaracao) e armazenado em cada uma dessas variaveis. A inexistencia de inicializacao implcita
em C, e muitas outras caractersticas da linguagem, tem como principal motivacao procurar pro-
porcionar maior eficiencia na execucao de programas (ou seja, procuram fazer com que programas
escritos em C levem menos tempo para serem executados).
Uma expressao de uma linguagem de programacao e formada a partir de variaveis e constantes,
usando funcoes ou operadores. Por exemplo, f ( x + y ) e uma expressao formada pela aplicacao de
uma funcao, de nome f, a expressao x + y, essa ultima formada pela aplicacao do operador + as
expressoes x e y (nesse caso, variaveis). Toda linguagem de programacao oferece um conjunto de
funcoes e operadores predefinidos, que podem ser usados em expressoes. Operadores sobre valores
de tipos basicos predefinidos em C sao apresentados no captulo a seguir.
O valor de uma expressao (ou valor retornado pela expressao, como e comum dizer, em
computacao) e aquele obtido pela avaliacao dessa expressao durante a execucao do programa. Por
exemplo, y +1 e uma expressao cujo valor e obtido somando 1 ao valor contido na variavel y.
Na linguagem C, cada expressao tem um tipo, conhecido estaticamente de acordo com
a estrutura da expressao e os tipos dos seus componentes. Estaticamente significa durante a
compilacao (ou seja, antes da execucao) ou, como e comum dizer em computacao, em tempo
de compilacao (uma forma de dizer que tem influencia da lngua inglesa). Isso permite que
um compilador C possa detectar erros de tipo, que sao erros devidos ao uso de expressoes em
contextos em que o tipo nao e apropriado. Por exemplo, supondo que + e um operador binario
(deve ser chamado com dois argumentos para fornecer um resultado), seria detectado um erro
na expressao x +, usada por exemplo i n t y = x +; uma vez que nao foram usados dois
argumentos (um antes e outro depois do operador +) nessa expressao. Um dos objetivos do uso de
tipos em linguagens de programacao e permitir que erros sejam detectados, sendo uma mensagam
de erro emitida pelo compilador, para que o programador possa corrigi-los (evitando assim que
esses erros possam ocorrer durante a execucao de programas).
A linguagem C prove os tipos basicos i n t , f l o a t , double, char e void. O tipo inteiro
representa valores inteiros (sem parte fracionaria).
Numeros de ponto flutuante ( f l o a t ou double) contem uma parte fracionaria. Eles sao re-
presentados em um computador por um valor inteiro correspondente a mantissa e um valor inteiro
correspondente ao expoente do valor de ponto flutuante. A diferenca entre f l o a t e double e
de precisao: um valor de tipo double usa um espaco (numero de bits) pelo menos igual, mas em
geral maior do que um valor de tipo f l o a t .
Um valor de tipo char e usado para armanezar um caractere. Em C um valor de tipo char e
um inteiro, sem sinal (com um tamanho menor ou igual ao de um inteiro).
Nao existe valor de tipo void; esse tipo e usado basicamente para indicar que uma funcao nao
retorna nenhum resultado, ou nao tem nenhum parametro.
Um comando de atribuicao armazena um valor em uma variavel. Em C, um comando de
atribuicao tem a forma:
v = e;
A execucao desse comando tem o efeito de atribuir o valor resultante da avaliacao da expressao
e a variavel v. Apos essa atribuicao, nao se tem mais acesso, atraves de v, ao valor que estava
armazenado anteriormente nessa variavel o comando de atribuicao modifica o valor da variavel.
E comum usar, indistintamente, os termos valor armazenado em uma variavel, valor contido
em uma variavel ou, simplesmente, valor de uma variavel.
Note o uso do smbolo = no comando de atribuicao da linguagem C, diferente do seu uso mais
comum, como smbolo de igualdade. Em C, o operador usado para teste de igualdade e ==. Usar =
em vez de == e um erro cometido com frequencia por programadores iniciantes; e bom ficar atento
para nao cometer tal erro.
Note tambem a distincao existente entre o significado de uma variavel v como variavel alvo
de um comando de atribuicao, isto e, em uma ocorrencia do lado esquerdo de um comando de
atribuicao, e o significado de um uso dessa variavel em uma expressao: o uso de v como variavel
alvo de uma atribuicao representa um lugar (o endereco da area de memoria alocada para v,
durante a execucao do programa), e nao o valor armazenado nessa area, como no caso em que a
variavel ocorre em uma expressao.
Por exemplo, supondo que o valor contido em uma variavel inteira x seja 10, apos a execucao
do comando de atribuicao x = x + 1;, o valor contido em x passa a ser 11: o uso de x no
17
lado direito desse comando retorna o valor 10, ao passo que o uso de x do lado esquerdo desse
comando representa uma posicao de memoria, onde o valor 11 e armazenado.
Um programa em linguagem como C pode incluir definicoes de novas funcoes, que podem entao
ser usadas em expressoes desse programa. Em linguagens imperativas, a possibilidade de uso de
comandos em definicoes de funcoes torna possvel que a avaliacao de uma expressao nao apenas
retorne um resultado, mas tambem modifique valores de variaveis. Quando uma expressao tem tal
efeito, diz-se que tem um efeito colateral .
Em C, o proprio comando de atribuicao e uma expressao (com efeito colateral). Por exemplo,
supondo que a e b sao duas variaveis inteiras, podemos escrever:
a = b = b + 1;
c1 ; c2 ;
i n t a ; i n t b ; i n t c ; a = 10; b = 20; c = a + b ;
1. Declarar uma variavel, de nome a, como tendo tipo i n t . Declarar uma variavel significa
alocar uma area de memoria e associar um nome a essa area, que pode conter valores do tipo
declarado.
2. Analogamente, declarar variaveis b e c, de tipo i n t .
3. Em seguida, atribuir o valor 10 a variavel a.
4. Em seguida, atribuir o valor 20 a variavel b.
5. Em seguida, atribuir o valor 30, resultante da avaliacao de a + b, a variavel c.
2.3 Selecao
Outra forma de combinacao de comandos, a selecao, possibilita selecionar um comando para
execucao, conforme o valor de um determinado teste seja verdadeiro ou falso. Em C, a selecao e
feita com o chamado comando if, que tem a seguinte forma:
if ( b ) c1 ; else c2 ;
18
Nesse comando, b tem que ser uma expressao de tipo i n t . A linguagem C nao tem um tipo para
representar diretamente os valores verdadeiro ou falso, e usa para isso o tipo i n t . A convencao
usada e que, em um contexto como o da expressao b, em que um valor verdadeiro ou falso e
esperado, 0 representa falso e qualquer valor diferente de zero representa verdadeiro.
Na execucao do comando i f , se o valor retornado pela avaliacao de b for qualquer valor
direrente de 0, o comando c1 e executado; se o valor retornado for 0, o comando c2 e executado.
A parte else c2 ; (chamada de clausula e l s e ) e opcional. Se nao for especificada, sim-
plesmente nenhum comando e executado no caso em que a avaliacao da expressao b retorna falso.
Em C, para se usar uma sequencia com mais de um comando no lugar de c1 , ou no lugar de c2 ,
devem ser usadas chaves para indicar o incio e o fim da sequencia de comandos. Por exemplo:
Uma sequencia de comandos entre chaves e chamada de um bloco. Note que, em C, se um bloco
for usado no lugar de um comando como no comando i f do exemplo anterior , o caractere
; nao deve ser usado apos o mesmo: o caractere ; e usado como um terminador de comandos,
devendo ocorrer apos cada comando do programa, que nao seja um bloco.
Existe tambem outra forma de comando de selecao, que seleciona um comando para ser execu-
tado, de acordo com o valor de uma expressao, dentre uma serie de possibilidades, e nao apenas
duas, como no caso do comando i f . Esse comando, tratado no Exerccio Resolvido 6 do Captulo
4, tem o mesmo efeito que uma sequencia de comandos i f , com testes sucessivos sobre o valor da
condicao especificada nesse comando.
2.4 Repeticao
Em um comando de repeticao, um determinado comando, chamado de corpo do comando
de repeticao, e executado repetidas vezes, ate que uma condicao de terminacao do comando de
repeticao se torne verdadeira, o que provoca o termino da execucao desse comando.
Cada avaliacao da condicao de terminacao, seguida da execucao do corpo desse comando, e
denominada uma iteracao, sendo o comando tambem chamado comando iterativo.
Para que a condicao de terminacao de um comando de repeticao possa tornar-se verdadeira,
depois de um certo numero de iteracoes, e preciso que essa condicao inclua alguma variavel que
tenha o seu valor modificado pela execucao do corpo desse comando (mais especificamente exista
um comando de atribuicao no corpo do comando de repeticao que modifique o valor de uma variavel
usada na condicao de terminacao).
O comando while e um comando de repeticao, que tem, em C, a seguinte forma, onde b e a
condicao de terminacao, e c, o corpo do comando:
while ( b ) c ;
A execucao desse comando consiste nos seguintes passos: antes de ser executado o corpo c,
a condicao b e avaliada; se o resultado for verdadeiro (isto e, diferente de 0), o comando c e
executado, e esse processo se repete; senao (i.e., se o resultado da avaliacao de b for falso), entao
a execucao do comando while termina.
Devido a esse laco envolvendo b e depois c repetidamente, um comando de repeticao e
tambem chamado de laco (em ingles, loop).
Como exemplo de uso do comando while, considere o seguinte trecho de programa, que atribui
a variavel soma a soma dos valores inteiros de 1 a n:
soma = 0;
i = 1;
while ( i <= n ) {
soma = soma + i ;
i = i + 1;
}
19
Essa sequencia de comandos determina que, primeiramente, o valor 0 e armazenado em soma,
depois o valor 1 e armazenado em i, e em seguida o comando while e executado.
Na execucao do comando while, primeiramente e testado se o valor de i e menor ou igual a
n. Se o resultado desse teste for falso (igual a 0), a execucao do comando termina. Caso contrario,
o corpo do comando while e executado, seguindo-se novo teste etc. A execucao do corpo do
comando while adiciona i ao valor da variavel soma, e adiciona 1 ao valor armazenado na
variavel i, nessa ordem. Ao final da n-esima iteracao, o valor da variavel i sera, portanto, n+1.
A avaliacao da condicao de terminacao, no incio da iteracao seguinte, retorna entao o valor falso
(0), e a execucao do comando while termina.
A linguagem C possui dois outros comandos de repeticao, alem do comando while: os coman-
dos do- while e f o r . Esses comandos tem comportamento semelhante ao do comando while,
e sao abordados no Captulo 4.
i n t main () {
i n t x = 1;
{ i n t x =2;
x = x +1;
}
i n t y = x +3;
}
20
seguinte a sua declaracao, excluindo escopos internos ao bloco de variaveis declaradas com o mesmo
nome. Essa regra costuma ser chamada: definir antes de usar .
Por exemplo, o escopo da variavel x declarada em main e o trecho da funcao main que segue
a declaracao de x no bloco de main (i.e. int x = 1;), excluindo o escopo de x no bloco onde x
e redeclarado (i.e. excluindo o escopo de x correspondente a declaracao i n t x =2;). Note que o
escopo de x declarado em main esta restrito a essa funcao, nao inclui outras funcoes que poderiam
estar definidas antes ou depois de mainq.
O tempo de vida de uma variavel e o intervalo da execucao do programa entre sua criacao e o
termino da existencia da variavel. Em uma linguagem baseada em uma estrutura de blocos, como
C, o tempo de vida de uma variavel, criada em um comando de declaradacao, que ocorre em um
bloco b, e o intervalo entre o incio e o termino da execucao do bloco b (i.e. o intervalo entre o
incio da execucao do primeiro e o ultimo comandos do bloco).
As nocoes de tempo de vida e escopo de variaveis serao mais abordadas no Captulo seguinte,
ao tratarmos de chamadas de funcoes, em particular de chamadas de funcoes recursivas.
P se P1 , P2 , . . . , Pn
21
2.7 Exerccios
1. Qual e o efeito das declaracoes de variaveis durante a execucao do trecho de programa abaixo?
int x;
i n t y = 10;
2. Quais sao os valores armazenados nas variaveis x e y, ao final da execucao do seguinte trecho
de programa?
i n t x ; i n t y = 10;
x = y * 3;
while ( x > y ) {
x = x - 5; y = y + 1;
}
3. Qual e o valor da variavel s ao final da execucao do seguinte trecho de programa, nos dois
casos seguintes:
s = 0;
i f ( a > b ) s = ( a + b )/2;
while ( a <= b ) {
s = s + a;
a = a + 1;
b = b - 2;
}
22
A programacao orientada por objetos explora adicionalmente, em relacao a programacao modu-
lar, o conceito de modulo como um tipo: uma parte de um programa, desenvolvida separadamente,
constitui tambem um tipo, que pode ser usado como tipo de variaveis e expressoes de um programa.
A influencia da linguagem Java se deve, em grande parte, ao enorme crescimento do interesse
por aplicacoes voltadas para a Internet, aliado as caractersticas do sistema de tipos da linguagem,
que favorecem a construcao de programas mais seguros, assim como ao grande numero de classes
e ferramentas existentes para suporte ao desenvolvimento de programas nessa linguagem.
O recente aumento do interesse por linguagens funcionais e devido, em grande parte, aos sis-
temas de tipos dessas linguagens. Os sistemas de tipos de linguagens funcionais modernas, como
ML [20] e Haskell [27, 5], possibilitam a definicao e uso de tipos e funcoes polimorficas, assim como
a inferencia automatica de tipos.
Uma funcao polimorfica e uma funcao que opera sobre valores de tipos diferentes todos eles
instancias de um tipo polimorfico mais geral. No caso de polimorfismo parametrico, essa funcao
apresenta um comportamento uniforme para valores de qualquer desses tipos, isto e, comporta-
mento independente do tipo especfico do valor ao qual a funcao e aplicada. De maneira semelhante,
tipos polimorficos sao tipos parametrizados por variaveis de tipo, de maneira que essas variaveis (e
os tipos polimorficos correspondentes) podem ser instanciadas, fornecendo tipos especficos. Em
pouco tempo de contacto com a programacao funcional, o programador e capaz de perceber que os
tipos das estruturas de dados e operacoes usadas repetidamente na tarefa de programacao sao, em
sua grande maioria, polimorficos. Por exemplo, a funcao que calcula o tamanho (ou comprimento)
de uma lista e polimorfica, pois seu comportamento independe do tipo dos elementos da lista.
Essas operacoes, usadas com frequencia em programas, sao tambem, comumente, funcoes de
ordem superior, isto e, funcoes que recebem funcoes como parametros ou retornam funcoes como
resultado. Por exemplo, a operacao de realizar uma determinada operacao sobre os elementos de
uma estrutura de dados pode ser implementada como uma funcao polimorfica de ordem superior,
que recebe como argumento a funcao a ser aplicada a cada um dos elementos dessa estrutura.
A inferencia de tipos, introduzida pioneiramente na linguagem ML, que tambem introduziu o
sistema de tipos polimorficos, possibilita combinar duas caractersticas convenientes: a seguranca
e maior eficiencia proporcionadas por sistemas com verificacao de erros de tipo em tempo de
compilacao e a flexibilidade de nao requerer que tipos de variaveis e expressoes de um programa
sejam especificados explicitamente pelo programador (essa facilidade era anteriormente encontrada
apenas em linguagens que realizam verificacao de tipos em tempo de execucao, as quais sao, por
isso, menos seguras e menos eficientes).
Estes e varios outros temas interessantes, como o uso de estrategias de avaliacao de expressoes
ate entao pouco exploradas e o modelamento de mudancas de estado em programacao funcional, sao
ainda objeto de pesquisas na area de projeto de linguagens de programacao. O leitor interessado
nesses temas certamente encontrara material motivante nos livros sobre programacao em linguagem
funcional mencionados acima.
Ao leitor interessado em aprender mais sobre o paradigma de programacao em logica e a
linguagem Prolog recomendamos a leitura de [14, 28].
23
24
Captulo 3
Primeiros Problemas
25
/
Primeiros exemplos
D e f i n i c o e s de f u n c o e s
/
i n t quadrado ( i n t x ) { return x * x ; }
i n t somaDosQuadrados ( i n t x , i n t y ) {
return ( quadrado ( x ) + quadrado ( y ));
}
i n t tresIguais ( i n t a , i n t b , i n t c ) {
return (( a == b ) && ( b == c ));
}
i n t eTriang ( i n t a , i n t b , i n t c ) {
// a , b e c p o s i t i v o s e
// c a d a um menor do q u e a soma d o s o u t r o s d o i s
return (a >0) && (b >0) && (c >0) &&
(a < b + c ) && (b < a + c ) && (c < a + b );
}
i n t max ( i n t a , i n t b ) {
i f ( a >= b ) return a ; e l s e return b ;
}
i n t max3 ( i n t a , i n t b , i n t c ) {
return ( max ( max (a , b , c ));
}
2. Em seguida vem o nome da funcao, e depois, entre parenteses, a lista dos seus parametros
(que pode ser vazia).
A especificacao de um parametro consiste em um tipo, seguido do nome do parametro. A
especificacao de cada parametro e separada da seguinte por uma vrgula.
Por exemplo, a declaracao de somaDosQuadrados especifica que essa funcao tem dois
parametros: o primeiro tem tipo i n t e nome x, o segundo tambem tem tipo i n t e nome y.
3. Finalmente, e definido o corpo do metodo, que consiste em um bloco, ou seja, uma sequencia
de comandos, delimitada por { (abre-chaves) e } (fecha-chaves).
return e;
26
Tabela 3.1: Operadores de comparacao
Essa chamada pode ser usada em qualquer contexto que requer um valor de tipo i n t ou, em outras
palavras, em qualquer lugar onde uma expressao de tipo i n t pode ocorrer.
O corpo do metodo tresIguais usa o operador &&, que representa a operacao booleana e.
Tres valores a, b e c sao iguais, se a e igual a b e b e igual a c, o que e expresso pela expressao
(a==b) && (b==c).
Como vimos anteriormente, o smbolo == e usado para comparar a igualdade de dois valores. A
avaliacao de uma expressao da forma e1 == e2 tem resultado verdadeiro (em C, qualquer valor
inteiro diferente de zero) se os resultados da avaliacao de e1 e e2 sao iguais, e falso (em C, o inteiro
zero), caso contrario.
A operacao de desigualdade e representada por !=. Outras operacoes de comparacao (tambem
chamadas operacoes relacionais) sao apresentadas na Tabela 3.1.
O operador && e tambem usado na expressao que determina o valor retornado pela funcao
eTriang \/. O valor dessa expressao sera falso (zero) ou verdadeiro (diferente de zero), conforme
os valores dos parametros da funcao a, b e c constituam ou nao lados de um triangulo. Isso
e expresso pela condicao: os valores sao positivos (a >0) && (b >0) && (c >0) e cada
um deles e menor do que a soma dos outros dois (a < b + c ) && (b < a + c ) && (c < a + b ).
O operador &&, assim como outros operadores logicos que podem ser usados em C, sao descritos
na Secao 3.8.
O metodo max retorna o maximo entre dois valores, passados como argumentos em uma cha-
mada a essa funcao, usando um comando i f . O valor retornado por uma chamada a max (a , b )
e o valor contido em a, se o resultado da avaliacao da expressao a >= b for verdadeiro, caso contrario
o valor contido em b.
Finalmente, no corpo da definicao de max3, o valor a ser retornado pelo metodo e determinado
pela expressao max ( max (a , b ) , c ), que simplesmente usa duas chamadas ao metodo max, de-
finido anteriormente. O resultado da avaliacao dessa expressao e o maximo entre o valor contido
em c e o valor retornado pela chamada max (a , b ), que e, por sua vez, o maximo entre os valores
dados pelos argumentos a e b.
Uma maneira alternativa de definir as funcoes max e max3 e pelo uso de uma expressao
condicional , em vez de um comando condicional. Uma expressao condicional tem a forma:
e ? e1 : e2
27
e2 , em caso contrario. Lembre que em C verdadeiro significa um valor inteiro diferente de zero.
As funcoes max e max3 podem ser entao definidas como a seguir:
i n t max ( i n t a , i n t b ) {
return ( a >= b ? a : b );
}
i n t max3 ( i n t a , i n t b , i n t c ) {
return ( a >= b ? max (a , c ) : max (b , c ));
}
i n t x = 10;
printf ( " Resultado = % d " , x );
faz com que o valor da variavel inteira x (10) seja impresso em notacao decimal, precidido da
sequencia de caracteres " Resultado = ", ou seja, faz com que seja impresso:
Resultado = 10
28
Existem ainda os caracteres x,o,e,f,s,c, usados para leitura e impressao de, respectivamente,
inteiros em notacao hexadecimal e octal, valores de tipo double, com (e e sem (f) parte referente
ao expoente, cadeias de caracteres (strings) e caracteres.
Um numero pode ser usado, antes do caractere indicador do tipo de conversao, para especificar
um tamanho fixo de caracteres a ser impresso (brancos sao usados para completar este numero
mnimo se necessario), assim como um ponto e um numero, no caso de impressao de valores de
ponto flutuante, sendo que o numero indica neste caso o tamanho do numero de dgitos da parte
fracionaria.
A sequencia de caracteres impressa em uma chamada a printf e igual a str mas pode conter
o que sao chamadas especificacoes de formato. Especificacoes de formato contem o caractere %
seguido de outro caractere indicador de um tipo de conversao que deve ser relizada. Especificacoes
de formato podem tambem ocorrer em chamadas a scanf , para conversao do valor lido, como
mostramos a seguir.
Por exemplo:
i n t x = 10;
printf ( " Resultado = % d " , x );
faz com que o valor da variavel inteira x (igual a 10) seja impresso em notacao decimal, precedido
da sequencia de caracteres " Resultado = ", ou seja, faz com que seja impresso:
Resultado = 10
Neste exemplo poderamos ter impresso diretamente a sequencia de caracteres "Resultado =
10), mas o intuito e ilustrar o uso de % d, que sera explorado depois de introduzirmos a funcao
scanf de entrada de dados, a seguir.
Usos da funcao scanf seguem o seguinte formato:
Esse comando deve ser entendido como: leia um valor inteiro do dispositivo de entrada entrada
padrao e armazene esse valor na variavel a.
O dispositivo de entrada padrao e normalmente o teclado, de modo que a operacao de leitura
interrompe a execucao do programa para esperar que um valor inteiro seja digitado no teclado,
seguido da tecla de terminacao de linha (Enter).
O uso de " % d " em uma especifacao de formato indica, como explicado acima, que deve ser
feita uma conversao do valor digitado, em notacao decimal, para um valor inteiro correspondente
(representado como uma sequencia de bits).
O caractere & siginifica que o segundo argumento e o endereco da variavel a (e nao o valor
armazenado nessa variavel). Ou seja, & a deve ser lido como endereco de a, ou referencia para
a. Esse assunto e abordado mais detalhadamente na secao 6.
A execucao do comando scanf acima consiste em uma espera, ate que um valor inteiro seja
digitado no teclado (se o dispositivo de entrada padrao for o teclado, como ocorre se nao houver
redirecionamento do dispositivo de entrada padrao, como explicado na secao 3.2.1) seguido do
caractere de terminacao de linha (Enter), e que o valor digitado seja armazenado na variavel a.
Para ler dois valores inteiros e armazena-los em duas variaveis inteiras a e b, a funcao scanf
pode ser usada como a seguir:
Podemos fazer agora nosso primeiro programa, que le dois inteiros e imprime o maior dentre
eles, como a seguir:
29
#i n c l u d e < stdio .h >
i n t max ( i n t a , i n t b ) {
return (a >= b ? a : b );
}
i n t main () {
i n t v1 , v2 ;
printf ( " Digite dois inteiros " );
scanf ( " % d % d " , & v1 , & v2 );
printf ( " Maior dentre os valores digitados = % d \ n " , max ( v1 , v2 ));
}
A primeira linha deste nosso primeiro programa indica que o arquivo stdio . h deve ser lido
e as definicoes contidas neste arquivo devem ser consideradas para compilacao do restante do
programa. Vamos chamar essa primeira linha de uma diretiva de pre-processamento.
Alem de ler valores, isto e, alem de modificar valores armazenados em variaveis, uma chamada
a funcao scanf tambem retorna um valor. Esse valor e igual ao numero de variaveis lidas, e pode
ser usado para detectar fim dos dados de entrada a serem lidos (isto e, se nao existe mais nenhum
valor na entrada de dados a ser lido). No Captulo 4 mostraremos exemplos de leitura de varios
inteiros ate que ocorra uma condicao ou ate que nao haja mais valores a serem lidos.
Toda diretiva de preprocessamento comeca com o caractere #, e deve ser inserida na primeira
coluna de uma linha. A linguagem C permite usar espacos em branco e dispor comandos e de-
claracoes como o programador desejar (no entanto, programadores usam tipicamente convencoes
que tem o proposito de homegeneizar a disposicao de trechos de programas de modo a facilitar
a leitura). Uma diretiva de preprocessamento no entanto e uma excecao a essa regra de dispor
livremente espacos, devendo comecar sempre na primeira coluna de uma linha. Apos o caractere
# vem o nome do arquivo a ser lido, o qual deve ter uma extensao .h, entre os caracteres < e >, no
caso de um arquivo de uma biblioteca padrao.
Para arquivos com extensao .h definido pelo programador, o nome do arquivo deve ser inserido
entre aspas duplas (como por exemplo em " interface . h ", sendo interface . h o nome do
arquivo).
A diretiva #i n c l u d e < stdio .h > e a diretiva mais comumente usada em programas C.
30
As operacoes de entrada de dados, que seriam feitas a partir do dispositivo de entrada padrao
(usualmente o teclado), sao realizadas entao a partir do arquivo de nome arquivoDeEntrada.
O nome desse arquivo pode ser uma especificacao completa de arquivo.
De modo similar, podemos redirecionar a sada padrao:
Ao chamarmos programa dessa forma, a sada de dados vai ser feito em um arquivo que vai ser
criado com o nome especificado, no caso arquivoDeSaida, em vez de a sada aparecer na tela do
computador.
Podemos e claro fazer o redirecionamento tanto da entrada quanto da sada:
%e pode tambem ser usado, para converter um valor em notacao cientfica. Numeros na notacao
cientfica sao escritos na forma a 10b , onde a parte a e chamada de mantissa e b de expoente.
%x e %o podem ser usados para usar, respetivamente, notacao hexadecimal e octal de cadeias
de caracteres (dgitos), para conversao de um valor inteiro em uma cadeia de caracteres.
As letras lf em %lf sao iniciais de long f l o a t .
%d e %i sao equivalentes para sada de dados, mas sao distintos no caso de entrada, com scanf.
%i considera a cadeia de caracteres de entrada como hexadecimal quando ela e precedida de " 0 x ",
e como octal quando precedida de " 0 ". Por exemplo, a cadeia de caracteres " 031 " e lida como
31 usando %d, mas como 25 usando %i (25 = 3 8 + 1).
Em uma operacao de sada, podem ser especificados varios parametros de controle. A sintaxe
de uma especificacao de formato e bastante elaborada, e permite especificar:
" % n . pt "
onde t e uma letra que indica o tipo da conversao (que pode ser d, i, c, s etc., como vimos acima),
e n, p especificam um tamanho, como explicado a seguir:
31
#i n c l u d e < stdio .h >
i n t main () {
printf ( " %3 d \ n " ,12345);
printf ( " %3 d \ n " ,12);
}
imprime:
12345
12
Se n for o caractere *, isso indica que o tamanho mnimo e indicado como parametro de
scanf antes do valor a ser impresso.
Por exemplo, printf ( " %* d " , 5 , 10) imprime " 10 " com tamanho mnimo 5.
Um valor inteiro p especifica um tamanho mnimo para o numero de dgitos da parte fra-
cionaria de um numero de ponto flutuante, no caso de valor numerico, e especifica o numero
maximo de caracteres impressos, no caso de cadeias de caracteres (fazendo com que uma
cadeia de caracteres com um tamanho maior do que o tamanho maximo especificado seja
truncada).
Por exemplo, o programa:
i n t main () {
printf ( " %3 d \ n " ,12345);
printf ( " %3 d \ n " ,12);\\
printf ( " %10.3 f \ n " ,12.34);\\
printf ( " %10.3 f \ n " ,1234.5678);\\
printf ( " %.3 s " , " abcde " );\\
}
imprime:
12345
12
12.340
1234.568
abc
Se p for o caractere *, isso indica que o tamanho mnimo da parte fracionaria de um valor
de ponto flutuante, ou o tamanho maximo de uma cadeia de caracteres, e especificado como
como parametro de scanf antes do valor a ser impresso.
Por exemplo, printf ( " \%* s " , 3 , " abcde " ) imprime "abc".
Nota sobre uso do formato %c com scanf em programas C executando sob o sistema operacional
Windows, em entrada de dados interativa:
O formato %c nao deve ser usado com scanf em programas C executando sob o sistema
operacional Windows; em vez disso, deve-se usar getChar. Para entender porque, considere as
execucoes dos dois programas a seguir, que vamos chamar de ecoar1.c e ecoar2.c:
32
ecoar1.c
#i n c l u d e < stdio .h >
i n t main () {
char c ;
i n t fim = scanf ( " % c " , & c );
while ( fim != EOF ) {
printf ( " % c " , c );
fim = scanf ( " % c " , & c );
}
return 0;
}
ecoar2.c
#i n c l u d e < stdio .h >
i n t main () {
i n t c = getchar ();
while ( c != EOF ) {
printf ( " % c " , c );
c = getchar ();
}
return 0;
}
3.3 Numeros
A area de memoria reservada para armazenar um dado valor em um computador tem um
tamanho fixo, por questoes de custo e eficiencia. Um valor de tipo i n t em C e usualmente
armazenado, nos computadores atuais, em uma porcao de memoria com tamanho de 32 ou 64 bits,
assim como um numero de ponto flutuante em C, um valor de tipo f l o a t ou double.
Tambem por questoes de eficiencia (isto e, para minimizar tempo ou espaco consumidos),
existem qualificadores que podem ser usados para aumentar ou restringir os conjuntos de valores
numericos inteiros e de numeros de ponto flutuante que podem ser armazenados ou representados
por um tipo numerico em C. Os qualificadores podem ser: short e long.
O tipo char, usado para representar caracteres, e tambem considerado como tipo inteiro, nesse
caso sem sinal. O qualificador unsigned tambem pode ser usado para tipos inteiros, indicando
que valores do tipo incluem apenas inteiros positivos ou zero.
A definicao da linguagem C nao especifica qual o tamanho do espaco alocado para cada variavel
de um tipo numerico especfico (char, short, i n t , long, f l o a t ou double), ou seja, a
linguagem nao especifica qual o numero de bits alocado. No caso de uso de um qualificador
(short ou long), o nome i n t pode ser omitido.
Cada implementacao da linguagem pode usar um tamanho que julgar apropriado. As unicas
condicoes impostas sao as seguintes. Elas usam a funcao s i z e o f , predefinida em C, que retorna
o numero de bytes de um nome de tipo, ou de uma expressao de um tipo qualquer:
33
Implementacoes usam comumente 32 ou 64 bits para variaveis de tipo i n t , 64 ou 128 bits para
long i n t e 16 ou 32 bits para variaveis de tipo short i n t .
Um numeral inteiro e do tipo long se ele tiver como sufixo a letra L, ou l (mas a letra L deve
ser preferida, pois a letra l se parece muito com o algarismo 1). Do contrario, o numeral e do
tipo i n t . Nao existem numerais do tipo short. Entretanto, um numeral do tipo i n t pode ser
armazenado em uma variavel do tipo short, ocorrendo, nesse caso, uma conversao implcita de
i n t para short. Por exemplo, no comando de atribuicao:
short s = 10;
o valor 10, de tipo i n t , e convertido para o tipo short e armazenado na variavel s, de tipo
short. Essa conversao obtem apenas os n bits mais a direita (menos significativos) do valor a ser
convertido, onde n e o numero de bits usados para a representacao de valores do tipo para o qual
e feita a conversao, sendo descartados os bits mais a esquerda (mais significativos) restantes.
Numeros inteiros podem tambem ser representados nos sistemas de numeracao hexadecimal e
octal. No primeiro caso, o numeral deve ser precedido dos caracteres 0x ou OX, sendo representado
com algarismos hexadecimais: os numeros de 0 a 15 sao representados pelos algarismos 0 a 9 e
pelas letras de a ate f, ou A ate F, respectivamente. Um numeral octal e iniciado com o dgito 0
(seguido de um ou mais algarismos, de 0 a 7).
Numeros de ponto flutuante sao numeros representados com uma parte inteira (mantissa) e
outra parte fracionaria, como, por exemplo:
3.4 Caracteres
Valores do tipo char, ou caracteres, sao usados para representar smbolos (caracteres visveis),
tais como letras, algarismos etc., e caracteres de controle, usados para indicar fim de arquivo,
mudanca de linha, tabulacao etc.
34
Cada caractere e representado, em um computador, por um determinado valor (binario). A
associacao entre esses valores e os caracteres correspondentes constitui o que se chama de codigo.
O codigo usado para representacao de caracteres em C e o chamado codigo ASCII (American
Standard Code for Information Interchange). O codigo ASCII e baseado no uso de 8 bits para
cada caractere.
Caracteres visveis sao escritos em C entre aspas simples. Por exemplo: a, 3, *, , %
etc. E preciso notar que, por exemplo, o caractere 3 e diferente do numeral inteiro 3. O primeiro
representa um smbolo, enquanto o segundo representa um numero inteiro.
Caracteres de controle e os caracteres e \ sao escritos usando uma sequencia especial de
caracteres. Por exemplo:
caractere indica
\n terminacao de linha
\t tabulacao
o caractere
Tabulacao significa o uso de um certo numero de espacos, para alcancar a proxima posicao
de uma linha que e dividida em posicoes fixas, tipicamente de 8 em 8 caracteres.
Um valor do tipo char pode ser usado em C como um valor inteiro. Nesse caso, ocorre uma
conversao de tipo implcita, tal como no caso de conversoes entre dois valores inteiros de tipos
diferentes (como, por exemplo, short e i n t ). O valor inteiro de um determinado caractere e
igual ao seu codigo ASCII (isto e, ao valor associado a esse caractere no codigo ASCII).
Valores de tipo char incluem apenas valores nao-negativos. O tipo char e, portanto, diferente
do tipo short, que inclui valores positivos e negativos. Conversoes entre esses tipos, e conversoes
de tipo em geral, sao abordadas na Secao 3.10.
As seguintes funcoes ilustram o uso de caracteres em C:
i n t minusc ( char x ) {
return ( x >= a ) && ( x <= z );
}
i n t maiusc ( char x ) {
return ( x >= A ) && ( x <= Z );
}
i n t digito ( char x ) {
return ( x >= 0 ) && ( x <= 9 );
}
Essas funcoes determinam, respectivamante, se o caractere passado como argumento e uma letra
minuscula, uma letra maiuscula, ou um algarismo decimal (ou dgito).
A funcao a seguir ilustra o uso de caracteres como inteiros, usada na transformacao de letras
minusculas em maiusculas, usando o fato de que letras minusculas e maiusculas consecutivas tem
(assim como dgitos) valores consecutivos no codigo ASCII:
minusc_maiusc ( b )
tem como resultado o valor dado por b + (A - a), que e igual a A + 1, ou seja, B.
Como mencionado na secao 3.4, caracteres podem ser expressos tambem por meio do valor da
sua representacao no codigo ASCII. Por exemplo, o caractere chamado nulo, que e representado
com o valor 0, pode ser denotado por:
35
const int NaoETriang = 0;
const int Equilatero = 1;
const int Isosceles = 2;
const int Escaleno = 3;
i n t tipoTriang ( i n t a , i n t b , i n t c ) {
return ( eTriang (a , b , c ) ?
( a == b && b == c ) ? Equilatero :
( a == b || b == c || a == c ) ? Isosceles :
Escaleno )
) : NaoETriang ;
}
\ x0000
Nessa notacao, o valor associado ao caractere no codigo ASCII (codigo ASCII do caractere)
e escrito na base hexadecimal (usando os algarismos hexadecimais 0 a F, que correspondem aos
valores de 0 a 15, representaveis com 4 bits), precedido pela letra x minuscula.
3.5 Constantes
Valores inteiros podem ser usados em um programa para representar valores de conjuntos
diversos, segundo uma convencao escolhida pelo programador em cada situacao. Por exemplo,
para distinguir entre diferentes tipos de triangulos (equilatero, isosceles ou escaleno), podemos
usar a convencao de que um triangulo equilatero corresponde ao inteiro 1, isosceles ao 2, escaleno
ao 3, e qualquer outro valor inteiro indica nao e um triangulo.
Considere o problema de definir uma funcao tipoTriang que, dados tres numeros inteiros
positivos, determina se eles podem formar um triangulo (com lados de comprimento igual a cada
um desses valores) e, em caso positivo, determina se o triangulo formado e um triangulo equilatero,
isosceles ou escaleno. Essa funcao pode ser implementada em C como na Figura 3.5.
Para evitar o uso de valores inteiros (0, 1, 2 e 3, no caso) para indicar, respectivamente, que
os lados nao formam um triangulo, ou que formam um triangulo equilatero, isosceles ou escaleno,
usamos nomes correspondentes NaoETriangulo, Equilatero, Isosceles e Escaleno.
Isso torna o programa mais legvel e mais facil de ser modificado, caso necessario.
O atributo const, em uma declaracao de variavel, indica que o valor dessa variavel nao pode
ser modificado no programa, permanecendo sempre igual ao valor inicial especificado na declaracao
dessa variavel, que tem entao que ser especificado. Essa variavel e entao, de fato, uma constante.
3.6 Enumeracoes
Em vez de atribuir explicitamente nomes a valores numericos componentes de um mesmo tipo
de dados, por meio de declaracoes com o atributo const, como foi feito na secao anterior, podemos
declarar um tipo enumeracao. Por exemplo, no caso das constantes que representam tipos de
triangulos que podem ser formados, de acordo com o numero de lados iguais, podemos definir:
Por exemplo, o programa da secao anterior (Figura 3.5) pode entao ser reescrito como mostrado
na Figura 3.6. Em C, a palavra reservada enum deve ser usada antes do nome do tipo enumeracao.
O uso de tipos-enumeracoes, como no programa da Figura 3.6, apresenta as seguintes vantagens,
em relacao ao uso de nomes de constantes (como no programa da Figura 3.5):
36
enum TipoTriang { NaoETriang , Equilatero , Isosceles , Escaleno };
enum TipoTriang tipoTriang ( i n t a , i n t b , i n t c ) {
return ( eTriang (a ,b , c ) ?
( a == b && b == c ) ? Equilatero :
( a == b || b == c || a == c ) ? Isosceles :
Escaleno
) : NaoETriang ;
}
37
Tabela 3.2: Operadores aritmeticos em C
Precedencia maior
Operadores Exemplos
*, /, % i * j / k (i * j ) / k
+, - i + j * k i + (j *k )
>, <, >=, <= i < j + k i < (j +k )
==,!= b == i < j b == (i < j )
& b & b1 == b2 b & (b1 == b2 )
^ i ^ j & k i ^ (j & k )
| i < j | b1 & b2 (i < j ) | (b1 & b2 )
&& i + j != k && b1 ((i + j) != k) && b1
|| i1 < i2 || b && j < k (i1 <i2 ) || (b && (j <k ))
? : i < j || b ? b1 : b2 ((i <j ) || b) ? b1 : b2
Precedencia menor
38
Tabela 3.4: Operadores logicos em C
Operador Significado
! Negacao
&& Conjuncao (nao-estrita) (e)
|| Disjuncao (nao-estrita) (ou)
i n t main () {
i n t i = 10 ,
i n t j = ( i =2) * i ;
printf ( " % d " , j );
}
0 && (0/0 == 0)
0 & (0/0 == 0)
39
3.9 Programas e Bibliotecas
Um programa en C consiste em uma sequencia de uma ou mais definicoes de funcoes, sendo que
uma dessas funcoes, de nome main, e a funcao que inicia a execucao do programa. A definicao de
uma funcao de nome main deve sempre estar presente, para que uma sequencia de definicoes de
funcoes forme um programa C.
A assinatura ou interface de uma funcao e uma definicao de uma funcao que omite o corpo
(sequencia de comandos que e executada quando a funcao e chamada) da funcao, mas especifica o
nome, o tipo de cada argumento e do resultado da funcao.
Em uma assinatura, os nomes dos argumentos sao opcionais (mas os tipos sao necessarios).
A assinatura da funcao main de um programa C deve ser:
i n t main ( void )
ou
stdio: contem funcoes para entrada e sada em dispositivos padrao, como printf e scanf
(descritas na secao 3.2).
string: contem funcoes para manipulacao de strings (secao 5.5).
ctype: contem funcoes sobre valores de tipo char, como:
i n t isdigit ( char ): retorna verdadeiro (diferente de zero) se o argumento e um
dgito (de 0 a 9),
isalpha ( char ): retorna verdadeiro se o argumento e uma letra,
i n t isalnum ( char ): retorna verdadeiro se o argumento e uma letra ou um dgito,
islower ( char ): retorna verdadeiro se o argumento e uma letra minuscula,
i n t isupper ( char ): retorna verdadeiro se o argumento e uma letra maiuscula
math: contem funcoes matematicas, como:
double cos ( double ): retorna o cosseno do argumento,
double sen ( double ): retorna o seno do argumento,
40
double exp ( double ): retorna e elevado ao argumento, onde e e a base do logartmo
natural (constante de Euler),
double fabs ( double ): retorna o valor absoluto do argumento,
double sqrt ( double ): retorna a raiz quadrada do argumento,
double pow ( double a , double b ): retorna a b (a elevado a b),
double pi: uma representacao aproximada do numero irracional .
stdlib: contem funcoes diversas, para diversos fins, como:
i n t abs ( i n t ): retorna o valor absoluto do argumento,
long labs ( long ): retorna o valor absoluto do argumento,
void srand ( unsigned i n t ): especifica a semente do gerador de numeros pseudo-
aleatorios rand,
i n t rand (): retorna um numero pseudo-aleatorio entre 0 e RAND_MAX,
i n t RAND_MAX: valor maior ou igual a 32767, usado pelo gerador de numeros pseudo-
aleatorios rand,
atoi, atol, atof convertem, respectivamente, string para valor de tipo i n t , long i n t
e double (veja secao 5.5),
malloc aloca memoria dinamicamente (secao 6);
i n t EXIT_SUCCESS: valor usado para indicar que a execucao de um programa ocor-
reu com sucesso,
i n t EXIT_FAILURE: valor usado para indicar que ocorreu alguma falha na execucao
de um programa.
Na secao 3.9 mostraremos como um programa em C pode ser dividido em varias unidades de
compilacao, cada uma armazenada em um arquivo, e como nomes definidos em uma unidade de
compilacao sao usados em outra unidade.
(t) e
onde t e um tipo e e uma expressao. A conversao de tipo indica que o valor resultante da avaliacao
da expressao e deve ser convertido para o tipo t. Em geral, a conversao de tipo pode resultar
em alteracao de um valor pelo truncamento de bits mais significativos (apenas os bits menos
significativos sao mantidos), no caso de conversao para um tipo t cujos valores sao representados
por menos bits do que valores do tipo da expressao e. No caso de uma conversao para um tipo
t cujos valores sao representados por mais bits do que valores do tipo da expressao e, ha uma
extensao do bit mais a esquerda do valor de e para completar os bits da representacao desse valor
como um valor do tipo t.
41
3.11 Exerccios Resolvidos
1. Escreva um programa que leia tres valores inteiros e imprima uma mensagem que indique se
eles podem ou nao ser lados de um triangulo e, em caso positivo, se o triangulo formado e
equilatero, isosceles ou escaleno.
Solucao:
i n t main () {
int a, b, c;
printf ( " Digite 3 valores inteiros : " );
scanf ( " % d % d % d " , &a , &b , & c );
enum Tipotriang t = tipoTriang (a , b , c );
printf ( " Os valores digitados " );
i f ( t == NaoETriang ) printf ( " nao podem formar um triangulo \ n " );
e l s e printf ( " formam um triangulo \% s \ n " , show ( t ));
}
1 || 0/0 == 0
1 | 0/0 == 0
a ? b : 0
42
Note que o valor da expressao e falso quando o valor de a e falso, independentemente do
valor de b (ou mesmo de a avaliacao de b terminar ou nao, ou ocasionar ou nao um erro).
Antes de ver a resposta abaixo, procure pensar e escrever uma expressao condicional analoga
a usada acima, mas que tenha o mesmo comportamento que a || b.
A expressao desejada (que tem o mesmo significado que a || b) e:
a ? 1 : b
3.12 Exerccios
1. Sabendo que a ordem de avaliacao de expressoes em C e da esquerda para a direita, respeitando
contudo a precedencia de operadores e o uso de parenteses, indique qual e o resultado da
avaliacao das seguintes expressoes (consulte a Tabela 3.3, se necessario):
(a) 2 + 4 - 3
(b) 4 - 3 * 5
(c) (4 - 1) * 4 - 2
(d) 2 >= 1 && 2 != 1
2. A funcao eQuadrado, definida a seguir, recebe como argumentos quatro valores inteiros e
retorna verdadeiro se esses valores podem formar os lados de um quadrado, e falso em caso
contrario.
i n t eQuadrado ( i n t a , i n t b , i n t c , i n t d ) {
// t o d o s o s v a l o r e s s a o p o s i t i v o s , e i g u a i s e n t r e s i
return (a >0) && (b >0) && (c >0) && (d >0) &&
( a == b && b == c && c == d );
}
Escreva um programa que leia quatro valores inteiros e imprima uma mensagem indicando
se eles podem ou nao os lados de um retangulo, usando funcao que, dados quatro numeros
inteiros, retorna verdadeiro se eles podem representar lados de um retangulo, e falso em caso
contrario.
i n t bissexto ( i n t ano ) {
return (( ano % 4 == 0 && ano % 100 != 0)
|| ano % 400 == 0 );
}
4. Defina uma funcao somaD3 que, dado um numero inteiro representado com ate tres al-
garismos, fornece como resultado a soma dos numeros representados por esses algarismos.
Exemplo: somaD3 (123) deve fornecer resultado 6.
Escreva um programa que leia um valor inteiro, e imprima o resultado da soma dos algarismos
do numero lido, usando a funcao somaD3. Note que voce pode supor que o inteiro lido contem
no maximo 3 algarismos (o seu programa deve funcionar corretamente apenas nesses casos).
43
5. Defina uma funcao inverteD3 que, dado um numero representado com ate tres algarismos,
fornece como resultado o numero cuja representacao e obtida invertendo a ordem desses
algarismos. Por exemplo: o resultado de inverteD3 (123) deve ser 321.
Escreva um programa que leia um valor inteiro, e imprima o resultado de inverter os algaris-
mos desse valor, usando a funcao inverteD3. Note que voce pode supor que o inteiro lido
contem no maximo 3 algarismos (o seu programa deve funcionar corretamente apenas nesses
casos).
final f l o a t pi = 3.1415 f ;
Use essa definicao do valor de pi para definir uma funcao que retorna o comprimento apro-
ximado de uma circunferencia, dado o raio.
Escreva um programa que leia um numero de ponto flutuante, e imprima o comprimento de
uma circunferencia que tem raio igual ao valor lido, usando a funcao definida acima. Por
simplicidade, voce pode supor que o valor lido e um numero de ponto flutuante algarismos
(o seu programa deve funcionar corretamente apenas nesse caso).
7. Defina uma funcao que, dados cinco numeros inteiros, retorna verdadeiro (inteiro diferente
de zero) se o conjunto formado pelos 2 ultimos numeros e um subconjunto daquele formado
pelos 3 primeiros, e falso em caso contrario.
Escreva um programa que leia 5 valores inteiros, e imprima o resultado de determinar se o
conjunto formado pelos 2 ultimos e um subconjunto daquele formado pelos tres primeiros,
usando a funcao definida acima. Por simplicidade, voce pode supor que os valores lidos sao
todos inteiros (o seu programa deve funcionar corretamente apenas nesse caso).
8. Defina uma funcao que, dado um valor inteiro nao-negativo que representa a nota de um
aluno em uma disciplina, retorna o caractere que representa o conceito obtido por esse aluno
nessa disciplina, de acordo com a tabela:
Nota Conceito
0 a 59 R
60 a 74 C
75 a 89 B
90 a 100 A
Escreva um programa que leia um valor inteiro, e imprima a nota correspondente, usando a
funcao definida acima. Por simplicidade, voce pode supor que o valor lido e um valor inteiro
(o seu programa deve funcionar corretamente apenas nesse caso).
9. Defina uma funcao que, dados dois caracteres, cada um deles um algarismo, retorna o maior
numero inteiro que pode ser escrito com esses dois algarismos. Voce nao precisa considerar
o caso em que os caracteres dados nao sao algarismos.
Escreva um programa que leia 2 caracteres, cada um deles um algarismo, e imprima o maior
numero inteiro que pode ser escrito com esses dois algarismos. Por simplicidade, voce pode
supor que os valores lidos sao de fato um caractere que e um algarismo (o seu programa deve
funcionar corretamente apenas nesse caso).
10. Escreva uma funcao que, dados um numero inteiro e um caractere representando respec-
tivamente a altura e o sexo de uma pessoa, sendo o sexo masculino representado por M ou
m e o sexo feminino representado por F ou f , retorna o peso supostamente ideal
para essa pessoa, de acordo com a tabela:
homens mulheres
(72, 7 altura) 58 (62, 1 altura) 44, 7
44
Escreva um programa que leia um numero inteiro e um caractere, que voce pode supor que
sejam respectivamente o peso de uma pessoa e um dos caracteres dentre M, m, F, f,
e imprima o peso ideal para uma pessoa, usando a funcao definida acima. Voce pode supor
que os dados de entrada estao corretos (o seu programa deve funcionar corretamente apenas
nesse caso).
45
46
Captulo 4
Recursao e Iteracao
A solucao de qualquer problema que envolva a realizacao de uma ou mais operacoes repetidas
vezes pode ser expressa, no paradigma de programacao imperativo, por meio de um comando de
repeticao (tambem chamado de comando iterativo, ou comando de iteracao), ou usando funcoes
com definicoes recursivas.
Definicoes recursivas de funcoes sao baseadas na mesma ideia subjacente a um princpio de
prova fundamental em matematica o princpio da inducao. A ideia e a de que a solucao de um
problema pode ser expressa da seguinte forma: primeiramente, definimos a solucao desse problema
para casos basicos; em seguida, definimos como resolver o problema para os demais casos, em
termos da solucao para casos mais simples que o original.
Uma maneira alternativa de pensar na solucao desse problema seria pensar na operacao de multi-
plicacao m * n como a repeticao, n vezes, da operacao de adicionar m, isto e:
m n = (m + . . . + m)n vezes
47
Raciocnio analogo pode ser usado para expressar a operacao de exponenciacao, com expoente
inteiro nao-negativo, em termos da operacao de multiplicacao.
O programa a seguir apresenta duas definicoes alternativas, na linguagem C, para computar o
resultado da multiplicacao e da exponenciacao de dois numeros, dados como argumentos dessas
operacoes. As funcoes mult e exp sao definidas usando-se um comando de repeticao. As funcoes
multr e expr sao definidas usando-se chamadas a propria funcao que esta sendo definida, razao
pela qual essas definicoes sao chamadas de recursivas.
Usamos a letra r adicionada como sufixo ao nome de uma funcao para indicar que se trata
de uma versao recursiva da definicao de uma funcao.
i n t mult ( i n t m , i n t n ) {
i n t r =0 , i ;
f o r ( i =1; i <= n ; i ++) r += m ;
return r ;
}
i n t multr ( i n t m , i n t n ) {
i f ( n ==0) return 0;
e l s e return ( m + multr (m , n -1));
}
i n t exp ( i n t m , i n t n ) {
i n t r =1 , i ;
f o r ( i =1; i <= n ; i ++) r *= m ;
return r ;
}
i n t expr ( i n t m , i n t n ) {
i f ( n ==0) return 1;
e l s e return ( m * expr (m , n -1));
}
Como vimos na Secao 2.4, a execucao de um comando de repeticao consiste na execucao do corpo
desse comando repetidas vezes, ate que a condicao associada a esse comando se torne falsa (caso
isso nunca ocorra, a repeticao prossegue indefinidamente). Para entender melhor o significado desse
comando e, portanto, o comportamento das funcoes mult e exp, consideramos seguir a execucao
de uma chamada a funcao mult por exemplo, mult (3 ,2).
Note que essas definicoes sao usadas apenas com fins didaticos, para explicar inicialmente
definicoes recursivas e comandos de repeticao. Essas definicoes nao tem utilidade pratica pois
existe, no caso da multiplicacao, o operador predefinido (*) e, no caso da exponenciacao, a funcao
math . pow (cf. secao 3.9).
A execucao de uma chamada de funcao e iniciada, como vimos na Secao 2.5, com a avaliacao
dos argumentos a serem passados a funcao: no exemplo acima, essa avaliacao fornece os valores
representados pelos literais 3 e 2, do tipo i n t . Esses valores sao atribudos aos parametros m e n,
respectivamente, e o corpo da funcao mult e entao executado.
Depois da execucao do corpo da funcao mult, os parametros, assim como, possivelmente,
variaveis declaradas internamente no corpo da funcao, deixam de existir, isto e, sao desalocados.
Ou seja, o tempo de vida de parametros e variaveis declaradas localmente e determinado pelo bloco
correspondente a funcao.
A execucao do corpo da funcao mult e iniciada com a criacao da variavel r, a qual e atribudo
inicialmente o valor 0. Em seguida, o comando f o r e executado.
O comando f o r e um comando de repeticao (assim como o comando while, introduzido na
Secao 2.4). A execucao do comando f o r usado no corpo da funcao mult consiste simplesmente
em adicionar (o valor contido em) m ao valor de r, a cada iteracao, e armazenar o resultado dessa
adicao em r. O numero de iteracoes executadas e igual a n. Por ultimo, o valor armazenado em
r apos a execucao do comando f o r e retornado, por meio da execucao do comando return r ;.
Consideramos, a seguir, o comando f o r mais detalhadamente.
48
Tabela 4.1: Passos na execucao de mult (3 ,2)
a primeira parte, de inicializacao, atribui valores iniciais a variaveis, antes do incio das
iteracoes. Essa parte de inicializacao so e executada uma vez, antes do incio das iteracoes.
a segunda parte, chamada de teste de continuacao, especifica uma expressao booleana que e
avaliada antes do incio de cada iteracao (inclusive antes da execucao do corpo do comando
for pela primeira vez). Se o valor obtido pela avaliacao dessa expressao for verdadeiro (em
C, diferente de zero), entao o corpo do comando f o r e executado, seguido pela avaliacao da
terceira clausula do cabecalho desse comando, e depois uma nova iteracao e iniciada; caso
contrario, ou seja, se o valor for falso (em C, zero), a execucao do comando f o r termina.
a terceira parte, de atualizacao, contem expressoes que tem o objetivo de modificar o valor de
variaveis, depois da execucao do corpo do comando for, a cada iteracao. No nosso exemplo,
i ++, que representa o mesmo que i = i +1, incrementa o valor de i a cada iteracao.
Vejamos agora, mais detalhadamente, a execucao de uma chamada multr (3 ,2). Cada cha-
mada da funcao multr cria novas variaveis, de nome m e n. Existem, portanto, varias variaveis
com nomes (m e n), devido as chamadas recursivas. Nesse caso, o uso do nome refere-se a variavel
49
Tabela 4.2: Passos na execucao de multr (3 ,2)
local ao corpo da funcao que esta sendo executado. As execucoes das chamadas de funcoes sao
feitas, dessa forma, em uma estrutura de pilha. Chamamos, genericamente, de estrutura de pilha
uma estrutura na qual a insercao (ou alocacao) e a retirada (ou liberacao) de elementos e feita de
maneira que o ultimo elemento inserido e o primeiro a ser retirado.
Como a execucao de chamadas de funcoes e feita na forma de uma estrutura de pilha, em cada
instante da execucao de um programa, o ultimo conjunto de variaveis alocados na pilha corresponde
as variaveis e parametros da ultima funcao chamada. No penultimo espaco sao alocadas as variaveis
e parametros da penultima funcao chamada, e assim por diante. Cada espaco de variaveis e
parametros alocado para uma funcao e chamado de registro de ativacao dessa funcao.
O registro de ativacao de cada funcao e desalocado (isto e, a area de memoria correspondente na
pilha e liberada para uso por outra chamada de funcao) no instante em que a execucao da funcao
termina. Como o termino da execucao da ultima funcao chamada precede o termino da execucao
da penultima, e assim por diante, o processo de alocacao e liberacao de registros de ativacao de
chamadas de funcoes se da como em uma estrutura de pilha.
As variaveis que podem ser usadas no corpo de uma funcao sao apenas os parametros dessa
funcao e as variaveis internas a declaradas (chamadas de variaveis locais da funcao), alem das
variaveis externas, declaradas no mesmo nvel da definicao da funcao, chamadas de variaveis globais.
Variaveis declaradas internamente a uma funcao sao criadas cada vez que uma chamada a essa
funcao e executada, enquanto variaveis globais sao criadas apenas uma vez, quando a execucao do
programa e iniciada ou anteriormente, durante a carga do codigo do programa na memoria.
No momento da criacao de uma variavel local a uma funcao, se nao foi especificado explicita-
mente um valor inicial para essa variavel, o valor nela armazenado sera indeterminado, isto e, sera
o valor ja existente nos bytes alocados para essa variavel na pilha de chamadas de funcoes.
Na Figura 4.2, representamos a estrutura de pilha criada pelas chamadas a funcao multr.
Os nomes m e n referem-se, durante a execucao, as variaveis mais ao topo dessa estrutura. Uma
chamada recursiva que vai comecar a ser executada e circundada por uma caixa. Nesse caso, o
resultado da expressao, ainda nao conhecido, e indicado por ....
Uma chamada de funcao corresponde entao nos seguintes passos, nesta ordem:
50
chamado de argumento.
Por exemplo, na chamada mult (3+1 ,2), 3+1 e 2 sao parametros reais. Os argumentos sao
4 e 2, obtidos pela avaliacao das expressoes 3+1 e 2, respectivamente.
As definicoes de exp e expr seguem o mesmo padrao de computacao das funcoes mult
e multr, respectivamente. A definicao de expr e baseada na seguinte definicao indutiva da
exponenciacao:
mn = (1 se m = 0, senao m mn1 )
Eficiencia
Uma caracterstica importante da implementacao de funcoes, e de algoritmos em geral, e a sua
eficiencia. A definicao alternativa da funcao de exponenciacao apresentada na Figura 4.1 ilustra
bem como implementacoes de uma mesma funcao, baseadas em algoritmos distintos, podem ter
eficiencia diferente. Essa nova definicao e uma implementacao de mn baseada na seguinte definicao
indutiva:
m0 = 1
mn = (mn/2 )2 se n e par (n > 0)
mn = m mn1 se n e mpar (n > 0)
A avaliacao de exp2 (m , n ) produz o mesmo resultado que a avaliacao de exp (m , n ) e de
expr (m , n ), mas de maneira mais eficiente em termos do numero de operacoes necessarias,
e portanto em termos do tempo de execucao. Para perceber isso, observe que a avaliacao de
exp2 (m , n ) e baseada em chamadas recursivas que dividem o valor de n por 2 a cada chamada;
a avaliacao de exp (m , n ), por outro lado, diminui o valor de n de 1 a cada iteracao (assim como
expr (m , n ) diminui o valor de n de 1 a cada chamada recursiva).
i n t exp2 ( i n t m , i n t n ) {
i f ( n == 0) return 1;
e l s e i f ( n % 2 == 0) { // n e p a r
i n t x = exp2 (m , n /2);
return x * x ; }
e l s e return m * exp2 (m , n -1);
}
51
Por exemplo, as chamadas recursivas que ocorrem durante a avaliacao da expressao exp2 (2 ,20)
sao, sucessivamente, as seguintes:
exp2 (2 ,20)
exp2 (2 ,10)
exp2 (2 ,5)
exp2 (2 ,4)
exp2 (2 ,2)
exp2 (2 ,1)
exp2 (2 ,0)
A diferenca em eficiencia se torna mais significativa quando o expoente n e maior. Sao realizadas
da ordem de log2 (n) chamadas recursivas durante a execucao de exp2 (m , n ) uma vez que n
e em media dividido por 2 em chamadas recursivas , ao passo que a execucao de exp (m , n )
requer n iteracoes.
Um exerccio interessante e aplicar a mesma tecnica usada na definicao de exp2 (ou seja, usar
divisao por 2 caso o segundo operando seja par) para obter uma definicao mais eficiente para a
multiplicacao (procure resolver esse exerccio antes de ver sua solucao, no Exerccio Resolvido 2).
Questoes sobre eficiencia de algoritmos sao importantes em computacao, envolvendo aspectos
relativos ao tempo de execucao e ao consumo de memoria (referidos abreviadamente como aspectos
de tempo e espaco). A analise da eficiencia de algoritmos e abordada superficialmente ao longo
deste texto, sendo um estudo mais detalhado desse tema deixado para cursos posteriores.
4.2 Fatorial
Nesta secao, exploramos um pouco mais sobre recursao e iteracao, por meio do exemplo classico
do calculo do fatorial de um numero inteiro nao-negativo n, usualmente definido como:
n! = n (n 1) (n 2) . . . 3 2 1
Duas formas comuns para implementacao dessa funcao sao mostradas a seguir:
i n t fat ( i n t n ) {
i n t f =1 , i ;
f o r ( i =1; i <= n ; i ++) f *= i ;
return f ;
}
i n t fatr ( i n t n ) {
i f ( n == 0) return 1;
e l s e return n * fatr (n -1);
}
A funcao fatr espelha diretamente a seguinte definicao indutiva, que especifica precisamente
o significado dos tres pontos (...) na definicao de n! dada acima:
n! = 1 se n = 0
n! = n (n 1)! em caso contrario
A versao iterativa, fat, adota a regra de calcular o fatorial de n usando um contador (i), que
varia de 1 a n, e uma variavel (f), que contem o produto 1 2 . . . i , obtido multiplicando o valor
de i por f a cada iteracao. Mudando o valor do contador e o valor do produto a cada iteracao
(para valores de i que variam de 1 a n), obtemos o resultado desejado na variavel f (ou seja, n!),
ao final da repeticao. Verifique que, no caso da execucao de uma chamada fat (0), nenhuma
iteracao do comando f o r e executada, e o resultado retornado pela funcao e, corretamente, igual
a 1 (valor inicialmente atribudo a variavel f).
O exemplo a seguir mostra como pode ser implementada uma versao recursiva do processo
iterativo para calculo do fatorial. Nesse caso, o contador de repeticoes (i) e a variavel (f), que
52
contem o produto 1 2 . . . i , sao passados como argumentos da funcao definida recursivamente,
sendo atualizados a cada chamada recursiva.
i n t fatIter ( i n t n , i n t i , i n t f ) {
return ( i > n ? f : fatIter (n , i +1 , f * i );
}
i n t fatr1 ( i n t n ) {
return fatIter (n ,1 ,1);
}
i n t pa1 ( i n t n ) {
int s = 0, i;
f o r ( i =1; i <= n ; i ++) s += i ;
return s ;
}
Assim como para o calculo do fatorial, duas versoes recursivas, que usam processos de calculo
diferentes, podem ser implementadas. Essas definicoes sao mostradas a seguir. A primeira (pa1r)
espelha diretamente a definicao indutiva de
n
X
i
i=1
i n t pa1r ( i n t n ) {
return ( n ==0 ? 0 : n + pa1r (n -1));
}
i n t pa1Iter ( i n t n , i n t i , i n t s ) {
return ( i > n ? s : pa1Iter (n , i +1 , s + i ));
}
i n t pa1rIter ( i n t n ) {
return pa1Iter (n ,1 ,0);
}
E simples modificar essas definicoes para o calculo de somatorios dos termos de series aritmeticas
com passo diferente de 1. Essas modificacoes sao deixadas como exerccio para o leitor.
Nenhuma dessas implementacoes constitui a maneira mais eficiente para calcular a soma de
termos de uma serie aritmetica, pois tal valor pode ser calculado diretamente, usando a formula:
53
n
X n(n + 1)
i=
i=1
2
O leitor com interesse em matematica pode procurar deduzir essas formulas. Conta a historia
que a formula acima foi deduzida e usada por Gauss quando ainda garoto, em uma ocasiao em que
um professor de matematica pediu-lhe, como punicao, que calculasse a soma dos 1000 primeiros
numeros naturais. Deduzindo a formula em pouco tempo, Gauss respondeu a questao prontamente.
De fato, e possvel deduzir a formula rapidamente, mesmo sem uso de papel e lapis, depois de
perceber que a soma
1 + 2 + ... + (n 1) + n
+ n + (n 1) + ... + 2 + 1
pode ser feita com artifcio semelhante i.e. multiplicando (x0 + . . . + xn ) por (x 1).
Essas definicoes servem, no entanto, para a ilustrar a implementacao de somatorios semelhantes,
como nos exemplos a seguir.
Considere a implementacao de uma funcao para calculo de:
n
X
xi
i=0
Tambem nesse caso, podemos usar um comando de repeticao, como na implementacao de pa1,
sendo adequado agora usar uma variavel adicional para guardar a parcela obtida em cada iteracao.
Essa variavel e chamada parc na implementacao apresentada a seguir, que calcula o somatorio
das parcelas da serie geometrica acima, para um dado x e um dado n:
i n t pg ( i n t n , i n t x ) {
i n t s = 1 , parc = x , i ;
f o r ( i =1; i <= n ; i ++) {
s += parc ; parc *= x ;
}
return s ;
}
O uso de parc em pg evita que seja necessario calcular x i (ou seja, evita que uma operacao
de exponenciacao tenha que ser efetuada) a cada iteracao. Em vez disso, a parcela da i-esima
iteracao (contida em parc), igual a x i 1 , e multiplicada por x para obtencao da parcela da
iteracao seguinte.
Para a implementacao de um somatorio, devemos, portanto, decidir se cada parcela a ser
somada vai ser obtida a partir da parcela anterior (e nesse caso devemos declarar uma variavel,
como a variavel parc, usada para armazenar o valor calculado para a parcela em cada iteracao),
ou se cada parcela vai ser obtida por meio do proprio contador de iteracoes. Como exemplo desse
segundo caso, considere a implementacao do seguinte somatorio:
n
X 1
i=1
i
54
f l o a t so ma Se ri eH ar mo ni ca ( i n t n ) {
f l o a t s = 0.0 f , i ;
f o r ( i =1; i <= n ; i ++)
s += 1/( f l o a t ) i ;
return s ;
}
Na maioria das vezes, uma parcela pode ser calculada de maneira mais facil e eficiente a partir da
parcela calculada anteriormente. Em varios casos, esse calculo nao usa a propria parcela anterior,
mas valores usados no calculo dessa parcela. Por exemplo, considere o seguinte somatorio, para
calculo aproximado do valor de :
1 1 1
= 4 (1 + + . . .)
3 5 7
Nesse caso, devemos guardar nao o valor mas o sinal da parcela anterior (para inverte-lo) e
o denominador usado para calculo dessa parcela (ao qual devemos somar 2 a cada iteracao):
f l o a t piAprox ( i n t n ) {
f l o a t s = 0.0 f , denom = 1.0 f ; i n t sinal = 1 , i ;
f o r ( i =1; i <= n ; i ++) {
s += sinal / denom ;
sinal = - sinal ;
denom += 2;
}
return 4 * s ;
}
Em nosso ultimo exemplo, vamos definir uma funcao para calcular um valor aproximado para
ex , para um dado x, usando a formula:
Para calcular a parcela a ser somada em cada iteracao i do comando de repeticao, a imple-
mentacao usa o denominador i ! e o numerador x i , calculado na parcela anterior. Optamos pelo
uso de um comando while (em vez do f o r ), para ilustracao:
f l o a t eExp ( f l o a t x , i n t n ) {
f l o a t s = 1.0 f ; i n t i =1;
f l o a t numer = x ; i n t denom = 1;
while (i <= n ) {
s += numer / denom ;
i ++;
numer *= x ;
denom *= i ;
}
return s ;
}
55
ate o final do bloco em que o comando while ocorre, mas no caso do comando f o r seu escopo e
apenas o comando f o r , nao podendo ser usada em comandos seguintes ao comando f o r .
Outro comando de repeticao que pode ser usado em programas C e o comando do-while. Esse
comando tem a forma:
do c;
while (b);
onde $c$ e um comando e $b$ e uma expressao que denota um valor falso ou verdadeiro. Esse
comando e bastante semelhante ao comando while. No caso do comando do-while, no entanto, o
comando $c$ e executado uma vez, antes do teste de continuacao ($b$). Por exemplo, considere
o seguinte trecho de programa, que imprime os inteiros de 1 a 10, no dispositivo de sada padrao:
i n t i = 1;
do { printf ( " % d " , i );
i ++;
} while ( i <= 10);
4.3.1 Nao-terminacao
Pode ocorrer, em princpio, que a avaliacao de certas expressoes que contem smbolos definidos
recursivamente, assim como a execucao de certos comandos de repeticao, nao termine. Considere,
como um exemplo extremamente simples, a seguinte definicao:
Essa declaracao especifica que a funcao infinito retorna um valor do tipo i n t . Qual seria
esse valor? A avaliacao de uma chamada a funcao infinito nunca termina, pois envolve, sempre,
uma nova chamada a essa funcao. O valor representado por uma chamada a essa funcao nao e,
portanto, nenhum valor inteiro. Em computacao, qualquer tipo, predefinido ou declarado em um
programa, em geral inclui um valor especial, que constitui um valor indefinido do tipo em questao
e que representa expressoes desse tipo cuja avaliacao nunca termina ou provoca a ocorrencia de
um erro (como, por exemplo, uma divisao por zero).
Muitas vezes, um programa nao termina devido a aplicacao de argumentos a funcoes cujo
domnio nao engloba todos os valores do tipo da funcao. Essas funcoes sao comumente chamadas,
em computacao, de parciais. Por exemplo, a definicao de fat, dada anteriormente, especifica
que essa funcao recebe como argumento um numero inteiro e retorna tambem um numero inteiro.
Mas note que, para qualquer n < 0, a avaliacao de fat ( n ) nao termina. A definicao de fat
poderia, e claro, ser modificada de modo a indicar a ocorrencia de um erro quando o argumento e
negativo.
Considere agora a seguinte definicao de uma funcao const1 que retorna sempre 1, para
qualquer argumento:
i n t const1 ( i n t x ) { return 1; }
A avaliacao da expressao:
nunca termina, apesar de const1 retornar sempre o mesmo valor (1), para qualquer argumento
(o qual, nesse caso, nao e usado no corpo da funcao) cuja avaliacao termina. Isso ocorre porque,
em C, assim como na grande maioria das linguagens de programacao, a avaliacao dos argumentos
de uma funcao e feita antes do incio da execucao do corpo da funcao.
56
4.4 Exerccios Resolvidos
1. Defina recursivamente o significado do comando while.
Solucao: Esse comando pode ser definido recursivamente pela seguinte equacao:
while ( b ) c =
i f ( b ) { c ; while ( b ) c }
Obs.: A notacao = e usada acima para expressar uma equacao matematica.
2. Defina uma funcao mult2 que use a mesma tecnica empregada na definicao de exp2. Ou
seja, use divisao por 2 caso o segundo operando seja par, para obter uma implementacao da
operacao de multiplicacao mais eficiente do que a apresentada em multr.
Exemplos do uso desse algoritmo sao encontrados em um dos documentos matematicos mais
antigos que se conhece, escrito por um escrivao egpcio (Ah-mose), por volta de 1700 a.C.
Solucao:
s t a t i c i n t mult2 ( i n t m , i n t n ) {
i f ( n == 0) return 0;
e l s e i f ( n % 2 == 0) { // n e p a r
i n t x = mult2 (m , n /2);
return x + x ; }
e l s e return m + mult2 (m , n -1);
}
3. De uma definicao recursiva para uma funcao pgr que espelhe o processo iterativo de pg.
Em outras palavras, defina recursivamente uma funcao que, dados um numero inteiro x e
um numero inteiro nao-negativo n, retorne o valor do somatorio:
n
X
xi
i=0
obtendo cada termo do somatorio a partir do termo anterior (pela multiplicacao desse termo
anterior por x ) e passando como argumento, em cada chamada recursiva, o termo e o so-
matorio obtidos anteriormente.
Escreva um programa que leia repetidamente pares de valoresPninteiros x e n, ate que o valor
de n seja zero ou negativo, e imprima o valor do somatorio i=0 xi , usando a funcao pgr.
Solucao:
i n t pgIter ( i n t x , i n t n , i n t i , i n t s , i n t t ) {
i f ( i > n ) return t ;
e l s e { i n t sx = s * x ;
return pgIter (x , n , i +1 , sx , t + sx );
}
}
i n t pgr ( i n t x , i n t n ) {
return pgIter (x ,n ,1 ,1 ,1);
}
i n t main () {
int x, n;
while (1) {
scanf ( " % d % d " , &x , & n );
i f ( n <= 0) break ;
printf ( " % d " , pgr (x , n ));
}
}
57
4. Escreva uma definicao para funcao exp2, definida na Figura 4.1, usando um comando de
repeticao, em vez de recursao.
Solucao:
i n t exp2 ( i n t m , int n) {
i n t r = 1;
while ( n != 0) {
i f ( n % 2 == 0) {
n = n / 2;
m = m * m;
} else { r = r * m;
n = n - 1;
}
return r ;
}
A definicao recursiva apresentada na Figura 4.1 e mais simples, pois espelha diretamente a
definicao indutiva de exp2, dada anteriormente. A definicao acima usa um esquema nao-
trivial (nao diretamente ligado a definicao da funcao) para atualizacao do valor de variaveis
no corpo do comando de repeticao.
5. Defina uma funcao para calcular o maximo divisor comum de dois numeros inteiros positivos.
Solucao: O algoritmo de Euclides e um algoritmo classico e engenhoso para calculo do
maximo divisor comum de dois numeros inteiros positivos:
mdc(a, b) = ase b = 0, senao mdc(b, a%b)
O operador % e usado acima como em C, ou seja, a%b representa o resto da divisao inteira de
a por b (a e b sao numeros inteiros). O algoritmo original de Euclides (escrito no famoso livro
Elementos, por volta do ano 3 a.C.) usava mdc(b, a b) em vez de mdc(b, a%b), na definicao
acima, mas o uso da divisao torna o algoritmo mais eficiente.
Note que esse algoritmo funciona se a b ou em caso contrario. Se a < b, a chamada
recursiva simplesmente troca a por b (por exemplo, mdc(20, 30) e o mesmo que mdc(30, 20)).
Em C, podemos escrever entao:
i n t mdc ( i n t a , i n t b ) {
return ( b == 0 ? a : mdc (b , a % b );
}
Podemos tambem escrever a funcao mdc usando um comando de repeticao, como a seguir:
i n t mdc ( i n t a , i n t b ) {
int t;
i f (a < b ) return mdc (b , a );
while (b >0) { t = a ; a = b ; b = t % b ; };
return a ;
}
6. Defina uma funcao calc que receba como argumentos um caractere, que indica uma operacao
aritmetica (+, -, * e /), e dois valores de ponto flutuante, e retorne o resultado da
aplicacao da operacao sobre os dois argumentos. Por exemplo: calc ( + , 1.0 , 1.0)
deve retornar 2.0 e calc ( * ,2.0 ,3.0) deve retornar 6.0.
Solucao: Vamos ilustrar aqui o uso do comando switch, existente em C, que permite escolher
um dentre varios comandos para ser executado. O comando switch tem a forma:
58
switch (e) {
case e1 : c1 ;
case e2 : c2 ;
...
case en : cn ;
}
Se o valor de e nao for igual a nenhum dos valores ei , para i n, um caso d e f a u l t pode
ser usado. O caso d e f a u l t pode ser usado no lugar de case ei , para qualquer ei , para
algum i = 1, . . . , n, mas em geral deve ser usado como ultimo caso. Se d e f a u l t nao for
especificado (e comum dizer se nao existir nenhum caso default), o comando switch
pode terminar sem que nenhum dos comandos ci , para i = 1, . . . , n, seja executado (isso
ocorre se o resultado da avaliacao de e nao for igual ao valor de nenhuma das expressoes ei ,
para i = 1, . . . , n).
A necessidade do uso de um comando break, sempre que se deseja que seja executado apenas
um dos comandos do comando switch, correspondente a um determinado caso (expressao),
e em geral reconhecida como um ponto fraco do projeto da linguagem C.
A expressao e, no comando switch, deve ter tipo i n t , short, byte ou char, devendo
o seu tipo ser compatvel com o tipo das expressoes e1 , . . . , en . As expressoes e1 , . . . , en tem
que ser valores constantes (e distintos).
Um comando switch pode ser tambem precedido de um rotulo um nome seguido do
caractere :. Nesse caso, um comando break pode ser seguido desse nome: isso indica
que, ao ser executado, o comando break causa a terminacao da execucao do comando
switch precedido pelo rotulo especificado.
7. Escreva uma definicao para a funcao raizq, que calcula a raiz quadrada de um dado valor
x, com erro de aproximacao menor que 0.0001. Solucao: A raiz quadrada de x e um numero
y tal que y 2 = x. Para certos numeros, como, por exemplo, 2, a sua raiz quadrada e um
numero que teria que ser representado com infinitos algarismos na sua parte decimal, nao
sendo possvel obter essa representacao em um tempo finito. Desse modo, o calculo desses
valores e feito a seguir, com um erro de aproximacao inferior a um valor preestabelecido
nesse caso, 0.0001. Vamos chamar de e o valor 0.0001. Denotando por |x| o valor absoluto
de x, o valor y a ser retornado por raizq(x ) deve ser tal que:
59
y 0 e |y 2 x| < e
Para definir a funcao raizq, vamos usar o metodo de aproximacoes sucessivas de Newton.
Para a raiz quadrada, o metodo de Newton especifica que, se yi e uma aproximacao para x,
entao uma aproximacao melhor e dada por:
y1 = (2 + 2/2)/2 = 1.5
y2 = (1.5 + 2/1.5)/2 = 1.4167
y3 = (1.4167 + 2/1.4167)/2 = 1.4142157 . . .
Repetindo esse processo, podemos obter aproximacoes para a raiz quadrada de 2 com qualquer
precisao desejada (sujeitas, e claro, as limitacoes da representacao de numeros do computa-
dor). A implementacao da funcao raizq pode ser feita usando um comando de repeticao,
como mostrado abaixo.
Um templo, na cidade de Hanoi, contem tres torres de diamante, em uma das quais
Deus colocou, quando criou o mundo, 64 discos de ouro, empilhados uns sobre os
outros, de maneira que os discos diminuem de tamanho da base para o topo, como
mostrado a seguir. Os monges do templo trabalham sem cessar para transferir,
um a um, os 64 discos da torre em que foram inicialmente colocados para uma das
duas outras torres, mas de forma que um disco nunca pode ser colocado em cima
de outro menor. Quando os monges terminarem de transferir todos os discos para
uma outra torre, tudo virara po, e o mundo acabara.
A questao que se coloca e: supondo que os monges trabalhem tao eficientemente quanto
possvel, e consigam transferir 1 disco de uma torre para outra em 1 segundo, quanto tempo
decorreria (em segundos) desde a criacao ate o fim do mundo?
Solucao: Uma solucao recursiva para o problema e baseada na ideia ilustrada abaixo. Nessa
solucao, supoe-se, como hipotese indutiva, que se sabe como transferir n 1 discos de uma
60
torre para outra (sem colocar um disco em cima de outro menor); o caso base consiste em
transferir um disco de uma torre para outra vazia. O numero total de movimentacoes de n
discos e definido indutivamente por:
f (1) = 1
f (n) = 2 f (n 1) + 1
A B C
A B C
A B C
Cada valor da sequencia de valores definida por f (n) representa, de fato, o menor numero
de movimentacoes requeridas para mover n discos de uma torre para outra, satisfazendo o
requerimento de que um disco nunca pode ser colocado sobre outro de diametro menor. Para
perceber isso, basta notar que, para mover um unico disco d, digamos, da torre A para a
torre B, e preciso antes mover todos os discos menores que d para a torre C. Portanto, a
resposta a questao proposta anteriormente e dada por f (64). Um resultado aproximado e
1, 844674 1019 segundos, aproximadamente 584,5 bilhoes de anos.
A definicao de f estabelece uma relacao de recorrencia para uma sequencia de valores f (i),
para i = 1, . . . , n (uma relacao de recorrencia para uma sequencia e tal que cada termo e
definido, por essa relacao, em termos de seus predecessores). A primeira equacao na definicao
de f e chamada condicao inicial da relacao de recorrencia.
Podemos procurar obter uma formula que define diretamente, de forma nao-recursiva, o valor
61
de f (n), buscando estabelecer um padrao que ocorre no calculo de f (n), para cada n:
f (1) = 1
f (2) = 2 f (1) + 1 = 2 1 + 1 = 2 + 1
f (3) = 2 f (2) + 1 = 2(2 + 1) + 1 = 22 + 2 + 1
f (4) = 2 f (3) + 1 = 2(22 + 2 + 1) + 1 = 23 + 22 + 2 + 1
...
f (n) = 2 f (n 1) + 1 = 2n1 + 2n2 + . . . + 2 + 1
O leitor interessado pode provar (usando inducao sobre n) que, para todo n, a definicao
recursiva de f de fato satisfaz a equacao f (n) = 2n 1.
9. O problema descrito a seguir foi introduzido em 1202 por Fibonacci tambem conhecido
como Leonardo de Pisa, e considerado como o maior matematico europeu da Idade Media:
Escreva um programa para solucionar esse problema, imprimindo o numero de coelhos exis-
tentes no final do ano.
Solucao: Note que: 1) o numero de (pares de) coelhos vivos no final de um mes k e igual
ao numero de (pares de) coelhos vivos no final do mes k 1 mais o numero de (pares de)
coelhos que nasceram no mes k; e 2) o numero de (pares de) coelhos que nasceram no mes k
e igual ao numero de (pares de) coelhos vivos no mes k 2 (pois esse e exatamente o numero
de coelhos que geraram filhotes no mes k).
Portanto, a quantidade de coelhos vivos ao fim de cada mes n e dada pelo numero fib(n) da
sequencia de numeros definida a seguir, conhecida como sequencia de Fibonacci:
fib(0) = 0
fib(1) = 1
fib(n) = fib(n 1) + fib(n 2) se n > 1
i n t fib ( i n t n ) {
i f ( n ==0) return 0;
e l s e i f ( n ==1) return 1;
e l s e return fib (n -1) + fib (n -2);
}
i n t main () {
printf ( " % d " , fib (12));
}
O numero de pares de coelhos no final do decimo segundo mes, fib (12), e igual a 144 (ou
seja, o numero de coelhos e igual a 288).
A funcao fib definido no programa acima e muito ineficiente, pois repete muitos calculos, de-
vido as chamadas recursivas a fib (n -1) e fib (n -2) (cada chamada de fib ( n ) envolve
calcular fib (n -1) e fib (n -2), e a chamada a fib (n -2) envolve chamar fib (n -1)
outra vez). A definicao a seguir e muito mais eficiente:
62
i n t main () {
i n t n , m , col = 1 , i ;
printf ( " Imprime 1 a n em notacao unaria , sem multiplos de m \ n " );
printf ( " Digite n e m : " );
scanf ( " % d % d " , &n , & m );
f o r ( i =1; i <= n ; i ++) {
printf ( " \ n " );
i f ( i % m == 0) continue ;
f o r ( col =1; col <= i ; col ++) printf ( " | " );
}
return EXIT_SUCCESS ;
}
i n t fib1 ( i n t n , i n t r1 , i n t r ) {
return ( n ==0 ? r : fib1 (n -1 , r , r1 + r );
}
i n t fib ( i n t n ) {
return ( n == 0 ? 0 : fib1 (n ,0 ,1)); }
}
i n t fib ( i n t n ) {
i n t r1 = 0 , r = 1 , t , i ;
f o r ( i =2; i <= n ; i ++) {
t = r1 ; r1 = r ; r = r + t ; }
return r ;
}
10. Os comandos break e continue podem ser usados no corpo de um comando de repeticao.
A execucao de um comando break causa o termino da repeticao e a execucao de um comando
continue causa o incio imediato de uma proxima iteracao. O comando continue prove
uma forma de saltar determinados casos em um comando de repeticao, fazendo com que o
controle passe para a iteracao seguinte.
O exemplo a seguir imprime a representacao unaria (usando o smbolo |) de todos os
numeros inteiros de 1 ate n que nao sao multiplos de um determinado valor inteiro m > 1. O
resultado da execucao de Exemplo continue 20 5 e mostrado abaixo.
Os comandos break e continue podem especificar um rotulo, de mesmo nome do rotulo
colocado em um comando de repeticao. No caso de um comando break, o rotulo pode ser
colocado tambem em um bloco ou em um comando switch (veja o Exerccio Resolvido 6).
Como ilustra esse exemplo, um comando continue sem um rotulo indica o incio da exe-
cucao da proxima iteracao do comando de repeticao mais interno em relacao a esse comando
continue. Analogamente, um comando break sem um rotulo indica o termino da execucao
do comando de repeticao ou comando switch mais interno em relacao a esse comando
break.
O comando break prove uma forma de sair de um comando de repeticao, que pode ser, em
algumas situacoes, mais facil (ou mais conveniente do que outras alternativas). Em alguns
casos, pode ser mais facil testar uma condicao internamente ao comando de repeticao e usar
63
Impressao de valores de 1 a n em notacao unaria, sem multiplos de m
Digite n e m: 20 5
|
||
|||
||||
||||||
|||||||
||||||||
|||||||||
|||||||||||
||||||||||||
|||||||||||||
||||||||||||||
||||||||||||||||
|||||||||||||||||
||||||||||||||||||
||||||||||||||||||
i n t fat ( i n t x ) {
i n t fatx = 1 , i ;
f o r ( i =1; i <= x ; i ++) fatx \/} *= i ;
return fatx ;
}
i n t main () {
int v;
while (1) {
printf ( " Digite um valor inteiro : " );
scanf ( " % d " , & v );
i f ( v < 0) break ;
printf ( " Fatorial de % d = %d , v , fat ( v ));
}
return EXIT_SUCCESS ;
}
11. Esse exerccio ilustra o uso de entrada de dados ate que uma condicao ocorra ou ate que se
chegue ao final dos dados de entrada. O caractere Control-d e usado para especificar fim
dos dados de entrada, em uma entrada de dados interativa no sistema operacional Linux e,
no sistema operacional Windows, Control-z no incio da linha seguido da tecla Enter (ou
Return).
Uma chamada a funcao scanf retorna o numero de variaveis lidas, e pode ser usado para
detectar fim dos dados de entrada a serem lidos (isto e, se nao existe mais nenhum valor na
64
entrada de dados a ser lido). O exemplo a seguir le varios inteiros do dispositivo de entrada
padrao e imprima a soma de todos os inteiros lidos. O programa termina quando nao ha
mais valores a serem lidos.
i n t main () {
i n t n , soma = 0 , testeFim ;
while (1) {
testeFim = scanf ( " % d " , & n );
i f ( testeFim != 1) break ;
soma = soma + n ;
}
printf ( " Soma = % d \ n " , soma );
return EXIT_SUCCESS ;
}
O programa a seguir funciona de modo semelhante, mas le varios inteiros positivos do dispo-
sitivo de entrada padrao e termina a execucao quando quando nao ha mais valores a serem
lidos ou quando um valor negativo ou zero for lido.
i n t main () {
i n t n , soma = 0 , testeFim ;
while (1)) {
testeFim = scanf ( " % d " , & n );
i f ( testeFim != 1 || n <=0) break ;
e l s e soma = soma + n ;
}
printf ( " Soma = % d \ n " , soma );
return EXIT_SUCCESS ;
}
12. Escreva um programa que leia, do dispositivo de entrada padrao, varios valores inteiros,
positivos ou nao, e imprima, no dispositivo de sada padrao, os dois maiores valores lidos.
A entrada termina com indicacao de fim dos dados de entrada (em entrada interativa,
Control-z seguido de Enter no Windows, ou Control-d no Linux).
Solucao: Sao usadas duas variaveis inteiras max1 e max2 para armazenar os dois maiores
valores lidos, e elas sao atualizadas adequadamente, se necessario, apos a leitura de cada
inteiro. O valor inicial atribudo a essas variaveis e o valor INT_MIN, menor inteiro arma-
zenavel em uma variavel de tipo i n t . Qualquer valor inteiro digitado sera maior ou igual a
esse valor e sera, caso for maior, atribudo a variavel. INT_MIN e definido em limits . h.
O valor retornado por scanf e usado para verificar fim dos dados de entrada: scanf retorna
-1 como indicacao de fim dos dados de entrada.
65
#i n c l u d e < stdio .h >
#i n c l u d e < limits .h >
i n t main () {
i n t max1 = INT_MIN , max2 = INT_MIN , valor , fim ;
while (1) {
fim = scanf ( " % d " , & valor );
i f ( fim == -1) break ;
i f ( valor > max1 ) { max2 = max1 ; max1 = valor ; }
e l s e i f ( valor > max2 ) max2 = valor ;
}
printf ( " Dois maiores = %d , % d \ n " , max1 , max2 );
}
13. Escreva um programa que leia, do dispositivo de entrada padrao, um texto qualquer, caractere
a caractere e imprima, no dispositivo de sada padrao, i) o numero de caracteres, ii) o numero
de palavras, e iii) o numero de linhas do texto.
Considere que uma palavra e uma sequencia de um ou mais caracteres que comeca com
qualquer caractere que nao e um delimitador de palavras. Um delimitador de palavras e
um caractere espaco (branco, i.e. ), fim-de-linha (\ n ) ou tab (caractere de tabulacao,
i.e. \ t ).
A entrada termina com indicacao de fim dos dados de entrada (em entrada interativa,
Control-z seguido de Enter no Windows, ou Control-d no Linux).
Solucao: A solucao mostrada a seguir usa scanf com formato % c para ler um caractere,
e o valor retornado por scanf para verificar fim dos dados de entrada (scanf retorna -1
para indicar fim dos dados).
A funcao isletter definida retorna verdadeiro (em C, valor inteiro diferente de zero) se
e somente se o caractere passado como argumento e uma letra. Para isso, e testado se tal
caractere esta entre a e z ou entre A e Z.
Para contar palavras, e usado uma variavel (fora) que indica se o caractere corrente, que
esta sendo lido, esta fora ou dentro de uma palavra. O numero de palavras (armazanado na
variavel palavras) e incrementado quando se esta fora de uma palavra e um caractere nao
delimitador de palavras e lido (como especificado no enunciado, um delimitador de palavras
e considerado como sendo um dos caracteres espaco, fim-de-linha ou tab).
66
#i n c l u d e < stdio .h >
i n t delim ( char c ) {
return ( c == \ ) || ( c == \ t ) || ( c == \ n );
}
i n t main () {
char c ;
i n t caracs =0 , palavras =0 , linhas =0 , isDelim , fim , fora =1;
while (1) {
fim = scanf ( " % c " , & c );
i f ( fim == -1) break ;
caracs ++;
isDelim = delim ( c );
i f ( isDelim ) {
i f ( c == \ n ) linhas ++;
fora =1;
}
e l s e i f ( fora ) { palavras ++; fora = 0; }
}
printf ( " Numero de caracteres , palavras , linhas = %d ,% d ,% d \ n " ,
caracs , palavras , linhas );
}
14. Esse e um exerccio baseado no problema PAR (Par ou mpar) , obtido de:
http://br.spoj.pl/problems/PAR
Ha uma modificacao motivada pelo fato de que nao vamos usar ainda leitura de cadeias de
caracteres, e por isso os nomes dos jogadores correspondentes a escolha pare mparsao
substitudos respectivamente por Pare Impar. O problema e descrito a seguir.
O problema consiste em determinar, para cada jogada de partidas do jogo Par ou Impar , o
vencedor da jogada.
A entrada representa uma sequencia de dados referentes a partidas de Par ou Impar . A
primeira linha de cada partida contem um inteiro n, que indica o numero de jogadas da
partida. As n linhas seguintes contem cada uma dois inteiros a e b que representam o
numero escolhido por cada jogador (0 a 5 e 0 B 5). O final da entrada e indicado
por n = 0.
A sada deve conter para cada partida, uma linha no formato Partida i, onde i e o numero da
partida: partidas sao numeradas sequencialmente a partir de 1. A sada deve conter tambem
uma linha para cada jogada de cada partida, contendo Par ou Impar conforme o vencedor
da partida seja o jogador que escolheu par ou mpar, respectivamente. Deve ser impressa
uma linha em branco entre uma partida e outra.
Por exemplo, para a entrada:
3
2 4
3 5
1 0
2
1 5
2 3
0
67
Partida 1
Par
Par
Impar
Partida 2
Par
Impar
Solucao: O programa abaixo define e usa a funcao processaPartida para separar o pro-
cessamento (calculo e impressao) de valores de cada partida, e a funcao par, que determina
se um dado valor e par ou nao. O numero de cada partida e armazenado em uma variavel,
que tem valor inicial igual a 1 e e incrementada apos o processamento de cada partida.
O programa usa tambem o recurso de definir a assinatura (ou interface, ou cabecalho) de cada
funcao definida, para usar (chamar) a funcao antes de defini-la. Na definicao da interface o
nome dos parametros e opcional, e e omitido no programa abaixo.
i n t main () {
i n t numPartida = 1 , numJogadas ;
while (1) {
scanf ( " % d " , & numJogadas );
i f ( numJogadas == 0) break ;
processaPartida ( numPartida , numJogadas );
numPartida ++;
}
}
15. Nesse exerccio vamos apresentar uma solucao para o problema RUMO9S (Rumo aos 9s) ,
obtido de
http://br.spoj.pl/problems/RUMO9s/
A solucao apresentada nao usa arranjo nem cadeia de caracteres (abordados no proximo
captulo), para ler, armazenar e imprimir os valores de entrada. Em vez disso, caracteres sao
lidos um a um (numeros nao podem ser lidos e armazenados como valores inteiros porque
podem ter ate 1000 dgitos decimais, e portanto nao poder ser armazenadas como valores de
tipo i n t ou long i n t ). O problema e descrito a seguir.
68
Um numero inteiro e multiplo de nove se e somente se a soma dos seus dgitos e multiplo de
9. Chama-se grau-9 de um numero inteiro n 0 o valor igual a 1 se n = 9, 0 se n < 9, e 1
mais o grau-9 da soma de seus dgitos, se n > 9.
Escreva um programa que, dado uma sequencia de inteiros positivos, imprime se cada um
deles e multiplo de nove e, em caso afirmativo, seu grau-9. A entrada, no dispositivo de
entrada padrao, contem uma sequencia de inteiros positivos, um em cada linha, e termina
com o valor 0. Exemplo:
Entrada:
999
27
9
998
0
Sada:
Solucao: A solucao usa a funcao isdigit para testar se um dado caractere e um dgito
(i.e. em C, um inteiro sem sinal, entre 0 e 9).
69
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
i n t so ma EI mp ri me Ca ra cs () {
char c ;
scanf ( " % c " , & c );
i f ( c == 0 ) return 0;
i n t soma =0;
while ( isdigit ( c )) {
soma += c - 0 ;
printf ( " % c " ,c );
scanf ( " % c " , & c );
}
return soma ;
}
i n t somaDigs ( i n t n ) {
return (n <10 ? n : ( n %10 + somaDigs ( n /10));
}
i n t grau9 ( i n t n ) {
i f (n <10) return ( n ==9 ? 1 : 0);
e l s e { i n t grau = grau9 ( somaDigs ( n ));
return ( grau == 0? 0 : 1 + grau );
}
}
i n t main () {
i n t v , grau9v ;
while (1) {
i n t n = so ma EI mp ri me Ca ra cs ();
i f ( n ==0) break ;
i n t grau9n = grau9 ( n );
i f ( grau9n ==0) printf ( " nao e multiplo de 9.\ n " , n );
e l s e printf ( " e multiplo de 9 , com 9 - degree % d .\ n " , grau9n );
}
return 0;
}
4.5 Exerccios
1. Escreva tres definicoes de funcao, chamadas somaIter, somaRec e soma, tais que, dados
dois numeros inteiros positivos a e b, retorne o valor a + b. As duas primeiras definicoes
devem usar apenas as operacoes mais simples de incrementar 1 e decrementar 1 (devem
supor que as operacoes de adicionar e de subtrair mais de uma unidade nao sao disponveis).
A primeira definicao deve usar um comando de repeticao, e a segunda definicao deve ser
recursiva. A terceira definicao deve usar o operador + de adicao.
Inclua essas tres definicoes em um programa de teste, e defina um programa de teste que
leia varios valores a a b do dispositivo de entrada padrao, e nao imprima nada se e somente
se, para cada par de valores a e b lidos, os resultados retornados pelas execucoes das tres
definicoes (somaIter, somaRec e soma) forem iguais. Se existir um par de valores a e
b lido para o qual o resultado retornado pela execucao das tres definicoes nao e igual, o
programa deve terminar e uma mensagem deve ser impressa, informando o par de valores a,
b para o qual o resultado da execucao foi diferente, assim como o resultado retornado por
cada definicao, junto com o nome da funcao que retornou cada resultado.
A leitura deve terminar quando nao houver mais valores a serem lidos (em entrada interativa,
70
quando o caractere que indica fim dos dados for digitado).
2. Escreva um programa que leia pares de numeros inteiros positivos a,b e imprima, para cada
par lido, o numero de vezes que uma divisao inteira pode ser realizada, comecando com o
primeiro numero como dividendo e usando sempre o segundo como divisor, e substituindo-se
o dividendo pelo quociente na vez seguinte. A leitura deve terminar quando um dos valores
lidos for menor ou igual a zero.
O programa deve definir e usar uma funcao numdiv que recebe dois numeros inteiros posi-
tivos e retorna o numero de vezes que uma divisao exata pode ser realizada, neste processo
de substituir o dividendo pelo quociente. O processo de divisoes sucessivas deve terminar
quando uma divisao exata nao existe.
Exemplos: numdiv (8 ,2) deve retornar 3 e numdiv (9 ,2) deve retornar 0.
n!
p! (n p)!
No entanto, note que uma implementacao baseada diretamente nessa ultima seria menos
eficiente do que uma implementacao baseada diretamente na primeira, uma vez que o numero
de operacoes de multiplicacao necessarias para o calculo seria maior nesse ultimo caso.
Escreva um programa que leia varios pares de numeros inteiros positivos n, p e imprima,
para cada par lido, o numero de combinacoes existentes de n objetos p a p. A leitura deve
terminar quando um dos valores lidos for menor ou igual a zero.
4. Escreva uma funcao para calcular qual seria o saldo de sua conta de poupanca depois de 5
anos, se voce depositou 1000 reais no incio desse perodo e a taxa de juros e de 6% ao ano.
5. Generalize a questao anterior, de maneira que se possa especificar quaisquer valores inteiros
como capital inicial, taxa de juros e prazo desejados.
Escreva um programa que leia, repetidamente, tres valores inteiros positivos c, j, t que
representam, respectivamente, o capital inicial, a taxa de juros anual e o numero de anos de
deposito, e imprima, para cada tres valores lidos, o saldo final da conta, calculado usando a
funcao acima. A leitura deve terminar quando um dos tres valores lidos for menor ou igual
a zero.
71
(244668...) 2 4 4 6 6
(e) 4 = (335577...) , usando n termos no produtorio (considere termos 3, 3, 5, 5, 7
etc.)
O resultado de cada item deve ser obtido usando i) um comando de repeticao e ii) uma funcao
recursiva.
Escreva um programa que leia repetidamente pares de valores inteiros x, k e imprima, para
cada par lido, o resultado de chamar a k-esima funcao acima (k variando de 1 a 5) com o
argumento x (que representa o numero de termos). A leitura deve terminar quando x 0
ou quando k nao for um valor entre 1 e 5.
7. Escreva funcoes para calcular um valor aproximado do seno e cosseno de um angulo dado em
radianos, usando as seguintes formulas:
x3 x5
sen (x) = x 3! + 5! ...
x2 x4
cos (x) = 1 2! + 4! ...
Cada funcao deve receber o valor de x em radianos e o numero de parcelas a serem usadas
no somatorio.
A definicao nao deve ser feita calculando o fatorial e a exponencial a cada parcela, mas sim
de modo que o valor do numerador e do denominador de cada parcela sejam obtidos a partir
dos valores respectivos da parcela anterior.
Escreva um programa que leia, do dispositivo de entrada padrao, um numero inteiro positivo
n, em seguida varios numeros de ponto flutuante (um a um), que representam valores de
angulos em graus e, para cada valor, imprima o seno e o cosseno desse valor, usando as
funcoes definidas acima com o numero de parcelas igual a n. Note que voce deve converter
graus em radianos nas chamadas as funcoes para calculo do seno e cosseno, e que essas funcoes
devem ter um parametro a mais que indica o numero de parcelas a ser usado.
A entrada deve terminar quando um valor negativo ou nulo for lido.
8. Escreva um programa que leia uma sequencia de valores inteiros diferentes de zero, separados
por espacos ou linhas, e imprima os valores pares dessa sequencia. Um valor e par se o resto
da divisao desse valor por 2 e igual a zero. A entrada termina quando um valor igual a zero
for lido.
Por exemplo, para a entrada:
1 72 20 15 24 10 3 0 13 14
a sada deve conter (os numeros pares da entrada antes do primeiro zero, ou seja):
72 20 24 10
9. Faca um programa para imprimir a seguinte tabela:
1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
...
10 20 30 40 50 60 70 80 90 100
10. Escreva um programa que leia um numero inteiro positivo n e imprima um triangulo como
o mostrado abaixo, considerando que o numero de linhas e igual a n.
*
***
*****
*******
*********
***********
*************
72
11. Escreva um programa que leia um numero inteiro positivo n e imprima um losango como o
mostrado abaixo, considerando que o numero de linhas e igual a n (n = 13 para o losango
mostrado abaixo). Se n for par, as duas linhas no meio do losango devem ter o mesmo
numero de asteriscos.
*
***
*****
*******
*********
***********
*************
***********
*********
*******
*****
***
*
12. Defina uma funcao que converta o valor de uma temperatura dada em graus Fahrenheit para
o valor correspondente em graus centgrados (ou Celsius). A conversao e dada pela formula:
5 (TF 32)
TC =
9
Use a funcao definida acima como parte de um programa que receba como argumentos o
valor de uma temperatura inicial, o valor de uma temperatura final e um passo p (valor de
incremento), e imprima uma tabela de conversao de graus Fahrenheit em graus centgrados,
desde a temperatura inicial ate o maior valor que nao ultrapasse a temperatura final, de p
em p graus Fahrenheit.
13. Os numeros mostrados na tabela a seguir formam a parte inicial do chamado triangulo de
Pascal (nome dado em homenagem a Blaise Pascal (16231662), que escreveu um influente
tratado sobre esses numeros). A tabela contem os valores das combinacoes de n elementos,
p a p, para valores crescentes de n e p.
0 1
1 1 1
2 1 2 1
3 1 3 3 1
4 1 4 6 4 1
5 1 5 10 10 5 1
6 1 6 15 20 15 6 1
7 1 7 21 35 35 21 7 1
8 1 8 28 56 70 56 28 8 1
9 1 9 36 84 126 126 84 36 9 1
10 1 10 45 120 210 252 210 120 45 10 1
As entradas em branco nessa tabela tem, de fato, valor igual a zero, tendo sido deixadas em
branco para evidenciar o triangulo formado pelas demais entradas da tabela.
Escreva um programa para imprimir o triangulo de Pascal, usando o fato de que
n
p (n p)
n
=
p+1 p+1
73
14. Escreva um programa que leia quatro valores inteiros positivos nA, nB , tA e tB represen-
tando respectivamente as populacoes atuais de dois pases A e B e as taxas de crescimento
anual dessas populacoes e determine o numero de anos necessarios para que a populacao
do pas A ultrapasse a de B , supondo que as taxas de crescimento dessas populacoes nao
variam e que nA < nB e tA > tB .
15. Escreva um programa que leia, do dispositivo de entrada padrao, um texto qualquer, caractere
a caractere e imprima, no dispositivo de sada padrao, i) o numero de vogais, ii) o numero
de consoantes e iii) o numero de outros caracteres diferentes de vogais e consoantes presentes
em palavras: considere que estes sao todos os demais caracteres que nao sejam espaco, fim-
de-linha (\ n ) ou tab (caractere de tabulacao, i.e. \ t ).
A entrada termina com indicacao de fim dos dados de entrada (em entrada interativa,
Control-z seguido de Enter no Windows, ou Control-d no Linux).
Dicas: Use scanf com formato % c para ler um caractere, e use o valor retornado por scanf
para verificar fim dos dados de entrada: scanf retorna -1 para indicar fim dos dados.
Defina e use funcao que retorna verdadeiro se e somente se o caractere passado como argu-
mento e uma letra; para defini-la, teste se tal caractere esta entre a e z ou entre A e
Z.
16. Modifique o programa da questao anterior de modo a eliminar a suposicao de que nA < nB
e calcular, nesse caso, se a menor populacao vai ou nao ultrapassar a maior e, em caso
afirmativo, o numero de anos necessario para que isso ocorra (em caso negativo, o programa
deve dar como resultado o valor 0).
http://br.spoj.pl/problems/BIT/
1
72
0
Teste 1
0 0 0 1
Teste 2
1 2 0 2
74
18. Resolva o problema ALADES disponvel em:
http://br.spoj.pl/problems/ALADES/
1 5 3 5
23 59 0 34
21 33 21 10
0 0 0 0
120
35
1417
75
(a) Temos:
Ou seja, use o valor de mdc (a , b ) para calcular mmc (a , b ), dividindo a por mdc (a , b )
e multiplicando por b.
(b) mmc de tres ou mais numeros pode ser calculado usando o fato de que:
76
Captulo 5
Arranjos
77
memoria e o mesmo, ou seja, independe do valor desse endereco. Um arranjo e, por isso, chamado
de uma estrutura de dados de acesso direto (ou acesso indexado). Ao contrario, em uma estrutura
de dados de acesso sequencial , o acesso ao i-esimo componente requer o acesso aos componentes
de ndice inferior a i.
i n t ai [3];
char ac [4];
As declaracoes acima criam as variaveis ai e ac. A primeira e um arranjo de 3 inteiros e a
segunda um arranjo de 4 caracteres.
A declaracao dessas variaveis de tipo arranjo envolvem tambem a criacao de um valor de tipo
arranjo. Na declaracao de ai, e criada uma area de memoria com tamanho igual ao de 3 variaveis
de tipo i n t . Similarmente, na declaracao de ac, e criada uma area de memoria com tamanho igual
ao de 4 variaveis de tipo char. Os valores contidos nessas areas de memoria nao sao conhecidos:
sao usados os valores que estao ja armazenados nessas areas de memoria.
i n t * pi ;
char * pc ;
pi = malloc (3 * s i z e o f ( i n t ));
pc = malloc (4 * s i z e o f ( char ));
A chamada malloc (3* s i z e o f ( i n t )) aloca uma area de memoria de tamanho 3 x, se x
e o tamanho da unidade de memoria ocupada por valores de tipo i n t . Similarmente, a chamada
malloc (4* s i z e o f ( char )) alloca uma area de memoria de tamanho 4*( s i z e o f ( char )),
sendo s i z e o f ( char ) o tamanho de uma area de memoria ocupada por um valor de tipo char.
Apos a atribuicao a variavel pi acima, pi contem o endereco da primeira posicao de um arranjo
com 3 componentes de tipo i n t . Analogamente, apos a atribuicao a variavel pc acima, ac contem
o endereco da primeira posicao de um arranjo com 4 componentes de tipo char.
No caso da declaracao de variaveis com um tipo que e explicitamente indicado como sendo
um tipo arranjo, para o qual o tamanho e indicado explicitamente (como no exemplo anterior das
variaveis ai eac), o arranjo nao e alocado na area dinamica mas na area de pilha da funcao na
qual a declaracao ocorre (no caso, na area de memoria alocada quando a execucao da funcao main
e iniciada).
78
/
Le n , d e p o i s n i n t e i r o s do d i s p o s i t i v o de e n t r a d a p a d r a o
e imprime o s n i n t e i r o s l i d o s em ordem i n v e r s a .
/
#i n c l u d e stdio . h ;
i n t main () {
i n t * arr , i =0 , n ;
scanf ( " % d " , & n );
arr = malloc ( n * s i z e o f ( i n t ));
f o r ( i =0; i < n ; i ++) scanf }( " % d " , & arr [ i ]);
f o r (i - -; i >=0; i - -) printf ( " % d " , arr [ i ]);
}
O tamanho de uma area de memoria alocada para variaveis de um tipo pode ser obtido em C por
meio do uso da funcao predefinida s i z e o f . A palavra reservada s i z e o f pode ser seguida de um
nome de tipo (como nos exemplos acima) ou por uma expressao. O resultado retornado pela ava-
liacao de s i z e o f e o tamanho em bytes da area alocada para variaveis do tipo ou expressao usada
como argumento. No caso de uso de um tipo, ele deve ser colocado entre parenteses, mas quando
uma expressao e usada, ela pode seguir s i z e o f sem necessidade de parenteses (respeitando-se a
precedencia de operadores e chamadas de funcoes).
Por exemplo, s i z e o f pi retorna o mesmo que s i z e o f (3* s i z e o f ( i n t )), depois da
atribuicao acima.
A linguagem C usa colchetes em declaracao de arranjos apos o nome da variavel; por exemplo,
a declaracao:
i n t a [5];
declara uma variavel a de tipo arranjo, mas o tipo i n t ocorre antes e a indicacao de que esse tipo
e um arranjo de componentes de tipo i n t ocorre apos o nome da variavel.
Isso e feito com o intuito de permitir declaracoes um pouco mais sucintas, como a seguinte, que
cria uma variavel b de tipo i n t e um arranjo a com componentes de tipo i n t :
i n t b , a [5];
79
void preenche ( i n t arr [] , i n t tam , i n t valor ) {
// P r e e n c h e t o d a s a s p o s i c o e s de a r r com v a l o r
int i;
f o r ( i =0; i < tam ; i ++) arr [ i ] = valor ;
}
evitar isso, ha duas opcoes: i) nao usar um arranjo, mas uma estrutura de dados encadeada, como
descrito na secao 7, ou ii) adotar um certo valor como maximo para o numero de valores a serem
digitados (de modo a alocar um arranjo com tamanho igual a esse numero maximo). Essa opcao
tem as desvantagens de que um numero maximo pode nao ser conhecido estaticamente ou ser
difcil de ser determinado, e o uso de um valor maximo pode levar a alocacao desnecessaria de area
significativa de memoria, que nao sera usada (ou seja, o maximo pode ser um valor muito maior
do que o de fato necessario). No caso de uso de um arranjo, e possvel testar se o numero maximo
foi alcancado e realocar um arranjo maior caso isso ocorra, mas essa e uma operacao dispendiosa
em termos de esforco de programacao e tempo de execucao (requer nao so testes para verificar se
o numero maximo foi alcancado mas, tambem, nesse caso, eventual necessidade de copia de todo
o arranjo para outra area de memoria).
Para percorrer um arranjo, e usado tipicamente um comando for, pois o formato desse comando
e apropriado para o uso de uma variavel que controla a tarefa de percorrer um arranjo: iniciacao
do valor da variavel usada para indexar o arranjo, teste para verificar se seu valor e ainda um
ndice valido do arranjo, e atualizacao do valor armazenado na variavel.
80
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
i n t main () {
i n t * arr1 , * arr2 , n1 , n2 , i ;
printf ( " Digite inteiro positivo n1 , n1 valores inteiros , " );
printf ( " inteiro positivo n2 , n2 valores inteiros \ n " );
scanf ( " % d " , & n1 );
arr1 = malloc ( n1 * s i z e o f ( i n t ));
f o r ( i =0; i < n1 ; i ++) scanf ( " % d " , &( arr1 [ i ]));
scanf ( " % d " , & n2 );
arr2 = malloc ( n2 * s i z e o f ( i n t ));
f o r ( i =0; i < n2 ; i ++) scanf ( " % d " , &( arr2 [ $i$ ]));
printf ( " Sequencia 1 e \% s sequencia 2 de valores inteiros . " ,
iguais ( arr1 , n1 } , arr2 , n2 ) ? " igual a " : " diferente da " );
return 0;
}
Figura 5.3: Exemplo de uso de funcao que testa igualdade entre arranjos
e o mesmo que:
Note que sao 5 componentes no arranjo str: o ultimo componente, de ndice 4, contem o
caractere \0 , e e inserido automaticamentre no literal de tipo cadeia de caracteres, e o tamanho
do arranjo deve ser igual ao numero de caracteres no literal mais 1.
O uso de cadeias de caracteres em C, e em particular a operacao de ler uma cadeia de caracteres,
deve levar em conta de que toda variavel que e uma cadeia de caracteres deve ter um tamanho fixo.
Para ler uma cadeia de caracteres, e necessario primeiro alocar espaco um tamanho maximo
para armazenar os caracteres que vao ser lidos. No entanto, no caso de passagem de parametros
para a funcao main (veja secao 3.9), o sistema aloca, automaticamente, cadeias de caracteres de
tamanho suficiente para armazenar os caracteres passados como parametros para a funcao main).
O uso de cadeias de caracteres em C envolve muitas vezes o uso de funcoes definidas na biblioteca
string, dentre as quais destacamos as seguintes:
81
Assinatura Significado
int strlen (char *s) Retorna tamanho da cadeia s, excluindo carac-
tere nulo no final de s
char* strcpy (char *dest, const char *fonte) Copia cadeia apontada por fonte, incluindo ca-
ractere \0 que indica terminacao da cadeia,
para dest; retorna fonte
char* strcat (char *dest, const char *fonte) Insere cadeia apontada por fonte, incluindo ca-
ractere \0 que indica terminacao da cadeia, no
final da cadeia apontada por dest, sobrepondo
primeiro caractere de fonte com caractere nulo
que termina dest; retorna cadeia dest atualizada
int strcmp (const char *s1, const char *s2) Comparacao lexicografica entre cadeias de carac-
teres, sendo retornado 0 se as cadeias sao iguais,
caractere a caractere, valor negativo se s1<s2, le-
xicograficamente, e positivo caso contrario.
Assinatura Significado
int atoi (char *) Converte cadeia de caracteres para valor de tipo int
double atof (char *) Converte cadeia de caracteres para valor de tipo float
int atol (char *) Converte cadeia de caracteres para valor de tipo long
Os significados sao ilustrados em comentarios no seguinte exemplo:
int i;
f l o a t f;
i = atoi ( s1 ); // i = 1234
f = atof ( s2 ); // f = 12.34
i = atoi ( s3 ); // i = 1234
i = atoi ( s4 ); // i = 123
i = atoi ( s5 ); // i = 0
82
Note que:
itoa converte valor para uma cadeia de caracteres, terminada com o caractere NULL, usando
a base base, armazena essa cadeia em str, e retorna str.
Se a base for 10 e valor for negativo, a cadeia resultante e precedida do sinal -. Em
qualquer outro caso, simplesmente se supoe que o valor e positivo.
str deve ser um arranjo com um tamanho grande o suficiente para conter a cadeia.
faz com que o interpretador da linha de comandos percorra a cadeia de caracteres "abc xy 123"
e coloque cada sequencia de caracteres que esta separada da seguinte por um ou mais delimitadores
em uma posicao de um arranjo, e passe como argumento para o metodo main do programa (prog)
o valor 3 que e igual ao tamanho do arranjo (numero de cadeias de caracteres) e esse arranjo.
Assim, o arranjo passado contem "abc" na variavel de ndice 0, "xy" na variavel de ndice 1 e
"123" na variavel de ndice 2.
83
5.5.4 Exerccios Resolvidos
1. Escreva um programa que leia um valor inteiro positivo n, em seguida uma cadeia de carac-
teres de tamanho menor que n e imprima a frequencia de todos os caracteres que ocorrem
nesta cadeia.
10
112223a
1 aparece 2 vezes
2 aparece 3 vezes
3 aparece 1 vez
a aparece 1 vez
A ordem de impressao dos caracteres e sua frequencia nao e importante, mas cada caractere
deve aparecer, com sua frequencia de ocorrencia, apenas uma vez na sada, e somente se essa
frequencia for diferente de zero.
Solucao: Cada caractere e representado no codigo ASCII por um valor inteiro compreendido
entre 0 e 127. Para armazenar a informacao sobre o numero de ocorrencias de cada um desses
caracteres, podemos usar um arranjo de valores inteiros com 128 componentes, fazendo cor-
responder a cada caractere representado pelo numero inteiro i a posicao i desse arranjo. Tal
arranjo pode ser declarado da seguinte forma:
i n t max = 128;\\
i n t * contchar = malloc ( max * s i z e o f ( char ));
Um exemplo de definicao de uma funcao main que usa a funcao contFreqChar e mostrada
a seguir. Os valores de entrada sao lidos, a funcao contFreqChar e chamada e a frequencia
de cada caractere que ocorre na cadeia de caracteres especificada na entrada e impressa.
84
i n t main () {
i n t tam ;
scanf ( " % d " , & tam );
char * s = malloc ( tam * s i z e o f ( char ));
scanf ( " % s " ,s );
printf ( " Na cadeia de caracteres % s \ n " , s );
i n t max = 128 ,
* contChar = malloc ( max * s i z e o f ( i n t )) ,
i , freq ;
f o r ( i =0; i < max ; i ++) contChar [ i ] = 0;
contFreqChar (s , contChar );
f o r ( i =0; i < max ; i ++)
i f ( contChar [ i ] != 0) {
freq = contChar [ i ];
printf ( " % c aparece % d % s \ n " ,( char )i ,
freq , freq ==1? " vez " : " vezes " );
}
}
2. Escreva um programa para determinar a letra ou algarismo que ocorre com maior frequencia
em uma cadeia de caracteres dada, com um tamanho maximo previamente fornecido, e
imprimir esse algarismo no dispositivo de sada padrao.
Solucao: Um caractere alfanumerico e um caractere que pode ser um algarismo ou uma letra.
A solucao consiste em, inicialmente, determinar a frequencia de ocorrencia de cada caractere
alfanumerico, de maneira analoga a do exerccio anterior. Ou seja, a solucao armazena cria
arranjos com componentes correspondentes a cada caractere alfanumerico. Cada componente
contem a frequencia de ocorrencia do caractere alfanumerico correspondente. Em seguida, o
ndice do componente de maior valor (isto e, maior frequencia) desse arranjo e determinado e
o caractere alfanumerico correspondente a esse ndice e impresso. Vamos usar tres arranjos,
um arranjo para dgitos com ndices de 0 a 9 , e os outros para letras minusculas e
maiusculas. O ndice do arranjo de dgitos correspondente a um dgito d e obtido por d -
0, usando o fato de que a representacao dos dgitos sao crescentes, a partir do valor da
representacao do caractere 0. Analogamente para letras minusculas e maiusculas, que tem
valores de representacao crescentes a partir, respectivamente, dos valores das representacoes
dos caractere a e A.
85
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
#i n c l u d e < ctype .h > // d e f i n e isdigit
i n t main () {
i n t tamstr ;
scanf ( " % d " , & tamstr );
char * str = malloc ( tamstr * s i z e o f ( char ));
scanf ( " % s " , str );
printf ( " Carac . alfanum . mais freq . em % s e : % c \ n " , str ,
maisFreq ( str , tamstr ));
}
A funcao maisFreq recebe como argumento uma cadeia de caracteres, em que cada carac-
tere representa um algarismo, e retorna o algarismo mais frequente nessa cadeia. Quando
existir mais de um algarismo com a maior frequencia de ocorrencia, o metodo retorna, dentre
esses, aquele que ocorre primeiro na cadeia. Por exemplo, maisFreq ( " 005552 " ) retorna
5, e maisFreq ( " 110022 " ) retorna 1.
3. Escreva um programa para resolver o exerccio 15, considerando a condicao de que inteiros
podem ter ate 1000 dgitos decimais.
Solucao: Usamos uma cadeia de caracteres de ate 1001 dgitos para armazenar inteiros que
podem ter ate 1000 dgitos, e mais o caractere \0 para indicar terminacao da cadeia. A
86
soma dos dgitos de um inteiro de ate 1000 dgitos sempre pode ser armazenada em um valor
de tipo i n t .
i n t somaDigsInt1 ( i n t n , i n t soma ) {
return ( n ==0 ? soma
: somaDigsInt ( n /10 , n %10 + soma );
}
i n t grau9Int ( i n t n ) {
i f ( n <= 9) return ( n == 9 ? 1 : 0);
e l s e { i n t grau = grau9Int ( somaDigsInt ( n ));
return ( grau == 0 ? grau
: grau +1);
}
}
i n t main () {
const i n t numDigs = 1001;
char v [ numDigs ];
while (1) {
scanf ( " % s " , & v );
i n t i =0;
while ( i < numDigs && v [ i ] == 0 ) i ++;
// Termina s e i n t e i r o l i d o i g u a l a z e r o
i f (i < numDigs && v [ i ]== \0 ) break ;
i n t grau9v = grau9 ( v );
printf ( " % s % se multiplo de 9 " , v , grau9v ==0 ? " nao "
: " " );
printf ( grau9v ==0 ? " \ n "
: " e tem grau -9 % d \ n " , grau9v );
}
return 0;
}
5.5.5 Exerccios
1. Escreva uma funcao inverte que estenda o Exerccio 5 da secao 3.12 para qualquer cadeia
de caracteres s. A funcao inverte tem como parametro adicional (alem da cadeia de
caracteres) o tamanho n da cadeia passada como argumento. O programa que chama a
funcao inverte deve ler, antes de cada cadeia de caracteres, um valor que, deve-se supor,
e maior que o tamanho da cadeia.
87
2. Defina uma funcao decPraBin que receba um numero inteiro nao-negativo como argumento
e retorne uma cadeia de caracteres que e igual a representacao desse numero em notacao
binaria.
Por exemplo, ao receber o numero inteiro 8, a funcao deve retornar "1000".
Para calcular o tamanho da cadeia de caracteres a ser alocada, defina e use uma funcao
numDiv2 que, ao receber um numero inteiro positivo n como argumento, retorne m + 1 tal
que 2m n < 2m+1 . Esse numero (m+1) e igual ao numero de vezes que n pode ser dividido
por 2 ate que o quociente da divisao seja zero. Por exemplo, 23 8 < 24 , e 4 e o numero de
caracteres da cadeia "1000", necessarios para representacao de 8 na base 2.
Note que, para cada n, deve ser alocada uma cadeia de caracteres de tamanho tam+2, onde
tam e o resultado de numDiv2 ( n ), para conter, alem dos caracteres necessarios para re-
presentacao de n na base 2, o caractere \0 (usado em C para terminacao de cadeias de
caracteres).
Escreva um programa que leia varios numeros inteiros positivos do dispositivo de entrada
padrao e imprima, para cada inteiro lido, a sua representacao em notacao binaria, usando a
funcao definida no item anterior (o programa que contem a funcao main deve conter tambem
a definicao das funcoes definidas acima).
A execucao deve terminar quando um inteiro negativo ou zero for lido.
3. Defina uma funcao que receba como argumento um numero inteiro nao-negativo b, em notacao
binaria, e retorne o valor inteiro (de tipo i n t ) correspondente, em notacao decimal.
Por exemplo, ao receber o numero inteiro 1000, a funcao deve retornar o valor 8.
Seu programa pode ler b como um valor inteiro e supor que o valor lido pode ser arma-
zenado como um valor de tipo i n t ou como uma cadeia de caracteres e supor que o
tamanho maximo da cadeia e de 30 dgitos binarios.
No caso de leitura como um valor de tipo i n t , cada dgito deve ser obtido como resto de
divisao por 10. Por exemplo, para obter cada dgito de 101, obtenha o 1 mais a direita como
resto da divisao de 101 por 10; depois obtenha o quociente da divisao de 101 por 10 (que e
igual a 10) e repita o processo.
Escreva um programa que leia, do dispositivo de entrada padrao, varias cadeias de caracteres
que representam numeros inteiros positivos em notacao binaria, e imprima, para cada valor
lido, a sua representacao em notacao decimal, usando a funcao definida acima (o programa
que contem a funcao main deve conter tambem a definicao da funcao definida acima).
A execucao deve terminar com o fim dos dados de entrada.
4. Escreva um programa que leia, do dispositivo de entrada padrao, um valor inteiro positivo t,
em seguida uma cadeia de caracteres s de tamanho menor que t e, em seguida, varias cadeias
de caracteres s1 , . . . , sn , tambem com tamanho menor que t, e imprima, para cada cadeia
si , para i entre 1 e n, uma mensagem que indica se s contem si ou nao. A entrada deve
terminar com o fim dos dados de entrada, isto e, quando fim-de-arquivo for detectado; em
entrada interativa, quando scanf retornar -1, devido ao fato de o usuario digitar Control-d
(no Unix) ou Control-z (no Windows).
Por exemplo, se a entrada for:
1000
abcdefghijklmnopqrstuvwxyz123456789
nopqr
789
xya
abc
88
nopqr Sim
789 Sim
xya Nao
abc Sim
5. Escreva um programa que leia um inteiro positivo n, em seguida varios pares s1 , s2 de cadeias
de caracteres, de tamanho menor que n, e imprima, para cada par lido, uma cadeia de
caracteres que e a concatenacao de s1 e s2 (a concatenacao de s1 com s2 e a cadeia formada
pelos caracteres de s1 seguidos pelos caracteres de s2 ).
Nao se esqueca de considerar que, em C, cadeias de caracteres sao armazenadas de modo a
terminar com o caractere \0 .
6. Escreva um programa que leia um valor n, duas cadeias de caracteres s1 e s2 , ambas com
tamanho menor que n, e imprima o resultado de remover de s2 todos os caracteres que
aparecem em s1 .
Por exemplo, para a entrada:
100
abci adefghiabaf
defghf
i n t ** a , n =4 , m =3;
a = malloc ( n * s i z e o f ( i n t *));
int i;
f o r ( i =0; i < n ; i ++)
a [ i ] = malloc ( m * s i z e o f ( i n t ));
Apos a execucao desse trecho de programa, a representa um arranjo com quatro componentes,
sendo cada componente um arranjo com tres componentes. Tal arranjo e algumas vezes chamado
de uma matriz , no caso uma matriz 4 por 3. Um arranjo de arranjos e tambem chamado de arranjo
multidimensional .
Um arranjo pode ter como componentes arranjos de tamanhos diferentes, alocados dinamica-
mente, como ilustra o exemplo a seguir.
i n t ** a ;
a = malloc (2 * s i z e o f ( i n t *));
...
a [0] = malloc (10 * s i z e o f ( i n t ));
...
a [1] = malloc (40 * s i z e o f ( i n t ));
...
89
5.6.1 Passagem de Arranjos Multidimensionais como Parametros
O modo mais simples, mas nao muito flexvel, de passar um arranjo multidimensional como
parametro e usar tipos estaticos na declaracao do parametro e do argumento. Por exemplo, pode-
mos declarar:
i n t f ( i n t arr [2][3]) { \\
...
}
i n t a [2][3];
...
f ( a ) ...
A definicao da funcao f acima so funciona para argumentos que sao arranjos de formados por 2
arranjos de 3 inteiros. Se quisermos usar argumentos cujo tipo sao arranjos de tamanhos estaticos,
so podemos deixar a primeira dimensao (o numero de linhas) nao especificado, em C; veja o exemplo
a seguir:
char * diasDaSemana [] = {
" Dom " , " Seg " ,
" Ter " , " Qua " ,
" Qui " , " Sex " ,
" Sab "
};
90
para declarar uma variavel diasDaSemana que e um arranjo de 7 componentes, sendo cada
componente uma cadeia de caracteres. O tamanho do arranho nao precisa ser especificado, sendo
determinado automaticamente de acordo com o numero de componentes especificado no valor
usado para inicializacao do arranjo. Ou seja, a declaracao acima e equivalente pode ser feita como
a seguir:
No entanto, se o tamanho for explicitamente especificado, como acima, a variavel de tipo arranjo
nao podera posteriormente conter um arranjo com tamanho diferente de 7, ou seja, a variavel so
podera ser indexada com valores entre 0 e 6.
Em casos em que nao se pode inicializar um arranjo no instante de sua declaracao (pois o valor
inicial ou o tamanho de um arranjo ainda nao sao conhecidos), e em geral conveniente inicializar
a variavel de tipo arranjo com o valor NULL (ponteiro nulo).
1. Escreva um programa que leia notas de alunos obtidas em tarefas de uma certa disciplina,
e imprima a nota total de cada aluno e a media das notas dos alunos nessa disciplina. A
entrada dos dados contem, primeiramente, o numero n de alunos da turma, depois o numero
k de tarefas da disciplina, e em seguida as k notas de cada aluno i, para i de 1 a n. Cada
valor e separado do seguinte por um ou mais espacos ou linhas.
Solucao: A solucao define uma funcao calcNotas que recebe um arranjo com as notas de
cada aluno em cada tarefa, e retorna um arranjo com as notas finais de cada aluno. Em C,
o arranjo passado como argumento, alocado dinamicamente, e um ponteiro para ponteiros-
para-inteiros.
Outra funcao, chamada media, recebe um arranjo de inteiros (em C, um ponteiro para
inteiros) e o tamanho do arranjo, e retorna um valor de tipo f l o a t que e igual a media dos
inteiros contidos no arranjo.
Na funcao main, um arranjo de notas de cada aluno em cada avaliacao e preenchido com
valores lidos, o metodo calcNotas e chamado para calculo das notas finais, e as notas finais
calculadas, assim como a media, calculada por chamada a funcao media, sao impressas.
91
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
// I n i c i a l i z a n o t a s f i n a i s de c a d a a l u n o com 0
f o r ( i =0; i < numAlunos ; i ++)
notasFinais [ i ] = 0;
return notasFinais ;
}
f l o a t media ( i n t vs [] , i n t n ) {
i n t i , soma =0;
f o r ( i =0; i < n ; i ++) soma += vs [ i ];
return (( f l o a t ) soma )/ n ;
}
i n t main () {
int n,k;
scanf ( " % d " , & n );
scanf ( " % d " , & k );
5.9 Exerccios
1. Considere que uma empresa comercial, que tem n lojas especializadas de certo tipo de ma-
terial, te contratou para fazer o seguinte programa em C.
A empresa tem dados armazenados sobre o numero de vendas realizadas em cada loja. Nao
importa qual tipo de material, a empresa esta interessada apenas no numero de unidades
vendidas.
A empresa quer um programa que leia, do dispositivo de entrada padrao, o valor de n, em
seguida n valores v1 , . . . , vn que correspondem ao numero de unidades vendidas em um mes
92
nas lojas de 1 a n, respectivamente, e imprima, no dispositivo de sada padrao, quais foram
as lojas de 1 a n nas quais o numero de unidades vendidas foi maior ou igual a media de
unidades vendidas em suas lojas.
2. Escreva um programa que leia um texto qualquer, caractere a caractere, e imprima:
o numero de algarismos que ocorrem no texto,
o numero de letras que ocorrem no texto, e
o numero de linhas do texto que contem pelo menos um caractere.
3. Escreva um programa que leia um valor inteiro positivo n, em seguida uma matriz quadrada
n n de valores inteiros, e imprima uma mensagem indicando se a matriz quadrada e ou
nao um quadrado magico.
Um quadrado magico e uma matriz quadrada na qual a soma dos numeros em cada linha,
coluna e diagonal e igual.
4. Os votos de uma eleicao sao representados de modo que 0 significa voto em branco, 1 a n
significam votos para os candidatos de numeros 1 a n, respectivamente, e qualquer valor
diferente desses significa voto nulo.
A eleicao tem um vencedor se o numero de votos em branco mais o numero de votos nulos e
menor do que 50% do total de votos, sendo vencedores, nesse caso, todos os candidatos com
numero de votos igual ao maior numero de votos.
Escreva um programa que leia um numero positivo n, que indica o numero de candidatos
de uma eleicao, em seguida leia cada um dos votos de uma eleicao, e determine se ha um
vencedor e, em caso positivo, determine o numero de vencedores da eleicao, quais sao esses
vencedores e o numero de votos dos mesmos.
5. Escreva um programa que leia um valor inteiro positivo n, em seguida um valor inteiro
positivo k, depois uma sequencia s de k valores inteiros diferentes de zero, cada valor lido
separado do seguinte por um ou mais espacos ou linhas, e imprima n linhas tais que: a 1a
linha contem os valores de s divisveis por n, a 2a linha contem os valores de s para os quais
o resto da divisao por n e 1, etc., ate a n-esima linha, que contem os valores de s para os
quais o resto da divisao por n e n 1.
Dica: use um arranjo de n posicoes com elementos que sao arranjos de k valores inteiros,
sendo um valor igual a zero indicativo de ausencia de valor naquela posicao.
Exemplo: Considere a entrada:
4 5 12 13 15 16 17
93
(a) um numero inteiro positivo n,
(b) n inteiros positivos v1 , . . . , vn ,
(c) 3 inteiros positivos i, k, m.
O programa deve imprimir, para cada n lido, uma linha com os valores vi , vi+k , vi+2k , . . . , vi+pk
tais que i+p k m. Os valores devem ser separados por espacos e impressos no dispositivo
de sada padrao.
Por exemplo, para a entrada:
10 11 25 33 40 50 69 73 85 96 101 3 2 7 0
33 50 73
10
ab1
10 ab*cd!
0
AB1
10 AB*CD!
8. Escreva um programa que leia, do dispositivo de entrada padrao, um texto qualquer, caractere
a caractere, e imprima, no dispositivo de sada padrao, i) o numero de palavras que tem um
genero, masculino ou feminino, ii) o numero de palavras do genero masculino, e iii) o numero
de palavras do genero feminino.
Voce pode considerar, para simplificar o problema, que:
94
Uma palavra tem genero masculino se seu ultimo caractere for a letra o ou se a
penultima letra for o e a ultima letra for s, ou se a palavra e igual a O ou "Os".
Analogamente, uma palavra tem genero feminino se seu ultimo caractere for a letra a
ou se a penultima letra for a e a ultima letra for s, ou se a palavra e igual a A
ou "As".
Dica: Use a funcao getchar para ler um caractere, e use o valor retornado por getChar
para verificar fim dos dados de entrada: getChar retorna o valor EOF (igual a -1) para
indicar fim dos dados.
3
1 10.0
2 20.0
3 30.0
1 30.0
95
96
Captulo 6
Ponteiros
int *p;
i n t *p , q ;
i n t *p , q ;
p = &q;
declara p como uma variavel de tipo i n t *, q como uma variavel de tipo i n t e armazena o
endereco de q em p.
O operador * e um operador chamado de derreferenciacao: aplicado a um ponteiro p, o
resultado e a variavel apontada por p (se usado como um valor, o resultado e o valor contido na
variavel apontada por p).
O uso de * em uma declaracao significa declaracao de um ponteiro; o uso de * precedendo um
ponteiro em uma expressao significa derreferenciacao.
97
Considere, por exemplo, o problema de trocar o conteudo dos valores contidos em duas variaveis
a e b de tipo i n t . Para fazer isso precisamos passar para uma funcao troca o endereco das
variaveis, i.e. devemos chamar troca (& a ,& b ), onde a funcao inatroca e definida como a seguir:
void troca ( i n t * x , i n t * y ) {
int t = *x;
*x = *y;
*y = t;
}
Note que uma chamada troca (a , b ) em vez de troca (& a ,& b ) fara com que a execucao do
programa use enderecos de memoria que sao valores inteiros contidos em a e b, e isso fara com que
o programa provavelmente termine com um erro devido a tentativa de acesso a endereco invalido
de memoria.
Note tambem que a funcao scanf modifica o valor de uma variavel, e e por isso que o endereco
da variavel e que deve ser passado como argumento de scanf.
Ocorre um erro durante a execucao de um programa quando um ponteiro e derreferenciado e
o ponteiro representa um endereco que nao esta no conjunto de enderecos validos que o programa
pode usar. Esse erro e comumente chamado em ingles de segmentation fault, ou seja, erro de
segmentacao. Isso significa que foi usado um endereco que esta fora do segmento (trecho da
memoria) associado ao processo que esta em execucao.
O endereco 0 e tambem denotado por NULL (definido em stdlib) e e usado para indicar um
ponteiro nulo, que nao e endereco de nenhuma variavel. E em geral tambem usado em inicia-
lizacoes de variaveis de tipo ponteiro para as quais nao se sabe o valor no instante da declaracao.
i n t a [10];
um arranjo de 10 posicoes e alocado e o nome a representa um ponteiro para a primeira posicao
desse arranjo. Ou seja, o nome a representa o mesmo que & a [0].
Sendo i uma expressao qualquer de tipo i n t , a expressao a [ i ] denota o mesmo que ( a + i )
ou seja, a i-esima posicao do arranjo a, i posicoes depois da 0-esima posicao. Se usada em
um contexto que requer um valor, essa expressao e derreferenciada, fornecendo o valor *( a + i ).
Portanto, em C a operacao de indexacao de arranjos e expressa em termos de uma soma de um
inteiro a um ponteiro.
98
Assim, quando um arranjo a e passado como argumento de uma funcao f, apenas o endereco
& a [0] e passado.
99
100
Captulo 7
Registros
Um registro (ou, como e chamado em C, uma estrutura) e um tipo, e tambem um valor desse
tipo, que e um produto cartesiano de outros tipos, chamados de campos ou componentes do
registro, com notacoes especiais para definicao dos componentes do produto e para acesso a esses
componentes.
Em matematica, e em algumas linguagens de programacao, a forma mais simples de combinar
valores para formacao de novos valores e a construcao de pares. Por exemplo, (10,*) e um par,
formado por um primeiro componente, 10, e um segundo componente, *. Um par e um elemento
do produto cartesiano de dois conjuntos o par (10,*) e um elemento do produto cartesiano
do conjunto dos valores inteiros pelo conjunto dos valores de tipo char.
Naturalmente, alem de pares, e tambem possvel formar triplas, quadruplas, quntuplas etc.
usualmente chamadas de tuplas que sao elementos de produtos cartesianos generalizados, ou
seja, elementos de um produto de varios conjuntos.
Em linguagens de programacao (como C por exemplo), no entanto, e mais comum o uso de regis-
tros, em vez de tuplas. Um registro e uma representacao de um valor de um produto cartesiano,
assim como uma tupla, mas cada componente, em vez de ser identificado pela sua posicao (como no
caso de tuplas), e identificado por um nome usualmente chamado de rotulo. Cada componente
tem um nome a ele associado.
Por exemplo, um valor como:
{ x = 10 , y = * }
representa, em C, um registro com dois componentes, em que um deles tem rotulo x e o outro
tem rotulo y. Nesse exemplo, o valor do componente e separado do rotulo pelo smbolo =. O
registro { y = *, x = 10 } representa o mesmo valor que o registro {x = 10, y = *}. Ou
seja, a ordem em que os componentes de um registro e escrita nao e relevante para determinacao
do valor representado, ao contrario do que ocorre com relacao a ordem dos componentes de uma
tupla. Deve existir, e claro, uma operacao para selecionar um componente de um registro, assim
como ocorre no caso de tuplas.
No entanto, em C a especificacao de valores de tipo registro deve seguir uma ordem para os
valores dos campos, que e a ordem em que os campos aparecem na definicao do tipo registro, e so
podem ser usados em declaracoes de variaveis, como veremos no exemplo a seguir.
Uma declaracao de um tipo registro consiste de uma sequencia de campos, cada um dos quais
com um tipo e um nome. Por exemplo:
s t r u c t contaBancaria {
i n t numero ;
char * idCorrentista ;
f l o a t saldo ;
};
101
s t r u c t contaBancaria conta ;
O tipo contaBancaria, assim como a variavel conta, tem tres campos ou componentes:
numero, idCorrentista e saldo.
O acesso aos componentes e feito usando-se um valor de tipo registro seguido de um ponto e do
nome do campo. Por exemplo, conta . numero tem tipo i n t , conta . idCorrentista tem
tipo char * e conta . saldo tem tipo f l o a t .
Esses componentes denotam (podem ser usados ou modificados como) uma variavel comum do
tipo do campo.
Valores de tipo registro podem ser construdos em C mas apenas na inicializacao de uma variavel
de tipo registro, de modo semelhante ao que ocorre no caso de arranjos. Um valor de tipo registro
especifica valores a cada um dos campos do registro, entre chaves, como mostrado no exemplo a
seguir.
O exemplo seguinte ilustra uma declaracao de uma variavel do tipo contaBancaria, definido
acima, especificando um valor inicial para a variavel:
s t r u c t Ponto { i n t x ; i n t y ; };
typedef s t r u c t Ponto Ponto ;
s t r u c t contaBancaria {
i n t numero ;
char * idCorrentista ;
f l o a t saldo ;
};
typedef s t r u c t contaBancaria contaBancaria ;
i n t main () {
Ponto p , q ;
ContaBancaria c ;
// a q u i vem u s o de v a r i a v e i s p , q , c ...
}
102
7.2 Ponteiros para registros
Ponteiros para registros podem ser usadas para passar valores de tipo registro sem ter que
copiar o registro, e tambem de modo a permitir a alteracao de campos de registros.
Um ponteiro para um registro pode ser derreferenciado como normalmente, usando o operador
*, mas existe em C a possibilidade de usar o operador ->, que alem da derreferenciacao faz tambem
acesso a um campo de um registro. Por exemplo:
s t r u c t Ponto { i n t x ; i n t y ; };
typedef s t r u c t Ponto Ponto ;
s t r u c t ListaInt {
i n t val ;
s t r u c t ListaInt * prox ;\\
};
Listas encadeadas sao estruturas de dados flexveis, pois nao requerem tamanho maximo, como
arranjos. Elas podem crescer e decrescer de tamanho a medida que dados vao sendo inseridos e
removidos.
A desvantagem, em relacao ao uso de arranjos, e que o acesso a um componente da estrutura
de dados requer um tempo que depende da posicao desse componente na estrutura: o acesso a
cada componente depende de acesso a cada um dos componentes anteriores a ele na lista.
Arvores binarias podem ser formadas de modo similar. Por exemplo, uma arvore binaria com
nodos que contem campos de tipo i n t pode ser formada com registros do seguinte tipo:
s t r u c t ArvBinInt {
i n t val ;
s t r u c t ArvBinInt * esq ;
s t r u c t ArvBinInt * dir ;
};
O seguinte exemplo ilustra o uso de uma lista encadeada para evitar a restricao de se ter que
especificar um numero maximo de valores, necessario para uso de arranjo. Considere o problema
do Exerccio Resolvido 1, da secao 5.5.4, que propoe que um texto qualquer seja lido e seja impressa
a frequencia de todos os caracteres que ocorrem no texto. Considere que o problema nao especifica
o tamanho do texto. A solucao a seguir usa uma lista encadeada de caracteres para armazenar o
texto, em vez de uma cadeia de caracteres.
Em casos como esse, pode ser mais adequado usar um arranjo flexvel, com um tamanho maximo
que pode ser aumentado, testando, antes de cada insercao de um novo valor no arranjo, se esse
tamanho maximo foi atingido. Se o tamanho maximo for atingido, um novo arranjo e alocado com
um tamanho maior (por exemplo, o dobro do tamanho anterior), o arranjo antigo e copiado para
o novo, e o novo arranjo passa a ser usado, no lugar do antigo. Arranjos flexveis sao estruturas
de dados bastante usadas em programas escritos em linguagems como, por exemplo, Java e C++.
103
7.4 Exerccios Resolvidos
1. Escreva um programa que funcione como o exemplo fornecido na secao 5.3 mas sem a condicao
de que o numero de valores a serem impressos, em ordem impressa, seja fornecido. Ou seja,
escreva um programa que leia do dispositivo de entrada padrao qualquer numero de valores
inteiros, separados por um ou mais espacos ou linhas, e imprima esses valores na ordem
inversa em que foram lidos.
Solucao:
s t r u c t nodoLista { i n t val ;
s t r u c t nodoLista * prev ; };
typedef s t r u c t nodoLista nodoLista ;
i n t main () {
i n t val , numValLidos ;
s t r u c t nodoLista * cur , * prev = NULL ;
while (1) {
numValLidos = scanf ( " % d " , & val );
i f ( numValLidos != 1) break ;
cur = malloc ( s i z e o f ( nodoLista ));
cur -> val = val ;
cur -> prev = prev ;
prev = cur ;
}
while ( cur != NULL ) {
printf ( " % d " , ur -> val );
cur = cur -> prev ;
}
}
A solucao usa a notacao ponteiro - > campo, uma abreviacao de (* ponteiro ). campo.
(a) uma linha contendo um numero inteiro n, que especifica o numero de times do campe-
onato;
(b) n linhas contendo dois valores i e si , onde i e um numero inteiro entre 1 e n e si e o
nome do time i; o nome de um time e uma cadeia de caracteres de tamanho maximo
30; a ordem em que essas n linhas aparecem na entrada deve ser irrelevante;
(c) varias linhas contendo 4 numeros inteiros nao-negativos t1 v1 t2 v2 , que indicam o
resultado da partida entre o time t1 e t2 : t1 marcou v1 gols e t2 marcou v2 gols; os
resultados terminam com o fim da entrada (EOF).
Os times na lista impressa devem estar separados por vrgula (se houver mais de um time
com mais pontos), e a ordem e irrelevante. Por exemplo, se a entrada for:
3
1 America
2 Atletico
104
3 Cruzeiro
1 1 2 2
1 2 3 3
2 1 3 1
Isso porque o America perdeu do Atletico (1x2) e do Cruzeiro (2x3), e o Atletico empatou
com o Cruzeiro (1x1).
Solucao: A solucao apresentada usa arranjos para armazenar nomes de times e para arma-
zenar pontos acumulados em partidas. Esses arranjos sao indexados com o numero do time
menos 1 (porque os numeros de time variam entre 1 e n e arranjos em C sempre tem ndice
inicial igual a 0). O programa constroi uma lista de vencedores (times com maior numero de
pontos) a medida em que tal maior numero de pontos e calculado. A lista de vencedores
chamada de maiores e inicialmente nula. Para cada time, do primeiro ao ultimo, se o
numero de pontos e maior do que o maior calculado ate cada instante desta iteracao, o maior
e atualizado, senao, se o numero de pontos for igual, este e inserido na lista de maiores.
105
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
s t r u c t Maiores {
i n t num ;
s t r u c t Maiores * prox ;
};
typedef s t r u c t Maiores Maiores ;
i n t main () {
i n t n , num ;
scanf ( " % d " , & n );
4 12 13 15 16 17
3. Reescreva o programa do exerccio anterior de modo a fazer com que a ordem dos valores
impressos seja a mesma ordem que os valores ocorrem na entrada. Para isso, use um arranjo
de ponteiros para a ultima posicao de cada lista encadeada.
107
108
Captulo 8
Exerccios
Este captulo descreve a solucao de diversos exerccios, que mostram como usar e decidir quando
usar os diversos comandos e estruturas de dados abordados neste livro.
Os enunciados dos exerccios sao obtidos da pagina Web http://br.spoj.pl/problems/.
SPOJ (Sphere Online Judge) e um sistema disponvel na Internet (na pagina http://br.spoj.pl/)
que permite o registro de novos problemas e a submissao de solucoes de problemas registrados.
Ha dezenas de milhares de usuarios e milhares de problemas ja registrados. A solucao pode ser
submetida em dezenas de linguagens de programacao, incluindo, e claro, C.
8.1 ENCOTEL
Letras Numero
ABC 2
DEF 3
GHI 4
JKL 5
MNO 6
PQRS 7
TUV 8
WXYZ 9
Escreva um programa que leia varias linhas, cada linha contendo uma tal representacao alfa-
numerica de numero de telefone, e imprima uma sequencia de representacoes para os numeros de
telefone, novamente uma em cada linha, que substitua letras maiusculas por dgitos de acordo com
a tabela mostrada.
Considere que cada representacao alfanumerica possui entre 1 e 30 caracteres. A entrada e
terminada por fim de arquivo (EOF).
Por exemplo, para a entrada:
1-HOME-SWEET-HOME
MY-MISERABLE-JOB
1-4663-79338-4663
69-647372253-562
109
A solucao mostrada abaixo usa um arranjo que armazenada, para cada letra maiuscula, seu
codigo, segundo a tabela apresentada. De fato, como em C o primeiro ndice de um arranjo tem
que ser 0, para cada letra maiuscula corresponde um ndice entre 0 e o numero de letras maiusculas
menos um.
char * mkCodLetras () {
char i = A , codigo = 2 ,
* cod = malloc ( n * s i z e o f ( char ));
int j, k;
while (i <= W ) {
k = ( i == P || i == W ) ? 4 : 3;
f o r ( j =0; j < k ; j ++)
cod [i - A + j ] = codigo ;
codigo ++; i += k ;
}
return cod ;
}
i n t main () {
const i n t max = 31; // 31 d e v i d o a t e r m i n a c a o com \ 0
char * codLetras = mkCodLetras () ,
* exp = malloc ( max * s i z e o f ( char ));
i n t numValLidos ;
while (1) {
numValLidos = scanf ( " % s " , exp );
i f ( numValLidos != 1) break ;
i n t i = 0 , c_A ; char c ;
while ( exp [ i ] != \0 ) {
c = exp [ i ]; c_A = c - A ;
printf ( " % c " , c_A >=0 && c_A < n ? codLetras [ c_A ] : c );
i ++;
}
printf ( " \ n " );
}
}
Essa solucao evita escrever um programa, relativamente ineficiente e mais longo, que testa, apos
a leitura de cada caractere, se o caractere e uma letra pertencente a um grupo especfico de letras
na tabela mostrada, para impressao do dgito correspondente a esse grupo de letras na tabela.
8.2 PAPRIMAS
Um numero primo e um numero que possui somente dois divisores: ele mesmo e o numero 1.
Exemplos de numeros primos sao: 1, 2, 3, 5, 17, 101 e 10007.
Neste problema voce deve ler um conjunto de palavras, onde cada palavra e composta somente
por letras no intervalo a-z e A-Z. Cada letra possui um valor especfico, a letra a vale 1, a letra b
vale 2 e assim por diante, ate a letra z, que vale 26. Do mesmo modo, a letra A vale 27, a letra B
vale 28 e a letra Z vale 52.
110
Voce deve escrever um programa para determinar se uma palavra e uma palavra prima ou nao.
Uma palavra e uma palavra prima se a soma de suas letras e um numero primo.
Entrada: Conjunto de palavras; cada palavra em uma linha, com L letras, onde 1 L 20.
Termino com fim de arquivo (EOF).
Sada: Para cada palavra imprimir: It is a prime word., se a soma das letras da palavra e um
numero primo, senao imprimir It is not a prime word.
Exemplo: Para a entrada:
UFRN
contest
AcM
a sada dever ser:
It is a prime word.
It is not a prime word.
It is not a prime word.
Uma solucao completa e mostrada na Figura 8.1. A funcao main le diversas palavras, ate
encontrar fim-de-arquvo, e imprime mensagem indicando se cada palavra lida e uma palavra prima
ou nao. O tamanho de cada palavra e restrito a um tamanho maximo de 20 caracteres.
A funcao primo verifica se um dado numero n e primo ou nao. Essa funcao pode ser imple-
mentada de diversos modos. A Figura 8.1 usa o metodo simples de divisoes sucessivas, por todos
os numeros menores que n. Para valores grandes de n, esse metodo gasta mais tempo do que
outros algoritmos existentes. Os programas mostrados a seguir usam o algoritmo conhecido como
crivo de Eratostenes. O leitor interessado pode consultar e.g.:
Uma solucao que usa um teste de primalidade baseado no algoritmo conhecido como crivo de
Eratostenes e mostrada na Figura 8.2.
O algoritmo do Crivo de Eratostenes, inventado por Eratostenes em 350 A.C., cria uma lista
de todos os primos menores que um valor maximo m. Para o problema PAPRIMAS, existe tal
valor maximo, que pode ser definido como vinte vezes o valor maximo possvel para uma eletra
(uma vez que podem ocorrer no maximo 20 letras em uma palavra). O algoritmo de Eratostenes
consiste no seguinte:
2. Atribuir, inicialmente, 2 a p.
3. Repetir o seguinte, ate que nao exista valor nao removido em l a partir de p:
111
s t r u c t ListaETamanho {
i n t * lista ;
i n t tam ;
};
typedef s t r u c t ListaETamanho ListaETamanho ;
ListaETamanho listaDePrimos ( i n t m ) {
i n t p = 2 , m2 = m +2 , nums [ m2 ] , * primos = malloc ( m ) , i ,j , k ;
f o r ( i =2 , j =0; i < m2 ; i ++ , j ++) { nums [ i ]=0; primos [ j ] = 0; }
j = 0;
do {
f o r ( i = p ; i < m2 ; i += p ) nums [ i ] = 1;
f o r ( k = p +1; k < m2 ; k ++) i f (! nums [ k ]) break ;
p = k ; // p e o p r o x i m o primo
primos [ j ] = p ;
j ++;
}
while (p < m2 );
ListaETamanho l ;
l . lista = primos ;
l . tam = j ;
return l ;
}
O algoritmo de pesquisa binaria compara o elemento que esta sendo procurado com o valor
que esta na metade do arranjo ordenado, permitindo assim que a busca prossiga ou na metade
inferior ou na superior do arranjo, conforme o elemento a ser procurado seja menor ou maior,
respectivamente, do que o valor que esta na metade do arranjo. Se o elemento a ser procurado e
igual ao elemento na metade, entao, e claro, a busca termina com sucesso.
Sao mostradas apenas as funcoes modificadas, que sao listaDePrimos e primo. A funcao
listaDePrimos e modificada apenas para retornar, alem do arranjo contendo os primos, o
numero de primos de fato armazenado nesse arranjo. A funcao primo percorre esse arranjo
usando pesquisa binaria.
112
#i n c l u d e < stdio .h > // d e f i n e s c a n f , p r i n t f
#i n c l u d e < math .h > // d e f i n e s q r t
i n t main () {
char palavra [ max ];
while ( ler ( palavra ))
printf ( " It is % s a prime word .\ n " , prima ( palavra ) ? " " : " not " );
}
i n t minusc ( char let ) {
return ( let >= a && let <= z );
}
i n t valor ( char let ) {
return ( minusc ( let ) ? let - a + 1
: let - A + ( z - a +1)+1);
}
i n t prima ( char * palavra ) {
i n t i , somaLet =0;
f o r ( i =0; i < max && palavra [ i ] != \0 ; i ++)
somaLet += valor ( palavra [ i ]);
return primo ( somaLet );
}
i n t primo ( i n t n ) {
i f ( n %2 == 0) return 0;
i n t k = 3 , sqrtn = sqrt (( double ) n );
// Se e x i s t i r d i v i s o r p r o p r i o de n ,
// tem q u e e x i s t i r d i v i s o r p r o p r i o menor q u e r a i z de n .
while ( k <= sqrtn ) {
i f ( n % k == 0) return 0;
k += 2;
}
return 1;
}
i n t ler ( char * palavra ) {
// R e t o r n a v e r d a d e i r o s s e l e i t u r a com s u c e s s o .
return ( scanf ( " % s " , & palavra ) == 1);\\
}
113
#i n c l u d e < stdio .h > // d e f i n e s c a n f , p r i n t f
#i n c l u d e < stdlib .h > // d e f i n e m a l l o c
i n t main () {
const i n t m = max *(2*( z - a +1));
// P r i m a l i d a d e d e v e s e r t e s t a d a p a r a v a l o r e s m
char palavra [ max ];
i n t * primos = listaDePrimos ( m );
while ( ler ( palavra ))
printf ( " It is % s a prime word .\ n " ,
prima ( palavra , primos , m ) ? " " : " not " );
}
i n t minusc ( char let ) { ... como na Figura 8.1 ... }
i n t valor ( char let ) { ... como na Figura 8.1 ... }
i n t * listaDePrimos ( i n t m ) {
i n t p =2 , m2 = m +2 , nums [ m2 ] , * primos = malloc ( m ) ,
// nums [ 0 ] , nums [ 1 ] nao u s a d o s ( p o r s i m p l i c i d a d e , i . e .
// p a r a q u e n d i c e de nums c o r r e s p o n d a a numero e n t r e 2 e m) ;
i ,j , k ;
f o r ( i =2 , j =0; i < m2 ; i ++ , j ++) { nums [ i ]=0; primos [ j ] = 0; }
j = 0;
do {
// marca m u l t i p l o s de p
f o r ( i = p ; i < m2 ; i += p ) nums [ i ] = 1;
// p r o c u r a p r o x i m o v a l o r nao marcado a p a r t i r de j
f o r ( k = p +1; k < m2 ; k ++) i f (! nums [ k ]) break ;
p = k ; // p e o p r o x i m o primo
primos [ j ] = p ;
j ++;
} while (p < m2 );
return primos ;
}
i n t primo ( i n t n , i n t * primos , i n t m ) {
int i;
f o r ( i =0; i < m ; i ++)
i f ( primos [ i ] >= n ) return ( primos [ i ]== n );
e l s e i f ( primos [ i ] == 0) return 0;
return 0;
}
i n t ler ( char * palavra ) { ... como na Figura 8.1 ... }
i n t prima ( char * palavra , i n t * primos , i n t $m$ ) {
i n t i , somaLet =0;
f o r ( i =0; i < max && palavra [ i ] != \0 ; i ++)
somaLet += valor ( palavra [ i ]);
return primo ( somaLet , primos , m );
}
114
Captulo 9
Este captulo descreve como realizar operacoes de leitura e escrita de dados em arquivos, em
vez de em dispositivos de entrada e sada (E/S) padroes.
Como mostramos na secao 3.2.1, a entrada e escrita de dados em dispositivos de E/S padroes
podem ser redirecionadas para serem feitas em arquivos. A desvantagem da E/S em arquivos com
redirecionamento e que tanto o arquivo de entrada quanto o arquivo de sada de dados devem
ser unicos e ambos determinados antes da execucao do programa. Ao contrario, a E/S comum
em arquivos definida na linguagem C permite que operacoes de E/S de dados sejam realizadas em
diversos arquivos, bastando para isso que esses arquivos sejam abertos e fechados de acordo com o
que for necessario, durante a execucao de um programa.
Para realizar operacoes de E/S em arquivos, a linguagem C prove o tipo FILE (secao 9.1), usado
para acesso a arquivos, operacoes de abertura e fechamento de arquivos (secao 9.2) e operacoes
especficas de leitura e escrita em arquivos (secao 9.3).
Essas secoes tratam das operacoes necessarias para manipulacao de arquivos em geral, que
sao as seguintes, nesta ordem: declarar variavel de tipo ponteiro-para-arquivo, abrir um arquivo,
realizar operacoes de leitura ou escrita sobre o arquivo e fechar o arquivo.
FILE * arq ;
cria uma variavel de nome arq que e um ponteiro para um arquivo. Com essa declaracao,
a variavel arq podera ser usada em comandos subsequentes em operacoes de E/S em arquivos,
como descrito a seguir.
A biblioteca stdio da linguagem C define tres variaveis pre-definidas para acesso a arquivos,
que sao disponveis para uso em operacoes de entrada ou sada diretamente, sem necessidade de
serem abertos (abertura e fechamento de arquivos sao descritos na secao seguinte): stdin (entrada
padrao), stdout (sada padrao) e stderr (dispositivo padrao de erro).
O dispositivo de entrada padrao e usualmente o teclado (de onde sao lidos os dados, quando se
usa a funcao scanf ), a menos que um redirecionamento seja feito.
O dispositivo de sada padrao e usualmente a tela (onde os dados sao escritos, quando se usa a
funcao printf ), a menos que um redirecionamento seja feito.
O dispositivo padrao de erro e onde mensagens de erro devem ser escritas. Usualmente, esse
dispositivo e o mesmo que o dispositivo de sada padrao, a menos que um redirecionamento seja
feito. Note que redirecionamentos dos dispositivos padroes de erro e de sada sao independentes.
Por exemplo, um redirecionamento da sada padrao nao altera o dispositivo padrao de erro.
115
9.2 Abertura e Fechamento de arquivos
9.2.1 Abertura de arquivos
Para abrir arquivo, deve-se usar a funcao fopen, definida na biblioteca stdio.
abre arquivo de nome nomeDoArquivo para realizar operacoes de entrada ou sada, conforme
especificado por modo:
modo denota uma cadeia de caracteres que representa o modo como o arquivo deve ser
aberto, tipicamente "r" para operacoes de leitura em arquivos-texto ou "w" para operacoes
de escrita em arquivos-texto.
Arquivos podem ser arquivos-texto ou arquivos binarios. Arquivos-texto sao arquivos contendo
caracteres, e arquivos binarios sao arquivos contendo valores de outros tipos. Arquivos-texto
sao arquivos legveis, uma vez que consistem de uma sequencia de caracteres legveis, ao passo
que arquivos binarios sao ilegveis, uma vez que contem dados binarios, como representados na
memoria de computadores.
O modo "r" (abreviacao de read) requer que o nome do arquivo especificado exista, para que
o arquivo seja aberto para leitura.
O modo "w" (abreviacao de write) cria um novo arquivo com o nome especificado; caso o
arquivo ja exista, os dados de operacoes de escrita realizadas no arquivo irao ser realizados a partir
do incio do arquivo; os dados existentes sao perdidos.
Ha outros modos de abrir um arquivo alem de simplesmente para leitura ("r") ou escrita ("w")
em arquivos-texto:
O modo de abertura de arquivo "a" e usado para operacoes de adicionar dados no fim de um
arquivo (em vez de a partir do incio).
O sufixo "b" e usado para indicar abertura de arquivos binarios, em vez de arquivos-texto; os
modos "rb", "wb" e "ab", que indicam o mesmo que os correspondentes "rb", "wb" e "ab",
mas em arquivos binarios.
O sufixo "+" e usado para indicar que tanto operacoes de leitura quanto de escrita podem
ser realizadas no arquivo; por exemplo, r+ indica abertura de arquivo para leitura e escrita,
que deve existir, e w+ indica abertura de arquivo tambem para leitura e escrita, mas que nao
precisa existir (se existir, serao perdidos; os dados de operacoes de escrita irao ser realizados
a partir do incio do arquivo).
O sufixo "+" deve ocorrer apos o sufixo b que indica arquivos binarios; por exemplo, para
abertura de arquivos binarios para operacoes tanto de leitura quanto de escrita devem ser
usados os modos "rb+", "wb+" ou "ab+".
A funcao fopen retorna o ponteiro NULL se ocorrer algum erro na abertura do arquivo. Por
exemplo, o valor NULL e retornado se um arquivo nao existente for aberto para leitura, com modo
"r".
O exemplo a seguir contem varios comandos de abertura de arquivos, usando fopen:
116
FILE * arqEntrada , * arqSaida ;
char * modo = " r " ;
char nomeArqSaida [] = " saida . txt " ;
i f ( arqEntrada == NULL )
printf ( " entrada . txt nao existe ou nao pode ser aberto .\ n " );
else {
arqSaida = fopen ( nomeArqSaida , " w " );
i f ( arqSaida == NULL )
printf ( " % s nao pode ser aberto .\ n " , nomeArqSaida );
}
Repetindo: ao usar o modo "r", o arquivo precisa existir, ao passo que, ao usar o modo "w", o
arquivo e criado para realizacao de operacoes de escrirta e, se ele existir, seu conteudo anterior as
operacoes de escrita sera perdido.
fclose ( arq );
O fechamento de uma arquivo pode ser especialmente importante no caso de arquivos de sada.
O motivo e que as operacoes de sada sao usualmente realizadas, por questao de eficiencia, em areas
temporarias da memoria (chamadas, em ingles, de buffers), antes de serem transferidas para o
arquivo. Somente quando o arquivo e fechado ou quando nao ha mais espaco na area temporaria
e que a operacao de escrita no arquivo e efetivamente realizada.
Desta forma, se o fechamento de um arquivo nao for realizado, os dados podem nao aparecer
no arquivo de sada, apesar de terem sido escritos na area temporaria.
117
e equivalente a:
i f ( arq != NULL )
while (1) {
testeFim = scanf ( " % d " , & val );
i f ( testeFim == EOF ) break ;
fprintf ( arq } , val );
}
118
poderamos usar:
i f ( arq != NULL )
while ( feof ( arq )) {
scanf ( " %d , & val );
fprintf ( arq , val );
}
Entretanto, como mencionamos, e prefervel nesse caso testar o resultado da chamada de scanf
para evitar erros de leitura de dados que sejam relacionados aos proprios dados sendo lidos em vez
de serem referentes a nao existencia de dados a serem lidos (isto e, em vez de serem referentes a
fim-de-arquivo).
1. Endereco da area de memoria no qual sera lido o valor (no caso de fread), ou do qual
sera copiado o valor (no caso de fwrite).
O endereco e usualmente determinado com uso de & v, onde v e o nome de uma variavel,
como acontece quando usamos scanf.
3. Numero de elementos. Para ler ou escrever um arranjo de elementos de tipo t, pode-se passar
o numero de elementos do arranjo. Em outros casos, o numero de elementos e igual a 1.
Ambas as funcoes fread e fwrite podem ser usadas para ler ou escrever quaisquer tipos de
dados, como arranjos e registros.
A funcao fread retorna o numero de elementos realmente lidos. Por exemplo, se fread for
usado para ler um arranjo de 100 inteiros mas o arquivo so contem 30 inteiros, o valor retornado
sera 30.
Para verificar se o fim do arquivo foi alcancado, a funcao feof pode ser usada. A funcao
feof recebe um ponteiro-para-arquivo e retorna verdadeiro se e somente se a posicao corrente (de
leitura ou escrita no arquivo) e a do fim do arquivo.
Considere os seguintes exemplos de programas que realizam operacoes de E/S em arquivos
binarios. O primeiro le varios inteiros do dispositivo de entrada padrao e escreve esses inteiros em
um arquivo binario. O segundo programa le, do dispositivo de entrada padrao, nomes de arquivos
binarios, com valores inteiros, de entrada e sada, e copia o conteudo do arquivo de entrada para o
de sada. O terceiro programa le, do dispositivo de entrada padrao, um nome de arquivo e mostra
o conteudo desse arquivo no dispositivo de sada padrao.
119
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
i n t main () {
FILE * arq = fopen ( " dados . int " , " wb " );
int n, v;
while (1) {
n = scanf ( " % d " , & v );
i f ( n != 1) break ;
fwrite (& v , s i z e o f ( i n t ) , 1 , arq );
}
fclose ( arq );
}
2. Escreva um programa que leia, do dispositivo de entrada padrao, dois nomes de arquivos
(nomes com no maximo 29 caracteres), e copie o conteudo do primeiro arquivo binario, de
valores inteiros, para o segundo. O programa termina quando nao ha mais dados a serem
lidos.
Solucao:
Solucao:
120
#i n c l u d e < stdio .h >
#i n c l u d e < stdlib .h >
i n t main () {
char max = 30;
char nomeArq = malloc ( max * s i z e o f ( char ));
9.5 Exerccios
1. Escreva um programa que leia, do dispositivo de entrada padrao, os nomes de dois arquivos e
imprima mensagem indicando se os conteudos dos dois arquivos, que voce pode considerar que
sao arquivos-texto, sao iguais entre si ou nao. Alem disso, se nao forem iguais, a mensagem
deve conter, alem da informacao de que sao diferentes, qual e a posicao do primeiro caractere
distinto encontrado, e quais sao estes caracteres distintos, nos dois arquivos. A posicao i e a
do i-esimo caractere (posicoes comecam de 1). O programa deve testar se os arquivos existem
e emitir mensagem apropriada, no dispositivo padrao de erro, caso um deles ou ambos nao
existam.
2. Escreva um programa que leia, do dispositivo de entrada padrao, os nomes de dois arquivos
e copie o conteudo do primeiro arquivo para o segundo, de modo que no final o segundo seja
uma copia do primeiro. Considere que os arquivos sao arquivos-texto. O programa deve
testar se o arquivo de entrada existe e emitir mensagem apropriada, no dispositivo padrao
de erro, caso nao exista.
3. Escreva um programa que leia, do dispositivo de entrada padrao, os nomes de tres arquivos-
texto, digamos arq1, arq2 e arq3, e crie um arquivo, de nome arq3, cujo conteudo seja
igual ao conteudo de arq1 seguido do conteudo de arq2. O programa deve emitir mensagem
no dispositivo padrao de erro caso arq1 ou arq2 nao exista ou outro erro ocorrer na abertura
dos arquivos.
121
122
Bibliografia
[1] Adele Goldberg, David Robinson. Smalltalk-80: The Language. Addison-Weslay, 1989.
[3] Angelo Guimaraes, Newton Lages. Algoritmos e Estruturas de Dados. Ltc Editora, 1988.
[4] Brian Kernighan, Dennis Ritchie. The C Programming Language. Prentice Hall, 1988.
[5] Bryan OSullivan, Don Stewart, John Goerzen. Real World Haskell. OReilly, 2008.
[6] Cordelia Hall, John ODonnell. Discrete Mathematics Using a Computer. Springer, 2000.
[7] David Patterson, John Hennessy. Computer Organization and Design: The Hardware Sofwtare
Interface. Morgan Kaufmann, 2a edition, 2002.
[9] Fethi Rabhi and Guy Lapalme. Algorithms: A functional programming approach. Addison-
Wesley, 1999.
[10] James Gosling et al. The Java Language Specification. Addison-Wesley, 2a edition, 2000.
[11] J. Ichbiah. Rationale for the design of the Ada programming language. ACM SigPlan Notices,
14(6B), 1979.
[12] Kathleen Jensen, Nicklaus Wirth. Pascal User Manual and Report. Springer-Verlag, 1991
(edicoes anteriores: 1974, 1985).
[13] Keith Devlin. Mathematics: The New Golden Age. Penguin Books & Columbia University
Press, 1999.
[14] Leon Sterling, Ehud Shapiro. The Art of Prolog. The MIT Press, 1994.
[16] Nicklaus Wirth. Algorithms + Data Structures = Programs. Prentice Hall, 1976.
[18] Nivio Ziviani. Projeto de Algoritmos com Implementcoes em Pascal e C. Pioneira Thomson,
2004.
[19] Ole-Johen Dahl, Bjorn Myrhaug, Kristen Nygaard. Simula an Algol-based Simulation
Language. Communications of the ACM, 9(9):671678, 1966.
[20] Lawrence Paulson. ML for the Working Programmer. Cambridge University Press, 1996.
Second edition.
[21] Robert Sedgewick and Kevin Wayne. Algorithms in C: Parts 1-4: Fundamentals, Data Struc-
tures, Sorting, Searching. Addison-Wesley, 3a edition, 1998.
[22] Roger Penrose. The Emperors New Mind: Concerning Computers, Minds and The Laws of
Physics. Oxford University Press, 1989.
123
[23] Samuel Harbison. Modula-3. Prentice Hall, 1992.
[24] Michael Sipser. Introduction to the Theory of Computation. PWS Publishing Company, 1997.
[25] Bjarne Stroustrup. The C++ Programming Language. Addison-Wesley, 3a edition, 1991.
[26] T. H. Cormen and others. Introduction to Algorithms. MIT Press, 2001.
[27] Simon Thompson. Haskell: The Craft of Functional Programming. Addison-Wesley, 2a edition,
1999.
[28] Ulf Nilsson, Jan Maluszynski. Logic Programming and Prolog. John Wiley & Sons, 2a edition,
1995.
[29] Daniel Velleman. How to Prove It: A Structured Approach. Cambridge University Press, 2a
edition, 2006.
[30] William Stallings. Arquitetura e Organizacao de Computadores: Projeto para o Desempenho.
Pearson Education do Brasil (traducao), 5a edition, 2002.
124
Apendice A
Escolha da linguagem C
O livro adota a linguagem de programacao C. A escolha dessa linguagem foi motivada pela
necessidade de homogeneizacao no ensino de disciplinas introdutorias de programacao de compu-
tadores nos cursos de varias universidades (por exemplo, nas disciplinas de Algoritmos e Estruturas
de Dados I do Departamento de Ciencia da Computacao da Universidade Federal de Minas Ge-
rais). O uso da linguagem C apresenta, reconhecidamente, vantagens no ensino de tais disciplinas
para cursos como os de Engenharia Eletrica e Engenharia de Controle e Automacao, por se tratar
de linguagem adequada a chamada programacao de sistemas, na qual se faz acesso direto a dispo-
sitivos e recursos de hardware. Para tais sistemas, a programacao na linguagem C e adequada pois
a linguagem permite acesso direto aos dispositivos e recursos de hardware, e portanto bibliotecas
e programas que fazem tais acessos diretos ao hardware podem ser mais facilmente encontrados e
usados. Para outros cursos, o uso da linguagem C e controverso, pelo fato de existir na linguagem
uma preocupacao central com eficiencia, e possibilidade de acesso direto a areas de memoria, o que
leva, principalmente, a duas consequencias indesejaveis do ponto de vista de um aprendizado em
programacao:
1. Ausencia, em muitos casos, de verificacao dos chamados erros de tipo. Tais erros ocorrem
devido ao uso de valores em contextos inadequados, ou seja, em contextos nos quais nao faz
sentido usar tais valores, e portanto nos quais tais valores nao deveriam ser usados.
Grande parte do desenvolvimento das linguagens de programacao nos dias atuais e relacio-
nado ao objetivo de tornar as linguagens cada vez mais seguras, no sentido de possibilitar a
detecao de um numero cada vez maior de erros de tipo, e ao mesmo tempo dando flexibi-
lidade ao programador, de modo que ele nao tenha que especificar ou mesmo se preocupar
com a anotacao explcita de tipos em seus programas, e procurando manter o algoritmo que
permite essa detecao de erros simples e eficiente.
Tipos algebricos: tipos que permitem representar disjuncao (ou) entre tipos e estru-
turas de dados, de modo seguro e simples. Tipos algebricos sao usados comumente, para
representar estruturas muito comuns em computacao, tais como:
enumeracoes, denotadas por tipos que consistem em uma enumeracao de todos
os possveis elementos do tipo, como por exemplo um tipo booleano (cada valor
consistindo de falso ou verdadeiro), ou estacoes do ano (primavera, verao, outono
ou inverno), etc.;
alternativas que estendem enumeracoes de modo a permitir diferentes modos de
construir valores, tal como por exemplo uma forma geometrica, que pode ser um
crculo com o tamanho de seu raio, ou um retangulo com os tamanhos de seus dois
lados etc.
estruturas recursivas que estendem alternativas por permitir recursividade na sua
definicao, como por exemplo uma lista, que pode ser vazia ou um elemento seguido
125
do restante da lista (tal restante consistindo, recursivamente, de uma lista), uma
arvore binaria, que pode ser uma folha contendo um valor de um determinado tipo
ou um par formado por duas subarvores, etc.;
Polimorfismo: tanto de valores (estruturas de dados) que podem ser instanciados para
quaisquer tipos, mantendo a mesma forma, quanto de funcoes que realizam operacoes
sobre tais estruturas de dados, que funcionam do mesmo modo, independentemente da
instancia sobre a qual realizam a operacao.
A ausencia de suporte a polimorfismo em C impede, em particular, que existam bibliote-
cas com funcoes de manipulacao de estruturas de dados de proposito geral como pilhas,
filas, dicionarios, conjuntos etc., comuns em linguagens mais modernas.
126