Qual é a diferença entre uma referência C # e um ponteiro?

86

Eu não entendo muito bem a diferença entre uma referência C # e um ponteiro. Ambos apontam para um lugar na memória, não é? A única diferença que posso descobrir é que os ponteiros não são tão inteligentes, não podem apontar para nada no heap, são isentos da coleta de lixo e podem apenas fazer referência a structs ou tipos de base.

Uma das razões pelas quais pergunto é que existe uma percepção de que as pessoas precisam entender bem os ponteiros (de C, eu acho) para ser um bom programador. Muitas pessoas que aprendem idiomas de nível superior perdem isso e, portanto, têm essa fraqueza.

Eu simplesmente não entendo o que é tão complexo sobre um ponteiro? É basicamente uma referência a um lugar na memória, não é? Ele pode retornar sua localização e interagir diretamente com o objeto naquele local?

Eu perdi um ponto importante?

Richard
fonte
1
A resposta curta é sim, você perdeu algo razoavelmente significativo, e essa é a razão para "... a percepção de que as pessoas precisam entender os ponteiros". Dica: C # não é a única linguagem que existe.
jdigital

Respostas:

51

As referências C # podem e serão realocadas pelo coletor de lixo, mas os ponteiros normais são estáticos. É por isso que usamos fixedpalavra-chave ao adquirir um ponteiro para um elemento de array, para evitar que seja movido.

EDIT: Conceitualmente, sim. Eles são mais ou menos iguais.

mmx
fonte
Não existe outro comando que impede uma referência de C # de ter o objeto de sua referência movido pelo GC?
Richard
Desculpe, pensei que era outra coisa porque a postagem se referia a um ponteiro.
Richard
Sim, um GCHandle.Alloc ou um Marshal.AllocHGlobal (além do fixo)
ctacke
Está corrigido em C #, pin_ptr em C ++ / CLI
mmx
Marshal.AllocHGlobal não alocará memória no heap gerenciado e, naturalmente, não está sujeito à coleta de lixo.
mmx
133

Existe uma ligeira, mas extremamente importante, distinção entre um ponteiro e uma referência. Um ponteiro aponta para um lugar na memória enquanto uma referência aponta para um objeto na memória. Os ponteiros não são "seguros para tipos" no sentido de que você não pode garantir a exatidão da memória para a qual eles apontam.

Tome por exemplo o seguinte código

int* p1 = GetAPointer();

Isso é seguro para o tipo, no sentido de que GetAPointer deve retornar um tipo compatível com int *. Ainda assim, não há garantia de que * p1 realmente apontará para um int. Pode ser um char, double ou apenas um ponteiro para a memória aleatória.

Uma referência, entretanto, aponta para um objeto específico. Os objetos podem ser movidos na memória, mas a referência não pode ser invalidada (a menos que você use um código não seguro). As referências são muito mais seguras a esse respeito do que os ponteiros.

string str = GetAString();

Nesse caso, str tem um de dois estados 1) não aponta para nenhum objeto e, portanto, é nulo ou 2) aponta para uma string válida. É isso aí. O CLR garante que esse seja o caso. Não pode e não será por um ponteiro.

JaredPar
fonte
13

Uma referência é um ponteiro "abstrato": você não pode fazer aritmética com uma referência e não pode fazer nenhum truque de baixo nível com seu valor.

Chris Conway
fonte
8

A principal diferença entre uma referência e um ponteiro é que um ponteiro é uma coleção de bits cujo conteúdo só importa quando está sendo usado ativamente como um ponteiro, enquanto uma referência encapsula não apenas um conjunto de bits, mas também alguns metadados que mantêm o estrutura subjacente informada de sua existência. Se existir um ponteiro para algum objeto na memória e esse objeto for excluído, mas o ponteiro não for apagado, a existência continuada do ponteiro não causará nenhum dano, a menos ou até que seja feita uma tentativa de acessar a memória para a qual ele aponta. Se nenhuma tentativa for feita para usar o ponteiro, nada se importará com sua existência. Por outro lado, estruturas baseadas em referência como .NET ou JVM exigem que sempre seja possível para o sistema identificar cada referência de objeto existente, e cada referência de objeto existente deve sempre sernull ou então identificar um objeto de seu tipo apropriado.

