Desenvolvimento - C#

Código Unsafe e Ponteiros no .NET Framework 2.0 com C#

Neste artigo, aprenderemos a tratar diretamente a memória do computador, com o uso de ponteiros na plataforma .NET 2.0 com a linguagem C#.

por Leandro Alves Santos



O .NET Framework possui um excelente gerenciamento de memória através da Garbage Collector em conjunto com os tipos por referência, mas nós podemos ter acesso direto a memória através dos ponteiros.

Veremos neste artigo como manipular ponteiros no .NET Framework 2.0 com C#.

1 - A Palavra Chave unsafe

A palavra chave unsafe delimita um contexto onde podemos escrever códigos que envolvam a manipulação de ponteiros. Pode ser utilizada na declaração de um membro ou um tipo e não pode ser utilizada em uma variável local, entretanto, podemos marcar um trecho de um método como unsafe.

A seguir, veremos alguns exemplos de onde podemos utilizar a palavra chave unsafe.

A) Declaração de uma classe como unsafe:

public unsafe class Classe { }

B) Declaração de membros da classe como unsafe:

private unsafe int *_valor; public unsafe void Incrementar() { }

C) Declaração unsafe no corpo de um método:

public void Incrementar() { unsafe { } }

Para que um código que seja declarado com o modificador unsafe possa ser compilado no Framework 2.0, precisamos informar isso ao compilador.

Quando a compilação é realizada através de linha de comando, informamos o compilador incluindo /unsafe, na Figura 1.1 podemos ver a estrutura do comando de compilação.


Figura 1: Compilando a aplicação.

Com o Visual Studio 2005, precisamos marcar a opção "Allow unsafe code", nas propriedades do projeto.


Figura 2: Propriedades do projeto.

Neste momento, nossa aplicação está apta a trabalhar com ponteiros.

2 - Ponteiros

Um ponteiro pode ser definido como uma variável que contém um endereço de memória.

Os ponteiros são considerados uma das ferramentas mais poderosas para quem desenvolve aplicações utilizando linguagens como C e C++.

No .NET Framework, podemos criar ponteiros dos seguintes tipos:

  • byte;
  • sbyte;
  • ushort;
  • short;
  • uint;
  • int;
  • ulong;
  • long;
  • float;
  • double;
  • decimal;
  • char;
  • bool;
  • enum;
  • struct (Ponteiros de estruturas só podem ser utilizados caso a estrutura possua apenas campos com os tipos que citamos anteriormente)
  • void(Ponteiro para um tipo indefinido)

Vejamos agora alguns exemplos

1º Exemplo:

static unsafe void Main(string[] args) { int inteiro = 10; int* pInteiro = &inteiro; *pInteiro = 20; Console.WriteLine( "Endereço da variável pInteiro:\t{0}", (UInt64)pInteiro); Console.WriteLine( "Endereço da variável inteiro:\t{0}", (UInt64)(&inteiro)); Console.WriteLine( "Valor contido no endereço de pInteiro:\t{0}", *pInteiro); Console.WriteLine( "Valor contido na variável inteiro:\t{0}", inteiro); Console.Read(); }

Nesse exemplo, declaramos a variável inteiro e a inicializamos com o valor 10. Na seqüência declaramos o ponteiro pInteiro e o inicializamos com o endereço de memória da variável inteiro.

Nesse momento a variável pInteiro está apontando para o mesmo endereço de memória da variável inteiro. O operador & nos possibilita acessar o endereço de memória e o * possibilita a declaração de um ponteiro e também o acesso ao valor armazenado na memória. Na seqüência, alteramos o valor contido na memória para a qual o ponteiro pInteiro está apontando para 20.

Para finalizar, mostramos o endereço de memória e o valor armazenado.

Compile e execute a aplicação. Podemos ver que tanto a variável pInteiro como a variável inteiro, retornam os mesmos resultados.

Note que este método foi declarado com o modificador unsafe. Experimente remover a palavra chave unsafe e tente compilar a aplicação para ver o erro que o compilador informa.

2º Exemplo:

static void Main(string[] args) { char caractere; Console.Write( "Pressione uma tecla:\t"); caractere = Convert.ToChar( Console.ReadKey().KeyChar); Console.WriteLine(); unsafe { char* pChar = &caractere; Console.WriteLine( "Código ASCII:\t\t{0}", (int)*pChar); Console.WriteLine( "Endereço da variável:\t{0}", (UInt64)(&pChar)); } Console.ReadLine(); }

Nesse exemplo, declaramos uma variável do tipo char com o nome caractere, solicitamos que o usuário digite uma tecla e atribuimos o resultado a nossa variável, na sequência declaramos um ponteiro do tipo char com o nome pChar e atribuimos o endereço de memória da variável caractere para este ponteiro.

Para finalizar este exemplo, convertemos o valor contido no endereço para qual pChar está apontando para um inteiro e mostramos o resultado. A conversão de uma variável do tipo char para um inteiro, retorna o código ASCII deste caractere. Na sequência, mostramos o endereço de memória para qual o ponteiro pChar está apontando.

3º Exemplo:

public struct Pessoa { public enum Gen { Feminino, Masculino } public Gen gen; public int idade; } static unsafe void Main(string[] args) { Pessoa pessoa = new Pessoa(); Pessoa* pPessoa = &pessoa; pPessoa->gen = Pessoa.Gen.Masculino; (*pPessoa).idade = 20; Console.WriteLine("Gen:\t{0}", pPessoa->gen); Console.WriteLine("Idade:\t{0}", (*pPessoa).idade); Console.Read(); }

