C avançado para sistemas

Compilação, bibliotecas, apontadores, tempo de vida, strings e argumentos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Um programa que soma vetores parte-se em cabeçalho, implementação e cliente. Cada ficheiro compila-se em separado e a ligação junta objetos e bibliotecas com -lvetor -lm. A ordem nessa linha importa e um erro de compilação tem uma fase diferente de um erro de ligação.

Do ficheiro fonte ao executável

O comando gcc coordena várias ferramentas. Pré-processamento expande includes e macros; compilação analisa sintaxe e tipos e gera assembly; montagem produz objetos; ligação resolve referências entre objetos e bibliotecas.

gcc -E programa.c -o programa.i
gcc -S programa.c -o programa.s
gcc -c programa.c -o programa.o
gcc programa.o -o programa

-c não liga. Se o programa usa uma função declarada mas sem definição disponível, a compilação do objeto pode passar e a ligação falhar com undefined reference. Um cabeçalho descreve a interface; incluir um .h não fornece automaticamente a implementação.

No terminal Linux, um ponto de partida é gcc -std=c17 -Wall -Wextra -Wpedantic -g programa.c -o programa. -g acrescenta informação para depuração, ou seja não corrige o programa. -Wall não ativa literalmente todos os avisos. Lê-os e corrige a causa. No debugger, põe um breakpoint em main e usa next e step para acompanhar linhas e entrar em funções.

Diagnosticar a fase do erro

Uma chamada a sqrt sem uma declaração visível pode produzir implicit declaration of function. A primeira correção é incluir <math.h> e confirmar que a interface fica visível. Se a compilação do objeto passa mas a ligação diz undefined reference to sqrt, falta a definição na ligação: num ambiente Linux que separa a biblioteca matemática, põe -lm depois do objeto que a usa.

São problemas diferentes. Acrescentar -lm não ensina ao compilador os tipos da função; incluir o cabeçalho não fornece o seu código binário. Um cabeçalho próprio também tem de ser incluído antes da chamada ou fornecer uma declaração equivalente.

Programa e processo

O executável é um ficheiro com código e dados iniciais. Um processo é uma execução desse programa, com estado próprio. O mesmo executável pode dar origem a vários processos com valores diferentes nas suas variáveis.

Num modelo típico, .text contém código, .data dados estáticos inicializados e .bss dados estáticos inicialmente a zero. A pilha contém quadros de chamadas e muitos objetos automáticos. O heap fornece blocos com duração controlada pelo programa. Compiladores podem manter variáveis em registos; o modelo ajuda a raciocinar, mas não fixa a posição física de cada variável.

Apontadores e passagem por valor

&x obtém o endereço de x; *p acede ao objeto apontado por p.

int x = 5, y = 3;
int *p = &x;
int *q = p;
*p = 7;
p = &y;
*q = 9;

No fim, x == 9 e y == 3. Alterar p não altera q: cada apontador guarda uma cópia do endereço. q continua a apontar para x.

C passa todos os argumentos por valor. swap(int a, int b) troca cópias. swap(int *a, int *b) também recebe cópias, mas essas cópias apontam para os objetos do chamador e permitem alterá-los com *a e *b.

Num array, p + 1 avança um elemento, não necessariamente um byte, porque para um int * o avanço é sizeof(int) bytes. a[i] equivale a *(a + i) quando o índice é válido.

Só podes desreferenciar dentro do array. O apontador imediatamente a seguir pode servir como limite, mas não para ler um elemento.

sizeof array / sizeof array[0] conta elementos quando array ainda é um array nesse âmbito. Num parâmetro int array[], o tipo ajusta-se para apontador e essa conta deixa de medir o array, por isso passa também o comprimento.

void * guarda um endereço de objeto sem fixar o seu tipo. É o retorno de malloc e o tipo do argumento de uma thread. Em C, podes atribuí-lo a um apontador para o tipo concreto antes de aceder ao objeto, sem cast. Não podes desreferenciar void * nem fazer a sua aritmética em C17. void f(void) declara uma função sem argumentos e sem retorno; em C17, f() numa declaração não especifica os argumentos.

Tempo de vida e propriedade

