¿Qué trampas de C ++ debo evitar?

Recuerdo haber aprendido sobre los vectores en el STL y después de un tiempo, quería usar un vector de bools para uno de mis proyectos. Después de ver un comportamiento extraño y hacer una investigación, aprendí que un vector de bools no es realmente un vector de bools .

¿Hay otras trampas comunes que evitar en C ++?

Una breve lista podría ser:

  • Evite memory leaks mediante el uso de punteros compartidos para administrar la asignación de memoria y la limpieza
  • Use el modismo de Adquisición de recursos es inicialización (RAII) para administrar la limpieza de recursos, especialmente en presencia de excepciones
  • Evite llamar a funciones virtuales en constructores
  • Utilice técnicas de encoding minimalistas siempre que sea posible; por ejemplo, declare las variables solo cuando sea necesario, las variables de scope y el diseño anticipado siempre que sea posible.
  • Comprenda verdaderamente el manejo de excepciones en su código, tanto con respecto a las excepciones que arroja, como a las que arroja las clases que puede estar usando indirectamente. Esto es especialmente importante en presencia de plantillas.

RAII, punteros compartidos y encoding minimalista no son específicos de C ++, pero ayudan a evitar problemas que surgen con frecuencia cuando se desarrollan en el lenguaje.

Algunos excelentes libros sobre este tema son:

  • Efectivo C ++ – Scott Meyers
  • Más efectivo C ++ – Scott Meyers
  • Normas de encoding C ++ – Sutter y Alexandrescu
  • Preguntas frecuentes sobre C ++ – Cline

Leer estos libros me ha ayudado más que nada para evitar el tipo de escollos por el que está preguntando.

Errores en orden decreciente de su importancia

En primer lugar, debe visitar las galardonadas preguntas frecuentes de C ++ . Tiene muchas buenas respuestas a las trampas. Si tiene más preguntas, visite ##c++ en irc.freenode.org en IRC . Estamos contentos de ayudarlo, si podemos. Tenga en cuenta que todas las siguientes trampas están escritas originalmente. No solo se copian de fonts aleatorias.


delete[] en new , delete en new[]

Solución : Hacer lo anterior cede a un comportamiento indefinido: todo podría suceder. Comprenda su código y lo que hace, y siempre delete[] lo que usted new[] , y delete lo new , entonces eso no sucederá.

Excepción :

 typedef T type[N]; T * pT = new type; delete[] pT; 

Debe delete[] aunque sea new , ya que ha creado una matriz nueva. Entonces, si está trabajando con typedef , tenga especial cuidado.


Llamar a una función virtual en un constructor o destructor

Solución : Llamar a una función virtual no llamará a las funciones principales en las clases derivadas. Llamar a una función virtual pura en un constructor o desctructor es un comportamiento indefinido.


Llamar a delete o delete[] en un puntero ya eliminado

Solución : asigne 0 a cada puntero que elimine. Llamar a delete o delete[] en un puntero nulo no hace nada.


Tomando el tamaño de un puntero, cuando se va a calcular el número de elementos de una ‘matriz’.

Solución : pase la cantidad de elementos junto al puntero cuando necesite pasar una matriz como puntero a una función. Utilice la función que se propone aquí si toma el tamaño de una matriz que se supone que es realmente una matriz.


Usar una matriz como si fuera un puntero. Por lo tanto, usando T ** para una matriz de dos dimensiones.

Solución : vea aquí por qué son diferentes y cómo los maneja.


Escribir en una cadena literal: char * c = "hello"; *c = 'B'; char * c = "hello"; *c = 'B';

Solución : asigne una matriz que se inicializa a partir de los datos de la cadena literal, luego puede escribir en ella:

 char c[] = "hello"; *c = 'B'; 

Escribir en una cadena literal es un comportamiento indefinido. De todos modos, la conversión anterior de una cadena literal a char * está en desuso. Entonces los comstackdores probablemente advertirán si aumenta el nivel de advertencia.


Crear recursos, y luego olvidarte de liberarlos cuando algo ocurra.

Solución : utilice punteros inteligentes como std::unique_ptr o std::shared_ptr como lo señalan otras respuestas.


Modificar un objeto dos veces como en este ejemplo: i = ++i;

