【问题标题】:Is it good practice to use pointers as class members?使用指针作为类成员是一种好习惯吗?
【发布时间】:2016-06-01 08:10:06
【问题描述】:

我是 C++ 的新手,我正在尝试了解构建类的良好做法。

假设我有一堂课Foo

class Foo {
  public:
    double foo;
    Foo(double foo);
    Foo add(Foo f);
}

我想创建一个类 Bar,它由两个 Foo 对象组成,并在构造时创建第三个。

第一种选择:对象作为类成员

class Bar {
  public:
    Foo foo1;
    Foo foo2;
    Foo foo3;
    Bar(const Foo &f1, const Foo &f2);  
}

Bar::Bar(const Foo &f1, const Foo &f2):
{
  foo1 = f1;
  foo2 = f2;
  foo3 = f1.add(f2);
}

因为我没有为Foo 定义默认构造函数,所以它不起作用。

第二个选项:指针作为类成员

class Bar {
  public:
    const Foo* foo1;
    const Foo* foo2;
    const Foo* foo3;
    Bar(const Foo &f1, const Foo &f2);  
}

Bar::Bar(const Foo &f1, const Foo &f2):
{
  foo1 = &f1;
  foo2 = &f2;
  foo3 = &(f1.add(f2));
}

注意:我必须将foo1foo2 声明为const 才能使构造函数工作。 但它仍然失败,因为对于foo3,我正在获取临时结果的地址,这是非法的。


哪个选项更自然(以及如何修复错误)?我觉得第一个选项可能更好,但是我的 Foo 对象必须在内存中创建两次,不是吗? (一次调用构造函数,第二次由构造函数自己调用)

任何帮助表示赞赏。

【问题讨论】:

  • 首先我没有定义默认构造函数:在这种情况下使用member initializer list。第二:使用指针做引用的事情,std::unique_ptr 用于所有权。第三:避免不必要的动态分配,尝试使用值类型编码并仅在真正需要时分配(即用于容器)。
  • 问问自己,指针所指向的对象是否曾经被破坏,以及何时、由谁破坏。如果答案很明确,那么对象的生命周期就在可控范围内,那么一切正常。
  • 如果您在第一个示例中没有Foo 的默认构造函数,您仍然可以使用成员初始化器列表正确初始化它。
  • 你的第二个选项简直太糟糕了。您正在获取作为 const 参考传递给您的某物的地址。这可能是暂时的!
  • @bullsy:这个问题显然是重复的,但那里的答案早于 C++11 ......而且这个问题的任何答案至少没有提到 std::unique_ptr 是错误的。不确定在这种情况下正确的 SO 协议是什么。

标签: c++ oop pointers


【解决方案1】:

使用指针作为成员很好,但在您的情况下,您只是在解决一个实际上不保证使用指针的小问题,并且使用指针可能是危险的,正如我将指出的一个问题所证明的那样很快。

按原样,它不起作用,因为我没有为 Foo 定义默认构造函数。

这很容易通过使用 Bar 的初始化程序来解决:

Bar(const Foo &f1, const Foo &f2) : foo1(f1), foo2(f2), foo3(f1.add(f2)) {}

如下所示:

#include <iostream>

class Foo {
  public:
    double m_foo;
    Foo(double foo) : m_foo(foo) {}
    Foo add(Foo f) { f.m_foo += m_foo; return f; } // returns temporary!
};

class Bar {
  public:
    Foo m_foo1;
    Foo m_foo2;
    Foo m_foo3;
    Bar(const Foo &foo1, const Foo &foo2);  
};

Bar::Bar(const Foo &foo1, const Foo &foo2)
    : m_foo1(foo1)
    , m_foo2(foo2)
    , m_foo3(m_foo1.add(m_foo2))
{
}

int main() {
    Foo foo1(20.0);
    Foo foo2(22.0);
    Bar bar(foo1, foo2);

    std::cout << bar.m_foo3.m_foo << "\n";

    return 0;
}

现场演示:http://ideone.com/iaNzJv

在您的指针解决方案中,您引入了一个明显的指针问题:指向临时对象的指针。

foo3 = &(f1.add(f2));

f1.add 返回一个临时的 Foo,你获取它的地址,然后它就消失了。这是一个悬空指针。

您的指针实现也没有明确将指针作为其输入,因此 f1 和 f2 可能会遇到相同的问题:

Bar(Foo(20), Foo(22));  // two temporary Foos passed by reference
                        // but having their addresses taken. ouch.

如果您正在获取指针,最好在您的班级的 api 中执行此操作;您将不得不关心所指事物的生命周期,并尽量让呼叫者更容易告诉您正在这样做。

Bar(Foo* f1, Foo* f2);

但是现在,如果您要使用 F3,您将负责管理它的内存:

Bar(Foo* f1, Foo* f2)
    : foo1(f1), foo2(f3), foo3(new Foo(*f1.add(*f2)))
{}

~Bar()
{
    delete f3;
}

因此,在您的示例中,使用成员可能要好得多。

将指针用于您绝对不想复制的大对象以及不能使用移动操作的地方。

