Memória

O funcionamento do garbage collector no .NET

A CLR – o ambiente de execução do .NET Framework – é um ambiente gerenciado e, por isso, não precisamos nos preocupar diretamente com questões relacionadas a liberação de memória. Isso é possível por causa de um componente importantíssimo da CLR: trata-se do garbage collector. O garbage collector tem exatamente a responsabilidade de lidar com a liberação da memória associada a uma variável e/ou objeto quando este não é mais utilizado em nenhum ponto do nosso código.

Entender como o garbage collector funciona no .NET é essencial para que possamos entender como um todo como a CLR funciona. Questões relacionadas ao garbage collector também são muito úteis quando precisamos escrever código de alta performance para aplicações que exigem respostas próximas ao real-time “de verdade”. Por isso, vamos entender como o garbage collector funciona na CLR.

Como funciona o gerenciamento de memória no .NET?

No artigo C# Gerenciamento de memória no C#: stack, heap, value-types e reference-types que escrevi aqui para o blog da TreinaWeb, apresentei as duas principais divisões da área de memória durante a execução de uma aplicação .NET: a stack, para os value-types; e a heap, para os reference-types. Dependendo do tipo de “dado” que você aloca em uma variável, o dado pode ser deslocado ou para a stack – se estivermos falando de structs – ou para a heap – se estivermos falando de tipos ditos “complexos”, como objetos.

Formação:
CONHEÇA A FORMAÇÃO

Quando falamos sobre o garbage collector, é importante frisar que este atua organizando a memória na heap, embora ele utilize as referências guardadas na stack para determinar o que ainda está em uso ou não. Os objetos que são alocados na heap acabam sendo divididos em três grupos, também chamados de gerações:

  • geração 0: é a primeira geração onde um objeto é alocado. Assim que algo é alocado na heap,o objeto alocado é colocado imediatamente como sendo parte da geração 0;
  • geração 1: trata-se de uma geração de transição, onde objetos que são utilizados de maneira “média” são alocados;
  • geração 2: é uma geração que contém objetos que são utilizados por mais tempo e que, por isso, precisam existir na memória por um tempo maior.

O que o garbage collector faz basicamente é migrar os objetos entre estas três áreas distintas e eliminar as áreas de memória associadas a objetos que não são mais utilizados.

O ciclo de trabalho do garbage collector na heap

O garbage collector trabalha em ciclos de análise sobre as gerações que existem na heap. O que diferencia o funcionamento do garbage collector sobre as gerações é a peridiocidade da inspeção: a geração 0, por ser menor na maioria das vezes, sofre análises do garbage collector mais frequentes do que as gerações 1 e 2 por exemplo. Nestas análises, o que o garbage collector faz é verificar se os integrantes das gerações estão ainda sendo utilizados e, caso algum integrante não seja mais necessário, este é removido, fazendo com que a área de memória correspondente seja liberada e fique disponível para novas alocações.

É importante notar também que os ciclos de análise ocorrem na geração que é alvo do ciclo e nas gerações anteriores. Por exemplo: se o garbage collector precisa analisar a geração 0, somente ela é analisada. Se o garbage collector precisa analisar a geração 1, as gerações 1 e 0 são analisadas. Se o garbage collector precisa analisar a geração 2, as gerações 2, 1 e 0 são analisadas. Em decorrência desse funcionamento, o ciclo de análise na geração 2 ganha o nome de coleta completa, pois todas as gerações acabam sendo analisadas.

Essa análise do garbage collector pode ocorrer em situações pré-definidas:

  • Quando o sistema operacional informa que possui pouca memória física disponível;
  • O tamanho das gerações é estourado;
  • O método GC.Collect() é invocado, o que caracteriza uma chamada explícita para o processo de análise do garbage collector dentro da aplicação.

Promoções de geração em objetos

