Você está na página 1de 136

Faculdade SATC

Técnicas de Programação II
Linguagem C

Engenharias Elétrica e Mecânica

Profº.: Giovani Martins Cascaes


giovani.cascaes@satc.edu.br

versão 1.02.136

2006/2007
Sumário

Listas de Figuras..........................................................................................................5
Lista de Tabelas...........................................................................................................6
1 Introdução.................................................................................................................7
1.1 Organização básica de programas C .....................................................................7
1.2 Criando, compilando e executando programas em linguagem C ............................9
2 Tipos, bases, operadores e expressões ....................................................................12
2.1 Componentes básicos de um computador ...........................................................13
2.1.1 Unidade Lógica e Aritmética .......................................................................14
2.1.2 Registradores ..............................................................................................14
2.1.3 Memória .....................................................................................................15
2.1.4 Bases ..........................................................................................................16
2.1.5 Unidade de Controle ...................................................................................18
2.2 Declarações de variáveis ....................................................................................18
2.2.1 Definindo contantes.....................................................................................19
2.2.2 Definindo variáveis globais ..........................................................................19
2.3 Expressões.........................................................................................................20
2.3.1 Expressões Aritméticas................................................................................21
2.3.2 Expressões Condicionais .............................................................................23
2.3.3 Prioridade de operadores.............................................................................24
2.4 Exercícios propostos..........................................................................................24
3 O controle do fluxo de execução.............................................................................26
3.1 Seqüências .........................................................................................................26
3.2 If’s.....................................................................................................................27
3.3 Switch................................................................................................................29
3.4 Comando ternário ..............................................................................................33
3.5 Repetições .........................................................................................................34
3.5.1 Comando while ...........................................................................................34
3.5.2 Comando do while ......................................................................................36
3.5.3 Comando for ...............................................................................................37
3.6 Exercícios propostos..........................................................................................42
4 Funções ...................................................................................................................46
4.1 Protótipos de funções.........................................................................................47
4.2 Escopo de variáveis............................................................................................48
4.2.1 Variáveis locais ...........................................................................................48
4.2.2 Variáveis estáticas .......................................................................................50

2
4.2.3 Variáveis globais .........................................................................................50
4.2.4 Parâmetros formais......................................................................................51
4.2.5 Passagem de parâmetros por valor...............................................................51
4.2.6 Passagem de parâmetros por referência .......................................................51
4.2.7 O comando de returno.................................................................................51
4.2.8 Funções sem retorno ...................................................................................52
4.3 Recursão............................................................................................................52
4.4 A função printf() ................................................................................................54
4.5 A função scanf().................................................................................................56
4.5.1 Operador de endereço (&)...........................................................................57
4.6 Funções getche() e getch() .................................................................................58
4.7 Funções getchar() e putchar().............................................................................59
4.6 Exercícios propostos..........................................................................................60
5 Arranjos ..................................................................................................................63
5.1 Vetores ..............................................................................................................64
5.1.1 Exercícios Propostos...................................................................................66
5.2.1 Vetores de caracteres ..................................................................................68
5.2.2 Funções para manipulação de vetores de caracteres .....................................71
5.2.2 Exercícios Propostos...................................................................................75
5.3 Matrizes.............................................................................................................76
5.3.1 Matrizes bidimensionais...............................................................................76
5.3.2 Matrizes de cadeias de caracteres ................................................................77
5.3.3 Inicializações...............................................................................................78
5.3.4 Inicializações sem a especificação de tamanho .............................................79
5.3.5 Passagem de vetores e matrizes ...................................................................79
5.3.6 Exercícios propostos ...................................................................................81
5.4 Definição de estruturas.......................................................................................82
5.5 Enumerações......................................................................................................84
5.6 Definição de nomes de tipos...............................................................................86
5.7 Exercícios propostos..........................................................................................87
6 Ponteiros .................................................................................................................91
6.1 Exercícios propostos..........................................................................................92
6.2 Aritmética de Ponteiros......................................................................................92
6.3 Ponteiros e Arranjos...........................................................................................93
6.3.1 Exercícios propostos ...................................................................................95
6.4 Ponteiro genérico ...............................................................................................96
6.5 Ponteiro como argumento de funções.................................................................96
6.6 Ponteiros e estruturas.........................................................................................98
6.7 Ponteiros para funções .....................................................................................101
6.8 Exercícios propostos........................................................................................103
7 Estruturas de dados..............................................................................................105
7.1 Filas .................................................................................................................105
7.1.1 Filas com prioridades.................................................................................108
7.2 Pilhas ...............................................................................................................109
7.2.1 Exercícios propostos .................................................................................118

3
7.3 Listas lineares ligadas .......................................................................................118
7.3.1 Função sizeof()...................................................................................119
7.3.2 Alocação dinâmica de memória .................................................................120
7.3.3 Lista ordenada...........................................................................................125
7.3.4 Exercícios propostos .................................................................................128
Bibliografia ..............................................................................................................133
Apêndice A - Palavras reservadas em C e C++ ......................................................134
Anexo A – Tabela de caracteres ASCII ..................................................................135

Obs.: Este material foi adaptado da apostila do Prof.º Ivan L.


M. Ricarte lotado no Departamento de Engenharia de
Computação e Automação Industrial da Unicamp.

Os exercícios presentes nas listas em cada capítulo


desta, foram retirados ou adaptados de livros e páginas
da internet e pertencem a seus respectivos donos.

4
Listas de Figuras

Figura 1.1 - Estrutura básica de um algoritmo...............................................................8


Figura 1.2 - Programa que faz nada. .............................................................................8
Figura 1.3 – Fluxo no compilador C..............................................................................9
Figura 2.1 - Composição básica de um computador.[Linhas tracejadas indicam fluxo de
controle e Linhas contínuas fluxo de dados] .........................................................15
Figura 2.2 – Complemento de base (2)........................................................................18
Figura 3.1 – Seqüência em C - Fluxograma.................................................................26
Figura 3.2 – Seleção com if ... else em C. .......................................................29
Figura 3.3 – Seleção usando switch ... case em C...........................................32
Figura 3.4 – Seleção usando switch ... case em C, omitindo-se o break. ......33
Figura 3.5 – Repetição usando while ... em C....................................................34
Figura 3.6 – Repetição usando do while ... em C...............................................36
Figura 3.7 – Repetição usando for ... em C. .......................................................38
Figura 5.1 – Armazenando caracteres na memória RAM. ............................................68
Figura 6.1 - Espaço ocupado pelas variáveis. ..............................................................92
Figura 7.1 – Fila com prioridade descendente............................................................108
Figura 7.2 – Lista ligada simples. ..............................................................................119

5
Lista de Tabelas

Tabela 2.1 – Tipos de dados em C ..............................................................................12


Tabela 2.2 – Caracteres de controle ............................................................................13
Tabela 2.3 – Operadores aritméticos ...........................................................................22
Tabela 2.4 – Operadores bit a bit ................................................................................22
Tabela 2.5 – Operadores relacionais............................................................................23
Tabela 2.6 – Operadores lógicos .................................................................................23
Tabela 2.7 – Tabela verdade dos operadores lógicos &&, || e !....................................24
Tabela 2.8 – Prioridade de operadores ........................................................................24

6
1 Introdução

A linguagem de programação C foi desenvolvida no início dos anos 70 nos


Laboratórios AT&T Bell, nos Estados Unidos. A motivação para que o autor de C,
Dennis Ritchie, criasse uma nova linguagem de programação foi o desenvolvimento do
sistema operacional Unix. C é uma ferramenta tão básica que praticamente todas as
ferramentas suportadas por Unix e o próprio sistema operacional foram desenvolvidas
em C.
C acompanhou o ritmo da distribuição do sistema operacional Unix, que foi
amplamente divulgado e livremente distribuído na década de 70. Apesar de haver
compiladores para linguagens mais “tradicionais” na distribuição Unix, aos poucos C foi
ganhando simpatizantes e adeptos. Atualmente, não há dúvidas de que C é uma das
linguagens de programação de maior aceitação para uma ampla classe de aplicações.
A simplicidade de C não restringe, no entanto, a potencialidade de suas
aplicações. Blocos desempenhando tarefas muito complexas podem ser criados a partir
da combinação de blocos elementares, e este mecanismo de combinação de partes pode
se estender por diversos níveis. Esta habilidade de construir aplicações complexas a
partir de elementos simples é um dos principais atrativos da linguagem.
O sucesso de C foi tão grande que diversas implementações de compiladores
surgiram, sendo que nem todos apresentavam o mesmo comportamento em pontos
específicos, devido a características distintas da arquitetura dos computadores ou a
“extensões” que se incorporavam à linguagem. Para compatibilizar o desenvolvimento de
programas em C, o Instituto Norte-Americano de Padrões (ANSI) criou em 1983 um
comitê com o objetivo de padronizar a linguagem. O resultado deste trabalho foi
publicado em 1990, e foi prontamente adotado como padrão internacional.

1.1 Organização básica de programas C

Na linguagem C, todo algoritmo deve ser traduzido para uma função. Uma
função nada mais é do que um conjunto de expressões da linguagem C (possivelmente
incluindo invocações ou chamadas para outras funções) com um nome e argumentos
associados. Em geral, a definição de uma função C tem a forma:

tipo nome (lista de argumentos) {


declaracoes;
comandos;
}

7
O tipo indica o valor de retorno de uma função, podendo assumir qualquer valor
válido da linguagem C (que serão vistos adiante). O tipo da função pode ser omitido,
sendo que neste caso o compilador irá assumir que o tipo int (inteiro) será retornado.
O nome é o rótulo dado à função, que em geral deve expressar de alguma forma
o que a função realiza.
Nos compiladores mais antigos, o número de caracteres em um nome era
limitado (em geral, a 6 ou 8 caracteres). Atualmente, não há restrições ao comprimento
de um nome, de forma que nomes significativos devem ser preferencialmente utilizados.
Em C, todo nome que estiver seguido por parênteses será reconhecido como o
nome de uma função. A lista de argumentos que fica no interior dos parênteses indica
que valores a função precisa para realizar suas tarefas. Quando nenhum valor é
necessário para a função, a lista será vazia, como em ( ).
O que se segue na definição da função, delimitado entre chaves { e }, é o corpo
da função. Declarações correspondem às variáveis internas que serão utilizadas pela
função, e comandos implementam o algoritmo associado à função.
Todo algoritmo (e, conseqüentemente, todo programa) deve ter um ponto de
início e um ponto de fim de execução. Na figura abaixo (fig.1.1), a estrutura básica de
um algoritmo é ilustrada - onde há uma “nuvem” faz alguma coisa, deve ser inserido o
corpo do algoritmo que descreve a função a ser realizada.

inicio

faz alguma
coisa

fim

Figura 1.1 - Estrutura básica de um algoritmo.

Um programa em C é basicamente um conjunto de funções. O ponto de início e


término de execução de um programa em C está associado com uma função de nome
especial: a função main “principal”. O menor programa em C que pode ser compilado
corretamente é um programa que nada faz (Figura 1.2). Este programa em C é:

main(){

}
inicio

fim

Figura 1.2 - Programa que faz nada.

8
Neste caso, a função de nome main() é definida sem nenhum comando. Neste
ponto, algumas observações devem ser feitas:

Todo programa em C tem que ter pelo menos uma função.

Pelo menos uma função do programa em C tem o nome main() - esta função
indica o ponto onde se iniciará a execução do programa e após executado seu
último comando o programa finaliza sua execução.

Ao contrário do que ocorre em Pascal ou FORTRAN, que diferenciam proce-


dimentos (sub-rotinas) de funções, em C há apenas funções - mesmo que elas não
retornem nenhum valor.

1.2 Criando, compilando e executando programas em linguagem C

Após as etapas de entendimento do problema e definição de uma ou mais


soluções para o mesmo é chegado o momento de criar a representação da solução do
problema por meio do código fonte. Este por sua vez nada mais é do que a
representação de uma solução utilizando a sintaxe correta de uma linguagem de
programação, tal qual o C. Uma ferramenta de edição de texto se faz necessária.
Prefira editores de texto que tenha poucos recursos e que sejam capazes de salvar
seus arquivos fontes em modo caractere, ou seja, ASCII (American Standard Code for
Information Interchange). Evite utilizar editores muito sofisticados tais como Microsoft
Word, Open Office Writer, etc. estes armazenam caracteres extras (de controle) que não
interessam ao programa ou código fonte.
Por convenção o arquivo fonte deve ter a extensão .c, em minúsculo. Esta
atitude facilita o reconhecimento imediato de um arquivo com código C, assim como
extensões .pas para Pascal ou .cpp para C++.

headers (.h) código fonte

Pré-processador

Compilador
código assembly

Montador
bibliotecas (.obj)
código objeto

Editor de Ligação

código executável

Figura 1.3 – Fluxo no compilador C.

9
Como mostra a figura anterior (fig 1.3) o código fonte é somente o início
do trabalho de criação de programas executáveis. Ainda na mesma figura (fig.
1.3) pode-se observar outras etapas. O conceito de cada uma destas etapas é
apresentado a seguir:

pré-processador – este é recebe o arquivo com o código fonte e é responsável


por retirar os comentários e interpretar diretivas especiais denotadas por #.

Por exemplo:

#include <math.h> - arq da biblioteca math.


#include <stdio.h> - arq da biblioteca padrão de E/S.

#define TAM_MAX_ARRAY 100

compilador – o compilador C traduz o código fonte em código assembly. Ele


recebe o arquivo fonte do pré-processador.

montador – cria o código objeto. No UNIX estes arquivos são encontrados com
a extensão .o e no MS DOS com .obj.

editor de ligação – se o arquivo fonte referencia alguma função de alguma


biblioteca ou de algum arquivo externo cabe ao editor de ligação combinar estes
arquivos com a função main() para criar o arquivo executável.

Exemplo 1.1 - Digite o programa abaixo e compile-o. O que o programa faz? (Não
incluir os números e os dois pontos)

1: #include <stdio.h>
2: #include <conio.h>
3: int raio, area;
4:
5: main()
6: {
7: printf("Entre com o raio (ex. 10): ");
8: scanf("%d", &raio);
9: area = (int) (3.14159 * raio * raio);
10: printf("\n\nArea = %d\n", area);
11: getch();
12: return(0);
13: }

Exemplo 1.2 - Digite e compile o programa a seguir. O que o programa faz?

1: #include <stdio.h>
2: #include <conio.h>
3: int x,y;
4:
5: main()
6: {
7: for (x = 0; x < 10; x++, printf ("\n"))

10
8: for (y = 0; y < 10; y++)
9: printf("X");
10: getch();
11: return(0);
12: }

Exemplo 1.3 - Caçador de Erros: O programa seguinte apresenta um problema. Use o


editor e o compilador, encontre e corrija o erro. Qual a linha contem o
erro?

1: #include <stdio.h>
2: #include <conio.h>
3: main();
4: {
5: printf("Fique Olhando!");
6: printf(" Voce o encontrara!\n");
7: getch();
8: return(0);
9: }

Exemplo 1.4 - Caçador de Erros: O programa seguinte apresenta um problema. Use o


editor e o compilador, encontre e corrija o erro. Qual a linha contem o
erro?

1: #include <stdio.h>
2: #include <conio.h>
3: main()
4: {
5: printf("Este e um programa com um ");
6: do_it ("problema!\n");
7: getch();
8: return(0);
9: }

Exemplo 1.5 - Faça a seguinte alteração no programa do exemplo 1.2. Use o editor e
o compilador e execute novamente o programa. O que acontece agora?

9: printf( "%c", 1 );

11
2 Tipos, bases, operadores e expressões

A linguagem C suporta os tipos de dados básicos usualmente suportados pelos


computadores. Na linguagem padrão, os tipos básicos suportados são char, int,
float e double. A tabela completa dos tipos de dados em C, aparece (tab. 2.1) logo
a frente.
O tipo char ocupa um único byte, sendo adequado para armazenar um
caractere do conjunto ASCII e pequenos valores inteiros.
O tipo int representa um valor inteiro que pode ser positivo ou negativo. O
número de bytes ocupado por este tipo (e, conseqüentemente, a faixa de valores que
podem ser representados) reflete o tamanho “natural” do inteiro na máquina onde o
programa será executado.
Os tipos float e double representam valores reais, limitados apenas pela
precisão da máquina que executa o programa. O tipo float oferece cerca de seis
dígitos de precisão enquanto que double suporta o dobro da precisão de um float.
Alguns destes tipos básicos podem ser modificados por qualificadores. Por
exemplo, o tipo char pode ser acompanhado pelo qualificador signed ou
unsigned. O tipo signed char seria utilizado para indicar que a variável do tipo
char estaria representando pequenos números inteiros (na faixa de -128 a +127). O
tipo unsigned char seria utilizado para indicar que a variável estaria armazenando
valores inteiros exclusivamente positivos (sem sinal) na faixa de 0 a 255.

Tabela 2.1 – Tipos de dados em C


Tipo Tamanho (Byte) Valor inicial Valor final
char 1 -128 +127
unsigned char 1 0 255
int 2 -32.768 +32.767
unsigned int 2 0 65535
long int 4 -2.147.483.648 +2.147.483.647
unsigned long int 4 0 4.294.264.295
short int 2 -32.768 +32.767
unsigned short int 2 0 65535
float 4 -3.4E-38 +3.4E+38
double 8 -1.7E-308 +1.7E+308
long double 10 -3.4E-4932 +3.4E+4932

O tipo int também pode ser qualificado. Um tipo unsigned int indica
que a variável apenas armazenará valores positivos. Um tipo short int indica que
(caso seja possível) o compilador deverá usar um número menor de bytes para

12
representar o valor numérico - usualmente, dois bytes são alocados para este tipo. Uma
variável do tipo long int indica que a representação mais longa de um inteiro deve
ser utilizada, sendo que usualmente quatro bytes são reservados para variáveis deste tipo.
Estas dimensões de variáveis denotam apenas uma situação usual definida por
boa parte dos compiladores, sendo que não há nenhuma garantia quanto a isto. A única
coisa que se pode afirmar com relação à dimensão de inteiros em C é que uma variável
do tipo short int não terá um número maior de bits em sua representação do que
uma variável do tipo long int.
Valores com representação em ponto flutuante (reais) são representados em C
por meio do uso do ponto decimal, como em 1.5 para representar o valor um e meio.
A notação exponencial também pode ser usada, como em 1.2345e-6 ou em
0.12E3.
Caracteres ASCII são denotados entre aspas simples, tais como ’A’. Cada
caractere ASCII corresponde também a uma representação binária usada internamente.
Por exemplo, o caractere ASCII A equivale a uma seqüência de bits que corresponde ao
valor hexadecimal 41H ou decimal 65. Os valores definidos para os caracteres ASCII
são apresentados na tabela localizada no anexo A.
Além dos caracteres alfanuméricos e de pontuação, que podem ser representados
em uma função diretamente pelo símbolo correspondente entre aspas, C também define
representações para caracteres especiais de controle do código ASCII por meio de
seqüências de escapes iniciados pelo símbolo \ (contrabarra). As principais seqüências
são encontradas na tabela a seguir (tab. 2.2).

Tabela 2.2 – Caracteres de controle


Caractere de Controle Finalidade
\n Nova linha
\t Tabulação
\b Retrocesso
\r Retorno de carro
\f Salto de página
\a Sinal sonoro
\\ Contra barra
\’ Apóstrofo
\” Aspas
\0 O caractere nulo

2.1 Componentes básicos de um computador

Apesar da existência de uma grande diversidade em termos de arquiteturas de


computadores, pode-se enumerar, em um ponto de vista mais genérico os componentes
básicos desta classe de equipamentos.
A figura a seguir (fig 2.1) apresenta um esquema de um computador, destacando
os elementos que o compõem. Apesar da grande evolução ocorrida na área de
informática desde o aparecimento dos primeiros computadores, o esquema apresentado
na figura seguinte (fig 2.1) pode ser utilizado tanto para descrever um sistema compu-
tacional atual como os computadores da década de 40, projetados por engenheiros como

13
John Von Neuman. Os parágrafos que seguem apresentam os componentes principais de
um computador genérico.

2.1.1 Unidade Lógica e Aritmética

O primeiro componente essencial num computador (ou sistema computacional) é


a Unidade Lógica e Aritmética (ALU), a qual, como o próprio nome indica, assume
todas as tarefas relacionadas às operações lógicas (ou, e, negação, etc...) e aritméticas
(adições, subtrações, etc...) a serem realizadas no contexto de uma tarefa realizada por
meio dos computadores.
Neste contexto, é importante observar a evolução que este elemento sofreu ao
longo dos anos e quais são os parâmetros a ele associados que influenciam no
desempenho global de um sistema computacional.
Um parâmetro importante é o tamanho da palavra processada pela unidade lógica
e aritmética, lembrando que o sistema de numeração adotado nas arquiteturas de
computadores é o binário, o qual tem como unidade básica de informação o bit, que
pode assumir os valores 0 ou 1. Quanto maior o tamanho da palavra manipulada pelo
microprocessador, maior é o seu potencial de cálculo e maior a precisão das operações
realizadas.
As primeiras CPU’s integradas num único chip, como por exemplo, o 4004
fabricado pela Intel em 1968 manipulava palavras (dados e instruções) expressas por 4
dígitos binários. Os microprocessadores mais recentes são capazes de manipular palavras
entre 32 bits (caso dos 486) e 64 bits (Pentium e Power PC).
A velocidade de cálculo é outro fator de peso para as arquiteturas de
computador, uma vez que ela será determinante para o tempo de resposta de um sistema
computacional com respeito à execução de uma dada aplicação.
A velocidade de cálculo está diretamente relacionada com a freqüência do relógio
que pilota o circuito da CPU como um todo.
Ainda relacionada com a ALU, é possível destacar a quantidade de operações
que ela suporta. Os primeiros processadores suportavam um conjunto relativamente
modesto de operações lógicas e aritméticas. Em particular, no que diz respeito às
operações aritméticas, os primeiros processadores suportavam apenas operações de
adição e subtração, sendo que as demais operações tinham de ser implementadas através
de seqüências destas operações básicas

2.1.2 Registradores

São dispositivos de alta velocidade, localizados fisicamente na UCP, para


armazenamento temporário de dados. O número de registradores varia em função da
arquitetura de cada processador. Alguns registradores são de uso específico e têm
propósitos especiais, enquanto outros são ditos de uso geral.

o contador de instruções (CI) ou program counter (PC) é o registrador respon-


sável pelo armazenamento de endereço da próxima instrução que a UCP deverá execu-
tar. Toda vez que a UCP executa uma instrução, o PC é atualizado com um novo
endereço;

14
o apontador da pilha (AP) ou stack pointer (SP) é o registrador que contém o
endereço de memória do topo da pilha, que é a estrutura onde o sistema mantém
informações sobre tarefas que estavam sendo processadas, e tiveram que ser
interrompidas por algum motivo.

2.1.3 Memória

Todo computador é dotado de uma quantidade (que pode variar de máquina para
máquina) de memória a qual se constitui de um conjunto de circuitos capazes de
armazenar (temporariamente ou por longos períodos de tempo) as unidades de dados e
os programas a serem executados pela máquina. Nos computadores de uso geral, é
possível encontrar diferentes denominações para as diferentes categorias de memória que
neles são encontradas:

a memória principal, ou memória de trabalho, onde normalmente devem estar


armazenados os programas e dados a serem manipulados pelo processador;

a memória secundária que permitem armazenar uma maior quantidade de dados


e instruções por um período de tempo mais longo; os discos rígidos são os exemplos
mais evidentes de memória secundária de um computador, mas podem ser citados outros
dispositivos como CD-ROM, DVD, cartões de memória, etc;

a memória cache, que se constitui de uma pequena porção de memória com


curto tempo de resposta, normalmente integrada aos processadores e que permite
incrementar o desempenho durante a realização de um programa.

UCP
R
e
g
UC i
s
t
r
a
d
o
r
ULA e
s

Dispositivos de Armazenamento Dispositivos de


Entrada Saída

Figura 2.1 - Composição básica de um computador.[Linhas tracejadas indicam


fluxo de controle e Linhas contínuas fluxo de dados]

Os circuitos de memória são normalmente subdivididos em pequenas unidades de


armazenamento denominadas palavras. Cada palavra é identificada no circuito por um

15
endereço único, o qual vai ser referenciado pelo processador no momento de consultar
ou alterar o seu conteúdo. Historicamente, cada palavra de memória permitia armazenar
8 dígitos binários (ou bits), o que introduziu o conceito de Byte como sendo uma palavra
de 8 bits, unidade até hoje utilizada para medir a capacidade de armazenamento das
memórias e o tamanho dos programas.
As quantidades de memória hoje são definidas em termos de KBytes
(QuiloBytes) que correspondem a 1024 Bytes ou (210 Bytes) e MBytes (MegaBytes),
que correspondem a 1024 KBytes ou (220 Bytes).

2.1.4 Bases

A representação de um número depende da base escolhida ou disponível na


máquina em uso. A base Decimal é a mais empregada atualmente. Na antiguidade, foram
utilizadas outras bases como a base 12, a base 60, etc. Já um computador opera na base
2. (Por quê?)

O que ocorre na interação entre o usuário (aplicação) e o computador?

O usuário passa seus dados no sistema decimal e estes são convertidos em binário
pelo computador. Os resultados numéricos obtidos no sistema binário são convertidos
para o sistema decimal e finalmente são transmitidos ao usuário.
A seguir os processos de conversão do sistema binário e decimal.

Binário para Decimal

(347)10 = 3 x 102 + 4 x 101 + 7 x 100

(10111)2 = 1 x 24 + 0 x 23 + 1 x 22 + 1 x 21 + 1 x 20

de modo geral, dado um número N na base b, na forma polinomial é:

N = an bn + an-1 bn-1 + ... + a2 b2 + a1 b1 + a0 b0

com esta representação pode-se facilmente converter um número do sistema binário para
o sistema decimal, por exemplo:

(10111)2 = 1 x 24 + 0 x 23 + 1 x 22 + 1 x 21 + 1 x 20

= 16 + 0 + 4 +2 +1 = (23)10

16
Decimal para Binário

Método da Divisão Sucessiva por 2.

(11)10 = (?)2

11 2

1 5 2

1 2 2

0 1

(1011)2

Representação de números

Como o computador armazena inteiros positivos?

Usando todos os bits disponíveis para armazenamento.


[0, b n-1]

Como o computador armazena inteiros negativos?

Sinal-magnitude

0 = ‘+’ e 1 = ‘-‘

Ex.:
1111, ..., 1000 = - 7, ..., - 0; 0000, ..., 0111 = 0, ..., 7;

Complemento da Base

Veja figura (fig. 2.2).

N = b n-1 - a
Ex.:
1000, ..., 1111 = - 8, ..., - 1; 0000, ..., 0111 = 0, ..., 7;

Como o computador armazena os caracteres? ?“não existe”?

Exercícios 1.1 – Sabe-se que os computadores têm a sua disposição apenas zeros e uns
para representarem os mais diversos dados. Abaixo vemos números em
notação decimal, normalmente utilizada pelo ser humano no seu
cotidiano. Converter esses números, em uma notação que o
computador possa trabalhar, ou seja, em números binários.

57d 79d 010d 33d 44d 67d 89d 204d 03d 99d 224d
27d 75d 101d 73d 94d 62d 19d 106d 13d 39d 020d

