在构造函数中做什么(不)
我想问你关于C ++构造函数的最佳实践。 我不太清楚我应该在构造函数中做什么,什么不是。
我应该只使用它的属性初始化,调用父构造函数等? 或者,我甚至可以把更复杂的function,如读取和parsingconfiguration数据,设置外部库aso
还是应该为此编写特殊function? RESP。 init()
/ cleanup()
?
什么是PRO和CON在这里?
我想通过使用init()
和cleanup()
,例如我可以摆脱共享指针。 我可以创build堆栈中的对象作为类属性,并在构造完成后对其进行初始化。
如果我在构造函数中处理它,我需要在运行时实例化它。 然后我需要一个指针。
我真的不知道如何决定。
也许你可以帮我吗?
复杂的逻辑和构造函数并不总是混合在一起,有强烈的支持者在构造函数中做重要的工作(有理由)。
基本的规则是构造函数应该产生一个完全可用的对象。
class Vector { public: Vector(): mSize(10), mData(new int[mSize]) {} private: size_t mSize; int mData[]; };
这并不意味着一个完全初始化的对象,只要用户不必考虑它,就可以推迟一些初始化(认为懒惰)。
class Vector { public: Vector(): mSize(0), mData(0) {} // first call to access element should grab memory private: size_t mSize; int mData[]; };
如果要完成繁重的工作,你可以selectinheritance一个构build器方法,在调用构造器之前完成繁重的工作。 例如,设想从数据库检索设置并构build设置对象。
// 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; }
如果推迟对象的构造会产生显着的好处,此构build器方法非常有用。 例如,如果对象占用大量内存,那么在可能发生故障的任务之后推迟内存获取可能不是一个坏主意。
这种构build方法隐含了私有构造函数和公共(或朋友)构build器。 请注意,拥有私有构造函数会对类的用法施加许多限制(例如,不能存储在STL容器中),因此您可能需要以其他模式进行合并。 这就是为什么这种方法只能用于特殊情况。
你可能也想考虑如何testing这样的实体,如果你依赖于外部的东西(文件/数据库),想想dependency injection,它确实有助于unit testing。
构造函数和析构函数中最常见的错误是使用多态。 多态性往往不能在构造函数中工作 !
例如:
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(); }
这是因为对象B在执行母类A的构造函数时还没有构造…因此不可能调用void doA();
的重载版本void doA();
本,在下面的评论中,要求我举一个例子,其中多态性将在构造函数中工作。
例如:
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(); }
这次背后的原因是:在调用virtual
函数doOverridenBehaviour()
的时候,对象b已经被初始化了(但尚未构造),这意味着它的虚拟表被初始化了,从而可以执行多态。
- 不要在构造函数中调用
delete this
或者析构函数。 - 不要使用init()/ cleanup()成员。 如果每次创build实例时必须调用init(),init()中的所有内容都应该在构造函数中。 构造函数是为了将实例放入一个一致的状态,允许任何公共成员以一个明确定义的行为被调用。 类似的清理(),加上清理()杀死RAII 。 (但是,当你有多个构造函数时,通常需要一个由它们调用的私有init()函数)。
- 在构造函数中做更复杂的事情是可以的,取决于类的预期用途和整体devise。 例如,在某种Integer或Point类的构造函数中读取文件不是一个好主意; 用户期望这些创build便宜。 考虑文件访问构造函数如何影响你编写unit testing的能力也很重要。 最好的解决scheme通常是有一个构造函数,它只需要构造成员所需的数据,然后编写一个非成员函数来执行文件parsing并返回一个实例。
简单的答案:这取决于。
在devise软件时,您可能需要通过RAII原理进行编程(“资源获取正在初始化”)。 这意味着(除其他外)对象本身负责其资源,而不是调用者。 另外,您可能需要熟悉exception安全 (不同程度)。
例如,考虑:
void func(){ MyFile f(“myfile.dat”); doSomething的(F); }
如果你以这种方式devise了类MyFile
,那么在doSomething(f)
f
被初始化之前你可以确定,你可以节省很多麻烦。 另外,如果你在析构f
中释放f
所持有的资源,即closures文件句柄,那么你就安全起来了,而且易于使用。
在这个特定的情况下,你可以使用构造函数的特殊属性:
- 如果从构造函数向外部引发exception, 则不会创build该对象。 这意味着,析构函数不会被调用,内存将被立即释放。
- 必须调用构造函数。 你不能强迫用户使用任何其他函数(析构函数除外),只能按照惯例。 所以,如果你想强制用户初始化你的对象,为什么不通过构造函数?
- 如果你有任何
virtual
方法,除非你知道你正在做什么,否则你不应该从构造函数中调用这些方法 – 你(或更高版本的用户)可能会惊讶为什么不调用虚拟重写方法。 最好不要混淆任何人。
构造函数必须使对象处于可用状态。 而且,因为让你的API难以使用是非常明智的,所以最好的办法就是使其正确使用 (对Scott Meyers来说)。 在构造函数中做初始化应该是你的默认策略 – 当然总是有例外。
所以:这是使用构造函数进行初始化的好方法。 这并不总是可能的,例如GUI框架经常需要被构build,然后被初始化。 但是如果你完全按照这个方向devise你的软件,你可以节省很多麻烦。
来自C ++编程语言 :
使用诸如
init()
为类对象提供初始化的函数是不雅观和错误的。 因为没有说明一个对象必须被初始化, 所以程序员可以忘记这样做 – 或者两次(通常同样是灾难性的结果) 。 一个更好的方法是允许程序员用明确的目的来初始化对象来声明一个函数。 因为这样的函数构造给定types的值,所以称为构造函数。
在devise一个类时,我通常会考虑以下规则:在构造函数执行后,我必须能够安全地 使用类的任何方法 。 这里的安全意味着如果对象的init()
方法没有被调用,你总是可以抛出exception,但我更喜欢有一些实际可用的东西。
例如,当你使用默认构造函数时, class std::string
可能不会分配任何内存,因为如果两个方法都返回空指针,那么大多数方法(即begin()
和end()
)都能正常工作,而c_str()
不一定返回由于其他devise原因,目前的缓冲区,因此它必须随时准备分配内存。 在这种情况下不分配内存仍然导致一个完全可用的string实例。
相反,使用RAII作为范围内的互斥锁守卫是一个可以执行任意长时间(直到锁的所有者释放它)的构造函数的例子,但仍然被普遍接受为良好实践。
在任何情况下,延迟初始化可能比使用init()
方法更安全。 一种方法是使用一些捕获构造函数的所有参数的中间类。 另一个是使用build设者模式。
预计构造函数将创build一个可以从单词go中使用的对象。 如果由于某种原因无法创build一个可用的对象,它应该抛出一个exception并且完成它。 因此,一个对象正常工作所需的所有补充方法/函数都应该从构造函数中调用(除非你想要像特性一样延迟加载)
我宁愿问:
What all to do in the constructor?
上面没有提到的任何东西都是OP的问题的答案。
我认为构造函数的唯一目的是
-
初始化所有的成员variables到一个已知的状态
-
分配资源(如果适用)。
项目#1听起来很简单,但是我发现这个项目被定期遗忘/忽略,只能通过静态分析工具来提醒。 永远不要低估这个(双关意图)。
你可以从一个构造函数中抛出,而且通常比创build一个僵尸对象(即一个具有“失败”状态的对象)更好。
但是,你应该永远不要从析构函数中抛出。
编译器将知道成员对象的构造顺序 – 它们在标题中出现的顺序。 然而,析构函数不会像你所说的那样被调用,这意味着如果你在一个构造函数中多次调用新的函数,你不能依靠你的析构函数为你调用删除函数。 如果将它们放入智能指针对象中,这些对象将被删除,这不成问题。 如果你想让它们作为原始指针,那么把它们暂时放到auto_ptr对象中,直到你知道你的构造函数不再抛出,然后在所有的auto_ptrs上调用release()。
一个构造函数用于构造一个对象 – 没有什么更多,也没有less。 您需要在构造函数中build立类不variables,以及它是多么的复杂,这取决于被初始化的对象的types。
单独的init()函数是一个好主意,只是由于某些原因你不能使用exception。
那么«build设者»来自build设,build设,build立。 所以这就是所有初始化发生的地方。 无论何时你实例化一个类,使用构造函数来确保一切都完成了,以便使新对象可用。
你可以做你想做的事情,但是为了这个目的使用构造函数来创build对象。 如果需要调用其他方法,那没关系。 只要遵循一条规则 – 不要把它变得比需要更复杂。 好的做法是尽可能简单地构造函数,但这并不意味着只需要初始化成员即可。
我认为最重要的是有点常识! 有很多关于做和不做的说法 – 一切都很好,但要考虑的关键是如何使用您的对象。 例如,
- 这个对象将创build多less个实例? (他们举行容器例如?)
- 他们多久创build和销毁? (循环?)
- 它有多大?
如果这个对象是一个单独的实例,那么这个实例就是在开始时构build的,并且构造不在关键path中 – 为什么不在构造函数中执行繁重的工作(只要适当地使用exception,它甚至可能更有意义)? 另一方面,如果它是一个非常轻量级的对象,在短时间内循环创build和销毁 – 尽量less做(例如,除了初始化成员)(函子是一个很好的例子这个的…)
有两个阶段的负载(或其他)有好处,但主要的缺点是忘记调用它 – 我们有多less人做了这个? 🙂
所以,我的婚姻是,不要坚持一条硬性规定,仔细看待你的目标是如何使用,然后devise它来适应!