- Введение
- Примеры ошибок
- О других ошибках в Firefox
- Вернемся к ошибкам в обработчиках ошибок
- Первая рекомендация
- Вторая рекомендация
- Третья рекомендация
- Четвертая рекомендация
- Заключение
Это уже четвертая заметка, где я хочу поделиться полезными наблюдениями о паттернах ошибок и том, как можно с ними бороться. В этот раз я затрону такую тему, как обработка редких и аварийных ситуаций в программах. Рассматривая множество программ, я пришел к выводу, что код обработки ошибок в Си/Си++ программах — одно из самых ненадежных мест.
К чему приводят такие дефекты? Программа вместо того, чтобы выдать сообщение «файл X не найден», падает и заставляет пользователя гадать, что он не так делает. Программа для работы с базой данных выводит невразумительное сообщение вместо того, чтобы сообщить, что неверно заполнено одно из полей. Попробуем сразиться с этой разновидностью ошибок, которые досаждают нашим пользователям.
Введение
Вначале информация для читателей, которые не знакомы с моими предыдущими заметками. Их можно найти здесь:
- Заметка N1 [Miranda IM];
- Заметка N2 [Chromium, Return to Castle Wolfenstein и т.д.];
- Заметка N3 [Qt SDK].
Как всегда, я буду не абстрактен, а начну с примеров. В этот раз примеры будут взяты из исходного кода Firefox. Я постараюсь продемонстрировать, что даже в качественном и известном приложении с кодом для обработки ошибок, всё обстоит не самым лучшим образом. Дефекты были найдены мной с помощью анализатора PVS-Studio 4.50.
Примеры ошибок
Пример N1. Неполноценная проверка целостности таблицы
int AffixMgr::parse_convtable(..., const char * keyword)
{
...
if (strncmp(piece, keyword, sizeof(keyword)) != 0) {
HUNSPELL_WARNING(stderr,
"error: line %d: table is corruptn",
af->getlinenum());
delete *rl;
*rl = NULL;
return 1;
}
...
}
Диагностика PVS-Studio: V579 The strncmp function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. affixmgr.cpp 3708
Здесь сделана попытка проверить целостность таблицы. К сожалению, эта проверка может сработать, а может и не сработать. Для вычисления длины ключевого слова используют оператор sizeof(), что естественно некорректно. В результате, работоспособность кода зависит от счастливого стечения обстоятельств (длины ключевого слова, размера указателя ‘keyword’ в текущей модели данных).
Пример 2. Неработающая проверка при чтении файла
int PatchFile::LoadSourceFile(FILE* ofile)
{
...
size_t c = fread(rb, 1, r, ofile);
if (c < 0) {
LOG(("LoadSourceFile: "
"error reading destination file: " LOG_S "n",
mFile));
return READ_ERROR;
}
...
}
Диагностика PVS-Studio: V547 Expression ‘c < 0’ is always false. Unsigned type value is never < 0. updater.cpp 1179
Это пример, когда код обработки ошибки пишется по принципу «чтобы было». Программист даже не задумался, что собственно он написал, и как это будет работать. Проверка некорректна. Функция fread() возвращает количество прочитанных байт с помощью беззнакового типа. Прототип функции:
size_t fread(
void *buffer,
size_t size,
size_t count,
FILE *stream
);
Естественно, для хранения результата используется переменная ‘c’, имеющая тип size_t. Как следствие, результат проверки (c < 0) всегда ложен.
Это хороший пример. На первый взгляд кажется, что есть какая-то проверка, но на практике выясняется, что она совершенно бесполезна.
Аналогичную ошибку можно увидеть и в других местах:
V547 Expression ‘c < 0’ is always false. Unsigned type value is never < 0. updater.cpp 2373
V547 Expression ‘c < 0’ is always false. Unsigned type value is never < 0. bspatch.cpp 107
Пример 3. Проверка указателя на NULL уже после его использования
nsresult
nsFrameSelection::MoveCaret(...)
{
...
mShell->FlushPendingNotifications(Flush_Layout);
if (!mShell) {
return NS_OK;
}
...
}
Диагностика PVS-Studio: V595 The ‘mShell’ pointer was utilized before it was verified against nullptr. Check lines: 1107, 1109. nsselection.cpp 1107
Если указатель равен нулю, то мы должны обработать этот специальный случай и вернуть из функции NS_OK. Смущает то, что указатель mShell до этого момента уже используется.
Скорее всего, этот код работает, так как указатель mShell всегда неравен NULL. Пример я привожу, чтобы показать, что можно допустить ошибку даже в очень простых проверках. Проверка есть, а смысла от неё нет.
Пример 4. Проверка указателя на NULL уже после его использования
CompileStatus
mjit::Compiler::performCompilation(JITScript **jitp)
{
...
JaegerSpew(JSpew_Scripts,
"successfully compiled (code "%p") (size "%u")n",
(*jitp)->code.m_code.executableAddress(),
unsigned((*jitp)->code.m_size));
if (!*jitp)
return Compile_Abort;
...
}
Диагностика PVS-Studio:V595 The ‘* jitp’ pointer was utilized before it was verified against nullptr. Check lines: 547, 549. compiler.cpp 547
Кстати, использование указателя до проверки — распространенная ошибка. Это ещё один пример на эту тему.
Пример 5. Неполная проверка входных значений
PRBool
nsStyleAnimation::AddWeighted(...)
{
...
if (unit[0] == eCSSUnit_Null || unit[1] == eCSSUnit_Null ||
unit[0] == eCSSUnit_Null || unit[0] == eCSSUnit_URL) {
return PR_FALSE;
}
...
}
Диагностика PVS-Studio: V501 There are identical sub-expressions ‘unit [0] == eCSSUnit_Null’ to the left and to the right of the ‘||’ operator. nsstyleanimation.cpp 1767
Кажется, здесь сразу 2 опечатки. Затрудняюсь сказать, как именно должен выглядеть здесь код, но, наверное, хотели написать так:
if (unit[0] == eCSSUnit_Null || unit[1] == eCSSUnit_Null ||
unit[0] == eCSSUnit_URL || unit[1] == eCSSUnit_URL) {
Из-за опечаток функция может начать обрабатывать некорректные входные значения.
Пример 6. Неполная проверка входных значений
nsresult PresShell::SetResolution(float aXResolution, float
aYResolution)
{
if (!(aXResolution > 0.0 && aXResolution > 0.0)) {
return NS_ERROR_ILLEGAL_VALUE;
}
...
}
Диагностика PVS-Studio: V501 There are identical sub-expressions to the left and to the right of the ‘&&’ operator: aXResolution > 0.0 && aXResolution > 0.0 nspresshell.cpp 5114
А вот ещё один пример неудачной проверки входных параметров. В этот раз из-за опечатки не проверяется значение аргумента aYResolution.
Пример 7. Неразыменованный указатель
nsresult
SVGNumberList::SetValueFromString(const nsAString& aValue)
{
...
const char *token = str.get();
if (token == '') {
return NS_ERROR_DOM_SYNTAX_ERR; // nothing between commas
}
...
}
Диагностика PVS-Studio: V528 It is odd that pointer to ‘char’ type is compared with the » value. Probably meant: *token == ». svgnumberlist.cpp 96
Проверка, что между запятыми ничего нет, не работает. Чтобы узнать, пустая строка или нет, можно сравнить первый символ с ». Но здесь с нулем сравнивается не первый символ, а указатель. Этот указатель всегда неравен нулю. Корректная проверка должна была выглядеть так: (*token == »).
Пример 8. Неподходящий тип для хранения индекса
PRBool
nsIEProfileMigrator::TestForIE7()
{
...
PRUint32 index = ieVersion.FindChar('.', 0);
if (index < 0)
return PR_FALSE;
...
}
Диагностика PVS-Studio: V547 Expression ‘index < 0’ is always false. Unsigned type value is never < 0. nsieprofilemigrator.cpp 622
Функция не вернёт PR_FALSE, если в строке нет точки и продолжит работать с некорректными данными. Ошибка в том, что для переменной ‘index’ выбран беззнаковый тип данных. Проверка (index < 0) не имеет смысла.
Пример 9. Формирование неправильного сообщения об ошибке
cairo_status_t
_cairo_win32_print_gdi_error (const char *context)
{
...
fwprintf(stderr, L"%s: %S", context, (wchar_t *)lpMsgBuf);
...
}
Диагностика PVS-Studio: V576 Incorrect format. Consider checking the third actual argument of the ‘fwprintf’ function. The pointer to string of wchar_t type symbols is expected. cairo-win32-surface.c 129
Даже если ошибка успешно обнаружена, её еще надо суметь правильно обработать. А поскольку обработчики ошибок тоже никто не тестирует, то там можно увидеть много интересного.
Функция _cairo_win32_print_gdi_error() распечатает абракадабру. В качестве третьего аргумента функция fwprintf() ожидает указатель на unicode-строку, а вместо этого получает строку в формате ‘const char *’.
Пример 10. Ошибка записи дампа
bool ExceptionHandler::WriteMinidumpForChild(...)
{
...
DWORD last_suspend_cnt = -1;
...
// this thread may have died already, so not opening
// the handle is a non-fatal error
if (NULL != child_thread_handle) {
if (0 <= (last_suspend_cnt =
SuspendThread(child_thread_handle))) {
...
}
Диагностика PVS-Studio: V547 Expression is always true. Unsigned type value is always >= 0. exception_handler.cc 846
Это другой пример в обработчике ошибок. Здесь некорректно обрабатывается результат, возвращаемый функцией SuspendThread. Переменная last_suspend_cnt имеет тип DWORD, а значит она всегда будет больше или равна 0.
О других ошибках в Firefox
Сделаю небольшое отступление и расскажу о результатах проверки Firefox в целом. Проект качественен, и PVS-Studio выявил мало ошибок. Однако, так как он большой, то в количественном отношении ошибок достаточно много. К сожалению, мне не удалось полноценно изучить отчет, выданный инструментом PVS-Studio. Дело в том, что для Firefox отсутствует файл проекта для Visual Studio. Проект проверялся консольной версией PVS-Studio, вызываемой из make-файла. Открыв отчет в Visual Studio, можно просмотреть все диагностические сообщения. Но раз нет проекта, то Visual Studio не подсказывает, где какие переменные объявлены, не позволяет перейти в место определения макросов и так далее. В результате, анализ неизвестного проекта крайне трудоемок, и я смог изучить только часть сообщений.
Ошибки встречаются разноплановые. Например, есть выход за границы массива:
class nsBaseStatis : public nsStatis {
public:
...
PRUint32 mLWordLen[10];
...
nsBaseStatis::nsBaseStatis(...)
{
...
for(PRUint32 i = 0; i < 20; i++)
mLWordLen[i] = 0;
...
}
...
};
Диагностика PVS-Studio: V557 Array overrun is possible. The value of ‘i’ index could reach 19. detectcharset.cpp 89
Хотя эта и подобные ошибки интересны, они не связаны с темой данной статьи. Поэтому, если интересно, можно посмотреть на некоторые другие ошибки в этом файле: mozilla-test.txt.
Вернемся к ошибкам в обработчиках ошибок
Я решил привести не парочку, а 10 примеров, чтобы убедить вас в актуальности проблем наличия дефектов в обработчиках ошибок. Конечно, обработчики ошибок не самые критичные и важные участки программы. Но ведь программисты их пишут, а значит, надеются с их помощью улучшить поведение программы. К сожалению, как показывают мои наблюдения, очень часто проверки и обработчики ошибок не работают. Смотрите, мне было достаточно одного, пусть и крупного проекта, чтобы показать множество ошибок данного типа.
Что же с этим делать и какие можно дать рекомендации?
Первая рекомендация
Нужно признать, что можно сделать ошибку даже в простой проверке. Это самое сложное и важное. Именно из-за того, что обработчики ошибок считаются простыми фрагментами кода, в них так много опечаток и иных дефектов. Обработчики ошибок не проверяют и не тестируют. На них не пишут тесты.
Конечно, писать тесты на обработчики ошибок очень сложно и часто экономически нецелесообразно. Но если программист хотя бы будет знать об опасности, это уже многое. Осведомлен, значит вооружен. С обработчиками ошибок можно привести и вот такую аналогию.
По статистике альпинисты чаще всего падают в конце подъема. Это случается не из-за усталости, а из-за того, что человек думает, что ему осталось совсем немного. Он расслабляется, теряет внимательность и, как результат, чаще допускает ошибки. С программистом при написании кода происходит что-то похожее. Он много сил и внимания тратит на алгоритм, а различные проверки пишет не сосредотачиваясь, так как уверен, что в них он не может допустить ошибку.
Итак, теперь вы предупреждены. И я уверен, это уже очень хорошо и полезно.
Если вы скажите, что подобные глупые ошибки допускают только студенты и неопытные программисты, то вы не правы. Опечатки легко делают все. Предлагаю на эту тему вот эту небольшую заметку «Миф второй — профессиональные разработчики не допускают глупых ошибок». Я могу подтвердить это множеством примеров из различных проектов. Но думаю, приведенных здесь вполне достаточно, чтобы задуматься.
Вторая рекомендация
Механизмы сохранения дампов, функции записи логов и другие подобные вспомогательные механизмы вполне заслуживают того, чтобы для них были сделаны юнит-тесты.
Неработающий механизм сохранения дампа не только бесполезен, он только создает видимость, что в случае беды им можно будет воспользоваться. Если пользователь пришлёт испорченный dump-файл, то он не только не поможет, а может только ввести в заблуждение и на поиски ошибок будет потрачено больше времени, чем если бы dump-файла вообще отсутствовал.
Рекомендация выглядит простой и очевидной. Но у многих ли, из читающих эту заметку, есть юнит-тесты для проверки класса WriteMyDump ?
Третья рекомендация
Используйте статические анализаторы кода. Возможность нахождения дефектов в обработчиках ошибок является одной из сильных сторон методологии статического анализа. Статический анализ покрывает все ветви кода, в не зависимости от частоты их использования в работающем приложении. Он может выявить ошибки, проявляющие себя крайне редко.
Другими словами, при статическом анализе покрытие кода составляет 100%. Достичь такого покрытия кода с помощью других видов тестирования практически нереально. Покрытие кода при юнит-тестах и регрессионном тестировании обычно составляет менее 80%. Оставшиеся 20% протестировать очень сложно. В эти 20% входят большинство обработчиков ошибок и редких ситуаций.
Четвертая рекомендация
Можно попробовать использовать методологию внесения неисправностей. Смысл в том, что ряд функций время от времени начинают возвращать различные коды ошибок, и программа должна корректно их обрабатывать. Например, можно написать свою функцию malloc(), которая время от времени будет возвращать NULL, даже если память ещё есть. Это позволит узнать, как будет вести себя программа, когда память действительно кончится. Аналогично можно поступать с такими функциями, как fopen(), CoCreateInstance(), CreateDC() и так далее.
Существуют специальные программы, которые позволяют автоматизировать этот процесс и не писать самостоятельно свои функции, приводящие временами к отказу. К сожалению, я не работал с подобными системами, поэтому затрудняюсь рассказать про них подробнее.
Заключение
Дефекты в обработчиках ошибок встречаются часто. К сожалению, я не уверен, что приведенные в статье рекомендации достаточны, чтобы их избежать. Но я надеюсь, что вас заинтересовала эта проблема, и вы сможете придумать способы сократить количество недочетов в своих программах. Также я и другие читатели будут признательны, если вы поделитесь своими идеями и методиками, которые позволят избежать ошибки рассмотренного вида.
Присылаем лучшие статьи раз в месяц
Я добрался до кода широко известного клиента мгновенных сообщений Miranda IM. Вместе с различными плагинами это достаточно большой проект, размер которого составляет около 950 тысяч строк кода на C и C++. И, как в любом солидном проекте с историей развития, в нем имеется немалое количество ошибок и опечаток.
Рассматривая дефекты в различных приложениях, я заметил некоторые закономерности. И сейчас на примере дефектов, найденных в Miranda IM, я попробую сформулировать некоторые рекомендации, которые позволят избежать многих ошибок и опечаток ещё на этапе написания кода.
Для анализа Miranda IM я использовал (да, вы уже угадали) анализатор PVS-Studio 4.14. Код проекта Miranda IM весьма качественен, что подтверждается популярностью этой программы. Я и сам являюсь пользователем этого клиента и не имею к нему претензий в плане качества. Проект собирается в Visual Studio с Warning Level 3 (/W3), а количество комментариев составляет 20% от всего текста программы.
1. Избегайте функции memset, memcpy, ZeroMemory и им аналогичные
Мы начнем с ошибок, возникающих при использовании низкоуровневых функций работы с памятью такими, как memset, memcpy, ZeroMemory и им подобных.
Рекомендую всячески избегать этих функций. Понятно, что не стоит буквально следовать моему совету и заменять все эти функции на циклы. Но я видел такое огромное количество ошибок, связанных с использованием этих функций, что очень рекомендую быть аккуратными с ними и использовать только по необходимости. На мой взгляд есть только два случая, когда использование этих функций оправдано:
1) Обработка больших массивов. То есть там, где оптимизированный алгоритм функции даст выигрыш по сравнению с обработкой элементов в простом цикле.
2) Обработка большого количества маленьких массивов. Также имеет смысл с точки зрения улучшения производительности.
Во всех остальных случаях лучше попробовать обойтись без них. Например, я считаю эти функции излишними в такой программе, как Miranda. Никаких ресурсоёмких алгоритмов или огромных массивов в ней нет. Следовательно, использование функций memset/memcpy проистекает только из удобства написания короткого кода. Однако, эта простота очень обманчива и, сэкономив несколько секунд на написании кода, можно неделями вылавливать нечётко проявляющуюся ошибку порчи памяти. Рассмотрим несколько примеров кода, взятых из проекта Miranda IM.
V512 A call of the ‘memcpy’ function will lead to a buffer overflow or underflow. tabsrmm utils.cpp 1080
typedef struct _textrangew { CHARRANGE chrg; LPWSTR lpstrText; } TEXTRANGEW; const wchar_t* Utils::extractURLFromRichEdit(...) { ... ::CopyMemory(tr.lpstrText, L"mailto:", 7); ... }
Копируем только кусок строки. Ошибка банальна до безобразия, но от этого не перестаёт быть ошибкой. Скорее всего, раньше здесь использовалась строка, состоящая из ‘char’. Затем перешли на Unicode строки, а поменять константу забыли.
Если изначально использовать для копирования строки функции, которые для этого и предназначаются, то такая ошибка просто не сможет возникнуть. Представим, что было написано так:
strncpy(tr.lpstrText, "mailto:", 7);
Тогда при переходе к Unicode строкам, число 7 изменять не надо:
wcsncpy(tr.lpstrText, L"mailto:", 7);
Я не говорю, что это идеальный код. Но он уже намного лучше, чем использование CopyMemory. Рассмотрим другой пример.
V568 It’s odd that the argument of sizeof() operator is the ‘& ImgIndex’ expression. clist_modern modern_extraimage.cpp 302
void ExtraImage_SetAllExtraIcons(HWND hwndList,HANDLE hContact) { ... char *(ImgIndex[64]); ... memset(&ImgIndex,0,sizeof(&ImgIndex)); ... }
Здесь хотелось обнулить массив, состоящий из 64-указателей. Но вместо этого мы обнулим только первый элемент. Эта же ошибка, кстати, присутствует ещё раз в другом файле. Скажем спасибо Copy-Paste:
V568 It’s odd that the argument of sizeof() operator is the ‘& ImgIndex’ expression. clist_mw extraimage.c 295
Корректный вариант кода должен выглядеть следующим образом:
memset(&ImgIndex,0,sizeof(ImgIndex));
Кстати, взятие адреса у массива может только дополнительно запутать того, кто читает код. Здесь взятие адреса не имеет никакого эффекта. И код может быть написан так:
memset(ImgIndex,0,sizeof(ImgIndex));
Следующий пример.
V568 It’s odd that the argument of sizeof() operator is the ‘& rowOptTA’ expression. clist_modern modern_rowtemplateopt.cpp 258
static ROWCELL* rowOptTA[100]; void rowOptAddContainer(HWND htree, HTREEITEM hti) { ... ZeroMemory(rowOptTA,sizeof(&rowOptTA)); ... }
Снова, вместо вычисления размера массива, мы вычисляем размер указателя. Правильным выражением будет «sizeof(rowOptTA)». Для очистки массива я предлагаю следующий вариант такого кода:
const size_t ArraySize = 100; static ROWCELL* rowOptTA[ArraySize]; ... std::fill(rowOptTA, rowOptTA + ArraySize, nullptr);
Я уже привык, что подобные строки любят размножаться по программе копированием:
V568 It’s odd that the argument of sizeof() operator is the ‘& rowOptTA’ expression. clist_modern modern_rowtemplateopt.cpp 308
V568 It’s odd that the argument of sizeof() operator is the ‘& rowOptTA’ expression. clist_modern modern_rowtemplateopt.cpp 438
Вы думаете, что это всё касательно низкоуровневой работы с масивами? Нет, не всё. Смотрите дальше, бойтесь и бейте по рукам любителей написать memset.
V512 A call of the ‘memset’ function will lead to a buffer overflow or underflow. clist_modern modern_image_array.cpp 59
static BOOL ImageArray_Alloc(LP_IMAGE_ARRAY_DATA iad, int size) { ... memset(&iad->nodes[iad->nodes_allocated_size], (size_grow - iad->nodes_allocated_size) * sizeof(IMAGE_ARRAY_DATA_NODE), 0); ... }
В этот раз размер копируемых данных вычислен корректно. Вот только перепутан местами второй и третий аргумент. Как следствие, мы заполняем 0 элементов. Корректный вариант:
memset(&iad->nodes[iad->nodes_allocated_size], 0, (size_grow - iad->nodes_allocated_size) * sizeof(IMAGE_ARRAY_DATA_NODE));
Как переписать этот фрагмент кода более изящно, я не знаю. Вернее, его нельзя переписать красиво, не затрагивая другие участки программы и структуры данных.
Возникает вопрос, а как же обойтись без memset при работе с такими структурами, как OPENFILENAME:
OPENFILENAME x; memset(&x, 0, sizeof(x));
Очень просто. Создать обнуленную структуру можно так:
OPENFILENAME x = { 0 };
2. Внимательно следите, работаете вы со знаковым или беззнаковым типом
Проблема путаницы между signed и unsigned типами может на первый взгляд показаться надуманной. Но этот вопрос зря недооценивается программистами.
В большинстве случаев, людям не нравится рассматривать предупреждения компилятора, связанные с тем, что переменная типа int сравнивается с переменной типа unsigned. И действительно, чаще всего подобный код совершенно корректен. Программисты отключают такие предупреждения или не обращают на них внимания. Или есть третий вариант, они вписывают явное приведение типа, чтобы заглушить предупреждение компилятора, не вдаваясь в подробности.
Предлагаю больше так не делать и каждый раз анализировать ситуацию, когда знаковый тип встречается с беззнаковым. И, вообще, быть внимательным к тому, какой тип имеет выражение или что возвращает функция. Теперь несколько примеров по рассматриваемой теме.
V547 Expression ‘wParam >= 0’ is always true. Unsigned type value is always >= 0. clist_mw cluiframes.c 3140
В коде программы имеется функция id2pos, которая возвращает значение ‘-1’ в случае ошибки. С этой функцией всё в порядке. В другом месте результат работы функции id2pos используется следующим образом:
typedef UINT_PTR WPARAM; static int id2pos(int id); static int nFramescount=0; INT_PTR CLUIFrameSetFloat(WPARAM wParam,LPARAM lParam) { ... wParam=id2pos(wParam); if(wParam>=0&&(int)wParam<nFramescount) if (Frames[wParam].floating) ... }
Проблема в том, что переменная wParam имеет беззнаковый тип. Следовательно, условие ‘wParam>=0’ всегда истинно. Если функция id2pos вернет ‘-1’, то условие проверки недопустимых значений не сработает, и мы начнем использовать отрицательный индекс.
Я почти уверен, что вначале был написан следующий код:
if (wParam>=0 && wParam<nFramescount)
Компилятор Visual C++ выдал предупреждение «warning C4018: ‘<‘: signed/unsigned mismatch». Это предупреждение как раз включено на Warning Level 3, с которым и собирается Miranda IM. И в этот момент этому месту было уделено недостаточное внимание. Предупреждение было подавлено явным приведением типа. Но ошибка не исчезла, а только спряталась. Корректным вариантом должен был стать следующий код:
if ((int)wParam>=0 && (int)wParam<nFramescount)
Так что внимание и ещё раз внимание к подобным местам. В Miranda IM я насчитал 33 условия, которые из-за путаницы с signed/unsigned всегда истинны или всегда ложны.
Продолжим. Следующий пример мне особенно нравится. А комментарий, просто прекрасен.
V547 Expression ‘nOldLength < 0’ is always false. Unsigned type value is never < 0. IRC mstring.h 229
void Append( PCXSTR pszSrc, int nLength ) { ... UINT nOldLength = GetLength(); if (nOldLength < 0) { // protects from underflow nOldLength = 0; } ... }
Думаю, пояснений к этому коду не требуется.
Конечно, в ошибках не всегда виноваты только программисты. Иногда свинью подкладывают и разработчики библиотек (в данном случае WinAPI).
#define SRMSGSET_LIMITNAMESLEN_MIN 0 static INT_PTR CALLBACK DlgProcTabsOptions(...) { ... limitLength = GetDlgItemInt(hwndDlg, IDC_LIMITNAMESLEN, NULL, TRUE) >= SRMSGSET_LIMITNAMESLEN_MIN ? GetDlgItemInt(hwndDlg, IDC_LIMITNAMESLEN, NULL, TRUE) : SRMSGSET_LIMITNAMESLEN_MIN; ... }
Если не обращать внимания на излишне сложное выражение, то код выглядит корректно. Кстати, это вообще была одна строка. Для удобства я разбил её на несколько. Впрочем, форматирование кода сейчас мы затрагивать не будем.
Проблема в том, что функция GetDlgItemInt() возвращает вовсе не ‘int’, как ожидал программист. Эта функция возвращает UINT. Вот её прототип из файла «WinUser.h»:
WINUSERAPI UINT WINAPI GetDlgItemInt( __in HWND hDlg, __in int nIDDlgItem, __out_opt BOOL *lpTranslated, __in BOOL bSigned);
В результате PVS-Studio выдает сообщение:
V547 Expression is always true. Unsigned type value is always >= 0. scriver msgoptions.c 458
И это действительно так. Выражение «GetDlgItemInt(hwndDlg, IDC_LIMITNAMESLEN, NULL, TRUE) >= SRMSGSET_LIMITNAMESLEN_MIN» всегда истинно.
Конкретно в данном случае, возможно, никакой ошибки и не будет. Но смысл моего предупреждения, надеюсь, понятен. Внимательно смотрите, что возвращают вам функции.
3. Избегайте большого количества вычислений в одной строке
Каждый программист знает и при обсуждениях ответственно заявляет, что надо писать простой и понятный код. Но на практике иногда кажется, что он участвует в тайном конкурсе, кто напишет строку похитрее, с использованием интересной конструкции языка или продемонстрирует умение жонглировать указателями.
Чаще всего ошибки возникают там, где программист, с целью создать компактный код, собирает в одну строку сразу несколько действий. При незначительном выигрыше в изящности он существенно увеличивает риск опечатки или того, что не учтёт побочные действия. Рассмотрим пример:
V567 Undefined behavior. The ‘s’ variable is modified while being used twice between sequence points. msn ezxml.c 371
short ezxml_internal_dtd(ezxml_root_t root, char *s, size_t len) { ... while (*(n = ++s + strspn(s, EZXML_WS)) && *n != '>') { ... }
Здесь возникает неопределенное поведение (undefined behavior). Этот код может корректно функционировать в течение долгого периода жизни программы. Но нет никакой гарантии, что он также будет вести себя после смены версии компилятора или ключей оптимизации. Компилятор в праве вначале вычислить ‘++s’, а затем взывать функцию ‘strspn(s, EZXML_WS)’. Или, наоборот, вначале он может вызвать функцию, а уже затем увеличить значение переменной ‘s’.
Приведу другой пример, почему не стоит пытаться собрать всё в одну строку. В Miranda IM некоторые ветви исполнения программы отключены/включены с помощью вставок вида ‘&& 0’. Примеры:
if ((1 || altDraw) && ... if (g_CluiData.bCurrentAlpha==GoalAlpha &&0) if(checkboxWidth && (subindex==-1 ||1)) {
С приведенными сравнениями всё ясно и они хорошо заметны. А теперь представьте, что вы встречаете фрагмент показанный ниже. Код я отформатировал. В программе это ОДНА строка.
V560 A part of conditional expression is always false: 0. clist_modern modern_clui.cpp 2979
LRESULT CLUI::OnDrawItem( UINT msg, WPARAM wParam, LPARAM lParam ) { ... DrawState(dis->hDC,NULL,NULL,(LPARAM)hIcon,0, dis->rcItem.right+dis->rcItem.left- GetSystemMetrics(SM_CXSMICON))/2+dx, (dis->rcItem.bottom+dis->rcItem.top- GetSystemMetrics(SM_CYSMICON))/2+dx, 0,0, DST_ICON| (dis->itemState&ODS_INACTIVE&&FALSE?DSS_DISABLED:DSS_NORMAL)); ... }
Если здесь нет ошибки, то всё равно непросто вспомнить и найти в этой строке слово FALSE. Вы его нашли? Согласитесь — непростая задача. А если это ошибка? Тогда вообще нет шансов найти её, просто просматривая код. Подобные выражения очень полезно выносить отдельно. Пример:
UINT uFlags = DST_ICON; uFlags |= dis->itemState & ODS_INACTIVE && FALSE ? DSS_DISABLED : DSS_NORMAL;
А я сам, пожалуй, написал бы это длинным, но более понятным способом:
UINT uFlags; if (dis->itemState & ODS_INACTIVE && (((FALSE)))) uFlags = DST_ICON | DSS_DISABLED; else uFlags = DST_ICON | DSS_NORMAL;
Да, это длиннее, но зато легко читается, а слово FALSE лучше заметно.
4. Выравнивайте в коде всё, что возможно
Выравнивание кода уменьшает вероятность допустить опечатку или ошибки при Copy-Paste. Если ошибка все же будет сделана, то найти её в процессе обзора кода будет в несколько раз проще. Рассмотрим пример кода.
V537 Consider reviewing the correctness of ‘maxX’ item’s usage. clist_modern modern_skinengine.cpp 2898
static BOOL ske_DrawTextEffect(...) { ... minX=max(0,minX+mcLeftStart-2); minY=max(0,minY+mcTopStart-2); maxX=min((int)width,maxX+mcRightEnd-1); maxY=min((int)height,maxX+mcBottomEnd-1); ... }
Монолитный фрагмент кода, читать который совершенно не интересно. Отформатируем его:
minX = max(0, minX + mcLeftStart - 2); minY = max(0, minY + mcTopStart - 2); maxX = min((int)width, maxX + mcRightEnd - 1); maxY = min((int)height, maxX + mcBottomEnd - 1);
Это не самый показательный пример, но согласитесь, теперь стало намного проще заметить, что два раза используется переменная maxX.
Мою рекомендацию по выравниванию не стоит понимать буквально и везде строить столбцы кода. Во-первых, это требует дополнительного времени при написании и правке кода. Во-вторых, это может привести к другим ошибкам. В следующем примере, как раз желание сделать красивый столбик привело к возникновению ошибки в коде Miranda IM.
V536 Be advised that the utilized constant value is represented by an octal form. Oct: 037, Dec: 31. msn msn_mime.cpp 192
static const struct _tag_cpltbl { unsigned cp; const char* mimecp; } cptbl[] = { { 037, "IBM037" }, // IBM EBCDIC US-Canada { 437, "IBM437" }, // OEM United States { 500, "IBM500" }, // IBM EBCDIC International { 708, "ASMO-708" }, // Arabic (ASMO 708) ... }
Желая сделать красивую колонку чисел, легко увлечься и вписать в начале ‘0’, сделав константу восьмеричной.
Уточняю рекомендацию. Выравнивайте в коде всё, что возможно. Но не выравнивайте числа, дописывая нули.
5. Не размножайте строку более, чем один раз
Копирование строк при программировании неизбежно. Но можно подстраховать себя, не вставляя строку из буфера обмена сразу несколько раз. В большинстве случаев лучше скопировать строку, затем отредактировать. Вновь скопировать и отредактировать. И так далее. Так трудней забыть что-то изменить в строке или изменить её неправильно. Рассмотрим пример кода:
V525 The code containing the collection of similar blocks. Check items ‘1316’, ‘1319’, ‘1318’, ‘1323’, ‘1323’, ‘1317’, ‘1321’ in lines 954, 955, 956, 957, 958, 959, 960. clist_modern modern_clcopts.cpp 954
static INT_PTR CALLBACK DlgProcTrayOpts(...) { ... EnableWindow(GetDlgItem(hwndDlg,IDC_PRIMARYSTATUS),TRUE); EnableWindow(GetDlgItem(hwndDlg,IDC_CYCLETIMESPIN),FALSE); EnableWindow(GetDlgItem(hwndDlg,IDC_CYCLETIME),FALSE); EnableWindow(GetDlgItem(hwndDlg,IDC_ALWAYSPRIMARY),FALSE); EnableWindow(GetDlgItem(hwndDlg,IDC_ALWAYSPRIMARY),FALSE); EnableWindow(GetDlgItem(hwndDlg,IDC_CYCLE),FALSE); EnableWindow(GetDlgItem(hwndDlg,IDC_MULTITRAY),FALSE); ... }
Скорее всего, никакой настоящей ошибки здесь нет. Просто два раза работаем с элементом IDC_ALWAYSPRIMARY. Тем не менее, ошибиться в подобных блоках из скопированных строк весьма легко.
6. Выставляйте высокий уровень предупреждений у компилятора и используйте статические анализаторы
По поводу многих ошибок невозможно дать рекомендаций, как снизить вероятность их появления в коде. Чаще всего они представляют собой опечатки, которые допускает как новичок, так и самый высококвалифицированный программист.
Тем не менее, многие из этих ошибок можно обнаружить ещё на этапе написания кода. В первую очередь — это предупреждения компилятора. А во вторую — отчеты от ночного запуска статических анализаторов кода.
Сейчас кто-то скажет, что это плохо скрываемая реклама. Но на самом деле это просто ещё одна рекомендация, которая позволяет сократить количество ошибок. Если я нашел ошибки, используя статический анализ, и не могу сказать, как избежать их появления в коде, то значит рекомендацией как раз и является использование анализаторов кода.
Рассмотрим некоторые примеры ошибок, которые могут быть оперативно выявлены анализаторами исходного кода:
V560 A part of conditional expression is always true: 0x01000. tabsrmm tools.cpp 1023
#define GC_UNICODE 0x01000 DWORD dwFlags; UINT CreateGCMenu(...) { ... if (iIndex == 1 && si->iType != GCW_SERVER && !(si->dwFlags && GC_UNICODE)) { ... }
Допущена опечатка. Вместо оператора ‘&’ используется оператор ‘&&’. Как здесь подстраховаться при написании кода, я не знаю. Корректный вариант условия:
(si->dwFlags & GC_UNICODE)
Следующий пример.
V528 It is odd that pointer to ‘char’ type is compared with the » value. Probably meant: *str != ». clist_modern modern_skinbutton.cpp 282
V528 It is odd that pointer to ‘char’ type is compared with the » value. Probably meant: *endstr != ». clist_modern modern_skinbutton.cpp 283
static char *_skipblank(char * str) { char * endstr=str+strlen(str); while ((*str==' ' || *str=='t') && str!='') str++; while ((*endstr==' ' || *endstr=='t') && endstr!='' && endstr<str) endstr--; ... }
В этом коде всего лишь забыты две звездочки ‘*’ для разыменования указателей. Результат может быть фатальным. Этот код предрасположен к access violation. Корректный вариант кода:
while ((*str==' ' || *str=='t') && *str!='') str++; while ((*endstr==' ' || *endstr=='t') && *endstr!='' && endstr<str) endstr--;
Здесь вновь сложно дать совет, кроме использования специальных инструментов для проверки кода.
Следующий пример.
V514 Dividing sizeof a pointer ‘sizeof (text)’ by another value. There is a probability of logical error presence. clist_modern modern_cachefuncs.cpp 567
#define SIZEOF(X) (sizeof(X)/sizeof(X[0])) int Cache_GetLineText(..., LPTSTR text, int text_size, ...) { ... tmi.printDateTime(pdnce->hTimeZone, _T("t"), text, SIZEOF(text), 0); ... }
На первый взгляд все хорошо. В функцию передается текст и его длина, посчитанная с помощью макроса SIZEOF. На самом деле макрос следует назвать COUNT_OF, но не в этом дело. Беда в том, что мы пытаемся посчитать количество символов в указателе. Здесь вычисляется «sizeof(LPTSTR) / sizeof(TCHAR)». Человек такие подозрительные места замечает плохо, а вот компилятор и статический анализатор хорошо. Исправленный вариант кода:
tmi.printDateTime(pdnce->hTimeZone, _T("t"), text, text_size, 0);
Следующий пример
V560 A part of conditional expression is always true: 0x29. icqoscar8 fam_03buddy.cpp 632
void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen) { ... else if (wTLVType = 0x29 && wTLVLen == sizeof(DWORD)) ... }
Здесь уместна рекомендация писать константу в условии на первом месте. Вот такой код просто не скомпилируется:
if (0x29 = wTLVType && sizeof(DWORD) == wTLVLen)
Но многим программистам, и мне в их числе, такой стиль не нравится. Например, мне он мешает читать код, так как вначале я хочу узнать, какую переменную сравнивают, а только потом с чем именно.
Если программист отказывается от такого стиля сравнений, то ему остается или полагаться на компилятор/анализатор, или рисковать.
Кстати, несмотря на то, что про эту ошибку все знают, она не самая редкая. Ещё три примера из Miranda IM, где анализатор PVS-Studio выдал предупреждение V559:
else if (ft->ft_magic = FT_MAGIC_OSCAR) if (ret=0) {return (0);} if (Drawing->type=CLCIT_CONTACT)
Анализ кода позволяет также выявить если и не ошибки, то очень подозрительные места в коде. Например, в Miranda IM указатели используют далеко не только как указатели. Если в одних местах кода такие игры выглядят нормально, то в других пугают. Пример кода, который меня крайне настораживает:
V542 Consider inspecting an odd type cast: ‘char *’ to ‘char’. clist_modern modern_toolbar.cpp 586
static void sttRegisterToolBarButton(..., char * pszButtonName, ...) { ... if ((BYTE)pszButtonName) tbb.tbbFlags=TBBF_FLEXSIZESEPARATOR; else tbb.tbbFlags=TBBF_ISSEPARATOR; ... }
Фактически мы проверяем, что адрес строки не кратен 256. Мне не понятно, что на самом деле хотели написать в условии. Возможно, это даже правильно, но сильно сомнительно.
Анализом кода можно найти немало некорректных условий. Пример:
V501 There are identical sub-expressions ‘user->statusMessage’ to the left and to the right of the ‘&&’ operator. jabber jabber_chat.cpp 214
void CJabberProto::GcLogShowInformation(...) { ... if (user->statusMessage && user->statusMessage) ... }
И так далее и так далее. Я могу ещё долго приводить другие примеры, но это не имеет смысла. Важно то, что большое количество ошибок можно обнаружить с помощью статического анализа на самых ранних этапах.
Когда в вашей программе статический анализатор находит мало ошибок, то его использование не кажется интересным. Но это неверный путь в рассуждениях. Ведь многие ошибки, которые анализатор смог бы найти на ранних этапах, вы исправляли потом и кровью, отлаживаясь часами.
Статический анализ представляет больший интерес в процессе разработки программы, а не в качестве разовых проверок. Множество ошибок и опечаток находится в процессе тестирования и создания юнит-тестов. Но если часть из этих ошибок можно найти ещё на этапе написания кода, то это будет колоссальный выигрыш времени и сил. Обидно два часа отлаживать программу, чтобы потом заметить лишнюю точку с запятой ‘;’ после оператора ‘for’. Такую ошибку можно часто обезвредить, потратив 10 минут на статический анализ измененных в процессе работы файлов.
Заключение
В этой статье я поделился только некоторыми мыслями по поводу того, как допускать меньше ошибок при программировании на Си++. Зреют и другие мысли, о которых я постараюсь написать в следующих статьях и заметках.
P.S.
Уже стало традицией, что после подобной статьи, кто-то спрашивает, а сообщили ли вы разработчикам программы/библиотеки о найденных ошибках. Заранее отвечу на вопрос, отправили ли мы баг репорт по проекту Miranda IM.
Нет, не отправили. Это слишком ресурсоёмкая задача. В статье показана только малая часть от того, что было найдено. В проекте около ста фрагментов, где стоит поправить код, и более ста, где я просто не знаю, ошибка это или нет. Однако, перевод этой статьи будет отправлен авторам Miranda IM и им будет предложена бесплатная версия анализатора PVS-Studio. Если они проявят интерес, то смогут сами проверить исходный код и исправить то, что сочтут нужным.
Ещё стоит пояснить, почему я часто не берусь судить ошибка тот или иной участок кода или нет. Пример неоднозначного кода:
V523 The ‘then’ statement is equivalent to the ‘else’ statement. scriver msglog.c 695
if ( streamData->isFirst ) { if (event->dwFlags & IEEDF_RTL) { AppendToBuffer(&buffer, &bufferEnd, &bufferAlloced, "rtlpar"); } else { AppendToBuffer(&buffer, &bufferEnd, &bufferAlloced, "ltrpar"); } } else { if (event->dwFlags & IEEDF_RTL) { AppendToBuffer(&buffer, &bufferEnd, &bufferAlloced, "rtlpar"); } else { AppendToBuffer(&buffer, &bufferEnd, &bufferAlloced, "ltrpar"); } }
Вот два одинаковых фрагмента кода. Возможно, это ошибка. А возможно, сейчас в любой ветке нужен одинаковый набор действий. И код написан специально так, чтобы потом его было просто модифицировать. Здесь необходимо знать программу, чтобы разобраться ошибочно это место или нет.
3 основных заблуждения при управлении качеством на предприятии
1. Решать проблемы качества может лишь организация. Руководители производств и
технологи на предприятии довольно часто жалуются – вынуждены работать с
оборудованием, которое уже ощутимо устарело. Этим обстоятельством также
аргументируют брак на предприятии, утверждая – покупка нового оборудования
позволит избавиться от проблем с качеством продукции. Однако не каждое
предприятие располагает ресурсами для модернизации, поэтому приходится
откладывать эту проблему. По своему опыту можем утверждать – в большинстве
случаев можно и без значительных инвестиций добиться модернизации и роста
качества, с налаживанием более эффективных производственных процессов. Да и
статистика подтверждает – нередко предприятия с новейшим оборудованием в своей
отрасли сталкиваются с еще более существенными проблемами качества продукции.
2. Сложно выявить, чем именно вызван брак на предприятии — типичный ответ
технологов и производственных менеджеров. По собственному опыту можем
утверждать – у каждой группы схожих случаев производственного брака существует
только одна коренная причина. Её решение станет профилактикой производственного
брака на будущее, даже при сохранении второстепенных причин.
3. Потребуется для устранения причин брака не один год. Довольно
распространенное заблуждение – обычно используется для объяснения, почему
проводится системная работа слишком медленно либо не осуществляется вовсе.
Однако практика подтверждает принцип Парето – на 20% проблем приходятся 80%
случаев брака. Устранив самые значимые проблемы, удастся значительно сократить
вероятность производственного брака.
Как избавиться от брака по вине работника на 100%
5 причин брака на предприятии и как с ними бороться
1. Сырье.
- выясните откуда у вас некачественное сырье;
- организуйте контроль качества на этапе поступления сырья в производство;
- включите в договор с поставщиками пункт о существенном штрафе при обнаружении некачественного сырья.
2. Оборудование
- установите сроки технического обслуживания и ремонта оборудования, а также
персональную ответственность за нарушение этих сроков и низкое качество
проведенных работ; - отслеживайте, на каком из агрегатов производится та или иная продукция. Тогда при обнаружении брака вы легко определите то оборудование, которое нуждается в наладке.
3. Технология производства
- поговорите с технологом и рабочими: им наверняка известно, какие
производственные методы несовершенны и влекут брак. Помните, что любое
внедрение новой технологии для уменьшения объемов брака должно окупаться.
4. Условия работы персонала
- организуйте сбор предложений рабочих по улучшению условий труда. Главное,
чтобы эти предложения не забывались, а реализовывались.
5. Непрофессионализм и безответственность рабочих
- постройте привлекательную для рабочих систему мотивации;
- обяжите сотрудников при поступлении некачественного сырья останавливать
работу и ставить в известность своего начальника; - внедряйте автоматизированные системы управления процессами, чтобы свести к минимуму человеческий фактор.
Пошаговый алгоритм ликвидации брака на предприятии
Первый шаг. Составить таблицу с указанием всех случаев брака на предприятии.
Для показательной статистики рекомендуем анализ данных минимум за год.
Второй шаг. Объединяем аналогичные причины производственного брака в общую
группу. Благодаря выделению группы схожих причин брака удастся рассчитать число
случаев за период, также потери от них. (см. табл. 2).
Третий шаг. Проведение анализа. Обычно после группировки оказывается, что
только несколько одинаковых причин регулярно повторяются, приводя к основной
доле производственного брака. Именно они заслуживают первоочередного внимания.
Четвертый шаг – выбираем причину брака на предприятии с максимальным
количеством случаев и наибольшими потерями.
Пятый шаг – снижаем или исключаем вероятность повторения частых причин
производственного брака.
В бережливом производстве существует термин пока-ёкэ (poka-yoke, япон. –
защита от ошибок). Данный термин предполагает: чтобы предотвратить
производственный брак в будущем, требуется обеспечение таких условий, когда
физически невозможно повторение брака, чтобы не было у сотрудника возможности
повторной ошибки и пр. До решения проблемы наше руководство нередко обвиняло
подчиненных, ссылаясь на проблему человеческого фактора. Однако
совершенствование производственного процесса позволило кардинально уменьшить
вероятность ошибки на предприятии – меньше начали выполняться операции в уме,
ответственность была делегирована между разными сотрудниками, удалось улучшить
благоприятные условия для работы.
Шестой шаг – разработка и введение в работу системы мотивации персонала,
ориентированной на сокращение производственного брака. В числе возможных мер
можно отметить определенный размер депремирования сотрудника за выпуск каждой
тонны товаров с браком, либо при допущенных ошибках. Также могут выплачиваться
премии за уменьшение доли брака до установленного норматива, индивидуальные
показатели работников можно размещать на стендах – будет стимулировать желание
работников сократить уровень брака.
Седьмой шаг – организация постоянного процесса повышения качества. Для каждого
сотрудника нужно определить индивидуальные показатели качества. Как правило,
достаточно 1-3 показателей. Затем каждый месяц вызывайте линейных руководителей
и запрашивайте данные по данным показателям. От каждого руководителя требуется
ежемесячный доклад о повышении либо снижении показателей работников, обращая
внимание на лучших сотрудников, освещая планируемая мероприятия на предстоящий
месяц. Руководителем выставляются оценки для своих работников. Также
оценивается и работа линейных руководителей – их вышестоящими начальниками.
Показатели лучших работников следует размещать на видных местах, предусмотреть
поощрения для отличившихся сотрудников.
При правильном анализе случаев производственного брака, с пониманием его
причин, и начавшейся работой для их искоренения, удастся уже в ближайшие 3-4
месяца ожидать снижение брака в работе среднего предприятия.
Как снизить брак на предприятии: примеры и рекомендации
Юрий Чинчик, Руководитель службы качества и промышленной безопасности
научно-производственной фирмы «Пакер»
Наша компания специализируется на поставках мелких серий оборудования под
заказ. Среди главных направлений – термическая и механическая (фрезерная,
токарная) обработка деталей и изделий. Для уменьшения процента брака в 2006-м
году у нас начала действовать система «Бриллиант» для фрезеровщиков, токарей и
слесарей-сборщиков – всех цеховых рабочих. Фактически, дефектную деталь
предъявляет сам работник, допустивший просчет.
Ведь почему допускается выпуск брака? Работник пытается скрыть свою ошибку,
чтобы не столкнуться с штрафами. Система «Бриллиант» предполагает.
1. Входной контроль. От каждого работника требуется оценка качества заготовок,
поступающих ему для работы. Если сразу видно, что не удастся сделать годную
деталь из этой заготовки, он не запускает её в работу, чтобы снизить стоимость
брака.
2. Выходной контроль. Сам работник должен проверить качество выполненной
детали. При выявлении брака он должен об этом сообщить, наказания не будет.
3. Межоперационный контроль. Осуществляется он контроллером, сотрудником ОТК.
У нас действует мелкосерийное производство. Чтобы в брак не ушла сразу вся
партия, рабочий должен показать контроллеру первую сделанную деталь – поскольку
именно на ней выше всего вероятность ошибки. В дальнейшем планируем исключить
такую практику проверки, но для этого еще потребуется комплекс подготовительных
работ. Примечательно, когда мы осуществляли опрос по этому вопросу, из 40
работников только двое отметили, что дополнительный контроль в их работе не
нужен.
4. Выдача личного клейма. Для работников, которые всегда сообщают про
допущенную ошибку либо вовсе не допускают брак, предоставляется личное клеймо.
Рабочие в таком случае сами выполняют роль контроллера ОТК, подтверждая
годность изделий своей печатью. Возможны ситуации, когда клеймо изымается у
работника. Основание для предоставления клейма и его лишения, порядок
сопутствующих процедур прописаны в инструкции для сотрудников.
5. Наказание не за брак, а за халатность. Несмотря на всю лояльность, у нас
всё же действуют определенные санкции по отношению к сотрудникам. Если работник
предъявил партию как годную, а контроллером либо другим сотрудником был выявлен
дефект, ожидает наказание. Но у нас действует наказание не за сам брак, а за
отсутствие должного контроля качества продукции, халатное отношение к работе. С
изготовителя в таком случае удерживаем стоимость заготовки и затраты на
предыдущие этапы обработки.
Роман Лукьянчиков, Генеральный Директор компании «Московские окна»
Чтобы уменьшить долю брака на предприятии и отходов, ввели на производстве
комплекс мер.
1. Обновили парк станков. Вместо ручных станков установлены автоматические
производственные линии. Подобные перемены дарят уверенность в стабильном
качестве продукции, снижая нашу зависимость от человеческого фактора. В
частности, в течение последних 5 лет нам удалось сократить уровень отходов при
производстве профиля ПВХ до 4. 2% вместо прежних 7. 5%. Но для оценки
эффективности оборудования недостаточно одного параметра. При модернизации
предприятия стремились также оптимизировать свой персонал, с ростом
автоматизации и производительности труда, сокращая время на обслуживание
станков. Если говорить об окупаемости оборудования только с учетом экономии на
отходах и браке, то выходит около 6 лет.
2. Подготовка программного обеспечения. Благодаря полной автоматизации всех
рабочих процессов удалось сократить уровень ошибок, с точным учетом рабочего
времени и задействованных материалов, с оптимизацией отходов материалов при
раскрое. Работаем сейчас с собственным комплексом программного обеспечения,
цикл разработки и обновления которого длился на протяжении десяти лет.
3. Работа с людьми. Достичь идеального результата можно только, если каждый
сотрудник лично заинтересован в улучшении работы. Закрепляется каждый этап
нашего производства за отдельным участком. При выявлении брака еще на стадии
производства, наказаний за него не предусмотрено. Но если продукция с браком
поступает уже самому клиенту, то вычитаем 100% цены товара и расходы на
повторную доставку с участка, который брак допустил. Наказание вводится также
для начальника смены. Действует полностью прозрачная система, поэтому работники
её считают справедливой, следовательно не работают с дефектным материалом,
оперативно реагируя на сбои оборудования – брак им абсолютно не выгоден.
Действует для каждого подразделения суммарный допустимый норматив брака. На
основном производстве он составляет 4. 25%. Уровень неисправимого брака (при
котором для переделки необходимы материальные расходы) составлять должен не
более 0. 35%. На остальные 3. 9% приходится косметический брак, когда выявленные
дефекты можно устранить. Для отдела монтажа допустимый процент брака составляет
2. 04%, для доставки – 1. 58%, замеров – 1. 85%.
Павел Меньшиков, Главный бухгалтер аппарата управления Генерального Директора
компании «Мостотрест», заместитель главного бухгалтера компании «Мостотрест»
На металлургическом предприятии была введена система «Черный ящик» для учета
деятельности производственных цехов. Цель нового проекта заключалась в
сокращении сверхнормативных расходов на производстве. Каждому сотруднику по
новой системе требовалось следование норме — из каждой поступившей на обработку
1 тонны полуфабрикатов выйти должны 0. 94 тонны готовой продукции.
Следовательно, у начальника цеха было право списания отходов только 60 кг. с 1
тонны металла (окалина, угар, обрез).
Для расчета норматива использовался достаточно простой принцип – брали
проценты окалины и угара из отраслевых стандартов, получив допустимый процент
обрези опытным путем за счет прогона оборудования.
Новые изменения создали комфортные условия для управления качеством. Теперь в
ситуации, если начальник цеха рассчитывает на получение премии, требуется
соблюдение заявленной нормы. У него при появлении проблем, которые угрожают
соблюдению норматива, появилось право уточнять вопрос с директором по
производству. При действительно объективной причине проблемы, оказывалась
помощь цеху в её решении.
Результат. Добиться улучшения ситуации удалось уже в сжатые сроки. Ранее
уровень потерь на одном из цехов составлял свыше 100 килограммов с каждой
тонны. Фактически, завалено всё помещение было бракованной продукцией и
обрезками. В отчете указывался непомерный процент угара – хотя приводилась
достаточно небольшая доля брака. Также постоянно встречались манипуляции с
цифрами – принимались полуфабрикаты по нормативному весу, а сдавались изделия
по фактическому. Новая система предполагала установку весов у обоих выездов,
изменения позволили сократить уровень отходов, который оказался даже меньше
запланированных показателей.
Совет. Перед принятием определенных мер по уменьшению брака необходимо, в
первую очередь, оценить экономический эффект таких изменений. Необходимо
располагать достоверными данными, однако именно с этим вопросом предприятие
часто испытывает проблемы. Решением вопроса становится один выход – вводимая на
предприятии информационная система. Она зарекомендовала себя высокой
эффективностью в сегменте массового производства. Но приходится признавать – у
нас практически нет качественных коробочных решений, предназначенных для
автоматизации производственного учета. Следовательно, потребуется авторская
разработка программного обеспечения для вашего предприятия или адаптация схожих
проектов под ваши требования.
Время на прочтение
8 мин
Количество просмотров 1.8K
Это третья статья, где я хочу рассказать про новую пару приёмов при программировании, которые помогут сделать код более простым и надежным. С предыдущими двумя заметками можно познакомиться здесь [1] и здесь [2]. В этот раз примеры будут взяты из проекта Qt.
Введение
Проект Qt 4.7.3. попался мне для изучения не случайно. Пользователи PVS-Studio обратили внимание, что анализ как-то слабоват, если дело касается проверки проектов, построенных на основе библиотеки Qt. Это не удивительно. Статический анализ позволяет находить ошибки за счет того, что смотрит на код более высокоуровнево, чем компилятор. Следовательно, он должен знать определенные паттерны кода и, что делают функции различных библиотек. В противном случае, он пройдет мимо многих замечательных ляпов. Поясню на примере:
if (strcmp(My_Str_A, My_Str_A) == 0)
Бессмысленно сравнивать строку саму с собой. Но компилятор промолчит. Он не задумывается, в чем суть функции strcmp(). У него хватает своих забот. А вот статические анализаторы могут заподозрить неладное. В Qt есть своя разновидность функции сравнения строк — qstrcmp(). И, соответственно, анализатор нужно обучить обращать внимание и на такую строку:
if (qstrcmp(My_Str_A, My_Str_A) == 0)
Осваивание библиотеки Qt и создание специализированных проверок — это большая и планомерная работа. И началом этой работы стала проверка самой библиотеки.
Закончив просмотр предупреждений, у меня созрело несколько новых мыслей по улучшению кода, которые, как я надеюсь, будут вам интересны и полезны.
1. Обрабатывайте переменные в той же последовательности, как они объявлены
Код библиотеки Qt очень качественен и практически не содержит ошибок. Зато в нём обнаружилось большое количество излишних инициализаций, излишних сравнений или лишних копирования значений переменных.
Приведу пару примеров для ясности:
QWidget *WidgetFactory::createWidget(...)
{
...
} else if (widgetName == m_strings.m_qDockWidget) { <<<===
w = new QDesignerDockWidget(parentWidget);
} else if (widgetName == m_strings.m_qMenuBar) {
w = new QDesignerMenuBar(parentWidget);
} else if (widgetName == m_strings.m_qMenu) {
w = new QDesignerMenu(parentWidget);
} else if (widgetName == m_strings.m_spacer) {
w = new Spacer(parentWidget);
} else if (widgetName == m_strings.m_qDockWidget) { <<<===
w = new QDesignerDockWidget(parentWidget);
...
}
Здесь два раза продублировано одно и то же сравнение. Это не ошибка, но совершенно избыточный код. Другой аналогичный пример:
void QXmlStreamReaderPrivate::init()
{
tos = 0; <<<===
scanDtd = false;
token = -1;
token_char = 0;
isEmptyElement = false;
isWhitespace = true;
isCDATA = false;
standalone = false;
tos = 0; <<<===
...
}
Вновь не ошибка, но совершенно ненужная двойная инициализация переменной. И подобных продублированных действий я насчитал очень много. Возникают они из-за длинных списков сравнений, присваиваний, инициализаций. Просто не видно, что переменная уже обрабатывается, из-за чего и появляются лишние операции. Я могу назвать три неприятных последствия наличия таких дублирующихся действий:
- Дубликаты увеличивают длину кода. А чем длиннее код, тем легче добавить еще один дубликат.
- Если мы захотим изменить логику программы и удалим одну проверку или одно присваивание, то дубликат этого действия может подарить вам несколько часов увлекательной отладки. Сами представьте, вы пишите «tos = 1» (см. первый пример), а потом в другой части программы удивляетесь, отчего же по-прежнему «tos» равен нулю.
- Замедление скорости работы. Практически всегда им можно пренебречь в таких ситуациях, но оно всё-таки есть.
Надеюсь, убедил, что дубликатам не место в нашем коде. Как с ними бороться? Как правило, подобные инициализации/сравнения идут блоком. И есть такой же блок переменных. Рационально писать код так, чтобы порядок объявлений переменных и порядок работы с ними совпадал. Приведу пример не очень хорошего кода:
struct T {
int x, y, z;
float m;
int q, w, e, r, t;
} A;
...
A.m = 0.0;
A.q = 0;
A.x = 0;
A.y = 0;
A.z = 0;
A.q = 0;
A.w = 0;
A.r = 1;
A.e = 1;
A.t = 1;
Естественно, это схематичный пример. Смысл в том, что когда инициализация не последовательна, то намного легче написать две одинаковых строки. В приведенном коде дважды инициализируется переменная ‘q’. И ошибка плохо заметна, если просматривать код бегло. Если теперь инициализировать переменные в той же последовательности, как они объявлены, то подобной ошибке просто не будет места. Улучшенный вариант кода:
struct T {
int x, y, z;
float m;
int q, w, e, r, t;
} A;
...
A.x = 0;
A.y = 0;
A.z = 0;
A.m = 0.0;
A.q = 0;
A.w = 0;
A.e = 1;
A.r = 1;
A.t = 1;
Конечно, я знаю, что не всегда так можно написать и работать с переменными в порядке их объявления. Но часто это возможно и полезно. Дополнительным плюсом станет то, что вам легче будет ориентироваться в коде.
Рекомендация. Добавляя новую переменную, постарайтесь, чтобы её инициализация и обработка, осуществлялась в соответствии с её положением относительно других переменных.
2. Табличные методы — это хорошо.
Про табличные методы хорошо написано у С. Макконнелла в книге «Совершенный код» в главе N18 [3]:
Табличный метод — это схема, позволяющая искать информацию в таблице, а не использовать для этого логические выражения, такие как if и case. Практически все, что вы можете выбирать посредством логических операторов, можно выбирать, применяя таблицы. В простых случаях логические выражения проще и понятней. Но при усложнении логических построений таблицы становятся всё привлекательнее.
Так вот, очень жаль, что программисты по-прежнему любят огромные switch() или густой лес конструкций if-else. Перебороть в себе это очень сложно. Кажется, ну еще-то один «case:» или маленький «if» не повредит. Но он вредит. И неудачно добавляют новые условия даже самые опытные программисты. В качестве примера пара дефектов, найденных в Qt.
int QCleanlooksStyle::pixelMetric(...)
{
int ret = -1;
switch (metric) {
...
case PM_SpinBoxFrameWidth:
ret = 3;
break;
case PM_MenuBarItemSpacing:
ret = 6;
case PM_MenuBarHMargin:
ret = 0;
break;
...
}
Длинный-предлинный switch(). И, естественно, имеется забытый оператор «break». Анализатор выявил эту ошибку за счет того, что переменной «ret» дважды подряд присваивается разное значение.
Пожалуй, намного лучше, было бы завести какой-то std::map<PixelMetric, int> и явно табличкой задать соответствие между метрикой и числами. Можно придумать и другие табличные варианты реализации функции.
Еще один пример:
QStringList ProFileEvaluator::Private::values(...)
{
...
else if (ver == QSysInfo::WV_NT)
ret = QLatin1String("WinNT");
else if (ver == QSysInfo::WV_2000)
ret = QLatin1String("Win2000");
else if (ver == QSysInfo::WV_2000) <<<=== 2003
ret = QLatin1String("Win2003");
else if (ver == QSysInfo::WV_XP)
ret = QLatin1String("WinXP");
...
}
В коде два раза сравниваем переменную ‘ver’ с константой WV_2000. Хороший пример, где самое место табличному методу. Например, такой метод мог бы выглядеть так:
struct {
QSysInfo::WinVersion; m_ver;
const char *m_str;
} Table_WinVersionToString[] = {
{ WV_Me, "WinMe" },
{ WV_95, "Win95" },
{ WV_98, "Win98" },
{ WV_NT, "WinNT" },
{ WV_2000, "Win2000" },
{ WV_2003, "Win2003" },
{ WV_XP, "WinXP" },
{ WV_VISTA,"WinVista" }
};
ret = QLatin1String("Unknown");
for (size_t i = 0; i != count_of(Table_WinVersionToString); ++i)
if (Table_WinVersionToString[i].m_ver == ver)
ret = QLatin1String(Table_WinVersionToString[i].m_str);
Конечно, это просто прототип, но он хорошо демонстрирует идею табличных методов. Согласитесь, что выявить в такой таблице ошибку намного проще.
Рекомендация. Не ленитесь написать функцию с использованием табличных методов. Да, это займет немного времени, но зато это окупится потом. Вам будет проще и быстрее добавлять новые условия, а вероятность ошибки при этом будет намного ниже.
3. Разное интересное
Поскольку Qt большая библиотека, то, несмотря на высокое качество, в ней можно встретить разнообразнейшие ошибки. Действует закон больших чисел. Размер *.cpp, *.h и аналогичных файлов проекта Qt составляет около 250 мегабайт. Как не маловероятна ошибка, в большом коде её встретить вполне реально. На основании других ошибок, которые я обнаружил в Qt, составить какие-то рекомендации сложно. Просто опишу некоторые ошибки, которые мне понравились.
QString decodeMSG(const MSG& msg)
{
...
int repCount = (lKeyData & 0xffff); // Bit 0-15
int scanCode = (lKeyData & 0xf0000) >> 16; // Bit 16-23
bool contextCode = (lKeyData && 0x20000000); // Bit 29
bool prevState = (lKeyData && 0x40000000); // Bit 30
bool transState = (lKeyData && 0x80000000); // Bit 31
...
}
Случайно используется оператор && вместо &. Обратите внимание, как полезно иметь в коде комментарии. Сразу становится понятно, что это действительно ошибка и как на самом деле должны обрабатываться биты.
Следующий пример будет на тему длинных выражений:
static ShiftResult shift(...)
{
...
qreal l = (orig->x1 - orig->x2)*(orig->x1 - orig->x2) +
(orig->y1 - orig->y2)*(orig->y1 - orig->y1) *
(orig->x3 - orig->x4)*(orig->x3 - orig->x4) +
(orig->y3 - orig->y4)*(orig->y3 - orig->y4);
...
}
Видите ошибку? Вот-вот, с ходу и не заметишь. Хорошо, подскажу. Беда вот здесь: «orig->y1 — orig->y1». Ещё меня третье умножение смущает, но возможно так и надо.
Да, еще вопрос. А у вас ведь тоже в программах вот такие вот блоки вычислений есть? Не пора ли попробовать статический анализатор кода PVS-Studio? Так, порекламировал. Пойдем дальше.
Использование неинициализированных переменных. Их можно найти в любом большом приложении:
PassRefPtr<Structure>
Structure::getterSetterTransition(Structure* structure)
{
...
RefPtr<Structure> transition = create(
structure->storedPrototype(), structure->typeInfo());
transition->m_propertyStorageCapacity = structure->m_propertyStorageCapacity;
transition->m_hasGetterSetterProperties = transition->m_hasGetterSetterProperties;
transition->m_hasNonEnumerableProperties = structure->m_hasNonEnumerableProperties;
transition->m_specificFunctionThrashCount = structure->m_specificFunctionThrashCount;
...
}
Здесь опять нужно дать подсказу, чтобы долго не мучить ваши глаза. Смотреть надо на инициализацию переменной «transition->m_hasGetterSetterProperties».
Уверен, почти каждый из нас, когда только начинал программировать, делал ошибку в духе:
const char *p = ...;
if (p == "12345")
И только потом приходило осознание, зачем нужны такие на первый взгляд странные функции, как strcmp(). К сожалению, язык Си++ настолько суров, что сделать такую ошибку можно и через много лет, будучи профессиональным разработчиком с опытом:
const TCHAR* getQueryName() const;
...
Query* MultiFieldQueryParser::parse(...)
{
...
if (q && (q->getQueryName() != _T("BooleanQuery") ...
...
}
Что бы еще показать. Вот, например, неправильно написанный обмен значений переменных.
bool qt_testCollision(...)
{
...
t=x1; x1=x2; x2=t;
t=y1; x1=y2; y2=t;
...
}
Это пример того, что можно ошибиться даже в очень простом коде. Так, пока ещё не было примеров на тему выхода за границы массива. Сейчас будет:
bool equals( class1* val1, class2* val2 ) const
{
...
size_t size = val1->size();
...
while ( --size >= 0 ){
if ( !comp(*itr1,*itr2) )
return false;
itr1++;
itr2++;
}
...
}
Условие «—size >= 0» всегда истинно, так как переменная size имеет беззнаковый тип. Если будут сравниваться одинаковые последовательности, то произойдет выход за границы массивов.
Можно продолжать и дальше. Надеюсь, вы как программисты понимаете, что ошибки в проекте такого объема описывать в одной статье нет никакой возможности. Поэтому последнее, на закуску:
STDMETHODIMP QEnumPins::QueryInterface(const IID &iid,void **out)
{
...
if (S_OK)
AddRef();
return hr;
}
Должно было быть что-то в духе «if (hr == S_OK)» или «if (SUCCEEDED(hr))». Макрос S_OK есть не что иное, как 0. Поэтому бяка с неправильным подсчетом количества ссылок здесь неизбежна.
Вместо заключения
Спасибо за внимание. Используйте статический анализ кода, и вы сможете сэкономить массу времени для более полезных дел, чем отладка и сопровождение кода.
Также я буду благодарен читателям, если вы пришлете мне интересные примеры ошибок, которые вы находили в своём или чужом коде и, для которых можно реализовать диагностические правила.
Библиографический список
- Андрей Карпов. Как уменьшить вероятность ошибки на этапе написания кода. Заметка N1. http://habrahabr.ru/blogs/cpp/115143/
- Андрей Карпов. Как уменьшить вероятность ошибки на этапе написания кода. Заметка N2. http://habrahabr.ru/blogs/cpp/116397/
- Макконнелл С. Совершенный код. Мастер-класс / Пер. с англ. — М.: Издательско-торговый дом «Русская Редакция»; СПб.: Питер, 2005. — 896 стр.: ил.
Третья причина смертности в Америке — медицинские ошибки. Официальная статистика Минздрава пугает не меньше: в России из-за врачебных ошибок ежегодно умирает семьдесят тысяч человек. К сожалению, такие ошибки — неотъемлемая часть медицины. Врачи — обычные люди, а людям свойственно ошибаться.
В книге «Неидеальная медицина» терапевт с 25-летним опытом Даниэль Офри предлагает свой взгляд на то, кто виноват и что делать, когда в больнице что-то идет не так. Что важно, Офри уверена: пациент может помочь врачам избежать опасных ошибок. Вот что, по мнению доктора Офри, может сделать каждый из нас, чтобы свести риск медошибки к минимуму.
Знать свой анамнез
Запишите все перенесенные операции и тяжелые заболевания. Перечислите аллергии. Перечень должен быть простым и четким. Исключение — сложные процедуры, например химиотерапия или пересадка органов. Тогда лучше описать мельчайшие подробности лечения.
Берите этот список на прием к каждому врачу. С ним доктор сразу увидит анамнез и поймет, на что у вас аллергия, а что категорически противопоказано из-за болезни.
Следить за списком лекарств
Если вы пьете лекарства, составьте еще один список и укажите точную дозировку. Часто врачи меняют назначения: вносите поправки в чек-лист. А лучше сложите таблетки в один пакет и всегда носите их на прием. Вот что говорит об этом доктор Офри:
«Думаю, этот метод очень эффективный. Другие врачи могут назначить моему пациенту дополнительные лекарства или вызванный на дом доктор может изменить дозировку. Об этом мне никак не узнать самой, а так все сразу понятно! Если вы принимаете пищевые, растительные добавки или безрецептурные препараты, тоже берите их на прием».
Мыть руки и просить медперсонал о том же
Чтобы предупредить нечаянные инфекции, мойте руки прямо в больнице. И следите, чтобы каждый медик делал то же, перед тем, как к вам прикасаться. Если понадобится, пошутите, сыронизируйте, но будьте настойчивы.
«Сделайте все возможное, чтобы не подпускать к себе кишащие микробами руки, пока они не будут вымыты с мылом или обработаны дезинфектантом».
Говорить с врачом напрямую
Снизить вероятность медицинской ошибки помогает общение. Рассказывайте врачу все, что считаете нужным. Если весь прием медик не отрывал глаз от компьютера, вы имеете полное право вежливо на это указать. Например, так: «Я понимаю, надо занести все в компьютер, но уделите мне минутку внимания, я быстро расскажу самое важное». Задавайте вопросы, уточняйте, выражайте сомнения.
«Когда медик говорит: «Мне кажется, у вас Х», спросите: «Почему вы так считаете?» Это поможет понять, насколько полученные данные убедили — или не убедили — врача. Затем спросите: «А это может быть что-то еще?» Так вы узнаете, что показала исключающая диагностика».
Просить близких о помощи
Мы не можем следить за тем, что творится вокруг, когда трудно дышать, выворачивает наизнанку или сломаны кости. Поэтому если состояние тяжелое, соберитесь с силами и попросите о помощи близкого человека. Такой человек может снизить вероятность врачебной ошибки, расспрашивая медиков вместо вас и записывая все, что происходит. Какой диагноз? Для чего нужно это лекарство? На какую побочку обратить внимание? О чем говорит результат сегодняшнего анализа? Зачем назначили МРТ? И так далее.
Задавая вопросы и конспектируя, человек зафиксирует диагностику и детали лечения. А если что-то пойдет не так, его записи помогут воссоздать хронологии событий.
«Сам факт, что врачи разъясняют каждый шаг, поможет избежать распространенных ошибок. Скорее всего, в отделении вашего помощника будут считать самым надоедливым родственником, но это нормально. Всегда можно оставить коробку печенья, чтобы успокоить затронутое эго».
Как могут предупредить ошибку сами врачи? Что делать, если ошибка уже случилась? Как пациентов защищает закон? Ответы на эти вопросы вы найдете в книге «Неидеальная медицина. Кто виноват, когда в больнице что-то идет не так, и как пациенту при этом не пострадать». Заказать ее можно по ссылке.
Расскажите всем, какую интересную статью вы нашли!
24 августа 2021 г.
Ошибки на работе могут помочь вам улучшить собственные процессы и укрепить доверие коллег. Это также показывает членам вашей команды, как вы справляетесь с ошибками, демонстрируя свой профессионализм. Изучение шагов по устранению ошибок в работе с вашей командой и выявление основной причины этих ошибок может помочь вам избежать подобных ошибок в будущем. В этой статье мы обсудим, что такое ошибки на работе, что делать, если вы допустили ошибку на работе, и советы по предотвращению ошибок в будущем.
Какие ошибки в работе?
Ошибки в работе варьируются от технических ошибок до досадных оплошностей, которые включают в себя такие вещи, как:
-
Делать грамматические или орфографические ошибки в презентации или важном электронном письме
-
Случайная отправка личного сообщения не тому получателю в чате или электронной почте
-
Забывание перепроверить цифры, которые оказались неверными во время презентации
-
Двойное бронирование клиентов в вашем календаре
-
Оговорка во время встречи
-
Отсутствие дедлайна для проекта
Что важно и что люди обычно помнят, так это то, как вы реагируете на ошибку после того, как вы ее совершили.
Что делать, если вы допустили ошибку на работе
Если вы допустили ошибку на работе, вы можете предпринять определенные шаги, чтобы уменьшить последствия и предотвратить ее повторение. Как только вы поймете, что допустили ошибку, вот что вы можете сделать:
-
Признайте ошибку.
-
Принесите извинения.
-
Найти решение.
-
Планируйте, что делать в следующий раз.
-
Создайте позитивную модель работы.
1. Признайте ошибку
Вполне естественно испытывать некоторое разочарование или смущение после совершения ошибки на работе, но также важно двигаться вперед и направлять свою энергию на то, чтобы правильно реагировать. Признание ошибки не только себе, но и другим подчеркивает ваш уровень профессионализма. Когда вы говорите другим о своей ошибке, будьте прямолинейны и прямолинейны.
2. Принесите извинения
Принося искренние извинения всем, кого затронула ваша ошибка, вы достигаете нескольких целей. Это показывает, что вы искренне сожалеете об ошибке и берете на себя ответственность за нее. Это также демонстрирует, что вы уважаете людей, которые пострадали. Если в связи с ошибкой остались какие-то негативные чувства, искреннее извинение должно помочь восстановить позитивные отношения.
3. Найдите решение
Затем пришло время исправить любые проблемы, возникшие в результате ошибки. Определите решение для устранения любых проблем, возникших из-за ошибки, и сообщите о своем решении всем ключевым заинтересованным сторонам. Это может означать работу в нерабочее время или просто личное извинение перед клиентом. Если ошибка повлияла на кого-то из коллег и в результате у них появилась дополнительная работа, поищите способы облегчить их нагрузку. Это должно помочь укрепить доверие в команде, поскольку вы демонстрируете, что берете на себя полную ответственность за ошибки.
4. Планируйте, что делать в следующий раз
Оцените, что вы могли бы сделать по-другому в следующий раз, чтобы ошибка не повторилась. Это означает выявление основной причины ошибки. Например, вы могли торопиться, чтобы уложиться в срок, или работали над несколькими проектами одновременно.
Что бы ни вызвало ошибку, определите проблему и устраните ее. Возможно, вам придется начать работу над проектом раньше, чтобы уложиться в срок, или выделить время, чтобы спокойно работать только над одним проектом. Сообщите решение своим менеджерам, чтобы они были уверены, что вы предприняли шаги, чтобы избежать подобных ошибок в будущем.
5. Создайте позитивную модель работы
Как только вы устраните ошибку, предпримите действия, которые покажут, что вы улучшаете свой стиль работы. Придерживайтесь новых правил, которые вы установили для себя, чтобы не совершать тех же ошибок. Если вы найдете метод, который работает особенно хорошо, поделитесь им со своими коллегами, которым он тоже может быть полезен. Этот пример побуждает к большему общению, что может быть полезно для определения способов снижения вероятности ошибок в будущем.
Советы, как снизить вероятность ошибок в работе
В дополнение к признанию ошибок, извинениям и их устранению, также важно быть активным в снижении вероятности очередной ошибки. Вот несколько советов, которые вы можете использовать, чтобы уменьшить вероятность совершения большего количества ошибок на работе:
-
Уделите своей работе все свое внимание в лучшие времена. В зависимости от вашего личного уровня энергии рекомендуется планировать свой день таким образом, чтобы вы работали над своими наиболее приоритетными задачами, когда вы чувствуете себя наиболее энергичным. Другая стратегия заключается в том, чтобы работать над этими проектами в то время дня, когда вас меньше всего будут беспокоить другие.
-
Перепроверьте все сообщения и презентации. Чем больше вы привыкнете проверять наличие ошибок, прежде чем нажимать кнопку «Отправить» в чате или электронной почте, или распечатывать документы для прочтения другими на собрании, тем больше вы будете уверены в том, что ваши сообщения безошибочны.
-
Создавайте контрольные списки. Контрольный список — это самый простой способ избежать ошибок, особенно при повторяющихся задачах. Когда у вас есть процесс, вам просто нужно выполнять определенные шаги каждый раз, когда вы выполняете эту задачу.
-
Просмотрите свою работу. Каждый раз, когда вы завершаете задачу или процесс — особенно работу с высоким приоритетом — проверяйте ее на наличие ошибок. Если возможно, сделайте перерыв в работе над проектом, прежде чем просмотреть его в последний раз. Это поможет вам более успешно выявлять ошибки.
-
Делайте перерывы. Делайте перерыв в работе каждые 90 минут или два часа, чтобы повысить вероятность безошибочной работы. Постарайтесь отдохнуть от своего рабочего места, чтобы полностью отвлечься от своих обязанностей.
-
Устраните отвлекающие факторы. Когда вы работаете над высокоприоритетными задачами, отложите телефон, закройте электронную почту и ненужные браузеры и переведите все рабочие мессенджеры в режим «не беспокоить». Держите под рукой ручку и блокнот, чтобы записывать любые посторонние мысли, которые помогут вам сосредоточиться на задаче.
-
Задавать вопросы. Когда вы начинаете новую работу или начинаете новый проект, задавайте столько вопросов, сколько вам нужно, чтобы полностью понять свою роль. Узнав больше о своих обязанностях и шагах, которые необходимо предпринять, вы сможете исключить возможность совершения ошибок.
-
Составьте подробный график. Чтобы убедиться, что вы укладываетесь в сроки, создайте календарь, в котором указано все, что вам нужно сделать в течение дня, недели и месяца. Вы даже можете планировать свои часы, чтобы тратить нужное количество времени на каждую задачу.
ПОМЕХОУСТОЙЧИВОЕ КОДИРОВАНИЕ Способ снижения вероятности ошибок приеме – введение избыточности в передаваемую информацию. Варианты введения избыточности: — помехоустойчивое кодирование; — многократная передача информации; — одновременная передача информации по нескольким параллельно работающим каналам. Помехоустойчивое кодирование — самый оптимальный способ. Применяется для обнаружения и исправления ошибок в сигнале при его передаче по каналам связи. Предполагает введение в передаваемое сообщение проверочных разрядов, формируемых в устройствах защиты от ошибок: кодере – на передающей стороне, декодере – на приемной. Избыточность позволяет отличить разрешенную и запрещенную (искаженную за счет ошибок) комбинации приеме.
Помехоустойчивый код характеризуется тройкой чисел (n, k, dmin): n – общее число разрядов кодовой комбинации (n = k + r); k – число информационных разрядов, r – число проверочных разрядов. Соответственно, – количество возможных кодовых комбинаций; – количество разрешенных кодовых комбинаций (N M); Расстояние между двумя кодовыми комбинациями – расстояние по Хэммингу – d. Минимальное расстояние по Хэммингу для данной совокупности разрешенных кодовых комбинаций dmin называют кодовым расстоянием dk
Связь исправляющей способности кода с dmin Количество обнаруживаемых ошибок: где – кратность ошибки (число искажённых разрядов). Если произошла ошибка в одном разряде, то она называется однократной, в двух разрядах – двукратной и т. д. Количество исправляемых ошибок: Количество единиц в коде называют весом кодовой комбинации. Например, кодовая комбинация 1100101: значность n = 7, вес V = 4.
Для указания разрядов кодовой комбинации, в которых произошли ошибки, используется понятие вектор ошибки Вектор ошибки n-разрядного кода – это n-разрядная комбинация, где единицы указывают положение разрядов искажённых символов. Например, = 0101, значит искажены символы во втором и четвертом разрядах. Вес вектора ошибки характеризует кратность ошибки Исходная неискажённая комбинация состоит из суммы по модулю 2 искажённой комбинации и вектора ошибки: a = a’
Геометрическое представление кодов Представление кодов с помощью матриц САМОСТОЯТЕЛЬНО
Пример Код задан производящей (генераторной) матрицей: Параметры кода: n – длина кодовой комбинации (количество столбцов в матрице), n = 6. Каждая строка матрицы [G] состоит из k информационных символов и r проверочных:
Разбиваем [G] на 2 части так, чтобы слева осталась единичная матрица; k равно количеству столбцов в этой единичной матрице (k = 3). a 1 a 2 a 3 b 4 b 5 b 6 – разряды кодовых комбинаций: a – информационные b – проверочные , Ek R Ek – единичная подматрица k-го (3 -го) порядка; R – проверочная подматрица. Определим N – количество возможных кодовых комбинаций длиной n: Определим М – количество разрешенных кодовых комбинаций:
Построим проверочную матрицу. Она состоит из двух подматриц: RТ Еn-k где RТ – транспонированная матрица R; Еn-k – единичная подматрица порядка n-k. Запишем уравнения проверок. В уравнение входят только те разряды, которым соответствуют единицы в соответствующих строках матрицы [H]:
Составим таблицу исправлений (синдромов): Синдром S 1 Конфигурация синдрома Ошибочная позиция S 2 S 3 101 110 011 a 2 a 3 Кодовое расстояние dmin равно числу единиц в строке матрицы [G] с минимальным весом: dmin = 3. Количество обнаруживаемых ошибок (кратность ошибки) определяется из неравенства: , где – кратность ошибки. 2. Таким образом, если в кодовой комбинации произошло одновременно две ошибки, то код позволит их обнаружить.
Количество исправляемых ошибок: 1. Таким образом, код может исправить только одиночную ошибку (ошибку в одном разряде).
Схема кодера Суммирование и вычитание по модулю два – операции эквивалентные. Поэтому алгоритм формирования контрольных символов можно получить, переписав уравнения проверок: . Кодер состоит из n-разрядного универсального регистра сдвига и n – k = r сумматоров по модулю два (в нашем случае сумматора три). В регистр можно одновременно записать k информационных символов (в нашем случае три). После этого формируются контрольные символы, согласно уравнениям проверок.
Схема кодера
Схема декодера, обнаруживающего ошибки
Схема декодера, обнаруживающего ошибки, также составляется по уравнениям проверок. Регистр также n-разрядный. Сумматоров столько же, сколько и в схеме кодера, только у каждого из них на один вход больше. Если ошибок не было, то уравнения проверок выполняются и на входах и выходе схемы «ИЛИ» – нули. . Если хотя бы в одной позиции была ошибка, то хотя бы на одном из входов и выходе появится « 1» , что сигнализирует об ошибке.
Схема декодера, исправляющего одиночную ошибку
Схема декодера, исправляющего одиночную ошибку Кодовые комбинации из канала в последовательном режиме записываются в регистр Рег. 1. С помощью трех сумматоров по модулю два осуществляется проверка на наличие ошибки в информационных разрядах, согласно уравнениям проверок. В зависимости от номера разряда, в котором произошла ошибка, на выходах 1 -го, 2 -го и 3 -го сумматоров будут различные кодовые комбинации. Если ошибка произошла в разряде a 1, то на выходах трех сумматоров появится комбинация 101, т. к. a 1 подается на первый и третий сумматоры; если ошибка в разряде a 2 – комбинация 110 (a 2 подается на первый и второй сумматоры); если ошибка в разряде a 3 – комбинация 011 (a 3 подается на второй и третий сумматоры). Если ошибки нет, то кодовая комбинация будет 000. Эти кодовые комбинации называются синдромами ошибок.
Схема декодера, исправляющего одиночную ошибку Анализ синдромов осуществляет дешифратор синдромов (ДС). Он имеет три выхода. При наличии ошибки « 1» появится на том выходе, в какой позиции произошла ошибка. На остальных выходах – нули. Ошибка исправляется следующим образом. Для исправления ошибки нужно знать номер позиции, в которой она произошла. Как следует из таблицы истинности для двухвходового сумматора по модулю «два» , если на одном входе « 0» , то для второго входа сумматор работает как повторитель. Если же на одном из входов « 1» , то для второго входа он работает как инвертор. Очевидно, что для того, чтобы исправить ошибку в двоичном коде, надо ошибочный разряд инвертировать. Например, произошла ошибка в разряде a 1. Синдром ошибки будет 101, на выходе дешифратора синдромов – 100. Поэтому второй и третий разряды инвертированы не будут, а первый разряд будет инвертирован. Таким образом, в регистр Рег. 2 запишется исправленная информационная кодовая комбинация, которую примет получатель информации.