为什么不保证memcpy对非POD类型是安全的?

时间:2013-07-23 12:26:58

标签: c++

我从SO上发布的几个问题中读到了这一段。

我无法弄清楚为什么{P}不保证对于非POD类型是安全的。memcpy。我的理解是memcpy只是一个有点明智的副本。

以下是标准

的引用
  

对于POD类型T的任何对象(基类子对象除外),无论对象是否包含T类型的有效值,   组成对象的底层字节(1.7)可以复制到charunsigned char的数组中.41)如果内容   将charunsigned char数组复制回对象,该对象随后将保留其原始对象   值。

# define N sizeof (T)
char buf[N];
T obj ; // obj initialized to its original value
std :: memcpy (buf , & obj , N); // between these two calls to std::memcpy,
                                 // obj might be modified
std :: memcpy (& obj , buf , N); // at this point, each subobject of obj of 
                                 // scalar type holds its original value

5 个答案:

答案 0 :(得分:17)

尝试逐位复制std::shared_ptr<>。您可能会发现您的程序经常在您的脸上爆炸。

对于任何其复制构造函数执行除逐位复制之外的操作的类,您将遇到此问题。在std::shared_ptr<>的情况下,它将复制指针但不会增加引用计数,因此您最终会提前释放共享对象及其引用计数,然后在复制时{{{{{ 1}}尝试减少释放的引用计数。


更新:有人指出,这并没有完全回答这个问题,这是公平的,因为我主要解决了将shared_ptr复制到shared_ptr而不是shared_ptr复制到char []并返回的想法。再次。但是,原则仍然存在。

如果你将shared_ptr按位逐位复制到char [],则为shared_ptr指定一个不同的值,然后将char []复制回来,最终结果可能是泄漏一个对象并双重删除另一个对象,即,UB。

POD可能会发生同样的情况,但这可能是程序逻辑中的一个错误。只要程序理解并适应这样的事件,逐位复制回POD等效于修改后的shared_ptr就完全有效。对std :: shared_ptr执行此操作通常不起作用。

答案 1 :(得分:5)

想象一个类,它包含一些指向缓冲区的指针:

class Abc {
    public:
    int* data;
    size_t n;
    Abc(size_t n)
    {
        this->n = n;
        data = new int[n];
    }

    // copy constructor:
    Abc(const Abc& copy_from_me)
    {
        n = copy_from_me.n;
        data = new int[n];
        memcpy(data, copy_from_me.data, n*sizeof(int));
    }
    Abc& operator=(const Abc& copy_from_me)
    {
        n = copy_from_me.n;
        data = new int[n];
        memcpy(data, copy_from_me.data, n*sizeof(int));
        return *this;
    }

    ~Abc()
    {
        delete[] data;
    }
} ;

如果只记忆其构建的实例之一,则会得到两个指向同一缓冲区data的实例,因为它们在data指针中具有相同的缓冲区地址。如果您在一个实例中修改数据,它也将在另一个实例中修改。

这意味着你没有真正将它克隆到两个独立的类中。此外,如果然后删除这两个类,缓冲区将从崩溃的内存中释放两次。 因此,类必须定义一个复制构造函数,您必须使用构造函数复制它。

答案 2 :(得分:3)

一般来说,问题是对象不仅会引入数据,还会引入行为

通过手动复制数据,我们可能会破坏对象的固有行为,这可能依赖于复制构造函数。

一个很好的例子是任何共享唯一指针 - 通过复制它我们打破了我们在使用它时对该类进行的“交易”。

无论复制过程在语义上是否正确,这背后的想法都是错误的,违反了对象编程范例。

示例代码:

/** a simple object wrapper around a pthread_mutex
 */
class PThreadMutex
{
   public:
    /** locks the mutex. Will block if mutex is already locked */
    void lock();

    /** unlocks the mutex. undefined behavior if mutex is unlocked */
    void unlock();

   private:
    pthread_mutex_t m_mutex;

};

/** a simple implementation of scoped mutex lock. Acquires and locks a Mutex on creation,
 * unlocks on destruction
 */
class ScopedLock
{
  public:
    /** constructor specifying the mutex object pointer to lock
     * Locks immediately or blocks until lock is free and then locks
     * @param mutex the mutex pointer to lock
     */
    ScopedLock ( PThreadMutex* mutex );

    /** default destructor. Unlocks the mutex */
    ~ScopedLock ();

    /** locks the mutex. Will block if mutex is already locked */
    void unlock();


  private:

    PThreadMutex* m_mutex;

    // flag to determine whether the mutex is locked
    bool m_locked;

    // private copy constructor - disable copying
    ScopedLock(ScopedLock &mutex) { (void)mutex; /* to get rid of warning */ };

};

如果复制ScopedLock类,手动解锁,然后恢复该值并在构造函数中执行另一个解锁,则会导致未定义的行为(或者析构函数中至少出现EPERM错误)。

答案 3 :(得分:1)

假设您正在编写String类。该类的任何实例都应该包含指向某个动态分配的char数组的指针。如果你记住这样一个实例,那么两个指针将是相等的。对一个字符串的任何修改都会影响另一个字符串。

答案 4 :(得分:1)

C ++ 11注意:问题中的引用是规则的旧版本。从C ++ 11开始,要求是平易可复制的,它比 POD 弱得多。


memcpy可以在任何对象中使用。你得到一个对象的按位图像。

如果对象不是POD,则不能使用图像,就好像它与原始对象的类型相同,因为生命周期规则需要首先完成初始化。

在这种情况下,图像只是一堆字节。这可能仍然有用,例如,检测对象的内部表示随时间的变化,但只有对字节有效的操作(例如两个图像之间的比较)是合法的,而不是需要原始类型的对象的操作。