Variáveis, Tipos e a Memória por Trás Deles

[339] Variáveis, Tipos e a Memória por Trás Deles

Um int não tem 4 bytes garantidos: o padrão fixa apenas mínimos e uma ordem, e quem depende do tamanho exato mede com sizeof ou recorre a stdint.h. Também estão aqui o especificador que o printf exige de você, o -1 que vira o maior unsigned, o char que é número, e const contra define.
Linguagem C

8 min de leitura

No artigo anterior, escrevemos nosso primeiro programa e entendemos o caminho até o executável. Hoje vamos guardar dados na memória — e já aqui o C revela sua natureza. Se você vem do Rust, espere uma inversão: em C, variáveis são mutáveis por padrão, e o compilador confia em você para não errar. Se vem de Python ou JavaScript, espere outra surpresa: aqui, todo dado tem um tipo fixo, declarado por você, e esse tipo determina exatamente quantos bytes ele ocupa na memória.

Declarando variáveis

Em C, você declara uma variável dizendo primeiro o tipo, depois o nome:

#include <stdio.h>

int main(void) {
    int idade = 30;
    printf("Idade: %d\n", idade);

    idade = 31; // mutável por padrão — sem cerimônia
    printf("Ano seguinte: %d\n", idade);

    return 0;
}

Repare no %d dentro do printf. Diferente de linguagens modernas, o C não sabe sozinho como imprimir uma variável — você precisa dizer o tipo dela com um especificador de formato. %d é para inteiros. Errar o especificador é uma armadilha clássica que veremos adiante.

Os tipos inteiros

C oferece uma família de tipos inteiros de tamanhos diferentes:

#include <stdio.h>

int main(void) {
    char        c = 65;      // 1 byte  (também guarda a letra 'A')
    short       s = 1000;    // geralmente 2 bytes
    int         i = 100000;  // geralmente 4 bytes
    long        l = 1000000; // 4 ou 8 bytes, depende da máquina
    long long   g = 9000000000LL; // pelo menos 8 bytes

    printf("%d %d %d %ld %lld\n", c, s, i, l, g);
    return 0;
}

Aqui está o primeiro grande "pega" do C: os tamanhos não são fixos pela linguagem. O padrão só garante mínimos e uma ordem (charshortintlonglong long). Um int costuma ter 4 bytes na maioria das máquinas modernas, mas isso não é lei. Se o seu código depende do tamanho exato, você precisa medir — e é isso que o operador sizeof faz:

#include <stdio.h>

int main(void) {
    printf("char:      %zu byte(s)\n", sizeof(char));
    printf("short:     %zu byte(s)\n", sizeof(short));
    printf("int:       %zu byte(s)\n", sizeof(int));
    printf("long:      %zu byte(s)\n", sizeof(long));
    printf("long long: %zu byte(s)\n", sizeof(long long));
    return 0;
}

O especificador %zu é o correto para o resultado de sizeof. Guarde: quando precisar de um inteiro de tamanho garantido, use os tipos de <stdint.h>int32_t, uint8_t, int64_t — que dizem o tamanho no próprio nome.

Veja os tamanhos mudarem: no Compiler Explorer dá para trocar o compilador e a arquitetura no menu do topo. Rode o programa do sizeof e alterne entre um alvo de 64 bits e um de 32: o long passa de 8 para 4 bytes na sua frente. É a demonstração mais direta de que o padrão garante mínimos, não tamanhos.

Com sinal e sem sinal

Todo tipo inteiro pode ser signed (com sinal, o padrão) ou unsigned (só valores não negativos, o que dobra o valor máximo positivo):

#include <stdio.h>

int main(void) {
    unsigned int positivos = 4000000000; // caberia? sim, num unsigned de 32 bits
    int          comSinal  = -42;

    printf("%u\n", positivos); // %u para unsigned
    printf("%d\n", comSinal);
    return 0;
}

Cuidado: misturar signed e unsigned numa mesma conta é fonte de bugs sutis. Um -1 atribuído a um unsigned não vira zero nem dá erro — ele "dá a volta" e vira o maior valor possível. C confia em você; ele não avisa.

Ponto flutuante

Para números com casas decimais, há float (precisão simples) e double (precisão dupla, o padrão recomendado):

#include <stdio.h>

int main(void) {
    float  altura = 1.75f;   // o 'f' marca literal float
    double pi     = 3.141592653589793;

    printf("Altura: %.2f\n", altura); // %.2f = duas casas decimais
    printf("Pi:     %.5f\n", pi);
    return 0;
}

Use double por padrão; recorra a float só quando memória for uma restrição real. E lembre-se: ponto flutuante é aproximado. 0.1 + 0.2 não dá exatamente 0.3 em nenhuma linguagem que use IEEE 754 — e o C não é exceção.

Caracteres são números

Em C, o tipo char guarda um caractere — mas, por baixo, é só um número inteiro pequeno (o código ASCII do caractere):

#include <stdio.h>

int main(void) {
    char letra = 'A';
    printf("%c tem código %d\n", letra, letra); // A tem código 65

    char proxima = letra + 1;
    printf("%c\n", proxima); // B

    return 0;
}

Isso não é curiosidade acadêmica: é a base de como se manipula texto em C. 'A' + 1'B' porque, para o C, letras são números. Aspas simples ('A') são para um único caractere; aspas duplas ("A") são para texto — e são coisas bem diferentes, como veremos ao estudar strings.

