¿Cuál es la diferencia entre una referencia de C # y un puntero?

85

No entiendo muy bien la diferencia entre una referencia de C # y un puntero. Ambos señalan un lugar en la memoria, ¿no es así? La única diferencia que puedo descubrir es que los punteros no son tan inteligentes, no pueden apuntar a nada en el montón, están exentos de la recolección de basura y solo pueden hacer referencia a estructuras o tipos base.

Una de las razones por las que pregunto es que existe la percepción de que la gente necesita entender bien los indicadores (de C, supongo) para ser un buen programador. Muchas personas que aprenden idiomas de nivel superior se pierden esto y, por lo tanto, tienen esta debilidad.

Simplemente no entiendo qué tiene de complejo un puntero. Básicamente es solo una referencia a un lugar en la memoria, ¿no es así? ¿Puede devolver su ubicación e interactuar con el objeto en esa ubicación directamente?

¿Me he perdido un punto importante?

Ricardo
fuente
1
La respuesta corta es sí, se ha perdido algo razonablemente significativo, y esa es la razón de "... la percepción de que la gente necesita entender los indicadores". Sugerencia: C # no es el único idioma que existe.
jdigital

Respuestas:

50

Las referencias de C # pueden, y serán reubicadas por el recolector de basura, pero los punteros normales son estáticos. Es por eso que usamos fixedpalabras clave cuando adquirimos un puntero a un elemento de matriz, para evitar que se mueva.

EDITAR: Conceptualmente, sí. Son más o menos iguales.

Mehrdad Afshari
fuente
¿No hay otro comando que evite que una referencia de C # tenga el objeto a su referencia movido por el GC?
Richard
Oh, lo siento, pensé que era otra cosa porque la publicación se refería a un puntero.
Richard
Sí, un GCHandle.Alloc o un Marshal.AllocHGlobal (más allá de lo fijo)
ctacke
Se corrigió en C #, pin_ptr en C ++ / CLI
Mehrdad Afshari
Marshal.AllocHGlobal no asignará memoria en el montón administrado y, naturalmente, no está sujeto a la recolección de basura.
Mehrdad Afshari
130

Existe una distinción leve, pero extremadamente importante, entre un puntero y una referencia. Un puntero apunta a un lugar en la memoria mientras que una referencia apunta a un objeto en la memoria. Los punteros no son "seguros para escribir" en el sentido de que no se puede garantizar la exactitud de la memoria a la que apuntan.

Tomemos, por ejemplo, el siguiente código

int* p1 = GetAPointer();

Este es un tipo seguro en el sentido de que GetAPointer debe devolver un tipo compatible con int *. Sin embargo, todavía no hay garantía de que * p1 apunte realmente a un int. Podría ser un char, doble o simplemente un puntero a una memoria aleatoria.

Sin embargo, una referencia apunta a un objeto específico. Los objetos se pueden mover en la memoria, pero la referencia no se puede invalidar (a menos que use código inseguro). Las referencias son mucho más seguras a este respecto que los punteros.

string str = GetAString();

En este caso, str tiene uno de dos estados: 1) no apunta a ningún objeto y, por tanto, es nulo o 2) apunta a una cadena válida. Eso es. El CLR garantiza que este sea el caso. No puede y no lo hará para un puntero.

JaredPar
fuente
14
Gran explicación
iTayb
13

Una referencia es un puntero "abstracto": no puedes hacer aritmética con una referencia y no puedes hacer trucos de bajo nivel con su valor.

Chris Conway
fuente
8

Una diferencia importante entre una referencia y un puntero es que un puntero es una colección de bits cuyo contenido solo importa cuando se usa activamente como puntero, mientras que una referencia encapsula no solo un conjunto de bits, sino también algunos metadatos que mantienen la marco subyacente informado de su existencia. Si existe un puntero a algún objeto en la memoria, y ese objeto se elimina pero el puntero no se borra, la existencia continua del puntero no causará ningún daño a menos que o hasta que se intente acceder a la memoria a la que apunta. Si no se intenta utilizar el puntero, nada se preocupará por su existencia. Por el contrario, los marcos basados ​​en referencias como .NET o la JVM requieren que siempre sea posible que el sistema identifique cada referencia de objeto existente, y cada referencia de objeto existente debe ser siemprenull o bien identificar un objeto de su tipo adecuado.