Solución : Lo anterior se suponía que le asignaba a i el valor de i+1 . Pero lo que hace no está definido. En lugar de incrementar i y asignar el resultado, también cambia i en el lado derecho. Cambiar un objeto entre dos puntos de secuencia es un comportamiento indefinido. Los puntos de secuencia incluyen || , && , comma-operator , semicolon y semicolon e entering a function (¡lista no exhaustiva!). Cambia el código a lo siguiente para que se comporte correctamente: i = i + 1;


Problemas diversos

Olvidarse de descargar flujos antes de llamar a una función de locking, como sleep .

Solución : purgue la secuencia al transmitir std::endl lugar de \n o llamando a stream.flush(); .


Declarar una función en lugar de una variable.

Solución : el problema surge porque el comstackdor interpreta, por ejemplo,

 Type t(other_type(value)); 

como una statement de función de una función que devuelve Type y que tiene un parámetro de tipo other_type que se llama value . Lo resuelve poniendo paréntesis alrededor del primer argumento. Ahora obtienes una t de tipo variable:

 Type t((other_type(value))); 

Llamar a la función de un objeto libre que solo está declarado en la unidad de traducción actual (archivo .cpp ).

Solución : el estándar no define el orden de creación de objetos libres (en el ámbito del espacio de nombres) definido en diferentes unidades de traducción. Llamar a una función miembro sobre un objeto aún no construido es un comportamiento indefinido. En su lugar, puede definir la siguiente función en la unidad de traducción del objeto y llamarla desde otras:

 House & getTheHouse() { static House h; return h; } 

Eso crearía el objeto a pedido y le dejaría un objeto completamente construido en el momento en que llame funciones.


Definir una plantilla en un archivo .cpp , mientras se usa en un archivo .cpp diferente.

Solución : casi siempre obtendrá errores como undefined reference to ... Coloque todas las definiciones de plantilla en un encabezado, de modo que cuando el comstackdor las esté utilizando, ya pueda producir el código necesario.


static_cast(base); si base es un puntero a una clase base virtual de Derived .

Solución : una clase base virtual es una base que ocurre solo una vez, incluso si es heredada más de una vez por diferentes clases indirectamente en un árbol de herencia. Hacer lo anterior no está permitido por el Estándar. Usa dynamic_cast para hacer eso, y asegúrate de que tu clase base sea polimórfica.


dynamic_cast(ptr_to_base); si la base no es polimórfica

Solución : el estándar no permite un downcast de un puntero o referencia cuando el objeto pasado no es polimórfico. Él o una de sus clases base debe tener una función virtual.


Hacer que su función acepte T const **

