Please review the following concepts as needed:
Constructors are, first and foremost, just functions. They can range from simple to complex. However, constructors are special functions that are called automatically whenever a program creates (i.e., instantiates) a new object. The primary purpose of constructors is to construct or initialize an object. Visually, constructors are set apart from "regular" functions by two characteristics: (a) they have the same name as the class for which they are constructing objects, and (b) they do not have a return type.
As you might suppose, constructors are an essential part of object-oriented programs, enough so that we name five different kinds to make them easier to talk about.
Constructor Prototypes | Constructor Calls |
---|---|
class Foo { public: Foo(); Foo(int x); Foo(int x, int y); }; |
Foo f1; // (a) Foo* f2 = new Foo; //Foo f1(); //Foo* f2 = new Foo(); Foo f3(5); // (b) Foo* f4 = new Foo(5); Foo f5(5, 10); // (c) Foo* f6 = new Foo(5, 10); |
Each constructor is designed to fill a specific programming need, but most classes will only need a few constructors - not all. The following sections describe each kind of constructor in detail, but the focus is on the constructor's visible operations. One object-oriented feature, polymorphism, requires that each object store a hidden pointer (called the vptr or virtual pointer). One of the tasks of every constructor is to initialize vptr, which it does by running code that the compiler automatically inserts into each constructor. If the class does not have any constructors, the compiler creates a simple default constructor to initialize the vptr. We'll explore polymorphism in more detail in a later chapter.
As described below, we typically distinguish constructors based on their parameters. However, we use the term "general constructor" to denote any constructor that doesn't fit into one of the other categories. Further confusing constructor identification, sometimes we need a general constructor having only one parameter, but we don't use it as a conversion constructor. Sometimes the distinction between a general and conversion constructor is how we use it rather than the number of parameters. We'll see in the CString example later in the chapter that this can sometimes cause problems.
The primary characteristic that sets a default constructor apart from the other constructors is that it has no parameters. A default constructor often creates an "empty" object or an initialized object with default values. Although the text didn't state it at the time, many of our previous examples have relied on the string class default constructor that creates a string object that does not contain any characters. We can also use a default constructor to create an "empty" instance of our Time class.
class Time { private: int hours; int minutes; int seconds; public: Time(); }; |
class Time { private: int hours = 0; int minutes = 0; int seconds = 0; }; |
(a) | (b) |
class Time { private: int hours = 0; int minutes = 0; int seconds = 0; public: Time(int h, int m, int s); }; |
class Time { private: int hours = 0; int minutes = 0; int seconds = 0; public: Time() {} Time(int h, int m, int s); }; |
(c) | (d) |
Time t1; Time* t2 = new Time; |
Time t3(); Time* t4 = new Time(); |
(e) | (f) |
A conversion constructor converts one data type into an instance of the class in which the constructor appears. What the conversion means and how the conversion function works depends solely on the source and destination types. A conversion constructor has one argument, and the one highlighted in the following example converts an int
into an instance of the Time
class:
The conversion constructor illustrated above is the second make_time
function from the struct Time example rewritten as a constructor. It is possible to generalize the pattern of a conversion constructor as follows:
class Foo { . . . public: Foo(Bar b); // converts a Bar into a Foo };
Where Bar may be the name of a primitive, built-in data type, or it may be the name of a class. Here is another example of a conversion constructor based on strings and C-strings:
string s("Hello, World!");
The definition of variable s converts a C-string ("Hello, World!") into the string object s.
A copy constructor creates a new object by copying an existing object. C++ bases two critical and fundamental programming operations on the copy constructor:
This means that whenever functions pass or return objects by value, the program copies the objects from one part of the program to another. Each operation creates a new object, and constructing new objects is always the task of a constructor function. These operations are so fundamental to programming that the compiler automatically generates a copy constructor for every class. In the previous examples, copying an existing object is easy enough for the compiler to create the constructor automatically. Later, we will see more complex situations where the compiler-generated copy constructor is insufficient, and in such cases, we must override it with our own constructor.
Since the copy constructor implements pass-by-value (i.e., passing an object by value), its argument cannot be passed by value (which would cause infinite recursion). For this reason, copy constructors have a particular signature that makes them easy to identify and which programmers must follow when overriding the compiler-generated copy constructor. Copy constructors always require a single argument that is the same class type as the class in which the constructor appears, and the parameter is always a reference variable:
ClassName(const ClassName& o);
class Person
{
private:
string name;
int weight;
double height;
public:
Person(const Person& p);
}; |
Person::Person(const Person& p)
{
name = p.name;
weight = p.weight;
height = p.height;
}
|
If one or more member variables is a pointer, the copy operation becomes more complex, and we defer dealing with this situation until the next chapter. For the curious or those facing a more immediate problem, please see The Copy Constructor in the next chapter.
fraction fraction::add(fraction f2) { fraction temp; temp.numerator = . . .; temp.denominator = . . .; return temp; } |
fraction fraction::add(fraction f2) { int d = . . .; int n = . . .; return fraction(n, d); } |
(a) | (b) |
Like the copy constructor, the move constructor can be identified by its distinctive argument list:
ClassName(ClassName&& o);
If a move constructor has additional arguments, they must have default values (i.e., default arguments). Unlike copy constructors, move constructors can take some or all the resources held by the argument object rather than copying them. The argument object remains in a valid but potentially incomplete state. Our move constructor study doesn't extend beyond recognizing and identifying it. The double ampersand, &&
, which denotes an r-value reference declarator, and the move constructor are otherwise beyond the scope of CS 1410, but you'll study them in detail in CS 2420.
No special syntax or pattern defines a general constructor. A general constructor simply does not fit into one of the previously described categories above. So, any constructor that has two or more parameters is a general constructor just because it's not (a) a default constructor (no parameters), (b) a conversion constructor (has one parameter that's not a reference), or (c) a copy constructor (one parameter that is a reference). It is possible to convert the first make_time
function from the struct Time example into a general constructor:
One common task of constructor functions is initializing (i.e., assigning the first or initial value to) an object's member variables, regardless of the constructor's overall complexity. Although programmers can initialize members in the constructor's body, most practitioners consider it a better practice to initialize them with an initializer list. An initializer list is a compact notation equivalent to a sequence of assignment statements. But they have the advantage of running before the constructor's body, so the member variables are ready to use as soon as the body runs. Initializer lists begin with a colon and appear between a function's parameter list and the body's opening brace. Initializer lists follow a simple pattern:
An initializer list is a comma-separated list of initializer elements. Each element behaves like an assignment, so numerator(n)
is equivalent to numerator = n
. The color coding in the figure above highlights the connection between a constructor's arguments and member variables: the first part of each element is the name of a member variable, and the second part (enclosed in parentheses) is the name of one of the function's parameters. With one exception, the list elements may appear in any order. We'll explore that exception, inheritance, in the next chapter.
Works | Preferred |
---|---|
fraction::fraction(int n, int d) { numerator = n; denominator = d; int common = gcd(numerator, denominator); numerator /= common; denominator /= common; } |
fraction::fraction(int n, int d) : numerator(n), denominator(d) { int common = gcd(numerator, denominator); numerator /= common; denominator /= common; } |
(a) | (b) |
int common = gcd(n, d); numerator = n / common; denominator = d / common;
Every function must have exactly one body. The body is often empty in the case of simple constructors whose only purpose is to initialize the object's member variables. In the following example, the {}
at the end is the function's empty body and not part of the initializer list.
Person(string a_name, double a_height, int a_weight) : name(a_name), height(a_height), weight(a_weight) {}
Initializer lists are a part of the function definition and not of the declaration or prototype. So, if the class only contains a function prototype and the function definition is in a separate .cpp file, then the initializer list goes with the function definition in the .cpp file:
.h File | .cpp File |
---|---|
class fraction { private: int numerator; int denominator; public: fraction(int n, int d); }; |
fraction::fraction(int n, int d) : numerator(n), denominator(d) { int common = gcd(numerator, denominator); numerator /= common; denominator /= common; } |
Caution:
.h File | .cpp File |
---|---|
class fraction { public: fraction(int n, int d) {} }; |
fraction::fraction(int n, int d) : numerator(n), denominator(d) { . . . . } |
Although the UML has always permitted class designers to specify initial values for member variables and function arguments, C++ originally did not allow programmers to initialize member variables in the class specification. So, programmers initialized member variables with constructors, and you may still see examples of this in existing code. However, C++ has always supported default arguments, which may be used with any C++ function (not just constructors). When we use default arguments with constructors, they must follow all of the rules listed in chapter 6 (and it's probably a good idea to review those rules now).
+fraction(n: int = 0, d : int = 1) |
fraction(int n = 0, int d = 1); |
(a) | (b) |
In "real world" C++ programs, it is common for the class specification to appear in a .h file and the member functions (including constructors) to appear in a .cpp file. When we follow this organization, there is one unfortunate aspect of initializer lists and constructor default arguments that we must memorize:
.h File | .cpp File |
---|---|
class fraction { private: int numerator; int denominator; public: fraction(int n = 0, int d = 1); }; |
fraction::fraction(int n, int d) : numerator(n), denominator(d) { int common = gcd(numerator, denominator); numerator /= common; denominator /= common; } |
(a) | (b) |
fraction();
// acts as a default constructorfraction(n);
// acts as a conversion constructorfraction(n,d);
If a class only needs a constructor to initialize the member variables, replacing the constructor with initializations directly in the class specification is appropriate. The compiler will automatically create a default constructor to initialize the vptr as needed. However, initializing member variables directly inside the class specification doesn't always replace a default constructor or default arguments when object construction requires operations more complex than member initialization. Furthermore, if the class defines one or more parameterized constructors, then a default constructor or default arguments are still needed if the programmer wishes to create an object without calling a parameterized constructor:
fraction f1;
fraction* f2 = new fraction;
Like any function, constructors can range from algorithmically simple to complex. Sometimes complex constructors perform the same operations as simple ones, followed by additional operations befitting their complex nature. We can often avoid the overhead of writing and maintaining duplicate constructor code by putting the common, often simple, code in a basic constructor and allowing more advanced constructors to call the basic one. Java has always supported in-class constructor chaining by using this(...) as the name of an overloaded constructor and the number and type of arguments to differentiate between them. Before the adoption of the C++ 2011 standard, C++ did not permit in-class constructor chaining, but it does now, albeit with limitations.
class Table { private: int rows; int cols; int** array; }; |
Table(int r, int c) : rows(r), cols(c) { array = new int*[rows]; for (int i = 0; i < rows; i++) array[i] = new int[cols]; } |
(a) | (b) |
Table(const Table& t) : Table(trows, cols) { for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) array[i][j] = array[i][j]; } |
Table(const Table& t) : Table(trows, cols), member(10) { . . } |
(c) | (d) |
Chaining constructors works well when the operations of the called or delegated constructor (e.g., (b)) must or can run before the operations in the calling or delegating constructor (e.g., (c)). When that is not the case, the best we can do, in C++ or Java, is use a helper function to implement the common code.
display() { ...; ...; } |
Window() { ...; display(); } |
(a) | (b) |
Window(int x, int y) { ...; display(); } |
Window(int x, int y, int color) { ...; display(); } |
(c) | (d) |
private
section, so users cannot call them directly.1 A bitwise copy simply means that the computer copies a patch of bits, bit-for-bit, from one memory location to another. A program can perform the copy operation with the memcpy function, which many computers support with a single machine instruction (and so it is very fast and efficient). For example, given the following code fragment:
class foo { . . . }; foo f1; foo f2; f1 = f2;
The compiler can create a simple copy constructor with a single statement:
memcpy(&f1, &f2, size of(foo));