Não devolvas &local de uma função, porque quando ela retorna a variável automática deixa de existir. O endereço pode continuar a ter bits aparentemente plausíveis, mas usá-lo tem comportamento indefinido. Não existe uma saída obrigatória, nem sequer a garantia de um segmentation fault.

Um bloco obtido com malloc continua a existir até free. A função que devolve esse bloco deve deixar claro quem o liberta. malloc não inicializa os bytes; calloc põe os bytes a zero. Testa NULL antes de escrever.

Reservar, copiar e libertar
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *original = malloc(sizeof *original);
    int *copia = malloc(sizeof *copia);
    if (original == NULL || copia == NULL) {
        free(original);
        free(copia);
        return 1;
    }
    *original = 4;
    *copia = *original;
    *copia = 9;
    printf("original=%d copia=%d\n", *original, *copia);
    free(copia);
    free(original);
    return 0;
}
Resultado

Executa o código para ver a saída e os gráficos.

O programa escreve original=4 copia=9. O segundo bloco tem uma cópia do valor, não do endereço. Depois de free, deixa de ser válido aceder ao bloco. Atribuir NULL ao apontador libertado pode ajudar a detetar usos locais, mas não corrige outros apontadores que ainda guardem o endereço.

Para redimensionares, usa um temporário, porque escrever diretamente valores = realloc(...) perderia o endereço antigo se a operação falhasse:

int *novo = realloc(valores, novo_tamanho * sizeof *valores);
if (novo == NULL) {
    /* valores continua válido, se o tamanho pedido não for zero */
} else {
    valores = novo;
}

Este é um excerto: confirma antes que novo_tamanho é positivo e que a multiplicação cabe em size_t. Quando tiver sucesso, o endereço antigo deixa de ser utilizável, mesmo que os bits não tenham mudado.

Strings e estruturas

Uma string C termina em \0. Para guardar "ola", precisas de quatro bytes. strlen conta bytes anteriores ao terminador; não mede a capacidade do buffer. sizeof de um apontador mede o apontador, não a string.

strcmp(a, b) devolve zero se forem iguais, negativo se a vier antes de b e positivo no caso contrário. Não exijas exatamente -1 ou 1. strcpy(dst, src) exige strlen(src)+1 bytes no destino. strcat(dst, src) exige strlen(dst)+strlen(src)+1 bytes, com dst já terminada. Em ambos, as regiões não se podem sobrepor e a reserva precisa de verificação de overflow antes de somar.

strtok modifica o buffer e mantém estado entre chamadas. Se precisares de conservar o original, faz uma cópia que possas alterar. O argumento de delimitadores é um conjunto de caracteres, não uma substring: " ," separa tanto em espaço como em vírgula. strtok ignora sequências de delimitadores; strsep também pode devolver tokens vazios, que o programa tem de conservar ou ignorar segundo o contrato. strsep é uma extensão comum de UNIX, não parte de C17. Ao usar tolower, converte primeiro para unsigned char se o valor vier de um char que pode ser negativo.

Uma struct reúne campos num objeto. A atribuição de estruturas copia os campos, incluindo apenas os endereços de campos apontador. Duas estruturas que copiem o mesmo apontador ainda podem partilhar o mesmo bloco. Para objetos pequenos, devolver uma estrutura por valor evita uma reserva dinâmica e simplifica a propriedade.

Argumentos e conversão

Em int main(int argc, char *argv[]), argc inclui o nome usado para invocar o programa. argv[argc] é NULL. Os argumentos são strings, mesmo quando escreves 42 na shell. Verifica argc antes de ler argv[1].

atoi é conveniente para um exercício com entradas garantidamente válidas, mas não permite distinguir bem zero de uma conversão inválida. Para validar um inteiro, usa strtol, com <stdlib.h>, <errno.h> e limites adequados:

errno = 0;
char *fim;
long n = strtol(argv[1], &fim, 10);
if (errno == ERANGE || fim == argv[1] || *fim != '\0') {
    /* rejeitar argumento */
}

fim == argv[1] significa que não foi convertido nenhum dígito; *fim != '\0' deteta texto que sobrou. Se precisas de int, verifica também INT_MIN <= n <= INT_MAX. Para um máximo de números que podem ser negativos, inicializa com o primeiro valor validado, não com zero.

Bibliotecas e APIs