17
Exercícios 1.2 – Os números binários abaixo, que bem poderiam estar na memória de
um computador, estão representando quais números decimais?

11011010b 11101010b 01011101b 00100011b 11010001b


11011110b 10001010b 01110101b 00110011b 11010010b
10011010b 10101010b 01010111b 00110111b 11010100b

Figura 2.2 – Complemento de base (2)

2.1.5 Unidade de Controle

Este último componente de um sistema computacional tem, sem dúvida alguma, a


maior importância na operação de um computador, uma vez que é esta unidade quem
assume toda a tarefa de controle das ações a serem realizadas pelo computador,
comandando todos os demais componentes de sua arquitetura. É este elemento quem
deve garantir a correta execução dos programas e a utilização dos dados corretos nas
operações que as manipulam. É a unidade de controle que gerencia todos os eventos
associados às operações dos computadores, particularmente as chamadas interrupções.
Tanto a unidade de controle quanto a unidade lógica e aritmética são imple-
mentadas em um circuito único conhecido sob a sigla de CPU (Central Processing Unit)
ou por Unidade Central de Processamento, Microprocessador ou, simplesmente,
Processador.

2.2 Declarações de variáveis

Variáveis representam uma forma de identificar por um nome simbólico uma


região da memória que armazena um valor sendo utilizado por uma função. Em C, uma
variável deve estar associada a um dos tipos de dados presentes na tabela anterior (tab.
2.1).
Toda variável que for utilizada em uma função C deve ser previamente declarada.
A forma geral de uma declaração de variável é:

tipo nome_variavel;

ou

tipo nome_var1, nome_var2, ... ;

18
onde nome_var1, nome_var2, ... são variáveis de um mesmo tipo de dado.

Exemplos válidos de declaração de variáveis em C são:

int um_inteiro;
unsigned int outro_inteiro;
char c1, c2;
float salario;
double x, y;

Nomes de variáveis podem ser de qualquer tamanho, sendo que usualmente


nomes significativos devem ser utilizados, mas somente os 31 primeiros caracteres são
considerados. C faz distinção entre caracteres maiúsculos e caracteres minúsculos, de
forma que Salario é diferente de salario.
Há restrições aos nomes de variáveis. Palavras associadas a comandos e
definições da linguagem (tais como if, for e int) são reservadas, não podendo ser
utilizadas para o nome de variáveis. A lista de palavras reservadas em C é apresentada no
Apêndice A. O nome de uma variável pode conter letras, números e o sublinha ( _ ), mas
deve começar com uma letra.
Como se pode observar no exemplo acima diversas variáveis de um mesmo tipo
podem ser declaradas em um mesmo comando, sendo que o nome de cada variável neste
caso estaria separado por vírgulas.
Além disto, variáveis podem ser também inicializadas enquanto declaradas, como
em:

int a = 0, b = 20;
char c = ’X’;
long int d = 12345678;

2.2.1 Definindo contantes

Uma maneira de se declarar constantes é muito semelhante à declaração de


variáveis com inicialização, exceto que o seu valor permanecerá inalterado. Uma variável
cujo valor não será alterado pelo programa pode ser qualificada como const, como em:

const int NotaMaxima = 100;

Neste caso, a variável NotaMaxima não poderá ter seu valor alterado.
Evidentemente, variáveis deste tipo devem ser inicializadas no momento de sua
declaração.
Outra forma de declarar constantes simbólicas é por meio da cláusula #define
logo no início do programa.

#define MAX 1024


#define PI 3.1415926

2.2.2 Definindo variáveis globais

19
Variáveis globais são definidas acima da função main() e seguem a forma
abaixo:

short int soma;


char letra;

