Проверяя много лет различные C/C++ проекты, я заявляю: самая неудачная и опасная функция — memset(). При использовании функции memset() допускают наибольшее количество ошибок, в сравнении с использованием других функций. Я понимаю, что мой вывод вряд ли потрясёт основы мироздания или невероятно ценен. Однако я думаю, читателям будет интересно узнать, почему я пришел к такому заключению.
Здравствуйте
Меня зовут Андрей Карпов. Я совмещаю много должностей и занятий. Но основное, что я делаю, это рассказываю программистам о пользе, которую может приносить статический анализ кода. Естественно я делаю это с корыстной целью, пытаясь заинтересовать читателей анализатором PVS-Studio. Впрочем, это не уменьшает интересность и полезность моих статей.
Единственный вид рекламы, который может пробить чешуйчатую броню программистов, это демонстрация примеров ошибок, которые умеет находить PVS-Studio. С этой целью я проверяю большое количество открытых проектов и пишу статьи о результатах исследований. Всеобщая выгода. Открытые проекты становятся немного лучше, а у нашей компании появляются новые клиенты.
Сейчас станет понятно, к чему я веду. Занимаясь проверкой открытых проектов, я накопил большую базу примеров ошибок. И теперь, основываясь на ней, могу находить интересные закономерности.
Например, одним из интересных наблюдений было, что программисты допускают ошибки при Copy-Paste чаще всего в самом конце. На эту тему предлагаю вниманию статью "Эффект последней строки".
Новое наблюдение
Теперь у меня есть ещё одно интересное наблюдение. Используя те или иные функции, программисты могут допускать ошибки. При этом вероятность допущения ошибки зависит от используемой функции. Другими словами, какие-то функции провоцируют ошибки, а какие-то — нет.
Так вот, я готов назвать функцию, при использовании которой есть наибольшая вероятность сесть в лужу.
Итак, победитель на глючность — функция memset!
Как так получилось — сложно сказать. Видимо у неё неудачный интерфейс. Плюс само её использование достаточно трудоемко и легко ошибиться, вычисляя значения фактических аргументов.
Почетное второе место занимает функция printf() и её разновидности. Думаю, это никого не удивит. Про опасность функции printf() не писал только ленивый. Возможно из-за общеизвестности связанных с printf() проблем, она и попала на второе место.
Всего у меня в базе 9055 ошибок. Это те ошибки, которые умеет находить анализатор PVS-Studio. Понятно, что он умеет далеко не всё. Однако большое количество найденных ошибок позволяет мне быть уверенным в своих выводах. Так вот, я посчитал, что с использование функции memset() связано 329 ошибок.
Итого, около 3,6% ошибок в базе связано с функцией memset(). Это много!
Примеры
Давайте рассмотрим некоторые типовые примеры ошибок. Рассматривая их, я думаю, вы согласитесь, что с функцией memset() что-то не так. Она притягивает зло.
Для начала освежим в памяти, как объявлена эта функция:
void * memset ( void * ptr, int value, size_t num );
- ptr — Pointer to the block of memory to fill.
- value — Value to be set. The value is passed as an int, but the function fills the block of memory using the unsigned char conversion of this value.
- num — Number of bytes to be set to the value. 'size_t' is an unsigned integral type.
Пример N1 (проект ReactOS)
void
Mapdesc::identify( REAL dest[MAXCOORDS][MAXCOORDS] )
{
memset( dest, 0, sizeof( dest ) );
for( int i=0; i != hcoords; i++ )
dest[i][i] = 1.0;
}
Ошибка в том, что в C и в C++ нельзя передавать массивы по значению (подробнее). Аргумент 'dest' является не чем иным как обыкновенным указателем. Поэтому оператор sizeof() вычисляет размер указателя, а не массива.
Вроде memset() и не виноват. Но с другой стороны, эта функция заполнит нулями только 4 или 8 байт (экзотические архитектуры не в счёт). Ошибка есть и произошла она при вызове функции memset().
Пример N2 (проект Wolfenstein 3D)
typedef struct cvar_s {
char *name;
...
struct cvar_s *hashNext;
} cvar_t;
void Cvar_Restart_f( void ) {
cvar_t *var;
...
memset( var, 0, sizeof( var ) );
...
}
Похожая ошибка. Допущена она скорее всего по невнимательности. Переменная 'var' является указателем. А значит memset() вновь обнулит только часть структуры. На практике будет обнулён только член 'name'.
Пример N3 (проект SMTP Client)
void MD5::finalize () {
...
uint1 buffer[64];
...
// Zeroize sensitive information
memset (buffer, 0, sizeof(*buffer));
...
}
Очень распространенный паттерн ошибки, про который тем не менее осведомлено мало программистов. Дело в том, что функция memset() будет удалена компилятором. Буфер после вызова memset() более не используется. И компилятор в целях оптимизации удаляет вызов функции. С точки зрения языка C/C++ это не оказывает никакого влияния на поведение программы. Это действительно так. То, что приватная информация останется в памяти, никак не повлияет на работу программы.
Это не ошибка компилятора. И это не мои фантазии. Компилятор действительно удаляет вызовы memset(). Каждый раз, когда я описываю эту ошибку уязвимости, я получаю письма, где со мной начинают спорить. Я уже устал отвечать на эти письма. Поэтому прошу всех сомневающихся, прежде чем начинать дискуссию, внимательно познакомиться со следующими материалами:
- Документация PVS-Studio. V597.
- Mansour Moufid. Zero and forget — caveats of zeroing memory in C.
- SEI CERT C Coding Standard. MSC06-C.
- Перезаписывать память — зачем?
Пример N4 (проект Notepad++)
#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
...
DockingManager::DockingManager()
{
...
memset(_iContMap, -1, CONT_MAP_MAX);
...
}
Часто забывают, что третий аргумент функции memset() это не количество элементов, а размер буфера в байтах. Именно так и произошло в приведенном выше фрагменте кода. В результате, заполнена будет только четверть буфера (при условии, что размер типа 'int' равен 4 байтам).
Пример N5 (проект Newton Game Dynamics)
dgCollisionCompoundBreakable::dgCollisionCompoundBreakable(....)
{
...
dgInt32 faceOffsetHitogram[256];
dgSubMesh* mainSegmenst[256];
...
memset(faceOffsetHitogram, 0, sizeof(faceOffsetHitogram));
memset(mainSegmenst, 0, sizeof(faceOffsetHitogram));
...
}
Имеем дело с опечаткой. Скорее всего кто-то поленился два раза набирать вызов функции memset(). Продублировали строчку. В одном месте заменили 'faceOffsetHitogram' на 'mainSegmenst', а в другом забыли.
Получается, что sizeof() вычисляет размер не того массива, который заполняется нулями. Вроде как функция memset() никак не виновата. Но неправильно будет работать именно она.
Пример N6 (проект CxImage)
static jpc_enc_tcmpt_t *tcmpt_create(....)
{
...
memset(tcmpt->stepsizes, 0,
sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t)));
...
}
Здесь присутствует лишний оператор sizeof(). Правильно размер вычислять так:
tcmpt->numstepsizes * sizeof(uint_fast16_t)
Но написали лишний sizeof() и получилась глупость:
sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t))
Здесь оператор sizeof() вычисляет размер типа size_t. Именно такой тип имеет выражение.
Я знаю, что хочется возразить. Уже не первый раз ошибка связана с оператором sizeof(). Т.е. программист ошибается, вычисляя размер буфера. Однако причиной этих ошибок всё равно является функция memset(). Она устроена так, что приходится делать эти различные вычисления, в которых так легко ошибиться.
Пример N7 (проект WinSCP)
TForm * __fastcall TMessageForm::Create(....)
{
....
LOGFONT AFont;
....
memset(&AFont, sizeof(AFont), 0);
....
}
Функция memset() всеядна. Поэтому спокойно отнесётся, если вы перепутаете 2 и 3 аргумент. Именно так здесь и произошло. Эта функция заполняет 0 байт.
Пример N8 (проект Multi Theft Auto)
А вот ещё одна аналогичная ошибка. Кажется, разработчики Win32 API пошутили, когда создали вот такой макрос:
#define RtlFillMemory(Destination,Length,Fill) memset((Destination),(Fill),(Length))
По смыслу это альтернатива memset(). Но надо быть внимательным. Обратите внимание, что меняется местами 2 и 3 аргумент.
Когда начинают использовать RtlFillMemory(), то относятся к ней как к memset(). И думают, что параметры у них совпадают. В результате возникают ошибки.
#define FillMemory RtlFillMemory
LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs )
{
....
PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol ;
FillMemory ( pSym , NULL , SYM_BUFF_SIZE ) ;
....
}
NULL есть ни что иное, как 0. Поэтому функция memset() заполнила 0 байт.
Пример N9 (проект IPP Samples)
Я думаю, вы понимаете, что примеры ошибок я могу приводить долго. Но это не очень интересно, так как они будут весьма однообразны и похожи на уже показанные в статье. Но ещё один случай давайте рассмотрим.
Хотя некоторые из приведенных выше ошибок были найдены в кода на языке C++, к C++ они никакого отношения не имеют. Другими словами, это ошибки возникают при программировании в стиле языка C.
Следующая ошибка связана как раз с неправильным использованием memset() в C++ программе. Пример достаточно длинный, поэтому можете в него не всматриваться. Прочитайте описание ниже и всё станет понятно.
class _MediaDataEx {
...
virtual bool TryStrongCasting(
pDynamicCastFunction pCandidateFunction) const;
virtual bool TryWeakCasting(
pDynamicCastFunction pCandidateFunction) const;
};
Status VC1Splitter::Init(SplitterParams& rInit)
{
MediaDataEx::_MediaDataEx *m_stCodes;
...
m_stCodes = (MediaDataEx::_MediaDataEx *)
ippsMalloc_8u(START_CODE_NUMBER*2*sizeof(Ipp32s)+
sizeof(MediaDataEx::_MediaDataEx));
...
memset(m_stCodes, 0,
(START_CODE_NUMBER*2*sizeof(Ipp32s)+
sizeof(MediaDataEx::_MediaDataEx)));
...
}
Функция memset() используется для инициализации массива, состоящих из объектов класса. Самая большая беда в том, что класс содержит виртуальные функции. Соответственно функция memset() не только обнуляет поля класса, но и указатель на таблицу виртуальных методов (vptr). К чему это приведёт неизвестно. Но ничего хорошего в этом точно нет. Нельзя так обращаться с классами.
Заключение
Как видите, функция memset() имеет крайне неудачный интерфейс. В результате, функция memset() больше всех остальных провоцирует появление ошибок. Будьте бдительны!
Я не готов сейчас сказать, как можно использовать моё наблюдение. Но надеюсь, вам было интересно познакомиться с этой заметкой. Возможно теперь, используя memset(), вы будете более внимательны. И это уже хорошо.
Спасибо всем за внимание и подписывайтесь на мой твиттер @Code_Analysis.
Ещё я веду ресурс C++Hints, где делюсь разными полезными советами, которые приходят мне в голову. Подписывайтесь.
Комментарии (83)
a_batyr
03.12.2015 12:12-14Пример N1 оператор sizeof() вычиялет размер указателя, а не массива
Пример N2 Переменная 'var' является указателем
Пример N3 компилятор в целях оптимизации удаляет вызов функции
Пример N4 третий аргумент функции memset() это не количество элементов, а размер буфера в байтах
Пример N5 Имеем дело с опечаткой
Пример N6 Здесь присутствует лишний оператор sizeof()
Пример N7 memset(&AFont, sizeof(AFont), 0);
Пример N8 разработчики Win32 API пошутили, когда создали вот такой макрос
Пример N9 Нельзя так обращаться с классами.
Во всех абсолютно примерах приведены ошибки программистов по невнимательности или по незнанию. Из того, что кто-то не умеет программировать делается просто чудесный выводфункция memset() имеет крайне неудачный интерфейс
Имея молоток можно отбить себе палец.
Я думал будет что-то по существу, а так статья ни о чём.Andrey2008
03.12.2015 12:22+17Из того, что кто-то не умеет программировать делается просто чудесный вывод
Эти кто-то, разработчики таких проектов как OpenCV, CoreCLR, UnrealEngine4, Haiku, Qt, Chromium,…a_batyr
03.12.2015 12:27-5Люди учатся на ошибках. Если вы падали на велосипеде, когда учились ездить, это не делает его самым опасным транспортом.
Indexator
04.12.2015 05:44+2Если вы падали на велосипеде, когда учились ездить,
это не значит, что вы не упадете на нем, даже если научились на нем ездить.Muzzy0
08.12.2015 13:32Я бы сказал — если вы научились на нём ездить, то вы с вероятностью 99.(9)% с него упадёте. Самоуверенность и закон Мерфи делают своё дело.
Также справедливо будет другое утверждение: большинство упавших с велосипеда умеют на нём ездить :)
a_batyr
03.12.2015 13:05+3Я в принципе-то не оспаривал тезисов статьи, вот к примеру тут они раскрыты хорошо.
Andrey2008
03.12.2015 13:09Спасибо. Интересная ссылка.
a_batyr
03.12.2015 13:14+2Вы тоже полезную ссылку на printf привели. А вашу статью я бы дополнил полезными примерами про:
1. memset can silently set the wrong value
2. memset can silently set an invalid value
3. memset is overkill when used on a string
4. memset can silently attack the wrong memory
5. memset must not be used on a struct nor on a class
и тогда бы тезис статьи был раскрыт по существу.Andrey2008
03.12.2015 13:16+1Ну не каждая статья получается удачной. :)
a_batyr
03.12.2015 13:22+4Если вы не будете писать дополнение, я могу написать перевод приведённой ссылки
xaizek
03.12.2015 16:05+5Если будете переводить, обязательно исправьте ошибки в самом начале, а то я даже не знаю стоит ли читать дальше шапки, в которой 4 из 7 примеров неправильные. Вот они:
char p1[25] = "" ; // will set the first character to 0 (нет, не только первый) // ... wchar_t p3[25] = L"" ; // will set the first character to 0 (нет, не только первый) // ... int p5[37] = {-1} ; // will set the 37 values to -1 (нет, только первый) unsigned int p6[10] = {89} ; // will set the 10 values to 89 (нет, только первый)
a_batyr
04.12.2015 15:31+2Спасибо. Начал исследовать вопрос корректности статьи, тоже нашёл ошибки в примерах, поэтому перевода не будет, слишком много нюансов, на которые я сам не знаю ответ, но дополнение и некоторое исследование постараюсь сделать в ближайшее время.
tbl
04.12.2015 11:15+1> memset must not be used on a struct
Как в C занулить структуру?
Помню, в проекте GCompris была проблема с тем, что на определенных архитектурах (amd64, например) мыша не работала.
Путем копания в исходниках выяснилось, что в GcomprisProperties были добавлены какие-то поля (defaultcursor, nosursor), которые забыли добавить в функцию инициализации.
По счастливой случайности после вызова malloc(sizeof (GcomprisProperties)) для x86 в nocursor был 0, и оно нормально работало. А в amd64 там был какой-то мусор.
Проблема решилась заменой malloc на calloc. А если бы в дальнейшем ее потребовалось занулить, то наверняка бы использовали memset.
Т.к. не всегда при добавлении полей в структуры, программист пробегает по всему коду, ища все её использования и проверяет инициализацию. В языках с конструкторами (C++) это было бы проще, там всё локализовано.tbl
04.12.2015 11:20Точнее, проблема решилась с принудительной инициализацией этих полей, и на будущее в предотвращение этих эксцессов заменой malloc на calloc.
VioletGiraffe
03.12.2015 12:29+2Я бы сказал, что memcpy даёт на один вариант выстрела в ногу больше, чем memset.
Andrey2008
03.12.2015 12:30+1Да, с memcpy() тоже очень опасная функция. Я её смотрел. Но победитель всё равно memset().
kyb27
03.12.2015 13:31Интересный продукт эта ваша студия, но сообщения об ошибках невнятные.
Без толмача не обойтись.Andrey2008
03.12.2015 13:32+2Но ведь есть отличная, удобная документация с примерами! Достаточно нажать в окне на код ошибки.
allter
03.12.2015 14:18+5Не согласен, что дело в функции memset. Исходя из приведённых примеров, только в примере №7 можно с натяжкой назвать причиной ошибки кривой интерфейс функции. А какая альтернатива? был бы другой порядок аргументов — также путались бы. А именование фактических параметров функций в конце 60х в языкостроении не было распространено (да и сейчас можно по пальцам руки, наверное, пересчитать такие языки). Все остальные — ошибки связанные с управлением памятью (в широком смысле).
Как говорится, в программировании на Cи всего 1 существенная трудность — управление памятью и ошибки на единицу. :)
nerudo
03.12.2015 15:01-1Ага. Неправильно используется sizeof, а виноват memset.
Andrey2008
03.12.2015 16:02Да, но ведь этот sizeof потребовался для memset().
nerudo
03.12.2015 17:18+11А memset() потребовался для main() — вот где корень зла!
Понятно, что функция напрямую работающая с памятью потенциально опасна и на нее все грехи свалить намного проще, чем на безобидную функцию, возвращающую результат вычислений или что-то подобное. Но корень зла-то именно в некорректном использовании sizeof(), который, наверняка, в вашей статистике встретится не только в контексте memset(), но и во множестве других мест.bigfatbrowncat
03.12.2015 20:34-2Понятно, что функция напрямую работающая с памятью потенциально опасна
… и люди, пришедшие к этому выводу изобрели много языков, в которых такие функции отсутствуют в принципе.
Программировать на Си, не влезая напрямую в память, можно (хотя, конечно, только довольно простые программы). Но memset и memcpy — две первых функции, за которые вынужденно берутся новички (ранее не имевшие дело с памятью напрямую). Вот и получается, что они — опасные. А вместе с ними и printf(str).
samo-delkin
04.12.2015 01:54-1Это ряд функций для работы с ПАМЯТЬЮ. Поэтому у них и приставка mem.
И sizeof вычисляет не размер типа в 6-ом примере
Здесь оператор sizeof() вычисляет размер типа size_t. Именно такой тип имеет выражение.
а размер получаемого значения.
printf("%d\n", sizeof "ab"); printf("%d\n", sizeof 1); printf("%d\n", sizeof 1.0f); printf("%d\n", sizeof 1.0); printf("%d\n", sizeof 1.0L);
Просто кое-кто стандарт C не читал и пытается делать выводы из знаний по C++.
И вот эти разработчики упомянутые — тоже. Как можно ошибиться при использовании memset()? Только если точно не знаешь, как она работает и для чего вообще делалась.mayorovp
04.12.2015 06:09И sizeof вычисляет не размер типа в 6-ом примере а размер получаемого значения.
Чем размер значения отличается от размера его типа?samo-delkin
04.12.2015 07:26Чем размер значения отличается от размера его типа?
Не у всех типов есть размер.
#include <stdio.h> int main(void) { printf("%d\n", sizeof (main)); return 0; }
[guest@localhost c]$ .ansi t.c -o t t.c: В функции «main»: t.c:6:27: предупреждение: недопустимое применение «sizeof» к типу функции [-Wpedantic] printf("%d\n", sizeof main); ^ [guest@localhost c]$ ./t 1 [guest@localhost c]$
samo-delkin
04.12.2015 07:31Повторяю код (не успел там стереть скобки, заблокировался коммент).
#include <stdio.h> int main(void) { printf("%d\n", sizeof main); return 0; }
[guest@localhost c]$ .ansi t.c -o t t.c: В функции «main»: t.c:6:27: предупреждение: недопустимое применение «sizeof» к типу функции [-Wpedantic] printf("%d\n", sizeof main); ^ [guest@localhost c]$ ./t 1 [guest@localhost c]$
mayorovp
04.12.2015 08:34Спасибо, я знаю о том что не у всех типов есть размер.
Но все-таки, чем размер значения отличается от размера его типа?samo-delkin
04.12.2015 08:41-1У типов нет размеров, размеры есть у значений. И вот этот код я тебе не просто так привёл. У типа функции нет размера, однако возвращается какое-то значение, у которого есть размер. А какого оно типа?
mayorovp
04.12.2015 08:52У типов нет размеров, размеры есть у значений.
В таком случае что вообще означает запись видаsizeof(int)
? Что это, если не размер типа int?
У типа функции нет размера, однако возвращается какое-то значение, у которого есть размер. А какого оно типа?
Возвращается кем? Функция main как бы возвращает int, оператор sizeof — size_t.
Или вы про тип значения main, размер которого измеряет sizeof в вашем коде? Тогда этоint()
И да, у типа функции, с точки зрения gcc при ваших настройках компилятора — очень даже есть размер. Это 1 байт, как вам и написала программа.samo-delkin
04.12.2015 09:05В таком случае что вообще означает запись вида sizeof(int)? Что это, если не размер типа int?
Он по значению вычисляет; какое значение туда поместится, его размер он и возвращает. Длина участка памяти в байтах.
И да, у типа функции, с точки зрения gcc при ваших настройках компилятора — очень даже есть размер. Это 1 байт, как вам и написала программа.
А что это за один байт?
Мои настройки просты, .ansi — это просто
[guest@localhost ~]$ alias .ansi alias .ansi='gcc -ansi -pedantic -Wall' [guest@localhost ~]$
то есть соответствие стандарту C89 с выдачей предупреждений при его нарушении.mayorovp
04.12.2015 09:59А что это за один байт?
Спросите лучше у разработчиков gcc, какой именно байт они имели в виду :)
Он по значению вычисляет; какое значение туда поместится, его размер он и возвращает. Длина участка памяти в байтах.
Так все-таки, чем отличается размер значения от размера типа?samo-delkin
04.12.2015 14:30Спросите лучше у разработчиков gcc
Код компилятора открыт. Скорее всего, это размер значения типа void.
Ogi
03.12.2015 16:31+8Получается, что sizeof() вычисляет размер не того массива, который заполняется нулями. Вроде как функция memset() никак не виновата. Но неправильно будет работать именно она.
Теперь всегда буду винить в своих опечатках memset, даже если я при этом её не использую и вообще пишу на совсем другом языке с автоматическим управлением памятью.
Amomum
03.12.2015 18:50+4Скажите, а какой следовало бы сделать интерфейс у memset, чтобы ошибок стало меньше? Мне кроме шаблонной версии (чтобы размер не в байтах принимать) как-то ничего в голову сходу не приходит.
bigfatbrowncat
03.12.2015 20:39Позвольте предложить идею вместо автора статьи.
Мне кажется, эта функция — результат плохого проектирования языка. Учитывая регулярную необходимость обнулять массивы не только при инициализации, можно было сделать для этого некоторое синтаксическое средство. Например, разрешить запись вида arr = { 0 } в любой точке кода (пусть она генерирует правильный вызов memset).
Хотя учитывая «родовую травму» Си, в котором длина массива — фантомное понятие, живущее только в голове разработчика, даже с этим будут проблемы… Так что memset — это своего рода плата за " int[] = int* "Amomum
03.12.2015 21:03+2Боюсь, что язык уже не удастся спроектировать заново и изменить поведение для массивов уже слишком сложно. Но может быть можно как-то выкрутиться с тем, что есть?
bigfatbrowncat
04.12.2015 00:40Ну тут же уже кто-то предлагал — использовать C++.
Си очень мощный, но большие программы на нем писать слишком сложно.
Andrey2008
03.12.2015 22:56+2Хороший вопрос. Я немного думал над этим, но понял, но что не знаю, как следовало бы сделать мир лучше. Стоит учитывать, что memset() это функция языка Си, а следовательно особенно не нафантазируешь. Единственное что я мог бы предложить, это сделать тип второго аргумента byte (unsigned char). По крайней мере было бы предупреждение, когда туда пытаются засунуть большое число, путая его с размером объекта.
Amomum
03.12.2015 22:58+5Вот кстати да, а почему второй аргумент int, если он все равно к uchar приводится потом? Как-то это уж совсем бессмысленно.
vladon
04.12.2015 13:15Просто `memset` появился чуток раньше возможности объявления прототипов функций в C (которые появились в C89), вот и все дела.
А без объявления прототипа с типом char нельзя передать char в функцию, даже явный литерал 'a' означает не char, а int (в C).Amomum
04.12.2015 18:02Что мешало memset обновить в том же С89 (или в С99 хотя бы) — лично мне не очень понятно.
vladon
04.12.2015 18:131) backward compatibility — наверняка есть исходники, где туда передаётся int
2) с int на самом деле производительнее, ведь char всё равно преобразуется в int перед пушем на стэк на большинстве платформ, а значит если объявлять как char, то потом лишние инструкции для преобразования. Это были те года, когда каждый такт процессора был на счету.Amomum
04.12.2015 18:27Я могу поверить в необходимость совместимости с С89, но в необходимость совместимости с С из 70-х уже не верю.
По-моему, это из той же оперы что и неявно возвращать int, если у функции не прописан тип возвращаемого значения. В С99 это запретили, не побоялись. Поломали совместимость. И ничего, все живы.vladon
04.12.2015 18:28Ну вот, видимо, здесь ещё боятся. Но может быть и второй аргумент (про производительность).
ivan386
03.12.2015 21:02+1Не понимаю вообще зачем вообще эти конструкции? Фатальный недостаток?
#define RtlFillMemory(Destination,Length,Fill) memset((Destination),(Fill),(Length))
DmitryMe
04.12.2015 10:38RtlFillMemory() также реализована в kernel32.dll и экспортируется из нее. То же самое с рядом других функций.
Этот #define предписывает препроцессору подставить вызовы memset(), которые компилятор сможет потом оптимизировать по месту.ivan386
04.12.2015 20:16Не понял. RtlFillMemory будет препроцессором заменён на memset и какой тогда толк в том что функция с аналогичным именем присутствует в kernel32.dll.
mayorovp
04.12.2015 21:12Смысл в том, что не в каждом языке программирования есть memset, поэтому аналог (RtlFillMemory) включен в WinAPI. А раз в WinAPI есть описанная в документации функция RtlFillMemory — то она должна быть в каждом языке программирования, даже если там уже есть нормальный memset. Но так как современный memset работает быстрее чем RtlFillMemory из WinAPI — то RtlFillMemory в целях оптимизации тихо заменили на макрос.
SkidanovAlex
03.12.2015 21:10+5> memset (buffer, 0, sizeof(*buffer));
тут проблема не только в том, что memset уберут, но еще и размер вычисляется от одного инта, нет?
а вообще самая опасная функция — это strncpy, потому что она не добавляет ноль в конце если строка заполнилась, и поэтому почти каждое ее использование в любом проекте неверно.
denis_g
03.12.2015 21:50+1Мне одному кажется, что в большинстве примеров проблема не в memset, а в неправильном использовании (непонимании сути; опечатке при использовании) оператора sizeof? Просто если следовать такой логике, то также очень опасны memcpy, read, write, recv, send и т.д. Для них тоже нужен sizeof, и они тоже позволяют незаметно выстрелить себе в ногу.
BalinTomsk
03.12.2015 22:33+5Очень часто проблема с memset возникает когда люди переводят проэкты на юникод.
поэтому обнуляет тока половину буффера.
char buff[100]; ::memset(buff, 0x00, sizeof(buff));
vladon
04.12.2015 18:23А можно глупый вопрос?
Почему половину? sizeof(buff) == 100 байт, ведь char всегда 1 байт, независимо от того, юникод это или нет.Andrey2008
04.12.2015 18:58Это пока. А потом поменяют char на wchar_t и будет беда. Правда не в этом примере. Здесь всё будет хорошо, так как используется sizeof() от буфера. Но в реальном коде будет масса проблем. Я описывал это в статье "Команда PVS-Studio расширяет кругозор, выполняя разработку на заказ" (см. раздел «Замена char на wchar_t»).
Salabar
04.12.2015 02:41+2Мне интересно, а сколько кода в принципе заполняет массивы чем-то кроме 0? Может, иногда нужны единицы, но я реально не представляю, для чего может пригодиться побайтовое заполнение массива чем-то типа 10101010.
lumag
08.12.2015 07:54В реализации Стрибога (ГОСТ Р 34.11-2012), например,
memset (hd->h, 1, 64);
Потому как массив заполняется именно[1, 1, 1, ...]
valeriyvan
05.12.2015 20:14-2>Впрочем, это не уменьшает интересность и полезность моих статей.
В общем, я согласен с этим утверждением. Но звучит оно как-то ну уж очень нескромно.
catnikita255
07.12.2015 20:54-2Давайте тогда сделаем «обертку» для memset, предотвращающую ошибки. Я могу сделать. И вместо sizeof'ов будут константы типа INT_SIZE, LONG_SIZE, UINT_SIZE и т. д. Это, скорее всего, сделает код немного быстрее из-за меньшего юзания sizeof.
Error1024
08.12.2015 05:09sizeof — это псевдо функция, при компиляции она заменяется на значение компилятором.
Ogra
Абсолютное значение количества ошибок для функции не имеет смысла.
Относительное значение, скажем, вероятность допустить ошибку при использовании функции гораздо информативнее.
Andrey2008
Я и говорю. Среди всех функций, которые имеются в фрагментах кода с ошибкой, больше всего вреда от memset(). :)
Ogra
Нет, нужно посмотреть сколько раз функция была использована, и сколько раз она была использована неправильно.
Andrey2008
Тогда победит какая-то очень редко используемая функция, которая из 100 проектов встретится только в 2. И в одном из проектов она будет использоваться неправильно. Получим, что в 50% она используется неправильно. Бессмысленное исследование.
Я же показал, какая функция наносит наибольший вред при программировании на Си/Си++.
Ogra
Редко используемые функции можно исключить из исследования, введя минимальную планку употребления. Скажем, 20% проектов.
Andrey2008
И всё равно смысла нет. Ну побудит какая-то другая функция. Но вреда от неё все равно будет меньше, чем от крайне распространённой memset(). Я даже без исследования могу сказать. что memset() входит в список наиболее часто используемых функций. По крайне мере в Си.
a_batyr
Andrey2008
Не зная брода, не лезь в воду. Часть вторая.
СТОП. Подожди читатель, не проходи мимо. Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.
a_batyr
dimview
Чтобы этого избежать, используется сглаживание Лапласа. Например, если функция использовалась n раз и из них k раз с ошибкой, то оценка вероятности ошибки (k+2)/(n+4).
Источник: Alan Agresti, Brian Caffo — Simple and Effective Confidence Intervals for Proportions and Differences of Proportions Result from Adding Two Successes and Two Failures