Question

Je me souviens d'avoir appris pour la première fois les vecteurs dans la STL et après un certain temps, j'ai voulu utiliser un vecteur de bools pour l'un de mes projets.Après avoir constaté un comportement étrange et fait quelques recherches, j'ai appris que un vecteur de booléens n'est pas vraiment un vecteur de booléens.

Existe-t-il d’autres pièges courants à éviter en C++ ?

Était-ce utile?

La solution

Une courte liste pourrait être :

  • Évitez les fuites de mémoire en utilisant des pointeurs partagés pour gérer l'allocation de mémoire et le nettoyage
  • Utilisez le L'acquisition de ressources est une initialisation (RAII) idiome pour gérer le nettoyage des ressources - en particulier en présence d'exceptions
  • Évitez d'appeler des fonctions virtuelles dans les constructeurs
  • Utilisez des techniques de codage minimalistes lorsque cela est possible - par exemple, en déclarant les variables uniquement lorsque cela est nécessaire, en définissant la portée des variables et en adoptant une conception précoce lorsque cela est possible.
  • Comprenez vraiment la gestion des exceptions dans votre code - à la fois en ce qui concerne les exceptions que vous lancez, ainsi que celles lancées par les classes que vous utilisez peut-être indirectement.Ceci est particulièrement important en présence de modèles.

RAII, pointeurs partagés et codage minimaliste ne sont bien sûr pas spécifiques au C++, mais ils permettent d'éviter les problèmes qui surviennent fréquemment lors du développement dans le langage.

Voici quelques excellents livres sur ce sujet :

  • C++ efficace - Scott Meyers
  • C++ plus efficace - Scott Meyers
  • Normes de codage C++ - Sutter & Alexandrescu
  • FAQ C++ - Cline

La lecture de ces livres m'a aidé plus que toute autre chose à éviter le genre d'écueils dont vous parlez.

Autres conseils

Pièges par ordre décroissant d’importance

Tout d'abord, vous devriez visiter le musée primé FAQ C++.Il contient de nombreuses bonnes réponses aux pièges.Si vous avez d'autres questions, visitez ##c++ sur irc.freenode.org dans IRC.Nous sommes heureux de vous aider, si nous le pouvons.Notez que tous les pièges suivants sont écrits à l’origine.Ils ne sont pas simplement copiés à partir de sources aléatoires.


delete[] sur new, delete sur new[]

Solution:Faire ce qui précède donne lieu à un comportement indéfini :Tout pourrait arriver.Comprenez votre code et ce qu'il fait, et toujours delete[] ce que tu new[], et delete ce que tu new, alors cela n'arrivera pas.

Exception:

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

Vous devez delete[] même si tu new, puisque vous avez créé un nouveau tableau.Donc si vous travaillez avec typedef, faites particulièrement attention.


Appel d'une fonction virtuelle dans un constructeur ou un destructeur

Solution:L'appel d'une fonction virtuelle n'appellera pas les fonctions de substitution dans les classes dérivées.Appeler un fonction virtuelle pure dans un constructeur ou un destructeur est un comportement indéfini.


Appel delete ou delete[] sur un pointeur déjà supprimé

Solution:Attribuez 0 à chaque pointeur que vous supprimez.Appel delete ou delete[] sur un pointeur nul ne fait rien.


Prendre la taille d'un pointeur, lorsque le nombre d'éléments d'un « tableau » doit être calculé.

Solution:Transmettez le nombre d'éléments à côté du pointeur lorsque vous devez transmettre un tableau comme pointeur dans une fonction.Utiliser la fonction proposée ici si vous prenez la taille d'un tableau qui est censé être vraiment un tableau.


Utiliser un tableau comme s'il s'agissait d'un pointeur.Ainsi, en utilisant T ** pour un tableau à deux dimensions.

Solution:Voir ici pourquoi ils sont différents et comment vous les gérez.


Écrire dans une chaîne littérale : char * c = "hello"; *c = 'B';

Solution:Allouez un tableau initialisé à partir des données de la chaîne littérale, vous pourrez ensuite y écrire :

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

Écrire dans une chaîne littérale est un comportement indéfini.Quoi qu'il en soit, la conversion ci-dessus d'une chaîne littérale en char * est obsolète.Les compilateurs vous avertiront donc probablement si vous augmentez le niveau d'avertissement.


Créer des ressources, puis oublier de les libérer quand quelque chose se lance.

