Home
C/C++
Pitfalls
 
Pitfalls

O que é um pitfall
Construtores
Destrutores
Herança
Stream
Sobrecarga
Exceções

O que é um pitfall

Neste contexto, é um código em C++ que compila, linkedita e executa, mas se comporta de modo diferente do que você espera.

Exemplo:

if (-0.5 <= x <= 0.5) return 0;

Pitfall: A expressão

if (-0.5 <= x <= 0.5) return 0;

não testa a condição matemática

-1.5 <= x <= 1.5

Ao contrário, primeiro ela computa -0.5 <= x que resulta em 0 ou 1, e então compara o resultado com 0.5

Moral: C++ não tem um tipo de dado genuinamente booleano. Dados booleanos em C++ são, na verdade, inteiros que podem ter valor 0 ou 1. O compilador não consegue verificar a validade de expressões como a do exemplo. Só conseguiria se um tipo de dado booleano fizesse parte da linguagem.

Nota: O tipo de dado booleano está para ser adicionado ao ANSI C++

Construtores

Exemplo:

              class String
              { 
              public:
                 String(const char s[] = "");
                 // ...
              };
              int main()
              {   String a("Hello");
                 String b();
                 String c = String("World");
                 // ...
                 return 0;
              } 

Pitfall: String b(); não constrói um objeto b do tipo String. É, na realidade, o protótipo de uma função b, que não tem argumentos, e que retorna um dado do tipo String.

Moral: Lembre-se de omitir o() quando invocar o construtor default.

O recurso do C que permite declarar uma função em um escopo local é inócuo neste caso, porque equivale a mentir sobre o verdadeiro escopo. A maioria dos programadores coloca todos os protótipos em arquivos-header, mas mesmo um recurso inócuo, que você nunca usa, pode surpreendê-lo.

Exemplo:

              template&ltclass T>              class Array
              {
              public:
                 Array(int size);
                 T& operator[](int);
                 Array&ltT>& operator=(const Array&ltT>&);
                 // ...
              };
              int main()
              {   Array&ltdouble> 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;
              } 

Pitfall: a = 36; surpreendentemente é compilado como 

a = Array&ltdouble>(36);

a é substituido por um novo array de 36 números.

Moral: Construtores com um único argumento incorrem em dupla conversão de tipo. Evite construtores com um único argumento inteiro.

Exemplo:

              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;
              } 

Pitfall:

Point p = (x + r * cos(a), y + r * sin(a));

deveria ser algo como

Point p(x + r * cons(a), y + r * sin(a));

ou

Point p = Point p(x + r * cons(a), y + r * sin(a));

A expressão

(x + r * cos(a), y + r * sin(a))

tem um significado especial. O operador , (vírgula) descarta x + r * cos(a) e avalia y + r * sin(a)

Assim, para

Point (double x = 0, double y = 0);

o construtor faz

Point (y + r * sin(a), 0)

Moral: Argumentos default podem resultar em calls não intencionais. No caso presente, a construção Point (double) não é aceitável, mas a construção Point () é plenamente aceitável. Use argumentos default apenas nos casos em que todos os calls padrões resultantes forem significativos para o problema.

Exemplo:

            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(); }

Aqui não há um construtor Point. Usamos então uma função virtual no construtor  Shape.

Pitfall:

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

Quando constrói Shape, a função Shape::reset() é chamada, ao invés da função virtual Point::reset(). Porque?

Moral: Funções virtuais não funcionam em construtores. O sub-objeto Shape é construido antes do objeto Point. Dentro do construtor Shape, o objeto construido é um objeto Shape.

Destrutores

Exemplo

              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;
              }

Onde está o erro em liberação de memória?

Pitfall: delete staff[i]; destrói todos os objetos através de
~Employee(). O string _dept do objeto Manager nunca é destruido.

Moral: Uma classe da qual outras derivam deve ter um destrutor virtual.

Exemplo:

              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; }

O que há de errado com a classe Manager?

Pitfall:

             int main()
              {   Manager m1 = Manager("Sally Smith",
                    "Joe Barnes");
                 Manager m2 = m1;
                 // ...
              }

Os destrutores de m1 e de m2 vão excluir o mesmo objeto Employee.

Moral: Uma classe com um destrutor precisa de um destrutor de cópia: Manager::Manager(const Manager&) e de um operador de atribuição Manager& Manager::operator=(const Manager&).

Herança

Exemplo:

              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;
              } 

Pitfall:

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

Independentemente do que diga o comentário, print(), seleciona a operação print da classe Manager. Por outro lado, dept() seleciona a operação da classe Employee, já que a classe Manager não a redefine.

Moral: Quando chamar uma operação da classe base, que tem o mesmo nome na classe derivada, use notação para especificação de escopo (scope resolution):

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

Exemplo:

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

Pitfall: Employee:print(); deveria ser Employee::print(); Porque Employee:print(); compila normalmente? Porque neste caso  Employee é tratado como um label para goto.

Moral: Mesmo aqueles recursos da linguagem que você nunca usa podem surpreendê-lo.

Exemplo:

              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;
              }

