Tópicos avançados

O ponteiro genérico void*

Em C, o tipo void* é especial: uma variável desse tipo é um ponteiro genérico, ou seja, um ponteiro para qualquer tipo.

Qualquer ponteiro pode ser convertido implicitamente para void*, e vice-versa:

int *p;
void *vp;
int x;

p = &x;
vp = p; /* OK: conversão implícita de int* para void* */
vp = &x; /* OK, pelo mesmo motivo (a expressão &x tem tipo int*) */
p = vp; /* OK: conversão implícita de void* para int* */

É importante notar que perdemos informação ao converter um ponteiro T* (em que T é um tipo qualquer) para void*: quando temos T*, sabemos o endereço em que a variável começa e sabemos o tamanho do tipo T (ou seja, sizeof(T)). Ao converter esse ponteiro para void*, perdemos a informação sobre o tipo original T, logo não sabemos mais qual o tamanho. Por essa razão, não podemos dereferenciar, nem incrementar ou decrementar um ponteiro void*:

void *vp;
int *p;
int x;

vp = &x;
p = &x;

*p = 5; /* OK: copia sizeof(int) bytes a partir do endereço em p */
*vp = 5; /* ERRO! Não sabemos quantos bytes temos que copiar */

p += 2; /* OK: aumenta o valor do endereço de p em 2 * sizeof(int) */
vp += 2; /* ERRO! Não sabemos em qual valor devemos aumentar o endereço */

(Nota: alguns compiladores, como o gcc, permitem fazer aritmética com ponteiros void*, mas isso é uma extensão não-portável.)

Ponteiros void* são usados sempre que se quer uma variável de tipo genérico. Por exemplo, uma implementação genérica de lista duplamente ligada em C poderia ser simplesmente:

struct lista {
  struct lista *next;
  struct lista *prev;
  void *data;
};

As funções malloc e calloc têm, ambas, tipo de retorno void*. Normalmente, usamos essas funções para alocar espaço suficiente para N variáveis de algum tipo, usando sizeof:

struct my_struct *foo = malloc(100 * sizeof(struct my_struct));

Porém, nada nos impede de fazer:

char *mem = malloc(40); /* aloca 40 bytes */

Ou seja, a função malloc não tem - e não precisa ter - nenhuma informação sobre o tipo que pretendemos alocar: só importa que a função receba o número total de bytes a ser alocado, e devolva o endereço do começo da região com esse tamanho. Então, como nenhuma informação sobre o tipo é usada, é natural que o tipo de retorno seja void*.
Da mesma forma, a função free recebe um void*: novamente, não importa o tipo do ponteiro que foi alocado, apenas seu endereço.

Nota: dissemos que void* é um tipo especial porque, em C,

T *p;

Declara p como um ponteiro para T. Seguindo esse padrão,

void *p;

Deveria declarar um ponteiro para o tipo void, o qual não tem nenhum valor - ou seja, um ponteiro para nada. Mas um ponteiro como esse não teria nenhuma utilidade! Logo, decidiu-se usar void* como ponteiro genérico em vez disso.

Ponteiro para função

Funções também ocupam um lugar na memória, e por isso é possível ter ponteiros para funções. Por exemplo:


void foo1(int x, int y)
{
  printf("foo1: x = %d, y = %d\n", x, y);
}

void foo2(int x, int y)
{
  printf("foo2: x = %d, y = %d\n", x, y);
}

int main(void)
{
  /* declara um ponteiro para função que recebe dois ints e não retorna nada */
  void (*fptr)(int, int);
  
  fptr = foo1;
  fptr(1, 2);
  fptr = foo2;
  fptr(3, 4);

  return 0;
}

Imprime:

foo1: x = 1, y = 2
foo2: x = 3, y = 4

Podemos notar, no exemplo acima, que

  1. o protótipo da função faz parte do tipo do ponteiro para função; e
  2. a chamada a um ponteiro de função tem a mesma sintaxe que uma chamada de função normal.

Na verdade, a mesma relação entre arrays e ponteiros (ambos guardam o endereço de seu primeiro elemento) existe entre uma função "normal" e um ponteiro de função:

void foo(int x, int y)
{
  printf("foo: x = %d y = %d\n", x, y);
}

int main(void)
{
  void (*fptr)(int, int);
  
  fptr = foo;
  printf("foo = %p, fptr = %p\n", foo, fptr);
}

Imprime o mesmo endereço para foo e para fptr.

Callbacks