Os objetos são inicialmente alocados na geração 0 – portanto, na geração que sofre coletas mais rápidas – porque a CLR supõe que estes objetos não serão mais necessários muito rapidamente, o que faria com que estes objetos fossem removidos rapidamente da memória. E isso é verdade para a maioria dos cenários. Porém, alguns objetos podem precisar sobreviver por mais de um ciclo de análise do garbage collector… Quando o garbage collector detecta um objeto que precisa sobreviver por mais tempo na memória do que o esperado para a geração onde ele se encontra, o objeto sofre o que é chamado de promoção: o objeto é deslocado para a geração superior, sofrendo ciclos mais espaçados de análise do garbage collector. Por exemplo: se um objeto que está na geração 0 sobrevive a um ciclo de análise, o mesmo é deslocado para a geração 1. Se um objeto sobrevive a um ciclo de análise sob a geração 1, o mesmo é deslocado para a geração 2, sofrendo a análise do garbage collector de maneira mais espaçada ainda.

Esse processo de promoção pode tornar a gerência de memória um processo muito lento, já que esse deslocamento de gerações pode exigir uma quantidade de processamento computacional considerável, dependendo da quantidade de objetos alocados… Por isso, o CLR sempre está “supervisionando” o trabalho do garbage collector. Se o garbage collector passa a detectar que a taxa de sobrevivência de objetos em uma determinada geração é muito alta, a CLR aumenta o tamanho da geração em questão, evitando que as gerações tenham seu tamanho estourado frequentemente. A CLR sempre tenta equilibrar os ciclos de análise do garbage collector e o tamanho das gerações – as gerações também não podem ser sempre expandidas, pois isso deixaria o sistema operacional sem memória em um curto espaço de tempo.

Áreas efêmeras

O CLR e o garbage collector supõe que os objetos que fazem parte das gerações 0 e 1 terão um ciclo de vida muito curto, sendo eliminados rapidamente da memória. Por isso, estas gerações também são chamadas de gerações efêmeras.

As gerações também costumam ser agrupadas em segmentos de memória, segmentos estes que são gerenciados pelo garbage collector. Segmentos que possuem as gerações 0 e 1 também são chamados de segmentos efêmeros.

O garbage collector também realiza a manipulação destes segmentos. Os segmentos recém-criados também são definidos como segmentos efêmeros. Quando o garbage collector cria mais um novo segmento, o segmento que antes era efêmero deixa de ter essa característica, passando a ser considerado um segmento de geração 2 (a geração com objetos que sobrevivem por mais tempo). O novo segmento passa a ser considerado neste momento como o segmento efêmero.

No próximo artigo, iremos analisar em detalhes como o garbage collector realiza o trabalho de remoção e promoção de objetos entre os segmentos gerenciados pelo garbage collector.

Generators no PHP

Generators foram adicionados no PHP na versão 5.5 (meados de 2013) e aqui estamos nós, quase cinco anos depois, falando sobre eles. É que esse ainda não é um assunto muito difundido, digo, não é trivial encontrar casos e mais casos de uso para eles no contexto padrão de desenvolvimento para o PHP.

Primeiro de tudo, é importante entendermos o que são Iterators, ademais, Iterators e Generators são assuntos intrinsicamente relacionados.

Curso de
CONHEÇA O CURSO

O que é um Iterator?

No contexto prático do PHP, Iterator é um mecanismo que permite que um objeto seja iterado e ele próprio fica no controle dessa iteração. Mas o seu uso não limita a essa “feature”, é o que veremos mais pra frente.

Há de se destacar que é possível iterar um objeto “limpo” (digo, aquele que não implementa nenhuma interface específica) e o que será levado em consideração são os seus atributos públicos.

Um exemplo:

<?php

class Maiable
{
    public $from =  'email@domain.com';
    public $to = 'email@domain.com';
    public $subject = "Assunto";
    protected $type = 1;
}

$maiable = new Maiable();

foreach($maiable as $atributo => $valor) {
    echo "<strong>{$atributo}:</strong> {$valor} <br>";
}

O resultado será:

from: email@domain.com 
to: email@domain.com 
subject: Assunto 

No entanto, temos no PHP a Iterator, uma interface que quando implementada, os objetos provenientes ganham a capacidade de serem iterados com base nas seguintes assinaturas de métodos:

Iterator extends Traversable
{
    abstract public mixed current ( void )
    abstract public scalar key ( void )
    abstract public void next ( void )
    abstract public void rewind ( void )
    abstract public boolean valid ( void )
}
  • current() – Retorna elemento corrente;
  • key() – Obtêm a chave corrente;
  • next() – Avança o cursor para o próximo elemento;
  • rewind() – Retornar o cursor para o início;
  • valid() – Checa se a posição atual existe;

Vamos então a um exemplo:

class BookStore implements Iterator
{
    private $books = [];
    private $index;

    public function __construct()
    {
        $this->index = 0;

        $this->books = [
            'Book 1',
            'Book 2',
            'Book 3',
            'Book 4',
            'Book 5',
        ];
    }

    public function current()
    {
        return $this->books[$this->index];
    }

    public function key()
    {
        return $this->index;
    }

    public function next()
    {
        $this->index++;
    }

    public function rewind()
    {
        $this->index = 0;
    }

    public function valid()
    {
        return array_key_exists($this->index, $this->books);
    }
}

Nota: Esse é um exemplo puramente didático. Estamos inicializando o array de forma estática e ele por si só não justifica o uso de Iterator. A ideia aqui é passar a essência do mecanismo. Adiante veremos casos de usos mais proximos da realidade, usando Generators.

Observe que os atributos são protegidos e que mantemos no $index o “cursor” atual da iteração.

Uma possível forma de iterar sobre esse objeto:

$books = new BookStore();

while ($books->valid()) {
    echo "<strong>[{$books->key()}]</strong> = {$books->current()} <br>";

    $books->next();
}

Resultado:

[0] = Book 1 
[1] = Book 2 
[2] = Book 3 
[3] = Book 4 
[4] = Book 5 

Essa interface faz com que o nosso objeto tenha a capacidade de decidir como e o que iterar. E destaco esses termos em negrito pois eles são a essência dos interators.

Enquanto existir um índice válido a ser recuperado os valores serão impressos. Na última iteração, $books->next() incrementa em mais um o valor de $index chegando ao valor 5, no entanto, no array $books o maior índice existente é o 4, logo, o while é interrompido.

Outra forma possível seria:

for ($books->rewind(); $books->valid(); $books->next()) {
    echo "<strong>[{$books->key()}]</strong> = {$books->current()} <br>";
}

Essas são as formas mais “primitivas” de iterar o nosso objeto iterável. Estamos manualmente cuidando de avançar o cursor usando $books->next();.

Podemos facilitar isso se usarmos a estrutura foreach, ela reconhece quando o objeto implementa a interface Traversable e trata de avançar o cursor e recuperar os valores automaticamente.

A interface Iterator estende a Traversable.

A nossa implementação pode ser simplificada para:

$books = new BookStore();

foreach($books as $key => $value) {
    echo "<strong>[{$key}]</strong> = {$value} <br>";
}

Refatorando para ArrayIterator

O lado chato da abordagem de implementar diretamente a interface Iterator é a necessidade de implementar todos os seus métodos. É util quando a lógica para se iterar ou avançar o cursor precisa ser específica. Agora, se ela é trivial, como é o caso do nosso exemplo, podemos usar a classe ArrayIterator, também nativa do PHP.

A ArrayIterator implementa não só a interface Iterator como várias outras (que não fazem parte do escopo desse artigo):

