Home RecentChanges

Lesson05

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

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:

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:

Exercícios:

Em C é possível efectuar operações sobre a representação binária. Manipulação de bits em inteiros (char, short, int, long):

Exmplos:

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:

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]);
}