Notas de rodapé: linguagem C


Variáveis booleanas e funções booleanas

Uma variável ou função é booleana se admite apenas dois valores:  01.

(A propósito, veja a interface stdbool.h, que define o tipo bool e as constantes true e false.)


Valor de expressão em C

Toda expressão em C tem um valor. Por exemplo, se x vale 20 então a expressão  3 * x + 4  tem valor 64.  Nas mesmas condições, a expressão

y = 3 * x + 4

(note que y = faz parte da expressão) tem valor 64.  O cálculo do valor dessa expressão tem o efeito colateral de atribuir à variável y o valor da subexpressão 3 * x + 4.  Assim, y passa a valer 64.

Outro exemplo:  se x vale 20 então o valor da expressão  x = x + 1  é  21.  Calcular o valor dessa expressão tem o efeito colateral de incrementar o valor de x.

Eis um exemplo mais delicado, muito comum em C:  se x vale 20 então o valor da expressão

x++

é  20.  Um exemplo sutilmente diferente:  se x vale 20 então o valor da expressão

++x

é  21.  Em ambos os casos, as expressões têm como efeito colateral o incremento do valor de x, que passa a valer 21.

Algo análogo vale para expressões da forma  x--  e  --x.

(Veja a resposta à pergunta What is the difference between I++ and I = I+1 in C language? no Quora.)

Mais um exemplo: O valor da expressão  c = (a = 1) + (b = 2)  é  3.  O efeito colateral de cálcular o valor da expressão é a atribuição dos valores 1, 2 e 3 às variáveis a, bc respectivamente.

Último exemplo: No trecho de código abaixo, o valor da expressão  c = i  é  255  (e não −1, como alguém poderia imaginar):

unsigned char c;
int i;
i = -1;
c = i;


Cálculo do valor de expressão booleana

Toda expressão booleana (= expressão lógica) em C tem valor 0 ou 1. Por exemplo, o valor da expressão

   tt == 32

é 1 se tt vale 32 e é 0 se tt não vale 32.  O valor da expressão

   rand () <= RAND_MAX / 2

é 1 se a função rand devolver um número menor ou igual a RAND_MAX / 2 e é 0 se a função devolver um número maior que RAND_MAX / 2.

A ordem dos fatores em uma expressão booleana.  O valor de toda expressão booleana é calculado da esquerda para a direita. Se o valor da expressão ficar claro antes que a expressão toda tenha sido examinada, o cálculo é interrompido. Essa regra — conhecida como minimal evaluation ou short-circuit evaluation — evita o cálculo de subexpressões de valor indefinido.

No exemplo abaixo, embora as duas expressões pareçam equivalentes, a primeira está correta enquanto a segunda pode estar errada se o valor de v[n] não estiver definido.

   if (j < n && v[j] < x) ...

   if (v[j] < x && j < n) ...


Divisão inteira

Se n é um número positivo do tipo int, o valor da expressão  n/2  é  n/2⌋ ,  ou seja,  o piso do quociente de n por 2.  Por exemplo: se n vale 9 então n/2 vale 4.

Se n é um número negativo do tipo int, o valor da expressão  n/2  é  −⌊−n/2⌋ .  (Parece estranho, mas é assim que funciona.)  Por exemplo: se n vale −9 então n/2 vale −4 e não −5.

De modo mais geral, se n e m são do tipo int e têm o mesmo sinal, então o valor da expressão  n/m  é  n/m.  Se os sinais de n e m são diferentes então o valor da expressão  n/m  é  −⌊−n/m.


Casting

Um  (int)  antes de uma expressão aritmética é um molde (= cast), ou coerção.  Se x é uma variável do tipo double, a expressão

   (int) (x/2)

tem por valor a parte inteira de x/2.  Assim, se x vale 9.0 então o valor da expressão é 4 (não 4.5).

Outro exemplo:  se n é uma variável do tipo int então a expressão

   (double) n / 2

significa o mesmo que

   ((double) n) / 2

e transforma o valor de n em um número do tipo double antes de fazer a divisão por 2.  Se o valor de n for 9 o valor da expressão será 4.5 (não 4).

Mais um exemplo: se h é uma variável do tipo int com valor −1 então o valor da expressão

   (unsigned) h

é UINT_MAX, ou seja, 4294967295.

O casting é automático em situações como

char *ptr;
ptr = malloc (1);

e portanto não é necessário escrever

ptr = (char *) malloc (1);


Constantes em C

No fragmento de código abaixo,  a  é uma variável,  999  é uma constante inteira,  e  'a'  é uma constante-caractere (também conhecida como literal):

   a = 999;
   c = 'a';

Da mesma forma, a expressão  "EXEMPLO"  no fragmento de código a seguir é uma constante-string (também conhecida como literal):

   strcpy (s, "EXEMPLO");

O primeiro argumento das funções printf e scanf é usualmente uma constante-string:

scanf ("%d", &n);
printf ("O valor de n é %d", n);


A sintaxe de typedef

A sintaxe do operador typedef é simples.  Primeiro, escreva a declaração de uma variável de algum tipo. Depois, escreva typedef antes da declaração.  Por exemplo,

   int *ptri;

declara uma variável ptri (do tipo ponteiro-para-inteiro).  Agora escreva typedef antes da declaração:

   typedef int *ptri;

Com isso, ptri passa a ser o nome de um novo tipo (idêntico ao tipo ponteiro-para-inteiro).  Esse tipo pode ser usado para declarar novos ponteiros-para-inteiro:

   ptri p, q;

Outro exemplo:  A seguinte declaração cria uma variável ponto (que é um par-de-inteiros):

   struct {
      int x;
      int y;
   } ponto;

Agora escreva typedef antes da declaração:

   typedef struct {
      int x;
      int y;
   } ponto;

