Student
Class ExampleAt this point you need to provide the Student
class declaration. It should, of course, include constructors and at least a few functions to provide an interface for the Student
class. Listing 14.1 does this, defining all the constructors inline. It also supplies some friends for input and output.
// studentc.h -- defining a Student class using containment
#ifndef STUDENTC_H_
#define STUDENTC_H_
#include <iostream>
#include <string>
#include <valarray>
class Student
{
private:
typedef std::valarray<double> ArrayDb;
std::string name; // contained object
ArrayDb scores; // contained object
// private method for scores output
std::ostream & arr_out(std::ostream & os) const;
public:
Student() : name("Null Student"), scores() {}
explicit Student(const std::string & s)
: name(s), scores() {}
explicit Student(int n) : name("Nully"), scores(n) {}
Student(const std::string & s, int n)
: name(s), scores(n) {}
Student(const std::string & s, const ArrayDb & a)
: name(s), scores(a) {}
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {}
~Student() {}
double Average() const;
const std::string & Name() const;
double & operator[](int i);
double operator[](int i) const;
// friends
// input
friend std::istream & operator>>(std::istream & is,
Student & stu); // 1 word
friend std::istream & getline(std::istream & is,
Student & stu); // 1 line
// output
friend std::ostream & operator<<(std::ostream & os,
const Student & stu);
};
#endif
In order to simplify notation, the Student
class contains this typedef
:
typedef std::valarray<double> ArrayDb;
This enables the remaining code to use the more convenient notation ArrayDb
instead of std::valarray<double>
. Thus, methods and friends can refer to the ArrayDb
type. Placing this typedef
in the private portion of the class definition means that it can be used internally in the Student
implementation but not by outside users of the Student
class.
Note the use of the keyword explicit
:
explicit Student(const std::string & s)
: name(s), scores() {}
explicit Student(int n) : name("Nully"), scores(n) {}
Recall that a constructor that can be called with one argument serves as an implicit conversion function from the argument type to the class type. This often is not a good idea. In the second constructor, for instance, the first argument represents the number of elements in an array rather than a value for the array, so having the constructor serve as an int
-to-Student
conversion function does not make sense. Using explicit
turns off implicit conversions. If this keyword were omitted, code like the following would be possible:
Student doh("Homer", 10); // store "Homer", create array of 10 elements
doh = 5; // reset name to "Nully", reset to empty array of 5 elements
Here, the inattentive programmer typed doh
instead of doh[0]
. If the constructor omitted explicit
, 5
would be converted to a temporary Student
object, using the constructor call Student(5)
, with the value of "Nully"
being used to set the name
member. Then assignment would replace the original doh
with the temporary object. With explicit
in place, the compiler will catch the assignment operator as an error.
Note that constructors all use the by-now-familiar member initializer list syntax to initialize the name
and scores
member objects. In some cases earlier in this book, such as the following, the constructors use it to initialize members that are built-in types:
Queue::Queue(int qs) : qsize(qs) {...} // initialize qsize to qs
This code uses the name of the data member (qsize
) in a member initializer list. Also constructors from previous examples, such as the following, use a member initializer list to initialize the base-class portion of a derived object:
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) {...}
For inherited objects, constructors use the class name in the member initializer list to invoke a specific base-class constructor. For member objects, constructors use the member name. For example, look at the last constructor in Listing 14.1:
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {}
Because it initializes member objects, not inherited objects, this constructor uses the member names, not the class names, in the initialization list. Each item in this initialization list invokes the matching constructor. That is, name(str)
invokes the string(const char *)
constructor, and scores(pd, n)
invokes the ArrayDb(const double *, int)
constructor, which, because of the typedef
, really is the valarray<double>(const double *, int)
constructor.
What happens if you don’t use the initialization list syntax? As with inherited components, C++ requires that all member objects be constructed before the rest of an object is constructed. So if you omit the initialization list, C++ uses the default constructors defined for the member objects’ classes.
The interface for a contained object isn’t public, but it can be used within the class methods. For example, here is how you can define a function that returns the average of a student’s scores:
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum()/scores.size();
else
return 0;
}
This defines a method that can be invoked by a Student
object. Internally, it uses the valarray size()
and sum()
methods. That’s because scores
is a valarray
object, so it can invoke the member functions of the valarray
class. In short, the Student
object invokes a Student
method, and the Student
method uses the contained valarray
object to invoke valarray
methods.
Similarly, you can define a friend function that uses the string
version of the <<
operator:
// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ":
";
...
}
Because stu.name
is a string
object, it invokes the operator<<(ostream &, const string &)
function, which is provided as part of the string
class package. Note that the operator<<(ostream & os, const Student & stu)
function has to be a friend to the Student
class so that it can access the name
member. (Alternatively, the function could use the public Name()
method instead of the private name
data member.)
Similarly, the function could use the valarray
implementation of <<
for output; unfortunately, there is none. Therefore, the class defines a private helper method to handle this task:
// private method
ostream & Student::arr_out(ostream & os) const
{
int i;
int lim = scores.size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << scores[i] << " ";
if (i % 5 == 4)
os << endl;
}
if (i % 5 != 0)
os << endl;
}
else
os << " empty array ";
return os;
}
Using a helper like this gathers the messy details together in one place and makes the coding of the friend function neater:
// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ":
";
stu.arr_out(os); // use private method for scores
return os;
}
The helper function could also act as a building block for other user-level output functions, should you choose to provide them.
Listing 14.2 shows the class methods file for the Student
class. It includes methods that allow you to use the []
operator to access individual scores in a Student
object.
// studentc.cpp -- Student class using containment
#include "studentc.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;
//public methods
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum()/scores.size();
else
return 0;
}
const string & Student::Name() const
{
return name;
}
double & Student::operator[](int i)
{
return scores[i]; // use valarray<double>::operator[]()
}
double Student::operator[](int i) const
{
return scores[i];
}
// private method
ostream & Student::arr_out(ostream & os) const
{
int i;
int lim = scores.size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << scores[i] << " ";
if (i % 5 == 4)
os << endl;
}
if (i % 5 != 0)
os << endl;
}
else
os << " empty array ";
return os;
}
// friends
// use string version of operator>>()
istream & operator>>(istream & is, Student & stu)
{
is >> stu.name;
return is;
}
// use string friend getline(ostream &, const string &)
istream & getline(istream & is, Student & stu)
{
getline(is, stu.name);
return is;
}
// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ":
";
stu.arr_out(os); // use private method for scores
return os;
}
Aside from the private helper method, Listing 14.2 doesn’t require much new code. Using containment allows you to take advantage of the code you or someone else has already written.
Student
ClassLet’s put together a small program to test the new Student
class. To keep things simple, it should use an array of just three Student
objects, each holding five quiz scores. And it should use an unsophisticated input cycle that doesn’t verify input and that doesn’t let you cut the input process short. Listing 14.3 presents the test program. Be sure to compile it along with studentc.cpp
.
// use_stuc.cpp -- using a composite class
// compile with studentc.cpp
#include <iostream>
#include "studentc.h"
using std::cin;
using std::cout;
using std::endl;
void set(Student & sa, int n);
const int pupils = 3;
const int quizzes = 5;
int main()
{
Student ada[pupils] =
{Student(quizzes), Student(quizzes), Student(quizzes)};
int i;
for (i = 0; i < pupils; ++i)
set(ada[i], quizzes);
cout << "
Student List:
";
for (i = 0; i < pupils; ++i)
cout << ada[i].Name() << endl;
cout << "
Results:";
for (i = 0; i < pupils; ++i)
{
cout << endl << ada[i];
cout << "average: " << ada[i].Average() << endl;
}
cout << "Done.
";
return 0;
}
void set(Student & sa, int n)
{
cout << "Please enter the student's name: ";
getline(cin, sa);
cout << "Please enter " << n << " quiz scores:
";
for (int i = 0; i < n; i++)
cin >> sa[i];
while (cin.get() != '
')
continue;
}
Here is a sample run of the program in Listings 14.1, 14.2, and 14.3:
Please enter the student's name: Gil Bayts
Please enter 5 quiz scores:
92 94 96 93 95
Please enter the student's name: Pat Roone
Please enter 5 quiz scores:
83 89 72 78 95
Please enter the student's name: Fleur O'Day
Please enter 5 quiz scores:
92 89 96 74 64
Student List:
Gil Bayts
Pat Roone
Fleur O'Day
Results:
Scores for Gil Bayts:
92 94 96 93 95
average: 94
Scores for Pat Roone:
83 89 72 78 95
average: 83.4
Scores for Fleur O'Day:
92 89 96 74 64
average: 83
Done.
C++ has a second means of implementing the has-a relationship: private inheritance. With private inheritance, public and protected members of the base class become private members of the derived class. This means the methods of the base class do not become part of the public interface of the derived object. They can be used, however, inside the member functions of the derived class.
Let’s look at the interface topic more closely. With public inheritance, the public methods of the base class become public methods of the derived class. In short, the derived class inherits the base-class interface. This is part of the is-a relationship. With private inheritance, the public methods of the base class become private methods of the derived class. In short, the derived class does not inherit the base-class interface. As you saw with contained objects, this lack of inheritance is part of the has-a relationship.
With private inheritance, a class does inherit the implementation. For example, if you base a Student
class on a string
class, the Student
class winds up with an inherited string
class component that can be used to store a string. Furthermore, the Student
methods can use the string
methods internally to access the string
component.
Containment adds an object to a class as a named member object, whereas private inheritance adds an object to a class as an unnamed inherited object. This book uses the term subobject to denote an object added by inheritance or by containment.
Private inheritance, then, provides the same features as containment: Acquire the implementation, don’t acquire the interface. Therefore it, too, can be used to implement a has-a relationship. In fact, you can produce a Student
class that uses private inheritance and has the same public interface as the containment version. Thus the differences between the two approaches affect the implementation, not the interface. Let’s see how you can use private inheritance to redesign the Student
class.
3.15.206.25