C avançado para sistemas
Compilação, bibliotecas, apontadores, tempo de vida, strings e argumentos.
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.
#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;
}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 . Somar (1,2) e (3,-1) dá (4,1); multiplicar dá . 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.
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
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.
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
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.
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.
#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;
}Executa o código para ver a saída e os gráficos.
Pista
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.
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
strtol antes de a comparar.Mais uma pista
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
12x, converter para int antes de testar os limites ou imprimir um máximo parcial antes de detetar o erro.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
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.