main () {

soma = 0;
letra = ‘a’;
...

Estas também podem ser inicializadas como mostra a função main() no mesmo
exemplo.

2.3 Expressões

Após a declaração das variáveis, o corpo de uma função é definido através dos
comandos que serão executados pela função. Estes comandos devem ser expressos sob a
forma de uma seqüência de expressões válidas da linguagem C.
Antes de mas nada, é interessante que se apresente a forma de se expressar
comentários em um programa C. Comentários em C são indicados pelos terminadores
/* (início de comentário) e */ (fim de comentário). Quaisquer caracteres entre estes
dois pares de símbolos são ignorados pelo compilador. Comentários em C não podem
ser aninhados, mas podem se estender por diversas linhas e podem começar em qualquer
coluna. Por exemplo,

/* Exemplo de
* comentario
*/

main(){

/* esta funcao que faz coisa alguma */


}
As expressões na linguagem C são sempre terminadas pelo símbolo ; (ponto e
vírgula). Uma expressão nula é constituída simplesmente pelo símbolo terminador.
Assim, o exemplo acima é equivalente a

/* Exemplo de
* comentario
*/
main(){

/* esta funcao nao faz coisa alguma */


;

20
2.3.1 Expressões Aritméticas

O comando de atribuição em C é indicado pelo símbolo =, como em:

main(){
int a, b, c;
a = 10; /* a recebe valor 10 */
b = c = a; /* b e c recebem o valor de a (10) */
}

Observe neste exemplo que a atribuição pode ser encadeada - na última linha da
função acima, c recebe inicialmente o valor da variável a, e então o valor de c será
atribuído à variável b.
Expressões aritméticas em C podem envolver os operadores binários (isto é,
operadores que tomam dois argumentos) de soma (+), subtração (-), multiplicação (*),
divisão (/), lista completa na tabela abaixo (tab 2.3). Valores negativos são indicados
pelo operador unário -. Adicionalmente, para operações envolvendo valores inteiros são
definidos os operadores de resto da divisão inteira ou módulo (%), incremento (++) e
decremento (--). Por exemplo,

main(){
int a = 10, b, c, d;
b = 2 * a; /* b = 20 */
a++; /* a = a+1 (11) */
c = b / a; /* divisao inteira: c = 1 */
d = b % a; /* resto da divisao: d = 9 */
}

Cada um dos operadores de incremento e decremento tem duas formas de uso,


dependendo se eles ocorrem antes do nome da variável (pré-incremento ou pré-
decremento) ou depois do nome da variável (pós-incremento ou pós-decremento). No
caso do exemplo acima, onde o operador de incremento ocorre de forma isolada em uma
expressão (sozinho na linha), as duas formas possíveis são equivalentes. A diferença
entre eles ocorre quando estes operadores são combinados com outras operações. No
exemplo acima, as linhas de atribuição à b e incremento de a poderiam ser combinados
em uma única expressão,

b = 2*(a++); /* b recebe 2*a e entao a recebe a+1 */

Observe como esta expressão é diferente de

b = 2*(++a); /* a recebe a+1 e entao b recebe 2*a */

Na prática, os parênteses nas duas expressões acima poderiam ser omitidos uma
vez que a precedência do operador de incremento é maior que da multiplicação, ou seja,
o incremento será avaliado primeiro. A subseção 2.3.3 apresenta a ordem de avaliação
para todos os operadores da linguagem C (tab. 2.7).

21
Tabela 2.3 – Operadores aritméticos
Símbolo Significado
+ adição
* multiplicação
- subtração ou inversor do sinal
/ divisão
% resto da divisão de inteiros
++ incremento
-- decremento
= atribuição
+=, -=, *=, /= formas compactas para operações aritméticas

A linguagem C tem também uma forma compacta de representar expressões na


forma:

var = var op (expr);

onde uma mesma variável var aparece nos dois lados de um comando de atribuição. A
forma compacta é:

var op= expr;

Por exemplo,

a += b; /* equivale a a = a+b */
c *= 2; /* equivale a c = c*2 */

O operador % trabalha somente com operadores int. Já a / (barra) funciona para


int e float. Mas esteja atento, pois r = 5/2 será igual a 2, mesmo que r tenha
sido declarado como float. Esteja certo de especificar r = 5/2.0 ou r = 5.0/2,
ou ainda melhor r = 5.0/2.0. Se preferir, r = (float) 5/2;.

A linguagem C oferece também operadores que trabalham sobre a representação


binária de valores inteiros e caracteres. Estes operadores são apresentados na tabela (tab.
2.4) a seguir.

Tabela 2.4 – Operadores bit a bit


Símbolo Significado
& and bit a bit (mascara de 0’s)
| or bit a bit (mascara de 1’s)
^ xor bit a bit
<< deslocamento de bits à esquerda (* por 2)
>> deslocamento de bits à direita (/ por 2)
~ complemento (inverte cada bit)

Estes operadores tomam dois argumentos exceto pelo operador ~, que é unário.
Ex.:

22
a = a & 0xDF; /* Dec 223, converte a para maiúsculo */
x = y >> 3; /* divide por 8 */
x = y << 2; /* multiplica por 4 */

2.3.2 Expressões Condicionais

Um tipo muito importante de expressão em C é a expressão condicional, cujo


resultado é um valor que será interpretado como falso ou verdadeiro. Como a linguagem
C não suporta diretamente o tipo de dado booleano, ela trabalha com representações
inteiras para denotar estes valores. O resultado de uma expressão condicional é um valor
inteiro que será interpretado como falso quando o valor resultante da expressão é igual a
0, e como verdadeiro quando o valor resultante é diferente de 0.
Assim, qualquer expressão inteira pode ser interpretada como uma expressão
condicional. A situação mais comum, entretanto, é ter uma expressão condicional
comparando valores através dos operadores relacionais. Os operadores relacionais (tab.
2.5) em C são:

Tabela 2.5 – Operadores relacionais


Operador Operação
== igual a
!= diferente de
< menor que
> maior que
<= menor que ou igual a
>= maior que ou igual a

Observe que o operador de igualdade é ==, e não = que é o símbolo de


atribuição conforme mostra a tabela anterior (tab. 2.3). Esta é uma causa comum de
erros para programadores que estão acostumados com outras linguagens onde = é um
operador relacional.
Expressões condicionais elementares (comparando duas variáveis ou uma
variável e uma constante) podem ser combinadas para formar expressões complexas
através do uso de operadores booleanos. Estes operadores (tab. 2.6) são:

Tabela 2.6 – Operadores lógicos


Operador Operação
&& conjunção (and)
|| disjunção (or)
! negação (not)

O operador && (and) resulta verdadeiro (tab. 2.7) quando as duas expressões
envolvidas são verdadeiras (ou diferente de 0). O operador || (or) resulta verdadeiro
quando pelo menos uma das duas expressões envolvidas é verdadeira. Além destes dois
conectores binários, há também o operador unário de negação, !, que resulta falso
quando a expressão envolvida é verdadeira (diferente de 0) ou resulta verdadeiro quando
a expressão envolvida é falsa (igual a 0) .

23
Tabela 2.7 – Tabela verdade dos operadores lógicos &&, || e !.
A B Operador && Operador || Operador ! (A)
1 1 1 1 0
1 0 0 1 0
0 1 0 1 1
0 0 0 0 1

Expressões lógicas complexas, envolvendo diversos conectores, são avaliadas da


esquerda para a direita. Além disto, && tem precedência maior que ||, e ambos têm
precedência menor que os operadores lógicos relacionais e de igualdade. Entretanto,
recomenda-se sempre a utilização de parênteses em expressões para tornar claro quais
operações são desejadas. A exceção a esta regra ocorre quando um número excessivo de
parênteses pode dificultar ainda mais a compreensão da expressão; em tais casos, o uso
das regras de precedência da linguagem pode facilitar o entendimento da expressão.

2.3.3 Prioridade de operadores

Durante a execução de uma expressão que envolve vários operadores, é


necessário existir certas prioridades, caso contrário pode-se obter valores que não
representam o resultado esperado. A linguagem C utiliza as seguintes prioridades de
operadores (tab. 2.8):

Tabela 2.8 – Prioridade de operadores


Alta prioridade
( ) [ ] -> .
! - * & sizeof (type) ++ -- da direita para esquerda
* / %
+ -
<< >>
< <= >= >
== !=
&
^
|
&&
||
?: da direita para esquerda
= += -= etc. da direita para esquerda
, vírgula
Baixa prioridade

2.4 Exercícios propostos

Exercício 2.3 – Entre com dois números e dê como resultado a soma, a média e o
somatório dos quadrados dos números.

24
Exercício 2.4 – Escreva um programa que mostre o maior e o menor valor de um
conjunto de 10 valores inteiros fornecidos.

Exercício 2.5 – Escreva um programa que leia um número fracionário representado um


número de grau Celsius e exiba como um número fracionário à
temperatura equivalente em Fahrenheit. Mostre o resultado como
segue:

100.0 graus Celsius convertido para 212.0 graus Fahrenheit.

Exercício 2.6 – Escreva um programa para ler um número de unidade de comprimento


(um fracionário) e mostre a área do círculo deste raio. Assuma com
valor do pi 3.14159 (uma apropriada declaração deve ser dado a esta
constante). A saída deveria ter a seguinte forma:

A área do círculo de raio ___ unidades e ___ unidades.

Se você desejar melhorar este código, exiba a mensagem: Erro: valores negativos
não são permitidos. Se o valor de estrada for negativo.

Exercício 2.7 – Dada uma entrada de um número de centímetros em fracionário,


mostre o equivalente em número de pés e polegadas (fracionário, 1
decimal), isto é, uma precisão de uma casa decimal. Assuma 2.54
centímetros por polegada e 12 polegadas por pé.

Se a entrada for 333.3, a saída deveria ser:

333.33 centímetros e 10 pés ou 11.2 polegadas.

Exercício 2.8 – Dada uma entrada de um número de segundos, exibir como saída o
equivalente em horas, minutos e segundos. Recomenda-se o formato
de saída alguma coisa como:

7322 segundos e equivalente a 2 horas, 2 minutos e 2 segundos.

Exercício 2.9 – Faça um programa que, dada a área de um círculo, calcule o perímetro
da circunferência que o limita. Use precisão simples. A = π r2, portanto
r = raiz (A / π) e P = 2 π r.

Exercício 2.10 – Faça um programa que determine o volume de uma esfera, sendo dado
o respectivo raio. (V = 4 / 3 π r3). Repare que, em C, a divisão de 4
por 3 dá um valor inteiro se não tomar medidas adequadas.

Exercício 2.11 – Faça um programa que determine se um ano introduzido pelo usuário é
ou não bissexto. Um ano é bissexto se for múltiplo de 4 sem ser de 100
ou se for múltiplo de 400.

25
3 O controle do fluxo de execução

C é uma linguagem que suporta a programação estruturada, ou seja, permite


agrupar comandos na forma de seqüência, seleção e repetição.

3.1 Seqüências

Uma seqüência de comandos em uma função C é denotada simplesmente como


uma seqüência de expressões, como exemplificado na figura a seguir (fig. 3.1). Há duas
possibilidades: usar comandos isolados (a forma mais utilizada) ou usar uma única
expressão com a seqüência de comandos separados pelo operador , (vírgula).

Figura 3.1 – Seqüência em C - Fluxograma.

Formas equivalentes em C:

a = 10;
b = a*5;
c = a+b;

ou

a=10, b=a*5, c=a+b;

26
3.2 If’s

A seguir mostram-se algumas partes de programas com exemplos de uso do


comando if:

Exemplo 3.1 - Validando a entrada de um inteiro que representa um dia do mês.

...
scanf("%d", &dia);
if (dia > 31 && dia < 1)
printf("Dia invalido\n");
...

Exemplo 3.2 - Teste com um número inteiro, indicando se este é positivo ou negativo.

...
scanf("%d", &numero);
if (numero >= 0)
printf("Numero positivo\n");
else
printf("Numero negativo\n");
...

Exemplo 3.3 - Determinando a alíquota a ser calculada sobre a variável fracionária


salario.

...
scanf("%f", &salario);
if (salario < 1280.00)
{
printf("Aliquota de imposto = 0.1\n");
imposto = salario * 0.1;
}
else
{
printf("Aliquota de imposto = 0.25\n");
imposto = salario * 0.25;
}
...

Uma construção que pode aparecer são os comandos if's aninhados ou


encaixados, cuja forma geral é a seguinte:

if (expressao)
comando;
else if (expressao)
comando;
else if (expressao)
comando;
...
else comando;

27
Exemplo 3.4 - O programa, Calculadora Simples, a seguir mostra um exemplo com
if's encaixados e aninhados.

#include <stdio.h>
#include <conio.h>

main(){

float num1,num2,res;
char oper;

printf("\nCalculadora simples\n");
printf("Por favor entre com os dois operandos.\n");
scanf("%f %f", &num1, &num2);
printf("%f %f\n", num1, num2);
printf("Qual a operacao \n");
oper = getch();
printf("A operacao e %c\n", oper);

if (oper == '+')
res = num1 + num2;
else if (oper == '-')
res = num1 - num2;
else if (oper == '*')
res = num1 * num2;
else if (oper == '/')
if (num2 == 0.0){
printf("Divisao por 0 invalida");
getch();
return(1);
}
else res = num1 / num2;
else{
printf("Operacao invalida!\n");
getch();
return(1);
}

printf("O resultado da %c vale %f.\n", oper, res);


getch();
return(0);
}

A construção de seleção IF-THEN-ELSE é expressa em C na forma if ...


else (fig. 3.2). Após a palavra-chave if deve haver uma expressão condicional entre
parênteses. Se a expressão for avaliada como verdadeira, então a expressão sob o if
será realizada; se for falsa, a expressão sob o else será executada.
Nesta figura (fig. 3.2), introduz-se o conceito de expressão composta, ou seja, a
expressão da parte else deste exemplo é na verdade um bloco contendo diversas
expressões. Neste caso, o bloco de comandos que deve ser executado nesta condição
deve ser delimitado por chaves { e }. Algumas observações adicionais relevantes com
relação a este comando são:

28
1. Em C, há diferenças entre letras minúsculas e maiúsculas. Como todos
os comandos em C, as palavras chaves deste comando estão em letras
minúsculas. Assim, as formas IF (ou If ou iF) não são formas válidas
em C para denotar o comando if.
2. Ao contrário do que ocorre em Pascal ou FORTRAN, a palavra then
não faz parte da sintaxe deste comando em C.
3. A cláusula else pode ser omitida quando a expressão a executar na
condição falsa for nula.
4. No caso de haver mais de um if que possa ser associado a uma
cláusula else, esta será associada ao comando if precedente mais
próximo.

Figura 3.2 – Seleção com if ... else em C.

Forma equivalente em C:

if (a == 10)
b = b-c;
else {
c = a*10;
b = b+a;
}

3.3 Switch

A construção estruturada de seleção CASE-SELECT é suportada em C pelo


comando switch ... case (fig. 3.3). Neste caso, após a palavra-chave switch
deve haver uma variável do tipo inteiro ou caractere entre parênteses. Após a variável,
deve haver uma lista de casos que devem ser considerados, cada caso iniciando com a
palavra-chave case seguida por um valor ou uma expressão inteira.
O comando if, em todas suas formas, é suficiente para resolver problemas de
seleção de comandos. Porém em alguns casos, como no exemplo anterior (ex. 3.4) o
programa se torna mais trabalhoso para ser escrito. O comando switch facilita a
escrita de partes de programa em que a seleção deve ser feita entre várias alternativas.
A forma geral do comando switch é a seguinte:

switch (expressao) {

29
case constante1:
comandos;
break;

case constante2:
comandos;
break;

case constante3:
comandos;
break;

...

default:
comandos;
}

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

1. A expressão é avaliada;
2. O resultado da expressão é comparado com os valores das constantes
que aparecem nos comandos case;
3. Quando o resultado da expressão for igual a uma das constantes, a
execução se inicia a partir do comando associado com esta constante.
A execução continua com a execução de todos os comandos até o fim
do comando switch, ou até que um comando break seja encontrado;
4. Caso não ocorra nenhuma coincidência o comando default é
executado. O comando default é opcional e se ele não aparecer
nenhum comando será executado;
5. O comando break é um dos comandos de desvio da linguagem C. O
break se usa dentro do comando switch para interromper a execução e
pular para o comando seguinte ao comando switch;

Há alguns pontos importantes que devem ser mencionados sobre o comando


switch:

1. O resultado da expressão deve ser um tipo compatível com um inteiro,


isto é, expressões com resultados tipo char também podem ser usadas;
2. Notar que caso não apareça um comando de desvio todas as instruções
seguintes ao teste case que teve sucesso serão executadas, mesmo as
que estejam relacionadas com outros testes case;
3. O comando switch só pode testar igualdade, veja exemplo a seguir (ex.
3.5);
4. Não podem aparecer duas constantes iguais em um case;

Exemplo 3.5 - O programa, Calculadora Simples 2, mostra um exemplo com a


utilização do comando switch.

#include <stdio.h>

30
#include <conio.h>

main(){

float num1,num2,res;
char oper;

printf("\nCalculadora simples\n");
printf("Por favor entre com os dois operandos.\n");
scanf("%f %f", &num1, &num2);
printf("%f %f\n", num1, num2);
printf("Qual a operacao \n");
oper = getch();
printf("A operacao e %c\n", oper);

switch (oper){

case ‘+’ :
res = num1 + num2;
break;
case ‘-’ :
res = num1 - num2;
break;
case ‘*’ :
case ‘x’ :
case ‘X’ :
res = num1 * num2;
break;
case ‘/’ :
if (num2 == 0.0){
printf("Divisao por 0 invalida");
getch();
return(1);
}
else{
res = num1 / num2;
break;
}
default:{
printf("Operacao invalida!\n");
getch();
return(1);
}
}

printf("O resultado da %c vale %f.\n", oper, res);


getch();
return(0);
}

31
Figura 3.3 – Seleção usando switch ... case em C.

Forma equivalente em C:

switch (a){

case 10: a = a+1;


break;
case 20: a = a+2;
break;
case 100:a = a+5;
break;
default: a = a+10;

Observe que o conjunto de ações associado a cada caso encerra-se com a


palavra-chave break.
Neste exemplo (ex. 3.5), a variável a pode ser do tipo int ou char. A
palavra-chave especial default indica que a ação deve ser tomada quando a variável
assume um valor que não foi previsto em nenhum dos casos. Assim como a condição
else no comando if é opcional, a condição default também é opcional para o
switch-case. Observe também a importância da palavra-chave break para
delimitar o escopo da ação de cada caso - fossem omitidas as ocorrências de break no
exemplo anterior (ex. 3.5), a semântica associada ao comando seria essencialmente
diferente (fig. 3.4).

32
Figura 3.4 – Seleção usando switch ... case em C, omitindo-se o
break.

Forma equivalente em C:

switch (a){

case 10: a = a+1;


case 20: a = a+2;
case 100:a = a+5;
default: a = a+10;

3.4 Comando ternário

O comando ternário tem este nome porque necessita de três operandos para ser
avaliado. O comando ternário tem a seguinte forma:

expressao1 ? expressao2 : expressao3

Para avaliar o resultado da expressão primeiro expressão1 é avaliada. Caso


este resultado seja correspondente ao valor verdadeiro então o resultado da
expressão será igual ao valor da expressao2, caso contrário a expressão3 é
avaliada e se torna o resultado.

Exemplo 3.6 - O programa determina o maior número entre dois valores fornecidos
pelo usuário usando o comando ternário.

#include <stdio.h>

33
#include <conio.h>
main(){

float num1,num2,maior;

printf("Por favor entre com os dois numeros.\n");


scanf("%f %f", &num1, &num2);

maior = (num1 > num2) ? num1 : num2;


printf("O maior dos numeros lidos e %f.\n", maior);

getch();
return(0);
}

3.5 Repetições

Comandos de repetição em C são suportados em três formas distintas. A primeira


forma é while, cuja construção equivale ao comando estruturado WHILE-DO,
enquanto que a segunda forma equivale ao comando estruturado DO-WHILE. Por fim,
tem-se o comando for que equivale a estrutura FOR-DO.

3.5.1 Comando while

O comando while tem a seguinte forma geral:

while (expressao)
comando;

A expressão pode assumir o valor falso (igual a 0) ou verdadeiro


(diferente de 0). Os passos para execução do comando são os seguintes:

1. A expressão é avaliada;
2. Se a expressão for verdadeira então o comando é executado, caso
contrário a execução do comando é terminada;
3. Voltar para o passo 1.
4. Uma característica do comando while, como pode ser visto dos
passos acima, é que o comando pode não ser executado.

Figura 3.5 – Repetição usando while ... em C.

34
Forma equivalente em C:

while (a != 10)
a = a+1;

Exemplo 3.7 - O programa que mostra os números de 1 a 100 usando o comando


while.

#include <stdio.h>
#include <conio.h>

main(){

int i;

i = 1;

while (i <= 100){


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

getch();
return(0);
}

O trecho acima (ex. 3.7) escrito de forma mais compacta fica:

i = 1;

while (i <= 100)


printf("Numero %d\n", i++);
...

A expressão do comando pode incluir chamada de função. Lembrar que qualquer


atribuição entre parênteses é considerada como uma expressão que tem o valor da
atribuição sendo feita. Por exemplo, o trecho abaixo (ex 3.8) repete um bloco de
comandos enquanto o usuário usar a tecla 'c' para continuar, qualquer outra tecla o bloco
é interrompido.

Exemplo 3.8 - O programa que o usuário pode controlar o número de vezes os


comandos do while irão ser executados.

#include <stdio.h>
#include <conio.h>

main(){
char c;

printf("Tecle c para continuar.\n");

35
while ((c=getche()) == 'c'){
printf("\nNao Acabou.\n");
}

printf("\nAcabou.\n");
getch();
return(0);
}

3.5.2 Comando do while

O comando do while tem a seguinte forma geral:

do
comando;
while (expressao);

Observar que neste comando a expressão de teste está após a execução do


comando, portanto o comando é executado pelo menos uma vez. A execução do
comando segue os seguintes passos:

1. Executa o comando;
2. Avalia a expressão;
3. Se a expressão for verdadeira então volta para o passo 1, caso
contrário interrompe-se o do-while.

Figura 3.6 – Repetição usando do while ... em C.

Forma equivalente em C:

do
a = a+1;
while (a != 10);

Exemplo 3.9 - O programa que mostra os números de 1 a 100 usando o comando do


while.

#include <stdio.h>

36
#include <conio.h>

main(){

int i;

i = 1;

do{
printf("Numero %d\n", i);
i++;
}while (i <= 100);

getch();
return(0);
}

3.5.3 Comando for

Este comando aparece em várias linguagens de programação, mas na linguagem


C ele apresenta um grau maior de flexibilidade. A idéia básica do comando for é a
seguinte:

A forma geral do comando for é a seguinte:

for (expressao1; expressao2; expressao3)


comando;

é equivalente a

expressao1;

while (expressao2){
comando;
expressao3;
}

As três expressões geralmente têm os seguintes significados:

A expressao1 é utilizada para inicializar a variável de controle do laço;


A expressao2 é um teste que controla o fim do laço;
A expressao3 normalmente faz um incremento ou decremento da variável de
controle.

A execução do comando for segue os seguintes passos:

1. A expressao1 é avaliada;
2. A expressao2 é avaliada para determinar se o comando deve ser
executado;

37
3. Se o resultado da expressao2 for verdadeiro o comando é
executado, caso contrário o laço é terminado;
4. A expressao3 é avaliada;
5. Voltar para o passo 2.

Figura 3.7 – Repetição usando for ... em C.

Forma equivalente em C:

for (a = 0; a < MAX; ++a)


b = 2 * b;

Exemplo 3.10 - O programa que mostra os números de 0 a 100 usando o comando


for.

#include <stdio.h>
#include <conio.h>

main(){

int i;

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


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

getch();
return(0);
}

É possível omitir qualquer uma das expressões. Por exemplo, se a expressao2


for omitida o programa assume que ela é sempre verdade de modo que o laço só termina
com um comando de desvio como o break.

Exemplo 3.11 - O programa que mostra os números de 0 a 5 usando o comando for


em conjunto com o comando break.

#include <stdio.h>
#include <conio.h>

main(){

38
int i;

for (i = 0; ; i++){
printf("Numero %d\n", i);

if (i == 5) break;
}

getch();
return(0);
}

Exemplo 3.12 - O programa que mostra a tabuada da multiplicação dos números de 1 a


10.

#include <stdio.h>
#include <conio.h>

main(){

int i, j;

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


printf("Tabuada de %d\n", i);
getch();
for (j = 1;j < 10; j++)
printf("\n%d x%d = %d\n", i, j, i * j);
}

getch();
return(0);
}

Exemplo 3.13 - O programa que mostra como se pode calcular o fatorial de um


número usando o comando for.

#include <stdio.h>
#include <conio.h>

int main(){

unsigned int numero, i, fat = 1;

printf("\nEntre com um numero positivo.");


scanf("%u", &numero);

for (i = numero; i > 1; i--)


fat = fat * i;

printf("O fatorial de %u vale %u.", numero, fat);

getch();
return(0);
}

39
Laços for com mais de um comando por expressão

Outra possibilidade que o comando for em C permite e a inclusão de mais de um


comando, separados por vírgulas, nas expressões. O exemplo a seguir (ex. 3.13) mostra
um exemplo de uso de comando for com vários comandos nas expressões.

Exemplo 3.13 - O programa que usa seqüência nas expressões do comando for.

#include <stdio.h>
#include <conio.h>

void main(){

int i,j;

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


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

getch();
}

Laços for com testes com outras variáveis

A expressão de controle não precisa necessariamente envolver somente um teste


com a variável que controla o laço. O teste de final do laço pode ser qualquer expressão
relacional ou lógica. No exemplo seguinte (ex. 3.14) o laço pode terminar porque a
variável de controle já chegou ao seu valor limite ou foi batida a tecla '*'. Obs.:
getchar() necessitado do <enter> para terminar.

Exemplo 3.14 - O programa que testa mais de uma expressão no comando for.

#include <stdio.h>
#include <conio.h>
#include <ctype.h>

void main(){

char c;
int i;

for (i = 0 ;(i < 5) && (c = getchar()) != '*';i++){


printf("%c\n", toupper(c));
c = getchar();
}

getch();
}

Laços for com expressões faltando

40
Um outro ponto importante do for é que nem todas as expressões precisam estar
presentes. No exemplo na seqüência (ex. 3.15) a variável de controle não é
incrementada. A única maneira de programa terminar é o usuário digitar o número -1.

Exemplo 3.15 - O programa que depende do usuário para terminar usando o comando
for.

#include <stdio.h>
#include <conio.h>

void main(){

int i;

for (i = 0 ;i != -1; ){
printf("%d\n", i);
scanf("%d", &i);
}

getch();
}

Laço infinito

Uma construção muito utilizada é o laço infinito. No laço infinito o programa


para quando se usa o comando break. No exemplo abaixo (ex. 3.16) o programa só
para quando for digitada ou a tecla 's' ou 'S'.

Exemplo 3.16 - O programa que depende do usuário para terminar usando o comando
for.

#include <stdio.h>
#include <conio.h>
#include <ctype.h>
void main(){

int c;

for ( ; ; ){
printf("\nVoce deseja parar?\n");
c = getche();
if (toupper(c) == ‘S’) break;
}

getch();
}

Laços aninhados

Uma importante construção aparece quando colocamos como comando a ser


repetido um outro comando for. Esta construção aparece quando estamos trabalhando

41
com matrizes. O exemplo anterior (ex. 3.12) mostra um programa que imprime uma
tabuada.

Desviando laços

Qualquer que seja a forma usada para indicar o comando de repetição - while,
do while ou for - há duas formas de se desviar a seqüência de execução do padrão
do comando. A primeira forma, continue, serve para indicar o fim prematuro de uma
iteração. A outra forma de interrupção de um comando de repetição é o comando
break, que indica o fim prematuro de todo o comando de iteração.

Como por exemplo:

...

for (a = 0; a < MAX; ++a){


if (b == 0){
b = a;
continue;
}
c = c / b;
b = b – 1 ;
}

...

se a linha com o comando continue for executada, o valor de a será incrementado e


então o teste da iteração será reavaliado para definir a continuidade ou não do laço de
repetição. Já no exemplo abaixo,

...

for (a = 0; a < MAX; ++a){


if (b == 0)
break;
c = c / b;
b = b - 1;
}

...

quando (se) b assumir o valor 0, o laço será simplesmente interrompido.

3.6 Exercícios propostos

Exercício 3.1 – Faça um programa que leia vários números e informe a média dos
números fornecidos. Quando o usuário digitar 0 (zero) o programa
termina.

42
Exercício 3.2 – Faça um programa que calcule que determine as raízes de uma equação
do tipo ax2 + bx + c = 0. Sendo ∆ = b2 - 4ac, as raízes da equação são
dadas por:

Se a = 0, o programa não deve efetuar nenhum cálculo, mas visualizar uma


mensagem adequada.

Exercício 3.3 – Faça um programa que determine se um ano introduzido pelo usuário é
ou não bissexto. Um ano é bissexto se for múltiplo de 4 sem ser de 100
ou se for múltiplo de 400.

Exercício 3.4 – Faça um programa que calcule a soma de dez números inteiros dados
pelo usuário.

Exercício 3.5 – Faça um programa que calcule a soma n números inteiros consecutivos
que começam num valor dado pelo usuário.

Exercício 3.6 – Ler um caractere do teclado e verificar se é um caractere de


pontuação: ’,’ ou ‘.’ ou ‘;’ ou ‘!’ ou ‘?’.

Exercício 3.7 – Verificar se um caractere lido do teclado é maiúsculo ou minúsculo


(entre ‘a’ e ‘z’ é minúsculo).

Exercício 3.8 – Ler uma string do teclado com a função gets(s) e imprimir o número de
ocorrências do caractere ‘a’.

Exercício 3.9 – Fazer maiúscula(s): transforma todas as letras minúsculas em


maiúsculas em s.

Exercício 3.10 – Escreva um programa que calcule x elevado a n. Assuma que n é um


valor inteiro.

Exercício 3.11 – Escreva um programa que exiba um menu com as opções "1-
multiplicar" e "2-somar", leia a opção desejada, leia dois valores,
execute a operação (utilizando o comando "if") e exiba o resultado.

43
Exercício 3.12 – Utilizando if's em escada, inclua, no programa do exercício anterior, as
opções "3-Subtrair" e "4-Dividir".

Exercício 3.13 – Simplifique os programas anteriores da seguinte forma:

Reescreva o programa do exercício 1 substituindo o comando "if" pelo


comando ternário.

Reescreva o programa do exercício 2 substituindo os if's em escada


pelo comando "switch".

Exercício 3.14 – Utilizando um laço "while" e o comando "break", escreva um


programa que exiba a mensagem “HA-HA-HA!! Você está preso” até
que a senha “FUI!!” seja digitada.

Exercício 3.15 – Utilizando um laço "for" dentro de outro, escreva um programa que
exiba as tabuadas de multiplicação dos números de 1 a 9.

Exercício 3.16 – Escreva um programa que tenha um número (inteiro) como entrada do
usuário e escreva como saída a seqüência de bits que forma esse
numero. Por exemplo, após ter digitado o número 10, a saída deve ser
0000000000001010.

Exercício 3.17 – Escreva um programa que imprima todos os números pares entre 0 e
50 e em seguida imprima todos os impares. Deixar um espaço entre os
números.

Exercício 3.18 – Escreva um programa que leia 10 números. O programa deve imprimir
a media, o maior e o menor deles. Obs: Os números devem ser entre 0
e 10.

Exercício 3.19 – Escreva um programa que leia 10 números. O programa deve imprimir
a media, o maior e o menor deles. Obs: Considere agora que os
números podem ser quaisquer.

Exercício 3.20 – Escreva um programa que exibe a tabela ascii.

Exercício 3.21 – Crie um programa para verificar se um número dado é primo.

Exercício 3.22 – Escreva um programa que leia um número inteiro do teclado e acha se
o número e primo ou não. O programa deve informar o menor divisor.

Exercício 3.23 – Escreva um programa que leia um número do teclado e ache todos os
seus divisores.

Exercício 3.24 – Escreva um programa que imprima a seqüência


"987654321876543217654321654321543214321321211", mas sem
imprimir nenhuma constante use apenas variáveis. Em outra linha
imprima as letras maiúsculas de A até Z (ABCD...Z).

44
Exercício 3.25 – Escreva um programa que conte de 100 a 999 (inclusive) e exiba, um
por linha, o produto dois três dígitos dos números. Por exemplo,
inicialmente o programa irá exibir:

0 (1*0*0)
0 (1*0*1)
0 (1*0*2)
(...)
0 (1*1*0)
1 (1*1*1)
2 (1*1*2)
até 9*9*9=729

Exercício 3.26 – Faça um programa que leia vários números e informar quantos estão
no intervalo de 50 e 100. Quando o usuário digitar 0 (zero) o
programa termina após mostrar a quantidade de números do intervalo.

Exercício 3.27 – Faça um programa que leia vários números e informar quantos estão
no intervalo de 50 e 100. Quando o usuário digitar 0 (zero) o
programa termina após mostrar a quantidade de números do intervalo.

Exercício 3.28 – Faça um programa que calcule n! (n factorial). Use a precisão dupla
para o cálculo de n!, porque, embora seja um valor inteiro, o seu
cálculo rapidamente conduz a valores elevados. O programa deve
ainda não aceitar valores de n que sejam negativos ou que originem um
cálculo de n! superior ao limite da precisão dupla (n > 170).

Exercício 3.29 – Faça um programa que leia dois valores inteiros e que visualize:
A mensagem "Valores iguais" se os valores lidos forem iguais;
A lista dos inteiros compreendidos entre os valores introduzidos
(inclusive) por ordem crescente. Coloque 10 valores em cada linha e
faça uma parada programada quando o visor estiver cheio. Considere
que uma linha pode conter até 80 caracteres e que o visor pode conter
até 25 linhas.

Exercício 3.30 – José tem 1,50m e cresce 2 centímetros por ano, enquanto Juca tem
1,10m e cresce 3 centímetros por ano. Faça um programa que calcule e
informe quantos anos seão necessários para que Juca seja maior que
José.

Exercício 3.31 – Faça um programa que calcule a fatura de energia elétrica para cada
consumidor. Leia o número do medidor, a quantidade de kWh
consumidor durante o mês e o tipo de consumidor. 1 – residencial,
preço R$ 0,34 por kWh, 2 – comercial, preço R$ 0,54 por kWh e 3 –
industrial, preço R$ 0,84 por kWh. Os dados devem ser lidos até que
seja fornecido um código 0 (zero) para um consumidor. Mostrar o
total da fatura do consumidor.

45
4 Funções

Em C, diferentemente de outras linguagens como Pascal, todas as ações ocorrem


dentro de funções. Na linguagem C não há conceito de um programa principal, o que
existe é uma função chamada main que é sempre a primeira a ser executada.

A forma geral de uma função em C é a seguinte:

tipo nome (tipo nome1, tipo nome2, ..., tipo nomeN){


declaração das variáveis
corpo da função
}

O tipo na definição da função especifica o tipo do resultado que será devolvido


ao final da execução da função. Caso nenhum tipo seja especificado o compilador
assume que um tipo inteiro é retornado. O tipo void (vazio) pode ser usado para
declarar funções que não retornam valor algum.
Há duas maneiras básicas de terminar a execução de uma função. Normalmente
usa-se o comando return para retornar o resultado da função. Portanto, quando o
comando

return (expressao) ;

for executado, o valor da expressão é devolvido para a função que chamou. Quando
não há valor para retornar o comando return não precisa ser usado e a função termina
quando a chave que indica o término do corpo da função é atingida.
O nome da função é qualquer identificador válido. A lista de
parâmetros é uma lista, separada por vírgulas, de variáveis com seus tipos
associados. É possível que existam funções que não tenham lista de parâmetros, mas
ainda assim é necessário que os parênteses sejam usados.
Os parâmetros são valores que a função recebe para realizar as tarefas para as
quais foi programada. Por exemplo, uma função que calcule a raiz quadrada de um
número do tipo float, deve declarar como parâmetro uma variável deste tipo para
receber o valor.
É importante notar que diferentemente de declarações de variáveis onde podemos
associar vários nomes de variáveis a uma declaração como em:

int a, dia, mes, i;

46
na lista de parâmetros é necessário associar um tipo a cada variável como no exemplo
abaixo:

float media (float n1, float n2, float n3)

Suponha que uma determinada função, A, deseje usar uma outra função, B. A
função A deve colocar no local desejado o nome da função B e a lista de valores que
deseja passar. Por exemplo, uma função que deseje usar a função media, cujo protótipo
foi definido acima, para calcular a média de três valores: nota1, nota2 e nota3, deve
escrever no local onde quer que a média seja calculada o seguinte comando:

resultado = media(nota1, nota2, nota3);

onde resultado é a variável que irá receber a média calculada.


É importante notar que os tipos e o número de parâmetros que aparecem na
declaração da função e na sua chamada devem estar na mesma ordem e ter tipos
equivalentes. Se os tipos são incompatíveis, o compilador não gera um erro, mas podem
ser gerados avisos na compilação e resultados estranhos.

4.1 Protótipos de funções

O padrão ANSI estendeu a declaração da função para permitir que o compilador


faça uma verificação mais rígida da compatibilidade entre os tipos que a função espera
receber e àqueles que são fornecidos.
Protótipos de funções ajudam a detectar erros antes que eles ocorram, impedindo
que funções sejam chamadas com argumentos inconsistentes.

A forma geral de definição de um protótipo é a seguinte:

tipo nome (tipo nome1, tipo nome2, ..., tipo nomeN);

Exemplo 4.1 - Programa para calcular a soma de dois números utilizando a declaração
de uma função e seu protótipo.

#include <stdio.h>
#include <conio.h>

/* Prototipo da funcao */
int soma (int a, int b);

/* Funcao Principal */
main(){
int x = 3, y = 4;

printf("\nSoma = %d", soma(x,y));

getch();
return (0);
}

47
/* Definicao da funcao */

int soma(int a, int b){

return (a + b);
}

4.2 Escopo de variáveis

Variáveis podem ser usadas dentro de uma função particular, e somente dentro
desta função, ou pode ocorrer que uma ou mais variáveis precisem ser acessíveis a
diversas funções diferentes. Por esta razão temos que definir onde as variáveis de um
programa podem ser definidas e a partir deste local inferir onde elas estarão disponíveis.
As variáveis podem ser declaradas basicamente em três lugares: dentro das
funções, fora de todas as funções e na lista de parâmetros das funções. As variáveis
dentro das funções são chamadas de variáveis locais, as que aparecem fora de todas as
funções são conhecidas como variáveis globais e aquelas que aparecem na lista de
parâmetros são os parâmetros formais.
É importante notar que em C todas as funções estão no mesmo nível, por isto é
não é possível definir uma função dentro de outra função.

4.2.1 Variáveis locais

As variáveis locais são aquelas declaradas dentro de uma função. Elas passam a
existir quando do início da execução do bloco de comandos ou função onde foram
definidas e são destruídas ao final da execução do bloco. Uma variável local só pode ser
referenciada, ou seja, usada, dentro das funções onde foram declaradas. Outro ponto
muito importante é que como as variáveis locais deixam de existir ao final da execução
da função, elas são invisíveis para outras funções do mesmo programa. O código que
define uma função e os seus dados são particulares a função.
O programa a seguir (ex. 4.2) exibe o uso de algumas variáveis locais. Na função
main tem-se cinco variáveis locais: numero, potencia, continua, para e
linha. A função Eleva possui somente a variável res, enquanto que
ParaMaiusculas não possui variável local. A única variável nesta função é c que é
um parâmetro.

Exemplo 4.2 - Programa para elevar um número qualquer a uma potência inteira
usando função. O programa deve terminar ao desejo do usuário.

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

#define SIM 1
#define NAO 0

int Eleva (int, int);


char ParaMaiuscula (char c);

48
main(){

int num, pot;


char continuar;
int parar;
char linha[80];

do{
puts("Entre com um numero");
gets(linha); num = atoi(linha);
puts("Entre com a potencia");
gets(linha); pot = atoi(linha);
printf("\n%d Elevado a %d ", num, pot);
printf("eh = %d\n", Eleva(num, pot));
puts("Continuar? [S]im ou [N]ao? ");
continuar = getchar();
getchar();
continuar = ParaMaiuscula(continuar);
parar = continuar == 'S'? NAO : SIM;
}while (!parar);

return(0);
}

int Eleva(int a, int b){


int res = 1;

for ( ; b > 0; b--)


res *= a;
return (res);
}

char ParaMaiuscula (char c){


if ('a' <= c && c <= 'z')
c = c - 'a' + 'A';
return (c);
}

Alguns autores usam o termo variáveis automáticas para se referir as variáveis


locais. Em C existe a palavra chave auto que pode ser usada para declarar variáveis
locais. No entanto, como todas as variáveis locais são por definição automáticas
raramente se usa esta palavra chave.
Observa-se que um bloco de comandos ou escopo se inicia em um { e termina
em um }. O bloco de comandos mais usado para definir uma variável é a função. Todas
as variáveis que serão usadas dentro de um bloco de comandos precisam ser declaradas
antes do primeiro comando do bloco. Declarações de variáveis, incluindo sua
inicialização, podem vir logo após o abre chaves que inicia um bloco de comandos, não
somente o que começa uma função. O exemplo (ex. 4.3) a seguir ilustra este tipo de
declaração:

Exemplo 4.3 - Declarando variáveis usando o escopo de comandos C, neste caso um


comando for.

49
#include <stdio.h>
#include <conio.h>

main(){

int i;

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


int t;
scanf("%d", &t);
printf("%d\n", i*t);
}

getch();
return(0);
}

Existem algumas vantagens em se declarar variáveis dentro de blocos ou escopo.


Como as variáveis somente passam a existir quando o bloco passa a ser executado, o
programa ocupa menos espaço de memória. Isto porque se a execução do bloco for
condicional a variável pode nem ser alocada. Outra vantagem é que como a variável
somente existe dentro do bloco pode-se controlar melhor o uso da variável, evitando
erros de uso indevido da variável.

4.2.2 Variáveis estáticas

Outra classe de variáveis locais são as do tipo estáticas. Uma variável static
pode ser acessada da função na qual ela foi declarada, como uma variável local normal.
A diferença é que as variáveis estáticas não são destruídas ao término da função, ao invés
disso seu valor é preservado e torna-se disponível novamente quando da próxima
chamada da função. Variáveis estáticas são declaradas como as variáveis locais, mas a
declaração é precedida pela palavra static.

static int contador;

Variáveis estáticas podem ser inicializadas normalmente. A inicialização é


executada somente uma vez, quando o programa é iniciado.

4.2.3 Variáveis globais

As variáveis globais são definidas fora de qualquer função e são portanto


disponíveis para qualquer função. Este tipo de variável pode servir como um canal de
comunicação entre funções, uma maneira de transferir valores entre elas. Por exemplo, se
duas funções tem de partilhar dados mais uma não chama a outra, uma variável global
tem de ser usada.

50
4.2.4 Parâmetros formais

As variáveis que aparecem na lista de parâmetros da função são chamadas de


parâmetros formais da função. Eles são criados no início da execução da função e
destruídos ao seu final.
Parâmetros são valores que as funções recebem da função que a chamou.
Portanto, os parâmetros permitem que uma função passe valores para outra.
Normalmente os parâmetros são inicializados durante a chamada da função, pois para
isto que foram criadas. No entanto, as variáveis que atuam como parâmetros são iguais a
todas as outras e podem ser modificadas, operadas, etc, sem nenhuma restrição.

4.2.5 Passagem de parâmetros por valor

Parâmetros podem ser passados para funções de duas maneiras: passagem por
valor ou passagem por referência.
Na passagem por valor uma cópia do valor do argumento é passada para a
função. Neste caso a função que recebe este valor ao fazer modificações no parâmetro
não estará alterando o valor original que somente existe na função que chamou.

4.2.6 Passagem de parâmetros por referência

Na passagem por referência o que é passado para a função é o endereço do


parâmetro e portanto a função que recebe pode por meio do endereço modificar o valor
do argumento na função que chamou.
Para a passagem de parâmetros por referência é necessário o uso de ponteiros.
Este assunto será discutido nos próximos capítulos e portanto neste capítulo serão
apresentadas somente funções com passagem por valor.

4.2.7 O comando de returno

O comando return é usado para retornar o valor calculado para a função que a
chamou. Qualquer expressão pode aparecer no comando, que tem a seguinte forma
geral:

return (expressao) ;

A função que chamou é livre para ignorar o valor retornado. Além disso a função
pode não conter o comando e portanto nenhum valor é retornado e neste caso a função
termina quando o último comando da função é executado. Quando o comando return
não existe o valor de retorno é considerado indefinido. As funções que não retornam
valores devem ser declaradas como do tipo void.
É importante observar que funções que são declaradas com um tipo válido
podem ser incluídas em qualquer expressão válidas em C.

51
4.2.8 Funções sem retorno

As funções void são funções que não retornam qualquer valor e comportam-se,
por isso, de forma equivalente aos procedimentos do Pascal. Estas funções são definidas
tendo como tipo de retorno a palavra void. A terminação pode fazer-se simplesmente
pelo esgotar das instruções contidas na função ou por meio de uma instrução de
return sem a especificação de qualquer valor adicional. Geralmente, este último tipo
de terminação é usado no meio das instruções da função, como por exemplo, dentro de
um comando if.
A palavra void também deverá ser usada no lugar dos parâmetros se a função
não tiver nenhum (ex. 4.4).

Exemplo 4.4 - Programa para calcular o quadrado dos números de 1 a 10, utilizando
uma função sem retorno.

#include <stdio.h>
#include <conio.h>

void quadrados(void){

int k;

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


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

void main(void){

quadrados();

getch();
}

Na chamada de funções sem argumentos é sempre obrigatório utilizar parênteses,


sem nada lá dentro, como mostra o exemplo anterior (ex. 4.4).

4.3 Recursão

Funções em C podem ser usadas recursivamente, isto é, uma função pode


chamar-se a si mesma. É como procurar no dicionário a definição da palavra recursão e
encontrar o seguinte texto:

recursão: s.f. Veja a definição em recursão.

Um exemplo simples de função que pode ser usada com chamadas recursivas é o
fatorial de um número inteiro. O fatorial de um número pode ser definido como o
produto deste número pelo fatorial de seu predecessor, ou seja,

n! = n * (n-1)!

52
A função fatorial implementada sem recursão está ilustrada em um exemplo do
capítulo 3 (ex. 3.13).
A alternativa é uma função recursiva em que cada chamada da função que calcula
o fatorial chama a própria função fatorial. O exemplo (ex. 4.5) ilustrado abaixo mostra
como a função pode ser escrita recursivamente.

Exemplo 4.5 - Programa para calcular o fatorial de um número usando uma função
recursiva.

#include <stdio.h>
#include <conio.h>

unsigned int fat(unsigned int n);

main(){

unsigned int num;

printf("\nEntre com um numero positivo.");


scanf("%u", &num);
printf("Fatorial de %u vale %u.", num, fat(num));

getch();
return(0);
}

unsigned int fat(unsigned int n){


unsigned int fato;

if (n == 1)
return(1);
else
fato = n * fat(n-1);

return(fato);
}

Quando a função fat (fatorial) recursiva é chamada, primeiramente é verificado


se o número recebido como parâmetro vale 1. Neste caso a função retorna o valor 1,
caso contrário ela devolve o valor da expressão n * fat(n-1), ou seja o produto do
número pelo valor do fatorial do número predecessor. Portanto, quando se inicia o
processo a função é chamada com o valor do número e a função sendo chamada sempre
com este número sendo decrementado até que ele seja um. Quando o processo se reverte
e as chamadas começam a serem respondidas.
Um ponto importante é que toda função recursiva deve prever cuidadosamente
como o processo de recursão deve ser interrompido. No caso da função fat o processo
é interrompido quando o valor do número vale 1.
Quando uma função chama a si mesmo recursivamente ela recebe um conjunto
novo de variáveis na pilha que é usada para transferência de valores entre funções. É
importante notar que recursão não trás obrigatoriamente economia de memória porque
os valores sendo processados têm de ser mantidos na pilha. Nem será mais rápido, e às

53
vezes pode ser até mais lento porque temos o custo de chamada às funções. As principais
vantagens da recursão são códigos mais compactos e provavelmente mais fáceis de
serem lidos.

4.4 A função printf()

A função printf é parte de um conjunto de funções pré-definidas armazenadas


em uma biblioteca padrão (stdio.h) de rotinas da linguagem C. Ela permite apresentar na
tela os valores de qualquer tipo de dado. Para tanto, printf utiliza o mecanismo de
formatação, que permite traduzir a representação interna de variáveis para a
representação ASCII que pode ser apresentada na tela.

Sintaxe:

printf("string de controle", lista de argumentos);

O primeiro argumento de printf é uma string de controle, uma seqüência de


caracteres entre aspas. Esta string, que sempre deve estar presente, pode especificar
através de caracteres especiais (as seqüências de conversão) quantos outros argumentos
estarão presentes nesta invocação da função. Estes outros argumentos serão variáveis
cujos valores serão formatados e apresentados na tela. Por exemplo, se o valor de uma
variável inteira x é 10, então a execução da função,

printf("Valor de x = %d", x);

imprime na tela a frase Valor de x = 10. Se y é uma variável do tipo caractere


com valor ’G’, então a execução de

printf("x = %d e y = %c\n", x, y);

imprime na tela a frase x = 10 e y = G seguida pelo caractere de nova linha


(\n), ou seja, a próxima saída para a tela aconteceria na linha seguinte. Observe que a
seqüência de conversão pode ocorrer dentro de qualquer posição dentro da string de
controle.
A função printf não tem um número fixo de argumentos. Em sua forma mais
simples, pelo menos um argumento deve estar presente — a string de controle. Uma
string de controle sem nenhuma seqüência de conversão será literalmente impressa na
tela. Com variáveis adicionais, a única forma de saber qual o número de variáveis que
será apresentado é por inspeção da string de controle. Desta forma, cuidados devem ser
tomados para que o número de variáveis após a string de controle esteja de acordo com
o número de seqüências de conversão presente na string de controle.
Além de ter o número correto de argumentos e seqüências de conversão, o tipo
de cada variável deve estar de acordo com a seqüência de conversão especificada na
string de controle. A seqüência de conversão pode ser reconhecida dentro da string de
controle por iniciar sempre com o caráter %.

Exemplo 4.6 - Programa para ler um número inteiro entre 0 e 100 e verificar se é
igual ao número previamente estabelecido.

54
#include <stdio.h>
#include <conio.h>

main(){

int num, vezes;


char continua =’s’;

while (continua == ‘s’){


printf("\nEntre com um numero entre 0 e 100: ");
scanf("%d", &num);
vezes = 1;
while (num != 50){
printf("\n%d Incorreto! ", num);
printf("\nTente novamente!");
scanf("%d", &num);
vezes++;
}
printf("\nAcertou em %d Tentativa(s)", vezes);
printf("\nJoga Novamente? s/n");
continua = getche();
}
getch();
return(0);
}

As principais seqüências de conversão para variáveis caracteres e inteiras são:

%c imprime o conteúdo da variável com representação ASCII;


%d imprime o conteúdo da variável com representação decimal com sinal;
%u imprime o conteúdo da variável com representação decimal sem sinal;
%o imprime o conteúdo da variável com representação octal sem sinal;
%x imprime o conteúdo da variável com representação hexadecimal sem sinal.

Uma largura de campo pode ser opcionalmente especificada logo após o


caractere %, como em %12d para especificar que o número decimal terá reservado um
espaço de doze caracteres para sua representação. Se a largura de campo for negativa,
então o número será apresentado alinhado à esquerda ao invés do comportamento
padrão de alinhamento à direita. Para a conversão de variáveis do tipo long, o caractere
l também deve ser especificado, como em %ld. Para converter variáveis em ponto
flutuante, as seqüências são:

%f imprime o conteúdo da variável com representação com ponto decimal;


%e imprime o conteúdo da variável com representação em notação científica
(exponencial);
%g formato geral, escolhe a representação mais curta entre %f e %e.

Como para a representação inteira, uma largura de campo pode ser especificada
para números reais. Por exemplo, %12.3f especifica que a variável será apresentada
em um campo de doze caracteres com uma precisão de três dígitos após o ponto
decimal.

55
Finalmente, se a variável a ser apresentada é uma seqüência de caracteres (uma
string), então o formato de conversão %s pode ser utilizado. Para apresentar o caractere
%, a seqüência %% é utilizada.

4.5 A função scanf()

A função scanf() é definida em stdio.h como segue:

scanf("expressão de controle", lista de argumentos);

Esta lê caracteres da entrada padrão e coloca os valores lidos e convertidos nas


variáveis cujos endereços são passados na lista de argumentos seguintes à indicação do
formato; retorna o número de caracteres lidos e convertidos.
A indicação do formato é muito semelhante à especificada para printf(). A
única exceção diz respeito aos especificadores f, e ou g, que aqui se referem
exclusivamente a valores do tipo float (os valores lidos e convertidos deverão ser
passados a apontadores para variáveis do tipo float). Para especificar valores do tipo
double deverão ser usados os especificadores lf, le ou lg.
A lista de argumentos, que irão receber os valores lidos, deverá conter apenas
apontadores ou endereços de variáveis. Por exemplo, para ler um valor inteiro do teclado
e colocá-lo na variável de tipo int i.

scanf("%d", &i);
%c lê uma variável com representação ASCII;
%d lê uma variável com representação decimal com sinal;
%u lê uma variável com representação decimal sem sinal;
%o lê uma variável com representação octal sem sinal;
%x lê uma variável com representação hexadecimal sem sinal;
%f lê uma variável com representação com ponto decimal;
%e lê uma variável com representação em notação científica (exponencial);
%s lê uma variável string (cadeia de caracteres);
%lf lê uma variável com representação com ponto decimal de precisão dupla
(double);

Exemplo 4.7 - Programa para ler um caractere e exibir seu correspondente em


decimal, octal e hexadecimal.

#include <stdio.h>
#include <conio.h>

main(){

char a;
printf("\nEntre com um caractere:");
scanf("%c", &a);
printf("\n%c = %d em decimal,", a, a);
printf("\n%o em octal, %x em hexadecimal", a, a);

getch();

56
return(0);
}

No caso de arrays (strings, por exemplo) o próprio nome pode ser diretamente
usado na lista de argumentos, uma vez que representa o endereço da 1ª posição do array.
Como mostra o exemplo na seqüência:

char str[80];
...
scanf("%s", str);

outros exemplos,

int num;
scanf("%d", &num);

char letra;
scanf("%c", &letra);

4.5.1 Operador de endereço (&)

Um endereço de memória é o local no qual o computador armazena/retorna o


dado que está sendo atribuído ou lido por meio de um identificador, ou seja, uma
variável. Toda variável ocupa uma área de memória e seu endereço é o do primeiro byte
por ela ocupado.

Ex.:
int 2 bytes
float 4 bytes
char 1 byte

Usa-se & precedendo uma variável para indicar o endereço da mesma na


memória. Assim, a função scanf() terá a localização exata para armazenar os dados
obtidos. Veja exemplo a seguir (ex. 4.8).

Exemplo 4.8 - Programa para ler um número inteiro e exibir seu endereço.

#include <stdio.h>
#include <conio.h>

main(){

int num;

printf("\nEntre com um numero:");


scanf("%d", &num);
printf("Valor = %d, endereco = %lu", num, &num);

getch();
return(0);
}

57
4.6 Funções getche() e getch()

A função scanf() exige que a tecla <enter> seja pressionada após a entrada
dos dados. Outra biblioteca (conio.h) do C oferece funções que lêem dados sem esperar
a tecla <enter>. Estas funções fazem a leitura dos códigos de teclado. Estes códigos
podem representar teclas de caracteres (A, y, *, 6, etc.), teclas de comandos
(<enter>, <delete>, <Page Up>, <F10>, etc.) ou combinação de teclas (<Alt>
+ <F>, <Shift> + <F12>, <Ctrl> + <Page Down>, etc.).

getche() lê um caractere do teclado ecoando-o na tela;

getch() lê um caractere do teclado sem ecoá-lo na tela;

Ao ser executada, a função getch() - get character, aguarda que uma tecla
(ou combinação de teclas) seja pressionada, recebe do teclado o código correspondente e
retorna este valor. A função getche() - get character and echoe, também escreve na
tela, quando possível, o caractere correspondente.

código ASCII

Ao ser pressionada uma tecla correspondente a um caractere ASCII, o teclado


envia um código ao buffer de entrada do computador e este código é lido. Por exemplo,
se a tecla A for pressionada o código 65 (ex. 4.10) será armazenado no buffer e lido pela
função.

Código especial

Ao serem pressionadas certas teclas (ou combinação de teclas) que não


correspondem a um caractere ASCII, o teclado envia ao buffer do computador dois
códigos, sendo o primeiro sempre 0. Por exemplo, se a tecla <F1> for pressionada os
valores 0 e 59 (ex. 4.9) serão armazenados e a função deve ser chamada duas vezes para
ler os dois códigos.

Exemplo 4.9 - Programa para a leitura de teclado. Este programa usa a função
getch() para reconhecer teclas e combinação de teclas.

#include <stdio.h>
#include <conio.h>

main(){

int c,d;

printf("\nPressione uma tecla ou combinacaos:");


printf("\nPressione [esc] para terminar!");

do{

58
c = getch();
if (c == 0){
d = getch();
printf("\n %3d %3d codigo extendido", c, d);
}
else{
printf("\n %3d codigo normal", c);
}
}while (c != 27);

getch();
return(0);
}

Exemplo 4.10 - Programa para a leitura de teclado usando a função getche().

#include <stdio.h>
#include <conio.h>

main(){

int n = 0;
char c;

printf("\nDigite uma frase: ");

do{

c = getche();
n++;
}while (c != 13);

printf("\nVoce digitou %d caracteres.", n - 1);

getch();
return(0);
}

4.7 Funções getchar() e putchar()

A rotina básica de entrada de dados lê um caractere do teclado, retornando seu


valor ASCII. Esta rotina é a função getchar().

#include <stdio.h>

int getchar();

A descrição acima deve ser lida da seguinte forma: o arquivo de cabeçalho


<stdio.h> tem que ser incluído no arquivo fonte que for usar esta rotina
getchar(), e esta rotina não tem nenhum argumento e retorna um valor inteiro. O
seguinte exemplo (ex. 4.11) ilustra o uso desta função.

59
Exemplo 4.11 - Programa para a leitura de teclado usando a função getchar().

#include <stdio.h>

main(){
int ch;

ch = getchar();

printf("ASCII de %c = %d (hexa %x)\n", ch, ch, ch);

getch();
return(0);
}

Este programa (ex. 4.11), quando executado, irá aguardar que o usuário entre
algum caractere via teclado. A saída será uma indicação de qual caractere foi obtido ao
longo da digitação com seu valor em representação decimal e hexadecimal.
A rotina correspondente a getchar() para a apresentação de um caractere na
tela é a função putchar().

#include <stdio.h>

int putchar(char);

Por exemplo, o programa anterior (ex. 4.11) poderia ser estendido de forma a
que um prompt (tal como >) indicasse ao usuário que uma entrada de dados é
aguardada, como no programa a seguir (ex. 4.12).

Exemplo 4.12 - Programa para a leitura de teclado usando a função getchar() e a


função putchar().

#include <stdio.h>

main(){
int ch;
putchar('>');
ch = getchar();

printf("ASCII de %c = %d (hexa %x)\n", ch, ch, ch);

getch();
return(0);
}

4.6 Exercícios propostos

Exercício 4.1 – Faça um programa que leia um número inteiro longo e que determine o
seu módulo sem usar nenhuma função disponível em C para essa
finalidade. Crie sua própria função.

60
Exercício 4.2 – Faça um programa que determine se um ano introduzido pelo usuário é
ou não bissexto. Desenvolva uma função que receba como parâmetro
um inteiro representando um ano qualquer e retorne 1 quando o ano
for bisexto ou 0 caso contrário. Um ano é bissexto se for múltiplo de 4
sem ser de 100 ou se for múltiplo de 400.

Exercício 4.3 – Faça um programa que calcule a soma de dez números inteiros a partir
de um número dado pelo usuário. Utilize uma função para realizar o
somatório e retorná-lo.

Exercício 4.4 – Faça um programa que calcule a soma de números inteiros dados pelo
usuário, o número 0 (zero) encerra a leitura de valores. Use uma
função recurssiva.

Exercício 4.5 – Fazer uma função que calcula a enésima potência de uma variável real
ou fracionária x:

f(x,n) = xn

Exercício 4.6 – Fazer uma função que calcula o fatorial de um número. Implementar
uma versão recursiva e uma versão com laço.

Exercício 4.7 – Dado que podemos calcular ex por:

ex = 1 + x + x2/2! + x3/3! + ...

Fazer um trecho de programa em linguagem C que leia um valor para x e calcula


o valor de ex. O valor deve ser calculado enquanto o termo calculado for maior
que 10E-6.

Exercício 4.8 – Exercício: fazer um programa em "C" que solicita o total gasto pelo
cliente de uma loja, imprime as opções de pagamento, solicita a opção
desejada e imprime o valor total das prestações (se houverem).

1) Opção: a vista com 10% de desconto;


2) Opção: em duas vezes (preço da etiqueta);
3) Opção: de 3 até 10 vezes com 3% de juros ao mês (somente para
compras acima de R$ 100,00).

Obs.: Fazer uma função que imprima as opções, solicite a opção desejada e
retorne a opção escolhida. No programa principal, testar a opção
escolhida e ativar a função correspondente (uma função para cada opção).

Exercício 4.9 – Faça um programa que calcule a soma de números inteiros dados pelo
usuário, o número 0 (zero) encerra a leitura de valores. Use uma
função recurssiva.

61
Exercício 4.10 – Faça um programa que calcule o horário de chegada de um avião,
sendo dado o horário de partida (horas e minutos) e a duração do voo
(horas e minutos). O programa deve ainda indicar se o avião chega no
próprio dia ou no dia seguinte. Se o usuário introduzir o número de
horas de voo igual ou superior a 24h, o programa não deve efetuar os
cálculos, mas deve mostrar uma mensagem apropriada.

Exercício 4.11 – Faça um programa que dado um natural n, utilize uma função para
determinar o número harmônico Hn definido por:

Exercício 4.12 – Uma sequência de Fibonacci é uma seqüência na qual cada termo é a
soma dos dois termos que precedem eles. A sucessão de Fibonacci,
que tem 1 como seu primeiro termo é: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,...

Faça um programa que com uma função mostre os n primeiros termos


da referida sucessão. O valor de n deve ser passado como parâmetro.

62
5 Arranjos

Em C, arranjos são definidos e acessados por meio do operador de indexação [],


como no exemplo a seguir (ex. 5.1).

Exemplo 5.1 - Declarando variáveis do tipo vetor de cinco posições de inteiros,


inicializando-os com zeros.

#include <stdio.h>
#include <conio.h>

main(){

int elem[5];
int i;

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


elem[i] = 0;
...
getch();
return(0);
}

Neste exemplo (ex. 5.1), um arranjo de nome elem é definido na função main.
Este arranjo tem espaço para armazenar cinco valores inteiros, que serão referenciados
no programa como elem[0], elem[1], elem[2], elem[3] e elem[4]. É
importante lembrar que o primeiro elemento de um arranjo em C é sempre o elemento de
índice 0 (elem[0]). Conseqüentemente, para um arranjo com N elementos o último
elemento é o de índice N-1, ou seja, elem[4] no exemplo anterior (ex. 5.1).
Assim como para variáveis de tipos básicos, os elementos de um arranjo podem
ser também inicializados durante a declaração da variável. Neste caso, os valores de cada
um dos elementos são delimitados por chaves. Para o exemplo acima (ex. 5.1), tem-se:

...

int elem[5] = {0,0,0,0,0};


int i;

...

O índice de um arranjo pode ser qualquer expressão inteira, incluindo-se variáveis


e constantes inteiras. C não verifica se o valor do índice está dentro da faixa declarada - é

63
responsabilidade do programador garantir que o acesso esteja dentro dos limites de um
arranjo.
Arranjos podem ser multidimensionais. Por exemplo, um arranjo bidimensional
pode ser declarado, inicializado e acessado como no exemplo seguinte (ex. 5.2).

Exemplo 5.2 - Declarando variáveis do tipo matriz 2x3 posições de inteiros,


inicializando-os com zeros.

#include <stdio.h>
#include <conio.h>

main(){

int elem[2][3] = { {0,0,0}, {0,0,0}};


int i, j;

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


for (j = 0; j < 3; j++)
elem[i][j] = i * 3 + j;
getch();
return(0);
}

Em C, um arranjo bidimensional é na verdade um arranjo unidimensional onde


cada elemento é um arranjo. Por este motivo, dois operadores de indexação são
utilizados ao invés da forma [i,j]. Elementos são armazenados por linha.
Em geral, arranjos com muitas dimensões não são utilizados em C visto que
ocupam muito espaço e o acesso a seus elementos não ocorre de forma eficiente.

5.1 Vetores

Estrutura formada por um conjunto unidimensional de dados de mesmo tipo


(homogêneo) e possuindo número fixo de elementos (Estático). Na declaração dos
vetores devemos informar o seu nome, seu tipo (int, float, char, ...), e seu
tamanho (quantidade de elementos). Cada elemento do vetor é identificado por um
índice (unidimensional), o qual indica a sua posição no vetor.

Exemplo 5.3 - Criar um algoritmo que armazene números inteiros em dois vetores com
três elementos cada. Gerar e imprimir o vetor soma.

#include <stdio.h>
#include <conio.h>

main(){

int vetor1[3], vetor2[3], vetorSoma[3];

/* Entrada de elementos do vetor1 */

printf("Entre com o 1o elemento do vetor 1: ");

64
scanf("%d",&vetor1[0]);
printf("Entre com o 2o elemento do vetor 1: ");
scanf("%d",&vetor1[1]);
printf("Entre com o 3o elemento do vetor 1: ");
scanf("%d",&vetor1[2]);

/* Entrada de elementos do vetor2 */

printf("Entre com o 1o elemento do vetor 2: ");


scanf("%d",&vetor2[0]);
printf("Entre com o 2o elemento do vetor 2: ");
scanf("%d",&vetor2[1]);
printf("Entre com o 3o elemento do vetor 2: ");
scanf("%d",&vetor2[2]);

/* Calculo do vetor soma */

vetorSoma[0] = vetor1[0] + vetor2[0];


vetorSoma[1] = vetor1[1] + vetor2[1];
vetorSoma[2] = vetor1[2] + vetor2[2];

/* Exibindo o vetor soma */

printf("\nVetor Soma [1] = %d", vetorSoma[0]);


printf("\nVetor Soma [2] = %d", vetorSoma[1]);
printf("\nVetor Soma [3] = %d", vetorSoma[2]);

getch();
return(0);
}

Exemplo 5.4 - Criar um algoritmo que armazene números inteiros em dois vetores com
dez elementos cada. Gerar e imprimir o vetor soma.

#include <stdio.h>
#include <conio.h>

#define MAX 10

main(){

int vet1[MAX], vet2[MAX];


int vetSoma[MAX];

int i;

/* Entrada de elementos do vet1 */

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


printf("Entre com o %do elem do vetor 1: ", i + 1);
scanf("%d",&vet1[i]);
}

/* Entrada de elementos do vetor2 */

65
for (i = 0; i < MAX; i++){
printf("Entre com o %do elem do vetor 2: ", i + 1);
scanf("%d",&vet2[i]);
}

/* Calcula e Exibe o vetor soma */

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


vetSoma[i] = vet1[i] + vet2[i];
printf("\nVetor Soma [%d] = %d", i+1, vetSoma[i]);
}

