Making Constructive Arguments - Introduction to Classes - C++ For Dummies (2014)

C++ For Dummies (2014)

Part III

Introduction to Classes

Chapter 16

Making Constructive Arguments

In This Chapter

arrow Making argumentative constructors

arrow Overloading the constructor

arrow Creating objects by using constructors

arrow Invoking member constructors

arrow Constructing the order of construction and destruction

A class 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.

Outfitting Constructors with Arguments

C++ enables programmers to define a constructor with arguments, as shown here:

class Student
{
public:
Student(const char *pName);

// ...class continues...
};

Using a constructor

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;
double gpa;
};

int main(int argcs, char* pArgs[])
{
// create a student locally and one off of the heap
Student s1("Jack");
Student* pS2 = new Student("Scruffy");

// 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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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 pNamebecause 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 Jack 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.

image 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.

image The const in the constructor declaration Student::Student(const char*) is necessary to allow statements such as the following:

Student s1("Jack");

The type of “Jack” 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 theconst 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. No harm there!

Placing Too Many Demands on the Carpenter: Overloading the Constructor

I can draw one more parallel between constructors and other more normal member functions in this chapter: Constructors can be overloaded.

image 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>
#include <string.h>

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("Pikup Andropov", 80, 2.5);

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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,
double xfrGPA = 0.0)
{
cout << "constructing student " << pName << endl;
name = pName;
semesterHours = xfrHours;
gpa = xfrGPA;
}

protected:
string name;
int semesterHours;
double gpa;
};
// ...the rest is the same...

Now all three objects are constructed using the same constructor; defaults are provided for non-existent arguments in noName and freshman.

image A slightly more flexible alternative added in the 2011 standard is to invoke one constructor from another as shown in ConstructorsCallingEachOther. This is known as delegating constructors:

// ConstructorsCallingEachOther - new for 2011,
// one constructor can invoke another constructor
// in the same class
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student

{
public:
Student(const char *pName,
int xfrHours,
double xfrGPA)
{
cout << "constructing student " << pName << endl;
name = pName;
semesterHours = xfrHours;
gpa = xfrGPA;
}
Student() : Student("No Name", 0, 0.0) {}
Student(const char *pName): Student(pName, 0, 0.0){}

protected:
string name;
int semesterHours;
double gpa;
};
// ...the rest is the same as before...

Here the declaration Student noName invokes the no argument constructor which turns around and calls the generic constructor, providing default arguments. The Student freshman declaration invokes the Student(const char*) constructor.

This is more flexible because you can default arguments other than the last one. In addition, you have more control over how arguments are defaulted. For example, it makes no sense to construct a student with semester hours but no GPA. This version would not allow such an object to be constructed since no Student(const char*, int) is provided.

image The somewhat bizarre syntax will seem a lot more reasonable by the time you reach the end of this chapter.

Defaulting Default Constructors

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).

If you define a constructor for your class, C++ doesn’t provide the automatic default constructor on its own. By creating a constructor, the author is in effect telling C++ that the default constructor is not good enough.

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.

image The C++ '11 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 '11 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;
};

Constructing Class Members

In the previous examples, all data members are of simple types, such as int and double. 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.

Constructing a complex data member

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 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)
{
cout << "constructing Student " << pName << endl;
name = pName;
semesterHours = 0;
gpa = 0.0;
}

// ...other public members...
protected:
string name;
int semesterHours;
double gpa;
StudentId id;
};

int main(int argcs, char* pArgs[])
{
// create a couple of students
Student s1("Jack");
Student s2("Scruffy");

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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 Jack
Take next student id 1001
constructing Student Scruffy
Press Enter 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 you 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 with the material that accompanies this book at www.dummies.com/extras/cplusplus):

class Student
{
public:
Student(const char *pName, int ssId)
{
cout << "constructing student " << pName << endl;
name = pName;
// don't try this at home kids. It doesn't work
StudentId id(ssId); // construct a student id
}
protected:
string name;
StudentId id;
};

int main(int argcs, char* pArgs[])
{
Student s("Jack", 1234);
cout << "This message from main" << endl;

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
}

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 Jack
assign student id 1234
destructing 1234
This message from main
Press Enter 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 theConstructDataMembers program (the only change between this program and its predecessor is to the Student class constructor — the entire program is with the accompanying material at www.dummies.com/extras/cplusplus):

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 argumentspName and ssId, respectively, of the Student constructor. Whatever data members are not called out in this fashion are constructed using their default constructor.”

