it-swarm-es.com

Nunca uso punteros en mi código C ++. ¿Estoy codificando C ++ mal?

Esta pregunta puede sonar extraña para ti, pero estoy aprendiendo C++ solo. No tengo a nadie a quien pueda pedirle asesoramiento y estaría muy contento de recibir algunos consejos.

Recientemente comencé a programar en C++ (aproximadamente 3 a 4 meses intensivos con aproximadamente 6 a 8 horas diarias). Mi experiencia es Java y he realizado algunos proyectos más grandes en Java con más de 10k LOC (que es para un estudiante universitario como yo)).

Utilicé C++ principalmente para la implementación y visualización de algoritmos, pero también apunto a proyectos de software más grandes. Las únicas bibliotecas que utilicé son para Catch2, OpenCV y un poco de Boost. Lo extraño de mi estilo de programación es que nunca he usado punteros en mi viaje; No es que no sepa cómo usar punteros, pero nunca encontré un momento en el que creo que un puntero sería útil. Cuando tengo que almacenar datos primitivos, prefiero std::vector sobre matriz. Cuando necesito usar un objeto de una clase, prefiero crear el objeto en la pila y pasarlo por referencia; sin nuevo/eliminar, sin punteros inteligentes.

La razón por la que hago esta pregunta (extraña) es porque siento que me falta una gran área de programación en C++. ¿Podría compartir conmigo su experiencia y tal vez darme algunos consejos?

57
Long Nguyen

Puede ser una buena idea evitar punteros, utilizando copias de objetos y referencias siempre que sea posible, como lo hace usted. Continúa esta práctica.

Sin embargo, hay un cierto número de cosas que pueden salir muy mal, si no tienes mucho cuidado:

  • Los objetos "en la pila" (la terminología de C++ hoy en día para esto clase de almacenamiento es "auto") deben permanecer válidos cuando utiliza su referencia. Esto generalmente funciona bien si pasa una referencia a una función. Pero devolver una referencia está condenado al fracaso: el objeto será destruido inmediatamente después de return y el uso de la referencia es UB. El mismo tipo de problemas ocurre cuando se inyecta una referencia en un objeto: es una bomba de relojería.
  • No puede usar polimorfismo con contenedores. Entonces, si nunca usa punteros, pero tiene vector de clases con virtual miembros de función, su código podría no funcionar como piensa debido a rebanar . Estos son errores extremadamente desagradables y son un error común cuando son nuevos en C++ con un fondo Java.

También hay algunos patrones de diseño OO) muy comunes que no son posibles sin punteros, como el patrón método de fábrica .

Evitar punteros no debería ser un fin per se. Si está trabajando con la visualización, supongo que el polimorfismo puede ser su amigo. Y aquí los punteros pueden desbloquear la situación. La buena noticia es que los punteros inteligentes hoy en día pueden administrar la memoria de forma segura por usted.

Entonces sí, su práctica puede muy bien funcionar. Pero puede contener algunos errores inadvertidos. Y tarde o temprano perderás características muy útiles.

56
Christophe

Está utilizando "referencia indirecta en el sentido general" en su programación de C++. No hay nada de malo en eso. Es casi tan poderoso como programar con punteros y referencias (en lenguaje C++).


Aunque, hay una dificultad técnica en la forma en que usa vector.

Por razones de seguridad de software, primero describiré esa trampa técnica. Es más importante saber acerca de esa trampa que responder a su pregunta principal.

Cuando los objetos se colocan en un vector, el vector contiene una copia del objeto. Estos objetos tienen direcciones, desde las cuales puede crear referencias (en el sentido de C++). Estas direcciones son estables hasta la próxima vez que haga que el vector se reasigne (crezca, se borre) o que los elementos cambien (por ejemplo, elimine un elemento en el medio). Si sigue utilizando direcciones que ya no son válidas (llamadas invalidación de puntero ), puede desencadenar un "comportamiento indefinido" (UB). Una vez que ocurre UB, puede seguir cualquier cosa: la corrección de la operación posterior ya no es una garantía.


