2010-03-26 8 views
29

lectura http://www.cprogramming.com/tutorial/references.html, que dice:¿Por qué no necesito verificar si las referencias son inválidas/nulas?

En general, las referencias siempre debería ser válido, ya que debe siempre inicializar una referencia. Esto significa que salvo algunas circunstancias extrañas (ver a continuación), puede ser seguro de que el uso de una referencia es solo como el uso de una simple variable no referencia . No necesita verificar , asegúrese de que una referencia no sea apuntando a NULO, y no obtendrá mordido por una referencia no inicializada para la que olvidó asignar la memoria para.

Mi pregunta es cómo sé que la memoria del objeto no se ha liberado o eliminado DESPUÉS de haber inicializado la referencia.

De lo que se trata es de que no puedo tomar este consejo en la fe y necesito una mejor explicación.

¿Alguien puede arrojar algo de luz?

+0

Relacionados: http://stackoverflow.com/questions/1919608/checking-for-null-before-pointer- uso/1921403 # 1921403. –

+20

"cómo sé que la memoria del objeto no se ha liberado/eliminado DESPUÉS de haber inicializado la referencia". ¿Cómo sabes que alguien no ha 'reinterpet_cast' algún objeto completamente incorrecto a tu tipo, y lo pasó como referencia? ¿O corrompió la memoria en su objeto y luego se la pasó a usted? No, en C y C++ te ves obligado a confiar en que tu interlocutor no será un muppet. Si hacen algo que el lenguaje define como no válido, como eliminar referencias de un puntero nulo o eliminar un objeto pero manteniendo una referencia al mismo, entonces ese es su error, y pueden depurarlo. –

Respuesta

66

No se puede saber si las referencias no son válidos:

No hay manera de saber si su referencia hace referencia a la memoria válidos salvo por el cuidado de cómo utiliza referencias. Por ejemplo, no desea utilizar una referencia con algo creado en el montón si no está seguro de cuándo se eliminará la memoria.

Además, nunca podrá saber si el puntero que está utilizando apunta a la memoria válida o no.

Puede hacer controles NULL con dos punteros y referencias, pero normalmente nunca haría un cheque NULO con una referencia, porque nadie podría escribir código como este:

int *p = 0; 
int &r = *p;//no one does this 
if(&r != 0)//and so no one does this kind of check 
{ 
} 

Cuándo utilizar ¿una referencia?

Es posible que desee utilizar referencias en casos como éste:

//I want the function fn to not make a copy of cat and to use 
// the same memory of the object that was passed in 
void fn(Cat &cat) 
{ 
    //Do something with cat 
} 

//...main... 
Cat c; 
fn(c); 

de disparo en el pie es duro con referencias:

Es mucho más difícil de disparar en el pie con referencias de lo que es con punteros.

Por ejemplo:

int *p; 
if(true) 
{ 
    int x; 
    p = &x; 
} 

*p = 3;//runtime error 

No se puede hacer este tipo de cosas con referencias desde una referencia debe ser inicializado con el valor. Y solo puede inicializarlo con valores que están en su alcance.

Todavía puede dispararse en el pie con referencias, pero tiene que intentar REALMENTE hacerlo.

Por ejemplo:

int *p = new int; 
*p = 3; 
int &r = *p; 
delete p; 
r = 3;//runtime error 
+1

MUY bonito escribir. +1. –

+6

"probablemente no desee utilizar una referencia con algo creado en el montón"? Por qué no? Si creo una instancia en el montón, la paso por referencia a un método y luego la libero, estoy bien. El único problema sería si el método almacenara esa referencia y supusiera que seguiría siendo válida. –

+0

@Steven Sudit: Seguramente hay algunos escenarios, así que es por eso que probablemente utilicé. Al menos puedo decir en mi propio código si lo hago, entonces es relativamente raro. –

2

Es necesario mantener la cordura de sus variables - es decir, sólo se pasa una referencia/indicador a una función de si se conoce el alcance función no sobrevivirá a hacer referencia a/puntero.

Si va y libera un poco de control y luego trata de usar dicha referencia, estará leyendo la memoria libre.

5

No puede. Tampoco puedes con un puntero. Tenga en cuenta:



struct X 
{ 
    int * i; 
    void foo() { *i++; } 
}; 

int main() 
{ 
    int *i = new int(5); 
    X x = { i }; 
    delete i; 
    x.foo(); 
} 

Ahora, qué código se puede poner en X :: foo() para asegurarse de que el puntero i es todavía válida?

La respuesta es que no hay una verificación estándar. Hay algunos trucos que podrían funcionar en msvc en modo de depuración (buscando 0xfeeefeee o lo que sea), pero no hay nada que funcione de manera consistente.

