como funciona a programação em c

87
Como funciona a programação em C por Marshall Brain - traduzido por HowStuffWorks Brasil Introdução A linguagem C é uma linguagem de programação popular e amplamente utilizada para criar programas de computador. Programadores no mundo todo utilizam a linguagem C, pois ela oferece o máximo controle e eficácia. Se você é um programador, ou pretende se tornar um, eis alguns benefícios que você desfrutará ao aprender a linguagem C: Você poderá ler e escrever código para diversas plataformas. Desde microcontroladores até sistemas científicos mais avançados podem ser criados em C e diversos sistemas operacionais modernos são escritos em C; o avanço para a linguagem C++ orientada para objeto torna-se muito mais fácil. A linguagem C++ é uma extensão de C e é praticamente impossível aprender C++ sem antes aprender C. Esta animação mostra a execução de um programa simples em C. Ao final deste artigo, você entenderá como ela funciona. Neste artigo, abordaremos toda a linguagem e mostraremos como se tornar um programador em C. Você ficará espantado com tudo o que pode criar ao aprender a linguagem C. O que é C? C é uma linguagem de programação de computadores. Isso significa que você pode usá-la para criar listas de instruções para um computador seguir. A linguagem C é uma dos milhares de linguagens de programação atualmente em uso. Existe há várias décadas e ganhou ampla aceitação por oferecer aos programadores

Upload: orlandoalberti

Post on 08-Aug-2015

73 views

Category:

Documents


2 download

TRANSCRIPT

Page 1: Como funciona a programação em C

Como funciona a programação em Cpor Marshall Brain - traduzido por HowStuffWorks Brasil

Introdução

A linguagem C é uma linguagem de programação popular e amplamente utilizada para criar programas de computador. Programadores no mundo todo utilizam a linguagem C, pois ela oferece o máximo controle e eficácia. Se você é um programador, ou pretende se tornar um, eis alguns benefícios que você desfrutará ao aprender a linguagem C:

Você poderá ler e escrever código para diversas plataformas. Desde microcontroladores até sistemas científicos mais avançados podem ser criados em C e diversos sistemas operacionais modernos são escritos em C; 

o avanço para a linguagem C++ orientada para objeto torna-se muito mais fácil. A linguagem C++ é uma extensão de C e é praticamente impossível aprender C++ sem antes aprender C.

Esta animação mostra a execução de um programa simples em C. Ao final deste artigo, você entenderá como ela funciona.

Neste artigo, abordaremos toda a linguagem e mostraremos como se tornar um programador em C. Você ficará espantado com tudo o que pode criar ao aprender a linguagem C.

 

O que é C?

C é uma linguagem de programação de computadores. Isso significa que você pode usá-la para criar listas de instruções para um computador seguir. A linguagem C é uma dos milhares de linguagens de programação atualmente em uso. Existe há várias décadas e ganhou ampla aceitação por oferecer aos programadores o máximo em controle e eficiência. A linguagem C é fácil de aprender: pode ter um estilo um tanto criptográfica comparada às outras linguagens, mas isso é logo superado.

Page 2: Como funciona a programação em C

A linguagem C é o que se chama de linguagem compilada. Isso significa que, uma vez escrito o programa em C, ele deve ser passado por um compilador para transformar seu programa em executável para o computador rodar (executar). Um programa em C possui um formato legível ao homem, enquanto o executável gerado no compilador possui a forma legível para a máquina e é executada por ela. Isto significa que para escrever e executar um programa em C, você precisa ter acesso a um compilador de C. Se estiver usando uma máquina UNIX (por exemplo, escrevendo scripts CGI em C no seu computador UNIX, ou se você é um estudante que trabalha em uma máquina UNIX de um laboratório), o compilador de C está disponível gratuitamente. Ele é chamado "cc" ou "gcc" e está disponível na linha de comando. Se você é um aluno, estão a escola lhe fornecerá um compilador (descubra qual a escola usa e aprenda mais sobre ele). Se estiver trabalhando em casa em uma máquina Windows, você precisará fazer o download de um compilador de C gratuito ou comprar um compilador comercial. Um compilador comercial amplamente utilizado é o ambiente Visual C++ da Microsoft (ele compila programas em C e C++). Mas, infelizmente, este programa custa caro. Caso não disponha de algumas centenas de dólares para gastar em um compilador comercial, você pode usar um dos compiladores gratuitos disponíveis na internet. Veja http://delorie.com/djgpp/ (em inglês) como ponto de partida em sua pesquisa.

Vamos começar com um programa em C bastante simples e progredir a partir dele. Vamos supor que você usará a linha de comando UNIX e o gcc como seu ambiente para estes exemplos, mas caso contrário, todos os códigos também funcionarão (basta compreender e usar o compilador que tiver disponível).

Vamos começar!

O mais simples programa em C

Page 3: Como funciona a programação em C

Vamos começar com o programa em C mais simples possível e usá-lo tanto para entender os fundamentos da linguagem C como o processo de compilação em C. Digite o programa seguinte em um editor de textos padrão (vi ou emacs no UNIX, Bloco de notas no Windows ou TeachText no Macintosh). Depois salve o programa em um arquivo denominado samp.c. Se deixar de incluir .c, você provavelmente receberá informação de algum tipo de erro ao compilá-lo (portanto, não se esqueça de inserir o .c). Certifique-se também de que o editor não anexe automaticamente caracteres extras (como .txt) ao nome do arquivo. Eis o primeiro programa: #include <stdio.h>

int main() { printf("Este é o resultado do meu primeiro program!\n"); return 0;}

Quando executado, este programa instrui o computador a imprimir a linha: "Este é o resultado do meu primeiro programa!", e depois encerra o programa.

PosiçãoAo escrever este programa, posicione #include de forma que o sinal # esteja na coluna 1 (mais à esquerda). Isso facilita o seu entendimento, mas na verdade o espaçamento e recuo podem ser do jeito que você preferir. Em alguns sistemas UNIX, você encontrará um programa chamado cb, o C Beautifier (algo como "embelezador de C"), que formata códigos. O espaçamento e recuo mostrados acima são um bom exemplo a seguir. 

Para compilar este código, siga estas etapas:

Em uma máquina UNIX, digite gcc samp.c -o samp (se gcc não funcionar, tente cc). Esta linha executa o compilador em C chamado gcc, pede que compile samp.c e que nomeie o arquivo executável criado como samp. Para executar o programa, digite samp (ou em algumas máquinas UNIX, ./samp).

Em uma máquina rodando DOS ou Windows e usando DJGPP (em inglês), digite o seguinte no prompt do MS-DOS: gcc samp.c -o samp.exe. Esta linha executa o compilador em C chamado gcc, Pede que compile samp.c e que nomeie o arquivo executável criado como samp.exe. Para executar o programa, digite samp.

Se estiver trabalhando com algum outro compilador ou sistema de desenvolvimento, leia e siga as instruções dele para compilar e executar o programa.

Você verá "Este é o resultado do meu primeiro programa!" ao executá-lo. Veja o que aconteceu ao compilar o programa:

Page 4: Como funciona a programação em C

Caso tenha cometido um erro de digitação no programa, ele não compilará ou não poderá ser executado. Se o programa não foi compilado ou não executa corretamente, edite-o e localize os erros de digitação. Corrija o erro e tente novamente.

O mais simples programa em C : o que está acontecendo?

Vamos analisar este programa e aprender o que as diferentes linhas de comando fazem: Este programa em C começa com #include <stdio.h>. Esta linha inclui uma

"biblioteca padrão de I/O" (entrada/saída) em seu programa, que permite ler a entrada a partir do teclado (denominada "entrada padrão"),  exibir o resultado em uma tela (denominada "saída padrão"), processar arquivos de texto armazenados em disco e assim por diante. É uma biblioteca extremamente útil. A linguagem C possui um grande número de bibliotecas padrão como stdio, incluindo bibliotecas de strings de caracteres, de horário e de funções matemáticas. Uma biblioteca é simplesmente um pacote de códigos que alguém escreveu anteriormente para simplificar a sua vida (discutiremos bibliotecas daqui a pouco).

A linha int main( ) declara a função principal. Todo programa em C deve ter uma função denominada main em algum lugar no código. Aprenderemos mais sobre as funções em breve. O programa começa a ser executado a partir da primeira linha da função main.

Em C, os símbolos { e } marcam o começo e término de um bloco de código. Neste caso, o bloco de código que compõe a função principal contém duas linhas.

A instrução printf em C permite enviar o resultado para a saída padrão (para nós, a tela). A parte entre aspas é denominada string de formato e descreve como os dados serão formatados quando impressos. A string de formato pode conter strings de caracteres como "Este é o resultado do meu primeiro

Page 5: Como funciona a programação em C

programa!", símbolos de quebra de linha (\n), e operadores como expressão de controle para variáveis (vide abaixo). Se você está usando UNIX, pode digitar man 3 printf para obter uma documentação completa sobre a função printf. Caso contrário, consulte a documentação incluída em seu compilador para mais detalhes sobre a função printf.

A linha return 0; faz com que a função retorne um código de erro de 0 (sem erros) à shell que iniciou a execução. Esta capacidade será discutida em detalhes mais tarde.

Variáveis

Como programador, você com freqüência desejará que seu programa se "lembre" de um valor. Por exemplo, se seu programa solicita um valor do usuário, ou se ele calcula um valor, você vai querer armazená-lo em algum lugar para usá-lo mais tarde. O modo como seu programa se lembra das coisas é por meio do uso de variáveis. Por exemplo: int b;

Esta linha diz: "Quero criar um espaço chamado b que possa conter um valor inteiro". Uma variável tem um nome (neste caso, b) e um tipo (neste caso, int, um inteiro). Você pode armazenar um valor em b escrevendo algo assim:

b = 5;

Você pode usar o valor em b escrevendo algo assim:

printf("%d", b);

Na linguagem C, há vários tipos padrões de variáveis:

int - valores inteiros (número inteiro) float - valores de ponto flutuante char - valores de caractere único (como "m" ou "Z")

Veremos exemplos destes outros tipos à medida que avançarmos.

Printf

A instrução printf permite enviar o resultado para a saída padrão. Para nós, o termo "saída padrão" se refere à tela (embora você possa redirecionar a saída padrão para um arquivo de texto ou outro comando).

Eis outro programa que o ajudará a aprender mais sobre printf:

#include <stdio.h>

int main( ) { int a, b, c; a = 5; b = 7; c = a + b; printf("%d + %d = %d\n", a, b, c); return 0; }

Digite este programa em um arquivo e salve-o como add.c. Compile-o com a linha gcc add.c -o add e depois execute-o digitando add (ou ./add). Você verá a linha "5 + 7 = 12" como resultado.

Eis uma explicação das diferentes linhas neste programa:

Page 6: Como funciona a programação em C

a linha int a, b, c; declara três variáveis de número inteiro denominadas a, b e c.

a próxima linha inicializa a variável nomeada a para o valor 5. a próxima linha define 7 para b . a próxima linha soma a e b e "atribui" o resultado a c.

O computador adiciona o valor em a (5) ao valor em b (7) para formar o resultado 12 e então coloca o novo valor (12) na variável c. Por este motivo, o sinal = nesta linha é denominado "operador de atribuição".

A instrução printf depois imprime a linha "5 + 7 = 12". As expressões de controle %d na instrução printf atuam como expressão de controle de valores. Há 3 expressões de controle %d, e no final da linha printf há três nomes de variável: a, b e c. A linguagem C liga o primeiro%d ao a e o substitui por 5. Ele liga o segundo %d com b e o substitui por 7, faz a correspondência do terceiro %d com c e o substitui por 12. Depois, imprime a linha completa na tela: 5 + 7 = 12. O +, o = e o espaçamento são parte da linha de formato e são automaticamente integrados entre os operadores %d conforme especificado pelo programador.

Printf: lendo os valores de usuário

O programa anterior é bom, mas seria melhor se ele lesse os valores 5 e 7 inseridos pelo usuário, em vez de usar constantes. Em vez disso, tente este programa:#include <stdio.h>

int main(){ int a, b, c; printf("Entre o primeiro valor:"); scanf("%d", &a); printf("Entre o segundo valor:"); scanf("%d", &b); c = a + b; printf("%d + %d = %d\n", a, b, c); return 0; }

Eis como este programa funciona ao ser executado:

Faça as alterações, depois compile e rode o programa para certificar-se de que funciona. Observe quescanf usa as mesmas strings de formato que printf (digite man scanf para mais informação). Observe também o & na frente de a e b. Este é o operador de endereço em C: ele retorna o endereço da variável

Page 7: Como funciona a programação em C

(isto não fará sentido até aprendermos sobre os ponteiros). Você tem de usar o operador & em scanf em qualquer variável do tipo char, int ou float, bem como tipos de estrutura (que discutiremos em breve). Se excluir o operador &, você receberá um erro ao executar o programa. Tente executá-lo para ver que tipo de erro ocorre.

Vamos ver algumas variações para entender printf completamente. Eis uma instrução simples de printf:

printf("Hello");

Esta chamada para printf tem uma string de formatos que diz ao printf para enviar a palavra "Hello" para a saída padrão. Compare-a com:

printf("Hello\n");

A diferença entre as duas é que a segunda versão exibe a palavra "Hello" seguida de uma quebra de linha.

As linha seguintes mostram como exibir o valor de uma variável usando printf.

printf("%d", b);

O %d é uma expressão de controle que será substituída pelo valor da variável b quando a instrução printf for executada. Freqüentemente, você vai desejar incluir o valor entre outras palavras. Uma forma de fazer isso é:

printf("A temperatura é "); printf("%d", b); printf(" degrees");

Um modo mais fácil de dizer isso é:

printf("A temperatura é %d graus\n", b);

Você também pode usar múltiplas expressões de controle %d em uma instrução printf:

printf("%d + %d = %d\n", a, b, c);

Na instrução printf, é extremamente importante que o número de operadores na string de formato corresponda exatamente ao número e tipo de variáveis que a seguem. Por exemplo, se a string de formatos contém três operadores %d, então ela deve ser seguida por exatamente três parâmetros do mesmo tipo e ordem que aqueles especificados pelos operadores.

Você pode imprimir todos os símbolos normais na linguagem C com printf usando expressões de controle diferentes:

int (valores inteiros) usam %d float (valores de ponto flutuante) usam %f char (valores de caractere simples) usam %c strings de caracteres (matrizes de caracteres, discutiremos mais

tarde) usam %sVocê pode aprender mais sobre as nuances do printf em uma máquina UNIX digitando man 3 printf. Qualquer outro compilador de C que você utilize provavelmente virá acompanhado de um manual ou arquivo de ajuda que contém uma descrição do printf.

Scanf

A função scanf permite aceitar entradas do dispositivo padrão, que, para nós, é geralmente o teclado. A função scanf pode fazer muitas coisas diferentes, mas pode ter resultados incertos quando não usada de forma simples. É falível pois não lida

Page 8: Como funciona a programação em C

muito bem com erros humanos. Mas para programas simples, ela é boa o suficiente e fácil de usar.

A aplicação mais simples de scanf se parece com:

scanf("%d", &b);

O programa lerá um valor inteiro digitado pelo usuário usando o teclado (%d é para inteiros, como em printf, assim b deve ser declarado como um int) e o colocará em b.

A função scanf usa as mesmas expressões de controle da printf:

int usa %d float usa %f char usa %c strings de caracteres (abordados mais tarde) usam %s

Você DEVE colocar & na frente da variável usada em scanf. A razão para isso ficará clara assim que você aprender sobre os ponteiros. É fácil esquecer o sinal &, e se você esquecer, seu programa quase sempre apresentará problemas ao ser executado.

Em geral, é melhor usar scanf como mostrado aqui, lendo apenas um valor do teclado. Use múltiplas chamadas do scanf para ler valores múltiplos. Em qualquer programa real, você usará as funções getsou fgets em vez de ler o texto em uma linha por vez. Então você fará a "análise" da linha para ler seus valores. Isso serve para detectar erros na entrada e controlá-los da maneira que achar adequada.

As funções printf e scanf exigirão um pouco de prática para serem inteiramente compreendidas, mas uma vez dominadas serão extremamente úteis.

Tente isto! Modifique este programa para que ele aceite 3