Isso transforma ponto em um novo tipo (idêntico a par-de-inteiros).  Esse tipo pode ser usado para declarar novos pares-de-inteiros:

   ponto a, b;

(A propósito, veja typedef versus #define in C em GeeksforGeeks.)


Funções em C e na matemática

A palavra função tem sentidos um pouco diferentes na computação e na matemática.  Na matemática, uma função recebe argumentos e devolve um valor.  Na computação, além de receber argumentos (= dados) e devolver um valor (= resultado), uma função pode também produzir efeitos colaterais (como modificar os argumentos, ou imprimir algo).


Chamada por valor

Em C, quando uma função é invocada, todos os argumentos são passados à função em valor (e não por referência).  Assim, a função recebe os valores de variáveis e não as variáveis propriamente ditas. (Podemos dizer, informalmente, que a função recebe cópias das variáveis.)  Essa maneira de passar argumentos a uma função é conhecida como call by value.

Isso vale, em particular, para argumentos que são vetores.  Um vetor em C é representado pelo endereço de seu primeiro elemento. Assim, quando um vetor é passado para uma função, a função recebe esse endereço (e não algum ponteiro que armazena esse endereço).


Variáveis globais

Uma variável é global (= global) se for definida fora de todas as funções que constituem o programa. Variáveis globais são acessíveis em qualquer ponto do programa (mesmo em outros módulos do programa).  Exemplo:

int glob; // variável global 

int main (void) {
   glob = 0; 
   func ();
   printf ("%d", glob);
   return EXIT_SUCCESS;
}

void func (void) {
   glob += 1;
}

Não use variáveis globais indiscriminadamente!  Use variáveis globais apenas quando elas são realmente necessárias!


A declaração static

A palavra-chave static antes da declaração de uma variável global torna a variável privada no módulo em que está sendo declarada. Em outras palavras, a variável é invisível para outros módulos.

A palavra-chave static tem o mesmo efeito sobre as declarações de funções: a função torna-se privada no módulo e invisível em outros módulos.

Por exemplo, num módulo que implementa o algoritmo Mergesort, é razoável que a função auxiliar intercala não possa ser vista de fora do módulo:

static 
void intercala (int p, int q, int r, int v[]) {
   . . . 
}

void mergesort (int p, int r, int v[]) {
   if (p < r-1) {
      int q = (p + r);
      mergesort (p, q, v);
      mergesort (q, r, v);
      intercala (p, q, r, v);
   }
}

(Veja static funcions e static variables em GeeksforGeeks.)


O que a função main devolve?

Todo programa em linguagem C é um conjunto de funções, uma das quais é main.  A execução do programa consiste na execução da função main (que tipicamente invoca outras funções do conjunto). A função main devolve um inteiro para informar o sistema operacional sobre o fim da execução do programa.  Por exemplo,

int main (void) {
   ...
   return 0;
}

ou

int main (int argc, char **argv)
   ...
   return 0;
}

A função main devolve 0 caso o programa tenha terminado de maneira normal e devolve um número não-nulo caso o programa tenha terminado de maneira excepcional.  (Veja também a função exit.)  Usamos as constantes EXIT_SUCCESS (que vale 0) e EXIT_FAILURE (que vale 1) como valor devolvido por main.

É um erro definir a função main como se ela fosse do tipo void:

void main (...) {
   ...
}

(Veja resposta de Terry Lambert à pergunta Why do most of the people use int as return type for main function and return value as 0 even when there is no use of returning it? no Quora.  Veja também Is it fine to write “void main()” or “main()” in C/C++? em GeeksforGeeks.)


A função exit

A função exit da biblioteca stdlib interrompe a execução do programa e fecha todos os arquivos que o programa tenha porventura aberto.  Se o argumento da função for 0, o sistema operacional é informado de que o programa terminou com sucesso; caso contrário, o sistema operacional é informado de que o programa terminou de maneira excepcional.

O argumento da função é tipicamente a constante EXIT_FAILURE, que vale 1, ou a constante EXIT_SUCCESS, que vale 0.

A propósito, a instrução  return XXX;  que encerra o código da função  main  é equivalente a  exit (XXX);.


Leiaute da declaração de ponteiros

O layout da definição de um ponteiro é sabidamente desconfortável.  Conceitualmente, um ponteiro-para-int é um novo tipo-de-dados e isso sugere que se escreva o * colado ao int:

int* p;

Do ponto de vista técnico, entretanto, o * modifica a nova variável e não o int. Isso sugere que o * seja colado ao p:

int *p;

O compilador C aceita qualquer das formas. Também aceita

int * p;

(Veja a pergunta In C/C++, why do people declare pointers as "int *x" instead of "int* x"? no Quora.)


Ponteiros soltos

Convém não deixar ponteiros soltos (= dangling pointers) no seu código, pois isso pode mascarar erros de programação e pode ser explorado por hackers para atacar sua máquina.  Portanto, atribua NULL a cada ponteiro liberado:

free (ptr);
ptr = NULL;

Atribuir um valor a um ponteiro que se tornou inútil é certamente deselegante.  Infelizmente, não há como lidar com hackers de maneira elegante…

Por motivos semelhantes, recomenda-se inicializar todo ponteiro (ou seja, atribuir algum valor ao ponteiro) tão logo ele é declarado. Por exemplo, recomenda-se escrever

int *p = NULL;

em vez que

int *p;

O presente sítio ignora essas recomendações de segurança para não cansar o leitor com detalhes repetitivos.


Newline no fim de arquivo de texto

É um costume civilizado colocar um caractere \n no fim de todo arquivo de texto.

(Também é uma boa ideia eliminar os espaços no fim das linhas, ou seja, evitar que o caractere que precede um \n seja um \ .)


C ainda é importante?