Observe que cada referência de objeto realmente encapsula dois tipos de informação: (1) o conteúdo do campo do objeto que identifica e (2) o conjunto de outras referências ao mesmo objeto. Embora não haja nenhum mecanismo pelo qual o sistema possa identificar rapidamente todas as referências que existem a um objeto, o conjunto de outras referências que existem a um objeto pode muitas vezes ser a coisa mais importante encapsulada por uma referência (isto é especialmente verdadeiro quando coisas do tipo Objectsão usadas como tokens de bloqueio). Embora o sistema mantenha alguns bits de dados para cada objeto para uso em GetHashCode, os objetos não têm identidade real além do conjunto de referências que existem para eles. Se Xmantém a única referência existente a um objeto, substituindoXcom uma referência a um novo objeto com o mesmo conteúdo de campo não terá nenhum efeito identificável, exceto para alterar os bits retornados por GetHashCode(), e mesmo esse efeito não é garantido.

supergato
fonte
5

Os ponteiros apontam para um local no espaço de endereço da memória. As referências apontam para uma estrutura de dados. Todas as estruturas de dados se moviam o tempo todo (bem, não com tanta frequência, mas de vez em quando) pelo coletor de lixo (para compactar o espaço de memória). Além disso, como você disse, as estruturas de dados sem referências terão o lixo coletado depois de um tempo.

Além disso, os ponteiros só podem ser usados ​​em contextos inseguros.

Tamas Czinege
fonte
5

Acho importante que os desenvolvedores entendam o conceito de ponteiro - ou seja, entendam a indireção. Isso não significa que eles necessariamente tenham que usar ponteiros. Também é importante entender que o conceito de referência difere do conceito de ponteiro , embora apenas sutilmente, mas que a implementação de uma referência quase sempre é um ponteiro.

Ou seja, uma variável que contém uma referência é apenas um bloco de memória do tamanho de um ponteiro que contém um ponteiro para o objeto. No entanto, essa variável não pode ser usada da mesma maneira que uma variável de ponteiro pode ser usada. Em C # (e C e C ++, ...), um ponteiro pode ser indexado como uma matriz, mas uma referência não. Em C #, uma referência é rastreada pelo coletor de lixo, um ponteiro não pode ser. Em C ++, um ponteiro pode ser reatribuído, uma referência não. Sintaticamente e semanticamente, ponteiros e referências são bastante diferentes, mas mecanicamente são iguais.