Para permitir de forma segura que se asignen (agreguen) y desasignen (eliminen) elementos individuales en una colección de C++, estos elementos deberán asignarse en el montón.

Aquí, el "montón" se refiere a el montón real , es decir, el sistema de asignación de memoria dinámica en C++. El sistema de asignación de memoria dinámica permite solicitar y renunciar asignaciones individuales.


Su estilo de programación, que le permite preasignar su vectors por adelantado, es comparable al estilo de asignación de memoria estática (en un nivel semántico).

El estilo de asignación de memoria estática era la norma hace varias décadas, y todavía es obligatorio para ciertos sistemas críticos de seguridad, como los sistemas de control electrónico de vehículos para la cadena de potencia y las partes de fluidos, ciertos sistemas de aviónica de aviones y sistemas de armas. Sin embargo, es menos potente que el estilo de asignación de memoria dinámica, ya que el estilo de asignación de memoria estática pone un límite a lo que se puede implementar. Dicho esto, para aplicaciones con un alcance bien definido y no extensible (por ejemplo, controlar un aspecto específico de un sistema mecánico y nada más, no tener que interactuar con nada más), es posible permanecer con un estilo de asignación de memoria estática.


Algunos de los ejemplos que mencionó son ejemplos de "handle body idiom" (enlace) .

En C++, una clase de identificador permite a sus usuarios utilizar normalmente la semántica de copia de C++ para lograr algo similar a los punteros y referencias de C++.

La clase Mat de OpenCV es una clase "handle". Para ilustrar esto, considere este fragmento de código:

cv::Mat matOne(cv::Size(640, 480), CV_8UC3);
cv::Mat matTwo = matOne;

Después de estas dos líneas de código, matTwo y matOne ambas hacen referencia al mismo objeto (la matriz o la imagen). Esto se debe al diseño y al detalle de implementación de cv::Mat clase.

Si desea implementar una clase que se comporte de manera similar, necesitará aprender sobre punteros y referencias de C++, es decir, el conocimiento que le interesa y siente que le falta.


Un poco sobre la "lingüística" de la palabra "semántica".

En C++, las frases "semántica de copia" y "semántica de referencia" se refieren a aspectos de la sintaxis de C++ y su uso. Por lo tanto, el uso de la palabra "semántica" es un nombre inapropiado, cuando se juzga por el estándar del idioma inglés.

19
rwong

Entre las referencias y std :: vec, muchos casos en los que un programa C usaría punteros no usan punteros en C++. Y en Swift, necesitaría profundizar en la biblioteca estándar para encontrar algo que se llama "puntero".

No usar punteros en absoluto es un poco inusual, pero bastante posible.

6
gnasher729

Cuando necesito usar un objeto de una clase, prefiero crear el objeto en la pila y pasarlo por referencia; sin nuevo/eliminar, sin punteros inteligentes.

Me sorprende que nadie haya mencionado, bueno, ya sabes ... ¡stackoverflow! Es raro tener más de 8 MB de pila disponibles (AFAIK el valor predeterminado para Linux). Es seguro asumir que no hay más de 1 MB disponible (AFAIK el valor predeterminado para Windows).

De todos modos, hay dos razones principales para usar el montón en lugar de la pila:

  1. Un objeto necesita sobrevivir a la función que lo creó.
  2. Un objeto es demasiado grande.

También parece que malinterpretas algo: ¡los vectores son trampas! Debido a que un vector asigna, gestiona y desasigna el montón bajo el capó. Es una envoltura muy delgada alrededor del puntero sin procesar y uno puede argumentar que la mayor diferencia es que los vectores también tienen tamaño (s). Entonces es una especie de puntero inteligente, ¿verdad?

5
freakish

Además de la respuesta de Christoph , también hay algunas estructuras de datos que no puede construir y usar fácilmente sin punteros:

  • Arboles de objetos

  • DAG

  • Listas vinculadas (cadenas de delegación, por ejemplo)