Tenga en cuenta que cada referencia de objeto en realidad encapsula dos tipos de información: (1) el contenido de campo del objeto que identifica y (2) el conjunto de otras referencias al mismo objeto. Aunque no existe ningún mecanismo mediante el cual el sistema pueda identificar rápidamente todas las referencias que existen a un objeto, el conjunto de otras referencias que existen a un objeto a menudo puede ser lo más importante encapsulado por una referencia (esto es especialmente cierto cuando las cosas de tipo Objectse usan como cosas como fichas de bloqueo). Aunque el sistema guarda algunos bits de datos para cada objeto para su uso GetHashCode, los objetos no tienen una identidad real más allá del conjunto de referencias que existen a ellos. Si Xtiene la única referencia existente a un objeto, reemplazandoXcon una referencia a un nuevo objeto con el mismo contenido de campo no tendrá ningún efecto identificable excepto para cambiar los bits devueltos por GetHashCode(), e incluso ese efecto no está garantizado.

Super gato
fuente
5

Los punteros apuntan a una ubicación en el espacio de direcciones de la memoria. Las referencias apuntan a una estructura de datos. Todas las estructuras de datos se mueven todo el tiempo (bueno, no tan a menudo, pero de vez en cuando) por el recolector de basura (para compactar el espacio de memoria). Además, como dijiste, las estructuras de datos sin referencias obtendrán basura recolectada después de un tiempo.

Además, los punteros solo se pueden utilizar en contextos no seguros.

Tamas Czinege
fuente
5

Creo que es importante para los desarrolladores comprender el concepto de puntero, es decir, comprender la indirección. Eso no significa que necesariamente tengan que usar punteros. También es importante comprender que el concepto de referencia difiere del concepto de puntero , aunque solo sutilmente, pero que la implementación de una referencia casi siempre es un puntero.

Es decir, una variable que contiene una referencia es solo un bloque de memoria del tamaño de un puntero que sostiene un puntero al objeto. Sin embargo, esta variable no se puede utilizar de la misma forma que se puede utilizar una variable de puntero. En C # (y C, y C ++, ...), un puntero se puede indexar como una matriz, pero una referencia no. En C #, el recolector de elementos no utilizados realiza un seguimiento de una referencia, no un puntero. En C ++, se puede reasignar un puntero, no una referencia. Sintácticamente y semánticamente, los punteros y las referencias son bastante diferentes, pero mecánicamente son lo mismo.

