Estrutura de Dados I

Compilando em C

Como instalar o compilador, compilar e executar um programa, e o que faz cada flag de compilação usada na disciplina.

Os programas desta disciplina são escritos em C e compilados com o gcc. Esta página mostra como compilar e executar um programa de um arquivo só e explica, uma a uma, as opções (flags) de compilação usadas na disciplina. Elas valem desde o primeiro programa: avisam sobre erros comuns e deixam as mensagens de erro muito mais úteis. Quando um programa passar a ter vários arquivos, a página Compilação separada e Make mostra como compilar cada parte e automatizar o processo.

O compilador

  • Linux: o gcc costuma vir instalado. Se não vier, no Ubuntu e no Debian, sudo apt install build-essential instala o gcc e o make.
  • Windows: a forma mais simples de ter o mesmo ambiente do laboratório é o WSL, que roda um Ubuntu dentro do Windows. Outra opção é o MinGW-w64, instalado pelo MSYS2. O AddressSanitizer não funciona no MinGW, e por isso o WSL é o recomendado.
  • macOS: xcode-select --install instala o compilador (o gcc do macOS é, na verdade, o clang, que aceita as mesmas flags).

Para conferir a instalação:

gcc --version

Compilando e executando

Com o programa salvo em ola.c:

gcc ola.c -o ola     # compila e gera o executável ola
./ola                # executa (no Windows sem WSL: ola.exe)

A flag -o escolhe o nome do executável. Sem ela, gcc ola.c gera um arquivo chamado a.out (ou a.exe, no Windows).

As flags da disciplina

O comando completo usado na disciplina é longo:

gcc -std=c11 -O0 -g -fno-omit-frame-pointer \
    -Wall -Wextra -Werror -Wshadow \
    -Wno-sign-compare -Wno-unused-parameter -Wno-unused-variable -Wno-unused-result \
    -fsanitize=address,undefined \
    programa.c -lm -o programa

A barra invertida no fim de cada linha continua o comando na linha seguinte. Também dá para escrever tudo em uma linha só.

Cada opção tem um motivo. Em resumo:

FlagO que faz
-o nomedá nome ao arquivo gerado (sem ela, o executável se chama a.out)
-std=c11usa o padrão C11 da linguagem
-O0desliga as otimizações
-gguarda informações de depuração (nomes de arquivos, linhas, variáveis)
-fno-omit-frame-pointerdeixa a pilha de chamadas mais fácil de reconstruir nos relatórios de erro
-Wall, -Wextraligam a maior parte dos avisos (warnings)
-Wshadowavisa quando uma variável esconde outra de mesmo nome
-Werrortransforma todo aviso em erro
-Wno-...desliga avisos específicos que geram muito ruído
-fsanitize=address,undefinedinclui verificações de memória e de comportamento indefinido durante a execução
-lmliga a biblioteca matemática (sqrt, pow, sin...)

Os exemplos abaixo mostram cada uma em ação. As mensagens são as do gcc em inglês. Em um sistema em português, várias aparecem traduzidas.

E aqui na página?

Os programas executados nesta página já são compilados com quase todas essas flags: -std=c11 -O0 -g -fno-omit-frame-pointer -Wall -Wextra -Wshadow, os mesmos -Wno-... e -fsanitize=address,undefined, mais -lm. A única diferença é que não usamos -Werror, para que você veja os avisos e ainda assim consiga executar o programa.

-std=c11

A linguagem C tem várias versões (padrões): C89, C99, C11, C17, C23. Cada uma acrescenta recursos, e cada versão do gcc usa um padrão diferente por omissão, além de extensões próprias. Fixar o padrão com -std=c11 faz o mesmo código ser aceito (ou recusado) do mesmo jeito em qualquer computador. Um exemplo de diferença: declarar a variável dentro do for só é permitido a partir do C99.

for (int i = 0; i < 3; i++)
    printf("%d\n", i);
