Pointers are notoriously error-prone. Nevertheless, their power and flexibility make them an indispensable part of programming. We can alleviate some anticipated problems by embedding pointers in classes and accessing them with member functions. Once the functions are debugged and validated, pointers are no longer a "monster in the closet." Indeed, smart pointers, a topic covered in CS 2420, are an abstract data type created with classes that further automate C++'s "raw" pointers and insulate programmers from some of their most troublesome behaviors. We can take a similar approach by using pointers to implement aggregation.
|
|
|
| (a) | (b) |
new and delete are inverse but complementary operators. The former allocates memory on the heap, and the latter deallocates it. Safe and secure programs always initialize object pointers and deallocate all unused heap memory. Constructors perform the initialization, and destructors handle the deallocation. Destructors and constructors are similar in many ways:
~, and they may not have any parameters (implying that programmers cannot overload them). Destructors can perform any "clean-up" steps necessary to destroy an object, but memory deallocation is the most common.
We extend the Person class introduced in the previous section to create a simple aggregation example. We create an aggregation relationship between the Person and string classes. One of Person's member variables is a pointer, making it the whole class and string the part. Programmers must carefully manage the address stored in the pointer and the object it points at to avoid memory errors and leaks.
|
|
class Person
{
private:
string* name = nullptr; // (a)
int weight = 0;
double height = 0;
public:
Person() {} // (b)
Person(string n, int w, double h)
: name(new string(n)), weight(w), height(h) {} // (c)
Person(int w, double h) : name(nullptr), weight(w), height(h) {} // (d)
~Person() // (e)
{
if (name != nullptr)
delete name;
}
void setName(string* n) // (f)
{
if (name != nullptr)
delete name;
name = n;
}
}; |
nullptr.
nullptr, which, until the ANSI C++14 standard, could only be done in the constructor as illustrated.Constructors can initialize a pointer member variable in various ways, but two are common. First, they can create a new part object from its constituent or "raw" ingredients (i.e., data), passed in as the constructor's parameters. Alternatively, the constructor's parameter may be the address of an existing object created elsewhere in the program. Classes may have both constructors but typically only need one. Which constructor programmers choose to implement depends on the object designated as the part's "owner," a question we explore later in this section.
class Engine // the part class
{
private:
double size;
int cylinders;
public:
Engine(double s, int c) : size(s), cylinders(c) {} // (a)
};
class Car // the whole class
{
private:
Engine* motor; // (b)
string model;
public:
Car(string m, double s, int c) // (c)
: motor(new Engine(s, c)), model(m) {}
Car(string m, Engine* e) // (d)
: motor(e), model(m) {}
};
new Engine(s, c)) is a function call to the Engine constructor, so the number and type of arguments in the call must match the number and type of parameters in the constructor. The variable names in the constructor call must match the names in the constructor's parameter list.It may not always be practical to build an aggregation relationship (i.e., initialize a pointer member) when constructing a whole object. And at other times, programmers may need to change an existing part. The solution in both cases is an accessor or setter function. But there is one subtle difference between a constructor and a setter. A constructor creates a new object, so initially, the pointer member cannot point to an existing part. Alternatively, a setter function may establish the whole's first part or replace an existing one. Unlike a constructor, a setter must detect which situation applies and behave accordingly.
class Car
{
private:
Engine* motor;
string model;
public:
Car(string s) : model(s), motor(nullptr) {}
void set_motor(double s, int c);
void set_motor(Engine* e);
};
|
void Car::set_motor(double s, int c)
{
delete motor;
motor = new Engine(s, c);
} |
| (b) | |
void Car::set_motor(Engine* e)
{
delete motor;
motor = e;
}
|
|
| (a) | (c) |
nullptr.Pointers are a small, fixed-size data type whose size is independent of the data (object) to which they point, and it's entirely possible to have multiple pointers pointing to the same data or object. Programmers can use multiple pointers to organize data in numerous ways without incurring the expense of duplicating that data. In this way, two or more whole objects can share a part. What the sharing means depends on the problem that the program solves. For the following example, imagine the Car illustrated in Figure 1 is a sponsored racing car. Racing cars are notoriously hard on engines, so it's not unreasonable to further imagine that the racing team has several spare engines. Finally, imagine that the team tracks the spare engines with a database class named Warehouse. Now it should be easier to see how a Car can share its Engine with another program object.
|
|
|
| (a) | (b) |
When two or more whole classes share a part, programmers must establish a protocol specifying which whole is responsible for managing the parts. Typically, we designate one class to create new part objects and destroy them when they are no longer needed. Alternatively, any whole can create a part and share it as needed. Finally, while we can devise an arbitrarily complex algorithm to determine which whole will destroy a part, I highly recommend assigning that responsibility to a single class.
We expect the number and variety of class relationships to grow as the size and complexity of programs increase. Programs build objects and the relationships that bind them together by creating constructor-call chains. The chains execute when the program instantiates an object from one class. The following examples demonstrate how we form the constructor chains. Aside from the class names, the two examples are similar, differing only in the aggregation's location relative to the inheritance relationship. We'll see how to use the chains in the next section.
class Address
{
private:
string city;
string state;
public:
Address(string c, string s)
: city(c), state(s) {}
};
|
|
class Person
{
private:
string name;
Address* addr; // aggregation
public:
Person(string n, string c, string s)
: addr(new Address(c, s)),
name(n) {}
};
|
class Student : public Person
{
private:
double gpa;
public:
Student(string n, double g, string c, string s)
: Person(n, c, s), gpa(g) {}
};
|
Student valedictorian("Alice", 4.00, "Ogden", "Utah");
The data needed to populate the three related objects is present as constructor arguments.
class Pet
{
private:
string name;
string vaccinations;
public:
Pet(string pn, string v)
: name(pn), vaccinations(v) {}
};
|
|
class Person
{
private:
string name;
public:
Person(string n) : name(n) {}
};
|
class Owner : public Person
{
private:
Pet* my_pet; // aggregation
int account;
public:
Owner(string n, int a, string pn, string v)
: Person(n), my_pet(new Pet(pn, v), account(a) {}
}; |
Owner client("McCartney", 123456, "Martha", "June 1, 1980");
The figure highlights the syntax forming the relationships in yellow, while the Owner constructor calls the Person and Pet constructors.