Solution:Utilisez des pointeurs intelligents comme std::unique_ptr ou std::shared_ptr comme l'ont souligné d'autres réponses.


Modifier un objet deux fois comme dans cet exemple : i = ++i;

Solution:Ce qui précède était censé attribuer à i la valeur de i+1.Mais ce qu’il fait n’est pas défini.Au lieu d'augmenter i et en attribuant le résultat, ça change i du côté droit également.Changer un objet entre deux points de séquence est un comportement indéfini.Les points de séquence incluent ||, &&, comma-operator, semicolon et entering a function (liste non exhaustive !).Modifiez le code comme suit pour qu'il se comporte correctement : i = i + 1;


Problèmes divers

Oublier de vider les flux avant d'appeler une fonction de blocage comme sleep.

Solution:Videz le flux en diffusant soit std::endl au lieu de \n ou en appelant stream.flush();.


Déclarer une fonction au lieu d'une variable.

Solution:Le problème se pose car le compilateur interprète par exemple

Type t(other_type(value));

comme déclaration de fonction d'une fonction t revenir Type et ayant un paramètre de type other_type qui est appelée value.Vous le résolvez en mettant des parenthèses autour du premier argument.Maintenant vous obtenez une variable t de type Type:

Type t((other_type(value)));

Appel de la fonction d'un objet libre déclaré uniquement dans l'unité de traduction courante (.cpp déposer).

Solution:La norme ne définit pas l'ordre de création des objets libres (au niveau de l'espace de noms) défini dans les différentes unités de traduction.L’appel d’une fonction membre sur un objet non encore construit est un comportement indéfini.Vous pouvez plutôt définir la fonction suivante dans l'unité de traduction de l'objet et l'appeler depuis d'autres :

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

Cela créerait l'objet à la demande et vous laisserait un objet entièrement construit au moment où vous appelleriez des fonctions dessus.


Définir un modèle dans un .cpp fichier, alors qu'il est utilisé dans un autre .cpp déposer.

Solution:Vous obtiendrez presque toujours des erreurs telles que undefined reference to ....Placez toutes les définitions de modèles dans un en-tête, de sorte que lorsque le compilateur les utilise, il puisse déjà produire le code nécessaire.


static_cast<Derived*>(base); si base est un pointeur vers une classe de base virtuelle de Derived.

Solution:Une classe de base virtuelle est une base qui n'apparaît qu'une seule fois, même si elle est héritée plusieurs fois par différentes classes indirectement dans un arbre d'héritage.Faire ce qui précède n’est pas autorisé par la norme.Utilisez Dynamic_cast pour ce faire et assurez-vous que votre classe de base est polymorphe.


dynamic_cast<Derived*>(ptr_to_base); si la base est non polymorphe

Solution:La norme n'autorise pas le downcast d'un pointeur ou d'une référence lorsque l'objet transmis n'est pas polymorphe.Lui ou l'une de ses classes de base doit avoir une fonction virtuelle.


Faire accepter votre fonction T const **

Solution:Vous pourriez penser que c'est plus sûr que d'utiliser T **, mais en réalité, cela causera des maux de tête aux personnes qui veulent passer T**:La norme ne le permet pas.Il donne un bon exemple de la raison pour laquelle il est interdit :

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

Acceptez toujours T const* const*; plutôt.

Un autre fil de discussion (fermé) sur les pièges du C++, pour que les personnes qui les recherchent les trouvent, est la question Stack Overflow Les pièges du C++.

Certains doivent avoir des livres C++ qui vous aideront à éviter les pièges courants du C++ :

C++ efficace
C++ plus efficace
STL efficace

Le livre Effective STL explique le problème du vecteur des booléens :)

Brian a une excellente liste :J'ajouterais "Toujours marquer les constructeurs à argument unique comme explicites (sauf dans les rares cas où vous souhaitez une conversion automatique)."

Pas vraiment un conseil spécifique, mais une ligne directrice générale :vérifiez vos sources.Le C++ est un langage ancien et il a beaucoup changé au fil des ans.Les meilleures pratiques ont évolué avec cela, mais malheureusement, il existe encore beaucoup d'informations anciennes.Il y a eu de très bonnes recommandations de livres ici - je peux acheter chacun des livres C++ de Scott Meyers.Familiarisez-vous avec Boost et avec les styles de codage utilisés dans Boost : les personnes impliquées dans ce projet sont à la pointe de la conception C++.

