Aggregation and composition are similar, constructive relationships whose primary difference is their relative binding strengths. Aggregation's weak binding allows a program to create the whole- and part objects at different times and to establish and break the connection between them at will. Alternatively, composition's strong binding requires a program to create and destroy the objects and the connection joining them simultaneously. Although C++ embeds the parts inside the whole, they are objects requiring initialization or construction. Composition requires the whole-class constructor to initialize its embedded or composed parts by calling a part-class constructor. Programmers manage the initialization process by creating chains of constructor calls - one constructor calls another. Chaining is automatic if the part class has a default constructor but requires an explicit constructor call otherwise.
When available, a program can call a default constructor automatically whenever it instantiates an object. Although we don't see this implicit call in the code, the program makes the call, and the function runs.
class Person { private: string name; int weight; double height; public: Person() : weight(0), height(0) {} }; |
||
(a) | (b) | (c) |
Person p;
As stated in the previous chapter, I don't advocate that every class must have a default constructor. And even when a class has a default, sometimes a constructor chain must call a parameterized constructor. Furthermore, default constructors often create "empty" objects. For example, an "empty" Person object may have an "empty" name (no characters and a zero-length) and zero-values for weight and height. If the class allows these values, we may need to program the other member functions to test for that condition and adjust their behaviors appropriately. Alternatively, the class designer may require valid data for all three members when instantiating an object, simplifying the member functions. The syntax illustrated in the following example works when the part class does not have a default constructor, or the default constructor is inappropriate.
class Person { private: string name; int weight; double height; public: Person(string n, int w, double h) : name(n), weight(), height(h) {} }; |
|
(a) | (b) |
Class designers may optionally name composition and aggregation relationships by labeling the connector symbol. Labeling or naming a relationship is helpful when the whole has two independent parts of the same class type. For example, if the Person class has two string parts: a name and an address. If the relationship is labeled, programmers typically use the label to name the member variable implementing it. Otherwise, they invent a suitable member name.
Person p("Alice", 130, 5.5);The syntax calling a part constructor is similar to the syntax introduced in the last chapter to initialize fundamental-type variables like integers and doubles. But this is a function call, so the number and type of its arguments must match the number and type of parameters in the function definition. The names in the initializer list are not arbitrary and must match the member and parameter names.
Classes may participate in multiple relationships at the same time. The following examples demonstrate three classes connected by inheritance and composition. The program chains the constructor calls together through their initializer lists. When the program instantiates an object, it passes data to the constructor. The first constructor uses some data to initialize the first object and passes the rest to the next constructor in the chain.
|
|
class Address { private: string city; string state; public: Address(string c, string s) : city(c), state(s) {} }; class Person { private: string name; Address addr; // (a) public: Person(string n, string c, string s) : addr(c, s), name(n) {} // (b) }; class Student : public Person // (c) { private: double gpa; public: Student(string n, double g, string c, string s) : Person(n, c, s), gpa(g) {} // (d) } |
|
Student valedictorian("Alice", 4.0, "Ogden", "Utah");
The classes in the following example are taken from a program used to help manage a veterinarian's office. If the veterinarian specializes in small animals, it makes sense for the program to have a Pet class. The Pet needs an Owner, so the program includes one as a subclass of Person, and we build a whole-part relationship between Pet and Owner.
|
|
class Pet { private: string name; string vaccinations; public: Pet(string n, string v) : name(n), vaccinations(v) {} }; class Person { private: string name; string phone; public: Person(string n, string p) : name(n), phone(p) {} }; class Owner : public Person // (a) { private: int account; Pet my_pet; // (b) public: Owner(string n, string p, int a, string pn, string v) : Person(n, p), // (c) my_pet(pn, v), account(a) {} // (d) }; |
|
Owner pet_owner("Dilbert", "801-555-1234", 123456, "Fido", "2020/06/01");