Pièges de C++

Original: http://www.horstmann.com/cpp/pitfalls.html


Cay Horstmann S.
Département d’informatique
San Jose State University
San Jose, CA 95192-0249
cay\x40horstmann.com
Copyright (C) Cay S. Horstmann 1997

Ce qui est un piège ?

Le code C++ qui
  • compile
  • Liens
  • s’exécute
  • quelque chose de différent que vous attend
Exemple :
             if (-0.5 <= x <= 0.5) return 0;
Piège :
              if (-0.5 <= x <= 0.5) return 0;
Cette expression doesnot tester la condition mathématique
              -1.5 <= x <= 1.5
Au lieu de cela, il calcule d’abord-0.5 < = x, qui est 0 ou 1 et puis compare le résultat avec 0,5.
Moral : même si C++ offre maintenant un type bool, booléens sont toujours librement convertibles int.
Étant donné que bool-> int est autorisé comme une conversion, le compilateur ne peut pas vérifier la validité des expressions. En revanche, le compilateur Java marquait cette déclaration comme une erreur.

Pièges de constructeur

Exemple :

int main()
{  string a(“Hello”);
string b();
string c = string(“World”);
// …
return 0;
}

Piège :
              string b();
Cette expression ne construit pas un objet b de type string. Au lieu de cela, c’est le prototype de fonction ter sans arguments et une chaîne de type de retour.
Moralité : N’oubliez pas d’omettre la () lors de l’appel du constructeur par défaut.
La fonction C de déclarer une fonction dans une portée locale est sans valeur, puisqu’il se trouve sur la portée réelle. La plupart des programmeurs placer tous les prototypes dans les fichiers d’en-tête. Mais même une fonctionnalité inutile que vous n’utilisez jamais pouvez vous hanter.
Exemple :

template<typename T>
class Array
{
public:
Array(int size);
T& operator[](int);
Array<T>& operator=(const Array<T>&);
// …
};

int main()
{  Array<double> a(10);
a[0] = 0; a[1] = 1; a[2] = 4;
a[3] = 9; a[4] = 16;
a[5] = 25; a = 36; a[7] = 49;
a[8] = 64; a[9] = 81;
// …
return 0;
}

Piège :
              a = 36;
Étonnamment, il compile :
              a = Array<double>(36);
un est remplacé par un nouveau tableau des 36 nombres.
Morale : Constructeurs avec un seul argument servent de double tâche conversions de type.
Évitez les constructeurs avec un seul argument entier ! Utilisez le mot clé explicit si vous ne pouvez pas les éviter.

Exemple :

template<typename T>
class Array
{
public:
explicit Array(int size);
// …
private:
T* _data;
int _size;
};

template<typename T>
Array<T>::Array(int size)
:  _size(size),
_data(new T(size))
{}

int main()
{  Array<double> a(10);
a[1] = 64; // program crashes
// …
}

Piège :

template<typename T>
Array<T>::Array(int size)
:  _size(size),
_data(new T(size)) // should have been new T[size]
{}

Pourquoi il n’ai compiler ?
              new T(size)
retourne un pointeur T * à un seul élément de type T, construit à partir de la taille de l’entier.
              new T[size]
Renvoie un T * pointeur sur un tableau d’objets de taille de type T, construit avec le constructeur par défaut.
Moralité : La dualité/pointeur de tableau est muet, mais malheureusement omniprésent en C / C++.
Le compilateur Java aurait rattraper cela–Java, comme la plupart des langages de programmation, prend en charge les types tableau authentique.

Exemple :

template<typename T>
class Array
{
public:
explicit Array(int size);
// …
private:
T* _data;
int _capacity;
int _size;
};

template<typename T>
Array<T>::Array(int size)
:  _size(size),
_capacity(_size + 10),
_data(new T[_capacity])
{}

int main()
{  Array<int> a(100);
. . .
// program starts acting flaky
}

Piège :

Array<T>::Array(int size)
:  _size(size),
_capacity(size + 10),
_data(new T[_capacity])
{}

Initialisation suit l’ordre de déclaration de membre, pas l’ordre de l’initialiseur !

