In This Chapter
Making argumentative constructors
Overloading the constructor
Creating objects by using constructors
Invoking member constructors
Constructing the order of construction and destruction
Aclass represents a type of object in the real world. For example, in earlier chapters, I use the class Student
to represent the properties of a student. Just like students, classes are autonomous. Unlike a student, a class is responsible for its own care and feeding — a class must keep itself in a valid state at all times.
The default constructor presented in Chapter 15 isn't always enough. For example, a default constructor can initialize the student ID to 0 so that it doesn't contain a random value; however, a Student
ID of 0 is probably not valid.
C++ programmers require a constructor that accepts some type of argument to initialize an object to other than its default value. This chapter examines constructors with arguments.
C++ enables programmers to define a constructor with arguments, as shown here:
class Student { public: Student(const char *pName); // ...class continues... };
Conceptually, the idea of adding an argument is simple. A constructor is a member function, and member functions can have arguments. Therefore, constructors can have arguments.
Remember, though, that you don't call the constructor like a normal function. Therefore, the only time to pass arguments to the constructor is when the object is created. For example, the following program creates an object s
of the class Student
by calling the Student(const char*)
constructor. The object s
is destructed when the function main()
returns.
// // ConstructorWArg - a class may pass along arguments // to the members' constructors // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; class Student { public: Student(const char* pName) { cout << "constructing Student " << pName << endl; name = pName; semesterHours = 0; gpa = 0.0; } // ...other public members... protected: string name; int semesterHours; float gpa; }; int main(int argcs, char* pArgs[]) { // create a student locally and one off the heap Student s1("Chester"); Student* pS2 = new Student("Trude"); // be sure to delete the heap student delete pS2; // wait until user is ready before terminating program // to allow the user to see the program results
system("PAUSE"); return 0; }
The Student
constructor here looks like the constructors shown in Chapter 15 except for the addition of the const char*
argument pName
. The constructor initializes the data members to their empty start-up values, except for the data member name
, which gets its initial value from pName
because a Student
object without a name is not a valid student.
The object s1
is created in main()
. The argument to be passed to the constructor appears in the declaration of s1
, right next to the name of the object. Thus, the student s1
is given the name Chester
in this declaration.
A second student is allocated off the heap on the very next line. The arguments to the constructor in this case appear next to the name of the class.
The third executable line in the program returns the newly allocated object to the heap before exiting the program. This may not be necessary; for example, Windows or Unix will close any files you may have open and return all heap memory when a program terminates even if you forget to do so yourself. However, it's good practice to delete your heap memory when you're finished.
The const
in the constructor declaration Student::Student(const char*)
is necessary to allow statements such as the following:
Student s1("Chester");
The type of "Chester" is const char *
. I could not pass a pointer to a constant character string to a constructor declared Student(char*)
. A function, including a constructor, declared this way might attempt to modify the character string, which would not be good. You cannot strip away the const
part of a declaration.
You can add const
-ness, however, as in the following:
void fn(char* pName) { // the following is allowed even though constructor // declared Student(const char*) Student s(pName); // ...do whatever... }
The function fn()
passes a char*
string to a constructor that promises to treat the string as if it were a constant.
I can draw one more parallel between constructors and other more normal member functions in this chapter: Constructors can be overloaded.
Overloading a function means to define two functions with the same short name but with different types of arguments. See Chapter 6 for the latest news on function overloading.
C++ chooses the proper constructor based on the arguments in the declaration of the object. For example, the class Student
can have all three constructors shown in the following snippet at the same time:
// // OverloadConstructor - provide the class multiple // ways to create objects by // overloading the constructor // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; class Student { public: Student() { cout << "constructing student No Name" << endl; name = "No Name"; semesterHours = 0; gpa = 0.0; } Student(const char *pName) { cout << "constructing student " << pName << endl; name = pName; semesterHours = 0; gpa = 0; } Student(const char *pName, int xfrHours, float xfrGPA) { cout << "constructing student " << pName << endl; name = pName; semesterHours = xfrHours; gpa = xfrGPA; }
protected: string name; int semesterHours; float gpa; }; int main(int argcs, char* pArgs[]) { // the following invokes three different constructors Student noName; Student freshman("Marian Haste"); Student xferStudent("Pikumup Andropov", 80, 2.5); // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
Because the object noName
appears with no arguments, it's constructed using the constructor Student::Student()
. This constructor is called the default constructor. The freshMan
is constructed using the constructor that has only a const char*
argument, and the xferStudent
uses the constructor with three arguments.
Notice the similarity in all three constructors. The number of semester hours and the GPA default to 0 if only the name is provided. Otherwise, there is no difference between the two constructors. You wouldn't need both constructors if you could just specify a default value for the two arguments.
C++ enables you to specify a default value for a function argument in the declaration to be used in the event that the argument is not present. By adding defaults to the last constructor, all three constructors can be combined into one. For example, the following class combines all three constructors into a single, clever constructor:
// // ConstructorWDefaults - multiple constructors can often // be combined with the definition // of default arguments // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; class Student
{ public: Student(const char *pName = "No Name", int xfrHours = 0, float xfrGPA = 0.0) { cout << "constructing student " << pName << endl; name = pName; semesterHours = xfrHours; gpa = xfrGPA; } protected: string name; int semesterHours; float gpa; }; int main(int argcs, char* pArgs[]) { // the following invokes three different constructors Student noName; Student freshman("Marian Haste"); Student xferStudent("Pikumup Andropov", 80, 2.5); // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
Now all three objects are constructed using the same constructor; defaults are provided for nonexistent arguments in noName
and freshMan
.
As far as C++ is concerned, every class must have a constructor; otherwise, you can't create objects of that class. If you don't provide a constructor for your class, C++ should probably just generate an error, but it doesn't. To provide compatibility with existing C code, which knows nothing about constructors, C++ automatically provides a default constructor (sort of a default default constructor) that sets each data member to the default value for its type: 0 for int
, 0.0 for float
and double
, and so on.
If you define a constructor for your class, C++ doesn't provide the automatic default constructor on its own. (Having tipped your hand that this isn't a C program, C++ doesn't feel obliged to do any extra work to ensure compatibility.)
The following code snippets help demonstrate this point. This is legal:
class Student { string name; }; int main(int argcs, char* pArgs[]) { Student noName; return 0; }
The automatically provided default constructor invokes the default string
constructor to create an empty name
object. The following code snippet does not compile properly:
class Student { public: Student(const char *pName) {name = pName;} string name; }; int main(int argcs, char* pArgs[]) { Student noName; // doesn't compile return 0; }
The seemingly innocuous addition of the Student(const char*)
constructor precludes C++ from automatically providing a Student()
constructor with which to build object noName
.
The C++ '09 standard allows you to "get the default constructor back" via the new keyword default
, as follows:
class Student { public: Student(const char *pName) { name = pName; } Student() = default;
string name; }; int main(int argcs, char* pArgs[]) { Student noName; return 0; }
The default
keyword says, in effect, "I know that I defined a constructor but I still want my automatic default constructor back."
The '09 standard also allows a default method such as the default constructor to be explicitly removed using the new keyword delete
:
class Student { public: Student() = delete; // remove the default constructor string name; };
This makes more sense when applied to automatic methods other than the default constructor, as you see later in the book.
In the previous examples, all data members are of simple types, such as int
and float
. With simple types, it's sufficient to assign a value to the variable within the constructor. Problems arise when initializing certain types of data members, however.
Members of a class have the same problems as any other variable. It makes no sense for a Student
object to have some default ID of 0. This is true even if the object is a member of a class. Consider the following example that creates a new class StudentId
to manage the student identification numbers instead of relying on a plain ol' integer variable:
// // ConstructingMembers - a class may pass along arguments // to the the members' constructors
// #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; int nextStudentId = 1000; // first legal Student ID class StudentId { public: // default constructor assigns id's sequentially StudentId() { value = nextStudentId++; cout << "take next student id " << value << endl; } // int constructor allows user to assign id StudentId(int id) { value = id; cout << "assign student id " << value << endl; } protected: int value; }; class Student { public: Student(const char* pName) { name = pName; } // ...other public members... protected: string name; StudentId id; }; int main(int argcs, char* pArgs[]) { // create a couple of students Student s1("Chester"); Student s2("Trude"); // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
A student ID is assigned to each student as the student
object is constructed. In this example, the default constructor for StudentId
assigns IDs sequentially using the global variable nextStudentId
to keep track.
The Student
class invokes the default constructor for the two students s1
and s2
. The output from the program shows that this is working properly:
take next student id 1000 constructing Student Chester take next student id 1001 constructing Student Trude Press any key to continue...
Notice that the message from the StudentId
constructor appears before the output from the Student
constructor. This implies that the constructor StudentId
was invoked even before the Student
constructor got underway.
If the programmer does not provide a constructor, the default constructor provided by C++ automatically invokes the default constructors for data members. The same is true come harvesting time. The destructor for the class automatically invokes the destructor for data members that have destructors. The C++−provided destructor does the same.
Okay, this is all great for the default constructor. But what if you want to invoke a constructor other than the default? Where do you put the object? The StudentId
class provides a second constructor that allows the student ID to be assigned to any arbitrary value. The question is, how do we invoke it?
Let me first show you what doesn't work. Consider the following program segment (only the relevant parts are included here — the entire program, ConstructSeparateID, is on the CD-ROM that accompanies this book):
class Student { public: Student(const char *pName, int ssId) { cout << "constructing student " << pName << endl; name = pName; // don't StudentId id(ssId); // construct a student id } protected: string name; StudentId id; };
int main(int argcs, char* pArgs[]) { Student s("Chester", 1234); cout << "This message from main" << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); return 0; }
Within the constructor for Student
, the programmer (that's me) has (cleverly) attempted to construct a StudentId
object named id
. (I also added a destructor to StudentId
that does nothing but output the ID of the object being destroyed.)
If you look at the output from this program, you can see the problem:
take next student id 1000 constructing student Chester assign student id 1234 destructing 1234 This message from main Press any key to continue...
We seem to be constructing two StudentId
objects: The first one is created with the default constructor as before. After control enters the constructor for Student
, a second StudentId
is created with the assigned value of 1234. Mysteriously, this 1234 object is then destroyed as soon as the program exits the Student
constructor.
The explanation for this rather bizarre behavior is clear. The data member id
already exists by the time the body of the constructor is entered. Instead of constructing the existing data member id
, the declaration provided in the constructor creates a local object of the same name. This local object is destructed upon returning from the constructor.
Somehow, we need a different mechanism to indicate "construct the existing member; don't create a new one." This mechanism needs to appear after the function argument list but before the open brace. C++ provides a construct for this, as shown in the following subset taken from the ConstructDataMembers program (the only change between this program and its predecessor is to the Student
class constructor — the entire program is on the CD-ROM):
class Student { public: Student(const char *pName, int ssId)
: name(pName), id(ssId) { cout << "constructing student " << pName << endl; } protected: string name; StudentId id; };
Notice in particular the first line of the constructor. Here's something you haven't seen before. The :
means that what follows are calls to the constructors of data members of the current class. To the C++ compiler, this line reads "Construct the members name
and id
using the arguments pName
and ssId
, respectively, of the Student
constructor. Whatever data members are not called out in this fashion are constructed using their default constructor."
The string
type is actually a conventional class defined in an include file which is included by iostream
. Programs prior to this example have been using the default string
constructor to create an empty name
and then copying the student's name into the object within the body of the constructor. It is more efficient to assign the string
object a value when it's created, if possible.
This new program generates the expected result:
assign student id 1234 constructing student Chester This message from main Press any key to continue...
A problem also arises when initializing a member that has been declared const
. Remember that a const
variable is initialized when it is declared and cannot be changed thereafter. How can the constructor assign a const
data member a value? The problem is solved with the following member initializer syntax:
class Mammal { public: Mammal(int nof) : numberOfFeet(nof) {} protected: const int numberOfFeet; };
Ostensibly, a given Mammal
has a fixed number of feet (barring amputation). The number of feet can, and should, be declared const
. This declaration assigns a value to the variable numberOfFeet
when the object is created. The numberOfFeet cannot be modified once it's been declared and initialized.
When there are multiple objects, all with constructors, programmers usually don't care about the order in which things are built. If one or more of the constructors has side effects, however, the order can make a difference.
The rules for the order of construction are as follows:
Local and static objects are constructed in the order in which their declarations are invoked.
Static objects are constructed only once.
Global objects are constructed in no particular order.
Members are constructed in the order in which they are declared in the class.
Objects are destructed in the opposite order in which they were constructed.
A static variable is a variable that is local to a function but retains its value from one function invocation to the next. A global variable is a variable declared outside a function.
Now we'll consider each of the preceding rules in turn.
Local objects are constructed in the order in which the program encounters their declaration. Normally, this is the same as the order in which the objects appear in the function, unless the function jumps around particular declarations. (By the way, jumping around declarations is a bad thing. It confuses the reader and the compiler.)
Static objects are similar to local variables, except that they are constructed only once. C++ waits until the first time control passes through the static's declaration before constructing the object. Consider the following trivial ConstructStatic program:
// // ConstructStatic - demonstrate that statics are only // constructed once // #include <cstdio> #include <cstdlib> #include <iostream> using namespace std; class DoNothing { public: DoNothing(int initial) : nValue(initial) { cout << "DoNothing constructed with a value of " << initial << endl; } ~DoNothing() { cout << "DoNothing object destructed" << endl; } int nValue; }; void fn(int i) { cout << "Function fn passed a value of " << i << endl; static DoNothing dn(i); } int main(int argcs, char* pArgs[]) { fn(10); fn(20); system("PAUSE"); return 0; }
Executing this program generates the following results:
Function fn passed a value of 10 DoNothing constructed with a value of 10 Function fn passed a value of 20 Press any key to continue... DoNothing object destructed
Notice that the message from the function fn()
appears twice, but the message from the constructor for DoNothing
appears only the first time fn()
is called. This indicates that the object is constructed the first time that fn()
is called but not thereafter. Also notice that the destructor is not invoked until the program returns from main()
as part of the program shutdown process.
All global variables go into scope as soon as the program starts. Thus, all global objects are constructed before control is passed to main()
.
Initializing global variables can cause real debugging headaches. Some debuggers try to execute up to main()
as soon as the program is loaded and before they hand over control to the user. This can be a problem because the constructor code for all global objects has already been executed by the time you can wrest control of your program. If one of these constructors has a fatal bug, you never even get a chance to find the problem. In this case, the program appears to die before it even starts!
The best way I've found to detect this type of problem is to set a breakpoint in every constructor that you even remotely suspect as well as the first statement in main()
. You will hit a breakpoint for each global object declared as soon as you start the program. Press Continue after each breakpoint until the program crashes — now you know that you pressed Continue once too often. Restart the program and repeat the process, but stop on the constructor that caused the program to crash. You can now single-step through the constructor until you find the problem. If you make it all the way to the breakpoint in main()
, the program did not crash while constructing global objects.
Figuring out the order of construction of local objects is easy. An order is implied by the flow of control. With globals, no such flow is available to give order. All globals go into scope simultaneously — remember? Okay, you argue, why can't the compiler just start at the top of the file and work its way down the list of global objects?
That would work fine for a single file (and I presume that's what most compilers do). Most programs in the real world consist of several files that are compiled separately and then linked. Because the compiler has no control over the order in which these files are linked, it cannot affect the order in which global objects are constructed from file to file.
Most of the time, the order of global construction is pretty ho-hum stuff. Once in a while, though, global variables generate bugs that are extremely difficult to track down. (It happens just often enough to make it worth mentioning in a book.)
Consider the following example:
class Student { public: Student (int id) : studentId(id) {} const int studentId; }; class Tutor { public: Tutor(Student& s) : tutoredId(s.studentId) {} int tutoredId; }; // set up a student Student randy(1234); // assign that student a tutor Tutor jenny(randy);
Here the constructor for Student
assigns a student ID. The constructor for Tutor
records the ID of the student to help. The program declares a student randy
and then assigns that student a tutor jenny
.
The problem is that the program makes the implicit assumption that randy
is constructed before jenny
. Suppose it were the other way around. Then jenny
would be constructed with a block of memory that had not yet been turned into a Student
object and, therefore, had garbage for a student ID.
The preceding example is not too difficult to figure out and more than a little contrived. Nevertheless, problems deriving from global objects being constructed in no particular order can appear in subtle ways. To avoid this problem, don't allow the constructor for one global object to refer to the contents of another global object.
Members of a class are constructed according to the order in which they're declared within the class. This isn't quite as obvious as it may sound. Consider the following example:
class Student { public: Student (int id, int age) : nAge(age), nId(id){} const int nId; const int nAge; double dAverage = 0.0; };
In this example, nId
is constructed before nAge
, even though nId
appears second in the constructor's initialization list because it appears before nAge
in the class definition. The data member dAverage
is constructed last for the same reason. The only time you might detect a difference in the construction order is when both data members are an instance of a class that has a constructor that has some mutual side effect.
The Code::Blocks/gcc compiler generates a warning message if your declaration lists the data members in an order other than the order they are constructed.
C++ views constructors with a single argument as way of converting from one type to another. Consider a user-defined type Complex
designed to represent complex numbers. Without getting too technical (for me, not for you), there is a natural conversion between real numbers and complex numbers just like the conversion from integers to real numbers, as in the following example:
double d = 1; // this is legal Complex c = d; // this should be allowed as well
In fact, C++ looks for ways to try to make sense out of statements like this. If the class Complex
has a constructor that takes as its argument a double
, C++ will use that constructor as a form of conversion, as if the preceding statement had been written as follows:
double d = 1; Complex c(d);
Some constructor-introduced conversions do not make sense. For example, you may not want C++ to convert an integer into a Student
object just because a Student(int)
constructor exists. Unexpected conversions can lead to strange runtime errors when C++ tries to make sense out of simple coding mistakes.
The programmer can use the keyword explicit
to avoid creating unexpected and unintended conversion paths. A constructor marked explicit
cannot be used as an implicit conversion path:
class Student { public: // the following "No Name" constructor cannot be used // as an implicit conversion path from int to Student explicit Student(int nStudentID); }; Student s = 1; // generates compiler error Student t(123456); // this is still allowed
The declaration of s
does not implicitly invoke the Student(int)
constructor since it is flagged as "explicitly invokable only." The explicit invoking of the constructor to create the object t
is still okay.
18.119.106.135