Desenvolvimento - C#

Usando ponteiros em C#

Neste artigo veremos como utilizar ponteiros na linguagem C#.

por Rodrigo Moreira



Apesar das semelhanças sintáticas, o C# se difere muito do C e do C++ com relação ao uso de ponteiros.  Enquanto as duas linguagens mais antigas oferecem a flexibilidade do uso de ponteiros explicitamente, o C# possui a comodidade dos objetos serem gerenciados pelo Garbage Collector. Mas e o que são os ponteiros? Porque e como eles são usados no C#?

Essas são algumas das perguntas que serão respondidas neste artigo.

Referências e Valores

Em primeiro lugar precisamos entender os conceitos de armazenamento “por valor” e “por referência”. Para isso, utilizaremos o código abaixo.

É fato que as variáveis de tipos primitivos armazenam o seu conteúdo “por valor”. Para provar isso, instanciamos duas variáveis nomeadas “i” e “j”. Depois de instanciadas, inicializamos a variável “i” com o valor 10 e a variável “j” com o valor que estiver armazenado em i. Como o armazenamento será por valor, o comportamento do computador será verificar o valor que existe na posição de memória onde a variável “i” está alocada e copiar para o endereço de memória onde a variável “j” está alocada. Sendo assim, ao incrementar a variável “j”, o valor da variável “i” não será alterado, já que a variável “j” apenas recebeu uma cópia do valor de “i”. A este armazenamento, onde apenas o valo da variável é copiado, chamamos de “armazenamento por valor”.

A seguir instanciamos dois objetos da classe “Teste”, nomeados “t1” e “t2”. Repare que ao instanciar t2, indicamos que t2 é o mesmo objeto que t1 (“Teste t2 = t1;”). Como objetos, são armazenados por referência, ao indicarmos que uma instância é igual à outra, o computador irá armazenar no espaço de memória reservado para t2 o mesmo endereço de memória em que t1 está inserido. Sendo assim, ao ser feita uma alteração em t1 a mesma será replicada em t2 e vice-versa.

Para concluir a explicação sobre referência e valor, execute o programa e veja o resultado a respeito das “cópias” por valor e referência.

Uma vez compreendidos os conceitos de armazenamento “por valor” e “por referência”, podemos dizer que uma das principais diferenças entre linguagens como C e C++” e linguagens como C# e Java é que em C e C++ para existência de referência é necessário a explícita utilização dos ponteiros (variáveis que armazenam endereços de memória) e em C# e Java, tudo isso fica a cargo das linguagens intermediárias. O grande motivo de não deixar para o programador a responsabilidade para gerenciar o uso de referência através dos ponteiros é evitar os diversos problemas que um ponteiro mal utilizado pode trazer. Exemplos disso são ponteiros que não foram esvaziados quando o endereço de memória apontado foi limpo e que poderá ter um valor inconsistente na hora da utilização, ponteiros sendo utilizados com valores nulos e ainda o acesso a endereços não autorizados na memória. Como nas linguagens modernas o uso de ponteiros é implícito, ninguém precisa perder seus cabelos por causa de um índice de vetor sendo utilizado fora do escopo. Além disso, como existe o Garbage Collector, quando o endereço de uma variável é limpo, o “ponteiro implícito” gerenciado pelo CLR é limpo também, evitando os perigosos “wild pointers”.

Mas, se em C# não precisamos nos preocupar com ponteiros, porque o artigo é sobre a utilização dos mesmos em C#?