ArrayIterator implements ArrayAccess, SeekableIterator, Countable, Serializable {

Ela recebe no seu construtor um array e itera sobre ele, abstraindo de nós a necessidade de implementar os métodos necessários da interface Iterator.

Podemos simplificar e alterar a nossa classe para implementar a interface IteratorAggregate que assina um único método, chamado getIterator(). E, nele, tudo o que temos que retornar é um Iterator, no caso, vamos retornar a instância da ArrayIterator:

class BookStore implements IteratorAggregate
{
    private $books;

    public function __construct()
    {
        $this->books = [
            'Book 1',
            'Book 2',
            'Book 3',
            'Book 4',
            'Book 5',
        ];
    }

    public function getIterator()
    {
        return new ArrayIterator($this->books);
    }
}

Qual o principal benefício de se usar um Iterator?

Os exemplos anteriores não tiveram o objetivo de explicitar isso, mas um grande benefício do uso de Iterators trata-se de um melhor aproveitamento da memória. Com eles, não precisamos carregar grandes datasets de uma só uma vez, podemos carregar item a item, sob demanda, quando preciso. Digamos, é uma implementação “lazy loading”.

Nos tópicos seguintes, sobre Generators, veremos alguns casos de uso mais reais e então entenderemos como os Iterators são importantes para lidar com grandes coleções de dados.

Curso de
CONHEÇA O CURSO

O que é um Generator?

Em termos práticos Generators são uma forma prática de se implementar Iterators. Isso quer dizer que, com Generators, podemos implementar complexos Iterators sem que precisemos criar objetos, implementar interfaces e toda àquela complexidade que vimos anteriormente. Generators dão para uma função a capacidade de retornar uma sequência de valores.

Para criar uma função Generator, basta que ela possua a palavra reservada yield. O operador yield é uma espécie de return, só que com algumas particularidades. E uma função desse tipo retorna um objeto da classe Generator, que é uma classe especial e específica para esse contexto, não sendo possível utilizá-la de outra forma. Esse objeto retornado pode ser iterado. É aí que entra a nossa base de Iterators que aprendemos anteriormente. A classe Generator implementa a interface Iterator.

Bom, vamos praticar um pouco? O exemplo mais elementar possível de uma função generator:

function getLinhas() {
    yield "Linha 1";
    yield "Linha 2";
    yield "Linha 3";
    yield "Linha 4";
    yield "Linha 5";
}

var_dump(getLinhas());

O resultado:

object(Generator)#1 (0) { }

Já confirmamos o que anteriormente foi explicado. Sempre que uma função usa o operador yield ela vai retornar um objeto do tipo Generator.

E, se Generator é um Iterator, logo, podemos iterar sobre ele, certo? Sim!

Vejamos:

<?php

function getLinhas() {
    yield "Linha 1";
    yield "Linha 2";
    yield "Linha 3";
    yield "Linha 4";
    yield "Linha 5";
}

foreach (getLinhas() as $linha) {
    echo "{$linha} <br>";
}

Resultado:

Linha 1 
Linha 2 
Linha 3 
Linha 4 
Linha 5 

Não precisamos de um yield para cada registro da nossa coleção. Normalmente o que vamos ver é um yield dentro de um laço como, por exemplo:

<?php

function getLinhas() {
    for ($i = 0; $i < 100; $i++) {
        yield "Linha {$i}";
    }
}

foreach (getLinhas() as $linha) {
    echo "{$linha} <br>";
}

E é possível que o yield retorne um conjunto de chave => valor:

function getLinhas() {
    for ($i = 0; $i < 100; $i++) {
        yield $i => 'Linha ' . $i * 2;
    }
}

foreach (getLinhas() as $chave => $valor) {
    echo "[{$chave}] => {$valor} <br>";
}

E a parte que “toca” a memória?

Comentamos anteriormente que o principal benefício de se usar um Iterator está no baixo consumo de memória associado. Com um Iterator recuperamos a informação sob demanda, sem alocar toda a coleção na memória.

É relativamente comum aplicações que recuperam milhares de dados de uma base de dados (quando não podem por algum requisito usar paginação) precisarem carregar esses dados em um array e em seguida formatar esses dados carregando-os em um novo array. O pico de consumo de memória nesses casos pode ser altíssimo.

Vamos emular uma situação onde possamos ver o benefício de usar Generators?

<?php

function getRegistros() {
    $registros = [];

    for ($i = 0; $i < 10000; $i++) {
        $registros[] = "Registro $i";
    }

    return $registros;
}

function formataRegistros($registros) {
    $new = [];

    foreach ($registros as $index => $registro) {
        $new[] = "[$index] -> {$registro}";
    }

    return $new;
}

$registros = formataRegistros(getRegistros());

echo 'Memória: ' . bcdiv(memory_get_peak_usage(), 1048576, 2) . ' MB<hr><br>';

foreach ($registros as $registro) {
    echo "{$registro} <br>";
}

A getRegistros() aloca na memória (em um array) milhares de registros e os retorna. A formataRegistros() recebe uma coleção de dados, formata-os, aloca-os na memória (em um array) e então os retorna.

O resultado da memória total gasta nessa operação foi:

Memória: 2.19 MB

Faça um paralelo disso com uma aplicação sua, real, que precisa recuperar milhares de registros, depois formatá-los de alguma maneira ou relacioná-los com outros registros etc.

Agora, refatorando o exemplo para que as duas funções se transformem em generators:

<?php

function getRegistros() {
    for ($i = 0; $i < 10000; $i++) {
        yield "Registro $i";
    }
}

function formataRegistros($registros) {
    foreach ($registros as $registro) {
        yield "-> {$registro}";
    }
}

$registros = formataRegistros(getRegistros());

echo 'Memória: ' . bcdiv(memory_get_peak_usage(), 1048576, 2) . ' MB<hr><br>';

foreach ($registros as $registro) {
    echo "{$registro} <br>";
}

O resultado:

Memória: 0.38 MB

Uma diferença muito grande. Quanto maior o dataset, maior a diferença será.

Outro possível caso de uso: Você precisa ler um documento de texto linha a linha e tratar essas informações de alguma maneira. Usando a forma tradicional de lidar com isso, teríamos algo como:

<?php

function getLinhas($arquivo) {
    $handle = fopen($arquivo, 'r');

    $linhas = [];

    while (($buffer = fgets($handle, 4096)) !== false) {
        $linhas[] = $buffer;
    }

    fclose($handle);

    return $linhas;
}

$linhas = getLinhas('file.txt');

foreach($linhas as $linha) {
    // TODO
}

echo 'Memória: ' . bcdiv(memory_get_peak_usage(), 1048576, 2) . ' MB';

Se o seu sistema é unix você pode gerar um arquivo com dados randômicos de 10MB (para testar o exemplo) executando:

base64 /dev/urandom | head -c 10000000 > file.txt

O resultado na minha máquina foi:

Memória: 19.56 MB

Se refatorarmos isso para um Generator:

<?php

function getLinhas($arquivo) {
    $handle = fopen($arquivo, 'r');

    while (($buffer = fgets($handle, 4096)) !== false) {
        yield $buffer;
    }

    fclose($handle);
}

$linhas = getLinhas('file.txt');

foreach($linhas as $linha) {
    // TODO
}

echo 'Memória: ' . bcdiv(memory_get_peak_usage(), 1048576, 2) . ' MB';

O resultado:

Memória: 0.37 MB

Veja que a diferença do consumo de memória entre as duas abordagens é enorme. Se o tamanho do arquivo que você está lendo ou precisa processar for muito grande você pode, inclusive, estourar o limite de memória do PHP se não usar um Iterator.

Com os Generators é possível que processemos arquivos de dezenas ou centenas de GB’s sem estourar o limite de memória disponível para a aplicação.

Concluindo

O uso de Generators é indicado e muitas vezes se faz necessário quando é preciso iterar uma grande coleção de dados de acesso sequencial.

Não são muitos os casos de uso para Generators na programação regular (digo, do dia a dia). Mas, certamente possuem o seu espaço. Generators são, inclusive, a base para programação assíncrona com PHP. E um framework que se destaca utilizando-os é o AMP.

Curso de
CONHEÇA O CURSO

Ponteiros em C, uma abordagem básica e inicial

Nesse artigo vou falar um pouco sobre o terror dos estudantes de programação, a verdadeira bruxa que come criancinhas em noite de lua cheia … Vou falar de ponteiros.

Mas calma, não se assuste! Vamos ver que esse assunto não é nenhum bicho de sete cabeças.

Pense em ponteiros como sendo aquele colega de trabalho “sacana” que não sabe nada a não ser apontar para você quando alguma pergunta é feita para ele. Quando seu chefe pergunta qualquer coisa o seu colega aponta para você responder, afinal é você quem têm na memória as informações.

Com ponteiros ocorre algo bem parecido, vamos ver uma explicação um pouco mais técnica sobre esse assunto.

Podemos dizer que ponteiros ou apontadores (também podemos nos referir a eles desta forma), são variáveis que armazenam endereços de memória.

Mas claro, não é qualquer endereço de memória, nossos ponteiros armazenam endereços de outras variáveis.

Então, veja que aquilo que chamamos de “apontar” na realidade é simplesmente a denominação que damos a um ponteiro que contém o endereço de uma variável qualquer (e de qualquer tipo).

Agora você deve estar se perguntando:

Por que devo aprender isso, qual é o grande benefício?

É simples: ponteiros são muito úteis quando temos uma situação em que uma variável precisa ser acessada em diferentes partes do programa.

Em um caso como esse o código pode ter vários ponteiros em diversas partes do programa apontando para uma variável específica.

E o melhor de tudo é que se o dado que estiver no local de memória apontado sofrer alguma alteração, não vai ter problema, pois os ponteiro espalhados no programa apontam para o endereço de memória e não exatamente para o valor.

Deu pra perceber como o uso de ponteiros ajuda o programador? Dificilmente você vai escrever um código com menos do que algumas dezenas até centenas de páginas e poder usar vários ponteiros em uma aplicação desse tipo é mais que uma mão na roda.

Curso de
CONHEÇA O CURSO

Afinal como declaro um ponteiro?

Depois de ver como um ponteiro pode melhorar sua qualidade de vida, você deve estar se perguntando como declarar uma maravilha dessa em seus códigos.

É simples. A sintaxe de um ponteiro é a seguinte:

tipo * nome_Ponteiro;

No exemplo acima temos o tipo que é o tipo de dado da variável que vamos apontar, podendo ser int, float ou até mesmo uma struct.

Depois temos o * (asterisco) que nesse caso determina que a variável é um ponteiro. E por fim temos “Nome_Ponteiro” que, como o próprio nome diz, é o nome do ponteiro.

Seguindo esses passos teremos a declaração de um ponteiro como o apresentado abaixo:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int * ptr;

    return EXIT_SUCCESS;
}

