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.
- 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
&xe mostrá-lo com%pe(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
&doscanf.
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 gavetas | Na memória |
|---|---|
| Uma gaveta | Um 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úmero | O endereço do byte. |
| Uma pasta grossa que precisa de 4 gavetas vizinhas | Uma 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. |
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
intocupa. 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
printfpara o resultado dosizeof, que é do tiposize_t(um inteiro sem sinal próprio para tamanhos). Ozquer dizer "do tamanho desize_t" e ou, "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, comoa && boua & b, é outra coisa: são os operadores lógicos e de bits.) - %p
- O formato do
printfpara endereços ("pointer", ponteiro). Ele mostra o número em hexadecimal, com0xna frente. - (void *)
- Uma conversão para "endereço genérico". A regra do C diz que o
%pespera 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,%pcom(void *)na frente. A aula Ponteiro void* explica o porquê. - 0x7ffe6741f06c
- O número da gaveta. O
0xavisa que ele está em hexadecimal (base 16): os "algarismos" vão de0a9e depois dea(10) af(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 mora | idade (4 bytes) = 29 | altura (8 bytes) = 1.65 | ||||||
&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.
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 *) ¬as[0]);
printf("notas[1] mora em %p\n", (void *) ¬as[1]);
printf("notas[2] mora em %p\n", (void *) ¬as[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
¬as[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.
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
| Forma | O que acontece | Use? |
|---|---|---|
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
%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 *'.
#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.
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 termo | O que é |
|---|---|
| byte | A menor "gaveta" da memória com número próprio (8 bits, valores de 0 a 255). |
| endereço | O 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 variavel | Quantos bytes a variável ocupa. Num array, o total de todos os itens. |
%zu | Formato do printf para o resultado do sizeof (size_t). |
&x | O 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á. |
| ASLR | Proteção do sistema que sorteia os endereços a cada execução. |
| Tamanho de um endereço | 8 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:
2. O endereço
Complete para mostrar o endereço da variável preco, do jeito correto:
3. O formato
Qual formato do printf mostra um endereço?
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).
sizeofdiz quantos bytes um tipo ou variável ocupa:char1,int4,double8. Num array, é o total.&xé o endereço dex: o número da primeira gaveta que ela ocupa. Mostre comprintf("%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
&doscanfentrega 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.