Pitfall:

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

Porque este código compila normalmente? O tipo de m + 1 é Manager*. Devido a herança, um Manager* é conversível para um pointer para a classe base  Employee*. Assim, make_them_happy recebe um Employee*. Todo mundo fica feliz.

Então qual é o problema? A notação e[i] computa um deslocamento de i*sizeof(employee).

Moral: Pointer são intensivamente usados em C++. Aqui nós temos duas interpretações para Employee* e

  • e aponta tanto para um Employee, quanto para um objeto de uma classe derivada, como Manager
  • e aponta tanto para um Employee, quanto para um conjunto de objetos Employee agrupados em um array

Essas duas interpretações são incompatíveis. Misturá-las pode levar a erros que se manifestam em tempo de execução do programa. Pior ainda, a intenção do programador não é percebida pelo compilador, já que ambas as idéias são expressas pela mesma construção: um pointer.

Stream

Exemplo:

List&ltint> a;
while (!cin.eof())
{  int x;
   cin >> x;
   a.append(x);
}

Pitfall:

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

Essa construção pode resultar em um loop infinito. Se o estado do stream tornar-se fail, o final do arquivo nunca será encontrado. O estado do stream se tornará fail se um não-dígito for encontrado quando se tenta ler um inteiro.

Exemplo

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

A conversão de tipo ios ----> void* é identica a !fail():

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

Pitfall:

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

Essa construção pode perder o último elemento do arquivo de entrada, se este for imediatamento seguido por um EOF.

Moral: Há quatro funções para teste de streams: good(), bad(), eof() e fail(). Note que bad() não significa o mesmo que !good(). Apenas uma função de teste de streams é realmente útil: fail().

Sobrecarga

Exemplo:

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;
}

Porque essa construção não imprime (0.0)?

Pitfall:

cout << i^2 + 1;

Usando as regras de precedência de operadores do C++, nós podemos adicionar parênteses à construção

cout << (i ^ (2 + 1));

O operador ^ é mais fraco que + mas é mais forte que <<

Moral: Você não pode alterar a precedência dos operadores quando usa sobrecarga de operadores. Não sobrecarregue um operador se a sua precência intrínseca não for intuitiva para o domínio do problema de que se está tratanto. A precedência de ^ é boa para XOR (ou exclusivo) mas não é boa para potenciação.

As classes de stream suportam a conversão de tipo ios ----> void* para testar se um stream é bom. 

while (cin)
{  int x;
    cin >> x;
    // ...

Porque converter para void* ? Uma conversão para int/bool faria mais sentido.

Exemplo:

            class ios
              { 
              public:
                 // ...
                 operator int() const;
              private:
                 // ...
              };
              ios::operator int() const
              {  if (fail()) return FALSE;
                 else return TRUE;
              } 

Pitfall:

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

Repare a construção para entrada de dados. Deveria ser cin >> x. Mas cin << x tem um significado não intencional: cin.operator int() deslocado de x bits

Moral: Use conversão para void*, não conversão para int ou bool, para implementar objetos que tenham utilidade real. Ao contrário de int, void* não tem qualquer operação que não seja a de comparação.

Exemplo:

              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];
                    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;
              }

Pitfall: 

             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;
              } 

A função swap obtém referências para a[3] e para a[4], mas a segunda execução move o array e invalida a primeira referência! a[4] é então processado por swap com uma referência absurda.

Moral: Você não pode, simultaneamente, mover blocos de memória e exportar suas referências. Impeça que [] aumente o array, ou use uma estrutura de dados na qual os elementos nunca se movem.

Exceções

Exemplo:

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

Porque há um problema nessa construção? Porque essa construção nào contém nenhum tratamento de exceção.  

Pitfall:

FILE* fp = fopen(filename, "r");
do_reading(fp);
fclose(fp);

pode não ser executado inteiramente. Se do_reading(fp) evoluir para uma exceção, ou chamar uma função que trata de exceções, o controle pode não mais retornar a esse código, e fp não será fechado, porque fclose(fp) não será executado.

Moral: O manejo de exceções altera drásticamente o controle do fluxo de execução do programa.Você não pode presumir que as funções de tratamento de exceções sempre retornam ao seu código.

Solução 1 (Popular e simplória):

int read_stuff(const char filename[])
{  FILE* fp = fopen(filename, "r");
   try
      do_reading(fp);
      catch(...)
      {  fclose(fp);
         throw;
      }
   fclose(fp);

Solução 2 (Inteligente):

int read_stuff(const char filename[])
{  ifstream fp(filename, ios::in);
   do_reading(fp);

Mesmo que do_reading evolua para uma exceção, fp será fechado pelo destrutor de ifstream.

Moral: Em códigos com tratamento de exceção - ou seja, em todos os compiladores C++ implementados a partir de 1994 - libere recursos apenas dentro dos destrutores.

Do original C++ Pitfalls, by Cay S. Horstmann, Department of Mathematics and Computer Science San Jose State University San Jose, CA 95192-0103, cay@horstmann.com
Tradução: Dagoberto Haele Arnaut

| Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site |