Мы люди, а все люди так или иначе однажды делают ошибки. Кто-то больше, кто-то меньше. В мире программирования ломать программу своими дерзкими действиями может как пользователь, использующий её, норовя запихать в данные — сущности неподходящих типов: то в целое число строку запихнёт, то возьмёт и что-нибудь на ноль поделит, — так и сами программисты своими неправильными вычислениями. Вот чтобы программист мог немного обезопасить себя от собственной погрешности, существует специальный макрос assert, действие которого такое же, как у if, но синтаксис немного другой.
Чтобы использовать assert, необходимо подключить заголовочный файл cassert или assert.h, если компилятор древний.
C++
1
2
3
4
5
6
7
8
//Листинг #1 компилятор g++ Пример кода с assert
#include <cassert> //Для использования assert
intmain(){
inta=10;
assert(a=10);//используем assert
}
Сам по себе листинг #1 не имеет смысла, с его помощью только отображено, как в коде пишется assert. Обозначает показанное действе, что после того, как a задана, программист обещает компилятору, что a всегда равно 10. Чтобы понять пользу от assert, нужно посмотреть на ситуацию, когда программист мог совершить ошибку в своих расчётах. Это важно: программист в своих расчётах может совершить ошибку, и чтобы немного обезопасить себя вставляет в такие места, где могут быть просчёты, макрос assert. Возьмём один не практический, а академический пример, очень простой для демонстрации этого.
Новичок пишет код, в котором генерирует числа для заполнения ими массива. Ему не нужно генерировать ноль, потому что дальше на эти числа будут делиться другое число или другие числа. Этот новичок плохо понимает диапазоны и часто ошибается на единичку (довольно распространённая ошибка у новичков выходить за пределы массива на одно значение, например). Он откуда-то знает про assert и активно его использует. Пишет вот такой код:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Листинг #2 компилятор g++ использование assert
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <cassert>
intmain()
{
srand(time(0));
intx;
constintN=10;
intarr[N];
for(inti=0;i<N;i++){
x=rand()%10;
assert(x!=0);//Программист сообщил компилятору, что после вычисления (x = rand()%10); x не будет 0
arr[i]=x;
}
}
Эта программа может несколько раз сработать как ни в чём не бывало, но поскольку в расчёте нашего новичка допущена ошибка, то или сразу, или, в худшем случае, спустя несколько запусков assert скажет своё резкое слово. В зависимости от среды разработки, языка, подключенных библиотек и определений тело assert может работать по-разному. Например, в приложении PC на языке C++ может вызываться исключение (throw/raise excepton), на языке C может происходить останов выполнения программы с выходом (abort, exit). Во встраиваемых приложениях (микроконтроллеры) обычной практикой реализации assert может быть простое бесконечное зацикливание, иногда это реализуется с выводом подходящего сообщения в последовательный порт (отладочную консоль, UART). Поскольку эту тему читают новички, то у вас скорее всего программа просто упадёт с характерным сообщением: {Assertion failed: выражение, имя_файла, номер_строки_файла}. Если программа всё-таки успеет упасть до того, как наш новичок пойдёт хвастаться своим друзьям готовым рабочим файлом, то наш новичок скорее всего сможет оперативно определить, где находится источник ошибки и исправить собственный просчёт.
Поскольку эта часть расчётов входит в обязанности нашего новичка-программиста, то целесообразно для проверок использовать не if, а assert. Если бы использовался if, то код бы засорился избыточной проверкой. А assert можно отключить при окончательной сборке программы с помощью отключающего этот макрос макроса NDEBUG.
Макрос NDEBUG надо прописывать до места прописанного включения assert
assert — это избыточные проверки, которые все внутри одной программы можно отключить одним движением: использованием макроса NDEBUG. Это намного удобнее, чем выкидывать избыточные if.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Листинг #3 компилятор g++ Отключение assert
#include <cstdlib>
#include <ctime>
#include <iostream>
#define NDEBUG //Отключаем assert, писать до подключения!
#include <cassert> //Включаем assert, но на этот раз он не включится, потому что ему сказано не включаться
intmain()
{
srand(time(0));
intx;
constintN=10;
intarr[N];
for(inti=0;i<N;i++){
x=rand()%10;
assert(x!=0);//Программист сообщил компилятору, что после вычисления (x = rand()%10); x не будет 0
arr[i]=x;
}
}
Макрос assert разработан для отлова ошибок программирования, а не для отлова ошибок пользователя или ошибок реального времени выполнения, поскольку весь этот механизм полностью запрещается после выхода программы из фазы отладки.
Показанные мной примеры, возможно, хороши для подачи к усвоению темы, но не отражают случаи из реального мира. В близкой к реальной ситуации может быть пример проверки невыхода за границы массива. Поскольку я упрощаю примеры, то реальный и не показываю, моделирую просто чем-то отдалённо напоминающий пример из реальных программ. Учитывайте это.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//Листинг #4.1 компилятор g++ assert и диапазон массива
#include <iostream>
#include <assert.h> /* assert */
usingstd::ifstream;
usingstd::cout;
/*Упрощённая структура имитирует класс-массив*/
structMyArr{
staticconstintsize=50;//фиксированное число элементов в массиве
intarr[size];
/*Функция для вывода элемента по индексу*/
intprint(inti){
returnarr[i];//Возвращаем вытаскиваемое из массива значение
};
/*Функция заполняет массив значениями, чтобы сейчас не было пустышки*/
voidfill(){
for(inti=0;i<size;i++){
arr[i]=i+1;
}
}
};
intmain()
{
MyArr ma;//Объект нашей структуры
ma.fill();//Заполняем значениями
for(inti=0;i<100;i++){//В массиве внутри структуры только 50 элементов, мы выходим за границу
cout<<ma.print(i)<<'\n';//Выводим на экран данные
}
}
В листинге #4.1 произошёл выход за пределы массива и на экран пошли мусорные значения. Мы ушли далеко за разрешённую границу. Иногда так бывает, что мусорные значения очень похожи на настоящие и участвуют в вычислениях, понять, что они мусорные, сходу не получается. Коли расчёты входят непосредственно в сферу действий программиста, написавшего этот код, то проверки с помощью if избыточны. Но проверки с помощью assert хоть и избыточны, но могут быть отключены одним движением руки. Кроме того, у нас есть условие, которое точно должно выполняться, в совокупности с расчётами программиста этот фактор также способствует использованию assert для обозначения соглашения между компилятором и программистом о том, что программист суёт именно такие данные, какие показывает, а программа не работает с данными незадекларированными этим соглашением. Своеобразный симбиоз получается. Программист, чтобы обезопасить себя, делает соглашение:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//Листинг #4.2 компилятор g++ assert и граница массива
//Листинг #6.2 компилятор g++ assert и диапазон массива
#include <iostream>
#include <assert.h> /* assert */
usingstd::ifstream;
usingstd::cout;
/*Упрощённая структура имитирует класс-массив*/
structMyArr{
staticconstintsize=50;
intarr[size];
/*Функция для вывода элемента по индексу*/
intprint(inti){
assert(i<size);//Добавили обещание, что i не окажется больше, чем size
returnarr[i];//Возвращаем вытаскиваемое из массива значение
};
voidfill(){
for(inti=0;i<size;i++){
arr[i]=i+1;
}
}
};
intmain()
{
MyArr ma;
ma.fill();
for(inti=0;i<100;i++){
cout<<ma.print(i)<<'\n';
}
}
Таким образом довольно удобно облегчать себе отладку программы. Можете посмотреть ещё на 2 примера, я надеюсь, что что к чему вам на этот момент уже стало понятно.
Одна из очень распространённых ошибок новичков связана с файлами: пытаются открыть для обработки файл, которого нет, что может быть, например, по причине банальной опечатки. Если название открываемого файла определяется самим программистом, а пользователь никак на него повлиять не сможет, то опять же можно использовать assert для индикации факта открытия файла:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Листинг #5 компилятор g++ assert и файлы
#include <fstream>
#include <assert.h> /* assert */
usingstd::ifstream;
intmain()
{
ifstreamf1("testfilefile.txt");
assert(f1);//Обещание, что файл f1 существует
f1.close();
return0;
}
Поскольку я задал такое имя, файл с которым вы скорее всего у себя не встретите, assert должен дать о себе знать.
Один из классических примеров связан с использованием указателей. Очень часто проверяют, что указатель не направлен на нулевой или на какой другой адрес.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Листинг #6 компилятор g++ assert и указатели
#include <iostream>
#include <assert.h> /* assert */
voidprint_number(int*myInt){
assert(myInt!=NULL);//обещание, что входящий указатель не направлен на NULL
cout<<*myInt;
}
intmain()
{
inta=10;
int*b=NULL;
int*c=NULL;
b=&a;
print_number(b);
print_number(c);//с == NULL, нарушение обещания!
return0;
}
Если в листинге #4 направить указатель c на любой адрес с нулевого, то ошибка будет исправлена. А в таком виде нарушается данное внутри вызываемой функции обещание, и assert сразу даёт об этом знать.
Конечно, assert помогает вылавливать ошибку в расчётах, но надо учитывать и то обстоятельство, что ошибка может затаиться и выскочить очень не скоро. Для демонстрации использую очень простой для понимания этого момента пример.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Листинг #7 assert таящаяся ошибка
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <cassert>
intmain()
{
srand(time(0));
intx=rand()%1000;
assert(x!=0);//Программист сообщил компилятору, что после вычисления x = rand()%10; x не будет 0
}
Сколько запусков пройдёт, пока ошибка проявится, не знает никто. Хоть этот пример и притянут за уши, он предназначен только для демонстрации самой возможности подобного развития событий.
Конечно же это не все возможные ситуации использования assert. Существует много других случаев. Но всё сводится к тому, что программист обещает компилятору, что в даваемых им компилятору расчётах будут определённые данные или наоборот — не будет их.
assert — это не средство борьбы с ошибками, а средство понижения сложности отладки и увеличения самотестируемости кода.
assert используют программисты для контроля самих себя.
assert — это соглашение между программистом и компилятором, что какое-то условие всегда выполняется.
С помощью assert происходит фильтрация и разделение проверок, нужных в ходе работы программы, и нужных самому программисту.
assert отключают, когда считается, что программа готова.
assert не исключает неудачу в ловле "скользкой" ошибки.
Иногда информации, даваемой assert по умолчанию, недостаточно для оперативного реагирования на исправление просчёта. В этом случае может помочь дополнительное информирование в случае срабатывания.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Листинг #8 assert своё информирование
#include <iostream>
#include <assert.h> /* assert */
usingstd::ifstream;
usingstd::cout;
voidfoo1(intx){
assert(("foo1",x!=0));//Выводим assert со своим сообщением
}
voidfoo2(intx){
assert(x!=0&& "foo2");//Выводим assert со своим сообщением
}
intmain()
{
// foo1(0); //Вызов любой из двух функций спровоцирует работу assert,
// foo2(0); //потому что внутри каждой из них заявлено, что входящее значение не будет нулевым
}
В этом коде показано два способа информирования. Поскольку мы можем не знать, в каком порядке будут вызваны функции, а в каждой функции одноподобное соглашение, нам не хватит информации быстро понять в какой из них произошла проблема, поэтому использование дополнительного информационного текста очень даже приходится кстати здесь. Я использовал разные способы, только чтобы показать их оба в компактном виде.
Добавить комментарий