Si necesita algún tipo de objeto que se asegure de que el puntero no apunta a la memoria liberada, necesitará algo mucho más inteligente que una referencia o un puntero estándar.

Es por esto que debe tener mucho cuidado con la semántica de propiedad y la administración de vida útil al trabajar con punteros y referencias.

+0

misma pregunta planteada a otra respuesta: ¿qué sucede si cuando elimino los punteros, configuro el puntero a nulo después y verifico si no? ¿No sería eso una manera de verificar si los punteros ya no son válidos? – jbu

+5

Siempre puede tener un puntero alias. Diga que 'p' y' q' son dos punteros a la misma memoria. Eliminar 'p' y establecer' p = NULL'. Ahora, ¿qué pasa con 'q'? No es válido y tampoco es NULO. No siempre es trivial saber cuáles son todos los alias de un puntero cuando elimina uno. –

1

Porque para cuando llegas allí, has hecho un comportamiento indefinido seguro. Voy a explicar :)

Digamos que tienes:

void fun(int& n); 

Ahora, si pasa algo como:

int* n = new int(5); 
fun(*n); // no problems until now! 

Pero si hace lo siguiente:

int* n = new int(5); 
... 
delete n; 
... 
fun(*n); // passing a deleted memory! 

Por Cuando llegue a fun, estará desmarcando * n, que es un comportamiento indefinido si el puntero se elimina como en el ejemplo anterior. Por lo tanto, no hay forma, y ​​debe haber ahora un camino real porque asumir parámetros válidos es el punto de referencia completo.

+0

¿Qué pasa si cuando borro los punteros, configuro el puntero a nulo luego y verifico nulo? ¿No sería eso una manera de verificar si los punteros ya no son válidos? – jbu

+0

@jbu: No importaría. Si tiene una referencia a un objeto y el objeto al que se refiere se va, tiene un comportamiento indefinido. Lo mejor es solucionar realmente el problema y asegurarse de que si se está refiriendo a algo no lo hará caer debajo de usted. – GManNickG

+1

@jbu: si después de la eliminación, configura el puntero como nulo, luego de eliminarlo * no debe * desreferenciar el puntero para inicializar una referencia. –

1

No hay sintaxis para comprobar si la referencia es válida. Puede probar el puntero para NULL, pero no hay una prueba válida/inválida para una referencia. Por supuesto, el objeto referenciado puede ser liberado o sobrescrito por algún código defectuoso. La misma situación es para los punteros: si el puntero no NULL apunta al objeto liberado, no puede probarlo.

+0

No puede probar una referencia en contra de NULL? ¿Qué pasa si lo hago & myref == NULL? – codymanix

+0

Puede probar una referencia para null. – codenheim

+0

@mrjoltcola: no, no puedes. Lo que @codymanix sugiere no está definido porque la existencia de una referencia nula no está definida. – jalf

1

Lo corto es que podría suceder, pero si lo hace, usted tiene un serio problema de diseño. Tampoco tiene una forma real de detectarlo. La respuesta es diseñar su programa para evitar que ocurra, sin tratar de construir algún tipo de control que realmente no funcione (porque no puede).

3

En C++, las referencias están destinadas principalmente a ser utilizadas como parámetros y tipos de funciones de retorno. En el caso de un parámetro, una referencia no puede referirse a un objeto que ya no existe (asumiendo un solo programa de subprocesos) debido a la naturaleza de una llamada de función. En el caso de un valor de retorno, uno debe restringirse a las variables de miembros de la clase que regresan, cuya vida útil es más larga que la llamada a la función, o los parámetros de referencia que se pasan a la función.

+0

A menos que el programa de un solo subproceso pase a eliminar la dirección de la referencia que sabe que está en el montón. Uno no haría esto, pero es posible :) –

+0

La respuesta de Neil es correcta. –

+0

"asumiendo un solo programa de subprocesos". Eh? 'void f (int & r, int * p) {eliminar p; r + = 1; } int main() {int * a = new int(); f (* a, a); } ' Crea una referencia colgante. No veo qué tiene que ver el algo único o multihilo del programa con nada. Incluso teniendo en cuenta solo las funciones automáticas, una vez que alguien tiene una referencia, pueden almacenarla en un objeto (a través de un constructor), almacenar un puntero a ese objeto (en forma global) para escapar del alcance del objeto y pasarlo más tarde como un parámetro de función. UB, por supuesto, entonces "no puede suceder", pero nada que ver con la naturaleza de una llamada fn. –

0

