Identificadores
Os identificadores em C são constituídos por sequências de letras, underscore, ou dígitos. O primeiro caracter é necessariamente uma letra ou underscore. Notar que os identificadores são case-sensitive, por exemplo em int i, I
; temos duas variáveis diferentes.
Utilizamos frequentemente underscore no início dos nomes da bibliotecas para minimizar possíveis conflitos. Por convenção os nomes das variáveis são em minúsculas e os nomes das constantes são em maiúsculas.
Existem nomes reservados, como if
, else
, int
, float
, etc.
Tipos de dados
char
int
float
double
long double
short int
oushort
long int
oulong
unsigned
, obedece à aritmética módulo $2^n$ (em que $n$ é o número de bits)unsigned char
signed char
(entre -128 e 127)
O tamanho de um char
é 1 byte (ou 8 bits). O tamanho típico de um int
é 4 bytes. O tamanho de um short
é sempre menor que o tamanho de um int
, que por sua vez é sempre menor que o tamanho de um long
.
Podemos obter o tamanho de um tipo através do sizeof
.
Formatos de leitura e escrita:
char
:%c
int
:%d
ou%i
(base decimal)int
:%x
(base hexadecimal)short int
:%hd
long int
:%ld
unsigned short int
:%hu
unsigned int
:%u
unsigned long int
:%lu
float
edouble
:%f
Conversão de tipos
Argumentos de operadores de diferentes tipos provocam transformação de tipos dos argumentos. Em geral as conversões automáticas ocorrem de representações "estreitas" para representações mais "largas", como no caso da conversão de int
para float
em f + i
.
Quando um operador binário (+
, *
, etc) tem operandos de tipos diferentes, os tipos dos operandos são convertidos. Quando não há argumentos unsigned
, se algum dos operandos é long double
, converte o outro para long double
, caso contrário, se um dos operandos é double
, converte o outro para double
, caso contrário, se um dos operandos é float
, converte o outro para float
, caso contrário, converte short
para int
e se algum dos operandos for long
, converte o outro para long
.
O char
é um inteiro pequeno e, portanto, podem-se fazer operações aritméticas com caracteres. Exemplo:
/* Funcao que recebe como argumento uma string composta apenas por * digitos e devolve o inteiro correspondente. */ int atoi(char s[]) { int i, n; n = 0; for (i = 0; s[i] >= ’0’ && s[i] <= ’9’; i++) n = 10 * n + (s[i] - ’0’); return n; }
Podemos também forçar a conversão de tipos através de cast,
(<tipo>) <expressao>
em que o valor da <expressão>
é convertido para o tipo <tipo>
como se se tratasse de uma atribuição. Exemplo:
int i = (int) 2.34;
Numa conversão de float
para int
ocorre uma truncagem. No caso da conversão de double
para float
pode ocorrer arredondamento ou truncagem.
Na chamada a funções não é necessário recorrer a uma conversão forçada de tipos, dada a função double sqrt (double x)
, root2 = sqrt(2)
é equivalente a root2 = sqrt(2.0)
.
Tipos enumerados
Tipo enumerado definido por sequência de constantes:
enum resposta { NAO, SIM }
Neste caso o tipo resposta
tem duas constantes, NAO
e SIM
.
As constantes de tipo enumerado têm valor inteiro (int
), em que a primeira constante vale 0
, a segunda vale 1
, etc. No caso do tipo resposta
o NAO
vale 0
e o SIM
vale 1
.
Podemos especificar valores para as constantes ou não:
enum meses { JAN=1, FEV=2, MAR, ABR, MAI, JUN, JUL, AGO, SET, OUT, NOV, DEZ }
Os tipos enumerados permitem criar uma abstração dos valores quando se programa usando apenas os nomes do tipo enumerado.
Exemplo:
#include <stdio.h> enum meses { JAN = 1, FEV, MAR, ABR, MAI, JUN, JUL, AGO, SET, OUT, NOV, DEZ }; int main () { enum meses mes; mes = FEV; mes++; if (mes == MAR) puts("Estamos em Março"); return 0; }
Declaração de variáveis
A declaração de variáveis precede a sua utilização e especifica o seu tipo. Em geral, a declaração de variáveis pode ocorrer em sequência:
int superior, inferior, passo; char c, linha[1000];
Ou em alternativa:
int superior; int inferior; int passo; char c; char linha[1000];
As variáveis devem ser sempre inicializadas. No caso particular d inicialização de variáveis externas (globais) e estáticas devemos declarar e inicializar:
<tipo> <variável> = <expressão constante>;
As variáveis globais são declaradas fora das funções. As variáveis estáticas podem ser locais a uma função, mas mantêm o valor entre chamadas à função. Em C só as variáveis globais e estáticas são inicializadas automaticamente a 0, se o utilizador não fornecer nenhuma inicialização explícita. A declaração de variáveis estáticas implica a utilização de static
imediatamente antes da declaração:
static int x = 10;
Inicialização de variáveis automáticas (locais):
<tipo> <variável> = <expressão>;
As variáveis automáticas são reinicializadas sempre que a função é invocada e, por omissão, o valor é indefinido.
Exemplo:
int global; int contador() { static int i = 1; return i++; } int main() { int a = global + contador(); int b = contador(); int c = contador(); printf(“a = %d, b = %d, c = %d\n”, a, b, c); return 0; }
Quais os valores escritos no standard output para a
, b
e c
? Porquê?
a = 1, b = 2, c = 3
O const
pode anteceder qualquer declaração e significa que valor não vai mudar. Se tentarmos modificar o valor, o compilador emitirá um erro e. Em geral o compilador utiliza a informação do const
para proceder a optimizações. Exemplos do uso de const
:
const double e = 2.71828182845905; const char msg[] = "bem vindo ao C"; int strlen(const char[]);
Operadores
Operadores Aritméticos: +
, -
, *
, /
e %
. Em geral o comportamento em situação de overflow e underflow não está definido.
Operadores Relacionais: >
, >=
, <
, e <=
(precedências iguais), ==
e !=
(precedências inferiores).
Operadores Lógicos: !
, &&
e ||
.
Precedências: aritméticos, relacionais, &&
e ||
.
Os operadores &&
e ||
avaliam argumentos da esquerda para direita e param se os argumentos são suficientes para definir valor. O valor numérico de uma expressão lógica é diferente de 0
se a expressão é verdadeira e é igual {{0}} se falsa.
Operadores prefixos e posfixos:
- operador incrementar variável (
++
) e decrementar variável (--
) (estes operadores retornam o valor da variável); - operadores prefixos (
++<var>
,--<var>
) primeiro incrementa/decrementa e depois retorna valores; - operadores posfixos (
<var>++
,<var>--
) primeiro retorna valor e depois incrementa/decrementa.
Exercícios:
- Se
n
é5
, qual é o valor dex
depois dex=n++
? - Se
n
é5
, qual é o valor dex
depois dex=++n
?
Em C é possível efectuar operações sobre a representação binária. Manipulação de bits em inteiros (char
, short
, int
, long
):
&
, AND bit a bit|
, OR bit a bitˆ
, XOR (OR exclusivo) bit a bit<<
, shift left>>
, shift right~
, complemento
Exmplos:
n = n & 15;
põe a zero todos os bits que não os4
de ordem mais baixa;n = n | 10;
põe a um os bits que estão a um na representação binária do número10
.n = x ˆ y;
põe emn
a um (zero) os bits que emx
ey
são diferentes (iguais).n = x << 2;
desloca bits2
posições para a esquerda (espaço libertado é preenchido com0
e cada bit deslocado equivale a uma multiplicação por2
).n = x >> 2;
desloca bits2
posições para a direita (espaço libertado é preenchido com0
e cada bit deslocado equivale a uma divisão por2
).
Exercício: Quais os valores de z
e w
no seguinte exemplo?
int x = 1, y = 2; /* x = 01, y = 10 */* int z = x & y; /* z = 00 */ int w = x && y; printf("z = %d w = %d \n", z, w); /* z = 0 w = 1*/
Atribuições e expressões
Exemplo:
i = i + 1;
Que pode ser reescrito como:
i += 1;
Portanto, +=
é um operador atribuição e existem outros operadores correspondentes utilizando -
, *
, /
, %
, >>
, <<
, &
, ˆ
, |
.
Notar que <expr1> <op>= <expr2>
equivale a <expr1> = (<expr1>) <op> (<expr2>)
. Os operadores atribuição têm algumas vantagens:
- mais próximo maneira de pensar de humanos (ex:
i += 2;
); - simplifica leitura de expressões complicadas (ex:
yyval[yypv[p3+p4]+yypv[p1+p2]] += 2;
).
Expressões condicionais
O valor de uma expressão condicional depende do valor de uma outra expressão:
<expr1> ? <expr2> : <expr3>
Se <expr1>
for verdadeiro, valor da expressão é <expr2>
. Se <expr1>
for falso, valor da expressão é <expr3>
.
Exemplo:
int maior (int a, int b) { if (a > b) return a; else return b; } int maior (int a, int b) { return (a > b ? a : b); }
Precedências e ordem de avaliação
() [] -> . ED Maior precedência ! ~ ++ -- + - * & (tipo) sizeof DE V * / % ED V + - ED V << >> ED V < <= > >= ED V == != ED V & ED V ˆ ED V | ED V && ED V || ED V ?: DE V = += -= *= /= %= &= ˆ= |= <<= >>= DE V , ED Menor precedência
Controlo de execução
O controlo de execução na linguagem C tem por base um conjunto de construções básicas, como já vimos nos exemplos apresentados nas aulas anteriores. Cada instrução termina com ;
, que denota o fim da mesma. As instruções podem ser agrupadas em blocos delimitados por chavetas, { }
. Os blocos são utilizados, por exemplo, na implementação de funções, onde contêm as instruções das mesmas, e para delimitar conjuntos de instruções em if
, switch
, while
, etc.
Os operadores if
e switch
permitem definir execuções condicionais. No caso genérico
if ( <expressao> ) { <instrucoes_1> } else { <instrucoes_2> }
se <expressao>
tiver um valor diferente de 0
, então as <instrucoes_1>
são executadas, caso contrário se <expressao>
tiver o valor 0
, então as <instrucoes_2>
são executadas.
Existem situações em que a decisão condicional depende de múltiplas opções e em que o switch
pode ser útil. No caso genérico
switch ( <expressao> ) { case <const-expr>: <instrucoes> case <const-expr>: <instrucoes> default: <instrucoes> }
a <expressao>
é avaliada e comparada com os valores <const-expr>
. Se a <expressao>
tiver um valor igual a um dos valores <const-expr>
, então as <instrucoes>
correspondentes a esse case
, e a todos os case
seguintes, são executadas. Para garantir que apenas as instruções associadas ao case
cujo valor é igual ao da expressão são executadas, temos de utilizar no final de <instrucoes>
a instrução break
:
switch ( <expressao> ) { case <const-expr>: <instrucoes> break; case <const-expr>: <instrucoes> break; default: <instrucoes>
As <instrucoes>
associadas ao default
são opcionais e são em geral executadas apenas quando a expressão é diferente de todos os valores <const-expr>
.
Os operadores for
, while
, e do while
permitem definir ciclos. No caso de ciclos genéricos utilizamos em geral o while
,
while ( <expressao> ) { <instrucoes> }
caso em que as <instrucoes>
são executadas enquanto a <expressao>
tiver um valor diferente de 0
e, portanto, o ciclo termina quando o valor da <expressao>
for 0
.
O for
permite definir ciclos contados,
for ( <instrucoes_de_inicializacao> ; <expressao> ; <instrucoes_do_passo> ) { <instrucoes> }
caso em que as <instrucoes_de_inicializacao>
são executadas antes do ciclo, após o que as <instrucoes>
são executadas enquanto a <expressao>
tiver um valor diferente de 0
. No final de cada execução das <instrucoes>
, as <instrucoes_do_passo>
são executadas antes da <expressao>
ser avaliada novamente. O ciclo termina quando o valor da <expressao>
for 0
. Notar que as instruções que fazem parte dos conjuntos <instrucoes_de_inicializacao>
e <instrucoes_do_passo>
são separadas por ,
e não por ;
.
Qualquer ciclo for
pode é claro ser reescrito como um ciclo while
:
<instrucoes_de_inicializacao> for ( <expressao> ) { <instrucoes> <instrucoes_do_passo> }
Existem situações em que a condição deve ser avaliada apenas no final do ciclo, ou pelo menos após a primeira iteração do mesmo, caso em que podemos utilizar o do while
,
do { <instrucoes> } while ( <expressao> );
caso em que, se forma semelhante ao while
, as <instrucoes>
são executadas enquanto a <expressao>
tiver um valor diferente de 0
e, portanto, o ciclo termina quando o valor da <expressao>
for 0
. No entanto, neste caso, as <instrucoes>
são executadas sempre pelo menos uma vez. Por exemplo, no caso abaixo o scanf
é sempre executado uma vez e voltará a ser executado até que o valor lido seja menor do que 2
.
/* O valor de n tem que ser superior ou igual a 2 */ int n = 0; do { scanf("%d", &n); } while (n < 2);
Da mesma forma que podemos utilizar a instrução break
para interromper a execução do switch
, também podemos utilizá-la para terminar a execução do while
, do for
, e do do while
. Por outro lado podemos utilizar a instrução continue
para desencadear de imediato a execução da próxima iteração do while
, do for
, e do do while
. Notar que no caso do continue
ocorrer num for
as instruções de passo são sempre executadas.
Exercício: O que acontece no caso abaixo? E se fosse break
em vez de continue
?
for(i = 0; i < n; i++) { if (a[i] < 0) continue; printf("%d\n", a[i]); }