Constantes: quando o valor não deve mudar

Se um valor não deve ser alterado, marque-o com const. O compilador passa a recusar qualquer tentativa de modificá-lo:

#include <stdio.h>

int main(void) {
    const double TAXA_JUROS = 0.05;
    // TAXA_JUROS = 0.06; // ERRO de compilação: assignment of read-only variable

    double saldo = 1000.0;
    saldo = saldo * (1.0 + TAXA_JUROS);
    printf("Saldo: %.2f\n", saldo);
    return 0;
}

Existe também o velho #define TAXA_JUROS 0.05, uma substituição feita pelo pré-processador. A diferença é importante: const cria uma variável real, com tipo e verificação; #define é só um "localizar e substituir" de texto, sem tipo. Prefira const no código moderno.

Um programa completo

Vamos reunir tudo num exemplo de juros compostos, semelhante ao que fizemos com o Rust — para você comparar as duas linguagens:

#include <stdio.h>

int main(void) {
    const double TAXA_JUROS = 0.05; // 5% ao ano
    double valor_inicial = 1000.0;
    double saldo = valor_inicial;
    int anos = 3;

    for (int ano = 1; ano <= anos; ano++) {
        saldo = saldo * (1.0 + TAXA_JUROS);
        printf("Ano %d: R$ %.2f\n", ano, saldo);
    }

    double rendimento = saldo - valor_inicial;
    printf("\nRendimento total: R$ %.2f\n", rendimento);
    return 0;
}

Saída:

Ano 1: R$ 1050.00
Ano 2: R$ 1102.50
Ano 3: R$ 1157.63

Rendimento total: R$ 157.63

O que vem a seguir

Hoje aprendemos a guardar dados e conhecemos os tipos fundamentais do C — e o fato incômodo, porém importante, de que seus tamanhos dependem da máquina. No próximo artigo, vamos organizar o código em funções: como declará-las, por que o C exige que você as anuncie antes de usá-las (ao contrário do Rust), e o conceito de passagem por valor, que é a chave para entender por que, mais tarde, vamos precisar de ponteiros.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva um programa que declare sua idade num int, sua altura num double e a inicial do seu nome num char, e imprima os três com os especificadores de formato corretos.

Ver resposta

✓ Resposta: Cada tipo pede seu especificador: %d, %f (ou %.2f) e %c:

#include <stdio.h>

int main(void) {
    int idade = 30;
    double altura = 1.75;
    char inicial = 'M';
    printf("%c, %d anos, %.2f m\n", inicial, idade, altura);
    return 0;
}

Exercício 2

Rode o programa do sizeof na sua máquina e anote os tamanhos. Depois, previsão: quantos bytes ocupa um unsigned long long? Confirme com sizeof.

Ver resposta

✓ Resposta: Na maioria das máquinas de 64 bits (Linux/macOS), você verá char = 1, short = 2, int = 4, long = 8, long long = 8. Um unsigned long long tem o mesmo tamanho do long long — normalmente 8 bytes. O unsigned muda a faixa de valores, não o tamanho.

Exercício 3

Atribua o valor -1 a uma variável unsigned int e imprima com %u. Explique o resultado que apareceu.

Ver resposta

✓ Resposta: O %u vai imprimir 4294967295 (num unsigned int de 32 bits). Como unsigned não representa negativos, o -1 "dá a volta" (wraparound) e vira o maior valor possível: 2³² − 1. O C não acusa erro — ele apenas reinterpreta os bits. Esse é um dos motivos para tomar cuidado ao misturar signed e unsigned.

Exercício 4

Imprima o resultado de 0.1 + 0.2 com %.17f. O que você observa? Por que isso acontece?

Ver resposta

✓ Resposta: Você verá algo como 0.30000000000000004. Isso acontece porque 0.1 e 0.2 não têm representação exata em binário (assim como 1/3 não tem representação exata em decimal). O padrão IEEE 754, usado por praticamente todos os processadores, armazena a aproximação mais próxima — e o pequeno erro aparece quando você força 17 casas.

Exercício 5

Troque o const double TAXA_JUROS do exemplo de juros por um #define. O programa continua funcionando? Qual a diferença prática entre as duas formas aqui?

Ver resposta

✓ Resposta: Continua funcionando e imprime o mesmo resultado. A diferença: const double TAXA_JUROS = 0.05; cria uma variável real, com tipo double e verificação pelo compilador. #define TAXA_JUROS 0.05 faz o pré-processador trocar o texto TAXA_JUROS por 0.05 antes da compilação — não há variável nem tipo, e erros ficam mais difíceis de rastrear. Por isso o C moderno prefere const para valores tipados.

Comentários

Mais em Linguagem C

Comunicação em Rede com Sockets
Comunicação em Rede com Sockets

Depois de conectado, um socket se lê e se escreve como arquivo — e é essa…

Ponteiros para Struct e o Operador Seta
Ponteiros para Struct e o Operador Seta

Os parênteses em (*ptr).x não são preciosismo: sem eles o ponto vence o…

Strings: Vetores de Caracteres e a Biblioteca string.h
Strings: Vetores de Caracteres e a Biblioteca string.h

A palavra Ana tem três letras e ocupa quatro bytes: o quarto é o terminador…