Você está na página 1de 103

Programao em C

Um guia para programao em linguagem C,


que pretende servir tambm como uma intro-
duo Computao. (Baseado, em parte,
no curso de Introduo Computao [MAC-
110/115] do IME-USP.)

Este guia um trabalho em progresso, em cons-


tante mutao! Algumas partes ainda esto in-
completas ou faltando.

Uma cpia deste guia, em sua verso mais re-


cente, pode ser encontrada na pgina
http://fig.if.usp.br/~esdobay/

E S. D
edudobay@gmail.com
Instituto de Fsica
Universidade de So Paulo

Verso de 1 de agosto de 2012


Sumrio

Prefcio 1

Introduo 3

1 Princpios bsicos 7
1.1 O ncleo de um programa . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Variveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Entrada e sada de dados . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4 Matemtica bsica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Boas maneiras em C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2 Controle de uxo 19
2.1 Desvios condicionais: if . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2 Repetindo operaes: laos (loops) . . . . . . . . . . . . . . . . . . . . . 22
2.3 Contadores e laos for . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4 Condies compostas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.5 Repeties encaixadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.6 Variveis booleanas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

3 Funes 37
3.1 Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.2 Os parmetros e o valor de sada . . . . . . . . . . . . . . . . . . . . . . 39
3.3 Usando funes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.4 Trabalhando com vrias funes . . . . . . . . . . . . . . . . . . . . . . . 42
3.5 Escopo de variveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

4 Mais sobre nmeros 45


4.1 Bases de numerao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.2 O armazenamento dos dados . . . . . . . . . . . . . . . . . . . . . . . . 47
4.3 Nmeros reais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.4 Funes matemticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.5 O tipo char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

i
ii Sumrio

5 Ponteiros e vetores 63
5.1 Prolegmenos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.2 Ponteiros como parmetros de funes . . . . . . . . . . . . . . . . . . . 66
5.3 Cuidado com os ponteiros! . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.4 Vetores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.5 Vetores como argumentos de funes . . . . . . . . . . . . . . . . . . . . 72
5.6 Ponteiros e vetores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.7 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.8 Mais sobre entrada e sada . . . . . . . . . . . . . . . . . . . . . . . . . . 79

6 Algoritmos 81

7 Mais ponteiros 83
7.1 Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
7.2 Alocao dinmica de memria . . . . . . . . . . . . . . . . . . . . . . . 84
7.3 Ponteiros duplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.4 Ponteiros para funes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
7.5 Escopo de variveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
7.6 Funes recursivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

8 Estruturas 91
8.1 Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
8.2 Listas ligadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93

Apndices

A Compilao 95

B Tabelas de referncia 97

Referncias Bibliogrcas 99
Prefcio

1
Introduo

Este captulo introduz alguns conceitos bsicos sobre computao, mas sem entrar na lin-
guagem C.

O que um computador?
A denio mais ampla de computador a de uma mquina que realiza uma srie de ope-
raes lgicas ou matemticas sobre um conjunto de dados e devolve o resultado para o
usurio. O computador que conhecemos no o nico exemplo disso; podemos citar diver-
sos aparelhos eletrnicos com essas caractersticas, de calculadoras a telefones celulares.
Um computador tem um conjunto de instrues que correspondem s operaes que po-
dem ser feitas com ele. Uma seqncia lgica dessas instrues tal qual uma receita de
bolo o que conhecemos como programa.
Nos primeiros computadores, toda a programao era feita diretamente no circuito do
aparelho: no se podia modicar o funcionamento do computador sem uma reconstruo
fsica dele ou de alguma de suas partes. Ainda existem computadores desse tipo o caso
de uma calculadora simples, por exemplo: voc no pode criar programas e introduzir nela;
necessrio mexer diretamente nas placas de circuito dela.
O computador que conhecemos faz parte de um conjunto especial dentro da nossa de-
nio de computador: nele, os programas so armazenados na memria (e no inseridos
diretamente atravs dos circuitos), da mesma maneira que os dados, e podem ser criados
e modicados utilizando o prprio Computador. A encarnao mais comum desse tipo de
funcionamento um modelo conhecido como arquitetura de von Neumann. (A palavra ar-
quitetura usada em geral para descrever o modelo de funcionamento interno de um com-
putador.)
Esse modelo descreve o funcionamento geral do computador em termos da interao
entre trs componentes:

uma unidade de processamento conhecida como CPU (unidade central de processa-


mento, na sigla em ingls) ou processador que responsvel por todas as operaes
matemticas e lgicas. basicamente o crebro do computador;

um espao para armazenamento (temporrio) de dados, que a memria RAM (sigla


em ingls para memria de acesso randmico);

3
4 Introduo

quaisquer dispositivos de entrada e sada, que recebem e enviam informaes de/para


o usurio. Os exemplos mais imediatos so o monitor (dispositivo de sada), o teclado
e o mouse (dispositivos de entrada). Tambm se encaixam aqui os dispositivos de
armazenamento de dados, como o disco rgido, que tm papel tanto de entrada quanto
de sada.

Memria

Monitor
Sada
Unidade de Mdias externas

processamento
(CPU) Teclado
Entrada Mouse
Mdias externas

Figura 0.1: Esquema da arquitetura de von Neumann.

A arquitetura de von Neumann um modelo, de certa forma, abstrato; na prtica, para


construir um computador, h vrios outros detalhes a se estabelecer. Diferentes escolhas des-
ses detalhes, devidas a interesses e propsitos diferentes, levaram a diferentes arquiteturas
de processadores. Por exemplo, o funcionamento do processador que existe num GameBoy
bem diferente do funcionamento do processador de um computador pessoal. Os computa-
dores pessoais de hoje em dia usam, em sua maioria, processadores baseados numa mesma
arquitetura, conhecida como arquitetura Intel, e que foi introduzida pela primeira vez em
processadores da Intel.

Linguagens de programao
J vimos que um programa simplesmente um conjunto de instrues ou comandos que
dizem ao computador como realizar as operaes que ele deve fazer. Mas como vm a ser
essas instrues? O processador do computador s consegue compreender uma linguagem
que chamamos de linguagem de mquina; essa linguagem, inclusive, diferente para cada
arquitetura de processadores. Um programa escrito em linguagem de mquina consiste de
uma srie de bits (os famosos zeros e uns), que, para ns, parecem no ter nenhum signicado.
Uma instruo razoavelmente simples seria escrita assim:

10110000 00101010

No seria nada prtico usar essa linguagem para fazer programas com alto nvel de comple-
xidade; por isso, foram desenvolvidas diversas linguagens de programao, que servem
como intermedirio entre a linguagem humana e a linguagem de mquina.
Uma linguagem de programao deve ser, por um lado, legvel e compreensvel para
um humano, e, por outro lado, sucientemente simples para que possa ser compreendida

Para ser mais preciso, a arquitetura Intel de 32 bits, ou, abreviadamente, IA-32. Mais tarde veremos o que
isso signica.
5

completamente e sem ambigidades por um computador. Geralmente, uma linguagem de


programao composta de:

um conjunto de palavras-chave (geralmente em ingls, como if, while, function),


associadas a certos comandos e recursos da linguagem;