Mas claro, isso não é o suficiente para que possamos usar um ponteiro, a única coisa que fizemos foi declarar um ponteiro e nada mais.

Agora precisamos atribuir a ele um endereço de memória de uma variável qualquer do tipo int. Para fazer isso é necessário que criemos essa variável. Por exemplo:

int valor = 10;

Depois disso teremos o endereço de memória que será atribuído a nosso ponteiro, mas é claro essa atribuição não é simples. Ela precisa ser diferenciada e isso é feito usando o & (E comercial), com esse caractere conseguimos atribuir o endereço de memória de uma variável a um ponteiro.

Veja a sintaxe:

ponteiro = &valor;

Bem simples não é mesmo? Então vamos misturar tudo isso em um código para ver no que vai dar.

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int * ptr;
    int valor = 10;

    ptr = &valor;

    printf("Endereço = %x", &valor);
    printf("Endereço = %x", ptr);
    printf("Valor = %d", *ptr);

    return EXIT_SUCCESS;
}

Exemplo de um possível output para essa execução:

Endereço = 5015936c 
Endereço = 5015936c 
Valor = 10 

Veja que no código acima temos a estrutura de um código em linguagem C e nele criamos uma variável do tipo int chamada valor a quem atribuímos o valor 10.

int valor = 10;

