LINGUAGEM C:
DESCOMPLICADA
Prof. André R. Backes
SUMÁRIO
1 Introdução
9
1.1 A linguagem C . . . . . . . . . . . . . . . . . . . . . . . . . .
9
1.1.1 Influência da linguagem C . . . . . . . . . . . . . . . .
9
1.2 Utilizando o Code::Blocks para programar em C . . . . . . . 11
1.2.1 Criando um novo projeto no Code::Blocks . . . . . . . 11
1.2.2 Utilizando o debugger do Code::Blocks . . . . . . . . 15
1.3 Esqueleto de um programa em linguagem C . . . . . . . . . 19
1.3.1 Indentação do código . . . . . . . . . . . . . . . . . . 21
1.3.2 Comentários . . . . . . . . . . . . . . . . . . . . . . . 22
1.4 Bibliotecas e funções úteis da linguagem C . . . . . . . . . . 23
1.4.1 O comando #include . . . . . . . . . . . . . . . . . . . 23
1.4.2 Funções de entrada e saı́da: stdio.h . . . . . . . . . . 24
1.4.3 Funções de utilidade padrão: stdlib.h
. . . . . . . . . 26
1.4.4 Funções matemáticas: math.h . . . . . . . . . . . . . 28
1.4.5 Testes de tipos de caracteres: ctype.h . . . . . . . . . 29
1.4.6 Operações em String: string.h . . . . . . . . . . . . . 29
1.4.7 Funções de data e hora: time.h . . . . . . . . . . . . . 30
2 Manipulando dados, variáveis e expressões em C
32
2.1 Variáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.1.1 Nomeando uma variável . . . . . . . . . . . . . . . . . 33
2.1.2 Definindo o tipo de uma variável . . . . . . . . . . . . 35
2.2 Lendo e escrevendo dados . . . . . . . . . . . . . . . . . . . 39
2.2.1 Printf
. . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1
2.2.2 Putchar . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.2.3 Scanf . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.2.4 Getchar . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.3 Escopo: tempo de vida da variável . . . . . . . . . . . . . . . 47
2.4 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.4.1 Comando #define . . . . . . . . . . . . . . . . . . . . 53
2.4.2 Comando const . . . . . . . . . . . . . . . . . . . . . . 53
2.4.3 seqüências de escape . . . . . . . . . . . . . . . . . . 54
2.5 Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.5.1 Operador de atribuição: “=” . . . . . . . . . . . . . . . 55
2.5.2 Operadores aritméticos . . . . . . . . . . . . . . . . . 58
2.5.3 Operadores relacionais . . . . . . . . . . . . . . . . . 60
2.5.4 Operadores lógicos . . . . . . . . . . . . . . . . . . . 62
2.5.5 Operadores bit-a-bit . . . . . . . . . . . . . . . . . . . 63
2.5.6 Operadores de atribuição simplificada . . . . . . . . . 66
2.5.7 Operadores de Pré e Pós-Incremento . . . . . . . . . 67
2.5.8 Modeladores de Tipos (casts) . . . . . . . . . . . . . . 69
2.5.9 Operador vı́rgula “,” . . . . . . . . . . . . . . . . . . . 70
2.5.10 Precedência de operadores . . . . . . . . . . . . . . . 71
3 Comandos de Controle Condicional
73
3.1 Definindo uma condição . . . . . . . . . . . . . . . . . . . . . 73
3.2 Comando if . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.2.1 Uso das chaves {} . . . . . . . . . . . . . . . . . . . . 78
3.3 Comando else . . . . . . . . . . . . . . . . . . . . . . . . . . 79
3.4 Aninhamento de if . . . . . . . . . . . . . . . . . . . . . . . . 83
2
3.5 Operador ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.6 Comando switch . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.6.1 Uso do comando break no switch . . . . . . . . . . . . 91
3.6.2 Uso das chaves {}no case . . . . . . . . . . . . . . . 94
4 Comandos de Repetição
96
4.1 Repetição por condição . . . . . . . . . . . . . . . . . . . . . 96
4.1.1 Laço infinito . . . . . . . . . . . . . . . . . . . . . . . . 97
4.2 Comando while . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.3 Comando for . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
4.3.1 Omitindo uma clausula do comando for . . . . . . . . 104
4.3.2 Usando o operador de vı́rgula (,) no comando for . . . 107
4.4 Comando do-while . . . . . . . . . . . . . . . . . . . . . . . . 109
4.5 Aninhamento de repetições . . . . . . . . . . . . . . . . . . . 112
4.6 Comando break
. . . . . . . . . . . . . . . . . . . . . . . . . 113
4.7 Comando continue . . . . . . . . . . . . . . . . . . . . . . . . 115
4.8 Goto e label . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
5 Vetores e matrizes - Arrays
119
5.1 Exemplo de uso . . . . . . . . . . . . . . . . . . . . . . . . . 119
5.2 Array com uma dimensão - vetor . . . . . . . . . . . . . . . . 120
5.3 Array com duas dimensões - matriz . . . . . . . . . . . . . . 124
5.4 Arrays multidimensionais . . . . . . . . . . . . . . . . . . . . 125
5.5 Inicialização de arrays . . . . . . . . . . . . . . . . . . . . . . 127
5.5.1 Inicialização sem tamanho . . . . . . . . . . . . . . . 129
5.6 Exemplo de uso de arrays . . . . . . . . . . . . . . . . . . . . 130
3
6 Arrays de caracteres - Strings
133
6.1 Definição e declaração de uma string . . . . . . . . . . . . . 133
6.1.1 Inicializando uma string . . . . . . . . . . . . . . . . . 134
6.1.2 Acessando um elemento da string . . . . . . . . . . . 134
6.2 Trabalhando com strings . . . . . . . . . . . . . . . . . . . . . 135
6.2.1 Lendo uma string do teclado . . . . . . . . . . . . . . 136
6.2.2 Escrevendo uma string na tela . . . . . . . . . . . . . 139
6.3 Funções para manipulação de strings . . . . . . . . . . . . . 140
6.3.1 Tamanho de uma string . . . . . . . . . . . . . . . . . 140
6.3.2 Copiando uma string . . . . . . . . . . . . . . . . . . . 141
6.3.3 Concatenando strings . . . . . . . . . . . . . . . . . . 142
6.3.4 Comparando duas strings . . . . . . . . . . . . . . . . 142
7 Tipos definidos pelo programador
144
7.1 Estruturas: struct . . . . . . . . . . . . . . . . . . . . . . . . . 144
7.1.1 Inicialização de estruturas . . . . . . . . . . . . . . . . 149
7.1.2 Array de estruturas . . . . . . . . . . . . . . . . . . . . 150
7.1.3 Atribuição entre estruturas . . . . . . . . . . . . . . . 152
7.1.4 Estruturas aninhadas . . . . . . . . . . . . . . . . . . 153
7.2 Uniões: union . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
7.3 Enumarações: enum . . . . . . . . . . . . . . . . . . . . . . . 158
7.4 Comando typedef
. . . . . . . . . . . . . . . . . . . . . . . . 163
8 Funções
167
8.1 Definição e estrutura básica . . . . . . . . . . . . . . . . . . . 167
8.1.1 Declarando uma função . . . . . . . . . . . . . . . . . 168
8.1.2 Parâmetros de uma função . . . . . . . . . . . . . . . 171
4
8.1.3 Corpo da função . . . . . . . . . . . . . . . . . . . . . 173
8.1.4 Retorno da função . . . . . . . . . . . . . . . . . . . . 176
8.2 Tipos de passagem de parâmetros . . . . . . . . . . . . . . . 181
8.2.1 Passagem por valor . . . . . . . . . . . . . . . . . . . 182
8.2.2 Passagem por referência . . . . . . . . . . . . . . . . 183
8.2.3 Passagem de arrays como parâmetros . . . . . . . . 186
8.2.4 Passagem de estruturas como parâmetros . . . . . . 190
8.2.5 Operador Seta . . . . . . . . . . . . . . . . . . . . . . 193
8.3 Recursão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
9 Ponteiros
200
9.1 Declaração . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
9.2 Manipulando ponteiros . . . . . . . . . . . . . . . . . . . . . . 202
9.2.1 Inicialização e atribuição . . . . . . . . . . . . . . . . . 202
9.2.2 Aritmética com ponteiros . . . . . . . . . . . . . . . . 208
9.2.3 Operações relacionais com ponteiros . . . . . . . . . 211
9.3 Ponteiros genéricos . . . . . . . . . . . . . . . . . . . . . . . 213
9.4 Ponteiros e arrays . . . . . . . . . . . . . . . . . . . . . . . . 215
9.4.1 Ponteiros e arrays multidimensionais . . . . . . . . . . 219
9.4.2 Array de ponteiros . . . . . . . . . . . . . . . . . . . . 220
9.5 Ponteiro para ponteiro . . . . . . . . . . . . . . . . . . . . . . 221
10 Alocação Dinâmica
225
10.1 Funções para alocação de memória . . . . . . . . . . . . . . 227
10.1.1 sizeof() . . . . . . . . . . . . . . . . . . . . . . . . . . 227
10.1.2 malloc() . . . . . . . . . . . . . . . . . . . . . . . . . . 228
10.1.3 calloc() . . . . . . . . . . . . . . . . . . . . . . . . . . 231
5
10.1.4 realloc() . . . . . . . . . . . . . . . . . . . . . . . . . . 233
10.1.5 free() . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
10.2 Alocação de arrays multidimensionais . . . . . . . . . . . . . 238
10.2.1 Solução 1: usando array unidimensional . . . . . . . . 238
10.2.2 Solução 2: usando ponteiro para ponteiro . . . . . . . 240
10.2.3 Solução 3: ponteiro para ponteiro para array . . . . . 244
11 Arquivos
248
11.1 Tipos de Arquivos . . . . . . . . . . . . . . . . . . . . . . . . 248
11.2 Sobre escrita e leitura em arquivos . . . . . . . . . . . . . . . 250
11.3 Ponteiro para arquivo . . . . . . . . . . . . . . . . . . . . . . 251
11.4 Abrindo e fechando um arquivo . . . . . . . . . . . . . . . . . 251
11.4.1 Abrindo um arquivo . . . . . . . . . . . . . . . . . . . 251
11.4.2 Fechando um arquivo . . . . . . . . . . . . . . . . . . 256
11.5 Escrita e leitura em arquivos . . . . . . . . . . . . . . . . . . 257
11.5.1 Escrita e leitura de caractere . . . . . . . . . . . . . . 257
11.5.2 Fim do arquivo . . . . . . . . . . . . . . . . . . . . . . 261
11.5.3 Arquivos pré-definidos . . . . . . . . . . . . . . . . . . 262
11.5.4 Forçando a escrita dos dados do “buffer” . . . . . . . 263
11.5.5 Sabendo a posição atual dentro do arquivo . . . . . . 264
11.5.6 Escrita e leitura de strings . . . . . . . . . . . . . . . . 265
11.5.7 Escrita e leitura de blocos de bytes . . . . . . . . . . . 269
11.5.8 Escrita e leitura de dados formatados . . . . . . . . . 277
11.6 Movendo-se dentro do arquivo . . . . . . . . . . . . . . . . . 282
11.7 Excluindo um arquivo . . . . . . . . . . . . . . . . . . . . . . 284
11.8 Erro ao acessar um arquivo . . . . . . . . . . . . . . . . . . . 285
6
12 Avançado
287
12.1 Diretivas de compilação . . . . . . . . . . . . . . . . . . . . . 287
12.1.1 O comando #include . . . . . . . . . . . . . . . . . . . 287
12.1.2 Definindo macros: #define e #undef . . . . . . . . . . 287
12.1.3 Diretivas de Inclusão Condicional . . . . . . . . . . . . 294
12.1.4 Controle de linha: #line . . . . . . . . . . . . . . . . . 297
12.1.5 Diretiva de erro: #error . . . . . . . . . . . . . . . . . . 298
12.1.6 Diretiva #pragma . . . . . . . . . . . . . . . . . . . . . 298
12.1.7 Diretivas pré-definidas . . . . . . . . . . . . . . . . . . 299
12.2 Trabalhando com Ponteiros . . . . . . . . . . . . . . . . . . . 299
12.2.1 Array de Ponteiros e Ponteiro para array . . . . . . . . 299
12.2.2 Ponteiro para função . . . . . . . . . . . . . . . . . . . 300
12.3 Argumentos na linha de comando . . . . . . . . . . . . . . . 308
12.4 Recursos avançados da função printf() . . . . . . . . . . . . . 311
12.4.1 Os tipos de saı́da
. . . . . . . . . . . . . . . . . . . . 312
12.4.2 As “flags” para os tipos de saı́da . . . . . . . . . . . . 317
12.4.3 O campo “largura” dos tipos de saı́da . . . . . . . . . 320
12.4.4 O campo “precisão” dos tipos de saı́da . . . . . . . . 320
12.4.5 O campo “comprimento” dos tipos de saı́da . . . . . . 323
12.4.6 Usando mais de uma linha na função printf() . . . . . 323
12.5 Recursos avançados da função scanf() . . . . . . . . . . . . 324
12.5.1 Os tipos de entrada . . . . . . . . . . . . . . . . . . . 325
12.5.2 O campo asterisco “*” . . . . . . . . . . . . . . . . . . 329
12.5.3 O campo “largura” dos tipos de entrada . . . . . . . . 329
12.5.4 Os “modificadores” dos tipos de entrada . . . . . . . . 330
12.5.5 Lendo e descartando caracteres . . . . . . . . . . . . 331
7
12.5.6 Lendo apenas caracteres pré-determinados . . . . . . 332
12.6 Classes de Armazenamento de Variáveis . . . . . . . . . . . 333
12.6.1 A Classe auto . . . . . . . . . . . . . . . . . . . . . . . 334
12.6.2 A Classe extern . . . . . . . . . . . . . . . . . . . . . 334
12.6.3 A Classe static . . . . . . . . . . . . . . . . . . . . . . 335
12.6.4 A Classe register . . . . . . . . . . . . . . . . . . . . . 337
12.7 Trabalhando com campos de bits . . . . . . . . . . . . . . . . 338
12.8 O Modificador de tipo “volatile” . . . . . . . . . . . . . . . . . 340
12.9 Funções com número de parâmetros variável . . . . . . . . . 342
8
1
INTRODUÇÃO
1.1
A LINGUAGEM C
A linguagem C é uma das mais bem sucedidas linguagens de alto nı́vel já
criadas e considerada uma das linguagens de programação mais utilizadas de todos os tempos. Define-se como linguagem de alto nı́vel aquela
que possui um nı́vel de abstração relativamente elevado, que está mais
próximo da linguagem humana do que do código de máquina. Ela foi criada em 1972 nos laboratórios Bell por Dennis Ritchie, sendo revisada e padronizada pela ANSI (Instituto Nacional Americano de Padrões, do inglês
American National Standards Institute) em 1989.
Trata-se de uma linguagem estruturalmente simples e de grande portabilidade. Poucas são as arquiteturas de computadores para que um compilador C não exista. Além disso, o compilador da linguagem gera códigos
mais enxutos e velozes do que muitas outras linguagens.
A linguagem C é uma linguagem procedural, ou seja, ela permite que um
problema complexo seja facilmente decomposto em módulos, onde cada
módulo representa um problema mais simples. Além disso, ela fornece
acesso de baixo nı́vel à memória, o que permite o acesso e a programação
direta do microprocessador. Ela também permite a implementação de programas utilizando instruções em Assembly, o que permite programar problemas onde a dependência do tempo é critica.
Por fim, a linguagem C foi criada para incentivar a programação multiplataforma, ou seja, programas escritos em C podem ser compilado para
uma grande variedade de plataformas e sistemas operacionais com apenas pequenas alterações no seu código fonte.
1.1.1
INFLUÊNCIA DA LINGUAGEM C
A linguagem C tem influenciado, direta ou indiretamente, muitas linguagem
desenvolvidas posteriormente, tais como C++, Java, C# e PHP. Na figura
abaixo é possı́vel ver uma bre história da evolução da linguagem C e de
sua influência no desenvolvimentos de outras linguagens de programação:
9
Provavelmente, a influência mais marcante da linguagem foi a sua sintática:
todas as linguagem mencionadas combinam a sintaxe de declaração e a
sintaxe da expressão da linguagem C com sistemas de tipo, modelos de
dados, etc. A figura abaixo mostra como um comando de impressão de
números variando de 1 até 10 pode ser implementado em diferentes linguagens:
10
1.2
UTILIZANDO O CODE::BLOCKS PARA PROGRAMAR EM C
Existem diversos ambientes de desenvolvimento integrado ou IDE’s (do
inglês, Integrated Development Environment) que podem ser utilizados para
a programação em linguagem C. Um deles é o Code::Blocks, uma IDE
de código aberto e multiplataforma que suporta mútiplos compiladores. O
Code::Blocks pode ser baixado diretamente de seu site
www.codeblocks.org
ou pelo link
prdownload.berlios.de/codeblocks/codeblocks-10.05mingw-setup.exe
esse último inclui tanto a IDE do Code::Blocks como o compilador GCC e
o debugger GDB da MinGW.
1.2.1
CRIANDO UM NOVO PROJETO NO CODE::BLOCKS
Para criar um novo projeto de um programa no software Code::Blocks,
basta seguir os passos abaixo:
1. Primeiramente, inicie o software Code::Blocks (que já deve estar
instalado no seu computador). A tela abaixo deverá aparecer;
2. Em seguida clique em “File”, e escolha “New” e depois “Project...”;
11
3. Uma lista de modelos (templates) de projetos irá aparecer. Escolha
“Console aplication”;
4. Caso esteja criando um projeto pela primeira vez, a tela abaixo irá
aparecer. Se marcarmos a opção “Skip this page next time”, essa
tela de bias vindas não será mais exibida da próxima vez que criarmos um projeto. Em seguinda, clique em “Next”;
12
5. Escolha a opção “C” e clique em “Next”;
6. No campo “Project title”, coloque um nome para o seu projeto. No
campo “Folder to create project in” é possı́vel selecionar onde o
projeto será salvo no computador. Clique em “Next” para continuar;
13
7. Na tela a seguir, algumas configurações do compilador podem ser
modificados. No entanto, isso não será necessário. Basta clicar em
“Finish”;
8. Ao fim desses passos, o esqueleto de um novo programa C terá sido
criado, como mostra a figura abaixo:
14
9. Por fim, podemos utilizar as seguintes opções do menu “Build” para
compilar e executar nosso programa:
• Compile current file (Ctrl+Shift+F9): essa opção irá ransformar seu arquivo de código fonte em instruções de máquina e
gerar um arquivo do tipo objeto;
• Build (Ctrl+F9): serão compilados todos os arquivos do seu
projeto e fazer o processo de linkagem com tudo que é necessário para gerar o executável do seu programa;
• Build and run (F9): além de gerar o executável, essa opção
também executa o programa gerado.
1.2.2
UTILIZANDO O DEBUGGER DO CODE::BLOCKS
Com o passar do tempo, nosso conhecimento sobre programação cresce,
assim como a complexidade de nossos programas. Surge então a necessidade de examinar o nosso programa a procura de erros ou defeitos
no código fonte. para realizar essa tarefa, contamos com a ajuda de um
depurador ou debugger.
O debugger nada mais é do que um programa de computador usado para
testar e depurar (limpar, purificar) outros programas. Dentre as principais
funcionalidades de um debugger estão:
• a possibilidade de executar um programa passo-a-passo;
• pausar o programa em pontos pré-definidos, chamados pontos de
parada ou breakpoints, para examinar o estado atual de suas variáveis.
15
Para utilizar o debugger do Code::Blocks, imagine o seguinte código abaixo:
Exemplo: código para o debugger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# include <s t d i o . h>
# include < s t d l i b . h>
int f a t o r i a l ( int n){
int i , f = 1;
f o r ( i = 1 ; i <= n ; i ++)
f = f ∗ i;
return f ;
}
i n t main ( ) {
int x , y ;
p r i n t f ( ‘ ‘ D i g i t e um v a l o r i n t e i r o : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
i f ( x > 0) {
p r i n t f ( ‘ ‘ X eh p o s i t i v o \n ’ ’ ) ;
y = fatorial (x) ;
p r i n t f ( ‘ ‘ F a t o r i a l de X eh %d\n ’ ’ , y ) ;
} else {
i f ( x < 0)
p r i n t f ( ‘ ‘ X eh n e g a t i v o \n ’ ’ ) ;
else
p r i n t f ( ‘ ‘ X eh Zero \n ’ ’ ) ;
}
p r i n t f ( ‘ ‘ Fim do programa ! \ n ’ ’ ) ;
system ( pause ) ;
return 0;
}
Todas as funcionalidades do debugger podem ser encontradas no menu
Debug. Um progama pode ser facilmente depurado seguindo os passos
abaixo:
1. Primeiramente, vamos colocar dois pontos de parada ou breakpoints
no programa, nas linhas 13 e 23. Isso pode ser feito de duas maneiras: clicando do lado direito do número da linha, ou colocando-se o
cursor do mouse na linha que se deseja adicionar o breakpoint e
selecionar a opção Toggle breakpoint (F5). Um breakpoint é identificado por uma bolinha vermelha na linha;
16
2. Iniciamos o debugger com a opção Start (F8). Isso fará com que o
programa seja executado normalmente até encontrar um breakpoint.
No nosso exemplo, o usuário deverá digitar, no console, o valor lido
pelo comando scanf() e depois retornar para a tela do Code::Blocks
onde o programa se encontra pausado. Note que existe um triângulo
amarelo dentro do primeiro breakpoint. Esse triângulo indica em
que parte do programa a pausa está;
3. Dentro da opção Debugging windows, podemos habilitar a opção
Watches. Essa opção irá abrir uma pequena janela que permite ver
o valor atual das variáveis de um programa, assim como o valor pas17
sado para funções. Outra maneira de acessar a janela Watches é
mudar a perspectiva do software para a opção Debugging, no menu
View, Perspectives;
4. A partir de um determinado ponto de pausa do programa, podemos
nos mover para a próxima linha do programa com a opção Next line
(F7). Essa opção faz com que o programa seja executado passo-apasso, sempre avançando para a linha seguinte do escopo onde nos
encontramos;
5. Frequentemente, pode haver uma chamada a uma função construı́da
pelo programador em nosso código, como é o caso da função fatorial(). A opção Next line (F7) chama a função, mas não permite que
a estudemos passo-a-passo. Para entrar dentro do código de uma
função utilizamos a opção Step into (Shift+F7) na linha da chamada
da função. Nesse caso, o triângulo amarelo que marca onde estamos
no código vai para a primeira linha do código da função (linha 4);
18
6. Uma vez dentro de uma função, podemos percorrê-la passo-a-passo
com a opção Next line (F7). Terminada a função, o debugger vai
para a linha seguinte ao ponto do código que chamou a função (linha
16). Caso queiramos ignorar o resto da função e voltar para onde
estavamos no código que chamou a função, basta clicar na opção
Step out (Shift+Ctrl+F7);
7. Para avançar todo o código e ir direto para o próximo breakpoint,
podemos usar a opção Continue (Ctrl+F7);
8. Por fim, para parar o debugger, basta clicar na opção Stop debugger.
1.3
ESQUELETO DE UM PROGRAMA EM LINGUAGEM C
Todo programa escrito em linguagem C que vier a ser desenvolvido deve
possuir o seguinte esqueleto:
Primeiro programa em linguagem C
1
2
3
4
5
6
7
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
p r i n t f ( ‘ ‘ H e l l o World \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
19
A primeira vista este parece ser um programa fútil, já que sua única finalidade é mostrar na tela uma mensagem dizendo Hello World, fazer uma
pausa, e terminar o programa. Porém, ele permite aprender alguns dos
conceitos básicos da lingaugem C, como mostra a figura abaixo:
Abaixo, é apresentada uma descrição mais detalhada do esqueleto do programa:
• Temos, no inı́cio do programa, a região onde são feitas as declarações
globais do programa, ou seja, aquelas que são válidas para todo o
programa. No exemplo, o comando #include <nome da biblioteca>
é utilizado para declarar as bibliotecas que serão utilizadas pelo programa. Uma biblioteca é um conjunto de funções (pedaços de código)
já implementados e que podem ser utilizados pelo programador. No
exemplo anterior, duas bibliotecas foram adicionadas ao programa:
stdio.h (que contém as funções de leitura do teclado e escrita em
tela) e stdlib.h;
• Todo o programa em linguagem C deve conter a função main(). Esta
função é responsável pelo inı́cio da execução do programa, e é dentro dela que iremos colocar os comandos que queremos que o programa execute.
• As chaves definem o inı́cio “{” e o fim “}” de um bloco de comandos / instruções. No exemplo, as chaves definem o inı́cio e o fim do
programa;
• A função main foi definida como uma função int (ou seja, inteira), e
por isso precisa devolver um valor inteiro. Temos então a necessi20
dade do comando return 0, apenas para informar que o programa
chegou ao seu final e que está tudo OK;
• A função printf() está definida na biblioteca stdio.h. Ela serve para
imprimir uma mensagem de texto na tela do computador (ou melhor,
em uma janela MSDOS ou shell no Linux). O texto a ser escrito deve
estar entre “aspas duplas”, e dentro dele podemos também colocar
caracteres especiais, como o “\n”, que indica que é para mudar de
linha antes de continuar a escrever na tela;
• O comando system(“pause”) serve para interromper a execução do
programa (fazer uma pausa) para que você possa analisar a tela de
saı́da, após o término da execução do programa. Ela está definida
dentro da biblioteca stdlib.h;
• A declaração de um comando quase sempre termina com um ponto
e vı́rgula “;”. Nas próximas seções, nós veremos quais os comandos
que não terminam com um ponto e vı́rgula “;”;
• Os parênteses definem o inı́cio “(” e o fim “)” da lista de argumentos
de uma função. Um argumento é a informação que será passada
para a função agir. No exemplo, podemos ver que os comandos
main, printf e system, são funções;
1.3.1
INDENTAÇÃO DO CÓDIGO
Outra coisa importante que devemos ter em mente quando escrevemos
um programa é a indentação do código. Trata-se de uma convensão de
escrita de códigos fonte que visa modificar a estética do programa para
auxiliar a sua leitura e interpretação.
A indentação torna a leitura do código fonte muito mais fácil
e facilita a sua modificação.
A indentação é o espaçamento (ou tabulação) colocado antes de começar
a escrever o código na linha. Ele tem como objetico indicar a hierarquia
do elementos. No nosso exemplo, os comandos printf, system e return
possuem a mesma hierarquia (portanto o mesmo espaçamento) e estão
todos contidos dentro do comando main() (daı́ o porquê do espaçamento).
O ideal é sempre criar um novo nı́vel de indentação para
um novo bloco de comandos.
21
A indentação é importante pois o nosso exemplo anterior poderia ser escrito em apenas três linhas, sem afetar o seu desempenho, mas com um
alto grau de dificuldade de leitura para o programador:
Programa sem indentação
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) { p r i n t f ( ‘ ‘ H e l l o World \n ’ ’ ) ; system ( ‘ ‘ pause ’ ’ )
; return 0;}
1.3.2
COMENTÁRIOS
Um comentário, como seu próprio nome diz, é um trecho de texto incluı́do
dentro do programa para descrever alguma coisa, por exemplo, o que
aquele pedaço do programa faz. Os comentários não modificam o funcionamento do programa pois são ignorados pelo compilador e servem,
portanto, apenas para ajudar o programador a organizar o seu código.
Um comentário pode ser adicionado em qualquer parte do código. Para
tanto, a linguagem C permite fazer comentários de duas maneiras diferentes: por linha ou por bloco.
• Se o programador quiser comentar uma única linha do código, basta
adicionar // na frente da linha. Tudo o que vier na linha depois do //
será considerado como comentário e será ignorado pelo compilador.
• Se o programador quiser comentar mais de uma linha do código, isto
é, um bloco de linhas, basta adicionar /* no começo da primeira linha
de comentário e */ no final da última linha de comentário. Tudo o que
vier depois do sı́mbolo de /* e antes do */ será considerado como
comentário e será ignorado pelo compilador.
Abaixo, tem-se alguns exemplos de comentários em um programa:
22
Exemplo: comentários no programa
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
/∗
Escreve
na
tela
∗/
p r i n t f ( ‘ ‘ H e l l o World \n ’ ’ ) ;
/ / f a z uma pausa no programa
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Outro aspecto importante do uso dos comentários é que eles permitem fazer a documentação interna de um programa, ou seja, permitem descrever
o que cada bloco de comandos daquele programa faz. A documentação
é uma tarefa extremamente importante no desenvolvimento e manutenção
de um programa, mas muitas vezes ignoradas.
Os comentários dentro de um código permitem que um programador entenda muito mais rapidamente um código que nunca tenha visto ou que ele
relembre o que faz um trecho de código a muito tempo implementado por
ele. Além disso, saber o que um determinado trecho de código realmente
faze aumenta as possibilidades de reutilizá-lo em outras aplicações.
1.4
BIBLIOTECAS E FUNÇÕES ÚTEIS DA LINGUAGEM C
1.4.1
O COMANDO #INCLUDE
O comando #include é utilizado para declarar as bibliotecas que serão
utilizadas pelo programa.
Uma biblioteca é um arquivo contendo um conjunto de
funções (pedaços de código) já implementados e que podem ser utilizados pelo programador em seu programa.
Esse comando diz ao pré-processador para tratar o conteúdo de um arquivo especificado como se o seu conteúdo houvesse sido digitado no
programa no ponto em que o comando #include aparece.
23
O comando #include permite duas sintaxes:
• #include <nome da biblioteca>: o pré-processador procurará pela
biblioteca nos caminhos de procura pré-especificados do compilador.
Usa-se essa sintaxe quando estamos incluindo uma biblioteca que é
própria do sistema, como as biblotecas stdio.h e stdlib.h;
• #include “nome da biblioteca”: o pré-processador procurará pela
biblioteca no mesmo diretório onde se encontra o nosso programa.
Podemos ainda optar por informar o nome do arquivo com o caminho
completo, ou seja, em qual diretório ele se encontra e como chegar
até lá.
De modo geral, os arquivos de bibliotecas na linguagem C
são terminados com a extensão .h.
Abaixo temos dois exemplos do uso do comando #include:
#include <stdio.h>
#include “D:\Programas\soma.h”
Na primeira linha, o comando #include é utilizado para adicionar uma biblioteca do sistema: stdio.h (que contém as funções de leitura do teclado e
escrita em tela). Já na segunda linha, o comando é utilizado para adicionar
uma biblioteca de nome soma.h, localizada no diretório “D:\Programas\”.
1.4.2
FUNÇÕES DE ENTRADA E SAÍDA: STDIO.H
Operações em arquivos
• remove: apaga o arquivo
• rename: renomeia o arquivo
Acesso a arquivos
• fclose: fecha o arquivo
• fflush: limpa o buffer. Quaisquer dados não escritos no buffer de
saı́da é gravada no arquivo
24
• fopen: abre o arquivo
• setbuf: controla o fluxo de armazenamento em buffer
Entrada/saı́da formatadas
• fprintf: grava uma saı́da formatada em arquivo
• fscanf: lê dados formatados a partir de arquivo
• printf: imprime dados formatados na saı́da padrão (monitor)
• scanf: lê dados formatados da entrada padrão (teclado)
• sprintf: grava dados formatados em uma string
• sscanf: lê dados formatados a partir de uma string
Entrada/saı́da de caracteres
• fgetc: lê um caractere do arquivo
• fgets: lê uma string do arquivo
• fputc: escreve um caractere em arquivo
• fputs: escreve uma string em arquivo
• getc: lê um caractere do arquivo
• getchar: lê um caractere da entrada padrão (teclado)
• gets: lê uma string da entrada padrão (teclado)
• putc: escreve um caractere na saı́da padrão (monitor)
• putchar: escreve um caractere na saı́da padrão (monitor)
• puts: escreve uma string na saı́da padrão (monitor)
• ungetc: retorna um caractere lido para o arquivo dele
Entrada/saı́da direta
• fread: lê um bloco de dados do arquivo
• fwrite: escreve um bloco de dados no arquivo
25
Posicionamento no arquivo
• fgetpos: retorna a posição atual no arquivo
• fseek: reposiciona o indicador de posição do arquivo
• fsetpos: configura o indicador de posição do arquivo
• ftell: retorna a posição atual no arquivo
• rewind: reposiciona o indicador de posição do arquivo para o inı́cio
do arquivo
Tratamento de erros
• clearerr: limpa os indicadores de erro
• feof: indicador de fim-de-arquivo
• ferror: indicador de checagem de erro
• perror: impressão de mensagem de erro
Tipos e macros
• FILE: tipo que contém as informações para controlar um arquivo
• EOF: constante que indica o fim-de-arquivo
• NULL: ponteiro nulo
1.4.3
FUNÇÕES DE UTILIDADE PADRÃO: STDLIB.H
Conversão de strings
• atof: converte string para double
• atoi: converte string para inteiro
• atol: converte string para inteiro longo
• strtod: converte string para double e devolve um ponteiro para o
próximo double contido na string
• strtol: converte string para inteiro longo e devolve um ponteiro para
o próximo inteiro longo contido na string
26
• strtoul: converte string para inteiro longo sem sinal e devolve um
ponteiro para o próximo inteiro longo sem sinal contido na string
Geração de seqüências pseudo-aleatórias
• rand: gera número aleatório
• srand: inicializa o gerador de números aleatórios
Gerenciamento de memória dinâmica
• malloc: aloca espaço para um array na memória
• calloc: aloca espaço para um array na memória e inicializa com zeros
• free: libera o espaço alocado na memória
• realloc: modifica o tamanho do espaço alocado na memória
Ambiente do programa
• abort: abortar o processo atual
• atexit: define uma função a ser executada no término normal do
programa
• exit: finaliza o programa
• getenv: retorna uma variável de ambiente
• system: executa um comando do sistema
Pesquisa e ordenação
• bsearch: pesquisa binária em um array
• qsort: ordena os elementos do array
Aritmética de inteiro
• abs: valor absoluto
• div: divisão inteira
• labs: valor absoluto de um inteiro longo
• ldiv: divisão inteira de um inteiro longo
27
1.4.4
FUNÇÕES MATEMÁTICAS: MATH.H
Funções trigonométricas
• cos: calcula o cosseno de um ângulo em radianos
• sin: calcula o seno de um ângulo em radianos
• tan: calcula a tangente de um ângulo em radianos
• acos: calcula o arco cosseno
• asin: calcula o arco seno
• atan: calcula o arco tangente
• atan2: calcula o arco tangente com dois parâmetros
Funções hiperbólicas
• cosh: calcula o cosseno hiperbólico de um ângulo em radianos
• sinh: calcula o seno hiperbólico de um ângulo em radianos
• tanh: calcula a tangente hiperbólica de um ângulo em radianos
Funções exponenciais e logarı́tmicas
• exp: função exponencial
• log: logaritmo natural
• log10: logaritmo comum (base 10)
• modf: quebra um número em partes fracionárias e inteira
Funções de potência
• pow: retorna a base elevada ao expoente
• sqrt: raiz quadrada de um número
Funções de arredondamento, valor absoluto e outras
• ceil: arredonda para cima um número
28
• fabs: calcula o valor absoluto de um número
• floor: arredonda para baixo um número
• fmod: calcula o resto da divisão
1.4.5
TESTES DE TIPOS DE CARACTERES: CTYPE.H
• isalnum: verifica se o caractere é alfanumérico
• isalpha: verifica se o caractere é alfabético
• iscntrl: verifica se o caractere é um caractere de controle
• isdigit: verifica se o caractere é um dı́gito decimal
• islower: verifica se o caractere é letra minúscula
• isprint: verifica se caractere é imprimı́vel
• ispunct: verifica se é um caractere de pontuação
• isspace: verifica se caractere é um espaço em branco
• isupper: verifica se o caractere é letra maiúscula
• isxdigit: verifica se o caractere é dı́gito hexadecimal
• tolower: converte letra maiúscula para minúscula
• toupper: converte letra minúscula para maiúscula
1.4.6
OPERAÇÕES EM STRING: STRING.H
Cópia
• memcpy: cópia de bloco de memória
• memmove: move bloco de memória
• strcpy: cópia de string
• strncpy: cópia de caracteres da string
Concatenação
29
• strcat: concatenação de strings
• strncat: adiciona “n” caracteres de uma string no final de outra string
Comparação
• memcmp: compara dois blocos de memória
• strcmp: compara duas strings
• strncmp: compara os “n” caracteres de duas strings
Busca
• memchr: localiza caractere em bloco de memória
• strchr: localiza primeira ocorrência de caractere em uma string
• strcspn: retorna o número de caracteres lidos de uma string antes
da primeira ocorrência de uma segunda string
• strpbrk: retorna um ponteiro para a primeira ocorrência na string de
qualquer um dos caracteres de uma segunda string
• strrchr: retorna um ponteiro para a última ocorrência do caratere na
string
• strspn: retorna o comprimento da string que consiste só de caracteres que fazem parte de uma outra string
• strtok: divide uma string em sub-strings com base em um caractere
Outras
• memset: preenche bloco de memória com valor especificado
• strerror: retorna o ponteiro para uma string de mensagem de erro
• strlen: comprimento da string
1.4.7
FUNÇÕES DE DATA E HORA: TIME.H
Manipulação do tempo
30
• clock: retorna o número de pulsos de clock decorrido desde que o
programa foi lançado
• difftime: retorna a diferença entre dois tempos
• mktime: converte uma estrutura tm para o tipo time t
• time: retorna o tempo atual do calendário como um time t
Conversão
• asctime: converte uma estrutura tm para string
• ctime: converte um valor time t para string
• gmtime: converte um valor time t para estrutura tm como tempo UTC
• localtime: converte um valor time t para estrutura tm como hora local
• strftime: formata tempo para string
Tipos e macros
• clock t: tipo capaz de representar as contagens clock e suportar
operações aritméticas
• size t: tipo inteiro sem sinal
• time t: tipo capaz de representar os tempos e suportar operações
aritméticas
• struct tm: estrutura contendo uma data e hora dividida em seus componentes
• CLOCKS PER SEC: número de pulsos de clock em um segundo
31
2
MANIPULANDO DADOS, VARIÁVEIS E EXPRESSÕES EM C
2.1
VARIÁVEIS
Na matemática, uma variável é uma entidade capaz de representar um
valor ou expressão. Ela pode representar um número ou um conjunto de
números, como na equação
x2 + 2x + 1 = 0
ou na função
f (x) = x2
Na computação, uma variável é uma posição de memória onde poderemos
guardar um determinado dado ou valor e modificá-lo ao longo da execução
do programa. Em linguagem C, a declaração de uma variável pelo programador segue a seguinte forma geral:
tipo da variavel nome da variavel;
O tipo da variavel determina o conjunto de valores e de operações que
uma variável aceita, ou seja, que ela pode executar. Já o nome da variavel
é como o programador identifica essa variável dentro do programa. Ao
nome que da variável o computador associa o endereço do espaço que
ele reservou na memória para guardar essa variável.
Depois declaração de uma variável é necessário colocar
um ponto e vı́rgula (;).
Isso é necessário uma vez que o ponto e vı́rgula é utilizado para separar
as instruções que compõem um programa de computador.
DECLARANDO VARIÁVEIS
Uma variável do tipo inteiro pode ser declarada como apresentado a seguir:
int x;
32
Além disso, mais de uma variável pode ser declarada para um mesmo tipo.
Para tanto, basta separar cada nome de variável por uma vı́rgula (,):
int x,y,z;
Uma variável deve ser declarada antes de ser usada no
programa.
Lembre-se, apenas quando declaramos uma variável é que o computador
reserva um espaço de memória para guardarmos nossos dados.
Antes de usar o conteúdo de uma variável, tenha certeza
de que o mesmo foi atribuı́do antes.
1
2
3
4
5
6
7
8
9
10
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int x ;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
x = 5;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x = qualquer valor
x=5
Quando falamos de memória do computador não existe o conceito de
posição de memória “vazia”. A posição pode apenas não estar sendo utilizada. Cada posição de memória do computador está preenchida com um
conjunto de 0’s e 1’s. Portanto, ao criarmos uma variável, ela automaticamente estará preenchida com um valor chamado de “lixo”.
2.1.1
NOMEANDO UMA VARIÁVEL
Quando criamos uma variável, o computador reserva um espaço de memória
onde poderemos guardar o valor associado a essa variável. Ao nome que
damos a essa variável o computador associa o endereço do espaço que
33
ele reservou na memória para guardar essa variável. De modo geral, interessa ao programador saber o nome das variáveis. Porém, existem algumas regras para a escolha dos nomes das variáveis na linguagem C.
• O nome de uma variável é um conjunto de caracteres que podem ser
letras, números ou underscores "_";
• O nome de uma variável deve sempre iniciar com uma letra ou o
underscore "_".
Na linguagem C, letras maiúsculas e minúsculas são consideradas diferentes.
A linguagem C é case-sensitive, ou seja, uma palavra escrita utilizando
caracteres maiúsculos é diferente da mesma palavra escrita com caracteres minúsculos. Sendo assim, as palavras Soma, soma e SOMA são
consideradas diferentes para a linguagem C e representam TRÊS variáveis
distintas.
Palavras chaves não podem ser usadas como nomes de
variáveis.
As palavras chaves são um conjunto de 38 palavras reservadas dentro da
linguagem C. São elas que formam a sintaxe da linguagem de programação
C. Essas palavras já possuem funções especı́ficas dentro da linguagem de
programação e, por esse motivo, elas não podem ser utilizadas para outro
fim como, por exemplo, nomes de variáveis. Abaixo, tem-se uma lista com
as 38 palavras reservadas da linguagem C.
auto
case
union
void
lista de palavras chaves da linguagem C
double int struct
break
else
long
enum
if typeof continue float return
const for short unsigned char extern
default do sizeof
volatile
goto register
switch
while
signed
static
O exemplo abaixo apresenta alguns nomes possı́veis de variáveis e outros
que fogem as regras estabelecidas:
34
Exemplo: nomeando variáveis
comp!
.var
int
.var
1cont
-x
Va-123
cont
Cont
Va 123
teste
int1
cont1
x&
2.1.2
DEFININDO O TIPO DE UMA VARIÁVEL
Vimos anteriormente que o tipo de uma variável determina o conjunto de
valores e de operações que uma variável aceita, ou seja, que ela pode
executar. A linguagem C possui um total de cinco tipos de dados básicos.
São eles:
Tipo
char
int
float
double
void
Bits
8
32
32
64
8
Intervalo de valores
-128 A 127
-2.147.483.648 A 2.147.483.647
1,175494E-038 A 3,402823E+038
2,225074E-308 A 1,797693E+308
sem valor
O TIPO CHAR
Comecemos pelo tipo char. Esse tipo de dados permite armazenar em um
único byte (8 bits) um número inteiro muito pequeno ou o código de um
caractere do conjunto de caracteres da tabela ASCII:
char c = ‘a’;
char n = 10;
Caracteres sempre ficam entre ‘aspas simples’!
Lembre-se: uma única letra pode ser o nome de uma variável. As ‘aspas
simples’ permitem que o compilador saiba que estamos inicializando nossa
variável com uma letra e não com o conteúdo de outra variável.
O TIPO INT
35
O segundo tipo de dado é o tipo inteiro: int. Esse tipo de dados permite
armazenar um número inteiro (sem parte fracionária). Seu tamanho depende do processador em que o programa está rodando, e é tipicamente
16 ou 32 bits:
int n = 1459;
Cuidado com a forma com que você inicializa as variáveis
dos tipos char e int.
Na linguagem C, os tipos char e int podem ser especificados nas bases decimal (padrão), octal ou hexadecimal. A base decimal é a base
padrão. Porém, se o valor inteiro for precedido por:
• “0”, ele será interpretado como octal. Nesse caso, o valor deve ser
definido utilizando os digitos de 0, 1, 2, 3, 4, 5, 6 e 7. Ex: int x = 044;
Nesse caso, 044 equivale a 36 (4 ∗ 81 + 4 ∗ 80 );
• “0x” ou “0X”, ele será interpretado como hexadecimal. Nesse caso, o
valor deve ser definido utilizando os digitos de 0, 1, 2, 3, 4, 5, 6, 7, 8
e 9, e as letras A (10), B (11), C (12), D (13), E (14) e F (15). Ex: int
y = 0x44; Nesse caso, 0x44 equivale a 68 (4 ∗ 161 + 4 ∗ 160 );
OS TIPOS FLOAT E DOUBLE
O terceiro e quarto tipos de dados são os tipos reais: float e double. Esses
tipos de dados permitem armazenar um valor real (com parte fracionária),
também conhecido como ponto flutuante. A diferença entre eles é de
precisão:
• tipo float: precisão simples;
• tipo double: dupla precisão. São úteis quando queremos trabalhar
com intervalos de números reais realmente grandes.
Em números reais, a parte decimal usa ponto e não vı́rgula!
A linguagem C usa o padrão numérico americano, ou seja, a parte decimal
fica depois de um ponto. Veja os exemplos:
36
float f = 5.25;
double d = 15.673;
Pode-se escrever números dos tipos float e double usando
notação cientı́fica.
A notação cientı́fica é uma forma de escrever números extremamente grandes ou extremamente pequenos. Nesse caso, o valor real é seguido por
uma letra “e” ou “E” e um número inteiro (positivo ou negativo) que indica
o expoente da base 10 (representado pela letra “e” ou “E” que multiplica o
número):
double x = 5.0e10;
equivale a
double x = 50000000000;
O TIPO VOID
Por fim, temos o tipo void. Esse tipo de dados permite declarar uma
função que não retorna valor ou um ponteiro genérico, como será visto
nas próximas seções.
A linguagem C não permite que se declare uma variável
do tipo void. Esse tipo de dados só deve ser usado
para declarar funções que não retornam valor ou ponteiros
genérico.
OS MODIFICADORES DE TIPOS
Além desses cinco tipos básicos, a linguagem C possui quatro modificadores de tipos. Eles são aplicados precedendo os tipos básicos (com a
exceção do tipo void), e eles permitem alterar o significado do tipo, de
modo a adequá-lo às necessidades do nosso programa. São eles:
• signed: determina que a variável declarada dos tipos char ou int
terá valores positivos ou negativos. Esse é o padrão da linguagem.
Exemplo:
signed char x;
signed int y;
37
• unsigned: determina que a variável declarada dos tipos char ou
int só terá valores positivos. Nesse caso, a variável perde o seu o
bit de sinal, o que aumenta a sua capacidade de armazenamento.
Exemplo:
unsigned char x;
unsigned int y;
• short: determina que a variável do tipo int terá 16 bits (inteiro pequeno), independente do processador. Exemplo:
short int i;
• long: determina que a variável do tipo int terá 32 bits (inteiro grande),
independente do processador. Também determina que o tipo double
possua maior precisão. Exemplo:
long int n;
long double d;
A linguagem C permite que se utilize mais de um modificador de tipo sobre um mesmo tipo.
Desse modo, podemos declarar um inteiro grande (long) e sem sinal (unsigned),
o que aumenta em muito o seu intervalo de valores posı́veis:
unsigned long int m;
A tabela a seguir mostra todas as combinações permitidas dos tipos básicos
e dos modificadores de tipo, o seu tamanhos em bits e seu intervalo de valores:
38
Tipo
char
unsigned char
signed char
int
unsigned int
signed int
short int
unsigned short int
signed short int
long int
unsigned long int
signed long int
float
double
long double
Bits
8
8
8
32
32
32
16
16
16
32
32
32
32
64
96
Intervalo de valores
-128 A 127
0 A 255
-128 A 127
-2.147.483.648 A 2.147.483.647
0 A 4.294.967.295
-32.768 A 32.767
-32.768 A 32.767
0 A 65.535
-32.768 A 32.767
-2.147.483.648 A 2.147.483.647
0 A 4.294.967.295
-2.147.483.648 A 2.147.483.647
1,175494E-038 A 3,402823E+038
2,225074E-308 A 1,797693E+308
3,4E-4932 A 3,4E+4932
2.2
LENDO E ESCREVENDO DADOS
2.2.1
PRINTF
A função printf() é uma das funções de saı́da/escrita de dados da linguagem C. Seu nome vem da expressão em inglês print formatted, ou seja,
escrita formatada. Basicamente, a função printf() escreve na saı́da de video (tela) um conjunto de valores, caracteres e/ou sequência de caracteres
de acordo com o formato especificado. A forma geral da função printf() é:
printf(“tipos de saı́da”, lista de variáveis)
A função printf() recebe 2 parâmetros de entrada
• “tipos de saı́da”: conjunto de caracteres que especifica o formato
dos dados a serem escritos e/ou o texto a ser escrito;
• lista de variáveis: conjunto de nomes de variáveis, separados por
vı́rgula, que serão escritos.
ESCREVENDO UMA MENSAGEM DE TEXTO
A forma geral da função printf() especifica que ela sempre receberá uma
lista de variáveis para formatar e escrever na tela. Isso nem sempre é
39
verdade. A função printf() pode ser usada quando queremos escrever
apenas um texto simples na tela:
ESCREVENDO VALORES FORMATADOS
Quando queremos escrever dados formatados na tela usamos a forma geral da função, a qual possui os tipos de saı́da. Eles especificam o formato
de saı́da dos dados que serão escritos pela função printf(). Cada tipo de
saı́da é precedido por um sinal de % e um tipo de saı́da deve ser especificado para cada variável a ser escrita. Assim, se quissessemos escrever
uma única expressão com o camando printf(), fariamos
Se fossem duas as expressões a serem escritas, fariamos
e assim por diante. Note que os formatos e as expressões a serem escritas com aquele formato devem ser especificados na mesma ordem, como
mostram as setas.
O comando printf() não exige o sı́mbolo & na frente do
nome de cada varável.
Diferente do comando scanf(), o comando printf() não exige o sı́mbolo &
na frente do nome de uma variável que será escrita na tela. Se usado,
ele possui outro significado (como será visto mais adiante) e não exibe o
conteúdo da variável.
A função printf() pode ser usada para escrever virtualmente qualquer tipo
de dado. A tabela abaixo mostra alguns dos tipos de saı́da suportados
pela linguagem:
40
%c
%d ou %i
%u
%f
%s
%p
%e ou %E
Alguns “tipos de saı́da”
escrita de um caractere
escrita de números inteiros
escrita de números inteiros sem sinal
escrita de número reais
escrita de vários caracteres
escrita de um endereço de memória
escrita em notação cientifı́ca
Abaixo, tem-se alguns exemplos de escrita de dados utilizando o comando
printf(). Nesse momento não se preocupe com o ‘\n’ que aparece dentro
do comando printf(), pois ele serve apenas para ir para uma nova linha ao
final do comando:
Exemplo: escrita de dados na linguagem C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t x = 10;
/ / E s c r i t a de um v a l o r i n t e i r o
p r i n t f ( ‘ ‘ % d\n ’ ’ , x ) ;
float y = 5.0;
/ / E s c r i t a de um v a l o r i n t e i r o e o u t r o r e a l
p r i n t f ( ‘ ‘ % d%f \n ’ ’ , x , y ) ;
/ / Adicionando espaço e n t r e os v a l o r e s
p r i n t f ( ‘ ‘ % d %f \n ’ ’ , x , y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
10
105.000000
10 5.000000
No exemplo acima, os comandos
printf(“%d%f\n”,x,y);
e
printf(“%d %f\n”,x,y);
imprimem os mesmos dados, mas o segundo os separa com um espaço.
Isso ocorre por que o comando printf() aceita textos junto aos tipos de
saı́da. Pode-se adicionar texto antes, depois ou entre dois ou mais tipos
de saı́da:
41
Junto ao tipo de saı́da, pode-se adicionar texto e não apenas espaços.
1
2
3
4
5
6
7
8
9
10
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t x = 10;
p r i n t f ( ‘ ‘ T o t a l = %d\n ’ ’ , x ) ;
p r i n t f ( ‘ ‘ % d c a i x a s \n ’ ’ , x ) ;
p r i n t f ( ‘ ‘ T o t a l de %d c a i x a s \n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Total = 10
10 caixas
Total de 10 caixas
Isso permite que o comando printf() seja usado para escrever não apenas
dados, mas sentenças que façam sentido para o usuário do programa.
2.2.2
PUTCHAR
A função putchar() (put character ) permite escrever um único caractere na
tela. Sua forma geral é:
int putchar(int caractere)
A função putchar() recebe como parâmetro de entrada um único valor
inteiro. Esse valor será convertido para caractere e mostrado na tela. A
função retorna
• Se NÂO ocorrer um erro: o próprio caractere que foi escrito;
• Se ocorrer um erro: a constante EOF (definida na biblioteca stdio.h)
é retornada.
42
Exemplo: putchar()
1
2
3
4
5
6
7
8
9
10
11
12
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char c = ’ a ’ ;
i n t x = 65;
putchar ( c ) ;
p u t c h a r ( ’ \n ’ ) ;
putchar ( x ) ;
p u t c h a r ( ’ \n ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
a
A
Perceba, no exemplo acima, que a conversão na linguagem C é direta no
momento da impressão, ou seja, o valor 65 é convertido para o caractere
ASCII correspondente, no caso, o caractere “A”. Além disso, o comando
putchar() também aceita o uso de seqüências de escape como o caractere
‘\n’ (nova linha).
2.2.3
SCANF
A função scanf() é uma das funções de entrada/leitura de dados da linguagem C. Seu nome vem da expressão em inglês scan formatted, ou seja,
leitura formatada. Basicamente, a função scanf() lê do teclado um conjunto de valores, caracteres e/ou sequência de caracteres de acordo com
o formato especificado. A forma geral da função scanf() é:
scanf(“tipos de entrada”, lista de variáveis)
A função scanf() recebe 2 parâmetros de entrada
• “tipos de entrada”: conjunto de caracteres que especifica o formato
dos dados a serem lidos;
• lista de variáveis: conjunto de nomes de variáveis que serão lidos e
separados por vı́rgula, onde cada nome de variável é precedido pelo
operador &.
43
Os tipo de entrada especificam o formato de entrada dos dados que serão
lidos pela função scanf(). Cada tipo de entrada é precedido por um sinal de
% e um tipo de entrada deve ser especificado para cada variável a ser lida.
Assim, se quissessemos ler uma única variável com o camando scanf(),
fariamos
Se fossem duas as variáveis a serem lidas, fariamos
e assim por diante. Note que os formatos e as variáveis que armazenarão
o dado com aquele formato devem ser especificados na mesma ordem,
como mostram as setas.
Na linguagem C, é necessário colocar o sı́mbolo de & antes
do nome de cada variável a ser lida pelo comando scanf().
Trata-se de uma exigência da linguagem C. Todas as variáveis que receberão valores do teclado por meio da scanf() deverão ser passadas pelos
seus endereços. Isso se faz colocando o operador de endereço “&” antes
do nome da variável.
A função scanf() pode ser usada para ler virtualmente qualquer tipo de
dado. No entando, ela é usada com mais freqüencia para a leitura de
números inteiros e/ou de ponto flutuante (números reais). A tabela abaixo
mostra alguns dos tipos de entrada suportados pela linguagem:
Alguns “tipos de entrada”
%c
leitura de um caractere
%d ou %i leitura de números inteiros
%f
leitura de número reais
%s
leitura de vários caracteres
Abaixo, tem-se alguns exemplos de leitura de dados utilizando o comando
scanf():
44
Exemplo: leitura de dados na linguagem C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int x , z ;
float y ;
/ / L e i t u r a de um v a l o r i n t e i r o
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
/ / L e i t u r a de um v a l o r r e a l
s c a n f ( ‘ ‘ % f ’ ’ ,& y ) ;
/ / L e i t u r a de um v a l o r i n t e i r o e o u t r o r e a l
s c a n f ( ‘ ‘ % d%f ’ ’ ,&x ,& y ) ;
/ / L e i t u r a de d o i s v a l o r e s i n t e i r o s
s c a n f ( ‘ ‘ % d%d ’ ’ ,&x ,& z ) ;
/ / L e i t u r a de d o i s v a l o r e s i n t e i r o s com espaço
s c a n f ( ‘ ‘ % d %d ’ ’ ,&x ,& z ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, os comandos
scanf(“%d%d”,&x,&z);
e
scanf(“%d %d”,&x,&z);
são equivalentes. Isso ocorre por que o comando scanf() ignora os espaços
em branco entre os tipos de entrada. Além disso, quando o comando
scanf() é usado para ler dois ou mais valores, podemos optar por duas
formas de digitar os dados no teclado:
• Digitar um valor e, em seguida, pressionar a tecla ENTER para cada
valor digitado;
• Digitar todos os valores separados por espaço e, por último, pressionar a tecla ENTER.
45
O comando scanf() ignora apenas os espaços em branco
entre os tipos de entrada. Qualquer outro caractere inserido entre os tipos de dados deverá ser digitado pelo
usuário, mas será descartado pelo programa.
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t dia , mes , ano ;
/ / L e i t u r a de t r ê s v a l o r e s i n t e i r o s
/ / com b a r r a s e n t r e e l e s
s c a n f ( ‘ ‘ % d/%d/%d ’ ’ ,& dia ,&mes,& ano ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Isso permite que o comando scanf() seja usado para receber dados formatados como, por exemplo, uma data: dia/mês/ano. No exemplo acima,
o comando scanf() é usado para a entrada de três valores inteiros separados por uma barra “/” cada. Quando o usuário for digitar os três valores,
ele será obrigado a digitar os três valores separados por barra (as barras
serão descartadas e não interferem nos dados). Do contrário, o comando
scanf() não irá ler corretamente os dados digitados.
2.2.4
GETCHAR
A função getchar() (get character ) permite ler um único caractere do teclado. Sua forma geral é:
int putchar(void)
A função getchar() não recebe parâmetros de entrada. A função retorna
• Se NÂO ocorrer um erro: o código ASCII do caractere lido;
• Se ocorrer um erro: a constante EOF (definida na biblioteca stdio.h)
é retornada.
46
Exemplo: getchar()
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char c ;
c = getchar ( ) ;
p r i n t f ( ‘ ‘ C a r a c t e r e : %c\n ’ ’ , c ) ;
p r i n t f ( ‘ ‘ Codigo ASCII : %d\n ’ ’ , c ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Perceba, no exemplo acima, que a conversão na linguagem C é direta no
momento da leitura, ou seja, embora a função retorne um valor do tipo
int, pode-se atribuir para uma variável do tipo char devido a conversão
automática da linguagem C.
2.3
ESCOPO: TEMPO DE VIDA DA VARIÁVEL
Quando declararamos uma variável, vimos que é preciso sempre definir o
seu tipo (conjunto de valores e de operações que uma variável aceita) e
nome (como o programador identifica essa variável dentro do programa).
Porém, além disso, é preciso definir o seu escopo.
O escopo é o conjunto de regras que determinam o uso e
a validade das variáveis ao longo do programa.
Em outras palavras, escopo de uma variável define onde e quando a
variável pode ser usada. Esse escopo está intimamente ligado com o local
de declaração dessa variável e por esse motivo ele pode ser: global ou
local.
ESCOPO GLOBAL
Uma variável global é declarada fora de todas as funções do programa,
ou seja, na área de declarações globais do programa (acima da cláusula
main). Essas variáveis existem enquanto o programa estiver executando,
ou seja, o tempo de vida de uma variável global é o tempo de execução do
programa. Além disso, essas variáveis podem ser acessadas e alteradas
em qualquer parte do programa.
47
Variáveis globais podem ser acessadas e alteradas em
qualquer parte do programa.
1
2
3
4
5
6
7
8
9
10
11
12
13
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t x = 5 ; / / v a r i á v e l g l o b a l
void i n c r ( ) {
x ++; / / acesso a v a r i á v e l g l o b a l
}
i n t main ( ) {
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ; / / acesso a v a r i á v e l
global
incr () ;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ; / / acesso a v a r i á v e l
global
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x=5
x=6
Na figura abaixo, é possı́vel ter uma boa representação de onde começa e
termina cada escopo do código anterior:
Note, no exemplo acima, que a variável x é declarada junto com as bibliotecas do programa, portanto, trata-se de uma variável global (escopo
global). Por esse motivo, ela pode ser acessada e ter seu valor alterado
em qualquer parte do programa (ou seja, no escopo global e em qualquer
escopo local).
48
De modo geral, evita-se o uso de variáveis globais em um
programa.
As variáveis globais devem ser evitadas porque qualquer parte do programa pode alterá-la. Isso torna mais difı́cil a manutenção do programa,
pois fica difı́cil saber onde ele é inicializada, para que serve, etc. Além
disso, variáveis globais ocupam memória durante todo o tempo de execução
do programa e não apenas quando elas são necessárias.
ESCOPO LOCAL
Já uma variável local é declarada dentro de um bloco de comandos delimitado pelo operador de chaves {}(escopo local). Essas variáveis são
visı́veis apenas no interior do bloco de comandos onde ela foi declarada,
ou seja, dentro do seu escopo.
Um bloco começa quando abrimos uma chave {e termina
quando fechamos a chave }.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
void func1 ( ) {
i n t x ; / / v a r i á v e l l o c a l
}
void func2 ( ) {
i n t x ; / / v a r i á v e l l o c a l
}
i n t main ( ) {
int x ;
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
i f ( x == 5 ) {
i n t y =1;
p r i n t f ( ‘ ‘ % d\n ’ ’ , y ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;}
Na figura abaixo, é possı́vel ter uma boa representação de onde começa e
termina cada escopo do código anterior:
49
Note, no exemplo acima, que a variável x é declarada três vezes. Cada
declaração dela está em um bloco de comandos distinto (ou seja, delimitado por um operador de chaves {}). Desse modo, apesar de possuiremos
o mesmo nome, elas possuem escopos diferentes e, consequentemente,
tempos de vida diferentes: uma não existe enquanto a outra existe. Já a
variável y só existe dentro do bloco de comandos pertencente a instrução
if(x == 5), ou seja, outro escopo local.
50
Quando um bloco possui uma variável local com o mesmo
nome de uma variável global, esse bloco dará preferência à
variável local. O mesmo vale para duas variáveis locais em
blocos diferentes: a declaração mais próxima tem maior
precedência e oculta as demais variáveis com o mesmo
nome.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
int x = 5;
i n t main ( ) {
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
int x = 4;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
{
int x = 3;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
}
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x=5
x=4
x=3
x=4
Na figura abaixo, é possı́vel ter uma boa representação de onde começa
e termina cada escopo do código anterior e como um escopo oculta os
demais:
51
Note, no exemplo acima, que a variável x é declarada três vezes. Cada
declaração dela está em um escopo distinto: uma é global e duas são
locais. Na primeira chamada do comando printf() (linha 5), a variável global x é acessada. Isso ocorre porque, apesar de estarmos em um escopo
local, a segunda variável x ainda não foi criada e portanto não existe. Já
na segunda chamada do comando printf() (linha 7), a segunda variável x
já foi criada, ocultando a variável global de mesmo nome. Por isso, esse
comando printf() imprime na tela de saı́da o valor x = 4. O mesmo acontece com a terceira chamada do comando printf() (linha 10): esse comando está dentro de um novo bloco de comandos, ou seja, delimitado
por um operador de chaves {}. A declaração da terceira variável x oculta
a declaração da segunda variável x. Por isso, esse comando printf() imprime na tela de saı́da o valor x = 3. No fim desse bloco de comandos, a
terceira variável x é destruı́da, o que torna novamente visı́vel a segunda
variável x, a qual é impressa na tela pela quarta chamada do comando
printf() (linha 12).
Como o escopo é um assunto delicado e que pode gerar
muita confusão, evita-se o uso de variáveis com o mesmo
nome.
2.4
CONSTANTES
Nós aprendemos que uma variável é uma posição de memória onde podemos guardar um determinado dado ou valor e modificá-lo ao longo da
execução do programa. Já uma constante permite guardar um determi52
nado dado ou valor na memória do computador, mas com a certeza de
que ele não se altera durante a execução do programa: será sempre o
mesmo, portanto constante.
Para constantes é obrigatória a atribuição do valor no momento da declaração.
Isso ocorre por que após a declaração de uma constante, seu valor não poderá mais ser alterado: será constante. Na linguagem C existem duas maneiras para criar constantes: usando os comandos #define e const. Além
disso, a própria linguagem C já possui algumas constantes pré-definidas,
como as sequências de escape.
2.4.1
COMANDO #DEFINE
Uma das maneiras de declarar uma constante é usando o comando #define, que segue a seguinte forma geral:
#define nome da constante valor da constante
O comando #define é uma diretiva de compilação que informa ao compilador que ele deve procurar por todas as ocorrências da palavra definida por
nome da constante e substituir por valor da constante quando o programa for compilado. Por exemplo, uma constante que represente o valor
de π pode ser declarada como apresentado a seguir:
#define PI 3.1415
2.4.2
COMANDO CONST
Uma outra maneira de declarar uma constante é usando o comando const,
que segue a seguinte forma geral:
const tipo da constante nome da constante = valor da constante;
Note que a forma geral do comando const se parece muito com a da
declaração de uma variável. Na verdade, o prefixo const apenas informa
53
ao programa que a variável declarada não poderá ter seu valor alterado.
E, sendo uma variável, esta constante está sujeita as mesmas regras que
regem o uso das variáveis. Por exemplo, uma constante que represente o
valor de π pode ser declarada como apresentado a seguir:
const float PI = 3.1415;
2.4.3
SEQÜÊNCIAS DE ESCAPE
A linguagem C possui algumas constantes pré-definidas, como as sequências
de escape ou códigos de barra invertida. Essas constantes As sequências
de escape permitem o envio de caracteres de controle não gráficos para
dispositivos de saı́da.
A tabela abaixo apresenta uma relação das sequências de escape mais
utilizadas em programação e seu significado:
Código
\a
\b
\n
\v
\t
\r
\’
\”
\\
\f
Comando
bip
retorcesso (backspace)
nova linha (new line)
tabulação vertical
tabulação horizontal
retorno de carro (carriage return)
apóstrofe
aspa
barra invertida (backslash)
alimentação de folha (form feed)
As sequências de escape permitem que o comando printf() imprima caracteres especiais na tela de saı́da, como tabulações e quebras de linha.
Veja o exemplo abaixo:
54
Exemplo: sequências de escape
1
2
3
4
5
6
7
8
9
10
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
p r i n t f ( ‘ ‘ H e l l o World \n ’ ’ ) ;
p r i n t f ( ‘ ‘ H e l l o \ nWorld \n ’ ’ ) ;
p r i n t f ( ‘ ‘ H e l l o \\ World \n ’ ’ ) ;
p r i n t f ( ‘ ‘ \ ” H e l l o World \ ” \ n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Hello World
Hello
World
Hello \World
“Hello World”
2.5
OPERADORES
2.5.1
OPERADOR DE ATRIBUIÇÃO: “=”
Uma das operações mais utilizadas em programação é a operação de
atribuição “=”. Ela é responsável por armazenar um determinado valor em
uma variável. Em linguagem C, o uso do operador de atribuição “=” segue
a seguinte forma geral
nome da variável = expressão;
Por expressão, entende-se qualquer combinação de valores, variáveis,
constantes ou chamadas de funções utilizando os operadores matemáticos
+,-, *, / e %, que resulte numa resposta do mesmo tipo da variável definida
por nome da variável. Veja o exemplo abaixo:
55
Exemplo: operador de atribuição “=”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
# include <math . h>
const i n t z = 9 ;
i n t main ( ) {
float x ;
/ / d e c l a r a y e a t r i b u i um v a l o r
float y = 3;
/ / a t r i b u i um v a l o r a x
x = 5;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
/ / a t r i b u i uma c o n s t a n t e a x
x = z;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
/ / a t r i b u i o r e s u l t a d o de uma
/ / express ão matem ática a x
x = y + 5;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
/ / a t r i b u i o r e s u l t a d o de uma funç ão a x
x = sqrt (9) ;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x = 5.000000
x = 9.000000
x = 8.000000
x = 3.000000
No exemplo acima, pode-se notar que o operador de atribuição também
pode ser utilizado no momento da declaração da variável (linha8). Desse
modo, a variável já é declarada possuindo um valor inicial.
56
O operador de atribuição “=” armazena o valor ou resultado
de uma expressão contida a sua direita na variável especificada a sua esquerda.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
# include <math . h>
const i n t z = 9 ;
i n t main ( ) {
float x ;
float y = 3;
/ / Correto
x = y + 5;
/ / ERRADO
y + 5 = x;
/ / Correto
x = 5;
/ / ERRADO
5 = x;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
É importante ter sempre em mente que o operador de atribuição “=” calcula a expressão à direita do operador “=” e atribui esse valor à variável à
esquerda do operador, nunca o contrário.
A linguagem C suporta múltiplas atribuições.
1
2
3
4
5
6
7
8
9
10
11
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
float x , y , z ;
x = y = z = 5;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
p r i n t f ( ‘ ‘ y = %f \n ’ ’ , y ) ;
p r i n t f ( ‘ ‘ z = %f \n ’ ’ , z ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x = 5.000000
y = 5.000000
z = 5.000000
57
No exemplo acma, o valor 5 é copiado para a variável z. Lembre-se, o valor
da direita é sempre armazenado na variável especificada a sua esquerda.
Em seguida, o valor de z é copiado para a variável y e, na sequência, o
valor de y é copiado para x.
A linguagem C também permite a atribuição entre tipos báscos diferentes. O compilador converte automaticamente o valor do lado direto para o
tipo do lado esquerdo do comando de atribuição “=”. Durante a etapa de
conversão de tipos, pode haver perda de informação.
Na conversão de tipos, durante a atribuição, pode haver
perda de informação.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Saı́da
2.5.2
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t x = 65;
char ch ;
float f = 25.1;
/ / ch recebe 8 b i t s menos s i g n i f i c a t i v o s de x
/ / c o n v e r t e para a t a b e l a ASCII
ch = x ;
p r i n t f ( ‘ ‘ ch = %c\n ’ ’ , ch ) ;
/ / x recebe p a r t e apenas a p a r t e i n t e i r a de f
x = f;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
/ / f recebe v a l o r 8 b i t s c o n v e r t i d o para r e a l
f = ch ;
p r i n t f ( ‘ ‘ f = %f \n ’ ’ , f ) ;
/ / f recebe o v a l o r de x
f = x;
p r i n t f ( ‘ ‘ f = %f \n ’ ’ , f ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
ch = A
x = 25
f = 65.000000
f = 25.000000
OPERADORES ARITMÉTICOS
Os operadores aritméticos são aqueles que operam sobre números (valores,
variáveis, constantes ou chamadas de funções) e/ou expressões e tem
58
como resultado valores numéricos. A linguagem C possui um total de cinco
operadores aritméticos, como mostra a tabela abaixo:
Operador
+
*
/
%
Significado
adição de dois valores
subtração de dois valores
multiplicação de dois valores
quociente de dois valores
resto de uma divisão
Exemplo
z=x+y
z=x-y
z=x*y
z=x/y
z=x%y
Note que os operadores aritméticos são sempre usados em conjunto com
o operador de atribuição. Afinal de contas, alguém precisa receber o resultado da expressão aritmética.
O operador de subtração também pode ser utilizado para
inverter o sinal de um número.
De modo geral, os operadores aritméticos são operadores binários, ou
seja, atuam sobre dois valores. Mas os operadores de adição e subtração
também podem ser aplicados sobre um único valor. Nesse caso, eles são
chamados de operadores unários. Por exemplo, na expressão:
x = -y;
a variável x receberá o valor de y multiplicado por -1, ou seja, x = (-1) * y;
59
Numa operação utilizando o operador de quociente /, se
ambos numerador e denominador forem números inteiros, por padrão o compilador irá retornar apenas a parte
inteira da divisão.
1
2
3
4
5
6
7
8
9
10
11
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
float x ;
x = 5/4;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
x = 5/4.0;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x = 1.000000
x = 1.250000
No exemplo acima, a primeira divisão (linha 5) possui apenas operandos
inteiros. Logo o resultado é um valor inteiro. Já a segunda divisão (linha
7), o número quatro é definido como real (4.0). Portanto, o compilador
considera essa divisão como tendo resultado real.
O operador de resto da divisão (%) só é válido para valores
inteiros (tipo int).
2.5.3
OPERADORES RELACIONAIS
Os operadores relacionais são aqueles que operam sobre dois valores
(valores, variáveis, constantes ou chamadas de funções) e/ou expressões
e verificam a magnitude (quem é maior ou menor) e/ou igualdade entre
eles.
Os operadores relacionais são operadores de comparação
de valores.
A linguagem C possui um total de seis operadores relacionais, como mostra a tabela abaixo:
60
Operador
>
>=
<
<=
==
!=
Significado
Maior do que
Maior ou igual a
Menor do que
Menor ou igual a
Igual a
Diferente de
Exemplo
x>5
x >= 10
x<5
x <= 10
x == 0
x != 0
Como resultado, esse tipo de operador retorna:
• o valor UM (1), se a expressão relacional for considerada verdadeira;
• o valor ZERO (0), se a expressão relacional for considerada falsa.
Não existem os operadores relacionais: “=<”, “=>” e “<>”.
Os sı́mbolos “=<” e “=>” estão digitados na ordem invertida. O correto é “<=”
(menor ou igual a) e “>=” (maior ou igual a). Já o sı́mbolo “<>” é o operador
de diferente da linguagem Pascal, não da linguagem C. O correto é “!=”.
Não confunda o operador de atribuição “=” com o operador
de comparação “==”.
Esse é um erro bastante comum quando se está programando em linguagem C. O operador de atribuição é definido por UM sı́mbolo de igual “=”,
enquanto o operador de comparação é definido por DOIS sı́mbolos de igual
“==”. Se você tentar colocar o operador de comparação em uma operação
de atribuição, o compilador acusará um erro. O mesmo não acontece se
você acidentalmente colocar o operador de atribuição “=” no lugar do operador de comparação “==”.
O exemplo abaixo apresenta o resultado de algumas expressões relacionais:
61
Exemplos de expressões relacionais
1
2
3
4
5
6
7
8
9
10
11
12
Saı́da
2.5.4
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int x = 5;
int y = 3;
p r i n t f ( ‘ ‘ Resultado :
(1)
p r i n t f ( ‘ ‘ Resultado :
p r i n t f ( ‘ ‘ Resultado :
(1)
p r i n t f ( ‘ ‘ Resultado :
(0)
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Resultado:
Resultado:
Resultado:
Resultado:
%d\n ’ ’ , x > 4 ) ; / / v e r d a d e i r o
%d\n ’ ’ , x == 4 ) ; / / f a l s o ( 0 )
%d\n ’ ’ , x ! = y ) ; / / v e r d a d e i r o
%d\n ’ ’ , x ! = y +2) ; / / f a l s o
1
0
1
0
OPERADORES LÓGICOS
Certas situações não podem ser modeladas apenas utilizando os operadores aritméticos e/ou relacionais. Um exemplo bastante simples disso é
saber se uma determinada variável x está dentro de uma faixa de valores.
Por exemplo, a expressão matemática
0 < x < 10
indica que o valor de x deve ser maior do que 0 (zero) e também menor do
que 10.
Para modelar esse tipo de situação, a linguagem C possui um conjunto de
3 operadores lógicos, como mostra a tabela abaixo:
Operador
&&
||
!
Significado
Operador E
Operador OU
Operador NEGAÇÃO
Exemplo
(x >= 0 && x <= 9)
(a == ‘F’ ||b != 32)
!(x == 10)
Esses operadores permitem representar situações lógicas, unindo duas ou
mais expressões relacionais simples numa composta:
62
• Operador E (&&): a expressão resultante só é verdadeira se ambas
as expressões unidas por esse operador também forem. Por exemplo, a expressão (x >= 0 && x <= 9) será verdadeira somente se as
expressões (x >= 0) e (x <= 9) forem verdadeiras;
• Operador OU (||): a expressão resultante é verdadeira se alguma
das expressões unidas por esse operador também for. Por exemplo,
a expressão (a == ‘F’ ||b != 32) será verdadeira se uma de suas duas
expressões, (a == ‘F’) ou (b != 32), for verdadeira;
• Operador NEGAÇÃO (!): inverte o valor lógico da expressão a qual
se aplica. Por exemplo, a expressão !(x == 10) se transforma em (x >
10 ||x < 10).
Os operadores lógicos atuam sobre valores lógicos e retornam um valor
lógico:
• 1: se a expressão é verdadeira;
• 0: se a expressão é falsa.
Abaixo é apresentada a tabela verdade, onde os termos a e b representam
duas expressões relacionais:
a
0
0
1
1
2.5.5
b
0
1
0
1
Tabela verdade
!a !b a&&b a||b
1 1
0
0
1 0
0
1
0 1
0
1
0 0
1
1
OPERADORES BIT-A-BIT
A linguagem C permite que se faça operações lógicas “bit-a-bit” em números.
Na memória do computador um número é sempre representado por sua
forma binária. Assim o número 44 é representado pelo seguinte conjunto
de 0’s e 1’s na memória: 00101100. Os operadores bit-a-bit permitem que
o programador faça operações em cada bit do número.
Os operadores bit-a-bit ajudam os programadores que
queiram trabalhar com o computador em “baixo nı́vel”.
63
A linguagem C possui um total de seis operadores bit-a-bit, como mostra a
tabela abaixo:
Operador
∼
&
|
∧
<<
>>
Significado
complemento bit-a-bit
E bit-a-bit
OU bit-a-bit
OU exclusivo
deslocamento de bits à esquerda
deslocamento de bits à direita
Exemplo
∼x
x & 167
x |129
x ∧ 167
x << 2
x >> 2
Na tabela acima, temos que os operadores ∼, &, |, e ∧ são operações
lógicas que atuam em cada um dos bits do número (por isso, bit-a-bit). Já
os operadores de deslocamento << e >> servem para rotacionar o conjunto
de bits do número à esquerda ou à direita.
Os operadores bit-a-bit só podem ser usados nos tipos
char, int e long.
Os operadores bit-a-bit não podem ser aplicados sobre valores dos tipos
float e double. Em parte, isso se deve a maneira como um valor real,
também conhecido como ponto flutuante, é representado nos computadores. A representação desses tipos segue a representação criada por
Konrad Zuse, onde um número é dividido numa mantissa (M) e um expoente (E). O valor representado é obtido pelo produto: M 2E . Como se vê, a
representação desses tipos é bem mais complexa: não se trata de apenas
um conjunto de 0’s e 1’s na memória.
Voltemos ao número 44, cuja representação binária é 00101100. Abaixo
são apresentados exemplos de operações bit-a-bit com esse valor:
64
Exemplos de operadores bit-a-bit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
unsigned char x , y ;
x = 44;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
y = ˜x;
p r i n t f ( ‘ ‘ ˜ x = %d\n ’ ’ , y ) ;
y = x & 167;
p r i n t f ( ‘ ‘ x & 167 = %d\n ’
y = x | 129;
p r i n t f ( ‘ ‘ x | 129 = %d\n ’
y = x ˆ 167;
p r i n t f ( ‘ ‘ x ˆ 167 = %d\n ’
y = x << 2 ;
p r i n t f ( ‘ ‘ x << 2 = %d\n ’ ’
y = x >> 2 ;
p r i n t f ( ‘ ‘ x >> 2 = %d\n ’ ’
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ,y) ;
’ ,y) ;
’ ,y) ;
,y) ;
,y) ;
x = 44
∼x = 211
x & 167 = 36
x |129 = 173
x ∧ 167 = 139
x << 2 = 176
x >> 2 = 11
No exemplo acima, a primeira operação é a de complemento bit-a-bit “∼”.
Basicamente, essa operação inverte o valor dos 0’s e 1’s que compõem o
número. Assim:
00101100 = x (44)
11010011 = x (211)
Já os operadores &, |, e ∧ são as operações lógicas de E, OU e OU EXCLUSIVO realizadas bit-a-bit:
• Operador E bit-a-bit (&): um bit terá valor 1 na expressão resultante
somente se ambas as expressões unidas por esse operador também
tiverem o valor 1 nos bits daquela posição:
65
00101100 = x (44)
10100111 = 167
00100100 = x & 167 (36)
• Operador OU bit-a-bit (|): um bit terá valor 1 na expressão resultante se alguma das expressões unidas por esse operador também
tiverem o valor 1 nos bits daquela posição:
00101100 = x (44)
10000001 = 129
10101101 = x |129 (173)
• Operador OU EXCLUSIVO bit-a-bit (∧): um bit terá valor 1 na expressão resultante somente se ambas as expressões unidas por
esse operador tiverem o valores de bits diferentes naquela posição:
00101100 = x (44)
10100111 = 167
10001011 = x ∧ 167 (139)
Por fim, os operadores de deslocamento << e >> servem simplesmente
para mover bits para a esquerda para a direita. Cada movimentação de
bits equivale a multiplicar ou dividir (divisão inteira) por 2. Assim:
00101100 = x (44)
10110000 = x << 2 (176)
00001011 = x >> 2 (11)
2.5.6
OPERADORES DE ATRIBUIÇÃO SIMPLIFICADA
Como vimos anteriormente, muitos operadores são sempre usados em
conjunto com o operador de atribuição. Para tornar essa tarefa mais simples, a linguagem C permite simplificar algumas expressões, como mostra
a tabela abaixo:
66
Operador
+=
-=
*=
/=
%=
&=
|=
∧=
<<=
>>=
Significado
soma e atribui
subtrai e atribui
multiplica e atribui
divide e atribui quociente
divide e atribui resto
E bit-a-bit e atribui
OU bit-a-bit e atribui
OU exclusivo e atribui
desloca à esquerda e atribui
desloca à direita e atribui
x += y
x -= y
x *= y
x /= y
x %= y
x &= y
x |= y
x∧=y
x <<= y
x >>= y
Exemplo
igual x = x + y
igual x = x - y
igual x = x * y
igual x = x / y
igual x = x % y
igual x = x & y
igual
x = x |y
igual x = x ∧ y
igual x = x<<y
igual x = x>>y
Como se pode notar, esse tipo de operador é muito útil quando a variável
que vai receber o resultado da expressão é também um dos operandos da
expressão. Por exemplo, a expressão
x = x + 10 * y;
pode ser reescrita usando o operador simplificado como sendo
x += 10 * y;
2.5.7
OPERADORES DE PRÉ E PÓS-INCREMENTO
Além dos operadores simplificados, a linguagem C também possui operadores de pré e pós-incremento. Estes operadores podem ser utilizados
sempre que for necessário necessário incrementar (somar uma unidade)
ou decrementar (subtrair uma unidade) um determinado valor, como mostra a tabela abaixo:
Operador
++
--
Significado
pré ou pós incremento
pré ou pós decremento
Exemplo
++x ou x++
- -x ou x- -
Tanto o operador de incremento (++) quanto o de decremento (- -) já
possui embutida uma operação de atribuição. Note, no entanto, que esse
operador pode ser usado antes ou depois do nome da variável, com uma
diferença significativa:
• ++x: incrementa a variável x antes de utilizar seu valor;
67
• x++: incrementa a variável x depois de utilizar seu valor.
Essa diferença de sintaxe no uso do operador não tem importância se o
operador for usado sozinho, como mostra o exemplo abaixo:
Exemplo de pós e pré incremento sozinho
Pré incremento
Pós incremento
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t x = 10;
5
++x ;
6
p r i n t f ( ‘ ‘ x = %d\n ’ ’ ,
x) ;
7
system ( ‘ ‘ pause ’ ’ ) ;
8
return 0;
9 }
Saı́da x = 11
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t x = 10;
5
x ++;
6
p r i n t f ( ‘ ‘ x = %d\n ’ ’ ,
x) ;
7
system ( ‘ ‘ pause ’ ’ ) ;
8
return 0;
9 }
x = 11
Porém, se esse operador for utilizado dentro de uma expressão aritmética,
como no exemplo abaixo, a diferença é entre os dois operadores é evidente:
Exemplo de pós e pré incremento numa expressão
Pré incremento
Pós incremento
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t y , x = 10;
5
/ / incrementa , depois
atribui
6
y = ++x ;
7
p r i n t f ( ‘ ‘ x = %d\n ’ ’ ,
x) ;
8
p r i n t f ( ‘ ‘ y = %d\n ’ ’ ,
y) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
Saı́da x = 11
y = 11
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t y , x = 10;
5
/ / a t r i b u i , depois
incrementa
6
y = x ++;
7
p r i n t f ( ‘ ‘ x = %d\n ’ ’ ,
x) ;
8
p r i n t f ( ‘ ‘ y = %d\n ’ ’ ,
y) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
x = 11
y = 10
68
Como se pode ver, no primeiro exemplo o operador de pré-incremento
(++x) é a primeira coisa a ser realizada dentro da expressão. Somente
depois de incrementado o valor de x é que o mesmo é atribuı́do a variável
y. Nota-se, nesse caso, que a expressão
y = ++x;
é equivalente a fazer
x = x + 1;
y = x;
Já no segundo exemplo, o operador de pós-incremento (x++) é a última
coisa a ser realizada dentro da expressão. Primeiro atribui-se o o valor de
x para a variável y para somente depois incrementar a variável x. Nota-se,
nesse caso, que a expressão
y = x++;
é equivalente a fazer
y = x;
x = x + 1;
2.5.8
MODELADORES DE TIPOS (CASTS)
Modeladores de tipos (também chamados de type cast) são uma forma
explı́cita de conversão de tipo, onde o tipo a ser convertido é explicitamente
definido dentro de um programa. Isso é diferente da conversão implicita,
que ocorre naturalmente quando tentamos atribuir um número real para
uma variável inteira. Em linguagem C, o uso de um modelador de tipo
segue a seguinte forma geral:
(nome do tipo) expressão
Um modelador de tipo é definido pelo próprio nome do tipo entre parêntese.
Ele é colocado a frente de uma expressão e tem como objetivo forçar o resultado da expressão a ser de um tipo especificado, como mostra o exemplo abaixo:
69
Exemplos de modeladores de tipo
1
2
3
4
5
6
7
8
9
10
11
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
float x , y , f = 65.5;
x = f /10.0;
y = ( int ) ( f /10.0) ;
p r i n t f ( ‘ ‘ x = %f \n ’ ’ , x ) ;
p r i n t f ( ‘ ‘ y = %f \n ’ ’ , y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
x = 6.550000
y = 6.000000
No exemplo acima, tanto os valores de x quanto de y são obtidos utilizando
a mesma expressão. Porém, no caso da variável y (linha 6), o resultado
da expressão é convertido para o tipo inteiro (int), o que faz com que seu
resultado perca as casas decimais.
2.5.9
OPERADOR VÍRGULA “,”
Na linguagem C, o operador vı́rgula “,” pode ser utilizado de duas maneiras:
• Como pontuação. Por exemplo, para separar argumentos de uma
função:
int minha funcao(int a, float b)
• Determinar uma lista de expressões que devem ser executadas sequencialmente.
x = (y = 2, y + 3);
Nesse caso, as expressões são executadas da esquerda para a direita: o valor 2 é atribuı́do a y, o valor 3 é somado a y e o total (5)
será atribuı́do à variável x. Pode-se encadear quantos operadores “,”
forem necessários.
Na linguagem C, o operador “,” é um separador de comandos, enquanto o operador “;” é um terminador de comandos.
70
2.5.10
PRECEDÊNCIA DE OPERADORES
Como podemos ver, a linguagem C contém muitos operadores. Consequentemente, o uso de múltiplos operadores em uma única expressão
pode tornar confusa a sua interpretação. Por esse motivo, a linguagem
C possui uma série de regras de precedência de operadores. Isso permite que o compilador possa decidir corretamente qual a ordem em que
os operadores deverão ser executado em uma expressão contendo vários
operadores. As regras de precedência seguem basicamente as regras da
matemática, onde a multiplicação e a divisão são executadas antes da
soma e da subtração. Além disso, pode-se utilizar de parênteses para
forçar o compilador a executar uma parte da expressão antes das demais.
A tabela abaixo mostra as regras de precedência dos operadores presentes na linguagem C. Quanto mais alto na tabela, maior o nı́vel de precedência (prioridade) dos operadores em questão. Na primeira linha da
tablea são apresentados os operadores executados em primeiro lugar, enquanto a última linha apresenta os operadores executados por último em
uma expressão:
71
++ –
()
[ ]
.
-¿
++ –
+!∼
(tipo)
*
&
sizeof
*/%
+<< >>
< <=
> >=
== !=
&
∧
|
&&
||
?:
=
+= -=
*= /= %=
<<= >>=
&= ∧ = |=
,
MAIOR PRECEDÊNCIA
Pré incremento/decremento
Parênteses (chamada de função)
Elemento de array
Elemento de struct
Conteúdo de elemento de ponteiro para struct
Pós incremento/decremento
Adição e subtração unária
Não lógico e complemento bit-a-bit
Conversão de tipos (type cast)
Acesso ao conteúdo de ponteiro
Endereço de memória do elemento
Tamanho do elemento
Multiplicação, divisão, e módulo (resto)
Adição e subtração
Deslocamento de bits à esquerda e à direita
“Menor do que” e “menor ou igual a”
“Maior do que” e “maior ou igual a”
“Igual a” e “Diferente de”
E bit-a-bi
OU exclusivo
OU bit-a-bit
E lógico
OU lógico
Operador ternário
Atribuição
Atribuição por adição ou subtração
Atribuição por multiplicação, divisão ou módulo (resto)
Atribuição por deslocalmento de bits
Atribuição por operações lógicas
Operador vı́rgula
MENOR PRECEDÊNCIA
É possı́vel notar que alguns operadores ainda são desconhecidos para
nós, apesar de alguns possuirem o mesmo sı́mbolo usado para outro operador (como é o caso do operador de acesso ao conteúdo de ponteiro, o
qual possui o mesmo sı́mbolo do operador de multiplicação “*”). Esses
operadores serão explicados ao longo da apostila, conforme surja a necessidade de utilizá-os.
72
3
COMANDOS DE CONTROLE CONDICIONAL
Os programas escritos até o momento são programas sequenciais: um
comando é executado após o outro, do começo ao fim do programa, na
ordem em que foram declarados no código fonte. Nenhum comando é
ignorado.
Entretanto, há casos em que é preciso que um bloco de comandos seja
executado somente se uma determinada condição for verdadeira. Para
isso, precisamos de uma estrutura de seleção, ou um comando de controle condicional, que permita selecionar o conjunto de comandos a ser
executado. Isso é muito similar ao que ocorre em um fluxograma, onde o
sı́mbolo do losango permitia escolher entre diferentes caminhos com base
em uma condição do tipo verdadeiro/falso:
Nesta seção iremos ver como funcionam cada uma das estruturas de seleção
presentes na linguagem C.
3.1
DEFININDO UMA CONDIÇÃO
Por condição, entende-se qualquer expressão relacional (ou seja, que use
os operadores >, <, >=, <=, == ou !=) que resulte numa resposta do tipo
verdadeiro ou falso. Por exemplo, para a condição x > 0 temos que:
• Se o valor de x for um valor POSITIVO, a condição será considerada
verdadeira;
• Se o valor de x igual a ZERO ou NEGATIVO, a condição será considerada falsa.
73
Já uma expressão condicional é qualquer expressão que resulte numa resposta do tipo verdadeiro ou falso. Ela pode ser construı́da utilizando operadores:
• Matemáticos : +,-, *, /, %
• Relacionais: >, <, >=, <=, ==, !=
• Lógicos: &&, ||
Esses operadores permitem criar condições mais complexas, como mostra
o exemplo abaixo, onde se deseja saber se a divisão de x por 2 é maior do
que o valor de y menos 3:
x/2 > y − 3
Uma expressão condicional pode utilizar operadores dos
tipos: matemáticos, relacionais e/ou lógicos.
x é maior ou igual a y?
x >= y
x é maior do que y+2?
x > y+2
x-5 é diferente de y+3?
x-5 != y+3
x é maior do que y e menor do que z?
(x > y) && (x < z)
Quando o compilador avalia uma condição, ele quer um valor de retorno
(verdadeiro ou falso) para poder tomar a decisão. No entanto, esta expressão condicional não necessita ser uma expressão no sentido convencional.
Uma variável sozinha pode ser uma “expressão condicional” e retornar o seu próprio valor.
Para entender isso, é importante lembrar que o computador trabalha, internamente, em termos de 0’s e 1’s. Assim, se uma condição
74
• é considerada FALSA, o computador considera que a condição possui valor ZERO;
• é considerada VERDADEIRA, o computador considera que a condição
possui valor DIFERENTE DE ZERO.
Isto significa que o valor de uma variável do tipo inteiro pode ser a resposta
de uma expressão condicional:
• se o valor da variável for igual a ZERO, a condição é FALSA;
• se o valor da variável for DIFERENTE DE ZERO, a condição é VERDADEIRA.
Abaixo é possı́vel ver algumas expressões que são consideradas equivalentes pelo compilador:
Se a variável possui valor DIFERENTE DE ZERO...
(num != 0)
...ela sozinha retorna uma valor que é considerado VERDADEIRO pelo computador.
(num)
e
Se a variável possui valor igual a ZERO...
(num == 0)
...sua negação retorna um valor que é considerado VERDADEIRO pelo computador.
(!num)
3.2
COMANDO IF
Na linguagem C, o comando if é utilizado sempre que é necessário escolher entre dois caminhos dentro do programa, ou quando se deseja executar um ou mais comandos que estejam sujeitos ao resultado de um teste.
A forma geral de um comando if é:
75
if (condição) {
sequência de comandos;
}
Na execução do comando if a condição será avaliada e:
• se a condição for verdadeira a sequência de comandos será executada;
• se a condição for falsa a sequência de comandos não será executada, e o programa irá continuar a partir do primeiro comando seguinte ao final do comando if.
Abaixo, tem-se um exemplo de um programa que lê um número inteiro
digitado pelo usuário e informa se o mesmo é maior do que 10:
Exemplo: comando if
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,&num) ;
i f (num > 10)
p r i n t f ( ‘ ‘O numero e maior do que 10\n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, a mensagem de que o número é maior do que 10
será exibida apenas se a condição for verdadeira. Se a condição for falsa,
nenhuma mensagem será escrita na tela. Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação de como os comandos
do exemplo anterior são um-a-um executados durante a execução do programa:
76
Diferente da maioria dos comandos, não se usa o ponto e
vı́rgula (;) depois da condição do comando if.
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,&num) ;
i f (num > 10) ; / / ERRADO
p r i n t f ( ‘ ‘O numero e maior que 10\n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Na linguagem C, o operador ponto e vı́rgula (;) é utilizado para separar as
instruções do programa. Colocá-lo logo após o comando if, como exemplificado acima, faz com que o compilador entenda que o comando if já
terminou e trate o comando seguinte (printf) como se o mesmo estivesse
fora do if. No exemplo acima, a mensagem de que o número é maior do
que 10 será exibida independente do valor do número.
77
O compilador não irá acusar um erro se colocarmos o operador ponto e vı́rgula (;) após o comando if, mas a lógica
do programa poderá estar errada.
3.2.1
USO DAS CHAVES {}
No comando if, e em diversos outros comandos da linguagem C, usa-se
os operadores de chaves { } para delimitar um bloco de instruções.
Por definição, comandos de condição (if e else) ou
repetição (while, for e do while) atuam apenas sobre o
comando seguinte a eles.
Desse modo, se o programador desejar que mais de uma instrução seja
executada por aquele comando if, esse conjunto de instruções deve estar
contido dentro de um bloco delimitado por chaves { }.
if (condição) {
comando 1;
comando 2;
...
comando n;
}
78
As chaves podem ser ignoradas se o comando contido dentro do if for único.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3.3
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,&num) ;
i f (num > 10)
p r i n t f ( ‘ ‘O numero e maior que 10\n ’ ’ ) ;
/ ∗OU
i f (num > 10) {
p r i n t f ( ‘ ‘O numero e maior que 10\n ’ ’ ) ;
}
∗/
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
COMANDO ELSE
O comando else pode ser entendido como sendo um complemento do
comando if. Ele auxı́lia o comando if na tarefa de escolher dentre os vários
caminhos a ser segudo dentro do programa.
O comando else é opcional e sua sequência de comandos
somente será executada se o valor da condição que está
sendo testada pelo comando if for FALSA.
A forma geral de um comando else é:
if (condição) {
primeira sequência de comandos;
}
else{
segunda sequência de comandos;
}
79
Se o comando if diz o que fazer quando a condição é verdadeira, o comando else trata da condição quando ela é
falsa.
Isso fica bem claro quando olhamos a representação do comando else em
um fluxograma:
Antes, na execução do comando if, a condição era avaliada e:
• se a condição fosse verdadeira, a primeira sequência de comandos
era executada;
• se a condição fosse falsa, a sequência de comandos não era executada e o programa seguia o seu fluxo padrão.
Com o comando else, temos agora que:
• se a condição for verdadeira, a primeira seqüência de comandos
(bloco if) será executada;
• se a condição for falsa, a segunda seqüência de comandos (bloco
else) será executada.
Abaixo, tem-se um exemplo de um programa que lê um número inteiro
digitado pelo usuário e informa se o mesmo é ou não igual a 10:
80
Exemplo: comando if-else
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &num) ;
i f (num == 10) {
p r i n t f ( ‘ ‘O numero e i g u a l a 10.\ n ’ ’ ) ;
} else {
p r i n t f ( ‘ ‘O numero e d i f e r e n t e de 10.\ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados durante a execução do programa:
81
O comando else não tem condição. Ele é o caso contrário
da condição do if.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &num) ;
i f (num == 10) {
p r i n t f ( ‘ ‘O numero e i g u a l a 10.\ n ’ ’ ) ;
} else (num ! = 10) { / / ERRO
p r i n t f ( ‘ ‘O numero e d i f e r e n t e de 10.\ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O comando else deve ser ser entendido como sendo um complemento do
comando if. Ele diz quais comandos se deve executar se a condição do
comando if for falsa. Portanto, não é necessário estabelecer uma condição
para o comando else: ele é o oposto do if.
Como no caso do if, não se usa o ponto e vı́rgula (;) depois
do comando else.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &num) ;
i f (num == 10) {
p r i n t f ( ‘ ‘O numero e i g u a l a 10.\ n ’ ’ ) ;
} else ; { / / ERRADO
p r i n t f ( ‘ ‘O numero e d i f e r e n t e de 10.\ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Como no caso do if, colocar o operador de ponto e vı́rgula (;) logo após o
comando else, faz com que o compilador entenda que o comando else já
82
terminou e trate o comando seguinte (printf) como se o mesmo estivesse
fora do else. No exemplo acima, a mensagem de que o número é diferente
de 10 será exibida independente do valor do número.
A seqüência de comandos do if é independente da
seqüência de comandos do else. Cada comando tem o
seu próprio conjunto de chaves {}.
Se o comando if for executado em um programa, o seu comando else
não será executado. Portanto, não faz sentido usar o mesmo conjunto de
chaves {}para definir os dois conjuntos de comandos.
Uso das chaves no comando if-else
Errado
Certo
1
2
3
4
5
6
i f ( condicao ) {
seq ü ência de comandos ;
}
else {
seq ü ência de comandos ;
}
1 i f ( condicao ) {
2
seq ü ência de comandos ;
3 else
4
seq ü ência de comandos ;
5 }
Como no caso do comando if, as chaves podem ser ignoradas se o comando contido dentro do else for único.
3.4
ANINHAMENTO DE IF
Um if aninhado é simplesmente um comando if utilizado dentro do bloco
de comandos de um outro if (ou else) mais externo. Basicamente, é um
comando if dentro de outro.
A forma geral de um comando if aninhado é:
if(condição 1) {
seqüência de comandos;
if(condição 2) {
seqüência de comandos;
if...
83
}
else{
seqüência de comandos;
if...
}
} else{
seqüência de comandos;
}
Em um aninhamento de if’s, o programa começa a testar as condições
começando pela condição 1. Se o resultado dessa condição for diferente
de zero (verdadeiro), o programa executará o bloco de comando associados a ela. Do contrário, irá executar o bloco de comando associados ao
comando else correspondente, se ele existir. Esse processo se repete para
cada comando if que o programa encontrar dentro do bloco de comando
que ele executar.
O aninhamento de if’s é muito útil quando se tem mais do que dois caminhos para executar dentro de um programa. Por exemplo, o comando if é
suficiente para dizer se um número é maior do que outro número ou não.
Porém, ele sozinho é incapaz de dizer se esse mesmo número é maior,
menor ou igual ao outro como mostra o exemplo abaixo:
Exemplo: aninhamento de if
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
i n t main ( ) {
4
i n t num ;
5
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
6
s c a n f ( ‘ ‘ % d ’ ’ , &num) ;
7
i f (num == 10) {
8
p r i n t f ( ‘ ‘O numero e i g u a l a 10.\ n ’ ’ ) ;
9
} else {
10
i f (num > 10)
11
p r i n t f ( ‘ ‘O numero e maior que 10.\ n ’ ’ ) ;
12
else
13
p r i n t f ( ‘ ‘O numero e menor que 10.\ n ’ ’ ) ;
14
}
15
system ( ‘ ‘ pause ’ ’ ) ;
16
return 0;
17 }
84
Isso fica bem claro quando olhamos a representação do aninhamento de
if’s em um fluxograma:
O único cuidado que devemos ter no aninhamento de if’s é
o de saber exatamente a qual if um determinado else está
ligado.
Esse cuidado fica claro no exemplo abaixo: apesar do comando else estar alinhado com o primeiro comando if, ele está na verdade associado ao
segundo if. Isso acontece porque o comando else é sempre associado ao
primeiro comando if encontrado antes dele dentro de um bloco de comandos.
if (cond1)
if (cond2)
seqüência de comandos;
else
seqüência de comandos;
No exemplo anterior, para fazer com que o comando else fique associado
ao primeiro comando if é necessário definir um novo bloco de comandos
(usando os operadores de chaves { }) para isolar o comando if mais interno.
85
if (cond1) {
if (cond2)
seqüência de comandos;
} else
seqüência de comandos;
Não existe aninhamento de else’s.
O comando else é o caso contrário da condição do comando if. Assim,
para cada else deve existir um if anterior, porém nem todo if precisa ter um
else.
if (cond1)
seqüência de comandos;
else
seqüência de comandos;
else //ERRO!
seqüência de comandos;
3.5
OPERADOR ?
O operador ? é também conhecido como operador ternário. Trata-se de
uma simplificação do comando if-else na sua forma mais simples, ou seja,
com apenas um comando e não blocos de comandos.
A forma geral do operador ? é:
expressão condicional ? expressão1 : expressão2;
O funcionamento do operador ? é idêntico ao do comando if-else: primeiramente, a expressão condicional será avaliada e
• se essa condição for verdadeira, o valor da expressão1 será o resultado da expressão condicional;
• se essa condição for falsa, o valor da expressão2 será o resultado
da expressão condicional;
86
O operador ? é tipicamente utilizado para atribuições condicionais.
O exemplo abaixo mostra como uma expressão de atribuição pode ser
simplificada utilizando o operador ternário:
Usando if-else
Usando o operador ternário
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int x , y , z ;
5
p r i n t f ( ‘ ‘ Digite x : ’ ’ ) ;
6
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
7
p r i n t f ( ‘ ‘ Digite y : ’ ’ ) ;
8
s c a n f ( ‘ ‘ % d ’ ’ ,& y ) ;
9
if (x > y)
10
z = x;
11
else
12
z = y;
13
p r i n t f ( ‘ ‘ Maior = %d ’ ’ ,
z) ;
14
system ( ‘ ‘ pause ’ ’ ) ;
15
return 0;
16 }
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int x , y , z ;
5
p r i n t f ( ‘ ‘ Digite x : ’ ’ ) ;
6
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
7
p r i n t f ( ‘ ‘ Digite y : ’ ’ ) ;
8
s c a n f ( ‘ ‘ % d ’ ’ ,& y ) ;
9
z = x > y ? x : y;
10
p r i n t f ( ‘ ‘ Maior = %d ’ ’ ,
z) ;
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
O operador ? é limitado e por isso não atende a uma gama muito grande de
casos que o comando if-else atenderia. Porém, ele pode ser usado para
simplificar expressões complicadas. Uma aplicação interessante é a do
contador circular, onde uma variável é incrementada até um valor máximo
e, sempre que atinge esse valor, a variável é zerada.
index = (index== 3) ? 0: ++index;
87
Apesar de limitado, o operador ?
atribuições apenas.
não é restrito a
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t num ;
5
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
6
s c a n f ( ‘ ‘ % d ’ ’ , &num) ;
7
(num == 10) ? p r i n t f ( ‘ ‘O numero e i g u a l a 10 .\ n
’ ’ ) : p r i n t f ( ‘ ‘O numero e d i f e r e n t e de 10.\
n’ ’);
8
system ( ‘ ‘ pause ’ ’ ) ;
9
return 0;
10 }
3.6
COMANDO SWITCH
Além dos comandos if e else, a linguagem C possui um comando de
seleção múltipla chamado switch. Esse comando é muito parecido com o
aninhamendo de comandos if-else-if.
O comando switch é muito mais limitado que o comando
if-else: enquanto o comando if pode testar expressões
lógicas ou relacionais, o comando switch somente verifica
se uma variável (do tipo int ou char) é ou não igual a um
certo valor constante.
88
A forma geral do comando switch é:
switch (variável) {
case valor1:
seqüência de comandos;
break;
case valor2:
seqüência de comandos;
break;
...
case valorN:
seqüência de comandos;
break;
default:
seqüência de comandos; }
O comando switch é indicado quando se deseja testar uma
variável em relação a diversos valores pré-estabelecidos.
Na execução do comando switch, o valor da variável é comparado, na
ordem, com cada um dos valores definidos pelo comando case. Se um
desse valores for igual ao valor da variável, a sequência de comandos
daquele comando case é executado pelo programa.
Abaixo, tem-se um exemplo de um programa que lê um caractere digitado
pelo usuário e informa se o mesmo é um sı́mbolo de pontuação:
89
Exemplo: comando switch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char ch ;
p r i n t f ( ‘ ‘ D i g i t e um simbolo de pontuacao : ’ ’ ) ;
ch = g e t c h a r ( ) ;
switch ( ch ) {
case ’ . ’ : p r i n t f ( ‘ ‘ Ponto . \ n ’ ’ ) ; break ;
case ’ , ’ : p r i n t f ( ‘ ‘ V i r g u l a . \ n ’ ’ ) ; break ;
case ’ : ’ : p r i n t f ( ‘ ‘ Dois pontos . \ n ’ ’ ) ; break ;
case ’ ; ’ : p r i n t f ( ‘ ‘ Ponto e v i r g u l a . \ n ’ ’ ) ; break ;
d e f a u l t : p r i n t f ( ‘ ‘ Nao eh pontuacao . \ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, será pedido ao usuário que digite um caractere. O valor
desse caractere será comparado com um conjunto de possı́veis sı́mbolos
de pontuação, cada qual identificado em um comando case. Note que,
se o caractere digitado pelo usuário não for um sı́mbolo de pontuação, a
seqüência de comandos dentro do comando default será exectada.
Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados durante a execução do programa:
90
O comando default é opcional e sua seqüência de comandos somente será executada se o valor da variável que
está sendo testada pelo comando switch não for igual a
nenhum dos valores dos comandos case.
O exemplo anterior do comando switch poderia facilmente ser reescrito
com o aninhamento de comandos if-else-if como se nota abaixo:
Exemplo: simulando o comando switch com if-else-if
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char ch ;
p r i n t f ( ‘ ‘ D i g i t e um simbolo de pontuacao : ’ ’ ) ;
ch = g e t c h a r ( ) ;
i f ( ch == ’ . ’ )
p r i n t f ( ‘ ‘ Ponto . \ n ’ ’ ) ;
else
i f ( ch == ’ , ’ )
p r i n t f ( ‘ ‘ Virgula .\n ’ ’ ) ;
else
i f ( ch == ’ : ’ )
p r i n t f ( ‘ ‘ Dois pontos . \ n ’ ’ ) ;
else
i f ( ch == ’ ; ’ )
p r i n t f ( ‘ ‘ Ponto e v i r g u l a . \ n ’ ’ ) ;
else
p r i n t f ( ‘ ‘ Nao eh pontuacao . \ n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Como se pode notar, o comando switch apresenta uma solução muito mais
elegante que o aninhamento de comandos if-else-if quando se necessita
comparar o valor de uma variável.
3.6.1
USO DO COMANDO BREAK NO SWITCH
Apesar das semelhanças entre os dois comandos, o comando switch e o
aninhamento de comandos if-else-if, existe uma diferença muito importante
entre esses dois comandos: o comando break.
91
Quando o valor associado a um comando case é igual
ao valor da variável do switch a respectiva seqüência de
comandos é executada até encontrar um comando break.
Caso o comando break não exista, a seqüência de comandos do case seguinte também será executada e assim por
diante
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char ch ;
p r i n t f ( ‘ ‘ D i g i t e um simbolo de pontuacao : ’ ’ ) ;
ch = g e t c h a r ( ) ;
switch ( ch ) {
case ’ . ’ : p r i n t f ( ‘ ‘ Ponto . \ n ’ ’ ) ;
case ’ , ’ : p r i n t f ( ‘ ‘ V i r g u l a . \ n ’ ’ ) ;
case ’ : ’ : p r i n t f ( ‘ ‘ Dois pontos . \ n ’ ’ ) ;
case ’ ; ’ : p r i n t f ( ‘ ‘ Ponto e v i r g u l a . \ n ’ ’ ) ;
d e f a u l t : p r i n t f ( ‘ ‘ Nao eh pontuacao . \ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, no exemplo acima, que caso o usuário digite o sı́mbolo de ponto (.)
todas as mensagens serão escritas na tela de saı́da.
O comando break é opcional e faz com que o comando
switch seja interrompido assim que uma das sequência de
comandos seja executada.
Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados durante a execução do programa:
92
De modo geral, é quase certo que se venha a usar o comando break dentro
do switch. Porém a sua ausência pode ser muito útil em algumas situações.
Por exemplo, quando queremos que uma ou mais sequências de comandos sejam executadas a depender do valor da variável do switch, como
mostra o exemplo abaixo:
Exemplo: comando switch sem break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t num ;
p r i n t f ( ‘ ‘ D i g i t e um numero i n t e i r o de 0 a 9 : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,&num) ;
switch (num) {
case 9 : p r i n t f ( ‘ ‘ Nove\n ’ ’ ) ;
case 8 : p r i n t f ( ‘ ‘ O i t o \n ’ ’ ) ;
case 7 : p r i n t f ( ‘ ‘ Sete \n ’ ’ ) ;
case 6 : p r i n t f ( ‘ ‘ Seis \n ’ ’ ) ;
case 5 : p r i n t f ( ‘ ‘ Cinco \n ’ ’ ) ;
case 4 : p r i n t f ( ‘ ‘ Quatro \n ’ ’ ) ;
case 3 : p r i n t f ( ‘ ‘ Tres \n ’ ’ ) ;
case 2 : p r i n t f ( ‘ ‘ Dois \n ’ ’ ) ;
case 1 : p r i n t f ( ‘ ‘Um\n ’ ’ ) ;
case 0 : p r i n t f ( ‘ ‘ Zero \n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
93
Note, no exemplo acima, que caso o usuário digite o valor 9, todas as
mensagens serão escritas na tela de saı́da. Caso o usuário digite o valor
5, apenas as mensagens desse case e as abaixo dele serão escritas na
tela de saı́da.
3.6.2
USO DAS CHAVES {}NO CASE
De modo geral, a sequência de comandos do case não precisam estar
entre chaves {}.
Porém, se o primeiro comando dentro de um case for a
declaração de uma variável, será necessário colocar todos
os comandos desse case dentro de um par de chaves {}.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char ch ;
int a ,b;
p r i n t f ( ‘ ‘ D i g i t e uma operacao matematica : ’ ’ ) ;
ch = g e t c h a r ( ) ;
p r i n t f ( ‘ ‘ D i g i t e d o i s numeros i n t e i r o s : ’ ’ ) ;
s c a n f ( ‘ ‘ % d%d ’ ’ ,&a ,& b ) ;
switch ( ch ) {
case ’ + ’ : {
int c = a + b;
p r i n t f ( ‘ ‘ Soma : %d\n ’ ’ , c ) ; }
break ;
case ’− ’ : {
int d = a − b;
p r i n t f ( ‘ ‘ Subtracao : %d\n ’ ’ , d ) ; }
break ;
case ’ ∗ ’ : {
int e = a ∗ b;
p r i n t f ( ‘ ‘ Produto : %d\n ’ ’ , e ) ; }
break ;
case ’ / ’ : {
int f = a / b;
p r i n t f ( ‘ ‘ D i v i s a o : %d\n ’ ’ , f ) ; }
break ;
d e f a u l t : p r i n t f ( ‘ ‘ Nao eh operacao . \ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
94
A explicação para esse comportamento do switch se deve a uma regra da
linguagem, que especı́fica que um salto condicional não pode pular uma
declaração de variável no mesmo escopo. Quando colocamos as chaves
{}depois do comando case e antes do comando break, estamos criando
um novo escopo, ou seja, a variável declarada existe apenas dentro desse
par de chaves. Portanto, ela pode ser “pulada” por um salto condicional.
95
4
COMANDOS DE REPETIÇÃO
4.1
REPETIÇÃO POR CONDIÇÃO
Na seção anterior, vimos como realizar desvios condicionais em um programa. Desse modo, criamos programas em que um bloco de comandos
é executado somente se uma determinada condição é verdadeira.
Entretanto, há casos em que é preciso que um bloco de comandos seja
executado mais de uma vez se uma determinada condição for verdadeira:
enquanto condição faça
sequência de comandos;
fim enquanto
Para isso, precisamos de uma estrutura de repetição que permita executar
um conjunto de comandos quantas vezes forem necessárias. Isso é muito
similar ao que ocorre em um fluxograma, onde o sı́mbolo do losango permitia escolher entre diferentes caminhos com base em uma condição do
tipo verdadeiro/falso, com a diferença de que agora o fluxo do programa é
desviado novamente para a condição ao final da sequência de comandos:
Exemplo: Pseudo-código e fluxograma
1 Leia B;
2 Enquanto A < B
3
A recebe A + 1 ;
4
Imprima A ;
5 Fim Enquanto
De acordo com a condição, os comandos serão repetidos
zero (se falsa) ou mais vezes (enquanto a condição for verdadeira). Essa estrutura normalmente é denominada laço
ou loop.
96
Note que a sequência de comandos a ser repetida está subordinada a
uma condição. Por condição, entende-se qualquer expressão relacional
(ou seja, que use os operadores >, <, >=, <=, == ou !=) que resulte numa
resposta do tipo verdadeiro ou falso. A condição pode ainda ser uma
expressão que utiliza operadores:
• Matemáticos : +,-, *, /, %
• Relacionais: >, <, >=, <=, ==, !=
• Lógicos: &&, ||
Na execução do comando enquanto, a condição será avaliada e:
• se a condição for considerada verdadeira, a sequência de comandos será executada. Ao final da sequência de comandos, o fluxo do
programa é desviado novamente para o teste da condição;
• se a condição for considerada falsa, a sequência de comandos não
será executada.
Como no caso do comando if, uma variável sozinha pode
ser uma “expressão condicional” e retornar o seu próprio
valor para um comando de repetição.
4.1.1
LAÇO INFINITO
Um laço infinito (ou loop infinito) é uma sequência de comandos em um
programa de computador que sempre se repete, ou seja, infinitamente.
Isso geralmente ocorre por algum erro de programação, quando
• não definimos uma condição de parada;
• a condição de parada existe, mas nunca é atingida.
Basicamente, um laço infinito ocorre quando cometemos algum erro ao
especificar a condição (ou expressão condicional) que controla a repetição,
como é o caso do exemplo abaixo. Note que nesse exemplo, o valor de X
é sempre diminuı́do em uma unidade, ou seja, fica mais negativo a cada
passo. Portanto, a repetição nunca atinge a condição de parada:
97
Exemplo 1: loop infinito
1 X recebe 4 ;
2 enquanto (X < 5 ) f a ç a
3
X recebe X − 1 ;
4
Imprima X ;
5 f i m enquanto
Outro erro comum que produz um laço infinito é o de esquecer de algum
comando dentro da sequência de comandos da repetição, como mostra o
exemplo abaixo. Note que nesse exemplo, o valor de X nunca é modificado dentro da repetição. Portanto a condição é sempre verdadeira, e a
repetição nunc termina:
Exemplo 2: loop infinito
1 X recebe 4 ;
2 enquanto (X < 5 ) f a ç a
3
Imprima X ;
4 f i m enquanto
4.2
COMANDO WHILE
O comando while equivale ao comando “enquanto” utilizado nos pseudocódigos apresentados até agora.
A forma geral de um comando while é:
while (condição){
sequência de comandos;
}
Na execução do comando while, a condição será avaliada e:
• se a condição for considerada verdadeira (ou possuir valor diferente
de zero), a sequência de comandos será executada. Ao final da
sequência de comandos, o fluxo do programa é desviado novamente
para o teste da condição;
98
• se a condição for considerada falsa (ou possuir valor igual a zero), a
sequência de comandos não será executada.
Abaixo, tem-se um exemplo de um programa que lê dois números inteiros
a e b digitados pelo usuário e imprime na tela todos os números inteiros
entre a e b:
Exemplo: comando while
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
while ( a < b ) {
a = a + 1;
p r i n t f ( ‘ ‘ % d \n ’ ’ , a ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
’ ’);
Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados durante a execução do programa:
99
O comando while segue todas as recomendações definidas para o comando if quanto ao uso das chaves e
definição da condição usada.
Isso significa que a condição pode ser qualquer expressão que resulte
numa resposta do tipo falso (zero) ou verdadeiro (diferente de zero), e que
utiliza operadores dos tipos matemáticos, relacionais e/ou lógicos.
Como nos comandos condicionais, o comando while atua apenas sobre o
comando seguinte a ele. Se quisermos que ele execute uma sequência
de comandos, é preciso definir essa sequência de comandos dentro de
chaves {}.
100
Como no comando if-else, não se usa o ponto e vı́rgula (;)
depois da condição do comando while.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
while ( a < b ) ; { / / ERRADO!
a = a + 1;
p r i n t f ( ‘ ‘ % d \n ’ ’ , a ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
’ ’);
Como no caso dos comandos condicionais, colocar o operador de ponto e
vı́rgula (;) logo após o comando while, faz com que o compilador entenda
que o comando while já terminou e trate o comando seguinte (a = a + 1)
como se o mesmo estivesse fora do while. No exemplo acima, temos um
laço infinito (o valor de a e b nunca mudam, portanto a condição de parada
nunca é atingida).
É responsabilidade do programador modificar o valor de
algum dos elementos usados na condição para evitar que
ocorra um laço infinito.
4.3
COMANDO FOR
O comando for é muito similar ao comando while visto anteriormente. Basicamente, o comando for é usado para repetir um comando, ou uma
sequência de comandos, diversas vezes.
A forma geral de um comando for é:
for (inicialização; condição; incremento) {
sequência de comandos;
}
101
Na execução do comando for, a seguinte sequência de passo é realizada:
• a clausula inicialização é executada: nela as variáveis recebem uma
valor inicial para usar dentro do for.
• a condição é testada:
– se a condição for considerada verdadeira (ou possuir valor diferente de zero), a sequência de comandos será executada. Ao
final da sequência de comandos, o fluxo do programa é desviado para o incremento;
– se a condição for considerada falsa (ou possuir valor igual a
zero), a sequência de comandos não será executada (fim do
comando for).
• incremento: terminada a execução da sequência de comandos, ocorre
a etapa de incremento das variáveis usadas no for. Ao final dessa
etapa, o fluxo do programa é novamente desviado para a condição.
Abaixo, tem-se um exemplo de um programa que lê dois números inteiros
a e b digitados pelo usuário e imprime na tela todos os números inteiros
entre a e b (incluindo a e b):
Exemplo: comando for
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b , c ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
f o r ( c = a ; c <= b ; c ++) {
p r i n t f ( ‘ ‘ % d \n ’ ’ , c ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
’ ’);
No exemplo acima, a variável c é inicializada como valor de a (c = a).
Em seguida, o valor de c é comparado com o valor de b (c <= b). Por
fim, se a sequência de comandos foi executada, o valor da variável c será
incrementado em uma unidade (c++).
102
Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados durante a execução do programa:
O comando for segue todas as recomendações definidas
para o comando if e while quanto ao uso das chaves e
definição da condição usada.
Isso significa que a condição pode ser qualquer expressão que resulte
numa resposta do tipo falso (zero) ou verdadeiro (diferente de zero), e que
utiliza operadores dos tipos matemáticos, relacionais e/ou lógicos.
Como nos comandos condicionais, o comando while atua apenas sobre o
comando seguinte a ele. Se quisermos que ele execute uma sequência
de comandos, é preciso definir essa sequência de comandos dentro de
chaves {}.
103
Exemplo: for versus while
while
for
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t i , soma = 0 ;
5
f o r ( i = 1 ; i <= 1 0 ; i
++) {
6
soma = soma + i ;
7
}
8
p r i n t f ( ‘ ‘ Soma = %d \n ’
’ ,soma ) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
4.3.1
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t i , soma = 0 ;
5
i = 1
6
while ( i <= 10) {
7
soma = soma + i ;
8
i ++;
9
}
10
p r i n t f ( ‘ ‘ Soma = %d \n ’
’ ,soma ) ;
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
OMITINDO UMA CLAUSULA DO COMANDO FOR
Dependendo da situação em que o comando for é utilizado, podemos omitir
qualquer uma de suas cláusulas:
• inicialização;
• condição;
• incremento.
Independente de qual cláusula é omitida, o comando for
exige que se coloque os dois operadores de ponto e vı́rgula
(;).
O comando for exige que se coloque os dois operadores de ponto e vı́rgula
(;) pois é este operador que indica a separação entre as cláusulas de
inicialização, condição e incremento. Sem elas, o compilador não tem certeza de qual cláusula foi omitida.
Abaixo, são apresentados três exemplos de comando for onde, em cada
um deles, uma das cláusulas é omitida.
COMANDO FOR SEM INICIALIZAÇÃO
No exemplo abaixo, a variável a é utilizada nas cláusulas de condição e incremento do comando for. Como a variável a teve seu valor inicial definido
104
através de um comando de leitura do teclado (scanf), não é necessário a
etapa de inicialização do comando for para definir o seu valor.
Exemplo: comando for sem inicialização
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b , c ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
f o r ( ; a <= b ; a++) {
p r i n t f ( ‘ ‘ % d \n ’ ’ , a ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
’ ’);
COMANDO FOR SEM CONDIÇÃO
Ao omitir a condição do comando for, criamos um laço infinito.
Para o comando for, a ausência da cláusula de condição é considerada
como uma condição que é sempre verdadeira. Sendo a condição sempre
verdadeira, não existe condição de parada para o comando for, o qual vai
ser executado infinitamente.
105
Exemplo: comando for sem condição
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b , c ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
/ / o comando f o r abaixo é um l a ç o i n f i n i t o
f o r ( c = a ; ; c ++) {
p r i n t f ( ‘ ‘ % d \n ’ ’ , c ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
COMANDO FOR SEM INCREMENTO
Por último, temos um exemplo de comando for sem a cláusula de incremento. Nessa etapa do comando for, um novo valor é atribuı́do para uma
(ou mais) varáveis utilizadas.
Exemplo: comando for sem incremento
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b , c ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
f o r ( c = a ; c <= b ; ) {
p r i n t f ( ‘ ‘ % d \n ’ ’ , c ) ;
c ++;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
’ ’);
No exemplo acima, a cláusula de incremento foi omitida da declaração do
comando for. Para evitar a criação de uma laço infinito (onde a condição
de parada existe, mas nunca é atingida), foi colocado um comando de incremento (c++) dentro da sequência de comandos do for. Perceba que,
106
desse modo, o comando for fica mais parecido com o comando while, já
que agora se pode definir em qual momento o incremento vai ser executado, e não apenas no final.
A cláusula de incremento é utilizada para atribuir um novo
valor a uma ou mais variáveis durante o comando for. Essa
atribuição não está restrita a apenas o operador de incremento (++).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b , c ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
’ ’);
’ ’);
/ / incremento de duas unidades
f o r ( c = a ; c <= b ; c=c +2) {
p r i n t f ( ‘ ‘ % d \n ’ ’ , c ) ;
}
/ / novo v a l o r é l i d o do t e c l a d o
f o r ( c = a ; c <= b ; s c a n f ( ‘ ‘ % d ’ ’ ,& c ) ) {
p r i n t f ( ‘ ‘ % d \n ’ ’ , c ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Nesse exemplo, fica claro que a cláusula de incremento pode conter qualquer comando que altere o valor de uma das variáveis utilizadas pelo comando for.
4.3.2
USANDO O OPERADOR DE VÍRGULA (,) NO COMANDO FOR
Na linguagem C, o operador “,” é um separador de comandos. Ele permite
determinar uma lista de expressões que devem ser executadas sequencialmente, inclusive dentro do comando for.
107
O operador de vı́rgula (,) pode ser usado em qualquer uma
das cláusulas.
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int i , j ;
f o r ( i = 0 , j = 100; i < j ; i ++ , j −−){
p r i n t f ( ‘ ‘ i = %d e j = %d \n ’ ’ , i , j ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, foram definidos dois comandos para a cláusula de
inicialização: i = 0 e j = 100. Cada comando na inicialização é separado
pelo operador de vı́rgula (,). A cláusula de inicialização só termina quando
o operador de ponto e vı́rgula (;) é encontrado. Na fase de incremento,
novamente o valor das duas variáveis é modificado: o valor de i é incrementado (i++) enquanto o de j é decrementado (j–). Novamente, cada
comando na cláusula de incremento é separado pelo operador de vı́rgula
(,).
A variável utilizada no laço for não precisa ser necessariamente do tipo int. Pode-se, por exemplo, usar uma variável
do tipo char para imprimir uma sequência de caracteres.
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char c ;
f o r ( c = ’A ’ ; c <= ’ Z ’ ; c ++) {
p r i n t f ( ‘ ‘ L e t r a = %c\n ’ ’ , c ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Nesse exemplo, utilizamos uma variável do tipo char para controle do laço.
Essa variável se inicia com o caractere letra “A” e o laço é executado até
que a variável do laço possua como valor o caractere “Z”.
108
4.4
COMANDO DO-WHILE
O comando do-while é bastante semelhante ao comando while visto anteriormente. Sua principal diferença é com relação a avaliação da condição:
enquanto o comando while avalia a condição para depois executar uma
sequência de comandos, o comando do-while executa uma sequência de
comandos para depois testar a condição.
A forma geral de um comando do-while é:
do{
sequência de comandos;
} while(condição);
Na execução do comando do-while, a seguinte ordem de passos é executada:
• a sequência de comandos é executada;
• a condição é avaliada:
– se a condição for considerada verdadeira (ou possuir valor diferente de zero), o fluxo do programa é desviado novamente
para o comando do, de modo que a sequência de comandos
seja executada novamente;
– se a condição for considerada falsa (ou possuir valor igual a
zero) o laço termina (fim do comando do-while).
O comando do-while é utilizado sempre que se desejar que
a sequência de comandos seja executada pelo menos uma
vez.
No comando while, a condição é sempre avaliada antes da sequência de
comandos. Isso significa que a condição pode ser falsa logo na primeira
repetição do comando while, o que faria com que a sequência de comandos não fosse executada nenhuma vez. Portanto, o comando while pode
repetir uma sequência de comandos zero ou mais vezes.
Já no comando do-while, a sequência de comandos é executada primeiro.
Mesmo que a condição seja falsa logo na primeira repetição do comando
do-while, a sequência de comandos terá sido executada pelo menos uma
vez. Portanto, o comando do-while pode repetir uma sequência de comandos uma ou mais vezes.
109
O comando do-while segue todas as recomendações definidas para o comando if quanto ao uso das chaves e
definição da condição usada.
Abaixo, tem-se um exemplo de um programa que exibe um menu de opções
para o usuário e espera que ele digite uma das suas opções:
Exemplo: comando do-while
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int i ;
do {
p r i n t f ( ‘ ‘ Escolha uma opç ão : \ n ’ ’ ) ;
p r i n t f ( ‘ ‘ ( 1 ) Opção 1\n ’ ’ ) ;
p r i n t f ( ‘ ‘ ( 2 ) Opção 2\n ’ ’ ) ;
p r i n t f ( ‘ ‘ ( 3 ) Opção 3\n ’ ’ ) ;
scanf ( ‘ ‘%d ’ ’ , & i ) ;
} while ( ( i < 1 ) | | ( i > 3 ) ) ;
p r i n t f ( ‘ ‘ Você escolheu a Opção %d . \ n ’ ’ , i ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Relembrando a idéia de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados durante a execução do programa:
110
Diferente do comando if-else, é necessário colocar um
ponto e vı́rgula (;) depois da condição do comando dowhile.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int i = 0;
5
do{
6
p r i n t f ( ‘ ‘ V a l o r %d\n ’ ’ , i ) ;
7
i ++;
8
} while ( i < 10) ; / / Esse ponto e v ı́ r g u l a é
n e c e s s á r i o !
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
No comando do-while, a sequência de comandos é definida antes do teste
da condição, diferente dos outros comando condicionais e de repetição.
Isso significa que o teste da condição é o último comando da repetição
do-while. Sendo assim, o compilador entende que a definição do comando
do-while já terminou e exige que se coloque o operador de ponto e vı́rgula
(;) após a condição.
É responsabilidade do programador modificar o valor de
algum dos elementos usados na condição para evitar que
ocorra um laço infinito.
111
4.5
ANINHAMENTO DE REPETIÇÕES
Uma repetição aninhada é simplesmente um comando de repetição utilizado dentro do bloco de comandos de um outro comando de repetições.
Basicamente, é um comando de repetição dentro de outro, semelhante ao
que é feito com o comando if.
A forma geral de um comando de repetição aninhado é:
repetição(condição 1) {
sequência de comandos;
repetição(condição 2) {
sequência de comandos;
repetição...
}
}
onde repetição representa um dos três possı́veis comandos de repetição
da linguagem C: while, for e do-while.
Em um aninhamento de repetições, o programa começa a testar as condições
começando pela condição 1 da primeira repetição. Se o resultado dessa
condição for diferente de zero (verdadeiro), o programa executará o bloco
de comando associados a ela, ai incluı́do o segundo comando de repetição.
Note que os comando da segunda repetição só serão executados se a
condição da primeira for verdadeira. Esse processo se repete para cada
comando de repetição que o programa encontrar dentro do bloco de comando que ele executar.
O aninhamento de comandos de repetição é muito útil quando se tem que
percorrer dois conjuntos de valores que estão relacionados dentro de um
programa. Por exemplo, para imprimir uma matriz identidade (composta
apenas de 0’s e 1’s na diagonal principal) de tamanho 4 × 4 é preciso
percorrer as quatro linhas da matriz e, para cada linha, percorrer as suas
quatro colunas. Um único comando de repetição não é suficiente para
realizar essa tarefa, como mostra o exemplo abaixo:
112
Exemplo: comandos de repetição aninhados
com for
com while
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int i , j ;
f o r ( i =1; i <5; i ++) {
f o r ( j =1; j <5; j ++) {
i f ( i == j )
printf ( ‘ ‘1 ’ ’ ) ;
else
printf ( ‘ ‘0 ’ ’ ) ;
}
printf ( ‘ ‘\n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t i =1 , j ;
while ( i <5){
j = 1;
while ( j <5){
i f ( i == j )
printf ( ‘ ‘1 ’ ’ ) ;
else
printf ( ‘ ‘0 ’ ’ ) ;
j ++;
}
printf ( ‘ ‘\n ’ ’ ) ;
i ++;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, no exemplo anterior, que a impressão de uma matriz identidade pode
ser feita com dois comandos for ou dois comandos while. É possı́vel ainda
fazê-lo usando um comando de cada tipo.
A linguagem C não proı́be que se misture comandos
de repetições de tipos diferentes no aninhamento de
repetições.
4.6
COMANDO BREAK
Vimos, anteriormente, que o comando break pode ser utilizado em conjunto com o comando switch. Basicamente, sua função era interromper o
comando switch assim que uma das sequências de comandos da cláusula
case fosse executada. Caso o comando break não existisse, a sequência
de comandos do case seguinte também seria executada e assim por diante.
Na verdade, o comando break serve para quebrar a execução de um comando (como no caso do switch) ou interromper a execução de qualquer
comando de laço (for, while ou do-while). O break faz com que a execução
do programa continue na primeira linha seguinte ao laço ou bloco que está
sendo interrompido.
113
O comando break é utilizado para terminar abruptamente
uma repetição. Por exemplo, se estivermos em uma
repetição e um determinado resultado ocorrer, o programa
deverá sair da iteração.
Abaixo, tem-se um exemplo de um programa que lê dois números inteiros
a e b digitados pelo usuário e imprime na tela todos os números inteiros
entre a e b. Note que no momento em que o valor de a atige o valor de b),
o comando break é chamado e o laço terminado:
Exemplo: comando break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int a ,b;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de a :
s c a n f ( ‘ ‘ % d ’ ’ ,&a ) ;
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de b :
s c a n f ( ‘ ‘ % d ’ ’ ,&b ) ;
while ( a <= b ) {
i f ( a == b )
break ;
a = a + 1;
p r i n t f ( ‘ ‘ % d \n ’ ’ , a ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
’ ’);
Relembrando o conceito de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados pelo
programa:
114
4.7
COMANDO CONTINUE
O comando continue é muito parecido com o comando break. Tanto o comando break quanto o comando continue ignoram o restante da sequência
de comandos da repetição que os sucedem. A diferença é que, enquanto o
comando break termina o laço de repetição, o comando break interrompe
apenas aquela repetição e passa para a proxima repetição do laço (se ela
existir).
Por esse mesmo motivo, o comando continue só pode ser utilizado dentro
de um laço.
Os comandos que sucedem o comando continue no bloco
não são executados.
Abaixo, tem-se um exemplo de um programa que lê, repetidamente, um
número inteiro do usuário e a imprime apenas se ela for maior ou igual a
1 e menor ou igual a 5. Caso o número não esteja nesse intervalo, essa
repetição do laço é desconsiderada e reiniciada:
115
Exemplo: comando continue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t opcao = 0 ;
while ( opcao ! = 5 ) {
p r i n t f ( ‘ ‘ Escolha uma opcao e n t r e 1 e 5 : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &opcao ) ;
i f ( ( opcao > 5 ) | | ( opcao < 1 ) )
continue ;
p r i n t f ( ‘ ‘ Opcao e s c o l h i d a : %d ’ ’ , opcao ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Relembrando o conceito de fluxogramas, é possı́vel ter uma boa representação
de como os comandos do exemplo anterior são um-a-um executados pelo
programa:
4.8
GOTO E LABEL
O comando goto é um salto condicional para um local especificado por
uma palavra chave no código. A forma geral de um comando goto é:
destino:
goto destino;
116
Na sintaxe acima, o comando goto (do inglês go to, literalmente “ir para”)
muda o fluxo do programa para um local previamente especificado pela expressão destino, onde destino é uma palavra definida pelo programador.
Este local pode ser a frente ou atrás no programa, mas deve ser dentro da
mesma função.
O teorema da programação estruturada prova que a instrução goto não é
necessária para escrever programas; alguma combinação das três construções
de programação (comandos sequenciais, condicionais e de repetição) são
suficientes para executar qualquer cálculo. Além disso, o uso de goto pode
deixar o programa muitas vezes ilegı́vel.
goto
Exemplo: goto versus for
for
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int i = 0;
5
inicio :
6
i f ( i < 5) {
7
p r i n t f ( ‘ ‘ Numero %d\n
’’,i);
8
i ++;
9
goto i n i c i o ;
10
}
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int i ;
5
f o r ( i = 0 ; i < 5 ; i ++)
6
p r i n t f ( ‘ ‘ Numero %d\n
’’,i);
7
8
system ( ‘ ‘ pause ’ ’ ) ;
9
return 0;
10 }
Como se nota no exemplo acima, o mesmo programa feito com o comando
for é muito mais fácil de entender do que o mesmo programa feito com o
comando goto.
117
Apesar de banido da prática de programação, o comando
goto pode ser útil em determinadas circunstâncias. Ex: sair
de dentro de laços aninhados.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int i , j , k ;
5
f o r ( i = 0 ; i < 5 ; i ++)
6
f o r ( j = 0 ; j < 5 ; j ++)
7
f o r ( k = 0 ; k < 5 ; k ++)
8
i f ( i == 2 && j == 3 && k == 1 )
9
goto f i m ;
10
else
11
p r i n t f ( ‘ ‘ Posicao [%d,%d,%d ] \
n ’ ’ ,i , j ,k) ;
12
13
14
fim :
15
p r i n t f ( ‘ ‘ Fim do programa \n ’ ’ ) ;
16
17
system ( ‘ ‘ pause ’ ) ;
18
return 0;
19 }
118
5
VETORES E MATRIZES - ARRAYS
5.1
EXEMPLO DE USO
Um array ou “vetor” é a forma mais comum de dados estruturados da linguagem C. Um array é simplesmente um conjunto de variáveis do mesmo
tipo, igualmente acessı́veis por um ı́ndice.
Imagine o seguinte problema: dada uma relação de 5 estudantes, imprimir o nome de cada estudante, cuja nota é
maior do que a média da classe.
Um algoritmo simples para resolver esse problema poderia ser o pseudocódigo apresentado abaixo:
Leia(nome1, nome2, nome3, nome4, nome5);
Leia(nota1, nota2, nota3, nota4, nota5);
media = (nota1+nota2+nota3+nota4+nota5) / 5,0;
Se nota1 > media então escreva (nome1)
Se nota2 > media então escreva (nome2)
Se nota3 > media então escreva (nome3)
Se nota4 > media então escreva (nome4)
Se nota5 > media então escreva (nome5)
O algoritmo anterior representa uma solução possı́vel para o problema. O
grande inconveniente dessa solução é a grande quantidade de variáveis
para gerenciarmos e o uso repetido de comandos praticamente idênticos.
Essa solução é inviável para uma lista de 100 alunos.
Expandir o algoritmo anterior para trabalhar com um total de 100 alunos
significaria, basicamente, aumentar o número de variáveis para guardar
os dados de cada aluno e repetir, ainda mais, um conjunto de comandos
praticamente idênticos. Desse modo, teriamos:
119
• Uma variável para armazenar cada nome de aluno: 100 variáveis;
• Uma variável para armazenar a nota de cada aluno: 100 variáveis;
• Um comando de teste e impressão na tela para cada aluno: 100
testes.
O pseudo-código abaixo representa o algoritmo anterior expandido para
poder trabalhar com 100 alunos:
Leia(nome1, nome2, ..., nome100);
Leia(nota1, nota2,..., nota100);
media = (nota1+nota2+...+nota100) / 100,0;
Se nota1 > media então escreva (nome1)
Se nota2 > media então escreva (nome2)
...
Se nota100 > media então escreva (nome100)
Como se pode notar, temos uma solução extremamente engessada para
o nosso problema. Modificar o número de alunos usado pelo algoritmo
implica em reescrever todo o código, repetindo comandos praticamente
idênticos. Além disso, temos uma grande quantidade de variáveis para
gerenciar, cada uma com o seu próprio nome, o que torna essa tarefa
ainda mais difı́cil de ser realizada sem a ocorrência de erros.
Como estes dados têm uma relação entre si, podemos declará-los usando um ÚNICO nome para todos os 100 elementos.
Surge então a necessidade de usar um array.
5.2
ARRAY COM UMA DIMENSÃO - VETOR
A idéia de um array ou “vetor” é bastante simples: criar um conjunto de
variáveis do mesmo tipo utilizando apenas um nome.
Relembrando o exemplo anterior, onde as variáveis que guardam as notas
dos 100 alunos são todas do mesmo tipo, essa solução permitiria usar
apenas um nome (notas, por exemplo) de variável para representar todas
as notas dos alunos, ao invés de um nome para cada variável.
120
DECLARANDO UM VETOR
Em linguagem C, a declaração de um array segue a seguinte forma geral:
tipo dado nome array[tamanho];
O comando acima define um array de nome nome array contendo tamanho elementos adjacentes na memória. Cada elemento do array é do tipo
tipo dado. Pensando no exemplo anterior, poderı́amos usar um array de
inteiros contendo 100 elementos para guardar as notas dos 100 alunos.
Ele seri declarado com mostrado abaixo:
int notas[100];
ACESSANDO UM ELEMENTO DO VETOR
Como a variável que armazena a nota de um aluno possui agora o mesmo
nome que as demais notas dos outros alunos, o acesso ao valor de cada
nota é feito utilizando um ı́ndice, como mostra a figura abaixo:
Note que na posição “0” do array está armazenado o valor “81”, na posição
“1” está armazenado o valor “55”, e assim por diante.
Para indicar qual ı́ndice do array queremos acessar, utilizase o operador de colchetes [ ]: notas[ı́ndice].
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t notas [ 1 0 0 ] ;
int i ;
f o r ( i = 0 ; i < 100; i ++) {
p r i n t f ( ‘ ‘ D i g i t e a nota do aluno %d ’ ’ , i ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,& notas [ i ] ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
121
No exemplo acima, percebe-se que cada posição do array possui todas
as caracterı́sticas de uma variável. Isso significa que ela pode aparecer
em comandos de entrada e saı́da de dados, expressões e atribuições. Por
exemplo:
scanf(“%d”,¬as[5]);
notas[0] = 10;
notas[1] = notas[5] + notas[0];
O tempo para acessar qualquer uma das posições do array
é o mesmo.
Lembre-se, cada posição do array é uma variável. Portanto, todas as
posições do array são igualmente acessı́veis, isto é, o tempo e o tipo de
procedimento para acessar qualquer uma das posições do array são iguais
ao de qualquer outra variável.
Na linguagem C a numeração começa sempre do ZERO e
termina em N-1, onde N é o número de elementos do array.
Isto significa que, no exemplo anterior, as notas dos alunos serão indexadas de 0 a 99:
notas[0]
notas[1]
...
notas[99]
Isso acontece pelo seguinte motivo: um array é um agrupamento de dados, do mesmo tipo, adjacentes na memória. O nome do array indica
onde esses dados começam na memória. O ı́ndice do array indica quantas
posições se deve pular para acessar uma determinada posição. A figura
abaixo exemplifica como o array está na memória:
122
Num array de 100 elementos, ı́ndices menores do que
0 e maiores do que 99 também podem ser acessados.
Porém, isto pode resultar nos mais variados erros durante
a execução do programa.
Como foi explicado, um array é um agrupamento de dados adjacentes na
memória e o seu ı́ndice apenas indica quantas posições se deve pular para
acessar uma determinada posição. Isso significa que se tentarmos acessar
o ı́ndice 100, o programa tentará acessar a centésima posição a partir da
posição inicial (que é o nome do array). O mesmo vale para a posição de
ı́ndice -1. Nesse caso o programa tentará acessar uma posição anterior ao
local onde o array começa na memória. O problema é que, apesar dessas
posições existirem na memória e serem acessı́veis, elas não pertencer ao
array. Pior ainda, elas podem pertencer a outras variáveis do programa, e
a alteração de seus valores pode resultar nos mais variados erros durante
a execução do programa.
É função do programador garantir que os limites do array
estão sendo respeitados.
Deve-se tomar cuidado ao se rabalhar com arrays. Principalmente ao se
usar a operação de atribuição (=).
123
Não se pode fazer atribuição de arrays.
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int v [ 5 ] = {1 ,2 ,3 ,4 ,5};
i n t v1 [ 5 ] ;
v1 = v ; / / ERRADO!
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Isso ocorre porque a linguagem C não suporta a atribuição de um array
para outro. Para atribuir o conteúdo de um array a outro array, o correto é
copiar seus valores elemento por elemento para o outro array.
5.3
ARRAY COM DUAS DIMENSÕES - MATRIZ
Os arrays declarados até o momento possuem apenas uma dimensão. Há
casos, em que uma estrutura com mais de uma dimensão é mais útil. Por
exemplo, quando trabalhamos com matrizes, onde os valores são organizados em uma estrutura de linhas e colunas.
DECLARANDO UM MATRIZ
Em linguagem C, a declaração de uma matriz segue a seguinte forma geral:
tipo dado nome array[nro linhas][nro colunas];
O comando acima define um array de nome nome array contendo nro linhas
× nro colunas elementos adjacentes na memória. Cada elemento do array
é do tipo tipo dado.
Por exemplo, para criar um array de inteiros que possua 100 linhas e
50 colunas, isto é, uma matriz de inteiros de tamanho 100×50, usa-se
a declaração abaixo:
int mat[100][50];
124
ACESSANDO UM ELEMENTO DA MATRIZ
Como no caso dos arrays de uma única dimensão, cada posição da matriz possui todas as caracterı́sticas de uma variável. Isso significa que ela
pode aparecer em comandos de entrada e saı́da de dados, expressões e
atribuições:
scanf(“%d”,&mat[5][0]);
mat[0][0] = 10;
mat[1][2] = mat[5][0] + mat[0][0];
Perceba, no entanto, que o acesso ao valor de uma posição da matriz é
feito agora utilizando dois ı́ndices: um para a linha e outro para a coluna.
Lembre-se, cada posição do array é uma variável. Portanto, todas as
posições do array são igualmente acessı́veis, isto é, o tempo e o tipo de
procedimento para acessar qualquer uma das posições do array são iguais
ao de qualquer outra variável.
5.4
ARRAYS MULTIDIMENSIONAIS
Vimos até agora como criar arrays com uma ou duas dimensões. A linguagem C permite que se crie arrays com mais de duas dimensões de maneira
fácil.
Na linguagem C, cada conjunto de colchetes [ ] representa
uma dimensão do array.
Cada par de colchetes adicionado ao nome de uma variável durante a sua
declaração adiciona uma nova dimensão àquela variável, independente do
seu tipo:
125
int vet[5]; // 1 dimensão
float mat[5][5]; // 2 dimensões
double cub[5][5][5]; // 3 dimensões
int X[5][5][5][5]; // 4 dimensões
O acesso ao valor de uma posição de um array multidimensional é feito utilizando um ı́ndice para cada dimensão do
array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t cub [ 5 ] [ 5 ] [ 5 ] ;
int i , j , k ;
/ / preenche o a r r a y de 3 dimens ões com zeros
f o r ( i =0; i < 5 ; i ++) {
f o r ( j =0; j < 5 ; j ++) {
f o r ( k =0; k < 5 ; k ++) {
cub [ i ] [ j ] [ k ] = 0 ;
}
}
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Apesar de terem o comportamento de estruturas com mais de uma dimensão, os dados dos arrays multidimensionais são armazenados linearmente na memória. É o uso dos colchetes que cria a impressão de
estarmos trabalhando com mais de uma dimensão.
Por esse motivo, é importante ter em mente qual a dimensão que se move
mais rapidamente na memória: sempre a mais a direita, independente do
tipo ou número de dimensões do array, como se pode ver abaixo marcado
em vermelho:
126
int vet[5]; // 1 dimensão
float mat[5][5]; // 2 dimensões
double cub[5][5][5]; // 3 dimensões
int X[5][5][5][5]; // 4 dimensões
Basicamente, um array multidimensional funciona como
qualquer outro array. Basta lembrar que o ı́ndice que varia mais rapidamente é o ı́ndice mais à direita.
5.5
INICIALIZAÇÃO DE ARRAYS
Um array pode ser inicializado com certos valores durante sua declaração.
Isso pode ser feito com qualquer array independente do tipo ou número de
dimensões do array.
A forma geral de inicialização de um array é:
tipo dado nome array[tam1][tam2]...[tamN] = {dados };
Na declaração acima, dados é uma lista de valores (do mesmo tipo do array) separados por vı́rgula e delimitado pelo operador de chaves {}. Esses
valores devem ser colocados na mesma ordem em que serão colocados
dentro do array.
A inicialização de uma array utilizando o operador de chaves {}só pode ser feita durante sua declaração.
A inicialização de uma array consiste em atribuir um valor inicial a cada
posição do array. O operador de chaves apenas facilita essa tarefa, como
mostra o exemplo abaixo:
127
Exemplo 1: inicializando um array
Com o operador de {}
Sem o operador de {}
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int vet [ 5 ] =
{15 ,12 ,91 ,35};
5
6
system ( ‘ ‘ pause ’ ’ ) ;
7
return 0;
8 }
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int vet [ 5 ] ;
vet [ 0 ] = 15;
vet [ 1 ] = 12;
vet [ 2 ] = 9;
vet [ 3 ] = 1;
vet [ 4 ] = 35;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Abaixo são apresentados alguns exemplos de inicialização de arrays de
diferentes tipos e número de dimensões:
Exemplo 2: inicializando um array
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int matriz1 [ 3 ] [ 4 ] = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12};
5
int matriz2 [ 3 ] [ 4 ] =
{{1 ,2 ,3 ,4} ,{5 ,6 ,7 ,8} ,{9 ,10 ,11 ,12}};
6
7
char s t r 1 [ 1 0 ] = { ’ J ’ , ’ o ’ , ’ a ’ , ’ o ’ , ’ \0 ’ } ;
8
char s t r 2 [ 1 0 ] = ‘ ‘ Joao ’ ’ ;
9
10
char s t r m a t r i z [ 3 ] [ 1 0 ] = { ‘ ‘ Joao ’ ’ , ‘ ‘ Maria ’ ’ , ‘ ‘ Jose ’
’ };
11
12
system ( ‘ ‘ pause ’ ’ ) ;
13
return 0;
14 }
Note no exemplo acima que a inicialização de um array de 2 dimensões
pode ser feita de duas formas distintas. Na primeira matriz (matriz1) os
valores iniciais da matriz são definidos utilizando um único conjunto de
chaves {}, igual ao que é feito com vetores. Nesse caso, os valores são
atribuı́dos para todas as colunas da primeira linha da matriz, para depois
passar para as colunas da segunda linha e assim por diante. Lembre-se,
128
a dimensão que se move mais rapidamente na memória é sempre a mais
a direita, independente do tipo ou número de dimensões do array. Já na
segunda matriz (matriz2) usa-se mais de um conjunto de chaves {}para
definir cada uma das dimensões da matriz.
Para a inicialização de um array de caracteres, pode-se usar o mesmo
princı́pio definido na inicialização de vetores (str1). Percebe-se que essa
forma de inicialização não é muito prática. Por isso, a inicialização de um
array de caracteres também pode ser feita por meio de “aspas duplas”,
como mostrado na inicialização de str2. O mesmo princı́pio é válido para
iniciar um array de caracteres de mais de uma dimensão.
Na inicialização de um array de caracteres não é necessário definir todos os seus elementos.
5.5.1
INICIALIZAÇÃO SEM TAMANHO
A linguagem C também permite inicializar um array sem que tenhamos
definido o seu tamanho. Nesse caso, simplesmente não se coloca o valor
do tamanho entre os colchetes durante a declaração do array:
tipo dado nome array[ ] = {dados };
Nesse tipo de inicialização, o compilador da linguagem C vai considerar o
tamanho do dado declarado como sendo o tamanho do array. Isto ocorre
durante a compilação do programa. Depois disso, o tamanho do array não
poderá mais ser modificado durante o programa.
Abaixo são apresentados alguns exemplos de inicialização de arrays sem
tamanhos:
129
Exemplos: inicializando um array sem tamanho
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
/ / A s t r i n g t e x t o t e r á tamanho 13
/ / (12 c a r a c t e r e s + o c a r a c t e r e ’ \ 0 ’ )
char t e x t o [ ] = ‘ ‘ Linguagem C. ’ ’ ;
/ / O n úmero de posiç ões do v e t o r ser á 1 0 .
int vetor [ ] = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10};
/ / O n úmero de l i n h a s de m a t r i z ser á 5 .
int matriz [ ] [ 2 ] = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10};
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note no exemplo acima que foram utilizados 12 caracteres para iniciar o
array de char “texto”. Porém, o seu tamanho final será 13. Isso ocorre por
que arrays de caracteres sempre possuem o elemento seguinte ao último
caractere como sendo o caractere ‘\0’. Mais detalhes sobre isso podem
ser vistos na seção seguinte.
Esse tipo de inicialização é muito útil quando não queremos
contar quantos caracteres serão necessários para inicializarmos uma string (array de caracteres).
No caso da inicialização de arrays de mais de uma dimensão, é necessário
sempre definir as demais dimensões. Apenas a primeira dimensão pode
ficar sem tamanho definido.
5.6
EXEMPLO DE USO DE ARRAYS
Nesta seção são apresentados alguns exemplos de operações básicas de
manipulação de vetores e matrizes em C.
130
Somar os elementos de um vetor de 5 inteiros
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t i , l i s t a [ 5 ] = {3 ,51 ,18 ,2 ,45};
i n t soma = 0 ;
f o r ( i =0; i < 5 ; i ++)
soma = soma + l i s t a [ i ] ;
p r i n t f ( ‘ ‘ Soma = %d ’ ’ ,soma ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Encontrar o maior valor contido em um vetor de 5 inteiros
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t i , l i s t a [ 5 ] = {3 ,18 ,2 ,51 ,45};
i n t Maior = l i s t a [ 0 ] ;
f o r ( i =1; i <5; i ++) {
i f ( Maior < l i s t a [ i ] )
Maior = l i s t a [ i ] ;
}
p r i n t f ( ‘ ‘ Maior = %d ’ ’ , Maior ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Calcular a média dos elementos de um vetor de 5 inteiros
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t i , l i s t a [ 5 ] = {3 ,51 ,18 ,2 ,45};
i n t soma = 0 ;
f o r ( i =0; i < 5 ; i ++)
soma = soma + l i s t a [ i ] ;
f l o a t media = soma / 5 . 0 ;
p r i n t f ( ‘ ‘ Media = %f ’ ’ , media ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
131
Somar os elementos de uma matriz de inteiros
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t mat [ 3 ] [ 3 ] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } , { 7 , 8 , 9 } } ;
i n t i , j , soma = 0 ;
f o r ( i =0; i < 3 ; i ++)
f o r ( j =0; j < 3 ; j ++)
soma = soma + mat [ i ] [ j ] ;
p r i n t f ( ‘ ‘ Soma = %d ’ ’ ,soma ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Imprimir linha por linha uma matriz
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t mat [ 3 ] [ 3 ] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } , { 7 , 8 , 9 } } ;
int i , j ;
f o r ( i =0; i < 3 ; i ++) {
f o r ( j =0; j < 3 ; j ++)
p r i n t f ( ‘ ‘ % d ’ ’ , mat [ i ] [ j ] ) ;
printf ( ‘ ‘\n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
132
6
ARRAYS DE CARACTERES - STRINGS
6.1
DEFINIÇÃO E DECLARAÇÃO DE UMA STRING
String é o nome que usamos para definir uma sequência de caracteres adjacentes na memória do computador. Essa sequência de caracteres, que
pode ser uma palavra ou frase, é armazenada na memória do computador
na forma de um array do tipo char.
Sendo a string um array de caracteres, sua declaração segue as mesmas
regras da declaração de um array convencional:
char str[6];
A declaração acima cria na memória do computador uma string (array de
caracteres) de nome str e tamanho igual a 6. No entanto, apesar de ser um
array, devemos ficar atentos para o fato de que as strings têm no elemento
seguinte a última letra da palavra/frase armazenada um caractere ‘\0’.
O caractere ‘\0’ indica o fim da sequência de caracteres.
Isso ocorre por que podemos definir uma string com um tamanho maior
do que a palavra armazenada. Imagine uma string definida com um tamanho de 50 caracteres, mas utilizada apenas para armazenar a palavra “oi”.
Nesse caso, temos 48 posições não utilizadas e que estão preenchidas
com lixo de memória (um valor qualquer). Obviamente, não queremos
que todo esse lixo seja considerado quando essa string for exibida na tela.
Assim, o caractere ‘\0’ indica o fim da sequência de caracteres e o inı́cio
das posições restantes da nossa string que não estão sendo utilizadas
nesse momento:
Ao definir o tamanho de uma string, devemos considerar o
caractere ‘\0’.
133
Como o caractere ‘\0’ indica o final de nossa string, isso significa que numa
string definida com um tamanho de 50 caracteres, apenas 49 estarão disponı́veis para armazenar o texto digitado pelo usuário.
6.1.1
INICIALIZANDO UMA STRING
Uma string pode ser lida do teclado ou já ser definida com um valor inicial. Para sua inicialização, pode-se usar o mesmo princı́pio definido na
inicialização de vetores e matrizes:
char str [10] = {‘J’, ‘o’, ‘a’, ‘o’, ‘\0’ };
Percebe-se que essa forma de inicialização não é muito prática. Por isso, a
inicialização de strings também pode ser feita por meio de “aspas duplas”:
char str [10] = “Joao”;
Essa forma de inicialização possui a vantagem de já inserir o caractere ‘\0’
no final da string.
6.1.2
ACESSANDO UM ELEMENTO DA STRING
Outro ponto importante na manipulação de strings é que, por se tratar de
um array, cada caractere pode ser acessado individualmente por indexação
como em qualquer outro vetor ou matriz:
char str[6] = “Teste”;
str[0] = ’L’;
134
Na atribuição de strings usa-se “aspas duplas”, enquanto
que na de caracteres, usa-se ’aspas simples’.
6.2
TRABALHANDO COM STRINGS
O primeiro cuidado que temos que tomar ao se trabalhar com strings é na
operação de atribuição.
Strings são arrays. Portanto, não se pode fazer atribuição
de strings.
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char s t r 1 [ 2 0 ] = ‘ ‘ H e l l o World ’ ’ ;
char s t r 2 [ 2 0 ] ;
s t r 1 = s t r 2 ; / / ERRADO!
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Isso ocorre porque uma string é um array e a linguagem C não suporta a
atribuição de um array para outro. Para atribuir o conteúdo de uma string a
outra, o correto é copiar a string elemento por elemento para a outra string.
135
Exemplo: Copiando uma string
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t count ;
char s t r 1 [ 2 0 ] = ‘ ‘ H e l l o World ’ ’ , s t r 2 [ 2 0 ] ;
f o r ( count = 0 ; s t r 1 [ count ] ! = ’ \0 ’ ; count ++)
s t r 2 [ count ] = s t r 1 [ count ] ;
s t r 2 [ count ] = ’ \0 ’ ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O exemplo acima permite copiar uma string elemento por elemento para
outra string. Note que foi utilizada a mesma forma de indexação que seria
feita com um array de qualquer outro tipo (int, float, etc). Infelizmente,
esse tipo de manipulação de arrays não é muito prática quando estamos
trabalhando com palavras.
Felizmente, a biblioteca padrão da linguagem C possui
funções especialmente desenvolvidas para a manipulação
de strings na biblioteca <string.h>.
A seguir, serão apresentadas algumas das funções mais utilizadas para a
leitura, escrita e manipulação de strings.
6.2.1
LENDO UMA STRING DO TECLADO
USANDO A FUNÇÃO SCANF()
Existem várias maneiras de se fazer a leitura de uma sequência de caracteres do teclado. Uma delas é utilizando a já conhecida função scanf()
com o formato de dados “%s”:
char str[20];
scanf(“%s”,str);
Quando usamos a função scanf() para ler uma string, o
sı́mbolo de & antes do nome da variável não é utilizado.
Os colchetes também não utilizados pois queremos ler a
string toda e não apenas uma letra.
136
Infelizmente, para muitos casos, a função scanf() não é a melhor opção
para se ler uma string do teclado.
A função scanf() lê apenas strings digitadas sem espaços,
ou seja, palavras.
No caso de ter sido digitada uma frase (uma sequência de caracteres contendo espaços), apenas os caracteres digitados antes do primeiro espaço
encontrado serão armazenados na string se a sua leitura for feita com a
função scanf().
USANDO A FUNÇÃO GETS()
Uma alternativa mais eficiente para a leitura de uma string é a função
gets(), a qual faz a leitura do teclado considerando todos os caracteres
digitados (incluindo os espaços) até encontrar uma tecla enter:
char str[20];
gets(str);
USANDO A FUNÇÃO FGETS()
Basicamente, para se ler uma string do teclado utilizamos a função gets().
No entanto, existe outra função que, utilizada de forma adequada, também
permite a leitura de strings do teclado. Essa função é a fgets(), cujo
protótipo é:
char *fgets (char *str, int tamanho, FILE *fp);
A função fgets() recebe 3 parâmetros de entrada
• str: a string a ser lida;
• tamanho: o limite máximo de caracteres a serem lidos;
• fp: a variável que está associado ao arquivo de onde a string será
lida.
e retorna
• NULL: no caso de erro ou fim do arquivo;
137
• O ponteiro para o primeiro caractere da string recuperada em str.
Note que a função fgets() utiliza uma variável FILE *fp, que está associado
ao arquivo de onde a string será lida.
Para ler do teclado, basta substituir FILE *fp por stdin,
o qual representa o dispositivo de entrada padrão (geralmente o teclado).
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char nome [ 3 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e um nome : ’ ’ ) ;
f g e t s ( nome , 30 , s t d i n ) ;
p r i n t f ( ‘ ‘O nome d i g i t a d o f o i : %s ’ ’ ,nome ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Como a função gets(), a função fgets() lê a string do teclado até que um
caractere de nova linha (ENTER) seja lido. Apesar de parecerem iguais, a
função fgets possui algumas diferenças e vantagens sobre a gets().
Na função fgets(), o caractere de nova linha (‘\n’) fará
parte da string, o que não acontecia com gets().
A função gets() armazena tudo que for digitado até o comando de enter.
Já a função fgets() armazena tudo que for digitado, incluindo o comando
de enter (‘\n’).
A função fgets() especı́fica o tamanho máximo da string de
entrada.
Diferente da função gets(), a função fgets() lê a string até que um caractere de nova linha seja lido ou “tamanho-1” caracteres tenham sido lidos.
Isso evita o estouro do buffer, que ocorre quando se tenta ler algo maior do
que pode ser armazenado na string.
LIMPANDO O BUFFER DO TECLADO
138
Ás vezes, podem ocorrer erros durante a leitura de caracteres ou strings
do teclado. Para resolver esse pequenos erros, podemos limpar o buffer
do teclado (entrada padrão) usando a função setbuf(stdin, NULL) antes
de realizar a leitura de caracteres ou strings:
Exemplo: limpando o buffer do teclado
leitura de caracteres
leitura de strings
1 char ch ;
2 s e t b u f ( s t d i n , NULL ) ;
3 s c a n f ( ‘ ‘ % c ’ ’ , &ch ) ;
1 char s t r [ 1 0 ] ;
2 s e t b u f ( s t d i n , NULL ) ;
3 gets ( s t r ) ;
Basicamente, a função setbuf() preenche um buffer (primeiro parâmetro)
com um determinado valor (segundo parâmetro). No exemplo acima, o
buffer da entrada padrão (stdin), ou seja, o teclado, é preenchido com
o valor vazio (NULL). Na linguagem C a palavra NULL é uma constante
padrão que significa um valor nulo. Um buffer preenchido com NULL é
considerado limpo/vazio.
6.2.2
ESCREVENDO UMA STRING NA TELA
USANDO A FUNÇÃO PRINTF()
Basicamente, para se escrever uma string na tela utilizamos a função
printf() com o formato de dados “%s”:
char str[20] = “Hello World”;
printf(“%s”,str);
Para escrever uma string, utilizamos o tipo de saı́da “%s”.
Os colchetes não são utilizados pois queremos escrever a
string toda e não apenas uma letra.
USANDO A FUNÇÃO FPUTS()
No entanto, existe uma outra função que, utilizada de forma adequada,
também permite a escrita de strings. Essa função é a fputs(), cujo protótipo
é:
139
int fputs (char *str,FILE *fp);
A função fputs() recebe 2 parâmetros de entrada
• str: a string (array de caracteres) a ser escrita na tela;
• fp: a variável que está associado ao arquivo onde a string será escrita.
e retorna
• a constante EOF (em geral, -1), se houver erro na escrita;
• um valor diferente de ZERO, se o texto for escrito com sucesso.
Note que a função fputs() utiliza uma variável FILE *fp, que está associado
ao arquivo onde a string será escrita.
Para escrever no monitor, basta substituir FILE *fp por stdout, o qual representa o dispositivo de saı́da padrão (geralmente a tela do monitor).
1
2
3
4
5
6
7
8
6.3
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 3 0 ] = ‘ ‘ H e l l o World \n ’ ’ ;
fputs ( texto , stdout ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
FUNÇÕES PARA MANIPULAÇÃO DE STRINGS
A biblioteca padrão da linguagem C possui funções especialmente desenvolvidas para a manipulação de strings na bibloteca <string.h>. A seguir
são apresentadas algumas das mais utilizadas.
6.3.1
TAMANHO DE UMA STRING
Para se obter o tamanho de uma string, usa-se a função strlen():
140
char str[15] = “teste”;
printf(“%d”,strlen(str));
Neste caso, a função retornará 5, que é o número de caracteres na palavra
“teste” e não 15, que é o tamanho do array de caracteres.
A função strlen() retorna o número de caracteres até o caractere ‘\0’, e não o tamanho do array onde a string está
armazenada.
6.3.2
COPIANDO UMA STRING
Vimos que uma string é um array e que a linguagem C não suporta a
atribuição de um array para outro. Nesse sentido, a única maneira de atribuir o conteúdo de uma string a outra é a copia, elemento por elemento,
de uma string para outra. A linguagem C possui uma função que realiza
essa tarefa para nós: a função strcpy():
strcpy(char *destino, char *origem)
Basicamente, a função strcpy() copia a sequência de caracteres contida
em origem para o array de caracteres destino:
Exemplo: strcpy()
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char s t r 1 [ 1 0 0 ] , s t r 2 [ 1 0 0 ] ;
p r i n t f ( ‘ ‘ E n t r e com uma s t r i n g :
gets ( s t r 1 ) ;
strcpy ( str2 , str1 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
Para evitar estouro de buffer, o tamanho do array destino
deve ser longo o suficiente para conter a sequência de caracteres contida em origem.
141
6.3.3
CONCATENANDO STRINGS
A operação de concatenação é outra tarefa bastante comum ao se trabalhar com strings. Basicamente, essa operação consistem em copiar
uma string para o final de outra string. Na linguagem C, para se fazer a
concatenação de duas strings, usa-se a função strcat():
strcat(char *destino, char *origem)
Basicamente, a função strcat() copia a sequência de caracteres contida
em origem para o final da string destino. O primeiro caractere da string
contida em origem é colocado no lugar do caractere ‘\0’ da string destino:
Exemplo: strcat()
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char s t r 1 [ 1 5 ] = ‘ ‘ bom ’ ’ ;
char s t r 2 [ 1 5 ] = ‘ ‘ d i a ’ ’ ;
s t r c a t ( str1 , str2 ) ;
p r i n t f ( ‘ ‘% s ’ ’ , s t r 1 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Para evitar estouro de buffer, o tamanho do array destino
deve ser longo o suficiente para conter a sequência de caracteres contida em ambas as strings: origem e destino.
6.3.4
COMPARANDO DUAS STRINGS
Da mesma maneira como o operador de atribuição não funciona para
strings, o mesmo ocorre com operadores relacionais usados para comparar duas strings. Desse modo, para saber se duas strings são iguais
usa-se a função strcmp():
int strcmp(char *str1, char *str2)
142
A função strcmp() compara posição a posição as duas strings (str1 e str2)
e retorna um valor inteiro igual a zero no caso das duas strings serem
iguais. Um valor de retorno diferente de zero significa que as strings são
diferentes:
Exemplo: strcmp()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char s t r 1 [ 1 0 0 ] , s t r 2 [ 1 0 0 ] ;
p r i n t f ( ‘ ‘ E n t r e com uma s t r i n g : ’ ’ ) ;
gets ( s t r 1 ) ;
p r i n t f ( ‘ ‘ E n t r e com o u t r a s t r i n g : ’ ’ ) ;
gets ( s t r 2 ) ;
i f ( strcmp ( s t r 1 , s t r 2 ) == 0 )
p r i n t f ( ‘ ‘ S t r i n g s i g u a i s \n ’ ’ ) ;
else
p r i n t f ( ‘ ‘ S t r i n g s d i f e r e n t e s \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
A função strcmp() é case-sensitive. Isso significa que letras maiusculas e minusculas tornam as strings diferentes.
143
7
TIPOS DEFINIDOS PELO PROGRAMADOR
Os tipos de variáveis vistos até agora podem ser classificados em duas
categorias:
• tipos básicos: char, int, float, double e void;
• tipos compostos homogêneos: array.
Dependendo da situação que desejamos modelar em nosso programa, esses tipos existentes podem não ser suficientes. Por esse motivo, a linguagem C permite criar novos tipos de dados a partir dos tipos básicos. Para
criar um novo tipo de dado, um dos seguintes comandos pode ser utlizado:
• Estruturas: comando struct
• Uniões: comando union
• Enumerações: comando enum
• Renomear um tipo existente: comando typedef
Nas seções seguintes, cada um desses comandos será apresentado em
detalhes.
7.1
ESTRUTURAS: STRUCT
Uma estrutura pode ser vista como um conjunto de variáveis sob um mesmo
nome, sendo que cada uma delas pode ter qualquer tipo (ou o mesmo
tipo). A idéia básica por trás da estrutura é criar apenas um tipo de dado
que contenha vários membros, que nada mais são do que outras variáveis.
Em outras palavras, estamos criando uma variável que contém dentro de
si outras variáveis.
DECLARANDO UMA ESTRUTURA
A forma geral da definição de uma nova estrutura é utilizando o comando
struct:
struct nome struct{
tipo1 campo1;
144
tipo2 campo2;
...
tipon campoN;
};
A principal vantagem do uso de estruturas é que agora podemos agrupar
de forma organizada vários tipos de dados diferentes dentro de uma única
variável.
As estruturas podem ser declaradas em qualquer escopo
do programa (global ou local).
Apesar disso, a maioria das estruturas são declaradas no escopo global.
Por se tratar de um novo tipo de dado, muitas vezes é interessante que
todo o programa tenha acesso a estrutura. Daı́ a necessidade de usar o
escopo global.
Abaixo, tem-se um exemplo de uma estrutura declarada para representar
o cadastro de uma pessoa:
Exemplo de estrutura.
1 struct cadastro {
2
char nome [ 5 0 ] ;
3
i n t idade ;
4
char rua [ 5 0 ] ;
5
i n t numero ;
6 };
Note que os campos da estrutura são definidos da mesma forma que
variáveis. Como na declaração de variáveis, os nomes dos membros de
uma estrutura devem ser diferentes um do outro. Porém, estruturas diferentes podem ter membros com nomes iguais:
145
struct cadastro{
char nome[50];
int idade;
char rua[50];
int numero; };
struct aluno{
char nome[50];
int matricula
float nota1,nota2,nota3;
};
Depois do sı́mbolo de fecha chaves (}) da estrutura é necessário colocar um ponto e vı́rgula (;).
Isso é necessário uma vez que a estrutura pode ser também declarada no
escopo local. Por questões de simplificações, e por se tratar de um novo
tipo, é possı́vel logo na definição da struct definir algumas variáveis desse
tipo. Para isso, basta colocar os nomes das variáveis declaradas após o
comando de fecha chaves (}) da estrutura e antes do ponto e vı́rgula (;):
struct cadastro{
char nome[50];
int idade;
char rua[50];
int numero;
} cad1, cad2;
No exemplo acima, duas variáveis (cad1 e cad2) são declaradas junto com
a definição da estrutura.
DECLARANDO UMA VARIÁVEL DO TIPO DA ESTRUTURA
Uma vez definida a estrutura, uma variável pode ser declarada de modo
similar aos tipos já existente:
struct cadastro c;
146
Por ser um tipo definido pelo programador, usa-se a palavra
struct antes do tipo da nova variável declarada.
O uso de estruturas facilita muito a vida do programador na manipulação
dos dados do programa. Imagine ter que declarar 4 cadastros, para 4
pessoas diferentes:
char nome1[50], nome2[50], nome3[50], nome4[50];
int idade1, idade2, idade3, idade4;
char rua1[50], rua2[50], rua3[50], rua4[50];
int numero1, numero2, numero3, numero4;
Utilizando uma estrutura, o mesmo pode ser feito da seguinte maneira:
struct cadastro c1, c2, c3, c4;
ACESSANDO OS CAMPOS DE UMA ESTRUTURA
Uma vez definida uma variável do tipo da estrutura, é preciso poder acessar seus campos (ou variáveis) para se trabalhar.
Cada campo (variável) da estrutura pode ser acessada
usando o operador “.” (ponto).
O operador de acesso aos campos da estrutura é o ponto (.). Ele é usado
para referenciar os campos de uma estrutura. O exemplo abaixo mostra
como os campos da estrutura cadastro, definida anteriormente, podem ser
facilmente acessados:
147
Exemplo: acessando as variáveis de dentro da estrutura
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
struct cadastro {
char nome [ 5 0 ] ;
i n t idade ;
char rua [ 5 0 ] ;
i n t numero ;
};
i n t main ( ) {
struct cadastro c ;
/ / A t r i b u i a s t r i n g ‘ ‘ C a r l o s ’ ’ para o campo nome
s t r c p y ( c . nome , ‘ ‘ C a r l o s ’ ’ ) ;
18
19
20
21
22
23
24
25 }
/ / A t r i b u i o v a l o r 18 para o campo idade
c . idade = 1 8 ;
/ / A t r i b u i a s t r i n g ‘ ‘ Avenida B r a s i l ’ ’ para o campo
rua
s t r c p y ( c . rua , ‘ ‘ Avenida B r a s i l ’ ’ ) ;
/ / A t r i b u i o v a l o r 1082 para o campo numero
c . numero = 1082;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
Como se pode ver, cada campo da esrutura é tratado levando em consideração
o tipo que foi usado para declará-la. Como os campos nome e rua são
strings, foi preciso usar a função strcpy() para copiar o valor para esses
campos.
E se quiséssemos ler os valores dos campos da estrutura
do teclado?
Nesse caso, basta ler cada variável da estrutura independentemente, respeitando seus tipos, como é mostrado no exemplo abaixo:
148
Exemplo: lendo do teclado as variáveis da estrutura
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
struct cadastro {
char nome [ 5 0 ] ;
i n t idade ;
char rua [ 5 0 ] ;
i n t numero ;
};
i n t main ( ) {
struct cadastro c ;
/ / Lê do t e c l a d o uma s t r i n g e armazena no campo nome
g e t s ( c . nome ) ;
/ / Lê do t e c l a d o um v a l o r i n t e i r o e armazena no campo
idade
s c a n f ( ‘ ‘ % d ’ ’ ,& c . idade ) ;
15
16
17
18
19
20
/ / Lê do t e c l a d o uma s t r i n g e armazena no campo rua
g e t s ( c . rua ) ;
21
22
23
24 }
/ / Lê do t e c l a d o um v a l o r i n t e i r o e armazena no campo
numero
s c a n f ( ‘ ‘ % d ’ ’ ,& c . numero ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
Note que cada variável dentro da estrutura pode ser acessada como se
apenas ela existisse, não sofrendo nenhuma interferência das outras.
Lembre-se: uma estrutura pode ser vista como um simples
agrupamento de dados.
Como cada campo é independente um do outro, outros operadores podem
ser aplicados a cada campo. Por exemplo, pode se comparar a idade de
dois cadastros.
7.1.1
INICIALIZAÇÃO DE ESTRUTURAS
Assim como nos arrays, uma estrutura também pode ser inicializada, independente do tipo das variáveis contidas nela. Para tanto, na declaração da
variável do tipo da estrutura, basta definir uma lista de valores separados
por vı́rgula e delimitado pelo operador de chaves {}.
149
struct cadastro c = {“Carlos”,18,“Avenida Brasil”,1082 };
Nesse caso, como nos arrays, a ordem é mantida. Isso significa que o
primeiro valor da inicialização será atribuı́do a primeira variável membro
(nome) da estrutura e assim por diante.
Elementos omitidos durante a inicialização são inicializados com 0. Se for
uma string, a mesma será inicializada com uma string vazia (“”).
struct cadastro c = {“Carlos”,18 };
No exemplo acima, o campo rua é inicializado com “” e numero com zero.
7.1.2
ARRAY DE ESTRUTURAS
Voltemos ao problema do cadastro de pessoas. Vimos que o uso de estruturas facilita muito a vida do programador na manipulação dos dados do
programa. Imagine ter que declarar 4 cadastros, para 4 pessoas diferentes:
char nome1[50], nome2[50], nome3[50], nome4[50];
int idade1, idade2, idade3, idade4;
char rua1[50], rua2[50], rua3[50], rua4[50];
int numero1, numero2, numero3, numero4;
Utilizando uma estrutura, o mesmo pode ser feito da seguinte maneira:
struct cadastro c1, c2, c3, c4;
A representação desses 4 cadastros pode ser ainda mais simplificada se
utilizarmos o conceito de arrays:
struct cadastro c[4];
Desse modo, cria-se um array de estruturas, onde cada posição do array é
uma estrutura do tipo cadastro.
150
A declaração de uma array de estruturas é similar a
declaração de uma array de um tipo básico.
A combinação de arrays e estruturas permite que se manipule de modo
muito mais prático várias variáveis de estrutura. Como vimos no uso de
arrays, o uso de um ı́ndice permite que usemos comando de repetição para
executar uma mesma tarefa para diferentes posições do array. Agora, os
quatro cadastros anteriores podem ser lidos com o auxı́lio de um comando
de repetição:
Exemplo: lendo um array de estruturas do teclado
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# include <s t d i o . h>
# include < s t d l i b . h>
struct cadastro {
char nome [ 5 0 ] ;
i n t idade ;
char rua [ 5 0 ] ;
i n t numero ;
};
i n t main ( ) {
struct cadastro c [ 4 ] ;
int i ;
f o r ( i =0; i <4; i ++) {
g e t s ( c [ i ] . nome ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,& c [ i ] . idade ) ;
g e t s ( c [ i ] . rua ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,& c [ i ] . numero ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Em um array de estruturas, o operador de ponto (.) vem
depois dos colchetes [ ] do ı́ndice do array.
Essa ordem deve ser respeitada pois o ı́ndice do array é quem indica qual
posição do array queremso acessar, onde cada posição do array é uma
estrutura. Somente depois de definida qual das estruturas contidas dentro
do array nós queremos acessar é que podemos acessar os seus campos.
151
7.1.3
ATRIBUIÇÃO ENTRE ESTRUTURAS
As únicas operações possı́veis em um estrutura são as de acesso aos
membros da estrutura, por meio do operador ponto (.), e as de cópia ou
atribuição (=). A atribuição entre duas variáveis de estrutura faz com que os
contéudos das variáveis contidas dentro de uma estrutura sejam copiado
para outra estrutura.
Atribuições entre estruturas só podem ser feitas quando as
estruturas são AS MESMAS, ou seja, possuem o mesmo
nome!
Exemplo: atribuição entre estruturas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# include <s t d i o . h>
# include < s t d l i b . h>
s t r u c t ponto {
int x ;
int y ;
};
s t r u c t novo ponto {
int x ;
int y ;
};
i n t main ( ) {
s t r u c t ponto p1 , p2= { 1 , 2 } ;
s t r u c t novo ponto p3= { 3 , 4 } ;
p1 = p2 ;
p r i n t f ( ‘ ‘ p1 = %d e %d ’ ’ , p1 . x , p1 . y ) ;
/ / ERRO! TIPOS DIFERENTES
p1 = p3 ;
p r i n t f ( ‘ ‘ p1 = %d e %d ’ ’ , p1 . x , p1 . y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, p2 é atribuı́do a p1. Essa operação está correta pois
ambas as variáveis são do tipo ponto. Sendo assim, o valor de p2.x é
copiado para p1.x e o valor de p2.y é copiado para p1.y.
152
Já na segunda atribuição (p1 = p3;) ocorre um erro. Isso por que os tipos
das estruturas das variáveis são diferentes: uma pertence ao tipo struct
ponto enquanto a outra pertence ao tipo struct novo ponto. Note que o
mais importante é o nome do tipo da estrutura, e não as variáveis dentro
dela.
No caso de estarmos trabalhando com arrays de estruturas, a atribuição entre diferentes elementos do array
também é válida.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
struct cadastro {
char nome [ 5 0 ] ;
i n t idade ;
char rua [ 5 0 ] ;
i n t numero ;
};
i n t main ( ) {
struct cadastro c [ 1 0 ] ;
...
c [ 1 ] = c [ 2 ] ; / / CORRETO
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Um array ou “vetor” é um conjunto de variáveis do mesmo tipo utilizando
apenas um nome. Como todos os elementos do array são do mesmo tipo,
a atribuição entre elas é possı́vel, mesmo que o tipo do array seja uma
estrutura.
7.1.4
ESTRUTURAS ANINHADAS
Uma estrutura pode agrupar um número arbitrário de variáveis de tipos diferentes. Uma estrutura também é um tipo de dado, com a diferença de se
trata de um tipo de dado criado pelo programador. Sendo assim, podemos
declarar uma estrutura que possua uma variável do tipo de outra estrutura previamente definida. A uma estrutura que contenha outra estrutura
dentro dela damos o nome de estruturas aninhadas. O exemplo abaixo
exemplifica bem isso:
153
Exemplo: struct aninhada.
1 s t r u c t endereco {
2
char rua [ 5 0 ]
3
i n t numero ;
4 };
5 struct cadastro {
6
char nome [ 5 0 ] ;
7
i n t idade ;
8
s t r u c t endereco
ender ;
9 };
No exemplo acima, temos duas estruturas: uma chamada endereco e
outra chamada de cadastro. Note que a estrutura cadastro possui uma
variável ender do tipo struct endereco. Trata-se de uma estrutura aninhada dentro de outra.
No caso da estrutura cadastro, o acesso aos dados da
variável do tipo struct endereco é feito utilizando-se novamente o operador “.” (ponto).
Lembre-se, cada campo (variável) da estrutura pode ser acessada usando
o operador “.” (ponto). Assim, para acessar a variável ender é preciso usar
o operador ponto (.). No entanto, a variável ender também é uma estrutura.
Sendo assim, o operador ponto (.) é novamente utilizado para acessar as
variáveis dentro dessa estrutura. Esse processo se repete sempre que
houver uma nova estrutura aninhada. O exemplo abaixo mostra como a
estrutura aninhada cadastro poderia ser facilmente lida do teclado:
154
Exemplo: lendo do teclado as variáveis da estrutura
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
s t r u c t endereco {
char rua [ 5 0 ]
i n t numero ;
};
struct cadastro {
char nome [ 5 0 ] ;
i n t idade ;
s t r u c t endereco ender ;
};
i n t main ( ) {
struct cadastro c ;
/ / Lê do t e c l a d o uma s t r i n g e armazena no campo nome
g e t s ( c . nome ) ;
18
19
20
21
22
23
24
25
26
27
28
29
30 }
7.2
/ / Lê do t e c l a d o um v a l o r i n t e i r o e armazena no campo
idade
s c a n f ( ‘ ‘ % d ’ ’ ,& c . idade ) ;
/ / Lê do t e c l a d o uma s t r i n g
/ / e armazena no campo rua da v a r i á v e l ender
g e t s ( c . ender . rua ) ;
/ / Lê do t e c l a d o um v a l o r i n t e i r o
/ / e armazena no campo numero da v a r i á v e l ender
s c a n f ( ‘ ‘ % d ’ ’ ,& c . ender . numero ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
UNIÕES: UNION
Uma união pode ser vista como uma lista de variáveis, sendo que cada
uma delas pode ter qualquer tipo. A idéia básica por trás da união é similar a da estrutura: criar apenas um tipo de dado que contenha vários
membros, que nada mais são do que outras variáveis.
Tanto a declaração quanto o acesso aos elementos de uma
união são similares aos de uma estrutura.
DECLARANDO UMA UNIÃO
155
A forma geral da definição de uma união é utilizando o comando union:
union nome union{
tipo1 campo1;
tipo2 campo2;
...
tipon campoN;
};
DIFERENÇA ENTRE ESTRUTURA E UNIÃO
Até aqui, uma união se parece muito com uma estrutura. No entanto,
diferente das estruturas, todos os elementos contidos na união ocupam
o mesmo espaço fı́sico na memória. Uma estrutura reserva espaço de
memória para todos os seus elementos, enquanto que numa union reserva espaço de memória para o seu maior elemento e compartilha essa
memória com os demais elementos.
Numa struct é alocado espaço suficiente para armazenar
todos os seus elementos, enquanto que numa union é alocado espaço para armazenar o maior dos elementos que a
compõem.
Tome como exemplo a seguinte declaração de união:
union tipo{
short int x;
unsigned char c; };
Essa união possui o nome tipo e duas variáveis: x, do tipo short int (2
bytes), e c, do tipo unsigned char (1 byte). Assim, uma variável declarada
desse tipo
union tipo t;
ocupará 2 (DOIS) bytes na memória, que é o tamanho do maior dos elementos da união (short int).
156
Em uma união, apenas um membro poderá ser armazenado de cada vez.
Isso acontece por que o espaço de memória é compartilhado. Portanto,
é de total responsabilidade do programador saber qual o dado foi mais
recentemente armazenado em uma união.
Como todos os elementos de uma união se referem a um
mesmo local na memória, a modificação de um dos elementos afetará o valor de todos os demais. Numa união é
impossı́vel armazenar valores independentes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
union t i p o {
short i n t x ;
unsigned char c ;
};
i n t main ( ) {
union t i p o t ;
t . x = 1545;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , t
p r i n t f ( ‘ ‘ c = %d\n ’ ’ , t
t . c = 69;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , t
p r i n t f ( ‘ ‘ c = %d\n ’ ’ , t
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
.x) ;
.c) ;
.x) ;
.c) ;
x = 1545
c=9
x = 1605
c = 69
No exemplo acima, a variável x é do tipo short int e ocupa 16 bits (2 bytes)
de memória. Já a variável c é do tipo unsigned char e ocupa os 8 (OITO)
primeiros bits (1 byte) de x. Quando atribuimos o valor 1545 a variável x, a
variável c receberá a porção de x equivalente ao número 9:
157
Do mesmo modo, se modificarmos o valor da variável c para 69, estaremos
automáticamente modificando o valor da variável x para 1605:
Um dos usos mais comum de uma união é unir um tipo
básico a um array de tipos menores.
Tome como exemplo a seguinte declaração de união:
union tipo{
short int x;
unsigned char c[2]; };
Sabemos que a variável x ocupa 2 bytes na memória. Como cada posição
da variável c ocupa apenas 1 byte, podemos acessar facilmente cada uma
das partes da variável x, sem precisar recorrer a operações de manipulação
de bits (operações lógicas e de deslocamento de bits):
7.3
ENUMARAÇÕES: ENUM
Uma enumeração pode ser vista como uma lista de constantes, onde cada
cosntante possui um nome significativo. A idéia básica por trás da enumeração
158
é criar apenas um tipo de dado que contenha várias constante, sendo que
uma variável desse tipo só poderá receber como valor uma dessas constantes.
DECLARANDO UMA ENUMERAÇÃO
A forma geral da definição de uma enumeração é utilizando o comando
enum:
enum nome enum {lista de identificadores };
Na declaração acima, lista de identificadores é uma lista de palavras separadas por vı́rgula e delimitadas pelo operador de chaves {}. Essss palavras constituem as constantes definidas pela enumeração. Por exemplo, o
comando
enum semana {Domingo, Segunda, Terca, Quarta, Quinta, Sexta,
Sabado };
cria uma enumeração de nome semana, onde seus valores constantes
são os nomes dos dias da semana.
As estruturas podem ser declaradas em qualquer escopo
do programa (global ou local).
Apesar disso, a maioria das enumerações são declaradas no escopo global. Por se tratar de um novo tipo de dado, muitas vezes é interessante
que todo o programa tenha acesso a enumaração. Daı́ a necessidade de
usar o escopo global.
Depois do sı́mbolo de fecha chaves (}) da enumeração é
necessário colocar um ponto e vı́rgula (;).
Isso é necessário uma vez que a enumeração pode ser também declarada
no escopo local. Por questões de simplificações, e por se tratar de um novo
tipo, é possı́vel logo na definição da enumeração definir algumas variáveis
desse tipo. Para isso, basta colocar os nomes das variáveis declaradas
após o comando de fecha chaves (}) da enumeração e antes do ponto e
vı́rgula (;):
159
enum semana {Domingo, Segunda, Terca, Quarta, Quinta, Sexta,
Sabado }s1, s2;
No exemplo acima, duas variáveis (s1 e s2) são declaradas junto com a
definição da enumeração.
DECLARANDO UMA VARIÁVEL DO TIPO DA ENUMERAÇÃO
Uma vez definida a enumeração, uma variável pode ser declarada de modo
similar aos tipos já existente
enum semana s;
e inicializada como qualquer outra variável, usando, para isso, uma das
constantes da enumeração
s = Segunda;
Por ser um tipo definido pelo programador, usa-se a palavra
enum antes do tipo da nova variável declarada.
ENUMERAÇÕES E CONSTANTES
Para o programador, uma enumeração pode ser vista como uma lista de
constantes, onde cada constante possui um nome significativo. Porém,
para o compilador, cada uma das constantes é representada por um valor inteiro, sendo que o valor da primeira constante da enumeração é 0
(ZERO). Desse modo, uma enumeração pode ser usada em qualquer expressão válida com inteiros, como mostra o exemplo abaixo:
160
Exemplo: enumeração e inteiros
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 enum semana {Domingo , Segunda , Terca , Quarta ,
Quinta ,
4 Sexta , Sabado } ;
5 i n t main ( ) {
6
enum semana s1 , s2 , s3 ;
7
s1 = Segunda ;
8
s2 = Terca ;
9
s3 = s1 + s2 ;
10
p r i n t f ( ‘ ‘ Domingo = %d\n ’ ’ , Domingo ) ;
11
p r i n t f ( ‘ ‘ s1 = %d\n ’ ’ , s1 ) ;
12
p r i n t f ( ‘ ‘ s2 = %d\n ’ ’ , s2 ) ;
13
p r i n t f ( ‘ ‘ s3 = %d\n ’ ’ , s3 ) ;
14
system ( ‘ ‘ pause ’ ’ ) ;
15
return 0;
16 }
Saı́da
Domingo = 0
s1 = 1
s2 = 2
s3 = 3
No exemplo acima, a constante Domingo, Segunda e Terca, possuem,
respectivamente, os valores 0 (ZERO), 1 (UM) e 2 (DOIS). Como o compilador trata cada uma das constantes internamente como um valor inteiro,
é possı́vel somar as enumerações, ainda que isso não faça muito sentido.
161
Na definição da enumeração, pode-se definir qual valor
aquela constante possuirá.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 enum semana {Domingo = 1 , Segunda , Terca , Quarta
=7 , Quinta , Sexta , Sabado } ;
4 i n t main ( ) {
5
p r i n t f ( ‘ ‘ Domingo = %d\n ’ ’ , Domingo ) ;
6
p r i n t f ( ‘ ‘ Segunda = %d\n ’ ’ , Segunda ) ;
7
p r i n t f ( ‘ ‘ Terca = %d\n ’ ’ , Terca ) ;
8
p r i n t f ( ‘ ‘ Quarta = %d\n ’ ’ , Quarta ) ;
9
p r i n t f ( ‘ ‘ Quinta = %d\n ’ ’ , Quinta ) ;
10
p r i n t f ( ‘ ‘ Sexta = %d\n ’ ’ , Sexta ) ;
11
p r i n t f ( ‘ ‘ Sabado = %d\n ’ ’ , Sabado ) ;
12
system ( ‘ ‘ pause ’ ’ ) ;
13
return 0;
14 }
Saı́da
Domingo = 1
Segunda = 2
Terca = 3
Quarta = 7
Quinta = 8
Sexta = 9
Sabado = 10
No exemplo acima, a constante Domingo foi inicializada com o valor 1
(UM). As constantes da enumeração que não possuem valor definido são
definidas automaticamente como o valor do elemento anterior acrescidos
de um. Assim, Segunda é inicializada com 2 (DOIS) e Terca com 3
(TRÊS). Para a constante Quarta foi definido o valor 7 (SETE). Assim,
as constantes definidas na sequência após a constante Quarta possuirão
os valores 8 (OITO), 9 (NOVE) e 10 (DEZ).
162
Na definição da enumeração, pode-se também atribuir valores da tabela ASCII para as constante.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 enum escapes { r e t r o c e s s o = ’ \b ’ , t a b u l a c a o = ’ \ t ’ ,
n o v a l i n h a = ’ \n ’ } ;
4 i n t main ( ) {
5
enum escapes e = n o v a l i n h a ;
6
p r i n t f ( ‘ ‘ Teste %c de %c e s c r i t a \n ’ ’ , e , e ) ;
7
e = tabulacao ;
8
p r i n t f ( ‘ ‘ Teste %c de %c e s c r i t a \n ’ ’ , e , e ) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
Saı́da
7.4
Teste
de
escrita
Teste de escrita
COMANDO TYPEDEF
A linguagem C permite que o programador defina os seus próprios tipos
baseados em outros tipos de dados existentes. Para isso, utiliza-se o comando typedef, cuja forma geral é:
typedef tipo existente novo nome;
onde tipo existente é um tipo básico ou definido pelo programador (por
exemplo, uma struct) e novo nome é o nome para o novo tipo estamos
definindo.
O comando typedef NÃO cria um novo tipo. Ele apenas
permite que você defina um sinônimo para um tipo já existente.
Pegue como exemplo o seguinte comando:
typedef int inteiro;
163
O comando typedef não cria um novo tipo chamado inteiro. Ele apenas
cria um sinônimo (inteiro) para o tipo int. Esse novo nome torna-se equivalente ao tipo já existente.
No comando typedef, o sinônimo e o tipo existente são
equivalentes.
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
typedef i n t i n t e i r o ;
i n t main ( ) {
i n t x = 10;
i n t e i r o y = 20;
y = y + x;
p r i n t f ( ‘ ‘ Soma = %d\n ’ ’ , y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, as variáveis do tipo int e inteiro são usadas de maneira conjunta. Isso ocorre pois elas são, na verdade, do mesmo tipo (int).
O comando typedef apenas disse ao compilador para reconhecer inteiro
como um outro nome para o tipo int.
O comando typedef pode ser usado para simplificar a
declaração de um tipo definido pelo programador (struct,
union, etc) ou de um ponteiro.
Imagine a seguinte declaração de uma struct:
struct cadastro{
char nome[50];
int idade;
char rua[50];
int numero; };
Para declarar uma variável deste tipo na linguagem C a palavra-chave
struct é necessária. Assim, a declaração de uma variável c dessa estrutura seria:
164
struct cadastro c;
O comando typedef tem como objetivo atribuir nomes alternativos aos tipos já existentes, na maioria das vezes
aqueles cujo padrão de declaração é pesado e potencialmente confusa.
O comando typedef pode ser usado para eliminar a necessidade da palavrachave struct na declaração de variáveis. Por exemplo, usando o comando:
typedef struct cadastro cad;
Podemos agora declarar uma variável deste tipo usando apenas a palavra
cad:
cad c;
O comando typedef pode ser combinado com a declaração
de um tipo definido pelo programador (struct, union, etc)
em uma única instrução.
Tome como exemplo a struct cadastro declarada anteriormente:
typedef struct cadastro{
char nome[50];
int idade;
char rua[50];
int numero; } cad;
Note que a definição da estrutura está inserida no meio do comando do
typedef formando, portanto, uma única instrução. Além disso, como estamos associando um novo nome a nossa struct, seu nome original pode
ser omitido da declaração da struct:
typedef struct {
char nome[50];
int idade;
char rua[50];
int numero; } cad;
165
O comando typedef deve ser usado com cuidado pois ele
pode produzir declarações confusas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
typedef unsigned i n t p o s i t i v o s [ 5 ] ;
i n t main ( ) {
positivos v ;
int i ;
f o r ( i = 0 ; i < 5 ; i ++) {
p r i n t f ( ‘ ‘ D i g i t e o v a l o r de v[%d ] : ’ ’ , i ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,& v [ i ] ) ;
}
f o r ( i = 0 ; i < 5 ; i ++)
p r i n t f ( ‘ ‘ V a l o r de v[%d ] : %d\n ’ ’ , i , v [ i ] ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, o comando typedef é usado para criar um sinônimo
(positivos) para o tipo “array de 5 inteiros positivos” (unsigned int [5]).
Apesar de válida, essa declaração é um tanto confusa já que o novo nome
(positivos) não dá nenhum indicativo de que a variável declarada (v) seja
um array e nem seu tamanho.
166
8
FUNÇÕES
Uma função nada mais é do que um bloco de código (ou seja, declarações
e outros comandos) que podem ser nomeado e chamado de dentro de um
programa. Em outras palavras, uma função é uma seqüência de comandos
que recebe um nome e pode ser chamada de qualquer parte do programa,
quantas vezes forem necessárias, durante a execução do programa.
A linguagem C possui muitas funções já implementadas e nós temos utilizadas elas constantemente. Um exemplo delas são as funções básicas de
entrada e saı́da: scanf() e printf(). O programador não precisa saber qual
o código contido dentro das funções de entrada e saı́da para utilizá-las.
Basta saber seu nome e como utilizá-la.
A seguir, serão apresentados os conceitos e detalhes necessários para um
programador criar suas próprias funções.
8.1
DEFINIÇÃO E ESTRUTURA BÁSICA
Duas são as principais razões para o uso de funções:
• estruturação dos programas;
• reutilização de código.
Por estruturação dos programas entende-se que agora o programa será
construı́do a partir de pequenos blocos de código (isto é, funções) cada
um deles com uma tarefa especifica e bem definida. Isso facilita a compreensão do programa.
Programas grandes e complexos são construı́dos bloco a
bloco com a ajuda de funções.
Já por reutilização de código entende-se que uma função é escrita para
realizar uma determinada tarefa. Pode-se definir, por exemplo, uma função
para calcular o fatorial de um determinado número. O código para essa
função irá aparecer uma única vez em todo o programa, mas a função
que calcula o fatorial poderá ser utilizadas diversas vezes e em pontos
diferentes do programa.
167
O uso de funções evita a cópia desnecessária de trechos
de código que realizam a mesma tarefa, diminuindo assim
o tamanho do programa e a ocorrência de erros.
8.1.1
DECLARANDO UMA FUNÇÃO
Em linguagem C, a declaração de uma função pelo programador segue a
seguinte forma geral:
tipo retornado nome função (lista de parâmetros){
sequência de declarações e comandos
}
O nome função é como aquele trecho de código será conhecido dentro
do programa. Para definir esse nome, valem, basicamente, as mesmas
regras para se definir uma variável.
LOCAL DE DECLARAÇÃO DE UMA FUNÇÃO
Com relação ao local de declaração de uma função, ela deve ser definida
ou declarada antes de ser utilizada, ou seja, antes da cláusula main, como
mostra o exemplo abaixo:
Exemplo: função declarada antes da cláusula main.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t Square ( i n t a ) {
r e t u r n ( a∗a ) ;
}
i n t main ( ) {
i n t n1 , n2 ;
p r i n t f ( ‘ ‘ E n t r e com um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &n1 ) ;
n2 = Square ( n1 ) ;
p r i n t f ( ‘ ‘O seu quadrado v a l e : %d\n ’ ’ , n2 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
168
Pode-se também declarar uma função depois da cláusula main. Nesse
caso, é preciso declarar antes o protótipo da função:
tipo retornado nome função (lista de parâmetros);
O protótipo de uma função, é uma declaração de função que omite o corpo
mas especifica o seu nome, tipo de retorno e lista de parâmetros, como
mostra o exemplo abaixo:
Exemplo: função declarada depois da cláusula main.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
/ / p r o t ó t i p o da funç ão
i n t Square ( i n t a ) ;
i n t main ( ) {
i n t n1 , n2 ;
p r i n t f ( ‘ ‘ E n t r e com um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &n1 ) ;
n2 = Square ( n1 ) ;
p r i n t f ( ‘ ‘O seu quadrado v a l e : %d\n ’ ’ , n2 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
i n t Square ( i n t a ) {
r e t u r n ( a∗a ) ;
}
O protótipo de uma função não precisa incluir os nomes
das variáveis passadas como parâmetros. Apenas os seus
tipos já são suficientes.
A inclusão de nome de cada parâmetro no protótipo de uma função é uma
tarefa opcional. Podemos declarar o seu protótipo apenas com os tipos dos
parâmetros que serão passados para a função. Os nomes dos parâmetros
são importantes apenas na implementação da função. Assim, ambos os
protótipos abaixo são válidos para uma mesma função:
int Square (int a);
int Square (int );
169
FUNCIONAMENTO DE UMA FUNÇÃO
Independente de onde uma função seja declarada, seu funcionamento é
basicamente o mesmo:
• o código do programa é executado até encontrar uma chamada de
função;
• o programa é então interrompido temporariamente, e o fluxo do programa passa para a função chamada;
• se houver parâmetros na função, os valores da chamada da função
são copiados para os parãmetros no código da função;
• os comandos da função são executados;
• quando a função termina (seus comandos acabaram ou o comando
return foi encontrado), o programa volta ao ponto onde foi interrompido para continuar sua execução normal;
• se houver um comando return, o valor dele será copiado para a
variável que foi escolhida para receber o retorno da função.
Na figura abaixo, é possı́vel ter uma boa representação de como uma chamada de função ocorre:
Nas seções seguintes, cada um dos itens que definem uma função serão
apresentados em detalhes.
170
8.1.2
PARÂMETROS DE UMA FUNÇÃO
Os parâmetros de uma função são o que o programador utiliza para passar
a informação de um trecho de código para dentro da função. Basicamente,
os parâmetros de uma função são uma lista de variáveis, separadas por
vı́rgula, onde é especificado o tipo e o nome de cada variável passada para
a função.
Por exemplo, a função sqrt possui a seguinte lista de
parâmetros: float sqrt(float x);
DECLARANDO OS PARÂMETROS DE UMA FUNÇÃO
Em linguagem C, a declaração dos parâmetros de uma função segue a
seguinte forma geral:
tipo retornado nome função (tipo nome1, tipo nome2, ... ,
tipo nomeN){
sequência de declarações e comandos
}
Diferente do que acontece na declaração de variáveis,
onde muitas variáveis podem ser declaradas com o mesmo
especificador de tipo, na declaração de parâmetros de uma
função é necessário especificar o tipo de cada variável.
1
2
3
4
5
6
7
8
9
/ / Declaraç ão CORRETA de par âmetros
i n t soma ( i n t x , i n t y ) {
return x + y ;
}
/ / Declaraç ão ERRADA de par âmetros
i n t soma ( i n t x , y ) {
return x + y ;
}
FUNÇÕES SEM LISTA DE PARÂMETROS
Dependendo da função, ela pode possuir nenhum parâmetro. Nesse caso,
pode-se optar por duas soluções:
171
• Deixar a lista de parâmetros vazia: void imprime ();
• Colocar void entre parênteses: void imprime (void).
Mesmo se não houver parâmetros na função,
parênteses ainda são necessários.
os
Apesar das duas declarações estarem corretas, existe uma diferença entre elas. Na primeira declaração, não é especificado nenhum parâmetro,
portanto a função pode ser chamada passando-se valores para ela. O o
compilador não irá verificar se a função é realmente chamada sem argumentos e a função não conseguirá ter acesso a esses parâmetros. Já na
segunda declaração, nenhum parâmetro é esperado. Nesse caso, o programa acusará um erro se o programador tentar passar um valor para essa
função.
Colocar void na lista de parâmetros é diferente de se colocar nenhum parâmetro.
O exemplo abaixo ilustra bem essa situação:
Sem void
Exemplo: função sem parâmetros
Com void
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 void imprime ( ) {
5
p r i n t f ( ‘ ‘ Teste de
funcao \n ’ ’ ) ;
6 }
7
8 i n t main ( ) {
9
imprime ( ) ;
10
imprime ( 5 ) ;
11
imprime ( 5 , ’ a ’ ) ;
12
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 void imprime ( void ) {
5
p r i n t f ( ‘ ‘ Teste de
funcao \n ’ ’ ) ;
6 }
7
8 i n t main ( ) {
9
imprime ( ) ;
10
imprime ( 5 ) ; / / ERRO
11
imprime ( 5 , ’ a ’ ) ; / / ERRO
12
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
172
Os parâmetros das funções também estão sujeitos ao escopo das variáveis.
O escopo é o conjunto de regras que determinam o uso e a validade de
variáveis nas diversas partes do programa.
O parâmetro de uma função é uma variável local da função
e portanto, só pode ser acessado dentro da função.
8.1.3
CORPO DA FUNÇÃO
Pode-se dizer que o corpo de uma função é a sua alma. É no corpo de
uma função que se define qual a tarefa que a função irá realizar quando
for chamada.
Basicamente, o corpo da função é formado por:
• sequência de declarações: variáveis, constantes, arrays, etc;
• sequência de comandos: comandos condicionais, de repetição, chamada de outras funções, etc.
Para melhor entender o corpo da função, considere que todo programa
possui ao menos uma função: a função main. A função mais é a função
“principal” do programa, o “corpo” do programa. Note que nos exemplo
usados até agora, a função main é sempre do tipo int, e sempre retorna o
valor 0:
int main () {
sequência de declarações e comandos
return 0;
}
Basicamente, é no corpo da função que as entradas (parâmetros) são processadas, as saı́das são geradas ou outras ações são feitas. Além disso,
a função main se encarrega de realizar a comunicação com o usuário, ou
seja, é ela quem realiza as operações de entrada e saı́da de dados (comandos scanf e printf). Desse modo, tudo o que temos feito dentro de
uma função main pode ser feito em uma função desenvolvida pelo programador.
173
Tudo o que temos feito dentro da função main pode ser feito
em uma função desenvolvida pelo programador.
Uma função é construı́da com o intuito de realizar uma tarefa especifica e
bem definida. Por exemplo, uma função para calcular o fatorial deve ser
construı́da de modo a receber um determinado número como parâmetro
e retornar (usando o comando return) o valor calculado. As operações de
entrada e saı́da de dados (comandos scanf e printf) devem ser feitas em
quem chamou a função (por exemplo, na main). Isso garante que a função
construı́da possa ser utilizada nas mais diversas aplicações, garantindo a
sua generalidade.
De modo geral, evita-se fazer operações de leitura e escrita
dentro de uma função.
Os exemplos abaixo ilustram bem essa situação. No primeiro exemplo
temos o cálculo do fatorial realizado dentro da função main:
Exemplo: cálculo do fatorial dentro da função main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
p r i n t f ( ‘ ‘ D i g i t e um numero i n t e i r o p o s i t i v o : ’ ’ ) ;
int x ;
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
int i , f = 1;
f o r ( i =1; i <=x ; i ++)
f = f ∗ i;
p r i n t f ( ‘ ‘O f a t o r i a l de %d eh : %d\n ’ ’ , x , f ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Perceba que no exemplo acima, não foi feito nada de diferente do que
temos feito até o momento. Já no exemplo abaixo, uma função especifica
para o cálculo do fatorial foi construı́da:
174
Exemplo: cálculo do fatorial em uma função própria
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# include <s t d i o . h>
# include < s t d l i b . h>
int f a t o r i a l
int i , f =
f o r ( i =1;
f = f ∗
( int n){
1;
i <=n ; i ++)
i;
return f ;
}
i n t main ( ) {
p r i n t f ( ‘ ‘ D i g i t e um numero i n t e i r o p o s i t i v o : ’ ’ ) ;
int x ;
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
int fat = f a t o r i a l (x) ;
p r i n t f ( ‘ ‘O f a t o r i a l de %d eh : %d\n ’ ’ , x , f a t ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note que dentro da função responsável pelo cálculo do fatorial, apenas o
trecho do código responsável pelo cálculo do fatorial está presente. As
operações de entrada e saı́da de dados (comandos scanf e printf) são
feitos em quem chamou a função fatorial, ou seja, na função main.
Operações de leitura e escrita não são proibidas dentro de
uma função. Apenas não devem ser usadas se esse não
for o foco da função.
Uma função deve conter apenas o trecho de código responsável por fazer
aquilo que é o objetivo da função. Isso não impede que operações de
leitura e escrita sejam utilizadas dentro da função. Elas só não devem ser
usadas quando os valores podem ser passados para a função por meio
dos parâmetros.
Abaixo temos um exemplo de função que realiza operações de leitura e
escrita:
175
Exemplo: função contendo operações de leitura e escrita.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# include <s t d i o . h>
# include < s t d l i b . h>
i n t menu ( ) {
int i ;
do {
p r i n t f ( ‘ ‘ Escolha uma opç ão : \ n ’ ’ ) ;
p r i n t f ( ‘ ‘ ( 1 ) Opcao 1\n ’ ’ ) ;
p r i n t f ( ‘ ‘ ( 2 ) Opcao 2\n ’ ’ ) ;
p r i n t f ( ‘ ‘ ( 3 ) Opcao 3\n ’ ’ ) ;
scanf ( ‘ ‘%d ’ ’ , & i ) ;
} while ( ( i < 1 ) | | ( i > 3 ) ) ;
return i ;
}
i n t main ( ) {
i n t op = menu ( ) ;
p r i n t f ( ‘ ‘ Vc escolheu a Opcao %d . \ n ’ ’ , op ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Na função acima, um menu de opções é apresentado ao usuário que tem
de escolher dentre uma delas. A função se encarrega de verificar se a
opção digitada é válida e, caso não seja, solicitar uma nova opção ao
usuário.
8.1.4
RETORNO DA FUNÇÃO
O retorno da função é a maneira como uma função devolve o resultado (se
ele existir) da sua execução para quem a chamou. Nas seções anterores
vimos que uma função segue a seguinte forma geral:
tipo retornado nome função (lista de parâmetros){
sequência de declarações e comandos
}
A expressão tipo retornado estabelece o tipo de valor que a função irá
devolver para quem chamá-la. Uma função pode retornar qualquer tipo
válido em na linguagem C:
• tipos básicos pré-definidos: int, char, float, double, void e ponteiros;
176
• tipos definidos pelo programador: struct, array (indiretamente), etc.
FUNÇÕES SEM RETORNO DE VALOR
Uma função também pode NÃO retornar um valor. Para
isso, basta colocar o tipo void como valor retornado.
O tipo void é conhecido como o tipo vazio. Uma função declarada com o
tipo void irá apenas executar um conjunto de comando e não irá devolver
nenhum valor para quem a chamar. Veja o exemplo abaixo:
Exemplo: função com tipo void
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
void imprime ( i n t n ) {
int i ;
f o r ( i =1; i <=n ; i ++)
p r i n t f ( ‘ ‘ Linha %d \n ’ ’ , i ) ;
}
i n t main ( ) {
imprime ( 5 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, a função imprime irá apenas imprimir uma mensagem
na tela n vezes. Não há o que devolver para a função main. Portanto,
podemos declarar ela como void.
Para executar uma função do tipo void, basta colocar no
código onde a função será chamada o nome da função e
seus parâmetros.
FUNÇÕES COM RETORNO DE VALOR
Se a função não for do tipo void, então ela deverá retornar um valor. O
comando return é utilizado para retornar esse valor para o programa:
177
return expressão;
A expressão da claúsula return tem que ser compatı́vel
com o tipo de retorno declarado para a função.
A expressão do comando return consiste em qualquer constante, variável
ou expressão aritmética que o programador deseje retornar para o trecho
do programa que chamou a função. Essa expressão pode até mesmo ser
uma outra função, como a função sqrt():
return sqrt(x);
Para executar uma função que tenha o comando return,
basta atribuir a chamada da função (nome da função e
seus parâmetros) a uma variável compatı́vel com o tipo do
retorno.
O exemplo abaixo mostra uma função que recebe dois parâmetros inteiros
e retorna a sua soma para a função main:
Exemplo: função com retorno
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t soma ( i n t x , i n t y ) {
return x + y ;
}
i n t main ( ) {
int a ,b , c ;
p r i n t f ( ‘ ‘ Digite a: ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &a ) ;
p r i n t f ( ‘ ‘ Digite b: ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ , &b ) ;
p r i n t f ( ‘ ‘ Soma = %d\n ’ ’ ,soma ( a , b ) ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, no exemplo acima, que a chamada da função foi feita dentro do comando printf. Isso é possı́vel pois a função retorna um valor inteiro (x+y)
e o comando printf espera imprimir um valor inteiro (%d).
178
Uma função pode ter mais de uma declaração return.
O uso de vários comandos return é útil quando o retorno da função está
relacionado a uma determinada condição dentro da função. Veja o exemplo
abaixo:
Exemplo: função com vários return
1 i n t maior ( i n t x , i n t y ) {
2
if (x > y)
3
return x ;
4
else
5
return y ;
6 }
No exemplo acima, a função será executada e dependendo dos valores
de x e y, uma das cláusulas return será executada. No entanto, é conveniente limitar as funções a usar somente um comando return. O uso de
vários comandos return, especialmente em função grandes e complexas,
aumenta a dificuldidade de se compreender o que realmente está sendo
feito pela função. Na maioria dos casos, pode-se reescrever uma função
para que ela use somente um comando return, como é mostrado abaixo:
Exemplo: substituindo os vários return da função
1 i n t maior ( i n t x , i n t y ) {
2
int z ;
3
if (x > y)
4
z = x;
5
else
6
z = y;
7
return z ;
8 }
No exemplo acima, os vários comando return foram substituidos por uma
variável que será retornada no final da função.
Quando se chega a um comando return, a função é encerrada imediatamente.
179
O comando return é utilizado para retornar um valor para o programa. No
entanto, esse comando também é usado para terminar a execução de uma
função, similar ao comando break em um laço ou switch:
Exemplo: finalizando a função com return
1 i n t maior ( i n t x , i n t y ) {
2
if (x > y)
3
return x ;
4
else
5
return y ;
6
p r i n t f ( ‘ ‘ Fim da funcao \n ’ ’ ) ;
7 }
No exemplo acima, a função irá terminar quando um dos comando return
for executado. A mensagem “Fim da funcao” jamais será impressa na tela
pois seu comando se encontra depois do comando return. Nesse caso, o
comando printf será ignorado.
O comando return pode ser usado sem um valor associado
a ele para terminar uma função do tipo void.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
# include <math . h>
void i m p r i m e l o g ( f l o a t x ) {
i f ( x <= 0 )
r e t u r n ; / / t e r m i n a a funç ão
p r i n t f ( ‘ ‘ Log = %f \n ’ ’ , l o g ( x ) ) ;
}
i n t main ( ) {
float x ;
p r i n t f ( ‘ ‘ Digite x : ’ ’ ) ;
scanf ( ‘ ‘% f ’ ’ , &f ) ;
imprime log ( x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Na função contida no exemploa cima, se o valor de x é negativo ou zero,
o comando return faz com que a função termine antes que o comando
printf seja executado, mas nenhum valor é retornado.
180
O valor retornado por uma função não pode ser um array.
Lembre-se: a linguagem C não suporta a atribuição de um array para outro.
Por esse motivo, não se pode ter como retorno de uma função um array.
É possı́vel retornar um array indiretamente, desde que ela
faça parte de uma estrutura.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# include <s t d i o . h>
# include < s t d l i b . h>
struct vetor {
int v [ 5 ] ;
};
struct vetor retorna array ( ) {
struct vetor v = {1 ,2 ,3 ,4 ,5};
return v ;
}
i n t main ( ) {
int i ;
struct vetor vet = retorna array ( ) ;
f o r ( i =0; i <5; i ++)
p r i n t f ( ‘ ‘ V a l o r e s : %d \n ’ ’ , v e t . v [ i ] ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
A linguagem C não suporta a atribuição de um array para outro. Mas ela
permite a atrbuição entre estruturas. A atribuição entre duas variáveis de
estrutura faz com que os contéudos das variáveis contidas dentro de uma
estrutura sejam copiado para outra estrutura. Desse modo, é possı́vel retornar um array desde que o mesmo esteja dentro de uma estrutura.
8.2
TIPOS DE PASSAGEM DE PARÂMETROS
Já vimos que, na linguagem C, os parâmetros de uma função é o mecanismo que o programador utiliza para passar a informação de um trecho
de código para dentro da função. Mas existem dois tipos de passagem de
parâmetro: passagem por valor e por referência.
181
Nas seções seguintes, cada um dos tipos de passagem de parâmetros
será explicado em detalhes.
8.2.1
PASSAGEM POR VALOR
Na linguagem C, os argumentos para uma função são sempre passados
por valor (by value), ou seja, uma cópia do dado é feita e passada para a
função. Esse tipo de passagem de parâmetro é o padrão para todos os tipos básicos pré-definidos (int, char, float e double) e estruturas definidas
pelo programador (struct).
Mesmo que o valor de uma variável mude dentro da função,
nada acontece com o valor de fora da função.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Saı́da
i n c l u d e <s t d i o . h>
i n c l u d e < s t d l i b . h>
void soma mais um ( i n t n ) {
n = n + 1;
p r i n t f ( ‘ ‘ Dentro da funcao : x = %d\n ’ ’ , n ) ;
}
i n t main ( ) {
int x = 5;
p r i n t f ( ‘ ‘ Antes da funcao : x = %d\n ’ ’ , x ) ;
soma mais um ( x ) ;
p r i n t f ( ‘ ‘ Depois da funcao : x = %d\n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Antes da funcao: x = 5
Dentro da funcao: x = 6
Depois da funcao: x = 5
No exemplo acima, no momento em que a função soma mais um é chamada, o valor de x é copiado para o parâmetro n da função. O parâmetro
n é uma variável local da função. Então, tudo o que acontecer com ele (n)
não se reflete no valor original da variável x. Quando a função termina, a
variável n é destruı́da e seu valor é descartado. O fluxo do programa é devolvido ao ponto onde a função foi inicialmente chamada, onde a variável
x mantém o seu valor original.
182
Na passagem de parâmetros por valor, quaisquer
modificações que a função fizer nos parâmetros existem
apenas dentro da própria função.
8.2.2
PASSAGEM POR REFERÊNCIA
Na passagem de parâmetros por valor, as funções não podem modificar o valor original de uma variável passada para a função. Mas existem casos em que é necessário que toda modificação feita nos valores
dos parâmetros dentro da função sejam repassados para quem chamou a
função. Um exemplo bastante simples disso é a função scanf: sempre que
desejamos ler algo do teclado, passamos para a função scanf o nome da
variável onde o dado será armazenado. Essa variável tem seu valor modificado dentro da função scanf e seu valor pode ser acessado no programa
principal.
A função scanf é um exemplo bastante simples de função
que altera o valor de uma variável e essa mudança se reflete fora da função.
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int x = 5;
p r i n t f ( ‘ ‘ Antes do s c a n f : x = %d\n ’ ’ , x ) ;
p r i n t f ( ‘ ‘ D i g i t e um numero : ’ ’ ) ;
s c a n f ( ‘ ‘ % d ’ ’ ,& x ) ;
p r i n t f ( ‘ ‘ Depois do s c a n f : x = %d\n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Quando se quer que o valor da variável mude dentro da função e essa
mudança se reflita fora da função, usa-se passagem de parâmetros por
referência.
Na passagem de parâmetros por referência não se passa
para a função os valores das variáveis, mas sim os
endereços das variáveis na memória.
Na passagem de parâmetros por referência o que é enviado para a função
é o endereço de memória onde a variável está armazenada, e não uma
183
simples cópia de seu valor. Assim, utilizando o endereço da variável na
memória, qualquer alteração que a variável sofra dentro da função será
também refletida fora da função.
Para passar um parâmetro por referência, usa-se o operador “*” na frente do nome do parâmetro durante a
declaração da função.
Para passar para a função um parâmetro por referência, a função precisa
usar ponteiros. Um ponteiro é um tipo especial de variável que armazena
um endereço de memória, da mesma maneira como uma variável armazena um valor. Mais detalhes sobre o uso de ponteiros serão apresentados
no capı́tulo seguinte.
O exemplo abaixo mostra a mesma função declarada usando a passagem
de parâmetro de valor e por referência:
Exemplo: passagem por valor e referência
Por valor
Por referência
1 void soma mais um ( i n t n )
{
2
n = n + 1;
3 }
1 void soma mais um ( i n t ∗n
){
2
∗n = ∗n + 1 ;
3 }
Note, no exemplo acima, que a diferença entre os dois tipos de passagem
de parâmetro é o uso do operador “*” na passagem por referência. Consequentemente, toda vez que a variável passada por referência for usada
dentro da função, o operador “*” deverá ser usado na frente do nome da
variável.
Na chamada da função é necessário utilizar o operador “&”
na frente do nome da variável que será passada por referência.
Lembre-se do exemplo da função scanf. A função scanf é um exemplo
de função que altera o valor de uma variável e essa mudança se reflete
fora da função. Quando chamamos a função scanf, é necessário colocar
o operador “&” na frente do nome da variável que será lida do teclado. O
mesmo vale para outra funções que usam passagem de parâmetro por
referência.
184
Na passagem de uma variável por referência é necessário
usar o operador “*” sempre que se desejar acessar o
conteúdo da variável dentro da função.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Saı́da
i n c l u d e <s t d i o . h>
i n c l u d e < s t d l i b . h>
void soma mais um ( i n t ∗n ) {
∗n = ∗n + 1 ;
p r i n t f ( ‘ ‘ Dentro da funcao : x = %d\n ’ ’ , ∗ n ) ;
}
i n t main ( ) {
int x = 5;
p r i n t f ( ‘ ‘ Antes da funcao : x = %d\n ’ ’ , x ) ;
soma mais um (& x ) ;
p r i n t f ( ‘ ‘ Depois da funcao : x = %d\n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Antes da funcao: x = 5
Dentro da funcao: x = 6
Depois da funcao: x = 6
No exemplo acima, no momento em que a função soma mais um é chamada, o endereço de x (&x) é copiado para o parâmetro n da função. O
parâmetro n é um ponteiro dentro da função que guarda o endereço de
onde o valor de x está guardado fora da função. Sempre que alteramos
o valor de *n (conteúdo da posição de memória guardada, ou seja, x), o
valor de x fora da função também é modificado.
Abaixo temos outro exemplo que mostra a mesma função declarada usando
a passagem de parâmetro de valor e por referência:
185
Exemplo: passagem por valor e referência
Por valor
Por referência
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 void Troca ( i n t a , i n t b ) {
5
i n t temp ;
6
temp = a ;
7
a = b;
8
b = temp ;
9
p r i n t f ( ‘ ‘ Dentro : %d e
%d\n ’ ’ , a , b ) ;
10 }
11
12 i n t main ( ) {
13
int x = 2;
14
int y = 3;
15
p r i n t f ( ‘ ‘ Antes : %d e %
d\n ’ ’ , x , y ) ;
16
Troca ( x , y ) ;
17
p r i n t f ( ‘ ‘ Depois : %d e
%d\n ’ ’ , x , y ) ;
18
system ( ‘ ‘ pause ’ ’ ) ;
19
return 0;
20 }
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 void Troca ( i n t ∗a , i n t ∗b ) {
5
i n t temp ;
6
temp = ∗a ;
7
∗a = ∗b ;
8
∗b = temp ;
9
p r i n t f ( ‘ ‘ Dentro : %d e
%d\n ’ ’ , ∗ a , ∗ b ) ;
10 }
11
12 i n t main ( ) {
13
int x = 2;
14
int y = 3;
15
p r i n t f ( ‘ ‘ Antes : %d e %
d\n ’ ’ , x , y ) ;
16
Troca (& x ,& y ) ;
17
p r i n t f ( ‘ ‘ Depois : %d e
%d\n ’ ’ , x , y ) ;
18
system ( ‘ ‘ pause ’ ’ ) ;
19
return 0;
20 }
Saı́da
Antes: 2 e 3
Dentro: 3 e 2
Depois: 2 e 3
8.2.3
Saı́da
Antes: 2 e 3
Dentro: 3 e 2
Depois: 3 e 2
PASSAGEM DE ARRAYS COMO PARÂMETROS
Para utilizar arrays como parâmetros de funções alguns cuidados simples
são necessários. Além do parâmetro do array que será utilizado na função,
é necessário declarar um segundo parâmetro (em geral uma variável inteira) para passar para a função o tamanho do array separadamente.
Arrays são sempre passados por referência para uma
função.
Quando passamos um array por parâmetro, independente do seu tipo, o
que é de fato passado para a função é o endereço do primeiro elemento
do array.
186
A passagem de arrays por referência evita a cópia desnecessária de grandes quantidades de dados para outras
áreas de memória durante a chamada da função, o que
afetaria o desempenho do programa.
Na passagem de um array como parâmetro de uma função podemos declarar a função de diferentes maneiras, todas equivalentes:
void imprime (int *m, int n);
void imprime (int m[], int n);
void imprime (int m[5], int n);
Mesmo especificando o tamanho de um array no parâmetro
da função a semântica é a mesma das outras declarações,
pois não existe checagem dos limites do array em tempo
de compilação.
O exemplo abaixo mostra como um array de uma única dimensão pode ser
passado como parâmetro para uma função:
Exemplo: passagem de array como parâmetro
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 void imprime ( i n t ∗n
, i n t m) {
5
int i ;
6
f o r ( i =0; i <m; i ++)
7
p r i n t f ( ‘ ‘ % d \n ’ ’
, n[ i ]) ;
8 }
9
10 i n t main ( ) {
11
int v [5] =
{1 ,2 ,3 ,4 ,5};
12
imprime ( v , 5 ) ;
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
Note, no exemplo acima, que apenas o nome do array é passado para a
função, sem colchetes. Isso significa que estamos passando o array inteiro.
187
Se usassemos o colchete, estariamos passando o valor de uma posição
do array e não o seu endereço, o que resultaria em um erro.
Na chamada da função, passamos para ela somente o
nome do array, sem os colchetes: o programa “já sabe” que
um array será enviado, pois isso já foi definido no protótipo
da função.
Vimos que, para arrays, não é necessário especificar o número de elementos para a função no parâmetro do array:
void imprime (int *m, int n);
void imprime (int m[], int n);
Arrays com mais de uma dimensão (por exemplo, matrizes), precisam da informação do tamanho das dimensões
extras.
Para arrays com mais de uma dimensão é necessário o tamanho de todas
as dimensões, exceto a primeira. Sendo assim, uma declaração possı́vel
para uma matriz de 4 linhas e 5 colunas seria a apresentada abaixo:
void imprime (int m[][5], int n);
A declaração de arrays com uma dimensão e com mais de uma dimensão é
diferente porque na passagem de um array para uma função o compilador
precisar saber o tamanho de cada elemento, não o número de elementos.
Um array bidimensional poder ser entendido como um array de arrays.
Para a linguagem C, um array bidimensional poder ser entendido como um
array de arrays. Sendo assim, o seguinte array
int m[4][5];
pode ser entendido como um array de 4 elementos, onde cada elemento
é um array de 5 posições inteiras. Logo, o compilador precisa saber o
tamanho de um dos elementos (por exemplo, o número de colunas da
matriz) no momento da declaração da função:
188
void imprime (int m[][5], int n);
Na notação acima, informamos ao compilador que estamos passando um
array, onde cada elemento dele é outro array de 5 posições inteiras. Nesse
caso, o array terá sempre 5 colunas, mas poderá ter quantas linhas quiser
(parâmetro n).
Isso é necessário para que o programa saiba que o array possui mais de
uma dimensão e mantenha a notação de um conjunto de colchetes por
dimensão.
O exemplo abaixo mostra como um array de duas dimensões pode ser
passado como parâmetro para uma função:
Exemplo: passagem de matriz como parâmetro
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
void i m p r i m e m a t r i z ( i n t m[ ] [ 2 ] , i n t n ) {
int i , j ;
f o r ( i =0; i <n ; i ++)
f o r ( j =0; j < 2 ; j ++)
p r i n t f ( ‘ ‘ % d \n ’ ’ , m[ i ] [ j ] ) ;
}
i n t main ( ) {
i n t mat [ 3 ] [ 2 ] = { { 1 , 2 } , { 3 , 4 } , { 5 , 6 } } ;
i m p r i m e m a t r i z ( mat , 3 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
As notações abaixo funcionam para arrays com mais de uma dimensão.
Mas o array é tratado como se tivesse apenas uma dimensão dentro da
função
void imprime (int *m, int n);
void imprime (int m[], int n);
O exemplo abaixo mostra como um array de duas dimensões pode ser
passado como um array de uma única dimensão para uma função:
189
Exemplo: matriz como array de uma dimensão
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
void i m p r i m e m a t r i z ( i n t ∗m, i n t n ) {
int i ;
f o r ( i =0; i <n ; i ++)
p r i n t f ( ‘ ‘ % d \n ’ ’ , m[ i ] ) ;
}
i n t main ( ) {
i n t mat [ 3 ] [ 2 ] = { { 1 , 2 } , { 3 , 4 } , { 5 , 6 } } ;
i m p r i m e m a t r i z (& mat [ 0 ] [ 0 ] , 6 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note que, nesse exemplo, ao invés de passarmos o nome do array nós
passamos o endereço do primeiro elemento (&mat[0][0]). Isso faz com que
percamos a notação de dois colchetes para a matriz, e ela seja tratada
como se tivesse apenas uma dimensão.
8.2.4
PASSAGEM DE ESTRUTURAS COMO PARÂMETROS
Vimos anteriormente que uma estrutura pode ser vista como um conjunto
de variáveis sob um mesmo nome ou, em outras palavras, a estrutura é
uma variável que contém dentro de si outras variáveis. Sendo assim, uma
estrutura pode ser passada como parâmetro para uma função de duas
formas distintas:
• toda a estrutura;
• apenas determinados campos da estrutura.
PASSAGEM DE ESTRUTURAS POR VALOR
Para passar uma estrutura como parâmetro de uma função, basta declarar na lista de parâmetros um parâmetro com o mesmo tipo da estrutura.
Dessa forma, teremos acesso a todos os campos da estrutura dentro da
função, como mostra o exemplo abaixo:
190
Exemplo: estrutura como parâmetro da função
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
s t r u c t ponto {
int x , y ;
};
void imprime ( s t r u c t ponto p ) {
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , p . x ) ;
p r i n t f ( ‘ ‘ y = %d\n ’ ’ , p . y ) ;
}
i n t main ( ) {
s t r u c t ponto p1 = { 10 ,20} ;
imprime ( p1 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Dependendo da aplicação, pode ser que não seja necessário passar todos
os valores da estrutura para a função. Nesse caso, a função é declarada
sem levar em conta a estrutura nos seus parâmetros. Mas é necessário
que o parâmetro da função seja compatı́vel com o campo da função que
será passado como parâmetro, como mostra o exemplo abaixo:
Exemplo: campo da estrutura como parâmetro da função
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# include <s t d i o . h>
# include < s t d l i b . h>
s t r u c t ponto {
int x , y ;
};
void i m p r i m e v a l o r ( i n t x ) {
p r i n t f ( ‘ ‘ V a l o r = %d\n ’ ’ , p . x ) ;
}
i n t main ( ) {
s t r u c t ponto p1 = { 10 ,20} ;
i m p r i m e v a l o r ( p1 . x ) ;
i m p r i m e v a l o r ( p1 . y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
PASSAGEM DE ESTRUTURAS POR REFERÊNCIA
Vimos anteriormente que para passar um parâmetro por referência, usase o operador “*” na frente do nome do parâmetro durante a declaração
191
da função. Isso também é válido para uma estrutura, mas alguns cuidados devem ser tomados ao acessar seus campos dentro da função. Para
acessar o valor de um campo de uma estrutura passada por referência,
devemos seguir o seguinte conjunto de passos:
1. utilizar o operador “*” na frente do nome da variável que representa a
estrutura;
2. colocar o operador “*” e o nome da variável entre parênteses ();
3. por fim, acessar o campo da estrutura utilizando o operador ponto “.”.
O exemplo abaixo mostra como os campos de uma estrutura passada por
referência devem ser acessado:
Exemplo: estrutura passada por referência
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
s t r u c t ponto {
int x , y ;
};
void a t r i b u i ( s t r u c t ponto ∗p ) {
(∗ p ) . x = 10;
(∗ p ) . y = 20;
}
i n t main ( ) {
s t r u c t ponto p1 ;
a t r i b u i (&p1 ) ;
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , p1 . x ) ;
p r i n t f ( ‘ ‘ y = %d\n ’ ’ , p1 . y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, no exemplo acima, que a função atribui recebe uma struct ponto
por referência, “*p”. Para acessar qualquer um dos seus campos (x ou
y), é necessário utilizar o operador “*” na frente do nome da variável que
representa a estrutura, “*p”, e em seguida colocar o operador “*” e o nome
da variável entre parênteses, “(*p)”. Somente depois de feito isso é que
podemos acessar um dos campos da estrutura com o operador ponto “.”
(linhas 7 e 8).
Ao acessar uma estrutura passada por referência não podemos esquecer de colocar os parêntese antes de acessar
o seu campo.
192
O uso dos parênteses serve para diferenciar que é que foi passado por
referência de quem é ponteiro. Um ponteiro é um tipo especial de variável
que armazena um endereço de memória, da mesma maneira como uma
variável armazena um valor (mais detalhes sobre o uso de ponteiros serão
apresentados no capı́tulo seguinte). A expressão
(*p).x
indica que a variável p é na verdade o ponteiro, ou melhor, a variável que
foi passada por referência. Isso ocorre porque o asterisco está junto de
p, e isolado de x por meio dos parênteses. Já nas notações abaixo são
equivalentes
*p.x
*(p.x)
e ambas indicam que a variável x é na verdade o ponteiro, e não p. Isso
ocorre pois o operador ponto “.” tem prioridade e é executado primeiro.
Logo, o operador asterisco “*” irá atuar sobre o campo da estrutura, e não
sobre a variável da estrutura.
8.2.5
OPERADOR SETA
De modo geral, uma estrutura é sempre passada por valor para uma função.
Mas ela também pode ser passada por referência sempre que desejarmos
alterar algum dos valores de seus campos.
Durante o estudo dos tipos definidos pelo programador, vimos que o operador “.” (ponto) era utilizado para acessar os campos de uma estrutura.
Se essa estrutura for passada por referência para uma função, será necessário usar ambos os operadores “*” e “.” para acessar os valores originais dos campos da estrutura.
• operador “*”: acessa o conteúdo da posição de memória (valor da
variável fora da função) dentro da função;
• operador “.”: acessa os campos de uma estrutura.
O operador seta “->” substitui o uso conjunto dos operadores “*” e “.” no acesso ao campo de uma estrutura passada
por referência para uma função.
193
O operador seta “->” é utilizado quando uma referência para uma estrutura
(struct) é passada para uma função. Ele permite acessar o valor do campo
da estrutura fora da função sem utilizar o operador “*”. O exemplo abaixo
mostra como os campos de uma estrutura passada por referência podem
ser acessado com ou sem o uso do operador seta “->”:
Exemplo: passagem por valor e referência
Sem operador seta
Com operador seta
1 s t r u c t ponto {
2
int x , y ;
3 };
4
5 void f u n c ( s t r u c t ponto ∗
p){
6
(∗ p ) . x = 10;
7
(∗ p ) . y = 20;
8 }
8.3
1 s t r u c t ponto {
2
int x , y ;
3 };
4
5 void f u n c ( s t r u c t ponto ∗
p){
6
p−>x = 1 0 ;
7
p−>y = 2 0 ;
8 }
RECURSÃO
Na linguagem C, uma função pode chamar outra função. Um exemplo
disso é quando chamamos qualquer uma das nossas funções implementadas na função main. Uma função pode, inclusive, chamar a si própria.
Uma função assim é chamada de função recursiva.
A recursão também é chamada de definição circular. Ela
ocorre quando algo é definido em termos de si mesmo.
Um exemplo clássico de função que usa recursão é o cálculo do fatorial de
um número. A função fatorial é definida como:
0! = 1
N! = N * (N - 1)!
A idéia básica da recursão é dividir um problema maior em um conjunto
de problemas menores, que são então resolvidos de forma independente
e depois combinados para gerar a solução final: dividir e conquistar.
Isso fica evidente no cálculo do fatorial. O fatorial de um número N é o
produto de todos os números inteiros entre 1 e N. Por exemplo, o fatorial
194
de 3 é igual a 1 * 2 * 3, ou seja, 6. No entanto, o fatorial desse mesmo
número 3 pode ser definido em termos do fatorial de 2, ou seja, 3! = 3 *
2!. O exemplo abaixo apresenta as funções com e sem recursão para o
cálculo do fatorial:
Com Recursão
Exemplo: fatorial
Sem Recursão
1 int f a t o r i a l ( int n){
2
i f ( n == 0 )
3
return 1;
4
else
5
r e t u r n n∗ f a t o r i a l ( n
−1) ;
6 }
1 int f a t o r i a l ( int n){
2
i f ( n == 0 )
3
return 1;
4
else {
5
int i , f = 1;
6
f o r ( i =2; i <= n ; i
++)
7
f = f ∗ i;
8
return f ;
9
}
10 }
Em geral, as formas recursivas dos algoritmos são consideradas “mais
enxutas” e “mais elegantes” do que suas formas iterativas. Isso facilita
a interpretação do código. Porém, esses algoritmos apresentam maior
dificuldade na detecção de erros e podem ser ineficientes.
Todo cuidado é pouco ao se fazer funções recursivas, pois
duas coisas devem ficar bem estabelecidas: o critério de
parada e o parâmetro da chamada recursiva.
Durante a implementação de uma função recursiva temos que ter em mente
duas coisas: o critério de parada e o parâmetro da chamada recursiva:
• Critério de parada: determina quando a função deverá parar de
chamar a si mesma. Se ele não existir, a função irá executar infinitamente. No cálculo de fatorial, o critério de parada ocorre quando
tentamos calcular o fatorial de zero: 0! = 1.
• Parâmetro da chamada recursiva: quando chamamos a função
dentro dela mesmo, devemos sempre mudar o valor do parãmetro
passado, de forma que a recursão chegue a um término. Se o valor do parâmetro for sempre o mesmo a função irá executar infinitamente. No cálculo de fatorial, a mudança no parâmetro da chamada
recursiva ocorre quando definimos o fatorial de N em termos no fatorial de (N-1): N! = N * (N - 1)! .
195
O exemplo abaixo deixa bem claro o critério de parada e o parâmetro da
chamada recursiva na função recursiva implementada em linguagem C:
Exemplo: fatorial
1 int f a t o r i a l ( int n){
2
i f ( n == 0 ) / / c r i t é r i o de parada
3
return 1;
4
else / / par âmetro do f a t o r i a l sempre muda
5
r e t u r n n∗ f a t o r i a l ( n−1) ;
6 }
Note que a implementação da função recursiva do fatorial em C segue
exatamente o que foi definido matemáticamente.
Algoritmos recursivos tendem a necessitar de mais tempo
e/ou espaço do que algoritmos iterativos.
Sempre que chamamos uma função, é necessário um espaço de memória
para armazenar os parâmetros, variáveis locais e endereço de retorno da
função. Numa função recursiva, essas informações são armazenadas para
cada chamada da recursão, sendo, portanto a memória necessária para
armazená-las proporcional ao número de chamadas da recursão.
Além disso, todas essas tarefas de alocar e liberar memória, copiar informações,
etc. envolvem tempo computacional, de modo que uma função recursiva
gasta mais tempo que sua versão iterativa (sem recursão).
O que acontece quando chamamos a função fatorial com
um valor como N = 3?
Nesse caso, a função será chamada tantas vezes quantas forem necessárias.
A cada chamada, a função irá verificar se o valor de N é igual a zero. Se
não for, uma nova chamada da função será realizada. Esse processo,
identificado pelas setas pretas, continua até que o valor de N seja decrementado para ZERO. Ao chegar nesse ponto, a função começa o processo
inverso (identificado pelas setas vermelhas): ela passa a devolver para
quem a chamou o valor do comando return. A figura abaixo mostra esse
processo para N = 3:
196
Outro exemplo clássico de recursão é a seqüência de Fibonacci:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, . . .
A sequênciade de Fibonacci é definida como uma função recursiva utilizando a fórmula abaixo:
O exemplo abaixo apresenta as funções com e sem recursão para o cálculo
da sequência de de Fibonacci:
Exemplo: seqüência de Fibonacci
Com Recursão
Sem Recursão
1 int fibo ( int n){
2
i n t i , t , c , a=0 , b =1;
3
f o r ( i =0; i <n ; i ++) {
4
c = a + b;
5
a = b;
6
b = c;
7
}
8
return a ;
9 }
1 int fibo ( int n){
2
i f ( n == 0 | | n == 1 )
3
return n ;
4
else
5
r e t u r n f i b o ( n−1) +
f i b o ( n−2) ;
6 }
197
Como se nota, a solução recursiva para a seqüência de Fibonacci é muito
elegante. Infelizmente, como se verifica na imagem abaixo, elegância não
significa eficiência.
Na figura acima, as setas pretas indicam quando uma nova chamada da
função é realizada, enquanto as setas vermelhas indicam o processo inverso, ou seja, quando a função passa a devolver para quem a chamou
o valor do comando return. O maior problema da solução recursiva está
nos quadrados marcados com pontilhados verde. Neles, fica claro que
o mesmo cálculo é realizado duas vezes, um desperdı́cio de tempo e
espaço!
Se, ao invés de calcularmos fibo(4) quisermos calcular fibo(5), teremos
um desperdı́cio ainda maior de tempo e espaço, como mostra a figura
abaixo:
198
199
9
PONTEIROS
Toda informação que manipulamos dentro de um programa (esteja ela
guardada em uma variável, array, estrutura, etc.) obrigatoriamente está
armazenada na memória do computador. Quando criamos uma variável,
o computador reserva um espaço de memória onde poderemos guardar
o valor associado a essa variável. Ao nome que damos a essa variável o
computador associa o endereço do espaço que ele reservou na memória
para guardar essa variável. De modo geral, interessa ao programador saber o nome das variáveis. Já o computador precisa saber onde elas estão
na memória, ou seja, precisa dos endereços das variáveis.
Ponteiros são um tipo especial de variáveis que permitem armazenam endereços de memória ao invés de dados
númericos (como os tipos int, float e double) ou caracteres (como o tipo char).
Por meio dos ponteiros, podemos acessar o endereço de uma variável e
manipular o valor que está armazenado lá dentro. Eles são uma ferramenta
extremamente útil dentro da linguagem C. Por exemplo, quando trabalhamos com arrays, nós estamos utilizando ponteiros.
Apesar de suas vantagens, muitos programadores tem
medo, ou até mesmo aversão, ao uso dos ponteiros. Isso
por que existem muitos perigos na utilização de ponteiros.
Isso ocorre por que os ponteiros permitem que um programa acesse objetos que não foram explicitamente declarados com antecedência e, consequentemente, permitem uma grande variedade de erros de programação.
Outro grande problema dos ponteiros é que eles podem ser apontados
para endereços (ou seja, armazenar o endereço de uma posição de
memória) não utilizados, ou para dados dentro da memória que estão
sendo usados para outros propósitos. Apesar desses perigos no uso de
ponteiros, seu poder é tão grande que existem tarefas que são difı́ceis de
serem implementadas sem a utilização de ponteiros.
A seguir, serão apresentados os conceitos e detalhes necessários para um
programador utilizar com sabedoria um ponteiro.
200
9.1
DECLARAÇÃO
Ponteiros são um tipo especial de variáveis que permitem armazenam
endereços de memória ao invés de dados númericos (como os tipos int,
float e double) ou caracteres (como o tipo char). É importante sempre
lembrar:
• Variável: é um espaço reservado de memória usado para guardar
um valor que pode ser modificado pelo programa;
• Ponteiro: é um espaço reservado de memória usado para guardar
um endereço de memória.
Na linguame C, um ponteiro pode ser declarado para qualquer tipo de variável (char, int, float, double, etc), inclusive
para aquelas criadas pelo programador (struct, etc).
Em linguagem C, a declaração de um ponteiro pelo programador segue a
seguinte forma geral:
tipo do ponteiro *nome do ponteiro;
É o operador asterisco (*) que informa ao compilador que
aquela variável não vai guardar um valor, mas sim um
endereço de memória para aquele tipo especificado.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
/ / Declara um p o n t e i r o para i n t
5
i n t ∗p ;
6
/ / Declara um p o n t e i r o para f l o a t
7
f l o a t ∗x ;
8
/ / Declara um p o n t e i r o para char
9
char ∗y ;
10
/ / Declara uma v a r i á v e l do t i p o i n t e um
p o n t e i r o para i n t
11
i n t soma , ∗p2 , ;
12
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
201
Na linguagem C, quando declaramos um ponteiro nós informamos ao compilador para que tipo de variável nós vamos poder apontá-lo. Um ponteiro
do tipo int* só pode apontar para uma variável do tipo int (ou seja, esse
ponteiro só poderá guardar o endereço de uma variável do tipo int)
Apesar de usarem o mesmo sı́mbolo, o operador *
(multiplicação) não é o mesmo operador que o *
(referência de ponteiros).
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int x = 3, y = 5, z ;
z = y ∗ x;
i n t ∗p ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, o operador asterisco (*) é usado de duas maneiras
distintas:
• Na linha 5: trata-se de um operador binário, ou seja que atua sobre
dois valores/variáveis (nesse caso, é a multiplicação das mesmas);
• Na linha 6: trata-se de um operador unário pré-fixado, ou seja atua
sobre uma única variável (nesse caso, é a declaração de um ponteiro).
Lembre-se: o significado do operador asterisco (*) depende de como ele é utilizado dentro do programa.
9.2
MANIPULANDO PONTEIROS
9.2.1
INICIALIZAÇÃO E ATRIBUIÇÃO
Ponteiros apontam para uma posição de memória. Sendo assim, a simples
declaração de um ponteiro não faz dele útil para o programa. Precisamos
indicar para que endereço de memória ele aponta.
202
Ponteiros não inicializados apontam para um lugar indefinido.
Quando um ponteiro é declarado, ele não possui um endereço associado.
Qualquer tentativa de uso desse ponteiro causa um comportamento indefinido no programa.
Isso ocorre por que seu valor não é um endereço válido ou porque sua
utilização pode danificar partes diferentes do sistema. Por esse motivo, os
ponteiros devem ser inicializados (apontado para algum lugar conhecido)
antes de serem usados.
APONTANDO UM PONTEIRO PARA NENHUM LUGAR
Um ponteiro pode ter um valor especial NULL, que é o
endereço de nenhum lugar.
A constante NULL está definida na biblioteca stdlib.h. Trata-se de um
valor reservado que indica que aquele ponteiro aponta para uma posição
de memória inexistente. O valor da constante NULL é ZERO na maioria
dos computadores.
Não confunda um ponteiro apontando para NULL com um
ponteiro não inicializado. O primeiro possui um valor fixo,
enquanto um ponteiro não inicializado pode possuir qualquer valor.
203
APONTANDO UM PONTEIRO PARA ALGUM LUGAR DA MEMÓRIA
Vimos que a constante NULL permite apontar um ponteiro para uma posição
de memória inexistente. Mas como fazer para atribuir uma posição de
memória válida para o ponteiro?
Basicamente, podemos fazer nosso ponteiro apontar para uma variável
que já exista no nosso programa. Lembre-se, quando criamos uma variável,
o computador reserva um espaço de memória. Ao nome que damos a essa
variável o computador associa o endereço do espaço que ele reservou na
memória para guardar essa variável.
Para saber o endereço onde uma variável está guardada na memória, usase o operador & na frente do nome da variável.
Para saber o endereço de uma variável do nosso programa
na memória usa-se o operador &.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
/ / Declara uma v a r i á v e l i n t contendo o v a l o r 10
5
i n t count = 1 0 ;
6
/ / Declara um p o n t e i r o para i n t
7
i n t ∗p ;
8
/ / A t r i b u i ao p o n t e i r o o endereço da v a r i á v e l
int
9
p = &count ;
10
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
No exemplo acima, são declarados uma variável tipo int (count) e um
ponteiro para o mesmo tipo (p). Na linha 9, o ponteiro p é inicializado
com o endereço da variável count. Note que usamos o operador de
endereçamento (&) para a inicialização do ponteiro. Isso significa que
o ponteiro p passa a conter o endereço de count, não o seu valor. Para
melhor entender esse conceito, veja a figura abaixo:
204
Tendo um ponteiro armazenado um endereço de memória, como saber o
valor guardado dentro dessa posição de memória? Simples, para acessar
o conteúdo da posição de memória para a qual o ponteiro aponta, usa-se
o operador asterisco (*) na frente do nome do ponteiro.
Para acessar o valor guardado dentro de uma posição na
memória apontada por um ponteiro, basta usar o operador
asterisco (*).
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
/ / Declara uma v a r i á v e l i n t contendo o v a l o r 10
5
i n t count = 1 0 ;
6
/ / Declara um p o n t e i r o para i n t
7
i n t ∗p ;
8
/ / A t r i b u i ao p o n t e i r o o endereço da v a r i á v e l
int
9
p = &count ;
10
p r i n t f ( ‘ ‘ Conteudo apontado por p : %d \n ’ ’ , ∗ p ) ;
11
/ / A t r i b u i um novo v a l o r à posiç ão de mem ória
apontada por p
12
∗p = 1 2 ;
13
p r i n t f ( ‘ ‘ Conteudo apontado por p : %d \n ’ ’ , ∗ p ) ;
14
p r i n t f ( ‘ ‘ Conteudo de count : %d \n ’ ’ , count ) ;
15
16
system ( ‘ ‘ pause ’ ’ ) ;
17
return 0;
18 }
Saı́da
Conteudo apontado por p: 10
Conteudo apontado por p: 12
Conteudo de count: 12
Note, no exemplo acima, que utilizamos o operador asterisco (*) sempre
que queremos acessar o valor contido na posição de memória apontada
por p. Note também que, se alterarmos o valor contido nessa posição de
memória (linha 12), alteramos o valor da variável count.
205
OS OPERADORES “*” E “&”
Ao se trabalhar com ponteiros, duas tarefas básicas serão sempre executadas:
• acessar o endereço de memória de uma variável;
• acessar o conteúdo de um endereço de memória;
Para realizar essas tarefas, iremos sempre utilizar apenas dois operadores:
o operador “*” e o operador “&”.
“*”
“&”
Operador “*” versus operador “&”
Declara um ponteiro: int *x;
Conteúdo para onde o ponteiro aponta: int y = *x;
Endereço onde uma variável está guardada na memória:
&y
ATRIBUIÇÃO ENTRE PONTEIROS
Devemos estar sempre atento a operação de atribuição quando estamos
trabalhando com ponteiros. Não só com relação ao uso corretos dos operadores, mas também ao que estamos atribuindo ao ponteiro.
De modo geral, um ponteiro só pode receber o endereço
de memória de uma variável do mesmo tipo do ponteiro.
Isso ocorre por que diferentes tipos de variáveis ocupam um espaço de
memória de tamanhos diferentes. Na verdade, nós podemos, por exemplo, atribuir a um ponteiro de inteiro (int *) o endereço de uma variável
do tipo float. O compilador não irá acusar nenhum erro. No entanto, o
compilador assume que qualquer endereço que esse ponteiro armazene
obrigatoriamente apontará para uma variável do tipo int. Consequentemente, qualquer tentativa de uso desse ponteiro causa um comportamento
indefinido no programa. Veja o exemplo abaixo:
206
Exemplo: atribuição de ponteiros
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p , ∗p1 , x = 1 0 ;
5
float y = 20.0;
6
p = &x ;
7
p r i n t f ( ‘ ‘ Conteudo apontado
8
p1 = p ;
9
p r i n t f ( ‘ ‘ Conteudo apontado
);
10
p = &y ;
11
p r i n t f ( ‘ ‘ Conteudo apontado
12
p r i n t f ( ‘ ‘ Conteudo apontado
13
p r i n t f ( ‘ ‘ Conteudo apontado
float ∗)p ) ) ;
14
15
system ( ‘ ‘ pause ’ ’ ) ;
16
return 0;
17 }
Saı́da
por p : %d \n ’ ’ , ∗ p ) ;
por p1 : %d \n ’ ’ , ∗ p1
por p : %d \n ’ ’ , ∗ p ) ;
por p : %f \n ’ ’ , ∗ p ) ;
por p : %f \n ’ ’ , ∗ ( (
Conteudo apontado por p: 10
Conteudo apontado por p1: 10
Conteudo apontado por p: 1101004800
Conteudo apontado por p: 0.000000
Conteudo apontado por p: 20.000000
No exemplo acima, um endereço de uma variável do tipo float é atribuido a
um ponteiro do tipo int (linha 10). Note que qualquer tentativa de acessar
o seu conteúdo se mostra falha (linhas 11 e 12). Só conseguimos acessar
corretamente o seu conteúdo quando utilizamos o operador de typecast
sobre o ponteiro e antes de acessar o seu conteúdo (linha 13).
Um ponteiro pode receber o endereço apontado por outro
ponteiro, se ambos forem do mesmo tipo.
Se dois ponteiros são do mesmo tipo, então eles podem quardar endereços
de memória para o mesmo tipo de dado. Logo a atribuição entre eles é
possı́vel. Isso é mostrado no exemplo anterior (linhas 8 e 9).
207
Um ponteiro pode receber um valor hexadecimal representado um endereço de memória diretamente. Isso é muito
útil quando se trabalha, por exemplo, com microcontroladores.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
/ / Endereço hexadecimal da p o r t a s e r i a l
5
i n t ∗p = 0x3F8 ;
6
/ / O v a l o r em decimal é c o n v e r t i d o para seu
v a l o r haxadecimal : 0x5DC
7
i n t ∗p1 = 1500;
8
p r i n t f ( ‘ ‘ Endereco em p : %p \n ’ ’ , p ) ;
9
p r i n t f ( ‘ ‘ Endereco em p1 : %p \n ’ ’ , p1 ) ;
10
system ( ‘ ‘ pause ’ ’ ) ;
11
return 0;
12 }
Saı́da
Endereco em p: 000003F8
Endereco em p1: 000005DC
Na linguagem C, um valor hexadecimal deve começar com “0x” (um zero
seguido de um x), seguido pelo valor em formato hexadecimal, que pode
ser formado por:
• dı́gitos: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9;
• letras: A, B, C, D, E, F.
Deve-se tomar muito cuidado com esse tipo de utilização de ponteiros,
principalmente quando queremos acessar o conteúdo daquela posição de
memória. Afinal de contas, o que existe na posição de memória 0x5DC?
Esse é um erro muito comum.
9.2.2
ARITMÉTICA COM PONTEIROS
As operações aritmética utilizando ponteiros são bastante limitadas, o que
facilita o seu uso. Basicamente, apenas duas operações aritméticas podem ser utilizadas nos endereços armazenados pelos ponteiros: adição e
subtração.
208
Sobre o valor de endereço armazenado por um ponteiro
podemos apenas somar e subtrair valores INTEIROS.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p = 0x5DC ;
5
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
6
/ / Incrementa p em uma posiç ão
7
p ++;
8
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
9
/ / Incrementa p em 15 posiç ões
10
p = p + 15;
11
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
12
/ / Decrementa p em 2 posiç ões
13
p = p − 2;
14
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
15
system ( ‘ ‘ pause ’ ’ ) ;
16
return 0;
17 }
Saı́da
p = Hexadecimal:
p = Hexadecimal:
p = Hexadecimal:
p = Hexadecimal:
%d \n ’ ’ ,
%d \n ’ ’ ,
%d \n ’ ’ ,
%d \n ’ ’ ,
000005DC Decimal: 1500
000005E0 Decimal: 1504
0000061C Decimal: 1564
00000614 Decimal: 1556
As operações de adição e subtração no endereço permitem avançar ou retroceder nas posições de memória do computador. Esse tipo de operação
é bastante útil quando trabalhamos com arrays, por exemplo. Lembre-se:
um array nada mais é do que um conjuno de elementos adjacentes na
memória.
Além disso, todas as operações de adição e subtração no endereço devem
ser inteiras. Afinal de contas, não dá para andar apenas MEIA posição na
memória.
No entanto, é possı́vel notar no exemplo anterior que a operação de incremento p++ (linha 7) não incrementou em uma posição o endereço, mas sim
em quatro posições: ele foi da posição 1500 para a 1504. Isso aconteceu
por que nosso ponteiro é do tipo inteiro (int *).
209
As operações de adição e subtração no endereço dependem do tipo de dado que o ponteiro aponta.
Suponha um ponteiro para inteiro, int *p. Esse ponteiro deverá receber um
endereço de um valor inteiro. Quando declaramos uma variável interia (int
x), o computador reserva um espaço de 4 bytes na memória para essa
variável. Assim, nas operações de adição e subtração são adicionados/subtraı́dos um total de 4 bytes por incremento/decremento, pois este é o
tamanho de um inteiro na memória e, portanto, é também o valor mı́nimo
necessário para sair dessa posição reservada de memória. Se o ponteiro
fosse para o tipo double, as operações de incremento/decremento mudariam a posição de memória em 8 bytes.
Sobre o conteúdo apontado pelo ponteiro valem todas as
operações aritméticas que o tipo do ponteiro suporta.
1
2
3
4
5
6
7
8
9
10
11
12
13
Saı́da
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p , x = 1 0 ;
p = &x ;
p r i n t f ( ‘ ‘ Conteudo apontado por p : %d \n ’ ’ , ∗ p ) ;
∗p = ( ∗ p ) ++;
p r i n t f ( ‘ ‘ Conteudo apontado por p : %d \n ’ ’ , ∗ p ) ;
∗p = ( ∗ p ) ∗ 1 0 ;
p r i n t f ( ‘ ‘ Conteudo apontado por p : %d \n ’ ’ , ∗ p ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Conteudo apontado por p: 10
Conteudo apontado por p: 11
Conteudo apontado por p: 110
Quando utilizamos o operador asterisco (*) na frente do nome do ponteiro
estamos acessando o conteúdo da posição de memória para a qual o ponteiro aponta. Em resumo, estamos acessando o valor guardado na variável
para qual o ponteiro aponta. Sobre esse valor, valem todas as operações
que o tipo do ponteiro suporta.
210
9.2.3
OPERAÇÕES RELACIONAIS COM PONTEIROS
A linguagem C permite comparar os endereços de memória armazenados
por dois ponteiros utilizando uma expressão relacional. Por exemplo, os
operadores == e ! = são usado para saber se dois ponteiros são iguais
ou diferentes.
Dois ponteiros são considerados iguais se eles apontam
para a mesma posição de memória.
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p , ∗p1 , x , y ;
p = &x ;
p1 = &y ;
i f ( p == p1 )
p r i n t f ( ‘ ‘ P o n t e i r o s i g u a i s \n ’ ’ ) ;
else
p r i n t f ( ‘ ‘ P o n t e i r o s d i f e r e n t e s \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Já os operadores >, <, >= e <= são usado para saber se um ponteiro
aponta para uma posição mais adiante na memória do que outro. Novamente, esse tipo de operação é bastante útil quando trabalhamos com
arrays, por exemplo. Lembre-se: um array nada mais é do que um conjuno
de elementos adjacentes na memória.
211
Exemplo: comparando ponteiros
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p , ∗p1 , x , y ;
5
p = &x ;
6
p1 = &y ;
7
i f ( p > p1 )
8
p r i n t f ( ‘ ‘O p o n t e i r o p aponta para uma posicao a
f r e n t e de p1\n ’ ’ ) ;
9
else
10
p r i n t f ( ‘ ‘O p o n t e i r o p NAO aponta para uma posicao a
f r e n t e de p1\n ’ ’ ) ;
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
Como no caso das operações aritméticas, quando utilizamos o operador
asterisco (*) na frente do nome do ponteiro estamos acessando o conteúdo
da posição de memória para a qual o ponteiro aponta. Em resumo, estamos acessando o valor guardado na variável para qual o ponteiro aponta.
Sobre esse valor, valem todas as operações relacionais que o tipo do ponteiro suporta.
Exemplo: comparando o conteúdo dos ponteiros
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p , ∗p1 , x = 10 , y = 2 0 ;
5
p = &x ;
6
p1 = &y ;
7
i f ( ∗ p > ∗p1 )
8
p r i n t f ( ‘ ‘O conteudo de p e maior do que o conteudo
de p1\n ’ ’ ) ;
9
else
10
p r i n t f ( ‘ ‘O conteudo de p NAO e maior do que o
conteudo de p1\n ’ ’ ) ;
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
212
9.3
PONTEIROS GENÉRICOS
Normalmente, um ponteiro aponta para um tipo especı́fico de dado. Porém,
pode-se criar um ponteiro genérico. Esse tipo de ponteiro pode apontar para todos os tipos de dados existentes ou que ainda serão criados.
Em linguagem C, a declaração de um ponteiro genérico segue a seguinte
forma geral:
void *nome do ponteiro;
Um ponteiro genérico é um ponteiro que pode apontar para
qualquer tipo de dado, inclusive para outro ponteiro.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 i n t main ( ) {
5
void ∗pp ;
6
i n t ∗p1 , p2 = 1 0 ;
7
p1 = &p2 ;
8
/ / recebe o endereço de um i n t e i r o
9
pp = &p2 ;
10
p r i n t f ( ‘ ‘ Endereco em pp : %p \n ’ ’ , pp ) ;
11
/ / recebe o endereço de um p o n t e i r o para
inteiro
12
pp = &p1 ;
13
p r i n t f ( ‘ ‘ Endereco em pp : %p \n ’ ’ , pp ) ;
14
/ / recebe o endereço guardado em p1 ( endereço
de p2 )
15
pp = p1 ;
16
p r i n t f ( ‘ ‘ Endereco em pp : %p \n ’ ’ , pp ) ;
17
system ( ‘ ‘ pause ’ ’ ) ;
18
return 0;
19 }
Note, no exemplo acima, que ponteiro genérico permite guardar o endereço
de qualquer tipo de dado. Essa vantagem vem com uma desvantagem:
sempre que tivermos que acessar o conteúdo de um ponteiro genérico
será necessário utilizar o operador de typecast sobre o ponteiro e antes
de acessar o seu conteúdo.
213
Sempre que se trabalhar com um ponteiro genérico é preciso converter o ponteiro genérico para o tipo de ponteiro
com o qual se deseja trabalhar antes de acessar o seu
conteúdo.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 i n t main ( ) {
5
void ∗pp ;
6
i n t p2 = 1 0 ;
7
/ / p o n t e i r o g e n é r i c o recebe o endereço de um
inteiro
8
pp = &p2 ;
9
/ / enta acessar o conte údo do p o n t e i r o g e n é r i c o
10
p r i n t f ( ‘ ‘ Conteudo : %d\n ’ ’ , ∗ pp ) ; / / ERRO
11
/ / c o n v e r t e o p o n t e i r o g e n é r i c o pp para ( i n t ∗ )
antes de acessar seu conte údo .
12
p r i n t f ( ‘ ‘ Conteudo : %d\n ’ ’ , ∗ ( i n t ∗ ) pp ) ; / /
CORRETO
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
No exemplo acima, como o compilador não sabe qual o tipo do ponteiro
genérico, acessar o seu conteúdo gera um tipo de erro. Somente é possı́vel
acessar o seu conteúdo depois de uma operação de typecast.
Outro cuidado que devemos ter com ponteiros genéricos: como o ponteiro
genérico não possui tipo definido, deve-se tomar cuidado com ao se realizar operações aritméticas.
214
As operações aritméticas não funcionam em ponteiros
genéricos da mesma forma como em ponteiros de tipos
definidos. Elas são sempre realizadas com base em uma
unidade de memória (1 byte).
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
void ∗p = 0x5DC ;
5
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
6
/ / Incrementa p em uma posiç ão
7
p ++;
8
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
9
/ / Incrementa p em 15 posiç ões
10
p = p + 15;
11
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
12
/ / Decrementa p em 2 posiç ões
13
p = p − 2;
14
p r i n t f ( ‘ ‘ p = Hexadecimal : %p Decimal :
p,p) ;
15
system ( ‘ ‘ pause ’ ’ ) ;
16
return 0;
17 }
Saı́da
p = Hexadecimal:
p = Hexadecimal:
p = Hexadecimal:
p = Hexadecimal:
%d \n ’ ’ ,
%d \n ’ ’ ,
%d \n ’ ’ ,
%d \n ’ ’ ,
000005DC Decimal: 1500
000005DD Decimal: 1501
000005EC Decimal: 1516
000005EA Decimal: 1514
No exemplo acima, como o compilador não sabe qual o tipo do ponteiro
genérico, nas operações de adição e subtração são adicionados/subtraı́dos
um total de 1 byte por incremento/decremento, pois este é o tamanho de
uma unidade de memória. Portanto, se o endereço guardado for, por exemplo, de um inteiro, o incremento de uma posição no ponteiro genérico (1
byte) não irá levar ao próximo inteiro (4 bytes).
9.4
PONTEIROS E ARRAYS
Ponteiros e arrays possuem uma ligação muito forte dentro da linguagem C. Arrays são agrupamentos de dados do mesmo tipo na memória.
Quando declaramos um array, informamos ao computador para reservar
uma certa quantidade de memória para armazenar os elementos do array
de forma sequencial. Como resultado dessa operação, o computador nos
215
devolve um ponteiro que aponta para o começo dessa sequência de bytes
na memória.
O nome do array é apenas um ponteiro que aponta para o
primeiro elemento do array.
Na linguagem C, o nome de um array sem um ı́ndice guarda o endereço
para o começo do array na memória, ou seja, ele guarda o endereço do
inı́cio de uma área de armazenamento dentro da memória. Isso significa
que as operações envolvendo arrays podem ser feitas utilizando ponteiros
e aritmética de ponteiros.
Exemplo: acessando arrays utilizando ponteiros.
Usando Array
Usando Ponteiro
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int vet [ 5 ] =
{1 ,2 ,3 ,4 ,5};
5
i n t ∗p = v e t ;
6
int i ;
7
f o r ( i = 0 ; i < 5 ; i ++)
8
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i
]) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int vet [ 5 ] =
{1 ,2 ,3 ,4 ,5};
5
i n t ∗p = v e t ;
6
int i ;
7
f o r ( i = 0 ; i < 5 ; i ++)
8
p r i n t f ( ‘ ‘ % d\n ’ ’ , ∗ ( p+
i));
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
No exemplo acima, temos o mesmo código utilizando a notação de colchetes e de aritmética de ponteiros para acessar os elementos de um array.
Note que se para acessar o elemento na posição i do array podemos escrever p[i] ou *(p+i).
Quanto a atribuição do endereço do array para o ponteiro, podemos fazê-la
de duas formas:
int *p = vet;
int *p = &vet[0];
Na primeira forma, o nome do array é utilizado para retornar o endereço
onde ele começa na memória. Já na segunda forma, nós utilizamos o
216
operador de endereço (&) para retornar o endereço da primeira posição do
array.
O operador colchetes [ ] substitui o uso conjunto de
operações aritméticas e de acesso ao conteúdo (operador
“*”) no acesso ao conteúdo de uma posição de um array.
Durante o estudo de ponteiros, vimos que o operador asterisco (*) é utilizado para acessar o valor guardado dentro de uma posição na memória
apontada por um ponteiro. Além disso, operações aritméticas podem ser
usadas para avançar sequencialmente na memória. Lembre-se, um array é um agrupamento sequencial de dados do mesmo tipo na memória.
Sendo assim, o operador colchetes apenas simplifica o uso conjunto de
operações aritméticas e de acesso ao conteúdo (operador “*”) no acesso
ao conteúdo de uma posição de um array.
Abaixo temos uma lista mostrado as equivalências entre arrays e ponteiros:
217
Exemplo: equivalências entre arrays e ponteiros.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
int vet [ 5 ] = {1 ,2 ,3 ,4 ,5};
5
i n t ∗p , i n d i c e = 2 ;
6
p = vet ;
7
/ / v e t [ 0 ] é e q u i v a l e n t e a ∗p ;
8
p r i n t f ( ‘ ‘ % d\n ’ ’ , ∗ p ) ;
9
p r i n t f ( ‘ ‘ % d\n ’ ’ , v e t [ 0 ] ) ;
10
/ / v e t [ i n d i c e ] é e q u i v a l e n t e
11
/ / a ∗ ( p+ i n d i c e ) ;
12
p r i n t f ( ‘ ‘ % d\n ’ ’ , v e t [ i n d i c e ] )
;
13
p r i n t f ( ‘ ‘ % d\n ’ ’ , ∗ ( p+ i n d i c e ) )
;
14
/ / v e t é e q u i v a l e n t e
15
/ / a &v e t [ 0 ] ;
16
p r i n t f ( ‘ ‘ % d\n ’ ’ , v e t ) ;
17
p r i n t f ( ‘ ‘ % d\n ’ ’ ,& v e t [ 0 ] ) ;
18
/ / &v e t [ i n d i c e ] é e q u i v a l e n t e
19
/ / a ( vet+indice ) ;
20
p r i n t f ( ‘ ‘ % d\n ’ ’ ,& v e t [ i n d i c e
]) ;
21
p r i n t f ( ‘ ‘ % d\n ’ ’ , ( v e t + i n d i c e )
);
22
system ( ‘ ‘ pause ’ ’ ) ;
23
return 0;
24 }
No exemplo anterior, note que o valor entre colchetes é o deslocamento a
partir da posição inicial. Nesse caso, p[indice] equivale a *(p+indice).
Um ponteiro também pode ser usado para acessar os dados de uma string.
Lembre-se: string é o nome que usamos para definir uma seqüência de
caracteres adjacentes na memória do computador. Essa seqüência de
caracteres, que pode ser uma palavra ou frase, é armazenada na memória
do computador na forma de um arrays do tipo char.
218
9.4.1
PONTEIROS E ARRAYS MULTIDIMENSIONAIS
Apesar de terem o comportamento de estruturas com mais de uma dimensão, os dados dos arrays multidimensionais são armazenados linearmente na memória. É o uso dos colchetes que cria a impressão de
estarmos trabalhando com mais de uma dimensão. Por exemplo, a matriz
int mat[5][5];
apesar de ser bidimensional, ela é armazenada como um simples array na
memória:
Nós podemos acessar os elementos de um array multidimensional usando
a notação tradicional de colchetes (mat[linha][coluna]) ou a notação por
ponteiros:
*(*(mat + linha) + coluna)
Para entender melhor o que está acontecendo, vamos trocar
*(mat + linha)
por um valor X. Desse modo, a expressão fica
*(X + coluna)
É possı́vel agora perceber que X é como um ponteiro, é que o seu conteúdo
é o endereço de uma outra posição de memória. Em outras palavras, o
valor de linhas é o deslocamento na memória do primeiro ponteiro (ou primeira dimensão da matriz), enquanto o valor de colunas é o deslocamento
na memória do segundo ponteiro (ou segunda dimensão da matriz).
219
Ponteiros permitem percorrer as várias dimensões de um
arrays multidimensional como se existisse apenas uma dimensão. As dimensões mais a direita mudam mais rápido.
Na primeira forma, o nome do array é utilizado para retornar o endereço
onde ele começa na memória. Isso é muito útil quando queremos construir
uma função que possa percorrer um array independente do número de
dimensões que ele possua. Para realizar essa tarefa, nós utilizamos o
operador de endereço (&) para retornar o endereço da primeira posição do
array, como mostra o exemplo abaixo:
Acessando um array multidimensional utilizando ponteiros.
Usando Array
Usando Ponteiro
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t mat [ 2 ] [ 2 ] =
{{1 ,2} ,{3 ,4}};
5
int i , j ;
6
f o r ( i =0; i <2; i ++)
7
f o r ( j =0; j <2; j ++)
8
p r i n t f ( ‘ ‘ % d\n ’ ’ ,
mat [ i ] [ j ] ) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
9.4.2
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t mat [ 2 ] [ 2 ] =
{{1 ,2} ,{3 ,4}};
5
i n t ∗ p = &mat [ 0 ] [ 0 ] ;
6
int i ;
7
f o r ( i =0; i <4; i ++)
8
p r i n t f ( ‘ ‘ % d\n ’ ’
, ∗ ( p+ i ) ) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
ARRAY DE PONTEIROS
A linguagem C também permite que declaremos arrays de ponteiros como
fazemos com com qualquer outro tipo de dado. A declaração de um array
de ponteiros segue a seguinte forma geral:
tipo dado *nome array[tamanho];
O comando acima define um array de nome nome array contendo tamanho elementos adjacentes na memória. Cada elemento do array é do tipo
tipo dado*, ou seja, é um ponteiro para tipo dado. Assim, a declaração de
um array de ponteiros para inteiros de tamanho 10 seria:
int *p[10];
220
Quanto ao seu uso, não existem diferenças de um array de ponteiros e um
ponteiro. Basta lembrar que um array é sempre indexado. Assim, para
atribuir o endereço de uma variável x a uma posição do array de ponteiros,
escrevemos:
p[indice] = &x;
E para retornar o conteúdo guardado nessa posição de memória:
*p[indice]
Cada posição de um array de ponteiros pode armazenar o
endereço de uma variável ou o endereço da posição inicial
de um outro array.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗ pvet [ 2 ] ;
5
i n t x = 10 , y [ 2 ] = { 20 ,30} ;
6
p v e t [ 0 ] = &x ;
7
pvet [ 1 ] = y ;
8
/ / imprime os endereços das v a r i a v é i s
9
p r i n t f ( ‘ ‘ Endereco p v e t [ 0 ] : %p\n ’ ’ , p v e t [ 0 ] ) ;
10
p r i n t f ( ‘ ‘ Endereco p v e t [ 1 ] : %p\n ’ ’ , p v e t [ 1 ] ) ;
11
/ / imprime o conte údo de uma v a r i á v e l
12
p r i n t f ( ‘ ‘ Conteudo em p v e t [ 0 ] : %d\n ’ ’ , ∗ p v e t [ 0 ] )
;
13
/ / imprime uma posiç ão do v e t o r
14
p r i n t f ( ‘ ‘ Conteudo p v e t [ 1 ] [ 1 ] : %d\n ’ ’ , p v e t
[1][1]) ;
15
system ( ‘ ‘ pause ’ ’ ) ;
16
return 0;
17 }
9.5
PONTEIRO PARA PONTEIRO
Ao longo dessa seção, vimos que toda informação que manipulamos dentro de um programa está obrigatoriamente armazenada na memória do
computador e, portanto, possui um endereço de memória associado a ela.
Ponteiros, como qualquer outra variável, também ocupam um espaço na
memória do computador e também possuem o endereço desse espaço de
memória associado ao seu nome. Como não existem diferenças entre a
221
maneira como uma variável e um ponteiro são guardados na memória, é
possı́vel criar um ponteiro que aponta para o endereço de outro ponteiro.
A linguagem C permite criar ponteiros com diferentes nı́veis
de apontamento, isto é, ponteiros que apontam para outros
ponteiros.
Em linguagem C, a declaração de um ponteiro para ponteiro pelo programador segue a seguinte forma geral:
tipo do ponteiro **nome do ponteiro;
Note que agora usamos dois asteriscos (*) para informar ao compilador
que aquela variável não vai guardar um valor, mas sim um endereço de
memória para outro endereço de memória para aquele tipo especificado.
Para ficar mais claro, veja o exemplo abaixo:
Exemplo: ponteiro para ponteiro.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t x = 10;
5
i n t ∗p = &x ;
6
i n t ∗∗p2 = &p ;
7
/ / Endereço em p2
8
p r i n t f ( ‘ ‘ Endereco em p2 : %
p\n ’ ’ , p2 ) ;
9
/ / Conteudo do endereço
10
p r i n t f ( ‘ ‘ Conteudo em ∗p2 :
%p\n ’ ’ , ∗ p2 ) ;
11
/ / Conteudo do endereço do
endereço
12
p r i n t f ( ‘ ‘ Conteudo em ∗∗p2 :
%d\n ’ ’ ,∗∗ p2 ) ;
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
No exemplo acima, foi declarado um ponteiro que aponta para outro ponteiro (p2). Nesse caso, esse ponteiro guarda o endereço de um segundo
ponteiro (linha 8, endereço de p), que por sua vez guarda o endereço de
uma variável. Assim, se tentarmos acessar o conteúdo do ponteiro (*p2),
222
iremos acessar o endereço guardado dentro do ponteiro (p), que nada
mais é do que o endereço da variável x (linha 10). Como o p2 é um ponteiro para ponteiro, isso significa que podemos acessar o seu conteúdo
duas vezes. Afinal, seu conteúdo (*p2) é um endereço. Assim, o comando
**p2 acessa o conteúdo do endereço do endereço apontado por (p2), isto
é, a variável x (linha 12).
Em um ponteiro para ponteiro, o primeiro ponteiro contém
o endereço do segundo ponteiro que aponta para uma
variável com o valor desejado.
A linguagem C permite ainda criar um ponteiro que aponte para outro ponteiro, que aponte para outro ponteiro, etc, criando assim diferentes nı́veis
de apontamento ou endereçamento. Com isso, podemos criar um ponteiro
para ponteiro, ou, um ponteiro para ponteiro para ponteiro, e assim por
diante.
É a quantidade de asteriscos (*) na declaração do ponteiro
que indica o número de nı́veis de apontamento do ponteiro.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
/ / v a r i á v e l i n t e i r a
5
int x ;
6
/ / p o n t e i r o para um i n t e i r o ( 1 n ı́ v e l )
7
i n t ∗p1 ;
8
/ / p o n t e i r o para p o n t e i r o de i n t e i r o ( 2 n ı́ v e i s )
9
i n t ∗∗p2 ;
10
/ / p o n t e i r o para p o n t e i r o para p o n t e i r o de
i n t e i r o ( 3 n ı́ v e i s )
11
i n t ∗∗∗p3 ;
12
system ( ‘ ‘ pause ’ ’ ) ;
13
return 0;
14 }
Consequentemente, devemos respeitar a quantidade de asteriscos (*) utilizados na declaração do ponteiro para acessar corretamente o seu conteúdo,
como mostra o exemplo abaixo:
223
Acessando o conteúdo de um ponteiro para ponteiro.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
char l e t r a = ’ a ’ ;
5
char ∗ p t r C h a r = & l e t r a ;
6
char ∗∗ p t r P t r C h a r = &
ptrChar ;
7
char ∗∗∗ p t r P t r = &
ptrPtrChar ;
8
p r i n t f ( ‘ ‘ Conteudo em ∗
p t r C h a r : %c\n ’ ’ , ∗
ptrChar ) ;
9
p r i n t f ( ‘ ‘ Conteudo em ∗∗
p t r P t r C h a r : %c\n ’ ’ ,∗∗
ptrPtrChar ) ;
10
p r i n t f ( ‘ ‘ Conteudo em ∗∗∗
p t r P t r : %c\n ’ ’ ,∗∗∗
ptrPtr ) ;
11
system ( ‘ ‘ pause ’ ’ ) ;
12
return 0;
13 }
A linguagem C permite que se crie um ponteiro com um número infinito
de nı́veis de apontamento. Porém, na prática, deve-se evitar trabalhar com
muitos nı́veis de apontamento. Isso ocorre por que cada nova nı́vel de
apontamento adicionada aumenta a complexidade em lidar com aquele
ponteiro e, consequentemente, dificulta a compreensão dos programas,
causando assim confusão e facilitando o surgimento de erros.
224
10
ALOCAÇÃO DINÂMICA
Sempre que escrevemos um programa, é preciso reservar espaço para os
dados que serão processados. Para isso usamos as variáveis.
Uma variável é uma posição de memória previamente reservada e que pode ser usada para armazenar algum
dado.
Uma variável é uma posição de memória que armazena um dado que pode
ser usado pelo programa. No entanto, por ser uma posição previamente
reservada, uma variável deve ser declarada durante o desenvolvimento do
programa.
Toda variável deve ser declarada antes de ser usada.
Infelizmente, nem sempre é possı́vel saber o quanto de memória um programa irá precisar.
Imagine o seguinte problema: precisamos construir um
programa que processe os valores dos salários dos funcionários de uma pequena empresa.
Uma solução simples para resolver esse problema poderia ser declarar um
array do tipo float bem grande com, por exemplo, umas 1.000 posições:
float salarios[1000];
Esse array parece uma solução possı́vel para o problema. Infelizmente,
essa solução possui dois problemas:
• Se a empresa tiver menos de 1.000 funcionários: esse array será um
exemplo de desperdı́cio de memória. Um array de 1.000 posições é
declarado quando não se sabe, de fato, se as 1.000 posiçoes serão
necessárias;
• Se a empresa tiver mais de 1.000 funcionários: esse array será insuficiente para lidar com s dados de todos os funcionários. O programa
não atende as necessidades da empresa.
225
Na declaração de uma array, é dito para reservar uma certa quantidade
de memória para armazenar os elementos do array. Porém, neste modo
de declaração, a quantidade de memória reservada deve ser fixa. Surge
então a necessidade de se utilizar ponteiros juntos com arrays.
Um ponteiro é uma variável que guarda o endereço de um
dado na memória.
Além disso, é importante lembrar que arrays são agrupamentos sequenciais de dados de um mesmo tipo na memória.
O nome do array é apenas um ponteiro que aponta para
o primeiro elemento do array.
A linguagem C permite alocar (reservar) dinamicamente (em tempo de
execução) blocos de memórias utilizando ponteiros. A esse processo dáse o nome de alocação dinâmica. A alocação dinâmica permite ao programador “criar” arrays em tempo de execução, ou seja, alocar memória
para novos arrays quando o programa está sendo executado, e não apenas quando se está escrevendo o programa. Ela é utilizada quando não se
sabe ao certo quanto de memória será necessário para armazenar os dados com que se quer trabalhar. Desse modo, pode-se definir o tamanho do
array em tempo de execução, evitando assim o desperdı́cio de memória.
A alocação dinâmica consiste em requisitar um espaço
de memória ao computador, em tempo de execução, o
qual devolve para o programa o endereço do inı́cio desse
espaço alocado usando um ponteiro.
226
10.1
FUNÇÕES PARA ALOCAÇÃO DE MEMÓRIA
A linguagem C ANSI usa apenas 4 funções para o sistema de alocação
dinâmica, disponı́veis na biblioteca stdlib.h. São elas:
• malloc
• calloc
• realloc
• free
Além dessas funções, existe também a função sizeof que auxilia as demais funções no processo de alocação de memória. A seguir, serão apresentados os detalhes necessários para um programador usar alocação
dinâmica em seu programa.
10.1.1
SIZEOF()
No momento da alocação da memória, deve-se levar em conta o tamanho
do dado alocado.
Alocar memória para um elemento do tipo int é diferente
de alocar memória para um elemento do tipo float.
Isso ocorre pois tipos diferentes podem ter tamanhos diferentes na memória.
O tipo float, por exemplo, ocupa mais espaço na memória que o tipo int.
A função sizeof() é usada para saber o número de bytes
necessários para alocar um único elemento de um determinado tipo de dado.
A função sizeof() é usada para se saber o tamanho em bytes de variáveis
ou de tipos. Ela pode ser usada de duas formas:
sizeof nome da variável
sizeof (nome do tipo)
227
O exemplo abaixo ilustra as duas formas de uso da função sizeof.
Exemplo: uso da função sizeof
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
10.1.2
# include <s t d i o . h>
# include < s t d l i b . h>
s t r u c t ponto {
int x , y ;
};
i n t main ( ) {
p r i n t f ( ‘ ‘ Tamanho char : %d\n ’ ’ , s i z e o f ( char ) ) ;
p r i n t f ( ‘ ‘ Tamanho i n t : %d\n ’ ’ , s i z e o f ( i n t ) ) ;
p r i n t f ( ‘ ‘ Tamanho f l o a t : %d\n ’ ’ , s i z e o f ( f l o a t ) ) ;
p r i n t f ( ‘ ‘ Tamanho double : %d\n ’ ’ , s i z e o f ( double ) ) ;
p r i n t f ( ‘ ‘ Tamanho s t r u c t ponto : %d\n ’ ’ , s i z e o f ( s t r u c t
ponto ) ) ;
int x ;
double y ;
p r i n t f ( ‘ ‘ Tamanho da v a r i a v e l x : %d\n ’ ’ , s i z e o f x ) ;
p r i n t f ( ‘ ‘ Tamanho da v a r i a v e l y : %d\n ’ ’ , s i z e o f y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
MALLOC()
A função malloc() serve para alocar memória durante a execução do programa. É ela quem faz o pedido de memória ao computador e retorna
um ponteiro com o endereço do inı́cio do espaço de memória alocado. A
função malloc() possui o seguinte protótipo:
void *malloc (unsigned int num);
A função malloc() recebe 1 parâmetros de entrada
• num: o tamanho do espaço de memória a ser alocado.
e retorna
• NULL: no caso de erro;
• O ponteiro para a primeira posição do array alocado.
228
Note que a função malloc() retorna um ponteiro genérico
(void*). Esse ponteiro pode ser atribuı́do a qualquer tipo de
ponteiro via type cast.
Existe uma razão para a função malloc() retornar um ponteiro genérico
(void*): ela não sabe o que iremos fazer com a memória alocada. Veja o
exemplo abaixo:
Exemplo: usando a função malloc()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p ;
p = ( i n t ∗ ) m a l l o c (5∗ s i z e o f ( i n t ) ) ;
int i ;
f o r ( i =0; i <5; i ++) {
p r i n t f ( ‘ ‘ D i g i t e o v a l o r da posicao %d :
s c a n f ( ‘ ‘ % d ’ ’ ,&p [ i ] ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’’,i);
No exemplo acima:
• estamos alocando um array contendo 5 posições de inteiros: 5*sizeof(int);
• a função sizeof(int) retorna 4 (número de bytes do tipo int na memória).
Portanto, são alocados 20 bytes (50 * 4 bytes);
• a função malloc() retornar um ponteiro genérico, o qual é convertido
para o tipo do ponteiro via type cast: (int*);
• o ponteiro p passa a ser tratado como um array: p[i].
229
Se não houver memória suficiente para alocar a memória
requisitada, a função malloc() retorna um ponteiro nulo.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p ;
5
p = ( i n t ∗ ) m a l l o c (5∗ s i z e o f ( i n t ) ) ;
6
i f ( p == NULL ) {
7
p r i n t f ( ‘ ‘ E r r o : Memoria I n s u f i c i e n t e ! \ n ’ ’ ) ;
8
exit (1) ;
9
}
10
int i ;
11
f o r ( i =0; i <5; i ++) {
12
p r i n t f ( ‘ ‘ D i g i t e o v a l o r da posicao %d : ’ ’ , i )
;
13
s c a n f ( ‘ ‘ % d ’ ’ ,&p [ i ] ) ;
14
}
15
system ( ‘ ‘ pause ’ ’ ) ;
16
return 0;
17 }
É importante sempre testar se foi possı́vel fazer a alocação de memória.
A função malloc() retorna um ponteiro NULL para indicar que não há
memória disponı́vel no computador, ou que algum outro erro ocorreu que
impediu a memória de ser alocada.
No momento da alocação da memória, deve-se levar em
conta o tamanho do dado alocado.
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char ∗p ;
/ / a l o c a espaço para 1.000 chars
p = ( char ∗ ) m a l l o c ( 1 0 0 0 ) ;
i n t ∗p ;
/ / a l o c a espaço para 250 i n t e i r o s
p = ( i n t ∗) malloc (1000) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
230
Lembre-se: no momento da alocação da memória deve-se levar em conta
o tamanho do dado alocado. Alocar 1000 bytes de memória equivale a um
número de elementos diferente dependendo do tipo do elemento:
• 1.000 bytes para char: um array de 1.000 posições de caracteres;
• 1.000 bytes para int: um array de 250 posições de inteiros.
10.1.3
CALLOC()
Assim como a função malloc(), a função calloc() também serve para alocar memória durante a execução do programa. É ela quem faz o pedido
de memória ao computador e retorna um ponteiro com o endereço do
inı́cio do espaço de memória alocado. A função malloc() possui o seguinte
protótipo:
void *calloc (unsigned int num, unsigned int size);
A função malloc() recebe 2 parâmetros de entrada
• num: o número de elementos no array a ser alocado;
• size: o tamanho de cada elemento do array.
e retorna
• NULL: no caso de erro;
• O ponteiro para a primeira posição do array alocado.
Basicamente, a função calloc() faz o mesmo que a função malloc(). A
diferença é que agora passamos os valores da quantidade de elementos
alocados e do tipo de dado alocado como parâmetros distintos da função.
231
Exemplo: malloc() versus calloc()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
/ / alocaç ão com m a l l o c
i n t ∗p ;
p = ( i n t ∗ ) m a l l o c (50∗ s i z e o f ( i n t ) ) ;
i f ( p == NULL ) {
p r i n t f ( ‘ ‘ E r r o : Memoria I n s u f i c i e n t e ! \ n ’ ’ ) ;
}
/ / alocaç ão com c a l l o c
i n t ∗p1 ;
p1 = ( i n t ∗ ) c a l l o c ( 5 0 , s i z e o f ( i n t ) ) ;
i f ( p1 == NULL ) {
p r i n t f ( ‘ ‘ E r r o : Memoria I n s u f i c i e n t e ! \ n ’ ’ ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, no exemplo acima, que enquanto a função malloc() multiplica o total
de elementos do array pelo tamanho de cada elemento, a função calloc()
recebe os dois valores como parâmetros distintos.
Existe uma outra diferença a função calloc() e a função
malloc(): ambas servem para alocar memória, mas a
função calloc() inicializa todos os BITS do espaço alocado
com 0.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 i n t main ( ) {
5
int i ;
6
i n t ∗p , ∗p1 ;
7
p = ( i n t ∗ ) m a l l o c (5∗ s i z e o f ( i n t ) ) ;
8
p1 = ( i n t ∗ ) c a l l o c ( 5 , s i z e o f ( i n t ) ) ;
9
p r i n t f ( ‘ ‘ c a l l o c \ t \ t m a l l o c \n ’ ’ ) ;
10
f o r ( i =0; i <5; i ++)
11
p r i n t f ( ‘ ‘ p1[%d ] = %d \ t p[%d ] = %d\n ’ ’ , i , p1 [
i ] , i ,p[ i ]) ;
12
system ( ‘ ‘ pause ’ ’ ) ;
13
return 0;
14 }
232
10.1.4
REALLOC()
A função realloc() serve para alocar memória ou realocar blocos de memória
previamente alocados pelas funções malloc(), calloc() ou realloc(). Essa
função tem o seguinte protótipo:
void *realloc (void *ptr, unsigned int num);
A função realloc() recebe 2 parâmetros de entrada
• Um ponteiro para um bloco de memória previamente alocado;
• num: o tamanho em butes do espaço de memória a ser alocado.
e retorna
• NULL: no caso de erro;
• O ponteiro para a primeira posição do array alocado/realocado.
Basicamente, a função realloc() modifica o tamanho da memória previamente alocada e apontada pelo ponteiro ptr para um novo valor especificado por num, sendo num o tamanho em bytes do bloco de memória
solicitado (igual a função malloc()).
233
O novo valor de memória alocada (num) pode ser maior
ou menor do que o tamanho previamente alocado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int i ;
i n t ∗p = m a l l o c (5∗ s i z e o f ( i n t ) ) ;
f o r ( i = 0 ; i < 5 ; i ++) {
p [ i ] = i +1;
}
f o r ( i = 0 ; i < 5 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
printf ( ‘ ‘\n ’ ’ ) ;
/ / D i m i n u i o tamanho do a r r a y
p = r e a l l o c ( p ,3∗ sizeof ( i n t ) ) ;
f o r ( i = 0 ; i < 3 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
printf ( ‘ ‘\n ’ ’ ) ;
/ / Aumenta o tamanho do a r r a y
p = r e a l l o c ( p ,10∗ s i z e o f ( i n t ) ) ;
f o r ( i = 0 ; i < 1 0 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
A função realloc() retorna um ponteiro (void *) para o novo bloco alocado.
Isso é necessário pois a função realloc() pode precisar mover o bloco antigo para aumentar seu tamanho. Se isso ocorrer, o conteúdo do bloco
antigo é copiado para o novo bloco, e nenhuma informação é perdida.
Se o novo tamanho é maior, o valor do bloco de memória
recém-alocado é indeterminado.
Isso ocorre pois a função realloc() se comporta como a função malloc().
Ela não se preocupa em inicializar o espaço alocado.
234
Se o ponteiro para o bloco de memória previamente alocado for NULL, a função realloc() irá alocar memória da
mesma forma como a função malloc() faz.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p ;
p = ( i n t ∗ ) r e a l l o c ( NULL,50∗ s i z e o f ( i n t ) ) ;
f o r ( i = 0 ; i < 5 ; i ++) {
p [ i ] = i +1;
}
f o r ( i = 0 ; i < 5 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Se não houver memória suficiente para a realocação, um ponteiro nulo é
devolvido e o bloco original é deixado inalterado.
Se o tamanho de memória solicitado (num) for igual a zero,
a memória apontada por *ptr será liberada.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p ;
p = ( i n t ∗ ) m a l l o c (50∗ s i z e o f ( i n t ) ) ;
f o r ( i = 0 ; i < 5 ; i ++) {
p [ i ] = i +1;
}
f o r ( i = 0 ; i < 5 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
/ / l i b e r a a mem ória alocada
p = ( int ∗) r e a l l o c (p , 0 ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, a função realloc() funciona da mesma maneira que a
função free() que veremos na próxima seção.
235
10.1.5
FREE()
Diferente das variáveis declarada durante o desenvolvimento do programa,
as variáveis alocadas dinamicamente não são liberadas automaticamente
pelo programa.
Sempre que alocamos memória de forma dinâmica
(malloc(), calloc() ou realloc()) é necessário liberar essa
memória quando ela não for mais necessária.
Desalocar, ou liberar, a memória previamente alocada faz com que ela se
torne novamente disponı́vel para futuras alocações. Para liberar um bloco
de memória previamente alocado utilizamos a função free() cujo protótipo
é:
void free (void *p);
A função free() recebe apenas um parâmetros de entrada: o ponteiro para
o inı́cio do bloco de memória alocado.
Para liberar a memória alocada, basta passar para o
parâmetro da função free() o ponteiro que aponta para o
inı́cio do bloco de memória alocado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p , i ;
p = ( i n t ∗ ) m a l l o c (50∗ s i z e o f ( i n t ) ) ;
i f ( p == NULL ) {
p r i n t f ( ‘ ‘ E r r o : Memoria I n s u f i c i e n t e ! \ n ’ ’ ) ;
exit (1) ;
}
f o r ( i = 0 ; i < 5 0 ; i ++) {
p [ i ] = i +1;
}
f o r ( i = 0 ; i < 5 0 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
/ / l i b e r a a mem ória alocada
free (p) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
236
Como o programa sabe quantos bytes devem ser liberados? Quando se
aloca a memória, o programa guarda o número de bytes alocados numa
“tabela de alocação” interna.
Apenas libere a memória quando tiver certeza de que ela
não será mais usada. Do contrário, um erro pode acontecer
ou o programa poderá não funcionar como esperado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗p , i ;
p = ( i n t ∗ ) m a l l o c (50∗ s i z e o f ( i n t ) ) ;
i f ( p == NULL ) {
p r i n t f ( ‘ ‘ E r r o : Memoria I n s u f i c i e n t e ! \ n ’ ’ ) ;
exit (1) ;
}
f o r ( i = 0 ; i < 5 0 ; i ++) {
p [ i ] = i +1;
}
/ / l i b e r a a mem ória alocada
free (p) ;
/ / tenta imprimir o array
/ / c u j a mem ória f o i l i b e r a d a
f o r ( i = 0 ; i < 5 0 ; i ++) {
p r i n t f ( ‘ ‘ % d\n ’ ’ , p [ i ] ) ;
}
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima nenhum erro ocorre. Isso por que a função free() apenas libera a memória. O ponteiro p continua com o endereço para onde ela
estava reservada. Sendo assim podemos tentar acessá-la. Como ela não
nos pertence mais (foi liberada) não há garantias do que está guardado lá.
Sempre libere a memória que não for mais utilizar.
Além disso, convém não deixar ponteiros “soltos” (dangling pointers) no
programa. Portanto, depois de chamar a função free(), atribua NULL ao
ponteiro:
free(p);
237
p = NULL;
É conveniente fazer isso pois ponteiros “soltos” podem ser explorado por
hackers para atacar o seu computador.
10.2
ALOCAÇÃO DE ARRAYS MULTIDIMENSIONAIS
Existem várias soluções na linguagem C para se alocar um array com mais
de uma dimensão. A seguir apresentaremos algumas dessas soluções.
10.2.1
SOLUÇÃO 1: USANDO ARRAY UNIDIMENSIONAL
Apesar de terem o comportamento de estruturas com mais de uma dimensão, os dados dos arrays multidimensionais são armazenados linearmente na memória. É o uso dos colchetes que cria a impressão de
estarmos trabalhando com mais de uma dimensão. Por exemplo:
int mat[5][5];
Sendo assim, uma solução trivial é simular um array bidimensional (ou
com mais dimensões) utilizando um único array unidimensional alocado
dinamicamente.
238
Podemos alocar um array de uma única dimensão e tratá-lo
como se fosse uma matriz (2 dimensões).
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p ;
5
i n t i , j , N l i n h a s = 2 , Ncolunas = 2 ;
6
p = ( i n t ∗ ) m a l l o c ( N l i n h a s ∗ Ncolunas ∗ s i z e o f
( int ) ) ;
7
f o r ( i = 0 ; i < N l i n h a s ; i ++) {
8
f o r ( j = 0 ; j < Ncolunas ; j ++)
9
p [ i ∗ Ncolunas + j ] = i + j ;
10
}
11
f o r ( i = 0 ; i < N l i n h a s ; i ++) {
12
f o r ( j = 0 ; j < Ncolunas ; j ++)
13
p r i n t f ( ‘ ‘ % d ’ ’ , p [ i ∗ Ncolunas + j ] ) ;
14
printf ( ‘ ‘\n ’ ’ ) ;
15
}
16
free (p) ;
17
system ( ‘ ‘ pause ’ ’ ) ;
18
return 0;
19 }
O maior inconveniente dessa abordagem é que temos que abandonar a
notação de colchetes para indicar a segunda dimensão da matriz. Como
só possuı́mos uma única dimensão, é preciso calcular o deslocamento no
array para simular a segunda dimensão. Isso é feito somando-se o ı́ndice
da coluna que se quer acessar ao produto do ı́ndice da linha que se quer
acessar pelo número total de colunas da “matriz”: [i * Ncolunas + j].
239
Ao simular uma matriz (2 dimensões) utilizando um array
de uma única dimensão perdemos a notação de colchetes
para indicar a segunda dimensão.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗p ;
5
i n t i , j , N l i n h a s = 2 , Ncolunas = 2 ;
6
p = ( i n t ∗ ) m a l l o c ( N l i n h a s ∗ Ncolunas ∗ s i z e o f
( int ) ) ;
7
f o r ( i = 0 ; i < N l i n h a s ; i ++) {
8
f o r ( j = 0 ; j < Ncolunas ; j ++)
9
p [ i ∗ Ncolunas + j ] = i + j ; / / CORRETO
10
p [ i ] [ j ] = i + j ; / / ERRADO
11
}
12
free (p) ;
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
10.2.2
SOLUÇÃO 2: USANDO PONTEIRO PARA PONTEIRO
Se quisermos alocar um array com mais de uma dimensão e manter a
notação de colchetes para cada dimensão, precisamos utilizar o conceito
de “ponteiro para ponteiro” aprendido anteriormente:
char ***ptrPtr;
A idéia de um ponteiro para ponteiro é similar a anotar o
endereço de um papel que tem o endereço da casa do seu
amigo.
O exemplo abaixo exemplifica como funciona o conceito de “ponteiro para
ponteiro”.
240
Exemplo: ponteiro para ponteiro.
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char l e t r a = ’ a ’ ;
char ∗ p t r C h a r ;
char ∗∗ p t r P t r C h a r ;
char ∗∗∗ p t r P t r ;
ptrChar = & l e t r a ;
p t r P t r C h a r = &p t r C h a r ;
p t r P t r = &p t r P t r C h a r ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Basicamente, para alocar uma matriz (array com 2 dimensões) utiliza-se
um ponteiro com 2 nı́veis.
Em um ponteiro para ponteiro, cada nı́vel do ponteiro permite criar uma nova dimensão no array.
Por exemplo, se quisermos um array com duas dimensões, precisaremos
de um ponteiro com dois nı́veis (**); Se queremos três dimensões, precisaremos de um ponteiro com três niveis (***) e assim por diante.
O exemplo abaixo exemplifica como alocar cada nı́vel de um “ponteiro
para ponteiro” para criar uma matriz (array com duas dimensões).
241
Exemplo: alocando cada nı́vel de um ponteiro para ponteiro.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t ∗∗p ; / / 2 ‘ ‘ ∗ ’ ’ = 2
n ı́ v e i s = 2 dimens ões
5
int i , j , N = 2;
6
p = ( i n t ∗ ∗ ) m a l l o c (N∗ s i z e o f
( int ∗) ) ;
7
f o r ( i = 0 ; i < N; i ++) {
8
p [ i ] = ( i n t ∗ ) m a l l o c (N∗
sizeof ( i n t ) ) ;
9
f o r ( j = 0 ; j < N; j ++)
10
s c a n f ( ‘ ‘ % d ’ ’ ,&p [ i ] [ j ] ) ;
11
}
12
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
No exemplo acima, utilizando um ponteiro com 2 nı́veis (int **p), nós alocamos no primeiro nı́vel do ponteiro um array de ponteiros representando
as linhas da matriz. Essa tarefa é realizada pela primeira chamada da
função malloc(), a qual aloca o array usando o tamanho de um ponteiro
para int:
sizeof(int *)
Em seguida, para cada posição desse array de ponteiros, nós alocamos
um array de inteiros, o qual representa o espaço para as colunas da matriz, as quais irão efetivamente manter os dados. Essa tarefa é realizada
pela segunda chamada da função malloc(), dentro do comando for, a qual
aloca o array usando o tamanho de um int:
sizeof(int)
Note que desse modo é possı́vel manter a notação de colchetes para representar cada uma das dimensões da matriz.
A figura abaixo exemplifica como funciona o processo de alocação de uma
matriz usando o conceito de ponteiro para ponteiro:
242
Preste bastante atenção ao exemplo da figura acima. Note que sempre
que se aloca memória, os dados alocados possuem um nı́vel a menos que
o do ponteiro usado na alocação. Assim, se tivermos um
• ponteiro para inteiro (int *), iremos alocar um array de inteiros (int);
• ponteiro para ponteiro para inteiro (int **), iremos alocar um array de
ponteiros para inteiros (int *);
• ponteiro para ponteiro para ponteiro para inteiro (int ***), iremos alocar um array de inteiros (int **);
Diferente dos arrays de uma dimensão, para liberar da
memória um array com mais de uma dimensão, é preciso liberar a memória alocada em cada uma de suas dimensões, na ordem inversa da que foi alocada.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗∗p ; / / 2 ‘ ‘ ∗ ’ ’ = 2 n ı́ v e i s = 2 dimens ões
int i , j , N = 2;
p = ( i n t ∗ ∗ ) m a l l o c (N∗ s i z e o f ( i n t ∗ ) ) ;
f o r ( i = 0 ; i < N; i ++) {
p [ i ] = ( i n t ∗ ) m a l l o c (N∗ s i z e o f ( i n t ) ) ;
f o r ( j = 0 ; j < N; j ++)
s c a n f ( ‘ ‘ % d ’ ’ ,&p [ i ] [ j ] ) ;
}
f o r ( i = 0 ; i < N; i ++) {
free (p [ i ] ) ;
}
free (p) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
243
Para alocar nossa matriz, utilizamos duas chamadas da funcção malloc():
a primeira chamada faz a alocação das linhas, enquanto a segunda chamada faz a alocação das colunas. Na hora de liberar a matriz, devemos
liberar a memória no sentido inverso da alocação: primero liberamos as
colunas, para depois liberar as linhas da matriz. Essa ordem deve ser respeitada pois, se liberarmos primeiro as linhas, perdemos os ponteiros para
onde estão alocadas as colunas e assim não poderemos liberá-las.
Esse tipo de alocação, usando ponteiro para ponteiro , permite criar matrizes que não sejam quadradas ou retangulares.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗∗p ; / / 2 ‘ ‘ ∗ ’ ’ = 2 n ı́ v e i s = 2 dimens ões
int i , j , N = 3;
p = ( i n t ∗ ∗ ) m a l l o c (N∗ s i z e o f ( i n t ∗ ) ) ;
f o r ( i = 0 ; i < N; i ++) {
p [ i ] = ( i n t ∗ ) m a l l o c ( ( i +1) ∗ s i z e o f ( i n t ) ) ;
f o r ( j = 0 ; j < ( i +1) ; j ++)
s c a n f ( ‘ ‘ % d ’ ’ ,&p [ i ] [ j ] ) ;
}
f o r ( i = 0 ; i < N; i ++) {
free (p [ i ] ) ;
}
free (p) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, no exemplo acima, que a segunda chamada da função malloc() está
condicionada ao valor de i: malloc((i+1)*sizeof(int)). Assim, as colunas de
cada linha da matriz terão um número diferente de elementos. De fato, o
código acima cria uma matriz triangular inferior, como fica claro pela figura
abaixo:
10.2.3
SOLUÇÃO 3: PONTEIRO PARA PONTEIRO PARA ARRAY
A terceira solução possı́vel para alocar um array com mais de uma dimensão e manter a notação de colchetes para cada dimensão é um misto
das duas soluções anteriores: simulamos um array bidimensional (ou com
mais dimensões) utilizando:
244
• um array unidimensional alocado dinamicamente e contendo as posições
de todos os elementos;
• um array de ponteiros unidimensional que irá simular as dimensões
e assim manter a notação de colchetes.
O exemplo abaixo exemplifica como simular uma matriz utilizando um array
de ponteiros e um array unidimensional contendo os dados:
245
Exemplo: ponteiro para ponteiro e um array unidimensional
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t ∗v ; / / 1 ‘ ‘ ∗ ’ ’ = 1 n ı́ v e l = 1 dimens ão
i n t ∗∗p ; / / 2 ‘ ‘ ∗ ’ ’ = 2 n ı́ v e i s = 2 dimens ões
i n t i , j , N l i n h a s = 2 , Ncolunas = 2 ;
v = ( i n t ∗ ) m a l l o c ( N l i n h a s ∗ Ncolunas ∗ s i z e o f ( i n t ) ) ;
p = ( i n t ∗∗) malloc ( Nlinhas ∗ sizeof ( i n t ∗) ) ;
f o r ( i = 0 ; i < N l i n h a s ; i ++) {
p [ i ] = v + i ∗ Ncolunas ;
f o r ( j = 0 ; j < Ncolunas ; j ++)
s c a n f ( ‘ ‘ % d ’ ’ ,&p [ i ] [ j ] ) ;
}
f o r ( i = 0 ; i < N l i n h a s ; i ++) {
f o r ( j = 0 ; j < Ncolunas ; j ++)
p r i n t f ( ‘ ‘%d ’ ’ ,p [ i ] [ j ] ) ;
printf ( ‘ ‘\n ’ ’ ) ;
}
free ( v ) ;
free (p) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, utilizando um ponteiro com 1 nı́vel (int *v), nós alocamos o toal de elementos da matriz (Nlinhas * Ncolunas). Essa tarefa é
realizada pela primeira chamada da função malloc(), a qual aloca o array
usando o tamanho de um tipo int:
sizeof(int)
Em seguida, utilizando um ponteiro com 2 nı́veis (int **p), nós alocamos
no primeiro nı́vel do ponteiro um array de ponteiros representando as
linhas da matriz. Essa tarefa é realizada pela primeira chamada da função
malloc(), a qual aloca o array usando o tamanho de um ponteiro para int:
sizeof(int *)
For fim, utilizando de aritmética de ponteiros, nós associamos cada posição
do array de ponteiros para uma porção do array de inteiros:
246
p[i] = v + i * Ncolunas;
Note que, como tempos cada posição do array p associada
a um porção de outro array (v), a notação de colchetes para
mais de uma dimnesão é mantida.
A figura abaixo exemplifica como funciona o processo de alocação de uma
matriz usando o conceito de ponteiro para ponteiro e array unidimensional:
Do ponto de vista de alocação, essa solução é mais simples do que a
anterior (Soluão 2). Ela utiliza apenas duas chamadas da função malloc()
para alocar toda a matriz. Consequentemente, apenas duas chamadas da
função free() são necessárias para liberar a memória alocada.
Por outro lado, para arrays com mais de duas dimensões, essa solução
pode se mostrar mais complicada de se trabalhar já que envolve aritmética
de ponteiros no cálculo que associa as linhas com o array contendo os
dados.
247
11
ARQUIVOS
Um arquivo, de modo abstrato, nada mais é do que uma coleção de bytes
armazenados em um dispositivo de armazenamento secundário, que é geralmente um disco rı́gido, CD, DVD, etc. Essa coleção de bytes pode ser
interpretada das mais variadas maneiras:
• caracteres, palavras, ou frases um documento de texto;
• campos e registos de uma tabela de banco de dados;
• pixels de uma imagem;
• etc.
O que define significado de um arquivo em particular é a maneira como
as estruturas de dados estão organizadas e as operações usadas por um
programa de processar (ler ou escrever) esse arquivo.
As vantagens de se usar arquivos são muitas:
• É geralmente baseado em algum tipo de armazenamento durável.
Ou seja, seus dados permanecem disponı́veis para uso dos programas mesmo que o programa que o gerou já tenha sido encerrado;
• Permitem armazenar uma grande quantidade de informação;
• O acesso aos dados pode ser ou não seqüencial;
• Acesso concorrente aos dados (ou seja, mais de um programa pode
utilizá-lo ao mesmo tempo).
A linguagem C permite manipular arquivos das mais diversas formas. Ela
possui um conjunto de funções que podem ser utilizadas pelo programador
para criar e escrever em novos arquivos, ler o seu conteúdo, independente
do tipo de dados que lá estejam armazenados. A seguir, serão apresentados os detalhes necessários para um programador poder rabalhar com
arquivos em seu programa.
11.1
TIPOS DE ARQUIVOS
Basicamente, a linguagem C trabalha com apenas dois tipos de arquivos:
arquivos texto e arquivos binários.
248
Um arquivo texto armazena caracteres que podem ser
mostrados diretamente na tela ou modificados por um editor de textos simples como o Bloco de Notas.
Os dados gravados em um arquivo texto são gravados exatamente como
seriam impressos na tela. Por isso eles podem ser modificados por um
editor de textos simples como o Bloco de Notas. No entanto, para que
isso ocorra, os dados são gravados como caracteres de 8 bits utilizando a
tabela ASCII. Ou seja, durante a gravação dos dados existe uma etapa de
“conversão” dos dados.
Essa “conversão” dos dados faz com que os arquivos texto
sejam maiores. Além disso, suas operações de escrita e
leitura consomem mais tempo em comparação as dos arquivos binários.
Para entender essa conversão dos dados em arquivos texto, imagine um
número inteiro com 8 dı́gitos: 12345678. Esse número ocupa 32 bits na
memória. Porém, quando for gravado em um arquivo texto, cada dı́gito
dela será convertdo para seu caractere ASCII, ou seja, 8 bits por dı́gito.
Como resultado final, esse número ocupará 64 bits no arquivo, o dobro do
seu tamanho na memória.
Dependendo do ambiente onde o aplicativo é executado,
algumas conversões de caracteres especiais podem ocorrer na escrita/leitura de dados em arquivos texto.
Isso ocorre como uma forma de adaptar o arquivo ao formato de arquivo
texto especı́fico do sistema. No modo de arquivo texto, um caractere de
nova linha, “\n”, pode ir a ser convertido pelo sistema para para o par de
caracteres retorno de carro + nova linha, “\r \n”.
Um arquivo binário armazena uma seqüência de bits que
está sujeita as convenções dos programas que o gerou.
Os dados gravados em um arquivo binário são gravados exatamente como
estão organizados na memória do computador. Isso significa que não
existe uma etapa de “conversão” dos dados. Portanto, suas operações
de escrita e leitura são mais rápidas do que as realizadas em arquivos
texto.
249
Voltemos ao nosso número inteiro com 8 dı́gitos: 12345678. Esse número
ocupa 32 bits na memória. Quando for gravado em um arquivo binário, o
conteúdo da memória será copiado diretamente para o arquivo, sem conversão. Como resultado final, esse número ocupará os mesmo 32 bits no
arquivo.
São exemplos de arquivos binários os arquivos executáveis, arquivos compactados, arquivos de registros, etc.
Para entender melhor a diferença entre esse esses dos arquivos, imagine
os seguintes dados a serem gravados:
char nome[20] = “Ricardo”;
int i = 30;
float a = 1.74;
A figura abaixo mostra como seria o resultado da gravação dees em um
arquivo texto e em um arquivo binário. Note que os dados de um arquivo
texto podem ser facilmente modificados por um editor de textos.
Caracteres são legı́veis tanto em arquivos textos quanto
binários.
11.2
SOBRE ESCRITA E LEITURA EM ARQUIVOS
Quanto as operações de escrita e leitura em arquivos, a linguagem C possui uma série de funções prontas para a manipulação de arquivos, cujos
protótipos estão reunidos na biblioteca padrão de estrada e saı́da, stdio.h.
250
Diferente de outras linguagens, a linguagem C não possui
funções que automaticamente leiam todas as informações
de um arquivo.
Na linguagem C, as funções de escrita e leitura em arquivos se limitam a
operações de abrir/fechar e ler/escrever caracteres e bytes. Fica a cargo
do programador criar a função que irá ler ou escrever um arquivo de uma
maneira especifı́ca.
11.3
PONTEIRO PARA ARQUIVO
A linguagem C usa um tipo especial de ponteiro para manipular arquivos.
Quando o arquivo é aberto, esse ponteiro aponta para o registro 0 (o primeiro registro no arquivo). É esse ponteiro que controla qual o próximo
byte a ser acessado por um comando de leitura. É ele também que indica
quando chegamos ao final de um arquivo, entre outras tarefas.
Todas as funções de manipulação de arquivos trabalham
com o conceito de “ponteiro de arquivo”.
Podemos declarar um ponteiro de arquivo da seguinte maneira:
FILE *p;
Nesse caso, p é o ponteiro que nos permitirá manipular arquivos na linguagem C. Um ponteiro de arquivo nada mais é do que um ponteiro para uma
área na memória chamada de “buffer”. Nela se encontram vários dados
sobre o arquivo aberto, tais como o nome do arquivo e posição atual.
11.4
ABRINDO E FECHANDO UM ARQUIVO
11.4.1
ABRINDO UM ARQUIVO
A primeira coisa que devemos fazer ao se trabalhar com arquivos é abrı́-lo.
Para abrir um arquivo usa-se a função fopen(), cujo protótipo é:
FILE *fopen(char *nome do arquivo,char *modo)
251
A função fopen() recebe 2 parâmetros de entrada
• nome do arquivo: uma string contendo o nome do arquivo que deverá ser aberto;
• modo: uma string contendo o modo de abertura do arquivo.
e retorna
• NULL: no caso de erro;
• O ponteiro para o arquivo aberto.
CAMINHO ABSOLUTO E RELATIVO PARA O ARQUIVO
No parâmetro nome do arquivo pode-se trabalhar com
caminhos absolutos ou relativos.
Imagine que o arquivo com que desejamos trabalhar esteja no seguinte
local:
“C:\Projetos\NovoProjeto\arquivo.txt”
O caminho absoluto de um arquivo é uma seqüência de diretórios separados pelo caractere barra (‘\’), que se inicia no diretório raiz e termina
com o nome do arquivo. Nesse caso, o caminho absoluto do arquivo é a
string
“C:\Projetos\NovoProjeto\arquivo.txt”
Já o caminho relativo, como o próprio nome diz, é relativo ao local onde o
programa se encontra. Nesse caso, o sistema inicia a pesquisa pelo nome
do arquivo a partir do diretório do programa. Se tanto o programa quanto
o arquivo estiverem no mesmo local, o caminho relativo até esse arquivo
será
“.\arquivo.txt”
ou
“arquivo.txt”
252
Se o programa estivesse no diretório “C:\Projetos”, o caminho relativo
até o arquivo seria
“.\NovoProjeto\arquivo.txt”
Ao se trabalhar com caminhos absolutos ou relativos,
sempre usar duas barras ‘\\’ ao invés de uma ‘\’ para separar os diretóris.
Isso é necessário para evitar que alguma combinação de caractere e barra
seja confundida com uma seqüências de escape que não seja a barra
invertida. As duas barras ‘\\’ são a seqüências de escape da própria barra
invertida. Assim, o caminho absoluto do arquivo anteriormente definido
passa a ser
“C:\\Projetos\\NovoProjeto\\arquivo.txt”
COMO POSSO ABRIR MEU ARQUIVO
O modo de abertura do arquivo determina que tipo de uso
será feito do arquivo.
O modo de abertura do arquivo diz à função fopen() qual é o que tipo de
uso que será feito do arquivo. Pode-se, por exemplo, querer escrever em
um arquivo binário, ou ler um arquivo texto. A tabela a seguir mostra os
modos válidos de abertura de um arquivo:
253
Modo
“r”
“w”
“a”
“rb”
“wb”
“ab”
“r+”
“w+”
“a+”
“r+b”
“w+b”
“a+b”
Arquivo Função
Texto
Leitura. Arquivo deve existir.
Escrita. Cria arquivo se não houver. Apaga o anTexto
terior se ele existir.
Escrita. Os dados serão adicionados no fim do
Texto
arquivo (“append”).
Binário Leitura. Arquivo deve existir.
Escrita. Cria arquivo se não houver. Apaga o anBinário
terior se ele existir.
Escrita. Os dados serão adicionados no fim do
Binário
arquivo (“append”).
Leitura/Escrita. O arquivo deve existir e pode ser
Texto
modificado.
Leitura/Escrita. Cria arquivo se não houver.
Texto
Apaga o anterior se ele existir.
Leitura/Escrita. Os dados serão adicionados no
Texto
fim do arquivo (“append”).
Leitura/Escrita. O arquivo deve existir e pode ser
Binário
modificado.
Leitura/Escrita. Cria arquivo se não houver.
Binário
Apaga o anterior se ele existir.
Leitura/Escrita. Os dados serão adicionados no
Binário
fim do arquivo (“append”).
Note que para cada tipo de ação que o programador deseja realizar existe
um modo de abertura de arquivo mais apropriado.
O arquivo deve sempre ser aberto em um modo que permita executar as operações desejadas.
Imagine que desejemos gravar uma informação em um arquivo texto. Obviamente, esse arquivo deve ser aberto em um modo que permita escrever
nele. Já um arquivo aberto para leitura não irá permitir outra operação que
não seja a leitura de dados.
254
Exemplo: abrir um arquivo binário para escrita
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ f p ;
f p = fopen ( ‘ ‘ exemplo . b i n ’ ’ , ‘ ‘ wb ’ ’ ) ;
i f ( f p == NULL )
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o . \ n ’ ’ ) ;
fclose ( fp ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, o comando fopen() tenta abrir um arquivo de nome
“exemplo.bin” no modo de escrita para arquivos binários, “wb”. Note que
foi utilizado o caminho relativo do arquivo. Na sequência, a condição if
(fp == NULL) testa se o arquivo foi aberto com sucesso. Isso é
FINALIZANDO O PROGRAMA NO CASO DE ERRO
No caso de um erro, a função fopen() retorna um ponteiro
nulo (NULL).
Caso o arquivo não tenha sido aberto com sucesso, provavelmente o programa não poderá continuar a executar. Nesse caso, utilizamos a função
exit(), presente na biblioteca stdlib.h, para abortar o programa. Seu protótipo
é:
void exit (int codigo de retorno)
A função exit() pode ser chamada de qualquer ponto do programa. Ela faz
com que o programa termine e retorne, para o sistema operacional, o valor
definido em codigo de retorno.
255
A convenção mais usada é que um programa retorne zero
no caso de um término normal e retorne um número não
nulo no caso de ter ocorrido um problema durante a sua
execução.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
FILE ∗ f p ;
5
f p = fopen ( ‘ ‘ exemplo . b i n ’ ’ , ‘ ‘ wb ’ ’ ) ;
6
i f ( f p == NULL ) {
7
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o . Fim
de programa . \ n ’ ’ ) ;
8
system ( ‘ ‘ pause ’ ’ ) ;
9
exit (1) ;
10
}
11
fclose ( fp ) ;
12
system ( ‘ ‘ pause ’ ’ ) ;
13
return 0;
14 }
11.4.2
FECHANDO UM ARQUIVO
Sempre que terminamos de usar um arquivo, devemos fechá-lo. Para realizar essa tarefa, usa-se a função fclose(), cujo protótipo é:
int fclose (FILE *fp)
Basicamente, a função fclose() recebe como parâmetro o ponteiro fp que
determina o arquivo a ser fechado. Como resultado, a função retorna um
valor inteiro igual a zero no caso de sucesso no fechamento do arquivo.
Um valor de retorno diferente de zero significa que houve um erro nessa
tarefa.
Por que devemos fechar o arquivo?
Ao fechar um arquivo, todo caractere que tenha permanecido no “buffer”
é gravado. O “buffer” é uma área intermediária entre o arquivo no disco e
o programa em execução. Trata-se de uma região de memória que armazena temporariamente os caracteres a serem gravados em disco. Apenas
quando o “buffer” está cheio é que seu conteúdo é escrito no disco.
256
Por que utilizar um “buffer” durante a escrita em um arquivo?
O uso de um “buffer” é uma questão de eficiência. Para ler e escrever
arquivos no disco rı́gido é preciso posicionar a cabeça de gravação em um
ponto especı́fico do disco rı́gido. E isso consome tempo. Se tivéssemos
que fazer isso para cada caractere lido ou escrito, as operações de leitura
e escrita de um arquivo seriam extremamente lentas. Assim a gravação
só é realizada quando há um volume razoável de informações a serem
gravadas ou quando o arquivo for fechado.
A função exit() fecha todos os arquivos que um programa
tiver aberto.
11.5
ESCRITA E LEITURA EM ARQUIVOS
Uma vez aberto um arquivo, pode-se ler ou escrever nele. Para realizar
essas tarefas, a linguagem C conta com uma série de funções de escrita
e leitura que variam de funcionalidade de acordo com o tipo de dado que
se deseja manipular. Desse modo, todas e as mais diversas aplicações do
programador podem ser atendidas.
11.5.1
ESCRITA E LEITURA DE CARACTERE
ESCREVENDO UM CARACTERE
As funções mais básicas e fáceis de se trabalhar em um arquivo são as
responsáveis pela escrita e leitura de um único caractere. Para se escrever
um caractere em um arquivo usamos a função fputc(), cujo protótipo é:
int fputc(char c,FILE *fp);
A função fputc() recebe 2 parâmetros de entrada
• c: o caractere a ser escrito no arquivo. Note que o caractere é passado como seu valor inteiro;
• fp: a variável que está associada ao arquivo onde o caractere será
escrito.
257
e retorna
• a constante EOF (em geral, -1), se houver erro na escrita;
• o prório caractere, se ele foi escrito com sucesso.
Cada chamada da função fputc() grava um único caractere
c no arquivo especificado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# include <s t d i o . h>
# include < s t d l i b . h>
# include <s t r i n g . h>
i n t main ( ) {
FILE ∗ arq ;
char s t r i n g [ 1 0 0 ] ;
int i ;
arq = fopen ( ‘ ‘ a r q u i v o . t x t ’ ’ , ‘ ‘w ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
p r i n t f ( ‘ ‘ E n t r e com a s t r i n g a s e r gravada no
arquivo : ’ ’ ) ;
gets ( s t r i n g ) ;
/ / Grava a s t r i n g , c a r a c t e r e a c a r a c t e r e
f o r ( i = 0 ; i < s t r l e n ( s t r i n g ) ; i ++)
f p u t c ( s t r i n g [ i ] , arq ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, a função fputc() é utilizada para escrever um caractere na posição atual do arquivo, como indicado pelo indicador de posição
interna do arquivo. Em seguida, esse indicador de posição interna é avançado
em um caractere de modo a ficar pronto para a escrita do próximo caractere.
A função fputc() também pode ser utilizada para escrever
um caractere no dispositivo de saı́da padrão (geralmente a
tela do monitor).
Para usar a função fputc() para escrever na tela, basta modificar o arquivo no qual se deseja escrever para a constante stdout. Essa constante
258
trata-se de um dos arquivos pré-definidos do sistema, um ponteiro para o
dispositivo de saı́da padrão (geralmente o vı́deo) das aplicações. Assim, o
comando
fputc(’*’, stdout);
escreve um “*” na tela do monitor (dispositivo de saı́da padrão) ao invés de
em um arquivo no disco rı́gido.
LENDO UM CARACTERE
Da mesma maneira que é possı́vel gravar um único caractere em um arquivo, também é possı́vel fazer a sua leitura. A função que correspondente
a leitura de caracteres é a função fgetc(), cujo protótipo é:
int fgetc(FILE *fp);
A função fgetc() recebe como parâmetro de entrada apenas a variável
que está associada ao arquivo de onde o caractere será lido. Essa função
retorna
• a constante EOF (em geral, -1), se houver erro na leitura;
• o caractere lido do arquivo, na forma de seu valor inteiro, se o mesmo
foi lido com sucesso.
259
Cada chamada da função fgetc() lê um único caractere do
arquivo especificado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
char c ;
arq = fopen ( ‘ ‘ a r q u i v o . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
int i ;
f o r ( i = 0 ; i < 5 ; i ++) {
c = f g e t c ( arq ) ;
p r i n t f ( ‘ ‘% c ’ ’ , c ) ;
}
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, a função fgetc() é utilizada para ler 5 caracteres de
um arquivo. Note que a função fgetc() sempre retorna o caractere atualmente apontado pelo indicador de posição interna do arquivo especificado.
A cada operação de leitura, o indicador de posição interna
do arquivo é avançado em um caractere para apontar para
o próximo caractere a ser lido.
Similar ao que acontece com a função fputc(), a função fgetc() também
pode ser utilizada para a leitura de caracteres do teclado. Para tanto,
basta modificar o arquivo do qual se deseja ler para a constante stdin.
Essa constante trata-se de um dos arquivos pré-definidos do sistema, um
ponteiro para o dispositivo de entrada padrão (geralmente o teclado) das
aplicações. Assim, o comando
char c = fgetc(stdin);
lê um caractere do teclado (dispositivo de entrada padrão) ao invés de um
arquivo no disco rı́gido.
260
O que acontece quando a função fgetc() tenta ler um caractere de um arquivo que já acabou?
Neste caso, precisamos que a função retorne algo indicando que o arquivo
acabou. Porém, todos os 256 caracteres da tabela ASCII são “válidos” em
um arquivo. Para evitar esse tipo de situação, a função fgetc() não devolve
um valor do tipo char, mas do tipo int. O conjunto de valores do tipo
char está contido dentro do conjunto de valores do tipo int. Se o arquivo
tiver acabado, a função fgetc() devolve um valor inteiro que não possa ser
confundido com um valor do tipo char.
Quando atinge o final de um arquivo, a função fgetc() devolve a constante EOF (End Of File), que está definida
na biblioteca stdio.h. Em muitos computadores o valor de
EOF é definido como -1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
char c ;
arq = fopen ( ‘ ‘ a r q u i v o . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
while ( ( c = f g e t c ( arq ) ) ! = EOF)
p r i n t f ( ‘ ‘% c ’ ’ , c ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, a função fgetc() é utilizada juntamente com a constante EOF para ler não apenas alguns caracteres, mas para continuar
lendo caracteres enquanto não chegarmos ao final do arquivo.
11.5.2
FIM DO ARQUIVO
Como visto anteriormente, a constante EOF (“End of file”) indica o fim de
um arquivo. Porém, quando manipulando dados binários, um valor inteiro
261
igual ao valor da constante EOF pode ser lido. Nesse caso, se utilizarmos
a constante EOF para verificar se chegamos ao final do arquivo, vamos
receber a confirmação de ter chegado ao final do arquivo, quando na verdade ainda não chegamos ao seu final. Para evitar este tipo de situação,
a linguagem C inclui a função feof() que determina quando o final de um
arquivo foi atingido. Seu protótipo é:
int feof(FILE *fp)
Basicamente, a função feof() recebe como parâmetro o ponteiro fp que
determina o arquivo a ser verificado. Como resultado, a função retorna um
valor inteiro igual a ZERO se ainda não tiver atingido o final do arquivo.
Um valor de retorno diferente de zero significa que já foi atingido o final do
arquivo.
Basicamente, a função feof() retorna um valor inteiro diferente de zero se o arquivo chegou ao fim, caso contrário,
retorna ZERO.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
11.5.3
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ f p ;
char c ;
f p = fopen ( ‘ ‘ a r q u i v o . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( ! fp ) {
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
while ( ! f e o f ( f p ) ) {
c = fgetc ( fp ) ;
p r i n t f ( ‘ ‘% c ’ ’ , c ) ;
}
fclose ( fp ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
ARQUIVOS PRÉ-DEFINIDOS
Como visto durante o aprendizado das funções fputc() e fgetc(), os ponteiros stdin e stdout podem ser utilizados para acessar os dispositivos
262
de entrada (geralmente o teclado) e saı́da (geralmente o vı́deo) padrão.
Porém, existem outros ponteiros que podem ser utilizados.
No inı́cio da execução de um programa, o sistema automaticamente abre alguns arquivos pré-definidos, entre eles
stdin e stdout.
stdin
Dispositivo de entrada padrão (geralmente o teclado)
stdout Dispositivo de saı́da padrão (geralmente o vı́deo)
stderr Dispositivo de saı́da de erro padrão (geralmente o vı́deo)
Dispositivo de saı́da auxiliar (em muitos sistemas, associstdaux
ado à porta serial)
Dispositivo de impressão padrão (em muitos sistemas, asstdprn
sociado à porta paralela)
11.5.4
FORÇANDO A ESCRITA DOS DADOS DO “BUFFER”
Vimos anteriormente que os dados gravados em um arquivo são primeiramente gravados em um “buffer”, uma área intermediária entre o arquivo
no disco e o programa em execução, e somente quando este “buffer” está
cheio é que seu conteúdo é escrito no disco. Também vismo que o uso do
“buffer” é uma questão de eficiência. Porém, a linguagem C permite que
nós forcemos a gravação de qualquer dado contido no “buffer” no momento
em que quisermos. Para realizar essa tarefa, usa-se a função fflush(), cujo
protótipo é:
int fflush(FILE *fp)
Basicamente, a função fflush() recebe como parâmetro o ponteiro fp que
determina o arquivo a ser manipulado. Como resultado, a função fflush()
retorna
• o valor 0 (ZERO), se a operação foi realizada com sucesso;
• a constante EOF (em geral, -1), se houver algum erro.
O comportamento da função fflush() depende do modo
como o arquivo foi aberto.
• Se o arquivo apontado por fp foi aberto para escrita, os dados contidos no “buffer de saı́da” são gravados no arquivo;
263
• Se o arquivo apontado por fp foi aberto para leitura, o comportamento depende da implementação da biblioteca. Em algumas implementações
os dados contidos no “buffer de entrada” são apagados, mas esse
não é um comportamento padrão;
• Se fp for um ponteiro nulo (fp = NULL), todos os arquivos abertos
são liberados.
Abaixo, tem-se um exemplo de um programa que utiliza a função fflush()
para forçar a gravação de dados no arquivo:
Exemplo: forçando a gravação de dados em um arquivo
# include <s t d i o . h>
# include < s t d l i b . h>
# include <s t r i n g . h>
i n t main ( ) {
FILE ∗ arq ;
char s t r i n g [ 1 0 0 ] ;
int i ;
arq = fopen ( ‘ ‘ a r q u i v o . t x t ’ ’ , ‘ ‘w ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
p r i n t f ( ‘ ‘ E n t r e com a s t r i n g a s e r gravada no
arquivo : ’ ’ ) ;
15
gets ( s t r i n g ) ;
16
f o r ( i = 0 ; i < s t r l e n ( s t r i n g ) ; i ++)
17
f p u t c ( s t r i n g [ i ] , arq ) ;
18
19
f f l u s h ( arq ) ;
20
f c l o s e ( arq ) ;
21
system ( ‘ ‘ pause ’ ’ ) ;
22
return 0;
23 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
11.5.5
SABENDO A POSIÇÃO ATUAL DENTRO DO ARQUIVO
Outra operação bastante comum é saber onde estamos dentro de um arquivo. Para realizar essa tarefa, usa-se a função ftell(), cujo protótipo é:
long int ftell(FILE *fp)
264
Basicamente, a função ftell() recebe como parâmetro o ponteiro fp que
determina o arquivo a ser manipulado. Como resultado, a função ftell()
retorna a posição atual dentro do fluxo de dados do arquivo:
• para arquivos binário, o valor retornado indica o número de bytes
lidos a partir do inı́cio do arquivo;
• para arquivos texto, não existe garantia de que o valor retornado seja
o número exato de bytes lidos a partir do inı́cio do arquivo;
• se um erro ocorrer, o valor -1 no formato long é retornado.
Abaixo, tem-se um exemplo de um programa que utiliza a função ftell()
para descobrir o tamanho, em bytes, de um arquivo:
Exemplo: descobrindo o tamanho de um arquivo
# include <s t d i o . h>
# include < s t d l i b . h>
# include <s t r i n g . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ a r q u i v o . b i n ’ ’ , ‘ ‘ r b ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o na a b e r t u r a do a r q u i v o ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
i n t tamanho ;
f s e e k ( arq , 0 , SEEK END) ;
tamanho = f t e l l ( arq ) ;
f c l o s e ( arq ) ;
p r i n t f ( ‘ ‘ Tamanho do a r q u i v o em b y t e s : %d : ’ ’ , tamanho
);
17
system ( ‘ ‘ pause ’ ’ ) ;
18
return 0;
19 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
11.5.6
ESCRITA E LEITURA DE STRINGS
Até o momento, apenas caracteres únicos puderam ser escritos em um
arquivo. Porém, existem funções na linguagem C que permitem escrever e
ler uma sequência de caracteres, isto é, uma string, em um arquivo.
ESCREVENDO UMA STRING
265
Para se escrever uma string em um arquivo usamos a função fputs(), cujo
protótipo é:
int fputs (char *str,FILE *fp);
A função fputs() recebe 2 parâmetros de entrada
• str: a string (array de caracteres) a ser escrita no arquivo;
• fp: a variável que está associada ao arquivo onde a string será escrita.
e retorna
• a constante EOF (em geral, -1), se houver erro na escrita;
• um valor diferente de ZERO, se o texto for escrito com sucesso.
Exemplo: escrevendo uma string em um arquivo com fputs()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char s t r [ 2 0 ] = ‘ ‘ H e l l o World ! ’ ’ ;
int result ;
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘w ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ Problemas na CRIACAO do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
r e s u l t = f p u t s ( s t r , arq ) ;
i f ( r e s u l t == EOF)
p r i n t f ( ‘ ‘ E r r o na Gravacao\n ’ ’ ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, o comando fopen() abre um arquivo de nome “ArqGrav.txt” no modo de escrita para arquivos texto, “w”. Na sequência, a
string contida na variável str é escrita no arquivo por meio do comando
fputs(str,arq), sendo o resultado dessa operação devolvido na variável result.
266
A função fputs() não coloca o caracter de nova linha ‘\n’,
nem nenhum outro tipo de caractere, no final da string escrita no arquivo. Essa tarefa pertence ao programador.
Imagine o seguinte conjunto de comandos:
fputs(“Hello”,arq);
fputs(“World”,arq);
O resultado da execução desses dois comandos será a escrita da string
“HelloWorld” no arquivo. Note que nem mesmo um espaço entre elas
foi adicionado. A função fputs() simplesmente escreve no arquivo aquilo
que o programador ordenou, e mais nada. Se o programador quisesse
separá-las com um espaço, deve fazer como abaixo:
fputs(“Hello ”,arq);
fputs(“World”,arq);
Note que agora existe um espaço ao final da string “Hello ”. Portanto,
o resultado no arquivo será a string “Hello World”. O mesmo vale para
qualquer outro caractere, como a quebra de linha ‘\n’.
Como a função fputc(), a função fputs() também pode ser
utilizada para escrever uma string no dispositivo de saı́da
padrão (geralmente a tela do monitor).
1
2
3
4
5
6
7
8
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 3 0 ] = ‘ ‘ H e l l o World \n ’ ’ ;
fputs ( texto , stdout ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
LENDO UMA STRING
Da mesma maneira como é possı́vel gravar uma string em um arquivo,
também é possı́vel fazer a sua leitura. A função utilizada para realizar
essa tarefa é a função fgets(), cujo protótipo é:
267
char *fgets (char *str, int tamanho, FILE *fp);
A função fgets() recebe 3 parâmetros de entrada
• str: a string onde os caracteres lidos serão armazenados;
• tamanho: o limite máximo de caracteres a serem lidos;
• fp: a variável que está associada ao arquivo de onde a string será
lida.
e retorna
• NULL: no caso de erro ou fim do arquivo;
• O ponteiro para o primeiro caractere da string recuperada em str.
Exemplo: lendo uma string de um arquivo com fgets()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char s t r [ 2 0 ] ;
int result ;
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ Problemas na ABERTURA do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
r e s u l t = f g e t s ( s t r , 1 3 , arq ) ;
i f ( r e s u l t == EOF)
p r i n t f ( ‘ ‘ E r r o na l e i t u r a \n ’ ’ ) ;
else
p r i n t f ( ‘ ‘% s ’ ’ , s t r ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, o comando fopen() abre um arquivo de nome “ArqGrav.txt” no modo de leitura para arquivos texto, “r”. Na sequência, uma
string de até 13 caracteres é lida do arquivo e armazenada na variável str
por meio do comando fgets(str,13,arq), sendo o resultado dessa operação
devolvido na variável result.
268
A função fgets() lê uma string do arquivo até que um caractere de nova linha (\n) seja lido ou “tamanho-1” caracteres
tenham sido lidos.
A string resultante de uma operação de leitura usando a função fgets()
sempre terminará com a constante ‘\0’ (por isto somente “tamanho-1” caracteres, no máximo, serão lidos). No caso do de um caractere de nova
linha (\n ou ENTER) ser lido antes de “tamanho-1” caracteres, ele fará
parte da string.
Como a função gets(), a função fgets(), também pode
ser utilizada para ler uma string do dispositivo de entrada
padrão (geralmente o teclado).
1
2
3
4
5
6
7
8
9
10
11.5.7
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char nome [ 3 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e um nome : ’ ’ ) ;
f g e t s ( nome , 30 , s t d i n ) ;
p r i n t f ( ‘ ‘O nome d i g i t a d o f o i : %s ’ ’ ,nome ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
ESCRITA E LEITURA DE BLOCOS DE BYTES
Até esse momento, vimos como é possı́vel escrever e ler em arquivos
caracteres e sequências de caracteres, as strings. Isso significa que foi
possı́vel para nós apenas escrever e ler dados do tipo char em um arquivo.
Felizmente, a linguagem C possui outras funções que permitem escrever
e ler dados mais complexos, como os tipos int, float, double, array, ou
mesmo um tipo definido pelo programador, como, por exemplo, a struct.
São as funções de escrita e leitura de blocos de bytes.
As funções de escrita e leitura de blocos de bytes devem
ser utilizadas preferencialmente com arquivos binários.
As funções de escrita e leitura de blocos de bytes trabalham com blocos
de memória apontados por um ponteiro. Dentro de um bloco de memória,
269
qualquer tipo de dado pode existir: int, float, double, array, struct, etc.
Dai a sua versatilidade. Além disso, como vamos escrever os dados como
estão na memória, isso significa que não existe uma etapa de “conversão”
dos dados. Mesmo que gravassemos esses dados em um arquivo texto,
seus valores seriam ilegiveis. Dai a preferência por arquivos binários.
ESCREVENDO BLOCOS DE BYTES
Iniciemos pela etapa de gravação. Para escrever em um arquivo um blocos
de bytes usa-se a função fwrite(), cujo protótipo é:
int fwrite(void *buffer, int nro de bytes, int count, FILE *fp)
A função fwrite() recebe 4 parâmetros de entrada
• buffer: um ponteiro genérico para a região de memória que contém
os dados que serão gravados no arquivo;
• nro de bytes: tamanho, em bytes, de cada unidade de dado a ser
gravada;
• count: total de unidades de dados que devem ser gravadas.
• fp: o ponteiro para o arquivo que se deseja trabalhar;
Note que temos dois valores inteiros: nro de bytes e
count. Isto significa que o número total de bytes gravados
no arquivo será: nro de bytes * count.
Como resultado, a função fwrite() retorna um valor inteiro que representa o
número total de unidades de dados gravadas com sucesso. Esse número
pode ser menor do que o número de itens esperado (count), indicando
que houve erro parcial de escrita.
270
O valor do retorno da função fwrite() será igual ao valor
de count a menos que ocorra algum erro na gravação dos
dados.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
FILE ∗ arq ;
5
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ wb ’ ’ ) ;
6
i f ( arq == NULL ) {
7
p r i n t f ( ‘ ‘ Problemas na CRIACAO do a r q u i v o
\n ’ ’ ) ;
8
system ( ‘ ‘ pause ’ ’ ) ;
9
exit (1) ;
10
}
11
int total gravado , v [ 5 ] = {1 ,2 ,3 ,4 ,5};
12
/ / grava todo o a r r a y no a r q u i v o ( 5 posiç ões )
13
t o t a l g r a v a d o = f w r i t e ( v , s i z e o f ( i n t ) , 5 , arq ) ;
14
i f ( t o t a l g r a v a d o != 5) {
15
p r i n t f ( ‘ ‘ E r r o na e s c r i t a do a r q u i v o ! ’ ’ ) ;
16
system ( ‘ ‘ pause ’ ’ ) ;
17
exit (1) ;
18
}
19
f c l o s e ( arq ) ;
20
system ( ‘ ‘ pause ’ ’ ) ;
21
return 0;
22 }
Note, que a função sizeof() foi usada aqui para determinar o tamanho,
em bytes, de cada unidade de dado a ser gravada. Trata-se, basicamente
do mesmo princı́pio utilizado na alocação dinâmica, onde alocavamos N
posições de sizeof() bytes de tamanho cada. Nesse caso, como queriamos gravar um array de 5 inteiros, o nro de bytes de cada inteiro é obtido
pela função sizeof(int), e o total de unidades de dados que devem ser
gravadas, count, é igual ao tamanho do array, ou seja, 5.
Abaixo, tem-se um exemplo de um programa que utiliza a função fwrite()
para gravar os mais diversos tipos de dados:
271
Exemplo: usando a função fwrite()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# include <s t d i o . h>
# include < s t d l i b . h>
# include <s t r i n g . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ wb ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ Problemas na CRIACAO do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
char s t r [ 2 0 ] = ‘ ‘ H e l l o World ! ’ ’ ;
float x = 5;
int v [ 5 ] = {1 ,2 ,3 ,4 ,5};
/ / grava a s t r i n g toda no a r q u i v o
f w r i t e ( s t r , s i z e o f ( char ) , s t r l e n ( s t r ) , arq ) ;
/ / grava apenas os 5 p r i m e i r o s c a r a c t e r e s da s t r i n g
f w r i t e ( s t r , s i z e o f ( char ) , 5 , arq ) ;
/ / grava o v a l o r de x no a r q u i v o
f w r i t e (& x , s i z e o f ( f l o a t ) , 1 , arq ) ;
/ / grava todo o a r r a y no a r q u i v o ( 5 posiç ões )
f w r i t e ( v , s i z e o f ( i n t ) , 5 , arq ) ;
/ / grava apenas as 2 p r i m e i r a s posiç ões do a r r a y
f w r i t e ( v , s i z e o f ( i n t ) , 2 , arq ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, nesse exemplo, que não é necessário gravar sempre o array por
inteiro. Podemos gravar parcialmente um array. Para isso, basta modificar
o valor do parâmetro count. As posições do array serão gravadas a partir
da primeira. Então, se o valor de count for igual a 2 (linha 22), a função
fwrite() irá gravar no arquivo apenas as 2 primeiras posições do array.
Note que ao gravar uma variável simples (int, float, double, etc.) e compostas (struct, etc) é preciso passar o
endereço da variável. Para tanto, usa-se o operador & na
frente do nome da variável. No caso de arrays, seu nome
já é o prórpio endereço, não sendo, portanto, necessário o
operador de &.
LENDO BLOCOS DE BYTES
272
Uma vez concluı́da a etapa de gravação de dados com a função fwrite(),
é necessário agora ler eles do arquivo. Para ler de um arquivo um blocos
de bytes usa-se a função fread(), cujo protótipo é:
int fread(void *buffer, int nro de bytes, int count, FILE *fp)
A função fread() recebe 4 parâmetros de entrada
• buffer: um ponteiro genérico para a região de memória que irá armazenar os dados que serão lidos do arquivo;
• nro de bytes: tamanho, em bytes, de cada unidade de dado a ser
lida;
• count: total de unidades de dados que devem ser lidas.
• fp: o ponteiro para o arquivo que se deseja trabalhar;
Note que, como na função fwrite(), temos dois valores inteiros: nro de bytes e count. Isto significa que o número
total de bytes lidos do arquivo será: nro de bytes * count.
Como resultado, a função fread() retorna um valor inteiro que representa o
número total de unidades de dados efetivamente lidas com sucesso. Esse
número pode ser menor do que o número de itens esperado (count), indicando que houve erro parcial de leitura.
273
O valor do retorno da função fread() será igual ao valor de
count a menos que ocorra algum erro na leitura dos dados.
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
FILE ∗ arq ;
5
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ r b ’ ’ ) ;
6
i f ( arq == NULL ) {
7
p r i n t f ( ‘ ‘ Problemas na ABERTURA do
a r q u i v o \n ’ ’ ) ;
8
system ( ‘ ‘ pause ’ ’ ) ;
9
exit (1) ;
10
}
11
int i , total lido , v [ 5 ] ;
12
/ / l ê 5 posiç ões i n t e i r a s do a r q u i v o s
13
t o t a l l i d o = f r e a d ( v , s i z e o f ( i n t ) , 5 , arq ) ;
14
i f ( t o t a l l i d o != 5) {
15
p r i n t f ( ‘ ‘ E r r o na l e i t u r a do a r q u i v o ! ’ ’ ) ;
16
system ( ‘ ‘ pause ’ ’ ) ;
17
exit (1) ;
18
} else {
19
f o r ( i = 0 ; i < 5 ; i ++)
20
p r i n t f ( ‘ ‘ v[%d ] = %d\n ’ ’ , i , v [ i ] ) ;
21
}
22
f c l o s e ( arq ) ;
23
system ( ‘ ‘ pause ’ ’ ) ;
24
return 0;
25 }
Abaixo, tem-se um exemplo de um programa que utiliza a função fread()
para ler os mais diversos tipos de dados:
274
Exemplo: usando a função fread()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ r b ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ Problemas na ABERTURA do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
char s t r 1 [ 2 0 ] , s t r 2 [ 2 0 ] ;
float x ;
i n t i , v1 [ 5 ] , v2 [ 2 ] ;
/ / l ê a s t r i n g toda do a r q u i v o
f r e a d ( s t r 1 , s i z e o f ( char ) , 1 2 , arq ) ;
s t r 1 [ 1 2 ] = ’ \0 ’ ;
p r i n t f ( ‘ ‘ % s\n ’ ’ , s t r 1 ) ;
/ / l ê apenas os 5 p r i m e i r o s c a r a c t e r e s da s t r i n g
f r e a d ( s t r 2 , s i z e o f ( char ) , 5 , arq ) ;
s t r 2 [ 5 ] = ’ \0 ’ ;
p r i n t f ( ‘ ‘ % s\n ’ ’ , s t r 2 ) ;
/ / l ê o v a l o r de x do a r q u i v o
f r e a d (& x , s i z e o f ( f l o a t ) , 1 , arq ) ;
p r i n t f ( ‘ ‘ % f \n ’ ’ , x ) ;
/ / l ê todo o a r r a y do a r q u i v o ( 5 posiç ões )
f r e a d ( v1 , s i z e o f ( i n t ) , 5 , arq ) ;
f o r ( i = 0 ; i < 5 ; i ++)
p r i n t f ( ‘ ‘ v1[%d ] = %d\n ’ ’ , i , v1 [ i ] ) ;
f r e a d ( v2 , s i z e o f ( i n t ) , 2 , arq ) ;
/ / l ê apenas as 2 p r i m e i r a s posiç ões do a r r a y
f o r ( i = 0 ; i < 2 ; i ++)
p r i n t f ( ‘ ‘ v2[%d ] = %d\n ’ ’ , i , v2 [ i ] ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, nesse exemplo, que após ler o conteúdo de uma string (linhas 15 e
19) é necessário atribuir o caractere ‘\0’ para indicar o fim da sequência de
caracteres e o inı́cio das posições restantes da nossa string que não estão
sendo utilizadas nesse momento. Nesse exemplo nós sabı́amos qual era
o tamanho da string a ser lida. De modo geral, é sempre bom gravar no
arquivo, antes da string, o seu tamanho. Isso facilita muito a sua leitura
posterior.
275
Ao se trabalhar com strings ou arrays, é sempre bom gravar
no arquivo, antes da string ou array, o seu tamanho. Isso
facilita muito a sua leitura posterior.
O exemplo abaixo mostra como uma string pode ser gravada juntamente
com seu tamanho:
Exemplo: gravando uma string e seu tamanho
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# include <s t d i o . h>
# include < s t d l i b . h>
# include <s t r i n g . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ wb ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
char s t r [ 2 0 ] = ‘ ‘ H e l l o World ! ’ ’ ;
int t = strlen ( str ) ;
f w r i t e (& t , s i z e o f ( i n t ) , 1 , arq ) ;
f w r i t e ( s t r , s i z e o f ( char ) , t , arq ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Já o exemplo seguinte mostra como uma string gravada juntamente com
seu tamanho pode ser lida:
276
Exemplo: lendo uma string e seu tamanho
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
11.5.8
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ r b ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
char s t r [ 2 0 ] ;
int t ;
f r e a d (& t , s i z e o f ( i n t ) , 1 , arq ) ;
f r e a d ( s t r , s i z e o f ( char ) , t , arq ) ;
s t r [ t ] = ’ \0 ’ ;
p r i n t f ( ‘ ‘ % s\n ’ ’ , s t r ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
ESCRITA E LEITURA DE DADOS FORMATADOS
As seções anteriores mostraram como é possı́vel ler e escrever em arquivos caracteres, strings e até mesmo blocos de bytes. Porém, em nenhum
momento foi mostrado como podemos escrever uma lista formatada de
variáveis em um arquivo como o fazemos na tela do computador. Nem
como podemos ler os dados desse mesmo arquivo, especificando aqual o
tipo de dado a ser lido (int, float, char ou double).
As funções de escrita e leitura de dados formatados permitem ao programador escrever e ler em arquivos da mesma
maneira como se escreve na tela e se lê do teclado.
ESCREVENDO DADOS FORMATADOS
Comecemos pela escrita. Para escrever em um arquivo um conjunto de
dados formatados usa-se a função fprintf(), cujo protótipo é:
int fprintf(FILE *fp, “tipos de saı́da”, lista de variáveis)
A função fprintf() recebe 3 parâmetros de entrada
277
• fp: o ponteiro para o arquivo que se deseja trabalhar;
• “tipos de saı́da”: conjunto de caracteres que especifica o formato
dos dados a serem escritos e/ou o texto a ser escrito;
• lista de variáveis: conjunto de nomes de variáveis, separados por
vı́rgula, que serão escritos.
e retorna
• Em caso de sucesso, o número total de caracteres escritos no arquivo é retornado;
• Em caso de erro, um número negativo é retornado.
O exemplo abaixo apresenta um exemplo de uso da função fprintf(). Perceba que a função fprintf() funciona de maneira semelhante a função
printf(). A diferença é que, ao invés de escrever na tela, a função fprintf()
direciona os dados para o arquivo especifı́cado.
278
Exemplo: usando a função fprintf()
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
FILE ∗ arq ;
5
char nome [ 2 0 ] = ‘ ‘ Ricardo ’ ’ ;
6
i n t i = 30;
7
float a = 1.74;
8
int result ;
9
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘w ’ ’ ) ;
10
i f ( arq == NULL ) {
11
p r i n t f ( ‘ ‘ Problemas na ABERTURA do a r q u i v o \n ’ ’ ) ;
12
system ( ‘ ‘ pause ’ ’ ) ;
13
exit (1) ;
14
}
15
r e s u l t = f p r i n t f ( arq , ‘ ‘ Nome : %s\ nIdade : %d\ n A l t u r a :
%f \n ’ ’ ,nome , i , a ) ;
16
i f ( r e s u l t < 0)
17
p r i n t f ( ‘ ‘ E r r o na e s c r i t a \n ’ ’ ) ;
18
f c l o s e ( arq ) ;
19
system ( ‘ ‘ pause ’ ’ ) ;
20
return 0;
21 }
LENDO DADOS FORMATADOS
Uma vez escritos os dados, é necessário agora ler eles do arquivo. Para ler
um conjunto de dados formatados de um arquivo usa-se a função fscanf(),
cujo protótipo é:
int fscanf(FILE *fp, “tipos de entrada”, lista de variáveis)
A função fscanf() recebe 3 parâmetros de entrada
• fp: o ponteiro para o arquivo que se deseja trabalhar;
• “tipos de entrada”: conjunto de caracteres que especifica o formato
dos dados a serem lidos;
• lista de variáveis: conjunto de nomes de variáveis separados por
vı́rgula, onde cada nome de variável é precedido pelo operador &.
279
e retorna
• Em caso de sucesso, a função retorna o número de itens lidos com
sucesso. Esse número pode ser menor do que o número de itens
esperado, indicando que houve erro parcial de leitura.
• a constante EOF, indicando que nenhum item foi lido com sucesso.
O exemplo abaixo apresenta um exemplo de uso da função fscanf(). Perceba que a função fscanf() funciona de maneira semelhante a função
scanf(). A diferença é que, ao invés de ler os dados do teclado, a função
scanf() direciona a leitura dos dados para o arquivo especifı́cado.
Exemplo: usando a função fscanf()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
char t e x t o [ 2 0 ] , nome [ 2 0 ] ;
int i ;
float a;
int result ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ Problemas na ABERTURA do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
f s c a n f ( arq , ‘ ‘ % s%s ’ ’ , t e x t o , nome ) ;
p r i n t f ( ‘ ‘ % s %s\n ’ ’ , t e x t o , nome ) ;
f s c a n f ( arq , ‘ ‘ % s %d ’ ’ , t e x t o ,& i ) ;
p r i n t f ( ‘ ‘ % s %d\n ’ ’ , t e x t o , i ) ;
f s c a n f ( arq , ‘ ‘ % s%f ’ ’ , t e x t o ,& a ) ;
p r i n t f ( ‘ ‘ % s %f \n ’ ’ , t e x t o , a ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Note, nesse exemplo, que foi preciso ler, em todos os comando fscanf(),
o texto que acompanha os dados gravados no arquivo do exemplo do comando fprintf().
280
A única diferença dos protótipos de fprintf() e fscanf() para
os protótipos de printf() e scanf(), respectivamente, são a
especificação do arquivo destino através do ponteiro FILE.
Embora as funções fprintf() e fscanf() sejam mais fáceis de escrever e ler
dados em arquivos, nem sempre elas são as escolhas mais apropriadas.
Tome como exemplo a função fprint(): os dados são gravados exatamente
como seriam impressos na tela e podem ser modificados por um editor de
textos simples como o Bloco de Notas. No entanto, para que isso ocorra,
os dados são gravados como caracteres de 8 bits utilizando a tabela ASCII.
Ou seja, durante a gravação dos dados existe uma etapa de “conversão”
dos dados. Essa “conversão” dos dados faz com que os arquivos sejam
maiores. Além disso, suas operações de escrita e leitura consomem mais
tempo.
Se a intenção do programador é velocidade ou tamanho
do arquivo, deve-se utilizar as funções fwrite() e fread() ao
invés de fprintf() e fscanf(), respectivamente.
O exemplo abaixo mostra como uma matriz pode ser gravada dentro de
um arquivo:
Exemplo: gravando uma matriz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ m a t r i z . t x t ’ ’ , ‘ ‘w ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
i n t mat [ 2 ] [ 2 ] = { { 1 , 2 } , { 3 , 4 } } ;
int i , j ;
f o r ( i = 0 ; i < 2 ; i ++)
f o r ( j = 0 ; j < 2 ; j ++)
f p r i n t f ( arq , ‘ ‘ % d\n ’ ’ , mat [ i ] [ j ] ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
281
O exemplo abaixo mostra como ler um conjunto de dados de um arquivo e
somá-los:
Exemplo: lendo uma matriz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
11.6
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ m a t r i z . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ E r r o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
i n t i , j , v , soma=0;
while ( ! f e o f ( arq ) ) {
f s c a n f ( arq , ‘ ‘ % d ’ ’ ,& v ) ;
soma += v ;
}
p r i n t f ( ‘ ‘ Soma = %d\n ’ ’ ,soma ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
MOVENDO-SE DENTRO DO ARQUIVO
De modo geral, o acesso a um arquivo é quase sempre feito de modo
seqüencial. Porém, a linguagem C permite realizar operações de leitura e
escrita randômica. Para isso, usa-se a função fseek(), cujo protótipo é:
int fseek(FILE *fp, long numbytes, int origem)
Basicamente, a função fseek() move a posição atual de leitura ou escrita no arquivo para um byte especı́fico, a partir
de um ponto especificado.
A função fseek() recebe 3 parâmetros de entrada
• fp: o ponteiro para o arquivo que se deseja trabalhar;
• numbytes: é o total de bytes a partir de origem a ser pulado;
282
• origem: determina a partir de onde os numbytes de movimentação
serão contados.
A função fseek() e retorna um valor inteiro igual a ZERO quando a movimentação
dentro do arquivo for bem sucedida. Um valor de retorno diferente de zero
significa que houve um erro durante a movimentação.
Os valores possı́veis para o parâmetro origem são definidos por constante
na biblioteca stdio.h e são:
Constante
SEEK SET
SEEK CUR
SEEK END
Valor
0
0
0
Significado
Inı́cio do arquivo
Ponto atual no arquivo
Fim do arquivo
Portanto, para movermos numbytes a partir do inı́cio do arquivo, a origem
deve ser SEEK SET. Se quisermos mover a partir da posição atual em que
estamos no arquivo, devemos usar a constante SEEK CUR. E, por fim,
se quisermos mover a partir do final do arquivo, a constante SEEK END
deverá ser usada.
Exemplo: usando a função fseek()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ ArqGrav . t x t ’ ’ , ‘ ‘w ’ ’ ) ;
i f ( arq == NULL ) {
p r i n t f ( ‘ ‘ Problemas na CRAICAO do a r q u i v o \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
}
f p u t s ( ‘ ‘1234567890 ’ ’ , arq ) ;
f s e e k ( arq , 5 , SEEK SET ) ;
f p u t s ( ‘ ‘ abcde ’ ’ , arq ) ;
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, o primeiro comando fputs() (linha 10) é utilizado para
escrever uma sequência de 10 dı́gitos em um arquivo. Em seguida, o
283
ponteiro do arquivo é movido em 5 posições a partir do seu inı́cio (linha
11). Isso significa que os dados escritos pelo segundo comando fputs()
(linha 12) serão escritos a partir do 6 byte do arquivo, sobreescrevendo o
que já havia sido escrito.
O valor do parâmetro numbytes pode ser negativo dependendo do tipo de movimentação que formos realizar.
Por exemplo, se quisermos se mover no arquivo a parir do ponto atual
(SEEK CUR) ou do seu final (SEEK END), um valor negativo de bytes
é possı́vel. Nesse caso, estariamos voltando dentro do arquivo a partir
daquele ponto.
VOLTANDO AO COMEÇO DO ARQUIVO
A linguagem C também permite que se volte para o começo
do arquivo. Para tanto, usa-se a função rewind().
Outra opção de movimentação dentro arquivo é simplesmente retornar
para o seu inı́cio. Para tanto, usa-se a função rewind(), cujo protótipo
é:
void rewind(FILE *fp)
A função rewind() recebe como parâmetro de entrada apenas o ponteiro
para o arquivo que se deseja retornar para o seu inı́cio.
11.7
EXCLUINDO UM ARQUIVO
Além de permitir manipular arquivos, a linguagem C também permite excluilos do disco rı́gido. Isso pode ser feito facilmente utilizando a função remove(), cujo protótipo é:
int remove(char *nome do arquivo)
Diferente das funções vistas até aqui, a função remove() recebe como
parâmetro de entrada o caminho e nome do arquivo a ser excluı́do do
284
disco rı́gido, e não um ponteiro para FILE. Como resultado, essa função
retorna um valor inteiro igual a ZERO quando houver sucesso na exclusão
do arquivo. Um valor de retorno diferente de zero significa que houve um
erro durante a sua exclusão.
No parâmetro nome do arquivo pode-se trabalhar com
caminhos absolutos ou relativos.
Abaixo, tem-se um exemplo de um programa que utiliza a função remove():
Exemplo: usando a função remove()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
11.8
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int status ;
s t a t u s = remove ( ‘ ‘ ArqGrav . t x t ’ ’ ) ;
i f ( status != 0) {
p r i n t f ( ‘ ‘ E r r o na remocao do a r q u i v o . \ n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
exit (1) ;
} else
p r i n t f ( ‘ ‘ A r q u i v o removido com sucesso . \ n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
ERRO AO ACESSAR UM ARQUIVO
Ao se trabalhar com arquivos, diversos tipos de erros podem ocorrer: um
comando de leitura pode falhar, pode não haver espaço suficiente em disco
para gravar o arquivo, etc. Para determinar se uma operação realizada com
o arquivo produziu algum erro existe a função ferror(), cujo protótipo é:
int ferror(FILE *fp)
Basicamente, a função ferror() recebe como parâmetro o ponteiro fp que
determina o arquivo que se quer verificar. A função verifica se o indicador
de erro associado ao arquivo está marcado, e retorna um valor igual a
zero se nenhum erro ocorreu. Do contrário, a função retorna um número
diferente de zero.
285
Como cada operação modifica a condição de erro do arquivo, a função ferror() deve ser chamada logo após cada
operação realizada com o arquivo.
Uma outra função interessante para se utilizar em conjunto com a função
ferror() é a função perror(). Seu nome vem da expressão em inglês print
error, ou seja, impressão de erro, e seu protótipo é:
void perror(char *str)
A função perror() recebe como parâmetro uma string que que irá preceder
a mensagem de erro do sistema. Abaixo é apresentado um exemplo de
uso das funções ferror() e perror(). Nele, um programador tenta acessar
um arquivo que não existe. A abertura desse arquivo irá falhar e a seguinte
mensagem será apresentada: “O seguinte erro ocorreu : No such file
or directory”.
Exemplo: usando as funções ferror() e perror()
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
FILE ∗ arq ;
arq = fopen ( ‘ ‘ NaoExiste . t x t ’ ’ , ‘ ‘ r ’ ’ ) ;
i f ( arq == NULL )
p e r r o r ( ‘ ‘O s e g u i n t e e r r o o c o r r e u ’ ’ ) ;
else
f c l o s e ( arq ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
286
12
AVANÇADO
12.1
DIRETIVAS DE COMPILAÇÃO
As diretivas de compilação são instruções incluı́das dentro do código fonte
do programa, mas que não são compiladas. Sua função é fazer alterações
no código fonte antes de enviá-lo para o compilador. Um exemplo dessas
diretivas de compilação é o comando #define, que usamos para declarar uma constante na Seção 2.4.1. Basicamente, essa diretiva informa ao
compilador que ele deve procurar por todas as ocorrências de uma determinada palavra e substituı́-la por outra quando o programa for compilado.
As principais diretivas de compilação são:
lista de diretivas de compilação
#include #define #undef
#ifdef
#ifndef
#if
#endif
#else
#elif
#line
#error #pragma
Note que todas as diretivas de compilação se iniciam com o caractere #.
Elas podem ser declaradas em qualquer parte do programa, porém, duas
diretivas não podem ser colocadas na mesma linha.
12.1.1
O COMANDO #INCLUDE
O comando #include já foi visto em detalhes na Seção 1.4.1. Ele é utilizado para declarar as bibliotecas que serão utilizadas pelo programa. Basicamente, esse comando diz ao pré-processador para tratar o conteúdo de
um arquivo especificado como se o seu conteúdo houvesse sido digitado
no programa no ponto em que o comando #include aparece.
12.1.2
DEFININDO MACROS: #DEFINE E #UNDEF
Um exemplo dessas diretivas de compilação é o comando #define, que
usamos para declarar uma constante na Seção 2.4.1. Basicamente, essa
diretiva informa ao compilador que ele deve procurar por todas as ocorrências
de uma determinada expressão e substituı́-la por outra quando o programa
for compilado.
O comando #define permite três sintaxes:
287
#define nome
#define nome da constante valor da constante
#define nome da macro(lista de parâmetros) expressão
DEFININDO SÍMBOLOS COM #DEFINE
O primeiro uso possı́vel do comando #define é simplesmente definir um
nome que poderá ser testado mais tarde com os comandos de inclusão
condicional, como mostra o exemplo abaixo:
Exemplo: inclusão condicional com #define
Com #define
Sem #define
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
# define v a l o r
i n t main ( ) {
# i f d e f valor
p r i n t f ( ‘ ‘ Valor
d e f i n i d o \n ’ ’ ) ;
# else
p r i n t f ( ‘ ‘ V a l o r NAO
d e f i n i d o \n ’ ’ ) ;
#endif
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 i n t main ( ) {
5
# i f d e f valor
6
p r i n t f ( ‘ ‘ Valor
d e f i n i d o \n ’ ’ ) ;
7
# else
8
p r i n t f ( ‘ ‘ V a l o r NAO
d e f i n i d o \n ’ ’ ) ;
9
#endif
10
system ( ‘ ‘ pause ’ ’ ) ;
11
return 0;
12 }
No exemplo anterior, o código da esquerda irá exibir a mensagem “Valor
definido” pois nós definimos o sı́mbolo valor como comando #define. Já
o código a direita irá exibir a mensagem “Valor NAO definido” pois em
nenhum momento se definiu quem era o sı́mbolo valor.
DEFININDO CONSTANTES COM #DEFINE
A segunda forma de usar o comando #define já foi usada para declarar
uma constante na Seção 2.4.1. Basicamente, essa diretiva informa ao
compilador que ele deve procurar por todas as ocorrências de uma determinada expressão nome da constante e substituı́-la por valor da constante
quando o programa for compilado, como mostra o exemplo abaixo:
288
Exemplo: constantes com #define
1
2
3
4
5
6
7
8
# include <s t d i o . h>
# include < s t d l i b . h>
# define PI 3.1415
i n t main ( ) {
p r i n t f ( ‘ ‘ V a l o r de PI = %f \n ’ ’ , PI ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O uso da diretivas de compilação #define permite declarar uma “constante” que possa ser utilizada como o tamanho dos arrays ao longo do
programa, bastando mudar o valor da diretiva para redimensionar todos os
arrays em uma nova compilação do programa:
#define TAMANHO 100
...
int VET[TAMANHO];
float mat[TAMANHO][TAMANHO];
DEFININDO FUNÇÕES MACROS COM #DEFINE
A terceira e última forma de usar o comando #define serve para declarar
funções macros: uma espécie de declaração de função onde são informados o nome e os parâmetros da função como sendo o nome da macro
e o trecho de código equivalente a ser utilizado na substituição. Abaixo
tem-se um exemplo:
289
Exemplo: criando uma macro com #define
Com macro
Sem macro
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 # define maior ( x , y ) x>y?x
:y
4 i n t main ( ) {
5
int a = 5;
6
int b = 8;
7
i n t c = maior ( a , b ) ;
8
p r i n t f ( ‘ ‘ Maior v a l o r =
%d\n ’ ’ , c ) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3
4 i n t main ( ) {
5
int a = 5;
6
int b = 8;
7
i n t c = a>b?a : b ;
8
p r i n t f ( ‘ ‘ Maior v a l o r =
%d\n ’ ’ , c ) ;
9
system ( ‘ ‘ pause ’ ’ ) ;
10
return 0;
11 }
No exemplo anterior, o código da esquerda irá substituir a expressão maior(a,b)
pela macro x>y?x:y, trocando o valor de x por a e o valor de y por b, ou
seja, a>b?a:b.
É aconselhável sempre colocar, na sequência de
substituição, os parâmetros da macro entre parênteses.
Isso serve para preservar a precedência dos operadores.
Considere o exemplo abaixo
Exemplo: macros com parênteses
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
# define prod1 ( x , y ) x∗y ;
# define prod2 ( x , y ) ( x ) ∗ ( y ) ;
i n t main ( ) {
int a = 1 , b = 2;
i n t c = prod1 ( a+2 ,b ) ;
i n t d = prod2 ( a+2 ,b ) ;
p r i n t f ( ‘ ‘ V a l o r de c = %d\n ’ ’ , c ) ;
p r i n t f ( ‘ ‘ V a l o r de d = %d\n ’ ’ , d ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Quando as macros forem substituı́das, as variáveis c e d serão preenchidas com os seguintes valores:
290
int a = 1, b = 2;
int c = a + 2 * b;
int d = (a+2) * (b);
Nesse exemplo, teremos que a variável c = 5, enquanto d = 6. Isso acontece por que uma macro não é uma função, e sim uma substituição de
sequências de comandos. O valor de a+2 não é calculado antes de ser
chamada a macro, mas sim colocado no lugar do parâmetro x. Como a
macro prod1 não possui parênteses nos parâmetros, a multiplicação será
executada antes da operação de soma. Já na macro prod2, os parênteses
garantem que a soma seja feita antes da operação de multiplicação.
Dependendo da macro criada, pode ser necessário colocar
a expressão entre chaves {}.
As macros permitem criar funções que podem ser utilizadas para qualquer
tipo de dado. Isso é possı́vel pois a macro permite que identifiquemos
como um dos seus parâmetros o tipo das variáveis utilizadas. Se porventura tivermos que declarar uma variável para esse tipo dentro da expressão
que substituirá a macro, o uso de chaves {}será necessário como mostra
o exemplo abaixo:
Exemplo: criando uma macro com #define e chaves {}
1
2
3
4
5
6
7
8
9
10
11
12
# include<s t d i o . h>
# include<s t d l i b . h>
# define TROCA( a , b , c ) {c t =a ; a=b ; b= t ; }
i n t main ( ) {
i n t x =10;
i n t y =20;
p r i n t f ( ‘ ‘ % d %d\n ’ ’ , x , y ) ;
TROCA( x , y , i n t ) ;
p r i n t f ( ‘ ‘ % d %d\n ’ ’ , x , y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, foi criada uma macro que troca os valores de duas
variáveis de lugar. Para realizar essa tarefa, é necessário declarar uma
terceira variável que dará suporte a essa operação. Essa é a variável t
da macro, a qual é do tipo c. Para que não ocorram conflitos de nomes
291
de variáveis, essa variável t deve ser criada em um novo escopo, o qual é
definido pelo par de {}. Desse modo, a variável t será criada para o tipo c
(que será substituı́do por int) apenas para aquele escopo da macro, sendo
destruı́da na sequência.
FUNÇÕES MACRO COM MAIS DE UMA LINHA
De modo geral, uma função macro deve ser escrita toda em
uma única linha. Porém, pode-se escrevê-la usando mais
de uma linha adicionando uma barra \ao final de cada linha
da macro.
Desse modo, a macro anteriormente criada
#define TROCA(a,b,c) {c t=a; a=b; b=t;}
pode ser reescrita como
#define TROCA(a,b,c) {c t=a; \
a=b; \
b=t;}
OPERADORES ESPECIAIS: # E ##
Definições de funções macro aceitam dois operadores especiais (# e ##) na sequência de substituição: # permite
transformar um texto em string, enquanto o segundo concatena duas expressões.
Se o operador # é colocado antes de um parâmetro na sequência de
substituição, isso significa que o parâmetro deverá ser interpretado como
se o mesmo estivesse entre “aspas duplas”, ou seja, será considerado
como uma string pelo compilador. Já o operador ##, quando colocado
entre dois parâmetro na sequência de substituição, fará com que os dois
parâmetros da macro sejam concatenados ignorando os espaços em branco
entre eles) e interpretados como um comando só. Veja os exemplos abaixo:
292
Exemplo: usando os operadores especiais # e ##
operador #
operador ##
1
2
3
4
5
6
7
8
1 # include<s t d i o . h>
2 # include<s t d l i b . h>
3 # define concatena ( x , y ) x
## y
4 i n t main ( ) {
5
concatena ( p r i n t , f ) ( ‘ ‘
Teste ! \ n ’ ’ ) ;
6
system ( ‘ ‘ pause ’ ’ ) ;
7
return 0;
8 }
# include<s t d i o . h>
# include<s t d l i b . h>
# define s t r ( x ) #x
i n t main ( ) {
p r i n t f ( s t r ( Teste ! \ n ) ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, o código da esquerda irá substituir a expressão str(Teste!\n)
pela string “Teste!\n”. Já o código da direita irá substituir a expressão
concatena(print,f) pela concatenação dos parâmetros, ou seja, o comando
printf.
APAGANDO UMA DEFINIÇÃO: #UNDEF
Por fim, temos a diretiva #undef, que possui a seguinte forma geral:
#undef nome da macro
Basicamente, essa diretiva é utilizada sempre que desejarmos apagar a
definição da macro nome da macro da tabela interna que guarda as macros. Em outras palavras, remove a definição de uma macro para que ela
possa ser redefinida.
Enquanto a diretiva #define cria a definição de uma macro,
a diretiva #undef remove a definição da macro para que ela
não seja mais usada ou para que possa ser redefinida.
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
# define v a l o r 10
i n t main ( ) {
p r i n t f ( ‘ ‘ V a l o r = %d\n ’ ’ , v a l o r ) ;
# undef v a l o r
# d e f i n e v a l o r 20
p r i n t f ( ‘ ‘ Novo v a l o r = %d\n ’ ’ , v a l o r ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
293
12.1.3
DIRETIVAS DE INCLUSÃO CONDICIONAL
O pré-processador da linguagem C também possui estruturas condicionais: são as diretivas de inclusão condicional. Elas permitem incluir ou
descartar parte do código de um programa sempre que uma determinada
condição é satisfeita.
DIRETIVAS #IFDEF E #IFNDEF
Comecemos pelas diretivas #ifdef e #ifndef. Essas diretivas permitem
verificar se uma determinada macro foi previamente definida (#ifdef) ou
não (#ifndef). A sua forma geral é:
#ifdef nome do sı́mbolo
código
#endif
e
#ifndef nome do sı́mbolo
código
#endif
Abaixo é possı́vel ver um exemplo para as diretivas #ifdef e #ifndef
Exemplo: usando as diretivas #ifdef e #ifndef
com #ifdef
com #ifndef
1
2
3
4
5
6
7
8
9
10
11
# include<s t d i o . h>
# include<s t d l i b . h>
# define TAMANHO 100
i n t main ( ) {
# i f d e f TAMANHO
i n t v e t o r [TAMANHO ] ;
#endif
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
# include<s t d i o . h>
# include<s t d l i b . h>
i n t main ( ) {
# i f n d e f TAMANHO
# d e f i n e TAMANHO 100
i n t v e t o r [TAMANHO ] ;
#endif
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo anterior, o código da esquerda irá verificar com a diretiva #ifdef se a macro TAMANHO foi definida. Como ela foi, o programa irá criar
um array de inteiros com TAMANHO elementos. Já o código da direita não
294
possui a macro TAMANHO definida. Por isso usamos a diretiva #ifndef
para verificar se a macro TAMANHO NÃO foi definida. Como ela NÃO
foi, o programa irá executar a diretiva #define para definir a macro TAMANHO para somente em seguida criar um array de inteiros com TAMANHO
elementos.
A diretiva #endif serve para indicar o fim de uma diretiva
de inclusão condicional do tipo #ifdef, #ifndef e #if.
DIRETIVAS #IF, #ELSE E #ELIF
As diretivas #if, #else e #elif são utilizadas para especificar algumas condições
a serem cumpridas para que uma determinada parte do código seja compilado. As diretivas #if e #else são equivalentes aos comandos condicionais
if e else. A forma geral dessas diretivas é
#if condição
sequência de comandos
#else
sequência de comandos
#endif
A diretiva #else é opcional quando usamos a diretiva #if.
Exatamente como o comando else é opcional no uso do
comando if.
Já a diretiva #elif serve para criar um aninhamento de diretivas #if. Ela é
utilizada sempre que desejamos usar novamente a diretiva #if dentro de
uma diretiva #else. A forma geral dessa diretiva é
#if condição1
sequência de comandos
#elif condição2
sequência de comandos
#else
sequência de comandos
#endif
295
Como no caso da diretiva #else, a diretiva #elif também
é opcional quando usamos a diretiva #if. Como a diretiva
#elif testa um nova condição, ela também pode ser seguida
pela diretiva #else ou outra #elif, ambas opcionais.
Abaixo é possı́vel ver um exemplo do uso das diretivas #if, #else e #elif:
Exemplo: usando as diretivas diretivas #if, #else e #elif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# include <s t d i o . h>
# include < s t d l i b . h>
# define TAMANHO 55
# i f TAMANHO > 100
#undef TAMANHO
# define TAMANHO 100
# e l i f TAMANHO < 50
#undef TAMANHO
# define TAMANHO 50
# else
#undef TAMANHO
# define TAMANHO 75
#endif
i n t main ( ) {
p r i n t f ( ‘ ‘ V a l o r de TAMANHO = %d\n ’ ’ ,TAMANHO) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Para entender o exemplo anterior, imagine que a diretiva #define possa
ser reescrita atribuindo diferentes valores para a macro TAMANHO. Se
TAMANHO for maior do que 100, a diretiva #if será executada e um novo
valor para TAMANHO será definido (100). Caso contrário, a condição da
diretiva #elif será testada. Nesse caso, se TAMANHO for menor do que 50,
a sequência de comandos da diretiva #elif será executada e um novo valor
para TAMANHO será definido (50). Se a condição da diretiva #elif também
for falsa, a sequência de comandos da diretiva #else será executada e um
novo valor para TAMANHO será definido (75).
As diretivas #if e #elif só podem ser utilizadas para avaliar
expressões constantes.
Como o código ainda não foi compilado, as diretivas #if e #elif não irão
296
resolver expressões matemáticas dentro da condição. Irão apenas fazer
comparações de valores já definidos, ou seja, constantes.
12.1.4
CONTROLE DE LINHA: #LINE
Sempre que ocorre um erro durante a compilação de um programa, o compilador mostra a mensagem relativa ao erro que ocorreu. Além dessa mensagem, o compilador também exibe o nome do arquivo onde o erro ocorreu
e em qual linha desse arquivo. Isso facilita a busca de onde o erro se encontra no nosso programa.
A diretiva #line, cuja forma geral é
#line numero da linha nome do arquivo
permite controlar o número da linha (numero da linha) e o nome do arquivo (nome do arquivo) onde o erro ocorreu. O parâmetro nome do arquivo
é opcional e se não for definido o compilador usará o próprio nome do arquivo. Veja o exemplo abaixo:
Exemplo: diretiva #line
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
# l i n e 5 ‘ ‘ E r r o de a t r i b u i c a o ’ ’
f l o a t a=;
p r i n t f ( ‘ ‘ V a l o r de a = %f \n ’ ’ , a ) ;
p r i n t f ( ‘ ‘ PI = %f \n ’ ’ , PI ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Nesse exemplo, declaramos a diretiva #line logo acima a declaração de
uma variável. Note que existe um erro de atribuição na variável a (linha
6). Durante o processo de compilação, o compilador irá acusar um erro,
porém, ao invés de afirmar que o erro se encontra na linha 6, ele irá informar que o erro se encontra na linha 5. Além disso, ao invés de exibir o
nome do arquivo onde o erro ocorreu, o compilador irá exibir a mensagem
“Erro de atribuicao”.
297
12.1.5
DIRETIVA DE ERRO: #ERROR
A diretiva #error segue a seguinte forma geral:
#error texto
Basicamente, essa diretiva aborta o processo de compilação do programa
sempre que ela for encontrada. Como resultado, ela gera a mensagem de
erro especificada pelo parâmetro texto. Veja o exemplo abaixo:
Exemplo: diretiva #error
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
# i f n d e f PI
# e r r o r O v a l o r de PI nao f o i d e f i n i d o
#endif
i n t main ( ) {
p r i n t f ( ‘ ‘ PI = %f \n ’ ’ , PI ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Nesse exemplo, em nenhum momento a macro PI foi definida. Portanto o
processo de compilação será abortado devido a falta da macro PI (linhas 4
a 6), e a mensagem de erro “O valor de PI nao foi definido” será exibida
para o programador.
12.1.6
DIRETIVA #PRAGMA
A diretiva #pragma é comumente utilizada para especificar diversas opções
do compilador.
A diretiva #pragma é especı́fica do compilador. Se um argumento utilizado em conjunto com essa diretiva não for
suportado pelo compilador, a diretiva será ignorada e nenhum erro será gerado.
298
Para poder utilizar de modo adequado e saber os possı́veis parâmetros
que você pode definir com a diretiva #pragma, consulte o manual de referência do seu compilador.
12.1.7
DIRETIVAS PRÉ-DEFINIDAS
A linguagem C possui algumas macros já pré-definidas, são elas:
•
LINE : retorna um valor inteiro que representa a linha onde a
macro foi chamada no arquivo de código fonte a ser compilado;
•
FILE : retorna uma string contendo o caminho e nome do arquivo
fonte a ser compilado.
•
DATE : retorna uma string contendo a data de compilação do
arquivo de código fonte no formato “Mmm dd yyyy”;
•
TIME : retorna uma string contendo a hora de compilação do arquivo de código fonte no formato “hh:mm:ss”.
12.2
TRABALHANDO COM PONTEIROS
12.2.1
ARRAY DE PONTEIROS E PONTEIRO PARA ARRAY
Vimos na Seção 9.4 que ponteiros e arrays possuem uma ligação muito
forte dentro da linguagem C. Arrays são agrupamentos sequenciais de
dados do mesmo tipo na memória. O seu nome é apenas um ponteiro que
aponta para o começo dessa sequência de bytes na memória.
O nome do array é apenas um ponteiro que aponta para o
primeiro elemento do array.
De fato, podemos atribuir o endereço de um array para um ponteiro facilmente. A única restrição para essa operação é que o tipo do ponteiro seja
o mesmo do array. E isso pode ser feito de duas formas distintas:
int vet[5] = {1, 2, 3, 4, 5 }
int *p1 = vet;
int *p2 = &vet[0];
299
A linguagem C também permite o uso de arrays e ponteiros de forma
conjunta na declaração de variáveis. Considere as seguintes declarações
abaixo:
typedef vetor int[10];
vetor p1;
vetor *p2;
int (*p3)[10];
int *p4[10];
No exemplo acima, o comando typedef é usado para criar um sinônimo
(vetor) para o tipo “array de 10 inteiros” (int [10]). Assim, a variável p1,
que é do tipo vetor, é um “array de 10 inteiros”. Já a variável p2 é um
ponteiro para o tipo “array de 10 inteiros”.
Temos também a declaração da variável p3. Note que (*p3) está dentro de um parênteses. Isso significa que estamos colocando ênfase na
declaração do ponteiro. Na sequência existe também a definição do tamanho de um array. Como apenas o ponteiro está dentro de parênteses, isso
significa para o compilador que estamos declarando um ponteiro para um
“array de 10 inteiros”. Isso significa que a declaração das variáveis p2 e
p3 são equivalentes.
Por fim, temos a declaração da variável p4. Apesar de semelhante a
declaração de p3, note que não existem parênteses colocando ênfase na
declaração do ponteiro. Isso significa para o compilador que estamos declarando um array de 10 “ponteiros para inteiros”.
Cuidado ao misturar ponteiros e arrays numa mesma
declaração. Nas declarações int (*p3)[10]; e int *p4[10];,
p3 é um ponteiro para um “array de 10 inteiros” enquanto
p4 é um array de 10 “ponteiros para inteiros”.
12.2.2
PONTEIRO PARA FUNÇÃO
Vimos nas seções anteriores, que as variáveis são espaços reservados
da memória utilizados para guardar nossos dados. Já um programa é, na
verdade, um conjunto de instruções armazenadas na memória, juntamente
com seus dados. Vimos também que uma função nada mais é do que um
bloco de código (ou seja, declarações e outros comandos) que podem ser
nomeado e chamado de dentro de um programa.
300
Uma função também é um conjunto de instruções armazenadas na memória. Portanto, podemos acessar uma
função por meio de um ponteiro que aponte para onde a
função está na memória.
A principal vantagem de se declarar um ponteiro para uma função é a
construção de códigos genéricos. Pense na ordenação de números: podemos definir um algoritmo que ordene números inteiros e querer reutilizar essa implementação para ordenar outros tipos de dados (por exemplo,
strings). Ao invés de reescrever toda a função de ordenação, nós podemos passar para esta função o ponteiro da função de comparação que
desejamos utilizar para cada tipo de dado.
Ponteiros permitem fazer uma chamada indireta à função e
passá-la como parâmetro para outras funções. Isso é muito
útil na implementação de algoritmos genéricos em C.
DECLARANDO UM PONTEIRO PARA FUNÇÃO
Em linguagem C, a declaração de um ponteiro para uma função segue a
seguinte forma geral:
tipo retornado (*nome do ponteiro)(lista de tipos);
O nome do ponteiro deve sempre
entre parênteses juntamente com
(*nome do ponteiro).
ser colocado
o asterisco:
Isso é necessário para evitar confusões com a declaração de funções que
retornem ponteiros. Por exemplo,
tipo retornado *nome da função(lista de parâmetros);
é uma função que retorna um ponteiro do tipo retornado, enquanto
tipo retornado (*nome do ponteiro)(lista de tipos);
é um ponteiro para funções que retornam tipo retornado.
301
Um ponteiro para funções só pode apontar para funções
que possuam o mesmo protótipo.
Temos agora que nome do ponteiro é um ponteiro para funções. Mas
não para qualquer função. Apenas para funções que possuam o mesmo
protótipo definido para o ponteiro. Assim, se declararmos um ponteiro para
funções como sendo
int (*ptr)(int, int);
ele poderá ser apontado para qualquer função que receba dois parâmetros
inteiros (independente de seus nomes) e retorne um valor inteiro:
int soma(int x, int y);
APONTANDO UM PONTEIRO PARA UMA FUNÇÃO
Ponteiros não inicializados apontam para um lugar indefinido.
Como qualquer outro ponteiro, quando um ponteiro de função é declarado
ele não possui um endereço associado. Qualquer tentativa de uso desse
ponteiro causa um comportamento indefinido no programa. O ponteiro
para função também pode ser inicializado com a constante NULL, o que
indica que aquele ponteiro aponta para uma posição de memória inexistente, ou seja, nenhuma função.
302
O nome de uma função é seu endereço na memória.
Basta atribuı́-lo ao ponteiro para que o ponteiro aponte para
a função na memória. O operador de & não é necessário.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <s t d i o . h>
# include < s t d l i b . h>
i n t max ( i n t a , i n t b ) {
return ( a > b ) ? a : b ;
}
i n t main ( ) {
int x , y , z ;
int (∗p ) ( int , int ) ;
p r i n t f ( ‘ ‘ D i g i t e 2 numeros : ’ ’ ) ;
s c a n f ( ‘ ‘ % d %d ’ ’ ,&x ,& y ) ;
/ / p o n t e i r o recebe endereço da funç ão
p = max ;
z = p(x , y) ;
p r i n t f ( ‘ ‘ Maior = %d\n ’ ’ , z ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Lembre-se, quando criamos uma função, o computador a guarda em um
espaço reservado de memória. Ao nome que damos a essa função o computador associa o endereço do espaço que ele reservou na memória para
guardar essa função. Assim, basta atribuir o nome da função ao ponteiro
para que o ponteiro passe a apontar para a função na memória (linha 12
do exemplo anterior).
Para usar a função apontada por um ponteiro, basta utilizar
o nome do ponteiro como se ele fosse o nome da função.
Pode-se ver um exemplo de uso da função apontada na linha 13 do exemplo anterior. Nele, utilizamos o ponteiro p como se ele fosse um outro
nome, ou um sinônimo, para a função max(). Abaixo é possı́vel ver outro
exemplo de uso de ponteiros para funções:
303
Exemplo: ponteiro para função
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# include <s t d i o . h>
# include < s t d l i b . h>
i n t soma ( i n t a , i n t b ) { r e t u r n a + b ; }
i n t subtracao ( i n t a , i n t b ) { return a − b ; }
i n t produto ( i n t a , i n t b ) { return a ∗ b ; }
i n t d i v i s a o ( i n t a , i n t b ) { return a / b ; }
i n t main ( ) {
int x , y ;
int (∗p ) ( int , int ) ;
char ch ;
p r i n t f ( ‘ ‘ D i g i t e uma operaç ão matematica
(+ , − ,∗ ,/) : ’ ’ ) ;
ch = g e t c h a r ( ) ;
p r i n t f ( ‘ ‘ D i g i t e 2 numeros : ’ ’ ) ;
s c a n f ( ‘ ‘ % d %d ’ ’ ,&x ,& y ) ;
switch ( ch ) {
case ’ + ’ : p = soma ; break ;
case ’− ’ : p = s u b t r a c a o ; break ;
case ’ ∗ ’ : p = p r o d u t o ; break ;
case ’ / ’ : p = d i v i s a o ; break ;
d e f a u l t : p = NULL ;
}
i f ( p ! =NULL )
p r i n t f ( ‘ ‘ Resultado = %d\n ’ ’ , p ( x , y ) ) ;
else
p r i n t f ( ‘ ‘ Operacao i n v a l i d a \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
PASSANDO UM PONTEIRO PARA FUNÇÃO COMO PARÂMETRO
Como dito anteriormente, a principal vantagem de se declarar um ponteiro
para uma função é que eles permitem a construção de códigos genéricos.
Isso ocorre porque esses ponteiros permitem fazer uma chamada indireta
à função, de modo que eles podem ser passados como parâmetro para
outras funções. Vamos lembrar de como é a declaração de um ponteiro
para uma função. A sua forma geral é
tipo retornado (*nome do ponteiro)(lista de tipos);
Agora, se quisermos declarar uma função que possa receber um ponteiro
para função como parâmetro, tudo o que devemos fazer é incorporar a
declaração de um ponteiro para uma função dentro da declaração dos
parâmetros da função. Considere o seguinte ponteiro para função:
304
int (*ptr)(int, int);
Se quisermos passar esse ponteiro para uma outra função, devemos declarar esse ponteiro na sua lista de parâmetros:
int executa(int (*ptr)(int, int), int x, int y);
Temos agora que a função executa() recebe três parâmetros:
• ptr: um ponteiro para uma função que receba dois parâmetros inteiros e retorne um valor inteiro;
• x: um valor inteiro;
• y: outro valor inteiro.
Abaixo podemos ver um exemplo de uso dessa função:
305
Exemplo: Passando um ponteiro para função como parâmetro
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# include <s t d i o . h>
# include < s t d l i b . h>
i n t soma ( i n t a , i n t b ) { r e t u r n a + b ; }
i n t subtracao ( i n t a , i n t b ) { return a − b ; }
i n t produto ( i n t a , i n t b ) { return a ∗ b ; }
i n t d i v i s a o ( i n t a , i n t b ) { return a / b ; }
i n t executa ( i n t ( ∗ p ) ( i n t , i n t ) , i n t x , i n t y ) {
return p ( x , y ) }
i n t main ( ) {
int x , y ;
int (∗p ) ( int , int ) ;
char ch ;
p r i n t f ( ‘ ‘ D i g i t e uma operaç ão matematica
(+ , − ,∗ ,/) : ’ ’ ) ;
ch = g e t c h a r ( ) ;
p r i n t f ( ‘ ‘ D i g i t e 2 numeros : ’ ’ ) ;
s c a n f ( ‘ ‘ % d %d ’ ’ ,&x ,& y ) ;
switch ( ch ) {
case ’ + ’ : p = soma ; break ;
case ’− ’ : p = s u b t r a c a o ; break ;
case ’ ∗ ’ : p = p r o d u t o ; break ;
case ’ / ’ : p = d i v i s a o ; break ;
d e f a u l t : p = NULL ;
}
i f ( p ! =NULL )
p r i n t f ( ‘ ‘ Resultado = %d\n ’ ’ , executa ( p , x , y ) ) ;
else
p r i n t f ( ‘ ‘ Operacao i n v a l i d a \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
CRIANDO UM ARRAY DE PONTEIROS PARA FUNÇÃO
Vamos relembrar a declaração de arrays. Para declarar uma variável, a
forma geral era
tipo nome;
Já para declarar um array, basta indicar, entre colchetes, o tamanho do
array que queremos criar:
tipo nome[tamanho];
306
Para declarar um array de ponteiros para funções, o
princı́pio é o mesmo usado na declaração de arrays dos
tipos básicos: basta indicar na declaração o seu tamanho entre colchetes para transformar essa declaração na
declaração de um array.
A declaração de arrays de ponteiros para funções funciona exatamente da
mesma maneira que a declaração para outros tipos, ou seja, basta indicar
na declaração do ponteiro para função o seu tamanho entre colchetes para
criar um array:
//ponteiro para função
tipo retornado (*nome do ponteiro)(lista de tipos);
//arrays de ponteiros para função com tamanho elementos tipo retornado
(*nome do ponteiro[tamanho])(lista de tipos);
Feito isso, cada posição do array pode agora apontar para uma função
diferente, como mostra o exemplo abaixo:
307
Exemplo: array de ponteiro para função
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
12.3
# include <s t d i o . h>
# include < s t d l i b . h>
i n t soma ( i n t a , i n t b ) { r e t u r n a + b ; }
i n t subtracao ( i n t a , i n t b ) { return a − b ; }
i n t produto ( i n t a , i n t b ) { return a ∗ b ; }
i n t d i v i s a o ( i n t a , i n t b ) { return a / b ; }
i n t main ( ) {
i n t x , y , i n d i c e = −1;
int (∗p [ 4 ] ) ( int , int ) ;
p [ 0 ] = soma ;
p [ 1 ] = su b t r a c a o ;
p [ 2 ] = produto ;
p [3] = divisao ;
char ch ;
p r i n t f ( ‘ ‘ D i g i t e uma operaç ão matematica
(+ , − ,∗ ,/) : ’ ’ ) ;
ch = g e t c h a r ( ) ;
p r i n t f ( ‘ ‘ D i g i t e 2 numeros : ’ ’ ) ;
s c a n f ( ‘ ‘ % d %d ’ ’ ,&x ,& y ) ;
switch ( ch ) {
case ’ + ’ : i n d i c e = 0 ; break ;
case ’− ’ : i n d i c e = 1 ; break ;
case ’ ∗ ’ : i n d i c e = 2 ; break ;
case ’ / ’ : i n d i c e = 3 ; break ;
d e f a u l t : i n d i c e = −1;
}
i f ( i n d i c e >= 0 )
p r i n t f ( ‘ ‘ Resultado = %d\n ’ ’ , p [ i n d i c e ] ( x , y ) ) ;
else
p r i n t f ( ‘ ‘ Operacao i n v a l i d a \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
ARGUMENTOS NA LINHA DE COMANDO
Ao longo dos vários exemplos de programas criados, foi visto que a clausula main indicava a função principal do programa. Ela era responsável
pelo inı́cio da execução do programa, e era dentro dela que colocávamos
os comandos que querı́amos que o programa executasse. Sua forma geral
era a seguinte:
int main() {
sequência de comandos
308
}
Quando aprendemos a criar nossas próprias funções, vimos que era possı́vel
passar uma lista de parâmetros para a função criada sempre que ela fosse
executada. Porém, a função main sempre foi utilizada sem parâmetros.
A clausula main também é uma função. Portanto ela
também pode receber uma lista de parâmetros no inı́cio
da execução do programa.
A função main pode ser definida de tal maneira que o programa receba
parâmetros que foram dados na linha de comando do sistema operacional.
Para receber esses parâmetros, a função main adquire a seguinte forma:
int main(int argc, char *argv[]) {
sequência de comandos
}
Note que agora a função main recebe dois parâmetros de entrada, são
eles:
• int argc: trata-se de um valor inteiro que indica o número de parâmetros
com os quais a função main foi chamada na linha de comando;
O valor de argc é sempre maior ou igual a 1. Esse
parâmetro vale 1 se o programa foi chamado sem nenhum
parâmetro (o nome do programa é contado como argumento da função), sendo somado +1 em argc para cada
parâmetro passado para o programa.
• char *argv[]: trata-se de um ponteiro para uma matriz de strings.
Cada uma das string contidas nesta matriz é um dos parâmetros com
os quais a função main foi chamada na linha de comando. Ao todo,
existem argc strings guardadas em argv.
A string guardada em argv[0] sempre aponta para o nome
do programa (lembre-se, o nome do programa é contado
como argumento da função).
309
O exemplo abaixo apresenta um programa que recebe parâmetros da linha
de comando:
Exemplo: parâmetros da linha de comando
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( i n t argc , char ∗ argv [ ] ) {
4
i f ( argc == 1 ) {
5
p r i n t f ( ‘ ‘ Nenhum parametro passado para o programa
%s\n ’ ’ , argv [ 0 ] ) ;
6
} else {
7
int i ;
8
p r i n t f ( ‘ ‘ Parametros passados para o programa %s : \
n ’ ’ , argv [ 0 ] ) ;
9
f o r ( i =1; i <argc ; i ++)
10
p r i n t f ( ‘ ‘ Parametro %d : %s\n ’ ’ , i , argv [ i ] ) ;
11
}
12
system ( ‘ ‘ pause ’ ’ ) ;
13
return 0;
14 }
Para testar o exemplo acima, copie o programa e salve em uma pasta qualquer (por exemplo, C:\). Vamos considerar que o programa foi salvo com o
nome “prog.c”. Gere o executável do programa (“prog.exe”). Agora abra
o console (se estiver no Windows: iniciar, executar, cmd), vá para o diretório onde o programa foi salvo (C:\), e digite: prog.exe. Ao apertarmos
a tecla enter, a seguinte mensagem irá aparecer:
Nenhum parâmetro passado para o programa prog.exe
Se, ao invés de digitar prog.exe, nós digitássemos prog.exe par1 par2, a
mensagem impressa seria:
Parametros passados para o programa prog.exe
Parametro 1: par1
Parametro 2: par2
Abaixo, tem-se outro exemplo de programa que recebe parâmetros da linha de comando. No caso, esse programa recebe como parâmetros dois
valores inteiros e os soma. Para isso, fazemos uso da função atoi, a qual
converte uma string para o seu valor inteiro:
310
Exemplo: soma dos parâmetros da linha de comando
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( i n t argc , char ∗ argv [ ] ) {
4
i f ( argc == 1 ) {
5
p r i n t f ( ‘ ‘ Nenhum parametro para s e r somado\n ’ ’ ) ;
6
} else {
7
i n t soma = 0 , i ;
8
p r i n t f ( ‘ ‘ Somando os parametros passados para o
programa %s : \ n ’ ’ , argv [ 0 ] ) ;
9
f o r ( i =1; i <argc ; i ++)
10
soma = soma + a t o i ( argv [ i ] ) ;
11
p r i n t f ( ‘ ‘ Soma = %d\n ’ ’ ,soma ) ;
12
}
13
system ( ‘ ‘ pause ’ ’ ) ;
14
return 0;
15 }
12.4
RECURSOS AVANÇADOS DA FUNÇÃO PRINTF()
Vimos na Seção 2.2.1 que a função printf() é uma das funções de saı́da/escrita
de dados da linguagem C. Sua funcionalidade básica é escrever na saı́da
de video (tela) um conjunto de valores, caracteres e/ou sequência de caracteres de acordo com o formato especificado. Porém, essa função permite uma variedade muito maior de formatações do que as vistas até
agora. Comecemos pela sua definição. A forma geral da função printf() é:
int printf(“tipos de saı́da”, lista de variáveis)
A função printf() recebe 2 parâmetros de entrada
• “tipos de saı́da”: conjunto de caracteres que especifica o formato
dos dados a serem escritos e/ou o texto a ser escrito;
• lista de variáveis: conjunto de nomes de variáveis, separados por
vı́rgula, que serão escritos.
Note também que a função printf() retorna um valor inteiro, ignorado até o
presente momento. Esse valor de retorno será
• o número total de caracteres escritos na tela, em caso de sucesso;
• um valor negativo, em caso de erro da função.
311
O valor de retorno da função printf() permite identificar o
funcionamento adequado da função.
Vimos também que quando queremos escrever dados formatados na tela
nós usamos os tipos de saı́da para especificar o formato de saı́da dos
dados que serão escritos. E que cada tipo de saı́da é precedido por um
sinal de % e um tipo de saı́da deve ser especificado para cada variável a
ser escrita.
A string do tipo de saı́da permite especificar mais caracterı́sticas dos dados além do formato. Essas caracterı́sticas são opcionais e são: flag, largura, precisão e
comprimento.
A ordem em que essas quatro caracterı́sticas devem ser especificadas é a
seguinte:
%[flag][largura][.precisão][comprimento]tipo de saı́da
Note que o campo precisão vem sempre começando com
um caractere de ponto (.).
Como o tipo de saı́da, cada uma dessas caracterı́sticas possui um conjunto de valores pré-definidos e suportados pela linguagem. Nas seções
seguintes são apresentados todos os valores suportados para cada uma
das caracterı́sticas de formatação possı́veis.
12.4.1
OS TIPOS DE SAÍDA
A função printf() pode ser usada para escrever virtualmente qualquer tipo
de dado. A tabela abaixo mostra todos os tipos de saı́da suportados pela
linguagem C:
312
“tipo de saı́da”
%c
%d ou %i
%u
%f
%s
%p
%e ou %E
%x
%X
%o
%g
%G
%%
Descrição
Escrita de um caractere
Escrita de números inteiros com sinal (signed)
Escrita de números inteiros sem sinal
(unsigned)
Escrita de números reais (float e double)
Escrita de vários caracteres (string)
Escrita de um endereço de memória (ponteiro)
Escrita de número reais (float e double) em
notação cientı́fica (usando caractere “e” ou “E”)
Escrita de números inteiros sem sinal
(unsigned) no formato hexadecimal (minúsculo)
Escrita de números inteiros sem sinal
(unsigned) no formato hexadecimal (Maiúsculo)
Escrita de números inteiros sem sinal
(unsigned) no formato octal (base 8)
Escrita de número reais (float e double). Compilador decide se é melhor usar %f ou %e
Escrita de número reais (float e double). Compilador decide se é melhor usar %f ou %E
Escrita do caractere %
A seguir, são apresentados alguns exemplos de como cada tipo de saı́da
pode ser utilizado para escrever determinado dado na tela.
EXIBINDO OS TIPOS BÁSICOS
A linguagem C possui vários tipos de saı́da que podem ser utilizados com
os tipos básicos, ou seja, char (“%c” e “%d”), int (“%d” e “%i”), float e
double (“%f”), e por fim array de char ou string (“%s”). Note que o tipo
char pode ser escrito na tela de saı́da por meio dos operadores “%c” e
“%d”. Nesse caso, “%c” irá imprimir o caractere armazenado na variável,
enquanto “%d” irá imprimir o seu valor na tabela ASCII. Abaixo, tem-se
alguns exemplos de escrita dos tipos básicos:
313
Exemplo: usando printf() para imprimir os tipos básicos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t n = 125;
float f = 5.25;
double d = 1 0 . 5 3 ;
char l e t r a = ’A ’ ;
char p a l a v r a [ 1 0 ] = ‘ ‘ programa ’ ’ ;
p r i n t f ( ‘ ‘ V a l o r i n t e i r o : %d\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r i n t e i r o : %i \n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r r e a l : %f \n ’ ’ , f ) ;
p r i n t f ( ‘ ‘ V a l o r r e a l : %f \n ’ ’ , d ) ;
p r i n t f ( ‘ ‘ C a r a c t e r e : %c\n ’ ’ , l e t r a ) ;
p r i n t f ( ‘ ‘ V a l o r numerico do c a r a c t e r e : %d\n ’ ’ , l e t r a ) ;
p r i n t f ( ‘ ‘ P a l a v r a : %s\n ’ ’ , p a l a v r a ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
EXIBINDO VALORES NO FORMATO OCTAL OU HEXADECIMAL
O exemplo abaixo mostra como exibir um valor inteiro nos formatos octal (base 8) ou hexadecimal (base 16). Para isso, usamos os tipos de
saı́da “%o” (sinal de porcento mais a letra “o”, não o zero “0”) para que
a função printf exiba o valor em octal, e “%x” para hexadecimal com
letras minúsculas e “%X” para hexadecimal com letras maiúsculas.
Abaixo, podemos ver alguns exemplos:
Exemplo: printf() com valores no formato octal e hexadecimal
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t n = 125;
p r i n t f ( ‘ ‘ V a l o r de n : %d\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r em o c t a l : %o\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r em hexadecimal : %x\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r em hexadecimal : %X\n ’ ’ , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
EXIBINDO VALORES COMO NOTAÇÃO CIENTÍFICA
314
O exemplo abaixo mostra como exibir um valor real (também chamado
ponto flutuante) no formato de notação cientı́fica. Para isso, usamos
os tipos de saı́da “%e” ou “%E”, sendo que o primeiro usará a letra E
minúscula enquanto o segundo usará ela maiúscula na saı́da.
Temos também os tipos de saı́da “%g” e “%G”. Esses tipos de saı́da,
quando utilizados, deixam para o compilador decidir se é melhor usar “%f”
ou “%e” (ou “%E”, se for utilizado “%G”). Nesse caso, o compilador usa
“%e” (ou “%E”) para que números muito grandes ou muito pequenos sejam
mostrados na forma de notação cientı́fica. Abaixo, podemos ver alguns
exemplos:
Exemplo: imprimindo float e double como notação cientı́fica
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
f l o a t f = 0.00000025;
double d = 1 0 . 5 3 ;
p r i n t f ( ‘ ‘ V a l o r r e a l : %e\n ’
p r i n t f ( ‘ ‘ V a l o r r e a l : %E\n ’
p r i n t f ( ‘ ‘ V a l o r r e a l : %g\n ’
p r i n t f ( ‘ ‘ V a l o r r e a l : %G\n ’
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’
’
’
’
,f);
,f);
,d) ;
,f);
EXIBINDO VALORES INTEIROS “SEM SINAL” E ENDEREÇOS
Para imprimir valores inteiros sem sinal, devemos utilizar o tipo de saı́da
“%u” e evitar o uso do tipo “%d”. Isso ocorre por que o tipo “%u” trata o
número inteiro como unsigned (sem sinal), enquanto “%d” o trata como
signed (com sinal).
A primeira vista os dois tipos podem parecer iguais. Se o valor inteiro
estiver entre 0 e INT MAX (231 − 1 em sistemas de 32 bits), a saı́da será
idêntica para os dois casos (“%d” e “%u”). Porém, se o valor inteiro for
negativo (para entradas com sinal, signed) ou estiver entre INT MAX e
UINT MAX (isto é, entre 231 e 232 − 1 em sistemas de 32 bits), os valores
impressos pelos tipos “%d” e “%u” serão diferentes. Neste caso, o tipo
“%d” irá imprimir um valor negativo, enquanto o tipo “%u” irá imprimir um
valor positivo.
Já para imprimir o endereço de memória de uma variável ou ponteiro, podemos utilizar o tipo de saı́da “%p”. Esse tipo de saı́da irá imprimir o
315
endereço no formato hexadecimal, sendo que o valor impresso depende do
compilador e da plataforma. O endereço de memória poderia ser também
impresso por meio do tipo “%x” (ou “%X”), porém, esse tipo de saı́da
pode gerar uma impressão incorreta do valor do endereço, principalmente
em sistemas 64-bit. Abaixo, podemos ver alguns exemplos:
Exemplo: imprimindo valores inteiro sem sinal e endereços
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
unsigned i n t n = 2147483647;
p r i n t f ( ‘ ‘ V a l o r r e a l : %d\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r r e a l : %u\n ’ ’ , n ) ;
n = n + 1;
p r i n t f ( ‘ ‘ V a l o r r e a l : %d\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ V a l o r r e a l : %u\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ Endereco de n = %p\n ’ ’ ,&n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
EXIBINDO O SÍMBOLO DE “%”
O caractere “%” é normalmente utilizado dentro da função printf() para
especificar o formato de saı́da em que um determinado dado será escrito. Porém, pode ser as vezes necessário imprimir o caractere “%” na
tela de saı́da. Para realizar essa tarefa, basta colocar dois caracteres “%”,
“%%”, para que ele seja impresso na tela de saı́da como mostra o exemplo
abaixo:
Exemplo: imprimindo o sı́mbolo de “%”
1
2
3
4
5
6
7
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
p r i n t f ( ‘ ‘ Juros de 25%%\n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
316
12.4.2
AS “FLAGS” PARA OS TIPOS DE SAÍDA
As “flags” permitem adicionar caracterı́sticas extras a um determinado formato de saı́da utilizado com a função printf(). Elas vem logo em seguida
ao sinal de % e antes do tipo de saı́da. A tabela abaixo mostra todas as
“flags” suportadas pela linguagem C:
“flags”
-
+
(espaço)
#
0
Descrição
imprime o valor justificado à esquerda dentro
da largura determinada pelo campo largura; Por
padrão, o valor é sempre justificado a direita.
imprime o sı́mbolo de sinal (+ ou -) antes do valor
impresso, mesmo para números positivos. Por
padrão, apenas os números negativos são impressos com o sinal.
imprime o valor com espaços em branco à
esquerda dentro da largura determinada pelo
campo largura.
Se usado com os tipos “%o”, “%x” ou “%X”, o
valor impresso é precedido de “0”, “0x” ou “0X”,
respectivamente, para valores diferentes de zero.
Se usado com valores do tipo float e double, imprime o ponto decimal mesmo se nenhum dı́gito
vir em seguida. Por padrão, se nenhum dı́gito for
especificado, nenhum ponto decimal é escrito.
imprime o valor com zeros (0) em vez de espaços
à esquerda dentro da largura determinada pelo
campo largura
JUSTIFICANDO UM VALOR A ESQUERDA
O exemplo abaixo mostra o uso das “flags” para justificar os dados na tela
de saı́da. Note que para justificar um valor e preciso definir o valor da
largura, isto é, a quantidade mı́nima de caracteres que se poderá utilizar
durante a impressão na tela de saı́da. No caso, definimos que a largura
são 5 caracteres:
317
Exemplo: justificando um valor a esquerda
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int n = 5;
// justifica a direita
p r i n t f ( ‘ ‘ n = %5d\n ’ ’ , n ) ;
/ / j u s t i f i c a a esquerda
p r i n t f ( ‘ ‘ n = %−5d\n ’ ’ , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
FORÇAR A IMPRESSÃO DO SINAL DO NÚMERO
Por padrão, a função printf() imprime apenas os números negativos com
o sinal. No entanto, pode-se forçar a impressão do sinal de positivo, como
mostra o exemplo abaixo:
Exemplo: imprimindo sempre o sinal do número
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int n = 5;
/ / sem s i n a l
p r i n t f ( ‘ ‘ n = %d\n ’ ’ , n ) ;
/ / com s i n a l
p r i n t f ( ‘ ‘ n = %+d\n ’ ’ , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
IMPRIMINDO “ESPAÇOS” OU ZEROS ANTES DE UM NÚMERO
Quando definimos a largura do valor, estamos definindo a quantidade
mı́nima de caracteres que será utilizada durante a impressão na tela de
saı́da. Por padrão, a função printf() justifica os dados a direita e preenche
o restante da largura com espaços. Porém, pode-se preencher o restante
da largura com zeros, como mostra o exemplo abaixo:
318
Exemplo: imprimindo espaços ou zeros antes do número
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int n = 5;
/ / com espaços ( padr ão )
p r i n t f ( ‘ ‘ n = % 5d\n ’ ’ , n ) ;
/ / com zeros
p r i n t f ( ‘ ‘ n = %05d\n ’ ’ , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
IMPRIMINDO O PREFIXO HEXADECIMAL E OCTAL E O PONTO
Por padrão, a função printf() imprime valores no formato octal e hexadecimal sem os seus prefixo (0 e 0x, respectivamente). Já o ponto decimal
dos valores em ponto flutuante é omitido caso não se tenha definido a precisão apesar de ter sido incluido na sua formatação o indicador de ponto
(“.”). Felizmente, pode-se forçar a impressão do prefixo e do ponto, como
mostra o exemplo abaixo:
Exemplo: imprimindo o prefixo e o ponto decimal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t n = 125;
/ / o c t a l e hexadecimal sem p r e f i x o
p r i n t f ( ‘ ‘ x = %o\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ x = %X\n ’ ’ , n ) ;
/ / o c t a l e hexadecimal com p r e f i x o
p r i n t f ( ‘ ‘ x = %#o\n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ x = %#X\n ’ ’ , n ) ;
float x = 5.00;
/ / f l o a t sem ponto
p r i n t f ( ‘ ‘ x = %. f \n ’ ’ , x ) ;
/ / f l o a t com ponto
p r i n t f ( ‘ ‘ x = %#. f \n ’ ’ , x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
319
12.4.3
O CAMPO “LARGURA” DOS TIPOS DE SAÍDA
O campo largura é comumente usado com outros campos (como visto com
as “flags”). Ele na verdade especifica o número mı́nimo de caracteres a
serem impressos na tela de saı́da. Ela pode ser definida de duas maneiras,
como mostra a tabela abaixo:
“largura”
“número”
*
Descrição
Número mı́nimo de caracteres a serem impressos. Se a largura do valor a ser impresso é inferior a este número, espaços em branco serão
acrescentados a esquerda.
Informa que a largura vai ser especificada por
um valor inteiro passado como parâmetro para
a função printf().
Abaixo, podemos ver um exemplo de uso do campo largura:
Exemplo: definindo o campo largura
1
2
3
4
5
6
7
8
9
10
11
12
12.4.4
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t n = 125;
i n t l a r g u r a = 10;
/ / l a r g u r a d e f i n i d a d e n t r o do campo
p r i n t f ( ‘ ‘ n = %10d\n ’ ’ , n ) ;
/ / l a r g u r a d e f i n i d a por uma v a r i á v e l i n t e i r a
p r i n t f ( ‘ ‘ n = %∗d\n ’ ’ , l a r g u r a , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O CAMPO “PRECISÃO” DOS TIPOS DE SAÍDA
O campo precisão é comumente usado com valores de ponto flutuante
(tipos float e double). De modo geral, esse campo especifica o número
de caracteres a serem impressos na tela de saı́da após o ponto decimal.
Note que o campo precisão vem sempre começando com
um caractere de ponto (.).
320
Porém, o campo precisão pode ser utilizado com outros tipos, como mostra a tabela abaixo:
“.precisão”
.número
.*
Descrição
Para os tipos “%d”, “%i”, “%u”, “%o”, “%x” e “%X”:
número mı́nimo de caracteres a serem impressos. Se a largura do valor a ser impresso é inferior a este número, zeros serão acrescentados a
esquerda
Para os tipos “%f”, “%e” e “%E”: número de
dı́gitos a serem impressos após o ponto decimal.
Para os tipos “%g” e “%G”: número máximo de
dı́gitos significativos a serem impressos.
Para o tipo “%s”: número máximo de caracteres
a serem impressos. Por padrão, todos os caracteres são impressos até que o caractere “\0” é
encontrado.
Para o tipo “%c”: sem efeito.
Se nenhum valor for especificado para a precisão, a precisão é considerada 0 (padrão).
Informa que a largura vai ser especificada por
um valor inteiro passado como parâmetro para
a função printf().
O CAMPO “PRECISÃO” PARA VALORES INTEIROS
O campo “precisão”, quando usado com valores inteiros (pode ser também
no formato octal ou hexadecimal), funciona de modo semelhante a largura
do campo, ou seja, especifica o número mı́nimo de caracteres a ser impressos, com a vantagem de já preencher o restante dessa largura com
zeros, como mostra o exemplo abaixo:
Exemplo: a “precisão” para valores inteiros
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t n = 125;
p r i n t f ( ‘ ‘ n = %.8d ( decimal ) \n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ n = %.8o ( o c t a l ) \n ’ ’ , n ) ;
p r i n t f ( ‘ ‘ n = %.8X ( hexadecimal ) \n ’ ’ , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
321
O CAMPO “PRECISÃO” PARA VALORES REAIS
O campo “precisão”, quando usado com valores de ponto flutuante (tipos
float e double), especifica o número de caracteres a serem impressos na
tela de saı́da após o ponto decimal. A única exceção é com os tipos de
saı́da “%g” e “%G”. Nesse caso, o campo “precisão” especifica o número
máximo de caracteres a serem impressos. Abaixo é possı́vel ver alguns
exemplos:
Exemplo: a “precisão” para valores reais
1
2
3
4
5
6
7
8
9
10
11
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
f l o a t n = 123.45678;
p r i n t f ( ‘ ‘ n = %.3 f \n ’
p r i n t f ( ‘ ‘ n = %.5 f \n ’
p r i n t f ( ‘ ‘ n = %.5e\n ’
p r i n t f ( ‘ ‘ n = %.5g\n ’
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’
’
’
’
,n) ;
,n) ;
,n) ;
,n) ;
O CAMPO “PRECISÃO” USADO COM STRINGS
O campo “precisão” também permite especificar o número máximo de caracteres a serem impressos de uma string. Por padrão, todos os caracteres da string são impressos até que o caractere “\0” é encontrado, como
mostra o exemplo abaixo:
Exemplo: a “precisão” para strings
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 2 0 ] = ‘ ‘ Meu programa C ’ ’ ;
p r i n t f ( ‘ ‘ % s\n ’ ’ , t e x t o ) ;
p r i n t f ( ‘ ‘ % . 3 s\n ’ ’ , t e x t o ) ;
p r i n t f ( ‘ ‘ % . 1 2 s\n ’ ’ , t e x t o ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O CAMPO “PRECISÃO” DEFINIDO POR UMA VARIÁVEL INTEIRA
322
Por fim, podemos informar que a “precisão” será especificada por um valor
inteiro passado como parâmetro para a função printf():
Exemplo: precisão como parâmetro
1
2
3
4
5
6
7
8
9
10
12.4.5
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
f l o a t n = 123.45678;
i n t precisao = 10;
/ / p r e c i s ã o d e f i n i d a por uma v a r i á v e l i n t e i r a
p r i n t f ( ‘ ‘ n = %.∗ f \n ’ ’ , p r e c i s a o , n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O CAMPO “COMPRIMENTO” DOS TIPOS DE SAÍDA
O campo “comprimento” é utilizado para imprimir valores que sejam do tipo
short int, long int e long double, como mostra a tabela abaixo:
“comprimento”
h
l
L
Descrição
Para os tipos “%d”, “%i”, “%u”, “%o”, “%x” e “%X”:
o valor é interpretado como short int ou unsigned short int
Para os tipos “%d”, “%i”, “%u”, “%o”, “%x” e “%X”:
o valor é interpretado como long int ou unsigned long int
Para os tipos “%c” e “%s”: permite imprimir caracteres e sequências de caracteres onde cada
caractere possui mais do que 8-bits.
Para os tipos “%f”, “%e”, “%E”, “%g” e “%G”: o
valor é interpretado como long double
Deve-se tomar cuidado com o campo “comprimento”, pois ele não funciona
corretamente dependendo do compilador e da plataforma utilizada.
12.4.6
USANDO MAIS DE UMA LINHA NA FUNÇÃO PRINTF()
Pode ocorrer de a linha que queiramos escrever na tela de saı́da seja muito
grande. Isso faz com que a string dentro da função printf() não possa ser
323
visualizada toda de uma vez. Felizmente, a função permite que coloquemos um caractere de barra invertida “\” apenas para indicar que a string
que estamos digitando continua na próxima linha:
Exemplo: a função printf() com mais de uma linha
1
2
3
4
5
6
7
8
9
12.5
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
p r i n t f ( ‘ ‘ Esse t e x t o que estou querendo e s c r e v e r \
na t e l a de s a i d a e muito grande . Por i s s o eu \
r e s o l v i quebrar e l e em v a r i a s l i n h a s \n ’ ’ ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
RECURSOS AVANÇADOS DA FUNÇÃO SCANF()
Vimos an Seção 2.2.3 que a função scanf() é uma das funções de entrada/leitura de dados da linguagem C. Sua funcionalidade básica é ler no
dispositivo de entrada de dados (teclado) um conjunto de valores, caracteres e/ou sequência de caracteres de acordo com o formato especificado.
Porém, essa função permite uma variedade muito maior de formatações
do que as vistas até agora. Comecemos pela sua definição. A forma geral
da função scanf() é:
int scanf(“tipos de entrada”, lista de variáveis)
A função scanf() recebe 2 parâmetros de entrada
• “tipos de entrada”: conjunto de caracteres que especifica o formato
dos dados a serem lidos do teclado;
• lista de variáveis: conjunto de nomes de variáveis que serão lidos e
separados por vı́rgula, onde cada nome de variável é precedido pelo
operador &.
Note também que a função scanf() retorna um valor inteiro, ignorado até o
presente momento. Esse valor de retorno será
• em caso de sucesso, o número total de itens lidos. Esse número
pode ser igual ou menor do que o número esperado de itens a serem
lidos;
324
• a constante EOF, em caso de erro da função.
O valor de retorno da função scanf() permite identificar o
funcionamento adequado da função.
Vimos também que quando queremos ler dados formatados do teclado
nós usamos os tipos de entrada para especificar o formato de entrada
dos dados que serão lidos. E que cada tipo de entrada é precedido por um
sinal de % e um tipo de entrada deve ser especificado para cada variável
a ser lida.
A string do tipo de entrada permite especificar mais caracterı́sticas dos dados lidos além do seu formato. Essas
caracterı́sticas são opcionais e são: *, largura e modificadores.
A ordem em que essas quatro caracterı́sticas devem ser especificadas é a
seguinte:
%[*][largura][modificadores]tipo de entrada
Como o tipo de entrada, cada uma dessas caracterı́sticas possui um conjunto de valores pré-definidos e suportados pela linguagem. Nas seções
seguintes são apresentados todos os valores suportados para cada uma
das caracterı́sticas de formatação possı́veis.
12.5.1
OS TIPOS DE ENTRADA
A função scanf() pode ser usada para lerer virtualmente qualquer tipo de
dado. A tabela abaixo mostra todos os tipos de entrada suportados pela
linguagem C:
325
“tipo de
entrada”
%c
%d
%u
%i
%f, %e, %E, %g,
%G
%o
%x ou %X
%s
Descrição
Leitura de um caractere (char). Se uma largura diferente do valor 1 é especificada, a função
lê o número de caracteres especificado na largura e os armazena em posições sucessivas de
memória (array) do ponteiro para char passado
como parâmetro. O caractere “\0” não é acrescentado no final.
Leitura de um número inteiro (int). Ele pode ser
precedido pelo sı́mbolo de sinal (+ ou -)
Leitura de um número inteiro sem sinal
(unsigned int)
Leitura de um número inteiro (int). O valor pode
estar precedido de “0x” ou “0X”) se for hexadecimal, ou pode ser precedido por zero (0) se for
octal.
Leitura de um número real (float e double). Ele
pode ser precedido pelo sı́mbolo de sinal (+ ou -),
e/ou seguido pelos caracteres “e” ou “E” (notação
cientı́fica) e/ou possuir o separador de ponto decimal
Leitura de um número inteiro (int) no formato octal (base 8). O valor pode ou não estar precedido
de zero (0).
Leitura de um número inteiro (int) no formato hexadecimal. O valor pode ou não estar precedido
de “0x” ou “0X”)
Leitura de um sequência de caracteres (string)
até um caractere de nova linha “\n” ou espaço
em branco seja encontrado.
A seguir, são apresentados alguns exemplos de como cada tipo de entrada
pode ser utilizado para ler determinado dado do teclado.
LENDO OS TIPOS BÁSICOS
A linguagem C possui vários tipos de entrada que podem ser utilizados
com os tipos básicos, ou seja, char (“%c” e “%d”), int (“%d” e “%i”), float
e double (“%f”). Note que o tipo char pode ser lido do teclado por meio
dos operadores “%c” e “%d”. Nesse caso, “%c” irá ler um caractere e armazenar na variável, enquanto “%d” irá ler um valor numérico e armazenar
na variável o caractere correspondente da tabela ASCII. Abaixo, tem-se
alguns exemplos de leitura dos tipos básicos:
326
Exemplo: usando scanf() para ler os tipos básicos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int n;
float f ;
double d ;
char l e t r a ;
/ / l e i t u r a de i n t
s c a n f ( ‘ ‘ % d ’ ’ ,&n ) ;
s c a n f ( ‘ ‘ % i ’ ’ ,&n ) ;
/ / l e i t u r a de char
s c a n f ( ‘ ‘ % d ’ ’ ,& l e t r a ) ;
s c a n f ( ‘ ‘ % c ’ ’ ,& l e t r a ) ;
/ / l e i t u r a de f l o a t e double
s c a n f ( ‘ ‘ % f ’ ’ ,& f ) ;
s c a n f ( ‘ ‘ % f ’ ’ ,&d ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
LENDO VALORES NO FORMATO OCTAL OU HEXADECIMAL
O exemplo abaixo mostra como ler um valor inteiro nos formatos octal
(base 8) ou hexadecimal (base 16). Para isso, usamos os tipos de entrada “%o” (sinal de porcento mais a letra “o”, não o zero “0”) para que a
função scanf() leia o valor em octal, e “%x” para ler um valor em hexadecimal com letras minúsculas e “%X” para hexadecimal com letras
maiúsculas. Note que em ambos os casos, o valor lido pode ou não estar
precedidode zero(0) se for octal ou“%x” (ou “%X”) se for hexadecimal.
Abaixo, podemos ver alguns exemplos:
Exemplo: scanf() com valores no formato octal e hexadecimal
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int n;
/ / l e i t u r a no f o r m a t o o c t a l
s c a n f ( ‘ ‘ % o ’ ’ ,&n ) ;
/ / l e i t u r a no f o r m a t o hexadecimal
s c a n f ( ‘ ‘ % x ’ ’ ,&n ) ;
s c a n f ( ‘ ‘ %X ’ ’ ,&n ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
327
LENDO VALORES COMO NOTAÇÃO CIENTÍFICA
De modo geral, podemos ler um valor em notação cientı́fica com qualquer
um dos tipos de entrada habilitados para lerem valores de ponto flutuante
(float e double): “%f”, “%e”, “%E”, “%g” e “%G”. Na verdade, esses tipos
de entrada não fazem distinção na forma como o valor em ponto flutuante
é escrito, desde que seja ponto flutuante, como mostra o exemplo abaixo:
Exemplo: lendo float e double como notação cientı́fica
1
2
3
4
5
6
7
8
9
10
11
12
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
float x ;
s c a n f ( ‘ ‘ % f ’ ’ ,& x ) ;
s c a n f ( ‘ ‘ % e ’ ’ ,& x ) ;
s c a n f ( ‘ ‘ %E ’ ’ ,& x ) ;
s c a n f ( ‘ ‘ % g ’ ’ ,& x ) ;
s c a n f ( ‘ ‘ %G ’ ’ ,& x ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
LENDO UMA STRING DO TECLADO
O exemplo abaixo mostra como ler uma string (ou array de caracteres,
char) do teclado. Para isso, usamos o tipo de entrada “%s”. Note que
quando usamos a função scanf() para ler uma string, o sı́mbolo de & antes do nome da variável não é utilizado. Além disso, a função scanf()
lê apenas strings digitadas sem espaços, ou seja, apenas palavras. No
caso de ter sido digitada uma frase (uma sequência de caracteres contendo espaços) apenas os caracteres digitados antes do primeiro espaço
encontrado serão armazenados na string.
Exemplo: lendo uma string com scanf()
1
2
3
4
5
6
7
8
9
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 2 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e algum t e x t o :
scanf ( ‘ ‘% s ’ ’ , t e x t o ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
328
’ ’);
12.5.2
O CAMPO ASTERISCO “*”
O uso de um asterisco “*” após o sı́mbolo de % indica que os dados formatados devem ser lidos do teclado mas ignorados, ou seja, não devem
ser armazenados em nenhuma variável. Abaixo é possı́vel ver um exemplo
de uso:
Exemplo: ignorando dados digitados
1
2
3
4
5
6
7
8
9
10
11
12
13
14
12.5.3
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int x , y ;
p r i n t f ( ‘ ‘ Digite tres inteiros : ’ ’ ) ;
s c a n f ( ‘ ‘ % d %∗d %d ’ ’ ,&x ,& y ) ;
p r i n t f ( ‘ ‘ Numeros l i d o s : %d e %d\n ’ ’ , x , y ) ;
char nome [ 2 0 ] , curso [ 2 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e nome , idade e curso : ’ ’ ) ;
s c a n f ( ‘ ‘ % s %∗d %s ’ ’ ,nome , curso ) ;
p r i n t f ( ‘ ‘ Nome : %s\nCurso : %s\n ’ ’ ,nome , curso ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O CAMPO “LARGURA” DOS TIPOS DE ENTRADA
Basicamente, o campo largura é um valor inteiro que especifica o número
máximo de caracteres que poderão ser lidos em uma operação de leitura
para um determinado tipo de entrada. Isso é muito útil quando queremos
limitar a quantidade de caracteres que serão lidos em uma string de modo
a não ultrapassar o tamanho máximo de armazenamento dela, ou quando
queremos limitar a quantidade de dı́gitos de uma valor como, por exemplo,
no caso do valor de um dia do mês (dois dı́gitos).
Os caracteres que ultrapassam o tamanho da largura determinado são descartados pela função scanf(), mas continuam no buffer do teclado. Uma outra chamada da função
scanf() irá considerar esses caracteres já contidos no buffer como parte do que será lido. Assim, para evitar confusões, é conveniente esvaziar o buffer do teclado com a
função fflush(stdin) a cada nova leitura.
Abaixo é possı́vel ver um exemplo de uso do campo largura:
329
Exemplo: limitando a quantidade de caracteres
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
12.5.4
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
int n;
p r i n t f ( ‘ ‘ D i g i t e um numero ( 2 d i g i t o s ) : ’ ’ ) ;
s c a n f ( ‘ ‘%2 d ’ ’ ,&n ) ;
p r i n t f ( ‘ ‘ Numero l i d o : %d\n ’ ’ , n ) ;
fflush ( stdin ) ;
char t e x t o [ 1 1 ] ;
p r i n t f ( ‘ ‘ D i g i t e uma p a l a v r a ( max : 10 c a r a c t e r e s ) : ’ ’ ) ;
s c a n f ( ‘ ‘%10 s ’ ’ , t e x t o ) ;
p r i n t f ( ‘ ‘ P a l a v r a l i d a : %s\n ’ ’ , t e x t o ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
OS “MODIFICADORES” DOS TIPOS DE ENTRADA
Os “modificadores” dos tipos de entrada se assemelham ao campo “comprimento” da função printf(). Eles são utilizados para ler valores que sejam
do tipo short int, long int e long double, como mostra a tabela abaixo:
“modificadores”
h
l
L
Descrição
Para os tipos “%d” ou “%i”: o valor é interpretado
como short int
Para os tipos “%u”, “%o”, “%x” e “%X”: o valor é
interpretado como unsigned short int
Para os tipos “%d” ou “%i”: o valor é interpretado
como long int
Para os tipos “%u”, “%o”, “%x” e “%X”: o valor é
interpretado como unsigned long int
Para os tipos “%f”, “%e”, “%E”, “%g” e “%G”: o
valor é interpretado como long double
Para os tipos “%f”, “%e”, “%E”, “%g” e “%G”: o
valor é interpretado como long double
Deve-se tomar cuidado com o uso desses “modificadores”, pois eles não
funcionam corretamente dependendo do compilador e da plataforma utilizada.
330
12.5.5
LENDO E DESCARTANDO CARACTERES
Por definição, a função scanf() descarta qualquer espaço em branco que
for digitado pelo usuário. Mesmo os espaços em branco adicionados junto
ao tipo de entrada não possuem efeito, o que faz com que as duas chamadas da função abaixo possuam o mesmo efeito: ler dois valores de inteiro:
int x, y;
scanf(“%d%d”,&x,&y);
scanf(“%d %d”,&x,&y);
Porém, qualquer caractere que não seja um espaço em branco que for digitado junto ao tipo de entrada faz com que a função scanf() exija a leitura
desse caractere e o descarte em seguida. Isso é interessante quando
queremos que seja feita a entrada de dados em uma formatação prédeterminada, como uma data. Por exemplo, “%d / %d / %d” faz com
que a função scanf() leia um inteiro, uma barra (que será descartada), outro valor inteiro, outra barra (que também será descartada) e , por fim, o
terceiro último inteiro.
Se o caractere a ser lido e descartado não é encontrado,
a função scanf() irá terminar, sendo os dados lidos de maneira incorreta.
Abaixo é possı́vel ver esse exemplo em ação:
Exemplo: lendo e descartando caracteres
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
i n t d ,m, a ;
p r i n t f ( ‘ ‘ D i g i t e a data no f o r m a t o d i a / mes / ano :
s c a n f ( ‘ ‘ % d/%d/%d ’ ’ ,&d ,&m,& a ) ;
p r i n t f ( ‘ ‘ % d − %d − %d\n ’ ’ , d ,m, a ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
331
’ ’);
12.5.6
LENDO APENAS CARACTERES PRÉ-DETERMINADOS
A função scanf() permite também que se defina uma lista de caracteres
pré-determinados, chamada de scanset, que poderão ser lidos do teclado
e armazenados em uma string. Essa lista é definida substituindo o tipo de
entrada %s, normalmente utilizado para a leitura de uma string, por %[],
onde, dentro dos colchetes, é definida a lista de caracteres que poderão
ser lidos pela função scanf(). Assim, se quisessemos ler uma string contendo apenas vogais, a função scanf() seria usada como mostrado abaixo:
Exemplo: lendo apenas caracteres pré-determinados
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 2 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e algumas v o g a i s :
s c a n f ( ‘ ‘ % [ aeiou ] ’ ’ , t e x t o ) ;
p r i n t f ( ‘ ‘ Texto : %s\n ’ ’ , t e x t o ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
Note que dentro da lista de caracteres pré-determinados foram digitadas as
vogais minúsculas. A linguagem C considera diferente letras maiúsculas
e minúsculas.
Se um dos caracteres digitados não fizer parte da lista de
caracteres pré-determinados (scanset) a leitura da string
é terminada e a função scanf() passa para o próximo tipo
de entrada, se houver.
USANDO UM INTERVALO DE CARACTERES PRÉ-DETERMINADOS
Ao invés de definir uma lista de caracteres, pode-se definir um intervalo
de caracteres, como por exemplo, todas as letras minúsculas, ou todos os
dı́gitos numéricos. Para fazer isso, basta colocar o primeiro e o último caracteres do intervalo separados por um hı́fen. Assim, se quiséssemos ler
apenas os caracteres de A a Z, a função scanf() ficaria como no exemplo
abaixo:
332
Exemplo: usando um intervalo de caracteres pré-determinados
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 2 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e algumas l e t r a s :
s c a n f ( ‘ ‘ % [ A−Z ] ’ ’ , t e x t o ) ;
p r i n t f ( ‘ ‘ Texto : %s\n ’ ’ , t e x t o ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
Pode-se ainda especificar mais de um intervalo de caracteres pré-determinados.
Para fazer isso, basta colocar o primeiro e o último caracteres do segundo
intervalo, separados por um hı́fen, logo após definir o primeiro intervalo.
Assim, se quiséssemos ler apenas os caracteres de A a Z e os dı́gitos de
0 a 9, a função scanf() ficaria como no exemplo abaixo:
Exemplo: usando mais de um intervalo de caracteres
1
2
3
4
5
6
7
8
9
10
12.6
# include <s t d i o . h>
# include < s t d l i b . h>
i n t main ( ) {
char t e x t o [ 2 0 ] ;
p r i n t f ( ‘ ‘ D i g i t e l e t r a s e n úmeros :
s c a n f ( ‘ ‘ % [ A−Z0−9] ’ ’ , t e x t o ) ;
p r i n t f ( ‘ ‘ Texto : %s\n ’ ’ , t e x t o ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
’ ’);
CLASSES DE ARMAZENAMENTO DE VARIÁVEIS
A linguagem C possui um conjunto de modificadores, chamados classes
de armazenamento, que permitem alterar a maneira como o compilador
vai armazenar uma variável.
As classes de armazenamento são utilizados para definir o
escopo e tempo de vida das variáveis dentro do programa.
333
Basicamente, as classes de armazenamento definem a acessibilidade de
uma variável dentro da linguagem C. Ao todo, existem quatro classes de
armazenamento:
• auto
• extern
• static
• register
12.6.1
A CLASSE AUTO
A classe de armazenamento auto permite definir variáveis locais. Nele,
as variáveis são automaticamente alocadas no inı́cio de uma função/bloco
de comandos, e automaticamente liberadas quando essa função/bloco de
comandos termina. Trata-se do modo padrão de definição de variáveis,
por esse motivo ela raramente é usada. Por exemplo, as duas variáveis
abaixo
int x;
auto int y;
possuem a mesma classe de armazenamento (auto).
A classe auto só pode ser utilizada dentro de funções e
blocos de comandos definidos por um conjunto de chaves
{}(escopo local).
12.6.2
A CLASSE EXTERN
A classe de armazenamento extern permite definir variáveis globais que
serão visı́veis em mais de um arquivo do programa. Ao contrário dos programas escritos até aqui, podemos escrever programas que podem ser
divididos em vários arquivos, os quais podem ser compilados separadamente.
Imagine que temos o seguinte trecho de código:
334
int soma = 0;
int main(){
escreve();
return 0;
}
Agora imagine que queiramos usar a variável global soma em um segundo
arquivo do nosso programa. Para fazer isso, basta adicionar a palavra
extern na declaração da variável para o comilador entender que ela já foi
definida em outro arquivo:
extern int soma;
void escreve(){
printf(“Soma = %d ”,soma);
}
Ao colocar a palavra extern antes da declaração da
variável soma, não estamos declarando uma nova variável,
mas apenas informando ao compilador que ela existe em
outro local de armazenamento previamente definido. Por
esse motivo, ela NÃO pode ser inicializada.
12.6.3
A CLASSE STATIC
O funcionamento da classe de armazenamento static depende de como
ela é utilizada dentro do programa. A classe static é o modo padrão de
definição de variáveis globais, ou seja, variáveis que existem durante todo
o tempo de vida do programa. Por esse motivo ela raramente é usada na
declaração de variáveis globais, como mostra o exemplo abaixo:
335
Exemplo: variáveis globais com static
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
i n t x = 20;
s t a t i c y = 10;
i n t main ( ) {
p r i n t f ( ‘ ‘ x = %d\n ’ ’ , x ) ;
p r i n t f ( ‘ ‘ y = %d\n ’ ’ , y ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, ambas as variáveis x e y possuem a mesma classe de
armazenamento (static).
A classe static também pode ser utilizada com variáveis locais, como as
variáveis definidas dentro de uma função. Nesse caso, a variável é inicializada em tempo de compilação e o valor da inicialização deve ser uma
constante. Uma variável local definida dessa maneira irá manter o seu valor entre as diferentes chamadas da função, portanto deve-se tomar muito
cuidado com a sua utilização, como mostra o exemplo abaixo:
Exemplo: variáveis locais com static
1
2
3
4
5
6
7
8
9
10
11
12
13
# include <s t d i o . h>
# include < s t d l i b . h>
void imprime ( ) {
static n = 0;
p r i n t f ( ‘ ‘ % d\n ’ ’ , n++) ;
}
i n t main ( ) {
int i ;
f o r ( i =1; i <=10; i ++)
imprime ( ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
No exemplo acima, o valor da variável n será diferente para cada chamada
da função imprime().
Por fim, a classe static também pode ser utilizada para definir funções,
como mostra o exemplo abaixo:
336
Exemplo: funções com static
1
2
3
4
5
6
7
8
9
10
# include <s t d i o . h>
# include < s t d l i b . h>
s t a t i c void imprime ( ) {
p r i n t f ( ‘ ‘ Executando funcao imprime ( ) \n ’ ’ ) ;
}
i n t main ( ) {
imprime ( ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
Uma função é, por padrão, da classe de armazenamento extern, ou seja,
as funções são visı́veis em mais de um arquivo do programa. Ao definirmos uma função como static estamos garantindo que ela seja visı́vel apenas dentro daquele arquivo do programa, ou seja, apenas funções dentro
daquele arquivo poderão ver uma função static.
12.6.4
A CLASSE REGISTER
A classe de armazenamento register serve para especificar que uma variável
será muito utilizada e que seria interessante armazená-la no registrador da
CPU do computador. Isso por que o tempo de acesso aos registradores
da CPU é muito menor que o tempo de acesso a memória RAM, onde as
variáveis ficam normalmente armazenadas. Uma variável da classe register é declarada como mostrado abaixo:
register int y;
Algumas considerações são necessárias sobre a classe register:
• não se pode usar o operador de endereço &. Isso por que a variável
está no registrador, e não mais na memória;
• o tamanho da variável é limitado pelo tamanho do registrador, portanto apenas variáveis de tipos pequenos (que ocupem poucos bytes) podem ser definidas como da classe register;
A classe de armazenamento register pode ser entendida
como uma dica de armazenamento que damos para o compilador. O compilador é livre para decidir se vai ou não
armazenar essa variável no registrador.
337
Se o compilador decidir ignorar classe register, a variável será definida
como sendo da classe auto. Isso significa que não podemos definir uma
variável global (static) como sendo da classe register.
A classe de armazenamento register é raramente utilizada. Os compiladores modernos fazem trabalhos de
otimização na alocação de variáveis melhores que os programadores.
12.7
TRABALHANDO COM CAMPOS DE BITS
A linguagem C possui meios de acessar diretamente os bits, ou um único
bit, dentro de um byte, sem fazer uso dos operadores bit-a-bit. Para isso,
ela conta com um tipo especial de membro de estruturas e uniões chamado
de campo de bits, ou bitfield. Seu uso é extremamente útil quando a
quantidade de memória para armazenamento de dados é limitada. Nesse
caso, várias informações podem ser armazenadas em um único byte, como
as “flags” indicando se determinado item do sistema está ativo (1) ou não
(0). Os campos de bits podem ainda ser utilizados para a leitura de arquivos externos, em especial, formatos não-padrão de arquivo como valores
de tipos inteiros com 9 bits. Outro uso frequente dos campos de bits são
para realizar a comunicação (entrada e saı́da de dados) com dispositivos
de hardware.
Campos de bits só podem ser utilizados em variáveis que
são membros de structs ou unions.
A forma geral de declaração de uma variável com campo de bit como membro de uma struct (ou union) segue o padrão abaixo:
tipo nome campo: comprimento;
Note que a declaração de uma variável com campo de bit é semelhante
a declaração de uma variável membro de uma struct/union, possuindo
apenas como informação extra o valor do comprimento (definido após os
dois pontos), que nada mais é do que a quantidade de bits que o campo irá
possuir. O valor do comprimento pode ser um número ou uma expressão
constante. Note ainda que o comprimento de um campo de bits não deve
exceder o número total de bits do tipo da variável utilizada na declaração.
338
Campos de bits só podem ser declarados como sendo do
tipo int, sendo possı́vel utilizar os modificadores signed e
unsigned. Se ele for do tipo int ou signed int, seu comprimento deverá ser maior do que um (1).
Se a variável com campo de bit for do tipo int ou signed int, ela irá possuir,
obrigatoriamente, um bit de sinal. Um campo de bit de comprimento um (1)
não pode ter sinal sendo necessário, portanto, um comprimento mı́nimo de
dois (2) bits. De qualquer modo, é aconselhável sempre utilizar campos de
bits com o tipo unsigned int.
Abaixo é possı́vel ver um exemplo de uma estrutura contendo variáveis
com campo de bits:
struct status{
unsigned int ligado:1;
signed int valor:4;
unsigned int :3;
};
Na estrutura acima, temos três campos de bits: ligado (1 bit), valor (4
bits), e um terceiro campo sem nome de tamanho 3 bits. Como o campo
ligado possui apenas 1 bit, seus valores possı́veis são 0 (desligado) ou 1
(ligado). Já o campo valor possui 4 bits, portanto, seus valores podem ir
de -8 até 7. Por fim, temos um campo de bits sem nome e de tamanho 3
bits. Note que esses 3 bits servem apenas para completar um total de 8
bits na estrutura.
Campos de bits sem nome são úteis para prencher uma
estrutura de modo a fazer com que ela esteja adequada a
um layout de especificado.
Os membros de uma estrutura que não são campos de bits estão sempre
alinhados aos limites dos bytes na memória. Os campos de bit sem nome
permitem criar lacunas não identificadas no armazenamento da estrutura,
completando os bytes e mantendo o alinhamento dos dados na memória.
Por fim, campos de bits sem nome não pode ser acessados ou inicializado.
Campos de bits podem ter comprimento ZERO (0). Neste
caso eles não podem possuir um nome. Sua função é a de
alinhamento dos bits.
339
Um campo de bits de comprimento ZERO (0) faz com que o próximo campo
de bits seja alinhado com o próximo byte de memória do mesmo tipo do
campo de bits. Em outras palavras, um campo de bits de comprimento
ZERO (0) indica que nenhum campo de bits adicional devem ser colocado
dentro desse byte.
Os membros de uma estrutura com campos de bits não
possuem endereços, e como tal não podem ser usados
com o operador de endereço (&). Por esse motivo, não
podemos ter ponteiros ou arrays deles. O operador sizeof
também não pode ser aplicado a campos de bits.
Abaixo tem-se um exemplo de uso de campos de bits:
Exemplo: trabalhando com campos de bits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
12.8
# include <s t d i o . h>
# include < s t d l i b . h>
struct status {
unsigned i n t l i g a d o : 1 ;
signed i n t v a l o r : 4 ;
unsigned i n t : 3 ;
};
void c h e c k s t a t u s ( s t r u c t s t a t u s s ) {
i f ( s . l i g a d o == 1 )
p r i n t f ( ‘ ‘ LIGADO\n ’ ’ ) ;
i f ( s . l i g a d o == 0 )
p r i n t f ( ‘ ‘ DESLIGADO\n ’ ’ ) ;
}
i n t main ( ) {
s t r u c t s t a t u s ESTADO;
ESTADO. l i g a d o = 1 ;
c h e c k s t a t u s (ESTADO) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
O MODIFICADOR DE TIPO “VOLATILE”
A linguagem C possui mais um modificador de tipos de variáveis. Trata-se
do modificador volatile. Sua forma geral de uso é
volatile tipo variável nome variável;
340
O modificador volatile pode ser aplicado a qualquer
declaração de variável, incluindo as estruturas, uniões e
enumerações.
O modificador volatile informa ao compilador que aquela variável poderá
ser alterada por outros meios e, por esse motivo, ela NÃO deve ser otimizada. O principal motivo para o seu uso tem a ver com problemas que
trabalham com sistemas dinâmicos, em tempo real ou com comunicação
com algum dispositivo de hardware que esteja mapeado na memória.
O modificador volatile diz ao compilador para não otimizar
qualquer coisa relacionada àquela variável.
Para entender melhor esse modificador, considere o seguinte trecho de
código:
int reposta;
void espera(){
reposta = 0;
while(reposta != 255);//laço infinito
}
Um compilador que seja otimizado irá notar que nenhum outro código pode
modificar o valor da variável resposta dentro da função espera(). Assim,
o compilador irá assumir que o valor armazenado em resposta é sempre
ZERO e, como nunca é modificado, esse laço é infinito. Por ser otimizado, o
compilador poderá substituir a condição do comando while por UM (1), indicando assim também um laço infinito, mas economizando a comparação
da variável resposta, como mostra o trecho de código abaixo:
int reposta;
void espera(){
reposta = 0;
while(1);//laço infinito
}
341
No entanto, vamos supor que a variável resposta possa ser modificada, a
qualquer momento, por um dispositivo de hardware mapeado na memória
RAM. Nesse caso, o valor da variável pode ser modificado enquanto ela
estiver sendo testada no comando while, finalizando o laço. Portanto, não
é interessante para o programa que esse laço seja otimizado e considerado
sempre como um laço infinito. Para impedir que o compilador faça esse tipo
de otimização, utilizamos o modificador volatile:
volatile int reposta;//variável não otimizada
void espera(){
reposta = 0;
while(reposta != 255);//laço pode não ser infinito
}
Com o modificador volatile a condição do laço não será otimizada, e o
sistema irá detectar qualquer alteração nela quando está ocorrer. Porém,
pode ser um exagero marcar uma variável como volatile. Isso porque
esse modificador desativa qualquer otimização na variável. Uma alternativa muito mais eficiente é utilizar de type cast sempre que não quisermos,
e apenas onde é necessário, otimizar a variável:
int reposta;
void espera(){
reposta = 0;
while(*(volatile int *)&reposta != 255);//laço pode não ser infinito
}
12.9
FUNÇÕES COM NÚMERO DE PARÂMETROS VARIÁVEL
Vimos na Seção 8 como criar nossas próprias funções. Vimos também
que é por meio dos parâmetros de uma função que o programador pode
passar a informação de um trecho de código para dentro da função. Esses parâmetros são uma lista de variáveis, separadas por vı́rgula, onde é
especificado o tipo e o nome de cada variável passada para a função.
No entanto, algumas funções, como a função printf(), podem ser utilizadas com um, dois, três, ou até mais parâmetros, como mostra o exemplo
abaixo:
342
Exemplo: printf() com vários parâmetros
1 # include <s t d i o . h>
2 # include < s t d l i b . h>
3 i n t main ( ) {
4
i n t x = 1 , y =2;
5
float z = 3;
6
p r i n t f ( ‘ ‘Um parametro : t e x t o \n ’ ’ ) ;
7
p r i n t f ( ‘ ‘ Dois parametros : t e x t o e %d\n ’ ’ , x ) ;
8
p r i n t f ( ‘ ‘ Tres parametros : t e x t o , %d e %d\n ’ ’ , x , y ) ;
9
p r i n t f ( ‘ ‘ Quatro parametros : t e x t o , %d , %d e %f \n ’ ’ , x ,
y,z) ;
10
system ( ‘ ‘ pause ’ ’ ) ;
11
return 0;
12 }
A linguagem C permite escrever funções que aceitam uma quantidade
variável de parâmetros, onde esses parâmetros podem ser de diversos
tipos, como é o caso das funções printf() e scanf(). A declaração, pelo
programador, de uma função com uma quantidade variável de parâmetros
segue a seguinte forma geral:
tipo retornado nome função (nome tipo nome parâmetros, ...){
sequência de declarações e comandos
}
Para declarar uma função com uma quantidade variável
de parâmetros basta colocar “...” como sendo o último
parâmetro na declaração da função.
São os “...” declarados nos parãmetros da função que informam ao compilador que aquela função aceita uma quantidade variável de parâmetros.
Uma função com uma quantidade variável de parâmetros
deve possuir pelo menos um parâmetro “normal” antes
dos “...”, ou seja, antes da parte variável.
Isso é necessário pois a função agora não sabe quantos parâmetros serão
passados para ela, nem os seus tipos. Portanto, o primeiro parâmetro
deve ser usado para informar isso dentro da função. Daı́ a necessidade da
função possuir pelo menos um parâmetro. A função printf(), por exemplo,
343
sabe quantos parâmetros ela irá receber, e os seus tipos, por meio dos
tipos de saı́da presentes dentro do primeiro parâmetro: %c para char,
%d para int, etc.
Uma vez declarada uma função com uma quantidade
de parâmetros variável, é necessário acessar esse
parâmetros. Para isso, usamos a biblioteca stdarg.h.
A biblioteca stdarg.h possui as definições de tipos e macros necessárias
para acessar a lista de parâmetros da função. São eles:
• va list: este tipo é usado como um parâmetro para as macros definidas na biblioteca stdarg.h para recuperar os parâmetos adicionais
da função;
• va start(lista, ultimo parametro): esta macro inicializa uma variável
lista, do tipo va list, com as informações necessárias para recuperar os parâmetros adicionais, sendo ultimo parametro o último
parâmetro declarado na função antes do “...”;
• va arg(lista, tipo dado): esta macro retorna o parâmetro atual contido na variável lista, do tipo va list, sob a forma do tipo informado
em tipo dado. Em seguida, a macro move a variável lista para o
próximo parâmetro, se este existir. Assim, x = va arg(lista, float) irá
retornar para a variável x o valor do parâmetro atual em lista formatado para o tipo float;
• va end(lista): esta macro deve ser executada antes da finalização da
função (ou antes do comando return, se este existir). Seu objetivo é
destruir a variável lista, do tipo va list, de modo apropriado.
Funções com uma quantidade variável de parâmetros devem ser usadas com moderação.
Não devemos utilizar constantemente esse tipo de função pois existe um
potencial muito grande para que uma função projetada para se trabalhar
com um tipo, seja usada com outro. Isso ocorre por que não existe definição
de tipos na lista de parâmetros variável, apenas dentro da função na macro
va arg().
Funções com uma quantidade variável de parâmetros podem expor o programa a uma série de problemas de
segurança baseada em tipo (type-safety).
344
Isso ocorre pois esse tipo de função não possui segurança baseada em
tipo (type-safety). A função permite que se tente recuperar mais parâmetros
do que foram passados, corrompendo assim o funcionamento do programa
que poderá apresentar um comportamento inesperado. A função printf(),
por exemplo, pode ser usada para ataques. Um usuário mal-intencionado
pode usar os tipos de saı́da %o e %x, entre outros, para imprimir os dados
de outras posições da memória
O exemplo abaixo apresenta uma função que retorna a soma de uma quantidade variável de parâmetros inteiros. Note que o primeiro parâmetro, n,
é o número de parâmetros que virão em seguida:
Exemplo: soma de uma quantidade variável de parâmetros
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# include <s t d i o . h>
# include < s t d l i b . h>
# include <s t d a r g . h>
i n t soma int ( i n t n , . . . ) {
va list lista ;
int i , s = 0;
va start ( lista ,n) ;
f o r ( i = 1 ; i <= n ; i ++)
s = s + va arg ( l i s t a , i n t ) ;
va end ( l i s t a ) ;
return s ;
}
i n t main ( ) {
i n t soma ;
soma = s o m a i n t ( 2 , 4 , 5 ) ;
p r i n t f ( ”Soma 2 parametros : %d\n ” ,soma ) ;
soma = s o m a i n t ( 3 , 4 , 5 , 6 ) ;
p r i n t f ( ”Soma 3 parametros : %d\n ” ,soma ) ;
soma = s o m a i n t ( 4 , 4 , 5 , 6 , 1 0 ) ;
p r i n t f ( ”Soma 4 parametros : %d\n ” ,soma ) ;
system ( ‘ ‘ pause ’ ’ ) ;
return 0;
}
345