INTRODUÇÃO À LINGUAGEM C
1.1 INTRODUÇÃO
As linguagens de programação evoluiram exponencialmente desde que os
primeiros computadores electrónicos foram feitos para calcular dados de
telemetria durante a 2.ª Guerra Mundial, onde os programadores trabalhavam
apenas com as mais primitivas instruções, a linguagem máquina. Essas
instruções consistiam em longas cadeias de uns e zeros.
De seguida apareceram os assembladores que mapeavam mnemónicas (mais
facilmente utilizadas) para a linguagem máquina.
Com o passar do tempo, apareceram linguagens de mais alto nível como
BASIC e COBOL. Estas linguagens permitiram que os programadores
utilizassem algo parecido com palavras e frases usadas normalmente como
"Let I = 100". Essas instruções eram então traduzidas para linguagem
máquina por interpretadores e compiladores.
Durante alguns anos, o primeiro objectivo de programação foi escrever
pequenas porções de código que eram executadas rapidamente. O programa
precisava de ser pequeno, visto que a memória era cara e precisava de ser
executado rapidamente, porque o poder de processamento também era
dispendioso. à medida que os computadores se tornaram mais pequenos, mais
baratos, mais rápidos e a memória mais barata, as prioridades mudaram. Hoje
em dia, o tempo dispendido por um programador numa aplicação é muito mais
caro do que o hardware utilizado. Código bem escrito e fácil de actualizar é a
prioridade da maioria das aplicações de hoje.
A linguagem C foi criada e implantada pela primeira vez por Dennis Ritchie
num Dec PDP-11, utilizando o sistema operativo Unix.
A linguagem C foi desenvolvida nos laboratórios Bell na década de 70, tendo
surgido a partir da necessidade de escrever programas, que utilizassem as
potencialidades da linguagem máquina, mas de uma forma mais simples e
portátil que esta.
Usada inicialmente para a programação de sistemas, viria pela sua flexibilidade
e poder, a tornar-se numa linguagem de uso geral nos mais diversos contextos,
sendo actualmente muito utilizada pelos programadores profissionais.
A linguagem C pode ser considerada uma linguagem de médio nível, porque
para além de possuir instruções de alto nível e ser tão estruturada como por
exemplo o PASCAL, também possui instruções de baixo nível.
1.2 CARACTERÍSTICAS
As suas principais características são :
Potencialidades e conveniência das linguagens quer de alto nível quer
de baixo nível.
Pequeno conjunto de palavras reservadas.
Permitir a criação de novos tipos de dados pelo utilizador.
Portabilidade. Os programas escritos em linguagem C podem ser
transportados entre máquinas de diferentes arquitecturas, com um
número muito reduzido de alterações.
Eficiência, através de uma boa ligação aos recursos da máquina.
Existem algumas particularidades na linguagem C que a tornam especialmente
interessante :
Permite conversões de quase todos os tipos de dados. (Os tipos
caracter e inteiro podem ser misturados na maioria das expressões).
Não possui nenhuma verificação de erros em tempo de processamento
(fim de vector ou compatibilidade do tipo de argumento), cabe
inteiramente ao programador saber o que o programa está a fazer.
1.3 ESTRUTURA DE UM PROGRAMA
Um programa em linguagem C é constituído por uma ou mais funções e
variáveis.
Uma função contém instruções que especificam as operações de computação
a serem executadas, enquanto as variáveis armazenam os valores.
A função permite encapsular (esconder certa complexidade) alguma
computação, podendo depois ser utilizada sabendo-se apenas o que faz e sem
preocupações quanto à forma como o faz.
As funções em C assemelham-se aos procedimentos e funções do Pascal. A
execução de um programa começa tipicamente numa função de nome main,
que deverá existir em qualquer parte do programa.
O aspecto geral de uma função é o seguinte :
tipo_retornado nome_função(declarações de parâmetros){
declarações locais;
instruções;
}
Assim, uma função é composta por um cabeçalho seguido do respectivo corpo.
O cabeçalho é constituído pelo nome da função a que se segue entre
parêntesis, uma lista de declarações de parâmetros. Caso estes não existam,
os parêntesis surgem vazios.
Quanto ao corpo da função, este é delimitado por chavetas
{ }
e contêm, declarações de variáveis, ou mesmo de outras funções, a serem
utilizadas, seguidas de instruções.
Sempre que se pretenderem incluir comentários num programa, pode-se fazê-
lo colocando esses comentários entre /* e */.
Uma função pode retornar um valor para a função que a chama, o que é
conseguido com a instrução return, sendo o seu aspecto:
return expressão;
em que,
expressão
pode ser qualquer expressão . Por exemplo :
return 5; /*Retorna a constante inteira 5 */
return val*val; /*Retorna o resultado do produto entre as variáveis
As funções são chamadas (invocadas para execução) dando-se o nome da
função, juntamente com os argumentos, entre parêntises:
nome_função(prim_arg, seg_arg, ...);
Os valores dos argumentos na chamada da função, são atribuidos aos
parâmetros da função pela ordem em que se encontram.
Exemplo 1 : Programa que imprime no ecrâ : "O meu primeiro programa !"
#include <stdio.h> /* Inclui dados sobre funções de entrada e saída
standards (standard input output), como a
printf */
main(){ /* Definição da função main */
printf (O meu primeiro programa !); /*Chama a função printf */
}
O resultado da execução do programa será a impressão no ecrâ do texto
contido entre aspas na função printf. A instrução #include <stdio.h> tem por
finalidade incluir no programa informação sobre funções de entrada e saída de
dados, tal como a função printf que será explicada no Capítulo 4 .
Exemplo 2 : O programa seguinte chama uma função mul(), que multiplica dois
números inteiros e retorna o resultado.
O valor retornado passa a ser o argumento da função printf() que o imprime.
Os dois parâmetros a e b da função mul(), recebem respectivamente os dois
argumentos 5 e 4, com que a função é chamada.
#include <stdio.h>
main(){
printf( A multiplicação de 5 com 4 é igual a %d,mul(5,4));
}
int mul(int a,int b){
return (a*b);
}
A impressão no ecrâ será : A multiplicação de 5 com 4 é igual a 20, o que
significa que %d vai ser substituido pelo valor retornado pela função mul().
Notar ainda que as variáveis a e b, definidas na função mul() só são
reconhecidas no interior desta.
Uma variável é o nome que é dado a uma zona de memória, cujo tamanho
varia conforme o tipo dessa variável. No exemplo anterior, a e b são variáveis
capazes de armazenar inteiros. Este assunto será abordado detalhadamente
na secção 2.3 .
A produção e execução de programas, depende do sistema operativo em que
se esteja a trabalhar, bem como do compilador utilizado.
A função main(), como função que é, também pode retornar um valor. Neste
caso, o valor será retornado ao ambiente exterior, responsável pela execução
do programa.
Habitualmente o valor 0 significa um final sem erro, e um valor diferente de 0
um final com erro. No entanto, e para simplificação não será utilizada a
instrução return 0 nas várias funções main() aqui apresentadas.
1.4 INSTRUÇÕES
Uma expressão seguida de ; (ponto e virgula) é uma instrução.
Expressões são formadas por constantes, variáveis e operadores combinados
de uma forma válida.
As instruções dividem-se em simples e compostas.
Simples quando se trata de apenas uma instrução, e compostas quando se
trata de um conjunto (bloco) de instruções.
Estes conceitos, de instrução simples e composta, farão mais sentido quando,
no capítulo 3, forem discutidas as instruções de controle de sequência.
Instrução simples : expressão;
Exemplo :
printf(O meu primeiro programa!);
Instruções Compostas : Instruções simples dentro de chavetas.
{
instrução;
instrução;
instrução;
...;
}
Exemplo:
{
X=10;
Y=X+X;
}
O sinal '=' significa atribuição, isto é, para o exemplo acima apresentado, à
variável X foi-lhe atribuido o valor 10.
2. OPERANDOS E OPERADORES
2.1 INTRODUÇÃO
Na base de qualquer programa estão os operandos e operadores, pois é
através destes, que se constroem as expressões que acabarão por formar um
programa.
As variáveis e as constantes constituem os operandos que representam os
dados, enquanto os operadores determinam o que lhes fazer.
Os operandos classificam-se através de três atributos:
nome
tipo
classe de armazenamento
As próximas secções tratam exactamente destes atributos, fundamentais para
um correcto entendimento da linguagem C.
2.2 NOMES
Um nome ou identificador, é uma sequência de letras ou dígitos que começa
obrigatoriamente por uma letra. O sinal _ (underscore) conta como uma letra, e
é normalmente usado para simular um espaço num nome.
Nomes com letras maiúsculas são diferentes de nomes com letras minúsculas.
Assim sendo, A e a representarão dois nomes distintos.
Normalmente na linguagem C, usam-se letras minúsculas para representar as
variáveis, e letras maiúsculas para representar as constantes.
Esta utilização de maiúsculas e minúsculas não é uma regra da linguagem,
mas ao ser seguida por quem programa facilita a leitura dos programas.
As palavras reservadas int,if,else, etc. da linguagem C, não são permitidas
como nomes.
Essas palavras são reservadas ao compilador para utilização da linguagem.
Não podem ser definidas classes, variáveis ou funções com esses nomes. A
lista seguinte é um pouco arbitrária, visto que algumas palavras reservadas são
específicas a um tipo de compilador.
Palavras Reservadas :
auto break case catch char class const continue default delete do double else
enum extern float for friend goto if int long mutable newo perator private
protected public register return short signed size of static structs witch template
this throw type def union unsigned virtual void volatile while
2.3 TIPOS DE DADOS
Todas as variáveis são de um determinado tipo de dado que é especificado na
definição da variável.
Ao definir-se uma variável, seja qual for o tipo, o compilador reserva o espaço
na memória necessário a essa variável.
Em C existem quatro tipos de dados básicos, que são:
char - caracter : 1 byte
int - inteiro; normalmente uma palavra de computador : 2 ou 4 bytes
float - número de virgula flutuante :normalmente 4 bytes
double - não inferior à precisão do float; normalmente o dobro da
precisão do float : 8 bytes.
Para além dos tipos de dados existem qualificadores que lhes podem ser
aplicados, e que são :
short, long, signed, unsigned.
O qualificador short aplica-se a inteiros e tem como objectivo reduzi-los para
metade do seu tamanho (ex: numa máquina em que os inteiros (int) sejam de 4
bytes, o short int será de 2 bytes).
O long aplicado a inteiros tem a função inversa do short. O qualificador long
também se aplica ao tipo double, mas com efeitos que dependem da sua
implementação no compilador.
Os qualificadores signed e unsigned são aplicados ao tipo char ou a qualquer
inteiro, e indicam se o número tem ou não sinal. No caso dos signed o seu bit
mais significativo está reservado ao sinal. Números unsigned são sempre
superiores ou iguais a zero, enquanto os signed poderão ser negativos.
Exemplificando, para o caso do char, uma variável signed char tomaria valores
entre -128 e 127, enquanto que se fosse unsigned char, tomaria valores entre 0
e 255. Isto significa que uma variável char pode conter qualquer caracter
ASCII, mais exactamente os respectivos códigos, pois estes variam de 0 a 127
( Existem extensões a este código onde se incluem caracteres de desenho).
Todas as variáveis devem ser declaradas antes de serem utilizadas. Uma
declaração especifica um tipo, e contém uma ou mais variáveis desse tipo.
Exemplos disso são :
int ano,mes,dia;
que declara três variáveis do tipo inteiro cujos nomes são ano, mes e dia.
unsigned char tamanho;
declara uma variável do tipo char sem sinal, e cujo nome é tamanho. Para este
caso o compilador alocaria espaço para um caracter (1 byte).
As variáveis podem ser inicializadas no momento da sua declaração:
int x=-691;
double media=0;
Existe em C um operador para a determinação do espaço em memória
ocupado por um determinado tipo de dado, e cujo nome é sizeof. O operador
sizeof devolve um valor inteiro que é o número de bytes correspondente ao
tamanho desse objecto. Por exemplo para determinar o espaço ocupado por
um inteiro usa-se :
sizeof (int);
Exercicio : Escreva um programa que diga qual o espaço reservado em
memória para os seguintes dados: char;short int; long int; float e double.
Resolução :
#include <stdio.h>
#include <conio.h>
main(){
clrscr();
printf (Tamanho dos Short Int =%d\n,sizeof (short int));
printf (Tamanho dos Long Int =%d\n,sizeof (long int));
printf (Tamanho dos Caracteres =%d\n,sizeof (char));
printf (Tamanho dos Float =%d\n,sizeof (float));
printf (Tamanho dos Double =%d\n,sizeof (double));
}
Saída:
Tamanho dos Short Int =2
Tamanho dos Long Int =4
Tamanho dos Caracteres =1
Tamanho dos Float =4
Tamanho dos Double =8
2.4 CLASSES DE ARMAZENAMENTO
As classes de armazenamento de um identificador definem o modo como o
compilador lhes reserva espaço.
A classe de armazenamento determina para uma variável o seu tempo de vida,
e o seu âmbito (escopo) , isto é :
tempo de vida : Tempo de vida da variável no programa.
escopo : Porção de programa onde a variável é reconhecida.
Os objectos em C caem em duas classes gerais dependendo onde são
definidas.
locais - objectos definidos dentro de um bloco, normalmente numa
função.
globais - objectos definidos no nível mais exterior, fora da função.
As declarações/definições das variáveis incluido a sua classe de
armazenamento têm a forma geral:
classe_armazenamento tipo_dado nome_variável ;
2.4.1 TIPOS DE CLASSES DE ARMAZENAMENTO
São quatro as classes de armazenamento de objectos em C:
auto: Classe de armazenamento interna para variáveis locais que
existem e são reconhecidas somente ao longo da duração da função.
Todas as variáveis locais são implicitamente auto.
static: O seu significado varia consoante o objecto é definido dentro ou
fora de uma função. Uma variável static quando é inicializada na sua
definição só é inicializada na primeira vez que a função é chamada. Nas
chamadas seguintes vai apresentar o valor da sua última utilização. O
interesse desta classe surge quando por exemplo se pretende que o
valor de uma variável de uma função não seja destruído por uma nova
chamada a essa função. O tempo de vida destas variáveis é o da
duração do programa.
extern:Para variáveis globais a todo o programa. Nesta classe caem por
exemplo as variáveis definidas externamente num ficheiro e que se
pretende utilizar noutro ficheiro do mesmo programa.
register: O mesmo que auto, excepto que o compilador, se possível,
escolhe locais de memória de acesso mais rápido (registos). É
normalmente utilizada para variáveis de ciclo. Só é aplicável a variáveis
do tipo int ou char.
2.5 CONSTANTES
As constantes são, como o seu nome indica, valores fixos que não podem ser
alterados durante o programa e, têm associado a cada uma delas um
determinado tipo de dado.
Os tipos de constantes inteiras são:
Decimal
Octal
Hexadecimal
Para identificar o tipo de dado como unsigned usa-se o sufixo u ou U, enquanto
para significar long usa-se o sufixo l ou L, e o sufixo ul ou UL para unsigned
long. O tipo int é assumido por defeito, desde que a constante seja inteira e
portanto não necessita de sufixo.
Um ponto decimal numa string (cadeia) de dígitos indica que o objecto é do tipo
double.
O expoente pode ser denotado usando e ou E.
Uma constante de vírgula flutuante com o sufixo l ou L é considerada do tipo
long double. Para significar float usa-se o sufixo f ou F.
Os caracteres são denotados através de plicas, por exemplo, 'q' representa o
caracter q.
Os caracteres têm uma representação interna numérica.
No código ASCII o caracter q é representado pelo inteiro (em octal ) 0161.
Então, c=0161; é equivalente a c='q';
c=49; é equivalente a c='1';
Vários caracteres especiais podem ser representados como se segue :
\n ->newline
\t ->tab
\b ->backspace
\\ ->backslash
\r ->carriage return
Qualquer caracter pode ser especificado pelo seu equivalente numérico usando
a notação \ddd onde ddd é um inteiro octal,ou por \xddd, onde ddd é um inteiro
em hexadecimal.
As sequências de escape para caracteres de não impressão são também
válidas dentro de strings de caracteres.
Por exemplo:
main()
{
printf (%s,OLA\n);
}
imprime no ecra a palavra OLA e em seguida há uma mudança de linha
provocada pelo caracter \n.
Uma constante também pode ser uma string, ou melhor, um array (vector) de
caracteres em que o último elemento é o caracter nulo (i.e, caracter cujo código
ASCII é 0.
Por Exemplo para a constante ARRAY (string de caracteres), a sua
representação interna seria o conjunto de caracteres, 'A' 'R' 'R' 'A' 'Y' '\0'.
2.6 OPERADORES.
Os operadores em C podem dividir-se em três classes :
Aritméticos.
Relacionais e Lógicos.
Bit a Bit (binários).
Consoante os operadores envolvam um,dois ou três operandos, classificam-se
em unários, binários, ternários.
2.6.1 OPERADORES ARITMÉTICOS
Os operadores aritméticos são :
+ Adição
- Subtracção
* Multiplicação
/ Divisão
% Módulo (resto da divisão de inteiros)
O operador % não pode ser aplicado a números de virgula flutuante. Na divisão
de inteiros a parte fraccionária é sempre truncada. Por exemplo de 5/2
resultaria 2.
Na Linguagem C qualquer operador aritmético pode ser combinado com a
atribuição, o que significa que :
x+=2 <=> x=x+2
a/=b+c <=> a=a/(b+c)
2.6.2 OPERADORES INCREMENTO E DECREMENTO.
Os operadores incremento e decremento, ++ e -- respectivamente, podem ser
usados tanto na forma prefixa como na forma sufixa. O operador ++ soma uma
unidade à variável, enquanto o operdor -- subtrai, como se pode ver nos casos
que se seguem:
x++; <=> x=x+1;
y=x++; <=> y=x; x=x+1;
y=++x <=> x=x+1; y=x;
Como se constata, na forma prefixa acontece primeiro o incremento e só
depois a atribuição, enquanto na sufixa sucede o contrário.
2.6.3 OPERADORES RELACIONAIS E OPERADORES LÓGICOS
Estes operadores permitem a comparação de expressões.
No caso de a relação ser verdadeira, resulta o valor 1 (verdade), no caso de a
relação ser falsa resulta o valor 0.
Relacionais:
x<y menor que
x<=y menor ou igual
x>y maior que
x>=y maior ou igual
x==y comparar igualdade
x!=y comparar diferença
Lógicos:
&& and (e)
|| or (ou)
! not (negação)
2.6.4 OPERADOR CONDICIONAL
O operador condicional é um operador ternário usado em C. O valor da
expressão:
expr0 ? expr1:expr2
é o valor de expr1 se expr0 for verdadeira ou o valor de expr2 se expr0 for
falsa.
Por exemplo, a instrução
x=a>b?a:b;
é exactamente equivalente à combinação das seguintes instruções :
if (a>b)
x=a;
else
x=b;
2.6.5 OPERADORES BIT A BIT
Os operadores bit a bit (binários) permitem, como o nome implica, operações
ao nível dos bits. São seis os operadores bit a bit, os quais só poderão ser
aplicados a expressões do tipo inteiro:
~ complementa bits (inverte o estado dos bits)
<< desloca para a esquerda os bits
>> desloca para a direita os bits
& conjunção
^ ou exclusivo
| disjunção
As tabelas de verdade para E,OU,OU exclusivo são respectivamente :
& 01 | 01 ^ 01
0 00 0 01 0 01
1 01 1 11 1 10
Exemplos :
Supôr duas variáveis x e y do tipo char, cuja representação binária é :
x=10111011
y=10101010
Os resultados de expressões envolvendo várias operações são (as operações
são executadas sobre os valores originais de x e y):
~x = 01000100
x<<2 = 11101100
y>>2 = 01010100
x&y = 10101010
x|y = 10111011
x^y = 00010001
Outros exemplos de carácter mais prático:
a=b&037 -> Atribuir a a os 5 bits de menor ordem de b.
a|=0200 ->O 8.º bit de a é posto a 1.
a&=017 -> limpa todos os bits excepto os 4 finais.
a^=077 -> reversa os últimos 6 bits
a<<=2 -> a é multiplicado por 4.
a~=b ->a fica complementar de b
a=b&1 ->a será 1 se o primeiro bit de b estiver a 1.
Atenção : Ter sempre cuidade em não confundir os operadores lógicos && e ||
com os operadores binários & e |.
2.6.6 OPERADORES DE PONTEIROS
Operadores de ponteiro são importantes em C. Permitem a passagem de
vectores, strings e estruturas para funções, além de permitir que essas
mesmas funções alterem o conteúdo dos argumentos chamados. Os dois
operadores de ponteiros são & e *.(Esses operadores usam os mesmos
símbolos da multiplicação e do E bit a bit, que são completamente des-
relacionados dos operadores de ponteiros).
O operador & retorna o endereço da variável a que precede.
O operador * leva o valor da variável à que precede e usa este valor como
endereço da informação na memória.
O estudo deste tipo de operadores devido à sua dimensão e complexidade, foi
deslocado para capítulo próprio, merecendo neste espaço apenas um lugar de
referência.
2.7 PRECEDÊNCIAS
O conhecimento da ordem de precedência dos vários operadores é
fundamental para que se possam construir expressões correctamente.
A tabela seguinte lista as precedências por ordem decrescente bem como o
sentido de avaliação (associatividade). Ter em atenção que alguns dos
operadores indicados na tabela ainda não foram discutidos.
Operadores Associatividade
() [] -> . da esquerda para a direita
! ^ ++ -- + - * & (tipo) sizeof da direita para a esquerda
*/% da esquerda para a direita
+- da esquerda para a direita
<< >> da esquerda para a direita
< <= > >= da esquerda para a direita
== != da esquerda para a direita
& da esquerda para a direita
^ da esquerda para a direita
| da esquerda para a direita
&& da esquerda para a direita
|| da esquerda para a direita
?: da direita para a esquerda
= += -= *= /= %= &= ^= |= <<= >>= da direita para a esquerda
, da esquerda para a direita
2.8 CONVERSÃO DE TIPOS
Por vezes surge a necessidade de trabalhar operandos cujos tipos de dados
diferem uns dos outros.
Nestas situações há que proceder à conversão de tipos, isto é a conversão
desses diferentes tipos num tipo comum, de forma a possibilitar a resolução
das expressões. As conversões caem em três classes : automática, requerida
pela expressão e forçada.
Automática
Feita automaticamente pelo compilador sempre que a variável seja do tipo
char, short ou float, não existindo possibilidade de perda de informação. As
converssões automáticas são feitas do seguinte modo :
char => int
short => int
float => double
Requerida pela expressão
Quando há mistura de tipos, sendo necessária para a resolução da expressão.
Exemplo:
int x;
double y; /* conversão num double seguida de */
x=x+y; /* uma conversão num inteiro */
A conversão para inteiro faz com que o resultado seja truncado.
Forçada (cast)
Este tipo de conversão pode ser requerido pelo utilizador sempre que este o
considere necessário, e faz-se, precedendo a variável pelo novo tipo entre
parêntises. Ter em atenção que a conversão para tipos menores pode truncar a
variável.
Forma Geral:
(tipo de dado) expressão;
Exemplo : A seguinte função devolve o resto da divisão de duas variáveis de
virgula flutuante.
double mod(double a,double b)
{
double temp;
temp = (int) (a/b);
temp *= b;
return (a-temp);
}
Neste exemplo o cast tornou-se necessário para que o resultado da divisão
fosse um inteiro, perdendo-se a parte fraccionária.
2.9 TYPEDEF
Se um tipo de dado é precedido pela palavra typedef então o identificador na
declaração é entendido pelo compilador como um novo nome para esse tipo de
dado.
Exemplo :Programa que calcula a área de um rectângulo . O resultado (área) é
guardado numa variável do tipo AREA, definido usando-se typedef. Assim em
todas as declarações de variáveis que representem áreas, poder-se-ia usar
AREA em vez de float.
/* programa area.c */
#include <stdio.h>
typedef float AREA;
main()
{
AREA x;
float lado1, lado2
printf (\nIntroduza os valores dos lados: );
scanf (%f%f,&lado1,&lado2); /* scanf lê dados do teclado com o tipo
indicado entre aspas.*/ /*Neste caso 2 floats %f%f */
x=lado1*lado2;
printf (A área é igual a %f,x); /* %f indica um float para escrita */
Ter em atenção que em última instância a variável x é um float, como se prova
pela formatação (com %f) utilizada no printf().
A utilização de typedef é mais frequente com tipos de dados compostos, como
se poderá ver no capítulo dedicado a esses tipos de dados.
No exemplo anterior foi utilizada a função scanf() com dois ou mais
argumentos. O primeiro argumento, entre aspas (string), indica os tipos de
dados a ler para as variáveis que ocupam os argumentos seguintes.
3. INSTRUÇÕES DE CONTROLE E DE SEQUÊNCIA
A linguagem C, como já foi dito, apresenta características de uma linguagem
estruturada. Assim sendo, possui instruções de controle de sequência, que
permitem especificar a ordem pela qual são executadas as instruções de um
programa. As intruções de controle de sequência são de dois tipos:
condicionais (if e switch) e de ciclo (while, do/while e for).
Ter em atenção que quando se tem mais do que uma instrução (instrução
composta) estas deverão ser colocadas entre chavetas, como já foi afirmado
anteriormente.
Existem duas instruções que permitem alterar a sequência normal de execução
dos ciclos: a instrução break e a instrução continue. A intrução break
interrompe definitivamente a execução de um ciclo , enquanto que a instrução
continue interrompe a execução de uma iteracção saltando para o inicio da
iteracção seguinte. A instrucção break também é usada na instrução switch.
3.1 INSTRUÇÃO IF
Esta é a instrução mais simples para controle de sequência. O seu
funcionamento é o seguinte:
Faz-se a avaliação de uma expressão, e caso ela seja verdadeira é executado
um primeiro bloco contendo uma ou mais instruções, caso contrário, será
executado se existir, um segundo bloco iniciado pela palavra else.
Existem duas formas de utilização:
if (expressão)
instrução;
ou
if (expressão)
instrução;
else
instrução;
Exemplo : Neste exemplo, a função idade(), verifica em que intervalo se situa
uma idade i dada como argumento. No caso de i ser maior do que 70 imprime
Idoso, senão, se for maior do que 30 imprime Adulto senão imprime Jovem.
void idade(int i) /* 'void' antes do nome da função para significar que esta
não retorna qualquer valor. */
{
if (i>70) printf (Idoso);
else
if (i>30) printf (Adulto);
else
printf (Jovem);
}
3.2 INSTRUÇÃO SWITCH
Esta instrução avalia uma expressão do tipo inteiro ou caracter, e depois
compara-a com constantes de tipo inteiro ou caracter, permitindo várias
decisões(vias). Esta instrução usa-se normalmente em situações em que
existam várias possibilidades de decisão.
Sintaxe :
switch (expressão)
{
case constante1 : Instrução;
break;
case constante2 : Instrução;
break;
...
default : Instrução
break;
}
A instrução break permite sair (quebrar) do switch. Caso esta não seja incluída
todas as instruções abaixo do case seriam executadas até que surgisse um
break ou a chaveta que termina o switch. A instrução default é opcional e é
avaliada quando nenhuma das constantes coincide com o resultado da
expressão.
3.3 CICLO WHILE
Esta instrução é usada quando se pretende que um bloco de instruções seja
executado enquanto uma determinada condição for verdadeira. O teste da
condição é feito no inicio de cada ciclo, sendo possível que as instruções não
sejam executadas uma única vez. O ciclo while sem nenhuma condição é
considerado sempre verdadeiro, dando origem a um ciclo infinito (do forever).
Sintaxe :
while (expressão)
instrução simples ou composta;
A instrução é executada enquanto a expressão é verdadeira.
3.4 CICLO DO-WHILE
Esta construção é idêntica ao ciclo while. Enquanto que no ciclo while o teste
da condição é feito no fim de cada iteracção, no do-while o teste é feito no fim.
Assim, mesmo que a condição seja sempre falsa, o bloco de instruções é
executado pelo menos uma vez (a primeira).
Sintaxe:
do
instrução simples ou composta
while (expressão);
3.5 CICLO FOR
A instrução for é aquela que habitualmente mais se utiliza na construção de
ciclos em C. Isto deve-se ao seu poder e flexibilidade, como poderá ser
observado. Identicamente ao while, a condição de teste é feita no inicio do
ciclo.
Sintaxe:
for (inicio;teste;incremento)
instrução
Gramaticalmente, os três componentes são expressões.
A inicio correspondem expressões normalmente da inicialização da(s)
variável(is) de controle do ciclo (se correspondem a mais do que uma as
expressões devem ser separadas por virgula.
A teste correspondem as condições de execução de cada ciclo.
Em incremento são normalmente efectuados os incrementos das variáveis de
ciclo (se forem mais do que uma as expressões deverão ser separadas por
virgula).
Na primeira iteração do ciclo for é executado inicio, depois teste, em seguida se
teste resultar verdadeiro é executado instrução (composto por uma ou mais
instruções), e por último incremento nas restantes iteracções do for, inicio não
é executado.
O ciclo for pode ser facilmente convertido num ciclo while com o seguinte
aspecto:
inicio;
while (teste)
{
instrução;
incremento;
4. OPERAÇÕES DE ENTRADA E SAÍDA
4.1 INTRODUÇÃO
Nos capitulos anteriores utilizaram-se algumas funções de entrada e saída de
dados que, só agora serão explicadas com mais pormenor. Estas funções, não
são intrínsecas à linguagem, tendo sendo desenvolvidas à parte e encontram-
se em bibliotecas (conjunto de ficheiros fonte compilados e agrupados num
ficheiro). As funções básicas de input e output disponíveis em todas as
implementações de C constituem a biblioteca de rotinas standard (Standard
library routines). Várias declarações e macros necessárias a estas rotinas são
incluidas num programa através da linha
#include <stdio.h>
4.2 SAÍDA FORMATADA
As rotinas de output formatado aqui apresentadas são :
printf (s_control,arg1,arg2,...);
Escreve a string de controle 's_control' no terminal. Os argumentos serão
introduzidos na string de controle de acordo com as especificações nelas
existentes.
sprintf (buf,s_control,arg1,arg2,...);
O mesmo que printf() excepto que o output é colocado num buffer (i.e. um
array) especificado por buf.
4.2.1 ESPECIFICAÇÕES DA SAÍDA FORMATADA
As especificações do formato para printf() começam com um caracter % e
terminam com um caracter de conversão. Existem várias opções e caracteres
de conversão possíveis. Os caracteres de conversão, e as resultantes
interpretações dos correspondentes argumentos são os seguintes:
c => Um caracter simples.
d => Inteiro na notação decimal
o => Inteiro na notação octal (não esquecer o zero do número, que deve
ser incluido explicitamente)
x => Inteiro na notação hexadecimal (não esquecer o '0x' do número
u => Inteiro sem sinal em notação decimal
s => Array de caracteres; os caracteres são imprimidos em sucessão até
o caracter nulo '\0' ser alcançado
f => float ou double, imprimido em notação decimal de acordo com a
especificação adicional descrita abaixo.
e => float ou double, imprimido em notação cientifica de acordo com a
especificação adicional descrita abaixo.
g => float ou double, imprimido como um %e ou %f.
Qualquer outro caracter é imprimido literalmente; por exemplo, printf (%%)
imprime o sinal de percentagem. Os números são escritos justificados à direita.
As especificações podem opcionalmente incluir (na ordem seguinte):
Um sinal menos que indica que o argumento deve ser justificado à
esquerda no campo de output.
Uma string de dígitos que especifica a largura mínima do campo para
este [Link] o argumento convertido tiver menos caracteres é
encostado à esquerda (ou à direita se o argumento é um ente justificado
à esquerda).
Um ponto que separa a largura do campo de precisão.
Uma string que especifica o n.º máximo de caracteres a ser imprimidos
da string, ou (se foi imprimido um n.º de virgula flutuante) o n.º de digitos
a ser imprimido à direita do ponto [Link] isto é omitido para o n.º de
virgula flutuante, a precisão por defeito é 6.
O caracter l indica que o argumento é um long.
Exemplos : Saídas da impressão com printf da string Instituto Politécnico,
simulando os espaços em branco com _ (Underscore).
%24s ________Instituto Politécnico
%-24s Instituto Politécnico________
%24 16s ________Instituto Polité
%-24 16s Instituto Polité________
%16s Instituto Polité
4.3 ENTRADA FORMATADA
A rotina scanf() de input formatado torna o input numa forma que é
rigorosamente o inverso do printf(). A sintaxe é:
scanf(s_control,arg1,arg2,...);
onde s_control é uma string. O input é lido e interpretado de acordo com a
string de controle. Cada um dos restantes argumentos deverá ser um
endereço, o input é armazenado nesse endereço tendo em atenção as
especificações de conversão da string de controle.
Se se pretender ler um inteiro, a tendência será para escrever
int i;
scanf (%d,i);
tal, no entanto, não funciona, porque os argumentos da scanf devem ser
endereços.
Assim, dever-se-á escrever,
scanf (%d,&i);
em que,& é o operador que permite obter o endereço da variável.
Deve-se ter em atenção que a situação é distinta quando se pretende ler uma
string, pois o nome da string é um endereço, como se discutirá no capítulo
sobre arrays.
Um dos erros mais comuns com scanf é esquecer isto; fazer sempre com que
todos os argumentos da função scanf sejam endereços.
A função
sscanf (buf,s_control,arg1,arg2,...);
funciona mais ou menos como a scanf excepto que lê dados de uma string buf
em vez de os ler do teclado.
A string de controle das funções scanf, ou sscanf pode conter espaços brancos
(os quais são ignorados), especificações de conversão e caracteres vulgares.
4.3.1 ESPECIFICAÇÕES DA ENTRADA FORMATADA
Especificações de conversão consistem no caracter %, no caracter opcional de
supressão de atribuição *, num n.º opcional especificando a largura máxima do
campo, e no caracter de conversão.
Os caracteres vulgares são esperados para combinar com o próximo caracter
não branco do input. Se a supressão de atribuição é indicada com *, então o
próximo campo de input é saltado. Os caracteres de conversão possíveis e as
correspondentes interpretações de input são :
d => Inteiro decimal
o => Inteiro octal
x => Inteiro hexadecimal
c => Caracter simples: neste caso o salto normal de espaçoes brancos é
suprimido no array de caracteres.
s => neste caso o argumento deve ser o endereco de um array de caracteres
suficientemente grande para armazenar o input (incluindo o caracter nulo). Os
caracteres são lidos até ser encontrado um espaço.
f => N.º de virgula flutuante, possivelmente incluindo o sinal e expoente.
e => o mesmo que 'f'.
Se os caracteres de conversão d,o ou x, são precedidos por um l, o input
correspondente é interpretado como um número de vírgula flutuante de
precisão dupla, double.
O argumento correspondente deve ser um endereço de um double em vez de
um float. Esquecer isto é outro erro comum quando se utiliza a função scanf.
A função scanf pára quando atinge o fim da string de controle ou quando o
input falha ao tentar cumprir com a especificação de controle. Retorna como
valor, o n.º de campos de entrada que cumpriram e aos quais tenham sido
atribuídos valores.
4.4 OUTRAS FUNÇÕES DA BIBLIOTECA STANDARD.
Para além das funções de entrada e saída formatada já estudadas, existem
outras funções também muito importantes. Estas funções tambem requerem a
instrução de inclusão do ficheiro stdio.h.
Ler um caracter do standard input (teclado) :
int c;
c=getchar();
Esta funcao retorna um inteiro correspondente ao caracter entrado, ou o valor
da constante EOF se atingiu o fim do ficheiro ou ocorreu um erro.
Escrever um caracter no standard output:
char string[80]
puts(string);
Ler uma string do standard input terminada com \n:
gets(string);
A string lida é colocada em string. A função retorna o valor da constante NULL
quando encontra o fim do ficheiro ou em caso de erro.
4.5 OUTRAS FUNÇÕES
Todas as funções apresentadas têm em comum, o fazer parte da livraria
(biblioteca) standard, no entanto os compiladores oferecem outras funções.
Estas dependem normalmente do sistema, e como tal, exploram bastante bem
as suas potencialidades.
São apresentadas as duas funções seguintes que existem na generalidade dos
compiladores:
c=getche();
em que c é um inteiro. Esta função permite ler um caracter do teclado,sem
necessidade de em seguida se digitar a tecla [ENTER]. O caracter lido é
mostrado (ecoado) no ecrâ. c=getch();
Idêntica à anterior apenas com a diferença de não mostrar o caracter lido no
ecrã (stdout). O facto de estas funções não necessitarem que a tecla [ENTER]
seja premida, faz com que seja de grande interesse a sua utilização.
Normalmente, é necessário a inclusão da instrução:
#include <conio.h>
para a sua utilização.
5. FUNÇÕES
5.1 INTRODUÇÃO.
Um programa em linguagem C pode ser composto por uma (função main()) ou
mais funções, distribuídas por um ou mais ficheiros.
As funções são a única forma de se agruparem as instruções que se
pretendem executar, constituindo assim uma unidade básica na programação
em C.
A linguagem C está feita de modo a que se construam facilmente funções
muito eficientes, de tal forma que podem ser utilizadas em diferentes
programas sem alterações.
5.2 DEFINIÇÃO.
Relembrando do primeiro capítulo, a forma geral da definição de uma função
é:
classe_armazenamento tipo_dado nome(declarações_lista_de_parâmetros)
{
declarações das variáveis locais
instruções
}
Sempre que se pretenda que uma função retorne um valor, então deverá ser
incluída a instrução return.
As variáveis da lista de parâmetros podem ser consideradas como variáveis
locais, que são inicializadas com os valores dos argumentos da chamada da
função.
O tipo de dado que precede o nome da função é o tipo do objecto que a função
retorna. Por exemplo, a definição double raiz(double num) significa uma função
que retorna um double.
Os tipos podem ser portanto os mesmos que os definidos para as variáveis.
Se o tipo de uma função não é declarado explicitamente, então o tipo retornado
por defeito é int. E se algum dos parâmetros não tiver a declaração explicita do
tipo, este é também assumido como int.
Uma função retorna automaticamente quando atinge o fim do corpo da função,
a não ser que antes encontre a instrução return. A sintaxe da instrução return é
:
return(expressão);
Os parêntises em torno da expressão são opcionais. O tipo da expressão
deverá estar de acordo com o tipo da função. Se uma função retorna um não
inteiro então qualquer rotina que a chame deve declarar o tipo do objecto que
ela retorna. Eis a distinção entre :
Definição da função -> Dá o código da função.
Declaração da função -> Somente informa o compilador do tipo de
objecto retornado.
Exemplo : Neste exemplo é calculada a raiz quadrada de um número, pelo
método de Newton. Em main() é declarada a função raiz(), para que o
compilador saiba qual o tipo de dado que ela retorna.
/* program raiz.c */
main()
{
float a,r,raiz(); /* Declaração da função raiz() */
printf (\n Introduza um número:);
scanf(%f,&a);
r=raiz(a);
printf (\nA raiz quadrada de %f é %f,a,r);
}
/* Função Raíz Quadrada através da iteracção do método de Newton. */
float raiz(float x) /* Definição da função 'raiz' que retorna um float . */
{
float y,z
y=x;
do
{
z=y;
y=(z+x/z)/2;
}
while (z!=y);
return (y);
}
No contexto das funções existem dois conceitos importantes que por vezes dão
origem a alguma confusão:
Argumentos - Lista de variáveis, constantes ou expressões colocados
entre os parêntises da função aquando da chamada desta. No exemplo
anterior, na chamada raiz(a), a é o argumento.
Parâmetros - Lista de declarações de variáveis aquando da definição da
função. No exemplo anterior a lista de parâmetros da função raiz(float x)
é apenas float x.
Muitas funções, não retornam qualquer valor. A forma de o declarar
explicitamente é usando o tipo de dado void. Uma declaração deste tipo, pode
ser :
void inverte(char str[]) /* Função que não retorna qualquer valor */
{
...
}
Quanto à classe de armazenamento de uma função pode ser :
static, significando que a função só é visível no ficheiro onde está
definida.
extern, significando que a função já foi definida em outro local ou ficheiro
não existir classe de armazenamento tratando-se portanto de uma
função global.
5.3 PARÂMETROS DE UMA FUNÇÃO
Em C, os argumentos para as funções são passados via chamada por valor, ao
passo que em Pascal e Fortran são chamados via chamada por referência.
A passagem por valor significa que a uma função (mais concretamente aos
seus parâmetros ), são apenas passados os valores dos argumentos, enquanto
na chamada por referência é como se fossem passadas as próprias variáveis.
Em Pascal, duas variáveis podem ser trocadas através da chamada ao
seguinte procedimento:
procedure troca(var x,y:integer)
var
temp:integer
begin
temp:=x
x:=y;
y:=temp;
end
O equivalente em Fortran,seria :
SUBROUTINE TROCA(X,Y)
INTEGER X,Y,TEMP
TEMP=X
X=Y
RETURN
END
O equivalente em C, seria ineficaz:
troca(x,y)
int x,y
{
int temp;
temp=x;
x=y;
y=temp;
}
Esta rotina troca unicamente os valores das variáveis da função na stack, e
deixa os valores originais da rotina chamadora inalteráveis. Supondo que
troca() era invocada do seguinte modo:
int a=1,b=2;
troca(a,b);
seriam trocados os valores de x e y da função mas manter-se-iam inálteráveis
os de a e b.
Para que na função se pudessem alterar os valores das variáveis da função
chamadora, haveria que passar os endereços dessas variáveis, e os
parâmetros da função teriam que ser apontadores. Então a função troca() seria
invocada do seguinte modo:
int a=1,b=2;
troca(&a,&b);
e teria o seguinte aspecto, com as variáveis X e Y definidas como apontadores
para inteiros (ou seja, variáveis que guardam endereços de posições de
memória onde estão armazenados inteiros):
troca (int *x, int*y)
{
int temp;
temp=*x;
*x=*y;
*y=temp;
}
Quer isto dizer que X e Y receberam os endereços de a e b respectivamente, e
que através do operador de conteúdo de um endereço (*), podem aceder aos
conteúdos desses endereços.
5.4 CONVERSÃO DE TIPOS.
O valor de uma expressão numa instrução return é convertido para o tipo da
função através das regras de conversão de tipos de dados. Com efeito, um
cast para o tipo apropriado é inserido automaticamente em qualquer instrução
return. Por exemplo, a função atoi() (que converte uma string num inteiro) pode
ser escrita em termos duma rotina de biblioteca atof() (a qual converte uma
string para um número de vírgula flutuante) como se segue :
atoi(char s[])
{
double atof();
return atof(s);
}
O valor produzido pela função atof() é automaticamente convertido para inteiro
na instrução return, em virtude da função atoi() retornar um inteiro.
As únicas conversões de tipo efectuadas durante a avaliação de expressões
numa lista de argumentos são as conversões de tipo automático: Assim, nas
outras situações há que forçar a conversão, como no exemplo seguinte, em
que a função log() espera um argumento do tipo double:
int n;
double x,log();
x=log((double)n);
e em que foi feito o cast da variável n de inteiro para um double.
5.5 ARGUMENTOS DA LINHA DE COMANDOS
É possível em C passar valores para o programa através da linha de
comandos. Estes valores são passados à função main(). Nesta função usam-se
habitualmente 0 ou 2 parâmetros formais. Esses dois parâmetros formais da
main() estão relacionados com os argumentos de linha de comando que são
dados na chamada do programa. A maior parte dos programadores escolhe os
nomes argc e argv para estes dois parâmetros:
main( int argc, char argv[])
{
...
}
O primeiro parâmetro argc é o número de argumentos da linha de comando
(isto é, palavras na linha de comando separadas por espaços brancos).
O segundo parâmetro argv é um array de strings (cada string é um array de
caracteres) representando os argumentos da linha de comando.
Como o nome do próprio programa que é executado está em argv[0], então o
argc é sempre no mínimo 1. O primeiro argumento verdadeiro está em argv[1]
e o último está em argv[argc-1].
argv[argc] possui normalmente um valor para distinção (por exemplo NULL).
Compreender-se-á melhor esta secção após a leitura do capítulo 6 sobre
vectores.
5.6 CONCLUSÃO
Como conclusão, recapitulam-se em seguida alguns aspectos mais importantes
sobre funções:
Permitem dividir o programa em partes funcionais mais pequenas.
Podem residir em bibliotecas e/ou noutros ficheiros fonte.
A comunicação do exterior para a função é feita através dos
argumentos.
A comunicação da função para o exterior é feita através de um valor de
retorno, sendo este do mesmo tipo da função.
Todas as funções têm um determinado tipo associado.
A definição da função pode ser feita em qualquer ponto.
A função pode invocar-se a si própria (recursividade).
Uma função cujo tipo é antecedido por static só é visível no ficheiro onde
está definida.
[Link]
6.1 INTRODUÇÃO
Supor, que se pretendia um programa para ler a média das notas de cada um
dos alunos de uma escola, calcular a média das médias, e depois para cada
aluno determinar o desvio da sua média relativamente à média das médias.
Uma solução para guardar cada uma das médias, seria definir por exemplo
3000 variáveis (número hipotético de alunos), por exemplo :
float med_1,med_2,med_3,med_4,med_5,med_6,med_7,med_8, ... ;
até med_3000, mais as instruções para a leitura das médias:
printf (\n Introduza a média do aluno n.º1:);
scanf (%f,med_1);
3000 vezes, o que se revela completamente impraticável. Seria preferível a
possibilidade de definir as 3000 variáveis de uma só vez, por exemplo da
seguinte forma:
float media[3000];
em que media[0] guardaria a média do aluno 1, media [1] a do aluno 2, e assim
sucessivamente fazendo variar o valor do indice até 2999. Para melhorar o
processo seria conveniente definir uma variável para indice, por exemplo:
int num;
Deste modo, para a leitura das 3000 variáveis, poder-se-ia utilizar um ciclo,
como a seguir se ilustra:
for (num=0;num<3000;num++)
{
printf (\n Introduza a média do aluno n.º %d,num);
scanf (%f,media[num]);
}
o que representaria uma melhoria extraordinária relativamente à primeira
solução. Felizmente a generalidade das linguagens de programação fornecem
este tipo de dados, chamados vectores (neste texto é usado também o termo
array para significar vector), os quais se explicam nas próximas secções.
6.2 VECTORES NA LINGUAGEM C
A linguagem C permite definir novos tipos de dados a partir dos existentes. Os
arrays, ou vectores, são um conjunto de elementos do mesmo tipo, agrupados
com o mesmo nome, e diferenciados através de um índice entre parêntises
rectos. Antes de mais, convém salientar que os arrays estão fortemente ligados
aos apontadores, os quais não serão aqui abordados.
Em C existem apenas arrays unidimensionais e o tamanho de um array deve
ser fixo e definido por uma constante na altura da compilação. No entanto, um
elemento de um array pode ser um objecto de qualquer tipo, inclusivé outro
array. Isto faz com que seja possível simular array multidimensionais.
A melhor forma de entender o modo de funcionamento dos arrays, é perceber a
sua declaração. Por exemplo,
int a[4];
diz que a é uma variável do tipo array de 4 elementos do tipo int. A sua
representação será:
___ ___ ___ ___
a[0] a[1] a[2] a[3]
em que cada posição quarda um inteiro.
Como se pode ver, os elementos do array ocupam posições de memória
contíguas e o seu índice varia obrigatoriamente de 0 a 3. Ou seja, tem-se, para
essas 4 posições os elementos
a[0], a[1], a[2], a[3]
que são tratados como se fossem quatro variáveis distintas.
Atribuindo os valores 1,2 e 3 repectivamente, aos três primeiros elementos,
a[0]=1;
a[1]=2;
a[2]=3;
virá:
_1_ _2_ _3_ ___
a[0] a[1] a[2] a[3]
Convém ainda saber que o nome array é uma constante e representa o
endereço da 1.ª posição do array. Isto é:
a==&a[0] /* O operador & dá o endereço da variável. Neste caso dá o endereço
de a[0]*/
Supondo que o vector se iniciava na posição de memória com o endereço 100,
e que cada inteiro gasta 2 bytes, ter-se-ia:
a==100
&a[0]==100
&a[1]==102
&a[2]==104
&a[3]==106
Em virtude de ser uma constante, não é possível atribuir valores ao nome de
array, como se ilustra no seguinte caso:
int a[4];
a=2
a[0]=2;
a[0]++;
a[1]=2*a[0];
A atribuição na segunda instrução é inválida. Também quando se passa um
array como argumento de uma função, na realidade o que é passado para a
função é o seu endereço, como se viu com a função scanf.
A dimensão de um vector é determinado pelo produto das suas linhas pelas
suas colunas. A seguinte declaração:
int ecra[25][80];
diz que ecra é um array de 25 arrays de 80 elementos inteiros cada. Assim,
sizeof(ecra) será 2000 (25*80).
A inicialização de um array pode ser feita no momento da sua definição:
int a[4]={1,2,3};
irá definir (criar) um vector de quatro inteiros e inicializar a[0] a 1, a[1] a 2, e
a[2] a 3. No entanto a definição:
int a[]={1,2,3};
irá definir um vector de apenas três elementos e inicializa-los de forma idêntica
ao anterior. O vector é criado com apenas 3 posições em virtude de não ser
explicitado entre parêntises o número de elementos, e portanto este número
será determinado pela lista de inicialização.
No caso de um array ser multidimensional a inicialização obedece às mesmas
regras.
Por exemplo :
int m[][3]={1,2,3,11,22,33};
ou
int m[][3]={{1,2,3},{11,22,33}};
em que se obtém os seguintes valores para cada elemento:
m[0][0] é igual a 1
m[0][1] é igual a 2
m[0][2] é igual a 3
m[1][0] é igual a 11
m[1][1] é igual a 22
m[1][2] é igual a 33
1 2 3
11 22 33
Exemplo 1: O programa media.c lê 100 notas para um array, e calcula a média
das notas.
/* Programa media.c*/
#include <stdio.h>
main()
{
int i;
float notas[100],soma;
/* Ciclo que lê 100 notas e armazena-as no vector notas.*/
for (i=0;i<100;i++)
{
printf (Nota %d=?,i+1);
scanf(%f,¬as[i]);
/* A variável acumuladora soma inicializada a zero.*/
soma=0;
/* Ciclo que soma as 100 notas. */
for (i=0;i<100;i++)
soma=soma+notas[i];
printf (A média é = %f,soma/100);
}
Como cada elemento do vector notas é um inteiro, há necessidade na função
scanf de lhe aplicar o operador & como acontece com qualquer outro inteiro.
6.3 VECTORES COMO ARGUMENTOS DE FUNÇÕES
Para melhor se compreender este assunto observe o seguinte exemplo:
O seguinte programa testa uma função que troca os dois primeiros elementos
de um vector dado como argumento.
#include <stdio.h>
void troca(); /*Declara a função como não retornando qualquer valor*/
main()
{
int v[2];
v[0]=2;
v[1]=3;
troca (v);
printf (v[0]=%d,v[1]=%d,v[0],v[1]);
void troca(int x[2])
{
int t;
t=x[0];
x[0]=x[1];
x[1]=t;
O resultado será v[0]=3 e v[1]=2. Neste exemplo é passado como argumento
da função o nome do vector, que é o endereço da 1.ª posição do array. Assim x
vai representar também um endereço, o de v[0] que é o mesmo de x[0], e
portanto as alterações feitas na função terão repercussões nas variáveis da
função chamadora.
Na definição da função também seria possível e com os mesmos resultados,
ter int x[] em lugar de int x[2], sendo nesse caso x dimensionado
automaticamente a 2.
Supôr que o endereço de v[0] é 200, então é porque v=200 como mostra a
figura:
(Supondo uma máquina de inteiros com 2 bytes, v[1] virá 2 bytes à frente
(202)).
v=200
Designação: v[0] v[1]
Conteúdo: 2 3
Endereço: 200 202
então a chamada da função
troca(v); <=> troca(200);
em que ao nome do array x corresponderá o valor de 200, ou seja, x será igual
a 200
x=200
Designação: x[0] x[1]
Conteúdo : 2 3
Endereço : 200 202
o que implica que após as trocas ficará:
Conteúdo : 3 2
Endereço : 200 204
Estas posições de memória(endereços) correspondem também a v[0] e a v[1],
respectivamente.
6.4 STRINGS
Em C, não existe o tipo de dado string. Pode no entanto ser simulado através
de um array de caracteres. Por convenção, delimita-se a string com o caracter
nulo (\0). Isto é, coloca-se o caracter nulo na primeira posição não preenchida
do array. Assim quando se define o tamnho do array de caracteres para ser
usado como string é necessário contar com mais uma posição.
A standard library fornace um conjunto de funções para a manipulação de
strings. Estas funções pressupõem que as strings que lhes são passadas são
terminadas com o caracter nulo.
int strlen(char s[]);
devolve o comprimento da string s. Exemplo:
char s[]=OLA;
printf (Comprimento da String==%d,strlen(s));
A definição anterior além de inicializar o array com a string, também o
dimensiona como um vector de quatro caracteres, como mostra a figura
seguinte:
'O' 'L' 'Á' '\0'
Quanto à saída de printf, ela seria: Comprimento da String == 3.
strcpy (char destino[], char origem[]);
Copia origem para destino. Exemplo:
char destino[]=ADEUS,origem[]=OLA;
strcpy(destino,origem);
printf (%s,destino);
a saida é OLA.
strcat (char primeira[], char segunda[]);
concatena (acrescenta) a string segunda à string primeira.
int strcmp(char s1[],char s2[]);
Compara s1 com s2,e devolve 0 se s1 igual a s2, >0 se s1>s2 e <0 se s1<s2.
Uma lista completa destas funções pode ser encontrada nos manuais de
qualquer compilador de C. Para a utilização destas funções, é requerida a linha
#include <string.h>
Estas funções podem ser facilmente construídas pelo programador.
7. - ESTRUTURAS
7.1. INTRODUÇÃO
Para resolver o problema de ter uma variável constituída por tipos de dados
diferentes, utilizam-se as estruturas que são explicadas em seguida.
7.2. DEFINIÇÃO
Uma estrutura é um tipo de dado composto que consiste em uma ou mais
variáveis agrupadas sob um nome. As variáveis na estrutura podem ter tipos
diferentes, mas normalmente existe uma relação entre eles que faz com que
seja conveniente tratá-los conjuntamente como um objecto no programa. Em
Pascal a noção análoga é um tipo de dado, definido pelo utilizador,chamado
record. Os componentes individuais de uma estrutura são chamados membros.
Uma declaração de estrutura tem a forma:
struct nome_estrutura
{
Declarações_dos_membros
};
Isto tem o efeito de definição de um novo tipo de dado. Aqui nome_estrutura é
um nome arbitrário e struct nome_estrutura pode ser pensado como o nome de
um novo tipo de dado. As Declarações_dos_membros são como quaisquer
outras declarações, excepto que não definem variáveis mas nomes de
componentes de uma instância da estrutura nome_estrutura.
Alguns exemplos:
struct num_complexo
{
double real;
double imaginário;
};
struct data
{
int ano;
char mes[10];
int dia;
};
struct venda
{
char comprador[SIZE];
double quantidade;
struct data quando;
};
Neste último caso existe uma estrutura encaixada noutra. Estas declarações
não definiram quaisquer variáveis, elas meramente definiram novos tipos de
dados e portanto, não lhes foi reservado espaço de memória. Qualquer
definição de variáveis do tipo destas anteriores utiliza as mesmas regras que
para qualquer outro tipo de dado. Como exemplo:
struct data d; /* d é uma variável do tipo struct
data */
struct venda a[20] /* a é um array de 20 estruturas*/
struct num_complexo x,y,z;
nestes casos são definidas variáveis e consequentemente é-lhes reservado
espaço em memória.
Os membros das estruturas são acedidos da seguinte forma:
variável_estrutura.membro;
Exemplos :
struct data d;
[Link]=1993
strcpy([Link],Fevereiro);
[Link]=4;
struct venda s;
[Link]=24
[Link]=1994
Repare-se que o membro da estrutura imbricada foi acedido da mesma
[Link] exemplos são:
struct complexo x,y
[Link]+=[Link];
[Link]ário+=[Link]ário;
As variáveis do tipo estrutura podem ser inicializadas na altura da sua
definição, colocando-se a lista de inicializações a seguir à definição. Por
exemplo,
struct complexo z={1,-1};
7.3 OPERAÇÕES COM ESTRUTURAS
As operações fundamentais que podem ser aplicadas às estruturas são:
Tomar o endereço de uma estrutura com o operador &
Aceder aos membros da estrutura com o operador.
Atribuição de variáveis de estrutura, por exemplo,
struct data d1,d2;
d1=d2;
Passar uma estrutura para uma função, por exemplo,
struct data d;
imprime_data(d);
Retorno de uma estrutura por uma função, por exemplo,
struct data d,aniversario();
d = aniversario(Portugal);
em que aniversario é uma função que retorna uma estrutura struct data.
Exemplo : A função soma_complexo, adiciona dois números complexos e
retorna o resultado.
struct complexo soma_complexos(struct complexo n1, struct complexo n2)
{
struct complexo soma;
[Link]=[Link]+[Link];
[Link]=[Link]+[Link];
return soma;
Apesar de não existir qualquer conflito entre membros e variáveis com o
mesmo nome, é necessário ter o maior cuidado com esta prática, de forma a
que não prejudique a clareza dos programas.
7.4 VECTORES DE ESTRUTURAS
As estruturas, como qualquer outro tipo de dado podem ser agrupadas em
vectores.
Exemplo 1 : Neste exemplo é criado um novo tipo de dado struct data e
definida uma variável feriado do tipo array de 5 estruturas struct data.
struct data {int dia, char mes[10], int ano};
struct data feriado[5] = {{1,Janeiro,1993},
{10,Junho,1993},
{25,Dezembro,1993}};
Para simplificar as declarações muito extensas, é conveniente atribuir nomes
aos tipos de estruturas. Por exemplo:
typedef struct data {int dia,char mes[10], int ano} DATA;
agora DATA representa o tipo de dado struct data, passando a poder ser usado
em lugar deste.
7.5 UNIONS
Uma union (união) permite criar variáveis capazes de suportar diferentes tipos
de dados, no mesmo espaço de memória em momentos diferentes. A
declaração de uma union é similar à declaração de uma estrutura. A diferença
é que com uma struct é alocado de uma vez espaço suficiente para todos os
objectos, enquanto que com uma union só é alocado espaço para o maior dos
objectos que a compõem, A declaração,
union Valor{
int ivalor;
double dvalor;
char cvalor;
}val;
significa que val poderá armazenar ou um inteiro, ou um double, ou um char.
O programador é responsável pelo conhecimento de qual dos tipos foi
armazenado mais recentemente na variável val. A sintaxe para aceder ao
conteúdos de uma variável union é exactamente a mesma que para as
estruturas. Seguidamente é mostrado o extracto de um programa em que é
utilizada a union acima descrita.
/* Definição de constantes */
#define INT 0
#define DOUBLE 1
#define CHAR 2
int tipo;
...
switch (tipo)
{
case INT:
printf (%d,[Link]);
break;
case DOUBLE:
printf (%lf,[Link]);
break;
case CHAR:
printf (%c,[Link]);
break;
O programa imprime um inteiro, um double ou um caracter dependendo do
valor de tipo.
8. - FICHEIROS
8.1. INTRODUÇÃO
Até ao momento, todos os dados têm sido inseridos nos programas através do
teclado, sendo guardados em variáveis, as quais residem em memória central
(também chamada de R.A.M, memória primária ou volátil) . Esta apresenta no
entanto os inconvenientes de ser volátil e cara, pelo que se torna
desinteressante para o armazenamento durante periodos que excedam o de
execução de um programa. Para armazenamentos mais demorados é utilizado
outro meio, a que se chama memória secundária ( i.e. o disco rígido, ou a
disquete), em que a informação é armazenada sob a forma de ficheiros. A
utilização destas estruturas (ficheiros) na linguagem C será o assunto deste
capítulo. As funções que irão ser estruturadas fazem parte da biblioteca
padrão, e portanto obrigam à inclusão nos programas da seguinte linha, já
conhecida:
#include <stdio.h>
Qualquer processo (programa) tem 3 ficheiros automáticamente abertos : o da
entrada padrão ( standar input), o da saída padrão ( standard output), e o da
saída de erro ( standard error output). A menos que redireccionados, estão
normalmente associados ao terminal, isto é , ao teclado, ao monitor e monitor
respectivamente. A cada um destes ficheiros corresponde uma constante,
definida em stdio.h que o identifica:
stdin standard input (teclado)
stout standard output (monitor)
stderr standard error (monitor)
O ficheiro de saída de erro é geralmente usado para escrever mensagens de
erro que não dizem respeito às saidas normais. Por exemplo, se ocorresse um
erro grave seria aborrecido se a mensagem de erro fosse redireccionada para
o ficheiro de saída sem que o utilizador se apercebesse disso. A solução seria
escrever a mensagem para o standard error.
A linguagem C permite dois tipos de ficheiros : binário, ou texto. Estes são um
conjunto de linhas, com zero ou mais caracteres cada, sendo cada uma
terminada pelo caractere newline (\n). Estas linhas estão sujeitas a algum
processamento feito pelo sistema operativo. Por exemplo o DOS na escrita,
substitui o \n por carriage return e mudança de linha (line feed). A substituição
inversa é feita na leitura.
Um ficheiro binário, é composto por uma sequência de bytes(caracteres) que
não são alterados pelo sistema.
8.2. ABERTURA E FECHO DE FICHEIROS
O ficheiro stdio.h contém definições e declarações necessárias às funções
standards de manipulação de ficheiros. Esse ficheiro contém um typedef que
define um tipo de dado chamado file. Nos programas em linguagem C um
ficheiro é especificado em termos de um apontador para um FILE que é obtido
pela chamada à função fopen. O utilizador não necessita conhecer os membros
de uma estrutura FILE. A sintaxe da função de abertura de ficheiros num
programa em C é:
FILE *fopen(char nome_do_ficheiro[], char modo[]);
A função fopen() devolve um apontador para um FILE, que passa a ser
utilizado em todos os acessos ao ficheiro. O primeiro argumento é o nome do
ficheiro a ser aberto. O segundo argumento é o modo de abertura. Algumas
das possibilidades para o argumento modo são:
R :Abertura para leitura.Dá erro se o ficheiro não existe.
W :Abertura para [Link] o ficheiro existir o conteúdo é apagado. Se
não existir, o ficheiro é criado.
A :Abertura para adição no fim do [Link] este existir então os novos
dados serão escritos no fim do [Link] o ficheiro não existir então
será criado.
R+ :Abertura do ficheiro para leitura e escrita.
b,t :Conforme se trate de um ficheiro do tipo binário ou do tipo texto,
adiciona-se b ou t a qualquer uma das opções anteriores (e.g.:wb).
A função fopen() retorna NULL se ocorrer um erro na abertura. Após ser
realizado o processo sobre um ficheiro aberto à que fechá-lo. A operação de
fecho de um ficheiro é feita pela função :
fclose (FILE *fp);
Esta função desaloca a estrutura FILE respectiva e fecha o ficheiro apontado
por fp. Em caso de erro (i.e. fechar um ficheiro que ainda não foi aberto)
retorna EOF, caso contrário retorna zero.
Exemplo : Exemplo de chamadas às funções fopen e fclose.
#include <stdio.h>
FILE *fp,*fopen();
...
fp=fopen(dados,r);
...
fclose (fp);
Neste exemplo o valor (apontador para um FILE ) retornado por fopen(), é
guardado na variável fp. A partir daqui todas as referências ao ficheiro são
feitas usando fp.
8.3. LEITURA E ESCRITA DE FICHEIROS
Funções Gerais
A função de leitura geral da bibloteca padrão é :
fread(void *buffer, unsigned size, unsigned num, file *fp);
A função fread lê num objectos de tamanho size de um ficheiro especificado
pelo apontador fp para a posição de memória indicada por buffer. Retorna o n.º
dos objectos completamente lidos, ou retorna 0 se fim de ficheiro. A declaração
void * significa apontador para qualquer tipo de variável, ou seja, um qualquer
endereço.
A função de escrita geral da standard library é :
fwrite (void *buffer, unsigned size, unsigned num, FILE *fp);
a qual escreve num objectos de tamanho size contidos em buffer, para um
ficheiro especificado por fp. Retorna o número de objectos completamente
escritos, o qual será igual a num a menos que tenha ocorrido um erro. Notar
que, programas escritos com fread e fwrite são portáteirs, mas os ficheiros que
foram escritos através destas rotinas poderão não ser, visto as representações
dos tipos de dados serem dependentes da máquina.
Orientada à palavra
A linguagem C fornece funções para leitura e escrita de palavras de
computador. Assim, palavras individuais (com o tipo de dado int) podem ser
lidas e escritas usando as rotinas:
getw(FILE *fp);
putw(int word,FILE *fp);
O seguinte extracto de um programa
int x;
FILE *fp;
...
putw(x,fp);
irá escrever a palavra x no ficheiro especificado por fp. A função retorna a
palavra escrita ou EOF caso ocorra um erro. A linha
x=getw(fp);
irá retornar a próxima palavra do ficheiro referido por fp.
Orientada ao Caractere
As rotinas mais primitivas de input e output são getc e [Link] rotinas lêm
ou escrevem um caracter de cada vez, de e para um ficheiro que é
especificado pelo apontador FILE obtido da chamada de fopen(). A chamada
c=getc(fp);
retorna o próximo caracter de um ficheiro especificado por fp ou EOF se foi
encontrado o fim do ficheiro (ou, se ocorreu um erro). A chamada
putc(c,fp);
escreve o caracter c no ficheiro especificado por fp. retorna também o caracter
c. A ideia que getchar e putchar são funções pode agora ser exposto como
uma fraude. Elas são definidas como macros no ficheiro stdio.h da seguinte
forma:
#define getchar() getc(stdin)
#define putchar() putc(c,stout)
De facto, getc e putc são igualmente macros definidas no stdio.h. A entrada e a
saída parecem funcionar caracter a caracter para conveniência do utilizador,
mas na realidade eles já são bufferizados.
Com formatação
Existem rotinas para entrada/saída formatada de dados, definidas na biblioteca
padrão. Para saída tem-se,
fprintf (FILE *fp, char controle[], arg1, arg2,...)
que é da família da printf, excepto que a saída é escrita para um ficheiro
especificado por fp. A função fprintf pode ser usada para escrever num canal
diferente do standard output. Caso se especificasse o ficheiro dado pela
constante stdout, então fprintf seria o mesmo que printf. Como exemplos ,
FILE *alunos;
...
alunos=fopen(...);
...
fprintf(alunos,%d, %s\n,numero,nome);
...
fprintf(stderr,Este é o %d-ésimo erro\n,nerro);
As especificações do formato para fprintf são as mesmas das do printf. Ter em
atenção, que o printf não trunca o argumento convertido para o ajustar à
largura do campo.
Para a leitura formatada a partir de um ficheiro existe a função fscanf, que
possui a seguinte sintaxe:
fscanf(fp,controle,arg1,arg2,...)
Funciona de forma idêntica ao scanf, excepto que, lê o input de um ficheiro
especificado por fp. A string de controle da função fscanf, é igual à da scanf já
estudada. Caso se especificasse o ficheiro dado pela constante stdin, então
fscanf seria o mesmo que scanf.
Orientada à Linha
A orientação de linha na entrada e saída pode ser obtida usando as funções
fgets, e fputs. A função fgets tem a seguinte sintaxe:
fgets(char linha[],int n, FILE *fp);
Esta função lê a próxima linha de input, incluindo o caracter newline (\n) se
existir, de um ficheiro especificado por fp para o array linha. São lidos todos os
caracteres até ao caractere newline, a não ser que encontre o fim de ficheiro
antes, ou atinja o limite n, e então o array linha é terminado automaticamente
com o caracter nulo. Se atingir o fim de ficheiro, ou houver erro, a função
retorna NULL.
A função de escrita orientada à linha é a fputs, cuja sintaxe se segue:
fputs(char linha[], FILE *fp);
A função escreve a string linha num ficheiro apontado por fp.
8.4 ACESSO ALEATÓRIO
Um ficheiro de input e output é normalmente sequencial. O acesso aleatório do
ficheiro pode ser obtido usando
fseek(FILE *fp, long deslocamento, int origem);
Uma chamada a fseek força o próximo input ou output do ficheiro fp a tomar
lugar numa posição deslocada deslocamento bytes relativamente à origem. Os
valores possíveis para origem são:
SEEK_SET => Início do ficheiro
SEEK_CUR => posição corrente no ficheiro
SEEK_END => fim do ficheiro
A função que retorna o deslocamento (Offset) desde o inicio do ficheiro fp é:
long ftell(FILE *fp);
Para voltar ao início do ficheiro apontado por fp existe a função:
rewind(fp);
Esta função é equivalente a : fseek(fp,0L,0);
8.5 OUTRAS FUNÇÕES DE ENTRADA / SAÍDA
Para completar esta apresentação sobre ficheiros, abordam-se em seguida as
funções de controlo de erros e fim de ficheiro.
ferror(fp);
Retorna um número não nulo se ocorreu um erro durante a leitura ou escrita do
ficheiro especificado por fp. A função de controle de fim de ficheiro é:
feof(fp);
que retorna um número não nulo se foi atingido o fim de ficheiro senão retorna
zero.
9. O PRÉ-PROCESSADOR
9.1 INTRODUÇÃO
O pré-processador é o responsável pela primeira fase da compilação de um
programa em C. Nesta fase são processadas todas as linhas começadas pelo
caracter #. São várias as directivas de pré-processamento.
9.2 INCLUDE
Se um programa em C contém a linha
#include FILE
então o pré-processador irá substituir essa linha pelo conteúdo do ficheiro
referido por FILE. Isto é feito antes de qualquer compilação. Isto é por vezes
usado por ficheiros header contendo definições dos dados usados em ficheiros
diferentes. FILE pode ter uma das duas formas:
nome-ficheiro => Ficheiro do utilizador
<nome-ficheiro> => Normalmente ficheiro do compilador.
Na primeira forma o pré-processador primeiro procura o ficheiro no directório
corrente, e só depois, caso não o encontre, é que o procura no directório
include (directório onde se encontram todas as headers do sistema). Na
segunda forma o pré-processador apenas procura o ficheiro no directório
include. Como exemplo, um programa poderá começar com as linhas :
#include pilha.h
#include <stdio.h>
#include <ctype.h>
#include <math.h>
que inclui um ficheiro do utilizador e três ficheiros do compilador. O ficheiro do
utilizador contém instruções em C, como qualquer outro programa escrito nesta
linguagem.
9.3 DEFINE
Se um ficheiro contém uma linha da forma
#define nome string
então cada ocorrência do identificador nome no resto do ficheiro irá ser
substituída por string. Isto é feito pelo pré-processador antes da compilação.
É um bom estilo de programação usar #define para todas as constantes. É
muito mais simples alterar uma linha da forma,
#define SIZE 1024
que modifica correctamente todas as ocorrências de SIZE no ficheiro.
Alguns pontos técnicos sobre o define:
A string inclui todos os caracteres (incluindo o espaço branco) até
encontrar o caracter newline.
o nome segue exactamente as regras dos nomes das variáveis.
Uma convenção standard é usar nome em maiusculas para se distinguir
as macros das variáveis vulgares.
Não são possíveis substituições nas strings, isto é, vendo no fragmento,
#define SIM 1
printf (SIM=%d\n,SIM);
o pré-processador só faz uma substituição.
A string no define pode ser superior a uma linha; para isso o caracter
newline terá de ser precedido pelo caracter \ Barra Invertida (backslash).
9.4 DEFINE COM ARGUMENTOS
Uma das facilidades da linguagem C consiste em invocar #define com
argumentos. Por exemplo,
#define isdigit(c) (0<=^(c)&&(c)<=9)
pode ser usado exactamente como se existisse uma finção que determine
quando é que o caracter c é um digito. Outro exemplo é a definição,
#define ABS(x) ((x<0? (-x):(x))
a qual faria com que uma linha da forma
y=ABS(a+b)
fosse substituida (pelo pré-processador antes da compilação ) por
y=( a+b<0 ? -a+b;a+b);
Alguns pontos sobre o uso de #define com argumentos:
#define pode ter vários argumentos:
Em :
#define nome(arg1,arg2,...)
não deverá ser deixado qualquer espaço entre o identificador nome e o
parêntises esquerdo.
Os argumentos arg1,... obedecem às mesmas condições dos nomes das
variáveis.
O uso abusivo do #define com argumentos pode levar a alguns erros
aborrecidos. Notar que a macro anterior ABS tem a vantagem de que
trabalhará para variáveis de diferentes tipos. Se uma macro tem uma string de
substituição muito longa ou complicada, talvez seja preferível implementá-la
como uma função.