Array<T>::Array(int size)
:  _data(new T[_capacity])
_capacity(_size + 10),
_size(size),

Conseil : N’utilisez pas de membres de données dans les expressions de l’initialiseur.
          Array<T>::Array(int size)
              :  _data(new T[size + 10])
                 _capacity(size + 10), 
                 _size(size), 

Exemple :

class Point
{
public:
Point(double x = 0, double y = 0);
// …
private:
double _x, _y;
};
int main()
{  double a, r, x, y;
// …
Point p = (x + r * cos(a), y + r * sin(a));
// …
return 0;
}

Piège :
              Point p = (x + r * cos(a), y + r * sin(a));
Il devrait s’agir
              Point p(x + r * cos(a), y + r * sin(a));
ou
              Point p = Point(x + r * cos(a), y + r * sin(a));
L’expression
              (x + r * cos(a), y + r * sin(a))
a un sens juridique. L’opérateur virgule ignore x + r * cos(a) et evaluatesy + r * sin(a). Le
              Point(double x = 0, double y = 0)
constructeur fait un Point(y + r * sin(a), 0).
Morale : Arguments par défaut peuvent entraîner des appels involontaires. Dans notre cas, la constructionPoint(double) n’est pas raisonnable, mais la constructionPoint() est. Utilisez uniquement les valeurs par défaut si tous les motifs d’appel qui en résultent sont significatives.

Exemple :

class Shape
{
public:
Shape();
private:
virtual void reset();
Color _color;
};

class Point : public Shape
{
public:
// …
private:
double _x, _y;
};

void Shape::reset() { _color = BLACK; }

void Point::reset()
{  Shape::reset();
_x = 0; _y = 0;
}

Shape::Shape() { reset(); }

Il n’y a noPoint constructeur–nous utilisons la fonction virtuelle dans le constructeur de forme.
Piège :

Shape::Shape() { reset(); }
Point p;

Lorsque constructingPoint, le Shape::reset(), pas la fonction virtual Point:: reset() est appelée. Pourquoi ?
Explication : Les fonctions virtuelles ne fonctionnent pas dans les constructeurs.
TheShape sous-objet construit avant l’objet point. À l’intérieur du constructeur de la forme, l’objet partiellement construit est encore une forme.

Exemple :

class Shape // an abstract class
{
public:
Shape();
private:
void init();
virtual void reset() = 0;
Color _color;
};

Shape::Shape() { init(); }
void Shape::init() { reset(); }

class Point : public Shape // a concrete derived class
{
public:
virtual void reset();
// …
private:
double _x, _y;
};

void Point::reset() { _x = _y = 0; }

Piège :

int main()
{  Point p; // program crashes
return 0;
}

Explication : Vous ne pouvez pas créer une instance d’une classe abstraite (une classe avec une pure, = 0, la fonction virtuelle).
        Shape s; // compile-time error; Shape is abstract
C’est une bonne chose : Si vous le pouviez, que se passerait-il si vous avez appelé
         s.reset(); // reset not defined for shapes
Mais… J’ai menti. Vous pouvez créer des instances des classes abstraites.
Quand construire un béton provenant, pour un instant fugace, la classe de base existe. Si vous appelez une fonction virtuelle pure avant que le constructeur de classe dérivée a exécuté, le programme se termine.

Pièges de destructeur

Exemple :

class Employee
{
public:
Employee(string name);
virtual void print() const;
private:
string _name;
};

class Manager : public Employee
{
public:
Manager(string name, string dept);
virtual void print() const;
private:
string _dept;
};

int main()
{  Employee* staff[10];
staff[0] = new Employee(“Harry Hacker”);
staff[1] = new Manager(“Joe Smith”, “Sales”);
// …
for (int i = 0; i < 10; i++)
staff[i]->print();
for (int i = 0; i < 10; i++)
delete staff[i];
return 0;
}

est la fuite de mémoire ?
Piège :
              delete staff[i];