Depois declaramos nosso ponteiro ptr e atribuímos a ele o endereço da variável valor.

int * ptr;

ptr = &valor;

Veja bem, ponteiros só aceitam endereços de memória. Não adianta tentarmos atribuir algum valor primitivo, por exemplo.

E para se obter o endereço de uma variável usamos o operador &. Foi o que fizemos.

Feito isso usamos printf() para exibir o valor do endereço da variável valor:

printf("Endereço = %x", &valor);
printf("Endereço = %x", ptr);

Se o ponteiro ptr está armazenando o endereço da variável valor, então quando imprimirmos o ponteiro ptr teremos o mesmo resultado (o mesmo endereço) que imprimir &valor (que retorna o endereço de memória da variável), não é verdade? Pois bem, foi isso que aconteceu. Viu o resultado da execução que mostramos ali em cima?

Endereço = 5015936c 
Endereço = 5015936c 
Valor = 10 

Por ultimo, exibimos o valor que existe na variável valor, tal valor que se acessado pelo ponteiro usamos a sintaxe *ptr.

printf("Valor = %d", *ptr);

Perceba que para acessar o endereço de memória é necessário duas coisas muito importantes:

  • Primeiro: dentro de printf() use %x para exibir o endereço de memória, pois o mesmo se trata de um valor hexadecimal.

  • Segundo: para acessar o endereço de memória de uma variável use & antes dela.

