c++ - que - Cuándo usar referencias vs. punteros
punteros en c (19)
Como otros ya respondieron: siempre use referencias, a menos que la variable sea NULL
/ nullptr
sea realmente un estado válido.
El punto de vista de John Carmack sobre el tema es similar:
Los punteros nulos son el mayor problema en C / C ++, al menos en nuestro código. El uso dual de un solo valor como marca y dirección causa una cantidad increíble de problemas fatales. Las referencias a C ++ deben favorecerse sobre los punteros siempre que sea posible; mientras que una referencia es "realmente" solo un puntero, tiene el contrato implícito de no ser NULL. Realice comprobaciones NULL cuando los punteros se conviertan en referencias, luego puede ignorar el problema a partir de entonces.
http://www.altdevblogaday.com/2011/12/24/static-code-analysis/
Editar 2012-03-13
El usuario Bret Kuhns comenta acertadamente:
El estándar C ++ 11 ha sido finalizado. Creo que es hora de mencionar en este hilo que la mayoría del código debería funcionar perfectamente con una combinación de referencias, shared_ptr y unique_ptr.
Es cierto, pero la pregunta sigue siendo, incluso cuando se reemplazan los punteros en bruto con punteros inteligentes.
Por ejemplo, tanto std::unique_ptr
como std::shared_ptr
se pueden construir como punteros "vacíos" a través de su constructor predeterminado:
- http://en.cppreference.com/w/cpp/memory/unique_ptr/unique_ptr
- http://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr
... lo que significa que usarlos sin verificar que no están vacíos corre el riesgo de un choque, que es exactamente de lo que trata la discusión de J. Carmack.
Y luego, tenemos el divertido problema de "¿cómo pasamos un puntero inteligente como parámetro de función?"
La answer Jon a la pregunta C ++: pasar referencias a boost :: shared_ptr , y los siguientes comentarios muestran que incluso entonces, pasar un puntero inteligente por copia o por referencia no es tan claro como a uno le gustaría por referencia "por defecto, pero podría estar equivocado).
Entiendo la sintaxis y la semántica general de los punteros frente a las referencias, pero ¿cómo debo decidir cuándo es más o menos apropiado usar referencias o punteros en una API?
Naturalmente, algunas situaciones necesitan una o la otra (el operator++
necesita un argumento de referencia), pero en general estoy encontrando que prefiero usar punteros (y punteros const), ya que la sintaxis es clara y las variables se pasan de forma destructiva.
Por ejemplo, en el siguiente código:
void add_one(int& n) { n += 1; }
void add_one(int* const n) { *n += 1; }
int main() {
int a = 0;
add_one(a); // Not clear that a may be modified
add_one(&a); // ''a'' is clearly being passed destructively
}
Con el puntero, siempre es (más) obvio lo que está pasando, por lo que para las API y similares donde la claridad es una gran preocupación, ¿los punteros no son más apropiados que las referencias? ¿Eso significa que las referencias solo deben usarse cuando sea necesario (p. Ej., operator++
)? ¿Hay algún problema de rendimiento con uno u otro?
EDITAR (ACTUALIZADO):
Además de permitir valores NULL y tratar con matrices en bruto, parece que la elección se reduce a las preferencias personales. He aceptado la respuesta a continuación que hace referencia a la Guía de estilo de C ++ de Google , ya que presentan la opinión de que "Las referencias pueden ser confusas, ya que tienen una sintaxis de valor pero una semántica de puntero".
Debido al trabajo adicional requerido para desinfectar los argumentos del puntero que no deben ser NULL (por ejemplo, add_one(0)
llamará a la versión del puntero y se interrumpirá durante el tiempo de ejecución), tiene sentido desde una perspectiva de mantenibilidad el uso de referencias donde un objeto DEBE estar presente, sin embargo Es una pena perder la claridad sintáctica.
Copiado de wiki -
Una consecuencia de esto es que, en muchas implementaciones, operar en una variable con vida útil automática o estática a través de una referencia, aunque sintácticamente similar a acceder directamente a ella, puede implicar operaciones de desreferencia oculta que son costosas. Las referencias son una característica sintácticamente controvertida de C ++ porque ocultan el nivel de direccionamiento indirecto de un identificador; es decir, a diferencia del código C, donde los punteros generalmente se destacan de manera sintáctica, en un bloque grande de código C ++ puede no ser inmediatamente obvio si el objeto al que se accede se define como una variable local o global o si es una referencia (puntero implícito) a alguna otra ubicación, especialmente si el código mezcla referencias y punteros. Este aspecto puede hacer que el código C ++ mal escrito sea más difícil de leer y depurar (ver Aliasing).
Estoy de acuerdo al 100% con esto, y es por eso que creo que solo debes usar una referencia cuando tengas una muy buena razón para hacerlo.
Cualquier diferencia de rendimiento sería tan pequeña que no justificaría utilizar el enfoque que es menos claro.
Primero, un caso que no se mencionó donde las referencias son generalmente superiores son las referencias const
. Para los tipos que no son simples, pasar una const reference
evita crear un temporal y no causa la confusión que le preocupa (porque el valor no se modifica). En este caso, obligar a una persona a pasar un puntero causa la misma confusión que le preocupa, ya que ver la dirección tomada y pasarla a una función podría hacerle pensar que el valor cambió.
En cualquier caso, básicamente estoy de acuerdo contigo. No me gustan las funciones que toman referencias para modificar su valor cuando no es muy obvio que esto es lo que hace la función. Yo también prefiero usar punteros en ese caso.
Cuando necesita devolver un valor en un tipo complejo, tiendo a preferir referencias. Por ejemplo:
bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative
Aquí, el nombre de la función deja claro que está obteniendo información de nuevo en una matriz. Así que no hay confusión.
Las principales ventajas de las referencias son que siempre contienen un valor válido, son más limpios que los punteros y admiten el polimorfismo sin necesidad de una sintaxis adicional. Si ninguna de estas ventajas se aplica, no hay razón para preferir una referencia sobre un puntero.
De C ++ FAQ Lite -
Use referencias cuando pueda y punteros cuando tenga que hacerlo.
Las referencias generalmente se prefieren a los punteros siempre que no necesite "reubicación". Esto generalmente significa que las referencias son más útiles en la interfaz pública de una clase. Las referencias suelen aparecer en la piel de un objeto y los punteros en el interior.
La excepción a lo anterior es cuando el parámetro o el valor de retorno de una función necesita una referencia "centinela", una referencia que no se refiere a un objeto. Por lo general, la mejor manera de hacer esto es devolver / tomar un puntero y darle al puntero NULL este significado especial (las referencias siempre deben ser alias de los objetos, no un puntero NULL sin referencia).
Nota: a los programadores de la antigua línea C a veces no les gustan las referencias, ya que proporcionan semánticas de referencia que no están explícitas en el código del llamante. Sin embargo, después de cierta experiencia en C ++, uno se da cuenta rápidamente de que esta es una forma de ocultación de información, que es un activo más que un pasivo. Por ejemplo, los programadores deben escribir código en el lenguaje del problema en lugar del lenguaje de la máquina.
Descargo de responsabilidad: aparte del hecho de que las referencias no pueden ser NULL ni "rebote" (lo que significa que no pueden cambiar el objeto del que son alias), realmente se trata de una cuestión de gusto, por lo que no voy a decir "este es mejor".
Dicho esto, no estoy de acuerdo con su última declaración en el post, ya que no creo que el código pierda claridad con las referencias. En tu ejemplo,
add_one(&a);
podría ser más claro que
add_one(a);
ya que sabes que lo más probable es que el valor de a cambie. Por otro lado, sin embargo, la firma de la función.
void add_one(int* const n);
de alguna manera tampoco está claro: ¿n va a ser un entero entero o una matriz? A veces solo tiene acceso a encabezados (mal documentados) y firmas como
foo(int* const a, int b);
No son fáciles de interpretar a primera vista.
En mi opinión, las referencias son tan buenas como punteros cuando no se necesita (re) asignación ni reencuadernación (en el sentido explicado anteriormente). Además, si un desarrollador solo usa punteros para arreglos, las firmas de funciones son algo menos ambiguas. Sin mencionar el hecho de que la sintaxis de los operadores es mucho más legible con referencias.
En general, una variable miembro nunca debe ser una referencia porque no hay ningún punto en eso. Hace que la clase no sea asignable si no proporciona un operador de asignación. Además, una vez que establece la referencia de miembro para referirse a algún objeto, no es posible cambiar ese miembro para referir otro objeto. El uso más apropiado de una referencia es usar como un parámetro de función que permite pasar por referencia.
Hay un problema con la regla de " usar referencias siempre que sea posible " y surge si desea mantener la referencia para un uso posterior. Para ilustrar esto con el ejemplo, imagina que tienes las siguientes clases.
class SimCard
{
public:
explicit SimCard(int id):
m_id(id)
{
}
int getId() const
{
return m_id;
}
private:
int m_id;
};
class RefPhone
{
public:
explicit RefPhone(const SimCard & card):
m_card(card)
{
}
int getSimId()
{
return m_card.getId();
}
private:
const SimCard & m_card;
};
Al principio puede parecer una buena idea tener un parámetro en el RefPhone(const SimCard & card)
pasado por una referencia, ya que evita que se pasen punteros incorrectos / nulos al constructor. De alguna manera fomenta la asignación de variables en la pila y la obtención de beneficios de RAII.
PtrPhone nullPhone(0); //this will not happen that easily
SimCard * cardPtr = new SimCard(666); //evil pointer
delete cardPtr; //muahaha
PtrPhone uninitPhone(cardPtr); //this will not happen that easily
Pero entonces los temporarios vienen a destruir tu mundo feliz.
RefPhone tempPhone(SimCard(666)); //evil temporary
//function referring to destroyed object
tempPhone.getSimId(); //this can happen
Entonces, si se apega ciegamente a las referencias, se intercambia la posibilidad de pasar punteros no válidos a la posibilidad de almacenar referencias a objetos destruidos, lo que básicamente tiene el mismo efecto.
edición: tenga en cuenta que me atengo a la regla "Use la referencia donde pueda, los punteros donde sea necesario. Evite los punteros hasta que no pueda". de la respuesta más votada y aceptada (otras respuestas también lo sugieren). Aunque debería ser obvio, el ejemplo no es mostrar que las referencias como tales sean malas. Sin embargo, pueden ser mal utilizados, al igual que los punteros, y pueden traer sus propias amenazas al código.
Hay diferencias siguientes entre punteros y referencias.
- Cuando se trata de pasar variables, el paso por referencia se parece al paso por valor, pero tiene una semántica de puntero (actúa como puntero).
- La referencia no se puede inicializar directamente a 0 (nulo).
- La referencia (referencia, objeto no referenciado) no se puede modificar (equivalente al puntero "* const").
- Referencia constante puede aceptar parámetro temporal.
- Las referencias constantes locales prolongan la vida útil de los objetos temporales.
Teniendo en cuenta eso, mis reglas actuales son las siguientes.
- Use referencias para los parámetros que se usarán localmente dentro del alcance de una función.
- Use punteros cuando 0 (nulo) es un valor de parámetro aceptable o si necesita almacenar un parámetro para su uso posterior. Si se acepta 0 (nulo), estoy agregando el sufijo "_n" al parámetro, use el puntero protegido (como QPointer en Qt) o simplemente documéntelo. También puede utilizar punteros inteligentes. Debe tener más cuidado con los punteros compartidos que con los punteros normales (de lo contrario, puede terminar con las fugas de memoria de diseño y el desorden de responsabilidad).
Las actuaciones son exactamente iguales, ya que las referencias se implementan internamente como punteros. Por lo tanto no tienes que preocuparte por eso.
No hay una convención generalmente aceptada sobre cuándo usar referencias y punteros. En algunos casos, tiene que devolver o aceptar referencias (copia del constructor, por ejemplo), pero aparte de eso, tiene la libertad de hacer lo que desee. Una convención bastante común que he encontrado es usar referencias cuando el parámetro debe referirse a un objeto existente y punteros cuando un valor NULO está bien.
Algunas convenciones de codificación (como la de Google''s ) prescriben que siempre se deben usar punteros o referencias const, porque las referencias tienen un poco de sintaxis poco clara: tienen un comportamiento de referencia pero una sintaxis de valor.
Las referencias son más limpias y fáciles de usar, y ocultan mejor la información. Sin embargo, las referencias no pueden ser reasignadas. Si necesita apuntar primero a un objeto y luego a otro, debe usar un puntero. Las referencias no pueden ser nulas, por lo que si existe alguna posibilidad de que el objeto en cuestión sea nulo, no debe usar una referencia. Debe utilizar un puntero. Si desea manejar la manipulación de objetos por su cuenta, es decir, si desea asignar espacio de memoria para un objeto en el montón en lugar de en la pila, debe usar el puntero
int *pInt = new int; // allocates *pInt on the Heap
Las siguientes son algunas pautas.
Una función utiliza datos pasados sin modificarlos:
Si el objeto de datos es pequeño, como un tipo de datos incorporado o una estructura pequeña, páselo por valor.
Si el objeto de datos es una matriz, use un puntero porque esa es su única opción. Hacer el puntero un puntero a const.
Si el objeto de datos es una estructura de buen tamaño, use un puntero de const o una referencia de const para aumentar la eficiencia del programa. Ahorre el tiempo y el espacio necesarios para copiar una estructura o un diseño de clase. Hacer el puntero o la referencia const.
Si el objeto de datos es un objeto de clase, use una referencia constante. La semántica del diseño de clase a menudo requiere el uso de una referencia, que es la razón principal por la que C ++ agregó esta característica. Por lo tanto, la forma estándar de pasar los argumentos del objeto de clase es por referencia.
Una función modifica los datos en la función de llamada:
1. Si el objeto de datos es un tipo de datos incorporado, use un puntero. Si ves código como fixit (& x), donde x es un int, está bastante claro que esta función intenta modificar x.
2. Si el objeto de datos es una matriz, use su única opción: un puntero.
3. Si el objeto de datos es una estructura, use una referencia o un puntero.
4. Si el objeto de datos es un objeto de clase, use una referencia.
Por supuesto, estas son solo pautas, y puede haber razones para tomar decisiones diferentes. Por ejemplo, cin usa referencias para tipos básicos, de modo que puede usar cin >> n en lugar de cin >> & n.
Mi regla de oro es:
- Use punteros para los parámetros salientes o in / out. Entonces se puede ver que el valor va a ser cambiado. (Debes usar
&
) - Use punteros si el parámetro NULL es un valor aceptable. (Asegúrate de que sea
const
si es un parámetro entrante) - Use referencias para el parámetro entrante si no puede ser NULL y no es un tipo primitivo (
const T&
). - Use punteros o punteros inteligentes al devolver un objeto recién creado.
- Use punteros o punteros inteligentes como estructura o miembros de clase en lugar de referencias.
- Utilice referencias para
int ¤t = someArray[i]
alias (por ejemplo,int ¤t = someArray[i]
)
Independientemente de cuál use, no olvide documentar sus funciones y el significado de sus parámetros si no son obvios.
No es una cuestión de gusto. Aquí hay algunas reglas definitivas.
Si desea referirse a una variable declarada estáticamente dentro del alcance en el que fue declarada, use una referencia de C ++, y estará perfectamente segura. Lo mismo se aplica a un puntero inteligente declarado estáticamente. Pasar parámetros por referencia es un ejemplo de este uso.
Si desea referirse a algo de un alcance que sea más amplio que el alcance en el que se declara, debe usar un puntero inteligente de referencia para que esté perfectamente seguro.
Puede referirse a un elemento de una colección con una referencia por conveniencia sintáctica, pero no es seguro; El elemento puede ser eliminado en cualquier momento.
Para mantener de forma segura una referencia a un elemento de una colección, debe utilizar un puntero inteligente contado de referencia.
Solo pongo mi centavo. Acabo de realizar una prueba. Una sneeky en eso. Simplemente dejo que g ++ cree los archivos de ensamblaje del mismo mini programa utilizando punteros en comparación con el uso de referencias. Al mirar la salida son exactamente iguales. Aparte de la simbolización. Así que mirar el rendimiento (en un ejemplo simple) no hay problema.
Ahora en el tema de punteros vs referencias. En mi humilde opinión creo que la claridad está por encima de todo. Tan pronto como leo el comportamiento implícito, mis dedos comienzan a curvarse. Estoy de acuerdo en que es un buen comportamiento implícito que una referencia no pueda ser NULL.
Desreferenciar un puntero NULO no es el problema. se bloqueará su aplicación y será fácil de depurar. Un problema mayor son los punteros sin inicializar que contienen valores no válidos. Esto probablemente resultará en daños en la memoria que causan un comportamiento indefinido sin un origen claro.
Aquí es donde creo que las referencias son mucho más seguras que los punteros. Y estoy de acuerdo con una declaración anterior, de que la interfaz (que debe estar claramente documentada, ver diseño por contrato, Bertrand Meyer) define el resultado de los parámetros de una función. Ahora, teniendo esto en cuenta, mis preferencias van al uso de referencias siempre que sea posible.
Use la referencia siempre que pueda, los punteros donde sea necesario.
Evita los punteros hasta que no puedas.
La razón es que los punteros hacen que las cosas sean más difíciles de seguir / leer, manipulaciones menos seguras y mucho más peligrosas que cualquier otra construcción.
Así que la regla de oro es usar punteros solo si no hay otra opción.
Por ejemplo, devolver un puntero a un objeto es una opción válida cuando la función puede devolver nullptr en algunos casos y se supone que lo hará. Dicho esto, una mejor opción sería usar algo similar para boost::optional
.
Otro ejemplo es usar punteros a la memoria en bruto para manipulaciones de memoria específicas. Eso debería estar oculto y localizado en partes muy estrechas del código, para ayudar a limitar las partes peligrosas de toda la base del código.
En su ejemplo, no tiene sentido usar un puntero como argumento porque:
- si proporcionas
nullptr
como argumento, vas a ir a undefined-behavior-land; - La versión del atributo de referencia no permite (sin trucos fáciles de detectar) el problema con 1.
- La versión del atributo de referencia es más fácil de entender para el usuario: debe proporcionar un objeto válido, no algo que pueda ser nulo.
Si el comportamiento de la función tendría que trabajar con o sin un objeto determinado, el uso de un puntero como atributo sugiere que puede pasar nullptr
como argumento y está bien para la función. Eso es una especie de contrato entre el usuario y la implementación.
Puntos a tener en cuenta:
Los punteros pueden ser
NULL
, las referencias no pueden serNULL
.Las referencias son más fáciles de usar,
const
puede usarse para una referencia cuando no queremos cambiar el valor y solo necesitamos una referencia en una función.El puntero se usa con un
*
mientras que las referencias se usan con un&
.Utilice punteros cuando se requiera la operación aritmética de punteros.
Puede tener punteros a un tipo de vacío
int a=5; void *p = &a;
int a=5; void *p = &a;
pero no puede tener una referencia a un tipo vacío.
Puntero vs referencia
void fun(int *a)
{
cout<<a<<''/n''; // address of a = 0x7fff79f83eac
cout<<*a<<''/n''; // value at a = 5
cout<<a+1<<''/n''; // address of a increment by 4 bytes(int) = 0x7fff79f83eb0
cout<<*(a+1)<<''/n''; // value here is by default = 0
}
void fun(int &a)
{
cout<<a<<''/n''; // reference of original a passed a = 5
}
int a=5;
fun(&a);
fun(a);
Veredicto de cuándo usar qué
Puntero : Para matriz, lista de enlaces, implementaciones de árbol y aritmética de punteros.
Referencia : En parámetros de función y tipos de retorno.
Para los punteros, los necesita para apuntar a algo, por lo que los punteros cuestan espacio de memoria.
Por ejemplo, una función que toma un puntero entero no tomará la variable entera. Por lo tanto, tendrá que crear un puntero para que primero pase a la función.
En cuanto a una referencia, no costará memoria. Tiene una variable entera y puede pasarla como una variable de referencia. Eso es. No es necesario crear una variable de referencia especialmente para ella.
Los punteros y las referencias tienen la misma velocidad, y los punteros pueden hacer todo lo que las referencias pueden y más. Se basa en gran medida en la opinión personal cuál usar. Es común usar referencias a menos que realmente necesite algunas de las características de los punteros.
Prefiero usar punteros. Al menos está claro lo que estás haciendo. Tengo la sensación de que las referencias se usan principalmente debido a STL y sus implicaciones de sintaxis en el código. Por eso también hay muchas novedades de la biblioteca estándar de C ++ como std :: move ..... para obtener exactamente lo que desea, y no lo que intuitivamente habría pensado.
Utilice referencias como último recurso. Asigne una instancia en la pila o el montón, úselos.
Utilice referencias para el alcance del parámetro para obtener el menor impacto. Si usa la referencia porque los punteros son demasiado difíciles para usted, cambie a otro idioma.