c++ - resueltos - punteros y arreglos en c




¿Por qué debería usar un puntero en lugar del objeto en sí? (15)

Vengo de un fondo de Java y he empezado a trabajar con objetos en C ++. Pero una cosa que se me ocurrió es que las personas a menudo usan punteros a los objetos en lugar de los propios objetos, por ejemplo esta declaración:

Object *myObject = new Object;

más bien que:

Object myObject;

O en lugar de usar una función, digamos testFunc() , así:

myObject.testFunc();

tenemos que escribir:

myObject->testFunc();

Pero no puedo entender por qué deberíamos hacerlo de esta manera. Supongo que tiene que ver con la eficiencia y la velocidad, ya que tenemos acceso directo a la dirección de la memoria. Estoy en lo cierto


Pero no puedo entender por qué deberíamos usarlo así.

Voy a comparar cómo funciona dentro del cuerpo de la función si usas:

Object myObject;

Dentro de la función, su myObject se destruirá una vez que esta función vuelva. Así que esto es útil si no necesitas tu objeto fuera de tu función. Este objeto se pondrá en la pila de hilos actual.

Si escribes dentro del cuerpo de la función:

 Object *myObject = new Object;

entonces, la instancia de la clase de objetos señalada por myObject no se destruirá una vez que la función finalice, y la asignación esté en el montón.