Por supuesto, puede evitar el uso de punteros para estas cosas. Los árboles y los DAG se pueden construir a partir de objetos almacenados en un std::vector<>, vinculándolos por sus respectivos índices dentro de ese std::vector<>, y para las listas vinculadas, simplemente puede recurrir a std::list<>. El último usa punteros debajo del capó, el primero reemplaza el puntero explícito con un int que cumple exactamente la misma función y sufre los mismos problemas. Bajo el capó, los punteros no son más que valores enteros que se usan para direccionar algunos bytes en la memoria, por lo que tal uso de índices es realmente tan malo como el uso de void punteros. Utilice mejor los punteros mecanografiados, los inteligentes cuando necesite administrar la propiedad.

Cuánto necesita tales estructuras de datos y, por lo tanto, punteros, depende en gran medida de sus casos de uso. Entonces, tal vez simplemente nunca tuvo un problema que exigía una verdadera cadena de delegación. Por ejemplo, cuando ejecuta una simulación física, simplemente desea algunos conjuntos 3D en los que pueda calcular el siguiente paso de tiempo; eso no implica ningún DAG sofisticado. Sin embargo, si estaba programando un software de control de versiones distribuido, absolutamente debería estar trabajando con punteros, ya que los commits forman un DAG, cada uno de los cuales hace referencia a uno o más commits primarios, commits que hacen referencia a sucursales, etc. pp.

Al final, los punteros no son más que una herramienta. Una herramienta para algunos propósitos específicos. A veces, un problema requiere esta herramienta, y a veces requiere otra. Es igualmente incorrecto no usar punteros en el primer caso, ya que es incorrecto usarlos en el último.

¡El diseñador original de C++, Bjarne Stroustrup, lo aprueba! En sus Pautas principales de CPP , aconseja a todos los programadores de C++ que "prefieran objetos de ámbito, no apilen innecesariamente". Recomienda el uso de RAII para administrar los recursos y dice que las referencias no deben ser propietarias. Si está creando objetos locales en el alcance de la función, en la pila, y pasándolos a otras funciones por referencia, ¡ya está siguiendo las Directrices!

Hay algunas cosas importantes que no puede hacer en C++ sin el uso de punteros. Otros han mencionado varios. Copiar elisión cubre la mayoría de los casos en los que necesitaría devolver un objeto de una función, pero no todos, y no los casos en los que desea transferir la propiedad. Todavía puede obtener muchos de los usos del polimorfismo al pasar un parámetro de función como &Base supportsTheInterface, pero necesita algún tipo de puntero para crear una estructura de datos polimórfica. En la mayoría de los programas grandes, querrás poder compartir datos. Y probablemente se le pedirá que mantenga o interactúe con el código que utiliza punteros. Otro caso de uso que todavía no he visto mencionar a nadie más: los punteros inteligentes son una forma muy útil de mover datos entre objetos de manera más eficiente de lo que podría copiarlos.

4
Davislor

¡Apuesto a que estás usando this punteros implícitamente! ;-)

En serio, C++ es un gran lenguaje, y la mayoría de los programas no lo usan todo. Sean Parent dio una charla algo famosa al sugerir que los programadores de C++ deberían nunca escribir bucles sin formato y en su lugar deberían aprender a usar los algoritmos proporcionados por las bibliotecas estándar. Para muchos, eso puede parecer más extremo que no usar punteros.

C++ tiene diferentes herramientas para diferentes trabajos. Debería usar herramientas como la meta programación de plantillas, herencia, lambdas solo cuando lo necesite, no solo porque pueda. No use un puntero cuando no lo necesite.

Hay muchas veces que las personas usan punteros cuando no lo necesitan, especialmente los punteros de metal desnudo. Dejar bibliotecas como <vector> maneja los punteros sin procesar y la asignación dinámica es simplemente inteligente. Tenga en cuenta que, en cierto sentido, todavía está utilizando punteros, solo confía en el código escrito por otros programadores de C++ para administrar los detalles de bajo nivel para que pueda concentrarse en un nivel más alto de abstracción.

