Anda di halaman 1dari 261

Sumário

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

2.8 Comando de Saı́da de Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36


2.9 Comandos de Seleção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.9.1 Comando de seleção simples . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.9.2 Comando de seleção dupla . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.9.3 Comando de seleção múltipla . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.10 Comandos de Repetição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.10.1 Comando de repetição com pré-condição . . . . . . . . . . . . . . . . . . . 49
2.10.2 Comando de repetição com pós-condição . . . . . . . . . . . . . . . . . . . 52
2.10.3 Comando de repetição condensado . . . . . . . . . . . . . . . . . . . . . . 55
2.11 Problema dos Lotes Encaixantes . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.12 Exercı́cios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.13 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.14 Exercı́cios Propostos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

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

4 Tipos Abstratos de Dados 109


4.1 Técnicas de Programação Top-down e Bottom-up . . . . . . . . . . . . . . . . . . 109
4.2 Tipos Compostos Heterogêneos (Estruturas) . . . . . . . . . . . . . . . . . . . . . 110
4.2.1 Definição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
4.2.2 Uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.3 Tipos Abstratos de Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
4.3.1 Definição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
SUMÁRIO 3

4.3.2 Definição de Atributos de um TAD . . . . . . . . . . . . . . . . . . . . . . 120


4.3.3 Definição de Operações de um TAD . . . . . . . . . . . . . . . . . . . . . 120
4.3.4 Uso do TAD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
4.3.5 Tipos de TADs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.4 Exercı́cios Resolvidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.5 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
4.6 Lista de Exercı́cios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
4.7 Trabalhos Sugeridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

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

8.8.1 O Tipo Abstrato de Dados TDicionario . . . . . . . . . . . . . . . . . . . 250


8.9 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
8.10 Exercı́cios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
8.11 Trabalhos Sugeridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
Capı́tulo 1

Introdução
Co-autor:

André Boechat

Objetivos:

• Apresentar um breve histórico da Computação;

• Apresentar noções sobre arquitetura de computadores, como funcionam o processador e a


memória;

• Definir o que são algoritmos e programas, além de apresentar algumas técnicas para de-
senvolvê-los;

• Definir as partes de um programa e a importância da documentação;

• Introduzir o conceito de tradução de programas.

Basicamente, pode-se considerar a programação de computadores como a forma que se dá


o relacionamento entre a máquina e o homem. Para programar de forma correta e eficiente,
muitas vezes não basta conhecer apenas os comandos de determinadas operações, é necessário
saber como a máquina, um ser não pensante, faz para compreender e executá-los. Assim, este
capı́tulo visa apresentar alguns conceitos fundamentais para o entendimento da programação,
desenvolvidos juntamente com a área mais recente da ciência, a Computação.

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 Arquitetura de Computadores


Para conhecer um pouco mais sobre o funcionamento dos computadores, é apresentada nesta
Seção uma noção básica sobre processadores e memórias.

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:

• Armazena dados e instruções do programa em execução;

• Proporciona ao computador acesso rápido aos dados e instruções armazenados por ela;

• Só mantem as informações armazenadas enquanto o computador estiver ligado.

A memória secundária geralmente possui maior capacidade de armazenamento de dados e


instruções, porém o tempo necessário para acessá-los é bem maior quando comparado ao da
memória principal. As informações permanecem armazenadas mesmo após o desligamento do
computador. Discos rı́gidos e CD-ROMs são exemplos de memórias secundárias.
A unidade básica da memória é o digito binário, conhecido como “bit”. Um bit pode assumir
apenas dois valores, normalmente 0 ou 1.
A memória é formada por um conjunto de células, ou posições, sendo que cada uma pode
guardar uma informação e possui um número de reconhecimento, conhecido como “endereço
de memória”. É por meio desse número de endereço que os programas podem acessar uma
determinada célula. Se uma célula possuir k bits, ela poderá armazenar qualquer uma das 2k
combinações possı́veis para os bits. A Figura 1.1 mostra um exemplo de memória com seis
células endereçáveis, onde cada célula possui dezesseis bits.

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

Figura 1.1: Exemplo de uma memória de 16 bits e 6 endereços.

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.

1.3 Algoritmos e Programas


Apesar do nome pouco comum, diversos algoritmos são executados por pessoas comuns todos os
dias. Ao escovar os dentes, fazer um bolo ou trocar o pneu de um carro, diversos procedimentos
são feitos em seqüência, seguindo, muitas vezes, uma certa ordem lógica.
Para exemplificar a execução de um algoritmo, descreve-se a seguir alguns passos necessários
para fazer algo simples, cotidiano, como o ato de escovar os dentes.

1. Pegar a escova e a pasta de dentes;

2. Colocar um pouco de pasta sobre as cerdas da escova;

3. Escovar os dentes do maxilar inferior;

4. Escovar os dentes do maxilar superior;

5. Expelir da boca o excesso de espuma;

6. Bochechar um pouco de água;

7. Lavar a escova e guardá-la;

8. Enxugar o rosto.

Após conhecer um exemplo comum de algoritmo, torna-se mais fácil compreender a sua definição:

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 deter-
minado problema.
10 CAPÍTULO 1. INTRODUÇÃ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:

1. Onde está a escova a ser usada?

2. Quanto, exatamente, se deve colocar de pasta sobre as cerdas?

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.

“Embora os computadores modernos sejam capazes de executar operações difı́ceis e


complexas, eles são máquinas dóceis e de uma inteligência restrita. Eles devem ser
ensinados exatamente o que fazer, e as instruções devem ser feitas em uma linguagem
simples, precisa e limitada que eles possam compreender. Estas instruções são mais
conhecidas como programa” (Darmell,1988).
1.4. TÉCNICA DE DESENVOLVIMENTO DE PROGRAMAS 11

1.4 Técnica de Desenvolvimento de Programas


Na maioria das vezes, o ato de programar não é uma tarefa simples e fácil. A sintaxe das
linguagens de programação é bem rı́gida e deve ser respeitada fielmente para que o computador a
compreenda. Esquecer de um “;” no lugar onde é necessário, por exemplo, significa comprometer
a execução de todo o programa (na verdade, o computador pode nem começar a executá-lo). Ou
pior, um programa teoricamente correto, que não apresente erros de sintaxe, pode fornecer um
resultado incorreto. Encontrar o erro torna-se um enorme problema à medida que o programa
cresce em tamanho e complexidade.
Para contornar a complexidade dos programas, os construtores de software costumam fazer
uso de uma poderosa técnica de programação, conhecida como “técnica dos refinamentos suces-
sivos”5 . Ela consiste basicamente em dividir um processo complexo em processos-componentes
menores, especificando apenas a entrada e a saı́da de cada um deles. E se, mesmo assim,
alguns processos menores continuarem complexos, repete-se a divisão no interior dos processos-
componentes, gerando processos cada vez mais simplificados.
Além de facilitar a depuração de erros, essa técnica pode tornar um complexo programa em
uma “união” de programas menores e mais simples. A divisão também possibilita outra grande
vantagem, o reúso de código, ou seja, o uso de um mesmo trecho de programa para realizar o
mesmo tipo de tarefa em diferentes ocasiões dentro de um programa maior.
Um exemplo prático do uso dos refinamentos sucessivos segue abaixo:

=⇒ Algoritmo “Escovação dentária”:

1. Pegar a escova e a pasta de dentes;

2. Colocar um pouco de pasta sobre as cerdas da escova;

3. Escovar os dentes do maxilar inferior;

4. Escovar os dentes do maxilar superior;

5. Expelir da boca o excesso de espuma;

6. Bochechar um pouco de água;

7. Lavar a escova e guardá-la;

8. Enxugar o rosto.

=⇒ Processo-componente “pegar a escova e a pasta de dentes”:

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.

=⇒ Processo-componente “escovar os dentes do maxilar inferior”:

1. Com as cerdas da escova na posição vertical, fazer movimentos de vai-e-vem na região


superior dos dentes;

2. Com as cerdas na posição horizontal, escovar a região frontal dos dentes;

3. Com as cerdas na posição horizontal, escovar a região detrás dos dentes.

=⇒ Processo-componente “escovar a região detrás dos dentes”:

1. Abrir bem a boca;

2. Afastar a lı́ngua da região a ser escovada;

3. Esfregar as cerdas da escova atrás dos dentes.

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.

1.5 Partes de um Programa


Como dito anteriormente, os programas podem ser comparados à diversas atividades do dia-a-
dia de qualquer pessoa. Um exemplo é o de receitas culinárias. Geralmente, essas seguem uma
determinada estrutura, como abaixo:

• Nome da receita;

• Ingredientes: descreve todo o material necessário para o preparo da receita;

• Modo de preparo: descreve a forma de trabalhar com os ingredientes para que se obtenha
o resultado esperado;

• Comentários sobre certos procedimentos ou ingredientes a fim de detalhar alguma peculi-


aridade que o cozinheiro poderia não conhecer previamente.

A estrutura de um bom programa segue um modelo semelhante. Basicamente, um programa


deve conter quatro partes:

• Cabeçalho: contém informações sobre o programa, como o seu nome;


1.5. PARTES DE UM PROGRAMA 13

• Dicionário de dados: define quais são os dados manipulados pelo programa;

• Corpo: define os procedimentos que o programa deve executar;

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

Um dos principais objetivos deste tipo de estrutura é aumentar a legibilidade do programa, ou


seja, facilitar o entendimento dos diversos processos que o programa executa. Um programa deve
valorizar a formatação, disposição fı́sica das linhas de código, para facilitar seu entendimento.
Colocar cada comando em uma linha e inserir uma linha em branco entre blocos de comandos
de finalidade comum são regras básicas para uma boa formatação. Os programas devem ser
lidos e entendidos por quem os escreveu e por outras pessoas, uma vez que podem necessitar de
correção, manutenção, modificação ou apenas de serem compreendidos por um simples usuário.
Para ilustrar essa estrutura, apresenta-se a seguir um programa, Pseudocódigo 1.1, que efetua
o cálculo das raı́zes reais de uma equação de segundo grau. Os dados de entrada do programa
são os três coeficientes da equação e ele deverá apresentar ao usuário as raı́zes reais da mesma,
caso existam.

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.

Pseudocódigo 1.2 Processo componente para o cálculo das raı́zes.


Processo-componente "Cálculo das Raı́zes":
Verificar a exist^encia de raı́zes reais
Se possui raı́zes reais:
Exiba para o usuário os valores da raı́zes;
Sen~
ao:
Exiba um alerta sobre a n~ ao exist^
encia de raı́zes reais;
FIM-Processo componente "Cálculo das Raı́zes"
14 CAPÍTULO 1. INTRODUÇÃO

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

De acordo com a estrutura de programa apresentada no inı́cio desta seção, as linhas 13 a


18 compõem o dicionário de dados e o restante compõe o corpo do programa. A documentação
está inserida no programa em forma de comentários, como as linhas 1 a 7, 20, 23 e 26.
Vale destacar a importância da disposição fı́sica das linhas de código para o fácil entendimento
do programa. A hierarquização de elementos por meio de espaços em branco é chamada de
indentação. O trecho de programa relacionado ao cálculo das raı́zes no Exemplo 1.1 demonstra
como a indentação pode facilitar a visualização da mudança do fluxo do programa.
Normalmente, linhas de código com mesma indentação são executadas de maneira contı́nua,
uma após a outra. Uma mudança na indentação indica uma mudança nesse fluxo de execução
do programa. No próximo capı́tulo são apresentadas diversas maneiras de mudar esse fluxo.

1.6 Tradução de Programas


Apesar do que foi dito na Seção 1.3, para ser executado pelo computador, um programa ainda
precisa ser traduzido para uma linguagem mais simples que as linguagens de alto nı́vel: a lingua-
gem de máquina. Chama-se de “código fonte” o arquivo que contém o conjunto de instruções
escritas em uma determinada linguagem de programação, normalmente familiares ao programa-
dor. Já o arquivo que as contém traduzidas para linguagem de máquina é chamado de “código
objeto”.
Para efetuar essa tradução, é necessário aplicar um programa, ou conjunto de programas,
que receba o código fonte e gere o código objeto. A seguir, descrevem-se dois métodos básicos
para um programa tradutor efetuar essa tarefa: a compilação e a interpretação de um código
fonte.

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

Figura 1.2: Método de compilação. Figura 1.3: Método de interpretaçã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.

• A memória é responsável por armazenar os programas e os dados. A unidade básica da


memória é o bit.

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

• A Técnica de Refinamentos Sucessivos consiste, basicamente, em dividir sucessivamente


problemas complexos em diversos problemas menores e mais simples.

• 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

• Basicamente, existem dois mecanismos de tradução de programas para linguagem de


máquina: compilação e interpretação. Na compilação, o código fonte é totalmente tradu-
zido antes de ser executado. Já na interpretação, a tradução do código fonte e a execução
do programa ocorrem quase simultaneamente, como um processo único.

1.8 Exercı́cios Propostos


1. Qual é o papel do processador?

2. Qual é o papel da memória?

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?

4. Utilizando o conceito de algoritmo apresentado no capı́tulo, descreva, através de um al-


goritmo, o ato de trocar um pneu furado de um carro. Não se esqueça de estabelecer as
condições iniciais do problema (por exemplo, carro com um pneu dianteiro furado, se a
chave de roda está no carro, se o carro está parado em uma ladeira, etc) e as condições de
saı́da (por exemplo, carro parado e frenado, etc).

5. Use a Técnica de Refinamentos Sucessivos para detalhar as seguintes atividades:

(a) Montagem de uma barraca de camping;


(b) Preparação de um bolo de aniversário;
(c) Cálculo da distância entre dois pontos no plano cartesiano.
Capı́tulo 2

Conceitos Básicos
Co-autores:

André Boechat
Bruno Pandolfi

Objetivos:
• Apresentar o conceito de variável;

• Estudar os principais tipos de dados básicos e expressões válidas;

• Mostrar como é feita a definição de variáveis e constantes;

• Apresentar os comandos elementares usados em programas.


A idéia de resolver problemas formalizando a solução através do emprego de algoritmos é a
espinha dorsal da programação procedural, na qual as tarefas são prescritas ao computador por
meio de uma seqüência de operações básicas.
Este capı́tulo trata do estudo dos conceitos e operações elementares da programação, forne-
cendo uma base sólida que, mais adiante, serve para construir algoritmos progressivamente mais
complexos e resolver problemas cada vez mais elaborados.

2.1 Variáveis e Células de Memória


Qualquer algoritmo tem por finalidade produzir algum resultado útil para compor a solução de
um problema. Os algoritmos computacionais, em sua maioria, operam sobre um conjunto de
dados para produzir novos resultados de acordo com a necessidade da tarefa em questão.
Quando corretamente transcritos para uma linguagem de programação, os algoritmos são
chamados de programas e podem ser executados pelo computador. É necessário, todavia, arma-
zenar as informações utilizadas pelos programas em um local organizado e seguro, para que as
consultas e alterações sejam efetuadas de maneira coerente e livre de erros. Os computadores
utilizam a memória para armazenar os dados de um programa em execução.

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.

Pseudocódigo 2.2 Cálculo da conta de energia elétrica utilizando variáveis.


Descrição: programa que calcula o valor da conta de energia elétrica.
Dados de Entrada: a leitura atual, a anterior e o preço unitário do kWh.
Saı́da do Programa: valor da conta.
Subtrair o conteúdo da variável ‘leituraAtual’ do conteúdo da variável
‘leituraAnterior’ e guardar na variável ‘diferenca’;
Multiplicar o conteúdo da variável ‘diferenca’ pelo conteúdo da variável
‘valorUnitario’ e guardar na variável ‘valorConta’;

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.

possı́vel tradução é exibida na Figura 2.3.


Como visto nos exemplos desta seção, o uso de variáveis em um algoritmo reduz a quantidade
de comentários explicativos, pois sua leitura é mais simples e seu entendimento é mais fácil. Em
caso de erros, a correção do programa é muito mais rápida e eficiente.
Apresentados os conceitos de variável e células de memória, é válido abordar um outro
significado de programa. Trata-se de considerar os programas como sendo processos de mudança
de estados.
Nessa abordagem, os dados de entrada, escritos em suas respectivas variáveis, são conside-
rados o estado inicial do programa. A partir daı́, realiza-se uma seqüência de operações para
chegar a um estado final. A cada operação, diz-se que o programa está em um novo estado in-
termediário. O estado final é atingido quando a tarefa em questão é considerada como realizada.
A transição do estado inicial para o estado final pode ser efetuada de diversas maneiras.
Conforme pode ser visto mais adiante neste capı́tulo, é possı́vel escolher entre duas ou mais
seqüências de comandos, bem como repeti-las, de acordo com o estado intermediário em que o
programa se encontra.

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

Exemplo 2.1: Nomes válidos de variáveis, concordando com as regras de nomenclatura.

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

Exemplo 2.2: Nomes inválidos de variáveis.

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 simples: começados com letra minúscula e demais caracteres minúsculos;

• 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

Exemplo 2.3: Nomes significativos para variáveis.


2.3. COMANDO DE ATRIBUIÇÃO 23

2.3 Comando de Atribuição


Os pseudocódigos da Seção 2.1 não mostram como os conteúdos das variáveis de entrada são
alterados para que o cálculo seja efetuado coerentemente. Em outras palavras, não se mostra
como as variáveis leituraAtual, leituraAnterior e valorUnitario são inicializadas. O responsável
pela alteração dessas variáveis, assim como de qualquer outra, é o comando de atribuição.
O comando de atribuição é muito importante e, com certeza, é o mais utilizado. É por meio
dele que as variáveis podem ter seus valores (conteúdos) alterados. Por isso, o comando de
atribuição está diretamente ligado às operações de memória.
No Exemplo 2.4 é transcrito o algoritmo do cálculo da conta de energia elétrica para o código
de um programa na linguagem C.

1 main () {
2 leituraAtual = 125;
3 leituraAnterior = 25;
4 valorUnitario = 2;
5 diferenca = leituraAtual - leituraAnterior ;
6 valorConta = diferenca * valorUnitario ;
7 }

Exemplo 2.4: Cálculo do valor da conta de energia elétrica.

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

Figura 2.4: Representação gráfica da linha 4 do Exemplo 2.4.

É muito importante compreender a diferença significativa existente entre o comando de atri-


buição e a igualdade matemática. Em muitas linguagens de programação (C, por exemplo) a
atribuição é também representada pelo sı́mbolo “=”, o que pode provocar interpretações equi-
vocadas. O Exemplo 2.5 mostra um trecho de programa que geralmente causa confusão para os
programadores iniciantes.

1 contador = 10;
2 contador = contador + 1;

Exemplo 2.5: Incremento de uma variável usando o comando de atribuição.

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.

Figura 2.5: Representação gráfica do incremento de uma variável.

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

2.4 Tipos de Dados


Um tipo de dados delimita o conjunto de valores possı́veis que uma determinada variável pode
representar. Além disso, define as operações básicas possı́veis para suas variáveis.
Sua necessidade decorre do fato de que as células de memória, sozinhas, conseguem represen-
tar apenas conjuntos de dados muito limitados. Entretanto, a maioria dos valores de interesse
pertence a conjuntos extensos, os quais não podem ser representados com o uso de apenas uma
célula de memória.
Os programadores, então, passaram a utilizar múltiplas células para representar um único
valor. Essa atitude resolveu a dificuldade de armazenar dados complexos, mas criou um outra
questão: os processos de leitura e escrita para valores de múltiplas células, bem como as de-
mais operações sobre tais valores, exigiam códigos que, além de não-triviais, fugiam do foco do
problema principal.
Não demorou para que as linguagens de programação incorporassem a definição dos tipos
de dados e suas operações essenciais, poupando os programadores da tarefa de defini-los em seu
código e tornando-o mais enxuto e legı́vel.
Tipos de dados, portanto, abstraem a forma de implementação e disponibilizam operações
sobre os elementos do conjunto para que o programador possa utilizá-las sem ter que implementá-
las.
Os tipos básicos mais importantes são o tipo inteiro, o ponto flutuante, o booleano e o tipo
caractere. Cada linguagem de programação possui certas particularidades em relação aos tipos
de dados.
Primeiramente, é preciso abordar um conceito imprescindı́vel da escrita de programas em
linguagem C: a declaração de variáveis. Em seguida, são descritas as caracterı́sticas mais comuns
dos tipos básicos principais.

2.4.1 Declaração de Variáveis


De acordo com o problema abordado, diferentes conjuntos de dados são necessários. Conforme
visto, os tipos de dados representam os conjuntos de valores possı́veis para suas variáveis, assim
como as operações válidas sobre elas. Também já foram abordadas a necessidade e as vantagens
do uso de variáveis em um programa. Entretanto, ainda não foi mencionado como as variáveis
são criadas, ou seja, como o programador declara formalmente uma variável em seu código.
Na linguagem C, o ato de declarar uma variável é simples e imprescindı́vel. Afinal, sem
esse procedimento, não é possı́vel usá-la. Quando a declaração de variáveis é esquecida, uma
mensagem de erro é apresentada ao programador, e a compilação do código é interrompida. Por
isso, toda variável deve ser declarada antes de ser utilizada.
A declaração de variáveis tem uma estrutura simples e algumas regras básicas. Em primeiro
lugar, escreve-se o nome do tipo de dados ao qual a variável pertence. Em seguida, separado
por um espaço em branco, escreve-se o identificador da variável.

<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 float valorFracionado , saldo , salarioMinimo ;


2 char letra , ultimaLetra ;
3 int entrada1 , entrada2 , entrada3 ;

Exemplo 2.6: Declaração múltipla de variáveis em uma mesma linha de código.

2.4.2 Tipo Inteiro


O tipo inteiro, como o próprio nome sugere, representa um intervalo finito do conjunto ma-
temático dos números inteiros.
De fato, uma linguagem de programação pode definir vários tipos inteiros, cada um com
intervalo diferente. Na linguagem C, por exemplo, existem vários tipos inteiros, tais como o int,
long int, unsigned int, short int. Neste livro, para bem da simplicidade, usa-se apenas o tipo
int.
O intervalo de valores para o tipo int depende do compilador que está sendo empregado.
Assumimos aqui que o intervalo do tipo int sempre varia de entre −32768 a 32767.
Em C, como na maioria das linguagens, as operações sobre as variáveis do tipo inteiro já
estão implementadas e à disposição do programador. As operações disponı́veis são as de soma,
subtração, multiplicação, divisão e resto de divisão, conforme o Exemplo 2.7.
O uso dessas funções é bastante simples e intuitivo, pois há até certa relação com os sı́mbolos
da escrita matemática. No Exemplo 2.7, as variáveis x e y são relacionadas entre si e os resultados
são armazenados nas variáveis de a a f.
Os operadores de soma e subtração funcionam tal qual na aritmética. O operador de sub-
tração pode ser aplicado para um único valor quando se deseja inverter seu sinal. A linha 12 do
Exemplo 2.7 mostra essa situação.
Um destaque deve ser dado ao operador de multiplicação. Seu único sı́mbolo é “*” (não há
o sı́mbolo “×” ou o “·”). Além disso, em oposição ao que ocorre na aritmética, a escrita do “*”
não pode ser omitida. Logo, a operação da linha 13 do Exemplo 2.7 geraria erro se não estivesse
comentada, pois o compilador trataria xy como uma variável não declarada.

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 }

Exemplo 2.7: Operações básicas com o tipo inteiro int.

Também é importante destacar o resultado da operação de divisão. No caso do Exemplo


2.7, o resultado correto da divisão de x — que armazena o valor 10 — por y — cujo valor é 3
— é a dı́zima periódica 3, 333 . . . Entretanto, como as variáveis envolvidas na linha 10 são todas
do tipo inteiro, o compilador trata de truncar o resultado, desconsiderando a parte decimal e
fornecendo 3 como resultado da operação.
Ainda no Exemplo 2.7, deve-se notar o uso do operador de resto de divisão inteira. Seu
sı́mbolo — “%” — deve ficar entre os valores do dividendo e do divisor. A linha 11 mostra a
obtenção do resto da divisão de x por y. Após a execução dessa operação, a variável e guardará
o valor 1, que é o resto da divisão de 10 por 3.

2.4.3 Tipo Ponto Flutuante


Esse tipo de dados representa um intervalo dos números racionais. Assim como o tipo inteiro,
há mais do que um intervalo possı́vel, dependendo da precisão escolhida.
Na linguagem C, os tipos de ponto flutuante são o float e o double. Neste livro, apenas o
tipo float é empregado.
Dependendo do compilador utilizado, os tipos de dados de ponto flutuante apresentam dife-
rentes precisões. Aqui se considera que o tipo float provê seis casas decimais de precisão.
As operações pré-definidas são as mesmas que as disponı́veis para o tipo int, exceto, é claro,
pela inexistência do operador de resto de divisão. O Exemplo 2.8 mostra o uso de variáveis do
tipo float em um programa.

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 }

Exemplo 2.8: Operações com variáveis do tipo float.

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.

2.4.4 Tipo Booleano


O tipo booleano é o tipo de dados mais simples. Ele contém apenas dois valores possı́veis:
verdadeiro e falso. É usado principalmente quando se precisa verificar condições no programa,
em expressões lógicas (Seção 2.6.3) e relacionais (Seção 2.6.2).
Na linguagem C não há uma representação especı́fica para esse tipo de dados e são utilizados
valores inteiros para codificá-lo. Desta forma, todo valor inteiro diferente de zero é considerado
valor “verdadeiro”. O valor zero é considerado “falso”.

2.4.5 Tipo Caractere