Um callback é um ponteiro para função que é passado para outra função, a fim de ser chamado numa ocasião específica. Callbacks são muito usados em programação orientada a eventos.
Frequentemente, a função que recebe um callback pertence a uma biblioteca, mas o callback em si é escrito pelo usuário. Para se possa ter um callback que lide com tipos definidos pelo usuário - e que não são conhecidos pela biblioteca -, os parâmetros do ponteiro de função devem ter tipo void*.
Um exemplo disso é a função qsort da biblioteca padrão, que implementa o quicksort. Seu protótipo é:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

Note que a função recebe um ponteiro de função compar, que por sua vez recebe dois ponteiros const void*. A página de manual de qsort explica que

O conteúdo da array é ordenado em ordem ascendente, de acordo com a função de comparação apontada por compar, que é chamada com dois argumentos que apontam para os objetos sendo comparados. [...]
A função de comparação deve retornar um inteiro menor que, igual a, ou maior que zero se o primeiro argumento é considerado, respectivamente, menor que, igual a ou maior que o segundo.

Então compar é o nosso callback, e como essa função recebe parâmetros de tipo const void*, podemos usá-la com tipos que nós definimos. Graças a isso, a função qsort pode ser usada com qualquer tipo definido pelo usuário, sem ter que se preocupar com sua lógica de comparação.

Um exemplo de uso dessa função seria:


/* guarda uma array de inteiros e o número de elementos que ela tem */
struct vec {
        int *a;
        size_t elem_num;
};

/* Queremos comparar dois vetores da seguinte forma:
 * v1 é considerado maior que v2 se a soma das entradas de v1
 * for maior que a soma das entradas de v2.
 */

/* NOTA: só chame esta função com ponteiros para struct vec! */
int vec_cmp(const void *p1, const void *p2)
{
  const struct vec *v1 = p1;
  const struct vec *v2 = p2;

  int i, sum1 = 0, sum2 = 0;

  for(i = 0; i < v1->elem_num; i++) {
    sum1 += v1->a[i];
  }
  for(i = 0; i < v2->elem_num; i++) {
    sum2 += v2->a[i];
  }
  /* este valor é maior, igual ou menor que zero
   * se sum1 é menor, igual ou menor que sum2, respetivamente
   */
  return sum1 - sum2;
}

int main(void)
{
  struct vec *v;
  size_t num_elem;
  /* leia v e num_vec de algum lugar (e.g. de um arquivo) 
   * de tal modo que v tenha num_elem elementos 
   */

  qsort(v, num_elem, sizeof(struct vec), vec_cmp);

  /* faca algo com os vetores em ordem */

  /* libere a memória alocada */
  return 0;
}

Orientação a objetos

É possível emular em C - com certas limitações - features presentes em linguagens de programação orientadas a objetos.
Usaremos structs para emular o funcionamento de uma classe.

Fábrica (Factory) / Construtor

Podemos emular uma fábrica como uma função que retorna um ponteiro alocado dinamicamente para a nossa struct.

Por exemplo, poderíamos ter, em um header vec3.h:

/* um vetor tridimensional (R3). */
struct vec3 {
  double x;
  double y;
  double z;
};

/* Nota ao usuário: este método deve ter seu valor de retorno dealocado
 * manualmente com free().
 */
struct vec3 *cria_vec3(double x, double y, double z); /* fábrica */

E, no arquivo de implementação vec3.c:

struct vec3 *cria_vec3(double x, double y, double z)
{
  struct vec3 *v = malloc(sizeof(struct vec3));
  if(v == NULL) {
    return NULL;
  }
  
  v->x = x;
  v->y = y;
  v->z = z;

  return v;
}

Podemos também criar algo semelhante a um construtor - uma função que inicializa os campos de uma struct recém-criada, e não retorna nada:

void init_vec3(double x, double y, double z)
{
  v->x = x;
  v->y = y;
  v->z = z;
}

Nesse caso, a responsabilidade de alocar a memória fica com o código do usuário. Além disso, o usuário deve se lembrar de chamar essa função para cada variável desse tipo, após ser criada.

Método estático

Um método estático é um método que pertence à classe, não a um objeto específico, e por isso não precisa acessar seu ponteiro this.

No exemplo anterior, poderíamos adicionar a vec3.h o protótipo:

/*
 * este método chama cria_vec3() e por isso seu valor de retorno também deve ser
 * manualmente dealocado.
 */
struct vec3 *soma_vec3(struct vec3 *v1, struct vec3 *v2); /* método estático! */

E na implementação, em vec3.c, teríamos

struct vec3 *soma_vec3(struct vec3 *v1, struct vec3 *v2); 
{
  return cria_vec3(v1->x + v2->x, v1->y + v2->y, v1->z + v2->z);
}

Declarações estranhas: a Regra da Espiral

