Адрес нарушения доступа - очень маленькое положительное смещение от 0xcdcdcdcd
Википедия говорит:
CDCDCDCD Используется библиотекой времени выполнения отладки C ++ от Microsoft для маркировки неинициализированной кучи памяти
Вот соответствующая страница MSDN .
Соответствующее значение после free равно 0xdddddddd , поэтому скорее всего это будет неполная инициализация, а не использование после освобождения.
РЕДАКТИРОВАТЬ: Джеймс спросил, как оптимизация может испортить вызовы виртуальных функций. По сути, это потому, что в настоящее время стандартизированная модель памяти C ++ не дает никаких гарантий о многопоточности. Стандарт C ++ определяет, что виртуальные вызовы, сделанные из конструктора, будут использовать объявленный тип выполняемого в данный момент конструктора, а не конечный динамический тип объекта. Таким образом, это означает, что с точки зрения модели памяти с последовательным выполнением C ++ механизм виртуального вызова (практически говоря, указатель v-таблицы) должен быть настроен до того, как конструктор начнет работать (я думаю, что конкретная точка находится после построения базового подобъекта в ctor-initializer-list и до создания подобъекта члена).
Теперь две вещи могут сделать наблюдаемое поведение другим в многопоточном сценарии:
Во-первых, компилятор может выполнять любую оптимизацию, которая в модели последовательного выполнения C ++ действовала бы так, как если бы правила выполнялись. Например, если компилятор может доказать, что внутри конструктора не выполняются виртуальные вызовы, он может подождать и установить указатель v-таблицы в конце тела конструктора вместо начала. Если конструктор не выдает указатель this
, поскольку вызывающая сторона конструктора также еще не получила свою копию указателя, то ни одна из функций, вызываемых конструктором, не может вызвать (виртуально или статически) обратный вызов строящийся объект. Но конструктор ДАЕТ указатель this
.
Мы должны присмотреться. Если функция, которой дан этот указатель, видна компилятору (то есть включена в текущий модуль компиляции), компилятор может включить его поведение в анализ. Мы не получили эту функцию в этом вопросе (конструктор и функции-члены class task
), но, вероятно, единственное, что происходит, это то, что указанный указатель хранится в подобъекте, который также недоступен извне конструктора .
«Грязь!», Вы кричите: «Я передал адрес этого task
подобъекта в библиотечную CreateThread
функцию, поэтому он достижим, и через него достижим главный объект». Ах, но вы не понимаете тайны "строгих правил наложения имен". Эта библиотечная функция не принимает параметр типа task *
, не так ли? И будучи параметром, тип которого, возможно, intptr_t
, но определенно ни task *
, ни char *
, компилятору разрешается предполагать, для целей оптимизации, как если бы, что он не указывает на объект task
( даже если это явно так). И если он не указывает на объект task
и единственное место, в котором хранится наш указатель this
, находится в подобъекте члена task
, то его нельзя использовать для виртуальных вызовов this
, поэтому компилятор может законно отложить настройку механизма виртуального вызова.
Но это еще не все. Даже если компилятор настраивает механизм виртуальных вызовов по расписанию, модель памяти ЦП гарантирует только то, что изменения видны текущему ядру ЦП. Записи могут стать видимыми для других ядер процессора в совершенно ином порядке. Теперь функция создания потока библиотеки должна вводить барьер памяти, который ограничивает переупорядочение записи ЦП, но тот факт, что ответ Коза о введении критического раздела (который, безусловно, включает в себя барьер памяти) меняет поведение, говорит о том, что, возможно, в оригинальный код.
И, переупорядочение записи ЦП может не только задержать указатель v-таблицы, но и сохранить этот указатель в подобъекте task
.
Надеюсь, вам понравился этот экскурсионный тур по одному маленькому уголку пещеры «Многопоточное программирование - это сложно».