détruit tous les objets with~Employee(). Chaînes de The_dept des objets gestionnaire ne sont jamais détruits.
Moralité : Une classe d’où vous dérivez doit avoir un destructeur virtuel.

Exemple :

class Employee
{
public:
Employee(string name);
virtual void print() const;
virtual ~Employee(); // <—–
private:
string _name;
};

class Employee
{
public:
Employee(string name);
private:
string _name;
};

class Manager
{
public:
Manager(string name, string sname);
~Manager();
private:
Employee* _secretary;
}

Manager::Manager(string name, string sname)
:  Employee(name),
_secretary(new Employee(sname))
{}

Manager::~Manager() { delete _secretary; }

Quel est le problème avec la classe de gestionnaire ?
Piège :

int main()
{  Manager m1 = Manager(“Sally Smith”,
“Joe Barnes”);
Manager m2 = m1;
// …
}

Les destructeurs de m1 et m2 supprimera le même objet employé.
Moralité : Une classe avec un destructeur a besoin d’un constructeur de copie
              Manager::Manager(const Manager&)
et un opérateur d’assignation
              Manager& Manager::operator=(const Manager&).
Les trois grands : Ce n’est pas seulement une bonne idée–c’est la Loi (Marshall Cline)
Pièges de l’héritage

Exemple :

class Employee
{
public:
Employee(string name, string dept);
virtual void print() const;
string dept() const;
private:
string _name;
string _dept;
};

class Manager : public Employee
{
public:
Manager(string name, string dept);
virtual void print() const;
private:
// …
};

void Employee::print() const
{  cout << _name << endl;
}

void Manager::print() const
{  print(); // print base class
cout << dept() << endl;
}

Piège :

void Manager::print() const
{  print(); // print base class
cout << dept() << endl;
}

Malgré ce que le commentaire says,print() sélectionne l’opération d’impression de classe Traoré. En revanche, dept() sélectionne l’opération de theEmployee classe sinceManager ne pas redéfinir.
Moralité : Lorsque vous appelez une opération de classe de base dans une opération de classe dérivée du même nom, utiliser la résolution de portée :

void Manager::print() const
{  Employee::print(); // print base class
cout << dept() << endl;
}


Exemple :

void Manager::print() const
{  Employee:print(); // print base class
cout << dept() << endl;
}

Piège :
              Employee:print();
Il devrait être
              Employee::print();
Mais pourquoi il compile ? Employé : est une étiquette goto !
Morale : les fonctionnalités de langage même que vous n’utilisez jamais peuvent vous mordre !

Exemple :

class Employee
{
public:
void raise_salary(double by_percent);
// …
};

class Manager : public Employee
{
public:
// …
};

void make_them_happy(Employee* e, int ne)
{  for (int i = 0; i < ne; i++)
e[i].raise_salary(0.10);
}

int main()
{  Employee e[20];
Manager m[5];
m[0] = Manager(“Joe Bush”, “Sales”);
// …
make_them_happy(e, 20);
make_them_happy(m + 1, 4); // let’s skip Joe
return 0;
}

Piège :

void make_them_happy(Employee* e, int ne);
Manager m[5];
make_them_happy(m + 1, 4);

Pourquoi il compile ?
Le type de m + 1 isManager *. À cause de l’héritage, un gestionnaire * peut être converti en un pointeur de classe de base employé *. make_them_happy reçoit l’anEmployee *. Tout le monde est heureux.
Quel est le problème ?
Le tableau calcul e [i] calcule un offset de i*sizeof(Employee).
Morale : Les pointeurs sont surexploitées en C++. Ici, nous voyons deux interprétations du anEmployee * e.
  • e pointe vers anEmployee ou un objet de classe dérivée, comme un gestionnaire.
  • e pointe vers soit anEmployee, soit un tas d’objets ofEmployee, empilés dans un tableau.
Ces deux interprétations sont incompatibles. Mélanger à conduit à des erreurs d’exécution. Toutefois, l’intention du programmeur est cachée au compilateur puisque les deux idées sont exprimées de la même construction–un pointeur.

Exemple :