Declarações em C podem ser crípticas, especialmente quando envolvem ponteiros de função. Considere:

const int (*const foo)[64](int, int);

Isso declara a variável foo - mas qual o seu tipo? A resposta curta é recorrer a este site. A resposta longa é usar a Regra da Espiral, que imita como o compilador analisará a expressão.

Partindo da variável cujo tipo queremos descobrir, percorremos a expressão em espirais de dentro para fora, em sentido horário:

const int (*const foo)[64](int, int);
                  +++^

(Os símbolos + indicam a parte da expressão que já analisamos.)

Encontramos um ), logo estamos dentro de uma sub-expressão com parênteses, e devemos voltar até o respectivo (:

((ter algum texto aqui é necessário para preservar o whitespace no começo da linha abaixo))

          v-------+++
const int (*const foo)[64](int, int);
                  +++^

Então foo é um ponteiro constante, mas para o que? Percorremos a expressão com uma nova espiral, partindo de toda a sub-expressão que já analisamos:

const int (*const foo)[64](int, int);
          ++++++++++++^

Encontramos um [, então devemos ir adiante até o respectivo ]:

const int (*const foo)[64](int, int);
          ++++++++++++^--^

Então foo é um ponteiro constante para uma array de 64 elementos, mas de que tipo? Outra vez, uma nova espiral:

const int (*const foo)[64](int, int);
          ++++++++++++++++^

Encontramos um (, então devemos ir adiante até o respectivo ):

const int (*const foo)[64](int, int);
          ++++++++++++++++^--------^

Então foo é um ponteiro constante para uma array de 64 ponteiros de função que recebem dois ints, mas qual o tipo de retorno?

const int (*const foo)[64](int, int);
          ++++++++++++++++++++++++++^

Achamos o fim da expressão (;), então voltamos para o começo:

vvvvvvvvv-++++++++++++++++++++++++++
const int (*const foo)[64](int, int);
          ++++++++++++++++++++++++++^

A expressão inteira foi analisada.
Portanto, foo é um ponteiro constante para uma array de 64 ponteiros de função que recebem dois ints e retornam um const int.

Tipos opacos

structs e unions podem ser declarados sem ser definidos:

struct foo;

Um tipo como esse - declarado mas não definido - é chamado de tipo opaco. A príncipio, o compilador não sabe o tamanho nem os campos desse tipo, o que gera algumas restrições em seu uso:

struct foo;

/* OK: podemos definir ponteiro para tipo opaco,
 * porque todo ponteiro tem o mesmo tamanho
 */
struct foo *ptr;
struct foo var; /* ERRO! Não podemos instanciar tipo opaco */

int main(void)
{
  ptr->stuff = 1; /* ERRO! Não podemos acessar nenhum membro de um tipo opaco */
  *ptr; /* ERRO! Não podemos de-referenciar ponteiro para tipo opaco */
  size_t foo_size = sizeof(struct foo); /* ERRO! Tamanho desconhecido! */ 
  return 0;
}

Com tantas restrições, por que usar um tipo opaco? A resposta: esses tipos permitem obter um certo nível de encapsulamento de dados. Por exemplo, ao criar uma biblioteca, é possível colocar, em um header (.h) que será incluído pelo usuário, apenas a declaração do tipo opaco e protótipos de funções que lidam com ponteiros para esse tipo, enquanto a definição do tipo e das funções iria nos arquivos-fonte (.c). Com isso, os usuários dessa biblioteca podem manipular os dados do tipo opaco por meio das funções fornecidas, e não precisam se preocupar com os detalhes de implementação do tipo.

Nota: um tipo opaco só pode se usado quando tem uma definição completa em outro arquivo, que deve ser unido (pelo linker) com o primeiro.

Funções variádicas

Considere as seguintes chamadas a printf:

printf("foo\n"); /* OK */
printf("%d\n", 3); /* OK */
printf("%f %f %f\n", 1.2, 3.4, 5.6); /* OK */

Como é possível que uma mesma função seja chamada com número e tipos de argumentos diferentes?
Isso ocorre porque printf é uma função variádica. Uma função variádica é indicada pelo uso de reticências (...) em seu protótipo; no caso de printf,

int printf(const char *format, ...);

Todos os argumentos antes das reticências têm tipo determinado e são obrigatórios a toda chamada a essa função; a partir das reticências, entende-se que haverá um número arbitrário de argumentos (que pode ser zero), de qualquer tipo.
Logo, as seguintes chamadas estão incorretas:

printf(); /* ERRO! */
printf(3.1); /* ERRO! */
printf(1, 2, 3); /* ERRO! */

Porque em nenhuma delas o primeiro argumento de printf é do tipo const char*.

Funções variádicas devem, obrigatoriamente, ter ao menos o primeiro argumento com tipo especificado. Logo, a declaração a seguir não é válida:

void variadic(...); /* ERRO! Primeiro argumento não especificado */

O compilador não tem nenhuma informação sobre os argumentos à função depois das reticências, mas a função em si precisa saber quais argumentos foram passados a ela para que possa usá-los devidamente. No caso de printf, essa informação está embutida na string de formatação - por exemplo, se há um %d, entende-se que foi passado um inteiro; se há um %f, um float, e assim por diante. Porém, o compilador não checa se os argumentos passados a uma função variádica correspondem aos que a função espera, de modo que todas essas chamadas compilam (talvez com warnings):

printf("abc\n", 5); /* OK, o segundo argumento não será usado */
printf("%d\n"); /* undefined behaviour, possível falha de segurança */
printf("%f\n", "abcd"); /* tenta imprimir o endereço da string literal como um float */

Importante: chamadas do tipo printf("%d"); em que printf espera mais argumentos do que foram passados, constituem uma falha de segurança; por isso, não permita que a string de formatação passada para printf dependa do input do usuário, a menos que esse input seja sanitizado antes (por exemplo, susbtiuindo todos os % por %%).

Funções variádicas são implementadas usando-se uma variável do tipo va_list, definido no header stdarg.h. Essa variável deve ser inicializada pela macro va_start e liberada por va_end; entre as duas chamadas, podemos usar a macro va_arg para extrair um argumento com um dado tipo. Como exemplo, temos uma implementação simplificada de printf:

#include <stdio.h>
#include <string.h>
#include <stdarg.h>

/* versão simplificada de printf() que não retorna nada, e entende apenas
 * as formatações %s, %c, %d, %f e %%
 */
void my_printf(const char *fmt, ...)
{
  int in_format = 0;
  va_list args; /* nossa lista de argumentos */ 

  /* fmt é a última variável conhecida pela função, e deve ser passada a va_start
   * para que a macro saiba onde a lista de argumentos variádicos deve começar
   */
  va_start(args, fmt);

  for(; *fmt; fmt++) {
    if(in_format) {
      in_format = 0; /* todas as nossas formatações têm apenas 1 letra */

      switch(*fmt) {
        case '%':
          fputc('%', stdout);
        break;
        case 's':
        {
          /* va_arg extrai um argumento do tipo dado (no caso, char*) e o retorna,
           * de tal modo que a próxima chamada a va_arg retornará o próximo argumento
           * da lista, e assim por diante.
           * Note que o compilador não pode checar se o tipo do argumento passado
           * é compatível com o que a função espera.
           */
          char *s = va_arg(args, char*);
          fputs(s, stdout);
        }
        break;
        case 'c':
        {
          char c = (char) va_arg(args, int); /* TODO explicar por que extraimos o argumento como int */
          fputc(c, stdout);
        }
        break;
        case 'd':
        {
          int i = va_arg(args, int);
          /* converta i para string e imprima com fputs() */
        }
        break;
        case 'f':
        {
          double d = va_arg(args, double);
          /* converta d para string e imprima com fputs() */
        }
        break;
        default:
          /* formatação inválida, ignore. */
      }
    } else if(*fmt == '%') {
      in_format = 1;
    } else {
      in_format = 0;
      fputc(*fmt, stdout);
    }
  }
  va_end(args); /* terminamos de usar a varáivel */
}

O operador ,

O operador , avalia seus dois operandos, e tem como resultado o valor do último operando (da direita), descartando o valor do outro. Por exemplo:

int main(void)
{
  int w, x, y, z;
  z = (w = 1, x = 2, y = 3);
  printf("w = %d, x = %d, y = %d, z = %d\n", w, x, y, z);
  return 0;
}

Imprime

w = 1, x = 2, y = 3, z = 3

A expressão é avaliada como: z = ((w = 1, x = 2), y = 3) -> z = ((1, 2), 3) -> z = (2, 3) -> z = 3 -> 3.

Um uso comum desse operador é em laços for que têm mais de um índice:

/* suponha que temos uma array quadrada arr de tamanho arrsize */
double sum_diag = 0;
int i, j;

/* note o uso do operador ',' para manipular os dois indices ao mesmo tempo */
for(i = 0, j = 0; i < arrsize && j < arrsize; i++, j++) {
  sum_diag += arr[i][j];
}
printf("soma das entradas da diagonal principal: %g\n", sum_diag);

Por ser conta-intuitivo, esse operador pode ser usado para obfuscação de código.

Inline assembly

TODO