【问题标题】:Learning C++: returning references AND getting around slicing学习 C++:返回引用并绕过切片
【发布时间】:2010-12-10 04:22:57
【问题描述】:

我在理解参考文献时遇到了麻烦。考虑以下代码:

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++ 的概念化),结果将是“bark bark bark bark”。我从previous question 了解到,这种差异是由于切片造成的,我现在对切片是什么有了很好的理解。

但是假设我想要一个返回一个真正是狗的 Animal 值的函数。

  1. 我是否正确理解我可以获得的最接近的是参考
  2. 此外,使用 rFunc 接口的人是否有义务查看返回的引用是分配一个 Animal&? (或者以其他方式故意将引用分配给通过切片丢弃多态性的 Animal。)
  3. 我到底应该如何返回对新生成对象的引用而不做我上面在 rFunc 中所做的愚蠢事情? (至少我听说这很愚蠢。)

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

如果我传回一个指针,如果是这种情况,我如何与程序员沟通该指针不是他们要删除的?或者,我如何传达指针随时可能被删除(来自同一个线程但不同的函数),以便调用函数不应该存储它,如果是这种情况。是通过 cmets 进行交流的唯一方法吗?这看起来很草率。

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

【问题讨论】:

  • Java 引用更像 C++ 指针。 Java 没有 C++ 引用。
  • @Alex 的建议很好。 Scott Meyers 所著的“Effective C++”和“More Effective C++”都是优秀的书籍,极大地帮助了我对 C++ 的理解,尤其是对于像你这样的棘手问题。当然,我现在已经忘记了大部分内容,因为我已经有几年没有认真研究过 C++ 了:(

标签: c++ reference polymorphism object-slicing


【解决方案1】:

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

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

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


编辑(响应您的更新:)很难想象您所描述的场景。如果调用者调用其他(特定)方法,返回的指针可能无效,这样说会更准确吗?

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

【讨论】:

【解决方案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.

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

【讨论】:

  • 需要注意的是,不能直接对弱指针进行操作,而必须创建一个共享指针来保证底层对象的生命周期。直接使用弱指针会产生未定义的行为(可以在测试之后和操作完成之前删除对象)。
【解决方案3】:

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

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

在这里,r2 正在实例化(使用编译器生成的复制 ctor),但它正在离开 关闭狗部分。如果您这样做,则不会发生切片:

Animal& r2 = rFunc();

但是,您的 vFunc() 函数会切入方法本身。

我还要提一下这个函数:

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

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

【讨论】:

  • 仍然可以用指针分割:Animal* pAnimal = *dog;会导致分裂。
  • 那么这个答案是否具有误导性? stackoverflow.com/a/3835757/1538531 因为当你需要多态返回时,你无法绕过返回指针。
【解决方案4】:

如果我传回一个指针,如果是这种情况,我如何与程序员沟通该指针不是他们要删除的?或者,我如何传达指针随时会被删除(来自同一个线程但不同的函数),以便调用函数不应该存储它,如果是这种情况。

如果你真的不能信任用户,那么根本不要给他们一个指针:传回一个整数类型的句柄并公开一个 C 风格的接口(例如,你身边有一个实例向量栅栏,并且您公开了一个函数,该函数将整数作为第一个参数,索引到向量并调用成员函数)。那是老式的方式(尽管我们并不总是有诸如“成员函数”之类的花哨的东西;))。

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

【讨论】:

    【解决方案5】:

    但是假设我想要一个返回一个真正是狗的 Animal 值的函数。

    1. 我是否正确理解我能得到的最接近的是参考?

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

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

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

    1. 此外,使用 rFunc 接口的人是否有义务查看返回的引用是分配一个 Animal&? (或者以其他方式故意将引用分配给通过切片丢弃多态性的 Animal。)

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

    所以,是的,rFunc() 的用户有责任,如果他们将返回值分配给任何东西,将其分配给引用。你可以看到为什么。如果将rFunc() 返回的引用分配给声明为Animal animal_variable 的变量,则最终会得到一个Animal 变量、一个Animal 对象和一个Dog 对象。与animal_variable 关联的Animal 对象尽可能是Dog 对象的副本,该对象通过引用从rFunc() 返回。但是,您无法从animal_variable 获得多态行为,因为该变量与Dog 对象无关。通过引用返回的Dog 对象仍然存在,因为您使用new 创建它,但它不再可访问——它已泄露。

    1. 我到底应该如何返回对新生成对象的引用而不做我上面在 rFunc 中所做的愚蠢事情? (至少我听说这很愚蠢。)

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

    { // 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
    

    new 在 C++ 中所做的所有事情都是在内存中创建对象,除非你这么说,否则它不会被销毁。但是,为了销毁它,您必须记住它在内存中的创建位置。你可以通过创建一个指针变量来做到这一点, Animal *AnimalPointer;, 并将new Animal()返回的指针分配给它, AnimalPointer = new Animal();。 要在完成后销毁 Animal 对象,您必须输入 delete AnimalPointer;

    【讨论】:

      【解决方案6】:

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

      第 2 点:您上面的内容称为分类法,它是分层分类方案。分类法是一种完全不适合面向对象建模的范例。您的简单示例仅有效,因为您的基础 Animal 假设所有动物都会发出声音,并且不能做任何其他有趣的事情。

      如果你尝试实现一个关系,比如

      virtual bool Animal::eats(Animal *other)=0;

      你会发现你做不到。问题是:Dog 不是 Animal 抽象的子类型。 Taxonomies 的全部意义在于分区的每个级别的类都有新的有趣的属性。

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

      要完全理解,您必须看到您不能制作 Dog 对象。毕竟,这是一个抽象,对吧?因为,有 Kelpies 和 Collies,而单独的 Dog 必须属于某些物种 .. 分类方案可以随心所欲,但它永远无法支持任何具体的个体。 Fido 不是狗,这只是他的分类标签。

      【讨论】:

        【解决方案7】:

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

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

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

        但编译器允许:

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

        因此编译器节省了时间!

        【讨论】:

        • 呃,没有。在Animal&amp; a = rFunc()的情况下,完成后如何删除对象?
        • @Dan 我知道,splitting 问题消失了......不是内存泄漏 :)
        • 好的,但不要鼓励新手 ;-)
        【解决方案8】:

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

        【讨论】:

          猜你喜欢
          • 2021-08-09
          • 1970-01-01
          • 2020-11-12
          • 2010-12-22
          • 1970-01-01
          • 2017-01-15
          • 1970-01-01
          • 2022-06-15
          相关资源
          最近更新 更多