$ gcc -std=c89 laco.c
laco.c:4:5: error: 'for' loop initial declarations are only allowed in C99 or C11 mode

Com -std=c11, o mesmo código compila normalmente.

E o C23?

O padrão mais recente é o C23 (publicado como ISO/IEC 9899:2024). Ele traz várias novidades, algumas bem convenientes:

int main(void) {
    bool achou = false;               /* bool, true e false sem <stdbool.h> */
    int *p = nullptr;                 /* ponteiro nulo com tipo próprio */
    constexpr int LIMITE = 1'000'000; /* constante e separador de dígitos */
    int mascara = 0b1010;             /* literal binário */
    typeof(mascara) copia = mascara;  /* o tipo de outra expressão */
    static_assert(sizeof(int) >= 4);  /* verificação em tempo de compilação */
    return 0;
}

Ele também muda o significado de código antigo. int f(); passa a declarar uma função sem parâmetros (como int f(void)), definições de função no estilo antigo deixam de existir e, como bool, true e false viraram palavras reservadas, código como typedef int bool; ou #define true 1, comum em livros e materiais mais antigos, não compila mais.

Mesmo assim, a disciplina usa C11, por um motivo prático: todo ambiente em que o seu código é compilado precisa aceitar o padrão escolhido. O suporte completo a -std=c23 só chegou no gcc 14, e muitas instalações ainda usam versões anteriores: o gcc do Ubuntu 24.04 (usado nos laboratórios) é o 13, o servidor que executa os programas desta página usa uma versão mais antiga, e compiladores instalados no Windows costumam estar desatualizados. Com C11, o mesmo código compila igual no laboratório, em casa e aqui. Além disso, quase todo o material de C disponível segue C99 ou C11, e nada do que a disciplina precisa depende das novidades do C23.

Sempre declare o padrão

A partir do gcc 15, o padrão usado quando nenhum -std é informado passou a ser o gnu23 (C23 com extensões do gcc). Quem compila sem -std está usando um padrão diferente em cada versão do compilador, sem perceber. Por isso, declare sempre o padrão: com -std=c11, o resultado não depende da versão instalada.

-O0

O gcc pode otimizar o código: eliminar contas repetidas, reorganizar laços, guardar variáveis em registradores em vez da memória. -O0 desliga essas otimizações (-O2 é o nível mais usado para gerar programas rápidos). Durante o desenvolvimento, -O0 é melhor: o executável segue o código linha a linha, o que deixa os relatórios dos sanitizers e do depurador fiéis ao que foi escrito. O preço é um programa mais lento, o que raramente importa nos exercícios.

-g e -fno-omit-frame-pointer

-g guarda no executável a ligação entre o código de máquina e o código fonte: nomes de arquivos, números de linha, nomes de variáveis. Ela não muda o comportamento do programa, mas muda muito a qualidade das mensagens de erro. Este programa escreve uma posição além do fim do vetor:

int *v = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++)      /* deveria ser i < 5 */
    v[i] = i;

Sem -g, o AddressSanitizer diz apenas que o erro aconteceu "em algum lugar de main":

==516==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000054
WRITE of size 4 at 0x503000000054 thread T0
    #0 0x557c0359a237 in main (/tmp/fl/g+0x1237)

Com -g, ele aponta o arquivo e a linha:

==523==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000054
WRITE of size 4 at 0x503000000054 thread T0
    #0 0x5614cb75b237 in main /tmp/fl/g.c:6

-fno-omit-frame-pointer complementa a -g: mantém em cada chamada de função a informação usada para reconstruir a pilha de chamadas, o que deixa a lista #0, #1, #2... dos relatórios completa e confiável.

Avisos: -Wall e -Wextra

Um aviso (warning) não impede a compilação, mas quase sempre aponta um erro de lógica. Sem nenhuma flag, o gcc mostra poucos avisos. Execute o programa abaixo e leia os avisos antes da saída:

avisos.cC

