Código não seguro, tipos de ponteiro e ponteiros de função
A maioria do código C# que você escreve é "código verifiavelmente seguro". Código verificávelmente seguro significa que as ferramentas do .NET podem verificar se o código é seguro. Em geral, o código seguro não acessa diretamente a memória usando ponteiros. Ele também não aloca memória bruta. Em vez disso, ele cria objetos gerenciados.
O C# dá suporte a unsafe um contexto no qual você pode escrever código não verificável . Em um unsafe contexto, o código pode usar ponteiros, alocar e liberar blocos de memória e chamar métodos usando ponteiros de função. O código não seguro em C# não é necessariamente perigoso; é apenas o código cuja segurança não pode ser verificada.
O código não seguro tem as propriedades a seguir:
- Blocos de código, tipos e métodos podem ser definidos como não seguros.
- Em alguns casos, o código não seguro pode aumentar o desempenho de um aplicativo removendo as verificações de limites de matriz.
- O código não seguro é necessário quando você chama funções nativas que exigem ponteiros.
- Usar o código não seguro apresenta riscos de segurança e estabilidade.
- O código que contém blocos não seguros deve ser compilado com a opção do compilador AllowUnsafeBlocks .
Tipos de ponteiro
Em um contexto não seguro, um tipo pode ser um tipo de ponteiro, além de um tipo de valor ou um tipo de referência. Uma declaração de tipo de ponteiro usa uma das seguintes formas:
type* identifier;
void* identifier; //allowed but not recommended
O tipo especificado antes do * em um tipo de ponteiro é chamado de tipo referent. Somente um tipo não gerenciado pode ser um tipo referent.
Os tipos de ponteiro não herdam do objeto e não existem conversões entre tipos de ponteiro e object. Além disso, a caixa boxing e a unboxing não são suportadas por ponteiros. No entanto, você pode converter entre diferentes tipos de ponteiro e tipos de ponteiro e tipos integrais.
Ao declarar vários ponteiros na mesma declaração, você escreve o asterisco (*) junto com o tipo subjacente apenas. Ele não é usado como um prefixo para cada nome de ponteiro. Por exemplo:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
Um ponteiro não pode apontar para uma referência ou para um struct que contém referências, porque uma referência de objeto pode ser coletada como lixo mesmo se um ponteiro estiver apontando para ele. O coletor de lixo não acompanha se um objeto está sendo apontado por nenhum tipo de ponteiro.
O valor da variável de ponteiro do tipo MyType* é o endereço de uma variável do tipo MyType. Estes são exemplos de declarações de tipos de ponteiro:
int* p:pé um ponteiro para um inteiro.int** p:pé um ponteiro para um ponteiro para um inteiro.int*[] p:pé uma matriz unidimensional de ponteiros para inteiros.char* p:pé um ponteiro para um char.void* p:pé um ponteiro para um tipo desconhecido.
O operador de indingação de * ponteiro pode ser usado para acessar o conteúdo no local apontado pela variável de ponteiro. Por exemplo, considere a seguinte declaração:
int* myVariable;
A expressão *myVariable denota a variável int encontrada no endereço contido em myVariable.
Há vários exemplos de ponteiros nos artigos na instruçãofixed . O exemplo a seguir usa a palavra-chave unsafe e a instrução fixed e mostra como incrementar um ponteiro interior. Você pode colar esse código na função principal de um aplicativo de console para executá-lo. Esses exemplos devem ser compilados com a opção do compilador AllowUnsafeBlocks definida.
// Normal pointer to an object.
int[] a = new int[5] { 10, 20, 30, 40, 50 };
// Must be in unsafe code to use interior pointers.
unsafe
{
// Must pin object on heap so that it doesn't move while using interior pointers.
fixed (int* p = &a[0])
{
// p is pinned as well as object, so create another pointer to show incrementing it.
int* p2 = p;
Console.WriteLine(*p2);
// Incrementing p2 bumps the pointer by four bytes due to its type ...
p2 += 1;
Console.WriteLine(*p2);
p2 += 1;
Console.WriteLine(*p2);
Console.WriteLine("--------");
Console.WriteLine(*p);
// Dereferencing p and incrementing changes the value of a[0] ...
*p += 1;
Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
}
}
Console.WriteLine("--------");
Console.WriteLine(a[0]);
/*
Output:
10
20
30
--------
10
11
12
--------
12
*/
Não é possível aplicar o operador de inderção a um ponteiro do tipo void*. No entanto, você pode usar uma conversão para converter um ponteiro nulo em qualquer outro tipo de ponteiro e vice-versa.
Um ponteiro pode ser null. Aplicar o operador de indireção a um ponteiro nulo causa um comportamento definido por implementação.
Passar ponteiros entre métodos pode causar um comportamento indefinido. Considere usar um método que retorne um ponteiro para uma variável local por meio de um parâmetro in, out ou ref, ou como o resultado da função. Se o ponteiro foi definido em um bloco fixo, a variável à qual ele aponta não pode mais ser corrigida.
A tabela a seguir lista os operadores e as instruções que podem operar em ponteiros em um contexto inseguro:
| Operador/Instrução | Uso |
|---|---|
* |
Executa indireção de ponteiro. |
-> |
Acessa um membro de um struct através de um ponteiro. |
[] |
Indexa um ponteiro. |
& |
Obtém o endereço de uma variável. |
++ e -- |
Incrementa e decrementa ponteiros. |
+ e - |
Executa aritmética de ponteiros. |
==, !=, <, >, <= e >= |
Compara ponteiros. |
stackalloc |
Aloca memória na pilha. |
fixed Declaração |
Corrige temporariamente uma variável para que seu endereço possa ser encontrado. |
Para obter mais informações sobre operadores relacionados a ponteiro, consulte Operadores relacionados a ponteiros.
Qualquer tipo de ponteiro pode ser convertido implicitamente em um void* tipo. Qualquer tipo de ponteiro pode ser atribuído ao valor null. Qualquer tipo de ponteiro pode ser convertido explicitamente em qualquer outro tipo de ponteiro usando uma expressão de conversão. Você também pode converter qualquer tipo integral em um tipo de ponteiro ou qualquer tipo de ponteiro em um tipo integral. Essas conversões exigem uma conversão explícita.
O exemplo a seguir converte um int* em um byte*. Observe que o ponteiro aponta para o menor byte endereçado da variável. Quando você incrementar sucessivamente o resultado, até o tamanho de int (4 bytes), você poderá exibir os bytes restantes da variável.
int number = 1024;
unsafe
{
// Convert to byte:
byte* p = (byte*)&number;
System.Console.Write("The 4 bytes of the integer:");
// Display the 4 bytes of the int variable:
for (int i = 0 ; i < sizeof(int) ; ++i)
{
System.Console.Write(" {0:X2}", *p);
// Increment the pointer:
p++;
}
System.Console.WriteLine();
System.Console.WriteLine("The value of the integer: {0}", number);
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Buffers de tamanho fixo
No C#, você pode usar a instrução fixed para criar um buffer com uma matriz de tamanho fixo em uma estrutura de dados. Buffers de tamanho fixo são úteis quando você escreve métodos que interoperam com fontes de dados de outras linguagens ou plataformas. A matriz fixa pode usar qualquer atributo ou modificador que for permitido para membros de struct regulares. A única restrição é que o tipo da matriz deve ser bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float ou double.
private fixed char name[30];
No código seguro, um struct C# que contém uma matriz não contém os elementos da matriz. O struct contém uma referência aos elementos em vez disso. Você pode inserir uma matriz de tamanho fixo em um struct quando ela é usada em um bloco de código não seguro.
O tamanho do seguinte struct não depende do número de elementos na matriz, pois pathName é uma referência:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Um struct pode conter uma matriz inserida em código não seguro. No exemplo a seguir, a matriz fixedBuffer tem tamanho fixo. É possível uma instrução fixed para estabelecer um ponteiro para o primeiro elemento. Os elementos da matriz são acessados por este ponteiro. A instrução fixed fixa o campo da instância fixedBuffer em um local específico na memória.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
internal unsafe class Example
{
public Buffer buffer = default;
}
private static void AccessEmbeddedArray()
{
var example = new Example();
unsafe
{
// Pin the buffer to a fixed location in memory.
fixed (char* charPtr = example.buffer.fixedBuffer)
{
*charPtr = 'A';
}
// Access safely through the index:
char c = example.buffer.fixedBuffer[0];
Console.WriteLine(c);
// Modify through the index:
example.buffer.fixedBuffer[0] = 'B';
Console.WriteLine(example.buffer.fixedBuffer[0]);
}
}
O tamanho da matriz char de 128 elementos é 256 bytes. Buffers char de tamanho fixo sempre levam 2 bytes por caractere, independentemente da codificação. Esse tamanho de matriz é o mesmo quando buffers char são marshallados para métodos de API ou structs com CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Para obter mais informações, consulte CharSet.
O exemplo anterior demonstra o acesso a fixed campos sem fixar, que está disponível a partir do C# 7.3.
Outra matriz de tamanho fixo comum é a matriz bool. Os elementos em uma bool matriz têm sempre 1 byte de tamanho. bool matrizes não são apropriadas para criar matrizes de bits ou buffers.
Buffers de tamanho fixo são compilados System.Runtime.CompilerServices.UnsafeValueTypeAttributecom o , que instrui o CLR (Common Language Runtime) de que um tipo contém uma matriz não orientada que pode potencialmente estouro. A memória alocada usando stackalloc também habilita automaticamente os recursos de detecção de estouro de buffer no CLR. O exemplo anterior mostra como um buffer de tamanho fixo pode existir em um unsafe struct .
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
O C# gerado pelo compilador do Buffer é atribuído da seguinte maneira:
internal struct Buffer
{
[StructLayout(LayoutKind.Sequential, Size = 256)]
[CompilerGenerated]
[UnsafeValueType]
public struct <fixedBuffer>e__FixedBuffer
{
public char FixedElementField;
}
[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}
Buffers de tamanho fixo diferem de matrizes regulares das seguintes maneiras:
- Só pode ser usado em um
unsafecontexto. - Só podem ser campos de instância de structs.
- Eles são sempre vetores ou matrizes unidimensionais.
- A declaração deve incluir o comprimento,
fixed char id[8]como. Você não pode usarfixed char id[]o.
Como usar ponteiros para copiar uma matriz de bytes
O exemplo a seguir usa ponteiros para copiar bytes de uma matriz para outra.
Este exemplo usa a palavra-chave não seguro, que permite que você use ponteiros no método Copy. A instrução fixo é usada para declarar ponteiros para as matrizes de origem e de destino. A instrução fixedfixa o local das matrizes de origem e de destino na memória para que elas não sejam movidas pela coleta de lixo. Os blocos de memória para as matrizes não serão fixado quando o bloco fixed for concluído. Como o Copy método neste exemplo usa a unsafe palavra-chave, ele deve ser compilado com a opção de compilador AllowUnsafeBlocks .
Este exemplo acessa os elementos das duas matrizes usando índices em vez de um segundo ponteiro não gerenciado. A declaração dos ponteiros pSource e pTarget fixa as matrizes. Esse recurso está disponível começando com o C# 7.3.
static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
int targetOffset, int count)
{
// If either array is not instantiated, you cannot complete the copy.
if ((source == null) || (target == null))
{
throw new System.ArgumentException("source or target is null");
}
// If either offset, or the number of bytes to copy, is negative, you
// cannot complete the copy.
if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
{
throw new System.ArgumentException("offset or bytes to copy is negative");
}
// If the number of bytes from the offset to the end of the array is
// less than the number of bytes you want to copy, you cannot complete
// the copy.
if ((source.Length - sourceOffset < count) ||
(target.Length - targetOffset < count))
{
throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
}
// The following fixed statement pins the location of the source and
// target objects in memory so that they will not be moved by garbage
// collection.
fixed (byte* pSource = source, pTarget = target)
{
// Copy the specified number of bytes from source to target.
for (int i = 0; i < count; i++)
{
pTarget[targetOffset + i] = pSource[sourceOffset + i];
}
}
}
static void UnsafeCopyArrays()
{
// Create two arrays of the same length.
int length = 100;
byte[] byteArray1 = new byte[length];
byte[] byteArray2 = new byte[length];
// Fill byteArray1 with 0 - 99.
for (int i = 0; i < length; ++i)
{
byteArray1[i] = (byte)i;
}
// Display the first 10 elements in byteArray1.
System.Console.WriteLine("The first 10 elements of the original are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray1[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of byteArray1 to byteArray2.
Copy(byteArray1, 0, byteArray2, 0, length);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of the last 10 elements of byteArray1 to the
// beginning of byteArray2.
// The offset specifies where the copying begins in the source array.
int offset = length - 10;
Copy(byteArray1, offset, byteArray2, 0, length - offset);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
/* Output:
The first 10 elements of the original are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
90 91 92 93 94 95 96 97 98 99
*/
}
Ponteiros de função
O C# fornece delegate tipos para definir objetos de ponteiro de função segura. Invocar um delegado envolve instanciar um tipo derivado de System.Delegate e fazer uma chamada de método virtual para seu Invoke método. Essa chamada virtual usa a callvirt instrução Il. Em caminhos de código críticos de desempenho, o uso da calli instrução Il é mais eficiente.
Você pode definir um ponteiro de função usando a delegate* sintaxe. O compilador chamará a função usando a calli instrução em vez de criar uma instância de um delegate objeto e chamar Invoke . O código a seguir declara dois métodos que usam um delegate ou um delegate* para combinar dois objetos do mesmo tipo. O primeiro método usa um System.Func<T1,T2,TResult> tipo delegate. O segundo método usa uma delegate* declaração com os mesmos parâmetros e tipo de retorno:
public static T Combine<T>(Func<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) =>
combinator(left, right);
O código a seguir mostra como você declararia uma função local estática e invoca o UnsafeCombine método usando um ponteiro para essa função local:
static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);
O código anterior ilustra várias das regras na função acessada como um ponteiro de função:
- Ponteiros de função só podem ser declarados em um
unsafecontexto. - Os métodos que usam
delegate*(ou retornam umdelegate*) só podem ser chamados em umunsafecontexto. - O
&operador para obter o endereço de uma função é permitido somente emstaticfunções. (Essa regra se aplica a funções de membro e a funções locais).
A sintaxe tem paralelos com tipos declarativos delegate e uso de ponteiros. O * sufixo em delegate indica que a declaração é um ponteiro de função. O & ao atribuir um grupo de métodos a um ponteiro de função indica que a operação usa o endereço do método.
Você pode especificar a Convenção de chamada para um delegate* usando as palavras-chave managed e unmanaged . Além disso, para unmanaged ponteiros de função, você pode especificar a Convenção de chamada. As declarações a seguir mostram exemplos de cada uma. A primeira declaração usa a managed Convenção de chamada, que é o padrão. Os próximos quatro usam uma unmanaged Convenção de chamada. Cada especifica uma das convenções de chamada ECMA 335: Cdecl , Stdcall , Fastcall ou Thiscall . A última declaração usa a unmanaged Convenção de chamada, instruindo o CLR a escolher a Convenção de chamada padrão para a plataforma. O CLR escolherá a Convenção de chamada em tempo de execução.
public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
combinator(left, right);
Você pode aprender mais sobre ponteiros de função na proposta de ponteiro de função para C# 9,0.
Especificação da linguagem C#
Para obter mais informações, consulte o capítulo código não seguro da especificação da linguagem C#.

Formed in 2009, the Archive Team (not to be confused with the archive.org Archive-It Team) is a rogue archivist collective dedicated to saving copies of rapidly dying or deleted websites for the sake of history and digital heritage. The group is 100% composed of volunteers and interested parties, and has expanded into a large amount of related projects for saving online and digital history.
