Шрифт:
Интервал:
Закладка:
void do_resources2(int a, int b, const char* s) // менее плохой код
{
ifstream is(s); // открываем файл
vector<int>v1(a); // создаем вектор (выделяем память)
if (b<=0) throw Bad_arg(); // может генерировать исключение
vector<int> v2(b); // создаем другой вектор (выделяем память)
}


FILE* do_resources3(int a, int* p, const char* s) // плохая функция
// неправильная передача ресурса
{
FILE* f = fopen(s,"r");
delete p;
delete var;
var = new int[27];
return f;
}
Правильно ли, что функция do_resources3() передает (предположительно) открытый файл обратно как возвращаемое значение? Правильно ли, что функция do_resources3() освобождает память, передаваемую ей как аргумент p? Мы также добавили действительно коварный вариант использования глобальной переменной var (очевидно, указатель). В принципе передача ресурсов в функцию и из нее является довольно распространенной и полезной практикой, но для того чтобы понять, корректно ли выполняется эта операция, необходимо знать стратегию управления ресурсами. Кто владеет ресурсом? Кто должен его удалять/освобождать? Документация должна ясно и четко отвечать на эти вопросы. (Помечтайте.) В любом случае передача ресурсов изобилует возможностями для ошибок и представляет сложность для тестирования.

26.3.3.3. Циклы

Большинство ошибок возникает в конце циклов.
• Правильно ли проинициализированы переменные в начале цикла?
• Правильно ли заканчивается цикл (часто на последнем элементе)?
Приведем пример, который содержит ошибку.
int do_loop(const vector<int>& v) // плохая функция
// неправильный цикл
{
int i;
int sum;
while(i<=vec.size()) sum+=v[i];
return sum;
}
Здесь содержатся три очевидные ошибки. (Какие именно?) Кроме того, хороший тестировщик немедленно выявит возможности для переполнения при добавлении чисел к переменной sum.

Широко известная и особенно опасная ошибка, связанная с циклами и заключающаяся в переполнении буфера, относится к категории ошибок, которые можно перехватить, систематически задавая два ключевых вопроса о циклах.
char buf[MAX]; // буфер фиксированного объема
char* read_line() // опасная функция
{
int i = 0;
char ch;
while(cin.get(ch) && ch!='n') buf[i++] = ch;
buf[i+1] = 0;
return buf;
}
Разумеется, вы не написали бы ничего подобного! (А почему нет? Что плохого в функции read_line()?) Однако эта ошибка, к сожалению, является довольно распространенной и имеет разные варианты.
// опасный фрагмент
gets(buf); // считываем строку в переменную buf
scanf("%s",buf); // считываем строку в переменную buf

26.3.3.4. Ветвление

• Все ли возможные варианты предусмотрены?
• Правильные ли действия связаны с правильными вариантами выбора?
Рассмотрим следующую бессмысленную функцию:
void do_branch1(int x, int y) // плохая функция
// неправильное использование инструкции if
{
if (x<0) {
if (y<0)
cout << "Большое отрицательное число n";
else
cout << "Отрицательное число n";
}
else if (x>0) {
if (y<0)
cout << "Большое положительное число n";
else
cout << "Положительное число n";
}
}
Наиболее очевидная ошибка в этом фрагменте заключается в том, что мы забыли о варианте, в котором переменная x равна нулю. Сравнивая числа (положительные или отрицательные) с нулем, программисты часто забывают о нем или приписывают неправильной ветви (например, относят его к отрицательным числам). Кроме того, существует более тонкая (хотя и распространенная) ошибка, скрытая в этом фрагменте: действия при условиях (x>0 && y<0) и (x>0 && y>=0) каким-то образом поменялись местами. Это часто случается, когда программисты пользуются командами “копировать и вставить”.
Чем более сложными являются варианты использования инструкций if, тем вероятнее становятся ошибки. Тестировщики анализируют такие коды и стараются не пропустить ни одной ветви. Для функции do_branch1() набор тестов очевиден.
do_branch1(–1,–1);
do_branch1(–1, 1);
do_branch1(1,–1);
do_branch1(1,1);
do_branch1(–1,0);
do_branch1(0,–1);
do_branch1(1,0);
do_branch1(0,1);
do_branch1(0,0);
По существу, это наивный подход “перебора всех альтернатив”, которой мы применили, заметив, что функция do_branch1() сравнивает значения с нулем с помощью операторов < и >. Для того чтобы выявить неправильные действия при положительных значениях переменной x, мы должны объединить вызовы функции с желаемыми результатами.
Обработка инструкций switch аналогична обработке инструкций if.
void do_branch1(int x, int y) // плохая функция
// неправильное