memset()
Проверяя много лет различные 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(). Каждый раз, когда я описываю эту ошибку уязвимости, я получаю письма, где со мной начинают спорить. Я уже устал отвечать на эти письма. Поэтому прошу всех сомневающихся, прежде чем начинать дискуссию, внимательно познакомиться со следующими материалами:
Пример 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)


  1. Ogra
    03.12.2015 11:58
    -2

    Абсолютное значение количества ошибок для функции не имеет смысла.
    Относительное значение, скажем, вероятность допустить ошибку при использовании функции гораздо информативнее.


    1. Andrey2008
      03.12.2015 12:06
      +2

      Я и говорю. Среди всех функций, которые имеются в фрагментах кода с ошибкой, больше всего вреда от memset(). :)


      1. Ogra
        03.12.2015 12:09
        +1

        Нет, нужно посмотреть сколько раз функция была использована, и сколько раз она была использована неправильно.


        1. Andrey2008
          03.12.2015 12:18
          +7

          Тогда победит какая-то очень редко используемая функция, которая из 100 проектов встретится только в 2. И в одном из проектов она будет использоваться неправильно. Получим, что в 50% она используется неправильно. Бессмысленное исследование.
          Я же показал, какая функция наносит наибольший вред при программировании на Си/Си++.


          1. Ogra
            03.12.2015 12:20
            +2

            Редко используемые функции можно исключить из исследования, введя минимальную планку употребления. Скажем, 20% проектов.


            1. Andrey2008
              03.12.2015 12:24

              И всё равно смысла нет. Ну побудит какая-то другая функция. Но вреда от неё все равно будет меньше, чем от крайне распространённой memset(). Я даже без исследования могу сказать. что memset() входит в список наиболее часто используемых функций. По крайне мере в Си.


          1. a_batyr
            03.12.2015 12:23
            +4

            Почетное второе место занимает функция printf() и её разновидности. Думаю, это никого не удивит. Про опасность функции printf() не писал только ленивый.
            Ждём статью про опасную printf().


            1. Andrey2008
              03.12.2015 12:26
              +4

              Не зная брода, не лезь в воду. Часть вторая.

              СТОП. Подожди читатель, не проходи мимо. Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.


              1. a_batyr
                03.12.2015 12:32
                +3

                Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.
                Та статья действительно по существу, про уязвимости и атаки. Если бы вы описали уязвимости memset, вместо «банальной истории о том, что функция не контролирует типы передаваемых аргументов», было бы очень полезно!


          1. dimview
            03.12.2015 15:39

            Чтобы этого избежать, используется сглаживание Лапласа. Например, если функция использовалась 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


  1. 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() имеет крайне неудачный интерфейс
    Имея молоток можно отбить себе палец.
    Я думал будет что-то по существу, а так статья ни о чём.


    1. Andrey2008
      03.12.2015 12:22
      +17

      Из того, что кто-то не умеет программировать делается просто чудесный вывод

      Эти кто-то, разработчики таких проектов как OpenCV, CoreCLR, UnrealEngine4, Haiku, Qt, Chromium,…


      1. a_batyr
        03.12.2015 12:27
        -5

        Люди учатся на ошибках. Если вы падали на велосипеде, когда учились ездить, это не делает его самым опасным транспортом.


        1. Indexator
          04.12.2015 05:44
          +2

          Если вы падали на велосипеде, когда учились ездить,
          это не значит, что вы не упадете на нем, даже если научились на нем ездить.


          1. Muzzy0
            08.12.2015 13:32

            Я бы сказал — если вы научились на нём ездить, то вы с вероятностью 99.(9)% с него упадёте. Самоуверенность и закон Мерфи делают своё дело.
            Также справедливо будет другое утверждение: большинство упавших с велосипеда умеют на нём ездить :)


      1. a_batyr
        03.12.2015 13:05
        +3

        Я в принципе-то не оспаривал тезисов статьи, вот к примеру тут они раскрыты хорошо.


        1. Andrey2008
          03.12.2015 13:09

          Спасибо. Интересная ссылка.


          1. 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
            и тогда бы тезис статьи был раскрыт по существу.


            1. Andrey2008
              03.12.2015 13:16
              +1

              Ну не каждая статья получается удачной. :)


              1. a_batyr
                03.12.2015 13:22
                +4

                Если вы не будете писать дополнение, я могу написать перевод приведённой ссылки


                1. Andrey2008
                  03.12.2015 13:26

                  Напишите. Я думаю эта статья достойна перевода.


                1. 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 (нет, только первый)
                  


                  1. a_batyr
                    04.12.2015 15:31
                    +2

                    Спасибо. Начал исследовать вопрос корректности статьи, тоже нашёл ошибки в примерах, поэтому перевода не будет, слишком много нюансов, на которые я сам не знаю ответ, но дополнение и некоторое исследование постараюсь сделать в ближайшее время.


                    1. a_batyr
                      07.12.2015 15:49

            1. 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++) это было бы проще, там всё локализовано.



  1. VioletGiraffe
    03.12.2015 12:29
    +2

    Я бы сказал, что memcpy даёт на один вариант выстрела в ногу больше, чем memset.


    1. Andrey2008
      03.12.2015 12:30
      +1

      Да, с memcpy() тоже очень опасная функция. Я её смотрел. Но победитель всё равно memset().


      1. buratino
        03.12.2015 22:50
        +3

        победитель scanf ;-)


  1. kyb27
    03.12.2015 13:31

    Интересный продукт эта ваша студия, но сообщения об ошибках невнятные.
    Без толмача не обойтись.


    1. Andrey2008
      03.12.2015 13:32
      +2

      Но ведь есть отличная, удобная документация с примерами! Достаточно нажать в окне на код ошибки.


  1. allter
    03.12.2015 14:18
    +5

    Не согласен, что дело в функции memset. Исходя из приведённых примеров, только в примере №7 можно с натяжкой назвать причиной ошибки кривой интерфейс функции. А какая альтернатива? был бы другой порядок аргументов — также путались бы. А именование фактических параметров функций в конце 60х в языкостроении не было распространено (да и сейчас можно по пальцам руки, наверное, пересчитать такие языки). Все остальные — ошибки связанные с управлением памятью (в широком смысле).

    Как говорится, в программировании на Cи всего 1 существенная трудность — управление памятью и ошибки на единицу. :)


    1. FoxCanFly
      03.12.2015 14:52
      +3

      Альтернатива — std::fill


    1. poxu
      03.12.2015 15:01
      +2

      Ну еще числа с плавающей точкой. То есть всего 1.00085433


      1. mayorovp
        03.12.2015 15:53
        +1

        Тогда уж 2.00085433


        1. poxu
          03.12.2015 17:13
          +2

          Действительно. Я ошибся ровно на еденицу :)


          1. IvanAnonym
            04.12.2015 09:23
            +2

            И ещё одну букву


  1. nerudo
    03.12.2015 15:01
    -1

    Ага. Неправильно используется sizeof, а виноват memset.


    1. Andrey2008
      03.12.2015 16:02

      Да, но ведь этот sizeof потребовался для memset().


      1. nerudo
        03.12.2015 17:18
        +11

        А memset() потребовался для main() — вот где корень зла!
        Понятно, что функция напрямую работающая с памятью потенциально опасна и на нее все грехи свалить намного проще, чем на безобидную функцию, возвращающую результат вычислений или что-то подобное. Но корень зла-то именно в некорректном использовании sizeof(), который, наверняка, в вашей статистике встретится не только в контексте memset(), но и во множестве других мест.


        1. bigfatbrowncat
          03.12.2015 20:34
          -2

          Понятно, что функция напрямую работающая с памятью потенциально опасна

          … и люди, пришедшие к этому выводу изобрели много языков, в которых такие функции отсутствуют в принципе.

          Программировать на Си, не влезая напрямую в память, можно (хотя, конечно, только довольно простые программы). Но memset и memcpy — две первых функции, за которые вынужденно берутся новички (ранее не имевшие дело с памятью напрямую). Вот и получается, что они — опасные. А вместе с ними и printf(str).


      1. 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()? Только если точно не знаешь, как она работает и для чего вообще делалась.


        1. mayorovp
          04.12.2015 06:09

          И sizeof вычисляет не размер типа в 6-ом примере а размер получаемого значения.
          Чем размер значения отличается от размера его типа?


          1. 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]$
            


            1. 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]$
              


            1. mayorovp
              04.12.2015 08:34

              Спасибо, я знаю о том что не у всех типов есть размер.

              Но все-таки, чем размер значения отличается от размера его типа?


              1. samo-delkin
                04.12.2015 08:41
                -1

                У типов нет размеров, размеры есть у значений. И вот этот код я тебе не просто так привёл. У типа функции нет размера, однако возвращается какое-то значение, у которого есть размер. А какого оно типа?


                1. mayorovp
                  04.12.2015 08:52

                  У типов нет размеров, размеры есть у значений.
                  В таком случае что вообще означает запись вида sizeof(int)? Что это, если не размер типа int?

                  У типа функции нет размера, однако возвращается какое-то значение, у которого есть размер. А какого оно типа?
                  Возвращается кем? Функция main как бы возвращает int, оператор sizeof — size_t.

                  Или вы про тип значения main, размер которого измеряет sizeof в вашем коде? Тогда это int()

                  И да, у типа функции, с точки зрения gcc при ваших настройках компилятора — очень даже есть размер. Это 1 байт, как вам и написала программа.


                  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 с выдачей предупреждений при его нарушении.


                    1. mayorovp
                      04.12.2015 09:59

                      А что это за один байт?
                      Спросите лучше у разработчиков gcc, какой именно байт они имели в виду :)

                      Он по значению вычисляет; какое значение туда поместится, его размер он и возвращает. Длина участка памяти в байтах.
                      Так все-таки, чем отличается размер значения от размера типа?


                      1. samo-delkin
                        04.12.2015 14:30

                        Спросите лучше у разработчиков gcc

                        Код компилятора открыт. Скорее всего, это размер значения типа void.


                1. 0xd34df00d
                  04.12.2015 16:36
                  +1

                  Наоборот, размеры есть у типов, а у значений есть типы.


  1. Ogi
    03.12.2015 16:31
    +8

    Получается, что sizeof() вычисляет размер не того массива, который заполняется нулями. Вроде как функция memset() никак не виновата. Но неправильно будет работать именно она.

    Теперь всегда буду винить в своих опечатках memset, даже если я при этом её не использую и вообще пишу на совсем другом языке с автоматическим управлением памятью.


  1. Amomum
    03.12.2015 18:50
    +4

    Скажите, а какой следовало бы сделать интерфейс у memset, чтобы ошибок стало меньше? Мне кроме шаблонной версии (чтобы размер не в байтах принимать) как-то ничего в голову сходу не приходит.


    1. bigfatbrowncat
      03.12.2015 20:39

      Позвольте предложить идею вместо автора статьи.

      Мне кажется, эта функция — результат плохого проектирования языка. Учитывая регулярную необходимость обнулять массивы не только при инициализации, можно было сделать для этого некоторое синтаксическое средство. Например, разрешить запись вида arr = { 0 } в любой точке кода (пусть она генерирует правильный вызов memset).

      Хотя учитывая «родовую травму» Си, в котором длина массива — фантомное понятие, живущее только в голове разработчика, даже с этим будут проблемы… Так что memset — это своего рода плата за " int[] = int* "


      1. Amomum
        03.12.2015 21:03
        +2

        Боюсь, что язык уже не удастся спроектировать заново и изменить поведение для массивов уже слишком сложно. Но может быть можно как-то выкрутиться с тем, что есть?


        1. bigfatbrowncat
          04.12.2015 00:40

          Ну тут же уже кто-то предлагал — использовать C++.

          Си очень мощный, но большие программы на нем писать слишком сложно.


    1. Andrey2008
      03.12.2015 22:56
      +2

      Хороший вопрос. Я немного думал над этим, но понял, но что не знаю, как следовало бы сделать мир лучше. Стоит учитывать, что memset() это функция языка Си, а следовательно особенно не нафантазируешь. Единственное что я мог бы предложить, это сделать тип второго аргумента byte (unsigned char). По крайней мере было бы предупреждение, когда туда пытаются засунуть большое число, путая его с размером объекта.


      1. Amomum
        03.12.2015 22:58
        +5

        Вот кстати да, а почему второй аргумент int, если он все равно к uchar приводится потом? Как-то это уж совсем бессмысленно.


        1. vladon
          04.12.2015 13:15

          Просто `memset` появился чуток раньше возможности объявления прототипов функций в C (которые появились в C89), вот и все дела.

          А без объявления прототипа с типом char нельзя передать char в функцию, даже явный литерал 'a' означает не char, а int (в C).


          1. Amomum
            04.12.2015 18:02

            Что мешало memset обновить в том же С89 (или в С99 хотя бы) — лично мне не очень понятно.


            1. vladon
              04.12.2015 18:13

              1) backward compatibility — наверняка есть исходники, где туда передаётся int
              2) с int на самом деле производительнее, ведь char всё равно преобразуется в int перед пушем на стэк на большинстве платформ, а значит если объявлять как char, то потом лишние инструкции для преобразования. Это были те года, когда каждый такт процессора был на счету.


              1. Amomum
                04.12.2015 18:27

                Я могу поверить в необходимость совместимости с С89, но в необходимость совместимости с С из 70-х уже не верю.

                По-моему, это из той же оперы что и неявно возвращать int, если у функции не прописан тип возвращаемого значения. В С99 это запретили, не побоялись. Поломали совместимость. И ничего, все живы.


                1. vladon
                  04.12.2015 18:28

                  Ну вот, видимо, здесь ещё боятся. Но может быть и второй аргумент (про производительность).


  1. ivan386
    03.12.2015 21:02
    +1

    Не понимаю вообще зачем вообще эти конструкции? Фатальный недостаток?

    #define RtlFillMemory(Destination,Length,Fill)   memset((Destination),(Fill),(Length))


    1. DmitryMe
      04.12.2015 10:38

      RtlFillMemory() также реализована в kernel32.dll и экспортируется из нее. То же самое с рядом других функций.

      Этот #define предписывает препроцессору подставить вызовы memset(), которые компилятор сможет потом оптимизировать по месту.


      1. ivan386
        04.12.2015 20:16

        Не понял. RtlFillMemory будет препроцессором заменён на memset и какой тогда толк в том что функция с аналогичным именем присутствует в kernel32.dll.


        1. mayorovp
          04.12.2015 21:12

          Смысл в том, что не в каждом языке программирования есть memset, поэтому аналог (RtlFillMemory) включен в WinAPI. А раз в WinAPI есть описанная в документации функция RtlFillMemory — то она должна быть в каждом языке программирования, даже если там уже есть нормальный memset. Но так как современный memset работает быстрее чем RtlFillMemory из WinAPI — то RtlFillMemory в целях оптимизации тихо заменили на макрос.


  1. SkidanovAlex
    03.12.2015 21:10
    +5

    > memset (buffer, 0, sizeof(*buffer));

    тут проблема не только в том, что memset уберут, но еще и размер вычисляется от одного инта, нет?

    а вообще самая опасная функция — это strncpy, потому что она не добавляет ноль в конце если строка заполнилась, и поэтому почти каждое ее использование в любом проекте неверно.


  1. denis_g
    03.12.2015 21:50
    +1

    Мне одному кажется, что в большинстве примеров проблема не в memset, а в неправильном использовании (непонимании сути; опечатке при использовании) оператора sizeof? Просто если следовать такой логике, то также очень опасны memcpy, read, write, recv, send и т.д. Для них тоже нужен sizeof, и они тоже позволяют незаметно выстрелить себе в ногу.


  1. BalinTomsk
    03.12.2015 22:33
    +5

    Очень часто проблема с memset возникает когда люди переводят проэкты на юникод.

    поэтому обнуляет тока половину буффера.

    char buff[100];
    ::memset(buff, 0x00, sizeof(buff));
    



    1. Andrey2008
      03.12.2015 23:04
      +2

      Кстати, да. Забыл упомянуть такой паттерн.


    1. vladon
      04.12.2015 18:23

      А можно глупый вопрос?

      Почему половину? sizeof(buff) == 100 байт, ведь char всегда 1 байт, независимо от того, юникод это или нет.


      1. Andrey2008
        04.12.2015 18:58

        Это пока. А потом поменяют char на wchar_t и будет беда. Правда не в этом примере. Здесь всё будет хорошо, так как используется sizeof() от буфера. Но в реальном коде будет масса проблем. Я описывал это в статье "Команда PVS-Studio расширяет кругозор, выполняя разработку на заказ" (см. раздел «Замена char на wchar_t»).


  1. Salabar
    04.12.2015 02:41
    +2

    Мне интересно, а сколько кода в принципе заполняет массивы чем-то кроме 0? Может, иногда нужны единицы, но я реально не представляю, для чего может пригодиться побайтовое заполнение массива чем-то типа 10101010.


    1. lumag
      08.12.2015 07:54

      В реализации Стрибога (ГОСТ Р 34.11-2012), например,

      memset (hd->h, 1, 64);
      


      Потому как массив заполняется именно
      [1, 1, 1, ...]


  1. spot62
    04.12.2015 10:54

    думал самая опасная это strncpy )


  1. valeriyvan
    05.12.2015 20:14
    -2

    >Впрочем, это не уменьшает интересность и полезность моих статей.
    В общем, я согласен с этим утверждением. Но звучит оно как-то ну уж очень нескромно.


  1. valeriyvan
    05.12.2015 20:25
    +1

    Вот вам еще один интересный подопытный кролик. Проверите?


  1. catnikita255
    07.12.2015 20:54
    -2

    Давайте тогда сделаем «обертку» для memset, предотвращающую ошибки. Я могу сделать. И вместо sizeof'ов будут константы типа INT_SIZE, LONG_SIZE, UINT_SIZE и т. д. Это, скорее всего, сделает код немного быстрее из-за меньшего юзания sizeof.


    1. Error1024
      08.12.2015 05:09

      sizeof — это псевдо функция, при компиляции она заменяется на значение компилятором.


  1. rafuck
    08.12.2015 18:05

    Пример 4 вообще странный. Мне кажется, в нем хотели в каждый элемент массива -1 записать. Но даже если исправить ошибку с размером буфера, будет все равно не то.
    Вообще, присоединюсь к Salabar, мое ощущение, что 99% использования memset — это заполнение памяти 0-байтами.


  1. San66
    08.12.2015 23:43

    Очепятка в слове «вычиялет»