getch();
return(0);
}

5.1.1 Exercícios Propostos

Exercício 5.1 – Faça um programa que dada uma seqüência de n números inteiros,
imprimi-la na ordem inversa à da leitura.

Exercício 5.2 – Dada uma seqüência (10 valores) de números inteiros não-nulos,
armazenar em outro vetor seus quadrados e mostrar ambos na ordem
primeiro valor e seu quadrado (armazenado no segundo vetor).

Exercício 5.3 – Faça um programa que dado um número inteiro positivo n, imprimir os
n primeiros naturais ímpares. Exemplo: Para n = 4 a saída deverá ser
1,3,5,7. Use uma função para determinar se o número é ímpar.
Armaszene-os em um vetor (Max 500 posições), exibindo-os logo a
seguir.

Exercício 5.4 – Faça um programa que dado um número inteiro positivo n, imprimir os
n primeiros naturais pares. Exemplo: Para n = 4 a saída deverá ser
0,2,4,6. Use uma função para determinar se o número é par e exibi-los.

Exercício 5.5 – Faça um programa que leia N (máximo 50) e uma lista de N números e
mostre a soma de todos os números da lista.

Exercício 5.6 – Escreva um programa que leia um conjunto de 100 números inteiros
positivos e determine o maior deles.

Exercício 5.7 – Escreva um programa que leia um número inteiro N (máximo 500) e
uma lista de N números inteiros positivos e determine o maior número
da lista.

Exercício 5.8 – Escreva um programa que leia um conjunto de números inteiros


positivos e determine o maior deles. A leitura do valor 0 (zero) ou no
máximo 100 números lidos indicam o fim dos dados (flag).

66
Exercício 5.9 – Faça um programa que leia uma lista de números inteiros positivos
terminada pelo número 0 (zero) ou no máximo 1000 números lidos. Ao
final, o programa deve mostrar a média aritmética de todos os números
lidos (excluindo o zero).

Exercício 5.10 – Faça um programa que leia um vetor de 10 inteiros e que calcule o
seguinte:
A média dos elementos do vector;
O número de elementos pares e o número de elementos ímpares;

Exercício 5.11 – Faça um programa que crie um vetor de inteiros de 10 posições, leia os
valores deste vetor e exiba o vetor na tela de trás para frente.

Exercício 5.12 – Um caçador submarino registou o tempo de cada um dos n mergulhos


que realizou num dia de caça. O registo do tempo é realizado em
segundos. Faça um programa que leia os referidos tempos para um
vetor, que determine o tempo máximo atingido num mergulho e em
que mergulho(s) é que esse tempo foi atingido. Considere que os
mergulhos são identificados perante o usuário por um número de 1 a n.
utilize uma função para determinar o tempo máximo.

Exercício 5.13 – Faça um programa que leia dois vetores de números compostos por 5
elementos que são fornecidos de maneira ordenada (números em
ordem crescente). Crie um terceiro vetor que é a união dos dois
primeiros vetores, sendo que este novo vetor de 10 elementos também
deve ser um vetor onde os seus elementos estão ordenados. Exiba a
soma total dos elementos contidos nos três vetores.

5.2 Strings

Um dos tipos de arranjos mais comum em C é o arranjo de caracteres, ou string.


O C padrão não suporta um tipo básico string; ao invés, há uma convenção para
tratamento de arranjos de caracteres que permite o uso de diversas funções de
manipulação de strings na linguagem.
Por convenção, C considera como uma string uma seqüência de caracteres
armazenada sob a forma de um arranjo de tipo char cujo último elemento é o caractere
NUL, ou ’\0’. Por exemplo, uma string poderia ser declarada e inicializada como em:

char exemplo[4] = {’a’,’b’,’c’,’\0’};

Observa-se que o espaço para o caractere ’\0’ deve ser previsto quando
dimensionado o tamanho do arranjo de caracteres que será manipulado como string. No
exemplo anterior, o arranjo de quatro caracteres pode receber apenas três letras, já que o
último caractere está reservado para o NUL.
A linguagem C suporta uma forma alternativa de representação de uma string
constante, que é através do uso de aspas:

char exemplo[4] = "abc";

67
Este exemplo é equivalente ao anterior, a string "abc" contém quatro
caracteres, sendo que o caractere ’\0’ é automaticamente anexado à string
exemplo[] pelo compilador.

5.2.1 Vetores de caracteres

Manipular variáveis contendo uma seqüência ou cadeia de caracteres poderia ser


tratado da mesma forma que outras variáveis numéricas. Algumas linguagens de
programação e entre elas a linguagem C o fazem de maneira idêntica a quaisquer outras
variáveis. Porém, para que o C trabalhe com variáveis de seqüência de caracteres (o tipo
string) é preciso utilizar a biblioteca <string.h>, a qual não é parte do ANSI C, ainda
que presente na maioria dos compiladores.
Sendo assim, uma variável string em ANSI C é um vetor de caracteres terminado
com um caractere nulo. O caractere nulo é um caractere com valor inteiro igual a zero
(código ASCII igual a 0). O terminador nulo também pode ser escrito usando a
convenção de barra invertida do C como sendo ’\0’. Embora o assunto vetores tenha
sido discutido anteriormente, apresentam-se aqui os fundamentos necessários para
poder-se utilizar as seqüências de caracteres ou strings.
Ao declarar uma variável chamada nome de 13 posições por meio de um vetor
de caracteres e inicializá-la com a palavra "Programas", o programador utiliza a
seguinte sintaxe:

char nome[13] = "Programas";

Na memória os caracteres são armazenados com mostrado a seguir (fig 5.1).

Memória (RAM)

P r o g r a m a s \0

Figura 5.1 – Armazenando caracteres na memória RAM.

No caso anterior, as três células de memória não usadas têm valores


indeterminados. Isto ocorre porque o compilador não inicializa as variáveis, cabendo ao
programador realizar esta tarefa. Portanto as únicas células de memória que são
inicializadas são as que contêm os caracteres ‘P‘, ‘r‘, ‘a‘, ‘g‘, ‘r‘, ‘a‘,
‘m‘, ‘a‘, ‘s‘ e ‘\0‘.
Para ler uma string fornecida pelo usuário pode-se utilizar a função scanf().
Um exemplo do uso desta função é apresentado logo a seguir (ex. 5.5). A função
scanf() coloca o terminador nulo no final do texto da string, quando o usuário
pressiona a tecla <enter>.

Exemplo 5.5 - Criar um programa que leia o primeiro nome e o último sobrenome de um
aluno. Logo após escrevê-lo em ordem de: sobrenome, nome.

68
#include <stdio.h>
#include <conio.h>

main(){

char nome[15],
sobrenome[15];

printf("Digite seu nome: ");


scanf("%s",nome);
printf("Agora seu sobre nome: ");
scanf("%s",sobrenome);
printf("\nObrigado!\n");
printf("Sr(a): %s, %s\n",&sobrenome, &nome);

getch();
return(0);
}

No exemplo anterior (ex. 5.5), o tamanho máximo das cadeias de caracteres ou


string que você pode fornecer é uma seqüência de 14 caracteres. Se você entrar com
uma seqüência de comprimento maior, o programa irá aceitar, mas os resultados podem
ser desastrosos.
Como as seqüências são vetores de caracteres, para se acessar um determinado
caractere de uma cadeia de caracteres, basta indexar, ou seja, usar um índice para acessar
o caractere desejado dentro da seqüência. Suponha uma seqüência chamada nome,
como no exemplo anterior (char nome [13] = "Programa";). Pode-se ter
acesso a quarta e sétima letra da variável nome da seguinte forma.

nome [3] = ‘g‘;


nome [6] = ‘m‘;

Por quê se está acessando a quarta letra e não a terceira?

Na linguagem C, o índice começa em zero. Assim, a primeira letra da string


sempre estará na posição 0. A segunda letra sempre estará na posição 1 e assim
sucessivamente. Segue um exemplo (ex. 5.6) que imprimirá a quarta e a sétima letra da
string "Algoritmo". Em seguida, ele mudará estas letras e apresentará a nova string ao
final.

Exemplo 5.6 - Criar um programa com uma variável chamada nome que contenha
inicialmente a palavra "Algoritmo" e em seguida altere o conteúdo da
mesma para "Algarismo", utilize o acesso a cada caractere (índice).

#include <stdio.h>
#include <conio.h>

main(){

char nome[15] = "Algoritmo";

printf("Palavra inicial: %s\n",&nome);

69
printf("A quarta letra: %c\n",nome[3]);
printf("A setima letra: %c\n",nome[6]);

nome [3] = ‘a‘;


nome [6] = ‘s‘;

printf("Palavra atual: %s\n",&nome);


printf("A quarta letra: %c\n",nome[3]);
printf("A setima letra: %c\n",nome[6]);

getch();
return(0);
}

Nesta cadeia de caracteres, o terminador nulo (‘\0‘) está na posição 9. Das


posições 0 a 8, têm-se caracteres válidos, e portanto pode-se escrevê-los. Note a forma
como foi inicializado a seqüência de caracteres nome com os caracteres ‘A‘, ‘l‘,
‘g‘, ‘o‘, ‘r‘, ‘i‘, ‘t‘, ‘m‘, ‘o‘ e ‘\0‘ simplesmente declarando
char nome[15] = "Algoritmo". Observe que "Algoritmo" (uma cadeia de
caracteres entre aspas) é uma cadeia de caracteres constante, isto é, uma cadeia de
caracteres que está pré-carregada com valores que não podem ser modificados. Já a
variável nome, pode sofrer alterações em seu conteúdo armazenado, como de fato foi
realizado.
No programa (ex 5.6) anterior, %s indica que printf() deve colocar uma
seqüência de caracteres (string) na tela.
Cadeias de caracteres (string) são vetores de caracteres (char). Nada mais e
nada menos. Deve-se apenas ficar atento para o fato de que as strings têm o seu último
elemento como um ‘\0‘.
Assim, deve-se lembrar que o tamanho da string deve incluir o ‘\0‘ final. A
biblioteca padrão do C possui diversas funções que manipulam strings. Estas funções são
úteis pois não se pode, por exemplo, igualar duas strings, da forma mostrada a seguir.

string1 = string2; /* Não faça isto */

Quando se desenvolve programas que tratam de cadeias de caracteres muitas


vezes pode-se fazer bom proveito do fato de que uma cadeia termina com ‘\0‘ (isto é,
o número inteiro 0). Veja o programa (ex. 5.7) a seguir que serve para igualar duas
cadeias de caracteres (isto é, copia os caracteres de uma string para o vetor da outra).

