2009-10-15 3 views
6

Classic memcpy gotcha с массивами C в качестве аргументов функции. Как указано ниже, у меня есть ошибка в моем коде, но ошибочный код работал в локальном контексте!Почему memcpy не копирует локальный элемент массива простого объекта?

Я только что столкнулся с этим странным поведением в задании портирования, где я эмулирую воспроизведение кода opcode Macintosh с использованием объектов. Мой объект DrawString рисовал мусор при воспроизведении, потому что он, по-видимому, не смог скопировать аргумент строки. Следующее - это тестовый сценарий, который я написал, - обратите внимание, как работает цикл копирования вручную, но memcpy терпит неудачу. Трассировка в отладчике Visual Studio показывает, что memcpy создает назначение с мусором.

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 
} 
+0

Как именно это происходит? – sharptooth

+0

Как я уже сказал, он перезаписывается мусором, когда я использую memcpy. –

+3

Ошибка в компиляторе. –

ответ

7

Это, вероятно, является хорошим примером того, почему (на мой взгляд) это плохая идея typedef Типы массивов.

В отличие от других контекстов, в объявлениях функций параметр типа массива всегда настраивается на эквивалентный тип указателя. Когда массив передается функции, он всегда распадается на указатель на первый элемент.

Эти два фрагменты эквивалентны:

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)); 
} 
+0

Gack! Я ненавижу C нюансы. Прошло несколько лет с тех пор, как мне пришлось бороться с необработанными идиомами указателя/массива. Когда я делаю этот порт, я обычно использую C++, а Str255 - класс с операторами, чтобы уловить глупость. К сожалению, на этот раз я должен использовать plain C, поскольку есть куча кода backend, который не будет компилироваться на C++, даже после преобразования объявлений K & R в ANSI. По другим причинам проекта я не могу изменить код слишком далеко от оригинала. –

+0

Энди Дент, если бы вы написали хороший c, вы были бы уверены в том, чтобы избегать таких ошибок; тогда как если бы вы написали хороший C++, вы бы использовали «struct Str» вместо простого typedef и записывали StrCopy (struct Str * p, struct Str * from), скажем, OOP и инкапсуляция смысла при записи C. – Test

+0

«если вы написали хорошее c "- я портирую его, я не писал его! Я не могу использовать структуры для вещей, которые предполагают исходные определения Apple, которые являются typedefs. Как я уже сказал, обычно при переносе кода, подобного этому, я использую объекты и перегрузку оператора для получения надежных типов. –

5

Вы должны написать memcpy(mStr255, s, sizeof(Str255));. Без '&'. Str255 уже указатель. Именно в соответствии с C Standard ++ 4.2:

Именующее выражение или Rvalue типа «массив NT» или «массив неизвестного связанного Т» может быть преобразован в RValue типа «указатель на 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), равно второму случаю.

+0

Да, это сработало! Итак, почему же он работает с двумя локальными переменными? Я чувствую себя глупо, Я сократил этот код с memcpy (& a [0], & b [1], somesize), который перемещался, а также копировал. –

+0

в случае локальных переменных вы перезаписываете неиспользуемый стек, скажем [. .. использовано пространство стека | Blaha | dest | неиспользуемое пространство стека ...], то есть вы написали 257 байт с позиции «dest» в сторону неиспользуемого направления. – Test

+0

Please попытайтесь выяснить значения dest и & dest и т. д. (стек), добавив printf («% p,% p,% p,% p \ n», & dest, & Baha, dest, Baha) и поделитесь с нами. – Test

-3

Если я правильно читаю (и мой C++ немного ржавый), ваш класс никогда не выделяет пространство для переменной mStr. Вы объявляете это (но не выделяете его) в частном разделе, и вы инициализируете первый элемент до 0 в конструкторе, но вы не видите, что каждый фактически создает объект Str255.

Вы, возможно, потребуется заменить частную декларацию с Str255 mStr(), или вам может понадобиться, чтобы сделать что-то в конструкторе, как mStr = new Str255()

+0

Да, ты ржавый. Str255 - это старомодный массив C, поэтому он выделяет пространство по своей сути. –

+0

Элемент «Str255 mStr255» в фрагменте кода исходного сообщения может быть расширен до «unsigned char mStr255 [255]», который использует память в пределах, выделенных для экземпляра HasMemberStr255. – Will

+0

Ах - спасибо за исправления! – atk