Ponteiros

Last updated: October 6th, 2018

Memória

O que é a memória?

A memória RAM é o armazenamento primário do computador, onde ficam informações (dados e instruções) dos programas sendo atualmente executados pelo computador. Isso significa, entre outras coisas, que as variáveis dos nossos programas ficam armazenadas na memória durante a execução.

No nosso caso, podemos ver a memória como um grande array de endereços.Como a memória é byte addressed, cada endereço é referente a 1 byte (8 bits).

Representação da memória

Cada posição da memória guarda exatamente 1 byte, e é referenciada por um endereço. O endereço é representado em base hexadecimal.

Qual o tamanho da memória?

Calculamos o tamanho da memória a partir do tamanho dos endereços. No exemplo acima, um endereço era um número hexadecimal de 4 dígitos:

0xFF00

  • 0x indica que o número está em base hexadecimal.
  • Cada dígito hexadecimal equivale a 4 dígitos binários (veja tabela de bases).
  • Portanto, cada endereço do nosso exemplo possui 16 bits.

Como cada byte da memória precisa de um endereço correspondente, calculamos o tamanho da memória vendo quantos endereços diferentes a memória é capaz de endereçar. No exemplo, cada endereço possui 16 bits, que podem representar 216 endereços, ou seja, esta memória é de 64 KiB.

O que determina o tamanho da memória é o tipo de processador:

  • Processador x86 faz endereçamento com 32 bits. Portanto o tamanho máximo é 232 bytes = 4GiB
  • Processador x64 faz endereçamento com 48 bits. Portanto o tamanho máximo é 248 bytes = 256TiB

Como usamos a memória?

Podemos utilizar a memória para:

  • Utilizamos a memória toda vez que alocamos espaço para variáveis em nossos programas.
  • Ler valores de variáveis e escrever novos valores para elas. Os valores lidos e escritos são sempre bytes.

Tipos de Variáveis

O que está na memória?

Do ponto de vista da máquina, na memória estão armazenados apenas bits. Veja no exemplo, em que cada byte é representado em base hexadecimal.

Representação da memória com bytes povoados.

Já do ponto de vista do programador, o que está armazenado na memória depende do tipo de variável associado ao endereço que nos referimos.

  • Se avaliarmos o byte do endereço 0xFF00 como um char, ele será avaliado como '\n'
  • Se avaliarmos o byte no endereço 0xFF03 como um char, ele será avaliado como 'û'
  • Interpretando a sequência de 4 bytes a partir do endereço 0xFF01 como int, ela será avaliada como 1047548
  • Interpretando a sequência de 4 bytes a partir do endereço 0xFF00 como um float, ela será avaliada como 6.16598e-33

O tipo da variável define 3 coisas:

  1. Quantos bytes a variável ocupa na memória
  2. Como interpretar o padrão de bytes
  3. Operações aritméticas (veremos adiante)

Tipo Ponteiro

Conhecemos variáveis do tipo int, float, double, char, etc. Todos esses tipos de variáveis armazenam dados do programa. Agora vamos conhecer o tipo ponteiro, que armazena endereços de variáveis do programa. Elas são do tipo ponteiro porque apontam para um espaço na memória.

Os tipos ponteiros são tipos inteiros, uma vez que existe um número inteiro de posições da memória. Para declarar um ponteiro para um tipo, acrescentamos um asterisco (*) na frente do nome do tipo.

int* ip;       /* ponteiro para inteiro */
char* cp;      /* ponteiro para char */
float* fp;     /* ponteiro para float */

Exemplos

Exemplo de ponteiros
  • Interpretando a sequência de 2 bytes a partir do endereço 0xFF03 como um char* ela será avaliada como 0xFBFC
  • Interpretando a sequência de 2 bytes a partir do endereço 0xFF04 como um char* ela será avaliada como 0xFCE0
  • Interpretando a sequência de 2 bytes a partir do endereço 0xFF04 como um int* ela será avaliada como 0xFCE0
  • Interpretando a sequência de 2 bytes a partir do endereço 0xFF03 como um double* ela será avaliada como 0xFBFC
  • Interpretando a sequência de 2 bytes a partir do endereço 0xFF03 como um void* ela será avaliada como 0xFBFC
  • Interpretando a sequência de 2 bytes a partir do endereço 0xFF04 como um void* ela será avaliada como 0xFCEO