Exemplo 5.7 - Criar um programa para copiar uma cadeia de caracteres em uma outra.

#include <stdio.h>
#include <conio.h>

main(){

int i;
char nome[10], copia[10];

70
printf("Digite seu nome: ");
scanf("%s",&nome);

for (i = 0;nome[i];i++){
copia[i] = nome[i];
}

copia[i] = ‘\0‘;

printf("Esta e uma copia: %s\n",copia);

getch();
return(0);
}

A condição na estrutura de repetição for anterior (ex. 5.7) é baseada no fato de


que a seqüência de caracteres que está sendo copiada termina em ‘\0‘. Quando o
elemento encontrado em nome[i] é o ‘\0‘, o valor retornado para o teste
condicional é falso (nulo). Desta forma a expressão que vinha sendo verdadeira (não
zero) continuamente, torna-se falsa terminado a estrutura de repetição.

5.2.2 Funções para manipulação de vetores de caracteres

Uma outra função para a leitura de uma cadeia de caracteres, bem como uma
série de funções que manipulam seqüências de caracteres ou strings, são apresentadas a
seguir.

gets()

A função gets(), assim como a função scanf(), lê ou obtêm uma seqüência


de caracteres do teclado. Sua sintaxe é:

gets(variavel_string);

Exemplo 5.8 - Criar um programa para copiar uma cadeia de caracteres em uma outra,
usando gets() para leitura da seqüência de caracteres.

#include <stdio.h>
#include <conio.h>

int main(){

int i;
char nome[40], copia[40];

puts("Digite seu nome: ");


gets(nome);

for (i = 0;nome[i];i++){
copia[i] = nome[i];
}

71
copia[i] = ‘\0‘;

printf("Esta e uma copia: %s\n",copia);

getch();
return(0);
}

No exemplo anterior (ex. 5.7) a função scanf() termina a seqüência de


caracteres digitados pelo usuário, quando este digita um <enter>, porém armazena
somente os caracteres diferentes de “espaço em branco”, ou seja, armazena todos os
caracteres digitados até encontrar o primeiro espaço ou chegar ao <enter>. Já com
gets() (ex. 5.8) isto não ocorre. Toda a cadeia é armazenada.

strcpy()

A função strcpy() copia uma cadeia de caracteres (string_origem) para


uma outra cadeia de caracteres (string_destino). Seu funcionamento é semelhante
ao da rotina apresentada anteriormente (ex. 5.7). Esta e as demais funções apresentadas
na seqüência estão definidas na biblioteca <stdio.h>, fazendo-se necessário à inclusão
da mesma em cada programa que as utilize. Abaixo, tem-se a sintaxe da função
strcpy() e a seguir é apresentado um exemplo (ex. 5.9) de uso desta função.

strcpy (string_destino, string_origem);

Exemplo 5.9 - Criar um programa para copiar uma cadeia de caracteres em uma outra,
usando strcpy().

#include <stdio.h>
#include <conio.h>

main(){

char nome[40], copia[40], mensagem[20];

puts("Digite seu nome: ");


gets(nome);

strcpy(copia, nome); /* copia nome em copia */


strcpy (mensagem, "Obrigado Sr(a)");
/* copia "Obrigado Sr(a)" em mensagem */

printf("\n%s: %s\n",mensagem,copia);

getch();
return(0);
}

strcat()

72
Esta função une (concatena) duas cadeias de caracteres. Após a execução da
função, a cadeia de caracteres (string_destino) terá o seu conteúdo acrescido do
conteúdo da cadeia de caracteres (string_origem) que permanecerá inalterada. A
função strcat() apresenta a seguinte sintaxe:

strcat(string_destino, string_origem);

Exemplo 5.10 - Criar um programa que leia um nome e um sobrenome, logo a seguir
unir uma cadeia de caracteres a outra usando strcat() e uma nova
variável para armazená-la.

#include <stdio.h>
#include <conio.h>

main(){
char nome[15],
sobrenome[15],
nomecompleto[30] = "",
mensagem[20];

puts("Digite seu nome: ");


gets(nome);
puts("Digite seu sobrenome: ");
gets (sobrenome);

strcat(nomecompleto, nome);
/* acrescenta nome em nomecompleto */
nomecompleto[strlen(nomecompleto)] = ' ';
nomecompleto[strlen(nomecompleto)] = '\0';
/* acrescenta espaço em nomecompleto */
strcat(nomecompleto, sobrenome);
/* acrescenta sobrenome em nomecompleto */

strcpy (mensagem, "Obrigado Sr(a)");


/* copia "Obrigado Sr(a)" em mensagem */

printf("\n%s: %s\n",mensagem,nomecompleto);

getch();
return(0);
}

strlen()

A função strlen() retorna o comprimento da cadeia de caracteres (string)


fornecida. O terminador nulo não é contado. Isto quer dizer que, de fato, o comprimento
do vetor de caracteres deve ser um a mais que o inteiro retornado pela função
strlen(). Sua sintaxe é a seguinte:

strlen(string);

73
Exemplo 5.11 - Criar um programa que leia um nome e um sobrenome, una as cadeia de
caracteres usando strcat() em uma nova variável e encontre a
quantidade de caracteres (tamanho) esta variável possui usando a função
strlen().

#include <stdio.h>
#include <conio.h>

main(){

int tamanho;
char nome[15],
sobrenome[15],
nomecompleto[30] = "";

puts("Digite seu nome: ");


gets(nome);
puts("Digite seu sobrenome: ");
gets(sobrenome);

strcat(nomecompleto, nome);
/* acrescenta nome em nomecompleto */
nomecompleto[strlen(nomecompleto)] = ' ';
nomecompleto[strlen(nomecompleto)] = '\0';
/* acrescenta espaço em nomecompleto */
strcat(nomecompleto, sobrenome);
/* acrescenta sobrenome em nomecompleto */

tamanho = strlen(nomecompleto);

printf("\nSeu nome completo e: %s\n",nomecompleto);


printf("Numero de caracteres = %d\n",tamanho);

getch();
return(0);
}

strcmp()

A função strcmp() compara a cadeia de caracteres (string_1) com a segunda


cadeia de caracteres (string_2). Se as duas forem idênticas à função retorna o valor 0
(zero). Se elas forem diferentes a função retorna algo diferente do valor 0 (não-zero).
Sua sintaxe é mostrada abaixo:

strcmp(string_1, string_2);

Exemplo 5.12 - Criar um programa que compare duas strings e mostre a mensagem
“São Iguais” ou “São diferentes” dependendo da condição da função
strcmp().

#include <stdio.h>
#include <conio.h>

74
main(){

char str1[15], str2[15];

puts("Digite uma string: ");


gets (str1);
puts("Digite outra string: ");
gets(str2);

if (strcmp(str1, str2)){
puts("\nSao diferentes\n");
}
else{
puts("\nSao iguais\n");
}

getch();
return(0);
}

5.2.2 Exercícios Propostos

Exercício 5.14 – Entre e exiba seu nome, endereço e idade.

Exercício 5.15 – Escreva um programa que mostre várias linhas (tal qual seu nome e
endereço). Você pode usar vários comandos printf, cada qual com
um caractere de nova linha ou um único printf com várias novas
linhas na string.

Exercício 5.16 - Criar um programa que leia um nome e um sobrenome, una as cadeia
de caracteres usando strcat() em uma nova variável, certifique-se
que o nome e sobrenome esteja separado corretamente por espaços em
branco. O programa deve verificar a existência do espaço em branco e
caso este não esteja presente, deve ser acrescido ao nome antes de
exibir o nome completo.

Exercício 5.17 - Faça um programa que leia quatro palavras pelo teclado, e armazene
cada palavra em uma string. Depois, concatene todas as strings lidas
em uma única string. Por fim apresente esta como resultado ao final do
programa.

Exercício 5.18 - Crie um programa que mostre o tamanho de cada variável, incluindo a
variável que armazena todas as palavras do exercício anterior.

Exercício 5.19 – Faça um programa que leia uma lista (máximo 50) de letras terminada
pela letra z. Ao final, o programa deve mostrar a quantidade lida de
cada vogal.

75
Exercício 5.21 - Desenvolver um programa de “criptografia” (codificação de dados
visando à privacidade de acesso as informações), onde dada uma String
este programa codifique os dados através de um processo de
substituição de letras (você pode definir o seu próprio método). Fazer
um outro programa complementar a este que deve ser capaz de
“descriptografar” a String, ou seja, deve pegar uma String codificada e
retorná-la ao texto original.

Exercício 5.22 – Faça um programa que crie um vetor de 26 elementos do tipo


caractere. Cada elemento do vetor deve conter uma letra do alfabeto,
onde o seu índice é dado pela ordem da letra no alfabeto. Exibir os
elementos com índices pares na tela, e depois os elementos com índices
ímpares.

5.3 Matrizes

Estrutura semelhante ao vetor, sendo que, pode possuir n dimensões. Desta


forma para fazer referência aos elementos de uma matriz, precisa-se de tantos índices
quanto forem suas dimensões.

5.3.1 Matrizes bidimensionais

Nas seções anteriores viu-se como declarar matrizes unidimensionais (vetores).


Agora as matrizes bidimensionais. A forma geral da declaração de uma matriz
bidimensional é muito parecida com a declaração de um vetor.

tipo nome_da_matriz [altura] [largura];

É muito importante ressaltar que, nesta estrutura, o índice da esquerda (altura)


indexa as linhas e o da direita (largura) indexa as colunas. Ao preencher ou ler uma
matriz no C o índice mais à direita varia mais rapidamente que o índice à esquerda. Mais
uma vez é bom lembrar que, na linguagem C, os índices variam de zero ao valor
declarado, menos um; mas o C não vai verificar isto para o usuário. Manter os índices na
faixa permitida é tarefa do programador. Abaixo, tem-se um exemplo (ex. 5.13) do uso
de uma matriz para armazenar valores inteiros.

Exemplo 5.13 - Criar um programa com uma matriz de 10 linhas e 5 colunas,


preenchendo-as com valores seqüências de 1 até 50.

#include <stdio.h>
#include <conio.h>

main(){

int matriz[10][5];
int i,j,valor = 1;
printf("\n Colunas ...");

76
for (i = 0; i < 10; i++){
printf ("\nLinha %d: ",i);
for (j = 0; j < 5; j++){
matriz[i][j] = valor;
valor++;
printf ("%d",matriz[i][j]);
/* Experimente tambem %2d e %02d */
}
}
getch();
return(0);
}

Exemplo 5.14 - Criar um programa que leia os elementos de uma matriz inteira 6x6 e
escreva os elementos da diagonal principal.

#include <stdio.h>
#include <conio.h>

int main(){

int matriz[6][6];
int i,j;

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


for (j = 0; j < 6; j++){
printf("Entre com M[%d,%d] = ",i,j);
scanf("%d",&matriz[i][j]);
}
}

printf("\nDiagonal principal...");

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


printf("\nM[%d,%d] = %d",i,i,matriz[i][i]);
}

getch();
return(0);
}

5.3.2 Matrizes de cadeias de caracteres

As matrizes de cadeia de caracteres (strings) são matrizes bidimensionais. Uma


string é um vetor de caracteres. Ao declarar uma matriz de vetores de caracteres cria-se
uma lista de vetores de caracteres. Esta estrutura é uma matriz bidimensional de chars.
A seguir, vê-se a definição geral uma matriz de cadeias de caracteres (strings).

char nome_do_variavel [num_de_strings] [compr_das_strings];

Pode então surgir à pergunta: como acessar cada string individual? Simples.
Utilizando apenas o primeiro índice. Assim, para acessar uma determinada string faça:

77
nome_da_variavel [indice];

Exemplo 5.15 - Criar um programa que leia uma lista de 5 nomes de alunos. Após a
leitura apresentá-lo em ordem inversa à entrada.

#include <stdio.h>
#include <conio.h>

int main(){

char nome[5][50];
int i;

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


printf("Entre com Nome do Aluno[%d]:",i);
gets(nome[i]);
}

printf("\nNomes em ordem inversa");

for (i = 4; i > -1; i--){


printf("\nNome do Aluno[%d] = %s",i+1,nome[i]);
}

getch();
return(0);
}

5.3.3 Inicializações

Pode-se inicializar matrizes, assim como se pode inicializar variáveis comuns. A


forma geral de uma matriz, com inicialização é:

tipo nome_do_variavel [tam1] [tam2] = {lista_de_valores};

A lista_de_valores é composta por valores do mesmo tipo da variável


separados por vírgula. Os valores devem ser dados na ordem em que serão inseridos na
matriz. A seguir (ex. 5.16) é apresentado alguns exemplos de inicializações de matrizes:
Exemplo 5.16 - Inicializando matrizes.

float vetor [6] = {2.3, 3.5, 3.4, 5.9, 1.0, 103.1};


int matriz [2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
char str [13] = {'J', 'o', 'a', 'o', '\0'};
char str [13] = "Joao";
char str_vetor [3][10] = {"Jesus", "Maria", "Jose"};

O primeiro (ex. 5.16) demonstra inicialização de vetores de números fracionários.


O segundo exemplo demonstra a inicialização de matrizes multidimensionais, onde
matriz está sendo inicializada com 1, 2, 3, 4 e 5 em sua primeira linha e 6, 7, 8, 9 e 10 na
segunda e última linha. No terceiro exemplo vê-se como inicializar uma cadeia de

78
caracteres e, no quarto exemplo, um modo mais compacto de também inicializar uma
seqüência de caracteres (string). O quinto exemplo combina as duas técnicas para
inicializar uma matriz de vetores de caracteres.

5.3.4 Inicializações sem a especificação de tamanho

Pode-se, em alguns casos, inicializar matrizes das quais não se sabe exatamente o
tamanho a priori. O compilador C irá, neste caso verificar o conteúdo de inicialização e
considerar como sendo este o tamanho da matriz. Isto ocorre no momento da
compilação e não poderá mais ser mudado durante a execução do programa, sendo
muito útil, por exemplo, quando se inicializa uma string e sem no entanto precisar contar
quantos caracteres serão necessários para armazená-la. Alguns exemplos a seguir.

Exemplo 5.17 - Inicializando vetores e matrizes sem especificar o tamanho destes.

int matriz [][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};


char str[] = "O compilador C determinarah o tamanho!";

No primeiro exemplo o valor não especificado será 2. No segundo exemplo, a


string str terá o tamanho 39. Veja que o artifício para realizar a inicialização sem
especificar o tamanho é não especificar o tamanho.

5.3.5 Passagem de vetores e matrizes

Matriz é um caso especial e uma exceção à regra que parâmetros são passados
sempre por valor. Como veremos mais adiante, o nome de um vetor corresponde ao
endereço do primeiro elemento do array. Quando um vetor é passado como parâmetro,
apenas o endereço do primeiro elemento é passado.
Existem basicamente três maneiras de declarar um vetor como um parâmetro de
uma função. Na primeira ele é declarado como tem sido apresentado em todos os
exemplos até agora. O exemplo (ex. 5.18) seguinte mostra um programa que usa uma
função para descobrir quantas vezes um caractere ocorre em um vetor. Observa-se que a
dimensão do vetor foi declarada explicitamente.

Exemplo 5.18 - Determinar o número de ocorrências de cada um dos caracteres


presentes no alfabeto em um vetor.

#include <stdio.h>
#include <conio.h>

#define DIM 80

char conta (char v[], char c);

main(){

char linha[DIM];
char c;

79
int maiusculas[26], minusculas[26];

puts("Entre com uma linha");


gets (linha);
for (c = 'a'; c <= 'z'; c++)
minusculas[c - 'a'] = conta(linha, c);
for (c = 'A'; c <= 'Z'; c++)
maiusculas[c - 'A'] = conta(linha, c);
for (c = 'a'; c <= 'z'; c++)
if (minusculas[c - 'a'])
printf("%c apareceu %d vezes\n", c, minusculas[c
- 'a']);
for (c = 'A'; c <= 'Z'; c++)
if (maiusculas[c - 'A'])
printf("%c apareceu %d vezes\n", c, maiusculas[c
- 'A']);
getch();
return(0);
}

char conta (char v[DIM], char c){


int i=0, vezes=0;

while (v[i] != '\0')


if (v[i++] == c) vezes++;

return(vezes);
}

Uma outra maneira, leva em conta que apenas o endereço do vetor é passado.
Neste modo o parâmetro é declarado como um vetor sem dimensão. Isto é perfeitamente
possível porque a função somente precisa receber o endereço onde se encontra o vetor.
Além disso C não confere limites de vetores e portanto a função precisa do endereço
inicial do vetor e uma maneira de descobrir o final do vetor. Esta maneira pode ser, por
exemplo, uma constante, ou o caractere '\0' em um vetor de caracteres. O exemplo
(ex. 5.19) mostra este modo de passar vetores com um programa que inverte o conteúdo
de um vetor.

Exemplo 5.19 - Determinar o número de ocorrências de cada um dos caracteres


presentes no alfabeto em um vetor.

#include <stdio.h>
#include <conio.h>
#define DIM 6

void Le_vetor (int v[], int tam);


void Imprime_vetor (int v[], int tam);
void Inverte_vetor (int v[], int tam);

main(){
int v[DIM];

Le_vetor(v, DIM);

80
Imprime_vetor (v, DIM);
Inverte_vetor (v, DIM);
Imprime_vetor (v, DIM);
getch();
return(0);
}

void Le_vetor (int v[], int tam){


int i;

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


printf("%d = ? ", i);
scanf("%d", &v[i]);
}
}

void Imprime_vetor (int v[], int tam){


int i;

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


printf("%d = %d\n", i, v[i]);
}

void Inverte_vetor (int v[], int tam){


int i, temp;

for (i = 0; i < tam/2; i++){


temp = v[i];
v[i] = v[tam-i-1];
v[tam-i-1] = temp;
}
}

A terceira maneira implica no uso de ponteiros, o que será visto no próximo


capítulo.

5.3.6 Exercícios propostos

Exercício 5.23 – Faça um programa que crie uma matriz de inteiros de 3 linhas por 3
colunas. Leia os valores desta matriz linha após linha e exiba a matriz
na tela.

Exercício 5.24 – Faça um programa que crie uma matriz de inteiros de 5 linhas por 10
colunas. Leia os valores desta matriz linha após linha e exiba a matriz
na tela coluna por coluna.

Exercício 5.25 – Faça um programa que leia e visualize uma matriz de números inteiros
e que localize a primeira ocorrência, nessa matriz, de um valor
introduzido pelo usuário. A função deve retornar 1 se o valor recebido
com parâmetro estiver presente na matriz e 0 (zero) caso contrário.

81
Exercício 5.26 – Faça um programa que crie uma matriz 3x3x3 onde cada elemento da
matriz seja igual à soma dos seus índices. Obtenha a soma de todos
elementos da matriz, soma dos elementos cujos valores são pares e a
soma dos elementos cujos valores são ímpares.

Exercício 5.27 – Faça um programa que leia três vetores independentes compostos por
5 números reais que são fornecidos pelo usuário. Crie uma matriz que
reúna estes três vetores em uma única estrutura. Exibir na tela o
conteúdo da matriz. Encontre o maior valor contido nesta matriz.

Exercício 5.28 – Programar a função int eDigito(char c), que retorna 1 se c é


um dígito (entre 0 e 9) e 0 caso contrário.

Exercício 5.29 – Implementar int strtam(char s[]) que retorna o tamanho do


string s.

Exercício 5.30 – Fazer um programa que procura a ocorrência de um caractere c em


uma string s e imprime “Achou!” caso c apareça em s e “Nada!”
caso contrário.

Exercício 5.31 – Faça um programa que, dado um número inteiro longo, verifique se é
negativo e, caso não o seja, determine se é ou não capicua. Um número
é capicua quando lido da esquerda para a direita ou da direita para a
esquerda representa sempre o mesmo valor, como por exemplo 77,
434, 6446 e 82328. Para obter um número capicua a partir de outro,
inverte-se a ordem dos algarismos e soma-se com o número dado, um
número de vezes até que se encontre um número capicua.

5.4 Definição de estruturas

Por meio de estruturas, a linguagem C oferece um mecanismo uniforme para a


definição de unidades de informações organizadas em campos. A forma geral de
definição de uma estrutura C é

struct nome {

/* declaracao de componentes: */
...
} [var1, var2, ..., varN];

A palavra chave struct inicia a definição da estrutura. O nome é opcional,


porém deve estar presente caso se deseje referenciar esta estrutura em algum momento
posterior. Da mesma forma, a lista de variáveis declaradas (var1, ... , varN) também
não precisa estar presente — a não ser que nome não esteja presente. Quando nome
está presente, as variáveis com essa mesma estrutura podem ser definidas
posteriormente, como na declaração a seguir,

struct nome varM;

82
que define varM como sendo uma variável do tipo struct nome. Alternativamente,
seria possível associar um nome de tipo para essa estrutura através da declaração,

typedef struct nome Nome;

e então declarar varM como

Nome varM;

Da mesma forma que tipos básicos, estruturas podem ser passadas como
argumentos ou serem valores de retorno de funções. Uma vez que uma estrutura tem
componentes internos que devem ser acessados para processamento, algum mecanismo
de acesso deve ser fornecido pela linguagem. O C oferece dois operadores que permitem
acessar elementos de estruturas.
O operador básico de acesso a elementos de estruturas é o operador . (ponto).
Uma versão simplificada da leitura de uma data sendo armazenada em uma estrutura
data é mostrada a seguir (ex. 5.20). A estrutura dados_pessoais também está
presente neste programa (ex. 5.20) para demonstrar a possibilidade de reutilizar a
estrutura data já existente.

Exemplo 5.20 - Desenvolver um programa para ler a data atual e o dados de um


estudante, usando estruturas.

#include <stdio.h>
#include <conio.h>

struct data{
int dia;
int mes;
int ano;
};

struct dados_pessoais{
char nome[40];
char endereco[50];
struct data nascimento;
};

main(){

struct data hoje;


struct dados_pessoais estudante;

printf("\nEntre com o dia de hoje: ");


scanf("%d",&hoje.dia);
printf("\nEntre com o mes atual: ");
scanf("%d",&hoje.mes);
printf("\nEntre com o ano corrente: ");
scanf("%d",&hoje.ano);

getchar();
printf("\nEntre com o seu nome: ");
gets(estudante.nome);

83
printf("\nEntre com o seu endereco: ");
gets(estudante.endereco);

printf("\nEntre com o dia de seu nascimento: ");


scanf("%d",&estudante.nascimento.dia);
printf("\nEntre com o mês de seu nascimento: ");
scanf("%d",&estudante.nascimento.mes);
printf("\nEntre com o ano de seu nascimento: ");
scanf("%d",&estudante.nascimento.ano);

...

getch();
return(0);
}

Em aplicações sucessivas desse operador, quando for necessário acessar


estruturas aninhadas, sua associatividade é da esquerda para a direita.

5.5 Enumerações

Uma outra forma de tipo composto em C é a enumeração. Usualmente, faz parte


do processo de desenvolvimento de um programa associar códigos numéricos a variáveis
que podem assumir um único valor dentre um conjunto finito de opções. O tipo
enumeração permite associar nomes descritivos a tais conjuntos de valores numéricos.
Considere uma extensão da estrutura dados_pessoais apresentada acima
que incorporasse também o sexo da pessoa. Há dois estados possíveis para uma variável
deste tipo: ela pode assumir o valor masculino ou o valor feminino. Uma
enumeração que poderia representar este tipo de informação seria,

enum sex {masculino, feminino};

Uma variável deste tipo de enumeração poderia ser então incorporada na


estrutura apresentada a seguir,

struct dados_pessoais {
char nome[40];
struct data nascimento;
enum sex genero;
};

O seguinte trecho de programa ilustra como os nomes descritivos definidos em


enumerações são utilizados como valores:

Exemplo 5.21 - Determinar a idade de uma pessoa utilizando estruturas e uma função
para realizar o cálculo.

#include <stdio.h>
#include <conio.h>

enum sex {masculino, feminino};

84
struct data{
int dia;
int mes;
int ano;
};
struct dados_pessoais{
char nome[40];
struct data nascimento;
enum sex genero;
};

int calc_idade(struct dados_pessoais p,


struct data h);

main(){

struct data hoje;


struct dados_pessoais pessoa;

printf("\nEntre com o dia de hoje: ");


scanf("%d",&hoje.dia);
printf("\nEntre com o mês atual: ");
scanf("%d",&hoje.mes);
printf("\nEntre com o ano corrente: ");
scanf("%d",&hoje.ano);

getchar();

printf("\nEntre com o seu nome: ");


gets(pessoa.nome);
printf("\nEntre com o dia de seu nascimento: ");
scanf("%d",&pessoa.nascimento.dia);
printf("\nEntre com o mês de seu nascimento: ");
scanf("%d",&pessoa.nascimento.mes);
printf("\nEntre com o ano de seu nascimento: ");
scanf("%d",&pessoa.nascimento.ano);
printf("\nSexo: (0) masculino e (1) feminino: ");
scanf("%d",&pessoa.genero);

printf("\nSua idade eh: %d",calc_idade(pessoa,hoje));

getch();
return(0);
}

int calc_idade(struct dados_pessoais p,


struct data h){
int idade;

idade = h.ano - p.nascimento.ano;


if (p.genero == feminino)
idade -= 5;

return(idade);

85
}

Internamente, o compilador designa o valor 0 para o primeiro símbolo da


enumeração, e incrementa de um o valor associado a cada símbolo na seqüência. Isto
pode ser modificado se o programador quiser através de atribuição explícita de um valor
inteiro a um símbolo, como em:

enum cedula {beijaflor = 1, garca = 5, arara = 10};

5.6 Definição de nomes de tipos

Embora C não permita a criação de novos tipos de dados, ela oferece uma
facilidade para criar novos nomes para os tipos existentes, sejam eles básicos ou
derivados. Este mecanismo, typedef, permite principalmente melhorar a facilidade de
compreensão de programas. A forma geral de uma definição de nome de tipo é:

typedef tipo novo_nome;

Por exemplo, os tipos de estruturas data e dados_pessoais definidos


anteriormente poderiam ser associados a nomes de tipos Data e Pessoa
respectivamente pelas declarações,

typedef struct data Data;


typedef struct dados_pessoais Pessoa;

Com estas definições, as declarações do programa que apresenta a idade de


pessoas poderiam ser reescritas como:

Exemplo 5.22 - Calcular a idade de uma pessoa com definição de tipos, estruturas e
enumerações.

#include <stdio.h>
#include <conio.h>

typedef enum sex {masculino, feminino} Sexo;

typedef struct data{


int dia;
int mes;
int ano;
}Data;

typedef struct dados_pessoais{


char nome[40];
Data nascimento;
Sexo genero;
}Pessoa;

int calc_idade(Pessoa p, Data d);


Data le_hoje();
Pessoa le_aluno();

86
main(){

Data hoje;
Pessoa estudande_graduacao;
int idade;

...
}

Outros exemplos de uso de typedef são:

typedef unsigned int Tamanho;


typedef enum {false = 0, true} Boolean;

5.7 Exercícios propostos

Exercício 5.32 – A função abaixo que recebe a data de início de um evento e a duração
do evento em dias. Ela devolve a data de fim do evento.

struct dma converte(struct dma datainicio, int duracao){


struct dma datafim;
. . .
. . .
datafim.dia = ...
datafim.mes = ...
datafim.ano = ...

return(datafim);
}

