Описание Функции isArray Проверяет
BOOL CheckDriveFreeSpace ( LPCTSTR szDrive)
{
ULARGE_INTEGER ulgAvail;
ULARGE_INTEGER ulgNumBytes;
ULARGE INTEGER ulgFree;
if ( FALSE == GetDiskFreeSpaceEx ( szDrive ,
&ulgAvail ,
&ulgNumBytes ,
&ulgFree ))
{
ASSERT ( FALSE);
return ( FALSE);
}
}
Здесь использован обычный макрос ASSERT, но в нем не специфицировано проверяемое условие. Панель сообщения данного утверждения показывала только, что проверяемое условие имеет значение FALSE, так что от него было мало пользы. При вызове функции утверждения нужно пытаться получать (через панель сообщения) как можно больше информации относительно ее неудачного завершения.
Мой друг Дейв Анджел (Dave Angel) указал мне, что в операторе ASSERT языков С и C++ можно использовать логическую операцию NOT (!), а в качестве ее операнда — строку символов. Эта комбинация позволяет выводить более информативные сообщения, из которых, по крайней мере, можно почерпнуть какую-то идею относительно того, в чем же заключалась ошибка (без просмотра исходного кода). Следующий пример показывает надлежащий способ контроля ложного условия. К сожалению, уловка Дейва Анджела не работает в Visual Basic.
// Надлежащее использование утверждения
BOOL CheckDriveFreeSpace ( LPCTSTR szDrive)
{
ULARGE_INTEGER ulgAvail;
ULARGE_INTEGER ulgNuinBytes;
ULARGE INTEGER ulgFree;
if ( FALSE = GetDiskFreeSpaceEx ( szDrive ,
&ulgAvail ,
&ulgNumBytes,
&ulgFree ))
{
ASSERT ( !"GetDiskFreeSpaceEx failed!");
return ( FALSE);
}
}
Можно усовершенствовать прием Дейва, используя для формирования проверочного условия логическую операцию AND (&&). В следующем примере показано, как можно добавить к тексту обычного ASSERT-сообщения дополнительное уточняющее сообщение:
BOOL AddToDataTree ( PTREENODE pNode)
{
ASSERT ( ( FALSE == IsBadReadPtr
( pNode, sizeof ( TREENODE))) &&
"Invalid parameter!");
.
.
.
}
Что проверяют операторы утверждений
Теперь посмотрим, что необходимо проверять с помощью операторов утверждений. Судя по предыдущим примерам, прежде всего нужно проверять те данные, которые поступают в функцию через аргументы ее вызовов из других программ. Существует опасность, что таким образом в функцию будут направляться некорректные данные (например, данные некорректных для этой функции типов или значений). Выполняя проверки соответствующих параметров, операторы утверждений значительно облегчают процесс отладки и реализуют идею профилактического программирования.
Ниже показан исходный код одной из ключевых функций (stopDebugging) простого отладчика, описанного в главе 4, в которой для контроля параметра использован макрооператор ASSERT. Обратите внимание, что в теле функции сначала выполняется оператор утверждения (ASSERT) и сразу за ним — обработка реальной ошибки. Напомним, что операторы утверждений лишь контролируют правильность параметров, да и то лишь на этапах отладки, и никоим образом не заменяют нормальной обработки ошибок.
BOOL DEBUGINTERFACE_DLLINTERFACE _stdcall
StopDebugging ( LPHANDLE IpDebugSyncEvents)
{
ASSERT ( FALSE ==
IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) * NUM_DEBUGEVENTS));
if ( TRUE == IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) * NUM_DEBUGEVENTS))
{
SetLastError ( ERROR_INVALID_PARAMETER);
return ( FALSE);
}
// Сигнал потоку отладки с именем события его закрытия.
VERIFY ( SetEvent ( IpDebugSyncEvents[ CLOSEDEBUGGER ]));
return ( TRUE);
}
Параметры внутренних private-функций программного модуля не всегда нуждаются в контроле с помощью утверждений. Он необходим только в том случае, если речь идет о внешнем вызове внутренней функции. Более того, если параметр, используемый внешним вызовом, однажды уже прошел через проверку оператором утверждения (на этапе отладки, например), то нет необходимости повторять такие проверки в отлаженном (рабочем) варианте программы.
Это значит, что из отлаженного варианта все операторы утверждений можно спокойно удалить. Однако на этапе отладки иногда полезно использовать сплошные ASSERT-проверки — для всех параметров всех внутренних функций модуля. Возможно, это позволит отловить некоторые внутренние ошибки в модуле.
При выборе параметров для проверок с помощью утверждений полезно придерживаться некоторой средней линии поведения и проверять не все, а лишь наиболее "опасные" для устойчивой работы модуля параметры внутренних вызовов. Правильный отбор таких параметров возможен лишь после приобретения достаточных навыков в разработке программ. Только накопив определенный опыт программирования, вы почувствуете, в каких точках программы можно столкнуться с проблемами, и сможете отобрать внутренние параметры для проверок с помощью утверждений.
Другими объектами ASSERT-контроля являются возвращаемые значения функций. ASSERT-оператор проверяет корректность возвращаемого значения непосредственно перед его возвратом в вызывающую программу. Некоторые разработчики предпочитают проверять с помощью операторов утверждений почти каждое возвращаемое значение. В листинге 3-1 приводится определение функции startDebugging (из отладчика, описанного в главе 4), использующее операторы утверждений для проверки корректности возвращаемых значений. Если в функции вычисляется некорректное значение, то оператор утверждения выводит на экран предупреждающее сообщение.
Листинг 3-1. Примеры ASSERT-проверок возвращаемых значений ;
HANDLE DEBUGINTERFACE_DLLINTERFACE _stdcall
StartDebugging ( LPCTSTR szDebuggee ,
LPCTSTR szCmdLine ,
LPDWORD IpPID ,
CDebugBaseUser * pUserClass ,
LPHANDLE IpDebugSyncEvents )
{
// ASSERT-проверки параметров.
ASSERT ( FALSE == IsBadStringPtr ( szDebuggee, MAX__PATH));
ASSERT ( FALSE == IsBadStringPtr ( szCmdLine, MAX_PATH));
ASSERT ( FALSE == IsBadWritePtr ( IpPID, sizeof ( DWORD)));
ASSERT ( FALSE == IsBadReadPtr ( pUserClass,
sizeof ( CDebugBaseUser *)));
ASSERT' ( FALSE == IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) *
NUM_DEBUGEVENTS));
// Обычные проверки параметров.
if ( ( TRUE == IsBadStringPtr ( szDebuggee, MAX_PATH) ) ||
( TRUE •== IsBadStringPtr ( szCmdLine, MAX_PATH) ) ||
( TRUE — IsBadWritePtr ( IpPID, sizeof ( DWORD) )) ||
( TRUE == IsBadReadPtr ( pUserClass,
sizeof ( CDebugBaseUser *))) ||
( TRUE == IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) *
NUM_DEBUGEVENTS) ) )
{
SetLastError ( ERROR_INVALID_PARAMETER);
return ( INVALID_HANDLE_VALUE);
}
// Обработка начального уведомления о том, что данная
// функция будет ждать, пока не начнет выполняться отладочный поток HANDLE hStartAck;
// Строка, используемая для начального уведомления TCHAR szStartAck [ МАХ_РАТН ];
// Загрузить строку начального уведомления,
if ( 0 == LoadString ( GetDllHandle () ,
IDS_DBGEVENTINIT ,
szStartAck ,
sizeof ( szStartAck) ))
{
ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!");
return ( INVALID_HANDLE_VALUE);
}
// Создать событие начального уведомления.
hStartAck = CreateEvent ( NULL , // Безопасность по умолчанию
TRUE , // Событие ручной переустановки FALSE ,
// Сигнал Initial state = Not szStartAck);
// Имя события ASSERT ( FALSE != hStartAck);
if ( FALSE == hStartAck)
{
TRACE ( "StartDebugging : Unable to create Start Ack event\n");
return ( INVALID_HANDLE_VALUE);
}
// Связать параметры.
THREADPARAMS StParams;
stParams.lpPID = IpPID;
stParams.pUserClass = pUserClass;
stParams.szDebuggee = szDebuggee;
stParams.szCmdLine = szCmdLine ;
// Дескриптор для потока отладки HANDLE hDbgThread;
// Попытка создать поток.
hDbgThread = (HANDLE)_beginthread ( DebugThread, 0, sstParams);
ASSERT ( NULL != hDbgThread);
if ( NULL == hDbgThread)
{
TRACE ( "StartDebugging : _beginthread failed\n");
VERIFY ( CloseHandle ( hStartAck));
return ( INVALID_HANDLE_VALUE);
}
// Ждать, пока поток отладки не стабилизируется, и запустить
::WaitForSingleObject ( hStartAck, INFINITE);
// Освободить дескриптор уведомления.
VERIFY ( CloseHandle ( hStartAck));
// Проверить, выполняется ли еще поток отладки. Если нет,
// отладка, вероятно, не может стартовать.
DWORD dwExitCode = ~STILL_ACTIVE;
if ( FALSE == GetExitCodeThread ( hDbgThread, SdwExitCode))
{
ASSERT ( !"GetExitCodeThread failed!");
return ( INVALID_HANDLE_VALUE);
}
ASSERT ( STILL_ACTIVE = dwExitCode);
if ( STILL_ACTIVE != dwExitCode)
{
TRACE ( "StartDebugging : GetExitCodeThread failedXn");
return ( INVALID_HANDLE_VALUE);
}
// Создать события синхронизации, чтобы главный поток мог
// сообщать циклу отладки, что делать.
BOOL bCreateDbgSyncEvts =
CreateDebugSyncEvents ( IpDebugSyncEvents, *lpPID);
ASSERT ( TRUE = bCreateDbgSyncEvts);
if ( FALSE = bCreateDbgSyncEvts)
{
// Это — серьезная проблема. Есть выполняющийся поток отладки,
//но нет возможности создавать события синхронизации, которые
// нужны потоку пользовательского интерфейса для управления
// потоком отладки. Здесь можно только завершить поток отладки и
// выполнить возврат. Больше сделать ничего нельзя.
TRACE ( "StartDebugging : CreateDebugSyncEvents failedW) ;
VERIFY ( TerminateThread ( hDbgThread, (DWORD)-1));
return ( INVALID_HANDLE_VALUE);
}
return ( hDbgThread);
}
И, наконец, операторы утверждений используются в том случае, когда возникает необходимость проверить некоторое предположение. Например, если в спецификациях функции говорится о том, что она требует 3 Мбайт дискового пространства, то нужно проверить это предположение с помощью оператора утверждения. Другой пример: если функция получает (через аргумент вызова) массив указателей на определенную структуру данных, то необходимо проверить данные этой структуры и подтвердить правильность каждого индивидуального элемента.
В обоих этих случаях, как и при проверке большинства других предположений, нет возможности проверять предположение с помощью обычных функций или макросов. В этих ситуациях следует использовать технику условной компиляции, которая, как указывалось ранее, должна стать частью комплекта инструментов для проверки утверждений. Поскольку код, который выполняется во время условной компиляции, работает на "живых" данных, нужно предпринять дополнительные меры предосторожности, гарантирующие неизменность состояния программы. В программах на Microsoft Visual C++ и Visual Basic я предпочитаю, если возможно, реализовывать эти типы утверждений в виде отдельных функций. Таким способом можно защитить от изменений любые локальные переменные внутри исходной функции. Кроме того, условно компилированные функции утверждений могут свободно использовать окно Watch (об этом мы поговорим в главе 5, где речь пойдет об отладчике Visual C++). Следующий пример показывает условно компилированную функцию-утверждение ValidatePointerArray, которая выполняет глубокие проверки корректности на массивах данных.
#ifdef _DEBUG
void VaiidatePointerArray ( STDATA * pData, int iCount)
{
// Сначала проверить буфер массива.
ASSERT ( FALSE == IsBadReadPtr ( pData,
iCount * sizeof ( STDATA *)));
for ( int i = 0; i < iCount; i++)
{
ASSERT ( pData[ i ].bFlags < DF_HIGHVAL);
ASSERT { FALSE == IsBadStringPtr ( pDataf i ].pszName,
MAX_PATH));
}
}
#endif
void PlotDataltems ( STDATA * pData, int iCount)
#ifdef _DEBUG
VaiidatePointerArray ( pData, iCount);
#endif
}
Макрос VERIFY
Прежде чем двигаться дальше, поговорим о макросе VERIFY, который использовался при разработке библиотеки классов Microsoft Foundation Classes MFC). В отладочных построениях этот макрос ведет себя так же, как обычное утверждение: если условие установлено в 0, то VERIFY открывает панель с предупреждающим сообщением. Однако, в отличие от обычного утверждения, в выпускной конфигурации параметр этого макроса остается в исходном коде и считается нормальной частью программной процедуры.
В сущности, VERIFY можно рассматривать как нормальное утверждение с побочными эффектами, которые сохраняются и в выпускных конфигурациях программы. Строго говоря, в утверждениях любого типа нельзя использовать условия, вызывающие какие-либо побочные эффекты. Все-таки в одной ситуации макрос VERIFY полезен: когда имеется функция, возвращающая ошибочное значение, которое нельзя проверить другим способом. Например, если вызывается функция ResetEvent, чтобы очистить дескриптор свободного события, и вызов терпит неудачу, то мало что можно сделать. Вот почему большинство программистов вызывает ResetEvent и никогда не проверяет возвращаемое значение ни в отладочных, ни в выпускных построениях. Если поместить вызов в макрос VERIFY, то, по крайней мере, можно будет получить уведомление во время отладочных построений, что что-то не так. Конечно, можно достичь тех же результатов, используя ASSERT, но VERIFY избавляет от необходимости создавать новую переменную только для того, чтобы сохранять и проверять возвращаемое значение. Такая переменная, вероятно, использовалась бы только в отладочных построениях.
Многие MFC-программисты, вероятно, применяют макрос VERIFY просто по привычке. Однако в большинстве случаев вместо этого нужно проверять возвращаемое значение. Хорошим примером использования VERIFY является метод cstring: :Loadstring, который загружает строки ресурса. Такой способ хорош в отладочных построениях, потому что если Loadstring завершается неудачно, то макрос VERIFY предупреждает об этом. Однако если сбой Loadstring происходит в выпускном построении, то приходится заканчивать работу с неинициализированной переменной. В лучшем случае здесь можно получить незаполненную строку, но, скорее всего, задача будет завершаться аварийно. Мораль этой истории заключается в том, что возвращаемые значения нужно проверять. Если же вы собираетесь использовать макрос VERIFY, то нужно всегда задаваться вопросом, не приведет ли отказ от проверки возвращаемого значения к каким-нибудь проблемам в выпускном построении?