É possível ainda acessar o endereço de memória de um ponteiro e isso nada tem a ver com o endereço de memória da variável, para isso, assim como fizemos com a variável valor, podemos fazer:

printf("Endereço de memória do ponteiro = %x", &ptr);

Então, recapitulando, dentro de um printf() se utilizarmos:

  • ptr estaremos acessando o endereço de memória associado ao ponteiro. Ou seja, o endereço de memória de uma variável.

  • &ptr aí já estaremos acessando o endereço de memória do ponteiro.

Para acessar o conteúdo daquele endereço associado ao ponteiro é necessário mudar um pouco a abordagem.

  • Primeiro: dentro de printf() use %d para que seja possível mostrar um inteiro.
  • Segundo: use o operador * (que nesse caso nada tem a ver com multiplicação, tudo bem?) antes do ponteiro para acessar seu valor: *ptr.

Altere o exemplo para:

printf("Endereço = %x", &valor);
printf("Endereço = %x", ptr);

printf("Valor = %d", *ptr);
printf("Valor = %d", valor);

Reiterando:

  • *ptr – A variável ptr tem o endereço da variável valor, não é? É meio caminho andado para encontrar o valor dela, não acha? E para encontrar esse valor usamos o operador * antes do nome do ponteiro.
  • valor – Estamos explicitamente imprimindo o conteúdo dessa variável valor do tipo inteiro.

É isso aí pessoal! simples, não é mesmo? Mas tudo que parece ser simples pode ser complicado.

Imagine que você queira se aventurar um pouco mais e sair da mesmice de ponteiros simples, se você esta nessa fase da vida, continue lendo …

Ponteiro para ponteiros

Opa. Quem diria, você por aqui? Bom, se chegou até aqui é porque quer se aventurar, né? To sabendo.

Então vamos lá! Para entender ponteiros para ponteiros precisamos de uma situação da vida real.

Imagine que você foi para uma balada e encontrou uma pessoa legal, vocês conversaram e essa pessoa escreveu em um papel velho seu número de telefone. Você pegou o papel, mas como ele está sujo e meio engordurado você resolve pegar um papel limpo e anotar o telefone novamente (e você está sem celular!).

Esse processo pode ser identificado em programação como um ponteiro para ponteiro. O também chamado ponteiro do ponteiro é aquele ponteiro que aponta para outro ponteiro.

Doeu minha cabeça! Vamos ver a sintaxe para ficar mais fácil de entender?

int *ptr;
int **pptr;

Veja que acima declaramos um ponteiro comum com apenas um *asterisco e depois declaramos o ponteiro do ponteiro, que nesse caso, utiliza-se dois ** asteriscos.

Essa é a declaração, já a atribuição é a seguinte:

ptr = &valor;
pptr = &ptr;

Bem simples, enquanto o ponteiro simples aponta para uma variável, o ponteiro do ponteiro aponta para o ponteiro simples.

Vamos misturar tudo isso e ver no que dá:

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int * ptr;
    int ** pptr;

    int valor = 10;

    ptr = &valor;
    pptr = &ptr;

    printf("Endereço de ptr = %x", &ptr);
    printf("Endereço de pptr = %x", &pptr);

    printf("Valor ptr = %d", *ptr);
    printf("Valor pptr = %d", **pptr);

    return EXIT_SUCCESS;
}

Veja que no código acima declaramos os ponteiros da forma que já foi explicado e suas atribuições também foram feitas.

E no final exibimos os endereços e valores dos ponteiros. Veja que apesar de os ponteiros terem endereços diferentes, o valor apontado é o mesmo.

Muito legal, não é mesmo? Finalizo por aqui. Nos vemos nos próximos artigos.

Curso de
CONHEÇA O CURSO