class Employee
{
public:
Employee(char name[]);
Employee(const Employee& b);
~Employee();
Employee& operator=(const Employee& b);
. . .
private:
char* _name;
};

class Manager : public Employee
{
public:
Manager(char name[], char dept[]);
Manager(const Manager& b);
~Manager();
Manager& operator=(const Manager& b);
. . .
private:
char* _dept;
};

Manager::Manager(const Manager& b)
: _dept(new char[strlen(b._dept) + 1])
{  strcpy(b._dept, _dept);
}

Manager::~Manager()
{  delete[] _dept;
}

Manager& Manager::operator=(const Manager& b)
{  if (this == &b) return *this;
delete[] _dept;
_dept = new char[strlen(b._dept) + 1];
strcpy(b._dept, _dept);
return *this;
}

Piège :

Manager& Manager::operator=(const Manager& b)
{  if (this == &b) return *this;
delete[] _dept;
_dept = new char[strlen(b._dept) + 1];
strcpy(b._dept, _dept);
return *this;
}

Constructeurs et destructeurs appellent automatiquement la base constructeurs et destructeurs. Mais l’opérateur = n’appelle pas automatiquement l’opérateur = de la classe de base.
Moralité : Quand redéfinir l’opérateur = dans une classe dérivée, explicitement appel opérateur = de la 
Grâce à Johann Deneux pour souligner un autre écueil : le constructeur de copie pour Manager ne va pas. Il veut appeler le constructeur par défaut pour l’employé, mais il n’est pas un. Et, bien sûr, il ne serait pas approprié de l’appeler s’il en est. Une version corrigée est

Manager::Manager(const Manager& b)
: Employee(*this), _dept(new char[strlen(b._dept) + 1])
{  strcpy(b._dept, _dept);
}


Pièges de flux

Exemple :

list<int> a;
while (!cin.eof())
{  int x;
cin >> x;
if (!cin.eof()) a.push_back(x);
}

Piège :

while (!cin.eof())
{  // …
}

Cela peut être une boucle infinie. Si l’état de flux tourne à l’échec, la fin du fichier ne sera jamais atteint.
L’état de flux de données aura la valeur échouent si un non-chiffre survient lorsque vous essayez de lire un entier.
Moralité : eof() n’est utile en combinaison avec fail(), pour savoir si les expressions du folklore était la cause de l’échec

Exemple :

while (cin.good())
{  int x;
cin >> x;
if (cin.good()) a.push_back(x);
}

Piège :

cin >> x; // <— may succeed and then encounter EOF
if (cin.good()) a.push_back(x);

Cette codemay manquez pas le dernier élément dans le fichier d’entrée, si elle est directement suivie d’expressions du folklore.

while (!cin.fail())
{  int x;
cin >> x;
if (!cin.fail()) a.push_back(x);
}

Le type conversion basic_ios—> void * est identique à! fail() :

while (cin)
{  int x;
cin >> x;
if (cin) a.push_back(x);
}

Moralité : Il y a quatre flux test functions:good(), bad(), eof(), andfail(). (Moyenne de doesnot note thatbad()! good().) Un seul d’entre eux est utile : fail().

Une surcharge de pièges

Exemple :

class Complex
{
public:
Complex(double = 0, double = 0);
Complex operator+(Complex b) const;
Complex operator-(Complex b) const;
Complex operator*(Complex b) const;
Complex operator/(Complex b) const;
Complex operator^(Complex b) const;
// …
private:
double _re, _im;
};

int main()
{  Complex i(0, 1);
cout << i^2 + 1; // i*i  is -1
return 0;
}

Pourquoi est-ce qu’il ne sera pas imprimé (0,0) ?
Piège :
              cout << i^2 + 1;
En utilisant les règles de priorité d’opérateur C/C++, nous pouvons ajouter des parenthèses :
              cout << (i ^ (2 + 1));
Le ^ opérateur est plus faible que + (mais plus fort que <<).

Moralité : Vous ne pouvez pas modifier la priorité des opérateurs lors de la surcharge des opérateurs. Ne pas surcharger un opérateur si sa priorité n’est pas intuitive pour le domaine de problème.
La priorité de ^ est très bien pour XOR, mais ne pas pour les élever à une puissance.