Los punteros son naturales y apropiados para ciertos tipos de soluciones, como vincular nodos en un gráfico. En algunas soluciones, son esenciales, como la asignación dinámica de objetos polimórficos. En la mayoría de los casos, aún puede evitar los detalles de bajo nivel confiando en punteros inteligentes, que, como std::vector son solo clases que administran detalles en su nombre.

En algún momento, es posible que tenga que escribir su propia clase de abstracción: un contenedor especializado o un nuevo tipo de puntero inteligente. Incluso Sean Parent reconoció que podría necesitar un bucle sin procesar para implementar un nuevo algoritmo, pero que debería encapsularlo.

Cuando llegue al punto en que necesite crear una abstracción dependiente del puntero (o cuando tenga que comprender el código o la interfaz de otra persona con alguna biblioteca que quiera intercambiar punteros), querrá haber tenido suficiente experiencia con punteros que Estás equipado para la tarea.

Los punteros (a veces en forma de dirección, identificador o referencia) son conceptos bastante fundamentales para muchos lenguajes de programación. No pierdas la oportunidad de aprender sobre ellos al contorsionar tu código para evitarlos cuando sean la herramienta adecuada.

2
Adrian McCarthy

Cuando tengo que almacenar datos primitivos, prefiero std::vector Sobre la matriz. Cuando necesito usar un objeto de una clase, prefiero crear el objeto en la pila y pasarlo por referencia; no new/delete, no hay punteros inteligentes.

La mayoría de los desarrolladores de C++ estarían de acuerdo con usted en todos estos puntos. Los contenedores adecuados son más seguros que las matrices primitivas, y si puede evitar asignar algo en el montón, debería hacerlo.

Sin embargo, hay algunos casos en los que necesita punteros. Si puede evitarlos en su código, entonces eso es genial, pero se vuelven más difíciles de evitar en proyectos más grandes.

Extrusión de alcance

Este es un nombre elegante para un concepto bastante simple. El alcance de una función es el cuerpo de esa función junto con todos los cuerpos de funciones que llama, los cuerpos de funciones que ellos llaman, y así sucesivamente. Si declaro una variable local en una función, entonces sé que permanecerá viva hasta que salga esa función. Así que puedo pasarlo libremente (por referencia) a cualquier otra función dentro de esa función, y saber que estoy haciendo algo seguro.

Pero, ¿qué sucede si quiero crear un objeto dentro de mi función, almacenarlo en algún lugar y que persista después de que la función haya salido? Si estuviéramos escribiendo Java, no lo pensaríamos dos veces antes de escribir algo como esto (por ejemplo, en una clase de fábrica).

public BigObject createBigObject()
{
  BigObject bigObject = new BigObject(param1, param2);
  return bigObject;
}

Incluso después de que createBigObject() se haya ejecutado, queremos que la variable bigObject siga viva. Como Java es un lenguaje recolectado de basura, esto está bien.

En C++, si intentamos escribir

BigObject& createBigObject() const
{
  BigObject bigObject(param1, param2);
  return bigObject;
}

entonces el comportamiento del programa es indefinido. En el mejor de los casos, su compilador le advertirá que está devolviendo una referencia a una variable local. Si intenta ejecutar el código, es posible que funcione algunas veces y se bloquee en otras, ya que la vida útil de bigObject finaliza cuando createBigObject regresa. Debe usar la asignación dinámica, ya sea con new o con un puntero inteligente. P.ej.:

BigObject* createBigObject() const
{
  BigObject* const bigObject = new BigObject(param1, param2);
  return bigObject;
}

Si lo desea, puede return *bigObject En su lugar y devolver un BigObject& Aquí.

Referencias mutables