Creo que "depende". Lo sé, esa no es una respuesta, pero realmente depende. Creo que codificar a la defensiva es una buena práctica a seguir. Ahora, si su pista de pila tiene 10 niveles de profundidad y cualquier falla en la ruta hace que toda la operación falle, entonces por supuesto, verifique en el nivel superior y deje que las excepciones lleguen a la cima. pero si puede recuperarse de alguien que le pasa una referencia nula, el cheque donde corresponda. En mi experiencia, donde tengo que combinar el código con otras compañías para integrarlas juntas, verificar (y registrar) todo a nivel de api público te permite desviar la señalización que ocurre cuando la integración no funciona como se esperaba.

1

Las referencias de C++ son alias. El efecto de esto es que las desreferencias a los indicadores no necesariamente ocurren donde aparecen, sino que ocurren donde se evalúan. Tomar una referencia a un objeto no evalúa el objeto, lo alía. Usar la referencia es lo que evalúa el objeto. C++ no puede garantizar que las referencias sean válidas; si lo hace, todos los compiladores de C++ están rotos. La única forma de hacerlo es eliminar toda posibilidad de asignación dinámica con referencias. En la práctica, la suposición es que una referencia es un objeto válido. Como * NULL no está definido & no válido, se deduce que para p = NULL, * p tampoco está definido. El problema con C++ es * p se pasará efectivamente a una función, o se retrasará en su evaluación hasta que la referencia se utilice realmente. Argumentar que no está definido no es el punto de la pregunta del que pregunta. Si fuera ilegal, el compilador lo haría cumplir, y también lo haría el estándar. Tampoco lo hace, de lo que estoy enterado.

int &r = *j; // aliases *j, but does not evaluate j 
if(r > 0) // evaluates r ==> *j, resulting in dereference (evaluate j) at this line, not what some expect 
    ; 

1) Se puede probar una referencia para aliasing un puntero NULL, & r es simplemente & (cualquiera que sea r alias a) (EDIT)

2) Cuando se pasa un puntero "dereferenced" (* i) como un parámetro de referencia, la desreferencia no ocurre en el callsite, puede que nunca suceda, porque es una referencia (las referencias son alias, no evaluaciones). Esa es la optimización de las referencias. Si se evaluaron en el callsite, o bien el compilador está insertando código adicional, o sería una llamada por valor y menos rendimiento que un puntero.

Sí, la referencia en sí no es NULA, no es válida, del mismo modo que * NULL no es válido. Es la demora en la evaluación de las expresiones de desreferencia que no concuerda con la afirmación de que es imposible tener una referencia inválida.

#include <iostream> 

int fun(int & i) { 
    std::cerr << &i << "\n"; 
    std::cerr << i << "\n"; // crash here 
} 

int main() { 
    int * i = new int(); 
    i = 0; 
    fun(*i); // Why not crash here? Because the deref doesn't happen here, inconsistent, but critical for performance of references 
} 

EDIT: cambió mi ejemplo, como se ha malinterpretado como sugerencia para referencias de pruebas, lo que no quería demostrar. Solo quería demostrar la referencia inválida.

+0

Su "* i" es un comportamiento indefinido ... tal vez funcione, tal vez se bloquee más tarde, o tal vez alcance con una mano detrás de su espalda y lo estrangule con un calcetín viejo ... –

+0

Comportamiento indefinido significa que la especificación no hacer un juicio El punto es que es un comportamiento legal en cada compilador que conozco y sucede en el código del mundo real que mezcla punteros con bibliotecas que hacen uso de parámetros de referencia. – codenheim

+0

"Algunas de las respuestas" están hablando de lo que define el estándar. El hecho de que algunas implementaciones garanticen cierto comportamiento en casos particulares de UB, o no lo garanticen pero realicen algo predecible, no significa que las respuestas sean incorrectas. Es solo la diferencia entre describir una interfaz y describir una o más implementaciones particulares de esa interfaz. –

4

Mi pregunta es cómo sé que la memoria del objeto no se ha liberado o eliminado DESPUÉS de haber inicializado la referencia.

En primer lugar, hay Nunca alguna manera de detectar si una ubicación de memoria ha sido liberado/borrado. Eso no tiene nada que ver con si es nulo o no. Lo mismo es cierto para un puntero. Dado un puntero, no tiene forma de asegurarse de que apunta a una memoria válida. Puede probar si un puntero es nulo o no, pero eso es todo. Un puntero no nulo aún puede apuntar a la memoria liberada. O puede señalar a algún lugar de basura.

