Почему memcpy не выполняет копирование в элемент локального массива простого объекта?
-
21-09-2019 - |
Вопрос
Классический memcpy gotcha с массивами C в качестве аргументов функции.Как указано ниже, у меня ошибка в моем коде, но ошибочный код сработал в локальном контексте!
Я только что столкнулся с этим странным поведением в задании на перенос, где я эмулирую воспроизведение кода операции Macintosh Picture с использованием objects.Мой объект DrawString рисовал мусор при воспроизведении, потому что ему, по-видимому, не удалось скопировать строковый аргумент.Ниже приведен тестовый пример, который я написал - обратите внимание, как работает цикл ручного копирования, но memcpy завершается с ошибкой.Трассировка в отладчике Visual Studio показывает, что memcpy ovewrites адресата с мусором.
Memcpy для двух локальных массивов Str255 работает нормально.
Когда один из них является членом объекта в стеке, происходит сбой (в другом тестировании это также приводит к сбою, когда объект находится в куче).
Следующий пример кода показывает, что memcpy вызывается в operator= .Я переместил его туда после сбоя в конструкторе, но разницы не было.
typedef unsigned char Str255[257];
// snippet that works fine with two local vars
Str255 Blah("\004Blah");
Str255 dest;
memcpy(&dest, &Blah, sizeof(Str255)); // THIS WORKS - WHY HERE AND NOT IN THE OBJECT?
/*!
class to help test CanCopyStr255AsMember
*/
class HasMemberStr255 {
public:
HasMemberStr255()
{
mStr255[0] = 0;
}
HasMemberStr255(const Str255 s)
{
for (int i = 0; i<257; ++i)
{
mStr255[i] = s[i];
if (s[i]==0)
return;
}
}
/// fails
void operator=(const Str255 s) {
memcpy(&mStr255, &s, sizeof(Str255));
};
operator const Str255&() { return mStr255; }
private:
Str255 mStr255;
};
-
/*!
Test trivial copying technique to duplicate a string
Added this variant using an object because of an apparent Visual C++ bug.
*/
void TestMacTypes::CanCopyStr255AsMember()
{
Str255 initBlah("\004Blah");
HasMemberStr255 blahObj(initBlah);
// using the operator= which does a memcpy fails blahObj = initBlah;
const Str255& dest = blahObj; // invoke cast operator to get private back out
CPPUNIT_ASSERT( dest[0]=='\004' );
CPPUNIT_ASSERT( dest[1]=='B' );
CPPUNIT_ASSERT( dest[2]=='l' );
CPPUNIT_ASSERT( dest[3]=='a' );
CPPUNIT_ASSERT( dest[4]=='h' );
CPPUNIT_ASSERT( dest[5]=='\0' ); // trailing null
}
Решение
Вероятно, это хороший пример того, почему (на мой взгляд) это плохая идея typedef
типы массивов.
В отличие от других контекстов, в объявлениях функций параметр типа array всегда настраивается на эквивалентный тип указателя.Когда массив передается функции, он всегда распадается на указатель на первый элемент.
Эти два фрагмента эквивалентны:
typedef unsigned char Str[257];
Str src = "blah";
Str dst;
memcpy( &dst, &src, sizeof(Str) ); // unconventional
unsigned char src[257] = "blah";
unsigned char dst[257];
memcpy(&dst, &src, sizeof(unsigned char[257])); // unconventional
В этом последнем случае &dst
и &src
оба относятся к типу unsigned char (*)[257]
но значение этих указателей совпадает со значением указателей на первый элемент каждого массива, что dst
и src
распался бы на, если бы был передан непосредственно в memcpy
вот так.
memcpy(dst, src, sizeof(unsigned char[257])); // more usual
memcpy
принимает void*
аргументы, поэтому типы исходных указателей не имеют значения, только их значения.
Из-за правила для объявлений параметров (тип массива любого или неуказанного размера подгоняется к эквивалентному типу указателя), эти объявления для fn
все они эквивалентны:
typedef unsigned char Str[257];
void fn( Str dst, Str src );
void fn( unsigned char dst[257], unsigned char src[257] );
void fn( unsigned char dst[], unsigned char src[] );
void fn( unsigned char* dst, unsigned char* src );
Глядя на этот код, становится более очевидным, что значения, передаваемые в memcpy
в этом случае это указатели на переданные указатели, а не указатели на фактические unsigned char
массивы.
// Incorrect
void fn( unsigned char* dst, unsigned char* src )
{
memcpy(&dst, &src, sizeof(unsigned char[257]));
}
С typedef ошибка не столь очевидна, но все еще присутствует.
// Still incorrect
typedef unsigned char Str[257];
void fn( Str dst, Str src )
{
memcpy(&dst, &src, sizeof(Str));
}
Другие советы
Вы должны написать memcpy(mStr255, s, sizeof(Str255));
.Без '&'. Str255
это уже указатель.Это соответствует стандарту C ++ 4.2:
Значение lvalue или rvalue типа “массив из N T“ или "массив с неизвестной границей T" может быть преобразовано в значение типа “указатель на T.” Результатом является указатель на первый элемент массива.
Почему это где-то работает?Есть два разных указателя (для mStr255
и &mStr255
) и у них есть разные типы — unsigned char *
и unsigned char (*)[257]
.Адрес массива совпадает с адресом
первого элемента в массиве, но когда вы передаете его в качестве аргумента функции, вы получите адрес переменной в стеке.С помощью набора текста Str255
вы скрываете разницу.Проверьте следующий образец:
unsigned char Blah[10] = "\004Blah";
struct X
{
void f1( unsigned char(&a)[10] ) // first case (1)
{
void* x1 = &a; // pointer to array of unsigned char
void* x2 = a; // pointer to unsigned char due to implicit conversion array-to-pointer
}
void f2( unsigned char* a ) // second case (2)
{
void* x1 = &a; // pointer to variable 'a' which is on the stack
void* x2 = a; // pointer to unsigned char
}
unsigned char x[10];
};
int main( int argc, char ** argv )
{
X m;
m.f1( Blah ); // pass by reference
m.f2( Blah ); // implicit array-to-pointer conversion
return 0;
}
Когда вы будете писать void f( Str255 a )
, это равно второму случаю.
Если я правильно читаю (а мой C ++ немного подзабылся), ваш класс на самом деле никогда не выделяет пространство для переменной mStr.Вы объявляете его (но, похоже, не выделяете его) в разделе private и инициализируете первый элемент значением 0 в конструкторе, но, похоже, не каждый из вас на самом деле создает объект Str255.
Возможно, вам потребуется заменить личное объявление на Str255 mStr()
, или вам может понадобиться сделать что-то в конструкторе, например mStr = new Str255()