Uma API diz que funções existem, que argumentos aceitam e o que devolvem. Uma implementação cumpre esse contrato. Por exemplo, uma biblioteca de vetores pode devolver struct vetor por valor ou devolver struct vetor * que exige libertação: são contratos diferentes, mesmo que representem o mesmo vetor.

Um exemplo mínimo completo tem três ficheiros. O cabeçalho vetor.h declara a função, a implementação vetor.c define-a e o cliente usar.c chama-a e imprime o resultado:

/* vetor.h */
#ifndef VETOR_H
#define VETOR_H
int soma(int a, int b);
#endif
/* vetor.c */
#include "vetor.h"
int soma(int a, int b) {
    return a + b;
}
/* usar.c */
#include <stdio.h>
#include "vetor.h"
int main(void) {
    int r = soma(2, 3);
    printf("2 + 3 = %d\n", r);
    return 0;
}
cc -std=c17 -Wall -Wextra -Wpedantic -c vetor.c
ar rcs libvetor.a vetor.o
cc -std=c17 -Wall -Wextra -Wpedantic usar.c -L. -lvetor -o usar
./usar

A saída observada é 2 + 3 = 5. O cliente inclui o cabeçalho para conhecer a declaração, mas só a ligação com a biblioteca fornece a definição. A biblioteca estática fornece código ao executável na ligação. -L. acrescenta um diretório de pesquisa e -lvetor procura uma biblioteca com esse nome. A ordem importa para arquivos estáticos: põe a biblioteca depois dos objetos que usam os seus símbolos. -lm fornece a biblioteca matemática em ambientes que a separam. Para inspecionar o conteúdo do arquivo, ar -t libvetor.a lista os objetos guardados.

Para uma biblioteca partilhada em Linux:

gcc -Wall -Wextra -fPIC -c vetor.c
gcc -shared vetor.o -o libvetor.so
gcc usar.c -L. -lvetor -o usar
LD_LIBRARY_PATH=. ./usar

Aqui distinguimos pesquisa na ligação de pesquisa durante a execução. -L. resolve a primeira; o carregador ainda precisa de encontrar a .so. A atribuição de LD_LIBRARY_PATH só vale para esse comando. Bibliotecas partilhadas podem permitir partilhar páginas de código e atualizar código sem reconstruir cada aplicação, mas exigem compatibilidade da interface binária.

Um apontador de função, como int (*f)(int), guarda uma função com esse tipo. f(3) chama-a. Serve para callbacks e tabelas de operações; antes de indexar uma tabela de funções, valida o índice.

Objetos compostos e listas

Numa biblioteca de números complexos, struct complexo { double real, imag; }; pode representar a+bia+bi. Somar (1,2) e (3,-1) dá (4,1); multiplicar dá (1×3−2×(−1),  1×(−1)+2×3)=(5,5)(1\times3-2\times(-1),\;1\times(-1)+2\times3)=(5,5). Devolver este objeto por valor não cria uma obrigação de free. Se a API devolvesse um apontador para um objeto reservado, precisaria de declarar essa obrigação. O mesmo cuidado aplica-se a vetores: distinguir um vetor matemático de dimensão fixa de um array dinâmico evita misturar contratos.

Numa lista ligada, cada nó guarda um valor e o endereço do seguinte. A cabeça é NULL na lista vazia. Inserir no início muda o apontador da cabeça, por isso uma função recebe o seu endereço, struct no **. Segue os estados ao inserir 5 e depois 2 numa lista vazia: a cabeça passa de NULL para o nó de 5, depois para o nó de 2, que aponta para o de 5. A função escreve através de *cabeca, por isso a alteração sobrevive ao retorno. Passar só a cabeça por valor mudaria uma cópia local e perderia a nova lista. Este excerto requer <stdlib.h> e uma cabeça inicialmente nula:

struct no {
    int valor;
    struct no *seguinte;
};

int inserir(struct no **cabeca, int valor) {
    struct no *novo = malloc(sizeof *novo);
    if (novo == NULL) return -1;
    novo->valor = valor;
    novo->seguinte = *cabeca;
    *cabeca = novo;
    return 0;
}

