¿Cómo debo pasar objetos a funciones?
Soy nuevo en la programación en C++, pero tengo experiencia en Java. Necesito orientación sobre cómo pasar objetos a funciones en C++.
¿Necesito pasar punteros, referencias o valores que no sean punteros ni referencias? Recuerdo que en Java no existen tales problemas ya que solo pasamos la variable que hace referencia a los objetos.
Sería genial si también pudieras explicar dónde usar cada una de esas opciones.
Reglas generales para C++11:
Pasar por valor , excepto cuando
- no necesita la propiedad del objeto y un alias simple será suficiente, en cuyo caso pasará por
const
referencia , - debes mutar el objeto, en cuyo caso, usa pasar por una
const
referencia que no sea de valor l , - pasa objetos de clases derivadas como clases base, en cuyo caso debe pasar por referencia . (Utilice las reglas anteriores para determinar si se pasa por
const
referencia o no).
Prácticamente nunca se recomienda pasar por el puntero. Los parámetros opcionales se expresan mejor como std::optional
( boost::optional
para bibliotecas estándar más antiguas), y el alias se realiza bien por referencia.
La semántica de movimiento de C++11 hace que pasar y regresar por valor sea mucho más atractivo incluso para objetos complejos.
Reglas generales para C++03:
Pasar argumentos por const
referencia , excepto cuando
- deben cambiarse dentro de la función y dichos cambios deben reflejarse en el exterior, en cuyo caso se pasa por no
const
referencia - la función debe poder ser invocada sin ningún argumento, en cuyo caso se pasa por puntero, para que los usuarios puedan pasar
NULL
// en su lugar; aplique la regla anterior para determinar si debe pasar un puntero a un argumento0
nullptr
const
- son de tipos integrados, que se pueden pasar mediante copia
- se deben cambiar dentro de la función y dichos cambios no deben reflejarse afuera, en cuyo caso se puede pasar por copia (una alternativa sería pasar según las reglas anteriores y hacer una copia dentro de la función)
(aquí, "pasar por valor" se llama "pasar por copia", porque pasar por valor siempre crea una copia en C++03)
Hay más sobre esto, pero estas pocas reglas para principiantes te llevarán bastante lejos.
Existen algunas diferencias al llamar a convenciones en C++ y Java. En C++, técnicamente hablando, sólo existen dos convenciones: paso por valor y paso por referencia, y alguna literatura incluye una tercera convención de paso por puntero (que en realidad es paso por valor de un tipo de puntero). Además de eso, puede agregar consistencia al tipo de argumento, mejorando la semántica.
Pasar por referencia
Pasar por referencia significa que la función recibirá conceptualmente la instancia de su objeto y no una copia del mismo. La referencia es conceptualmente un alias del objeto que se usó en el contexto de llamada y no puede ser nula. Todas las operaciones realizadas dentro de la función se aplican al objeto fuera de la función. Esta convención no está disponible en Java o C.
Pasar por valor (y pasar por puntero)
El compilador generará una copia del objeto en el contexto de llamada y usará esa copia dentro de la función. Todas las operaciones realizadas dentro de la función se realizan en la copia, no en el elemento externo. Esta es la convención para tipos primitivos en Java.
Una versión especial consiste en pasar un puntero (dirección del objeto) a una función. La función recibe el puntero, y todas y cada una de las operaciones aplicadas al puntero mismo se aplican a la copia (puntero); por otro lado, las operaciones aplicadas al puntero desreferenciado se aplicarán a la instancia del objeto en esa ubicación de memoria, por lo que la función puede tener efectos secundarios. El efecto de usar el paso por valor de un puntero al objeto permitirá que la función interna modifique los valores externos, como ocurre con el paso por referencia, y también permitirá valores opcionales (pasar un puntero nulo).
Esta es la convención utilizada en C cuando una función necesita modificar una variable externa, y la convención utilizada en Java con tipos de referencia: la referencia se copia, pero el objeto referido es el mismo: los cambios en la referencia/puntero no son visibles fuera la función, pero los cambios en la memoria puntiaguda sí lo son.
Agregando constante a la ecuación
En C++ puedes asignar constancia a objetos al definir variables, punteros y referencias en diferentes niveles. Puede declarar una variable como constante, puede declarar una referencia a una instancia constante y puede definir todos los punteros a objetos constantes, punteros constantes a objetos mutables y punteros constantes a elementos constantes. Por el contrario, en Java sólo se puede definir un nivel de constante (palabra clave final): el de la variable (instancia para tipos primitivos, referencia para tipos de referencia), pero no se puede definir una referencia a un elemento inmutable (a menos que la clase misma sea inmutable).
Esto se usa ampliamente en las convenciones de llamadas de C++. Cuando los objetos son pequeños puedes pasar el objeto por valor. El compilador generará una copia, pero esa copia no es una operación costosa. Para cualquier otro tipo, si la función no cambia el objeto, puede pasar una referencia a una instancia constante (generalmente llamada referencia constante) del tipo. Esto no copiará el objeto, sino que lo pasará a la función. Pero al mismo tiempo el compilador garantizará que el objeto no cambie dentro de la función.
Reglas de juego
Estas son algunas reglas básicas a seguir:
- Prefiere pasar por valor para tipos primitivos
- Prefiere pasar por referencia con referencias a constantes para otros tipos
- Si la función necesita modificar el argumento, utilice el paso por referencia.
- Si el argumento es opcional, utilice el puntero de paso (a constante si el valor opcional no debe modificarse)
Existen otras pequeñas desviaciones de estas reglas, la primera de las cuales es el manejo de la propiedad de un objeto. Cuando un objeto se asigna dinámicamente con nuevo, se debe desasignar con eliminar (o las [] versiones del mismo). El objeto o función responsable de la destrucción del objeto se considera propietario del recurso. Cuando se crea un objeto asignado dinámicamente en un fragmento de código, pero la propiedad se transfiere a un elemento diferente, generalmente se hace con semántica de paso por puntero o, si es posible, con punteros inteligentes.
Nota al margen
Es importante insistir en la importancia de la diferencia entre las referencias de C++ y Java. En C++, las referencias son conceptualmente la instancia del objeto, no un acceso al mismo. El ejemplo más simple es implementar una función de intercambio:
// C++
class Type; // defined somewhere before, with the appropriate operations
void swap( Type & a, Type & b ) {
Type tmp = a;
a = b;
b = tmp;
}
int main() {
Type a, b;
Type old_a = a, old_b = b;
swap( a, b );
assert( a == old_b );
assert( b == old_a );
}
La función de intercambio anterior cambia ambos argumentos mediante el uso de referencias. El código más cercano en Java:
public class C {
// ...
public static void swap( C a, C b ) {
C tmp = a;
a = b;
b = tmp;
}
public static void main( String args[] ) {
C a = new C();
C b = new C();
C old_a = a;
C old_b = b;
swap( a, b );
// a and b remain unchanged a==old_a, and b==old_b
}
}
La versión Java del código modificará las copias de las referencias internamente, pero no modificará los objetos reales externamente. Las referencias de Java son punteros de C sin aritmética de punteros que se pasan por valor a funciones.
Pasar por valor:
void func (vector v)
Pase variables por valor cuando la función necesite un aislamiento completo del entorno, es decir, para evitar que la función modifique la variable original y para evitar que otros subprocesos modifiquen su valor mientras se ejecuta la función.
La desventaja son los ciclos de CPU y la memoria adicional que se gasta para copiar el objeto.
Pasar por referencia constante:
void func (const vector& v);
Este formulario emula el comportamiento de paso por valor y al mismo tiempo elimina la sobrecarga de copia. La función obtiene acceso de lectura al objeto original, pero no puede modificar su valor.
La desventaja es la seguridad de los subprocesos: cualquier cambio realizado en el objeto original por otro subproceso se mostrará dentro de la función mientras aún se está ejecutando.
Pasar por referencia no constante:
void func (vector& v)
Úselo cuando la función tenga que escribir algún valor en la variable, que finalmente será utilizado por la persona que llama.
Al igual que el caso de referencia constante, esto no es seguro para subprocesos.
Pasar por puntero constante:
void func (const vector* vp);
Funcionalmente igual que pasar por referencia constante excepto por la sintaxis diferente, además del hecho de que la función que llama puede pasar un puntero NULL para indicar que no tiene datos válidos para pasar.
No es seguro para subprocesos.
Pasar por puntero no constante:
void func (vector* vp);
Similar a la referencia no constante. La persona que llama normalmente establece la variable en NULL cuando se supone que la función no debe escribir un valor. Esta convención se ve en muchas API de glibc. Ejemplo:
void func (string* str, /* ... */) {
if (str != NULL) {
*str = some_value; // assign to *str only if it's non-null
}
}
Al igual que todos pasan por referencia/puntero, no son seguros para subprocesos.