Las referencias en C++ son inmutables : deben inicializarse para referirse a otro objeto cuando se crean y no pueden apuntar hacia otro objeto. En ese sentido, un T& Es un poco como un T* const (Y un T const& Es un poco como un T const* const). Ahora suponga que está configurando una clase y desea mantener una referencia a algún otro objeto como una variable miembro en esa clase. Si el otro objeto está configurado cuando construye la clase, entonces está bien, puede inicializar la referencia de inmediato, pero si tiene que configurar las cosas más adelante en un paso de inicialización, deberá usar un puntero para que puede apuntarlo al objeto solo una vez que se ha creado.

Polimorfismo (más o menos)

Si ha programado en Java, probablemente haya utilizado mucho interface s. En C++, si desea hacer lo mismo, normalmente necesita usar punteros (más o menos, a veces quiere usar plantillas en su lugar). En Java podemos escribir

List<String> list = new ArrayList<String>();

para indicar que solo nos importa que list sea algún tipo de lista; el hecho de que sea un ArrayList es un detalle de implementación. En C++, escribir

List<std::string> list = ArrayList<std::string>();

(suponiendo que List y ArrayList existieran como tipos en su código) intentaría construir un List y luego establecerlo igual a ArrayList, que sería un error de compilación si List tenía miembros virtuales puros, ya que sería imposible crear objetos List. list tendría que ser una referencia List& O un puntero List*.

Ahora a veces puede usar referencias para hacer polimorfismo (como podría hacerlo en este ejemplo), pero en tales casos, generalmente es mejor hacer polimorfismo en tiempo de compilación usando plantillas. En C++ usualmente usamos el polimorfismo de tiempo de ejecución cuando queremos almacenar los objetos polimórficos en algún contenedor (por ejemplo, un std::vector). Y esto vuelve al punto anterior: es imposible almacenar una referencia sobre la marcha, ya que las referencias deben inicializarse cuando se crean. Entonces tenemos que almacenarlos como punteros.

1
John Gowers

Los punteros son un mecanismo de programación de bajo nivel. El "punto completo" de la programacióncita necesaria es hacer mecanismos de nivel superior a partir de mecanismos de nivel inferior, donde, en general, nivel inferior significa más poderoso pero más difícil de usar, y nivel superior significa menos poderoso pero más fácil de usar.

Entonces, tal vez, en general, los punteros tienen más sentido en el código de nivel inferior, y menos sentido en el código de nivel superior.

0
Dave Cousineau

No, no estás haciendo nada malo. Punteros (no referencias, no contadores de referencia, no envoltorios únicos) son un concepto que se utiliza para modelar la ubicación de un valor en un espacio de direcciones de memoria dado. Entonces:

  • Si el dominio de su programa está estrechamente relacionado con la forma en que se presentan las cosas en la memoria (por ejemplo, si está escribiendo un asignador de memoria o algo así), los punteros aparecerán tarde o temprano.
  • De lo contrario (y este es el escenario más probable) es perfectamente normal que no aparezcan punteros.
    • A menos que esté utilizando un lenguaje que no tiene otra forma de expresar una referencia, como C. En ese caso, aparecerán punteros independientemente de su dominio. Pero esta es la razón por la cual las personas generalmente no usan C para programas que no operan en ese dominio.

Definir mal. Si su código se ejecuta correctamente, entonces, en un sentido simplista, no está mal.

Sin embargo, al no utilizar punteros en absoluto, está creando una serie de problemas potenciales para usted.

1) Si nunca usa punteros, nunca los comprenderá bien. Entonces, ¿qué harás cuando tengas que entender o interactuar con el código escrito por alguien que usa punteros?

2) Los punteros son (en mi humilde opinión: las opiniones individuales pueden variar) a menudo son mucho más fáciles de entender y codificar que las alternativas que está utilizando.

3) Los punteros a menudo (aunque, por supuesto, no siempre) pueden ser más eficientes que algunas de esas alternativas, por lo que si su objetivo es escribir código crítico para el rendimiento, se ha perjudicado a sí mismo.

0
jamesqf