Tipos definidos pelo usuário

struct

Uma struct é um tipo composto por vários outros. Por exemplo:

struct cliente {
  char *nome;
  int idade;
  char *endereco;
  /* ... */
};

Define o tipo struct cliente como uma composição dos tipos entre { }, com os nomes acima. Cada sub-tipo da struct é chamado de campo da struct.

Podemos declarar uma variável desse tipo simplesmente como:

struct cliente c1;

Note que o nome do tipo é struct cliente e o da variável é c1.

Podemos também declarar variáveis de um tipo struct juntamente com a declaração do tipo:

struct ponto {
  double x;
  double y;
  double z;
} p1, p2, p3;

Declara as variáveis p1, p2 e p3 do tipo struct ponto. Depois disso, ainda podemos declarar mais variáveis desse tipo normalmente:

struct ponto p4, p5;

Se declararmos variáveis juntamente com a struct tipo de declaração, é possível omitir o nome da struct:

struct {
  int x;
  int y;
} vetor1, vetor2;

Nesse caso temos uma struct anônima. Como esse tipo não tem nome, não é possível declarar nenhuma outra variável desse tipo além de vetor1 e vetor2.

Podemos acessar cada um dos campos de um tipo struct com o operador .

p1.x = 1;
p1.y = 2;
p1.z = -3;
printf("x = %f y = %f z = %f\n", p1.x, p1.y, p1.z);

Como em outras variáveis, é possível atribuir um valor a structs durante sua declaração, de um modo similar à atribuição de arrays:

/* atribui o primeiro campo da struct ao valor 1, o segundo ao valor 2 e o terceiro, a -3 */
struct ponto p1 = {1, 2, -3};

Como com qualquer outro tipo, é possível declarar ponteiros para structs, e é possível alocar memória para esses ponteiros dinamicamente:

struct ponto *ptr = malloc(sizeof(struct ponto));
/* ... */
free(ptr);
ptr = NULL;

Nesse caso, poderíamos acessar o campo x desse ponto usando (*ptr).x (os parênteses são necessários por causa da precedência dos operadores). Porém, por conveniência, criou-se o operador ->, de modo que podemos escrever ptr->x, o que é equivalente, por definição, à construção anterior.
Note que podemos alocar memória para vários pontos de maneira semelhante:

struct ponto *ptr = malloc(10 * sizeof(struct ponto)); /* 10 pontos */
/* ... */
free(ptr);
ptr = NULL;

Nesse caso, ainda podemos acessar o campo x do primeiro ponto com ptr->x, mas para acessar esse mesmo campo de outro ponto (digamos, o quarto) teríamos que fazer (*(ptr+3)).x, o que equivale a ptr[3].x.

Funções podem receber e/ou retornar structs. Contudo, nos dois casos, gera-se uma cópia da struct, o que pode demandar muita memória se a struct tiver muitos campos. Por isso, costuma-se usar ponteiros para struct nesse caso. (Mas lembre-se que não podemos retornar ponteiros para variáveis locais!)

Uma struct pode conter um ponteiro para seu próprio tipo. Na verdade, essa é uma implementação comum de listas ligadas:

struct lista {
  int val; /* o valor desse elemento */
  struct lista *prox; /* ponteiro para o próximo elemento */
};

Mas como esse tipo pode declarar um ponteiro para si mesmo se a definição de struct lista ainda não terminou quando prox é declarado? A resposta tem a ver com tipos opacos.

Nota: declarações de structs são frequentemente acompanhadas de typedef, para que se possa omitir o nome struct ao usar o tipo. Por exemplo:

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

Nesse caso, Ponto se torna um outro nome para o tipo struct ponto. Porém, usar typedef com structs é considerado má prática por vários guias de estilo, entre eles o do Linux, pois com uma quantidade suficientemente grande de tipos, pode-se esquecer que aquele tipo é uma struct e tratá-lo como um tipo primitivo.

Nota: embora C seja uma linguagem imperativa, structs podem ser usadas juntamente com ponteiros de funções para emular a orientação a objetos, de maneira limitada.

union

Uma variável de um tipo union pode ter apenas um entre os tipos especficados em um dado momento. Esses tipos são definidos da mesma forma

union number {
  int i;
  float f;
};

Variáveis de um tipo union podem ser declaradas das mesmas maneiras que variáveis de tipo struct.
Ao contrário das structs, numa union apenas um dos campos é válido em um dado instante. Por exemplo:

union number num;

num.i = 5;
printf("%d\n", num.i); /* OK num.i é válido agora */
printf("%f\n", num.f); /* comportamento indefindo! */

num.f = 3.0;
printf("%f\n", num.f); /* OK, num.f é válido agora */
printf("%d\n", num.i); /* comportamento indefindo! */

Ou seja, se atribuirmos a um dos campos da union, qualquer tentativa de ler de qualquer outro campo gera um valor indefinido, mas a qualquer momento podemos atribuir um valor a outro campo.
Para saber qual campo da union é válido, costuma-se usar colocar a union dentro de uma struct, juntamente com uma enum que guarda o tipo:

struct numero {
  union {
    int i;
    float f;
  } valor; /* note que a union é anônima, valor é a variável, que é um campo da struct */
  enum {
    NUMERO_FLOAT,
    NUMERO_INT
  } tipo; /* enum também é anônima */
};

Por exemplo:

struct numero num;
num.valor.i = 2;
num.tipo = NUMERO_INT;
/* ... */

Nota: o tamanho de um tipo union é geralmente o tamanho do maior de seus campos.

enum

Um enum é um tipo que pode conter apenas alguns valores especificados. Por exmeplo:

enum cor_semaforo {
  VERMELHO,
  AMARELO,
  VERDE
};

Qualquer variável do tipo enum cor_semaforo pode conter apenas um desses três valores.
Cada valor da enum é internamente representado por um inteiro, e é possível atribuir valores explícitos a eles:

enum cor_semaforo {
  VERMELHO = 0,
  AMARELO = 1,
  VERDE = 2
};

Isso é um tanto parecido com

const int VERMELHO = 0;
const int AMARELO = 1;
const int VERDE = 2;
/* e então usamos int em vez de `enum cor_semaforo` */

Porém, usando-se a enum, há a garantia de que a variável só pode valer um desses três valores; o mesmo não ocorre quando se usa int.