📄 ec3.htm
字号:
Effective C++, 2E | Chapter 3: Constructors, Destructors, and Assignment Operators Back to Item 10: Write operator delete if you write operator new.Continue to Item 11: Declare a copy constructor and an assignment operator for classes with dynamically allocated memory.Constructors, Destructors, and Assignment OperatorsAlmost every class you write will have one or more constructors, a destructor, and an assignment operator. Little wonder. These are your bread-and-butter functions, the ones that control the fundamental operations of bringing a new object into existence and making sure it's initialized; getting rid of an object and making sure it's been properly cleaned up; and giving an object a new value. Making mistakes in these functions will lead to far-reaching and distinctly unpleasant repercussions throughout your classes, so it's vital that you get them right. In this section, I offer guidance on putting together the functions that comprise the backbone of well-formed classes. Back to Constructors, Destructors, and Assignment OperatorsContinue to Item 12: Prefer initialization to assignment in constructors.Item 11: Declare a copy constructor and an assignment operator for classes with dynamically allocated memory.Consider a class for representing String objects: // a poorly designed String classclass String {public: String(const char *value); ~String(); ... // no copy ctor or operator=private: char *data;};String::String(const char *value){ if (value) { data = new char[strlen(value) + 1]; strcpy(data, value); } else { data = new char[1]; *data = '\0'; }}inline String::~String() { delete [] data; }Note that there is no assignment operator or copy constructor declared in this class. As you'll see, this has some unfortunate consequences.If you make these object definitions, String a("Hello");String b("World");the situation is as shown below:Inside object a is a pointer to memory containing the character string "Hello". Separate from that is an object b containing a pointer to the character string "World". If you now perform an assignment, b = a;there is no client-defined operator= to call, so C++ generates and calls the default assignment operator instead (see Item 45). This default assignment operator performs memberwise assignment from the members of a to the members of b, which for pointers (a.data and b.data) is just a bitwise copy. The result of this assignment is shown below.There are at least two problems with this state of affairs. First, the memory that b used to point to was never deleted; it is lost forever. This is a classic example of how a memory leak can arise. Second, both a and b now contain pointers to the same character string. When one of them goes out of scope, its destructor will delete the memory still pointed to by the other. For example: String a("Hello"); // define and construct a{ // open new scope String b("World"); // define and construct b ... b = a; // execute default op=, // lose b's memory} // close scope, call b's // destructorString c = a; // c.data is undefined! // a.data isalready deletedThe last statement in this example is a call to the copy constructor, which also isn't defined in the class, hence will be generated by C++ in the same manner as the assignment operator (again, see Item 45) and with the same behavior: bitwise copy of the underlying pointers. That leads to the same kind of problem, but without the worry of a memory leak, because the object being initialized can't yet point to any allocated memory. In the case of the code above, for example, there is no memory leak when c.data is initialized with the value of a.data, because c.data doesn't yet point anywhere. However, after c is initialized with a, both c.data and a.data point to the same place, so that place will be deleted twice: once when c is destroyed, once again when a is destroyed.The case of the copy constructor differs a little from that of the assignment operator, however, because of the way it can bite you: pass-by-value. Of course, Item 22 demonstrates that you should only rarely pass objects by value, but consider this anyway: void doNothing(String localString) {}String s = "The Truth Is Out There";doNothing(s);Everything looks innocuous enough, but because localString is passed by value, it must be initialized from s via the (default) copy constructor. Hence, localString has a copy of the pointer that is inside s. When doNothing finishes executing, localString goes out of scope, and its destructor is called. The end result is by now familiar: s contains a pointer to memory that localString has already deleted.By the way, the result of using delete on a pointer that has already been deleted is undefined, so even if s is never used again, there could well be a problem when it goes out of scope.The solution to these kinds of pointer aliasing problems is to write your own versions of the copy constructor and the assignment operator if you have any pointers in your class. Inside those functions, you can either copy the pointed-to data structures so that every object has its own copy, or you can implement some kind of reference-counting scheme (see Item M29) to keep track of how many objects are currently pointing to a particular data structure. The reference-counting approach is more complicated, and it calls for extra work inside the constructors and destructors, too, but in some (though by no means all) applications, it can result in significant memory savings and substantial increases in speed.For some classes, it's more trouble than it's worth to implement copy constructors and assignment operators, especially when you have reason to believe that your clients won't make copies or perform assignments. The examples above demonstrate that omitting the corresponding member functions reflects poor design, but what do you do if writing them isn't practical, either? Simple: you follow this Item's advice. You declare the functions (private, as it turns out), but you don't define (i.e., implement) them at all. That prevents clients from calling them, and it prevents compilers from generating them, too. For details on this nifty trick, see Item 27.One more thing about the String class I used in this Item. In the constructor body, I was careful to use [] with new both times I called it, even though in one of the places I wanted only a single object. As described in Item 5, it's essential to employ the same form in corresponding applications of new and delete, so I was careful to be consistent in my uses of new. This is something you do not want to forget. Always make sure that you use [] with delete if and only if you used [] with the corresponding use of new. Back to Item 11: Declare a copy constructor and an assignment operator for classes with dynamically allocated memory.Continue to Item 13: List members in an initialization list in the order in which they are declared.Item 12: Prefer initialization to assignment in constructors.Consider a template for generating classes that allow a name to be associated with a pointer to an object of some type T: template<class T>class NamedPtr {public: NamedPtr(const string& initName, T *initPtr); ...private: string name; T *ptr;};(In light of the aliasing that can arise during the assignment and copy construction of objects with pointer members (see Item 11), you might wish to consider whether NamedPtr should implement these functions. Hint: it should (see Item 27).)When you write the NamedPtr constructor, you have to transfer the values of the parameters to the corresponding data members. There are two ways to do this. The first is to use the member initialization list: template<class T>NamedPtr<T>::NamedPtr(const string& initName, T *initPtr ): name(initName), ptr(initPtr){}The second is to make assignments in the constructor body: template<class T>NamedPtr<T>::NamedPtr(const string& initName, T *initPtr){ name = initName; ptr = initPtr;}There are important differences between these two approaches.From a purely pragmatic point of view, there are times when the initialization list must be used. In particular, const and reference members may only be initialized, never assigned. So, if you decided that a NamedPtr<T> object could never change its name or its pointer, you might follow the advice of Item 21 and declare the members const: template<class T>class NamedPtr {public: NamedPtr(const string& initName, T *initPtr); ...private: const string name; T * const ptr;};This class definition requires that you use a member initialization list, because const members may only be initialized, never assigned. You'd obtain very different behavior if you decided that a NamedPtr<T> object should contain a reference to an existing name. Even so, you'd still have to initialize the reference on your constructors' member initialization lists. Of course, you could also combine the two, yielding NamedPtr<T> objects with read-only access to names that might be modified outside the class: template<class T>class NamedPtr {public: NamedPtr(const string& initName, T *initPtr); ...private: const string& name; // must be initialized via // initializer list T * const ptr; // must be initialized via // initializer list};The original class template, however, contains no const or reference members. Even so, using a member initialization list is still preferable to performing assignments inside the constructor. This time the reason is efficiency. When a member initialization list is used, only a single string member function is called. When assignment inside the constructor is used, two are called. To understand why, consider what happens when you declare a NamedPtr<T> object.Construction of objects proceeds in two phases: Initialization of data members. (See also Item 13.) Execution of the body of the constructor that was called.(For objects with base classes, base class member initialization and constructor body execution occurs prior to that for derived classes.)For the NamedPtr classes, this means that a constructor for the string object name will always be called before you ever get inside the body of a NamedPtr constructor. The only question, then, is this: which string constructor will be called?That depends on the member initialization list in the NamedPtr classes. If you fail to specify an initialization argument for name, the default string constructor will be called. When you later perform an assignment to name inside the NamedPtr constructors, you will call operator= on name. That will total two calls to string member functions: one for the default constructor and one more for the assignment.On the other hand, if you use a member initialization list to specify that name should be initialized with initName, name will be initialized through the copy constructor at a cost of only a single function call.Even in the case of the lowly string type, the cost of an unnecessary function call may be significant, and as classes become larger and more complex, so do their constructors, and so does the cost of constructing objects. If you establish the habit of using a member initialization list whenever you can, not only do you satisfy a requirement for const and reference members, you also minimize the chances of initializing data members in an inefficient manner.In other words, initialization via a member initialization list is always legal, is never less efficient than assignment inside the body of the constructor, and is often more efficient. Furthermore, it simplifies maintenance of the class (see Item M32), because if a data member's type is later modified to something that requires use of a member initialization list, nothing has to change.There is one time, however, when it may make sense to use assignment instead of initialization for the data members in a class. That is when you have a large number of data members of built-in types, and you want them all initialized the same way in each constructor. For example, here's a class that might qualify for this kind of treatment: class ManyDataMbrs {public: // default constructor ManyDataMbrs(); // copy constructor ManyDataMbrs(const ManyDataMbrs& x);private: int a, b, c, d, e, f, g, h; double i, j, k, l, m;};Suppose you want to initialize all the ints to 1 and all the doubles to 0, even if the copy constructor is used. Using member initialization lists, you'd have to write this: ManyDataMbrs::ManyDataMbrs(): a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0), j(0), k(0), l(0), m(0){ ... }ManyDataMbrs::ManyDataMbrs(const ManyDataMbrs& x): a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0), j(0), k(0), l(0), m(0){ ... }This is more than just unpleasant drudge work. It is error-prone in the short term and difficult to maintain in the long term.However, you can take advantage of the fact that there is no operational difference between initialization and assignment for (non-const, non-reference) objects of built-in types, so you can safely replace the memberwise initialization lists with a function call to a common initialization routine: class ManyDataMbrs {public: // default constructor ManyDataMbrs(); // copy constructor ManyDataMbrs(const ManyDataMbrs& x);private: int a, b, c, d, e, f, g, h; double i, j, k, l, m; void init(); // used to initialize data // members};void ManyDataMbrs::init(){ a = b = c = d = e = f = g = h = 1; i = j = k = l = m = 0;}ManyDataMbrs::ManyDataMbrs()
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -