C Memória e Endereços (&)

Bem-vindo ao Nível 2. As próximas aulas são sobre ponteiros, o assunto que tem fama de ser o mais difícil do C. A fama existe porque muita gente começa pelos ponteiros sem antes entender a coisa que eles apontam: a memória. Esta aula não tem ponteiro nenhum. Ela só mostra como a memória é organizada, quanto espaço cada variável ocupa e como descobrir onde uma variável mora. Com isso bem entendido, ponteiro vira uma ideia simples.

Você vai aprender
  • Que a memória é uma fileira enorme de gavetas numeradas (os bytes), e que o número de cada gaveta é o seu endereço.
  • Quantas gavetas cada tipo ocupa, com sizeof.
  • Como ver o endereço de uma variável com &x e mostrá-lo com %p e (void *).
  • Por que o endereço muda a cada execução (ASLR) e por que, no editor do guia, ele é um número bem menor.
  • O que é, afinal, aquele & do scanf.

Todos os programas foram compilados na máquina da Ana (usuária ana, computador notebook, um Linux de 64 bits), na pasta ~/c, com o comando padrão do guia, gcc -std=c23 -Wall -Wextra arquivo.c -o programa (veja Compilar com gcc).


A ideia: uma fileira de gavetas numeradas

Pense num arquivo de aço de escritório, só que com bilhões de gavetas pequenas, todas numa fileira só. Cada gaveta tem uma plaquinha com um número, e não existem duas gavetas com o mesmo número. Dentro de cada gaveta cabe um único valor pequeno, de 0 a 255. A memória do computador (a RAM) funciona assim:

No arquivo de gavetasNa memória
Uma gavetaUm byte: o menor pedaço da memória que tem número próprio. Guarda um valor de 0 a 255 (8 bits, oito "zeros ou uns").
A plaquinha com o númeroO endereço do byte.
Uma pasta grossa que precisa de 4 gavetas vizinhasUma variável int, que ocupa 4 bytes seguidos.
"A pasta está a partir da gaveta 1000"O endereço de uma variável é o número da primeira gaveta que ela ocupa.
10001001100210031004'A'1005?1006?1007?idade (int) = 29inicialoutras coisas
Um pedaço da fileira, com números de gaveta inventados para ficar fácil de ler. A variável idade (um int) ocupa 4 gavetas, da 1000 à 1003; o endereço dela é 1000. A variável inicial (um char) cabe numa gaveta só, a 1004.

Quando você escreve int idade = 29;, o compilador escolhe 4 gavetas livres e vizinhas, guarda o 29 nelas e anota para si: "idade fica a partir da gaveta tal". O nome idade só existe no seu código-fonte. Lá dentro do programa, só existem os números das gavetas.


1. Quantas gavetas cada tipo ocupa: sizeof

O operador sizeof ("size of", tamanho de) responde quantos bytes um tipo ocupa. Você já o viu em Tamanho dos Tipos; aqui vamos usá-lo para pensar em gavetas:

#include <stdio.h>

