Ponteiros em C#

Nos dias em C que era a principal linguagem de programação, os ponteiros significavam programação e vice-versa. Agora, em linguagens mais sofisticadas como C#, os ponteiros são apenas um recurso fornecido, mas não é realmente incentivado.

Em uma linguagem moderna, o argumento é que você nunca precisará descer ao nível de ponteiros, porque isto simplesmente mostra que você está pensando em um nível muito primitivo e pode por em perigo a união com o hardware. Embora isto seja verdade, ainda há momentos quando em que você tem que interagir com o hardware de uma forma que só pode ser alcançada usando ponteiros. Além disso, há todos aqueles programas em C/C++ que usam ponteiros e precisam ser convertido para algo mais polido e seguro. Em resumo, você certamente deve saber sobre ponteiros mesmo esperando que você nunca irá realmente usa-los em um programa real. Se você acha que os ponteiros são essenciais para algo de baixo nível que você está tentando implementar, em seguida, também é importante que você saiba como implementá-los em um possível método seguro.

Referência, ponteiro e endereço

Primeiro precisamos esclarecer algumas confusões que existe entre os três termos “referência”, “ponteiro” e “endereço”. Começando como o mais básico “endereço”, é o número do endereço de algo que está armazenado. A maioria dos hardwares do computador atribui endereços usando um esquema simples de incremento a partir de algum valor e continuam até outro valor, por exemplo, 0-1000, e cada endereço corresponde a uma posição de memória capaz de armazenar alguns dados com um determinado número de bits. Esta ideia simples tornou-se cada vez mais complicada com a evolução do hardware e a introdução do mapeamento de memória de hardware. Agora o endereço que os dados são armazenados é mudado sem que eles sejam movidos, devido à utilização de hardware de conversão de endereços. Mesmo que o hardware não possa mudar o endereço, ele pode ser mudado quando o aplicativo for executado, pelo sistema operacional ou o sistema de coleta de lixo, que normalmente efetua algumas mudanças para o sistema ficar mais eficiente. O ponto é que embora uma vez que o endereço de memória era uma forma fixa e confiável de acesso, hoje é cercado por uma serie de problemas.

O “ponteiro” é a abstração de um endereço. Na sua forma mais básica, um ponteiro é simplesmente uma variável que pode ser usada para armazenar o endereço de alguma coisa. Você pode usar um ponteiro para acessar o índice do dado que ele está apontando – um processo chamado desreferenciamento. Você também pode utilizar o ponteiro para fazer operações aritméticas, que podem mudar sua localização no armazenamento ou memória. Ou seja, se na memória os objetos são armazenados um após o outro, você pode executar a “aritmética de ponteiro” para alterar o objeto que está sendo apontado. Aritméticas de ponteiro é a razão pela qual, muitos programadores gostam de ponteiros, mas também é a razão pela qual os ponteiros são perigosos. Um erro no cálculo pode resultar em que o ponteiro aponte para um lugar onde ele não deve estar apontando e todo o sistema pode falhar com o resultado. Não há realmente nenhuma razão para que um ponteiro não deva ser basicamente a abstração de um endereço, mas na maioria dos casos, os ponteiros são apenas invólucros para endereços da máquina e isto também levanta a questão sobre o que acontece se o sistema fizer algo que mude o endereço de um objeto. Veremos mais sobre isso posteriormente.

Finalmente chegamos ao ponto mais alto da abstração de endereço na forma de uma “referência”. Uma referência é apenas isso – uma referência a um item de dados ou um objeto. Se isso soa como um ponteiro, há um sentindo, já que isto é verdade, mas a ideia fundamental é que você não pode manipular uma referência diretamente. Ou seja, enquanto há certamente uma aritmética de ponteiros, não pode haver uma aritmética de referência. Tudo o que você pode fazer com uma referência é passá-la para outro usuário ou referenciá-lo e acessar os dados que ele faz referência. Como no caso dos ponteiros, não há razão para que uma referência, não deve ser uma abstração do endereço da máquina subjacente, mas na maioria dos casos, e em C#, em particular, a referência é apenas um invólucro para um endereço. Em futuras implementações, contudo, uma referencia pode ser implementada como um identificador para uma tabela, em outra tabela, a um índice de recursos e assim por diante, até que finalmente o hardware converta a referência em um endereço de objeto dados reais. O ponto é que, enquanto sabemos que é uma referência, e para que serve um ponteiro, em C#, eles são simplesmente um invólucro para um endereço e este é um detalhe de implementação, e não algo que você deva se apegar.