valores em vez de 2 e some todos eles juntos: #include <stdio.h> int main() { int a, b, c; printf("Entre o primeiro valor:"); scanf("%d", &a); printf("Entre o segundo valor:"); scanf("%d", &b); c = a + b; printf("%d + %d = %d\n", a, b, c); return 0;

} Tente apagar ou adicionar caracteres ou

palavras aleatórias em um dos programas anteriores e veja como o compilador reage a tais erros de compilação.

Por exemplo, apague a variável b na primeira linha do programa anterior e veja o que o compilador faz quando você se esquece de declarar uma variável. Apague um ponto-e-vírgula e veja o que acontece. Omita uma das chaves. Remova um dos parênteses próximos à função principal. Faça uma alteração por vez e compile o programa para ver o que acontece. Simulando erros como esses você pode aprender sobre diferentes erros de compilação, o que facilitará futuras detecções

Page 9: Como funciona a programação em C

quando você os cometer.

Erros a serem evitados na linguagem C Usar letras maiúsculas e minúsculas

aleatoriamente. Letras maiúsculas e minúsculas são importantes na linguagem C, portanto você não pode digitar Printf ouPRINTF. É obrigatório que seja printf.

Esquecer de usar o & em scanf. Parâmetros em excesso ou a falta deles após

a instrução de formato em printf ou scanf. Esquecer de declarar o nome de uma variável

antes de utilizá-la.

Desvio e looping

Em C, as instruções if e loops while regem-se pelos princípios das expressões Booleanas. Eis um programa simples em C que demonstra uma instrução if:  

#include <stdio.h>

int main(){    int b;    printf("Digite um valor:");    scanf("%d", &b);    if (b < 0)        printf("O valor é negativo ");    return 0;}

Este programa aceita um número do usuário. Ele então testa esse número utilizando uma instrução if para ver se ele é menor que 0. Se for, o programa imprime uma mensagem. Caso contrário, o programa não faz nada. A parte (b < 0) do programa é a expressão booleana. A linguagem C avalia esta expressão para decidir se imprime ou não a mensagem. Se a expressão booleana se mostraVerdadeira, então a linguagem C executa a linha imediatamente posterior à instrução if (ou um bloco de linhas entre chaves logo após a instrução if). Se a expressão booleana se mostrar Falsa, então a linguagem C pula a linha ou bloco de linhas logo após a instrução if.

Page 10: Como funciona a programação em C

Eis um exemplo um pouco mais complexo:

#include <stdio.h>

int main(){    int b;    printf("Digite um valor:");    scanf("%d", &b);    if (b < 0)        printf("O valor é negativo ");    else if (b == 0)        printf("O valor é zero ");    else        printf("O valor é positivo ");    return 0;}

Neste exemplo, as seções else if e else avaliam tanto para valores positivos como zero.

Eis uma expressão booleana mais complicada:

if ((x==y) && (j>k))    z=1;

Page 11: Como funciona a programação em C

else    q=10;

Esta instrução diz: "Se o valor da variável x for igual ao valor da variável y, e se o valor da variável j for maior que o valor da variável k, então defina a variável z como 1; de outro modo, defina a variável q como 10". Você usará instruções if como esta em todos os seus programas C para tomar decisões. De um modo geral, a maioria das decisões será simples como o primeiro exemplo, mas, eventualmente, as coisas podem ser um pouco mais complicadas.

Observe que a linguagem C utiliza == para testar a igualdade, enquanto utiliza = para atribuir um valor a uma variável. O símbolo && em C representa uma operação booleana   AND .

Aqui estão todos os operadores booleanos em C:

igualdade == menor que < maior que > menor ou igual <= maior ou igual >= desigualdade != e && ou || não !

Você descobrirá que as instruções while são tão fáceis de utilizar como as instruções if. Por exemplo:

while (a < b){ print("%d\n", a); a = a + 1;}

Isso faz com que duas linhas entre chaves sejam executadas repetidamente até que a seja maior ou igual a b. Em geral, a instrução while funciona assim:

A linguagem C também oferece uma estrutura do-while:

do { printf("%d\n", a); a = a + 1; } while (a < b);

Page 12: Como funciona a programação em C

O loop for na linguagem C é apenas um atalho para expressar uma instrução while. Por exemplo, suponha que você tenha o seguinte código em C:

x=1; while (x<10) { blá blá blá x++; /* x++ é o mesmo que dizer x=x+1 */ }

Você pode converter isso em um loop for da seguinte forma:

for(x=1; x<10; x++) { blá blá blá }

Observe que o loop while contém uma etapa de inicialização (x=1), uma etapa de teste (x<10) e uma de incremento (x++). O loop for permite colocar as três partes em uma única linha, mas você pode colocar qualquer coisa nelas. Por exemplo, suponha que você tenha o seguinte loop:

a=1; b=6; while (a < b) { a++; printf("%d\n",a); }

Você também pode colocá-lo para indicação:

for (a=1,b=6; a < b; a++,printf("%dn",a));

É um pouco confuso, mas é possível. O operador vírgula permite separar diversas instruções diferentes nas seções de inicialização e incremento do loop for (porém não na seção de teste). Muitos programadores de linguagem C gostam de concentrar muitas informações em uma única linha de código. Outros acham que isto torna o código mais difícil de entender, e portanto desmembram essas instruções.

= versus == em expressões booleanasO sinal == é um problema na linguagem C pois freqüentemente você se esquece e digita apenas = em uma expressão booleana. Este é um erro comum de ocorrer, mas para o compilador há uma diferença significativa. A linguagem C aceitará = e == em uma expressão booleana, mas o comportamento do programa mudará consideravelmente quando usamos um ou outro.

As expressões booleanas em C avaliam os inteiros e os inteiros podem ser usados dentro de expressões booleanas. O valor inteiro 0 em C é Falso, enquanto qualquer outro valor inteiro é Verdadeiro. É código seguinte é permitido em C:

#include <stdio.h>

int main() { int a; printf("Digite um número:"); scanf("%d", &a); if (a) {

Page 13: Como funciona a programação em C

printf("O valor é verdadeiro\n"); } return 0; }

Se a for qualquer número diferente de 0, a instrução printf é executada.

Em C, uma instrução como if (a=b) significa: “Atribuir b para a, e então testar a para seu valor Booleano". Assim, se a retornar o valor 0, a instrução if é Falsa. Caso contrário, é Verdadeira. O valor de a muda durante o processo. Este não é o comportamento desejado se você pretendeu digitar == (embora esta característica seja útil quando usada corretamente), assim, tenha cuidado com o uso de = e ==. 

Looping: um exemplo real

Suponhamos que você queira criar um programa que imprima uma tabela de conversão Fahrenheit para Celsius. Isso pode ser facilmente obtido com um loop for ou loop while:#include <stdio.h> int main(){ int a; a = 0; while (a <= 100) { printf("%4d graus F = %4d graus C\n", a, (a - 32) * 5 / 9); a = a + 10; } return 0;}

Se executar este programa, ele produzirá uma tabela de valores iniciando em 0 graus F e terminando em 100 graus F. O resultado será este:

0 graus F = -17 graus C 10 graus F = -12 graus C20 graus F = -6 graus C30 graus F = -1 graus C40 graus F = 4 graus C 50 graus F = 10 graus C 60 graus F = 15 graus C 70 graus F = 21 graus C 80 graus F = 26 graus C 90 graus F = 32 graus C 100 graus F = 37 graus C

Os valores da tabela estão em incrementos de 10 graus. Observe como é fácil alterar os valores iniciais, finais ou de incremento da tabela que o programa produz.

Para valores mais precisos, você pode usar valores de ponto flutuante:

#include <stdio.h>

int main(){ float a;

Page 14: Como funciona a programação em C

a = 0; while (a <= 100) { printf("%6.2f graus F = %6.2f graus C\n", a, (a - 32.0) * 5.0 / 9.0); a = a + 10; } return 0; }

Você pode ver que a declaração de a foi alterada para flutuante, e o símbolo %f substitui o símbolo %dna instrução printf. Além disso, o símbolo %f possui alguma formatação: o valor será impresso com 6 dígitos inteiros e 2 decimais.

Agora vamos supor que queiramos modificar o programa para que a temperatura 98.6 seja inserida na tabela na posição adequada. Isto é, queremos que a tabela aumente a cada 10 graus e que inclua uma linha extra para 98.6 graus F, que é a temperatura corpórea normal do ser humano. O programa seguinte atende essa finalidade:

#include <stdio.h>

int main() { float a; a = 0; while (a <= 100) { if (a > 98.6) { printf("%6.2f graus F = %6.2f graus C\n", 98.6, (98.6 - 32.0) * 5.0 / 9.0); } printf("%6.2f graus F = %6.2f graus C\n", a, (a - 32.0) * 5.0 / 9.0); a = a + 10; } return 0; }

Este programa funciona se o valor final for 100, mas se você alterar o valor final para 200, verá que o programa tem um bug. Ele imprime a linha de 98.6° várias vezes. Este problema pode ser corrigido de várias formas. Eis uma:

#include <stdio.h>

int main(){ float a, b; a = 0; b = -1; while (a <= 100) { if ((a > 98.6) && (b < 98.6)) { printf("%6.2f graus F = %6.2f graus C\n", 98.6, (98.6 - 32.0) * 5.0 / 9.0); } printf("%6.2f graus F = %6.2f graus C\n", a, (a - 32.0) * 5.0 / 9.0); b = a;

Page 15: Como funciona a programação em C

a = a + 10; } return 0;}

Tente isto Tente alterar o programa conversor

Fahrenheit-Celsius de modo que ele utilize scanf para aceitar o valor inicial, final e de incremento daquele usuário.

Adicione uma linha de cabeçalho à tabela que é gerada.

Tente encontrar uma solução diferente para o bug corrigido pelo exemplo anterior.

Crie uma tabela que converta libras em quilogramas ou milhas em quilômetros.

Erros a serem evitados na linguagem C

Colocar = quando se deseja == em uma instrução if ou while.

Esquecer de aumentar o contador dentro do loop while. Isso causa um loop infinito (o loop nunca acaba).

Acidentalmente colocar um ; no final de um loop for ou instrução if, pois isto anula a instrução. Por exemplo:

for (x=1; x<10; x++); printf("%d\n",x);

imprime apenas um valor, pois o ponto-e-vírgula após a instrução for atua como uma linha para a execução do loop for.

Matrizes

Nesta seção, criaremos um pequeno programa em C que gera 10 números aleatórios e os ordena. Para tal, utilizaremos uma nova disposição de variável denominada matriz.Uma matriz permite declarar e trabalhar com uma coleção de valores de mesmo tipo. Por exemplo, você pode querer criar uma coleção de 5 inteiros. Uma forma para fazer isso seria declarar 5 inteiros diretamente: int a, b, c, d, e;

Isso está certo, mas e se você precisasse de milhares de números inteiros? Uma forma mais fácil é declarar uma matriz de 5 inteiros.

int a[5];

Page 16: Como funciona a programação em C

Os cinco inteiros individuais dentro desta matriz são acessados por umíndice. Todas as matrizes iniciam em zero e vão até n-1 no C. Assim,int a[5]; contém 5 elementos. Por exemplo:

int a[5];

a[0] = 12; a[1] = 9; a[2] = 14; a[3] = 5; a[4] = 1;

Uma das vantagens sobre a indexação de matriz é que você pode usar um loop para manipular o índice. Por exemplo, o código a seguir inicializa todos os valores na matriz em 0:

int a[5];

int i; for (i=0; i<5; i++) a[i] = 0;

O código seguinte inicializa seqüencialmente os valores na matriz e então os imprime:

#include <stdio.h>

int main() { int a[5]; int i; for (i=0; i<5; i++) a[i] = i; for (i=0; i<5; i++) printf("a[%d] = %dn", i, a[i]); }

As matrizes são usadas a toda hora em C. Para entender seu uso, inicie um editor e digite o seguinte código:

#include <stdio.h>

#define MAX 10

int a[MAX]; int rand_seed=10;

/* from K&R - retorna um número aleatório entre 0 e 32767.*/ int rand() { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } int main() { int i,t,x,y; /* preenche a matriz */ for (i=0; i < MAX; i++) {

Page 17: Como funciona a programação em C

a[i]=rand(); printf("%dn",a[i]); } /* mais coisas aparecerão aqui em breve */ return 0; }

Este código contém vários conceitos novos. A linha #define declara uma constante denominada MAX e a define em 10. Os nomes de constantes em geral são escritos em letras maiúsculas para destacá-los no código. A linha int a[MAX]; mostra como declarar uma matriz de inteiros em C. Observe que por causa da posição da declaração da matriz, ela é global ao programa.

A linha int rand_seed=10 também declara uma variável global, desta vez denominada rand_seed, que é inicializada em 10 sempre que o programa inicia. Este valor é o inicial para o código de números aleatórios que segue. Em um gerador de números aleatórios reais, o seed deve inicializar como um valor aleatório, como a hora do sistema. Aqui, a função rand produzirá os mesmos valores sempre que executar o programa.

A linha int rand() é uma instrução de função. A função rand não aceita parâmetros e retorna um resultado inteiro: aprenderemos mais sobre as funções em breve. As quatro linhas que seguem implementam a função rand. Por hora, nós as ignoraremos.

A função principal é normal. Quatro inteiros locais são declarados e a matriz é preenchida com 10 valores aleatórios usando um loop for. Observe que a matriz a contém 10 inteiros individuais. Você aponta para um inteiro específico na matriz usando colchetes. Assim a[0] refere-se ao primeiro inteiro na matriz, a[1] refere-se ao segundo, e assim por diante. A linha que começa com /* e termina com */ é denominada de comentário. O compilador ignora completamente a linha de comentário. Você pode colocar notas para si próprio ou outros programadores nos comentários.

Agora adicione o seguinte código no lugar do comentário mais coisas...:

/* ordenação por bolha da matriz */ for (x=0; x < MAX-1; x++) for (y=0; y < MAX-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } /* imprime matriz classificada */ printf("--------------------n"); for (i=0; i < MAX; i++) printf("%dn",a[i]);

Esta codificação classifica os valores aleatórios e os imprime ordenadamente. Sempre que o executar, você obterá os mesmos valores. Para alterar os valores classificados, altere o valor de rand_seedsempre que executar o programa.

O único modo fácil para realmente entender o que o código está fazendo é executá-lo "à mão". Isto é, assuma que MAX é 4 para facilitar, pegue uma folha de papel e finja que é o computador. Desenhe a matriz no papel e coloque 4 valores aleatórios e não-classificados na matriz. Execute cada linha da seção de classificação do código e desenhe exatamente o que acontece. Você verá que, sempre que submetidos ao loop interno, os valores maiores na matriz são empurrados para baixo e os valores menores vão para o topo da matriz.

Page 18: Como funciona a programação em C

Tente isto No primeiro trecho do código, tente alterar o

loop for que preenche a matriz para uma única linha de código. Certifique-se de que o resultado seja igual ao do código original.

Pegue o código de ordenação por bolha e coloque-o em sua própria função. O cabeçalho de função será void bubble_sort(). Depois transfira as variáveis utilizadas pela ordenação por bolha para a função e torne-as locais. Por ser uma matriz local, você não precisa passar parâmetros.

Inicialize a semente do número aleatório para diferentes valores.

Erros que devem ser evitados na linguagem C A linguagem C não tem nenhuma verificação

de dimensão, portanto, se você indexar além do fim da matriz, ele não o informará a respeito. Ele provavelmente travará ou apresentará dados incorretos.

A chamada de função deve incluir () mesmo se nenhum parâmetro for informado. Por exemplo, C aceitaráx=rand; mas a chamada não funcionará. O endereço de memória da função rand será colocado em x. Você deve dizer x=rand();.

Mais sobre matrizes

Tipos de variáveisHá 3 tipos padrão de variáveis em linguagem C:

inteiro: int ponto flutuante: float caractere: char

Um int é um valor inteiro de 4 bytes. Um float é um valor de ponto flutuante de 4 bytes. Um char é um caractere único de 1 byte (como "a" ou "3"). Uma string de caracteres é declarada como uma matriz de caracteres.

Há vários tipos derivados:

duplo (valor de ponto flutuante de 8 bytes) curto (inteiro de 2 bytes) curto sem sinal ou int sem sinal (números inteiros positivos, sem

bit de sinal)

