Livro Progc
Livro Progc
Livro Progc
1 Introdução 6
1.1 Histórico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Arquitetura de Computadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.1 Memória . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.2 Processador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3 Algoritmos e Programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.4 Técnica de Desenvolvimento de Programas . . . . . . . . . . . . . . . . . . . . . 11
1.5 Partes de um Programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.6 Tradução de Programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.6.1 Compilação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.6.2 Interpretação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.7 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.8 Exercı́cios Propostos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2 Conceitos Básicos 18
2.1 Variáveis e Células de Memória . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2 Identificadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3 Comando de Atribuição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.4 Tipos de Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.4.1 Declaração de Variáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.4.2 Tipo Inteiro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.4.3 Tipo Ponto Flutuante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4.4 Tipo Booleano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.4.5 Tipo Caractere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.4.6 Conversão de Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.5 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.6 Expressões . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.6.1 Expressões Aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.6.2 Expressões Relacionais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.6.3 Expressões Lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.7 Comando de Entrada de Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1
2 SUMÁRIO
3 Modularização 70
3.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
3.2 Subprogramas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.3 Partes de um Subprograma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
3.3.1 Cabeçalho . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
3.3.2 Dicionário de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.3.3 Corpo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
3.3.4 Comentários . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.4 Chamada de subprogramas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3.5 Passagem de parâmetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
3.6 Retorno de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.6.1 Encerramento antecipado de execução . . . . . . . . . . . . . . . . . . . . 84
3.7 Funções sem lista de parâmetros . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.8 Funções sem retorno de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
3.9 Recursividade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.9.1 Implementação não recursiva equivalente . . . . . . . . . . . . . . . . . . 90
3.10 Exercı́cios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.11 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
3.12 Exercı́cios Propostos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
3.13 Trabalhos Sugeridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5 Vetores 135
5.1 Vetores e sua importância . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
5.2 Representação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5.3 Definição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5.3.1 Definição do Tamanho do Vetor . . . . . . . . . . . . . . . . . . . . . . . . 139
5.4 Operações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
5.4.1 Acesso indevido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
5.5 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
5.6 TAD Implementacional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
5.6.1 Atributos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
5.6.2 Operações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
5.7 Exercı́cios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
5.8 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
5.9 Lista de Exercı́cios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
5.10 Trabalhos Sugeridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
6 Matrizes 164
6.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
6.2 Definição e Acesso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
6.2.1 Definição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
6.2.2 Acesso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
6.3 O TAD implementacional tMatriz . . . . . . . . . . . . . . . . . . . . . . . . . . 168
6.3.1 Atributos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
6.3.2 Operações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
6.4 Exercı́cios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
6.5 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
6.6 Exercı́cios Propostos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
6.7 Trabalhos Sugeridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
6.8 Tópicos Avançados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
4 SUMÁRIO
7 Apontadores 187
7.1 Variáveis Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
7.2 A Sintaxe dos Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
7.2.1 Operador Endereço de Memória . . . . . . . . . . . . . . . . . . . . . . . 190
7.2.2 O Operador Seta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
7.3 Acesso à Variável por Meio de Apontadores . . . . . . . . . . . . . . . . . . . . . 191
7.4 Uso de Apontadores nas Passagens de Parâmetros . . . . . . . . . . . . . . . . . 192
7.5 Alocação Dinâmica de Memória . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
7.6 Problemas Gerados por Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . 198
7.6.1 Apontadores Não Inicializados . . . . . . . . . . . . . . . . . . . . . . . . 198
7.6.2 Objetos Pendentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
7.6.3 Referência Pendente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
7.6.4 Programação Macarrônica . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
7.7 TAD Implementacional Lista Encadeada tLista . . . . . . . . . . . . . . . . . . . 200
7.7.1 Definição do Tipo tNo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
7.7.2 Atributos de tLista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
7.7.3 Operações de tLista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
7.7.4 Uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
7.8 Exercı́cios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
7.9 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
7.10 Lista de Exercı́cios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
7.11 Trabalhos Sugeridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
8 Arquivos 235
8.1 Variáveis Transientes X Variáveis Persistentes . . . . . . . . . . . . . . . . . . . . 235
8.2 Tipos de Arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
8.2.1 Tipos de Arquivos - Arquivos Texto . . . . . . . . . . . . . . . . . . . . . 236
8.2.2 Tipos de Arquivos - Arquivos Binários . . . . . . . . . . . . . . . . . . . . 237
8.3 Definição de arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
8.4 Operação sobre arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
8.4.1 Abertura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
8.4.2 Fechamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
8.5 Operações sobre arquivos texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
8.5.1 Leitura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
8.5.2 Escrita . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
8.6 Operações sobre arquivos binários . . . . . . . . . . . . . . . . . . . . . . . . . . 244
8.6.1 Leitura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
8.6.2 Escrita . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
8.7 Outras funções úteis para arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . 246
8.7.1 feof() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
8.7.2 fseek() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
8.8 Exercicios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
SUMÁRIO 5
Introdução
Co-autor:
André Boechat
Objetivos:
• Definir o que são algoritmos e programas, além de apresentar algumas técnicas para de-
senvolvê-los;
1.1 Histórico
Dado o alto grau tecnológico dos computadores atuais, pode ser difı́cil imaginar que os primeiros
computadores eram totalmente mecânicos. Diversos tipos foram projetados e construı́dos ao
longo da evolução, chegando aos modernos computadores digitais; porém, alguns se destacam
pela inovação e complexidade que marcaram suas épocas.
6
1.1. HISTÓRICO 7
A primeira máquina programável que se tem notı́cia foi construı́da pelo professor de ma-
temática da Universidade de Cambridge, Charles Babbage (1792-1871). A “máquina analı́tica”,
como ficou conhecida, era totalmente mecânica, composta basicamente por engrenagens que
formavam quatro componentes: a memória, a unidade de cálculo ou computação, a unidade
de entrada e a de saı́da. A unidade de computação recebia operandos da memória para reali-
zar sobre eles as operações de soma, subtração, multiplicação ou divisão, e depois armazenar o
resultado na memória.
Como a máquina analı́tica executava as instruções lidas pela unidade de entrada, era possı́vel
executar diferentes seqüências de cálculos, bastando para isso que um programa diferente fosse
utilizado. Assim, a primeira pessoa no mundo a programar um computador foi a jovem Ada
Augusta Lovelace, contratada pelo próprio Babbage a fim de produzir o software necessário para
o funcionamento da máquina.
A procura por máquinas calculadoras cresceu com a Segunda Guerra Mundial, estimulando
o surgimento dos primeiros computadores eletrônicos. O professor de fı́sica da Universidade da
Pensilvânia, John Mauchley, junto com seu aluno de mestrado, J. Presper Eckert, construiu
um computador chamado ENIAC (Electronic Numerical Integrator And Computer )[2], o qual
detinava-se ao cômputo de trajetórias táticas que exigissem conhecimento substancial em ma-
temática. O ENIAC tinha 18.000 válvulas e 1.500 relés, pesava 30 toneladas e consumia 140
quilowatts de energia elétrica. Para programar o ENIAC, era necessário ajustar a posição de
6.000 chaves de várias posições e conectar um número imenso de soquetes por meio de uma ver-
dadeira floresta de cabos. Este gigantesco computador só ficou pronto em 1946, após o término
da guerra.
Um dos pesquisadores envolvidos no projeto do ENIAC, John von Neumann, construiu para
o Instituto de Estudos Avançado de Princeton (Princeton Institute of Advanced Studies — IAS )
a máquina IAS[3], a qual ainda é a base de praticamente todas as máquina atuais. Ele imaginou
que os programas poderiam ser representados em formato digital na memória, junto com os
dados.
A invenção do transı́stor e o desenvolvimento de circuitos integrados revolucionaram os pro-
jetos de computadores do final da década de 1950, tornando obsoletos os computadores valvula-
dos. Nas décadas de 1960 e 1970, as famı́lias de computadores da IBM1 (International Business
Machines), System/360, e da DEC2 (Digital Equipament Corporation), PDP-11, dominavam o
mercado.
Nesse perı́odo surgiu uma das linguagens de programação mais usadas para o desenvolvi-
mento de softwares e também adotada neste livro, a linguagem C[1]. Ela é, ainda hoje, muito uti-
lizada para a criação de programas diversos, como processadores de texto, planilhas eletrônicas,
programas para a solução de problemas de engenharia, e muitos outros.
Diversos fatores, como a criação de linguagens de programação mais semelhantes à linguagem
humana3 , o que facilitava a vida dos programadores, e a integração de circuitos em escala muito
alta, o que aumentou o desempenho e diminuı́u o tamanho das máquinas, deram inı́cio a era
1
http://www.ibm.com
2
http://www.hp.com
3
Essas linguagens são chamadas de “linguagens de alto nı́vel”
8 CAPÍTULO 1. INTRODUÇÃO
dos computadores pessoais. E foi nesse contexto, na década de 80, que a IBM construiu o
computador mais vendido de toda a história, o Personal Computer — o famoso PC.
O processador utilizado no PC foi construı́do por uma promissora empresa da época, a Intel4 .
E a versão inicial desse computador vinha com o sistema operacional MS-DOS fornecido por
outra empresa, a recém-criada Microsoft Corporation.
1.2.1 Memória
A memória é a parte do computador onde as operações a serem executadas pelo computador
(instruções) e as informações a serem processadas pelo mesmo (dados) são armazenados.
Na memória, o processador lê e escreve informações ao executar as instruções de um pro-
grama. Existem dois tipos de memória: a memória principal e a secundária. A principal,
conhecida também como RAM (Random Access Memory), possui as seguintes caracterı́sticas:
• Proporciona ao computador acesso rápido aos dados e instruções armazenados por ela;
1.2.2 Processador
O processador é o “cérebro” do computador. Basicamente, o processador é responsável por
buscar instruções na memória, decodificá-las para determinar seus operandos (dados que serão
4
http://www.intel.com
1.3. ALGORITMOS E PROGRAMAS 9
usados ao processar a instrução) e quais operações devem ser realizadas com os mesmos, e
executar tais operações. Essas tarefas compõem o processo de execução de um programa.
8. Enxugar o rosto.
Após conhecer um exemplo comum de algoritmo, torna-se mais fácil compreender a sua definição:
Apesar de conseguirem executar operações muito complexas, os computadores não são do-
tados da mesma capacidade de compreensão que os humanos. Uma seqüência de tarefas consi-
derada simples e óbvia para uma pessoa qualquer pode ser incompreensı́vel e pouco detalhada
para um computador.
Voltando ao exemplo da escovação, um computador poderia considerá-lo incompleto, pois
há diversas questões sobre como algum passo pode ser executado, por exemplo:
3. O que fazer se não houver pasta? Escovar mesmo sem a pasta ou interromper o processo
de escovação?
Questões como essas são facilmente contornadas por um humano, decididas instantâneamente
de acordo com o ambiente que o cerca. Então, para que uma máquina também possa “decidir”
o que fazer em situações semelhantes, é necessário que se estabeleçam as condições iniciais
(condições de entrada) e as finais (condições de saı́da) do problema. Dessa forma, ela saberá
quais ferramentas poderão ser usadas para atingir a condição de saı́da desejada.
Assim, o problema da escovação seria mais bem definido da seguinte maneira:
Condições de Entrada: Dentes sujos com restos de alimentos, uma escova dental em condições
de uso, 90 gramas de creme dental e 300 mililitros de água tratada.
Condições de Saı́da: Dentes limpos (sem restos de alimentos visı́veis), uma escova dental em
condições de uso e 85 gramas de creme dental. Toda a quantidade de água deve ser
utilizada.
Portanto, para um computador, os algoritmos definem o conjunto de atividades que ele deve
desempenhar para solucionar um problema.
Contudo, tão importante quanto saber “o que” escrever para a máquina é saber “como”
escrever. Para que um computador possa executar um algoritmo, é necessário que esse algoritmo
seja traduzido para uma linguagem de programação, geralmente incompreensı́vel para a maioria
das pessoas. Ao se traduzir um algoritmo para uma linguagem de programação, obtém-se um
programa.
8. Enxugar o rosto.
1. Enquanto não encontrar a escova e o tubo de pasta, continuar procurando por cada gaveta
do armário;
5
Também conhecida como “dividir para conquistar”.
12 CAPÍTULO 1. INTRODUÇÃO
2. Caso tenham acabado as gavetas e não tenha encontrado a escova e o tubo, interromper a
tarefa.
O exemplo anterior mostra apenas parte do processo de refinamentos, que deve prosseguir
até que cada atividade esteja suficientemente detalhada, possibilitando que o computador as
reconheça e execute.
• Nome da receita;
• Modo de preparo: descreve a forma de trabalhar com os ingredientes para que se obtenha
o resultado esperado;
• Documentação: explica certos aspectos não muito claros do programa, tanto no corpo do
programa quanto no cabeçalho ou no dicionário de dados.
Pseudocódigo 1.1 Cálculo das raı́zes reais de uma equação de segundo grau.
Descrição: programa que calcula as raı́zes reais de uma equação de segundo grau.
Dados de Entrada: os três coeficientes da equação.
Saı́da do Programa: raı́zes reais da equação.
Leitura dos coeficientes da equaç~ao;
Cálculo do discriminante (delta);
Cálculo das raı́zes pelo processo componente "Cálculo das Raı́zes";
Utilizando a técnica dos refinamentos sucessivos, na última linha do Pseudocódigo 1.1 ocorre
a execução do processo componente Cálculo das Raı́zes, que, a partir dos valores dos coeficien-
tes lidos e do discriminante calculado, encontra os valores das raı́zes reais da equação. No geral,
o processo componente Cálculo das Raı́zes deve conter os passos descritos no Pseudocódigo
1.2.
No Exemplo 1.1, o programa para o cálculo das raı́zes está transcrito para a linguagem de
programação C. Deve-se destacar que o texto escrito entre os sı́mbolos “/*” e “*/” é sempre
ignorado pelo computador, não alterando em nada o funcionamento do programa. Porém, esse
texto serve para comentar o código e deixá-lo mais claro. Pode-se fazê-los também utilizando o
sı́mbolo “//”, que transforma em comentário tudo aquilo que estiver a sua direita, até o final
da linha.
1 /*
2 Programa para cálculo das raı́zes de segundo grau .
3 Dados de entrada : os coeficientes a , b e c de uma equaç~ ao
4 da forma ax ^2 + bx + c = 0
5 Dados de saı́da : imprime na tela as raı́zes reais da equaç~a o , caso existam .
6 Restriç~
a o : N~
a o se considera o zero como um possı́vel valor de a .
7 */
8
9 # include < math .h >
10 # include < stdio .h >
11
12 main () {
13 float a ; // Coeficiente angular .
14 float b ; // Coeficiente linear .
15 float c ; // Termo independente .
16 float delta ; // Discriminante .
17 float raiz1 ; // A primeira raiz .
18 float raiz2 ; // A segunda raiz .
19
20 // Leitura dos coeficientes .
21 scanf ( " % f % f % f " ,&a , &b , & c ) ;
22
23 // Cálculo do discriminante ( delta ) .
24 delta = b * b - 4 * a * c ;
25
26 // Cálculo das raı́zes .
27 if ( delta < 0) {
28 printf ( " A equaç~
a o n~
a o possui raı́zes reais " ) ;
29 } else {
30 raiz1 = ( - b + sqrt ( delta ) ) / (2* a ) ;
31 raiz2 = ( - b - sqrt ( delta ) ) / (2* a ) ;
32 printf ( " As raı́zes da equaç~ao s~
a o : % f e % f " , raiz1 , raiz2 ) ;
33 }
34 }
Exemplo 1.1: Programa para o cálculo das raı́zes reais de uma equação de segundo grau.
Na linguagem C, o cabeçalho dos programas sempre tem o nome main() e o inı́cio do corpo
do programa é marcado pela chave “{”, assim como o final é marcado pela última chave “}”.
As linhas 9 e 10 permitem o uso dos comandos sqrt e printf (o significado desses comandos é
explicado no próximo capı́tulo).
1.6. TRADUÇÃO DE PROGRAMAS 15
1.6.1 Compilação
O processo de compilação efetua a tradução integral do programa fonte para o código de máquina.
Uma vez traduzido, o programa em linguagem de máquina pode ser executado diretamente. A
Figura 1.2 ilustra esse processo.
A grande vantagem desse método de tradução é a rapidez na execução dos programas, pois
não é necessário fazer qualquer tradução durante a execução. Porém, o código objeto gerado pela
compilação é, geralmente, especı́fico para um determinado tipo de arquitetura de computador.
Assim, é necessário uma nova compilação do programa para cada tipo diferente de computador.
A linguagem C, adotada neste livro, é um exemplo de linguagem de programação compilada.
1.6.2 Interpretação
No processo de interpretação, cada instrução do código fonte é traduzida no instante imedia-
tamente anterior à execução dela. Desse modo, em contraste com a compilação, a tradução e
execução de programas interpretados podem ser vistas como um processo único (veja as Figuras
1.2 e 1.3).
16 CAPÍTULO 1. INTRODUÇÃO
Apesar de ser um processo mais lento quando comparado ao método anterior, a interpretação
apresenta maior flexibilidade na construção de programas e facilidade de depuração de código.
A presença do interpretador durante a execução permite, por exemplo, a execução de trechos
de programas criados, alterados ou obtidos durante a própria execução.
1.7 Resumo
• O processador é o componente do computador responsável por executar os programas
armazenados na memória.
• Algoritmo é uma seqüência de operações que deve ser executada em uma ordem definida
e não-ambı́gua, com o propósito de solucionar um determinado problema. Ao traduzir-se
um algoritmo para uma linguagem de programação, obtém-se um programa.
• Um programa bem estruturado deve conter as seguintes partes bem definidas: cabeçalho,
dicionário de dados, corpo e documentação.
1.8. EXERCÍCIOS PROPOSTOS 17
3. Considere uma memória que possua células de 8 bits cada uma. Qual o número de possı́veis
informações diferentes cada célula pode armazenar?
Conceitos Básicos
Co-autores:
André Boechat
Bruno Pandolfi
Objetivos:
• Apresentar o conceito de variável;
18
2.1. VARIÁVEIS E CÉLULAS DE MEMÓRIA 19
Como dito na Seção 1.2.1, a memória pode ser entendida como uma seqüência de células,
cada célula podendo armazenar uma porção dos dados de um programa. Graças a essa ordenação
seqüencial, cada célula possui um número identificador chamado endereço, que está relacionado
com a posição que a célula ocupa na seqüência. Por meio dos endereços é possı́vel acessar
qualquer célula para ler ou alterar seu conteúdo. A Figura 2.1 esboça essa idéia, porém, em
contraste com a Figura 1.1, omite a representação dos bits.
Figura 2.1: Modelo para a estrutura da memória do computador, mostrando uma fração de seis
células, seus endereços e conteúdos.
As seis células de memória da Figura 2.1 podem guardar valores numéricos ou letras em seus
interiores. Cada célula é apontada por um endereço único e seqüencial.
A estrutura da memória favorece a execução de suas duas operações básicas: leitura e escrita.
A leitura é o processo de consulta do conteúdo da célula. Essa operação não altera o valor
guardado na célula.
A escrita é o processo de armazenamento de um novo dado na célula indicada por um
endereço conhecido. Após a execução da escrita, o valor que estava anteriormente armazenado
na célula é perdido.
Um algoritmo para o cálculo do valor da conta mensal de energia elétrica é um bom exemplo
para compreender as operações básicas da memória. Os dados de entrada desse algoritmo são: a
leitura atual do medidor, a leitura registrada no mês anterior e o preço do quilowatt-hora (kWh)
— unidade comercial de energia elétrica.
Na prática, o que se faz para calcular o valor da conta é encontrar a diferença entre a leitura
atual do medidor e a leitura do mês anterior e multiplicar esse valor pelo preço do quilowatt-hora.
O algoritmo computacional, por sua vez, é descrito no Pseudocódigo 2.1.
No Pseudocódigo 2.1, o computador lê os conteúdos das células 43 e 46 (que guardam os
valores das leituras atual e anterior, respectivamente), realiza a subtração e o resultado é guar-
dado na célula 41. Posteriormente, essa diferença é lida e seu valor multiplicado pelo conteúdo
da célula 44, que guarda o valor do preço unitário do kWh. Finalmente, o valor da conta é
guardado na célula 42.
Acompanhando a descrição acima é possı́vel chegar à situação encontrada na Figura 2.2.
Nos primórdios da programação, percebeu-se que acessar a memória por meio de endereços
era trabalhoso demais e causa constante de erros. Isso porque o programador deveria escolher os
endereços das células com as quais iria trabalhar, tanto das células que teriam valores a serem
lidos quanto das que seriam usadas para a escrita de resultados.
Essa situação confusa é observável no exemplo da conta de energia elétrica. Sem o comentário
20 CAPÍTULO 2. CONCEITOS BÁSICOS
Pseudocódigo 2.1 Cálculo da conta de energia elétrica utilizando os endereços das células de
memória.
Descrição: programa que calcula o valor da conta de energia elétrica.
Dados de Entrada: células 43 e 46 contendo, respectivamente, a leitura atual e a anterior; e valor
unitário do kWh na célula 44.
Saı́da do Programa: valor da conta, armazenada na célula 42.
Subtrair o conteúdo da célula 43 do conteúdo da célula 46 e guardar na
célula 41;
Multiplicar o conteúdo da célula 41 pelo conteúdo da célula 44 e guardar na
célula 42;
Figura 2.2: Conteúdo da memória após execução do algoritmo que calcula o valor de uma conta de
energia elétrica.
no inı́cio do Pseudocódigo 2.1, seria impossı́vel ter a mı́nima idéia do que o programa faz em
cada passo. Isso torna difı́cil não só a escrita, mas também a correção dos programas.
Para resolver essa questão, o conceito de variável foi criado. Uma variável nada mais é do
que uma abstração para o endereço de memória. Com o emprego de variáveis, as células de
memória são referenciadas nos programas por meio de rótulos, definidos com ajuda do bom-
senso do programador. O compilador fica encarregado do trabalho de transformar rótulos em
endereços para que as operações de acesso à memória sejam realizadas.
O Pseudocódigo 2.2 mostra como fica o programa que calcula o valor da conta de energia
elétrica, agora escrito utilizando variáveis.
A partir do Pseudocódigo 2.2, um compilador pode converter os rótulos para posições quais-
quer na memória, evitando que o programador tenha que se preocupar em manipulá-las. Uma
2.2. IDENTIFICADORES 21
Figura 2.3: Uma possı́vel tradução para endereços de memória, a partir dos rótulos do código que utiliza
variáveis.
2.2 Identificadores
Em geral, as linguagens de alto nı́vel possuem dois tipos de elementos: os elementos definidos
pela própria linguagem — sı́mbolos para operadores, nome de comandos etc — e os elementos
definidos pelo programador — identificadores, comentários etc.
Um identificador é um sı́mbolo que pode representar alguma entidade criada pelo progra-
mador, como uma variável, por exemplo. Cada linguagem define uma regra para formação de
identificadores. Em geral, sempre é possı́vel utilizar uma seqüência de caracteres alfanuméricos
— letras ou dı́gitos, sem acentos e sem cedilha — sendo que o primeiro caractere deve ser obri-
gatoriamente alfabético. Os Exemplos 2.1 e 2.2 apresentam, respectivamente, nomes corretos e
nomes inválidos de variáveis na linguagem C.
Algumas linguagens, como C, fazem diferenciação entre letras maiúsculas e minúsculas.
Desta forma, uma variável de nome Saldo é considerada diferente de outra de nome saldo,
ou, ainda, de nome SALDO. É importante ressalvar que não é uma boa prática de programação
criar identificadores que apenas se diferenciem pelo formato das letras, como no exemplo do
saldo, pois isso tem impacto negativo na legibilidade dos programas.
22 CAPÍTULO 2. CONCEITOS BÁSICOS
1 abc
2 x1
3 y2
4 letra
5 SOMA_TOTAL
6 B_32
1 fim ? // ‘ ‘? ’ ’ n~
a o é um caractere alfanumérico
2 % percentual % // ‘ ‘% ’ ’ n~
a o é um caractere alfanumérico
3 123 quatro // Iniciado por número
4 ! hola ! // ‘ ‘! ’ ’ n~
a o é um caracter alfanumérico
5 @ARROBA // ‘‘@ ’’ n~ a o é um caractere alfanumérico
Normalmente, em grandes projetos de software, são adotados padrões para a escrita dos
identificadores a fim de que os programadores possam trocar seus códigos, entendê-los e alterá-
los sem grande dificuldade. Neste texto, é adotado como regra na escrita:
• Nomes compostos: primeira parte é iniciada por letra minúscula e as demais partes inici-
adas por letra maiúscula. Os demais caracteres são minúsculos.
Como dito na Seção 1.5, um bom programa deve necessariamente ser legı́vel e de fácil com-
preensão. A escolha dos nomes dos identificadores influenciam diretamente esses dois aspectos.
Logo, é muito importante que os nomes sejam significativos, deixando bem clara a sua referência.
A fim de contrastar com a declaração de variáveis do Exemplo 2.1, o Exemplo 2.3 ilustra
a convenção proposta, apresentando nomes significativos para variáveis. Neste, o rótulo das
variáveis já é suficiente para informar a funcionalidade das mesmas, tornando rápida a compre-
ensão do programa que as utiliza e dispensando a necessidade de comentários.
1 delta
2 raiz1
3 idade
4 letra
5 perc entualDeLucro
6 primeiraLetra
7 indiceBovespa
1 main () {
2 leituraAtual = 125;
3 leituraAnterior = 25;
4 valorUnitario = 2;
5 diferenca = leituraAtual - leituraAnterior ;
6 valorConta = diferenca * valorUnitario ;
7 }
No Exemplo 2.4, o sı́mbolo “=” representa o comando de atribuição. Em seu lado esquerdo
sempre haverá a variável destino, na qual será escrito o resultado da expressão do lado direito
do comando. As expressões mais simples são as que contêm valores constantes, tais como as que
ocorrem nas linhas 1, 2 e 3 do Exemplo 2.4.
Essas três linhas são omitidas propositalmente do Pseudocódigo 2.2 para torná-lo mais sim-
ples. Entretanto, elas não são difı́ceis de compreender, pois apenas informam ao computador
quais valores ele deve manipular para encontrar o resultado esperado: o valor da conta mensal
de energia elétrica.
Dessa forma, o comando da linha 1 do Exemplo 2.4 simplesmente realiza a atribuição do valor
125 à variável leituraAtual. A partir da efetivação desse comando, o conteúdo dessa variável
pode ser usado para os cálculos. O mesmo vale para as demais variáveis.
As linhas 4 e 5 do Exemplo 2.4 são transcrições diretas dos dois passos do algoritmo da
conta de energia. O que ocorre de diferente é que o valor a ser atribuı́do às variáveis do lado
esquerdo é primeiramente calculado na expressão para, em seguida, ser guardado na variável
correspondente. A Figura 2.4 representa graficamente essa passagem.
O comando de atribuição é executado sempre da mesma maneira pelo computador. Em
primeiro lugar a expressão que está do lado direito do comando é calculada e, para isso, todas as
variáveis que aparecem têm seus valores lidos e lançados na expressão. Após o fim dos cálculos na
expressão, o computador escreve o resultado na variável destino, no lado esquerdo. Basicamente,
a forma geral do comando de atribuição é a seguinte:
<variável> = <express~
ao>;
24 CAPÍTULO 2. CONCEITOS BÁSICOS
1 contador = 10;
2 contador = contador + 1;
No Exemplo 2.5 ocorrem duas atribuições. A primeira ocorre na linha 1, sendo mais um
exemplo simples de inicialização de uma variável, como já discutido anteriormente. Na linha 2,
a variável contador aparece em ambos os lados da expressão, o que pode parecer um absurdo
matemático. Porém, é necessário lembrar que, nesse contexto, o sı́mbolo “=” atribui o valor da
expressão da direita à variável que aparece à esquerda. Ou seja, avalia-se primeiro o resultado
da expressão à direita (10 + 1 = 11) e, posteriormente, atribui-se o valor à variável à esquerda
(contador = 11). Isso é representado graficamente na Figura 2.5.
Mais uma observação é necessária: O programador deve ficar atento aos tipos de dados
manipulados em um comando de atribuição, pois o tipo da expressão à direita deve ser compatı́vel
com o tipo da variável à esquerda. A manipulação dos tipos de dados é detalhada na Seção 2.4.
2.4. TIPOS DE DADOS 25
<tipo> <identificador>;
26 CAPÍTULO 2. CONCEITOS BÁSICOS
É possı́vel declarar mais de uma variável em uma mesma linha, desde que todas sejam do mesmo
tipo, conforme o Exemplo 2.6.
1 main () {
2 int x , y , a , b , c , d , e , f ;
3
4 x = 10;
5 y = 3;
6
7 a = x + y; // a armazena 13
8 b = x - y; // b armazena 7
9 c = x * y; // c armazena 30
10 d = x / y; // d armazena 3
11 e = x % y; // e armazena 1
2.4. TIPOS DE DADOS 27
12 f = -a ; // f armazena -13
13 // c = xy ;
14 }
1 main () {
2 float p , q , x , z ;
3 int y ;
4
5 x = 10;
6 y = 4;
7
8 p = 50 / 30; // p guardará 1
9 q = 10.0 / 4; // q guardará 2.5
10
11 z = x / y; // z guardará 2.5
12 }
As operações entre variáveis do tipo float são escritas com os mesmos sı́mbolos das operações
do tipo int. O Exemplo 2.8 mostra particularidades do processo de compilação que podem gerar
erros nos programas.
28 CAPÍTULO 2. CONCEITOS BÁSICOS
A linha 8 do Exemplo 2.8 possui um comentário explicando que a variável p recebe 1 como
resultado da operação de divisão. Tal erro não ocorre para a divisão da linha seguinte. A
justificativa para essa situação é que, conforme é mostrado na Seção 2.3, o computador primeiro
calcula o resultado da expressão do lado direito do comando de atribuição para, em seguida,
escrever esse valor na correspondente variável de destino.
Durante a compilação, o compilador checa os tipos das variáveis envolvidas nos cálculos
para realizar as operações correspondentes. Entretanto, quando há apenas valores numéricos
envolvidos, o compilador considera os números escritos sem ponto decimal como sendo do tipo
inteiro. É por isso que a divisão realizada na linha 8 do Exemplo 2.8 resulta em 1. O computador
entende a operação 50/30 como uma divisão inteira, descartando a parte decimal do resultado.
Já na linha 9, o compilador enxerga o valor 10.0 como sendo do tipo float devido à presença
explı́cita do ponto decimal, e realiza a divisão, resultando no valor esperado.
No caso da última linha do Exemplo 2.8, nenhum resultado inesperado ocorre porque o
tipo mais abrangente — tipo float da variável x — encontrado na expressão é considerado nas
operações, garantindo o resultado esperado. Caso a variável x também fosse do tipo int, então
o mesmo incoveniente da linha 8 ocorreria.
Com o passar do tempo, a tabela ASCII sofreu modificações. Ela foi atualizada e incorporou
mais 128 novos sı́mbolos, os quais incluem, por exemplo, os caracteres acentuados e o cedilha,
do Português. Essa nova versão é conhecida como tabela ASCII estendida.
Para fazer com que uma variável do tipo char aponte para um sı́mbolo de interesse, o
programador precisa apenas atribuir a essa variável o caractere desejado, colocando-o entre
aspas simples. O Exemplo 2.9 exibe essa situação.
1 main () {
2 char letraA , letraFMaiuscula , simboloSoma ;
3
4 letraA = ‘a ’;
30 CAPÍTULO 2. CONCEITOS BÁSICOS
5 letraFMaiuscula = ‘F ’;
6 simboloSoma = ‘+ ’;
7 }
As diferenças nos tamanhos são justificáveis, uma vez que, quanto maior o conjunto a ser
representado, mais células de memória são necessárias para acomodar uma variável. Desta
forma, sendo o conjunto dos caracteres o menor, é preciso apenas um byte (uma célula) para
representá-lo. Já o conjunto do tipo float, que é o maior dos três, precisa de 4 bytes (4 células)
para uma representação coerente de seus valores.
É preciso ter em mente que, devido a essas restrições de implementação, não se deve utili-
zar operações de atribuição entre variáveis sem tomar algum cuidado. Se elas forem de tipos
diferentes, problemas podem acontecer.
Não há problemas em atribuir uma variável do tipo int a outra do tipo float. Tal qual ocorre
na matemática, toda variável do tipo int é possı́vel de ser representada por outra do tipo float,
pois o conjunto dos números racionais contém o conjunto dos inteiros. Um raciocı́nio similar
pode ser empregado para justificar as atribuições de variáveis char a variáveis de tipo int ou
float, que também ocorrem sem erros.
O problema é quando se tenta atribuir uma variável de um conjunto mais amplo a uma de
um conjunto mais restrito. Veja o Exemplo 2.10.
Observando o Exemplo 2.10, um programador distraı́do pode pensar que houve uma atri-
buição correta. Entretanto, o valor armazenado pela variável teste é 125. Não havendo condições
de armazenar corretamente (um código de 4 bytes não pode ser escrito num espaço de apenas
2 bytes), o compilador simplesmente descarta a parte decimal do número, armazenando apenas
2.5. CONSTANTES 31
sua parte inteira na variável. Logo, é preciso permanecer atento às atribuições desse tipo para
não se equivocar sobre o valor armazenado por uma variável.
1 main () {
2 int teste ;
3
4 teste = 125.568;
5 }
2.5 Constantes
Em algumas situações surge a necessidade de se utilizar um determinado valor constante em
diversas partes do código. Para simplificar a alteração dessas constantes e facilitar a leitura do
código, é possı́vel definir um nome signicativo para elas de maneira simples, por meio de uma
estrutura especı́fica.
Essa estrutura é iniciada pela diretiva #define, seguida pelo identificador da constante e
pelo seu respectivo valor, todos separados por um espaço em branco.
#define <identificador> <valor>
Na linguagem C, a declaração de constantes deve ser feita no inı́cio do código, antes da função
main. O Exemplo 2.11 apresenta declarações válidas de constantes.
1 # define PI 3.141593
2 # define FALSO 0
3 # define VERDADEIRO 1
4 # define RAIZDEDOIS 1.414214
5
6 main () {
7 float raio , comprimento , area ;
8
9 raio = 10;
10 area = PI * raio * raio ;
11 comprimento = 2 * PI * raio ;
12 }
Na declaração de constantes, a escolha dos nomes segue as mesmas regras para a escrita dos
nomes de variáveis. Geralmente, para diferenciar as constantes no código, opta-se por escrever
seus nomes com todas as letras maiúsculas.
O Exemplo 2.11 mostra um exemplo simples onde diversas constantes são declaradas de
acordo com a convenção proposta. Durante a compilação do código, o compilador troca tocas
as ocorrências da constante PI por seu respectivo valor, definido na linha 1.
32 CAPÍTULO 2. CONCEITOS BÁSICOS
2.6 Expressões
As variáveis e constantes podem ser combinadas com os operadores associados a cada tipo de
dados, gerando expressões.
2. Adição e subtração.
Para se obter uma seqüência diferente de cálculos, vários nı́veis de parênteses podem ser
usados para quebrar as prioridades definidas. Não é permitido o uso de colchetes e chaves, uma
vez que estes sı́mbolos já são utilizados com outras finalidades. O Exemplo 2.12 exibe uma série
de expressões aritméticas válidas.
1 main () {
2 int x , y , z ;
3
4 x = 10 * 10 + 25; // x = 125
5 y = 5 * (7 + 3) ; // y = 50
6 z = 10 * (( x - 25) / y + ( x - 25) * 2) ; // z = 2020
7 }
Exemplo 2.12: Expressões aritméticas e o uso de parênteses para determinar a precedência das
operações.
<nome da funç~
ao>(<valor>)
Nome Descrição
abs Módulo ou valor absoluto de um valor inteiro
fabs Módulo ou valor absoluto de um valor racional
sin Seno de um ângulo em radianos
cos Cosseno de um ângulo em radianos
sqrt Raiz quadrada de um número
pow Potenciação
floor Maior inteiro não maior que o número de entrada
ceil Menor inteiro não menor que o número de entrada
log Logaritmo neperiano
exp Exponencial
Para o uso das funções matemáticas apresentadas no Exemplo 2.13 é necessário a inclusão
da biblioteca math.h, como na linha 1. Os mesmos cálculos para a procura das raı́zes de uma
equação de segundo grau são executados nas linhas 13 a 15, onde a função sqrt calcula a raiz
quadrada do seu operando — a variável delta. Já as linhas 15 a 17 apresentam cálculos avulsos:
a linha 17 executa o cálculo do seno de −π/2 e armazena o resultado na variável x; a linha
18 armazena, na variável y, o módulo do valor armazenado em x; e a linha 19 calcula o valor
armazenado em y elevado à potência de 2 e armazena o resultado na variável z.
34 CAPÍTULO 2. CONCEITOS BÁSICOS
As expressões relacionais resultam sempre em 0 (zero) para uma comparação falsa ou 1 para
uma comparação verdadeira.
As variáveis do tipo char também podem ser comparadas entre si, respeitando a ordenação
do padrão de codificação utilizado.
É importante atentar que, embora sejam escritos de maneira parecida, há uma grande dife-
rença entre o operador de comparação “==” e o comando de atribuição “=”. Essa semelhança é
uma fonte comum de erros de programação, geralmente difı́ceis de detectar.
Os resultados obtidos das expressões lógicas também são valores do tipo inteiro: 0 para falso
e 1 para verdadeiro.
Para melhor entender o que cada operador realiza, são levados em conta dois valores P e Q
de entrada, que podem ser verdadeiros (V) ou falsos (F). A Tabela 2.4 resume todos os casos
possı́veis.
2.7. COMANDO DE ENTRADA DE DADOS 35
P Q P && Q P || Q !P !Q
V V V V F F
V F F V F V
F V F V V F
F F F F V V
O uso dos operadores relacionais e lógicos fica bem mais claro adiante, neste capı́tulo, quando
os comandos de seleção e repetição são estudados.
Essa estrutura é dividida em duas partes distintas. A primeira, colocada entre aspas duplas,
contém os formatos, os quais são relacionados diretamente com os tipos das variáveis a serem
lidas. A segunda parte é uma lista dos nomes dessas variáveis, com uma relação direta entre
a posição nessa lista e o respectivo formato descrito na primeira parte do comando. Conforme
mostra a descrição da sintaxe, os nomes das variáveis devem ser precedidos pelo caractere “&”.
A Tabela 2.5 mostra alguns dos possı́veis formatos.
Código Significado
%c Lê um único caracter
%d Lê um inteiro decimal
%f Lê um número em ponto flutuante
diferentes contas, bastando, para isso, executar novamente o programa. O código modificado é
exibido no Exemplo 2.14.
De acordo com o formato descrito no comando de leitura, os valores das variáveis leituraA-
tual, leituraAnterior e valorUnitario devem ser digitados pelo usuário, nesta ordem, separados
por espaços em branco.
Exemplo 2.14: Emprego do comando de entrada de dados para o algoritmo do Exemplo 2.4.
Observando o código do Exemplo 2.14, é importante ressaltar que não há mais a parte de
inicialização das variáveis. Essa tarefa foi substituı́da pelo comando de entrada de dados e os
valores são, agora, passados pelo usuário do programa. Esses valores podem ser alterados a cada
execução do programa para que várias contas diferentes possam ser calculadas.
Código Significado
%c Caracter
%d Inteiros decimais com sinal
%e Notação cientı́fica
%f Ponto flutuante decimal
%% Escreve o sı́mbolo “%”
Tabela 2.6: Códigos para formatos de dados para o comando de saı́da printf .
Pode-se, ainda, mesclar uma mensagem de texto com valores de variáveis. O Exemplo 2.16
ilustra essa idéia. Nele, é possı́vel notar que, para cada especificador de formato na primeira
parte do comando, deve existir uma variável de tipo correspondente posicionada adequadamente
na segunda parte.
Uma versão mais completa do código para o cálculo da conta de energia elétrica é exibida no
Exemplo 2.17. Nele, o comando de saı́da é usado antes de cada comando de entrada de dados
(linhas 5, 8 e 11), escrevendo mensagens para indicar ao usuário do programa qual valor ele
deverá informar. A última linha de código utiliza o comando de saı́da para exibir o valor do
cálculo da conta. Trata-se de uma mensagem mista, em que o especificador %f será substituı́do
pelo valor da variável valorConta. Supondo que, após os cálculos, essa variável armazene o valor
187, a última mensagem exibida na tela seria:
6
7 printf ( " Digite o valor da leitura ATUAL : " ) ;
8 scanf ( " % d " , & leituraAtual ) ;
9
10 printf ( " Digite o valor da leitura ANTERIOR : " ) ;
11 scanf ( " % d " , & leituraAnterior ) ;
12
13 printf ( " Digite o preco do Quilowatt - hora : " ) ;
14 scanf ( " % f " , & valorUnitario ) ;
15
16 diferenca = leituraAtual - leituraAnterior ;
17 valorConta = diferenca * valorUnitario ;
18
19 printf ( " Valor da conta R$ : % f " , valorConta ) ;
20 }
Exemplo 2.17: Algoritmo mais interativo para o exemplo de cálculo da conta de energia elétrica.
Quando se usa o comando printf , o caractere especial “\n” é bastante útil. Ele serve para
quebrar a linha exatamente na posição onde for inserido no texto. A Tabela 2.7 explica melhor
o funcionameto desse caractere.
Código Resultado
printf("OI.\nComo vai?"); OI.
Como vai?
int x; O valor de x eh:
x = 10; 10
printf("O valor de x eh:\n%d", x);
int a; Primeira linha.
a = 20; Segunda linha (a = 20).
printf("Primeira linha.\nSegunda linha (a = %d). \n\nFIM.", a);
FIM.
O comando de seleção permite que um programa possa realizar diferentes alternativas de seqüências
de instruções durante sua execução. Dependendo do valor de uma expressão ou de uma variável,
o programa segue executando uma ou outra seqüência de comandos.
Existem três categorias distintas: seleção simples, seleção dupla e seleção múltipla, as quais
são abordadas a seguir.
2.9. COMANDOS DE SELEÇÃO 39
O Exemplo 2.18 inicia exibindo uma mensagem ao usuário (linha 5), solicitando que sejam
digitados dois números. Esses valores são armazenados em variáveis correspondentes (linha 6)
e, em seguida, são mostrados ao usuário por meio de uma mensagem (linha 7). A expressão
lógica do comando de seleção da linha 9 determina se há necessidade de inverter os números
antes de exibi-los ordenadamente. É importante ressaltar que não há necessidade de ordenar os
números caso o usuário já os tenha digitado na seqüência correta. Nesse caso, o programa pode
seguir normalmente e exibir na tela os valores das variáveis. Por último, os valores são exibidos
em ordem crescente (linha 14).
Ainda no Exemplo 2.18, as linhas 10, 11 e 12 são importantes o bastante para serem destaca-
das. Elas tratam da operação de troca de valores entre duas variáveis, a qual é muito utilizada
e deve ser bem entendida.
O comando de atribuição é ideal para copiar o valor de uma variável para outra. A questão é
que a variável de destino tem seu valor alterado permanentemente após a realização do comando.
Ou seja, o valor que estava escrito nessa variável antes da execução da atribuição é apagado e o
novo valor é escrito em seu lugar.
No caso da operação de troca de valores, se fossem usadas apenas as duas variáveis envolvidas,
uma delas teria seu valor perdido quando a primeira atribuição fosse realizada. Esse problema
ocorre no Exemplo 2.19, no qual o valor inicial da variável var1 é perdido.
1 main () {
2 int var1 , var2 ;
3
4 var1 = 100;
5 var2 = 200;
6
7 var1 = var2 ; // var1 guarda 200
8 var2 = var1 ; // var2 guarda 200
9 }
2.9. COMANDOS DE SELEÇÃO 41
A solução para o problema da perda de valores é simples e pode ser encontrada nas linhas
10, 11 e 12 do Exemplo 2.18. Basta usar uma variável auxiliar e copiar para ela o valor da
primeira variável de destino usada nas atribuições. Naquele exemplo, o valor da variável num1
foi salvo na variável aux para não ser perdido na atribuição da linha 11. Em seguida, esse valor
pôde ser copiado corretamente para a variável num2 na linha 12.
if (<express~ao lógica>){
<Seqü^
encia de comandos 1>
}else{
<Seqü^
encia de comandos 2>
}
Pseudocódigo 2.4 Informa se um aluno está aprovado ou não, de acordo com a média das
notas obtidas no semestre.
Descrição: Algoritmo para decidir se um aluno está aprovado ou não, com base na média das notas
semestrais.
Dados de Entrada: matrı́cula do aluno, notas.
Saı́da do Programa: mensagem dizendo se o aluno está aprovado ou não.
Funç~
ao Principal:
Leia a matrı́cula e as notas do aluno.
Calcule a média parcial.
SE a média parcial for menor que 7 ENT~
AO
Imprima uma mensagem dizendo que o aluno deve fazer prova final.
SEN~
AO
Imprima uma mensagem dizendo que o aluno está aprovado.
FIM-SE.
FIM-Funç~
ao Principal
9
10 if ( mediaParcial < 7.0) {
11 printf ( " \ nO aluno % d deve fazer prova final . " , matricula ) ;
12 } else {
13 printf ( " \ nO aluno % d foi aprovado com media % f " , matricula , media ) ;
14 }
15 }
Exemplo 2.20: Uso do comando de seleção if para a verificação da média parcial de um aluno.
Pseudocódigo 2.5 Compara dois números e informa se foram digitados em ordem crescente,
decrescente ou se são iguais.
Descrição: Algoritmo para decidir se dois números digitados estão em ordem crescente, decrescente ou
se são iguais.
Dados de Entrada: matrı́cula do aluno, notas.
Saı́da do Programa: mensagem dizendo se o aluno está aprovado ou não.
Funç~
ao Principal:
Leia dois números inteiros.
SE o primeiros for maior que o segundo ENT~ AO
Imprima uma mensagem dizendo que é ordem crescente.
SEN~
AO
SE o segundo for maior que o primeiro ENT~ AO
Imprima uma mensagem dizendo que é ordem decrescente.
SEN~
AO
Imprima uma mensagem dizendo que os números s~
ao iguais.
FIM-SE.
FIM-SE.
FIM-Funç~
ao Principal
10
11 } else {
12 if ( num2 > num1 ) {
13 printf ( " \ nOrdem crescente . " ) ;
14
15 } else {
16 printf ( " \ nNumeros iguais . " ) ;
17 }
18 }
19 }
A estrutura dos comandos de seleção do Exemplo 2.21 permite testar todos os casos. Observe
que não há uma verificação explı́cita para a igualdade dos números. Isso acontece porque todos
os demais casos possı́veis são testados antes e, se não se tratar de nenhum desses, então os
números certamente são iguais. Nesse caso, basta apenas exibir a mensagem correspondente.
Em muitos casos, além de ser elegante, o uso da estrutura de seleção aninhada pode ser
mais eficiente. Verifica-se esse fato nos Exemplos 2.22 e 2.23. Ambos os programas lêem três
números digitados pelo usuário e identificam o maior deles, porém, um dos programas faz menos
verificações que o outro, ganhando eficiência.
3 int n1 , n2 , n3 ;
4
5 printf ( " Entre com os tres numeros : " ) ;
6 scanf ( " % d % d % d " , & n1 , & n2 , & n3 ) ;
7
8 if (( n1 > n2 ) && ( n1 > n3 ) ) {
9 printf ( " O maior numero digitado foi : % d . " , n1 ) ;
10 }
11
12 if (( n2 > n1 ) && ( n2 > n3 ) ) {
13 printf ( " O maior numero digitado foi : % d . " , n2 ) ;
14 }
15
16 if (( n3 > n1 ) && ( n3 > n2 ) ) {
17 printf ( " O maior numero digitado foi : % d . " , n3 ) ;
18 }
19 }
Exemplo 2.22: Programa que identifica o maior de três números sem o uso do comando de
seleção aninhado.
Exemplo 2.23: Programa que identifica o maior de três números, usando o uso do comando de
seleção aninhado.
Pseudocódigo 2.6 Informa se três números podem constituir os lados de um triângulo e o tipo
do triângulo que pode ser formado.
Descrição: Algoritmo para calcular se três números podem representar os lados de um triângulo e,
caso seja possı́vel, o tipo do triângulo.
Dados de Entrada: três valores inteiros.
Saı́da do Programa: mensagem dizendo o tipo do triângulo ou se não pode ser formado um triângulo.
Funç~
ao Principal:
Leia tr^
es números inteiros.
SE os tr^es valores forem iguais ENT~
AO
Imprima uma mensagem dizendo que é tri^
angulo equilátero.
~
SENAO
SE cada valor é menor do que a soma dos outros dois ENT~AO
es valores forem diferentes ENT~
SE os tr^ AO
Imprima uma mensagem dizendo que é tri^
angulo escaleno.
SEN~
AO
Imprima uma mensagem dizendo que é tri^
angulo isósceles.
FIM-SE.
SEN~
AO
Imprima uma mensagem dizendo que os valores n~ao podem ser lados de um tri^
angul
FIM-SE.
FIM-SE.
FIM-Funç~
ao Principal
No Exemplo 2.24, o comando if mais externo (linha 13) confere se os números formam um
triângulo equilátero, verificando se os três são iguais. Caso não formem um triângulo equilátero,
na linha 17 é verificado se os três números formam realmente um triângulo, comparando a soma
de dois lados com a medida do terceiro (para todas as possibilidades). Caso verdadeiro, o if mais
interno confere se os três lados são diferentes um do outro, formando um triângulo escaleno; o
resultado “falso”para esse último teste implica necessariamente na formação de um triângulo
isósceles.
switch (<express~ao>){
case <valor1>:
<seqü^
encia de comandos 1>
break;
case <valor2>:
<seqü^
encia de comandos 2>
break;
.
2.9. COMANDOS DE SELEÇÃO 47
.
.
case <valorN>:
<seqü^
encia de comandos N>
break;
default :
<seqü^
encia de comandos>
}
3 int numero ;
4
5 printf ( " URNA ELETR O ^ NICA - SEU VOTO PARA PREFEITO : " ) ;
6 scanf ( " % d " , & numero ) ;
7
8 switch ( numero ) {
9 case 1:
10 printf ( " Candidato escolhido : Hort^ e ncia da Silva " ) ;
11 break ;
12 case 2:
13 printf ( " Candidato escolhido : José dos Cravos " ) ;
14 break ;
15 case 3:
16 printf ( " Candidato escolhido : Margarida S . Pereira " ) ;
17 break ;
18 default :
19 printf ( " Número digitado inválido . Voto anulado . " ) ;
20 }
21 }
Exemplo 2.25: Uso do comando switch para simular uma urna eletrônica de eleição.
As aplicações para os comandos de repetição são praticamente infinitas porque quase todas
as tarefas contêm partes que devem ser executadas mais de uma vez.
Na linguagem C, existem três principais estruturas de repetição. A opção pelo uso de uma
ou outra depende normalmente da preferência do programador e do problema em questão,
normalmente buscando-se o máximo de clareza ou facilidade de escrita do código. Essas três
estruturas são abordadas a seguir.
Exemplo 2.26: Comando de repetição while para imprimir os n primeiros números inteiros.
O próximo programa, Exemplo 2.27, imprime os n primeiros números ı́mpares. Essa quan-
tidade n é definida pelo usuário.
2.10. COMANDOS DE REPETIÇÃO 51
Exemplo 2.27: Programa que imprime os n primeiros números ı́mpares, utilizando o comando
de repetição while.
No código do Exemplo 2.28, o usuário tem a possibilidade de corrigir o seu voto sem que o
programa termine.
Exemplo 2.28: Comando de repetição while utilizado para melhorar o código do Exemplo 2.25.
do{
2.10. COMANDOS DE REPETIÇÃO 53
<Seqü^
encia de comandos>
} while(<express~ao lógica>);
O Pseudocódigo 2.9 pode ser modificado para melhorar sua legibilidade. Com o emprego do
comando FAÇA-ENQUANTO, o algoritmo fica mais simples.
Pseudocódigo 2.10 Urna eletrônica com opção de corrigir voto (versão melhorada).
Descrição: programa para imprimir na tela os n primeiros números positivos.
Dados de Entrada: número do candidato, resposta da confirmação.
Saı́da do Programa: mensagem indicando o candidato escolhido.
Funç~
ao Principal:
FAÇA:
Leia o número do candidato.
Imprima o nome do candidato escolhido.
Leia a resposta da confirmaç~
ao.
ENQUANTO resposta for igual a zero.
FIM-Funç~ao Principal
O programa do Exemplo 2.28 pode ser reescrito utilizando-se o comando do-while. Nesse
caso, o uso do do-while permite ao programador ter a opção de não inicializar a variável resposta
antes da execução da seqüência de comandos, responsabilizando o usuário pela inicialização
(conforme se nota no Exemplo 2.29).
Exemplo 2.29: Aplicação para o comando do-while, modificando o código do Exemplo 2.28.
No Exemplo 2.29, nota-se que não há mais a inicialização explı́cita da variável resposta.
Com o comando do-while, garante-se que a variável será inicializada pelo usuário antes de seu
valor ser testado na expressão lógica da linha 25, utilizada como critério de parada.
Pode parecer estranho, à primeira vista, como a expressão lógica !resposta funciona. Na
verdade essa espressão é equivalente ao teste resposta == 0.
Para entender essa equivalência, basta lembrar que o operador lógico de negação inverte o
valor lógico da variável ao qual ele é aplicado. É bom recordar também que, para os números
inteiros, o valor zero equivale a falso enquanto que qualquer número diferente de zero é tomado
como valor lógico verdadeiro. Assim sendo, sempre que o programador desejar testar se um
valor inteiro é igual a zero, basta testar o resultado do operador de negação, pois o único caso
em que ele retornará verdadeiro será quando o valor numérico testado for zero.
Um outro exemplo simples para o uso do comando do-while é um algoritmo no qual o usuário
informa uma seqüência de valores positivos e, em seguida, a média desses valores é apresentada.
O usuário deverá digitar zero quando quiser finalizar a seqüência e saber a média. O algoritmo
desse programa está listado no Pseudocódigo 2.11.
Exemplo 2.30: Programa que calcula a média dos valores de uma seqüência digitada pelo usuário.
for (<inicializaç~
ao>; <express~
ao lógica>; <incremento>){
<Sequencia de comandos>
}
1 main () {
2 float primeiroTermo , razao , n , i , termo ;
3
4 printf ( " Informe o valor do primeiro termo da PG : " ) ;
5 scanf ( " % f " ,& primeiroTermo ) ;
6 printf ( " Informe o valor da razao da PG : " ) ;
7 scanf ( " % f " ,& razao ) ;
8 printf ( " Informe quantos termos devem ser impressos : " ) ;
9 scanf ( " % f " ,& n ) ;
10 termo = primeiroTermo ;
11 for ( i =0; i < n ; i = i +1) {
12 printf ( " % f " , termo ) ;
13 termo = termo * razao ;
14 }
15 }
Exemplo 2.31: Programa para exibir os n primeiros termos de uma progressão geométrica utilizando o
comando de repetição for.
Exemplo 2.32: Programa para realizar a operação de exponenciação utilizando o comando de repetição
for.
No Exemplo 2.32, os valores da base e do expoente são fornecidos pelo usuário via comando
de entrada de dados (linha 6). A variável de controle i é inicializada com o valor zero dentro
do comando for e, a cada iteração, é incrementada de 1. Quando o valor de i atinge valor igual
ao valor da variável expoente, as iterações são terminadas e o programa segue para a linha 13,
58 CAPÍTULO 2. CONCEITOS BÁSICOS
Considera-se, como exemplo, o problema de um professor que ministra uma disciplina para
diversas turmas. Nesse caso, é interessante saber qual turma obteve melhor rendimento, o aluno
2.11. PROBLEMA DOS LOTES ENCAIXANTES 59
que mais se destacou em cada turma e os que menos renderam nos estudos. Os dados, nesse
caso, podem ser observados em quatro nı́veis.
O nı́vel de notas é o mais simples. Ele é indivisı́vel e é caracterizado pelas notas dos trabalhos
e provas realizados no semestre. O nı́vel seguinte é o das médias semestrais em que as notas são
agrupadas por aluno. O terceiro agrupamento é o das turmas. Os alunos são reunidos de acordo
com as turmas a que pertencem. Por último, o nı́vel mais complexo, é a própria disciplina. Ela é
a entidade mais abrangente, que é composta diretamente pelas turmas existentes no determinado
semestre.
Problemas de lotes encaixantes, normalmente, são resolvidos por meio do emprego de coman-
dos de repetição aninhados. Quanto mais externo for o comando de repetição, maior o nı́vel de
complexidade das entidades que estão sendo examinadas. A Figura 2.8 exibe essa idéia, usando
como exemplo o problema das notas escolares.
Figura 2.8: Comandos de repetição aninhados, utilizados para resolver um problema de lotes
encaixantes.
Pseudocódigo 2.12 Problema dos Lotes Encaixantes para o problema das notas escolares
Descrição: programa para analisar as notas de uma disciplina escolar.
Dados de Entrada: código da turma, matrı́culas dos alunos, notas dos alunos.
Saı́da do Programa: média de cada aluno, matrı́culas dos alunos de melhor e de pior médias de cada
turma, código da turma com melhor rendimento.
Funç~
ao Principal:
Leia o código de uma turma.
ENQUANTO código for diferente de -1 FAÇA:
Leia o número de matrı́cula de um aluno.
ENQUANTO o número de matrı́cula for diferente de -1 FAÇA:
Leia uma nota do aluno.
ENQUANTO a nota digitada n~ ao for negativa FAÇA:
Leia outra nota do aluno.
Atualize os dados do aluno.
FIM-ENQUANTO
Atualize os dados da turma.
Imprima a média das notas do aluno.
Leia o número de matrı́cula de um aluno.
FIM-ENQUANTO
Atualize os dados da disciplina.
Imprima os números de matrı́cula e as médias dos alunos com maior
e menor médias da turma.
Leia o código de uma turma.
FIM-ENQUANTO
Impria o código da turma com melhor aproveitamento.
FIM-Funç~
ao Principal
15 numAprovadosTurma = 0;
16 numAlunosTurma = 0;
17
18 printf ( " Informe o numero de matricula do aluno : " ) ;
19 scanf ( " % d " , & matricula ) ;
20 while ( matricula != -1) {
21 numNotasAluno = 0;
22 somaNotasAluno = 0;
23 numAlunosTurma = numAlunosTurma + 1;
24
25 printf ( " Informe a nota do aluno : " ) ;
26 scanf ( " % f " , & nota ) ;
27 while ( nota != -1) {
28 somaNotasAluno = somaNotasAluno + nota ;
29 numNotasAluno = numNotasAluno + 1;
30 printf ( " Informe a nota do aluno ou ’ -1 ’ para encerrar : " ) ;
31 scanf ( " % f " , & nota ) ;
32 }
33
34 mediaAluno = somaNotasAluno / numNotasAluno ;
35
36 if ( mediaAluno >= MEDIAMINIMA ) {
37 numAprovadosTurma = numAprovadosTurma + 1;
38 }
39
40 if ( mediaAluno >= maiorMediaTurma ) {
41 maiorMediaTurma = mediaAluno ;
42 alunoMaiorMedia = matricula ;
43 }
44
45 if ( mediaAluno <= menorMediaTurma ) {
46 menorMediaTurma = mediaAluno ;
47 alunoMenorMedia = matricula ;
48 }
49 printf ( " Media do aluno : % f \ n " , mediaAluno ) ;
50 printf ( " Informe o numero de matricula do aluno ou ’ -1 ’ para encerrar : "
);
51 scanf ( " % d " , & matricula ) ;
52 }
53
54 a p roveitamentoTurma = ( numAprovadosTurma / numAlunosTurma ) * 100;
55 if ( melhorAproveitamento < aproveitamentoTurma ) {
56 melhorAproveitamento = aproveitamentoTurma ;
57 melhorTurma = codTurma ;
58 }
59 printf ( " O aluno % d obteve a melhor media da turma (% f ) \ n " , alunoMaiorMedia
, maiorMediaTurma ) ;
60 printf ( " O aluno % d obteve a pior media da turma (% f ) \ n " , alunoMenorMedia ,
menorMediaTurma ) ;
61 printf ( " Informe o codigo da turma ou ’ -1 ’ para encerrar : " ) ;
62 scanf ( " % d " , & codTurma ) ;
62 CAPÍTULO 2. CONCEITOS BÁSICOS
63 }
64 printf ( " A turma % d obteve o melhor aproveitamento (% f %%) " , melhorTurma ,
m e l horAproveitamento ) ;
65 }
Exemplo 2.33: Problema dos lotes encaixantes aplicado a notas de uma disciplina
Fazendo uma comparação com a Figura 2.8 e com o Pseudocódigo 2.12, não fica complicado
entender o funcionamento do código do Exemplo 2.33. Após a declaração das variáveis, o
programa inicializa a variável melhorAproveitamento (linha 8), que armazena o percentual de
aprovações da turma de melhor aproveitamento. O valor ajustado é o menor valor possı́vel (zero)
e sua escolha será justificada adiante. Em seguida, o programa solicita ao usuário que digite um
código para identificar a primeira turma que será analisada (linhas 10 e 11).
O primeiro comando de repetição, na linha 12, serve para percorrer todas as turmas, uma
a uma. Sempre que sua expressão lógica resultar verdadeiro, é sinal que uma nova turma terá
seus dados digitados.
Aqui vale destacar um artifı́cio útil que é empregado nesse código. Todos os comandos de
repetição fazem a verificação de suas variáveis de controle comparando-as com o mesmo valor
−1. Ocorre que, para evitar que o usuário seja obrigado a saber, a priori, da quantidade de
elementos de um conjunto de dados que será passado ao programa, utiliza-se um valor absurdo
(qualquer valor que não pertença ao conjunto em questão) para servir de sinalização para o
programa de que o cunjunto de dados foi inteiramente percorrido. Esse valor é, comumente,
chamado de flag.
No Exemplo 2.33, considera-se que os códigos das turmas e das matrı́culas são inteiros
positivos e que as notas são valores racionais não negativos. Assim sendo, o valor −1 foi utilizado
para indicar ao programa quando todas as notas de um aluno foram já digitadas. Ou que todos
os dados dos alunos de uma determinada turma foram digitados. Ou ainda, que todas as turmas
tiveram seus dados informados.
A seqüência de comandos começa, então, com a inicialização das variáveis da turma em
questão (linhas 13 a 16). As variáveis maiorMediaTurma e menorMediaTurma guardam, res-
pectivamente, a melhor e a pior média obtidas na turma. Já numAprovadosTurma e nu-
mAlunosTurma armazenam, respectivamente, a quantidade de alunos aprovados na turma e a
quantidade total de alunos da turma. Os valores dessas duas variáveis servem para calcular o
aproveitamento da turma.
Nas linhas 18 e 19, o programa solicita ao usuário que informe a matrı́cula do primeiro aluno
da turma que terá lidas suas notas. Na linha seguinte aparece o segundo comando de repetição,
o qual interage com os dados dos alunos de uma turma.
Assim como no comando de repetição mais externo, o comando da linha 21 inicia sua
seqüência de comandos realizando a inicialização das variáveis pertinentes. As variáveis num-
NotasAluno e somaNotasAluno servem para calcular a média aritmética simples das notas do
aluno em questão armazenando, respectivamente, a quantidade de notas do aluno e a soma
delas. Na linha 23, a variável numAlunosTurma é incrementada, indicando a adição dos dados
de mais um aluno.
2.11. PROBLEMA DOS LOTES ENCAIXANTES 63
As linhas 25 e 26 requerem ao usuário que ele digite o valor da primeira nota do aluno em
questão. Em seguida, o programa executa o comando de repetição mais interno, que serve para
registrar todas as notas de um determinado aluno.
Esse comando coleta os dados das notas, solicitando ao usuário que digite o valor da nota
(linhas 30 e 31) e atualizando os valores das variáveis que calculam a média do aluno no semestre.
Encerrada a digitação das notas, o usuário deve digitar −1 para encerrar e, quando o faz, o
programa passa para a rotina de cálculo da média (linha 34).
Com o valor da média, algumas verificações são feitas para atualizar as estatı́sticas da turma
em questão. Em primeiro lugar, é verificado se a média alcançada pelo aluno é suficiente para
aprovação. Em caso afirmativo, a variável numAprovadosTurma é incrementada.
É verificado, em seguida (linha 40), se a média do aluno é maior que a maior média encontrada
até então. Em caso verdadeiro, a variável maiorMediaTurma é atualizada, assim como a variável
alunoMaiorMediaTurma, que guardará a matrı́cula desse aluno.
O comando de seleção da linha 45 é equivalente ao da linha 40. Desta vez, a intenção é
atualizar os dados do aluno com pior rendimento.
Vale um comentário sobre os valores de inicialização das variáveis maiorMediaTurma e me-
norMediaTurma. É preciso ter em mente que, para a primeira verificação de melhor e pior
aluno de cada turma, os respectivos comandos de seleção devem comparar o valor da média
calculada com os valores já armazenados. O problema é que, para o primeiro aluno de cada
turma, essas variáveis ainda não contém valores válidos. Sendo assim, para evitar erros, a
variável maiorMediaTurma é inicializada com o menor valor possı́vel para a média e a variável
menorMediaTurma, ao contrário, é inicializada com o maior valor possı́vel. Essa inicilização
garante que os dados do primeiro aluno serão corretamente assimilados como o melhor e o pior
resultado encontrado. Essa condição é correta no inı́cio, pois os dados do primeiro aluno são os
únicos passados ao sistema até então.
O comando da linha 49 imprime os dados do aluno, informando sua matrı́cula e a média
alcançada. Em seguida o usuário deve informar o código da matrı́cula do próximo aluno ou
encerrar o cadastro dos dados da turma em análise, digitando −1.
Caso tenha encerrado as digitações de uma turma, o programa executa o cálculo do respectivo
aproveitamento (linha 54).
Feito isso, um comando de seleção verifica se o aproveitamento calculado é melhor que o
melhor já encontrado. Em caso afirmativo, as variáveis melhorAproveitamento e melhorTurma
são atualizadas para apontar para a turma recém analisada (linhas 56 e 57, respectivamente).
Terminando a seqüência de instruções do segundo comando de repetição, os dados do melhor
e do pior aluno da turma são exibidos (linhas 59 e 60). Por último, é perguntado ao usuário
se ele deseja incluir os dados de uma outra turma, bastando, para isso, digitar seu código. Se
digitar a flag −1, o comando de repetição é encerrado e o programa exibirá, em sua última linha,
os dados da melhor turma (linha 64).
64 CAPÍTULO 2. CONCEITOS BÁSICOS
1 main () {
2
3 float a , b , c , delta , raizDelta , raiz1 , raiz2 ;
4
5 printf ( " Informe os valores das constantes ’a ’, ’b ’ e ’c ’ ( separados por
espaços ) : " ) ;
6 scanf ( " % f % f % f " , &a , &b , & c ) ;
7
8 delta = b * b - 4* a * c ;
9
10 if ( delta >= 0) {
11 if ( delta == 0) {
12 raiz1 = -b / (2* a ) ;
13 printf ( " As duas raizes sao iguais a : % f . " , raiz1 ) ;
14 } else {
15 raizDelta = sqrt ( delta ) ;
16 raiz1 = ( - b + raizDelta ) / (2 * a ) ;
17 raiz2 = ( - b - raizDelta ) / (2 * a ) ;
18 printf ( " As duas raizes s~ a o : % f e % f " , raiz1 , raiz2 ) ;
2.12. EXERCÍCIOS RESOLVIDOS 65
19 }
20 } else {
21 printf ( " Nao existem raizes reais desta equacao ! " ) ;
22 }
23 }
Exemplo 2.34: Programa para calcular as raı́zes reais de uma equação do segundo grau.
1 main () {
2 int n , termoAtual , penultimoTermo , antePenultimoTermo , i ;
3
4 printf ( " Informe o numero de termos a serem impressos : " ) ;
5 scanf ( " % d " , & n ) ;
6
7 if ( n >= 1) {
8 printf ( " 0 " ) ;
9 }
10 if ( n >= 2) {
11 printf ( " 1 " ) ;
12 }
13
14 antePenultimoTermo = 0;
15 penultimoTermo = 1;
16 termoAtual = 2;
17 for ( i =0; i < n -2; i = i +1) {
18 printf ( " % d " , termoAtual ) ;
19 antePenultimoTermo = penultimoTermo ;
20 penultimoTermo = termoAtual ;
21 termoAtual = penultimoTermo + antePenultimoTermo ;
22 }
23 }
Exemplo 2.35: Programa para calcular as raı́zes reais de uma equação do segundo grau.
1 main () {
2 int n1 , n2 , r ;
3
4 printf ( " \ nTABUADA de 1 a 9\ n \ n " ) ;
5 n1 =1;
6 n2 =1;
7 while ( n1 <10) {
8 while ( n2 <10) {
66 CAPÍTULO 2. CONCEITOS BÁSICOS
9 r = n1 * n2 ;
10 printf ( " % d * % d = % d \ n " ,n2 , n1 , r ) ;
11 n2 = n2 +1;
12 }
13 printf ( " \ n " ) ;
14 n2 =1;
15 n1 = n1 +1;
16 }
17 }
4. Calcular o Maximo Divisor Comum (MDC) de dois números. O MDC de dois números
pode ser obtido escolhendo o maior deles e subtraindo-lhe o valor do menor. Esta operação
é repetida até que os dois sejam iguais, cujo valor será o MDC dos números iniciais:
33 15 45 18
18 15 27 18
03 15 09 18
03 12 09 09
03 09 MDC = 09
03 06
03 03
MDC = 03
1 main () {
2 int numero1 , numero2 , auxiliar1 , auxiliar2 ;
3
4 printf ( " Digite dois numeros para calcular seu MDC : " ) ;
5 scanf ( " % d % d " , & numero1 , & numero2 ) ;
6
7 auxiliar1 = numero1 ;
8 auxiliar2 = numero2 ;
9
10 while ( auxiliar1 != auxiliar2 ) {
11 if ( auxiliar1 > auxiliar2 ) {
12 auxiliar1 = auxiliar1 - auxiliar2 ;
13 } else {
14 auxiliar2 = auxiliar2 - auxiliar1 ;
15 }
16 }
17 printf ( " O MDC vale % d \ n " , auxiliar1 ) ;
18 }
5. Faça um programa que converta um valor em base binária para base decimal.
1 main () {
2 int binario , aux1 , aux2 , aux3 , decimal ;
3
4 printf ( " Digite um valor em base binaria : " ) ;
5 scanf ( " % d " , & binario ) ;
6
7 aux1 = binario ;
8 aux2 = 1;
9 decimal = 0;
10 while ( aux1 > 0) {
11 aux3 = aux1 % 10;
12 decimal = decimal + aux3 * aux2 ;
13 aux2 = aux2 *2;
14 aux1 = aux1 /10;
15 }
16 printf ( " O valor % d em base binaria equivale a % d em base decimal \ n " ,
binario , decimal ) ;
17 }
2.13 Resumo
• O computador guarda as informações dos programas na memória. A memória pode ser
entendida como uma seqüência de células, cada uma identificada por um número conhecido
como endereço de memória. Nas linguagens de programação, as variáveis permitem o
acesso às celulas de memória sem a necessidade de manipular seus endereços diretamente.
• Na linguagem C, para a declaração de uma variável, o programador deve informar qual o
tipo de dados que ela irá manipular.
• O comando de atribuição permite atribuir uma determinada informação (ou valor) a uma
variável do programa.
• Os identificadores são nomes que identificam uma variável ou constante. Eles são formados
por caracteres alfanuméricos, sendo que o primeiro deve ser obrigatoriamente alfabético.
• Na programação, os quatro tipos de dados mais comuns são: tipo inteiro, tipo ponto
flutuante, tipo booleano e tipo caractere.
• Variáveis e constantes podem ser combinadas com operadores para formarem expressões.
No comando de atribuição, o valor da expressão (termos à direita do comando) é atribuı́do
à variável (à esquerda do comando).
• Os principais tipos de expressões são aritmética, relacional e lógica.
68 CAPÍTULO 2. CONCEITOS BÁSICOS
6. Fazer um programa que calcule e escreva uma tabela de graus centı́grados em função de
graus farenheit que variam de 50 a 150 de 1 em 1.
7. Faça um programa para ler uma seqüência de n números inteiros positivos (um por vez),
e verificar se eles são pares ou ı́mpares.
2.14. EXERCÍCIOS PROPOSTOS 69
8. Suponha que a população de um paı́s A seja de 9.000 habitantes com uma taxa anual de
crescimento de 3% e que a população de um paı́s B seja, aproximadamente, de 20.000 de
habitantes com uma taxa anual de crescimento de 1,5%, fazer um programa que calcule
e escreva o número de anos necessários para que a população de paı́s A ultrapasse ou se
iguale à população do paı́s B, mantidas estas taxas de crescimento.
9. Uma certa firma fez uma pesquisa de mercado para saber se as pessoas gostaram ou não de
um novo produto lançado no mercado. Para isso, obteve, para cada pessoa entrevistada,
informações a respeito do sexo do entrevistado e sua resposta (S = Sim ou N = Não).
Sabe-se que foram entrevistados 2000 pessoas, fazer um programa que calcule e escreva:
10. A fábrica de chocolates ”MENINO” está com problemas financeiros e pretende fazer um
corte na folha de pagamento. Para isso, o setor de finanças adotou o seguinte critério para
reduzir a despesa com pessoal:
• funcionários com tempo de serviço menor que 2 anos (24 meses) serão demitidos;
• funcionários com tempo de serviço superior (ou igual) a 2 anos (24 meses) e menor
que 10 anos (120 meses) terão seus salários reduzidos em 10%;
• funcionários com tempo de serviço superior (ou igual) a 10 anos (120 meses) não serão
demitidos e nem terão seus salários reduzidos. Eles poderão optar por um plano de
demissão voluntária com a seguinte proposta de indenização: 2 salários atuais para
cada ano de serviço.
Modularização
Co-autor:
Gilberto Segundo
Objetivos:
Os programas são escritos a fim de resolverem vários tipos de problemas. Alguns desses
problemas exigem mais tempo e maiores esforços do programador. Por isso, é indispensável que
o programador utilize de técnicas que visam uma maior organização e consequente entendimento
facilitado do programa. Uma das técnicas mais utilizadas é a modularização.
Este capı́tulo trata sobre o conceito e a prática de programas modularizados.
3.1 Introdução
Quando se trabalha em qualquer problema complexo, seja em programação, seja em outra área,
o ser humano sente a necessidade de dividir esse problema maior em problemas menores. Cada
problema menor é então trabalhado de forma a solucionar as questões que lhe cabem e, juntando-
se cada uma dessas soluções, tem-se a solução do problema maior. Esse processo é chamado de
modularização e baseia-se na conhecida técnica de “dividir para conquistar”.
Pode-se aplicar a modularização em diversas áreas na vida cotidiana. Para a fabricação de
um automóvel, por exemplo, são necessários diversos serviços: funilaria, montagem, estofamento,
pintura, soldagem, etc. A princı́pio, poderia existir apenas um robô que fizesse todas as etapas
da fabricação do carro, mas isso acarretaria em alguns problemas, tais como: complexidade
70
3.2. SUBPROGRAMAS 71
do robô, que deveria fazer todos os serviços de forma completa e eficiente; alto prejuı́zo em
eventuais serviços de manutenção, que teria que parar todo o processo de montagem; falta de
clareza no entendimento do processo de fabricação, o que prejudicaria a expansão do processo
de montagem; entre outras.
Uma ótima alternativa para esse problema é fazer vários robôs, cada um com sua função
especı́fica. O que se estaria fazendo na verdade é uma modularização.
Pode-se facilmente mover as idéias contidas no problema apresentado para a elaboração de
um programa. O programa pode ser dividido em várias partes, sendo que cada parte trata de
uma determinada funcionalidade.
Dessa forma, o programa tem a sua legibilidade melhorada, uma vez que fica mais fácil
entender o que o programa faz por completo ou como o programa faz determinada subtarefa. A
modificabilidade do programa também é melhorada, uma vez que para se alterar determinada
funcionalidade, é necessário apenas alterar um pequena parte do código, não sendo necessário
modificar vários pontos do código.
Como as funcionalidades do programa estando separadas logicamente, o programador tem a
oportunidade de reutilizar uma parte do programa para escrever um outro programa que utilize,
em uma de suas tarefas, o mesmo bloco de instruções. A confiabilidade do programa também é
aumentada pelo fato dele ser mais fácil de ser entendido e corrigido.
Por fim, a produtividade para a elaboração de um programa também é aumentada, uma vez
que cada parte do programa pode ser trabalhada por uma equipe diferente, além de que cada
equipe só precisa saber o que as partes da outra equipe fazem e não como fazem.
Nas seções seguintes serão mostradas técnicas de como fazer um programa modularizado
utilizando a linguagem C.
3.2 Subprogramas
Um subprograma é um trecho de um programa que realiza qualquer operação computacional.
Ele efetua parte de uma tarefa que um algoritmo maior deveria executar, ou seja, ele é uma
parte do programa, especializado em alguma funcionalidade. Na linguagem C, o uso de funções
é a principal forma de modularização.
Uma função matemática f (x) é uma relação que mapeia um dado valor x de um domı́nio em
um único valor y de um conjunto imagem. Em programação, a idéia é semelhante: uma função
é um conjunto de instruções que recebe alguns valores como dados de entrada e, a partir deles,
produz um valor como saı́da. A Figura 3.1 ilustra o funcionamento de uma função matemática.
3.3.1 Cabeçalho
O cabeçalho do subprograma é onde estão definidos o nome do subprograma, os tipos dos seus
parâmetros de entrada e o tipo de dado de retorno.
Os parâmetros da função são os ingredientes que ela precisa para executar as suas instruções
e seus tipos devem ser fielmente respeitados. Já o tipo de retorno da função simboliza o tipo de
produto gerado por ela. Se a função diz retornar um número float, quer dizer que o programador
deve esperar receber apenas esse tipo de dado e tomar as medidas necessárias para manipulá-lo
posteriormente.
O nome do subprograma serve para identificá-lo. O programador deve escolher nomes auto-
explicativos sobre a funcionalidade da função, ou seja, para que ela serve. Isso torna o código
mais legı́vel e menos susceptı́vel a erros por parte do programador.
3.3. PARTES DE UM SUBPROGRAMA 73
No Exemplo 3.1, a função calculaMedia possui dois parâmetros de entrada do tipo float
e retorna um valor também do tipo float. O primeiro parâmetro recebe o nome a e o segundo
recebe o nome b. A função retorna um dado do tipo float, ou seja, retornará um número que
contém casas decimais. Mais detalhes sobre passagem de parâmetros, assim como o retorno de
funções serão mostrados posteriormente.
Percebe-se que o nome da função já nos dá a idéia do que que ela faz: calcula a média de dois
números. Porém, maiores explicações sobre essa média, se é aritmética, geométrica ou outra,
devem ser explicadas nos comentários da função.
Quando a quantidade de parâmetros de uma função não é respeitada, a função não tem todos
os dados necessários para a realização de suas instruções. Na função do Exemplo 3.1, quando
o programador chama a função calculaMedia passando apenas um número, ao invés de dois,
o compilador avisa o programador sobre tal erro, não deixando que o programa seja gerado.
Assim, evita-se erros de execução.
Também podem ocorrer erros quando a ordem ou tipo dos parâmetros não são respeitadas.
No cotidiano, também ocorrem esses tipos de erros. Por exemplo, para que um carro funcione,
ele precisa, entre outras coisas, de água e óleo lubrificante. Caso a água seja colocada no lugar do
óleo lubrificante, ou o contrário, é previsı́vel que o carro, em algum momento, apresente falhas
na execução de suas operações, não se locomovendo. Assim, fica fácil perceber que um bom
programador deve sempre verificar se está respeitando os tipos dos parâmetros das funções.
variáveis: quantidade de tinta e cor da tinta. Essa variáveis não são visı́veis para os outros
robôs; em particular, para o robô de solda.
3.3.3 Corpo
O corpo do subprograma é onde estão contidas as instruções a serem executadas pelo subpro-
grama. Nele, podem ser realizadas operações, tais como: atribuições de valores às variáveis,
chamadas de outras funções, cálculos de expressões, leitura de dados e apresentação de resulta-
dos.
O Pseudocódigo 3.1 descreve uma função que calcula a distância entre dois pontos no plano
cartesiano.
3.3.4 Comentários
Para melhor legibilidade e entendimento do código do subprograma, é recomendável fazer co-
mentários.
Acima de cada subprograma pode-se colocar comentários com as seguintes finalidades: dizer
para quê essa função serve, quais os parâmetros que ela recebe, explicitando como devem ser
esses parâmetros (unidade de medida, relevância para a função, etc), restrições para aplicações,
entre outras.
No dicionário de dados, os comentários podem ser usados para explicar o significado de al-
guma variável cujo nome não é suficientemente significativo. No corpo da função, os comentários
podem ser usados para explicar o que foi feito em determinado conjunto de instruções cujo en-
tendimento não é fácil ou que não tem utilidade aparente.
Na linguagem C, os comentários dos subprogramas devem ser feitos da mesma maneira usada
para comentar o programa, entre “/*” e “*/” ou após “//”, como explicado no Capı́tulo 1.
O Exemplo 3.3 mostra um comentário geral da função distancia, explicando os parâmetros
da função e como é feito o cálculo.
1 /*
2 Funç~
a o para calcular a dist^
a ncia entre dois pontos no plano cartesiano .
3 Dados de entrada :
4 float x1 : a coordenada x do primeiro ponto .
5 float y1 : a coordenada y do primeiro ponto .
6 float x2 : a coordenada x do segundo ponto .
7 float y2 : a coordenada y do segundo ponto .
8
9 Dados de saı́da :
10 dist : retorna a dist^
a ncia entre os pontos passados
11 */
12
13 float distancia ( float x1 , float y1 , float x2 , float y2 ) {
14
76 CAPÍTULO 3. MODULARIZAÇÃO
15 float dx , dy , dist ;
16
17 dx = x2 - x1 ;
18
19 dy = y2 - y1 ;
20
21 dist = sqrt ( dx * dx + dy * dy ) ;
22
23 return dist ;
24 }
7
8 dx = x2 - x1 ;
9
10 dy = y2 - y1 ;
11
12 dist = sqrt ( dx * dx + dy * dy ) ;
13
14 return dist ;
15 }
16
17
18 main () {
19 float xa , xb , ya , yb , dist ;
20
21 printf ( " Forneça as coordenadas x e y ( em quil^
o metros ) das cidades A e B ,
respectivamente : " ) ;
22
23 scanf ( " % f % f % f % f " , & xa ,& ya ,& xb ,& yb ) ;
24
25 dist = distancia ( xa , ya , xb , yb ) ;
26
27 printf ( " Dist^
a ncia em quil^
o metros entre as cidades A e B : % f \ n " , dist ) ;
28
29 printf ( " Forneça as coordenadas x e y ( em metros ) da cadeira e da mesa do seu
escritório : " ) ;
30 scanf ( " % f % f % f % f " , & xa ,& ya ,& xb ,& yb ) ;
31
32 dist = distancia ( xa , ya , xb , yb ) ;
33
34 printf ( " Dist^ a ncia em metros entre a cadeira e a mesa do seu escritório : % f \ n
" , dist ) ;
35 }
Quando o programa principal chama o subprograma distancia, os valores que estão contidos
nas variáveis xa, ya, xb e yb são passados para as variáveis x1, y1, x2 e y2 do subprograma.
É importante perceber que, quando é feita uma passagem de parâmetros, apenas os valores
das variáveis são passados para o subprograma chamado e não as variáveis em si. Esse método
de passagem de parâmetro é chamado de passagem por cópia, ou então, passagem por valor.
A primeira implicação dessa regra é a a não necessidade de se usar variáveis como entrada
para outra função, podendo-se usar diretamente um valor. O Exemplo 3.5 ilustra essa idéia. No
programa principal é feita a chamada ao subprograma distancia passando-se os valores 10 ,
−15 , 26 e −23 como parâmetros. Na função distancia, as variáveis x1, y1, x2 e y2 recebem
esses valores, para só então a função iniciar suas instruções.
3.5. PASSAGEM DE PARÂMETROS 79
Outra implicação do fato de que apenas valores são passados como parâmetro para uma
função é a impossibilidade de mudar o conteúdo das variáveis que são usadas como entrada
para a função. No Exemplo 3.6, a variável a, da função dobraValor , recebe apenas o valor
da variável x da função principal. Com isso, a variável x permanece com o seu valor anterior à
chamada da função dobraValor , apesar da variável a dessa função ter o seu valor alterado.
19 }
A saı́da da execução do programa do Exemplo 3.6 é mostrada no Exemplo 3.7. Note que,
como esperado, a variável x permaneceu com o seu valor original.
1 Número original : 10
2 O seu dobro é : 20
3 O valor de x é : 10
Uma outra implicação do método de passagem de valores é a não visibilidade das variáveis
do programa principal no subprograma chamado, ou seja, o subprograma não pode utilizar as
variáveis do programa principal ou de algum outro subprograma. No Exemplo 3.8, as variáveis
x e y não podem ser acessadas dentro da função alteraValor . Com isso, o compilador emite
um erro quando o programa é compilado, dizendo que as variáveis x e y não foram declaradas
anteriormente e a compilação do programa é interrompida.
O que ocorre no Exemplo 3.8 também pode ser comparado com os robôs da fábrica de
automóveis. O “Robô de Solda” não pode usar a variável local cor do automóvel do “Robô de
Pintura”. O “Robô de Solda” não sabe o que é essa variável, nem que ela existe, assim, não
pode usá-la.
Na linguagem C, deve-se declarar o tipo de dado retornado pela função em seu cabeçalho
e usa-se o comando return para especificar a variável ou valor que deve ser retornado. Após a
sua execução, a execução da função corrente termina, mesmo que existam mais instruções após
o return. Na implementação da função distancia, feita no Exemplo 3.2, o valor contido na
variável dist é retornado.
Como a main também é uma função, também temos que especificar o tipo de dado retornado
por ela. Por padrão, na linguagem C, a função main retorna um valor tipo int. Quando o
programador não coloca nenhum valor para ser retornado pela main (como no Exemplo 3.9),
alguns compiladores fazem com que o valor 0 seja retornado por padrão. O valor 0 também é
usado para simbolizar que nada aconteceu de errado na execução do programa.
Neste livro, optou-se por não explicitar o valor retornado pela função main, já que este valor
não será usado no programa. Contudo, vale lembrar que alguns compiladores exigem que o
retorno da main esteja escrito no código fonte, ficando a cargo do programador a percepção da
obrigatoriedade de escrevê-lo.
Considere um programa que calcula a média de um aluno e diz se ele foi aprovado ou não.
O Pseudocódigo 3.2 mostra os passos a serem realizados pelo programa.
Uma possı́vel implementação do Pseudocódigo 3.2 está transcrita no Exemplo 3.9. Primei-
ramente, o programa pede que o usuário digite os valores das notas das duas provas realizadas
pelo aluno. Essas notas são armazenadas nas variáveis nota1 e nota2 respectivamente e, logo
em seguida, são passadas como parâmetros para a função calculaMedia. A função calcula-
Media recebe dois valores do tipo float e faz a média aritmética deles, retornando, ao final da
82 CAPÍTULO 3. MODULARIZAÇÃO
sua execução, o valor dessa média. O valor retornado é armazenado na variável resultado da
função principal, que por sua vez serve de condição para a aprovação do aluno. Caso o valor
armazenado em resultado seja maior ou igual a sete, o aluno está aprovado; caso contrário, o
programa pede a nota da prova final do aluno, que é armazenada na variável notaFinal. As
variáveis notaFinal e resultado são então usadas como parâmetros da função calculaMedia.
Caso o resultado retornado seja maior ou igual a cinco, o aluno está aprovado.
Note que, na primeira chamada da função calculaMedia, o valor retornado foi armazenado
em uma variável, pois precisa ser usado posteriormente. Já na segunda chamada da função, o
3.6. RETORNO DE DADOS 83
valor retornado é usado diretamente na condicional de aprovação ou não do aluno, não sendo
necessário seu armazenamento em alguma variável. Mas atenção: caso fosse necessário usar esse
valor posteriormente, deverı́amos guardá-lo em alguma variável, ao invés de chamar outra vez
a função calculaMedia usando os mesmos parâmetros. Essa chamada sendo feita novamente
acarretaria em perda de desempenho do programa, já que a função teria que recalcular valores,
o que levaria um certo tempo para ser feito.
Em muitos casos é interessante, ou até necessário, que o subprograma altere as variáveis
criadas no programa que o chamou. No exemplo da fábrica de automóveis é isso que deve
acontecer. A Figura 3.5 ilustra esse processo.
Nesse exemplo, o carro que é passado para o robô de solda está com várias peças soltas.
Então, o robô solda essas peças e passa o carro para o robô de pintura. Nesse caso, percebe-
se que deve-se passar o mesmo carro para o próximo robô e não apenas um cópia das suas
caracterı́sticas, que simbolizam os valores das variáveis em programas. Então, o carro recebe a
pintura e é passado como parâmetro para uma próxima função, ou seja, para um outro robô.
Como discutido na Seção 3.5, não é possı́vel para um subprograma que recebe o valor de
uma variável de um outro subprograma, alterar diretamente o valor da variável do subprograma
que o chamou. Nesse caso, pode-se retornar o valor calculado e então armazená-lo na variável
desejada. A expressão geral é x = f (x).
Por exemplo, pode-se fazer uma variação do uso da função dobraValor usada no Exemplo
3.6 para alterar o valor da variável do programa que a chamou. Essa alteração está descrita no
Exemplo 3.10. Nesse exemplo, o valor retornado pela função dobraValor agora é armazenado
na variável x.
5
6 a = 2* a ;
7
8 printf ( " O seu dobro é : % d \ n " , a ) ;
9
10 return a ;
11 }
12
13 main () {
14 int x = 10;
15
16 x = dobraValor ( x ) ;
17
18 printf ( " O valor de x é : % d \ n " , x ) ;
19 }
A saı́da da execução do programa do Exemplo 3.10 é mostrada no Exemplo 3.11. Note que
a variável x passou a ter o valor 20.
1 Número original : 10
2 O seu dobro é : 20
3 O valor de x é : 20
Ainda no Exemplo 3.12, o encerramento antecipado de execução foi feito para evitar um
erro na execução do programa. Porém, o encerramento antecipado de execução pode ser feito
naturalmente, sem o objetivo de evitar erros de execução.
Considere o caso do Exemplo 3.9. O programa principal poderia ser na verdade um sub-
programa que retorna 1 caso o aluno esteja aprovado e −1 caso contrário. Se o aluno obtiver
média 7 apenas com as duas primeiras notas, a execução do programa é interrompida e o valor
1 é retornado. Essa alteração está descrita no Exemplo 3.13. Note que não é necessário o uso
do else na linha 20, pois caso a o resultado da média seja maior ou igual a sete, a função irá
interromper sua execução, não acarretando o cálculo inválido da média considerando a nota da
prova final do aluno, que não foi feita.
9 }
10
11 int aprovado ( float nota1 , float nota2 , float notaPF ) {
12 float resultado ;
13
14 resultado = calculaMedia ( nota1 , nota2 ) ;
15
16 if ( resultado >= 7) {
17 return 1;
18 }
19
20 if ( calculaMedia ( resultado , notaPF ) >= 5) {
21 return 1;
22 }
23 else {
24 return 0;
25 }
26 }
27
28 main () {
29 float nota1 , nota2 , notaPF ;
30 int passou ;
31
32 printf ( " Forneça as notas das duas provas do aluno e a nota da prova final .
Caso o aluno n~ a o tenha feito prova final , digite zero : " ) ;
33 scanf ( " % f % f % f " , & nota1 , & nota2 , & notaPF ) ;
34
35 passou = aprovado ( nota1 , nota2 , notaPF ) ;
36
37 if ( passou == 1) {
38 printf ( " O aluno passou de semestre .\ n " ) ;
39 }
40 else {
41 printf ( " O aluno n~a o obteve o rendimento mı́nimo para passar de semestre .
Está reprovado .\ n " ) ;
42 }
43 }
Funções sem retorno de dados podem guardar os valores gerados em algum arquivo. Caso isso
seja feito, os dados podem ser recuperados lendo-se esses arquivos. A manipulação de arquivos,
tanto para leitura, tanto para escrita, é discutida na capı́tulo 8.
Há também a opção da função não ter nenhum parâmetro de entrada e nenhum dado retor-
88 CAPÍTULO 3. MODULARIZAÇÃO
nado. O Exemplo 3.16 mostra uma função para exibir uma saudação ao usuário. Note que a
função não possui parâmetros de entradas nem valor de retorno.
3.9 Recursividade
A recursividade ocorre quando algo é definido a partir de si mesmo. Uma recursão sempre é
definida a partir de uma ou mais relações recursivas (quando o próximo elemento é definido a
partir do anterior), e uma ou mais bases de recursão (pontos de parada).
Em programação, o método recursivo pode ser usado para definir uma função. Por exemplo,
pode-se definir uma lista como sendo a união de um elemento com uma lista. Essa lista poderia
ser mais um elemento com mais uma lista ou apenas uma lista vazia. Pode-se simplificar essa
definição por:
Por exemplo, o cálculo do fatorial de um número inteiro não negativo pode ser implementado
através de uma função recursiva. Pode-se definir o fatorial de um número n como sendo o próprio
número n multiplicado pelo fatorial do número n-1. A definiçao de fatorial pode ser representada
por:
Nas definições recursivas anteriores são usados critérios de parada a fim de que o processo
recursivo tivesse um limite. Imagine se uma lista fosse definida somente como um elemento
concatenado com uma lista. Por essa definição nunca se teria uma lista finita, pois sempre novos
elementos teriam que ser adicionados. Para evitar isso usa-se um critério de parada, chamado
de base da recursão. No exemplo da definição da lista, a base da recursão é uma lista vazia. No
exemplo do fatorial de um número, a base da recursão pode ser o número 0, ou seja, é como se
dissesse: “quando chegar ao número 0 pare”.
O Pseudocódigo 3.4 mostra os passos a serem seguidos para o cálculo do fatorial de um
número de forma recursiva.
3.9. RECURSIVIDADE 89
Em geral, toda função recursiva pode ser implementada sem recursão, através do uso de coman-
dos de repetição. A vantagem da versão não recursiva é a eficiência. Já a vantagem da recursiva
é a elegância, legibilidade e redigibilidade.
O Exemplo 3.18 mostra uma forma não recursiva da implementação da função fatorial. O
comando for é utilizado para multiplicar o f por i a cada valor de i, até que i assuma o valor de
n, passado como parâmetro.
Faça uma função que receba um número inteiro positivo n, calcule a soma dos n primeiros
números naturais e retorne esse valor. Considere os números naturais começando do 1. Não use
a fórmula de P.A. (Progessão aritmética).
Solução Possı́vel:
Para produzir a soma dos n primeiros inteiros deve-se gerar a série dos n primeiros valores
inteiros e acumulá-los em uma variável soma.
Na Figura 3.7 é mostrado um exemplo de solução para n = 4. Note que à medida que os
valores de i são gerados eles são acumulados em soma. Fica claro que é necessária a realização
de uma repetição para produzir os valores de i e atualizar soma.
i soma
1 1
2 3
3 6
4 10
O Pseudocódigo 3.5 mostra o algoritmo que descreve os passos a serem realizados pela função.
O Exemplo 3.19 mostra a implementação desse algoritmo. Note que o pseudocódigo foi
seguido fielmente, acrescentado-se apenas instruções necessárias e especı́ficas da linguagem C.
92 CAPÍTULO 3. MODULARIZAÇÃO
Faça um programa que leia um número natural n e dois números naturais i e j diferentes de
0 e imprima em ordem crescente os n primeiros naturais que são múltiplos de i ou de j. Resolva
o problema utilizando uma função que verifique se um número é múltiplo do outro. Exemplo:
Para n = 6 , i = 2 e j = 3 a saı́da deverá ser : 0,2,3,4,6,8.
Solução Possı́vel:
O Pseudocódigo 3.6 mostra um algoritmo que descreve os passos a serem realizados pela
função.
3.10. EXERCÍCIOS RESOLVIDOS 93
Faça uma função que dados três números naturais, verifique se eles formam os lados de um
triângulo retângulo. Os números dados simbolizam o comprimento de cada lado do triângulo.
Solução Possı́vel:
Na geometria, o Triângulo Retângulo é um triângulo que possui um ângulo reto e outros
dois ângulos agudos. Sabe-se ainda que o quadrado da hipotenusa (o maior lado do retângulo)
é igual a soma dos quadrados dos catetos.
Sendo assim, para resolver esse exercı́cio, precisamos primeiro saber qual dos três lados
dados, a, b ou c, corresponde à hipotenusa. Para tanto, compara-se todos os lados, dois a dois,
colocando-se o maior lado na variável a.
Como o maior lado estará guardado em a é certo dizer que os lados dados correspondem a
um triângulo retângulo se, e somente se, a2 = b2 + c2 .
O Pseudocódigo 3.7 mostra um algoritmo que descreve os passos a serem realizados pela
função.
O Exemplo 3.21 mostra a implementação desse algoritmo. Note que para trocar os valores
das variáveis a e b teve-se que ter uma variável auxiliar, chamada aux, para guardar o valor de
a para só depois atribuir o valor da variável b para a variável a e posteriormente atribuir o valor
antigo da variável a (que está guardado na variável aux) para a variável b.
11 aux = a ;
12 a = c;
13 c = aux ;
14 }
15
16 if ( a * a == b * b + c * c ) {
17 return 1;
18 }
19 else {
20 return 0;
21 }
22 }
Faça um programa que dado dois números inteiros, m e n, gere uma tabuada conforme a
Figura 3.9, sendo que para esse exemplo os números fornecidos foram: 22 e 37.
Solução Possı́vel:
Para gerar a tabuada proposta, é preciso perceber onde são usados os números dados. Note
que a tabuada representa o resultado da multiplicação de um número x por um número y. O
número x assume valores desde 0 até o segundo número dado como argumento, enquanto o
número y assume valores desde 1 até o primeiro número dado como argumento.
É fácil perceber que é necessário o uso de comandos de repetição para solucionar o problema.
Pode-se usar 2 comandos de repetição, um contido no outro, diretamente ou indiretamente, via
uma função externa.
O Pseudocódigo 3.8 mostra um algoritmo que descreve os passos a serem realizados pela
função.
3.10. EXERCÍCIOS RESOLVIDOS 97
Funç~a o Principal :
Coletar os valores das duas variáveis ( n e m )
Para um loop com x variando de 0 a m :
Chamar processo componente " mult " , passando como par^
a metro n e x
FIM - Funç~
a o Principal
O Exemplo 3.22 mostra a implementação desse algoritmo. Note que a variável a da função
mult contém o valor da variável n da função main, enquanto a variável b da função mult
contém o valor da variável x da função main.
Seja n inteiro positivo diferente de zero. Faça um programa para exibir os n primeiros
98 CAPÍTULO 3. MODULARIZAÇÃO
números primos.
Solução Possı́vel:
Para exibir os n primeiros números primos é necessário fazer primeiro uma função que verifi-
que se um dado número x é primo ou não. Por definição: “Número primo é um número natural
que pode ser dividido (o resto da sua divisão é zero) apenas por dois números naturais, o 1 (um)
e ele mesmo, com excessão do número 1, que não é tido como primo”.
Sabe-se também que o único número que é primo e par ao mesmo tempo é o número 2. Logo,
qualquer número que seja par (resto da divisão por 2 igual a zero) que não seja o dois, não é
primo.
Então, o que se tem a fazer para saber se determinado número é primo é verificar se ele é o
número dois ou se ele tem apenas 2 divisores. Mas, caso o número em questão seja o número
1, múltiplo de 2 (com excessão do zero e do dois), ou tiver mais de 2 divisores, então ele não é
primo.
Todos os números inteiros positivos diferentes de zero têm ao menos 2 divisores: o 1 e ele
mesmo, com excessão do número 1. Logo, se for encontrado mais algum divisor, o número
estudado não é primo. Então, após feitas as verificações anteriores, para encontrar algum outro
divisor do número, basta ir dividindo-o por todos os números ı́mpares maiores ou iguais a 3 e
menores ou iguais a raiz quadrada do número em questão. Caso não encontremos divisores nesse
intervalo, então não existem outros divisores além do 1 e o próprio número.
O Pseudocódigo 3.9 mostra um algoritmo que descreve os passos a serem realizados pela
função.
O Exemplo 3.23 mostra a implementação desse algoritmo. Note que o valor retornado pela
função ehPrimo foi convencionado. Caso o número passado seja primo, a função retornará o
valor um, caso contrário retornará o valor zero. O programador tem a liberdade de convencionar
o valor retornado, mas deverá indicar isso no comentário do programa.
3.10. EXERCÍCIOS RESOLVIDOS 99
Funç~a o Principal :
Coletar o valor da variável quant que representa o número de primos a serem
impressos
Iniciar numero com valor 1.
Para um loop com cont variando de zero a quant :
Usar a funç~a o ehPrimo para verificar se cont é primo
Caso afirmativo ;
Imprimir cont
Incrementar cont
FIM - Funç~
a o Principal
19 }
20
21 if ( x % 2 == 0) {
22 return 0;
23 }
24
25 for ( i = 3; i <= sqrt ( x ) ; i = i +2) {
26 if ( x % i == 0) {
27 return 0;
28 }
29 }
30
31 return 1;
32 }
33
34 int main () {
35
36 int quant , numero , cont ;
37
38 printf ( " Digite a quantidade de números primos a serem impressos : " ) ;
39
40 scanf ( " % d " , & quant ) ;
41
42 printf ( " Os números primos s~
ao : " ) ;
43
44 for ( numero = 1 , cont = 0; cont < quant ; numero ++) {
45 if ( ehPrimo ( numero ) == 1) {
46 printf ( " % d " , numero ) ;
47
48 cont ++;
49 }
50 }
51
52 printf ( " \ n " ) ;
53
54 return 0;
55 }
Um palı́ndromo é uma palavra, frase ou qualquer outra sequência de unidades (como uma
cadeia de ADN) que tenha a propriedade de poder ser lida tanto da direita para a esquerda
como da esquerda para a direita.
Sabendo-se disso, faça uma função que verifique se um número inteiro é palı́ndromo.
Exemplo:
101 é palı́ndromo
1012 não é palı́ndromo
3.10. EXERCÍCIOS RESOLVIDOS 101
Solução Possı́vel:
Para fazer essa função é necessário ler o número de trás para frente. Para ler o último
algarismo de um número guardado numa variável x basta guardamos numa variável m o resto
da divisão inteira desse número por 10. Após isso, guarda-se em x a parte inteira da divisão de
x por 10, excluindo-se assim o último algarismo do número. Nesse momento a variável m possui
o último algarismo original de x, enquanto esta possui agora apenas os primeiros algarismos
originais, excluindo-se apenas o último.
É fácil perceber que basta fazer os passos acima recursivamente para obter em m o número
x original, mas na ordem inversa. Tendo-se esse número, basta compará-lo com o valor original,
guardado anteriormente em uma outra variável.
A Figura 3.10 ilustra o comportamento das variáveis x e m com o decorrer das iterações
para um x inicial igual a 52325. A coluna x /10 mostra a parte inteira da divisão de x por 10,
enquanto a coluna x % 10 mostra o resto dessa divisão. Percebe-se que o processo iterativo deve
acabar quando o x final for igual a zero. Note que no final desse exemplo a variável m contém
o número inicial x do processo, mostrando que 52325 é palı́ndromo.
O Pseudocódigo 3.10 mostra o algoritmo que descreve os passos a serem realizados pela
função.
102 CAPÍTULO 3. MODULARIZAÇÃO
O Exemplo 3.24 mostra a implementação desse algoritmo. Note que a variável c guarda o
valor inicial de x e que esta variável é comparada com o valor de m ao final do processo. Aqui
também usou-se a convenção de usar o valor de retorno 1 para indicar sucesso na verificação e
zero caso contrário.
Faça uma função que calcule o máximo divisor comum entre dois números naturais passados
como parâmetros da função. Faça a função na forma recursiva e na forma iterativa.
Solução Possı́vel:
3.10. EXERCÍCIOS RESOLVIDOS 103
O Pseudocódigo 3.12 mostra o algoritmo que descreve os passos a serem realizados pela
função iterativa.
O Exemplo 3.26 mostra a implementação desse algoritmo. Note que nesse caso a imple-
mentação na forma iterativa é obtida facilmente a partir da forma recursiva.
3.11 Resumo
• A modularização baseia-se na conhecida técnica de “dividir para conquistar”e pode ser
definida como a divisão de um problema em várias partes. Cada uma dessas partes pode
ser desenvolvida independentemente das outras.
• As partes principais de um subprograma são: cabeçalho, dicionário de dados, corpo e
comentários.
• Os parâmetros de um subprograma são os dados iniciais necessários para a função poder
realizar o seu trabalho. O subprograma pode também não receber nenhum parâmetro para
realizar suas funções. Na linguagem C, quando a função não têm parâmetros usa-se void
na declaração da função, em sua lista de parâmetros.
3.12. EXERCÍCIOS PROPOSTOS 105
• Um subprograma poderá produzir no decorrer das suas instruções um valor que será retor-
nado, chamado de valor de retorno. Na linguagem C, quando a função não produz nenhum
valor de retorno usa-se void para simbolizar o tipo de dado retornado.
• Geralmente é possı́vel criar versões recursivas e não recursivas de uma mesma função.
• Produtividade: o programa pode ser dividido em módulos e cada módulo pode ser traba-
lhado por uma equipe diferente. Além disso, uma equipe só precisa saber o que determi-
nado módulo de outra equipe faz e não como faz;
a b
567890 890 =>encaixa
1243 1243 =>encaixa
2457 245 =>não encaixa
457 2457 =>não encaixa
(b) Usando a função do item anterior, faça um programa que lê dois inteiros positivos a e
b e verifica se o menor deles é segmento do outro.
Exemplo:
a b
567890 678 => b é segmento de a
1243 2212435 => a é segmento de b
235 236 => um não é segmento do outro
106 CAPÍTULO 3. MODULARIZAÇÃO
2. Faça uma função que receba como argumento os lados de um triângulo e calcule o seu
perı́metro e em seguida o valor da área.
4. Faça uma função que receba como parâmetro uma quantia em reais (valor inteiro). Calcule
o número de cédulas de cada tipo (1, 2, 5, 10, 20, 50, 100), necessário para pagar a quantia.
Exiba apenas o número não nulo de cédulas de cada tipo.
5. Faça uma função que receba como entrada dois valores inteiros, a e b, e exiba a sequência
de números pares do intervalo (a, b), com a < b.
7. Faça uma função que calcule e retorne o n-ésimo termo de uma PA (progressão aritmética)
sendo fornecidos como entrada o número de termos, o primeiro termo e a razão.
8. Elabore uma função em C que calcule o valor aproximado de π com precisão de cinco
décimos através da série:
1 1 1
π= 13
− 33
+ 53
. . .
Considere que a precisão de cinco décimos requer que a soma dos elementos da série só
deve ser interrompida quando o valor do termo é inferior a 0,00001. Para implementar a
função, use uma subfunção que, dados x e y, calcule xy .
3.13. TRABALHOS SUGERIDOS 107
+2x4+0x3-25x2+1x1-5x0=
ou seja, a leitura deverá ser pelo teclado e sempre na mesma ordem: <operador> <coeficiente>
<variável> <potência>. Observação: a leitura do teclado deve ser feita termo a termo,
por exemplo, “+2x4 [enter] +0x3 [enter]”. Apenas o caractere “=” poderá ser lido sepa-
radamente.
Uma função f (x) = 2x4 − 25x2 + x − 5 seria digitada da seguinte maneira: (Opção 2)
+2x4-25x2+1x-5=
Ou seja, a leitura deverá ser pelo teclado, porém com algumas diferenças em relação à
opção anterior. Serão omitidos a potência “1”, a variável com potência “0” e os termos
com coeficiente “0”. Observação: nesse caso a leitura também deve ser feita termo a termo,
porém, deve-se lembrar que alguns termos podem ser menores que outros (“+2x4” e “1x”,
por exemplo).
Em ambos os casos, a flag de final de leitura será o caracter “=” e os coeficientes serão
número reais (float). A idéia é que, se o programador optar pela segunda opção, o algoritmo
deverá ser genérico ao ponto de receber as duas formas de entrada.
Armazenada a função de entrada, o programa deverá imprimir, no mesmo formato de
entrada, a derivada e a integral da função. Como se não bastasse a folga do monitor, ele
ainda deseja que o programa imprima também as raı́zes das funções quando estas forem
do primeiro ou do segundo grau.
Exemplos de Saı́da
Usando a função de entrada: f (x) = x2 − 5x + 6
Objetivos:
Este capı́tulo busca esclarecer como a criação e manipulação de novos tipos de dados permite
facilitar o entendimento, a confecção e o reaproveitamento de código.
109
110 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS
As formas de representação de dados fornecidas pelo computador são insuficientes para que o
programador possa representar em sua totalidade as possı́veis coleções de dados existentes. Pro-
gramadores há muito tempo reconhecem o valor de se organizar itens de dados correlacionados
em uma única entidade computacional. As Estruturas são entidades que representam tipos de
dados e que permitem o agrupamento de várias variáveis de diversos tipos.
Suponha um programa que faça o cadastro de estudantes de uma universidade, cada um
possuindo um conjunto de atributos correlacionados, como por exemplo: matrı́cula, idade, co-
eficiente de rendimento e perı́odo. Utilizando as entidades de programação já conhecidas, no
cadastro de um único aluno seriam necessárias quatro variáveis para representar todos os seus
atributos; para dois estudantes, seriam necessárias oito variáveis; já para diversos estudantes
seria necessário um conjunto de variáveis consideravelmente grande.
A utilização de muitas variáveis aumenta o trabalho do programador e dificulta que outra
pessoa entenda a aplicação. Para solucionar esse problema, muitas linguagens de programação,
entre elas a linguagem C, possibilitam ao programador criar tipos compostos heterogêneos. Na
linguagem C, os tipos compostos heterogêneos são chamados struct (estruturas). As estruturas
são conjuntos de dados correlacionados, que podem ser representados em sua totalidade por uma
única variável. No exemplo mencionado, cada estudante poderia ser representado por uma única
variável, dessa forma, seria necessário adicionar apenas uma variável para cada novo estudante
a ser cadastrado.
Dada a impossibilidade de se antecipar todos os tipos de dados utilizados por um programa,
uma linguagem de programação apenas implementa tipos de dados simples. Desse modo, fica a
cargo do programador usar desses dados para a definição de novos tipos de dados.
Além de facilitar e tornar mais clara a implementação, os tipos compostos heterogêneos têm
como finalidades principais a possibilidade de retorno de mais de um valor por função e a criação
de tipos abstratos de dados. Essas caracterı́sticas são discutidas mais adiante neste capı́tulo.
4.2. TIPOS COMPOSTOS HETEROGÊNEOS (ESTRUTURAS) 111
4.2.1 Definição
Uma estrutura é uma coleção de variáveis, que podem ou não ser de tipos diferentes, colocadas
sob um único nome para manipulá-las. As estruturas ajudam na organização do código e fa-
cilitam a vida do programador, pois juntam variáveis relacionadas e permitem que elas sejam
tratadas como uma unidade maior.
Na prática, uma estrutura é uma “caixa” onde podem ser agrupados diversos dados correla-
cionados. Essa caixa, na verdade, é o conjunto de alguns bytes de memória correspondente ao
somatório dos tamanhos dos dados que se pretende agrupar.
A Figura 4.1 ilustra uma estrutura que representa um estudante, tEstudante. Note que a
estrutura tEstudante ocupa um espaço equivalente ao espaço necessário para guardar todos os
seus atributos.
Sintaxe
Para criar uma estrutura com n atributos, deve-se obdecer à sintaxe exposta abaixo.
A palavra struct indica que a entidade criada é uma estrutura e as chaves delimitam o trecho
onde os atributos da estrutura são definidos. Cada atributo é definido por seu tipo seguido de
um identificador.
O Exemplo 4.1 apresenta a sintaxe de uma estrutura para o caso do estudante.
1 struct tEstudante {
2 int idade ;
3 int matricula ;
112 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS
4 float coeficiente ;
5 int periodo ;
6 };
4.2.2 Uso
Como as estruturas são conjuntos de dados, existem duas formas básicas de manipulá-las, se-
lecionando um único elemento de dado de forma seletiva, ou manipulando toda a estrutura de
forma integral.
Seletivo
Para manipular cada um dos atributos da estrutura se utiliza o mecanismo de seleção conforme
a sintaxe apresentada a seguir.
O Exemplo 4.2 ilustra o uso seletivo da estrutura estudante. Nesse Exemplo, a leitura de
dados é feita atribuindo-se cada valor lido a um atributo da estrutura.
Integral
Quando se deseja manipular a estrutura como um todo, se utiliza simplesmente o nome da
variável onde a mesma está armazenada. O uso integral em atribuições é descrito pela sintaxe
a seguir.
4.2. TIPOS COMPOSTOS HETEROGÊNEOS (ESTRUTURAS) 113
No Exemplo 4.3, ao final da execução do programa os valores dos atributos de outro são
iguais aos valores dos atributos de aluno.
O comando typedef pode ser utilizado para renomear tipos simples ou compostos. Para
utilizar o comando typedef, deve-se obdecer à sintaxe exposta abaixo.
O Exemplo 4.5 apresenta a adaptação do Exemplo 4.3 para a utilização do comando typedef,
permitindo que o tipo composto heterogêneo struct tEstudante seja utilizado através da palavra
struct tEstudante.
6 int matricula ;
7 float coeficiente ;
8 int periodo ;
9 };
10
11 typedef struct tEstudante tEstudante ;
12
13 main () {
14 tEstudante aluno , outro ;
15 printf ( " Digite os dados do estudante " ) ;
16 scanf ( " % d % d % f % d " ,& aluno . idade ,& aluno . matricula ,& aluno . coeficiente ,& aluno .
periodo ) ;
17 outro = aluno ;
18 }
1 void funcao1 ( int idade , int matricula , float coeficiente , int periodo ) { ... }
2
3 void funcao2 ( tEstudante aluno ) { ... }
A mesma função pode ser escrita dos dois modos apresentados no Exemplo 4.6 na funcao1
quatro variáveis relacionadas ao estudante são passadas como parâmetro; na funcao2 , o próprio
estudante é passado como parâmetro. A funcao2 é mais compreensı́vel que a funcao1 , pois a
pessoa que ler o código da funcao1 terá que inferir que aquelas quatro variáveis referem-se a
um mesmo estudante.
Da mesma forma, o retorno de funções pode ser simplificado pelo uso de estruturas. Suponha
que se deseje fazer a leitura de dados de um estudante. O Exemplo 4.7 mostra duas maneiras de
fazer isso: utilizando funções que retornam estruturas ou funções que retornam tipos simples.
8 int periodo ;
9 };
10
11 typedef struct tEstudante tEstudante ;
12
13 int lePeriodo () {
14 int periodo ;
15 scanf ( " % d " ,& periodo ) ;
16 return periodo ;
17 }
18
19 int leMatricula () {
20 int matricula ;
21 scanf ( " % d " ,& matricula ) ;
22 return matricula ;
23 }
24
25 float leCoeficiente () {
26 float coeficiente ;
27 scanf ( " % f " ,& coeficiente ) ;
28 return coeficiente ;
29 }
30
31 int leIdade () {
32 int idade ;
33 scanf ( " % d " ,& idade ) ;
34 return idade ;
35 }
36
37 tEstudante leEstudante () {
38 tEstudante aluno ;
39 scanf ( " % d % d % f % d " ,& aluno . idade ,& aluno . matricula ,& aluno . coeficiente ,& aluno .
periodo ) ;
40 return aluno ;
41 }
42
43 main () {
44 tEstudante aluno1 , aluno2 ;
45 aluno1 . periodo = lePeriodo () ;
46 aluno1 . matricula = leMatricula () ;
47 aluno1 . coeficiente = leCoeficiente () ;
48 aluno1 . idade = leIdade () ;
49 aluno2 = leEstudante () ;
50 }
No Exemplo 4.7 é realizada a leitura de dados de aluno1 sem utilizar o retorno de estruturas,
e de aluno2, utilizando-o. Para realizar a leitura de aluno1 foram necessárias a declaração de
quatro funções diferentes, uma para cada atributo, e quatro chamadas de função no programa
principal. Para realizar a leitura de aluno2 foi necessário apenas a declaração de uma função
116 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS
e uma chamada de função no programa principal. Assim, pode-se perceber que o retorno de
estruturas torna o código mais compacto, legivel e eficiente, por ter menos chamadas de função.
4.3.1 Definição
Um tipo abstrato de dados é um tipo de dado definido em termos do seu comportamento e
não em termos de sua representação. A idéia de tipo abstrato de dados é desvincular o tipo de
dado (valores e operações) de sua implementação, ou seja, quando definimos um tipo abstrato
de dados estamos preocupados com o que ele faz e não como ele faz.
Os programas que usam um determinado tipo abstrato de dados são chamados clientes; e
o programa que define sua estrutura e comportamento é conhecido como implementação. Um
TAD pode ser definido como a composição de uma estrutura de dados e das operações definidas
sobre estes dados.
• inicializaData: função que inicializa uma data a partir de valores passados como parâmetro.
• leData: função que inicializa uma data a partir de valores lidos do teclado.
• alteraData: função que altera uma data a partir de valores passados como parâmetro.
O código do Exemplo 4.8 ilustra a definição de um Tipo Composto com Operações Pré-
definidas. O tipo tData contém os atributos de uma data. Uma variável desse tipo é inicializada
através da função inicializarValores, essa função permite inicialização dos atributos sem que
estes sejam acessados. A função de leitura leData obtém uma data do teclado e retorna uma
variável do tipo tData contendo os valores lidos. A função alteraData apresenta o mesmo
código que a função de inicialização, contudo sua utilização destina-se a datas já inicializadas.
A função eBissexto verifica se uma dada data está em um ano bissexto, o algoritmo considera a
seguinte regra: São bissextos todos os anos múltiplos de 400, não são bissextos todos os múltiplos
de 100 e não de 400, são bissextos todos os múltiplos de 4 e não múltiplos de 100, por fim, não
são bissextos todos os demais anos. A função diasNoMes determina o número de dias de um
do mês de uma determinada data. Os meses de abril, junho, setembro e novembro possuem 30
dias. Caso a data seja do mês de fevereiro, verifica-se se o ano é bissexto, se for bissexto o mês
4.3. TIPOS ABSTRATOS DE DADOS 119
tem 29 dias, se não for bissexto o mês tem 28 dias. Caso a data não esteja em nenhum dos
meses citados, o mês apresenta 31 dias.
Observe que, no Exemplo 4.9, a intenção do programador foi adicionar um dia à data lida,
entretanto, este não utilizou a função do tipo diaSeguinte. Para grande parte dos valores de
data, o programa funcionaria corretamente, entretanto, para datas que são o último dia do mês,
esse programa gera inconsistência, pois a data obtida não será uma data válida. No decorrer da
aplicação, um valor inválido de data será repassado para outras partes do programa. Assim essa
inconsistência pode ter um efeito colateral que acarrete em problemas no código. Sendo assim,
é altamente recomendável que se utilizem apenas as operações já definidas para manipular as
estruturas do TAD e que não ocorra a manipulação de atributos.
• Integridade: A manipulação dos atribuitos por operações definidas sobre o tipo impedem
a ocorrência de inconsistências.
Na prática, essas vantagens tornam o código mais fácil de se escrever, mais compreensı́vel
para quem lê e mais fácil de se modificar.
Construtoras
Operações construtoras são aquelas que inicializam variáveis, logo devem ser utilizadas antes de
qualquer outra para garantir que o TAD foi inicializado corretamente. Observe no Exemplo 4.10
diferentes implementações de funções construtoras para o TAD estudante.
1 tEstudante inicializar () {
2 tEstudante novo ;
4.3. TIPOS ABSTRATOS DE DADOS 121
3
4 novo . idade =0;
5 novo . matricula =0;
6 novo . coeficiente =0;
7 novo . periodo =0;
8 return novo ;
9 }
10
11 tEstudante inicializarValores ( int idade , int matricula , float coeficiente , int
periodo ) {
12 tEstudante novo ;
13
14 novo . idade = idade ;
15 novo . matricula = matricula ;
16 novo . coeficiente = coeficiente ;
17 novo . periodo = periodo ;
18 return novo ;
19 }
A função inicializar inicia com zero todos os atributos de uma variável do tipo tEstudante,
esta variável é retornada pela função de inicialização. A função inicializarValores exerce o
mesmo papel de inicializar que a função inicializar . Contudo, esta recebe os valores a serem
incializados como parâmetros da função.
Analisadoras
Modificadoras
As operações modificadoras, ou atualizadoras, permitem alterações de atributos do TAD. O
Exemplo 4.12 mostra uma função modificadora para o TAD tEstudante.
Produtoras
As operações produtoras são aquelas que, a partir dos dados de um TAD, produzem uma nova
informação. A função maiorIdade, do Exemplo 4.13, produz um resultado (a maior idade) a
partir de dois estudantes dados.
Destrutoras
As operações destrutoras são utilizadas para liberar recursos de memória quando o TAD não é
mais necessário. Exemplos e explicações sobre essa função são dados no Capı́tulo 7.
9
10 data = leData () ;
11 anoBissexto = eBissexto ( data ) ;
12
13 if ( anoBissexto == 1) {
14 printf ( " Ano Bissexto " ) ;
15 } else {
16 printf ( " Ano n~
a o Bissexto " ) ;
17 }
18
19 nDias = diasNoMes ( data ) ;
20 printf ( " Número de dias no m^
e s : " , nDias ) ;
21 }
O Exemplo 4.14 determina se uma data digitada pelo teclado é de um ano bissexto e o
número de dias no mês da data digitada. Pode-se afirmar que o programa usuário manipula o
TAD somente através das operações pré-definidas pelo implementador, assim o código usuário
fica mais legı́vel. Pode-se observar que as operações separam o código usuário do código de
implementação do TAD. A redigibilidade também aumenta, visto que o acesso aos dados é
realizado apenas por simples operações. O código também fica mais confiável, pois o usuário
não altera livremente os dados, isso só pode ser feito através das operações do TAD. E, por fim,
caso a implementação do TAD precise ser alterada, o código usuário não sofrerá mudanças, a
menos que os cabeçalhos das funções sejam alterados.
TADs de Domı́nio
São aqueles que definem um tipo de dados que está no domı́nio do problema. Como por exemplo,
os TADs tEstudante e tData.
TADs Implementacionais
São de objetos de programação que não tem relação direta com o problema, mas sim com sua
implementação. Podemos tomar como exemplos as listas, as árvores, os grafos e as filas. Alguns
desses conceitos são apresentados nos próximos capı́tulos.
Faça um programa que imprima de uma estrutura do tipo data, a data de nascimento
30/01/1984.
Solução Possı́vel:
Não existem dados de entrada, a data de nascimento a ser informada é armazenada direta-
mente em uma estrutura data. A impressão da data é realizada no formato abreviado utilizando
o comando printf . A saı́da do programa é a impressão da data 30/01/1984.
Defina, agora, a estrutura do exemplo anterior como um tipo e realize operações (sobre este
tipo) de edição e consulta.
Solução Possı́vel:
Este programa utiliza operações de alteração e consulta sobre o TAD para datas. A estrutura
para datas é declara como um tipo utilizando o comando typedef. A operação alteraData
recebe como parâmetros o dia, mês e ano de uma determinada data e passa, como retorno de
função, a data informada. A operação consultaData realiza a impressão de uma data. Assim,
pode-se observar as vantangens da utilização de TADs, através destas duas operações é necessário
escrever menos, visto que as operações podem ser aproveitadas. A data é inicializada no corpo
do programa em 30/01/1984, em seguida é alterada. A saı́da do programa é a impressão da
data 16/05/2007.
Escreva um programa em C que leia duas datas e retorne a menor data cronologicamente.
Observação: As datas devem ser armazenadas em estruturas.
Solução Possı́vel:
O programa que determina qual é a menor data entre duas. Agora, a inicialização das
datas é realizada por leitura do teclado. O retorno da operação menorData é a menor entre
duas datas, a primeira verificação é realizada quanto ao ano, caso o ano das datas sejam iguais
verifica-se o mês, se os meses são iguais verifica-se os dias.
Defina o tipo Tponto para representar os pontos do plano cartesiano de duas dimensões.
Solução Possı́vel:
Para representar um ponto, é criada uma estrutura com dois atributos, as coordenadas x e
y.
4.4. EXERCÍCIOS RESOLVIDOS 127
1 typedef struct {
2 int x ;
3 int y ;
4 } Tponto ;
Solução Possı́vel:
A operação simetricoOrigem tem como retorno uma variável do tipo Tponto. Para obter
um ponto simétrico a outro em relação a origem, basta inverter as coordenadas do ponto dado.
Solução Possı́vel:
Dado um ponto, a operação qualQuadrante retorna a qual quadrante esse ponto pertence.
A função retorna 0 quando o ponto for parte do limite de quadrantes, 1 quando o ponto pertencer
ao 1o quadrante, 2 quando pertencer ao 2o quadrante, 3 se pertencer ao 3o quadrante e 4 caso seja
do 4o quadrante. Para determinar a que quadrante o ponto pertence, é utilizado um conjunto
de if e else aninhados que verificam os sinais das coordenadas.
12 if ( ponto . y > 0) {
13 return 2;
14 } else {
15 return 3;
16 }
17 }
18 }
Defina um programa que calcule a distância entre dois pontos, implemente e use operações
construtoras e destrutoras, implemente também a operação float distanciaPontos (Tponto
a, Tponto b), de cálculo de distância entre dois pontos.
Solução Possı́vel:
Inicialmente, são definidas a estrutura Tponto e as operações aplicaveis a ela. Além das
funções relacionadas ao problema, deve-se sempre definir as funções inicializadora e destrutora.
A função ler() inicializa o Tponto a partir de valores fornecidos pelo usuário, a função zera()
zera as coodernadas do ponto e a função distanciaPontos calcula a distancia entre pontos, dada
pela raiz quadrada da soma do quadrado da diferença entre as abscissas e entre as ordenadas.
A função principal utiliza os métodos declarados de forma a corresponder à funcionalidade
desejada.
25
26 return origem ;
27 }
28
29 // Operaç~
a o Produtora :
30 float distanciaPontos ( Tponto a , Tponto b ) {
31 float distancia , deltaX , deltaY ;
32
33 deltaX = a . x - b . x ;
34 deltaY = a . y - b . y ;
35
36 distancia = sqrt ( deltaX ^2 + deltaY ^2) ;
37 return distancia ;
38 }
39
40 main () {
41 Tponto a , b ;
42 float distancia ;
43
44 a = ler () ;
45 b = ler () ;
46
47 distancia = distanciaPontos (a , b ) ;
48 printf ( " Distancia entre pontos : % f \ n " , distancia ) ;
49
50 a = zera () ;
51 b = zera () ;
52 }
4.5 Resumo
• Inicialmente foram abordadas as seguintes abordagens na programação:
– bottom-up: que é uma técnica que propõe considerar o programa como um conjunto
de módulos correlacionados, implementar cada um desses módulos e depois juntá-los
por meio de uma estrutura global.
– top-down: que é uma técnica que proporciona que um programa seja visto como
uma descrição de um processo. O processo é dividido em subprogramas, os quais são
responsáveis por cumprir partes da funcionalidade geral do processo. Por sua vez,
cada subprograma ainda pode ser dividido em novos subprogramas.
• Utilizou-se à técnica ”dividir para conquistar”nas duas abordagens, onde cada abordagem
deve ser utilizada de acordo com as informações conhecidas do sistema. Aqui enfatizou-se
a abordagem bottom-up porque ela permite que o programador usuário não se preocupe
com a implementação de cada módulo, bastando saber a funcionalidade de cada módulo
para que os mesmos possam ser integrados.
130 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS
• Uma estrutura agrupa várias variáveis numa só. Funciona como uma ficha pessoal que
tenha nome, telefone e endereço. A ficha seria uma estrutura. A estrutura, então, serve
para agrupar um conjunto de dados não similares, formando um novo tipo de dados.
Estas provêem uma grande facilidade para o armazenamento de tipos não definidos pela
linguagem. O uso de estruturas é considerado uma ótima prática de programação, visto
que, além de permitirem uma organização melhor, possibilitam realizar atribuições ou
passagem de parâmetros em uma única vez. Uma estrutura pode ser manipulada de forma
seletiva (acesso a um único atributo) ou de forma integral (acesso a estrutura como um
todo).
• Os TADS são generalizações das estruturas, pois associamos várias operações sobre a
estrutura, estas podem ser dos seguintes tipos: construtoras, analisadoras, modificadoras,
produtoras e destrutoras. Assim, os TADS são definidos como uma estrutura de dados e as
operações sobre os dados. A importância de um TAD está na transparência que ele oferece,
o usuário do tipo não precisa se preocupar em como o TAD é implementado. O uso de
TADs permite inúmeras vantagens como abstração, facilidade de alteração, reutilização,
ocultamento e integridade. Os Tipos Abstratos de Dados são classificados em TADs de
Domı́nio e TADs Implementacionais. TADs de domı́nio são aqueles que descrevem um tipo
do problema, enquanto TADs Implementacionais descrevem uma estrutura que irá ajudar
a resolver o problema, mas não se refere ao problema. Exemplos de TADs de domı́nio são
os registros de alunos, funcionários, datas, etc. Exemplos de TADs implementacionais são
as pilhas, listas, etc.
• Contudo, é importante dizer que a idéia do TAD é que o código da implementação do tipo
de dado, fique invisı́vel para o programador que for usar o tipo. Assim, na verdade, C não
implementa TAD, o que se faz é uma simulação de TDAs. No entanto, a simulação de
TADs em C se torna uma boa prática de programação, pois torna o código do programa
mais compreensı́vel e simplifica a definição das estruturas de dados. Em linguagens que
permitam Programação Orientada a Objetos (POO), os TADs podem ser implementados
com a confiabilidade que C não permite.
Implemente um TAD para o armazenamento dos dados de um cilindro. A entrada dos dados
referentes ao cilindro será realizada via console(teclado). As possı́veis operações sobre o tipo
são:
• Inicializa Cilindro;
• Altera Altura;
• Altera Raio;
• Calcula Volume;
• Imprime Dimensões;
• Imprime Volume;
Modifique o TAD tData incluindo uma operações que determine à idade de uma pessoa, e
quantos dias faltam para seu aniverário tendo como entradas a data de nascimento e a data
atual. Deve-se considerar anos bissextos e meses com o número de dias conforme o calendário
ocidental.
Implemente um TAD Ponto para representar um ponto no plano com as seguintes operações:
Implemente um TAD Circulo para representar um cı́rculo (Observe que o centro é um ponto)
com as seguintes operações:
Você foi contratado para fazer algumas pesquisas no cadastro de uma imobiliária. Considere
que cada imóvel possui um código de identificação, uma área total e o preço por metro quadrado
do imóvel. Implemente um programa que leia os dados dos imóveis, armazene e imprima o
imóvel mais caro e o de maior área em um TAD imóvel.
O sistema deve ler do ticket do usuário o horário e a data de entrada, deve identificar o
horário atual e a respectiva data, calcular o tempo de uso do estacionamento e o valor a ser
pago.
Caso o motorista permaneça no estacionamento por menos de 15 minutos ele não deverá
pagar pelo uso do estacionamento. Caso o motorista permaneça por menos de uma hora ele
deverá pagar pelo preço de uma hora completa.
Implemente um TAD horário que armazene os horários e as datas de entrada e saı́da para
cada veı́culo e que tenha operações como:
• cálculo de custo;
O gerente de uma agência bancária te contratou para fazer um programa em C para extrair
algumas informações das contas dos correntistas de sua agência. Para tanto leia sucessivamente
os dados do número da conta (um valor inteiro positivo) e valor da operação (um valor ponto
flutuante positivo para as operações de crédito e negativo para as operações de débito) realizadas
no último mês. Seu programa deve usar os dados da listagem para informar:
1. As operações suspeitas, isto é, aquelas que movimentaram acima de 20.000 reais
Nos ı́tens 1, 2, e 3 também devem ser apresentados os números das contas dos correntistas.
Considere que os dados de cada conta são fornecidos contiguamente, isto é, são fornecidos
todos os dados das operações de uma conta, depois todos os dados de outra conta e assim
sucessivamente. O processamento deve ser encerrado quando for lido um número de conta igual
a zero.
134 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS
O diretor do NPD da UFES te contratou para fazer um programa em C para extrair algumas
informações a respeito dos cursos e dos alunos da UFES. Para tanto leia sucessivamente os dados
código do curso (um valor inteiro positivo), a matrı́cula do aluno (um valor inteiro positivo), a
carga horária da disciplina (um valor inteiro positivo) e a média final obtida em uma disciplina
cursada pelo aluno (um valor ponto flutuante). Seu programa deve esses dados para informar:
2. O melhor aluno de cada curso, isto é, o aluno de melhor coeficiente de rendimento no
curso.
3. O curso mais difı́cil, isto é, aquele que tem o menor coeficiente de rendimento médio.
Vetores
Autores:
Andrezinho
Thiago Paris Salviato
Objetivos:
135
136 CAPÍTULO 5. VETORES
vezes (ou a utilização de 50 variáveis - não tente isso em casa!): uma para calcular a média e
outra para verificar quais alunos tiveram notas superiores à média. O Exemplo 5.1 mostra uma
maneira de resolver o problema do professor utilizando a Linguagem C, fazendo a dupla leitura
citada acima.
Todos os problemas citados até agora no capı́tulo poderiam ser resolvidos com a utilização
de vetores.
Um vetor é uma estrutura de dados composta homogênea, isto é, ele possui, em geral, mais
de um elemento - por isso, composta - sendo que estes elementos são sempre de um mesmo tipo
- por isso, homogênea.
Resumindo, a utilização de um vetor se faz necessária sempre que for preciso armazenar
uma quantidade finita de dados de um mesmo tipo, para ser manuseado durante a execução do
programa. No Exemplo 5.1, por exemplo, as notas de todos os alunos da turma poderiam ter
sido armazenadas num único vetor com apenas uma leitura e a partir de então eles estariam
5.2. REPRESENTAÇÃO 137
5.2 Representação
A Figura 5.1 mostra a maneira mais comum de se representar um vetor graficamente.
A Figura 5.1 representa um vetor de inteiros chamado vet com os nove primeiros números
primos, onde o terceiro elemento (elemento de ı́ndice igual a 2) é o número 5 e o de ı́ndice 4 é o
número 11, por exemplo. É importante lembrar que todo o primeiro elemento de um vetor na
linguagem C possui o ı́ndice zero.
Analizando a Figura 5.1 fica evidente outras importantes caracterı́sticas dessa estrutura: a
seqëncialização e a indexação de seus elementos e a sua dimensão.
Quando se declara um vetor num programa, significa que durante a sua execução será re-
servado um espaço de memória seqüencial com o tamanho necessário para que seja possı́vel
armazenar todos os elementos do vetor. Portanto os elementos serão armazenados seqüencial-
mente na memória.
Essa propriedade facilita bastante o acesso do programa ao elemento desejado dentro do
vetor, pois, para que o dado seja recuperado, fica sendo necessário apenas o nome do vetor, que
localiza o primeiro elemento, e um ı́ndice, que indicará ao programa a que distância do inı́cio
está o dado desejado.
Mas para que seja feita a reserva do espaço de memória, deve ser informado no código qual
o tamanho do vetor, isto é, o número máximo de elementos que ele pode armazenar. A Figura
5.2 mostra uma ilustração de como um vetor é armazenado na memória do computador.
Dados os conceitos teóricos do vetor, chega a hora de discutir como essa estrutura pode ser
utilizada na prática, abordando assuntos como a sua definição, as operações definidas sobre ela
e outros. Isso será feito a partir de agora.
5.3 Definição
O primeiro passo para uma utilização clara e efetiva dos vetores é uma declaração que deixe claro
para quê a estrutura está sendo usada. Isso ajuda a dar legibilidade ao código, logo, facilida a
implementação.
Na linguagem C, o vetor é declarado da seguinte maneira:
<tipo_dados> <nome_vetor>[<tam_vetor>];
138 CAPÍTULO 5. VETORES
onde tipo dados representa o tipo dos dados dos elementos que serão armazenados no vetor,
nome vetor o nome pelo qual o vetor será referenciado e tam vetor o número de elementos que
cabem lá dentro. O Exemplo 5.2 mostra alguns exemplos de declaração de vetores.
No Exemplo 5.2, o primeiro vetor declarado pode ser representado graficamente pela Figura
5.1 mostrada na seção anterior, ou seja, seu nome é vet e ele pode armazenar até nove elementos
5.3. DEFINIÇÃO 139
do tipo int. O segundo e o terceiro foram os representados pela Figura 5.2. Aquele, chamado
nome, armazena no máximo 6 elementos do tipo char, e este, chamado notas, até seis do tipo
float. Já o último poderia ser utilizado no problema do professor citado anteriormente, por
exemplo. Neste vetor, chamado notasAlunos, podem ser armazenados cinqüênta valores do tipo
float, que poderiam representar as notas dos cinqüênta alunos.
A estrutura proposta no Exemplo 5.3 possui um vetor chamado nome, do tipo char e de
tamanho igual a 50 para armazenar o nome completo de um aluno; duas variáveis do tipo int
chamadas rg e ano, para armazenar, respectivamente, o número do RG do aluno e seu ano
(série); uma variável do tipo char chamada turma, para armazenar sua turma; e um vetor do
tipo float chamado notas e de tamanho igual a 8, para armazenar as notas desse aluno nas
diferentes disciplinas que ele pode cursar.
140 CAPÍTULO 5. VETORES
Repare que as notas e o nome do aluno podem ocupar um espaço menor que o tamanho
máximo do vetor. Essa caracterı́stica dos vetores definidos em tempo de compilação pode acar-
retar em desperdı́cio de memória, pois os espaços armazenados para eles podem não ser utilizados
totalmente.
Repare que declarando o vetor dessa forma, o desperdı́cio de memória pode ser eliminado,
já que o vetor é utilizado por completo, desde que se tenha conhecimento prévio do número de
elementos que serão armazenados e que se possa informá-lo à aplicação.
É importante ressalvar que essa forma dinâmica de definição de tamanho não funciona no
caso de vetores dentro de estruturas, como o vetor nota da estrutura tAluno do Exemplo 5.3,
por exemplo.
5.4 Operações
Na linguagem C não existem operações pré-definidas para a manipulação de um vetor como um
todo. Por exemplo, não é permitida a leitura de um vetor por inteiro com o comando scanf. As
operações só podem ser feitas para cada elemento do vetor, individualmente.
5.4. OPERAÇÕES 141
Como já mencionado na Seção 5.2, para acessar um elemento de um vetor, são necessários
apenas seu nome e o ı́ndice que informa sua localização. Na linguagem C a sintaxe de acesso a
um elemento de um vetor é dada por:
<nome_vetor> [<ı́ndice>]
onde o ı́ndice pode ser tanto um número inteiro maior ou igual a zero quanto uma expressão
inteira composta por variáveis e números.
Sabendo acessar o elemento ele pode ser manipulado como uma variável qualquer e ser
utilizado em operações das mais diversas como atribuição, operações aritméticas, etc. O Exemplo
5.5 mostra algumas dessas operações em Linguagem C.
No Exemplo 5.5, primeiramente são declarados a variável i, do tipo int; os vetores vet1 e
vet2, ambos do tipo int e de tamanho igual a 10; o vetor notas do tipo float e de tamanho
igual a 8; e as variáveis soma e media. Em seguida, o primeiro elemento do vetor vet1, ou seja,
o elemento de ı́ndice 0 (zero), recebe o valor 43 (linha 11); todos os elementos de vet1 de ı́ndice
ı́mpar recebem o valor 10 (linhas 13 a 15); todos os elementos de vet2 recebem o valor zero
(linhas 17 a 19); cada uma das 8 posições do vetor notas é preenchido por um valor lido do
teclado (linhas 21 a 23); os elementos do vetor vet1 são copiados para vet2 (linhas 25 a 27); e,
por último, é calculada a média dos valores contidos no vetor notas e armazenada na variável
media (linhas 29 a 32).
É importante ressaltar a grande importância da utilização de expressões como ı́ndice, pois
ela é essencial para o acesso rápido a todos os elementos do vetor.
Um outro exemplo interessante para ilustrar as operações sobre os vetores é a resolução do
problema do professor citado na seção 5.1. Já foi proposta uma solução fazendo uma dupla
leitura das notas dos alunos (Exemplo 5.1). O Exemplo 5.6 mostra uma solução utilizando
vetores.
Repare que, dessa vez, os valores referentes às notas são lidos do teclado apenas uma vez e
são, então, armazenados no vetor notas e imediatamente somados para o cálculo da média da
turma, valor este armazenado na variável media. Para a contagem de alunos com nota acima da
média, não é necessária uma nova leitura do teclado, apenas uma consulta ao vetor notas que
já possui todos os valores armazenados.
Os vetores também podem ser passados como parâmetro de funções, mas nunca como retorno.
O Exemplo 5.7 mostra um código em linguagem C que resolve o problema anterior, mas com a
utilização de funções que recebem um vetor como parâmetro.
41
42 for ( i = 0 ; i < 50 ; i ++) {
43 printf ( " Digite uma nota " ) ;
44 scanf ( " % f " ,& notas [ i ]) ;
45 }
46
47 media = calculaMedia { notas };
48
49 quant = alunosAcimaMedia ( notas , media ) ;
50
51 printf ( " \ n % d alunos obtiveram nota acima da média \ n " , quant ) ;
52 }
5.5 Strings
O vetor de elementos do tipo char do Exemplo 5.2, chamado nome, assim como todos os vetores
do tipo char, podem ser considerados de um novo tipo, o tipo string. Strings correspondem a
uma seqüência de caracteres. Geralmente, são usadas para realizar a entrada e saı́da de dados
em um programa e para armazenar dados não numéricos.
É comum representar uma string como um vetor de caracteres (tipo char), dispondo das
mesmas operações comuns a essa estrutura de dados. A Figura 5.3 exemplifica a representação
de uma string como um vetor de caracteres.
Toda string armazenada em um vetor é terminada por um caracter especial, conhecido como
caracter nulo ‘\0’ (“barra-zero”). A principal função desse caracter é alertar o fim de uma string,
5.5. STRINGS 145
evitando um acesso indesejado a uma posição do vetor que se encontra “vazia”. O programa
do Exemplo 5.8 demonstra uma forma não usual de imprimir strings, mas aplica o conceito de
caracter nulo.
No Exemplo 5.8, a condição de parada do laço for é encontrar o caracter ‘\0’, e, durante cada
iteração, cada caracter do vetor é impresso separadamente. A forma mais comum (e simples)
de imprimir uma string é a apresentada no Exemplo 5.9.
Exemplo 5.9: Impressão de uma string sem a utilização direta do caracter “barra-zero”.
O programa do Exemplo 5.9 produz o mesmo resultado do Exemplo 5.8, porém, naquele o
caracter ‘\0’ é verificado implicitamente pela função printf.
A biblioteca padrão de C ainda oferece algumas funções muito úteis para a manipulação de
strings, como a strcpy (cópia), a strlen (tamanho) e a strcmp (comparação). O Exemplo 5.10
demonstra o uso dessas funções.
A primeira linha dentro da função main mostra uma maneira de inicializar um vetor, no
caso do exemplo o vetor chamado vet1, e já preenchê-lo com valores, no mesmo caso com o texto
ola enfermeira (linha 5). Em seguida são declarados mais dois vetores de caracteres: vet1 e
vet2, ambos de tamanho igual a 20. Depois disso as funções da biblioteca padrão string.h são
utilizadas para, respectivamente: imprimir o tamanho da string em vet1, com a função strlen
(linha 9); copiar o conteúdo de vet1 em vet2, com a função strcpy e em seguida imprimı́-los na
tela com a função printf da biblioteca stdio.h (linhas 11 e 12); copiar a string sim senhor para
dentro do vetor vet3 e em seguida imprimir seu conteúdo na tela (linhas 14 e 15); e, finalmente,
comparar o conteúdo das strings contidas em vet1 e vet2 (linhas 17 a 21), e vet1 e vet3 (linhas
23 a 27) com a função strcmp.
Para o uso da função strcpy, a ordem dos parâmetros é de suma importância; o primeiro
parâmetro é o vetor de destino da string que se deseja copiar, enquanto o segundo é o vetor de
origem da string. Em relação a função strcmp, ela retorna o valor zero se as strings armazenadas
em ambos os vetores forem iguais (letras em caixa alta são consideradas diferentes das em caixa
baixa).
5.6.1 Atributos
O Exemplo 5.11 mostra a definição da estrutura tVetorInt. Ela possui um vetor de inteiros
chamado vet, de tamanho definido por TAM e um atributo n do tipo int que representa o
número corrente de elementos em vet, ou seja, o número de elementos armazenados no vetor
num determinado momento da execução do programa. Esse valor deve ser zero quando o vetor
estiver vazio e deve ser incrementado ou decrementado sempre que se adicionar ou excluir um
elemento do vetor, respectiviamente.
A primeira linha do código do Exemplo 5.11 faz com que toda a palavra TAM existente
no código do programa seja substituı́da pelo valor 100 na hora da compilação. Esse artifı́cio é
bastante utilizado quando se trabalha com vetores com tamanho máximo, já que permite que
uma alteração no valor de TAM no inı́cio do código modifique os tamanhos de todos os vetores
do código que possuem TAM como definição de tamanho.
A Figura 5.4 mostra uma maneira simples de representar graficamente a estrutura proposta
no Exemplo 5.11.
A figura mostra duas estruturas do tipo. Como pode ser observado, apesar de vet possuir
tamanho igual a 100, ele possui apenas 9 elementos na primeira estrutura e 5 na segunda, valores
representados pelo atributo n.
5.6.2 Operações
Agora serão ilustradas as funções que devem ser utilizadas como operações para manipular a
estrutura tVetorInt. Tal como visto no capı́tulo anterior, essas operações se dividem em quatro
categorias diferentes: contrutoras, analizadoras, produtoras e modificadoras.
148 CAPÍTULO 5. VETORES
Construtoras
Para o caso do TAD tVetorInt, podem ser criadas duas funções dessa natureza, uma que inicializa
a estrutura com um vetor vazio e outra que a inicializa já lendo para dentro do vetor um
determinado número de elementos ignorando o que havia antes. Para efeito de comparação,
essas duas funções são semelhantes à operação ” = ” utilizada sobre as variáveis dos tipos
primitivos (int, float, char, etc). Uma dessas duas funções deve ser utilizada antes de qualquer
outra operação sobre a estrutura.
A função do Exemplo 5.12 é uma maneira de se implementar uma função que inicializa a
estrutura tVetorInt sem nunhum elemento dentro do vetor.
Repare que a função tem como parâmetro de entrada uma estrutura do tipo tVetorInt, ou
seja, antes de ser utilizada, uma variável do tipo deve ser declarada no corpo principal do
programa, a função main da linguagem C.
O Exemplo 5.13 mostra o código da outra função construtora citada.
A função do Exemplo 5.13 recebe como parâmetro de entrada a estrutura do tipo tVetorInt
que se quer inicializar e o número de elementos que se quer ler para dentro do vetor. Ela, então,
lê do teclado os elementos desejados e armazena-os dentro do vetor da estrutura.
Analisadoras
As operações analizadoras são utilizadas para que se possa obter informações relacionadas com
os valores do TAD. Serão mostradas aqui três funções desse tipo.
A primeira função, que pode ser visualizada no Exemplo 5.14 é para que possam ser visua-
lizados em tela os valores de todos os elementos armazenados no vetor da estrutura.
A função simplesmente acessa todos os elementos do vetor e imprime seus valores na tela
(linha 7).
As outras duas funções citadas como analizadoras são ambas utilizadas para determinar a
existência de um determinado elemento dentro do vetor. Essas funções, além de funcionarem
como uma simples consulta dentro de um vetor, podem ser utilizadas dentro de outras funções
como, por exemplo, para achar um determinado elemento que se queira apagar.
A função do Exemplo 5.15 varre o vetor seqüencialmente à procura de um elemento e retorna
seu ı́ndice se encontrá-lo. Caso contrário, ela retorna -1.
A função do Exemplo 5.15 recebe como parâmetro de entrada a estrutura do tipo tVetorInt
que possui o vetor que se deseja pesquisar e o elemento a ser encontrado. Ela, então, compara,
a partir do primeiro, o valor de todos os elementos com o valor procurado. Se achar um igual,
150 CAPÍTULO 5. VETORES
retorna o valor de seu ı́ncide. Se chegar ao final do vetor e não tiver encontrado nenhum valor
igual ao que se procura, a função retorna o valor -1.
Repare que se existir no vetor dois elementos iguais, a função obtem apenas o ı́ndice do
primeiro. Isso acontece, porque ela retorna quando o primeiro valor é encontrado. Outra carac-
terı́stica dessa função que deve ser evidenciada é sua ineficiência quando o valor procurado está
nas últimas posições ou quando ele não existe no vetor.
Para resolver o problema da ineficiência citada, outras funções foram desenvolvidas para
otimizar o processo de localização de elementos. O Exemplo 5.16 mostra uma delas: a Pesquisa
Binária. Essa função deve ser utilizada apenas em vetores ordenados de forma crescente. Ela
percorre o vetor partindo-o ao meio à procura do elemento e retorna seu ı́ndice se encontrá-lo e
-1 caso contrário.
A função do Exemplo 5.16 recebe como parâmetro de entrada a estrutura do tipo tVetorInt
que possui o vetor que se deseja pesquisar e o elemento a ser encontrado. São declaradas,
então, três variáveis: inicio, fim e meio. A variável inicio recebe, o valor do ı́ndice do menor
elemento do vetor, ou seja, zero, e fim recebe o valor do ı́ndice do maior elemento, ou seja,
v.n − 1. Enquanto o valor de inicio for menor ou igual ao valor de fim, meio recebe o ı́ndice do
elemento central da região por eles delimitada. É exatamente esse elemento central que sempre
será comparado ao elemento que se está procurando. Se aquele for maior que este, fim recebe o
valor do ı́ndice anterior à meio. Caso contrário, inicio recebe o valor do ı́ndice posterior à meio.
Se nenhuma das duas alternativa acontecerem, quer dizer que o elemento foi encontrado e seu
ı́ndice é igual à meio. Se em algum momento inicio passar a ser maior que fim, o elemento não
existe na lista e a função retorna -1.
As Figuras 5.5 e 5.6 mostram o funcionamento do algoritmo do Exemplo 5.16 para duas
pesquisas diferentes.
A Figura 5.5 ilustra as etapas da busca do elemento 4 dentro do vetor vet da estrutura
5.6. TAD IMPLEMENTACIONAL 151
Figura 5.5: Etapas da pesquisa binária com o elemento procurado no inı́cio do vetor.
do tipo tVetorInt mostrada na figura. Primeiramente inicio e fim adquirem os valores 0 e 10,
respectivamente. Como inicio é menor que fim, meio recebe o ı́ndice 5, e o elemento apontado
por ele, o valor 10, é comparado ao valor procurado. Como 10 é maior que 4, fim recebe o ı́ndice
4. Como inicio continua menor que fim, meio recebe o ı́ndice 2. Dessa vez, o elemento apontado
por meio é igual ao elemento procurado. A função retorna, então, o valor de seu ı́ndice: 2.
Já a Figura 5.6 mostra a busca do elemento 23 dentro do mesmo vetor apresentado na Figura
5.5. Primeiramente inicio e fim adquirem os valores 0 e 10, respectivamente. Como inicio é
menor que fim, meio recebe o ı́ndice 5, e o elemento apontado por ele, o valor 10, é comparado
ao valor procurado. Como 10 é menor que 23, inicio recebe o ı́ndice 6. Como inicio continua
menor que fim, meio recebe o ı́ndice 8, e o elemento apontado por ele, o valor 19, é comparado
ao valor procurado. Novamente 19 é menor que 23 e inicio recebe o ı́ndice 9. Como inicio
continua menor que fim, meio recebe o ı́ndice 9 também. Dessa vez, o elemento apontado por
meio é igual ao elemento procurado. A função retorna, então, o valor de seu ı́ndice: 9.
É importante observar que, para esta última busca, o algoritmo de pesquisa binária faz
apenas três comparações enquanto que o algoritmo de pesquisa seqüencial teria de fazer dez.
Produtoras
As funções dessa natureza geram algum tipo de produto como resultado de operações feitas
sobre dois ou mais vetores. Dentre elas podem ser citadas a Adição e o Produto Escalar.
A Adição, Exemplo 5.17, soma cada elemento de mesmo ı́ndice de um vetor com outro
com o mesmo número de elementos correntes e armazena o resultado em um terceiro vetor. Se
os vetores a serem somados não possuirem o mesmo número de elementos correntes, a função
retorna uma mensagem de erro.
152 CAPÍTULO 5. VETORES
Figura 5.6: Etapas da pesquisa binária com o elemento procurado no final do vetor.
A função somaVetorInt recebe como parâmetros de entrada três estruturas do tipo tVetorInt.
Duas delas, v1 e v2, devem possuir os vetores que se deseja somar, e a outra, soma, o vetor onde
a soma será armazenada. Primeiramente verifica-se se os vetores de v1 e v2 possuem o mesmo
número de elementos correntes. Se não possuı́rem, uma mensagem de erro é exibida na tela.
5.6. TAD IMPLEMENTACIONAL 153
Caso contrário, cada elemento de mesmo ı́ndice de v1 e v2 são somados e armazenados com o
mesmo ı́ndice no vetor de soma e este é retornado pela função.
Já a função Produto Escalar, cujo código pode ser encontrado no Exemplo 5.18 calcula, como
o próprio nome já diz, o produto escalar entre dois vetores. Para isso, os vetores devem possuir o
mesmo número de elementos correntes. Caso contrário a função também retorna uma mensagem
de erro.
A função Produto Escalar recebe como parâmetros de entrada duas estruturas tVetorInt:
v1 e v2. Primeiro o número de elementos correntes de seus vetores são verificados. Caso seja
diferente, uma mensagem de erro é exibida na tela. Se forem iguais, o produto escalar de seus
vetores, que vem a ser a soma dos produtos dos elementos de mesmo ı́ndice, são calculados.
Modificadoras
As operações a seguir são responsáveis por realizar alterações no vetor do TAD de forma a
garantir a coerência da estrutura. Para excluir um elemento no meio de um vetor, por exemplo,
a função de exclusão deve, além de apagá-lo, cuidar para que o número de elementos correntes
do vetor seja atualizado e que todos os elementos com ı́ndice maior que o dele sejam transferidos
uma posição para a esquerda. Além da operação de exclusão, serão apresentadas a seguir funções
de inserção e ordenação.
A função do Exemplo 5.19 mostra um algoritmo para inserção de um elemento numa posição
pré-determinada. Ela insere o elemento na posição desejada considerando a posição zero como
a primeira do vetor.
comparando os elementos nele contidos com o valor do elemento a ser excluı́do. Se for igual, ele
move todos os elementos seguintes para a esquerda apagando, assim, o elemento alvo. Depois
disso ele continua a procura por outros elementos a que também devem ser excluı́dos. Quando
chega ao final do vetor, ela retorna a estrutura modificada.
As duas próximas funções a serem apresentadas são de ordenação. Ambas colocam em ordem
crescente os elementos de um vetor, mas utilizando métodos diferentes. A função do Exemplo
5.21 mostra a ordenação pelo método do menor.
A função ordenaMenosInt recebe a estrutura tVetorInt cujo vetor se deseja ordenar. Pri-
meiramente ela varre o vetor procurando o elemento de menor valor e o troca de posição com
o primeiro elemente do vetor. Feito isso, ela procura o menor elemento no restante do vetor e
o troca de posição com o segundo elemento e assim sucessivamente com o terceiro, quarto, . . .,
v.n − 1 elementos. A Figura 5.7 ilustra o funcionamento desse algoritmo.
Já a função ordenaBolhaInt, Exemplo 5.22, utiliza o método da bolha.
11 v . vet [ b ] = temp ;
12 }
13 }
14 }
15 return ( v ) ;
16 }
Será apresentado agora um exemplo de utilização do TAD tVetorInt. Vale lembrar que o Exemplo
5.23 apresenta apenas o programa principal contendo a chamada das funções definidas até aqui.
Para seu funcionamento correto, todas as funções dever estar contidas no arquivo do programa.
1 int main () {
2 tVetorInt vet1 ;
3 tVetorInt vet2 ;
4 tVetorInt vet3 ;
5 int elem ;
6 int pos ;
7 int prodEscalar ;
8 int enter ;
9
10 vet1 = leituraVetorInt ( vet1 ,10) ;
11 vet2 = leituraVetorInt ( vet2 ,10) ;
12 vet3 = in iciaVazioVetorInt ( vet3 ) ;
13
14 printf ( " \ n \ n " ) ;
15 printf ( " Vet1 : " ) ;
16 escritaVetorInt ( vet1 ) ;
17 printf ( " Vet2 : " ) ;
18 escritaVetorInt ( vet2 ) ;
19
20 printf ( " \ n \ nQual elemento deseja localizar utilizando a pesquisa sequencial ?\ n \ n
");
21 scanf ( " % d " ,& elem ) ;
22
23 pos = p e s qu isaSe quen cialI nt ( vet1 , elem ) ;
24 if ( pos >= 0) printf ( " \ n \ nElemento encontrado na posicao % d \ n \ n " , pos ) ;
5.6. TAD IMPLEMENTACIONAL 157
5.8 Resumo
• Um vetor é uma estrutura de dados composta homogênea, isto é, ele possui, em geral,
mais de um elemento de um mesmo tipo.
• Deve-se tomar cuidado para não se acessar um vetor indevidamente, como, por exemplo
utilizando um ı́ndice maior que seu tamanho.
• Quando acessado individualmente, cada elemento de um vetor pode ser encarado como
uma variável básica.
Primeira lista:
abcdjklm
Segunda lista:
efghi
Ordem:
5
Resultado:
abcdefghijklm
160 CAPÍTULO 5. VETORES
M: 5
N: 4
Primeiro vetor:
1 7 13 14 30
Segundo vetor:
2 3 7 16
Resultado:
1 2 3 7 7 13 14 16 30
Obs.: O terceiro vetor não pode ser gerado copiando-se primeiramente os dois vetores e
fazendo a ordenação posteriormente.
7. Faça um programa em linguagem C que leia N (N < 1000) números de matrı́culas de alunos
que fazem PD1 e os coloque em ordem crescente num vetor à medida que vão sendo lidas
as matrı́culas. Posteriormente escreva uma função que identifique se um certo conjunto
de M alunos fazem PD1. Deve-se utilizar o algoritmo de pesquisa binária para fazer esta
verificação.
(a) leia N números reais colocando-os num vetor de 100 elementos (considerar N < 1000);
(b) ordene crescentemente os elementos de ı́ndices ı́mpares utilizando o método da bolha
para tal(considerar apenas os N elementos);
(c) escreva os N números após o ajuste do ı́tem (b);
10. Um armazém trabalha com 100 mercadorias diferentes identificadas pelos números de 1 a
100. O dono anota as quantidades de cada mercadoria vendida no mês. Ele tem uma tabela
que indica para cada mercadoria o preço de venda. Escreva um algoritmo em linguagem
C que calcule o faturamento mensal do armazém, onde:
12. Dado um vetor contendo uma frase com 80 caracteres (incluindo brancos), escreva um
algoritmo em linguagem C para:
13. Faça um algoritmo em linguagem C para ler um vetor de comprimento N (par menor ou
igual a 50) e imprimir seu conteúdo depois de feita a troca dos elementos das posições
pares com os elementos das posições ı́mpares.
14. Faça um programa em linguagem C que, dado um nome terminado por ponto, devolva o
número de vogais nele presentes.
15. Faça um programa em linguagem C que, dada uma frase terminada por ponto, retire todos
os espaços em branco da mesma e retorne a frase modificada.
16. Faça um programa em linguagem C que, dada duas listas de nomes, compare-as e retorne
o número de vezes que cada palavra da segunda lista aparece na primeira lista. Assuma
que cada nome seja composto de no máximo 15 caracteres.
Matrizes
Autores:
Clebson Oliveira
Julio Dalfior
Objetivos:
6.1 Introdução
Uma matriz é um tipo de dado composto homogêneo na qual seus elementos são organizados
em uma estrutura multidimensional. Por exemplo, uma matriz bidimensional é formada pela
combinação de linhas horizontais e verticais. A figura 6.1 mostra uma matriz 5 por 3. Note que
a matriz possui 5 linhas e 3 colunas. Embora matrizes possam ter mais que 2 dimensões, neste
livro serão abordadas apenas matrizes bidimensionais. Uma matriz com m linhas e n colunas é
chamada matriz m por n.
Para mostrar que matrizes bidimensionais são muito comuns no nosso dia-a-dia podem-
se citar vários exemplos: um cartão de bingo, uma agenda de compromissos (6.2), etc. É
interessante que haja uma maneira de representar esse tipo de estrutura, para que se possa
utilizá-la na solução de problemas.
Na figura 6.2 tem-se que as linhas representam os dias do mês, as colunas representam os
meses do ano e os elementos da matriz representam um determinado dia do mês.
Considerando que essa matriz será utilizada para representar se há ou não compromisso
naquele dia do mês, temos uma matriz bidimensional porque a cada célula dessa matriz estão
164
6.2. DEFINIÇÃO E ACESSO 165
associadas duas informações (mês e dia). Por outro lado, se for necessário listar varios compro-
missos para cada dia do mês ter-se-á uma matriz tridimensional, porque a cada célula da matriz
estarão associadas três informações (mês, dia e hora).
É fato que a visualização das matrizes utilizadas facilita o entendimento deste conceito. Por
exemplo, uma matriz bidimensional pode ser visualizada como um quadro e uma tridimensional
pode ser visualizada como um cubo formado por vários cubos menores. Apesar de ser difı́cil
visualizar matrizes com mais de três dimensões, vale ressaltar que elas existem e que é possı́vel
declará-las em C. Como exemplo da utilização de matrizes com mais de três dimensões pode-se
utilizar o exemplo anterior supondo que se queira representar também o ano. Então, teremos
uma matriz de quatro dimensões (ano, mês, dia e hora).
Para que um problema seja modelado através de uma matriz é necessário que se defina uma
variável para representá-la, para que assim se possa utilizar seus elementos para guardar in-
formações e posteriormente aplicar operações sobre essa estrutura.
166 CAPÍTULO 6. MATRIZES
6.2.1 Definição
Para definir uma variável do tipo matriz é necessário que se saiba o tamanho de cada dimensão.
No entanto, existem duas maneiras de estabelecer o tamanho dessas dimensões: estaticamente
e dinamicamente.
Estaticamente
No momento em que se define a matriz é necessário estabelecer um tamanho para cada dimensão,
sendo que esse tamanho não poderá ser alterado durante a execução do programa. O código do
exemplo 6.1 mostra como isso pode ser feito em C:
O número entre o primeiro par de colchetes ([5]) define o tamanho da primeira dimensão
(número de linhas). O número entre o segundo par de colchetes ([10]) define o tamanho da
segunda dimensão (número de colunas), e assim por diante. No caso acima a variável matriz
possui 5 linhas e 10 colunas, como se pode ver na figura 6.3. Os traços na matriz representam a
informação armazenada na célula, a qual é o compartimento onde estão localizados os elementos
da matriz.
Inicialização
Vale ressaltar que na declaração de matrizes mostrada anteriormente, os valores das células da
matriz não foram inicializados e isso pode gerar resultados inesperados. Uma vez que não se
sabe quais valores estavam armazenados anteriormente nas áreas de memória utilizadas. Para
evitar esse tipo de problema é uma boa prática de programação inicializar os elementos das
matrizes antes de tentar acessar seus valores, mesmo que posteriormente esses elementos sejam
configurados com outros valores. A forma mais simples de se fazer isso, em C, pode ser vista no
exemplo 6.2.
6.2. DEFINIÇÃO E ACESSO 167
1 int main () {
2
3 int i , j ;
4 int matriz [2][2] = {0};
5
6 return 0;
7 }
Na linha 4 do exemplo 6.2 temos a inicialização de todos os elementos da matriz com o valor
zero.
6.2.2 Acesso
Agora que a matriz já foi inicializada, para se utilizar os dados contidos nela será necessário
fazer uma operação de acesso, como no exemplo 6.3:
1 # define NMESES 12
2 # define NDIAS 31
3 # define NHORAS 24
4
5 main () {
6
7 int reservaSala [ NMESES ][ NDIAS ][ NHORAS ] = {0};
8 int resposta =0;
9 .
10 .
11 .
12 resposta = reservaSala [5][1][10];
13
14 if ( resposta ==0) {
15
16 reservaSala [5][1][10]=1;
17 }
18
19 }
6.3.1 Atributos
O primeiro passo para definir o TAD tMatriz envolve escolher os seus atributos. Como a idéia
é modelar o problema por uma matriz, é necessário que um dos atributos seja a própria matriz
que armazenará os elementos. Além disso é necessário guardar a quantidade de elementos de
cada dimensão.
Apesar de ser possı́vel definir uma matriz com um número qualquer de dimensões e que
armazene um tipo qualquer de dado (int, float, char, etc), neste capı́tulo serão utilizadas apenas
matrizes bidimensionais de inteiros, por serem comumente utilizadas.
Portanto, os atributos de uma matriz bidimensional de inteiros são:
No exemplo 6.4 pode ser vista uma maneira de se definir o TAD tMatrizInt, de forma a
representar matrizes bidimensionais de inteiros.
1 # define MAX_DIM 5
2
3 typedef struct {
4
5 int valores [ MAX_DIM ][ MAX_DIM ];
6 int nLinhas ;
7 int nColunas ;
8 } tMatrizInt ;
No exemplo 6.4 a matriz valores é declarada estaticamente, sendo seu tamanho definido pela
constante MAX DIM. Como os próprios nomes evidenciam, nLinhas e nColunas são variáveis
inteiras que representam o número de linhas e colunas, respectivamente, da matriz.
Na figura 6.4 pode-se visualizar a estrutura da matriz dados de uma variável do tipo tMa-
trizInt inicializada como no exemplo abaixo:
1 tMatrizInt dados ;
6.3. O TAD IMPLEMENTACIONAL TMATRIZ 169
2 int i , j ;
3
4 dados . nLinhas = 3;
5 dados . nColunas = 3;
6
7 for ( i =0; i < dados . nLinhas ; i ++) {
8 for ( j =0; j < dados . nColunas ; j ++) {
9 dados . valores [ i ][ j ]=0;
10 }
11 }
Nas linhas 4 e 5 o tamanho da matriz é definido como 3 por 3. Na linha 7, o comando for
percorre as linhas da matriz de ı́ndice zero a nLinhas-1. O segundo comando for percorre todas
as colunas (para cada laço do for anterior) de ı́ndice zero a nColunas-1. Dessa forma percorrem-
se todos os elementos da matriz valores, dentro das dimensões passadas, atribuindo-se a eles o
valor zero.
A figura 6.4 representa a matriz valores da estrutura dados do exemplo 6.5.
Figura 6.4: Estrutura da matriz valores pertencente à variável dados (para MAX DIM igual a
5)
Apesar da matriz valores pertencente à variável dados ter 5 linhas e 5 colunas, somente os
elementos pertencentes às 3 primeiras linhas e colunas foram inicializados. Isso porque definiu-se,
nas linhas 4 e 5, que a matriz deveria ter somente esse número de linhas e colunas. Dessa forma,
em todas as operações efetuadas sobre essa variável somente os elementos pertencetes a essa
submatriz (formada pelos zeros na figura) devem ser considerados. Todos os outros (marcados
com sinal ’-’) possuem valores não determinados, por não terem sido inicializados, e devem ser
ignorados.
6.3.2 Operações
Uma das vantagens de se implementar um tipo abstrato de dado é facilitar a solução de pro-
blemas, através do uso de operações que atuem sobre o tipo abstrato de dado definido. Dessa
170 CAPÍTULO 6. MATRIZES
Inicialização
Uma operação essencial para se realizar sobre tMatrizInt é a inicialização. Ela deve ser a primeira
a ser realizada, caso contrário as demais podem gerar resultados inesperados (como término do
programa, por exemplo). Isso porque, durante a inicialização, preparam-se as estruturas internas
do TAD para se adequar ao caso desejado. No caso do tipo tMatrizInt proposto nesse capı́tulo,
a operação de inicialização definirá as dimensões iniciais da matriz.
O código do exemplo 6.6 mostra como isso pode ser implementado:
Na definição da função Inicializa (linha 1), os argumentos nLinhas e nColunas servem para
definir a quantidade de linhas e colunas da matriz respectivamente, e esses valores são atribuı́dos
à estrutura tMatriz nas linhas 6 e 7. Finalmente, na linha 16 a estrutura é retornada através da
variável resultado.
Atualização de Valor
Como não é interessante que uma matriz apresente todos os elementos com os mesmos valores,
é necessário definir uma forma de atualizá-los individualmente. Isso pode ser feito através da
operação de escrita definida pela função do exemplo 6.7.
1 tMatrizInt atualizaMatriz ( tMatrizInt matriz , int linha , int coluna , int valor ) {
2
3 if ( ( linha >=0 && linha < matriz . nLinhas ) && ( coluna >=0 && coluna < matriz .
nColunas ) ) {
4 matriz . valores [ linha ][ coluna ] = valor ;
5 }
6
7 return matriz ;
8 }
6.3. O TAD IMPLEMENTACIONAL TMATRIZ 171
Na linha 3 é verificado se o elemento pertence à matriz(se o números de sua linha e de sua coluna
estão dentro das dimensões da matriz). Caso tudo esteja correto, o valor passado é atribuı́do ao
elemento desejado e a função retorna o número 0, informando que a operação foi realizada com
sucesso. Caso contrário a função retorna o número -1 para informar que a escrita não pôde ser
efetuada.
Acesso a valor
Tendo em vista a importância de utilizar o valor de um determinado ı́tem, é necessário construir
uma função para acessar os dados da matriz (exemplo 6.8).
Exibição
Para poder visualizar todos os elemetos de uma matriz de inteiros é necessário definir uma
operação de exibição (ou impressão) da matriz na tela (exemplo 6.9).
No exemplo 6.9 tem-se, na linha 5, um comando for para varrer as linhas da matriz, e para
cada linha tem-se outro comando for, na linha 6, para varrer os elementos dessa linha. Na linha
7, o trecho “%5d ” faz com que os números sejam exibidos sempre com 5 dı́gitos, o que faz com
que a forma de exibição seja regular, como pode ser visto na figura 6.5.
A variável soma é inicializada com o valor zero. O for da linha 5 percorre todos os elementos
da linha da matriz e, para cada laço, adiciona o valor da célula da matriz a soma. A variável
soma funciona, então, como um acumulador e, ao final dos laços, armazena o valor da soma dos
elementos da linha.
6.3. O TAD IMPLEMENTACIONAL TMATRIZ 173
A variável soma é inicializada com o valor zero. O for da linha 5 percorre todos os elementos
da coluna da matriz e, para cada laço, adiciona o valor da célula da matriz a soma. A variável
soma funciona, então, como um acumulador e, ao final dos laços, armazena o valor da soma dos
elementos da coluna.
Produto Escalar
Agora, será definida uma operação de composição chamada produto escalar, que gera uma matriz
M de cujos elementos são resultados de uma composição dos elementos de A e B 1 (M=A.B).
Nesta operação os elementos mij da matriz M são resultantes do somatório dos produtos entre
os elementos das linhas i da primeira matriz, e os elementos das colunas j da segunda matriz,
para que esta operação seja executada é necessário que o número de colunas da matriz A seja
igual ao número de linhas da matriz B.
15
16 produto . valores [ i ][ j ] += matriz1 . valores [ i ][ k ] *
17 matriz2 . valores [ k ][ j ];
18 }
19 }
20 }
21
22 produto . nLinhas = matriz1 . nLinhas ;
23 produto . nColunas = matriz2 . nColunas ;
24 } else {
25
26 produto . nLinhas = produto . nColunas =0;
27 }
28
29 return produto ;
30 }
Solução Possı́vel:
Para se armazenar as informações de reserva da sala será utilizada uma matriz com 31 linhas
(representando os dias) e 12 colunas (representado os meses). Dessa forma, para se reservar a
sala para o dia 1o de janeiro, atribui-se ao elemento da linha 0 e coluna 0 o valor 1.
Para resolver o problema será criada uma função para cada operação.
1. Reservar a sala:
A primeira coisa a ser feita é verificar a reserva da sala para a data informada. Se a sala
estiver livre será reservada, uma mensagem de sucesso impressa na tela e a matriz alterada
será retornada. Caso contrário, a matriz é retornada inaltarada e uma mensagem de erro
impressa na tela.
6.4. EXERCÍCIOS RESOLVIDOS 175
2. Cancelar a reserva:
Novamente, deve-se verificar a reserva da sala para a data informada. Se a sala estiver
reservada a reserva será cancelada, uma mensagem de sucesso impressa na tela e a matriz
alterada será retornada. Caso contrário, a matriz é retornada inaltarada e uma mensagem
de erro impressa na tela.
Uma vez definidas as funções que realizam as operações, falta então definir a função principal
do programa onde a interação com o usuário será feita e as funções serão chamadas.
1 int main ()
2 {
3 tMatrizInt reservas ;
4 int codigo , dia , mes ;
5
6 reservas = inicializa (31 , 12) ;
7
8 for ( mes =0; mes <12; mes ++) {
9 for ( dia =0; dia <31; dia ++)
10 reservas . valores [ dia ][ mes ] = 0;
11 }
6.4. EXERCÍCIOS RESOLVIDOS 177
12
13 do {
14 printf ( " \\ n \\ n \\ n \\ n \\ n \\ n \\ n " ) ;
15 printf ( " ## Reserva de Salas ##\\ n \\ nEscolha uma operaç~ a o :\\ n " ) ;
16 printf ( " 1 - Reservar a sala \\ n " ) ;
17 printf ( " 2 - Cancelar uma reserva \\ n " ) ;
18 printf ( " 3 - Listar dias livres \\ n " ) ;
19 printf ( " 4 - Exibir ı́ndice de ocupaç~ a o \\ n " ) ;
20 printf ( " 5 - Sair \\ n " ) ;
21 printf ( " Digite o codido da opecaç~ a o escolhida : " ) ;
22 scanf ( " % d " , & codigo ) ;
23
24 switch ( codigo ) {
25 case 1:
26 printf ( " \\ n \\ nDigite a data da reserva ( dia / mes ) : " ) ;
27 scanf ( " % d /% d " , & dia , & mes ) ;
28 reservas = reservaSala ( reservas , dia , mes ) ;
29 break ;
30
31 case 2:
32 printf ( " \\ n \\ nDigite a data da reserva a cancelar ( dia / mes ) : " ) ;
33 scanf ( " % d /% d " , & dia , & mes ) ;
34 reservas = cancelaReserva ( reservas , dia , mes ) ;
35 break ;
36
37 case 3:
38 printf ( " \\ n \\ nDigite o mes : " ) ;
39 scanf ( " % d " , & mes ) ;
40 listaDiasLivres ( reservas , mes ) ;
41 break ;
42
43 case 4:
44 imp rime Indic eOcu pacao ( reservas ) ;
45 break ;
46 }
47 } while ( codigo != 5) ;
48
49 return 0;
50
51 }
Dada uma matriz bidimensional de inteiros, defina operações que realizem os seguintes
cálculos:
• A soma dos elementos da diagonal principal
• A soma dos elementos da submatriz triangular
178 CAPÍTULO 6. MATRIZES
Solução Possı́vel:
Para calcular a soma dos elementos da diagonal principal de uma matriz M[i,j] é necessário
notar que todos os elementos da diagonal principal apresentam ı́ndices referentes a coluna
e à linha iguais (M[i,i]). Assim, varrer-se a diagonal principal da matriz através de um
loop, na linha 6, onde a cada laço soma-se o valor do elemento M[i,i] à variável soma e
incrementa-se o valor do ı́ndice i de uma unidade.
Como os elementos da matriz triangular superior são os elementos que estão situados acima
dos elementos da diagonal principal, pode-se notar que para cada linha esses elementos
estão situados nas colunas à direita do elemento da diagonal principal. Dessa forma, varre-
se a matriz triangular superior através de um loop, na linha 6, que varre todas as linhas i
da matriz, e para cada linha tem-se outro loop, linha 7, para varrer as colunas de ı́ndice
i+1 até nColunas. Então, soma-se o valor de cada elemento visitado à variável soma,
linha 8.
10 }
11
12 return soma ;
13 }
Dada uma matriz bidimensional nxn de ‘’zeros” e ‘’uns”, defina uma operação para calcular
o número de uns isolados dessa matriz. Considere que um número um está isolado se nenhum
dos elementos adjacentes a ele, apenas na horizontal e vertical, são ‘’uns”.
Solução Possı́vel:
Para cada célula que contenha o valor um, será necessário checar no máximo quatro células
adjacentes a ela para saber se a mesma contém um número um isolado. Os loops das linhas 5 e
7 servem para varrer a matriz completamente, e a cada célula visitada checa-se se suas células
adjacentes são zeros, dependendo de quantas ela possa ter. Assim, se o elemento visitado estiver
na primeira linha (linha 9) ele pode ser: o primeiro da linha (11), e então só terá adjacentes
abaixo e à direita (12); o último da linha (linha 16), e então só terá adjacentes abaixo e à esquerda
(17); ou caso não seja nem o primeiro e nem o último, terá adjacentes abaixo, à esquerda e à
direita. Caso o elemento esteja na última linha (linha 27) ele pode ser: o primeiro da linha
(linha 29), e então só terá adjacentes acima e à direita (linha 30); o último da linha (linha 34),
e então só terá adjacentes acima e à esquerda (linha 35); ou caso não seja nem o primeiro e nem
o último terá adjacentes acima, à esquerda e à direita (linha 39). Após checar se o elemento
visitado está na primeira ou na última linha da matriz, temos o caso dele estar na primeira
ou na última coluna, desconsiderando os elementos dessas colunas que estejam na primeira ou
na última linha. Se ele estiver na primeira coluna (linha 45) terá adjacentes acima, abaixo e à
direita (linha 46); e se ele estiver na última coluna (linha 50), terá adjacentes acima, abaixo e à
esquerda (linha 51). Finalmente, se o elemento visitado não estiver na primeira linha, na última
linha, na primeira coluna e nem na última coluna (linha 53); ele terá adjacentes acima, abaixo,
à esquerda e à direita (linha 55).
6.5 Resumo
• Uma matriz é um tipo de dado composto homogêneo na qual seus elementos são organi-
zados em uma estrutura multidimensional.
• É necessário inicializar a matriz para evitar situações de acesso a elementos que contenham
valores indeterminados.
• O TAD tMatriz é composto pelos seguintes atributos: número de linhas da matriz, número
de colunas da matriz e a estrutura que armazenará os elementos.
• Quando se tenta acessar um elemento com posição não definida na matriz, é necessário
retornar uma mensagem de erro para indicar que ocorreu um erro na execução do programa.
2. Altere o subprograma anterior para exibir na tela todas as posições da matriz em que se
encontra tal valor máximo.
3. Faça um programa que calcule a matriz resultante da soma de duas matrizes de dimensões
m linhas e n colunas, com valores inteiros.
182 CAPÍTULO 6. MATRIZES
4. Escreva um subprograma que calcule a transposta de uma dada matriz. Considere como
matriz trasposta At de A a matriz de cujos elementos At [i,j] são iguais a os elementos
A[j][i] para 1<=i<=m e 1<=j<=n, onde m representa o número de linhas e n o número
de colunas da matriz A.
5. Escreva uma função que verifica se uma matriz nxn é simétrica. Uma matriz A é simétrica
se A[i,j] = A[j,i] para todo 1<=i,j<=n.
6. Faça um subprograma que calcule a soma dos termos que se situam na diagonal secundária
ou abaixo dela numa matriz quadrada com elementos inteiros. Assuma que o número
máximo de linhas e colunas é 100, mas que a matriz pode estar preenchida apenas parci-
almente.
Observação: Antes de escrever o subprograma, você deve apresentar a declaração do tipo
da matriz que deve ser colocada nos programas que usarão este subprograma.
1 2 3
4 5 6
7 8 9
antes
1 4 7
2 5 8
3 6 9
depois
8. Uma matriz quadrada inteira é chamada de ‘’quadrado mágico” se a soma dos elementos
de cada linha, a soma dos elementos de cada coluna e a soma dos elementos das diagonais
principal e secundária são todos iguais.
Exemplo de um quadrado mágico:
8 0 7
4 5 6
3 10 2
6.6. EXERCÍCIOS PROPOSTOS 183
Escreva uma função que verifica se uma matriz de n linhas e n colunas representa um
quadrado mágico.
9. Um quadrado latim de ordem n contém os números de 1 até n de forma que nenhum número
é repetido na mesma linha ou coluna. Este quadrado pode ser usado, por exemplo, em
alguns torneios esportivos para assegurar que cada equipe joga com todas outras equipes
uma e somente uma vez. Escreva uma função booleana que recebe uma matriz quadrada
e checa se ele é realmente um quadrado latim.
Exemplo de quadrado latim:
1 2 3 4
2 3 4 1
3 4 1 2
4 1 2 3
(número de
10. Considere 2 matrizes quadradas do tipo tMatrizInt. Faça um subprograma para cada item
abaixo (note que as matrizes devem obrigatoriamente ser passadas como parâmetros aos
subprogramas):
12. Considere uma matriz de inteiros. Define-se como vizinhança de uma posição da matriz a
soma de todos os números que estejam em posições adjacentes. Escreva um programa que
determine a posição do elemento de maior vizinhança.
a)Exibir uma matriz com as caracterı́sticas definidas acima (considere que a dimensão
N, isto é, seu número de linhas ou colunas, da matriz será lida no programa principal e
passada como parâmetro para o subprograma);
b)determinar se as duas matrizes são idênticas;
c)determinar se a segunda matriz é uma rotação de 90 graus à direita sobre a primeira;
184 CAPÍTULO 6. MATRIZES
d)determinar se a segunda matriz é uma rotação de 180 graus à direita sobre a primeira;
e)determinar se a segunda matriz é a imagem espelhada vertical da primeira;
f)determinar se a segunda matriz é a imagem espelhada horizontal da primeira;
As figuras abaixo ilustram cada tipo de matriz com um exemplo de dimensão
3 x 3:
1 2 3
4 5 6
7 8 9
original
1 2 3
4 5 6
7 8 9
idêntica
7 4 1
8 5 2
9 6 3
90◦ à direita
9 8 7
6 5 4
3 2 1
180◦ à direita
13. Escreva ainda um programa que leia 2 matrizes e determine as relações existentes entre
elas utilizando os subprogramas do exercı́cio anterior.
14. Escreva uma função que receba uma matriz preenchida com caracteres maiúsculos do
tipotMatrizChar e um vetor do tipo tVetor contendo uma palavra em letras maiúsculas
e retorne o número de ocorrências da palavra na matriz. A palavra pode estar escrita
na matriz de cima para baixo, da esquerda para a direita ou nas diagonais paralelas à
diagonal principal ou na própria.
6.7. TRABALHOS SUGERIDOS 185
F L O R E S T A R S
H J V N O A C I W Z
C A P E L A P I S C
I K V D I P M B S F
T Y G C S A L U O P
Q L K Ç A D U O L Ç
A A S A R P R Y O A
N E N E F T E B A U
J K D D S H W L F L
I O T U G J J V A A
Arquivo de saı́da:
Lista de palavras encontradas em ordem alfabética:
186 CAPÍTULO 6. MATRIZES
AULA
CAPELA
FLORESTA
LAPIS
Lista de palavras não encontradas em ordem alfabética:
ALEGRIA
MANGA
Lista de palavras encontradas, ordenadas pelo número de ocorrências na matriz:
CAPELA 2 (2, 0) horizontal (4, 3) diagonal
AULA 1 (6,9) vertical
FLORESTA 1 (0, 0) horizontal
LAPIS 1 (2,4) horizontal
Direção em que mais palavras foram encontradas: horizontal
Número de palavras encontradas nesta direção: 3
Nesse exemplo a matriz só será definida após a execução da função scanf, onde o tamanho
de suas dimensões será lido do teclado.
Capı́tulo 7
Apontadores
Autores:
Objetivos:
Neste capı́tulo procurou-se dar ênfase a aspectos relevantes, de carácter introdutório, para
que este texto seja utilizado como primeira leitura, e posterior consulta, sobre apontadores.
187
188 CAPÍTULO 7. APONTADORES
No lugar do tipo do apontador, devem-se utilizar alguns dos tipos padrões da linguagem C,
como char, int e float, ou até mesmo novos tipos definidos pelo programador. No Exemplo 7.1,
tem-se como criar apontadores dos tipos char, int, float e tAluno.
1 typedef struct {
2 char nome [30];
3 int matricula ;
4 } tAluno ;
5
6 main ( ) {
7 char * c ;
8 int * p ;
9 float * f ;
10 tAluno * x ;
11 }
190 CAPÍTULO 7. APONTADORES
Assim, no código do Exemplo 7.1, c pode apontar para uma área de memória que guarda
uma variável do tipo caracter, enquanto p, f e x dos tipos inteiro, ponto flutuante e tAluno,
respectivamente.
Dois outros operadores também estão associados à sintaxe de apontadores: o operador en-
dereço de memória e o operador seta. Eles serão discutidos nas duas próximas subseções.
1 int * p ;
2 int d = 10;
3 p = &d;
4 printf ( " % p \ n % p \ n " , &d , p ) ;
1 typedef struct {
2 char nome [30];
3 int matricula ;
4 } tAluno ;
5
6 main ( ) {
7.3. ACESSO À VARIÁVEL POR MEIO DE APONTADORES 191
7 tAluno * x ;
8 tAluno y ;
9
10 x = &y;
11 x - > matricula = 2031;
12
13 printf ( " % d \ n " , y . matricula ) ;
14 }
Exemplo 7.3: Acesso ao atributo de uma estrutura por meio do operador seta
Note que no Exemplo 7.3, x é um apontador para uma estrutura tAluno e y uma variável
tAluno. Após fazer x apontar para o endereço de y, para acessar a matrı́cula de y através de x,
deve ser utilizado o operador − >. Já para imprimir o atual valor da matricula de y, que passou
a ser 2031, basta utilizar o ponto.
1 int *p;
2 int c = 10;
3 int d;
4 p = &c;
5 d = *p;
1 float * p ;
2 float c = 15;
3 p = &c;
4 * p = * p + 1;
5 printf ( " % f " , c ) ;
Pode ser observado no Exemplo 7.5, que p aponta para c. Assim, a expressão na linha 4
incrementa o conteúdo da variável apontada por p em uma unidade, ou seja, c passa a ter o
valor 16, o qual é impresso na tela.
Uma variável pode receber o valor de outra por meio de apontadores. Na linha 7 do Exemplo
7.6, a variável i apontada por v tem seu valor alterado para o de d, pois d está apontado por j.
1 float * v ;
2 float * j ;
3 float d = 15.0;
4 float i = 17.0;
5 j = &d;
6 v = &i;
7 *v = *j;
8 v = j;
Um apontador também pode ser atribuı́do a outro apontador. No Exemplo 7.6, linha 8, v
passa a apontar para o mesmo endereço que j aponta.
Por fim, esse acesso a variáveis através de apontadores é muito importante, pois é por
meio deste recurso que as variáveis passadas como parâmetros de uma função são alteradas
definitivamente, dentro da própia função.
7 }
8
9 main ( ) {
10 int a = 0;
11
12 adicionaX1 (10 , a ) ;
13 printf ( " a = %d\n", a);
14 adicionaX2 (10 , & a ) ;
15 printf ( " a = %d\n", a);
16 }
Na Figura 7.5, repare que na passagem por cópia, o valor de a é copiado para o conteúdo de
b, enquanto que na passagem por apontador, b passa a apontar para o endereço de a.
Uma aplicação da passagem de valor por apontadores é proporcionar que uma função retorne
múltiplos valores. O Exemplo 7.8 mostra como isso é feito.
3
4 aux = * x ;
5 *x = *y;
6 * y = aux ;
7 }
8
9 main ( ) {
10 int a, b;
11 a = 10;
12 b = 20;
13
14 troca (& a , & b ) ;
15 printf ( " a = %d , b = % d \ n " , a , b ) ;
16 }
No Exemplo 7.8, desejava-se uma função para trocar o conteúdo de a e b. Contudo, uma
função pode retornar apenas um valor no return. Assim, por meio dos apontadores, os dois
conteúdos são alterados e retornados no escopo da função, pois a e b são passados sem cópia,
ou seja, são passados seus endereços como parâmetros da função troca.
que era livre, passa a ser apontado por uma variável apontador, podendo assim ser utilizado, o
que é ilustrado na Figura 7.7.
1 int * p
2 p = ( int *) malloc (4*( sizeof ( int ) ) ) ;
No Exemplo 7.9, a expressão sizeof retorna o número de bytes de um tipo passado como
parâmetro, no caso int. Além disso, como a função malloc devolve um apontador do tipo void
(void *) para um bloco de bytes consecutivos, esse apontador deve ser convertido em apontador
para o tipo desejado, por meio do cast (int *) nesse caso, uma vez que a variável p é do tipo int,
garantindo assim, a consistência de tipos.
A Figura 7.8 ilustra um espaço de 4 inteiros, alocados dinâmicamente pelo Exemplo 7.9.
Um espaço de memória deve ser desalocado quando não é mais necessário. Isto significa
que a área de memória que foi apontada por um apontador agora passa a ser novamente livre.
Somente variáveis alocadas dinamicamente podem ser desalocadas em tempo de execução.
A desalocação de memória, em C, está no Exemplo 7.10.
1 int * p ;
2 p = ( int *) malloc (4 * sizeof ( int ) ) ;
3 free ( p ) ;
No Exemplo 7.10, a função free, assim como malloc, está na biblioteca stdlib.h e desaloca
uma área de memória. Note que a função free deve receber como parâmetro uma variável
196 CAPÍTULO 7. APONTADORES
apontador. Assim, o área de memória apontada por ele será então liberada. A Figura 7.9 ilustra
a desalocação de memória.
Para contornar o problema de perda de memória com espaços não utilizados, faz-se uso da
alocação dinâmica. A cada necessidade de se ler uma linha com certo número de caracteres,
aloca-se o exato tamanho da linha, ou seja, a quantidade de caracteres da string que será lida.
O Exemplo 7.11 mostra a implementação em linguagem C de uma matriz de caracteres,
dinamicamente alocada. Esta matriz possui 4 linhas de tamanhos alocados de acordo com o
tamanho da string armazenada, e cada linha da matriz recebe uma linha do texto da Figura
7.10. A matriz declarada no Exemplo 7.11 é simplesmente um vetor de apontadores para strings
e os elementos do vetor g contém os endereços dos primeiros elementos de cada string. Esse
vetor de apontadores para strings é chamado matriz de apontadores ou matriz de strings. Na
Figura 7.11 está ilustrada a matriz de strings correspondente ao texto.
1 main ( ) {
2 char * g [4];
3 g [0] = ( char *) malloc ( strlen ( " Joao ama Maria " ) * sizeof ( char ) ) ;
4 strcpy ( g [0] , " Joao ama Maria " ) ;
5 g [1] = ( char *) malloc ( strlen ( " Maria ama pedro " ) * sizeof ( char ) ) ;
6 strcpy ( g [1] , " Maria ama pedro " ) ;
7 g [2] = ( char *) malloc ( strlen ( " Ana ama quem ama Maria " ) * sizeof ( char ) ) ;
8 strcpy ( g [2] , " Ana ama quem ama Maria " ) ;
9 g [3] = ( char *) malloc ( strlen ( " Quem Ana ama ? " ) * sizeof ( char ) ) ;
10 strcpy ( g [3] , " Quem Ana ama ? " ) ;
198 CAPÍTULO 7. APONTADORES
11 }
1 float *p , h = 15.0;
2 *p = h;
1 int * p ;
2 int * h ;
3 h = ( int *) malloc ( sizeof ( int ) ) ;
200 CAPÍTULO 7. APONTADORES
4 p = h;
5 free ( h ) ;
1 typedef struct {
2 int numerosala ;
3 int numeroalunos ;
4 } tsala ;
5
6 acresentaAluno ( tsala * g ) {
7 g - > numeroalunos = g - > numeroalunos + 1;
8 }
9
10 main ( ) {
11 int p ;
12 tsala sala ;
13 sala . numerosala = 1;
14 sala . numeroalunos = 0;
15 tsala * x ;
16 tsala * y ;
17
18 y = & sala ;
19 x = y;
20 acrescentaAluno ( x ) ;
21 acrescentaAluno ( y ) ;
22 }
No Exemplo 7.15, o número de alunos da variável sala é alterado duas vezes, uma pelo
apontador x, outra por meio do apontador y. Portanto, quando apontadores são usados de uma
maneira indiscriminada, pode-se diminuir a legibilidade do código.
indica a quantidade atual destes na lista. Cada um dos nós, por sua vez, possuem informações
armazenadas, um apontador para o próximo nó e no caso de uma lista duplamente encadeada,
também um apontador para o nó anterior. Na Figura 7.14, tem-se um esquema de uma lista
simplesmente encadeada, com três nós e com apontadores para o primeiro e o último elemento.
Note que cada um dos nós possui um apontador para o próximo, com exceção do último, no
qual há um apontador para null, pois não há próximo. Repare também que a estrutura da lista
possui apontadores para o inı́cio e o fim da lista, além de armazenar seu tamanho corrente, no
caso, três elementos.
Pode-se perceber que incluir em uma lista encadeada torna-se um processo simples. Se um
novo dado necessita ser incluı́do, basta alocar um espaço de memória para o nó, atualizar os
apontadores da seqüência de nós e a quantidade deles na lista. Excluir um dado da lista é um
processo semelhante, com a diferença de que, ao invés de se alocar, desaloca-se uma área da
memória.
Vale destacar que as estruturas da lista e de cada um dos dados armazenados em um nó
são exemplos do uso de abstração, pois se encapsulam informações, seleciona-se o que deve
ser apresentado de uma função à outra e possibilita-se o trabalho em nı́veis de implementação
e uso, prática fundamental na modularização de um código. Conforme visto no capı́tulo 4,
pode-se dizer que o tipo de abstração utilizada na lista encadeada é uma abstração de dados
implementacional, por meio do uso de Tipo Abstratos de Dados - TADs.
Anteriormente foi visto que TADs são conjuntos de valores com comportamento uniforme
definido por operações, que são tipicamente os componentes responsáveis pela interface entre
os diferentes nı́veis e módulos de implementação. Essa interface é responsável por encapsular e
proteger os dados.
A implementação do TAD lista encadeada tLista será apresentada a seguir. Também serão
detalhadas várias operações fundamentais sobre a lista. Inicialmente, será definido o tipo tNo,
o qual é um componente importante de tLista.
202 CAPÍTULO 7. APONTADORES
1 typedef struct {
2 tNo * primeiro ;
3 tNo * marcador ;
4 tNo * ultimo ;
5 int tam ;
6 } tLista ;
Repare que três atributos de tLista são apontadores para o tipo tNo, o qual foi definido na
seção anterior. Dois deles, primeiro e ultimo, apontam para o começo e para o fim da lista,
enquanto marcador aponta para qualquer posição. A variável inteira tam é a responsável por
guardar o atual tamanho, ou número de nós, da lista.
O apontador marcador é responsável por apontar o nó corrente num processo de busca
seqüencial. Por exemplo, na operação de imprimir a lista, que será explicada mais adiante,
a informação é extraı́da do nó apontado por marcador e, este vai sendo posicionado desde o
primeiro nó até o último.
Operações Construtoras
As operações construtoras são responsáveis por garantir a alocação da estrutura tLista, além de
inicializá-la com os valores desejados, geralmente com os apontadores apontando para NULL e
as variáveis numéricas com valor zero.
Inicialização (vazia)
indevida. Em seguida, o tamanho da lista é zerado uma vez que o atual tamanho da lista é nulo.
Operações Analisadoras
Em algumas situações é importante analisar a posição atual do marcador, tal como se ele está
no fim da lista, e se a lista está vazia. Por exemplo, ao se percorrer a lista, uma condição de
parada importante é quando o final da lista for alcançado e, ao se incluir ou excluir um elemento
da lista, é necessário saber se a lista está vazia.
Essas análises são importantes para que não se cometa acessos indevidos com os apontadores,
um dos problemas explicados neste capitulo.
Final
Já na Figura 7.16 o marcador aponta para NULL, ou seja para o fim da lista. Assim, o
retorno da função é 1.
Vazia
Assim como na análise do fim da lista, vaziaLista retorna o resultado de uma comparação.
Nesse caso, porém, se o tamanho da lista é igual zero. Como o tamanho da lista é inicializado
com este valor e, sempre que um novo nó for adicionado, ou removido, este tamanho deve ser
atualizado, a única possibilidade de ele ser zero é quando a lista estiver realmente vazia.
Um exemplo da utilização de vazia é quando se deseja incluir, ou excluir um nó da lista. É
necessário um tratamento especial na primeira inclusão, pois o primeiro e ultimo elemento são
os mesmos, e na exclusão, uma vez que seria um erro tentar remover elementos de uma lista
vazia.
Operações Modificadoras
Uma das caracterı́sticas interessantes da lista encadeada é a facilidade de se incluir e excluir
elementos, em tempo de execução, com praticidade, uma vez que é necessário somente alocar e
desalocar nós, respectivamente. É importante, então, conhecer as funções de inserção e exclusão,
tratadas como operações modificadoras, pois alteram a composição corrente da lista. Seguem,
assim, os exemplos de implementação.
Inserção no Final
No Exemplo 7.22, o novo elemento e a lista no qual ele será inserido são passados como argu-
mentos. A passagem do cabeçalho lista é feita por apontador, pois assim, qualquer modificação
em seus elementos é feita diretamente na função, sem necessidade de passar toda lista por cópia
e retorná-la ao fim da execução da função. O elemento será incluı́do no fim da lista, mas dife-
rentes implementações, nas quais ele é inserido no começo, ou numa dada posição, também são
possı́veis.
206 CAPÍTULO 7. APONTADORES
Como pode ser visto no exemplo acima, inicialmente o novo nó deve ser criado para armazenar
o elemento a ser incluı́do. Para isso é utilizada a função inicializadora criaNo, que aloca
um espaço de memória do tamanho de tNo, faz a chave ser igual ao elemento passado como
argumento e retorna um apontador para o espaço alocado. A Figura 7.17 mostra o estado da
lista, antes da inclusão, e o novo nó a ser incluı́do.
Agora que se já se tem o novo nó, é necessário incluı́-lo na lista, ou seja, atualizar o valor de
tam e os apontadores. Como a lista tem mais um nó, seu tamanho deve ser incrementado em
uma unidade. Quanto à atualização dos apontadores, os apontadores ultimo e marcador devem
apontar para o nó incluı́do, respectivamente, porque ele é incluı́do no fim da lista e no último
nó acessado. Antes, porém, é importante verificar se a lista está vazia, pois caso esteja, quem
também vai apontar para nó a ser incluı́do, será o apontador primeiro da lista, caso contrário,
será apontador proximo do ultimo nó da lista. Vale destacar que a análise se a lista está vazia
é um exemplo de uso da função vaziaLista, que foi explicada anteriormente.
A Figura 7.18 exibe a lista após a inclusão do novo nó.
Exclusão
7.7. TAD IMPLEMENTACIONAL LISTA ENCADEADA TLISTA 207
Na função de exclusão, são passadas como parâmetros a lista e a posição do nó a ser excluı́do.
Assim, é importante saber, inicialmente, se o valor de pos é válido e, caso não seja, a função é
encerrada.
São criados dois apontadores auxiliares do tipo tNo: auxA e auxB. Para buscar o nó da
posição pos, auxA aponta para o primeiro nó da lista e inicia-se um laço for, até que o valor de
i, que inicialmente é zero, seja pos. A cada iteração desse laço, auxB vai apontar para o último
nó apontado por auxA e este, por sua vez, aponta para o próximo nó da lista. Portanto, quando
auxA estiver apontando para o nó a ser excluı́do, o anterior a ele é apontado por auxB, o que é
importante na hora de se atualizarem os apontadores, conforme será explicado.
Após ter encontrado o nó da posição pos, tem que se analisar quatro situações possı́veis: 1)
esse nó a ser excluı́do não ser nem o primeiro e nem o último nó da lista; 2) ser o último; 3) ser
o primeiro; 4) e ser o único nó. No caso 1, basta fazer o apontador proximo do nó apontado por
auxB apontar para o nó apontado pelo proximo do nó apontado por auxA. No caso 2, também
se deve fazer o último nó da lista ser auxB. Vale lembrar que, no caso de auxA ser o último,
ao se fazer o proximo de auxB ser o proximo de auxA, está se fazendo simplesmente auxB ter
como próximo o valor NULL. Já no caso 3, coloca-se o próximo de auxA como o nó inicial e,
no caso 4, faz-se também o apontador ultimo da lista apontar para o próximo de auxA. Nesse
último caso, está se redirecionando os apontadores, primeiro e ultimo da lista, para NULL.
Agora que já se tem os apontadores atualizados e retirado o nó que será excluı́do da seqüência
de nós, coloca-se o marcador da lista como NULL, pois será perdido o último nó acessado. Então,
diminui-se o tamanho da lista e, finalmente, libera-se o espaço de memória apontado por auxA.
A Figura 7.19 ilustra a lista da Figura 7.16 após a exclusão do nó 2.
Operações Produtoras
As funções produtoras possibilitam extrair alguma informação da lista. Dividem-se em duas
categorias: as que permitem posicionar o marcador no nó, cuja informação será extraı́da, e as
que realmente obtém a informação. Serão mostradas inicialmente as operações que marcam a
posição de um elemento. Apresentam-se a que posiciona o marcador no primeiro nó da lista e a
que posiciona no próximo nó do marcador atual. A fim de ilustrar a utilização destas funções,
será dado como exemplo o uso delas na função imprimir.
O Exemplo 7.24 é a implementação da função que posiciona o marcador no primeiro nó da
7.7. TAD IMPLEMENTACIONAL LISTA ENCADEADA TLISTA 209
lista.
Para fazer o marcador de uma lista apontar para o primeiro elemento dela, basta igualar
o marcador ao apontador primeiro. O objetivo da operação primeiroLista é posicionar o
marcador no inı́cio da lista, para permitir que seja feita uma busca nos elementos da lista a
partir do seu inı́cio.
A operação proximoLista é mostrada no Exemplo 7.25.
A função proximoLista é para posicionar o marcador, fazendo-o apontar para o seu próximo
elemento da lista. A verificação se o marcador atual é diferente de NULL evita o acesso indevido
à memória.
O Exemplo 7.26 mostra a operação responsável por retornar a informação do nó apontado
pelo marcador da lista.
A função obterInfo tem por objetivo retornar o conteúdo do nó apontado pelo marcador.
É gravado no endereço x se houve sucesso, ou não, em se obter a informação. Assim, impede-se
de acessar um local indevido da memória.
O Exemplo 7.27 ilustra o uso dessas informações por meio do uso de uma função que imprime
todos os dados de uma lista. Para a função é passada a lista cujos elementos serão impressos.
Inicialmente primeiroLista é utilizada para posicionar o marcador no inı́cio da lista e, em
seguida, começa um laço enquanto o final dela não é alcançado, verificação feita pela função
finalLista, conforme descrito anteriormente.
210 CAPÍTULO 7. APONTADORES
Para cada posição do marcador, obterInfo retorna o valor de info do nó apontado pelo
marcador da lista. Esse valor é impresso caso tenha ocorrido sucesso na obtenção da informação,
ou seja, a variável erro ter o valor 1. O valor a ser impresso é inteiro, visto que x é do tipo tInfo
(int, como declarado anteriormente). Por fim, proximoLista posiciona o marcador no próximo
nó da lista e, somente quando este marcador for NULL, o laço enquanto será encerrado.
Operações Destrutoras
Quando a lista não é mais necessária, é importante desalocar o espaço de memória ocupado por
seus nós. As funções responsáveis por desalocar a lista e seus elementos são conhecidas como
destrutoras.
Finalização
Exemplo 7.28: Desalocação das áreas de memória ocupados pelos nós da lista
7.7.4 Uso
A fim de ilustrar a utilização do TAD implementacional lista encadeada, será utilizado um
cadastro simples de alunos na universidade. Veja o Pseudocódigo 7.1.
O Pseudocódigo 7.1 descreve os passos para o programa de cadastro simples, no qual apenas
pode-se ler e armazenar algumas entradas. Após a leitura, alguns dados podem ser excluı́dos e
as informações restantes serem impressas.
212 CAPÍTULO 7. APONTADORES
No cadastro, haverá apenas o nome e o número de matrı́cula do aluno, mas vale lembrar que,
para um registro de informaçõess mais detalhado, basta acrescentar mais atributos no exemplo
de estrutura tInfo do exemplo apresentado.
Observe o Exemplo 7.29, no qual se tem a transcrição do Pseudocódigo 7.1. Foram utilizadas
todas as funções estudadas nesta seção, porém com algumas modificações, uma vez que o campo
info, da estrutura do nó, não é mais um inteiro, e sim, uma outra estrutura. Só estão mostradas
as estruturas e as operações anteriores que sofreram modificação.
1 typedef struct {
2 int matricula ;
3 char nome [30];
4 } tInfo ;
5
6 void imprimirLista ( tLista lista ) {
7 tInfo x ;
8 int erro = 0;
9
10 primeiroLista (& lista ) ;
11 while (! finalLista (& lista ) ) {
12 x = obterInfo (& lista , & erro ) ;
13 if ( erro ) {
14 printf ( " Aluno : % s \ n " , x . nome ) ;
15 printf ( " Matricula : % d \ n \ n " , x . matricula ) ;
16 }
17 proximoLista (& lista ) ;
18 }
19 }
20
21 main ( ) {
22 tLista lista ;
23 tInfo dados ;
24 int pos ;
25
26 iniciaLista (& lista ) ;
27
28 do {
29 printf ( " Entre com o nome do aluno : " ) ;
30 scanf ( " % s " , & dados . nome ) ;
31 printf ( " Entre com a matricula do aluno : " ) ;
32 scanf ( " % d " , & dados . matricula ) ;
33 if ( dados . matricula <= 0)
34 break ;
35 incluirFimLista (& lista , dados ) ;
36 } while (1) ;
37
38 imprimirLista ( lista ) ;
39
40 printf ( " Digite a posiç~ a o do elemento a ser excluı́do : " ) ;
41 scanf ( " % d " , & pos ) ;
7.8. EXERCÍCIOS RESOLVIDOS 213
Para o trecho de código do Exemplo 7.30, ilustre os estados das variáveis apontadores e
não-apontadores. Utilize para a ilustração os padrões adotados na Figura 7.3 e Figura 7.4, para
representar uma variável não apontador e apontador respectivamente.
1 int *p;
2 int *q;
3 int a;
4 int b;
5 int c;
6
7 b = 10;
8 c = 15;
9 a = c;
10 p = &b;
11 q = &c;
12 *p = *q;
Solução Possı́vel:
Inicialmente são criados dois apontadores para inteiro, p e q, e três variáveis inteiras: a b e
c. Em seguida, b passa a armazenar o valor 10, enquanto c, 15. Já a rebece o valor de c, ou
seja, 15. Isso é ilustrado nas três primeiras cenas da Figura 7.20.
5 }
6
7 main ( ) {
8 int x ;
9 int quadrado ;
10
11 scanf ( " % d " , & x ) ;
12 quadrado = calculaPot ( x ) ;
13 printf ( " Quadrado de % d = % d \ n " , x , quadrado ) ;
14 }
Solução Possı́vel:
O dado de entrada do programa é um número inteiro, e as saı́das devem ser as pontências
quadrada e cúbica deste número, calculadas pela função apresentada no Exemplo 7.31, com suas
devidas alterações. O Pseudocódigo 7.2 exibe as etapas para a solução.
Para atender a etapa Cálculo das potências pela função calculaPot, a função calcu-
laPot deve ser alterada para também calcular e retornar o cubo do valor. Contudo, o retorno
dessa função já é o quadrado do parâmetro de entrada. Nesse sentido, a passagem por apon-
tadores é a ferramenta para múltiplos retornos numa função, o que torna importante o passo
Passagem de parâmetros para a função calculaPot
As modificações no Exemplo 7.31 podem ser observadas no Exemplo 7.32.
Como pode ser observado, calculaPot passa a ter um parâmetro que irá receber o endereço
da variável cubo, declarada na main. Dessa forma, ao término da execução de calculaPot,
cubo irá conter a potência cúbica do valor.
Foi visto anteriormente como inserir um elemento no final da lista. Agora, pretende-se criar
uma função que insere no começo de uma lista simplesmente encadeada.
Solução Possı́vel:
Para inserir um novo nó, no começo da lista, os passos que devem ser seguidos pela função
aparecem no Pseudocódigo 7.3, onde também pode-se observar os dados de entrada e o retorno
da função.
O Exemplo 7.33 mostra a implementação em C. Para tal função, a passagem da lista será
feita por apontador, pois assim, qualquer modificação em seus elementos será feita diretamente
na função e, por consegüinte, o retorno é void. A lista poderia ser passada por cópia, mas a
lista modificada teria estar após um comando return.
2 tNo * no ;
3
4 no = criaNo ( elem ) ;
5 if ( vaziaLista ( lista ) ) {
6 lista - > ultimo = no ;
7 } else {
8 no - > proximo = lista - > primeiro ;
9 }
10 lista - > primeiro = lista - > marcador = no ;
11 lista - > tam ++;
12 }
No Exemplo 7.33, um novo nó é criado com a utilização de criaNo. Ao incluı́-lo no inicı́o, os
apontadores primeiro e marcador devem apontar para o nó ser incluı́do, respectivamente porque
ele é incluı́do no começo da lista e no último nó acessado.
Contudo, antes de atualizar o apontador primeiro, é fundamental verificar se a lista está
vazia, pois caso não esteja, o novo nó deve guardar, como seu próximo, o antigo primeiro da
lista. Já se a lista estiver vazia, basta fazer o apontador ultimo também apontar para o novo
nó. Por fim, o tamanho da lista deve ser incrementado em uma unidade.
A figura 7.21 mostra a Figura 7.17, após a inclusão do novo nó no começo da lista.
Para ilustrar a odenação de uma lista, considere que ela é formada por nós cuja informação
armazenada seja um nome e uma idade. Ulizando as estruturas e funções já apresentadas, além
de novas funções, crie um programa que lê as informações do teclado até que uma idade inválida
seja digitada (menor que zero). O programa deve imprimir a lista ordenada crescentemente por
218 CAPÍTULO 7. APONTADORES
Solução Possı́vel:
De forma geral, o programa deve seguir o Pseudocódigo 7.4.
O passo Ordenação da lista pode ser decomposto como segue no Pseudocódico 7.5. A
idéia consiste em posicionar o marcador no começo da lista e, enquanto o final desta não é
atingido, buscar o menor elemento à frente do atual marcador. Se encontrar algum, deve-se
trocar as informações entre o nó marcador e o que guarda a menor idade à frente.
1 typedef struct {
2 char nome [30];
3 int idade ;
4 } tInfo ;
5
6 void imprimirLista ( tLista lista ) {
7 tInfo x ;
8 int erro = 0;
9
10 primeiroLista (& lista ) ;
11 while (! finalLista (& lista ) ) {
12 x = obterInfo (& lista , & erro ) ;
13 if ( erro ) {
14 printf ( " Nome : % s \ n " , x . nome ) ;
15 printf ( " Idade : % d \ n \ n " , x . idade ) ;
16 }
17 proximoLista (& lista ) ;
18 }
19 }
20
21 tNo * menorIdade ( tLista lista ) {
22 tNo * aux , * menor ;
23
24 menor = lista . marcador ;
25 aux = menor - > proximo ;
26 while ( aux != NULL ) {
27 if ( aux - > info . idade < menor - > info . idade )
28 menor = aux ;
29 aux = aux - > proximo ;
30 }
31 return menor ;
32 }
33
34 void ordenaPorIdade ( tLista * lista ) {
35 tNo * aux ;
36 tInfo x ;
37
38 primeiroLista ( lista ) ;
39 while (! finalLista ( lista ) ) {
40 aux = menorIdade (* lista ) ;
41 if ( lista - > marcador != aux ) {
42 x = lista - > marcador - > info ;
43 lista - > marcador - > info = aux - > info ;
44 aux - > info = x ;
45 }
46 proximoLista ( lista ) ;
47 }
48 }
49
220 CAPÍTULO 7. APONTADORES
50 main ( ) {
51 tLista lista ;
52 tInfo dados ;
53 int pos ;
54
55 iniciaLista (& lista ) ;
56 do {
57 printf ( " Entre com o nome : " ) ;
58 scanf ( " % s " , & dados . nome ) ;
59 printf ( " Entre com a idade : " ) ;
60 scanf ( " % d " , & dados . idade ) ;
61 if ( dados . idade < 0)
62 break ;
63 incluirFimLista (& lista , dados ) ;
64 } while (1) ;
65 ordenaPorIdade (& lista ) ;
66 imprimirLista ( lista ) ;
67 destroiLista (& lista ) ;
68 }
Considere uma lista de funcionários de uma empresa na qual são armazenados o nome e o
salário destes. Pretende-se imprimir na tela todos os funcionários cujos salários estão acima da
7.8. EXERCÍCIOS RESOLVIDOS 221
média.
Solução Possı́vel:
O programa principal segue os passos descritos no Pseudocódigo 7.6.
O passo Verificar quem está acima da média pode ser descrito como o Pseudocódigo
7.7. Para achar a média, basta somar todos os salários e dividir pelo total de nós. Tendo a
média, procura-se, nó por nó, quem tem o salário maior que ela.
Pseudocódigo 7.7 Processo para ver quem está com salário acima da média
Processo componente "Verificar quem está acima da média":
Posicionar o marcador no começo da lista;
Enquanto n~ao chegar no fim da lista:
Somar os salários;
Posicionar o marcador no próximo nó da seqü^
encia da lista;
Achar a média (Divide-se o total somado pelo tamanho da lista);
Posicionar o marcador no começo da lista;
Enquanto n~ao chegar no fim da lista:
Se existir um nó com salário maior que a média:
Imprimir as informaç~
oes do funcionário.
Posicionar o marcador no próximo nó da seqü^
encia da lista;
FIM-Processo componente "Verificar quem está acima da média".
222 CAPÍTULO 7. APONTADORES
1 typedef struct {
2 char nome [30];
3 float salario ;
4 } tInfo ;
5
6 void imprimirLista ( tLista lista ) {
7 tInfo x ;
8 int erro = 0;
9
10 primeiroLista (& lista ) ;
11 while (! finalLista (& lista ) ) {
12 x = obterInfo (& lista , & erro ) ;
13 if ( erro ) {
14 printf ( " Nome : % s \ n " , x . nome ) ;
15 printf ( " Salario : % f \ n \ n " , x . salario ) ;
16 }
17 proximoLista (& lista ) ;
18 }
19 }
20
21 void acimaMedia ( tLista lista ) {
22 tInfo x ;
23 int erro = 0;
24 float total , media ;
25
26 total = 0;
27 primeiroLista (& lista ) ;
28 while (! finalLista (& lista ) ) {
29 x = obterInfo (& lista , & erro ) ;
30 if ( erro )
31 total += x . salario ;
32 proximoLista (& lista ) ;
33 }
34
35 media = total / lista . tam ;
36
37 primeiroLista (& lista ) ;
38 while (! finalLista (& lista ) ) {
39 x = obterInfo (& lista , & erro ) ;
40 if ( erro ) {
41 if ( x . salario > media ) {
42 printf ( " Nome : % s \ n " , x . nome ) ;
43 printf ( " Salario : % f \ n \ n " , x . salario ) ;
44 }
45 }
46 proximoLista (& lista ) ;
7.9. RESUMO 223
47 }
48 }
49
50 main ( ) {
51 tLista lista ;
52 tInfo dados ;
53 int pos ;
54
55 iniciaLista (& lista ) ;
56 do {
57 printf ( " Entre com o nome : " ) ;
58 scanf ( " % s " , & dados . nome ) ;
59 printf ( " Entre com o salario : " ) ;
60 scanf ( " % f " , & dados . salario ) ;
61 if ( dados . salario < 0)
62 break ;
63 incluirFimLista (& lista , dados ) ;
64 } while (1) ;
65 acimaMedia ( lista ) ;
66 destroiLista (& lista ) ;
67 }
7.9 Resumo
• Apontadores são um tipo de variável que guarda o endereço de outras variáveis. Trata-se
de um conceito de baixo nı́vel, ligado essencialmente à arquitetura de computadores.
• O operador & retorna o endereço de memória de uma variável. Já o operador seta (− >)
é utilizado para acessar, por meio de apontadores, os atributos de uma estrutura.
224 CAPÍTULO 7. APONTADORES
• Os apontadores podem alterar o conteúdo da variável apontado por ele. Por meio de
apontadores também é possı́vel a passagem de parâmetros sem cópia. A passagem de
parâmetros por apontadores permite múltiplos retornos em uma função, evita a cópia de
muitos dados e possibilita a alteração das variáveis do programa no decorrer da execução
da função.
• Dentre os principais problemas com o uso de apontadores estão: apontadores não iniciali-
zados, objetos pendentes, referências pendentes e programação macarrônica.
• O TAD Implementacional Lista Encadeada é uma forma eficiente de armazenar dados num
programa, pois aloca-se e desaloca-se os espaços para os dados dinâmicamente, o que torna
muito simples as operações de incluir e excluir elementos.
1 main ( ) {
2 int p ;
3 int * d ;
4 int q = 10;
5 float * j ;
6 float t = 15.0;
7 j = &t;
8 p = &q;
9 d = j;
10 }
1 main ( ) {
2 int * p = ( int *) malloc ( sizeof ( int ) ) ;
3 int * q = ( int *) malloc ( sizeof ( int ) ) ;
4 int * j ;
5 int * h ;
6 int * v ;
7 int d = 20;
8 int e = 30;
9 *q = e;
10 *j = d;
11 p = &d;
12 h = &e;
13 v = q;
14 free ( q ) ;
15 }
9. Crie uma funcão que insere um novo elemento na lista numa dada posição. Considere que
os argumentos da função são:
- Um apontador para a lista no qual o elemento será inserido.
- Um elemento de um tipo tInfo previamente definido.
- Um inteiro que indica a posição na qual ocorrerá a inserção.
Sugestão: verifique se a posição fornecida à função é válida.
10. Considere duas listas simplesmente encadeadas ”A”e ”B”, contendo inteiros ordenados
crescentemente. Assim, pede-se implementar uma função que retorne uma lista encade-
ada ordenada formada pela intercalação dos elementos de ”A”e ”B”, considerando que a
226 CAPÍTULO 7. APONTADORES
lista resultante não possua chaves repetidas e que ”A”e ”B”também não possuem chaves
repetidas.
11. Numa lista duplamente encadeada, os nós possuem um apontador para o nó anterior a
ele na lista, além do já apresentado apontador para o proximo. Com base nisso, imple-
mente todas as funçãoes e estruturas discutidas no capı́tulo na forma de lista duplamente
encadeada.
12. Pilhas são um cso particular da lista simplesmente encadeada, no qual insere-se e retira-se
um elemento apenas do fim, uma polı́tica conhecida como LIFO (last in first out). Seja
então P=( a(1), a(2), ..., a(n) ) uma pilha. Assim, a(1) é o elemento da base da pilha;
a(n) é o elemento topo da pilha; e a(i+1) está acima de a(i). As operações associadas são:
Com base nessas informações, implemente uma pilha de dados ”pilha de inteiros”em C,
utilizando as estruturas e funções apresentadas sobre lista simplemente encadeada.
13. Duas pilha seqüênciais numéricas estão ordenadas crecentemente a partir do topo. Trans-
fira os elementos dessas pilhas para uma terceira pilha, inicialmente vazia, de modo que ela
fique ordenada decrescentemente (maior valor no topo). Suponha que não haja restrições
quanto a capacidade das pilhas.
14. O problema do abre/fecha parênteses. Este problema consiste em verificar se uma ex-
pressão matemática está corretamente formada em termos de abre/fecha parênteses.
Exemplos:
7-((X*((X+Y)/(J-3))+Y)/(4-2.5))
((A+B)
)A+B(-C
(A + B)) - (C + D
Numa expressão correta o número de ”)”deve ser igual ao número de ”(”. Cada ”)”deve
ser precedido por um ”(”. Para isto, pode utilizar-se de um contador inicialmente igual a
zero que, ao percorrer a expressão da direita para a esquerda, é decrementado quando se
encontra um ”(”e incrementado quando se encontra um ”)”. Assim, o contador no final
da expressão deve ser igual a zero e, em nenhum momento, o ele deve ser menor que zero.
7.10. LISTA DE EXERCÍCIOS 227
Então, implemente um programa C que leia uma expressão com parênteses e verifique
se ela está corretamente formada em termos de abre/fecha parênteses usando o método
acima, utilizando as estruturas e funções de pilha criadas nos exercı́cios anteriores, mas
modificadas para receber caracteres.
15. Outro caso particular da lista simplesmente encadeada é a fila. Agora, a inserção é feita
apenas no fim da lista e a remoção no inı́cio. Seja então F=( a(1), a(2), ..., a(n) ) uma
fila. Dessa forma, a(1) é o começo da fila; a(n) é o final da pilha; e a(i+1) está atrás de
a(i). As operações associadas são:
Com base nessas informações, implemente uma fila de dados ”fila de nomes”em C, utili-
zando as estruturas e funções apresentadas sobre lista simplemente encadeada.
16. Duas filas seqüênciais de nomes estão ordenadas crecentemente a partir do inı́cio. Transfira
os elementos que ocorrem nessas duas filas para uma terceira fila, inicialmente vazia, de
modo que ela também fique ordenada crescentemente, ou seja, o primeiro nome em ordem
alfabética no começo da fila.
17. Uma palavra é um palı́ndromo se tem a mesma seqüência de letras, quer seja lida da
esquerda para a direita ou da direita para a esquerda (exemplo: raiar). Implemente uma
solução para verificar se uma palavra é um palı́ndromo, usando pilha(s) e/ou fila(s).
18. Suponha uma fila de inteiros F. Mude a posição de um elemento desta fila, tendo apenas
uma pilha vazia P e uma váriavel do tipo inteiro x como auxiliares. Considere apenas as
operações associadas aos tipos fila e pilha.
19. Implemente uma lista encadeada que armazene as informações sobre os DVDs de uma
locadoras. Tais informações são tı́tulo do filme, nome do diretor, principais atores e número
de cópias disponı́veis na locadora. Apresente um menu e crie funções que atendam as
seguintes opções:
- Dado um ator, imprimir as informações sobre os filmes nos quais ele foi um dos atores
principais.
20. Sabe-se que um texto é uma seqüência de caracteres contendo apenas letras, espaços em
branco e sinais de pontuação. Uma palavra é definida como um segmento do texto que
consiste apenas de letras. Escreva uma função que recebe um texto do teclado e imprime
uma relação de todas as palavras que ocorrem no texto juntamente com o número de
ocorrências de cada palavra.
(a) Ler os dados da listagem e colocá-los em quatro listas. O usuário deve poder incluir
tantos dados quanto quiser. A indicação de fim de entrada de dados é feita através
de um aluno de Matrı́cula 0 (zero).
(b) Mostrar as notas de um aluno numa disciplina. Lembre-se que o aluno pode ter
cursado mais de uma vez uma mesma disciplina por motivo de reprovação.
(c) Incluir uma nota de um aluno em uma disciplina.
(d) Retificar uma nota de um aluno em uma disciplina.
(e) Excluir uma nota de um aluno em uma disciplina.
(f) Excluir todos os dados de um aluno jubilado.
(g) Calcular o o coeficiente de rendimento (C.R.) de um aluno. Lembre-se que o C.R. é
calculado da seguinte maneira:
7.11. TRABALHOS SUGERIDOS 229
Exemplo de Programa
Escolha Operação:
0 - Sair
1 - Ler dados
2 - Mostrar Notas
3 - Incluir Nota
4 - Corrigir Nota
5 - Excluir Nota
6 - Excluir Aluno
7 - CR de Aluno
8 - Melhor Aluno
9 - Maus Alunos
10 - Formando
Opção: 1
Dados do aluno: 18 1 60 4.5
Dados do aluno: 1111 3232 30 8.7
Dados do aluno: 234 500 75 9.0
Dados do aluno: 18 1 60 7.0
... ... ...
Dados do aluno: 0 0 0 0.0
Opção: 2
Matrı́cula: 18
Disciplina: 1
Notas: 4.5 7.0
Opção: 3
Matrı́cula: 1111 Disciplina: 316
Carga Horária: 60
Nota: 7.0
230 CAPÍTULO 7. APONTADORES
Opção: 4
Matrı́cula: 1111 Disciplina: 316
Carga Horária: 60
Nota: 7.0
Nova Nota: 8.5
Opção: 5
Matrı́cula: 1111 Disciplina: 316
Nota: 7.0
Opção: 6
Matrı́cula: 234
Opção: 7
Matrı́cula: 1111
CR: 7.2
Opção: 8
Matrı́cula: 234
Opção: 9
Matrı́culas: 194
Opção: 10
Matrı́cula: 1111
Não cumpriu carga horária mı́nima.
Opção: 0
Até a próxima!
Descrição
Controle de Tráfego Aéreo é um serviço prestado por controladores, em terra, que guiam
aeronaves (geralmente aviões) no ar e no solo, para garantir um fluxo de tráfego seguro e
7.11. TRABALHOS SUGERIDOS 231
Espaço Aéreo
Em muitos paı́ses, os serviços de tráfego aéreo são prestados em toda a extensão do espaço
aéreo e estes serviços são utilizados por todos os usuários (aeronaves privadas, militares e
comerciais). Os espaços aéreos onde o controlador é responsável por prover separação entre
as aeronaves são chamados de espaço áereo controlado em oposição ao espaço aéreo
espaço áereo não controlado no qual pilotos das aeronaves são responsáveis por manter
a separação entre a sua aeronave e outras. Dependendo do tipo de vôo e de classe do espaço
aéreo, o controlador de tráfego aéreo pode emitir instruções que os pilotos devem seguir
ou apenas informações de vôo para ajudar os pilotos operando no espaço aéreo. Em todos
os casos, entretanto, o piloto tem a responsabilidade final pela segurança da aeronave, e
pode não cumprir as intruções numa emergência.
Os serviços de controloe de tráfego aéreo incluem o controle de rotas das aeronaves que
estão no espaço áereo do aeroporto, o controle da autorização de pousos e decolagens e o
controle das pistas de táxi-aéreo e pátios.
O Trabalho
Controle de rotas
rotas envolvidas. Existem duas ocoasiões em que ocorrerão colisões: se duas aero-
naves estiverem utilizando a mesma rota ou se duas aeronaves estiverem utilizando
rotas que apresentem intersecção. Considere que duas rotas apresentam intersecção
se uma delas é múltipla da outra.
Controle de aproximação
Dado que o controle de rotas já foi realizado será necessário definir qual será a ordem
das aeronaves a pousar. Para isso deverá ser considerada a seqüência de prioridades
abaixo:
CLASSE > ALTITUDE > VELOCIDADE
Assim, define-se que existem apenas duas classes (militar e comercial) e que duas
alttudes são consideradas iguais se a diferença entre elas for menor que 2000 pés.
Além disso, a classe militar tem prioridade frente a classe comercial; quanto menor
a altitude de uma aeronave maior será a sua prioridade e quanto maior a velocidade
de uma aeronave maior será a sua prioridade. Considere que cada operação de pouso
dura 5 minutos.
• Controle de pista
Após o pouso de uma aeronave é necessário que o tráfego em solo também seja
controlado. Com isso, a segunda parte do consistirá em controlar o momento em
que cada aeronave deverá encostar em uma plataforma que está livre. Caso todas
as três plataformas estejam ocupadas no momento em que uma aeronave pouse, esta
deverá esperar no pátio de espera até que alguma das plataformas seja desocupada.
Considere que cada operação de desembarque dure 30 minutos.
Especificação
Módulo 1
Essa primeira parte do trabalho consiste em determinar a fila de aeronaves que irá pusar e
as novas rotas das aeronaves que precisam mudar de rota. Para isso, considere que quando
duas aeronaves apresentem chance de colisão a aeronave que apresentar número de rota
maior deverá ser redirecionada para uma rota com número igual ao primeiro número primo
maior que a rota de maior número.
Módulo 2
7.11. TRABALHOS SUGERIDOS 233
Entrada
Exemplo:
0103
2
militar
7100
670
14:30
0307
10
comercial
8200
770
14:29
0200
13
militar
1000
710
14:30
234 CAPÍTULO 7. APONTADORES
0708
10
comercial
8600
640
14:29
1102
4
militar
9100
700
14:30
Saı́da
Os dados de saı́da do programa devem ser exibidos no console. O formato da saı́da deve
seguir o exemplo abaixo.
Exemplo:
0200
13
15:00
0103
2
15:05
1102
17
15:10
0307
19
15:30
0708
23
15:35
Capı́tulo 8
Arquivos
Autores:
Objetivos:
• Definir e apresentar arquivos;
235
236 CAPÍTULO 8. ARQUIVOS
resolver esse problema, existem as variáveis persistentes, as quais têm seu conteúdo armazenado,
e possı́vel de ser acessado, independentemente do programa que as criou estar na memória princi-
pal. Variáveis persistentes devem ser armazenadas em algum dispositivo de memória secundária,
como discos rı́gidos, CD’s, memória flash e DVD’s, os quais tem a capacidade de manter por
um longo tempo o valor da informação neles contida, independente do conteúdo da memória
principal do computador, ou mesmo de ele estar ligado. Deve-se ressaltar que o tempo de acesso
e de gravação das variáveis armazenadas em meio secundário é muito maior que o das variáveis
armazenadas na memória principal, por isso essa última é tão importante.
O conceito por trás das variáveis persistentes é o de arquivo, e esse será o tema desse
capı́tulo. Por meio de arquivos, essas variáveis podem ser armazenadas e as informações que
elas guardam, acessadas e processadas no futuro, tanto pelo programa que as criou quanto por
outros programas.
Por meio de um arquivo binário, as variáveis armazenadas na memória principal podem ter seus
bytes armazenados num arquivo fı́sico, sem tradução para caracteres e com correspondência
de um para um. Isso significa que, usando-se arquivos binários, torna-se possı́vel criar um
”espelho”da memória principal, mas salvo em memória secundária.
A fim de exemplificar esse conceito na linguagem C, será citada a gravação de uma struct,
uma das razões pela qual arquivos binários são tão úteis nessa linguagem. Já foi estudado que
uma struct é um tipo especial de variável que armazena outras variáveis. Tome, por exemplo, a
struct
struct pessoa {
char nome[50];
int idade;
float salário;
}
Como guardar essa informação em um arquivo? Pode-se guardá-la num arquivo texto,
escrevendo-se o nome da pessoa, então sua idade e finalmente seu salário. Sabendo o formato
como essa informação foi escrita, o programador pode criar um programa que a lê.
De modo alternativo, pode-se armazená-la num arquivo binário usando um comando (que
será mostrado depois) que trata toda a struct como uma única variável. Ela pode ser lida
também se usando um comando que a trata da mesma forma. Isso é possı́vel, pois se sabe o
formato como essa struct foi armazenada no arquivo binário. Esse formato poderia ser, para
uma arquitetura de 32 bits de palavra, na linguagem C, de 50 bytes que armazenam cada uma
das variáveis char, mais 32 bits (4 bytes) que armazenam uma variável int, e mais 32 bits (4
bytes) que armazenam uma variável float.
FILE *arq;
8.4.1 Abertura
Após a declaração de uma variável do tipo arquivo, essa deve ser associada a um nome de
arquivo, o qual identifica um arquivo de fato, com correspondência na memória secundária.
Se o arquivo com o nome especificado já existir, então o programador terá acesso a seu
conteúdo, e poderá lê-lo e alterá-lo. Mas um nome de arquivo que ainda não existe também
pode ser utilizado. Nesse caso, um novo arquivo, ”em branco”, com o nome especificado, será
criado. Esses detalhes são resolvidos passando-se as informações corretas à função responsável
por abrir o arquivo.
Na linguagem C, essa função é a fopen(). Seu protótipo é:
A função retorna um ponteiro para FILE, que deverá ser recebido pela variável usada para
manipular o arquivo. O primeiro argumento de fopen(), nomearq, é uma string, e refere-se ao
nome do arquivo que será aberto. O segundo argumento, modo, também uma string, é um
código passado a fopen(), responsável por indicar que tipo de arquivo e operação será realizada
sobre esse arquivo. Os códigos são descritos na tabela 8.1:
Parâmetro Efeito
r Abre um arquivo-texto para leitura
w Cria um arquivo-texto para escrita
a Abre um arquivo-texto para gravar ao fim dele
r+ Abre um arquivo-texto para leitura/escrita
w+ Cria um arquivo-texto para leitura/escrita
a+ Abre ou cria (se não existir) um arquivo-texto para ler dele ou gravar ao fim dele
rb Abre um arquivo binário para leitura
wb Cria um arquivo binário para escrita
ab Abre um arquivo binário para gravar ao fim dele
r+b Abre um arquivo binário para leitura/escrita
w+b Cria um arquivo binário para leitura/escrita
a+b Abre ou cria um arquivo binário para gravar ao fim dele
No exemplo 8.1, a seguir, serão criados dois arquivos: o arquivo texto somente escrita.txt,
o qual não existia antes da execução de fopen(), e que será usado somente para gravação de
informação (no caso, caracteres); e leio e escrevo.meu, binário, no qual as informações poderão
ser lidas e escritas. Note como as variáveis FILE* são declaradas, inicialmente, e depois recebem
o retorno da função fopen().
Um ponto importante a ser ressaltado sobre a função fopen() é a avaliação de seu valor de
retorno. Muitas vezes ocorrem erros na abertura de um arquivo, como, por exemplo, passar para
fopen() o código ”r”mas o arquivo com o nome especificado não existir no disco. Esses erros
podem ser detectados porque a função fopen() retornará NULL para indicar que houve falha na
abertura de um arquivo.
No exemplo 8.2, é declarada uma variável de arquivo. Em seguida, é chamada fopen() para
realizar a abertura de arquivo. Imediatamente depois, é verificado se ele foi aberto corretamente.
Se não foi, o usuário será notificado e o programa, encerrado. Esse encerramento é feito com o
240 CAPÍTULO 8. ARQUIVOS
uso da função exit() (definida na biblioteca stdlib.h), a qual finaliza o programa e retorna para
o Sistema Operacional o código numérico passado como argumento.
1 FILE * arq_ex3 ;
2 arq_ex3 = fopen ( " alunos . txt " , " r " ) ;
3 if ( arq_ex3 == NULL ) {
4 printf ( " Erro com a abertura de arquivo ! O programa sera abortado ... " ) ;
5 exit (1) ;
6 }
Todas as vezes que fopen() for usada deve-se verificar imediatamente se a abertura de arquivo
ocorreu sem problemas. Essa abordagem evitará muitos problemas posteriores.
Quando um arquivo é aberto, passa a estar associado a ele um indicador de posição, res-
ponsável por indicar em qual ponto do arquivo novas informações são lidas ou gravadas (ele pode
ser imaginado como o cursor em um texto sendo editado num editor de texto). No momento em
que um arquivo é aberto, esse indicador estará no começo do arquivo; e sempre que as variáveis
persistentes do arquivo forem sendo lidas ou gravadas, o indicador de posição se movimenta,
ficando sempre à frente da ultima variável lida ou gravada.
8.4.2 Fechamento
Quando um arquivo aberto não for mais utilizado num programa, ele deve ser fechado. Esse
fechamento desassocia o arquivo fı́sico, em disco (por exemplo), com a variável do tipo arquivo
usada para abrı́-lo. Um arquivo aberto pode ser fechado usando-se uma função que execute essa
tarefa e que o receba como argumento.
É muito importante lembrar-se de fechar um arquivo. Arquivos abertos são recursos que
o sistema operacional deve gerenciar; dessa forma, muitos arquivos abertos significam mais
recursos sendo gerenciados.
Na linguagem C, a função fclose() é usada para fechar um arquivo. Seu protótipo é:
8.5.1 Leitura
A linguagem C fornece uma serie de funções para leitura de informações em arquivos de texto.
Serão mostradas a seguir duas delas: fscanf() e fscanf()
fscanf()
A função fscanf() funciona de forma semelhante à scanf(), já estudada em capı́tulos anteriores.
A diferença está no fato de que fscanf() trabalha com arquivos, e não com a entrada padrão
(normalmente o teclado), caso de scanf()
Seu protótipo é:
1 \ begin { verbatim }
2 # include < stdio .h >
3 # define NOME_ARQUIVO " meu_texto . txt "
4 # define TAM_STR 50
5
6 int main ( void )
7 {
8 int inteiro ;
9 float real ;
10 char str [ TAM_STR ];
11 FILE * arq ;
12
13 if ( !( arq = fopen ( NOME_ARQUIVO , " r " ) ) ) {
242 CAPÍTULO 8. ARQUIVOS
1145
segunda_linha
45.99
caso lido com o código acima, resultaria o valor 1145 na variável inteiro, ”segunda linha”em
str e 45.99 na variável real.
fgets()
Outra função de C utilizada para ler informações de um arquivo texto é fgets(), que permite ler
strings completas. Seu protótipo é:
Essa função lê os caracteres do arquivo fp passado como argumento e armazena-os na string str,
também passada como argumento, até que ou length-1 caracteres sejam lidos ou um caractere de
nova linha (’\n’) seja lido. Se um caractere de nova linha for lido, ele será armazenado em str. Já
quando a leitura de caracteres do arquivo finaliza, fgets() encerrará a string str armazenando ’\0’ no
próximo caractere (ou seja, finalizando a string).
Dessa forma, um arquivo texto que contenha os seguintes caracteres: ”primeira linha\nsegunda
linha”, tal como mostrado a seguir,
primeira linha
segunda linha
pode ser lido usando-se duas chamadas a funções fgets(). Duas strings serão lidas e gravadas
nas variáveis string: str1 conterá ”primeira linha\n”, e str2 conterá ”segunda linha”. O exemplo 8.4
mostra como isso pode ser feito:
7 exit (1) ;
8 }
9
10 fgets ( str1 , 50 , arquivo ) ;
11 fgets ( str2 , 40 , arquivo ) ;
Note que o tamanho máximo passado a fgets() corresponde ao tamanho das strings definidas.
8.5.2 Escrita
A linguagem C oferece a função fprintf(), que pode ser usada para gravar em arquivos de texto. Ela
será estudada a seguir.
fprintf()
A função fprintf() é semelhante a printf().
Seu protótipo é:
A passagem de parâmetros é semelhante a de printf(), com o acréscimo de que deve ser colocado
como primeiro argumento o ponteiro para FILE representando o arquivo no qual serão gravadas as
informações.
A gravação de strings pode ser feita sem problemas com o uso de fprintf().
O exemplo 8.5 mostra o uso dessa função. Nesse programa, três variáveis, uma float, uma de
caracteres e uma string, são criadas e têm valores associados; então, o arquivo é aberto e elas são
gravadas lá.
547.32
Isso e uma string constante
teste para string
T
8.6.1 Leitura
A linguagem C possui a função fread() que pode ser usada para ler um arquivo binário.
fread()
size t fread (void *buffer, size t num bytes, size t count, FILE *fp);
O primeiro parâmetro dessa função, buffer, é um ponteiro para uma região de memória que
receberá as variáveis lidas do arquivo. O número de bytes a serem lidos é especificado por num bytes;
é essa variável que informa à função qual o tipo de variável persistente a ser lida, pelo uso do operador
sizeof() (que retorna o tamanho do tipo passado como argumento). O parâmetro count indica
quantas variáveis do tamanho num bytes deverão ser lidas com essa chamada da função fread(). E
fp é um ponteiro para o arquivo de onde serão lidos os valores.
O tipo de retorno da função, size t, é definido no arquivo stdio.h, e é aproximadamente igual
a um inteiro sem sinal. Esse retorno pode ser avaliado para verificar se algum erro ocorreu, pois
fread() retornará a quantidade de itens lidos. Esse valor deve ser igual a count; se não for, então o
arquivo chegou ao final antes de ler a quantidade solicitada ou um erro ocorreu.
O exemplo 8.6 mostra a leitura de um inteiro, depois de um float, e então de um caractere, de
um arquivo binário. Considere que o arquivo arq1 já foi aberto nesse trecho de código.
8.6. OPERAÇÕES SOBRE ARQUIVOS BINÁRIOS 245
1 int inteiro ;
2 float real ;
3 char carac ;
4 /* ... */
5 fwrite (& inteiro , sizeof ( int ) , 1 , arq1 ) ;
6 fwrite (& real , sizeof ( float ) , 1 , arq1 ) ;
7 fwrite (& carac , sizeof ( char ) , 1 , arq1 ) ;
Note que as três variáveis devem estar gravadas em seqüência no arquivo. Note também que
buffer é um ponteiro para as variáveis que armazenarão valores lidos do arquivo.
8.6.2 Escrita
Já para se realizar a escrita de variáveis persistentes num arquivo binário, a linguagem C oferece a
função fwrite().
fwrite()
Seu protótipo é:
size t fwrite (void *buffer, size t num bytes, size t count, FILE *fp);
Essa função se assemelha muito com fread(). O parâmetro buffer é um ponteiro para a variável
que será escrita no arquivo. Já num bytes é usado com o operador sizeof() para informar à função
quantos bytes contém o tipo da variável a ser escrita. count indica quantas variáveis (do tamanho
de num bytes) serão escritas. E, finalmente, fp é o ponteiro para FILE que indica em qual arquivo
devem ser escritas as variáveis.
O exemplo 8.7 mostra um programa completo que gera uma struct, a grava em um arquivo e
depois fecha-o. Então, esse arquivo é reaberto e essa struct é lida dele (somente para exemplo, pois
os valores da struct ainda estavam corretos). Depois, esses dados lidos serão gravados em um outro
arquivo, texto.
12 TPessoa p ;
13
14 p . nome = " Marcia Maia " ;
15 p . idade = 46;
16 p . sexo = ’F ’;
17
18 if ( !( arq = fopen ( " arquivo_bin . teste " , " wb " ) ) ) {
19 printf ( Erro na abertura de arquivo ! Abortando o programa ...) ;
20 exit (1) ;
21 }
22 fwrite (& p , sizeof ( TPessoa ) , 1 , arq ) ;
23 fclose ( arq ) ;
24
25 if ( !( arq = fopen ( " arquivo_bin . teste " , " rb " ) ) ) {
26 printf ( Erro na abertura de arquivo ! Abortando o programa ...) ;
27 exit (1) ;
28 }
29 fread (& p , sizeof ( TPessoa ) , 1 , arq ) ;
30 fclose ( arq ) ;
31
32 if ( !( arq = fopen ( " arquivo_texto . txt " , " w " ) ) ) {
33 printf ( Erro na abertura de arquivo ! Abortando o programa ...) ;
34 exit (1) ;
35 }
36 fprintf ( arq , " % s \ n % d \ n % c " , p . nome , p . idade , p . sexo ) ;
37 fclose ( arq ) ;
38 }
Pode-se ver que ao final da execução do programa, arquivo texto.txt conterá os dados da pessoa,
e poderão ser lidos assim que o arquivo for aberto por um editor de texto qualquer (fato que não
ocorre com o arquivo arquivo bin.teste, binário).
As seções anteriores mostraram as funções básicas para se trabalhar com arquivos em C. Porém,
essas funções não são suficientes para realizar todas as tarefas que arquivos permitem.
Serão apresentadas duas funções nessa seção: feof(), usada para verificar se o indicador de
posição num arquivo chegou ao final dele; e fseek(), usada para posicionar o indicador de posição
num local especı́fico do arquivo.
Os exemplos apresentados nessa sessão serão de programas completos e mais complexos dos que
os anteriormente mostrados.
8.7. OUTRAS FUNÇÕES ÚTEIS PARA ARQUIVOS 247
8.7.1 feof()
Uma função essencial usada para trabalhar com arquivos é feof(). Seu protótipo é:
Essa função retornará 0 se o arquivo fp passado como argumento ainda não chegou ao final.
A verificação se dá pelo indicador de posição no arquivo. Assim, a função feof() é utilizada para
verificar quando um arquivo, tanto de texto quando binário, termina.
No exemplo 8.8, existe um arquivo previamente criado, binario, que armazena muitas variáveis
struct do tipo TFuncionario. O programa abre esse arquivo, lê todas as variáveis lá presentes (uma
quantia inicialmente desconhecida), e vai gravando-as num arquivo texto.
Note que esse programa usa o tipo TFuncionario como um TAD (tipo abstrato de dados):
existe uma função para inicializar o funcionário, e funções que operam com um funcionário (lendo,
gravando...). A função main(), que usa essas funções, não acessa os campos de uma struct TFun-
cionario. Aliás, o programador da função main(), que usa o TAD funcionário, nem sequer precisa
saber como ele é implementado!
O programa funciona da seguinte forma: a função main() abre os arquivos, e então, usa a função
le func() para ler um funcionário de cada vez do arquivo de leitura (se a leitura do funcionário não
ocorrer corretamente, a função le func() usa a função invalida func() para indicar que a leitura
desse funcionário não foi feita corretamente). Então, a main() verifica se esse funcionário foi lido
corretamente, usando a função func valido(). Se sim, grava-o no arquivo texto, usando a função
grava func().
23
24 /* Bloco de codigo responsável pela correta abertura dos arquivos */
25 if ( !( arqbin = fopen ( ARQUIVO_LEITURA , " rb " ) ) ||
26 !( arqtex = fopen ( ARQUIVO_ESCRITA , " rb " ) ) ) {
27 printf ( " Erro na abertura de arquivos ! Abortando ... " ) ;
28 exit (1) ;
29 }
30
31 /* Bloco de codigo que contem o loop while , que usa a funç~ a o feof () para
32 ler o arquivo */
33 while ( ! feof ( arqbin ) ) {
34 f = le_func ( arqbin ) ;
35 if ( func_valido ( f ) ) {
36 grava_func (f , arqtex ) ;
37 } else {
38 printf ( " Erro na leitura de funcionarios . Abortando ... " ) ;
39 exit (1) ;
40 }
41 }
42 }
43
44 TFuncionario inicializa_func ( void )
45 {
46 TFuncionario f ;
47 f . matricula = 0;
48 return ( f ) ;
49 }
50
51 TFuncionario le_func ( FILE * arqbin )
52 {
53 TFuncionario f ;
54
55 if ( ( fread (& f , sizeof ( TFuncionario ) , 1 , arqbin ) ) != 1) {
56 f = invalida_func () ;
57 }
58
59 return ( f ) ;
60 }
61
62 TFuncionario invalida_func ( void )
63 {
64 TFuncionario f ;
65 f . matricula = -1;
66 return ( f ) ;
67 }
68
69
70 int func_valido ( TFuncionario f )
71 {
72 if ( f . matricula < 0) {
73 return (0) ;
8.7. OUTRAS FUNÇÕES ÚTEIS PARA ARQUIVOS 249
74 }
75 else {
76 return (1) ;
77 }
78 }
79
80 void grava_func ( TFuncionario f , FILE * arqtex )
81 {
82 fprintf ( arqtex , Nome : % s \ nSexo : % c \ nIdade : % d \ nMatricula : % d \ n
83 Salario : % f \ n \n , f . nome , f . sexo , f . idade , f . matricula , f . salario ) ;
84 }
8.7.2 fseek()
Muitas vezes, deseja-se ter controle sobre o indicador de posição num arquivo. Ao se saber, por
exemplo, qual o ı́ndice de uma estrutura especı́fica já gravada, do total de todas as estruturas que
fazem parte de um arquivo binário, pode-se buscar essa estrutura desejada sem ter que carregar para
a memória todas as outras. Isso pode ser feito posicionando-se o indicador de posição no arquivo e
lendo somente a próxima estrutura (que será a desejada).
A função em C que permite fazer isso é fseek(). Seu protótipo é:
A função retornará 0 se for bem-sucedida; caso contrário, retornará diferente de 0. Seus argu-
mentos são: fp, um ponteiro para arquivo já aberto por fopen(); numbytes, um número que indica
quantos bytes a partir do parâmetro origin estará o indicador de posição; e, finalmente, origin será
uma das três macros mostradas abaixo (que estão definidas no arquivo STDIO.H):
Posição Parâmetro
Inı́cio do arquivo SEEK TEST
Posição atual SEEK CUR
Final do arquivo SEEK END
Assim, para se posicionar duas variável int à frente da posição inicial do arquivo arq (ou seja,
pronto para ler a terceira variável int armazenada), faça:
O exemplo 8.9 mostra a função obtem func posicao(). Essa função é usada para ler structs
TFuncionario (definidas no Exemplo 8.8) contidas no arquivo binário (também definido no exemplo)
passando-se o ı́ndice do funcionário que se deseja ler. Ela recebe um ponteiro para o arquivo binário
250 CAPÍTULO 8. ARQUIVOS
de leitura, já aberto com fopen(), de onde tentará ler a struct de ı́ndice i, um parâmetro inteiro
passado. Assim, se i=1, o retorno da função será a primeira struct; se i=3, será a terceira; etc.
Ela usará a função invalida func(), também definida nesse exemplo, para indicar que houve erro na
leitura do funcionário especificado.
aceitável pelo tempo que isso demanda: o acesso ao disco se dá na ordem de milissegundos, muito
superior aos tempos envolvidos nas operações da memória e do processador.
Por esse motivo, um segundo arquivo estará também associado a cada variável TDicionario: um
arquivo de ı́ndices. Esse arquivo conterá duplas (chave, ı́ndice), em que chave corresponde às mesmas
chaves incluı́das no arquivo de dados, e ı́ndice indica o ı́ndice (numérico) dessa chave no arquivo de
dados.
Segue exemplo de uma variável TDicionario denominada meu dic, e associada a arquivos com
nome meu dicionario. Note que ambos os arquivos são binários, assim, o que é mostrado abaixo é
apenas o aspecto deles – eles não poderiam ser lidos diretamente de um editor de texto convencional.
Espı́rito Santo
estado brasileiro localizado na regi~ ao Sudeste
C
linguagem de programaç~
ao de nı́vel médio
cachorro
mamı́fero considerado "o melhor amigo do homem"
Espı́rito Santo
0
C
1
cachorro
2
O arquivo de ı́ndices, por ser consideravelmente menor que o de dados, poderá ser carregado todo
para a memória e então acessado, a fim de realizar-se uma busca. Dessa forma, pode-se descobrir
que ’cachorro’ é, sim, uma entrada, e está no ı́ndice 2 (terceira entrada) do arquivo de dados.
As entradas do arquivo de dados serão structs com dois campos strings, e as do arquivo de ı́ndices
structs com um campo string e um campo int. Uma variável TDicionario será uma struct com campos
que indicam o nome dos seus dois arquivos associados, um campo que armazena a matriz de ı́ndices
em memória, e um campo que guarda o total de entradas armazenadas no dicionário.
As operações que o TAD dicionário aceitará são: inicialização de um novo dicionário, abertura
de um dicionário já existente, busca de uma palavra e acréscimo de uma palavra. Seguem abaixo
definições dessas funções:
Ao chamar essa função, o usuário passará o nome do dicionário como argumento (uma string).
Então os dois arquivos serão criados; seus nome serão a string nome do dicionario acrescida de
252 CAPÍTULO 8. ARQUIVOS
” dados”, para o caso do arquivo de dados, e ” indice”, para o arquivo de ı́ndices. Na memória
ficará armazenada uma tabela de ı́ndices (ou seja, um vetor de structs), inicialmente vazia.
Essa função é usada para abrir um dicionário já existente. Sua chamada acarreta na abertura do
arquivo de ı́ndices (com nome indicado por nome do dicionario) e seu carregamento para a memória;
após isso, o arquivo de ı́ndices será fechado.
O uso dessa função acarreta na busca, na matriz de ı́ndices em memória (indicada por dic), da
string indicada por palavra. Se for encontrada, então se saberá seu ı́ndice; o arquivo de ı́ndice então
será aberto, o indicador de posição será posicionado nesse ı́ndice indicado, e a informação desejada (o
valor correspondente a essa chave) será lida e armazenada no argumento retorno. A função retornará
1 para indicar que a palavra foi encontrada e 0 senão.
Essa função será usada para inserir, no dicionário dic, a palavra chave com a sua descrição valor.
Inicialmente uma busca pela chave deverá ser realizada (na tabela de ı́ndices em memória); se ela
não existir, aı́ sim poderá ser inserida. Nesse caso, o arquivo de dados é aberto, e a chave e o
valor são armazenados ao fim dele; ele é fechado após isso. A matriz de ı́ndices em memória deverá
também ser modificada, com o acréscimo, ao fim dela, da chave e do seu ı́ndice no arquivo de dados
(é possı́vel saber o ı́ndice observando a quantidade de entradas já armazenadas, indicada por dic). O
arquivo de ı́ndices deverá então ser aberto, a matriz de ı́ndices em memória (já atualizada) gravada
nele, e logo em seguida fechado.
14 } entrada_dic ;
15
16 /* duplas ( chave , indice ) */
17 typedef struct {
18 char chave [ TAM_CHAVE ];
19 int indice ;
20 } indice_dic ;
21
22 /* definicao da variavel do TAD dicionario */
23 typedef struct {
24 char nome_arq_dados [ TAM_NOME_ARQ ];
25 char nome_arq_indices [ TAM_NOME_ARQ ];
26 indice_dic matriz_indices [ MAX_ENTRADAS ];
27 int tam ;
28 } TDicionario ;
29
30 /* funcao que inicializa um novo dicionario */
31 TDicionario inicializa_dic ( char * nome_do_dicionario )
32 {
33 TDicionario dic ;
34 FILE * arq ;
35
36 /* acrescenta " _indice " ao nome_do_dicionario */
37 strcpy ( dic . nome_arq_indices , nome_do_dicionario ) ;
38 strcat ( dic . nome_arq_indices , " _indice " ) ;
39
40 /* criacao do arquivo de indices */
41 if ( ( arq = fopen ( dic . nome_arq_indices , " wb " ) ) == NULL ) {
42 printf ( " Erro na criacao de arquivos ! Abortando ... " ) ;
43 exit (1) ;
44 }
45 fclose ( arq ) ;
46
47 /* acrescenta " _dados " ao nome_do_dicionario */
48 strcpy ( dic . nome_arq_dados , nome_do_dicionario ) ;
49 strcat ( dic . nome_arq_dados , " _dados " ) ;
50
51 /* criacao do arquivo de dados */
52 if ( ( arq = fopen ( dic . nome_arq_dados , " wb " ) ) == NULL ) {
53 printf ( " Erro na criacao de arquivos ! Abortando ... " ) ;
54 exit (1) ;
55 }
56 fclose ( arq ) ;
57
58 dic . tam = 0;
59
60 return ( dic ) ;
61 }
62
63 /* funcao que carrega um dicionario */
64 void carrega_dic ( TDicionario * dic , char * nome_do_dicionario )
254 CAPÍTULO 8. ARQUIVOS
65 {
66 FILE * arq ;
67 int i ;
68
69 /* acrescenta " _dados " ao nome_do_dicionario */
70 strcpy ( dic - > nome_arq_dados , nome_do_dicionario ) ;
71 strcat ( dic - > nome_arq_dados , " _dados " ) ;
72
73 /* acrescenta " _indice " ao nome_do_dicionario */
74 strcpy ( dic - > nome_arq_indices , nome_do_dicionario ) ;
75 strcat ( dic - > nome_arq_indices , " _indice " ) ;
76
77 /* abertura do arquivo de indices */
78 if ( ( arq = fopen ( dic - > nome_arq_indices , " rb " ) ) == NULL ) {
79 printf ( " Erro na abertura de arquivos ! Abortando ... " ) ;
80 exit (1) ;
81 }
82
83 /* leitura e contagem do numero de entradas no arquivo de indice */
84 dic - > tam = 0;
85 for ( i =0; ! feof ( arq ) ; i ++) {
86 fread (& dic - > matriz_indices [ i ] , sizeof ( indice_dic ) , 1 , arq ) ;
87 dic - > tam ++;
88 }
89 }
90
91 /* funcao que busca uma chave no dicionario passado . Se encontrar
92 retornara 1 e retornara seu valor no argumento retorno */
93 int busca_dic ( TDicionario * dic , char * chave , char * retorno )
94 {
95 int i ;
96 FILE * arq ;
97
98 entrada_dic entrada ;
99
100 /* procura na tabela de indices em memoria a chave */
101 for ( i =0; i < dic - > tam ; i ++) {
102 if (! strcmp ( dic - > matriz_indices [ i ]. chave , chave ) ) {
103 /* se estou aqui , entao encontrei a palavra ! */
104
105 if ( ( arq = fopen ( dic - > nome_arq_dados , " rb " ) ) == NULL ) {
106 printf ( " Erro na abertura de arquivos ! Abortando ... " ) ;
107 exit (1) ;
108 }
109 /* posiciona , le e retorna o valor da chave */
110 fseek ( arq , i * sizeof ( entrada_dic ) , SEEK_SET ) ;
111 fread (& entrada , sizeof ( entrada_dic ) , 1 , arq ) ;
112 strcpy ( retorno , entrada . valor ) ;
113 fclose ( arq ) ;
114 return 1;
115 }
8.8. EXERCICIOS RESOLVIDOS 255
116 }
117 /* se cheguei ate aqui , entao nao encontrei a palavra */
118 return 0;
119 }
120
121 /* funcao que insere , no dicionario , a chave ( palavra ) passada
122 e seu valor . Retorna 1 se inserir , 0 senao */
123 int insere_dic ( TDicionario * dic , char * chave , char * valor )
124 {
125 char pega_valor [ TAM_VALOR ];
126 FILE * arq ;
127 entrada_dic entrada ;
128 indice_dic chave_ind ;
129
130 if ( ! busca_dic ( dic , chave , pega_valor ) ) {
131 /* Se a chave nao existe no dic . , anexo ao arquivo de dados */
132
133 if ( ( arq = fopen ( dic - > nome_arq_dados , " ab " ) ) == NULL ) {
134 printf ( " Erro na abertura de arquivos ! Abortando ... " ) ;
135 exit (1) ;
136 }
137
138 strcpy ( entrada . chave , chave ) ;
139 strcpy ( entrada . valor , valor ) ;
140
141 fwrite (& entrada , sizeof ( entrada_dic ) , 1 , arq ) ;
142 fclose ( arq ) ;
143
144 /* insere ao final da tabela e arquivo de indices */
145
146 strcpy ( dic - > matriz_indices [ dic - > tam ]. chave , chave ) ;
147 dic - > matriz_indices [ dic - > tam ]. indice = dic - > tam ;
148 dic - > tam ++;
149
150 if ( ( arq = fopen ( dic - > nome_arq_indices , " ab " ) ) == NULL ) {
151 printf ( " Erro na abertura de arquivos ! Abortando ... " ) ;
152 exit (1) ;
153 }
154
155 strcpy ( chave_ind . chave , chave ) ;
156 chave_ind . indice = dic - > tam ;
157 fwrite (& chave_ind , sizeof ( indice_dic ) , 1 , arq ) ;
158 fclose ( arq ) ;
159
160 return 1;
161 }
162 else printf ( " A chave passada ja existe no dicionario !\ n " ) ;
163 return 0;
164 }
165
166 int main ()
256 CAPÍTULO 8. ARQUIVOS
167 {
168 TDicionario dic , outro_dic ;
169 entrada_dic entrada ;
170
171 dic = inicializa_dic ( " meu_dicionario " ) ;
172
173 strcpy ( entrada . chave , " Espirito Santo " ) ;
174 strcpy ( entrada . valor , " estado brasileiro localizado na regiao Sudeste " ) ;
175 insere_dic (& dic , entrada . chave , entrada . valor ) ;
176
177 strcpy ( entrada . chave , " C " ) ;
178 strcpy ( entrada . valor , " linguagem de programacao de nivel medio " ) ;
179 insere_dic (& dic , entrada . chave , entrada . valor ) ;
180
181 carrega_dic (& outro_dic , " meu_dicionario " ) ;
182 strcpy ( entrada . chave , " C " ) ;
183 strcpy ( entrada . valor , " linguagem de programacao de nivel medio " ) ;
184 insere_dic (& outro_dic , entrada . chave , entrada . valor ) ;
185 }
de ı́ndices em memória, para então ser descoberto seu ı́ndice no arquivo de dados, e a dupla (chave,
informação) desejada finalmente ser carregada para memória.
8.9 Resumo
• Variáveis transientes são armazenadas na memória principal. Tem tempo de acesso rápido, mas
duração limitada: só existem enquanto o programa que as está utilizando estiver na memória.
Variáveis persistentes não perdem suas informações quando o programa que as criou não está
mais em memória, pois estão armazenadas em meio secundário: disco rı́gido, memória flash,
disquete, CD...
• Arquivos texto só armazenam caracteres, e são compreensı́veis ao serem lidos em um editor de
texto. Já arquivos binários armazenam variáveis no formato em que elas são armazenadas em
memória;
• Um arquivo deve ser aberto para poder ser utilizado, e após essa utilização, deve ser fechado;
• As variáveis persistentes podem ser escritas e lidas dos arquivos. Existem funções especı́ficas
para essas tarefas, algumas designadas para arquivos binários e outras para arquivos texto;
• A linguagem C oferece uma série de funções para lidar com arquivos, e esse capı́tulo apre-
sentou algumas, como fopen(), usada para abrir arquivos, e fread(), usada para ler variáveis
persistentes de arquivos binários.
8.10 Exercı́cios
1. Escreva um programa em C que abra um arquivo texto e que conte a quantidade de caracteres
armazenados nele. Imprima o número na tela. O programa deve solicitar ao usuário que digite
o nome do arquivo.
3. Considere um arquivo texto que armazene números em ponto flutuante em cada uma de suas
linhas. Ou seja, o arquivo inicia com uma quantidade de um ou mais caracteres representando
números inteiros, então segue um caractere de ponto (’.’) e mais alguns inteiros; em seguida,
uma quebra de linha; na próxima linha esse formato prossegue, até que, no fim da última linha,
não ocorre quebra de linha. Escreva um programa em C que determine o valor máximo, o valor
mı́nimo e a média desses valores armazenados no arquivo. Imprima esses valores na tela.
4. Considere o programa escrito para a questão anterior. Exiba na tela, agora, o valor n corres-
pondente à quantidade de números reais contidos no arquivo e o valor máximo, mı́nimo e a
258 CAPÍTULO 8. ARQUIVOS
média calculados anteriormente, porém agora divididos por n. Depois, gere um novo arquivo
que contenha cada um dos números reais do arquivo divididos por n, um por linha. Procure
reutilizar, nessa questão, o maior número possı́vel das funções que você tenha construı́do para
a questão anterior. Modularizar seu código (usar funções) é uma forma de organização e de
poupar trabalho duplicado.
5. Considere um arquivo texto que armazene caracteres variados, ou seja, um texto digitado.
Escreva um programa que o leia e gere um novo arquivo que contenha somente as letras do
arquivo original, na ordem em que lá aparecem (ou seja, caracteres de A-Z ou a-z).
6. Para um arquivo do mesmo tipo do da questão anterior (que armazene caracteres variados),
escreva um programa em C que determine a média dos comprimentos de todas as palavras
que se encontram nele. Entende-se por palavra um conjunto de caracteres de letras que está
separado de outros conjuntos de caracteres no arquivo por um (ou mais) caractere de espaço
em branco ( ’ ’, tabulações ou quebra de linha); e seu comprimento será a quantidade de
caracteres que o formam.
7. Considere um arquivo texto como o usado na questão anterior. Faça um programa que o leia
e que permita ao usuário consultar uma das linhas do arquivo, solicitando a ele que informe
o ı́ndice n dessa linha. O programa deve imprimir a linha especificada ou a mensagem de que
ela não existe.
8. Escreva um programa em C que receba via teclado o nome de um arquivo texto e uma palavra.
O programa deve imprimir todas as linhas que possuem essa palavra. Dica: procure usar
algumas funções que você tenha construı́do para a questão anterior; provavelmente, algumas
delas poderão ser reaproveitadas.
9. Escreva um programa em C que receba via teclado o nome de um arquivo texto. O programa
deve solicitar ao usuário que digite o ı́ndice da linha inicial e da linha final, e o programa deve
imprimi-las e todas as linhas entre elas. Se o ı́ndice superior de linhas não existe, esse erro deve
ser informado ao usuário (mas as linhas existentes devem, ainda, ser impressas). Novamente,
procure usar algumas funções que você tenha escrito para as questões anteriores.
10. Considere, agora, os números inteiros armazenados num arquivo texto como os usados nas
questões anteriores. Entende-se por um número inteiro nesse arquivo um conjunto de caracteres
representando números inteiros (ou seja, no intervalo 0-9) separados de outros conjuntos de
caracteres por um (ou mais) caractere de espaço em branco ( ’ ’, tabulações ou quebras de
linha). Escreva um programa em C que produza dois arquivos texto: o primeiro com os números
pares da seqüência original e o segundo com os números impares. Os arquivos de saı́da devem
conter um número por linha.
8.10. EXERCÍCIOS 259
11. Escreva um programa em C que leia um texto fornecido pelo usuário via teclado e o armazene
em um arquivo. O fim da entrada de texto é sinalizada por um ponto no inı́cio de uma nova
linha. O ponto utilizado para marcar o fim da entrada de texto não deve aparecer no arquivo
gerado pelo programa.
12. Considere um arquivo que apresente o nome de um aluno na primeira linha; na seguinte a
nota de sua primeira prova; e na seguinte a nota de sua segunda prova. Essas 3 linhas de
informações se repetem no total de alunos no arquivo de entrada. Faça um programa que
imprima os nomes de todos os alunos que têm a média das duas notas menor que 7.0.
13. Escreva um programa que leia um arquivo texto contendo linhas de dados. Em cada linha do
arquivo há o nome de um aluno e duas notas. Esses dados estão separados por ponto e vı́rgula.
Existe um ponto e vı́rgula no final de cada linha. O programa deve ler esses dados e imprimir
os valores lidos, a média das duas notas, e se o aluno foi aprovado ou não (com nota maior ou
igual a 5).
14. Escreva um programa em C que solicita ao usuário a digitação de um nome de arquivo. Esse
arquivo será criado e o usuário informará números em ponto flutuante, que serão gravados no
arquivo. O arquivo será um arquivo binário. Considere que ele pode armazenar no máximo 50
valores, mas que o usuário pode encerrar a entrada de novos valores com a digitação de um
número negativo.
15. Escreva um programa em C que receba o nome de um arquivo binário, o qual armazena números
em ponto flutuante, no mesmo formato que na questão anterior. O programa deve ler todos
esses valores e gravá-los na memória; então, ordená-los, e regravá-los num arquivo texto, com
um número por linha.
16. Escreva um programa em C que receba via teclado o nome de um arquivo binário. Esse arquivo
armazenará nome, idade e sexo de pessoas, como no TAD TFuncionario. O programa deve
solicitar ao usuário a digitação desses dados e armazená-los no arquivo depois. O programa
termina quando o usuário digitar enter na entrada do nome, ou seja, informar um nome vazio.
17. Escreva um programa em C que receba via teclado o nome do arquivo binário da questão
anterior. Então, ele deve abrir o arquivo. Deve existir um menu que ofereça ao usuário a
possibilidade de poder: 1) imprimir (na tela) todos os registros lá contidos; 2) acessar os
dados de uma pessoa passando o ı́ndice dela no arquivo (1 para a primeira pessoa no arquivo,
5 para a quinta pessoa no arquivo...); 3) o programa deve informar a quantidade total de
homens e mulheres armazenadas no arquivo; 4) O programa deve permitir ao usuário digitar
novas pessoas. Esses novos dados digitados devem ser gravados no arquivo (e posteriormente
descarregados da memória) somente quando o usuário escolher, no menu, a opção para sair
do programa.
260 CAPÍTULO 8. ARQUIVOS
[1] BRIAN KERNIGHAN and DENNIS RITCHIE. The C Programming Language. second edition,
1988.
261