Ponteiros

Em C# podemos fazer uso de referências o tempo todo, sob a forma de variáveis que são atribuídas qualquer tipo de referência. Por exemplo, se você criar uma instância de uma classe, a variável de instância não é um objeto, mas uma referência a um objeto. Isto é:


        MyClass MyObject = new MyClass();

Então MyObject é uma referência a uma instância de MyClass. Na prática, contém o endereço da ocorrência, mas como já foi explicado, você não deve contar com essa forma de execução. Este armazenamento de uma referencia no MyObjeto invés de um valor é mais evidente se você criar uma outra variável que referencia o mesmo objeto de MyObject:


        MyClass MyObject2 = MyObject;

Naturalmente, agora nós não temos outra cópia completa e independente do objeto referenciado porMyObject, ao invés de referência, temos a variável na mesma instância. Se você fizer uma alteração na instância usando MyObject2, as mesmas alterações serão encontradas através de MyObject. A diferença entre uma referência e um valor, basicamente se resume a esta semântica, a referencia não cria uma copia dos dados/objetos originais. Se isso acontecer, então temos um valor semântico, se não temos semântica de referência.

Os ponteiros são uma generalização de um modelo de referência para incluir a aritmética de ponteiro. Ponteiros são tão perigosos que devem ser colocados em quarentena dentro do seu código. Primeiro, o projeto tem que ser marcado como não seguro (usafe) usando as propriedades do projeto para definir o Build como, Allow Usafe Code. Então, qualquer uso de ponteiros tem que ser incluído no blocousafe{} para marca-lo ainda mais claramente. Ainda mais restritivo é o fato de que você não pode usar a memória para fazer uso do ponteiro menos complicado. Essencialmente, você só pode criar um ponteiro para qualquer tipo de valor simples, por exemplo, intfloadcharenum, para outro ponteiro ou struct que não contenha outros tipos gerenciados. Então você não pode ter um ponteiro para um objeto, ou um delegate ou uma referência. Isto é bastante restritivo e basicamente, não permite que se criem ponteiros para qualquer coisa criada no heap (pilha de memória) ou algum objeto que usa uma gestão dinâmica de memória. No entanto, você pode ter um ponteiro para uma struct que contém tipo de valores simples e você pode criar ponteiros para arrays de tipos de valores simples. Você também pode ter um ponteiro do tipo void, ou seja, um ponteiro para um tipo desconhecido, mas para ser de alguma utilidade, por exemplo, para usar a aritmética de ponteiros, você precisa converter um ponteiro do tipo void para um ponteiro de um determinado tipo.

Para declarar um ponteiro em C#, usa-se a mesma sintaxe do C++:

tipo * variavel;

O asterisco (*) é o operador de referência ou operador indireto, e geralmente é usado em conjunto com o endereço do operador e que como o próprio nome sugere, retorna o endereço de uma variável (sua referência). Por exemplo:


unsafe
{
     int* pMyInt;
     int MyInt;
     pMyInt = &MyInt;
}

O código acima cria um ponteiro de inteiro, ou seja, pMyInt armazena no ponteiro o endereço do inteiro MyInt. A primeira coisa importante, é que um ponteiro não herda de um objeto e assim não há métodos associados a ele ou boxing e unboxing. Por exemplo, você não pode usar um método toString() para mostrar o valor do ponteiro.O que você pode fazer, é converter o ponteiro para um tipo simples e assim usar os métodos associados a este tipo, como no exemplo abaixo:


MessageBox.Show(((int)pMyInt).ToString());

Claro que isto pressupõe que no int caberá o ponteiro, ou seja, um endereço de memória.

O operador de referencia, também retorna os valores armazenados no endereço que o ponteiro está apontando. Por exemplo:


MessageBox.Show((*pMyInt).ToString());

O código acima mostra o conteúdo de MyInt. Um ponteiro pode ser nulo e se neste caso for aplicado o operador de referencia (*), gera uma exceção. Obviamente não faz sentido querer exibir o conteúdo de um ponteiro nulo, mas você sempre pode converter um ponteiro void e então evitar o engano. Em alguns casos, você pode produzir um “erro” de converter incorretamente um ponteiro. Por exemplo:


void* pMyData = &MyInt;
MessageBox.Show((*(double*)pMyData).ToString());

No código acima é definido um ponteiro nulo para apontar para um inteiro, ou seja, um número inteiro de 32 bits, em seguida, ele é convertido para um ponteiro de double, ou seja, double *, e finalmente é aplicado o operador de referência para retornar o valor apontado. Se você tentar fazer isso, verá que ele funciona, mas gera um resultado inesperado, porque o int original era de apenas 4 bytes de armazenamento e do double é de 8 bytes. De onde é que será adicionado os 4 bytes? A resposta é de um local de memória vizinho, um que você não seria capaz de acessar só apontando para o inteiro. Ler uma posição de memória que você não conhecida até pode ser seguro, mas quem sabe que efeitos isto pode trazer para o seu código ou sistema operacional. Por exemplo, tente:


int MyInt2 = 0;
int MyInt=1234;
void* pMyData = &MyInt;
*(double*)pMyData = 123456.789;
MessageBox.Show(MyInt2.ToString());

Você pode se surpreender ao descobrir que o valor de MyInt2 mudou e já não é mais zero, embora não seja atribuído um novo valor para ela dentro do programa. A explicação é simples, o armazenamento do  MyInt2 é alocado ao lado de MyInt e quando vamos atribuir um valor de 8 bytes para MyInt os4bytes extras substituem de MyInt2. Isto é claramente perigoso, inesperado e indesejado, geralmente por este tipo de comportamento que o código é considerado “inseguro”.

Um uso muito comum de ponteiros é para manipular a estrutura de um tipo de dado. Por exemplo, suponha que você deseje recuperar os quatro bytes que compõem um número inteiro de 32 bits:


int MyInt = 123456789;

Sempre podemos usar um ponteiro nulo para obter o endereço de qualquer variável:


void* MyPointer;
MyPointer = &MyInt;

Então podemos atribuí-lo a qualquer ponteiro de qualquer um dos tipos padrão e usar aritmética de ponteiro, como no exemplo abaixo, onde um byte, é convertido para um char:


byte* pMyByte = (byte*)MyPointer;
char MyChar = (char)*(pMyByte + 3);
MessageBox.Show(MyChar.ToString());

A razão porque não atribuímos diretamente para um ponteiro de char é que um char tem dois bytes de tamanho e estamos convertendo um inteiro ASCII de 4 bytes para caracteres Unicode.

Na maioria dos casos, são existem maneiras de ter acesso à estrutura interna dos tipos de dados comuns usando as classes Converter ou BitConvertor. No caso do BitConvertor o método GetBytespode ser usado para converter o int para um array de bytes e então qualquer um dos bytes pode ser convertida para um caractere usando a classe Convert:


Byte[] Bytes = BitConverter.GetBytes(MyInt);
MyChar = Convert.ToChar(Bytes[3]);

Múltipla referência

Se você tiver alguma dúvida, então esta é a hora de tirar todas. Porque se você entender como funciona um ponteiro, vai entender como lidar com ponteiro de ponteiro, e ponteiro de ponteiro de ponteiro, e assim por diante. Em teoria, isto é fácil, na prática não tão complicado como você deve pensar. Por exemplo:


int** ppMyInt;
int* pMyInt;
int MyInt=1234;
pMyInt = &MyInt;
ppMyInt = &pMyInt;
MessageBox.Show((**ppMyInt).ToString());

No código acima, declaramos um ponteiro de ponteiro, ou seja, ppMyInt e usamos ele para apontar para pMyInt. Para exibir o valor apontado por pMyInt, ou seja, o valor de MyInt, usamos dois níveis de referência, **ppMyInt. Neste caso a referência dupla é fácil de entender, mas em casos reais, ela pode se tornar difícil de trabalhar, quando você precisar de um ponteiro de ponteiro de ponteiro e assim por diante.

Ponteiros, arrays e fixed

Existe uma relação muito estreita entre ponteiros e arrays, realmente pode-se dizer que os ponteiros em linguagens como C e C++ foram introduzidos apenas para permitir que se criassem arrays. Em principio, o endereço do primeiro elemento de um array pode ser usado para localizar qualquer elemento do array, mas as coisas são um pouco mais complicadas. Por exemplo:


    int[] MyArray = new int[10];
    for (int i = 0; i < 10; i++)
        MyArray[i] = i;

    int* pMyArray = &MyArray[0];
    MessageBox.Show((*pMyArray).ToString());