Operadores e precedência de operadoresOs operadores em C são semelhantes aos operadores na maioria das linguagens:

+: adição -: subtração /: divisão *: multiplicação % - mod

O operador / realiza a divisão de inteiros se ambos os operandos forem números inteiros; caso contrário, realiza a divisão por ponto flutuante. Por exemplo:

void main() {

Page 19: Como funciona a programação em C

float a; a=10/3; printf("%f\n",a); }

Este código imprime um valor de ponto flutuante desde que a seja declarado como tipo float, mas aserá 3.0 porque o código executou uma divisão de inteiros.

A precedência de operador em C também é similar para a maioria das linguagens. A divisão e multiplicação ocorrem primeiro, depois a adição e a subtração. O resultado do cálculo 5+3*4 é 17, e não 32. Em C, o operador * tem precedência sobre o +. Você pode usar parênteses para alterar a ordem de precedência normal: (5+3)*4 é 32. O 5+3 é executado primeiro porque está entre parênteses. Abordaremos a parte da precedência mais tarde, já que ela se torna um pouco complicada em C quando os ponteiros são introduzidos.

Conversão de tipoA linguagem C permite executar conversões de tipo na sua execução. Você utiliza este recurso com freqüência ao usar ponteiros. A conversão de tipo também ocorre durante a operação de atribuição para determinados tipos. Por exemplo, no código acima, o valor inteiro foi automaticamente convertido para um flutuante.

Você faz a conversão de tipo em C colocando o nome de tipo entre parênteses e colocando-o na frente do valor que deseja alterar. Assim, no código acima, ao substituir a linha a=10/3; por a=(float)10/3;produz 3.33333 como resultado, pois 10 é convertido em um valor de ponto flutuante antes da divisão.

TypedefVocê declara tipos nomeados e definidos pelo usuário em C com a instrução typedef. O exemplo a seguir mostra um tipo que aparece freqüentemente no código C:

#define TRUE 1 #define FALSE 0 typedef int boolean; void main() { boolean b;

b=FALSE; blá blá blá }

Este código permite declarar tipos booleanos em programas C.

Se você não gosta da palavra "float" para números reais, você pode dizer:

typedef float real;

e mais tarde dizer:

real r1,r2,r3;

Você pode colocar declarações typedef em qualquer lugar em um programa C contanto que venham antes de sua primeira utilização na codificação.

EstruturasEstruturas em C permitem agrupar variáveis em um pacote. Eis um exemplo:

struct rec {

Page 20: Como funciona a programação em C

int a,b,c; float d,e,f; }; struct rec r;

Como demonstrado, sempre que desejar declarar estrutura do tipo rec, você precisa dizer struct rec. Esta linha é muito fácil de esquecer, e o compilador gera muitos erros caso você omita o struct. Você pode compactar o código desse modo:

struct rec { int a,b,c; float d,e,f; } r;

onde a declaração de tipo para rec e a variável r são declaradas na mesma instrução. Ou você pode criar uma instrução typedef para o nome de estrutura. Por exemplo, se você não gosta de dizer struct rec r sempre que deseja declarar um registro, pode dizer:

typedef struct rec rec_type;

e então declarar os registros de tipo rec_type dizendo:

rec_type r;

Você acessa os campos da estrutura usando um ponto, por exemplo, r.a=5;.

MatrizesVocê declara matrizes ao inserir um tamanho de matriz após a instrução normal, como abaixo:

int a[10]; /* matriz de inteiros */ char s[100]; /* matriz de caracteres (uma string em C) */ float f[20]; /* matriz de reais */ struct rec r[50]; /* matriz de registros */ Incrementação Caminho Longo Caminho Curto i=i+1; i++; i=i-1; i--; i=i+3; i += 3; i=i*j; i *= j;

Tente isto Tente diferentes trechos da codificação para

investigar a conversão de tipo e precedência. Tenteint, char, float e assim por diante.

Crie uma matriz de registros e digite um código para classificar a matriz em um campo de número inteiro.

Erros que devem ser evitados na linguagem C Como descrito acima, o uso do operador /

com 2 inteiros quase sempre produzirá um resultado inesperado, portanto pense bem ao utilizá-lo.

Funções

A maioria das linguagens permite criar funções de algum tipo. Funções permitem dividir um longo programa em seções nomeadas, de forma que as seções possam

Page 21: Como funciona a programação em C

ser reutilizadas ao longo do programa. As funções aceitam parâmetros e retornam um resultado. Funções de C podem aceitar um número ilimitado de parâmetros. Em geral, a linguagem C não se preocupa em qual ordem você coloca suas funções no programa, contanto que o nome da função seja conhecido pelo compilador antes de ser invocado.

Já falamos um pouco sobre funções. A função rand previamente descrita é bastante simples. Ela não aceita parâmetros e retorna um resultado inteiro:

int rand() /* de K&R - produz um número aleatório entre 0 e 32767.*/ { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; }

A linha int rand() declara a função rand para o resto do programa e especifica que aquele rand não aceitará nenhum parâmetro e retornará um resultado inteiro. Esta função não tem nenhuma variável local, mas se elas fossem necessárias, iriam logo abaixo da abertura {. A linguagem C permite declarar variáveis após qualquer {: elas existem até o programa atingir o } correspondente e então desaparecem. Desta forma, as variáveis locais de uma função desaparecem assim que o } correspondente é atingido na função. Enquanto existirem, as variáveis locais residem na pilha do sistema. Observe que não há nenhum ; depois de () na primeira linha. Caso tenha colocado um, acidentalmente, você receberá uma enorme cascata de mensagens de erro do compilador, que não farão sentido algum. Observe também que, mesmo sem parâmetros, você precisa usar o (). Eles indicam ao compilador que você está declarando uma função em vez de simplesmente declarar um int.

A instrução return é importante para qualquer função que retorna um resultado. Ela especifica o valor que a função retornará e a encerra imediatamente. Isto significa que você pode colocar diversas instruções de retorno na função para proporcionar vários pontos de saída. Se você não colocar uma instrução de retorno em uma função, a função retorna quando atinge } e retorna um valor aleatório (muitos compiladores o advertirão se você não retornar um valor específico). Em C, uma função pode retornar valores de qualquer tipo: int, float, char, struct, etc.

Há vários modos corretos para executar a função rand. Por exemplo: x=rand( );. A variável x recebe o valor retornado por rand nesta instrução. Observe que você deve usar ( ) na chamada da função, mesmo que nenhum parâmetro seja passado. Caso contrário, x recebe o endereço de memória da função rand, que geralmente não é desejável.

Você também pode invocar rand desta forma:

if (rand() > 100)

Ou deste modo:

rand();

No último caso, a função é chamada, mas o valor retornado por rand é descartado. Talvez você nunca faça isso com rand, mas muitas funções retornam algum tipo de código de erro por meio da função, e se você não está muito preocupado com o código de erro (por exemplo, por saber que um erro é impossível), você poderá descartá-lo.

Funções podem usar um retorno tipo void caso se deseje que não se retorne nada. Por exemplo:

void print_header()

Page 22: Como funciona a programação em C

{ printf("Programa Número 1n"); printf("por Marshall Brainn"); printf("Versão 1.0, lançada em 26/12/91\n"); }

Esta função não retorna valor algum. Você pode invocá-la com a seguinte instrução:

print_header();

Você deve incluir ( ) na chamada. Se não o fizer, a função não é invocada, embora possa ser compilada corretamente em outros sistemas.

As funções em C podem aceitar qualquer tipo de parâmetro. Por exemplo:

int fact(int i) { int j,k; j=1; for (k=2; k<=i; k++) j=j*k; return j; }

retorna o fatorial de i, que é passado como um parâmetro inteiro. Separe vários parâmetros com vírgulas:

int add(int i, int j) { return i+j; }

A linguagem C evoluiu ao longo dos anos. Às vezes, você verá funções como add escritas do "modo antigo", como mostrado abaixo:

int add(i,j) int i; int j; { return i+j; }

É importante saber ler o código escrito no estilo antigo. Não há nenhuma diferença na forma em que o código é executado, apenas a notação é diferente. Você deve usar o "novo estilo" (conhecido comoANSI C) com o tipo declarado, como parte da lista de parâmetros, a menos que tenha conhecimento prévio de que o código será enviado a alguém com um compilador em "estilo antigo" (sem suporte a ANSI).

Funções: protótipos de função

É considerada boa prática utilizar protótipos de função para todas as funções em seu programa. Um protótipo declara o nome de função, seus parâmetros e seu tipo de retorno para o resto do programa antes da declaração real da função. Para entender a utilidade dos protótipos, digite o seguinte código e execute-o: #include <stdio.h>

void main() { printf("%d\n",add(3)); } int add(int i, int j)

Page 23: Como funciona a programação em C

{ return i+j; }

Este código compila em diversos compiladores sem emitir um aviso, apesar de add esperar dois parâmetros e receber apenas um. Ele funciona porque muitos compiladores C não verificam se o parâmetro corresponde ao tipo ou à quantidade. Você pode perder horas na depuração de um código no qual você está declarando parâmetros a mais ou a menos por engano. O código anterior compila corretamente, mas produz a resposta errada.

Para resolver este problema, a linguagem C permite colocar protótipos de função no início (na verdade, em qualquer lugar) de um programa. Se fizer isso, a linguagem C verifica tipos e quantidades de todas as listas de parâmetros. Tente compilar o seguinte:

#include <stdio.h>

int add (int,int); /* protótipo de função para add */

void main() { printf("%d\n",add(3)); } int add(int i, int j) { return i+j; }

O protótipo faz com que o compilador sinalize um erro na instrução printf.

Coloque um protótipo para cada função no início de seu programa. Os propótipos podem economizar bastante tempo de depuração e também resolver o problema que ocorre ao compilar com funções que são utilizadas antes de serem declaradas. Por exemplo, o seguinte código não compilará:

#include <stdio.h> void main() { printf("%d\n",add(3)); } float add(int i, int j) { return i+j; }

Você pode perguntar: "Por que ele compilará quando add retorna um int mas não quando retorna um float?" Porque os compiladores em C mais antigos padronizaram um valor de retorno para int. O uso de um protótipo resolverá este problema. Os "compiladores antigos" (sem suporte a ANSI) permitem protótipos, porém a lista de parâmetros do protótipo deve estar vazia. Os compiladores antigos não fazem a verificação de erros em listas de parâmetros.

Tente isto Retorne ao exemplo de classificação bubble

sort anteriormente apresentado e crie uma função. Retorne aos programas anteriores e crie uma

função para obter a entrada de dado do usuário, em vez de obter a entrada de dado da função principal.

Page 24: Como funciona a programação em C

Bibliotecas

Bibliotecas são muito importantes na linguagem C, pois a linguagem suporta apenas os recursos mais básicos de que necessita. A linguagem C não contém sequer funções de I/O para ler a partir do teclado e digitar na tela. Qualquer coisa que vá além da linguagem básica deve ser escrita por um programador. Geralmente, os trechos de código resultantes são colocados em bibliotecas para torná-los facilmente reutilizáveis. Vimos a biblioteca padrão de I/O padrão ou stdio: as bibliotecas padrão existem para I/O padrão, funções matemáticas, manipulação da string de caracteres, manipulação de tempo e assim por diante. Você pode utilizar as bibliotecas em seus próprios programas para dividi-los em módulos. Isso os torna fáceis de entender, testar e depurar, como também possibilita a reutilização do código por outros programas que você criar.

Você pode criar suas próprias bibliotecas facilmente. Como exemplo, pegaremos um código do artigo anterior desta série e criaremos uma biblioteca a partir de duas de suas funções. Eis o código com o qual iniciaremos:

#include <stdio.h> #define MAX 10

int a[MAX]; int rand_seed=10;

