使用指针作为类成员是否是一种好的做法?

3
我对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 对象不是需要在内存中创建两次吗?(一次调用构造函数,一次由构造函数本身)
感谢任何帮助。

3
首先,我没有定义默认构造函数:在这种情况下,请使用成员初始化列表。第二:使用指针引用事物,并且对于所有权使用std::unique_ptr。第三:避免不必要的动态分配,尝试使用值类型进行编码,并仅在真正需要时(例如在容器中)进行分配。 - BeyelerStudios
1
问问自己,如果指针所指向的对象是否会被销毁,以及何时、由谁销毁。如果答案明确,则对象的生命周期受控制,一切都能正常工作。 - user3528438
如果在第一个示例中您没有为Foo定义默认构造函数,您仍然可以使用成员初始化列表正确地初始化它。 - πάντα ῥεῖ
1
你的第二个选项真是糟糕透了。你正在获取一个作为const引用传递给你的东西的地址。它可能是一个临时对象! - SergeyA
1
@bullsy:这个问题显然是重复的,但那里的答案都早于C++11...而且任何一个没有至少提到std::unique_ptr的回答都是错误的。不确定在这种情况下正确的SO协议是什么。 - Nemo
显示剩余6条评论
3个回答

6

可以将指针作为成员使用,但在您的情况下,您只是在简单地解决小问题,这并不需要使用指针。而且使用指针可能会存在危险,我马上会指出一个问题。

目前的代码无法工作,我没有为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_ptr和std::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_ptr 会确保它们所拥有的对象被销毁,我们不必记得手动调用 delete

在上面的例子中,把 m_f2 设为成员变量而非指针可能更好。


使用原始指针来管理所有权是极其不好的做法,这是任何有经验的C++程序员所知道的。-1。 - Nemo
@Nemo,又犯了同样的错误。没有表明拥有指针的迹象。你的负评是不合理的。 - SergeyA
@SergeyA:阅读最后几段,从“...你将负责管理...”开始。使用原始指针进行所有权控制,这导致我给出了反对票。(实际上,我会认为任何在此处没有仔细区分拥有和非拥有指针的答案都是不好的。正如我们自己的争论所表明的那样。) - Nemo

4
如果对象不太昂贵,可以传递对象作为成员。如果出于某些原因需要使用指针,则需要制定所有权策略。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), ... {}
};

1
即使对象传递起来很昂贵,它们不一定需要共享,您仍然可以“移动”它们。 - user3528438
@SergeyA,我很期待你对这个问题的回答。 - R Sahu
@SergeyA:这不是“货物崇拜”,而是扎实的C++设计。使用原始指针进行所有权控制会使编写异常安全代码变得几乎不可能。std::unique_ptr在空间和时间上都没有额外开销,并且可以产生更简单、更安全的代码。任何像样的C++程序员都不会使用原始指针来控制所有权。 - Nemo
@Nemo,原始答案没有提到所有权。它做出了一个通用的错误陈述(“如果你有某些原因需要使用指针,请优先使用智能指针。”)。拥有指针应该是智能指针,这点没有人会争论。然而,在许多情况下必须使用非拥有指针。 - SergeyA
@SergeyA,我明白你的想法。 - R Sahu
显示剩余6条评论

0

我认为对象和原始变量是相同的,这是无稽之谈。

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
}

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