Academia.eduAcademia.edu

Apostila C

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”,&notas[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