int rand() /* de K&R - produz um número aleatório entre 0 e 32767.*/ { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } void main() { int i,t,x,y;

/* preenche a matriz*/ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } /* ordenação por bolha da matriz */ for (x=0; x < MAX-1; x++) for (y=0; y < MAX-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } /* imprime matriz classificada */ printf("--------------------n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]); }

Este código preenche uma matriz com números aleatórios, ordena-os em bubble sort e depois os exibe na lista ordenada.

Pegue o código bubble sort e use o que aprendeu no artigo anterior para criar uma função a partir dele. Visto que a matriz a e a constante MAX são globalmente

Page 25: Como funciona a programação em C

conhecidas, a função que você cria não precisa de parâmetros, tampouco apresentar um resultado. Porém, você deve usar variáveis locais para x, y e t.

Uma vez testada a função para verificar se ela está funcionado, passe o número de elementos como um parâmetro em vez de utilizar MAX:

#include <stdio.h> #define MAX 10

int a[MAX]; int rand_seed=10; /* de K&R - retorna um número aleatório entre 0 e 32767.*/ int rand() { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } void bubble_sort(int m) { int x,y,t; for (x=0; x < m-1; x++) for (y=0; y < m-x-1; y++) if (a[y] > a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } } void main() { int i,t,x,y; /* preenche a matriz */ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } bubble_sort(MAX); /* imprime matriz classificada */ printf("--------------------n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]); }

Você também pode generalizar ainda mais a função bubble_sort passando a como um parâmetro:

bubble_sort(int m, int a[])

Esta linha diz: "Aceitar a matriz do inteiro a de qualquer tamanho como parâmetro". Nada precisa ser alterado no corpo da função bubble_sort. Para chamar bubble_sort, altere a chamada para:

bubble_sort(MAX, a);

Observe que &a não foi usado na chamada de função, embora a classificação vá mudar a. O motivo para tal ficará mais evidente depois que você entender os ponteiros.

Criando uma biblioteca

Page 26: Como funciona a programação em C

Uma vez que as funções rand e bubble_sort nos programas anteriores são úteis, você provavelmente vai querer reutilizá-las em outros programas que criar. Você pode colocá-las em uma biblioteca de utilitários para facilitar sua reutilização.

Toda biblioteca possui duas partes: um arquivo de cabeçalho e o arquivo de código. O arquivo de cabeçalho, em geral indicado por um sufixo .h, contém informações sobre a biblioteca que os programas que o utilizam precisam saber. Em geral, o arquivo de cabeçalho contém constantes e tipos, junto com protótipos de funções disponíveis na biblioteca. Digite o seguinte arquivo de cabeçalho e salve-o com o nome util.h.

/* util.h */ extern int rand(); extern void bubble_sort(int, int []);

Estas duas linhas são protótipos de função. A palavra "extern" em C representa funções que serão posteriormente vinculadas. Se estiver usando um compilador antigo, remova os parâmetros da lista de parâmetros do bubble_sort.

Digite o seguinte código em um arquivo denominado util.c.

/* util.c */ #include "util.h" int rand_seed=10; /* de K&R - produz um número aleatório entre 0 e 32767.*/ int rand() { rand_seed = rand_seed * 1103515245 +12345; return (unsigned int)(rand_seed / 65536) % 32768; } void bubble_sort(int m,int a[]) { int x,y,t; for (x=0; x < m-1; x++) for (y=0; y > m-x-1; y++) if (a[y] < a[y+1]) { t=a[y]; a[y]=a[y+1]; a[y+1]=t; } }

Observe que o arquivo inclui seu próprio arquivo de cabeçalho (util.h) e que utiliza aspas em vez de símbolos < e >, que são utilizados apenas para bibliotecas de sistema. Como pode ver, isto parece um código normal em C. Observe que a variável rand_seed, por não estar no arquivo de cabeçalho, não pode ser visualizada ou modificada pelo programa usando esta biblioteca. Isto se chama ocultação de informação. O acréscimo da palavra static na frente de int força a ocultação completamente.

Digite o seguinte programa principal em um arquivo chamado main.c.

#include <stdio.h> #include "util.h" #define MAX 10

int a[MAX]; void main() { int i,t,x,y;

Page 27: Como funciona a programação em C

/* preenche matriz*/ for (i=0; i < MAX; i++) { a[i]=rand(); printf("%d\n",a[i]); } bubble_sort(MAX,a); /* imprime matriz classificada*/ printf("--------------------n"); for (i=0; i < MAX; i++) printf("%d\n",a[i]); }

Este código inclui a biblioteca de utilitários. O benefício principal em utilizar uma biblioteca é que a codificação no programa principal fica muito menor.

Compilando e executando com uma bibliotecaPara compilar a biblioteca, digite o seguinte na linha de comando (assumindo que você está usando UNIX) (substitua gcc por cc, se seu sistema usa cc):

gcc -c -g util.c

O -c faz com que o compilador produza um arquivo de objeto para a biblioteca. O arquivo objeto contém o código de máquina da biblioteca. Ele não pode ser executado até ser vinculado a um arquivo de programa que contenha a função principal. O código de máquina reside em um arquivo separado chamado util.o.

Para compilar o programa principal, digite o seguinte:

gcc -c -g main.c

Esta linha cria um arquivo denominado main.o que contém o código de máquina do programa principal. Para criar o executável final que contém o código de máquina de todo o programa, vincule os dois arquivos objetos digitando o seguinte:

gcc -o main main.o util.o

Isso vincula main.o e util.o para formar um executável denominado main. Para executá-lo, digite main.

Makefiles facilitam o trabalho com as bibliotecas. Você descobrirá mais sobre os makefiles na próxima página.

Makefiles

Pode ser inconveniente ter que digitar todas as linhas do gcc novamente, especialmente se você estiver realizando inúmeras alterações no código e possui várias bibliotecas. O utilitário make resolve este problema. Você pode usar o seguinte makefile para substituir a seqüência de compilação anterior: main: main.o util.o gcc -o main main.o util.o main.o: main.c util.h gcc -c -g main.c util.o: util.c util.h gcc -c -g util.c

Digite isto em um arquivo chamado makefile e digite make para criar o executável. Observe que vocêdeve preceder todas as linhas gcc com um espaço de parágrafo (8 espaços não bastarão, é necessário uma tabulação; as demais linhas devem estar alinhadas à esquerda).

Page 28: Como funciona a programação em C

Este makefile contém dois tipos de linhas. As linhas que aparecem alinhadas à esquerda são linhas de dependência. As linhas precedidas por uma tabulação são linhas executáveis, que podem conter qualquer comando UNIX válido. Uma linha de dependência indica que um arquivo é dependente de outro conjunto de arquivos. Por exemplo, main.o: main.c util.h diz que o arquivo main.o depende dos arquivos main.c e util.h. Se qualquer um destes dois arquivos for alterado, as linhas executáveis seguintes devem ser executada(s) para recriar main.o.

Observe que o executável final produzido pelo makefile inteiro é main, na linha 1 no makefile. O resultado final do makefile deve sempre ir na linha 1, onde este makefile diz que o arquivo maindepende de main.o e util.o. Em caso de alterações, execute a linha gcc -o main main.o util.o para recriar main.

É possível colocar múltiplas linhas para serem executadas abaixo da linha de dependência (todas devem começar com uma tabulação). Um programa grande pode ter várias bibliotecas e um programa principal. O makefile recompila automaticamente tudo o que precisa ser recompilado por conta de uma alteração.

Se não estiver trabalhando em uma máquina UNIX, seu compilador provavelmente tem a funcionalidade equivalente à dos makefiles. Leia a documentação de seu compilador para aprender como utilizá-lo.

Agora você entende por que se tem incluído stdio.h nos programas anteriores. Ele é simplesmente uma biblioteca padrão que alguém criou há muito tempo e disponibilizou a outros programadores para facilitar suas vidas.

Arquivos de texto

Arquivos de texto em C são diretos e fáceis de entender. Todas as funções e tipos de arquivo de texto em C vêm da biblioteca stdio.

Quando você precisa de I/O de texto em um programa em C e precisa apenas de uma fonte para informação de entrada e uma fonte para informação de saída, você pode usar o stdin (entrada padrão/teclado) e stdout (saída padrão/monitor). Você pode usar o redirecionamento de entrada e saída na linha de comando para mover diferentes fluxos de informações pelo programa. Há 6 comandos diferentes de I/O em <stdio.h> que você pode usar com stdin e stdout:

printf - imprime a saída formatada para stdout; scanf - lê a entrada formatada do stdin; puts - imprime uma string de caracteres para stdout; gets: lê uma seqüência de caracteres do stdin; putc - imprime um caractere para stdout; gets, getchar - lê um caractere do stdin.

A vantagem do stdin e stdout é que eles são fáceis de usar. Da mesma forma, a capacidade para redirecionar a I/O é muito poderosa. Por exemplo, talvez você queira criar um programa que leia de stdin e conta o número de caracteres:

#include <stdio.h> #include <string.h>

void main() { char s[1000]; int count=0; while (gets(s)) count += strlen(s); printf("%d\n",count);

Page 29: Como funciona a programação em C

}

Digite este código e execute-o. Ele aguardará pela entrada de stdin, portanto digite algumas linhas. Quando terminar, pressione CTRL-D para indicar o final do arquivo (eof, de "end-of-file"). A função gets lê uma linha até detectar o eof, depois retorna um 0, encerrando o loop while. Ao pressionar CTRL-D, você vê uma contagem do número de caracteres em stdout (a tela). Use man gets ou a documentação de seu compilador para saber mais sobre a função gets.

Agora, suponha que você queira contar os caracteres em um arquivo. Se o programa for compilado em um executável nomeado xxx, você pode digitar o seguinte:

xxx < nome do arquivo

Em vez de aceitar a entrada do teclado, será usado o conteúdo do arquivo denominado filename. Você pode obter o mesmo resultado usando o caractere barra vertical:

cat < filename | xxx

Você também pode redirecionar o resultado para um arquivo:

xxx < filename > out

Este comando coloca a contagem de caracteres produzida pelo programa em um arquivo de texto denominado out.

Às vezes, você precisa usar um arquivo de texto diretamente. Por exemplo, você pode precisar abrir um arquivo específico e ler ou escrever nele. Talvez você queira gerenciar vários fluxos de entrada ou saída, ou criar um programa como um editor de textos que possa salvar e recuperar arquivos de dados ou configurações no comando. Nesse caso, use as funções de arquivo de texto em stdio:

fopen - abre um arquivo de texto; fclose - fecha um arquivo de texto; feof - detecta o marcador de final de arquivo em um arquivo; fprintf - imprime a saída formatada em um arquivo; fscanf - lê a entrada formatada de um arquivo; fputs - imprime uma string de caracteres para um arquivo; fgets - lê uma string de caracteres de um arquivo; fputc - imprime um caractere para um arquivo; fgetc - lê um caractere de um arquivo.

Arquivos de texto: abrindo

Use fopen para abrir um arquivo. Ele abre um arquivo para um modo especificado (os três mais comuns são r, w e a, para leitura, gravação e anexação). Ele então retorna um ponteiro de arquivo que você usa para acessar o arquivo. Por exemplo, suponha que você queira abrir um arquivo e escrever nele os números 1 a 10. Você pode usar a seguinte codificação: #include <stdio.h> #define MAX 10

int main() { FILE *f; int x; f=fopen("out","w";); if (!f) return 1; for(x=1; x<=MAX; x++)

Page 30: Como funciona a programação em C

fprintf(f,"%d\n",x); fclose(f); return 0; }

A instrução fopen abre um arquivo denominado out com o modo w. Este é um modo de escrita destrutivo, o que significa que se out não existir ele será criado, porém se ele já existir, será destruído e um novo arquivo será criado em seu lugar. O comando fopen retorna um ponteiro para o arquivo, que é armazenado na variável f. Esta variável é usada para referência ao arquivo. Se o arquivo não pode ser aberto por algum motivo, f irá conter NULL.

Valores de retorno da função principalEste programa é o primeiro programa desta série que retorna um valor de erro do programa principal. Se o comando fopen falhar, f contém um valor NULL (zero). Vamos testar este erro com a instrução if. A instrução if verifica o valor de Verdadeiro/Falso da variável f. Lembre-se de que em C, 0 é Falso e qualquer outro valor é verdadeiro. Se houvesse um erro ao abrir o arquivo, f conteria zero, que é Falso. O ! é o operador NOT. Ele inverte um valor booleano. Assim, a instrução if poderia ter sido escrita desta forma:

if (f == 0)

As duas formas são equivalentes. Porém, if (!f) é mais comum.

Se houver um erro de arquivo, retornamos um 1 da função principal. Em UNIX, você pode testar este valor na linha de comando. Veja a documentação de shell para mais detalhes. 

A instrução fprintf deve parecer bastante familiar: ela é igual a printf, mas usa o ponteiro de arquivo como seu primeiro parâmetro. A instrução fclose fecha o arquivo quando você termina.

Arquivos de texto: lendo

Para ler um arquivo, abra-o com o modo r. Em geral, não é uma boa idéia usar fscanf para leitura: a menos que o arquivo esteja perfeitamente formatado, fscanf não o manipulará corretamente. Em vez disso, use fgets para ler cada linha e então analisar gramaticalmente as partes necessárias.

O seguinte código demonstra o processo de leitura de um arquivo e o descarregamento de seu conteúdo na tela:

#include <stdio.h>

int main() { FILE *f; char s[1000]; f=fopen("infile","r"); if (!f) return 1; while (fgets(s,1000,f)!=NULL) printf("%s",s); fclose(f); return 0; }

Page 31: Como funciona a programação em C

A instrução fgets retorna um valor NULL ao marcador de final de arquivo. Ele lê uma linha (até 1 mil caracteres, neste caso) e depois a imprime para stdout. Observe que a instrução printf não inclui n na string de formatação, pois fgets adiciona n ao final de cada linha que lê. Assim, você pode saber se uma linha não está completa no evento que excede o comprimento máximo da linha especificado no segundo parâmetro para fgets.

Erros que devem ser evitados na linguagem C Não digite acidentalmente close em vez

de fclose. A função close existe, portanto o compilador a aceita. Ela até parecerá funcionar se o programa apenas abrir ou fechar alguns arquivos. Entretanto, se o programa abrir e fechar um arquivo em um loop, eventualmente ficará sem manipuladores de arquivos disponíveis e/ou espaço em memória e travará, pois close não está fechando os arquivos corretamente.

Ponteiros

Ponteiros são bastante usados em C. Assim, se deseja usar a linguagem C por completo, você precisa ter uma boa compreensão sobre ponteiros. Eles devem se tornar fáceis de usar para você. A meta desta e das várias próximas seções é ajudá-lo a construir um entendimento completo sobre os ponteiros e como a linguagem C os utiliza. Para a maioria das pessoas, isto demora um pouco, pois requer prática familiarizar-se com os ponteiros, mas depois de dominá-los, você se torna um programador completo em linguagem C.

A linguagem C utiliza ponteiros de três modos diferentes:

A linguagem C usa ponteiros para criar estruturas dinâmicas de dados, que são estruturas de dados criadas a partir de blocos de memória localizados na pilha durante o tempo de execução.

A linguagem C usa ponteiros para manipular parâmetros de variáveis passados para as funções.

Os ponteiros em C oferecem um modo alternativo para acessar informações armazenadas em matrizes. As técnicas de ponteiro são especialmente valiosas quando se trabalha com strings de caracteres. Há uma estreita relação entre matrizes e ponteiros em linguagem C.

Em alguns casos, os programadores de C também usam ponteiros porque eles tornam o código um pouco mais eficiente. O que você descobrirá é que, depois de se sentir familiarizado com os ponteiros, irá a usá-los o tempo todo.

Iniciaremos esta discussão com uma introdução básica sobre ponteiros e seus conceitos relacionados, e depois passaremos para as três técnicas descritas acima. Você provavelmente vai querer ler este artigo duas vezes. Ao lê-lo da primeira vez, você aprenderá os conceitos. A segunda leitura permitirá ligar os conceitos e criar um todo integrado em sua mente. Após ler o material pela segunda vez, ele fará muito mais sentido.

Ponteiros: por quê?

Imagine que você gostaria de criar um editor de texto: um programa que permite editar arquivos de texto ASCII normal, como o "vi", do UNIX, ou o "Bloco de notas", do Windows. Um editor de textos é uma coisa bastante comum para alguém criar, pois é provavelmente o segmento de software mais usado pelo programador. O editor de textos é a ligação íntima de um programador com o computador, pois é nele que o programador digita todas as suas idéias e as organiza. Obviamente,

Page 32: Como funciona a programação em C

como qualquer coisa que você utilize com tanta freqüência e com a qual trabalhe constantemente, você também vai desejar que o editor de textos funcione perfeitamente. Assim, muitos programadores criam seus próprios editores e os personalizam para atender seus estilos e preferências de trabalho individuais.

Então, em um certo dia, você se senta para começar a trabalhar em seu editor. Depois de pensar sobre as características que deseja, você começa a pensar sobre a "estrutura de dados" de seu editor. Quer dizer, começa a pensar sobre como armazenará o documento que está editando na memória, de forma que possa manipulá-lo em seu programa. O que você precisa é de um modo para armazenar as informações inseridas de forma que possam ser manipuladas rápida e facilmente. Você acredita que um modo de fazer isso é organizar os dados com base na linha de caracteres. Visto o que já foi discutido, a única coisa disponível aqui é a matriz. Você pensa: "Bem, uma linha típica tem 80 caracteres de comprimento e um arquivo típico não tem mais do que mil linhas”. Assim, você declara uma matriz bi-dimensional como esta:

char doc[1000][80];

Esta instrução solicita uma matriz de mil linhas com 80 caracteres. Esta matriz tem um tamanho total de 80 mil caracteres.

Enquanto pensa sobre o editor e sua estrutura de dados, você pode se dar conta de três coisas:

Alguns documentos são listas longas. Cada linha é curta, mas há milhares de linhas;

Alguns arquivos de texto com finalidade especial possuem linhas muito longas. Por exemplo, certo arquivo de dados poderia ter linhas contendo 542 caracteres, com cada caractere representando um par de aminoácidos do DNA;

Na maioria dos editores modernos, você pode abrir múltiplos arquivos de uma só vez.

Digamos que você determinou um máximo de 10 arquivos abertos por vez, um comprimento máximo de linha de 1 mil caracteres e um tamanho de arquivo máximo de 50 mil linhas. Sua declaração agora se parecerá com: char doc[50000][1000][10];

Não parece uma coisa absurda, até você pegar sua calculadora, multiplicar 50 mil por 1 mil e depois por 10 e constatar que a matriz irá conter 500 milhões de caracteres! Atualmente, a maioria dos computadores tem problemas com matrizes deste tamanho. Eles não têm a memória RAM, ou até mesmo o espaço de memória virtual para suportar uma matriz desse tamanho. Se os usuários tentassem executar três ou quatro cópias deste programa simultaneamente, mesmo num sistema multiusuário maior, isso poderia representar um esforço muito grande a suas instalações.

Mesmo que o computador aceitasse uma solicitação para uma matriz tão grande, você veria que isto seria um desperdício extravagante de espaço. Parece estranho declarar uma matriz de 500 milhões de caracteres se, na maioria dos casos, você executará este editor para pesquisar arquivos de 100 linhas, que consomem no máximo 4 ou 5 mil bytes. O problema com uma matriz é o fato de que você precisa declará-la como tendo o tamanho máximo em cada dimensão, desde o começo. Os tamanhos máximos freqüentemente se multiplicam para formar números muito grandes. Também, se precisar editar um arquivo que ocasionalmente possua 2 mil carcacteres em uma linha, você terá problemas. Não há como prever e manipular o comprimento máximo de uma linha de um arquivo de texto, pois, tecnicamente, este número é infinito.

Page 33: Como funciona a programação em C

Os ponteiros são projetados para resolver esse problema. Com eles, você pode criar estruturas dinâmicas de dados. Em vez de declarar o pior caso de consumo de memória com antecedência em uma matriz, você aloca memória da pilha enquanto o programa está sendo executado. Dessa forma, você pode usar a quantia exata de memória que o documento precisa, sem desperdício. Além disso, quando você fecha um documento, pode devolver a memória à pilha para que outros trechos do programa possam usá-la. Com os ponteiros, a memória pode ser reciclada enquanto o programa está sendo executado.

A propósito, se você leu a discussão anterior e uma de suas grandes dúvidas continua sendo "o que é, de fato, um byte?”, então o artigo Como funcionam os bits e os bytes irá ajudá-lo a compreender esses conceitos e também coisas como "mega", "giga" e "tera". Dê uma olhada nele e depois volte aqui.

Fundamentos sobre ponteiros

Para compreender os ponteiros, convém compará-los às variáveis normais.

Uma "variável normal" é um local na memória que pode conter um valor. Por exemplo, quando você declara uma variável i como um inteiro, 4 bytes de memória são separados para isso. Em seu programa, você se refere àquele local na memória pelo nome i. No nível da máquina, aquele local tem um endereço de memória. Os 4 bytes naquele endereço são conhecidos pelo programador como i, e os 4 bytes podem conter valores inteiros.

Um ponteiro é diferente. Um ponteiro é uma variável que aponta para outra variável. Isto significa que um ponteiro mantém o endereço de memória de outra variável. Em outras palavras, o ponteiro não contém um valor no sentido tradicional, mas sim o endereço de outra variável. Um ponteiro "aponta para" esta outra variável mantendo uma cópia de seu endereço.

Como um ponteiro contém um endereço, e não um valor, terá duas partes. O ponteiro contém um endereço e o endereço aponta para um valor. Há o ponteiro e o valor para o qual ele aponta. Este fato pode ser um tanto confuso, até você se familiarizar com ele. Superada a etapa da familiarização, ele se torna extremamente eficaz.

A codificação exemplificada a seguir mostra um típico ponteiro:

#include <stdio.h>

int main() { int i,j; int *p; /* um ponteiro para um inteiro */ p = &i; *p=5; j=i; printf(("%d %d %d\n", i, j, *p); return 0; }

A primeira declaração neste programa mostra duas variáveis inteiras normais, nomeadas i e j . A linhaint *p declara um ponteiro denominado p. Esta linha pede que o compilador declare uma variável p que seja um ponteiro para um inteiro. O * indica que foi declarado um ponteiro em vez de uma variável normal. Você pode criar um ponteiro para qualquer coisa: um flutuante, uma estrutura, um caractere e assim por diante. Basta usar um * para indicar que deseja um ponteiro em vez de uma variável normal.

Page 34: Como funciona a programação em C

A linha p = &i; trará algo totalmente novo para você. Em linguagem C, & é denominado operador de endereço. A expressão &i significa: "O endereço de memória da variável i". Assim, a expressão p = &i;significa: "Atribuir para p o endereço de i". Uma vez executada esta instrução, p "aponta para" i. Antes de prosseguir, lembre-se que p contém um endereço aleatório e desconhecido e que sua utilização causará uma falha de segmentação ou erro de programa similar.

Um bom método para visualizar o que está acontecendo é fazer um desenho. Depois que i, j e p são declarados, as coisas parecerão assim:

Neste desenho, as três variáveis i, j e p foram declaradas, mas nenhuma delas foi inicializada. As duas variáveis inteiras foram desenhadas como caixas contendo os pontos de interrogação (elas poderiam conter qualquer valor nesta altura da execução do programa). O ponteiro é desenhado como um círculo para distingüi-lo de uma variável normal que contém um valor e as setas aleatórias indicam que ele pode estar apontando para qualquer lugar neste momento.

Depois da linha p = &I;, p é inicializado e aponta para i, desta forma:

Assim que p aponta para i, o local de memória i tem 2 nomes. Ele ainda é conhecido por i , mas agora também é conhecido como *p. É assim que a linguagem C se comunica com as duas partes de uma variável de ponteiro: p é o local que mantém o endereço, enquanto *p é o local apontado por aquele endereço. Logo, *p=5 significa que o local apontado por p deve ser definido como 5, assim:

Visto que o local *p também é i, i também assume o valor 5. Conseqüentemente, j=i; define j como 5 e a instrução printf produz 5 5 5.

A principal característica de um ponteiro é sua dupla natureza. O próprio ponteiro contém um endereço. O ponteiro também aponta para um valor de um tipo específico: o valor no endereço do ponteiro. O próprio ponteiro, neste caso, é p. O valor apontado é *p.

Ponteiros: entendendo os endereços da memória

A discussão anterior torna-se um pouco mais clara se você compreender como funciona o endereçamento de memória em um computador. Caso ainda não tenha

Page 35: Como funciona a programação em C

lido, agora é uma boa hora para ler Como funcionam os bits e os bytes para entender sobre bits, bytes e palavras.

Todos os computadores têm memória, também conhecida por RAM (memória de acesso aleatório). Por exemplo, seu computador pode ter 16, 32 ou 64 megabytes de memória RAM instalados. A memória RAM mantém os programas que atualmente estão em execução no computador, junto aos dados que estão sendo manipulados (suas variáveis e estruturas de dados). A memória pode ser vista como uma matriz de bytes. Nesta matriz, cada local de memória tem seu próprio endereço. O endereço do primeiro byte é 0, seguido por 1, 2, 3, e assim sucessivamente. O endereço de memória atua como os índices de uma matriz normal. O computador pode acessar qualquer endereço na memória, a qualquer momento (por isso o nome "memória de acesso aleatório"). Ele também pode agrupar os bytes necessários para formar matrizes, variáveis e estruturas maiores. Por exemplo, uma variável de ponto flutuante consome 4 bytes contínuos em memória. Você pode fazer a seguinte declaração global em um programa:

float f;

Esta instrução diz: "Declare um local denominado f que possa manter um valor de ponto flutuante”. Quando o programa é executado, o computador reserva espaço para a variável f em algum lugar na memória. Aquele local tem um endereço fixo no espaço de memória, assim:

A variável f consome 4 bytes de RAM na memória.Este local tem um endereço específico, neste caso 248,440.

Enquanto você pensa na variável f, o computador pensa em um endereço específico na memória (por exemplo, 248,440). Assim, ao criar uma instrução como esta:

f = 3.14;

O compilador poderia traduzir isso como: "Carregar o valor 3.14 no local de memória 248,440". O computador está sempre pensando na memória em termos de endereço e valores para tais endereços.

A propósito, existem vários efeitos colaterais interessantes na forma com a qual seu computador gerencia a memória. Por exemplo, suponhamos que você incluiu a seguinte codificação em um de seus programas:

Page 36: Como funciona a programação em C

int i, s[4], t[4], u=0; for (i=0; i<=4; i++) { s[i] = i; t[i] =i; } Printf("s:tn"); for (i=0; i<=4; i++) printf("("%d:%dn", , s[i], t[i]); printf("u = %dn", u);

O resultado que você vê no programa provavelmente será algo assim:

s:t 1:5 2:2 3:3 4:4 5:5 u = 5

Por que t[0] e u estão incorretos? Se olhar cuidadosamente o código, você verá que os loops for estão escrevendo um elemento além do final de cada matriz. Na memória, as matrizes são colocadas lado a lado, como mostrado abaixo:

Logo, ao tentar escrever para s[4], que não existe, o sistema escreve para t[0], uma vez que t[0] é onde s[4] deveria estar. Ao escrever para t[4], na verdade você está escrevendo em u. No que se refere ao computador, s[4] é simplesmente um endereço e pode ser escrito. Como você pode ver, mesmo que o computador execute o programa, ele não está correto ou válido. O programa corrompe a matriz t no processo de execução. A execução da seguinte instrução pode resultar em conseqüências mais graves:

s[1000000] = 5;

O local s[1000000] provavelmente está fora do espaço de memória de seu programa. Em outras palavras, você está escrevendo na memória que seu

Page 37: Como funciona a programação em C

programa não possui. Em um sistema com espaços protegidos de memória (UNIX, Windows 98/NT), este tipo de instrução fará com que o sistema encerre a execução do programa. Em outros sistemas (Windows 3.1, Mac), todavia, o sistema não sabe o que você está fazendo. Você acaba danificando o código ou as variáveis em outro aplicativo. O efeito da violação pode variar de nenhum a uma tremenda pane do sistema. Na memória, i, s, t e u são todos colocados próximos uns aos outros em endereços específicos. Portanto, se você escrever além dos limites de uma variável, o computador fará o que foi solicitado, porém acabará corrompendo outro local de memória.

As linguagens C e C++ não executam qualquer tipo de verificação de extensão ao acessar um elemento na matriz. É essencial que você, como programador, preste atenção às extensões de matriz e respeite seus limites. A leitura ou escrita não-intencional fora dos limites da matriz quase sempre leva ao comportamento errôneo do programa.

Como outro exemplo, tente o seguinte:

#include int main() { int i,j; int *p; /* um ponteiro para um inteiro */ printf("%d %dn", p, &i); p = &i; printf("%d %dn", p, &i); return 0; }

Este código diz ao compilador para imprimir o endereço contido em p, junto com o endereço de i. A variável p começa com um valor estranho ou com 0. O endereço de i geralmente é um valor alto. Por exemplo, quando executei este código, recebi o seguinte resultado:

0 2147478276 2147478276 2147478276

que significa que o endereço de i é 2147478276. Após executar a instrução p = &i;, p contém o endereço de i. Tente também:

#include void main() { int *p; /* um ponteiro para um inteiro */ printf("%dn",*p); }

Este código diz ao compilador para imprimir o valor para o qual p aponta. Porém, p ainda não foi inicializado. Ele contém o endereço 0 ou algum endereço aleatório. Na maioria dos casos, resulta em uma falha de segmentação (ou algum outro erro no tempo de execução), o que significa que você usou um ponteiro que aponta para uma área inválida da memória. Quase sempre, um ponteiro não inicializado ou um endereço de ponteiro inválido é a causa das falhas de segmentação.

Dito isso, agora podemos ver os ponteiros por uma ótica totalmente diferente. Veja este programa, por exemplo:

#include

Page 38: Como funciona a programação em C

int main() { int i; int *p; /* um ponteiro para um inteiro*/ p = &i; *p=5; printf("%d %dn", i, *p); return 0; }

Eis o que está acontecendo:

A variável i consome 4 bytes de memória. O ponteiro p também consome 4 bytes (na maioria das máquinas atuais, um ponteiro consome 4 bytes de memória; os endereços de memória tem 32 bits de extensão na maioria das CPUs, embora exista uma tendência de crescimento para o endereçamento de 64 bits). O local de i tem um endereço específico, neste caso 248,440. O ponteiro p contém aquele endereço, uma vez que você disse que p = &i;. As variáveis *p e i são, portanto, equivalentes.

O ponteiro p literalmente contém o endereço de i. Você pode dizer algo assim em um programa:

printf("%d", p);

o resultado é o endereço corrente da variável i.

Ponteiros: apontando para o mesmo endereço

Eis um aspecto interessante da linguagem C: qualquer número de ponteiros pode apontar para o mesmo endereço. Por exemplo, você pode declarar p, q, e r como ponteiros de inteiros e defini-los para apontar para i, assim: int i; int *p, *q, *r;

Page 39: Como funciona a programação em C

p = &i; q = &i; r = p;

Observe que neste código, r aponta para o mesmo que p, que é i. Você pode atribuir ponteiros uns aos outros, e o endereço é copiado da direita para a esquerda durante a atribuição. Ao executar o código acima, você verá algo assim:

A variável i agora tem 4 nomes: i, *p, *q e *r. Não há limite de número de ponteiros que podem conter (e apontar para) o mesmo endereço.

Ponteiros: bugs comuns

Bug n.º1 - Ponteiro não inicializadoUm dos meios mais fáceis de criar um bug de ponteiro é tentar fazer referência ao valor de um ponteiro, mesmo que ele não seja inicializado e ainda não aponte para um endereço válido. Por exemplo:

int *p; *p = 12;

O ponteiro p não é inicializado e aponta para um local aleatório na memória ao ser declarado. Ele poderia apontar para a pilha do sistema, variáveis globais, espaço de código do programa ou o sistema operacional. Quando você diz *p=12;, o programa simplesmente tentará escrever um 12 em qualquer local aleatório para onde p apontar. O programa pode explodir imediatamente ou daqui a meia hora, ou ainda pode corromper sutilmente os dados em qualquer outra parte do seu programa sem que você nunca perceba. Isso pode dificultar bastante o rastreamento de um erro. Certifique-se de inicializar todos os ponteiros para um endereço válido antes de referenciá-los.

Bug n.º 2 - Referências de ponteiro inválidas Uma referência inválida ocorre quando o valor de um ponteiro é referido, mesmo que o ponteiro não aponte para um bloco válido.

Uma forma de criar este erro é dizer p=q;, quando q não é inicializado. O ponteiro p se tornará não inicializado e qualquer referência a *p será uma referência de ponteiro inválida.

O único modo para evitar este bug é desenhar imagens de cada etapa do programa e se certificar de que todos os ponteiros apontam para algum lugar. Referências de

Page 40: Como funciona a programação em C

ponteiro inválidas fazem com que o programa trave inexplicavelmente pelos mesmos motivos citados no bug nº 1.

Bug nº 3 - Referências de ponteiro Uma referência nula ocorre sempre que um ponteiro indicando zero é utilizado em uma instrução que tenta fazer referência a um bloco. Por exemplo, se p é um ponteiro de um inteiro, o seguinte código está inválido:

p = 0; *p = 12;

Como não há um bloco apontado por p, tentar ler ou escrever a partir de qualquer coisa ou para aquele bloco é uma referência de ponteiro nula e inválida. Existem motivos bons e válidos para apontar um ponteiro para zero, como veremos nos artigos posteriores. Entretanto, não fazer referência a um ponteiro desta maneira é inválido.

Todos estes bugs são fatais para um programa. Você deve prestar atenção à sua codificação para que estes bugs não ocorram. A melhor forma para tal é desenhar imagens de cada etapa de execução do código.

Usando ponteiros para parâmetros de função

A maioria dos programadores em C usa ponteiros para implementar algo chamado de parâmetros variáveis em funções. Na verdade, você tem usado parâmetros variáveis na função scanf, por isso você precisou usar & (o operador de endereço) em variáveis com scanf. Agora que você já entende os ponteiros, pode ver o que realmente está acontecendo.

Para entender como os parâmetros variáveis funcionam, vamos ver como nos saímos ao implementar uma função swap em C. Para implementar uma função swap, talvez você queira passar por duas variáveis fazendo-as trocar seus valores. Eis uma tentativa de implementação. Digite e execute a seguinte codificação e veja o que acontece:

#include <stdio.h>

void swap(int i, int j) { int t;

t=i; i=j; j=t; } void main() { int a,b;

a=5; b=10; printf("%d %d\n", a, b); swap(a,b); printf("%d %d\n", a, b); }

Ao executar este programa, você verá que nenhuma troca ocorrerá. Os valores de a e b são passados para troca, a função swap os troca, porém quando a função retorna, nada acontece.

Para que esta função opere corretamente, você pode usar ponteiros:

Page 41: Como funciona a programação em C

#include <stdio.h> void swap(int *i, int *j) { int t; t = *i; *i = *j; *j = t; } void main() { int a,b; a=5; b=10; printf("%d %d\n",a,b); swap(&a,&b); printf("%d %d\n",a,b); }

Para ter uma idéia do que esta codificação faz, imprima-a e desenhe os inteiros a e b e digite 5 e 10 neles. Agora desenhe os dois ponteiros i e j, juntamente com o inteiro t. Quando swap é executado, ele passa os endereços de a e b. Assim, i aponta para a (desenhe uma seta de i para a) e j aponta para b(desenhe outra seta de b para j). Depois que os ponteiros são inicializados pela função call, *i é outro nome para a, e *j é outro nome para b. Agora execute o código em swap. Quando o código usa *i e *j, na verdade quer dizer a e b. Quando a função se completa, a e b foram trocados.

Suponha que você se esqueça do & quando a função swap é executada, e que a linha swapacidentalmente se pareça assim: swap(a, b);. Isso causa uma falha de segmentação. Quando você omite o &, o valor de a é passado em vez de seu endereço. Assim, i aponta para local inválido na memória e o sistema trava quando *i é usado.

É por isso também que scanf trava se você se esquecer do & em variáveis passadas a ele. A funçãoscanf está usando ponteiro para colocar o valor que ele lê de volta à variável que você passou. Sem o&, scanf recebe um endereço inválido e trava.

Os parâmetros variáveis são um dos usos mais comuns de ponteiros em C. Agora você entende o que está acontecendo.

Estruturas de dados dinâmicas

As estruturas de dados são aquelas que crescem e encolhem conforme você precisa alocar e desalocar memória de um lugar chamado pilha. Elas são

Page 42: Como funciona a programação em C

extremamente importantes em C, pois permitem ao programador controlar exatamente o consumo de memória.

Elas alocam blocos de memória a partir da pilha conforme as necessidades e vinculam estes blocos em um tipo de estrutura de dados que usa ponteiros. Quando a estrutura de dados já não precisar de um bloco de memória, ela retorna o bloco à pilha para reutilização. Esta reciclagem faz uso eficiente da memória.

Para entender completamente estruturas de dados dinâmicas, precisamos começar com a pilha.

Estruturas de dados dinâmicas: a pilha

Um computador pessoal ou estação de trabalho tem normalmente, algo entre 16 e 64 megabytes de RAM instalados. Usando uma técnica chamada memória virtual, o sistema pode fazer trocas entre a memória e o disco rígido, criando para a CPU a ilusão de que ela possui mais memória, por exemplo, 200 a 500 megabytes. Enquanto esta ilusão se completa para a CPU, ela pode tornar as coisas extremamente lentas para o usuário. Apesar desta desvantagem, a memória virtual é uma técnica extremamente útil para "aumentar" a quantidade de RAM em uma máquina de um modo bem barato. Vamos supor, para fins de argumentação, que um computador tradicional possui espaço de memória total de, por exemplo, 50 megabytes (independentemente de a memória ser implementada no formato RAM físico ou virtual).

O sistema operacional   em uma máquina está encarregado dos 50 megabytes de espaço de memória. O sistema operacional usa o espaço de diversas maneiras, como exemplificamos aqui:

Page 43: Como funciona a programação em C

O sistema operacional e diversos aplicativos, juntamente com suas variáveis globais e espaços de pilha, consomem partes da memória.

Quando um programa completa a execução, libera sua memória para ser reutilizada por outros programas. Observe que parte do espaço de memória permanece inutilizada em qualquer momento.

Isto, obviamente, é uma idealização, porém os princípios básicos estão corretos. Como vocÊ pode ver, a memória contém o código executável para os diferentes aplicativos em execução na máquina, junto com o código executável do próprio sistema operacional. Cada aplicativo tem determinadas variáveis globais associadas a ele. Estas variáveis também consomem memória. Finalmente, cada aplicativo usa uma área da memória chamada pilha, que mantém todas as variáveis locais e parâmetros usados por qualquer função. A pilha também mantém a ordem de execução das funções, para que os retornos das funções ocorram corretamente. Sempre que uma função é executada, suas variáveis locais e parâmetros são "empurrados" sobre a pilha. Quando a função retorna, estes locais e parâmetros são "lidos e removidos". Por isso, o tamanho da pilha de um programa flutua constantemente conforme a execução do programa, mas tem um tamanho máximo.

Ao terminar a execução de um programa, o sistema operacional descarrega o programa, seus globais e seu espaço de pilha da memória. Um novo programa pode usar aquele espaço posteriormente. Desta forma, a memória em um sistema de computador é constantemente "reciclada" e reutilizada por programas, à medida que são executados e concluídos.

Page 44: Como funciona a programação em C

Em geral, cerca de 50% do espaço total de memória do computador pode estar ocioso em um dado momento. O sistema operacional possui e administra a memória não utilizada, coletivamente denominada de pilha. A pilha é extremamente importante, pois está disponível para aplicativos durante a execução usando as funções em C malloc (alocação de memória) e free. A pilha permite que programas aloquem a memória exatamente quando precisarem dela, durante a execução de um programa, em vez de pré-alocar a memória com uma declaração de matriz de tamanho específico.

Estruturas de dados dinâmicas: Malloc e Free

Vamos supor que você queira alocar certa quantidade de memória durante a execução de seu aplicativo. Você pode chamar a função malloc a qualquer momento e ela solicitará um bloco de memória da pilha. O sistema operacional reservará um bloco de memória para seu programa e você poderá usá-lo da maneira que quiser. Quando você termina de usar o bloco, você o retorna ao sistema operacional para reciclagem, invocando a função free. Depois, os outros aplicativos podem reservá-lo para seu próprio uso.

Por exemplo, o código a seguir demonstra o uso mais simples possível da pilha:

int main() { int *p; p = (int *)malloc(sizeof(int)); if (p == 0) { printf("ERRO: Sem memória\n"); return 1;

} *p = 5; printf("&d\n", *p); free(p); return 0; }

A primeira linha neste programa invoca a função malloc. Esta função faz três coisas:

1. Primeiro, a instrução malloc analisa a quantidade de memória disponível na pilha e pergunta: “Há memória suficiente disponível para alocar um bloco de memória do tamanho solicitado?". A quantidade de memória necessária para o bloco é conhecida a partir do parâmetro passado em malloc - neste caso, sizeof(int) é 4 bytes. Se não houver memória suficiente disponível, a função malloc retorna o endereço zero para indicar o erro (outro nome para zero é NULL e você verá que ele é usado por todo o código C). Caso contrário, a função malloc prossegue;

2. Se houver memória disponível na pilha, o sistema "aloca" ou "reserva" um bloco da pilha do tamanho especificado. O sistema reserva o bloco de memória de forma que ele não seja usado acidentalmente por mais de uma instrução malloc;

3. O sistema então coloca na variável do ponteiro (neste caso, p) o endereço do bloco reservado. A própria variável do ponteiro contém um endereço. O bloco alocado é capaz de manter um valor do tipo especificado e o ponteiro aponta para ele.

O seguinte diagrama mostra o estado de memória depois de executar malloc:

Page 45: Como funciona a programação em C

O bloco à direita é o bloco de memória malloc alocada

O programa então verifica o ponteiro para garantir que a solicitação de alocação ocorreu com a linha if (p == 0) (que também poderia ter sido escrita como if (p == NULL) ou mesmo if (!p) Em caso de falha da alocação (se p for zero) o programa encerra. Em caso de êxito na alocação, o programa inicializa o bloco com o valor 5, imprime o valor e executa a função free para retornar a memória à pilha antes do programa encerrar.

Não existe diferença entre este código e o código anterior que determine p igual ao endereço de um inteiro existente i. A única distinção é que, no caso da variável i, a memória existiu como parte do espaço de memória pré-alocado do programa e tem dois nomes: i e *p. No caso da memória alocada da pilha, o bloco tem o único nome de *p e é alocado durante a execução do programa. Duas dúvidas comuns:

É mesmo importante verificar se o ponteiro é zero após cada alocação? Sim. Como a pilha varia de tamanho constantemente, dependendo dos programas em execução e quantidade de memória que alocaram, etc., não há garantias de que uma invocação de malloc será bem sucedida. Você deve verificar o ponteiro depois de qualquer chamada a malloc para conferir se o ponteiro é válido.

O que acontece se eu esquecer de apagar um bloco de memória antes que o programa encerre? Quando um programa encerra, o sistema operacional "faz a limpeza" depois do encerramento, liberando o espaço de código executável, pilha, espaço de memória global e qualquer alocação de pilha para reciclagem. Assim, não há conseqüências de longo prazo em deixar as alocações pendentes no término do programa. Todavia, é considerada uma forma inadequada e os "vazamentos de memória" durante a execução de um programa são prejudiciais, como discutido abaixo.

Os dois programas a seguir mostram dois usos válidos e distintos de ponteiros. Tente distingüi-los usando um ponteiro e um valor de ponteiro: void main() { int *p, *q;

p = (int *)malloc(sizeof(int)); q = p; *p = 10; priprintf("%d\n", *q); *q = 20; pr printf("%d\n", *q); }

O resultado final deste código seria 10 n linha 4 e 20 na linha 6. Eis um diagrama:

Page 46: Como funciona a programação em C

O seguinte código é um pouco diferente:

void main() { int *p, *q; p = (int *)malloc(sizeof(int)); q = (int *)malloc(sizeof(int)); *p = 10; *q = 20; *p = *q; prprintf("%d\n", *p); }

O resultado final deste código seria 20 na linha 6. Eis um diagrama:

Observe que o compilador permitirá *p = *q pois *p e *q são ambos inteiros. Esta instrução diz: "Mova o valor inteiro apontado por q em um valor inteiro apontado por p". A instrução move os valores. O compilador também permitirá p = q, pois p e q são ambos ponteiros e apontam para o mesmo tipo (se s for um ponteiro para um caractere, p = s não é permitido pois eles apontam para tipos diferentes). A instrução p = q diz: "Aponte p para o mesmo bloco para o qual q aponta". Em outras

Page 47: Como funciona a programação em C

palavras, o endereço apontado por q é transferido para p, assim ambos apontam para o mesmo bloco. Esta instrução transfere os endereços.

De todos estes exemplos, você pode ver que há quatro modos diferentes para inicializar um ponteiro. Quando um ponteiro é declarado, como em int *p, ele inicia no programa em um estado não inicializado. Como ele pode apontar para qualquer lugar, remover sua referência é um erro. A inicialização de uma variável de ponteiro envolve apontá-la para um local conhecido na memória.

1. Um modo, como já visto, é usar a instrução malloc. Esta instrução aloca um bloco de memória da pilha e aponta o ponteiro para o bloco. Isso inicializa o ponteiro, pois ele agora aponta para um local conhecido. O ponteiro é inicializado pois foi preenchido com um endereço válido: o endereço do novo bloco;

2. O segundo modo, como visto recentemente, é usar uma instrução como p = q para que p aponte para o mesmo lugar que q. Se q estiver apontando para um bloco válido, então p é inicializado. O ponteiro p é carregado com o endereço válido contido em q. Entretanto, se q não for inicializado ou for inválido, p obterá o mesmo endereço inútil;

3. O terceiro modo é apontar o ponteiro para um endereço conhecido, como o endereço de uma variável global. Por exemplo, se i é um inteiro e p é um ponteiro de um inteiro, então a instruçãop=&i inicializa p apontando-o para i;

4. A quarta forma de inicialização de um ponteiro é usar um valor zero. Zero é um valor especial usado com ponteiros, como mostrado aqui: p = 0;

ou:

p = NULL;

O que isto faz fisicamente é colocar um zero em p. O endereço do ponteiro p é zero. Isso é geralmente diagramado como:

Qualquer ponteiro pode ser definido para apontar para zero. Quando p aponta para zero, ele não aponta para um bloco. O ponteiro simplesmente contém o endereço zero e este valor é útil como uma tag. Você pode usá-lo em declarações como: if (p == 0) { ... }

ou:

while (p != 0) { ... }

O sistema também reconhece o valor zero e gerará mensagens de erro se você remover a referência de um ponteiro zero. Por exemplo, no seguinte código:

p = 0;

Page 48: Como funciona a programação em C

*p = 5;

O programa geralmente travará. O ponteiro p não aponta para um bloco, aponta para zero, assim um valor não pode ser atribuído a *p. O ponteiro zero será usado como sinalizador quando chegarmos às listas vinculadas.

O comando malloc é usado para alocar um bloco de memória. Também é possível desalocar um bloco de memória quando ele não é mais necessário. Quando um bloco é desalocado, ele pode ser reutilizado por um comando malloc subseqüente que permite ao sistema reciclar a memória. O comando usado para desalocar a memória é chamado free e aceita um ponteiro como seu parâmetro. O comando free faz duas coisas:

5. O bloco de memória apontado pelo ponteiro não está reservado e é devolvido à memória livre na pilha. Ele pode ser reutilizado posteriormente por novas declarações;

6. O ponteiro permanece em estado não inicializado e deve ser reinicializado antes que possa ser novamente utilizado.

A instrução free simplesmente retorna um ponteiro para seu estado original não-inicializado e devolve o bloco para a pilha.

O exemplo a seguir mostra como usar a pilha: ele aloca um bloco de inteiros, preenche, escreve e o descarta:

#include <stdio.h> int main() { int *p; p = (int *)malloc(sizeof(int)); *p=10; printf("%d\n",*p); free(p); return 0; }

Este código só é útil para demonstrar o processo de alocação, desalocação e como usar um bloco em C. A linha malloc aloca um bloco de memória do tamanho especificado - neste caso, sizeof(int) bytes (4 bytes). O comando sizeof em C retorna o tamanho, em bytes, de qualquer tipo. A codificação poderia ter dito malloc(4), uma vez que sizeof(int) é igual a 4 bytes na maioria das máquinas. Todavia, o uso de sizeof torna a codificação mais portátil e legível.

A função malloc retorna um ponteiro para o bloco alocado. Este ponteiro é genérico. O uso do ponteiro sem conversão de tipo geralmente produz um tipo de aviso do compilador. O (int *) converte o ponteiro genérico retornado por malloc em um "ponteiro para um número inteiro", que é esperado por p. A instrução free em C retorna um bloco para a pilha para reutilização.

O segundo exemplo ilustra as mesmas funções que o exemplo anterior, mas usa uma estrutura em vez de um inteiro. Em C, o código se parece com:

#include <stdio.h> struct rec { int i; float f; char c; }; int main()

Page 49: Como funciona a programação em C

{ struct rec *p; p=(struct rec *) malloc (sizeof(struct rec)); (*p).i=10; (*p).f=3.14; (*p).c='a'; printf("%d %f %c\n",(*p).i,(*p).f,(*p).c); free(p); return 0; }

Observe a seguinte linha:

(*p).i=10;

Muitos se perguntam por que não funciona:

*p.i=10;

A resposta está relacionada com a precedência de operadores em C. O resultado do cálculo 5+3*4 é 17, e não 32, pois o operador * tem maior precedência do que + na maioria das linguagens de computação. Em C, o operador . tem precedência maior que *, portanto os parênteses forçam a devida precedência.

A maioria das pessoas se cansa de digitar (*p).i o tempo todo, de forma que o C oferece uma anotação de taquigrafia. As duas instruções a seguir são equivalentes, porém a segunda é mais fácil de digitar:

(*p).i=10; p->i=10;

Você verá a segunda instrução mais freqüentemente do que a primeira ao ler a codificação de outras pessoas.

Ponteiros avançados

Em geral, você usará os ponteiros de formas mais complexas do que aquelas mostradas em alguns dos exemplos anteriores. Por exemplo, é muito mais fácil criar um inteiro normal e trabalhar com ele do que criar e usar um ponteiro para um inteiro. Nesta seção serão explorados alguns dos modos mais comuns e avançados de trabalhar com ponteiros.

Tipos de ponteirosÉ possível, aceitável e benéfico criar tipos de ponteiros em C, como mostrado abaixo:

typedef int *IntPointer; ... IntPointer p;

Isso é o mesmo que dizer:

int *p;

Esta técnica será usada em muitos exemplos nas páginas a seguir. A técnica freqüentemente torna uma declaração de dados mais fácil de ler e compreender, facilitando também a inclusão de ponteiros em estruturas ou a passagem de parâmetros de ponteiros em funções.

Ponteiros para estruturas

Page 50: Como funciona a programação em C

É possível criar um ponteiro para virtualmente qualquer tipo em C, incluindo tipos definidos pelo usuário. É extremamente comum criar ponteiros para estruturas. Um exemplo é mostrado abaixo: typedef struct { char nome[21]; char cidade[21]; char estado[3]; } Rec; typedef Rec *RecPointer; RecPointer r; r = (RecPointer)malloc(sizeof(Rec));

O pointeiro r é um ponteiro de estrutura. Observe que, como r é um ponteiro, ele retira 4 bytes de memória como qualquer outro ponteiro. Porém, a instrução malloc aloca 45 bytes de memória da pilha.*r é uma estrutura como qualquer estrutura de tipo Rec. A codificação seguinte mostra os usos freqüentes de variável de ponteiro:

strcpy((*r).nome, "Leigh"); strcpy((*r).cidade, "Raleigh"); strcpy((*r).estado, "NC"); printf("%s\n", (*r).cidade); free(r);

Você lida com *r como lida com uma variável normal de estrutura, porém precisa ter cuidado com a precedência de operadores em C. Se deixasse de usar parênteses ao redor de *r, o código não compilaria, pois o operador "." tem maior precedência sobre o operador "*". Por se tornar tedioso digitar tantos parênteses ao trabalhar com ponteiros em estrutura, o C inclui uma anotação de taquigrafia que faz exatamente a mesma coisa:

strcpy(r->nome, "Leigh");

A notação r-> é equivalente ao (*r)., mas tem 2 caracteres a menos.

Ponteiros de matrizesTambém é possível criar ponteiros para matrizes:

int *p; int i;

p = (int *)malloc(sizeof(int[10])); for (i=0; i<10; i++) p[i] = 0; free(p);

ou:

int *p; int i; p = (int *)malloc(sizeof(int[10])); for (i=0; i<10; i++)

*(p+i) = 0; free(p);

Page 51: Como funciona a programação em C

Observe que ao criar um ponteiro para uma matriz de inteiros, você só precisa criar um ponteiro normal para int. A chamada para malloc aloca uma matriz de qualquer tamanho desejável e o ponteiro aponta para o primeiro elemento daquela matriz. Você pode indexar pela matriz apontada por p usando a indexação de matriz normal ou pode fazer isso usando a aritmética de ponteiros. A linguagem C vê ambas as formas como equivalentes.

Esta técnica particular é extremamente útil ao se trabalhar com strings. Ela permite alocar armazenamento suficiente para manter exatamente um string de determinado tamanho.

Matrizes de ponteirosÀs vezes, pode-se economizar bastante espaço ou resolver problemas de memória declarando uma matriz de ponteiros. No código de exemplo abaixo, uma matriz de 10 ponteiros de estruturas é declarada, em vez de declarar uma matriz de estruturas. Em vez disso, se uma matriz de estruturas tivesse sido criada, 243 * 10 = 2.430 bytes seriam necessários para a matriz. Usar a matriz de ponteiros permite que a matriz ocupe o menor espaço possível até que os registros reais sejam alocados com as instruções malloc. O código abaixo simplesmente aloca um registro, coloca um valor e descarta o registro para demonstrar o processo:

typedef struct {

char s1[81]; char s2[81];

char s3[81]; } Rec;

Rec *a[10]; a[0] = (Rec *)malloc(sizeof(Rec)); strcpy(a[0]->s1, "olá"); free(a[0]);

Page 52: Como funciona a programação em C

Estruturas contendo ponteirosAs estruturas podem conter ponteiros, como mostrado abaixo:

typedef struct {

char nome[21]; char cidade[21]; char telefone[21]; char *comentario; } Addr; Addr s; char comm[100];

gets(s.nome, 20); gets(s.cidade, 20); gets(s.telefone, 20); gets(com., 100); s.comentário = (char *)malloc(sizeof(char[strlen(comm)+1])); strcpy(s.comentario, com.);

Esta técnica é útil quando apenas alguns registros realmente contêm um comentário no campo de comentário. Se não houver nenhum comentário para o registro, então o campo consiste em apenas um ponteiro (4 bytes). Depois, estes registros que têm um comentário alocam exatamente o espaço necessário para manter a string de caracteres do comentário, com base na extensão do string digitada pelo usuário.

Ponteiros para ponteiros

Page 53: Como funciona a programação em C

É possível e freqüentemente útil criar ponteiros de ponteiros. Esta técnica é às vezes chamada dehandle e é útil em certas situações nas quais o sistema operacional precisa da capacidade de mover blocos de memória na pilha a seu critério. O exemplo a seguir demonstra um ponteiro de ponteiro: int **p; int *q;

p = (int **)malloc(sizeof(int *)); *p = (int *)malloc(sizeof(int)); **p = 12; q = *p; printf("%d\n", *q); free(q); free(p);

O Windows e o Mac OS usam esta estrutura para permitir a consolidação de memória na pilha. O programa administra o ponteiro p, enquanto o sistema operacional administra o ponteiro *p. Uma vez que o sistema operacional gerencia *p, o bloco apontado por *p (**p) pode ser movido e *p pode ser alterado para refletir a transferência sem afetar o programa que utiliza p. Os ponteiros de ponteiros também são freqüentemente usados em C para identificar parâmetros de ponteiros em funções.

Ponteiros para estruturas contendo ponteirosTambém é possível criar ponteiros para estruturas que contenham ponteiros. O exemplo seguinte usa o registro Addr da seção anterior:

typedef struct { char nome[21]; char cidade[21]; char telefone[21];

char *comentário; } Addr; Addr *s; char comm[100]; s = (Addr *)malloc(sizeof(Addr)); gets(s->nome, 20); gets(s->cidade, 20); gets(s->telefone, 20); gets(comm, 100); s->comentário = (char *)malloc(sizeof(char[strlen(comm)+1])); strcpy(s->comentário, comm);

Page 54: Como funciona a programação em C

O ponteiro s aponta para uma estrutura que contém um ponteiro que aponta para uma string de caracteres:

Neste exemplo, é muito fácil criar blocos perdidos se você não for cuidadoso. Por exemplo, aqui está uma versão diferente do exemplo AP.

s = (Addr *)malloc(sizeof(Addr)); gets(comm, 100); s->comentário = (char *)malloc(sizeof(char[strlen(comm)+1])); strcpy(s->comentário, comm); free(s);

Este código cria um bloco perdido, pois a estrutura que contém o ponteiro que aponta para a string é descartada antes que o bloco de string seja descartado, como mostrado abaixo:

LigaçãoFinalmente, é possível criar estruturas capazes de apontar para estruturas idênticas. Esta capacidade pode ser usada para ligar uma string de registros idênticos em uma estrutura denominada lista encadeada.

typedef struct { char nome[21]; char cidade[21]; char estado[21]; Addr *next; } Addr; Addr *first;

O compilador permitirá que você faça isso e, com um pouco de experiência, ele pode ser usado para criar estruturas como esta demonstrada abaixo:

Page 55: Como funciona a programação em C

Um exemplo de pilha encadeada

Um bom exemplo de estruturas dinâmicas de dados é uma biblioteca de pilha simples que utiliza uma lista dinâmica e inclui funções init, clear, push e pop. O arquivo de cabeçalho da biblioteca tem o seguinte aspecto: /* Biblioteca Estática. Esta biblioteca oferece as operações básicas de pilha para uma pilha de inteiros (facilmente alterável) */

typedef int stack_data;

extern void stack_init(); /* Inicializa esta biblioteca. Chama primeiro antes de chamar qualquer outra coisa. */

extern void stack_clear(); /* Apaga todas as entradas da pilha. */

extern int stack_empty(); * Retorna 1 se a pilha estiver vazia, caso contrário, 0. */ extern void stack_push(stack_data d); /* Força o valor d na pilha. */

extern stack_data stack_pop(); /* Retorna o elemento de cima da pilha, e o remove. Retorna lixo se a pilha estiver vazia. */

O arquivo de código da biblioteca vem a seguir:

#include "stack.h" #include <stdio.h> /* Biblioteca Estática. Esta biblioteca oferece as operações básicas de pilha para uma pilha de inteiros */

struct stack_rec { stack_data data; struct stack_rec *next; };

struct stack_rec *top=NULL;

void stack_init() /* Inicializa esta biblioteca. Chama antes de chamar qualquer outra coisa. */ { top=NULL; }

Page 56: Como funciona a programação em C

void stack_clear() /* Apaga todas as entradas da pilha. */ { stack_data x;

while (!stack_empty()) x=stack_pop(); }

int stack_empty() /* Retorna 1 se a pilha estiver vazia, caso contrário, 0. */ { if (top==NULL) return(1); else return(0); } void stack_push(stack_data d) /* Força o valor d na pilha. */ { struct stack_rec *temp; temp= (struct stack_rec *)malloc(sizeof(struct stack_rec)); temp->data=d; temp->next=top; top=temp; } stack_data stack_pop() /* Retorna o elemento de cima da pilha, e o remove. Retorna lixo se a pilha estiver vazia. */ { struct stack_rec *temp; stack_data d=0; if (top!=NULL) { d=top->data; temp=top; top=top->next; free(temp); } return(d); }

Observe como esta biblioteca realiza a ocultação de informações: quem vir apenas o arquivo de cabeçalho não poderá dizer se a pilha é implementada com matrizes, ponteiros, arquivos ou de outro modo. Observe também que a linguagem C usa NULL. NULL está definido em stdio.h, de forma que quase sempre você terá que incluir stdio.h quando usar ponteiros. NULL é o mesmo que zero.

Tente isto Adicione uma função dup, count e add para a

biblioteca de pilha duplicar o elemento do topo da pilha, retornar uma soma do número de elementos na pilha e adicionar os dois elementos de cima da pilha. Crie um programa de driver e um makefile e compile a biblioteca de pilha com o driver para verificar se funciona.

Erros que devem ser evitados na linguagem C Esquecer de incluir os parênteses quando

Page 57: Como funciona a programação em C

você fizer referência a um registro, como em (*p).i acima.

Falhar em descartar um bloco alocado. Por exemplo, você não deve dizer top=NULL na função stack, pois tal ação deixa órfãos os blocos que precisam ser descartados.

Esquecer de incluir stdio.h com qualquer operação de ponteiro, de forma que tenha acesso a NULL.

Como utilizar ponteiros com matrizes

Matrizes e ponteiros estão intimamente ligados em C. Para usar matrizes efetivamente, você precisa saber como usar os ponteiros com elas. O total entendimento da relação entre os dois provavelmente vai requerer dias de estudo e experiências, porém esse esforço será recompensado.

Vamos começar com um exemplo simples de matrizes em C:

#define MAX 10

void main(){    int a[MAX];    int i;    int *p;

    p=a;    for(i=0; i<MAX; i++)        a[i]=i;    printf("%d ",*p);}

Digite este código e tente compilá-lo. Você verá que a linguagem C não compilará. Para copiar a em b, você precisa digitar algo como:  

Ou, trocando em miúdos:     

Melhor ainda, use o utilitário memcpy em string.h.  

As matrizes em C são incomuns, enquanto as variáveis a e b não são, tecnicamente, matrizes.  Em vez disso, elas são ponteiros permanentes para matrizes. a e b apontam permanentemente para os primeiros elementos de suas respectivas matrizes (elas mantêm os endereços de a[0] e b[0], respectivamente). Visto que são ponteiros permanentes, você não pode alterar seus endereços.  Logo, a instrução a=b; não funciona.  

Sendo a e b ponteiros, você pode fazer várias coisas interessantes com ponteiros e matrizes. Por exemplo, o código seguinte funciona:    

A instrução p=a; funciona pois a é um ponteiro. Tecnicamente, a aponta para o endereço do elemento zero da matriz. Como este elemento é um inteiro, a é um ponteiro de inteiro simples.  Assim sendo, declarar p como um ponteiro de um inteiro e defini-lo como igual a a funciona.  Outro modo de dizer a mesma coisa seria substituir p=a; por p=&a[0];. Considerando que a contém o endereço de a[0], a e&a[0] significam a mesma coisa.  

A figura abaixo mostra o estado das variáveis antes de iniciar a execução do loop for:   

Page 58: Como funciona a programação em C

Agora que p está apontando para o elemento 0 de a, você pode fazer algumas coisas incomuns com ele.  A variável a é um ponteiro permanente e não pode ser alterada, mas p não está sujeita a tais restrições.  Na verdade, a linguagem C o estimula a mudá-lo usando a aritmética de ponteiro. Por exemplo, se você disser p++;, o compilador sabe que p aponta para um inteiro e esta instrução aumentap o número apropriado de bytes para transferi-lo para o próximo elemento na matriz. Se p apontasse para uma matriz de estruturas de 100 bytes, p++; mudaria p para mais de 100 bytes. A linguagem C se encarrega dos detalhes de tamanho de elemento.   

Você também pode copiar a matriz a em b usando ponteiros.  O seguinte código pode substituir (para i=0; i<MAX; a[i]=b[i], i++);:    

Você pode abreviar este código da seguinte maneira:     

Você pode abreviar ainda mais:   

E se você for além do final da matriz a ou b com os ponteiros p ou q?  Para a linguagem C isso não importa, pois ela continua aumentando p e q, copiando sobre outras variáveis. Você precisa ter cuidado ao indexar em matrizes em C, porque a linguagem C assume que você sabe o que está fazendo.   

Você pode passar uma matriz como a ou b para uma função de dois modos diferentes.  Imagine uma função dump que aceita uma matriz de inteiros como um parâmetro e imprime o conteúdo da matriz para stdout.  Há duas maneiras de codificar dump:  

ou:      

A variável nia (number_in_array) é necessária para que o tamanho da matriz seja conhecido.  Note que apenas um ponteiro para a matriz, em vez do conteúdo da matriz, passa para a função. Observe também que as funções em C podem aceitar matrizes de tamanho variável como parâmetros.   

Strings de caracteres

As strings de caracteres em C são profundamente interligadas aos ponteiros. Você precisa se familiarizar com os conceitos de ponteiros abordados nos artigos anteriores para usar as strings de caracteres em C de forma eficaz. Uma vez habituado com elas, você pode realizar manipulações de strings de caracteres de forma bastante eficiente.

Uma string de caracteres em C é apenas uma matriz de caracteres. A seguinte linha declara uma matriz que pode conter uma string de até 99 caracteres.

char str[100];

Page 59: Como funciona a programação em C

Ela contém caracteres, como você pode esperar: str[0] é o primeiro caractere da string, str[1] é o segundo caractere e assim por diante. Mas por que uma matriz de 100 elementos não é capaz de suportar 100 caracteres? Porque a linguagem C utiliza strings de caracteres de terminação nula, significando que o final de qualquer string de caracteres é marcado por um valor ASCII de 0 (o caractere nulo), também representado em C como ''.

A terminação nula é muito diferente do modo que outras linguagens lidam com strings de caracteres. Por exemplo, em Pascal, cada string de caracteres consiste em uma matriz de caracteres, com byte de extensão que mantém a contagem do número de caracteres armazenada na matriz. Esta estrutura dá ao Pascal uma vantagem definitiva ao indagar a extensão de uma string de caracteres. O Pascal pode simplesmente devolver o byte de extensão, enquanto a linguagem C precisa contar os caracteres até encontrar. Este fato torna a linguagem C muito mais lenta do que a Pascal em alguns casos, mas mais rápida em outros, como veremos nos exemplos a seguir.

Visto que a linguagem C não oferece qualquer apoio explícito para strings de caracteres na própria linguagem, todas as funções de manipulação de strings de caracteres são implementadas em bibliotecas. As operações de I/O de strings de caracteres (gets, puts, etc.) são implementadas em <stdio.h> e um conjunto de funções de manipulação de strings de caracteres bastante simples é implementado em <string.h> (em alguns sistemas, <strings.h>).

O fato das strings de caracteres não serem nativas em C força a criação de alguma codificação indireta. Por exemplo, suponha que você queira atribuir uma string de caracteres a outra, ou seja, deseja copiar o conteúdo de uma string de caracteres para outra. Em C, como vimos no último artigo, você não pode simplesmente atribuir uma matriz à outra. Você precisa copiar elemento por elemento. A biblioteca de strings (<string.h> ou <strings.h>) contém uma função chamada strcpy para esta tarefa. Eis uma parte extremamente comum de codificação encontrada em um programa em C comum:

char s[100]; strcpy(s, "olá");

Depois que estas duas linhas são executadas, o seguinte diagrama mostra o conteúdo de s:

O diagrama acima mostra a matriz com seus caracteres. O diagrama inferior mostra os valores em ASCII equivalentes aos caracteres e é assim que a linguagem C vê uma string de caracteres (como uma matriz de bytes contendo valores inteiros). Veja Como funcionam os bits e os bytes para uma explicação sobre os códigos ASCII.

O seguinte código mostra como usar strcpy em C:

#include <string.h>

Page 60: Como funciona a programação em C

int main() { char s1[100],s2[100]; strcpy(s1,"Olá"); /* copy "Olá" em s1 */ strcpy(s2,s1); /* copy s1 em s2 */ return 0; }

strcpy é usado sempre que uma string de caracteres é inicializada em C. Você utiliza a ação strcmp na biblioteca de strings de caracteres para comparar duas seqüências. Ela retorna um inteiro que indica o resultado da comparação. Zero significa duas strings de caracteres iguais, um valor negativo significa que s1 é menor que s2 e um valor positivo significa que s1 é maior que s2.

#include <stdio.h> #include <string.h>

int main() { char s1[100],s2[100]; gets(s1); gets(s2); if (strcmp(s1,s2)==0) printf("igual\n"); else if (strcmp(s1,s2)<0) printf("s1 menor que s2\n"); else printf("s1 maior que s2\n"); return 0; }

Outras funções comuns na biblioteca de strings de caracteres incluem strlen, que retorna a extensão de uma string, e strcat, que concatena duas strings. A biblioteca de strings de caracteres contém várias outras funções que você pode usar lendo a página principal.

Para começar a criar funções de strings de caracteres e ajudá-lo a compreender o código de outro programador (cada um tem seu próprio conjunto de funções para fins especiais em um programa), veremos 2 exemplos, strlen e strcpy. Veja a seguir uma versão estritamente em Pascal de strlen:

int strlen(char s[]) { int x; x=0; while (s[x] != '\0') x=x+1; return(x); }

A maioria dos programadores de C evita esta abordagem por parecer ineficiente. Em vez disso, quase sempre usam uma abordagem com base em ponteiros:

int strlen(char *s) { int x=0; while (*s != '\0') { x++; s++; } return(x); }

Page 61: Como funciona a programação em C

Você pode abreviar esta codificação da seguinte maneira:

int strlen(char *s) { int x=0; while (*s++) x++; return(x); }

Acredito que um verdadeiro especialista em C encurtaria ainda mais esta codificação.

Quando compilo estes três trechos de código em um MicroVAX com gcc, sem qualquer otimização, e executo cada um 20 mil vezes em uma string de 120 caracteres, a primeira parte do código gera um tempo de 12,3 segundos, a segunda 12,3 segundos e a terceira 12,9 segundos. O que isso significa? Para mim, significa que você deve escrever o código do modo mais fácil para o seu entendimento. Os ponteiros em geral produzem um código mais rápido, porém o código strlen acima mostra que nem sempre isso ocorre.

Podemos passar pela mesma evolução com strcpy:

strcpy(char s1[],char s2[]) { int x; for (x=0; x<=strlen(s2); x++) s1[x]=s2[x]; }

Observe aqui que <= é importante no loop for porque a codificação copia o ''. Certifique-se de copiar ''. Os bugs importantes ocorrem mais tarde se você não os excluir, pois a string de caracteres não tem fim e, portanto, tamanho desconhecido. Observe também que este código é muito ineficiente, pois strlen é sempre invocado pelo loop for. Para resolver este problema, você pode usar a seguinte codificação:

strcpy(char s1[],char s2[]) { int x,len; len=strlen(s2); for (x=0; x<=len; x++) s1[x]=s2[x]; }

A versão de ponteiro é similar.

strcpy(char *s1,char *s2) { while (*s2 != '\0') { *s1 = *s2; s1++; s2++; } }

Você pode abreviar esta codificação ainda mais:

strcpy(char *s1,char *s2) { while (*s2) *s1++ = *s2++; }

Page 62: Como funciona a programação em C

Se quiser, pode até mesmo dizer while (*s1++ = *s2++);. A primeira versão de strcpy leva 415 segundos para copiar uma string de 120 caracteres 10 mil vezes, a segunda versão leva 14,5 segundos, a terceira versão 9,8 segundos e a quarta 10,3 segundos. Como você pode ver, aqui os ponteiros oferecem um aumento significativo de desempenho.

O protótipo para a função strcpy na biblioteca de strings indica que ele foi projetado para retornar um ponteiro para uma string:

char *strcpy(char *s1,char *s2)

A maioria das funções de strings de caracteres retorna um ponteiro de seqüência como resultado, estrcpy retorna o valor de s1 como resultado.

Às vezes, usar ponteiros com strings de caracteres pode resultar em melhorias definidas em velocidade, e você pode tirar vantagem dela se pensar um pouco a respeito. Por exemplo, suponha que queira remover os espaços em branco iniciais de uma string. Você talvez queira transferir os caracteres para cima das partes em branco para removê-las. Em C, você pode evitar o movimento completamente:

#include <stdio.h> #include <string.h> int main() { char s[100],*p; gets(s); p=s; while (*p==' ') p++; printf("%s\n",p); return 0; }

Isto é muito mais rápido do que a técnica de transferência, especialmente para strings de caracteres longas.

Você aprenderá muitos outros truques com strings de caracteres, à medida que praticar e ler outras codificações. Praticar é a chave.

Nota especial sobre strings de caracteres

Nota especial sobre strings de caracteresSuponha que você crie os seguintes fragmentos de códigos e os execute:

Fragment 1 { char *s;

s="hello"; printf("%s\n",s); } Fragment 2

{ char s[100]; strcpy(s,"hello"); printf("%s\n",s); }

Page 63: Como funciona a programação em C

Estes dois fragmentos produzem o mesmo resultado, porém seu comportamento interno é bastante diferente. No fragmento 2, você não pode dizer s="hello";. Para entender as diferenças, você precisa entender como funciona a tabela de constantes de strings de caracteres em C.

Quando seu programa é compilado, o compilador cria o arquivo objeto que contém seu código de máquina e uma tabela com todas as constantes de strings declaradas no programa. No fragmento 1, a instrução s="hello"; faz com que s aponte para o endereço da string de caracteres hello na tabela de constantes de string. Como esta string está na tabela de constantes de string, tecnicamente parte do código executável, não pode ser modificada. Você só pode apontar para ela e utilizá-la para leitura.

No fragmento 2, a seqüência hello também existe na tabela de constantes, portanto você pode copiá-la na matriz de caracteres denominada s. Visto que s não é um ponteiro, a instrução s="hello"; não funcionará no fragmento 2. Ele sequer compilará.

Nota especial sobre o uso de seqüências de caracteres com mallocSuponha que você escreveu o seguinte programa:

int main() { char *s;

s=(char *) malloc (100); s="hello"; free(s); return 0; }

Ele compila corretamente, mas indica uma falha de segmentação na linha free ao ser executado. A linha malloc aloca um bloco de memória de 100 bytes e aponta s para ele, porém agora a linhas="hello"; é um problema. Ela está sintaticamente correta, pois s é um ponteiro, mas quandos="hello"; é executado, s aponta para a seqüência na tabela de constante de seqüências e o bloco alocado é deixado órfão. Considerando que s está apontando para a tabela de constante de string, a string de caracteres não pode ser alterada e free falha, pois não consegue desalocar um bloco na região executável.

A codificação correta é a seguinte:

int main() { char *s;

s=(char *) malloc (100); strcpy(s,"hello"); free(s); return 0; }

Tente isto Crie um programa que lê em uma seqüência

contendo um nome seguido por um espaço, seguido por um sobrenome. Escreva funções para remover espaços em branco no início ou no final. Escreva outra função que retorne o sobrenome;

Escreva uma função que converte uma

Page 64: Como funciona a programação em C

seqüência em maiúsculas; Escreva uma função que obtenha a primeira

palavra de uma string de caracteres e retorne o restante da string.

Erros que devem ser evitados na linguagem CPerder o caractere , o que é fácil se você não for cuidadoso, pode resultar em alguns bugs. Certifique-se de copiar ao copiar as strings de caracteres. Se você criar uma nova string, certifique-se de colocar nela. Se copiar uma string para outra, certifique-se de que a string receptora é suficientemente grande para comportar a string de origem, incluindo . Finalmente, se você apontar um ponteiro de caracteres para alguns caracteres, certifique-se de terminá-los com .

Precedência de operador

A linguagem C contém muitos operadores, e por causa do modo como funciona a precedência, as interações entre múltiplos operadores podem se tornar confusas.

x=5+3*6;

X recebe o valor 23, não 48, pois em C a multiplicação e divisão têm maior precedência do que a adição e subtração.

char *a[10];

a é um ponteiro simples para uma matriz de 10 caracteres, ou uma matriz de 10 para caractere? A menos que você saiba as convenções de precedência em C, não há como descobrir. Semelhantemente, em E.11, vimos que por conta das instruções de precedência como *p.i = 10; não funciona. Em vez disso, a forma (*p).i = 10; deve ser usada para forçar a precedência correta.

A tabela a seguir de Linguagem de programação em C, de Kernighan e Ritchie, mostra a hierarquia de precedência em C. A linha de cima tem a maior precedência.

Operadores Associação ( [ - . Esquerda para direita ! - ++ -{- + * & (type-cast) sizeof Direita para esquerda (na linha acima , +, - e * estão em formas unárias) * / % Esquerda para direita + - Esquerda para direita << >> Esquerda para direita < <= > >= Esquerda para direita == != Esquerda para direita & Esquerda para direita ^ Esquerda para direita | Esquerda para direita && Esquerda para direita || Esquerda para direita ?: Esquerda para direita = += -= *= /= %= &= ^= |= <<= >>= Direita para esquerda , Esquerda para direita

Usando esta tabela, você pode ver que char *a[10]; é uma matriz de 10 ponteiros para caractere. Você também pode ver por que os parênteses são necessários se (*p).i for manipulado corretamente. Após adquirir prática, você memorizará a maior parte desta tabela, mas, às vezes, alguma coisa poderá não funcionar devido a algum problema de precedência.

Page 65: Como funciona a programação em C

Argumentos de linha de comando

A linguagem C oferece um mecanismo bastante simples para recuperar parâmetros de linha de comando digitados pelo usuário. Ele passa um parâmetro argv para a função principal no programa. As estruturas argv aparecem em número expressivo das mais avançadas chamadas de biblioteca, portanto compreendê-las é útil a qualquer programador de C.

Digite o seguinte código e compile-o:  

#include <stdio.h>

int main(int argc, char *argv[]){    int x;

    printf("%d\n",argc);     for (x=0; x<argc; x++)       printf("%s\n",argv[x]);     return 0;}

Neste código, o programa principal aceita dois parâmetros, argv e argc. O parâmetro argv é uma matriz de ponteiros para string que contém os parâmetros digitados quando o programa foi chamado na linha de comando do UNIX. O inteiro argc contém a soma do número de parâmetros. Esta parte do código representa os parâmetros da linha de comando. Para testar, compile o código em um arquivo executável denominado aaa e digite aaa xxx yyy zzz. O código imprimirá os parâmetros de linha de comando xxx, yyy e zzz, um em cada linha.  

A linha char *argv[] é uma matriz de ponteiros para string. Em outras palavras, cada elemento da matriz é um ponteiro e cada ponteiro aponta para uma string (tecnicamente, ao primeiro caractere da string). Assim, argv[0] aponta para uma string de caracteres que contém o primeiro parâmetro na linha de comando (o nome do programa), argv[1] aponta para o próximo parâmetro e assim sucessivamente. A variável argc indica quantos ponteiros na matriz são válidos. Você verá que o código precedente não faz nada além de imprimir cada string válida apontada por argv.   

Por haver o argv, você pode permitir que o programa reaja aos parâmetros de linha de comando digitados pelo usuário de forma fácil. Por exemplo, você pode querer que seus programas detectem a palavra help como o primeiro parâmetro depois do nome do programa e esvaziem um arquivo de ajuda em stdout.  Os nomes de arquivos também podem ser passados e usados em suas instruções fopen.  

Arquivos binários

Arquivos binários são bem parecidos com matrizes de estruturas, exceto pelas estruturas, que residem no arquivo de disco, em vez de uma matriz na memória. Uma vez que as estruturas em um arquivo binário estão em disco, você pode criar coleções muito grandes desses arquivos (limitadas apenas pelo espaço em disco disponível). Elas também são permanentes e estão sempre disponíveis. A única desvantagem é a lentidão decorrente do tempo de acesso ao disco.

Os arquivos binários têm duas características que os distinguem de arquivos de texto:

Você pode pular instantaneamente para qualquer estrutura no arquivo que ofereça acesso aleatório como em uma matriz;

Você pode mudar o conteúdo de uma estrutura em qualquer lugar e hora no arquivo.

Page 66: Como funciona a programação em C

Arquivos binários geralmente têm tempos de leitura e escrita mais rápidos que os arquivos de texto, já que uma imagem binária do registro é armazenada diretamente da memória para o disco (ou vice-versa). Em um arquivo de texto, tudo precisa ser convertido de trás para frente em texto e isto consome tempo.

A linguagem C suporta o conceito de arquivo de estruturas de forma básica. Ao abrir o arquivo, você pode ler uma estrutura, escrever uma estrutura ou pesquisar qualquer estrutura no arquivo. Este conceito de arquivo suporta o conceito de um ponteiro de arquivo. Quando ele é aberto, o ponteiro aponta para o registro 0 (o primeiro registro no arquivo). Qualquer operação de leitura lê a estrutura apontada e move o ponteiro um nível abaixo na estrutura. Qualquer operação de escrita grava na estrutura apontada e move o ponteiro um nível abaixo na estrutura. Seek move o ponteiro para o registro solicitado.

Lembre-se de que a linguagem C considera tudo no arquivo de disco como blocos de bytes lidos do disco na memória ou lidos da memória para o disco. A linguagem C usa um ponteiro de arquivo, mas pode apontar para qualquer byte no arquivo.

O programa abaixo ilustra estes conceitos:

#include <stdio.h>

/* descrição de registro aleatório - pode ser qualquer uma */ struct rec { int x,y,z; };

/* grava e depois lê 10 registros arbitrários do arquivo "junk". */ int main() { int i,j; FILE *f; struct rec r;

/* cria o arquivo de 10 registros */ f=fopen("junk","w"); if (!f) return 1; for (i=1;i<=10; i++) { r.x=i; fwrite(&r,sizeof(struct rec),1,f); } fclose(f);

/* lê os 10 registros */ f=fopen("junk","w"); if (!f) return 1; for (i=1;i<=10; i++) { fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); } fclose(f); printf("\n"); /* usa fseek para ler os 10 registros em ordem inversa */

Page 67: Como funciona a programação em C

f=fopen("junk","w"); if (!f) return 1; for (i=9; i>=0; i--) { fseek(f,sizeof(struct rec)*i,SEEK_SET); fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); } fclose(f); printf("\n"); /* usa fseek para ler todo os outros registros */ f=fopen("junk","r"); if (!f) return 1; fseek(f,0,SEEK_SET); for (i=0;i<5; i++) { fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); fseek(f,sizeof(struct rec),SEEK_CUR); } fclose(f); printf("\n");

/* usa fseek para ler o 4º registro, altere-o e escreva-o de volta */ f=fopen("junk","r+"); if (!f) return 1; fseek(f,sizeof(struct rec)*3,SEEK_SET); fread(&r,sizeof(struct rec),1,f); r.x=100; fseek(f,sizeof(struct rec)*3,SEEK_SET); fwrite(&r,sizeof(struct rec),1,f); fclose(f); printf("\n"); /* lê os 10 registros para garantir que o 4º arquivo foi alterado */ f=fopen("junk","r"); if (!f) return 1; for (i=1;i<=10; i++) { fread(&r,sizeof(struct rec),1,f); printf("%d\n",r.x); } fclose(f); return 0; }

Neste programa, uma descrição de estrutura rec foi usada, mas você pode usar a descrição de estrutura que quiser. Você também pode ver que fopen e fclose trabalham exatamente da mesma forma que nos arquivos de texto.

As novas funções aqui são fread, fwrite e fseek. A função fread captura quatro parâmetros:

um endereço de memória;

Page 68: Como funciona a programação em C

o número de bytes a ser lido por bloco; o número de blocos a ser lido; a variável de arquivo.

Assim, a linha fread(&r,sizeof(struct rec),1,f); diz para ler 12 bytes (o tamanho de rec) do arquivo f (a partir do local atual do ponteiro de arquivo) no endereço de memória &r. Um bloco de 12 bytes é solicitado. Seria fácil ler 100 blocos por disco em uma matriz na memória alterando 1 para 100.

A função fwrite opera da mesma forma, mas move o bloco de bytes da memória para o arquivo. A função fseek move o ponteiro de arquivo para um byte no arquivo. Em geral, você muda o ponteiro em incrementos de sizeof(struct rec) para manter o ponteiro nos limites do registro. Você pode usar três opções ao pesquisar:

SEEK_SET SEEK_CUR SEEK_END

SEEK_SET move o ponteiro x bytes para baixo, a partir do início do arquivo (desde o byte 0 no arquivo). SEEK_CUR move o ponteiro x bytes para baixo da posição atual do ponteiro. SEEK_ENDmove o ponteiro do final do arquivo (com esta opção você precisa usar offsets negativos).

Várias opções diferentes aparecem no código acima. Observe em particular a seção na qual o arquivo é aberto com o modo r+. Isto abre o arquivo para leitura e gravação, permitindo que os registros sejam alterados. O código busca um registro, lê e altera um campo. Depois busca novamente, pois a leitura deslocou o ponteiro, e grava de volta a alteração.