【问题标题】:How to clone objects containing pointers?如何克隆包含指针的对象?
【发布时间】:2012-10-17 11:15:47
【问题描述】:

我有问题。我需要克隆包含指针的对象类。问题的一个示例在以下代码中:

#include "stdafx.h"
#include <iostream>
#include <string.h>
#include <vector>

class CPoint
{
protected:
    int m_x;
    int m_y;

    int *m_p;

public:
    CPoint();
    CPoint(int x, int y);
    ~CPoint();

    CPoint*         clone();
    static CPoint*  clone(CPoint& p);

    int getX();
    int getY();
    void setX(int x);
    void setY(int y);

    void toString();
};

int CPoint::getX()
{
    return m_x;
}

int CPoint::getY()
{
    return m_y;
}

void CPoint::setX( int x )
{
    m_x = x;
}

void CPoint::setY( int y )
{
    m_y = y;
}

void CPoint::toString()
{
    std::cout << "(" << m_x << ", " << m_y<< ", " << *m_p << ")" << std::endl;
}

CPoint::CPoint( int x, int y )
{
    m_x = x;
    m_y = y;    

    m_p = new int();
    *m_p = x + y;
}

CPoint::CPoint()
{
    m_p = new int();
    *m_p = 1000;
}

CPoint* CPoint::clone()
{
    CPoint *p = new CPoint();
    *p = *this;
    return p;
}

CPoint* CPoint::clone( CPoint& p )
{
    CPoint *q = new CPoint();
    *q = p;
    return q;
}