Acontece que ao contrário de linguagens como o Java, o C# mesmo possuindo o suporte a ponteiros implícito, permite que os ponteiros sejam usados explícitamente. Como demonstrado anteriormente, não é necessário (e muito menos recomendado) utilizar ponteiros para realização de referência comum, porém existem cenários típicos para a utilização dos mesmos em C# como:

     Figura 1 – Habilitando a execução de código inseguro.

    Nota do Autor: Repare que o uso de instrução unsafe é fortemente não-recomendado a não ser que você realmente saiba o que está fazendo. É tão não-recomendado que é necessário até configuração extra no compilador. Tenha em mente que ao rodar um código unsafe, você estará abrindo mão de uma série de features importantes do ambiente .net, que atuam inclusive na segurança e disponibilidade do sistema.

    Uma vez tudo configurado, é hora de colocar a instrução dentro do seu código. A instrução unsafe deverá ser colocada dentro do escopo em que seu código será executado. Nas seguintes declarações é permitido o uso do unsafe:

    Classes, Structs, Interfaces, Delegates, Eventos, Campos, Propriedades, Métodos, Operadores, Construtores, Destrutores.

    A palavra reservada sempre é utilizada após o modificador de acessibilidade, por exemplo: public unsafe class Teste.

    Além de utilizar o unsafe nas declarações, o mesmo também pode ser utilizado dentro do código de um método, seguindo o exemplo da Figura 2.

    Figura 2 – Marcação Unsafe

    Devemos ter em mente que por convenção, quanto menor for o escopo da utilização do unsafe, melhor. Sendo assim só valerá a pena utilizar o “unsafe” em uma classe, caso ela utilize extensivamente ponteiros.

    A palavra unsafe não interfere na declaração do tipo. Sendo assim a sobrecarga exemplificada na figura 3 é válida, já que somente o método “Teste” da superclasse faz uso de ponteiros.

    Figura 3 – Unsafe não interfere na declaração do tipo.

    Porém quando o ponteiro é parte da assinatura do método, faz-se necessário a utilização da palavra unsafe em todas as utilizações, como demonstrado na Figura 4

    Figura 4 – Ponteiros na declaração do método.

    Uma vez absorvida a utilização do unsafe, vamos prosseguir para o uso dos ponteiros. A declaração de ponteiros segue a sintaxe:

    Tipo* nomeDoPonteiro

    Porém a declaração de múltiplos ponteiros não segue a sintaxe do C e C++, sendo necessário a indicação de ponteiro apenas uma vez, como abaixo:

    Tipo* nomeDoPonteiro1, nomeDoPonteiro2;

    Como regra, um ponteiro pode assumir apenas tipos que não são gerenciados pelo Garbage Collector (GC), ou seja, apenas pode ser tipado por um tipo-primitivo e nunca por um objeto (Tipos chamados de “unmanaged-type”). A razão disto é que como o GC pode desalocar os recursos de um objeto inativo, o ponteiro ficará apontando para uma região vazia na memória, ou alocada por um outro recurso não desejado, perdendo assim a referência. Dado este problema, é proibido para um ponteiro receber o endereço de um objeto. Um ponteiro pode conter referencia dos seguintes tipos:

    sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, enum, ponteiro e structs definidas pelo usuário que contenham apenas campos “unmanaged-typed”. Repare que os tipos são escritos com letra minúscula, porque estamos falando dos tipos-primitivos e não de suas classes wrappers.

    Exemplo Descrição
    byte* Ponteiro para byte.
    char* Ponteiro para char
    int** Ponteiro para ponteiro para inteiro
    int*[] Vetor uni-dimensional de ponteiros para inteiros
    void* Ponteiros para tipos desconhecidos

    Tabela 1 – Exemplos de Ponteiros

    Ponteiros possuem ainda operadores específicos. O operador “*” retorna o valor dentro do endereço de memória armazenado no ponteiro. O operador “&” retorna o endereço de memória do ponteiro. Os operadores ++ e -- incrementam e decrementam o ponteiro. Vale lembrar que o ++ e o -- não alteram o valor do endereço de memória, mas sim o endereço. Como vetores são colocados em posições subseqüentes, um vetor de int apontado para um ponteiro seria iterado pelo mesmo incrementando o endereço de memória do mesmo. Existe ainda o operador “->” que será utilizado para acessar um membro de uma struct dentro de um ponteiro. Vejamos na Figura 5 a utilização dos ponteiros e seus operadores.

    Figura 5 – Exemplo da utilização de ponteiro e seus operadores.

    Ainda sobre este assunto, existem os operadores stackallock e fixed, sendo o primeiro utilizado para “reservar um espaço em memória” e o outro para impedir que um objeto tenha seu endereço de memória trocado durante a execução de um trecho de código.

    Para conferir um exemplo de aplicação prática, visite o link abaixo na MSDN para acessar o exemplo de acesso a uma das APIs do Windows que fazem uso de parâmetros com ponteiros.

    How to: Use the Windows ReadFile Function: http://msdn2.microsoft.com/en-us/library/2d9wy99d(VS.80).aspx

    Existem muitos outros assuntos sobre ponteiros no C#. Mas eles ficam para uma próxima ocasião.

Rodrigo Moreira

Rodrigo Moreira - Arquiteto de Software, trabalha com .net há mais de 5 anos e possui certificações Microsoft tanto na versão 1.x quanto 2.0 do .net framework (MCP e MCPTS). É Microsoft Student Partner Co-Lead, apaixonado pelo mundo de segurança no desenvolvimento de software e mantém o blog http://jackflashspot.spaces.live.com, onde escreve sobre seus devaneios no mundo do Software, além de seu trabalho como Microsoft Student Partner.