resueltos que punteros nombres matriz funciones ejercicios ejemplos caracteres cadenas arreglo array almacenar c++ c++11

que - punteros c++



¿Hay casos de uso válidos para usar punteros nuevos y eliminados, o matrices de estilo c con C++ moderno? (19)

Cuando desee crear matrices multidimensionales pero no esté familiarizado con la sintaxis de C ++ 11 como std :: move, o no esté familiarizado con la escritura de eliminadores personalizados para punteros inteligentes.

Aquí hay un video notable ( Deje de enseñar C ) sobre ese cambio de paradigma para enseñar el lenguaje c ++.

Y una publicación de blog también notable

Tengo un sueño ...

Estoy soñando con los llamados cursos / clases / currículos de C ++ que dejarán de enseñar (requieren) que sus estudiantes usen: ...

Desde C ++ 11 como estándar establecido, tenemos las instalaciones de administración de memoria dinámica , también conocidas como punteros inteligentes .
Incluso a partir de estándares anteriores, tenemos la biblioteca de Contenedores estándar de c ++ como un buen reemplazo para las matrices sin procesar (asignadas con la new T[] ) (especialmente el uso de std::string lugar de las matrices de caracteres terminadas en NUL estilo c).

Pregunta (s) en negrita :

Dejando a un lado la new anulación de ubicación, ¿hay algún caso de uso válido que no se pueda lograr usando punteros inteligentes o contenedores estándar, sino solo usando new y delete directamente (además de la implementación de tales clases de contenedor / puntero inteligente, por supuesto)?

A veces se rumorea (como here o here ) que usar new y delete handrolled puede ser "más eficiente" para ciertos casos. ¿Cuáles son estos en realidad? ¿No es necesario que estos casos extremos realicen un seguimiento de las asignaciones de la misma manera que los contenedores estándar o los punteros inteligentes deben hacer?

Casi lo mismo para las matrices de tamaño fijo sin formato de estilo c: hay std::array hoy en día, que permite todo tipo de asignación, copia, referencia, etc. de manera fácil y sintácticamente coherente, como todos esperan. ¿Hay algún caso de uso para elegir un T myArray[N]; matriz de estilo c en preferencia de std::array<T,N> myArray; ?

Con respecto a la interacción con bibliotecas de terceros:

Se supone que una biblioteca de terceros devuelve punteros sin formato asignados con new gusta

MyType* LibApi::CreateNewType() { return new MyType(someParams); }

siempre puede ajustarlo a un puntero inteligente para asegurarse de que se llame a delete :

std::unique_ptr<MyType> foo = LibApi::CreateNewType();

incluso si la API requiere que llame a su función heredada para liberar el recurso como

void LibApi::FreeMyType(MyType* foo);

aún puede proporcionar una función de eliminación:

std::unique_ptr<MyType, LibApi::FreeMyType> foo = LibApi::CreateNewType();

Estoy especialmente interesado en casos de uso válidos "todos los días" en contraste con los requisitos y restricciones académicos / educativos , que no están cubiertos por las instalaciones estándar mencionadas.
Que new y delete se puede usar en marcos de gestión de memoria / recolector de basura o la implementación de contenedor estándar está fuera de cuestión 1 .

Una gran motivación ...

... hacer esta pregunta es dar un enfoque alternativo frente a cualquier pregunta (tarea), que está restringida a usar cualquiera de los constructos mencionados en el título, pero preguntas serias sobre el código listo para producción.

A menudo se los conoce como los fundamentos de la gestión de la memoria, lo cual es totalmente erróneo o malentendido de la OMI como adecuado para las clases y tareas para principiantes .

1) Agregar: con respecto a ese párrafo, este debería ser un indicador claro de que new y delete no es para estudiantes principiantes de c ++, sino que debe dejarse para los cursos más avanzados.


3 ejemplos comunes donde tienes que usar new en lugar de make_... :

  • Si su objeto no tiene un constructor público
  • Si quieres usar un eliminador personalizado
  • Si está utilizando c ++ 11 y desea crear un objeto administrado por un unique_ptr (aunque recomendaría escribir su propio make_unique en ese caso).

Sin embargo, en todos esos casos, envolvería directamente el puntero devuelto en un puntero inteligente.

