I l@ve RuBoard |
![]() ![]() |
4.2 What Are Class Constructors and the Class Destructor?Each of our numeric sequences is a good candidate for a class. A numeric sequence class object represents a range of elements within its associated sequence. By default, the beginning position is 1. For example, Fibonacci fib1( 7, 3 ); defines a Fibonacci class object of 7 elements beginning at position 3, and Pell pel( 10 ); defines a Pell class object of 10 elements beginning at the default position of 1. Finally, Fibonacci fib2( fib1 ); initializes fib2 to a copy of fib1. Each class must keep track both of its length ?how many elements of the series are represented ?and of a beginning position. A 0 or negative beginning position or length is not permitted. We store both the length and the beginning position as integers. For the moment, we define a third member, _next, which keeps track of the next element to iterate over: class Triangular { public: // ... private: int _length; // number of elements int _beg_pos; // beginning position of range int _next; // next element to iterate over }; The data members are stored within each Triangular class object. When I write Triangular tri( 8, 3 ); tri contains an instance of _length (initialized to 8), _beg_pos (initialized to 3), and _next (initialized to 2 because the third element is indexed within the vector at position 2). Notice that it doesn't contain an instance of the actual vector holding the triangular sequence elements. Why? It's because we don't want a copy of that vector in each class object; one instance is enough for all class objects. (In Section 4.5 we look at how to support that.) How do these data members get initialized? No, magic is not an option; the compiler does not do it for us. However, if we provide one or more special initialization functions, the compiler does invoke the appropriate instance each time a class object is defined. These special initialization functions are called constructors. We identify a constructor by giving it the same name as the class.The syntactic rules are that the constructor must not specify a return type nor return a value. It can be overloaded. For example, here are three possible Triangular class constructors: class Triangular { public: // overloaded set of constructors Triangular(); // default constructor Triangular( int len ); Triangular( int len, int beg_pos ); // ... }; A constructor is invoked automatically based on the values supplied to the class object being defined. For example, Triangular t; causes the default constructor to be applied to t. Similarly, Triangular t2( 10, 3 ); causes the two-argument constructor to be applied. The values in parentheses are treated as the values to be passed to the constructor. Similarly, Triangular t3 = 8; causes the one-argument integer constructor to be applied. Surprisingly, the following does not define a Triangular class object: Triangular t5(); // not what it seems :-) Rather, this defines t5 to be a function with an empty parameter list and returning a Triangular object. Obviously, this is a weird interpretation. Why is it interpreted this way? It's because C++ once needed to be compatible with the C language, and in C the parentheses following t5 in this case identify it as a function. The correct declaration of t5 is the same as t, shown earlier: Triangular t5; // ok The simplest constructor is the default constructor. A default constructor requires no arguments. This means one of two things. Either it takes no arguments: Triangular::Triangular() { // default constructor _length = 1; _beg_pos = 1; _next = 0; } or, more commonly, it provides a default value for each parameter: class Triangular { public: // also a default constructor Triangular( int len = 1, int bp = 1 ); // ... }; Triangular::Triangular( int len, int bp ) { // _length and _beg_pos both must be at least 1 // best not to trust the user to always be right _length = len > 0 ? len : 1; _beg_pos = bp > 0 ? bp : 1; _next = _beg_pos-1; } Because we provide a default value for both integer parameters, the single default constructor instance supports the original three constructors: Triangular tri1; // Triangular::Triangular( 1, 1 ); Triangular tri2( 12 ); // Triangular::Triangular( 12, 1 ); Triangular tri3( 8, 3 ); // Triangular::Triangular( 8, 3 ); The Member Initialization ListA second initialization syntax within the constructor definition uses the member initialization list: Triangular::Triangular( const Triangular &rhs ) : _length ( rhs._length ), _beg_pos( rhs._beg_pos ),_next( rhs._beg_pos-1 ) {} // yes, empty! The member initialization list is set off from the parameter list by a colon. It is a comma-separated list in which the value to be assigned the member is placed in parentheses following the member's name; it looks like a constructor call. In this example, the two alternative constructor definitions are equivalent. There is no significant benefit in choosing one form over the other. The member initialization list is used primarily to pass arguments to member class object constructors. For example, let's redefine the Triangular class to contain a string class member: class Triangular { public: // ... private: string _name; int _next, _length, _beg_pos; }; To pass the string constructor the value with which to initialize _name, we use the member initialization list. For example, Triangular::Triangular( int len, int bp ) : _name( "Triangular" ) { _length = len > 0 ? len : 1; _beg_pos = bp > 0 ? bp : 1; _next = _beg_pos-1; } Complementing the constructor mechanism is that of the destructor. A destructor is a user-defined class member function that, if present, is applied automatically to a class object before the end of its lifetime. The primary use of a destructor is to free resources acquired within the constructor or during the lifetime of the object. A destructor is given the name of the class prefixed by a tilde (~). It must not specify a return value and must declare an empty parameter list. Because it has an empty parameter list, the class destructor cannot be overloaded. Consider the following Matrix class. Within its constructor, the new expression is applied to allocate an array of doubles from the heap. The destructor is used to free that memory: class Matrix { public: Matrix( int row, int col ) : _row( row ), _col( col ) { // constructor allocates a resource // note: no error checking is shown _pmat = new double[ row * col ]; } ~Matrix() { // destructor frees the resource delete [] _pmat; } // ... private: int _row, _col; double *_pmat; }; In effect, we have automated the heap memory management within the Matrix class through the definition of its constructor and destructor. For example, consider the following statement block: { Matrix mat( 4, 4 ); // constructor applied here // ... // destructor applied here } The compiler applies the Matrix class constructor implicitly following the definition of mat. Internally, _pmat is initialized with the address of an array of 16 doubles allocated on the program's free store. Just before the closing brace of the statement block, the Matrix destructor is applied implicitly by the compiler. Internally, the array of 16 doubles addressed by _pmat is freed through the delete expression. Users of the Matrix class do not need to know any of the memory management details. This arrangment loosely mimics the design of the standard library container classes. It is not always necessary to define a destructor. In our Triangular class, for example, the three data members are stored by value. They come into existence when the class object is defined and are deallocated automatically when the lifetime of the class object ends. There is no real work for the Triangular destructor to do. We are under no obligation to provide a destructor. The hard part is to understand when one is or is not necessary. Memberwise InitializationBy default, when we initialize one class object with another, as in Triangular tri1( 8 ); Triangular tri2 = tri1; the data members of the class are copied in turn. In our example, _length, _beg_pos, and _next are copied in turn from tri1 to tri2. This is called default memberwise initialization. In the case of the Triangular class, default memberwise initialization correctly copies the class data members and there is nothing we need to do explicitly. In the case of the Matrix class introduced earlier, the default memberwise behavior is not adequate. For example, consider the following: { Matrix mat( 4, 4 ); // constructor applied here { Matrix mat2 = mat; // default memberwise copy applied // ... use mat2 here // destructor applied here for mat2 } // ... use mat here // destructor applied here for mat } The default initialization of the _pmat member of mat2 with that of mat, mat2._pmat = mat._pmat; causes the two instances of _pmat to address the same array in heap memory. When the Matrix destructor is applied to mat2, the array is deallocated. Unfortunately, the _pmat member of mat continues to address and manipulate the now deallocated array. This is a serious program bug. How can we fix this? In this case, we must override the default memberwise behavior. We do that by providing an explicit instance of the Matrix class copy constructor. (By we I mean the designer of the Matrix class. The users of the Matrix class just presume we have done the right thing.) If the designer of the class provides an explicit instance of the copy constructor, that instance is used in place of default memberwise initialization. The source code of the user need not change, although it must be recompiled. What does our copy constructor look like? Its single argument is a const reference to an object of the Matrix class: Matrix::Matrix( const Matrix &rhs ){ // what should go here? } How should it be implemented? Let's create a separate copy of the array so that the destruction of one class object does not interfere with the behavior of the second: Matrix::Matrix( const Matrix &rhs ) : _row( rhs._row ), _col( rhs._col ) { // create a "deep copy" of the array addressed by rhs._pmat int elem_cnt = _row * _col; _pmat = new double[ elem_cnt ]; for ( int ix = 0; ix < elem_cnt; ++ix ] _pmat[ ix ] = rhs._pmat[ ix ]; } When we design a class, we must ask ourselves whether the default memberwise behavior is adequate for the class. If it is, we need not provide an explicit copy constructor. If it is not, we must define an explicit instance and within it implement the correct initialization semantics. If our class requires a copy constructor, it also requires a copy assignment operator (see Section 4.8). For a more detailed discussion of class constructors and destructors see [LIPPMAN98], Chapter 14, and [LIPPMAN96a], Chapters 2 and 5. ![]() |
I l@ve RuBoard |
![]() ![]() |