P papai
fonte
A coisa do array parece interessante, é basicamente onde você pode dizer ao ponteiro para deslocar a localização da memória como um array, enquanto você não consegue fazer isso com uma referência? Não consigo imaginar quando isso seria útil, mas ainda assim interessante.
Richard
Se p for um int * (um ponteiro para um int), então (p + 1) é o endereço identificado por p + 4 bytes (o tamanho de um int). E p [1] é o mesmo que * (p + 1) (ou seja, ele "desreferencia" o endereço 4 bytes após p). Em contraste, com uma referência de array (em C #), o operador [] executa uma chamada de função.
P Daddy
5

Primeiro eu acho que você precisa definir um "Pointer" em sua semática. Você quer dizer o ponteiro que você pode criar em código inseguro com fixo ? Você quer dizer um IntPtr que você recebe talvez de uma chamada nativa ou Marshal.AllocHGlobal ? Você quer dizer um GCHandle ? Todos são essencialmente a mesma coisa - uma representação de um endereço de memória onde algo está armazenado - seja uma classe, um número, uma estrutura, seja o que for. E para o registro, eles certamente podem estar na pilha.

Um ponteiro (todas as versões acima) é um item fixo. O CG não tem ideia do que está naquele endereço e, portanto, não tem capacidade de gerenciar a memória ou a vida do objeto. Isso significa que você perde todos os benefícios de um sistema de coleta de lixo. Você deve gerenciar manualmente a memória do objeto e tem potencial para vazamentos.

Uma referência, por outro lado, é basicamente um "ponteiro gerenciado" que o GC conhece. Ainda é o endereço de um objeto, mas agora o GC conhece os detalhes do alvo, então pode movê-lo, fazer compactações, finalizar, descartar e todas as outras coisas legais que um ambiente gerenciado faz.

A principal diferença, realmente, está em como e por que você os usaria. Para a grande maioria dos casos em uma linguagem gerenciada, você usará uma referência de objeto. Os ponteiros se tornam úteis para fazer interoperabilidade e a rara necessidade de um trabalho realmente rápido.

Edit: Na verdade, aqui está um bom exemplo de quando você pode usar um "ponteiro" no código gerenciado - neste caso, é um GCHandle, mas a mesma coisa poderia ter sido feita com AllocHGlobal ou usando fixo em uma matriz de byte ou estrutura. Eu tendo a preferir o GCHandle porque me parece mais ".NET".

ctacke
fonte
Um pequeno problema de que talvez você não deva dizer "ponteiro gerenciado" aqui - mesmo com aspas assustadoras - porque isso é algo bastante diferente de uma referência de objeto, em IL. Embora haja sintaxe para ponteiros gerenciados em C ++ / CLI, eles geralmente não são acessíveis a partir de C #. Em IL, eles são obtidos com as instruções (isto é) ldloca e ldarga.
Glenn Slayden
5

Um ponteiro pode apontar para qualquer byte no espaço de endereço do aplicativo. Uma referência é fortemente restrita, controlada e gerenciada pelo ambiente .NET.

jdigital
fonte
1

O que os torna um tanto complexos não é o que são, mas o que você pode fazer com eles. E quando você tem um ponteiro para um ponteiro para um ponteiro. É quando realmente começa a ficar divertido.

Robert C. Barth
fonte
1

Um dos maiores benefícios das referências sobre os ponteiros é maior simplicidade e legibilidade. Como sempre, quando você simplifica algo, você o torna mais fácil de usar, mas ao custo da flexibilidade e do controle que obtém com o material de baixo nível (como outras pessoas mencionaram).

Os ponteiros são frequentemente criticados por serem “feios”.

class* myClass = new class();

Agora, toda vez que você usá-lo, você deve desreferenciá-lo primeiro por

myClass->Method() or (*myClass).Method()

Apesar de perder alguma legibilidade e adicionar complexidade, as pessoas ainda precisavam usar ponteiros frequentemente como parâmetros para que você pudesse modificar o objeto real (em vez de passar por valor) e para o ganho de desempenho por não ter que copiar objetos grandes.

Para mim, é por isso que as referências 'nasceram' em primeiro lugar para fornecer o mesmo benefício que os ponteiros, mas sem toda aquela sintaxe de ponteiro. Agora você pode passar o objeto real (não apenas seu valor) E você tem uma maneira normal e mais legível de interagir com o objeto.

MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

As referências C ++ diferem das referências C # / Java porque, uma vez que você atribui um valor a ele, não é possível reatribuí-lo (e ele deve ser atribuído quando foi declarado). Isso era o mesmo que usar um ponteiro const (um ponteiro que não pode ser re-apontado para outro objeto).

Java e C # são linguagens modernas de nível muito alto que limparam muitas das bagunças que se acumularam em C / C ++ ao longo dos anos e os ponteiros eram definitivamente uma daquelas coisas que precisavam ser 'limpas'.

No que diz respeito ao seu comentário sobre saber ponteiros o torna um programador mais forte, isso é verdade na maioria dos casos. Se você sabe 'como' algo funciona, ao invés de apenas usá-lo sem saber, eu diria que isso pode lhe dar uma vantagem. O quanto de uma borda sempre variará. Afinal, usar algo sem saber como é implementado é uma das muitas belezas de OOP e Interfaces.

Neste exemplo específico, o que saber sobre ponteiros o ajudaria com referências? Entender que uma referência C # NÃO é o próprio objeto, mas aponta para o objeto, é um conceito muito importante.

# 1: Você NÃO está passando por valor Bem, para começar, quando você usa um ponteiro, você sabe que o ponteiro contém apenas um endereço, é isso. A variável em si está quase vazia e é por isso que é tão bom passar como argumentos. Além do ganho de desempenho, você está trabalhando com o objeto real, portanto, quaisquer alterações feitas não são temporárias

# 2: Polimorfismo / Interfaces Quando você tem uma referência que é um tipo de interface e aponta para um objeto, você só pode chamar métodos dessa interface, embora o objeto possa ter muito mais habilidades. Os objetos também podem implementar os mesmos métodos de maneira diferente.

Se você entende bem esses conceitos, não acho que esteja perdendo muito por não ter usado ponteiros. C ++ é frequentemente usado como uma linguagem para aprender programação porque às vezes é bom sujar as mãos. Além disso, trabalhar com aspectos de nível inferior faz com que você aprecie os confortos de uma linguagem moderna. Comecei com C ++ e agora sou um programador C # e sinto que trabalhar com ponteiros brutos me ajudou a ter um melhor entendimento do que está acontecendo nos bastidores.

Não acho que seja necessário que todos comecem com ponteiros, mas o importante é que eles entendam por que referências são usadas em vez de tipos de valor e a melhor maneira de entender isso é olhar para seu ancestral, o ponteiro.

Despertar
fonte
1
Pessoalmente, acho que C # teria sido uma linguagem melhor se a maioria dos lugares que a usam .usasse ->, mas foo.bar(123)era sinônimo de uma chamada ao método estático fooClass.bar(ref foo, 123). Isso teria permitido coisas como myString.Append("George"); [o que modificaria a variável myString ], e tornou mais óbvia a diferença de significado entre myStruct.field = 3;e myClassObject->field = 3;.
supercat