ejemplos - punteros en c++ ejercicios resueltos pdf
¿Por qué debería usar un puntero en lugar del objeto en sí? (22)
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.
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
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 ovalgrind
(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.
C ++ le ofrece tres formas de pasar un objeto: por puntero, por referencia y por valor. Java te limita con la última (la única excepción son los tipos primitivos como int, boolean, etc.). Si quieres usar C ++ no solo como un juguete raro, entonces es mejor que conozcas la diferencia entre estas tres formas.
Java pretende que no existe un problema como "¿quién y cuándo debería destruir esto?". La respuesta es: El recolector de basura, grande y horrible. Sin embargo, no puede proporcionar una protección del 100% contra las pérdidas de memoria (sí, Java puede perder memoria ). En realidad, GC te da una falsa sensación de seguridad. Cuanto más grande sea su SUV, más largo será su camino hacia el evacuador.
C ++ te deja cara a cara con la gestión del ciclo de vida del objeto. Bueno, hay medios para lidiar con eso (familia de punteros inteligentes , QObject en Qt y así sucesivamente), pero ninguno de ellos se puede usar en forma de ''disparar y olvidar'' como GC: siempre se debe tener en cuenta el manejo de la memoria. No solo debe preocuparse por destruir un objeto, sino que también debe evitar destruir el mismo objeto más de una vez.
¿No tienes miedo todavía? Ok: referencias cíclicas - manéjalos usted mismo, humano. Y recuerda: mata cada objeto precisamente una vez, a los tiempos de ejecución de C ++ no nos gustan los que se meten con los cadáveres, dejamos a los muertos en paz.
Entonces, volvamos a tu pregunta.
Cuando transfiere su objeto por valor, no por puntero o por referencia, copia el objeto (todo el objeto, ya sea un par de bytes o un gran volcado de base de datos: es lo suficientemente inteligente como para evitarlo, no lo es). t usted?) cada vez que haces ''=''. Y para acceder a los miembros del objeto, usas ''.'' (punto).
Cuando pasa su objeto por el puntero, copia solo unos pocos bytes (4 en sistemas de 32 bits, 8 en los de 64 bits), es decir, la dirección de este objeto. Y para mostrar esto a todos, utiliza este elegante operador ''->'' cuando accede a los miembros. O puede usar la combinación de ''*'' y ''.''.
Cuando utiliza referencias, obtiene el puntero que pretende ser un valor. Es un puntero, pero se accede a los miembros a través de ''.''.
Y, para volar tu mente una vez más: cuando declaras varias variables separadas por comas, entonces (mira las manos):
- El tipo es dado a todos.
- Valor / puntero / modificador de referencia es individual
Ejemplo:
struct MyStruct
{
int* someIntPointer, someInt; //here comes the surprise
MyStruct *somePointer;
MyStruct &someReference;
};
MyStruct s1; //we allocated an object on stack, not in heap
s1.someInt = 1; //someInt is of type ''int'', not ''int*'' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value ''2''
s1.somePointer = &s1;
s1.someReference = s1; //note there is no ''&'' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value ''3''
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value ''4''
s1.someReference.someInt = 5; //now s1.someInt has value ''5''
//although someReference is not value, it''s members are accessed through ''.''
MyStruct s2 = s1; //''NO WAY'' the compiler will say. Go define your ''='' operator and come back.
//OK, assume we have ''='' defined in MyStruct
s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it''s two completely different objects, not the references to the same one
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:
- 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.
- 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 .
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.
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.
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, comostd::optional
(introducido en C ++ 17 - con estándares de C ++ anteriores, useboost::optional
).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 .
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 muchas respuestas excelentes a esta pregunta, incluidos los casos de uso importantes de declaraciones a futuro, polimorfismo, etc., pero siento que no se responde a una parte del "alma" de su pregunta, es decir, qué significan las diferentes sintaxis en Java y C ++.
Examinemos la situación comparando los dos idiomas:
Java:
Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java
object1 = object2;
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other
El equivalente más cercano a esto, es:
C ++:
Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don''t do that, the next line would
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use
//and that we have no way to reclaim...
object1 = object2; //Same as Java, object1 points to object2.
Veamos la forma alternativa de C ++:
Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...
La mejor manera de pensarlo es que, más o menos, Java maneja los punteros a los objetos (implícitamente), mientras que C ++ puede manejar los punteros a los objetos o los objetos en sí. Hay excepciones a esto, por ejemplo, si declara los tipos "primitivos" de Java, son valores reales que se copian y no punteros. Asi que,
Java:
int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.
Dicho esto, el uso de punteros NO es necesariamente la forma correcta o incorrecta de manejar las cosas; sin embargo otras respuestas han cubierto eso satisfactoriamente. Sin embargo, la idea general es que en C ++ tienes mucho más control sobre la vida útil de los objetos y sobre dónde vivirán.
Punto de partida: la construcción Object * object = new Object()
es en realidad la más cercana a la semántica típica de Java (o C #).
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.
Técnicamente es un problema de asignación de memoria, sin embargo, aquí hay dos aspectos prácticos más de esto. Tiene que ver con dos cosas: 1) Ámbito, cuando define un objeto sin un puntero, no podrá acceder a él después del bloque de código en el que está definido, mientras que si define un puntero con "nuevo", puede acceder a él desde cualquier lugar que tenga un puntero a esta memoria hasta que llame "eliminar" en el mismo puntero. 2) Si desea pasar argumentos a una función, desea pasar un puntero o una referencia para que sea más eficiente.Cuando pasa un Objeto, entonces el objeto se copia, si este es un objeto que usa mucha memoria, esto podría consumir CPU (por ejemplo, si copia un vector lleno de datos). Cuando pasa un puntero, todo lo que pasa es un int (dependiendo de la implementación, pero la mayoría de ellos son un int).
Aparte de eso, debe comprender que "nuevo" asigna memoria en el montón que debe liberarse en algún momento. Cuando no tenga que usar "nuevo", le sugiero que use una definición de objeto regular "en la pila".
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 -
- Eficiencia (como ya has señalado). Pasar objetos a funciones significa crear nuevas copias de objetos.
- 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).
- Si el objeto es propietario de un recurso y desea que la propiedad no debe ser sahred con otros objetos.
Ya hay muchas respuestas excelentes, pero déjame darte un ejemplo:
Tengo una clase de artículo simple:
class Item
{
public:
std::string name;
int weight;
int price;
};
Hago un vector para contener un montón de ellos.
std::vector<Item> inventory;
Creo un millón de objetos de Objeto y los empujo de nuevo sobre el vector. Ordeno el vector por nombre, y luego hago una búsqueda binaria iterativa simple para un nombre de elemento particular. Pruebo el programa, y tarda más de 8 minutos en terminar de ejecutarse. Entonces cambio mi vector de inventario de esta manera:
std::vector<Item *> inventory;
... y crear mis objetos de millones de artículos a través de nuevos. Los ÚNICOS cambios que hago en mi código son para usar los punteros a los Elementos, a excepción de un bucle que agrego para la limpieza de la memoria al final. Ese programa se ejecuta en menos de 40 segundos, o mejor que un aumento de velocidad de 10x. EDITAR: El código está en http://pastebin.com/DK24SPeW Con las optimizaciones del compilador, muestra solo un aumento de 3.4x en la máquina en la que lo probé, lo cual sigue siendo considerable.
"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 Object
tipo, 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:
-
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. -
void foo(std::string* xml);
En este caso, pasará el puntero al objeto, la misma velocidad que lasize_t
variable que pasa , sin embargo, esta declaración es propensa a errores, ya que puede pasar elNULL
puntero o el puntero no válido. Los punteros usualmente se usanC
porque no tiene referencias. -
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). -
void foo(const std::string* xml);
Aquí es lo mismo que segundo, solo el valor del puntero no se puede cambiar. -
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 new
o normal ).
Otra cosa a mencionar, cuando creas un objeto de manera regular , asignas memoria en la pila, pero mientras lo creas con new
tu 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::string
También es contenedor, algunos chicos lo olvidaron :)
Digamos que tiene class A
ese contenido. class B
Cuando quiera llamar a alguna función de class B
fuera class A
, simplemente obtendrá un puntero a esta clase y podrá hacer lo que quiera y también cambiará el contexto de class B
suclass A
Pero ten cuidado con el objeto dinámico
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).
Incluiré un caso de uso importante de puntero. Cuando estás almacenando algún objeto en la clase base, pero podría ser polimórfico.
Class Base1 {
};
Class Derived1 : public Base1 {
};
Class Base2 {
Base *bObj;
virtual void createMemerObects() = 0;
};
Class Derived2 {
virtual void createMemerObects() {
bObj = new Derived1();
}
};
Entonces, en este caso, no puede declarar bObj como un objeto directo, debe tener un puntero.
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.
Usted no debería . La gente (mucha gente, tristemente) lo escribe por ignorancia.
A veces, la asignación dinámica tiene su lugar pero, en los ejemplos que da, está mal .
Si quiere pensar en la eficiencia, esto es peor , porque no introduce una buena dirección indirecta. Este tipo de programación es más lenta y más propensa a errores .
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.