int retirar(struct no **cabeca, int *valor) {
    if (*cabeca == NULL) return -1;
    struct no *antigo = *cabeca;
    *valor = antigo->valor;
    *cabeca = antigo->seguinte;
    free(antigo);
    return 0;
}

Assumimos argumentos apontador válidos e uso por uma tarefa. Depois de inserir 5 e depois 2, a cadeia é 2 → 5 → NULL. Retirar devolve 2 e deixa a cabeça no nó de 5. Guardamos seguinte antes de libertar: ler antigo->seguinte depois de free seria usar um objeto que já não existe. Se a reserva falhar, a cabeça conserva o valor anterior.

A lista é dona dos seus nós. Quem a destrói percorre os nós, guarda o próximo e liberta o atual. Se cada nó contivesse uma string reservada, o contrato também precisaria de dizer se a lista é dona dessa string. Copiar apenas a cabeça cria dois apontadores para os mesmos nós, não duas listas independentes. Inserir no fim exige percorrer a lista ou manter uma cauda cuja consistência também tens de preservar.

Exercícios

Cabeça, remoção e propriedade dos nós

Uma lista inicialmente vazia usa as funções inserir e retirar da lição. Todas as reservas têm sucesso. Inseres 5, depois 2, e retiras uma vez.

Desenha os três estados da lista, indica o valor retirado e explica por que retirar recebe struct no **. Para destruir o resto, que ordem deve haver entre ler seguinte e chamar free? Copiar o apontador da cabeça daria uma segunda lista independente?

Solução

Os estados são 5 → NULL, 2 → 5 → NULL e, após retirar 2, 5 → NULL. A função escreve em *cabeca para substituir o apontador que pertence ao chamador. Receber só struct no * permitiria mudar uma cópia local da cabeça, mas não o apontador do chamador.

Para destruir, guarda proximo = atual->seguinte, liberta atual e passa a proximo. Depois de free, o nó já não existe e não podes consultar o seu campo seguinte. Copiar a cabeça dá dois apontadores para os mesmos nós. Uma remoção por um deles pode deixar o outro inválido; uma cópia independente precisa de novos nós e de um contrato para os dados guardados neles.

Confere a tua resposta:

  • Segui a cabeça de NULL para 5 e depois para 2.
  • Expliquei por que a função precisa de alterar a cabeça do chamador.
  • Guardei o nó seguinte antes de libertar o atual.
Voltar à explicação
Seguir dois apontadores

Começa com x=2, y=6, p=&x e q=p. Executa p=&y; *q=8; *p=4. Qual é o valor final de x?

Pista
Atribuir a p não reescreve q.
Solução

x vale 8. q preservou o endereço de x; p mudou para y. A última escrita altera y para 4.

Erros frequentes

Achar que q segue automaticamente todas as alterações de p.

Voltar à explicação
Devolver um objeto

Uma função declara uma struct local e devolve o seu endereço. O chamador usa esse endereço depois do retorno. Explica o erro e duas formas de o evitar.

Pista
Compara a duração da variável com a duração do uso.
Mais uma pista

Devolver a estrutura por valor copia o objeto; uma reserva dinâmica mantém-no vivo até free.

Solução

A variável automática deixou de existir quando a função retornou. Usá-la pelo endereço guardado tem comportamento indefinido. Pode devolver a estrutura por valor ou reservar no heap e devolver o apontador, definindo que o chamador liberta o bloco.

Confere a tua resposta:

  • Rejeitei devolver o endereço da variável local.
  • Dei uma alternativa por valor ou heap.
  • Indiquei quem deve libertar um bloco do heap.

Erros frequentes

Dizer que um endereço numericamente igual prova que o objeto continua vivo.

Voltar à explicação
Corrigir a contagem de linhas

A função deve contar caracteres de mudança de linha numa string. Corrige a função e mantém os três exemplos de verificação. A string vazia tem zero; “abc” tem zero; “a\nb\n” tem duas.

Contar mudanças de linha
#include <stdio.h>

static size_t linhas(const char *texto) {
    size_t n = 1;
    for (size_t i = 0; texto[i] != '\0'; i++)
        if (texto[i] == '\n') n++;
    return n;
}