image 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 Jack
This message from main
Press Enter to continue...

image Now you can see where the syntax for invoking one constructor from another came from!

Combining this with member initialization

image So what happens when a constructor competes with a C++ '11-style member initializer? Consider the following contrived example:

// ConstructMembersWithInitializers - this program
// demonstrates what happens when a data member
// with an initializer is constructed
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class StudentId
{
public:
StudentId(int id) : value(id)
{
cout << "id = " << value << endl;
}

protected:
int value;
};

int nextStudentId = 1000;
class Student
{
public:
Student(const char *pName, int ssId)
: name(pName), id(ssId)
{
cout << "constructing student " << pName << endl;
}
Student(const char *pName): name(pName)
{
cout << "constructing student " << pName << endl;
}
protected:
string name;
StudentId id = nextStudentId++;
};

int main(int argcs, char* pArgs[])
{
Student s1("Jack", 1234);
Student s2("Scruffy");

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
return 0;
}

Here I have provided the StudentID class with a single constructor. It is now up to the Student class to decide which id to use. The output from this program is enlightening:

id = 1234
constructing student Jack
id = 1000
constructing student Scruffy
Press Enter to continue...

In the first case, the student Jack is created using the student ID 1234 provided in the constructor. The student Scruffy accepts the default student ID, the next value starting with 1000. But this is curious — if the member initializer had been invoked when Jack was constructed, then Scruffy should have been assigned the ID 1001.

image The moral to this story is that the member initializer (that's the StudentId id = nextStudentId++) is ignored if the member is constructed in the class constructor.

Constructing a constant data member

Argument construction solves a similar problem with const data members as shown in the following example:

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 constructor definition assigns a value to the variable numberOfFeet when the object is created. The numberOfFeet cannot be modified once it's been declared and initialized.

Reconstructing the Order of Construction

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.

· All global objects are constructed before main().

· Global objects are constructed in no particular order.

· Members are constructed in the order in which they are declared in the class within a given access type (that is, all the public members are declared in order declared and all the protected members in the order that they're declared)

· Objects are destructed in the opposite order in which they were constructed.

image 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 construct in order

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 construct only once

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);
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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 Enter 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 objects construct before main()

All global variables go into scope as soon as the program starts. Thus, all global objects are constructed before control is passed to main().

image 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!

image 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.

Global objects construct in no particular order

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 janet(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 janet.

The problem is that the program makes the implicit assumption that randy is constructed before janet. Suppose it were the other way around. Then janet 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.

image 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 construct in the order in which they are declared

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.

Destructors destruct in the reverse order of the constructors

Finally, no matter in what order the constructors kick off, you can be assured that the destructors are invoked in the reverse order. (It’s nice to know that at least one rule in C++ has no ifs, ands, or buts.)

Constructing Arrays

When you declare an array, each element of the array must be constructed. For example, the following declaration calls the default Student constructor five times, once for each member of the array:

Student s[5];

image The 2011 standard allows you to invoke a constructor other than the default constructor using an initializer list, as shown in this truncated example program (the full program is available in the online material at www.dummies.com/extras/cplusplus):

//
// ConstructArray - construct an array of objects
//

// ...same Student class with overloaded constructors...

int main(int argcs, char* pArgs[])
{
// the following invokes three different constructors
Student s[]{"Marian Haste", "Pikup Andropov"};
Student t[]{{"Jack", 0, 0.0}, {"Scruffy", 12, 2.5}};

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
return 0;
}

The array s is created with two members by calling the Student(const char*) constructor twice. The array t is constructed with the Student(const char*, int, double) constructor. The output of this program appears as follows:

constructing freshman Marian Haste
constructing freshman Pikup Andropov
constructing transfer Jack
constructing transfer Scruffy
Press Enter to continue...

image A string of objects contained within braces is known as an initializer list.

Constructors as a Form of Conversion

C++ views constructors with a single argument as a 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 run-time errors when C++ tries to make sense out of simple coding mistakes.

image 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.

A complete TypeConversion program to demonstrate this principle is included with the online material at www.dummies.com/extras/cplusplus.