Exemple : Les classes de flux en charge un type conversion basic_ios—> void * pour tester si un flux est heureux :

while (cin)
{  int x;
cin >> x;
// …
}

Pourquoi convertir void * ? La conversion vers bool semblerait plus judicieux.

template<typename C, typename T = char_traits<C> >
class basic_ios
{
public:
// …
operator bool() const
{  if (fail()) return false;
else return true;
}
private:
// …
};

privé :

while (cin)
{  int x;
cin << x;
// …
}

Notez la typo–il faut cin >> x.
Butcin << x a un bool() meaning:cin.operator involontaire, converti en int et décalé de bits x.
Moralité : Utilisez conversion tovoid *, pas de conversion en int ou bool, pour implémenter des objets donnant des valeurs de vérité. Contrairement aux int ou bool, void * n’ont aucune opération juridique autre que == comparaison.

Exemple : Une classe array avec un operator [] qui pousse le tableau à la demande

class Array
{
public:
Array();
~Array();
Array(const Array&);
Array& operator=(const Array&);
int& operator[](int);
private:
int _n; // current number of elements
int* _a; // points to heap array
};

int& Array::operator[](int i)
{  if (i > _n)
{  int* p = new int[i + 1];
for (int k = 0; k < _n; k++)
p[k] = _a[k];
for (; k < i; k++) p[k] = 0;
delete[] _a;
_a = p;
_n = i;
}
return _a[i];
}

int main()
{  Array a;
for (int s = 1; s <= 100; s++)
a[s] = s * s;
return 0;
}

Piège :

void swap(int& x, int& y)
{  int temp = x;
x = y;
y = temp;
}

int main()
{  Array a;
a[3] = 9;
swap(a[3], a[4]);
return 0;
}

La fonction swap obtient des références à un [3], puis [4], mais le deuxième calcul déplace du tableau et invalide la première référence! un [4] est échangé avec une référence sauvage.
Morale : Vous ne peut pas en même temps déplacer un bloc de mémoire et exporter une référence à celui-ci.
Soit faire [] pas croître le tableau, ou utiliser une structure de données dans lequel les éléments vers jamais (c’est-à-dire une séquence de segments, comme dans std::deque).

Pièges d’exception

Exemple :

void read_stuff(const char filename[])
{  FILE* fp = fopen(filename, “r”);
do_reading(fp);
fclose(fp);
}

Pourquoi estce un « piège d’exception » ? Il n’ya pas toutes les exceptions dans le code !
Piège :

FILE* fp = fopen(filename, “r”);
do_reading(fp);
fclose(fp); // <– may never get here

Si do_reading lève une exception, ou appelle une fonction qui lève une exception, il ne vient jamais revenir! fp n’est jamais fermée.
Morale : Gestion drastique des exceptions modifie le flux de contrôle. Vous ne pouvez pas tenir pour acquis qu’une fonction retourne jamais.
Solution 1: (populaire, mais muets)

void read_stuff(const char filename[])
{  FILE* fp = fopen(filename, “r”);
try
do_reading(fp);
catch(…)
{  fclose(fp);
throw;
}
fclose(fp);
}

Solution 2: (smart)

void read_stuff(const char filename[])
{  fstream fp(filename, ios_base::in);
do_reading(fp);
}

Même ifdo_reading lève une exception, fp est fermée par le destructeur ifstream.
Moralité : Dans le code avec exceptions (c’est-à-dire tous les code de C++ à partir de 1994), renoncer à ressources uniquement dans les destructeurs !

Exemple :

double find_salary_increase(auto_ptr<Employee>);

void do_stuff(const char name[])
{  auto_ptr<Employee> pe = new Employee(name);
// can’t use
// Employee* pe = new Employee(name)
// that’s not not exception safe
double rate = find_salary_increase(pe);
pe->raise_salary(rate);
}

Piège :
            find_salary_increase(pe);
