Entiendo la sintaxis y la semántica general de los punteros versus 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 u otra ( operator++
necesita un argumento de referencia), pero en general estoy descubriendo que prefiero usar punteros (y punteros constantes) ya que la sintaxis es clara de que las variables se pasan de manera 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á sucediendo, 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 (por ejemplo operator++
)? ¿Hay problemas de rendimiento con uno u otro?
EDITAR (ACTUALIZADO):
Además de permitir valores NULL y tratar con matrices sin procesar, parece que la elección se reduce a preferencias personales. Acepté la respuesta a continuación que hace referencia a la Guía de estilo 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 deberían 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 mantenimiento usar referencias donde un objeto DEBE estar presente, aunque es una pena perder la claridad sintáctica
add_one(a);
no está claro si sea
va a modificar? Dice justo en el código: agregue uno .addOneTo(...)
. Si eso no es lo que quieres hacer, solo mira la declaración.Respuestas:
Utilice 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.
Entonces, la regla general 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 a
boost::optional
.Otro ejemplo es usar punteros a la memoria sin procesar 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:
nullptr
como argumento, vas a una tierra de comportamiento indefinido;Si el comportamiento de la función tuviera que funcionar con o sin un objeto dado, entonces usar 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.fuente
add_one(a)
no debería devolver el resultado, en lugar de establecerlo como referencia?add_one(a)
es confuso, entonces eso se debe a un nombre incorrecto.add_one(&a)
tendría la misma confusión, solo que ahora podría estar incrementando el puntero y no el objeto.add_one_inplace(a)
Evitaría toda confusión.NULL
ynullptr
, y se les tiene por una razón. Y no es un consejo bien considerado o incluso realista dar que "nunca use punteros", y / o "nunca use NULL, siempre useboost::optional
". Eso es una locura. No me malinterpreten, los punteros en bruto se necesitan con menos frecuencia en C ++ que en C, pero aún así, son útiles, no son tan "peligrosos" como a algunas personas de C ++ les gusta decir (eso también es una exageración), y de nuevo: cuando es más fácil usar un puntero ereturn nullptr;
indicar un valor perdido ... ¿Por qué importar todo el Boost?if
es obsoleto y uno debería usarlowhile() { break; }
, ¿verdad? Además, no se preocupe, he visto y trabajado con bases de código grandes, y sí, si es descuidado , entonces la propiedad es un problema. No obstante, si te apegas a las convenciones, úsalas de manera consistente y comenta y documenta tu código. Pero después de todo, debería usar C porque soy demasiado tonto para C ++, ¿verdad?Las actuaciones son exactamente las mismas, ya que las referencias se implementan internamente como punteros. Por lo tanto, no necesita preocuparse por eso.
No existe una convención generalmente aceptada con respecto a cuándo utilizar referencias y punteros. En algunos casos, debe devolver o aceptar referencias (constructor de copias, por ejemplo), pero aparte de eso, puede hacer lo que desee. Una convención bastante común que he encontrado es usar referencias cuando el parámetro debe hacer referencia a un objeto existente y punteros cuando un valor NULL está bien.
Algunas convenciones de codificación (como la de Google ) prescriben que siempre se deben usar punteros o referencias constantes, porque las referencias tienen un poco de sintaxis clara: tienen un comportamiento de referencia pero sintaxis de valor.
fuente
add_one
está rota :,add_one(0); // passing a perfectly valid pointer value
kaboom. Debe verificar si es nulo. Algunas personas responderán: "bueno, simplemente documentaré que mi función no funciona con nulo". Eso está bien, pero luego rechazas el propósito de la pregunta: si vas a mirar la documentación para ver si nulo está bien, también verás la declaración de la función .int& i = *((int*)0);
Esta no es una réplica válida. El problema en el código anterior radica en el uso del puntero, no con la referencia . Las referencias nunca son nulas, punto.De C ++ FAQ Lite -
fuente
C with class
(que no es C ++).Mi regla de oro es:
&
)const
un parámetro entrante)const T&
).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.
fuente
Descargo de responsabilidad: aparte del hecho de que las referencias no pueden ser NULL ni "rebotar" (lo que significa que no pueden cambiar el objeto del que son el alias), realmente se trata de una cuestión de gustos, así que no voy a decir "este es mejor".
Dicho esto, no estoy de acuerdo con su última declaración en la publicación, ya que no creo que el código pierda claridad con las referencias. En tu ejemplo
podría ser más claro que
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
de alguna manera tampoco está claro: ¿n va a ser un solo entero o una matriz? A veces solo tienes acceso a encabezados (mal documentados) y firmas como
No son fáciles de interpretar a primera vista.
En mi opinión, las referencias son tan buenas como los punteros cuando no se necesita (re) asignación ni reenlace (en el sentido explicado anteriormente). Además, si un desarrollador solo usa punteros para matrices, 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.
fuente
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:
http://www.altdevblogaday.com/2011/12/24/static-code-analysis/
Editar 2012-03-13
El usuario Bret Kuhns comenta correctamente:
Es cierto, pero la pregunta sigue siendo, incluso cuando se reemplazan los punteros sin formato con punteros inteligentes.
Por ejemplo, ambos
std::unique_ptr
ystd::shared_ptr
se pueden construir como punteros "vacíos" a través de su constructor predeterminado:... lo que significa que usarlos sin verificar que no están vacíos conlleva el riesgo de un accidente, 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 respuesta de 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 quisiera (me favorezco el " por referencia "por defecto, pero podría estar equivocado).
fuente
shared_ptr
, yunique_ptr
. La semántica de propiedad y las convenciones de parámetros de entrada / salida se ocupan de una combinación de estas tres piezas y constancia. Casi no hay necesidad de punteros sin formato en C ++, excepto cuando se trata de código heredado y algoritmos muy optimizados. Las áreas donde se usan deben estar tan encapsuladas como sea posible y convertir cualquier puntero sin procesar al equivalente semántico apropiado "moderno".No es cuestión de gustos. Aquí hay algunas reglas definitivas.
Si desea hacer referencia a una variable declarada estáticamente dentro del alcance en el que se declaró, utilice una referencia de C ++ y será 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 hacer referencia a cualquier cosa desde un alcance que sea más amplio que el alcance en el que se declara, entonces debe usar un puntero inteligente de referencia para que sea perfectamente seguro.
Puede referirse a un elemento de una colección con una referencia por conveniencia sintáctica, pero no es seguro; El elemento se puede eliminar en cualquier momento.
Para mantener de forma segura una referencia a un elemento de una colección, debe usar un puntero inteligente de referencia contada.
fuente
Cualquier diferencia de rendimiento sería tan pequeña que no justificaría usar el enfoque que es menos claro.
Primero, un caso que no se mencionó donde las referencias son generalmente superiores son las
const
referencias. Para los tipos no simples, pasar unconst reference
evita crear un temporal y no causa la confusión que le preocupa (porque el valor no se modifica). Aquí, obligar a una persona a pasar un puntero causa la confusión que le preocupa, ya que ver la dirección tomada y pasada a una función puede 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 está haciendo 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:
Aquí, el nombre de la función deja en claro que está recuperando información en una matriz. Entonces 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 ninguna sintaxis adicional. Si ninguna de estas ventajas se aplica, no hay razón para preferir una referencia sobre un puntero.
fuente
Copiado de wiki -
Estoy 100% de acuerdo con esto, y es por eso que creo que solo debes usar una referencia cuando tienes una muy buena razón para hacerlo.
fuente
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
se pueden usar como referencia cuando no queremos cambiar el valor y solo necesitamos una referencia en una función.Puntero usado con un
*
tiempo referencias usadas con a&
.Utilice punteros cuando se requiera la operación aritmética del puntero.
Puede tener punteros a un tipo vacío
int a=5; void *p = &a;
pero no puede tener una referencia a un tipo vacío.Referencia de puntero vs
Veredicto cuando usar qué
Puntero : para arreglos, lista de enlaces, implementaciones de árbol y aritmética de puntero.
Referencia : en parámetros de función y tipos de retorno.
fuente
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 un ejemplo, imagine que tiene las siguientes clases.
Al principio, puede parecer una buena idea tener un parámetro en el
RefPhone(const SimCard & card)
constructor pasado por una referencia, porque evita pasar punteros incorrectos / nulos al constructor. De alguna manera, fomenta la asignación de variables en la pila y aprovecha los beneficios de RAII.Pero luego los temporales vienen a destruir tu mundo feliz.
Entonces, si se apega ciegamente a las referencias, intercambia la posibilidad de pasar punteros inválidos por la posibilidad de almacenar referencias a objetos destruidos, lo que básicamente tiene el mismo efecto.
editar: Tenga en cuenta que me apegué a la regla "Utilice la referencia siempre que 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 son malas. Sin embargo, pueden ser mal utilizados, al igual que los punteros, y pueden traer sus propias amenazas al código.
Existen las siguientes diferencias entre punteros y referencias.
Teniendo esto en cuenta, mis reglas actuales son las siguientes.
fuente
const SimCard & m_card;
es solo un código mal escrito.const SimCard & m_card
es correcto o no depende del contexto. El mensaje en esta publicación no es que las referencias no sean seguras (aunque pueden serlo si uno se esfuerza mucho). El mensaje es que no debe ceñirse ciegamente al mantra "usar referencias siempre que sea posible". El ejemplo es el resultado del uso agresivo de la doctrina de "usar referencias siempre que sea posible". Esto debería estar claro.const SimCard & m_card
. Si desea ser eficiente con los temporales, agregue elexplicit RefPhone(const SimCard&& card)
constructor.Las siguientes son algunas pautas.
Una función usa 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. Haga que el puntero sea un puntero para const.
Si el objeto de datos es una estructura de buen tamaño, use un puntero constante o una referencia constante para aumentar la eficiencia del programa. Ahorre el tiempo y el espacio necesarios para copiar una estructura o un diseño de clase. Haga 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 argumentos de objetos de clase es por referencia.
Una función modifica datos en la función de llamada:
1. Si el objeto de datos es un tipo de datos incorporado, use un puntero. Si detecta código como fixit (& x), donde x es un int, está bastante claro que esta función tiene la intención de 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 diferentes decisiones. Por ejemplo, cin usa referencias para tipos básicos para que pueda usar cin >> n en lugar de cin >> & n.
fuente
Las referencias son más limpias y fáciles de usar, y hacen un mejor trabajo al ocultar información. Sin embargo, las referencias no pueden reasignarse. 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 pueda ser nulo, no debe usar una referencia. Debes usar 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
fuente
El ejemplo escrito correctamente debería verse así
Es por eso que las referencias son preferibles si es posible ...
fuente
Solo pongo mi moneda de diez centavos. Acabo de realizar una prueba. Un estornudo por eso. Solo dejo que g ++ cree los archivos de ensamblaje del mismo mini programa usando punteros en comparación con el uso de referencias. Al mirar la salida, son exactamente iguales. Aparte de la simbolización. En cuanto al rendimiento (en un ejemplo simple) no hay problema.
Ahora sobre 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 puede ser NULL.
Desreferenciar un puntero NULL no es el problema. bloqueará su aplicación y será fácil de depurar. Un problema mayor son los punteros no inicializados que contienen valores no válidos. Lo más probable es que esto provoque daños en la memoria que causen 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, 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 todo esto en cuenta, mis preferencias van a usar referencias donde sea / cuando sea posible.
fuente
Para los punteros, necesita que apunten 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, deberá crear un puntero para que ese 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 necesita crear una variable de referencia especialmente para ella.
fuente
&address
. Una referencia ciertamente costará memoria si es miembro de un objeto, y además, todos los compiladores existentes implementan referencias como direcciones, por lo que tampoco ahorra nada en términos de pasar parámetros o desreferenciarlos.