O código deve levar em conta a existência de meses com 31 dias, de meses com
30 dias, com 29 dias, etc. Eis como essa função poderia ser usada:

int main(void){
struct dma inicio;
struct dma fim;
int dura;
scanf ("%d%d%d", &inicio.dia, &inicio.mes, &inicio.ano);
scanf ("%d", &dura);
fim = converte (inicio, dura);
printf ("%d %d %d\n", fim.dia, fim.mes, fim.ano);
return(EXIT_SUCCESS);
}

Complete o código da função do exercício.

Exercício 5.33 – Escreva uma função que receba duas structs do tipo dma, cada um
representando uma data válida, e devolva o número de dias que
decorreram entre as duas datas.

87
Exercício 5.34 – Escreva uma função que receba um número inteiro que representa um
intervalo de tempo medido em minutos e devolva o correspondente
número de horas e minutos (por exemplo, converte 131 minutos em 2
horas e 11 minutos). Use uma struct como a seguinte:

struct horasEminutos{
int horas;
int minutos;
};

Exercício 5.35 – Defina uma struct empregado para guardar os dados (nome,
sobrenome, data de nascimento, RG, data de admissão, salário) de um
empregado de sua empresa. Defina um vetor de empregados para
armazenar todos os empregados de sua empresa..

Exercício 5.36 – Suponha que Noé queria manter um registo informatizado, utilizando
um programa em C, de todos os animais que levava na sua arca. Defina
um tipo, chamado Animal, para guardar a seguinte informação:

nome - cadeia de caracteres com um maximo de 30 caracteres


habitat - um dos valores terra, agua e ar
peso - um float

a) Escreva uma função que leia e armazene as informações acima para


cada ums dos animais guardados na arca. A função termina quando o
usuário indicar um nome vazio.

b) Escreva uma função que receba lista de todos os animais guardados


na arca e calcule o peso total dos animais.

c) Escreva uma função que receba lista de todos os animais guardados


na arca e encontre o animal mais pesado da arca.

Exercício 5.37 – Fazer uma rotina que receba como parâmetro um vetor de 5 posições
contendo as notas de um aluno ao longo do ano e devolve a média do
aluno.

Exercício 5.38 – Fazer uma rotina que receba um array do tipo double e o número de
valores que devem ser solicitados ao usuário e devolve o array
preenchido com os valores digitados.

Exercício 5.39 – Fazer um programa em linguagem C que leia um conjunto de 10


valores inteiros e verifica se algum dos valores é igual à média dos
mesmos.

Exercício 5.40 – Fazer um programa que leia valores para uma matriz do tipo float
de 5 linhas por 3 colunas e imprima a diferença entre a média dos
elementos das colunas pares e a média dos elementos das linhas
ímpares.

88
Exercício 5.41 – Fazer um programa em C que leia uma string qualquer de no máximo
80 caracteres e imprima:

a) Quantos caracteres têm a string;


b) Quantos caracteres são de pontuação;
c) Quantos caracteres são números;
d) Quantos caracteres são minúsculos.

Exercício 5.42 – Fazer um programa em C que leia uma string contendo palavras
separadas por um espaço em branco cada e as imprima uma abaixo das
outras.

Exercício 5.43 – Fazer um programa em C que leia uma string do teclado e que se
utilize uma rotina recursiva para imprimir a string de maneira normal e
de traz para frente.

Exercício 5.44 – Fazer uma função para ler e retornar o valor das 3 notas de um aluno.

Exercício 5.45 – Fazer um programa em C que pergunte o nome, o endereço, o telefone


e a idade de uma pessoa e monte uma string com a seguinte frase:

"Seu nome é ..., você tem ... anos, mora na rua ... e seu telefone é ... ."

Exercício 5.46 – Fazer uma rotina que aguarda uma string do teclado e retorna o valor
1 se a string digitada foi "SIM" e 0 se a string digitada foi "NAO". A
rotina só deve retornar alguma coisa se a string digitada for "SIM" ou
"NAO".

Exercício 5.47 – Fazer uma rotina que recebe uma string como parâmetro e imprime
quantas palavras (separadas por espaços em branco) o mesmo contém.

Exercício 5.48 – Implemente uma rotina que faça a mesma coisa que a função "strcpy".

Exercício 5.49 – Fazer um programa em C que solicite um número inteiro e soletra o


mesmo na tela. Ex: 124: um, dois, quatro.

Exercício 5.50 – Escrever uma função que recebe uma string e um caractere como
parâmetro e remova todas as ocorrências do caractere da string.

Exercício 5.51 – Escreva uma função em C que receba uma string, um caractere e o
índice de uma posição da string como parâmetro e insira o caractere na
posição "empurrando" todos os demais para o lado.

Exercício 5.52– Fazer uma rotina em C que recebe uma string como parâmetro e
devolve o endereço do primeiro caractere branco encontrado.

Exercício 5.53 – Fazer uma função que retorna a soma, a diferença e o produto entre
dois números.

89
Exercício 5.54 – Fazer uma função em C que retorna a razão entre dois números. A
função deve retornar pelo comando return o valor 1 se a operação
for possível e o valor 0 se a operação não for possível (divisão por
zero, por exemplo). O resultado da divisão deve retornar por um
parâmetro por referência.

Exercício 5.55 – Fazer uma rotina em C que recebe um vetor de números inteiros como
parâmetro onde todos os valores exceto o último são positivos e
devolve:

a) a média dos valores do vetor;


b) o menor valor do vetor (sem considerar o último);
c) o maior valor do vetor.

Exercício 5.56 – Construir um programa em C que implementa uma agenda eletrônica.


O programa deve ter um menu com as seguintes opções:

a) Entrar um novo nome na agenda;


b) Imprimir na tela os dados de uma das pessoas cadastradas
(conforme solicitação);
c) Imprimir a lista de nomes cadastrados que comecem pela letra
indicada;
d) Fim.

Cada entrada da agenda deve ter os seguintes campos:

char nome[30];
char endereco[100];
char fone[10];
long int CEP;

Obs: A agenda deve ter capacidade para 100 entradas.

Exercício 5.57 – Criar uma função que testa um caractere recebido e verifica se é vogal.
Devolve o valor 1 se for vogal e 0 (zero) se for consoante. Fazer um
programa principal que recebe uma frase, verifica todas as letras
usando a função e no final informa o total de vogais, consoantes e
brancos. Também informar o valor em percentual de vogais,
consoantes e brancos, com relação ao total geral de letras (+ brancos)
da frase.

90
6 Ponteiros

Ponteiros constituem um dos recursos mais utilizados na programação C. Eles


fornecem um mecanismo poderoso, flexível e eficiente de acesso as variáveis. Um
ponteiro é uma variável que contém um endereço de outra variável.
Este mecanismo deve ser utilizado com critério e disciplina. O uso descuidado de
ponteiros pode levar a situações onde um endereço inválido é acessado, levando a erros
de execuções de programas.
Para definir que uma variável vai guardar um endereço, o operador unário * é
utilizado na declaração, como em:

/* define um ponteiro para um inteiro */


int *ap;

/* algumas variaveis inteiras */


int x, y;

Nas instruções acima, ap é uma variável do tipo ponteiro para inteiro, ou seja,
ela irá receber um endereço de uma variável inteira. Para se obter o endereço de uma
variável, o operador unário & pode ser utilizado. Ainda no exemplo acima, atribui-se à
ap o endereço de uma variável int, uma vez que ap é um apontador para um int.

/* ap recebe o endereco de x */
ap = &x;

Após esta instrução, diz-se que ap aponta para x (fig. 6.1). É possível acessar o
valor da variável x por meio do ponteiro usando o operador unário *, como em:

/* y recebe o conteudo da variavel apontada por ap */


y = *ap;

Observa-se que a combinação *ap é um inteiro, que pode assim ser atribuído a
outro inteiro. Esta associação pode facilitar a compreensão da declaração de ponteiros.
No exemplo, int *ap pode ser lido como “*ap é um inteiro”. Também seria possível
definir o valor de x por meio do ponteiro, como em:

/* o conteudo da variavel apontada por ap recebe y */


*ap = y;

Ponteiros podem tomar parte em expressões aritméticas. Assim, a seguinte


expressão é perfeitamente válida:

91
y = *ap + 1;

ou seja, y receberia o valor de x (variável apontada por ap) incrementado de 1


(uma unidade).

Figura 6.1 - Espaço ocupado pelas variáveis.

6.1 Exercícios propostos

Exercício 6.1 – Pratique a declaração e utilização de ponteiros.


1) defina e inicialize uma variável inteira;
2) defina um ponteiro para inteiro;
3) modifique o valor da variável por meio do ponteiro;
4) verifique os novos valores da variável usando printf();

6.2 Aritmética de Ponteiros

Não apenas os conteúdos dos ponteiros podem tomar parte em expressões


aritméticas. C também suporta o conceito de operações sobre endereços, embora as
operações que possam ser utilizadas neste caso sejam limitadas. Tais operações definem
a aritmética de ponteiros. Para apresentar o conceito de aritmética de ponteiros,
considere o seguinte exemplo (ex. 6.1):

Exemplo 6.1 - Inicializando um vetor de 10 posições utilizando um ponteiro para um


inteiro.

#include <stdio.h>
#include <conio.h>

main(){

int arr[10]; /* arr: arranjo com 10 inteiros */


int *el; /* el: ponteiro para um inteiro */
int i;

el = &arr[0]; /* inicializa ponteiro */

/* inicializa conteudo do arranjo via ponteiro */


for (i = 0; i < 10; i++)
*(el + i) = 0;
getch();

92
return(0);
}

O ponteiro el aponta inicialmente para o primeiro elemento do arranjo arr, ou


seja, arr[0] - &arr[0] é o endereço deste elemento. Assim, para acessar este
elemento por meio do ponteiro, a expressão *el poderia ser utilizada. No entanto, é
possível também acessar outros elementos do arranjo por meio do ponteiro. Para acessar
o elemento seguinte, a expressão *(el + 1) retorna o endereço do próximo inteiro
armazenado após o endereço el, ou seja, o endereço de arr[1]. Portanto, o que a
instrução interna ao laço no exemplo está realizando é o acesso a cada elemento do
arranjo por meio de um ponteiro.
Um aspecto fundamental da aritmética de ponteiros é que ela libera o
programador de saber qual a dimensão alocada para cada tipo de dado. Quando um
ponteiro é incrementado, este incremento irá refletir o tamanho do tipo da variável que
está sendo apontada. Assim, o exemplo anterior (ex. 6.1) funcionará independentemente
da máquina no qual ele for executado, ocupe a representação de um inteiro dois ou
quatro bytes.
Quando a expressão el + i é encontrada, o endereço do i-ésimo inteiro após
o endereço el é obtido - ou seja, esta expressão aponta para o elemento arr[i].
A aritmética de ponteiros está limitada a quatro operadores: soma, subtração,
incremento e decremento.
Outra limitação é que, no caso de soma, o outro operando além do ponteiro deve
ser uma expressão (ou variável ou constante) inteira. A subtração de dois ponteiros para
um mesmo tipo de dado é uma operação válida, retornando o número de elementos entre
os dois ponteiros. A comparação entre dois ponteiros também é uma operação legal.
Há uma única exceção ao uso legal de aritmética de ponteiros: quando um
ponteiro é definido para o tipo void. Neste caso, a variável tipo ponteiro contém um
endereço genérico, sobre um tipo que não pode ser determinado. Portanto, operações
aritméticas sobre este tipo de ponteiro não são permitidas.

6.3 Ponteiros e Arranjos

Como observado no exemplo anterior (ex. 6.1), ponteiros e arranjos estão


intimamente relacionados em C. Na verdade, qualquer referência a um arranjo é
convertida internamente para uma referência do tipo ponteiro. Por este motivo, quando
eficiência de tempo de acesso é uma preocupação, muitos programadores trabalham
diretamente com ponteiros.
O nome de um arranjo é uma expressão do tipo ponteiro que corresponde ao
endereço do primeiro elemento do arranjo. Assim, a inicialização do ponteiro no
exemplo acima poderia ser reescrita como

el = arr; /* arr equivale a &arr[0] */

Observa-se que, uma vez que arr equivale a um ponteiro, o elemento arr[i]
poderia ser acessado da mesma forma como *(arr + i).
Por outro lado, o inverso (usar o operador de indexação como uma variável
ponteiro) também é possível. Assim, o laço de atribuição no exemplo anterior (ex. 6.1)
poderia ter sido escrito como:

93
for (i = 0; i < 10; i++)
el[i] = 0;

Apesar da forma usando o operador * ser mais eficiente, programadores


iniciantes muitas vezes acham mais simples entender o acesso usando o operador de
indexação, e acabam preferindo esta forma. Uma diferença fundamental entre um
ponteiro e o nome de um arranjo é que o ponteiro é uma variável, enquanto que o nome
de um arranjo é uma constante. Assim, expressões como arr++ ou &arr não fazem
sentido.
Outra diferença que deve ser ressaltada é o fato de que a declaração de um
arranjo efetivamente reserva o espaço para as variáveis, enquanto que a declaração de
um ponteiro reserva apenas espaço para guardar um endereço. Considere o seguinte
exemplo (ex 6.2):

Exemplo 6.2 - Uso indevido de um ponteiro para um inteiro.

#include <stdio.h>
#include <conio.h>

main(){

int *el; /* el: ponteiro para inteiro */


int i;

/* inicializa conteudo */
for (i = 0; i < 10; i++)
el[i] = 0; /* onde esta el[i]? */
getch();
return(0);
}

Uma vez que o ponteiro el não foi inicializado, a expressão el[i] pode estar
apontando para qualquer posição da área de memória — possivelmente, para alguma
posição inválida. Observa-se que o fato de ter declarado o ponteiro não significa que esta
variável possa ser utilizada como um arranjo. Para tal, o ponteiro deve estar com o
endereço de alguma posição válida, seja por meio de uma atribuição envolvendo um
arranjo, seja por meio do uso de rotinas de alocação dinâmica.
Uma vez que ponteiros são variáveis, nada impede que arranjos de ponteiros
sejam definidos. De fato, uma declaração tal como:

int *aa[10];

define uma variável aa que é um arranjo de dez ponteiros para variáveis inteiras. Cada
elemento deste arranjo, desde aa[0] até aa[9], é um ponteiro para inteiro(s) que tem
a mesma propriedade que os ponteiros vistos até o momento.
Observa-se que esta forma suporta uma alternativa para trabalhar com arranjos
multidimensionais, desde que respeitadas as diferenças entre ponteiros e arranjos.
Arranjos de ponteiros trazem uma flexibilidade adicional pelo fato de que cada “linha”
pode ter tamanho variável. No exemplo acima, cada ponteiro aa[i] pode estar

94
apontando para um inteiro, para o primeiro elemento de um arranjo com diversos
inteiros, ou mesmo para nenhum inteiro.

Soma

Ao somar-se um inteiro n a um ponteiro, endereçam-se n elementos a mais (n


positivo) ou a menos (n negativo):

pf[2] equivale a *(pf+2)


*(pf + n) endereça n elementos à frente;
*(pf - n) endereça n elementos atrás;
pf++ endereça o próximo elemento do array;
pf-- endereça o elemento anterior do array;

Operações válidas sobre ponteiros

1) somar ou subtrair um inteiro a um ponteiro (pi ± int)


2) incrementar ou decrementar ponteiros (pi++, pi--)
3) subtrair ponteiros (produz um inteiro) (pf - pi)
4) comparar ponteiros (>, >=, <, <=, ==)

Não é válido

1) somar ponteiros (pi + pf)


2) multiplicar ou dividir ponteiros (pi * pf, pi / pf)
3) operar ponteiros com double ou float (pi ± 2.0)

6.3.1 Exercícios propostos

Exercício 6.2 – Escreva um programa que imprima um array de inteiros na ordem


inversa endereçando os elementos com um ponteiro.

Exercício 6.3 – O C não controla os limites dos arrays, o programador deve fazê-lo.
Encontre os erros e corrija-os:
a)
void main(){
int arint[] = {1,2,3,4,5,6,7};
int size = 7, i, *pi;
for (pi = arint, i = 0; i < size; i++, pi += 2)
printf("%d", *pi);
}
b)
void main(){
int arint[] = {1,2,3,4,5,6,7};
int size = 10;
int i;
for (pi = arint, i = 0; i < size; i++)
printf("%d", arint[i]);
}

95
6.4 Ponteiro genérico

Um ponteiro genérico é um ponteiro que pode apontar para qualquer tipo de


dado. Define-se um ponteiro genérico utilizando-se o tipo void:

Exemplo 6.3 - Definindo um ponteiro para void.

#include <stdio.h>
#include <conio.h>

main(){

void *pv;
int x = 10;

float f = 3.5;

pv = &x; /* aqui pv aponta para um inteiro */


pv = &f; /* aqui, para um float */
}

O tipo de dado apontado por um ponteiro void deve ser controlado pelo
programador. Usando um type cast (conversão de tipo) o programa pode tratar
adequadamente o ponteiro.

Exemplo 6.4 - Definindo um ponteiro para void e acessando o conteúdo apontado


por ele com conversão de tipo.

#include <stdio.h>
#include <conio.h>

main(){

void *pv;
int x = 10;
float f = 3.5;

pv = &x; /* aqui pv aponta para um inteiro */


printf("Inteiro: %d\n", *(int *)pv); /*=> 10*/
pv = &f; /* aqui, para um float */
printf("Fracionario: %f\n", *(float *)pv); /*=> 3.5*/

getch();
return(0);
}

6.5 Ponteiro como argumento de funções

A passagem por valor, padrão em C, não permite que uma função manipule
diretamente uma variável que lhe esteja sendo passada. Quando se deseja manipular

96
diretamente a variável, ponteiros devem ser usados como o recurso de acesso. Para
ilustrar esta condição, imagine como implementar a rotina troca que recebe dois
argumentos de um mesmo tipo e troca seus valores. Por exemplo, para trocar dois
inteiros, algo similar à seguinte função seria desejado (ex. 6.5):

Exemplo 6.5 - Definindo uma função capaz de trocar dois valores (errado).

#include <stdio.h>
#include <conio.h>

void troca_erro(int el1, int el2);

main(){

int x = 2, y = 3;

printf("x = %d e y = %d\n", x, y);

troca_erro(x,y);

printf("x = %d e y = %d\n", x, y);

getch();
return(0);
}

void troca_erro(int el1, int el2){


int temp; /* variavel temporaria */

temp = el1;
el1 = el2;
el2 = temp;
}

Entretanto, como pode ser observado, no comentário inicial, esta função não
funciona. Supondo que a função main() do programa anterior (ex. 6.5) tente acessar
esta rotina, tem-se como resultado o seguinte:

x = 2, y = 3
x = 2, y = 3

Como se observa, a função troca_erro não realiza a troca de valores das


variáveis x e y de main(), apesar de sua lógica interna estar correta. O que
troca_erro faz é trocar os valores das cópias destas variáveis, que são apenas suas
variáveis locais.
A fim de obter-se o efeito desejado, ponteiros devem ser utilizados como
argumentos. Assim, os elementos a serem trocados serão acessados por seus endereços,
e seus conteúdos serão efetivamente alterados. Nesta nova versão (ex. 6.6), a função
troca é definida como:

Exemplo 6.6 - Definindo uma função capaz de trocar dois valores (correto).

97
#include <stdio.h>
#include <conio.h>

void troca(int *el1, int *el2);

main(){
int x = 2, y = 3;

printf("x = %d e y = %d\n", x, y);

troca(&x,&y);

printf("x = %d e y = %d\n", x, y);

getch();
return(0);
}

void troca(int *el1, int *el2){


int temp; /* variavel temporaria */

temp = *el1;
*el1 = *el2;
*el2 = temp;
}

A chamada à função deve passar os endereços das variáveis, como em

troca(&x,&y);

a saída obtida neste caso seria:

x = 2, y = 3
x = 3, y = 2

como desejado.

Outros usos de ponteiros como argumentos incluem funções que devem retornar
mais de um valor e a passagem de arranjos para funções. Quando um arranjo é passado
para uma função, na verdade o que se passa é o endereço de seu primeiro elemento.
Quando o argumento é um arranjo multidimensional, apenas a dimensão do primeiro
índice pode ser omitida - as demais devem ser fornecidas. Caso contrário, seria
impossível saber como acessar corretamente os elementos do arranjo.

6.6 Ponteiros e estruturas

Uma vez que variáveis do tipo estrutura são tratadas exatamente da mesma forma
que variáveis de tipos básicos, é possível definir variáveis do tipo ponteiro para
estruturas, como em:

struct dados_pessoais *pa;

98
Componentes de uma estrutura podem ser ponteiros para outros tipos de dados
ou estruturas. Em algumas situações, pode haver a necessidade de ter como um dos
componentes da estrutura um ponteiro para um tipo da própria estrutura. Um exemplo
típico é a construção de listas ligadas (será visto em detalhes no capítulo 7), compostas
por nós onde um dos dados armazenados em cada nó é um ponteiro para o próximo nó.
Esta situação seria representada como:

struct no_lista {
/* conteudo do no: */
...
/* ponteiro ao proximo no: */
struct no_lista *proximo;
};

Uma forma básica de acesso aos membros de uma estrutura (por meio do
operador (.) ponto) já foi descrita. A outra forma de acesso aos membros das
estruturas facilitam a notação quando ponteiros para estruturas estão envolvidos. Para
ilustrar esta forma, suponha que uma função que leia os dados de um aluno no programa
a seguir (ex. 6.7) retorne na verdade um ponteiro para uma estrutura do tipo
dados_pessoais.

struct dados_pessoais *le_aluno(); /* prototipo */


struct dados_pessoais *aluno_pt; /* ponteiro */
aluno_pt = le_aluno();

O acesso a membros da variável aluno_pt poderia ser feito da forma usual,


ou seja, *aluno_pt é uma estrutura, então seria possível acessar seus membros como
em:

printf("%s nasceu em %2d/%2d/%4d\n",


(*aluno_pt).nome,
(*aluno_pt).nascimento.dia,
(*aluno_pt).nascimento.mes,
(*aluno_pt).nascimento.ano);

A notação simplificada utiliza o ponteiro -> (seta) para substituir uma


construção na forma (*A).M por A -> M.

Exemplo 6.7 - Calculando a idade de um aluno usando funções e ponteiros.

#include <stdio.h>
#include <conio.h>

struct data{
int dia;
int mes;
int ano;
};

struct dados_pessoais{
char nome[40];

99
struct data nascimento;
}pessoa;

int calc_idade(struct dados_pessoais *p, struct data h);


struct data le_hoje(void);
struct dados_pessoais *le_aluno(void);

main(){

struct data hoje;


struct dados_pessoais *aluno_pt;
int idade;

hoje = le_hoje();
getchar(); /* Limpa o buffer de entrada */
aluno_pt = le_aluno();
idade = calc_idade(aluno_pt, hoje);

printf("Idade de %s: %d\n", aluno_pt->nome, idade);

getch();
return(0);
}

struct data le_hoje(void){

struct data h;
printf("\nEntre com o dia de hoje: ");
scanf("%d",&h.dia);
printf("\nEntre com o mes atual: ");
scanf("%d",&h.mes);
printf("\nEntre com o ano corrente: ");
scanf("%d",&h.ano);

return(h);
}

struct dados_pessoais *le_aluno(void){

struct dados_pessoais *pp;

pp = &pessoa;

printf("\nEntre com o seu nome: ");


gets(pp->nome);
printf("\nEntre com o dia de seu nascimento: ");
scanf("%d",pp->nascimento.dia);
printf("\nEntre com o mês de seu nascimento: ");
scanf("%d",pp->nascimento.mes);
printf("\nEntre com o ano de seu nascimento: ");
scanf("%d", pp->pp->nascimento.ano);

return(pp);
}

100
int calc_idade(struct dados_pessoais *p, struct data h){

int idade;
idade = h.ano – p->nascimento.ano;

return(idade);
}

para realizar o cálculo.

6.7 Ponteiros para funções

Como foi visto no Capítulo 1, um programa é um conjunto de instruções


armazenado na memória, assim como seus dados. Por este motivo, é possível referenciar
o endereço de uma função. Em C, o endereço de uma função é acessível ao
programador por meio de uma variável do tipo ponteiro para função.
Ponteiros para funções podem ser passados como argumentos para outras
funções, e a função apontada pode ser invocada a partir de seu ponteiro. Um exemplo
prático desta capacidade é seu uso em uma rotina de ordenação de elementos de um
arranjo. Se o arranjo é de inteiros, então uma função de comparação de inteiros deverá
ser suportada, tal como:

/*
* compara dois inteiros, retornando:
* 0 se os dois elementos forem iguais
* um inteiro negativo se o primeiro elemento for menor
* um inteiro positivo se o primeiro elemento for maior
*/

int comp_int(int *e1, int *e2){


return(*e1 - *e2);
}

O problema surge quando se deseja usar o mesmo algoritmo de ordenação para


ordenar outros arranjos de tipos que não sejam inteiros. Por exemplo, se os elementos a
comparar forem strings, então a rotina de comparação acima não mais serviria, apesar de
todo o restante do algoritmo de ordenação ainda ser basicamente o mesmo.
A forma de declarar uma variável do tipo ponteiro para função é ilustrada no
seguinte exemplo (ex. 6.8), com uma referência à função comp_int() definida acima:

Exemplo 6.8 - Determinando o maior valor entre dois números usando um ponteiro
para uma função.

#include <stdio.h>
#include <conio.h>

/* prototipo de comp_int: */
int comp_int(int , int);

main(){

101
/* ponteiro para uma funcao retornando inteiro */
int (*apcmp)();

int a = 4, b = 5, c;
apcmp = &comp_int; /* inicializa ponteiro */

c = (*apcmp)(a, b); /* invoca funcao */

printf("\nMaior: %d", c);

getch();
return(0);
}

int comp_int(int x, int y){


return((x > y) ? x : y);
}

Algumas observações relativas a este exemplo (ex. 6.8) são importantes. A


primeira refere-se à declaração do ponteiro. A declaração de um ponteiro para a função
deve incluir os parênteses em torno do nome da variável ponteiro. Uma definição na
forma int *apcmp(); seria interpretada como o protótipo de uma função
retornando um ponteiro para um inteiro, o que não é o desejado neste caso.
A segunda observação refere-se à forma utilizada para definir o valor do ponteiro
no comando de atribuição. Como o protótipo de comp_int já havia sido definido,
então o compilador sabe que este identificador refere-se a uma função. Quando o
identificador comp_int é encontrado novamente, desta vez sem parênteses, ele é
identificado como o endereço desta função, podendo assim ser atribuído a um ponteiro
para uma função com o mesmo tipo de retorno. Repare a semelhança com referências a
nomes de arranjos.
Finalmente, a invocação da função por meio de seu ponteiro: a forma usando o
operador de referência (*apcmp) indica o conteúdo do ponteiro apcmp, que é a
função (neste caso, comp_int). Assim, a última linha no exemplo é apenas uma
invocação para a rotina apontada por apcmp, e o que vem a seguir de (*apcmp) são
simplesmente os argumentos para a função. O padrão ANSI também permite que a
forma equivalente,

apcmp(a, b);

seja utilizada. Muitos programadores preferem a forma apresentada no exemplo original