Isso deve criar um ponteiro para o primeiro elemento do array, mas se você tentar executar, vai descobrir que na verdade obterá um erro, já que você não pode pegar o endereço de uma expressão não corrigida. A razão é que enquanto MyArray[0] é apenas uma variável int, o compilador sabe que oarray é um objeto gerenciado e pode ser movido a qualquer momento. Se você pegasse o endereça de um array e em seguida, o endereço se tornaria inútil. Para ter um endereço de um array, você tem que usar a palavra-chave fixed:


     fixed (declaração de ponteiro)
     {
          // instruções a serem realizadas
     }

Por exemplo:


     fixed (int* pMyArray= &MyArray[0])
     {
         MessageBox.Show((*pMyArray).ToString());
     }

O código acima irá mostrar o conteúdo do primeiro elemento do array. Você também pode usar o nome do array como uma abreviação para &MyArray[0]:


     fixed (int* pMyArray= MyArray)
     {
          MessageBox.Show((*pMyArray).ToString());
     }

Observe que o ponteiro declarado no fixed, deixa de existir assim que o fixed é executado, assim você não pode usar pMyArray fora do fixed.

Se você deseja acessar outros elementos do array, basta usar a aritmética de ponteiro:


     fixed (int* pMyArray= MyArray)
     {
          MessageBox.Show((*pMyArray+5).ToString());
     }

Ele mostra MyArray[5]. Observe que adicionamos 5 no endereço de inicio do array, realmente isto funciona porque o array é de inteiro, ou seja, estamos adicionando 5 inteiros no endereço do array. Mas e se o array fosse de double? É para esses casos que existe o operador sizeof, que retorna o valor do tamanho de qualquer tipo e isso é usado para descobrir o que acrescentar a um ponteiro. Ou seja, ponteiro+5 é traduzido para ponteiro+5*sizeof(tipoponteiro).

Para completar a conexão entre arrays e ponteiros você também pode usar a indexação de arraycomo atalho para aritmética de ponteiros. Isso porque, ponteiro[5] é um sinônimo de *ponteiro+5


      fixed (int* pMyArray = &MyArray[0])
      {
           MessageBox.Show(pMyArray[5].ToString());
      }

Há uma restrição sobre o uso do ponteiro declarado na declaração do fixed. Mas não há nenhum problema de fazer uma cópia dele:


      fixed (int* pMyArray = MyArray)
      {
           int* ptemp = pMyArray;
           MessageBox.Show((*++ptemp).ToString());
      }

Mostra o conteúdo de MyArry[1].

Este tipo de manipulador de ponteiro trabalha com arrays. Por exemplo:


     int[,] MyArray = new int[10,10];

     fixed (int* pMyArray= &MyArray[0,0])
     {
         for (int i = 0; i < 100; i++)
         {
              *(pMyArray + i) = i;
         }
     }

Inicializa um array bidimensional, acessando-o como um bloco linear de memória. Naturalmente, a ordem que o array é inicializado depende de como ele é armazenado na memória e deixo isto para execução do exemplo e veja se ele é armazenado como uma linha ou uma coluna. É esse tipo de truque, ou seja, o acesso a um array 2D como se fosse uma estrutura 1D, que fez (e ainda faz alguns) ponteiros tão atraentes.

Observe que você pode inicializar vários ponteiros dentro de uma instrução fixed, desde que todos eles sejam do mesmo tipo. Para inicializar vários ponteiros de tipos diferentes, você tem que usar declarações fixed aninhadas para cada tipo.

Structs

Agora é hora de olharmos para as structs (estruturas) e ponteiros para structs. Você deve imaginar que assim como array, para usar ponteiro para structs, temos que coloca-lo em um bloco fixed, mas você não precisa porque uma struct é um tipo de valor e é alocado na pilha. Então podemos usar assim:


      public struct MyStructType
      {
           public int x;
           public int y;
      };
      MyStructType MyStruct=new MyStructType();
      MyStructType* pMyStruct = &MyStruct;

Agora, como você acessa um campo usando um ponteiro? A coisa mais óbvia:


      (*pMyStruct).x = 1;

Ou seja, o ponteiro pode usar o habitual ponto. No entanto, em uma homenagem ao C++, você também pode escrever:


     pMyStruct->y = 2;
     MessageBox.Show(pMyStruct->x.ToString());

