| 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
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<class
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;
}
| Pitfall: a = 36; surpreendentemente é compilado como |
a = Array<double>(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;
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 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;
// ...
}
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);
List<int> 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);
}
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;
}
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);
}
| 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 | | |
|
|
|
|
|