Ne réinventez pas la roue.Familiarisez-vous avec le STL et le Boost et utilisez leurs installations autant que possible en roulant les vôtres.En particulier, utilisez des chaînes et des collections STL, sauf si vous avez une très, très bonne raison de ne pas le faire.Apprenez à connaître auto_ptr et la bibliothèque de pointeurs intelligents Boost, comprenez dans quelles circonstances chaque type de pointeur intelligent est destiné à être utilisé, puis utilisez des pointeurs intelligents partout où vous auriez pu utiliser des pointeurs bruts.Votre code sera tout aussi efficace et beaucoup moins sujet aux fuites de mémoire.

Utilisez static_cast, Dynamic_cast, const_cast et reinterpret_cast au lieu des conversions de style C.Contrairement aux moulages de style C, ils vous permettront de savoir si vous demandez réellement un type de moulage différent de celui que vous pensez demander.Et ils se démarquent visuellement, alertant le lecteur qu'un casting est en cours.

La page internet Pièges du C++ par Scott Wheeler couvre certains des principaux pièges du C++.

Deux pièges que j'aurais aimé ne pas avoir appris à mes dépens :

(1) De nombreuses sorties (telles que printf) sont mises en mémoire tampon par défaut.Si vous déboguez du code qui plante et que vous utilisez des instructions de débogage mises en mémoire tampon, la dernière sortie que vous voyez peut pas être vraiment la dernière instruction d'impression rencontrée dans le code.La solution consiste à vider le tampon après chaque impression de débogage (ou à désactiver complètement la mise en mémoire tampon).

(2) Soyez prudent avec les initialisations - (a) évitez les instances de classe en tant que globales/statiques ;et (b) essayez d'initialiser toutes vos variables membres à une valeur sûre dans un ctor, même s'il s'agit d'une valeur triviale telle que NULL pour les pointeurs.

Raisonnement:l'ordre d'initialisation des objets globaux n'est pas garanti (les globaux incluent des variables statiques), vous pouvez donc vous retrouver avec du code qui semble échouer de manière non déterministe car il dépend de l'initialisation de l'objet X avant l'objet Y.Si vous n'initialisez pas explicitement une variable de type primitif, telle qu'un membre bool ou une énumération d'une classe, vous vous retrouverez avec des valeurs différentes dans des situations surprenantes -- encore une fois, le comportement peut sembler très non déterministe.

Je l'ai déjà mentionné à plusieurs reprises, mais les livres de Scott Meyers C++ efficace et STL efficace valent vraiment leur pesant d'or pour leur aide en C++.

À bien y penser, celui de Steven Dewhurst Les pièges du C++ est également une excellente ressource « des tranchées ».Son article sur le lancement de vos propres exceptions et la manière dont elles doivent être construites m'a vraiment aidé dans un projet.

Utiliser C++ comme C.Avoir un cycle de création et de publication dans le code.

En C++, ce n'est pas une exception et la version ne peut donc pas être exécutée.En C++, on utilise RAII pour résoudre ce problème.

Toutes les ressources qui ont une création et une libération manuelles doivent être encapsulées dans un objet afin que ces actions soient effectuées dans le constructeur/destructeur.

// C Code
void myFunc()
{
    Plop*   plop = createMyPlopResource();

    // Use the plop

    releaseMyPlopResource(plop);
}

En C++, cela doit être enveloppé dans un objet :

// 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.
}

Le livre Les pièges du C++ peut s'avérer utile.