Tipo void*

void é um tipo especial. Não é possível criar variáveis do tipo void, porém, podemos criar variáveis do tipo ponteiro para void. A utilidade é armazenar endereços de variáveis de qualquer tipo

Na prática, isso quer dizer que uma variável do tipo void* pode armazenar o endereço de uma variável do tipo int, ou char, ou double, ou qualquer outro tipo.

Assim, podemos ter ponteiros para qualquer tipo, inclusive para outros ponteiros, isto é, podemos ter ponteiros para ponteiros. Eles são definidos da mesma forma que os ponteiros normais:

int i = 50; /* inteiro */
int* pi; /* ponteiro de inteiro */
int** ppi; /* ponteiro de ponteiro de inteiro */
void* ptr; /* ponteiro void */
void** pptr; /* ponteiro de ponteiro void */

Conclusões sobre ponteiros

  1. O espaço ocupado por um ponteiro de qualquer tipo é sempre o mesmo, e é o mesmo tamanho de um endereço definido pelo processador.
  2. A interpretação de um ponteiro de qualquer tipo é smepre a mesma, um endereço da memória.

Aritmética

Referenciação

Aprendemos que é possível criar variáveis que armazenam o endereço de outras variáveis. Como vou saber qual é o endereço de uma variável, para que eu possa armazená-lo?

Com o operador &. A linguagem C nos dá este operador que retorna o endereço de uma variável

int main() {
  int a = 50;
  int b = 10;
  int* pi = &a;
  int c = 520;
  int** ppi = π
  return 0;
}
Code Execution

Desreferenciação

Agora que já sabemos como armazenar os endereços das minhas variáveis. O que eu faço com eles?

Conhecendo o endereço de uma variável, é possível conhecer o seu valor sem usar seu nome diretamente!

E quando precisarei conhecer o valor de uma variável sem usar seu nome diretamente?

  • Quando eu conhecer ão o nome, mas apenas o endereço de uma variável, ou seja, em funções!

Através do operador *, fazemos o que chamamos de desreferenciação.

A peração de desreferenciar é auto-explicativa.

Quando possuímos um ponteiro para uma variável, costumamos dizer que possuímos uma referência para esta variável.

Se desreferenciamos um ponteiro, tiramos a referência e passamos a lidar com a variável propriamente dita.

Quando mandamos imprimir o valor de um ponteiro, o que vemos é um número escrito na base hexadecimal.

int main() {
    int x = 50;
    int y = 40;
    int *ptr = &x;
    printf("%x\n",ptr);
    return 0;
}
Code Execution

Ou seja, quando imprimimos o valor de um ponteiro, imprimimos o que ele significa: um endereço de memória.

Quando mandamos imprimir o valor de um ponteiro desreferenciado, o que vemos é o conteúdo da variável para a qual ele aponta.

int main() {
    int x = 50;
    int y = 40;
    int *ptr = &x;
    printf("%x\n",ptr);
    int valor = *ptr;
    printf("%d\n",valor);
    return 0;
}
Code Execution

Ou seja, ao desreferenciar um ponteiro, podemos tanto ler o valor da variável apontada, quanto escrever no valor da variável apontada.

int main() {
    int x = 50;
    int y = 40;
    int *ptr = &x;
    printf("%x\n",ptr);
    *ptr = 30;
    int valor = *ptr;
    printf("%d\n",valor);
    return 0;
}
Code Execution

Materiais Extras

Tabela de Bases

Decimal Hexadecimal Binário
00 0 0000
01 1 0001
02 2 0010
03 3 0011
04 4 0100
05 5 0101
06 6 0110
07 7 0111
08 8 1000
09 9 1001
10 A 1010
11 B 1011
12 C 1100
13 D 1101
14 E 1110
15 F 1111