Nesse exemplo, criamos a estrutura Pessoa com o enum Gen, uma variável do tipo do enum e um inteiro.

No método Main nós criamos uma variável do tipo da estrutura com o nome pessoa e um ponteiro com o nome pPessoa e atribuimos o endereço de memória da variável pessoa. Na sequência atribuimos os valores e mostramos o resultado.

Para acessar um membro de uma estrutura nós podemos utilizar o operador -> ou (*nome_do_ponteiro).membro. Em nosso exemplo, podemos observar essas duas formas de acesso aos membros de uma estrutura, tanto na atribuição como na recuperação do valor. É importante saber que essas formas de acesso retornam os valores e não os endereços de memória.

3 - A Palavra Chave fixed

Antes de analisarmos as necessidades da palavra chave fixed, precisamos entender um pouco da estrutura do .NET Framework e como consequência, analisarmos a necessidade da utilização da palavra chave fixed.

Como vimos na seção Ponteiros, podemos criar ponteiros de vários tipos. Os tipos que vimos, são os Value Types do .NET Framework, ou seja, não podemos criar ponteiros para Reference Types.

Caso você esteja com dúvidas ou ainda não tenha conhecimento sobre os tipos no .NET Framework, o artigo Struct e Class: Quando usar? escrito pelo Renato Guimarães trata este assunto de forma bem clara.

Vejamos o seguinte exemplo:

public struct Pessoa { public int idade; } static unsafe void Main(string[] args) { Pessoa p = new Pessoa(); p.idade = 15; int* pIdade = &p.idade; Console.WriteLine(*pIdade); Console.Read(); }

Neste exemplo, temos a struct Pessoa que contém um membro do tipo inteiro com o nome idade. No método Main, declaramos uma variável do tipo Pessoa e atribuímos um valor para o membro idade. Após isso, declaramos um ponteiro do tipo inteiro e atribuímos o endereço de memória do membro idade.

Agora, vamos transformar a nossa estrutura em uma classe. Esse trabalho é simples, a única coisa que precisamos fazer é substituir a palavra chave struct por class. O nosso código ficará da seguinte forma:

public class Pessoa { public int idade; }

Mantenha o método Main com o mesmo código escrito no exemplo anterior e tente compilar a aplicação.

O nosso código não foi compilado e a seguinte mensagem de erro foi informada:
You can only take the address of an unfixed expression inside of a fixed statement initializer

Nós alteramos a nossa estrutura para uma classe e as classes são Reference Types. Os Reference Types podem ser compostos por um conjunto de Value Types e nós podemos criar ponteiros para estes tipos, inclusive quando eles compõem um Reference Type. Só que a Garbage Collector pode re-alocar essas variáveis na memória, ou seja, se o nosso exemplo anterior compilasse, correríamos o risco de trabalhar com um endereço de memória inválido.

Com a palavra chave fixed, criamos um contexto onde essa variável é fixada, ou seja, durante este contexto ela não é re-alocada pela Garbage Collector. Vamos modificar o método Main incluindo a palavra chave fixed e as chaves que delimitam o seu contexto.

static unsafe void Main(string[] args) { Pessoa p = new Pessoa(); p.idade = 15; fixed (int* pIdade = &p.idade) { Console.WriteLine(*pIdade); } Console.Read(); }

4 - Aritmética de Ponteiros

Na aritmética de ponteiros, as únicas operações possíveis são de soma e subtração e quando ocorre uma dessas operações, o cálculo é feito com base no tipo do ponteiro que estamos trabalhando. Por exemplo: Se temos um ponteiro do tipo inteiro apontando para o endereço de memória 60091584 e subtraímos um ao ponteiro, após essa operação ele estará apontando para 60091580. Esse cálculo é feito de acordo com a quantidade de bytes necessária para a alocação de um valor na pilha ou seja, pode variar de acordo com o tipo de dados que estamos trabalhando.

Vejamos agora um programa em que criamos dois ponteiros do tipo inteiro e trocamos os endereços de memória que eles estão apontando entre eles:

static unsafe void Main(string[] args) { int inteiro1 = 13; int inteiro2 = 25; int* pInteiro1 = &inteiro1; int* pInteiro2 = &inteiro2; Console.WriteLine("Endereço de memória de pInteiro1: {0}", (UInt64)pInteiro1); Console.WriteLine("Endereço de memória de pInteiro2: {0}", (UInt64)pInteiro2); pInteiro1--; pInteiro2++; Console.WriteLine("\nEndereço de memória de pInteiro1: {0}", (UInt64)pInteiro1); Console.WriteLine("Endereço de memória de pInteiro2: {0}", (UInt64)pInteiro2); Console.Read(); }

5 - A Palavra Chave stackalloc

O mecanismo de stackalloc nos permite alocar um bloco de memória na pilha. O retorno é um ponteiro para o bloco de memória alocado. Trabalhamos com este bloco de memória de forma semelhante a um array, através de um índice.

static unsafe void Main(string[] args) { int* buffer = stackalloc int[10]; for (int indice = 0; indice < 10; indice++) { buffer[indice] = indice; Console.WriteLine("Endereço: {0}\tValor: {1}", (UInt64)(&buffer[indice]), buffer[indice]); } Console.Read(); }

Neste artigo, aprendemos a manipular ponteiros na plataforma .NET 2.0. Vimos também onde podemos declarar um código como unsafe, criando um contexto onde é possível usufruir do poder dos ponteiros.

Leandro Alves Santos

Leandro Alves Santos - Visite o blog do autor: http://weblogs.pontonetpt.com/las/.