para tornar claro que um ponteiro para função está sendo usado, embora internamente
não haja diferenças entre a ativação de uma função por seu nome ou por meio de um
ponteiro.
Ponteiros para funções tornam-se interessantes quando o programador não pode
determinar qual função deve ser executada em uma dada situação há não ser durante a
execução do programa. Em tais casos, o trecho do programa referenciando esta “função
variável” pode ser escrito em termos de ativação de uma função por meio de ponteiros
para funções, os quais são corretamente inicializados em tempo de execução.

102
6.8 Exercícios propostos

Exercício 6.4 – Procure determinar quais valores são impressos ao final deste
programa. Confira sua resposta testando o programa na ferramenta de
programação (C Quietly). Execute-o passo a passo conferindo o valor
das variáveis em cada momento.

a) void main(){
int a,b,*c;
a = 3;
b = 4;
c = &a;
b++;
*c = a+2;
printf("%d %d",a,b);
}

b) void main(){
int a,b,*c;
a = 4;
b = 3;
c = &a;
*c = *c +1;
c = &b;
b = b+4;
printf("%d %d %d",a,b,*c);
}

c) void main(){
int a,b,*c,*d,*f;
a = 4;
b = 3;
c = &a;
d = &b;
*c /= 2;
f = c;
c = d;
d = f;
printf("%d %d",*c,*d);
}

d) int calcula(int);
void main(){
int a, b, c;
char d;
a = 1;b = 2;c = 3;d = 'A';
a += b * c;
d = (a > 7) ? d - 1 : d + 1;
b = calcula(b);
c = calcula(calcula(a));
a = c++;
printf("%d - %d - %d - %c\n", a, b, c, d);
}

103
int calcula(int x){
int i;
if ((x = x * 2) > 5) return(x + 3);
for(i = 0;i < 10; i++){
if (i < 5) continue;
if (x > 8) break;
x += 2;}
return(x);
}

Exercício 6.5 – Ciar um programa que utilize um ponteiro para exibir uma string
inserida pelo usuário. Mostrando a string completa num único
comando.

Exercício 6.6 – Repita o exercício anterior (ex. 6.5) mostrando agora caractere a
caractere.

Exercício 6.7 – Desenvolva um programa que leia uma frase e um caractere e procure
utilizando uma função que devolve um ponteiro para a posição do
caractere na string lida.

Exercício 6.8 – Utilize a função do exrcício anterior (ex. 6.7) para imprimir a string a
partir de um caractere informado.

104
7 Estruturas de dados

Apresentam-se a seguir os conceitos de filas, pilhas e listas como estruturas de


armazenamento de dados. Estas estruturas são criadas a partir de tipos elementares,
vetores e estruturas.

7.1 Filas

Uma FILA é uma coleção ordenada de elementos na qual estes podem ser
retirados de uma das extremidades da fila conhecida como início ou frente da fila. Novos
elementos podem ser inseridos na outra extremidade, ou seja, fim da fila.

Filas: First In First Out (FIFO).

Um exemplo de aplicação é chamado de buffer como em uma comunicação


assíncrona entre dois equipamentos, especialmente quando um deles é mais lento que o
outro. Exemplo: computador-impressora.

Operações elementares

Insere (fila, elemento)


Insere novo elemento no fim da fila.
Ex.: Insere(fila,'D');

elemento = Remove (fila)


Remove elemento da frente da fila.
Ex.: elemento_frente = Remove (fila);

Um elemento só pode ser retirado da fila, se ela não estiver vazia, evidentemente.
Assim como, só se pode inserir um elemento em uma fila que não esteja cheia.

Operações auxiliares

FilaVazia (fila)
Retorna V (1) se a fila estiver vazia e F (0) caso contrário.
Ex.: if (FilaVazia(fila))
printf("Fila vazia");

105
FilaCheia (fila)
Retorna V (1) se a fila estiver cheia, e F (0) caso contrário.
Ex.: if (FilaCheia(fila))
printf("Fila Cheia");

InicializaFila (fila)
Cria fila vazia.
Ex.: InicializaFila(fila);

Declaração do tipo de dados fila

Usar um vetor para guardar os elementos da fila e as duas variáveis frente e


final para guardar as posições (índices), dentro do vetor, do primeiro e do último
elemento da fila, respectivamente. A declaração do tipo de dados fila cujos elementos
são valores inteiros aparece no exemplo a seguir (ex. 7.1)

Exemplo 7.1 - Definindo uma estrutura de fila.

...

#define MAXFILA 20

typedef int TipoDado;

typedef struct{
TipoDado dados[MAXFILA];
int frente, final;
} Fila;

...

Uma fila vazia é definida como tendo o fim "antes" da frente, ou seja, final <
frente. Inicialmente a fila vazia deve ter: frente = 0 e final = -1.

Implementação das operações: SOLUÇÃO 1

void InicializaFila (Fila *f){


f->frente = 0; /* valores iniciais */
f->final = -1;
}

int FilaVazia (Fila *f){


/* a fila está vazia quando final < frente */
}

int FilaCheia (Fila *f){


/* A fila está cheia quando final == MAXFILA - 1 */
}

void Insere (Fila *f, TipoDado dado){


/* se fila cheia, não faz nada (ou erro)
se fila não estiver cheia, incremente o valor do final

106
coloque dado na posição final dos dados da fila */
}

TipoDado Remove (Fila *f){


/* se fila vazia, não faz nada (ou erro)
guarda elemento da posição frente da fila
soma 1 à frente da fila
retorne elemento guardado */
}

Exercício 7.1 – Implemente um programa em C as operações sobre fila conforme o


modelo proposto anteriormente. Considere MAXFILA = 5 e as
seguintes seqüências de operações sobre uma fila f:

InicializaFila(f);
Insere(f,1);
Insere(f,2);
Insere(f,3);
x = Remove(f);
x = Remove(f);
Insere(f,4);
Insere(f,5);
x = Remove(f);
x = Remove(f);
x = Remove(f);

No final destas operações, f->final == 4 e f->frente == 5. Isto


significa que a fila está vazia e ao mesmo tempo está cheia! Problema...

Implementação das operações: SOLUÇÃO 2

Alterar a operação Remove de forma a deslocar todos os elementos para a


"esquerda" (posição zero do vetor) cada vez que o elemento da frente for retirado (o
elemento da frente estará sempre na posição zero).

Exercício 7.2 – Altere a operação remove para implementar esta solução.

Esta solução, apesar de correta é muito ineficiente. Imagine deslocar todos os


elementos de uma fila com milhares de elementos para se retirar apenas àquele que está à
frente!

Implementação das operações: SOLUÇÃO 3 - Vetores circulares

Ver o vetor contendo os elementos da fila como sendo circular ao invés de linear.

Problema - determinar quando a fila está vazia.

Solução - frente é o índice do vetor que precede o índice do primeiro


elemento.

Se i é índice de um vetor circular:

107
Prox(i) = 0, se i == MAXFILA-1
i+1, caso contrário.

A fila estará vazia quando frente == final.


A fila está cheia quando, fim vier logo antes de frente no vetor circular, isto é,
Prox(final) == frente.

Exercício 7.3 – Implemente em C as operações sobre filas considerando o vetor de


dados como sendo circular.

7.1.1 Filas com prioridades

Uma fila com prioridades é uma estrutura de dados na qual a ordem intrínseca
dos elementos determina os resultados das suas operações básicas, em particular, a
operação Remove.

Tipos

1) Fila com prioridade ascendente: é uma coleção de elementos na qual


novos elementos podem ser inseridos normalmente (como em filas sem
prioridade) e da qual somente o MENOR elemento pode ser removido.

2) Fila com prioridade descendente: é uma coleção de elementos na qual


novos elementos podem ser inseridos normalmente (como em filas sem
prioridade) e da qual somente o MAIOR elemento pode ser removido.

3) Elementos com mesmo valor (ou mesma prioridade) são retirados na


ordem em que foram inseridos, seguindo a regra para filas normais ou FIFO.

Aplicações: Transmissão assíncrona com prioridades (cancelamento de job de


impressão, ...).

Normalmente, os elementos de uma fila com prioridades são compostos de dois


campos: os dados propriamente ditos + as prioridades dos dados.

dado A C D F H K
prioridade 1 7 4 3 2 5
índice 0 1 2 3 4 5

Figura 7.1 – Fila com prioridade descendente.

Se a fila anterior (fig 7.1) for uma fila com prioridade descendente, o primeiro
elemento a sair será o C e não o A que está na frente da fila, pois aquele (o C) tem
prioridade maior (7).

108
Uma pilha pode ser vista como sendo uma fila com prioridades descendentes
cujos elementos estão ordenados pelo instante no tempo representando a ordem de
inserção.
Todas as operações em filas com prioridades são idênticas às operações em filas
normais, exceto a operação Remove, na qual o elemento que tiver maior ou menor
prioridade deve ser retirado, estando ele ou não na frente da fila.

int RemovePriorDesc(Fila *f){


/* busca o maior elemento na fila guardando-o em
variável auxiliar */
/* desloca o fim ou começo da fila para preencher lugar
do maior elemento*/
/* retorna o maior elemento armazenado na variável
auxiliar */
}

Exercício 7.4 – Altere/Implemente a operação RemovePriorDesc acima em C, de


forma a remover o maior elemento da uma fila de inteiros.

Exercício 7.5 - Elabore um programa que implemente uma FILA controlada por um
array de ponteiros para char *, char *ptr[DIM]. A fila deve
armazenar datas de reuniões que vão sendo alocadas por ordem de
entrada e vão sendo retiradas de acordo com o princípio F.I.F.O.

7.2 Pilhas

O problema do abre/fecha parênteses. Este problema consiste em verificar se uma


expressão matemática está corretamente formada em termos de abre/fecha parênteses.
Com este exemplo (ex. 7.2) pretende-se mostrar a aplicabilidade de pilhas como
estruturas de armazenamento de dados.

Exemplo 7.2 - Expressões matemáticas diversas.

7-((X*((X+Y)/(J-3))+Y)/(4-2.5))
((A+B)
) A + B ( - C
(A + B)) - (C + D

Solução:

O número de “)” deve ser igual ao número de “(“.


Cada ")" deve ser precedido por um "(".

Para tanto, pode-se utilizar um contador inicialmente igual a zero que, ao


percorrer a expressão da esquerda para a direita, é incrementado quando se encontra um
"(" e decrementado quando se encontra um ")".
O contador no final da expressão deve ser igual a zero.
Em nenhum momento o contador deve ser menor que zero.

109
Exercício 7.6 – Implemente um programa C que leia uma expressão com parênteses e
verifique se ela está corretamente formada em termos de abre/fecha
parênteses usando o método acima.

E em caso de existência também de chaves e de colchetes? E a ordem chaves-


colchetes-parênteses seja importante? Solução complicada...

Melhor solução - a estrutura de dados pilha

Uma pilha é uma coleção ordenada de elementos na qual os elementos são


inseridos e retirados de uma das extremidades somente, chamada topo da pilha.
Tipicamente, todos os elementos da pilha são do mesmo tipo (por exemplo,
char, int, float ou até struct).
Vetores permitem com que elementos sejam inseridos e lidos em qualquer
posição. Mas em pilhas, a inserção/remoção de elementos somente pode acontecer no
topo.
O último elemento inserido em uma pilha é o primeiro a ser retirado e por esta
razão, uma pilha é uma estrutura "LIFO" (Last In First Out).

Como o problema do abre e fecha: parênteses, chaves e colchetes pode ser


resolvido com uma pilha de caracteres?

Outras aplicações de pilhas: desfazer (undo); voltar; endereço de retorno de


funções pelo registrador; etc.

Operações elementares

Push (pilha, elemento)


Insere novo elemento no topo da pilha.
Ex.: Push(pilha,'A');

elemento = Pop (pilha)


Remove elemento do topo da pilha.
Ex.: elemento_topo = Pop (pilha);

Operações auxiliares

PilhaVazia (pilha)
Retorna V (1) se a pilha estiver vazia, e F (0) caso contrário.
Ex.: if (PilhaVazia(pilha))
printf("Pilha vazia");

PilhaCheia (pilha)
Retorna V (1) se a pilha estiver cheia, e F (0) caso contrário.
Ex.: if (PilhaCheia(pilha))
printf("Pilha Cheia");

InicializaPilha (pilha)
Cria pilha vazia.

110
Ex.: InicializaPilha(pilha);

Declaração do tipo de dados pilha

Coleção ordenada em C, vetores.


Uma pilha em C pode ser definida como sendo uma estrutura contendo dois
campos:
1) Um vetor contendo os dados ("dados"); e
2) Um inteiro indicando a posição do topo no vetor ("topo").

Exemplo 7.3 - Declaração do tipo de dados pilha cujos elementos são valores inteiros.

...

#define MAXPILHA 200

typedef int TipoDado;

typedef struct{
TipoDado dados[MAXPILHA];
int topo;
} Pilha;

...

Implementação das funções

void InicializaPilha (Pilha *p){


/* inicializa topo da pilha end. de p (valor = -1) */
}

int PilhaVazia (Pilha *p){


/* retorna 1 (V) se topo p é -1. 0 (F) c.c. */
}

int PilhaCheia (Pilha *p){


/* retorna 1 (V) se topo p == MAXPILHA-1. 0 (F) c.c. */
}

void Push (Pilha *p, TipoDado x){


/* se pilha p cheia, erro */
/* senão, incremente o valor do topo de p */
/* coloque x no vetor dados na posição topo de p */
}

TipoDado Pop (Pilha *p){


/* se pilha p vazia, erro */
/* c. c., armazene elemento do topo da pilha p */
/* decremente o topo de p */
/* retorne elemento armazenado */
}

TipoDado elemTopo(Pilha *p){

111
/* se pilha p vazia, erro */
/* senão, retorne elemento armazenado */
}

Exercício 7.7 – Implemente em C as operações sobre pilhas acima. Crie inicialmente


um tipo pilha no qual os elementos são valores inteiros. Crie um
programa principal que leia 10 valores colocando-os um a um na pilha
que, em seguida deve ser esvaziada.

Exercício 7.8 – Altere o programa do exercício anterior (ex. 7.7) para que implemente
uma pilha na qual os elementos são do tipo char.

Notação completamente Parentizada: acrescentam-se sempre parênteses a cada par


de operandos e seu operador.

Exemplo: tradicional: A * B - C / D
parentizada: ((A*B)-(C/D))

Notação Polonesa: os operadores aparecem imediatamente antes dos operandos. Esta


notação especifica quais operadores e em que ordem eles devem
ser calculados. Por esse motivo dispensa o uso de parênteses, sem
ambigüidades.

Exemplo: tradicional: A * B - C / D
polonesa: - * A B / C D

Notação Polonesa Reversa (ou pós-fixa): é como a polonesa, exceto que os


operandos aparecem após os operadores.

Exemplo: tradicional: A * B - C / D
polonesa reversa: A B * C D / -

Exemplo 7.4 - Exemplo de uso do método RPN para cálculo de: (3+5) ÷ (7+6) = x.

Método algébrico: some 3 + 5 = 8. Escreva a resposta ou guarda na memória. Some 7 +


6 = 13. Agora digite 8 da primeira resposta e a seguir divida-o
digitando a segunda resposta para obter x = 0,62.

Método RPN: digite 3 e depois a tecla ENTER. Digite 5 e depois a tecla +. Digite 7
e depois ENTER. Digite 6 e depois a tecla +. Note que a resposta
para a segunda soma é exibida. Agora aqui está a parte mágica.
Pressione a tecla de dividir e a calculadora exibe a resposta 0,62.

Algébrico: 13 toques, não contando o esforço de escrever ou memorizar a


primeira resposta enquanto calcula a segunda resposta.

RPN: 9 toques e não há necessidade de escrever nada.

Funcionamento das calculadoras RPN

112
O RPN mantém um registro dos cálculos colocando-os em uma pilha (2). No
exemplo acima, quando você pressionar a tecla ENTER pela segunda vez, a resposta da
primeira soma foi empurrada (3) para cima na pilha, aguardando a próxima ação. Após
digitar uma segunda soma, pressione a tecla dividir para chamar a primeira soma, dividi-
la pela segunda e recuperar (4) a resposta da pilha. Em outras palavras, o RPN executou
um cálculo em uma ordem lógica.
Aprender como usar uma calculadora RPN geralmente leva apenas alguns
minutos e pode economizar muito tempo e esforço a longo prazo. Aqui está um exemplo
prático que usa RPN.

1) Modo algébrico: este é o nome da notação matemática usada em todas as


calculadoras que não usam RPN quando você digitar uma equação
matemática como esta 1+3*(3+(2-5)/3). No modo algébrico, parêntesis e a
ordem dos operadores são extremamente importantes.

2) Pilha: uma pilha, também chamada LIFO é a base do sistema RPN, assim
como a memória que permite ao usuário digitar números.

3) Empurrar/empurrando: esta é a ação de adicionar o número na base de


uma pilha empurrando todos os outros números para cima.

4) Limpar/limpando: esta é a ação de remover o último número que foi


empurrado em uma pilha.
Fonte: http://h30091.www3.hp.com/produtos/calculadoras/rpn.html

Algoritmo para transformação de notação infixa para pós-fixa com parênteses

Passos para criação de um programa para transformação de notação infixa para


pós-fixa ou Notação Polonesa Reversa.

Passo 1: Entrar com uma expressão devidamente com parênteses;

Passo 2: Percorrer a expressão da esquerda para a direita, e para cada símbolo


encontrado tomar a seguinte decisão:
Se for parêntese de abertura, ignorá-lo;
Se for operando, copiá-lo para a expressão pós-fixa (saída);
Se for operador, colocá-lo na pilha;
Se for parêntese de fechamento, desempilhar o operador presente
no topo da pilha;

Ao final do passo 2, a pilha deve estar vazia, caso contrário algo de errado
ocorreu na conversão da notação infixa para pós-fixa. Veja exemplo (ex. 7.4) a seguir.

Exemplo 7.4 - Conversão de notação infixa completamente parentizada para pós-fixa.


Para o perfeito funcionamento deste programa é necessário a construção
de outras funções auxiliares.

...
InicializaPilha(p);

113
i = pos = 0;

while (infixa[i] != '\0'){


if (infixa[i] >= ‘A’ && infixa[i] <= ‘Z’){
posfixa[pos++] = infixa[i];
}
else{
switch (infixa[i]){
case ‘*’ :
case ‘/’ :
case ‘+’ :
case ‘-’ : Push(p, infixa[i]);
break;

case ‘)’ : posfixa[pos++] = Pop(p);


break;
}
}
i++;
}

posfixa[pos] = ‘\0’;
...

Exercício 7.9 – Implemente em C as operações sobre pilhas da Notação Polonesa


Reversa para cálculo de uma expressão neste formato. Faça os teste de
evolução da pilha.

Algoritmo genérico para conversão de notação infixa para pós-fixa

Para que a solução apresentada anteriormente funcione corretamente é necessário


inserir manualmente todos os parênteses com o objetivo de determinar a precedência de
operações. O programa (ex. 7.5) a seguir pretende eliminar a esta necessidade
estabelecendo prioridades para os operadores.

Exemplo 7.5 - Usando uma escala de prioridade para os operadores de uma notação
infixa. Para o perfeito funcionamento deste programa é necessário a
construção de outras funções auxiliares.

