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 (char ≤ short ≤ int ≤ long ≤ long 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 dá '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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 2, Types, Operators and Expressions
- cppreference — Fundamental types — https://en.cppreference.com/w/c/language/types
- cppreference —
<stdint.h>— tipos inteiros de tamanho fixo — https://en.cppreference.com/w/c/types/integer - What Every Computer Scientist Should Know About Floating-Point Arithmetic, David Goldberg — o texto clássico sobre
float/double— https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html - Modern C, Jens Gustedt — Cap. sobre tipos básicos — https://gustedt.gitlabpages.inria.fr/modern-c/
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.