学习C ++:返回引用并绕过切片

时间:2010-12-10 04:22:58

标签: c++ reference polymorphism object-slicing

我有一段时间了解参考文献。请考虑以下代码:

class Animal
{
public:
    virtual void makeSound() {cout << "rawr" << endl;}
};

class Dog : public Animal
{
public:
    virtual void makeSound() {cout << "bark" << endl;}
};

Animal* pFunc()
{
    return new Dog();
}

Animal& rFunc()
{
    return *(new Dog());
}

Animal vFunc()
{
    return Dog();
}

int main()
{
    Animal* p = pFunc();
    p->makeSound();

    Animal& r1 = rFunc();
    r1.makeSound();

    Animal r2 = rFunc();
    r2.makeSound();

    Animal v = vFunc();
    v.makeSound();
}

结果是:“bark bark rawr rawr”。

以Java的思维方式(显然已经破坏了我对C ++的概念化),结果将是“树皮树皮树皮”。我从previous question了解到,这种差异是由于切片造成的,我现在对切片的理解有了很好的理解。

但是,让我们说我想要一个返回Animal值的函数,它实际上是一个Dog。

  1. 我是否正确理解我能得到的最接近的是参考
  2. 此外,使用rFunc接口的人是否有责任看到返回的引用分配了一个Animal&amp;? (或以其他方式故意将对动物的引用分配给动物,通过切片,丢弃多态性。)
  3. 我是怎么回事对新生成的对象的引用而不做我在rFunc上面做的愚蠢的事情? (至少我听说这是愚蠢的。)

  4. 更新:因为到目前为止每个人似乎都同意rFunc它是非法的,这带来了另一个相关的问题:

    如果我传回指针,如果是这种情况,我如何与程序员沟通指针不是他们要删除的?或者,我如何传达指针在任何时候(来自同一个线程但是不同的函数)都要删除,以便调用函数不应该存储它,如果是这种情况。是通过评论进行沟通的唯一方法吗?这似乎很草率。

    注意:所有这些都导致了我正在研究的模板化shared_pimpl概念的想法。希望我能学到足够的东西,在几天内发布一些相关信息。

8 个答案:

答案 0 :(得分:5)

1)如果你正在创建新对象,你永远不想要返回一个引用(参见你自己对#3的评论。)你可以返回一个指针(可能由std::shared_ptrstd::auto_ptr包裹)。 (您也可以通过副本返回,但这与使用new运算符不兼容;它也与多态性略有不兼容。)

2)rFunc是错的。不要那样做。如果您使用new创建对象,则通过(可选地包装)指针返回它。

3)你不应该这样做。这就是指针的用途。


编辑(回复您的更新:)很难描绘您所描述的情景。一旦调用者调用其他(特定)方法,返回指针可能无效会更准确吗?

我建议不要使用这样的模型,但是如果你绝对必须这样做,并且你必须在你的API中强制执行这个,那么你可能需要添加一个间接级别,甚至两个。示例:将实际对象包装在包含实际指针的引用计数对象中。删除真实对象时,引用计数对象的指针设置为null。这很难看。 (可能有更好的方法,但它们可能仍然很难看。)

答案 1 :(得分:2)

回答问题的第二部分(“我如何传达指针随时可能被删除”) -

这是一种危险的做法,并且您需要考虑细微的细节。这本质上是活泼的。

如果可以在任何时间删除指针,那么从另一个上下文中使用它是绝对不安全的,因为即使你检查“你仍然有效吗?”每次,检查后可能会删除一小部分,但在您开始使用之前。

执行这些操作的一种安全方法是“弱指针”概念 - 将对象存储为共享指针(一个间接级别,可以随时释放),并使返回值为弱指针 - 在您可以使用之前必须查询的东西,并且必须在使用它之后释放。这样,只要对象仍然有效,您就可以使用它。

伪代码(基于发明的弱和共享指针,我没有使用Boost ......) -

weak< Animal > animalWeak = getAnimalThatMayDisappear();
// ...
{
    shared< Animal > animal = animalWeak.getShared();
    if ( animal )
    {
        // 'animal' is still valid, use it.
        // ...
    }
    else
    {
        // 'animal' is not valid, can't use it. It points to NULL.
        // Now what?
    }
}
// And at this point the shared pointer of 'animal' is implicitly released.

但这很复杂且容易出错,并且可能会让您的生活更加艰难。如果可能的话,我建议选择更简单的设计。

答案 2 :(得分:1)

为了避免切片,您必须返回或传递指针到对象。 (注意,引用基本上是'永久取消引用的指针'。

Animal r2 = rFunc();
r2.makeSound();

这里,r2是实例化的(使用编译器生成的拷贝ctor),但它正在离开 脱掉狗的部分。如果你这样做,切片将不会发生:

Animal& r2 = rFunc();

但是你的vFunc()函数在方法本身内切片。

我还会提到这个功能:

Animal& rFunc()
{
    return *(new Dog());
}

这很奇怪而且不安全;您正在创建对临时未命名变量的引用(dereferenced Dog)。返回指针更合适。返回引用通常用于返回成员变量等等。

答案 3 :(得分:1)

  

如果我传回指针,如果是这种情况,我如何与程序员沟通指针不是他们要删除的?或者,我如何在任何时候(从相同的线程但不同的函数)传达指针被删除的信息,以便调用函数不应该存储它,如果是这种情况。

如果你真的不能信任用户,那就不要给它们一个指针:传回一个整数类型的句柄并暴露一个C风格的界面(例如,你有一个实例向量)对于fence,你公开了一个函数,该函数将整数作为第一个参数,索引到向量并调用成员函数)。这是老式的方式(尽管我们并不总是有像“成员函数”这样的花哨的东西;))。