appelle le constructeur de copie d’auto_ptr <Employee>qui transfère la propriété à la copie.
Seul auto_ptr peut posséder un objet de tas. Le propriétaire auto_ptr appelle le destructeur quand il est hors de portée.
Remède : Ne copiez pas une auto_ptr dans une fonction.

double find_salary_increase(Employee*);

void do_stuff(const char name[])
{  Employee* pe = new Employee(name);
auto_ptr<Employee> ape = pe; // use only for destruction
double rate = find_salary_increase(pe);
pe->raise_salary(rate);
}


Pièges de conteneur

Exemple : Un ensemble de pointeurs

set<Employee*> staff;
vector<string> names;
for (int i = 0; i < names.size(); i++)
staff.insert(new Employee(names[i]);

Piège: A ordonné l’utilisation de conteneurs (jeu, carte, multiset, multimap) < pour comparaison. Il est supposé que < est un classement total.
[NOTE : grâce à Liam Devine et Richard Smith d’avoir signalé que cela périmé. Bien sûr, les conteneurs vraiment utilisent moins <T>pour la comparaison et le C++ standard Section 20.3.3.8 déclare: “pour les modèles plus, moins, greater_equal et less_equal, les spécialisations pour n’importe quel type de pointeur donnent un total de la commande, même si les opérateurs intégrés <, >, < =, > = ne sont pas. » Quand j’ai écrit cela, mémoire segmentée modèles étaient encore largement utilisés, et la norme C++ était encore en chantier:-)]
Dans un ensemble de pointeurs
              set<Employee*> staff;
les pointeurs sont comparés avec <.
Étant donné deux arbitraires employé * pointeurs p et q, est p < q défini ? Seulement s’ils pointent vers le même tableau.
Dans un modèle de mémoire segmentée, seuls décalages sont comparés. Ex. p == 0x740A0004 et q == compare 0x7C1B0004 identiques.
Remède: (risqué) seulement écrire du code pour un espace de mémoire plat le pointeur word taille == 

bool employee_ptr_less(const Employee* a, const Employee* b)
{  return a->salary() < b->salary();
}

set<Employee*, bool (*)(const Employee*, const Employee*)>
staff(employee_ptr_less);


Example: Emballement itérateurs

       list<int> a, b;
       // ...

       list<int>::iterator p
             = find(a.begin(), b.end(), 100); 
       if (p != a.end()) b.push_back(*p);

Piège :

             find(a.begin(), b.end(), 100); // oops, should have been a.end()
Pour savoir pourquoi ce code se bloque de façon spectaculaire, examinez la mise en œuvre de trouver :

template<typename I, typename T>
I find(I from, I to, const T& target)
{  while (from != to && *from != target)
++from;
return from;
}

Lors de a.end() atteint, * d’et ++ de ne sont pas définis.
Moralité : Itérateurs ne sais pas leur état. Il n’y a aucune raison pourquoi une liste <int>itérateur ne pouvait pas savoir son état, mais STL a été construit dans le but de faire des itérateurs pas plus puissant que les pointeurs dans un tableau de C. Avantage : Vous pouvez appeler les algorithmes standards avec des tableaux de C :

int a[30];
int* p = find(a, a + 30, 100);

Inconvénient : Programmation avec itérateurs est axée sur le pointeur, et ne pas orienté objet.
Remède : Utilisation sécuritaire STL (http://www.horstmann.com/safestl.html)

Exemple : Itérateurs confus

list<int> a;
list<int> b;
list<int>::iterator p = a.begin();

a.insert(50);
b.insert(100);

b.erase(p);
cout << a.length() << endl; // length is 1
cout << b.length() << endl; // length is 0

Piège : dans
       b.erase(p);
l’itérateur p a fait à l’intérieur un ! Le comportement est indéfini, mais l’implémentation standard de STL effectue les opérations suivantes :
  • * p est effacée de quelle liste il arrive d’être en, juste en suivant vers l’avant et en arrière un lien
  • la longueur de b est décrémentée
Moralité : Itérateurs ne sais pas leur propriétaire.
Remède : Utilisation sécuritaire STL (http://www.horstmann.com/safestl.html)

Comments are closed.