En cuanto a las referencias, lo mismo se aplica en el sentido de que no tiene forma de determinar si hace referencia a un objeto que todavía es válido. Sin embargo, no existe una "referencia nula" en C++, por lo que no es necesario verificar si la referencia "es nula".

Por supuesto, es posible escribir código que crea lo que parece ser una "referencia nula", y ese código se compilará. Pero no será correcto. De acuerdo con el estándar de lenguaje C++, las referencias a null no se pueden crear. Intentar hacerlo es un comportamiento indefinido.

Lo que se pretende es que no puedo tomar este consejo sobre la fe y necesito una explicación mejor

La mejor explicación es la siguiente: "unos puntos de referencia a un objeto válido porque configúralo para que apunte a un objeto válido ". No tienes que tomarlo por fe. Solo tiene que mirar el código donde creó la referencia. Señaló a un objeto válido en ese momento, o no lo hizo. Si no lo hizo, entonces el código es incorrecto y debe cambiarse.

Y la referencia sigue siendo válida porque sabe que se va a utilizar, por lo que se ha asegurado de no invalidar el objeto al que hace referencia.

Es así de simple. Las referencias se mantienen válidas siempre que no destruyas el objeto al que apuntan. Así que no destruyas el objeto al que apunta hasta que la referencia ya no sea necesaria.

+0

@jalf: Ignorar las características basadas en "no está definido" no está ayudando al solicitante, en mi opinión. Todos sabemos que ocurre "diversión (\ * i)". Cualquier función con parámetros de referencia posiblemente reciba una "referencia nula" que no se encontrará hasta que se evalúe en el destinatario. Esa es la diferencia. Sí, debes verificar el puntero antes de la llamada, estoy de acuerdo. Pero si C++ _realmente_ hiciera la garantía que usted indica, tendría que verificar las referencias del puntero en el punto de la expresión "* i", antes de la llamada a la función. Por favor, apúntame al estándar que cubre esto. – codenheim

+2

@mrjoltcola: cuando C++ "garantiza" algo, cualquier cosa, esa garantía está sujeta a que el programa esté bien formado y no invoque un comportamiento indefinido. Si C++ no "realmente garantiza" que una referencia no es nula, entonces tampoco "realmente garantiza" que 1 + 1 == 2. Cualquiera o ambos pueden fallar una vez que se ha invocado el comportamiento indefinido. 1.3.12 dice "ignorar la situación completamente con resultados impredecibles" es permisible. –

+0

Me has convencido, pero las respuestas que dicen "no puede pasar" son incorrectas y eso es lo que provocó mi respuesta. "No puede suceder" es la elección incorrecta de las palabras.Creo que todos han malinterpretado mi punto. – codenheim

0

Creo que se podría beneficiar de un simple paralelismo:

  • T & es similar a T * const
  • T const & es similar a T const * const

Las referencias son muy similares a const en su intento, se llevan un significado y ayudan a escribir un código más claro, pero no proporcionan un comportamiento de tiempo de ejecución diferente.

Ahora para responder a su pregunta: sí, es posible que una referencia sea nula o no válida. Puede hacer una prueba para una referencia nula (T& t = ; if (&t == 0)) pero no debería suceder >> por contrato una referencia es válida.

Cuándo utilizar referencia vs puntero? Utilizar un puntero si:

  • que desea ser capaz de cambiar la pointee
  • desea expresar la posible nulidad

En cualquier otro caso, utilice una referencia.

Algunos ejemplos:

// Returns an object corresponding to the criteria 
// or a special value if it cannot be found 
Object* find(...); // returns 0 if fails 

// Returns an object corresponding to the criteria 
// or throw "NotFound" if it cannot be found 
Object& find(...); // throw NotFound 

Paso de argumentos:

void doSomething(Object* obj) 
{ 
    if (obj) obj->doSomething(); 
} 

void doSomething(Object& obj) { obj.do(); obj.something(); } 

Atributos:

struct Foo 
{ 
    int i; 
    Bar* b; // No constructor, we need to initialize later on 
}; 

class Foo 
{ 
public: 
    Foo(int i, Bar& b): i(i), b(b) {} 
private: 
    int i; 
    Bar& b; // will always point to the same object, Foo not Default Constructible 
}; 

class Other 
{ 
public: 
    Other(Bar& b): b(&b) {} // NEED to pass a valid object for init 

    void swap(Other& rhs); // NEED a pointer to be able to exchange 

private: 
    Bar* b; 
}; 

Funcionalmente referencias y punteros juegan el mismo papel. Es solo una cuestión de contrato.Y desafortunadamente, ambos pueden invocar Comportamiento no definido si elimina el objeto al que se refieren, no hay ningún ganador allí;)

Cuestiones relacionadas