Você está na página 1de 398

Este material aborda o conteúdo de

uma disciplina de introdução à


programação para cursos de Ciência da
Computação e Engenharias. A
linguagem de programação utilizada é
a Linguagem C. A primeira parte
engloba o histórico da computação e os
fundamentos necessários à
compreensão do mundo digital. A
segunda parte contempla os principais
elementos de programação do
paradigma imperativo e procedural.

Introdução à
Programação:
Conceitos e Práticas.

Jorge Habib Hanna El Khouri


Introdução à Programação:
Conceitos e Práticas.
(notas de aulas)

Introdução à Programação: Conceitos e Práticas. Página 1


Introdução à Programação:
Conceitos e Práticas.
(notas de aulas)

Jorge Habib Hanna El Khouri

Professor Assistente do Centro de Engenharias e Ciências Exatas


Universidade Estadual do Oeste do Paraná
Campus de Foz do Iguaçu

Revisão Técnica:

Dez-2019

Introdução à Programação: Conceitos e Práticas. Página 2


CONTEÚDO

1. História da Computação .................................................................................. 10


1.1. Principais Eventos ........................................................................................... 10
1.2. Gerações de Computadores ........................................................................... 15
1.2.1. Primeira Geração (1938 - 1953) ...................................................................... 16
1.2.2. Segunda Geração (1952 - 1963) ..................................................................... 17
1.2.3. Terceira Geração (1962 - 1975) ...................................................................... 20
1.2.4. Quarta Geração (1972 - ...) ............................................................................. 21
2. Sistemas de Numeração ................................................................................. 24
2.1. Sistema de Numeração Egípcio ...................................................................... 24
2.2. Sistema de Numeração Romano .................................................................... 25
2.3. Sistema de Numeração Maia .......................................................................... 25
2.4. Sistema de Numeração Decimal ..................................................................... 26
3. Conceito de Bases .......................................................................................... 30
3.1. Sistema Decimal ............................................................................................. 30
3.2. Sistema Hexadecimal...................................................................................... 31
3.3. Sistema Binário ............................................................................................... 32
3.4. Base Qualquer ................................................................................................ 33
3.5. Conversão entre Bases ................................................................................... 33
3.5.1. Conversão de Qualquer Base para a Base 10 ................................................ 34
3.5.2. Conversão da Base 10 para Qualquer Base ................................................... 36
3.6. Exercícios ........................................................................................................ 39
3.6.1. Conversão da Base 2n para a Base 2 ............................................................. 39
3.6.2. Conversão da Base 4 para a Base 2............................................................... 40
3.6.3. Conversão da Base 8 para a Base 2............................................................... 40
3.6.4. Conversão da Base 16 para a Base 2............................................................. 41
3.7. Exercícios ........................................................................................................ 42
4. Operações com Números Binários.................................................................. 43
4.1. Operações Aritméticas .................................................................................... 43
4.1.1. Soma ............................................................................................................... 43
4.1.2. Subtração ........................................................................................................ 44
4.1.3. Multiplicação ................................................................................................... 44
4.1.4. Divisão ............................................................................................................ 45
4.1.5. Exercícios ........................................................................................................ 46
4.2. Operações Lógicas ......................................................................................... 47
4.2.1. NOT (NÃO) ..................................................................................................... 47
4.2.2. AND (E) ........................................................................................................... 49
4.2.3. OR (OU) .......................................................................................................... 52
4.2.4. XOR / OU EXCLUSIVO ................................................................................... 54
4.2.5. Exemplos ........................................................................................................ 55

Introdução à Programação: Conceitos e Práticas. Página 3


4.2.6. Exercícios ........................................................................................................ 56
4.3. Operações Aritméticas Binárias com Circuitos Lógicos .................................. 57
4.3.1. Somador Completo ......................................................................................... 57
4.3.2. Subtrator Completo ......................................................................................... 59
5. Representação de Dados ................................................................................ 61
5.1. Representação de Números Inteiros ............................................................... 62
5.1.1. Inteiros Sem Sinal – Binário Puro.................................................................... 62
5.1.2. Inteiro Com Sinal ............................................................................................. 62
5.1.2.1. Módulo Sinal ................................................................................................... 62
5.1.2.2. Complemento de 1 .......................................................................................... 63
5.1.2.3. Complemento de 2 .......................................................................................... 64
5.1.2.4. Excesso de 2N-1 ............................................................................................... 65
5.1.2.5. Visualização .................................................................................................... 66
5.1.3. Soma em Complemento de 2 .......................................................................... 67
5.1.4. Exercícios ........................................................................................................ 69
5.2. Representação de Números Reais ................................................................. 69
5.2.1. Formato IEEE 754 ........................................................................................... 73
5.2.2. Exercícios ........................................................................................................ 74
5.3. Representação de Informações Textuais ........................................................ 76
6. Introdução a Arquitetura .................................................................................. 78
6.1. Processador - CPU ......................................................................................... 80
6.1.1. Seção de Controle........................................................................................... 81
6.1.2. Seção de Processamento ............................................................................... 81
6.1.3. Barramentos .................................................................................................... 82
6.1.4. O sinal de Clock .............................................................................................. 84
6.1.5. Organização do Computador .......................................................................... 85
6.1.5.1. Pipeline ........................................................................................................... 85
6.1.5.2. Cache .............................................................................................................. 86
6.1.5.3. Outras Técnicas .............................................................................................. 87
7. Introdução à Programação .............................................................................. 88
8. O Processo de Tradução ................................................................................ 91
9. A Linguagem C ............................................................................................... 92
10. Estrutura de um Programa C .......................................................................... 95
11. Identificadores ................................................................................................. 95
12. Constantes ...................................................................................................... 97
13. Variáveis ......................................................................................................... 99
14. Tipos de Dados Numéricos ........................................................................... 100
15. Operador Atribuição ...................................................................................... 102
16. Expressões Numéricas ................................................................................. 104
16.1. Operadores Matemáticos .............................................................................. 104
16.2. Funções Matemáticas ................................................................................... 107
16.3. Exemplos ...................................................................................................... 120
17. Comandos de Entrada e Saída ..................................................................... 122
17.1. Entrada de Dados ......................................................................................... 122
17.2. Saída de Dados ............................................................................................ 126
17.3. Exemplos ...................................................................................................... 131
18. Implementando Funções ............................................................................... 133
18.1. Exemplos ...................................................................................................... 134
18.2. Exercícios ...................................................................................................... 137
19. Expressões Lógicas ...................................................................................... 144

Introdução à Programação: Conceitos e Práticas. Página 4


19.1. Operadores Relacionais ................................................................................ 144
19.2. Operadores Lógicos ...................................................................................... 146
19.3. Exemplos ...................................................................................................... 147
20. O Tipo Ponteiro ............................................................................................. 154
21. Passagem de Parâmetros ............................................................................. 158
21.1. Passagem de Parâmetro por Cópia de Valor ................................................ 158
21.2. Passagem de Parâmetro por Cópia de Endereço ......................................... 161
21.3. Exemplo ........................................................................................................ 164
22. Instruções de Controle .................................................................................. 169
22.1. Seleção – IF-THEN-ELSE ............................................................................. 170
22.1.1. Sintaxe e Semântica ..................................................................................... 170
22.1.2. Exemplos ...................................................................................................... 171
22.2. Seleção – SWITCH-CASE ............................................................................ 179
22.2.1. Sintaxe e Semântica ..................................................................................... 179
22.2.2. Exemplos ...................................................................................................... 181
22.3. Repetição – WHILE ....................................................................................... 185
22.3.1. Sintaxe e Semântica ..................................................................................... 185
22.3.2. Exemplos ...................................................................................................... 187
22.4. Repetição – FOR........................................................................................... 202
22.4.1. Sintaxe e Semântica ..................................................................................... 202
22.4.2. Exemplos ...................................................................................................... 203
22.5. Repetição DO-WHILE ................................................................................... 210
22.5.1. Sintaxe e Semântica ..................................................................................... 210
22.5.2. Exemplos ...................................................................................................... 211
23. Array.............................................................................................................. 213
23.1. Vetor.............................................................................................................. 214
23.1.1. Sintaxe e Semântica ..................................................................................... 214
23.1.2. Exemplos ...................................................................................................... 217
23.2. Strings em C ................................................................................................. 234
23.2.1. Operações com String................................................................................... 236
23.3. Vetor e Ponteiro ............................................................................................ 257
23.3.1. Expressões com ponteiros ............................................................................ 259
23.3.2. String e ponteiro ............................................................................................ 263
23.4. Funções da Biblioteca <string.h> .................................................................. 267
23.5. Matriz ............................................................................................................ 274
23.5.1. Exemplos ...................................................................................................... 276
24. Agregados Heterogêneos - Estruturas .......................................................... 299
24.1. Tipo struct ..................................................................................................... 300
24.2. Passagem de struct como parâmetro: por cópia ........................................... 304
24.3. Passagem de struct como parâmetro: por endereço..................................... 305
24.4. Retorno de struct como resultado de função ................................................. 306
24.5. Exercícios ...................................................................................................... 307
24.6. Abstrações da Linguagem C ......................................................................... 311
25. Arquivos ........................................................................................................ 313
25.1. Arquivo Texto ................................................................................................ 318
25.2. Arquivo Binário .............................................................................................. 330
26. Enumerados .................................................................................................. 338
27. Expressões Binárias...................................................................................... 341
27.1. Operadores Binários ..................................................................................... 341
27.2. Operadores de Deslocamento de Bits........................................................... 342

Introdução à Programação: Conceitos e Práticas. Página 5


27.3. Exemplos ...................................................................................................... 344
28. Estrutura de dados dinâmica ......................................................................... 350
28.1. Organização da memória de um programa em execução ............................. 350
28.2. Alocação dinâmica ........................................................................................ 351
28.3. Exemplos ...................................................................................................... 353
28.4. Lista Encadeada............................................................................................ 368
29. Interpretador da Linha Comando .................................................................. 379
29.1. Exemplos ...................................................................................................... 380
30. Recursividade ............................................................................................... 386
30.1. Conceito ........................................................................................................ 386
30.2. Exemplos ...................................................................................................... 388
31. Referências Bibliográficas ............................................................................. 392

Introdução à Programação: Conceitos e Práticas. Página 6


FIGURAS

Figura 1: Ábaco. ....................................................................................................... 10


Figura 2: Ábaco e o seu uso. .................................................................................... 10
Figura 3: O mecanismo de Anticítera. ...................................................................... 11
Figura 4: Réplica do mecanismo de Anticítera. ........................................................ 11
Figura 5: Esquema da máquina de Anticítera........................................................... 11
Figura 6: Tabela de Logaritmo de Napier ................................................................. 11
Figura 7: Manuscrito Original de Schickard. ............................................................. 12
Figura 8: Réplica da Calculadora de Schickard. ....................................................... 12
Figura 9: La Pascaline. ............................................................................................. 12
Figura 10: La Pascaline. ........................................................................................... 12
Figura 11: Régua de Cálculo. ................................................................................... 13
Figura 12: Máquina de Leibniz. ................................................................................ 13
Figura 13: Tear Automático de Jacquard. ................................................................ 13
Figura 14: Máquina Analítica de Babbage. ............................................................... 14
Figura 15: Formulário: Primeiro algoritmo de Ada. ................................................... 14
Figura 16: Detalhe de parte do Formulário. .............................................................. 14
Figura 17: Cartão perfurado de Hollerith. ................................................................. 15
Figura 18: Máquina de Tabulação. ........................................................................... 15
Figura 19: ENIAC. .................................................................................................... 16
Figura 20: Memória de Núcleo. ................................................................................ 18
Figura 21: Ambiente de Programação da Segunda Geração. .................................. 19
Figura 22: Evolução no nível de Programação. ........................................................ 20
Figura 23: Principais características de Software e Hardware por geração. ............ 23
Figura 24: Circuitos Digitais: Contribuições .............................................................. 43
Figura 25: Circuito Somador Completo de 1 bit. ....................................................... 58
Figura 26: Circuito da Soma de N bits. ..................................................................... 59
Figura 27: Circuito Subtrator Completo de 1 bit........................................................ 60
Figura 28: Mundo Real x Mundo Digital. .................................................................. 61
Figura 29: Métodos de Representação de Dados .................................................... 61
Figura 30: Representação em Módulo-Sinal ............................................................ 63
Figura 31: Faixa de Representação de Números Reais. .......................................... 72
Figura 32: Organização típica de um computador. ................................................... 78
Figura 33: Programa na Memória RAM: Código + Dados. ....................................... 79
Figura 34: Unidade Central de Processamento típica. ............................................. 80
Figura 35: Passos na execução de uma instrução. .................................................. 81

Introdução à Programação: Conceitos e Práticas. Página 7


Figura 36: A sincronização da execução de uma instrução com o sinal de clock. ... 84
Figura 37: Execução e Pipeline. ............................................................................... 85
Figura 38: Memória Cache. ...................................................................................... 86
Figura 39: Genealogia das Linguagens – Base FORTRAN. .................................... 88
Figura 40: Genealogia das Linguagens. ................................................................... 89
Figura 41: Níveis de Programação. .......................................................................... 89
Figura 42: Ciclo de Desenvolvimento de Software. .................................................. 90
Figura 43: Processo de Tradução ............................................................................ 91
Figura 44: Estilo de Programação Spaghetti. ........................................................... 92
Figura 45: Evolução no estilo de programação. ....................................................... 93
Figura 46: Analogia variável vs cadeira. ................................................................... 99
Figura 47: Precedência de Operadores,. ................................................................ 105
Figura 48: Estrutura de uma função. ...................................................................... 133
Figura 49: Estrutura da função getimc. ................................................................... 136
Figura 50: Contexto da chamada da função foo. .................................................... 159
Figura 51: Ponto 1 – Passagem por Cópia. ............................................................ 160
Figura 52: Ponto 2 – Passagem por Cópia. ............................................................ 160
Figura 53: Ponto 3 – Passagem por Cópia. ............................................................ 160
Figura 54: Ponto 1 – Passagem por “Referência”. ................................................. 162
Figura 55: Ponto 2 – Passagem por “Referência”. ................................................. 162
Figura 56: Ponto 3 – Passagem por “Referência”. ................................................. 163
Figura 57: Ponto 1 – Calculo Raizes. ..................................................................... 165
Figura 58: Ponto 2 – Calculo Raizes. ..................................................................... 165
Figura 59: Ponto 3 – Calculo Raizes. ..................................................................... 166
Figura 60: Diagrama lógico: cálculo das raízes de equação do 2º grau. ................ 174
Figura 61: Instrução switch-case ............................................................................ 180
Figura 62: Estrutura de Controle do tipo for............................................................ 202
Figura 63: Instrução BREAK................................................................................... 206
Figura 64: Ilustração de array com cadeiras........................................................... 216
Figura 65: Armazenamento de string. .................................................................... 235
Figura 66: Conversão Decimal - Binário ................................................................. 245
Figura 67: Conversão de string para inteiro............................................................ 251
Figura 68: Relação Vetor e Ponteiro ...................................................................... 257
Figura 69: Quadro ilustrativo de configuração inicial de memória. ......................... 260
Figura 70: Comparação Escalar, Vetor e Matriz ..................................................... 274
Figura 71: Matriz de 100x50 ................................................................................... 275
Figura 72: Utilização de parte de uma matriz. ........................................................ 277
Figura 73: Repetições em uma matriz. ................................................................... 278
Figura 74: Array como agregado homogêneo. ....................................................... 299
Figura 75: Exemplo de agregado heterogêneo - struct. ......................................... 299
Figura 76: Struct com string e vetor de float. .......................................................... 302
Figura 77: Abstrações típicas de uma linguagem procedural ................................. 311
Figura 78: Fluxo de dados Teclado-Memória. ........................................................ 313
Figura 79: Fluxo de dados Memória-Tela de Vídeo................................................ 314
Figura 80: Fluxo de dados Arquivo-Memória. ......................................................... 314
Figura 81: Fluxo de dados Memória-Arquivo. ......................................................... 315
Figura 82: Estruturas de memória. ......................................................................... 321
Figura 83: Organização da Memória RAM. ............................................................ 351

Introdução à Programação: Conceitos e Práticas. Página 8


PREFÁCIO

O conteúdo deste material é fruto de um conjunto de notas de aulas preparadas para a


disciplina introdutória à programação da grade do curso de Ciência da Computação e
posteriormente estendido para cursos de engenharias, principalmente Engenharia
Elétrica e Mecânica. A estrutura do material não está centrada em uma linguagem de
programação, mas sim no estabelecimento de uma sequência de conteúdos que
proporcione, desde o seu princípio, o suporte lógico para a construção de programas
iniciais. No avançar das temáticas o conhecimento vai sendo refinado e ao mesmo
tempo testado com problemas mais elaborados.

Antecedendo o conteúdo de programação são apresentados tópicos que ilustram os


pontos na história que marcaram a evolução da computação, onde conhecimentos das
mais diversas áreas foram combinados a fim de delinear o estado atual da tecnologia
da informação. Uma parte fundamental da computação está na compreensão da
natureza dos dados e como eles circulam no ambiente computacional. Assim, os
sistemas de numeração são abordados visando alcançar um entendimento sobre a
notação binária que predomina no mundo digital. Uma breve exposição sobre a
destaca o trânsito da informação entre as camadas de e
de , e expõe a importância de se compreender toda a transformação a que é
submetido um determinado problema até se transformar efetivamente em um
programa ativo na arquitetura do computador. Um estudante de computação ou de
engenharia (relacionada ao tema) deve ter a clara noção de todas as camadas de
abstração que formam o contexto computacional. Os principais métodos de
representação de dados elementares são expostos e proporcionam conhecimentos
essenciais ao entendimento dos seus impactos nos algoritmos.

A parte de programação estabelece uma sequência de exposição do conteúdo tanto


nos aspectos conceituais quanto nas aplicações práticas. Os recursos de programação
apresentados são os típicos da família de linguagens denominada imperativa ou
procedural, e a linguagem é a escolhida para concretizar os ensinamentos. A grande
similaridade entre as linguagens procedurais atuais faz com que a transposição dos
conhecimentos apresentados para outras linguagens seja muito facilitada.

Introdução à Programação: Conceitos e Práticas. Página 9


1. História da Computação

Este capítulo resume os principais eventos históricos que culminaram com o estágio
atual da tecnologia digital. Os desenvolvimentos ocorreram nas mais variadas áreas
da Ciência e o processo de evolução tecnológica foi gerado em grande parte pelo
cruzamento destes conhecimentos. Destaca-se como fonte de consulta para
aprofundamento dos temas aqui citados a publicação [ ] . Além disso, o
acervo do e do site , principalmente as
fotos, também serviram de fonte de consulta.

1.1. Principais Eventos

O uso de ferramentas para apoiar as atividades do homem não é recente, e desde os


primórdios de nossa história tem-se observado um constante aumento no uso de
equipamentos e instrumentos que auxiliam nas tarefas normalmente realizadas por
seres humanos.

No campo da informática, o processo de evolução se deu de forma lenta durante


séculos, experimentando uma explosão tecnológica acentuada na segunda metade do
século passado.

Na busca por alternativas para o trabalho realizado pelo homem, principalmente na


mecanização do processo de contagem e na realização de trabalhos exaustivos,
obstáculos importantes foram sendo superados, a saber:

a) a.C. - uso do ábaco no vale entre os rios Tigre e Eufrates;


b) a.C. - aparecimento do ábaco chinês, chamado Suan-Pan, e do Soroban no
Japão;

Figura 1: Ábaco. Figura 2: Ábaco e o seu uso.

c) a.C. – Alfabeto Fenício;


d) a.C. – uso de um mecanismo denominado Anticítera 1 com um complexo
sistema de engrenagens provavelmente aplicado à navegação. Foi resgatado em

1
http://pt.wikipedia.org/wiki/Máquina_de_Anticítera

Introdução à Programação: Conceitos e Práticas. Página 10


na costa da ilha grega de Anticítera. O sistema de engrenagens simulava as
órbitas da lua, sol e de mais cinco planetas, além de prever eclipses da lua e do sol.

Figura 4: Réplica do
Figura 3: O mecanismo de mecanismo de Figura 5: Esquema da máquina
Anticítera. Anticítera2. de Anticítera.

e) - Johannes Gutenberg (Mainz – Sacro Império Romano Germânico,


— de fevereiro de ), inventou a Prensa Móvel.
f) séc. - John Napier (Edimburgo – Escócia, — de abril de ) inventou
o logaritmo e uma máquina capaz de multiplicar e dividir automaticamente;

Figura 6: Tabela de Logaritmo de Napier


g) - Wilhelm Schickard ( de abril de , Herrenberg, Alemanha — de
outubro de , Tübingen) construiu uma calculadora mecânica, baseada em
rodas dentadas, capaz de realizar as operações básicas com números de seis
dígitos e indicar um overflow através do toque de um sino.

2
Disponível no Museu Arqueológico Nacional de Atenas (feita por Robert J. Deroski, com base em Derek J. de
Solla Price. http://pt.wikipedia.org/wiki/Máquina_de_Anticítera

Introdução à Programação: Conceitos e Práticas. Página 11


Figura 7: Manuscrito Original de Schickard. Figura 8: Réplica da Calculadora de Schickard.

h) - Blaise Pascal (Clermont-Ferrand - França, de Junho de — Paris,


de Agosto de ) utilizando o princípio do ábaco, implementou uma máquina
automática de calcular - La pascaline. Efetuava somas e subtrações;

Figura 9: La Pascaline. Figura 10: La Pascaline.

i) – Padre Willian Oughtred (Eton-Inglaterra, de Março de — Albury,


de Junho de ) inventou a régua de cálculo (Slide rule/ slipstick) baseada nos
logaritmos de Napier e divulgou o uso do sinal para multiplicação, tendo
introduzido os termos , e .

j) – Seth Patridge, Inglês, desenvolveu a régua de cálculo deslizante. Este


dispositivo foi muito utilizado até os anos setenta;

Introdução à Programação: Conceitos e Práticas. Página 12


Figura 11: Régua de Cálculo.
k) - Gottfried Wilhelm von Leibniz (Leipzig - Alemanha, de julho de —
Hanover, de novembro de ) aprimorou a máquina de Pascal e obteve uma
calculadora que somava, subtraía, multiplicava e dividia;

Figura 12: Máquina de Leibniz.


l) - Gottfried Wilhelm von Leibniz (Leipzig - Alemanha, de julho de —
Hanover, de novembro de ) publica o artigo "Explication de l'Arithmétique
Binaire" organizando o conhecimento sobre o sistema numérico binário;
m) - James Watt (Greenock, Reino Unido, 19 de Janeiro de 1736 — Heathfield
Hall, Reino Unido, 25 de Agosto de 1819) – Máquina a Vapor – Revolução
Industrial – Indústria 1.0.
n) – Joseph Marie Charles - apelido Jacquard (Lyon – França, de Julho de
– de Agosto de ), mecânico, construiu um tear automático com entrada
de dados via cartões perfurados para controlar a confecção dos tecidos e seus
desenhos;

Figura 13: Tear Automático de Jacquard.

Introdução à Programação: Conceitos e Práticas. Página 13


o) – Charles Babbage (Londres - Inglaterra, de Dezembro de —
Londres, de Outubro de ) projetou a sua Máquina Analítica ou Diferencial,
que possuía memória, programa, unidade de controle e periféricos;

Figura 14: Máquina Analítica de Babbage.

p) - Ada Augusta Byron King, Condessa de Lovelace (Londres – Inglaterra,


de Dezembro de - de Novembro de ), conhecida como Ada Lovelace,
fez os primeiros estudos sobre aritmética binária. Escreveu o primeiro algoritmo
para ser processado por uma máquina, a máquina analítica de Charles Babbage.

Figura 15: Formulário: Primeiro algoritmo


de Ada.

Figura 16: Detalhe de parte do Formulário.

q) - George Boole (Lincoln - Inglaterra, de Novembro de — Ballintemple,


de Dezembro de ) desenvolveu a teoria da Álgebra de Boole (livro: An
Investigation of the Laws of Thought), que permitiu o desenvolvimento da Teoria
dos Circuitos Lógicos;

Introdução à Programação: Conceitos e Práticas. Página 14


r) - Herman Hollerith (Buffalo - EUA, de Fevereiro de — Washington,
D.C., de Novembro de ) empresário norte-americano, construiu a Máquina
de recenseamento utilizada no censo de dos EUA. A máquina efetuava a
leitura dos cartões de papel perfurados com as informações em código
(Decimal Codificado em Binário). Os dados do censo de foram tabulados em
apenas um ano. O censo de levou oito anos para ser tabulado;

Figura 17: Cartão perfurado de


Hollerith. Figura 18: Máquina de Tabulação.

s) - Hollerith e Watson fundam a IBM;


t) - Alan Mathison Turing (Londres – Inglaterra, de Junho de — de
Junho de ) desenvolveu a teoria de uma capaz de resolver qualquer
problema computável. Foi denominada de ;
u) - Claude Elwood Shannon (Petoskey, Michigan – EUA, de Abril de
— de Fevereiro de ) escreve sua tese de mestrado “A Symbolic Analysis of
Relay and Switching Circuits” onde utiliza aritmética binária e Álgebra de Boole para
implementar lógicas através de circuitos com chaves e relés, tornando-se um marco
para os circuitos digitais;
v) - sob a liderança de Alan Turing foi projetado o , computador inglês
utilizado na Segunda Guerra Mundial para quebrar o segredo alemão codificado
pela máquina .

Este processo evolutivo culminou com o surgimento das primeiras máquinas capazes
de realizar cálculos mais complexos do que as operações básicas. A partir de então,
várias gerações de computadores foram surgindo até os dias atuais.

1.2. Gerações de Computadores

Na sequência são listadas as principais contribuições em cada geração de


computadores, enfatizando-se a tecnologia utilizada para a construção do hardware
e a forma de programação. Trata-se apenas de um sumário bastante resumido, que
serve como referência de termos para pesquisas mais detalhadas.

Introdução à Programação: Conceitos e Práticas. Página 15


1.2.1. Primeira Geração (1938 - 1953)

A primeira geração foi marcada pela construção do primeiro computador eletrônico, o


V
(Electronic Numerical Integrator and Computer), em , tornando-se um
marco na evolução da computação. O começou a operar em , sendo
concluído totalmente em e encerrando suas operações em .

O desenvolvimento do se deu na Universidade da Pensilvânia (Filadélfia, EUA),


e contou com o apoio de John von Neumann.

Figura 19: ENIAC.

O continha válvulas, relés, resistores, capacitores


o que resultava em mais de toneladas de equipamentos distribuídos em .
Seu consumo chegava a . A entrada e saída de dados eram feitas por meio de
leitora de cartões de perfurados, enquanto que a programação se dava pela
construção de circuitos específicos para tratar o problema por ajustes manuais de
chaves e conexões de cabos.

A unidade de sincronização trabalhava com ciclos de microsegundos ( ciclos de


clock de ), sendo capaz de executar adições/subtrações ou
multiplicações ou divisões ou raiz quadrada por segundo. A memória era capaz de
conter números ( bytes).

Sua utilização inicial foi em trabalhos com soluções numéricas de problemas


relacionados com trajetórias balísticas e no desenvolvimento da bomba de hidrogênio.

Em junho de , von Neumann publicou o trabalho sobre o (Electronic


Discrete Variable Automatic Computer), e estabeleceu as bases para as arquiteturas
de computadores das gerações seguintes, atualmente conhecida como “arquitetura de
von Neumann”, que trazia o conceito de programa armazenado.

Introdução à Programação: Conceitos e Práticas. Página 16


O esforço de guerra levou à construção, em , pelos ingleses da máquina
, concebida para quebrar o código da máquina alemã , utilizada na
transmissão de mensagens. Pelo fato de ser uma máquina estratégica no campo
militar, a sua divulgação foi bastante limitada, o que restringiu o reconhecimento da sua
importância no desenvolvimento da computação. Os trabalhos de Alan Turing sobre
computabilidade foram consideravelmente relevantes na construção dos algoritmos da
máquina .

Nesta geração, a programação era em linguagem de máquina composta da


codificação binária das instruções da máquina. Por exemplo, a soma de dois números
produzindo um resultado na memória tinha um código semelhante à:

O entendimento desta lógica era restrito aos profissionais com conhecimento da


arquitetura do computador.

Em resumo, a primeira geração se caracterizou por:

 Surgimento do ;
 Utilização de relés como dispositivos de chaveamento;
 Utilização de válvulas a vácuo;
 Hardware caro, grande e de difícil manutenção;
 Programação em linguagem de máquina;
 permitiu o uso de software.

1.2.2. Segunda Geração (1952 - 1963)

A segunda geração foi caracterizada pela invenção do por John Bardeen,


Walter Brattain e William Shockley. Em dezembro de Brattain e Moore
apresentaram o dispositivo para um grupo da Bell Labs.

Em a Bell Labs conclui o , tido como o primeiro computador totalmente


transistorizado. As vantagens dos transistores em relação às válvulas a vácuo são
principalmente: menos consumo de energia, peso e tamanho menores, reduzida perda
de calor e aquecimento, maior confiabilidade e robustez, maior duração e resistência a
impacto e vibração.

Também nesta geração foram consolidados dispositivos como as impressoras, as fitas


magnéticas e os discos para armazenamento, dentre outros.

A memória do computador passou a utilizar tecnologia de núcleo magnético, também


denominada , composta por diminutos anéis magnéticos conectados por
fios que permitiam a leitura e gravação de informação. Os valores e eram
associados ao sentido de magnetização deste núcleo.

Introdução à Programação: Conceitos e Práticas. Página 17


Figura 20: Memória de Núcleo.

O surgimento dos sistemas operacionais e das linguagens de programação acelerou o


desenvolvimento dos computadores.

A forma de programação evoluiu da para


, onde os códigos binários eram associados a mnemônicos que
indicavam o tipo de operação realizada. Assim, a soma de dois números produzindo
um resultado na memória tinha um código semelhante à:

LINGUAGEM ASSEMBLY LINGUAGEM DE MÁQUINA

A percepção do poder de codificação em linguagens mais acessíveis levou a um


importante salto das linguagens de alto nível com a criação do em e
em iniciando assim um forte ciclo de desenvolvimento de novas
linguagens.

O mesmo problema podia agora ser codificado em uma de


forma muito semelhante à sua notação matemática:

LINGUAGEM DE ALTO NÍVEL LINGUAGEM ASSEMBLY LINGUAGEM DE MÁQUINA

A forma de utilização dos computadores era semelhante a de uma calculadora, onde o


programa e dados eram introduzidos via cartões perfurados, fita de papel perfurado ou
fita magnética e os resultados eram gerados para estes mesmos dispositivos. Em
a primeira impressoraVI, fabricada pela Remington-Rand, foi utilizada como dispositivo
de saída integrado ao computador , dos mesmos criadores do .

Neste período, os computadores ocupavam salas específicas que acomodavam toda a


estrutura de equipamentos e sua operação demandava técnicos especializados. É
importante lembrar que os computadores eram caros e de uso restrito a governos,
universidades e grandes corporações. Os programas eram produzidos e escritos em
formulário próprio e levados à sala de perfuração de cartão. O maço de cartões era
levado á sala com a leitora, onde um operador procedia a leitura e processamento do
pacote com o programa. Ao encerrar o processamento deste lote de cartões então o

Introdução à Programação: Conceitos e Práticas. Página 18


operador se dirigia a sala de impressora para retirar o relatório para entrega ao usuário.
A seguinte figura esboça este fluxo:

Figura 21: Ambiente de Programação da Segunda Geração.

Cada programa que necessitava de processamento tinha que seguir este fluxo. A
necessidade de melhores mecanismos de interação entre usuário e o computador,
facilidades para administrar o processamento dos programas e formas de gerenciar o
computador e seus periféricos levou ao desenvolvimento de um conjunto de programas
que foi denominado de .

Assim, surgiu a ideia de processamento em lote onde grupos de programas (maços de


cartões) eram lidos e gravados em uma fita. A fita era levada à sala de processamento
e lida para o computador, que processava toda a sequência e gerava uma saída
também em fita, cuja impressão acontecia em uma unidade separada (impressão off-
line).

Em resumo, a segunda geração se caracterizou por:

 invenção do transistor;
 1º computador a transistor ( ) pela ;
 uso de transistores e diodos;
 uso de memória de núcleo;
 uso de linguagem Assembly até , e de e a partir daí;
 surgimento dos Sistemas Operacionais para Processamento em (Lote)
de programas.

Introdução à Programação: Conceitos e Práticas. Página 19


1.2.3. Terceira Geração (1962 - 1975)

Na geração anterior, o elemento básico para construção dos processadores evoluiu da


válvula a vácuo para os transistores. Na terceira geração, o transistor continuou a ser o
dispositivo de construção elementar, porém surgiu o circuito integrado ( ou chip) em
pequena ( componentes eletrônicos por ) e média escala ( componentes
eletrônicos por ), que são circuitos eletrônicos em miniatura compostos
principalmente por dispositivos semicondutores.

A invenção do circuito integrado ou ( ) é atribuída a Jack Kilby da Texas


Instruments e Robert Noyce, da Fairchild Semiconductor, cabendo a Kilby o Prêmio
Nobel de Física no ano 2000 pela participação na criação.

O uso de circuitos integrados permitiu maior compactação e confiabilidade dos


computadores com expressiva redução de custos. A tecnologia de fabricação da
memória de acesso aleatório (RAM) evolui da para dispositivos de
estado sólido.

O tinha as maiores atenções no projeto de um computador, porém o


estava ganhando importância rapidamente. Foram criadas várias linguagens
de programação, entre elas , , , , , dentre outras. O
processo de produção de programas começou a ser visto como atividade de
engenharia, com todo o rigor que isto significa.

A figura a seguir ilustra como o programador foi se afastando da necessidade de


codificar diretamente em binário na medida em que surgiam novas linguagens de
programação. Inicialmente a permitiu maior produtividade na
programação, e a tradução para a era feita por um software
tradutor denominado Em seguida, com o surgimento da
, como o o programador codificava de forma
mais adequada aos problemas matemáticos, sendo que a tradução para o
era feita por um software denominado .

Figura 22: Evolução no nível de Programação.

Houve importante melhoria nos compiladores, que passaram a produzir códigos tão
eficientes quanto um bom programador de linguagem assembly.

Introdução à Programação: Conceitos e Práticas. Página 20


O mercado de computadores era majoritariamente dominado pelos produtos da ,
com destaque para os ou computadores de grande porte. Os sistemas
operacionais apresentaram significativa melhora e incorporaram recursos de
multiprogramação, time-sharing e memória-virtual. A multiprogramação permitia que o
sistema operacional trabalhasse com vários programas ativos. Neste caso, se um
determinado programa demandava dados armazenados em fita, então outro programa
entrava em processamento, até que o recurso do programa anterior fosse
disponibilizado. O , similar ao multiprocessamento, permitiu que um único
computador tivesse seu uso compartilhado por múltiplos usuários com acesso via
terminal on-line. A memória virtual é um técnica que permite que programas utilizem
mais memória do que o disponível fisicamente na memória principal. A memória
secundária (em disco, fita ou outro meio de armazenamento) é utilizada como suporte
para esta técnica de gerenciamento de memória.

Outro recurso típico da terceira geração permitiu que programas fossem carregados
diretamente dos cartões para discos. Assim, logo que um determinado programa
tivesse sua execução encerrada, o sistema operacional iria carregar um dos
programas do disco para a partição de memória destinada aos serviços em execução.
Este recurso recebeu o nome de (
).

Em resumo, a terceira geração se caracterizou por:

 uso de circuitos integrados em pequena ( ) e média escala ( );


 uso de circuitos impressos em multicamadas;
 uso de memória de estado sólido;
 linguagens de alto nível com compiladores inteligentes;
 uso de multiprogramação
 sistemas operacionais para uso em (tempo-compartilhado);
 utilização de memória virtual.

1.2.4. Quarta Geração (1972 - ...)

Os circuitos integrados apresentaram um aumento significante na quantidade de


transistores. A tecnologia foi denominada de Integração em Escala Muito Grande
( – ) incorporando de a
transistores. As escalas seguintes de integração, ultra e super, comportam a e
a dispositivos semicondutores respectivamente.

Este nível de integração e compactação permitiu que os incorporassem cada vez


mais blocos funcionais da arquitetura do computador, ao ponto que todos os elementos
básicos de uma (Central Processing Unit) coubessem em um único chip, surgindo
o microprocessador.

A Intel teve importante papel no desenvolvimento dos microprocessadores, tendo


iniciado com o modelo Intel produzido em . Este processador era um
dispositivo de bits que continha uma (Aritmetic and Logic Unit), uma unidade de
controle e alguns registradores. O marco importante foi o lançamento em , do seu
primeiro processador de bits, o .

Introdução à Programação: Conceitos e Práticas. Página 21


Logo em seguida, a lança em o que se tornou padrão para
microcomputadores. O respaldo da foi essencial para a aceitação e o crescimento
deste equipamento. Uma série de fabricantes passou a produzir componentes para
esta arquitetura, enquanto que a própria se manteve na linha dos computadores
de grande porte, os .

Os meios de armazenamentos tais como unidades de disco e fita ocupavam espaços


consideráveis nos (Centro de Processamento de Dados), tornando assim um
desafio produzir uma unidade que fosse compatível com uso pessoal. Neste ponto, as
unidades de discos flexíveis (floppy disk) foram fundamentais para a viabilização dos
PC´s.

Os sistemas operacionais incorporavam cada vez mais facilidades de gerenciamento


dos recursos computacionais, tais como , memória, processos e arquivos. Os
sistemas operacionais e tiveram seu período de consolidação, sendo o
no ambiente Intel e o basicamente nas . A
iniciou sua trajetória com o , e esta plataforma estimulou o surgimento de um novo
mercado de desenvolvimento de softwares.

A necessidade de compartilhar informações e itens de impulsionou o


surgimento das redes de computador e do processamento distribuído. E os sistemas
operacionais tiveram que ampliar suas capacidades para incluir o gerenciamento de
mais estes recursos, fazendo que toda esta complexidade fosse transparente ao
usuário.

O nível de integração dos circuitos integrados alavancou arquiteturas de computadores


com múltiplos processadores permitindo o processamento paralelo.

Em resumo, a quarta geração se caracterizou por:

 uso de circuitos integrados em escala muito grande ( );


 compactação em alta densidade (computadores menores);
 Linguagens de alto nível que manuseiam tanto dados escalares como vetores;
 sistema operacionais em Time-sharing e Memória Virtual;
 sistemas com multiprocessadores;
 Processadores paralelos;
 aparecimento dos microprocessadores;
 uso de disquetes (floppy disk) como meio de armazenamento;

Algumas literaturas qualificam o estágio atual como sendo , principalmente


pelo forte emprego da tecnologia de . Porém, é difícil classificar
uma geração fazendo parte dela, pois o avanço tecnológico atual traz inovações a todo
o momento. Por exemplo, poderíamos qualificar como marco extraordinário a abertura
da internet e o surgimento da e do primeiro browser em , por
Tim Berners-Lee. O interessante é que seria uma geração caracterizada
principalmente por criações de .

Introdução à Programação: Conceitos e Práticas. Página 22


O quadro a seguir destaca os principais elementos de e de que
caracterizaram as gerações:

Sistemas Operacionais
Compiladores Inteligentes
Software Linguagem de Máquina Linguagem Assembly Linguagem de Alto Nível Circuitos Integrados VLSI
Hardware Válvulas e Relés Diodo e Transistor Circuitos Integrados Microprocessadores

1ª geração 2ª geração 3ª geração 4ª geração

1935 1945 1955 1965 1975

Figura 23: Principais características de Software e Hardware por geração.

Introdução à Programação: Conceitos e Práticas. Página 23


2. Sistemas de Numeração

Há cerca de anos o homem começou a ter necessidade de contar (agricultura


e pastoreio), onde os pastores de ovelhas tinham necessidades de controlar os
rebanhos. Precisavam saber se não faltavam ovelhas. (III)
Alguns vestígios indicam que os pastores faziam o controle de seu rebanho usando
conjuntos de pedras. Ao soltar as ovelhas, o pastor separava uma pedra para cada
animal que passava e guardava o monte de pedras.
Quando os animais voltavam, o pastor retirava do monte uma pedra para cada
ovelha que passava. Se sobrassem pedras, ficaria sabendo que havia perdido
ovelhas. Se faltassem pedras, saberia que o rebanho havia aumentado. Desta forma
mantinha tudo sob controle.
Uma ligação do tipo: para cada ovelha, uma pedra chama-se, em Matemática,
correspondência um a um. Provavelmente passou a utilizar o dedo como apoio à
contagem, sem, no entanto poder guardar a informação.
O processo de contagem evoluiu para o agrupamento quando tratava de grandes
quantidades, p. ex: João fez pontos ( pontos).

2.1. Sistema de Numeração Egípcio

Essa ideia de agrupar marcas foi utilizada nos sistemas mais antigos de numeração.
Os egípcios da antiguidade criaram um sistema muito interessante para escrever
números, baseado em agrupamentos.
 era representado por uma marca que se parecia com um bastão |
 por duas marcas ||
 E assim por diante.

Símbolo descrição valor


bastão

calcanhar
rolo de corda

flor de lótus

dedo apontando
peixe

homem

Introdução à Programação: Conceitos e Práticas. Página 24


Os números eram compostos pela associação destes símbolos e a quantidade
representada era obtida pela soma dos valores individuais. Por exemplo, é
representado por:

2.2. Sistema de Numeração Romano

O sistema de numeração romano possui um conjunto símbolos, conforme tabela


abaixo, sendo que um número é representado pela combinação destes símbolos e o
valor é a soma de cada uma das parcelas.

A fim de evitar representações muito extensas, um determinado valor pode ser


representado a partir de um símbolo de referência acompanhado de símbolos a
esquerda (subtração do valor principal) ou a direita (soma ao valor principal). Por
exemplo, ( )e ( – ).

Este sistema é bastante limitado para aplicações da matemática que vão além da
representação de números, onde mesmo as operações básicas são difíceis de
serem efetuadas.

2.3. Sistema de Numeração Maia

A cultura teve destaque nos últimos anos principalmente pelas questões


relativas ao calendário e o suposto fim dos tempos. Porém, os também
dispunham de um sistema de numeração bem estruturado que se caracterizava
como posicional, vigesimal e símbolo para o . Os valores dos símbolos vão de
a , sendo eles:

● ● ● ●

●● ●● ●● ●●

●●● ●●● ●●● ●●●

●●●● ●●●● ●●●● ●●●●

Introdução à Programação: Conceitos e Práticas. Página 25


Os números são estruturados pela combinação destes símbolos, sendo cada
símbolo contribuindo com o seu valor multiplicado por elevado a sua posição. A
soma destas parcelas fornece o valor do número. Por exemplo, o número
, que no sistema fica:

𝟐 ●

𝟗 ●●●●

Este sistema permite a execução das operações básicas com a mesma facilidade
que fazemos no nosso sistema decimal. Vejamos o exemplo que segue:

A coluna indica a operação desejada. As colunas e indicam a decomposição


do e no sistema vigesimal. As
colunas e contém o e no sistema . A coluna indica a soma
provisória que se obtém juntando as parcelas. Na sequência vêm os ajustes
necessários, transferindo os excedentes a , ou seja, cada unidades excedentes
em uma casa equivale a uma unidade na casa superior. Como o símbolo
equivale a ●●●●●, então na coluna são transferidas unidades ( e ●)
da posição para a posição . Isto faz surgir um símbolo ● a mais na posição
. Na coluna tem-se os ●●●●● da posição substituídos por um . Na
coluna tem-se a notação convertida para uma expressão convencional e o
resultado da soma destas parcelas é indicado na coluna , tal como se esperava
da soma .

2.4. Sistema de Numeração Decimal

Os sistemas numéricos egípcio, romano e maia apresentados são apenas parte dos
muitos sistemas adotados por diferentes civilizações da antiguidade, tais como os
babilônios, gregos, chineses, hindus entre outros. Cabe destacar, que com exceção
dos maias, que habitavam a América, as civilizações da Europa, Oriente e Oriente

Introdução à Programação: Conceitos e Práticas. Página 26


Médio mantinham um sistema de troca de mercadorias e comumente impérios que
se sucediam, onde um dos efeitos era a difusão de conhecimentos.
A ciência dos gregos atingiu grande desenvolvimento no século com
Euclides, cuja obra sobre Geometria (Os Elementos) é importante até hoje no ensino
dessa matemática. Os gregos tinham seu próprio sistema de numeração, com base
, utilizando letras para representar os números, o que não facilitava os cálculos.
Os romanos, que expandiram seus domínios a partir do século ., foram aos
poucos ocupando o espaço do império deixado por Alexandre, ao mesmo tempo que
foram assimilando parte da ciência grega. O interesse dos romanos foi pelas
aplicações práticas na engenharia (construção de estradas e aquedutos) e na
medicina, com pouca contribuição na matemática.
As invasões bárbaras, nos séculos e , colocaram fim ao Império Romano
no ocidente e conduziu a Europa a um período de trevas no campo da Ciência.
Entretanto, enquanto o Império Romano declinava, uma grande civilização florescia
no Oriente, no vale do rio Indo, entre as regiões que atualmente constituem o
Paquistão e a Índia.
É importante destacar que os sistemas de numeração egípcio e romano não eram
propícios para efetuar cálculos. As dificuldades destes sistemas foram superadas
pelos hindus, que desenvolveram as bases do moderno sistema de numeração. Eles
souberam reunir três características que estavam dispersos em outros sistemas
numéricos da antiguidade, a saber:
 Decimal: uso de dez símbolos para representar os dígitos de zero a nove (o
egípcio, o romano e o chinês também o eram);
 Posicional: os símbolos assumem diferentes valores de acordo com a posição
que ocupa na formação do número (o babilônio e o maia também eram);
 Presença do Zero: Foi um importante avanço a adoção de um símbolo para o
nada.
Estas três características, reunidas, tornaram o sistema de numeração hindu o mais
prático de todos. Não é sem motivo que hoje ele é usado quase no mundo todo.
Enquanto os hindus, que habitavam o vale do rio Indo, desenvolviam seu sistema de
numeração, grandes acontecimentos tiveram início na Península Arábica.
No século floresce o Islamismo, e estabelece um grande império que abrangia a
região do rio Indo, a leste, o norte da África e a Península Ibérica.
A expansão Islâmica através da conquista veio na sequência de grandes impérios,
tais como o babilônio, persa, grego (macedônio) e romano. A matemática era uma
das áreas de interesse dos estudiosos árabes e o contato com o saber oriental e
toda a história de conhecimento dos impérios que o antecedeu permitiu um salto
importante na ciência.
Um grande passo foi dado na direção do extremo oriente, quando os árabes
entraram em contato com a cultura hindu e logo manifestaram particular interesse
pelo seu sistema numérico, reconhecendo sua simplicidade e praticidade.
Em outra frente, os árabes haviam ocupado parte da Península Ibérica, o que serviu
de ponte para a ciência oriental ser introduzida na Europa medieval. A expansão
árabe resultou também no surgimento, entre os séculos e , de várias

Introdução à Programação: Conceitos e Práticas. Página 27


universidades e bibliotecas, desde Bagdá, no atual Iraque, até Granada e Córdoba,
na atual Espanha.
Um marco relevante na tentativa de introduzir o sistema hindu-arábico na Europa foi
a publicação de Leonardo de Pisa (também conhecido por ), intitulada
(O Livro do Ábaco). O pai de Fibonacci era dono de casas de comércio
ultramarino, sendo uma no norte da África. Ainda jovem Fibonacci mudou-se para
esta região e conviveu com esta nova matemática e aprofundou seus
conhecimentos no sistema árabe de numerais e cálculo. Ao retornar a Pisa,
Leonardo deu sequência a seus estudos e publicou pela primeira vez em o
destacado livro, cujo conteúdo introduzia a notação árabe, as operações
fundamentais com números inteiros, cálculo envolvendo séries, proporções, raízes
quadradas e cúbicas, e uma breve abordagem sobre geometria e álgebra. Neste
livro foi apresentado o problema de crescimento da população de coelhos de acordo
com uma série que levou o nome de . As páginas do livro
tornavam difíceis a reprodução de exemplares dado que este processo era
totalmente manual, o que limitou sua difusão.IV
O início da retomada, em , do território ibérico dos mouros pelo rei da Espanha,
permitiu que populações árabes passassem ao domínio europeu, facilitando a
absorção do conhecimento escrito em árabe, principalmente os trabalhos de
.
Entretanto, foram necessários vários séculos para que o sistema hindu-arábico fosse
aceito de fato na Europa. O intervalo entre a queda do império romano e a era dos
descobrimentos se caracterizou como período das trevas em função da pouca
produção de conhecimento. Não foi diferente em relação ao sistema de
numeraçãohindu e da nova forma de realizar as operações aritméticas, que
enfrentou resistência frente aos métodos baseados no ábaco, herança dos romanos.
Uma parte do espaço deixado pela queda do império romano no ocidente no século
foi ocupada pela Igreja, que praticamente deu continuidade a utilização do sistema
numérico criado pelos romanos. A chegada dos árabes à Europa no século
trouxe consigo o sistema de numeração hindu, que certamente em algum momento
iria levar à confrontação de sistemas.
Basta efetuar algumas operações aritméticas utilizando algarismos romanos, para
perceber as inegáveis vantagens que o sistema numérico hindu-arábico tem sobre o
sistema romano. Mesmo assim, o novo sistema de numeração acabou prevalecendo
na Europa somente no século .
Atualmente quando comparamos as representações dos dígitos numéricos em árabe
e a forma ocidental observamos uma diferença sensível, e muitas vezes não
atentamos que possuem a mesma raiz. Porém, as grandes distâncias entre o
Oriente Médio, Península Ibérica e o extremo oriente combinadas com os séculos de
história fizeram que sequências de pequenas alterações na forma de representar os
dígitos resultassem em diferenças significativas na configuração final. Até o século
todo o material produzido era escrito a mão, e diferentes copistas possuíam
diferentes caligrafias que associadas á direção da escrita (direita para a esquerda ou
esquerda para a direita) contribuíram para as sucessivas alterações nas aparências
dos dígitos numéricos.

Introdução à Programação: Conceitos e Práticas. Página 28


A invenção da imprensa por Gutenberg no século permitiu que tanto as letras
quanto os símbolos utilizados para representar os dígitos numéricos adquirissem
uma forma mais estável e definitiva.
A seguinte tabela ilustra as modificações sofridas na forma de representação dos
dígitos utilizados no sistema de numeração criado pelos hindus, adotado pelos
árabes e passado aos europeus:

Por volta do século , os hindus


representavam os algarismos assim, ainda
sem um símbolo para o nada:
No século , já com o , a
representação evoluiu para:

No século os hindus representavam os


dez dígitos assim:

No mesmo século , os árabes que


estavam no Ocidente representaram assim:

No século os árabes orientais


empregavam esta representação:

As formas usadas pelos europeus nos


séculos e :

A representação ocidental atual: 1 2 3 4 5 6 7 8 9 0

A representação árabe escrita da esquerda


para a direita: ۱۲۳٤٥٦٧٨٩۰

Introdução à Programação: Conceitos e Práticas. Página 29


3. Conceito de Bases

Em nosso cotidiano lidamos com números representados na base decimal. Isto se


deve a razões e históricas, talvez fundamentadas na quantidade de dedos nas mãos.
Porém, os computadores consolidaram o sistema binário como elemento de referência
no projeto dos circuitos. A utilização da base proporciona grande facilidade na
representação eletrônica e digital dos números e . A possibilidade de representar os
dígitos binários através de valores de tensões ou correntes, por exemplo, e ,
simplifica a construção dos processadores e demais elementos de uma arquitetura.
Combinado com este fato há toda uma álgebra desenvolvida no contexto da
informação binária/digital, que inclui uma série de operações sobre dados
binários/lógicos que serviu de base para a construção das principais funcionalidades
computacionais (memória, circuitos aritméticos, ...).

Um sistema de numeração é determinado fundamentalmente pela base, que é o


número de símbolos utilizados.

Na notação posicional os dígitos assumem valores diferentes para cada posição que
ocupa na representação do número.

Tomando o ponto como separador para a parte inteira e a parte fracionária, temos
que a cada posição que se desloca o dígito a esquerda do ponto o seu valor é
multiplicado cumulativamente pela base. Da mesma forma, a cada posição que se
desloca o dígito a direita do ponto o seu valor é dividido cumulativamente pela base.

3.1. Sistema Decimal

O sistema decimal é o mais comumente utilizado nas operações do dia a dia. O


conjunto de dígitos utilizado é:

{ } → Base → Símbolos.

Por exemplo, partindo do número decimal , observa-se que cada dígito


possui um valor diferente dependendo da posição que ocupa no número, a saber:

Posição 2 Posição 1 Posição 0 Posição -1

Esta característica é um dos pilares do sistema decimal utilizado, dando-lhe o aspecto


, que atribui a cada posição do número um peso distinto. Decompondo
em uma somatória, levando em conta o peso da posição, tem-se:

Introdução à Programação: Conceitos e Práticas. Página 30


As parcelas
mostram claramente, através da sua forma geral
, a contribuição de cada elemento do
sistema de numeração utilizado.

EXERCÍCIOS: Representar na forma de polinômio os seguintes números na base


:

i.
ii.
iii.

3.2. Sistema Hexadecimal

O sistema hexadecimal tem todas as características do sistema decimal, porém utiliza


símbolos para a representação dos dígitos. Logo:

{ } → Base → Símbolos.

Os símbolos utilizados são os do sistema decimal, acrescidos dos seguintes


símbolos, com seus respectivos valores:

Valor
Símbolo
Absoluto

EXERCÍCIOS: Representar na forma de polinômio os seguintes números na base


:

i.
ii.
iii.

Introdução à Programação: Conceitos e Práticas. Página 31


3.3. Sistema Binário

O sistema binário também tem todas as características do sistema decimal porém


utilizando dois dígitos:

{ } → Base → Símbolos.

É o sistema de numeração utilizado internamente pelos computadores modernos. Em


informática, cada dígito de um número representado neste sistema é denominado de
bit (binary digit). Além disso, alguns conjuntos destes bits recebem denominação
específica, a saber:

 = conjunto de (exemplo: );
 ( ) = conjunto de , ou mais bytes, que determina a
capacidade de processamento do computador (exemplo: palavra =
bits, palavra = , Arquitetura Pentium = );
 = conjunto de (KBytes);
 = conjunto de (MBytes);
 = conjunto de (GBytes);

De acordo com a norma : Quantities and units – Part 13: Information


science and technology, de , foram estabelecidos os seguintes prefixos:

Potência de 10 Potência de 2
Potência Valor
Nome Símbolo Nome Símbolo
quilo k kibi Ki 1024
mega M mebi Mi 1 048 576
giga G gibi Gi 1 073 741 824
tera T tebi Ti 1 099 511 627 776
peta P pebi Pi 1 125 899 906 842 624
exa E exbi Ei 1 152 921 504 606 846 976
zetta Z zebi Zi 1 180 591 620 717 411 303 424
yotta Y yobi Yi 1 208 925 819 614 629 174 706 176

Apesar de diferentes, é comum utilizar os termos das potências de para se referir


às potências de .

EXERCÍCIOS: Representar na forma de polinômio os seguintes números na base :

i.
ii.
iii.

Introdução à Programação: Conceitos e Práticas. Página 32


3.4. Base Qualquer

É possível generalizar os conceitos anteriores para números em qualquer base,


bastando selecionar a quantidade de símbolos adequada, e seguir os princípios do
sistema de numeração, cuja essência é a . Assim, uma base
qualquer, pode ser estabelecida da seguinte forma:

{ } → Base → Símbolos, onde o último símbolo


representa o valor .

Denotando um número na base com seus dígitos, temos:

sendo a parte inteira com dígitos e a parte fracionária com dígitos, sendo que
. Expandindo na forma de polinômio, tem-se:

Ou, colocando este sob a forma de


somatório, tem-se:

Desta forma, temos todos os elementos para formar um sistema em qualquer base:
os dígitos a base e as posições em . Por exemplo, para a , teria:

{ } → Base → Símbolos/Dígitos.

Uma determinada quantidade pode ser representada por diferentes números em


qualquer base. Quanto menor a base mais dígitos são necessários para representar
a mesma quantidade.

3.5. Conversão entre Bases

Uma determinada quantidade pode ser representada de forma distinta dependendo


da base utilizada. Por exemplo, a quantidade pode ser representada na base
como , enquanto que em outras bases sua representação será diferente.
Mais especificamente, no mundo computacional é muito comum sermos requisitados
a interpretar informações codificadas em binário ( ). Porém, estarmos
habituados ao sistema decimal o que frequentemente demandará o uso de metodos
de conversão nos dois sentidos.

Introdução à Programação: Conceitos e Práticas. Página 33


Além disso, é fundamental que um profissional da área de computação ou
engenharia relacionada, compreenda as transformações aplicadas a uma
informação em todo o seu trajeto, desde a forma natural no mundo real até a
completa representação no ambiente digital, tanto por software quanto pelo
hardware.

Desta forma, serão apresentados métodos para realizar as seguintes conversões de


quantidades:

i.
ii.
iii.

3.5.1. Conversão de Qualquer Base para a Base 10

Para converter de uma base qualquer para a base , basta aplicar o


de um número na base :
∑ , e efetuar as operações tal como estamos habituados na base , e
o resultado obtido será nesta base .

Exemplos:

O número com as posições dos dígitos: ( )

O número com as posições dos dígitos: ( )

O número com as posições dos dígitos: ( )

É importante conhecer os limites estabelecidos em termos de quantidade de dígitos em


uma determinada base. Por exemplo, sabemos que com três dígitos decimais é
possível representar quantidades distintas, que vão do número ao número
. Depreende-se que a maior quantidade representável é quando todas as posições
forem ocupadas pelo dígito de maior valor. Assim, para o sistema binário e para o
tamanho de um , a maior quantidade possível de ser representada é obtida
quando todos os bits forem iguais a . A questão é: “Quanto é este valor em decimal?”.
A resposta vem da conversão deste binário para decimal, conforme segue:

Introdução à Programação: Conceitos e Práticas. Página 34


De uma forma geral, o equivalente decimal do maior número possível de ser
representado por bits é obtido através da fórmula:

Observa-se que temos o somatório de uma Progressão Geométrica de termos:

Para o caso em que (1 ) temos:

Introdução à Programação: Conceitos e Práticas. Página 35


3.5.2. Conversão da Base 10 para Qualquer Base

A conversão da base para uma base qualquer é feita de forma distinta para a
Parte inteira e para a Parte fracionária:

i. Parte inteira:

 Divide-se a parte inteira pela base desejada, obtendo-se assim um


quociente e um resto. Repita esta divisão com o quociente, e assim
sucessivamente com os demais quocientes até obter quociente zero.
Agrupe os restos, conforme exemplo a seguir, para obter o número
convertido para a base desejada, sabendo que o primeiro resto é o dígito
menos significativo;
Exemplos:

47 2 33 2
1 23 2 1 16 2
1 11 2 0 8 2
1 5 2 0 4 2
1 2 2 0 2 2
0 1 2 0 1 2
1 0 1 0

O dígito menos significativo de um número inteiro é aquele que ocupa a posição ,


sendo que o dígito mais significativo é aquele que ocupa a posição de maior ordem
. Quando se trata de um número binário, os termos utilizado são
e , ou seus equivalentes em inglês:
e . Também podem ser
encontrados os termos bit de mais baixa ou alta ordem.

235 8 2030 16
3 29 8 14 126 16
5 3 8 14 7 16
3 0 7 0

Introdução à Programação: Conceitos e Práticas. Página 36


Vamos comprovar este processo utilizando a forma polinomial de representação do
número inteiro.

Efetuando a divisão deste polinômio por , temos:

Esta primeira divisão de inteiros produziu como resto o dígito . O quociente obtido
foi , que é um polinômio que representa um número
composto pelos demais dígitos, deslocados uma posição à direita
. De fato, a divisão por faz com que todos os dígitos sejam
deslocados uma posição à direita e elimina o dígito mais a direita. Assim, a lógica de
fundo é uma sequência de deslocamentos à direita e a obtenção de um dígito
através do resto. O término deste processo ocorre quando o quociente é zerado.
Neste caso a próxima divisão seria:

ii. Parte fracionária

 Multiplica-se a parte fracionária pela base desejada, obtendo-se assim um


número composto por uma parte inteira e uma parte fracionária. Repita
este processo para esta parte fracionária, e assim sucessivamente até que
o resultado obtido seja um inteiro ou quando a quantidade de dígitos
desejada foi alcançada. Agrupe as partes inteiras, conforme exemplo a
seguir, para obter o número convertido para a base desejada:

Introdução à Programação: Conceitos e Práticas. Página 37


0.75 x 2 = 1.5 0.0378 x 16 = 0.6048
0.6048 x 16 = 9.6768
0.5 x 2 = 1.0
0.6768 x 16 = 10.8288
0.8288 x 16 = 13.2608
0.2608 x 16 = 4.1728
0.1728 x 16 = 2.7648
0.7648 x 16 = 12.2368
0.2368 x 16 = 3.7888
0.7888 x 16 = 12.6208
0.6208 x 16 = 9.9328
0.9328 x 16 = 14.9248
0.924805 x 16 = 14.7969
0.796875 x 16 = 12.7500
0.75 x 16 = 12.0000

0.15 x2= 0.3000 0.44 x8= 3.5200


0. x2= 0.6000 0.52 x8= 4.1600
0.60 x2= 1.2000 0.16 x8= 1.2800
0.20 x2= 0.4000 0.28 x8= 2.2400
0.40 x2= 0.8000 0.24 x8= 1.9200
0.80 x2= 1.6000 0.92 x8= 7.3600
0.60 x2= 1.2000 0.36 x8= 2.8800
0.20 x2= 0.4000 0.88 x8= 7.0400
0.40 x2= 0.8000 0.04 x8= 0.3200
0.80 x2= 1.6000 0.32 x8= 2.5600

Observe a dízima

Podemos verificar facilmente este método de conversão partindo da expansão do


número na forma de polinômio.

Considerando somente a parte fracionária, temos:

Introdução à Programação: Conceitos e Práticas. Página 38


Multiplicando o polinômio pela base B, temos:

Podemos interpretar através da parcela que o dígito foi deslocado para


a posição , ou seja, foi deslocado para assumir uma posição na parte inteira. De
fato, a multiplicação por faz com que todos os dígitos sejam deslocados uma
posição à esquerda. Logo, a lógica de fundo é uma sequência de deslocamentos à
esquerda e a obtenção de um dígito por vez. Aplica-se novamente este processo
para a nova parte fracionária . O término deste ciclo ocorre
quando a parte fracionária é zerada , ou a quantidade de dígitos obtida já
alcançou a precisão estabelecida.

3.6. Exercícios

a) Converter para a base os seguintes números que estão na base


informada:

i.
ii.
iii.
iv.
v.
vi.

b) Converter para base informada os seguintes números que estão na base


:

i.
ii.
iii.
iv.

3.6.1. Conversão da Base 2n para a Base 2

A conversão de uma base com potência inteira de ( ) para a base pode ser
feita através do uso de uma tabela.

Sem a tabela, a conversão demandaria as seguintes transformações:

Introdução à Programação: Conceitos e Práticas. Página 39


O uso da tabela evita os cálculos das duas conversões. Esta tabela de duas colunas
pode ser facilmente montada, como segue:

i. Relacionar na primeira coluna os dígitos da base


ii. Escrever na segunda coluna o equivalente binário do valor de cada
dígito utilizando ;

3.6.2. Conversão da Base 4 para a Base 2

A tabela para a conversão da Base para a Base fica:

Ao converter um número na base para a base , basta substituir cada dígito pela
respectiva combinação de bits.

Ao converter um número na base para a base , primeiro ajusta-se a quantidade


de bits da parte inteira e parte fracionária para uma quantidade múltipla de , e em
seguida substitui-se cada agrupamento de pelo equivalente dígito da base .
Este ajuste não pode alterar o valor do número. Assim, quando necessário a parte
inteira é complementada com zeros a esquerda, e a parte fracionária é ajustada com
zeros a direita.

Exemplo:

Os agrupamentos e os complementos estão indicados em negrito.

3.6.3. Conversão da Base 8 para a Base 2

A tabela para a conversão da Base para a Base fica:

Introdução à Programação: Conceitos e Práticas. Página 40


Ao converter um número na base para a base , basta substituir cada dígito pela
respectiva combinação de bits.

Ao converter um número na base para a base , tomar os mesmos cuidados


anteriores e ajustar a quantidade de bits para um número múltiplo de .

Exemplo:

Os agrupamentos e os complementos estão indicados em negrito.

3.6.4. Conversão da Base 16 para a Base 2

A tabela para a conversão da Base para a Base fica:

Ao converter um número na base para a base , basta substituir os dígitos pela


respectiva combinação de bits.

Ao converter um número na base para a base , tomar os mesmos cuidados


anteriores e ajustar a quantidade de bits para um número múltiplo de .

Exemplo:

Introdução à Programação: Conceitos e Práticas. Página 41


A Conversão entre base e base , por exemplo, pode ser feita pela aplicação de
duas tabelas, ou seja:

3.7. Exercícios

1. Converter para a base os seguintes números que estão na base


informada:

i.
ii.
iii.
iv.
v.
vi.
vii.
viii.
ix.
x.
xi.
xii.
xiii.

2. Converter para a base informada os seguintes números que estão na base


:

i.
ii.
iii.
iv.
v.

3. Converter os seguintes números:

i.
ii.

Introdução à Programação: Conceitos e Práticas. Página 42


4. Operações com Números Binários

O conjunto de dígitos binários combinado com as operações aritméticas dão forma


ao sistema binário, cuja praticidade serviu de base para o surgimento da
e dos circuitos digitais. O seguinte diagrama ilustra as
contribuições fundamentais para o desenvolvimento da tecnologia digital.

Figura 24: Circuitos Digitais: Contribuições

4.1. Operações Aritméticas

Na sequência são apresentadas as quatro operações aritméticas aplicadas a


números binários. O entendimento dos detalhes relativos a estas operações
contribui para uma melhor compreensão do funcionamento dos computadores, uma
vez que a aritmética binária é a base para a lógica digital.

4.1.1. Soma

A soma de números binários é simplificada em relação à soma decimal, uma vez


que é necessário conhecer as somas possíveis para dois dígitos binários. Porém se
o resultado da soma de dois bits não couber em um bit, é produzido o bit de .
Este é o caso da soma de com , que resulta em com . Pode ocorrer que
somando com surja o bit da soma anterior, produzindo . Neste
caso, o resultado em binário é , ou em decimal. A interpretação do é que o
resultado é e ocorre o . Assim, têm-se as seguintes combinações:





Exemplos:

11 1 1 11111111 111111 1 11
111010101 469 11111111 255 10111100 188 10011001 153

Introdução à Programação: Conceitos e Práticas. Página 43


+ 10010100 148 + 1 1 + 1111111 127 + 10110100 180
---------- --- ---------- --- ---------- --- ---------- ---
1001101001 617 100000000 256 100111011 315 101001101 333

4.1.2. Subtração

A subtração de números binários é semelhante à soma binária, com atenção


especial quando é necessário o empréstimo para habilitar a subtração. Assim, têm-
se as seguintes combinações:

 –
 –
 –
 –

2
-1 2 -1-1
0 0
- 1 - 1
--- ---
1 0

Neste caso – não é possível, Neste caso – , tendo ainda que


então empresta 1 da conta futura. pagar um empréstimo anterior não
Este empréstimo vale 2, o que gera é possível, então empresta 1 da
, produzindo 1 como conta futura. Este empréstimo vale
resultado. 2, o que gera ,
produzindo 0 como resultado.

Exemplos:

222222 2 2
11111111 *******2 **2**2
111010101 469 11111111 255 10111100 188 10110100 180
- 10010100 148 - 1 1 - 1111111 127 - 10011001 153
---------- --- ---------- --- ---------- --- ---------- ---
101000001 321 11111110 254 00111101 61 00011011 27

O símbolo indica que houve um empréstimo para viabilizar a subtração.

4.1.3. Multiplicação

O produto de dois números binários também fica simplificado uma vez que a tabela
de multiplicação dos dígitos se reduz a quatro combinações. A composição das
parcelas é feita através da soma. As combinações possíveis são:


Introdução à Programação: Conceitos e Práticas. Página 44



Exemplos:

1101 13 11010 26 1001 9 10111 23


x 11 3 x 10 2 x 101 5 x 100 4
------- --- ------- --- ------- --- ------- ---
1101 00000 1001 00000
1101 11010 0000 00000
-------- --- -------- --- 1001 10111
100111 39 110100 52 -------- --- -------- ---
101101 45 1011100 92

Estes exemplos permitem constatar que a multiplicação binária é a soma de


parcelas oriundas da multiplicação de um dígito do primeiro operando pelo segundo
operando. Como o dígito será ou , logo as parcelas serão compostas ou por zero
ou pelo segundo operando. Além disso, observa-se o deslocamento dos bits a cada
parcela. Estas características reforça a simplificação proporcionada pela notação
binária.

4.1.4. Divisão

A divisão entre binários, apesar de não ser tão direta quanto à multiplicação,
também é simplificada, uma vez que o quociente final é montado a partir de
quocientes parciais que vão agregando e . Além disso, cada dígito do
quociente inicia uma subtração para completar o cálculo. Desta forma, a divisão é
executada através de uma série de subtrações. Observe o seguinte exemplo:

-1+2
1 0 1 1 0 1 1 22 3
- 1 1 1 1 1 - 21 7
1 0 1 quociente 1 quociente
- 1 1 resto
0 1 0 0
- 1 1
0 0 0 1
resto
O quadro da esquerda apresenta o processo de divisão binária e o quadro da direita
mostra os equivalentes decimais.

A divisão de por inicia pelo agrupamento da esquerda para a direita de


uma quantidade mínima de que seja divisível por , que no caso seriam três
bits: ̅̅̅̅̅ .

10110 11
11 1
10

Introdução à Programação: Conceitos e Práticas. Página 45


A divisão prossegue abaixando o próximo dígito e verificando se o grupo é maior ou
igual ao divisor. Não sendo, é abaixado o próximo dígito e adicionado o bit ao
quociente. Segue outro exemplo:

101101 101
101 1001
000101
101
0

O resultado pode ser certificado pelo equivalente decimal, onde neste caso é
dividido por , resultando em quociente e resto .

4.1.5. Exercícios

1. Somar os seguintes números em binário e hexadecimal:

i.
ii.
iii.
iv.
v.
vi.
vii.

2. Subtrair os seguintes números binários:

i. –
ii. –
iii. –

3. Multiplicar os seguintes números binários:

i.
ii.
iii.

4. Dividir os seguintes números binários:

i.
ii.
iii.

Introdução à Programação: Conceitos e Práticas. Página 46


4.2. Operações Lógicas

A ou combina uma série de operações, denominadas operações


lógicas, aplicáveis ao conjunto de números { } produzindo como resultado ou .
Constitui-se em um dos fundamentos mais importantes na construção de circuitos
digitais e consequentemente da arquitetura de um computador.

As operações lógicas podem ser implementadas em circuitos, denominados portas


lógicas, que por sua vez podem ser combinados na construção de circuitos mais
complexos, como circuito de soma, subtração dentre outros.

Além disso, o conjunto { }, também pode ser mapeado como { },


{ }, { }, { } ...

Na sequência a descrição das operações lógicas.

4.2.1. NOT (NÃO)

A operação requer apenas um argumento e produz um resultado. Esta


operação retorna o complemento de (negação ou inversão) do bit de argumento.

Circuito Equivalente

A lâmpada acenderá ( ) quando


a chave estiver aberta ( ).
Circuito Eletrônico 𝑉𝑐𝑐

𝑋 𝐴

Introdução à Programação: Conceitos e Práticas. Página 47


Símbolo para o Circuito Lógico

Representação da Operação

Tabela Verdade
NOT 0 1
1 0

Introdução à Programação: Conceitos e Práticas. Página 48


4.2.2. AND (E)

A operação requer dois argumentos e produz um resultado. O resultado


será quando as duas entradas forem , e para outras combinações.

Circuito Equivalente

A lâmpada acenderá ( ) quando as


chaves e forem fechadas ( ).
Circuito Eletrônico 𝑉𝑐𝑐

𝑋
𝑋

Símbolo para o Circuito Lógico

Representação da Operação

Introdução à Programação: Conceitos e Práticas. Página 49


Tabela Verdade
AND 0 1
0 0 0
1 0 1

Característica da Operação

A representa todas as combinações da expressão lógica ,


onde os valores de , e estão representados nas seguintes células:

Nesta representação podemos constatar que terá valor somente quando os


valores de e também forem . As características da operação podem ser
extraídas da tabela, observando o comportamento de . Na operação , quando
tem valor , a linha correspondente em resulta inteira em , o que permite
deduzir a característica . Esta expressão pode ser lida como: independente
do valor do bit , quando fazemos um com , o resultado será sempre .
Assim, o operador tem a característica de ser utilizado sempre que se precisa
anular ( , , , ...) bits.

Uma outra característica pode ser extraída analisando a linha de correspondente


ao valor de igual . Neste caso, esta segunda linha de é idêntica à linha do
argumento , o que permite deduzir a expressão , ou seja: independente do
valor do bit , quando fazemos um com , o resultado será sempre o valor do
próprio bit ( ). Observamos que a operação se assemelha à operação
aritmética de multiplicação, o que lhe confere também a denominação de produto
lógico.

Em muitas situações a tabela verdade pode ser construída no seguinte formato:

AND
A B X
0 0 0
0 1 0
1 0 0
1 1 1

A coluna expressa os resultados da operação aplicada linha a linha para


cada valor das colunas e . A tabela possui quatro linhas ( ) em virtude de
estarmos trabalhando com a expressão lógica contendo dois ( )
argumentos ( e ). Neste caso, as colunas e representam todas as

Introdução à Programação: Conceitos e Práticas. Página 50


combinações de e possíveis para duas variáveis. Em uma expressão lógica
envolvendo variáveis a tabela verdade conteria linhas, e assim por diante.

Exemplo: Supor que temos um número inteiro com : , e


desejamos obter um composto desta forma: , ou seja com os
quatro bits altos iguais aos de e os quatro bits baixos zerados. Isto pode ser obtido
fazendo a operação de com um número bit a bit, tal que o resultado produza
a configuração desejada, ou seja:

ou

O número utilizado como argumento é normalmente chamado de ( ).

Introdução à Programação: Conceitos e Práticas. Página 51


4.2.3. OR (OU)

A operação ( ) requer dois argumentos e produz um resultado. O resultado


será quando as duas entradas forem , e para outras combinações.

Circuito Equivalente

A lâmpada apagará ( ) quando as chaves


e forem abertas ( ).
Circuito Eletrônico 𝑉𝑐𝑐

𝑋 𝐴 𝐵
𝑋 ̅̅̅̅̅̅̅̅
𝐴 𝐵

𝐴 𝐵

Símbolo para o Circuito Lógico

Introdução à Programação: Conceitos e Práticas. Página 52


Representação da Operação

Tabela Verdade
OR 0 1
0 0 1
1 1 1

Característica da Operação

Analisando a da operação podemos extrair suas principais


características.

Na operação , a primeira linha de , quando é igual a , é idêntica à linha do


argumento , o que permite deduzir a expressão , ou seja: independente
do valor do bit , quando fazemos um com , o resultado será sempre o valor
do próprio bit ( ).

Quando tem valor , a linha correspondente em resulta inteira em , o que


permite deduzir a característica . Esta expressão pode ser lida como:
independente do valor do bit , quando fazemos um com , o resultado será
sempre . O operador tem a característica de ser utilizado quando que se
precisa ( , , ...) bits.

Exemplo: Supor que temos um número inteiro com : , e


desejamos obter um composto desta forma: , ou seja com os
quatro bits altos iguais aos de e os quatro bits baixos ligados. Isto pode ser obtido
fazendo a operação de com um número bit a bit, tal que o resultado produza a
configuração desejada, ou seja:

ou

Introdução à Programação: Conceitos e Práticas. Página 53


4.2.4. XOR / OU EXCLUSIVO

A operação ( ) requer dois argumentos e produz um resultado. É


semelhante ao , porém quando os dois argumentos forem o resultado será . O
termo se refere ao fato que dois argumentos
produz o resultado .

Símbolo para o Circuito Lógico

Representação da Operação

Tabela Verdade
XOR 0 1
0 0 1
1 1 0

Característica da Operação
̅ , onde ̅ significa a negação de

Analisando a da operação podemos extrair suas principais


características.

Na operação , a primeira linha de , quando é igual a , é idêntica à linha do


argumento , o que permite deduzir a expressão , ou seja: independente do
valor do bit , quando fazemos um com , o resultado será sempre o valor do
próprio bit ( ).

Quando tem valor , a linha correspondente em resulta em valores invertidos em


relação ao argumento , o que permite deduzir a característica ̅ . O operador
tem a característica de ser utilizado quando se quer inverter ( , ,
) o valor de um bit específico.

Introdução à Programação: Conceitos e Práticas. Página 54


Exemplo: Supor que temos um número inteiro com : , e
desejamos obter um composto desta forma: ̅̅̅̅̅̅̅̅̅̅̅̅̅, ou seja com os
quatro bits altos iguais aos de e os quatro bits baixos invertidos. Isto pode ser
obtido fazendo a operação de A com um número bit a bit, tal que o resultado
produza a configuração desejada, ou seja:

ou

A operação pode ser utilizada para realizar uma criptografia básica. Efetuando
, e em seguida calculando , produz-se novamente o valor de
.

4.2.5. Exemplos

i. Montar a tabela verdade para a seguinte expressão lógica . Desenhar o


circuito lógico equivalente.

A tabela deverá considerar os valores possíves para , e e o resultado . A


quantidade de linhas da tabela deve conter todas as combinações para os valores de
, e . Como cada uma destas variáveis admitem os valores , logo as três
variáveis produzirão ou linhas. Por conveniência podem ser adicionadas colunas
com valores intermediários o que confere maior clareza na obtenção da coluna com
o resultado final. Assim, a tabela final seria:

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

O circuito lógico equivalente é:

Introdução à Programação: Conceitos e Práticas. Página 55


ii. Montar a tabela verdade para a seguinte expressão lógica . Desenhar
o circuito lógico equivalente.

A tabela deverá considerar os valores possíves para e e o resultado . Serão


ou linhas. Assim, a tabela final seria:

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

Observe que o resultado de coincide com a tabela verdade do operador . De


fato a operação entre e equivale a expressão lógica .

O circuito lógico equivalente é:

Uma notação alternativa mais compacta utiliza um círculo na entrada da porta


principal para denotar a operação , conforme ilustra o seguinte diagrama:

4.2.6. Exercícios

Montar a tabela verdade e o circuito lógico para cada uma das expressões abaixo:

i.
ii.
iii.
iv.
v.

Introdução à Programação: Conceitos e Práticas. Página 56


vi.
vii.
viii.
ix.
x.
xi.
xii.
̅̅̅̅̅̅̅̅
xiii.
xiv.
xv.
xvi.
xvii.

4.3. Operações Aritméticas Binárias com Circuitos Lógicos

Este tópico faz a correlação entre as operações de soma e subtração de binários


com expressões lógicas e finalmente com circuitos lógicos. É importante destacar
que este material apresenta uma breve introdução aos conceitos de circuitos lógicos
e não se aprofunda nas questões relacionadas com redução de expressões.

4.3.1. Somador Completo

É possível relacionar a operação de soma aritmética com as operações lógicas.


Para isto, temos que montar uma tabela verdade completa para a soma de com
e , produzindo como resultado e um novo .

A tabela abaixo mostra todas as combinações para . A coluna contém


o resultado da soma e a coluna o resultante.

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

Apenas com os conhecimentos até o momento apresentados, é possível observar


que a coluna pode ser obtida por . Já pode ser obtido por
para as quatro primeiras linhas, quando ,e para as
quatro últimas, quando . Combinando as duas equações para , tem-se

Introdução à Programação: Conceitos e Práticas. Página 57


que também pode ser escrita como
. A seguinte tabela verdade mostra que o valor de é o mesmo para as
duas expressões e idêntico aos valores obtidos na tabela verdade da soma binária
completa:

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

Utilizando as portas lógicas é possível montar facilmente um circuito somador


completo, que receba e produza , como na figura abaixo:

Figura 25: Circuito Somador Completo de 1 bit.

Este circuito é capaz de realizar a soma de bit. Para somar um inteiro com mais
bits, basta sequenciar vários somadores completos. Por exemplo, se o projeto da
prevê a soma de dois inteiros de , então o circuito poderia ser similar á:

Introdução à Programação: Conceitos e Práticas. Página 58


Figura 26: Circuito da Soma de N bits.

4.3.2. Subtrator Completo

A abordagem feita para o somador completo pode ser aplicada para o subtrator
completo. Para isto, temos que montar uma Tabela Verdade completa para a
subtração de por com eventual pagamento do ,
produzindo como resultado e um novo indicador de .

A tabela abaixo mostra todas as combinações para . A coluna contém


o resultado da subtração e a coluna o resultante.

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

Por exemplo, as linhas e com os valores e para são


entendidas como sendo as seguintes operações:

2 2
-1 0 -1-1
0 0
- 1 - 1
--- ---
1 0

Neste caso – não é possível, Neste caso – , tendo ainda que


então empresta da conta futura. pagar um empréstimo anterior não
Este empréstimo vem somando , o é possível, então empresta da
que gera , produzindo conta futura. Este empréstimo vem
como resultado. somando , o que gera

Introdução à Programação: Conceitos e Práticas. Página 59


, produzindo como
resultado.

De modo similar à soma, é possível observar que a coluna pode ser obtida por
. Já pode ser obtido por para as quatro primeiras
linhas e para as quatro últimas. Combinando as duas equações para
, tem-se . Também é possível obter para
as quatro primeiras linhas e para as quatro últimas. Combinando as
duas equações para , tem-se ( ). Esta última expressão
pode ser trabalhada para reduzir o número de portas lógicas do circuito final e
aproveitar uma operação do cálculo de e chega-se a
( ).

Um circuito lógico para o subtrator completo seria:

Figura 27: Circuito Subtrator Completo de 1 bit.

Introdução à Programação: Conceitos e Práticas. Página 60


5. Representação de Dados

A figura abaixo ilustra a separação entre as informações que circulam no mundo real
e seus equivalentes no mundo digital. Estão exemplificadas informações clássicas,
tais como números e textos. Para entrar no mundo digital é fundamental que o dado
exterior seja convertido para e através de algum método ou convenção.

Figura 28: Mundo Real x Mundo Digital.

Os métodos aqui apresentados serão agrupados conforme segue:

Representação
de Dados

Números Textos

Inteiros Reais

Inteiro sem Inteiro com


sinal sinal

Módulo- Comple- Comple- Excesso de


Binário Puro
Sinal mento de 1 mento de 2 2n-1

Figura 29: Métodos de Representação de Dados

Na representação de dados no mundo digital é necessário definir uma quantidade de


bits disponíveis para o enquadramento do dado. No caso de números inteiros,

Introdução à Programação: Conceitos e Práticas. Página 61


normalmente as arquiteturas e as linguagens disponibilizam os tamanhos com
e . Para os exemplos que seguem adotaremos o tamanho de
, até que se informe o contrário.

5.1. Representação de Números Inteiros

Na sequência são apresentados os métodos mais referenciados para representação


de números inteiros. Inicialmente será considerado tamanho .

5.1.1. Inteiros Sem Sinal – Binário Puro

A representação de inteiros sem sinal é simplesmente a conversão do número


desejado para no tamanho estabelecido.

Exemplo Representação
00001010
01011101
01111111
00000000
11111111
100000000  Não foi possível representar em 8 bits.

Assim, a faixa de representação é dada por:

5.1.2. Inteiro Com Sinal

A representação de inteiros com sinal demanda algum artifício para codificar o sinal
no padrão digital. Na sequência são apresentados os quatro métodos mais
conhecidos.

5.1.2.1. Módulo Sinal

O método ( ) utiliza o bit mais significativo para codificar o sinal do


número e os demais bits codificam a magnitude do número em binário.

Introdução à Programação: Conceitos e Práticas. Página 62


Figura 30: Representação em Módulo-Sinal

Exemplo Representação
00001010
10001010
01111111
00000000
10000000
010000000  Não foi possível representar em 8 bits.
110000000  Não foi possível representar em 8 bits.
11111111
01100100

Assim, a faixa de representação é dada por:

5.1.2.2. Complemento de 1

O método consiste na conversão do valor absoluto


(magnitude) de para binário. Se o número for negativo, segue-se com a inversão
dos bits (toma-se o complemento do que falta para que cada bit de seja ).

Introdução à Programação: Conceitos e Práticas. Página 63


Exemplo Representação
00001010
: 00001010
C-1: 11110101  Ok
01111111
00000000
: 00000000
C-1: 11111111  Ok
10000000  Sinal inconsistente. Não foi possível
representar em 8 bits.
: 10000000
C-1: 01111111 Sinal inconsistente. Não foi
possível representar em 8 bits.
: 01111111
C-1: 10000000  Ok
01100100

Assim, a faixa de representação é dada por:

5.1.2.3. Complemento de 2

O método consiste na conversão do valor absoluto de


para binário. Se o número for negativo, segue-se com a inversão dos bits (toma-se o
complemento do que falta para que cada bit de seja ) e soma-se a este .
O último é desprezado.

Exemplo Representação
00001010
: 00001010
C-1: 11110101
+ 1
--------
C-2: 11110110  Ok
01111111
00000000
: 00000000
C-1: 11111111

Introdução à Programação: Conceitos e Práticas. Página 64


+ 1
--------
C-2: 00000000  Ok
10000000  Sinal inconsistente. Não foi possível
representar em 8 bits.
: 10000000
C-1: 01111111
+ 1
--------
C-2: 10000000  Ok
: 01111111
C-1: 10000000
+ 1
--------
C-2: 10000001  Ok
01100100

Assim, a faixa de representação é dada por:

O método proporciona a mesma representação para o e .

5.1.2.4. Excesso de 2N-1

O método de Excesso de consiste em somar a o valor de , e então


codificar esta soma em binário. Se o resultado desta soma for negativo ou exceder a
quantidade de bits permitida, então o valor de estará fora da faixa para os
definidos para a representação.

  , verificar coerência do sinal  

O método também proporciona a mesma representação para o e . A


notação de sinal na representação final é invertida em relação aos demais métodos.

Exemplo Representação
: 10001010  Ok
: 01110110  Ok
: 11111111  Ok
: 10000000  Ok
: 10000000  Ok
: 100000000  Não foi
possível representar em 8 bits.
: 00000000  Ok

Introdução à Programação: Conceitos e Práticas. Página 65


: 00000001  Ok
: 11100100  Ok

Assim, a faixa de representação é dada por:

5.1.2.5. Visualização

Podemos facilmente visualizar a relação entre um número do mundo real e sua


representação no mundo digital. O quadro abaixo ilustra uma sala com várias
cadeiras, onde as cadeiras são numeradas em binário com .

Inteiro sem sinal

Binário Puro

0 1 ... 127
00..00 00.01 ... 01..11

128 129 ... 255


10..00 10.01 ... 11..11

Do lado do mundo real uma determinada família de números quer adentrar ao


mundo digital e ocupar uma cadeira. A primeira restrição é o tamanho da sala. Não
há cadeiras para todos os elementos da família, apenas alguns poderão ocupar um
assento. No caso da família dos , a sala típica com cadeiras
numeradas com , estarão disponíveis cadeiras, desde a cadeira
até a .O de valor ocupará a cadeira , o valor
ocupará a cadeira , e assim por diante até o valor que ocupará a
cadeira . O protocolo de conversão determina qual a cadeira a ser
ocupada por um determinado valor. No caso dos vimos que o
método utilizado para conversão é o . As características da sala
permitem que apenas os valores definidos por , poderão ser
representados. Caso seja necessário abranger uma faixa maior, deverá ser utilizada
uma sala com cadeiras maiores, ou seja com maior quantidade de bits.

Para a família dos , temos uma sala para cada protocolo de


conversão: e

Introdução à Programação: Conceitos e Práticas. Página 66


Inteiro com sinal

Módulo-Sinal Complemento de 1

+0 +1 ... +127 +0 +1 ... +127


01..1 00..0 00.0 01..1
00..00 00.01 ... 1 0 1 ... 1

-0 -1 ... -127 -127 -126 ... -0


11..1 10..0 10.0 11..1
10..00 10.01 ... 1 0 1 ... 1

Complemento de 2 Excesso

±0 +1 ... +127 -128 -127 ... -1


01..1 00..0 00.0 01..1
00..00 00.01 ... 1 0 1 ... 1

-128 -127 ... -1 ±0 +1 ... +127


11..1 10..0 10.0 11..1
10..00 10.01 ... 1 0 1 ... 1

Nas salas do e do podemos visualizar que o


número , tanto o quanto o , ocupam distintas cadeiras, o que caracteriza
uma desvantagem do método, pois uma destas cadeiras poderia ser ocupada por
mais um elemento dos . Nas salas do e do
constatamos que o , quer seja o ou o , são levados, como
consequência do método, para a mesma cadeira, o que acaba liberando um assento
para o .

5.1.3. Soma em Complemento de 2

A representação em traz uma grande vantagem para a operação


de soma, pois trata de forma transparente os inteiros sem sinal e com sinal.

Tomemos como exemplo a seguinte soma de binários, para um

Exemplo Inteiro sem Sinal Inteiro com Sinal


Binário Puro Complemento de 2
1111 C-2 10001001
00001111 15 15 - 1
+ 10001001 + 137 + -119 --------
-------- ----- ----- C-1 10001000
10011000 152 -104 |X| 01110111
X = -119

Introdução à Programação: Conceitos e Práticas. Página 67


C-2 10011000
- 1
--------
C-1 10010111
|X| 01101000
X = -104

O que se observa é que independente da interpretação dada ao binário, se inteiro


sem sinal ou inteiro com sinal em , o resultado é coerente.

Na sequência são apresentados exemplos com algumas incoerências nos


resultados das somas de inteiros sem sinal e nas somas de inteiros com sinal. Pode
ser observada facilmente a lógica destas incoerências, a saber:

 Quando somamos dois inteiros sem sinal e ocorre o no último bit então
estamos diante de um , ou estouro de capacidade.

 Quando somamos dois inteiros com sinal deve ser observada a coerência dos
sinais: Soma de positivos deve gerar resultado positivo e soma de negativos
deve gerar resultado negativo.

Exemplo Inteiro sem Sinal Inteiro com Sinal


Binário Puro Complemento de 2
1111 C-2 11100011
01111001 121 121 - 1
+ 01101010 + 106 + 106 --------
-------- ----- ------ C-1 11100010
11100011 227 -29 |X| 00011101
X = -29
(ok) (overflow)

11 1 C-2 11001001
11001001 201 -55 - 1
+ 11101110 + 238 + -18 --------
-------- ----- ------ C-1 11001000
10110111 183 -73 |X| 00110111
X = -55
(overflow) (ok) C-2 11101110
- 1
--------
C-1 11101101
|X| 00010010
X = -18
C-2 10110111
- 1
--------
C-1 10110110
|X| 01001001
X = -73

Introdução à Programação: Conceitos e Práticas. Página 68


1 11 C-2 10000101
10000101 133 -123 - 1
+ 10001110 + 142 + -114 --------
-------- ----- ------ C-1 10000100
00010011 19 + 19 |X| 01111011
X = -123
(overflow) (overflow) C-2 10001110
- 1
--------
C-1 10001101
|X| 01110010
X = -114

5.1.4. Exercícios

1. Obtenha os valores de X que a sequência de binário representa para cada um


dos métodos:

BP MS C–1 C–2
0000 1111
1000 1101
1111 0000
0111 1111
1000 0000
1111 1111

5.2. Representação de Números Reais

A representação de números reais é mais complexa e exige a codificação de vários


componentes de . Os passos para se chegar a representação digital incluem:

i. Converter para a base


ii. Normalizar
iii. Enquadrar

Exemplo:

i. Converter para a base

0.32 x16 = 5.12


0.12 x16 = 1.92
0.92 x16 = 14.72
0.72 x16 = 11.52
0.52 x16 = 8.32

Introdução à Programação: Conceitos e Práticas. Página 69


ii. Normalizar

Normalizar equivale a reescrever o número binário na notação científica,


reposicionando o “ponto decimal” a frente do primeiro dígito e fazendo a devida
compensação multiplicando-se o binário por , onde é o número de casas
deslocadas, sendo quando para esquerda e quando para a direita.

Assim, tem-se a seguinte forma geral para um número normalizado:

, onde:

 , com exceção quando . Todas as partes serão


.
 será um inteiro com sinal.

Os componentes que devem ser representados são o eo .

iii. Enquadrar

Agora, resta enquadrar as partes de em um determinado padrão codificado em


binário. Para os nossos exemplos, até que se coloque em contrário, adotaremos o
seguinte padrão para números reais:

 Serão assim divididos:

S Expoente ( ) Mantissa ( )

o O bit de Sinal será para positivo ou para negativo


o Os bits do campo Expoente acomodarão a codificação de como
inteiro com sinal em algum dos métodos apresentados. Normalmente
utiliza-se ou ; Nos exemplos que seguem utilizaremos .
o O campo Mantissa deverá acomodar os bits que veem após o
“ponto decimal” da notação normalizada.

Introdução à Programação: Conceitos e Práticas. Página 70


S Expoente ( ) Mantissa ( )
0 0 0 0 0 1 1 0 1 0 1 1 1 0 0 1 0 1 0 0 0 1 1 1

o O Sinal ficou com , pois é positivo;


o O Expoente ficou , pois , e a codificação é em ;
o A Mantissa ficou , pois são os bits comportados
pela notação.

Exemplo:

i. Converter para a base

0.004600000 x 16 = 0.073600000
0.073600000 x 16 = 1.177600000
0.177600000 x 16 = 2.841600000
0.841600000 x 16 = 13.465600000
0.465600000 x 16 = 7.449600000
0.449600000 x 16 = 7.193600000

ii. Normalizar

iii. Enquadrar

Agora, resta enquadrar as partes de no padrão estabelecido em binário.

S Expoente (7) Mantissa (16)


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

Introdução à Programação: Conceitos e Práticas. Página 71


Onde a parte do foi obtida da seguinte forma:

BP: 0000111
C-1: 1111000
+ 1
--------
C-2: 1111001  Ok

Ou simplesmente:

No caso de representação de números reais, é importante conhecer a capacidade


da notação, principalmente as características de amplitude e precisão. A estrutura
do exemplo não é capaz de representar todos os números reais com precisão
infinita. Logo, a técnica utilizada mapeia apenas um subconjunto dos números reais.
Considere a reta dos reais abaixo indicada:

Figura 31: Faixa de Representação de Números Reais.

Nem todos os pontos das retas dos reais são representados. Neste caso interessa
conhecer os maiores e os menores números representáveis. Estes limites são
calculados a partir da equação normalizada:

O Maior Positivo ( ) é obtido com o produto da maior Mantissa ( ) com


o maior valor de , onde e a representação em . Neste caso temos
a seguinte faixa para o Expoente ( ): . Assim, tem-se:

Como:

Logo:

O Menor Positivo ( ) é obtido com o produto da menor Mantissa ( ) com o menor


valor de . Assim, tem-se:

Introdução à Programação: Conceitos e Práticas. Página 72


Logo:

5.2.1. Formato IEEE 754

O IEEE padronizou algumas representações de números reais na norma IEEE


(IEC ). A maioria dos computadores modernos adota este padrão. O seguinte
quadro relaciona os tipos disponibilizados nesta norma:

Dígitos
Total Expoente Bits
Tipo Sinal Expoente Mantissa Significativos
bits bias Precisão
Decimal
Half 1 5 10 16 15 11 ~3.3
Single 1 8 23 32 127 24 ~7.2
Double 1 11 52 64 1023 53 ~15.9
Double extended
1 15 64 80 16383 64 ~19.2
(80-bit)
Quad 1 15 112 128 16383 113 ~34.0

Detalhando o tipo , tem-se a seguinte estrutura:

S Expoente ( ) Mantissa ( )

o O bit de Sinal é para positivo ou para negativo


o Os do campo acomodam a codificação de como
inteiro com sinal utilizando um método semelhante ao , porém
com um de , que para é . A faixa de
representação é de .
o O campo acomoda , porém com um diferencial.
Quando se normaliza um número binário, a mantissa sempre iniciará
com o dígito , com exceção para o número zero. Assim, este dígito
fica implícito, sem a necessidade de ocupar fisicamente um posição na
notação. Logo, os representam uma precisão real de .

Exemplo:

i. Converter X para a base 2

Introdução à Programação: Conceitos e Práticas. Página 73


11. 0.141592654 x 16 = 2.265482457
0.265482457 x 16 = 4.247719319
0.247719319 x 16 = 3.963509104
0.963509104 x 16 = 15.416145661
0.416145661 x 16 = 6.658330571
0.658330571 x 16 = 10.533289135
0.533289135 x 16 = 8.532626152
0.532626152 x 16 = 8.522018433
0.522018433 x 16 = 8.352294922

ii. Normalizar

Observe que o primeiro bit da mantissa reposicionado na parte inteira ficará oculto
na representação.

iii. Enquadrar

Agora, resta enquadrar as partes de X no padrão binário IEEE.

Onde a parte do Expoente foi obtida da seguinte forma:

, em Excesso com bias de

Ou simplesmente:

5.2.2. Exercícios

i. Representar em números reais os seguintes números:







Introdução à Programação: Conceitos e Práticas. Página 74


 –


 –

ii. Que números estão representados pelas seguintes codificações binárias:



Introdução à Programação: Conceitos e Práticas. Página 75


5.3. Representação de Informações Textuais

A representação de informações textuais é feita através de um acordo que


convenciona atribuir para cada caractere um código numérico. A convenção mais
aceita é dada por uma tabela denominada Tabela (acrônimo para American
Standard Code for Information Interchange, ou em português "Código Padrão
Americano para o Intercâmbio de Informação"). Desenvolvida a partir de ,
grande parte das codificações de caracteres modernas tomam esta tabela como
base.

Caracteres não imprimíveis ( ):

Representados como a parte não imprimível da tabela , os caracteres de


controle tiveram sua origem nos primórdios da computação, quando se usavam
máquinas teletipo e fitas de papel perfurado.

Binário Decimal Hexa Controle Abreviação Descrição


0000 0000 0 0 ^@ NUL Null Nulo
0000 0001 1 1 ^A SOH Start of Header Início do cabeçalho
0000 0010 2 2 ^B STX Start of Text Início do texto
0000 0011 3 3 ^C ETX End of Text - Fim do texto
0000 0100 4 4 ^D EOT End of Tape Fim de fita
0000 0101 5 5 ^E ENQ Enquire Interroga Term ID
0000 0110 6 6 ^F ACK Acknowledge Reconhecimento
0000 0111 7 7 ^G BEL Bell Campainha
0000 1000 8 8 ^H BS Back-space Espaço atrás
0000 1001 9 9 ^I HT Horizontal Tab Tabulação horizontal
0000 1010 10 0A ^J LF Line-Feed Alimenta linha
0000 1011 11 0B ^K VT Vertical Tabulation Tabulação vertical
0000 1100 12 0C ^L FF Form-Feed Alimenta formulário
0000 1101 13 0D ^M CR Carriage-Return Enter
0000 1110 14 0E ^N SO Shift-Out Saída do shift
0000 1111 15 0F ^O SI Shift-In Entrada no shift
0001 0000 16 10 ^P DLE Data-Link Escape
0001 0001 17 11 ^Q DC1 Device-Control 1
0001 0010 18 12 ^R DC2 Device-Control 2
0001 0011 19 13 ^S DC3 Device-Control 3
0001 0100 20 14 ^T DC4 Device-Control 4
0001 0101 21 15 ^U NAK Neg-Acknowledge Não-reconhecimento
0001 0110 22 16 ^V SYN Synchronous Idle
0001 0111 23 17 ^W ETB End-of-Transmission
0001 1000 24 18 ^X CAN Cancel
0001 1001 25 19 ^Y EM End-Of-Medium
0001 1010 26 1A ^Z SUB Substitute
0001 1011 27 1B ^[ ESC Escape
0001 1100 28 1C ^\ FS File Separator
0001 1101 29 1D ^] GS Group Separator
0001 1110 30 1E ^^ RS Record Separator
0001 1111 31 1F ^_ US Unit Separator

Introdução à Programação: Conceitos e Práticas. Página 76


0111 1111 127 7F ^? DEL Delete

Caracteres não imprimíveis ( ), Caracteres imprimíveis ( ) e Caracteres


Estendidos ( ):

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 2 3 4 5 6 7 8 9 A B C D E F
0 0 NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI

1 1 DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
2 2 ! " # $ % & ' ( ) * + , - . /
3 3 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
4 4 @ A B C D E F G H I J K L M N O
5 5 P Q R S T U V W X Y Z [ \ ] ^ _
6 6 ` a b c d e f g h i j k l m n o
7 7 p q r s t u v w x y z { | } ~ •
8 8 Ç ü é â ä à å ç ê ë è ï î ì Ä Å
9 9 É æ Æ ô ö ò û ù ÿ Ö Ü ø £ Ø × ƒ
10 A á í ó ú ñ Ñ ª º ¿ ® ¬ ½ ¼ ¡ « »
11 B ░ ▒ ▓ │ ┤ Á Â À © ╣ ║ ╗ ╝ ¢ ¥ ┐
12 C └ ┴ ┬ ├ ─ ┼ ã Ã ╚ ╔ ╩ ╦ ╠ ═ ╬ ¤
13 D ð Ð Ê Ë È ı Í Î Ï ┘ ┌ █ ▄ ¦ Ì ▀
14 E Ó ß Ô Ò õ Õ µ þ Þ Ú Û Ù ý Ý ¯ ´
15 F ± ‗ ¾ ¶ § ÷ ¸ ° ¨ · ¹ ³ ² ■

Caracter . Caracter .

Exemplo: O texto seria codificado como uma sequência de bytes


equivalentes aos seguintes códigos:

J o a o d a S i l v a
74 111 97 111 32 100 97 32 83 105 108 118 97

Exemplo: O texto seria codificado com os binários


equivalentes aos seguintes códigos:

A v B r a s I l , 3 2 2 . A p t º 1 0 6 C
65 118 32 66 114 97 115 73 108 44 32 51 50 50 46 32 65 112 116 186 32 49 48 54 67

Os caracteres estendidos podem ser diferentes dependendo da fonte. Neste último


exemplo, os códigos foram obtidos através de um recurso da planilha Excel, o que
justifica algumas diferenças em relação à tabela acima.

Introdução à Programação: Conceitos e Práticas. Página 77


6. Introdução a Arquitetura

A arquitetura de um computador é um modelo da organização e funcionamento de


um sistema de processamento. A descrição da arquitetura destaca as funções dos
componentes básicos de um computador, a interconexão destes componentes e o
modo como os componentes interagem. A maioria das arquiteturas está baseada na
proposta de :

Figura 32: Organização típica de um computador.

A dinâmica da execução de um programa inicia quando o usuário submete um


determinado arquivo executável para ser processado pelo computador. Inicialmente
este programa está contido em um arquivo e o primeiro passo é carrega-lo para a
memória principal ( – ) onde ocupará um espaço
designado pelo Sistema Operacional. Neste espaço, uma área conterá os bytes
correspondentes às instruções do programa e outra área é reservada ao
armazenamento de resultados provenientes da execução destas instruções. Esta
última área também é denominada área de dados. A seguinte figura ilustra de forma
simplificada o uso da memória por um programa em execução:

Introdução à Programação: Conceitos e Práticas. Página 78


RAM
A: 1111
B: 1110

DADOS
C: 1101
X: 1100
Y: 1011
Z: 1010
T: 1001
mov eax, A 1000
add eax, B 0111
INSTRUÇÕES mov C, eax 0110
(código) mov ebx, eax 0101
mov eax, X 0100
sub eax, Y 0011
mov Z, eax 0010
add ebx, eax 0001
mov T, eax 0000

Figura 33: Programa na Memória RAM: Código + Dados.

A execução do programa inicia pelo processamento da instrução que estiver no


endereço de memória designado como início do código. A primeira instrução é então
levada da memória para a onde é tratada e processada, e quando for o caso,
um resultado é armazenado na memória ou na própria . Em seguida o mesmo
tratamento é dado para a segunda instrução e assim até o término do programa,
quando o mesmo é liberado da memória.

A gestão deste ambiente é feita pelo Sistema Operacional, enquanto que a é


responsável pelo ciclo de execução da instrução. Observa-se que durante a
execução de um programa há um forte trânsito de dados entre a e a memória
principal ( ). A trabalha em uma velocidade muito superior a velocidade da
memória RAM. Assim, durante a execução do programa a passa muito tempo
aguardando os bytes chegarem da/na , o que acarreta desperdício deste
recurso precioso que é a . Este limitante onde é comumente
chamado de Gargalo de Von Newmann.

Segue uma descrição resumida dos elementos que formam a arquitetura


apresentada:
 Processador ( ) é o componente ativo, controlador de todo o sistema. É o
processador que realiza todas as operações sobre os dados, de acordo com
o indicado pelas instruções no código do programa.
 memória principal ( ) armazena as instruções que são executadas pelo
processador, e os dados que serão manipulados.
 interfaces de entrada e de saída ( ) são as portas de comunicação para o
mundo externo, às quais estão conectados os dispositivos periféricos tais
como vídeo, teclado, discos e impressora.
 Estes componentes estão interconectados por meio de um ,
através do qual o processador realiza o acesso a instruções e dados
armazenados na memória principal.

Introdução à Programação: Conceitos e Práticas. Página 79


 É também através deste mesmo barramento que o processador recebe ou
envia dados de ou para as interfaces de entrada/saída.
 Instruções e dados estão armazenados em , na memória
principal.

6.1. Processador - CPU

A estrutura básica de um processador é composta normalmente por uma


e uma . A figura a seguir ilustra uma
– Central Processing Unit – simplificada:

Figura 34: Unidade Central de Processamento típica.

A pode ser vista como uma fábrica que processa matéria prima gerando
produtos. Neste cenário a é a responsável pelo gerenciamento
da fábrica, incluindo a entrada da matéria prima, a fabricação do produto e a entrega
deste produto. Cada instrução recebida pela se assemelha a
chegada de uma ordem de serviço para fabricação de um produto. Inicialmente o
gerente da fábrica busca compreender a instrução de fabricação do produto,
partindo em seguida para o preparo da linha de produção para a fabricação, e uma
vez pronto, o produto é encaminhado para o depósito, de onde tomará seu destino
final.

Introdução à Programação: Conceitos e Práticas. Página 80


6.1.1. Seção de Controle

As operações básicas que ocorrem dentro da seção de processamento são todas


comandadas pela unidade de controle. Ao efetuar a busca da instrução, a unidade
de controle interpreta a instrução de modo a identificar quais as operações básicas
que devem ser realizadas, bem como ativa os sinais de controle que fazem uma
operação básica de fato acontecer.

Os sinais de controle que são ativados, bem como a sequência com que são
ativados, dependem de cada instrução em particular.

Assim, a execução de uma instrução envolve a realização de uma sequência de


passos, que podemos chamar de . Em geral, a execução de uma
instrução envolve quatro passos, como mostra a seguinte:

busca decodificação execução resultado

Figura 35: Passos na execução de uma instrução.

 No primeiro passo, denominado busca ( ), o processador realiza o


acesso ao código binário da instrução, armazenado na memória principal.
 A etapa seguinte é a decodificação ( ) da instrução, na qual as
informações contidas no código da instrução são interpretadas.
 No terceiro passo, denominado execução, no qual a operação indicada pela
instrução (por exemplo, uma operação na ) é efetuada.
 Finalmente no quarto passo, chamado resultado ( ), é armazenado em
um registrador ou na memória o resultado produzido pela instrução.
 Cada passo de execução envolve a realização de várias operações básicas.

 transferência de dados entre os registradores e a ;


 transferência de dados entre os registradores;
 transferência de dados entre os registradores e a memória;
 operações aritméticas e lógicas realizadas pela .

6.1.2. Seção de Processamento

A seção de processamento é formada basicamente pela unidade lógica e aritmética


( ) e por diversos registradores.

A realiza as operações aritméticas, tais como adição e subtração, e operações


lógicas, tais como .

Os registradores são utilizados para armazenar informações internamente no


processador. Um registrador pode ser utilizado tanto para acesso de leitura quanto

Introdução à Programação: Conceitos e Práticas. Página 81


para acesso de escrita. Os diversos registradores possuem um uso bem definido
dentro da arquitetura, e de uma maneira geral podem ser classificados em três tipos:

 registradores de uso geral,


 registradores de uso específico e
 registradores auxiliares.

Os registradores de uso geral normalmente são usados para armazenar dados que
serão processados pela , bem como resultados produzidos pela . O
registrador de estado (status register) associado à é um registrador de uso
específico, e contém informações sobre o resultado produzido pela . Este
3
registrador possui bits sinalizadores que são ativados ou desativados de acordo
com o tipo de resultado produzido pela ( ...).
Um outro exemplo de registrador de uso específico é o contador de programa
( ), cuja função é guardar o endereço da locação de memória onde
se encontra a próxima instrução a ser executada pelo processador.

6.1.3. Barramentos

A interligação entre a e os registradores é feita através de barramentos


internos.

Os barramentos internos permitem a transferência de dados dos registradores para


a e permite a transferência do resultado produzido pela , de um registrador
temporário, para outro registrador.

Uma arquitetura de processador é uma arquitetura de n bits quando todas as


operações da podem ser realizadas sobre operandos de até .

Normalmente, em uma arquitetura de os registradores de dados e os


barramentos internos também são de , de forma a permitir que os dados sejam
armazenados e transferidos de forma eficiente.

A arquitetura Intel, desde o até o Pentium, inclui três barramentos distintos:


Barramento de Dados ( ), Barramento de Endereço ( ) e
Barramento de Controle ( ).

 Barramento de Dados: por onde trafega informação qualificada como dado;


 Barramento de Endereço: trafega informação qualificada como endereço;

3
Considera-se aqui que um bit é ativado quando ele recebe o valor lógico 1, sendo desativado ao receber o
valor lógico 0.

Introdução à Programação: Conceitos e Práticas. Página 82


O movimento de dados é uma das instruções mais utilizadas em um programa.
Equivale ao comando de atribuição em uma linguagem de alto nível, como Pascal e
C. É comum trazer dados da memória para a e vice-versa.

Exemplo: Supondo que queiramos executar a seguinte instrução:

[ ]

Que corresponde a guardar o número na célula de memória do endereço . Os


seguintes passos serão realizados:

i. Colocar no barramento de endereço;


ii. Colocar no barramento de dados;
iii. Ativar o sinal no barramento de controle;
iv. Aguardar enquanto o controlador da memória armazena o dado na célula de
indicada;
v. Desativar o sinal no barramento de controle;

Esta sequência culmina com uma ordem ao controlador da memória em pegar estas
informações dos barramentos e efetuar a operação de gravar na memória.

De modo semelhante, a seguinte instrução: [ ] , que determina que a


informação contida no endereço da memória seja copiada para um registrador,
iniciará a seguinte sequência de utilização dos barramentos:

i. Colocar no barramento de endereço;


ii. Ativar o sinal no barramento de controle;
iii. O controlador da memória lê este pedido e copia a informação solicitada para
o barramento de dados;
iv. Transferir o número que está no barramento de dados para o Registrador;
v. Desativar o sinal no barramento de controle

O tamanho em bits dos barramentos de dados e de endereço influenciam diferentes


aspectos da arquitetura.

O tamanho do barramento de dados está ligado diretamente a capacidade de


transferência de informações entre partes distintas da e entre a e outros
componentes do sistema de processamento, como a memória . Por exemplo,
se deseja trazer da memória para um registrador da , e o
barramento de dados comporta simultâneos, então este trabalho será feito
em duas instruções, Caso este mesmo barramento comportasse simultâneos
o trabalho seria feito em uma instrução.

O tamanho do barramento de endereços influencia diretamente na capacidade de


endereçamento físico de memória . Uma analogia pode ser feita com os
Códigos de Endereçamento Postal (CEP). Se cada cidade tivesse um CEP com
apenas dígitos decimais, então este sistema permitiria endereçar até cidades
( ). Quando relacionamos cidades com células ( ) de memória e

Introdução à Programação: Conceitos e Práticas. Página 83


o CEP com a informação colocada no barramento de endereços identificando um
destino na memória, logo vemos que quanto mais bits estiverem disponíveis no
barramento de endereços mais bytes de memória poderão ser endereçados. Por
exemplo: se tivermos de tamanho do barramento de endereços, então a
capacidade de endereçamento será de células (bytes). O endereço da primeira
célula seria e da última .

Na arquitetura Intel o tamanho dos barramentos evoluiu da seguinte forma:

Barramento de Endereço
Tamanho dos Barramento
Processador Tamanho Quantidade de Bytes
Registradores de Dados
de Memória
8086 20 16 16
80286 24 16 16
80386 32 32 32
80486 32 32 32
Pentium 32/36/644 32/64 32/64

6.1.4. O sinal de Clock

Para atender as relações de tempo requeridas na ativação dos sinais de controle, a


unidade de controle opera em sincronismo com um sinal de .
1 ciclo

clock

leitura decodificação execução resultado

Figura 36: A sincronização da execução de uma instrução com o sinal de clock.

A execução de uma instrução consome certo número de ciclos de . O número


de ciclos de por instrução não é o mesmo para todas as instruções, já que
cada instrução pode envolver um número diferente de operações básicas em cada
passo de execução.
O tamanho do ciclo de é um dos fatores que determinam diretamente o
desempenho de um processador. Quanto menor o tamanho do ciclo de , menor
será o tempo de execução das instruções, e assim maior será o número de
instruções executadas por unidade de tempo.
Em um processador com frequência de clock de , o período será:

4
Primeiras implementações utilizam parte dos 64 bits. O uso integral permitiria 16 EBytes,
http://en.wikipedia.org/wiki/X86-64

Introdução à Programação: Conceitos e Práticas. Página 84


A frequência de clock dos processadores Intel era de nos primeiros
processadores chegando a no .

6.1.5. Organização do Computador

A busca por melhor desempenho impulsionou não só o desenvolvimento de


processadores com melhores características de frequência de , tamanho de
barramento, tamanho de registradores e quantidade de memória, mas também levou
a pesquisa de novos elementos de arquitetura que pudessem impactar no
desempenho do processador e na sua interação com a memória principal.

Segue um resumo das principais técnicas.

6.1.5.1. Pipeline

Uma destas técnicas foi inspirada no conceito da linha de produção. Foi visto que o
processamento de uma instrução na passa pelas etapas de: , ,
e ; ou Busca, Decodificação, Execução e Resultado. Assim, uma
instrução quando trazida da memória para a ocupava a até o término da
sua execução. Então, os projetistas trabalharam no redesenho da de tal forma
que os circuitos fossem reagrupados em partes independentes, uma para cada
etapa do ciclo de execução. Esta técnica, inspirada na linha de produção, foi
denominada de .

O seguinte diagrama ilustra o processamento de instruções na CPU com pipeline.

Figura 37: Execução e Pipeline.

Inicialmente uma instrução ocupa a unidade de Busca, e na sequencia a unidade


de Decodificação, liberando o estágio anterior, que pode ser ocupada pela instrução
. Enquanto avançam de estágio novas instruções podem ir ocupando os estágios

Introdução à Programação: Conceitos e Práticas. Página 85


liberados. Neste exemplo, vemos que no tempo quatro instruções ocupam a ,
melhorando sensivelmente o desempenho do processamento.

A pipeline perde desempenho quando processa instrução de desvio, e somente terá


a confirmação que o desvio será feito no estágio de execução, e as instruções
anteriormente trabalhadas serão desperdiçadas. Isto se deve ao fato de que o
desvio para outro trecho do programa demandará uma nova sequencia de
instruções. Processadores modernos concentram esforços para contornar este
problema, geralmente adotando mais de uma pipeline.

6.1.5.2. Cache

Citamos anteriormente a existência de um limitante derivado do aspecto onde


, denominado . Para assegurar a
disponibilidade de uma grande quantidade de memória , fundamental às
aplicações atuais, os fabricantes tiveram que utilizar tecnologias que contrastam
custo quantidade tempo de acesso. Para obter um custo reduzido e permitir o
uso de grandes quantidades de memória, foi utilizada tecnologia de memória que
resulta em maior tempo de acesso, quando comparada aos tempos de uma .O
ideal seria que a memória tivesse o mesmo desempenho de um Registrador da
. Porém, os registradores são poucos e específicos para alguns fins.

Desta forma, os projetistas avaliaram o comportamento dos programas quando


estão sendo executados e observaram um aspecto, onde trechos dos programas
quando processados permaneciam manipulando segmentos bem limitados
(endereços próximos) da memória por um longo tempo. É importante destacar
que um programa na memória tem o seu fluxo de processamento composto
basicamente por instruções executadas em sequencia ou por instruções de desvios.
As instruções de desvios normalmente se referem a uma seleção de caminhos de
processamento ou uma iteração. É neste último caso que situações típicas envolvem
o processamento repetitivo de uma área de memória com endereços próximos.

Logo os projetistas concluíram que era possível desenvolver uma memória com
tempo de acesso menor que o tempo de acesso da , e em menor quantidade, o
suficiente para acomodar o trecho da que está sendo referenciado num
determinado momento.

Figura 38: Memória Cache.

Introdução à Programação: Conceitos e Práticas. Página 86


Assim, a memória se situa entre a e a memória , e possui um chip
controlador que implementa uma política que decide sobre a ocupação deste espaço
de memória. Esta lógica tem que decidir, quando encontra um acesso a um
endereço da não disponível no seu domínio, como se dará a substituição para
que este novo bloco passe a ocupar a .

Um programa com excesso de desvios para trechos de códigos que manipulam


áreas de memória distintas degradam o desempenho, uma vez que são produzidas
muitas faltas na .

6.1.5.3. Outras Técnicas

Outras técnicas são empregadas visando elevar ao máximo o desempenho global


de um sistema de processamento. Muito resumidamente, seriam:

i. Processamento Paralelo: Esta técnica visa multiplicar uma parte ou toda


uma permitindo que trechos distintos de um programa ou mesmo
distintos programas possam ser processados simultaneamente. Além de ser
uma característica da , também os Sistemas Operacionais e as
Linguagens de Programação devem ser capazes de explorar este recurso.
Processadores modernos com tecnologia empregam esta técnica.

ii. Processamento Vetorial: A com processamento escalar é capaz de


operar sobre apenas um dado. Por exemplo: a instrução de soma é capaz de
somar um número com outro número produzindo um resultado. Em um
processador vetorial, um único registrador da é capaz de conter múltiplos
dados, e as instruções podem operar com estes múltiplos dados. Por
exemplo: em uma com processamento vetorial é possível trazer quatro
inteiros da para a e acomodá-los em um registrador; em seguida
uma instrução de soma poderá adicionar estes quatro inteiros a outros quatro
inteiros produzindo como resultado quatro inteiros, tudo ao mesmo tempo. As
tecnologias dos processadores incorporam esta
característica, onde uma instrução pode operar múltiplos dados.

Introdução à Programação: Conceitos e Práticas. Página 87


7. Introdução à Programação

Atualmente não restam mais dúvidas sobre o sucesso da tecnologia digital, pois em
curto espaço de tempo alcançou um grau de disseminação que transcende
fronteiras, culturas e classes sociais. Certamente o nível de desenvolvimento
tecnológico que presenciamos hoje representa apenas uma amostra do que há por
vir.

Este avanço se deve ao surgimento do computador nos anos , que de forma


inegável marcou o início de uma revolução que se propagaria em toda a sociedade.
Porém, a linguagem de programação poderia ser igualmente qualificada como
revolucionária, pois a sua utilização proporcionou uma incrível flexibilidade aos
computadores.
5
A linguagem teve um papel relevante nesta revolução pelo fato ter sido a
primeira linguagem com grande aceitação, sucesso este em grande parte creditado
a sua maior apoiadora, a Após este sucesso, teve início uma onda de criação
de novas linguagens, cuja amostra pode ser observada na seguinte árvore
genealógica.

FORTRAN (54)

COBOL (59)
1960 ALGOL 60

BASIC (64) PL/I (65)

Simula 67 ALGOL 68
1970
C (72) Pascal (71)

Modula 2 (75) Conc Pascal (75)


CLU (75) Ada (79)
1980
Ada (83) ANSI
C++(85)
Eifell (86)

1990

Java (95) Ada (95)


Figura 39: Genealogia das Linguagens – Base FORTRAN.

5
http://pt.wikipedia.org/wiki/Fortran

Introdução à Programação: Conceitos e Práticas. Página 88


Esta árvore destaca claramente a influência da linguagem , gerando uma
grande quantidade de descendentes, o que não se observa em linguagens com
diferentes propostas de estilos de programação, como ilustra a figura abaixo:

LISP (59)

APL (62) SNOBOL (62)

Smalltalk (69)

PROLOG (72)

OPS5 (77)

Perl (87) Tcl/Tk (88)

Figura 40: Genealogia das Linguagens.

Um dos fatores que foram determinantes no sucesso das linguagens de


programação foi a possibilidade de fazer com que o computador desempenhe
diferentes funções, bastando para isto programar o comportamento desejado. Além
disso, a produtividade proporcionada por uma linguagem de alto nível, como o
, estimulou largamente o emprego de programas para realizar funções das
mais variadas.

A expansão e os aprimoramentos incorporados às linguagens fizeram com que a


atividade de programação saísse dos ambientes de pesquisa e começasse a
permear os mais diferentes setores da economia e incorporasse programadores
originários de várias outras profissões.

A seguinte figura ilustra como a produção de programas foi facilitada pelas


linguagens de programação de alto nível.

Figura 41: Níveis de Programação.

Introdução à Programação: Conceitos e Práticas. Página 89


Tanto o código quanto o código de máquina da figura são de uma
compilação real de uma expressão simples em linguagem de alto nível, que produziu
três linhas em e dezesseis bytes para o código de máquina.

Assim, com a facilidade proporcionada pelas linguagens de alto nível, as décadas de


e experimentaram uma expansão em larga escala no uso de programas de
computadores, gerando uma nova situação que era a preocupação em como
gerenciar este novo elemento, chamado programa de computador.

A ausência de um processo de engenharia no desenvolvimento de programas e a


ausência de boas práticas de gerenciamento levaram ao surgimento do termo
, inicialmente citado por Edsger Dijkstra em .

Esta constatação despertou para a necessidade de abordar a atividade de


programação como um processo de engenharia, demandando a aplicação de
metodologias, técnicas, ferramentas e padrões durante todo o ciclo de vida do
software. A figura a seguir ilustra um processo de desenvolvimento de software:

Figura 42: Ciclo de Desenvolvimento de Software.

O software passou a ser visto como um produto e rapidamente se converteu em


tecnologia presente nos mais diversificados dispositivos, tais como: aviões, carros,
televisões, telefone, micro-ondas, foguetes, trens, dentre outros.

Introdução à Programação: Conceitos e Práticas. Página 90


8. O Processo de Tradução

A Linguagem de Programação reduziu a lacuna entre o problema e o programa, pois


permitiu que o código ficasse mais próximo da formulação do problema. No caso
particular do a forma de programar se assemelhava a descrição dos
problemas em notação matemática. Isto lhe valeu o acrônimo de
.

Porém, os computadores continuavam a compreender apenas a linguagem binária.


As instruções eram entendidas pelas apenas no formato numérico. Logo, era
necessário traduzir o programa expresso na forma textual para um formato
adequado para processamento na . A figura a seguir ilustra os diversos
conversores e montadores necessários para efetuar esta tradução:

Figura 43: Processo de Tradução

Atualmente estão disponíveis avançados ambientes de programação denominados


– que tornam transparente ao
programador as etapas de tradução. O programador interage com o digitando o
programa em uma linguagem de alto nível e aciona o comando ou a tecla de
compilação. Assim obtém o código final, que pode ser executado ou depurado no
próprio ambiente do .

O depurador é uma importante ferramenta que permite ao programador executar


pausadamente cada linha de código, permitindo inspecionar a memória em busca de
erros na lógica.

Introdução à Programação: Conceitos e Práticas. Página 91


9. A Linguagem C

A linguagem foi por muito tempo utilizada no ensino de programação.


Porém, as versões iniciais da linguagem não proporcionavam mecanismos que
ajudassem de forma natural na escrita de programas com qualidade de produto. De
forma simplificada um programa em execução na memória pode ser visto em dois
blocos, sendo um composto pelas instruções e outro por uma área de dados para
armazenamento e recuperação de informações. As instruções, por sua vez podem
ser de duas naturezas: e . A de tipo é aquela que
concluída a sua execução é dada a vez para a próxima na memória. A de tipo
é aquela que testa uma condição para decidir qual a próxima instrução a ser
executada. Conforme o resultado da condição testada poderá ser executada a
instrução subsequente ou uma instrução em outro endereço.

Assim, programas nos primórdios do seu uso tinham a seguinte


aparência, onde uma série de instruções de desvio ( ou ) alterava o fluxo
de execução do programa, dificultando o entendimento da lógica:

L01 instrução
L02 instrução
L03 jump L06
L04 instrução
L05 jump L16
L06 instrução
L07 jump L10
L08 instrução
L09 jump L04
L10 instrução
L11 jump L15
L12 instrução
L13 jump L02
L14 instrução
L15 jump L01
L16 instrução
L17 jump L10
L18 instrução
Figura 44: Estilo de Programação Spaghetti.

Esta aparência rendeu a denominação de estilo de programação em


função da aparência confusa e semelhante a este típico prato italiano. Neste
período, fins dos anos e década de , o que norteava o desenvolvimento de
programas era a necessidade de economizar os recursos computacionais (memória
e tempo de ), pois o custava muito mais do que o tempo dos
desenvolvedores. Para tal fim, os programadores utilizavam técnicas de
reaproveitamento de memória e otimização das instruções que resultava no médio
prazo em grande dificuldade de manutenção.

Nas décadas seguintes foram surgindo novos estilos de programação com o objetivo
de melhorar a engenharia dos programas, e consequentemente elevar o grau de
qualidade e confiabilidade destes produtos. Estes estilos visavam a melhor
organização das entidades que compõem um programa, principalmente dados e

Introdução à Programação: Conceitos e Práticas. Página 92


rotinas, e também almejavam melhorar a produtividade do programador através do
reuso de códigos. O seguinte quadro ilustra a evolução nos estilos de programação.

Figura 45: Evolução no estilo de programação.

Após o , o esforço seguinte que mereceu destaque na história das


6
linguagens foi o projeto do , que nasceu da intenção de prover uma
linguagem que conduzisse a programas melhores construídos.

O era uma linguagem grande e complexa e dadas as condições da época,


não houveram implementações completas de compiladores. Além disso, neste
período o tinha a como patrocinador, o que dificultava a aceitação e
disseminação de novas linguagens e compiladores. A computação evoluía e
alcançava vários segmentos, além da área científica, como a área comercial,
estudos de inteligência comercial, bem como o desenvolvimento de sistemas
operacionais e software básico.

No setor específico de sistema operacional ( ) e software básico, havia um nível de


retrabalho considerável sempre que se alterava as características do hardware.
Praticamente toda a programação do , que era escrito em , tinha que ser
refeita para se adequar a um novo processador ou a um novo desenho da
arquitetura do computador.

A fim de buscar uma solução para minimizar o esforço de desenvolvimento de


para novos computadores, a , pertencente a empresa , investiu no

6
http://pt.wikipedia.org/wiki/ALGOL

Introdução à Programação: Conceitos e Práticas. Página 93


projeto de desenvolvimento do Sistema Operacional , a partir de um
chamado . A equipe de desenvolvimento incluía Ken Thompson, Dennis
Ritchie, dentre outros.

Havia a necessidade de dispor de uma linguagem apropriada para desenvolvimento


do , visto que a linguagem mudava conforme a arquitetura do hardware.
Assim, nasceu a Linguagem , após várias tentativas com outras linguagens. Uma
sucessão de adaptações e melhorias levaram a uma versão estável da linguagem.
Na raiz da sua estrutura está a linguagem e a linguagem (Combined
Programming Language).

A linguagem também era complexa, e acabou dando origem a uma versão mais
simples, denominada (Basic ). Novas adaptações geraram a linguagem ,
para se consolidar em uma proposta de linguagem que foi denominada de .

Enfim, havia uma linguagem que tornava o desenvolvimento de menos


desgastante. Bastava ter um núcleo mínimo de para iniciar o computador, um
compilador para a Linguagem , e o esforço restante poderia ser feito na linguagem
de alto nível, incluindo o reuso de softwares já desenvolvidos. Em , o
chamado desenvolvido em por Thomson e Ritchie estava disponível em um
computador . O teve seu nome alterado para por Brian
Kernighan.

O sucesso da Linguagem veio com a publicação de um livro clássico em , por


Brian Kernighan e Dennis Ritchie, com o título The C Programming Language. Esta
versão da linguagem ficou conhecida como C Kernighan e Ritchie ou C K&R.

Introdução à Programação: Conceitos e Práticas. Página 94


10. Estrutura de um Programa C

A Figura 33: Programa na Memória RAM: Código + Dados. ilustrou um programa na


memória separado em duas áreas distintas, sendo uma para acomodar a
codificação das instruções e a outra para conter os resultados produzidos por estas
instruções. Esta organização reflete também o título do livro
, de Niklaus Wirth, publicado em , afirmando que
um programa é a combinação de algoritmo e estrutura de dados, ou em termos mais
simples: .

De forma semelhante, um programa tem a seguinte estrutura básica:

#include <stdio.h>

/* área destinada a inclusão de bibliotecas */

/* área destinada a dados e estruturas globais */

/* área destinada a funções */

int main () {
/* área destinada a dados e estruturas locais */

/* área destinada às instruções */

Assim, para iniciar a programação em é necessário conhecer tanto os recursos


disponíveis para definição de dados quando os recursos para manipulação dos
dados.

Uma das vantagens das linguagens de alto nível em relação às linguagens de baixo
nível foi a possibilidade de trabalhar com nomes em vez de endereços físicos de
memória. Assim, na sequência são apresentadas as regras para a criação de nomes
ou identificadores.

11. Identificadores

O programador terá que se habituar a dar nomes para as entidades de um


programa, tais como constantes, tipos, variáveis, e funções. A regra para
nomes ou identificadores é dada por:

Introdução à Programação: Conceitos e Práticas. Página 95


A notação7 acima indica que um identificador válido inicia com um dos caracteres
listados em seguido por zero ou mais caracteres em A
definição admite letra ou dígito. A linguagem distingue entre maiúsculo e
minúsculo, ou seja: uma entidade que tenha o nome todo em maiúsculo difere da
entidade com o nome todo em minúsculo. Seguem alguns exemplos:

Identificadores válidos e distintos Identificadores não válidos


A SOMA TOTAL
B 9AUX
Soma SOMA-TOTAL
soma Dado%
SOMA Valor$
SOMA_TOTAL #qdade
B20 aluno@nome
A1
Qdade
I
_J
K_

Alguns identificadores são palavras reservadas da linguagem, ficando


impossibilitado ao programador utiliza-los como nome de objetos. Segue uma lista
destas palavras:

auto continue double for int signed struct void


break do else if long static switch while
case default enum goto register sizeof typedef volatile
char const extern float return short union unsigned

7
Notação semelhante à BNF:
 O símbolo “::=” significa “é definido como”.
 O símbolo “*” significa “repetir 0 ou mais vezes”.

Introdução à Programação: Conceitos e Práticas. Página 96


12. Constantes

É importante conhecer as formas permitidas para escrever uma constante numérica.


O valor de uma constante inteira pode ser expresso em decimal, octal, hexadecimal
ou caracter . Além disso, a constante pode incorporar um caracter que define o
seu tamanho.

O código fonte descreve a lógica de um programa utilizando padrão textual,


conforme a sintaxe de uma linguagem. A utilização de constantes numéricas e
textuais é bastante frequente na construção de programas.

A representação textual de constantes numéricas consiste na utilização de símbolos


ASCII na sua representação. Porém, a forma adequada para o propósito de
processamento numérico destes valores é a representação binária. Assim, um
trabalho essencial do compilador é converter as constantes representadas no
padrão textual para o modo binário.

Um inteiro pode ser escrito em diversas notações, tais como:

 inteiro sem sinal em decimal;


 inteiro com sinal em decimal:
 Quando uma constante iniciar com o dígito ele será entendido
como escrito em octal;
 Uma constante hexadecimal deve iniciar com ou ;
 A representação equivale ao valor numérico estabelecido pela
tabela para o referido caractere. Neste caso é o decimal .

Estas diversas formas representam o mesmo valor numérico, porém em notações


distintas.

Como citado anteriormente, uma constante inteiro pode ter um sufixo, caracter
adicionado a direita, para designar como (U) e (L). Este sufixo pode
ser maiúsculo ou minúsculo e pode estar em qualquer ordem. Por exemplo: ,
e .

No caso do , a representação binária é resultante da conversão deste valor para


binário pelo método de . Supondo um tamanho de bits, a representação
binária teria a seguinte forma em hexa .

No caso do o valor binário é formado por uma sequência de bits


, como resultante da codificação do em .

Um valor real pode ser representado de diversas formas. Considere a seguinte


definição de variável. Algumas das formas de representação de constantes são:

x = 3.1415;
x = -1234.4567;
x = +.4567;
x = +1234.;

Introdução à Programação: Conceitos e Práticas. Página 97


x = 1.234e-10;
x = -1.234E15;
x = 1.234e+5;
x = 10.0F;
x = 10.0L;
x = 100;

A maioria dos formatos coincide com o estilo utilizado na matemática. Destacamos a


notação 1.234e-10 que equivale à . O símbolo '.', quando presente,
serve como separador das partes inteira e fracionária. Não há separador para
agrupamento de milhar.

A informação textual consiste de uma sequência de caracteres e também são


denominados como string. A representação de uma constante string consiste de
uma sequência de caracteres delimitados por aspas. Cada caractere utiliza byte de
memória. Seguem alguns exemplos:

"Ola mundo" é uma string formada por caracteres.


"" string vazio composta por caractere.
"texto \"entre\" aspas" a sequência \" equivale ao símbolo ". Neste
exemplo, resulta em texto "entre" aspas.

Alguns caracteres de controle, código entre e , podem ser incluídos na


string. Os efeitos de alguns destes caracteres podem ser observados na janela de
execução do programa. Vejamos alguns destes caracteres:

'\n' significa nova linha.


'\0' equivale ao caractere de código zero ( ).
'\t' equivale a tecla .
'\r' significa .
'\\' equivale ao '\'

Os detalhes sobre strings serão apresentados em capítulo exclusivo sobre o tema.

Introdução à Programação: Conceitos e Práticas. Página 98


13. Variáveis

Associar nomes com endereços de memória facilitou a atividade de programação e


deu origem ao conceito de variável. Assim, variável é uma referência a um endereço
de uma área de memória, através da qual se pode armazenar ou recuperar o seu
valor.

A seguinte figura ilustra uma sequência de bytes na memória (endereços de a


) e a associação com os nomes , , e , formando o conceito de variáveis. As
células de memória podem ser trabalhadas através destes nomes.

Nome Célula - RAM Endereço


a: 303
b: 302
c: 301
x: 300

A reserva destas células de memória em se dá pela declaração de variáveis no


trecho destinado aos dados globais ou locais. Além do nome que se deseja associar
à área de memória também é necessário indicar a natureza do dado que se deseja
acomodar nesta área, que em termos de linguagem de programação é o
. A declaração de variável é feita da seguinte forma:

Ou

Onde, – – é uma sequência de nomes de variáveis separados por


vírgula.

Faremos uma analogia da variável com uma cadeira. Observe o seguinde esquema:

Figura 46: Analogia variável vs cadeira.

Introdução à Programação: Conceitos e Práticas. Página 99


A cadeira pode ser referenciada de duas formas, através de um , assentado no
encosto, ou através de um número, assentado sob a base. Este número, sob a base,
é o da variável. No assento propriamente dito temos assentado
diretamente na base um valor , composto por e . É desta forma que
o hardware compreende. Sobre este binário temos uma camada, com o da
poltrona, que orienta como este binário será interpretado por um observador externo.
Finalmente, uma das formas de como este binário poderá ser interpretado é definido
como o .

Os tipos de dados primitivos disponíveis em bem como o operador de atribuição,


serão detalhados a seguir.

14. Tipos de Dados Numéricos

A linguagem disponibiliza tipos de dados numéricos que permitem a representação


de inteiros sem sinal, inteiros com sinal e reais, com diversos tamanhos e precisões.

Os tipos básicos permitidos pela linguagem são: , , , , e


. Os tipos e serão detalhados mais adiante.

A seguinte tabela detalha o significado dos tipos primitivos:

Tipo Faixa #bytes


char Inteiro com sinal 1
int Inteiro com sinal 4
float real , 4
double real , 8

O tamanho do tipo é dependente da arquitetura e do compilador. No compilador


de bits o tipo ocupa bytes.

A linguagem permite adicionar qualificadores de tamanho e/ou de sinal ao tipo


base gerando novos tipos. Os qualificadores de tamanho são e e os de
sinal são e . O qualificador de tamanho não pode ser aplicado ao
tipo , cujo tamanho é definido pela linguagem como sendo . Este
qualificador também não pode ser aplicado ao tipo . O qualificador de sinal não
pode ser aplicado aos tipos e . Desta forma, os tipos disponibilizados
pela linguagem são:

Tipo Natureza Faixa #bytes


char Inteiro com sinal 1
unsigned char Inteiro sem sinal 1
signed char Inteiro com sinal 1
int Inteiro com sinal 4
short int Inteiro com sinal 2

Introdução à Programação: Conceitos e Práticas. Página 100


long int Inteiro com sinal 4
long long int Inteiro com sinal 8
unsigned int Inteiro sem sinal 4
unsigned short int Inteiro sem sinal 2
unsigned long int Inteiro sem sinal 4
unsigned long long int Inteiro sem sinal 8
float real , 4
double real , 8
long double real , 10

Quando o qualificador de sinal não for explicitado, é assumido como .

Agora temos mais elementos para compreender o conceito de variável. Assim, ao


declararmos uma variável estamos fornecendo elementos ao compilador sobre a
natureza da célula de memória e orientando sobre as regras permitidas para o
manuseio deste espaço.

Introdução à Programação: Conceitos e Práticas. Página 101


15. Operador Atribuição

O primeiro recurso a ser apresentado e um dos mais utilizados é o operador de


atribuição, cuja função é armazenar valores na memória. A sintaxe é:

O símbolo representa o operador atribuição, cujo significado é armazenar na


célula de memória indicada pela variável o valor final da expressão. O nome da
aparece no da operação de atribuição e o aparece no
. A instrução ou linha de código é finalizada com o símbolo (ponto-e-
vírgula). Para fazer uso efetivo da atribuição é preciso aprender a escrever
expressões, que podem ser de natureza numérica/aritmética, lógicas ou binárias.

Observe o exemplo a seguir:

Código Analogia variável vs cadeira

int v;

int main () {

v = +5;

Neste exemplo, temos na primeira coluna um pequeno trecho de programa em


que declara uma variável do tipo e associa ao nome . Na sequência, o
programa principal ( ), que é o ponto de partida para a execução do programa,
tem uma única instrução, cujo efeito é atribuir o valor à variável .

Na segunda coluna temos a associação desta variável com o modelo da cadeira. O


nome identifica textualmente a cadeira. Esta cadeira também pode ser identificada
pelo número , que é o endereço da variável. Neste caso, o endereço é
apenas ilustrativo. O definirá o endereço de memória onde a variável ficará
vinculada. O programa é uma sequência de caracteres que descreve a
lógica do programa em um formato textual.

Assentado diretamente na cadeira na sua camada mais inferior temos o valor


formado por , que neste exemplo é .
A camada logo acima desta camada física indica a forma como a camada binária
deve ser interpretada. O tipo foi utilizado como modelo do assento da cadeira.
Isto indica que um observador externo quando se referir a cadeira poderá interpretar
o binário como um inteiro com sinal. A instrução determinou que fosse atribuído o

Introdução à Programação: Conceitos e Práticas. Página 102


valor à variável . Logo, será este valor que será compreendido pelo observador
externo.

A atribuição, por ser um operador da linguagem, gera um resultado, que é o valor


atribuído. Assim, a expressão tem como resultado o valor . Esta
abordagem permite uma expressão com vários operadores de atribuição. Por
exemplo:

int main () {
int x, y, z; A expressão irá fazer com o valor
x = y = z = 0;
seja atribuído a variável , em seguida a e a .

É importante observar que a referência a um nome de variável, em uma expressão


com atribuição, possui significados distintos conforme a variável apareça no lado
esquerdo ou no lado direito do operador de atribuição. Na expressão a
referência a no lado direito da atribuição é substituída pelo de . Já a
referência a no lado esquerdo é interpretada como o de . Assim, esta
expressão pode ser descrita como: Armazenar no endereço de memória
representado por o valor que está armazenado no endereço de memória
representado por .

Introdução à Programação: Conceitos e Práticas. Página 103


16. Expressões Numéricas

As expressões numéricas combinam constantes, variáveis, operadores e funções a


fim de obter um novo valor calculado. A sintaxe da linguagem de programação deve
ser entendida para que uma expressão escrita em notação matemática normal seja
corretamente convertida para a forma computacional. Alguns exemplos de
expressões que serão reescritas em :

16.1. Operadores Matemáticos

Vamos ampliar a capacidade de manipular dados através dos operadores


aritméticos, aplicados a um ou dois operandos para produzir um resultado.

Operador Operação Observação


Binário
Soma Os operandos podem ser expressões com inteiro ou real. Se
Subtração ambos os operandos forem inteiros o resultado será inteiro. Se
Multiplicação um dos operandos for real, então o resultado será real.
Divisão O resultado será um número real se qualquer um dos operandos
for real.
Divisão Inteira Operandos e resultado são inteiros. Retorna o quociente da
divisão.
Resto Operandos e resultado são inteiros. Retorna o resto da divisão.

Os operadores e também podem ser aplicados a apenas um operando (unário),


passando a ter os seguintes significados:

Operador Operação Observação


Unário
Identidade Expressão retorna o mesmo valor de Expressão.
Inversão Expressão retorna o valor de Expressão com sinal trocado.

Quando estiverem presentes em uma expressão vários operadores, a linguagem


executa primeiramente o operador com maior nível de precedência. Os operadores
e tem precedência de execução em relação a e .

Introdução à Programação: Conceitos e Práticas. Página 104


A seguinte tabela relaciona os operadores, as respectivas precedências e critério de
associatividade:

Prece- Categoria Operador Descrição Associa-


dência tividade
1 Pós-fixo Incremento e decremento Sufixado/pós-fixado Esq. p/
Chamada de Função Dir.
[] Índice de Array
Acesso a membro de uma Struct ou Union
Acesso a membro de uma Struct ou Union com
ponteiro
2 Unário Incremento e decremento prefixado Dir. p/
Mais e menos unários Esq.
NOT Lógico e NOT Binário
Type cast – conversão de tipo
Indireção – derefêrencia
Endereço de variável
Tamanho de tipo/valor
3 Multiplicação */% Multiplicação, divisão e resto Esq. p/
Dir.
4 Adição Soma e subtração Esq. p/
Dir.
5 Desloca- Deslocamento de bits para esquerda e direita Esq. p/
mento Dir.
6 Relacional Operadores relacionais e Esq. p/
Operadores relacionais e Dir.
7 Igualdade Operadores relacionais e Esq. p/
Dir.
8 AND Binário AND Binário Esq. p/
Dir.
9 XOR Binário XOR Binário (OR exclusivo) Esq. p/
Dir.
10 OR Binário OR Binário (OR inclusivo) Esq. p/
Dir.
11 AND Lógico AND Lógico Esq. p/
Dir.
12 OR Lógico OR Lógico Esq. p/
Dir.
13 Condicional Condicional ternário Dir. p/
Esq.
14 Atribuição Atribuição simples Dir. p/
Atribuição acumulando soma e subtração Esq.
Atribuição acumulando produto, quociente e resto
Atribuição acumulando deslocamento a esq/dir
Atribuição acumulando AND, XOR e OR binário
15 Vírgula Vírgula Esq. p/
Dir.

Figura 47: Precedência de Operadores8,9.

Considere a seguinte expressão:

8
https://en.cppreference.com/w/c/language/operator_precedence, acessado em 05-mar-2019 as 12:03
9
https://www.tutorialspoint.com/cprogramming/c_operators_precedence.htm, acessado em 05-mar-2019 as
12:15

Introdução à Programação: Conceitos e Práticas. Página 105


A operação de multiplicação é executada primeiro e depois a soma. Caso queira
efetuar primeiro a soma então se deve fazer uso de parêntesis, como na expressão:

Neste outro exemplo:

quando escrita em tem-se:

Foram utilizados dois níveis de parêntesis a fim de agrupar adequadamente as


operações. Somente parêntesis podem ser usados para agrupar as operações. Os
símbolos que estamos acostumados na Álgebra tradicional [ { } ] não possuem
esta função em .

A associatividade serve para compreender a ordem das operação quando uma


expressão é composta por vários operadores de mesma precedência. Por exemplo:

A tabela de precedência informa que o operador segue a regra de associatividade


da esquerda para a direita. Isto significa que primeiro irá efetuar , para então
computar .

Ao longo deste material serão explanados quase todos os operadores citados na


tabela.

Introdução à Programação: Conceitos e Práticas. Página 106


16.2. Funções Matemáticas

A linguagem disponibiliza várias funções aritméticas que podem ser utilizadas na


construção de expressões matemáticas. Para fazer uso da maioria destas funções
no programa é necessário incluir a biblioteca . Os exemplos podem ser
copiados e compilados como atividade prática. A função será detalhada na
sequência. No momento basta saber que esta função é responsável em apresentar
informações na tela do computador. Segue a relação de funções ordenadas por
relacionamento:

a)
b)
c)
d)
e)
f)
g)
h)
i)
j)
k)
l)
m)
n)
o)
p)
q)
r)
s)
t)
u)
v)
w)

Segue uma descrição resumida destas funções:

1)

Retorna o ângulo em radianos, cujo cosseno é o valor de . O resultado


estará no intervalo [ ].

Exemplo:

#include <stdio.h>
#include <math.h>

Introdução à Programação: Conceitos e Práticas. Página 107


int main () {
double ret;

ret = acos(-1);
printf("O valor de PI = %lf\n", ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de PI = 3.141593

2)

Retorna o ângulo em radianos, cujo seno é o valor de . O resultado estará


no intervalo [ ].

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

ret = asin(1.0);
printf("O valor de PI = %lf\n", 2*ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de PI = 3.141593

3)

Retorna o ângulo em radianos, cuja tangente é o valor de . O resultado


estará no intervalo [ ].

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

Introdução à Programação: Conceitos e Práticas. Página 108


ret = atan(1.0);
printf("O valor de PI = %lf\n", 4*ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de PI = 3.141593

4)

Retorna o ângulo em radianos, cuja tangente é dada por . O resultado


estará no intervalo [ ].

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

ret = atan2(0.0, -1.0);


printf("O valor de PI = %lf\n", ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de PI = 3.141593

5)

Retorna o cosseno de um ângulo em radianos. O resultado estará no


intervalo [ ].

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret, pi;

pi = acos (-1.0);
ret = cos(pi/3);
printf("O valor de cos(PI/3) = %lf\n", ret);

return 0;

Introdução à Programação: Conceitos e Práticas. Página 109


}

Após compilar e processar o resultado apresentado será:

O valor de cos(PI/3) = 0.500000

6)

Retorna o cosseno hiperbólico de um ângulo em radianos.

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

ret = cosh (1.0);


printf("O valor de cosh(1.0) = %lf\n", ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de cosh(1.0) = 1.543081

7)

Retorna o seno de um ângulo em radianos. O resultado estará no intervalo


[ ].

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret, pi;

pi = acos (-1.0);
ret = sin (pi/6);
printf("O valor de sin(PI/6) = %lf\n", ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

Introdução à Programação: Conceitos e Práticas. Página 110


O valor de sin(PI/6) = 0.500000

8)

Retorna o seno hiperbólico de um ângulo em radianos.

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

ret = sinh (1.0);


printf("O valor de sinh (1.0) = %lf\n", ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de sinh (1.0) = 1.175201

9)

Retorna a tangente hiperbólica de um ângulo em radianos.

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

ret = tanh (1.0);


printf("O valor de tanh (1.0) = %lf\n", ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

Introdução à Programação: Conceitos e Práticas. Página 111


O valor de tanh (1.0) = 0.761594

10)

Retorna o valor da constante elevado a potência .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret, euler;

euler = exp (1.0);


ret = (exp(2.0) – exp(-2.0))/2 ;
printf("O valor da constante e = %lf\n", euler);
printf("O valor de sinh (2.0) = %lf\n", ret);
printf("O valor de sinh (2.0) = %lf\n", sinh(2.0));

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor da constante e = 2.718282


O valor de sinh (2.0) = 3.626860
O valor de sinh (2.0) = 3.626860

11)

Um número real escrito na forma normalizada na base tem a seguinte


forma geral:

, onde

Dado um valor , o valor da é retornado como resultado da função


, enquanto que o é guardado no endereço indicado em
.

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double mantissa, x = 1.0/16; int expoente;

mantissa = frexp (x, &expoente);


printf("x = %f = %f * 2^%d\n", x, mantissa, expoente);

Introdução à Programação: Conceitos e Práticas. Página 112


return 0;
}

Após compilar e processar o resultado apresentado será:

x = 0.062500 = 0.500000 * 2^-3

12)

Retorna o resultado da seguinte operação.

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double x = 0.5, ret;
int n = 4;

ret = ldexp(x ,n);


printf("%f * 2^%d = %f\n", x, n, ret);
return 0;
}

Após compilar e processar o resultado apresentado será:

0.500000 * 2^4 = 8.000000

13)

Retorna o logaritmo natural de um número .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret;

ret = log (exp(3));


printf("O valor de ln (e^3) = %lf\n", ret);

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 113


Após compilar e processar o resultado apresentado será:

O valor de ln (e^3) = 3.000000

14)

Retorna o logaritmo na base de um número .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double ret, x = 0.0001;

ret = log10 (x);


printf("O valor de log10 (%g) = %lf\n", x, ret);

return 0;
}

Após compilar e processar o resultado apresentado será:

O valor de log10 (0.0001) = -4.000000

15)

A função separa um número real em suas partes inteira e fracionária.

Dado um valor real , o valor real equivalente a parte fracionária é retornado


como resultado da função , enquanto que o equivalente real da parte
é guardada no endereço indicado em .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double x = 123.456, parte_frac, parte_int;

parte_frac = modf(x ,&parte_int);


printf("%g = %g + %g\n", x, parte_int, parte_frac);
return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 114


Após compilar e processar o resultado apresentado será:

123.456 = 123 + 0.456

16)

A função retorna o valor de elevado a potência .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
printf("Raiz quadrada de 2 = %lf\n", pow(2, 1.0/2));
printf("Raiz cubica de 8 = %lf\n", pow(8, 1.0/3));
return 0;
}

Após compilar e processar o resultado apresentado será:

Raiz quadrada de 2 = 1.414214


Raiz cubica de 8 = 2.000000

17)

A função retorna a raíz quadrada de um número .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
printf("Raiz quadrada de 2 = %lf\n", sqrt(2));
printf("Raiz quadrada de 64 = %lf\n", sqrt(64));
return 0;
}

Após compilar e processar o resultado apresentado será:

Raiz quadrada de 2 = 1.414214


Raiz quadrada de 64 = 8.000000

Introdução à Programação: Conceitos e Práticas. Página 115


18)

A função retorna o valor inteiro imediante superior ou igual a .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
printf("ceil (-1.4) = %lf\n", ceil (-1.4));
printf("ceil (-1.5) = %lf\n", ceil (-1.5));
printf("ceil (-1.6) = %lf\n", ceil (-1.6));
printf("ceil (1.4) = %lf\n", ceil (1.4));
printf("ceil (1.5) = %lf\n", ceil (1.5));
printf("ceil (1.6) = %lf\n", ceil (1.6));

return 0;
}

Após compilar e processar o resultado apresentado será:

ceil (-1.4) = -1.000000


ceil (-1.5) = -1.000000
ceil (-1.6) = -1.000000
ceil (1.4) = 2.000000
ceil (1.5) = 2.000000
ceil (1.6) = 2.000000

19)

A função retorna o valor absoluto de ( ).


A função retorna o valor absoluto de ( ).

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double x = -11.11, y = 22.22;
int z = -123;

printf("O valor absoluto de %lf\t = %lf\n", x, fabs(x));


printf("O valor absoluto de %lf\t = %lf\n", y, fabs(y));
printf("O valor absoluto de %d\t = %d\n", z, abs (z));

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 116


Após compilar e processar o resultado apresentado será:

O valor absoluto de -11.110000 = 11.110000


O valor absoluto de 22.220000 = 22.220000
O valor absoluto de -123 = 123

20)

A função retorna o valor inteiro imediante inferior ou igual a .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
printf("floor (-1.4) = %lf\n", floor (-1.4));
printf("floor (-1.5) = %lf\n", floor (-1.5));
printf("floor (-1.6) = %lf\n", floor (-1.6));
printf("floor (1.4) = %lf\n", floor (1.4));
printf("floor (1.5) = %lf\n", floor (1.5));
printf("floor (1.6) = %lf\n", floor (1.6));

return 0;
}

Após compilar e processar o resultado apresentado será:

floor (-1.4) = -2.000000


floor (-1.5) = -2.000000
floor (-1.6) = -2.000000
floor (1.4) = 1.000000
floor (1.5) = 1.000000
floor (1.6) = 1.000000

21)

A função retorna o valor mais próximo de .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double x;
x = 7.3; printf("round (%5.1lf) = %5.1lf\n", x, round(x));
x = 7.5; printf("round (%5.1lf) = %5.1lf\n", x, round(x));
x = 10.5; printf("round (%5.1lf) = %5.1lf\n", x, round(x));
x = -10.5; printf("round (%5.1lf) = %5.1lf\n", x, round(x));

Introdução à Programação: Conceitos e Práticas. Página 117


x = -7.5; printf("round (%5.1lf) = %5.1lf\n", x, round(x));
x = -7.3; printf("round (%5.1lf) = %5.1lf\n", x, round(x));

return 0;
}

Após compilar e processar o resultado apresentado será:

round ( 7.3) = 7.0


round ( 7.5) = 8.0
round ( 10.5) = 11.0
round (-10.5) = -11.0
round ( -7.5) = -8.0
round ( -7.3) = -7.0

22)

A função retorna a parte inteira de .

Exemplo:

#include <stdio.h>
#include <math.h>

int main () {
double x;
x = 7.3; printf("trunc (%5.1lf) = %5.1lf\n", x, trunc(x));
x = 7.5; printf("trunc (%5.1lf) = %5.1lf\n", x, trunc(x));
x = 10.5; printf("trunc (%5.1lf) = %5.1lf\n", x, trunc(x));
x = -10.5; printf("trunc (%5.1lf) = %5.1lf\n", x, trunc(x));
x = -7.5; printf("trunc (%5.1lf) = %5.1lf\n", x, trunc(x));
x = -7.3; printf("trunc (%5.1lf) = %5.1lf\n", x, trunc(x));

return 0;
}

Após compilar e processar o resultado apresentado será:

trunc ( 7.3) = 7.0


trunc ( 7.5) = 7.0
trunc ( 10.5) = 10.0
trunc (-10.5) = -10.0
trunc ( -7.5) = -7.0
trunc ( -7.3) = -7.0

23)

A função retorna o resto da divisão de por .

Exemplo:

Introdução à Programação: Conceitos e Práticas. Página 118


#include <stdio.h>
#include <math.h>

int main () {
double x, y;
x = 7.5; y = 2.3;
printf("resto de %lf/%lf = %lf\n", x, y, fmod(x, y));
x = acos(-1.0); y = 1.0;
printf("resto de %lf/%lf = %lf\n", x, y, fmod(x, y));

return 0;
}

Após compilar e processar o resultado apresentado será:

resto de 7.500000/2.300000 = 0.600000


resto de 3.141593/1.000000 = 0.141593

Introdução à Programação: Conceitos e Práticas. Página 119


16.3. Exemplos

Os exemplos que seguem apresentam algumas expressões na forma usual e o


respectivo programa escrito em . Para cada um dos itens é apresentada a saída
esperada para o programa. Todos os programas devem prever os :

Equação Programa
#include <stdio.h>
#include <math.h>
int main () {
double x = 3, y = 3, z;

z = sqrt (x*x + y*y*y)/fabs(x+y);


printf("z = %lf\n", z);
i. return 0;
√ }
int main () {
double x = 3, y = 3, z;

z = sqrt (pow(x,2) + pow(y,3))/fabs(x+y);


printf("z = %lf\n", z);

return 0;
}
z = 1.000000
int main () {
double x = M_PI/2, z;
ii. z = (1 + sin(x))/(1 + cos(x));
printf("z = %lf\n", z);

return 0;
}
z = 2.000000
int main () {
double x = 2, z;
iii.
z = 1 + 1/x + 1/(x*x) + 1/(x*x*x) +
1/(x*x*x*x);
printf("z = %lf\n", z);

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 120


Que pode ser reescrita como: int main () {
double x = 2, y, z;

y = 1/x;
z = 1 + y*(1 + y*(1 + y*(1 + y)));
( ( ( ))) printf("z = %lf\n", z);

return 0;
}
z = 1.937500
int main () {
double x = 2, y = 1, z, w;
iv.
w = x/y;
( ) z = w - (x + pow(w,2))/(y - pow(w,2));
printf("z = %lf\n", z);
( )
return 0;
}
z = 4.000000
int main () {
double x = 25, z;

z = sqrt(M_PI +
v.
sqrt(exp(3) +
sqrt(4 +
sqrt(x)
√ √ √ √ )));
printf("z = %lf\n", z);

return 0;
}
z = 2.818924

Observe na expressão do item uma codificação alternativa a partir da reescrita


da expressão original. Além disso, uma variável auxiliar ( ) foi utilizada, o que evitou
a repetição de uma mesma operação ao longo da expressão.

Algumas variáveis foram inicializadas, a fim de permitir ter alguma informação


previsível apresentada na tela. Na sequência serão apresentadas os funções que
permitem incluir recursos de entrada e saída.

Introdução à Programação: Conceitos e Práticas. Página 121


17. Comandos de Entrada e Saída

A seguinte figura ilustra um programa em execução interagindo com o usuário


através dos dispositivos básicos de Entrada e Saída, que são o teclado e o monitor
de vídeo.

17.1. Entrada de Dados

A função permite que o programa obtenha dados digitados pelo usuário. Ao


processar o programa o sistema operacional abre uma janela denominada de
ambiente de execução. Quando o fluxo de execução do programa alcançar a linha
de código com a chamada à função o programa fica aguardando que o
usuário digite uma linha com os dados, conforme especificado, e pressione a tecla
. Em seguida, esta linha é interpretada e os dados são armazenados nas
variáveis (endereços de memória) indicadas.

A sintaxe do para leitura do dispositivo padrão de entrada de dados (teclado)


é:

O primeiro argumento é uma string contendo a especificação do formato dos dados


a serem lidos. Na sequência temos uma lista de argumentos com os endereços de
memória onde a função deverá armazenar os dados contidos na string
digitada.

Vamos reforçar alguns conceitos sobre string em a fim de fazer uso das funções
de entrada e saída. É necessário no momento compreender o uso de uma string
literal ou constante string, já mencionado anteriormente. É bastante simples: é uma
sequência de caracteres delimitados no início e no fim pelo símbolo aspas. É

Introdução à Programação: Conceitos e Práticas. Página 122


um texto entre aspas. Por exemplo: . Não confundir com o símbolo
apóstrofe usado para definir uma constante do tipo ou , como em ou

A string de formato conterá uma orientação de como extrair e converter as


informações da linha de texto digitada pelo usuário do programa. Por exemplo, se o
programador desejar que o usuário digite dois números inteiros decimal com
possibilidade de sinal, então ele deve indicar desta forma: O símbolo
seguido de um ou mais caracteres indica o que é esperado para aquela posição da
string de entrada. A indicação orienta o a extrair do texto digitado um
número inteiro em decimal para aquela posição relativa.

Logo após a string de formato vem uma lista com tantos endereços de memória
quantos forem os dados indicados na string de formato. Neste exemplo, usamos
uma string de formato indicando que serão fornecidos dois números inteiros em
decimal. Logo, deverão ser fornecidos dois endereços de memória separados por
vírgula.

O endereço de uma variável é aquele número que indicamos sob o assento da


cadeira, no modelo de analogia variável vs cadeira. Este endereço pode ser obtido
por meio de uma expressão usando o operador seguido do nome da variável.

Considere as seguintes variáveis e a associação com as cadeiras:

Assim, se desejarmos que o usuário forneça dois números inteiros, a fim de


armazená-los nas variáveis e , teríamos que escrever a seguinte linha de cógigo:

Ao executar esta linha de código, o cursor ficará posicionado na janela de execução


aguardando que o usuário digite um texto com o formato indicado: dois números
inteiros em decimal e separados por um ou mais espaços em branco. Vamos
assumir que no momento que foi executado, o usuário digitou o seguinte texto,
seguido da tecla :

35 56

Introdução à Programação: Conceitos e Práticas. Página 123


A função efetuará a seguinte lógica: comparará o texto digitado com a string
de formato indicada no primeiro argumento e montará os números, caractere a
caractere, até encontrar um caractere separador. Neste caso, o espaço em branco é
o separador dos números.

Há uma extensa possibilidade de formatos, sendo as mais comuns estas que


seguem:

Formato Descrição
Inteiro decimal com sinal
Inteiro decimal sem sinal
Inteiro – decimal, octal ou hexa
Inteiro octal
ou Inteiro hexa
Float
Double
Float com notação de engenharia. Ex. ou
Um caractere
Uma string, sequência de até encontrar um separador

A função também retorna a quantidade de dados lidos. Considere o seguinte


código:

#include <stdio.h>
#include <math.h>

int main () {
int x, y, z, n;
n = scanf ("%d %d",&x,&y);
z = x + y;
printf ("foram fornecidos %d dados\n", n);
printf ("O valor de %d + %d = %d\n", x, y, z);

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 124


A expressão utiliza o resultado da função e
armazena na variável . Na sequência o valor de é apresentado na tela com o
. Após compilar e processar o resultado apresentado será:

35 56
foram fornecidos 2 dados
O valor de 35 + 56 = 91

A primeira linha corresponde ao texto digitado pelo usuário e as duas seguintes


foram produzidas pelos dois . Vamos analisar um segundo caso, onde não
tenha sido digitado nenhum texto e a entrada de dados tenha sido interrompida pelo
usuário através da tecla ( , seguido da tecla . Nesta situação
teremos a seguinte saída:

^Z
foram fornecidos -1 dados
O valor de 0 + 4202064 = 4202064

A primeira linha corresponde ao texto digitado pelo usuário, que neste caso não
forneceu nenhum número e teclou ( seguido de ). Assim, o
retorna , indicando que a digitação foi encerrada pelo usuário sem o
fornecimento de dado algum. Observe que as variáveis e ficaram com resquícios
da memória, comumente chamado de .

Vamos explorar mais um caso, onde o usuário digita um número e interrompe a


entrada teclando ( seguido de ). Neste caso teremos a
seguinte saída:

35^Z
foram fornecidos 1 dados
O valor de 35 + 4202064 = 4202099

Fica a cargo do leitor interpretar a saída apresentada.

Introdução à Programação: Conceitos e Práticas. Página 125


17.2. Saída de Dados

A função permite que o programa escreva um ou mais valores na tela do


computador. Ao processar o programa o sistema operacional abre um ambiente de
execução com sua respectiva janela. Ao executar a função o programa
escreve a partir da posição atual do cursor os valores indicados nos argumentos..

A sintaxe do para escrita na tela é:

O primeiro argumento é uma string contendo a especificação de como deve ser


composto a string que será impressa na tela. Na sequência temos uma lista de
valores que combinados com a string de formato resulta no texto a ser impresso.

Complementando o exemplo anterior, temos o seguinte código:

#include <stdio.h>
#include <math.h>

int main () {
int x, y, z;
scanf ("%d %d",&x,&y);
z = x + y;
printf ("O valor de %d + %d = %d\n",x, y, z);

return 0;
}

Após compilar e processar o resultado apresentado será:

35 56
O valor de 35 + 56 = 91

O diagrama que segue apresenta os efeitos de cada uma das três linhas de códigos.
Na primeira linha, a função aguarda a digitação de um texto composto por
dois números inteiros em decimal. O texto fornecido é interpretado pela função
, de acordo com a orientação dada pela string de formato, e dois números
inteiros são extraídos, sendo que o primeiro é armazenado no endereço associado
a variável e o segundo no endereço associado a variável .

Introdução à Programação: Conceitos e Práticas. Página 126


A segunda linha computa a expressão , levando em conta os valores
contidos nestas variáveis, e armazena o resultado em .

Finalmente, a terceira linha com a função é responsável por apresentar um


texto na tela. Este texto terá a estrutura da string de formato, onde haverá a
substituição dos três pelos valores das três expressões indicadas nos
argumentos seguintes: , e . Uma vez feitas as substituições, obtém-se o texto
final , que então é escrito na janela de execução a partir
do ponto em que o cursor está posicionado.

A função retorna a quantidade de caracteres processados. Observe o


seguinte exemplo:

#include <stdio.h>

int main () {

printf ("%d", printf ("ola mundo\n"));

return 0;
}

Após compilar e processar o resultado apresentado será:

ola mundo
10

A chamada escreve no dispositivo de saída o texto indicado


e posiciona o cursor na próxima linha. Ao todo são processados caracteres. A
chamada mais externa escreve este resultado na tela.

A lista de possibilidades de fomatos é bastante extensa. Segue uma relação das


principais.

Introdução à Programação: Conceitos e Práticas. Página 127


Formato Descrição/Exemplo
Inteiro decimal com sinal
Inteiro decimal sem sinal
Inteiro – decimal, octal ou hexa
Inteiro octal
ou Inteiro hexa
Float
Double
Float com notação de engenharia. Ex. ou
Um caractere
Uma string, sequencia de até encontrar um separador

Vamos utilizar o para explorar algumas caracteríscas e limites da linguagem,


por exemplo a quantidade de bytes necessária para o tipo , o maior valor possível
para um , e assim por diante. Seguem alguns programas que apresentam estes
valores:

O programa que segue explora os tamanhos dos tipos básicos da linguagem para o
compilado em uso. Os valores podem alterar conforme a plataforma utilizada.

#include <stdio.h>
#include <math.h>

int main () {

printf ("sizeof (char) = %d\n", sizeof (char));


printf ("sizeof (int) = %d\n", sizeof (int));
printf ("sizeof (float) = %d\n", sizeof (float));
printf ("sizeof (double) = %d\n", sizeof (double));
printf ("sizeof (void) = %d\n", sizeof (void));
printf ("sizeof (char *) = %d\n", sizeof (char *));
printf ("sizeof (long int) = %d\n", sizeof (long int));
printf ("sizeof (long long int) = %d\n", sizeof (long long int));
printf ("sizeof (long double) = %d\n", sizeof (long double));

return 0;
}

Após compilar e processar o resultado apresentado será:

sizeof (char) = 1
sizeof (int) = 4
sizeof (float) = 4
sizeof (double) = 8
sizeof (void) = 1
sizeof (char *) = 4
sizeof (long int) = 4
sizeof (long long int) = 8
sizeof (long double) = 12

Introdução à Programação: Conceitos e Práticas. Página 128


O programa que segue explora os maiores valores positivo e negativo para ,
maior para o , bem como os máximos positivo e negativo, e o menor
valor para e . Estas constantes estão definidas em e
. Os valores podem alterar conforme a plataforma utilizada.

#include <stdio.h>
#include <float.h>
#include <math.h>
#include <limits.h>

int main(void)
{
printf("INT_MIN = %+d\n", INT_MIN);
printf("INT_MAX = %+d\n", INT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
printf("\n");

printf("LONG_MIN = %+ld\n", LONG_MIN);


printf("LONG_MAX = %+ld\n", LONG_MAX);
printf("ULONG_MAX = %lu\n", ULONG_MAX);
printf("\n");

printf("FLT_MIN = %e\n", FLT_MIN);


printf("FLT_MAX = %e\n", FLT_MAX);
printf("FLT_EPSILON = %e\n", FLT_EPSILON);
printf("\n");

printf("DBL_MIN = %e\n", DBL_MIN);


printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);

return 0;
}

Após compilar e processar o resultado apresentado será:

INT_MIN = -2147483648
INT_MAX = +2147483647
UINT_MAX = 4294967295

LONG_MIN = -2147483648
LONG_MAX = +2147483647
ULONG_MAX = 4294967295

FLT_MIN = 1.175494e-038
FLT_MAX = 3.402823e+038
FLT_EPSILON = 1.192093e-007

DBL_MIN = 2.225074e-308
DBL_MAX = 1.797693e+308
DBL_EPSILON = 2.220446e-016

Observe que o formato expressa um em decimal.

Introdução à Programação: Conceitos e Práticas. Página 129


No compilador utilizado para este programa, o tipo não correspondeu
a expectativa, que era apresentar os limites conforme tabela de tipo apresentada
anteriormente. Assim, foi testada uma versão semelhante, porém em para o
mesmo compilador, com as devidas adaptações dos nomes das constantes. Segue
o código fonte alterado:

#include <iostream>
#include <limits>

int main(void)
{
printf("INT_MIN = %+d\n", INT_MIN);
printf("INT_MAX = %+d\n", INT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
printf("\n");

printf("LONG_MIN = %+ld\n", LONG_MIN);


printf("LONG_MAX = %+ld\n", LONG_MAX);
printf("ULONG_MAX = %lu\n", ULONG_MAX);
printf("\n");

printf("FLT_MIN = %e\n", __FLT_MIN__);


printf("FLT_MAX = %e\n", __FLT_MAX__);
printf("FLT_EPSILON = %e\n", __FLT_EPSILON__);
printf("\n");

printf("DBL_MIN = %e\n", __DBL_MIN__);


printf("DBL_MAX = %e\n", __DBL_MAX__);
printf("DBL_EPSILON = %e\n", __DBL_EPSILON__);
printf("\n");

printf("LDBL_MIN = %Le\n", __LDBL_MIN__);


printf("LDBL_MAX = %Le\n", __LDBL_MAX__);
printf("LDBL_EPSILON = %Le\n", __LDBL_EPSILON__);

return 0;
}

Após compilar e processar o resultado apresentado será:

INT_MIN = -2147483648
INT_MAX = +2147483647
UINT_MAX = 4294967295

LONG_MIN = -2147483648
LONG_MAX = +2147483647
ULONG_MAX = 4294967295

FLT_MIN = 1.175494e-038
FLT_MAX = 3.402823e+038
FLT_EPSILON = 1.192093e-007

DBL_MIN = 2.225074e-308
DBL_MAX = 1.797693e+308
DBL_EPSILON = 2.220446e-016

Introdução à Programação: Conceitos e Práticas. Página 130


LDBL_MIN = 3.362103e-4932
LDBL_MAX = 1.189731e+4932
LDBL_EPSILON = 1.084202e-019

17.3. Exemplos

i. Elaborar um programa que leia dados como: Peso, Altura e Sexo; e calcule o Índice
de Massa Corpórea.

#include <stdio.h>
#include <math.h>

int main () {
double peso, altura, imc;
char sexo;

printf ("Entre com o sexo (M ou F): ");


scanf ("%c", &sexo);
printf ("Entre com o peso: ");
scanf ("%lf", &peso);
printf ("Entre com a altura: ");
scanf ("%lf", &altura);

imc = peso / (altura * altura);

printf ("O seu Indice de Massa Corporea = %6.2lf\n", imc);

return 0;
}

Após compilar e processar o resultado apresentado será:

Entre com o sexo (M ou F): M


Entre com o peso: 80
Entre com a altura: 1.76
O seu Indice de Massa Corporea = 25.83

O programa inicia com a declaração das variáveis. São utilizadas três variáveis do
tipo para representar as grandezas peso, altura e o imc. Uma variável do tipo
é reservada para representar o sexo.

A linha de código apresenta a mensagem


na tela, mantendo o cursor ao final do texto.

Na sequência temos a linha de código que aguarda o usuário


digitar um texto contendo um caractere, seguido de . Este texto é processado
pelo , que irá extrair o caractere ( ) e armazená-lo no endereço de memória
indicado ( ).

Introdução à Programação: Conceitos e Práticas. Página 131


Linhas de código semelhantes são feitas para o e . Com as variáveis
inicializadas com os dados fornecidos pelo usuário, o programa passa então para o
cálculo do , que é feito aplicando a expressão , que na
sintaxe é . O quadrado da altura foi obtido com o
operador multiplicação, em vez de usar .

Por fim, o valor do é apresentado na tela através da linha de código


. O valor da variável
é escrito com o formato indicado que significa formato com seis
caractere, sendo dois deles para as casas decimais, o que resulta em:

0 2 5 . 8 3

Introdução à Programação: Conceitos e Práticas. Página 132


18. Implementando Funções

Com os recursos já apresentados é possível construirmos nossas próprias funções


em complemento às disponibilizadas com a linguagem. O uso de funções é
fundamental tanto para a modularização dos programas quanto para facilitar a
reutilização de códigos já escritos. Assim, ao depararmos com um problema
devemos pensar na forma de decompô-lo em unidades funcionais simples, de modo
que o programa final trata apenas de integrá-los adequadamente.

No exemplo anterior, onde é calculado o – a partir do


peso e altura de uma pessoa, o conceito de função permite que escondamos os
cálculos atrás de um nome, complementado com os argumentos necessários à
realização das contas. Deste modo, sempre que for preciso calcular este , basta
referenciar este nome com seus respectivos argumentos.

A forma geral de uma função é:

A seguinte figura ilustra os principais elementos e conceitos relacionados com


funções:

Figura 48: Estrutura de uma função.

No exemplo que segue serão feitas associações entre o código em e os conceitos


apresentados nesta figura.

Introdução à Programação: Conceitos e Práticas. Página 133


18.1. Exemplos

O programa e a função para cálculo do poderiam adquirir a seguinte estrutura:

#include <stdio.h>
#include <math.h>

double getimc (double Peso, double Altura) {


double IMC;

IMC = Peso / (Altura * Altura);

return IMC;
};

int main () {
double peso, altura, imc;

printf ("Entre com o peso: "); scanf ("%lf", &peso);


printf ("Entre com a altura: "); scanf ("%lf", &altura);

imc = getimc (peso, altura);


printf ("O seu Indice de Massa Corporea = %6.2lf\n", imc);

return 0;
}

Por conveniência, retiramos a entrada do dados sobre o sexo da pessoa.


Adicionalmente, o layout do código foi alterado, escrevendo dois comandos em
sequencia na mesma linha de código. O passo a passo deste programa, iniciando
pelo programa principal, é dado por:

1)

Escrever na tela o texto .

2)

Aguardar a digitação de uma linha até que seja teclado , para em


seguida extrair a informação desejada e armazenar na memória. É esperado
que o texto digitado contenha um número real compatível com o tipo .
O valor extraído do texto de entrada será armazenado no endereço de
memória correspondente a variável .

3)

Estas duas linhas seguem a lógica já comentada, porém aplicável ao dado de


.

4)

É feita a para executar os cálculos com os


e . Estes são copiados para os

Introdução à Programação: Conceitos e Práticas. Página 134


definidos no . Neste ponto, a execução do
programa passa o controle para a função que ao terminar,
para o , que copia o da
para a variável .

5)

Escrever uma linha na tela, composta pelo texto


, seguido pelo valor de formatado com
seis posições sendo duas casas decimais.

Após compilar e processar o resultado apresentado será:

Entre com o peso: 80


Entre com a altura: 1.76
O seu Indice de Massa Corporea = 25.83

A estrutura da função ficou:

1)

É o onde são definidos: o


e os . Neste caso, temos:

a) Tipo do Resultado: Define que o será um .


b) Nome:
c) Parâmetros: são dois do tipo , onde:
 primeiro parâmetro chama-se e serve para receber o do
primeiro argumento;
 segundo parâmetro chama-se e e serve para receber o
do segundo argumento;

2) {

Início do .

3)

Cria uma para uso exclusivo no corpo da função, sendo do tipo


e nome .

4)

É uma lógica simples onde o cálculo é dado por . O


resultado é provisoriamente armazenado na variável local .

5)

Introdução à Programação: Conceitos e Práticas. Página 135


Antes de encerrar a lógica da função , o resultado, que está
armazenado provisoriamente na variável local , é copiado para um local
apropriado, onde a função chamadora possa acessar. Isto é feito com a
instrução seguida de uma expressão cujo valor corresponde ao
trabalho esperado para a função.

6) }

Por fim vem o símbolo que indica o fim do bloco de código que corresponde
ao .

O seguinte diagrama ilustra os elementos que formam a função , bem como o


uso desta função a partir da função .

Figura 49: Estrutura da função getimc.

É importante constatar que os parâmetros ( e ) e a variável local ( )


são visíveis apenas no corpo da função . A função organiza a ordem
com que a lógica principal será executada. É a partir da função que inicia o
processamento. Neste caso, a função possui dentro de seu escopo três
variáveis locais, que são elas: , e . Assim, caso o programador queira
atribuir um nome a uma variável local ou parâmetro que tenha o mesmo o nome de
outra variável local definida em uma segunda função, não haverá conflito algum. Isto
se deve ao fato de estarem em diferentes contextos.

Poderíamos dizer que o nome completo de uma variável ou parâmetro seria


composto de nome e sobrenome, onde o nome é o que está registrado na
declaração e o sobrenome é a identidade da função. Assim, teríamos a variável
e o parâmetro .

Introdução à Programação: Conceitos e Práticas. Página 136


A opção foi dar nomes diferentes usando uma característica da linguagem que é
fazer distinção entre maiúsculo e minúsculo ( ). Mas não teria problema
algum se tivéssemos optados em dar o mesmo nome, pois se trata de contextos
distintos.

18.2. Exercícios

i. Elaborar um programa que calcule a distância entre dois pontos dados pelas suas
coordenadas

#include <stdio.h>
#include <math.h>

double getDIST (double x1, double y1, double x2, double y2) {

return sqrt(pow(x2-x1, 2) + pow(y2-y1, 2));

};

int main () {
double D, Ax, Ay, Bx, By;

printf ("Entre com as coordenadas dos pontos:\n");


scanf ("%lf %lf %lf %lf", &Ax, &Ay, &Bx, &By);
D = getDIST(Ax, Ay, Bx, By);
printf ("Distancia = %.4lf\n", D);

return 0;
}

O programa principal implementa uma interface simples, onde é solicitado ao


usuário informar as coordenadas de dois pontos e . As coordenadas
informadas são armazenadas nas variáveis . Em seguida é atribuído à
variável o resultado de uma expressão que contém apenas a chamada a função
com os copiando os argumentos para os parâmetros. Na
execução da função os argumentos já estão copiados para os parâmetros
. A única linha de código faz efetivamente os cálculos (
√ ) para obtenção da distância.

Após compilar e processar o resultado apresentado será:

Entre com as coordenadas dos pontos:


3.5 4.5 6.5 8.5
Distancia = 5.0000

ii. Escrever uma function que receba dois números ( e ) e calcule o valor de .
Seria o equivalente ao papel da função .

#include <stdio.h>
#include <math.h>

double power (double X, double Y) {

Introdução à Programação: Conceitos e Práticas. Página 137


return exp (Y*log(X));
};

int main () {
double x = 2, y = 0.5;
printf ("pow (%lf, %lf) = %.15lf\n", x, y, pow (x, y));
printf ("power (%lf, %lf) = %.15lf", x, y, power (x, y));
return 0;
}

Vamos implementar uma forma de calcular e comparar com o resultado


com o valor fornecido pela função da biblioteca . Para isto,
faremos uma transformação da expressão para fazer uso das funções
que corresponde a , e que corresponde ao logaritmo
neperiano ou natural. Assim, lançaremos mão das seguintes transformações
até que se chegue a uma combinação destas funções.

( )

A expressão final utiliza somente as funções e disponíveis no e a


função codifica esta lógica.

Após compilar e processar o resultado apresentado será:

pow (2.000000, 0.500000) = 1.414213562373095


power (2.000000, 0.500000) = 1.414213562373095

Observe que o formato usado para apresentar o resultado é ,


produzindo uma saída com quinze casas decimais. A precisão alcançada pela
função , neste exemplo, equivale a da função .

iii. Escrever uma function que calcule a média aritmética entre dois números:

#include <stdio.h>
#include <math.h>

double media (double a, double b) {


return (a+b)/2;
};

Introdução à Programação: Conceitos e Práticas. Página 138


int main () {
double x, y, z;
printf ("entre com dois numeros\n");
scanf ("%lf %lf", &x, &y);
z = media (x, y);
printf ("media (%lf, %lf) = %lf", x, y, z);

return 0;
}

Especificamente para a linha os e são


copiados para os e ; a função é executada e o
resultado final é copiado para a variável .

Após compilar e processar o resultado apresentado será:

entre com dois numeros


2.46 6.54
media (2.460000, 6.540000) = 4.500000

iv. Escrever uma function que calcule a média aritmética entre quatro números:

#include <stdio.h>
#include <math.h>

double media (double a, double b) {


return (a+b)/2;
};

double media4 (double a, double b, double c, double d) {


return media (media(a, b), media (c, d));
};

int main () {
double x, y, u, v, z;
printf ("entre com quatro numeros\n");
scanf ("%lf %lf %lf %lf", &x, &y, &u, &v);
z = media4 (x, y, u, v);
printf ("media (%lf, %lf, %lf, %lf) = %lf", x, y, u, v, z);

return 0;
}

A solução adotada para a média aritmética de quatro números foi o de


aproveitar a função já pronta que calcula a média de dois números, ou seja:

Introdução à Programação: Conceitos e Práticas. Página 139


( )

Após compilar e processar o resultado apresentado será:

entre com quatro numeros


2.4 3.6 4.8 3.2
media (2.400000, 3.600000, 4.800000, 3.200000) = 3.500000

v. Escrever uma function que calcule o seno de um ângulo utilizando a seguinte série:

Vamos limitar a série aos cinco primeiros termos, pois não temos recursos
adequados para ir muito além de forma estruturada.

#include <stdio.h>
#include <math.h>

double torad (double graus) {


return graus*M_PI/180;
};

double seno (double rad) {

return rad
- pow(rad, 3)/(3*2)
+ pow(rad, 5)/(5*4*3*2)
- pow(rad, 7)/(7*6*5*4*3*2)
+ pow(rad, 9)/(9*8*7*6*5*4*3*2);
};

int main () {
double ang_g, ang_r, y1, y2;

printf ("Entre com um angulo em graus\n");


scanf ("%lf", &ang_g);
ang_r = torad (ang_g);
y1 = sin (ang_r);
y2 = seno (ang_r);
printf ("sin (%lf) = %.14lf\n", ang_g, y1);
printf ("seno (%lf) = %.14lf\n", ang_g, y2);

return 0;

Introdução à Programação: Conceitos e Práticas. Página 140


}

Para calcular o pela série dada, o ângulo deve estar em radianos. O


programa principal, que testará a lógica, solicitará ao usuário que forneça um
ângulo para o cálculo do . Não é razoável que o programa requeira a
entrada em radianos, uma vez que não é a forma mais direta de
compreensão.

Assim, o programa solicita a entrada de um número que representa o ângulo


em graus e armazena na variável . Em seguida, uma expressão
invocando a função converte o ângulo para radianos e armazena o
resultado na variável . As duas linhas que seguem calculam o pela
função da biblioteca ( ) e pela função implementada ( ). Na sequência
os resultados são apresentados na tela. Observe a interface abaixo com três
execuções seguidas, para os ângulos e graus.

Entre com um angulo em graus


30
sin (30.000000) = 0.50000000000000
seno (30.000000) = 0.50000000002028

Entre com um angulo em graus


60
sin (60.000000) = 0.86602540378444
seno (60.000000) = 0.86602544509978

Entre com um angulo em graus


1110
sin (1110.000000) = 0.50000000000000
seno (1110.000000) = 877678.04339667060000

Podemos observar que os resultados para e graus apresentam uma


boa precisão, mesmo com a grande simplificação feita nos cálculos. Porém,
para o ângulo de graus, cujo seno deveria ser o mesmo que o seno de
graus tem-se uma discrepância alarmante. Isto se deve ao fato de que a
série possui termos do tipo . No caso de temos uma convergência
rápida da série. Quando o valor de for muito grande os cálculos divergem e
levam a valores inconsistentes como o visto acima. A mistura de operações
com números muito grandes/pequenos em uma mesma expressão pode levar
a resultados inconsistentes. Neste caso, iremos contornar parte do problema,
reduzindo o ângulo para os quadrantes iniciais através da seguinte operação
com o ângulo em radianos:

( )

A parte fracionária do número real pode ser obtida aplicando a função


que retorna o resto da divisão entre dois números reais. Assim, dado um
número real , podemos obter a parte fracionária com a expressão
. Neste caso, fazemos ser a divisão do ângulo por , e ficamos
apenas com a parte fracionária, descartando a parte inteira, que representa o

Introdução à Programação: Conceitos e Práticas. Página 141


número de voltas no círculo trigonométrico. A parte fracionária multiplicada
novamente por resulta no ângulo reduzido à primeira volta.

A seguinte solução acrescenta a função que faz a redução do ângulo


( ) e inclui este cálculo ( ) na função :

#include <stdio.h>
#include <math.h>

double reduzir (double rad) {


return 2 * M_PI * fmod (rad/(2 * M_PI), 1);
}

double torad (double graus) {


return graus*M_PI/180;
};

double seno (double rad) {


rad = reduzir (rad);
return rad
- pow(rad, 3)/(3*2)
+ pow(rad, 5)/(5*4*3*2)
- pow(rad, 7)/(7*6*5*4*3*2)
+ pow(rad, 9)/(9*8*7*6*5*4*3*2);
};

int main () {
double ang_g, ang_r, y1, y2;

printf ("Entre com um angulo em graus\n");


scanf ("%lf", &ang_g);
ang_r = torad (ang_g);
y1 = sin (ang_r);
y2 = seno (ang_r);
printf ("sin (%lf) = %.14lf\n", ang_g, y1);
printf ("seno (%lf) = %.14lf\n", ang_g, y2);

return 0;
}

Processando novamente este programa para as entradas de dados para os


ângulos e graus tem-se:

Introdução à Programação: Conceitos e Práticas. Página 142


Entre com um angulo em graus
30
sin (30.000000) = 0.50000000000000
seno (30.000000) = 0.50000000002028

Entre com um angulo em graus


60
sin (60.000000) = 0.86602540378444
seno (60.000000) = 0.86602544509978

Entre com um angulo em graus


1110
sin (1110.000000) = 0.50000000000000
seno (1110.000000) = 0.50000000002028

O seno de graus pela função ficou mais adequado e igual ao


de graus.

Mesmo com todos estes ajustes, esta forma de implementação da lógica do


ainda é muito primitiva e será consideravelmente melhorada na medida
em que forem conhecidos outros recursos da linguagem.

Introdução à Programação: Conceitos e Práticas. Página 143


19. Expressões Lógicas

Expressão lógica é um recurso fundamental nas linguagens de programação, pois é


elemento presente na construção de qualquer algoritmo significativo. A linguagem
não oferece um tipo exclusivo para dados lógicos. É comum em outras linguagens
dispor de um tipo de dado para representar dados que se limitem à dois
valores possíveis, como e .

No caso da Linguagem os tipos inteiros são utilizados para desempenharem o


papel da família do valores lógicos ou booleanos. O inteiro zero ( ) equivale ao
, enquanto que o um ( ) ou qualquer valor diferente de zero ( ) faz o papel
de .

Os operadores relacionais podem ser utilizados para gerar valores lógicos ou , a


partir de comparações. Valores lógicos podem ser combinados através dos
operadores lógicos para formarem expressões lógicas. Os operadores lógicos
trabalham sobre os valores e , que representam condições ou ,
produzindo resultados deste mesmo conjunto.

19.1. Operadores Relacionais

Os operadores relacionais funcionam como se fossem perguntas feitas sobre seus


operandos a fim de produzir respostas do tipo ( ) ou ( ). A sintaxe
destes operadores demanda a presença de dois operandos, sendo um à esquerda e
outro à direita do operador. Os operandos podem ser expressões de qualquer tipo,
porém compatíveis entre si. Assim, se um operando produz um valor numérico
( , ou ), o outro também deverá ser do tipo numérico. Os operadores
relacionais do são: e , cuja descrição é:

Operador Relacional Exemplos


i. #include <stdio.h>

int main( )
Avalia se o valor do primeiro argumento é {
ao valor do segundo argumento. float x = 3.14;
Aplica-se tanto a valores numéricos e int a = 100;
ponteiros. char c = 'b';
ii. int z;

z = (c > 'd'); printf ("%d\n", z);


Avalia se o valor do primeiro argumento é
z = (a == 0100); printf ("%d\n", z);
que o valor do segundo argumento. z = (a == 0x100); printf ("%d\n", z);
No caso de argumentos o z = (a == 0x64); printf ("%d\n", z);
entendimento é o que normalmente z = (a == 'd'); printf ("%d\n", z);
utilizamos em matemática. Quando os z = (x >= 3); printf ("%d\n", z);
argumentos forem , o termo se z = (c != 'a'); printf ("%d\n", z);
aplica a posição do elemento na tabela z = (c == 97); printf ("%d\n", z);
. Um char é que outro quando z = (c <= 0x61); printf ("%d\n", z);
ocupa uma posição superior na tabela z = (x < a); printf ("%d\n", z);
. Se os argumentos forem , o z = (x > c); printf ("%d\n", z);
termo equivale a perguntar se o z = (c >= a - 3); printf ("%d\n", z);

Introdução à Programação: Conceitos e Práticas. Página 144


primeiro argumento vem após o segundo
argumento no caso de ordem alfabética, return 0;
também orientada pela tabela . }
iii. l)
Ao executar este programa, a seguinte tela
de saída será gerada decorrente do :
Explicação semelhante ao .
0
iv. 0
0
Explicação semelhante ao . 1
1
v. 1
1
0
Explicação semelhante ao . 0
1
vi. 0
1
Explicação semelhante ao

Na primeira linha de código temos . A variável foi declarada como


e iniciada com o número inteiro correspondente ao caractere na tabela
: Assim, a expressão terá como resultado a aplicação
do operador aos valores dos operandos. Como o conteúdo da variável é
e o valor da constante é , logo o resultado de produzirá o valor
( ), que será armazenado em . O uso dos parêntesis visa facilitar a leitura da
expressão. A linha poderia ser escrita como .

Nas três linhas seguintes temos testes sobre o valor da variável inteira , que foi
inicializada com o valor : .

O primeiro teste é ). Deve ser observado que a constante


expressa um número em octal, cujo valor decimal é o . Logo este teste fornecerá o
valor ( ).

O segundo teste é ). Deve ser observado que a constante


expressa um número em hexadecimal, cujo valor decimal é o . Logo este teste
fornecerá também o valor ( ).

No terceiro teste temos ). Deve ser observado que a constante


expressa um número em hexadecimal, cujo valor decimal é o . Logo este teste
fornecerá o valor ( ).

Na quarta avaliação de temos ). Deve ser observado que a constante


expressa um número interpretado de acordo com a tabela , cujo valor decimal
éo . Logo este teste fornecerá o valor ( ).

As demais expressões ficam a cargo do leitor as respectivas interpretações.

Introdução à Programação: Conceitos e Práticas. Página 145


19.2. Operadores Lógicos

A Álgebra de Boole possui vários operadores lógicos que se aplicam a dados


lógicos: ou , produzindo como resultado igualmente um valor ou . No caso da
linguagem , é importante fixar que o tipo lógico ou booleano é desempenhado
pelos inteiros. Seguem as operações lógicas e respectivas tabelas verdade:

Operadores Tabelas-Verdade
i. (AND)

O resultado é ( ) quando
todos os operandos forem 0 1 A B X
( ). Nas demais situações o 0 0 0 0 0 0
resultado é ( . 1 0 1 0 1 0
1 0 0
ii. 1 1 1
iii. (OR)

O resultado é ( quando 0 1 A B X
todos os operandos forem 0 0 1
. Nas demais situações 0 0 0
1 1 1 0 1 1
o resultado é ( .
1 0 1
1 1 1
iv. (XOR)

O resultado é ( quando
um e apenas um dos operandos 0 1 A B X
for Nas demais 0 0 1 0 0 0
situações o resultado é 1 1 0 0 1 1
( .
1 0 1
1 1 0

v. (NOT)
! 0 1
O resultado é a negação do 1 0 A X
valor do operando. 0 1
1 0

O operador também é chamado de e o operador de


. Esta denominação permite compreender melhor a prioridade na
execução das operações. O operador é executado antes que o operador
quando estiverem no mesmo nível na expressão. O parêntesis deve ser utilizado
quando se deseja priorizar outra ordem.

Está claro que codificar expressões lógicas operando somente sobre constantes
ou não permitirá construir programas práticos. Assim, normalmente utilizamos os

Introdução à Programação: Conceitos e Práticas. Página 146


para produzir os valores ou que necessitamos em uma
expressão lógica.

19.3. Exemplos

i. Elaborar um programa que informa se um determinado caractere é uma vogal.

A solução proposta utiliza uma função chamada que avalia se o caractere


dado é ou não uma vogal. O programa principal serve para testar alguns casos com
o objetivo de validar a lógica. A função pode ser codificada de várias
maneiras, utilizando apenas os recursos já apresentados.

#include <stdio.h>
#include <ctype.h>

int isvogal (char ch) {


ch = toupper (ch);
return (ch == 'A') || (ch == 'E') ||
(ch == 'I') || (ch == 'O') ||
(ch == 'U');
}

int main( )
{

printf ("%d\n", isvogal('a'));


printf ("%d\n", isvogal('O'));
printf ("%d\n", isvogal('0'));
printf ("%d\n", isvogal('T'));
printf ("%d\n", isvogal('*'));

return 0;
}

Ao executar este programa, a seguinte tela de saída será gerada decorrente dos
:

1
1
0
0
0

A função utiliza uma expressão lógica completa composta por cinco


perguntas, sendo cada uma delas avaliando se o caractere é uma das vogais. Antes
de fazer o teste, o valor de é substituído pelo seu equivalente maiúsculo, através
da expressão: .

A função pertence à biblioteca e retorna o equivalente maiúsculo


que corresponde ao valor do argumento passado. Caso o argumento não seja um
caractere minúsculo, a função retorna uma cópia do argumento informado. De forma
semelhante está disponível a função . Segue o cabeçalho destas funções:

i.
Retorna a equivalente maiúscula de um caractere.

Introdução à Programação: Conceitos e Práticas. Página 147


ii.
Retorna a equivalente minúscula de um caractere.

Um principiante em programação normalmente pergunta se a expressão não poderia


ser escrita na forma:

A resposta é . Infelizmente este código é compilável em , porém não produz a


lógica esperada. Será feita a operação entre os valores numéricos dos caracteres.
Isto equivale a fazer o entre diversos valores ( ou ), o que resulta em
. E por fim o teste de igualdade entre o valor e o valor de .

ii. Elaborar um programa que informa se um determinado caractere é uma consoante.

A solução proposta utiliza duas funções de apoio, sendo uma a e outra a


que avalia se um caractere é ou não uma letra do alfabeto. Assim, um
caractere será uma consoante se for ao mesmo tempo uma letra do alfabeto e não
for vogal. O programa principal serve para testar alguns casos com o objetivo de
validar a lógica.

#include <stdio.h>
#include <ctype.h>

int isletra (char ch) {


ch = toupper (ch);
return (ch >= 'A') && (ch <= 'Z');
}

int isvogal (char ch) {


ch = toupper (ch);
return (ch == 'A') || (ch == 'E') ||
(ch == 'I') || (ch == 'O') ||
(ch == 'U');
}

int isconsoante (char ch) {


ch = toupper (ch);
return isletra(ch) && !isvogal(ch);
}

int main( )
{
printf ("%d\n", isconsoante('a'));
printf ("%d\n", isconsoante('O'));
printf ("%d\n", isconsoante('0'));
printf ("%d\n", isconsoante('T'));
printf ("%d\n", isconsoante('*'));

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 148


Ao executar este programa, a seguinte tela de saída será gerada decorrente dos
:

0
0
0
1
0

A função testa se um caractere está entre o e o na tabela . A


função combina adequadamente as funções e através
da expressão:

A função retornará ( ) quando retornar ( e


resultar também em ( , isto é, quando retornar
).

O único teste que resultou em foi o aplicado ao caractere .

A função poderia ser substituída pela equivalente disponível na


biblioteca , que possui a mesma funcionalidade. Outras funções desta
biblioteca úteis para o teste de valores são:

i.
Verifica se um cararactere é uma letra ou dígito.

ii.
Verifica se um cararactere é uma letra maiúscula ou minúscula.

iii.
Verifica se um cararactere é um dos caracteres de controle (código de
a ). Podem existir outros, dependendo da arquitetura.

iv.
Verifica se um cararactere é um dos dígitos númericos.

v.
Verifica se um cararactere possui representação gráfica.

vi.
Verifica se um cararactere é um dos caracteres de a '.

vii.
Verifica se um cararactere é passível de ser impresso.

viii.
Verifica se um cararactere é um caractere gráfico ( ) e não é um
caractere alfanumérico ( ).

ix.

Introdução à Programação: Conceitos e Práticas. Página 149


Verifica se um cararactere é um . Os seguintes
caracteres são considerados desta natureza:

' ' SPC (0x20) espaço


'\t' TAB (0x09) tab horizontal
'\n' LF (0x0a) nova linha
'\v' VT (0x0b) tab vertical
'\f' FF (0x0c) nova pagina
'\r' CR (0x0d) retorno de carro

x.
Verifica se um cararactere é um dos caracteres de a '.

xi.
Verifica se um cararactere é um dos símbolos utilizados na base hexadecimal
( a , a , e ).

iii. Elaborar um programa que informa o maior entre dois números inteiros.

A solução proposta utiliza o operador condicional ternário , cuja forma de uso é:

A é avaliada e caso o seu valor seja ( ) então o resultado


do operador é o valor da , caso contrário o resultado será o valor da
.

Segue uma lógica proposta:

#include <stdio.h>

int maior (int x, int y) {

return x > y ? x : y;

int main( )
{
int a, b, c;
scanf ("%d %d", &a, &b);
printf ("%d\n", maior (a, b));

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 150


A fim de favorecer a legibilidade foi utilizada uma função para encapsular o uso do
operador condicional ternário. A função recebe dois argumentos inteiros
através dos parâmetros e . Em relação a expressão , a expressão
teste é avaliada e caso a comparação seja então o operador resulta no
valor de , caso contrário resulta no valor de . Observe o seguinte diagrama:

Ao executar este programa, a seguinte tela de entrada e saída será gerada:

30 40
40

iv. Elaborar um programa que informa a quantidade de dias de um determinado mês e


ano.

A solução proposta utilizará o operador condicional ternário:

A solução consiste em retornar a quantidade de dias conforme o valor do parâmetro


mês. Para os meses e a quantidade de dias é ; para os meses
e a quantidade de dias é ; e para o mês a quantidade de dias é para
anos normais e para ano bissexto. Assim, a solução proposta inclui uma função
para indicar se um ano é bissexto ou não.

Em função das inúmeras alterações que o calendário foi submetido ao longo dos
tempos, acabou sendo consolidado o padrão solar. O ano solar, ou seja, o tempo
que a leva para completar um ciclo em torno do te a duração de
ou dias. Para sincronizar o calendário e o tempo solar
foi introduzido, pelos imperadores e , o ano bissexto para
compensar a fração de dia não considerada no total de dias do ano. Esta mudança
fez com que os anos que fossem múltiplos de 4 passassem a ter dias, e os
demais dias. Mesmo com esta alteração, a diferença entre a duração média do
novo calendário com dias e o valor real de conduzia a defasagem
significativa no longo prazo. Deste modo, o , em
, suprimiu dias do calendário e determinou que o dia
fosse seguido pelo dia . As regras para
foram ajustadas e ficou estabelecido que10:

 de em anos é ano bissexto.


 de em anos não é ano bissexto.
 de em anos é ano bissexto.
 prevalecem as últimas regras sobre as primeiras.

10
http://pt.wikipedia.org/wiki/Ano_bissexto
Leap Years: the rule. U.S. Naval Observatory (14 September 2007).

Introdução à Programação: Conceitos e Práticas. Página 151


De forma simplificada os anos múltiplos de são bissextos. Os anos múltiplos de
são excluídos, e os múltiplos de incluídos novamente. Segue a implementação
proposta:

#include <stdio.h>

int leap (int ano) {

return ano % 4 == 0 && (ano % 100 != 0 || ano % 400 == 0);

int days (int m, int a) {


int t31, t30, t02;

t31 = m == 1 || m == 3 || m == 5 || m == 7 ||
m == 8 || m == 10 || m == 12;

t30 = m == 4 || m == 6 || m == 9 || m == 11;

t02 = m == 2;

return t31 ? 31 :
t30 ? 30 :
t02 ? 28 + leap (a) : 0;

int main( )
{
printf ("%d\n", days ( 2, 2000));
printf ("%d\n", days ( 1, 1996));
printf ("%d\n", days ( 2, 1992));
printf ("%d\n", days ( 2, 1994));
printf ("%d\n", days ( 2, 1996));
printf ("%d\n", days (12, 1600));
printf ("%d\n", days ( 2, 2013));
printf ("%d\n", days (15, 2000));

return 0;
}

A função testa se valor do parâmetro atende às condições de ano bissexto.


O resultado da expressão
resultará em True caso a condição de bissexto seja atendida.

A função calcula o número de dias de um determinado mês e ano. É utilizado o


operador condicional ternário para a construção da lógica. Primeiramente são
gerados três flags: , , que conterão ou para o teste do mês
quanto a ser mes de ou dias, ou ser o mês de .

Em seguida temos o retorno do valor da expressão:

Introdução à Programação: Conceitos e Práticas. Página 152


Foram utilizados vários operadores condicionais ternário embutidos, cuja lógica é
melhor visualizada no seguinte diagrama:

O valor de indica se é ou não um mês de dias. Em caso positivo o resultado é


imediato , caso contrário há um novo operador condicional ternário embutido na
parte destinada ao resultado . Neste operador é avaliado o valor de , que
em caso de ser o resultado é , senão um terceiro operador condicional avalia
a variável para teste de fevereiro. A avaliação positiva implica em número de dias
dado pela expressão que retorna acrescido de ou , caso o ano
seja bissexto ou não. Não sendo um mês válido, o resultado será .

Ao executar este programa, a seguinte tela de entrada e saída será gerada:

29
31
29
28
29
31
28
0

Introdução à Programação: Conceitos e Práticas. Página 153


20. O Tipo Ponteiro

O tipo ponteiro faz parte do conjunto de tipos básicos da linguagem . Este tipo é
destinado a criação de variáveis com capacidade de armazenar um endereço de
memória. O conceito é bastante simples e ao mesmo tempo com grande poder de
construção de estruturas complexas.

Vamos retornar ao nosso modelo que associa uma variável a uma cadeira.
Recordemos que a cadeira possui um nome que provê acesso direto ao valor
assentado sobre ela. Sob o assento tem uma outra identificação da cadeira que é o
número que representa o endereço de memória. Considerando as variáveis:
teríamos a seguinte representação visual:

Uma variável do tipo ponteiro é declarada da seguinte forma:

A natureza da variável é , que a designa como projetada para receber o


endereço de uma cadeira do modelo dado por . Por exemplo, observe a seguinte
declaração:

Neste caso é declarada uma variável cujo nome é e cujo tipo é .


Inicialmente o seu conteúdo é desconhecido. Vamos representar no seguinte
modelo:

Vamos atribuir um valor à esta variável. Pela declaração ela pode receber o
endereço de qualquer cadeira do tipo . O operador já foi apresentado e
bastante utilizado junto com função , para infomar os endereços de memória

Introdução à Programação: Conceitos e Práticas. Página 154


onde os valores correspondentes ao texto digitado pelo usuário deveriam ser
guardados.

Agora vamos lançar mão deste operador para obter o endereço da variável e
armazenar este valor na variável :

Ficaríamos com a seguinte configuração de memória, considerando o contexto com


as variáveis , e .

Apresentamos agora uma segunda maneira de acessar o conteúdo da cadeira . A


primeira forma é através do seu nome , o que chamamos de acesso direto. A
segunda forma é através de , também denominado acesso indireto. Desenhando
apenas as duas cadeiras, temos:

Assim, a expressão fornece acesso direto ao valor , que neste exemplo é o


endereço da cadeira . Porém, a expressão fornece acesso indireto ao
conteúdo da cadeira . Observe a seguinte ilustração:

Vamos implementar este cenário e observar os valores na plataforma utilizada para


a compilação e execução:

#include <stdio.h>
#include <math.h>

Introdução à Programação: Conceitos e Práticas. Página 155


int main () {
int x = 35, y = 56, z = 91;
int *ptr;

ptr = &x;

printf ("&x = %d\n", &x);

printf ("&x = %p\n", &x);


printf ("&y = %p\n", &y);
printf ("&z = %p\n", &z);
printf ("&ptr = %p\n", &ptr);

printf ("x = %d\n", x);


printf ("y = %d\n", y);
printf ("z = %d\n", z);
printf ("ptr = %p\n", ptr);

printf ("*ptr = %d\n", *ptr);

ptr = NULL;
printf ("ptr = %p\n", ptr);

return 0;
}

Os destinados para apresentar o valor de um endereço de memória utiliza o


formato (ponteiro) que imprime o endereço no formato acrescentando
zeros a esquerda para completar o tamanho ocupado pelo tipo ponteiro.

&x = 6422188
&x = 0061FEAC
&y = 0061FEA8
&z = 0061FEA4
&ptr = 0061FEA0
x = 35
y = 56
z = 91
ptr = 0061FEAC
*ptr = 35
ptr = 00000000

O endereço de foi impresso no formato inteiro decimal e no formato para


efeito de comparação. Os demais endereços foram impressos apenas no formato
ponteiro. Observe que a variável , também chamada variável tipo ponteiro, tem
como seu conteúdo ( ) o endereço da variável ( ).

Os endereços apresentados estão em uma sequência distantes quatro unidades


entre si. O número quatro é decorrente do tamanho em do tipo e do tipo
ponteiro, que na plataforma utilizada é de quatro bytes. Ao final a instrução
atribui o valor ao ponteiro. A constante possui valor zero. A
partir deste ponto um acesso indireto acarretará em erro, uma vez que faz
referência a um endereço de memória não permitido ao programa.

Introdução à Programação: Conceitos e Práticas. Página 156


Várias são as aplicações para ponteiros, sendo elas:

a) Acesso em uma função a variáveis declaradas em outra função,


principalmente para obter os benefícios da semântica da passagem de
parâmetro por referência;
b) Retornar múltiplos valores de uma função;
c) Forma eficiente de acessar elementos de um array;
d) Alocação dinâmica de memória;
e) Construção de estruturas de dados dinâmicas;
f) Compartilhamento de memória para troca de informações entre processos ou
com aplicações externas;

O conceito de ponteiros foi apenas introduzido. É possível efetuar operações


aritméticas e lógicas com ponteiros. A medida que avançamos no conteúdo
apresentaremos as funcionalidades adicionais.

No tópico que segue é apresentada uma aplicação fundamental para ponteiros no


contexto da passagem de parâmetro.

Introdução à Programação: Conceitos e Práticas. Página 157


21. Passagem de Parâmetros

O método de passagem de parâmetros estabelece como se dá a ligação entre


argumento e parâmetro. A linguagem permite apenas uma forma de passagem de
parâmetros, denominada passagem por cópia de .

Esta forma de passagem de parâmetro consiste na simples cópia do valor do


argumento para o espaço destinado ao parâmetro. As alterações feitas nos
parâmetros não refletem nos argumentos.

Em várias situações será necessário transferir valores calculados no corpo de uma


função chamada para variáveis que se encontram declaradas na função chamadora.
Grande parte das linguagens de programação disponibilizam uma segunda forma de
passagem de parâmetro, denominada passagem de parâmetro por ou
por .

No caso da linguagem , por não oferecer passagem de parâmetro por


de forma natural, é lançado mão de ponteiros para acesso indireto de memória, que
proporciona os efeitos equivalente à passagem de parâmetro por .

21.1. Passagem de Parâmetro por Cópia de Valor

A passagem de parâmetro por cópia é o método disponibilizado na linguagem . O


principal efeito desta forma de passagem de parâmetro é a separação entre
argumento e parâmetro. As alterações feitas nos parâmetros não afetamos
argumentos associados. Observe o seguinte exemplo:

#include <stdio.h>
#include <math.h>

int foo (int a, int b) { //(1)


a = a + 1;
b = b *2;
return a + b;
}; //(2)

int main () {
int x = 7, y = 3, z;

z = foo (x, y);


//(3)
printf ("x = %d\ny = %d\nz = %d", x, y, z);

return 0;
}

Neste exemplo, tem-se a função que trabalha com dois parâmetros ( e ) e um


programa principal que utiliza esta função através da linha de código: .
A figura a seguir ilustra o contexto da função e de sua utilização.

Introdução à Programação: Conceitos e Práticas. Página 158


Figura 50: Contexto da chamada da função foo.

A ligação entre os argumentos e os parâmetros se dá pela semântica do método de


, onde simplesmente o valor do é copiado para o
. Neste exemplo, no momento da chamada o valor do
primeiro argumento ( ) é copiado para o parâmetro , o mesmo acontecendo para o
segundo argumento, cujo valor é copiado para o parâmetro .

Ao executar este programa, a seguinte tela de saída será gerada decorrente do


:

x = 7
y = 3
z = 14

Para melhor compreender estes valores, vamos apresentar a configuração de


memória em três momentos distintos, destacados no código fonte como pontos e
.

i. Início da função :

Neste momento os valores dos argumentos foram copiados para os


parâmetros. Nesta forma de passagem de parâmetro não há
compartilhamento de memória.

Introdução à Programação: Conceitos e Práticas. Página 159


Figura 51: Ponto 1 – Passagem por Cópia.

ii. Final da função :

Neste momento a função inteira já foi processada. Os valores dos parâmetros


e foram alterados e não afetam os argumentos, pois são áreas distintas
de memória. A área destinada ao resultado da função contém o valor final.

Figura 52: Ponto 2 – Passagem por Cópia.

iii. Após a execução da linha de código que utiliza a função :

Após o término da função e retorno ao programa chamador as


áreas de memória destinadas aos parâmetros e resultado da função são
liberadas. Particularmente o resultado da função é copiado para uma área
temporária onde o programa chamador pode acessar o valor e armazenar em
uma área definitiva, no caso a variável .

Figura 53: Ponto 3 – Passagem por Cópia.

Introdução à Programação: Conceitos e Práticas. Página 160


21.2. Passagem de Parâmetro por Cópia de Endereço

A linguagem não implementa de forma nativa a forma de passagem de parâmetro


por endereço. Logo, é necessário emular esta técnica utilizando ponteiros. A técnica
consiste em passar argumentos, ponteiros para variáveis definidas no contexto do
programa chamador, para os parâmetros da função chamada. Por sua vez, a função
chamada terá acesso indireto às variáveis da função chamadora, sendo que tudo
que for alterado via parâmetros afetará de forma indireta as variáveis da função
chamadora.

Vamos modificar o programa anterior que utiliza a função e a própria função


, para que as atribuições feitas internamente por meio dos parâmetros reflitam
nos argumentos passados.

#include <stdio.h>
#include <math.h>

int foo (int *a, int *b) { //(1)


*a = *a + 1;
*b = *b * 2;
return *a + *b;
}; //(2)

int main () {
int x = 7, y = 3, z;

z = foo (&x, &y);


//(3)
printf ("x = %d\ny = %d\nz = %d", x, y, z);

return 0;
}

Observe as alterações feitas nas definições dos parâmetros. Nesta versão, a função
aguarda como parâmetros os endereços das variáveis que ela terá possibilidade
de alterar. Ao executar este programa, a seguinte tela de saída será gerada
decorrente dos :

x = 8
y = 6
z = 14

A diferença em relação ao programa anterior está nos valores finais das variáveis
e , que passoaram a conter os valores e respectivamente. Isto foi consequência
do forma de passagem de parâmetro. Observar na sequência a visualização do
comportamento do programa, nos pontos e , perante esta modificação.

Introdução à Programação: Conceitos e Práticas. Página 161


i. Início da função :

O diagrama abaixo apresenta uma diferença visual entre as duas formas de


passagem de parâmetros. O parâmetro está definido como ponteiro e é
utilizado para acesso indireto às variáveis declaradas em outras funções. A
mesma definição foi feita para o parâmetro . A característica principal da
passagem de ponteiro como parâmetro é permitir o
. Na representação gráfica é utilizada
uma notação que indica que o conteúdo do parâmetro é o endereço da
variável no momento da chamada. Neste exemplo, a função ativa a
função vinculando o parâmetro com o endereço de e o parâmetro
com a variável .

Figura 54: Ponto 1 – Passagem por “Referência”.

ii. Final da função :

Neste momento a função já foi processada. Os valores das células vinculadas


aos parâmetros e foram alterados. A área destinada ao resultado da
função contém o valor final.

Figura 55: Ponto 2 – Passagem por “Referência”.

iii. Após a execução da linha de código que utiliza a função :

Após o término da função e retorno ao programa chamador as


áreas de memória destinadas aos parâmetros e resultado da função são
liberadas. Particularmente o resultado da função é copiado para uma área
temporária onde o programa chamador pode acessar o valor e armazenar em
uma área definitiva, no caso a variável . As variáveis e que foram
utilizadas como argumentos reterão os valores atribuídos de forma indireta

Introdução à Programação: Conceitos e Práticas. Página 162


pelos parâmetros e . Observar a forma de uso nas expressões, onde
significa acesso à célula vinculada.

Figura 56: Ponto 3 – Passagem por “Referência”.

Assim, fica demonstrada a razão por que apresentará o valor e o valor


ao final do processamento da função.

A simulação da passagem de parâmetro por referência é utilizada quando se deseja


capturar os cálculos feitos por uma função. Nesta situação, basta o programador
utilizar o endereço de uma variável como argumento e estabelecer a ligação
argumento/parâmetro por meio do acesso indireto.

Quando a função produz apenas um resultado de interesse do programa chamador,


é usual que o programador utilize o retorno da para implementar a solução.
Porém, se a deve produzir mais de um resultado que interessa ao módulo
chamador, então a passagem do endereço pode ser utilizada para capturar estes
valores.

Introdução à Programação: Conceitos e Práticas. Página 163


21.3. Exemplo

i. Vamos utilizar o problema do cálculo das raízes de uma equação do segundo grau
para ilustrar bem o caso de um módulo que deve produzir mais de um resultado. É
esperado que uma lógica deste tipo produzisse pelo menos as duas raízes. Assim,
segue uma solução para o problema de obter as raízes de uma equação do segundo
grau a partir dos coeficientes , e .

As raízes são calculadas pela expressão:

Segue a primeira versão de um programa para o cálculo das raízes.

#include <stdio.h>
#include <math.h>

void raizes (double a, double b, double c,


double *r1, double *r2) { //(1)
double delta;

delta = b*b - 4*a*c;


*r1 = (-b + sqrt(delta))/(2*a);
*r2 = (-b - sqrt(delta))/(2*a);
}; //(2)

int main () {
double x1, x2;

raizes (1, -12, 35, &x1, &x2);


//(3)
printf ("raiz 1 = %.5lf\nraiz 2 = %.5lf", x1, x2);

return 0;
}

A solução adotada utiliza uma função para cálculo das raízes com retorno .
Desta forma todo o trabalho da função será capturado por meio de acesso indireto
via parâmetros. Os parâmetros , e são passados por cópia de valor e os
parâmetros e são cópia de endereços de memória (ponteiros). Estes últimos
parâmetros servirão como mecanismo para transferir o resultado para a função
chamadora, que neste caso é a função . O diagrama mais adiante ilustra o
contexto da .

Observar na sequência a visualização do comportamento do programa, nos pontos


e :

i. Início da função :

O diagrama abaixo destaca as duas formas de passagem de parâmetros. Os


parâmetros , e utilizam o mecanismo de passagem de cópia do valor do
argumento e os parâmetros e utilizam o mecanismo de passagem de cópia de
endereço. A chamada da função na função é dada por:

Introdução à Programação: Conceitos e Práticas. Página 164


,

Os argumentos e são copiados para os parâmetros , e , enquanto que


os dois argumentos seguintes expressam os endereços das variáveis e . Desta
forma, através dos parâmetros e a função terá acesso indireto às
variáveis e . Os valores iniciais d são desconhecidos, uma vez que não
foram inicializadas. É criada uma variável local chamada para armazenar o
valor da discriminante da equação do grau.

Figura 57: Ponto 1 – Calculo Raizes.

ii. Final da função :

Neste momento a função já foi processada. As raízes foram calculadas e


armazenadas nas células apontadas pelos parâmetros e : e .

Figura 58: Ponto 2 – Calculo Raizes.

Introdução à Programação: Conceitos e Práticas. Página 165


iii. Após a execução da linha de código que utiliza a função :

Após o término da execução da função e retorno ao programa chamador,


imediatamente após a linha as áreas de memória
destinadas aos parâmetros e variáveis locais são liberadas. As variáveis e que
tiveram seus endereços como argumentos reterão os últimos valores defidos pelas
atribuições a * e .

Figura 59: Ponto 3 – Calculo Raizes.

Ao imprimir os valores de e , será visualizada a seguinte tela:

raiz 1 = 7.00000
raiz 2 = 5.00000

ii. Elaborar um programa que converte segundos corridos em horas, minutos e


segundos.

Neste problema, o usuários irá informar um número inteiro que corresponde a


quantidade de segundos decorridos a partir da zero hora e calculará as horas,
minutos e segundos correspondentes.

Os cálculos para conversão serão implementados em uma função específica, que


iremos denominar . Está função receberá na sua interface de parâmetros o
valor correspondente aos segundos corridos. É necessário decidir a forma pela qual
a função entregará os resultado à função chamadora.

Uma possibilidade é utilizar a interface específica para resultado de função. Porém,


até o momento esta interface permite entregar apenas um valor escalar, e a função
necessita de fato entregar três valores: horas, minutos e segundos.

Assim, a solução escolhida é utilizar a interface de parâmetros para também entregar


estes três resultados. Para isto, será utilizado o acesso indireto. A função chamadora
irá fornecer o valor dos segundos corridos e mais três endereços de memória, onde
serão depositados os valores calculados pela função. A saída regular de resultado da
função não será utilizada. Segue a solução proposta:

Introdução à Programação: Conceitos e Práticas. Página 166


#include <stdio.h>

void tohms (int seg, int *h, int *m, int *s) {

*h = seg / 3600;
seg = seg % 3600;
*m = seg / 60;
*s = seg % 60;
}

int main () {
int segundos, hh, mm, ss;
scanf ("%d", &segundos);
tohms (segundos, &hh, &mm, &ss);
printf ("%d = %02d:%02d:%02d", segundos, hh, mm, ss);
return 0;
}

A função foi definida com quatro parâmetros, sendo eles: , onde é


destinado a receber um valor inteiro correspondente aos segundos corridos;
que é o parâmetro de tipo projetado para receber o endereço de uma área de
memória associada a um inteiro, cujo acesso se dará indiretamente através de .
Além disso, o resultado da função foi definido como , dispensando o uso
do no seu interior.

void tohms (int seg, int *h, int *m, int *s) {

*h = seg / 3600;
seg = seg % 3600;
*m = seg / 60;
*s = seg % 60;
}

No interior da função os parâmetros são trabalhados para computar os valores


desejados. Assim, a expressão *h calcula o quociente inteiro da
divisão entre e para obter a quantidade de horas. O resultado deste cálculo
é armazenado na célula apontada por , pelo recurso de acesso indireto *h. As
expressões que seguem computam os valores de minuto e segundos do dia,
lançando os resultados nos endereços indicados por e , igualmente pelo recurso
de acesso indireto *m e *s.

A função utiliza a função , carregando adequadamente os quatro


parâmetros. A chamada copia o valor da variável
para o primeiro parâmetro de , copia o endereço de para o
segundo parâmetro, copia o endereço de para o terceiro parâmetro e por
fim o endereço de para o quarto parâmetro. Ao encerrar o processamento
de e retornar o fluxo para , as variáveis , e de conterão os
valores calculados.

Introdução à Programação: Conceitos e Práticas. Página 167


Ao executar este programa, a seguinte tela de saída será gerada decorrente dos
:

37850
37850 = 10:30:50

Introdução à Programação: Conceitos e Práticas. Página 168


22. Instruções de Controle

Até o momento foram apresentados recursos de programação que permitem


escrever algoritmos compostos por linhas de código cuja execução ocorre em
sequência, uma linha após a outra. Ao concluir o processamento de uma linha a
próxima será executada, e assim sucessivamente. A seguinte figura ilustra o fluxo de
processamento de um módulo contendo apenas instruções executadas em
.

O processamento inicia no e flui executando cada uma das linhas de código,


até alcançar o final do módulo no .

Estas linhas de código, com os recursos vistos, podem ser do tipo instrução de
atribuição ou chamadas de funções. O lado direito da instrução pode incluir
expressões com funções. Assim, quando uma linha de código invoca uma ,
tem-se um fluxo semelhante ao da figura que segue:

Ainda nesta situação, tem-se o seguinte fluxo único para a execução do programa.

Os problemas práticos demandam programas suficientementes robustos, que não


produzam erros de execução que causam a parada do sistema. Isto significa dizer
que as lógicas devem ser completas no sentido de incluir o tratamento adequado de
possíveis erros e quando possível prevenir que instruções sejam executadas na
iminência de falhas. Como exemplo, citamos a lógica já implementada de cálculo
das duas raízes de uma equação do segundo grau. A lógica codifica apenas as
expressões que levam ao cálculo das raízes. Porém, há erros potenciais que podem
se manifestar quando as expressões forem avaliadas, tais como a divisão por zero e
a raiz de um delta negativo. Assim, o programador deve pensar na lógica que desvie

Introdução à Programação: Conceitos e Práticas. Página 169


o fluxo de processamento para um caminho seguro que evita a avaliação das
expressões nos casos que levam a erro de execução. As linguagens de
programação disponibilizam instruções que permitem ao programador fazer a
entre caminhos alternativos de processamento.

Outra categoria de problemas práticos requer a codificação de cálculos que repetem


um mesmo padrão da lógica por inúmeras vezes. Um exemplo deste comportamento
é o cálculo da função através de uma série, onde as parcelas possuem uma
forma padrão. Para estes problemas o algoritmo normalmente submete a forma
padrão a um processamento repetitivo de cálculo. Assim, é fundamental que a
linguagem permita que uma ou mais instruções sejam processadas várias vezes
sem que o programador tenha que duplicar códigos semelhantes. A estrutura que
permite este recurso é denominada de .

Em síntese, uma linguagem de programação de alto nível da família


imperativa/procedural deve prover mecanismos de controle do fluxo de execução
que se enquadrem nas seguintes categorias:

 ;
 ;e
 .

Assim, é possível codificar qualquer problema computável combinando estas três


construções de controle.

22.1. Seleção – IF-THEN-ELSE

O disponibiliza uma instrução de seleção que possibilita ao usuário definir dois


caminhos alternativos que serão escolhidos no momento da execução do programa
dependendo do valor de uma expressão lógica. É importante reforçar que em uma
expressão lógica tem a mesma natureza de uma expressão inteira. Como a
expressão lógica admite dois valores, então um caminho de processamento é
associado ao valor equivalente ao , desempenhado por qualquer inteiro
diferente de , e o outro caminho é associado ao valor , desempenhado
pelo inteiro .

22.1.1. Sintaxe e Semântica

A sintaxe da instrução é:

Desenhando um diagrama para melhor visualizar a estrutura da , tem-


se:

Introdução à Programação: Conceitos e Práticas. Página 170


Se qualquer um dos caminhos tiver mais de uma instrução associada, então as
instruções deverão estar definidas como blocos através do uso do { }.

A execução do inicia com a avaliação da e em


seguida com a comparação do valor resultante. Caso o valor da expressão seja um
inteiro diferente de zero ( ) então serão processadas as instruções codificadas
logo na sequência da expressão teste, também denominado caminho . Caso
contrário, serão executadas as instruções associadas ao caminho .

A cláusula é opcional, o que proporciona uma variação da instrução:

O diagrama para a é dado por:

A execução do inicia com a avaliação da expressão lógica ou expressão


teste e em seguida com a comparação do valor resultante. Caso o valor da
expressão seja um inteiro diferente de zero ( ) então serão processadas as
instruções codificadas logo na sequência da expressão teste, também denominado
caminho . Caso contrário, o processamento segue para a próxima instrução
após a instrução .

22.1.2. Exemplos

Introdução à Programação: Conceitos e Práticas. Página 171


Os exemplos abaixo relacionados apresentam várias formas de utilização da instrução de
controle a partir de diagramas com as lógicas.

Diagrama Programa
#include <stdio.h>

int main () {
double X, Y;
int A, B;

if (X > Y)
A = 1;
else
A = 2;
i.
// Se a expressão for então será
// executada a instrução caso contrário
// será executada a instrução .
if (X > 10) {
A = 1;
B = 2;
} else A = B + 1;

// Se a expressão for então


ii. // serão executadas as duas instruções
// associadas ao caminho , caso contrário
// será executada a única instrução associada ao
// . Observe o { } envolvendo as
// duas instruções vinculadas ao .
// Observe a ausência do antes do
if (X <= 10)
A = 1;
else {
A = 2;
iii.
B = 1;
}

if (X >= 10) {
A = -1;
B = -2;
} else {
iv.
A = 3;
B = 4;
}
// Observe a ausência do antes do

Quando o else é precedido de um bloco, como { } , este bloco não pode ser encerrado com
. Observe nos exemplos e que o não vem precedido deste símbolo. Outra

Introdução à Programação: Conceitos e Práticas. Página 172


situação que ocorre com frequência é a utilização de , como nos seguintes
casos:

if (X > Y) if (X > Y)
if (X != 0) if (X != 0)
A = 1; A = 1;
else A = 2; else A = 2;

Observe que os dois códigos são idênticos, pois possuem exatamente a mesma sequência
de palavras ( ). O deslocamento ( ) do texto mais para esquerda ou para a
direita não afeta em nada a forma como a linguagem interpreta a sentença. Assim, uma das
duas construções não representa o diagrama correspondente. Está claro no desenho que são
lógicas distintas. Este caso ficou conhecido no Algol como . Esta ambiguidade é
resolvida vinculando o ao mais interno. Havendo a necessidade em vincular
o ao mais externo, um bloco { } deverá ser utilizado, como indicado no código abaixo:

if (X > Y) { if (X > Y)
if (X != 0) if (X != 0)
A = 1; A = 1;
} else A = 2;
else A = 2;

v. Calcular as raízes e de uma equação do segundo grau a partir dos coeficientes


, e . A solução deverá produzir um valor que indica o dos cálculos de
acordo com a seguinte convenção:

  sem erro: raízes calculadas


  se
  se

A solução adotada anteriormente para o cálculo das raízes poderá em fim ser
completada com o tratamento adequado das condições que poderiam levar a um
erro na execução da lógica.

Assim, devemos construir uma nova lógica que desvia o fluxo de execução do
módulo para que não ocorram nem nem .

Observe no diagrama abaixo proposto, que a cláusula é utilizada


para testar inicialmente o valor do coeficiente e desviar para o cálculo das raízes
somente no caso de passar pelo teste de A != 0. Superado este primeiro teste, o

Introdução à Programação: Conceitos e Práticas. Página 173


é calculado e um novo teste é feito direcionando o fluxo para o cálculo das
raízes somente em caso de . Nos casos de falha nos testes a cláusula
else é executada atribuindo ao parâmetro a condição do erro.

Figura 60: Diagrama lógico: cálculo das raízes de equação do 2º grau.

#include <stdio.h>
#include <math.h>

void show (int code, double x1, double x2) {


if (code == 0) {
printf ("Raiz 1 = %8.4lf\n", x1);
printf ("Raiz 2 = %8.4lf\n", x2);
}
else if (code == 1) printf ("erro: a = 0\n");
else if (code == 2) printf ("erro: delta < 0\n");
}

void raizes (double a, double b, double c,


double *r1, double *r2,
int *code) {
double d;
if (a != 0) {
d = b*b - 4*a*c;
if (d >= 0) {
*code = 0;
*r1 = (-b + sqrt(d))/(2*a);
*r2 = (-b - sqrt(d))/(2*a);
}
else *code = 2;
}
else *code = 1;
};

int main () {
double a1, b1, c1, x1, x2;
int code;

Introdução à Programação: Conceitos e Práticas. Página 174


printf ("Entre com os coeficientes A, B e C:\n");
scanf ("%lf %lf %lf", &a1, &b1, &c1);
raizes (a1, b1, c1, &x1, &x2, &code);
show (code, x1, x2);

return 0;
}

A solução proposta inclui o módulo que permite apresentar na tela os


resultados do processamento da seguinte forma:

 Se  mostrar as raízes calculadas


 Se  mostrar a mensagem
 Se  mostrar a mensagem

A seguinte tela mostra os resultados de quatro processamentos independentes do


programa completo:

Entre com os coeficientes A, B e C:


1 4 4
Raiz 1 = -2.0000
Raiz 2 = -2.0000

Entre com os coeficientes A, B e C:


1 6 3
Raiz 1 = -0.5505
Raiz 2 = -5.4495

Entre com os coeficientes A, B e C:


4 6 3
erro: delta < 0

Entre com os coeficientes A, B e C:


0 3 5
erro: a = 0

vi. Uma família de problemas muito comum em situações reais é a relacionada com a
manipulação de datas. Assim, este exemplo pede uma solução para o cálculo da
quantidade de dias de um determinado mês e ano.

#include <stdio.h>

int leap (int ano) {


return ano % 4 == 0 && (ano % 100 != 0 || ano % 400 == 0);
}

int days (int mes, int ano){

if (mes == 1 || mes == 3 || mes == 5 || mes == 7 ||


mes == 8 || mes == 10 || mes == 12)
return 31;
else if (mes == 4 || mes == 6 || mes == 9 || mes == 11)
return 30;
else if (mes == 2)
return 28 + leap (ano);

Introdução à Programação: Conceitos e Práticas. Página 175


else
return 0;
}

int main () {
printf ("%d\n", days ( 2, 2000));
printf ("%d\n", days ( 1, 1996));
printf ("%d\n", days ( 2, 1992));
printf ("%d\n", days ( 2, 1994));
printf ("%d\n", days ( 2, 1996));
printf ("%d\n", days (12, 1600));
printf ("%d\n", days ( 2, 2013));
printf ("%d\n", days (15, 2000));

return 0;
}

A saída para o programa é:

29
31
29
28
29
31
28
0

A função poderia ser codificada da seguinte forma:

int leap (int ano) {


if (ano % 400 == 0) return 1;
if (ano % 100 == 0) return 0;
if (ano % 4 == 0) return 1;
return 0;
}

vii. Implementar uma solução que incrementa em um dia uma data válida (dia, mês e
ano).

A solução consiste em incrementar o dia em , e testar se o resultado não extrapolou


a quantidade de dias permitida para o mês. Em caso positivo, o dia passa para e o
mês é incrementado de , testando agora se o mês não excedeu aos permitidos
para o ano. Em caso positivo, o mês passa para e o ano é incrementado de .
Segue esta solução:

#include <stdio.h>
#include <math.h>

int leap (int ano) {


// copiar função implementada anteriormente
}

int days (int mes, int ano) {

Introdução à Programação: Conceitos e Práticas. Página 176


// copiar função implementada anteriormente
}

void incdate (int *d, int *m, int *a) {


(*d)++;
if (*d > days (*m, *a)) {
*d = 1;
(*m)++;
if (*m > 12) {
*m = 1;
(*a)++;
};
};
};

int main () {
int dia, mes, ano;
printf ("entre com uma data: dd mm aaaa\n");
scanf ("%d %d %d", &dia, &mes, &ano);
incdate (&dia, &mes, &ano);
printf ("o dia seguinte eh: %02d/%02d/%d\n", dia, mes, ano);

return 0;
}

A solução adotada para os parâmetros da função consiste na definição de


três variáveis do tipo endereço de inteiro. Tecnicamente, esta modalidade de
equivale a , permitindo que toda alteração
nos parâmetros possa ser captada pelos argumentos.

A lógica da função inicia incrementado a variável vinculada ao parâmetro


destinado ao : . É importante destacar a importância dos parêntesis
envolvendo o acesso indireto ao local destinado ao . Esta sintaxe faz com que o
operador ++ seja aplicado à variável apontada por .

Caso tivéssemos codificado como . sem o uso do jogo de parêntesis, o efeito


seria primeiramente incrementar a variável , fazendo com que passasse a conter
o endereço inicial acrescido de . Na sequência a referência não teria
efeito algum.

Em seguida a lógica testa se o resultado deste incremento excede o número de dias


permitido para o mês e ano indicados. Havendo excedido, então o local destinado ao
mês é incrementado de : e o local destinado ao dia é ajustado em :
.

Na sequência, é feito o teste de plausibilidade para o mês.

Os módulos e tiveram seus códigos omitidos para melhor visualização da


solução. A seguinte tela ilustra as entradas e saídas para alguns testes:

Introdução à Programação: Conceitos e Práticas. Página 177


entre com uma data: dd mm aaaa
1 1 2000
o dia seguinte eh: 02/01/2000

entre com uma data: dd mm aaaa


10 10 2010
o dia seguinte eh: 11/10/2010

entre com uma data: dd mm aaaa


31 1 2012
o dia seguinte eh: 01/02/2012

entre com uma data: dd mm aaaa


31 12 2009
o dia seguinte eh: 01/01/2010

Introdução à Programação: Conceitos e Práticas. Página 178


22.2. Seleção – SWITCH-CASE

A construção de controle do tipo proporciona dois caminhos


alternativos para o fluxo de processamento. Uma expressão lógica ou inteira é
utilizada como ponto de decisão para a escolha do trajeto a ser tomado. Um dos
caminhos está associado a qualquer inteiro diferente de zero ( ) e o caminho
alternativo está associado ao valor zero ( ).

Em determinados problemas é comum que o código resultante se enquadre em um


aninhamento (estruturas embutidas) de , como o visto na função
. Especificamente para situações semelhantes, a linguagem disponibiliza a
construção que permite a escolha de um entre múltiplos caminhos
possíveis. O ponto de decisão admite múltiplos caminhos alternativos, denpendendo
dos valores da expressão de teste.

22.2.1. Sintaxe e Semântica

A sintaxe da construção de controle é dada por:

Introdução à Programação: Conceitos e Práticas. Página 179


O seguinte diagrama ilustra o significado da construção :

Figura 61: Instrução switch-case

Não é permitido o uso de expressão do tipo ou , pois estes tipos não


são da família dos inteiros.

O diagrama anterior indica que o inicia com uma expressão inteira,


cujo valor é testado quanto a igualdade com o valor da primeira entrada , que
em caso positivo as instruções associadas a este serão executadas. Em caso
negativo, é feito o teste seguinte em relação ao valor da próxima entrada , que
em caso positivo as instruções associadas a este serão executadas, e assim
sucessivamente. Opcionalmente, pode ser incluída uma cláusula que será
executada no caso de falha dos testes anteriores.

Observe no diagrama que após um teste de bem sucedido, as instruções


associadas serão executadas, e perigosamente, o fluxo de processamento segue
executando as instruções dos seguintes. Assim, caso o programador queira
que um seja executado de forma exclusiva, é necessário incluir como última
linha do bloco das instruções associdas a cada a instrução , que encerra
o processamento da construção .

O seguinte diagrama ilustra a estutura com a inclusão da instrução


ao final de cada bloco de instruções associadas ao :

Introdução à Programação: Conceitos e Práticas. Página 180


Figura : Instrução switch-case com breaks

Os exemplos que seguem destacam as formas de uso desta construção.

22.2.2. Exemplos

i. O seguinte exemplo ilustra o efeito do switch-case.

#include <stdio.h>

int main () {
int x;
printf ("entre com um inteiro\n");
scanf ("%d", &x);
switch (x) {
case 0 : printf ("o valor eh zero\n");
case 1 : printf ("o valor eh um\n");
case 2 : printf ("o valor eh dois\n");
default :
printf ("o valor eh maior que dois\n");
}

return 0;
}

O programa solicita a entrada de um número inteiro e armazena em . Em seguida


testa o valor de para , e , e imprime um texto correspondente. Qualquer valor
diferente destes é apresentada uma mensagem adequada. Vamos rodar alguns
casos.

Introdução à Programação: Conceitos e Práticas. Página 181


entre com um inteiro
10
o valor eh maior que dois

Neste caso, o usuário forneceu o valor para . Os testes para o , e


falharam, e a cláusula captura o fluxo e imprime a mensagem
adequada. Vamos a um outro caso.

entre com um inteiro


1
o valor eh um
o valor eh dois
o valor eh maior que dois

Neste caso, o usuário forneceu o valor para . O teste para falha. Então o
processamento segue para o teste do , que é bem sucedido e o fluxo é
desviado para executar as instruções associadas a esta condição. A mensagem
correspondente a este case é impressa, e o fluxo segue processando as instruções
dos demais . Certamente não era o comportamento pensado pelo programador
para este problema.

Este efeito deve ser corrigido acrescentando a instrução ao final de cada


entrada . Observe a nova versão:

#include <stdio.h>
#include <math.h>

int main () {
int x;
printf ("entre com um inteiro\n");
scanf ("%d", &x);
switch (x) {
case 0 : printf ("o valor eh zero\n"); break;
case 1 : printf ("o valor eh um\n"); break;
case 2 : printf ("o valor eh dois\n"); break;
default :
printf ("o valor eh maior que dois\n");
}

return 0;
}

Nesta nova situação teremos o seguinte resultado para o valor .

entre com um inteiro


1
o valor eh um

Uma mensagem única é apresentada, pois ao validar um , as suas instruções


associadas são executadas, e ao final o encerra o .

Introdução à Programação: Conceitos e Práticas. Página 182


ii. Refazer o programa que informa se um determinado caractere é uma consoante.

A função será reescrita utilizando a construção , conforme o


código listado abaixo:

#include <stdio.h>
#include <ctype.h>

int isvogal (int c) {


switch (toupper(c)) {
case 'A' :
case 'E' :
case 'I' :
case 'O' :
case 'U' : return 1;
default :
return 0;
};
};

int isconsoante (int c) {


c = toupper (c);
return isalpha(c) && !isvogal(c);
};

int main () {
printf ("%d\n", isconsoante('a'));
printf ("%d\n", isconsoante('B'));
printf ("%d\n", isconsoante('x'));
printf ("%d\n", isconsoante('3'));

return 0;
}

O utilizado na função avalia em cada se o caractere dado


é uma vogal específica. Como para qualquer uma das vogais o processamento é o
mesmo, ou seja retorna , então o fluxo, no caso de vogal, é desviado para um bloco
sem instruções, e segue até alcançar as instruções do último . Não sendo uma
vogal, então o fluxo é capturado pelo bloco . Assim, não é necessário inserir
um e a cada .

A saída para o programa é:

0
1
1
0

iii. Refazer o programa para o cálculo da quantidade de dias de um determinado mês e


ano.

Segue a solução proposta:

#include <stdio.h>

int leap (int ano) {

Introdução à Programação: Conceitos e Práticas. Página 183


return (ano % 4 == 0) &&
((ano % 100 != 0) || (ano % 400 == 0));
}

int days (int mes, int ano){


switch (mes) {
case 1: case 3: case 5: case 7:
case 8: case 10: case 12:
return 31;
case 4: case 6: case 9: case 11:
return 30;
case 2:
return 28 + leap (ano);
}
return 0;
}

int main () {
printf ("%d\n", days ( 2, 2000));
printf ("%d\n", days ( 1, 1996));
printf ("%d\n", days ( 2, 1992));
printf ("%d\n", days ( 2, 1996));
printf ("%d\n", days (12, 1600));
printf ("%d\n", days ( 2, 2013));
printf ("%d\n", days (15, 2000));

return 0;
}

Observe que a função foi reescrita com o . Por conveniência,


várias entradas foram escritas na mesma linha. Não foi necessário utilizar o
a cada bloco , uma vez que o possui como efeito encerrar a
função.

Introdução à Programação: Conceitos e Práticas. Página 184


22.3. Repetição – WHILE

A estrutura de repetição completa os recursos de uma linguagem tipo e amplia as


possibilidades de controle sobre o fluxo de execução de um programa. A estrutura
de repetição permite que uma ou mais instruções sejam executadas várias vezes de
acordo com a necessidade do problema. Assim, ao modelar um problema e
desenhar o algoritmo, deve ser pensada uma lógica que represente a situação de
uma forma geral. Por exemplo, se é necessário obter as notas de alunos para
calcular a média da turma, não é razoável construir uma lógica com .
Deve haver um mecanismo que determine a repetição de vezes a instrução
desejada.

As linguagens da família disponibilizam várias estruturas de repetição, sendo elas:

 Repetição controlada por contador;


 Repetição controlada por expressão lógica;
o Pré-teste;
o Pós-teste

A estrutura de controle do tipo repetição controlada por expressão lógica é essencial


em uma linguagem de programação do paradigma imperativo ou procedural, da qual
faz parte. O objetivo desta construção de controle é prover um mecanismo onde
uma ou mais instruções são executadas zero ou mais vezes dependendo do valor
de uma expressão lógica. O ciclo é repetido enquanto o valor da expressão lógica
for . No caso da linguagem , o tipo lógico é desempenhado pelos tipos inteiros.

22.3.1. Sintaxe e Semântica

A sintaxe da instrução de repetição tipo é:

Onde é a condição que determina o controle da repetição.

A seguinte figura ilustra os componentes da instrução , bem como destaca que


a dinâmica da estrutura inclui vários ciclos de execução da instrução controlada,
onde o fim da repetição ocorre quanto a expressão inteira assume o valor zero
( ).

Introdução à Programação: Conceitos e Práticas. Página 185


Havendo mais de uma instrução controlada, estas devem estar agrupadas em um
bloco { }.

O seguinte diagrama lógico permite visualizar o fluxo do processamento:

A expressão teste do é avaliada e seu valor comparado com zero. Caso seja
!= o fluxo do processamento segue para as instruções controladas. Após executar
todas as instruções controladas, retorna novamente ao para um novo teste do
valor da expressão inteira. O é encerrado quando o valor da expressão teste for
. Observe que utilizaremos o termo expressão teste, expressão inteira ou
expressão lógica como equivalentes.

Introdução à Programação: Conceitos e Práticas. Página 186


22.3.2. Exemplos

i. Implementar um programa que efetua leitura de dados e cálculo da soma, da média


aritmética e do maior valor dentre os valores fornecidos, utilizando a repetição
controlada por expressão lógica. A única forma de encerrar a entrada de dados é
através do dado .

A utilização da instrução irá simplificar a última implementação feita com o


, pois deverá dispensar o . A permanência no loop será definida pela
condição . Segue a lógica proposta:

#include <stdio.h>

void lerdados () {
int t;
float maior, x, s, m;

s = 0;
t = 0;
printf ("Entre com os dados\n");
scanf ("%f", &x);
maior = x;
while (x != -1) {
t++;
if (x > maior)
maior = x;
s = s + x;
scanf ("%f", &x);
};
m = s/t;
printf ("soma = %7.2f\nmedia = %7.2f\nmaior = %7.2f", s, m, maior);
};

int main () {

lerdados();
return 0;
}

A procedure foi alterada, pois o parâmetro que indica a quantidade de


dados a serem lidos não é mais necessária. O final da repetição se dá unicamente
pela entrada do valor . A lógica tem as seguintes características:

 São inicializadas as variáveis e . Não há mais contador, assim a variável


armazenará a quantidade de dados válidos digitados. É útil para o cálculo da
média;
 É feita uma leitura antes da repetição, que servirá de inicialização para a
variável ;
 Dentro do a variável é incrementada de a cada entrada válida.

Ao executar este programa, teremos a seguinte saída:

Introdução à Programação: Conceitos e Práticas. Página 187


Entre com os dados
20
30
10
30.5
20.5
10.5
-1
soma = 121.50
media = 20.25
maior = 30.50

ii. Elaborar um programa que calcule a raiz quadrada ( √ ) de um número pelo método
de .

O método de calcula a raiz quadrada de um através de um método


iterativo (repetitivo) onde cada ciclo calcula um novo valor a partir do valor do ciclo
anterior, convergindo em direção ao resultado desejado. A fórmula de recorrência é
dada por:

( )

A repetição deve ser encerrada quando a diferença entre o valor atual e o anterior for
inferior a um determinado . Normalmente o erro estabelecido é o menor número
possível de ser representado na arquitetura em que o programa está rodando. Este
erro pode ser obtido por uma função que executa divisões sucessivas a partir de
até alcançar o , sendo que o valor anterior ao é o menor número
representável naquela arquitetura. A solução proposta é dada por:

#include <stdio.h>
#include <math.h>

double GetEPS () {
double EPS, x;

x = 1;
while (x != 0) {
EPS = x;
x = x/2;
};
return EPS;
};

double raizq (double x) {


double EPS, r, R;

if (x >= 0) {
R = 0; r = 1;
EPS = GetEPS();
while (fabs(r - R) >= EPS) {
R = r;
r = (R + x / R) / 2;

Introdução à Programação: Conceitos e Práticas. Página 188


};
} else r = 0;

return r;
};

int main () {

printf ("%.16f\n%.16f", raizq(2), sqrt(2));


return 0;
}

Na função , antes de caminhar para o cálculo da raiz, o valor de é testado


para evitar o processamento de número negativo. A lógica principal da função é
semelhante a fórmula apresentada:

 Os valores das variáveis que representam o valor da raiz no ciclo atual ( ) e o


valor do ciclo anterior ( ) são inicializados com valores distintos para que o
erro ( ) seja diferente de .

R = 0; r = 1;

 A variável que estabelece qual a precisão requerida para o cálculo é


inicializada com o ) da máquina, que é o menor número real antes
de chegar ao zero.

EPS = GetEPS();

 O principal determina que seja calculada uma nova raiz a cada ciclo
a diferença entre o valor do ciclo atual e o do ciclo anterior seja
superior à precisão estabelecida. A é muito
semelhante à expressão codificada.

while (fabs(r - R) >= EPS) {


R = r;
r = (R + x / R) / 2; ( )
};

Ao executar este programa, teremos a seguinte saída:

1.4142135623730951
1.4142135623730951

Os valores calculados pela função implementada e pela função interna da biblioteca


do são idênticos.

iii. Elaborar um programa que calcule a raiz ( √ ) de um número pelo método


de .

O método de calcula a raiz de um através de um método


iterativo (repetitivo), similar ao método da raiz quadrada. A fórmula de recorrência é
dada por:

Introdução à Programação: Conceitos e Práticas. Página 189


( )

Segue a solução proposta:

#include <stdio.h>
#include <math.h>

double GetEPS () {
double EPS, x;

x = 1;
while (x != 0) {
EPS = x;
x = x/2;
};
return EPS;
};

double powint (double x, int t) {


double p; int i;

p = i = 1;
while (i <= t) {
p *= x;
i++;
}
return p;
};

double raizn (double x, int n) {


double EPS, r, R;

if (x >= 0) {
R = 0; r = 1;
EPS = GetEPS();
while (fabs(r - R) >= EPS) {
R = r;
r = ((n-1)*R + x / powint(R, n-1)) / n;
};
} else r = 0;

return r;
};

int main () {
double y
y = raizn (25, 3);
printf ("%.15f\n%.15f", y, powint(y, 3));
return 0;
}

A utiliza um cálculo do tipo na parte . Como o


expoente é um número inteiro positivo, preferiu-se implementar uma versão da
função exclusivamente para este caso ( ), evitando assim operar com
expoente .

Ao executar este programa, teremos a seguinte saída:

Introdução à Programação: Conceitos e Práticas. Página 190


2.924017738212866
24.999999999999996

No programa principal, o teste foi feito calculando os valores de √ e de . Os


valores apresentados indicam que o cálculo foi realizado com boa precisão.

A função pode ser implementada de outra forma, tal como a que segue,
(I)
baseada no exemplo do , publicado pelo prof. Niklaus Wirth.

double powint (double x, int t) {


double w, z; int i;

w = x; z = 1; i = t;
while (i) {
if (i % 2) z *= w;
i /= 2;
w *= w;
};
return z;
};

Nesta função fizemos uso de algumas simplificações e recursos da linguagem . A


repetição aproveita o próprio valor do inteiro como condição de
permanência na repetição. O é encerrado quando a expressão de controle for
falsa. Como o inteiro desempenha o papel de falso, então o será encerrado
quando contiver o valor zero. É dispensada a expressão completa != .

A expressão equivale a
A expressão equivale a
A expressão equivale a

iv. Implementar a seguinte somatória para N termos:

A série possui termos, onde pode ser qualquer valor maior que um. Os termos
da série possuem a seguinte forma geral:

Assim, é necessário agora formular uma solução para gerar cada um dos termos, e
somá-los a fim de atender a especificação do problema. Segue solução
implementada:

#include <stdio.h>

double soma (int n) {


int i;
double s;
s = 0;
i = 1;
while (i <= n) {

Introdução à Programação: Conceitos e Práticas. Página 191


s = s + 1.0/i;
i = i + 1;
}

return s;
}

int main () {
printf ("%8.6lf\n", soma (2));
printf ("%8.6lf\n", soma (5));
printf ("%8.6lf\n", soma (100));
return 0;
}

A função utiliza duas variáveis, sendo a variável destinada a contar a


quantidade de repetições e a variável para acumular os termos gerados em cada
ciclo de execução. A função recebe como parâmetro a quantidade de termos que se
pretende considerar para a série. A seguinte tabela ilustra os cinco ciclos de
execução da chamada .

Instrução S
1
2
3
4
5

A coluna contém os valores da variável contadora de repetições em cada ciclo. A


coluna apenas reforça que a cada ciclo a mesma instrução é executada.
A coluna destaca como a variável vai sendo atualizada a cada ciclo pela
atribuição do valor da expressão a direita e acumulando o valor de cada termo. Ao
final do processamento do , a variável conterá o valor .

Ao executar este programa, teremos a seguinte saída:

1.500000
2.283333
5.187378

É pedido ao leitor alterar a expressão para compilar e


executar novamente e observar o resultado obtido. O que explica um resultado
apresentado.

v. Implementar a seguinte somatória para termos:

Esta série difere da anterior pela alternância dos sinais dos termos. Uma solução
possível é utilizar variável contadora de repetições para definir o sinal do termo a ser
adiconado.

Introdução à Programação: Conceitos e Práticas. Página 192


Assim, é necessário formular uma solução para gerar cada um dos termos, e
executar a operação de soma ou subtração. Segue a implementação desta lógica:

#include <stdio.h>

double soma (int n) {


int i;
double s;
s = 0;
i = 1;
while (i <= n) {
if (i % 2 == 0)
s = s - 1.0/i;
else
s = s + 1.0/i;

i = i + 1;
}

return s;
}

int main () {
printf ("%8.6lf\n", soma (2));
printf ("%8.6lf\n", soma (5));
printf ("%8.6lf\n", soma (100));
return 0;
}

A lógica adotada aproveita o valor de para definir o sinal da parcela. Assim, quando
for par o termo deve ser subtraído (negativo) de e quando for impar deve ser
somado (positivo). O teste se é par é feito avaliando o resto da divisão de por .

Uma alternativa a lógica apresentada para a função soma e usar uma variável
adicional para fornecer o sinal correto a cada ciclo. Segue esta versão:

double soma (int n) {


int i, SINAL;
double s;
s = 0;
i = 1;
SINAL = 1;
while (i <= n) {
s += (double) SINAL/i;
SINAL = -SINAL;
i++;
}
return s;
}

A variável é inicializada com e a cada ciclo a instrução é


executada com o objetivo de alternar o valor de entre e . As parcelas
passam a apresentar o seguinte termo geral , que constrói exatamente os
termos com o sinal em alternância. Um cuidado adicional é necessário em função da
expressão envolver apenas inteiros. Desta forma o resultado será para
e zero para os demais valores de . Uma solução seria redefinir uma das

Introdução à Programação: Conceitos e Práticas. Página 193


variáveis da expressão para , ou indicar para o compilador que a expressão
seja tratada como . Esta última alternativa é o denominada de ou
simplesmente . O tipo desejado é indicado entre parêntesis a esquerda da
expressão, como em: .

Além disso, observe que a instrução foi substituída por ++;, onde o
operador ++ soma à variável indicada. De modo semelhante, para acumular
valores na variável foi utilizado o operador resultando na expressão
.

vi. Implementar a seguinte somatória para a quantidade de termos indicada:

Esta série poderia ser reescrita para ficar explícita a ordem de cada termo, como
segue:

O termo geral desta série tem a seguinte forma:

Uma solução possível é utilizar a instrução com a variável contadora de


repetições assumindo os valores { } , e para cada um destes valores
escrever um mecanismo para produzir o termo geral, bem como acumular estas
parcelas.

#include <stdio.h>
#include <math.h>

double soma (double x, int n) {


int i; double s;
s = 0;
i = 0;
while (i <= n) {
s += pow(x, i);
i++;
}

return s;
}

int main () {
printf ("%8.6lf\n", soma (2, 2));
printf ("%8.6lf\n", soma (2, 5));
printf ("%8.6lf\n", soma (1, 100));
return 0;
}

Neste exemplo, a variável contadora de repetições assume valores de a , tendo


associação direta com o expoente do termo geral. A lógica fica reduzida a acumular
em o valor de .

Ao executar este programa, teremos a seguinte saída:

Introdução à Programação: Conceitos e Práticas. Página 194


7.000000
63.000000
101.000000

A desvantagem desta solução é que a cada ciclo é calculado integralmente o valor


de , o que torna esta lógica ineficiente. Por exemplo, quando , o termo é
calculado de forma completa, com as nove multiplicações. Porém, no ciclo anterior
foi calculado o valor de , caso fosse aproveitado este esforço computacional,
bastaria agregar mais uma multiplicação no ciclo atual para chegar em . Assim, a
seguinte lógica melhora a eficiência para esta série:

double soma (double x, int n) {


int i; double s, p;
s = 0;
p = 1;
i = 0;
while (i <= n) {
s += p;
p *= x;
i++;
}

return s;
}

Esta solução utiliza dois acumuladores: e . A variável representa cada parcela,


que é calculada efetiamdp a acumulação do produto de ao longo das repetições.
Esta lógica está codificada na instrução . O cuidado que deve ser tomado ao
acumular um produto, é o de inicializar a variável adequadamente, que neste caso é
inicializado com . A variável acumula a soma das parcelas com .

Uma nova variante para a implementação desta série é dada pela seguinte
transformação, para alguns termos:

( ( ( )))

Assim, a lógica pode ser melhorada para esta série:

double soma (double x, int n) {


int i; double s;
s = 0;
i = 0;
while (i <= n) {
s = s*x + 1;
i++;
}

return s;
}

Esta solução é a mais eficiente, pois apresenta , e


por ciclo de execução do .

Introdução à Programação: Conceitos e Práticas. Página 195


vii. Refazer o programa que calcula o seno de um ângulo através da série:

A lógica do já codificada foi limitada aos primeiros seis termos e se resumiu a


transcrição da série em uma expressão aritmética com a soma explícita de todas as
seis parcelas. Agora, com o recurso da repetição podemos calcular o seno com
melhor precisão. A primeira versão será a mais básica e intuitiva, pois utilizará e
para gerar os termos.

#include <stdio.h>
#include <math.h>

double reduzir (double x) {


return fmod (x, 2*M_PI);
}

double torad (double graus) {


return graus*M_PI/180;
}

double fat (int n) {


double f; int i;
i = f = 1;
while (i <= n) {
f *= i;
i++;
}
return f;
}

double seno (double x) {


double s;
int t, i;
x = reduzir (x);
t = 1;
s = 0;
i = t = 1;
while (i <= 15) {
s += t*pow(x, 2*i-1)/fat(2*i-1);
t = -t;
i++;
};

return s;
}

int main () {
double Graus, Rad, Y1, Y2;
int i = 0;
while (i <= 24) {
Graus = i*15;
Rad = torad (Graus);
Y1 = sin (Rad);
Y2 = seno (Rad);
printf ("%4d %4.0lf %18.15lf %18.15lf %18.15lf\n",

Introdução à Programação: Conceitos e Práticas. Página 196


i, Graus, Y1, Y2, Y1-Y2);
i++;
}
return 0;
}

A função inclui uma repetição para o cálculo de termos, onde o termo geral
tem a seguinte forma:

A menos da obtenção do sinal, a expressão que codifica cada termo tem exatamente
este formato. O sinal foi gerado a partir da variável que alterna entre e a
cada ciclo. Isto evita utilizar apenas para obter e .

O testa a função em comparação à função interna do , e apresenta


os valores dos senos e a diferença entre estes valores. O de vezes ( a )
serve para gerar os ângulos de a de em graus, através da
expressão .

Ao executar este programa, teremos a seguinte saída:

I X SIN(X) SENO(X) ERRO


0 0 0.000000000000000 0.000000000000000 0.000000000000000
1 15 0.258819045102521 0.258819045102521 0.000000000000000
2 30 0.500000000000000 0.500000000000000 0.000000000000000
3 45 0.707106781186547 0.707106781186547 0.000000000000000
4 60 0.866025403784439 0.866025403784438 0.000000000000000
5 75 0.965925826289068 0.965925826289068 0.000000000000000
6 90 1.000000000000000 1.000000000000000 -0.000000000000000
7 105 0.965925826289068 0.965925826289068 0.000000000000000
8 120 0.866025403784439 0.866025403784439 -0.000000000000000
9 135 0.707106781186548 0.707106781186548 0.000000000000000
10 150 0.500000000000000 0.500000000000000 0.000000000000000
11 165 0.258819045102521 0.258819045102521 0.000000000000000
12 180 0.000000000000000 0.000000000000000 -0.000000000000000
13 195 -0.258819045102521 -0.258819045102521 0.000000000000000
14 210 -0.500000000000000 -0.500000000000000 -0.000000000000000
15 225 -0.707106781186547 -0.707106781186547 -0.000000000000000
16 240 -0.866025403784438 -0.866025403784436 -0.000000000000003
17 255 -0.965925826289068 -0.965925826289051 -0.000000000000018
18 270 -1.000000000000000 -0.999999999999913 -0.000000000000087
19 285 -0.965925826289068 -0.965925826288596 -0.000000000000472
20 300 -0.866025403784439 -0.866025403782136 -0.000000000002302
21 315 -0.707106781186548 -0.707106781176110 -0.000000000010438
22 330 -0.500000000000000 -0.499999999955982 -0.000000000044019
23 345 -0.258819045102521 -0.258819044928400 -0.000000000174121
24 360 -0.000000000000000 0.000000000000000 -0.000000000000000

A diferença em relação a função da biblioteca aumenta a medida que o ângulo se


aproxima de .

A lógica implementada é ineficiente, pois não aproveita os cálculos feitos no ciclo


anterior, o que aumenta consideravelmente a quantidade de multiplicações e

Introdução à Programação: Conceitos e Práticas. Página 197


divisões para obter o resultado final. Este mesmo resultado pode ser obtido usando
uma onde um termo atual pode ser definido em função do
termo calculado anteriormente. Observa-se nas equações abaixo, que dois termos
consecutivos possuem uma relação entre eles cujo estilo se conserva ao longo da
série.

O termo atual é o termo anterior multiplicado por e dividido pelo produto de dois
números, que também podem ser generalizados em função da posição do termo na
série. Assim, temos a seguinte relação de recorrência:

Onde:

Assim, a seguinte solução é preferida em relação a anterior que utiliza e :

double seno (double x) {


double p, s;
int i;
x = reduzir (x);
s = 0;
p = x;
i = 1;
while (i <= 15) {
s = s + p;
p = -p*x*x/((2*i)*(2*i+1));
i++;
};

return s;
}

Introdução à Programação: Conceitos e Práticas. Página 198


Ao executar este programa, teremos a seguinte saída:

I X SIN(X) SENO(X) ERRO


0 0 0.000000000000000 0.000000000000000 0.000000000000000
1 15 0.258819045102521 0.258819045102521 0.000000000000000
2 30 0.500000000000000 0.500000000000000 0.000000000000000
3 45 0.707106781186547 0.707106781186547 0.000000000000000
4 60 0.866025403784439 0.866025403784438 0.000000000000000
5 75 0.965925826289068 0.965925826289068 0.000000000000000
6 90 1.000000000000000 1.000000000000000 -0.000000000000000
7 105 0.965925826289068 0.965925826289068 0.000000000000000
8 120 0.866025403784439 0.866025403784439 -0.000000000000000
9 135 0.707106781186548 0.707106781186548 -0.000000000000000
10 150 0.500000000000000 0.500000000000000 -0.000000000000000
11 165 0.258819045102521 0.258819045102521 0.000000000000000
12 180 0.000000000000000 0.000000000000000 -0.000000000000000
13 195 -0.258819045102521 -0.258819045102521 0.000000000000000
14 210 -0.500000000000000 -0.500000000000000 0.000000000000000
15 225 -0.707106781186547 -0.707106781186547 -0.000000000000000
16 240 -0.866025403784438 -0.866025403784436 -0.000000000000003
17 255 -0.965925826289068 -0.965925826289054 -0.000000000000015
18 270 -1.000000000000000 -0.999999999999912 -0.000000000000088
19 285 -0.965925826289068 -0.965925826288598 -0.000000000000470
20 300 -0.866025403784439 -0.866025403782133 -0.000000000002306
21 315 -0.707106781186548 -0.707106781176113 -0.000000000010435
22 330 -0.500000000000000 -0.499999999955984 -0.000000000044016
23 345 -0.258819045102521 -0.258819044928398 -0.000000000174122
24 360 -0.000000000000000 0.000000000000000 -0.000000000000000

Além da sensível redução na quantidade de cálculos esta solução apresentou um


erro levemente menor em relação a função interna quando comparado ao erro
apresentado pela lógica anterior. Mais adiante, no tópico seguinte, será apresentada
uma solução que reduz ainda mais o erro.

viii. Refazer o programa que calcula o seno de um ângulo a fim de que não tenha
diferença com a função interna do :

Para praticamente zerar a diferença em relação a função interna do , faremos uma


transformação no cálculo do seno. A série será mais rapidamente convergente nos
casos que , e de preferência com o ângulo mais perto de zero. Assim,
faremos algumas transformações para reduzir o ângulo a um menor valor
equivalente. Segue a transformação proposta:

( )

( ) ( )( )

( )√ ( )( )

A equação indica que se calcularmos o podemos chegar ao pela


última expressão, ou seja:

Introdução à Programação: Conceitos e Práticas. Página 199


(√ )
Onde:
( )

Segue a solução proposta, cujos detalhes ficam a cargo do leitor analisar:

#include <stdio.h>
#include <math.h>

double reduzir (double x) { return fmod (x, 2*M_PI); }

double torad (double graus) { return graus*M_PI/180; }

double sen (double x) {


double p, s, S;
int i;
s = 0; S = x; p = x; i = 2;
while (s != S) {
S = s;
s = s + p;
p = -p*x*x/(i*(i+1));
i += 2;
};

return s;
}

double seno (double x) {


double s;
x = reduzir(x)/4;
s = sen(x);
s = 4*s*sqrt(1-s*s)*(1-2*s*s);

return s;
};

int main () {
double Graus, Rad, Y1, Y2;
printf (" I X SIN(X) SENO(X)
ERRO\n");
int i = 0;
while (i <= 24) {
Graus = i*15;
Rad = torad (Graus);
Y1 = sin (Rad);
Y2 = seno (Rad);
printf ("%4d %4.0Lf %18.15Lf %18.15Lf %18.15Lf\n",
i, Graus, Y1, Y2, Y1-Y2);
i++;
}
return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 200


A função que calcula o seno de um ângulo implementa uma lógica indireta, onde
primeiro o ângulo é reduzido ao primeiro quadrante ( ). Na
sequência o seno desejado é obtido levando este valor de para a expressão que
relaciona e , conforme indica o código abaixo:

A lógica do módulo principal, que calcula o valor da série, foi alterada para utilizar o
com o critério de parada definido quando dois cálculos sucessivos não
acrescentam precisão alguma ao resultado.

Observar na saída obtida que a diferença foi praticamente zerada.

I X SIN(X) SENO(X) ERRO


0 0 0.000000000000000 0.000000000000000 0.000000000000000
1 15 0.258819045102521 0.258819045102521 0.000000000000000
2 30 0.500000000000000 0.500000000000000 0.000000000000000
3 45 0.707106781186547 0.707106781186547 0.000000000000000
4 60 0.866025403784439 0.866025403784438 0.000000000000000
5 75 0.965925826289068 0.965925826289068 0.000000000000000
6 90 1.000000000000000 1.000000000000000 0.000000000000000
7 105 0.965925826289068 0.965925826289068 -0.000000000000000
8 120 0.866025403784439 0.866025403784439 0.000000000000000
9 135 0.707106781186548 0.707106781186547 0.000000000000000
10 150 0.500000000000000 0.500000000000000 0.000000000000000
11 165 0.258819045102521 0.258819045102521 -0.000000000000000
12 180 0.000000000000000 0.000000000000000 -0.000000000000000
13 195 -0.258819045102521 -0.258819045102521 -0.000000000000000
14 210 -0.500000000000000 -0.500000000000001 0.000000000000001
15 225 -0.707106781186547 -0.707106781186548 0.000000000000000
16 240 -0.866025403784438 -0.866025403784438 -0.000000000000000
17 255 -0.965925826289068 -0.965925826289068 0.000000000000000
18 270 -1.000000000000000 -1.000000000000000 0.000000000000000
19 285 -0.965925826289068 -0.965925826289069 0.000000000000000
20 300 -0.866025403784439 -0.866025403784439 0.000000000000001
21 315 -0.707106781186548 -0.707106781186549 0.000000000000002
22 330 -0.500000000000000 -0.499999999999998 -0.000000000000003
23 345 -0.258819045102521 -0.258819045102516 -0.000000000000005
24 360 -0.000000000000000 0.000000000000000 -0.000000000000000

Introdução à Programação: Conceitos e Práticas. Página 201


22.4. Repetição – FOR

Podemos observar nos exemplos anteriores. que em diversas circunstâncias foram


utilizadas a seguinte lógica:

i = 0;
while (i <= n) {
...
i++;
}

Em função deste tipo de solução ser muito frequente, a linguagem disponibiliza


uma instrução que compacta a referida construção.

22.4.1. Sintaxe e Semântica

A sintaxe da instrução de repetição tipo é:

Onde,

 é a expressão que será executada antes de iniciar a repetição;


 é a expressão lógica que define a condição para entrada na repetição;
 é a expressão que será processada a cada ciclo de execução das
instruções controladas;

A seguinte figura ilustra os componentes da instrução , bem como permite


visualizar o fluxo do processamento:

Figura 62: Estrutura de Controle do tipo for

Introdução à Programação: Conceitos e Práticas. Página 202


22.4.2. Exemplos

i. Implementar uma solução para o cálculo do fatorial de um inteiro:

O fatorial de um é o produto de todos os inteiros entre e . A proposta é utilizar a


instrução com a variável contadora de repetição produzindo os valores
{ }, e para cada um destes valores utilizar a técnica de acumular o produto
de em uma variável. O seguinte código implementa o cálculo do fatorial.

#include <stdio.h>

int fat (int n) {


int f, i;
f = 1;
for (i = 1; i <= n; i++)
f *= i;

return f;
};

int main () {
int k;
for (k = 0; k <= 30; k++)
printf ("%3d %d\n", k, fat(k));

return 0;
}

Porém há um problema crítico, que reside no fato que o fatorial de um número tem
um rápido crescimento dos valores, uma vez que há um acúmulo de multiplicação. A
função retorna um resultado do tipo , que tem de comprimento e
admite uma faixa de a . Este tamanho em bits para o
resultado limita ao valor , a partir da qual há um estouro de capacidade. A
solução é utilizar um tipo com faixa maior. Assim, a função foi testada para os tipos
e . As seguintes saídas foram obtidas para cada um destes
tipos:

int long long int double


0 1 0 1 0 1.000000000000000000e+000
1 1 1 1 1 1.000000000000000000e+000
2 2 2 2 2 2.000000000000000000e+000
3 6 3 6 3 6.000000000000000000e+000
4 24 4 24 4 2.400000000000000000e+001
5 120 5 120 5 1.200000000000000000e+002
6 720 6 720 6 7.200000000000000000e+002
7 5040 7 5040 7 5.040000000000000000e+003
8 40320 8 40320 8 4.032000000000000000e+004
9 362880 9 362880 9 3.628800000000000000e+005
10 3628800 10 3628800 10 3.628800000000000000e+006
11 39916800 11 39916800 11 3.991680000000000000e+007
12 479001600 12 479001600 12 4.790016000000000000e+008
13 1932053504 13 6227020800 13 6.227020800000000000e+009
14 1278945280 14 87178291200 14 8.717829120000000000e+010
15 2004310016 15 1307674368000 15 1.307674368000000000e+012

Introdução à Programação: Conceitos e Práticas. Página 203


16 2004189184 16 20922789888000 16 2.092278988800000000e+013
17 -288522240 17 355687428096000 17 3.556874280960000000e+014
18 -898433024 18 6402373705728000 18 6.402373705728000000e+015
19 109641728 19 121645100408832000 19 1.216451004088320000e+017
20 -2102132736 20 2432902008176640000 20 2.432902008176640000e+018
21 -1195114496 21 -4249290049419214848 21 5.109094217170944000e+019
22 -522715136 22 -1250660718674968576 22 1.124000727777607700e+021
23 862453760 23 8128291617894825984 23 2.585201673888497800e+022
24 -775946240 24 -7835185981329244160 24 6.204484017332394100e+023
25 2076180480 25 7034535277573963776 25 1.551121004333098600e+025
26 -1853882368 26 -1569523520172457984 26 4.032914611266056500e+026
27 1484783616 27 -5483646897237262336 27 1.088886945041835200e+028
28 -1375731712 28 -5968160532966932480 28 3.048883446117138400e+029
29 -1241513984 29 -7055958792655077376 29 8.841761993739700800e+030
30 1409286144 30 -8764578968847253504 30 2.652528598121910300e+032

A linha sublinhada indica o valor de a partir da qual há um estouro de capacidade.


Assim, a opção é pela versão abaixo com o uso do tipo para o retorno da
função:

#include <stdio.h>

double fat (int n) {


double f; int i;
f = 1;
for (i = 1; i <= n; i++)
f *= i;

return f;
};

int main () {
int k;
for (k = 0; k <= 30; k++)
printf ("%3d %.10e\n", k, fat(k));

return 0;
}

A listagem para o tipo também chama atenção para o fato de que a partir de
certo valor perde-se a precisão, pois a quantidade de dígitos significativos é bastante
limitada, mesmo que a faixa de representação admita números de alta magnitude.
Para o tipo escolhido neste exemplo a capacidade é de dígitos
significativos, e fica evidente na listagem o complemento feito com zeros a partir de
igual a .

A recomendação é sempre evitar algoritmos que acumulem extensos produtos que


podem levar ao estouro de capacidade. A indicação é buscar mecanismos que
aproveitam os cálculos de ciclos anteriores, e observar se há possibilidade de
priorizar operações na expressão que contribua para manter os resultados
intermediários dentro de limites seguros. Isto pode ser obtido em algumas situações
priorizando divisões antes das multiplicações. Por exemplo, na seguinte expressão:

Introdução à Programação: Conceitos e Práticas. Página 204


Na situação em que os valores de , , e estejam próximos ao limite do tipo
definido, é praticamente certo que os produtos levarão ao estouro de capacidade e
consequentemente a um erro de lógica ou de execução. Esta mesma expressão
pode ser reescrita para avaliar primeiro as divisões:

ii. Elaborar um programa que leia vários números fornecidos via teclado e apresente ao
final a somatória destes valores.

A lógica deste problema é bastante simples e consiste em um onde um dado


deve ser lido e acumulado em uma variável. Ao encerrar a repetição o resultado é
impresso. Segue a solução proposta:

#include <stdio.h>

void lerdados (int n) {


int i;
double x, s;
s = 0;
printf ("entre com os dados\n");
for (i = 0; i < n; i++) {
scanf ("%lf", &x);
s += x;
};
printf ("soma = %7.2lf\n", s);
};

int main () {

lerdados (10);
return 0;
}

Ao executar este programa, teremos a seguinte saída:

entre com os dados


10
23
54
98
102
75
98
17
34.5
56.25
soma = 567.75

iii. Elaborar um programa semelhante ao anterior, porém o usuário pode interromper a


entrada de dados fornecendo o valor .

A lógica do problema anterior é adaptada para encerrar o antes do seu final,


testando pelo valor de .

Introdução à Programação: Conceitos e Práticas. Página 205


O recurso da linguagem que permite encerrar o e desviar o processamento para
a próxima instrução após o fim do é a instrução . Normalmente o
vem combinado com um teste de condição, que em caso positivo desvia o fluxo para
fora do . O seguinte diagrama ilustra visualmente a instrução inserida em
um .

Figura 63: Instrução BREAK.

Segue a solução proposta:

#include <stdio.h>

void lerdados (int n) {


int i;
double x, s;
s = 0;
printf ("entre com os dados\n");
for (i = 0; i < n; i++) {
scanf ("%lf", &x);
if (x == -1) break;
s += x;
};
printf ("soma = %7.2lf\n", s);
};

int main () {

lerdados (10);
return 0;
}

Ao executar este programa, teremos a seguinte saída:

Introdução à Programação: Conceitos e Práticas. Página 206


entre com os dados
10
20
30
40
10.5
20.5
30.5
-1
soma = 161.50

iv. Elaborar um programa semelhante ao anterior, porém fornecendo ao final a soma e a


média aritmética dos valores fornecidos.

O cálculo da média é feito dividindo a soma dos dados pela quantidade de dados
fornecidos. A primeira impressão poderia levar a uma solução utilizando o valor do ,
variável de controle do loop, acreditando que ela reterá a quantidade de termos.
Porém esta não é uma boa prática de programação, pois não há garantias para o
uso da variável de controle fora do . Ao encerrar a repetição, esta variável não
contém um valor confiável. Assim, a solução demanda que dentro do loop
resolvamos esta questão.

Segue a solução proposta:

#include <stdio.h>

void lerdados (int n) {


int i;
double x, s;
s = 0;
printf ("entre com os dados\n");
for (i = 0; i < n; i++) {
scanf ("%lf", &x);
if (x == -1) break;
s += x;
};
printf ("soma = %7.2lf\n", s);
printf ("media = %7.2lf", s/i);
};

int main () {

lerdados (10);
return 0;
}

Observe que a variável contadora de loop servirá também como indicador da


quantidade de valores lidos.

Ao executar este programa, teremos a seguinte saída:

Introdução à Programação: Conceitos e Práticas. Página 207


entre com os dados
10
20
30
10.5
20.5
30.5
-1
soma = 121.50
media = 20.25

v. Elaborar um programa semelhante ao anterior, porém fornecendo ao final a soma, a


média aritmética e o maior valor dentre os valores fornecidos.

Deve ser agregada ao programa anterior a lógica para obter o maior entre os
números fornecidos. A estratégia consiste em inicializar uma variável com um valor
pequeno, e dentro do loop atualizar esta variável toda vez que um valor fornecido ( )
for superior ao atual maior. Segue a lógica proposta:

#include <stdio.h>

void lerdados (int n) {


int i;
double maior, x, s;
maior = s = 0;
printf ("entre com os dados\n");
for (i = 0; i < n; i++) {
scanf ("%lf", &x);
if (x == -1) break;
if (x > maior) maior = x;
s += x;
};
printf ("soma = %7.2lf\n", s);
printf ("media = %7.2lf\n", s/i);
printf ("maior = %7.2lf", maior);
};

int main () {

lerdados (10);
return 0;
}

No código acima foram destacadas as linhas que tratam a lógica do maior:

 Inicializa a variável que conterá o maior valor:

 Para cada valor fornecido, verificar se o mesmo é maior que o maior atual, e
em caso positivo faça a substituição:

Ao executar este programa, teremos a seguinte saída:

Introdução à Programação: Conceitos e Práticas. Página 208


entre com os dados
10
20
30
10.5
20.5
30.5
-1
soma = 121.50
media = 20.25
maior = 30.50

Esta lógica tem um problema quando os valores fornecidos forem todos negativos.
Neste cenário, o programa informará que o maior número é . Uma melhoria na
estratégia seria inicializar a variável com o primeiro número informado, pois
assim fica assegurada a consistência da lógica.

#include <stdio.h>

void lerdados (int n) {


int i;
double maior, x, s;
s = 0;
printf ("entre com os dados\n");
scanf ("%lf", &x);
maior = x;
for (i = 0; i < n; i++) {
if (x == -1) break;
if (x > maior) maior = x;
s += x;
scanf ("%lf", &x);
};
printf ("soma = %7.2lf\n", s);
printf ("media = %7.2lf\n", s/i);
printf ("maior = %7.2lf", maior);
};

int main () {

lerdados (10);
return 0;
}

Ao executar este programa, teremos a seguinte saída:

entre com os dados


-10
-20
-45
-5
-100
-1
soma = -180.00
media = -36.00
maior = -5.00

Introdução à Programação: Conceitos e Práticas. Página 209


A alteração introduzida para melhorar a lógica do consiste em efetuar uma leitura de
dados antes de iniciar a repetição, e deslocar a leitura interna à repetição para o final do
. Com isto fica resolvido o problema da inicialização da variável .

22.5. Repetição DO-WHILE

O objetivo da construção de controle do tipo é prover um mecanismo


onde uma ou mais instruções são executadas uma ou mais vezes dependendo do
valor de uma expressão lógica. O ciclo é repetido enquanto o valor da expressão
lógica seja .

22.5.1. Sintaxe e Semântica

A sintaxe da instrução de repetição tipo é:

Onde é a condição que determina o controle da repetição.

A seguinte figura ilustra os componentes da instrução , bem como


destaca que a dinâmica da estrutura inclui vários ciclos de execução das instruções
controladas, onde o fim da repetição ocorre quanto a expressão lógica assume o
valor .

As instruções controladas são executas uma vez. Em seguida o valor da expressão


teste do é avaliada. Caso seu valor seja o fluxo do processamento é
desviado para o início das instruções controladas. O é encerrado quando o
valor da expressão teste for . É importante destacar que em o é o
inteiro eo é qualquer inteiro diferente de .

A construção é menos utilizada do que suas equivalentes e .

Introdução à Programação: Conceitos e Práticas. Página 210


22.5.2. Exemplos

i. Elaborar um programa que leia um inteiro e apresente um dígito por linha, iniciando
pelo dígito menos significativo. Segue a lógica proposta:

#include <stdio.h>

int main () {
int x;

scanf ("%d", &x);


do {
printf ("%d\n", x % 10);
x /= 10;
} while (x != 0);

return 0;
}

A lógica consiste em calcular o dígito decimal que está na posição da unidade e


imprimi-lo na tela. Em seguida o conteúdo da variável é substituído pelo valor
anterior dividido por , cujo efeito é deslocamento de todos os dígitos uma posição
a direita e a eliminação do dígito da unidade. O processo se repete até que alcance
o valor zero.

Ao executar este programa, teremos a seguinte saída:

12345
5
4
3
2
1

Neste exemplo o valor inicial de é . A expressão resulta em . Este


valor é apresentado na tela. A expressão atualiza dividindo o valor atual
por , resultando em . Como o conteúdo de não é zero, então o fluxo de
processamento retorna ao início do bloco , executando novamente a
lógica descrita. O encerramento da repetição ocorre quando alcançar zero.

ii. Elaborar um programa que lê vários números de a , e compõe um inteiro


conforme indicado na tabela abaixo. O valor indica fim da entrada de dados.

Entrada Saída
1 4321
2
3
4
-1

Segue a lógica proposta:

Introdução à Programação: Conceitos e Práticas. Página 211


#include <stdio.h>

int main () {
int digit, x;

digit = x = 0;
do {
x = 10*x + digit;
scanf ("%d", &digit);
} while (digit != -1);

printf ("%d\n", x);

return 0;
}

A lógica consiste em deslocar os dígitos de a esquerda com a operação


e depois adicionar o fornecido. A cada ciclo os dígitos se deslocam a
esquerda e um novo dígito entra na posição da unidade..

Ao executar este programa, teremos a seguinte saída:

6
5
4
3
2
1
-1
654321

Introdução à Programação: Conceitos e Práticas. Página 212


23. Array

Os tipos de dados vistos até o momento são de natureza escalar, ou seja, permitem
definir variáveis com capacidade para representar apenas um elemento. Uma
variável do tipo tem capacidade para armazenar apenas um número em
determinado instante. O conteúdo pode ser alterado, porém sempre será apenas um
valor por vez. Esta característica é inerente aos tipos escalares: , , ,
e .

Uma série de problemas práticos conduz a situações onde é fundamental manter


simultaneamente na memória uma coleção de elementos de uma determinada
natureza. Alguns exemplos seriam: nomes de alunos de uma turma, disciplinas de
um curso, notas de uma turma de uma disciplina em um bimestre, notas de uma
turma de uma disciplina em todos os bimestres, relação de professores de um curso,
temperaturas diárias em um ano, cotação da bolsa em cinco anos, vendas diárias de
uma loja, vendas diárias de todas as filiais, medições de grandezas, salários de
empregados de uma empresa, e etc.

Os exemplos citados poderiam ser submetidos a lógicas para obter: média de cada
aluno em uma disciplina ordenados de várias formas, livro de chamada, relação de
professores por curso, disciplina que mais reprova, regressão linear de grandezas
medidas, semana com a menor temperatura média, folha de pagamento, e etc.

Assim, é importante que uma linguagem de programação permita modelar e


processar entidades que representam uma coleção de dados. Em contraste ao tipo
escalar, o tipo de dado voltado para representar simultaneamente vários elementos
é denominado tipo vetorial.

A linguagem disponibiliza o tipo para definir variáveis capazes de


armazenar uma coleção de elementos. Com este tipo de dado é possível modelar
problemas que precisam dos conceitos de e .

Introdução à Programação: Conceitos e Práticas. Página 213


23.1. Vetor

Um vetor é uma estrutura composta por uma quantidade conhecida de elementos,


sendo todos do mesmo tipo. O vetor também é chamado de agregado homogêneo.
A seguinte figura compara uma variável com uma variável do :

X: V:

Tabela 1: Comparação Escalar x Vetor

A variável é do tipo escalar (p. ex. ) e a variável é do tipo vetor ou .


Apenas uma célula de memória está disponível para uso através de , enquanto que
através de várias células estão disponíveis para o programa. A variável foi
declarada como , e declarada como [ ]

A linguagem é essencialmente escalar, sendo que as expressões refletem esta


característica e operam somente sobre dados escalares. Assim, as células de um
vetor devem ser refenciadas individualmente, o que demanda que a variável seja
complementada com uma informação que identifique especificamente uma célula.
Este complemento é o , que indica a sua posição no vetor. Por
exemplo, a primeira célula seria [ ] , a segunda [ ] e assim sucessivamente.
Agora temos a definição completa do nome da célula vinculada a um elemento do
vetor. Esta célula individualizada preenche todos requisitos de uma variável escalar,
e também é denominada .

23.1.1. Sintaxe e Semântica

O vetor é um com apenas uma dimensão, também denominado


unidimensional. A sintaxe simplificada para a definição do tipo vetor é:

[ ]

A de elementos de um array é necessariamente um valor inteiro.

Introdução à Programação: Conceitos e Práticas. Página 214


O seguinte exemplo ilustra as várias possibilidades de declaração de uma variável
do tipo . À direita são apresentadas algumas atribuições às células de cada
, permitindo também compreender o processo de indexação.

int main () {
char s[100];
int vi[50];
float vf[200];
double vd[150];
long long int vl[1000];
unsigned int vu[300];

s[0] = 'a';
vi[3] = -1;
vf[0] = 1.25;
vd[10] = -3.18e300;
vl[2] = 0x0102030405060708;
vu[4] = -2;
}

A declaração [ ] faz a reserva de espaço de memória o suficiente para


acomodar elementos do tipo . Na sequência, a declaração [ ] faz a
reserva de espaço de memória o suficiente para acomodar elementos do tipo .
As demais declarações tem efeito semelhante.

O primeiro elemento de um possui índice . As células serão de tamanho


suficiente para acomodar um elemento do tipo base.

Na utilização de um elemento de um , o índice deve ser uma expressão que


produza um resultado do tipo inteiro. Seguem alguns exemplos de índices com
expressões cujo resultado é inteiro.

int main () {
char s[100];
int vi[50];
int i;

vi[i - 'a'] = 0;
s[i % 2] = '\n';
s[vi[0]] = 'x';

Introdução à Programação: Conceitos e Práticas. Página 215


Considere a seguinte declaração [ ] como
sendo um com dez elementos do tipo
inteiro. A figura ao lado utiliza a notação de
para ilustrar o conceito. A referência
está associada a sala composta por várias
cadeiras. Como a linguagem é uma linguagem
escalar, onde seus recursos lidam apenas com
cadeiras que comportam um valor de cada vez,
logo é necessário fazer referência direta às
cadeiras. Desta forma, o nome de uma dada
cadeira é obtido acrescentando um índice ao
nome da sala. A primeira cadeira é referenciada
diretamente por [ ], a próxima será [ ], até a
última cadeira cujo índice será o – .

O seguinte código atribui o valor para cada


cadeira:

int main () {
int v[10], i;

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


v[i] = -1;

A figura também atribui endereços ilustrativos a


cada cadeira. A primeira cadeira ( [ ] ) está
associada ao endereço hipotético .
Considerando que a cadeira é do tipo , e que
neste caso, o tamanho do é de bytes, logo a
segunda cadeira ( [ ] ) iniciaria no endereço
e assim sucessivamente.

A cadeira [ ] ocuparia os bytes dos endereços


, , e .

Os exemplos que seguem ilustram várias


aplicações para vetores.
Figura 64: Ilustração de array com
cadeiras

Introdução à Programação: Conceitos e Práticas. Página 216


23.1.2. Exemplos

i. O usuário deverá informar uma sequência de inteiros. Estes valores devem ser
armazenados em um vetor. O programa apresentará os valores informados em
ordem invertida. O conteúdo do vetor não é alterado. O primeiro valor informado
indica a quantidade de elementos do vetor.

Entrada Saída
4
75
12
42
28
28
42
12
75

A lógica deste problema será alterada em relação às várias versões já


implementadas. A parte de entrada de dados será separada da parte de
processamento dos dados. Assim, um módulo fará a leitura dos dados e os
armazenará em um vetor e outros módulos farão os cálculos que forem necessários.
O programa principal fará a junção destes módulos para atender ao requisito do
problema. Segue a solução proposta:

#include <stdio.h>

void lerdados (int vx[], int *nx) {


int i;

scanf ("%d", nx);


for (i = 0; i < *nx; i++)
scanf ("%d", &vx[i]);
}

void printinv (int vx[], int nx) {


int i;

for (i = nx-1; i >= 0; i--)


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

int main () {
int v[100], n;

lerdados (v, &n);


printf ("\n");
printinv (v, n);

return 0;
}

Este exercício ilustra vários conceitos. Inicialmente temos a declaração das


seguintes variáveis na função :

[ ]

Introdução à Programação: Conceitos e Práticas. Página 217


Esta declaração faz a reserva de memória para um escalar inteiro e para um vetor
capaz de armazenar inteiros. O enunciado do problema define que o usuário
informará um inteiro que indicará a quantidade de valores que serão fornecidos na
sequência. Logo, não é obrigatório que os casos tenham necessariamente
elementos. Este é apenas o limite da . É neste momento que entra a variável
. Ela irá conter a quantidade de cadeiras da que de fato foram carregadas
com valores que são parte do problema. É praticamente uma regra que quando
houver um problema envolvendo um vetor, será necessário manter uma variável que
conterá a quantidade de valores válidos neste vetor.

Na sequência do temos a lógica aplicada sobre estes dados:

A função tem o seguinte cabeçalho:

[]

Esta função é responsável pela leitura dos dados de entrada, sendo fornecido
primeiramente a quantidade de elementos, e em seguida os valores do vetor. Assim,
deve alterar variáveis que estão declaradas em outra função. No tópico
sobre passagem de parâmetros foi explicado que ao defrontar com esta situação, a
forma de passagem de parâmetros deve ser por endereço ou referência. A função
que irá alterar os dados deve receber os endereços das variáveis a serem alteradas.
Observe que o segundo parâmetro foi declarado como sendo uma cadeira chamada
preparada para receber um valor que significa ou endereço de um inteiro.

O primeiro parâmetro foi definido com o nome [] . Isto significa que é um


parâmetro do tipo vetor de inteiro. A Linguagem dispensa, com esta notação, a
necessidade de usar ponteiros. Sempre que um parâmetro for definido como vetor
haverá uma vinculação direta com o argumento, e tudo que for alterado no parâmetro
refletirá no argumento. Há uma definição implícita na passagem de parâmetro de
como sendo da natureza de passagem por endereço.

O corpo da função é:

[]

Observe que a instrução utiliza a expressão para indicar o


endereço onde deve ser armazenado o valor digitado. Não é utilizado , pois
estaríamos incorrendo em um erro. Em temos a chamada ,
onde o primeiro argumento é o nome do vetor que ficará vinculado com o
parâmetro . Na sequência é passado o valor da expressão , fazendo com que o
valor do endereço de seja copiado para o parâmetro . Assim, o parâmetro
terá o endereço de memória onde deverá ser armazenado o inteiro que define a
quantidade de elementos do vetor a ser lido.

Na linha seguinte a lógica codifica uma repetição onde a variável inicia com o
índice da primeira cadeira e cresce de em ( ) até alcançar o índice do último

Introdução à Programação: Conceitos e Práticas. Página 218


elemento do vetor . Para cada valor de é lido um inteiro e
armazenado na cadeira [ ]: [ ] . Isto encerra a função ,
retornando o fluxo de execução para a função .

Em , é executada a linha de código que salta uma linha na tela


apenas para separar a entrada de dados da saída de dados que virá na sequência.

A função chamada passa a execução para a função com os


argumentos e . O cabeçalho desta função é dado por:

[]

Assim, o parâmetro estará vinculado ao argumento , enquanto que o parâmetro


receberá uma cópia do valor do argumento .

A função executa a seguinte lógica:

[]

É utilizada uma repetição onde a variável inicia com o índice do último elemento do
vetor e decresce de em ( ) até alcançar o índice , que
corresponde a primeira cadeira. Para cada valor de é apresentado na tela o valor
que está na cadeira [ ]: []

Ao executar este programa, teremos a seguinte saída:

4
12
28
42
75

75
42
28
12

ii. Reescrever o programa que lê vários números fornecidos via teclado e apresente ao
final algumas estatísticas, tais como: somatória destes valores, média, maior valor, e
etc.

A lógica deste problema será alterada em relação às versões já implementadas. A


parte de entrada de dados será separada da parte de processamento dos dados.
Assim, um módulo fará a leitura dos dados e os armazenará em um vetor e outros
módulos farão os cálculos que forem necessários. A função fará a organização
destes módulos para atender ao requisito do problema. Segue a solução proposta:

#include <stdio.h>

void lerdados (double vx[], int *nx) {

Introdução à Programação: Conceitos e Práticas. Página 219


int i; double x;

printf ("Entre com os dados\n");


scanf ("%lf", &x);
for (i = 0; x != -1; i++) {
vx[i] = x;
scanf ("%lf", &x);
}
*nx = i;
}

double somavet (double vx[], int nx) {


int i; double s;

for (s = i = 0; i < nx; i++)


s += vx[i];

return s;
}

double getmaior (double vx[], int nx) {


int i; double m;

m = vx[0];
for (i = 1; i < nx; i++)
if (vx[i] > m)
m = vx[i];

return m;
}

int main () {
double v[100];
int n;
double soma, media, maior;

lerdados (v, &n);


soma = somavet (v, n);
media = soma/n;
maior = getmaior (v, n);
printf ("SOMA = %7.2lf\n", soma);
printf ("MEDIA = %7.2lf\n", media);
printf ("MAIOR = %7.2lf" , maior);

return 0;
}

O problema foi modularizado e deu origem a três funções: , e


. A função organiza as chamadas a fim de atender a especificação do
problema. Observe que no corpo da , inicialmente é invocada a função
, que interage com o usuário fazendo a aquisição de dados e armazenando
na memória. Em seguida, três linhas de código fazem os cálculos da , e
do . Seguem os comentários de cada módulo:

A função está definida através do seguinte cabeçalho:

[]

Introdução à Programação: Conceitos e Práticas. Página 220


Esta definição informa que são necessários dois parâmetros passados por endereço.
Isto indica que o trabalho feito pela rotina será devolvido ao módulo chamador
através dos parâmetros e . É importante destacar que será prática comum na
manipulação de arrays que além da variável com os elementos, também
acompanhará a variável que indica a quantidade de elementos efetivamente em
uso. Isto ocorre normalmente pelo fato da definição da variável array sempre
reservar um tamanho máximo fixo, e de fato as lógicas trabalham com qualquer
quantidade de elementos até este limite máximo. Esta quantidade deve ser indicada
para as rotinas que processarão o vetor.

Voltando à rotina , temos que o primeiro parâmetro é do tipo e o


segundo é do tipo . O tipo array em faz automaticamente o vínculo entre
parâmetro e argumento.

A especificação do módulo é efetuar a leitura de alguns números via


teclado e armazená-los no parâmetro . Além disso, a quantidade de dados lidos
será armazenada no endereço indicado pelo parâmetro ..

A lógica interna de consiste em:

int i; double x; Define contador e espaço para um double


printf ("Entre com os Escrever mensagem na tela
dados\n"); Ler um double e armazenar em
scanf ("%lf", &x); Loop enquanto valor digitado
for (i = 0; x != -1; i++) { Copia o para uma posição do vetor
vx[i] = x; Ler dado e armazenar em
scanf ("%lf", &x); Incrementar contador e voltar fluxo para início do
} loop
*nx = i; Armazena em *nx a quantidade lida

O seguinte diagrama lógico ilustra graficamente o fluxo do processamento:

Introdução à Programação: Conceitos e Práticas. Página 221


Vamos supor que o programa está em execução e durante a sessão de execução o
usuário entrou com os seguintes números: , , e . Nestas condições
apresentamos na sequência o comportamento da memória nos vários ciclos da
repetição:

a) Inicialmente é zero, o usuário fornece o número , a condição do loop é testada, o


processamento entra no bloco , armazena o valor de na célula [ ] e
ainda no interior do faz uma nova leitura de dado e incrementa que passa a
conter .

b) O usuário fornece o número , a condição do loop é testada, o processamento


entra no bloco , armazena o valor de na célula [ ] e ainda no interior do
faz uma nova leitura de dado e incrementa que passa a conter .

c) O usuário fornece o número , a condição do loop é testada, o processamento


entra no bloco , armazena o valor de na célula [ ] e ainda no interior
do faz uma nova leitura de dado e incrementa que passa a conter .

d) O usuário fornece o número , a condição do loop é testada e indica o fim da


repetição;

e) A execução passa pela instrução que armazena a quantidade de valores


fornecidos no endereço indicado por .

Após o final do loop a variável conterá , indicando a quantidade de elementos


armazenados, e as três primeiras células do vetor conterão os dados fornecidos.
O fluxo de execução retorna para a unidade chamadora, que neste caso é a função
, e avança para a próxima linha de código.

lerdados (v, &n); Chamar com e


soma = somavet (v, n); Obter a soma com com e
media = soma/n; Obter a média
maior = getmaior (v, n); Obter o maior com com e
printf ("SOMA = %7.2lf\n", soma); Mostrar a soma
printf ("MEDIA = %7.2lf\n", media); Mostrar a média
printf ("MAIOR = %7.2lf" , maior); Mostrar o maior

Seguindo o fluxo, a atribuição avalia a expressão que


contém apenas a chamada a função com os argumentos e . A lógica
interna de consiste em:

double somavet (double vx[], int nx) O primeiro parâmetro é um vetor e o segundo
{ parâmetro é um inteiro que indica a quantidade de
elementos do vetor.
int i; double s; Variáveis para o e para acumular a soma.
Zerar a soma e o contador de repetições
for (s = i = 0; i < nx; i++) Repetir enquanto
s += vx[i]; Acumula em o valor de [ ]
Incrementar o contador de repetições

return s; Resultado da função em


}

Introdução à Programação: Conceitos e Práticas. Página 222


Encerrada a chamada de o controle volta para a unidade chamadora que é
a função , que completa o processamento da linha, atribuindo o resultado de
à variável .

Na sequência é executada a linha que avalia a expressão


indicada, calculando a divisão da soma pelo número de termo, e armazena o
resultado em .

A próxima linha é que invoca a função copiando


os argumentos e para os parâmetros desta função. A lógica interna de
consiste em:

double getmaior (double vx[], int nx) O primeiro parâmetro é um vetor e o segundo
{ parâmetro é um inteiro que indica a quantidade
de elementos do vetor.
int i; double m; Variáveis para o e para armazenar o maior

m = vx[0]; Iniciar com o primeiro elemento de


for (i = 1; i < nx; i++) Repetir a partir do 2º elemento até
if (vx[i] > m) Se o elemento [ ] maior atual
m = vx[i]; Então faça [ ] ser o
Incrementar o contador de repetições

return m; Resultado da função em maior


}

Encerrada a chamada de o controle volta para a unidade chamadora que é


a fnção , que completa o processamento da linha, atribuindo o resultado de
à variável .

As próximas três linhas:

escrevem na tela os conteúdos das três variáveis , e . A seguinte


interface de entrada e saída ilustra a interação do programa com o usuário:

Entre com os dados


25
32
18.2
-1
SOMA = 75.20
MEDIA = 25.07
MAIOR = 32.00

Introdução à Programação: Conceitos e Práticas. Página 223


iii. Escrever um programa que calcule o valor de a partir dos coeficientes do
polinômio e de um valor de :

A sugestão para este problema é estruturar a lógica nos módulos para:

a) Entrada de dados que obtém via teclado os valores de


e de e os armazena na memória;
b) Cálculo do valor de a partir de
c) Apresentar o vetor e os resultados na tela.

Segue a solução proposta:

#include <stdio.h>

void lerdados (double vx[], int *nx) {


int i; double x;

printf ("Entre com os dados\n");


scanf ("%lf", &x);
for (i = 0; x != -1; i++) {
vx[i] = x;
scanf ("%lf", &x);
}
*nx = i;
}

void printvet (double vx[], int nx) {


int i;

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


printf ("%8.3lf\n", vx[i]);
}

double polinomio (double vx[], int nx, double x) {


int i; double s = 0;

for (i = nx - 1; i >= 0; i--)


s = s*x + vx[i];

return s;
};

int main () {
double v[100];
int n;
double x, y;

lerdados (v, &n);


printf ("Entre com x = "); scanf ("%lf", &x);
y = polinomio (v, n, x);
printvet (v, n);
printf ("p(%.2lf) = %7.2lf\n", x, y);

return 0;

Introdução à Programação: Conceitos e Práticas. Página 224


}

A procedure é a mesma do exercício anterior. A função que calcula o valor


de foi implementada utilizando a seguinte forma, denominada fatoração de
:

( ( ( )))

A lógica é semelhante ao código aplicado para solucionar os exercícios sobre séries.


Compare as duas lógicas e identifique a pequena alteração feita.

A procedure apresenta na tela os coeficientes do polinômio permitindo ao


usuário inspecionar se foram considerados os dados certos.

A função ordena as chamadas na sequência lógica estabelecida para este


problema, e proporciona a seguinte interface:

Entre com os dados


1
2
3
-1
Entre com x = 3
v[0] = 1.000
v[1] = 2.000
v[2] = 3.000
p(3.00) = 34.00

iv. Escrever um programa que localiza a posição de um elemento em um vetor.

Problemas de busca são corriqueiros em grande parte dos problemas. Isto faz com
que se estude muito sobre métodos eficientes para este propósito. Neste exercício
faremos implementações básicas, ficando fora do escopo métodos mais elaborados.
Segue uma solução para a busca por um elemento em um vetor não ordenado:

#include <stdio.h>

void lerdados (double vx[], int *nx) {


int i; double x;

printf ("Entre com os dados\n");


scanf ("%lf", &x);
for (i = 0; x != -1; i++) {
vx[i] = x;
scanf ("%lf", &x);
}
*nx = i;
}

int buscar (double vx[], int nx, double x) {


int i;

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


if (vx[i] == x) return i;

Introdução à Programação: Conceitos e Práticas. Página 225


return -1;
}

int main () {
double v[100], x; int n, p;

lerdados (v, &n);


printf ("Entre com x = "); scanf ("%lf", &x);
p = buscar (v, n, x);
printf ("posicao de %.2lf = %d\n", x, p);

return 0;
}

Serão apresentadas várias versões de uma busca linear, onde os elementos do vetor
são percorridos a fim de encontrar a primeira ocorrência do elemento . Caso este
elemento não seja encontrado a função retorna .

A primeira versão utiliza a seguinte lógica:

int buscar (double vx[], int nx, double x) {


int i;

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


if (vx[i] == x) return i;

return -1;
}

É percorrido todo o vetor em busca de um elemento [ ] que coincida com , que


uma vez encontrado a repetição e a função são encerradas e o resultado será o valor
de . Caso a repetição complete a busca em todo o vetor sem encontrar uma
ocorrência de , então o fluxo de processamento segue para o encerramento da
função com resultado . Esta lógica efetua dois testes lógicos a cada ciclo: ,
para limitar a busca ao tamanho do vetor, e [ ] para verificar pelo valor
pesquisado. Estas verificações podem ser agrupadas na expressão de teste da
instrução , dando origem a uma versão levemente alterada, conforme segue:

int buscar (double vx[], int nx, double x) {


int i;

for (i = 0; i < nx && vx[i] != x; i++)


;

if (i < nx) return i;

return -1;
}

A estratégia é a mesma da implementação anterior, porém o considera a


expressão lógica que controla a repetição indicando que o deve ser encerrado
assim que encontrar um [ ] ou quando já percorreu todo o vetor sem nada
encontrar . Observe que não foi necessário incluir instrução alguma no ,
pois toda a lógica foi resolvida no seu próprio cabeçalho. Ao terminar o , o valor
de é testado para conhecer a razão que levou ao final da repetição. Caso o valor de

Introdução à Programação: Conceitos e Práticas. Página 226


não tenha extrapolado o tamanho do vetor, então temos a indicação de que foi
encontrada uma ocorrência de .

Uma terceira versão utiliza a seguinte lógica:

int buscar (double vx[], int nx, double x) {


int i;

vx[nx] = x;
for (i = 0; vx[i] != x; i++)
;

if (i < nx) return i;

return -1;
}

Esta função visa simplificar a expressão que controla o , fazendo com que reste
apenas um teste. Para que isto seja feito com segurança, observe que antes de
iniciar a repetição o elemento é inserido no final do vetor. Assim, há a certeza de
que pelo menos um elemento exista no vetor. Caso a busca indique que o
elemento encontrado está na posição , então significa que no vetor original o não
foi encontrado. Destaca-se também que a instrução controlada pelo é
simplemente o incremento de , cujo significado é simplesmente avançar a busca
para o próximo elemento. É importante certificar que a posição do vetor é área
válida de memória para o programa.

Segue uma interface de entrada e saída para um caso exemplo:

Entre com os dados


18
32
28
17
-1
Entre com x = 28
posicao de 28.00 = 2

Uma quarta versão considera o endereço da célula como retorno da função.


Tomando como base a primeira logica de busca, temos a seguinte função:

double *buscar (double vx[], int nx, double x) {


int i;

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


if (vx[i] == x) return &vx[i];

return NULL;
}

O retorno da função é alterado para que significa ponteiro para um


ou endereço de um . A função também deve ser alterada para se
adequar a esta versão de :

Introdução à Programação: Conceitos e Práticas. Página 227


int main () {
double v[100], x, *p; int n;

lerdados (v, &n);


printf ("Entre com x = "); scanf ("%lf", &x);
p = buscar (v, n, x);
printf ("posicao de %.2lf = %p\n", x, p);

return 0;
}

A variável que recebe o resultado de é do tipo . Além disso, o


resultado apresentado é o endereço da célula de cujo valor coincide com o valor de
.

Segue uma interface de entrada e saída para esta versão:

Entre com os dados


18
32
28
17
-1
Entre com x = 28
posicao de 28.00 = 0061FB98

O endereço apresentado vale para o ambiente computacional em que este programa


foi executado. Certamente em outros ambientes serão alocados outros endereços.

v. Escrever um programa que ordene um vetor de números em ordem crescente:

Segue a proposta de uma lógica bem simples:

#include <stdio.h>

void lerdados (double vx[], int *nx) {


int i; double x;

printf ("Entre com os dados\n");


scanf ("%lf", &x);
for (i = 0; x != -1; i++) {
vx[i] = x;
scanf ("%lf", &x);
}
*nx = i;
}

void printvet (double vx[], int nx) {


int i;

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


printf ("v[%2d] = %8.3lf\n", i, vx[i]);
}

Introdução à Programação: Conceitos e Práticas. Página 228


void trocar (double *a, double *b) {
double temp;
temp = *a;
*a = *b;
*b = temp;
};

int posmenor (double vx[], int p, int q) {


int m, i;

m = p;
for (i = p+1; i <= q; i++)
if (vx[i] < vx[m])
m = i;

return m;
};

void menorfirst (double vx[], int p, int q) {


int m, i;

m = posmenor (vx, p, q);


trocar (&vx[p], &vx[m]);
};

void ordenar (double vx[], int nx) {


int i;

for (i = 0; i < nx - 1; i++)


menorfirst (vx, i, nx-1);

};

int main () {
double v[100]; int n;

lerdados (v, &n);


ordenar (v, n);
printvet (v, n);

return 0;
}

O programa principal indica a sequência da lógica mais abstrata, que é:

1. Ler os dados do vetor para a memória;


2. executar a ordenação; e
3. apresentar o resultado na tela.

Os módulos e são semelhantes aos do exemplo anterior. A função


encaminha o vetor e seu tamanho para o processo de ordenação. A
procedure recebe estes argumentos nos parâmetros e . É importante
destacar que a passagem de parâmetro de array em é sempre por referência,
implicando que toda alteração no parâmetro reflete no argumento .

Introdução à Programação: Conceitos e Práticas. Página 229


void ordenar (double vx[], int nx) {
int i;

for (i = 0; i < nx - 1; i++)


menorfirst (vx, i, nx-1);

};

A lógica da função é:

1. Trazer o menor elemento entre e para a posição ;


2. Trazer o menor elemento entre e para a posição ;
3. Trazer o menor elemento entre e para a posição ;
4. ...
5. Trazer o maior elemento entre e para a posição .

A cada execução há uma permutação entre elementos de tal forma que o menor
valor encontrado em um domínio do vetor seja posicionado no início deste domínio.
Ao final desta sequência o vetor estará em ordem crescente. A forma geral desta
sequência é:

 Trazer o maior elemento entre e para a posição , com variando de


a . Esta lógica foi transformada em:

for (i = 0; i < nx - 1; i++)


menorfirst (vx, i, nx-1);

A rotina identifica a posição do maior elemento entre as posições e


e traz para a posição do vetor , como indicado no código abaixo:

void menorfirst (double vx[], int p, int q) {


int m, i;

m = posmenor (vx, p, q);


trocar (&vx[p], &vx[m]);
};

A abstração está novamente presente nesta rotina, pois trazer para a posição do
vetor o menor elemento entre e , foi codificado em duas linhas, sendo uma que
chama que retorna a posição do menor elemento de entre as posições
e . A segunda linha executa o módulo que os conteúdos das células e .

A função tem o seguinte código:

int posmenor (double vx[], int p, int q) {


int m, i;

m = p;
for (i = p+1; i <= q; i++)
if (vx[i] < vx[m])
m = i;

return m;

Introdução à Programação: Conceitos e Práticas. Página 230


};

Neste módulo a lógica utilizada assume que o primeiro elemento do intervalo dado é
a posição do menor elemento, e em seguida o vetor é percorrido do segundo
elemento do intervalo até o seu final, perguntando se o elemento é menor que o
menor atual. Em caso positivo, a posição do novo elemento assume como a posição
do menor. Ao final, este valor é retornado como resultado da função.

A função recebe dois parâmetros por referência, ou seja, dois endereços de


memória, e executa a troca dos valores apontados por estes parâmetros. Os
conteúdos das células indicadas pelos parâmetros são permutados. Uma variável
auxiliar é utilizada para não resultar em inconsistências nas movimentações.

O programa principal ordena as chamadas na sequência lógica estabelecida para


este problema, e proporciona a seguinte interface:

Entre com os dados


6
8
3
9
2
7
-1
v[ 0] = 2.000
v[ 1] = 3.000
v[ 2] = 6.000
v[ 3] = 7.000
v[ 4] = 8.000
v[ 5] = 9.000

A lógica utilizada é chamada de método de ordenação por seleção direta. Outros


métodos estão disponíveis e são objetos de disciplina específica. A título de
exemplo, segue o módulo que ordena pelo método denominado :

void bubblesort (float vx[], int nx) {


int i, j;

for(i = nx - 1; i > 0; i--)


for(j = 0; j < i; j++)
if(vx[j] > vx[j+1])
trocar(&vx[j], &vx[j+1]);

};

O primeiro passo é deslocar o maior valor de todo o vetor (da primeira a última
célula) para a última posição. O segundo passo é deslocar o maior elemento entre a
primeira e penúltima célula, para o final deste intervalo. Repetir este processo e
reduzindo o intervalo tratado até que restar apenas dois elementos. Ao final, o vetor
estará completamente ordenado. Formalizando estes passos, tem-se:

1) deslocar o maior valor de entre as posições


e para a posição ;

Introdução à Programação: Conceitos e Práticas. Página 231


2) deslocar o maior valor de entre as posições
e para a posição ;
3)
4) deslocar o maior valor de entre as posições e
para a posição ;
5) deslocar o maior valor de entre as posições e
para a posição ;

Assim, a função pode ser rescrita atribuindo uma abstração ao mais


interno, responsável pelo deslocamento do maior valor de um intervalo para o final
do intervalo:

void bubblesort (double vx[], int nx) {


int i;

for(i = nx - 1; i > 0; i--)


shiftmaior (vx, 0, i);
};

A função é responsável pelo deslocamento do maior valor de um vetor,


no intervalo entre as posições e , para a posição .

void shiftmaior (dobule v[], int a, int b)


{
int i;
for(i = a; i < b; i++)
if(v[i] > v[i+1])
trocar(&v[i], &v[i+1]);
}

A mesma estrutura poderia ser aplicada ao deslocamento do menor valor para o


início de um invervalo. Assim, teríamos a seguinte sequência de passos:

1) deslocar o menor valor de entre as posições


e para a posição ;
2) deslocar o menor valor de entre as posições
e para a posição ;
3)
4) deslocar o menor valor de entre as
posições e para a posição ;
5) deslocar o menor valor de entre as
posições e para a posição ;

Assim, a função pode ser rescrita deslocando o menor valor de um


intervalo para o início do intervalo:

void bubblesort (double vx[], int nx) {


int i;

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


shiftmenor (vx, i, nx-1);
};

Introdução à Programação: Conceitos e Práticas. Página 232


A função desloca o menor valor de um vetor, no intervalo entre as
posições e , para a posição .

void shiftmenor (dobule v[], int a, int b)


{
int i;
for(i = b; i > a; i--)
if(v[i] < v[i-1])
trocar(&v[i], &v[i-1]);
}

Introdução à Programação: Conceitos e Práticas. Página 233


23.2. Strings em C

As expressões textuais expandem nossa capacidade de processar informações para


além da manipulação de números. A linguagem apresentava nas suas
primeiras versões uma característica bem marcante que era a vocação para codificar
fórmulas matemáticas. Porém, a demanda por programas para tratamento de textos
impulsionou as linguagens no sentido de prover estes recursos.

Diversas linguagens incluíram o como tipo primitivo, bem como a


possibilidade de utilização de expressões textuais que combinam constantes,
variáveis, operadores e funções de tal forma a prover transformações sobre este tipo
de dado. A Linguagem não dispõem de um tipo específico para manipulação de
strings. Este recurso é disponibilizado como uma extensão do .

Uma em é definida como um contendo uma sequência de


caracteres, sendo utilizado o caractere como sendo o indicador de final
da string. Não há operador destinado para string, sendo que qualquer
processamento deve ser feito por meio de função específica, geralmente tratando
caractere a caractere. O indicador de final de string é o inteiro zero, podendo ser
expresso no formato caractere ou formato inteiro . Nunca deve ser confundido
com o símbolo cujo valor numérico é .

Observe o seguinte exemplo:

#include <stdio.h>

int main () {
char str[100];
char texto[100];
char dias[] = "DSTQQSS";
char vogais[] = {'a', 'e', 'i', 'o', 'u', '\0'};
char hexdigits[17] = "0123456789ABCDEF";

scanf ("%s", str);


scanf ("%s", texto);

printf ("%s\n", str);


printf ("%s\n", texto);
printf ("%s\n", dias);
printf ("%s\n", vogais);
printf ("%s\n", hexdigits);
}

As variáveis e são definidas como um de elementos do tipo


A variável é criada como de porém com tamanho definido
pela inicialização. Neste caso a string irá demandar bytes, sendo um
byte para cada caractere, acrescido de mais um byte para o indicador de final de
string ou o inteiro , cuja atribuição está implícita devido a notação utilizando o
formato . A variável é criada como de com tamanho também
definido pela inicialização. Neste caso a inicialização é individualizada para cada
caractere. O indicador de final de string deverá ser explicitamente atribuído pelo

Introdução à Programação: Conceitos e Práticas. Página 234


programador. A seguinte figura ilustra a configuração de memória para as variáveis
e .

dias[0] dias[1] dias[2] dias[3] dias[4] dias[5] dias[6] dias[7]


dias 'D' 'S' 'T' 'Q' 'Q' 'S' 'S' '\0'

vogais[0] vogais[1] vogais[2] vogais[3] vogais[4] vogais[5]


vogais 'a' 'e' 'i' 'o' 'u' '\0'

Figura 65: Armazenamento de string.

A declaração da variável inclui tanto a definição explícita do tamanho do


quanto inicializa seu conteúdo. Como a intenção é atribuir uma sequência de
caracteres que formam os símbolos da base hexadecimal, o tamanho foi fixado
em , reservando a última posição para o valor Devido ao uso da notação
para a constante literal, o é implicitamente inserido pela linguagem.

As variáveis e são inicializadas a partir da entrada de dados do usuário.


Considere as seguintes entradas e saídas relativas ao programa anterior:

ola mundo belo


ola
mundo
DSTQQSS
aeiou
0123456789ABCDEF

O usuário digitou , seguido de um . A linha de comando


utiliza o formato para indicar entrada de . Porém, este
formato orienta o a retirar do buffer do teclado uma sequência de caracteres
até alcançar um caractere separador, neste caso o espaço em branco. Assim, este
captura apenas o texto A linha de comando seguinte
utiliza o mesmo formato para indicar entrada de .
Restou no buffer do teclado a sendo que este irá capturar
apenas o texto para a variável .

Observar que diferentemente de variáveis escalares ( , , , ), onde


o endereço da variável era passado como parâmetro para a função no caso
do array de foi utilizado o nome da variável. Os detalhes serão explicitados na
seção que aborda a manipulação de por meio da notação ponteiro.
Antecipamos que no caso de (vetor), a expressão com o nome da variável
tem como valor o endereço da célula índice zero ( [ ] ). As seguintes
expressões produzem o mesmo efeito: , e [ ].

Caso o programador deseja atribuir toda a linha digitada para uma única string,
poderá então lançar mão da função . Assim, neste caso os dois podem
ser substituídos por e , como ilustra o seguinte resultado:

Introdução à Programação: Conceitos e Práticas. Página 235


ola mundo belo
separado em dois strings
ola mundo belo
separado em dois strings
DSTQQSS
aeiou
0123456789ABCDEF

Neste exemplo, a primeira linha digitada é acomodada integralmente na ,


enquanto que a segunda linha é atribuída à . No decorrer deste material,
outras alternativas de entrada de serão comentadas.

Quando o recurso foi apresentado, foi destacado que é usual manter uma
variável inteira cujo valor indica a quantidade de células do que deve ser
considerada naquele contexto da lógica. A em é um que dispensa a
presença desta variável, pois o flag é adotado para indicar a última célula válida
no presente contexto.

23.2.1. Operações com String

A Linguagem não disponibiliza de forma nativa operações para manipulação de


strings. Assim, todo e qualquer processamento deve ser implementado através de
funções. Na sequência serão codificadas algumas funções de uso frequente. Será
utilizada a notação indexada para acessar os caracteres do array. Mais adiante
veremos tanto a notação com acesso via ponteiros quanto as rotinas
disponibilizadas em biblioteca que acompanha a linguagem.

i. Escrever um programa que determina a quantidade de caracteres de uma string.

A solução consiste em percorrer cada caractere do array em busca do . Uma


variável é inicializada com o índice da primeira célula, valor zero, sendo
incrementada a cada caractere testado. Segue uma solução para a função que
determina a quantidade de caracteres de uma string:

#include <stdio.h>

int length (char src[]) {


int i;
for(i = 0; src[i] != '\0'; i++)
;
return i;
}

int main () {
char str[100];
gets (str);
printf ("tamanho = %d", length(str));
}

Introdução à Programação: Conceitos e Práticas. Página 236


A função tem como lógica principal a seguinte repetição:

for(i = 0; src[i] != '\0'; i++)


;

A segunda expressão do responsável em manter o , testa se o valor do


caractere na posição [ ] é o flag indicador de final de ,o . A repetição é
mantida até que este flag seja encontrado. Ao encerrar o loop, a variável conterá o
índice da posição em que o foi encontrado. O valor deste índice reflete a
quantidade de caracteres. Convém observar que a expressão [] poderia
também ser escrita como [] , pois tanto quanto refletem a mesma
quantidade , porém em notações distintas.

Uma variação desta lógica faz uso de um facilidade da linguagem onde


expressões booleanas são equivalentes a expressões inteiras. Assim, a instrução
mantem o até que o valor de seja .
Quando surgir um no espaço destinado à , o loop será encerrado. Desta
forma, a função pode ser reescrita como:

int length (char src[]) {


int i;
for(i = 0; src[i]; i++)
;
return i;
}

Segue uma interface de entrada e saída para um caso exemplo:

ola mundo
tamanho = 9

ii. Escrever um programa que copia todos os caracteres de uma string de origem para
uma string de destino.

A solução consiste em percorrer cada caractere do de origem em busca do


. Uma variável atuará como índice da célula. A cada repetição o caractere da
de origem será copiado para a destino. Segue uma solução:

#include <stdio.h>

void cpystr (char dst[], char src[]) {


int i;
for(i = 0; src[i]; i++)
dst[i] = src[i];

dst[i] = '\0';
}

int main () {
char str[100], bak[100];

gets (str);
cpystr (bak, str);
printf ("backup = [%s]", bak);

Introdução à Programação: Conceitos e Práticas. Página 237


}

A função tem como lógica principal a seguinte repetição:

for(i = 0; src[i]; i++)


dst[i] = src[i];

O será encerrado quando a expressão [ ] resultar em zero, ou seja, quando


alcançar o flag indicador de final de , o . Encontra-se interno ao loop a
operação que copia um a um os caracteres da string para a string . O
caractere finalizador da string não será copiado, pois o é encerrado e a operação
de cópia não é executada. Assim, uma linha é inserida após o para colocar o
valor ou na posição imediatamente após o último caractere copiado para
[] .

Uma variação desta lógica faz uso de um facilidade da linguagem onde a


atribuição é um operador. Desta forma, uma expressão do tipo tem como
resultado o valor atribuído. Este recurso permite que se tenha vários operadores de
atribuição em uma mesma expressão, como em: , sendo executada a
partir de , que além de atribuir o valor a variável , gera zero como resultado,
que é atribuído a , e assim sucessivamente. Assim, podemos utilizar a atribuição
[] [ ] também como expressão de teste do . A repetição será encerrada
quando o for copiado. Desta forma, a função pode ser reescrita como:

void cpystr (char dst[], char src[]) {


int i;
for(i = 0; dst[i] = src[i]; i++)
;

Segue uma interface de entrada e saída para um caso exemplo:

ola mundo belo


backup = [ola mundo belo]

iii. Escrever um programa que transforma os caracteres de uma string de minúsculos


para maiúsculos.

A solução consiste em percorrer cada caractere do em busca do . Uma


variável atuará como índice da célula. A cada repetição o caractere da é
convertido para maiúsculo, caso se enquadre como minúsculo. Segue uma solução:

#include <stdio.h>

int isminusculo (char c) {


return (c >= 'a' && c <= 'z');
}

char upcase (char c) {


if (isminusculo (c))
return 'A' + c - 'a';

return c;

Introdução à Programação: Conceitos e Práticas. Página 238


}

void maiusculo (char src[]) {


int i;
for(i = 0; src[i]; i++)
src[i] = upcase (src[i]);
}

int main () {
char str[100];

gets (str);
maiusculo (str);
printf ("%s", str);

A função tem como lógica principal a seguinte repetição:

for(i = 0; src[i]; i++)


src[i] = upcase (src[i]);

O será encerrado quando a expressão [ ] resultar em zero, ou seja, quando


alcançar o flag indicador de final de , o . Encontra-se interno ao a
operação que substitui o valor de [ ] pelo equivalente maiúsculo fornecido pela
expressão [] .

A função tem a seguinte lógica:

char upcase (char c) {


if (isminusculo (c))
return 'A' + c - 'a';

return c;
}

Um valor é recebido pela função . A primeira ação é testar este caracter,


passando uma cópia para a função , que resultará em verdadeiro ou
falso, caso o caractere seja um dos caracteres de a . No caso de teste positivo
a função retorna o valor da expressão , cujo valor é o
equivalente maiúsculo do caractere . Do contrário é retornado uma cópia do próprio
caractere .

De forma semelhante ao , podemos adequar a lógica para fazer uso da


facilidade da linguagem , onde a atribuição é um operador.

void maiusculo (char src[]) {


int i;
for(i = 0; src[i] = upcase (src[i]); i++)
;
}

Segue uma interface de entrada e saída para um caso exemplo:

Introdução à Programação: Conceitos e Práticas. Página 239


ola mundo belo
OLA MUNDO BELO

iv. Escrever um programa que conta a quantidade de vogais contidas em uma string.

A solução consiste em percorrer cada caractere do em busca do . Uma


variável atuará como índice da célula. A cada repetição um contador é atualizado
sempre que o caractere da se enquadra como vogal. Segue uma solução:

#include <stdio.h>

int isvogal (char c) {

return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||


c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U';

int contavogal (char src[]) {


int i, c;
for(i = c = 0; src[i]; i++)
c += isvogal(src[i]);
return c;
}

int main () {
char str[100];

gets (str);
printf ("%d", contavogal (str));

A função tem como lógica principal a seguinte repetição:

for(i = c = 0; src[i]; i++)


c += isvogal(src[i]);

O será encerrado quando a expressão [ ] resultar em zero, ou seja, quando


alcançar o flag indicador de final de , o . Encontra-se interno ao a
expressão que soma ou ao contador, dependendo se o caractere testado por
resultar em falso ou verdadeiro.

A função já apresentada em sessão passada, tem a seguinte lógica:

int isvogal (char c) {

return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||


c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U';
}

Vamos explorar uma segunda lógica a partir de uma função que recebe uma string e
um caractere, e retorna o índice da primeira posição do array que coincide com o

Introdução à Programação: Conceitos e Práticas. Página 240


caractere informado. Quando o caractere não está contido na string, o valor é
retornado. A seguinte função codifica esta lógica:

int poschar (char src[], char ch) {


int i;
for(i = 0; src[i]; i++)
if(src[i] == ch) return i;

return -1;
}

A string é varrida em busca de um caractere igual a . Ao encontrar, a função é


encerrada, e o valor do índice retornado. No caso de busca sem sucesso, o valor
é retornado.

Assim, uma segunda versão de pode ter a seguinte forma:

int isvogal (char c) {


return poschar("aeiouAEIOU", c) != -1;
}

O processamento de consiste em buscar pelo caractere em uma string


contendo todas as vogais. Em caso de sucesso a função retornará o índice
da célula correspondente ao caractere ou , se a busca falhar.

Segue uma interface de entrada e saída para um caso exemplo:

aAxxeEixxoxU
7

v. Escrever um programa que verifica se uma palavra forma um palíndromo. O


programa encerra quando o usuário digitar o texto .

A solução consiste em utilizar dois índices que percorrem simultaneamente a string,


sendo que um percorre do início para o fim e o outro do fim para o início. A cada ciclo
do loop é testado se os caracteres em posição simétrica são diferentes. Em caso
positivo, conclui-se imediatamente que o texto não forma um palíndromo. Se após
testar todos os pares simétricos não for encontrada diferença, então conclui-se que é
um palíndromo. Segue uma solução:

#include <stdio.h>

int length (char src[]) {


int i;
for(i = 0; src[i]; i++)
;
return i;
}

int igual (char sa[], char sb[]) {


int i;
for (i = 0; sa[i] ; i++)
if (sa[i] != sb[i]) return 0;

return sa[i] == sb[i];

Introdução à Programação: Conceitos e Práticas. Página 241


}

int ispalin (char src[]) {


int i, j;
j = length(src)-1;

for(i = 0; i < j; i++, j--)


if(src[i] != src[j]) return 0;

return 1;
}

int main () {
char str[100];

while (!igual(gets (str), "fim"))


printf (">> %s\n", ispalin(str) ? "sim" : "nao");

A função tem a seguinte lógica:

int ispalin (char src[]) {


int i, j;
j = length(src)-1;

for(i = 0; i < j; i++, j--)


if(src[i] != src[j]) return 0;

return 1;
}

A variável inicia em zero e é incrementado a cada ciclo da repetição. A variável


inicia com índice do último caractere da string. Para tal, é utilizada a função
para conhecer a quantidade de caracteres. O será encerrado imediatamente
antes dos valores de e se cruzarem na metade da string. Se durante a varredura
for encontrado um par de caracteres em posições simétricas distintos entre si, então
a função é encerrada com resultado falso . Se a repetição é encerrada
normalmente, é indicativo de que a string forma um palíndromo, logo a função
retorna verdadeiro .

A função contém a lógica de tratamento das entradas de dados, como segue:

int main () {
char str[100];

while (!igual(gets (str), "fim"))


printf (">> %s\n", ispalin(str) ? "sim" : "nao");

A função serve para verificar se duas strings possuem conteúdos idênticos,


conforme a lógica que segue:

int igual (char sa[], char sb[]) {


int i;

Introdução à Programação: Conceitos e Práticas. Página 242


for (i = 0; sa[i] ; i++)
if (sa[i] != sb[i]) return 0;

return sa[i] == sb[i];


}

A repetição é encerrada normalmente quando é alcançado o final da string A


cada ciclo do é testado se dois caracteres em posições equivalentes em ambas
as strings são diferentes. Em caso positivo, a função é encerrada com o valor
falso . Se a repetição é encerrada normalmente, então é testada a configuração
final. As duas strings serão idênticas se na posição do índice , em ambas as strings,
encontram-se dois caracteres iguais, que no caso será o caractere .

Retornando ao , temos o teste , onde a função é


chamada com dois argumentos. O primeiro argumento aproveita o efeito duplo da
função , que além de preencher a string indicada com o texto digitado, também
retorna uma referência a esta mesma string. É equivalente à se
considerarmos que já esteja inicializado.

Segue uma interface de entrada e saída para casos exemplos:

arara
>> sim
aba
>> sim
abcdcba
>> sim
abcdcab
>> nao
fim

vi. Elaborar um programa que obtém a primeira palavra de uma . Considerar que
o texto dado contenha várias palavras separadas por um ou mais espaços em
branco. Incluir também a lógica que retorna uma cópia da sem a primeira
palavra.

A solução proposta utiliza uma função denominada para obter a primeira


palavra de uma , e a função para retornar um cópia do texto sem a
primeira palavra.

A função tem lógica semelhante. Segue implementação completa.

#include <stdio.h>

void strcar (char dst[], char src[]) {


int i, j;

for (i = 0; src[i] && src[i] == ' '; i++)


;

for (j = 0; src[i] && src[i] != ' '; i++, j++)


dst[j] = src[i];

dst[j] = '\0';

Introdução à Programação: Conceitos e Práticas. Página 243


}

void strcdr (char dst[], char src[]) {


int i, j;
for (i = 0; src[i] && src[i] == ' '; i++)
;

for (; src[i] && src[i] != ' '; i++)


;

for (; src[i] && src[i] == ' '; i++)


;

for (j = 0; dst[j] = src[i]; i++, j++)


;

int main () {
char str[100], car[100], cdr[100];

gets (str);
strcar (car, str);
strcdr (cdr, str);
printf ("[%s]\n", car);
printf ("[%s]\n", cdr);

A função tem a seguinte lógica:

void strcar (char dst[], char src[]) {


int i, j;

for (i = 0; src[i] && src[i] == ' '; i++)


;

for (j = 0; src[i] && src[i] != ' '; i++, j++)


dst[j] = src[i];

dst[j] = '\0';

O primeiro salta todos os caracteres branco ( ) no início da string e posiciona a


variável no primeiro caractere não branco. Esta repetição também é controlada para
que não extrapole o final da string: [] [] . A segunda repetição
copia toda a sequência de caracteres não branco para a string .

A função tem a seguinte lógica:

void strcdr (char dst[], char src[]) {


int i, j;
for (i = 0; src[i] && src[i] == ' '; i++)
;

Introdução à Programação: Conceitos e Práticas. Página 244


for (; src[i] && src[i] != ' '; i++)
;

for (; src[i] && src[i] == ' '; i++)


;

for (j = 0; dst[j] = src[i]; i++, j++)


;

}
O primeiro salta todos os caracteres branco ( ) no início da string e posiciona a
variável no primeiro caractere não branco. Esta repetição também é controlada para
que não extrapole o final da string: [] [] . A segunda repetição
salta todos os caracteres não branco e posiciona a variável no primeiro caractere
branco. Isto equivale a saltar a primeira palavra. A terceira repetição volta a saltar
brancos para posicionar a variável no início da segunda palavra. O último copia
toda a sequência de caracteres a partir da segunda palavra para a string .

Segue uma interface de entrada e saída para um caso exemplo:

ola mundo belo


[ola]
[mundo belo]

vii. Elaborar um programa que preencha uma com o equivalente de um


número inteiro decimal.

A seguinte figura ilustra o processo de conversão da base decimal para a binária.


São feitas divisões sucessivas do número que se deseja converter pela base destino.
Os restos destas divisões fornecem os dígitos binários, e o processo se encerra
quando o quociente alcançar zero:

Figura 66: Conversão Decimal - Binário

Segue a lógica proposta:

#include <stdio.h>

Introdução à Programação: Conceitos e Práticas. Página 245


int length (char src[]) {
int i;
for(i = 0; src[i]; i++)
;
return i;
}

void trocar (char *a, char *b) {


char t;
t = *a;
*a = *b;
*b = t;
}

void inverter (char str[]) {


int i, j = length(str) - 1;

for(i = 0; i < j; i++, j--)


trocar (&str[i], &str[j]);
}

void tobin (char s[], int num) {


int i;

for (i = 0; num; i++, num /= 2)


s[i] = '0' + num%2;
s[i] = '\0';
inverter (s);
}

int main () {
char str[100];
int x;

scanf ("%d", &x);


while (x > 0) {
tobin (str, x);
printf (">> %s\n", str);
scanf ("%d", &x);
}

A função codifica a lógica de conversão:

void tobin (char s[], int num) {


int i;

for (i = 0; num; i++, num /= 2)


s[i] = '0' + num%2;
s[i] = '\0';
inverter (s);
}

A conterá o binário convertido, porém os dígitos estarão em ordem invertida:


o dígito mais significativo será guardado na posição [ ]. A repetição se encerra
quando for igual a zero, e este é atualizado a cada ciclo do com o

Introdução à Programação: Conceitos e Práticas. Página 246


novo valor dado por . Antes, porém, o resto da divisão de por é
adicionado ao valor do caractere ou para obter o símbolo correspondente ao
valor do dígito. Após encerrada a montagem da string, é adicionado o caractere ,
e por fim a string é invertida para representar adequadamente o equivalente binário.

A função possui a seguinte lógica:

void inverter (char str[]) {


int i, j = length(str) - 1;

for(i = 0; i < j; i++, j--)


trocar (&str[i], &str[j]);
}

São utilizados dois índices, sendo um indicando a posição do primeiro caractere da


e o outro a posição do último caractere. Em seguida as células indicadas
por e por tem seus conteúdos permutados. Os índices são atualizados para
indicarem o próximo par de caracteres a serem trocados. Esta lógica prossegue até
que os índices se encontrem no meio da string, quando então a inversão estará
completa.

A função obterá um número inteiro e apresentará o equivalente binário, até que


o usuário forneça um valor zero ou negativo.

Segue uma interface de entrada e saída para um caso exemplo:

5
>> 101
255
>> 11111111
32
>> 100000
4000
>> 111110100000
19
>> 10011
-1

Um segunda solução monta a string já em ordem adequada. Para isto, é necessário


conhecer de forma antecipada a quantidade de símbolos que o número ocupará na
base . Segue esta solução:

#include <stdio.h>

int numdigit (int x, int base) {


int i;
for (i = 0; x; i++, x/=base)
;

return i;
}

void tobin (char s[], int num) {


int i, n;

n = numdigit (num, 2);

Introdução à Programação: Conceitos e Práticas. Página 247


s[n] = '\0';

for (i = n-1; num; i--, num /= 2)


s[i] = '0' + num%2;
}

int main () {
char str[100];
int x;

scanf ("%d", &x);


while (x > 0) {
tobin (str, x);
printf (">> %s\n", str);
scanf ("%d", &x);
}
}

A análise da lógica fica a cargo do leitor.

viii. Elaborar um programa que converta um número inteiro decimal para uma
contendo o equivalente em uma base indicada.

A lógica é semelhante ao do problema anterior bastando substituir o divisor pela


base desejada. Segue a lógica proposta:

#include <stdio.h>

int numdigit (int x, int base) {


int i;
for (i = 0; x; i++, x/=base)
;

return i;
}

void tobase (char s[], int num, int base) {


char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int i, n;

n = numdigit (num, base);


s[n] = '\0';

for (i = n-1; num; i--, num /= base)


s[i] = digits[num%base];
}

int main () {
char str[100];
int x, b;

printf ("entre com a base desejada: ");


scanf ("%d", &b);
printf ("entre com os numeros a serem convertidos:\n");
scanf ("%d", &x);
while (x > 0) {
tobase (str, x, b);

Introdução à Programação: Conceitos e Práticas. Página 248


printf (">> %s\n", str);
scanf ("%d", &x);
}
}

O primeiro resto obtido com o processo de divisão corresponde ao dígito menos


significativo do número. Este primeiro resto deve gerar um símbolo correspondente a
ser armazenado na última posição da string. Os demais restos deverão ocupar as
demais posições, movendo-se em direção ao índice zero.

Em função desta característica, a primeira ação é calcular a quantidade de dígitos do


número na base indicada. A posição da string recebe o flag . A função que
calcula o número de dígitos contabiliza a quantidade de vezes que o número dado
precisa ser dividido pela base até resultar em zero. Esta quantidade indica o número
de dígitos. Segue a lógica de :

int numdigit (int x, int base) {


int i;
for (i = 0; x; i++, x/=base)
;

return i;
}

A função , que realiza a processamento principal, recebe como parâmetros


uma referência à string que será preenchida, o número para conversão e a base
desejada:

void tobase (char s[], int num, int base) {


char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int i, n;

n = numdigit (num, base);


s[n] = '\0';

for (i = n-1; num; i--, num /= base)


s[i] = digits[num%base];
}

A variável serve como índice de acesso a cada posição da string a ser preenchida
com o símbolo correspondente ao dígito da base. O valor inicial de indica o índice
da primeira posição da string a ser ocupada pelo primeiro resto. A repetição é
controlada pelo valor de , quando este número alcançar o loop é encerrado.
A cada ciclo o valor de é substituído por . A expressão
fornece o valor numérico do dígito na indicada e ao mesmo tempo é o índice
para a string onde se encontra o símbolo correspondente. Uma segunda
versão poderia dispensar a variável , ficando apenas com .

Introdução à Programação: Conceitos e Práticas. Página 249


void tobase (char s[], int num, int base) {
char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int i;

i = numdigit (num, base);


s[i] = '\0';

for (i--; num; i--, num /= base)


s[i] = digits[num%base];
}

A função obterá um número inteiro contendo a base de referência, e em


seguida os números a serem convertidos, até que o usuário forneça um valor zero ou
negativo.

Segue uma interface de entrada e saída para um caso exemplo:

entre com a base desejada: 16


entre com os numeros a serem convertidos:
255
>> FF
1000
>> 3E8
65535
>> FFFF
1020304050
>> 3CD09AB2
0

Uma segunda rodada de execução com exemplos na base :

entre com a base desejada: 32


entre com os numeros a serem convertidos:
31
>> V
1025
>> 101
123456
>> 3OI0
987654321
>> TDSQ5H
0

A Linguagem disponibiliza uma função que realiza papel semelhante à nossa


função , que é a função , presente na cujo processamento é
preencher uma string com a representação de um inteiro em uma determinada base.

Preenche a string com o resultado da conversão de para a indicada.


A função retorna o endereço da primeira célula da string .

Introdução à Programação: Conceitos e Práticas. Página 250


ix. Elaborar um programa que converta para inteiro uma string contendo a
representação textual de um número inteiro em uma determinada base.

A seguinte figura ilustra a conversão de uma string contendo um número binário


para inteiro. O resultado será construído na variável , cujo valor inicial é
zero. A montagem do resultado consiste em atualizar com valor da expressão
, onde é o valor numérico do símbolo. No exemplo abaixo, a
aplicação sucessiva desta expressão chega-se ao valor .

0
s[0] '1' 1 1
s[1] '1' 1 3
s[2] '0' 0 6
s[3] '1' 1 13
s[4] '\0'

Figura 67: Conversão de string para inteiro

Segue a lógica proposta:

#include <stdio.h>

int valor (char c) {


char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int i;
for (i = 0; digits[i]; i++)
if (digits[i] == c) return i;

return -1;
}

int toint (char s[], int base) {


int i, x;
for (x = i = 0; s[i]; i++)
x = x*base + valor(s[i]);

return x;
}

int igual (char sa[], char sb[]) {


int i;
for (i = 0; sa[i] ; i++)
if (sa[i] != sb[i]) return 0;

return sa[i] == sb[i];


}

int main () {
char str[100];
int x, b;

printf ("entre com a base de referencia: ");

Introdução à Programação: Conceitos e Práticas. Página 251


scanf ("%d%*c", &b);
printf ("entre com os numeros a serem convertidos:\n");
while (!igual(gets (str), "fim")) {
x = toint (str, b);
printf (">> %d\n", x);
}
}

A função , que realiza o processamento principal, recebe como parâmetros uma


referência à string que será preenchida e a base em questão. O resultado da função
é o número inteiro fruto da conversão:

int toint (char s[], int base) {


int i, x;
for (x = i = 0; s[i]; i++)
x = x*base + valor(s[i]);

return x;
}

A lógica utilizada e já explicada consiste em aplicar a expressão


[ ] para cada símbolo da string. A função fornece o valor
correspondente a um símbolo.

int valor (char c) {


char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int i;
for (i = 0; digits[i]; i++)
if (digits[i] == c) return i;

return -1;
}

O programa suporta até a base . A função busca pelo símbolo em uma


string contendo todos os símbolos possíveis. A posição encontrada é o valor do
símbolo. Segue uma lógica alternativa baseada nos testes disponíveis na biblioteca
. No caso de utilizar esta versão, é necessário o devido .

int valor (char c) {


if (isdigit(c)) return c - '0';
if (islower(c)) return 10 + c - 'a';
if (isupper(c)) return 10 + c - 'A';

return -1;
}

A função obterá um número inteiro contendo a base de referência, e em


seguida as strings a serem convertidas, até que o usuário forneça o texto .

Observar que a entrada do inteiro correspondente a base é feita pelo código:


. A string com o formato da entrada acrescentou o
elemento que serve para descartar o que foi teclado logo após a
digitação do número. Isto deve ser observado sempre que a próxima leitura for de
strings ou de caractere. Este descarte pode ser feito de várias formas, sendo elas:

Introdução à Programação: Conceitos e Práticas. Página 252


incluindo a linha após o ou apenas acrescentar um espaço em
branco após o .

Segue uma interface de entrada e saída para um caso exemplo:

entre com a base de referencia: 16


entre com os numeros a serem convertidos:
FFFF
>> 65535
CAFE
>> 51966
3E8
>> 1000
fim

Uma segunda rodada de execução com exemplos na base :

entre com a base de referencia: 32


entre com os numeros a serem convertidos:
V
>> 31
101
>> 1025
3OI0
>> 123456
TDSQ5H
>> 987654321
fim

A Linguagem disponibiliza uma função que realiza papel semelhante à nossa


função , que é a função , presente na cujo processamento é
converter o conteúdo de uma string para inteiro.

Converte o conteúdo decimal da string para inteiro. Em caso de falha na


conversão é retornado .

x. Elaborar um programa que substitui qualquer sequência de caracteres branco em


uma string por apenas um caractere. O resultado é guardado em uma segunda
string.

Segue a lógica proposta:

#include <stdio.h>

int igual (char sa[], char sb[]) {


int i;
for (i = 0; ; i++) {
if (!sa[i]) return !sb[i];
if (!sb[i]) return !sa[i];
if (sa[i] != sb[i]) return 0;
}
return 1;
}

Introdução à Programação: Conceitos e Práticas. Página 253


void delbranco (char dst[], char src[]) {
int i, j;
if (!(dst[0] = src[0])) return;

for (i = 1, j = 0; src[i]; i++)


if (src[i] != ' ' || dst[j] != ' ') dst[++j] = src[i];

dst[++j] = 0;
}

int main () {
char str[100], txt[100];

while (!igual(gets (str), "fim")) {


delbranco (txt, str);
printf (">> [%s]\n", txt);
}

A função recebe referências a dois strings, sendo um o destino do


processamento e o outro a string de origem:

void delbranco (char dst[], char src[]) {


int i, j;
if (!(dst[0] = src[0])) return;

for (i = 1, j = 0; src[i]; i++)


if (src[i] != ' ' || dst[j] != ' ') dst[++j] = src[i];

dst[++j] = 0;
}

Inicialmente é copiado o primeiro caractere da string origem para a string destino.


Caso este primeiro caractere seja o indicador de final de string, então a função é
encerrada. Havendo pelo menos um caractere na string origem, então o
processamento prossegue. A variável contém o índice do caractere da string de
origem candidata a ser copiada para a string de destino. A variável indica a posição
do último caractere inserido na string destino. Assim, todos os caracteres da string
origem são analisados. A cópia será feita da origem para o destino somente se os
caracteres indicados por e por não sejam iguais ao branco ( ). Assim, se o
caractere analisado é o branco e o último copiado é também o branco, então o
processamento segue sem efetuar a cópia.

A função foi implementada de uma forma diferente da versão anterior. Pede-se


ao leitor estudar a lógica proposta.

A função apresenta um loop que se encerra quando for digitado o texto .


Para cada texto digitado é apresentada a versão filtrada.

Segue uma interface de entrada e saída para um caso exemplo:

Introdução à Programação: Conceitos e Práticas. Página 254


ola mundo belo
>> [ola mundo belo]
ola mundo belo
>> [ola mundo belo]
ola mundo belo
>> [ ola mundo belo ]
o l a m u n d o
>> [o l a m u n d o]
fim

xi. Escrever um programa que conta a quantidade de vezes que cada letra do alfabeto
se repete em uma .

A solução deverá receber uma e produzir como resultado um vetor com as


quantidades de vezes que cada letra aparece repetida nesta . Segue a
proposta de uma solução:

#include <stdio.h>
#include <ctype.h>

int igual (char sa[], char sb[]) {


int i;
for (i = 0; ; i++) {
if (!sa[i]) return !sb[i];
if (!sb[i]) return !sa[i];
if (sa[i] != sb[i]) return 0;
}
return 1;
}

void contar (int vc[], int nc, char s[]) {


int i;
for (i = 0; i < nc; i++) vc[i]= 0;

for (i = 0; s[i]; i++)


if (isalpha(s[i])) vc[tolower(s[i])-'a']++;

void mostrar (int vc[], int nc) {


int i;

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


if (vc[i]) printf ("#%c = %d\n", 'a' + i, vc[i]);
}

int main () {
char str[100];
int n = 'z' - 'a' + 1, v[n];

while (!igual(gets (str), "fim")) {


contar (v, n, str);
mostrar (v, n);
}

Introdução à Programação: Conceitos e Práticas. Página 255


A função define como as contagens serão modeladas. Observe que foi criada a
variável com valor igual a quantidade de letras do alfabeto ( ). Este
valor de também foi utilizado para definir o tamanho do vetor de inteiros . As
posições deste vetor estão reservadas para conter a quantidade de repetições de
cada letra: a posição [ ] para a letra ou , e assim sucessivamente.

A rotina implementa a lógica principal:

void contar (int vc[], int nc, char s[]) {


int i;
for (i = 0; i < nc; i++) vc[i]= 0;

for (i = 0; s[i]; i++)


if (isalpha(s[i])) vc[tolower(s[i])-'a']++;

O primeiro serve para zerar cada uma das células que irão conter as
quantidades de cada letra contidas na .

O segundo percorre cada caractere da string , testando se é uma letra do


alfabeto, através da função da biblioteca Em caso positivo, a célula
do vetor que corresponde àquela letra é incrementada de . A expressão
[] fornece os índices , , … para cada letra do alfabeto. A função
também pertence a biblioteca .

Segue uma interface de entrada e saída:

Ola mundo belo


#a = 1
#b = 1
#d = 1
#e = 1
#l = 2
#m = 1
#n = 1
#o = 3
#u = 1
AaAbbD
#a = 3
#b = 2
#d = 1
fim

Introdução à Programação: Conceitos e Práticas. Página 256


23.3. Vetor e Ponteiro

A linguagem oferece o recurso de manipulação de vetores por meio de ponteiros.


Esta característica permite construir algoritmos simples e eficientes. A seguinte
figura ilustra este conceito, onde um array de dados pode ser interpretado sob a
ótica de duas abstrações distintas.

int v[10];
int *ptr;
ptr = &v[0];

Figura 68: Relação Vetor e Ponteiro

A primeira célula de possui agora duas formas de acesso ao seu conteúdo, uma
por meio de acesso indexado [ ] e outra por meio de acesso indireto . A
expressão é uma forma alternativa de se obter o valor do endereço da primeira
célula, dispensando a expressão [ ] . De forma semelhante, a expressão
também produz o mesmo resultado, ou seja o endereço da primeira célula de .

A seguinte tabela ilustra as equivalências entre a notação indexada e ponteiro, para


um vetor como [ ]

Descrição Notação Indexada Notação ponteiro


Endereço da primeira célula [ ]
Endereço da célula []
Conteúdo da célula [ ]
Conteúdo da célula []

Introdução à Programação: Conceitos e Práticas. Página 257


Na maioria das situações está presente um ponteiro que é inicializado com o
endereço da primeira célula de um vetor. Em seguida tem-se a lógica que opera os
elementos do vetor a partir deste ponteiro. Há uma equivalência sintática entre a
declaração [ ]e , embora a primeira declaração faz reserva de
células do tipo inteiro e a segunda de apenas uma célula do tipo ponteiro. Esta
característica é explorada com bastante frequência na passagem de parâmetros,
onde um argumento contendo o endereço inicial de um vetor é copiado para um
parâmetro. Observe o seguinte exemplo:

#include <stdio.h>

int main () {
int v[] = {30, 40, 50, 60, 70, 80, 90, 100, 110, 120};
int *ptr;
ptr = &v[0];

printf ("%p\n", v);


printf ("%p\n", &v[0]);
printf ("%p\n", &v);
printf ("%p\n", ptr);
printf ("%p\n", &ptr);
printf ("%d\n", v[0]);
printf ("%d\n", *v);
printf ("%d\n", *ptr);

Segue uma interface de entrada e saída:

0061FE88
0061FE88
0061FE88
0061FE88
0061FE84
30
30
30

Analisando os resultados observamos que os quatro primeiros números são


idênticos. Eles representam um endereço de memória no ambiente em que o
programa foi executado. Em relação a ilustração mostrada anteriormente o valor
equivale ao nosso , enquanto que o valor equivale ao
nosso . Podemos constatar que os quatro primeiros números, que equivalem a
quatro endereços de memória, referenciam a mesma célula. Isto confirma que as
expressões , & [ ], e produziram o mesmo valor. No caso da variável ,
isto foi possível em função de ter sido carregada com o valor de [ ].

O exemplo também apresenta três vezes o valor , sendo uma como resultado de
[ ] uma segunda vez como resultado de e a terceira a partir de . Isto
confirma a possibilidade de se obter o conteúdo de uma célula de um vetor tanto
pela forma indexada quanto de forma indireta por meio de ponteiro.

Introdução à Programação: Conceitos e Práticas. Página 258


A expressão deve produzir um aviso do compilador, pois embora o valor da
expressão reflete o endereço da primeira célula deste vetor, a natureza do resultado
é do tipo , ponteiro para ponteiro. Assim, a atribuição do resultado desta
expressão para uma variável do tipo demandará o para evitar a
mensagem do compilador: . Esta forma de expressão é pouco usual.

É possível construir expressões envolvendo ponteiros e operadores, o que acaba


conferindo a ponteiros uma vasta gama de aplicações. O próximo tópico detalha
este recurso.

23.3.1. Expressões com ponteiros

Expressões com ponteiros podem envolver atribuíção, soma, subtração, incremento,


decremento e comparações. Desta forma, uma variável que contenha uma
referência a uma célula de memória pode ser modificada mediante operações que
atualizem o seu valor. Estas operações devem cumprir algumas regras. Vamos
supor que e sejam dois ponteiros para , e seja um . Logo as
seguintes operações são permitidas:

Expressão Significado
atribuir um endereço

––
––

expressões com operadores


relacionais comparando os
endereços de memória

No caso dos ponteiros apontarem para outros tipos de dados, o significado das
operações serão adequadas, considerando .

Vamos avaliar o comportamento do seguinte programa:

#include <stdio.h>

int main () {
double v[10];

Introdução à Programação: Conceitos e Práticas. Página 259


double *p, *q;
p = &v[2];
q = &v[6];

printf ("%p\n", v);


printf ("%p\n", p);
printf ("%p\n", q);
printf ("%p\n", p+1);
printf ("%p\n", q-3);
printf ("%d\n", q - p);
printf ("%p\n", ++p);
printf ("%p\n", --q);
printf ("%p\n", --p);
printf ("%p\n", ++q);
printf ("%p\n", &p);
printf ("%p\n", &q);
p = NULL;
printf ("%p\n", p);
}

Segue uma interface de entrada e saída:

0061FE60
0061FE70
0061FE90
0061FE78
0061FE78
4
0061FE78
0061FE88
0061FE70
0061FE90
0061FE5C
0061FE58
00000000

O seguinte diagrama ilustra a configuração inicial de memória para este exemplo:

0 1 2 3 4 5 6 7
0061FE60 v[0]
0061FE68 v[1]
0061FE5C p 0061FE70 0061FE70 v[2]
0061FE78 v[3]
0061FE80 v[4]
0061FE88 v[5]
0061FE58 q 0061FE90 0061FE90 v[6]
0061FE98 v[7]
0061FEA0 v[8]
0061FEA8 v[9]

Figura 69: Quadro ilustrativo de configuração inicial de memória.

O vetor é composto por . No ambiente computacional utilizado o tipo


ocupa bytes. O primeiro elemento do vetor foi alocado no endereço

Introdução à Programação: Conceitos e Práticas. Página 260


Os demais elementos são alocados sequencialmente, com seus
endereços saltando de em bytes. Os endereços são apresentados em
hexadecimal. A variável foi inicializada com o endereço do elemento [ ]
( ) e a variável inicializada com o endereço de [ ] ( ). A
diferença – é . Este resultado equivale a direferença entre os índices
As operações, –– , , e –– alteraram os conteúdos de e
em unidades, fazendo com que movam a referência exatamente a quantidade
indicada de . A expressão equivale a zerar uma variável ponteiro.
De fato, o valor impresso de é zero. As demais saídas podem ser verificadas de
forma semelhante.

Oberve o seguinte exemplo, ainda com notação indexada, onde um vetor é


carregado com valores introduzidos via teclado e a soma dos seus elementos é
apresentada na tela:

#include <stdio.h>

void ler (int vx[], int * p) {


int i;

scanf("%d", p);

for(i = 0 ; i < *p ; i++)


scanf("%d", &vx[i]);
}

int somar (int vx[], int nx) {


int i, s;

for (i = s = 0; i < nx; i++)


s += vx[i];

return s;

int main () {
int va[1000], na;

ler (va, &na);


printf("soma = %d\n", somar (va, na));

return 0;
}

Segue uma interface de entrada e saída:

5
12
23
32
34
10
soma = 111

Introdução à Programação: Conceitos e Práticas. Página 261


A função pode ser reescrita utilizando a notação ponteiro, mantendo-se a função
inalterada:

Notação Indexada Notação ponteiro


void ler (int vx[], int *p) { void ler (int *vx, int *p) {
int i; int i;

scanf("%d", p); scanf("%d", p);

for(i=0 ; i < *p ; i++) for(i = 0 ; i < *p ; i++)


scanf("%d", &vx[i]); scanf("%d", vx+i);
} }
int somar (int vx[], int nx) { int somar (int *vx, int nx) {
int i, s; int i, s;

for (i = s = 0; i < nx; i++) for (i = s = 0; i < nx; i++)


s += vx[i]; s += *(vx + i);

return s; return s;

} }

As chamadas das funções e permanecem as mesmas, sendo a alteração


apenas nas definições dos parâmetros destas funções. Na versão indexada, a
função , quando chama , utiliza [ ] para informar o endereço da célula
que se deseja atribuir o valor digitado. Na versão com ponteiro o mesmo endereço é
expresso como . Na função , o conteúdo de uma célula do vetor é obtido
com a expressão [ ] , enquanto que na forma ponteiro a expressão usada é
.

Uma segunda versão destas funções faz com que o parâmetro seja atualizado a
cada repetição a fim de referenciar a próxima célula. No início da função, o
parâmetro contém o endereço da primeira célula do vetor originalmente passado
como argumento da chamada da função. A expressão faz com que o valor do
parâmetro seja alterado para o endereço da próxima célula de inteiro.

#include <stdio.h>

void ler (int *vx, int *p) {


int i;
scanf("%d", p);

for(i = 0 ; i < *p ; i++, vx++)


scanf("%d", vx);
}

int somar (int *vx, int nx) {


int i, s;

for (i = s = 0; i < nx; i++, vx++)


s += *vx;

return s;

Introdução à Programação: Conceitos e Práticas. Página 262


}

int main () {
int va[1000], na;

ler (va, &na);

printf("soma = %d\n", somar (va,na));

return 0;
}

É importante destacar que não é possível alterar o endereço da variável array no


contexto em que ela foi declarada. Na função temos a variável , declarada
como [ ] . No contexto da não é possível usar expressões que
alterem , tais como , –– e equivalentes. Para usufruir das facilidades de
operações com ponteiro, no contexto em que o array é declarado, é necessário
utilizar uma variável auxiliar do tipo ponteiro (p. ex. ), ficando neste
caso livre para operar sobre .

23.3.2. String e ponteiro

A string é uma extensão de vetor em , sendo bastante frequente o uso de ponteiros


para manipular os seus caracteres em vez da forma indexada. Apresentamos na
sequência as implementações de lógicas utilizando acesso indireto via ponteiros:

#include <stdio.h>
#include <ctype.h>

int strlen (char *str) {


char *p;

for (p = str; *p; p++)


;

return p - str;
}

char *strcpy (char *dst, char *src) {


char *p = dst;

while (*dst++ = *src++)


;

return p;
}

char *strupr (char *str) {


char *p = str;

for (; *str = toupper(*str); str++)


;

return p;

Introdução à Programação: Conceitos e Práticas. Página 263


}

int main () {
char sa[100], sb[100];
gets (sa);

printf ("%d\n", strlen (sa));


printf ("[%s]\n", strcpy(sb, sa));
printf ("[%s]\n", strupr(sb));

Segue uma interface de entrada e saída:

ola mundo belo


14
[ola mundo belo]
[OLA MUNDO BELO]

A função anteriormente chamada de foi reescrita dispensando


totalmente a variável :

int strlen (char *str) {


char *p;

for (p = str; *p; p++)


;

return p - str;
}

O parâmetro da função foi definido como ponteiro para que é compatível


com um . Assim, ao iniciar a função o parâmetro conterá o
endereço do primeiro caractere da string passada como argumento. Este endereço é
copiado para a variável . Em seguida, a repetição busca pelo caractere . Ao
encontrar o é encerrado, e conterá o endereço da célula que contém . A
diferença entre o endereço onde se encontra o e o endereço do primeiro
caractere resulta na quantidade de caracteres da string.

Seguem distintas formas de implementação da função que calcula o comprimento de


uma string, iniciando com uma versão indexada e movendo para a versão apenas
com ponteiros.

Introdução à Programação: Conceitos e Práticas. Página 264


int length (char src[]) { int strlen (char *str) {
int i; int i;
for(i = 0; src[i]; i++)
; for (i = 0; *(str + i); i++)
return i; ;
}
return i;
}
int strlen (char *str) { int strlen (char *str) {
int i; char *p = str;

for (i = 0; *str; i++, str++) while (*p++)


; ;

return i; return p - str - 1;


} }
int strlen (char *str) {
char *p;

for (p = str; *p; p++)


;

return p - str;
}

Em seguida é implementada a função que copia uma string para uma outra string,
aqui chamada de . Esta função recebe como parâmetros o endereço do
primeiro caractere da string de destino e o endereço do primeiro caractere da string
de origem. Após efetivar a cópia, a função retorna o endereço do primeiro
caractere da string de destino.

char *strcpy (char *dst, char *src) {


char *p = dst;

while (*dst++ = *src++)


;

return p;
}

O fato de retornar o endereço inicial da string destino permite, além da cópia


efetivada, utilizar este valor como argumento para chamada de outra função. Isto
pode ser observado na linha de código do [ ] ,
que utiliza o retorno da função como argumento para o Se
quiséssemos copiar a string para e poderíamos codificar como:
.

A lógica que efetua a cópia de string ficou bastante simplificada, em função de


aspectos da linguagem , tais como operação com ponteiros e a atribuição ser um
operador. O parâmetro contém o endereço da primeira célula da string destino.
O parâmetro contém o endereço da primeira célula da string origem. O valor
original do endereço da string destino é salvo na variável local ( ).
Em seguida, a repetição copia o do caractere em para , e incrementa
e , fazendo com que apontem para as respectivas próximas células. A repetição

Introdução à Programação: Conceitos e Práticas. Página 265


será encerrada quando o for copiado. Por fim, o endereço da primeira célula da
string destino é retornado como resultado da função.

Seguem distintas formas de implementação da função que copia uma string para um
destino, iniciando com uma versão indexada e movendo para a versão apenas com
ponteiros.

void cpystr (char dst[], char src[]) { void cpystr (char dst[], char src[]) {
int i; int i;
for(i = 0; src[i]; i++) for(i = 0; dst[i] = src[i]; i++)
dst[i] = src[i]; ;
}
dst[i] = '\0';
}
char *strcpy (char *dst, char *src) {
char *p = dst;

while (*dst++ = *src++)


;

return p;
}

Caberá ao leitor analisar a função .

Introdução à Programação: Conceitos e Práticas. Página 266


23.4. Funções da Biblioteca <string.h>

Os compiladores para a linguagem disponibilizam via arquivo acesso à


uma variedade de funções para manipulação de strings e de memória. Em exemplos
anteriores foram codificadas funções similares às que serão apresentadas. Segue
uma descrição resumida das principais:

Retorna o comprimento da string terminado com ;

Concatena a string ao final da strings . Retorna .

Retorna o endereço da primeira ocorrência de um


caractere em uma string. O caractere é considerado
parte da string. Retorna se não encontrar.
Retorna o endereço da primeira ocorrência de um
caractere em uma string na busca reversa. Em outras
palavras, retorna o endereço da última ocorrência de em
. O caractere é considerado parte da string. Retorna
se não encontrar.

Copia a string dada por para . Retorna .

Converte a string para maiúsculo. Retorna .

Converte a string para minúsculo. Retorna .

Preenche uma string com um determinado caractere .


Retorna .

Inverte o conteúdo de uma string. Retorna .


Retorna o endereço inicial da primeira ocorrência da string
na string . Retorna no caso de nenhuma
ocorrência.
Compara a string com a string . Retorna um inteiro
indicando a seguinte relação entre e :

Valor
Significado
Retornado
Em relação ao primeiro caractere que causou a
diferença: o de str1 tem um valor menor do que o de
str2:

Em relação ao primeiro caractere que causou a


diferença: o de str1 tem um valor maior do que o de
str2:

Preenche a área de memória apontada por e de


tamanho dado por com o valor definido por
(interpretado como ).

Introdução à Programação: Conceitos e Práticas. Página 267


Seguem alguns exercícios com aplicações das funções da biblioteca :

i. Elaborar o seguinte programa: O primeiro valor informado é uma string de referência.


Em seguida o usuário deverá informar uma sequência de strings de trabalho. Para
cada caso, o programa deverá informar a quantidade de vezes que cada caractere
da string de referência se repete na string de trabalho. Considerar que a string de
referência não possui caracteres repetidos. A entrada de dados é encerrada quando
o usuário entrar com a string . O resultado deverá ser apresentado na tela.

Entrada Saída
abcde
ola mundo belo #a = 1
#b = 1
#c = 0
#d = 1
#e = 1

abcdabcdabc #a = 3
#b = 3
#c = 3
#d = 2
#e = 0
fim

Segue a lógica proposta:

#include <stdio.h>
#include <string.h>

void contar (int *v, int n, char *str, char *ref) {


char *q ;

memset (v, 0, n*sizeof(int));

for (; *str; str++)


if (q = strchr(ref, *str))
v[q-ref]++;

void show (int *v, int n, char *ref) {

for (; *ref; ref++, v++)


printf ("#%c = %d\n", *ref, *v);
}

int main () {
char ref[100];
char str[100];
int vc[100], nc;

Introdução à Programação: Conceitos e Práticas. Página 268


gets (ref);
nc = strlen (ref);
while (strcmp (gets(str), "fim")) {
contar (vc, nc, str, ref);
show (vc, nc, ref);
}
}

A função faz as reservas de memória e organiza as chamadas para as funções


que farão os processamentos mais detalhados. Para este problema serão
necessários dois arrays de para armazenar a string de referência ( ), com os
caracteres que se deseja contar, e a string de trabalho ( ). Também é criado o
vetor de inteiros ( , ) para acomodar um contador para cada caractere da string
de referência.

A primeira ação é obter a string . A quantidade de caracteres desta


string também define a quantidade necessária de contadores: . Na
sequência vem o principal que obtém a string de trabalho ( ) e ativa a função
, junto com os argumentos necessários para realizar o cálculo da frequência
de cada caractere contida em , armazenando o resultado em . A repetição é
encerrada quando a string de trabalho for igual à string . A comparação é feita
em , que no caso de igualdade retorna o valor zero, e
finaliza o .

A função possui a seguinte estrutura:

void contar (int *v, int n, char *str, char *ref) {


char *q ;

memset (v, 0, n*sizeof(int));

for (; *str; str++)


if (q = strchr (ref, *str))
v[q-ref]++;

Os parâmetros da função incluem o vetor onde será armazenada a frequência com


que cada caractere da string se repete em . Os parâmetros do tipo array estão
declarados com a notação ponteiro. O primeiro passo é zerar cada célula de :
( ) cujo papel é representar a contagem de cada
caractere de . Foi utilizada a função , cujo efeito é semelhante à seguinte
lógica:

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


v[i] = 0;

O código seguinte percorre cada caractere de até encontrar o . A cada


execução da repetição é feita uma busca do caractere na string e o
resultado da busca armazenado em . O teste será verdadeiro quando receber um
valor diferente de . Neste caso, a célula de cujo índice corresponde ao
caractere testado será incrementado de . Como há uma relação direta entre um
caractere da string e uma célula do vetor , o índice da célula a ser incrementada
é dada pela diferença – . Supondo que o endereço da primeira célula de

Introdução à Programação: Conceitos e Práticas. Página 269


seja , e que o caractere tenha sido encontrado em no endereço
, então a diferença também será o índice de para o
contador: [ ]++.

A expressão [ ]++ poderia ser reescrita utilizando exclusivamente operação


com ponteiros. Seguem as transformações da notação indexada para a notação
ponteiro:

Indexado para ponteiro Descrição


[ ] Valor da célula índice do vetor .
Retirar os símbolos de indexação: [ e ].
Utilizar a operação para conectar as partes
separadas resultando agora em uma expressão que
fornece o endereço da célula [ ]
Equivale à [ ]
Deferenciar o ponteiro e obter o valor da célula
referente ao endereço dado pela expressão: .
Equivale a [ ]
Incrementa o valor da célula indicada pela referência.
++
Equivale a [ ]++  ++

ii. O usuário fornecerá várias strings. O programa deverá ordenar os caracteres da


string de acordo com os valores numéricos dos caracteres dados pela tabela .
A entrada de dados é encerrada quando o usuário entrar com a string . O
resultado deverá ser apresentado na tela.

Entrada Saída
dbcayx [abcdxy]
ola mundo belo [ abdellmnooou]
rua 19, casa 32. [ ,.1239aaacrsu]
1032437698AaBbCcZzWw [0123346789ABCWZabcwz]
fim

Segue a lógica proposta:

#include <stdio.h>
#include <string.h>

void trocar (char *a, char *b) {


char t;
t = *a;
*a = *b;
*b = t;
}

void shiftmaior (char *u, char *v) {

for (; u < v; u++)


if (*u > *(u+1)) trocar (u, u+1);

char *ordenar (char *s) {


char *q;

Introdução à Programação: Conceitos e Práticas. Página 270


q = s + strlen (s) - 1;
for (; q > s; q--)
shiftmaior (s, q);

return s;
}

int main () {
char str[100];

while (strcmp (gets(str), "fim"))


printf ("[%s]\n", ordenar (str));

A função faz as reservas de memória e organiza as chamadas para a função


que fará os processamentos mais detalhados. Para este problema será necessário
um array de para armazenar a string de trabalho ( ).

A lógica consiste em um principal que obtém a string de trabalho ( ) e chama


a função que irá reposicionar os caracteres da string passada como
parâmetro de tal forma a ficarem em ordem alfabética, de acordo com a tabela .
A repetição é encerrada quando a string de trabalho for igual à string . A
comparação é feita em , que no caso de igualdade retorna
o valor zero, e finaliza o .

A função possui a seguinte estrutura:

char *ordenar (char *s) {


char *q;
q = s + strlen(s) - 1;
for (; q > s; q--)
shiftmaior (s, q);

return s;
}

A lógica da função consiste em deslocar os caracteres de maior valor para o


fim da string. Este algoritmo ( ) é semelhante ao exemplo com vetor de
números, anteriormente comentado. O quadro abaixo ilustra a dinâmica dos
deslocamentos, bem como o estreitamento da faixa de memória, até que todo o array
de tenha sido processado. Vamos supor que a função tenha sido chamada com
o parâmetro contendo . Além disso, vamos supor que equivale ao
endereço . A configuração de memória seria dada por:

s 1000 x
1001 y
1002 d
1003 a
1004 c
1005 \0

Introdução à Programação: Conceitos e Práticas. Página 271


O primeiro passo é fazer com que o caractere de maior valor entre os endereços
e seja deslocado para o endereço . Como a comparação é feita entre
um caractere e o seu sucessor, então a última comparação será entre os elementos
em e . A lógica não pode permitir que se compare o elemento em
com o elemento em .

Na função temos com o endereço e iniciado com o valor da


expressão – , ou seja o valor ,
considerando o caso ilustrado. Em seguida, é iniciada uma repetição
–– , onde o valor de é decrementado até alcançar o valor ( ). Assim,
assumirá os valores , , e . Para cada um destes valores de , a
função será chamada: .

Na primeira iteração, para , temos o deslocamento do


caractere para o endereço , ficando com :
shiftmaior (1000, 1004);

'x' > 'y' 'y' > 'd' 'y' > 'a' 'y' > 'c'
s 1000 x x x x
1001 y y|d d d
1002 d d|y y|a a
1003 a a a|y y|c
1004 c c c c|y
1005 \0 \0 \0 \0

Na segunda iteração, para , temos o deslocamento do


caractere para o endereço , ficando com :
shiftmaior (1000, 1003);

'x' > 'd' 'x' > 'a' 'x' > 'c'


s 1000 x|d d d
1001 d|x x|a a
1002 a a|x x|c
1003 c c c|x
1004 y y y
1005 \0 \0 \0

Na terceira iteração, para , temos o deslocamento do


caractere para o endereço , ficando com :

Introdução à Programação: Conceitos e Práticas. Página 272


shiftmaior (1000, 1002);
'd' > 'a' 'd' > 'c'
s 1000 d|a a
1001 a|d d|c
1002 c c|d
1003 x x
1004 y y
1005 \0 \1

Na quarta e útima iteração, para , não acontecerá a troca,


pois o caractere não é maior que o caractere , ficando em seu conteúdo final
com :

shiftmaior (1000, 1001);


'a' > 'c'
s 1000 a
1001 c
1002 d
1003 x
1004 y
1005 \0

A função processa estas comparações e realiza a troca quando


necessário, considerando a faixa de endereços dadas por e .

void shiftmaior (char *u, char *v) {

for (; u < v; u++)


if (*u > *(u+1)) trocar (u, u+1);

Introdução à Programação: Conceitos e Práticas. Página 273


23.5. Matriz

O tipo utilizado para definir vetores também pode ser utilizado para definir
matrizes. Muitos problemas do mundo real podem ser modelados sob a forma de
matrizes tanto pela conveniência da representação quanto pela necessidade de
fazer uso dos recursos da álgebra matricial. A maioria das linguagens imperativas
clássicas não trazem operadores embutidos para processar matrizes, ficando ao
programador a incumbência de codificar as lógicas usando os recursos escalares da
linguagem e suas estruturas de controle. Algumas linguagens do paradigma
funcional, tal como , disponibilizam uma grande quantidade de operadores
matriciais. A seguinte figura ilustra os tipos , e :

int x; int v[100], i; int m[100][50], i, j;


[0] [1] ... [j] ... [49]
x: v: [0] m: [0]
[1] [1]
[2] [2]
x [3] [3]
[4] ...
[i]
...
[i] v[i] [99]

m[i][j]

[99]

Figura 70: Comparação Escalar, Vetor e Matriz

A variável pode ter seu conteúdo acessado diretamente através do nome da


variável, enquanto que um necessita do nome mais um índice, e a matriz
requer além do nome mais dois índices, um para linha e outro para coluna. Este
último caso, para matrizes de duas dimensões. De forma geral, uma matriz de
dimensão demanda índices para identificar uma célula. A linguagem não
oferece o tipo matriz como tipo específico. Uma matriz de duas dimensões deve ser
criada como vetor de vetor. Uma matriz de três dimensões deve ser criada como
vetor de vetor de vetor, e assim sucessivamente. Observe a seguinte definição:

int m[100][50];

A matriz é declarada como sendo composta de fileiras, onde cada fileira é


composta por células.

Introdução à Programação: Conceitos e Práticas. Página 274


[0] [1] [2] ... [48] [49]
m m[0] m[0][0] m[0][1] m[0][2] ... m[0][48] m[0][49]

m[1] m[1][0] m[1][1] m[1][2] ... m[1][48] m[1][49]

m[2] m[2][0] m[2][1] m[2][2] ... m[2][48] m[2][49]


. . . . . .
. . . . . .
. . . . . .
m[99] m[99][0] m[99][1] m[99][2] ... m[99][48] m[99][49]

Figura 71: Matriz de 100x50

A expressão [ ] é uma referência a fileira zero, sendo semanticamente equivalente


a um array de inteiros. Da mesma forma temos referências as demais fileiras. Em
cada fileira, o valor de uma célula pode ser obtido com o uso de dois índices, o da
fileira/linha e o da coluna. O valor da primeira célula da primeira linha é obtido pela
expressão [ ][ ]. A primeira linha é sempre índice zero, o mesmo ocorrendo com
o índice da primeira coluna. O índice pode resultar de qualquer expressão inteira,
ficando para o programador a responsabilidade de observar os limites e tamanhos
estabelecidos na definição da matriz. A expressão [ ][ ] expressa o valor da célula
da linha e coluna , considerando que e tenham sido declaradas como

Observar que as dimensões de foram definidas através de constantes numéricas.


Em determinadas situações podem surgir lógicas que fazem uso frequente do
tamanho máximo de uma determinada dimensão. Assim, ao longo do código
poderão surgir referências a este valor. Por exemplo, um determinado programa
define a dimensão de uma matriz para linhas colunas. Adicionalmente,
em vários locais do programa existem expressões aritméticas e lógicas usando este
valores nas suas lógicas. Porém, quando por alguma necessidade a quantidade de
linhas/colunas for redefinida para outro valor, o programador deverá percorrer todo o
código em busca destes números para as devidas readequações.

Nestas circunstâncias, é recomendado o uso da diretiva , onde valores e


expressões podem estar associadas a .

O exemplos que segue ilustra o uso deste recurso:

#define MAXL 100


#define MAXC 50

int main() {
int m[MAXL][MAXC];

//programa principal
}

Introdução à Programação: Conceitos e Práticas. Página 275


A diretiva requer um símbolo e em seguida a expressão associada a este
símbolo. Na fase de pré-compilação, todos os símbolos serão substituídos pela
expressão associada. Deve-se evitar o uso de ao final, quando não se deseja que
este símbolo faça parte da expressão a ser substituída. O seguinte exemplo faz uso
do em um contexto que faz sentido:

#define EVER ;;

int main() {

for (EVER) {
// loop forever
}

//programa principal
}

Neste exemplo, o identificador será substituído por antes da compilação,


gerando o código que equivale a um laço infinito.

23.5.1. Exemplos

i. Escrever um programa que preenche uma matriz bidimensional com dados lidos via
teclado e apresente estes dados na tela.

Segue a proposta de uma solução:

#include <stdio.h>

#define MAXL 100


#define MAXC 100
#define SKIP printf ("\n")

void ler (int M[][MAXC], int *L, int *C) {


int i, j;
scanf ("%d %d", L, C);
for (i = 0; i < *L; i++)
for (j = 0; j < *C; j++)
scanf ("%d", &M[i][j]);
}

void show (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++) {
for (j = 0; j < C; j++)
printf ("%4d", M[i][j]);
SKIP;
}
}

int main () {
int mat[MAXL][MAXC];
int nl, nc;

ler (mat, &nl, &nc);

Introdução à Programação: Conceitos e Práticas. Página 276


show (mat, nl, nc);

Uma matriz bidimensional é um array de array. Neste caso, a matriz foi definida
com linhas por colunas, resultando na alocação de células do tipo .
Esta é a capacidade máxima de armazenamento para esta matriz. No entanto, os
problemas podem utilizar uma região desta matriz, em um determinado contexto do
programa.

[0] [1] [2] ... [98] [99]


mat mat[0] mat[0][0] mat[0][1] mat[0][2] ... mat[0][98] mat[0][99]

mat[1] mat[1][0] mat[1][1] mat[1][2] ... mat[1][98] mat[1][99]

mat[2] mat[2][0] mat[2][1] mat[2][2] ... mat[2][98] mat[2][99]


. . . . . .
. . . . . .
. . . . . .
mat[99] mat[99][0] mat[99][1] mat[99][2] ... mat[99][98] mat[99][99]

Figura 72: Utilização de parte de uma matriz.

Esta figura ilustra o uso de uma porção de uma matriz definida como
. No caso de vetor, era comum vincular uma variável inteira indicando a
quantidade efetiva de células utilizada naquele contexto de execução. Para a matriz
de duas dimensões, serão necessárias duas variáveis inteiras a fim de manter o
controle sobre a área da matriz efetivamente em uso em um determinado momento.
Uma variável indica a quantidade de linhas e a outra indica a quantidade de colunas,
normalmente tendo como referência a linha e a coluna zero. Neste exercício, as
variáveis e desempenharão este papel.

Assim, para percorrer cada uma das células da matriz, quer seja para armazenar um
dado ou obter o dado armazenado, será demandado do programa, no caso de uma
lógica simples, a utilização de duas repetições aninhadas, cada uma controlando um
índice. A repetição mais interna processa mais rapidamente que a repetição
envolvente. Observe a função :

void ler (int M[][MAXC], int *L, int *C) {


int i, j;
scanf ("%d %d", L, C);
for (i = 0; i < *L; i++)
for (j = 0; j < *C; j++)
scanf ("%d", &M[i][j]);

A chamada da
função a partir de toma a seguinte forma:
. O primeiro argumento é a referência a matriz a ser preenchida,
enquanto que os dois outros argumentos são os endereços das variáveis e ,
onde serão armazenados a dimensão da área da matriz efetivamente em uso.

Introdução à Programação: Conceitos e Práticas. Página 277


No contexto da função os parâmetros foram declarados de forma compatível com
os argumentos. O primeiro parâmetro [][ ] identifica como sendo uma
matriz de várias linhas, onde cada linha possui tamanho máximo de
elementos. Nas definições de parâmetros do tipo array, qualquer que seja a
dimensão, é dispensável informar o tamanho da primeira dimensão. Como
característica da Linguagem , há uma vinculação entre o parâmetro e o
argumento passado na hora da chamada, onde toda alteração nas células de
refletem nas células do argumento. Os outros dois parâmetros foram declarados
como , onde e são ponteiros para as células de inteiros nas quais
serão armazenadas as quantidades de linhas e de colunas para aquele contexto de
execução.

A primeira ação da função é obter os valores para o número de linhas e de


colunas: . Os valores de e de já indicam o endereço onde
os valores lidos deverão ser armazenados. Assim, e são referências as células
de inteiros destinados para as dimensões da matriz; e e representam os
valores destes inteiros.

Na sequência foram implementados dois loops, que iremos identificar como e


:

for (i = 0; i < *L; i++)


for (j = 0; j < *C; j++)
scanf ("%d", &M[i][j]);

O loop gera em os índices de a , um valor para cada linha, enquanto


que o gera em os índices de a um para cada coluna. Como o
está envolvido pelo , então para cada valor de tem-se toda a sequência de
valores para . Desta forma, são gerados pares que permitem referenciar todas
as células da área delimitada pelo número de linhas e número de colunas, conforme
ilustra o seguinte quadro:

[0] [1] ... [j] ... [*C-1] ... [99]


M: [0]
[1]
[2]
...
[i]
...
[*L-1]
...
[99]

M[i][j]

Figura 73: Repetições em uma matriz.

Assim, na parte mais interna do loop, dentro do é inserido o código


[ ][ ] que faz uso de todos os pares de índices para
referenciar cada célula válida da matriz. A expressão [ ][ ] indica o endereço da
célula que irá receber o valor obtido pelo .

Introdução à Programação: Conceitos e Práticas. Página 278


A função é responsável em apresentar os valores da matriz:

void show (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++) {
for (j = 0; j < C; j++)
printf ("%4d", M[i][j]);
SKIP;
}

Os parâmetros definidos incluem a matriz com suas dimensões, com exceção da


primeira, e os inteiros e que definem as quantidades de linhas e de colunas.
Temos novamente uma estrutura com e , caracterizando o interesse em
percorrer cada uma das células da matriz . O foi embutido no a fim de
fazer uso de todos os pares de valores gerados pelas duas repetições:

A fim de demonstrar as possibilidades da diretiva , foi feita uma vinculação


do identificador com a expressão Assim, um salto de linha é feito
logo após o encerramento do . Observe que o está apenas na trecho
controlado pelo .

Após imprimir todos os elementos de uma linha da matriz, é iniciada uma nova linha
na tela, e o loop retorna para processar o próximo , apresentando uma linha da
matriz após a outra.

Segue uma interface de entrada e saída:

3 2
#[0][0] = 10
#[0][1] = 18
#[1][0] = 6
#[1][1] = 12
#[2][0] = 43
#[2][1] = 17
10 18
6 12
43 17

Introdução à Programação: Conceitos e Práticas. Página 279


ii. Escrever um programa que preencha uma matriz conforme os padrões que seguem
e apresente estas matrizes na tela.

a) b)

| | | |
| | | |

c) d)

| | | |
| | | |

O formato geral de uma matriz é:

| |

| |

Os casos apresentados possuem lógicas que define o valor de uma célula de acordo
com os índices associados a ela. É importante buscar uma associação entre os
índices e o valor pretendido para a posição. Por exemplo, no
onde se pretende preencher uma matriz de acordo com o padrão dado pela matriz
identidade, a lógica que orienta a forma de preenchimento é:

Esta lógica deve ser aplicada a cada célula da matriz. Assim, a lógica para percorrer
cada elemento da célula requer duas repetição do tipo , sendo a mais externa
produzindo os índices para as linhas { } e a interna os índices para as
colunas { }. As combinações de e geradas serão semelhantes às
indicadas no exemplo anterior.

No , fica evidente que cada célula é preenchida com o seu índice de


linha. O , a diagonal secundária é preenchida com e as demais
células com zero. No caso de matriz quadrada ( ), uma célula pertencerá a
diagonal secundária, quando a soma dos seus índices coincidir com o valor de
.

Introdução à Programação: Conceitos e Práticas. Página 280


O trata de identificar quando uma célula está acima da diagonal
principal. Uma rápida análise nos índices permite certificar que esta condição ocorre
quando .

Segue a proposta de uma solução:

#include <stdio.h>

#define MAXL 100


#define MAXC 100
#define SKIP printf ("\n")

void fill_a (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = (i == j);
}

void fill_b (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = i;
}

void fill_c (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = (i + j) == L - 1;
}

void fill_d (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = (j >= i);
}

void show (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++) {
for (j = 0; j < C; j++)
printf ("%4d", M[i][j]);
SKIP;
}
}

int main () {
int mat[MAXL][MAXC];
int nl, nc;

nl = 5; nc = 5;
fill_a (mat, nl, nc); printf ("\nMatriz a)\n"); show (mat, nl, nc);
fill_b (mat, nl, nc); printf ("\nMatriz b)\n"); show (mat, nl, nc);
fill_c (mat, nl, nc); printf ("\nMatriz c)\n"); show (mat, nl, nc);
fill_d (mat, nl, nc); printf ("\nMatriz d)\n"); show (mat, nl, nc);
}

Introdução à Programação: Conceitos e Práticas. Página 281


O preenchimento conforme a matriz identidade pode ser feito de diversas formas,
conforme ilustrado abaixo:

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


for (j = 0; j < C; j++) for (j = 0; j < C; j++)
M[i][j] = (i == j); if (i == j) M[i][j] = 1;
else M[i][j] = 0;

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


for (j = 0; j < C; j++)
M[i][j] = 0;

M[i][i] = 1;
}

Observe que a terceira lógica dispensa a comparação , presente nas duas


versões anteriores. As células [ ][ ] de uma linha inteira são incondicionalmente
preenchidas com , e após o encerramento do , e estando apenas sob controle
do , o valor da célula [ ][ ] é substituído por .

Segue uma interface de entrada e saída:

Matriz a)
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
0 0 0 0 1

Matriz b)
0 0 0 0 0
1 1 1 1 1
2 2 2 2 2
3 3 3 3 3
4 4 4 4 4

Matriz c)
0 0 0 0 1
0 0 0 1 0
0 0 1 0 0
0 1 0 0 0
1 0 0 0 0

Matriz d)
1 1 1 1 1
0 1 1 1 1
0 0 1 1 1
0 0 0 1 1
0 0 0 0 1

Introdução à Programação: Conceitos e Práticas. Página 282


iii. Escrever um programa que forneça as seguintes informações a partir dos dados de
uma matriz:

a. As somas dos elementos de cada linha;


b. As somas dos elementos de cada coluna;
c. O valor da posição do maior elemento.

A soma dos elementos de cada linha deverá produzir um valor por linha, que
resultará em um vetor com o comprimento igual a quantidade de linhas da matriz.

| |

| |

Esta matriz deverá ser processada para produzir a seguinte soma:

[ ]
[ ]
| | | |
[ ]
| |
| [ ]|
[ ]
Onde significa a somatória de todos os elementos da linha . De forma geral
[] ∑ .

De forma semelhante, tem-se a somatória dos elementos de cada coluna:

| |

| |

[ ] [ ] [ ] [ ] [ ]

Neste caso significa a somatória de todos os elementos da coluna . De forma


geral [ ] ∑ .

Segue a proposta de uma solução:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

Introdução à Programação: Conceitos e Práticas. Página 283


#define MAXL 100
#define MAXC 100
#define MAXN 100
#define SKIP printf ("\n")

void posmaior (int M[][MAXC], int L, int C, int *LIN, int *COL) {
int i, j, lin = 0, col = 0;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
if (M[i][j] > M[lin][col]) {
lin = i;
col = j;
}
*LIN = lin;
*COL = col;
}

int somavet (int *v, int n) {


int s, *last = v + n - 1;
for (s = 0; v <= last; v++)
s += *v;

return s;
}

void somalin (int M[][MAXC], int L, int C, int V[], int *N) {
int i;
*N = L;
for (i = 0; i < L; i++)
V[i] = somavet (M[i], C);
}

void somacol (int M[][MAXC], int L, int C, int V[], int *N) {
int i, j, s;
*N = C;
for (j = 0; j < C; j++) {
for (s = i = 0; i < L; i++)
s += M[i][j];
V[j] = s;
}
}

void fill_random (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = rand () % 20;
}

void show (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++) {
for (j = 0; j < C; j++)
printf ("%4d", M[i][j]);
SKIP;
}
}

Introdução à Programação: Conceitos e Práticas. Página 284


void showvet (int *v, int n) {
int *last = v + n - 1;

for (; v <= last; v++)


printf ("%4d", *v);
}

int main () {
int mat[MAXL][MAXC], vx[MAXN];
int nl, nc, nx, lin, col;

srand (time(NULL));
fill_random (mat, nl = 4, nc = 5);
printf ("Matriz randomica\n"); show (mat, nl, nc);

somalin (mat, nl, nc, vx, &nx);


printf ("\nSoma do Vetor Linha\n"); showvet (vx, nx);

somacol (mat, nl, nc, vx, &nx);


printf ("\n\nSoma do Vetor Coluna\n"); showvet (vx, nx);

posmaior (mat, nl, nc, &lin, &col);


printf ("\n\nmaior[%d][%d] = %d\n", lin, col, mat[lin][col]);
}

A lógica da função aproveita uma característica da linguagem, onde uma


matriz bidimensional é um vetor de vetor. Assim, cada linha da matriz pode ser visto
como um vetor independente. A expressão [ ] é uma referência ao vetor formado
pelos elementos da linha da matriz .

void somalin (int M[][MAXC], int L, int C, int V[], int *N) {
int i;
*N = L;
for (i = 0; i < L; i++)
V[i] = somavet (M[i], C);
}

Os parâmetros da função incluem as seguintes informações: Referência a Matriz


( [][ ]), Número de Linhas ( ) e Número de Colunas ( ); e os dados
do vetor onde serão armazenados a soma dos elementos: Referência ao Vetor
( [] ou ) e o endereço para a quantidade de elementos ( ). A lógica
principal inicia definindo que o tamanho do vetor é dado pela quantidade de linhas da
matriz ( ). Em seguida o gera um índice para cada linha e despacha o
cálculo da soma do vetor linha para a função , [] [] .A
função tem a seguinte estrutura:

int somavet (int *v, int n) {


int s, *last = v + n - 1;
for (s = 0; v <= last; v++)
s += *v;

return s;
}

A soma dos elementos de um vetor foi implementada fazendo uso de ponteiros. O


parâmetro contém o endereço da primeira célula do vetor. A variável local foi

Introdução à Programação: Conceitos e Práticas. Página 285


definida como ponteiro para inteiro e inicializada com o endereço da última célula do
vetor de tamanho : – . A repetição percorre cada elemento do vetor
a partir do endereço indicado por acumulando o valor da célula em uma variável:
. Para cada iteração o valor de é atualizado para apontar para o próximo
elemento do vetor: . A repetição se encerra quando ultrapassar o último
elemento do array.

Não é possível utilizar a mesma lógica para a função , pois os elementos de


uma coluna não estão armazenados em sequência na memória. A maioria das
linguagens adotam a organização por linha, onde os elementos de uma linha são
armazenados em sequência na memória. A linguagem é uma exceção a
este padrão, tendo optado pela organização por coluna. Desta forma, a função
foi implementada com duas repetições explícitas:

void somacol (int M[][MAXC], int L, int C, int V[], int *N) {
int i, j, s;
*N = C;
for (j = 0; j < C; j++) {
for (s = i = 0; i < L; i++)
s += M[i][j];
V[j] = s;
}
}

Inicialmente, o tamanho do vetor é definido pela quantidade de colunas da matriz


( ). Em seguida, o gera um índice para cada coluna da matriz, e o
embutido acumula em cada um dos valores dos elementos da linha vinculada a
coluna ( [ ][ ]). Ao encerrar o a soma dos elementos da coluna que
está em é copiado para o vetor com as somas ( [ ] ).

A terceira funcionalidade solicitada pelo problema é obter a posição do


maior elemento da matriz:

void posmaior (int M[][MAXC], int L, int C, int *LIN, int *COL) {
int i, j, lin = 0, col = 0;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
if (M[i][j] > M[lin][col]) {
lin = i;
col = j;
}
*LIN = lin;
*COL = col;
}

A lógica consiste em percorrer cada elemento da matriz ( e ) testando se o


valor da célula [ ][ ] é maior que o elemento da célula [ ][ ]. As variáveis
e , iniciadas com foram criadas para conter os índices do elemento da matriz
com o maior valor. Sempre que o valor da célula testada superar o valor da célula
marcada, os índices são atualizados com os valores de . Após encerrar
as duas repetições, tendo percorrido toda a matriz, os índices da célula marcada são
copiados para os lugares definitivos, conforme indicado pelos parâmetros e
.

Introdução à Programação: Conceitos e Práticas. Página 286


A matriz de teste foi preenchida com valores aleatórios, conforme a lógica da função
:

void fill_random (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = rand () % 20;
}

A função gera um número inteiro entre e um máximo inteiro de maneira


aleatória. A expressão reduz o domínio para um número aleatório entre
e . Na função foi incluído o código que inicializa a
de geração de números aleatórios com o valor da expressão ,
cujo resultado é a quantidade de segundos decorridos a partir de uma data de
referência definida para o Sistema Operacional.

As funções de apresentação de resultados na tela tem lógica semelhante as muitas


já ilustradas.

Segue uma interface de entrada e saída:

Matriz randomica
8 5 18 15 0
10 4 12 18 5
2 13 2 13 16
5 19 19 16 3

Soma do Vetor Linha


46 49 46 62

Soma do Vetor Coluna


25 41 51 62 24

maior[3][1] = 19

iv. Escrever um programa que efetua o produto matricial.

Considerando o seguinte produto de duas matrizes:

O produto será possível quando o número de colunas de for igual ao número de


linhas de ( ). A matriz resultante será de dimensão ( ). Segue um
exemplo simplificado para o produto:

| | | | | |

Um elemento da matriz é obtido pela soma dos produtos dos elementos da


linha de pelos elementos da coluna de .

Introdução à Programação: Conceitos e Práticas. Página 287


Neste exemplo, o elemento é obtido da seguinte forma:

| | | | | |

Ou seja:

Observe que todos os elementos de na expressão possuem a mesma linha do


elemento de , e todos os elementos de possuem a mesma coluna do elemento de
. Segue a expressão adaptada para esta expressão:

Generalizando esta expressão para qualquer dimensão, e sabendo que ,


tem-se:

Sob a forma de somatória, um elemento de é obtido por:

Logo, para todos os elementos de , tem-se:

Segue a proposta de uma solução:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAXL 100


#define MAXC 100
#define SKIP printf ("\n")

void fill_random (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++)
for (j = 0; j < C; j++)
M[i][j] = rand () % 5;
}

int matprod (int Mc[][MAXC], int *Lc, int *Cc,


int Ma[][MAXC], int La, int Ca,
int Mb[][MAXC], int Lb, int Cb) {

Introdução à Programação: Conceitos e Práticas. Página 288


int i, j, k, s;

if (Ca != Lb) return 1;


*Lc = La;
*Cc = Cb;

for (i = 0; i < *Lc; i++)


for (j = 0; j < *Cc; j++) {
for (s = k = 0; k < Ca; k++)
s += Ma[i][k]*Mb[k][j];
Mc[i][j] = s;
}

return 0;
}

void show (int M[][MAXC], int L, int C) {


int i, j;
for (i = 0; i < L; i++) {
for (j = 0; j < C; j++)
printf ("%4d", M[i][j]);
SKIP;
}
}

int main () {
int ma[MAXL][MAXC], la, ca;
int mb[MAXL][MAXC], lb, cb;
int mc[MAXL][MAXC], lc, cc;

srand (time(NULL));
fill_random (ma, la = 3, ca = 4);
printf ("Matriz A\n"); show (ma, la, ca);

fill_random (mb, lb = 4, cb = 2);


printf ("\nMatriz B\n"); show (mb, lb, cb);

matprod (mc, &lc, &cc, ma, la, ca, mb, lb, cb);
printf ("\nMatriz C\n"); show (mc, lc, cc);

A função foi utilizada para inicializar duas matrizes com valores de a ,


facilitando desta forrma a conferência do resultado.

Em seguida a função realiza o produto matricial da matriz de dimensões


pela matriz de , armazenando o resultado na matriz de
dimensões . Segue a lógica da função :

int matprod (int Mc[][MAXC], int *Lc, int *Cc,


int Ma[][MAXC], int La, int Ca,
int Mb[][MAXC], int Lb, int Cb) {
int i, j, k, s;

if (Ca != Lb) return 1;


*Lc = La;
*Cc = Cb;

Introdução à Programação: Conceitos e Práticas. Página 289


for (i = 0; i < *Lc; i++)
for (j = 0; j < *Cc; j++) {
for (s = k = 0; k < Ca; k++)
s += Ma[i][k]*Mb[k][j];
Mc[i][j] = s;
}

return 0;
}

Inicialmente é testada a viabilidade da operação, comparando . Em caso


positivo, a lógica do produto é iniciada.

O produto matricial demanda três sendo o eo para referenciar cada


elemento da matriz resultante , e o para obter este elemento realizando a
soma acumulada dos produtos de [ ][ ] [ ][ ] A função retorna ou ,
indicando se produto foi possível ou não.

A soma acumulada poderia ser feita diretamente em [ ][ ] mas optou-se por


utilizar a variável para melhorar a legigilidade. A eficiência também é melhorada,
uma vez que se utiliza uma em vez de uma
dentro do loop mais interno.

A seguinte interface ilustra uma entrada e saída do programa:

Matriz A
2 2 2 2
1 4 3 3
1 0 1 1

Matriz B
1 4
4 2
4 3
1 3

Matriz C
20 24
32 30
6 10

v. Escrever um programa que resolva um sistema de equações lineares.

Considerando que o sistema tenha a seguinte representação matricial:

, onde:

[ ] [ ] [ ]

Introdução à Programação: Conceitos e Práticas. Página 290


A matriz e o vetor são fornecidos, sendo a solução desejada. O seguinte
sistema de equações será utilizado como exemplo para detalhar os passos de um
método simples de solução.

Pelo método de as linhas são combinadas através de


operações matemáticas que zeram as parcelas abaixo da diagonal principal,
produzindo uma matriz triangular.

O consiste em efetuar transformações nas três linhas abaixo da linha


de tal forma a eliminar a parcela de nas linhas e .

A visualização no formato matricial melhora o entendimento, quando justamos a


matriz e o vetor produzindo a [ ]:

[ ]

Para o caso exemplo, tem-se:

[ ]

Fixando o elemento como referência, a , produz:

[ ]

A lógica utilizada no foi:

Um consiste em eliminar os termos abaixo do elemento .

[ ]

Introdução à Programação: Conceitos e Práticas. Página 291


A lógica utilizada no foi:

O consiste em eliminar os termos abaixo do elemento .

| |

A lógica utilizada no foi:

Podemos combinar os passos com a lógica do escalonamento, obtendo:

Para completar a lógica, basta detalhar as operações com as linhas , elemento a


elemento. Neste caso, surgiria mais um para codificar a operação
. O detalhe pode ser inspecionado no código mais adiante.

A próxima etapa, com a matriz já escalonada, é obter os valores de efetuando as


devidas substituições de baixo para cima ( ). A linha fornece o
valor de , que levado a linha fornece o valor de , e assim sucessivamente:

( )

( )

Introdução à Programação: Conceitos e Práticas. Página 292


Assim, o vetor solução é , e a forma geral da solução é:

O seguinte programa implementa as lógicas apresentadas acima:

#include <stdio.h>
#include <stdlib.h>

#define MAXL 100


#define MAXC 100

void load (float M[][MAXC], int *L, int *C) {


int i,j;

scanf("%d %d",L, C);

for(i=0; i < *L; i++)


for(j=0; j < *C; j++)
scanf ("%f", &M[i][j]);
}

void showmat (float M[][MAXC],int L, int C) {


int i,j;

printf("#%d x %d\n", L, C);

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


for(j = 0; j < C; j++)
printf("%8.3f", M[i][j]);

printf("\n");
}
}

void showvet (float *V, int N) {


int i;
printf("#%d\n", N);

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


printf("%10.3f\n", *V++ );
}

int combina (float M[][MAXC], int L, int C, int Lx, int Ly) {
int j; float k;

k = -M[Lx][Lx] / M[Ly][Lx];

for(j = Lx; j < C; j++)


M[Ly][j] = (M[Ly][j] * k) + M[Lx][j];
}

int escalona (float M[][MAXC], int L, int C) {


int i,j;

for(i = 0; i < L - 1; i++)

Introdução à Programação: Conceitos e Práticas. Página 293


for(j = i + 1; j < C; j++)
combina(M, L, C, i, j);
}

int backsubstitution (float M[][MAXC], int L, int C, float *V, int *N) {
int i,j;

*N = L;

for(i = *N-1; i >= 0; i--) {


V[i] = M[i][C-1];

for(j = i + 1; j < C-1; j++)


V[i] -= M[i][j] * V[j];

V[i] /= M[i][i];
}
}

int main(int argc, char *argv[]) {


float mat[MAXL][MAXC];
float vet[MAXL];
int nl, nc, nv;

load (mat, &nl, &nc);


escalona (mat, nl, nc);
showmat (mat, nl, nc);
backsubstitution (mat, nl, nc, vet, &nv);
showvet (vet, nv);
}

A função implementa o método de , onde as linhas da


matriz aumentada são combinadas a fim de produzir uma matriz triangular. Observar
que a lógica do código é equivalente aos passos feitos no exemplo dado.

int escalona (float M[][MAXC], int L, int C)


{
int i,j;

for(i = 0; i < L - 1; i++)


for(j = i + 1; j < C; j++)
combina (M, L, C, i, j);
}

A função realiza a combinação linear entre as duas linhas indicadas:

int combina (float M[][MAXC], int L, int C, int Lx, int Ly) {
int j; float k;

k = -M[Lx][Lx] / M[Ly][Lx];

for(j = Lx; j < C; j++)


M[Ly][j] = (M[Ly][j] * k) + M[Lx][j];
}

A função recebe a matriz triangular e calcula as raízes, armazenando o


resultado no parâmetro .

Introdução à Programação: Conceitos e Práticas. Página 294


int backsubstitution (float M[][MAXC], int L, int C,
float *V, int *N) {
int i,j;

*N = L;

for(i = *N - 1; i >= 0; i--) {


V[i] = M[i][C-1];

for(j = i + 1; j < C - 1; j++)


V[i] -= M[i][j] * V[j];

V[i] /= M[i][i];
}
}

A seguinte interface ilustra uma entrada e saída do programa:

4 5
1 3 2 -4 -3
2 1 -3 1 -1
1 2 -5 3 2
-2 2 2 -1 4
#4 x 5
1.000 3.000 2.000 -4.000 -3.000
0.000 2.500 3.500 -4.500 -2.500
0.000 0.000 -14.000 13.000 10.000
0.000 0.000 0.000 -1.538 -6.154
#4
1.000
2.000
3.000
4.000

Introdução à Programação: Conceitos e Práticas. Página 295


Um segundo teste com uma matriz aumentada de , previamente testada em
uma planilha, produziu os seguintes resultados:

10 11
-4.70 -2.80 -3.00 -2.70 3.10 1.70 -3.20 -4.10 0.60 3.60 3.50
-2.00 3.40 -1.70 0.10 1.20 2.50 1.40 1.10 -1.20 3.30 -4.50
4.30 -2.10 -4.10 -4.70 0.60 -0.30 2.40 -1.20 -1.10 0.00 -3.10
-0.10 -1.20 -1.90 -3.50 1.60 0.20 1.90 -0.70 4.50 0.40 -3.40
-3.40 4.20 3.50 2.70 -3.30 3.50 -3.00 -4.20 1.00 0.70 3.30
2.90 0.40 -2.80 -3.90 3.80 5.00 -0.20 -2.50 -4.40 4.20 -3.60
-0.60 -1.00 -4.90 0.00 -3.10 -4.40 -4.10 4.70 0.20 4.00 -1.10
1.00 3.90 2.20 -4.00 3.20 -3.10 1.00 3.00 4.40 -2.80 -2.70
-4.50 5.00 0.00 4.80 3.10 -0.30 4.30 -4.60 1.00 1.70 0.70
-2.50 -4.90 3.20 1.00 -5.00 -2.70 -2.80 1.70 -1.70 -1.00 0.30
#10 x 11
-4.700 -2.800 -3.000 -2.700 3.100 1.700 -3.200 -4.100 0.600 3.600 3.500
0.000 -10.790 0.995 -2.935 0.280 -4.175 -6.490 -6.685 3.420 -4.155 14.075
0.000 -0.000 16.838 13.661 -7.673 -7.081 -5.269 4.775 4.695 -11.778 13.839
-0.000 -0.000 -0.000 -13.506 5.375 -1.832 17.750 5.588 40.479 -5.165 -29.199
-0.000 0.000 -0.000 -0.000 21.631 -17.780 33.625 49.585 35.364 -5.591 -53.287
-0.000 0.000 -0.000 -0.000 0.000 -52.240 67.240 74.358 87.105 -35.442 -79.702
-0.000 -0.000 0.000 0.000 0.000 -0.000 31.448 -17.035 -17.950 -24.857 10.516
0.000 -0.000 -0.000 0.000 -0.000 0.000 0.000 -36.629 -62.725 -21.276 58.877
0.000 -0.000 -0.000 0.000 -0.000 -0.000 0.000 -0.000 -69.450 -18.982 36.809
-0.000 0.000 -0.000 -0.000 0.000 0.000 0.000 0.000 0.000 -17.961 27.447
#10
-0.224
0.019
-0.833
0.767
0.477
0.050
-1.223
-0.527
-0.112
-1.528

O método apresentado e sua respectiva implementação tem efeito introdutório e não


exaure o tema de resolução de sistemas de equações lineares.

vi. Escrever um programa que ordene um vetor de strings.

Um vetor de strings em é representado como vetor de vetor, ou matriz de


caracteres. A lógica de ordenação se assemelha as já apresentadas para vetor de
números. A principal diferença está nas comparações, já que para string é
necessário utilizar uma função que teste os caracteres do vetor.

Segue a lógica proposta:

#include <stdio.h>
#include <string.h>

#define MAXL 100


#define MAXC 100

void load (char M[][MAXC], int *L) {


int i;
for(i = 0; gets(M[i]); i++)
;
*L = i;

Introdução à Programação: Conceitos e Práticas. Página 296


}

void show (char M[][MAXC], int L) {


int i;

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


printf("%s\n", M[i]);

}
void trocar (char *a, char *b) {
char t[MAXC];
strcpy (t, a);
strcpy (a, b);
strcpy (b, t);
}

void shiftmaior (char M[][MAXC], int a, int b) {


int i;
for(i = a; i < b; i++)
if(strcmp(M[i], M[i+1]) > 0)
trocar (M[i], M[i+1]);
}

void ordena (char M[][MAXC], int L) {


int i;
for (i = L-1; i > 0; i--)
shiftmaior (M, 0, i);
}

int main (int argc, char *argv[]) {


char mat[MAXL][MAXC];
int nl;

load (mat, &nl);


ordena (mat, nl);
printf ("\nOrdenados:\n");
show (mat, nl);
}

A função faz a leitura de várias strings e armazena na matriz. Não foi


necessário criar uma variável que indica a quantidade de colunas efetivamente em
uso, uma vez que as linhas podem conter strings de distintos tamanhos, unicamente
controlado pelo caractere . Uma única linha foi utilizada para as leituras:
[] , onde o final da leitura é detectado pelo retorno da função .
O usuário deverá teclar as teclas para encerrar a digitação. Neste
caso a função gets retornará . O efeito visual de ter teclado é .

A função tem a estrutura semelhante ao já comentado


anteriormente. É feito o deslocamento da string com a maior ordem léxica para o final
da matriz. Após os deslocamentos sucessivos, o vetor de strings ficará em ordem
crescente, considerando os valores dos caracteres conforme a tabela .

A função verifica se uma string tem ordem superior a próxima string, que
em caso positivo a troca de conteúdo é efetuada. Executando esta lógica em uma
sequência de strings, a que tiver maior ordem léxica vai sendo deslocada para o final
do vetor. A comparação entre duas strings é feita utilizando a função .

Introdução à Programação: Conceitos e Práticas. Página 297


void shiftmaior (char M[][MAXC], int a, int b) {
int i;
for(i = a; i < b; i++)
if(strcmp(M[i], M[i+1]) > 0)
trocar (M[i], M[i+1]);
}

A função permuta o conteúdo de duas strings. A lógica utiliza a função


para o câmbio dos conteúdos. A variável temporária é um vetor, a fim de acomodar
uma string inteira. Quando for apresentada a alocação dinâmica, esta lógica poderá
ser melhorada, fazendo apenas a permutação de endereços de memória, em vez de
arrays.

void trocar (char *a, char *b) {


char t[MAXC];
strcpy (t, a);
strcpy (a, b);
strcpy (b, t);
}

A seguinte interface ilustra uma entrada e saída do programa:

Josue Ribeiro
Carlos Galhardo
Andrea Conti Santos
Maria dos Santos
Justino Roman
Mario Valdivino
Roberto Diniz
Adair Pereira
Leona Mattos
Adalto Cardoso
Antonio Barbara
Pedro Antunes
Afonso Vargas
Alexandre Teixeira
Romana Peres
^Z

Ordenados:
Adair Pereira
Adalto Cardoso
Afonso Vargas
Alexandre Teixeira
Andrea Conti Santos
Antonio Barbara
Carlos Galhardo
Josue Ribeiro
Justino Roman
Leona Mattos
Maria dos Santos
Mario Valdivino
Pedro Antunes
Roberto Diniz
Romana Peres

Introdução à Programação: Conceitos e Práticas. Página 298


24. Agregados Heterogêneos - Estruturas

O tipo de dado permite representar um agrupamento de elementos, sendo


todos da mesma natureza. Porém, existem situações onde uma entidade do
problema possui informações de diversas naturezas. Neste caso, a utilização de
levará a distribuir estas informações em múltiplas estruturas, pois não é
possível agregar em um elementos de tipos diferentes. Assim, surge um novo
recurso que permite encapsular em um mesmo modelo informações de diversos
tipos.

A seguinte figura ilustra a característica principal do como agregado


homogêneo:

Figura 74: Array como agregado homogêneo.

A Linguagem disponibiliza o recurso de para modelar entidades utilizando


agregado heterogêneo. Neste caso, o programador deve definir primeiro o modelo
que irá representar este agregado. Em seguida, variáveis poderão ser criadas a
partir deste modelo. O seguinte exemplo ilustra uma variável criada a partir de um
modelo de que agrupa três informações de naturezas distintas.

Figura 75: Exemplo de agregado heterogêneo - struct.

Na notação utilizada o elemento de informação é identificado pelo tipo e pelo nome:

A seguinte expressão é comumente utilizada


para descrever os componentes principais de um projeto de um programa. A
linguagem de programação permite utilizar a abstração no ambiente computacional,
e desempenha papel essencial no ciclo de refinamento do problema até a sua
materialização em um produto acabado. Desta forma, podemos expressar a
seguinte relação: onde
vemos reforçada a importância não só apenas em organizar o código e os

Introdução à Programação: Conceitos e Práticas. Página 299


algoritmos, mas de forma idêntica dedicar esforços para uma boa modelagem dos
dados.

24.1. Tipo struct

Segue um programa que exemplifica o uso de , para o caso do diagrama


apresentado anteriormente:

#include <stdio.h>

struct DADOS {
int a;
float x;
char c;
};

int main () {
struct DADOS r;

scanf ("%c %d %f", &r.c, &r.a, &r.x);


printf ("[%c] [%d] [%f]\n", r.c, r.a, r.x);

return 0;
}

Observe que a primeira ação é definir o modelo que representará o agregado de


dados:

struct DADOS {
int a;
float x;
char c;
};

A sintaxe de uso do requer o nome do modelo e o bloco com a lista de


elementos ( ) que farão parte do agregado. Neste exemplo, o modelo foi
nomeado como , e a lista de campos contém os elementos , e
.

A função faz uso deste modelo para criar e manipular uma variável:

int main () {
struct DADOS r;

scanf ("%c %d %f", &r.c, &r.a, &r.x);


printf ("[%c] [%d] [%f]\n", r.c, r.a, r.x);

return 0;
}

A instrução cria a variável como sendo do tipo . A


fim de simplificar a forma de declarar variáveis e parâmetros, é possível associar um

Introdução à Programação: Conceitos e Práticas. Página 300


sinônimo ao tipo , utilizando um nome simples, em vez da forma
composta. A instrução é utilizada para este fim. Segue o exemplo:

#include <stdio.h>

struct DADOS {
int a;
float x;
char c;
};

typedef struct DADOS TDADOS;

int main () {
TDADOS r;

scanf ("%c %d %f", &r.c, &r.a, &r.x);


printf ("[%c] [%d] [%f]\n", r.c, r.a, r.x);

return 0;
}

O foi utilizado para informar que o tipo também está


disponível apenas como . A definição da variável foi alterada para
. O significado do programa permanece o mesmo com esta vinculação.

É possível combinar o e o em uma mesma instrução, omitindo o


identificador do . A definição tomaria a seguinte forma:

typedef struct {
int a;
float x;
char c;
} TDADOS;

Como referenciar as informações que compõe a variável ? No caso de array, os


elementos são referenciados acrescentando um índice ao nome do conjunto. Por
exemplo, se o nome do array é , os elementos serão [ ], sendo [ ], o
primeiro elemento, [ ] o segundo, e assim sucessivamente.

No caso de , os elementos do agregado, também chamados de campos da


estrutura, são referenciados utilizando a notação , ou seja: .
Assim, o elemento é referenciado como , o elemento é e o elemento é
. É importante destacar que tem todas as prerrogativas de uma variável do
tipo . O mesmo ocorre com , como ,e como .

Após a criação da variável , seguem instruções que carregam cada um dos seus
campos a partir de valores fornecidos via teclado, e apresenta estas informações na
tela. A instrução passa os endereços de cada
campo para as devidas manipulações.

Introdução à Programação: Conceitos e Práticas. Página 301


A seguinte interface ilustra uma entrada e saída do programa:

x 32 -3.14
[x] [32] [-3.140000]

Assim, podemos ampliar o conceito de variável para englobar as seguintes


características:

Tipo de Variáveis Exemplos


Variável simples de acesso direto
Variável simples de acesso indireto
Variável indexada [] [ ][ ]
Variável identificadora de campo

Neste próximo exemplo o agregado é ampliado com a adição de uma string e de um


vetor de , conforme a seguinte ilustração:

Figura 76: Struct com string e vetor de float.

A estrutura anterior com os campos , e , recebe mais dois componentes: e .


O campo é um de [ ] e o campo é uma estrutura
denominada e contém dois componentes: um inteiro e um de
[ ] . O agregado completo é denominado Uma variável
chamada é criada a partir do tipo

O seguinte programa define as estruturas indicadas e carrega valores a partir do


teclado:

#include <stdio.h>
#define MAX 100
#define MAXC 200

typedef struct {
int n;
float v[MAX];
} TVETOR;

Introdução à Programação: Conceitos e Práticas. Página 302


typedef struct {
int a;
float x;
char c;
char s[MAXC];
TVETOR vet;
} TDADOS;

void lervet (float vx[], int *nx) {


int i;
scanf ("%d", nx);
for (i = 0; i < *nx; i++)
scanf ("%f", &vx[i]);
}

void showvet (float vx[], int nx) {


int i;
for (i = 0; i < nx; i++)
printf ("#%d = %f\n", i, vx[i]);
}

int main () {
TDADOS r;

scanf ("%c %d %f%*c", &r.c, &r.a, &r.x);


scanf ("%[^\n]", r.s);
lervet (r.vet.v, &r.vet.n);
printf ("[%c] [%d] [%f]\n%s\n", r.c, r.a, r.x, r.s);
showvet (r.vet.v, r.vet.n);

return 0;
}

A variável é uma referência direta a uma estrutura do tipo . Observe a


forma de referência a cada um dos seus campos:

Referência Direta Tipo

[]

[]

A função faz as cargas diretas de , , , e repassa a carga de


e para a função A função é definida como
[] , onde seus parâmetros são do tipo vetor de e
ponteiro para . É importante reforçar que a passagem de parâmetro de
concede automaticamente à função chamada a permissão para alterar o conteúdo
das células. No caso de variáveis simples ou escalares a permissão deve ser
explícita informando à função chamada o endereço da respectiva variável.

Introdução à Programação: Conceitos e Práticas. Página 303


A seguinte interface ilustra uma entrada e saída do programa:

* -1 3.14
ola mundo belo
3
1.5
3.2
4.7
[*] [-1] [3.140000]
ola mundo belo
#0 = 1.500000
#1 = 3.200000
#2 = 4.700000

24.2. Passagem de struct como parâmetro: por cópia

O programa anterior será alterado para que toda a saída de dados seja feita em uma
única função. A variável será passada como parâmetro para a função ,
que fará as impressões na tela. Seguem a função modificada e a função
:

// mantida as definições anteriores.

void showrec (TDADOS rec) {


printf ("[%c] [%d] [%f]\n%s\n", rec.c, rec.a, rec.x, rec.s);
showvet (rec.vet.v, rec.vet.n);
}

int main () {
TDADOS r;

scanf ("%c %d %f%*c", &r.c, &r.a, &r.x);


scanf ("%[^\n]", r.s);
lervet (r.vet.v, &r.vet.n);
showrec (r);
return 0;
}

A passagem de parâmetro de não segue o conceito aplicado a , onde


implicitamente o endereço do argumento é copiado para o parâmetro. No caso de
argumento do tipo , todo o agregado é copiado para o parâmetro. Assim, há
um desacoplamento entre argumento e parâmetro. As alterações feitas, pela função
chamada, nos campos do parâmetro não são refletidas no argumento da função
chamadora. No caso de haverá sempre uma vinculação entre argumento e
parâmetro. Isto não acontece com .

A função main chama a função passando uma cópia da variável .


Internamente à esta cópia é vinculada ao parâmetro . Na sequência, um
código semelhante ao que se encontrava na versão anterior é executado, porém
referenciando os campos do parâmetro .

Introdução à Programação: Conceitos e Práticas. Página 304


24.3. Passagem de struct como parâmetro: por endereço

Uma nova versão de pode ser feita transferindo todo o código que carrega
valores para os campos de para uma função específica. Neste caso, é desejado
que uma outra função tenha permissão para alterar o conteúdo da . Para obter
esta permissão, é necessário que a função chamadora passe o endereço da variável
para a função chamada.

Segue uma versão alterada do programa anterior, onde a função carrega


valores para os campos da informada como parâmetro:

// mantida as definições anteriores.

void getrec (TDADOS *rec) {


scanf ("%c %d %f%*c", &(*rec).c, &(*rec).a, &(*rec).x);
scanf ("%[^\n]", (*rec).s);
lervet ((*rec).vet.v, &(*rec).vet.n);
}

int main () {
TDADOS r;

getrec (&r);
showrec (r);
return 0;
}

O parâmetro da função contém o endereço de uma do tipo


. O acesso a área onde estão os dados se dará de forma indireta. Se é
uma referência indireta a esta área, então a expressão é uma referência direta.
Temos aqui o mesmo conceito utilizado com as variáveis simples. Assim, a
referência ao campo é feita pela expressão . A seguinte tabela ilustra as
referências a cada um dos campos a partir de uma referência indireta ao :

Referência Indireta Tipo

[]

[]

É notório que a sintaxe para acesso indireto a pode se tornar bastante


rebuscada quando envolver várias indireções. Assim, a linguagem disponibiliza
uma forma alternativa para acesso a campos de uma a partir de um ponteiro.
Continuando com o exemplo, onde é um parâmetro que aponta para um
do tipo , então podemos utilizar o operador para ter acesso aos
campos. A esquerda deste operador deve vir o ponteiro para a e a direita
o nome do campo, dispensando o ponto, como ilustrado abaixo:

Introdução à Programação: Conceitos e Práticas. Página 305


Referência Indireta Tipo

[]

[]

A função passaria a ter a seguinte forma:

void getrec (TDADOS *rec) {


scanf ("%c %d %f%*c", &rec->c, &rec->a, &rec->x);
scanf ("%[^\n]", rec->s);
lervet (rec->vet.v, &rec->vet.n);
}

Observar que o vetor e a variável estão encapsulados no campo . Neste


caso, é uma referência direta ao agregado . A expressão é
uma referência direta ao campo que está contido no agrupamento apontado por
. Por esta razão, os campos de são referenciados utilizando a notação ponto.

24.4. Retorno de struct como resultado de função

Até o momento, vimos que uma função pode retornar um tipo escalar simples
como resultado, e não pode retornar uma cópia de todo um . No caso de
, quando necessário a função retorna o endereço do primeiro elemento como
resultado. Porém, é permitido que uma função retorne como resultado uma cópia de
uma com todos os seus campos. O programa exemplo foi modificado para
demonstrar este recurso:

// mantida as definições anteriores.

TDADOS getrec () {
TDADOS temp;
scanf ("%c %d %f%*c", &temp.c, &temp.a, &temp.x);
scanf ("%[^\n]", temp.s);
lervet (temp.vet.v, &temp.vet.n);
return temp;
}

int main () {
TDADOS r;

r = getrec ();
showrec (r);
return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 306


A função chama a função sem argumento algum. A função cria
a variável local como para armazenar provisoriamente as
informações fornecidas, e ao final retorna todo o conjunto representado por
como resultado. Como é uma variável de referência direta, os seus campos
são acessados com a notação ponto. Ao retornar para , este bloco de dados é
copiado para a variável .

É notória a vantagem do uso de para organizar e agrupar informações


correlacionadas entre si. É importante destacar que o conceito de variável se aplica
integralmente aos campos de uma . A referência a um campo através da
descrição completa tem todas as prerrogativas de uma variável convencional, e
pode ser utilizada da mesma forma, tal como compor expressões e serem passadas
como parâmetro.

Não há restrições para composição entre variáveis , e , sendo


possível modelar um , com campo , dentre outras.

24.5. Exercícios

i. Elaborar um programa que calcula o índice de massa corpórea ( ) a partir do


peso e altura de várias pessoas. O nome e o sexo da pessoa também serão
fornecidos.

A seguinte estrutura de dados agrupa as informações de uma pessoa:

É necessário utilizar um vetor formado por elementos do tipo . Assim, cada


elemento do vetor conterá uma estrutura com todas as informações relativas a uma
pessoa.

Segue o programa proposto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 100
#define MAXC 150

typedef struct {
char nome[MAXC];
char sexo;
float peso, altura, imc;

Introdução à Programação: Conceitos e Práticas. Página 307


} TPESSOA;

void load (TPESSOA v[], int *n) {


int i;

for (i = 0; scanf ("%[^\n]", v->nome) != EOF; i++, v++) {


scanf ("%*c%c%*c", &v->sexo);
scanf ("%f %f%*c", &v->peso, &v->altura);
}
*n = i;
}

int imcint (float x) {


if (x < 18.5) return 0;
if (x < 25.0) return 1;
if (x < 30.0) return 2;
return 3;
}

void relat (TPESSOA *v, int n) {


char IMCSTR[4][10] = {"BAIXO", "NORMAL", "EXCESSO", "OBESO"};
int i;
printf ("%-4s%-40s%-3s%6s%6s%6s%6s\n", "NUM", "NOME", "SEX", "PESO",
"ALT", "IMC", "OBS");
for (i = 0; i < n; i++, v++)
printf ("%-4d%-40s %c %6.2f %6.2f %6.2f %s\n",i+1, v->nome, v->sexo,
v->peso, v->altura,
v->imc, IMCSTR[imcint(v->imc)]);
}

void getimc (TPESSOA *v, int n) {


int i;
for (i = 0; i < n; i++, v++)
v->imc = v->peso/(v->altura*v->altura);
}

int main() {
TPESSOA vp[MAX];
int np;

load (vp, &np);


getimc (vp, np);
relat(vp, np);

return 0;
}

A função define o vetor [ ], capaz de conter elementos. A


variável indicará quantas posições do vetor foram efetivamente inicializadas.
Em seguida são chamadas as funções que carregam valores para estas variáveis,
calcula o e gera o relatório.

A função tem a seguinte lógica:

void load (TPESSOA v[], int *n) {


int i;
for (i = 0; scanf ("%[^\n]", v->nome) != EOF; i++, v++) {
scanf ("%*c%c%*c", &v->sexo);

Introdução à Programação: Conceitos e Práticas. Página 308


scanf ("%f %f%*c", &v->peso, &v->altura);
}
*n = i;
}

A entrada de dados é encerrada quando usuário tecla no momento da


entrada do , fazendo com que a função seja encerrada com retorno
( ). Os parâmetros da função são: o vetor de estruturas do tipo e o
endereço do inteiro que conterá a quantidade de pessoas informadas. O primeiro
parâmetro [ ] é do tipo , e por conseguinte segue a regra de
passagem de parâmetro de , onde o parâmetro conterá o endereço do primeiro
elemento do vetor informado pelo argumento. Desta forma, este parâmetro é
compatível com a definição alternativa: Fica a cargo do programador
escolher entre a notação indexada ou ponteiro para manipular os elementos de .

Nesta versão, usando a notação ponteiro, o parâmetro vai sendo atualizado para
apontar para o próximo agregado do vetor. A variável contadora de repetição é
utilizada para obter de forma simples a quantidade de dados fornecidos.

A função que calcula o percorre cada elemento do vetor, calculando o índice de


massa corpórea a partir do peso e altura ( ) e armazena o
resultado no campo apropriado.

void getimc (TPESSOA *v, int n) {


int i;
for (i = 0; i < n; i++, v++)
v->imc = v->peso/(v->altura*v->altura);
}

As demais funções seguem lógica semelhante. Considerando a seguinte entrada


de dados:

LUIS MEDEIROS DE OLIVEIRA


M
78.9
1.99
MARCOS ROBERTO DA SILVA
M
55.8
1.35
.
.
.

Introdução à Programação: Conceitos e Práticas. Página 309


A seguinte interface ilustra a saída do programa:

NUM NOME SEX PESO ALT IMC OBS


1 LUIS MEDEIROS DE OLIVEIRA M 78.90 1.99 19.92 NORMAL
2 MARCOS ROBERTO DA SILVA M 55.80 1.35 30.62 OBESO
3 MARIANA DA SILVA SANTOS F 61.20 1.72 20.69 NORMAL
4 PEDRO SEBASTIAN CAMARGO M 54.70 1.60 21.37 NORMAL
5 ROGERIO BATISTA M 88.10 1.58 35.29 OBESO
6 JOAO ROCHA PALMARES M 79.20 1.31 46.15 OBESO
7 THIAGO MINO CARTA M 86.60 1.84 25.58 EXCESSO
8 JOSEFINA CATIANE PEDREIRA F 62.00 1.52 26.84 EXCESSO
9 VALÉRIA MARIA DOS SANTOS F 52.70 1.56 21.66 NORMAL
10 JONATAS FERREIRA DA SILVA M 72.50 1.42 35.96 OBESO

Introdução à Programação: Conceitos e Práticas. Página 310


24.6. Abstrações da Linguagem C

A seguinte figura esboça um diagrama com os principais recursos da linguagem


organizados por elementos de abstração:

Figura 77: Abstrações típicas de uma linguagem procedural

Introdução à Programação: Conceitos e Práticas. Página 311


Este diagrama destaca que a linguagem proporciona de forma similar recursos
voltados tanto para a codificação da lógica do problema quanto para a modelagem
dos dados. Assim, o programador deve dedicar atenção e tempo às duas vertentes
da abstração.

Introdução à Programação: Conceitos e Práticas. Página 312


25. Arquivos

Os tópicos anteriores abordaram os tipos de dados e , sendo que quase


todos os exemplos apresentaram a necessidade de entrada de dados via teclado.
Esta situação é bastante comum, pois de alguma forma o ou devem
ser populado com as informações que serão processadas. Porém, ao codificar a
lógica de um problema e iniciar os testes é frequente o surgimento de erros. Então,
inicia um ciclo de correção e teste que inclui a repetição de toda a entrada dos
dados. O trabalho de depuração do programa torna-se exaustivo. Este cenário se
acentua quando a entrada de dados envolve grande volume de informações. Para
melhor compreensão dos conceitos, vamos ilustrar as principais abstrações
envolvidas com o fluxo de dados, desde a sua origem, o teclado, até o seu destino
final, a memória:

Figura 78: Fluxo de dados Teclado-Memória.

O diagrama apresenta o dispositivo de origem dos dados, o , e o local onde


se pretende armazenar as informações digitadas, a . Para operacionalizar
este fluxo, várias abstrações são responsáveis por mediarem estes elementos. Para
os detalhes que interessam neste momento, iremos considerar uma abstração que
permite a comunicação entre o dispositivo de entrada e o programa.

O Sistema Operacional ( ) é o responsável por facilitar a interface com o teclado.


Assim, uma das funções do é capturar as teclas digitadas e disponibilizar em
um local que o programa possa ler. Por sua vez, a linguagem de programação
disponibiliza uma série de funções para transferência de dados desta área para a
memória.

Na figura apresentada, temos pelo lado do programa a expressão ,


onde é definido como . O efeito deste código é carregar a variável com um
número real a ser obtido via teclado. A partir de então, os caracteres serão lidos a
partir da área que serve de interface com o teclado. Esta área é representada por
uma variável denominada e sua definição é . A conexão entre
esta variável e o teclado é implícita ao programador. O e
buscam as informações neste dispositivo. Caso não haja dados disponíveis em
, então o aguardará que o usuário proceda a digitação, preenchendo
assim esta área de espera.

De forma semelhante temos o fluxo inverso, quando se pretende escrever algo na


tela de vídeo. O seguinte diagrama ilustra os principais elementos:

Introdução à Programação: Conceitos e Práticas. Página 313


Figura 79: Fluxo de dados Memória-Tela de Vídeo.

A instrução alimenta a área que serve de interface entre o


programa e o dispositivo de saída, a tela de vídeo. Os dados a serem impressos na
tela são convertidos para um padrão e armazenados na área representada pela
variável , cuja definição é .O é responsável pelos detalhes
que causam o efeito visual na tela de vídeo.

No diagrama da leitura do teclado, o fluxo das informações está no sentido que


denominamos , enquanto que no processo de escrita na tela, o fluxo está no
sentido denominado . Neste caso, a conexão entre a variável e as
funções de escrita na tela, tais como , e também é implícita.

Até o momento as funções e e suas assemelhadas, foram utilizadas


sem necessidade de conhecer a existência das variáveis e .

Uma organização similar pode ser utilizada para explicar o processamento


envolvendo arquivos. Da mesma forma que o teclado serve como origem de dados
para alimentar um programa, um arquivo também pode exercer este papel.
Analogamente, a tela de vídeo que serve como destino final de informações, pode
ser substituída por arquivos.

Observe o seguinte diagrama, para o fluxo de entrada de dados a partir de arquivos:

Figura 80: Fluxo de dados Arquivo-Memória.

O conjunto teclado-vídeo deu lugar ao arquivo. Dentre as principais funções do ,


podemos destacar o gerenciamento dos recursos de hardware (memória e ),

Introdução à Programação: Conceitos e Práticas. Página 314


controle dos processos, administração do sistema de arquivos e o gerenciamento
dos dispositivos de Entrada e Saída. Vemos aqui um destaque ao sistema de
arquivos. O conceito de arquivo é bastante simples, embora os detalhes envolvam
aspectos complexos.

A simplicidade do uso advém da capacidade de abstração proporcionada pelo


ambiente computacional, principalmente o Sistema Operacional e a linguagem de
programação. Neste sentido, arquivo pode ser entendido como uma abstração de
dados que serve tanto como fonte de informações que alimenta um fluxo de bytes
para o programa, quanto um depósito de dados, apto a receber um fluxo de bytes
gerado por um programa. Esta característica denota semelhança com os papeis
desempenhados pelo teclado e vídeo, e suas capacidades de prover e receber
fluxos de dados.

No caso do conjunto teclado-vídeo alguns aspectos eram implícitos ao programa,


como por exemplo as variáveis de interface ( e ), e referências diretas
aos dispositivos teclado e vídeo. Porém, quando o arquivo surge em cena, há a
necessidade de definições explícitas.

Um arquivo está associado a um nome, geralmente acrescido da identificação da


unidade de armazenamento e todo o caminho de pastas e subpastas até se chegar
ao local efetivo de residência do arquivo (por ex.:
"c:\dados\trabalhos\comp\lista.txt"). Assim, enquanto o teclado e o vídeo eram
recursos implícitos, o arquivo passa a ter um nome vinculado. A conexão entre as
variáveis de interface ( e ) com o teclado e vídeo era transparente ao
programador.

Agora, a associação entre a variável e o recurso arquivo passa a ser declarado pelo
programador. As funções de leitura do teclado e escrita na tela de vídeo são
substituídas por funções semelhantes que operam os fluxos com arquivos.

A seguinte figura completa a comparação entre a arquitetura de fluxos de


informação, com ênfase para o sentido entre memória e arquivo:

Figura 81: Fluxo de dados Memória-Arquivo.

A tabela abaixo compara os programas para leitura de dados do teclado e do


arquivo texto:

Introdução à Programação: Conceitos e Práticas. Página 315


Leitura do Teclado Leitura de Arquivo
#include <stdio.h> #include <stdio.h>
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
float x; float x;
FILE *fp;

fp = fopen ("dados.txt", "r");


scanf ("%f", &x); fscanf (fp, "%f", &x);
printf ("[%.2f]", x); printf ("[%.2f]", x);
fclose (fp);
} }
Este programa faz a leitura de um Este programa faz a leitura do primeiro
digitado pelo usuário e o imprime na tela. de um arquivo texto. O arquivo foi
Uma entrada/saída para este caso é: previamente criado usando um editor de
textos e salvo com o nome .
-3.14 Uma entrada/saída para este caso é:
[-3.14]
[-3.14]
A primeira linha representa o eco do ,
A segunda linha é resultante do . A única linha é resultante do

Abordaremos neste primeiro contexto a manipulação de . Neste tipo de


arquivo é suposto que os bytes armazenados são codificações de caracteres de
acordo com o padrão , e seu conteúdo representa informações textuais. O
usuário pode visualizar o conteúdo do arquivo usando um editor de texto, e o
entendimento da informação será imediato.

Os formam uma outra classificação, onde o conteúdo é também


uma sequência de bytes, porém representando codificações compreensíveis a
programas ou ao processador. Se abrirmos um com um editor de
texto, a visualização destes bytes como símbolos da tabela não resultará em
algo compreensível ao usuário.

Por exemplo, os arquivos executáveis são casos de O seu


conteúdo foi gerado por um compilador com a intenção de representar as
codificações numéricas das instruções de um programa em linguagem de máquina
cuja interpretação caberá ao processador. Por outro lado, o programa fonte escrito
em e armazenado em arquivo, utiliza o padrão de pois a informação
ali contida é compreendida pelo programador.

Iremos comentar os detalhes do programa exemplo apresentado anteriormente:

int main(int argc, char *argv[]) {


float x;
FILE *fp;

fp = fopen ("dados.txt", "r");


fscanf (fp, "%f", &x);
printf ("[%.2f]", x);
fclose (fp);
}

Introdução à Programação: Conceitos e Práticas. Página 316


O dispositivo padrão para leitura de dados é comumente o teclado. Neste caso,
muitos detalhes de programação ficam implícitos. Quando desejamos sair deste
padrão e obter dados contido em um arquivo, então os detalhes deverão ser
incluídos no programa. A primeira ação é declarar uma variável do tipo ,
cujo efeito é preparar uma área de memória para gerenciar a interface com o
arquivo. Neste exemplo, foi criada a variável . O tipo é uma contida
em com a seguinte definição:

typedef struct _iobuf {


char* _ptr; // ponteiro para a posição atual no buffer
int _cnt; // byte no buffer
char* _base; // ponteiro para onde se encontra o buffer
int _flag; // file mode
int _file; // número com o file descriptor
int _charbuf;
int _bufsiz; // tamanho do buffer
char* _tmpfname; // nome de eventual arquivo temporário
} FILE;

Algumas outras definições incluem:

#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2

#define stdin (&_iob[STDIN_FILENO])


#define stdout (&_iob[STDOUT_FILENO])
#define stderr (&_iob[STDERR_FILENO])

É suposto que o programador não deva fazer uso direto dos elementos desta
estrutura, ficando isto a cargo das funções de entrada/saída da linguagem.

Em seguida , é necessário requisitar ao permissão para uso


de um determinado arquivo, mencionando o nome e o tipo de operação desejada. A
função foi utilizada para esta tarefa. O resultado desta chamada é um
endereço de memória onde a estrutura de interface foi alocada. Este endereço é
armazenado na variável , que a partir de então será o elemento de acesso ao
arquivo.

A função é definida com dois parâmetros do tipo strings, sendo um com a


identificação do arquivo ( ) e outro com o tipo de
operação desejada. Neste caso, o nome do aquivo é e a operação
desejada é de leitura ( ). Na eventualidade de ocorrer algum impedimento de uso
do arquivo solicitado, a função resultará no valor .

Na sequência , já é possível ler o conteúdo do arquivo. A função foi


utilizada para efetuar a leitura de um número real. O primeiro parâmetro é a variável
que indica o arquivo de origem dos dados, e os demais parâmetros seguem o
mesmo padrão da função . O número lido foi armazenado na variável , e
impresso na tela através do que segue.

Introdução à Programação: Conceitos e Práticas. Página 317


Concluído o trabalho com o arquivo , o programa deve informar ao Sistema
Operacional, através da expressão que o arquivo está liberado para
outros usos.

Além da facilidade nos testes, problemas reais quase sempre lidam com situações
que determinam a necessidade de armazenar dados em meios permanentes. Neste
contexto, os dados que servem de entrada para algum programa poderiam ter sido
informados apenas uma vez e guardados em algum meio persistente, ou seja, com
capacidade de reter estas informações. O recurso de proporciona esta
facilidade, permitindo que dados sejam lidos e gravados em meios de
armazenamento permanentes.

Desta forma, a poderá ser feita através da leitura de informações


pré-gravadas em . Quando conveniente, parte das informações pode ser lida
a partir de entrada via teclado (dispositivo de entrada padrão). A
pode ser direcionada para ser gravada também em , mantendo quando
conveniente a apresentação de informações na tela do computador (dispositivo de
saída padrão).

25.1. Arquivo Texto

A linguagem oferece vários recursos para manipular arquivos. Alguns deles já


utilizados anteriormente. O arquivo é um recurso gerenciado pelo
e possui vários atributos, tais como: nome, extensão, pasta
onde está armazenado, data de criação, permissões de uso, tipo dentre outros.

O uso do arquivo texto será contextualizado através de exemplos que permitirão


conhecer as suas principais funções de manipulação.

CASO 1: Este exercício elabora um relatório de notas, incluindo o cálculo das


médias de cada aluno e das médias da turma para cada bimestre. Iremos considerar
a existência de um arquivo texto já previamente configurado com os dados a serem
processados:

"notas.txt"
4
Adair Pereira; 8.4 3.3 3.4 4.2
Adalto Cardoso; 9.9 4.2 6.8 9.1
Afonso Vargas; 5.1 5.6 6.5 6.2
Alexandre Teixeira; 8.8 8.9 6 7.2
Andrea Conti Santos; 4.1 2.5 8.9 2.3
Antonio Barbara; 4.4 9.4 7.4 7.6
Carlos Galhardo; 2.9 9 9.8 2.8
Josue Ribeiro; 4.8 9.9 6.7 4.9
Justino Roman; 3.6 9.8 6.3 2
Leona Mattos; 4.7 5.4 3.8 2.4
Maria dos Santos; 7.7 4.8 5.5 9.1
Mario Valdivino; 3.4 3.4 9.5 7.2
Pedro Antunes; 7.3 10 9.4 9.5
Roberto Diniz; 6.4 4.2 5.2 9.6
Romana Peres; 7.9 2.1 2.1 8.4

Introdução à Programação: Conceitos e Práticas. Página 318


A primeira linha do arquivo contém um número inteiro que indica a quantidade de
notas por aluno. Em seguida, virão várias linhas, cada uma contendo o nome do
aluno separado das notas parciais pelo .

O programa deverá ler estes dados e estruturar adequadamente na memória. Em


seguida, deverá realizar os cálculos das médias aritméticas de cada aluno e das
médias aritméticas de cada bimestre. Por fim, um relatório com estas informações
deverá ser gerado.

Segue uma proposta de solução:

#include <stdio.h>
#include <string.h>

#define MAXL 100


#define MAXC 100
#define MAXB 10

float *lervet (FILE *f, float *V, int N) {


int i;
for (i = 0; i < N; i++)
fscanf (f, "%f", V+i);
fscanf (f, "%*c");
return V;
}

float *printvet (FILE *f, float *V, int N) {


int i;
for (i = 0; i < N; i++)
fprintf (f, "%5.1f", *(V+i));
return V;
}

void ler (char *fn, char NOMES[][MAXC], float NOTAS[][MAXB],


int *L, int *C) {
int i;
FILE *fp;

fp = fopen (fn, "r");


if (fp == NULL) return;

fscanf (fp, "%d%*c", C);


for (i = 0; !feof(fp); i++) {
fscanf (fp, "%[^;]%*c", NOMES[i]);
lervet (fp, NOTAS[i], *C);
}

*L = i;
fclose (fp);
}

void cabecalho (FILE *arq, int N) {


int j;
fprintf (arq, "%-20s", " ALUNO");
for (j = 1; j <= N; j++)
fprintf (arq, "%4s%d", "N", j);
fprintf (arq, " Med\n");

Introdução à Programação: Conceitos e Práticas. Página 319


}

void linha (FILE *arq, char *str, float V[], int N) {


int j;
fprintf (arq, "\n%-20s", str);
printvet (arq, V, N);
}

void show (char *fn, char NOMES[][MAXC], float NOTAS[][MAXB],


float MEDIAS_AL[], float MEDIAS_BIM[], int L, int C) {
int i;
FILE *fp;

fp = fopen (fn, "w");

cabecalho (fp, C);

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


linha (fp, NOMES[i], NOTAS[i], C);

fprintf (fp, "%5.1f", MEDIAS_AL[i]);


}

linha (fp, "MEDIA BIMESTRAL", MEDIAS_BIM, C);

fclose (fp);
}

void calc_med_al (float NOTAS[][MAXB], float MEDIAS_AL[], int L, int C) {


int i, j;
float s;
for (i = 0; i < L; i++) {
s = 0;
for (j = 0; j < C; j++)
s += NOTAS[i][j];
MEDIAS_AL[i] = s/C;
}
}

void calc_med_bim (float NOTAS[][MAXB], float MEDIAS_BIM[], int L, int C)


{
int i, j;
float s;
for (j = 0; j < C; j++) {
s = 0;
for (i = 0; i < L; i++)
s += NOTAS[i][j];
MEDIAS_BIM[j] = s/L;
}
}

int main () {
char nomes[MAXL][MAXC]; int lin;
float notas[MAXL][MAXB];
float medias_al[MAXL];
float medias_bim[MAXB]; int col;

ler ("notas.txt", nomes, notas, &lin, &col);


calc_med_al (notas, medias_al, lin, col);

Introdução à Programação: Conceitos e Práticas. Página 320


calc_med_bim (notas, medias_bim, lin, col);
show ("relat.txt", nomes, notas, medias_al, medias_bim, lin, col);

A função define as estruturas de dados para o problema e divide os trabalhos


entre várias funções. Os nomes dos alunos serão armazenados em um vetor de
strings, também definida como matriz de caracteres: [ ][ ]. As
notas parciais estarão associadas a uma matriz de números reais:
[ ][ ]. As médias de cada aluno e as médias bimestrais ficarão
em dois vetores: [ ]e [ ].

A seguinte figura ilustra as tabelas definidas para este problema, bem como as duas
variáveis de controle para as quantidades de linhas ( ) e quantidades de colunas
( ) efetivamente em uso no contexto da execução.

char nomes[MAXL][MAXC] float notas[MAXL][MAXB] float medias_al[MAXL]


... ...
... ...
lin ... lin ... lin
... ...
...
...
...
...
...
...
...
...
...

...

...

...

...

...

...

...

...
... ...
... ...
... ...
... ...

col

float medias_bim[MAXB]
...

col

Figura 82: Estruturas de memória.

Em seguida, a função divide o processamento entre as funções


e responsáveis por popular as variáveis a partir
de um arquivo texto, processar os cálculos das médias e gerar um relatório em
arquivo:

a) A função faz o carregamento dos dados residentes em um arquivo texto e


armazena em diversas estruturas na memória:
void ler (char *fn, char NOMES[][MAXC], float NOTAS[][MAXB],
int *L, int *C) {
int i, j;
FILE *fp;

fp = fopen (fn, "r");


if (fp == NULL) return;

fscanf (fp, "%d%*c", C);

Introdução à Programação: Conceitos e Práticas. Página 321


for (i = 0; !feof (fp); i++) {
fscanf (fp, "%[^;]%*c", NOMES[i]);
lervet (fp, NOTAS[i], *C);
}

*L = i;
fclose (fp);
}

O primeiro parâmetro da função é uma referência à string contendo o nome do


arquivo a ser carregado. Os parâmetros e são referências aos locais
onde serão armazenados os nomes dos alunos e suas respectivas notas. Os
parâmetros e são ponteiros vinculados as células com o número de linhas e
número de colunas aplicáveis ao contexto da execução.

Os principais elementos da função para efetivar a leitura dos dados do arquivo


são:

i. Criação de uma variável do tipo que será utilizada para se conectar ao


arquivo texto;

ii. Chamada da função que liga a variável a um arquivo designado pelo seu
nome completo, seguido da descrição da operação desejada:
A função tem a seguinte definição:

O parâmetro deve conter uma string contendo o nome do arquivo a ser


aberto. O segundo parâmetro descreve o modo de acesso ao arquivo, podendo ser
uma das seguintes opções:

Abrir o arquivo apenas para operações de leitura dos


dados. O arquivo deve existir.
Criar um arquivo vazio para operações de escrita. Caso
exista um arquivo com o mesmo nome, o seu conteúdo
será apagado, equivalendo a criar um arquivo vazio.
Abrir um arquivo para operações de escrita a partir do final
do aquivo. As operações de escrita irão adicionar os dados
a partir do final do arquivo. Um arquivo vazio será criado,
no caso de não existir.
Abrir um arquivo para atualização (leitura e escrita). O
arquivo deve existir.
Criar um arquivo vazio e abrir para operações de
atualização (leitura e escrita). Caso exista um arquivo com
o mesmo nome, o seu conteúdo será apagado,
equivalendo a criar um arquivo vazio.
Abrir um arquivo para operações de atualização (leitura e
escrita). As operações de escrita irão adicionar os dados a
partir do final do arquivo. Um arquivo vazio será criado, no
caso de não existir.

Introdução à Programação: Conceitos e Práticas. Página 322


No caso de sucesso, a função retorna um ponteiro para uma estrutura do tipo
, cujo valor deve ser guardado para uso em operações futuras com o arquivo
relacionado. Em caso de insucesso, é retornado. Na maioria dos compiladores,
uma variável global chamada contém um inteiro que identifica melhor o tipo de
falha ocorrida.

iii. O primeiro dado a ser lido do arquivo é o número inteiro que indica a quantidade de
notas atribuídas a cada aluno. A sintaxe é bastante semelhante a um para
leitura do teclado. É necessário indicar que a leitura se dará a partir de um arquivo,
informando o ponteiro retornado pelo que no caso em questão foi
armazenado na variável :

É comum efetuar leituras sucessivas de arquivo. Assim, a linguagem mantém um


indicador de posição que marca o ponto de parada da última leitura. Desta forma,
leituras seguidas vão consumindo as informações do arquivo, em direção ao seu
final.

iv. A instrução mantém uma variável contadora de


repetição, que servirá como indicador da quantidade de alunos. A repetição é
encerrada quando as leituras sucessivas levarem ao final do arquivo vinculado à .
A função retorna verdadeiro ( ) ou falso ( ) se o
indicador de posição corresponde ao final do arquivo. Assim, a expressão
, que indica a condição para permanência no , será falso quando o
final de arquivo for alcançado.

v. Na condição em que o final de arquivo não foi alcançado, então o é adentrado, e


as leituras das informações são processadas. Primeiramente, a instrução
[ ] [ ] faz a leitura de toda a sequência de caracteres a
partir do indicador de posição do arquivo até alcançar o caractere ( [ ] ), e em
seguida este caractere é lido e descartado ( ). Neste ponto, o indicador de
posição do arquivo está posicionado sobre o primeiro caractere após o .

Observar que o parâmetro está definido como uma matriz de , ou um


vetor de vetor. Por esta característica, a expressão [ ] , sem o segundo
índice, é uma referência a linha da matriz, que configura um vetor de ,
compatível assim com uma string.

vi. A próxima instrução, ainda embutida no realiza a leitura, nota a nota, do aluno ,
armazenando estes valores nas células do vetor [ ] . A função é
chamada para realizar estas leituras:

float *lervet (FILE *f, float *V, int N) {


int i;
for (i = 0; i < N; i++)
fscanf (f, "%f", V+i);
fscanf (f, "%*c");
return V;
}

Os parâmetros desta função indicam o arquivo com os dados, e o local de memória a


serem armazenados, bem como a quantidade de valores a serem lidos. A repetição
consiste em leituras de um dado e armazenamento na respectiva célula de
um vetor de .

Introdução à Programação: Conceitos e Práticas. Página 323


O é responsável por estas leituras. Ao finalizar o e consequentemente as
leituras das notas, o indicador de posição estará parado sobre o caractere
correspondente ao ( .

A expressão lê e descarta o , fazendo com que o indicador


de posição do arquivo fique estacionado sobre o primeiro caractere do próximo nome
de aluno, e apto para iniciar a leitura dos dados do próximo aluno.

A função retorna o endereço da primeira célula do vetor informado.

vii. Retornando o fluxo do processamento para a função ,o é encerrado quando


as leituras sucessivas levarem o indicador de posição ao final do arquivo. Neste
ponto, a expressão retornará verdadeiro, fazendo com que o valor da
expressão seja falso, encerrando o . O último valor da variável
contém a quantidade de alunos no arquivo, e seu valor deve ser copiado para o local
destinado pelo parâmetro ( ). Finalizando as leituras, o programa deve
informar ao Sistema Operacional que o arquivo está liberado. O Sistema Operacional
mantém um registro dos recursos que os programas requisitam durante a execução
(por ex. memória e arquivos). É boa prática informar o Sistema Operacional quando
o recurso não é mais necessário. A instrução faz esta liberação. A partir
deste ponto, referências ao arquivo vinculado a levarão a erro de lógica.

b) Após realizar os cálculos das médias dos alunos e das médias de cada bimestre,
todas as estruturas de dados estão devidamente preenchidas. O próximo passo é
preparar um relatório com estas informações. A função produz um relatório
em arquivo texto:
void show (char *fn, char NOMES[][MAXC], float NOTAS[][MAXB],
float MEDIAS_AL[], float MEDIAS_BIM[], int L, int C) {
int i;
FILE *fp;

fp = fopen (fn, "w");

cabecalho (fp, C);

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


linha (fp, NOMES[i], NOTAS[i], C);

fprintf (fp, "%5.1f", MEDIAS_AL[i]);


}

linha (fp, "MEDIA BIMESTRAL", MEDIAS_BIM, C);

fclose (fp);
}

Os parâmetros da função se assemelham aos da função , acrescidos dos


vetores com as médias. Os principais elementos da função são:

i. Criação de uma variável do tipo que será utilizada para se conectar ao


arquivo texto;

Introdução à Programação: Conceitos e Práticas. Página 324


ii. Chamada da função que liga a variável a um arquivo designado pelo seu
nome completo, seguido da descrição da operação desejada:
A string para o modo de abertura determina a criação de um arquivo vazio para
operações de escrita. Caso exista um arquivo com o mesmo nome, o seu conteúdo
será apagado, equivalendo a criar um arquivo vazio.

iii. A primeira ação após a criação do arquivo é gravar um cabeçalho descrevendo as


colunas da tabela que virá na sequência.

void cabecalho (FILE *arq, int N) {


int j;
fprintf (arq, "%-20s", " ALUNO");
for (j = 1; j <= N; j++)
fprintf (arq, "%4s%d", "N", j);
fprintf (arq, " Med\n");
}

A função possui dois parâmetros, sendo o primeiro uma referência a um


arquivo e o outro parâmetro um inteiro que define a quantidade de colunas. No caso
de igual a colunas, a função produzirá a seguinte linha:

ALUNO N1 N2 N3 N4 Med

A instrução – utiliza o formato – que define


um padrão com caracteres, sendo o texto da string alinhado à esquerda neste
espaço. O quadro seguinte utiliza um régua para melhor compreensão do formato.
Observe que o texto foi ajustado a esquerda em um espaço com
caracteres.

123456789012345678901234512345123451234512345
ALUNO N1 N2 N3 N4 Med

Caso o formato fosse , teríamos a seguinte saída, com alinhamento a direita


no espaço de caracteres:

123456789012345678901234512345123451234512345
ALUNO N1 N2 N3 N4 Med

Em seguida, tem-se o que imprime o texto seguido do valor de para


produzir a saída indicada. Por fim, o produz o texto da última
coluna.

iv. Na sequência da função tem-se o , que a cada repetição produz uma linha
do relatório com os dados do aluno. Cada linha contém o nome do aluno, seguido
das notas e na última coluna a média do aluno. A função é responsável por
imprimir o nome do aluno e suas notas, conforme a seguinte lógica:

void linha (FILE *arq, char *str, float V[], int N) {


int j;
fprintf (arq, "\n%-20s", str);
printvet (arq, V, N);

Introdução à Programação: Conceitos e Práticas. Página 325


}

A lógica é simples e consiste de um primeiro que imprime o nome do aluno


em um espaço de caracteres com alinhamento a esquerda. Na sequência, a
função é chamada para imprimir cada uma das notas do aluno com um
formato O seguinte quadro ilustra os campos impressos até a primeira linha:

123456789012345678901234512345123451234512345
ALUNO N1 N2 N3 N4 Med

Adair Pereira 8.4 3.3 3.4 4.2

v. Após imprimir o nome e as notas bimestrais do aluno, o processamento retorna para


a função , onde a linha é completada com a gravação da média do aluno , pela
instrução [] .

123456789012345678901234512345123451234512345
ALUNO N1 N2 N3 N4 Med

Adair Pereira 8.4 3.3 3.4 4.2 4.8

O da função fará a impressão de uma linha para cada aluno contido na


estrutura.

vi. Após a impressão dos dados de todos os alunos, a função grava uma última
linha com as médias do bimestre. A função é também utilizada para este
propósito, porém com os argumentos alterados para produzir o texto
seguido dos valores do vetor .

Segue o arquivo completo produzido para o caso exemplo:

ALUNO N1 N2 N3 N4 Med

Adair Pereira 8.4 3.3 3.4 4.2 4.8


Adalto Cardoso 9.9 4.2 6.8 9.1 7.5
Afonso Vargas 5.1 5.6 6.5 6.2 5.9
Alexandre Teixeira 8.8 8.9 6.0 7.2 7.7
Andrea Conti Santos 4.1 2.5 8.9 2.3 4.4
Antonio Barbara 4.4 9.4 7.4 7.6 7.2
Carlos Galhardo 2.9 9.0 9.8 2.8 6.1
Josue Ribeiro 4.8 9.9 6.7 4.9 6.6
Justino Roman 3.6 9.8 6.3 2.0 5.4
Leona Mattos 4.7 5.4 3.8 2.4 4.1
Maria dos Santos 7.7 4.8 5.5 9.1 6.8
Mario Valdivino 3.4 3.4 9.5 7.2 5.9
Pedro Antunes 7.3 10.0 9.4 9.5 9.0
Roberto Diniz 6.4 4.2 5.2 9.6 6.4
Romana Peres 7.9 2.1 2.1 8.4 5.1
MEDIA BIMESTRAL 6.0 6.2 6.5 6.2

CASO 2: Este exemplo trata de inspecionar o conteúdo de um arquivo texto. Um


relatório será gerado contendo cada caractere do aquivo de entrada e seu respectivo

Introdução à Programação: Conceitos e Práticas. Página 326


código . Por exemplo, se for obtido o caractere , então será gravado no
arquivo relatório a seguinte informação: "a 61", onde em hexadecimal equivale
ao em decimal. Assim será feito para cada caractere do arquivo de entrada. Caso
o caractere seja algum caractere de controle , o símbolo deverá
ser utilizado para representá-lo.

Segue uma proposta de solução:

#include <stdio.h>

int ascii (char *fnsrc, char *fndst, int width) {


FILE *fsrc, *fdst;
int i, c;

if ((fsrc = fopen (fnsrc, "r")) == NULL) return 1;


if ((fdst = fopen (fndst, "w")) == NULL) return 2;

for (i = 1; (c = fgetc (fsrc)) != EOF; i++) {


fprintf (fdst, " %c %02X |", (c < 32) ? '*' : c, (unsigned char) c);
if (i % width == 0) fprintf (fdst, "\n");
}

fclose (fsrc);
fclose (fdst);
return 0;
}

int main () {
char fnsrc[255], fndst[255];
int code;
printf ("Entre com o nome do arquivo de entrada:\n");
gets (fnsrc);
printf ("Entre com o nome do arquivo de saída:\n");
gets (fndst);
if (code = ascii (fnsrc, fndst,8))
printf ("Erro: %d\n", code);

A função faz as leituras de duas strings contendo os nomes dos arquivos de


entrada e de saída. Em seguida, a função é chamada com estas strings como
argumentos e a quantidade de caracteres por linha. O arquivo de saída é criado com
o relatório pedido. Caso tenha havido algum problema com os arquivos a função
retornará um valor diferente de zero. Neste caso o código do erro é
apresentado na tela. Segue a função :

int ascii (char *fnsrc, char *fndst, int width) {


FILE *fsrc, *fdst;
int i, c;

if ((fsrc = fopen (fnsrc, "r")) == NULL) return 1;


if ((fdst = fopen (fndst, "w")) == NULL) return 2;

for (i = 1; (c = fgetc (fsrc)) != EOF; i++) {


fprintf (fdst, " %c %02X |", (c < 32) ? '*' : c, (unsigned char) c);

Introdução à Programação: Conceitos e Práticas. Página 327


if (i % width == 0) fprintf (fdst, "\n");
}

A função define duas variáveis para manipulação de arquivos


, e duas variáveis de apoio.

Em seguida, é usada a função para abrir o aquivo de entrada vinculando o


nome dado pela string com a variável . Em uma mesma expressão é feita
a atribuição para e o teste para certificar que seu valor é diferente de ,
significando que a abertura do arquivo foi bem sucedida. O modo de abertura para
este arquivo é definindo que o arquivo deve existir e que as operações serão
apenas para leitura. Em caso de insucesso, a função é encerrada com o código de
erro .

Um código semelhante foi empregado para o arquivo de relatório. O modo de


abertura para este arquivo é definindo que o arquivo deve ser criado, e no caso
de já existir, recriado, e que as operações serão apenas para escrita. Em caso de
insucesso, a função é encerrada com o código de erro .

Com os arquivos disponíveis para as operações indicadas, então temos a repetição


que combina um contador de com a expressão de teste
( ) Primeiramente é avaliada a expressão
que lê o próximo caractere do arquivo vinculado a variável . Em seguida é
testado se o resultado da leitura é o valor , significando que o indicador de
posição deste arquivo se encontra sobre o fim de arquivo.

O indentificador está definido na com o valor , tornando a


expressão mais legível. Assim, a repetição será encerrada ao alcançar o fim do
arquivo de entrada. O poderia ser usado para este propósito, e a expressão
seria ( )

Interno ao , tem-se o processamento do caractere lido:

O formato é combinado com os valores das expressões


e , gerando a string que será gravada no arquivo texto. O operador
ternário retorna o símbolo ou o próprio valor de , dependendo se o valor
numérico do caractere seja inferior a . Este valor está associado ao formato
, cuja apresentação será o símbolo. A próxima expressão
resulta no valor de redefinido como , que combinado com o formato
irá produzir uma string com o valor hexadecimal de , usando os símbolos
maiúsculo para os dígitos de a .

Na sequência, a instrução avalia se uma


nova linha deve ser iniciada, gravando o no arquivo de destino.

Introdução à Programação: Conceitos e Práticas. Página 328


Ao executar o programa teremos a seguinte interface:

Entre com o nome do arquivo de entrada:


teste.txt
Entre com o nome do arquivo de saída:
ascii.txt

Supondo um arquivo de entrada com o seguinte conteúdo:

Lower: abcd..z
Upper: ABCD..Z
Digits: 012..9
TAB < >
<<FIM>>

O arquivo de saída gerado terá o seguinte conteúdo:

L 4C | o 6F | w 77 | e 65 | r 72 | : 3A | 20 | a 61 |
b 62 | c 63 | d 64 | . 2E | . 2E | z 7A | * 0A | U 55 |
p 70 | p 70 | e 65 | r 72 | : 3A | 20 | A 41 | B 42 |
C 43 | D 44 | . 2E | . 2E | Z 5A | * 0A | D 44 | i 69 |
g 67 | i 69 | t 74 | s 73 | : 3A | 20 | 0 30 | 1 31 |
2 32 | . 2E | . 2E | 9 39 | * 0A | T 54 | A 41 | B 42 |
20 | < 3C | * 09 | > 3E | * 0A | < 3C | < 3C | F 46 |
I 49 | M 4D | > 3E | > 3E |

Para cada caractere lido do arquivo de entrada foi gerada uma informação no
arquivo de saída com o seu símbolo e valor numérico. Os caracteres de controle,
cujos valores estão entre e foram representados pelo símbolo . As linhas
contém informações de oito caracteres, conforme o valor passado para o argumento
da função . A interpretação do arquivo de saída permite observamos
alguns detalhes da organização de um arquivo texto. Podemos constatar que os
caracteres são armazenados como bytes, cujo valor numérico é o convencionado
pela tabela . Por exemplo, o caractere é armazenado como byte de valor
em hexa, que é em decimal. Os caracteres representados pelo símbolo são os
caracteres de controle.

Podemos buscar no arquivo de saída pelo primeiro e veremos que o valor


numérico de fato é , que equivale ao decimal. Este valor foi gerado quando
teclado no momento da digitação do texto usando um editor. É o mesmo
valor gerado para o . Se seguirmos na inspeção, certificaremos que haverão
tantos , quantos forem os saltos de linha dados na edição do arquivo de entrada.
Podemos verificar também que há um associado ao número . Este valor foi
gerado a partir da quarta linha do arquivo de entrada, onde entre os símbolos foi
pressionada a tecla . Sabemos desta forma, que a tecla tem valor na
tabela , configurando-se então como caractere de controle.

Introdução à Programação: Conceitos e Práticas. Página 329


25.2. Arquivo Binário

Os bytes de um arquivo texto representam informações formatadas como string ou


texto. As funções e utilizadas para ler e escrever dados em arquivo
utilizam a string de formato para orientar a conversão dos dados. Por exemplo, a
instrução , sendo um com valor , produzirá a string
– e gravará no arquivo estes caracteres. Porém, sabemos que a variável
representa um endereço de memória a partir da qual estão bits que codificam o
seu valor em . O valor binário em do
equivale à , que na memória o byte menos significativo fica no endereço
mais baixo, como segue:

18
FC
FF
FF

Dizemos que estes quatro bytes ou


associados a são a representação binária ou representação bruta do inteiro com
sinal. Os cinco bytes da string – ,(– ) ou ,
representam a forma textual do número.

Um arquivo binário contém bytes que representam valores brutos de alguma


informação. Normalmente, o seu conteúdo é gerado e compreendido por algum
programa específico. No caso de um arquivo binário com dados, provavelmente as
informações foram gravadas diretamente da memória. Entre a memória e o arquivo,
a informação não precisou ser convertida para nenhum formato. No caso do arquivo
texto, a informação ( , , , , ...) é convertida para string, para
então ser gravada no arquivo.

Enquanto que as funções e se aplicam a leitura e gravação de


arquivos textos, para arquivos binários são utilizadas as funções e .
Seguem alguns exemplos:

CASO 1: Elaborar um programa que leia um vetor de inteiros a partir da entrada


padrão (teclado) e cria um arquivo texto e um arquivo binário com o conteúdo do
vetor.

Segue uma proposta de solução:

#include <stdio.h>

void ler (int *V, int *N) {


int i;

for (i = 0; scanf ("%d", V) != EOF; i++, V++)


;

*N = i;
}

Introdução à Programação: Conceitos e Práticas. Página 330


int to_txt (char *fn, int *V, int N) {
FILE *fp;
int i;

if ((fp = fopen (fn, "w")) == NULL) return 1;

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


fprintf (fp, "%d\n", *V++);

fclose (fp);
return 0;
}

int to_bin (char *fn, int *V, int N) {


FILE *fp;

if ((fp = fopen (fn, "wb")) == NULL) return 1;

fwrite (V, sizeof(int), N, fp);

fclose (fp);
return 0;
}

int main () {
int v[100], n;

ler (v, &n);


to_txt ("vetor.txt", v, n);
to_bin ("vetor.bin", v, n);
}

A função declara o vetor de inteiros e a variável de controle da quantidade


de células ocupadas no contexto da execução. A função preenche este vetor
com dados informados via teclado. Em seguida, o conteúdo deste vetor é gravado
no formato arquivo texto ( ) e no formato arquivo binário ( ).

A função obtém uma sequência de números inteiros e finaliza quando o usuário


pressionar que aparecerá na tela como . Em seguida, a função
grava o conteúdo do vetor em um arquivo texto:

int to_txt (char *fn, int *V, int N) {


FILE *fp;
int i;

if ((fp = fopen (fn, "w")) == NULL) return 1;

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


fprintf (fp, "%d\n", *V++);

fclose (fp);
return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 331


Os elementos presentes nesta função já foram explanados em exemplos anteriores.
Na sequência, a função grava o conteúdo do vetor em um arquivo binário:

int to_bin (char *fn, int *V, int N) {


FILE *fp;

if ((fp = fopen (fn, "wb")) == NULL) return 1;

fwrite (V, sizeof(int), N, fp);

fclose (fp);
return 0;
}

Observamos que a maioria dos recursos utilizados na manipulação de arquivo texto,


também está presente para arquivo binário. A primeira diferença está no modo de
criação do arquivo, que para o caso binário foi usada a string , onde o indica
operação de criação de um novo arquivo para operação de escrita e o para
caracterizar como binário. A segunda diferença está no uso da função no
lugar da .

Segue a sintaxe da função e o diagrama que ilustra seus parâmetros:

ptr

size

blocks
...

A função grava no arquivo indicado no parâmetro a quantidade de


blocos de memória indicada em , sendo cada elemento de tamanho definido
por . A função retorna a quantidade de blocos escritos. Desta forma, a
gravação do vetor no arquivo binário demandou apenas uma linha:
que faz o para o arquivo dos de
memória a partir do endereço indicado por , considerando blocos, sendo cada
bloco de tamanho .

Também é possível gravar um bloco de inteiro por vez. A lógica ficaria:

for (i = 0; i < N; i++, V++)


fwrite (V, sizeof(int), 1, fp);

Introdução à Programação: Conceitos e Práticas. Página 332


CASO 2: Elaborar um programa que carregue o conteúdo do arquivo binário gerado
no caso anterior e apresente seu conteúdo na tela.

Segue uma proposta de solução:

#include <stdio.h>
#define MAX 100

void show (int *V, int N) {


int i;

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


printf ("%d\n", *V++);

int load (char *fn, int *V, int *N) {


FILE *fp;

if ((fp = fopen (fn, "rb")) == NULL) return 1;

*N = fread (V, sizeof(int), MAX, fp);

fclose (fp);
return 0;
}

int main () {
int v[MAX], n;

load ("vetor.bin", v, &n);


show (v, n);
}

A função declara o vetor de inteiros e a variável de controle da quantidade


de células ocupadas no contexto da execução. O tamanho do vetor é definido como
. A função preenche este vetor com dados lidos diretamente de um arquivo
binário para a memória. Em seguida, o conteúdo deste vetor é apresentado na tela
( ). A função tem a seguinte lógica:

int load (char *fn, int *V, int *N) {


FILE *fp;

if ((fp = fopen (fn, "rb")) == NULL) return 1;

*N = fread (V, sizeof(int), MAX, fp);

fclose (fp);
return 0;
}

O arquivo com o nome indicado por é aberto usando o modo , onde o


indica abertura do arquivo para operação de leitura e o para caracterizar como

Introdução à Programação: Conceitos e Práticas. Página 333


binário. Em seguida é usada a função para efetuar a leitura:
. A sintaxe é é:

Os parâmetros da função tem significado equivalente ao explanado para o


, sendo a diferença no sentido dos dados, que agora é do arquivo para a
memória. A função retorna a quantidade de blocos lidos. Caso a expressão
solicite mais blocos do que os disponíveis no arquivo, o resultado da função serve
como quantitivo de blocos efetivamente lidos. Neste caso, esta informação será a
quantidade de inteiros lidos.

Os valores informados no deverão ser impressos na tela pelo .

CASO 3: Elaborar um programa que crie um arquivo binário a partir de alguma


estrutura de memória previamente preenchida. Em seguida, o programa deverá
alterar algum registro diretamente no arquivo e carregar os dados deste arquivo para
a estrutura de memória. Apresentar os dados na tela para as devidas
comprovações.

Segue uma proposta de solução:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define MAX 100

typedef struct {
int t;
float a;
char c;
} TDADO;

void preencher (TDADO *p, int seq) {


p->t = seq;
p->a = exp(seq);
p->c = 'a' + seq;

void fill (TDADO V[], int N) {


int i;
for (i = 0; i < N; i++)
preencher (&V[i], i);

void show (TDADO V[], int N) {


int i;
for (i = 0; i < N; i++)
printf ("%d\t%9.3f\t%c\n", V[i].t, V[i].a, V[i].c);

int save (char *fn, TDADO *V, int N) {

Introdução à Programação: Conceitos e Práticas. Página 334


FILE *fp;
fp = fopen (fn, "wb+");
if (fp == NULL) return 1;

fwrite (V, sizeof(TDADO), N, fp);

fclose (fp);
return 0;
}

int load (char *fn, TDADO *V, int *N) {


FILE *fp;
fp = fopen (fn, "rb+");
if (fp == NULL) return 1;

*N = fread (V, sizeof(TDADO), MAX, fp);


printf ("\n%d\n", *N);

fclose (fp);
return 0;
}

int replace (char *fn, int X, TDADO *R) {


FILE *fp;
fp = fopen (fn, "rb+");
if (fp == NULL) return 1;

fseek (fp, X*sizeof(TDADO), SEEK_SET);


fwrite (R, sizeof(TDADO), 1, fp);

fclose (fp);
return 0;
}

int main (int argc, char *argv[]) {


TDADO v[MAX]; TDADO rec = {.t = -1, .a = 3.14, 'x'};
int n = 5;

fill (v, n);


printf ("DADOS ORIGINAIS\n");
show (v, n);
save ("arq.dat", v, n);

replace ("arq.dat", 3, &rec);

load ("arq.dat", v, &n);


printf ("DADOS ALTERADOS\n");
show (v, n);
return 0;
}

A função declara o vetor de elementos do tipo e a variável de


controle da quantidade de células utilizadas no contexto da execução. O tamanho
limite do vetor é definido como . A função inicializa este vetor. Em seguida,
o conteúdo deste vetor é apresentado na tela ( ). A função altera um
determinado registro do arquivo binário. A função carrega os dados do arquivo
para a estrutura de memória indicada.

Introdução à Programação: Conceitos e Práticas. Página 335


A função tem a seguinte lógica:

int replace (char *fn, int X, TDADO *R) {


FILE *fp;
fp = fopen (fn, "rb+");
if (fp == NULL) return 1;

fseek (fp, X*sizeof(TDADO), SEEK_SET);


fwrite (R, sizeof(TDADO), 1, fp);

fclose (fp);
return 0;
}

A função realiza a seguinte chamada: . O primeiro


parâmetro é o nome do arquivo a ser modificado. O segundo parâmetro indica o
número do registro a ser alterado e o terceiro parâmetro é uma referência à estrutura
que contém a nova informação a ser gravada na posição indicada.

O arquivo informado é aberto com os atributos para as operações de leitura e


gravação em arquivo binário. Em seguida, o indicador de posição do arquivo é
movido para o início do bloco que se deseja substituir:
. A função tem a seguinte sintaxe:

Onde os parâmetros têm o seguinte significado:


 – É o ponteiro para vinculado ao arquivo em questão.
 – Quantidade de bytes a serem saltados a partir de um ponto de
referência.
 – É um inteiro que informa qual a referência para o . Pode ser um
deslocamento a partir do início do arquivo ( ), final do arquivo
( ) ou da posição corrente ( ). Estas constantes
estão definidas em .
A função retorna zero no caso de sucesso ou um valor diferente de zero.
Após mover o indicador de arquivo para o início do bloco a ser substituído, um novo
bloco é sobreescrito: . A função gravará no
arquivo o conteúdo do bloco localizado no endereço indicado por cujo tamanho é
.
Ao executar o programa teremos a seguinte saída:

DADOS ORIGINAIS
0 1.000 a
1 2.718 b
2 7.389 c
3 20.086 d
4 54.598 e

Introdução à Programação: Conceitos e Práticas. Página 336


5
DADOS ALTERADOS
0 1.000 a
1 2.718 b
2 7.389 c
-1 3.140 x
4 54.598 e

CASO 4: Elaborar um programa que apresente o tamanho em bytes de um arquivo.

Segue uma proposta de solução:

#include <stdio.h>
#include <sys/stat.h>

int sizeoffile (char *fn) {


struct stat recfile;
stat(fn, &recfile);
return recfile.st_size;
}

int filesize (char *fn) {


FILE *fp;
int size;
if ((fp = fopen (fn, "r")) == NULL) return 0;

fseek(fp, 0, SEEK_END);
size = ftell(fp);
fclose (fp);
return size;
}

int main () {
char fn[255];
printf ("Entre com o nome do arquivo\n");
gets (fn);
printf ("filesize(\"%s\") = %d\n", fn, filesize (fn));
printf ("filesize(\"%s\") = %d\n", fn, sizeoffile (fn));
}

Fica a cargo leitor a compreensão dos recursos utilizados. Ao executar o programa


teremos a seguinte saída:

Entre com o nome do arquivo


filesize.c
filesize("filesize.c") = 554
filesize("filesize.c") = 554

Introdução à Programação: Conceitos e Práticas. Página 337


26. Enumerados

O tipo também é classificado como e , e é definido como


uma relação de valores representados por identificadores. Desta forma, o
procedimento normal é criar um tipo enumerado associando um nome a uma relação
de identificadores, sendo que cada identificador representa um valor que é a ordem
do identificador no conjunto de enumerados.

Segue um programa que exemplifica o uso de :

#include <stdio.h>
#include <string.h>

enum TDia {DOM, SEG, TER, QUA, QUI, SEX, SAB};

typedef enum TDia TDIA;

char DIAS[7][4] = {"DOM", "SEG", "TER", "QUA", "QUI", "SEX", "SAB"};

char *diatostr (char *dst, TDIA dia) {


strcpy (dst, DIAS[dia]);
return dst
}

TDIA strtodia (char *str) {


TDIA dia;
for (dia = DOM; dia <= SAB; dia++)
if (strcmp (str, DIAS[dia]) == 0) return dia;

return -1;
}

int main () {
TDIA d; char s[10];
d = QUA;
printf ("%s\n", diatostr(s, d));
d++;
printf ("%s\n", diatostr(s, d));
d-=2;
printf ("%s\n", diatostr(s, d));
d = strtodia (strcpy(s, "QAU"));
printf ("#%s = %d\n", s, d);
}

Neste exemplo são apresentados diversos aspectos associados a enumerados. O


problema em questão é a conversão de um valor enumerado representando o dia da
semana em uma e vice-versa. Os seguintes comentários explicam os
principais elementos do programa:

a. Criar o tipo que relaciona um identificador para cada dia da semana:

enum TDia {DOM, SEG, TER, QUA, QUI, SEX, SAB};


typedef enum TDia TDIA;

Introdução à Programação: Conceitos e Práticas. Página 338


Foram enumerados os dias da semana e associados ao novo tipo . É
fundamental compreender que os identificadores não são strings. Os
elementos do enumerados equivalem ao número inteiro associado à posição
do identificador no enumerado. Assim, o identificador está vinculado ao
inteiro , ao inteiro e assim por diante. O mesmo efeito poderia ser
obtido por meio de uma macro para cada dia da semana com a seguinte
forma , assim sucessivamente.

Foi utilizado o para simplificar a declaração de variáveis e


parâmetros. O tipo também pode ser referenciado como .

b. Criar a variável que serve de base para a conversão de uma variável do tipo
para um texto :

char DIAS[7][4] = {"DOM", "SEG", "TER", "QUA",


"QUI", "SEX", "SAB"};

A variável é um array de strings, onde na posição ( ) está contida


a string , e assim até a posição ( ) com a string equivalente. Este
serve para mapear um índice para uma , e vice-versa.

c. Criar a função que mapeia um valor em uma equivalente:

char *diatostr (char *dst, TDIA dia) {


strcpy (dst, DIAS[dia]);
return dst;
}

A função copia para o parâmetro a string equivalente ao valor do


parâmetro , e retorna o endereço de .

d. Criar a função que mapeia um valor em um valor :

TDIA strtodia (char *str) {


TDIA dia;
for (dia = DOM; dia <= SAB; dia++)
if (strcmp (str, DIAS[dia]) == 0) return dia;

return -1;
}

A função implementa o mapeamento de uma contendo três


letras que designam o dia da semana para um valor do tipo . É
importante reforçar que um enumerado tem todas as prerrogativas de um .
Uma variável do tipo pode ser trabalhada como .

A repetição percorre cada linha do vetor buscando por uma


string que coincida com o parâmetro . Ao encontrar, o valor da variável de
controle será retornado. No caso de não haver encontrado o texto contido em
, a função retorna .

Introdução à Programação: Conceitos e Práticas. Página 339


e. Criar o programa principal para testar a implementação:

int main () {
TDIA d; char s[10];
d = QUA;
printf ("%s\n", diatostr(s, d));
d++;
printf ("%s\n", diatostr(s, d));
d-=2;
printf ("%s\n", diatostr(s, d));
d = strtodia (strcpy(s, "QAU"));
printf ("#%s = %d\n", s, d);
}

A legibilidade é um dos efeitos positivos do uso de enumerados, pois auxilia


no entendimento do programa. A atribuição é mais facilmente
compreendida do que , e sem perda de eficiência, pois na compilação
as duas formas são equivalentes.

O tipo é uma abstração de um inteiro, podendo ser aplicados os


mesmos operadores. Desta forma, as expressões e –– alteram em uma
unidade o valor da variável .

Na linha de código ( ) a string é copiada


para a string e passada como parâmetro para a função O
resultado será , pois não há string equivalente a na variável .A
impressão dos resultados confirma a expectativa.

O usuário é responsável em assegurar o domínio adequado para uma


variável do tipo enumerado. A atribuição direta de um valor inteiro que não
possui equivalente no enumerado pode incorrer em erro de execução.

A execução deste programa principal produz a saída indicada na sequência:

QUA
QUI
TER
#QAU = -1

Caso o programador queira vincular valores inteiros distintos dos valores padrões
para os indentificadores do enumerado, isto pode ser feito da seguinte
forma:

enum TDia {DOM, SEG, TER=10, QUA, QUI=15, SEX=20, SAB};

Neste caso os identificadores assumirão os valores { }. A lógica


construída neste exemplo não se aplicaria nesta nova definição. É importante
destacar que um mesmo identificador não pode fazer parte de distintos enumerados.

Introdução à Programação: Conceitos e Práticas. Página 340


27. Expressões Binárias

As expressões binárias são recursos da linguagem que permitem manipular os


da representação de um número inteiro. Algumas aplicações que envolvem
aquisição de dados, programação em baixo nível para sistemas operacionais,
drivers, protocolos de comunicação, criptografia e computação gráfica exigem
frequentemente lógicas que operem diretamente sobre os bits de um número. Além
disso, determinadas operações matemáticas envolvendo multiplicação ou divisão
são mais eficientemente implementadas utilizando operações binárias
( ).

Na sequência são apresentados os operadores binários e de deslocamento de bits.

27.1. Operadores Binários

Os operadores binários são equivalentes aos operadores lógicos, porém aplicáveis


aos da representação de um inteiro. Os operadores e as tabelas
verdade são as mesmas apresentadas anteriormente, que em resumo são:

AND 0 1 OR 0 1 XOR 0 1 NOT 0 1


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

AND OR XOR NOT


A B X A B X A B X A X
0 0 0 0 0 0 0 0 0 0 1
0 1 0 0 1 1 0 1 1 1 0
1 0 0 1 0 1 1 0 1
1 1 1 1 1 1 1 1 0

Observar que os símbolos dos operadores binários diferem dos símbolos dos
operadores lógicos. Abaixo das tabelas estão indicadas as formas de uso dos
operadores , , e , bem como os símbolos utilizados O seguinte
quadro ilustra uma expressão com o operador e o efeito nos de um inteiro de
8 bits:

Binário Hexa Decimal


1 1 1 0 0 1 1 0 E6 230
0 0 1 1 1 0 1 1 3B 59
0 0 1 0 0 0 1 0 22 34

Introdução à Programação: Conceitos e Práticas. Página 341


A expressão com e produz um resultado em que é a
aplicação da operação bit a bit na representação binária de e . Neste caso,
considera-se que e foram declarados como que equivale a
inteiro sem sinal com .

Este outro exemplo ilustra a operação envolvendo operandos do tipo , que


equivale a inteiro com sinal com :

Binário Hexa Decimal


1 1 1 1 1 1 1 1 FF 255
0 0 1 1 1 0 1 1 3B 59
0 0 1 1 1 0 1 0 2A 42

Os inteiros com sinal são codificados em binário utilizando a notação em


. Logo, ao atribuir o valor para a variável a codificação
binária resultante é .

Uma análise na tabela verdade destaca que o operador é adequado para zerar
( ) determinados bits de uma palavra. O operador é aplicado nos casos em
que se deseja ligar ( ) bits de uma palavra. O operador é mais indicado
quando é necessário inverter ( ) determinados , enquanto que o operador
é útil para inverter todos os bits de uma representação.

27.2. Operadores de Deslocamento de Bits

A manipulação de se completa com os operadores que executam a


movimentação dos bits para a esquerda ou para a direita.

O operador ( )desloca os bits de um inteiro para a esquerda,


acrescentando zeros a direita. O seguinte diagrama ilustra esta operação para o
deslocamento de a esquerda em um inteiro de .

Tabela 2: Operador Shift Left

Introdução à Programação: Conceitos e Práticas. Página 342


O efeito numérico que a operação produz em um inteiro sem sinal é equivalente a
operação matemática de multiplicação por , onde é o número de
deslocados. Se e , teremos então:

O valor de é que equivale ao decimal , ou ao valor de


. Os compiladores procuram otimizar o código, e sempre que possível
traduzem expressões do tipo por .

Um segundo operador é o ( ),que desloca os bits de um inteiro sem


sinal para a direita, acrescentando zeros à esquerda. O seguinte diagrama ilustra
esta operação para o deslocamento de a direita em um inteiro de .

O efeito numérico que a operação produz em um inteiro sem sinal é equivalente a


operação matemática de divisão de inteiro por , onde é o número de bits
deslocados. Se e , teremos então:

O valor de é que equivale ao decimal , ou ao valor de


. Os compiladores procuram otimizar o código, e sempre que possível
traduzem expressões do tipo por . Observar que os
perdidos equivalem matematicamente ao valor de .

No caso de inteiro com sinal, o operador ( ), desloca os bits para a


direita, completando a esquerda com valor do bit de sinal. O seguinte diagrama

Introdução à Programação: Conceitos e Práticas. Página 343


ilustra esta operação para o deslocamento de a direita em um inteiro com sinal
de .

Na sequência serão apresentados exemplos que ilustram as aplicações com


operações binárias.

27.3. Exemplos

i. Elaborar um programa que efetue as seguintes lógicas11:

a. Ligar o da posição de um inteiro .

b. Zerar o da posição de um inteiro .

c. Substituir o da posição de um inteiro pelo menos significativo de .

d. Inverter o valor do da posição de um inteiro .

 ̅̅̅

e. Testar se o da posição de um inteiro está ligado.

f. Girar os de , posições para a esquerda.

g. Girar os de , posições para a direita.

11
http://bits.stephan-brumme.com/

Introdução à Programação: Conceitos e Práticas. Página 344


Segue o programa proposto:

#include <stdio.h>
#include <string.h>

char *tobin (char *dst, int x, int n) {


unsigned y = x;
dst[n] = '\0';
while (n) {
dst[--n] = '0' + (y & 1);
y >>= 1;
}
return dst;
}

int setbit (int x, int t) {


int mask;
mask = 1 << t;
return x | mask;
}

int clrbit (int x, int t) {


int mask;
mask = 1 << t;
return x & (~mask);
}

int chgbit (int x, int t, int r) {


int mask;
mask = 1 << t;
return (x & (~mask)) | ((r << t) & mask);
}

int flipbit (int x, int t) {


int mask;
mask = 1 << t;
return x ^ mask;
}

int testbit (int x, int t) {


x >>= t;
return x & 1;
}

int rol (int x, int t) {


int n = (sizeof(x) << 3) - t;
return (x << t) | ((unsigned) x >> n);
}

int ror (int x, int t) {


int n = (sizeof(x) << 3) - t;
return (x << n) | ((unsigned) x >> t);
}

int main () {
char str[100];
int x = 0;
printf ("%s\n", tobin (str, x, 32));

Introdução à Programação: Conceitos e Práticas. Página 345


x = setbit (x, 3);
x = setbit (x, 5);
x = setbit (x, 31);
printf ("%s\n", tobin (str, x, 32));

x = clrbit (x, 5);


printf ("%s\n", tobin (str, x, 32));

x = chgbit (x, 5, 1);


x = chgbit (x, 6, 1);
printf ("%s\n", tobin (str, x, 32));

x = flipbit (x, 31);


printf ("%s\n", tobin (str, x, 32));

x = flipbit (x, 31);


printf ("%s\n", tobin (str, x, 32));

x = rol (x, 5);


printf ("%s\n", tobin (str, x, 32));

x = ror (x, 5);


printf ("%s\n", tobin (str, x, 32));
}

A fim de visualizar os efeitos das operações sobre os de um inteiro optou-se em


mostrar na tela o equivalente binário da variável utilizada como teste. A função que
converte um inteiro para binário foi adaptada em relação a versão já apresentada
anteriormente. A seguinte tabela compara as duas versões:

int numdigit (int x, int base) {


int i;
for (i = 0; x; i++, x/=base)
;

return i;
}

void tobin (char s[], int num) { char *tobin (char *dst, int x, int n) {
int i, n; unsigned y = x;
dst[n] = '\0';
n = numdigit (num, 2); while (n) {
s[n] = '\0'; dst[--n] = '0' + (y & 1);
y >>= 1;
for (i = n-1; num; i--, num /= 2) }
s[i] = '0' + num%2; return dst;
} }

A versão implementada para este exemplo necessita três parâmetros, sendo


referência a string onde será armazenado o resultado, o inteiro a ser convertido e
a quantidade de dígitos na formatação da resultado. Esta formatação é
recomendada, pois facilita a visualização e comparação dos .

A primeira diferença que se observa é que esta versão não utiliza os operadores e
para obter os dígitos binários. A lógica empregada isola o menos
significativo através da expressão , onde é uma versão sem sinal de . A
constante deve ser vista na forma representada em binário com tamanho estendido

Introdução à Programação: Conceitos e Práticas. Página 346


para o mesmo tamanho de . Assim, o resultado desta expressão será ou
. Após o tratamento do menos significativo, a linha atualiza o
valor de com o deslocamento de todos os uma posição a direita, cujo efeito é a
eliminação do menos significativo. Esta lógica é repetida enquanto o valor de é
diferente de zero. A expressão retorna o caractere equivalente ao valor
do bit, resultando em ou .

Na sequência são comentadas algumas das funções de manipulação de bits:

a. Ligar o da posição de um inteiro .

Para ligar um determinado bit de um inteiro faz-se uso de uma propriedade do


operador em que , para qualquer valor do bit e . Assim,
deve ser montada uma máscara que tenha na posição e nas demais. A
operação desta máscara com o número dado resulta no bit desejado com valor
e os demais inalterados. Segue o código da função:

int setbit (int x, int t) {


int mask;
mask = 1 << t;
return x | mask;
}

A máscara com o bit na posição é obtida efetuando um deslocamento à


esquerda de posições no inteiro . Como sabemos que o decimal 1 é
representado em binário e estendido para equiparar ao tamanho da variável
destino , então a operação produzirá a máscara desejada. Em
seguida, a expressão resultará em um valor que é um inteiro com todos
os bits iguais aos bits de com exceção do bit da posição que será
forçosamente .

As funções e seguem o mesmo princípio.

c. Substituir o da posição de um inteiro pelo bit menos significativo de .

Neste problema, tem-se um com seus bits , um com seus


bits e um número . A combinação destes dados produzirá
, onde o bit de deverá ser substituído pelo bit de .

A seguinte rotina codifica esta lógica:

int chgbit (int x, int t, int r) {


int mask;
mask = 1 << t;
return (x & (~mask)) | ((r << t) & mask);
}

O quadro que segue ilustra o passo a passo das principais transformações feitas
com os bits bem como as combinações necessárias para produzir o resultado

Introdução à Programação: Conceitos e Práticas. Página 347


desejado. A leitura do quadro deve se dar de cima para o meio e de baixo para o
meio, onde então se posiciona a sequência de bits com o resultado da função.

f. Girar os de , posições para a esquerda.

Esta funcionalidade é muito comum na linguagem da maioria das


arquiteturas. Neste problema, tem-se um com seus bits
e um número que indica quantos bits devem girados
para a esquerda. Não há perda de bits, pois os bits que saem pela direita servem
de preenchimento pela esquerda. Assim, supondo , então a rotação de
posições produziria .

Ao desmembrar a sequência chega-se a duas


sequências abaixo indicadas que podem ser obtidas por deslocamento de bits e
unidas pela operação para produzir o giro solicitado:

Generalizando para quaisquer bits tem-se . Ainda é


possível parametrizar o número , uma vez que representa a quantidade de bits
do valor a ser rotacionado. A função fornece a quantidade de bytes
destinada para , que multiplicado por fornece a quantidade de bits. Esta
multiplicação por foi substituída pela operação que
matematicamente equivale a multiplicar por . Segue a codificação da função
:

int rol (int x, int t) {


int n = (sizeof(x) << 3) - t;
return (x << t) | ((unsigned) x >> n);
}

Observe que foi utilizado o de para , evitando que o


deslocamento fosse preenchido com o de sinal. A rotação para a direita tem
lógica equivalente.

Introdução à Programação: Conceitos e Práticas. Página 348


O quadro abaixo apresenta tanto a saída resultante da execução do programa
principal quanto os comentários passo a passo sobre as alterações sofridas pelos
bits:

00000000000000000000000000000000 x = 0;
10000000000000000000000000101000 set bits 3, 5 e 31
10000000000000000000000000001000 clear bit 5
10000000000000000000000001101000 change bits 5 e 6 para 1
00000000000000000000000001101000 flip bit 31
10000000000000000000000001101000 flip bit 31
00000000000000000000110100010000 rotate to the left 5 bits
10000000000000000000000001101000 rotate to the right 5 bits

Introdução à Programação: Conceitos e Práticas. Página 349


28. Estrutura de dados dinâmica

Em seções anteriores foram abordados o conceito e algumas aplicações de


ponteiros, porém sempre referenciando memória alocada pela linguagem. É
importante compreender a estrutura de memória de um programa em execução a fim
de entender adequadamente a manipulação de endereços e ponteiros, e
consequentemente visualizar o suporte computacional para as estruturas de dados
dinâmicas.

28.1. Organização da memória de um programa em execução

A compilação de um programa fonte (ver Figura 43: Processo de Tradução) produz um


módulo objeto com o seu conteúdo refletindo em forma binária as instruções
codificadas em linguagem de alto nível. O módulo objeto e demais módulos da
biblioteca são ligados para produzir o módulo executável final. Enfim, é possível
processar este arquivo executável no ambiente computacional gerenciado pelo
. Neste momento o arquivo executável é interpretado
pelo e carregado para a memória onde adquire uma nova estrutura, a saber:

a) As variáveis globais são agrupadas em uma área de memória denominada


. De forma geral neste segmento residem as variáveis
estáticas, aquelas que têm o seu espaço de memória reservado no momento
da carga do executável e ficam ligadas a esta memória até o fim do
programa. Em uma variável é considerada estática se for declarada no
escopo do programa não local a qualquer função, ou se declarada no escopo
de uma função com o qualificador . O ainda pode
ser organizado em duas áreas: para variáveis inicializadas e variáveis não
inicializadas.
b) Os bytes que representam as instruções codificadas ficam residentes em uma
área designada como . São estes que fluem
para a para serem interpretados e processados formando assim o ciclo
de execução das instruções.

c) Uma terceira região de memória, denominada é destinada


à suportar as chamadas de sub-programas (funções) acomodando de forma
temporária os dados necessários a execução destas rotinas, tais como:
parâmetros, endereço de retorno, variáveis locais (semi-dinâmicas), resultado
da função, link estático e link dinâmico. Este agrupamento de dados é
denominado de , e cada chamada de rotina
demanda o empilhamento de um . Assim, uma variável local fica ligada a
um espaço de memória apenas enquanto a rotina na qual ela foi declarada
estiver ativa. As variáveis locais à função recaem nesta categoria,
porém com um ciclo de vida que coincide com o ciclo do programa e das
variáveis estáticas.

d) O programa dispõe ainda de uma área extra de memória, chamada ,


destinada a alocação dinâmica de dados, onde o momento de reserva de

Introdução à Programação: Conceitos e Práticas. Página 350


espaço de memória e o momento de liberação deste espaço são definidos
pelo programador através de comandos explícitos para tais fins.
A seguinte figura ilustra a organização da memória:

Figura 83: Organização da Memória RAM.

28.2. Alocação dinâmica

A consiste na requisição de memória em tempo de execução de


forma explícita pelo programador através da inclusão de uma linha de código com a
instrução própria para este fim. De forma semelhante, a liberação desta memória
alocada deverá se dar por ordem explícita do programador. A área de memória
destinada para acomodar os dados dinâmicos é a .

A alocação de memória em é feita pela seguinte função:

O parâmetro é do tipo inteiro indicando a quantidade de memória em bytes que se


deseja alocar. A função solicita ao um bloco contínuo de memória com o
tamanho informado. Em caso de disponibilidade, um montante de memória é
reservado e o endereço do primeiro é retornado como resultado. Em caso de
falha de alocação é retornado.

int main () {
char *str; (1)
str = NULL; (2)

str = malloc (100); (3)

Introdução à Programação: Conceitos e Práticas. Página 351


scanf ("%[^\n]", str);
printf ("[%s]", str);

free (str); (4)


}

Este exemplo apresenta o conceito de alocação dinâmica de memória de forma bem


simples. É necessário dispor de uma variável do tipo ponteiro para armazenar um
endereço de memória. A declaração cria a variável do tipo ponteiro
para com capacidade para conter um endereço de memória. A expressão
inicializa com o valor , a fim de certificar-se de um conteúdo
conhecido. Em seguida, a expressão solicita ao um bloco de
de memória e armazena em o endereço do primeiro . Caso o
não contemple o pedido, um valor é retornado. A rigor, a utilização deste
ponteiro deveria vir precedido do teste para assegurar que o conteúdo seja um
endereço válido.

O seguinte diagrama ilustra um mapa de memória em três momentos distintos,


sendo o primeiro logo após a criação de , porém sem inicialização, o segundo
após a atribuição do valor à , e o terceiro após o :

VARIÁVEL NÃO VARIÁVEL INICIALIZADA VARIÁVEL INICIALIZADA HEAP


INICIALIZADA COM NULL COM malloc

str ??? str str

(1) (2) (3)

O valor inicial de é indefinido e o local apontado é incerto, e consequentemente o


uso de deve acarretar em erro de execução. O segundo cenário indica que
foi inicializado com . Neste momento, a referência a área apontada
também acarreta em erro. O terceiro cenário, após a alocação de memória com
efetiva a reserva de bytes e o endereço inicial deste bloco é
atribuído à . Assim, podemos afirmar que contém o endereço de um ,
tendo todas as prerrogativas de uma string, ou vetor de

A partir da alocação, o programador pode utilizar a variável:

scanf ("%[^\n]", str);


printf ("[%s]", str);

Neste caso, foi feita a leitura de uma string a partir do teclado e apresentado o seu
valor na tela. A partir do momento em que a área de memória não é mais
necessária, o programador pode explicitamente solicitar ao a liberação do bloco
alocado: . Segue a sintaxe desta função:

O endereço fornecido pela função deve ser passado para à função


quando efetuar o pedido de liberação.

Introdução à Programação: Conceitos e Práticas. Página 352


Ainda neste exemplo, podemos ajustar a quantidade de memória alocada para a
quantidade exata de bytes utilizada. Assim, podemos solicitar para que o bloco
alocado seja realocado em um novo endereço com um novo tamanho. A seguinte
função é utilizada para este propósito:

A função requer um parâmetro com o endereço do bloco e um outro


parâmetro com o novo tamanho. O resultado da função é o endereço da nova área
de memória. Neste exemplo, podemos incluir a seguinte instrução logo após o
:

str = realloc (str, strlen (str) + 1);

Após a inicialziação da , o seu tamanho pode ser determinado pela função


. Desta forma, a pode ser realocada em uma nova área com o tamanho
exato, definido pela quantidade de caracteres acrescido de byte para o

28.3. Exemplos

i. Elaborar um programa que carregue o conteúdo de um arquivo texto para a


memória. Cada linha do arquivo forma uma string. A estrutura de memória deverá
suportar até strings de no máximo caracteres cada. Além disso, a estrutura
final deverá ocupar uma quantidade justa de memória. Um arquivo de saída deverá
ser gerado a partir da estrutura de memória, a fim de assegurar a correção da lógica.
Após completado o processamento, a memória alocada deverá ser liberada.

Sem o recurso de alocação dinâmica, o conteúdo do arquivo seria acomodado em


um vetor de strings ou matriz de Seria necessário definir as quantidades
máximas de linhas e de colunas, e tão logo o programa fosse executado, o
reservaria um bloco de memória para acomodar a matriz inteira. Neste exemplo, com
os limites estabelecidos, a matriz ocuparia bytes, independente da
quantidade de strings carregadas do arquivo:

int main () {
char m[1000][100]; int n;
load ("dados.txt", m, &n);
...
}

Mesmo a variável sendo local a , o seu tempo vida coincide com o tempo de
vida do programa, uma vez que a função principal permanece na memória do início
ao fim do programa.

Para um caso, onde o arquivo contivesse três linhas com as strings , e ,


teríamos a seguinte alocação de memória:

Introdução à Programação: Conceitos e Práticas. Página 353


0 1 2 3 98 99
m[0] a \0
m[1] b \0
m[2] c \0
m[3]
m[4]

m[997]
m[998]
m[999]

n 3

É facilmente observado um grande desperdício de memória. Uma solução seria


solicitar memória ao conforme a demanda. Desta forma, podemos alterar a
organização dos dados para algo como segue:

HEAP

m[0] a \0
m[1]
m[2] b \0
m[3]
m[4] c \0

m[997]
m[998]
m[999]

n 3

Nesta versão, a variável é definida como um vetor de ponteiros para A


medida que for necessário, aloca-se uma nova string na área dinâmica, mantendo na
área fixa apenas o endereço em que a string foi acomodada. Houve uma redução
considerável no desperdício de memória. A nova estrutura de dados teria a seguinte
definição:

int main () {
char *m[1000]; int n;
load ("dados.txt", m, &n);
...
}

Segue o programa proposto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXL 1000
#define MAXC 100

Introdução à Programação: Conceitos e Práticas. Página 354


int save (char *fn, char *M[], int N) {
FILE *fp;
int i;
if ((fp = fopen (fn, "w")) == NULL) return 1;
for (i = 0; i < N; i++)
fprintf (fp, "%s\n", M[i]);

fclose (fp);
return 0;
}

int load (char *fn, char *M[], int *N) {


FILE *fp; char buf[MAXC];
int i;
if ((fp = fopen (fn, "r")) == NULL) return 1;
for (i = 0; fscanf (fp, "%[^\n]", buf) != EOF; i++) {
M[i] = malloc (strlen (buf) + 1);
strcpy (M[i], buf);
fscanf (fp, "%*c");
buf[0] = '\0';
}
*N = i;
fclose (fp);
return 0;
}

void liberar (char *M[], int N) {


int i;
for (i = 0; i < N; i++)
free (M[i]);
}

int main () {
char *m[MAXL];
int n;

load ("dados.txt", m, &n);


save ("saida.txt", m, n);
liberar (m, n);

return 0;
}

A variável é declarada no escopo da função como um vetor de ponteiros


para . Destacamos novamente a característica do recurso da linguagem que
permite interpretar um ponteiro para um de forma semelhante a um vetor de
.

A declaração [ ] faz com que na execução do programa seja reservada


memória suficiente para um vetor de endereços de memória. Assim, cada
célula de é capaz de armazenar um endereço de memória, que seria o endereço
de um determinado O conteúdo inicial das células deste vetor refere-se a
endereço inválido, até que uma alocação seja feita. A função é responsável em
alocar memória para uma string a medida que avança na leitura do arquivo,
conforme segue:

Introdução à Programação: Conceitos e Práticas. Página 355


int load (char *fn, char *M[], int *N) {
FILE *fp; char buf[MAXC];
int i;
if ((fp = fopen (fn, "r")) == NULL) return 1;
for (i = 0; fscanf (fp, "%[^\n]", buf) != EOF; i++) {
M[i] = malloc (strlen (buf) + 1);
strcpy (M[i], buf);
fscanf (fp, "%*c");
buf[0] = '\0';
}
*N = i;
fclose (fp);
return 0;
}

As linhas de cógido para manipulação de arquivo já são conhecidas. Cabe destacar


o uso do para realizar a leitura da string. O formato [ ] indica a
recuperação de toda a sequência de caracteres exclusive o ' . A função
também poderia ser utilizada para obtenção da string. A repetição ficaria:

for (i = 0; fgets (buf, MAXC, fp) != NULL; i++) {


M[i] = malloc (strlen (buf) + 1);
strcpy (M[i], buf);
fscanf (fp, "%*c");
buf[0] = '\0';
}

O vetor será carregado com todos os caracteres do arquivo indicado por


limitado pelo tamanho , fim de linha ou fim de arquivo. O caracter ' será
armazenado na string. A função retorna o endereço da string informada ou
no caso de impossibilidade de leitura. A função tem sintaxe similar ao
.

O ponto de interesse na função é o gerenciamento da memória. A variável local


foi criada para auxiliar na alocação de memória. A string lida do arquivo é
temporariamente acomodada na variável . Com isto teremos informação
suficiente para fazer a alocação dos bytes necessários. Em seguida, a expressão
solicita ao a medida certa de memória. O endereço
retornado pela função é armazenado na célula do vetor . Na sequência a
string lida e acomodada na variável é copiada para a área recém alocada
dinamicamente na . Este processo é repetido para cada linha do arquivo de
entrada dos dados.

A função recebe este vetor de ponteiros para e gera um arquivo com as


strings armazenadas na memória.

A função tem a seguinte lógica:

void liberar (char *M[], int N) {


int i;
for (i = 0; i < N; i++)
free (M[i]);
}

Introdução à Programação: Conceitos e Práticas. Página 356


Os blocos de memória que foram alocados, com seus endereços armazenados
nas células do parâmetro , são liberados um a um.

ii. Aprimorar o mecanismo de alocação de memória do exercício anterior.

Uma possibilidade de efetuar alocação de memória mais ajustada as necessidades


do problema seria transferir o vetor de ponteiros para a área dinâmica, mantendo na
área de alocação fixa apenas uma variável contendo o endereço da primeira célula
deste vetor. Ainda é necessário manter uma variável inteira sincronizada com a
quantidade de elementos utilizados no contexto da execução do programa.

A seguinte figura ilustra o novo modelo de armazenamento dos dados:

ÁREA FIXA HEAP

m m[0] a \0
m[1]
n 3 m[2] b \0

c \0

A nova estrutura de dados teria a seguinte definição:

int main () {
char **m; int n;
load ("dados.txt", m, &n);

}

Segue o programa proposto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXL 1000
#define MAXC 100

int save (char *fn, char *M[], int N) {


FILE *fp;
int i;
if ((fp = fopen (fn, "w")) == NULL) return 1;
for (i = 0; i < N; i++)
fprintf (fp, "%s\n", M[i]);

fclose (fp);
return 0;
}

int load (char *fn, char *M[], int *N) {


FILE *fp; char buf[MAXC];
int i;
if ((fp = fopen (fn, "r")) == NULL) return 1;
for (i = 0; fscanf (fp, "%[^\n]", buf) != EOF; i++) {

Introdução à Programação: Conceitos e Práticas. Página 357


M[i] = malloc (strlen (buf) + 1);
strcpy (M[i], buf);
fscanf (fp, "%*c");
buf[0] = '\0';
}
*N = i;
fclose (fp);
return 0;
}

void liberar (char *M[], int N) {


int i;
for (i = 0; i < N; i++)
free (M[i]);
}

int main () {
char **m;
int n;

m = malloc (MAXL * sizeof (char *));


load ("dados.txt", m, &n);
m = realloc (m, n * sizeof (char *));
save ("saida.txt", m, n);
liberar (m, n);
free (m);

return 0;
}

A variável é declarada no escopo da função como sendo do tipo ponteiro


para um elemento do tipo ponteiro para Apesar de sugerir uma certa
complexidade, o recurso da linguagem que permite interpretar um ponteiro para um
de forma semelhante a um vetor de facilitará a manipulação desta
estrutura.

Como não sabemos o quantidade de linhas contidas no arquivo de entrada de dados,


então é feita provisorimente a alocação de um vetor de ponteiros com ( )
células: ( )

A expressão indica a quantidade de bytes necessária para


acomoda elementos de tamanho individual igual a Após
efetuar o e conhecido o valor , então este vetor é realocado em uma nova área
com o tamanho exato para acomodar a estrutura:

As funções e mantiveram-se iguais a versão anterior, justamente pela


intercambialidade entre as notações ponteiro e vetor. Após liberar as áreas alocadas
para cada string, também é liberada a área destinada ao vetor de ponteiros:
.

Introdução à Programação: Conceitos e Práticas. Página 358


iii. Uma pesquisadora de linguística precisa fazer uma estatística de ocorrência de
palavras em um texto. Sabendo disso, os alunos da disciplina de Computação logo
se prontificaram a fazer o programa. Na solução proposta, bastava a pesquisadora
digitar o texto no arquivo que o programa gerava dois arquivos: sendo um
com a relação das palavras em ordem alfabética e outro com a relação das distintas
palavras com suas frequências. O programa considera como palavra apenas
sequências de caracteres alfabéticos. Qualquer caractere que não seja alfabético é
considerado como separador. Segue um exemplo de arquivo de entrada:

dados.txt
as palavras se repetem e repetem com muita frequencia
e quando se 12repetem &*¨* com muita frequencia, estas palavras
devem ter a 9987 frequencia contada. Assim, as -=33\* palavras devem
apenas conter caracteres de a a z ou de A a Z.

Segue o programa proposto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAXC 50
#define MAXL 1000
#define MAXW 100

typedef struct {
char *s;
int c;
} TDADO;

int load (char *fn, char *V[], int *N) {{


FILE *fp;
int i;
char buf[MAXC];

if ((fp = fopen (fn, "r")) == NULL) return 1;

fscanf (fp, "%*[^a-zA-Z]");


for (i = 0; fscanf (fp, "%[a-zA-Z]%*[^a-zA-Z]", buf) != EOF; i++) {
V[i] = malloc (strlen(buf)+1);
strcpy (V[i], buf);
}

*N = i;
fclose (fp);
return 0;
}

int relat (char *fn, char *V[], int N) {


FILE *fp;
int i;

if ((fp = fopen (fn, "w")) == NULL) return 1;


for (i = 0; i < N; i++) {
fprintf (fp, "%d [%s]\n", i+1, V[i]);

Introdução à Programação: Conceitos e Práticas. Página 359


}

fclose (fp);
return 0;
}

int relatw (char *fn, TDADO *V[], int N) {


FILE *fp;
int i, c;

if ((fp = fopen (fn, "w")) == NULL) return 1;


for (c = i = 0; i < N; i++) {
fprintf (fp, "%d- [%s] = #%d\n", i+1, V[i]->s, V[i]->c);
c += V[i]->c;
}

fprintf (fp, "\n[TOTAL] = #%d\n", c);

fclose (fp);
return 0;
}

void ordenar (char *V[], int N) {


int i, j; char *temp;

for(i = N - 1; i > 0; i--)


for(j = 0; j < i; j++)
if(strcmp(V[j], V[j+1]) > 0) {
temp = V[j];
V[j] = V[j+1];
V[j+1] = temp;
}

};

void ordenarw (TDADO *V[], int N) {


int i, j; TDADO *temp;

for(i = N - 1; i > 0; i--)


for(j = 0; j < i; j++)
if(V[j]->c < V[j+1]->c) {
temp = V[j];
V[j] = V[j+1];
V[j+1] = temp;
}

};

void contar (char *M[], int L, TDADO *V[], int *N) {


int i, j;

V[0] = malloc (sizeof (TDADO));


V[0]->c = 1;
V[0]->s = M[0];

for (i = j = 1; i < L; i++)


if (strcmp (M[i], M[i-1])) {
V[j] = malloc (sizeof (TDADO));
V[j]->c = 1;

Introdução à Programação: Conceitos e Práticas. Página 360


V[j++]->s = M[i];
} else V[j-1]->c++;

*N = j;
}

void liberarw (TDADO *V[], int N) {


int i;
for (i = 0; i < N; i++)
free (V[i]);
}

void liberar (char *V[], int N) {


int i;
for (i = 0; i < N; i++)
free (V[i]);
}

int main (int argc, char *argv[]) {


char **vs; int ns;
TDADO **vw; int nw;

vs = malloc (MAXL*sizeof (char *));


load ("dados.txt", vs, &ns);
vs = realloc (vs, ns*sizeof (char *));

ordenar (vs, ns);

vw = malloc (MAXW*sizeof (TDADO *));


contar (vs, ns, vw, &nw);
vw = realloc (vw, nw*sizeof (TDADO *));

ordenarw (vw, nw);

relat ("relat.txt", vs, ns);


relatw ("relatw.txt", vw, nw);

liberar (vs, ns);


liberarw (vw, nw);

free (vs);
free (vw);

return 0;

A solução prevê duas estruturas de dados para resolver o problema. A primeira


estrutura consiste de um vetor de strings destinado a armazenar as
palavras contidas no arquivo. A segunda estrutura consiste de um
vetor de estrutura, onde cada elemento consiste de um ponteiro para uma string e
um contador. Esta estrutura é destinada a conter as distintas palavras e suas
respectivas contagens de repetições.

Os dados para a estrutura serão alocados dinamicamente. A variável conterá o


endereço de um elemento do tipo . A linguagem oferece o recurso de
manipular um ponteiro para um determinado tipo como vetor de elementos deste

Introdução à Programação: Conceitos e Práticas. Página 361


tipo. Assim, poderá ser manipulado como vetor de ponteiros para , ou vetor
de strings.

De forma semelhante, os dados para a estrutura serão alocados dinamicamente.


A variável conterá o endereço de um elemento do tipo . Assim,
poderá ser manipulado como vetor de ponteiros para .

A primeira ação em é alocar espaço para conter um vetor de


ponteiros para Em seguida, as
palavras contidas no arquivo indicado são carregadas para
Por fim, a estrutura é aqequada para o tamanho
real:

A função tem a seguinte lógica:

int load (char *fn, char *V[], int *N) {


FILE *fp;
int i;
char buf[MAXC];

if ((fp = fopen (fn, "r")) == NULL) return 1;

fscanf (fp, "%*[^a-zA-Z]");


for (i = 0; fscanf (fp, "%[a-zA-Z]%*[^a-zA-Z]", buf) != EOF; i++) {
V[i] = malloc (strlen(buf)+1);
strcpy (V[i], buf);
}

*N = i;
fclose (fp);
return 0;
}

Após a abertura do arquivo, é feita uma leitura para descartar eventual sequência de
caracteres não alfabéticos contida no início do arquivo, e posicionar o indicador de
leitura na primeira palavra: [ – – ] Em seguida há uma
repetição que obtém uma sequência de caracteres alfabéticos e descarta uma
sequência de caracteres não alfabéticos:
[ – – ] [ – – ] A string obtida é armazenada de forma
provisória na variável local . No interior do , para cada palavra, é feita uma
alocação de memória com o tamanho exato da string lida e o endereço da string é
guardado no vetor indicado: [ ] A palavra lida é
copiada para a área alocada: [] A repetição é encerrada ao ser
alcançado o final de arquivo.

Após a leitura das palavras do arquivo, o conteúdo da estrutura é colocado em


ordem alfabética: Segue a lógica para ordenar um vetor de strings,
cujos detalhes já foram discutidos em capítulos anteriores:

Introdução à Programação: Conceitos e Práticas. Página 362


void ordenar (char *V[], int N) {
int i, j; char *temp;

for(i = N - 1; i > 0; i--)


for(j = 0; j < i; j++)
if(strcmp(V[j], V[j+1]) > 0) {
temp = V[j];
V[j] = V[j+1];
V[j+1] = temp;
}

};

Foi aplicado o , fazendo o deslocamento da string de maior posição


alfabética para o final do vetor. O elemento de maior ordem léxica entre a primeira e
última posições é deslocado para a última posição do vetor. Em seguida o elemento
de maior ordem léxica entre a primeira e penúltima posições é deslocado para a
penúltima posição do vetor. Este movimento é feito até que todo o vetor resulte em
ordem alfabética. A comparação entre strings é feita com a função :
[] [ ] No caso de teste positivo, onde a string [ ] é
lexicamente superior à string [ ], é feita apenas uma troca de endereços, sem a
necessidade de copia de strings. Isto torna o algoritmo mais eficiente. Assim, o
elemento [ ] que apontava para uma determinada string, passará a apontar para a
string referenciada por [ ] e vice-versa.

A organização de em ordem alfabética facilita a contagem da frequência com que


as diferentes palavras aparecem na estrutura:

vw = malloc (MAXW*sizeof (TDADO *));


contar (vs, ns, vw, &nw);
vw = realloc (vw, nw*sizeof (TDADO *));

Os resultados da contagem são armazenados na estrutura . Antes de acionar a


lógica da função , a variável recebe o endereço da área com capacidade
para ponteiros para . A estrutura está definida como:

typedef struct {
char *s;
int c;
} TDADO;

A função irá popular a estrutura com a estatística de frequência:

void contar (char *M[], int L, TDADO *V[], int *N) {


int i, j;

V[0] = malloc (sizeof (TDADO));


V[0]->c = 1;
V[0]->s = M[0];

for (i = j = 1; i < L; i++)


if (strcmp (M[i], M[i-1])) {
V[j] = malloc (sizeof (TDADO));
V[j]->c = 1;

Introdução à Programação: Conceitos e Práticas. Página 363


V[j++]->s = M[i];
} else V[j-1]->c++;

*N = j;
}

O parâmetro está vinculado à estrutura onde se encontram as referências ás


palavras ordenadas alfabeticamente, enquanto que o parâmetro está vinculado à
estrutura que conterá a estatística de frequência. A alocação de espaço para os
elementos da estrutura é feita dinamicamente: [ ]
Em seguida, o elemento da estrutura é vinculado à primeira palavra da estrutura
e sua contagem iniciada em [ ] [ ] [ ]

A contagem é bastante facilitada pelo fato da estrutura estar em ordem alfabética.


O algoritmo consiste em percorrer a estrutura a partir do segundo elemento,
testando se a string atual é igual a string anterior, que em caso negativo, estaremos
diante de uma nova palavra, e um elemento é criado em . Caso a palavra atual seja
igual a palavra anterior, basta incrementar o contador.

Retornando a função e com a estrutura adequadamente montada com


referências às distintas palavras e suas frequências, é momento de ordená-lá. Antes,
porém é realocada em área com o tamanho adequado ao tamanho real:
e em seguida ordenada:

void ordenarw (TDADO *V[], int N) {


int i, j; TDADO *temp;

for(i = N - 1; i > 0; i--)


for(j = 0; j < i; j++)
if(V[j]->c < V[j+1]->c) {
temp = V[j];
V[j] = V[j+1];
V[j+1] = temp;
}

};

A lógica para ordenar a estrutura é semelhante á utilizada para ordenar . É feita


comparação entre dois valores consecutivos de frequências, e se necessário
processa-se o deslocamento da referência à estrutura para o final do vetor de
ponteiros. A troca feita é de endereços de memória, o que torna o algoritmo mais
eficiente. Ao final, a estrutura ligada ao parâmetro resultará em ordem
decrecente de contagem de frequência.

Introdução à Programação: Conceitos e Práticas. Página 364


Os relatórios emitidos permitem visualizar o resultado obtido.

relat.txt ...continuação relatw.txt ...continuação


1 [A] 21 [e] 1- [a] = #4 14- [Z] = #1
2 [Assim] 22 [estas] 2- [frequencia] = #3 15- [apenas] = #1
3 [Z] 23 [frequencia] 3- [palavras] = #3 16- [caracteres] = #1
4 [a] 24 [frequencia] 4- [repetem] = #3 17- [contada] = #1
5 [a] 25 [frequencia] 5- [as] = #2 18- [conter] = #1
6 [a] 26 [muita] 6- [com] = #2 19- [estas] = #1
7 [a] 27 [muita] 7- [de] = #2 20- [ou] = #1
8 [apenas] 28 [ou] 8- [devem] = #2 21- [quando] = #1
9 [as] 29 [palavras] 9- [e] = #2 22- [ter] = #1
10 [as] 30 [palavras] 10- [muita] = #2 23- [z] = #1
11 [caracteres] 31 [palavras] 11- [se] = #2
12 [com] 32 [quando] 12- [A] = #1 [TOTAL] = #39
13 [com] 33 [repetem] 13- [Assim] = #1
14 [contada] 34 [repetem]
15 [conter] 35 [repetem]
16 [de] 36 [se]
17 [de] 37 [se]
18 [devem] 38 [ter]
19 [devem] 39 [z]
20 [e]

Todas as alocações feitas dinamicamente são liberadas antes da conclusão de .

iv. Em relação ao exercício anterior, substituir as lógicas de ordenação por chamadas à


função disponível na biblioteca

A função utiliza um algoritmo mais eficiente que o . Os seus


detalhes são objetos de uma disciplina específica de algoritmos e estruturas de
dados. Porém a aplicação da função pode se dar omitindo-se estes detalhes. Segue
o cabeçalho da função:

A função requer quatro argumentos:

i. O endereço inicial ou endereço base do vetor a ser ordenado;


ii. A quantidade de elementos do vetor;
iii. O tamanho em bytes de cada elemento do vetor;
iv. A função que executa a comparação entre dois elementos do vetor e informa
a relação existente entre eles: inferior ( ), iguais ( ) ou superior ( ).

Os três primeiros parâmetros são triviais. O quarto parâmetro requer que seja
codificada em separado uma função de comparação, cujo cabeçalho deverá ter o
seguinte formato:

Os parâmetros da função de comparação são ponteiros para os elementos a serem


comparados. Como o cabeçalho define ponteiros genéricos na
implementação da lógica de comparação será necessário utilizar recurso de
para as devidas reinterpretações dos parâmetros.

Introdução à Programação: Conceitos e Práticas. Página 365


Para ordenar a estutura do exemplo anterior, seria necessária a seguinte
alteração:

void ordenar (char *V[], int N) {

qsort (V, N, sizeof (char *), cmpstr);

};

A função se reduz a uma linha fazendo a chamada à função . O


primeiro argumento é que representa o endereço do primeiro elemento do vetor de
strings, o segundo argumento é , com a quantidade de elementos de o terceiro
argumento é o tamanho de cada elemento de e o último argumento é a função que
efetua a comparação entre dois elementos quaisquer. A função de comparação
implementada foi:

int cmpstr(const void * a, const void * b) {

return strcmp (*(char **)a, *(char **)b);

Como um elemento do vetor a ser ordenado é um logo os parâmetros da


função são ponteiros para ou Assim, o parâmetro deve
ser reinterpretado como , por isso o A função
efetuará a comparação e retornará a condição entre os argumentos e . Observar
que o endereço da string a ser comparada se encontra onde aponta, logo o uso de
, com reinterpretado como o que resulta em Uso
semelhante se aplica ao parâmetro

A ordenação da estrutura também será feita utilizando :

void ordenarx (TDADO *V[], int N) {

qsort (V, N, sizeof (TDADO *), cmpcnt);

};

Os argumentos indicam os atributos do vetor: endereço inicial, quantidade de


elementos e tamanho de cada elemento, e por fim a função de comparação. O
objetivo é organizar em ordem decrescente de frequência de repetição, conforme a
função :

int cmpcnt(const void * a, const void * b) {


int x = (*(TDADO **)a)->c , y = (*(TDADO **)b)->c;
return y - x;
}

Como um elemento do vetor a ser ordenado é um logo os parâmetros da


função são ponteiros para ou Assim, o parâmetro
deve ser reinterpretado como , por isso o A função
efetuará a comparação e retornará a condição entre os argumentos
e . Observar que o endereço da estrutura que contém o valor a ser
comparado está onde aponta, logo o uso de , com reinterpretado como

Introdução à Programação: Conceitos e Práticas. Página 366


o que resulta em Uso semelhante se aplica ao
parâmetro A diferença entre os contadores de frequência resultará na condição
desejada: ou

A ordenação de poderia ser melhorada agregando mais uma condição de


ordenação. Entre as palavras com a mesma frequência, o segundo critério de
ordenação deve ser a ordem alfabética. Neste caso, teríamos as seguintes
alterações:

void ordenarx (TDADO *V[], int N) {

qsort (V, N, sizeof (TDADO *), cmpcntstr);

};

A função deverá compor duas chaves para comparação, combinando os


elementos que irão definir o critério de ordenação. A função é utilizada para
criar as strings com as chaves de comparação. Neste caso, teríamos as seguintes
alterações:

int cmpcntstr(const void * a, const void * b) {


char keya[MAXC], keyb[MAXC];
int ca = (*(TDADO **)a)->c , cb = (*(TDADO **)b)->c;
char *sa = (*(TDADO **)a)->s, *sb = (*(TDADO **)b)->s;
sprintf (keya, "%08x%s", -ca, sa);
sprintf (keyb, "%08x%s", -cb, sb);
return strcmp (keya, keyb);
}

O contador de frequência é a chave principal e a palavra é a chave secundária. A


chave é composta por oito caracteres referentes a formatação da frequência como
inteiro sem sinal em hexadecimal seguidos dos caracteres da palavra. O objetivo de
reinterpretar o valor da frequência como inteiro sem sinal é fazer uso da codificação
em para indicar ordem crescente com valor positivo e ordem
decrescente como valor negativo.

A função tem função semelhante às funções e A função


combina uma string de formato e uma lista de valores para gerar uma string
que será apresentada na tela. A função combina uma string de formato e
uma lista de valores para gerar uma string que será gravada em arquivo. A função
combina uma string de formato e uma lista de valores para gerar uma string
que será armazenada no endereço de memória indicado pelo primeiro parâmetro.

Assim, as strings e conterão as chaves compostas pelos respectivos


contador e palavra. A função retorna o resultado desta comparação que será
utilizada pela função para definir a ação de ordenação.

Introdução à Programação: Conceitos e Práticas. Página 367


28.4. Lista Encadeada

A é uma estrutura de dados que pode ser utilizada para organizar


informações. Os exercícios anteriores mostraram diversas formas de representar
uma coleção de dados na memória, incluindo abordagens estáticas e dinâmicas.

Uma nova forma de organização será apresentada, consistindo em vincular um


conjunto de dados em um lista encadeada na memória. Iremos considerar um
arquivo com vários como origem dos dados. O seguinte diagrama ilustra
como a lista encadeada invertida será estruturada na memória:

ÁREA FIXA HEAP

35.45 17.8 41.42 3.14


Head

Os principais elementos da lista são a ( ) e os . A cabeça da lista é


uma variável do tipo ponteiro que deverá conter o endereço do primeiro nodo. No
caso de uma lista vazia, sem nodo alocado, o conteúdo de será . Os
nodos são todos do mesmo tipo e consistem de uma com pelo menos dois
campos, sendo um reservado para o que se deseja armazenar e o outro nodo
um ponteiro para uma estrutura do mesmo tipo do nodo. As seguintes definições de
tipo e de variáveis proporcionam o suporte inicial necessário para estruturar uma
lista encadeada invertida:

#include <stdio.h>
#include <stdlib.h>

typedef struct NODO TNODO;

struct NODO {
float dado;
TNODO *next;
};

int main () {
TNODO *Head;

Head = NULL;
}

O registro representa o modelo de cada nodo e é composto pelo campo


do tipo e pelo campo do tipo . O campo foi definido
para conter um número e o campo apontará para o próximo elemento da
lista.

Introdução à Programação: Conceitos e Práticas. Página 368


Neste exemplo será adotada a onde o dado mais
recentemente adicionado é inserido na primeira posição. De outra forma teríamos
uma . A variável é inicializada com indicando lista
vazia. Na sequência são descritos os passos para criar e inserir um nodo na lista.

Vamos considerar que o arquivo contenha os seguintes valores: {


} Inicialmente a lista está vazia:

PASSO 0

O próximo passo é inserir o primeiro nodo na lista. Será utilizada uma variável
ponteiro , do tipo para auxiliar no processo de montagem da lista:

i. É criado um nodo na através da instrução


. A variável aponta para este nodo.

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
PASSO 1.1 3 T->next = H;
4 H = T;
T 1
??
H ??

ii. O campo já pode receber a informação desejada ( .

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
PASSO 1.2 3 T->next = H;
4 H = T;
T 1
35.45 2
H ??

Introdução à Programação: Conceitos e Práticas. Página 369


iii. A inserção deste nodo na lista se dá fazendo com que o próximo de
seja o atual primeiro (onde aponta).

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
PASSO 1.3 3 T->next = H;
4 H = T;
T 1
35.45 2
H 3

iv. Em seguida o nodo criado torna-se cabeça da lista ( ).

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
PASSO 1.4 3 T->next = H;
4 H = T;
T 1
35.45 2
H 4 3

Para fixar o conceito, seguem os passos para inserir o segundo nodo na lista:

i. É criado um nodo na através da instrução


. A variável aponta para este nodo.

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
3 T->next = H;
4 H = T;
PASSO 2.1 1
??
T ??
35.45
H

Introdução à Programação: Conceitos e Práticas. Página 370


ii. O campo já pode receber a informação desejada ( .

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
3 T->next = H;
4 H = T;
PASSO 2.2 1
17.8 2
T ??
35.45
H

iii. A inserção deste nodo na lista se dá fazendo com que o próximo de


seja o atual primeiro (onde aponta).

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
3 T->next = H;
4 H = T;
PASSO 2.3 1
17.8 2
T 3
35.45
H

iv. Em seguida o nodo criado torna-se cabeça da lista ( ).

1 T = malloc (sizeof (TNODO));


2 T->dado = x;
3 T->next = H;
4 H = T;
PASSO 2.4 1
17.8 2
T 3
4 35.45
H

De modo semelhante os demais nodos são criados e inseridos na lista. As


informações atribuídas ao campo são oriundas de um arquivo texto contendo a
relação de valores.

Introdução à Programação: Conceitos e Práticas. Página 371


TNODO *load (char fn[]) {
FILE *fp;
TNODO *T, *H = NULL;
float x;

fp = fopen (fn, "r");


if (fp == NULL) return H;
while (!feof (fp)) {
fscanf (fp, "%f", &x);
H = add (H, x);
}

fclose (fp);
return H;
}

A função chama a função para realizar a inserção do valor na lista


encabeçada por .

TNODO *add (TNODO *H, float x) {


TNODO *T;
T = malloc (sizeof (TNODO));
T->dado = x;
T->next = H;
return T;
}

Para cada nodo que se deseja inserir na lista, é feita a alocação de memória para
uma , o campo recebe o valor de , o campo passa a
apontar para o primeiro nodo e por fim o endereço do nodo recém criado é retornado
para a função chamadora, que o fará como o novo cabeça da lista.

Para percorrer uma lista encadeada e utilizar o dado armazenado no nodo também é
utilizada uma variável ponteiro auxiliar Inicialmente aponta para onde aponta o
cabeça da lista ( ). Em seguida é iniciada uma repetição que a cada ciclo
faz com que avance e aponte para o seu próximo elemento ( ). A
repetição é executada enquanto o valor for diferente de , indicando assim
que alcançou o final da lista. A seguinte figura ilustra uma lista encadeada e
destaca a variável se deslocando pelos nodos a cada ciclo do .

3.14 76.4 17.8 35.45


H

Introdução à Programação: Conceitos e Práticas. Página 372


A seguinte função conta a quantidade de nodos de uma lista encadeada:

int contanodo (TNODO *H) {


TNODO *T;
int c = 0;

for (T = H; T != NULL; T = T->next)


c++;

return c;
}

A constante equivale ao inteiro zero. Logo é possível simplificar a expressão


para simplesmente , fazendo a repetição tomar a seguinte forma:

Segue o programa proposto com várias operações com uma lista encadeada.

#include <stdio.h>
#include <stdlib.h>

typedef struct NODO TNODO;

struct NODO {
float dado;
TNODO *next;
};

TNODO *add (TNODO *H, float x) {


TNODO *T;
T = malloc (sizeof (TNODO));
T->dado = x;
T->next = H;
return T;
}

TNODO *load (char fn[]) {


FILE *fp;
TNODO *T, *H = NULL;
float x;

fp = fopen (fn, "r");


if (fp == NULL) return H;
while (!feof (fp)) {
fscanf (fp, "%f", &x);
H = add (H, x);
}

fclose (fp);
return H;
}

void show (TNODO *H) {


TNODO *T;
printf ("====\n");

Introdução à Programação: Conceitos e Práticas. Página 373


T = H;
while (T) {
printf ("%6.2f\n", T->dado);
T = T->next;
}
}

int contanodo (TNODO *H) {


TNODO *T;
int c = 0;

for (T = H; T != NULL; T = T->next)


c++;

return c;
}

TNODO *delfirst (TNODO *H) {


TNODO *T;
if (H == NULL) return NULL;
T = H;
H = H->next;
free (T);
return H;
}

TNODO *clear (TNODO *H) {


while (H = delfirst (H))
;
return H;
}

TNODO *delete (TNODO *H, float x) {


TNODO *T, *P;

for (P = NULL, T = H; T != NULL && T->dado != x; T = T->next)


P = T;

if (T == NULL) return H;
if (P == NULL) return delfirst (H);

P->next = T->next;
free (T);

return H;
}

int main () {
TNODO *Head;

Head = load ("dados.txt");


printf ("tamanho = %d\n", contanodo(Head));
show (Head);
Head = add (Head, -45.5);
show (Head);
Head = delete (Head, -45.5);
Head = delete (Head, 17.8);
Head = delete (Head, 35.45);
show (Head);

Introdução à Programação: Conceitos e Práticas. Página 374


Head = clear (Head);
printf ("tamanho = %d\n", contanodo(Head));
}

A função já foi explicada e serve para criar a lista encadeada invertida a partir
dos dados contidos em um arquivo texto. A função apresenta os dados
contidos nos nodos, permitindo que seja verificada a correção da lógica de
montagem. O módulo retira da lista e libera a memória alocada do nodo cujo
dado contenha um valor procurado. A função desaloca todos os nodos da lista.

A função demanda a manutenção de dois ponteiros auxiliares:

TNODO *delete (TNODO *H, float x) {


TNODO *T, *P;

for (P = NULL, T = H; T != NULL && T->dado != x; T = T->next)


P = T;

if (T == NULL) return H;
if (P == NULL) return delfirst (H);

P->next = T->next;
free (T);

return H;
}

O ponteiro apontará para o nodo que se deseja eliminar da lista, enquanto que o
ponteiro apontará para o nodo anterior ao nodo . A seguinte figura ilustra a
configuração desejada para e após a busca pelo nodo a ser eliminado:

P
3.14 76.4 98.23 35.45
H

Esta configuração é o resultado da seguinte repetição:

for (P = NULL, T = H; T != NULL && T->dado != x; T = T->next)


P = T;

A repetição será encerrada quando encontrar o nodo com o dado procurado ou


quando alcançar o final da lista. Enquanto caminha pelo nodo testado, a variável
caminha um nodo atrasado, ou o nodo anterior a . A busca na lista pode levar a
três situações, com os devidos encaminhamentos:

Introdução à Programação: Conceitos e Práticas. Página 375


a) O dado procurado não foi encontrado ;

if (T == NULL) return H;

Não há o que fazer, simplesmente retorna o valor do cabeça da lista.

b) O dado procurado se encontra no primeiro nodo ;

if (P == NULL) return delfirst (H);

Neste caso, o primeiro nodo é eliminado da lista:

TNODO *delfirst (TNODO *H) {


TNODO *T;
if (H == NULL) return NULL;
T = H;
H = H->next;
free (T);
return H;
}

Para eliminar o primeiro elemento de uma lista encadeada, é utilizada uma


variável auxiliar que apontará para o primeiro nodo Em seguida, o
segundo elemento é feito cabeça da lista Com o antigo
elemento isolado, é possível fazer a liberação da memória alocada

c) O dado procurado se encontra no meio da lista e .

Neste caso, o nodo a ser eliminado é retirado da lista, fazendo com que o próximo
do nodo anterior aponte para o próximo do nodo a ser eliminado. Em seguida, o
nodo pode ser liberado, conforme ilustrado na figura a seguir:

P->next = T->next;
free (T);

T
76.4
P
3.14 98.23 35.45
H
P->next = T->next

Introdução à Programação: Conceitos e Práticas. Página 376


A execução deste programa produz a saída indicada na sequência:

tamanho = 6
====
3.14
76.40
98.23
41.42
17.80
35.45
====
-45.50
3.14
76.40
98.23
41.42
17.80
35.45
====
3.14
76.40
98.23
41.42
tamanho = 0

A fim de facilitar o entendimento do conceito de lista encadeada evitou-se o uso de


passagem de parâmetro do em casos onde o cabeça da lista poderia ser
alterado. Porém, é muito comum soluções em que a alteração do cabeça de uma
lista é feita por meio de parâmetros. Na solução adotada neste exercício, optou-se
em retornar o cabeça da lista como resultado de função. No caso da função ,o
endereço do primeiro nodo é retornado como resultado.

Como a variável , declarada no (valor inicial , será alterada pelo


, para conter o endereço do primeiro nodo, então a função deve informar o
endereço de para a função . Segue a versão das funções e que
atuam no cabeça da lista via parâmetro:

TNODO *add (TNODO **pH, float x) {


TNODO *T;
T = malloc (sizeof (TNODO));
T->dado = x;
T->next = *pH;
*pH = T;
return T;
}

TNODO *load (char fn[], TNODO **pH) {


FILE *fp;
TNODO *T, *H = NULL;
float x;

fp = fopen (fn, "r");


if (fp == NULL) return H;
while (!feof (fp)) {
fscanf (fp, "%f", &x);

Introdução à Programação: Conceitos e Práticas. Página 377


add (&H, x);
}

fclose (fp);
*pH = H;
return H;
}

O parâmetro da função vinculado ao cabeça da lista é definido como ponteiro


para ponteiro de , caracterizando que a função poderá alterar o cabeça
da lista de forma indireta.

Introdução à Programação: Conceitos e Práticas. Página 378


29. Interpretador da Linha Comando

O interpretador de comando ou de um sistema operacional é um programa que


proporciona interação entre o usuário e o ambiente computacional. Antes do
surgimento das interfaces mais elaboradas, notadamente as interfaces gráficas, a
era o único meio de execução de comandos. Os comandos típicos incluem:

i. gerenciamento de pastas ou diretórios (consultar, criar, renomear, apagar,


copiar);
ii. gerenciamento de arquivos;
iii. controle de processos (consultar, executar e );
iv. manipulação de data e hora (consultar e alterar), dentre outros.

Além do comando é necessário conhecer os parâmetros de execução, o que na


maioria dos casos dificulta o uso dos recursos disponíveis no sistema operacional.

O conhecimento das funcionalidades proporcionadas por um interpretador de


comandos é fundamental para profissionais responsáveis pela administração de
serviços e recursos computacionais. Principalmente nos sistemas ou as
são verdadeiros ambientes de programação que disponibilizam poderosas
linguagens interpretadas que permitem o desenvolvimento de programas ( )
com as mais variadas lógicas para execução de comandos.

O surgimento e a disseminação de interfaces gráficas reduziu drasticamente a


necessidade do uso do interpretador de comandos para tarefas básicas. O seguinte
comando lista o conteúdo de uma pasta ou diretório no ambiente :

$ pwd
/home/arquivos
$ ls –l *.txt
drwxr--r-- 1 arquivos editors 4096 dados.txt
-rw-r--r-- 1 arquivos editors 30405 pessoas.txt
-r-xr-xr-x 1 arquivos fred 8460 notas.txt

O comando admite vários parâmetros via linha de comando, como


– – – – – – – - , que orientam a forma de apresentar o conteúdo da pasta. Ao
utilizar uma interface gráfica, o usuário navega facilmente pelas pastas a um simples
toque no mouse ou na tela, e muitas vezes não tem noção que as informações
apresentadas foram obtidas através de chamadas ás funções do sistema
operacional.

Assim, é importante conhecer o mecanismo da linguagem que permite capturar os


argumentos passados através da linha de comando. A linguagem permite capturar
os argumentos passados na linha de comando através dos parâmetros da função
. Assim, será necessário definir os parâmetros de conforme segue:

[]

O primeiro parâmetro ( ) indica a quantidade de argumentos fornecidos na linha


de comando, incluindo o executável. Caso só tenha sido executado o programa em
si, sem argumentos, o resultado é um.

Introdução à Programação: Conceitos e Práticas. Página 379


O parâmetro é um vetor de ponteiros para . Cada posição de
equivale a um argumento da linha de comando no formato string. O valor de [ ]
retorna a identificação do arquivo executado.

O seguinte exemplo ilustra a utilização deste recurso:

#include <stdio.h>

int main (int argc, char *argv[]) {


int i;

printf ("Linha de Comando processada: [%s]\n", argv[0]);


printf ("Quantidade de argumentos : [%d]\n", argc);
for (i = 0; i < argc; i++)
printf ("#%d = %s\n", i, argv[i]);

return 0;
}

A execução deste programa principal produz a saída indicada na sequência:

C:\...>param -a -d 100 *.txt ola "mundo belo"


Linha de Comando processada: [param]
Quantidade de argumentos : [7]
#0 = param
#1 = -a
#2 = -d
#3 = 100
#4 = *.txt
#5 = ola
#6 = mundo belo

O espaço em branco é interpretado como separador de argumentos. O arquivo


executado foi o com os argumentos .

29.1. Exemplos

i. Elaborar um programa que divida um arquivo em vários outros arquivos de menor


tamanho. O nome do aquivo a ser dividido e o tamanho de referência deverão ser
fornecidos via linha de comando.

Este aplicativo é útil quando se deseja transmirtir um arquivo em partes menores.


Como exemplo, considere o arquivo de cuja necessidade é
dividí-lo em arquivos de O programa criaria os arquivos
e com cada um e o arquivo com

Segue o programa proposto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

Introdução à Programação: Conceitos e Práticas. Página 380


enum STATUS {INIT, GETFN, GETSIZE, ERRO};
typedef enum STATUS TSTATUS;

int filesize (char *fn) {


struct stat recfile;
stat(fn, &recfile);
return recfile.st_size;
}

int existfile (char *fn) {


FILE *fp;
fp = fopen (fn, "r");
if (fp == NULL) return 0;

fclose (fp);

return 1;
}

char *getfn (char fs[], char fn[], int t) {


char fe[50];
strcpy (fs, fn);
sprintf (fe, ".%03d", t);
strcat (fs, fe);
return fs;
}

int save (char *fn, char *buffer, int n) {


FILE *fw;
if ((fw = fopen (fn, "wb")) == NULL) return 1;
fwrite (buffer, 1, n, fw);
fclose (fw);
return 0;
}

int split (char *fn, int size) {


FILE *fp;
char *buffer, fnw[300];
int lidos, i;

if ((fp = fopen (fn, "rb")) == NULL) return 1;


if ((buffer = malloc (size)) == NULL) {fclose (fp); return 2;}
for (i = 1; lidos; i++) {
if (lidos = fread (buffer, 1, size, fp)) {
if (save (getfn (fnw, fn, i), buffer, lidos))
{fclose (fp); free (buffer); return 3;}
printf ("%s -> criado (#%d bytes)\n", fnw, filesize(fnw));
}
}

fclose (fp);
free (buffer);

return 0;

int getparam (int argc, char *argv[], char *fn, int *sizep) {

Introdução à Programação: Conceitos e Práticas. Página 381


int i;
TSTATUS status = INIT;
for (i = 1; i < argc && status != ERRO; i++) {
if (status == INIT) {
if (strcmp (argv[i], "-f") == 0) status = GETFN; else
if (strcmp (argv[i], "-s") == 0) status = GETSIZE; else
status = ERRO;
} else {
if (status == GETFN) strcpy (fn, argv[i]); else
if (status == GETSIZE) *sizep = atoi (argv[i]);
status = INIT;
}
}
return status != ERRO;
}

int main (int argc, char *argv[]) {


int size = 1024;
char fn[255] = "";

if (getparam (argc, argv, fn, &size)) {


printf ("fn = %s (#%d bytes)\n", fn, filesize(fn));
printf ("size = %d\n", size);

if (fn[0])
if (existfile (fn))
split (fn, size);
else printf ("arquivo [%s] não encontrado\n\
forma de uso: split -f FNAME -s SIZE\n", fn);
else printf ("forma de uso: split -f FNAME -s SIZE\n");
} else printf ("forma de uso: split -f FNAME -s SIZE\n");

return 0;
}

A execução deste programa na do produz uma saída semelhante a indicada


na sequência:

C:\...\split>split -f info.dat -s 5000


fn = info.dat (#29861 bytes)
size = 5000
info.dat.001 -> criado (#5000 bytes)
info.dat.002 -> criado (#5000 bytes)
info.dat.003 -> criado (#5000 bytes)
info.dat.004 -> criado (#5000 bytes)
info.dat.005 -> criado (#5000 bytes)
info.dat.006 -> criado (#4861 bytes)

C:\...\split>dir

Pasta de C:\...\split

13/11/2019 10:35 <DIR> .


13/11/2019 10:35 <DIR> ..
12/11/2019 16:57 29.861 info.dat
13/11/2019 10:35 5.000 info.dat.001
13/11/2019 10:35 5.000 info.dat.002
13/11/2019 10:35 5.000 info.dat.003
13/11/2019 10:35 5.000 info.dat.004

Introdução à Programação: Conceitos e Práticas. Página 382


13/11/2019 10:35 5.000 info.dat.005
13/11/2019 10:35 4.861 info.dat.006
13/11/2019 10:34 29.879 split.exe

Este programa é um bom caso de estudo, pois utiliza vários recursos da linguagem
, tais como: enumerado, arquivo binário, parâmetros da linha de comando, bem
como algumas técnicas de programação. Fica a cargo de leitor aprofundar a
compreensão da lógica.

A função apresentada anteriormente, retorna o tamanho de um arquivo em


bytes. A função testa se um arquivo existe. A função gera uma string
combinando um nome de arquivo e um inteiro, produzindo o padrão onde
é um sequencial.

A função interpreta os parâmetros da linha de comando e inicializa as


variáveis indicadas por e

A função tem a seguinte lógica:

int split (char *fn, int size) {


FILE *fp;
char *buffer, fnw[300];
int lidos, i;

if ((fp = fopen (fn, "rb")) == NULL) return 1;


if ((buffer = malloc (size)) == NULL) {fclose (fp); return 2;}
for (i = 1; lidos; i++) {
if (lidos = fread (buffer, 1, size, fp)) {
if (save (getfn (fnw, fn, i), buffer, lidos))
{fclose (fp); free (buffer); return 3;}
printf ("%s -> criado (#%d bytes)\n",
fnw, filesize(fnw));
}
}

fclose (fp);
free (buffer);

return 0;

O arquivo a ser dividido é aberto no modo e . Uma área de memória


com o tamanho indicado é alocada dinamicamente. Na sequência uma repetição irá
criar tantos arquivos quantos resultarem da divisão do arquivo de origem. Um bloco
de dados com o tamanho especifido é lido do arquivo de origem e gravado em um
novo arquivo com o mesmo nome do arquivo de origem acrescido de uma extensão
sequencial. Quando alcançar o fim do arquivo de origem, o número de bytes lidos
será zero, e a repetição será encerrada.

ii. Elaborar um programa que agrupa vários arquivos binários em um único arquivo,
seguindo as definições do exemplo anterior.

Segue o programa proposto:

Introdução à Programação: Conceitos e Práticas. Página 383


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

int filesize (char *fn) {


struct stat recfile;
stat(fn, &recfile);
return recfile.st_size;
}

int savefile (char *fn) {


FILE *fp; int i; char fnew[300]; int result = 1;

if ((fp = fopen (fn, "rb")) == NULL) return 1;


fclose (fp);
strcpy (fnew, fn);
for (i=0; i<10 && result; i++) {
strcat (fnew, ".bak");
result = rename (fn, fnew);
}
return !result;
}

char *getfn (char fs[], char fn[], int t) {


char fe[50];
strcpy (fs, fn);
sprintf (fe, ".%03d", t);
strcat (fs, fe);
return fs;
}

int add (FILE *fd, FILE *fs, int n) {


int lidos; char *buffer;

if ((buffer = malloc (n)) == NULL) return 1;


lidos = fread (buffer, 1, n, fs);
fwrite (buffer, 1, lidos, fd);
fclose (fs);
free (buffer);
return 0;
}

int join (char *fn) {


FILE *fp, *fw;
char fnw[300];
int i;

if ((fp = fopen (fn, "wb")) == NULL) return 1;


for (i = 1; ; i++) {
if ((fw = fopen (getfn (fnw, fn, i), "rb")) == NULL) break;
if (add (fp, fw, filesize(fnw))) break;
}

fclose (fp);

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 384


int main (int argc, char *argv[]) {

if (argc > 1)
if (savefile (argv[1]))
join (argv[1]);
else printf ("erro rename = [%s]\n", argv[1]);
else printf ("falta argumento com filename\n");

return 0;
}

A execução deste programa na do produz uma saída semelhante a indicada


na sequência:

C:\...\split>join info.dat

C:\...\split>dir

Pasta de C:\...\split

13/11/2019 11:19 <DIR> .


13/11/2019 11:19 <DIR> ..
13/11/2019 11:19 29.861 info.dat
13/11/2019 10:35 5.000 info.dat.001
13/11/2019 10:35 5.000 info.dat.002
13/11/2019 10:35 5.000 info.dat.003
13/11/2019 10:35 5.000 info.dat.004
13/11/2019 10:35 5.000 info.dat.005
13/11/2019 10:35 4.861 info.dat.006
13/11/2019 11:04 29.861 info.dat.bak
13/11/2019 11:09 29.861 info.dat.bak.bak
13/11/2019 11:11 29.861 info.dat.bak.bak.bak
13/11/2019 11:03 27.495 join.exe
13/11/2019 10:34 29.879 split.exe

O usuário deve informar apenas o nome do arquivo a ser montado. O programa irá
buscar por todos os arquivos que tenham o mesmo nome acrescido de um
sequencial. A partir do sequencial os arquivos serão copiados um a um para formar
um único arquivo. Caso haja um arquivo com o mesmo nome do arquivo que se
deseja montar, o programa salva um cópia com a extensão . Havendo um
feito, então cria um novo arquivo com mais uma extensão .

Introdução à Programação: Conceitos e Práticas. Página 385


30. Recursividade

O estilo de programação determina a aparência de um programa. A linguagem


pertence a família das linguagens imperativas e provê bons mecanismos para a
programação estruturada. A solução de um problema conduz geralmente a uma
organização do programa que combina as construções de controle clássicas,
agrupadas em procedimentos e funções, executando operações sobre estruturas de
dados. Porém, há uma forma diferenciada de representação de problemas que
resulta em programas com estilo bem específico, denominada
.

30.1. Conceito

A programação recursiva é um estilo de resolução de problemas que consiste na


descrição da lógica em dois passos, sendo um passo elementar e direto e um
segundo passo mais abrangente que generaliza a lógica invocando a si mesmo até
reduzir o problema à chamada do passo elementar.

O poder desta técnica influenciou fortemente os paradigmas funcional ( e )


e lógico ( ) onde a forma principal de resolução de problemas é pelo uso da
lógica recursiva.

Um exemplo clássico de recursividade é o cálculo do fatorial de onde a


declaração do problema pode ser feita da seguinte forma:

{ }

Reescrevendo esta definição tem-se:


Observamos que aparece no lado esquerdo e direito do segundo passo, o


que caracteriza a recursão. O programa que implementa esta lógica é:

#include <stdio.h>
typedef long long int64;

int64 fatorial (int N) {


if (N == 0) return 1;
return N * fatorial (N-1);
}

int main (int argc, char *argv[]) {


int k;

for (k = 0; k <= 25; k++)


printf ("fat (%d) = %lld\n", k, fatorial (k));

return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 386


A seguinte figura ilustra a sequência das chamadas recursivas para o cálculo do
fatorial de :

Fatorial (5)

5*Fatorial (4) 5*4*3*2*1*1

4*Fatorial (3) 4*3*2*1*1

3*Fatorial (2) 3*2*1*1

2*Fatorial (1) 2*1*1

1*Fatorial (0) 1*1

As chamadas recursivas acumulam uma sequência de contextos abertos que


começam a ser resolvidos e fechados quando encontram uma definição básica não
recursiva. A figura anterior ilustra a execução de forma descendente e o fechamento
dos contextos de forma ascendente, iniciando com . A partir deste
ponto, os resultados vão retornando às chamadas abertas, resolvendo o fatorial, e
assim sucessivamente até alcançar à chamada inicial com

Na sequência o resultado para a execução indicada.

fat (0) = 1
fat (1) = 1
fat (2) = 2
fat (3) = 6
fat (4) = 24
fat (5) = 120
fat (6) = 720
fat (7) = 5040
fat (8) = 40320
fat (9) = 362880
...
fat (19) = 121645100408832000
fat (21) = -4249290049419214848
fat (22) = -1250660718674968576
fat (23) = 8128291617894825984
fat (24) = -7835185981329244160
fat (25) = 7034535277573963776

A partir do fatorial de o resultado passa a ser inconsistente. Isto significa que o


resultado não pode ser acomodado em uma variável inteira de . A função
fatorial cresce rapidamente, e sempre que possível o seu cálculo direto deve ser
evitado.

Introdução à Programação: Conceitos e Práticas. Página 387


30.2. Exemplos

i. Elaborar um programa que:

a. retorna a posição da primeira ocorrência de em um vetor ;


b. busca pela última ocorrência de em .

A busca recursiva por um em um vetor não ordenado pode ser definido como:

{ [] }
[]

Em síntese a declaração deste problema estabelece que a busca será feita em um


determinado intervalo de elementos ( a ), comparando o valor de com o primeiro
elemento deste invervalo. Caso este primeiro elemento não seja igual a , então é
feita uma nova busca em um intervalo que exclua o antigo primeiro elemento
( a ). O elemento não pertencerá ao vetor quando após uma sucessão de
estreitamentos do intervalo de busca chega-se ao ponto que o índice do primeiro
elemento do intervalo supera o índice do último elemento.

A busca pela última ocorrência de em é feita de modo semelhante, porém


comparando-se ao o último elemento do intervalo e o estreitamento ocorre no
índice do último elemento ( a ).

{ [ ] }
[ ]

O programa que implementa está lógica é:

#include <stdio.h>

int buscadir (int *V, int i, int N, int X) {


if (i < 0) return -1;
if (i > N) return -1;
if (V[i] == X) return i;
return buscadir (V, i+1, N, X);
}

int buscarev (int *V, int i, int N, int X) {


if (i < 0) return -1;
if (i > N) return -1;
if (V[N] == X) return N;
return buscarev (V, i, N - 1, X);
}

int main (int argc, char *argv[]) {


int v[] = {7, 12, 25, 38, 45, 56, 69, 78, 91, 109}, n =10, x;

x = 25;
printf ("#%d = %d\n", x, buscadir(v, 0, n-1, x));
x = 55;
printf ("#%d = %d\n", x, buscadir(v, 0, n-1, x));

Introdução à Programação: Conceitos e Práticas. Página 388


x = 56;
printf ("#%d = %d\n", x, buscarev(v, 0, n-1, x));
return 0;
}

É notório o poder da técnica de recursividade tanto na descrição do problema quanto


na implementação do programa. A solução é compacta, porém com relevante
conteúdo computacional. Além disso, pode ser constatado que o código produzido é
bastante semelhante à descrição do problema. O programa principal ilustra três
chamadas. O vetor é declarado e preenchido com valores inteiros. Em seguida
são chamadas as funções recursivas para a busca direta e a busca reversa. Neste
caso, os resultados apresentados serão:

#25 = 2
#55 = -1
#56 = 5

O valor foi encontrado na posição , o valor não foi encontrado e valor foi
encontrado na posição .

ii. Elaborar um programa que forneça os termos de uma série de .

A série ou sequência de é uma sucessão de números, onde os dois


primeiros elementos são e 1 e os demais são obtidos pela soma dos dois valores
anteriores. Assim, podemos enumerar os seguintes termos:

Vários eventos da natureza podem ser modelados através da série de , tais


como o crescimento das pétalas de determinadas flores, espiral de conchas,
população de coelhos, dentre outros. A definição recursiva para os termos desta
série é dada por:

{ }

O programa que implementa esta lógica é:

Program ExFIBO;
#include <stdio.h>

int fib (int N) {


if (N == 0) return 0;
if (N == 1) return 1;
return fib(N-1) + fib(N-2);
}

int main (int argc, char *argv[]) {


int i;

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


printf ("fib(%d) = %d\n", i, fib(i));
return 0;
}

Introdução à Programação: Conceitos e Práticas. Página 389


Esta solução não é a mais adequada devido a sua pouca eficiência, pois os cálculos
feitos para não são aproveitados para . Segue uma versão
mais eficiente:

#include <stdio.h>

int fib2 (int N, int p0, int p1) {


if (N == 1) return p1;
return fib2(N-1, p1, p0 + p1);
}

int fib (int N) {


if (N == 0) return 0;
return fib2(N, 0, 1);
}

int main (int argc, char *argv[]) {


int i;

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


printf ("fib(%d) = %d\n", i, fib(i));

return 0;
}

Na sequência o resultado para a execução indicada.

fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
...
fib(28) = 317811
fib(29) = 514229
fib(30) = 832040

iii. Elaborar um programa para:

a. calcular a soma dos elementos de um vetor;


b. contar a quantidade de vezes que um determinado valor aparece em um
vetor.

A soma dos elementos de um vetor pode ser descrita de forma recursiva. O passo
primitivo é para o caso do vetor não possuir elemento algum, resultando em soma
igual a zero. O passo geral estabelece que a soma dos elementos do vetor é igual
à adição do primeiro elemento com a soma dos demais elementos.

Introdução à Programação: Conceitos e Práticas. Página 390


{ }
[ ]

A quantidade de vezes que um elemento aparece em um vetor também pode ser


descrita na forma recursiva, conforme segue:

{ [ ] }
[ ]

O programa que implementa esta lógica é:

#include <stdio.h>

int somar (int *V, int N) {


if (N == 0) return 0;
return *V + somar (V+1, N-1);
}

int contar (int *V, int N, int X) {


if (N == 0) return 0;
if (*V == X) return 1 + contar (V+1, N-1, X);
return contar (V+1, N-1, X);
}

int main (int argc, char *argv[]) {


int v[] = {7, 12, 25, 38, 45, 56, 38, 78, 91, 38}, n = 10, x;

printf ("soma = %d\n", somar(v, n));


x = 38;
printf ("#%d = %d vezes\n", x, contar(v, n, x));

return 0;
}

Na sequência o resultado para a execução indicada:

soma = 428
#38 = 3 vezes

Introdução à Programação: Conceitos e Práticas. Página 391


31. Referências Bibliográficas

I. Wirth, Niklaus. The Programming Language Pascal: Revised Report.


Eidgenössische Technische Hochschule. 1973. Modo de acesso: World Wide
Web:
.
II. GHEMAWAT, Pankaj (Adaptado do Livro de). A estratégia e o cenário de
negócios – Caso 1: Intel Corporation 1968-1997, Editora Bookman, 1999.
III. Programa Educar: Curso para professores de 1ª a 4ª série do Ensino
Fundamental. Sistemas de Numeração. ICMC/USP. Modo de acesso: World
Wide Web: .
IV. Fonseca Filho, Cléuzio. História da computação [recurso eletrônico]: O
Caminho do Pensamento e da Tecnologia. Porto Alegre. EDIPUCRS,
2007. 205 p. Modo de acesso: World Wide Web:

V. "The ENIAC Story". Modo de acesso: World Wide Web:


. Acessado em 03-02-2014.
VI. "History of Computer Printers". Modo de acesso: World Wide Web:

Acessado em 03-02-2014.
VII. Intel Corporation. Intel® 64 and IA-32 Architectures Software Developer’s
Manual. Volume 1: Basic Architecture. Order Number: 253665-043US. May
2012.
VIII. Lancharro, Eduardo Alcalde. Informatica Basica. MAKRON BOOKS ISBN:
0074605100.
IX. Meirelles, Fernando S. Informática: Novas aplicações com
microcomputadores. Makron Books.
X. Barroso, L. C. et al., Cálculo Numérico com Aplicações, 2 ed. Ed.
HARBRA, São Paulo, 1987. ISBN:8529400895.
XI. Wirth, Niklaus. Algoritmos e estruturas de dados. Rio de Janeiro: LTC -
Livros Técnicos e Científicos, 1989. 255p. ISBN 8521611900.
XII. Wirth, Niklaus. Programação Sistemática. Editora Campus. 1978.
XIII. Ralston, Anthony; Neill, Hugh. Algorithms (Teach Yourself). Editora
McGraw-Hill. 1997. 194p.

Introdução à Programação: Conceitos e Práticas. Página 392


ÍNDICE REMISSIVO

A B
ábaco, 10, 12, 28
base
ábaco chinês, 10
binário, 32
Ada, 14
conceito, 30
Álgebra de Boole, 14, 15, 47
conversão, 33
Al-Kharazmi, 28
decimal, 30
Alocacação Dinâmica
dígitos, 33
free, 352
hexadecimal, 31
Heap, 351
qualquer, 33
Lista Encadeada, 368
símbolos, 33
malloc, 351
Blaise Pascal, 12
realloc, 353
Anticítera, 10, 11
aritmética binária, 14 Ch
Arquivos
Arquivo Texto, 318 Charles Babbage, 14
Arquivos Binários, 316
fclose, 316 C
feof, 323
fgetc, 328 circuito integrado, 20
fgets, 356 Colossus, 15
FILE *, 316
fopen, 316, 322
fputs, 356
D
fread, 334 decimal, 70, 245, 248
fscanf, 316, 324 Decimal Codificado em Binário, 15
fscanf, 323
fseek, 336
ftell, 337
E
fwrite, 332 EDVAC, 16
modo de acesso, 322 ENIAC, 16
stat, 337 Enigma, 15
stdin, 314 Entrada de Dados
stdout, 314 scanf, 122
struct FILE, 317 Enumerado
ASCII conceito, 338
\\, 98 identificador, 338
\0, 98 Expressões Numéricas, 104
\n, 98
\r, 98
\t, 98 F
Assembly, 19, 20 Fibonacci, 28
atoi, 253

Introdução à Programação: Conceitos e Práticas. Página 393


FORTRAN, 19, 20, 88 int contavogal (char src[]), 240
Função int days (int m, int a), 152
Argumentos, 134 int days (int mes, int ano), 183
Cabeçalho, 135 int days (int mes, int ano), 175
Chamada, 134 int escalona (float M[][MAXC], int L, int C), 293
exemplo int existfile (char *fn), 380
char *diatostr (char *dst, TDIA dia), 338 int fat (int n), 203, 204
char *getfn (char fs[], char fn[], int t), 380, 384 int fib (int N), 389, 390
char *ordenar (char *s), 270 int fib2 (int N, int p0, int p1), 390
char *strcpy (char *dst, char *src), 263 int filesize (char *fn), 337, 380, 384
char *strupr (char *str), 263 int flipbit (int x, int t), 345
char *tobin (char *dst, int x, int n), 345 int foo (int *a, int *b), 161
char upcase (char c), 238 int foo (int a, int b), 158
double *buscar (double vx[], int nx, double x), int igual (char sa[], char sb[]), 241, 251, 253,
227 255
double double fat (int n), 196 int imcint (float x), 307
double getDIST (double x1, double y1, double int isconsoante (char ch), 148
x2, double y2), 137 int isconsoante (int c), 183
double GetEPS (), 188, 190 int isletra (char ch), 148
double getimc (double Peso, double Altura), int isminusculo (char c), 238
134 int ispalin (char src[]), 241
double getmaior (double vx[], int nx), 219 int isvogal (char c), 241
double media (double a, double b), 138 int isvogal (char c), 240
double media4 (double a, double b, double c, int isvogal (char ch), 147, 148
double d), 139 int isvogal (int c), 183
double polinomio (double vx[], int nx, double int join (char *fn), 384
x), 224 int leap (int ano), 183
double power (double X, double Y), 137 int leap (int ano), 152, 175
double powint (double x, int t), 190 int length (char src[]), 241, 245
double raizn (double x, int n), 190 int length (char src[]), 236, 237
double raizq (double x), 188 int load (char *fn, char *M[], int *N), 354, 357
double reduzir (double rad), 142 int load (char *fn, char *V[], int *N), 359
double reduzir (double x), 196, 200 int load (char *fn, int *V, int *N), 333
double sen (double x), 200 int load (char *fn, TDADO *V, int *N), 334
double seno (double rad), 140, 142 int maior (int x, int y), 150
double seno (double x), 196, 200 int matprod (int Mc[][MAXC], int *Lc, int *Cc,
double soma (double x, int n), 194 int Ma[][MAXC], int La, int Ca, int
double soma (int n), 191, 193 Mb[][MAXC], int Lb, int Cb), 288
double somavet (double vx[], int nx), 219 int numdigit (int x, int base), 247, 248
double torad (double graus), 200 int poschar (char src[], char ch), 241
double torad (double graus), 140, 142, 196 int posmenor (double vx[], int p, int q), 228
float *lervet (FILE *f, float *V, int N), 319 int relat (char *fn, char *V[], int N), 359
float *printvet (FILE *f, float *V, int N), 319 int relatw (char *fn, TDADO *V[], int N), 359
int getparam (int argc, char *argv[], char *fn, int replace (char *fn, int X, TDADO *R), 334
int *sizep), 380 int rol (int x, int t), 345
int add (FILE *fd, FILE *fs, int n), 384 int ror (int x, int t), 345
int ascii (char *fnsrc, char *fndst, int width), int save (char *fn, char *buffer, int n), 380
327 int save (char *fn, char *M[], int N), 354, 357
int backsubstitution (float M[][MAXC], int L, int int save (char *fn, TDADO *V, int N), 334
C, float *V, int *N), 293 int savefile (char *fn), 384
int buscadir (int *V, int i, int N, int X), 388 int setbit (int x, int t), 345
int buscar (double vx[], int nx, double x), 225, int sizeoffile (char *fn), 337
226, 227 int somar (int *V, int N), 391
int buscarev (int *V, int i, int N, int X), 388 int somar (int *vx, int nx), 262
int chgbit (int x, int t, int r), 345 int somar (int vx[], int nx), 261
int clrbit (int x, int t), 345 int somavet (int *v, int n), 283
int cmpcnt (const void * a, const void * b), 366 int split (char *fn, int size), 380
int cmpcntstr (const void * a, const void * b), int strlen (char *str), 263
367 int testbit (int x, int t), 345
int cmpstr (const void * a, const void * b), 366 int to_bin (char *fn, int *V, int N), 330
int combina (float M[][MAXC], int L, int C, int int to_txt (char *fn, int *V, int N), 330
Lx, int Ly), 293 int toint (char s[], int base), 251
int contanodo (TNODO *H), 373 int valor (char c), 251
int contar (int *V, int N, int X), 391

Introdução à Programação: Conceitos e Práticas. Página 394


int64 fatorial (int N), 386 void printvet (double vx[], int nx), 224, 228
TDADOS getrec (), 306 void raizes (double a, double b, double c,
TDIA strtodia (char *str), 338 double *r1, double *r2), 164
TNODO *add (TNODO *H, float x), 373 void raizes (double a, double b, double c,
TNODO *clear (TNODO *H), 373 double *r1, double *r2, int *code), 174
TNODO *delete (TNODO *H, float x), 373 void relat (TPESSOA *v, int n), 307
TNODO *delfirst (TNODO *H), 373 void shiftmaior (char *u, char *v), 270
TNODO *load (char fn[]), 373 void shiftmaior (char M[][MAXC], int a, int b),
void bubblesort (float vx[], int nx), 231, 232 296
void cabecalho (FILE *arq, int N), 319 void shiftmaior (dobule v[], int a, int b), 232
void calc_med_al (float NOTAS[][MAXB], float void shiftmenor (dobule v[], int a, int b), 233
MEDIAS_AL[], int L, int C), 319 void show (char *fn, char NOMES[][MAXC],
void calc_med_bim (float NOTAS[][MAXB], float NOTAS[][MAXB], float MEDIAS_AL[],
float MEDIAS_BIM[], int L, int C), 319 float MEDIAS_BIM[], int L, int C), 319
void contar (char *M[], int L, TDADO *V[], int void show (char M[][MAXC], int L), 296
*N), 359 void show (int *V, int N), 333
void contar (int *v, int n, char *str, char *ref), void show (int *v, int n, char *ref), 268
268 void show (int code, double x1, double x2),
void contar (int vc[], int nc, char s[]), 255 174
void cpystr (char dst[], char src[]), 237, 238 void show (int M[][MAXC], int L, int C), 276,
void delbranco (char dst[], char src[]), 253 281, 283, 288
void fill_a (int M[][MAXC], int L, int C), 281 void show (TDADO V[], int N), 334
void fill_b (int M[][MAXC], int L, int C), 281 void show (TNODO *H), 373
void fill_c (int M[][MAXC], int L, int C), 281 void showmat (int M[][MAXC], int L, int C), 293
void fill_d (int M[][MAXC], int L, int C), 281 void showrec (TDADOS rec), 304
void fill_random (int M[][MAXC], int L, int C), void showvet (float *V, int N), 293
283, 288 void showvet (float vx[], int nx), 302
void getimc (TPESSOA *v, int n), 307 void showvet (int *v, int n), 283
void getrec (TDADOS *rec), 305 void somacol (int M[][MAXC], int L, int C, int
void incdate (int *d, int *m, int *a), 176 V[], int *N), 283
void int fcmp (const void * a,const void * b), void somalin (int M[][MAXC], int L, int C, int
365 V[], int *N), 283
void inverter (char str[]), 245 void strcar (char dst[], char src[]), 243
void ler (char *fn, char NOMES[][MAXC], float void strcdr (char dst[], char src[]), 243
NOTAS[][MAXB], int *L, int *C), 319 void tobase (char s[], int num, int base), 248,
void ler (int *V, int *N), 330 250
void ler (int *vx, int *p), 262 void tobin (char s[], int num), 245, 247
void ler (int M[][MAXC], int *L, int *C), 276 void tohms (int seg, int *h, int *m, int *s), 167
void ler (int vx[], int * p), 261 void trocar (char *a, char *b), 245, 270, 296
void lerdados (), 187 void trocar (double *a, double *b), 228
void lerdados (double vx[], int *nx), 219, 224, Forma Geral, 133
225, 228 passagem de parâmetro
void lerdados (int n), 205, 206, 207, 208, 209 array, 229
void lerdados (int vx[], int *nx), 217 conceito, 158
void lervet (float vx[], int *nx), 302 cópia de endereço, 161
void liberar (char *M[], int N), 354, 357 cópia de valor, 158
void liberar (char *V[], int N), 359 struct, 304
void liberarw (TDADO *V[], int N), 359 endereço, 305
void linha (FILE *arq, char *str, float V[], int N), Principais Elementos, 133
319 retorno
void load (char M[][MAXC], int *L), 296 struct, 306
void load (float M[][MAXC], int *L, int *C), 293 funções de teste
void load (TPESSOA v[], int *n), 307 isalnum, 149, 150
void maiusculo (char src[]), 238, 239 isalpha, 149
void menorfirst (double vx[], int p, int q), 228 iscntrl, 149
void mostrar (int vc[], int nc), 255 isdigit, 149
void ordena (char M[][MAXC], int L), 296 isgraph, 149
void ordenar (char *V[], int N), 359 islower, 149
void ordenar (double vx[], int nx), 228 isprint, 149
void ordenarw (TDADO *V[], int N), 359 ispunct, 149
void posmaior (int M[][MAXC], int L, int C, int isspace, 149
*LIN, int *COL), 283 isupper, 150
void preencher (TDADO *p, int seq), 334 isxdigit, 150
void printinv (int vx[], int nx), 217 tolower, 148

Introdução à Programação: Conceitos e Práticas. Página 395


toupper, 147 Head, 368
funções matemáticas Inserir, 369
abs, 116 Percorrer, 372
acos, 107
asin, 108
atan, 108
M
atan2, 109 Máquina Analítica, 14
ceil, 116
cos, 109
cosh, 110 N
exp, 112 Napier, 11, 12
fabs, 116 Números Inteiros
floor, 117 Inteiro Com Sinal, 62
fmod, 118
frexp, 112
ldexp, 113 O
log, 113 Operador de Atribuição, 102
log10, 114 Operadores Binários
modf, 114 &, 341
pow, 115 ^, 341
round, 117 |, 341
sin, 110 ~, 341
sinh, 111 Operadores de deslocamento e bits
sqrt, 115 <<, 342
tanh, 111 >>, 343
trunc, 118 Operadores Lógicos
!, 146
G &&, 146
? :, 151
George Boole, 14 ^^, 146
Gutenberg, 11, 29 ||, 146
Operadores Matematicos
H *=, 190
/=, 191
Hollerith, 15 ++, 177, 194
+=, 194, 195
I >>=, 345
Divisão (/), 104
identificadores, 95 Divisão Inteira (/), 104
Inteiro com sinal Identidade (+), 104
Complemento de 2, 330 Inversão (-), 104
Complemento de 2, 342 Multiplicação (*), 104
Inteiro com Sinal Resto (%), 104
Módulo Sinal, 62 Soma (+), 104
itoa, 250 Subtração (-), 104
Operadores Relacionais
J !=, 145
<, 145
Jacquard, 13 <=, 145
John Napier, 11 ==, 144
>, 144
>=, 145
L Organização da memória
Leibniz, 13 Heap, 350
linguagem de máquina, 17, 316 Segmento de Código/Text, 350
Linha de Comando Segmento de Dados, 350
argc, 379 Segmento de Pilha, 350
argv, 380 Oughtred, 12
Parâmetros, 379
Lista Encadeada P
Campo dado, 368
Campo next, 368 Patridge, 12
Eliminar um nodo, 375

Introdução à Programação: Conceitos e Práticas. Página 396


ponteiro, 105, 154, 156, 162, 227, 235, 257, 258, 259, posicional, 27
261, 262, 263, 264, 269, 270, 286, 303, 305, 309, 317, zero, 27
323, 336, 352, 355, 358, 368, 369, 372, 375, 378 maia, 25
printf, 126 origem, 27
Formato, 128 polinômio geral, 33, 34
Prioridade de Operadores posicional, 30
AND Binário, 105 Progressão Geométrica, 35
AND Lógico, 105 romano, 25
Atribuição, 105 Sistema de Numeração
Condicional, 105 binário, 20, 34, 40, 43, 62, 63, 64, 65, 68, 69, 70, 71,
Deslocamento, 105 73, 74, 81, 246
Multiplicação, 105 sizeof, 128
OR Binário, 105 string.h
OR Lógico, 105 memset, 267
Pós-fixo, 105 strcat, 267
Relacional, 105 strchr, 267
Unário, 105 strcmp, 267
Vírgula, 105 strcpy, 267
XOR Binário, 105 strlen, 267
Programação recursiva strlwr, 267
Busca em vetor, 388 strrchr, 267
Cálculo do Fatorial, 386 strrev, 267
Conceito, 386 strset, 267
Soma de vetor, 390 strstr, 267
strupr, 267
Q
T
qsort, 365
tear, 13
R Tim Berners-Lee, 22
Tipos Inteiros
relés, 17 char, 100
Representação de Dados, 61 int, 100
Números Inteiros, 62 long int, 101
long long int, 101
short int, 100
S signed char, 100
Saída de Dados unsigned char, 100
printf, 126 unsigned int, 101
scanf, 122 unsigned long int, 101
scanf unsigned long long int, 101
Formato, 124 unsigned short int, 101
Schickard, 11 Tipos reais
Shannon, 15 double, 100, 101
Sistema de numeração float, 100, 101
base, 30 long double, 101
base qualquer, 33 Turing, 15
binário, 30, 32, 34
Conversão base 10 para outra base, 36 V
Conversão base 16 para base 2, 41
Conversão base 2n para base 2, 39 válvulas, 17
Conversão base 4 para base 2, 40 void qsort(void *base,size_t n,size_t size,int
Conversão base 8 para base 2, 40 (*fcmp)(const void *,const void*)), 365
decimal, 30, 31
egípcio, 24 W
hexadecimal, 31
hindu-arábico, 27 Wilhelm Schickard, 11
decimal, 27

Introdução à Programação: Conceitos e Práticas. Página 397

Você também pode gostar