C estruturado e ferramentas

Tipos, conversões, módulos, compilação separada e bibliotecas estáticas em C.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Um contador de ticks precisa de um inteiro que persista entre eventos. Uma posição precisa de coordenadas que possam diminuir. Um registo do teclado precisa de oito bits. A escolha do tipo e do módulo determina como o programa representa e altera estes dados.

De C++ para C

C tem funções, estruturas, apontadores e controlo de fluxo. Não tem classes, referências de C++, sobrecarga de funções nem new e delete. Uma struct guarda dados; funções recebem o seu endereço para os consultar ou alterar. Para alocar memória usamos malloc e para a devolver usamos free.

C e C++ partilham muita sintaxe, mas não aceitam todos os mesmos programas. Em C, escreve struct position se não tiveres criado um typedef. A declaração int f(void) indica que f não recebe argumentos. Os protótipos permitem conferir os tipos usados nas chamadas.

#include <stdbool.h>
#include <stdint.h>

typedef struct {
  int16_t x;
  int16_t y;
} position;

bool position_equal(position a, position b) {
  return a.x == b.x && a.y == b.y;
}

Esta função recebe cópias das posições, por isso alterar a.x dentro dela não altera a posição do chamador. Para alterar um objeto do chamador, a função precisa de receber um apontador para esse objeto.

Tipos e saída formatada

O tamanho de int depende da implementação. Quando o tamanho faz parte do contrato de um dispositivo, usa os tipos de <stdint.h>: uint8_t para um byte sem sinal, uint16_t para 16 bits e uint32_t para 32 bits. Usa size_t para tamanhos e índices de objetos na memória.

printf não deduz os tipos pela cadeia de formato. Cada conversão tem de corresponder ao argumento: %d recebe int, %u recebe unsigned int, %zu recebe size_t e %p recebe void *. Para tipos de largura fixa, <inttypes.h> fornece macros como PRIu32.

#include <inttypes.h>
#include <stdio.h>

int main(void) {
  uint32_t ticks = 300;
  uint8_t status = 0xA5;
  printf("ticks=%" PRIu32 " status=0x%02X\n",
         ticks, (unsigned)status);
  return 0;
}

O cast explicita o tipo pedido por %X, por isso o programa imprime ticks=300 status=0xA5. Uma incompatibilidade de formato não é apenas uma apresentação errada, porque pode causar comportamento indefinido. Ativa os avisos do compilador.

Promoções e conversões de inteiros

Antes de muitas operações, C promove valores mais pequenos do que int. Num sistema onde int representa todos os valores de uint8_t, um byte torna-se um int. A operação deixa de ter apenas oito bits.

Considera uint8_t value = 0x5A. O complemento de oito bits seria 0xA5. Porém, ~value opera sobre o valor promovido. Com int de 32 bits e representação em complemento para dois, o resultado tem os bits de 0xFFFFFFA5. Deslocar esse valor negativo à direita tem resultado definido pela implementação em C (C11 §6.5.7): o gcc do laboratório documenta extensão de sinal, mas código portátil não pode contar com esse comportamento. Não derives um resultado portátil a partir de um deslocamento de um inteiro negativo.

Para complementar exatamente o byte, reduz o resultado a uint8_t antes do deslocamento:

uint8_t value = 0x5A;
uint8_t result = (uint8_t)~value >> 4;

O cast produz 0xA5. Para o deslocamento, este valor promove-se a um int positivo. O resultado é 0x0A, ou 10. Converter apenas no fim não corrige um deslocamento já efetuado sobre um inteiro negativo.

Outro caso frequente mistura um inteiro negativo com um inteiro sem sinal:

int delta = -1;
unsigned int width = 10;
bool inside = delta < width;

Na comparação, delta converte-se para unsigned int. Num sistema com este tipo de 32 bits, -1 passa a 4294967295, maior do que 10. O teste dá falso. Valida primeiro delta >= 0 e só depois compara (unsigned int)delta < width. Para posições com movimento negativo, mantém as contas num tipo com sinal até validares os limites.

Apontadores, arrays e tamanho

&x obtém o endereço de x. Se p aponta para um int válido, *p lê ou altera esse inteiro. p + 1 avança o tamanho de um int, não um byte. O tipo do apontador determina a unidade da aritmética.