int main(void) {
    printf("char:      %zu byte\n", sizeof(char));
    printf("int:       %zu bytes\n", sizeof(int));
    printf("float:     %zu bytes\n", sizeof(float));
    printf("double:    %zu bytes\n", sizeof(double));
    printf("long:      %zu bytes\n", sizeof(long));
    printf("long long: %zu bytes\n", sizeof(long long));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra tamanhos.c -o tamanhos
$ ./tamanhos
char:      1 byte
int:       4 bytes
float:     4 bytes
double:    8 bytes
long:      8 bytes
long long: 8 bytes
sizeof(int)
Quantos bytes (gavetas) um int ocupa. Não é uma função: é um operador da linguagem, como o +. O compilador já sabe a resposta antes de o programa rodar.
%zu
O formato do printf para o resultado do sizeof, que é do tipo size_t (um inteiro sem sinal próprio para tamanhos). O z quer dizer "do tamanho de size_t" e o u, "unsigned", sem sinal.

O char sempre ocupa 1 byte, por definição. Os outros tamanhos dependem da máquina e do compilador. Os números acima são os de um Linux de 64 bits com o gcc, o caso mais comum hoje. No editor do guia, o long ocupa 4 bytes, porque o programa roda em WebAssembly de 32 bits (veja O Editor do Guia). A tabela completa está em Tipos e Limites.

sizeof de uma variável

Também dá para perguntar o tamanho de uma variável. Com variável, os parênteses são opcionais (sizeof altura); com um tipo, são obrigatórios (sizeof(int)). Num array, a resposta é o tamanho de todos os itens juntos:

#include <stdio.h>

int main(void) {
    int idade = 29;
    double altura = 1.65;
    int notas[5] = {7, 8, 6, 9, 10};

    printf("idade:  %zu bytes\n", sizeof(idade));
    printf("altura: %zu bytes\n", sizeof altura);
    printf("notas:  %zu bytes\n", sizeof(notas));
    printf("itens:  %zu\n", sizeof(notas) / sizeof(notas[0]));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra sizeof-var.c -o sizeof-var
$ ./sizeof-var
idade:  4 bytes
altura: 8 bytes
notas:  20 bytes             « 5 itens × 4 bytes = 20 gavetas
itens:  5                    « 20 bytes ÷ 4 bytes por item

A conta sizeof(notas) / sizeof(notas[0]) (o tamanho total dividido pelo tamanho de um item) é o jeito clássico de descobrir quantos itens um array tem. Guarde bem: ela tem uma armadilha, que aparece em Ponteiros e Arrays.

Missão: o programa já mostra o tamanho de idade. Crie uma variável char inicial = 'A'; e mostre inicial ocupa 1 byte, usando sizeof(inicial) (não vale escrever o 1 direto no texto).

idade ocupa 4 bytes
inicial ocupa 1 byte
#include <stdio.h>

int main(void) {
    int idade = 29;

    printf("idade ocupa %zu bytes\n", sizeof(idade));
    // crie a variável inicial e mostre o tamanho dela

    return 0;
}
#include <stdio.h>

int main(void) {
    int idade = 29;
    char inicial = 'A';

    printf("idade ocupa %zu bytes\n", sizeof(idade));
    printf("inicial ocupa %zu byte\n", sizeof(inicial));

    return 0;
}

2. Onde a variável mora: o endereço &x

Colocar & antes do nome de uma variável pergunta: "qual é o número da sua gaveta?". O resultado é o endereço da variável. Para mostrá-lo com o printf, use o formato %p:

#include <stdio.h>

int main(void) {
    int idade = 29;
    double altura = 1.65;

    printf("idade  vale %d e mora em %p\n", idade, (void *) &idade);
    printf("altura vale %.2f e mora em %p\n", altura, (void *) &altura);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra endereco.c -o endereco
$ ./endereco
idade  vale 29 e mora em 0x7ffe6741f06c
altura vale 1.65 e mora em 0x7ffe6741f070

Vamos por partes, porque cada pedacinho de (void *) &idade tem um trabalho:

&idade
O operador de endereço: "o endereço de idade". Lê-se "e comercial idade" ou "endereço de idade". O & grudado no nome de uma variável sempre quer dizer isso. (No meio de uma conta, como a && b ou a & b, é outra coisa: são os operadores lógicos e de bits.)
%p
O formato do printf para endereços ("pointer", ponteiro). Ele mostra o número em hexadecimal, com 0x na frente.
(void *)
Uma conversão para "endereço genérico". A regra do C diz que o %p espera exatamente esse tipo. Na prática, quase sempre funciona sem ela, mas escrever (void *) é o jeito correto e evita um aviso (veja "Quando dá errado"). Por enquanto, leia como uma fórmula: para mostrar um endereço, %p com (void *) na frente. A aula Ponteiro void* explica o porquê.
0x7ffe6741f06c
O número da gaveta. O 0x avisa que ele está em hexadecimal (base 16): os "algarismos" vão de 0 a 9 e depois de a (10) a f (15). Endereços são números enormes (este, em decimal, passa de 140 trilhões), e o hexadecimal os escreve mais curto. Você não precisa converter: endereços servem para comparar, não para fazer contas de cabeça.
Endereço (gaveta)…06c…06d…06e…06f…070…071……077
Quem moraidade (4 bytes) = 29altura (8 bytes) = 1.65
As gavetas da execução acima. &idade é …06c, a primeira das 4 gavetas de idade. Contando em hexadecimal, depois de 06c vêm 06d, 06e, 06f e 070: a altura começa exatamente 4 gavetas depois.

Rode de novo: o endereço muda

Rode o mesmo programa outra vez, sem mudar nada:

$ ./endereco
idade  vale 29 e mora em 0x7fff1480758c
altura vale 1.65 e mora em 0x7fff14807590

Os valores são os mesmos, mas as gavetas são outras. Isso é normal e proposital. A cada execução, o Linux sorteia em que parte da memória as regiões do programa começam. Essa proteção se chama ASLR (Address Space Layout Randomization, "embaralhamento do mapa da memória") e atrapalha quem tenta invadir um programa adivinhando onde estão as coisas. Repare que a distância entre as duas variáveis continua a mesma (de …58c para …590 são 4 gavetas): o sorteio muda o ponto de partida, não a arrumação.

Consequência prática Quando você rodar os exemplos desta aula, vai ver outros números. Está certo. Nunca escreva um endereço fixo no seu código, e nunca espere que o mesmo endereço apareça duas vezes.

E no editor do guia?

O editor roda o programa em WebAssembly, dentro do navegador, que organiza a memória do seu próprio jeito. Este mesmo endereco.c, no editor, mostra:

idade  vale 29 e mora em 0x7ffff8
altura vale 1.65 e mora em 0x7ffff0

Três diferenças: os números são bem menores (o WebAssembly do editor é de 32 bits); eles não mudam de uma execução para outra (lá não existe ASLR); e desta vez a altura ficou antes da idade. O compilador escolhe a ordem e a posição das variáveis como achar melhor. A ideia é a mesma: cada variável tem a sua gaveta, e & mostra qual é.

Experimente: rode e veja onde as variáveis moram. Clique em Rodar de novo e compare. Depois crie uma variável char inicial = 'A'; e mostre o endereço dela também, com %p e (void *) &inicial.

#include <stdio.h>

int main(void) {
    int idade = 29;
    double altura = 1.65;

    printf("idade:  %zu bytes, mora em %p\n", sizeof(idade), (void *) &idade);
    printf("altura: %zu bytes, mora em %p\n", sizeof(altura), (void *) &altura);
    return 0;
}

3. Num array, as gavetas são vizinhas

Com variáveis soltas, o compilador pode deixar espaço entre elas ou inverter a ordem. Com um array, a regra é fixa: os itens moram sempre colados, um depois do outro, na ordem dos índices:

#include <stdio.h>

int main(void) {
    int notas[3] = {7, 8, 6};

    printf("notas[0] mora em %p\n", (void *) &notas[0]);
    printf("notas[1] mora em %p\n", (void *) &notas[1]);
    printf("notas[2] mora em %p\n", (void *) &notas[2]);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vizinhos.c -o vizinhos
$ ./vizinhos
notas[0] mora em 0x7fff83bc1eec
notas[1] mora em 0x7fff83bc1ef0     « 4 gavetas depois (eec → ef0)
notas[2] mora em 0x7fff83bc1ef4     « mais 4 gavetas

&notas[1] quer dizer "o endereço do item 1". Os colchetes são resolvidos primeiro: pega-se o item, e o & pergunta onde ele mora.

…eec7…ef08…ef46notas[0]notas[1]notas[2]
Os três itens do array, colados. Cada caixa representa as 4 gavetas de um int; acima dela está o endereço da primeira. Sempre 4 a mais que o anterior.

Essa "fileira sem buracos" é o que vai permitir andar de item em item com ponteiros, na aula Ponteiros e Arrays.


4. O & que você já conhecia: o do scanf

Desde a aula Entrada com scanf você escreve scanf("%d", &idade), com um & que parecia um enfeite obrigatório. Agora dá para entender: o scanf precisa saber em que gaveta guardar o número que a pessoa digitou. Você entrega o endereço, e ele coloca o valor lá dentro:

#include <stdio.h>

int main(void) {
    int idade = 0;

    printf("idade mora em %p\n", (void *) &idade);
    printf("Quantos anos você tem? ");
    scanf("%d", &idade);
    printf("Guardei %d na gaveta %p\n", idade, (void *) &idade);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra idade.c -o idade
$ ./idade
idade mora em 0x7ffe577a5224
Quantos anos você tem? 29          « a Ana digitou 29 e Enter
Guardei 29 na gaveta 0x7ffe577a5224

A gaveta é a mesma antes e depois: o scanf não criou variável nenhuma, só escreveu dentro da gaveta que você indicou. É como pedir a um entregador: "deixe o pacote na caixa postal 5224". Sem o número da caixa, ele não tem onde deixar. A aula Passar por Ponteiro mostra como escrever as suas próprias funções que funcionam assim.


5. Um endereço também ocupa gavetas

O endereço é um número, e números precisam de espaço para ser guardados. Quantos bytes ocupa um endereço?

#include <stdio.h>

int main(void) {
    char inicial = 'A';
    int idade = 29;
    double altura = 1.65;

    printf("&inicial ocupa %zu bytes\n", sizeof(&inicial));
    printf("&idade   ocupa %zu bytes\n", sizeof(&idade));
    printf("&altura  ocupa %zu bytes\n", sizeof(&altura));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra tamanho-endereco.c -o tamanho-endereco
$ ./tamanho-endereco
&inicial ocupa 8 bytes
&idade   ocupa 8 bytes
&altura  ocupa 8 bytes

Não importa se a variável tem 1, 4 ou 8 bytes: o número da gaveta dela sempre ocupa 8 bytes num Linux de 64 bits. (É por isso que se fala em "64 bits": 8 bytes × 8 bits.) No editor do guia, que é de 32 bits, a resposta é 4. Guardar esse número numa variável é exatamente o que um ponteiro faz, e é o assunto da próxima aula.


Formas de mostrar um endereço: qual usar

FormaO que aconteceUse?
printf("%p", (void *) &x)Mostra o endereço em hexadecimal. É o jeito que o padrão do C garante.Sim, sempre.
printf("%p", &x)Funciona no gcc e no Clang, mas não segue a regra à risca. Com -Wpedantic, o gcc avisa.Evite.
printf("%d", &x)Errado: %d é para int. O número sai cortado ou sem sentido, e o -Wall avisa.Nunca.

Quando dá errado

Endereço com %d: "format ‘%d’ expects argument of type ‘int’"
#include <stdio.h>

int main(void) {
    int idade = 29;
    printf("Com %%p sem (void *): %p\n", &idade);
    printf("Com %%d: %d\n", &idade);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra semcast.c -o semcast
semcast.c: In function ‘main’:
semcast.c:6:23: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘int *’ [-Wformat=]
    6 |     printf("Com %%d: %d\n", &idade);
      |                      ~^     ~~~~~~
      |                       |     |
      |                       int   int *
      |                      %ls
$ ./semcast
Com %p sem (void *): 0x7ffd7fd607a4
Com %d: 2144733092

O que significa: "o formato %d espera um argumento do tipo int, mas o argumento 2 é do tipo int *". O int * é o nome do tipo de &idade: "endereço de um int" (você vai conhecê-lo na próxima aula). O programa até roda, mas o %d pega só um pedaço do endereço de 8 bytes: o 2144733092 é a parte final, 7fd607a4, escrita em decimal. A sugestão %ls do gcc não ajuda aqui; ignore. (O %% dentro do texto é só o jeito de escrever um % de verdade, veja Formatos do printf.)

Como consertar: para endereços, use %p com (void *). Repare que a primeira linha, %p sem (void *), não deu aviso com -Wall -Wextra. Com -Wpedantic (a opção que cobra o padrão à risca), dá:

$ gcc -std=c23 -Wall -Wextra -Wpedantic semcast.c -o semcast
semcast.c: In function ‘main’:
semcast.c:5:36: warning: format ‘%p’ expects argument of type ‘void *’, but argument 2 has type ‘int *’ [-Wformat=]
    5 |     printf("Com %%p sem (void *): %p\n", &idade);
      |                                   ~^     ~~~~~~
      |                                    |     |
      |                                    |     int *
      |                                    void *
      |                                   %ls
semcast.c:6:23: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘int *’ [-Wformat=]
    6 |     printf("Com %%d: %d\n", &idade);
      |                      ~^     ~~~~~~
      |                       |     |
      |                       int   int *
      |                      %ls

No editor do guia, o Clang avisa do %d com outras palavras: format specifies type 'int' but the argument has type 'int *'.

"lvalue required as unary ‘&’ operand": endereço de uma conta
#include <stdio.h>

int main(void) {
    int idade = 29;
    printf("%p\n", (void *) &(idade + 1));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra lvalue.c -o lvalue
lvalue.c: In function ‘main’:
lvalue.c:5:29: error: lvalue required as unary ‘&’ operand
    5 |     printf("%p\n", (void *) &(idade + 1));
      |                             ^

O que significa: "é preciso um lvalue como operando do &". Um lvalue é algo que tem gaveta própria e pode ficar do lado esquerdo de um =: uma variável, um item de array. O resultado de idade + 1 (o número 30) é só um valor de passagem, que não mora em gaveta nenhuma; não tem endereço para mostrar. O mesmo vale para &29. No editor, o Clang diz: cannot take the address of an rvalue of type 'int' ("não dá para pegar o endereço de um valor de passagem do tipo int").

Como consertar: guarde o resultado numa variável (int depois = idade + 1;) e pegue o endereço dela: &depois.

"Os meus endereços são diferentes dos da aula"

O que aconteceu: nada de errado. Por causa do ASLR, os endereços mudam a cada execução, em qualquer computador. No editor do guia, eles têm outro formato e não mudam, porque o programa roda em WebAssembly.

Como resolver: não há o que resolver. Compare endereços do mesmo programa, na mesma execução (a distância entre eles), nunca o número em si.


Referência rápida

Código ou termoO que é
byteA menor "gaveta" da memória com número próprio (8 bits, valores de 0 a 255).
endereçoO número de um byte. O endereço de uma variável é o da primeira gaveta que ela ocupa.
sizeof(tipo)Quantos bytes o tipo ocupa. sizeof(int) → 4.
sizeof variavelQuantos bytes a variável ocupa. Num array, o total de todos os itens.
%zuFormato do printf para o resultado do sizeof (size_t).
&xO endereço de x. Só funciona com o que tem gaveta própria (variável, item de array).
printf("%p", (void *) &x)Mostra o endereço de x em hexadecimal (0x…).
scanf("%d", &x)Entrega ao scanf o endereço de x, para ele guardar o número lá.
ASLRProteção do sistema que sorteia os endereços a cada execução.
Tamanho de um endereço8 bytes no Linux de 64 bits (laboratório); 4 bytes no editor do guia.

Exercícios

1. Tamanho de um tipo

Complete para mostrar quantos bytes um double ocupa:

printf("%zu\n", (double));

2. O endereço

Complete para mostrar o endereço da variável preco, do jeito correto:

printf("%p\n", (void *) );

3. O formato

Qual formato do printf mostra um endereço?

printf("\n", (void *) &idade);

4. Você rodou o mesmo programa duas vezes no Linux e o endereço de idade mudou. Por quê?

5. notas[0] é um int e mora em 0x1000. Onde mora notas[1]?

6. Por que o scanf("%d", &idade) precisa do &?

Missão final: quantos preços?

Missão: a lojinha da Ana guarda 6 preços num array de double. Sem escrever os números 48 e 6 no printf, use sizeof para mostrar quantos bytes o array ocupa e quantos itens ele tem:

Bytes: 48
Itens: 6
#include <stdio.h>

int main(void) {
    double precos[6] = {4.5, 3.0, 12.9, 7.25, 1.99, 5.0};

    // mostre "Bytes: " e o tamanho total do array
    // mostre "Itens: " e o total dividido pelo tamanho de um item

    return 0;
}
#include <stdio.h>

int main(void) {
    double precos[6] = {4.5, 3.0, 12.9, 7.25, 1.99, 5.0};

    printf("Bytes: %zu\n", sizeof(precos));
    printf("Itens: %zu\n", sizeof(precos) / sizeof(precos[0]));

    return 0;
}

Resumo

  • A memória é uma fileira de bytes (gavetas); cada byte tem um endereço (o número da gaveta).
  • sizeof diz quantos bytes um tipo ou variável ocupa: char 1, int 4, double 8. Num array, é o total.
  • &x é o endereço de x: o número da primeira gaveta que ela ocupa. Mostre com printf("%p", (void *) &x).
  • No Linux, o endereço muda a cada execução (ASLR). No editor do guia, é um número pequeno que não muda. Compare distâncias, nunca o número.
  • Os itens de um array moram em gavetas vizinhas, na ordem.
  • O & do scanf entrega o endereço da variável, para ele saber onde guardar.
  • Um endereço ocupa 8 bytes no Linux de 64 bits (4 no editor). Próxima aula: Ponteiros, variáveis que guardam um endereço.