A destructor is a member function whose name is the class name preceded by the tilde character (~). A destructor's usual purpose is to destroy values of the class type, typically by using delete. Destructors
Destructors are usually used to deallocate memory and do other cleanup for a class object and its class members when the object is destroyed. A destructor is called for a class object when that object passes out of scope or is explicitly deleted. class X { public: // Constructor for class X X(); // Destructor for class X ~X(); }; A destructor takes no arguments and has no return type. Its address cannot be taken. Destructors cannot be declared const, volatile, const volatile or static. A destructor can be declared virtual or pure virtual. If no user-defined destructor exists for a class and one is needed, the compiler implicitly declares a destructor. This implicitly declared destructor is an inline public member of its class. The complement of the constructor is the destructor. In many circumstances, an object will need to perform some action or actions when it is destroyed. Local objects are created when their block is entered, and destroyed when the block is left. Global objects are destroyed when the program terminates. When an object is destroyed, its destructor (if it has one) is automatically called. There are many reasons why a destructor may be needed. For example, an object may need to deallocate memory that it had previously allocated or it may need to close a file that it had opened. In C++, it is the destructor that handles deactivation events. The destructor has the same name as the constructor, but it is preceded by a ~. For example, here is the stack class and its constructor and destructor. (Keep in mind that the stack class does not require a destructor; the one shown here is just for illustration.) // This creates the class stack. class stack { int stck[SIZE]; int tos; public: stack(); // constructor ~stack(); // destructor void push(int i); int pop(); }; // stack's constructor stack::stack() { tos = 0; cout << "Stack Initialized\n"; } // stack's destructor stack::~stack() { cout << "Stack Destroyed\n"; } Page 2
Constructors and destructors do not have return types, and cannot use return (expression) statements. // counter.cpp // object represents a counter variable #include <iostream> using namespace std; //////////////////////////////////////////////////////////////// class Counter { private: unsigned int count; //count public: Counter() : count(0) //constructor{ /*empty body*/ } void inc_count(){ count++; } int get_count(){ return count; } }; //////////////////////////////////////////////////////////////// int main() { Counter c1, c2; //define and initialize cout << “\nc1=” << c1.get_count(); //display cout << “\nc2=” << c2.get_count(); c1.inc_count(); //increment c1 c2.inc_count(); //increment c2 c2.inc_count(); //increment c2 cout << “\nc1=” << c1.get_count(); //display again cout << “\nc2=” << c2.get_count(); cout << endl; return 0; } The Counter class has one data member: count, of type unsigned int (since the count is always positive). It has three member functions: the constructor Counter(), inc_count(), which adds 1 to count; and get_count(), which returns the current value of count. Page 3
When an object of type Counter is first created, we want its count to be initialized to 0. After all, most counts start at 0. We could provide a set_count() function to do this and call it with an argument of 0, or we could provide a zero_count() function, which would always set count to 0. However, such functions would need to be executed every time we created a Counter object.
So the effect of this single statement is to not only create two objects, but also to initialize their count variables to 0.
Use constructor to initialize values of class data members //Modulo numbers and constructor initialization class mod_int { public: mod_int(int i) { v = i % modulus; } void assign(int i) { v = i % modulus; } void print() const { cout << v << '\t'; } const static int modulus; private: int v; }; const int mod_int::modulus = 60;
The integer v is restricted in value to 0, 1, 2, ... , modulus - 1. It is the programmer's responsibility to enforce this restriction by having all member functions guarantee this behavior. There are some unusual aspects of constructor functions. First, it is no accident that they have exactly the same name (Counter in this example) as the class of which they are members. This is one way the compiler knows they are constructors. Second, no return type is used for constructors. Why not? Since the constructor is called automatically by the system, there is no program for it to return anything to; a return value would not make sense. This is the second way the compiler knows they are constructors. Page 4
We can declare objects of type mod_int by using the mod_int constructor. Here are two examples: mod_int a(0); // a.v = 0; mod_int b(61); // b.v = 1; We could not, however, declare an object of type mod_int without providing a parameter. The statement: mod_int a;
would be incorrect. Only fairly simple classes can rely on the synthesized default constructor. The most common reason that a class must define its own default constructor is that the compiler generates the default for us only if we do not define any other constructors for the class. If we define any constructors, the class will not have a default constructor unless we define that constructor ourselves. The basis for this rule is that if a class requires control to initialize an object in one case, then the class is likely to require control in all cases. The compiler generates a default constructor automatically only if a class declares no constructors. A second reason to define the default constructor is that for some classes, the synthesized default constructor does the wrong thing. Remember that objects of builtin or compound type (such as arrays and pointers) that are defined inside a block have undefined value when they are default initialized The same rule applies to members of built-in type that are default initialized. Therefore, classes that have members of built-in or compound type should ordinarily either initialize those members inside the class or define their own version of the default constructor. Otherwise, users could create objects with members that have undefined value. Warning: Classes that have members of built-in or compound type usually should rely on the synthesized default constructor only if all such members have in-class initializers.
A third reason that some classes must define their own default constructor is that sometimes the compiler is unable to synthesize one. Page 5
A constructor with a void argument list or a list whose arguments all have defaults is called the default constructor. A default constructor has the special purpose of initializing arrays of objects of its class. It is often convenient to overload the constructor with several function declarations. In our mod_int example from the last lesson, it could be desirable to have the default value of v be 0. By adding the default constructor mod_int() { v = 0; } as a member function of mod_int, it is possible to have the following declarations: mod_int s1, s2; // init private member v to 0 mod_int d[5]; // arrays properly initialized
In both of these declarations, the empty parameter list constructor is invoked. A default constructor is one that can be called with no arguments. A default constructor is used to create a "generic object", but it is also important when the compiler is told to create an object but is not given any details. For example, if you take the struct Y defined previously and use it in a definition like this, Y y2[2] = { Y(1) }; the compiler will complain that it cannot find a default constructor. The second object in the array wants to be created with no arguments, and that is where the compiler looks for a default constructor. In fact, if you simply define an array of Y objects,Y y3[7]; the compiler will complain because it must have a default constructor to initialize every object in the array. The same problem occurs if you create an individual object like this: Y y4; Remember, if you have a constructor, the compiler ensures that construction always happens, regardless of the situation. The default constructor is so important that if (and only if) there are no constructors for a structure (struct or class), the compiler will automatically create one for you. So this works: // Automatically-generated default constructor class V { int i; // private }; // No constructor int main() { V v, v2[10]; } ///:~ If any constructors are defined, however, and there is no default constructor, the instances of V above will generate compile-time errors. You might think that the compiler-synthesized constructor should do some intelligent initialization, like setting all the memory for the object to zero. But it does not, since that would add extra overhead but be out of the programmer's control. If you want the memory to be initialized to zero, you must do it yourself by writing the default constructor explicitly. Although the compiler will create a default constructor for you, the behavior of the compiler-synthesized constructor is rarely what you want. You should treat this feature as a safety net, but use it sparingly. In general, you should define your constructors explicitly and not allow the compiler to do it for you. Page 6
Add a constructor, an initializer list, and a destructor to the person class you created earlier. We can also use a constructor to initialize sub-elements of an object. Constructor initializers are specified in a comma-separated list that follows the constructor parameter list and is preceded by a colon.
An initializer's form is member name (expression list) foo::foo(int* t):i(7), x(9.8), z(t) //initializer list { //other executable code follows here ....} We will now recode the mod_int constructor so it initializes the class member v. Instead of coding the constructor like this: mod_int::mod_int(int i = 0) { v = i % modulus; } we can code it using an initializer list like this: //Default constructor for mod_int mod_int::mod_int(int i = 0) : v(i % modulus){}
Notice how initialization replaced assignment. In general, initialization is preferred to assignment. Syntax 4.9 - Constructor with Field Initializer List Purpose: Supply the implementation of a constructor, initializing data fields before the body of the constructor. Page 7
Constructors with a single parameter are automatically conversion functions. One reason C++ has implicit conversions for ADTs is that, according to OOP conventions, ADTs should have the look and feel of the native types.
//ASCII printable characters class pr_char { public: pr_char(int i = 0) : c(i % 128) {} void print() const { cout << rep[c]; } private: int c; static char* rep[128]; }; char* pr_char::rep[128] = { "nul", "soh", "stx", ..... "w", "x", "y", "z",""{", "|", "}", "~", "del" }; int main() { int i; pr_char c; for (i = 0; i < 128; ++i) { c = i; //or: c = static_cast<pr_char>(i); c.print(); cout << endl; } }
It is very important to understand precisely how arguments are passed to a function. This affects how you write functions and ultimately how they operate. There are also a number of pitfalls to be avoided. In general, the function
arguments should correspond in type and sequence with the list of parameters in the function definition. You have no latitude so far as the sequence is concerned, but you do have some flexibility in the argument types. If you specify
a function argument of a type that does not correspond to the parameter type, then the compiler inserts an implicit conversion of the argument to the type of the parameter where possible.
The constructor creates an automatic conversion from integers to pr_char. Notice, the c = i; statement in the loop implies this conversion. It is also possible to explicitly use a cast. Page 8Lesson 11 In this module you learned how to use constructor member functions to create objects from classes. In particular, you learned:
Objects generally need to initialize variables or assign dynamic memory during their process of creation to become operative and to avoid returning unexpected values during their execution. A class can include a special function called a constructor, which is automatically called whenever a new object of this class is created. This constructor function must have the same name as the class, and cannot have any return type. Wouldn’t it be nice to be able to initialize a rational object with a numerator and denominator, and have them properly reduced automatically? You can do that by writing a special member function that looks and acts a little like assign, except the name is the same as the name of the type (rational), and the function has no return type or return value. Listing 4-11 shows how to write this special member function.Listing 4-11 #include <cassert> #include <cstdlib> #include <iostream> /* Compute GCD of two integers, using Euclid’s algorithm. */ int gcd(int n, int m) { n = std::abs(n); while (m != 0) { int tmp(n % m); n = m; m = tmp; } return n; } /// Represent rational number. struct rational { /// Construct rational object, given numerator and denominator. /// Always reduce to normal form. /// @param num numerator /// @param den denominator /// @pre denominator > 0 rational(int num, int den) : numerator{num}, denominator{den} { reduce(); }/// Assign numerator and denominator, then reduce to normal form. /// @param num numerator /// @param den denominator /// @pre denominator > 0 void assign(int num, int den) { numerator = num; denominator = den; reduce(); } /// Reduce the numerator and denominator by their GCD. void reduce() { assert(denominator != 0); int div{gcd(numerator, denominator)}; numerator = numerator / div; denominator = denominator / div; } int numerator; /// < numerator gets the sign of the rational value int denominator; ///< denominator is always positive }; int main() { rational pi{1420, 452}; std::cout << "pi is about " << pi.numerator << "/" << pi.denominator << '\n'; }
Click the Quiz link below to take a multiple-choice quiz covering the topics presented in this module. |