Como o próprio nome indica, o tipo caractere registra os códigos para letras, números e caracteres
especiais ($, &, #, @ etc). Normalmente, esses códigos e caracteres variam para as diversas
regiões do planeta, conforme suas particularidades linguı́sticas. Um padrão largamente utilizado
atualmente, pricipalmente no Ocidente, é o ASCII (American Standard Code of Information
Interchange — Padrão Americano para Intercâmbio de Informações), especificado pelo ANSI
(American National Standars Institute — Instituto Americano Nacional de Padrões).
O padrão ASCII é uma tabela. Cada posição da tabela simboliza um caractere. Na linguagem
C, o tipo char é empregado para conter o ı́ndice para a posição da tabela que codifica um
determinado caractere.
Originalmente, essa tabela era composta por apenas 128 sı́mbolos, os quais incluı́am todos os
caracteres alfanuméricos do Inglês, sinais de pontuação e alguns outros sı́mbolos. Os primeiros 32
caracteres eram utilizados para controle de fluxo de dados (em comunicações entre o computador
e seus periféricos) e não imprimı́veis. Atualmente, esses caracteres caı́ram em desuso. A Tabela
2.1 mostra os 128 caracteres da tabela ASCII original.
2.4. TIPOS DE DADOS 29

Código Caractere Código Caractere Código Caractere Código Caractere


0 NUL (null) 32 SPACE 64 @ 96 `
1 SOH (start of heading) 33 ! 65 A 97 a
2 STX (start of text) 34 ” 66 B 98 b
3 ETX (end of text) 35 # 67 C 99 c
4 EOT (end of transmission) 36 $ 68 D 100 d
5 ENQ (enquiry) 37 % 69 E 101 e
6 ACK (acknowledge) 38 & 70 F 102 f
7 BEL (bell) 39 ’ 71 G 103 g
8 BS (backspace) 40 ( 72 H 104 h
9 HT (horizontal tab) 41 ) 73 I 105 i
10 LF (NL line feed,new line) 42 * 74 J 106 j
11 VT (vertical tab) 43 + 75 K 107 k
12 FF (NP from feed,new page) 44 , 76 L 108 l
13 CR (carriage return) 45 - 77 M 109 m
14 SO (shift out) 46 . 78 N 110 n
15 SI (shift in) 47 / 79 O 111 o
16 DLE (data link escape) 48 0 80 P 112 p
17 DC1 (device control 1) 49 1 81 Q 113 q
18 DC2 (device control 2) 50 2 82 R 114 r
19 DC3 (device control 3) 51 3 83 S 115 s
20 DC4 (device control 4) 52 4 84 T 116 t
21 NAK (negative acknowledge) 53 5 85 U 117 u
22 SYN (synchronous idle) 54 6 86 V 118 v
23 ETB (end of trans. block) 55 7 87 W 119 w
24 CAN (cancel) 56 8 88 X 120 x
25 EM (end of medium) 57 9 89 Y 121 y
26 SUB (substitute) 58 : 90 Z 122 z
27 ESC (escape) 59 ; 91 [ 123 {
28 FS (file separator) 60 < 92 \ 124 |
29 GS (group separator) 61 = 93 ] 125 }
30 RS (record separator) 62 > 94 ˆ 126 ˜
31 US (unit separator) 63 ? 95 127 DEL

Tabela 2.1: Os 128 caracteres da tabela ASCII original.

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 }

Exemplo 2.9: Uso de variáveis do tipo caractere.

2.4.6 Conversão de Tipos


Agora, conhecidas algumas informações sobre os tipos de dados básicos, torna-se mais simples
compreender o problema existente na atribuição entre tipos diferentes, mencionada na Seção
2.3.
Considera-se que os tipos int, float e char ocupam a quantidade de memória representada
na Figura 2.6.

Figura 2.6: Tamanho da memória utilizada pelos tipos de dados básicos.

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 }

Exemplo 2.10: Atribuição entre tipos diferentes com perda de informação.

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 }

Exemplo 2.11: Definição de constantes.

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.6.1 Expressões Aritméticas


A expressão aritmética é aquela cujos operadores e operandos são de tipos numéricos (int ou
float, por exemplo).
Nas operações aritméticas, é guardada sempre a seguinte relação de prioridade:

1. Multiplicação, divisão e resto de divisão;

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.

No Exemplo 2.12, é importante notar que, no comando de atribuição à variável x, a multi-


plicação 10 × 10 é executada primeiro. Na linha seguinte, a soma 7 + 3 é executada primeiro
e seu valor é, em seguida, multiplicado por 5 e atribuı́do à variável y. A análise da linha 6 é
deixada para o leitor.
Além das operações básicas já citadas, é possı́vel usar, nas expressões aritméticas, outras
operações bastante comuns na matemática, definidas dentro da linguagem na forma de funções.
A sintaxe geral para o uso dessas funções é a seguinte:

<nome da funç~
ao>(<valor>)

Algumas funções matemáticas são mostradas na Tabela 2.2.


O Exemplo 2.13 exibe vários usos de funções matemáticas e expressões aritméticas.
2.6. EXPRESSÕES 33

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

Tabela 2.2: Principais funções matemáticas já definidas na linguagem C.

1 # include < math .h >


2
3 # define PI 3.1415
4
5 main () {
6 float a , b , c , delta , raiz1 , raiz2 ;
7 float x , y , z ;
8
9 a = 10;
10 b = 50;
11 c = 30;
12
13 delta = b * b - 4 * a * c ;
14 raiz1 = -b + sqrt ( delta ) / (2 * a ) ;
15 raiz2 = -b - sqrt ( delta ) / (2 * a ) ;
16
17 x = sin ( - PI / 2) ;
18 y = fabs ( x ) ;
19 z = pow (y ,2) ; // y elevado à pot^
e ncia 2
20 }

Exemplo 2.13: Uso de funções matemáticas pré-definidas.

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

2.6.2 Expressões Relacionais


As expressões relacionais comparam valores entre si. Tal qual se faz na prática, o operador
relacional trabalha com dois valores: o da expressão que está a sua direita e o da expressão que
está a sua esquerda. É importante destacar que uma variável ou uma constante isolada, por si
só, pode ser considerada uma expressão.
A Tabela 2.3 mostra exemplos de operadores relacionais da linguagem C e alguns casos de
uso.
Sı́mbolo Nome Exemplo
< menor que a < 10
<= menor ou igual a x <= y
== igual a 4 == 2*2
> maior que t>0
>= maior ou igual a delta >= 0
!= diferente de x != 9+8*8

Tabela 2.3: Operadores relacionais.

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.

2.6.3 Expressões Lógicas


As expressões lógicas são utilizadas para relacionar os resultados de um conjunto de operações
relacionais. Elas realizam as principais operações da lógica booleana e, na linguagem C, têm
como operadores:

• “&&” (AND): Realiza o “E” lógico;

• “||” (OR): Realiza o “OU” lógico;

• “!” (NOT): Realiza a negação.

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

Tabela 2.4: Todas as combinações possı́veis para os operadores lógicos.

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.

2.7 Comando de Entrada de Dados


O comando de entrada de dados serve para captar do usuário do programa um ou mais valores
necessários para a execução das tarefas. Na verdade, o que se faz é ler os dados de uma fonte
externa, normalmente do teclado, para depois usá-los de alguma maneira dentro do programa.
Por meio desse comando de leitura de dados, uma ou mais variáveis podem ter seus valores
lidos durante a execução do programa. Essa caracterı́stica permite criar programas mais flexı́veis.
Na linguagem C, a sintaxe para o uso do comando é a seguinte:

scanf("<formato1> <formato2> ... <formatoN>", &var1, &var2, ..., &varN);

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

Tabela 2.5: Códigos para formatos de dados do comando de entrada scanf .

Um exemplo claro da utilidade do comando de entrada de dados é o cálculo da conta de


energia elétrica. O exemplo apresentado até agora tinha os valores de consumo e do preço do
quilowatt-hora escritos diretamente no código. Isso é bastante incômodo, pois, quando se quer
calcular o valor de uma conta diferente, o código deve ser alterado e recompilado.
Uma maneira de tornar o programa mais flexı́vel é utilizar o comando de entrada de dados.
Com ele, é possı́vel digitar os valores desejados para as variáveis e efetuar os cálculos para
36 CAPÍTULO 2. CONCEITOS BÁSICOS

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.

1 # include < stdio .h >


2 main () {
3 int leituraAtual , leituraAnterior , diferenca ;
4 float valorUnitario , valorConta ;
5
6 scanf ( " % d % d % f " , & leituraAtual , & leituraAnterior , & valorUnitario ) ;
7 diferenca = leituraAtual - leituraAnterior ;
8 valorConta = diferenca * valorUnitario ;
9 }

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.

2.8 Comando de Saı́da de Dados


Até agora, os exemplos de códigos e os conceitos estudados não mostravam como os programas
comunicavam seus resultados ao usuário ou até mesmo a outros programas. Esse é o papel do
comando de saı́da de dados.
O comando de saı́da de dados serve para escrever mensagens e exibir valores de variáveis,
proporcionando mais informação e legibilidade tanto para o usuário quanto para o próprio pro-
gramador.
Na linguagem C, a sintaxe para o uso do comando de saı́da é mostrada a seguir.

printf("<formato1> <formato2> ... <formatoN>", var1, var2, ..., varN);

A estrutura do comando de saı́da de dados é bastante semelhante à do comando de entrada.


A principal diferença é sobre o uso do caractere “&”. No comando de saı́da, o caractere “&”
não deve ser escrito à frente dos identificadores das variáveis. A segunda parte do comando é
opcional, conforme é mostrado logo adiante nesta seção.
A Tabela 2.6 mostra alguns dos possı́veis formatos a serem usados.
Conforme já mencionado, o comando de saı́da é muito utilizado para exibir mensagens para
o usuário. Não é obrigatório que as mensagens contenham apenas valores ou caracteres armaze-
nados em variáveis, sendo possı́vel escrever textos como mensagem. O Exemplo 2.15 demonstra
essa possibilidade.
2.8. COMANDO DE SAÍDA DE DADOS 37

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 .

1 # include < stdio .h >


2
3 main () {
4 printf ( " Bom dia a todos ! " ) ;
5 printf ( " Outra mensagem " ) ;
6 }

Exemplo 2.15: Escrevendo mensagens para o usuário do programa.

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.

1 printf ( " Valor do saldo : % f " , saldo ) ;


2 printf ( " A metade de % d vale % d " , numero , metade ) ;
3 printf ( " O menor numero digitado foi % d " , menor ) ;

Exemplo 2.16: Escrevendo mensagens que incluem valores de variáveis.

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:

Valor da conta: R$ 187.000000

1 # include < stdio .h >


2
3 main () {
4 int leituraAtual , leituraAnterior , diferenca ;
5 float valorUnitario , valorConta ;
38 CAPÍTULO 2. CONCEITOS BÁSICOS

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.

Tabela 2.7: Exemplos de uso do caractere especial “\n”.

2.9 Comandos de Seleção

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

2.9.1 Comando de seleção simples


É possı́vel imaginar diversas situações cotidianas que se assemelham ao comportamento do
comando de seleção simples. Uma comparação clássica leva em conta a tomada de um desvio
durante uma viagem.
Quando se vai de carro para algum lugar, é normal se conhecer uma rota seqüenciada de
lugares intermediários pelos quais se passa antes de chegar ao destino. Entretanto, é possı́vel
que ocorra algum imprevisto e seja necessário tomar um desvio.
Há tarefas computacionais que também se deparam com tais cenários. Antes de exemplificar,
é melhor apresentar a estrutura do comando de seleção simplificado. Ela pode ser observada a
seguir.
if(<express~ao lógica>){
<seqü^
encia de comandos>
}
Não é difı́cil entendê-la. Quando o computador executa esse comando, o que é realizado em
primeiro lugar é o teste da expressão lógica. Se ela resultar “verdadeiro”, então a seqüência de
comandos é executada. Caso contrário, o programa segue executando normalmente as instruções
posteriores.
Desta forma, a decisão de desviar a execução normal do programa é condicionada ao resultado
da expressão lógica escrita no inı́cio do comando.
O uso do comando de seleção simples é exibido no Pseudocódigo 2.3, o qual descreve um
algoritmo que faz a leitura de dois números inteiros e os imprime como foram digitados e,
também, em ordem crescente.

Pseudocódigo 2.3 Ordena dois números inteiros digitados pelo usuário.


Descrição: Algoritmo para imprimir, em ordem crescente, dois números inteiros digitados pelo usuário.
Dados de Entrada: dois números inteiros.
Saı́da do Programa: números inteiros impressos na ordem em que foram digitados e em ordem cres-
cente.
Funç~
ao Principal:
Leia dois valores inteiros.
Imprima os valores coletados.
SE o primeiro número lido for maior que o segundo ENT~
AO
Ordene os valores.
FIM-SE
Imprima os valores ordenados.
FIM-Funç~
ao Principal

No Pseudocódigo 2.3, a operação SE-ENTÃO é executada tal qual o comando de seleção


simples if. O Exemplo 2.18 apresenta um código em linguagem C para o algoritmo do referido
pseudocódigo.
40 CAPÍTULO 2. CONCEITOS BÁSICOS

1 # include < stdio .h >


2 main () {
3 int num1 , num2 , aux ;
4
5 printf ( " Digite dois numeros inteiros separados por espaços : " ) ;
6 scanf ( " % d % d " , & num1 , & num2 ) ;
7 printf ( " Valores digitados : % d % d .\ n " , num1 , num2 ) ;
8
9 if ( num1 > num2 ) {
10 aux = num1 ;
11 num1 = num2 ;
12 num2 = aux ;
13 }
14 printf ( " Valores ordenados : % d % d .\ n " , num1 , num2 ) ;
15 }

Exemplo 2.18: Comando if simplificado.

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

Exemplo 2.19: Tentativa equivocada de trocar os valores de duas variáveis.

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.

2.9.2 Comando de seleção dupla


O comando de seleção dupla pode ser comparado com a bifurcação de um caminho. É preciso
escolher uma das alternativas e segui-la.
Em programação, o comando de seleção dupla serve para executar sempre uma de duas
alternativas de seqüências de instruções, de acordo com a verificação de uma condição. A
estrutura do comando na linguagem C é mostrada a seguir.

if (<express~ao lógica>){
<Seqü^
encia de comandos 1>
}else{
<Seqü^
encia de comandos 2>
}

Primeiramente, o computador testa a expressão lógica. Caso resulte “verdadeiro”, então a


seqüência de comandos 1 é executada. Caso contrário, a seqüência de comandos 2 é realizada.
Assim, apenas uma e sempre uma das seqüências de comandos é executada durante uma mesma
realização do comando de seleção dupla.
Após a operação de uma das seqüências de comando, o programa continua sua execução na
primeira linha de código após o conjunto de instruções do comando de seleção.
Para exemplificar o uso do comando de seleção dupla, o algoritmo do Pseudocódigo 2.4
mostra como se faz para decidir se um aluno está aprovado em uma disciplina, com base na
média de suas notas semestrais.
O comando SE-ENTÃO-SENÃO do Pseudocódigo 2.4 deve ser interpretado da mesma ma-
neira que é o comando de seleção dupla if. No Exemplo 2.20, é listado o código em C para o
algoritmo do pseudocódigo recém-mencionado.

1 # include < stdio .h >


2 main () {
3 int matricula ;
4 float n1 , n2 , n3 , mediaParcial ;
5
6 printf ( " Digite a matricula e as notas parciais do aluno : " ) ;
7 scanf ( " % d % f % f % f " , & matricula , & n1 , & n2 , & n3 ) ;
8 mediaParcial = ( n1 + n2 + n3 ) / 3.0;
42 CAPÍTULO 2. CONCEITOS BÁSICOS

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.

No Exemplo 2.20, as linhas 3 e 4 correspondem à declaração das variáveis; a linha 7, à


inicialização delas. Após o cálculo da média parcial das notas (linha 8), o comando de seleção
verifica se tal média é menor que 7, 0 e, assim, executa a instrução da linha 11 ou da linha 13.
Como pode ser visto no Exemplo 2.21, é possı́vel usar comandos de seleção de forma ani-
nhada, ou seja, pode-se colocar um comando de seleção dentro de uma das seqüências de co-
mandos de outro. Nesse programa, compara-se dois números inteiros digitados pelo usuário,
informando se foram digitados em ordem crescente, decrescente ou se são iguais. Antes, entre-
tanto, é bom observar o Pseudocódigo 2.5, que corresponde ao código do Exemplo 2.21.

1 # include < stdio .h >


2 main () {
3 int num1 , num2 ;
4
5 printf ( " Digite dois numeros inteiros quaisquer : " ) ;
6 scanf ( " % d % d " , & num1 , & num2 ) ;
7
8 if ( num1 > num2 ) {
9 printf ( " \ nOrdem decrescente . " ) ;
2.9. COMANDOS DE SELEÇÃO 43

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 }

Exemplo 2.21: Comando de seleção if aninhado em outro comando if.

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.

1 # include < stdio .h >


2 main () {
44 CAPÍTULO 2. CONCEITOS BÁSICOS

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.

1 # include < stdio .h >


2 main () {
3 int n1 , n2 , n3 ;
4
5 printf ( " Entre com os tres numeros separados por espacos : " ) ;
6 scanf ( " % d % d % d " , & n1 , & n2 , & n3 ) ;
7
8 if ( n1 > n2 ) {
9 if ( n1 > n3 ) {
10 printf ( " O maior numero digitado foi : % d . " , n1 ) ;
11 } else {
12 printf ( " O maior numero digitado foi : % d . " , n3 ) ;
13 }
14 } else {
15 if ( n2 > n3 ) {
16 printf ( " O maior numero digitado foi : % d . " , n2 ) ;
17 } else {
18 printf ( " O maior numero digitado foi : % d . " , n3 ) ;
19 }
20 }
21 }

Exemplo 2.23: Programa que identifica o maior de três números, usando o uso do comando de
seleção aninhado.

No caso do Exemplo 2.22, o uso de vários comandos if isolados acarreta em perda de


eficiência. Quando o primeiro número digitado pelo usuário for o maior, o programa realiza
dois testes desnecessários (linhas 12 e 16). E quando o segundo número for o maior, o programa
2.9. COMANDOS DE SELEÇÃO 45

realiza um teste desnecessário (linha 16).


No Exemplo 2.23, apenas dois testes são executados (linhas 8 e 9 ou linhas 8 e 15), diminuindo
significativamente o número de comparações em relação ao Exemplo 2.22.
O programa do Exemplo 2.24 recebe do usuário três números inteiros e verifica se eles podem
ser tamanhos dos lados de um triângulo. Caso verdadeiro, o programa ainda indica a classificação
do triângulo. O Pseudocódigo 2.6 lista o algoritmo para esse exemplo.

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

1 # include < stdio .h >


2 main () {
3 int lado1 , lado2 , lado3 ;
4 int soma1 , soma2 , soma3 ;
5
6 printf ( " Entre com tres numeros inteiros : " ) ;
7 scanf ( " % d % d % d " , & lado1 , & lado2 , & lado3 ) ;
8
9 soma1 = lado1 + lado2 ;
10 soma2 = lado1 + lado3 ;
11 soma3 = lado2 + lado3 ;
12
46 CAPÍTULO 2. CONCEITOS BÁSICOS

13 if (( lado1 == lado2 ) && ( lado2 == lado3 ) ) {


14 printf ( " Triangulo equilatero \ n " ) ;
15 } else {
16
17 if (( soma1 > lado3 ) && ( soma2 > lado2 ) && ( soma3 > lado1 ) ) {
18
19 if (( lado1 != lado2 ) && ( lado1 != lado3 ) && ( lado2 != lado3 ) ) {
20 printf ( " Triangulo escaleno \ n " ) ;
21 } else {
22 printf ( " Triangulo isosceles \ n " ) ;
23 }
24 } else {
25 printf ( " Os tres lados nao formam um triangulo \ n " ) ;
26 }
27 }
28 }

Exemplo 2.24: Programa que indica se três números formam um triângulo.

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.

2.9.3 Comando de seleção múltipla


O comando de seleção if é bastante poderoso e largamente utilizado. Entretanto, ele não é o
único comando de seleção existente. Há um comando que, embora seja menos abrangente que o
comando if, resolve muitas situações de forma clara e elegante. Trata-se do comando de seleção
múltipla.
O comando de seleção múltipla sempre pode ser substituı́do por um ou mais comandos if
aninhados, mas ele torna o código muito mais claro quando se quer executar várias seqüências
de comandos diferentes, sendo que cada seqüência deva corresponder a um determinado valor
de uma expressão. A estrutura seguinte facilita a compreensão do comando.

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

Na estrutura do comando de seleção múltipla, a expressão normalmente é uma expressão


aritmética ou uma variável de tipo numérica, e associa-se o valor da expressão ao valor de um
determinado case. Assim, apenas a seqüência de comandos do bloco case correspondente será
executada. É importante observar que cada seqüência de comandos é encerrada pelo comando
break.
A cláusula default é opcional. Ela marca a seqüência de comandos padrão, que deve ser
executada quando nenhuma comparação obtiver sucesso. Se não estiver presente, nada será
executado nesse caso.
Na linguagem C, cada case deve servir para comparar o resultado da expressão com um
valor especı́fico. Não se permite comparações com o resultado de outras expressões.
No Exemplo 2.25 usa-se o comando switch na construção de um programa que simula o
funcionamento de uma urna eletrônica. O Pseudocódigo 2.7 exibe o algoritmo desse exemplo.

Pseudocódigo 2.7 Algoritmo de uma urna eletrônica simplificada.


Descrição: Algoritmo para simular uma urna eletrônica simplificada. Não há a opção para voto em
branco.
Dados de Entrada: o número do candidato.
Saı́da do Programa: mensagem dizendo o nome do candidato escolhido ou se o voto foi anulado.
Funç~
ao Principal:
Leia o número do candidato.
CASO o numero do candidato SEJA:
1, ENT~AO imprima uma mensagem dizendo que Hort^ensia da Silva foi escolhida.
2, ENT~AO imprima uma mensagem dizendo que José dos Cravos foi escolhido.
3, ENT~AO imprima uma mensagem dizendo que Margarida S. Pereira foi escolhida.
OUTRO VALOR, ENT~AO imprima uma mensagem dizendo que o voto foi anulado.
FIM-CASO
FIM-Funç~
ao Principal

O comportamento do comando CASO-SEJA do Pseudocódigo 2.7 deve ser pensado da mesma


maneira que o do comando de seleção múltipla switch.

1 # include < stdio .h >


2 main () {
48 CAPÍTULO 2. CONCEITOS BÁSICOS

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.

No programa do Exemplo 2.25, os números de 1 a 3 estão associados a determinados candi-


datos e, para qualquer outro número, a cláusula default é associada.
Após a inicialização da variável numero com o comando de entrada de dados (linha 6), o
comando de seleção compara o número digitado com os números dos três candidatos inscritos.
Em cada caso, se o número digitado coincide com a constante, a seqüência de comandos corres-
pondente ao candidato escolhido é executada. Se nenhuma comparação obtiver sucesso, então
o usuário digitou um número de candidato que não existe. Nesse caso, o programa exibe uma
mensagem de alerta para o usuário, avisando-lhe que o número digitado é inválido para a eleição
(linha 19).
Quando o comando break não termina uma seqüência de comandos cuja comparação resultou
verdadeira, a seqüência de comandos seguinte é executada sem que o teste correspondente seja
efetuado. Na prática, o que acontece é que quando uma comparação do switch resultar em
verdadeiro, todas as seqüências de comando seguintes são executadas até que o primeiro comando
break seja encontrado.
É preciso, portanto, ficar atento e finalizar cada seqüência de comandos com o comando
break evitando, assim, erros de programação muitas vezes difı́ceis de detectar.

2.10 Comandos de Repetição


A capacidade que as máquinas possuem de repetir tarefas exaustivamente com a mesma quali-
dade é uma das principais razões do sucesso de sua invenção. Os computadores podem repetir
uma ou mais seqüência de comandos quantas vezes for necessário, e é o programador quem
decide o critério de parada das repetições.
2.10. COMANDOS DE REPETIÇÃO 49

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.

2.10.1 Comando de repetição com pré-condição


A estrutura do comando while é a mais simples das três e seu funcionamento é igualmente
simples de compreender. A estrutura é a seguinte:

while (<express~ao lógica>){


<Seqü^
encia de comandos>
}

O computador inicia o comando testando a validade da expressão lógica. Se for verdadeira,


então ele executa a seqüência de comandos e retorna ao teste de validação da expressão lógica,
reiniciando o ciclo. O computador repete esse processo até que o resultado da expressão lógica
seja falso.
No caso em que o primeiro teste de validação da expressão lógica resulte em falso, a seqüência
de comandos não é executada nenhuma vez.
O Pseudocódigo 2.8 representa um algoritmo que recebe um número inteiro positivo n e
imprime na tela os n primeiros inteiros positivos.

Pseudocódigo 2.8 Imprimir os n primeiros números positivos.


Descrição: programa para imprimir na tela os n primeiros números positivos.
Dados de Entrada: n (quantidade de inteiros a serem impressos).
Saı́da do Programa: os números inteiros de 1 a n.
Funç~
ao Principal:
Leia n.
Valor recebe 1.
ENQUANTO Valor for menor ou igual a n FAÇA:
Imprima Valor.
Incremente Valor.
FIM-ENQUANTO
FIM-Funç~
ao Principal

O comando ENQUANTO do Pseudocódigo 2.8 funciona da mesma maneira que o comando


de repetição com pré-condição while. O programa do Exemplo 2.26 mostra um código em C
que realiza as operações do referido pseudocódigo.
50 CAPÍTULO 2. CONCEITOS BÁSICOS

1 # include < stdio .h >


2 main () {
3 int n , valor ;
4
5 printf ( " Digite um número inteiro positivo : " ) ;
6 scanf ( " % d " , & n ) ;
7 valor = 1;
8
9 while ( valor <= n ) {
10 printf ( " % d " , valor ) ;
11 valor = valor + 1;
12 }
13 }

Exemplo 2.26: Comando de repetição while para imprimir os n primeiros números inteiros.

As variáveis n e valor do Exemplo 2.26 representam a quantidade de números impressos e o


próprio número, respectivamente. A variável n é inicializada pelo usuário através do comando
de entrada de dados; valor é inicializada com o número 1, para posteriormente ser incrementada
a cada iteração.
Pela expressão lógica do while, nota-se que a condição de parada de iterações ocorre quando
a variável valor atingir um número maior que o número n, ou seja, o bloco de comandos do
while (linhas 10 e 11) é executado enquanto o número armazenado em valor for menor ou igual
ao número armazenado em n.
A condição de parada em qualquer comando de repetição é um fator muito importante que
se deve escolher com bastante cuidado. É preciso garantir que o computador efetue um número
finito de repetições para que, em algum momento, o programa possa seguir seu fluxo normal
de execução. Além de resultados incorretos, uma condição de parada errônea pode acarretar
uma execução de programa sem término (nessa situação, diz-se que o programa está em “loop
infinito”).
A Tabela 2.8 apresenta o resultado da execução do programa do Exemplo 2.26, quando o
usuário informa o valor 4 para a variável n.

n valor valor <= n Tela


4 1 Sim 1
4 2 Sim 12
4 3 Sim 123
4 4 Sim 1234
4 5 Não 1234

Tabela 2.8: Detalhes da execução do comando while do Exemplo 2.26.

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

1 # include < stdio .h >


2 main () {
3 int iterador , valor , n ;
4
5 printf ( " Imprimir numeros impares . Entre com a quantidade de termos : " ) ;
6 scanf ( " % d " , & n ) ;
7
8 iterador = 1;
9 valor = 1;
10
11 while ( iterador <= n ) {
12 printf ( " \ n % d " , valor ) ;
13 valor = valor + 2;
14 iterador = iterador + 1;
15 }
16 }

Exemplo 2.27: Programa que imprime os n primeiros números ı́mpares, utilizando o comando
de repetição while.

No programa do Exemplo 2.27, a quantidade de números ı́mpares a ser impresso é fornecido


pelo usuário através do comando scanf. A variável iterador é usada para armazenar o número a
ser impresso em cada iteração do comando while. O bloco de instruções do comando de repetição
(linhas 11 e 12) é executado enquanto o número armazenado em iterador for menor ou igual ao
número fornecido pelo usuário.
Com os comandos de repetição é possı́vel melhorar o programa da urna eletrônica proposto
no Exemplo 2.25, da seção anterior. Naquele exemplo, o programa não confirma o voto do
usuário, impossibilitando que um erro de digitação pudesse ser corrigido durante a execução do
programa. O Pseudocódigo 2.9 lista o algoritmo melhorado.

Pseudocódigo 2.9 Urna eletrônica com opção de corrigir voto.


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:
Resposta recebe zero.
ENQUANTO resposta for igual a zero FAÇA:
Leia o número do candidato.
Imprima o nome do candidato escolhido.
Leia a resposta da confirmaç~
ao.
FIM-ENQUANTO.
FIM-Funç~
ao Principal
52 CAPÍTULO 2. CONCEITOS BÁSICOS

No código do Exemplo 2.28, o usuário tem a possibilidade de corrigir o seu voto sem que o
programa termine.

1 # include < stdio .h >


2 main () {
3 int numero , resposta ;
4
5 resposta = 0;
6
7 while ( resposta == 0) {
8 printf ( " URNA ELETRONICA - SEU VOTO PARA PREFEITO : " ) ;
9 scanf ( " % d " , & numero ) ;
10
11 switch ( numero ) {
12 case 1:
13 printf ( " Candidato escolhido : Hortencia da Silva " ) ;
14 break ;
15 case 2:
16 printf ( " Candidato escolhido : Jose dos Cravos " ) ;
17 break ;
18 case 3:
19 printf ( " Candidato escolhido : Margarida S . Pereira " ) ;
20 break ;
21 default :
22 printf ( " Numero digitado invalido . Voto NULO . " ) ;
23 }
24
25 printf ( " \ nDigite 1 para CONFIRMAR ou 0 para CORRIGIR " ) ;
26 scanf ( " % d " , & resposta ) ;
27 }
28 }

Exemplo 2.28: Comando de repetição while utilizado para melhorar o código do Exemplo 2.25.

O critério de parada escolhido no Exemplo 2.28 é a alteração do valor da variável resposta.


Enquanto essa variável armazena o valor zero, o programa repetirá o processo de votação e
perguntará ao usuário se confirma o voto ou se gostaria de corrigi-lo. Quando o valor da
variável resposta é alterado, a expressão lógica da linha 7 retorna falso, encerrando o comando
de repetição.

2.10.2 Comando de repetição com pós-condição


Em contraste com o comando de pré-condição, o comando de repetição com pós-condição só
efetua o teste da expressão lógica (condição de parada) após a primeira execução da seqüência
de comandos. Logo, o bloco de comandos é executado pelo menos uma vez, independente da
expressão lógica. A própria sintaxe do comando sugere essa diferença:

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

1 # include < stdio .h >


2 main () {
3 int numero , resposta ;
4 /* A variável resposta n~ a o é mais inicializada pelo programador */
5 do
6 {
7 printf ( " URNA ELETRONICA - SEU VOTO PARA PREFEITO : " ) ;
8 scanf ( " % d " , & numero ) ;
9 switch ( numero )
10 {
11 case 1:
12 printf ( " Candidato escolhido : Hortencia da Silva " ) ;
13 break ;
14 case 2:
15 printf ( " Candidato escolhido : Jose dos Cravos " ) ;
16 break ;
17 case 3:
18 printf ( " Candidato escolhido : Margarida S . Pereira " ) ;
19 break ;
20 default :
21 printf ( " Numero digitado invalido . Voto NULO . " ) ;
22 }
54 CAPÍTULO 2. CONCEITOS BÁSICOS

23 printf ( " \ nDigite 1 para CONFIRMAR ou 0 para CORRIGIR " ) ;


24 scanf ( " % d " , & resposta ) ;
25 } while (! resposta ) ;
26 }

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.

Pseudocódigo 2.11 Cálculo da média de números positivos digitados pelo usuário.


Descrição: Algoritmo para cálculo da média de números positivos digitados pelo usuário.
Dados de Entrada: números não negativos.
Saı́da do Programa: média dos valores digitados.
Funç~
ao Principal:
FAÇA:
Leia um valor.
SE valor é positivo ENT~
AO
Atualize a soma dos números digitados.
Incremente a quantidade de números digitados.
FIM-SE.
ENQUANTO valor digitado for positivo.
SE o usuário digitou algum valor positivo ENT~AO
Calcule a média dos valores digitados.
Imprima essa média.
FIM-SE.
FIM-Funç~ao Principal

O Exemplo 2.30 exibe o programa para o Pseudocódigo 2.11.


2.10. COMANDOS DE REPETIÇÃO 55

1 # include < stdio .h >


2 main () {
3 float valor , soma , media ;
4 int quantidade ;
5
6 soma = 0;
7 quantidade = 0;
8 do {
9 printf ( " Informe um valor positivo ou zero para encerrar o programa : " ) ;
10 scanf ( " % f " ,& valor ) ;
11 if ( valor > 0) {
12 soma = soma + valor ;
13 quantidade = quantidade + 1;
14 }
15 } while ( valor > 0) ;
16 if ( quantidade > 0) {
17 media = soma / quantidade ;
18 printf ( " Media dos valores digitados : % f \ n " , media ) ;
19 }
20 }

Exemplo 2.30: Programa que calcula a média dos valores de uma seqüência digitada pelo usuário.

O funcionamento do programa do Exemplo 2.30 é simples. A variável soma guarda a soma


dos valores digitados pelo usuário e deve ser inicializada com zero. A cada novo valor positivo
digitado, seu valor deve ser atualizado. Essa atualização ocorre na linha 12. Observa-se que o
comando de seleção da linha 11 impede que haja atualização das variáveis soma e quantidade
quando o usuário quer encerrar o programa e digita zero.
A variável valor é a condição de parada do comando de repetição. Sempre que o usuário
digita zero (ou até mesmo um número negativo), o comando de repetição é encerrado.
Para saber quantos números foram digitados, a variável quantidade é empregada. Ela é
inicializada com zero e, sempre que o usuário digita um número positivo, ela é incrementada
(linha 13).
Quando o comando de repetição é encerrado, é preciso verificar se o usuário digitou algum
valor. Essa verificação se deve ao fato de que o primeiro valor digitado pode ser zero, indicando
que o usuário saiu do programa e não quis calcular nada. No caso de algum valor válido ter
sido informado (expressão lógica da linha 16), então a média é calculada e uma mensagem com
o resultado é exibida ao usuário (linhas 17 e 18).

2.10.3 Comando de repetição condensado


O comando de repetição condensado permite agrupar, em um único comando, a inicialização
de uma variável, o incremento desta variável e o teste de parada. Seu uso é adequado para
situações em que o número de repetições da seqüência de comandos já é conhecido.
A estrutura do comando é exibida a seguir:
56 CAPÍTULO 2. CONCEITOS BÁSICOS

for (<inicializaç~
ao>; <express~
ao lógica>; <incremento>){
<Sequencia de comandos>
}

Inicialização é um comando de atribuição usado para colocar um valor na variável de controle


utilizada na expressão lógica. Assim como os demais comandos de repetição, a expressão lógica
representa a condição de parada das iterações. Incremento define como a variável de controle
que será modificada a cada iteração.
Um exemplo de como utilizar o comando for está listado no código do Exemplo 2.31. Esse
programa serve para imprimir os n primeiros termos de uma PG (progressão geométrica). O
usuário deve informar os valores do primeiro termo, da razão e da quantidade de termos a serem
exibidos.

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.

No Exemplo 2.31, as variáveis primeiroTermo, razao e n são inicializadas pelo usuário,


usando o comando de entrada de dados. A linha 10 trata da inicialização da variável termo,
que guarda o valor do termo da PG a ser impresso a cada repetição (iteração) do comando for.
O comando de repetição, em primeiro lugar, inicializa a variável de controle i com o valor
zero. Em seguida, ocorre a verificação da expressão lógica. Caso o usuário tenha digitado
zero, o comando não executa sua seqüência de instruções nenhuma vez, pois tanto i quanto n
armazenam zero e tornam falso o resultado da expressão lógica. Nesse caso, nada é impresso na
tela.
Mas se o usuário digitou um valor maior que zero, a seqüência de comandos será repetida até
que a quantidade solicitada de termos seja impressa. Para isso, o comando de saı́da de dados é
chamado logo no inı́cio da seqüência de comandos (linha 12). No caso em que i é igual a zero
(primeira iteração), o primeiro termo é exibido. A linha 13 conclui a seqüência de comandos
atualizando a variável termo para guardar o próximo termo da seqüência.
2.10. COMANDOS DE REPETIÇÃO 57

Um passo implı́cito é que o comando realiza o incremento da variável i no momento após


a execução da última instrução da seqüência de comandos. Feita essa atualização, a condição
de parada é novamente verificada e, caso seja verdadeira, uma nova repetição é começada para
imprimir outro termo da PG.
A Tabela 2.9 mostra passo a passo a execução completa do comando de repetição para o
programa do Exemplo 2.31. Os valores informados são para uma PG de razão igual a 2, primeiro
termo igual a 2 e quantidade de termos a serem impressos igual a 4. As colunas mostram as
variáveis principais do comando de repetição (i, termo e n), o resultado da expressão lógica a
cada iteração e o que foi impresso na tela. Os valores são referentes ao momento posterior à
execução da seqüência de instruções, antes do incremento da variável de controle.

i termo n i < n Tela


0 4 4 Verdadeiro 2
1 8 4 Verdadeiro 24
2 16 4 Verdadeiro 248
3 32 4 Verdadeiro 2 4 8 16
4 32 4 Falso 2 4 8 16

Tabela 2.9: Detalhes da execução do comando for do Exemplo 2.31


.

O programa do Exemplo 2.32 realiza a operação de exponenciação de um número, utilizando


o comando de repetição for.

1 # include < stdio .h >


2 main () {
3 int base , expoente , resultado , i ;
4
5 printf ( " Informe os valores para base e expoente : " ) ;
6 scanf ( " % d % d " , & base , & expoente ) ;
7
8 resultado = 1;
9 for ( i = 0; i < expoente ; i = i + 1) {
10 resultado = resultado * base ;
11 }
12
13 printf ( " % d elevado a % d = % d \ n " , base , expoente , resultado ) ;
14 }

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

imprimindo os valores da base, do expoente e do resultado da exponenciação.


Assim como o comando while, o comando for efetua primeiramente o teste da condição de
parada antes de executar a seqüência de comandos pela primeira vez.

2.11 Problema dos Lotes Encaixantes


Com o conhecimendo dos conceitos básicos da programação, uma primeira classe de problemas
pode ser estudada. Trata-se do Problema dos Lotes Encaixantes, que corresponde a situações
em que se precisa executar uma varredura em uma massa de dados e extrair dela algumas
informações estatı́sticas.
O termo lotes encaixantes refere-se ao fato de que, na maioria das vezes, problemas como esse
envolvem entidades complexas compostas por outras mais simples. As entidades mais simples
se encaixam para formar outras com um grau de complexidade ainda maior. Assim sendo, é
possı́vel visualizar vários nı́veis que variam desde os mais simples e com muitos elementos até
os mais complexos e com algumas dezenas de integrantes.
A Figura 2.7 ajuda a explicar a idéia. Nela, os quadrados de bordas contı́nuas e finas repre-
sentam o nı́vel mais simples. Eles se agrupam para formar os quadrados de bordas pontilhadas,
que representam um nı́vel intermediário de complexidade. Juntos, os quadrados pontilhados
compõem o nı́vel mais complexo, representado pelo grande quadrado de borda grossa. Na
prática, podem existir não apenas três, mas quantos nı́veis forem necessários para caracterizar
os dados de um programa.

Figura 2.7: Uma representação gráfica para os lotes encaixantes

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.

O Pseudocódigo 2.12 descreve um algoritmo para realizar um levantamento estatı́stico do


desempenho de alunos e turmas de uma disciplina hipotética num determinado semestre.
O Exemplo 2.33 ilustra o código para um programa que lê os dados das notas dos alunos e
exibe a média de cada um. Para cada turma, será exibida a melhor e a pior média. Ao final,
exibe a turma com maior percentual de aprovações da disciplina.
60 CAPÍTULO 2. CONCEITOS BÁSICOS

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

1 # include < stdio .h >


2 # define MEDIAMINIMA 7.0
3
4 main () {
5 int codTurma , matricula , numNotasAluno , numAlunosTurma , alunoMaiorMedia ,
alunoMenorMedia , melhorTurma ;
6 float nota , mediaAluno , numAprovadosTurma , somaNotasAluno , maiorMediaTurma ,
menorMediaTurma , aproveitamentoTurma , melhorAproveitamento ;
7
8 m e l h o r Aproveitamento = 0;
9
10 printf ( " Informe o codigo da turma : " ) ;
11 scanf ( " % d " , & codTurma ) ;
12 while ( codTurma != -1) {
13 maiorMediaTurma = 0;
14 menorMediaTurma = 10;
2.11. PROBLEMA DOS LOTES ENCAIXANTES 61

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

2.12 Exercı́cios Resolvidos


1. Faça um programa que calcule as raı́zes reais da equação do segundo grau ax2 + bx + c.

Pseudocódigo 2.13 Cálculo das raı́zes da equação do segundo grau ax2 + bx + c.


Descrição: Algoritmo para cálculo das raı́zes de uma equação do segundo grau ax2 + bx + c.
Dados de Entrada: coeficientes a, b e c.
Saı́da do Programa: raı́zes da equação.
Funç~
ao Principal:
Leia os coeficientes a, b e c.
Calcule delta igual a b*b-4*a*c.
SE delta for maior ou igual a zero ENT~ AO
SE delta for igual a zero ENTAO~
Calcule a única raiz igual a -b/(2*a).
Imprima o valor da raiz.
SEN~
AO
Calcule a raiz1 igual a (-b + raiz quadrada de delta)/(2*a).
Calcule a raiz2 igual a (-b - raiz quadrada de delta)/(2*a).
Imprima os valores das raı́zes.
FIM-SE
SEN~
AO
Imprima uma mensagem dizendo que n~ ao há raı́zes reais.
FIM-SE
FIM-Funç~
ao Principal

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.

2. Escreva um programa que escreva os n primeiros termos da seqüência de Fibonacci. Essa


seqüência tem zero como primeiro termo e 1 como segundo. Do terceiro termo em diante,
a fórmula para obtenção é a soma dos dois anteriores.

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.

3. Fazer um programa para imprimir a tabuada de 1 a 9.

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 }

Exemplo 2.36: Programa para exibir a tabuada de 1 a 9.

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 }

Exemplo 2.37: Programa para exibir o MDC de dois números.


2.13. RESUMO 67

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 }

Exemplo 2.38: Programa para converter números de binário para decimal.

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

• Na maioria dos problemas de computação, os comandos de entrada de dados, saı́da de


dados, seleção e repetição, são imprescindı́veis.

• Diversos problemas computacionais encaixam-se na modelagem do Problema dos Lotes


Encaixantes. Essa classe de problemas é facilmente resovida com o aninhamento de co-
mandos de repetição, os quais destinam-se a examinar um nı́vel especı́fico da massa de
dados.

2.14 Exercı́cios Propostos


1. Escreva um programa, em C, que recebe um valor de ângulo em graus e informa os valores
do seno, do cosseno e da tangente desse ângulo.

2. Faça um programa, em C, que converta valores de temperatura de Fahrenheit para Celsius.


A proporção utilizada é:
T emperaturaCelsius (T emperaturaF ahrenheit − 32)
=
5 9
3. Faça um programa que calcule a soma
1 3 5 99
S= + + + ... +
1 2 3 50
4. Faça um programa que calcule a soma
37 × 38 36 × 37 35 × 36 1×2
S= − + − ... +
1 2 3 37
5. Um comerciante deseja aumentar suas vendas fazendo uma promoção. Os produtos que
ele trabalha e seus respectivos preços e códigos são: a - anel - R$ 3,00, p - pulseira - R$
5,00, b - brinco - R$ 5,00 e c - cinto - R$ 10,00. As promoções oferecidas são:

• comprar 1 unidade de cada produto, ganha-se um desconto de 10% no total da com-


pra.
• comprar mais de 1 unidade de algum produto, ganha-se 5% de desconto no valor
total da compra. Faça um programa para simular a venda na loja do comerciante,
recebendo como dado de entrada o código do produto e o código da promoção que o
freguês quiser. Imprimir o valor original da compra e o valor com desconto. Utilize o
comando de seleção múltipla (switch).

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:

• O número de pessoas que responderam sim


• O número de pessoas que responderam não
• A porcentagem de pessoas do sexo feminino que responderam sim
• A porcentagem de pessoas do sexo masculino que responderam não

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.

Faça um programa para:

(a) ler mês e ano correntes;


(b) ler os dados dos funcionários que são: matrı́cula do funcionário, salário atual, mês e
ano de ingresso na fábrica. (obs: não se sabe, a priori, o número de funcionários da
fábrica).
(c) aplicar o critério acima descrito;
(d) imprimir para cada funcionário: a matrı́cula, o caso em que ele se enquadra no critério.
Se o funcionário se enquadrar na redução do salário, imprimir o salário novo. Se ele se
enquadrar no plano de demissão voluntária, imprimir a indenização que o funcionário
receberá.
Capı́tulo 3

Modularização
Co-autor:

Gilberto Segundo

Objetivos:

• Definir o que é modularização;

• Apresentar as vantagens de um programa modularizado;

• Apresentar métodos de como criar e adaptar programas modularizados;

• Introduzir o conceito de recursividade e como usá-la

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.

Figura 3.1: Função simples.


72 CAPÍTULO 3. MODULARIZAÇÃO

Em programação, os dados de entrada são chamados de parâmetros e o valor da saı́da é


chamado de retorno. Durante a execução da função, os dados de entrada são manipulados de
maneira a produzir o resultado esperado. Durante essa manipulação de dados a função pode
chamar outras funções, que contêm rotinas que auxiliam na elaboração do resultado final. A
Figura 3.2 ilustra esse processo.

Figura 3.2: Função usando outras funções.

Na fabricação de um automóvel, algum processo realizado por um determinado robô pode


solicitar serviços de outros robôs. Por exemplo, o robô que faz a colocação das peças no carro
pode solicitar por diversas vezes o serviço do robô de soldagem de peças. Após a finalização da
soldagem da peça, o robô de montagem poderá colocar outra peça no carro.

3.3 Partes de um Subprograma


As partes de um subprograma são as mesmas de um programa, ou seja: cabeçalho, dicionário
de dados, corpo e comentário.
A seguir serão comentadas cada uma dessas partes, enfocando sua importância nos subpro-
gramas.

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.

1 float calculaMedia ( float a , float b ) ;

Exemplo 3.1: Cabeçalho de um subprograma na linguagem C.

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.

3.3.2 Dicionário de dados


O dicionário de dados é onde se faz a declaração de variáveis e constantes usadas no subprograma
e não declaradas no cabeçalho da função.
Quando se declara variáveis em subprogramas, estas só podem ser utilizadas naquele sub-
programa. Qualquer outro subprograma, mesmo aquele que chamou a função, não tem acesso
a essas variáveis. Assim, por existirem e serem acessı́veis apenas naquele subprograma, são
chamadas de variáveis locais.
Considere novamente o exemplo do funcionamento da fábrica de automóveis. Enquanto
o subprograma “Robô de solda” estiver fazendo seu trabalho (soldagem de peças), algumas
informações usadas em alguns procedimentos necessitam ser criadas, tais como: quantidade de
solda, qualidade da solda, método de soldagem, etc. Quando o “Robô de solda” termina seu
trabalho, essas informações não são mais necessárias. Note que os outros robôs não precisam
saber dessas informações para poderem executar seus trabalhos.
Para representar em um programa o funcionamento de uma fábrica de automóveis, cada
robô poderia ser representado por um subprograma. As informações seriam representadas por
variáveis locais. A Figura 3.3 ilustra essa correspondência. O “Robô de pintura” tem duas
74 CAPÍTULO 3. MODULARIZAÇÃO

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.

Figura 3.3: Dicionário de dados.

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.

Pseudocódigo 3.1 Passos a serem relizados pela função


Processo componente " distancia " :
Usar o teorema de Pitágoras para calcular a dist^
a ncia dos pontos dados

Retornar o valor da dist^ a ncia


FIM - Processo componente " distancia "

O Exemplo 3.2 mostra a implementação na linguagem C do Pseudocódigo 3.1. O corpo é


composto de atribuição de valores às variáveis dx, dy e dist através de cálculos matemáticos e
pelo retorno do valor contido em dist. O retorno de dados é estudado na Seção 3.6.
Essa função recebe como parâmetro as coordenadas de dois ponto no plano cartesiano e
retorna a distância entre esses pontos. Para fazer o cálculo dessa distância, primeiramente
calcula-se a distância entre as coordenadas x (armazenando esse valor em dx) e a distância
entre as coordenadas y (armazenando esse valor em dy). Posteriormente, usa-se o teorema de
Pitágoras para calcular a distância entre os pontos dados.
3.3. PARTES DE UM SUBPROGRAMA 75

1 float distancia ( float x1 , float y1 , float x2 , float y2 ) {


2
3 float dx , dy , dist ;
4
5 dx = x2 - x1 ;
6
7 dy = y2 - y1 ;
8
9 dist = sqrt ( dx * dx + dy * dy ) ;
10
11 return dist ;
12 }

Exemplo 3.2: O corpo do subprograma são as instruções contidas nele.

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 }

Exemplo 3.3: Comentários em um subprograma.

3.4 Chamada de subprogramas


Em programação, quando um subprograma solicita serviços de um outro subprograma dizemos
que foi feita uma chamada ao subprograma. Durante a execução de um programa, podem ser
feitas diversas chamadas a um mesmo subprograma.
Considere o exemplo do funcionamento da fábrica de automóveis. O “Robô de Solda” pode
ser chamado diversas vezes durante o processo de fabricação de um carro e, é claro, em pon-
tos de montagem diferentes. Toda vez que ele é chamado, executa novamente o seu serviço,
considerando as particularidades de cada caso.
Em programação acontece o mesmo. Um mesmo subprograma pode ser chamado em diversos
pontos do código. A função distancia, descrita no Exemplo 3.2, pode ser usada para fazer
vários cálculos de distâncias entre pontos diferentes e em vários lugares diferentes de um mesmo
programa.
A chamada de um subprograma na linguagem C é feita digitando o nome da função e, em se-
guida, digitando os dados de entrada necessários entre parênteses, para que assim o subprograma
execute suas instruções.
No Exemplo 3.4, a função distancia é chamada duas vezes. Na primeira vez, ela é cha-
mada para calcular a distância entre duas cidades, sendo fornecidas as coordenadas x e y em
quilômetros de distância das cidades. A segunda chamada da função é feita para calcular a
distância entre dois móveis de um escritório.
Note que as coordenadas passadas para o subprograma em cada momento podem ser di-
ferentes, fazendo com que o programa calcule distâncias diferentes. A interpretação fı́sica das
coordenadas passadas também pode ser diferente, não alterando a funcionalidade do subpro-
grama.

1 # include < stdio .h >


2 # include < math .h >
3
4 float distancia ( float x1 , float y1 , float x2 , float y2 ) {
5
6 float dx , dy , dist ;
3.5. PASSAGEM DE PARÂMETROS 77

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 }

Exemplo 3.4: Chamadas de um mesmo subprograma

3.5 Passagem de parâmetros


Os parâmetros de uma função são os dados iniciais necessários que ela possa realizar o seu
trabalho. Por exemplo, a função matemática: y(x) = x2 calcula o quadrado do número x, que
foi passado como parâmetro para a função. Voltando ao exemplo da fabricação de um carro,
pode-se dizer que a fábrica é o programa principal. Ao final do processo, ela deve fornecer um
carro pronto para o uso. Mas a fábrica, como já foi dito, não conta apenas com um robô para
fazer todo o processo e sim com vários robôs, cada um com sua função especı́fica.
Cada robô deve receber uma entrada e fornecer uma saı́da. Para o “Robô de Pintura”,
as entradas são: a carroceria do carro, feita por outro robô, e a cor usada para pintá-la. A
partir dessas entradas, o “Robô de Pintura” executa as instruções estabelecidas para pintar a
carroceria. Ao final da pintura, o robô está pronto para fornecer a saı́da: a carroceria pintada.
78 CAPÍTULO 3. MODULARIZAÇÃO

Essa saı́da pode servir como entrada para outro robô.


Cada parâmetro de uma função possui um tipo, declarado no cabeçalho da função, como
mostrado no Exemplo 3.1, onde os parâmetros a e b são do tipo float. Quando é feita uma cha-
mada ao subprograma, podem ser passados como parâmetro os valores de variáveis do programa
principal, que obrigatoriamente devem ser do mesmo tipo dos parâmetros da função chamada.
No Exemplo 3.4, são feitas várias chamadas ao subprograma distancia. Em cada uma dessas
chamadas são passadas entradas distintas, nesse caso, pontos diferentes no plano cartesiano.
Considere que na primeira chamada os pontos passados são: xa = 10, ya = 30, xb = 20, yb =
60. Já na segunda chamada foram passados os pontos: xa = 15, ya = 18, xb = 5, yb = 53. A
Figura 3.4 ilustra esse processo.

Figura 3.4: Passagem de parâmetro.

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

1 # include < stdio .h >


2
3 float distancia ( float x1 , float y1 , float x2 , float y2 ) {
4
5 float dx , dy , dist ;
6
7 dx = x2 - x1 ;
8
9 dy = y2 - y1 ;
10
11 dist = sqrt ( dx * dx + dy * dy ) ;
12
13 return dist ;
14 }
15
16 main () {
17 int dist ;
18
19 dist = distancia (10 , -15 , 26 , -23) ;
20
21
22 }

Exemplo 3.5: Passagem de parâmetros.

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.

1 # include < stdio .h >


2
3 int dobraValor ( int a ) {
4 printf ( " Número original : % d \ n " , a ) ;
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 dobraValor ( x ) ;
17
18 printf ( " O valor de x é : % d \ n " , x ) ;
80 CAPÍTULO 3. MODULARIZAÇÃO

19 }

Exemplo 3.6: Passagem de valor na chamada de subprogramas.

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

Exemplo 3.7: Saı́da da execução do programa do Exemplo 3.6.

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.

1 void alteraValor ( void ) {


2 x = 20;
3 y = x *5;
4 }
5
6 main () {
7 int x = 10 , y = 15;
8
9 alteraValor () ;
10 }

Exemplo 3.8: Visibilidade das variáveis.

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.

3.6 Retorno de dados


Esta seção trata da forma como os dados são retornados em cada subprograma, isto é, como se
produz uma saı́da da função.
Um subprograma produz, no decorrer das suas instruções, um valor final (uma saı́da), que
deverá ser passado para o programa ou mesmo subprograma que o chamou. O valor retornado
pelo subprograma será atribuı́do a alguma variável do programa que chamou essa função, ou
então usado em alguma expressão.
3.6. RETORNO DE DADOS 81

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.

Pseudocódigo 3.2 Programa para calcular a média de um aluno.


Funç~a o Principal :
Usar o processo componente " calculaMedia " passando - se as 2 notas de provas
do aluno como par^ a metros .
Verificar se a média retornada é maior ou igual a sete .
Caso afirmativo :
O aluno está aprovado .
Caso negativo :
Usar o processo componente " calculaMedia " passando - se a média
anterior e a nota da prova final do aluno como par^ a metros .
Verificar se a nova média retornada é maior ou igual a cinco .
Caso Afirmativo ;
O aluno está aprovado .
Caso negativo :
O aluno está reprovado .
FIM - Funç~a o Principal

Processo componente " calculaMedia " :


Somar os dois números passados como par^
a metros e dividir o resultado por
dois .
Retornar o resultado acima .
FIM - Processo componente " calculaMedia "

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.

1 # include < stdio .h >


2
3 float calculaMedia ( float a , float b ) {
4 float media ;
5
6 media = ( a + b ) /2;
7
8 return media ;
9 }
10
11 main () {
12 float nota1 , nota2 , notaFinal , resultado ;
13
14 printf ( " Forneça as notas das duas provas do aluno : " ) ;
15 scanf ( " % f % f " , & nota1 , & nota2 ) ;
16
17 resultado = calculaMedia ( nota1 , nota2 ) ;
18
19 printf ( " A média do aluno foi : % f \ n " , resultado ) ;
20
21 if ( resultado >= 7) {
22 printf ( " O aluno passou de semestre .\ n " ) ;
23 }
24 else {
25 printf ( " O aluno teve que fazer prova final . Forneça a nota da prova final
do aluno : " ) ;
26 scanf ( " % f " , & notaFinal ) ;
27
28 if ( calculaMedia ( resultado , notaFinal ) >= 5) {
29 printf ( " O aluno passou de semestre .\ n " ) ;
30 }
31 else {
32 printf ( " O aluno n~
a o obteve o rendimento mı́nimo para passar de semestre
. Está reprovado .\ n " ) ;
33 }
34 }
35 }

Exemplo 3.9: Retorno de dados.

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.

Figura 3.5: Passagem de parâmetro por referência.

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.

1 # include < stdio .h >


2
3 int dobraValor ( int a ) {
4 printf ( " Número original : % d \ n " , a ) ;
84 CAPÍTULO 3. MODULARIZAÇÃO

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 }

Exemplo 3.10: Atualização da variável de outra função.

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

Exemplo 3.11: Saı́da da execução do programa do Exemplo 3.6.

3.6.1 Encerramento antecipado de execução


Observando a definição de calculaMedia, implementada no Exemplo 3.9, nota-se que o valor
só é retornado ao final da função, quando todas as outras instruções foram executadas. No
entanto, é possı́vel ocorrer de um subprograma ter vários pontos de retorno distintos.
No Pseudocódigo 3.3 são mostrados os passos realizados pela função ehDivisor , que verifica
se um número é divisor do outro. Para evitar um erro matemático, tem-se que verificar se o
divisor é diferente de zero. Se for igual a zero, então a execução da função deve ser interrompida
e o erro deverá ser indicado de alguma forma. Nesse caso, escolheu-se retornar o valor 0.
No Exemplo 3.12 é apresentada a implementação, na linguagem C, do Pseudocódigo 3.3.
Note que a função apresenta três pontos distintos de retorno. No primeiro, é verificado se o
divisor é zero e caso isso seja verdade é retornado o valor 0, caso contrário, a função continua
executando suas instruções. No segundo, é verificado se o resto da divisão inteira de x por y é
zero, ou seja, é verificado se y é divisor de x. Caso isso seja verdade é retornado o valor 1, caso
contrário, é retornado o valor −1, simbolizando que y não é divisor de x. Aqui, o valor 1 foi
usado para simbolizar sucesso na verificação.
Note ainda que esse recurso torna a função mais eficiente e legı́vel.
3.6. RETORNO DE DADOS 85

Pseudocódigo 3.3 Função que determina se um número é divisor do outro


Processo componente " ehDivisor " :
Verificar se o divisor é igual a zero
Caso afirmativo :
Retornar o valor zero

Verificar se o resto da divis~ a o do dividendo pelo divisor é igual a zero


Caso afirmativo :
Retornar o valor 1
Caso contrário :
Retornar o valor -1
FIM - Processo componente " ehDivisor "

1 int ehDivisor ( int x , int y ) {


2 if ( y == 0) {
3 return 0;
4 }
5
6 if ( x % y == 0) {
7 return 1;
8 } else {
9 return -1;
10 }
11 }

Exemplo 3.12: Encerramento antecipado para evitar erros

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.

1 # include < stdio .h >


2
3 float calculaMedia ( float a , float b ) {
4 float media ;
5
6 media = ( a + b ) /2;
7
8 return media ;
86 CAPÍTULO 3. MODULARIZAÇÃO

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 }

Exemplo 3.13: Encerramento antecipado natural.

3.7 Funções sem lista de parâmetros


Até agora as funções mostradas continham dados de entrada, ou seja, uma lista de parâmetros.
Mas há casos em que isso não é necessário, pois a função executa suas instruções sem precisar
de dados de entrada. Alguns leitores podem achar que isso faz com que o subprograma retorne
sempre o mesmo dado, afinal, a ausência de dados de entrada tornaria a função uma função
constante.
Esse pensamento estaria correto se o subprograma não pudesse coletar dados externos,
3.8. FUNÇÕES SEM RETORNO DE DADOS 87

usando por exemplo a função scanf.


No Exemplo 3.14 a função lerNumeros não tem nenhum parâmetro de entrada. Isso foi feito
pois ela sempre executará a mesma rotina: lerá 5 números digitados pelo usuário e fará a soma
deles. É fácil perceber que o resultado retornado pode variar a cada chamada da função. Isso
acontece pois essa função usa os dados retornados por scanf para completar suas instruções.
Note que em C usa-se void na lista de parâmetros para simbolizar que a função não possui
parâmetros de entrada.

1 int lerNumeros ( void ) {


2 int x =0 , temp , i ;
3
4 for ( i =0 ; i <5 ; i ++) {
5 printf ( " digite um numero : " ) ;
6 scanf ( " % d " , & temp ) ;
7 x += temp ;
8 }
9
10 return x ;
11 }

Exemplo 3.14: Lista de parâmetros vazia

3.8 Funções sem retorno de dados


Assim como há funções que não têm nenhum parâmetro, também há funções que não retornam
nenhum tipo de dado. Na linguagem C, para simbolizar essa situação usa-se void como tipo de
dado retornado.
O Exemplo 3.15 exibe uma função que não retorna dados para a função que a chamou. A
função multiplica recebe três números e exibe o resultado da multiplicação destes. Nota-se que
nenhum dado é retornado, nem mesmo o valor da multiplicação. O programador pode decidir
por este tratamento caso o resultado da multiplicação não seja usado posteriormente.

1 void multiplica ( float a , float b , float c ) {


2
3 printf ( " Resultado = % f " , a * b * c ) ;
4
5 }

Exemplo 3.15: Função sem retorno.

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.

1 void saudacao ( void ) {


2 printf ( " Bem - vindo ao programa Cálculo Eletr^ o nico , onde seus cálculos
s~
a o resolvidos rapidamente \ nPara mais informaç~ o es e atualizaç~
o es
acesse o site : www . programaemc . com . br " ) ;
3
4 }

Exemplo 3.16: Função sem parâmetro e sem 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:

lista = lista vazia ⇒ base da recursao


lista = elemento + lista ⇒ relação recursiva

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:

fatorial (0) = 1 ⇒ base da recursao


fatorial (n) = n * fatorial (n-1) ⇒ relação recursiva

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

Pseudocódigo 3.4 Fatorial de um número de forma recursiva.


Processo componente " fatorial " :
Verificar se o número passado como par^a metro é igual a zero
Caso Afirmativo :
Retornar o valor 1.
Caso Negativo :
Retornar o número passado como par^a metro multiplicado pelo valor do
fatorial do seu antecessor .
FIM - Processo componente " fatorial "

A implementação recursiva do fatorial na linguagem C é mostrada no Exemplo 3.17. O


programa chama ele mesmo quantas vezes forem necessárias até que o parâmetro n seja 0.
Quando isso acontece, a função retorna 1 e a execução prossegue a partir da última chamada da
função, multiplicando o valor retornado (1) pelo valor 1 da chamada anterior da função e assim
sucessivamente, até se obter o fatorial do número n inicial.

1 int fatorial ( int n ) {


2 if ( n == 0) {
3 return 1;
4 } else {
5 return n * fatorial (n -1) ;
6 }
7 }

Exemplo 3.17: Implementação recursiva do fatorial.

É importante o compreender que a cada chamada do programa fatorial é reservado um


novo espaço de memória, ou seja, os dados do programa fatorial anterior são preservados e
caso a função recursiva tivesse variáveis locais, novas instâncias dessas variáveis seriam criadas
a cada chamada, sem qualquer relação com as variáveis da outra chamada do subprograma. Por
isso, funções recursivas tendem a necessitar de mais memória do computador.

Chamando a função fatorial passando-se como argumento o número 5, tem-se o esquema


ilustrado na Figura 3.6. As setas direcionadas para baixo significam a chamada do programa
apontado, exibindo o valor passado para como parâmetro. As setas direcionadas para cima
significam o retorno do dado do programa chamado, mostrando o valor retornado pela função.
90 CAPÍTULO 3. MODULARIZAÇÃO

Figura 3.6: Fatorial utilizando recursão.

3.9.1 Implementação não recursiva equivalente

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.

1 int fatorial2 ( int n ) {


2 int i , f ;
3
4 f = 1;
5
6 for ( i =2 , i <= n ; i ++) {
7 f= f*i;
8 }
9 return f ;
10 }

Exemplo 3.18: Implementação não recursiva do fatorial.


3.10. EXERCÍCIOS RESOLVIDOS 91

3.10 Exercı́cios Resolvidos

Exercı́cio Resolvido 3.1 - Soma de inteiros

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

Figura 3.7: Solução para n = 4.

O Pseudocódigo 3.5 mostra o algoritmo que descreve os passos a serem realizados pela função.

Pseudocódigo 3.5 Passos a serem realizados pela função soma.


Processo componente " soma " :
Inicializar soma com o valor zero
Para um loop de 1 a n :
Acumular em soma o valor corrente de i
Retornar o valor soma
FIM - Processo componente " soma "

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

1 int soma ( int n ) {


2 int i , soma ;
3
4 soma = 0;
5
6 for ( i = 1; i <= n ; i = i + 1) {
7 soma = soma + i ;
8 }
9
10 return soma ;
11 }

Exemplo 3.19: Exercı́cio Resolvido - Soma de inteiros.

Exercı́cio Resolvido 3.2 - Múltiplos

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:

Para exibir os n primeiros naturais múltiplos de i e j deve-se testar os números naturais um


a um, começando de zero e verificando se são múltiplos de i ou de j. Caso o número natural
corrente, contido na variável natural, seja múltiplo da variável i ou j, uma variável contadora
cont deverá ser incrementada e a verificação continuará com o próximo número natural. A
verificação deverá acabar quando n números naturais múltiplos de i ou de j forem encontrados.
Essa condição será alcançada quando a variável cont for igual a variável n, que contém o número
de múltiplos a serem impressos.

A Figura 3.8 mostra os passos realizados pelo programa para n = 6, i = 2 e j = 3.

Para saber se um número x é múltiplo de um número y basta verificar se o resto da divisão


inteira de x por y é zero.

O Pseudocódigo 3.6 mostra um algoritmo que descreve os passos a serem realizados pela
função.
3.10. EXERCÍCIOS RESOLVIDOS 93

No Natural Múltiplo de j ? Múltiplo de i ? No de múltiplos de i ou j


0 sim sim 1
1 não não 1
2 não sim 2
3 sim não 3
4 não sim 4
5 não não 4
6 sim sim 5
7 não não 5
8 não sim 6

Figura 3.8: Solução para n = 6, i = 2 e j = 3.

Pseudocódigo 3.6 Passos a serem relizados pela função.


Funç~a o Principal :
Ler os valores de n , i e j
Chamar o processo componente multiplos passando - se como par^
a metros n , i e j
.
FIM - Funç~a o Principal

Processo componente " multiplos " :


Inicializar a variável do número atual e a variável indicando o npumero de
m´ultiplos com o valor zero .
Enquanto o número de multiplos for menor que n :
Verificar se o número atual é múltiplo de i ou de j através do processo
componente ehMultiplo
Caso afirmativo :
Incrementar a variável contadora de múltiplos e exibir o valor do
número atual .
Incrementar a variável do número atual
FIM - Processo componente " multiplos "

Processo componente " ehMultiplo " :


Verificar se o resto da divis~ a o de x por y é zero .
Caso afirmativo :
Retornar o valor 1 , indicando sucesso .
Caso negativo :
Retornar o valor 0.
FIM - Processo componente " ehMultiplo "

O Exemplo 3.20 mostra a implementação do algoritmo do Pseudocódigo 3.6. Na função


ehMultiplo usou-se como convenção o valor 1 para indicar sucesso na verificação de x ser
múltiplo de y e 0 caso contrário. Note também que no comando de repetição for da função
múltiplos foi omitido o terceiro parâmetro. Isso foi necessário pois a variável cont só deverá
94 CAPÍTULO 3. MODULARIZAÇÃO

ser incrementada se natural for múltiplo de i ou j, o que nem sempre é verdade.

1 # include < stdio .h >


2
3 int ehMultiplo ( int x , int y ) {
4
5 if ( x % y == 0) {
6 return 1;
7 }
8 else {
9 return 0;
10 }
11 }
12
13 void multiplos ( int i , int j , int n ) {
14 int natural , cont ;
15
16 natural = 0;
17
18 printf ( " Os % d primeiros múltiplos de % d ou de % d s~
ao : " , n , i , j ) ;
19
20 cont = 0;
21 while ( cont < n ) {
22
23 if ( ehMultiplo ( natural , i ) == 1 || ehMultiplo ( natural , j ) == 1) {
24 printf ( " % d " , natural ) ;
25 cont ++;
26 }
27
28 natural ++;
29 }
30 printf ( " \ n " ) ;
31
32 }
33
34 int main () {
35 int n , i , j ;
36
37 printf ( " Digite 3 números naturais , o primeiro sendo a quantidade n de
múltiplos que ser~ a o impressos e os dois últimos sendo os números i e j
que servir~ a o como base para a geraç~
a o dos múltiplos : " ) ;
38
39 scanf ( " % d % d % d " , &n , &i , & j ) ;
40
41 multiplos (i , j , n ) ;
42
43 return 0;
44 }

Exemplo 3.20: Exercı́cio Resolvido - Múltiplos.


3.10. EXERCÍCIOS RESOLVIDOS 95

Exercı́cio Resolvido 3.3 - Triângulo Retângulo

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.

Pseudocódigo 3.7 Passos a serem relizados pela função.


Processo componente " trianguloRetangulo " :
Comparar se o lado b é maior do que o lado a
Caso afirmativo , trocar os valores de a e b.
Comparar se o lado c é maior do que o lado a
Caso afirmativo , trocar os valores de a e c.
Verificar se a * a = b * b + c * c
Caso afirmativo :
Retornar o valor 1
Caso negativo :
Retornar o valor 0
FIM - Processo componente " trianguloRetangulo "

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.

1 int tr ia nguloRetangulo ( int a , int b , int c ) {


2 int aux ;
3
4 if ( b > a ) {
5 aux = a ;
6 a = b;
7 b = aux ;
8 }
9
10 if ( c > a ) {
96 CAPÍTULO 3. MODULARIZAÇÃO

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 }

Exemplo 3.21: Exercı́cio Resolvido - Triângulo Retângulo.

Exercı́cio Resolvido 3.4 - Tabuada

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.

0x1=0 1x1=1 ... 37 x 1 = 37


0x2=0 1x2=2 ... 37 x 2 = 74
. . ... .
. . ... .
0 x 22 = 0 1 x 22 = 22 ... 37 x 22 = 814

Figura 3.9: Solução para m = 22 e n = 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

Pseudocódigo 3.8 Passos a serem relizados pela função.


Processo componente " mult " :
Para um loop variando de 1 a n :
Guardar o resultado de cont vezes x em r e exibir o resultado
FIM - Processo componente " mult "

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.

1 # include < stdio .h >


2
3 void mult ( int a , int b ) {
4 int r , cont ;
5
6 for ( cont =1; cont <= a ; cont ++) {
7 r = b * cont ;
8
9 printf ( " % dx % d =% d \ n " , b , cont , r ) ;
10 }
11
12 printf ( " \ n " ) ;
13 }
14
15 int main ( void ) {
16 int m , n , x ;
17
18 printf ( " Entre com dois números inteiros : " ) ;
19 scanf ( " % d % d " , &n ,& m ) ;
20
21 for ( x =0; x <= m ; x ++) {
22 mult (n , x ) ;
23 }
24
25 return 0;
26 }

Exemplo 3.22: Exercı́cio Resolvido - Tabuada.

Exercı́cio Resolvido 3.5 - N primeiros números primos

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

Pseudocódigo 3.9 Passos a serem relizados pela função.


Processo componente " ehPrimo " :
Verificar se x é igual a um
Caso afirmativo :
Retornar o valor zero
Verificar se x é igual a 2
Caso afirmativo :
Retornar o valor um
Verificar se x é divisı́vel por 2
Caso afirmativo :
Retornar o valor zero
Para um loop com i variando de 3 a raiz quadrada de x :
Verificar se x é divisı́vel por i
Caso afirmativo :
Retornar o valor zero
Incrementar o valor de i em 2 unidades
Retornar o valor um
FIM - Processo componente " ehPrimo "

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

1 # include < stdio .h >


2 # include < math .h >
3
4 /*
5 Verifica se o número passado como par^ a metro é primo .
6 Entrada : um número inteiro positivo diferente de zero
7 Saı́da : retorna 1 para o caso do número informado for primo e zero caso contrário
8 */
9 int ehPrimo ( int x ) {
10
11 int i ;
12
13 if ( x == 1) {
14 return 0;
15 }
16
17 if ( x == 2) {
18 return 1;
100 CAPÍTULO 3. MODULARIZAÇÃO

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 }

Exemplo 3.23: Exercı́cio resolvido - N primeiros números primos.

Exercı́cio Resolvido 3.6 - Palı́ndromo

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.

Então, multiplicando-se m por 10 e somando-se a esse resultado o resto da divisão inteira do


novo valor de x por 10, teremos em m os últimos dois algarismo de x na ordem inversa.

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

Iteração x inicial x / 10 x % 10 m final x final


1 52325 5232 5 5 5232
2 5232 523 2 52 523
3 523 52 3 523 52
4 52 5 2 5232 5
5 5 0 5 52325 0

Figura 3.10: Solução para x = 52325.

O Pseudocódigo 3.10 mostra o algoritmo que descreve os passos a serem realizados pela
função.
102 CAPÍTULO 3. MODULARIZAÇÃO

Pseudocódigo 3.10 Passos a serem relizados pela função.


Processo componente " palindromo " :
Guardar em c o valor de x
Iniciar m com zero
Enquanto x for diferente de zero :
Fazer m igual a multiplicaç~a o de m por 10 com a soma da divis~
a o inteira
de x por 10.
Fazer x igual a divis~a o inteira de x por 10
Verificar se m é igual a c
Caso positivo :
Retornar o valor um
Caso negativo :
Retornar o valor zero .
FIM - Processo componente " palindromo "

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.

1 int palindromo ( int x ) {


2 int c , m ;
3
4 c = x;
5
6 m = 0;
7
8 while ( x != 0) {
9 m = m *10 + ( x % 10) ;
10 x = x / 10;
11 }
12
13 if ( m == c ) {
14 return 1;
15 } else {
16 return 0;
17 }
18 }

Exemplo 3.24: Exercı́cio resolvido - Palı́ndromo.

Exercı́cio Resolvido 3.7 - Máximo Divisor Comum

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

A apresentação mais simples consiste em comparar-se os números e colocar a diferença entre


os dois (o menor subtraı́do do maior). Agora compara-se o resultado dessa subtração com o
menor número anterior, repetindo-se o processo até que se obtenha igualdade entre os números
nas duas colunas, que é o resultado procurado.
Para melhor compreender o funcionamento do método, basta recorrer a um raciocı́nio muito
simples. Com efeito, se observarmos com atenção a tabuada da multiplicação de um número
qualquer, podemos ver que a diferença entre dois produtos é sempre um produto que figura
na mesma tabuada. Portanto, se dois números forem múltiplos de um terceiro, então a sua
diferença também é, o que nos permite substituir o maior deles por essa diferença, para efeitos
de cálculo do MDC; e tudo isso tantas vezes quantas forem necessárias, até que se chegue a um
ponto em que os dois números se identificam no mesmo, o que nos remete para a questão mais
simples: qual é o MDC de dois números iguais? O que é precisamente a situação a que se chegou
no nosso método.
O Pseudocódigo 3.11 mostra o algoritmo que descreve os passos a serem realizados pela
função iterativa.

Pseudocódigo 3.11 Passos a serem relizados pela função.


Processo componente " mdc_Recursivo " :
Verificar se a é igual a b
Caso positivo :
Retornar o valor de a
Verificar se a é maior que b
Caso positivo :
Chamar a funç~
a o mdc_Recursivo passando - se como primeiro par^ a metro o
valor da subtraç~
a o de a - b e como segundo par^
a metro o b
Caso negativo :
Chamar a funç~
a o mdc_Recursivo passando - se como primeiro par^ a metro o
valor da subtraç~
a o de b - a e como segundo par^
a metro o a
FIM - Processo componente " mdc_Recursivo "

O Exemplo 3.25 mostra a implementação desse algoritmo.

1 int mdc_Recursivo ( int a , int b ) {


2 if ( a == b ) {
3 return a ;
4 }
5
6 if ( a > b ) {
7 return mdc (a -b , a ) ;
8 } else {
9 return mdc (a , b - a ) ;
10 }
11 }

Exemplo 3.25: Exercı́cio resolvido - M.D.C. (modo recursivo).


104 CAPÍTULO 3. MODULARIZAÇÃO

O Pseudocódigo 3.12 mostra o algoritmo que descreve os passos a serem realizados pela
função iterativa.

Pseudocódigo 3.12 Passos a serem relizados pela função.


Processo componente " mdc_Iterativo " :
Enquanto a for diferente de b faça :
Verificar se a é maior que b :
Caso positivo :
Colocar em a o resultado da subtraç~
ao a - b
Caso contrário :
Colocar em b o resultado da subtraç~
ao b - a
Retornar o valor de a
FIM - Processo componente " mdc_Iterativo "

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.

1 int mdc_Iterativo ( int a , int b ) {


2
3 while ( a != b ) {
4 if ( a > b ) {
5 a = a-b;
6 } else {
7 b = b-a;
8 }
9 }
10
11 return a ;
12 }

Exemplo 3.26: Exercı́cio resolvido - M.D.C. (modo iterativo).

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.

• Algumas vezes é interessante antecipar o encerramento da execução de programas.

• A programação recursiva é uma técnica muito utilizada na implementação de funções.

• Geralmente é possı́vel criar versões recursivas e não recursivas de uma mesma função.

As vantagens encontradas em um programa modularizado são:


• Legibilidade: facilidade de leitura e entendimento do código fonte;

• Manutenibilidade: facilidade de modificar o código fonte;

• Reusabilidade: facilidade de reutilizar total ou parcialmente o código fonte;

• Confiabilidade: como o programa é fácil de ser entendido e corrigido, torna-se mais


confiável;

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

3.12 Exercı́cios Propostos


1. (a) Construa uma função encaixa que, dados dois inteiros positivos a e b, verifica se b
corresponde aos últimos dı́gitos de a.
Ex.:

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.

3. Considere o valor de π = 3.141592. Construa uma função para calcular a área de um


cı́rculo tendo como dado de entrada, o valor do raio. Em seguida, fazer outra função para
calcular o raio do cı́rculo que possui como área, a metade da área calculada anteriormente.

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.

6. Uma loja de material de construção precisa de um programa para saber a quantidade de


metros quadrados (m2) que devem ser usados para colocar piso nas casas dos clientes. É
fornecido o número de cômodos, os lados dos quadriláteros de cada cômodo e o preço do
piso que o cliente escolheu. Faça uma função que receba essas informações como dados de
entrada e imprima a quantidade de piso (em m2 ) e o valor da compra. Obs: Todos os
cômodos da casa têm a forma de quadriláteros.

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

3.13 Trabalhos Sugeridos


1. Aplicando Conhecimentos de Cálculo
Em um belo dia, um preguiçoso monitor de Cálculo teve uma simples idéia para facilitar
seu trabalho. Percebendo que vários alunos tinham dúvidas sobre derivadas e integrais
de polinômios, ele resolveu pedir a um programador (nesse caso, você) um algoritmo que
fizesse esse “trabalho”.
Por experiência, o monitor restringiu o programa a polinômios de até grau nove e passou
as seguintes especificações ao programador:
Exemplos de Dados de Entrada
Uma função seria digitada da seguinte maneira: (Opção 1)

+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

• Derivada 2.00x1-5.00x0= (Opção 1) 2.00x-5.00= (Opção 2)


108 CAPÍTULO 3. MODULARIZAÇÃO

• Integral 0.33x3-2.50x2+6.00x1+C= (Opção 1) 0.33x3-2.50x2+6.00x+C= (Opção 2)


• Raı́zes (apenas para funções de primeiro e segundo grau) x1=2.00 x2=3.00 Os coefi-
cientes e as raı́zes deverão ser impressos com 2 casas decimais.
Capı́tulo 4

Tipos Abstratos de Dados


Co-Autores:

Bruna Vello Colnago


Rodrigo Lopes Batista

Objetivos:

• Introduzir o conceito de Tipos Compostos Heterogêneos;

• Explicar as vantagens da utilização de Tipos Abstratos de dados;

• Tornar o código do programa mais compreensı́vel;

• Simplificar a confecção de aplicações mais complexas.

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.

4.1 Técnicas de Programação Top-down e Bottom-up


Para escrever um programa, os programadores podem adotar duas técnicas: a Top-down e a
Bottom-up. A técnica Bottom-up consiste em 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. Já a técnica Top-down consiste em primeiramente definir a estrutura
global do programa e posteriormente definir cada uma das partes que detalham as suas tarefas.
Analogamente, um projetista de automóveis pode inicialmente definir como será o carro,
velocidade, rapidez, rotações máximas, etc , e posteriormente confeccionar seus componentes
(motor, rodas, freios, suspensão...), conforme a necessidade - Top-down - ou confeccionar cada
um dos componentes separadamente, garantir seu funcionamento e depois juntá-los em um carro
cujas metas sejam compatı́veis com as possibilidades dos componentes - Bottom-up.

109
110 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

Na abordagem Bottom-up, cada módulo é implementado segundo suas funcionalidades e


objetivos, independentemente do restante do programa, assim torna-se mais simples redigir o
código do programa, facilitando o trabalho do programador. Além do mais, a separação do
programa em módulos permite a realização de testes especı́ficos, facilitando a depuração de
erros. Outra importância dessa abordagem é a facilidade de alterar partes do programa, sendo
necessário alterar apenas alguns módulos sem se importar com os demais.
Por fim, o programa torna-se muito mais fácil de ser entendido, ao passo que o leitor não
precisa ler todo o código do programa, bastando saber as funcionalidades de cada módulo. A
implementação dos mesmos importa somente ao programador. Assim, a abordagem Bottom-up
é mais vantajosa em relação à Top-down, ao passo que é muito mais intuitiva ao programador.

4.2 Tipos Compostos Heterogêneos (Estruturas)

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.

Figura 4.1: Estrutura tEstudante

Sintaxe
Para criar uma estrutura com n atributos, deve-se obdecer à sintaxe exposta abaixo.

struct <nome da estrutura>{


<tipo do atributo 1> <nome do atributo 1>;
<tipo do atributo 2> <nome do atributo 2>;
...
<tipo do atributo n> <nome do atributo n>;
};

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

Exemplo 4.1: Sintaxe da estrutura tEstudante

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.

<nome da variável>.<nome do atributo>

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.

1 # include < stdio .h >


2 # include < stdlib .h >
3
4 struct tEstudante {
5 int idade ;
6 int matricula ;
7 float coeficiente ;
8 int periodo ;
9 };
10
11 main () {
12 struct tEstudante aluno ;
13 printf ( " Digite os dados do estudante " ) ;
14 scanf ( " % d % d % f % d " ,& aluno . idade ,& aluno . matricula ,& aluno . coeficiente ,& aluno .
periodo ) ;
15 }

Exemplo 4.2: Acesso seletivo a atributos 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

<nome da variável 1> = <nome da variável 2>

O Exemplo 4.3 ilustra o uso integral da estrutura estudante.

1 # include < stdio .h >


2 # include < stdlib .h >
3
4 struct tEstudante {
5 int idade , matricula , periodo ;
6 float coeficiente ;
7 };
8
9 main () {
10 struct tEstudante aluno , outro ;
11 printf ( " Digite os dados do estudante " ) ;
12 scanf ( " % d % d % f % d " ,& aluno . idade ,& aluno . matricula ,& aluno . coeficiente ,& aluno .
periodo ) ;
13 outro = aluno ;
14 }

Exemplo 4.3: Acesso integral à estrutura

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.

typedef <nome do tipo simples ou composto> <novo nome>;

Na prática, a renomeação de tipos facilita o entendimento e a escrita de código.


O Exemplo 4.4 apresenta a utilização do typedef.

1 typedef struct tEstudante tEstudante ;


2
3 typedef float distancia ;

Exemplo 4.4: Uso do comando typedef

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.

1 # include < stdio .h >


2 # include < stdlib .h >
3
4 struct tEstudante {
5 int idade ;
114 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

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 }

Exemplo 4.5: Acesso integral à estrutura utilizando o comando typedef

Simplificação na Passagem de Parâmetros e Retorno de Função


Conforme comentou-se anteriormente, as estruturas são conjuntos de dados inter-relacionados,
dessa forma a passagem de parâmetros de função é simplificada, pois ao invés de cada um dos
atributos ser um parâmetro da função, apenas a estrutura é passada. Note a diferença entre a
lista de parâmetros das duas funções representadas no Exemplo 4.6.

1 void funcao1 ( int idade , int matricula , float coeficiente , int periodo ) { ... }
2
3 void funcao2 ( tEstudante aluno ) { ... }

Exemplo 4.6: Passagem de estrutura por parâmetro.

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.

1 # include < stdio .h >


2 # include < stdlib .h >
3
4 struct tEstudante {
5 int idade ;
6 int matricula ;
7 float coeficiente ;
4.2. TIPOS COMPOSTOS HETEROGÊNEOS (ESTRUTURAS) 115

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 }

Exemplo 4.7: Estrutura como retorno de função.

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 Tipos Abstratos de Dados


Tipos abstratos de dados (TADs) são novos tipos de dados implementados pelo programador,
nos quais ele define tanto as estruturas de dados quanto as operações a elas aplicáveis, conforme
suas necessidades para a resolução de um determinado problema.
Os TADs podem ser considerados generalizações de tipos primitivos de dados, assim como
funções são generalizações de operações primitivas, tais como adição, subtração e multiplicação.
Da mesma forma que funções podem ser usadas para encapsular partes de algoritmos, o TAD
pode ser usado para encapsular tipos de dados.
O TAD pode ser discutido pela perspectiva do implementador e do usuário do tipo. O im-
plementador cria as estruturas de dados, implementa as funções para manipulá-las. Já o usuário
utiliza esse TAD como se fosse um tipo de dados fornecido pela linguagem de programação.
Deste modo, o usuário só deve manipular os atributos do TAD através das funções definidas
pelo implementador do tipo.

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.

Exemplo da Definição de um Tipo Composto com Operações Pré-definidas


Suponha que se deseja definir um tipo abstrato de dados tData, considerando os atributos dia,
mês e ano. Primeiramente, deve-se decidir as operações a serem definidos para o TAD, que
podem ser as seguintes:

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

• eBissexto: função que indica se um ano é bissexto ou não.

• diasNoMes: função que indica a quantidade de dias do mês em questão.


4.3. TIPOS ABSTRATOS DE DADOS 117

• diaSeguinte: função que altera a data para o dia seguinte.


Pode-se então implementar o TAD na linguagem desejada. No Exemplo 4.8, observa-se a
implementação do TAD tData na linguagem C.

1 # include < stdio .h >


2
3 typedef struct data {
4 int dia ;
5 int mes ;
6 int ano ;
7 } tData ;
8
9 tData in icializarValores ( int d , int m , int a ) {
10 tData dt ;
11
12 dt . dia = d ;
13 dt . mes = m ;
14 dt . ano = a ;
15 return dt ;
16 }
17
18 tData leData () {
19 tData d ;
20
21 printf ( " Entre com a data " ) ;
22 scanf ( " % d % d % d " ,& d . dia ,& d . mes ,& d . ano ) ;
23 return d ;
24 }
25
26 tData alteraData ( int d , int m , int a ) {
27 tData dt ;
28
29 dt . dia = d ;
30 dt . mes = m ;
31 dt . ano = a ;
32 return dt ;
33 }
34
35 int eBissexto ( tData d ) {
36 if ( d . ano %400==0) {
37 return 1;
38 } else if ( d . ano %100==0) {
39 return 0;
40 } else if ( d . ano %4==0) {
41 return 1;
42 } else {
43 return 0;
44 }
45 }
46
118 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

47 int diasNoMes ( tData d ) {


48 if ( d . mes ==4|| d . mes ==6|| d . mes ==9|| d . mes ==11) {
49 return 30;
50 } else {
51 if ( d . mes =2) {
52 if ( eBissexto ( d ) ) {
53 return 29;
54 } else {
55 return 28;
56 }
57 } else {
58 return 31;
59 }
60 }
61 }
62
63 tData diaSeguinte ( tData d ) {
64 if ( d . dia < diasNoMes ( d ) ) {
65 d . dia ++;
66 } else {
67 d . dia =1;
68 if ( d . mes <12) {
69 d . mes ++;
70 } else {
71 d . mes =1;
72 d . ano ++;
73 }
74 }
75 return d ;
76 }
77
78 main () {
79 }

Exemplo 4.8: Definição do TAD tData.

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.

Importância de Definir Tipos com Operações Próprias


A criação de um conjunto de operações próprias de um TAD tem como finalidade torná-lo,
aos olhos do usuário, um tipo de dados da própria linguagem, ou seja, o usuário não deve se
preocupar em como são implementados os TADs. Além do mais, utilizar as operações do TAD
torna o código mais confiável. Suponha uma aplicação que utilize o TAD tData conforme o
Exemplo 4.9.

1 # include < stdio .h >


2
3 // A definiç~
a o do TAD , bem como suas operaç~
o es , está no Exemplo 4.6
4
5 main () {
6 tData d ;
7
8 d = leData () ;
9 printf ( " Passou um dia !\ n " ) ;
10 d . dia ++;
11 printf ( " Hoje é % d /% d /% d .\ n " ,d . dia , d . mes , d . ano ) ;
12 }

Exemplo 4.9: Problema de Confiabilidade no uso do TAD tData.

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.

Vantagens de Usar TADs


As vantagens de utilizar um TAD são:
• Facilidade de Manutenção: Pode-se alterar o tipo usado sem alterar a aplicação. Por
exemplo, pode-se incluir novos atributos ou operações sem que o código que utilize o tipo
seja alterado.
• Reutilização: Um TAD bem generalizado pode ser utilizado em diversas aplicações.
• Abstração: A abstração de informações através do TAD permite a melhor compreensão
dos algoritmos e maior facilidade de programação.
120 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

• Ocultamento: Separa o código de implementação do código de uso do TAD, que funciona


como um tipo de dados fornecido pela linguagem.

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

Problema do uso de TADs em C


A idéia do TAD é esconder o código da implementação do usuário, que não deve ter acesso
indiscriminado aos atributos do TAD. A linguagem C, entretanto, não oferece a funcionalidade
de proteção ao acesso aos atributos e operações. Na verdade, C não implementa TAD, o que se
faz é apenas uma simulação, visto que apesar de se definir as operações sobre o tipo, o usuário
tem a liberdade para acessar diretamente os atributos sem nenhuma proteção, tornando o código
não confiável.

4.3.2 Definição de Atributos de um TAD


Os atributos de um TAD são os dados que se relacionam a ele. Por exemplo, no caso do
estudante, seus atributos são: idade, matrı́cula, coeficiente de rendimento e perı́odo. Conforme
definido na Seção 4.2.1.

4.3.3 Definição de Operações de um TAD


As operações de um TAD são funções que utilizadas para acesso dos dados. Estas operações
visam impedir o acesso direto aos dados, assim, o usuário só deve acessar os dados através dessas
operações.

Tipos de Operações de um TAD


Um TAD não é definido por seus atributos, mas sim por suas operações. As operações são
a interface do programador usuário com a própria representação interna. Existem cinco tipos
diferentes de operações que podem ser realizadas sobre um TAD:

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 }

Exemplo 4.10: Diferentes formas de Implementação da função construtora para o TAD


tEstudante.

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

As operações analisadoras ou consultoras analisam o conteúdo de um TAD e retornam proprie-


dades, ou seja, essas operações obtêm informações do TAD. O Exemplo 4.11 mostra uma função
anasiladora.

1 int bomAluno ( tEstudante aluno ) {


2 if ( aluno . coeficiente >7.0) {
3 return 1;
4 } else {
5 return 0;
6 }
7 }

Exemplo 4.11: Função analisadora.

Observa-se, no Exemplo 4.11, que a função bomAluno é um exemplo de função analisadora


para o TAD tEstudante, pois retorna uma propriedade do mesmo (se é um bom aluno).
122 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

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.

1 tEstudante alteraIdade ( tEstudante aluno , int novaIdade ) {


2 aluno . idade = novaIdade ;
3 return aluno ;
4 }

Exemplo 4.12: Função modificadora.

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.

1 int maiorIdade ( tEstudante est1 , tEstudante est2 ) {


2 if ( est1 . idade > est2 . idade ) {
3 return est1 . idade ;
4 }
5 return est2 . idade ;
6 }

Exemplo 4.13: Exemplo de função produtora.

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.

4.3.4 Uso do TAD


Considere o Exemplo 4.14 que faz uso do TAD tData. Note que o TAD é acessado apenas pelas
operações pré-definidas.

1 # include < stdio .h >


2
3 // A definiç~
a o do TAD , bem como suas operaç~
o es , está no Exemplo 4.6
4
5 main () {
6 tData data ;
7 int anoBissexto ;
8 int nDias ;
4.4. EXERCÍCIOS RESOLVIDOS 123

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 }

Exemplo 4.14: Uso do TAD tData.

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.

4.3.5 Tipos de TADs


Os TADs podem se dividir em TADs de Domı́nio e TADs Implementacionais, conforme sua
relação com o problema ou com sua solução .

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.

4.4 Exercı́cios Resolvidos


Exercı́cio Resolvido4.1 - Leitura da Estrutura tData
124 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

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.

1 # include < stdio .h >


2
3 struct data { // estrutura do tipo data
4 int dia ;
5 int mes ;
6 int ano ;
7 };
8
9 int main () {
10 struct data dataNascimento ;
11
12 // inicializa a estrutura data de nascimento
13 dataNascimento . dia = 30;
14 dataNascimento . mes = 01;
15 dataNascimento . ano = 1984;
16 printf ( " Data : % d /% d /% d \ n " , dataNascimento . dia , dataNascimento . mes ,
dataNascimento . ano ) ;
17 return 0;
18 }

Exercı́cio Resolvido 4.2 - Operações sobre a Estrutura Data

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.

1 # include < stdio .h >


2
4.4. EXERCÍCIOS RESOLVIDOS 125

3 typedef struct data {


4 int dia ;
5 int mes ;
6 int ano ;
7 } tData ;
8
9 tData alteraData ( int d , int m , int a ) {
10 tData dt ;
11
12 dt . dia = d ;
13 dt . mes = m ;
14 dt . ano = a ;
15 return dt ;
16 }
17
18 void consultaData ( tData data ) {
19 printf ( " Data : % d /% d /% d \ n " , data . dia , data . mes , data . ano ) ;
20 }
21
22 int main () {
23 tData dataNascimento ;
24
25 dataNascimento = alteraData (30 ,01 ,1984) ;
26 dataNascimento = alteraData (16 ,05 ,2007) ;
27
28 consultaData ( dataNascimento ) ;
29 return 0;
30 }

Exercı́cio Resolvido 4.3 - Menor Data

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.

1 # include < stdio .h >


2
3 struct data { // estrutura do tipo data
4 int dia ;
5 int mes ;
6 int ano ;
7 };
8
126 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

9 struct data menorData ( struct data data0 , struct data data1 ) {


10 if ( data0 . ano < data1 . ano ) {
11 return data0 ;
12 } else if ( data1 . ano < data0 . ano ) {
13 return data1 ;
14 } else if ( data0 . mes < data1 . mes ) {
15 return data0 ;
16 } else if ( data1 . mes < data0 . mes ) {
17 return data1 ;
18 } else if ( data0 . dia < data1 . dia ) {
19 return data0 ;
20 } else {
21 return data1 ;
22 }
23 }
24
25 struct data leData () {
26 struct data d ;
27
28 printf ( " Entre com a data " ) ;
29 scanf ( " % d % d % d " ,& d . dia ,& d . mes ,& d . ano ) ;
30 return d ;
31 }
32
33 main () {
34 // declaraç~
a o de variaveis
35 struct data data0 , data1 , menor ;
36
37 // entrada das datas
38 printf ( " Primeira data :\ n " ) ;
39 data0 = leData () ;
40 printf ( " \ nSegunda data :\ n " ) ;
41 data1 = leData () ;
42
43 // determina a menor data
44 menor = menorData ( data0 , data1 ) ;
45
46 printf ( " \ nA menor data eh : % d / % d / % d .\ n " , menor . dia , menor . mes , menor . ano ) ;
47 }

Exercı́cio Resolvido 4.4 - TAD Tponto

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 ;

Exercı́cio Resolvido 4.5 - Operação Tponto simetricoOrigem (Tponto ponto)

Agora, implemente a operação Tponto simetricoOrigem (Tponto ponto), para deter-


minar o ponto simétrico de um ponto em relação à origem.

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.

1 Tponto simetricoOrigem ( Tponto ponto ) {


2 ponto . x = - ponto . x ;
3 ponto . y = - ponto . y ;
4
5 return ponto ;
6 }

Exercı́cio Resolvido 4.6 - Operação int qualQuadrante (Tponto ponto)

Agora, implemente a operação int qualQuadrante (Tponto ponto) que determina em


qual quadrante pertence um ponto.

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.

1 int qualQuadrante ( Tponto ponto ) {


2 if ( ponto . x == 0 || ponto . y == 0) {
3 return 0;
4 } else {
5 if ( ponto . x > 0) {
6 if ( ponto . y > 0) {
7 return 1;
8 } else {
9 return 4;
10 }
11 } else {
128 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

12 if ( ponto . y > 0) {
13 return 2;
14 } else {
15 return 3;
16 }
17 }
18 }

Exercı́cio Resolvido 4.8 - Operação float distanciaPontos (Tponto a, Tponto b)

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.

1 # include < stdio .h >


2
3 struct ponto {
4 int x , y ;
5 }
6
7 typedef struct ponto Tponto ;
8
9 // Operaç~
a o construtora :
10 Tponto ler () {
11 Tponto ponto ;
12
13 printf ( " Entre com as coordenadas \ n " ) ;
14 printf ( " X : " ) ;
15 scanf ( " % d " , & ponto . x ) ;
16 printf ( " Y : " ) ;
17 scanf ( " % d " , & ponto . y ) ;
18
19 return ponto ;
20 }
21
22 // Operaç~
a o destrutora :
23 Tponto zera () {
24 Tponto origem = {0 , 0};
4.5. RESUMO 129

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.

4.6 Lista de Exercı́cios

Exercı́cio 4.1 - TAD tCilindro

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:

• leitura dos dados;


4.6. LISTA DE EXERCÍCIOS 131

• Inicializa Cilindro;

• Altera Altura;

• Altera Raio;

• Calcula Volume;

• Imprime Dimensões;

• Imprime Volume;

Exercı́cio 4.2 - Determinando a Idade

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.

Exercı́cio 4.3 - TAD tPonto

Implemente um TAD Ponto para representar um ponto no plano com as seguintes operações:

• leitura: leitura via console;

• desloca x: realiza um deslocamento horizontal;

• desloca y: realiza um deslocamento vertical;

• atribui x y: atribui novos valores às coordenadas de um ponto;

• distancia: calcula a distância entre dois pontos;

• imprime: imprime as coordenadas do ponto.

Exercı́cio 4.4 - TAD tCirculo

Implemente um TAD Circulo para representar um cı́rculo (Observe que o centro é um ponto)
com as seguintes operações:

• leitura: leitura via console;


132 CAPÍTULO 4. TIPOS ABSTRATOS DE DADOS

• atribui raio: atribui um valor r ao raio do circulo;

• atribui centro: atribui novo valor ao centro do circulo;

• area: calcula a área do cı́rculo;

• interior: verifica se um dado ponto está dentro do cı́rculo.

Exercı́cio 4.5 - TAD tTrapezio

Implemente um TAD Trapézio para representar um trapézio com as seguintes operações:

• leitura: função leitura para cada lado do trapézio;

• atribuicao: atribui individualmente cada lado;

• area: calcula a área do trapézio;

• interior: verifica se um dado ponto está dentro do trapézio.

Exercı́cio 4.6 - Imobiliaria

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.

4.7 Trabalhos Sugeridos

Trabalho 4.1 - Estacionamento

Considere o sistema responsável pelo controle de entrada e saı́da de veı́culos de um estaci-


onamento que funciona 24 h por dia. Um motorista ao entrar no estacionamento recebe um
ticket indicando o horário e a data de entrada, na saı́da do estacionamento o atendente realiza
a cobrança pelo uso do serviço.
O preço do uso do estacionamento por hora é de R$ 1,00 nos dias úteis e R$ 2,00 em sábados,
domingos e feriados.
4.7. TRABALHOS SUGERIDOS 133

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:

• ler horário de entrada;

• ler data de entrada;

• obter horário do sistema;

• obter data do sistema;

• cálculo de tempo no estacionamento;

• cálculo de custo;

• determinação do horário e data de saı́da para um determinado custo de uso do estaciona-


mento considerando um determinado horário de entrada.

Trabalho 4.2 - Banco

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

2. Os dois correntistas que produziram o maior saldo nas suas operações.

3. O saldo total das operações na agência .

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

Trabalho 4.3 - NPD UFES

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:

1. O coeficiente de rendimento de cada aluno .

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.

4. Os três alunos com maior número de reprovações na UFES.

Nos ı́tens 1 e 2 devem ser apresentados o curso, a matrı́cula e o coeficiente do aluno. No


item 3 devem ser apresentados o curso e o coeficiente de rendimento médio. No item 4, devem
ser apresentados o curso, a matrı́cula e o número total de reprovações. Considere que os dados
de cada aluno e curso são fornecidos contiguamente, isto é, são fornecidos todos os dados das
disciplinas cursadas por um aluno em um curso, depois todos os dados de outro aluno deste
curso e assim sucessivamente até terminar todos os dados de alunos do curso. Esse processo se
repete para todos os cursos da UFES. O processamento deve ser encerrado quando for lido um
código de curso igual a zero.
Capı́tulo 5

Vetores
Autores:

Andrezinho
Thiago Paris Salviato

Objetivos:

• Introduzir a estrutura de dados vetor e mostrar sua importância;

• Apresentar suas operações básicas e discutir os cuidados a serem tomados ao utilizá-las;

• Definir um TAD Implementacional tVetorInt;

• Apresentar aplicações para o TAD.

5.1 Vetores e sua importância


Imagine que se queira fazer um programa para manusear dados de um mesmo tipo - as idades
das pessoas de uma famı́lia, ou os salários dos funcionários de uma empresa, por exemplo.
Uma alternativa seria a utilização de várias variáveis ou então a leitura de valores sempre que
necessários. Mas será que essas seriam as melhores soluções? E se o programa for grande e
haja necessidade de que ele seja modularizado? Não ficaria ruim na hora de passar todas essas
variáveis como parâmetros? Ao se refletir sobre essas perguntas, outra idéia pode vir à tona:
a utilização de um TAD, já que, dessa forma, as variáveis podem ser encapsuladas numa só
estrutura e apenas ela poderia ser passada como parâmetro! Boa idéia! Mas e se fossem 100
valores diferentes? Ou se houvesse a necessidade de mantê-los ordenados, por exemplo? Como
já se pode perceber, nem sempre os tipos básicos de dados (int, float, char) são suficientes para
exprimir estruturas de dados em algoritmos.
Para ilustrar melhor o problema considere que um professor tenha uma turma de 50 alunos
e deseja saber quantos deles tiveram nota acima da média da turma. Com os instrumentos
apresentados até o capı́tulo anterior, seria necessária a leitura das notas dos alunos por duas

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.

1 # include < stdio .h >


2
3 main () {
4 int i ;
5 int quant ;
6 float nota ;
7 float soma ;
8 float media ;
9
10 nota = 0.0;
11 soma = 0.0;
12 media = 0.0;
13
14 for ( i = 0; i < 50 ; i ++) {
15 printf ( " Digite uma nota " ) ;
16 scanf ( " % f " ,& nota ) ;
17 soma = soma + nota ;
18 }
19
20 media = soma /50;
21
22 quant = 0;
23
24 for ( i = 0; i < 50 ; i ++) {
25 printf ( " Digite uma nota \ n " ) ;
26 scanf ( " % f " ,& nota ) ;
27 if ( nota > media ) quant ++;
28 }
29
30 printf ( " \ n % d alunos obtiveram nota acima da média \ n " , quant ) ;
31 }

Exemplo 5.1: Resolução do exemplo do professor sem a utilização de vetores

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

disponı́veis para serem utilizados durante todos os cálculos.

5.2 Representação
A Figura 5.1 mostra a maneira mais comum de se representar um vetor graficamente.

Figura 5.1: Representação de um vetor

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

Figura 5.2: Vetores na memória do computador

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.

1 int vet [9];


2
3 char nome [9];
4
5 float notas [6];
6
7 float notasAlunos [50];

Exemplo 5.2: Algumas declarações 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.

5.3.1 Definição do Tamanho do Vetor

Na linguagem C, o tamanho do vetor pode ser definido em dois momentos diferentes: em


tempo de compilação e em tempo de execução; lembrando sempre que, em ambos os casos,
uma vez que ele for definido e que a memória for alocada, o tamanho não poderá ser alterado.
Alguns fatores devem ser considerados na hora de escolher entre essas duas abordagens, como o
consumo de memória e o tipo de aplicação.

Definição em tempo de compilação

A definição em tempo de compilação é caracterizada pelo número inteiro entre colchetes na


declaração do vetor. Todas as declarações mostradas no Exemplo 5.2 são exemplos de vetores
definidos em tempo de compilação, antes da execução do programa.
Então, para armazenar dados nesse tipo de vetor, é necessário que se tenha conhecimento
prévio da quantidade máxima de elementos que nele será armazenado.
Suponha que se queira fazer um programa para cadastrar os alunos de uma escola de ensino
médio com cinco turmas por ano e fazer um acompanhamento de suas notas. Considere também
que cada aluno pode cursar, no máximo, oito matérias por ano, duas delas optativas. O Exemplo
5.3 mostra uma estrutura que poderia ser utilizada para representar um aluno na linguagem C.

1 typedef struct aluno {


2 char nome [50];
3 int rg ;
4 int ano ;
5 char turma ;
6 float notas [8];
7 } tAluno ;

Exemplo 5.3: Definição da Estrutura Aluno

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.

Definição em tempo de execução

A definição do tamanho em tempo de execução, pode ser implementada substituindo o


número inteiro que é colocado entre colchetes na declaração do vetor por uma variável que só
ganhará um valor durante a execução do programa. O Exemplo 5.4 mostra como isso pode ser
feito na linguagem C.

1 # include < stdio .h >


2
3 main () {
4
5 int num ;
6
7 ...
8
9 printf ( " Quantas notas deseja armazenar no vetor ? " ) ;
10 scanf ( " % d " ,& num ) ;
11
12 float notas [ num ];
13
14 ...
15
16 }

Exemplo 5.4: Definição de um vetor em tempo de execução

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.

1 # include < stdio .h >


2
3 main () {
4 int i ;
5 int vet1 [10];
6 int vet2 [10];
7 float notas [8];
8 float soma ;
9 float media ;
10
11 vet1 [0] = 43;
12
13 for ( i =0; i <5; i ++) {
14 vet1 [2* i +1] = 10;
15 }
16
17 for ( i =0; i <10; i ++) {
18 vet2 [ i ] = 0;
19 }
20
21 for ( i =0; i <8; i ++) {
22 scanf ( " % f " ,& notas [ i ]) ;
23 }
24
25 for ( i =0; i <10; i ++) {
26 vet2 [ i ] = vet1 [ i ];
27 }
28
29 for ( i =0; i <8; i ++) {
30 soma += notas [ i ];
31 }
32 media = soma /8;
33 }

Exemplo 5.5: Operações sobre elementos de um vetor


142 CAPÍTULO 5. VETORES

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.

1 # include < stdio .h >


2
3 main () {
4 int i ;
5 int quant ;
6 float notas [50];
7 float soma ;
8 float media ;
9
10 nota = 0.0;
11 soma = 0.0;
12 media = 0.0;
13
14 for ( i = 0; i < 50 ; i ++) {
15 printf ( " Digite uma nota " ) ;
16 scanf ( " % f " ,& notas [ i ]) ;
17 soma = soma + nota [ i ];
18 }
19
20 media = soma /50;
21
22 quant = 0;
23
24 for ( i = 0; i < 50 ; i ++) {
25 if ( notas [ i ] > media ) quant ++;
26 }
27
28 printf ( " \ n % d alunos obtiveram nota acima da média \ n " , quant ) ;
29 }

Exemplo 5.6: Resolução do exemplo do professor com a utilização de vetores


5.4. OPERAÇÕES 143

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.

1 # include < stdio .h >


2
3 float calculaMedia ( float vetor [50]) {
4 int i ;
5 float soma ;
6 float media ;
7
8 soma = 0.0;
9
10 for ( i = 0 ; i < 50 ; i ++)
11 soma = soma + vetor [ i ];
12 }
13
14 media = soma /50;
15
16 return ( media ) ;
17 }
18
19 int alunosAcimaMedia ( float vetor [50] , float media ) {
20 int i ;
21 int quant ;
22
23 quant = 0;
24 for ( i = 0 ; i < 50 ; i ++)
25 if ( vetor [ i ] > media ) quant ++;
26 }
27
28 return ( quant ) ;
29 }
30
31 main () {
32 int i ;
33 int quant ;
34 float notas [50];
35 float soma ;
36 float media ;
37
38 nota = 0.0;
39 soma = 0.0;
40 media = 0.0;
144 CAPÍTULO 5. VETORES

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 }

Exemplo 5.7: Passagem de vetores para função

Algumas caracterı́sticas da linguagem C tornam a utilização de vetores passados como


parâmetro perigosa quando não tomados os devidos cuidados. Por isso essa técnica não será
usada agora.

5.4.1 Acesso indevido


A Figura 5.2 da seção 5.2 também pode ser utilizada para ilustrar um exemplo de um problema
que ocorre com certa freqüência em programas na linguagem C, já que ela não faz nenhuma
restrição ao acesso de elementos fora dos limites do vetor. Portanto, se, por acidente, um
programador tentasse acessar em seu programa o ı́ndice 14 do vetor nome representado na
figura, por exemplo, nenhum erro seria acusado em tempo de compilação, ou de execução. O
erro só poderia ser percebido em tempo de execução, quando o computador retornasse um valor
inesperado no programa.
Mas isso é o mı́nimo que pode acontecer. Se o programador utilizasse um ı́ndice diferente,
ele poderia acessar e apagar alguma faixa de memória de outro programa e acabar causando
danos irreversı́veis. Logo, a solução mais adequada é sempre avaliar os limites de um vetor antes
de manipulá-lo.

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

Figura 5.3: Vetor de caracteres.

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.

1 void imprime_string ( char palavra []) {


2 int i ;
3
4 for ( i =0; palavra [ i ] != ‘\0 ’; i ++) {
5 printf ("% c " , palavra [ i ]) ;
6 }
7 printf ("\ n ") ;
8 }

Exemplo 5.8: Subprograma para impressão de strings, utilizando diretamente o “barra-zero”.

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.

1 void imprime_string ( char palavra []) {


2
3 printf ( " % s \ n " , palavra ) ;
4 }

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.

1 # include < stdio .h >


2 # include < string .h >
3
4 main () {
5 char vet1 [20] = " ola enfermeira " ;
6 char vet2 [20];
7 char vet3 [20];
8
9 printf ( " Tamanho da string : % d \ n \ n " , strlen ( vet1 ) ) ;
10
146 CAPÍTULO 5. VETORES

11 strcpy ( vet2 , vet1 ) ;


12 printf ( " % s \ n % s \ n \ n " , vet1 , vet2 ) ;
13
14 strcpy ( vet3 , " sim senhor " ) ;
15 printf ( " % s \ n \ n " , vet3 ) ;
16
17 if ( strcmp ( vet1 , vet2 ) == 0) {
18 printf ( " As strings sao iguais " ) ;
19 } else {
20 printf ( " As strings sao diferentes " ) ;
21 }
22
23 if ( strcmp ( vet1 , vet3 ) == 0) {
24 printf ( " As strings sao iguais " ) ;
25 } else {
26 printf ( " As strings sao diferentes " ) ;
27 }
28 }

Exemplo 5.10: Programa demonstrativo de algumas funções da biblioteca padrão de C para


manipulação de strings.

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 TAD Implementacional


Como dito na seção 5.4, na linguagem C não existem operações básicas para a manipulação
de vetores como um todo. Porém a manipulação individual de todos os seus elementos pode
ser muito trabalhosa e muitas vezes operações comuns devem ser repetidas ao longo do código
do programa comprometendo sua legibilidade, confiabilidade e modificabilidade. Por isso, as
operações básicas sobre um vetor devem ser modularizadas pela utilização de subprogramas e,
5.6. TAD IMPLEMENTACIONAL 147

para isso, a utilização de um TAD se mostra oportuna.


Nessa seção será apresentado na linguagem C um TAD de natureza implementacional, cha-
mado tVetorInt, que poderá ser usado em aplicações que necessitam manipular vetores de intei-
ros. Para isso será definida uma estrutura contendo um vetor e o número de elementos correntes
do vetor.

5.6.1 Atributos

O Exemplo 5.11 mostra os atributos da estrutura utilizada no TAD Implementacional tVetorInt.

1 # define TAM 100


2
3 typedef struct vetor {
4 int vet [ TAM ];
5 int n ;
6 } tVetorInt ;

Exemplo 5.11: Definição da Estrutura tVetorInt

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

Figura 5.4: Representação gráfica da estrutura tVetorInt.

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.

1 tVetorInt iniciaVazioVetorInt ( tVetorInt v ) {


2 v . n = 0;
3 return ( v ) ;
4 }

Exemplo 5.12: Inicialização de um tVetorInt vazio

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.

1 tVetorInt leituraVetorInt ( tVetorInt v , int num ) {


2 int i ;
3 for ( i = 0 ; i < num ; i ++) scanf ( " % d " ,& v . vet [ i ]) ;
4 v . n = num ;
5 return ( v ) ;
6 }

Exemplo 5.13: Inicialização de um tVetorInt com leitura de elementos


5.6. TAD IMPLEMENTACIONAL 149

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.

1 void escritaVetorInt ( tVetorInt v ) {


2 int i ;
3
4 printf ( " \ n \ n " ) ;
5 printf ( " Os elementos que estao atualmente no vetor sao : " ) ;
6 printf ( " \ n " ) ;
7 for ( i = 0 ; i < v . n ; i ++) printf ( " % d " ,v . vet [ i ]) ;
8 printf ( " \ n \ n " ) ;
9 }

Exemplo 5.14: Escrita em tela

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.

1 int p e s q u isaSe quen cial Int ( tVetorInt v , int elem ) {


2 int i ;
3
4 for ( i = 0 ; i < v . n ; i ++) {
5 if ( v . vet [ i ]== elem ) return ( i ) ;
6 }
7 return ( -1) ;
8 }

Exemplo 5.15: Pesquisa Seqüencial

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.

1 int pe sq uisaBinariaInt ( tVetorInt v , int elem ) {


2 int inicio ;
3 int fim ;
4 int meio ;
5
6 inicio = 0;
7 fim = v . n - 1;
8
9 while ( inicio <= fim ) {
10 meio = ( inicio + fim ) /2;
11 if ( elem < v . vet [ meio ]) fim = meio - 1;
12 else if ( elem > v . vet [ meio ]) inicio = meio + 1;
13 else return ( meio ) ;
14 }
15 return ( -1) ;
16 }

Exemplo 5.16: Pesquisa Binária

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.

1 tVetorInt somaVetorInt ( tVetorInt v1 , tVetorInt v2 , tVetorInt soma ) {


2 int i ;
3
4 if ( v1 . n != v2 . n ) {
5 printf ( " \ n \ n " ) ;
6 printf ( " Vetores com tamanhos diferentes " ) ;
7 printf ( " \ n \ n " ) ;
8 soma . n = 0;
9 } else {
10 soma . n = v1 . n ;
11 for ( i = 0 ; i < soma . n ; i ++) {
12 soma . vet [ i ] = v1 . vet [ i ] + v2 . vet [ i ];
13 }
14 }
15 return ( soma ) ;
16 }

Exemplo 5.17: Adição de dois vetores

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.

1 int escalaVetorInt ( tVetorInt v1 , tVetorInt v2 ) {


2 int i ;
3 int prod ;
4
5 prod = 0;
6 if ( v1 . n != v2 . n ) {
7 printf ( " \ n \ n " ) ;
8 printf ( " Vetores com tamanhos diferentes " ) ;
9 printf ( " \ n \ n " ) ;
10 } else {
11 for ( i = 0 ; i < v1 . n ; i ++) {
12 prod = prod + v1 . vet [ i ] * v2 . vet [ i ];
13 }
14 }
15 return ( prod ) ;
16 }

Exemplo 5.18: Produto Escalar

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.

1 tVetorInt insereVetorInt ( tVetorInt v , int elem , int pos ) {


2 int i ;
3
154 CAPÍTULO 5. VETORES

4 if (( pos < 0) || ( pos > v . n ) ) {


5 printf ( " \ n \ n " ) ;
6 printf ( " Entrada de Dados Incorreta " ) ;
7 printf ( " \ n \ n " ) ;
8 } else {
9 v . n ++;
10 for ( i = v . n - 2 ; i > pos - 1 ; i - -) {
11 v . vet [ i +1] = v . vet [ i ];
12 }
13 v . vet [ i ] = elem ;
14 }
15 return ( v ) ;
16 }

Exemplo 5.19: Inserção em Posição Pré-determinada

A função insereVetorInt recebe como parâmetros de entrada uma estrutura tVetorInt, o


elemento a ser inserido e a posição em que ele deve ser inserido. Se a posição for inválida, ou
seja, menor que 0 ou maior que v.n, uma mensagem de erro é impressa na tela e o elemento não
é inserido. Caso contrário os elementos de dentro do vetor vão sendo movidos para a direita de
modo a deixar a posição desejada para a inserção livre. Finalmente, o vetor recebe o valor do
elemento na posição desejada e a estrutura é retornada.
A função do Exemplo 5.20 mostra um algoritmo de exclusão de um elemento. Ela percorre
o vetor da estrutura sequencialmente procurando o elemento a ser excluı́do. Se encontrá-lo,
retira-o e continua procurando outros iguais para excluir até não achar mais.

1 tVetorInt excluiVetorInt ( tVetorInt v , int elem ) {


2 int i ;
3 int pos ;
4 int quant ;
5
6 pos = 0;
7 quant = 0;
8 while ( pos < v . n ) {
9 if ( v . vet [ pos ] == elem ) {
10 for ( i = pos ; i < v . n - 1 ; i ++) {
11 v . vet [ i ] = v . vet [ i +1];
12 }
13 v .n - -;
14 }
15 pos ++;
16 }
17 return ( v ) ;
18 }

Exemplo 5.20: Exclusão de Elemento

A função excluiVetorInt recebe como parâmetros de entrada uma estrutura tVetorInt e o


elemento que se quer excluir. Ela, então, varre o vetor da estrutura da primeira à última posição
5.6. TAD IMPLEMENTACIONAL 155

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.

1 tVetorInt ordenaMenorInt ( tVetorInt v ) {


2 int pos ;
3 int i ;
4 int indice_menor ;
5 int menor ;
6
7 for ( pos = 0 ; pos < v . n - 1 ; pos ++) {
8 menor = v . vet [ pos ];
9 for ( i = pos + 1 ; i < v . n ; i ++) {
10 if ( v . vet [ i ] < menor ) {
11 menor = v . vet [ i ];
12 indice_menor = i ;
13 }
14 }
15 v . vet [ indice_menor ] = v . vet [ pos ];
16 v . vet [ pos ] = menor ;
17 }
18 return ( v ) ;
19 }

Exemplo 5.21: 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.

1 tVetorInt ordenaBolhaInt ( tVetorInt v ) {


2 int a ;
3 int b ;
4 int temp ;
5
6 for ( a = 1 ; a < v . n ; a ++) {
7 for ( b = v . n - 1 ; b >= a ; --b ) {
8 if ( v . vet [b -1] > v . vet [ b ]) {
9 temp = v . vet [b -1];
10 v . vet [b -1] = v . vet [ b ];
156 CAPÍTULO 5. VETORES

11 v . vet [ b ] = temp ;
12 }
13 }
14 }
15 return ( v ) ;
16 }

Exemplo 5.22: Ordenação pelo método da bolha

A função ordenaBolhaInt também recebe como parâmetro de entrada apenas a estrutura


tVetorInt. Então ela percorre o vetor inteiro, do maior para o menor ı́ndice, fazendo comparações
entre seus elementos e trocando-os quando o elemento de maior ı́ndice possui seu valor menor
que o elemento de menor ı́ndice. Feito isso uma vez, o elemento de menor valor já fica na
primeira posição do vetor. Então o algoritmo ignora esse elemento e repete o procedimento para
o resto do vetor. Esse procedimento é repetido v.n − 1 vezes. Finalmente, a função retorna a
estrutura com o vetor ordenado. A Figura 5.8 ilustra o funcionamento desse algoritmo.

Exemplo de Utilização do TAD tVetorInt

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

25 else printf ( " \ n \ nElemento nao encontrado \ n \ n " ) ;


26
27 printf ( " \ n \ nQual elemento deseja localizar utilizando a pesquisa binaria ?\ n \ n " ) ;
28 scanf ( " % d " ,& elem ) ;
29
30 pos = pe squisaBinariaInt ( vet1 ,3) ;
31 if ( pos >= 0) printf ( " \ n \ nElemento encontrado na posicao % d \ n \ n " , pos ) ;
32 else printf ( " \ n \ nElemento nao encontrado \ n \ n " ) ;
33
34 vet3 = somaVetorInt ( vet1 , vet2 , vet3 ) ;
35
36 printf ( " \ n \ n " ) ;
37 printf ( " Vet1 : " ) ;
38 escritaVetorInt ( vet1 ) ;
39 printf ( " Vet2 : " ) ;
40 escritaVetorInt ( vet2 ) ;
41 printf ( " Vet3 : " ) ;
42 escritaVetorInt ( vet3 ) ;
43
44 prodEscalar = escalaVetorInt ( vet1 , vet2 ) ;
45
46 printf ( " \ n \ n " ) ;
47 printf ( " Vet1 : " ) ;
48 escritaVetorInt ( vet1 ) ;
49 printf ( " Vet2 : " ) ;
50 escritaVetorInt ( vet2 ) ;
51
52 printf ( " \ n \ nProduto escalar = % d \ n \ n " , prodEscalar ) ;
53
54 vet3 = leituraVetorInt ( vet3 ,0) ;
55
56 vet3 = insereVetorInt ( vet3 ,5 ,0) ;
57 printf ( " Vet3 : " ) ;
58 escritaVetorInt ( vet3 ) ;
59
60 vet3 = insereVetorInt ( vet3 ,3 ,0) ;
61 printf ( " Vet3 : " ) ;
62 escritaVetorInt ( vet3 ) ;
63
64 vet3 = insereVetorInt ( vet3 ,7 ,1) ;
65 printf ( " Vet3 : " ) ;
66 escritaVetorInt ( vet3 ) ;
67
68 vet3 = insereVetorInt ( vet3 ,10 ,3) ;
69 printf ( " Vet3 : " ) ;
70 escritaVetorInt ( vet3 ) ;
71
72 vet3 = excluiVetorInt ( vet3 ,3) ;
73 printf ( " Vet3 : " ) ;
74 escritaVetorInt ( vet3 ) ;
75 vet3 = excluiVetorInt ( vet3 ,7) ;
158 CAPÍTULO 5. VETORES

76 printf ( " Vet3 : " ) ;


77 escritaVetorInt ( vet3 ) ;
78 vet3 = excluiVetorInt ( vet3 ,10) ;
79 printf ( " Vet3 : " ) ;
80 escritaVetorInt ( vet3 ) ;
81 vet3 = excluiVetorInt ( vet3 ,5) ;
82 printf ( " Vet3 : " ) ;
83 escritaVetorInt ( vet3 ) ;
84
85 vet1 = ordenaMenorInt ( vet1 ) ;
86 printf ( " Vet1 : " ) ;
87 escritaVetorInt ( vet1 ) ;
88
89 vet2 = ordenaBolhaInt ( vet2 ) ;
90 printf ( " Vet2 : " ) ;
91 escritaVetorInt ( vet2 ) ;
92
93 }

Exemplo 5.23: Exemplo de Utilização do TAD tVetorInt

5.7 Exercı́cios Resolvidos

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.

• São caracterı́sticas importantes dos vetores a seqëncialização, a indexação e a sua


dimensão.

• Deve-se tomar cuidado para não se acessar um vetor indevidamente, como, por exemplo
utilizando um ı́ndice maior que seu tamanho.

• A dimensão de um vetor pode ser definida, em C, tanto em tempo de compilação, quanto


em tempo de execução.

• Na linguagem C não existem operações básicas para a manipulação de um vetor como um


todo. Seus elementos devem ser manipulado separadamente.

• Quando acessado individualmente, cada elemento de um vetor pode ser encarado como
uma variável básica.

• A utilização de expressões como ı́ndice no acesso a elementos de um vetor é uma ferramenta


poderosa e deve ser usada.
5.9. LISTA DE EXERCÍCIOS 159

• A utilização de funções para a manipulação de vetores como um todo é importante para


a legibilidade e reusabilidade do programa e deve ser usada.
• A utilização de TAD’s Implementacionais é um artifı́cio para facilitar a manipulação de
vetores e aumentar a legibilidade e reusabilidade do programa.
• O TAD Implementacional tVetorInt, implementado aqui na linguagem C, pode ser utili-
zado, sempre que necessário, para a manipulação de vetores de números inteiros.

5.9 Lista de Exercı́cios


1. Faça um programa em linguagem C que leia 100 números reais e os imprima na ordem
inversa em que foram lidos.
2. Crie uma função em linguagem C que some os elementos de um vetor com 100 elementos
inteiros.
3. Faça um programa em linguagem C que leia as notas e as matrı́culas dos alunos de uma
turma de no máximo 50 alunos numa prova e obtenha:
(a) a melhor nota e o aluno que a obteve;
(b) a pior nota e o aluno que a obteve;
(c) a média da turma;
(d) os alunos que obtiveram nota superior à média da turma.
4. Faça um programa em linguagem C que determine os 100 primeiros números primos.
Considere que:
(a) um número é primo quando só é divisı́vel por si mesmo;
(b) é suficiente testar a divisibilidade de um número X por números primos que o ante-
cedem até um limite igual a raiz de X.
5. Faça um programa em linguagem C que leia duas listas de caracteres com até 100 elementos
e coloque cada um num vetor respectivo. Após ter sido realizado a leitura das duas listas,
deve ser lida a ordem a partir da qual deve ser inserida a segunda lista na primeira lista
de caracteres. Considere o seguinte exemplo:

Primeira lista:
abcdjklm
Segunda lista:
efghi
Ordem:
5
Resultado:
abcdefghijklm
160 CAPÍTULO 5. VETORES

6. Faça um programa em linguagem C que leia dois vetores ordenados em M e N elementos


respectivamente e os intercale gerando um novo vetor U, também ordenado. Considere o
exemplo:

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.

8. Faça um programa em linguagem C que:

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

9. Crie um procedimento em linguagem C que cumpra a seguinte especificação:

(a) Dados de entrada:


i. um vetor de caracteres com um máximo de 100 elementos;
ii. o tamanho corrente do vetor (suponha que o vetor inicie na primeira posição);
iii. um caracter a ser inserido;
iv. a posição do vetor onde o caracter deve ser inserido.
(b) Dados de saı́da:
i. o vetor com o caracter inserido na posição;
ii. o vetor deve ser mantido inalterado se a posição onde deveria ser inserido o carcter
superior ao tamanho corrente do vetor ou se o vetor já estiver completo.
5.10. TRABALHOS SUGERIDOS 161

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:

F aturamento = Σ(quantidadei ∗ precoi ), i = 1, 2, . . . , 100.

11. Escreva um algoritmo em linguagem C que:

(a) leia um conjunto A de 100 elementos reais;


(b) construa e imprima um outro conjunto B formado da seguinte maneira:
i. os elementos de ordem par são os correspondentes de A divididos por 2;
ii. os elementos de ordem ı́mpar são os correspondentes de A multiplicados por 3.

12. Dado um vetor contendo uma frase com 80 caracteres (incluindo brancos), escreva um
algoritmo em linguagem C para:

(a) contar quantos brancos existem na frase;


(b) contar quantas vezes aparece a letra ’A’;
(c) contar quantas vezes aparece um mesmo par de letras na frase e quais são eles.

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.

5.10 Trabalhos Sugeridos


162 CAPÍTULO 5. VETORES

Figura 5.7: Método do Menor


5.10. TRABALHOS SUGERIDOS 163

Figura 5.8: Método da Bolha


Capı́tulo 6

Matrizes
Autores:

Clebson Oliveira
Julio Dalfior

Objetivos:

• Mostrar a importância de se utilizar a estrutura implementacional de dados matrizes para


resolver problemas

• Introduzir o conceito do tipo abstrato de dado matriz (tMatriz)

• Mostrar como modelar um problema através do TAD tMatriz

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

Figura 6.1: Uma matriz 5 por 3

Figura 6.2: Agenda de compromissos e sua representação matricial

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

6.2 Definição e Acesso

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:

1 int matriz [5][10];

Exemplo 6.1: Definição estática

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.

Figura 6.3: Matriz 5 por 10

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 }

Exemplo 6.2: Inicialização de uma matriz

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 }

Exemplo 6.3: Acesso e atualização de um elemento

A variável resposta recebe 0 ou 1 indicando, respectivamente, se às 11 horas do dia 2 de


junho a sala está livre ou reservada. Note que, assim como em vetores, o primeira posição
de cada dimensão da matriz tem ı́ndice 0. Após acessar um elemento da matriz chamada
reservaSala (linha 12) e verificar que em uma determinada data e horário a sala está livre (linha
14), atualiza-se o estado da sala de livre para ocupada (linha 16).
168 CAPÍTULO 6. MATRIZES

6.3 O TAD implementacional tMatriz


O TAD implementacional tMatriz é usado como ferramenta para resolver problemas que possuam
caracterı́stica de armazenamento relacionados à matrizes. Uma de suas principais vantagens é
poder simular uma matriz cujo tamanho pode ser alterado dinamicamente, o que proporciona a
reutilização dessa estrutura.

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:

• Número de linhas (nLinhas): Armazena a quantidade de elementos de cada coluna.

• Número de colunas (nColunas): Armazena a quantidade de elementos de cada linha.

• Matriz (valores): Armazena os dados da matriz propriamente dita.

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 ;

Exemplo 6.4: Estrutura básica do tipo 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 }

Exemplo 6.5: Inicialização de uma estrutura tMatrizInt

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

forma, vamos definir algumas operações sobre o tMatrizInt: inicialização, atualização de


valor, acessoa valor, exibição, soma dos elementos de uma linha, soma dos elementos
de uma coluna e produto escalar.

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:

1 tMatrizInt inicializa ( int nLinhas , int nColunas ) {


2
3 tMatrizInt resultado ;
4
5 resultado . nLinhas = nLinhas ;
6 resultado . nColunas = nColunas ;
7
8 return resultado ;
9 }

Exemplo 6.6: Função de inicialização

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

Exemplo 6.7: Escrita em um elemento da matriz

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

1 int acessaMatriz ( tMatrizInt matriz , int linha , int coluna )


2 {
3
4 if ( ( linha >=0 && linha < matriz . nLinhas ) && ( coluna >=0 && coluna < matriz .
nColunas ) ) {
5 return matriz . valores [ linha ][ coluna ];
6
7 } else {
8 return -1;
9 printf ( ‘ ‘ Ítem inexistente \ n ’ ’) ;
10
11 }
12 }

Exemplo 6.8: Função de acesso aos dados da matriz

Na linha 4 é realizada a mesma verificação que na linha equivalente do exemplo 6.7. Se o


elemento pertencer à matriz o seu valor é retornado. Caso contrário, o valor -1 é retornado e
uma mensagem de erro é impressa na tela. Note que, na maioria das aplicações, não será possı́vel
verificar a execução correta da função analisando o valor de retorno, pois o valor retornado (-1)
pode fazer parte do universo de valores válidos para a aplicação.

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

1 int exibeMatriz ( tMatrizInt matriz ) {


2
3 int i =0 , j =0;
4
5 for ( i =0; i < matriz . nLinhas ; i ++) {
172 CAPÍTULO 6. MATRIZES

6 for ( j =0; j < matriz . nColunas ; j ++) {


7 printf ( " %5 d \0 " , matriz . valores [ i ][ j ]) ;
8
9 }
10 printf ( " \ n " ) ;
11 }
12 }

Exemplo 6.9: Exibição da matriz na tela

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.

Figura 6.5: Exemplo de saı́da da operação exibe

Soma dos elementos de uma linha


Como em muitas aplicações é necessário obter o valor da soma dos elementos de uma linha, será
definida uma operação para efetuar esse cálculo.

1 int somaLinhaMatriz ( tMatrizInt matriz , int linha ) {


2
3 int i =0 , soma =0;
4
5 for ( i =0; i < matriz . nColunas ; i ++) {
6 soma = soma + matriz . valores [ linha ][ i ];
7 }
8
9 return soma ;
10 }

Exemplo 6.10: Soma dos elementos de uma linha

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

Soma dos elementos de uma coluna


Anteriormante foi definida a somda dos elementos de uma linha. Agora será definida uma
operação para efetuar a soma dos elementos de uma coluna.

1 int somaColunaMatriz ( tMatrizInt matriz , int coluna ) {


2
3 int i =0 , soma =0;
4
5 for ( i =0; i < matriz . nLinhas ; i ++) {
6 soma = soma + matriz . valores [ i ][ coluna ];
7 }
8
9 return soma ;
10 }

Exemplo 6.11: Soma dos elementos de uma coluna

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.

1 tMatrizInt produtoEscalar ( tMatrizInt matriz1 , tMatrizInt matriz2 ) {


2
3 int i =0 , j =0 , k =0;
4 tMatrizInt produto ;
5
6 if ( matriz1 . nColunas == matriz2 . nLinhas ) {
7
8 for ( i =0; i < matriz1 . nLinhas ; i ++) {
9
10 for ( j =0; j < matriz2 . nColunas ; j ++) {
11
12 produto . valores [ i ][ j ]=0;
13
14 for ( k =0; k < matriz1 . nColunas ; k ++) {
1
Vale ressaltar que A.B é diferente de B.A . Logo, o produto escalar entre matrizes não é comutativo
174 CAPÍTULO 6. MATRIZES

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 }

Exemplo 6.12: Soma dos elementos de uma fila

6.4 Exercı́cios Resolvidos


Exercı́cio Resolvido 6.1 - Alocação de sala

Deseja-se desenvolver um programa para controlar a reserva de uma sala ao longo de um


ano. Ao usuário devem ser fornecidas as seguintes operaçoes:
1. Reservar a sala
2. Cancelar reserva
3. Dado um mês, listar dias em que a sala está disponı́vel
4. Informar o ı́ndice de ocupação de cada mês
Implemente o programa utilizando o TAD tMatrizInt.

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

1 tMatrizInt reservaSala ( tMatrizInt mat , int dia , int mes ) {


2
3 if ( acessaMatriz ( mat , dia , mes ) == 0) {
4 printf ( " Sala reservada com sucesso \ n " ) ;
5 return atualizaMatriz ( mat , dia , mes , 1) ;
6 } else {
7 printf ( " Sala já reservada para a data informada \ n " ) ;
8 return mat ;
9 }
10 }

Exemplo 6.13: Reservar a sala

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.

1 tMatrizInt cancelaReserva ( tMatrizInt mat , int dia , int mes ) {


2
3 if ( acessaMatriz ( mat , dia , mes ) != 0) {
4 printf ( " Reserva cancelada " ) ;
5 return atualizaMatriz ( mat , dia , mes , 0) ;
6 } else {
7 printf ( " Sala n~
a o reservada para a data informada \ n " ) ;
8 return mat ;
9 }
10 }

Exemplo 6.14: Cancelar reserva

3. Listar dias livres do mês:


Deve-se verificar, em um laço, todos os dias do mês escolhido e, para cada dia, verificar
a reserva da sala. Se ela estiver ocupada, então o dia será impresso na tela de modo a
informar isso ao usuário. Note que o número de dia de cada mês varia e, portanto, deve
haver um controle de forma que o laço termine com o número de dias correto para o mês
escolhido.

1 void listaDiasLivres ( tMatrizInt mat , int mes ) {


2
3 int dia ;
4 int numeroDiasMes [] = {31 ,28 ,31 ,30 ,31 ,30 ,31 ,31 ,30 ,31 ,30 ,31};
5 printf ( " Dias livres para o m^
e s % d \ n " , mes +1) ;
6
176 CAPÍTULO 6. MATRIZES

7 for ( dia = 0; dia < numeroDiasMes [ mes ]; dia ++) {


8 if ( acessaMatriz ( mat , dia , mes ) == 0) {
9 printf ( " % d \ n " , dia +1) ;
10 }
11 }
12 }

Exemplo 6.15: Listar dias livres do mês

4. ı́ndide de ocupação de cada mês:


O ı́ndice de ocupação de um mês nada mais é do que o número de dias com a sala reservada
naquele mês. Além disso, como a sala ocupada é representada pelo número 1 e a sala vazia
pelo número 0, somando-se a coluna que representa um determinado mês obtem-se o
número de dias ocupados para esse mês. Assim sendo, para resolver o problema, basta
percorrer todas as colunas da matriz e, para cada uma delas, efetuar a soma de seus
elementos.

1 void im prim eIndi ceOc upaca o ( tMatrizInt mat ) {


2
3 int mes ;
4 int indice ;
5
6 printf ( " ı́ndice de ocupaç~
a o dos meses do ano \ n " ) ;
7
8 for ( mes = 0; mes < 12; mes ++) {
9 indice = somaColunaMatriz ( mat , mes ) ;
10
11 printf ( " Mes % d : % d dias ocupados \ n " , mes , indice ) ;
12 }
13 }

Exemplo 6.16: ndide de ocupação de cada mês

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 }

Exemplo 6.17: Interação com usuário

Exercı́cio Resolvido 6.2 - Soma de elementos

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:

• A soma dos elementos da diagonal principal

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.

1 int somaDiagonal ( tMatrizInt matriz ) {


2
3 int soma =0;
4 int i ;
5
6 for ( i =0; i < matriz . nLinhas && i < matriz . nColunas ; i ++) {
7 soma = soma + acessaMatriz ( matriz , i , i ) ;
8 }
9
10 return soma ;
11 }

Exemplo 6.18: Soma da diagonal principal

• A soma dos elementos da submatriz triangular superior

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.

1 int somaTriangular ( tMatrizInt matriz ) {


2
3 int soma =0;
4 int i , j ;
5
6 for ( i =0; i < matriz . nLinhas ; i ++) {
7 for ( j = i +1; j < nColunas ; j ++) {
8 soma = soma + acessaMatriz ( matriz , i , j ) ;
9 }
6.4. EXERCÍCIOS RESOLVIDOS 179

10 }
11
12 return soma ;
13 }

Exemplo 6.19: Soma da matriz triangular superior

Exercı́cio Resolvido 6.3 - Uns isolados

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

1 int unsIsolados ( tMatrizInt matriz ) {


2
3 int i ,j , unsisolados =0;
4
5 for ( i =0; i < matriz . nLinhas ; i ++) {
6
7 for ( j =0; j < matriz . nColunas ; j ++) {
8
9 if ( i ==0) {
10
11 if ( j ==0) {
180 CAPÍTULO 6. MATRIZES

12 if ( acessaMatriz ( matriz , i , j +1) ==0 && acessaMatriz ( matriz , i +1 , j


) ==0 && acessaMatriz ( matriz ,i , j ) ==1)
13 unsisolados ++;
14 } else {
15
16 if ( j == matriz . nColunas -1) {
17 if ( acessaMatriz ( matriz , i , j -1) ==0 && acessaMatriz ( matriz , i
+1 , j ) ==0 && acessaMatriz ( matriz ,i , j ) ==1)
18 unsisolados ++;
19 } else {
20
21 if ( acessaMatriz ( matriz , i +1 , j ) ==0 && acessaMatriz ( matriz , i ,
j +1) ==0 && acessaMatriz ( matriz , i , j -1) ==0 && acessaMatriz
( matriz ,i , j ) ==1)
22 unsisolados ++;
23 }
24 }
25 } else {
26
27 if ( i == matriz . nLinhas -1) {
28
29 if ( j ==0) {
30 if ( acessaMatriz ( matriz , i , j +1) ==0 && acessaMatriz ( matriz , i
-1 , j ) ==0 && acessaMatriz ( matriz ,i , j ) ==1)
31 unsisolados ++;
32 } else {
33
34 if ( j == matriz . nColunas -1) {
35 if ( acessaMatriz ( matriz , i , j -1) ==0 && acessaMatriz ( matriz ,
i -1 , j ) ==0 && acessaMatriz ( matriz ,i , j ) ==1)
36 unsisolados ++;
37 } else {
38
39 if ( acessaMatriz ( matriz , i -1 , j ) ==0 && acessaMatriz ( matriz ,
i , j +1) ==0 && acessaMatriz ( matriz , i , j -1) ==0 &&
acessaMatriz ( matriz ,i , j ) ==1)
40 unsisolados ++;
41 }
42 }
43 } else {
44
45 if ( j ==0 && i >0 && i < matriz . nLinhas -1) {
46 if ( acessaMatriz ( matriz , i , j +1) ==0 && acessaMatriz ( matriz , i
+1 , j ) ==0 && acessaMatriz ( matriz , i -1 , j ) ==0 &&
acessaMatriz ( matriz ,i , j ) ==1)
47 unsisolados ++;
48 } else {
49
50 if ( j == matriz . nColunas -1 && i >0 && i < matriz . nLinhas -1) {
51 if ( acessaMatriz ( matriz , i , j -1) ==0 && acessaMatriz ( matriz ,
i +1 , j ) ==0 && acessaMatriz ( matriz , i -1 , j ) ==0 &&
6.5. RESUMO 181

acessaMatriz ( matriz ,i , j ) ==1)


52 unsisolados ++;
53 } else {
54
55 if ( acessaMatriz ( matriz , i , j +1) ==0 && acessaMatriz ( matriz ,
i , j -1) ==0 && acessaMatriz ( matriz , i +1 , j ) ==0 &&
acessaMatriz ( matriz , i -1 , j ) ==0 && acessaMatriz ( matriz ,
i , j ) ==1)
56 unsisolados ++;
57 }
58 }
59 }
60 }
61 }
62 }
63
64 return unsisolados ;
65 }

Exemplo 6.20: Soma da matriz triangular superior

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.

6.6 Exercı́cios Propostos


1. Escreva um subprograma que determine o maior valor em uma matriz de valores inteiros
com n > 0 linhas e m > 0 colunas.

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.

7. Faça um subprograma que receba uma matriz quadrada (dimensões N x N) totalmente


preenchida com números inteiros e troque os elementos acima da diagonal principal pe-
los que estão abaixo dela. Atente para o fato que a matriz recebida deve ser retornada
modificada e que você não pode usar uma matriz ou vetor auxiliar para fazer a troca dos
elementos.
Exemplo para matriz 3 x 3:

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

11. A matriz abaixo representa o triângulo de pascal de ordem 6:


1
1 1
1 2 1
1 3 31
1 4 641
1 5 10 10 5 1
Os elementos extremos de cada linha são iguais a 1. Os outros elementos são obtidos
somando-se os dois elementos que aparecem imediatamente acima e à esquerda na linha
anterior. Assim 10 = 4 + 6. Escreva um programa que, dado n, gere e exiba na tela o
triângulo de Pascal de ordem n. A impressão não deve conter zeros acima da diagonal
principal.

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

6.7 Trabalhos Sugeridos


1. Caça-Palavras
Considere o jogo caça-palavras que consiste em procurar uma palavra dada em uma matriz
de letras. Considera-se que a palavra foi encontrada se ela estiver inteiramente disposta
em uma linha, em uma coluna ou em uma diagonal da matriz. Define-se uma palavra como
uma sequência de letras maiúsculas (para facilitar não considere acentuação). As palavras
estão escritas na matriz sempre de cima para baixo, da esquerda para a direita ou nas
diagonais paralelas à diagonal principal ou na própria diagonal. Faça um programa que
tenha como entrada um conjunto de n palavras e a matriz de caracteres, ambas fornecidas
pelo teclado. O programa deve ser capaz de:
1. Verificar quais palavras foram encontradas e computar o número de ocorrências de cada
uma delas na matriz.
2. Escrever em um arquivo texto, a lista das palavras encontradas e a listas das palavras
não encontradas. As duas listas devem estar em ordem alfabética.
3. Escrever em seguida, no mesmo arquivo texto, a lista das palavras encontradas na ma-
triz, ordenadas pelo seu número de ocorrências (considerar ordem decrescente).
Informar também nestes casos, a posição na matriz do caractere inicial de cada ocorrência
da palavra, além da direção que ela se encontra na matriz. As direções são definidas como:
diagonal, vertical, horizontal.
4. Informe a direção que mais palavras foram encontradas: diagonal, vertical ou horizontal.
Informe também a quantidade de palavras encontradas nesta direção.
O arquivo texto de saı́da deve seguir o formato apresentado no exemplo a seguir:
Exemplo:
Conjunto de palavras: CAPELA, LAPIS, FLORESTA, AULA, ALEGRIA, MANGA
Matriz:

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

6.8 Tópicos Avançados


Definição Dinâmica de uma matriz
Além da definição estática abordada na seção 6.2.1, uma matriz pode ser definida dinamica-
mente. Nesse caso o tamanho de suas dimensões só será estabelecido durante a execução do
programa, conforme é visto no exemplo 6.21:

1 # include < stdio .h >


2
3 main () {
4
5 int n , m ;
6
7 printf ( ‘ ‘ Forneça as dimens~ o es da matriz : ’ ’) ;
8 scanf ( ‘ ‘% d % d ’ ’ ,&n ,& m ) ;
9 int matriz [ n ][ m ];
10 .
11 }

Exemplo 6.21: Definição dinâmica

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:

Igor Magri Vale


Douglas Almonfrey
Flávio Varejão

Objetivos:

• Definir o que são apontadores;

• Apresentar a sintaxe de apontadores;

• Mostrar a importância dos apontadores na passagem de parâmetros sem cópia, no retorno


de múltiplos valores nas funções e na aloção dinâmica de memória;

• Apresentar os principais problemas no uso de apontadores;

• Definir o TAD Lista Encadeada e suas principais operações;

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.

7.1 Variáveis Apontadores


Em um programa, cada variável possui seu endereço e um conteúdo. A Figura 7.1 exibe cinco
endereços de memória e seus respectivos conteúdos.
Os apontadores são um tipo especial de variável, cujo conteúdo não é um simples valor, e sim
um endereço de memória. Na Figura 7.2, pode ser observado que o conteúdo de uma variável
apontador corresponde a um endereço de uma variável não-apontador.
Neste capı́tulo, serão estudados os apontadores, também chamados de ponteiros. Como os
apontadores são variáveis que guardam endereços, trata-se de um conceito de baixo nı́vel, muito
ligado à arquitetura de computadores. Apontadores são ferramentas poderosas na linguagem de

187
188 CAPÍTULO 7. APONTADORES

Figura 7.1: Formato Abstrato da Memória

Figura 7.2: Apontadores e Não-Apontadores

programação C. Dentre diversas funções, apontadores permitem passagem de parâmetros sem


cópia de muitos dados, retorno de mltiplos valores em funções, alocação dinâmica de memória e
construção de listas encadeadas.
No decorrer do capı́tulo, serão utlizados modelos de figuras ilustrativas para auxiliar na
explicação. Na Figura 7.3, tem-se o modelo para representar uma variável e seu endereço.
Como pode ser observado na Figura 7.3, um quadrado será usado como formato abstrato
de variável de memória. Dentro deste quadrado estará representado o conteúdo da variável.
Além disso, um cı́rculo negro no seu vértice superior esquerdo representará abstratamente seu
endereço.
Já para representar a ligação entre um apontador e a variável que está sendo apontada por
ele, será utilizado o modelo da Figura 7.4. Tal ligação é ilustrada por meio de uma seta, que sai
da variável apontador, e aponta o cı́culo negro (endereço) da variável não-apontador.

7.2 A Sintaxe dos Apontadores


Na linguagem C, o operador asterisco * é que especifica se uma variável guarda um endereço,
ou seja, se é um apontador. A declaração de um apontador segue o seguinte formato:
7.2. A SINTAXE DOS APONTADORES 189

Figura 7.3: Formato Abstrado de uma Variável

Figura 7.4: Abstração de uma Variável Apontador

<tipo do apontador> * <nome da variável>

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

Exemplo 7.1: Declaração de 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.

7.2.1 Operador Endereço de Memória


Em C, o operador unário &, quando aplicado a uma variável, retorna o endereço dela. Portanto,
quando um apontador é declarado, pode-se inicializá-lo usando o operador &, que faz o apontador
receber um endereço de uma variável já existente.
O Exemplo 7.2 mostra o uso do operador (&) para inicializar uma variável apontador. A
variável p recebe o endereço da variável d. Note que ambas variáveis são do mesmo tipo, pois
é fundamental que o apontador aponte para uma variável de tipo compatı́vel com a declaração
dele.

1 int * p ;
2 int d = 10;
3 p = &d;
4 printf ( " % p \ n % p \ n " , &d , p ) ;

Exemplo 7.2: Inicialização de um apontador usando o operador endereço de memória

No Exemplo 7.2, serão impressos o endereço de d, e logo após, o valor do apontador p,


que é o endereço de d. Portanto, dois valores iguais serão impressos na tela. O %p, utilizado
no Exemplo 7.2, é usado pela função printf para identificar o tipo de valor que será impresso,
neste caso %p informa que o valor de um apontador será impresso, ou seja, o endereço de uma
variável.

7.2.2 O Operador Seta


Outro operador associado a apontadores é a seta (− >). Quando um apontador indica uma
variável do tipo estrutura, para que se acesse os atributos desta variável, ao invés do ponto (.),
utiliza-se (− >). Observe o Exemplo 7.3.

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.

7.3 Acesso à Variável por Meio de Apontadores


O operador unário * tem uma outra função além de especificar uma multiplicação ou se uma
variável é apontador. Este operador também pode retornar o valor armazenado na variável
apontada pelo apontador. Por isso, apontadores podem ser usados para atribuir valores a outras
variáveis. Observe o Exemplo 7.4.

1 int *p;
2 int c = 10;
3 int d;
4 p = &c;
5 d = *p;

Exemplo 7.4: Acesso ao conteúdo de variáveis por meio de apontadores

No Exemplo 7.4, p recebe o endereço de c. Através de p portanto, pode-se agora acessar o


valor de c. Com esse recurso, na quinta linha do Exemplo 7.4, d recebe o valor de c por meio
de p.
Uma variável pode ser atribuı́da através do uso de apontadores. No Exemplo 7.5, a variável
c tem seu valor alterado através de uma atribuição feita a p.

1 float * p ;
2 float c = 15;
3 p = &c;
4 * p = * p + 1;
5 printf ( " % f " , c ) ;

Exemplo 7.5: Alterando valor de variáveis por meio de apontadores


192 CAPÍTULO 7. APONTADORES

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;

Exemplo 7.6: Alterando valor de variáveis através de apontadores

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.4 Uso de Apontadores nas Passagens de Parâmetros


O conceito de função em C, conforme já discutido em capı́tulos precedentes, é fundamental.
Uma das principais vantagens de apontadores, no que diz respeito à função, é a passagem de
valores sem cópia.
Na passagem por cópia, os variáveis que são passadas como argumentos da função tem seus
valores copiados. São essas cópias que a função utilizará no decorrer da sua execução. Assim,
qualquer alteração dos valores das cópias não implicará em mudança nas variáveis originais.
Por meio de apontadores, é possı́vel alterar os valores das variáveis passadas como argumentos
para uma função. Ao declarar que um parâmetro de uma função é apontador, deve-se passar
um endereço de uma variável como argumento correspondente. Nesse tipo de passagem, não é
copiado o valor da variável argumento, mas com o endereço dela, pode-se acessar o conteúdo e
modificá-lo no decorrer da execução da função. O Exemplo 7.7 mostra a passagem por cópia e
por apontadores.

1 void adicionaX1 ( int x , int b ) {


2 b = b + x;
3 }
4
5 void adicionaX2 ( int x , int * b ) {
6 *b = *b + x;
7.4. USO DE APONTADORES NAS PASSAGENS DE PARÂMETROS 193

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 }

Exemplo 7.7: Passagem de valor por cópia e através de apontadores

Em adicionaX1 , no Exemplo 7.7, uma cópia do valor de a é passada com argumento da


função, ou seja, b recebe uma cópia do valor 0 de a. Dentro da função, o valor de b muda para
10, mas o de a continua o mesmo. Assim, o primeiro valor impresso é 0. Já em adicionaX2 , o
endereço de a é passado para o ponteiro b e, na execução da função, é adicionado x ao conteúdo
da variável apontada por b, o que faz a ter seu valor modificado para 10. Portanto, o que
impresso no segundo printf é a = 10. A Figura 7.5 ilustra as passagens de parâmetro de a para
b, em adicionaX1 e adicionaX2 .

Figura 7.5: Passagem de Valor por Cópia e por Apontadores

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.

1 void troca ( int *x , int * y ) {


2 int aux ;
194 CAPÍTULO 7. APONTADORES

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 }

Exemplo 7.8: Retorno de múltiplos valores por uma função

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.

7.5 Alocação Dinâmica de Memória


Apontadores também são fundamentais no que diz respeito à alocação dinâmica, economia de
memória e tempo de execução.
Apontadores permitem alocação de memória em tempo de execução (alocação dinâmica).
Para tanto, uma variável apontador aponta para uma área dememória livre definida durante a
execução do programa. As Figuras 7.6 e 7.7 ilustram o processo de alocação dinâmica.

Figura 7.6: Áreas de Memória Livres

A Figura 7.6 mostra as áreas livres da memória. Em um determinado momento, um endereço,


7.5. ALOCAÇÃO DINÂMICA DE MEMÓRIA 195

que era livre, passa a ser apontado por uma variável apontador, podendo assim ser utilizado, o
que é ilustrado na Figura 7.7.

Figura 7.7: Área de Memória Alocada Dinamicamente

Em C, a função malloc (abreviatura de memory allocation), da biblioteca padrão stdlib.h,


aloca um bloco de bytes consecutivos na memória do computador e devolve o endereço desse
bloco. O Exemplo 7.9 mostra seu uso.

1 int * p
2 p = ( int *) malloc (4*( sizeof ( int ) ) ) ;

Exemplo 7.9: Alocação dinâmica de quatro inteiros

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

Exemplo 7.10: Utilização da função free para liberar memória

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

Figura 7.8: Vetor de Inteiros Alocados Dinamicamente

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.

Figura 7.9: Desalocação Dinâmica de Memória

Na Figura 7.9, pode-se observar a que no decorrer da execução do programa, a variável


apontador deixa de apontar para área de memória, que volta para a grupo de endereços livres.
O processo de alocação dinâmica pode ser usado para ler linhas de um documento de texto.
Usando-se alocação dinâmica, define-se o tamanho n x m fixo da matriz limitando o tamanho do
texto e da linha que podem ser lidos. Para se conseguir ler todas as linhas, define-se o tamanho
m da matriz como o tamanho da maior linha que será lida e n um número alto suficiente para
armazenar todas as linhas. Entretanto, esse procedimento acarreta perda de memória com linhas
pequenas, que não utilizam todo o espaço alocado para elas, e tratando-se de textos com grandes
quantidades de linhas essa perda torna-se considerável.
Considere a Figura 7.10, que ilustra como seria o processo de alocação dinâmica de uma
matriz para ler um pequeno texto de caracteres. Percebe-se facilmente para a quarta linha da
matriz, que 9 espaços seriam perdidos.
7.5. ALOCAÇÃO DINÂMICA DE MEMÓRIA 197

Figura 7.10: Matriz que Armazena um Texto

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.

Figura 7.11: Matriz de Strings

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 }

Exemplo 7.11: Definição de uma matriz de caracteres dinâmica

7.6 Problemas Gerados por Apontadores


Apesar de serem muito importantes, quando usados inapropiadamente, apontadores são fontes de
erros difı́ceis de serem encontrados. Apontadores não inicializados, objetos pendentes, referência
pendente, e programação macarrônica são alguns desses erros.

7.6.1 Apontadores Não Inicializados


Quando se trata de apontadores, um erro muito comum é utilizá-lo antes de fazê-lo apontar
para algum endereço válido, ou seja, sem inicializá-lo. O Exemplo 7.12 mostra como isso pode
ocorrer. O valor da variável apontada por p recebe o valor de h, mas p não foi inicializado ainda,
ou seja, não apontava para nenhum espaço de memória válido.

1 float *p , h = 15.0;
2 *p = h;

Exemplo 7.12: Apontador não inicializado

As conseqüências da utilização de um apontador não inicializado são imprevisı́veis, podendo


provocar uma paralisação do sistema.

7.6.2 Objetos Pendentes


Objetos pendentes ocorrem quando uma área de memória alocada fica inacessı́vel. Veja o Exem-
plo 7.13. A área de memória apontada por p fica inacessı́vel, pois p passa a apontar para o mesmo
endereço de g. A Figura 7.12 ilustra como ocorre objetos pendentes.

Figura 7.12: Objetos pendentes


7.6. PROBLEMAS GERADOS POR APONTADORES 199

1 int * p = ( int *) malloc ( sizeof ( int ) ) ;


2 int * q = ( int *) malloc ( sizeof ( int ) ) ;
3 p = q;

Exemplo 7.13: Objetos pendentes

7.6.3 Referência Pendente


Quando é liberado um endereço de memória, que é apontado por mais de uma variável apontador,
ocorre a referência pendente. Assim, algumas variáveis ficam referenciando um endereço que
foi desalocado. Veja o Exemplo 7.14 que mostra um caso de referência pendente. O endereço
da variável h é liberado, portanto p aponta para um espaço de memória liberado. A Figura
7.13 ilustra o problema de referência pendente. Um espaço de memória livre pode ser alocado
a qualquer momento por outras variáveis do sistema. Ao se utilizar variáveis que tiveram seu
espaço de memória liberado, pode-se estar acessando áreas importantes do sistema, que alocacou
o espaço de memória que acabara de ser liberado, e isto pode provocar uma paralisação do
sistema.

Figura 7.13: Referência pendente

1 int * p ;
2 int * h ;
3 h = ( int *) malloc ( sizeof ( int ) ) ;
200 CAPÍTULO 7. APONTADORES

4 p = h;
5 free ( h ) ;

Exemplo 7.14: Referência pendente

7.6.4 Programação Macarrônica


Um mesmo endereço de memória pode ser apontado por vários apontadores. Isso pode tornar
confuso o entendimento do programa e a identificação de erros. O Exemplo 7.15 mostra um
trecho de código difı́cil de entender devido ao uso indiscriminado de apontadores.

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 }

Exemplo 7.15: Programação macarrônica causada pelo uso indiscriminado de apontadores.

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.

7.7 TAD Implementacional Lista Encadeada tLista


Um exemplo de implementação utilizando os apontadores é o TAD Lista Encadeada.
Uma lista encadeada é uma estrutura cujos elementos são acessados seqüencialmente por
meio de apontadores. Nessa estrutura estão armazenados os apontadores para o começo e
o fim de uma seqüência de blocos, conhecidos como nós, além de uma variável inteira que
7.7. TAD IMPLEMENTACIONAL LISTA ENCADEADA TLISTA 201

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.

Figura 7.14: Esquema de Uma Lista Simplesmente Encadeada

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

7.7.1 Definição do Tipo tNo


De acordo com o que foi dito até agora, cada nó deve ser responsável por armazenar um dado,
além de conter um apontador para o próximo nó, no caso de uma lista simplesmente encadeada.
Assim, segue abaixo um exemplo de como pode ser a estrutura tNo:

1 typedef int tInfo ;


2
3 typedef struct no {
4 tInfo info ;
5 struct no * proximo ;
6 } tNo ;

Exemplo 7.16: Estrutura tNo

Há necessidade de se incluir o termo no após o primeiro struct porque, ao se incluir a


estrutura proximo, ocorre uma definição recursiva do tipo no, ou seja, a estrutura no possui um
campo do mesmo tipo que ela. Assim, caso fosse retirado o primeiro termo no, não se saberia
qual o tipo de proximo. Também é importante dizer que a variável inteira info poderia ser
substituı́da por qualquer outro tipo, tais como char, float e, até mesmo, estruturas compostas
por uma variedade de tipos. Bastaria alterar o termo int na definição do tipo tInfo.
A partir de agora, devido ao uso do typedef, sempre que for utilizado o termo tNo antes de
um identificador, significa que está sendo declarada uma estrutura como a do Exemplo 7.16.
Como freqüentemente será necessário incluir novos nós, torna-se importante criar uma função
que aloque dinamicamente um espaço de memória para um nó. O Exemplo 7.17 mostra uma
forma de implementar tal função.

1 tNo * criaNo ( tInfo elem ) {


2 tNo * no = ( tNo *) malloc ( sizeof ( tNo ) ) ;
3 no - > proximo = NULL ;
4 no - > info = elem ;
5 return no ;
6 }

Exemplo 7.17: Implementação da operação criaNo

Após a alocação do espaço de memória, o apontador proximo é inicializado como NULL. Em


seguida, o campo info do nó criado é associado ao valor do elemento passado como argumento.
Por fim, é retornado um apontador para a área de memória inicializada na função.

7.7.2 Atributos de tLista


Será utilizada uma estrutura para tLista com apontadores para o primeiro e último nó. Essa
estrututa é chamada cabeçalho da lista e cabe ressalvar que existem diferentes implementações.
A adotada aqui (ver Exemplo 7.18) é apenas uma delas.
7.7. TAD IMPLEMENTACIONAL LISTA ENCADEADA TLISTA 203

1 typedef struct {
2 tNo * primeiro ;
3 tNo * marcador ;
4 tNo * ultimo ;
5 int tam ;
6 } tLista ;

Exemplo 7.18: Estrutura 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.

7.7.3 Operações de tLista


Como já se têm definidas as abstrações de dados, tNo e tLista, necessita-se criar as operações
que serão responsáveis por trabalhar com estes conjuntos de valores, concluindo assim, a imple-
mentação do TAD lista encadeada.

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)

O Exemplo 7.19 mostra a função de inicialização de uma lista.

1 void iniciaLista ( tLista * lista ) {


2 lista - > primeiro = NULL ;
3 lista - > marcador = NULL ;
4 lista - > ultimo = NULL ;
5 lista - > tam = 0;
6 }

Exemplo 7.19: Inicialização de uma lista

Na função iniciaLista os apontadores primeiro, marcador e ultimo são inicializados com


NULL, pois não há nós na lista e, garante-se assim, que estes apontem para uma área de memória
204 CAPÍTULO 7. APONTADORES

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

O Exemplo 7.20 mostra a implementação da função analisadora de fim da lista.

1 int finalLista ( tLista * lista ) {


2 return ( lista - > marcador == NULL ) ;
3 }

Exemplo 7.20: Verificação de fim da lista

Essa função simplesmente retorna o resultado da comparação do marcador da lista com


NULL. Isso porque o último nó da lista aponta tem como próximo este valor, bem como a
lista vazia, que tem seus apontadores iniciais todos apontando para NULL. Na Figura 7.15, o
marcador não está apontando para NULL, assim não é o fim da lista e a função final retorna 0.

Figura 7.15: Marcador Apontando para uma Posição da Lista

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

O Exemplo 7.21 mostra a implementação da função analisadora de lista vazia.


7.7. TAD IMPLEMENTACIONAL LISTA ENCADEADA TLISTA 205

Figura 7.16: Marcador Apontando para o Fim da Lista

1 int vaziaLista ( tLista * lista ) {


2 return ( lista - > tam == 0) ;
3 }

Exemplo 7.21: Verificação se a lista está 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

1 void incluirFimLista ( tLista * lista , tInfo elem ) {


2 tNo * no ;
3
4 no = criaNo ( elem ) ;
5 if ( vaziaLista ( lista ) ) {
6 lista - > primeiro = no ;
7 } else {
8 lista - > ultimo - > proximo = no ;
9 }
10 lista - > ultimo = lista - > marcador = no ;
11 lista - > tam ++;
12 }

Exemplo 7.22: Função de inclusão de um novo nó na lista

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.

Figura 7.17: Lista Antes da Inclusão

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

Figura 7.18: Lista Após a Inclusão

O Exemplo 7.23 mostra a implementação de uma operação de exclusão.

1 void excluirNoLista ( tLista * lista , int pos ) {


2 tNo * auxA , * auxB ;
3 int i ;
4
5 if ( pos < 0 || pos >= lista - > tam )
6 return ;
7
8 auxA = lista - > primeiro ;
9 for ( i =0; i < pos ; i ++) {
10 auxB = auxA ;
11 auxA = auxA - > proximo ;
12 }
13 if ( auxA != lista - > primeiro ) {
14 auxB - > proximo = auxA - > proximo ;
15 if ( auxA == lista - > ultimo ) {
16 lista - > ultimo = auxB ;
17 }
18 } else {
19 lista - > primeiro = auxA - > proximo ;
20 if ( lista - > ultimo == auxA ) {
21 lista - > ultimo = auxA - > proximo ;
22 }
23 }
24 lista - > marcador = NULL ;
25 lista - > tam - -;
26 free ( auxA ) ;
27 }

Exemplo 7.23: Função de exclusão de um nó da lista


208 CAPÍTULO 7. APONTADORES

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.

Figura 7.19: Lista 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.

1 void primeiroLista ( tLista * lista ) {


2 lista - > marcador = lista - > primeiro ;
3 }

Exemplo 7.24: Posicionado o marcador no começo da 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.

1 void proximoLista ( tLista * lista ) {


2 if ( lista - > marcador != NULL ) {
3 lista - > marcador = lista - > marcador - > proximo ;
4 }
5 }

Exemplo 7.25: Posicionado o marcador no proximo nó da lista

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.

1 tInfo obterInfo ( tLista * lista , int * x ) {


2 if ( lista - > marcador == NULL ) {
3 * x = 0;
4 }
5 * x = 1;
6 return lista - > marcador - > info ;
7 }

Exemplo 7.26: Função para recuperar a informação de um nó

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

1 void imprimirLista ( tLista lista ) {


2 tInfo x ;
3 int erro = 0;
4
5 primeiroLista (& lista ) ;
6 while (! finalLista (& lista ) ) {
7 x = obterInfo (& lista , & erro ) ;
8 if ( erro ) {
9 printf ( " Elemento : % d \ n " , x ) ;
10 }
11 proximoLista (& lista ) ;
12 }
13 }

Exemplo 7.27: Impressão de todos elementos de uma lista

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

A seguinte função para a finalização da lista é um exemplo de operação destrutora. Repare


que a desalocação de memória é feita nó por nó. Note que ao final da operação, os valores dos
atributos de lista são modificados para apontarem para NULL para evitar que fiquem referenci-
ando áreas já desalocadas.

1 void destroiLista ( tLista * lista ) {


2 tNo * aux ;
3
4 primeiroLista ( lista ) ;
5 while (! finalLista ( lista ) ) {
6 aux = lista - > marcador ;
7 proximoLista ( lista ) ;
8 free ( aux ) ;
9 }
10 lista - > primeiro = NULL ;
11 lista - > marcador = NULL ;
7.7. TAD IMPLEMENTACIONAL LISTA ENCADEADA TLISTA 211

12 lista - > ultimo = NULL ;


13 lista - > tam = 0;
14 }

Exemplo 7.28: Desalocação das áreas de memória ocupados pelos nós da lista

Semelhante a função de imprimir, primeiroLista é utilizada para posicionar o marcador


no inicio da lista e, em seguida, inicia-se um laço enquanto o final dela não é alcançado.
A cada iteração, um nó auxiliar aux guarda o endereço de memória apontado pelo marcador
da lista. Em seguida, posiciona-se o marcador no próximo nó da lista e a região apontada por
aux é liberada com o uso do free.
Quando se chega ao final da lista, o laço enquanto será encerrado e os atributos de listas são
incializados, assim, seus apontadores apontam para NULL e o tamanho é zero.

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.

Pseudocódigo 7.1 Uso de uma lista simplesmente encadeada.


Descrição: Programa de cadastro de alunos.
Dados de Entrada: nomes e as respectivas matriculas.
Saı́da do Programa: lista impressa.
Inicializaç~
ao da lista;
Leitura de um nome;
Leitura da matricula;
Validaç~
ao da matricula
Se a matricula for menor ou igual a zero:
Parar de ler informaç~
oes. Ir para "Leitura da posiç~
ao";
Se n~ao for menor que zero:
Continuar a ler informaç~
oes. Ir para "Leitura de um nome";
Leitura da posiç~
ao
Validaç~
ao da Posiç~
ao
Se a posiç~
ao for maior ou igual a zero:
Se a posiç~
ao contiver algum nó:
Excluir o nó, imprimir a lista e voltar para "Leitura da posiç~
ao";
Se for menor que zero:
Parar de ler informaç~
oes. Ir para "Destruiç~
ao da lista";
Destruiç~
ao da lista.

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

42 while ( pos >= 0) {


43 excluirNoLista (& lista , pos ) ;
44 imprimirLista ( lista ) ;
45 scanf ( " % d " , & pos ) ;
46 }
47
48 destroiLista (& lista ) ;
49 }

Exemplo 7.29: Cadastro de alunos na universidade

O programa implementado no Exemplo 7.29 apenas lê as informações do teclado enquanto


não se digita um valor inválido, no caso matricula menor ou igual a 0. Se for digitado tal valor
para matricula o comando break faz a execução do programa sair do laço infinito (a condição
é sempre 1). Cada nome e matricula são armazendos na lista e, após o término de leitura de
dados, eles são impressos na tela.
Em seguida, pode-se apagar as informações fornecendo a posição na lista do dado a ser
apagado. A cada elemento excluı́do, a lista é atualizada e impressa e, quando for passado
um valor de posição negativo, encerra-se o laço de exclusão. Por fim, antes de se encerrar o
programa, a lista é destruı́da.

7.8 Exercı́cios Resolvidos

Exercı́cio Resolvido 7.1 - Acesso ao Conteúdo de Variáveis por Apontadores

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;

Exemplo 7.30: Código para o Exercı́cio Resolvido 7.1


214 CAPÍTULO 7. APONTADORES

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.

Figura 7.20: Solução do Exercı́cio Resolvido 7.1

Por fim, p e q passam a apontar b e c, repectivamente. Assim, na última linha, o conteúdo


da variável apontada por p recebe o valor de c, pois este é a variável apontada por q, o que faz
b receber o valor 15. Vejas os três últimos passos da Figura 7.20.

Exercı́cio Resolvido 7.2 - Múltiplo Retorno em uma Função

A função calculaPot, do Exemplo 7.31, calcula a potência quadrada de um inteiro. Modi-


fique esta função para que ela passe a retornar também a potência cúbica deste número. Faça
as devidas alterações na main.

1 int calculaPot ( int valor ) {


2 int potQuadrada ;
3 potQuadrada = valor * valor ;
4 return potQuadrada ;
7.8. EXERCÍCIOS RESOLVIDOS 215

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 }

Exemplo 7.31: Função calculaPot

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.

Pseudocódigo 7.2 Cálculo das potências quadradas e cúbicas de um número inteiro.


Descrição: Programa que retorna as potências quadrada e cúbica de um número inteiro, calculadas por
uma função apenas.
Dados de Entrada: um número inteiro.
Saı́da do Programa: potências quadrada e cúbica do valor de entrada.
Leitura do número inteiro;
Passagem de par^ametros para a funç~
ao calculaPot;
Cálculo das pot^
encias pela funç~
ao calculaPot.

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.

1 int calculaPot ( int valor , int * potCubica ) {


2 int potQuadrada ;
3 potQuadrada = valor * valor ;
4 * potCubica = valor * valor * valor ;
5 return potQuadrada ;
6 }
7
8 main ( ){
9 int x;
10 int quadrado ;
11 int cubo ;
12
216 CAPÍTULO 7. APONTADORES

13 scanf ( " % d " , & x ) ;


14 quadrado = calculaPot (x , & cubo ) ;
15 printf ( " Quadrado de % d = % d \ nCubo de % d = % d \ n " , x , quadrado , x , cubo ) ;
16 }

Exemplo 7.32: Múltiplo retorno em calculaPot

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.

Exercı́cio Resolvido 7.3 - Inserção no Inı́cio de Lista Simplesmente Encadeada

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.

Pseudocódigo 7.3 Inserção no começo de uma lista simplesmente encadeada.


Descrição: Função que insere um nó no começo de uma lista simplesmente encadeada.
Dados de Entrada: a lista e o elemento a ser enserido.
Saı́da da Função: a lista após a inserção.
Criaç~
ao de um nó para armazenar o novo elemento;
Verificar se a lista está vazia
Se estiver vazia:
O novo nó também passa a ser o último da lista;
Se n~ao estiver vazia:
Faz o novo nó ter como seu próximo o antigo primeiro;
O primeiro da lista passa a ser o nó criado;
Posicionar o marcador no nó incluı́do;
Incrementar o tamanho da lista.

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.

1 void i nc luirInicioLista ( tLista * lista , tInfo elem ) {


7.8. EXERCÍCIOS RESOLVIDOS 217

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 }

Exemplo 7.33: Função de inclusão de um novo nó no começo da lista

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.

Figura 7.21: Lista da Figura 7.17, Após a Inclusão do Nó no Começo

Exercı́cio Resolvido 7.4 - Ordenação de uma Lista Simplesmente Encadeada

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

idade, após a leitura das informações.

Solução Possı́vel:
De forma geral, o programa deve seguir o Pseudocódigo 7.4.

Pseudocódigo 7.4 Ordenação de uma lista simplesmente encadeada.


Descrição: Programa que ordena um grupo de pessoas por idade.
Dados de Entrada: nomes e as respectivas idades.
Saı́da do Programa: lista de nomes ordenados crescentemente por idade.
Inicializaç~ao da lista;
Leitura de um nome;
Leitura da idade;
Validaç~
ao da idade
Se a idade for menor que zero:
Parar de ler informaç~
oes. Ir para "Ordenaç~
ao da Lista";
Se n~ao for menor que zero:
Continuar a ler informaç~
oes. Ir para "Leitura de um nome";
Ordenaç~
ao da lista;
Impress~ao da lista ordenada;
Destruiç~ao da lista.

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.

Pseudocódigo 7.5 Processo para a ordenação de uma lista simplesmente encadeada.


Processo componente "Ordenaç~ao da lista":
Posicionar o marcador no começo da lista;
Enquanto n~
ao chegar no fim da lista:
Buscar o nó que guarda uma idade menor que a do marcador;
Se existir um nó com menor idade:
Trocar as informaç~
oes entre o marcador e o nó encontrado;
Posicionar o marcador no próximo nó da seqü^
encia da lista;
FIM-Processo componente "Ordenaç~ao da lista".
7.8. EXERCÍCIOS RESOLVIDOS 219

No Exemplo 7.34, tem-se a trascrição dos Pseudocódigos 7.4 e 7.5 para C.

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 }

Exemplo 7.34: Cadastro de nomes e idades

Novamente, o programa implementado apenas lê as informações do teclado enquanto não se


digita uma idade inválida (nesse cado, menor que 0). Se for digitado tal valor para a idade, o
comando break faz a execução do programa sair do laço infinito.
Cada nome e idade são armazendos na lista e, após o término de leitura de dados, eles são
ordenados por ordem crescente de idade. Na função ordenaPorIdade, posiciona-se o marcador
no começo da lista e, enquanto o final desta não é atingido, busca-se o menor elemento à frente
do atual marcador. Caso seja encontrado algum, trocam-se as informações dos nós marcador e
aux, o qual guarda o nó com menor idade obtido da função menorIdade.
A função menorIdade procura e retorna um nó que guarda uma idade menor do que a que
está no marcador, e que se encontra à frente deste na lista. Assim, a variável menor guarda o nó
que contém a menor idade e é inicializado como sendo o atual marcador. A partir do próximo nó
após ao atual marcador, enquanto o final da lista não é atingido, busca-se um nó que contenha
uma idade menor do a que está armazenada em menor. Se for encontrada alguma, menor passa
a apontar para o nó que contém esta menor idade. Repare que é utilizada uma variável aux
para percorrer a lista, pois não se pretende alterar a posição do marcador, o qual é utilizado na
função ordenaPorIdade.
No fim da execução do programa, a lista ordena é impressa e, antes de se encerrar o programa,
ela é destruı́da.

Exercı́cio Resolvido 7.5 - Media de valores de uma Lista Simplesmente Encadeada

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.

Pseudocódigo 7.6 Média dos valores de uma lista simplesmente encadeada.


Descrição: Programa que imprime os funcionários cujos salários estão acima da média.
Dados de Entrada: nomes e os respectivos salários.
Saı́da do Programa: lista de nomes que tem o salário acima da média.
Inicializaç~ao da lista;
Leitura de um nome;
Leitura da idade;
Validaç~
ao da idade
Se a idade for menor que zero:
Parar de ler informaç~
oes. Ir para "Verificar quem está acima da média";
Se n~ao for menor que zero:
Continuar a ler informaç~
oes. Ir para "Leitura de um nome";
Verificar quem está acima da média;
Destruiç~ao da lista.

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

No Exemplo 7.35, o programa implementado contém uma main semelhante à do Exemplo


7.34. A diferença é que agora é fornecido o salário em vez da idade. Além disso, no lugar da
ordena, é chamada a função que procura e imprime os funcionários com salário acima da média.

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 }

Exemplo 7.35: Cadastro de nomes e salários

Na função acimaMedia, total é inicializado como 0 e, a partir do primeiro elemento da


lista, esse total é incrementado com o salário de cada nó. Quando o final da lista é alcançado,
a media de salários é computada como sendo o total obtido dividido pelo tamanho da lista, ou
seja, o número de funcionários.
Com a média calculada, inicia-se um nova busca na lista, desde o primeiro elemento até o
final da lista, a fim de que se encontre os salários acima da média. Caso algum nó contenha um
salário maior que a média, os dados do funcionário são impressos na tela.

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.

• Em C, a declaração de uma variável apontador segue o formato:

<tipo do apontador> * <nome da variável>

No lugar de tipo do apontador, podem ser utilizados os tipos padrõess da linguagem C,


assim como os tipos definidos pelo programador.

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

• A alocação dinâmica é uma forma de reservar espaços de memória no decorrer da execução


do programa, o que evita o despercı́cio de recursos da máquina. Em C, as funções malloc
e free são responsáveis por alocar e desalocar áreas de memória, respectivamente.

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

7.10 Lista de Exercı́cios


1. Cite três vantagens e três desvantagens de se utilizar apontadores.

2. Explique quais as funcionalidades dos operadores * e &.

3. Liste as diferenças entre variáveis apontadores e não apontadores. Faça um código em


C, que realize a soma de duas variáveis float a e b, usando dois apontadores g e h, que
apontem para a e b respectivamente. Quais são os tipos dos apontadores g e h?

4. No Exemplo 7.36, escrito na linguagem C, encontre os erros de sintaxe na utilização de


apontadores. Justifique cada erro encontrado.

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 }

Exemplo 7.36: Exercı́cio Proposto 4


7.10. LISTA DE EXERCÍCIOS 225

5. No Exemplo 7.37, escrito na linguagem C, encontre os problemas causados pela utilização


indevida de apontadores. Justifique cada problema encontrado.

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 }

Exemplo 7.37: Exercı́cio Proposto 5

6. Ilustre as atribuições e referenciamentos feitos no trecho de código do Exemplo 7.6. 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.

7. Implemente na linguagem C, uma matriz de strings alocada estáticamente e uma alocada


dinamicamente. Explique um caso em que a alocação dinâmica seja importante para
economia de memória.

8. Explique o que é alocação e desalocação de memória em tempo de execução e quais funções


em C executam essas funcionalidades. Cite uma vantagem de se desalocar memória não
mais utilizada.

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:

- criar (P) - criar uma pilha P vazia.


- push (P, x) - inserir o elemento x no topo de P (empilha).
- vazia (P) - verifica se P está vazia.
- topo (P) - acessa o elemento do topo da pilha, sem desempilhar.
- pop (P) - elimina o elemento do topo de P (desempilha).

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:

- criar (F) - criar uma fila F vazia.


- insere (F, x) - inserir o elemento x no final de F.
- vazia (F) - verifica se F está vazia.
- inicio (F) - acessa o elemento do inı́cio da fila, sem retirá-lo.
- retira (F) - elimina o elemento do final de F.

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 tı́tulo de filme, verificar se locadora possui em seu acervo.


- Dado um tı́tulo de filme, verificar se há alguma cópia disponı́vel.
- Dado um diretor, imprimir as informações sobre os filmes dirigido por ele.
228 CAPÍTULO 7. APONTADORES

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

7.11 Trabalhos Sugeridos


1. Notas de Alunos
O Colegiado de Engenharia de Computação mantém uma listagem com informações sobre
o desempenho de cada aluno do curso. Esta listagem contém as notas obtidas pelo aluno
em cada uma das disciplinas que ele cursou e é organizada da seguinte maneira:

Matrı́cula do Aluno Código da Disciplina Carga Horária Nota


00018 0001 60 4.5
11111 3232 30 8.7
00234 0500 75 9.0
00018 0001 60 7.0
... ... ... ...
09999 0786 60 7.5

Para facilitar a consulta a esta listagem, o coordenador do colegiado precisa de um pro-


grama em C que realize algumas operações sobre os dados da lista. Ele te pediu para que
você elabore um programa interativo que permita ao usuário:

(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

C.R. = (Σ (Carga Horária x Nota)) / (Σ (Carga Horária))

(h) Identificar o aluno de melhor CR.


(i) Identificar os alunos com CR abaixo de 5.0.
(j) Identificar se um determinado aluno já cumpriu a carga horária mı́nima do curso
(defina um valor).

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!

2. Controlador de Tráfego Aéreo

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

ordenado. Os controladores de tráfego áereo forencem indicações e autorizações de vôo,


de acordo com as caracterı́sticas operacionais das aeronaves e as condições de tráfego em
determinado momento. Estas autrorizações podem incidir sobre a rota, altitude e/ou ve-
locidade propostas pelo operador da aeronave para determinado vôo, devendo os pilotos
cumprirem as instruções recebidas.

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

O trabalho consiste em implementar um programa que faça o controle dos pousos em um


aeroporto que apresente três portões de embarque e desembarque (A,B,C). O trabalho deve
ser feito em dois módulos: controle de espaços aéreos e controle de pista, e a estrutura
para armazenamento dos dados deve ser uma lista encadeada dinâmica.

• Controle de espaço aéreo


Como o controle da torre consiste em controlar as aeronaves enquanto elas estiverem
no espaço aéreo do aeroporto, pode-se dividir essa tarefa em duas subtarefas:

Controle de rotas

A partir do momento em que as aeronaves entram no espaço aéreo do aeroporto, o


controle das suas rotas deverá ser realizado pelo seu programa. Para isso considere
que a função básica do seu programa é impedir que ocorram colisões, e para isso uma
das aeronaves em risco deverá ser redirecionada para a rota mais próxima a uma das
232 CAPÍTULO 7. APONTADORES

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

A segunda parte consiste em determinar a que horas a operação de desembarque de cada


aeronave foi realizada.

Entrada

As informações referente às aeronaves serão fornecidas através do teclado. As informações


serão as seguites: identificação da aeronave (0103, 0407, 1114,...); número inteiro que ser-
virá para indentificar qual rota que a aeronave está percorrendo (1, 2, 4, 6,...); classe da
aeronave (”comercial”ou ”militar”); altitude na qual a aeronave se encontra no momento
em que entra no espaço aéreo do aeroporto (7700, 9060, 1120, ...); velocidade com qual
a aeronave está (650, 680, 810, ...) e a hora de entrada da aeronave no espaço áereo do
aeroporto (14:30, 14:29, 14:31, ...). Considere que a hora apresentará o caracter ”:”para
separar hora de minuto.

Obs.: As horas de entrada no espaço aéreo do aeroporto são muito próximas,


com diferença entre o menor e o maior horário de no máximo 5 minutos. Cada
nova aeronave deve ser adicionada na lista já na ordem de prioridade de pouso,
do mais para o menos prioritário. Assim, após todas as inserções, as colisões
deve ser verificadas, aeronave por aeronave, na seqüência de pouso obtida.

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:

Estefhan Dazzi Wandekokem


Flávio Varejão

Objetivos:
• Definir e apresentar arquivos;

• Definir e diferenciar variáveis transientes e variáveis persistentes;

• Definir e diferenciar arquivos de texto e arquivos binários;

• Mostrar algumas operações que podem ser feitas com arquivos;

• Mostrar outras funções e exemplos de programas que usam arquivos;

8.1 Variáveis Transientes X Variáveis Persistentes


Quando os programas são executados, eles lidam com variáveis. Elas são a forma abstrata
pela qual os programadores enxergam os dados que seus programas manipulam. Internamente,
variáveis indicam células de memória. Por exemplo, numa arquitetura de 32 bits (que pode ser
um Pentium da Intel), normalmente o tipo inteiro de C corresponderá a 32 bits, o que pode
significar que uma variável int dessa linguagem ocupará 4 bytes (4 células de memória).
A memória principal de um computador (muitas vezes chamada de memória RAM) é o local
onde os programas em execução, e também as variáveis que eles utilizam, são armazenados.
Chamam-se essas variáveis armazenadas na memória principal de variáveis transientes porque
seu tempo de existência é limitado pelo tempo que o programa se encontra em memória. Assim
que o usuário ou o Sistema Operacional decidir que o programa deve ser finalizado, todas essas
variáveis deixarão de existir.
Os computadores teriam sua utilidade muito reduzida se a informação só pudesse ser arma-
zenada enquanto os programas estivessem na memória principal (e a máquina, ligada). Para

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.

8.2 Tipos de Arquivos


Existem dois tipos de arquivos: arquivos texto e arquivos binários. Ambos têm a mesma fi-
nalidade: armazenar variáveis persistentes. Quando o programador cria um arquivo em seu
programa, deve indicar a qual tipo ele pertencerá. Isso determina o formato das variáveis per-
sistentes que ele gravará e acessará do arquivo.

8.2.1 Tipos de Arquivos - Arquivos Texto


Um arquivo texto é uma seqüência de caracteres. Se um arquivo texto for aberto num editor
de texto convencional, o usuário poderá ler seu conteúdo (ou ao menos ver os caracteres lá
contidos), e poderá até mesmo modificá-lo de uma forma que faça sentido.
Por exemplo, na linguagem C, tome uma variável int que guarde o numero 43. Uma forma
de escrever essa variável num arquivo texto seria gravar ”43”, mas note que, com isso, dois
caracteres foram gravados: o ’4’ seguido do ’3’. Isso poderia significar, por exemplo, para
um arquivo numa arquitetura que salve caracteres como variáveis de 1 byte, que dois bytes
foram gravados no arquivo, o primeiro armazenando o caractere ’4’ e o segundo armazenando o
caractere ’3’.

Figura 8.1: Exemplo de um arquivo texto.


8.2. TIPOS DE ARQUIVOS 237

8.2.2 Tipos de Arquivos - Arquivos Binários

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.

Figura 8.2: Exemplo de um arquivo binário.


238 CAPÍTULO 8. ARQUIVOS

8.3 Definição de arquivos


Para serem utilizados nos programas, os arquivos são tratados como variáveis. Então, como
todas as outras variáveis, eles devem ser declarados. Dessa forma, também podem ser usados
como argumentos para funções.
Para lidar com arquivos, existem funções responsáveis por realizar todas as operações ne-
cessárias. Por exemplo, criar um novo arquivo em branco, excluir um já existente, gravar
informação ou ler as já contidas num arquivo, dentre outras.
Na linguagem C, o tipo FILE, definido na biblioteca stdio.h, é usado para se tratar arquivos.
Um ponteiro para FILE deve ser declarado sempre que uma variável arquivo for utilizada no
programa. Por exemplo, o seguinte comando declara uma variável do tipo ponteiro para arquivo
denominada arq:

FILE *arq;

Os ponteiros para FILE tratam arquivos referenciando-os a áreas de memória chamadas


buffers, as quais armazenam, na memória principal, informações que foram lidas de um arquivo
fı́sico ou que serão escritas em um, mas que ainda não foram descarregadas (gravadas no disco
rı́gido, por exemplo). De qualquer forma, o programador pode abstrair o conceito de buffer;
ele usará a variável FILE* declarada, associará um arquivo fı́sico a ela, e então a usará como
argumento para as funções de arquivo.

8.4 Operação sobre arquivos


Arquivos demandam algumas operações, como abertura e fechamento, para seu funcionamento.
Os valores já armazenados nos arquivos devem poder ser lidos, e novos valores devem poder ser
escritos, para que tenham utilidade. As operações que realizam essas tarefas são fornecidas pela
linguagem de programação, em vários formatos.

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

FILE *fopen (const char *nomearq, const char *modo);


8.4. OPERAÇÃO SOBRE ARQUIVOS 239

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

1 FILE * arq_ex1 , * arq_ex2 ;


2 /* ... */
3 arq_ex1 = fopen ( " somente_escrita . txt " , " w " ) ;
4 arq_ex2 = fopen ( " leio_e_escrevo . meu " , " a + b " ) ;

Exemplo 8.1: Abertura de arquivo

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 }

Exemplo 8.2: Abertura de arquivo com verificação de sucesso

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

int fclose (FILE *fp);


bel=exe:fseek,
Ela retorna 0 se a operação de fechamento foi bem-sucedida, e diferente de 0 caso não.
Recebe como parâmetro um ponteiro para FILE, o qual é o arquivo a ser fechado.
Após fechar um arquivo, ele deve ser aberto novamente, usando-se fopen(), caso se queira
usá-lo no programa outra vez.
Quando fclose() é chamada, qualquer informação que ainda esteja no buffer será gravada no
arquivo. Somente após isso o arquivo fı́sico é finalmente desassociado da variável FILE* usada.
8.5. OPERAÇÕES SOBRE ARQUIVOS TEXTO 241

8.5 Operações sobre arquivos texto


Já foi mostrado que um arquivo texto é aquele que armazena caracteres. Dessa forma, somente
caracteres são lidos e gravados em um arquivo-texto; algumas funções especiais existem, contudo,
para que strings (vetores de caracteres) e números possam ser lidos e gravados.

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

int fscanf ( FILE *fp, const char *format, ... );

A passagem de argumentos é semelhante à de scanf()., com o acréscimo de que deve ser


colocado como primeiro argumento o ponteiro para FILE representando o arquivo do qual serão
lidas as variáveis.
No formato comum de uso, fcanf(), assim como scanf()., encerra a leitura de uma string (ou
seja, argumento de código ”%s”) assim que um caractere de espaço em branco, tabulação ou
quebra de linha é encontrado. Dessa forma, fscanf() não é recomendada para a leitura de strings.
C oferece outra função para esse fim, que será estudada em seguida: fgets().
O exemplo 8.3 mostra como a função fscanf() pode ser usada para ler um inteiro, uma string
(não formada por espaços ou quebras de linha) e um float de um arquivo-texto. Note que é
necessário o uso de & antes da variável int e float, já que deve-se passar o endereço delas a
fscanf(), e que isso não é necessário com a variável string (como ocorre com o uso de scanf() ).

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

14 printf ( Erro na abertura de arquivo ! Abortando o programa ...) ;


15 exit (1) ;
16 }
17 fscanf ( arq , " % d \ n % s \ n % f " , & inteiro , str , & real ) ;
18 }

Exemplo 8.3: Uso de fscanf().

Dessa forma, um arquivo texto que contenha os seguintes caracteres:

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

char *fgets (char *str, int length, FILE *fp);

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:

1 char str1 [50];


2 char str2 [40];
3 FILE * arquivo ;
4
5 If ( !( arquivo = fopen ( NOME_ARQUIVO , " r " ) ) ) {
6 printf ( Erro na abertura de arquivo ! Abortando o programa ...) ;
8.5. OPERAÇÕES SOBRE ARQUIVOS TEXTO 243

7 exit (1) ;
8 }
9
10 fgets ( str1 , 50 , arquivo ) ;
11 fgets ( str2 , 40 , arquivo ) ;

Exemplo 8.4: Uso de fgets().

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

int fprintf ( FILE *fp, const char *format, ... );

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

1 # include < stdio .h >


2 # define NOME_ARQUIVO " meu_texto . txt "
3 # define TAM_STR 50
4
5 int main ( void )
6 {
7 float real = 547.32;
8 char carac = ’T ’;
9 char str [ TAM_STR ] = " teste para string " ;
10 FILE * arq ;
11
12 if ( !( arq = fopen ( NOME_ARQUIVO , " w " ) ) ) {
13 printf ( Erro na abertura de arquivo ! Abortando o programa ...) ;
14 exit (1) ;
15 }
16 fprintf ( arq , " % f \ nIsso e uma string constante \ n % s \ n % c " , real , str , carac ) ;
17 }
244 CAPÍTULO 8. ARQUIVOS

Exemplo 8.5: Uso de fprintf().

A execução desse programa resultará num arquivo com o seguinte texto:

547.32
Isso e uma string constante
teste para string
T

8.6 Operações sobre arquivos binários


Como já explicado, arquivos binários armazenam bytes que correspondem diretamente aos valores
das variáveis na memória principal (as variáveis transientes), e não somente armazenam valores
caracteres, como os arquivos de texto.
São muito úteis principalmente por poderem ser usados para ler e escrever diretamente tipos
definidos pelo programador, como estruturas.

8.6.1 Leitura
A linguagem C possui a função fread() que pode ser usada para ler um arquivo binário.

fread()

O protótipo da função 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 ) ;

Exemplo 8.6: Uso de fwrite().

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.

1 # include < stdio .h >


2
3 typedef struct {
4 char nome [50];
5 int idade ;
6 char sexo ;
7 } TPessoa ;
8
9 int main ( void )
10 {
11 FILE * arq ;
246 CAPÍTULO 8. ARQUIVOS

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 }

Exemplo 8.7: Gravação e leitura de struct em arquivos

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

8.7 Outras funções úteis para arquivos

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

int feof (FILE *fp);

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

1 # include < stdio .h >


2 # define ARQUIVO_LEITURA binario
3 # define ARQUIVO_ESCRITA texto . txt
4
5 typedef struct {
6 char nome [50];
7 char sexo ;
8 int idade ;
9 int matricula ;
10 float salário ;
11 } TFuncionario ;
12
13 TFuncionario inicializa_func ( void ) ;
14 TFuncionario le_func ( FILE * arqbin ) ;
15 TFuncionario invalida_func ( void ) ;
16 int func_valido ( TFuncionario f ) ;
17 void grava_func ( TFuncionario f , FILE * arqtex ) ;
18
19 int main ( void )
20 {
21 FILE * arqbin , * arqtex ;
22 TFuncionario f = inicializa_func () ;
248 CAPÍTULO 8. ARQUIVOS

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 }

Exemplo 8.8: Uso de feof() e outras funções no TAD TFuncionario

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

int fseek (FILE *fp, long numbytes, int origin);

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:

fseek (arq, 2 * sizeof(int), SEEK SET);

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.

1 funcionario obtem_func_posicao ( int i , FILE * arqbin )


2 {
3 funcionario f ;
4
5 if ( fseek ( arqbin , (i -1) * sizeof ( funcionario ) , SEEK_SET ) ) {
6 f = invalida_func () ;
7 return ( f ) ;
8 }
9 if ( ( fread (& f , sizeof ( funcionario ) , 1 , arqbin ) ) != 1) {
10 f = invalida_func () ;
11 }
12 return ( f ) ;
13 }

Exemplo 8.9: Uso de fseek() para posicionamento num arquivo binário

8.8 Exercicios Resolvidos


Nessa seção será proposto e resolvido um exercı́cio que engloba muitos temas que foram estudados
nesse e nos capı́tulos anteriores.

8.8.1 O Tipo Abstrato de Dados TDicionario


O exercı́cio consiste na na implementado de um TAD (Tipo Abstrato de Dados) Dicionário. Esse
tipo corresponderá a uma série de duplas (chave, valor), em que tanto chave quanto valor são strings;
a interpretação desses dados é que chave corresponde a palavras que serão explicadas por valor. Por
exemplo, duas entradas do dicionário poderiam ser (”Espı́rito Santo”, ”estado brasileiro localizado
na região Sudeste”) e (”C”, ”Linguagem de programaçao de nı́vel médio”).
A uma variável do tipo TDicionario declarada corresponderão vários valores de (chave, valor).
Essas entradas deverão ficar armazenadas num arquivo, denominado arquivo de dados. Assim, sempre
que se desejar consultar se alguma palavra está definida no dicionário (exemplo: ”há alguma entrada
para ’cachorro’ ?”), o arquivo de dados daquela variável dicionário deverá ser consultado. O mesmo
para quando uma palavra for inserida num dicionário: ela será acrescentada ao arquivo de dados
desse dicionário, ao final dele.
Supõe-se que o arquivo de dados será muito grande e que não poderá ser carregado todo para
a memória principal. Também, carregar, do disco para a memória, sequencialmente, as entradas do
dicionário salvas no arquivo de dados (a fim de fazer uma busca, por exemplo) não será uma solução
8.8. EXERCICIOS RESOLVIDOS 251

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.

Arquivo de dados: meu dicionario dados

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"

Arquivo de ı́ndices: meu dicionario indice

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:

TDicionario inicializa dic (char *nome do dicionario)

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.

TDicionario carrega dic (char *nome do dicionario)

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.

int busca dic (TDicionario dic, char *palavra, char **retorno)

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.

int insere dic (TDicionário dic, char *chave, char *valor)

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.

Implementação do TAD TDicionario

1 # include < stdio .h >


2 # include < string .h >
3 # include < stdlib .h >
4
5 # define TAM_CHAVE 50
6 # define TAM_VALOR 500
7 # define TAM_NOME_ARQ 20
8 # define MAX_ENTRADAS 200
9
10 /* duplas ( chave , valor ) */
11 typedef struct {
12 char chave [ TAM_CHAVE ];
13 char valor [ TAM_VALOR ];
8.8. EXERCICIOS RESOLVIDOS 253

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 }

Exemplo 8.10: Implementação do TAD TDicionario

Alguns aprimoramentos para o TAD TDicionario


Pelo fato das inserções de novos valores se darem sempre ao final das estruturas de dados, já que
insere-se ao final da tabela de indices em memória, do arquivo de indices e também do arquivo de
dados, os ı́ndices das entradas acabam sempre coincidindo. Isso faz com que, por exemplo, a terceira
entrada do dicionário seja a terceira entrada do arquivo de dados, do arquivo de ı́ndices e da tabela
de ı́ndices em memória (e que a tabela armazene o ı́ndice 2 para esse elemento).
Uma alteração pode ser feita com a tabela de ı́ndices em memória. Ela pode ser mantida
ordenada se cada novo elemento for inserido nela respeitando a ordem alfabética de sua chave. Com
essa ordenação, as entradas podem ser buscadas rapidamente na tabela, mediante o uso, por exemplo,
de uma busca binária, um algoritmo de busca que trabalha particionando o vetor de busca (ordenado)
em duas metades, e então, recursivamente, refaz a busca ou na metade superior ou inferior, via a
observação do valor a ser buscado. Uma sugestão para a implementação desse aprimoramento é
a criação de duas novas funções: uma delas, que seria usada pela função busca dic(), realizaria a
busca binária de um valor string num vetor de indice dic passado; a outra função, a ser usada por
insere dic(), faria a inserção ordenada no vetor de indice dic passado (note que essa função poderia
usar a função de busca binária anteriormente descrita).
O arquivo de ı́ndices poderia ser mantido desordenado, via a inserção de novos valores sempre
ao seu final. O importante, para demandar o menor tempo computacional possı́vel, é usar menos
entrada e saı́da (como o acesso aos dados do disco). Mas, como a tabela de ı́ndices em memória deve
ser mantida organizada, ao ser carregado um dicionario (via a funcao carrega dic() ), as informações
devem ser lidas do arquivo e inseridas ordenadamente no vetor em memória.
Com esse aperfeiçoamento descrito, um valor seria encontrado muito mais rapidamente na tabela
8.9. RESUMO 257

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.

2. Escreva um programa em C que solicite ao usuário a digitação do nome de um arquivo texto


já existente, e que então gere um outro arquivo, que será uma cópia do primeiro.

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

8.11 Trabalhos Sugeridos


Falta acesso aos trabalhos sugeridos dos outros capı́tulos.
Referências Bibliográficas

[1] BRIAN KERNIGHAN and DENNIS RITCHIE. The C Programming Language. second edition,
1988.

[2] PAUL STRATHERN. Turing e o Computador em 90 Minutos. first edition, 2000.

[3] ANDREW S TANENBAUM and JAMES R GOODMAN. Structured Computer Organization.


fourth edition, 1999.

261

Anda mungkin juga menyukai