Ejemplos 2-3 (probablemente no tan comunes), donde no querría / no puede usar punteros inteligentes:

  • Si tiene que pasar sus tipos a través de una c-api (usted es el que implementa create_my_object o implementa una devolución de llamada que debe anularse *)
  • Casos de propiedad condicional: piense en una cadena, que no asigna memoria cuando se crea a partir de una cadena literal, sino que solo apunta a esos datos. Sin embargo, probablemente podría usar una std::variant<T*, unique_ptr<T>> lugar, pero solo si está de acuerdo con la información sobre la propiedad almacenada en la variante y acepta la sobrecarga de verificar qué miembro es activo para cada acceso. Por supuesto, esto solo es relevante si no puede / no desea permitirse el gasto de tener dos punteros (uno propio y otro no)
    • Si desea basar su propiedad en algo más complejo que un puntero. Por ejemplo, desea usar un gsl :: owner para que pueda consultar fácilmente su tamaño y tener todos los demás beneficios (iteración, comprobación de rango ...). Es cierto que lo más probable es que lo envuelva en su propia clase, por lo que esto podría caer en la categoría de implementación de un contenedor.

A veces tienes que llamar a new cuando usas constructores privados.

Supongamos que decide tener un constructor privado para un tipo al que pretende llamar una fábrica amiga o un método de creación explícito. Puedes llamar new dentro de esta fábrica pero make_unique no funcionará.


Además de otras respuestas, hay algunos casos en los que new / delete tiene sentido:

  1. Integración con una biblioteca de terceros que devuelve el puntero sin procesar y espera que devuelva el puntero a la biblioteca una vez que haya terminado (la biblioteca tiene su propia funcionalidad de administración de memoria).
  2. Trabajando en un dispositivo integrado con recursos limitados donde la memoria (RAM / ROM) es un lujo (incluso unos pocos kilobytes). ¿Está seguro de que desea agregar más requisitos de memoria en tiempo de ejecución (RAM) y compilados (ROM / Overlay) a su aplicación o desea programar cuidadosamente con nuevo / eliminar?
  3. Desde el punto de vista purista, en algunos casos los punteros inteligentes no funcionarán intuitivamente (debido a su naturaleza). Por ejemplo, para el patrón de construcción, debe usar reinterpret_pointer_cast, si está utilizando punteros inteligentes. Otro caso es donde necesita convertir de un tipo base a un tipo derivado. Se pone en peligro si obtiene el puntero sin procesar del puntero inteligente, lo echó y lo puso en otro puntero inteligente y terminó liberando el puntero varias veces.

Algunas API pueden esperar que cree objetos con new pero se harán cargo de la propiedad del objeto. La biblioteca Qt , por ejemplo, tiene un modelo padre-hijo donde el padre elimina a sus hijos. Si usa un puntero inteligente, se encontrará con problemas de doble eliminación si no tiene cuidado.

Ejemplo:

{ // parentWidget has no parent. QWidget parentWidget(nullptr); // childWidget is created with parentWidget as parent. auto childWidget = new QWidget(&parentWidget); } // At this point, parentWidget is destroyed and it deletes childWidget // automatically.

En este ejemplo en particular, aún puede usar un puntero inteligente y estará bien:

{ QWidget parentWidget(nullptr); auto childWidget = std::make_unique<QWidget>(&parentWidget); }

porque los objetos se destruyen en orden inverso a la declaración. unique_ptr eliminará childWidget primero, lo que hará que childWidget se childWidget registro de parentWidget y, por lo tanto, evitará la doble eliminación. Sin embargo, la mayoría de las veces no tienes esa pulcritud. Hay muchas situaciones en las que los padres serán destruidos primero, y en esos casos, los niños serán eliminados dos veces.

En el caso anterior, somos dueños del padre en ese ámbito y, por lo tanto, tenemos el control total de la situación. En otros casos, el padre podría no ser horas, pero estamos cediendo la propiedad de nuestro widget hijo a ese padre, que vive en otro lugar.

Puede estar pensando que para resolver esto, solo tiene que evitar el modelo padre-hijo y crear todos sus widgets en la pila y sin un padre:

QWidget childWidget(nullptr);

o con un puntero inteligente y sin un padre:

auto childWidget = std::make_unique<QWidget>(nullptr);

Sin embargo, esto también explotará en su cara, ya que una vez que comience a usar el widget, podría volver a ser criado a sus espaldas. Una vez que otro objeto se convierte en el padre, obtiene doble eliminación cuando usa unique_ptr , y la eliminación de la pila al crearlo en la pila.

La forma más fácil de trabajar con esto es usar new . Cualquier otra cosa es invitar a problemas, o más trabajo, o ambos.

Dichas API se pueden encontrar en software moderno y no obsoleto (como Qt), y se han desarrollado hace años, mucho antes de que los punteros inteligentes fueran una cosa. No se pueden cambiar fácilmente ya que eso rompería el código existente de las personas.


Creo que este es un buen caso de uso y / o pauta a seguir:

  • Cuando el puntero es local al alcance de una sola función.
  • La memoria dinámica se maneja en la función y necesita el montón.
  • No está pasando el puntero y no está saliendo del alcance de las funciones.

Código PSEUDO:

#include <SomeImageLibrary> // Texture is a class or struct defined somewhere else. unsigned funcToOpenAndLoadImageData( const std::string& filenameAndPath, Texture& texture, some optional flags (how to process or handle within function ) { // Depending on the above library: file* or iostream... // 1. OpenFile // 2. Read In Header // 3. Process Header // 4. setup some local variables. // 5. extract basic local variables from the header // A. texture width, height, bits per pixel, orientation flags, compression flags etc. // 6. Do some calculations based on the above to find out how much data there is for the actual ImageData... // 7. Raw pointer (typically of unsigned char). // 8. Create dynamic memory for that pointer or array. // 9. Read in the information from the file of that amount into the pointer - array. // 10. Verify you have all the information. // 11. Close the file handle. // 12. Process some more information on the actual pointer or array itself // based on its orientation, its bits per pixel, its dimensions, the color type, the compression type, and or if it exists encryption type. // 13. Store the modified data from the array into Your Structure (Texture - Class/Struct). // 14. Free up dynamic memory... // 15. typically return the texture through the parameter list as a reference // 16. typically return an unsigned int as the Texture''s numerical ID. }

Esto es bastante efectivo; eficiente, no necesita ningún uso de punteros inteligentes; es rápido, especialmente si se incluye la función. Este tipo de función puede ser independiente o incluso miembro de una clase. Si un patrón sigue esto, entonces es bastante seguro usar new & delete o new [] & delete [] si se hace correctamente.

EDITAR

En el (los) caso (s) mencionado (s) arriba, a veces quiere los punteros sin procesar y los quiere en el montón. Supongamos que tiene una aplicación que cargará, por ejemplo, 5,000 archivos de textura, 500 archivos de modelo, 20 archivos de escena, 500-1000 archivos de audio. No desea que su tiempo de carga sea lento, también quiere que sea amigable con el "caché". La carga de textura es un muy buen ejemplo de tener el puntero en el montón en lugar de la pila de funciones porque la textura podría ser de gran tamaño y exceder las capacidades de memoria local.

En este contexto, llamará a estas funciones de carga una vez por objeto, pero las llamará varias veces. Una vez que cargó y creó sus recursos o activos y los almacenó internamente, es cuándo y dónde desea utilizar contenedores en lugar de matrices y punteros inteligentes en lugar de punteros sin procesar.

Cargará un solo activo una vez, pero puede tener 100 o 1000 instancias de él. Es con estas instancias que preferiría el uso de contenedores y el uso de punteros inteligentes para administrar su memoria dentro de su aplicación en lugar de punteros y matrices sin procesar. La carga inicial es donde preferiría estar más cerca del metal sin toda la carga adicional no deseada.

Si estaba trabajando en un juego de clase A + y podía ahorrarle a su audiencia de 15 a 30 segundos o más de tiempo de carga por pantalla de carga, entonces está en el círculo de ganadores. Sí, es necesario tener cuidado y sí, aún puede tener excepciones no controladas, pero ningún código es 100% prueba completa.

Este tipo de diseño rara vez es propenso a pérdidas de memoria, excepto por las excepciones que aún se pueden manejar en muchos de los casos. Además, para administrar de forma segura los punteros en bruto, las macros de preprocesador funcionan bien para facilitar la limpieza.

Muchos de estos tipos de biblioteca también trabajan y se ocupan de raw data , raw memory allocation , etc, y muchas veces punteros inteligentes no necesariamente se ajustan a este tipo de trabajos.


Cuando la propiedad no debe ser local.

Como ejemplo, un contenedor de puntero puede no querer que la propiedad sobre los punteros en él resida en los punteros mismos. Si intentas escribir una lista vinculada con ptrs únicos hacia adelante, en el momento de la destrucción puedes volar fácilmente la pila.

Un contenedor tipo vector de punteros propietarios puede ser más adecuado para almacenar la operación de eliminación en el nivel de contenedor o subcontenedor, y no en el nivel de elemento.

En esos y otros casos similares, ajusta la propiedad como lo hace un puntero inteligente, pero lo hace a un nivel superior. Muchas estructuras de datos (gráficos, etc.) pueden tener problemas similares, donde la propiedad reside adecuadamente en un punto más alto que donde están los punteros, y es posible que no se asignen directamente a un concepto de contenedor existente.

En algunos casos, puede ser fácil factorizar la propiedad del contenedor del resto de la estructura de datos. En otros puede que no.

A veces tiene vidas contadas increíblemente complejas, no locales y sin referencia. No hay lugar sensato para colocar el puntero de propiedad en esos casos.

Determinar la corrección aquí es difícil, pero no imposible. Existen programas que son correctos y tienen una semántica de propiedad tan compleja.

Todos estos son casos de esquina, y pocos programadores deberían encontrarse con ellos más de un puñado de veces en una carrera.


Cuando tiene que pasar algo a través del límite de la DLL. Usted (casi) no puede hacer eso con punteros inteligentes.


El OP pregunta específicamente acerca de cómo / cuándo el manejo manual será más eficiente en un caso de uso diario, y lo abordaré.

Suponiendo un compilador / stl / plataforma moderno, no hay un uso diario en el que el uso manual de new y delete sea más eficiente. Para el caso shared_ptr, creo que será marginal. En un bucle (s) extremadamente estrecho (s) podría haber algo que ganar simplemente usando raw new para evitar el recuento de referencias (y encontrar algún otro método de limpieza, a menos que se le imponga de alguna manera, elige usar shared_ptr por alguna razón), pero ese no es un ejemplo cotidiano o común. Para el unique_ptr en realidad no hay ninguna diferencia, por lo que creo que es seguro decir que se trata más de rumores y folklore y que, en cuanto al rendimiento, en realidad no importará en absoluto (la diferencia no será medible en casos normales).

Hay casos en los que no es deseable o posible utilizar una clase de puntero inteligente como ya está cubierta por otros.


El caso de uso principal en el que todavía uso punteros sin procesar es cuando se implementa una jerarquía que usa tipos de retorno covariantes .

Por ejemplo:

#include <iostream> #include <memory> class Base { public: virtual ~Base() {} virtual Base* clone() const = 0; }; class Foo : public Base { public: ~Foo() override {} // Case A in main wouldn''t work if this returned `Base*` Foo* clone() const override { return new Foo(); } }; class Bar : public Base { public: ~Bar() override {} // Case A in main wouldn''t work if this returned `Base*` Bar* clone() const override { return new Bar(); } }; int main() { Foo defaultFoo; Bar defaultBar; // Case A: Can maintain the same type when cloning std::unique_ptr<Foo> fooCopy(defaultFoo.clone()); std::unique_ptr<Bar> barCopy(defaultBar.clone()); // Case B: Of course cloning to a base type still works std::unique_ptr<Base> base1(fooCopy->clone()); std::unique_ptr<Base> base2(barCopy->clone()); return 0; }


Otro caso de uso puede ser una biblioteca de terceros que devuelve un puntero sin procesar que está cubierto internamente por un recuento de referencias intrusivo (o una gestión de memoria propia, que no está cubierta por ninguna interfaz API / usuario).

Un buen ejemplo es OpenSceneGraph y su implementación del contenedor osg :: ref_ptr y la clase base referenciada osg ::.

Aunque puede ser posible utilizar shared_ptr, el recuento de referencias intrusivas es mucho mejor para los gráficos de escenas como los casos de uso.

Personalmente, veo algo "inteligente" en unique_ptr. Es solo alcance bloqueado nuevo y eliminar. Aunque shared_ptr se ve mucho mejor, requiere una sobrecarga que, en muchos casos prácticos, es inaceptable.

Entonces, en general, mi caso de uso es:

Cuando se trata de envoltorios de puntero sin formato STL.


Otro posible caso de uso válido es cuando codifica algún recolector de basura .

Imagine que está codificando algún intérprete de Scheme en C ++ 11 (o algún intérprete de código de bytes Ocaml). Ese lenguaje requiere que codifique un GC (por lo que debe codificar uno en C ++). Entonces, la propiedad no es local, como respondió Yakk . ¡Y desea recolectar basura de los valores de Scheme, no de la memoria sin procesar!

Probablemente terminará usando explícitamente new y delete .

En otras palabras, los punteros inteligentes C ++ 11 favorecen algún esquema de conteo de referencias . Pero esa es una técnica de GC deficiente (no es amigable con las referencias circulares, que son comunes en Scheme).

Por ejemplo, una forma ingenua de implementar un GC de marcado y barrido simple sería recopilar en algún contenedor global todos los punteros de los valores del Esquema, etc.

Lea también el manual de GC .


Para casos de uso simples, los punteros inteligentes, los contenedores estándar y las referencias deberían ser suficientes para no usar punteros y asignación y desasignación sin procesar.

Ahora para los casos en los que puedo pensar:

  • desarrollo de contenedores u otros conceptos de bajo nivel: después de todo, la biblioteca estándar está escrita en C ++ y hace uso de punteros sin formato, nuevos y eliminados
  • Optimización de bajo nivel. Nunca debería ser una preocupación de primera clase, porque los compiladores son lo suficientemente inteligentes como para optimizar el código estándar , y la capacidad de mantenimiento es normalmente más importante que el rendimiento en bruto. Pero cuando la creación de perfiles muestra que un bloque de código representa más del 80% del tiempo de ejecución, la optimización de bajo nivel tiene sentido, y esa es una de las razones por las cuales la biblioteca estándar de bajo nivel C sigue siendo parte de los estándares C ++

Todavía existe la posibilidad de usar malloc/free en C ++, ya que puede usar new/delete y cualquier otro nivel superior que envuelva las plantillas de memoria STL proporcionadas.

Creo que para aprender realmente C ++ y comprender especialmente las plantillas de memoria C ++ 11, debe crear estructuras simples con new y delete . Solo para entender mejor cómo funcionan. Todas las clases de punteros inteligentes se basan en esos mecanismos. Entonces, si comprende lo que hace new y delete , apreciará más la plantilla y realmente encontrará formas inteligentes de usarla.

Hoy personalmente trato de evitarlos tanto como sea posible, pero una de las razones principales es el rendimiento, que debe tener en cuenta si es crítico.

Estas son mis reglas generales que siempre tengo en mente:

std::shared_ptr : gestión automática de punteros, pero debido al recuento de referencias que utiliza para rastrear los punteros accedidos, tiene un rendimiento peor cada vez que accede a estos objetos. Comparado punteros simples, diría 6 veces más lento. Tenga en cuenta que puede usar get() y extraer el puntero primitivo y continuar accediendo a él. De ustedes deben tener cuidado con eso. Me gusta como referencia con *get() , por lo que el peor rendimiento no es realmente un trato.

std::unique_ptr El acceso al puntero solo puede ocurrir en un punto del código. Debido a que esta plantilla prohíbe la copia, gracias a la función r-references && , es mucho más rápida que std::shared_ptr . Como todavía hay algo de sobrecarga de propiedad en esta clase, diría, son aproximadamente el doble de lentos que un puntero primitivo. Accede al objeto que el puntero primitivo dentro de esa plantilla. También me gusta usar el truco de referencia aquí, para accesos menos necesarios al objeto.

Sobre el rendimiento, puede ser cierto, que esas plantillas son más lentas, pero tenga en cuenta que si desea optimizar el software, primero debe crear un perfil y ver qué es lo que realmente requiere muchas instrucciones. Es muy poco probable que el problema sean los punteros inteligentes, pero seguro que depende de su implementación.

En C ++, a nadie debería importarle malloc y free , pero existen para el código heredado. Básicamente difieren en el hecho de que no saben nada acerca de las clases de c ++, que con el caso de operador new y delete es diferente.

Utilizo std::unique_ptr y std::shared_ptr en mi proyecto Commander Genius en todas partes y estoy muy feliz de que existan. No tengo que lidiar con fugas de memoria y segfaults desde entonces. Antes de eso, teníamos nuestra propia plantilla de puntero inteligente. Entonces, para el software productivo, no puedo recomendarlos lo suficiente.


Todavía puede usar new y delete si queremos crear nuestro propio mecanismo de asignación de memoria ligera. Por ejemplo

1.Utilizando In-Place new: generalmente se usa para asignar desde memoria preasignada;

char arr[4]; int * intVar = new (&arr) int; // assuming int of size 4 bytes

2.Utilizando asignadores específicos de clase: si queremos un asignador personalizado para nuestras propias clases.

class AwithCustom { public: void * operator new(size_t size) { return malloc(size); } void operator delete(void * ptr) { free(ptr); } };


Un caso de uso válido es tener que interactuar con el código heredado. Especialmente si pasa punteros sin procesar a funciones que se apropian de ellos.

Es posible que no todas las bibliotecas que use estén utilizando punteros inteligentes y para usarlos es posible que necesite proporcionar o aceptar punteros sin procesar y administrar sus vidas de forma manual. Este puede incluso ser el caso dentro de su propia base de código si tiene una larga historia.

Otro caso de uso es tener que interactuar con C, que no tiene punteros inteligentes.


Uno de los problemas con los que trato es extraer estructuras de big data para el diseño de hardware y análisis de lenguaje con unos pocos cientos de millones de elementos. El uso de la memoria y el rendimiento es una consideración.

Los contenedores son una buena forma conveniente de reunir datos rápidamente y trabajar con ellos, pero la implementación utiliza memoria adicional y desreferencias adicionales que afectan tanto a la memoria como al rendimiento. Mi reciente experimento con la sustitución de punteros inteligentes con una implementación personalizada diferente proporcionó un aumento del rendimiento del 20% en un preprocesador Verilog. Hace unos años comparé listas personalizadas y árboles personalizados frente a vectores / mapas y también vi ganancias. Las implementaciones personalizadas se basan en regular nuevo / eliminar.

Por lo tanto, new / delete son útiles en aplicaciones de alta eficiencia para estructuras de datos diseñadas a medida.


Voy a ser contrario, y dejaré constancia de que digo "no" (al menos a la pregunta que estoy seguro de que realmente pretendías hacer, para la mayoría de los casos que se han citado).

Lo que parecen ser casos de uso obvios para usar new y delete (por ejemplo, memoria sin procesar para un montón de GC, almacenamiento para un contenedor) realmente no lo son. Para estos casos, desea almacenamiento "en bruto", no un objeto (o una matriz de objetos, que es lo que proporcionan new y new[] respectivamente).

Como desea almacenamiento sin procesar, realmente necesita / desea utilizar el operator new y el operator delete para administrar el almacenamiento sin procesar en sí. Luego, utiliza la ubicación new para crear objetos en ese almacenamiento sin procesar e invoca directamente al destructor para destruir los objetos. Dependiendo de la situación, es posible que desee utilizar un nivel de indirección para eso, por ejemplo, los contenedores en la biblioteca estándar usan una clase Allocator para manejar estas tareas. Esto se pasa como un parámetro de plantilla, que proporciona un punto de personalización (por ejemplo, una forma de optimizar la asignación en función del patrón de uso típico de un contenedor particular).

Entonces, para estas situaciones, terminas usando la new palabra clave (tanto en la ubicación nueva como en la invocación del operator new ), pero no en algo como T *t = new T[N]; , que es lo que estoy seguro de que querías preguntar.


otro ejemplo que aún no se ha mencionado es cuando necesita pasar un objeto a través de una devolución de llamada C heredada (posiblemente asincrónica). Por lo general, estas cosas toman un puntero de función y un vacío * (o un controlador opaco) para pasar algo de carga útil. Siempre que la devolución de llamada brinde alguna garantía sobre cuándo / cómo / cuántas veces se invocará, recurrir a una nueva y simple-> cast-> callback-> cast-> delete es la solución más sencilla (ok, la eliminación será probablemente administrado por un sitio unique_ptr en devolución de llamada, pero lo nuevo sigue ahí). Por supuesto, existen soluciones alternativas, pero siempre requiere la implementación de algún tipo de "gestor de vida útil del objeto" explícito / implícito en ese caso.