在构造函数中做什么(不)

tyr*_*dis 41 c++ oop constructor shared-ptr

我想问你关于C++中构造函数的最佳实践.我不太确定我应该在构造函数中做什么,什么不能.

我应该只将它用于属性初始化,调用父构造函数等吗?或者我甚至可以将更复杂的函数放入其中,例如读取和解析配置数据,设置外部库aso

或者我应该为此编写特殊功能?RESP.init()/ cleanup()

什么是PRO和CON?

我想出了,例如,我可以在使用init()和时删除共享指针cleanup().我可以在堆栈上创建对象作为类属性,并在以后构建它时对其进行初始化.

如果我在构造函数中处理它,我需要在运行时实例化它.然后我需要一个指针.

我真的不知道该如何决定.

也许你可以帮帮我吗?

Ste*_*and 28

最常见的错误在构造函数中,以及在析构函数做的,就是用多态.多态性通常在构造函数中不起作用!

例如:

class A
{
public:
    A(){ doA();} 
    virtual void doA(){};
}

class B : public A
{
public:
    virtual void doA(){ doB();};
    void doB(){};   
}


void testB()
{
    B b; // this WON'T call doB();
}
Run Code Online (Sandbox Code Playgroud)

这是因为在执行母类A的构造函数时尚未构造对象B ...因此不可能调用覆盖版本的 void doA();

在下面的评论中,Ben向我询问了一个多态性在构造函数中起作用的例子.

例如:

class A
{
public: 
    void callAPolymorphicBehaviour()
    {
        doOverridenBehaviour(); 
    }

    virtual void doOverridenBehaviour()
    {
        doA();
    }

    void doA(){}
};

class B : public A
{
public:
    B()
    {
        callAPolymorphicBehaviour();
    }

    virtual void doOverridenBehaviour()
    {
        doB()
    }

    void doB(){}
};

void testB()
{
   B b; // this WILL call doB();
}
Run Code Online (Sandbox Code Playgroud)

这一次,背后的原因是:在调用virtual函数时doOverridenBehaviour(),对象b已经初始化(但尚未构造),这意味着它的虚拟表被初始化,因此可以执行多态.


Mat*_* M. 23

复杂的逻辑和构造函数并不总是很好地混合,并且有强大的支持者反对在构造函数中做繁重的工作(有原因).

基本规则是构造函数应该生成一个完全可用的对象.

class Vector
{
public:
  Vector(): mSize(10), mData(new int[mSize]) {}
private:
  size_t mSize;
  int mData[];
};
Run Code Online (Sandbox Code Playgroud)

它并不意味着一个完全初始化的对象,只要用户不必考虑它就可以推迟一些初始化(想想懒惰).

class Vector
{
public:
  Vector(): mSize(0), mData(0) {}

  // first call to access element should grab memory

private:
  size_t mSize;
  int mData[];
};
Run Code Online (Sandbox Code Playgroud)

如果要完成繁重的工作,您可以选择继续使用构建器方法,这将在调用构造函数之前完成繁重的工作.例如,假设从数据库中检索设置并构建设置对象.

// in the constructor
Setting::Setting()
{
  // connect
  // retrieve settings
  // close connection (wait, you used RAII right ?)
  // initialize object
}

// Builder method
Setting Setting::Build()
{
  // connect
  // retrieve settings

  Setting setting;
  // initialize object
  return setting;
}
Run Code Online (Sandbox Code Playgroud)

如果推迟构造对象产生显着的好处,则此构建器方法很有用.例如,如果对象占用大量内存,则在可能失败的任务之后推迟内存获取可能不是一个坏主意.

此构建器方法隐含私有构造函数和公共(或朋友)构建器.请注意,使用Private构造函数会对可以对类执行的用法施加一些限制(例如,不能存储在STL容器中),因此您可能需要合并其他模式.这就是为什么这种方法只应在特殊情况下使用的原因.

您可能也希望考虑如何测试这些实体,如果您依赖于外部事物(文件/数据库),请考虑依赖注入,它确实有助于单元测试.

  • 在惰性基础上调用init()方法的问题是你必须确保在使用前调用它们. (2认同)
  • +"基本规则是构造函数应该产生一个完全可用的对象." (2认同)