CPoint::~CPoint()
{
    if (m_p) {
        delete m_p;
        m_p = NULL;
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    CPoint *p1 = new CPoint(10, 20);
    CPoint *p2 = new CPoint(30, 40);

    p1->toString();
    p2->toString();

    CPoint *p3;
    p3 = CPoint::clone(*p1);

    p3->toString();

    CPoint *p4;
    p4 = p2->clone();

    p4->toString();

    p1->setX(50);
    p1->setY(60);
    p2->setX(80);
    p2->setY(90);

    p3->toString();
    p4->toString();

    delete p1;
    delete p2;
    delete p3;
    delete p4;

    int a;
    std::cin >> a;

    return 0;
}

变量m_p 的问题。在p3p4上克隆对象p1p2时,内存地址p1p3不同,但m_p地址相同。显然,当删除p1p3 删除失败。与p2p4 是一样的。

如何克隆 CPoint 类对象?

【问题讨论】:

  • m_p 到底是什么?整数还是数组?如果是前者,为什么不只是int?如果是后者,为什么不是std::vector,因为你已经有了#include &lt;vector&gt;?另外,为什么clone 而不是复制构造函数?这是某种层次结构中的多态基类吗?如果是这样,为什么析构函数不是虚拟的?
  • 在这个例子中是一个整数,但可以是任何类型。我的问题是克隆一个包含指向另一种类型的指针的对象。谢谢。
  • 您不需要克隆复制构造函数的用途。当我们在这里时,不要执行 tostring()。这就是operator&lt;&lt; 的用途。另请注意,由于您不遵守三人规则,您的班级从根本上被破坏了。故事的寓意不要在语言“B”中使用语言“A”中的模式,它只是行不通。学习使用语言“B”及其习语。

标签: c++ oop class


【解决方案1】:

您似乎将其他一些 Java 之类 语言的规则应用于 C++。
这是一个根本性的问题,从长远来看会导致各种问题。

你需要学习 C++ 的习语。

在 C++ 中,您希望使用 C++ 字符串 (std::string) 而不是 C-String 接口。

#include <string.h>   // C-Interface

// What you really want
#include <string>     // C++ Interface

如果你的类包含一个指针,那么你可能做错了什么。 RAW 指针应包装在智能指针(或容器)中,以正确控制其寿命。如果您将指针放入业务类,您将打破关注点分离原则。

class CPoint
{
    protected:
        int m_x;
        int m_y;

        int *m_p;  // What is it supposed to be?
                   // Who owns it?

由于您的班级有一个指针,它违反了三规则。
如果您想管理此类中的指针(而您不想(破坏关注点分离)),那么您应该已经实现了三规则(C++ 11 中的五规则)(查找)。如果你想了解如何处理 RAW 指针,请看这里https://stackoverflow.com/a/1846409/14065

不需要克隆方法。这就是复制构造函数的用途。您不是在编写需要克隆的类(否则它会有一个虚拟的析构函数)。您的类不是多态的,不会派生自。因此,复制构造函数将完美地工作。

CPoint*         clone();
static CPoint*  clone(CPoint& p);

// Copy constructor looks like this:
CPoint(CPoint const& rjs)

// Assignment operator looks like this:
CPoint& operator=(CPoint& rhs)

但如果将 RAW 指针正确包装在适当的类中,则不需要这样做。编译器生成的这些方法的默认版本可以正常工作。

完全破坏封装的好方法。

int getX();
int getY();
void setX(int x);
void setY(int y);

串起来!船尾。你真正想要的是一个序列化方法。

void toString();

// serializer look like this:

friend std::ostream& operator<<(std::ostream& stream, CPoint const& data)
{
     // Convert CPoint (data) to the stream.
     return stream;
}

在 C++ 中,除非需要,否则我们不会动态创建对象。
在这里你不需要。创建本地对象效果更好,因为即使存在异常也可以保证它们的生命周期。

// Rather than dynamically creating them
CPoint *p1 = new CPoint(10, 20);
CPoint *p2 = new CPoint(30, 40);

// Just declare two local variables:
CPoint  p1 = CPoint(10, 20);
CPoint  p2(30, 40);           // Alternative to the above but means the same.

// Much better to use operator<<
// Also shows the functions are badly named. You are not converting to string.
// but rather printing them to a stream.
p1->toString();
p2->toString();

std::cout << p1;
myFileStream << p2;  // allows you to easily specify the actual stream.

复制构造函数在复制对象时效果更好

CPoint *p3;
p3 = CPoint::clone(*p1);

// If we were still using pointers. 
CPoint* p3 = new CPoint(p1);

// But much nicer to not even use pointers
CPoint  p3(p1);

如果您在函数中看到手动调用删除,这通常是设计错误。

delete p1;
delete p2;
delete p3;
delete p4;

如果你有将它们包装在智能指针(或容器)中的指针,比如类,则它们可以异常安全地使用。这是因为对于本地对象,保证会调用析构函数,因此当指针超出范围时,您的对象将正确删除指针。目前这段代码不是异常安全的,如果异常传播通过它们就会泄漏。

小提示:main() 比较特殊。如果您不指定返回值,编译器会为您植入return 0;。如果您的应用程序没有错误状态,最好使用此功能向其他开发人员表明您的代码将始终干净退出。

return 0;

我会这样重写:

#include <iostream>
#include <string>
#include <vector>

class CPoint
{
protected:
    int m_x;
    int m_y;

    std::vector<int> m_p;

public:
    // If you don't explicitly initialize m_x and m_y them
    // they will have indeterminate (random) values.
    CPoint()             : m_x(0), m_y(0) {m_p.push_back(1000);}
    CPoint(int x, int y) : m_x(x), m_y(y) {m_p.push_back(x + y);}

    int getX()        { return m_x;}
    int getY()        { return m_y;}
    void setX(int x)  { m_x = x;}
    void setY(int y)  { m_y = y;}

    friend std::ostream& operator<<(std::ostream& stream, CPoint const& d)
    {
        return stream << "(" << d.m_x << ", " << d.m_y<< ", " << d.m_p[0] << ")" << std::endl;
    }
};




int main(int argc, char* argv[])
{
    CPoint p1(10, 20);
    CPoint p2(30, 40);

    std::cout << p1 << p2;

    CPoint p3(p1);

    std::cout << p3;

    CPoint p4(p2);
    std::cout << p4;

    p1.setX(50);
    p1.setY(60);
    p2.setX(80);
    p2.setY(90);

    std::cout << p1 << p2 << p3 << p4;
    int a;
    std::cin >> a;
}

【讨论】:

  • 您所说的“完全破坏封装的好方法”是什么意思?我认为您正在评论此语句下方的代码块 - 访问器和修改器。为什么这会破坏封装?
  • @larrylampco:get(ers) 和 set(ers) 破坏封装。这是因为它们通过接口暴露了内部实现细节。最好的方法是让您公开可以对数据执行的工作(动词)(例如打印,现在您不需要公开任何实现细节)。
  • get(ers) 和 set(ers) 不会破坏封装,它会促进封装,这基本上是一种禁止在没有任何控制的情况下直接访问数据成员的方法。我也对这句话感到惊讶:“在 C++ 中,除非我们需要,否则我们不会动态创建对象”。这是完全错误的,在 C++ 中使用指针对于允许作为面向对象编程的支柱的 polimorfism 是必不可少的。
  • @kato2 在这个问题的所有部分都是“polimorfism”。在这里,我将“动态”与“本地”进行对比。如果您可以声明一个局部变量,则无需动态创建对象。因此,这句话的unless we need to 部分。
  • @kato2 问题在于暴露了成员的内部类型(从而破坏了对象的封装)。你想要的是动作方法。而不是obj.setSpeed(obj.getSpeed() + 5) 暴露speed 对象的type 并因此将您锁定在必须维护的特定实现中,您应该拥有obj.increaseSpeed(5) 让内部对象以您喜欢的任何方式实现速度。也许我可以写:"This technique breaks type encapsulation of an class"
【解决方案2】:

在这个例子中是一个整数,但可以是任何类型。我的问题是克隆一个包含指向另一种类型的指针的对象。

我认为这里基本上有两种情况:您希望包含对象拥有指向对象;或者您不希望包含对象拥有指向的对象。

让我们从非拥有开始。 C++ 提供什么工具来表示非拥有指针?好吧,常规指针是非拥有的。以及如何复制常规指针?你什么都不做。你让编译器处理它,生成你可以随意使用的正确的复制构造函数(当你在它的时候,让编译器也生成一个析构函数)。

那么拥有呢?拥有指针的工具是什么?好吧,在大多数情况下,您甚至不需要指针:只需直接存储一个值,然后再次让编译器生成正确的复制构造函数(以及析构函数!)。在提供的示例中,int m_p; 可以很好地工作。

当涉及多态基类时,这种情况有一个烦恼:复制可能会导致切片。 C++ 是否为这种情况提供了工具?可悲的是,它没有。你必须手写。但请帮自己一个忙,不要将这些问题与课堂上的其他人混为一谈(单一职责原则)。

编写一个拥有单个指针的可重用类(加分项:使其成为模板),在销毁时清理它,并在复制构造函数中执行多态复制(常见的习惯用法涉及虚拟克隆函数)。然后在您的CPoint 中输入该可重用类的值,然后……您猜对了!让编译器生成正确的拷贝构造函数。

【讨论】:

    【解决方案3】:

    除了浅拷贝直接数据成员m_xm_y,还需要深拷贝指针成员m_p。由于您没有显示此类的构造函数或m_p 真正指向的内容,我将假设m_p 指向int 数组的第一个元素。深度复制需要:

    1. 实例化一个与原始数组大小相同(或更大)的 int 新数组
    2. 将原始数组中的每个元素复制到新数组中
    3. 在克隆对象中设置m_p 指向这个新数组的第一个元素

    如何做到这一点的示例:

    CPoint* CPoint::clone(CPoint& rhs)
    {
      CPoint* ret = new CPoint;
      ret->m_x = rhs.m_x;
      ret->m_y = rhs.m_y;
    
      size_t m_p_count = /* somehow determine the size of rhs.m_p */;
      ret->m_p = new int[m_p_count];
      std::copy(&rhs.m_p[0], &rhs.m_p[m_p_count], ret->m_p);
    
      return ret;
    }
    

    关于您的代码的几点说明:

    1. 最好使用vector&lt;int&gt; 而不是指向int 数组的原始指针。
    2. 除了 #1,您应该使用智能指针而不是原始指针
    3. 我在上面的代码中没有看到任何方法来确定数组的大小。如果您使用vector&lt;int&gt;,这将很容易——只需调用vecctor&lt;int&gt;::size()。显然,您需要知道数组的大小才能复制它。
    4. clone() 类型函数通常仅在通过基类指针复制多态对象时有用。由于您的课程以及您对它的使用不属于这一类,因此clone() 函数首先不是正确的方法。考虑使用复制构造函数和复制赋值运算符,不要忘记也实现析构函数。更好的是,完全避免所有这些东西并关注Rule of Zero

    【讨论】:

    • 我没有看到他使用 int 数组
    • @DenisErmolin:由于 OP 没有向我们展示m_p 是如何分配的,我们都不知道m_p 真正指向什么。我必须做出一个假设,我已经提到过。通常指向int 的指针会指向一个数组,所以我假设m_p 是指向int 数组的第一个元素的指针。其他假设可能同样有效。
    • 由于类不是多态的 clone() 是错误的方法。
    • @Loki:虽然是真的,但clone() 是由 OP 提供的——我只是按要求回答了这个问题。但是,我确实也尝试解决了 OP 在 C++ 中使用 Java-ish 习语的许多明显问题,并且我也进行了编辑以包含您的观察。
    【解决方案4】:

    您必须为CPoint 内的所有指针重新分配内存,并将它们的数据复制到新内存中。在您的情况下,您必须执行以下操作:

    CPoint clone()
    {
       CPoint p;
       p = *this;
       p.m_p = new int();
       *p.m_p = *m_p;
       return p;
    }
    

    【讨论】:

      【解决方案5】:

      您必须问自己:您的对象的每个实例是否都“拥有”它所指向的对象,或者它们是否都引用了其他东西所拥有的公共对象?

      当您拥有所有权案例时,每个实例都必须指向一个单独的副本。这意味着您不必复制指针,您必须创建它指向的对象的克隆并将这个新对象分配给副本的指针。

      【讨论】:

        【解决方案6】:

        假设 m_p 只指向一个整数(而不是整个数组),克隆可以这样完成:

        CPoint* CPoint::clone()
        {
            CPoint* cloned = new CPoint(m_x, m_y);
            if (m_p)
            {
                cloned->m_p = new int;
                *cloned->m_p = *m_p;
            }
            return cloned;
        }
        

        请注意,此类成员指针的唯一目的是增加具有 NULL 值的额外可能性 - 这可能具有单独的含义。

        另请注意,必须执行以下操作以避免内存泄漏和堆损坏:

        • 必须“禁用”复制构造函数和赋值运算符(声明为私有)
        • 析构函数必须删除 m_p。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-04-01
          • 2016-04-08
          • 1970-01-01
          • 1970-01-01
          • 2013-02-01
          • 2023-04-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多