Papi
fuente
La cuestión de la matriz suena interesante, ¿es básicamente donde puede decirle al puntero que compense la ubicación de la memoria como una matriz mientras no puede hacer esto con una referencia? No puedo pensar cuándo sería útil pero interesante de todos modos.
Richard
Si p es un int * (un puntero a un int), entonces (p + 1) es la dirección identificada por p + 4 bytes (el tamaño de un int). Y p [1] es lo mismo que * (p + 1) (es decir, "desreferencia" la dirección 4 bytes después de p). Por el contrario, con una referencia de matriz (en C #), el operador [] realiza una llamada de función.
P Daddy
5

Primero creo que necesitas definir un "Puntero" en tu semántica. ¿Te refieres al puntero que puedes crear en código inseguro con fijo ? ¿Te refieres a un IntPtr que recibes de quizás una llamada nativa o Marshal.AllocHGlobal ? ¿Te refieres a un GCHandle ? Todos son esencialmente lo mismo, una representación de una dirección de memoria donde se almacena algo, ya sea una clase, un número, una estructura, lo que sea. Y para que conste, ciertamente pueden estar en el montón.

Un puntero (todas las versiones anteriores) es un elemento fijo. El GC no tiene idea de qué hay en esa dirección y, por lo tanto, no tiene capacidad para administrar la memoria o la vida del objeto. Eso significa que pierde todos los beneficios de un sistema de recolección de basura. Debe administrar manualmente la memoria del objeto y existe la posibilidad de que se produzcan fugas.

Una referencia, por otro lado, es prácticamente un "puntero administrado" que el GC conoce. Sigue siendo una dirección de un objeto, pero ahora el GC conoce los detalles del objetivo, por lo que puede moverlo, hacer compactaciones, finalizar, desechar y todas las demás cosas buenas que hace un entorno administrado.

La principal diferencia, en realidad, está en cómo y por qué los usaría. Para la gran mayoría de los casos en un lenguaje administrado, usará una referencia de objeto. Los punteros se vuelven útiles para hacer interoperabilidad y la rara necesidad de un trabajo realmente rápido.

Editar: De hecho, aquí hay un buen ejemplo de cuándo puede usar un "puntero" en el código administrado; en este caso, es un GCHandle, pero exactamente lo mismo podría haberse hecho con AllocHGlobal o usando fijo en una matriz de bytes o estructura. Tiendo a preferir GCHandle porque me parece más ".NET".

ctacke
fuente
Una pequeña objeción que tal vez no debería decir "puntero administrado" aquí, incluso con comillas de miedo, porque esto es algo bastante diferente de una referencia de objeto, en IL. Aunque existe una sintaxis para punteros administrados en C ++ / CLI, generalmente no son accesibles desde C #. En IL, se obtienen con las instrucciones (ie) ldloca y ldarga.
Glenn Slayden
5

Un puntero puede apuntar a cualquier byte en el espacio de direcciones de la aplicación. Una referencia está estrictamente restringida y controlada y administrada por el entorno .NET.

jdigital
fuente
1

Lo que tienen los punteros que los hace algo complejos no es lo que son, sino lo que puedes hacer con ellos. Y cuando tienes un puntero a un puntero a un puntero. Ahí es cuando realmente comienza a divertirse.

Robert C. Barth
fuente
1

Uno de los mayores beneficios de las referencias sobre los punteros es una mayor simplicidad y legibilidad. Como siempre, cuando simplifica algo, lo hace más fácil de usar, pero a costa de la flexibilidad y el control que obtiene con las cosas de bajo nivel (como han mencionado otras personas).

Los punteros a menudo son criticados por ser "feos".

class* myClass = new class();

Ahora, cada vez que lo use, primero debe eliminar la referencia

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

A pesar de perder algo de legibilidad y agregar complejidad, las personas aún necesitaban usar punteros a menudo como parámetros para poder modificar el objeto real (en lugar de pasar por valor) y para obtener una ganancia de rendimiento al no tener que copiar objetos grandes.

Para mí, esta es la razón por la que las referencias 'nacieron' en primer lugar para proporcionar el mismo beneficio que los punteros pero sin toda esa sintaxis de punteros. Ahora puede pasar el objeto real (no solo su valor) Y tiene una forma más legible y normal de interactuar con el objeto.

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

Las referencias de C ++ difieren de las referencias de C # / Java en que una vez que le asigna un valor que era, no puede reasignarlo (y debe asignarse cuando se declaró). Esto era lo mismo que usar un puntero constante (un puntero que no se podía volver a apuntar a otro objeto).

Java y C # son lenguajes modernos de muy alto nivel que limpiaron muchos de los líos que se habían acumulado en C / C ++ a lo largo de los años y los punteros eran definitivamente una de esas cosas que necesitaban ser 'limpiadas'.

En la medida en que su comentario acerca de conocer los punteros lo convierte en un programador más fuerte, esto es cierto en la mayoría de los casos. Si sabe 'cómo' funciona algo en lugar de simplemente usarlo sin saberlo, diría que esto a menudo puede darle una ventaja. La cantidad de ventaja siempre variará. Después de todo, usar algo sin saber cómo se implementa es una de las muchas bellezas de OOP e Interfaces.

En este ejemplo específico, ¿qué le ayudaría saber sobre punteros con las referencias? Comprender que una referencia de C # NO es el objeto en sí, sino que apunta al objeto, es un concepto muy importante.

# 1: NO está pasando por valor Bueno, para empezar, cuando usa un puntero, sabe que el puntero contiene solo una dirección, eso es todo. La variable en sí está casi vacía y por eso es tan agradable pasarla como argumentos. Además de la ganancia de rendimiento, está trabajando con el objeto real, por lo que los cambios que realice no son temporales

# 2: Polimorfismo / Interfaces Cuando tienes una referencia que es un tipo de interfaz y apunta a un objeto, solo puedes llamar a métodos de esa interfaz aunque el objeto tenga muchas más habilidades. Los objetos también pueden implementar los mismos métodos de manera diferente.

Si comprende bien estos conceptos, no creo que se esté perdiendo demasiado por no haber utilizado punteros. C ++ se usa a menudo como lenguaje para aprender a programar porque a veces es bueno ensuciarse las manos. Además, trabajar con aspectos de nivel inferior te hace apreciar las comodidades de un lenguaje moderno. Comencé con C ++ y ahora soy un programador de C # y siento que trabajar con punteros en bruto me ha ayudado a comprender mejor lo que sucede debajo del capó.

No creo que sea necesario que todos comiencen con punteros, pero lo importante es que comprendan por qué se usan referencias en lugar de tipos de valor y la mejor manera de entenderlo es mirar a su ancestro, el puntero.

Despertar
fuente
1
Personalmente, creo que C # habría sido un lenguaje mejor si la mayoría de los lugares que lo usan lo .usaran ->, pero foo.bar(123)fuera sinónimo de una llamada al método estático fooClass.bar(ref foo, 123). Eso habría permitido cosas como myString.Append("George"); [que modificaría la variable myString ], y hizo más obvia la diferencia de significado entre myStruct.field = 3;y myClassObject->field = 3;.
supercat