image

Acesse bootcamps ilimitados e +650 cursos

50
%OFF
Article image
Rodrigo Weber
Rodrigo Weber10/10/2022 19:00
Compartilhe

Trabalhando com Ponteiros e entendo como funciona a memória do computador

    Introdução

    Ponteiros são o novo tipo de variável que iremos conhecer em C. Com certeza é um dos tópicos mais temidos por quem inicia na linguagem, mas, não se preocupe. Comigo você está em boas mãos. Ao final desse capítulo você estará dominando tanto ponteiros e gostando tanto deles que irá abordar até mesmo as pessoas na rua para explicá-las sobre como os ponteiros funcionam.

    Preparado(a)?

    image

    Declaração e atribuição de um ponteiro

    Um ponteiro possui um endereço e portanto será necessário que eu o declare numa variável.

    #include <stdio.h>
    
    int main(void)
    {
     int a;
    return (0);
    }
    

    Ao criarmos a variável a ela já está na nossa memória (na stack) ou seja, ela já possui um endereço pelo qual podemos acessá-la.

    Agora para criar a variável do tipo ponteiro, você precisa informar ao compilador qual tipo de variável se encontra naquele endereço.

    int *ptr;
    

    Pronto! Seu primeiro ponteiro foi criado, parabéns. Não doeu tanto né? Pois então, para atribuir o endereço da variável para a variável *ptr que você acabou de criar, simplesmente digite o seguinte:

    ptr = &a;
    

    Onde o & informa que você quer o endereço onde a variável está na memória. Ao mandar imprimir na tela com o comando:

    printf("%p", ptr);
    

    você terá como retorno algo parecido com 0x7fff58763b38. Esse é um número em hexadecimal que representa o espaço na memória RAM de seu computador que está sendo utilizado em seu computador pela variável a nesse momento.

    O código todo ficou assim:

    #include <stdio.h>
    
    int main(void)
    {
     int a;
     int *ptr;
    ptr = &a;
     print("%p\n", ptr);
     return (0); 
    }
    

    Faça um teste com esse seu programa. Execute ele algumas vezes e observe cada resultado de endereço que ele retorna. Como comentado acima, os programas em C usam memória RAM para rodar, sendo ela volátil (seu computador em nenhum momento para de executar tarefas em segundo plano), toda vez que você rodar seu programa ele irá utilizar uma região diferente de alocação da sua memória RAM.

    Assim como uma variável “normal”, o ponteiro também possui o seu próprio endereço na memória, portanto se declararmos uma variável assim:

    int **ptr2;
    
    ptr2 = &ptr;
    

    estamos criando o ponteiro para um ponteiro e atribuindo a ele o endereço de ptr. Simples né?

    Desreferenciação

    Um ponteiro tem o endereço de uma variável e o que nos interessa é ver o que há nesse endereço e poder modificar a variável que o ocupa. É para isso que usamos os ponteiros.

    #include <stdio.h>
    
    int main(void)
    {
     int a;
     int *ptr;
     
     a = 56;
     ptr = &a;
     printf("%d\n", *ptr);
     return (0);
    }
    

    Perceba os valores que estão dentro da função printf():

    • %d → mostra que iremos imprimir um dado do tipo inteiro. %p seria um ponteiro e %c um tipo char;
    • \n → equivale à uma quebra de linha em C;
    • *ptr → ao invés de colocarmos somente o ptr e o tipo %p como no primeiro exemplo que criamos, aqui nós usamos com *. Isso informa ao programa que ele deverá pegar não o endereço, mas o que está dentro da variável que está sendo apontada. Como nosso ponteiro está apontando para a variável a, é dela que ele irá extrair o valor que precisa.

    Agora sim, finalmente podemos entender o que é a tal desreferenciação.

    #include <stdio.h>
    
    int main(void)
    {
     int a;
     int *ptr;
    a = 56;
     ptr = &a;
     printf("%d\n", *ptr);
     *ptr = 42; 
     printf("%d\n", *ptr);
     return (0);
    }
    

    Ao compilar e executar o código acima temos como resultado:

    $ 56
    $ 42
    

    Mas por que isso acontece? Bom, assim como podemos acessar o endereço da memória de uma variável usando os ponteiros, você também pode alterar o valor da variável diretamente usando eles. Lembre-se, o código é lido pelo computador de cima para baixo. No primeiro printf() o valor de a é de 56. Já no segundo usamos *ptr = 42 para modificar a variável.

    Isso permite a você fazer coisas desse tipo:

    #include <stdio.h>
    
    int main(void)
    {
     int a;
     int *ptr;
     int **ptr2;
     int ***ptr3;
     int ****ptr4;
     int *****ptr5;
     int ******ptr6;
    a = 42;
     ptr = &a;
     ptr2 = &ptr;
     ptr3 = &ptr2;
     ptr4 = &ptr3;
     ptr5 = &ptr4;
     ptr6 = &ptr5;
     printf("%d\n", ******ptr6);
     return (0);
    }
    

    O retorno desse código será 42. Ele simplesmente vai fazendo referência com os endereços apontando até o conteúdo da variável a.

    image

    Aritmética de ponteiros

    Você pode fazer cálculos utilizando ponteiros, adicionando bytes ao final do endereço. Lembre-se o endereço da memória é dado por um número hexadecimal, para fazer os cálculos é necessário usar esse padrão.

    O sistema de números pelo qual estamos acostumados é o decimal (números de 0 a 9). O hexadecimal possui 16 números indo de 0 até 15 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f) a=10, b=11, c=12, d=13, e=14, f=15.

    Por exemplo: 30 + 16

    • no sistema decimal ficaria 46;
    • no sistema hexadecimal, 40;

    Aí você me pergunta: tá, mas como assim? Você ficou louco?

    E eu respondo: ainda não, mas estou quase. E quero que você enlouqueça junto comigo.

    image

    Lembre da sequência de números hexadecimais ali listados e acompanhe a soma. Caso eu fizesse 40 + 10 em hexadecimal não retornaria 50, mas sim 4a (lembre-se o a equivale ao número 10). Assim como 40 + 11 retorna 4b ((40 + 12 = 4c), (40 +13 = 4d), (40+ 14 = 4 ), (40 + 15 = 4f ) e (40 + 16 = 50)).

    #include <stdio.h>
    
    int main(void)
    {
     int b;
     int a;
     int *ptr;
    b = 23;
     ptr = &a;
     printf("%p\n", ptr);
     printf("%p\n", &b);
     printf("%p\n", ptr + 1);
     printf("%d\n", *(ptr + 1);
    return (0);
    }
    

    Esse programa retorna:

    • 0x7fff56bddbb34
    • 0x7fff56bddbb38
    • 0x7fff56bddbb38
    • 23

    Por que isso acontece? Perceba os endereços de memória impressos. Quando adicionamos 1 ao ponteiro adicionamos 4 bytes ao final (4 porque números inteiros ocupam 4 bytes. Se fosse um char, seria 1 byte e um long, 8 bytes, por exemplo).

    Sabendo disso quando colocamos para imprimir o que está no endereço de memória seguinte, acessamos a memória dele e devolvemos o valor que ele contém.

    Muito louco né?

    Imagine o quanto podemos manipular nosso programa com isso.

    *(ptr + 1) = 78;
    

    Com o código acima podemos alterar o valor da variável b atribuindo o endereço de a e adicionando um inteiro (4 bytes). Fazendo isso o endereço sobe um e vai até a variável seguinte.

    Tabelas

    As tabelas e os ponteiros estão intimamente ligadas.

    #include <stdio.h>
    
    int main(void)
    {
     int tab[3];
    printf("%p\n", tab);
     return (0);
    }
    

    Ao executar esse programa temos como resultado algo parecido com: 0x7fff5c764b2c que é o endereço da memória no qual a variável tab está alocada. Perceba que no momento em que tab é criada, um espaço da memória é dedicada a ela (assim como os bytes seguintes para preencher o vetor). O endereço de tab pode ser chamado utilizando somente tab ou tab[0] (esse é o motivo que um array começa a contagem por zero, o que equivale a dizer a que distância o elemento está do início do vetor).

    #include <stdio.h>
    
    int main(void)
    {
     int tab[3];
     int *ptr;
    ptr = tab;
     printf("%p\n", tab);
     return (0);
    }
    

    Retorna o endereço da memória de tab, algo parecido com isso: 0x7fff5c764b2c.

    Isso permite fazer coisas interessantes no seu programa. Por exemplo:

    #include <stdio.h>
    
    int main(void)
    {
     int tab[3];
     int *ptr;
     tab[0] = 12;
     tab[1] = 13;
     ptr = tab;
     printf("%d\n", *tab);
     printf("%d\n", *(tab + 1));
     return (0);
    }
    

    O código acima retorna o seguinte:

    $ 12
    $ 13
    

    Outras formas de chamar os índices do vetor:

    • *(tab[1] + 2) = 20;
    • tab[1][2] = 18;

    As duas formas acima podem ser utilizadas para atribuir um valor ao terceiro elemento de tab (tab[2]).

    String de Caracteres

    Preciso começar explicando que string de caracteres é uma convenção feita por programadores. A string de caracteres NÃO existe no computador. Em C ela NÃO existe de forma nenhuma!!!

    #include <stdio.h>
    
    int main(void)
    {
     char c;
    c = '0';
     printf("%d\n", c);
     return (0);
    }
    

    Por estarmos usando %d temos como retorno o valor numérico da tabela ASCII referente ao caractere ‘0’ (zero) que é 48.

    Se ao invés disso você optasse por colocar:

    printf("%c\n", c);
    

    Você teria como retorno o zero como caractere ao invés de número.

    Uma string de caracteres em C termina com “\0” (barra zero), informando ao C o final da cadeia de caracteres.

    #include <stdio.h>
    
    int main(void)
    {
     char *str;
     
     str = "Basecamp";
     printf("%c %s\n", *str, str);
     return (0);
    }
    

    Quando eu escrevo lol é colocado em algum lugar da memória a letra l seguida das letras ol e com um \0 no final para informar o final da string. Ou seja, são 4 caracteres na memória.

    Você pode alterar os elementos de uma string da seguinte forma:

    #include <stdio.h>
    
    int main(void)
    {
     char str[] = "lol";
    str[0] = 'p';
     printf("%c %s\n", *str, str);
     return (0);
    

    Ao executar o programa acima você tem como retorno:

    $ p pol
    

    Utilização geral

    Ponteiro Nulo e usos gerais de ponteiros

    Pois então, estamos chegando ao final do nosso estudo de ponteiros. Muita coisa para digerir não? Mas você percebeu o potencial que há em dominá-los para poder manipular o uso da memória, alterar variáveis e tudo o mais.

    Animados para dominar mais alguns conceitos?

    image

    Então se segure que agora essa brincadeira fica ainda mais interessante. Vem comigo!!!

    Observe o programa abaixo:

    #include <stdio.h>
    
    void fct(int a)
    {
     a = a + 42;
    }
    int main(void)
    {
     int a;
     
     a = 42;
     printf("%d", a);
     fct(a);
     printf("%d", a);
     return (0); 
    }
    

    Ao executar o código acima, qual saída você acha que ele retorna?

    $ 42 84
    
    //ou
    $ 42 42
    

    Preste atenção agora, pois se você entender esse processo, dará um passo muito grande na linguagem C.

    Respondendo a pergunta acima, o retorno será 42 42. Mas por que isso acontece? Eu passei para a função o valor de a e ele foi alterado adicionando o valor 42. Na verdade, o que você passou para a função fct() foi uma cópia do valor, não a variável em si. Caso queira passar a variável, você precisa passar o endereço dela.

    fct(&a);      
    //lembre-se: o & na frente representa que você está usando o endereço
    

    Mas isso não é suficiente. Você ainda precisa alterar sua função fct() para que ela altere os valores dentro de a.

    void fct(int *a)
    {
     *a = *a + 42;
    }
    

    Esta linha acima diz:

    “Quero ver qual é o valor apontado por “a” (no caso o “a” da função main), colocando o valor dentro do valor apontado por “a” (por isso o * antes do “a”).”

    image

    Esse é um dos usos mais clássicos de ponteiros. Muito louco né?

    Aviso importante: se esse conteúdo está muito denso, dê um tempo para seu cérebro processar essas informações. Não se cobre tanto logo de início. É muito natural sentir-se perdido quando começamos a estudar algo novo.

    Ahh, ia quase me esquecendo. Temos o ponteiro nulo.

    #include <stdio.h>
    
    int main(void)
    {
     int *ptr;
     
     ptr = 0;
     return (0);
    }
    

    Perceba que coloquei um ponteiro apontando para 0. Não para uma variável que contenha o valor 0, mas para zero mesmo. Na verdade, não posso colocar nada em 0. Nunca terá nada lá. Há uma convenção que diz que se um ponteiro tem valor zero, ele é um ponteiro nulo.

    Não vamos entrar nisso a fundo agora, mas permite várias coisas, como, por exemplo, saber quais ponteiros estão apontando para algum elemento e quais não. Todos os que têm 0 ainda não tem local para onde ir e, os que têm endereço, é porque podemos ir até esse endereço.

    Ponteiro para void

    A palavra-chave void é utilizada para dizer a uma função não retornar nada ou que não pegue nenhum parâmetro. Você com isso pode tentar umas coisas meio doidas, tipo isso:

    #include <stdio.h>
    
    int main(void)
    {
     void *ptr;
     int *ptr_i;
     char *ptr_c;
     
     ptr = ptr_c;
     ptr = &ptr;
     ptr = ptr_i;
     ptr_c = ptr;
    return (0);
    }
    

    Parabéns!!! Você chegou ao final desse conteúdo sobre ponteiros. Mas você percebeu que sua jornada apenas começou. Lembre-se de uma frase que sempre uso quando estou me sentindo desmotivado:

    Apaixone-se pelo processo!

    Comemore cada etapa alcançada! Cada desafio vencido! Não se martirize tanto ao errar. Isso é extremamente normal. Você já venceu pelo simples fato de chegar até aqui. Repito: PARABÉNS!!!

    Bons estudos, e muito obrigado pela companhia, por disponibilizar seu ativo mais precioso nessa leitura: o seu tempo.

    Compartilhe
    Comentários (4)
    Wesley Lima
    Wesley Lima - 11/10/2022 15:16

    top

    Anderson Porto
    Anderson Porto - 19/10/2022 02:15

    ( norminette ok ) muito bom Rodrigo.

    Matheus Lima
    Matheus Lima - 10/10/2022 22:44

    Excelente apresentação de conteúdo e totalmente completo.

    JC

    Jacinaldo Coelho - 10/10/2022 20:02

    Ótima explicação parabéns gostei do conteúdo.