Classes de Armazenamento: static, extern e o Tempo de Vida

[368] Classes de Armazenamento: static, extern e o Tempo de Vida

A mesma palavra static faz duas coisas diferentes: dentro de uma função, estica o tempo de vida; em escopo de arquivo, esconde o nome dos demais. Aqui ficam os três eixos que as classes de armazenamento controlam — escopo, tempo de vida e ligação —, o extern que compartilha, e o register que o tempo aposentou.
Linguagem C

12 min de leitura

Ao longo do curso, dois temas nos acompanharam de perto: onde uma variável vive e por quanto tempo. Vimos variáveis locais que nascem e morrem com a função, globais que persistem, o static que torna coisas privadas a um arquivo. Hoje juntamos todas essas peças num quadro único e formal — as classes de armazenamento do C. São quatro palavras-chave que controlam o escopo (onde um nome é visível), o tempo de vida (por quanto tempo o valor existe) e a ligação (se o nome é compartilhado entre arquivos). Dominar isso encerra a Fase 3 e consolida a base sobre a qual toda a organização de código se apoia.

Os três eixos que definem uma variável

Antes das palavras-chave, fixe os três conceitos que elas controlam, porque são independentes entre si. O escopo responde "onde este nome pode ser usado?" — pode ser o bloco, a função ou o arquivo inteiro. O tempo de vida responde "quando o valor existe na memória?" — pode durar só enquanto a função roda (na pilha) ou pela execução inteira do programa. E a ligação (linkage) responde "este nome é o mesmo em arquivos diferentes?" — determina se dois arquivos que usam o nome contador se referem à mesma variável ou a duas distintas. As classes de armazenamento ajustam esses três eixos.

auto: o padrão que você nunca escreve

A classe auto é o comportamento padrão de toda variável local. Ela indica: escopo de bloco, tempo de vida limitado à execução do bloco, sem ligação. Como é o padrão, você nunca precisa escrevê-la:

#include <stdio.h>

int main(void) {
    auto int x = 5;  // idêntico a: int x = 5;
    int y = 10;      // já é 'auto' implicitamente
    printf("%d %d\n", x, y);
    return 0;
}

Escrever auto explicitamente é tão raro que a palavra é praticamente uma curiosidade histórica em C. Toda variável local que você declarou até hoje foi auto — ela nasce ao entrar no bloco e some ao sair, vivendo na pilha, como estudamos na aula A Pilha e o Heap: Onde Cada Coisa Vive. Mencionamos auto apenas para completude; na prática, você a ignora.

register: uma sugestão que o tempo aposentou

A classe register era uma dica ao compilador: "se possível, guarde esta variável num registrador do processador, em vez da memória, para acesso mais rápido". Fazia sentido em compiladores antigos e código de altíssimo desempenho:

void laco(void) {
    register int i;  // "por favor, mantenha i num registrador"
    for (i = 0; i < 1000000; i++) {
        // ...
    }
}

Hoje, register é essencialmente obsoleta. Os compiladores modernos otimizam a alocação de registradores muito melhor do que qualquer sugestão manual — eles ignoram a dica e fazem escolhas próprias, geralmente superiores. A única consequência prática de register que ainda vale é que você não pode tomar o endereço de uma variável register (não pode fazer &i). Salvo em código legado ou situações muito especiais, não a use; deixe a otimização com o compilador.

static: a palavra de dois poderes

Aqui está a classe mais importante desta aula, e ela tem dois significados distintos conforme onde é usada. Já conhecemos um deles; hoje formalizamos ambos.

O primeiro poder aparece quando static é aplicada a uma variável local, dentro de uma função. Ela muda o tempo de vida: a variável passa a existir durante toda a execução do programa, preservando seu valor entre chamadas — embora continue visível apenas dentro daquela função:

#include <stdio.h>

void contar(void) {
    static int chamadas = 0;  // inicializada UMA vez; persiste entre chamadas
    chamadas++;
    printf("Esta função foi chamada %d vez(es).\n", chamadas);
}

int main(void) {
    contar(); // 1
    contar(); // 2
    contar(); // 3
    return 0;
}

Repare no que acontece: chamadas não volta a zero a cada chamada de contar. Ela é inicializada uma única vez (na primeira execução) e mantém seu valor entre as chamadas, porque seu tempo de vida é o do programa inteiro, não o da função. Uma variável local comum viveria na pilha e morreria a cada retorno; a static vive na região de dados globais, persistindo. Isso é útil para contadores, memoização e estados que precisam sobreviver entre chamadas — mas use com parcimônia, pois estado oculto que persiste pode dificultar o raciocínio sobre o código.

O segundo poder é o que vimos na aula anterior: static aplicada a uma variável ou função global (em escopo de arquivo) muda a ligação, tornando-a privada ao arquivo — invisível para outros arquivos:

// modulo.c
static int segredo = 42;        // só este arquivo enxerga 'segredo'
static void ajudante(void) { }  // só este arquivo pode chamar 'ajudante'

void publica(void) {            // esta, sem static, é visível a outros arquivos
    // ...
}

São dois usos que compartilham a palavra static mas atuam em eixos diferentes: dentro de função, mexe no tempo de vida (persiste); em escopo de arquivo, mexe na ligação (esconde). Saber distinguir os dois é sinal de que você entende o modelo por baixo.

extern: compartilhando uma variável entre arquivos

A classe extern é a contraparte do segundo uso de static. Enquanto static esconde, extern compartilha: ela declara que uma variável global existe em algum outro arquivo, permitindo que vários arquivos acessem a mesma variável. O idioma exige cuidado: a variável é definida (criada) em um único .c, e declarada com extern nos cabeçalhos ou arquivos que querem usá-la.

// config.c — a DEFINIÇÃO (cria a variável, uma única vez)
int versao_global = 3;
// config.h — a DECLARAÇÃO extern (anuncia que ela existe alhures)
#ifndef CONFIG_H
#define CONFIG_H
extern int versao_global;  // "existe uma versao_global em algum .c"
#endif
// main.c — o USO
#include <stdio.h>
#include "config.h"
int main(void) {
    printf("Versão: %d\n", versao_global); // acessa a variável de config.c
    return 0;
}

A lógica é a mesma dos protótipos de função, mas aplicada a variáveis: o extern int versao_global; no cabeçalho é uma promessa ("esta variável existe, o ligador vai encontrá-la"), e a definição real int versao_global = 3; no config.c é onde ela de fato nasce. Se você definisse a variável (sem extern) em dois arquivos, teria o erro de definição múltipla que já conhecemos. Uma ressalva honesta: variáveis globais compartilhadas via extern são poderosas, mas perigosas — qualquer arquivo pode alterá-las, dificultando rastrear mudanças de estado. Use-as com moderação; muitas vezes, passar dados por parâmetros é mais claro.

Um quadro para fixar

Vale consolidar como as classes afetam os três eixos. Uma variável local comum (auto) tem escopo de bloco, vive na pilha (morre no fim do bloco) e não tem ligação. Uma local static tem escopo de bloco, mas vive o programa inteiro (persiste entre chamadas). Uma global comum tem escopo de arquivo, vive o programa inteiro e tem ligação externa (visível a outros arquivos via extern). Uma global static é igual à global comum, exceto que sua ligação é interna (invisível a outros arquivos). Esse quadro amarra tudo o que vimos sobre escopo (aula Funções, Escopo e Passagem por Valor), pilha e heap (aula A Pilha e o Heap: Onde Cada Coisa Vive) e modularização (aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos) num modelo coerente.

Um exemplo que reúne as ideias

#include <stdio.h>

int chamadas_totais = 0;   // global comum: poderia ser compartilhada via extern

void registrar_acesso(void) {
    static int acessos_locais = 0;  // static local: persiste, mas só aqui
    acessos_locais++;
    chamadas_totais++;
    printf("Acessos nesta função: %d | Total no programa: %d\n",
           acessos_locais, chamadas_totais);
}

int main(void) {
    registrar_acesso(); // Acessos: 1 | Total: 1
    registrar_acesso(); // Acessos: 2 | Total: 2
    registrar_acesso(); // Acessos: 3 | Total: 3
    return 0;
}

Neste exemplo, acessos_locais (static local) e chamadas_totais (global) sobem juntos porque ambos persistem por toda a execução — mas com escopos diferentes: o primeiro só é visível dentro de registrar_acesso, o segundo em todo o arquivo. Se houvesse um segundo contador static em outra função, ele seria independente deste, apesar do tempo de vida igual. É a distinção entre tempo de vida e escopo, tornada concreta.

Fechando a Fase 3

Com esta aula, encerramos a fase de tipos compostos e organização de código. Você entrou nela sabendo agrupar dados em structs; sai dela dominando structs, enums, unions, typedef, o pré-processador, a compilação em módulos e, agora, o modelo formal de escopo, tempo de vida e ligação que sustenta tudo isso. Seu código deixou de ser um arquivo monolítico e passou a ser um projeto organizado, com interfaces limpas e detalhes bem encapsulados. Na próxima fase, o programa vai ganhar voz e memória duradoura: começamos a Fase 4 — Entrada, Saída e a Biblioteca Padrão, mergulhando primeiro a fundo em printf e scanf, para depois aprender a ler e gravar arquivos, fazendo nossos dados sobreviverem ao fim da execução.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva uma função proximo_id(void) que retorne um identificador único crescente a cada chamada (1, 2, 3, ...), usando uma variável static local. Chame-a quatro vezes em main e confirme a sequência.

Ver resposta

✓ Resposta:

#include <stdio.h>
int proximo_id(void) {
    static int id = 0;  // persiste entre chamadas
    id++;
    return id;
}
int main(void) {
    printf("%d %d %d %d\n", proximo_id(), proximo_id(), proximo_id(), proximo_id());
    return 0;
}

Cada chamada incrementa a mesma variável static, produzindo 1 2 3 4. (Nota: a ordem de avaliação dos argumentos de printf não é garantida pelo C, então, para um teste rigoroso, chame a função em linhas separadas; na maioria dos compiladores, porém, a sequência aparecerá.)

Exercício 2

Demonstre a diferença entre uma variável local static e uma comum: escreva duas funções quase idênticas, uma com static int n = 0; e outra com int n = 0;, ambas incrementando e imprimindo n. Chame cada uma três vezes e explique por que os resultados diferem.

Ver resposta

✓ Resposta:

#include <stdio.h>
void com_static(void) {
    static int n = 0;
    n++;
    printf("static:  %d\n", n);
}
void sem_static(void) {
    int n = 0;
    n++;
    printf("comum:   %d\n", n);
}
int main(void) {
    com_static(); com_static(); com_static(); // 1, 2, 3
    sem_static(); sem_static(); sem_static(); // 1, 1, 1
    return 0;
}

A versão static imprime 1, 2, 3 porque n é inicializado uma única vez e persiste entre chamadas — seu tempo de vida é o do programa. A versão comum imprime 1, 1, 1 porque n é uma variável de pilha, recriada e reinicializada a 0 a cada chamada, morrendo no retorno.

Exercício 3

Explique, com suas palavras, os dois significados distintos da palavra-chave static em C, conforme ela seja aplicada a uma variável local ou a uma variável/função global.

Ver resposta

✓ Resposta: Os dois significados: (1) Aplicada a uma variável local (dentro de uma função), static altera o tempo de vida — a variável deixa de viver só durante a chamada e passa a existir por toda a execução do programa, preservando seu valor entre chamadas, embora continue visível apenas naquela função. (2) Aplicada a uma variável ou função global (em escopo de arquivo), static altera a ligação — o nome deixa de ser visível a outros arquivos e passa a ser privado ao arquivo onde foi declarado. No primeiro caso, static mexe em quando o valor existe; no segundo, em quem pode ver o nome. A mesma palavra atua em eixos diferentes conforme o contexto.

Exercício 4

Crie dois arquivos: dados.c, que define int total_registros = 0;, e dados.h, que a declara com extern. Escreva um main.c que inclua dados.h, incremente total_registros e o imprima. Compile os dois .c juntos. Por que a variável precisa de extern no cabeçalho, mas não na definição?

Ver resposta

✓ Resposta:

// dados.c
int total_registros = 0;   // DEFINIÇÃO: cria a variável
// dados.h
#ifndef DADOS_H
#define DADOS_H
extern int total_registros; // DECLARAÇÃO: anuncia que existe alhures
#endif
// main.c
#include <stdio.h>
#include "dados.h"
int main(void) {
    total_registros++;
    total_registros++;
    printf("%d\n", total_registros); // 2
    return 0;
}

Compilação: gcc main.c dados.c -o programa. A variável precisa de extern no cabeçalho porque o cabeçalho apenas anuncia sua existência a quem o inclui — sem extern, cada arquivo que incluísse dados.h tentaria criar sua própria total_registros, gerando definição múltipla. A definição real (sem extern) fica em um único lugar, dados.c, onde a variável de fato nasce e ocupa memória. É a mesma separação entre "declarar" (prometer que existe) e "definir" (criar de fato) que vimos com os protótipos de função.

Exercício 5

Explique por que a classe de armazenamento register é considerada obsoleta hoje, e cite a única restrição prática que ela ainda impõe sobre uma variável.

Ver resposta

✓ Resposta: A classe register é obsoleta porque os compiladores modernos fazem a alocação de registradores automaticamente e de forma muito mais eficiente do que qualquer dica manual — eles analisam o código inteiro e decidem quais variáveis manter em registradores, ignorando a sugestão register (que é apenas uma dica, não uma ordem). A única restrição prática que register ainda impõe é que você não pode tomar o endereço de uma variável declarada com ela: &variavel é proibido para uma variável register, já que registradores não têm endereço de memória. Fora essa peculiaridade, usá-la não traz benefício em código moderno.

Comentários

Mais em Linguagem C

O Mapa da Jornada: O que Você Vai Dominar em 52 Aulas
O Mapa da Jornada: O que Você Vai Dominar em 52 Aulas

Nove fases, da primeira linha impressa na tela ao programa que cria processos…

Undefined Behavior: O Território Perigoso do C
Undefined Behavior: O Território Perigoso do C

Um índice a mais num vetor de cinco pode imprimir tudo certo, travar, ou virar…

Ponteiros para Função: Passando Comportamento como Argumento
Ponteiros para Função: Passando Comportamento como Argumento

O nome de uma função, escrito sem os parênteses, é um endereço — e dessa…