um conjunto de smbolos (como #, ", {, etc.); e


um conjunto de regras que ditam o signicado de todas essas coisas e como elas podem
ser combinadas e utilizadas.

No entanto, as linguagens de programao no so entendidas diretamente pelo compu-


tador (lembre-se, ele s entende a linguagem de mquina). Para que o computador possa
executar cdigos escritos em linguagens de programao, existem programas que so feitos
para entender essas linguagens e traduzir os cdigos para a linguagem de mquina. H dois
tipos desses programas: os compiladores e os interpretadores. O processo de traduo do
cdigo para a linguagem de mquina chama-se compilao.
Um interpretador l um programa escrito numa certa linguagem de programao e ime-
diatamente executa as instrues nele contidas a traduo para a linguagem de mquina
feita na hora da execuo. J um compilador apenas traduz o programa para a linguagem de
mquina (compila o programa), deixando-o pronto para ser executado posteriormente sem
depender de outro programa; isso tem a vantagem de tornar a execuo do programa mais
rpida.
Os programas em linguagem C so sempre compilados. Para aprender a usar um compi-
lador (o que voc certamente deve aprender, j que precisar disso para poder executar seus
programas), dirija-se ao o Apndice ??.
Princpios bsicos
1
1.1 O ncleo de um programa
Um programa em C estruturado em funes, que so, basicamente, trechos de cdigo que
podem ser chamados vrias vezes para realizar uma certa tarefa. Todos os comandos que o
seu programa executar estaro dentro de alguma funo.
As funes, em geral, podem devolver valores o resultado de algum clculo ou con-
sulta, por exemplo e tambm podem receber parmetros por exemplo, os dados de
entrada para um clculo, ou os critrios a serem usados na busca. Nesse sentido, funes em
C so semelhantes s funes s quais nos referimos em matemtica.
Como acabamos de dizer, todos os comandos dum programa devem estar dentro de uma
funo. Em C, no entanto, existe uma funo privilegiada a funo main (principal).
Ela como se fosse o roteiro principal do programa: a execuo do programa sempre comea
por ela. Todas as outras funes (quando houver) so chamadas, direta ou indiretamente, a
partir da main.
Todo programa deve ter uma funo main. Outras funes podem ser criadas (veremos
como um pouco mais adiante), e tambm podemos usar funes prontas h uma srie de
funes que vem disponvel em qualquer compilador C, chamada de biblioteca padro do C.
Vamos iniciar nosso estudo com um programa extremamente simples, que apenas im-
prime uma mensagem na tela:

#include <stdio.h>

int main()
{
printf("Ol, maravilhoso mundo da programao!\n");
return 0;
}

Vamos ver o que acontece em cada linha:

Veja que, aqui, imprimir no tem nada a ver com a sua impressora que joga tinta no papel. Nesse livro,
sempre que usar essa palavra, entenda como uma referncia ao ato de mostrar alguma mensagem na tela.

7
8 Captulo 1. Princpios bsicos

#include <stdio.h>

Esta linha pede ao compilador que disponibilize para ns algumas funes de entrada
e sada de dados, que permitem que voc exiba mensagens na tela e leia dados que o
usurio digitar no teclado. Mais adiante veremos como que essa instruo funciona.

int main()

aqui que denimos nossa funo main. As chaves { } servem para delimitar o seu
contedo.
Se voc est curioso, esse int signica que o valor que a funo devolve um nmero
inteiro, e os parnteses vazios indicam que a funo no recebe nenhum parmetro. No
se preocupe se isso no zer muito sentido; um pouco mais adiante estudaremos funes
com detalhe, e voc poder entender melhor o que tudo isso signica. Aceitar isso como
uma frmula pronta por enquanto no lhe far nenhum mal.

printf("Ol, maravilhoso mundo da programao!\n");

Aqui printf o nome uma funo da biblioteca padro ( possvel us-la graas ao #in-
clude que colocamos l em cima!). Essa funo simplesmente toma a mensagem (uma
sequncia de caracteres) que lhe foi passada e mostra-a na tela.
Nessa linha, passamos como parmetro a essa funo a mensagem que queremos impri-
mir (usando aspas duplas, veja bem). E o que aquele \n intruso no nal? Ele um
cdigo especial que representa uma quebra de linha indicando que qualquer coisa que
for impressa depois da nossa mensagem deve ir para a linha seguinte. Se omitssemos
o \n, a prxima mensagem que fosse eventualmente impressa sairia grudada nessa; isso
til em vrias situaes, mas no particularmente nessa; em geral melhor terminar a
sada de um programa com uma quebra de linha.
Essa coisa entre aspas chamada de sequncia de caracteres (adivinhe por qu!), e
tambm bastante conhecida pelo seu nome em ingls, string (literalmente, cadeia [de
caracteres]). Para falar a verdade, usarei principalmente a palavra string daqui em diante.
Note que usamos um ponto-e-vrgula aqui. Da mesma maneira que em portugus usa-
mos um ponto nal para encerrar uma frase, em C precisamos usar obrigatoriamente um
ponto-e-vrgula para encerrar um comando.

return 0;

Essa instruo encerra a execuo do programa (por isso, deve ser sempre a ltima da
funo main). Alm disso, o nmero zero serve para indicar ao sistema que o pro-
grama terminou com sucesso (nmeros diferentes de zero indicariam um erro); uma
conveno da linguagem. Voc entender melhor como isso funciona quando falarmos
detalhadamente de funes, no captulo 3.
Note novamente o ponto-e-vrgula.
Variveis 9

Um comentrio adicional sobre a obrigatoriedade do ponto-e-vrgula: veja que ele no


foi usado nem com o #include nem com a denio da funo main. Neste ltimo caso, no
se usa ponto-e-vrgula pois a denio da funo no propriamente um comando, mas um
bloco com vrios comandos.
J no primeiro caso, o ponto-e-vrgula no usado porque o #include um comando, de
certa maneira, especial. Por enquanto, apenas guarde isso: linhas comeadas com # no
levam ponto-e-vrgula no nal. Veremos mais adiante como funciona esse tipo de comando.
importante saber que a linguagem C diferencia maisculas e minsculas, ou seja, se
voc digitar PRINTF ou Printf, ou trocar return por RETURN, o programa no ir funcionar.

1.2 Variveis
Uma das necessidades mais comuns em programao a de guardar dados em algum lugar
da memria. Para isso, a maioria das linguagens de programao usa o conceito de vari-
veis, que so simplesmente pedaos da memria que servem para guardar um certo valor
(um nmero, por exemplo) e que tm um nome. Com isso, voc no precisa se preocupar
em saber em que lugar da memria voc guardar seus dados; todo o trabalho ca para o
compilador.
Em C, h trs tipos primitivos de dados: nmeros inteiros, nmeros de ponto utuante
(um nome pomposo para os nmeros fracionrios, que tem a ver com a maneira como os
computadores trabalham com eles) e caracteres (como a, #, Z, 5 o caractere que
representa o nmero 5, e no o nmero em si). A esses trs tipos de dados correspondem os
quatro tipos primitivos de variveis, listados na tabela 1.1.
Na verdade, eu poderia dizer que so apenas dois esses tipos: os nmeros inteiros e os
de ponto utuante. O que ocorre que os caracteres so representados na memria como se
fossem nmeros (inteiros); por exemplo, quando eu escrevo os caracteres abc em algum lugar
(em ltima instncia, isso ca em algum lugar da memria), o que o computador guarda na
memria so os trs nmeros 97, 98, 99. Isso est relacionado conhecida tabela ASCII,
que nada mais que uma conveno de quais caracteres correspondem a quais nmeros
(consulte o apndice para ver uma tabela ASCII completa). Por causa disso, o tipo de dados
usado para armazenar caracteres tambm podem ser usado para guardar nmeros, embora
isso no seja to comum, pois ele s permite guardar nmeros pequenos.
Esses dois ou trs tipos de dados traduzem-se em quatro tipos primitivos de variveis,
resumidos na tabela 1.1.

Tabela 1.1: Os quatro tipos primitivos de variveis na linguagem C

Tipo Utilidade
int Armazena um nmero inteiro
float Armazena um nmero de ponto utuante
double Como oat, mas fornece maior preciso
char Guarda um nico caractere. Com esse tipo tambm podemos guardar sequn-
cias de caracteres (strings), mas isso requer um outro recurso da linguagem
que s veremos mais tarde.

Esses tipos so chamados de primitivos porque eles podem ser modicados e combi-
nados de certas maneiras, de modo a criar estruturas mais complexas de dados (por exemplo,
10 Captulo 1. Princpios bsicos

as tais sequncias de caracteres), como veremos mais tarde. Por enquanto, usaremos apenas
os tipos inteiros; trabalharemos com nmeros de ponto utuante a partir do captulo ??.
Cada varivel que quisermos usar deve ser criada com antecedncia; para isso, pre-
cisamos dizer o nome e o tipo das variveis. Para isso, usamos um comando no seguinte
formato:
tipo_da_variavel nome_da_variavel;
Esse tipo de comando chama-se declarao de variveis, e faz com que o computador re-
serve na memria um espao suciente para guardar valores do tipo que voc pediu. Vejamos
alguns exemplos de declaraes:

int dia;
int mes;
int ano;

float preco_unitario;
float preco_total;

Se voc tem vrias variveis do mesmo tipo, voc pode declar-las todas de uma vez
s simplesmente coloque os vrios nomes separados por vrgulas. Poderamos, assim,
condensar o exemplo anterior com apenas duas linhas:

int dia, mes, ano;


float preco_unitario , preco_total;

Mas ateno: nos nomes de variveis voc no pode usar acentos nem nenhum outro
tipo de caractere especial; o C s aceita os caracteres alfabticos de A a Z (minsculos e
maisculos), dgitos de 0 a 9 e o trao inferior (_). H ainda a restrio de que o primeiro
caractere no pode ser um nmero. Alm disso, como a linguagem sensvel diferena
entre maisculas e minsculas, a varivel var diferente da varivel VAR (e portanto ambas
podem existir ao mesmo tempo).
Aps criar variveis, o prximo passo aprender a us-las. Uma das coisas mais simples
que podemos fazer guardar um valor numa varivel: usamos um sinal de igual, escrevendo
sua esquerda o nome da varivel, e direita o valor que queremos guardar:

dia = 22;
mes = 5;
ano = 2008;

Isso um comando de atribuio: ele joga o valor da direita na varivel da esquerda.


Veja que a posio importante nesse caso; no faria sentido escrever 22 = dia. Voc pode
pensar no operador = como uma echa que joga o valor da direita na varivel esquerda:
dia 22.
Os valores que voc joga nas variveis no precisam ser constantes; voc pode tambm
usar o contedo de uma outra varivel:

int a, b;
a = 5;
b = a;
Entrada e sada de dados 11

Na verdade, voc pode atribuir a uma varivel o valor de uma expresso qualquer; por exem-
plo, expresses aritmticas envolvendo variveis e constantes, como veremos a seguir.
A primeira atribuio de uma varivel costuma ser chamada de inicializao. A ini-
cializao de uma varivel muito importante pois, quando voc declara uma varivel, o
lugar da memria que ela ocupa apenas reservado para seu uso, sem atribuir nenhum valor
padro, como 0 ou 23. Ali h inicialmente um valor aleatrio, que foi deixado por algum
programa que j usou aquele lugar da memria. Por isso,

Voc no pode tentar acessar o valor de uma varivel antes de lhe atribuir um valor.

Muitos dos problemas esquisitos que podem ocorrer em programas esto relacionados a va-
riveis que no foram inicializadas.
Devemos ter em mente que uma varivel apenas um lugar para guardar dados, assim
como uma caixa ou uma gaveta, desprovido de qualquer tcnica de magia ou adivinhao. O
valor de uma varivel s muda quando voc o zer explicitamente. Por exemplo:

int a, b, c;
a = 5;
b = a;
c = 5*a;
a = 7;

Se eu escrever isso, ao nal desse trecho, o valor de a ser 7, mas o valor de b continuar
sendo 5 e o de c continuar sendo 25. As variveis so sempre independentes, no h como
criar vnculos do tipo c sempre vale 5a.
Tambm vale reforar que uma varivel um lugar, de certa maneira, temporrio. Quando
lhe atribumos um valor, qualquer valor que nela houvesse anteriormente completamente
esquecido.

1.3 Entrada e sada de dados


Um programa de computador praticamente intil se no tiver nenhum tipo de interao
com o usurio. Por exemplo, quando aprendermos a fazer clculos, precisaremos aprender
tambm alguma maneira de mostrar os resultados ao usurio para que isso tenha alguma
utilidade. A forma mais simples de fazer isso de mostr-los na tela isso chamado de
sada de dados.
J zemos um exemplo bem simples de sada de dados, que foi o programa inicial que
imprime uma string pr-xada; mas, em geral, um programa deve ser capaz de imprimir
valores que mudam conforme a execuo do programa e/ou que dependem, por exemplo,
dos dados que o usurio forneceu. Com o que vimos, voc talvez que tentado a escrever
isso para imprimir o valor de uma varivel inteira:

int num;
num = 17;
printf(num);

Infelizmente, isso est errado! No entanto, ainda possvel usar a funo printf para
esse propsito; s precisamos mudar um pouco a maneira de us-la. Observe o seguinte
exemplo:
12 Captulo 1. Princpios bsicos

#include <stdio.h>

int main()
{
int dia, mes, ano;

dia = 22;
mes = 7;
ano = 2008;

printf("Hoje dia %d/%d/%d.\n", dia, mes, ano);


return 0;
}

Este exemplo mostra como podemos imprimir uma string que contenha o valor de uma
varivel: colocamos o cdigo %d onde queremos que a varivel aparea; a varivel em si
especicada direita da string. Neste exemplo, j colocamos trs variveis de uma vez (com
o cdigo repetido em trs lugares diferentes), e o computador as substitui na ordem em que
elas aparecem aps a mensagem que voc escreveu. H uma ilustrao disso na gura 1.1.

Figura 1.1: Ilustrao do funcionamento dos guardadores de lugar da funo printf.

O cdigo %d um guardador de lugar para as variveis que se seguem. Mais especica-


mente, o computador l a string, trechinho por trechinho; quando encontra um cdigo desses,
ele pega a primeira varivel que encontrar depois da string; a cada novo cdigo encontrado,
ela pega a prxima varivel da lista, at que todas se esgotem.
Na verdade, o cdigo %d s serve se a varivel que voc quer imprimir um inteiro; se
for um nmero de ponto utuante, por exemplo, usamos outro cdigo. Na verdade, existem
diversos desses cdigos, que podem ser utilizados para fazer algumas alteraes no formato
da sada do programa.
bom acostumar-se com a nomenclatura: dentro dos parnteses que seguem o nome da
funo printf, cada um dos 4 itens separados por vrgulas um parmetro ou argumento.
Em outras palavras, quando imprimimos uma mensagem na tela com a funo printf, o es-
queleto da mensagem o primeiro parmetro, e os demais parmetros devem corresponder
s expresses que sero inseridas dentro do esqueleto (nos guardadores de lugar).

No menos importante que a sada a entrada de dados, ou seja, o ato de ler informaes
fornecidas pelo usurio. Novamente, uma das maneiras mais simples de fazer isso ler dados
que o usurio digita pelo teclado. Para isso, usamos uma funo parente da printf, a funo
scanf. O que ela faz passar o controle para o teclado, esperar que o usurio digite algo
Matemtica bsica 13

e termine com a tecla Enter, interpretar o que o usurio escreveu e salvar em uma ou mais
variveis.
Para cada dado que voc quiser ler, voc deve fornecer funo scanf duas informaes:
o tipo de dado (inteiro, nmero de ponto utuante, etc.) e o lugar onde o dado deve ser
armazenado (uma varivel). Um exemplo simples:

#include <stdio.h>

int main()
{
int idade;

printf("Qual a sua idade? ");


scanf("%d", &idade);
return 0;
}

Veja que aqui apareceu novamente o cdigo %d, que signica o mesmo que na funo
printf, mas funciona ao contrrio: ele l um inteiro do teclado e guarda seu valor na prxima
varivel da lista de parmetros. Voc tambm poderia ler vrios valores e guardar em vrias
variveis num comando s:

#include <stdio.h>

int main()
{
int dia, mes, ano;

printf("Qual a sua data de nascimento (no formato dd mm aaaa)? ")-


;
scanf("%d %d %d", &dia, &mes, &ano);
return 0;
}

Mas aqui ainda tem uma coisa esquisita: tem um E comercial (&) antes do nome de cada
varivel. Pra que isso serve? Ele uma maneira de dizer que estamos nos referindo ao
lugar em que est a varivel (um lugar na memria), e no ao seu valor. Isso necessrio
justamente para que a funo possa colocar o valor lido no lugar que queremos veja que
no estamos atribuindo explicitamente nenhum valor s nossas variveis. (Veremos como
isso funciona no Captulo ??, Ponteiros.) Voc no pode deixar esse E comercial de lado;
se voc o omitir, o valor lido no ir para a varivel que voc pediu, e seu programa poder
terminar com um erro do tipo Falha de segmentao.

1.4 Matemtica bsica


A palavra computador vem do latim computus, que signica clculo, conta. Era justa-
mente esse o objetivo do computador na sua primeira concepo: fazer contas. Ento, nada
mais razovel que dedicar um tempo para aprender a fazer contas em C.
14 Captulo 1. Princpios bsicos

As operaes matemticas bsicas no so nenhum mistrio em C. A linguagem pos-


sui os mesmos operadores matemticos com os quais estamos acostumados (com algumas
adaptaes que foram outrora necessrias), de acordo com a seguinte tabela:

Operador Signicado
+ adio
- subtrao
* multiplicao
/ diviso
% resto da diviso inteira

Os operadores em C funcionam da maneira usual para realizar uma operao com dois
nmeros, basta colocar o operador entre eles. Esses nmeros podem ser tanto constantes
numricas quanto variveis (ou qualquer coisa que em C for considerada uma expresso).
Formalmente, os nmeros ou as expresses s quais se aplica um operador so chamados de
operandos.
Podemos tambm montar expresses aritmticas mais complexas, e nelas as operaes
seguem tambm a ordem usual: multiplicaes e divises so realizadas antes de adies e
subtraes, a menos que voc use parnteses para alterar a ordem. Observe que para esse
m voc no pode usar os colchetes e chaves; em compensao, voc pode usar vrios nveis
de parnteses, como em 3 * (5 * (7 - 1)).
O operador - (o sinal de menos) tambm pode ser usado para obter o valor de uma
varivel com o sinal trocado. Ele tambm usado como na matemtica: -num corresponde
ao valor de num com o sinal trocado. Note que esse valor no muda o valor da varivel, apenas
obtm uma cpia dele com o sinal trocado. Para alterar o valor da varivel, necessrio
dizer explicitamente que voc quer guardar o valor com sinal trocado de volta na varivel.
Veja alguns exemplos de uso dos operadores aritmticos, com os resultados anotados ao
lado:
x = 14 / 7; /* 2 */
y = 12 % 7; /* 5 (resto da diviso) */
x = 12 + 5 * 7; /* 47 */
x = (12 + 5) * 7; /* 119 */
x = ((12 + 5) * 7 + 2) * 3; /* 363 */
fat = 1 * 2 * 3 * 4 * 5; /* 120 */

x = y + 2*z; /* usando os valores atualmente


guardados em outras variveis -
*/

x = -x; /* troca o sinal de x e


guarda de volta em x */

x = x + 1; /* aumenta em uma unidade o valor


de x, guardando de volta em x -
*/

Aqui est includo tambm o uso do operador %, j que o resto tambm resultado de uma operao de diviso.
Matemtica bsica 15

Note (pelos dois ltimos exemplos) que podemos usar a mesma varivel nos dois lados
da operao de atribuio. Isso no tem nenhuma ambigidade para o computador; ele sabe
reconhecer que, no lado direito, voc est fazendo algum clculo que envolve o valor da
varivel, e que o lado esquerdo representa o lugar onde voc quer guardar o resultado desse
clculo. O computador primeiro faz o clculo com o valor atual da varivel, e depois guarda
de volta na varivel.
Voc pode usar expresses aritmticas diretamente na funo printf: os parmetros no
precisam ser nomes de variveis. Por exemplo:

int x;
printf("Digite um nmero inteiro: ");
scanf("%d", &x);
printf("O dobro de %d %d.\n"
"O quadrado de %d %d.\n", x, 2*x, x, x*x);

Na funo scanf, lgico que o lugar de armazenamento tem de ser, obrigatoriamente,


uma varivel; no faz sentido nenhum escrever algo como &(2*x), pois a expresso 2*x no
representa um lugar bem denido onde um valor pode ser guardado.

1.4.1 Contas com inteiros


O exemplo a seguir converte uma temperatura (inteira) dada na escala Celsius para a escala
Fahrenheit. Para quem no se lembra, a relao entre dois valores TC (em Celsius) e TF (em
Fahrenheit) correspondentes mesma temperatura
9
TF 32 = TC .
5

#include <stdio.h>

int main()
{
int celsius , fahrenheit;

printf("Digite a temperatura de hoje: ");


scanf("%d", &celsius);
fahrenheit = 9 * celsius / 5 + 32;

printf("Hoje est fazendo %d graus Fahrenheit!\n", fahrenheit);

return 0;
}

Voc, que conhece a propriedade comutativa da multiplicao, deve imaginar que a conta
principal do programa poderia ser escrita de vrias maneiras:

fahrenheit = 9 * celsius / 5 + 32;


fahrenheit = celsius * 9 / 5 + 32;
fahrenheit = celsius / 5 * 9 + 32;
fahrenheit = 9 / 5 * celsius + 32;
16 Captulo 1. Princpios bsicos

Em teoria, isso estaria certo. Agora tente executar o programa com as quatro variaes,
testando algumas temperaturas diferentes. Voc deve reparar que as duas primeiras variaes
do os valores mais prximos do valor correto, enquanto que a terceira d um erro maior e a
quarta d um erro realmente grosseiro. Por que isso ocorre? Para o computador, faz sentido
que uma operao entre inteiros deve dar um resultado inteiro; por isso, quando voc divide
dois inteiros, voc obtm apenas a parte inteira do resultado. Dessa maneira, quando fazemos
no programa a diviso por 5, obtemos a verso arredondada do valor que esperaramos obter,
por exemplo, numa calculadora.
Os resultados foram diferentemente incorretos porque os arredondamentos so reali-
zados em diferentes etapas. Sabendo que o C calcula expresses matemticas sempre da
esquerda para a direita, podemos prever, por exemplo, que, na ltima variao da nossa
conta, o primeiro clculo realizado 9 / 5, que d 1 (com resto 4). Assim, a temperatura
em graus Celsius simplesmente somada com 32, o que d um erro muito grosseiro. Na
terceira conta, o problema na diviso da temperatura por 5 qualquer temperatura que
no seja mltipla de 5 sofrer um arredondamento para baixo, de modo que 29 C seriam
convertidos para 77 F, que na verdade correspondem a 25 C.
Por isso, neste caso, a melhor opo a usar a primeira ou a segunda, j que o arredon-
damento na diviso feito sobre um nmero maior e ocorre numa das ltimas etapas, de
modo que o erro no se propaga mais para outras contas ou melhor, ele s se propaga
para a adio, na qual no h perigo de haver outros erros, pois a adio de inteiros sempre
exata.
Mais adiante veremos como trabalhar com nmeros de ponto utuante, o que resolver
o problema das divises mas tambm traz alguns outros problemas, como tambm vamos
estudar.

1.5 Boas maneiras em C


1.5.1 Comentrios
Talvez voc tenha reparado que num certo ponto usei os smbolos /* e */ para anotar resulta-
dos de contas. Eles tambm so parte da linguagem; eles so usados para fazer comentrios
no meio do cdigo. Voc pode escrever o que quiser entre esses dois smbolos, pois tudo que
ali estiver ser ignorado pelo compilador. A nica coisa que voc no pode fazer colocar
um comentrio dentro do outro:

/* um comentrio /* isto um erro */ dentro do outro */

Existe tambm uma outra maneira de fazer comentrios, que foi copiada da linguagem
C++: usam-se duas barras //, mas o comentrio s vale at o nal da linha. Essa segunda
maneira s foi xada no padro mais recente da linguagem (C99); se seu compilador estiver
operando no modo ANSI, ele no gostar desse segundo tipo de comentrio.

/* Comentrio estilo C
* ===================
* Comea com barra e asterisco ,
* termina com asterisco e barra.
*/
Boas maneiras em C 17

// Comentrio estilo C++


// =====================
// Comea com duas barras,
// vale apenas at o final da linha.

Tenho algumas palavrinhas a proferir sobre o uso dos comentrios, mas postergarei esse
assunto para o prximo captulo, quando tivermos visto um pouco mais de teoria.

1.5.2 Estrutura e estilo


Em C, praticamente todos os espaos, tabulaes e quebras de linha supruos de agora
em diante, sempre que eu falar de espaos, entenda que tabulaes e quebras de linha tam-
bm esto includas so ignorados pelo compilador (exceto, claro, em uma string). Os
programas abaixo so, perante as regras da linguagem, absolutamente equivalentes:

#include <stdio.h>
int main(){printf("Ol, maravilhoso mundo da programao!\n");return -
0;}

#include <stdio.h>

int main()
{
printf("Ol, maravilhoso mundo da programao!\n");
return 0;
}

#include <stdio.h>

int main(

){
printf(

"Ol, maravilhoso mundo da programao!\n"

);
return 0;
}

Mas qual deles mais fcil de ler? O segundo, no ? Embora o estilo especco que
voc ir usar possa variar de acordo com sua preferncia, h alguns princpios gerais que
voc deve procurar seguir ao escrever um programa:

Em ingls, h a expresso whitespace, que indica qualquer sequncia de espaos, tabulaes e quebras de linha.
No conheo nenhuma expresso equivalente em portugus, ento teremos de conviver com essa ambiguidade.
18 Captulo 1. Princpios bsicos

Escreva uma instruo (um comando) por linha. Quando for necessrio, divida a ins-
truo em mais de uma linha para melhorar a legibilidade (voc pode quebrar a linha
em qualquer lugar onde poderia colocar um espao). Em algumas situaes tambm
pode ser aceitvel juntar duas instrues em uma linha s, mas no abuse disso.

Sempre que voc tiver um bloco de comandos (algo delimitado por chaves), o contedo
do bloco deve estar mais afastado da margem que o exterior do bloco. isso que
chamamos de indentao; ela torna muito mais fcil a visualizao da estrutura do
cdigo e de onde os blocos se encaixam. Voc tambm deve deixar com o mesmo
recuo as linhas que pertencem ao mesmo bloco, seno acaba aparecendo uma falsa
noo de hierarquia (ou uma verdadeira noo de baguna).
Controle de uxo
2
Com o pouco que aprendemos at agora, s possvel construir programas lineares, ou seja,
que s sabem fazer uma determinada sequncia de operaes, quaisquer que sejam os dados
fornecidos (ou outras condies). Na maioria dos programas, isso no suciente; pre-
ciso que o programa saiba fazer decises. A capacidade do computador de fazer decises
conhecida como controle de uxo.

2.1 Desvios condicionais: if


Uma das estruturas mais simples de controle de uxo a construo if (se), que verica se
uma certa condio satisfeita, e executa um conjunto de comandos em caso armativo. Em
C, isso se escreve assim:

if (condio)
{
/* comandos aqui */
}

O funcionamento dessa estrutura simples. Quando a execuo do programa atinge o if,


a condio avaliada, e h duas sadas possveis:
se a condio for satisfeita, o programa executa os comandos colocados dentro do par
de chaves, e depois continua a executar os comandos fora do par de chaves;
se a condio no for satisfeita, o programa simplesmente pula o que est entre chaves
e continua a executar os comandos aps as chaves.
Para que isso tenha alguma utilidade, precisamos aprender como so escritas as tais con-
dies. Dentre as condies mais simples que podemos escrever esto as comparaes entre
nmeros. Elas so escritas com os operadores a seguir, denominados operadores relacionais:

19
20 Captulo 2. Controle de uxo

Tabela 2.1: Operadores relacionais, para comparao entre nmeros.

Operador Signicado
< menor que
<= menor que ou igual a
> maior que
>= maior que ou igual a
== igual a (cuidado! so DOIS iguais!)
!= diferente de

Do mesmo jeito que ocorre para os operadores matemticos, no h nenhum grande


mistrio no uso dos operadores relacionais. Por exemplo, a condio a menor do que b
escrita como a < b; a igualdade entre a e b pode ser vericada pela expresso a == b, com
DOIS iguais. Porm, uma condio composta como x est entre a e b (matematicamente,
a < x < b) no pode ser escrita como a < x < b; a expresso at sintaticamente vlida,
mas seu resultado completamente diferente do que voc esperaria. O jeito certo de fazer
essa comparao ser esclarecido daqui a pouco.

if (a == b)
{
printf("Os nmeros so iguais!\n");
}

Existe uma abreviao muito til que pode ser usada com a estrutura if (e tambm pode
ser usada com outros tipos de estruturas que veremos a seguir). Quando houver apenas
um comando dentro do bloco delimitado pelas chaves, voc pode omitir as chaves. Assim,
poderamos escrever o trecho acima de forma um pouco mais compacta:

if (a == b)
printf("Os nmeros so iguais!\n");

Reforo que voc s pode fazer essa abreviao quando dentro do bloco houver apenas
um comando. Se voc zer isso quando houver mais de um comando, mesmo que estejam
todos na mesma linha ou com a mesma indentao, todos os comandos a partir do segundo
sero dados como externos ao bloco. Por exemplo, observe o cdigo a seguir:

if (a < 0)
printf("O valor de A no pode ser negativo!\n");
a = 0; /* indentao enganosa! */

A terceira linha foi indentada de modo que parea fazer parte do if; na verdade ela est
fora dele, e ser executada em qualquer caso, seja a negativo ou no. Esse cdigo equiva-
lente a este outro a seguir; este sim d a correta impresso da organizao do programa.

if (a < b) {
printf("A menor que B!\n");
}
a = b;
Desvios condicionais: if 21

Outro erro comum confundir o operador de comparao de igualdade, ==, com o ope-
rador de atribuio, =. O cdigo a seguir gramaticalmente correto, mas no faz o que ele
aparenta fazer:

if (a = b)
printf("A igual a B!\n");

O que est acontecendo aqui? Copiamos o valor de b para a varivel a; o comando de atri-
buio a = b por si mesmo uma expresso, cujo valor igual ao valor que foi atribudo.
Esse valor , ento, interpretado como se fosse o valor de uma condio cobriremos isso
mais adiante, na seo 2.6; mas veja que, independentemente de quem ele , esse valor con-
dicional no tem a ver com a relao entre a e b, pois ele s depende do valor da varivel b!
Portanto, a concluso de que a igual a b a partir desse if est errada.
Esse um dos vrios erros que podem no ser detectados pelo compilador, pois o c-
digo, ainda que semanticamente incorreto, sintaticamente vlido; por causa disso, voc
conseguir compilar e executar seu programa, mas alguma parte dele exibir algum compor-
tamento inesperado. Portanto, muito cuidado ao fazer comparaes de igualdade.

2.1.1 Condies alternativas: else


Suponha que voc quer ler um nmero do teclado e imprimir uma mensagem dizendo se o
nmero par ou mpar. Para vericar se par, podemos simplesmente ver se ele divsivel
por 2, ou seja, ver se o resto da diviso por 2 zero. Isso poderia ser feito assim:

#include <stdio.h>

int main()
{
int num;
printf("Digite um nmero: ");
scanf("%d", &num);

if (num % 2 == 0)
printf("O nmero par.\n");

if (num % 2 != 0)
printf("O nmero mpar.\n");

return 0;
}

Mas pense bem: sendo o nmero digitado inteiro, ou ele par ou mpar; ento, se j
vericamos que ele no par, no precisamos fazer outra conta para ver que ele mpar.
Esse tipo de padro associar um procedimento ao caso em que a condio satisfeita, e
outro procedimento ao caso em que ela falsa extremamente comum em programao.
Por isso, a cada condio voc pode associar tambm um outro bloco de comandos, a ser
executado caso ela seja falsa. Para isso, voc usa a palavra-chave else (que signica seno),
da seguinte maneira:

if (condio) {
22 Captulo 2. Controle de uxo

/* SE a condio for satisfeita , executa estes comandos */


}
else {
/* SENO, executa estes outros comandos */
}

(Continua existindo a abreviao do um-comando: se houver apenas um comando dentro do


else, voc pode tirar as chaves.)
Assim, nosso programa acima pode ser reescrito da seguinte maneira, que tambm deixa
bem mais claro o fato de que as duas situaes so opostas, complementares:

#include <stdio.h>

int main()
{
int num;
printf("Digite um nmero: ");
scanf("%d", &num);

if (num % 2 == 0)
printf("O nmero par.\n");
else
printf("O nmero mpar.\n");

return 0;
}

2.2 Repetindo operaes: laos (loops)


O computador til para automatizar tarefas; por exemplo, tarefas repetitivas que seriam
muito trabalhosas se realizadas de outra maneira. Por isso, interessante criar, numa lin-
guagem de programao, uma estrutura que permita a execuo repetida de uma tarefa ou
de vrias tarefas parecidas.
Em C, existem trs tipos de estruturas de repetio, mas o princpio de funcionamento de
todas o mesmo: repetir um certo conjunto de comandos at que uma certa condio de pa-
rada seja satisfeita. Esse tipo de estrutura costuma ser chamado de lao ou, equivalentemente
em ingls, loop.
O tipo de lao mais simples o while (enquanto). O funcionamento dele simples
uma condio avaliada; se ela for falsa, continua a execuo do resto do programa; se ela for
verdadeira, um bloco de comandos executado, e volta-se ao comeo do processo (avaliando
a condio novamente). Em outras palavras, o bloco de comandos executado enquanto a
condio for satisfeita. Isso representado no esquema da gura 2.1.
A sintaxe desse lao, bem parecida com a do if, a seguinte:

while (condio)
{
/* comandos aqui */
}
Repetindo operaes: laos (loops) 23

Cada execuo do bloco de comandos chamada de iterao no s para o while,


mas tambm para os outros laos que veremos posteriormente. Assim, uma observao
importante a fazer que, se a condio j for falsa quando o while for atingido, nenhuma
iterao ser executada o bloco de comandos ser ignorado.

Figura 2.1: Esquema de execuo da estrutura while.

Vamos criar um exemplo bem bsico: um programa que imprime na tela, em ordem
crescente, os nmeros inteiros de 1 a 10. Como fazer isso usando while?
Uma idia usar uma varivel que tem o valor inicial 1, e aumentar esse valor de 1 em
1 at que chegue a 10 (aqui entra o while!), imprimindo o valor atual antes de realizar cada
incremento. Ficou confuso? Vamos ver um esquema disso, usando no lugar do C um esboo
em uma linguagem de programao simplicada e genrica:

num 1
repita enquanto num 10:
imprima num
num num + 1

Para ter certeza de que cou tudo claro, vamos simular a execuo desse pseudocdigo.
Veja que inicialmente a varivel num vale 1. Como 1 10, entramos no bloco do repita;
assim, imprimimos o nmero 1 e a varivel passa a valer 2. Voltamos para a avaliao
da condio e obtemos novamente um resultado positivo, 2 10; o programa imprime o
nmero 2 e a varivel recebe o valor 3. Isso continua at que o valor de num seja 9; esse
nmero impresso e o valor 10 colocado na varivel. A ocorre a ltima iterao do lao:
imprimimos o nmero 10, e num passa a valer 11. Com isso, a condio do lao deixa de
ser verdadeira, pois ela ser avaliada como 11 10.

Pseudocdigo o nome dado a esse tipo de linguagem de programao simplicada


e genrica, e geralmente usado para enfatizar o algoritmo de uma certa operao, sem
depender das peculiaridades das diversas linguagens.
Algoritmo, por sua vez, a seqncia de instrues usada para completar uma certa
tarefa computacional. Essa denio muito parecida com a de programa; de fato, um
programa pode ser considerado uma grande e complexa associao de algoritmos para as
diversas tarefas que ele tem de realizar. Em geral, o algoritmo para realizar uma tarefa
no nico; pode haver vrios, alguns mais ecientes ou prticos que outros.

Agora vamos passar isso de volta para o C. A traduo quase literal, excluindo-se o
cabealho e o rodap do programa:
24 Captulo 2. Controle de uxo

#include <stdio.h>

int main()
{
int num;
num = 1;
while (num <= 10) {
printf("%d\n", num);
num = num + 1;
}

return 0;
}

Esse tipo de cdigo muito importante em programao, e ilustra um padro conhe-


cido como contador uma varivel usada em um lao, cujo valor varia em uma unidade
a cada iterao. Esse padro usado em diversas situaes que envolvam a repetio de
vrias operaes iguais ou bem parecidas, que possam ser diferenciadas apenas pelo valor
do contador.

E 2.1 Faa um programa que l um nmero n do teclado e, em seguida, imprime


todos os nmeros inteiros de 1 a n junto com seus quadrados.

Vamos construir um exemplo mais sosticado: calcular o fatorial de um nmero inteiro


no-negativo fornecido pelo usurio.

Denio. Dado um nmero natural n, denimos como fatorial de n, e denotamos n!,


o produto de todos os nmeros naturais menores que ou iguais a n. Observe que 1! = 1.
Denimos tambm 0! = 1.
Por exemplo, 5! = 1 2 3 4 5 = 120.

Como podemos usar o while nessa tarefa? Suponhamos primeiro que n maior que
1. Nessa situao, devemos fazer vrias multiplicaes, cada vez com fatores diferentes
anal, a multiplicao com n fatores se dene indutivamente a partir da multiplicao
elementar de dois fatores. Se realizarmos as operaes da esquerda pela direita, faremos as
seguintes contas:

12
(1 2) 3
..
.
(1 2 (n 1)) n

Tendo isso em vista, podemos montar um algoritmo no qual, em cada passo, o resultado
do passo anterior (a) multiplicado pelo valor atual de um contador (b), que vai de 2 at n:

a 1
b2
Repetindo operaes: laos (loops) 25

repita enquanto b n:
a a * b
bb+1

Ao nal do algoritmo, a varivel a conter o fatorial do nmero n, conforme desejvamos.


Agora pensemos nos casos n = 1 e n = 0. Se n = 1, a condio do lao (2 1) ser, j
de incio, falsa, e o valor inicial de a garantir o resultado correto, 1! = 1. Caso n = 0, a
condio tambm ser falsa de incio, e assim tambm teremos o resultado 0! = 1. Agora
vamos escrever tudo isso em C:

/* Calcula o fatorial de um nmero inteiro no-negativo. */


#include <stdio.h>

int main()
{
int n, a, b;

printf("Digite um nmero: ");


scanf("%d", &n);

a = 1;
b = 2;
while (b <= n) {
a = a * b;
b = b + 1;
}

printf("%d! = %d\n", n, a);


return 0;
}

E 2.2 Elabore um algoritmo (e construa um programa) que calcule o fatorial de


um nmero n multiplicando os fatores da direita para esquerda (ou seja, comeando de n e
indo at 1). Quais problemas voc encontra?

E 2.3 Faa um programa que l do teclado dois nmeros, a > 0 e b 0, e


imprime o valor da potncia ab .

Vamos agora ver outro tipo de programa que vale a pena ser ressaltado. Buscaremos
resolver o seguinte problema: ler do teclado uma sequncia de nmeros, cujo tamanho no
inicialmente conhecido, mas que sabemos ser terminada por um zero (ou seja, os demais
nmeros da sequncia so todos no-nulos), e somar os nmeros lidos. Que tipo de algoritmo
podemos usar para atacar esse problema?
Obviamente, se no sabemos a priori quantos elementos a sequncia tem, no podemos
armazenar todos os elementos para depois somar todos; precisamos acumular as somas par-
ciais a cada nmero lido. Utilizaremos para isso uma varivel chamada soma, que dever ser
inicializada com o valor zero, e qual se somar cada nmero lido.
A idia central ler nmeros do teclado indenidamente at que o nmero lido seja
zero. Sempre que o nmero for diferente de zero, devemos inclu-lo na soma e continuar
26 Captulo 2. Controle de uxo

lendo nmeros. Declarando uma varivel num para armazenar os nmeros lidos, podemos
proceder assim:

while (num != 0) {
soma = soma + num;

printf("Digite outro nmero ou 0 para terminar: ");


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

Mas preciso prestar ateno a um detalhe: num dever ser lido pela primeira vez antes
desse lao caso contrrio, a varivel no estaria inicializada!. Feitas essas observaes,
nosso programa car com essa cara:

#include <stdio.h>

int main()
{
int num, soma;

soma = 0;

printf("Digite um nmero ou 0 para terminar: ");


scanf("%d", &num);

while (num != 0) {
soma = soma + num;

printf("Digite um nmero ou 0 para terminar: ");


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

printf("Soma = %d\n", soma);


return 0;
}

Vamos agora adicionar mais uma tarefa ao nosso problema: encontrar o elemento m-
ximo da sequncia digitada pelo usurio. Para ver como faremos isso, imagine que voc
dispe de papel e caneta, e que algum est ditando para voc a tal sequncia de nmeros.
Como voc realizaria essa tarefa? Voc anota o primeiro nmero e, caso oua posterior-
mente um nmero maior que ele, anota-o logo abaixo; se ouvir outro nmero ainda maior,
anota-o abaixo do anterior; e assim por diante no necessrio tomar nota de todos os
nmeros. No nal, o ltimo nmero anotado ser o maior nmero da sequncia.
Um computador poderia fazer isso exatamente da mesma maneira. Em vez de anotar os
nmeros, ele os guardaria em variveis; alm disso, ele no precisaria manter as anotaes
anteriores; basta substituir o valor da varivel. Assim, nosso programa completo poderia ser
escrito assim:

#include <stdio.h>
Contadores e laos for 27

int main()
{
int soma, /* somas parciais */
num, /* ltimo nmero lido */
max; /* candidatos a mximo */

soma = 0;

printf("Digite um nmero ou 0 para terminar: ");


scanf("%d", &num);
max = num;

while (num != 0) {
soma = soma + num;
if (num > max) /* para procurar o mximo */
max = num;

printf("Digite um nmero ou 0 para terminar: ");


scanf("%d", &num); /* l o prximo nmero */
}

printf("Soma = %d\n", soma);


printf("Mximo = %d\n", max);
return 0;
}

2.3 Contadores e laos for


Se voc olhar com ateno para os exemplos do fatorial e do programa que conta de 1 a
10, que utilizam uma varivel-contador, ver que os dois programas tm estruturas bem
parecidas: primeiro era denido o valor inicial do contador; depois, escrevia-se um lao
que denia o valor mximo desse contador; no nal desse lao, o valor do contador era
aumentado. Esquematicamente, tnhamos
inicializao do contador ;
while (controle do valor mximo)
{
comandos;
incremento do contador ;
}

Esse tipo de situao uma das mais frequentes aplicaes dos laos; por isso, essa
estrutura comum foi abreviada em outro tipo de lao: a estrutura for. Com ela, a escrita
desses laos ca mais compacta e de mais fcil visualizao:

for (inicializao; controle; incremento)


{
comandos;
28 Captulo 2. Controle de uxo

Com isso, podemos reescrever os exemplos anteriores da seguinte maneira:


/* Imprime todos os nmeros inteiros de 1 at 10
* junto com seus quadrados */
#include <stdio.h>

int main()
{
int num;
for (num = 1; num <= 10; num = num + 1)
printf("%d\n", num);
return 0;
}

/* Calcula o fatorial de um nmero inteiro no-negativo. */


#include <stdio.h>

int main()
{
int n, a, b;
printf("Digite um nmero: ");
scanf("%d", &n);

a = 1;
for (b = 2; b <= n; b = b + 1)
a = a * b;

printf("%d! = %d\n", n, a);


return 0;
}

O funcionamento da estrutura for pode ser mais detalhado como a seguir (voc pode ver
isso de maneira mais esquemtica na gura 2.2):
1. A varivel do contador inicializada com um valor especicado; isso ocorre apenas
uma vez, antes do incio do lao. A inicializao costuma ser uma instruo do tipo
i = 1.

2. A condio de controle especica que condio a varivel do contador deve satisfazer


para que os comandos sejam executados; em outras palavras, quando essa condio
deixar de valer, o lao ir parar de se repetir. Por exemplo, uma condio do tipo
i <= n diz que o lao ser interrompido assim que o contador i car maior que n. Se
voc quiser que seu contador decresa at um valor mnimo, voc usar uma condio
do tipo i >= n.
3. Sempre que a condio de controle for satisfeita, executada uma iterao do bloco
de comandos fornecido, de cabo a rabo. Mesmo que a condio de controle deixe de
ser satisfeita no meio do bloco, o lao s ser abandonado quando chegarmos ao nal
do bloco. Essa ltima observao vale tanto para o for quanto para o while.
Contadores e laos for 29

Figura 2.2: Esquema de execuo da estrutura for.

4. Ao nal de cada iterao, executa-se a instruo de incremento ela pode ser qual-
quer instruo que altere o valor do contador, como i = i + 1 ou i = i - 2. Apesar
do nome incremento, voc pode variar o valor do contador da maneira que desejar
diminuindo, por exemplo.

Apesar de termos vinculado nossa discusso do for a uma varivel-contador, o for uma
estrutura bastante exvel. No entanto, isso no signica que voc deve sair usando o for a
torto e a direito e esquecer o while; deve existir uma boa razo para existirem os dois tipos de
estrutura. E essa razo semntica: voc deve usar o for quando seu lao tiver um contador
ou algo com funcionamento semelhante; nos outros casos, voc deve, em geral, ater-se ao
while.

2.3.1 Abreviaes
Atribuies que envolvem o valor original da varivel que est sendo alterada (como a nossa
j conhecida i = i + 1) so muito comuns, j que so usadas o tempo todo nos laos com
contadores; por isso, foram criadas algumas abreviaes bastante teis. Quando representa
um dos cinco operadores aritmticos, podemos abreviar a = a b para a = b. Assim,
temos:

Abreviao Signicado
var += expr var = var + expr
var -= expr var = var - expr
var *= expr var = var * expr
var /= expr var = var / expr
var %= expr var = var % expr

Existe ainda outro tipo de abreviao qui o mais usado em C que serve para
aumentar ou diminuir em uma unidade o valor de uma varivel. So os operadores ++ e --,
que podem ser usados tanto antes quanto depois do nome das variveis a serem alteradas. Ou
seja, para aumentar o valor de uma varivel, podemos escrever

var++;
30 Captulo 2. Controle de uxo

++var; /* equivalem a: var = var + 1 */

e, para diminuir, usamos


var--;
--var; /* equivalem a: var = var - 1 */

No contexto atual, o mais comum usar os operadores depois dos nomes das variveis.
Porm, mais tarde veremos que existe uma diferena importante entre as duas possibilidades
de posio.
Dessa maneira, a cara de um lao com contador passa a car parecida com isso (aqui
foi reescrito o lao do ltimo exemplo):
for (b = 2; b <= n; b++)
a *= b;

Exerccios
2.4. Crie um programa que l um nmero natural n do teclado e imprime todos os divisores
desse nmero. Ao nal, imprima tambm a soma dos divisores encontrados.
2.5. Aproveite o programa do exerccio anterior para vericar se um nmero n dado
primo.
2.6. Crie um programa que calcula o fatorial duplo de um nmero natural n (lido do te-
clado), que denido como a seguir:
{
n(n 2) 4 2, se n par;
n!! =
n(n 2) 3 1, se n mpar.

2.7. Faa um programa que l dois nmeros inteiros a e b, sendo a > 0 e b 0, e calcula
a potncia ab . Veja se seu algoritmo funciona para o caso b = 0; tente elabor-lo de
modo que no seja preciso um teste especial para esse caso.
2.8. Faa um programa que l uma sequncia de nmeros naturais do teclado (terminada
por zero) e imprime a quantidade de nmeros pares e mpares da sequncia. Imprima
tambm o maior e o menor mpar e o maior e o menor par encontrados na sequncia.
2.9. Variao sobre o mesmo tema: Mesmo problema que o anterior; mas agora o tamanho
n da sequncia conhecido previamente. Antes de tudo voc deve ler n, e depois ler
os n nmeros.
2.10. Faa um programa que l do teclado uma sequncia de nmeros inteiros no-nulos (ter-
minada por zero) e os soma e subtrai alternadamente por exemplo, para a sequncia
[5, 7, 1, 4, 9], seu programa dever calcular 5 7 + 1 (4) + 9.
2.11. [Problema] Crie um programa que recebe um nmero n e a seguir l uma sequncia
de n algarismos de 0 a 9; voc dever calcular e imprimir o nmero que corresponde
queles algarismos, na mesma ordem (no vale imprimir dgito por dgito! O objetivo
construir o nmero a partir da sua sequncia de dgitos). Por exemplo, para a sequncia
[5, 4, 2, 0, 7, 0], seu programa dever imprimir o nmero 542070.
Condies compostas 31

2.12. [Idem] Crie um programa que l um nmero natural n e imprime a soma de seus
dgitos (na base decimal).

2.4 Condies compostas


Em muitos casos, voc precisar vericar condies mais complexas do que as simples ex-
presses com operadores relacionais, como x > 10 ou cod == 15. Haver muitas vezes em
que voc precisar fazer alguma coisa somente quando duas (ou mais) condies forem sa-
tisfeitas ao mesmo tempo; em outras, voc precisar fazer algo quando qualquer uma, dentre
vrias condies, for satisfeita.
Por exemplo, suponha que voc queira que o usurio digite nmeros entre 0 e 10 e que
voc precise vericar se os nmeros realmente esto entre 0 e 10. J falamos anteriormente
que, em C, isso no se escreve como 0 <= x <= 10 esse cdigo vlido, mas no faz o
que gostaramos. Mas podemos contornar isso usando ifs encadeados, pois (0 x 10)
equivalente a ((x 0) e (x 10)):

if (x >= 0) {
if (x <= 10)
printf("Ok!\n");
else
printf("O nmero precisa estar entre 0 e 10!\n");
}
else
printf("O nmero precisa estar entre 0 e 10!\n");

No entanto, esse mtodo tem duas desvantagens: (1) ele no deixa to evidente o fato de
que queremos que as duas condies sejam satisfeitas ao mesmo tempo; e (2) tivemos mais
trabalho para vericar quando as condies no so satisfeitas foi necessrio usar dois
blocos else, e acabamos usando o mesmo cdigo nos dois.
Quando precisamos de critrios compostos como esse dentro de um lao, a situao
torna-se ainda mais complicada; com o que aprendemos at agora, isso simplesmente im-
possvel de se implementar mesmo com outros recursos da linguagem, a soluo no
seria nem um pouco prtica ou clara. Por isso, precisamos introduzir dois operadores que
permitem a criao de condies compostas:

&& e || ou

O operador &&, chamado de E lgico, serve para vericar se duas condies so satisfeitas
simultaneamente. O operador ||, o OU lgico, verica se, dentre duas condies, pelo menos
uma satisfeita. (Os nomes desses operadores sero grafados em maisculas para evitar
confuses no texto.)
32 Captulo 2. Controle de uxo

Note que, coloquialmente, quando usamos a palavra ou para unir duas oraes, geral-
mente imaginamos que apenas uma delas verdadeira (por exemplo, Independncia ou
morte cada uma das possibilidades exclui o acontecimento da outra). Nesse caso,
dizemos que trata-se de um ou exclusivo. Em outras palavras, esse ou signica que,
dentre as duas sentenas, uma e apenas uma verdadeira.
J em computao, quando dizemos A ou B, geralmente queremos dizer que, das duas
sentenas (condies), pelo menos uma verdadeira. Esse conhecido como ou inclu-
sivo. Daqui para a frente, a menos que indiquemos o contrrio, usaremos sempre o ou
inclusivo.

Com esses operadores, podemos reescrever aquele exemplo assim:

if (x >= 0 && x <= 10)


printf("Ok!\n");
else
printf("O nmero precisa estar entre 0 e 10!\n");

Bem mais claro, no? Veja que tambm podemos inverter as coisas para escrever nosso
exemplo usando um operador OU se um nmero no satisfaz a condio de estar entre 0
e 10, ento necessariamente ele ou menor que zero ou maior que 10:

if (x < 0 || x > 10)


printf("O nmero precisa estar entre 0 e 10!\n");
else
printf("Ok!\n");

2.5 Repeties encaixadas


Uma grande classe de problemas computacionais comuns requer, para sua soluo, o uso
de repeties encaixadas. Isso, conceitualmente, no nenhuma novidade; trata-se simples-
mente de colocar um lao dentro do outro: para cada iterao do lao externo, o lao interno
ser executado tantas vezes quanto for necessrio para que ele termine.
Podemos encaixar os laos da maneira que quisermos, ou seja, qualquer mistura de for
e while permitida. O nico cuidado que devemos ter o de no misturar as variveis de
controle dos vrios laos por exemplo, se encaixamos dois laos for, devemos dar nomes
diferentes aos contadores (por exemplo, i para o lao externo e j para o interno). No entanto,
os contadores no precisam ser completamente independentes: os limites dos contadores dos
laos internos podem depender da posio atual no lao principal.
Vamos estudar um exemplo. Veremos um programa que encontra os primeiros N nme-
ros primos, onde N um nmero dado. Para isso, passamos por todos os nmeros naturais
(enquanto no tivermos selecionado os N primeiros primos) e vericamos se cada um deles
primo. No difcil testar se um nmero n primo: devemos vericar se ele tem um
divisor no-trivial, isto , um que no seja igual a 1 ou n. Assim, vericamos entre todos os
candidatos (a princpio, {2, 3, . . . , n 1}, mas essa lista ainda pode ser bem renada) se h
algum divisor de n. Se no acharmos nenhum, porque n primo.
Agora vejamos como escrever a soluo para nosso problema. Se voc fez o exerccio 2.5
da pgina 30, j tem o problema quase inteiro resolvido. (Se no fez, volte e tente resolver!)
Vamos ver uma possvel implementao da vericao de primalidade de um nmero:
Repeties encaixadas 33

int divisores , /* conta o nmero de divisores */


k, /* candidato a divisor */
n; /* nmero a ser testado */

divisores = 0;

/* testa todos os candidatos */


for (k = 2; k < n; k++)
if (n % k == 0)
divisores++;

if (divisores == 0) {
/* no achamos nenhum divisor dentre os possveis
candidatos , ento primo */
}

Essa uma das implementaes mais inecientes possveis, pois vrios dos candidatos da
lista {2, 3, . . . , n 1} podem ser sumariamente descartados. Por exemplo, nenhum nmero
maior que n 2 pode ser divisor de n, pois isso signicaria que n tambm tem um divisor
entre 1 e 2. Ainda mais, se s precisamos vericar se n primo (e no achar todos os seus
divisores), basta vericar os possveis divisores d n (pois, se d for realmente divisor,
n/d n tambm ser divisor). Mas, no momento, para o propsito atual, vamos nos
contentar com a maneira ineciente.
Agora tudo o que precisamos fazer usar esse cdigo vrias vezes (usando um lao) para
encontrar N nmeros primos:
int N, /* quantidade de primos a procurar */
i, /* primos j encontrados */
n, /* candidato atual */
divisores; /* conta o nmero de divisores */

for (n = 2, i = 0; i < N; n++) {


divisores = 0;

/* testa todos os candidatos a divisor */


for (k = 2; k < n; k++)
if (n % k == 0)
divisores++;

if (divisores == 0) {
/* no achamos nenhum divisor dentre os possveis
candidatos , ento primo */
printf("%d\n", n);
i++;
}
}

Observe que cada iterao do lao for externo verica se um nmero n primo, e s
aumenta o contador i em caso armativo. Veja tambm que o limite do contador k, no for
34 Captulo 2. Controle de uxo

interno, depende do contador n do lao externo.


Mais um detalhe que foi usado no programa mas ainda no havia sido mencionado: na
inicializao e no incremento de um lao for, possvel escrever comandos compostos. Po-
demos separar vrias atribuies por vrgulas (lembre-se que o ponto-e-vrgula separa a
inicializao da condio e esta do incremento), como foi feito no comando de inicializao
do lao externo:

for (n = 2, i = 0; i < N; n++) {

Da mesma maneira, poderamos criar um incremento composto, como i++, j++.

2.6 Variveis booleanas


As estruturas if, while e for dependem da avaliao de expresses condicionais para exe-
cutarem seu trabalho. Para que essas expresses realmente faam sentido como condies,
elas s podem assumir dois valores: verdadeiro ou falso (que indicam se o bloco correspon-
dente ser ou no executado). Variveis que s indicam esses dois estados so chamadas
booleanas ou lgicas.
Em C, no h nenhum tipo primitivo especicamente para lidar com variveis booleanas;
por isso, convenciona-se que qualquer valor diferente de zero interpretado como verdadeiro,
e o zero interpretado como falso. Ou seja, qualquer varivel ou expresso, independente de
seu tipo, pode ter um valor booleano que ou verdadeiro ou falso.
Os operadores que devolvem um valor lgico usam os valores 0 e 1 (alm de serem a
escolha mais simples, esses dois valores so sempre representveis em qualquer tipo num-
rico) para representar os resultados falso e verdadeiro, respectivamente; assim, quando
fazemos alguma comparao do tipo a < 10 ou x == y, o valor da expresso comparativa
0 ou 1. No entanto, em geral no devemos nos preocupar com essa representao, pois essas
expresses j tm um valor lgico bem denido.

A partir desse conhecimento, podemos analisar o que acontece em algumas das situaes
capciosas que mencionamos anteriormente. Primeiro temos as comparaes compostas do
tipo a < b < c; quando escrevemos em C exatamente dessa maneira, o que ocorre que a ex-
presso avaliada da esquerda para a direita, dois operandos por vez. Assim, primeiramente
avaliada a expresso a < b, cujo resultado poder ser 0 ou 1 chamemo-no genericamente
de R. Ento avaliada a expresso R < c, que nada tem a ver com a relao entre b e c.
Olhemos agora para as comparaes errneas que usam o operador de atribuio em vez
do operador de comparao de igualdade, como if (a = b). O valor de uma expresso como
a = b corresponde ao valor que foi atribudo, ou seja, b (ou a aps a atribuio). Assim, o
cdigo if (a = b) equivalente a

a = b;
if (b)

No entanto, como a varivel b est num contexto lgico, seu valor ser interpretado como
um valor booleano, ou seja, falso se b == 0 e verdadeiro se b != 0. Assim, nalmente, a
comparao if (a = b) na verdade equivalente a

Nota para o futuro: na verdade, s podem ser interpretados como booleanos os tipos escalares, ou seja, os
tipos numricos (inteiro e ponto utuante), alm dos ponteiros, que s veremos no captulo 5.
Variveis booleanas 35

a = b;
if (b != 0)

que, claramente, no tem nada a ver com a igualdade (prvia) entre a e b.

R 05/02/2011
Funes
3
3.1 Introduo
Em C, uma funo um pedao de cdigo, dentro de um programa maior, que realiza
uma certa tarefa com uma certa independncia do resto do programa. Funes podem ser
executadas vrias vezes, e uma grande vantagem disso a reutilizao de cdigo: em vez
de repetir vrias vezes o cdigo para executar certa tarefa, podemos simplesmente chamar
vrias vezes a funo que executa essa tarefa. Alm de economizar linhas de cdigo, isso
permite que voc mude facilmente o cdigo associado a essa tarefa se no fosse pelas
funes, voc teria de buscar em seu programa por todos os locais em que voc executou
essa tarefa e alterar o cdigo em cada um. Mais ainda, ao organizarmos o cdigo em vrias
funes, podemos focar cada parte do cdigo em uma s tarefa, deixando o programa mais
claro e limpo.
Em C, uma funo deve ter as seguintes caractersticas:

Um nome pela qual ela possa ser chamada. Os nomes possveis seguem as mesmas
restries que os nomes de variveis: devem comear com uma letra ou com um
sublinhado (_), e podem conter qualquer combinao desses e dos algarismos 09.
Lembre-se de que h distino entre maisculas e minsculas.

Valores de entrada ou parmetros so os valores sobre os quais a funo deve ope-


rar. Os parmetros das funes (tambm chamados de argumentos) atuam de maneira
anloga s variveis das funes matemticas.
Tambm possvel criar funes sem argumentos por exemplo, se voc quiser
criar uma funo que calcula o valor (aproximado, claro) do nmero , no precisa
de nenhum parmetro (a princpio; voc poderia introduzir parmetros se quisesse
por exemplo, a preciso desejada , mas eles no so necessrios se voc quer
executar a operao de uma maneira pr-determinada).
Um valor de sada, que corresponde ao resultado da funo para o conjunto dos pa-
rmetros de entrada fornecidos. Tambm possvel criar funes que no devolvem
nenhum valor de sada. Por exemplo, uma funo que simplesmente exibe uma mensa-
gem na tela no precisa devolver nenhum valor embora a funo tenha um resultado,
ele mostrado na tela, e no devolvido internamente para o programa.

37
38 Captulo 3. Funes

Burocraticamente, ao se denir uma funo, precisamos sempre especicar todas essas


caractersticas: o nome da funo, a quantidade de parmetros e o tipo de cada um, alm do
tipo do valor de sada (caso haja valor de sada). E, claro, voc deve denir o que a funo
vai fazer.
Para denir uma funo, usamos a seguinte estrutura:
tipo_da_sada nome_da_funo (parmetros )
{
contedo da funo;
}

Essa denio deve ser colocada no nvel superior do arquivo, ou seja, no deve estar
dentro de outra funo como o main. Todas as funes dentro de um arquivo devem car no
mesmo nvel, cada uma aps o nal da anterior. Na verdade, enquanto eu no falar de uma
outra coisa (ainda neste captulo), as demais funes devem car antes da funo main.
O tipo do valor de sada pode ser qualquer um dos tipos usados para variveis (por en-
quanto, voc s conhece o int). No caso em que no h valor de sada, voc deve usar no
lugar do tipo a palavra void (vazio, em ingls). Ela no um tipo de varivel; ela apenas
indica a ausncia de um valor. (Muitos falam do tipo void, mas isso apenas um abuso
de linguagem.)
A denio dos parmetros semelhante declarao de variveis. Cada parmetro deve
ter um nome (seguindo, novamente, as mesmas restries vlidas para os nomes de variveis)
e um tipo. Para especicar esses parmetros, voc deve usar o formato

tipo_1 nome_1, tipo_2 nome_2, , tipo_n nome_n


Note que, nesse caso, no existe nenhum tipo de abreviao para vrios parmetros do
mesmo tipo (como ocorria na declarao de variveis). No caso de no haver parmetros,
voc deve usar a palavra void sozinha dentro dos parnteses:

tipo_da_sada nome_da_funo (void)


Ateno: segundo muitos textos, em caso de ausncia de parmetros bastaria deixar os parn-
teses vazios, sem nada no meio. Segundo o padro da linguagem, nesse caso o compilador
apenas entenderia que no h informao sobre os parmetros; mesmo assim, isso costuma
ser aceito pela maioria dos compiladores.
O conjunto dessas trs denies do nome, do tipo de sada e da lista de parme-
tros da funo chamado de cabealho da funo. Vamos construir alguns exemplos de
cabealhos:

Uma funo que calcula a soma dos divisores de um nmero inteiro n. Como entrada,
teremos obviamente o nmero n, que ser uma varivel do tipo int. Como sada,
teremos outro valor do tipo int, que corresponder soma dos divisores de n. Com
isso, o cabealho ca
int soma_divisores(int n)

Uma funo que recebe dois nmeros inteiros, a e b, e devolve o valor da potncia
ab . Novamente, todos os valores envolvidos so do tipo int, e nosso cabealho vem a
ser
Os parmetros e o valor de sada 39

int potencia(int a, int b)

Voc est criando uma funo que recebe um ms e um ano e imprime na tela o ca-
lendrio desse ms. Nesse caso, no h nenhum valor de sada (os dados so enviados
diretamente para a tela, com a funo printf), o que indica que usaremos a palavra
void no lugar do tipo da sada; mas h dois parmetros do tipo int, ento o cabealho
ca assim:
void imprime_calendario(int mes, int ano)

Suponha agora que voc quer fazer uma funo que l um inteiro do teclado (usando a
funo scanf como intermediria). Para isso, voc no precisa de nenhum parmetro
de entrada; voc simplesmente devolver o nmero lido.
int le_inteiro(void)

Agora, o prato principal: como escrever o contedo da funo? Isso o menor dos
mistrios tudo que voc precisaria saber sobre isso j foi feito na funo main, que
uma funo (quase) como outra qualquer. Basta colocar um par de chaves aps o cabealho
e colocar no meio das chaves tudo o que voc souber fazer em C: declarar variveis, fazer
contas, chamar as funes scanf e printf, usar laos e controles, etc.
Antes de poder criar exemplos concretos, precisamos ver um pouco mais de teoria.

x y

int funcao(int x, int y)

Figura 3.1: Representao ilustrativa de uma funo.

3.2 Os parmetros e o valor de sada


J vimos como denir as entradas e sadas de uma funo; agora precisamos saber lidar com
elas: como acessar os parmetros de entrada e como devolver o valor de sada. A primeira
40 Captulo 3. Funes

tarefa muito simples: uma vez denidos os parmetros no cabealho da funo, voc pode
acess-los como se fossem variveis normais. Por exemplo, se quisssemos criar uma funo
que recebe dois inteiros e imprime sua soma na tela, poderamos escrever:

void imprime_soma(int a, int b)


{
int soma;
soma = a + b;
printf("%d\n", soma);
}

Veja que nesse caso a funo no tem nenhum resultado a devolver para o programa,
ento usamos a palavra void para o tipo de sada. Lembre-se de que no necessrio criar
uma varivel intermediria para fazer essa conta. Poderamos ter escrito apenas

void imprime_soma(int a, int b)


{
printf("%d\n", a + b);
}

Para devolver o valor de sada, usamos a instruo return seguida do valor de sada
(terminando com um ponto-e-vrgula). O valor pode ser qualquer expresso que seja legtima
de se colocar no lado direito de uma atribuio: o valor de uma varivel, uma constante
numrica, uma expresso aritmtica, etc. Por exemplo:

return 0;
return x*x;
return y + 1;

Vamos ver um exemplo mais concreto. A funo a seguir devolve para o programa a
soma dos dois nmeros recebidos como parmetros:

int soma(int a, int b)


{
return a + b;
}

importante ressaltar que a instruo return tambm encerra a execuo da funo, ou


seja, voc s pode execut-la quando no houver mais nada a fazer dentro da funo. Se voc
colocar uma instruo return no meio da funo, ela devolver o valor indicado e ignorar
todo o resto da funo.
Vale tambm salientar que uma funo void no pode devolver nenhum valor. Seria um
erro escrever algo do tipo return 0 numa funo void da mesma maneira, errado usar
uma instruo return sem valor numa funo que no void. No entanto, voc pode usar a
instruo return (sem nenhum valor) para terminar uma funo void no meio. Por exemplo:

void imprime_numero(int n)
{
if (n < 0) {
printf("No quero imprimir nmeros negativos!\n");
return;
Usando funes 41

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

Vamos criar um exemplo mais elaborado: uma funo que calcula a potncia ab , dados
dois inteiros a e b. (Para deixar o cdigo mais claro, chamamos a de base e b de expoente.)
Veja que o cdigo da funo essencialmente o mesmo que havamos criado antes.

int potencia(int base, int expoente)


{
int pot, i;

pot = 1;
for (i = 1; i <= expoente; i++)
pot *= base;
return pot;
}

3.3 Usando funes


J sabemos como criar funes; mas como fazemos para us-las? Tendo em mos o nome da
funo e a lista de valores que desejamos mandar como parmetros de entrada, a frmula
para chamar uma funo simples, e no depende de a funo ter ou no um valor de sada:

nome_da_funo (parmetro_1, parmetro_2, )

Caso a funo no tenha nenhum parmetro, simplesmente deixe os parnteses sozinhos


sem nada no meio:

nome_da_funo ()
Esse tipo de comando uma chamada de funo; ele simplesmente faz com que o
computador pule para a funo chamada, execute-a por inteiro e depois volte para o mesmo
ponto de onde saiu.
Se a funo tiver um valor de sada, provavelmente vamos querer aproveit-lo nesse
caso, basta colocar essa chamada de funo no meio de uma expresso qualquer; por exemplo,
podemos guard-lo numa varivel, coloc-lo no meio de uma expresso aritmtica ou mesmo
mand-lo como parmetro para outra funo. Um exemplo, utilizando duas funes que j
escrevemos acima:

int a, b, c;
a = potencia(2, 3);
b = soma(a, 8);
c = potencia(3, soma(b, a) + b);

Se no houver valor de sada, simplesmente colocamos a chamada de funo com o tra-


dicional ponto-e-vrgula no nal. Por exemplo:
42 Captulo 3. Funes

int a, b;
a = potencia(2, 3);
b = soma(a, 8);
imprime_soma(a, b + potencia(a, b));

Vamos ver um exemplo de programa completo usando funes. O funcionamento dele


ser bem simples: leremos um par de inteiros do teclado e calcularemos o primeiro elevado ao
segundo (o segundo deve ser positivo!), usando a funo potencia j escrita anteriormente:

#include <stdio.h>

int potencia(int base, int expoente)


{
int pot, i;

pot = 1;
for (i = 1; i <= expoente; i++)
pot *= base;
return pot;
}

int main()
{
int base, expoente;

printf("Digite a base: ");


scanf("%d", &base);

printf("Digite o expoente (inteiro positivo): ");


scanf("%d", &expoente);

printf("Resultado: %d\n", potencia(base, expoente));

return 0;
}

Veja que a funo potencia foi colocada antes da main, como j observamos que seria
necessrio. Se voc trocasse a ordem das funes, receberia uma mensagem de erro do
compilador; veremos a seguir por que isso ocorre e uma outra maneira de denir as funes
que, de certa maneira, elimina essa limitao.

3.4 Trabalhando com vrias funes


A linguagem C bastante rgida quanto aos tipos dos objetos (variveis e funes) usados nos
programas: toda varivel deve ser declarada antes de ser utilizada, para que o computador
saiba como organizar a memria para o acesso a essa varivel. Da mesma maneira, para que
o computador saiba como organizar o trnsito dos valores de entrada e sada, toda funo
deve ser declarada antes de ser chamada.
Escopo de variveis 43

A princpio, isso no um grande problema, pois, quando fazemos a denio de uma


funo (isto , escrevemos seu cabealho e logo em seguida seu contedo), a declarao feita
implicitamente. No entanto, essa declarao s vale para o que vier depois dela; ento uma
funo no pode ser usada dentro de outra funo que vem antes dela. Por isso que eu disse
que todas as funes deveriam ser denidas antes da main; se chamamos a funo potencia
dentro da funo main e a denio de potencia s vem depois de main, o compilador no
tem nenhuma informao sobre quem potencia, ento no tem como chamar essa funo
corretamente.
Por esse motivo, uma funo pode ser declarada explicitamente antes de ser propria-
mente denida. A declarao explcita de uma funo bastante simples: basta reescrever
o cabealho da funo, seguido de um ponto-e-vrgula:

tipo_da_sada nome_da_funo (parmetros);


Por exemplo, para declarar a funo potencia que criamos anteriormente, escreveramos
simplesmente
int potencia(int base, int expoente);

J vimos que no estritamente necessrio declarar explicitamente as funes em todos


os casos: em vrias situaes possvel contornar o problema com uma simples reordenao
das funes. No entanto, com programas maiores e mais complexos, sempre uma boa idia
declarar todas as funes no comeo do arquivo, e voc encorajado a sempre faz-lo
isso nos permite uma melhor organizao do cdigo, sem precisar nos prender ordem de
dependncia das funes; alm disso, a lista de declaraes pode servir como um pequeno
ndice das funes que foram denidas num certo arquivo.

3.5 Escopo de variveis


Uma caracterstica bastante til (e importante) da linguagem C que as funes so to-
talmente independentes quanto s variveis declaradas dentro delas. Se voc declara uma
varivel dentro de uma funo essas so chamadas de variveis locais , ela s existe
dentro da prpria funo; quando a funo termina sua execuo, as variveis locais so, de
certa maneira, perdidas. Nenhuma operao feita com variveis locais poder afetar outras
funes. Ainda mais, voc pode declarar variveis locais com o mesmo nome em funes
diferentes, sem o menor problema; cada funo saber qual a sua varivel e no mexer
nas variveis das outras funes.
bom ressaltar o papel dos parmetros de funes nessa histria: eles funcionam exata-
mente como as variveis locais, com a diferena de que seus valores so atribudos de forma
implcita. Cada vez que voc chama uma funo com um certo conjunto de parmetros, os
valores desses parmetros so copiados para a funo chamada. Assim, voc tambm pode
modicar os valores dos parmetros de uma funo, e nada mudar na funo que a chamou.
Um exemplo clssico para ilustrar esse comportamento uma tentativa de criar uma
funo que troca o valor de duas variveis:
#include <stdio.h>

void troca(int a, int b)


{
int temp;
44 Captulo 3. Funes

printf("funo troca - antes: a = %d, b = %d\n", a, b);


temp = a;
a = b;
b = temp;
printf("funo troca - depois: a = %d, b = %d\n", a, b);
}

int main()
{
int x, y;
x = 10;
y = 5;
printf("main - antes: x = %d, y = %d\n", x, y);
troca(x, y);
printf("main - depois: x = %d, y = %d\n", x, y);
return 0;
}

Se voc executar esse exemplo, ver que o valor das variveis x e y dentro da funo
main no se altera. A sada desse programa ser:

main - antes: x = 10, y = 5


funo troca - antes: a = 10, b = 5
funo troca - depois: a = 5, b = 10
main - depois: x = 10, y = 5

Voc deve entender por que isso acontece: quando trocamos as variveis a e b, estamos
nada mais que trocando de lugar as cpias das variveis originais x e y. Quando a funo
troca encerrada, essas cpias so inutilizadas e a funo main volta a ser executada. No
zemos nada explicitamente com as variveis da funo main; no h motivo para que elas
mudem.
Existe, sim, uma maneira de criar uma funo que troque o valor de duas variveis
ela envolve o uso de ponteiros, conforme veremos no Captulo 5.

As variveis locais podem ser declaradas no apenas dentro de funes, mas dentro de
laos ou estruturas de controle (if, while, for, etc.). Isso quer dizer que podemos criar va-
riveis que s existem dentro de um bloco if (por exemplo), e no podem ser acessadas fora
dele, mesmo dentro da mesma funo. O contexto em que uma varivel existe e pode ser
acessada denominado escopo. Ento dizemos que o escopo de uma certa varivel um
certo bloco ou uma certa funo.
Mesmo podendo declarar variveis dentro de qualquer bloco (bloco como se costuma
designar genericamente qualquer estrutura de comandos delimitada por { }, seja uma funo
ou uma estrutura de controle), continua valendo a restrio de que as declaraes de variveis
devem vir no comeo do bloco correspondente, no podendo haver nenhum outro tipo de
comando antes dessas declaraes.
Mais sobre nmeros
4
4.1 Bases de numerao
Ao criar uma mquina que faz clculos, nada mais crucial que a escolha da maneira de re-
presentao dos nmeros ou do formato em que eles sero armazenados. Para ns, o sistema
decimal parece ser o mais natural contamos com os 10 dedos das mos (uma possvel
explicao para o uso da base 10 e no de outra); usamos as potncias de 10 (dez, cem, mil)
como referncia ao falar os nmeros por extenso.
No entanto, os componentes de um computador funcionam com base em correntes el-
tricas, e com eles muito mais simples implementar um sistema de numerao binria (ou
base 2), no qual existem apenas dois dgitos (0 e 1), correspondendo, por exemplo, a um
nvel baixo ou nulo (0) e a um nvel alto (1) de corrente ou voltagem.
Se tomarmos um instante para compreender como a nossa usual base 10 funciona, ser
mais fcil entender como a base 2 funciona. Tomemos um nmero; por exemplo, 41 502.
Ele pode ser tambm escrito como

41502 = 40000 + 1000 + 500 + 2


= 4 10000 + 1 1000 + 5 100 + 0 100 + 2 1
= 4 104 + 1 103 + 5 102 + 0 101 + 2 100

Veja, ento, que cada algarismo funciona como um multiplicador de uma potncia de
10. Os expoentes so contados a partir da direita, comeando pelo zero. Assim, o primeiro
algarismo (a partir da direita) multiplicado por 100 , o segundo por 101 , e assim por diante.
Perceba a regrinha implcita do sistema: o valor de cada algarismo sempre menor que a
base (o maior algarismo, 9, menor que a base, 10) e maior ou igual a zero. Isso necessrio
para que cada nmero tenha uma nica representao nessa base.
Para lembrar como so as regras para se contar, pense em um contador analgico (por
exemplo, o odmetro dos carros mais antigos, ou os medidores de consumo de gua e energia
eltrica nas casas), daqueles que mostram cada algarismo em uma rodinha. Cada rodinha
marca um algarismo de 0 a 9 e corresponde a uma ordem de grandeza (1, 10, 100, ).
Sempre que uma das rodinhas d uma volta completa (passando do 9 para o 0), a rodinha
sua esquerda aumenta seu dgito. Se esse dgito j era 9, o mesmo processo repetido
sucessivamente para as outras rodinhas da esquerda.

45
46 Captulo 4. Mais sobre nmeros

Sabendo isso, podemos, por analogia, proceder base 2. As regras so, pois, as seguintes:

Como cada algarismo deve ter um valor menor que o da base e maior e igual a zero, s
podemos ter os algarismos 0 e 1, como voc provavelmente j sabia. Por isso, contar
em binrio bastante simples pense num contador binrio, no qual cada rodinha
s tem duas posies: 0 e 1. Quando uma rodinha passa do 1 para o 0, a prxima
rodinha (a da esquerda) se movimenta: se estiver no 0, passa para o 1; se estiver no 1,
passa para o 0, propagando o movimento para a esquerda.
Os nmeros de um a dez escrevem-se assim em binrio:

1, 10, 11, 100, 101, 110, 111, 1000, 1001, 1010

Os algarismos binrios, como j disse, costumam corresponder ao estado de um com-


ponente quanto passagem de corrente. Esses algarismos so geralmente chamados
de bits abreviao de binary digit.
O valor do nmero obtido multiplicando-se cada algarismo por uma potncia de 2
(e no mais de 10) de acordo com sua posio. Assim, o nmero 1101101 poderia ser
traduzido para o sistema decimal da seguinte maneira:

(1101101)2 = 1 26 + 1 25 + 0 24 + 1 23 + 1 22 + 0 21 + 1 20 = (109)10

Veja que usamos a notao ( )2 ou ( )10 para especicar a base em que estamos
escrevendo. (Quando no houver indicao, car entendido que o nmero est na
base decimal.)

4.1.1 Uma base genrica


Podemos generalizar esse raciocnio para qualquer base de numerao b; outras bases comu-
mente usadas em computao so a octal (b = 8) e hexadecimal (b = 16). Se um nmero
representado como uma sequncia de n + 1 dgitos, an an1 a1 a0 , seu valor dado
por
n
an bn + an1 bn1 + + a1 b + a0 = ak bk
k=0

Para que a representao de um nmero seja nica, necessrio que os valores de todos os
seus algarismos sejam menores do que b, ou seja, no mximo b1. Caso contrrio, o nmero
b2 , por exemplo, poderia ser representado pelas duas sequncias (b, 0) e (1, 0, 0). Os valores
dos dgitos tambm no podem ser negativos seno surgiria mais uma ambiguidade na
representao.
importante nesse ponto no confundir os valores dos dgitos com as suas representa-
es (que costumam ser os algarismos arbicos sempre que possvel). Aqui, os aj simbo-
lizam os valores dos dgitos (como objetos matemticos abstratos), independentemente de
sua representao. Quando a base no ultrapassa 10, os valores confundem-se com suas re-
presentaes (usamos os algarismos de 0 a b 1 para representar os valores de 0 a b 1).
Quando a base maior que 10, precisamos de outra conveno para a base hexadecimal,
por exemplo, os dgitos com valores de 10 a 15 so representados pelas seis primeiras letras
do alfabeto, enquanto os dgitos at 9 continuam sendo representados da maneira usual.
A frmula acima nos permite, dados os valores dos dgitos de um nmero, achar o valor
do nmero. Podemos tambm querer realizar o processo inverso: dado um nmero, achar
O armazenamento dos dados 47

os valores dos seus dgitos na representao em base b. Olhando para a frmula acima,
podemos ver que, ao dividir (com resto) o valor do nmero por b, obtemos como resto o
valor de a0 ; o quociente o novo nmero

an bn1 + an1 bn2 + + a1

E, ao dividi-lo por b, obteremos como resto o prximo dgito, a1 (que pode muito bem ser
zero). No difcil concluir que, se realizarmos n + 1 divises por b, os restos sero, na
ordem, os dgitos
(a0 , a1 , a2 , . . . , an1 , an ).
Ou seja, dividindo repetidamente por b, obteremos os dgitos da direita para a esquerda.
Note que, na ltima diviso, obtemos an como resto e zero como quociente.
Para ilustrar esse algoritmo, vamos fazer uma funo que recebe um nmero inteiro e
imprime seus dgitos na representao binria.

void imprime_binario(int numero)


{
while (numero > 0) {
printf("%d", numero % 2);
numero /= 2;
}
printf("\n");
}

Notemos, primeiro, que essa funo fornece os dgitos na ordem inversa (conforme foi
observado acima); por exemplo, o nmero 12 = (1100)2 seria impresso como 0011.
Veja tambm que, em vez de xar o nmero de divises, estabelecemos como critrio
de parada o anulamento do quociente. Com isso, no precisamos saber de antemo quantos
dgitos binrios tem o nmero; vimos que o quociente ser zero quando s sobrar o dgito
mais signicativo.

4.2 O armazenamento dos dados


Ao armazenar um nmero na memria do computador, precisamos estabelecer um tamanho
padronizado (uma espcie de nmero de algarismos) que um nmero ocupar. Por que isso?
Imagine que a memria do computador uma enorme aglomerao de rodinhas numeradas.
Se no tivesse um tamanho padro, como saberamos onde comea e onde termina cada
nmero? Precisaramos de algum outro nmero para saber onde cada nmero comearia, e
essa histria no terminaria nunca.
Num computador, a menor unidade possvel de informao o bit; mas a menor unidade
de informao que pode ser de fato acessada o byte, que, nos microcomputadores moder-
nos, equivale a 8 bits. No entanto, um byte sozinho no serve para muita coisa; nmeros
inteiros costumam ser armazenados em espaos de 1, 2, 4 ou 8 bytes (ou seja, 8, 16, 32 ou
64 bits, respectivamente).
Apesar de o tamanho de um inteiro ser padronizado, ainda possvel que existam intei-
ros de mais de um tamanho. Por exemplo, em C existem pelo menos trs tipos inteiros, de
tamanhos diferentes. O que ocorre que cada processador trabalha naturalmente com um
certo tamanho de inteiro (usualmente, 32 ou 64 bits) todos os seus espaos internos de
48 Captulo 4. Mais sobre nmeros

armazenamento (eles se chamam registradores) tm esse mesmo tamanho. Quando quere-


mos usar inteiros de tamanhos diferentes, o processador simplesmente trabalha com pedaos
desses tais registradores ou com vrios registradores juntos.
Agora voltemos um pouco ao C. J conhecemos os dois tipos bsicos de inteiros: int e,
embora no o tenhamos usado ainda, char (como vimos no comeo do primeiro captulo, os
caracteres so armazenados como se fossem inteiros). O tipo char tem um tamanho nico:
um byte ou seja, 8 bits. O tipo int geralmente tem um tamanho padro de 32 bits, mas
ele tambm possui alguns subtipos com tamanhos diferentes. So eles:

short int, um inteiro curto, que costuma ter 16 bits;


long int, um inteiro longo, que costuma ter 32 bits (no h nada de errado aqui; o
tipo int realmente costuma ser igual ao long).
long long int, que geralmente tem 64 bits. (Somente no padro C99.)

Nota: No padro da linguagem C, os tamanhos desses tipos de dados so denidos de uma maneira
mais formal (e mais vaga), devido grande diferena entre os vrios tipos de sistemas em que o C
pode ser usado. Eu os deni de uma maneira mais prtica, segundo os tamanhos que esses tipos
tm na maioria dos computadores pessoais de hoje em dia; uma denio mais correta pode ser
encontrada na especicao da linguagem.

Todos esses subtipos podem (e costumam) ser abreviados, tirando deles a palavra int.
Assim, short int costuma ser escrito como simplesmente short (e assim por diante).
Quo grandes so esses tamanhos? Que valores cabem num inteiro de 32 ou 16 bits
(ou de qualquer outro nmero n)? Para isso, podemos pensar no menor e no maior nmero
que pode ser representado com n bits. O resultado anlogo ao caso decimal: se temos k
algarismos, o maior nmero que podemos representar uma sequncia de k algarismos 9,
que igual a 10k 1. Da mesma maneira, com n bits, o maior nmero representvel (em
binrio) 2n 1, que corresponde a uma sequncia de n algarismos 1.
Assim, com 32 bits, por exemplo, podemos representar todos os nmeros de 0 a 232 1,
que vale 4 294 967 295. Na tabela a seguir voc pode ver as capacidades dos tamanhos de
inteiros mais comuns.

Tabela 4.1: Os tamanhos mais comuns de inteiros nos microcomputadores atuais, e os maiores n-
meros armazenveis com tais tamanhos.
Tipo Bits Bytes Maior nmero
char 8 1 255
short 16 2 65 535
int, long 32 4 4 294 967 295
long long 64 8 18 446 744 073 709 551 615

4.2.1 Nmeros negativos


Talvez no meio disso tudo voc tenha se perguntado se existe alguma maneira de escrever
nmeros negativos no sistema binrio. Numa escrita humana, o natural seria simplesmente

Voc tambm pode chegar a esses nmeros pensando em termos das nossas somas ak bk , com todos os
ak = b 1. Lembre da frmula de soma de uma progresso geomtrica e verique que, de fato, o resultado o
mesmo.
O armazenamento dos dados 49

escrever o sinal de menos para os nmeros negativos. Nos computadores, a codicao mais
comum de nmeros negativos tem como princpio reservar um dos bits do nmero para o sinal
assim, um nmero de 32 bits ca com 31 bits para guardar o mdulo do nmero e 1 bit para
guardar o sinal. Geralmente usado o bit mais signicativo (o que estiver mais esquerda na
nossa representao usual), e ele vale 1 para nmeros negativos ou 0 para nmeros positivos.
Sabendo isso, talvez voc poderia imaginar que, por exemplo, se o nmero 14 armazenado
em 8 bits como 0000 1110, o nmero 14 seria armazenado como 1000 1110. No bem
assim.
O que acontece que essa representao no muito eciente quando o computador
vai fazer clculos. Existe uma outra representao, conhecida como representao do com-
plemento de 2, que permite que nmeros negativos sejam operados exatamente da mesma
maneira que os nmeros positivos, isto , sem que o computador precise vericar se um
nmero negativo para saber como fazer a conta.
Complemento de 2 no o melhor nome (seria mais adequado complemento de 2n );
mas o mtodo consiste em guardar cada nmero negativo k como se fosse o nmero positivo
2n k, sendo n o nmero de bits reservado para o inteiro. Por exemplo, ao trabalhar com
8 bits, o nmero 14 seria guardado como se fosse 256 14 = 242 (em binrio, isso seria
1111 0010).
Agora, se olharmos para o nmero 128, veremos que ele o seu prprio complementar
quando usamos n = 8 bits: 256 128 = 128. Mas lembre-se que a representao binria
de 128 1000 0000 como o bit de sinal 1, faz muito mais sentido convencionar que
essa ser a representao do nmero negativo 128. Ento, nessa representao, o 128 no
existe (precisaramos usar mais que 8 bits); os nmeros positivos param no 127. Os nmeros
negativos vo at o 128.
Generalizando essas concluses, num espao de n bits possvel guardar, com sinal,
todos os nmeros inteiros de 2n1 at 2n1 1. (Veja como isso equivalente ao nosso
intervalo de 128 a 127 para a representao de 8 bits.)
Um algoritmo prtico para calcular o complemento de 2 de um nmero, j na represen-
tao binria, o seguinte: preencha com zeros esquerda para completar a quantidade de
bits que est sendo usada; ento inverta todos os bits da representao (trocar os 0 por 1 e
vice-versa) e some 1. Lembre-se de inverter tambm os zeros esquerda que voc incluiu.
Em C possvel armazenar tanto nmeros negativos quanto positivos, como j mencio-
namos no comeo. Mais ainda, ao declarar uma varivel voc pode escolher se quer guardar
nmeros negativos e positivos (com sinal) ou s positivos (sem sinal) como acabamos de
ver, isso faz diferena no intervalo de nmeros que pode ser guardado. Assim, uma varivel
de 8 bits pode suportar nmeros de 0 a 255 ou nmeros de 128 a 127. Para indicar isso,
voc pode modicar os tipos numricos inteiros (char e int) com mais dois adjetivos: sig-
ned (com sinal) e unsigned (sem sinal). Quando voc no diz nada, o computador assume
que voc quer guardar os nmeros com sinal (signed).

Se voc quiser vericar por que os dois mtodos so equivalentes, pense na soma de um nmero com o que
tem todos os seus bits invertidos.
50 Captulo 4. Mais sobre nmeros

Tabela 4.2: As faixas de nmeros armazenveis nos tipos inteiros com sinal.
Tipo Regio
char (8 bits) 128 a 127
short (16 bits) 32 768 a 32 767
int, long (32 bits) 2 147 483 648 a 2 147 483 647
long long (64 bits) 9 223 372 036 854 775 808 a 9 223 372 036 854 775 807

4.3 Nmeros reais


possvel fazer muita coisa s com nmeros inteiros, mas em muitas situaes somos obri-
gados a usar nmeros fracionrios ou reais (nmeros com vrgula). Em C, os nmeros reais
esto encarnados em dois tipos de dados: oat e double. Os dois funcionam mais ou menos
do mesmo jeito; a diferena entre eles a preciso de cada um dizemos que o oat tem
preciso simples e o double tem preciso dupla. Na maioria das linguagens de programao
(inclusive C), esses tipos de nmeros so conhecidos como nmeros de ponto utuante
mais adiante veremos o porqu desse nome.
Falamos em preciso porque, assim como no caso dos inteiros, necessrio impor um
tamanho para cada nmero, o que limita a representao dos nmeros veremos mais
adiante como exatamente isso se d.
Antes de entrar na parte mais terica, vamos ver como se usam os tais nmeros de ponto
utuante. A declarao de variveis funciona da mesma maneira que para os inteiros:

double x, y;
float z, w;

Para escrever nmeros fracionrios em C, usamos o ponto (.) para separar a parte inteira
da fracionria por exemplo, 3.14159 ou -0.001. Podemos tambm usar uma espcie de
notao cientca para trabalhar com nmeros muito grandes ou muito pequenos escreve-
mos o valor principal do nmero da maneira que acabei de descrever, e usamos a letra e ou
E para indicar a potncia de 10 pela qual o nmero deve ser multiplicado. Alguns exemplos:
( ) ( ) ( )
3.14159e-7 3,14159 107 1.234E+26 1,234 1026 4.56e5 4,56 105

Veja que no faz diferena se o caractere E maisculo ou minsculo, nem se colocamos o


sinal de + nos expoentes positivos.
To importante quanto saber escrever esses nmeros saber imprimi-los na tela ou l-
los do teclado. Usando a funo printf, usamos o cdigo %f (da mesma maneira que o nosso
conhecido %d), tanto para o double quanto para o oat, como no seguinte exemplo:

#include <stdio.h>

int main()
{
double x = 5.0;
float y = 0.1;
int z = 27;

printf("x = %f\n", x);


Nmeros reais 51

printf("y = %f, z = %d\n", y, z);

return 0;
}

Para ler nmeros reais do teclado, precisamos dizer se a varivel onde vamos guardar
do tipo double ou oat. No primeiro caso, usamos o cdigo %lf; no segundo, apenas %f
(como no printf)

#include <stdio.h>

int main()
{
double x, z;
float y;

printf("Digite dois nmeros de ponto flutuante: ");


scanf("%lf %f", &x, &y);

z = x + y;
printf("%f\n", z);

return 0;
}

Veja que podemos fazer contas entre nmeros de ponto utuante da mesma maneira que
fazamos com os inteiros; podemos at misturar doubles com oats. Mais adiante faremos
algumas observaes sobre isso.

4.3.1 Mais sobre printf


Quando trabalhamos com nmeros de ponto utuante, pode ser til exibir nmeros em no-
tao cientca. Para isso, podemos trocar nosso cdigo %f por %e ou %E: o resultado ser
impresso com a notao que estabelecemos anteriormente, usando um E maisculo ou mi-
nsculo dependendo do que for especicado. Por exemplo,

printf("%e %E\n", 6.0, 0.05);


/* resultado: 6.000000e+00 5.000000E-02 */

Mas o printf tambm tem uma opo muito inteligente, que a %g ela escolhe o melhor
formato entre %f e %e de acordo com a magnitude do nmero. Nmeros inteiros no muito
grandes so impressos sem casas decimais (e sem ponto); nmeros muito grandes ou muito
pequenos so impressos com notao cientca. Tambm podemos usar um G maisculo se
quisermos que o E da notao cientca saia maisculo. Por exemplo:

printf("%g %g %G\n", 6.0, 0.00005, 4000000.0);


/* resultado: 6 5e-05 4E+06 */

Outra coisa que podemos mudar o nmero de casas decimais exibidas aps o ponto. O
padro para o formato %f de 6 casas; isso pode ser demais em vrios casos por exemplo,
52 Captulo 4. Mais sobre nmeros

se voc for imprimir preos de produtos. Para que o nmero de casas decimais exibidas seja
n, trocamos o cdigo %f por

%.nf

Para imprimirmos um nmero com 2 casas, por exemplo, podemos escrever assim:

printf("Preo = %.2f\n", 26.5);


/* resultado: 26.50 */

Se colocarmos n = 0, o ponto decimal tambm ser apagado. Veja que os nmeros


sero sempre arredondados conforme necessrio. Por exemplo:

printf("%.2f %.0f\n", 26.575, 26.575);


/* resultado: 26.58 27 */

4.3.2 A representao
Falamos um pouco acima que tanto o oat quanto o double so tipos de ponto utuante.
Isso diz respeito representao dos nmeros reais na memria do computador trata-se
de uma espcie de notao cientca. Nessa representao, um nmero tem duas partes: a
mantissa, que so os algarismos signicativos, e o expoente, que indica a posio da vrgula
decimal em relao aos algarismos signicativos.
Pensando na base decimal, um nmero como 0,000395 teria a representao 3,95104 ,
cuja mantissa 3,95 e cujo expoente 4 (indica que a vrgula deve ser deslocada 4 dgitos
para a esquerda). Na notao cientca, estabelecemos que a mantissa deve ser maior que
ou igual a 1, mas no pode ter mais de um dgito esquerda da vrgula por exemplo, a
mantissa pode ser 1,00 ou 9,99, mas no 15,07 ou 0,03. Assim, estamos de fato dividindo
a informao do nmero em duas partes: o expoente d a ordem de grandeza do nmero, e
a mantissa d o valor real dentro dessa ordem de grandeza.
Nem todo nmero tem uma representao decimal nita em outras palavras, nem
todo nmero tem uma mantissa nita. Por exemplo, a frao 5/3 pode ser expandida inni-
tamente como 1,6666 . . .; se quisermos express-la com um nmero nito de dgitos (como
necessrio em um computador), devemos parar em algum dgito e arredondar conforme
necessrio por exemplo, 1,6667, se precisarmos trabalhar com 5 dgitos.
Assim, para colocar um nmero nessa representao, necessrio primeiro normalizar
a mantissa para que que entre 1 (inclusive) e 10, e depois arredond-la para um nmero de
dgitos pr-estabelecido.
A representao usual de ponto utuante no computador praticamente igual a essa;
antes de ver como ela realmente funciona, vamos ver como funciona a representao dos
nmeros fracionrios na base binria.
O que a representao decimal de um nmero? Considere um nmero que pode ser
escrito como an an1 a1 a0 ,b1 b2 (veja que a parte inteira, esquerda da vrgula,
tem um nmero nito de dgitos; a parte fracionria, direita da vrgula, pode ter innitos
dgitos). J vimos como funciona a representao da parte inteira; falta analisarmos a parte
fracionria.
Vejamos primeiro o caso de um nmero cuja parte fracionria nita e tem m dgitos
no vamos nos importar com a parte inteira; suponha que zero. Esse nmero pode ser
escrito como 0,b1 b2 bm . Ento, se multiplicarmos esse nmero por 10m , ele voltar
Nmeros reais 53

a ser inteiro, e ser escrito como b1 b2 bm . J sabemos como funciona a representao


inteira de um nmero! Esse nmero vale
m
b1 10m1 + b2 10m2 + + bm1 101 + bm 100 = bk 10mk
k=1
m
Como esse o nmero original multiplicado por 10 , o nmero original vale

m
b1 101 + b2 102 + + bm1 10(m1) + bm 10m = bk 10k
k=1

No difcil estender essas contas para os casos em que a parte fracionria no nita
(ou em que a parte inteira no zero), mas seria muito tedioso reproduzi-las aqui; deix-las-
ei como exerccio se voc quiser pensar um pouco, e direi apenas que aquela representao
(possivelmente innita) an an1 a1 a0 ,b1 b2 corresponde ao nmero

n

k
ak 10 + bk 10k
k=0 k=1

Como no caso dos inteiros, claro que todos os dgitos ak e bk s podem assumir os va-
lores {0, 1, . . . , 9}, para que a representao de cada nmero seja nica. (Na verdade isso
no suciente para que a representao seja nica; precisamos, para isso, exigir que haja
um nmero innito de dgitos diferentes de 9, evitando assim as representaes do tipo
0,999999 . . . 1.)
Veja que essa representao uma extenso da representao dos inteiros; poderamos
trocar os bs por as com ndices negativos (bk = ak ) e estender a somatria para os ndices
negativos:
n
ak 10k
k=
Com isso, a transio para a base binria est muito bem encaminhada. O que mudar na
base binria que os dgitos so multiplicados por potncias de 2 (o dgito ak multiplicado
por 2k ), e s podem assumir os valores 0 e 1.
Por exemplo, o nmero 3/8 = 1/8 + 1/4 = 22 + 23 poder ser representado como
(0,011)2 . E o nmero 1/10, que aparentemente muito simples? Na base 2, ele no tem
uma representao nita! fcil ver o porqu: se ele tivesse representao nita, bastaria
multiplicar por uma potncia de 2 para torn-lo inteiro; sabemos que isso no verdade
(nenhuma potncia de 2 divisvel por 10!).
Como descobrir a representao binria de 1/10? Vou falar sobre dois jeitos de fazer
isso. O primeiro envolve alguns truquezinhos algbricos. Vamos escrever
1 1 1 1 3 3 1 3 1
= = = 4 = 24
10 2 5 2 15 2 2 1 2 1 24
Agora a ltima frao a soma da srie geomtrica de razo 24 . Podemos ento escrever
1 ( )
= 3 25 1 + 24 + 28 +
10 ( )
= (1 + 2) 25 1 + 24 + 28 +
( ) ( )
= 24 + 25 1 + 24 + 28 +
1 ( ) ( )
= 24 + 28 + 212 + + 25 + 29 + 213 +
10
54 Captulo 4. Mais sobre nmeros

Assim, identicando esse resultado com a frmula da representao de um nmero, temos


bk = 1 se k = 4, 8, 12, . . . ou se k = 5, 9, 13, . . . , e bk = 0 caso contrrio. Ou seja,
os dgitos nas posies 4, 5, 8, 9, 12, 13, . . . so 1, e os outros so 0. Logo, a representao
binria de 1/10
1
= (0,0 0011 0011 0011 . . .)2
10
A partir dessas contas, podemos ver alguns padres interessantes (em analogia com a base
10). Um deles que multiplicar e dividir por 2 tem, na base binria, o mesmo efeito que
tinham as multiplicaes/divises por 10 na base decimal. Ou seja, ao multiplicar por 2 um
nmero, a vrgula vai uma posio para a direita na representao binria (ou acrescenta-se
um zero caso o nmero seja inteiro).
Outro padro que fraes do tipo 2na1 , em que a < 2n 1 um inteiro, so timas
geradoras de dzimas peridicas (binrias): podemos escrev-las como

a a 1 a ( )
= n n
= n 1 + 2n + 22n +
2n 1 2 12 2
Como a representao de a tem no mximo n dgitos, e a srie geomtrica direita uma
dzima de perodo n, a representao da nossa frao ser uma dzima cujo perodo a
representao de a (com zeros esquerda para completar os n dgitos se necessrio).
O outro mtodo de achar a representao decimal de uma frao voc j conhece:
o algoritmo da diviso. Para aplic-lo ao caso das fraes, basta escrever o denominador
e o numerador em binrio e fazer o processo de diviso, lembrando que as regras do jogo
mudam um pouco por exemplo, voc s pode multiplicar o divisor por 0 ou 1. No vou
ensinar os detalhes aqui no o propsito deste livro. Mas basicamente este o algoritmo
usado pelo computador para converter nmeros fracionrios para a representao binria;
assim que ele pode controlar quantos dgitos sero mantidos na representao basta parar
o algoritmo na casa desejada.

Fizemos uma longa digresso sobre representao binria de nmeros. Para que isso ser-
viu? Agora poderemos entender melhor como funciona a representao de ponto utuante.
Como dissemos acima, na representao de ponto utuante um nmero tem duas partes: a
mantissa e o expoente. Para a mantissa M devemos impor uma condio de normalizao
como no caso da notao cientca decimal: devemos ter 1 M < 2 para que esquerda
da vrgula haja um e apenas um dgito (no-nulo). Assim, uma frao como 21/4 seria
normalizada da seguinte maneira:

21 21 (10101)2
= 4= 22 = (1,0101)2 22
4 16 24
Devemos considerar que a mantissa deve ser representada com um tamanho xo (e nito)
de dgitos. No caso da preciso simples do oat, esse nmero costuma ser de 24 dgitos.
Assim, muitas fraes precisam ser arredondadas tanto as que tm representaes innitas
quanto as que tm representaes nitas porm muito longas. Por exemplo, a frao 1/10
(representao innita) seria arredondada para

(1,100 1100 1100 1100 1100 1101)2 24 = 0,100000001490116119384765625

Agora vamos chegar mais perto da representao que realmente usada pelo computador.
Um nmero de ponto utuante representado com a seguinte estrutura:
Nmeros reais 55

. . . .
sinal expoente mantissa
31 30 23 22 0
63 62 52 51 0

Figura 4.1: Esboo da estrutura de representao de ponto utuante. Em cada parte foram indicados
os nmeros dos bits utilizados nas representaes de preciso simples e dupla, respectivamente.

Eu ainda no havia falado do sinal: a representao de ponto utuante usa um bit para o
sinal, assim como na representao de inteiros. Mas, ao contrrio desta, em ponto utuante
no se usa nada parecido com o complemento de 2; a nica diferena entre as representa-
es de dois nmeros de mesma magnitude e sinais opostos o bit de sinal.
O expoente (relacionado a uma potncia de 2, no de 10!) representado (quase) como
um nmero inteiro comum, e por isso h uma limitao nos valores que ele pode assumir.
Na preciso simples, so reservados 8 bits para o expoente, o que permite at 256 valores
possveis no so exatamente de 128 a 127; na verdade, os dois extremos so reservados
para situaes especiais; os expoentes representveis so de 127 at 126. Na preciso dupla,
usam-se 11 bits, e a gama de expoentes muito maior: de 1023 at 1022.
Sobre a mantissa no resta muito a falar; os bits da mantissa so armazenados sequenci-
almente, como na representao binria que construmos acima. Em preciso simples, so
reservados 23 bits para a mantissa. O leitor atento notar que eu falei em 24 dgitos al-
guns pargrafos atrs. De fato, a representao binria s reserva 23 dgitos. Mas, como a
mantissa est sempre entre 1 e 2, esquerda da vrgula temos sempre um algarismo 1 sozi-
nho; ento, convenciona-se que esse algarismo no ser escrito (o que nos deixa com 1 bit
a mais!), e assim temos uma mantissa de 24 dgitos em um espao de apenas 23 bits. Na
preciso dupla, so reservados 52 bits (que na verdade representam 53).
Na verdade, h vrios outros detalhes sobre a representao de ponto utuante; poderia
gastar mais algumas pginas com essa descrio, mas no vem ao caso. Meu objetivo era dar
uma idia geral do funcionamento dessa representao.

At agora, s explorei a parte terica dessa representao. Precisamos conhecer tambm


as caractersticas prticas da representao de ponto utuante. Por exemplo, em termos
da representao decimal, como se traduzem os limites de representao do expoente e da
mantissa? No vou fazer contas aqui, vou apenas mostrar os resultados prticos. Descreverei
primeiro que resultados so esses:

Devido aos limites de expoentes, existe um nmero mximo e um nmero mnimo que
podem ser representados com uma dada preciso. O nmero mximo corresponde ao
maior expoente possvel com a maior mantissa possvel; o nmero mnimo, analoga-
mente, corresponde ao menor expoente possvel com a menor mantissa possvel. Na
tabela, so apresentadas as faixas aproximadas.
Os mesmos limites se aplicam para os mdulos dos nmeros negativos, j que nme-
ros negativos e positivos so tratados de maneira simtrica na representao de ponto
utuante.
Como vrios nmeros precisam ser arredondados para serem representados, a repre-
sentao tem um certo nmero de algarismos signicativos de preciso quantas
casas decimais esto corretas na representao de um nmero. Como os nmeros no
so representados em base 10, a quantidade de casas corretas pode variar de nmero
para nmero; os valores apresentados na tabela seguir correspondem quantidade m-
nima.
56 Captulo 4. Mais sobre nmeros

Tabela 4.3: Comparao dos diferentes tipos de nmero de ponto utuante em C


Tipo Expoente Mantissa Intervalo Preciso
oat (32 bits) 8 bits 23 bits 10 45
10 38
6 decimais
double (64 bits) 11 bits 52 bits 10324 10308 15 decimais
long double (80 bits) 15 bits 64 bits 104950 104932 19 decimais

Nota: A representao de ponto utuante pode no ser a mesma em todos os computadores. As


informaes aqui descritas esto de acordo com a representao conhecida como IEEE 754, que
a mais usada nos computadores pessoais hoje em dia.

O tipo long double ainda no tinha sido mencionado. Ele um tipo que, em geral, tem
preciso maior que o double (embora certos compiladores no sigam essa norma), mas seu
tamanho no to padronizado como os outros dois tipos (preciso simples e dupla). Alm
de 80 bits, possvel encontrar tamanhos de 96 ou 128 bits.
De maneira similar ao double, necessrio utilizar o cdigo %Lf em vez de apenas %f
quando queremos ler com scanf uma varivel long double. Nesse caso tambm necessrio
utilizar esse mesmo cdigo para o printf (em contraste com o double que continua usando
o cdigo %f).

4.3.3 Problemas e limitaes


Como j vimos, os tipos de ponto utuante no conseguem representar com exatido todos
os nmeros fracionrios. Devido aos arredondamentos, acontecem alguns problemas que,
segundo a matemtica que conhecemos, so inesperados. Contas que, matematicamente,
do o mesmo resultado, podem ter resultados diferentes. At uma simples troca de ordem
das operaes pode alterar o resultado.
Vamos ilustrar isso com um exemplo. Pediremos ao usurio que digite os coecientes
reais de uma equao quadrtica

ax2 + bx + c = 0,

e analisaremos os trs tipos de soluo que podem ocorrer dependendo do valor de =


b2 4ac (ainda no vimos como calcular raiz quadrada, ento vamos apenas analisar os
casos):

Se > 0, a equao tem duas razes reais distintas.


Se = 0, a equao tem uma raiz real dupla.
Se < 0, a equao no tem razes reais.

O programa caria assim:

#include <stdio.h>

int main()
{
float a, b, c, delta;
printf("D os coeficientes da equao ax^2 + bx + c = 0: ");
scanf("%f %f %f", &a, &b, &c);
Nmeros reais 57

delta = b*b - 4*a*c;


printf("Delta = %g\n", delta);

if (delta > 0)
printf("A equao tem duas razes reais distintas.\n");
else if (delta == 0)
printf("A equao tem uma raiz real dupla.\n");
else
printf("A equao no tem razes reais.\n");

return 0;
}

Esse programa, apesar de estar logicamente correto, apresenta um problema. Considere


as equaes que tm uma raiz dupla , ou seja, equaes do tipo a(x )2 : para elas,
exatamente igual a 0. No entanto, se voc testar o programa para vrias dessas equaes,
descobrir um comportamento estranho: para muitos valores de , o valor de calculado
pelo programa no igual a zero. Por exemplo, para valores no inteiros e prximos de 1,
obtemos um da ordem de 107 .
Por que isso acontece? O problema so os arredondamentos que j vimos serem necess-
rios para representar a maioria dos nmeros; por conta deles, nossa conta no d exatamente
zero no nal.
Neste momento, considere isso como um alerta evite fazer comparaes de igualdade
com nmeros de ponto utuante; um pouco mais adiante, veremos o que possvel fazer
para contornar (ou conviver melhor com) essas limitaes.

4.3.4 Ponto utuante vs. inteiros


As operaes entre nmeros de ponto utuante funcionam basicamente da mesma maneira
que operaes entre inteiros; voc pode at realizar operaes mistas entre nmeros inteiros
e de ponto utuante o inteiro convertido automaticamente para ponto utuante e a
operao realizada como se os dois nmeros fossem de ponto utuante. Em geral, sempre
que voc fornece um inteiro num lugar onde era esperado um nmero de ponto utuante, o
inteiro convertido para ponto utuante.
Por outro lado, a coerncia matemtica da linguagem exige que o resultado de uma ope-
rao entre dois inteiros seja um inteiro. Agora lembremos que a diviso entre dois nmeros
inteiros nem sempre exata, e da surgem os nmeros racionais a diviso entre dois
nmeros inteiros deve ser, em geral, um nmero racional. Como resolver esse conito?
Em C, a primeira regra (fechamento das operaes entre inteiros) a que prevalece:
quando dividimos dois inteiros, obtemos apenas o quociente da diviso inteira entre eles.
Para obter o resultado racional da diviso entre dois inteiros, precisamos converter um deles
em ponto utuante.
Suponha que voc deseja imprimir a expanso decimal da frao 1/7. Uma possvel
primeira tentativa seria a seguinte:

float x;
x = 1/7;
printf("1/7 = %f\n", x);
58 Captulo 4. Mais sobre nmeros

Para sua frustrao, voc obteria o nmero 0.000000 como resultado. Claro, se voc fez
uma diviso entre inteiros, o resultado foi a diviso inteira entre eles; ao guardar o resultado
numa varivel de ponto utuante, apenas o resultado convertido quem decide que tipo
de operao ser realizada so os operandos em si.
Para fazer o que queramos, pelo menos um dos operandos dever ser de ponto utuante.
Ou seja, devemos escolher uma das alternativas:
x = 1.0 / 7;
x = 1 / 7.0;
x = 1.0 / 7.0;

Mas e se quisssemos calcular a diviso entre dois nmeros que no conhecemos a priori?
Devemos usar a

4.3.5 Converso explcita de tipos (casting)


Muitas converses de tipo so feitas automaticamente em C, mas em alguns casos, como na
motivao que antecede esta seo, preciso fazer uma converso forada. Esse tipo de
converso explcita tambm conhecido pelo nome tcnico (em ingls) de casting. Ela feita
da seguinte maneira:

(novo_tipo) varivel_ou_valor
Por exemplo, para calcular a razo (fracionria) entre dois inteiros fornecidos pelo usu-
rio, poderamos converter um deles para o tipo oat e realizar a diviso:
int a, b;
float x;
/* (...) leitura dos nmeros */
x = (float)a / b;
printf("%f\n", x);

Note, porm, que o operador de converso de tipos s atua sobre a varivel que vem
imediatamente depois dele por exemplo, se quisssemos converter uma expresso inteira,
precisaramos de parnteses:
x = (float)(a / b) / c;

Nesse caso, seria realizada a diviso inteira entre a e b, e depois seu quociente seria dividido
por c, numa diviso racional.

4.4 Funes matemticas


Em diversas reas, as quatro operaes bsicas no cobrem todas as contas que precisam ser
feitas em um programa. muito comum, mesmo que no estejamos lidando diretamente
com matemtica, precisar das funes trigonomtricas, raiz quadrada, exponencial, entre
outras. Todas essas funes de uso comum constam no padro da linguagem C e fazem
parte da biblioteca matemtica padro. Para us-las em um programa, precisamos de mais
uma instruo no comeo do arquivo de cdigo:
#include <math.h>
Funes matemticas 59

Ao compilar com o GCC, tambm necessrio incluir a biblioteca matemtica na linha


de comando da compilao; isso feito com a opo -lm. (Na verso do MinGW para
Windows, devido diferena de organizao do sistema, isso no necessrio.)
Uma lista completa das funes disponveis nesse arquivo pode ser facilmente encontrada
nas referncias sobre a biblioteca padro; na tabela 4.4 so listadas apenas as principais. A
maioria das funes aqui listadas recebe apenas um parmetro, exatamente como esperado;
casos excepcionais sero indicados.

Tabela 4.4: Principais funes matemticas da biblioteca padro

Funo Signicado
sin, cos, tan Funes trigonomtricas: seno, cosseno e tangente. Os ngulos
so sempre expressos em radianos.
asin, acos, atan Funes trigonomtricas
[ inversas.
] asin e atan devolvem um n-
gulo no intervalo 2 , 2 ; acos devolve um ngulo no intervalo
[0, ].
sinh, cosh, tanh Funes hiperblicas (seno, cosseno e tangente)
sqrt Raiz quadrada (square root)
exp Funo exponencial (ex )
log Logaritmo natural, base e (ln)
log10 Logaritmo na base 10
abs, fabs Mdulo (valor absoluto) de um nmero. Use abs para inteiros e
fabs para nmeros de ponto utuante.
pow(x, y) Potenciao: xy (x e y podem ser nmeros de ponto utuante)

4.4.1 Clculo de sries


Vrias das funes implementadas na biblioteca matemtica podem ser calculadas por meio
de expanses em srie (somatrias innitas); por exemplo, a funo exponencial:

xk x2 x3 x4
ex = =1+x+ + + +
k! 2 3! 4!
k=0

claro que, em um computador, impossvel calcular todos os termos dessa srie para
chegar ao resultado; aps um certo ponto, devido convergncia das sries, o valor de cada
termo muito pequeno, de modo que, frente preciso do computador, som-los no faz
mais diferena. Dessa maneira, devemos somar um nmero nito N de termos dessa srie.
Mas como escolher N? Seguem alguns dos critrios possveis:

Escolher um N xo. Esse critrio no muito bom, porque a preciso at o N-simo


termo costuma depender do argumento x; em vrios casos, para valores grandes de
x necessrio somar um nmero maior de parcelas, enquanto, para x bem pequeno,
poucas parcelas j do uma aproximao muito boa.
Limitar a magnitude das parcelas a serem somadas interromper a soma quando o
mdulo da parcela for menor que um dado (por exemplo, = 109 ). Esse critrio
melhor que o anterior, mas no leva em conta a magnitude do resultado; por exemplo,
se o valor da funo for da ordem de 109 , uma parcela de 109 realmente no faz
60 Captulo 4. Mais sobre nmeros

diferena; se o valor da funo for da ordem de 106 , a mesma parcela de 109 ainda
signicativa.

Podemos modicar um pouco esse critrio levando isso em conta: em vez de limitar
o tamanho de cada parcela, limitaremos o tamanho da parcela dividido pelo valor
acumulado at ento. Se dividirmos pela soma do passo anterior, que no inclui essa
parcela, obtemos a quantidade conhecida como variao relativa se Sn indica a
soma das primeiras n parcelas, a (n + 1)-sima parcela corresponde a Sn+1 Sn , e
ento o critrio de limitao na variao relativa traduz-se matematicamente em

Sn+1 Sn
< ,
Sn

parando no passo n + 1 se essa condio for satisfeita. Esse critrio bastante usado
em diversos mtodos numricos.

Devido preciso do computador, em algum momento as parcelas caro to pe-


quenas que absolutamente no iro mais alterar a soma por exemplo, se estamos
trabalhando com 5 algarismos signicativos, somar 0,0001 no nmero 100 no faz
a menor diferena. Assim, podemos comparar a soma de cada passo com a soma
anterior, e interromper o processo quando as somas forem iguais.
No entanto, necessrio tomar cuidado com possveis otimizaes do compilador
como voc somou uma parcela no-nula, logicamente a soma deve ter-se alterado;
portanto, com certas opes de otimizao ativadas, o compilador adianta a resposta
da comparao entre as duas somas e diz que elas so diferentes, sem vericar se a
variao realizada estava realmente dentro da preciso do ponto utuante. O resultado
disso um lao innito.

4.4.2 De nindo constantes


A linguagem C permite que voc crie apelidos para constantes que voc usa no seu pro-
grama. Isso ajuda muito a manter o cdigo organizado, pois evita que o cdigo que cheio
de nmeros annimos difceis de entender. Alm disso, quando utilizamos uma constante
pelo nome, ganhamos uma grande exibilidade: se precisamos alterar o valor dela, podemos
alterar simplesmente a sua denio, e a alterao reetir-se- em todos os lugares onde a
constante foi utilizada.
Para denir constantes, utilizamos o comando #define, da seguinte maneira:

#de ne CONSTANTE valor


As restries sobre os nomes das constantes so as mesmas que para nomes de variveis e
funes. Para utilizar essas constantes,
Um exemplo de aplicao disso: suponha que voc criou um programa que mostra o
cardpio de uma pizzaria, recebe um pedido e calcula o valor total. Agora a pizzaria quer
que voc refaa o programa pois os preos foram reajustados. Se voc tiver escrito os pre-
os diretamente no meio do cdigo do programa, voc ter bastante trabalho procurando
os preos e modicando em diversos lugares. No entanto, se voc denir constantes como
PRECO_MARGHERITA e PRECO_PORTUGUESA e us-las no cdigo, voc s precisar alterar a de-
nio das constantes.
O tipo char 61

#include <stdio.h>

#define PRECO_MUSSARELA 15.00


#define PRECO_MARGHERITA 16.00
#define PRECO_PORTUGUESA 18.50

void imprime_cardapio()
{
printf (
"Mussarela %5.2f\n"
"Margherita %5.2f\n"
"Portuguesa %5.2f\n",
PRECO_MUSSARELA , PRECO_MARGHERITA , PRECO_PORTUGUESA);
}

/* Deixarei como exerccio para voc pensar a parte de registrar um


* pedido. Na verdade , por enquanto voc pode se preocupar apenas em
* calcular o preo total.
* Sugesto: use um terminador para poder saber quando o pedido acaba.
*/

4.5 O tipo char


Voc j conheceu trs dos quatro tipos fundamentais da linguagem C. Falta apenas um, o
tipo char que j mencionamos, mas no tivemos a oportunidade de usar. Como o nome
sugere, ele designado para guardar caracteres (como a Y ^ { @ # %). No entanto, ele
apenas mais um tipo inteiro, com um intervalo de valores permitidos mais modesto que
o de um inteiro comum. Isso ocorre assim porque, para um computador, um caractere
apenas um nmero; caracteres so codicados como nmeros de acordo com uma tabela de
correspondncia por exemplo, a letra A maiscula armazenada como 65, o cifro $ como
36, etc. Esses exemplos so os cdigos da conhecida tabela ASCII, que a base do padro
atual para a codicao de caracteres.
Uma varivel do tipo char pode conter um caractere (apenas um!), e caracteres so re-
presentados entre aspas simples (no confunda com as aspas duplas como as que voc usa
nas funes printf e scanf). Alguns exemplos: 'a', '2', '@'. Um exemplo de varivel do
tipo char seria:

char opcao;
opcao = 'b';

H alguns caracteres que no so representados literalmente, mas por meio de cdigos


especiais. Um deles j lhe foi apresentado, a saber, o caractere de quebra de linha, repre-
sentado pela sequncia '\n'. Alguns outros exemplos so apresentados na tabela 4.5, junto
com os cdigos numricos correspondentes na tabela ASCII.
62 Captulo 4. Mais sobre nmeros

Tabela 4.5: Algumas das sequncias utilizadas para representar caracteres especiais.
Sequncia ASCII Signicado
\t 9 Tabulao. Avana o cursor para posies pr-denidas ao longo
da linha; usualmente, essas posies so denidas de 8 em 8 ca-
racteres a partir da primeira posio da linha.
\n 10 Quebra de linha. Esse caractere comumente conhecido pela sigla
NL, new line, ou por LF, line feed.
\r 13 Retorno de carro (CR, carriage return): retorna o cursor para o
incio da margem esquerda. (Ver observao adiante.)
\" 34 Aspa dupla.
\' 39 Aspa simples.
\\ 92 Barra invertida.
\nnn Permite especicar qualquer caractere pelo seu cdigo em base
octal (de 000 a 377). Cada n um dgito de 0 a 7.
\xnn Idem, em base hexadecimal (de 00 a FF). Cada n um dgito de
0 a 9 ou uma letra de A a F (maiscula ou minscula).

Os caracteres CR e LF (alm de outros que no foram indicados aqui) so uma herana


da poca das mquinas de escrever. Para comear a escrever texto na linha seguinte, eram
necessrias duas aes: retroceder o carro de impresso para o comeo da margem (CR)
e alimentar mais uma linha de papel (LF). Em sistemas Windows, o nal de uma linha de
texto usualmente indicado pela sequncia CR + LF, ao invs de simplesmente LF, como
de praxe nos sistemas Unix (enquadram-se aqui o Linux e o Mac OS X). Nos sistemas Mac
antigos usava-se apenas CR, sem LF.
Veja que os caracteres " ' \ precisam de uma representao alternativa: as duas aspas
porque podemos querer represent-las como caracteres sem que sejam interpretadas como
o nal da sequncia de caracteres; a barra invertida porque ela prpria usada em outras
sequncias de caracteres especiais.

4.5.1 Entrada e sada


Para ler e imprimir variveis do tipo char, voc pode usar o cdigo de formato %c nas funes
printf e scanf. Por exemplo:

char opcao;
scanf("%c", &opcao);
printf("Voc escolheu a opo (%c)!\n", opcao);

int opcao;
opcao = getchar();
putchar('a');

importante notar que, tanto com a funo scanf quanto com a getchar, os caracteres
digitados no so recebidos pelo programa em tempo real. Cada vez que voc digita um
caractere, ele armazenado temporariamente numa regio da memria chamada buer de
teclado, e s aps o nal da linha que o contedo do buer liberado para o nosso programa,
atravs dessas funes.
Ponteiros e vetores
5
5.1 Prolegmenos
Num computador, cada varivel guardada em uma certa posio da memria. Essas posi-
es de memria so numeradas, de modo que cada uma tem um endereo numrico
como se cada uma fosse uma casa em uma rua.
Em C, um ponteiro (tambm chamado de apontador) uma varivel que guarda uma
referncia a outra varivel seu valor o endereo de uma outra varivel. Se o valor de um
ponteiro p o endereo de uma varivel X, ento dizemos que p aponta para X. Veja uma
ilustrao disso na gura 5.1. Se um ponteiro aponta para uma varivel, voc pode acessar
essa varivel (ler ou alterar seu valor) atravs do ponteiro logo veremos como.

Figura 5.1: Esquema do funcionamento de um ponteiro. O ponteiro p contm o endereo da varivel


n (1032), ou seja, p aponta para n.

Isso pode parecer apenas uma maneira de complicar as coisas, mas na realidade tem
diversas utilidades, das quais citamos algumas:

Quando precisamos transmitir uma grande quantidade de dados a outra parte do pro-
grama, podemos passar apenas um ponteiro para esses dados em vez de fazer uma
cpia dos dados e transmitir a cpia. Isso economiza tempo o processo de duplicar
os dados gasta tempo de processamento e, obviamente, espao na memria.

Uma funo em C s pode devolver um valor com a instruo return. No entanto,


se uma funo recebe como parmetros ponteiros para outras variveis, voc poder
gravar valores nessas variveis, e com isso uma funo pode gerar vrios valores de
sada.

63
64 Captulo 5. Ponteiros e vetores

O conceito de ponteiros pode ser estendido para funes possvel passar um pon-
teiro para uma funo como parmetro. Por exemplo, podemos criar uma funo
chamada acha_raiz com um algoritmo numrico que acha razes de uma funo ma-
temtica f; a funo f poderia ser passada (na forma de ponteiro) como parmetro da
funo acha_raiz.

5.1.1 Declarao de ponteiros


Em C, para declarar uma varivel que funciona como ponteiro, colocamos um asterisco (*)
antes do seu nome. Um ponteiro s pode apontar para um tipo de varivel, j que a maneira
de armazenar o valor de uma varivel na memria depende do seu tipo. Por exemplo, um
ponteiro que aponta para uma varivel inteira no pode ser usado para apontar para uma
varivel de ponto utuante. Assim, um ponteiro tambm precisa de um tipo, que deve ser
igual ao tipo de varivel para a qual ele ir apontar.
Por exemplo, se queremos criar um ponteiro p que ir apontar para uma varivel inteira,
declaramo-no da seguinte maneira:
int *p;

Essa instruo apenas declara um ponteiro, sem apont-lo para nenhuma varivel.
Se quisermos declarar vrios ponteiros com uma nica instruo, devemos colocar o
asterisco em cada um deles. Se voc escrever o asterisco apenas no primeiro nome, apenas a
primeira varivel ser um ponteiro!
int *p1, *p2, *p3; /* ok, os trs so ponteiros */
double *p4, p5, p6; /* problema! s p4 ser um ponteiro */

Para fazer um ponteiro apontar para uma varivel, devemos atribuir-lhe como valor o
endereo de outra varivel, e no um nmero comum. Para isso, usamos o operador &
(E comercial), que fornece o endereo de uma varivel aqui ele ser conhecido como o
operador endereo-de. Se temos uma varivel var, seu endereo representado por &var.
Por exemplo:
int n;
int *p;
p = &n;

Aqui criamos uma varivel inteira chamada n, e em seguida criamos um ponteiro p, que
apontado para a varivel n.

5.1.2 Acesso indireto por ponteiros


Para acessar a varivel que apontada por um ponteiro, usamos o operador * (o mesmo
asterisco usado na declarao), chamado operador de indireo ou operador de desreferenci-
ao. Esse operador faz a volta do processo que leva da varivel ao ponteiro (a referencia-
o da varivel); por isso o chamamos de operador de desreferenciao. O nome indireo
usado simplesmente porque isso um acesso indireto varivel.
Esse um termo difcil de se traduzir. Em ingls, diz-se dereference, que seria uma combinao do prexo
de- (equivalente, nesse caso, ao nosso des-) e da palavra que signica referncia, no sentido de desfazer uma
referncia. Muitas pessoas tentam traduzir isso como de-referncia, mas essa palavra no consta nos dicionrios.
(Tudo bem, desreferenciao tambm no, mas eu achei melhor.)
Prolegmenos 65

Nomenclaturas parte, se p um ponteiro, podemos acessar a varivel para a qual ele


aponta com *p. Essa expresso pode ser usada tanto para ler o contedo da varivel quando
para alter-lo.

&var

var p
*p


Figura 5.2: Ilustrao da relao entre ponteiros e variveis.

Por exemplo,

int n, *p; /* veja que podemos declarar os dois */


p = &n; /* num mesmo comando */

*p = 5;
printf("n = %d\n", n); /* imprime 5 */

n = 10;
printf("*p = %d\n", *p); /* imprime 10 */

Vemos, ento, que acessar um ponteiro para uma varivel , de certa forma, equivalente
a acessar a varivel apontada. Podemos tambm mudar a varivel para a qual um ponteiro
aponta, e a partir da as operaes com o ponteiro s afetaro a varivel nova:

int a, b, *p;

p = &a;
*p = 5;
printf("a = %d\n", a); /* imprime 5 */

p = &b;
*p = 10;
printf("a = %d\n", a); /* ainda imprime 5 */
printf("b = %d\n", b); /* imprime 10 */

Desreferenciao ou multiplicao? O leitor atento deve ter-se perguntado se o fato de o


asterisco dos ponteiros ser o mesmo asterisco da multiplicao no gera nenhum problema.
E de fato no h nenhum problema; podemos inclusive multiplicar os valores apontados por
dois ponteiros, por exemplo:

c = *p1 * *p2;
66 Captulo 5. Ponteiros e vetores

Esse cdigo teria o efeito de obter os valores apontados por p1 e p2, multiplic-los e guardar
o resultado em c, como poderamos esperar.
Isso ocorre porque os operadores de desreferenciao so sempre interpretados antes
dos de multiplicao. Aps a interpretao das expresses *p1 e *p2, o que sobra so os dois
valores apontados pelos ponteiros, com um asterisco entre eles; isso s pode signicar uma
multiplicao se o asterisco do meio fosse atuar como operador de desreferenciao, a
expresso caria invlida, alm de ele estar agindo sobre algo que j no seria um ponteiro!
Apesar do cdigo mostrado acima ser vlido e inambguo (para um computador), re-
comendvel que voc use parnteses quando tiver de fazer esse tipo de coisa isso deixa o
cdigo bem mais legvel:
c = (*p1) * (*p2);

5.2 Ponteiros como parmetros de funes


Uma grande utilidade dos ponteiros a possibilidade de se modicar as variveis que foram
passadas como parmetros para uma funo. Anteriormente vimos um exemplo de uma
funo que tenta (sem sucesso) trocar o valor de duas variveis; sem usar ponteiros, a tarefa
era impossvel, pois os valores dos parmetros de uma funo so sempre copiados, de modo
que ela no tem acesso s variveis originais.
Agora que conhecemos ponteiros, a tarefa ca fcil. Se queremos que uma funo mo-
dique uma varivel, basta passar a ela um ponteiro para a varivel, em vez de passar o valor
da varivel (que o comportamento padro). Para de fato modicar a varivel dentro da fun-
o, devemos usar o operador de indireo para trocar os valores apontados pelos ponteiros
(seno, mudaramos apenas o lugar para o qual o ponteiro aponta, o que no nos interessa).
Vamos comear com um exemplo bem simples, que apenas dobra o valor de uma varivel.
No cabealho da funo, o ponteiro especicado da mesma maneira que nas declaraes
de varivel com um asterisco entre o tipo e o nome do parmetro.
void dobra_variavel(int *var)
{
*var = (*var) * 2;
}

Para chamar essa funo, usamos o operador & para passar o endereo de uma varivel.
Veja que voc no pode passar um nmero (uma constante) diretamente para essa funo,
pois ele no tem um endereo! Outro detalhe que deve ser levado em conta que a varivel
apontada deve ser inicializada antes de chamarmos a funo, j que a funo se baseia no
valor que havia na varivel.
int main()
{
int num;

num = 10;
dobra_variavel(&num);
printf("%d\n", num); /* 20 */

return 0;
Ponteiros como parmetros de funes 67

Com esse exemplo em mente, no devemos ter diculdades para montar uma funo que
de fato troca o valor de duas variveis (essa funo, na prtica, no incrivelmente til, mas
boa para ilustrar esse conceito):

void troca(int *a, int *b)


{
int temp = *a;
*a = *b;
*b = temp;
}

Naturalmente, tambm ser necessrio alterar a chamada funo troca, j que agora
precisamos passar os endereos das variveis e no mais os seus valores. Para cham-la,
escreveremos algo como o seguinte:

int m, n;
m = 10;
n = 20;
troca(&m, &n);

Em computao, comum usar os nomes chamada por valor e chamada por referncia;
eles indicam se a funo chamada recebe apenas uma cpia do valor da varivel, ou se recebe
uma referncia (um ponteiro) para a varivel. Em C, as chamadas de funes so intrinse-
camente por valor, mas podemos usar ponteiros para obter o comportamento das chamadas
por referncia.

5.2.1 Usando ponteiros para devolver valores


Em muitos casos, uma funo pode ter mais de uma sada. Como uma funo s pode de-
volver um nico valor atravs da instruo return, estamos um pouco limitados. No entanto,
se passarmos a uma funo o endereo de uma varivel, a funo pode gravar diretamente
sua sada naquela varivel.
Vamos rever o exemplo da equao de segundo grau ax2 + bx + c = 0, com coecientes
reais. Sabemos que, de acordo com o sinal do discriminante = b2 4ac, pode haver
duas, uma ou nenhuma raiz real. Vamos escrever uma funo que distingue entre esses trs
casos, como j zemos na pgina 56, e calcula as razes reais da equao quando for o caso.
A funo dever devolver o nmero de razes reais (distintas), e gravar as razes encontradas
(quando for o caso) em variveis fornecidas pelo usurio. Veja nossa soluo:

int equacao_quadratica (double a, double b, double c, double *raiz1,


double *raiz2)
{
double delta;

delta = b*b - 4*a*c;


if (delta < 0) {
return 0;
}
68 Captulo 5. Ponteiros e vetores

else if (delta == 0) {
*raiz1 = -b/(2*a);
return 1;
}
else {
*raiz1 = (-b - sqrt(delta)) / (2*a);
*raiz2 = (-b + sqrt(delta)) / (2*a);
return 2;
}
}

5.3 Cuidado com os ponteiros!


Um grande poder exige uma grande responsabilidade. Os ponteiros so uma ferramenta muito
poderosa da linguagem C, mas devem ser usados com muita cautela. Se um ponteiro no
estiver apontando para o lugar que voc espera, coisas terrveis podem ocorrer. comum que
apaream bugs ou erros esquisitos no seu programa simplesmente porque voc est acessando
um ponteiro invlido. Dizemos que um ponteiro invlido, em geral, quando ele aponta para
uma posio de memria que no uma varivel do seu programa. Isso costuma ocorrer em
situaes como as seguintes:

Voc no inicializou o ponteiro (isto , no atribuiu nenhum valor a ele), de modo que
no se tem idia do lugar da memria para o qual ele aponta.
Voc apontou o ponteiro para um endereo arbitrrio, que no pertence ao seu pro-
grama. Por exemplo, se voc tentar atribuir o valor 300 ao seu ponteiro, ele apontar
para a posio 300 da memria do seu computador no temos idia do que pode
haver nesse lugar.
Seu ponteiro caiu num lugar invlido da memria, apesar de ter sido inicializado
corretamente. Isso ocorre mais frequentemente quando voc est variando atravs de
um lao (for, while) a posio apontada pelo ponteiro (veremos isso mais adiante) e
no pra na hora certa alguma hora, seu ponteiro acaba apontando para um lugar
que no deveria.

Por exemplo, voc no deve fazer nada parecido com isso:

int *p;
*p = 5; // erro terrvel!!

O que ocorreu aqui que voc criou um ponteiro mas no deniu para onde ele aponta.
Seu valor inicial ser aleatrio, e portanto, ao tentar atribuir um valor varivel apontada
pelo ponteiro, voc estar acessando uma posio aleatria da memria. Repito: no faa
isso!
Geralmente, quando voc tentar acessar ponteiros invlidos, o programa ir dar um erro
durante a execuo e fechar. No Linux, voc ver algo do tipo Segmentation fault ou Falha
de segmentao (isso quer dizer que voc acessou um segmento de memria invlido; tem a
ver com a organizao interna da memria). No Windows, voc dever ver uma das famosas
e genricas mensagens Este programa executou uma operao ilegal e ser fechado.
Vetores 69

s vezes tambm pode acontecer de o programa continuar funcionando mesmo com um


erro desses; os sintomas sero mais sutis, como valores inesperados nas suas variveis, e a
que esses erros so mais difceis de rastrear. Portanto, olho vivo ao usar ponteiros!

5.4 Vetores
Vetores so uma maneira prtica de guardar um grande conjunto de variveis relacionadas
por exemplo, sequncias de nmeros. Em C, um vetor uma srie de variveis indexadas
que podem ser acessadas por meio de um ndice inteiro, por exemplo vetor[4]. H
uma pequena restrio: um vetor s guarda variveis do mesmo tipo ou seja, voc pode
fazer um vetor de inteiros e um vetor de nmeros de ponto utuante, mas no um vetor que
contenha ambos.

H vrios termos diferentes para se referir aos vetores. muito comum encontrar array, que o
termo original em ingls; tambm vemos matrizes, termo que prero reservar apenas aos vetores
multidimensionais, como veremos mais tarde. O termo lista tambm possvel, mas pouco usado
em C (ele pode ser usado em outro contexto semelhante, sendo mais comum em outras linguagens).

Essas variveis so todas guardadas sequencialmente (sem buracos) na memria e, em


um vetor de n elementos, so identicadas por ndices de 0 a n 1 (veja a gura 5.3). Em
C, podemos nos referir ao elemento de ndice i de um vetor V pela expresso V[i].

.
V[0] V[1] V[n 1]
.

Figura 5.3: Ilustrao do modo de armazenamento de um vetor com n elementos.

Para usar um vetor, precisamos primeiro declar-lo, como era feito para qualquer varivel
normal. A declarao de um vetor feita da seguinte maneira:

tipo_de_dados nome_vetor[tamanho];
O compilador entende essa frase como: reserve na memria um espao para tamanho va-
riveis do tipo tipo_de_dados, e chame esse espao de nome_vetor. Veja dois exemplos desse
tipo de declarao:

int sequencia[40];
double notas[100];

importante ressaltar que o compilador apenas reserva o espao de memria pedido,


sem colocar nenhum valor especial nele. Isso signica que o vetor conter inicialmente uma
seleo aleatria de valores (que sobraram da execuo de algum programa que usou aquele
espao), exatamente como ocorria para as variveis comuns.
Voc deve prestar ateno a alguns detalhes do funcionamento dos vetores em C:

Os elementos de um vetor so numerados a partir de zero. Dessa maneira, num vetor


V que tem 5 elementos, os elementos so: V[0], V[1], V[2], V[3] e V[4]. No se

Isso no apenas uma extravagncia do C; muitas linguagens funcionam dessa maneira, que a mais natural
para um computador logo mais veremos por qu.
70 Captulo 5. Ponteiros e vetores

confunda com a declarao! Um tal vetor seria declarado com uma instruo do tipo
int V[5], mas o elemento V[5] no existiria!
realmente necessrio tomar bastante cuidado com a numerao dos elementos. Se
voc tentar acessar um elemento invlido, como V[5] ou V[100] (para este caso com
5 elementos), o compilador no o avisar disso, e erros estranhos comearo a ocorrer
no seu programa o mesmo tipo de erro que pode ocorrer com os ponteiros.
O tamanho deve ser um valor constante (no pode depender de valores de variveis).
Ou seja, voc no pode perguntar ao usurio o tamanho desejado, guardar numa va-
rivel n e depois declarar um vetor do tipo int V[n]. Em outras palavras, o tamanho
do vetor deve ser um valor que possa ser estabelecido na hora da compilao do pro-
grama.
Por isso, quando for necessrio ler uma quantidade de dados que s estipulada na
execuo do programa, a princpio teremos de estabelecer um teto no nmero de dados
a serem lidos, usando um vetor de tamanho xo. Caso o teto no seja atingido, algumas
entradas do vetor caro sem ser utilizadas.
possvel, sim, criar vetores cujo tamanho s conhecido a posteriori; no padro
C99 possvel fazer declaraes do tipo int V[n] com algumas restries. H outro
recurso, de certa maneira mais exvel, que permite criar vetores cujo tamanho s
conhecido na execuo do programa: a alocao dinmica de memria, que ser vista
no Captulo 7.
Alm de constante, o tamanho dos vetores imutvel, ou seja, se eu declarei um vetor
de 5 entradas, eu no posso aument-lo para que caibam 10 entradas. Se eu quero que
caibam 10 entradas, eu preciso reservar espao para 10 entradas logo no comeo.
(Novamente, a alocao dinmica de memria salva a ptria nesse aspecto; no captulo
7, voc ver o que possvel fazer quanto a isso.)

Podemos acessar os elementos de um vetor, em geral, tratando-os como se fossem vari-


veis normais. O operador de endereo tambm pode ser usado com os elementos individuais
do vetor. Por exemplo,

int lista[3];

scanf("%d", &lista[0]);
lista[1] = 37;
lista[2] = lista[1] - 3*lista[0];

printf("%d %d %d\n", lista[0], lista[1], lista[2]);

Dito isso, vamos ver algumas aplicaes simples do uso de vetores.

E 5.1. Vamos fazer um programa que l uma lista de n nmeros (n 100) e os


imprime na ordem inversa (em relao ordem em que foram lidos). Para isso, necessrio
armazenar cada um dos nmeros lidos antes de comear a imprimi-los isso no possvel
(a no ser com um cdigo extremamente pedestre e propenso a erros) com o que tnhamos
aprendido antes.

#include <stdio.h>
Vetores 71

int main()
{
int lista[100];
int i, n;

printf("Digite o nmero de elementos (no mximo 100): ");


scanf("%d", &n);
if (n > 100) {
printf("n no pode ser maior que 100! S lerei os "
"100 primeiros nmeros.\n");
n = 100;
}

printf("Digite a seguir os %d elementos:\n", n);


/* leitura dos elementos */
for (i = 0; i < n; i++)
scanf("%d", &lista[i]);

/* impresso dos elementos , na ordem inversa */


for (i = n-1; i >= 0; i--)
printf("%d ", lista[i]);
printf("\n");

return 0;
}

Um aspecto que ainda no reforamos foi a validao da entrada do usurio. Voc pode
pedir encarecidamente que o usurio digite um nmero at 100, mas nada garante que o
usurio no ser desonesto ou distrado e digite um nmero fora dos limites pedidos. Nessas
horas, voc no deve conar no usurio, e deve vericar se o valor digitado vlido, para
evitar que aconteam coisas ms em seu programa nesse caso, se o usurio digitasse um
nmero maior que 100 e no zssemos essa vericao, acabaramos acessando posies
invlidas do vetor (acima do ndice 99).

5.4.1 Inicializao de vetores


Em algumas situaes voc precisar usar vetores cujo contedo seja determinado inicial-
mente por voc, e no lido do teclado ou de algum arquivo. Obviamente seria muito cansativo
ter de inicializar elemento por elemento, como a seguir:

int lista[100];

lista[0] = 9;
lista[1] = 35;
.
.
.
lista[99] = -1;
72 Captulo 5. Ponteiros e vetores

Felizmente, o C permite que voc inicialize os valores de um vetor junto com a declarao
(assim como de uma varivel escalar comum). Isso feito da seguinte maneira:

tipo_de_dados nome_vetor[tamanho] = { lista de valores };

Na realidade, voc no precisa escrever o tamanho explicitamente se especicar todos os


elementos o compilador simplesmente contar quantos elementos voc digitou ( neces-
srio, no entanto, manter os colchetes, para que o compilador saiba que isso um vetor).
Assim, o nosso rduo exemplo poderia ser escrito da seguinte maneira (vamos reduzir um
pouco o tamanho do vetor, s para no precisarmos digitar tantos elementos):

int lista[] = {9, 35, -17, 9, -4, 29, -2, 10, -1};

Note que isso s possvel na hora da declarao. No possvel, fora desse contexto,
denir de uma s vez todos os elementos do vetor, como a seguir ( necessrio atribuir
elemento por elemento):

/* isto est errado! */


lista = {7, 42, 0, -1, 3, 110, 57, -43, -11};

Tambm possvel declarar um vetor de um certo tamanho mas s inicializar parte dele
(desde que seja a parte do comeo). Por exemplo, suponha que queremos espao para uma
sequncia de 100 inteiros, mas que vamos comear inicializando apenas os 10 primeiros.
Nesse caso, escrevemos explicitamente o tamanho do vetor, mas s especicamos os ele-
mentos desejados:

int lista[100] = {9, 35, -17, 9, -4, 29, -2, 10, -1, 47};

Nesse caso, os elementos que no foram especicados sero automaticamente inicializados


com o valor 0.

5.5 Vetores como argumentos de funes


No h, a princpio, problema nenhum em passar vetores como argumentos para uma funo.
O que acontece geralmente que vetores podem ser bem grandes, e portanto no seria muito
prtico copiar todos os valores para a funo; por isso, vetores so naturalmente passados
por referncia para as funes ou seja, quando voc passa um vetor para uma funo,
ela na verdade recebe apenas o endereo dos dados; quando sua funo for acessar os dados
do vetor, ela ser apontada diretamente para a posio deles no vetor original. Com isso,
qualquer modicao num vetor passado como parmetro reetida na funo original que
passou o vetor.
Uma consequncia da passagem por referncia dos vetores que no possvel passar
para uma funo um vetor livre, ou seja, uma lista de valores ad hoc que no est vinculada
a uma varivel na prtica, isso quer dizer que no possvel escrever coisas do tipo

funcao({2, 3, 4, 5});

pois o vetor livre no possui um endereo a ser passado para a funo.


Vamos primeiro ver na prtica como podemos passar vetores como argumentos de fun-
es. Quanto ao cabealho da funo, basta imitar a declarao de vetores, exceto por um
Vetores como argumentos de funes 73

detalhe: no devemos fornecer o tamanho do vetor os colchetes devem ser deixados so-
zinhos, sem nada no meio. A funo a seguir recebe um vetor de inteiros como parmetro
e imprime seu primeiro elemento:

void imprime_primeiro(int v[])


{
printf("%d\n", v[0]);
}

Para mandar um vetor como parmetro de uma funo, voc simplesmente deve escrever
o nome dele, sem colchetes ou qualquer outro smbolo:

int vetor[] = {1, 2, 3, 4};


imprime_primeiro(vetor); /* 1 */

Agora, se a funo no impe nenhum tamanho para o vetor, como vamos descobrir qual
o tamanho do vetor que a funo recebeu? A resposta que a funo no tem como descobrir
isso sozinha; em geral, voc quem deve cuidar disso. Por exemplo, voc pode passar para
a funo dois parmetros: o vetor e o tamanho do vetor. Uma ilustrao disso uma funo
que imprime todos os elementos de um vetor:

void imprime_vetor(int v[], int n)


{
int i;
for (i = 0; i < n; i++)
printf("%d\n", v[i]);
}

Agora, para cham-la, devemos incluir o tamanho do nosso vetor original:

int vetor[] = {1, 2, 3, 4};


imprime_vetor(vetor, 4);

Da mesma maneira, poderamos fazer uma rotina que l do teclado uma srie de nmeros
inteiros e armazena todos eles num vetor. Uma implementao simples seria como a seguir:

void le_vetor(int v[], int n)


{
int i;
for (i = 0; i < n; i++)
scanf("%d", &v[i]);
}

Para cham-la, faramos da mesma maneira que no exemplo anterior:

int vetor[20];
le_vetor(vetor, 20);

Se o tamanho do vetor for explicitamente fornecido, o compilador no deve reclamar, mas o programa no
ser forado de maneira alguma a respeitar esse tamanho.
74 Captulo 5. Ponteiros e vetores

Fornecer o tamanho correto do vetor funo de suma importncia! Se a funo achar


que seu vetor maior do que realmente , ela tentar acessar elementos invlidos (depois do
m do vetor), o que j vimos que um erro bem grave especialmente se a funo tentar
gravar dados nessas reas!

5.6 Ponteiros e vetores


Vimos que vetores so naturalmente passados por referncia como parmetros de funes.
Na verdade, vetores em C tm uma estreita relao com ponteiros. O nome de um vetor
funciona, na prtica, como um ponteiro para seu primeiro elemento. Isso d conta de explicar
a passagem por referncia natural dos vetores: sabendo o endereo do primeiro elemento,
podemos encontrar todos os outros elementos do vetor.
Como fazer isso? Como os elementos so guardados juntinhos na memria, sem buracos,
basta saber o tamanho T de cada elemento (por isso que importante que o vetor tenha valores
de um tipo s; sabendo o tipo, sabemos o tamanho), por exemplo, em bytes. Da, partindo
da posio do primeiro elemento, fazemos um salto de tamanho T tantas vezes quanto for
necessrio para chegar ao elemento de ndice n, devemos saltar n vezes. por isso,
tambm, que os ndices comeam de 0 e no de 1: para o computador mais intuitiva a
noo de deslocamento (em relao ao comeo do vetor) do que a noo de posio.
Dado um ponteiro para o primeiro elemento de um vetor, fcil realizar (em C) esse
salto: basta somar ao ponteiro o nmero de elementos desejado. Note que voc no precisa
se preocupar em saber o nmero de bytes, apenas o nmero de elementos! Por exemplo,
se p aponta para o comeo de um vetor, p + 1 aponta para o segundo elemento, indepen-
dentemente de quantos bytes ocupa cada elemento. Da mesma maneira podemos usar os
operadores como ++ e += (e, analogamente, os de subtrao):

int v[10];
int *p;
p = v; /* faz o ponteiro apontar para o comeo do vetor */
/* (equivale a: p = &v[0]) */
p++; /* aponta o ponteiro para o segundo elemento */
p = p+5; /* faz mais um salto e aponta para o 7 elemento */

Essas operaes com ponteiros so conhecidas como aritmtica de ponteiros. Veja que
elas apenas alteram o local de destino dos ponteiros; elas no mexem com os valores apon-
tados. Note tambm que no existem, nem fariam sentido, as operaes de multiplicao e
diviso de ponteiros.
Agora como usamos isso para acessar o i-simo elemento do vetor v? A expresso v + i
(ou p + i neste exemplo acima) certamente um ponteiro para ele, pelo que acabamos de
ver; ento podemos usar *(v+i) para acess-lo. Como isso usado com muita frequncia,
existe uma abreviao sinttica, que nada mais que v[i]. Assim, ao acessar elementos
de vetores, estamos o tempo todo utilizando a aritmtica de ponteiros!
Isso nos d uma nova maneira de caminhar pelos vetores: criar um ponteiro que aponta
inicialmente para o comeo do vetor e aument-lo de uma unidade num lao for, por exemplo:

int v[10];
int *p;

for (p = v; /* condio */; p++) {


Strings 75

/* o elemento atual do vetor pode ser acessado


* simplesmente pela expresso *p. */
}

Aparentemente h um problema: onde parar o lao se a posio no controlada por um


ndice? Veremos em seguida um tipo de vetor para o qual isso no um problema uma
soluo.

5.7 Strings
Uma das principais utilidades do tipo char usar vetores para formar sequncias de ca-
racteres, conhecidas em computao como strings (esse termo tornou-se to difundido que
no costuma ser traduzido; possveis tradues so sequncias ou cadeias de caracteres).
Lembre-se de que caracteres nada mais so que inteiros com um signicado especial, codi-
cados atravs de uma tabela (em geral, a tabela ASCII e suas extenses). Em C, uma string
simplesmente um vetor de caracteres, com uma conveno especial: como o valor zero no
utilizado por nenhum caractere, ele utilizado para marcar o nal de uma string, ou seja, ele
o terminador da string. O caractere de cdigo zero comumente chamado de caractere
nulo, e representado pela sequncia especial '\0' ou simplesmente pela constante inteira
0.
Sabendo disso, podemos declarar uma string da mesma maneira como declaramos veto-
res:
char string[] = {'O', 'l', '', '\0'};

Certamente no nada prtico escrever assim! Felizmente essa notao pode ser bas-
tante abreviada: em vez de escrever explicitamente um vetor de caracteres com um caractere
nulo, podemos deixar mais clara a cara de string, colocando os caracteres desejados entre
aspas duplas, sem separao entre eles, e o resto (inclusive o caractere nulo) car por conta
do compilador. (Veja que j usamos essa sintaxe o tempo todo quando usamos as funes
scanf e printf!) O cdigo acima poderia ser reescrito como a seguir, e as duas formas so
absolutamente equivalentes:
char string[] = "Ol";

Uma das grandes vantagens de usar o terminador '\0' (explicita ou implicitamente) que
voc no precisa se preocupar com o tamanho das strings: ao caminharmos pelos caracteres
de uma string, no precisamos registrar a posio em que estamos para saber quando parar;
s parar quando encontrarmos o caractere nulo. Com isso, em vez de percorrer os caracteres
de uma string usando um ndice de um vetor, podemos usar apenas um ponteiro. Veja um
uso disso neste exemplo, que imprime os caracteres da string indicada, um por linha:
char string[] = "Bom dia!";
char *p;

for (p = string; *p != 0; p++)


printf("Caractere: '%c'\n", *p);

A condio de parada do loop simplesmente o caractere nulo, que no depende de


forma alguma do tamanho da string! Podemos usar isso, por exemplo, para achar o tamanho
de uma string:
76 Captulo 5. Ponteiros e vetores

int n = 0;
char string[] = "Bom dia!";
char *p;

for (p = string; *p != 0; p++)


n++;
printf("A string tem comprimento %d\n", n);

5.7.1 Alterando strings


Lembre-se de que, se quisermos modicar o valor dos elementos de um vetor comum,
necessrio atribuir os valores elemento por elemento; para as strings isso no haveria de
ser diferente. Outro fator que complica nossa vida o fato de vetores terem um tamanho
imutvel; se declararmos um vetor de um certo tamanho e precisarmos posteriormente de
mais espao, no h, a princpio, o que fazer. Tudo isso muito relevante pois, se queremos
trocar o contedo de uma string, muito provvel que seu tamanho precise mudar tambm.
Como lidar com essas limitaes?
A questo do tamanho pode ser contornada, para certos propsitos, de maneira fcil:
como uma string contm informao sobre o seu prprio tamanho (indiretamente atravs
do terminador \0), podemos declarar um vetor que tenha mais espao do que o necessrio
para guard-la, e ainda ser fcil distinguir onde a frase comea e termina. Por exemplo,
podemos declarar um vetor de 100 entradas, para guardar uma frase de apenas 20 caracteres,
sendo a 21 entrada o terminador da string; as outras 79 entradas no sero usadas.
Essa soluo nos permite alterar posteriormente o contedo do vetor para qualquer sequn-
cia de caracteres que no ultrapasse o tamanho que denimos inicialmente. O enfado de
atribuir elemento por elemento pode ser eliminado graas funo strcpy, que copia o con-
tedo de uma string para outra. Mas como isso nos ajuda? Quando precisamos passar um
array comum como parmetro para uma funo, no possvel usar uma lista de valores ad
hoc, mas apenas o nome de uma varivel j declarada. Entretanto, as strings so, de certa
forma, privilegiadas: possvel usar uma expresso de string com aspas duplas nesse tipo de
situao (e em algumas outras tambm) tanto que j as utilizamos diversas vezes, parti-
cularmente com as funes printf e scanf. Deste modo, usa-se a funo strcpy (string copy)
para copiar para o vetor j declarado uma string pr-fabricada por exemplo:

char mensagem[20];
strcpy(mensagem , "Bom dia!");

Isso equivalente a copiar individualmente cada caractere da nossa string pr-fabricada para
a varivel de destino:

char mensagem[20];
mensagem[0] = 'B';
mensagem[1] = 'o';
mensagem[2] = 'm';
mensagem[3] = ' ';
mensagem[4] = 'd';
mensagem[5] = 'i';
mensagem[6] = 'a';
Strings 77

mensagem[7] = '!';
mensagem[8] = 0;

Note que o terminador tambm copiado! Assim, na string de destino, devemos garantir
que o espao seja suciente para guardar todos os caracteres da string original mais um (o
terminador). Se o destino tem tamanho 20, podemos copiar no mximo uma string de 19
caracteres.
Observe tambm que no utilizamos o operador de endereo com a nossa varivel tipo
string, pois ela um vetor e, como j vimos, vetores so naturalmente passados por referncia
em parmetros de funes.
Essa funo tambm pode, naturalmente, ser usada para copiar o contedo de uma string
para outra quando ambas esto contidas em vetores j declarados:

char origem[20] = "Boa noite!";


char destino[20];
strcpy(destino , origem);

Como sempre, devemos ter cuidado para no exceder o tamanho do vetor original. Se
tentssemos copiar uma mensagem muito longa para o vetor, nosso programa iria tentar
gravar dados fora da rea reservada para o vetor, o que j vimos que no uma boa ideia.
Por isso, existe tambm uma outra funo, strncpy, que copia o contedo de uma string para
outra sem exceder um certo nmero mximo de caracteres (especicado por um parmetro
da funo):

strncpy(destino, origem, n mximo);


Por exemplo:

char mensagem[20];
strncpy(mensagem , "Copiando uma mensagem muito longa", 20);

O resultado deste cdigo ser copiar a sequncia Copiando uma mensage, com exatamente 20
caracteres, para a varivel mensagem. O problema aqui que, como a string de origem era
mais longa do que o destino poderia suportar, no foi inserido nenhum terminador. Nesse
caso, o que podemos fazer inserir o terminador manualmente, e pedir para copiar no m-
ximo 19 caracteres:

char mensagem[20];
strncpy(mensagem , "Copiando uma mensagem muito longa", 19);
mensagem[19] = 0;

Caso a origem tenha tamanho menor do que o tamanho mximo especicado, o terminador
ser copiado automaticamente.

J vimos, na seo anterior, como possvel encontrar o tamanho de uma string. Como
esse cdigo muito usado, existe uma funo da biblioteca padro que faz exatamente a
mesma coisa: strlen (string length). Seu uso bem simples:

char mensagem[50] = "Uma mensagem muito longa";


printf("A mensagem tem comprimento %d.\n", strlen(mensagem));
78 Captulo 5. Ponteiros e vetores

Note que a funo devolve o comprimento da string (24), e no o comprimento do vetor que
foi usado para guard-la (que 50).

Outra tarefa muito comum descobrir se duas strings so iguais. Como strings so ve-
tores, que so ponteiros, uma comparao do tipo s1 == s2 compara os endereos em que
esto guardadas as strings. Obviamente essa comparao s verdadeira quando compa-
ramos uma string com ela mesma; nosso objetivo comparar duas strings distintas que, no
entanto, possam apresentar o mesmo contedo.
Para isso, devemos comparar as strings elemento a elemento; convenientemente h uma
funo que j incorpora esse trabalho, strcmp (string compare). Dadas duas strings s1 e s2 ,
strcmp(s1, s2) devolve o valor zero caso elas sejam iguais, e um valor diferente de zero caso
sejam diferentes. Esse valor tambm serve para determinar a ordem lexicogrca (a ordem
do dicionrio) das duas strings: um nmero negativo indica que s1 < s2 , e um nmero
positivo indica que s1 > s2 . Por exemplo, dadas s1 = verde e s2 = vermelho, temos
s1 < s2 pois as duas palavras coincidem nas 3 primeiras letras, mas, na 4 letra, d vem
antes de m.
A ordem lexicogrca estabelecida de acordo com a tabela ASCII por exemplo,
nmeros vm antes de letras maisculas, que vm antes das minsculas, de modo que verde
diferente de Verde (por exemplo, Vermelho < verde < vermelho). Se quisermos fazer
uma comparao seguindo outra ordem, precisaremos construir nossa prpria funo, o que
seremos capazes de fazer depois de estudar, no Captulo 6, o funcionamento interno da funo
strcmp.

5.7.2 Entrada e sada


No h uma maneira direta de trabalhar com vetores de nmeros pelas funes de entrada/-
sada da biblioteca padro (como scanf e printf); mas, como a entrada e a sada padro so
baseadas num uxo de caracteres, possvel trabalhar diretamente com strings usando essas
funes.
Para imprimir uma string, podemos usar o cdigo %s na funo printf. Por exemplo:

char cor[] = "vermelho";


printf("Meu carro %s.\n", cor);

Para ler uma string, possvel tambm usar o cdigo %s na funo scanf; no entanto, h
alguns cuidados que devem ser tomados, como explicaremos adiante. agora o momento
de introduzir uma nova funo: fgets. A novidade dessa funo que ela tambm pode ser
usada para ler uma string de um arquivo, e portanto precisamos de um parmetro especial
para dizer que o arquivo de que vamos ler a entrada do usurio pelo teclado esse
parmetro stdin, que a abreviao em ingls para entrada padro (standard input), que
o termo usado para denotar o canal de comunicao entre o programa e o terminal.
Antes de dizer como se escreve isso em C, vamos observar que, para ler uma string,
precisamos de um vetor grande o bastante para guardar os caracteres. Como no sabemos
a priori quantos caracteres sero digitados, a estratgia que seguimos em geral a seguinte:
comeamos reservando um espao inicial (temporrio) para os caracteres, e lemos os dados

Se voc se perguntou por que cargas dgua precisvamos usar esse termo tcnico em vez de simplesmente
dizer teclado, adianto que a entrada padro pode ser redirecionada para pegar dados, por exemplo, de um arquivo
em vez do teclado, e portanto uma estrutura exvel que permite diversos tipos de entrada. Veremos um pouco
sobre isso mais adiante.
Mais sobre entrada e sada 79

disponveis at encher esse espao. Se no houver dados para preencher todo o espao, ter-
minamos nosso trabalho; se ainda sobrarem dados para ler, precisamos transferir os dados
lidos para outro lugar, para poder continuar lendo o restante dos dados, repetindo o proce-
dimento conforme for necessrio. O nome que costuma ser dado a esse espao temporrio
buer, que tem exatamente esse sentido um espao temporrio para armazenar dados
provenientes de algum dispositivo (que o nosso caso, com o teclado), ou destinados a algum
dispositivo.
A funo fgets precisa, alm do parmetro especial stdin, de duas informaes: quantos
caracteres (no mximo) ler, e onde guard-los. Isso feito da seguinte maneira:

fgets(destino, n, stdin);
Um comentrio importante faz-se necessrio: o parmetro n na verdade indica o nmero
mximo de caracteres da string menos um, pois a funo fgets sempre adiciona o terminador
\0 aps o ltimo caractere lido: o tamanho n refere-se ao comprimento da string quando se
inclui o terminador. Nesse sentido preciso prestar ateno diferena em relao funo
strncpy. Assim, se temos um vetor de tamanho 20, podemos chamar a funo fgets com o
parmetro n = 20, e no mximo 19 caracteres sero lidos e armazenados.
Essa funo ir ler uma linha da entrada padro at o mximo de n 1 caracteres. Se a
linha tiver menos do que isso, a leitura acabar no nal da linha; se a linha tiver mais do que
isso, a leitura acabar no (n 1)-simo caractere.

possvel ler strings usando a funo scanf de duas maneiras diferentes, ambas um pouco
diferentes do funcionamento de fgets. Para evitar problemas de acesso a lugares errados na
memria, sempre necessrio especicar o nmero mximo n de caracteres que podero ser
armazenados (Nos itens abaixo, no meio dos cdigos, entenda um n como esse nmero, e
no como uma letra n a ser digitada literalmente.)

Com o cdigo %nc, sero lidos n caracteres da entrada padro, incluindo espaos e
quebras de linha. O terminador \0 no includo.
Usando o cdigo %ns, so lidos no mximo n caracteres da entrada padro, parando
assim que encontrar algum espao em branco que no ser armazenado na string.
Um terminador includo automaticamente ao nal da string, mas o tamanho n no o
leva em conta. Ou seja, o vetor deve ter espao para n + 1 caracteres.
necessrio prestar ateno diferena de comportamento das diferentes funes em
relao incluso do terminador da string. Sempre que estiver em dvida, consulte o manual
da biblioteca padro ou, melhor ainda, faa um programa simples para testar!

5.8 Mais sobre entrada e sada

Espao em branco a denominao genrica para os caracteres que representam algum tipo de espaamento,
seja horizontal ou vertical a saber, o espao comum (ASCII 32), a tabulao horizontal \t (ASCII 9) e quebras
de linha (\n, ASCII 10, ou o retorno de carro \r, ASCII 13), entre outros de uso mais raro.
80 Captulo 5. Ponteiros e vetores

Tabela 5.1: Resumo das funes de manipulao de strings

Funo Descrio
strcpy(dest , origem) Copia todo o contedo de origem para dest.
strncpy(dest , origem, n) Copia no mximo n caracteres de origem para dest.
strlen(str) Devolve o tamanho da string str.
strcmp(s1, s2) Compara as strings s1 e s2, e devolve 0 caso elas
sejam iguais.
fgets(dest , n, stdin) L uma linha da entrada padro, com no mximo
n 1 caracteres, armazenando em dest.
Algoritmos
6

81
Mais ponteiros
7
7.1 Matrizes
Os vetores que vimos at agora eram usados para guardar variveis escalares; vamos explorar
agora outra possibilidade: usar um vetor para guardar um conjunto de vetores. Por exemplo,
se temos 3 vetores de 5 inteiros, podemos criar um vetor que contm esses 3 vetores, e
podemos acessar os inteiros usando dois ndices: primeiro o ndice que identica cada um
dos trs vetores, depois o ndice que identica cada inteiro dentro de cada vetor. Podemos
interpretar isso como uma matriz: o primeiro ndice indica a linha em que um elemento est,
e o segundo indica a posio (coluna) desse elemento dentro da linha correspondente.
Em suma, nessa representao, cada linha de uma matriz um vetor de n nmeros, e
a matriz um vetor de m vetores-linha, formando assim uma matriz m n (m linhas, n
colunas). A seguir v-se uma ilustrao dessa representao, na qual as barras mais claras
representam os vetores-linha, que esto contidos na caixa mais escura, que corresponde
matriz:

.
a0,0 .
a0,1 . .
a0,n1
.
a1,0 .
a1,1 . .
a1,n1
.
...
.
.
am1,0 .
am1,1 . .
am1,n1

Poderamos tambm inverter nessa representao o papel das linhas e colunas isto ,
o ndice principal seria o da coluna e o secundrio seria uma posio (linha) dentro dessa
coluna. Preferimos manter a linha como ndice principal para cooperar com a conveno
dos matemticos, mas no h nenhuma razo computacional que nos obrigue a escolher
uma dessas interpretaes em detrimento da outra. A estrutura computacional subjacente s
determina que h uma hierarquia de ndices, que podemos interpretar como quisermos.
Para declarar uma varivel do tipo matriz, usamos a seguinte sintaxe, muito semelhante
sintaxe de vetores:

tipo_de_dados nome_matriz[linhas][colunas];

83
84 Captulo 7. Mais ponteiros

Aplicam-se as mesmas observaes apontadas para os vetores: os nmeros de linhas e


colunas devem ser constantes, e os ndices dos elementos so numerados a partir do zero.
Podemos inicializar matrizes de maneira similar dos vetores, introduzindo dois nveis
de chaves os internos para agrupar os elementos de uma mesma linha, os externos para
agrupar as linhas. No entanto, no possvel deixar os colchetes de tamanho vazios; ne-
cessrio preencher pelo menos o ltimo:

int matriz[3][2] = {{2, 3}, {5, 7}, {9, 11}}; /* ok */


int matriz[][2] = {{2, 3}, {5, 7}, {9, 11}}; /* ok */
int matriz[][] = {{2, 3}, {5, 7}, {9, 11}}; /* invlido! -
*/

As matrizes que criamos tambm so chamadas de vetores de duas dimenses; tambm


possvel criar vetores com mais do que duas dimenses ou seja, vetores com mais do que
dois ndices, como vetor[2][3][1] , e no difcil deduzir como se faz isso. Se um vetor
tem d dimenses, cada ndice tem um intervalo de valores possveis no caso das matrizes, o
primeiro ndice variava entre os nmeros de linhas e o segundo entre os nmeros de colunas.
Dizemos que nj o comprimento do vetor ao longo da dimenso j (j = 1, 2, . . . , d), o
que equivale a dizer que o j-simo ndice varia de 0 at nj 1. Para criar um vetor de d
dimenses, com nj entradas ao longo da dimenso j (j = 1, 2, , d), fazemos o seguinte:

tipo_de_dados nome_vetor[n1 ][n2 ] [nd ];

7.2 Alocao dinmica de memria


comum a necessidade de lidar com uma quantidade de dados cujo tamanho imprevisvel
no momento em que escrevemos nosso programa; por exemplo, suponha que queremos ler
todo o contedo de um arquivo que est armazenado no computador, e armazenar numa
varivel. Essa varivel dever ser um vetor de caracteres, mas no sabemos, ao escrever
nosso programa, o tamanho que isso poder ocupar. Se inventarmos um tamanho mximo,
reservando, por exemplo, espao para um vetor de 100 000 entradas, corremos o risco de nos
deparar com um arquivo maior do que isso, e dessa situao no h muita sada, pois no h
como aumentar o tamanho desse vetor posteriormente.
Para lidar com esse tipo de situao de maneira bem mais elegante e prtica, a biblioteca
padro do C inclui um conjunto de funes que permitem a alocao dinmica de mem-
ria; so funes que, ao serem chamadas, pedem ao Guardio da Memria um pedao de
memria de um certo tamanho, e, se o pedido for aceito (isto , se houver memria dispon-
vel), devolvem o endereo de um pedao de memria conforme pedido (isto , um ponteiro
para a regio de memria). A declarao de um vetor como fazamos anteriormente, em
contrapartida, chamada de alocao esttica de memria, pois ela pr-estabelecida na
compilao do programa, ao contrrio da alocao dinmica que de fato realizada durante
a execuo do programa.
Isso soluciona um dos nossos antigos problemas: a declarao de vetores ou matrizes
lidos pelo usurio. Voc deve se lembrar que no tnhamos como declarar um vetor cujo
tamanho seja uma varivel lida do usurio ramos obrigados a estipular um tamanho limite
e reservar um espao desse tamanho, mesmo que fssemos usar menos (e nos impedindo de
usar mais). Usando a alocao dinmica, podemos, sabendo o nmero n de dados a serem

Vulgo sistema operacional.


Alocao dinmica de memria 85

lidos, pedir um pedao de memria no qual caibam n inteiros (ou variveis de qualquer
outro tipo). O bom disso que, como a poro de memria alocada contgua (ou seja, sem
buracos no meio), ela pode ser usada como se fosse um vetor comum.

7.2.1 Mos obra


Para pedir um bloco de memria, voc usar a funo malloc, passando como parmetro o
nmero de bytes de que voc precisa:

ponteiro = malloc(num_bytes);

No entanto, na maioria das vezes, queremos alocar espao para um certo nmero de
dados ou de elementos de um vetor. Resta ento saber o nmero de bytes que cada dado (ou
elemento) ocupa. Para isso, usaremos o operador sizeof, que diz quantos bytes ocupa uma
varivel de um certo tipo. Veja como ele usado a partir deste exemplo:

printf("O tamanho de um char %d bytes\n", sizeof(char));


printf("O tamanho de um int %d bytes\n", sizeof(int));
printf("O tamanho de um double %d bytes\n", sizeof(double));

Voc sempre deve usar o operador sizeof em vez de assumir, por exemplo, que o tama-
nho de um inteiro de 4 bytes. Isso poderia causar grandes problemas quando o programa
for transportado para um sistema em que o tamanho diferente, alm de no deixar claro o
que signica aquele 4 no meio do cdigo.
Dito isso, temos todo o material necessrio para realizar a alocao dinmica de um
vetor. Notemos que, por exemplo, se um inteiro ocupa 4 bytes, ento um vetor de n inteiros
ocupar 4n bytes, podemos escrever a rotina de alocao desta maneira:

int *v_int;
double *v_double;
char *v_char;
v_int = malloc(n * sizeof(int));
v_double = malloc(n * sizeof(double));
v_char = malloc(n * sizeof(char));

Note que o endereo devolvido pela funo malloc deve ser armazenado num ponteiro do
tipo apropriado; ou seja, o espao alocado para inteiros deve ser armazenado num ponteiro
int *, e assim por diante.
Se a memria tiver sido alocada com sucesso, poderemos acessar esses ponteiros como
vetores normais:

v_int[0] = -5;
v_int[1] = 4;
scanf("%d", &v_int[2]);
v_double[5] = (v_int[0] + v_int[1]) * 1.0/v_int[2];

Quando voc terminar de usar um bloco de memria alocado dinamicamente, voc deve
sempre liber-lo usando a funo free com o ponteiro correspondente como argumento,
como em free(ponteiro). Ao liberar um pedao de memria, o sistema operacional retoma
a guarda dele, permitindo que ele seja posteriormente usado por outros programas.
86 Captulo 7. Mais ponteiros

Da surge uma possvel fonte de erros: se voc tentar acessar um ponteiro depois que a
memria tiver sido liberada, essa memria poder estar sendo usada por outro programa, e
portanto no nada legal mexer nesse pedao de memria! Ponteiros que apontam para um
pedao de memria que foi desalocado so chamados, em ingls, de dangling pointers (lite-
ralmente, ponteiros pendentes), que vou preferir traduzir como ponteiros desapropriados.
Assim, ao liberar um bloco de memria, o ponteiro que apontava pra ele torna-se inv-
lido, e voc no deve tentar us-lo novamente (a menos que reaponte o ponteiro para outro
bloco de memria que voc esteja permitido a usar). Uma soluo que usada com certa
frequncia transformar o ponteiro em um ponteiro nulo aps a liberao da memria
um ponteiro nulo simplesmente um ponteiro que aponta para o endereo zero (a expres-
so NULL simplesmente um apelido para o nmero zero que no causa problemas quando
usada como endereo), que no usado para nenhuma rea vlida da memria. Um exemplo
disso o seguinte:

free(ponteiro);
ponteiro = NULL;

O mrito dessa soluo est no fato de ser muito mais fcil vericar se um ponteiro
nulo do que vericar se ele aponta para um lugar imprprio tanto dentro do seu programa
quanto pelo sistema operacional. Quando o programa tenta acessar um ponteiro nulo, o
sistema detecta a tentativa e encerra a execuo do programa; acessos a ponteiros desapro-
priados nem sempre podem ser detectados.

7.2.2 Falta de memria


Pode acontecer de haver algum erro e o sistema no conseguir alocar a memria que voc
pediu geralmente porque no h mais memria disponvel. Nessas situaes, a funo
malloc devolver um ponteiro nulo (NULL), que no aponta para regio nenhuma da memria;
coisas terrveis acontecero se voc tentar acess-lo (desreferenci-lo). Por isso, voc sempre
deve vericar se o ponteiro devolvido vlido, usando um cdigo parecido com esse:

ponteiro = malloc(tamanho);
if (ponteiro == NULL)
{
printf("Socorro! No foi possvel alocar memria!\n");
/* executar alguma ao para sair deste imbrglio */
}

Sendo essa uma tarefa comum, que ser executada vrias vezes no programa, til es-
crever uma funo que cuida desse trabalho repetitivo:

void *mallocX(size_t tamanho)


{
void *ponteiro;
ponteiro = malloc(tamanho);
if (ponteiro == NULL)
{
printf("Socorro! No foi possvel alocar memria!\n");

Tanto a ideia quanto o nome da funo foram inspirados nas notas do Prof. Paulo Feolo [1].
Ponteiros duplos 87

exit(EXIT_FAILURE);
}
}

A funo exit que foi aqui usada serve para terminar a execuo do programa imedia-
tamente, como se tivesse terminado a funo main. O argumento EXIT_FAILURE (uma cons-
tante denida pelo sistema) indica ao sistema operacional que houve algum erro na execu-
o do programa. O efeito dessa instruo equivalente a voltar funo main e escrever
return EXIT_FAILURE;. No entanto, usar a funo exit mais prtico pois assim no ne-
cessrio nos preocupar com a parte do programa em que estvamos.

7.3 Ponteiros duplos


Ainda no vimos como possvel alocar memria dinamicamente para uma matriz, ou qual-
quer vetor de mais de uma dimenso. Podemos pensar em duas maneiras de fazer isso; a
primeira delas uma no-soluo: para criar uma matriz de m linhas e n colunas, alocamos
um vetor de m n elementos, convencionando que o primeiro grupo de n elementos cor-
responde primeira linha, que o segundo corresponde segunda linha, e assim por diante.
Dessa maneira, para acessar um elemento (i, j) da matriz (usando 0 i < m e 0 j < n),
temos de fazer uma conta para descobrir em que posio k do vetor ele se encontra. Os
elementos j da primeira linha (i = 0) podem ser acessados simplesmente pelo ndice j; os
elementos j da segunda linha (i = 1) podem ser acessados somando n ao ndice j; prosse-
guindo assim, fcil concluir que o ndice linearizado do elemento (i, j) k = j + i n.
Da mesma maneira podemos proceder para vetores de d dimenses, de comprimentos
(n1 , . . . , nd ); os ndices so denotados (i1 , . . . , id ). O primeiro grupo de elementos corres-
ponde aos elementos com o primeiro ndice igual a zero (i1 = 0); dentro desse grupo, haver
vrios subgrupos, cada um correspondendo a um valor do segundo ndice (i2 ); e assim por
diante. No difcil completar o raciocnio para encontrar a frmula do ndice linearizado
do elemento (i1 , . . . , id ). (Veja a resposta no rodap.)
Essa maneira tem a vantagem de s necessitar de uma alocao de memria, com o
(pequeno) custo de precisar fazer uma conta para encontrar as posies de cada elemento.
Na verdade, ao alocar uma matriz estaticamente, dessa maneira que o computador trabalha:
com um grande bloco de memria, para o qual os ndices multidimensionais so linearizados
automaticamente pelo compilador.
Antes de prosseguir, vamos mostrar um exemplo simples de uso dessa tcnica:
void exemplo_matriz(int m, int n)
{
/* aloca a matriz de tamanho (m, n) */
int *matriz = mallocX(m*n * sizeof(int));
int i, j;

/* preenche cada elemento com a posio linearizada


correspondente */
for (i = 0; i < m; i++)
for (j = 0; j < n; j++)
matriz[j + i*n] = j + i*n;
( ( ))
k = id + nd id1 + nd1 (i2 + n2 i1 )
88 Captulo 7. Mais ponteiros

A outra maneira de criar uma matriz dinamicamente permite que ela seja acessada com
a mesma linguagem que as matrizes estticas (matriz[i][j]), porm aumentando a carga
computacional tanto do acesso aos elementos quanto da alocao da matriz. Para matrizes
bidimensionais (m, n), ela consiste em alocar m vetores de n elementos, e alocar um vetor
de m ponteiros no qual sero colocados os m vetores. importante notar que os elementos
desse ltimo vetor so ponteiros para o tipo de dados que queremos guardar, o que gera uma
pequena diferena no clculo do tamanho de cada elemento:
void exemplo_matriz(int m, int n)
{
/* aloca o vetor que guardar as m linhas */
int **matriz = mallocX(m * sizeof(int*));
int i, j;

/* aloca cada linha com n elementos */


for (i = 0; i < m; i++)
matriz[i] = mallocX(n * sizeof(int));

/* preenche cada elemento com a posio linearizada


correspondente */
for (i = 0; i < m; i++)
for (j = 0; j < n; j++)
matriz[i][j] = j + i*n;
}

A carga computacional da alocao evidente: em vez de fazer uma alocao, zemos


m+1 alocaes. J para acessar os elementos matriz[i][j], devemos observar, em primeiro
lugar, que essa expresso equivalente a (matriz[i])[j], ou seja, que primeiro devemos
olhar para o vetor de linhas para achar a posio de memria em que est a linha i (no h
nenhuma garantia de que as m linhas estejam guardadas em posies contguas), e depois ir
at essa posio e encontrar o j-simo elemento.
Dessa forma, bom um pouco de cuidado ao escolher essa segunda tcnica, pois, apesar
de mais fcil quanto ao acesso dos elementos, ela pode ser menos eciente. Isso no signica
que ela nunca deve ser usada; em cada caso pode-se escolher o que for mais apropriado.
Na verdade, podemos melhorar um pouco a segunda tcnica usando a ideia da primeira
tcnica de alocar um grande bloco de memria de mn elementos em vez de alocar m blocos
de n elementos. Dessa maneira, as m + 1 alocaes de memria reduzem-se a apenas duas,
com o custo de ter de calcular as localizaes de cada linha para inserir no vetor matriz.

7.4 Ponteiros para funes


Comeo com um exemplo simples: voc quer estimar a derivada de uma funo f num ponto
x0 , atravs de avaliaes sucessivas do quociente de Newton,
f(x) f(x0 )
,
x x0
para valores de x cada vez mais prximos de x0 . O algoritmo dessa operao bastante
simples, e o mesmo para qualquer funo f. Seria interessante, portanto, ter um mecanismo
Ponteiros para funes 89

de criar em C uma funo genrica calcula_derivada que zesse isso, dada uma funo f e
um ponto x0, bastando escrever um cdigo que fosse, esquematicamente, como a seguir:

calcula_derivada(f, x0);

Em C, o que passaramos para a funo calcula_derivada seria no a funo f em si,


mas um ponteiro para ela. Dessa maneira, a funo calcula_derivada poderia chamar a
funo f sem saber de antemo quem ela : ela apenas chamaria a funo que apontada
pelo ponteiro que foi dado.
Agora vamos ver como se faz isso.

7.4.1 Declarando um ponteiro para uma funo


A declarao de um ponteiro para uma funo pode parecer um pouco capciosa (de fato, no
das mais simples), mas no chega a ser um bicho-de-sete-cabeas. O caso mais simples
seria o de um ponteiro para uma funo sem parmetros e que devolve um valor de um certo
tipo. Sua declarao seria feita como:

tipo (*ponteiro)();

Os parnteses em torno de *ponteiro so absolutamente necessrios; se no os ussse-


mos, teramos (verique) uma declarao de uma funo que devolve um ponteiro algo
totalmente diferente!
Se a funo tiver parmetros, voc deve colocar seus tipos dentro do segundo par de
parnteses. Note bem: voc s deve colocar os tipos dos parmetros no coloque os
nomes. Por exemplo, se tivssemos uma funo que devolve um inteiro e recebe um inteiro
e um nmero real, um ponteiro para ela seria declarado assim:

int (*ponteiro)(int, float);

Obviamente, como voc j deve saber, um ponteiro no serve para nada se ele no aponta
para algo que conhecemos. Pois bem, para que um ponteiro aponte para uma funo, pro-
cedemos da mesma maneira que para os ponteiros para variveis:

ponteiro = &funcao;

Muitos compiladores (o GCC inclusive) permitem que voc omita o E comercial ao criar um pon-
teiro para uma funo. No entanto, recomendado que voc o escreva explicitamente para garantir
a mxima portabilidade do seu cdigo.

Talvez voc tenha achado estranho usarmos o endereo de uma funo. Mas, exatamente
como ocorre com as variveis, as funes so guardadas em algum lugar da memria quando
o programa carregado, de modo que elas tambm tm um endereo.

7.4.2 Chamando uma funo atravs de um ponteiro


Se voc tem um ponteiro que aponta para uma funo, voc pode chamar a funo desrefe-
renciando o ponteiro desta maneira:

Novamente, tudo isso necessrio porque o compilador precisa saber que parmetros a funo est esperando,
para poder fazer as manipulaes corretas dos dados na memria.
90 Captulo 7. Mais ponteiros

(*ponteiro)(parametro1 , parametro2 , ...);

Novamente, voc no deve se esquecer dos parnteses em volta de *ponteiro eles


esto l para indicar que a funo a ser chamada a funo resultante da desreferenciao
de ponteiro; se esquecssemos dos parnteses, estaramos chamando a funo ponteiro e
desreferenciando o valor por ela devolvido. Se voc quer simplicar sua vida, voc pode usar
a sintaxe abreviada (apenas para a chamada), que escrita exatamente como uma chamada
de funo comum:

ponteiro(parametro1 , parametro2 , \ldots);

Voc tambm pode, naturalmente, capturar o valor da funo, da maneira usual:

x = (*ponteiro)(parametro1 , parametro2 , ...);


x = ponteiro(parametro1 , parametro2 , ...);

7.4.3 Ponteiros para funes como parmetros de funes


Uma das grandes utilidades dos ponteiros para funes reside na possibilidade de pass-los
entre funes, como parmetros. Uma vez compreendida a maneira de declarar e acessar
ponteiros para funes, no difcil us-los como parmetros. No cabealho da funo, um
ponteiro para funo especicado da mesma maneira que seria declarada uma varivel do
mesmo tipo. Veja o exemplo:

int operacao(int a, int b, int (*funcao)(int, int))


{
return (*funcao)(a, b);
}

7.5 Escopo de variveis


7.6 Funes recursivas
Estruturas
8
8.1 Structs
Em cada parte de um programa geralmente h vrias variveis associadas realizao de
uma tarefa especca. Por causa disso, conveniente ter um modo de agrupar um conjunto
de variveis relacionadas. Conhecemos anteriormente os vetores, que so agrupamentos de
uma srie de variveis do mesmo tipo, cada uma identicada por um nmero.
Se, por outro lado, quisermos um tipo de agrupamento que englobe variveis de tipos
diferentes, ou no qual cada varivel possa ser identicada por um nome especco, usamos
um tipo de estrutura chamado de registro (mais conhecido por seu nome em ingls, struct,
uma abreviao de structure, estrutura).
Esse recurso da linguagem C permite que o usurio dena seus prprios tipos de dados
a partir dos tipos primitivos da linguagem. Esse tipo de estrutura um exemplo de tipo de
dados composto o segundo a ser apresentado aqui, os vetores tendo sido o primeiro.
Um struct em C funciona de maneira similar a um registro (uma linha) em um banco de
dados: ele contm um conjunto de variveis, que tm tipos xados e so identicadas por
nomes (como as variveis comuns). Por exemplo, um registro que representa um produto
numa compra pode conter as seguintes variveis:

char *descricao;
int quantidade;
double preco_unitario;
double desconto;
double preco_total;

Essas variveis so denominadas campos ou membros do registro. O conjunto de nomes


e tipos dos campos constitui um tipo de registro. Cada registro em si uma varivel, e tem
boa parte dos privilgios de uma outra varivel qualquer.
Um registro declarado usando a palavra-chave struct seguida de um bloco (delimitado
por chaves) contendo as declaraes dos membros, como se fossem declaraes de variveis
comuns. Um registro como exemplicado acima poderia ser declarado como no cdigo a
seguir, em que ele armazenando numa varivel chamada produto:

struct {
char *descricao;

91
92 Captulo 8. Estruturas

int quantidade;
double preco_unitario;
double desconto;
double preco_total;
} produto;

Podemos usar esse tipo de comando para declarar vrios registros ao mesmo tempo,
colocando vrios nomes de varivel em vez de apenas um (separando por vrgulas). Contudo,
essa forma de declarao no permite reutilizar o mesmo tipo de registro em outro comando
posterior.
Para resolver essa inconvenincia, basta dar um nome ao tipo de registro: iniciamos sua
declarao por struct nome (substituindo o nome escolhido, que segue as mesmas regras de
sempre para nomes de coisas em C), em vez de apenas struct assim, declaramos um tipo
de registro, no apenas um registro. Da em diante, s digitar struct nome para se referir
ao tipo j declarado.
Tambm possvel declarar o tipo sem declarar nenhuma varivel desse tipo, bastando
para isso no colocar nenhum nome de varivel; necessrio manter o ponto-e-vrgula, no
entanto. Com essa possibilidade, interessante separar em comandos diferentes a declarao
do tipo (que ca na parte exterior do programa) e as declaraes de variveis desse tipo (que
cam no lugar certo para cada varivel), como no exemplo a seguir:

/* Declarao do tipo info_produto */


struct info_produto {
char *descricao;
int quantidade;
double preco_unitario;
double desconto;
double preco_total;
};

/* Declarao de duas variveis desse tipo */


struct info_produto produtoA , produtoB;

Para acessar campos de um registro, usamos o operador . (um ponto), colocando


esquerda dele o nome da varivel que contm o registro, e direita o nome do campo.
Ainda no exemplo anterior, podemos acessar o preo do produto A usando a expresso
produtoA.preco_unitario, como se fosse uma varivel comum.
Podemos, por exemplo, tomar o endereo de um membro de um registro e com isso criar
um apontador que aponte para esse membro. No exemplo a seguir, utilizamos uma funo
que, supostamente, calcula o preo total de um item (dadas as informaes necessrias nos
trs primeiros argumentos) e o armazena na varivel apontada pelo quarto argumento.

calcular_preco_total(produtoA.preco_unitario , produtoA.-
quantidade , produtoA.desconto , &produtoA.preco_total);

8.1.1 Registros e ponteiros


Listas ligadas 93

Resumo
Declarao de um tipo de registro

struct nome_do_tipo {
/* declaraes dos membros */
};

Declarao de um registro

struct nome_do_tipo nome_da_variavel;

Acesso de membros de um registro

variavel.nome_do_membro

8.2 Listas ligadas


Compilao
A

95
Tabelas de referncia
B

97
Referncias Bibliogr cas

[1] Paulo Feolo, Projeto de Algoritmos, http://www.ime.usp.br/~pf/algoritmos/


[2] Brian W. Kernighan, Dennis M. Ritchie, The C Programming Language, Prentice Hall,
second edition, 1988.

99

Você também pode gostar