Voici quelques écueils dans lesquels j'ai eu le malheur de tomber.Tout cela a de bonnes raisons que je n’ai compris qu’après avoir été mordu par un comportement qui m’a surpris.

  • virtual fonctions dans les constructeurs ne sont pas.

  • Ne violez pas le ODR (règle d'une définition), c'est à cela que servent les espaces de noms anonymes (entre autres choses).

  • L'ordre d'initialisation des membres dépend de l'ordre dans lequel ils sont déclarés.

    class bar {
        vector<int> vec_;
        unsigned size_; // Note size_ declared *after* vec_
    public:
        bar(unsigned size)
            : size_(size)
            , vec_(size_) // size_ is uninitialized
            {}
    };
    
  • Valeurs par défaut et virtual ont une sémantique différente.

    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`
    

Le piège le plus important pour les développeurs débutants est d’éviter toute confusion entre C et C++.Le C++ ne doit jamais être traité comme un simple meilleur C ou C avec classes car cela réduit sa puissance et peut le rendre encore plus dangereux (surtout lors de l'utilisation de la mémoire comme en C).

Vérifier boost.org.Il fournit de nombreuses fonctionnalités supplémentaires, en particulier leurs implémentations de pointeurs intelligents.

PRQA a une norme de codage C++ excellente et gratuite basé sur les livres de Scott Meyers, Bjarne Stroustrop et Herb Sutter.Il rassemble toutes ces informations dans un seul document.

  1. Ne pas lire le FAQ C++ légère.Cela explique de nombreuses mauvaises (et bonnes !) pratiques.
  2. N'utilise pas Booster.Vous vous épargnerez bien des frustrations en profitant de Boost lorsque cela est possible.

Soyez prudent lorsque vous utilisez des pointeurs intelligents et des classes conteneurs.

Éviter pseudo-classes et quasi-classes...Surconception en gros.

Oublier de définir un destructeur de classe de base virtuel.Cela signifie qu'appeler delete sur une Base* ne finira pas par détruire la pièce dérivée.

Gardez les espaces de noms droits (y compris la structure, la classe, l'espace de noms et l'utilisation).C'est ma principale frustration lorsque le programme ne se compile tout simplement pas.

Pour faire des erreurs, utilisez beaucoup les pointeurs droits.Au lieu de cela, utilisez RAII pour presque tout, en vous assurant bien sûr que vous utilisez les bons pointeurs intelligents.Si vous écrivez « supprimer » n'importe où en dehors d'une classe de type handle ou pointeur, vous vous trompez très probablement.

  • Blizpasta.C'est énorme, je vois beaucoup...

  • Les variables non initialisées sont une énorme erreur que commettent mes étudiants.Beaucoup de gens Java oublient que le simple fait de dire "int counter" ne met pas le compteur à 0.Puisque vous devez définir des variables dans le fichier h (et les initialiser dans le constructeur/configuration d'un objet), c'est facile à oublier.

  • Erreurs ponctuelles activées for accès aux boucles/tableaux.

  • Ne nettoie pas correctement le code objet au démarrage du vaudou.

  • static_cast abattu sur une classe de base virtuelle

Pas vraiment...Maintenant à propos de mon idée fausse :Je pensais que A ce qui suit était une classe de base virtuelle alors qu'en fait ce n'est pas le cas ;c'est, selon 10.3.1, un classe polymorphe.En utilisant static_cast ici ça semble aller.

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

struct D : B { };

En résumé, oui, c’est un écueil dangereux.

Vérifiez toujours un pointeur avant de le déréférencer.En C, vous pouvez généralement compter sur un crash au moment où vous déréférencez un mauvais pointeur ;en C++, vous pouvez créer une référence invalide qui plantera à un endroit très éloigné de la source du problème.

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
}

Oublier un & et créant ainsi une copie au lieu d'une référence.

Cela m'est arrivé deux fois de différentes manières :

  • Une instance se trouvait dans une liste d'arguments, ce qui provoquait le placement d'un objet volumineux sur la pile, entraînant un débordement de pile et un crash du système embarqué.

  • j'ai oublié le & sur une variable d'instance, avec pour effet que l'objet a été copié.Après m'être inscrit en tant qu'auditeur de la copie, je me suis demandé pourquoi je n'avais jamais reçu les rappels de l'objet d'origine.

Les deux sont plutôt difficiles à repérer, car la différence est petite et difficile à voir, et sinon les objets et les références sont utilisés syntaxiquement de la même manière.

L'intention est (x == 10):

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

Je pensais que je ne ferais jamais cette erreur moi-même, mais je l'ai fait récemment.

L'essai/l'article Pointeurs, références et valeurs est très utile.Il parle d’éviter les pièges et les bonnes pratiques.Vous pouvez également parcourir l'ensemble du site, qui contient des conseils de programmation, principalement pour le C++.

J'ai passé de nombreuses années à faire du développement C++.j'ai écrit un résumé rapide des problèmes que j'ai eu avec ça il y a des années.Les compilateurs conformes aux normes ne posent plus vraiment de problème, mais je soupçonne que les autres pièges évoqués sont toujours valables.

#include <boost/shared_ptr.hpp>
class A {
public:
  void nuke() {
     boost::shared_ptr<A> (this);
  }
};

int main(int argc, char** argv) {
  A a;
  a.nuke();
  return(0);
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top