| < Day Day Up > |
|
Class behavior can be implemented by building upon the behavior provided by other classes. In other words, a class type object can contain other class type objects. A class built upon the functionality of other classes is referred to as an aggregate class type or aggregation. There are two types of aggregation: simple and composite. Aggregation is also referred to as a “has-a” or a “uses-a” relationship between the whole class and its part classes where the whole or aggregate class uses the services of its part classes.
The aggregation relationship is expressed in terms of the whole class and the part class. Given two classes, class B and class A, if class B contains class A then class B is referred to as the whole class and class A is referred to as the part class.
The primary difference between simple and composite aggregation lies in who controls the lifetime of the object.
A simple aggregate object does not control the lifetimes of its part objects. Part objects involved in simple aggregations can be associated with more than one aggregation.
A composite aggregate object controls the lifetimes of its part objects. Part objects involved in composite aggregations cannot be associated with more than one aggregation.
Example 12.1 gives the class declarations for class A that will be used to demonstrate the two different types of aggregation. Class A will be the contained or part class. Example 12.2 shows the implementation code for class A which would be in a file named a.cpp
Listing 12.1: a.h
1 #ifndef _CLASS_A_H 2 #define _CLASS_A_H 3 4 class A { 5 public: 6 A(); 7 ~A(); 8 }; 9 #endif
Listing 12.2: a.cpp
1 #include <iostream> 2 using namespace std; 3 #include "a.h" 4 5 A::A(){ 6 cout<<"An object of type A created!"<<endl; 7 } 8 9 A::~A(){ 10 cout<<"An object of type A destroyed!"<<endl; 11 }
Class A is relatively simple. All it does is print a message to the screen when a class A object is created and destroyed. These messages will come in handy for learning about the behavior of aggregate objects.
Example 12.3 gives the class declaration for class B containing a class A object. Class B is the aggregate or whole class and class A is the part class.
Example 12.3: b.h
Said another way, an object of type B has an object of type A.
Example 12.4 shows the implementation code for class B. It looks exactly like the implementation code for class A except the name of the class has changed.
Listing 12.4: b.cpp
1 #include <iostream> 2 using namespace std; 3 #include "b.h" 4 5 B::B(){ 6 cout<<"An object of type B created!"<<endl; 7 } 8 9 B::~B(){ 10 cout<<"An object of type B destroyed!"<<endl; 11 }
Example 12.5 gives the code for a main() function that creates a class B object and figure 12-1 shows the results obtained from running the program.
Listing 12.5: main.cpp
1 #include <iostream> 2 using namespace std; 3 #include "b.h" 4 5 int main(){ 6 B b1; 7 return 0; 8 }
Figure 12-1: Results of Running Example 12.5
Let us pause here for a moment of discussion. Study figure 12-1. Notice how the A object’s constructor was called prior to the B object’s constructor. From this experiment we can deduce that ordinary part objects will be created before the whole aggregate object is created. An important distinction to make here is that object creation is not complete until the constructor has finished executing. The important thing to take away from this example is that the life of a part object is controlled by the composite aggregate whole object. Notice in example 12.5 that only a B object is created. This causes the creation of B’s part object. For an object with part members to be fully created, all of its part members must first be created.
The B class will be slightly modified to show another why to create aggregate objects using pointers. Example 12.6 gives the code for the modified class B declaration.
Listing 12.6: b.h
1 #ifndef _CLASS_B_H 2 #define _CLASS_B_H 3 #include "a.h" 4 5 class B{ 6 public: 7 B(); 8 ~B(); 9 private: 10 A *its_a_ptr; 11 }; 12 #endif
The only change made to the B class declaration appears on line 10. B’s private data member was changed from an A object to a pointer to an A object. The name of the identifier was also changed to reflect its new role as a pointer.
Example 12.7 shows the modified class B implementation code.
Listing 12.7: b.cpp
1 #include <iostream> 2 using namespace std; 3 #include "b.h" 4 5 B::B(){ 6 its_a_ptr = new A(); 7 cout<<"An object of type B created!"<<endl; 8 } 9 10 B::~B(){ 11 delete its_a_ptr; 12 cout<<"An object of type B destroyed!"<<endl; 13 }
Several changes were made to this file. First, the code on line 6 was added to the constructor to explicitly create the A object and assign its address to its_a_ptr. The second change is to the destructor. The code on line 11 was added to explicitly call the A object’s destructor by deleting the pointer.
The result of running example 12.5 again is shown in figure 12-2.
Figure 12-2: Results of Running Example 12.5 Again
The order of the messages in figure 12-2 is of minor importance. Special code is added to the constructor and destructor to create and destroy B’s A object. Let us now take a look at a simple composite class.
To demonstrate simple aggregation the A and B class files will be once again modified. Example 12.8 gives the source code for the revised a.h file.
Listing 12.8: a.h
1 #ifndef _CLASS_A_H 2 #define _CLASS_A_H 3 4 class A { 5 public: 6 A(); 7 ~A(); 8 void sayHi(); 9 }; 10 #endif
The only change to the A class declaration is the addition of another public function on line 8 named sayHi(). The modified a.cpp file is shown in example 12.9.
Listing 12.9: a.cpp
1 #include <iostream> 2 using namespace std; 3 #include "a.h" 4 5 A::A(){ 6 cout<<"An object of type A created!"<<endl; 7 } 8 9 A::~A(){ 10 cout<<"An object of type A destroyed!"<<endl; 11 } 12 13 void A::sayHi(){ 14 cout<<"Hi!"<<endl; 15 }
The sayHi() function definition begins on line 13. All it will do is print a simple message to the screen. Now, in addition to the constructor and destructor messages, any object that contains an A object will be able to call the A object’s sayHi() function. The modified B class declaration is given in example 12.10.
Listing 12.10: b.h
1 #ifndef _CLASS_B_H 2 #define _CLASS_B_H 3 #include "a.h" 4 5 class B{ 6 public: 7 B(A *a_ptr); 8 ~B(); 9 void makeContainedObjectSayHi(); 10 private: 11 A *its_a_ptr; 12 }; 13 #endif
Two modifications were made to the B class declaration. A parameter is added to the B constructor function of type pointer to A. When a B object is created it will expect to be passed the address of an A object. The second modification is the addition of one additional public function named makeContainedObjectSayHi(). This seems to be a little long winded for a function name but it accurately reflects the purpose of the function and hints at its intended behavior when called. The modified b.cpp implementation file is shown in example 12.11.
Listing 12.11: b.cpp
1 #include <iostream> 2 using namespace std; 3 #include "b.h" 4 #include "a.h" 5 6 B::B(A *a_ptr):its_a_ptr(a_ptr){ 7 cout<<"An object of type B created!"<<endl; 8 } 9 10 B::~B(){ 11 cout<<"An object of type B destroyed!"<<endl; 12 } 13 14 void B::makeContainedObjectSayHi(){ 15 if(its_a_ptr != NULL) 16 its_a_ptr->sayHi(); 17 }
Several modifications were made to b.cpp. First, the code to create and destroy the A object from the constructor and destructor was removed. Since a pointer to an A object will be passed to a B object when one is created that code was no longer required. This emphasizes that the lifetimes of A objects are clearly not at the mercy of B objects. Next, the constructor was modified to add an initializer list to initialize the A class pointer data member named its_a_ptr. Lastly, the makeContainedObjectSayHi() function is implemented beginning on line 14. Notice that just a touch of error checking was introduced. If, for some reason, a B object is fed a NULL pointer when it is created, it would be a mistake to try and call any functions using its_a_ptr since it would be initialized to NULL. If its_a_ptr is not a NULL value then the sayHi() function is called on the contained-by-reference object via the shorthand pointer member access operator “->”.
All that is left now is to look an a main() function that uses the new versions of the A and B classes. Example 12.12 gives the code.
Listing 12.12: main.cpp
1 #include <iostream> 2 using namespace std; 3 #include "b.h" 4 #include "a.h" 5 6 int main(){ 7 A a1; 8 a1.sayHi(); 9 B b1(&a1); 10 b1.makeContainedObjectSayHi(); 11 return 0; 12 }
Starting on line 7, an A object named a1 is created and on the next line the sayHi() function is called to demonstrate the existence of the A object outside of the B object. Next, a B object is created and its constructor is called with the address of the A object obtained by using the & operator. On line 10 the makeContainedObjectSayHi() function is called on the B object. This in turn calls the sayHi() function by using the pointer to the A object as shown on line 16 of example 12.11 above.
| < Day Day Up > |
|