Nas aulas práticas apercebi-me que vocês cometem erros de sintaxe na programação com a linguagem C de ``bradar aos céus''.
Julgo que este problema resulta duma compreensão deficiente de alguns conceitos associados a linguagens de programação, obtida, talvez, da leitura de apontamentos resumidos que foram criados para suporte de aulas teóricas. Para corrigir estes problemas, sugiro que leiam com cuidado um livro como The C Programming Language de Kernighan e Ritchie, o qual faz uma apresentação sintética mas completa dos conceitos envolvidos.
Correndo o risco de mais uma vez apresentar conceitos duma forma resumida, este documento procura esclarecer alguns aspectos que me parecem fundamentais, mas dos quais muitos de vocês têm ideias nebulosas.
Em C, cada variável tem um tipo, o qual deve ser indicado quando a variável é declarada ou definida. Por exemplo:
int main() { int n;define a variável de nome n como sendo do tipo int, i.e. inteiro.
O tipo duma variável especifica:
Por exemplo:
n = 3.14;não é uma instrução correcta porque atribui um valor real a uma variável do tipo inteiro. (Embora seja possível que o compilador gere código que trunque a parte decimal do valor real, e não indique qualquer erro.)
De igual modo, em
float x, pi = 3.14; x = pi % 3;a instrução
x = pi % 3;
não é válida porque
a operação %
(módulo, ou resto da divisão inteira)
requer que ambos os operandos sejam inteiros.
Tal como as variáveis, as constantes também têm tipos. Por
exemplo, 3
é uma constante do tipo inteiro, enquanto que
3.0
é uma constante do tipo real e não pode ser usada
como operando da operação %
.
Mais, qualquer expressão da linguagem C tem um tipo. O tipo
duma expressão depende do tipo dos seus operandos e das
operações envolvidas na expressão. Por exemplo, se
n
fôr do tipo inteiro, a expressão:
n/2é do tipo inteiro. No entanto, a expressão:
n/2.0é do tipo real, mesmo assumindo que
n
é do tipo inteiro.
Uma regra fundamental é:
numa instrução de atribuição os tipos das expressões dos dois têm de ser idênticos, ou pelo menos terá ser possível converter o tipo do lado direito no tipo do lado esquerdo
Esta regra é análoga à regra da física:
Numa equação, as unidades do lado esquerdo têm ser idênticas às unidades do lado direito.
Violar esta regra dos tipos é um erro inaceitável e que permite classificar a pessoa como um ``programador do fundo da escala''.
Um erro semelhante à violação desta regra diz respeito à invocação duma função com argumentos cujos tipos não são os (ou não são convertíveis nos) especificados na definição da função.
A maioria dos erros devido à incompatibilidade dos tipos das expressões usadas surgem no contexto de apontadores.
Um apontador é uma variável cujo valor é um endereço duma variável de um determinado tipo. Por exemplo:
int *p;define a variável
p
como sendo do tipo endereço
de inteiro, ou seja um apontador para inteiro.
Apontadores podem ser inicializados usando o operador &
.
Por exemplo:
int n; int *p = &n;inicializa
p
com o endereço da variável n
,
como ilustra a figura seguinte:
Por exemplo, o código seguinte:
int n = 2; int *p = &n; printf(``n = %d\t *p = %d\t p = %p \t &n = %p\n'', n, *p, p, &n);imprimirá qualquer coisa como:
n = 2 *p = 2 p = 0xbffffc84 &n = 0xbffffc84Isto é, o endereço da variável
n
é o valor
da variável p
e vale 0xbffffc84
(um endereço
de 32 bits).
Para interpretar expressões usando apontadores ou endereços, pode ajudá-lo ser as seguintes expressões como indicado:
n
;
p
;
Muitas vezes, o uso de apontadores aparece associado à alocação
dinãmica de memória. De facto, a função malloc()
da biblioteca de C retorna o endereço da primeira posiçào
de memória do bloco reservado. Por exemplo:
char *buf; buf = (char *)malloc(BUF_SIZE * sizeof(char));reserva um buffer, i.e. um bloco de memória, de tamanho
BUF_SIZE
, em princípio para guardar carácteres.
Outro uso comum de endereços na linguagem C é como argumentos de funções. De facto, a passagem dum endereço como argumento duma função permite que essa função altere o valor da variável apontada pelo argumento, e deste modo tornar esta alteração visível fora dessa função. Por exemplo:
int inet_aton(char *cp, struct in_addr *inp);a qual converte um endereço IP em formato dotted decimal no formato especificado na norma IP, usa 2 apontadores como argumentos:
struct in_addr
a inicializar com o endereço IP;
inp
seja visível fora de inet_aton()
.
(Uma alternativa era inet_aton()
retornar o valor
da conversão, o qual seria atribuído a uma variável de tipo
apropriado.)
Na linguagem C, vectores e strings estão intimamente ligados a endereços e apontadores.
De facto, o nome dum vector denota o endereço do primeiro elemento desse vector. Por exemplo, o código seguinte:
int fib[10]; int *p1 = fib; int *p2 = &(fib[0]); fib[0] = 1; printf(``fib = %p \t p1 = %p\t p2 = %p\n'', fib, p1, p2); printf(``fib[0] = %d \t*p1 = %d\t *p2 = %d\n'', fib[0], *p1, *p2);imprime qualquer coisa como:
fib = 0xbffffc68 p1 = 0xbffffc68 p2 = 0xbffffc68 fib[0] = 1 *p1 = 1 *p2 = 1
Atendendo a que o nome dum vector é um endereço, e não uma variável, a seguinte atribuição:
int fib[10]; int n; fib = &n;não é legal, e o compilador assinala um erro de compilação. De facto, esta atribuição é análoga à seguinte:
5 = 4;que, espero, vocês reconheçam imediatamente como um erro.
O seguinte exemplo ilustra mais uma vez a relação entre vectores e endereços:
char buf[BUF_SIZE] int s; ... recv(s, buf, BUF_SIZE, 0);Note que o nome do vector buf[] é passado como segundo à função
recv()
, a qual espera como segundo argumento
um endereço de void. Ora, buf
é o endereço
da primeira posição do vector buf[]
, i.e. o endereço
dum carácter, há assim que usar um cast para indicar
ao compilador que sabemos o que estamos a fazer, e não nos
incomodar com mensagens de aviso.
Sendo uma string uma sequência de carácteres guardados em posições de memória consecutivas, ``não é preciso ser engenheiro'' para reconhecer que uma string pode ser acedida usando o endereço do seu primeiro carácter. Assim, por exemplo:
char *msg = ``Hello World!!!\n''; printf(``%s'', msg);gera a mensagem que seria de esperar.
De facto, conhecer o primeiro carácter duma string não
é suficiente para especificar a string: qualquer prefixo
dessa string tem o mesmo primeiro carácter. A convenção
que se usa em C é a seguinte: o último carácter duma
string é o carácter cujo código é 0x00
(lembre-se que o tipo char
usa apenas um ``baite'') e que
se costuma designar por end of string character.
Remover o carácter 0x00
do fim duma string pode
conduzir a resultados inesperados, o menos inócuo dos quais
será um core dump durante a fase de teste.
A relação entre strings e vectores de carácteres parece também ser óbvia: ambos podem ser identificados usando os endereços dos seus primeiros elementos.
No entanto a relação entre strings e vectores de carácteres é bem mais subtil do que a observação acima deixa transparecer. Por exemplo:
char buf[BUF_SIZE]; buf = ``Hello World!\n''não é legal, embora os tipos dos 2 lados da atribuição sejam compatíveis. O problema aqui foi já mencionado acima:
buf
é um endereço, ou melhor, uma constante, não uma
variável, pelo que não pode ser modificado.
No entanto, o seguinte segmento já é válido:
char buf[BUF_SIZE] = ``Hello World!\n'';Neste caso, o que se passa é que os primeiros elementos do vector
buf
são inicializados com os carácteres
da string ``Hello World!\\n''
(carácter
end-of-string incluído).
Mas a única maneira de inicializar um vector de carácteres é quando da sua declaração? A resposta é não, como ilustra o seguinte exemplo:
char buf[BUF_SIZE]; strcpy(buf, ``Hello World!\n'');Neste caso usa-se a função:
char *strcpy(char *dest, const char *src)para copiar a string ``Hello World!\n'' para o vector
buf[]
. Este exemplo ilustra dois outros
aspectos já mencionados:
Foi com surpresa que constatei a dificuldade de alguns de vocês em usar funções em C.
Antes de mais, o conceito de função na linguagem C é essencialmente o conceito matemático de função. Isto é, uma função associa um valor dum conjunto, ``domínio'', a um valor dum outro conjunto.
Por exemplo, a função int fact(int n)
associa um inteiro ao seu factorial (i.e., a outro inteiro
que é o valor da função matemática factorial).
Um exemplo mais complexo é a função
int inet_aton(char *cp, struct in_addr *inp)
,
a qual associa um inteiro a um par de endereços,
o primeiro um endereço dum carácter, e o segundo
um endereço duma struct in_addr
. Além disso,
a função inet_aton()
tem um efeito lateral:
inicializar a struct in_addr
cujo endereço é o valor de
inp
, com a representação em formato de rede do
endereço IP, cujo valor em formato dotted decimal
é apontado por cp
.
Note, que algumas funções, como fact()
não têm
efeitos laterais, enquanto que outras funções só têm
efeitos laterais (nomeadamente, aquelas cujo tipo do valor
retornado é void
).
Na linguagem C, as funções estão associadas a diferentes tipos de construções, nomeadamente:
Nos parágrafos seguintes consideramos cada uma destas construções.
Qualquer função usada num programa em C tem que ser definida. A definição duma função inclui as instruções que a função executa quando é invocada.
Por exemplo, a função int fact(int n)
que calcula o
factorial dum inteiro pode ser definida como se segue:
unsigned int fact(unsigned int n) { unsigned int f; for( f = 1; n > 0; n--) f = f * n; return f; }
n
é o argumento (formal) da função fact()
, a qual
retorna um inteiro sem sinal: o factorial de n
.
Note que os argumentos formais duma função são essencialmente
variáveis usadas nas instruções que constituem a função
propriamente dita. De facto, a função fact
usa o
argumento n
de modo análogo à variável f
.
Os valores dos argumentos são inicializados quando a função
é invocada.
Note que um programador não tem que definir todas as funções
que invoca num programa. Aliás, esta é uma das vantagens do
uso de funções: reutilizar código que executa tarefas
frequentemente usadas. Por exemplo, as funções inet_aton()
e printf()
estão definidas na biblioteca da linguagem
C, pelo que não é necessário, ou melhor, não devem, ser
definidas de novo.
Como dito acima, as instruções especificadas na definição duma função são executadas quando essa função é invocada.
Na linguagem C, uma função é invocada especificando
o seu nome e o valor de cada um dos seus argumentos. Por exemplo,
a função fact()
poderia ser invocada como se segue:
int main() { int i, f; for(i = 0; i < 10; i++) { f = fact(i); printf("%d! = %d\n", i, f); } }Neste exemplo, a função
fact()
é invocada 10 vezes,
variando o valor do seu argumento de 0 a 9, inclusivé. Como
indicado, pode atribuir-se o valor retornado por uma função
a uma variável de tipo apropriado. Poder-se-ia
ter usado o valor retornado directamente na função
printf()
:
printf("%d! = %d\n", i, fact(i));De facto, o argumento duma função é o valor da expressão correspondente. Assim, neste caso, o 3o argumento de
printf()
é o valor retornado pela invocação fact(i)
.
Lembre-se que os tipos das expressões usadas nos argumentos na invocação duma função devem ser compatíveis com os tipos especificados na definição da função, devendo usar-se casts se necessário.
Por exemplo, em:
char buf[BUF_SIZE]; int s; ... if( send(s, buf, BUF_SIZE, 0) == -1 ) { ...o segundo argumento de
send()
é buf
, o
nome dum vector de caracteres, ou seja, uma expressão cujo tipo
é char *
, o qual é compatível com o tipo do segundo
argumento void *
especificado na definição de send()
:
void *
é um tipo especial: qualquer tipo endereço é compatível com o tipovoid *
, pelo que não é necessário usar casts
A declaração duma função consiste essencialmente no
texto que precede o corpo duma função. Por exemplo, a
função fact()
pode ser declarada como se segue:
unsigned int fact(unsigned int);
A linguagem C requer que uma função seja declarada antes de ser invocada, sempre. Fazer a declaração duma função antes da sua invocação permite que o compilador verifique se a função é invocada correctamente: nomeadamente que é usada numa expressão com o tipo apropriado e que os tipos dos seus argumentos são compatíveis com os declarados.
Tipicamente, as declarações de funções são
feitas em header files, as quais deverão ser
incluídas nos ficheiros que invocam essas funções.
Por exemplo, <stdio.h>
inclui a declaração de
printf()
.
Obs. Se definir a função num ficheiro, antes de invocar essa função, não precisa de a declarar.
This document was generated using the LaTeX2HTML translator Version 98.2 beta6 (August 14th, 1998)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html -t INST-C -split 0 inst_C.tex
The translation was initiated by Pedro Souto on 2000-04-01