Os três avisos vêm de -Wall e os três apontam erros reais:

  • if (n = 0) atribui 0 a n em vez de comparar: a condição é sempre falsa e n perde o valor lido (suggest parentheses around assignment);
  • maior termina sem return quando a <= b, e o valor devolvido é lixo (control reaches end of non-void function);
  • %d com um double imprime lixo (format '%d' expects argument of type 'int').

-Wextra liga avisos adicionais:

avisos2.cC

O ; logo depois do if (n > 5) é o corpo (vazio) do if, e o printf da linha seguinte executa sempre, apesar da indentação (suggest braces around empty body). E total < 0 nunca é verdade, porque total é unsigned (comparison of unsigned expression in '< 0' is always false).

-Wshadow

Avisa quando uma variável é declarada com o mesmo nome de outra que já existia em um escopo mais externo. O resultado compila, mas a variável de fora fica escondida (shadowed):

sombra.cC

O int a mais na linha 7 cria uma nova soma, local ao laço. A soma de fora nunca muda, e o total impresso é 0. Esse aviso não faz parte de -Wall nem de -Wextra, por isso é ligado à parte.

-Werror

Transforma todo aviso em erro: o programa só compila quando não há nenhum aviso. Na disciplina, isso obriga a ler e resolver os avisos, em vez de ignorá-los.

$ gcc -Wall -Werror avisos.c
avisos.c:12:9: error: suggest parentheses around assignment used as truth value [-Werror=parentheses]
...
cc1: all warnings being treated as errors

Os -Wno-...

Com -Werror, alguns avisos de -Wall e -Wextra atrapalham mais do que ajudam, principalmente em código que ainda está sendo escrito. Cada -Wno-xxx desliga um aviso específico:

int conta(const char *s, char c) {          /* c ainda não é usado */
    int total = 0, i;                       /* i não é usado */
    for (int j = 0; j < strlen(s); j++)     /* int comparado com size_t */
        total++;
    return total;
}
warning: comparison of integer expressions of different signedness: 'int' and 'size_t' [-Wsign-compare]
warning: unused variable 'i' [-Wunused-variable]
warning: unused parameter 'c' [-Wunused-parameter]
FlagDesliga o aviso dePor que desligar
-Wno-sign-comparecomparação entre int e um tipo sem sinali < strlen(s) é muito comum e correto para tamanhos razoáveis
-Wno-unused-parameterparâmetro que a função não usafunções de um TAD ainda incompletas, ou que não precisam de todos os parâmetros
-Wno-unused-variablevariável declarada e não usadacódigo em construção não deixa de compilar por isso
-Wno-unused-resultvalor de retorno ignorado de funções como scanfem algumas instalações, cada scanf sem teste gera um aviso

-fsanitize=address,undefined

Inclui no programa verificações feitas durante a execução: acesso fora dos limites de um vetor, uso de memória já liberada, vazamentos (o AddressSanitizer) e comportamentos indefinidos como estouro de inteiro com sinal ou índice fora do vetor (o UndefinedBehaviorSanitizer). Os detalhes e os tipos de erro estão em Valgrind e sanitizers.

Quando o programa é compilado em várias etapas (um arquivo de cada vez e a ligação no fim), essa flag precisa aparecer em todas elas. A página Compilação separada e Make explica por quê.

-lm

As funções de math.h (sqrt, pow, sin, floor...) ficam na biblioteca matemática, que não é ligada automaticamente. -l seguida do nome (m) pede para incluí-la. Sem ela, a compilação falha na última etapa:

$ gcc raiz.c -o raiz
/usr/bin/ld: /tmp/ccQ2vK1a.o: in function `main':
raiz.c:(.text+0x2e): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status

O #include <math.h> só traz os protótipos das funções. O código de sqrt está na biblioteca, e quem junta as duas coisas é o ligador (ld). As etapas da compilação e o papel do ligador estão em Compilação separada e Make. A -lm deve vir depois do arquivo .c no comando.

Referências

Nesta página