...
switch (infixa[i]){
case ‘(’ : prioridade = 1;
break;

case ‘-’ :
case ‘+’ : prioridade = 2;
break;

case ‘*’ :
case ‘/’ : prioridade = 3;
break;
}
...

114
Após determinar a prioridade dos operadores sugere-se a seqüência de passos a
seguir, o programa é apresentado logo na seqüência (ex. 7.6).

Passo 1: Iniciar uma pilha vazia;


Realizar uma varredura na expressão infixa, copiando todos os
operandos diretamente para expressão de saída;

Passo 2: Ao encontrar um operador:


Enquanto a pilha não estiver vazia e houver um operador no seu topo
de prioridade maior ou igual ao encontrado na expressão infixa,
desempilhe o operador e copie-o na a expressão de saída;
Empilhe o operador encontrado na expressão;

Passo 3: Ao encontrar parêntese de abertura, empilhe-o;

Passo 4: Ao encontrar um parêntese de fechamento remova um símbolo da pilha


e copie-o na saída. Repita esse passo até que seja desempilhado o
parêntese de abertura;

Passo 5: Ao final da varredura, esvazie a pilha, movendo dessa forma os


símbolos desempilhados para a saída. Pronto! Desta forma é possível
obter na saída a notação pós-fixa, partindo da notação infixa de
entrada.

Exemplo 7.6 - Conversão de notação infixa em pós-fixa usando a escala de prioridade


definida anteriormente (ex. 7.5). Para o perfeito funcionamento deste
programa é necessário a construção de outras funções auxiliares.

...

InicializaPilha(p);

i = pos = 0;

while (infixa[i] != '\0'){


if (infixa[i] >= ‘A’ && infixa[i] <= ‘Z’){
posfixa[pos++] = infixa[i];
}
else{
switch (infixa[i]){
case ‘*’ :
case ‘/’ :
case ‘+’ :
case ‘-’ : pr = prioridade(infixa[i]);
while (!PilhaVazia(p) &&
prioridade(elemTopo(p))>=pr){
posfixa[pos++] = Pop(p);
}
Push(p, infixa[i]);
break;

115
case ‘(’ : Push(p, infixa[i]);
break;

case ‘)’ : x = Pop(p);


while (x != ‘(‘){
posfixa[pos++] = x;
x = Pop(p);
}
break;
}
}
i++;
}

while (!PilhaVazia(p)){
posfixa[pos++] = Pop(p);
}

posfixa[pos] = ‘\0’;
...

Avaliando as expressões em notação pós-fixa

As transformações realizadas até aqui têm o objetivo de facilitar a avaliação (ex.


7.7) da expressão, ou seja, resolvê-la levando em consideração os valores numéricos de
cada uma das variáveis presentes na expressão infixa e convertidas para a notação pós-
fixa.

Passo 1: Iniciar uma pilha vazia;

Passo 2: Varrer a expressão, e para cada símbolo encontrado:


Se for operando, empilha-se seu valor;
Se for operador, desempilhar os dois últimos valores. Em seguida,
efetuar a operação com eles. O resultado será empilhado novamente.

Passo 3: O resultado da avaliação da expressão estará no topo da pilha;

Exemplo 7.7 - Avaliação da expressão em notação pós-fixa. Para o perfeito


funcionamento deste programa é necessário a construção de outras
funções auxiliares.

...
InicializaPilha(p);

i = pos = 0;

while (posfixa[i] != '\0'){


if (posfixa[i] >= ‘A’ && posfixa[i] <= ‘Z’){
printf(“Entre com o valor de %c = ”, posfixa[i]);
scanf(“%d”,&val);
Push(p, val);
}

116
else{
switch (infixa[i]){
case ‘*’ :
case ‘/’ :
case ‘+’ :
case ‘-’ : y = Pop(p);
z = Pop(p);
switch (infixa[i]){
case ‘*’ : Push(p, z * y);
break;

case ‘/’ : Push(p, z / y);


break;

case ‘+’ : Push(p, z + y);


break;

case ‘-’ : Push(p, z - y);


break;
}
break;
}
i++;
}

resultado = Pop(p);
...

Exemplo 7.8 – A função fatorial.

...
int fatorial(unsigned int n){
int res = 1;

if (n > 0)
res = n * fatorial(n-1);

return(res);
}
fatorial(5)
=> (5 != 0)
return(5 * fatorial(4))
=> (4 != 0)
return(4 * fatorial(3))
=> (3 != 0)
return(3 * fatorial(2))
=> (2 != 0)
return(2 * fatorial(1))
=> (1 != 0)
return(1 * fatorial(0))
=> (0 == 0)
<= return(1)
<= return(1 * 1) (1)
<= return(2 * 1) (2)

117
<= return(3 * 2) (6)
<= return(4 * 6) (24)
<= return(5 * 24) (120)

120

7.2.1 Exercícios propostos

Exercício 7.10 – Calcule manualmente o valor das expressões em notação pós-fixa a


seguir. Assuma que os valores de A, B, C e D são respectivamente, 5,
6, 7, 8.

AB*C–D+ ABC+D-
ABC+*D/ AD/CB+-

Exercício 7.11 – Transforme as expressões a seguir de infixa em notação pós-fixa.

A +B*C–D ABC+D-
A/B-C+D (A + D) / (C – B) + E / F

Exercício 7.12 – Usando o algoritmo genérico de conversão da notação infixa para pós-
fixa, passe a expressão aritmética a seguir para a conversão pós-fixa.
Mostre a evolução da pilha para cada caractere da expressão. Ao final
permita o cálculo do valor da expressão para valores fornecidos pelo
usuário. Sendo: A * B – (C / D * E – F / G) + H + I.

Exercício 7.13 – Desenvolva um programa que inverta o conteúdo de uma pilha, ou


seja, o elemento do topo passe a ser a base, o elemento antes do topo
será o segundo após a base e assim por diante. Diga: utilize outra pilha.

Exercício 7.14 – Crie um programa que faça a conversão de números decimais para
binários.

7.3 Listas lineares ligadas

Vetores estavam sendo utilizados para o armazenamento de dados nas seções


anteriores. Eles facilitam o entendimento e compreensão de filas e pilhas, porém
apresentam alguns inconvenientes, tais como subdimensionamento ou superdimensio-
namento do tamanho do vetor. Caso tenham-se três vetores v1, v2 e v3 com capacidade
de 50, 200 e 300 posições respectivamente e, durante a execução do programa, tenha
sido necessário armazenar 60 posições em v1, 140 em v2 e 240 em v3. Fica evidente que
ocorrerá um estouro em v1, enquanto v2 e v3 ocupam áreas de memória, que não estão
sendo utilizadas. Para evitar tais situações é que a alocação dinâmica de memória é
apresentada a seguir.

118
Uma lista ligada ou encadeada é uma estrutura de dados linear e dinâmica. Ela é
composta por células que apontam para o próximo elemento da lista. Para se ter uma
lista ligada basta guardar seu primeiro e seu último elemento, sendo que este último
aponta para uma célula nula. O esquema a seguir (fig. 7.2) representa uma lista ligada
com 4 elementos:

Célula 1 Célula 2 Célula 3 Célula 4 Nulo

Figura 7.2 – Lista ligada simples.

Para manipular estas listas, ou seja, inserir ou remover dados é necessário ter-se
sempre em atenção um ponteiro que aponte para o 1º elemento e outro que aponte para
o fim. Isto porque ao inserir ou apagar dados que estejam no início ou no fim da lista a
operação é rapidamente executada. Caso seja um nó que se encontra no meio da lista
haverá a necessidade de uma procura até encontrar a posição desejada. Esta seção
apresenta uma visão sobre a alocação de memória necessária para construção de listas,
além de algumas funções imprescindíveis para o entendimento das mesmas. Muitas
destas funções estão incompletas ou a implementar, o que deve despertar o interesse no
desenvolvimento das mesmas por parte do programador.

Vantagens

A inserção de um elemento no meio da lista não implica mover todos os


elementos.

Desvantagens

O acesso seqüencial, para eliminação ou inserção de um elemento no meio da


lista.

Espaço alocado para uma estrutura

struct aluno{
char *nome; /* ponteiro 2 bytes */
short idade; /* 2 bytes */
char matricula[8]; /* array 8 bytes */
};
struct aluno al;

al.nome = "John";
al.idade = 30;
strcpy(al.matricula, "00/0001");

7.3.1 Função sizeof()

A função sizeof(tipo) retorna o tamanho em bytes ocupado em memória


pelo tipo de dado passado como parâmetro. Ex.:

119
sizeof(int) => 2 bytes
sizeof(char) => 1 byte

7.3.2 Alocação dinâmica de memória

As funções calloc e malloc permitem alocar blocos de memória em tempo


de execução.

#include <alloc.h>

void * malloc();

retorna um ponteiro void para n bytes de memória não iniciados. Se não há memória
disponível malloc retorna NULL.

número de bytes alocados size_t n

#include <alloc.h>

void * calloc(size_t n, size_t size);

calloc retorna um ponteiro para um array com n elementos de tamanho size


cada um ou NULL se não houver memória disponível. Os elementos são iniciados em
zero.
O ponteiro retornado por malloc e calloc deve ser convertido para o tipo de
ponteiro que invoca a função.

int *pi = (int *) malloc (sizeof(int));


/* aloca espaço para um inteiro */

int *ai = (int *) calloc (n, sizeof(int));


/* aloca espaço para um array de n inteiros */

Toda memória não mais utilizada deve ser liberada por meio da função free().

free(ai); /* libera todo o array */


free(pi); /* libera o inteiro alocado */

Estruturas auto-referenciadas

É possível definir dentro de uma estrutura um ponteiro para a própria estrutura:

struct Qualquer{
tipo_dado_1 dado_1;
...
struct Qualquer *proximo;
}

proximo aponta para uma estrutura do mesmo tipo.

120
Lista de inteiros

Um exemplo de lista encadeada é a lista de inteiros, onde raiz é um ponteiro


para o primeiro nó da lista. Após definição da struct é alocada memória para
struct IntNode e seu ponteiro guardado em raiz. Um valor inteiro é atribuído à
raiz->dado e finalmente raiz->proximo aponta para NULL.

struct IntNode{
int dado;
struct IntNode *proximo;
} *raiz;

int main(){

raiz = (struct IntNode *)malloc(sizeof(struct


IntNode));

raiz->dado = 15;
raiz->proximo = NULL;
...
return(0);
}

Inserindo um segundo elemento na lista

A seguir apresenta-se uma forma de inserir um valor x a uma lista encadeada de


inteiros, sendo que este será inserido no fim da lista. O endereço (ponteiro) do novo nó
da lista é atribuído para pnode e em seguida pnode->dado recebe o valor i. Em
seguida a manipulação de ponteiro é realizada para garantir a integridade da lista, assim a
raiz que em um momento inicial apontava para o primeiro elemento agora irá apontar
para pnode. Enquanto pnode->proximo assume NULL.

struct IntNode *insere_int (int i){

struct IntNode *pnode;

pnode = (struct IntNode *)malloc(sizeof(struct


IntNode));

pnode->dado = i;
pnode->proximo = NULL ; /*terminador de lista*/
raiz->proximo = pnode;

return(pnode);
}

Algo não está muito certo com esta função, você não acha? Será que ela
funcionará para novas inserções? Algo precisa ser feito para melhorá-la!

Percorrendo a lista

121
Percorre-se uma lista encadeada por meio de um ponteiro, veja função exibe()
a seguir. Tendo-se o início da lista em raiz fica relativamente simples percorrer toda a
lista utilizando-se de um ponteiro móvel e de uma estrutura de repetição. De forma, o
ponteiro móvel pnode recebe o endereço do primeiro elemento da lista raiz e irá
receber sempre o endereço seguinte na lista, após apresentação do dado propriamente
dito, por meio de pnode->proximo.

void exibe(){

struct IntNode *pnode;

pnode = raiz;

while (pnode != NULL){


printf(“%d“, pnode->dado);
pnode = pnode->proximo;
}

Insere Nodo antes do início da lista (novo início)

Mudar o nó raiz e desta forma inserir um novo dado antes dele na da lista é
declarar um novo início para a lista. Para realizar esta tarefa basta alocar espaço para
uma nova estrutura e manipular corretamente os ponteiros da raiz e do novo espaço
alocado. pnode->proximo recebe o endereço do primeiro elemento da lista raiz e
raiz irá receber o endereço de pnode. Desta maneira, pnode torna-se o novo nó
raiz.

struct IntNode *insere_int (int i){

struct IntNode *pnode;

pnode = (struct IntNode *)malloc(sizeof(struct


IntNode));

if (pnode){
pnode->dado = i;
pnode->proximo = raiz;
raiz = pnode;
}
return(pnode);
}

Insere Nodo entre nodos da lista

Esta situação de inserção é bastante comum em lista encadeada ordenadas. Para


realizar esta tarefa é necessário alocar espaço para uma nova estrutura e manipular
corretamente os ponteiros da pos e do novo espaço alocado. A função irá receber o
endereço a partir do qual o novo nó deverá ser inserido. Desta maneira, pnode-
>proximo apontará para o endereço contido em pos->proximo e pos-
>proximo receberá o endereço de pnode, recentemente alocado. Será que esta função
permite também a inserção de um novo nó ao final da lista? Teste!

122
struct IntNode *insere_int (int i;
struct IntNode *pos){

struct IntNode *pnode;

pnode = (struct IntNode *)malloc(sizeof(struct


IntNode));

if (pnode){
pnode->dado = i;
pnode->proximo = pos->proximo;
pos->proximo = pnode;
}
return(pnode);
}

Imprime a lista

Esta função recebe o início da lista em pnode e lista todos os dados da lista.
Compare-a com a função exibe(), apresentada anteriormente.

void imprime(struct IntNode *pnode){

printf("\nLista = ");

while (pnode){
printf(" %d ", pnode->dado);
pnode = pnode->proximo;
}
}

Busca em uma lista encadeada

Veja como é fácil verificar se um inteiro x pertence a uma lista encadeada, ou


seja, se ele é igual ao conteúdo de algum nó ou célula da lista. A função busca(x,
ini) recebe um inteiro x e uma lista encadeada de inteiros. O endereço da lista é ini e
a função devolve o endereço de uma célula que contém x. Se tal célula não existe, a
função devolve NULL.

struct IntNode *busca(int x, struct IntNode *ini){

struct IntNode *pnode;


pnode = ini;

while (pnode != NULL && pnode->dado != x)


pnode = pnode->proximo;

return(pnode);
}

Remoção em uma lista

123
Para remover uma certa célula da lista, a idéia mais óbvia seria apontar para a
célula que se quer remover. Pode-se perceber que essa idéia não é muito boa. É melhor
apontar para a célula anterior à que se quer remover.
Suponha que p é o endereço de uma célula da lista e que se deseja remover a
célula apontada por p->proximo. Note que a função de remoção não precisa saber
onde a lista começa. Esta função recebe o endereço p de uma célula de uma lista
encadeada. A função remove da lista a célula p->proximo. A função supõe que p !=
NULL e p->proximo != NULL.

void remove(struct IntNode *p){

struct IntNode *morta;

morta = p->prox;
p->prox = morta->prox;

free(morta);
}

Esvaziando a lista

Remover toda a lista utilizando-se uma única chamada de função.

void esvazia_lista(){

IntNode *tmp;

while (raiz != NULL){


tmp = raiz;
raiz = raiz->proximo;
free(tmp);
}
}

Busca e remoção

Suponha-se que ini é o endereço de uma lista encadeada. O problema: Dado


um inteiro y, remover da lista a primeira célula que contém y, se tal célula não existir,
não faça nada.

void buscaEremove (int y, struct IntNode *ini){


struct IntNode *p, *q;

q = ini;

while (q != NULL && q->dado != y){


p = q;
q = q->proximo;
}
if (q != NULL){
p->proximo = q->proximo;

124
free(q);
}
}

No início de cada iteração imediatamente antes da comparação de q com NULL,


tem-se q == p->proximo, ou seja, q está sempre um passo à frente de p.

Busca e inserção

Mais uma vez, suponha que se tenha uma lista encadeada ini. O problema:
Inserir na lista uma nova célula com conteúdo x imediatamente antes da primeira célula
que tiver conteúdo y, se tal célula não existir inserir x no fim da lista.

void buscaEinsere(int x, int y, IntNode *ini){

struct IntNode *p, *q, *pnode;

pnode = (struct IntNode *)malloc (sizeof(struct IntNode));


pnode->dado = x;
q = ini;

while (q != NULL && q->dado != y){


p = q;
q = q->proximo;
}

pnode->proximo = q;
p->proximo = pnode;
}

As funções, apresentadas anteriormente, são a base para criação de novas


funções para manipulação de listas de armazenamento de dados.

7.3.3 Lista ordenada

Os nós de uma lista podem aparecer em qualquer seqüência na lista. Porém em


vários casos é preferível que estes estejam. Um polinômio, expresso em uma lista
encadeada, é um exemplo de uma lista que deve estar ordenada para o perfeito
entendimento do polinômio. Vejam a seguir quais as funções serão necessárias para a
criação, a inserção, a apresentação e, como não poderia ficar de fora, a resolução de
polinômios.

Representação de Polinômios em uma lista

Uma fila ordenada apresenta uma maneira adequada para armazenar e


posteriormente permitir o acesso a cada elemento do polinômio na forma:

P(X) = Cn Xn + Cn-1 Xn-1 + ... + C3 X3 + C2 X2 + C1 X1 + C0

125
É possível representar a estrutura anterior por um conjunto de pares associados a
X, que correspondem ao coeficiente e a potência. Por exemplo, P(X) = 5X6 - 2X4
+ 7X3 - 3X2 - 8 seria representado pelos pares {(5,6), (-2,4), (7,3),
(-3,2), (-8,0)}. Para cada par o primeiro elemento representa o coeficiente de X,
e o segundo elemento representa o valor da potência de X.
A potência de X poderá ser utilizada como chave de ordenação de uma lista
ordenada que representará o polinômio. Ao usar a potência de X para ordenar a lista, o
exemplo anterior ficaria assim: {(-8,0), (-3,2), (7,3), (-2,4), (5,6)}

Definição de nós do polinômio

Sobre as considerações anteriores, pode-se definir a seguinte representação para


cada um dos nós da lista para armazenamento de polinômios.

struct polin{
float coeficiente;
int expoente;
struct polin *prox;
};
typedef struct polin NODO;

Criando um polinômio nulo

A criação de um elemento 0X0 na lista facilitará a inserção, posteriormente, de


novos nós na lista.

void criaLista (NODO **plista){

NODO *p;
p = getnodo();
p->coeficiente = 0;
p->expoente = 0;
p->prox = NULL;
*plista = p;
}

Observa-se uma nova função getnodo()sendo utilizada. Definição a seguir.

NODO *getnodo(void){

NODO *p;
p = (NODO *)malloc (sizeof(struct polin));
return(p);
}

Inserindo um termo

Uma função chamada insTermo (NODO *p, float c, int e) será


responsável pela inserção de novos termos do polinômio na lista ordenada. Observa-se
que durante a inclusão de um novo termo é realizada uma pesquisa para identificar se o
termo já está presente no polinômio. Caso afirmativo, o coeficiente será acrescido ao já

126
existente, evitando que seja gerada uma nova posição na lista com a repetição do
expoente. Caso o expoente não esteja presente, um novo nó será criado na lista
mantendo a ordenação da mesma.

void insTermo (NODO *p, float c, int e){

NODO *n;

if (e == p->expoente)
p->coeficiente = p->coeficiente + c;
else{
while ((p->prox!=NULL)&&(e>p->prox->expoente))
{
p = p->prox;
}
if ((p->prox!=NULL)&&(e==p->prox->expoente))
p->prox->coeficiente = p->prox->coeficiente+c;
else{
n = getnodo();
n->coeficiente = c;
n->expoente = e;
n->prox = p->prox;
p->prox = n;
}
}
}

Simular a criação do polinômio P(X) = 7X2 - 3X - 8 usando as funções


descritas anteriormente, como segue:

...
NODO *p;
p = NULL;

criaLista(&p);

insTermo(p, 7, 2);
insTermo(p, -3, 1);
insTermo(p, -8, 0);
insTermo(p, -3, 2);

mostra(p, ‘P’);
printf("\nP ( 2 ) = %.2f ", calPolin(p,2));

printf("\nDigite uma tecla para finalizar! ");


getch();
...
A simulação prevê ainda a inclusão de -3X2, o que resulta em P(X) = 4X2 -
3X - 8.

Exibindo o conteúdo do polinômio

127
Criar uma função de exibição torna-se uma tarefa simples uma vez que a lista
apresenta-se ordenada. Considera-se, x^y com sendo xy.

void mostra (NODO *p, char ch){

float coef;

printf("%c ( X ) = ", ch);


while (p != NULL){
if (p->coeficiente != 0){
if (p->coeficiente < 0)
printf(" - ");
else
printf(" + ");
coef = fabs(p->coeficiente);
printf("%.2f", coef);
if (p->coeficiente < 0)
printf("*X^%i", p->expoente);
}
p = p->prox;
}
}

Resolvendo o polinômio

A função calPolin() calcula o valor do polinômio armazenado na lista


ordenada. Veja a seguir:

float calPolin (NODO *p, float x){

float s = 0.0;

while (p != NULL){
s = s + p->coeficiente * pow(x, p->expoente);
p = p->prox;
}
return(s);
}

7.3.4 Exercícios propostos

Exercício 7.15 - Elabore um programa que implemente uma FILA com uma estrutura
de lista ligada simples, não ordenada, com uma função guardar(),
para armazenar elementos à cabeça da lista e retirar(), para retirar
elementos na cauda da lista. Os elementos da fila/lista são definidos do
seguinte modo:

typedef struct elem{


char nome[30];
char telef[10];
struct elem *seg; /* ponteiro para o
seguinte */

128
} ELEM;

ELEM *fila; /* ponteiro para inicio da fila */

Exercício 7.16 - Elabore um programa que implemente uma FILA CIRCULAR


suportada por uma lista ligada simples, com um número finito de
elementos, definidos pelo valor de uma constante, NL. A estrutura dos
elementos da Fila/Lista. Deverá ser definida do seguinte modo:

typedef struct elem{


char matric[30]; /* campo de dados */
struct elem *seg; /* ponteiro para o
seguinte */
} ELEM;

O programa deverá incluir funções para guardar() e retirar() matrículas


de viaturas na Fila, de acordo com o princípio F.I.F.O. e ainda funções para listar as
matrículas armazenadas e procurar uma matrícula qualquer na fila. Permiti-se o uso de
três variáveis globais do tipo ponteiro para ELEM.

ELEM *fila; /* ponteiro para inicio da fila */


ELEM *sai, *entra; /* ponteiro para as posições de
saída e de entrada na FILA
CIRCULAR */

Exercício 7.17 - Altere o exercício anterior (ex.: 7.16), criando uma FILA CIRCULAR
controlada pelo array de ponteiros de dimensão fixo: char
*ptr[DIM]. Onde DIM é uma constante que define a dimensão do
array.

Exercício 7.18 - Escreva uma função que converta um vetor de caracteres (char) numa
lista ligada simples cujos elementos contém os caracteres do vetor
antes referido. A função deve ter como argumento de entrada o
ponteiro para a string e devolver um ponteiro para lista recém criada.

Exercício 7.19 - Escreva uma função idêntica à anterior considerando que o vetor é do
tipo:

typedef struct aluno{


unsigned long numero;
char nome[50];
char morada[100];
char telefone[9];
char cod_postal[7];
} ALUNO;

Exercício 7.20 - Formule uma estrutura que permita guardar informação relativa a
DVD, nomeadamente: titulo, atores (3), tipo de vídeo (ação, romance,
drama, etc.), duração (minutos).

129
a) Escreva uma função que tendo como argumentos de entrada: o ponteiro para
uma lista simplesmente ligada e a informação relativa a um vídeo, efetue a
inserção de um nó com a referida informação.

b) Escreva uma função que efetue a listagem, no vídeo, dos vídeos existentes na
lista.

c) Escreva uma função que verifique a existência de um vídeo na lista e devolva o


ponteiro para o nó onde se encontra o vídeo (passe a informação do número do
vídeo em questão por argumentos de referência).

d) Escreva uma função que aceitando como argumento a lista resultante em a), a
inverta.

Exercício 7.21 - O restaurante Mac Donald’s em Porto Alegre possui um sistema de


atendimento que funciona mais ou menos assim: existem três filas
consecutivas pelas quais o cliente passa.

Nome do cliente: João a) inserir na fila de pedido


b) remover da fila de pedido
c) remover da fila de pagamento
d) remover da fila de encomenda
digite sua opção (a, b, c, d): a

Fila Pedidos Fila Pagamento Fila Encomenda

1. Pedro 1. Maria 1. Maria Cliente


2. Lucas 2. Abraão 2. Rui Atendido:
3. João 3. Jonas 3. Ana
Salomão

A primeira é a fila na qual o cliente faz seu pedido; saindo desta fila ele entra em
uma segunda fila, na qual faz o pagamento de seu pedido; saindo desta segunda fila ele
entra em uma terceira fila, na qual lhe é entregue a sua encomenda. Construa um
programa em linguagem C que simule o funcionamento deste sistema. As seguintes
funcionalidades são esperadas: inserção de cliente na fila de pedido; remoção de cliente
da fila de pedido; remoção de cliente da fila de pagamento; remoção de cliente da fila de
pegar encomenda. Para facilitar a compreensão do funcionamento deste programa,
sugere-se a interface anterior para o mesmo.

Exercício 7.22 - Critique a função abaixo. Ao receber uma lista encadeada e um inteiro
x, ela promete devolver o endereço de uma célula com conteúdo x. Se
tal célula não existe, promete devolver NULL.

130
celula *busca (int x, celula *ini){

int achou;
celula *p;
achou = 0;
p = ini;

while (p != NULL && !achou){


if (p->conteudo == x) achou = 1;
p = p->prox;
}
if (achou)
return(p);
else
return(NULL);
}

Exercício 7.23 - Escreva uma função que receba uma lista encadeada e devolve o
endereço de um nó que esteja o mais próximo possível do meio da
lista. Após determinar o provável nó central, exiba o conteúdo da lista
e mostre uma mensagem ao chegar no nó central encontrado
anteriormente.

Exercício 7.24 - Critique a seguinte versão da função remove:

void remove(celula *p, celula *ini){

celula *morta;
morta = p->prox;
if (morta->prox == NULL)
p->prox = NULL;
else
p->prox = morta->prox;

free(morta);
}

Exercício 7.25 - Escreva uma função que copie um vetor para uma lista encadeada.

Exercício 7.26 - Escreva uma função que copie uma lista encadeada para um vetor.

Exercício 7.27 - Escreva uma função que faça uma cópia de uma lista dada.

Exercício 7.28 - Escreva uma função que concatena duas listas encadeadas (isto é,
"amarra" a segunda no fim da primeira).

Exercício 7.29 - Escreva uma função que conta o número de células de uma lista
encadeada.

Exercício 7.30 - Escreva uma função que insere na lista uma nova célula com conteúdo
x entre a k-ésima e a k+1-ésima células.

131
Exercício 7.31 - Escreva uma função que verifica se duas listas dadas são iguais, ou
melhor, se tem o mesmo conteúdo.

Exercício 7.32 - Escreva uma função que libera() (função free()) todos os nós de
uma lista encadeada. Considere que cada nó da lista foi originalmente
alocado por malloc().

Exercício 7.33 - Escreva uma função que inverte a ordem das células de uma lista
encadeada (o primeiro passa a ser o último, o segundo passa a ser o
penúltimo etc.). Faça isso sem usar espaço auxiliar: somente altere os
ponteiros.

Exercício 7.34 - A partir de agora, tudo é festa: você pode inventar uma grande
variedade de tipos de listas encadeadas. Por exemplo, você pode fazer
uma lista encadeada circular: a última célula aponta para a primeira e
não para NULL. Para especificar uma lista circular basta fornecer um
endereço (por exemplo, o endereço da última célula).

Exercício 7.35 - Outro tipo útil é a lista duplamente encadeada: cada célula contém o
endereço da célula anterior e o da célula seguinte. Pense como seria
isto?

Exercício 7.36 - Pense nas seguintes questões, apropriadas para qualquer tipo de lista
encadeada. Em que condições a lista está vazia? Como remover a
célula apontada por p? Idem para a célula seguinte à apontada por p?
Idem para a célula anterior à apontada por p? Como inserir uma nova
célula entre o elemento apontado por p e o seu antecessor? Idem entre
p e seu sucessor?

Exercício 7.37 - Descreva, em linguagem C, a estrutura de uma das células de uma lista
duplamente encadeada.

Exercício 7.38 - Escreva uma função que remove de uma lista duplamente encadeada a
célula apontada por p. (Que dados sua função recebe? Que coisa
devolve?)

Exercício 7.39 - Escreva uma função que insira em uma lista duplamente encadeada,
logo após a célula apontada por p, uma nova célula com conteúdo y.
(Que dados sua função recebe? Que coisa devolve?)

Exercício 7.40 - Escreva uma função para remover de uma lista encadeada todos os
elementos que contêm y.

132
Bibliografia

HOLZNER, Steven, GROUP, The Peter Norton Computing . Programando em


C++: um guia prático para programação profissional. Tradução de Sylvio
Carlos Montenegro Branco. Rio de Janeiro: Campus, 1993.

HUBBARD, John R. Teoria e problemas de programação em C++ / John R.


Hubbard; Trad. Edson Furmankigwicz. – 2. ed. – Porto Alegre : Bookman, 2003.

JAMSA, Kris. Programando em C++ - A bíblia / Kris Jamsa e Lars Klander.


Tradução: e revisão Técnica: Jeremias René D. Pereira dos Santos – São Paulo :
Makron Books, 1999.

KERNIGHAN, Brian W., RITCHIE, Dennis M. Ritchie. C a linguagem de


programação: Padrão ANSI. Tradução Daniel Viera. Rio de Janeiro: Campus,
1989.

LOPES, Anita, GARCIA, Guto. Introdução à programação. Rio de Janeiro:


Campus, 2002.

MANZANO, José Augusto N. G., OLIVEIRA, Jayr Figueiredo de. Estudo


Dirigido de Algoritmos. São Paulo: Érica, 1997.

PEREIRA, Silvio do Lago. Estruturas de dados fundamentais: conceitos e


aplicações / Silvio do Lago Pereira. – São Paulo : Érica, 1996.

SCHILDT, Herbert. C, completo e total. 3 ed; Tradução e revisão técnica:


Roberto Carlos Mayer. São Paulo: Pearson Makron Books, 1997.

SENNE, Edson Luiz França. Primeiro curso de programação em C. 2 ed;


Florianópolis: Visual Books, 2006.

133
Apêndice A - Palavras reservadas em C e C++

asm auto break case


catch char class const
continue default delete do
double else enum extern
float for friend goto
if inline int long
new operator private protected
public register return short
signed sizeof static struct
switch template this throw
try typedef union unsigned
virtual void volatile while

134
Anexo A – Tabela de caracteres ASCII

DEC HEX ASC DEC HEX ASC DEC HEX ASC DEC HEX ASC
0 0 NUL 64 40 @ 128 80 Ç 192 C0 +
1 1 SOH 65 41 A 129 81 ü 193 C1 -
2 2 STX 66 42 B 130 82 é 194 C2 -
3 3 ETX 67 43 C 131 83 â 195 C3 +
4 4 EOT 68 44 D 132 84 ä 196 C4 -
5 5 ENQ 69 45 E 133 85 à 197 C5 +
6 6 ACK 70 46 F 134 86 å 198 C6 ã
7 7 BEL 71 47 G 135 87 ç 199 C7 Ã
8 8 BS 72 48 H 136 88 ê 200 C8 +
9 9 HT 73 49 I 137 89 ë 201 C9 +
10 A LF 74 4A J 138 8A è 202 CA -
11 B VT 75 4B K 139 8B ï 203 CB -
12 C FF 76 4C L 140 8C î 204 CC ¦
13 D CR 77 4D M 141 8D ì 205 CD -
14 E SO 78 4E N 142 8E Ä 206 CE +
15 F SI 79 4F O 143 8F Å 207 CF ¤
16 10 DLE 80 50 P 144 90 É 208 D0 ð
17 11 DC1 81 51 Q 145 91 æ 209 D1 Ð
18 12 DC2 82 52 R 146 92 Æ 210 D2 Ê
19 13 DC3 83 53 S 147 93 ô 211 D3 Ë
20 14 DC4 84 54 T 148 94 ö 212 D4 È
21 15 NAK 85 65 U 149 95 ò 213 D5 i
22 16 SYN 86 56 V 150 96 û 214 D6 Í
23 17 ETB 87 57 W 151 97 ù 215 D7 Î
24 18 CAN 88 58 X 152 98 ÿ 216 D8 Ï
25 19 EM 89 59 Y 153 99 Ö 217 D9 +
26 1A SUB 90 5A Z 154 9A Ü 218 DA +
27 1B ESC 91 5B [ 155 9B ø 219 DB _
28 1C FS 92 5C \ 156 9C £ 220 DC _
29 1D GS 93 5D ] 157 9D Ø 221 DD ¦
30 1E RS 94 5E ^ 158 9E × 222 DE Ì
31 1F US 95 5F _ 159 9F ƒ 223 DF _
32 20 SP 96 60 ` 160 A0 á 224 E0 Ó
33 21 ! 97 61 a 161 A1 í 225 E1 ß
34 22 " 98 62 b 162 A2 ó 226 E2 Ô
35 23 # 99 63 c 163 A3 ú 227 E3 Ò
36 24 $ 100 64 d 164 A4 ñ 228 E4 õ
37 25 % 101 65 e 165 A5 Ñ 229 E5 Õ
38 26 & 102 66 f 166 A6 ª 230 E6 µ
39 27 ' 103 67 g 167 A7 º 231 E7 þ
40 28 ( 104 68 h 168 A8 ¿ 232 E8 Þ
41 28 ) 105 69 i 169 A9 ® 233 E9 Ú

135
42 2A * 106 6A j 170 AA ¬ 234 EA Û
43 2B + 107 6B k 171 AB ½ 235 EB Ù
44 2C , 108 6C l 172 AC ¼ 236 EC ý
45 2D - 109 6D m 173 AD ¡ 237 ED Ý
46 2E . 110 6E n 174 AE « 238 EE ¯
47 2F / 111 6F o 175 AF » 239 EF ´
48 30 0 112 70 p 176 B0 _ 240 F0
48 31 1 113 71 q 177 B1 _ 241 F1 ±
50 32 2 114 72 r 178 B2 _ 242 F2 _
51 33 3 115 73 s 179 B3 ¦ 243 F3 ¾
52 34 4 116 74 t 180 B4 ¦ 244 F4 ¶
53 35 5 117 75 u 181 B5 Á 245 F5 §
54 36 6 118 76 v 182 B6 Â 246 F6 ÷
55 37 7 119 77 w 183 B7 À 247 F7 ¸
56 38 8 120 78 x 184 B8 © 248 F8 °
57 39 9 121 79 y 185 B9 ¦ 249 F9 ¨
58 3A : 122 7A z 186 BA ¦ 250 FA ·
59 3B ; 123 7B { 187 BB + 251 FB ¹
60 3C < 124 7C | 188 BC + 252 FC ³
61 3D = 125 7D } 189 BD ¢ 253 FD ²
62 3E > 126 7E ~ 190 BE ¥ 254 FE _
63 3F ? 127 7F DEL 191 BF + 255 FF DEL

136

Você também pode gostar