¿Cómo pasar objetos a funciones en C ++?

249

Soy nuevo en la programación de C ++, pero tengo experiencia en Java. Necesito orientación sobre cómo pasar objetos a funciones en C ++.

¿Necesito pasar punteros, referencias o valores sin puntero y sin referencia? Recuerdo que en Java no hay tales problemas ya que pasamos solo la variable que contiene referencia a los objetos.

Sería genial si también pudiera explicar dónde usar cada una de esas opciones.

Rakesh K
fuente
66
¿De qué libro estás aprendiendo C ++?
17
Está fuertemente que el libro no se recomienda. Vaya a C ++ Primer por Stan Lippman.
Prasoon Saurav
23
Bueno, ahí está tu problema. Schildt es básicamente cr * p: obtén C ++ acelerado de Koenig & Moo.
99
Me pregunto cómo nadie mencionó el lenguaje de programación C ++ de Bjarne Stroustrup. Bjarne Stroustrup es el creador de C ++. Un muy buen libro para aprender C ++.
George
15
@George: TC ++ PL no es para principiantes, pero se considera la Biblia para C ++. XD
Prasoon Saurav

Respuestas:

277

Reglas generales para C ++ 11:

Pase por valor , excepto cuando

  1. no necesita la propiedad del objeto y lo hará un simple alias, en cuyo caso pasa por constreferencia ,
  2. debe mutar el objeto, en cuyo caso, use pasar por una constreferencia que no sea de valor ,
  3. pasa objetos de clases derivadas como clases base, en cuyo caso debe pasar por referencia . (Use las reglas anteriores para determinar si debe pasar por constreferencia o no).

Pasar por puntero prácticamente nunca se aconseja. Los parámetros opcionales se expresan mejor como a std::optional( boost::optionalpara bibliotecas estándar más antiguas), y el aliasing se hace 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 prácticas para C ++ 03:

Pase argumentos por constreferencia , excepto cuando

  1. deben cambiarse dentro de la función y dichos cambios deben reflejarse afuera, en cuyo caso pasará por no constreferencia
  2. la función debe poder invocarse sin ningún argumento, en cuyo caso pasa por puntero, para que los usuarios puedan pasar NULL/ 0/ en su nullptrlugar; aplique la regla anterior para determinar si debe pasar por un puntero a un constargumento
  3. son de tipo incorporado, que se pueden pasar por copia
  4. deben cambiarse dentro de la función y dichos cambios no deben reflejarse afuera, en cuyo caso puede pasar por copia (una alternativa sería pasar de acuerdo con 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 en esto, pero estas pocas reglas para principiantes te llevarán bastante lejos.

sbi
fuente
17
+1: también me gustaría señalar que algunos (es decir, Google) sienten que los objetos que se cambiarán dentro de la función deben pasarse a través de un puntero en lugar de una referencia no constante. El razonamiento es que cuando la dirección de un objeto se pasa a una función, es más evidente que dicha función puede cambiarla. Ejemplo: con referencias, la llamada es foo (bar); si la referencia es constante o no, con un puntero es foo (& bar); y es más evidente que a foo se le pasa un objeto mutable.
RC.
19
@RC Todavía no te dice si el puntero es constante o no. Las pautas de Google han recibido muchas críticas en las diversas comunidades en línea de C ++, en mi humilde opinión.
14
Mientras que en otros contextos google puede estar liderando el camino, en C ++ su guía de estilo no es realmente tan buena.
David Rodríguez - dribeas
44
@ArunSaha: Como una guía de estilo pura, Stroustrup tiene una guía desarrollada para una empresa aeroespacial. Hojeé la guía de google y no me gustó por un par de razones. Sutter & Alexandrescu C ++ Coding Standards es un gran libro para leer y puede obtener bastantes buenos consejos, pero en realidad no es una guía de estilo . No conozco ningún verificador automático de estilo , aparte de los humanos y el sentido común.
David Rodríguez - dribeas
3
@anon Sin embargo, tiene la garantía de que si un argumento no se pasa a través de un puntero, NO se cambia. Eso es bastante valioso en mi humilde opinión, de lo contrario, al tratar de rastrear lo que le sucede a una variable en una función, debe examinar los archivos de encabezado de todas las funciones a las que se pasa para determinar si ha cambiado. De esta manera, solo tiene que mirar los que se pasaron a través del puntero.
smehmood
107

Existen algunas diferencias en las convenciones de llamadas en C ++ y Java. En C ++, técnicamente solo hay dos convenciones: paso por valor y paso por referencia, con cierta literatura que 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 constancia al tipo de argumento, mejorando la semántica.

Pase por referencia

Pasar por referencia significa que la función recibirá conceptualmente su instancia de objeto y no una copia de la misma. La referencia es conceptualmente un alias del objeto que se utilizó en el contexto de la llamada y no puede ser nulo. 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 la 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 de él es 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 en sí 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 con el paso por referencia y también permitirá valores opcionales (pasar un puntero nulo).

Esta es la convención usada en C cuando una función necesita modificar una variable externa, y la convención usada 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 son.

Agregar const a la ecuación

En C ++ puede asignar constancia a los objetos al definir variables, punteros y referencias en diferentes niveles. Puede declarar que una variable es 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 solo puede definir un nivel de constancia (palabra clave final): el de la variable (instancia para tipos primitivos, referencia para tipos de referencia), pero no puede definir una referencia a un elemento inmutable (a menos que la clase en sí sea inmutable).

Esto se usa ampliamente en convenciones de llamadas de C ++. Cuando los objetos son pequeños, puede 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 cambiará 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 se cambie dentro de la función.

Reglas de juego

Estas son algunas reglas básicas a seguir:

  • Prefiero pasar por valor para tipos primitivos
  • Prefiere pasar por referencia con referencias a constante para otros tipos
  • Si la función necesita modificar el argumento use pass-by-reference
  • Si el argumento es opcional, use el paso por puntero (a constante si el valor opcional no debe modificarse)

Hay otras pequeñas desviaciones de estas reglas, la primera de las cuales es manejar 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 el 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 una 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 descriptor de 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 sus 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 Java son punteros en C sin aritmética de punteros que se pasan por valor a las funciones.

David Rodríguez - dribeas
fuente
44
@ david-rodriguez-dribeas Me gustan las reglas generales, especialmente "Prefiero pasar por valor para tipos primitivos"
yadab
Según yo, esta es una respuesta mucho mejor a la pregunta.
unrealsoul007
22

Hay varios casos a considerar.

Parámetro modificado (parámetros "out" y "in / out")

void modifies(T &param);
// vs
void modifies(T *param);

Este caso se trata principalmente de estilo: ¿desea que el código se vea como call (obj) o call (& obj) ? Sin embargo, hay dos puntos donde la diferencia es importante: el caso opcional, a continuación, y desea utilizar una referencia al sobrecargar operadores.

... y opcional

void modifies(T *param=0);  // default value optional, too
// vs
void modifies();
void modifies(T &param);

Parámetro no modificado

void uses(T const &param);
// vs
void uses(T param);

Este es el caso interesante. La regla general es que los tipos "baratos de copiar" se pasan por valor, generalmente son tipos pequeños (pero no siempre), mientras que otros se pasan por const ref. Sin embargo, si necesita hacer una copia dentro de su función, debe pasar por valor . (Sí, esto expone un poco de detalle de implementación. C'est le C ++. )

... y opcional

void uses(T const *param=0);  // default value optional, too
// vs
void uses();
void uses(T const &param);  // or optional(T param)

Aquí hay la menor diferencia entre todas las situaciones, así que elige la que te haga la vida más fácil.

Const por valor es un detalle de implementación

void f(T);
void f(T const);

¡Estas declaraciones son en realidad exactamente la misma función! Al pasar por valor, const es puramente un detalle de implementación. Pruébalo:

void f(int);
void f(int const) { /* implements above function, not an overload */ }

typedef void NC(int);       // typedefing function types
typedef void C(int const);

NC *nc = &f;  // nc is a function pointer
C *c = nc;    // C and NC are identical types

fuente
3
+1 No sabía acerca de constser una implementación al pasar por valor.
balki
20

Pase por valor:

void func (vector v)

Pase las 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, así como para que otros hilos modifiquen su valor mientras se ejecuta la función.

La desventaja son los ciclos de la CPU y la memoria adicional gastada para copiar el objeto.

Pase por referencia constante:

void func (const vector& v);

Este formulario emula el comportamiento de paso por valor mientras 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 del subproceso: cualquier cambio realizado en el objeto original por otro subproceso aparecerá dentro de la función mientras aún se está ejecutando.

Pase por referencia no constante:

void func (vector& v)

Use esto cuando la función tenga que volver a escribir algún valor en la variable, que en última instancia será utilizada por la persona que llama.

Al igual que el caso de referencia constante, esto no es seguro para subprocesos.

Pase por puntero constante:

void func (const vector* vp);

Funcionalmente igual que pasar por referencia constante, excepto por la sintaxis diferente, más el hecho de que la función de llamada puede pasar el puntero NULL para indicar que no tiene datos válidos para pasar.

No es seguro para subprocesos.

Pase por puntero no constante:

void func (vector* vp);

Similar a la referencia no constante. La persona que llama generalmente establece la variable en NULL cuando se supone que la función no debe reescribir 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 es seguro para subprocesos.

nav
fuente
0

Como nadie mencionó que estoy agregando, cuando pasa un objeto a una función en c ++, se llama al constructor de copia predeterminado del objeto si no tiene uno que cree un clon del objeto y luego lo pase al método, entonces cuando cambia los valores del objeto que se reflejarán en la copia del objeto en lugar del objeto original, ese es el problema en c ++, por lo tanto, si hace que todos los atributos de clase sean punteros, los constructores de copia copiarán las direcciones de atributos de puntero, por lo que cuando las invocaciones de métodos en el objeto que manipula los valores almacenados en las direcciones de atributos de puntero, los cambios también se reflejan en el objeto original que se pasa como parámetro, por lo que esto puede comportarse igual que Java, pero no olvide que toda su clase los atributos deben ser punteros, también debe cambiar los valores de los punteros,será muy claro con la explicación del código.

Class CPlusPlusJavaFunctionality {
    public:
       CPlusPlusJavaFunctionality(){
         attribute = new int;
         *attribute = value;
       }

       void setValue(int value){
           *attribute = value;
       }

       void getValue(){
          return *attribute;
       }

       ~ CPlusPlusJavaFuncitonality(){
          delete(attribute);
       }

    private:
       int *attribute;
}

void changeObjectAttribute(CPlusPlusJavaFunctionality obj, int value){
   int* prt = obj.attribute;
   *ptr = value;
}

int main(){

   CPlusPlusJavaFunctionality obj;

   obj.setValue(10);

   cout<< obj.getValue();  //output: 10

   changeObjectAttribute(obj, 15);

   cout<< obj.getValue();  //output: 15
}

Pero esta no es una buena idea, ya que terminarás escribiendo mucho código relacionado con punteros, que son propensos a pérdidas de memoria y no olvides llamar a los destructores. Y para evitar que c ++ tenga constructores de copia donde creará nueva memoria cuando los objetos que contienen punteros se pasen a argumentos de función que dejarán de manipular los datos de otros objetos, Java pasa por valor y el valor es referencia, por lo que no requiere constructores de copia.

murali krish
fuente
-1

Hay tres métodos para pasar un objeto a una función como parámetro:

  1. Pase por referencia
  2. pasar por valor
  3. agregando constante en el parámetro

Ve a través del siguiente ejemplo:

class Sample
{
public:
    int *ptr;
    int mVar;

    Sample(int i)
    {
        mVar = 4;
        ptr = new int(i);
    }

    ~Sample()
    {
        delete ptr;
    }

    void PrintVal()
    {
        cout << "The value of the pointer is " << *ptr << endl
             << "The value of the variable is " << mVar;
   }
};

void SomeFunc(Sample x)
{
cout << "Say i am in someFunc " << endl;
}


int main()
{

  Sample s1= 10;
  SomeFunc(s1);
  s1.PrintVal();
  char ch;
  cin >> ch;
}

Salida:

Digamos que estoy en someFunc
El valor del puntero es -17891602
El valor de la variable es 4

amerr
fuente
Solo hay 2 métodos (los primeros 2 que mencionó). No tengo idea de lo que quisiste decir con "pasar constante en el parámetro"
MM
-1

Las siguientes son las formas de pasar argumentos / parámetros para funcionar en C ++.

1. por valor.

// passing parameters by value . . .

void foo(int x) 
{
    x = 6;  
}

2. por referencia.

// passing parameters by reference . . .

void foo(const int &x) // x is a const reference
{
    x = 6;  
}

// passing parameters by const reference . . .

void foo(const int &x) // x is a const reference
{
    x = 6;  // compile error: a const reference cannot have its value changed!
}

3. por objeto.

class abc
{
    display()
    {
        cout<<"Class abc";
    }
}


// pass object by value
void show(abc S)
{
    cout<<S.display();
}

// pass object by reference
void show(abc& S)
{
    cout<<S.display();
}
Yogeesh HT
fuente
1
"pasar por objeto" no es una cosa. Solo hay pasar por valor y pasar por referencia. Su "caso 3" en realidad muestra un caso de pasar por valor y un caso de pasar por referencia.
MM