12

我在理解参考资料方面遇到了麻烦。考虑以下代码:

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”。我从之前的问题中了解到,这种差异是由于切片造成的,我现在对切片是什么有了很好的理解。

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

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

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

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

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

4

8 回答 8

6

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

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

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


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

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

于 2010-12-10T04:35:41.187 回答
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.

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

于 2010-12-10T05:15:13.273 回答
1

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

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

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

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

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

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

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

所以,是的rFunc(),如果他们将返回值分配给任何东西,则用户有责任将其分配给引用。你可以看到为什么。如果将返回的引用分配给rFunc()声明为 的变量Animal animal_variable,则最终得到一个Animal变量、一个Animal对象和一个Dog对象。Animal关联的对象animal_variable尽可能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

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

于 2010-12-16T22:21:47.940 回答
1

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

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

在这里, r2 正在实例化(使用编译器生成的复制 ctor),但它忽略了 Dog 部分。如果你这样做,切片将不会发生:

Animal& r2 = rFunc();

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

我还会提到这个功能:

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

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

于 2010-12-10T04:40:19.497 回答
1

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

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

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

于 2010-12-10T06:02:42.060 回答
0

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

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

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

但编译器允许:

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

因此编译器节省了一天!

于 2010-12-10T04:44:05.153 回答
0

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

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

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

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

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

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

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

于 2010-12-10T04:45:49.160 回答
0

If you want to return a polymorphic type from a method and don't want to assign it on the heap you could consider making it a field in that method's class and making the function to return a pointer to it of whatever base class you want.

于 2013-04-17T21:38:14.980 回答