

Básico da Linguagem C
Serão abordados tópicos fundamentais para a elaboração de programas
em C, desde programas simples até rotinas mais sofisticadas usando estruturas
de dados. Veremos também algumas técnicas para tratamento de arquivos em disco
nos sistemas de micros da linha PC, disponíveis nos laboratórios.
Nesta linha de raciocínio, serão abordados tópicos exclusivos do compilador TURBO C da Borland, uma vez que este será o compilador usado nos testes de laboratório, por sua flexibilidade como ferramenta para desenvolvimento de programas.
Em 1969, os laboratórios Bell procuravam uma alternativa para
o sistema operacional Multics para o computador PDP-7. Uma versão básica do
sistema operacional UNIX foi então escrita em Assembly. Neste mesmo período,
uma linguagem experimental estava sendo desenvolvida por Keneth Thompson.
Esta linguagem se chamava B, e a partir dela foi projetada a linguagem C,
em 1972.
Logo depois, em 1973, o sistema operacional UNIX foi melhorado
e cerca de 90% de seu código foi escrito em C. Por causa desta libertação
do Assembly, UNIX ( e conseqüentemente C) adquiriu grande portabilidade, foi
rapidamente adaptado a uma série de computadores e seu uso não parou de crescer.
No final da década de 70 e inicio da década de 80, a proliferação de UNIX
e C foi muito grande e chegou até os micros. Além do que, C ficou independente
do sistema operacional UNIX e uma série de compiladores C surgiram para muitos
equipamentos.
Hoje, C é a linguagem preferida dos programadores profissionais
por várias razões, e chega a substituir Assembly em boa parte do software
recentemente desenvolvido.
Basta dar uma olhada nas revistas e livros que trazem algoritmos
e dicas para programadores, vê-se que as implementações que, há alguns anos,
vinham em PASCAL ou Assembly, estão hoje quase totalmente escritas em C.
Com o advento da Programação Orientada para Objetos (OOP -
Object Oriented Programming), a linguagem que se tornou mais usada para esta
técnica de programação é uma extensão da linguagem C, chamada C++.
A linguagem C é basicamente muito simples, mas a maioria dos
compiladores vêm com uma grande quantidade de funções já criadas e compiladas
em bibliotecas que são juntadas às definidas pelo programador durante o processo
de compilação.
A estrutura básica de um programa C é a seguinte:
| comentários |
| diretivas de compilação |
| definições globais |
| protótipos de funções |
| definição das funções |
Comentários podem, e devem, estar em qualquer ponto do programa,
mas é aconselhável colocar logo no inicio do programa algumas informações
como: nome do programa, programador, período de elaboração, para que serve,
se é parte de algum sistema maior, restrições de acesso por motivo de segurança,
etc. Os comentários são escritos entre os delimitadores /* e */,
e desta forma não são considerados pelo compilador.
Diretivas de compilação não são instruções da linguagem C,
são mensagens ao compilador para que execute alguma tarefa no momento da compilação.
Deve haver uma linha inteira para cada diretiva , que são iniciadas pelo caracter
#. As diretivas mais comuns são #include e #define, respectivamente
usadas para, especificar bibliotecas de funções a serem incorporadas na compilação,
e macro substituições, como veremos mais adiante.
Definições globais normalmente são especificações de constantes,
tipos e variáveis que serão válidas em todas as funções que formam o programa.
Embora sejam de relativa utilidade, não é boa prática de programação
definir muitas variáveis globais, pois um descuido em algum ponto do programa,
alterando seus valores, pode provocar problemas em muitos outros pontos, uma
vez que tais variáveis são visíveis em todas as partes do programa.
Protótipos de funções são semelhantes aos cabeçalhos das funções
e não são obrigatórios. Os protótipos são usados pelo compilador para fazer
verificações durante a compilação, para ver se as partes do programa que acionam
tais funções o fazem de modo correto, com nome certo, número e tipo de parâmetros
corretos, etc. Esta é a melhor maneira de programar em C.
Como foi dito, um programa C é basicamente uma coleção de funções
que fazem o papel dos subprogramas em outras linguagens, como por exemplo
PASCAL. Em C temos apenas funções e não as procedures do PASCAL, mas temos
grande liberdade para trabalhar com estas funções, podendo inclusive desprezar
seu valor de retorno, acionando-as como as procedures em PASCAL.
Uma das funções deve ter o nome "main", e esta será
a função por onde começa a execução do programa. Equivale à rotina principal
dos programas PASCAL.
Não há ordem obrigatória para codificar as funções. No entanto
procuraremos sempre começar pela função main, para facilitar a tarefa
de manutenção do programa. Todas as outras funções serão codificadas em seguida,
com a liberdade de que uma função pode acionar uma outra mesmo que esta não
tenha sido ainda definida, liberdade esta, proprocionada pelos protótipos.
A seguir vem um exemplo de programa C, apenas como ilustração.
/* Exemplo.C
Programa para calcular e exibir a soma de dois inteiros digitados.
Por sua simplicidade, este programas não tem
variáveis globais. */
#include <stdio.h> /* diretiva */
int sum(int a, int b); /* protótipo */
void main() /* função principal*/
{
int a, b, c;
printf("Digite dois numeros inteiros: ");
scanf("%d %d", &a, &b);
c = sum(a,b);
printf("Resultado da soma: %d \n", c);
}
int sum(int a, int b) /* função soma */
{
return a+b ;
}
Finalmente, como se vê no exemplo acima, não é obrigatório,
mas aconselhável, colocarmos uma instrução em cada linha.
A forma geral para esta diretiva é a seguinte:
#include <nome do arquivo>
ou
#include "nome do arquivo"
A diferença entre estas duas formas está no local onde o compilador
vai procurar o arquivo no momento da compilação. No primeiro formato, o arquivo
é procurado no diretório já definido pelo TURBO C como sendo aquele que contem
os "header files", isto é, os arquivos com extensão .h que contém definições
de tipos, dados e funções já prontas que vem com o TURBO C. O segundo formato
é usado quando queremos que o TURBO busque o arquivo especificado no diretório
atual do disco. Esta forma é usada normalmente quando queremos incorporar
arquivos por nós criados e salvos no diretório atual.
Aqui estão alguns arquivos já prontos e usados com maior frequência:
| stdio.h | rotinas padrão de entrada e saída definidas pelos criadores da linguagem C. |
| alloc.h | funções para gerenciamento de memória |
| float.h | funções para tratar números de ponto flutuante |
| math.h | funções matémáticas |
| stddef.h | vários tipos de dados e macro substituições |
| stdlib.h | várias rotinas muito usadas, conversão, sort, etc.. |
| string.h | rotinas p/ manipular strings e memória |
O leitor deve procurar o manual do compilador para maiores
informações sobre estes arquivos a partir do momento que se fizerem necessários
às suas aspirações.
No momento, veremos como substituir palavras por valores. A
sintaxe seria algo assim:
#define palavra valor
Exemplo:
#define imposto 0.25
#define pi 3.1415
#define e 2.718281828
Basicamente, temos os seguintes tipos:
| int | inteiros entre -32768 e 32767 |
| char | inteiros entre -128 e 127 |
| float | reais entre 3.4e-38 e 3.4e+38 (positivos e negativos) |
| double | reais entre 1.7e-3.8 e 1.7e+3.8 (positivos e negativos) |
Há ainda o modificador long, que se aplica ao tipo int, e o
modificador unsigned, que se aplica aos tipos int e char , além de poder ser
usado com long. Estes modificadores indicam outra forma de representação interna
e assim produzem novas faixas de valores representáveis. Em C, unsigned int
pode ser abreviado para apenas unsigned, e long int para apenas long.
| unsigned int (ou unsigned) | 0..65535 |
| unsigned char | 0..255 |
| long int (ou long) | 2147483648..2147483647 |
| unsigned long | 0..4294967295 |
Para criar tais "nomes" em C, há algumas regras:
· começar sempre por uma letra ou sublinha,
· somente um certo numero de caracteres é considerado, dependendo do compilador (normalmente 32),
· letras maiúsculas são DIFERENTES de minúsculas
Para declarar variáveis nos programas devemos especificar seus
tipos e nomes da seguinte forma:
tipo nome, nome, ...;
Exemplo:
int a, b, c;
float x, y;
char ch;
As variáveis podem ser basicamente, globais ou locais. As globais
são aquelas definidas fora de qualquer função do programa e são válidas (visíveis)
por qualquer parte do programa a partir do ponto em que está sua definição.
As locais são definidas dentro de alguma função e são válidas somente nesta
função.
No momento da declaração, as variáveis podem ser inicializadas
com algum valor. Para isto basta colocar à frente do nome da variável, o símbolo
= e o valor desejado.
Exemplo:
int a = 0, b = 1;
O protótipo de uma função tem o mesmo formato que o cabeçalho
da função.
tipo nome da função ( parâmetros ) ;
O ponto e vírgula no final é obrigatório, o tipo é referente
ao valor que a função retorna, o nome da função é um identificador definido
pelo programador, e os parâmetros são especificados um a um com tipo e nome,
ou somente tipo.
Exemplo:
int sum(int a, int b);
float raiz(float);
void tela(void);
O um tipo chamado "void" serve para indicar que a função
não retorna nenhum valor. Este tipo é indicado nos casos em que a função será
acionada como se fosse uma procedure do PASCAL, e não dentro de uma expressão.
Protótipos em que especificamos o tipo void entre parênteses indicam
que a função não tem parâmetros.
Sua forma geral é a seguinte:
tipo nome da função( parâmetros )
{
corpo da função
}
O cabeçalho tem a forma dos protótipos, e o corpo da função
vem escrito entre chaves. Estas chaves fazem o papel de BEGIN e END em PASCAL.
O cabeçalho tem os parâmetros definidos completamente, isto
é, com tipo e nome de cada um, ou com os parênteses vazios, no caso de não
haver parâmetros.
No corpo da função são feitas as definições locais e, usando
os comandos da linguagem C , é implementado o algoritmo da tarefa que a função
executa.
Uma coisa muito importante a entender é que C nos dá uma grande
liberdade para trabalhar com funções, ao ponto de podermos simplesmente desprezar
seu valor de retorno acionando-as como se fossem procedures em PASCAL. No
entanto, é preciso estar atento a certas particularidades da linguagem C,
principalmente no que se refere aos parâmetros.
Em PASCAL, é possível passar parâmetros de dois tipos às subrotinas,
tipo valor ou tipo variável. Usamos o tipo variável (escrevendo a palavra
VAR antes do nome do parâmetro) quando desejamos receber de volta um novo
valor para este parâmetro.
Em C, só podemos passar parâmetros tipo valor, e aparece um
problema. O que fazer quando desejamos receber de volta novos valores para
os parâmetros que passamos à subrotina ?
A resposta é simples. Neste caso, basta passar como parâmetro
o endereço da variável que desejamos alterar. Deste modo podemos usar tal
endereço na subrotina e alterar o conteúdo daquela posição de memória, e assim,
ao voltarmos à rotina que acionou a subrotina, teremos na memória um novo
conteúdo para aquele dado.
Finalmente, para especificar o valor de retorno de uma function
em PASCAL, atribuímos tal valor ao nome da função. Em C, as coisas são diferentes.
Para especificar o valor de retorno de uma função usa-se o comando return
seguido do valor de retorno. Tal valor pode ser uma constante, conteúdo de
uma variável ou resultado de uma expressão.
Em C, a atribuição de valores à variáveis é considerada uma
operação.
| Operador | Operação | Comentários |
| = | Atribuição | Usado para atribuir valores à variáveis.
Ex: a = b + c * 1.2; C permite agrupar várias operações deste tipo. Ex: x = y = z = 0; |
Como em outras linguagens, há vários operadores aritméticos.
| Operador | Operação | Comentários |
| + | Adição | |
| - | Subtração | |
| * | Multiplicação | |
| / | Divisão | |
| % | Módulo (Resto) | Ex:
11 % 4 resulta 2. |
| ++ | Incremento | Usado para adicionar 1 (um) a algum
valor.
Ex: a++ ou ++a Estas duas formas diferem quando o operador aparece numa expressão, ++a soma 1 ao valor de a e depois o novo valor de a será usado na expressão, a++ fará com que o valor atual de a seja usado na expressão e depois some-se 1 ao valor de a. Ex: c = ++a + b ou c = b + a++ |
| -- | Decremento | Análogo ao incremeento. |
| << | Shift para esquerda | Este operador desloca os bits um
dado número de posições para esquerda, e equivale a multiplicar um valor
por uma potência de base 2.
Ex: x = y << 2; |
| >> | Shift para direita | Análogo ao anterior, mas equivale a dividir por uma potência de base 2. |
| & | AND | Operação AND com os bits de dois
valores.
Ex: a = b & c |
| | | OR | Operação OR com os bits. |
| ^ | XOR | Operação XOR (OU exclusivo) com bits. |
| ~ | NOT | Negação com os bits.
Ex: b = ~a |
Com algumas das operações acima, é possível fazer abreviações
usando, na verdade, alguns operadores da linguagem C. Qualquer expressão da
forma:
variável = variável operador expressão
pode ser substituída por
variável operador= expressão
Exemplo:
a = a + 5 a += 5
b = b - 3 b -= 3
c = c * 2 c *= 2
a = a / 3 a /= 3
x = x % y x %= y
analogamente para <<, >>, &, |, e ^.
Veremos agora os operadores relacionais, usados para comparar
dois valores.
| Operador | Descrição |
| > | maior que |
| >= | maior ou igual a |
| < | menor que |
| <= | menor ou igual a |
| == | igual a |
| != | diferente de |
O uso destes operadores é semelhante a seus correspondentes
em PASCAL, porém há uma diferença fundamental no que se refere ao resultado
das comparações.
Como sabemos, o resultado de uma comparação deve ser "verdadeiro"
ou "falso". Em PASCAL temos as constantes false e true que definem o tipo
de dado chamado BOOLEAN, mas em C isto não existe. C trabalha basicamente
com números e não há constantes lógicas. O que ocorre é o seguinte: se o resultado
de uma comparação for falso, o resultado da operação será 0 (zero); e se a
comparação for verdadeira, o resultado da operação será 1 (um).
Uma convenção semelhante se aplica aos chamados operadores
lógicos em C, vistos aqui:
| Operador | Operação |
| && | AND |
| || | OR |
| ! | NOT |
Devemos tomar cuidado para não confundir estes operadores com
aqueles que operam sobre os bits ( &, !, ~ ). Os operadores lógicos produzem
como resultado valores lógicos, isto é, zero ou um, enquanto que os outros
operadores agem diretamente sobre os bits de valores inteiros.
Outro ponto importante e diferente do PASCAL é que os operadores
&& e || são otimizados, isto é, se tivermos em uma expressão algo
como:
exp1 && exp2
exp1 é analisada primeiro e se for falsa, ex2 nem será
considerada.
Por exemplo, a expressão (sum = 5+3) tem o valor 8, e a expressão
relacional ((sum = 5+3) <= 10) será verdadeira, resultando 1.
Veja isto,
if ((ch=getch()) == 'q')
puts("Desistindo, hein ?\n") ;
else
puts("Continuou, hein ?\n");
Neste exemplo, a função getch() tem o mesmo funcionamento
de READKEY em TURBO PASCAL.
Por exemplo, (oldch = ch, ch = getch())
Aqui estão as etapas que o TURBO C segue no momento de fazer
uma operação aritmética:
unsigned char int
float double
Assim, quaisquer dois valores associados a um operador serão
ou int (incluindo long e unsigned) ou double.
3. Se não, se um dos operandos é do tipo unsigned long, o
outro é convertido para unsigned long.
4. Se não, se um dos operandos é do tipo long, o outro é
convertido para long.
5. Se não, se um dos operandos é do tipo unsigned, o outro
é convertido para unsigned.
6. Se não, ambos os operandos são do tipo int.
7. A operação é executada e o resultado tem o mesmo tipo
dos operandos.
· Conversão de tipos
Para converter um valor de um tipo para outro basta escrever
entre parênteses o nome do novo tipo antes do valor original.
Exemplo: f = (float) 4 / 3
No entanto, C já faz automaticamente uma série de conversões
para facilitar a vida dos programadores. Por exemplo, em atribuições ou em
passagem de parâmetros para funções, os tipos numéricos são automaticamente
convertidos.
Para entrada de dados vindos do teclado veremos as funções usadas com mais frequência: scanf, gets, getch e getchar.
scanf ( string de formato , endereço, endereço, ... );
Uma string é escrita em C entre aspas, e não entre apóstrofos
como em PASCAL. A string de formato é uma série de especificadores de formato
para os dados que serão digitados. Cada especificador é formado pelo caracter
% seguido de uma letra que indica a natureza (tipo) do dado.
%u unsigned int
%d int
%c char
%f double or float em notação não científica
%e double ou float em notação científica
%s string
Uma coisa importante a lembrar na função scanf é que
são especificados os endereços de memória das variáveis que receberão os valores
digitados, e não os nomes destas variáveis como no READLN do PASCAL. Isto
poderia ser um problema, mas felizmente C possui um operador unário que devolve
o endereço da variável a que ele se aplica. Trata-se do operador &.
Exemplo: &a (devolve o endereço da variável a)
Em uma mesma chamada de scanf podemos especificar vários
dados a serem digitados, mas para separá-los na digitação devemos usar o mesmo
símbolo usado na string de formato para separar os especificadores.
Por exemplo, para a chamada
scanf("%d,%f",&x, &y);
poderíamos digitar a seguinte linha
12,3.14
o valor 12 ficaria na variável x e o valor
3.14 ficaria na variável y.
Uma limitação importante que se deve destacar, é que para ler
strings, scanf lê uma seqüência de caracteres até encontrar um espaço
em branco ou ENTER, o que é inviável quando desejamos digitar, por exemplo,
o nome completo de uma pessoa.
A sintaxe para chamar esta função é:
gets( variável );
Exemplo:
gets(profissao);
Note que aqui é especificado o nome da variável e não seu endereço.
Note também que a chamada se faz como que uma procedure em PASCAL, uma vez
que o valor de retorno que interessa vem no parâmetro.
Quanto ao parâmetro, veremos mais adiante como definir variáveis
para armazenar strings, uma vez que isto é bem diferente em C do que e em
PASCAL.
Um problema ocorre quando usamos um gets após um scanf.
O ENTER usado para finalizar a entrada de dados via scanf permanece
no buffer de teclado e será captado pelo gets. Desta forma o programa
"passa direto" pelo gets, sem parar para digitação. A solução para
o problema é usar uma função de biblioteca que descarrega o buffer de teclado.
Pode-se usar a função flushall.
Exemplo:
....
scanf("%f", &salario);
....
flushall();
gets(profissao);
....
Exemplo:
char ch;
ch = getch();
Caso seja necessário o aparecimento do caracter digitado na
tela, pode-se usar a função getchar, equivalente a getch. No
entanto, enquanto getch não mostra o caracter digitado na tela e não
espera por um ENTER para confirmar a digitação, getchar, além de exibir
o caracter digitado, espera por um ENTER.
Ha três funções mais usadas para exibir dados na tela: printf, puts e putchar.
printf( string de formato, item, item, ...);
A string de formato é uma seqüência de caracteres que também
são exibidos na tela, e deve conter especificadores de formato para cada item
especificado, se existirem.
Os especificadores de formato são os mesmos que usamos na função
scanf, mas é possível, e muito útil, colocar indicadores de tamanho
de campo para os dados de saída. Trata-se do número de posições a serem ocupadas
pelo dado de saída. Este número é indicado por um inteiro colocado entre o
símbolo % e a letra que designa o tipo do dado. Para strings e inteiros usa-se
um único número, mas para dados tipo float, pode-se usar um número que indica
o total de posições, um ponto, e um número que indica quantas das posições
são reservadas para decimais. Um sinal negativo antes do indicador de tamanho
provocara o alinhamento pela esquerda, ao contrário do alinhamento padrão
pela direita.
Além dos especificadores de formato, é possível colocar também
algumas "scape sequences", que são representações de caracteres especiais
inseridos na string de formato. Aqui estão algumas destas scape sequences:
\n new line
\t tab
\b backspace
\f formfeed ou clear screen
Caso seja necessário que a "barra invertida" faça parte da
string, como por exemplo em nomes de diretórios, deve-se escrevê-las duas
vezes seguidas.
Exemplo: "C:\\TC\\BGI"
Os itens após a string de formato podem ser constantes, conteúdo
de variáveis ou resultado de expressões.
Podemos ver aqui alguns exemplos:
printf("Digite alguma coisa: ")
printf("Resultado = %f", result)
printf("Area do triangulo: &f \n", base*altura/2)
puts( string );
Exemplo:
puts("Esta e uma string");
Uma observação importante é que puts automaticamente
envia um caracter new line após a string.
putchar( caracter );
exemplo:
putchar('a');
Ao contrário de strings, escritas entre aspas, constantes tipo
caracter são escritas entre apóstrofos em C.
Aí você pergunta, porque usar puts e putchar
se temos printf ?
A resposta é que a função printf é muito maior que as
outras, e se você está preocupado com o tamanho do programa EXE que será gerado
na compilação, esta é uma boa razão.
Do mesmo modo que em PASCAL, um pointer é um dado que armazena
endereços de memória. Nestes locais da memória podemos colocar valores e em
seguida aproveitá-los.
O tipo dos valores colocados na memória é importante na definição
do pointer, pois é este tipo que diz ao compilador qual é a quantidade de
memória necessária para armazenar os valores. Isto é imortante, como veremos
mais adiante, em aritmética de pointers.
Para definir uma variável pointer a sintaxe é:
tipo *nome da variável,...;
Exemplo:
int *apint;
float *apfloat;
Deve-se tomar cuidado, pois nestes exemplos acima, apint
e apfloat são variáveis que armazenam endereços de memória e não valores
tipo int ou float.
Quando queremos nos referir ao valor na memória e não ao endereço,
usamos o asterisco antes do nome da variável.
Exemplo:
*apfloat = 3.2;
Este asterisco é um operador. Na verdade, o operador contrário
a &, que serve para nos das o endereço de uma variável.
TURBO C trabalha com alguns tipos de pointers. Entre estes
tipos veremos dois deles, pointers tipo near e pointers tipo far.
Para entender a diferença entre estes tipos de pointers, é
preciso antes entender como o DOS trabalha com a memória do computador.
Desde sua primeira versão, mais por causa das limitações dos
microprocessadores disponíveis na época, o DOS trabalha com a memória dividida
em blocos de 64Kb, chamados "segmentos". Um segmento é representado
pelo endereço de seu primeiro byte, que pode ser qualquer um, ou quase qualquer
um, em um universo de 1Mb. No entanto, para um endereço de até 1Mb são necessários
20 bits, enquanto que o microprocessador, ao menos os mais antigos, têm registradores
de apenas 16 bits (registradores são dispositivos do microprocessador para
armazenar dados processados ou a serem processados). Por este motivo, o endereço
de um segmento deve ser tal, que os primeiros 4 bits sejam zeros, assim podemos
armazenar somente os 16 bits restante, e assim cabem em um registrador. Por
causa destes quatro bits zerados implícitos, o endereço de um segmento pode
ser qualquer um dentro de 1Mb, mas que seja divisível por 16 (decimal).
Além do segmento em que se encontra, para que um byte tenha
sua localização completamente definida, é preciso saber qual dos 64Kb do segmento
é aquele que nos interessa. Para tanto, mais uma informação é necessária,
trata-se do offset dentro do segmento. O primeiro byte do segmento
possui offset 0, o segundo offset 1, o terceiro offset 2, e assim por diante.
TURBO C permite que o usuário escolha um entre alguns modelos
de utilização de memória. Por exemplo, modelo Tiny, modelo Small, modelo Large,
etc. Um programa compilado e linkado com um destes modelos sofre as limitações
que tal modelo impõe. Por exemplo: no modelo Tiny, o código em linguagem de
máquina, os dados e outros itens prezentes na execução do programa, devem
ocupar um único segmento da memória; no modelo Small, o código ocupa um segmento,
os dados ocupam outro segmento, e assim por diante; com modelos maiores podemos
ter programas maiores que 64K e estruturas de dados também maiores que 64K.
Os modelos de memória tem importância porque determinam o tipo
de pointer que podemos, ou não, utilizar em nosso programas. Dependendo do
modelo com o TURBO C estiver configurado, ao definirmos um pointer, é assumido
um determinado tipo para este pointer. Por exemplo, no modelo Small, apenas
pointers tipo near são utilizados, mas no modelo Large podemos utilizar pointers
tipo far.
A diferença entre estes tipos é a seguinte: pointer tipo near
ocupa apenas dois bytes, 16 bits na memória, isto é, pode guardar apenas valores
(endereços) dentro da faixa de 0 a 65535, ou seja, dá para guardar apenas
o offset dentro de um determinado segmento ; no entanto, pointer tipo far
ocupa 32 bits, e neste caso armazena endereços compostos por segmento e offset.
O segmento é representado pelo endereço de seu primeiro byte, e o offset indica
qual dos bytes do segmento é aquele que nos interessa. Costuma-se escrever
tais endereços na forma SSSS:OOOO, onde SSSS são quatro dígitos hexadecimais
que indicam o segmento, e OOOO são quatro dígitos hexadecimais que indicam
o offset.
Para trabalhar com pointers é preciso muito cuidado. Ao ser
definido, um pointer tem como conteúdo não um endereço, mas algo indefinido.
Se tentamos usar o pointer assim mesmo, corremos o risco de que o conteúdo
do pointer seja interpretado como o endereço de algum local da memória vital
para o programa ou mesmo para o funcionamento da máquina. Neste caso podemos
provocar danos no programa, nos dados, ou mesmo travar a máquina.
Seja um pointer tipo near ou far, devemos tomar cuidado ao
inicializar tais variáveis. O valor a elas atribuído deve ser realmente um
endereço disponível na memória.
Por exemplo, podemos colocar em um pointer o endereço de uma
variável já definida:
int ivar, *iptr;
iptr = &ivar;
ivar = 421;
Equivalente a:
int ivar, *iptr;
iptr = &ivar;
*iptr = 421;
Outro recurso usado é acionar uma função de biblioteca que
procura um local livre na memória, reserva este local e devolve o endereço
do byte inicial. A sintaxe para chamada é:
malloc( número );
Onde número é a quantidade de bytes a ser reservada.
Esta quantidade deve ser suficiente para o tipo de valor a
ser colocado na memória, aquele tipo com que foi definido o pointer. Para
facilitar a vida do programador, há um operador que devolve o número de bytes
necessários para representar valores de um dado tipo. A sintaxe para tanto
é:
sizeof( tipo );
Portanto, podemos combinar malloc e sizeof, mas
há ainda outro ponto a destacar. O endereço devolvido por malloc é
na verdade do tipo *void, compatível com qualquer pointer. No entanto,
isto pode não ser verdade para alguns compiladores mais antigos, ou mesmo
para tipos definidos pelo programador. Nestes casos, um simples typecast resolve
o problema.
Exemplo:
registro *ptr;
prt = (registro *) malloc(sizeof(registro));
ptr->codigo = 421;
Os pointers são usados para implementar uma técnica que proporciona
uma utilização mais racional da memória chamada "alocação dinâmica". Trata-se
da alocação de espaço de memória para armazenar dados "durante" a execução
do programa, somente quando tal armazenamento se fizer realmente necessário.
Pode-se ainda "liberar" memória quando os dados não forem mais necessários.
Este conceito envolve estruturas de dados que não são nosso objetivo no momento.
Porém, a liberação da memória que está reservada e cujo endereço se encontra
em um pointer, pode ser facilmente implementada com o uso da função de biblioteca
free.
Exemplo:
int a = calloc(10, sizeof(int));
.....
free(a);
Ainda relativo a pointers, a função de saída printf possui um especificador de formato bastante interessante e que envolve conceitos importantes em programação em abiente DOS, trata-se do especificador %p, que pode também aparecer na forma %Fp. Este especificador se aplica quando desejamos exibir o conteúdo de um pointer, tipo near no primeiro formato e tipo far no segundo formato. No caso de pointer tipo near, o formato com que o endereço será exibido é formado por quatro dígitos hexadecimais, suficientes para representar endereços dentro de um único segmento. Porém, no caso de pointers tipo far, o formato com que os dados serão exibidos é dividido em duas partes, segmento e offset, ambos com quatro dígitos haxadecimais e separados por "dois pontos"(:).
Exemplo:
Para o trecho de codigo abaixo,
int i;
int *pt = &i;
...
printf("%Fp", pt);
...
A saída poderia ser algo assim:
00FA:0000
calloc recebe dois parâmetros. A sintaxe seria algo
assim:
calloc( inteiro1, inteiro2);
Exemplo:
apont = (int *) calloc(3, sizeof(int));
Para entender o que significam os parâmetros observemos o exemplo
acima: sizeof(int) é o número de bytes necessários para armazenar um
dado tipo int. Sabe-se que neste caso são necessários dois bytes. O primeiro
parâmetro é o número três, e indica que devem ser alocados três vezes sizeof(int)
bytes, cujo endereço do byte inicial será atribuído ao pointer apont.
Desta forma temos espaço em memória para armazenar três números
inteiros. Para ver como colocar tais números na memória observe o seguinte
exemplo:
*apont = 32;
*(apont + 1) = -123;
*(apont + 2) = 1987;
Parece estranho somar números a pointers e deve-se tomar muito
cuidado.
Suponha que apont tenha recebido o endereço 64001 (em
notação decimal) após a chamada da função calloc. Foram reservados
seis bytes de memória, portanto uma área que vai desde o byte 64001 até o
byte 64006.
Quando escrevemos apont + 1, isto não equivale a 64001
+ 1. De fato isto équivale a 64001 + 1 * sizeof(int), pois apont é
um pointer para o tipo int. De modo análogo, apont + 2 equivale a 64001
+ 2 * sizeof(int). Esta é a aritmética de pointers, e vale para adição e subtração.
Observe que no exemplo acima não há problemas porque a quantidade
de memória alocada foi suficiente. No entanto, pode ser desastroso fazer tal
coisa quando a quantidade de bytes alocados não é suficiente. Pode-se danificar
conteúdo de memória importante e até provocar o travamento da máquina.
A sintaxe para definir um array é:
tipo nome do array[número de elementos];
Exemplo:
int vetor[3];
Os elementos de um array são identificados através de um índice
inteiro que começa sempre em 0.
Portanto, no exemplo acima temos vetor[0], vetor[1] e vetor[2].
Na verdade, em C o nome de um vetor pode ser usado como um
pointer que guarda o endereço do primeiro byte do local na memória que guarda
os elementos do array. Considerando o exemplo acima, especificar apenas o
nome vetor, é o mesmo que um pointer para tipo int, uma vez que se
trata de um vetor de inteiros. Seria um pointer para o primeiro elemento do
array.
Na verdade, para acessar os elementos de um array podemos usar,
tanto os índices, quanto a aritmética de pointers vista antes.
Outro ponto a destacar é que um array pode ser inicializado
no momento de sua definição, como já vimos para outros dados. Nestes caso,
a sintaxe é um pouco diferente, mas o exemplo a seguir ilustar o processo.
Os valores iniciais para cada elemento do array são escritos entre chaves
e separados por vírgula.
Exemplo:
float vet[5] = { 1.001, 2.123, 4.555, -2.345, 123.3 };
Arrays multidimensionais, equivalentes a matrizes, podem ser
definidos de modo análogo a arrays unidimensionais, exceto pelo fato de que
são especificadas mais de uma dimensão. O exemplo a seguir mostra a definição
e inicializacao de uma matriz 3x2 de elementos tipo float.
Exemplo:
float mat[3][2] = { {1,2}, {3,4}, {5,6} };
Em TURBO PASCAL, há um tipo string, mas em C as coisas são
diferentes. Uma string é na verdade um array de caracteres, como de fato ocorre
no PASCAL padrão. No entanto, como o nome de um array é na verdade um pointer,
há outra maneira de definir uma variável string, ela pode ser um pointer para
o tipo char.
Exemplos:
char nome[20];
char *profissao;
No primeiro exemplo, a variável nome guarda o endereço
do primeiro byte de um local na memória que pode armazenar strings de até
19 caracteres. O ultimo caracter de uma string é sempre null (código
zero ASCII). Na verdade, null não pertence de fato à string, serve
apenas para indicar seu final.
Uma importante diferença entre definir uma variável string
como array de char e como pointer para char está na memória.
Quando a definimos como array, a memória é automaticamente reservada, como
para qualquer array, mas quando o fazemos como pointer, não há memória reservada
previamente. Pode ser necessário faze-lo através da função malloc.
Outro aspecto importante está em como atribuir valores a estas
variáveis.
Quando escrevemos uma string entre aspas, TURBO C cria tal
string seguida por null em algum lugar no meio do programa objeto.
O endereço de tal lugar é que esta disponível para ser então usado no programa.
Desta forma, atribuição direta como no exemplo a seguir é perfeitamente válida.
Exemplo:
char *nome;
nome = "Geraldo Ribeiro";
puts(nome);
Esta forma se aplica bem à variáveis string definidas como
pointers para char, mas para arrays e outras operações mais complicadas,
há uma série de funções de bliblioteca prontas à disposição dos programadores
em todos os compiladores.
Há uma função de biblioteca que copia os caracteres de uma
string para outra. Tais strings podem ser especificadas através de pointers
ou mesmo constantes.
Sintaxe:
strcpy( destino, origem );
Exemplo:
char nome[20];
strcpy(nome, "Geraldo Ribeiro");
puts(nome);
Ao acionarmos a função puts passamos como parâmetro
o endereço do primeiro byte da string, isto é, tanto faz passar um pointer
ou o nome de um array. De modo análogo especifica-se o parâmetro da função
de entrada gets.
O corpo de uma função é escrito entre chaves, e dentro da função,
quando quisermos agrupar várias instruções, por exemplo, para estarem todas
subordinadas a uma condição, devemos colocá-las entre chaves.
Em C, toda instrução deve terminar por ponto-e-vírgula, exceto
aquelas que terminam com uma chave.
Sintaxe:
if (expressão)
instrução1;
else
instrução2;
Se o valor da expressão é diferente de zero, a instrução1 é
executada, caso contrário a instrução2 é executada.
A parte do else é opcional, e se desejarmos, ao invés
de uma única, executar mais de uma instrução subordinada ao if, devemos
usar um bloco de comandos, no lugar da instrução1 e/ou da instrução2.
Há também um comando equivalente ao CASE do PASCAL, o que permite
a escolha entre vários procedimentos distintos em função do resultado de uma
expressão. O comando é switch.
Sintaxe:
switch (expressão) {
case item : instruções;
case item : instruções;
...
case item : instruções;
default : instruções;
}
Onde o resultado da expressão, seja int ou char, é comparado
aos itens à frente das palavras case. Diferentemente do PASCAL, à frente
de cada palavra case deve haver um único item, e as instruções,
zero ou mais, terminam por ponto-e-virgula.
Outro ponto importante, e diferente do PASCAL, é que se o resultado
da expressão for igual a um dos itens à frente de alguma palavra case
e as instruções correspondentes forem executadas, a execução não salta
imediatamente para o comando após o switch, mas sim a comparação continua
com os próximos itens à frente das palavras case que ainda restarem.
Caso se deseje que, após executar os comandos associados a algum item, o comando
switch se encerre, basta finalizar a seqüência de comandos com um comando
break, como veremos no exemplo a seguir.
switch (getchar()) {
case 'C': compile();
break;
case 'R': compile();
roda_programa();
break;
case 'S': salve_arquivo();
break;
case 'E': edita_arquivo();
break;
case 'Q': alve_arquivo();
break;
default: mensagem("Opção inválida...");
}
while (expressão)
instrução;
Onde expressão resulta em zero ou não zero, e instrução
é simples ou composta (bloco). Enquanto a expressão for verdadeira
(diferente de zero), a instrução será executada.
Exemplo:
char *msg;
int cont;
mas = "Teste";
cont = 1;
while (cont <= 3) {
printf("%d vez: %s\n", cont, msg);
cont++;
};
do
instrução;
while (expressão);
Aqui há uma diferença fundamental em relação ao comando visto
anteriormente, enquanto que no comando while pode ocorrer que a instrução
não seja executada nem mesmo uma única vez (teste no inicio), no comando do...while
a instrução é executada certamente ao menos uma vez (teste no final).
Do mesmo modo que antes, instrução pode ser simples
ou composta (bloco).
Neste caso a sintaxe é:
for (exp1; exp2; exp3)
instrução;
Do mesmo modo que em PASCAL, o comando for pode ser
usado para repetir uma instrução enquanto uma variável contador é incrementada
ou decrementada a cada repetição. No entanto, em C o comando é muito mais
flexível e poderoso.
exp1 é normalmente usada para inicializar o contador
exp2 é o teste para continuação do loop
exp3 é normalmente alguma modificação do contador
instrução pode ser simples ou composta (bloco)
Exemplo:
int i;
for (i = 0; i <= 10; i++)
printf("%d x 3 = %d\n",i,i*3);
Para entender melhor como funciona este comando, lembre-se
sempre que o for é equivalente ao seguinte:
exp1;
while (exp2) {
instrução;
exp3;
}
Lembrando que as expressões podem ser formadas por outras expressões
(com o uso da vírgula) e conter atribuições, chamadas de funções, etc, vê-se
como é versátil o comando for em C. Na verdade, um programa inteiro
em C pode estar em um único for, mas isto é coisa para programadores
mais experientes.
Para tratar telas, TURBO C nos fornece uma série de funções
especificas cujos protótipos se encontram no arquivo CONIO.H. Há funções para
limpar a tela, limpar uma linha, posicionar o cursor, tratar cores, etc. O
programador deve buscar maiores informações nos manuais da sua versão de TURBO
C.
Em C, usamos a diretiva typedef, cuja sintaxe é:
typedef tipo nome;
Exemplo:
typedef unsigned char uchar;
typedef int inteiro;
for (dia = segunda; dia <= sexta; dia++) { ...
Com o que vimos até agora seria perfeitamente possível fazer
tal coisa se tivéssemos colocado as seguintes linhas em algum ponto anterior
do programa:
#define segunda 0
#define terca 1
#define quarta 2
...
#define quinta 3
#define sexta 4
No entanto, C nos dá uma outra alternativa. Podemos fazer o
mesmo que as linhas acima, com uma boa economia de código, usando um tipo
enumerado.
enum dia_util {segunda, terca, quarta, quinta, sexta};
int dia;
for (dia = segunda; dia <= sexta; dia++) {....
}
Na verdade, o que ocorre quando definimos um tipo enumerado
é o automático relacionamento do primeiro item da seqüência a 0, do segundo
item a 1, e assim por diante.
Tipos enumerados são conjuntos de identificadores criados pelo
programados associados aos inteiros a partir de 0 (primeiro item), 1(segundo
item), 2, 3, etc.
A sintaxe é:
enum nome do tipo { seqüência de identificadores};
O nome do tipo é dispensável e somente deve ser colocado se
for intenção do programador definir variáveis deste tipo. Neste caso, a palavra
enum deverá preceder o nome do tipo na definição de variáveis.
Exemplo:
enum vogal { a, e, i, o, u }; /* tipo enumerado vogal */
enum vogal v; /* variável v */
Há ainda a possibilidade de usar a diretiva typedef,
como no exemplo a seguir:
typedef enum { masculino, feminino, indefinido} sexo;
Usando a diretiva typedef para relacionar um nome à
estrutura, a sintaxe seria:
typedef struct {
tipo campos;
tipo campos;
...
} nome do tipo;
Exemplo:
typedef struct {
char nome[20], endereço[40];
float salario;
} registro;
registro reg;
Podemos definir a estrutura diretamente no momento de definir
as variáveis do seu tipo.
Exemplo:
struct registro {
char nome[20], endereço[40];
float salario;
} reg;
Neste caso, a palavra registro é dispensável e somente
deve estar presente se for intenção do programador definir variáveis de tal
tipo em outro ponto do programa.
Para nos referirmos a um determinado campo de uma variável
tipo estrutura, usamos uma notação semelhante ao ponto no PASCAL, o
nome da variável, um ponto, e o nome do campo.
No entanto, se tivermos apenas o endereço de estrutura em um
pointer, a sintaxe para acessar um campo é o nome do pointer, seguido pelo
símbolo -> e o nome do campo.
Exemplo:
struct estrutura {
char nome[20];
char fone[15];
float salario;
};
struct estrutura reg;
struct estrutura *apont;
reg.salario = 0;
apont->salario = 0;
Neste exemplo, observe também que a definição de uma estrutura
como um tipo pode ser feita sem typedef, sem especificar variáveis
imediatamente. Neste caso, ao definirmos variáveis mais adiante no programa,
devemos usar a palavra struct antes do nome da estrutura.
Veremos agora alguns recursos que a linguagem C oferece para
trabalharmos com arquivos em disco. Teremos apenas um idéia básica. Quem quiser
mais detalhes deve procurar estudar os manuais do compilador que estiver usando.
O arquivo header stdio.h possui uma série de definições relativas
ao tratamento de arquivos em disco. Há um tipo pré definido chamado FILE
para o qual definimos um pointer, e este pointer representará o arquivo nas
instruções que virão mais adiante em nosso programa.
Todo arquivo deve ser aberto para ser processado e depois fechado.
Para abrir um arquivo em C usamos a função fopen, quando então especificamos
o nome externo do arquivo, isto é, o nome com o qual ele está, ou estará,
gravado no disco, e o modo com que estamos abrindo este arquivo, isto é, se
estamos criando o arquivo, abrindo-o somente para leitura, para acrescentar
dados, etc. Outra coisa indicada no momento da abertura é o tipo do arquivo.
C trabalha basicamente com dois tipos de arquivos: arquivos texto e arquivos
binários. Após o processamento, fechamos o arquivo com a função fclose.
Enquanto o arquivo está aberto, podemos usar uma série de rotinas
pré definidas para processa-lo. Veremos agora algumas destas rotinas e para
que servem, além daquelas já citadas acima.
apontador = fopen( nome externo do arquivo, modo de abertura);
Tanto o nome externo do arquivo quanto o modo são strings.
O nome externo do arquivo pode estar acompanhado por indicação de drive, diretório,
etc. Os modos de abertura são indicados por caracteres que podem estar combinados.
| w | Serve para indicar a criação de um novo arquivo, sobrepondo-se a um eventual já existente. |
| r | Serve para especificar abertura apenas para leitura, neste caso, o arquivo deve já existir. |
| a | Serve para acrescentar dados ao final de um arquivo, ou cria-lo caso não exista. |
| + | Caracter que pode ser acrescentado aos anteriores para indicar que é permitida leitura e gravação. |
| t | Caracter que serve para indicar um arquivo texto (normalmente default), também acrescentado aos outros. |
| b | Especifica arquivo tipo binário, também acrescentável aos outros. |
fopen retorna o endereço que deve ser colocado no pointer
para tipo FILE. Caso a função retorne o valor NULL, houve algum
erro na abertura do arquivo. Logo, a abertura de um arquivo normalmente faz
parte de um teste para já tomar alguma providência no caso de qualquer erro
de abertura.
fclose(apontador );
Esta função bastante simples fecha o arquivo.
fprintf(apontador do arquivo, string de formato, valores);
Esta função é muito semelhante àquela usada para saída no monitor
de vídeo, apenas recebendo a mais como parâmetro o apontador para tipo FILE
que indica um arquivo já aberto para receber gravação. Caso a função tenha
sucesso, retorna o número de bytes gravados, mas em caso de erro retorna um
valor especial que pode ser identificado pela palavra pré definida EOF.
fputc(caracter, apontador para o arquivo);
Esta função grava um único caracter no arquivo. Retorna o próprio caracter ou, no caso de erro, EOF.
fscanf(apontador do arquivo, string de formato, endereços);
Também esta função é muito semelhante àquela já vista para
entrada via teclado. Possui as mesmas limitações para ler strings, uma vez
que um espaço em branco é encarado como separador de campos na leitura. Esta
função retorna o número de campos lidos com sucesso e, no caso de encontrar
o fim do arquivo em uma leitura, retorna EOF.
fgets(string, numero de caracteres, apontador do arquivo);
Esta função resolve o problema de ler strings com espaços no
meio, do mesmo modo que sua correspondente para entrada via teclado. A leitura
pára quando for encontrado um caracter new line ou quando forem lidos
o numero de caracteres especificados menos um. O new line não é acrescentado
à string lida, e no seu final é colocado um caracter nulo (para indicar o
fim da string). Retorna um apontador para a string lida ou NULL no
caso de erro ou fim de arquivo.
caracter = fgetc(apontador para arquivo);
Lê um único caracter do arquivo, retornando-o como resultado.
No caso de erro ou fim de arquivo retorna EOF.
fread(apontador, tamanho em bytes, numero de itens, apontador
para o arquivo);
Esta função lê um certo número de itens, cada um deles do tamanho
especificado, e coloca estes dados na região de memória com endereço no apontador
especificado. Esta função se adapta bem quando desejamos, por exemplo, gravar
estruturas em um arquivo. Retorna o número de itens lidos e, em caso de erro,
retorna um valor menor que o número de itens especificado.
fwrite(apontador, tamanho, numero de itens, apontador do
arquivo);
Análoga à anterior, esta função grava no arquivo um ou mais
itens de tamanho especificado e que estavam na memória na região indicada
pelo apontador especificado. Retorna o número de itens gravados. Em caso de
erro, retorna um valor menor que o número de itens especificado.
fseek(apontador do arquivo, offset, ponto de referência);
Esta função posiciona um apontador interno que o arquivo possui
desde o momento em que é aberto. Tal apontador indica onde os dados são gravados
no arquivo e de onde são lidos. Sua posição é atualizada automaticamente após
cada comando para leitura ou gravação usando uma das funções já comentadas.
fseek posiciona o apontador offset bytes à partir do ponto de referência
especificado. O ponto de referência pode ser: o inicio do arquivo, representado
pelo valor 0 ou o identificador SEEK_SET; a posição atual do apontador
interno, representada pelo valor 1 ou pelo identificador SEEK_CUR;
ou o fim do arquivo, representado pelo valor 2 ou por SEEK_END.
posição = fteel(apontador do arquivo);
Esta função devolve a posição atual do apontador interno.
O próximo exemplo ilustra os aspectos acima discutidos...
/* Teste com arquivos em C */
#include <stdio.h>
#include <conio.h>
typedef struct {
int codigo;
char descricao[21];
float preco;
} registro;
FILE *arq;
registro reg;
char menu();
void inclusao();
void alteracao();
void listagem();
void main() {
char op;
if ((arq=fopen("estoque.txt", "r+"))==NULL)
arq=fopen("estoque.txt", "w+");
do
switch (op=menu()) {
case '1' : inclusao(); break;
case '2' : alteracao(); break;
case '3' : listagem(); break;
case '4' : break;
default : printf("\n\nOpcao invalida...\nPress Enter..."); getch();
}
while (op!='4');
fclose(arq);
}
char menu() {
clrscr();
printf("1=Inclusao\n2=Alteracao\n3=Listagem\n4=Fim\nSua opcao: ");
return getch();
}
void inclusao() {
clrscr();
printf("Codigo: ");
scanf("%d", ®.codigo);
flushall();
printf("Descricao: ");
gets(reg.descricao);
printf("Preco: ");
scanf("%f", ®.preco);
fseek(arq, 0, SEEK_END);
fwrite(®, sizeof(registro), 1, arq);
}
void alteracao() {
int wcod, pos, achou;
clrscr();
printf("Codigo: ");
scanf("%d", &wcod);
fseek(arq, 0, SEEK_SET);
while ((pos=ftell(arq),fread(®, sizeof(registro), 1, arq)==1) && !(achou=reg.codigo==wcod));
if (achou)
{
printf("Descricao: %s\n", reg.descricao);
printf("Preco: %12.2f\n", reg.preco);
printf("\nNovos dados...\n\n");
printf("Codigo: ");
scanf("%d", ®.codigo);
flushall();
printf("Descricao: ");
gets(reg.descricao);
printf("Preco: ");
scanf("%f", ®.preco);
fseek(arq, pos, SEEK_SET);
fwrite(®, sizeof(registro), 1, arq);
}
else
{
printf("\n\nRegistro nao encontrado...\nPressione Enter...");
getch();
}
}
void listagem() {
clrscr();
fseek(arq, 0, SEEK_SET);
while (fread(®, sizeof(registro), 1, arq)==1)
printf("%5d %-20s %12.2f\n", reg.codigo, reg.descricao, reg.preco);
printf("\n\nPressione Enter...");
getch();
}
Atualizado em 31-Out-2000.
Dúvidas, critícas ou qualquer problema no site, favor entrar em contato com o WebMaster.