Ste*_*e M 15

  • 不要delete this在构造函数中调用析构函数.
  • 不要使用init()/ cleanup()成员.如果每次创建实例时都必须调用init(),init()中的所有内容都应该在构造函数中.构造函数旨在将实例置于一致状态,允许以明确定义的行为调用任何公共成员.类似地,对于cleanup(),加上cleanup()会杀死RAII.(但是,当你有多个构造函数时,拥有一个由它们调用的私有init()函数通常很有用.)
  • 在构造函数中执行更复杂的操作是可以的,具体取决于类的预期用途和整体设计.例如,在某种Integer或Point类的构造函数中读取文件不是一个好主意; 用户希望这些产品便宜.考虑文件访问构造函数如何影响您编写单元测试的能力也很重要.最好的解决方案通常是让构造函数只获取构造成员所需的数据,并编写一个非成员函数来执行文件解析并返回实例.

  • -1:不,做更复杂的事情**不是**好吧.在构造函数或析构函数中使用多态通常会导致**失败**.请参阅下面的示例. (3认同)
  • "不要在构造函数中调用`delete this`" - 你没有听说资源获取是无效吗? (3认同)

tow*_*owi 10

简单回答:这取决于.

在设计软件时,您可能希望通过RAII原则进行编程("资源获取是初始化").这意味着(除其他外)对象本身负责其资源,而不是调用者.此外,您可能希望熟悉异常安全性(在不同程度上).

例如,考虑:

void func() {
    MyFile f("myfile.dat");
    doSomething(f);
}

如果你设计类MyFile的方式,之前你可以肯定doSomething(f)的是f被初始化,为您节省了不少麻烦检查这一点.此外,如果您释放f析构函数中保留的资源,即关闭文件句柄,则您可以放心使用并且易于使用.

在这种特定情况下,您可以使用构造函数的特殊属性:

  • 如果从构造函数向其外部世界抛出异常,则不会创建该对象.这意味着,不会调用析构函数,并且会立即释放内存.
  • 必须调用构造函数.您不能强制用户使用任何其他功能(析构函数除外),只能按惯例.那么,如果你想强迫用户初始化你的对象,为什么不通过构造函数呢?
  • 如果你有任何virtual方法,你不应该从构造函数中调用那些方法,除非你知道你在做什么 - 你(或后来的用户)可能会惊讶为什么不调用虚拟覆盖方法.最好不要混淆任何人.

构造函数必须使对象处于可用状态.而且因为它是跳投明智的,难以用你的API错误,做的最好的事情是可以很容易地使用正确的(原文如此斯科特迈尔斯).在构造函数中进行初始化应该是您的默认策略 - 当然,总会有例外.

所以:这是使用构造函数进行初始化的好方法.并非总是可能,例如,通常需要构建GUI框架,然后进行初始化.但如果你完全按照这个方向设计你的软件,你可以省去很多麻烦.


And*_*ron 6

来自C++编程语言:

诸如init()为类对象提供初始化的函数的使用是不优雅的并且是错误的.因为没有声明对象必须被初始化,程序员可能会忘记这样做 - 或者两次这样做(通常会带来同样灾难性的结果).更好的方法是允许程序员声明一个具有初始化对象的明确目的的函数.因为这样的函数构造给定类型的值,所以它被称为构造函数.

在设计类时,我通常会考虑以下规则:在构造函数执行后,我必须能够安全地使用该类的任何方法.这里安全意味着如果没有调用对象的方法,你总是可以抛出异常,但我更喜欢有一些实际可用的东西.init()

例如,class std::string当您使用默认构造函数时,可能不会分配任何内存,因为如果两个方法都返回空指针,并且由于其他设计原因不一定返回当前缓冲区,则大多数方法(即begin()end())都能正常工作c_str(),因此必须准备好随时分配内存.在这种情况下,不分配内存仍然会导致完全可用的字符串实例.

相反,在用于互斥锁的范围保护中使用RAII是可以执行任意长时间(直到锁的所有者释放它)的构造函数的示例,但仍然被普遍接受为良好实践.

在任何情况下,延迟初始化可以比使用init()方法更安全的方式完成.一种方法是使用一些中间类来捕获构造函数的所有参数.另一种是使用构建器模式.