【问题标题】:C++ Object InstantiationC++ 对象实例化
【发布时间】:2008-12-02 09:23:35
【问题描述】:

我是一名试图理解 C++ 的 C 程序员。许多教程使用 sn-p 演示对象实例化,例如:

Dog* sparky = new Dog();

这意味着稍后你会这样做:

delete sparky;

这是有道理的。现在,在不需要动态内存分配的情况下,是否有任何理由使用上述代替

Dog sparky;

并在 sparky 超出范围时调用析构函数?

谢谢!

【问题讨论】:

    标签: c++ instantiation


    【解决方案1】:

    相反,您应该始终更喜欢堆栈分配,因为根据经验,您永远不应该在用户代码中使用 new/delete。

    正如您所说,当变量在堆栈上声明时,它的析构函数会在超出范围时自动调用,这是您跟踪资源生命周期和避免泄漏的主要工具。

    所以一般来说,每次需要分配资源时,无论是内存(通过调用new)、文件句柄、套接字还是其他任何东西,都将它包装在一个类中,构造函数获取资源,析构函数释放它.然后,您可以在堆栈上创建该类型的对象,并保证您的资源在超出范围时被释放。这样您就不必到处跟踪新/删除对来确保避免内存泄漏。

    这个成语最常见的名字是RAII

    还可以查看用于在极少数情况下包装结果指针的智能指针类,当您必须在专用 RAII 对象之外分配新的东西时。相反,您将指针传递给智能指针,然后智能指针跟踪其生命周期,例如通过引用计数,并在最后一个引用超出范围时调用析构函数。标准库有 std::unique_ptr 用于简单的基于范围的管理,std::shared_ptr 使用引用计数来实现共享所有权。

    许多教程演示对象 使用 sn-p 进行实例化,例如 ...

    所以您发现大多数教程都很糟糕。 ;) 大多数教程教你糟糕的 C++ 实践,包括在不需要时调用 new/delete 来创建变量,并且让你很难跟踪分配的生命周期。

    【讨论】:

    • 当您想要类似 auto_ptr 的语义(转移所有权)但保留非抛出交换操作并且不想要引用计数的开销时,原始指针很有用。边缘情况可能,但很有用。
    • 这是一个正确的答案,但我永远不会养成在堆栈上创建对象的习惯的原因是因为它永远不会完全清楚该对象有多大。您只是要求堆栈溢出异常。
    • 格雷格:当然。但正如你所说,一个边缘案例。一般来说,最好避免使用指针。但他们使用这种语言是有原因的,不可否认。 :) dviljoen:如果对象很大,则将其包装在 RAII 对象中,该对象可以在堆栈上分配,并包含指向堆分配数据的指针。
    • @dviljoen:不,我不是。 C++ 编译器不会不必要地膨胀对象。您会看到的最糟糕的情况通常是四舍五入到最接近的四个字节的倍数。通常,包含指针的类将占用与指针本身一样多的空间,因此不会占用堆栈。
    【解决方案2】:

    虽然在分配和自动释放方面将东西放在堆栈上可能是一个优势,但它也有一些缺点。

    1. 您可能不想在堆栈上分配大对象。

    2. 动态调度!考虑这段代码:

    #include <iostream>
    
    class A {
    public:
      virtual void f();
      virtual ~A() {}
    };
    
    class B : public A {
    public:
      virtual void f();
    };
    
    void A::f() {cout << "A";}
    void B::f() {cout << "B";}
    
    int main(void) {
      A *a = new B();
      a->f();
      delete a;
      return 0;
    }
    

    这将打印“B”。现在让我们看看使用 Stack 会发生什么:

    int main(void) {
      A a = B();
      a.f();
      return 0;
    }
    

    这将打印“A”,这对于熟悉 Java 或其他面向对象语言的人来说可能不直观。原因是您不再拥有指向B 实例的指针。而是创建B 的实例并将其复制到a 类型为A 的变量中。

    有些事情可能会不经意地发生,尤其是当您是 C++ 新手时。在 C 中,你有你的指针,就是这样。你知道如何使用它们,而且它们总是一样的。在 C++ 中,情况并非如此。想象一下,当你在这个例子中使用 a 作为方法的参数时会发生什么 - 事情变得更加复杂,如果 a 的类型为 AA* 甚至 A&amp; ,它确实会产生巨大的差异(引用调用)。许多组合是可能的,它们的行为都不同。

    【讨论】:

    • -1:人们无法理解值语义和多态性需要引用/指针(以避免对象切片)这一简单事实并不构成语言的任何问题。 c++ 的强大功能不应仅仅因为某些人无法学习其基本规则而被视为缺点。
    • 感谢您的提醒。我在使用对象而不是指针或引用的方法方面遇到了类似的问题,这真是一团糟。该对象内部有指针,由于这种应对方式,它们很快就被删除了。
    • @underscore_d 我同意;应删除此答案的最后一段。不要认为我编辑了这个答案就意味着我同意它;我只是不想阅读其中的错误。
    • @TamaMcGlinn 同意了。感谢您的良好编辑。我删除了意见部分。
    【解决方案3】:

    嗯,使用指针的原因与在 C 中使用 malloc 分配的指针的原因完全相同:如果您希望对象的寿命比变量长!

    如果可以避免的话,甚至强烈建议不要使用 new 运算符。特别是如果您使用异常。一般来说,让编译器释放你的对象会更安全。

    【讨论】:

      【解决方案4】:

      我从不太了解 & 地址操作符的人那里看到了这种反模式。如果他们需要使用指针调用函数,他们将始终在堆上分配,以便获得指针。

      void FeedTheDog(Dog* hungryDog);
      
      Dog* badDog = new Dog;
      FeedTheDog(badDog);
      delete badDog;
      
      Dog goodDog;
      FeedTheDog(&goodDog);
      

      【讨论】:

      • 或者:void FeedTheDog(Dog&hungryDog);狗狗; FeedTheDog(狗);
      • @ScottLangham 是的,但这不是我想要说明的重点。例如,有时您调用的函数采用可选的 NULL 指针,或者它可能是无法更改的现有 API。
      【解决方案5】:

      将堆视为非常重要的不动产并非常明智地使用它。基本的经验法则是尽可能使用堆栈,并在没有其他方法时使用堆。通过在堆栈上分配对象,您可以获得许多好处,例如:

      (1)。如果出现异常,您不必担心堆栈展开

      (2)。您不必担心由于堆管理器分配的空间过多而导致的内存碎片。

      【讨论】:

      • 应该考虑对象的大小。 Stack 的大小是有限的,因此应该在 Heap 分配非常大的对象。
      • @Adam 我认为 Naveen 在这里仍然是正确的。如果你可以把一个大对象放在堆栈上,那就去做吧,因为这样更好。你可能做不到的原因有很多。但我认为他是对的。
      【解决方案6】:

      我担心的唯一原因是 Dog 现在分配在堆栈上,而不是堆上。所以如果 Dog 的大小是兆字节,你可能会遇到问题,

      如果您确实需要走新/删除路线,请注意例外情况。因此,您应该使用 auto_ptr 或 boost 智能指针类型之一来管理对象的生命周期。

      【讨论】:

        【解决方案7】:

        当您可以在堆栈上分配时,没有理由(在堆上)new(除非出于某种原因您有一个小堆栈并且想要使用堆。

        如果您确实想在堆上分配,您可能需要考虑使用标准库中的 shared_ptr(或其变体之一)。一旦对 shared_ptr 的所有引用都不存在,它将为您处理删除操作。

        【讨论】:

        • 有很多原因,举个例子,看我的回答。
        • 我想您可能希望对象的寿命超过函数的范围,但 OP 似乎并没有暗示这是他们想要做的。
        【解决方案8】:

        还有一个其他人没有提到的其他原因,您可能会选择动态创建对象。基于堆的动态对象允许您使用polymorphism

        【讨论】:

        • 您也可以多态地使用基于堆栈的对象,在这方面我看不出堆栈/堆分配对象之间有任何区别。例如: void MyFunc() { Dog dog;喂狗); } void Feed(Animal& animal) { auto food = animal->GetFavouriteFood(); PutFoodInBowl(食物); } // 在这个例子中,GetFavouriteFood 可以是一个虚函数,每个动物都可以用它自己的实现来覆盖它。这将在不涉及堆的情况下以多态方式工作。
        • -1:多态只需要一个引用或指针;它与底层实例的存储持续时间完全无关。
        • @underscore_d "使用通用基类意味着成本:对象必须在堆上分配为多态;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
        • 大声笑,我不在乎 Stroustrup 本人是否说过,但这对他来说是一个令人难以置信的尴尬报价,因为他错了。这个自己测试不难,你知道的:在栈上实例化一些 DerivedA、DerivedB 和 DerivedC;还实例化一个堆栈分配的指向 Base 的指针,并确认您可以将其与 A/B/C 一起放置并以多态方式使用它们。对声明应用批判性思维和标准参考,即使它们是由该语言的原作者提出的。这里:stackoverflow.com/q/5894775/2757035
        • 这样说,我有一个对象包含 2 个独立的家庭,每个家庭有近 1000 个多态对象,具有自动存储持续时间。我在堆栈上实例化这个对象,并通过引用或指针引用这些成员,它获得了对它们的全部多态能力。我的程序中没有任何东西是动态/堆分配的(除了堆栈分配的类拥有的资源),它不会通过 iota 改变我的对象的功能。
        【解决方案9】:

        我在 Visual Studio 中遇到了同样的问题。你必须使用:

        yourClass->classMethod();

        而不是:

        yourClass.classMethod();

        【讨论】:

        • 这没有回答问题。您宁愿注意必须使用不同的语法来访问在堆上分配的对象(通过指向对象的指针)和在堆栈上分配的对象。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-26
        • 2012-08-04
        • 2015-04-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多