--- 编辑 ---

在现代 C++(C++11 及更高版本)中,通过“智能指针”,尤其是 std::unique_ptrstd::shared_ptr,已在很大程度上解决了传递指针所有权的问题。

尽管需要学习一些较新的 C++ 概念,但通常认为使用这些而不是原始指针是最佳实践。

#include <memory>

struct Foo {};
class Bar {
public:
    std::unique_ptr<Foo> m_f1; // we will own this
    std::unique_ptr<Foo> m_f2; // and this

    Bar(std::unique_ptr<Foo> f1) // caller must pass ownership
        : m_f1(std::move(f1))    // assume ownership
        , m_f2(std::make_unique<Foo>()) // create a new object
    {}

    ~Bar()
    {
        // nothing to do here
    }
};

int main() {
    auto f = std::make_unique<Foo>();
    Bar(std::move(f)); // the 'move' emphasizes that
                       // we're giving you ownership
    // 'f' is now invalid.

    return 0;
}

现场演示:http://ideone.com/9BtGkn

这样做的好处在于,当 Bar 超出范围时,unique_ptrs 将确保它们拥有的对象为我们销毁——我们不必记住 delete 它们。

在上面的示例中,将m_f2 设为成员而不是指针可能会好得多。

【讨论】:

  • 任何有经验的 C++ 程序员都知道,由于多种原因,使用原始指针作为所有权是非常糟糕的做法。 -1.
  • @Nemo,又犯了同样的错误。没有拥有指针的迹象。您的反对票是没有根据的。
  • @SergeyA:阅读最后几段,从“......你将负责管理......”开始。使用原始指针作为所有权,这值得我投反对票。 (实际上,我认为任何没有仔细区分拥有指针和非拥有指针的答案都是不好的。正如我们自己的论点所示。)
【解决方案2】:

如果传递的对象不是太昂贵,我建议使用对象作为成员。

如果您出于某种原因需要使用指针,则需要制定所有权政策。 Bar 对象是否拥有这些对象? Bar 是否只持有指向对象的指针而不负责释放它们使用的资源?

如果Bar 拥有Foo 对象,则更喜欢使用智能指针之一。您需要使用 new 复制这些对象并保留这些指针。

我是这样看的:

class Bar {
  public:
    std::unique_ptr<Foo> foo1;
    std::unique_ptr<Foo> foo2;
    std::unique_ptr<Foo> foo3;
    Bar(const Foo &f1, const Foo &f2) : foo1(new Foo(f1)), ... {}
};

std::unique_ptr 没有复制构造函数。因此,您必须为Bar 提供一个复制构造函数,并从该副本中适当地初始化其成员。

如果Bar 不拥有Foo 对象,您可以通过使用引用作为成员数据来获取。

class Bar {
  public:
    Foo const& foo1;
    Foo const& foo2;
    Foo const& foo3;
    Bar(const Foo &f1, const Foo &f2) : foo1(f1), ... {}
};

【讨论】:

  • 又一个货物崇拜者。 “更喜欢使用其中一个智能指针”是错误的,任何使用对象作为成员或非成员的方法通常不依赖于“传递它”的费用。 “从副本中适当地初始化其成员”作为答案是不够的。
  • 即使对象的传递成本很高,它们也不一定要共享,您仍然可以move它们。
  • @SergeyA,我很想看到你对这个问题的回答。
  • RSahu,@kfsone 已经给出了最佳答案,我没有什么要补充的。
  • @SergeyA:这不是“货物崇拜”;它是可靠的 C++ 设计。使用原始指针作为所有权使得异常安全的代码基本上不可能编写。 std::unique_ptr 在空间和时间上都实现了零开销,并产生了更简单、更安全的代码。从来没有像样的 C++ 程序员使用原始指针来获取所有权。
【解决方案3】:

我认为对象与原始变量相同是无稽之谈。

class Foo {
  public:
    double _stocks;
    Business* _business;
    Foo(double stocks, Business* business):_stocks(stocks), _business(business){}
    Foo* add(const Foo& f) {
        _stocks += f._stocks;
        _busines->merge(f._business);
        return this;
    }
    virtual ~Foo() {  delete _business;  }
}
class Bar {
  public:
    Foo* _foo1;
    Foo* _foosub;
//    Foo* _foo3;
    Bar(Foo* f1, Foo* f2); // unable const for f1 at least 
}
Bar::Bar(Foo* f1, Foo* f2):
{
    _foo1 = f1;
    _foosub = f2;
    _foo1.add(*f2);
    // _foo3 is the same as _foo1
}
void main() {
    Foo company1(100.00, BusinessFactory.create("car"));
    Foo company2(2000.00, BusinessFactory.create("food"));
    Bar conglomerate(&company1, &company2);
    // to be continued
}

【讨论】:

    猜你喜欢
    • 2011-01-28
    • 2021-07-23
    • 1970-01-01
    • 1970-01-01
    • 2011-03-06
    • 1970-01-01
    • 1970-01-01
    • 2017-06-14
    • 2011-08-30
    相关资源
    最近更新 更多