学习C++:返回引用和解决切片问题

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"。我从上一个问题中了解到,这种差异是由于切片引起的,现在我很好地理解了什么是切片。

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

  1. 我是否正确理解,我能得到的最接近的是一个引用
  2. 此外,使用rFunc接口的人是否有责任确保返回的引用被分配给Animal&?(或者有意将引用分配给通过切片丢弃多态性的Animal。)
  3. 我应该如何返回对新生成的对象的引用,而不是像rFunc中所做的愚蠢之举?(至少我听说过这很愚蠢。)

更新:由于大家似乎都认为rFunc是不合法的,那么就会引出另一个相关问题:
如果我传回一个指针,如何告诉程序员如果这样的情况发生,该指针不属于他们删除?或者,如果该指针随时可能被删除(来自同一线程但不同函数),如何通知调用函数不应存储它?这种情况下,唯一的沟通方式是通过注释吗?那似乎有些粗糙。
注意:所有这些都是为了我正在开发的一个模板化shared_pimpl概念。希望几天后能学到足够多的知识,发布一些相关内容。

4
Java 的引用更像 C++ 的指针。Java 没有像 C++ 引用一样的概念。 - Billy ONeal
@Alex的建议很好。Scott Meyers所著的《Effective C++》和《More Effective C++》都是非常优秀的书籍,对于像你这样的棘手问题,它们能够极大地帮助你理解C++。当然,由于我已经几年没有进行严肃的C++编程了,所以现在我已经忘记了大部分内容 :( - Cameron Skinner
8个回答

6

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

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

3) 您不应该这样做。这就是指针的作用。


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

我建议不要使用这种模型,但如果您绝对必须这样做,并且必须在您的API中执行此操作,则可能需要添加一级或两级间接性。例如:将真实对象包装在引用计数对象中,该对象包含真实指针。当删除真实对象时,引用计数对象的指针设置为null。这很丑陋。(可能有更好的方法来解决它,但它们可能仍然很丑陋。)


指针万岁。它们不会隐式转换为值,因此您的调用者会收到有关潜在多态性的警告。 - Ben Voigt
你可能会对我的新问题感兴趣:https://dev59.com/8FLTa4cB1Zd3GeqPbpAO - JnBrymn

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.

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


需要注意的是,您不能直接操作弱指针,而必须创建一个共享指针来确保底层对象的生命周期。直接使用弱指针会导致未定义的行为(在测试之后和操作完成之前,对象可能已被删除)。 - David Rodríguez - dribeas

1
但假设我想要一个返回一个实际上是狗的动物值的函数。 我理解正确,我能得到的最接近的是引用吗?
是的,你是正确的。但我认为问题并不在于你不理解引用,而是你不理解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&?(或者有意将引用分配给通过切片丢弃多态性的Animal。)
使用引用和指针时,可以访问对象的值而无需将其复制到您想要使用它的位置。这使您可以从声明该对象存在的花括号外部更改它。引用通常用作函数参数或返回对象的私有数据成员,而不必进行新的复制。通常,当您接收到一个引用时,不会将其分配给任何东西。以您的示例为例,与将由 rFunc() 返回的引用分配给变量不同,通常会键入 rFunc().makeSound();

好的,是的,如果使用rFunc()的用户将返回值分配给任何东西,那么就应该将其分配给一个引用。你可以明白为什么了。如果将rFunc()返回的引用分配给一个声明为Animal animal_variable的变量,那么你最终会得到一个Animal变量、一个Animal对象和一个Dog对象。与animal_variable相关联的Animal对象尽可能地复制了从rFunc()以引用返回的Dog对象。但是,你无法从animal_variable获得多态行为,因为该变量与Dog对象没有关联。使用new创建了仍存在的Dog对象,但它不再可访问--已泄漏。

  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;

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

在这里,r2正在被实例化(使用编译器生成的复制构造函数),但它留下了Dog部分。如果你像这样做,就不会发生切片:

Animal& r2 = rFunc();

然而,您的vFunc()函数在方法本身内部进行切片。

我还要提到这个函数:

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

这很奇怪也不安全;你正在创建一个对临时未命名变量(解引用的 Dog)的引用。更合适的做法是返回指针。通常使用返回引用来返回成员变量等。


你仍然可以使用指针进行分割:Animal* pAnimal = *dog; 会导致分割。 - Doug T.
那么这个答案是误导性的吗?https://dev59.com/Fm865IYBdhLWcg3wZNvT#3835757 因为当你需要多态返回时,你无法避免返回一个指针。 - Derek

1
如果我传回一个指针,如何告诉程序员如果这种情况发生,该指针不是他们的删除对象?或者,如果在同一线程但不同函数中可以随时删除该指针,如何通知调用函数不要存储它?
如果您真的不能信任用户,那么根本不要给他们指针:传回整数类型的句柄并公开C风格接口(例如,您在界面的一侧有实例向量,并公开一个以整数作为第一个参数的函数,索引到向量并调用成员函数)。这是老式的方法(尽管我们并不总是拥有像“成员函数”这样的花哨东西;))。
否则,请尝试使用具有适当语义的智能指针。没有理智的人会认为delete &*some_boost_shared_ptr;是个好主意。

0
(我忽略了您在动态内存中使用引用导致内存泄漏的问题...)
当Animal是一个抽象基类时,您的分裂问题就会消失。这意味着它至少有一个纯虚拟方法,并且不能直接实例化。以下内容将成为编译器错误:
Animal a = rFunc();   // a cannot be directly instantiated
                      // spliting prevented by compiler!

但编译器允许:

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

因此编译器拯救了这一天!


嗯,不对。在 Animal& a = rFunc() 这种情况下,你使用完对象后如何删除它呢? - Dan Breslau
@Dan 我知道,分割问题已经解决了...但内存泄漏还没解决 :) - Doug T.
好的,但不要鼓励新手 ;-) - Dan Breslau

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

0

第一点:不要使用引用。请使用指针。

第二点:你上面的东西被称为分类法,是一种层次化的分类方案。分类法是一种非常不适合于面向对象建模的范例。你的简单示例之所以能够工作,是因为你的基本动物假设所有动物都会发出噪音,而且没有其他有趣的特征。

如果你尝试实现关系,例如:

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

你会发现自己无法做到。问题在于:狗不是动物抽象的子类型。分类法的整个重点在于每个分区级别的类具有新的有趣属性。

例如:脊椎动物有脊椎,我们可以询问其是否由软骨或骨头构成。我们甚至无法向无脊椎动物询问这个问题。

要完全理解,你必须明白你不能创建一个狗对象。毕竟,它只是一个抽象,对吧?因为有牧羊犬和柯利犬,一个个体狗必须属于某种物种...分类方案可以非常深入,但它永远不可能支持任何具体的个体。菲多不是狗,那只是他的分类标签。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接