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-essentialinstala o gcc e omake. - 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 --installinstala o compilador (ogccdo macOS é, na verdade, o clang, que aceita as mesmas flags).
Para conferir a instalação:
gcc --versionCompilando 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 programaA 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:
| Flag | O que faz |
|---|---|
-o nome | dá nome ao arquivo gerado (sem ela, o executável se chama a.out) |
-std=c11 | usa o padrão C11 da linguagem |
-O0 | desliga as otimizações |
-g | guarda informações de depuração (nomes de arquivos, linhas, variáveis) |
-fno-omit-frame-pointer | deixa a pilha de chamadas mais fácil de reconstruir nos relatórios de erro |
-Wall, -Wextra | ligam a maior parte dos avisos (warnings) |
-Wshadow | avisa quando uma variável esconde outra de mesmo nome |
-Werror | transforma todo aviso em erro |
-Wno-... | desliga avisos específicos que geram muito ruído |
-fsanitize=address,undefined | inclui verificações de memória e de comportamento indefinido durante a execução |
-lm | liga 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.
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 modeCom -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.
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:
Os três avisos vêm de -Wall e os três apontam erros reais:
if (n = 0)atribui 0 anem vez de comparar: a condição é sempre falsa enperde o valor lido (suggest parentheses around assignment);maiortermina semreturnquandoa <= b, e o valor devolvido é lixo (control reaches end of non-void function);%dcom umdoubleimprime lixo (format '%d' expects argument of type 'int').
-Wextra liga avisos adicionais:
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):
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 errorsOs -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]| Flag | Desliga o aviso de | Por que desligar |
|---|---|---|
-Wno-sign-compare | comparação entre int e um tipo sem sinal | i < strlen(s) é muito comum e correto para tamanhos razoáveis |
-Wno-unused-parameter | parâmetro que a função não usa | funções de um TAD ainda incompletas, ou que não precisam de todos os parâmetros |
-Wno-unused-variable | variável declarada e não usada | código em construção não deixa de compilar por isso |
-Wno-unused-result | valor de retorno ignorado de funções como scanf | em 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 statusO #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
- Using the GNU Compiler Collection: Warning Options. Documentação do gcc.
- Program Instrumentation Options (sanitizers). Documentação do gcc.
- C Dialect Options (
-std). Documentação do gcc.