int main(void) {
    const char *dados[] = {"", "abc", "a\nb\n"};
    const size_t esperado[] = {0, 0, 2};
    for (size_t i = 0; i < 3; i++)
        printf("caso %zu: obtido=%zu esperado=%zu\n",
               i, linhas(dados[i]), esperado[i]);
    return 0;
}
Resultado

Executa o código para ver a saída e os gráficos.

Pista
Qual é a contagem antes de ler qualquer byte?
Mais uma pista

A função já incrementa por cada newline; não precisa de acrescentar um antes do ciclo.

Solução

Inicializa n com zero. Os resultados passam a 0, 0 e 2, iguais aos esperados. Contar linhas visuais com uma última linha sem newline seria outro contrato, como mostra o caso “abc”.

Confere a tua resposta:

  • Contei mudanças de linha, sem inventar uma linha final.
  • Conservei os três resultados esperados.
  • O ciclo só percorre bytes anteriores ao terminador.

Erros frequentes

Mudar os esperados para combinar com o erro ou tratar só a string vazia com um caso especial, deixando “abc” incorreto.

Voltar à explicação
Escrever um comando de máximo

Escreve em C um comando maximo que recebe um ou mais inteiros decimais representáveis por int. Deve escrever o máximo, ou terminar com erro sem imprimir um resultado se algum argumento for inválido. Verifica ./maximo -8 -2 -11, ./maximo 0, ./maximo, ./maximo 12x e um número maior do que INT_MAX. O executor C do navegador não passa estes argumentos; usa o terminal UNIX.

Pista
Valida cada string com strtol antes de a comparar.
Mais uma pista
O primeiro número válido inicializa o máximo. Um zero devolvido por strtol só é válido se houve conversão completa e não houve erro.
Solução

Guarda este programa em maximo.c e compila com cc -std=c17 -Wall -Wextra -Wpedantic maximo.c -o maximo.

#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "uso: %s inteiro [inteiros]\n", argv[0]);
        return 1;
    }
    int maximo = 0;
    for (int i = 1; i < argc; i++) {
        errno = 0;
        char *fim;
        long valor = strtol(argv[i], &fim, 10);
        if (errno == ERANGE || fim == argv[i] || *fim != '\0' ||
            valor < INT_MIN || valor > INT_MAX) {
            fprintf(stderr, "inteiro invalido: %s\n", argv[i]);
            return 1;
        }
        if (i == 1 || valor > maximo) maximo = (int)valor;
    }
    if (printf("%d\n", maximo) < 0 || fflush(stdout) == EOF) return 1;
    return 0;
}

A atribuição com i == 1 substitui o zero inicial antes de qualquer comparação. Os dois primeiros comandos escrevem -2 e 0; os restantes terminam com estado 1. Uma entrada inválida posterior, como ./maximo 7 12x, também rejeita todo o pedido.

Confere a tua resposta:

  • Rejeitei falta de números, texto inválido e valores fora de int.
  • Obtive -2 para -8 -2 -11 e 0 para 0.
  • Só apresentei o máximo depois de validar todos os argumentos.

Erros frequentes

Aceitar só o prefixo numérico de 12x, converter para int antes de testar os limites ou imprimir um máximo parcial antes de detetar o erro.
Voltar à explicação
Contrato de uma biblioteca

Descreve os três ficheiros de uma biblioteca de vetores com uma função de soma, os comandos de arquivo e ligação, e o contrato quando a API devolve um apontador para um bloco reservado.

Pista
Incluir o cabeçalho não fornece a implementação.
Mais uma pista

A biblioteca estática entra depois dos objetos que usam os seus símbolos.

Solução

O cabeçalho declara a função, a implementação define-a e o cliente inclui o cabeçalho e chama-a. Arquiva o objeto com ar e liga com a biblioteca depois dos objetos do cliente. Se a API devolve um apontador para um bloco reservado, o contrato diz que o chamador o liberta; devolver por valor não cria essa obrigação. O exemplo da lição compila estes três ficheiros e imprime 2 + 3 = 5.

Confere a tua resposta:

  • Separei cabeçalho, implementação e cliente.
  • Indiquei a ordem de ligação com a biblioteca estática.
  • Defini quem liberta cada bloco devolvido.

Erros frequentes

Achar que incluir o cabeçalho substitui a ligação, ou devolver um endereço de variável local como se fosse um bloco do heap.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.