Solución : Puede pensar que es más seguro que usar T ** , pero en realidad causará dolor de cabeza a las personas que quieran aprobar T** : el estándar no lo permite. Da un claro ejemplo de por qué no está permitido:

 int main() { char const c = 'c'; char* pc; char const** pcc = &pc; //1: not allowed *pcc = &c; *pc = 'C'; //2: modifies a const object } 

Siempre acepta T const* const*; en lugar.

Otro (cerrado) tema de trampas sobre C ++, por lo que la gente que los busque los encontrará, es Stack Overflow question C ++ trampas .

Algunos deben tener libros en C ++ que lo ayudarán a evitar las trampas comunes de C ++:

Eficaz C ++
Más efectivo C ++
Eficaz STL

El libro Effective STL explica el vector del bools issue 🙂

Brian tiene una excelente lista: agregaría “Siempre marque explícitamente a los constructores de un único argumento (excepto en los raros casos en que desee un casting automático)”.

No es realmente un consejo específico, sino una guía general: revisa tus fonts. C ++ es un lenguaje antiguo, y ha cambiado mucho a lo largo de los años. Las mejores prácticas han cambiado con eso, pero desafortunadamente todavía hay mucha información antigua por ahí. Ha habido algunas muy buenas recomendaciones de libros aquí. Puedo comprar cada uno de los libros de Scott Meyers C ++. Familiarícese con Boost y con los estilos de encoding utilizados en Boost: las personas involucradas en ese proyecto están a la vanguardia del diseño C ++.

No reinventes la rueda. Familiarícese con el STL y Boost, y use sus instalaciones siempre que sea posible para hacer su propio esfuerzo. En particular, use strings y colecciones de AWL a menos que tenga una razón muy, muy buena para no hacerlo. Conozca muy bien auto_ptr y la biblioteca de punteros inteligentes Boost, entienda bajo qué circunstancias se pretende utilizar cada tipo de puntero inteligente y luego use punteros inteligentes en cualquier lugar en el que pueda haber usado punteros sin procesar. Su código será igual de eficiente y mucho menos propenso a memory leaks.

Utilice static_cast, dynamic_cast, const_cast y reinterpret_cast en lugar de versiones en C. A diferencia de los modelos de estilo C, te dejarán saber si realmente estás pidiendo un tipo diferente de elenco de lo que crees que estás pidiendo. Y se destacan viisalmente, alertando al lector de que se está produciendo un yeso.

La página web C ++ Pitfalls de Scott Wheeler cubre algunos de los principales escollos de C ++.

Dos errores que desearía no haber aprendido de la manera difícil:

(1) Una gran cantidad de salida (como printf) está almacenada de forma predeterminada. Si está depurando el código bloqueado y está utilizando declaraciones de depuración almacenadas en búfer, el último resultado que ve puede no ser realmente la última instrucción de impresión encontrada en el código. La solución es eliminar el búfer después de cada impresión de depuración (o desactivar por completo el almacenamiento en búfer).

(2) Tenga cuidado con las inicializaciones: (a) evite instancias de clase como globales / estáticas; y (b) intente inicializar todas sus variables miembro en un valor seguro en un ctor, incluso si es un valor trivial como NULL para los punteros.

Razonamiento: el ordenamiento de la inicialización de objetos global no está garantizado (las variables globales incluyen variables estáticas), por lo que puede terminar con un código que parece no determinante ya que depende del objeto X inicializado antes del objeto Y. Si no inicializa explícitamente un objeto variable de tipo primitivo, como un miembro bool o enum de una clase, terminará con valores diferentes en situaciones sorprendentes; una vez más, el comportamiento puede parecer muy no determinista.

Ya lo mencioné un par de veces, pero los libros de Scott Meyers Effective C ++ y Effective STL realmente valen su peso en oro para ayudar con C ++.

Ahora que lo pienso, el C ++ Gotchas de Steven Dewhurst también es un excelente recurso “desde las trincheras”. Su artículo sobre cómo implementar sus propias excepciones y cómo deberían construirse realmente me ayudó en un solo proyecto.

Usando C ++ como C. Tener un ciclo de crear y lanzar en el código.

En C ++, esta no es una excepción segura y, por lo tanto, la versión no se puede ejecutar. En C ++, usamos RAII para resolver este problema.

Todos los recursos que tienen una creación y liberación manual deben envolverse en un objeto, por lo que estas acciones se realizan en el constructor / destructor.

 // C Code void myFunc() { Plop* plop = createMyPlopResource(); // Use the plop releaseMyPlopResource(plop); } 

En C ++, esto debe ser envuelto en un objeto:

 // C++ class PlopResource { public: PlopResource() { mPlop=createMyPlopResource(); // handle exceptions and errors. } ~PlopResource() { releaseMyPlopResource(mPlop); } private: Plop* mPlop; }; void myFunc() { PlopResource plop; // Use the plop // Exception safe release on exit. } 

El libro C ++ Gotchas puede ser útil.

Aquí hay algunos pozos en los que tuve la desgracia de caer. Todo esto tiene buenas razones que solo entendí después de haber sido mordido por un comportamiento que me sorprendió.

  • virtual funciones virtual en constructores no lo son .

  • No viole la ODR (Regla de una definición) , para eso están los espacios de nombres anónimos (entre otras cosas).

  • El orden de inicialización de los miembros depende del orden en que se declaran.

     class bar { vector vec_; unsigned size_; // Note size_ declared *after* vec_ public: bar(unsigned size) : size_(size) , vec_(size_) // size_ is uninitialized {} }; 
  • Los valores predeterminados y virtual tienen una semántica diferente.

     class base { public: virtual foo(int i = 42) { cout < < "base " << i; } }; class derived : public base { public: virtual foo(int i = 12) { cout << "derived "<< i; } }; derived d; base& b = d; b.foo(); // Outputs `derived 42` 

Las trampas más importantes para los desarrolladores principiantes es evitar la confusión entre C y C ++. C ++ nunca debe tratarse como una mera mejor C o C con las clases, ya que esto reduce su potencia y puede hacerlo incluso peligroso (especialmente cuando se utiliza la memoria como en C).

Echa un vistazo a boost.org . Proporciona mucha funcionalidad adicional, especialmente sus implementaciones de punteros inteligentes.

PRQA tiene un estándar de encoding C ++ excelente y gratuito basado en libros de Scott Meyers, Bjarne Stroustrop y Herb Sutter. Reúne toda esta información en un solo documento.

  1. No leyendo las preguntas frecuentes de C ++ Lite . Explica muchas malas (¡y buenas!) Prácticas.
  2. No usando Boost . Te ahorrarás mucha frustración al aprovechar Boost siempre que sea posible.

Tenga cuidado al usar punteros inteligentes y clases de contenedor.

Evita pseudo clases y cuasi clases … Overdesign básicamente.

Olvidando definir un destructor de clase base virtual. Esto significa que llamar a delete en una base * no terminará destruyendo la parte derivada.

Mantenga los espacios de nombres derechos (incluyendo estructura, clase, espacio de nombres y uso). Esa es mi principal frustración cuando el progtwig simplemente no comstack.

Para desordenar, usa mucho los punteros directos. En su lugar, usa RAII para casi cualquier cosa, asegurándote, por supuesto, de que uses los punteros inteligentes correctos. Si escribe “eliminar” en cualquier lugar fuera de un manipulador o clase de puntero, es muy probable que lo esté haciendo mal.

Lea el libro C ++ Gotchas: Cómo evitar problemas comunes de encoding y diseño .

  • Blizpasta. Esa es una gran cosa que veo mucho …

  • Las variables no inicializadas son un gran error que los alumnos míos hacen. Mucha gente de Java olvida que solo decir “int counter” no establece el contador 0. Como tiene que definir variables en el archivo h (e inicializarlas en el constructor / configuración de un objeto), es fácil olvidarlo.

  • Errores de uno por uno en bucles / acceso a la matriz.

  • No limpiar correctamente el código del objeto cuando comienza el vudú.

  • static_cast en una clase base virtual

En realidad no … Ahora sobre mi concepto erróneo: pensé que A a continuación era una clase base virtual cuando en realidad no lo es; es, de acuerdo con 10.3.1, una clase polimórfica . Usar static_cast aquí parece estar bien.

 struct B { virtual ~B() {} }; struct D : B { }; 

En resumen, sí, este es un peligro peligroso.

Siempre revise un puntero antes de desreferenciarlo. En C, por lo general, podría contar con un locking en el punto en el que desreferencia un puntero malo; en C ++, puede crear una referencia no válida que se bloqueará en un lugar muy alejado del origen del problema.

 class SomeClass { ... void DoSomething() { ++counter; // crash here! } int counter; }; void Foo(SomeClass & ref) { ... ref.DoSomething(); // if DoSomething is virtual, you might crash here ... } void Bar(SomeClass * ptr) { Foo(*ptr); // if ptr is NULL, you have created an invalid reference // which probably WILL NOT crash here } 

Olvidar una & y, por lo tanto, crear una copia en lugar de una referencia.

Esto me pasó dos veces de diferentes maneras:

  • Una instancia estaba en una lista de argumentos, lo que provocó que un objeto grande se pusiera en la stack con el resultado de un desbordamiento de la stack y un locking del sistema integrado.

  • Olvidé la & en una variable de instancia, con el efecto de que el objeto fue copiado. Después de registrarme como oyente en la copia, me pregunté por qué nunca recibí las devoluciones de llamada del objeto original.

Ambos son bastante difíciles de detectar, porque la diferencia es pequeña y difícil de ver, y de otro modo los objetos y las referencias se usan sintácticamente de la misma manera.

La intención es (x == 10) :

 if (x = 10) { //Do something } 

Pensé que nunca cometería este error, pero lo hice recientemente.

El ensayo / artículo Punteros, referencias y valores es muy útil. Habla evitar evitar trampas y buenas prácticas. También puede navegar por todo el sitio, que contiene sugerencias de progtwigción, principalmente para C ++.

Pasé muchos años haciendo desarrollo C ++. Escribí un resumen rápido de los problemas que tuve hace años. Los comstackdores que cumplen con los estándares ya no son un problema, pero sospecho que los otros inconvenientes descritos siguen siendo válidos.

 #include  class A { public: void nuke() { boost::shared_ptr (this); } }; int main(int argc, char** argv) { A a; a.nuke(); return(0); }