int values[] = {10, 20, 30};
int *p = values;
p++;
*p = 25;

Agora values contém {10, 25, 30}, porque p++ avançou um int e *p = 25 alterou o segundo elemento. Em muitas expressões, o array converte-se num apontador para o primeiro elemento. O array e o apontador continuam a ser objetos diferentes: sizeof values dá o tamanho de todo o array, mas sizeof p dá o tamanho de um endereço.

Um parâmetro int values[] é ajustado para int *values. A função não recebe o comprimento do array. Passa-o à parte e nunca acedas fora do intervalo. Podes formar um apontador para uma posição imediatamente a seguir ao fim para controlar um ciclo, mas não o podes desreferenciar.

Strings e fim do array

Uma string de C termina no carácter nulo \0. Esse byte faz parte do armazenamento, mas não do comprimento devolvido por strlen, de <string.h>. O excerto seguinte distingue os dois tamanhos:

char name[] = "LC";
size_t length = strlen(name); /* 2 caracteres antes de \0 */
size_t storage = sizeof name; /* 3 bytes, incluindo \0 */

O array contém L, C e \0. Uma função que procura o fim da string exige que esse terminador exista dentro do objeto. Não passes um array de bytes de hardware diretamente a %s ou strlen: um pacote PS/2 pode conter zeros nos dados e não tem um terminador de string.

char name[] = "LC" cria um array modificável. const char *name = "LC" aponta para um literal, que não deves alterar. Para copiar uma string para um array, reserva espaço para todos os caracteres e o terminador; um limite de bytes copiados que deixa o destino sem \0 não produz uma string válida.

Módulos e encapsulamento

O ficheiro .h declara o que outros módulos podem usar. O .c implementa essas operações. Inclui o próprio cabeçalho no .c, para o compilador conferir que a definição corresponde à declaração.

O exemplo seguinte é um módulo de contador completo e portátil, sem dependências do Minix:

#ifndef COUNTER_H
#define COUNTER_H

#include <stdint.h>

void counter_reset(void);
void counter_tick(void);
uint32_t counter_value(void);

#endif
#include "counter.h"

static uint32_t ticks;

void counter_reset(void) { ticks = 0; }
void counter_tick(void) { ++ticks; }
uint32_t counter_value(void) { return ticks; }
#include "counter.h"
#include <inttypes.h>
#include <stdio.h>

int main(void) {
  counter_reset();
  counter_tick();
  counter_tick();
  printf("%" PRIu32 "\n", counter_value());
  return 0;
}

A variável ticks tem duração estática e visibilidade limitada ao ficheiro porque foi declarada com static. Os outros módulos chamam as funções, sem modificar o contador por acesso direto. Não definas uma variável global num cabeçalho incluído por vários .c. Se a partilha for necessária, declara-a com extern no cabeçalho e define-a uma única vez.

Compilar e ligar

A construção tem quatro etapas: o pré-processador expande #include e macros; o compilador traduz C para assembly; o assembler produz código objeto; o linker resolve referências entre objetos e bibliotecas. Um .o ainda pode conter chamadas a funções cuja definição está noutro objeto.

Com os três ficheiros anteriores na mesma pasta:

cc -std=c11 -Wall -Wextra -Wpedantic -c counter.c
cc -std=c11 -Wall -Wextra -Wpedantic -c main.c
cc main.o counter.o -o demo
./demo

Incluir counter.h dá a declaração, mas não acrescenta counter.c ao executável. A ligação dos dois objetos resolve as chamadas e ./demo imprime 2. Um erro de sintaxe ou de tipo aparece na compilação. Uma referência a counter_tick sem definição aparece na ligação.

Segue o diagnóstico pela fase. Se a mensagem fala de declaração em falta ou tipo incompatível, volta ao .c e ao cabeçalho. Se fala de referência indefinida a counter_tick, o problema está na linha de ligação. Compila primeiro cada fonte com cc -c e só depois liga com cc main.o counter.o -o demo. Se mudares counter.h, recompila os dois objetos, porque ambos dependem dele.

Para criar e usar uma biblioteca estática:

ar rcs libcounter.a counter.o
cc main.o -L. -lcounter -o demo

ar agrupa código objeto num arquivo. -L. acrescenta a pasta atual à procura; -lcounter procura uma biblioteca com nome como libcounter.a. Com linkers que processam arquivos por ordem, coloca a biblioteca depois dos objetos que usam os seus símbolos. A ligação estática copia para o executável o código necessário do arquivo, não liga o programa ao código fonte.

Make, Git e documentação

Um Makefile descreve alvos e dependências. Se mudares counter.h, tanto counter.o como main.o precisam de ser recompilados. As linhas das receitas começam com uma tabulação.

CC = cc
CFLAGS = -std=c11 -Wall -Wextra -Wpedantic

demo: main.o counter.o
	$(CC) main.o counter.o -o demo

main.o: main.c counter.h
counter.o: counter.c counter.h

clean:
	rm -f demo main.o counter.o

.PHONY: clean

make compara dependências e datas para construir o que mudou. Os Makefiles do ambiente de LC também ligam as bibliotecas de apoio necessárias; não substituas esse ambiente por este exemplo portátil.

No Git, revê git diff antes de guardar uma alteração. diff -u old.c new.c produz um patch de texto; patch aplica-o a uma base compatível. Um patch não é um executável nem resolve automaticamente alterações incompatíveis na mesma linha.

Documenta a interface com Doxygen: o que a operação faz, pré-condições, parâmetros, resultado e quem liberta a memória. Uma descrição como “incrementa contador” não explica a política de overflow nem o instante em que o contador reinicia. A documentação útil permite chamar a função sem ler a sua implementação.

Exercícios

Complementar um byte

Com uint8_t byte = 0x96, quanto vale (uint8_t)~byte >> 3 em decimal? Considera int capaz de representar todos os valores de uint8_t.

Pista
O cast acontece antes do deslocamento.
Solução

0x96 tem complemento de oito bits 0x69, que vale 105. Depois do cast, a promoção produz um inteiro positivo. O deslocamento dá o quociente inteiro de 105/8, ou 13.

Erros frequentes

Fazer o complemento num inteiro com sinal e deslocar antes do cast não corresponde à expressão pedida.

Voltar à explicação
Ligação e dependências do cabeçalho

main.c e counter.c incluem counter.h. Ambos compilam sem erros, mas cc main.o -o demo falha com uma referência indefinida a counter_tick, definida em counter.c.

  1. Explica a causa e corrige o comando de ligação, usando counter.o.
  2. Depois, alteras a declaração da função em counter.h e adaptas a definição e a chamada. Que objetos recompilas antes de construir o novo executável? Justifica.

Pista

O cabeçalho permite conferir a chamada. Que ficheiro fornece as instruções da função?

Solução

A inclusão do cabeçalho fornece a declaração, não a definição. main.o conserva uma referência externa que o linker ainda precisa de resolver. Usa cc main.o counter.o -o demo.

Após a alteração da interface, recompila main.c e counter.c, pois ambos dependem de counter.h. Volta a ligar os dois objetos novos. Ligar um objeto antigo com outro novo pode misturar chamadas e definições incompatíveis.

Confere a tua resposta:

  • Distingui declaração, definição e fase de ligação.
  • Liguei main.o com counter.o.
  • Recompilei os dois objetos após a alteração do cabeçalho e voltei a ligar.
Voltar à explicação
Comparar uma posição negativa

Num sistema com int e unsigned int de 32 bits, delta vale -2 e width é unsigned int com valor 100. Explica por que delta < width dá falso e escreve um teste seguro para 0 ≤ delta < width.

Pista
A comparação aplica as conversões aritméticas usuais.
Solução

Como unsigned int representa valores que int não representa, delta converte-se para unsigned int. O valor 4294967294 é maior do que 100. Um teste seguro é delta >= 0 && (unsigned int)delta < width. O segundo operando só é avaliado se a posição não for negativa.

Confere a tua resposta:

  • Identifiquei a conversão para unsigned.
  • Validei a posição negativa antes do cast.
  • Mantive a comparação com o limite coerente.

Erros frequentes

Converter delta para unsigned antes de testar o sinal perde a possibilidade de detetar diretamente a posição negativa.

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.