Ahora, si usted es programador de Java, el segundo ejemplo es más cercano a cómo funciona la asignación de objetos en java. Esta línea: Object *myObject = new Object; es equivalente a java: Object myObject = new Object(); . La diferencia es que en java myObject se recolectará la basura, mientras que en c ++ no se liberará, en algún lugar debe llamar explícitamente `delete myObject; ' De lo contrario introducirás fugas de memoria.

Desde c ++ 11 puede usar formas seguras de asignaciones dinámicas: new Object , almacenando valores en shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Además, los objetos se almacenan a menudo en contenedores, como map-s o vector-s, que administrarán automáticamente la vida útil de sus objetos.


Prefacio

Java no es nada como C ++, contrariamente a la exageración. La máquina de publicidad de Java desea que creas que, dado que Java tiene una sintaxis similar a C ++, los lenguajes son similares. Nada puede estar más lejos de la verdad. Esta información errónea es parte de la razón por la que los programadores de Java van a C ++ y usan una sintaxis similar a la de Java sin entender las implicaciones de su código.

En adelante vamos

Pero no puedo entender por qué deberíamos hacerlo de esta manera. Supongo que tiene que ver con la eficiencia y la velocidad, ya que tenemos acceso directo a la dirección de la memoria. Estoy en lo cierto

Al contrario, en realidad. La pila es mucho más lenta que la pila, porque la pila es muy simple en comparación con la pila. Las variables de almacenamiento automático (también denominadas variables de pila) tienen sus destructores llamados una vez que están fuera del alcance. Por ejemplo:

{
    std::string s;
}
// s is destroyed here

Por otro lado, si usa un puntero asignado dinámicamente, su destructor debe llamarse manualmente. delete llama a este destructor para ti.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Esto no tiene nada que ver con la new sintaxis que prevalece en C # y Java. Se utilizan para fines completamente diferentes.

Beneficios de la asignación dinámica.

1. No tienes que saber el tamaño de la matriz de antemano

Uno de los primeros problemas con los que se encuentran muchos programadores de C ++ es que cuando aceptan la entrada arbitraria de usuarios, solo se puede asignar un tamaño fijo para una variable de pila. Tampoco puede cambiar el tamaño de las matrices. Por ejemplo:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Por supuesto, si usó std::string lugar, std::string redimensiona internamente para que no sea un problema. Pero esencialmente la solución a este problema es la asignación dinámica. Puede asignar memoria dinámica según la entrada del usuario, por ejemplo:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Nota al margen : un error que cometen muchos principiantes es el uso de matrices de longitud variable. Esta es una extensión de GNU y también una en Clang porque reflejan muchas de las extensiones de GCC. Por lo tanto, no se debe confiar en el siguiente int arr[n] .

Debido a que la pila es mucho más grande que la pila, uno puede asignar / reasignar arbitrariamente toda la memoria que necesita, mientras que la pila tiene una limitación.

2. Las matrices no son punteros.

¿Cómo es esto un beneficio que pides? La respuesta se aclarará una vez que comprenda la confusión / mito detrás de las matrices y los indicadores. Se asume comúnmente que son lo mismo, pero no lo son. Este mito proviene del hecho de que los punteros se pueden subíndicar como matrices y debido a que las matrices decaen a punteros en el nivel superior en una declaración de función. Sin embargo, una vez que una matriz se desintegra a un puntero, el puntero pierde su sizeof información. Entonces, sizeof(pointer) dará el tamaño del puntero en bytes, que generalmente es de 8 bytes en un sistema de 64 bits.

No puede asignar a matrices, solo inicializarlas. Por ejemplo:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

Por otro lado, puedes hacer lo que quieras con punteros. Desafortunadamente, debido a que la distinción entre punteros y matrices se hace con la mano en Java y C #, los principiantes no entienden la diferencia.

3. polimorfismo

Java y C # tienen instalaciones que le permiten tratar los objetos como otros, por ejemplo, usando la palabra clave as . Entonces, si alguien quisiera tratar un objeto de Entity como un objeto de Player , uno podría hacer Player player = Entity as Player; Esto es muy útil si pretende llamar a funciones en un contenedor homogéneo que solo debería aplicarse a un tipo específico. La funcionalidad se puede lograr de manera similar a continuación:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Entonces, si solo Triángulos tuviera una función de Rotar, sería un error del compilador si intentara llamarlo a todos los objetos de la clase. Usando dynamic_cast , puede simular la palabra clave as . Para ser claros, si una conversión falla, devuelve un puntero no válido. Así que !test es esencialmente una forma abreviada de comprobar si la test es NULL o un puntero no válido, lo que significa que la conversión falló.

Beneficios de las variables automáticas.

Después de ver todas las grandes cosas que puede hacer la asignación dinámica, probablemente se esté preguntando ¿por qué alguien NO usaría la asignación dinámica todo el tiempo? Ya te dije una razón, el montón es lento. Y si no necesitas toda esa memoria, no deberías abusar de ella. Así que aquí hay algunas desventajas en ningún orden en particular:

  • Es propenso a errores. La asignación manual de memoria es peligrosa y es propenso a fugas. Si no valgrind el uso del depurador o valgrind (una herramienta de pérdida de memoria), puede quitarse el cabello de la cabeza. Afortunadamente, los modismos RAII y los punteros inteligentes alivian esto un poco, pero debes estar familiarizado con prácticas como la Regla de tres y la Regla de cinco. Es mucha información para asimilar, y los principiantes que no saben o no les importa caerán en esta trampa.

  • No es necesario. A diferencia de Java y C #, donde es idiomático usar la new palabra clave en todas partes, en C ++, solo debe usarla si lo necesita. La frase común dice: todo parece un clavo si tienes un martillo. Mientras que los principiantes que comienzan con C ++ tienen miedo de los punteros y aprenden a usar las variables de pila por costumbre, los programadores de Java y C # comienzan a usar los punteros sin entenderlo. Eso es, literalmente, pararse con el pie equivocado. Debes abandonar todo lo que sabes porque la sintaxis es una cosa, aprender el idioma es otra.

1. (N) RVO - Aka, (nombrado) Optimización del valor de retorno

Una optimización que muchos compiladores hacen son cosas que se llaman elision y optimización de valor de retorno . Estas cosas pueden obviar copias innecesarias que son útiles para objetos que son muy grandes, como un vector que contiene muchos elementos. Normalmente, la práctica común es usar punteros para transferir la propiedad en lugar de copiar los objetos grandes para moverlos . Esto ha llevado a la creación de semánticas de movimiento y punteros inteligentes .

Si está utilizando punteros, (N) RVO NO se produce. Es más beneficioso y menos propenso a errores aprovechar las ventajas de (N) RVO en lugar de devolver o pasar punteros si le preocupa la optimización. Las fugas de error pueden ocurrir si la persona que llama a una función es responsable de delete un objeto asignado dinámicamente. Puede ser difícil rastrear la propiedad de un objeto si los punteros se pasan como una papa caliente. Solo usa las variables de pila porque es más simple y mejor.


En C ++, los objetos asignados en la pila (utilizando el Object object; instrucción dentro de un bloque) solo vivirán dentro del alcance en el que se declaran. Cuando el bloque de código termina de ejecutarse, el objeto declarado se destruye. Mientras que si asigna memoria en el montón, utilizando Object* obj = new Object() , continuarán viviendo en el montón hasta que llame a delete obj .

Me gustaría crear un objeto en el montón cuando me gusta usar el objeto no solo en el bloque de código que lo declaró / asignó.


Es muy desafortunado que veas la asignación dinámica tan a menudo. Eso solo muestra cuántos programadores malos de C ++ hay.

En cierto sentido, tienes dos preguntas agrupadas en una sola. El primero es ¿cuándo debemos usar la asignación dinámica (usar la new )? El segundo es ¿cuándo debemos usar punteros?

El mensaje importante para llevar a casa es que siempre debe usar la herramienta adecuada para el trabajo . En casi todas las situaciones, hay algo más apropiado y más seguro que realizar una asignación dinámica manual y / o usar punteros en bruto.

Asignación dinámica

En tu pregunta, has demostrado dos formas de crear un objeto. La principal diferencia es la duración de almacenamiento del objeto. Al hacer Object myObject; dentro de un bloque, el objeto se crea con la duración del almacenamiento automático, lo que significa que se destruirá automáticamente cuando salga de su alcance. Cuando crea un new Object() , el objeto tiene una duración de almacenamiento dinámico, lo que significa que permanece activo hasta que lo delete explícitamente. Solo debe usar la duración del almacenamiento dinámico cuando lo necesite. Es decir, siempre debe preferir crear objetos con duración de almacenamiento automático cuando pueda .

Las dos situaciones principales en las que puede requerir una asignación dinámica:

  1. Necesita el objeto para sobrevivir el alcance actual : ese objeto específico en esa ubicación de memoria específica, no una copia de él. Si está de acuerdo con copiar / mover el objeto (la mayoría de las veces debería estarlo), debería preferir un objeto automático.
  2. Debe asignar una gran cantidad de memoria , que puede llenar fácilmente la pila. Sería bueno si no tuviéramos que preocuparnos por esto (la mayoría de las veces no deberías hacerlo), ya que está realmente fuera del alcance de C ++, pero desafortunadamente tenemos que lidiar con la realidad de los sistemas que utilizamos. estamos desarrollando para

Cuando realmente requiere una asignación dinámica, debe encapsularla en un puntero inteligente o algún otro tipo que realice RAII (como los contenedores estándar). Los punteros inteligentes proporcionan semántica de propiedad de objetos asignados dinámicamente. Eche un vistazo a std::unique_ptr y std::shared_ptr , por ejemplo. Si los usa adecuadamente, puede evitar casi por completo realizar su propia administración de memoria (consulte la Regla de cero ).

Punteros

Sin embargo, existen otros usos más generales para los punteros sin procesar más allá de la asignación dinámica, pero la mayoría tiene alternativas que debería preferir. Como antes, siempre prefiera las alternativas a menos que realmente necesite punteros .

  1. Necesita semántica de referencia . A veces desea pasar un objeto utilizando un puntero (independientemente de cómo se asignó) porque quiere que la función a la que le está pasando tenga acceso a ese objeto específico (no una copia). Sin embargo, en la mayoría de las situaciones, debería preferir los tipos de referencia a los punteros, porque esto es específicamente para lo que están diseñados. Tenga en cuenta que esto no tiene que ver necesariamente con extender la vida útil del objeto más allá del alcance actual, como en la situación 1 anterior. Como antes, si está de acuerdo con pasar una copia del objeto, no necesita semántica de referencia.

  2. Necesitas polimorfismo . Solo puede llamar a funciones polimórficamente (es decir, según el tipo dinámico de un objeto) a través de un puntero o una referencia al objeto. Si ese es el comportamiento que necesita, entonces necesita usar punteros o referencias. Una vez más, se deben preferir las referencias.

  3. Desea representar que un objeto es opcional permitiendo que se pase un nullptr cuando se omite el objeto. Si se trata de un argumento, debería preferir usar argumentos predeterminados o sobrecargas de funciones. De lo contrario, debería preferir usar un tipo que encapsule este comportamiento, como std::optional (introducido en C ++ 17 - con estándares de C ++ anteriores, use boost::optional ).

  4. Desea desacoplar las unidades de compilación para mejorar el tiempo de compilación . La propiedad útil de un puntero es que solo necesita una declaración de reenvío del tipo apuntado (para usar el objeto, necesitará una definición). Esto le permite desacoplar partes de su proceso de compilación, lo que puede mejorar significativamente el tiempo de compilación. Ver el lenguaje Pimpl .

  5. Debe interactuar con una biblioteca de C o una biblioteca de estilo C. En este punto, estás obligado a usar punteros en bruto. Lo mejor que puedes hacer es asegurarte de soltar los punteros en bruto en el último momento posible. Puede obtener un puntero en bruto de un puntero inteligente, por ejemplo, utilizando su función de miembro de get . Si una biblioteca realiza alguna asignación para usted que espera que se desasigne mediante un identificador, a menudo puede envolver el identificador en un puntero inteligente con un borrado personalizado que desasignará el objeto de manera adecuada.


Hay muchos casos de uso para los punteros.

Comportamiento polimórfico . Para los tipos polimórficos, se utilizan punteros (o referencias) para evitar el corte:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Referencia semántica y evitando copiar . Para tipos no polimórficos, un puntero (o una referencia) evitará copiar un objeto potencialmente costoso

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Tenga en cuenta que C ++ 11 tiene semánticas de movimiento que pueden evitar muchas copias de objetos caros en el argumento de la función y como valores de retorno. Pero usar un puntero definitivamente los evitará y permitirá múltiples punteros en el mismo objeto (mientras que un objeto solo se puede mover una vez).

Adquisición de recursos . Crear un puntero a un recurso utilizando el new operador es un antipatrón en C ++ moderno. Utilice una clase de recurso especial (uno de los contenedores estándar) o un puntero inteligente ( std::unique_ptr<> o std::shared_ptr<> ). Considerar:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

contra

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Un puntero en bruto solo debe usarse como una "vista" y no debe implicar de ninguna manera a la propiedad, ya sea a través de la creación directa o implícitamente a través de los valores de retorno. Consulte también estas preguntas y respuestas de las Preguntas frecuentes de C ++ .

Control de tiempo de vida más detallado Cada vez que se copia un puntero compartido (por ejemplo, como un argumento de función), el recurso al que apunta se mantiene vivo. Los objetos normales (no creados por new , ya sea directamente por usted o dentro de una clase de recurso) se destruyen cuando se salen del alcance.


Otra buena razón para usar punteros sería para las declaraciones a futuro . En un proyecto lo suficientemente grande, realmente pueden acelerar el tiempo de compilación.


Con punteros ,

  • Puede hablar directamente a la memoria.

  • puede evitar muchas fugas de memoria de un programa mediante la manipulación de punteros.


Hay muchos beneficios de usar punteros para objetar -

  1. Eficiencia (como ya has señalado). Pasar objetos a funciones significa crear nuevas copias de objetos.
  2. Trabajar con objetos de bibliotecas de terceros. Si su objeto pertenece a un código de un tercero y los autores pretenden utilizar sus objetos solo a través de los punteros (no hay constructores de copia, etc.), la única manera de pasar este objeto es usar punteros. Pasar por valor puede causar problemas. (Copia profunda / problemas de copia superficial).
  3. Si el objeto es propietario de un recurso y desea que la propiedad no debe ser sahred con otros objetos.

"La necesidad es la madre de la invención." La diferencia más importante que me gustaría señalar es el resultado de mi propia experiencia de codificación. A veces hay que pasar objetos a funciones. En ese caso, si su objeto es de una clase muy grande, al pasarlo como un objeto se copiará su estado (que es posible que no desee ... Y PUEDE SER GRANDE EN EL EXTRANJERO), lo que da como resultado una sobrecarga de copia del objeto. Tamaño de 4 bytes (suponiendo 32 bits). Otras razones ya se mencionan anteriormente ...


Bueno, la pregunta principal es ¿Por qué debería usar un puntero en lugar del objeto en sí? Y mi respuesta es que (casi) nunca debe usar el puntero en lugar del objeto, porque C ++ tiene references , es más seguro que los punteros y garantiza el mismo rendimiento que los punteros.

Otra cosa que mencionaste en tu pregunta:

Object *myObject = new Object;

¿Como funciona?Crea un puntero de Objecttipo, asigna memoria para ajustarse a un objeto y llama al constructor predeterminado, suena bien, ¿verdad? Pero en realidad no es tan bueno, si asignó dinámicamente memoria (palabra clave utilizada new), también tiene que liberar memoria manualmente, eso significa que en el código debería tener:

delete myObject;

Esto llama a destructor y libera memoria, parece fácil, sin embargo, en grandes proyectos puede ser difícil detectar si un hilo libera memoria o no, pero para eso puede probar punteros compartidos , esto reduce ligeramente el rendimiento, pero es mucho más fácil trabajar con ellos. ellos.

Y ahora, una introducción ha terminado y vuelve a la pregunta.

Puede usar punteros en lugar de objetos para obtener un mejor rendimiento al transferir datos entre funciones.

Eche un vistazo, tiene std::string(también es un objeto) y contiene mucha información, por ejemplo, un gran XML, ahora necesita analizarlo, pero para eso tiene una función void foo(...)que se puede declarar de diferentes maneras:

  1. void foo(std::string xml); En este caso, copiará todos los datos de su variable a la pila de funciones, esto llevará algún tiempo, por lo que su rendimiento será bajo.
  2. void foo(std::string* xml);En este caso, pasará el puntero al objeto, la misma velocidad que la size_tvariable que pasa , sin embargo, esta declaración es propensa a errores, ya que puede pasar el NULLpuntero o el puntero no válido. Los punteros usualmente se usan Cporque no tiene referencias.
  3. void foo(std::string& xml); Aquí se pasa la referencia, básicamente es lo mismo que pasar el puntero, pero el compilador hace algunas cosas y no se puede pasar la referencia no válida (en realidad es posible crear una situación con una referencia no válida, pero es un compilador engañoso).
  4. void foo(const std::string* xml); Aquí es lo mismo que segundo, solo el valor del puntero no se puede cambiar.
  5. void foo(const std::string& xml); Aquí es lo mismo que tercero, pero el valor del objeto no se puede cambiar.

Además, quiero mencionar que puede utilizar estas 5 formas de pasar datos sin importar la asignación que haya elegido (con newo normal ).

Otra cosa a mencionar, cuando creas un objeto de manera regular , asignas memoria en la pila, pero mientras lo creas con newtu asignación de montón. Es mucho más rápido para asignar pila, pero se deja ver una pequeña para realmente grandes conjuntos de datos, por lo que si usted necesita gran objeto que se debe utilizar montón, ya que puede contraer desbordamiento de pila, pero por lo general se resuelve este problema utilizando contenedores STL y recordar std::stringTambién es contenedor, algunos chicos lo olvidaron :)


En áreas donde la utilización de la memoria es primordial, los punteros son útiles. Por ejemplo, considere un algoritmo minimax, donde se generarán miles de nodos utilizando una rutina recursiva, y luego, utilícelos para evaluar el siguiente mejor movimiento en el juego, la capacidad de desasignar o restablecer (como en los punteros inteligentes) reduce significativamente el consumo de memoria. Mientras que la variable no puntero continúa ocupando espacio hasta que su llamada recursiva devuelve un valor.


Esto se ha discutido extensamente, pero en Java todo es un puntero. No hace distinción entre las asignaciones de pila y pila (todos los objetos se asignan en la pila), por lo que no se da cuenta de que está utilizando punteros. En C ++, puede mezclar los dos, dependiendo de sus requisitos de memoria. El rendimiento y el uso de la memoria es más determinista en C ++ (duh).


Un puntero hace referencia directamente a la ubicación de la memoria de un objeto. Java no tiene nada como esto. Java tiene referencias que hacen referencia a la ubicación del objeto a través de tablas hash. No puede hacer nada como la aritmética de punteros en Java con estas referencias.

Para responder a su pregunta, es sólo su preferencia. Prefiero usar la sintaxis de Java.


Una razón para usar punteros es para interactuar con las funciones C. Otra razón es ahorrar memoria; por ejemplo: en lugar de pasar un objeto que contiene una gran cantidad de datos y tiene un constructor de copias con un uso intensivo del procesador a una función, simplemente pase un puntero al objeto, ahorrando memoria y velocidad, especialmente si está en un bucle, sin embargo la referencia sería mejor en ese caso, a menos que esté utilizando una matriz de estilo C.


Object *myObject = new Object;

Al hacer esto se creará una referencia a un Objeto (en el montón) que debe eliminarse explícitamente para evitar la pérdida de memoria .

Object myObject;

Al hacer esto, se creará un objeto (myObject) del tipo automático (en la pila) que se eliminará automáticamente cuando el objeto (myObject) quede fuera del alcance.







c++11