Ou seja, o -> desreferencia o ponteiro e seleciona o campo de uma vez.

Strings

Costuma-se dizer que você não pode ter um ponteiro para uma string, porque a string é um objeto gerenciado, mas assim como no array, você pode usar o fixed para transformar a string em um arrayde char e então você pode inicializar um ponteiro de char para o primeiro caractere na string. Por exemplo:


     string MyString = "Hello pointer world";
     fixed (char* pMyString = MyString)
     {
         MessageBox.Show((*(pMyString + 6)).ToString());
}

Cria uma sequencia de forma habitual, corrige-o e obtém um ponteiro char ao seu primeiro char. Então podemos executar a aritmética de ponteiros para acessar o sexto elemento na string.

A alocação de memória

Bem como trabalhar com tipo de valor que você pode criar seus próprios tipos de dados primitivos usando a pilha. A declaração stackalloc tipo[n] aloca n bytes para o tipo e retorna um ponteiro. Você não precisa se preocupar com o armazenamento, a pilha não é modificada, ou a coleta de lixo, enquanto as variáveis estão no escopo de execução. Você também não precisa se lembrar de desalocar a memória, porque a pilha é automaticamente limpa quando as variáveis saem do âmbito da aplicação, geralmente quando o método que as declarou retorna. Por exemplo:


int* pMyArray = stackalloc int[100];
pMyArray[10] = 34;
MessageBox.Show(pMyArray[10].ToString());

Aloca 100 números inteiros, ou seja, 400 bytes, na pilha e usa o ponteiro para armazenar 34 em 4bytes começando no byte 40 e exibe este valor.

Observe que usamos a indexação do array, para fazer a atribuição e para exibir o valor, exatamente com em um array padrão. No entanto, o bloco de memória é apenas um bloco de memória que você pode fazer o que quiser com ele. Por exemplo:

public struct MyStructType
{
    public int x;
    public int y;
};
MyStructType* pMyDataBlock = stackalloc MyStructType[1];
pMyDataBlock->x = 34;

Aloca uma strutc na pilha, mas isso é, aloca o tamanho do struct que possui dois números inteiros, ou seja, 8 bytes.

Podemos usar o ponteiro para a struct de forma habitual para definir e acessar um campo. Ou seja, podemos usar o bloco como se fosse uma estrutura, mas se quisermos podemos simplesmente tratá-lo como um bloco de 8 bytes e utilizá-lo como outras estruturas de dados. Por exemplo, se você quiser tratar os dados como um array de int, você pode:


*((int*)pMyDataBlock) = 36;

Esta expressão pode ser difícil de entender, então vamos por partes:


int* pMyArray = (int*)pMyDataBlock;
pMyArray[0] = 36;

Cria um ponteiro para inteiro, e usa indexação de array para acessar o primeiro elemento do array que é o mesmo do campo x da estrutura.

Observe que essa forma depende de como a estrutura é organizada em memória e atualmente, o compilador do C# armazena os campos na ordem em que eles são declarados. Uma serie de atributos podem ser usados para pedir ao compilador para usar layouts especiais de memória especial para uma struct, consulte Mastering structs in C# para mais informações, mas mesmo assim você está fazendo uso de detalhes de implementação que podem mudar e tornar o seu programa inválido.

O que é tudo isto?

Você pode estar animando para aprender tudo sobre ponteiros, mas como você deve ter percebido, eles não são uma boa ideia. Programadores C e C++ que mudaram para C# tendem a pensar em aplicar ponteiros, mas na prática quase nunca você vai precisar deles. Se você necessita fazer uma conversão rápida de um programa C/C++ que usa ponteiro, então pode ser aceitável usar ponteiros emC#, mas você deve considerar apenas como uma medida temporária até que você possa implementar tudo sem ponteiros.

A única vez que os ponteiros podem ser necessários é para fazer uso de chamadas de API no âmbito do subsistema de P/Invoke. Mesmo assim geralmente alternativas, e muitas vezes passando por referencia resolve o mesmo problema de ponteiro para ponteiro. As coisas ficam mais complicadas quando você precisa passar um ponteiro para um bloco de memória e neste caso você pode ter que usar fixed com um array ou alocar memória na pilha e passá-lo diretamente para a chamada da API.

Fonte: http://www.treinaweb.com.br/ler-artigo/25/ponteiros-em-c#

Deixe um comentário