否则,请尝试使用具有适当语义的智能指针。没有人会认为delete &*some_boost_shared_ptr;是个好主意。

答案 4 :(得分:1)

  

但是,让我们说我想要一个返回Animal值的函数,它实际上是一个Dog。

     
      
  1. 我是否理解我最接近的是参考?
  2.   

是的,你是对的。但我认为问题并不在于你不理解引用,而是你不了解C ++中不同类型的变量或者new如何在C ++中工作。在C ++中,变量可以是原始数据(int,float,double等),对象或对基元和/或对象的指针/引用。在Java中,变量只能是基元或对象的引用。

在C ++中,当您声明一个变量时,会分配实际内存并将其与变量相关联。在Java中,您必须使用new显式创建对象,并将新对象显式分配给变量。这里的关键点是,在C ++中,当变量是指针或引用时,用于访问的对象和变量不是同一个东西。 Animal a;表示与Animal *a;不同的内容,这意味着与Animal &a;不同的内容。这些都没有兼容类型,并且它们不可互换。

在C ++中键入Animal a1时。创建了一个新的Animal对象。因此,当您键入Animal a2 = a1;时,最终会在内存中的不同位置生成两个变量(a1a2)和两个Animal个对象。两个对象具有相同的值,但您可以根据需要单独更改其值。在Java中,如果键入相同的确切代码,则最终会得到两个变量,但只有一个对象。只要您没有重新分配任何一个变量,它们就会始终具有相同的值。

  
      
  1. 此外,使用rFunc接口的人是否有责任看到返回的引用分配了一个Animal&amp;? (或以其他方式故意将对动物的引用分配给动物,通过切片,丢弃多态性。)
  2.   

使用引用和指针时,可以访问对象的值,而无需将其复制到要使用它的位置。这允许您从声明对象的花括号外部更改它。引用通常用作函数参数或返回对象的私有数据成员而不创建它们的新副本。通常,当您收到参考时,您不会将其分配给任何内容。使用您的示例,而不是将rFunc()返回的引用分配给变量,通常会键入rFunc().makeSound();

所以,是的,rFunc()的用户有责任,如果他们将返回值分配给任何东西,则将其分配给引用。你可以看到原因。如果您将rFunc()返回的引用分配给声明为Animal animal_variable的变量,则最终会得到一个Animal变量,一个Animal对象和一个Dog宾语。与Animal关联的animal_variable对象尽可能是Dog引用从rFunc()返回的animal_variable对象的副本。但是,您无法从Dog获得多态行为,因为该变量与Dog对象无关。引用返回的new对象仍然存在,因为您使用{ // the following expressions evaluate to ... Animal local; // an object that will be destroyed when control exits this block Animal(); // an unamed object that will be destroyed immediately if not bound to a reference new Animal(); // an unamed Animal *pointer* that can't be deleted unless it is assigned to a Animal pointer variable. { // doing other stuff } } // <- local destroyed 创建了它,但它不再可访问 - 它已被泄露。

  
      
  1. 我是怎么回事对新生成的对象的引用而不做我在rFunc上面做的愚蠢的事情? (至少我听说这是愚蠢的。)
  2.   

问题是您可以通过三种方式创建对象。

new

C ++中的所有Animal *AnimalPointer;都是在内存中创建对象,直到你这么说才会销毁它。但是,为了摧毁它,你必须记住它在内存中的创建位置。你通过创建指针变量来做到这一点, new Animal(), 并将AnimalPointer = new Animal();返回的指针指定给它, Animal。 要在完成后隐藏delete AnimalPointer;对象,您必须输入{{1}}。

答案 5 :(得分:0)

(我忽略了动态内存进入引用导致内存泄漏的问题......)

当Animal是一个抽象基类时,你的分裂问题就会消失。这意味着它至少有一个纯虚方法,无法直接实例化。以下内容成为编译器错误:

Animal a = rFunc();   // a cannot be directly instantiated
                      // spliting prevented by compiler!

但编译器允许:

Animal* a = pFunc();  // polymorphism maintained!
Animal& a = rFunc();  // polymorphism maintained!

因此编译器节省了一天!

答案 6 :(得分:0)

第1点:不要使用引用。使用指针。

第2点:你上面的东西被称为分类法,它是分层分类方案。分类法是一种完全不适合面向对象建模的示例。你的琐碎的例子只有作用,因为你的基础动物假设所有的动物发出噪音,并且不能做任何有趣的事情。

如果您尝试实现关系,例如

虚拟bool Animal :: eats(Animal * other)= 0;

你会发现你不能这样做。问题是:狗不是动物抽象的子类型。分类法的整个要点是分区的每个级别的类都有一个新的有趣属性。

例如:脊椎动物有骨干,我们可以问它是由软骨或骨头组成的......我们甚至不能问无脊椎动物的问题。

要完全理解,您必须看到您无法制作Dog对象。毕竟,这是一个抽象,对吧?因为,有凯尔派和牧羊犬,并且个体狗必须属于某些物种..分类方案可以如您所愿,但它永远不会支持任何具体的个体。 Fido是 not-a-Dog ,这只是他的分类标签。

答案 7 :(得分:0)

如果要从方法返回多态类型并且不想在堆上分配它,可以考虑将其作为该方法类中的字段,并使该函数返回指向任何基类的指针你要。