Courses/CS 2124/Lab Manual/Composition of Objects

From A-State Computer Science Wiki
Jump to: navigation, search

Introduction

This lab illustrates the utility of composition of objects and builds on the concepts of pointers and dynamic memory allocation.

A Student Class

Download the starter resource code provided with this assignment. It should consist of two files:

  • Student.h : header file defining a Student class.
  • Student.cpp : implementation for Student.

You will use the Student class for the remainder of this lab. No changes should be made to "Student.h" or "Student.cpp" unless you are specifically instructed to do so.

The structure of class Student looks like the following:

class Student {
public:
    Student( ) = default;
    Student( std::string name, int id );
    ~Student( );

    std::string get_name( ) const;
    int         get_id( ) const;
    double      get_exam_score( int exam_number ) const;
    double      get_average( ) const;
    double      get_exam_count( ) const;
    char        get_letter_grade( ) const;
    void        set_name( std::string name );
    void        set_id( int id );
    void        set_exam_score( int exam_number, double score );
    void        add_exam_score( double score );

    std::ostream& write( std::ostream& strm = std::cout ) const;

private:
    std::string         _name;
    int                 _id{0};
    std::vector<double> _exams;
};

Consider a collection of Students. In order to group the individual Student objects together, an array data structure will be used. In the following sections, four different versions of an array will be developed; the design will be refined in each successive version.

Automatic Array of Objects

The data structure might first be represented by the following class definition:

class StudentArrayV1 {  // version 1: automatic array of objects
public:
    StudentArrayV1( ) = default;
    void write( );

private:
    const static int n_members = 5;
    Student          members[n_members];
    int              number_of_students = n_members;
};

Note how the object class StudentArrayV1 is composed of objects of another class, Student. Storage is provided for the information of five students. This composition of objects demonstrates a very useful and powerful aspect of object-oriented programming: complex data structures can be built using the composition of relatively simple individual elements in a hierarchical fashion.

Put the definition for class StudentArrayV1 in project file StudentArrayV1.h; place the definitions of the constructor and write methods of the class in file StudentArrayV1.cpp.

Because of the automatic size of the private data array members, and because we used modern C++ member initialization to set initial values for number_of_students, the constructor can be specified as default (this explicitly says that the default constructor behavior is acceptable).

The array method write simply invokes the method Student::write (with no arguments) for each of the five array members.

This design for an array class is not very practical, however. The problem lies in the way the constructor for individual Student objects worked in the previous lab. The moment that memory is set aside for a StudentArrayV1 object, the constructors for each of the five Student object elements of the array will be called.

Test this behavior with a declaration and call to the write method in function main in file main.cpp.

int main( ) {
    StudentArrayV1 cs1114;

    cout << "contents of cs1114:\n";
    cs1114.write( );
    
    return 0;
}

Dynamic Array of Objects

Consider a revised definition for an array of Student objects, to be placed in file StudentArrayV2.h.

class StudentArrayV2 {  // version 2: dynamic array of objects
public:
    StudentArrayV2( int size );
    ~StudentArrayV2( );
    void write( );

private:
    Student* members            = nullptr;
    int      number_of_students = 0;
};

Here, no Student objects are allocated automatically with the creation of StudentArrayV2 since none are listed as data. However, Student objects may be allocated dynamically by the constructor. Write the constructor in file StudentArrayV2.cpp to accept as its parameter the number of students in the course, then dynamically allocate an array of Student objects of this size with operator new []. The constructors for all of these objects will be called at that time; note that the number of constructor calls will precisely match the data at hand, an improvement over the previous version of the array.

A destructor is now necessary as the logical and physical objects of the class are not the same. The destructor for the class is obligated to remove from memory that part of the logical StudentArrayV2 object which exists outside the physical object. In this particular situation, logical minus physical will consist of the arbitrary number of Student objects.

Put the implementation of the destructor for the class in the file StudentArrayV2.cpp.

Test this second version of an array with an extension to function main in file main.cpp.

    // version 2
    cout << "Enter the number of students for version 2: ";
    int number_of_students;
    cin >> number_of_students;

    StudentArrayV2 cs2114( number_of_students );
    cout << "contents of cs2114:\n";
    cs2114.write( );


Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate your class implementation and testing code before continuing.


Automatic Array of Pointers to Objects

In the preceding versions of the array, constructor calls for all of the objects in the array occur in a cluster since memory for all Student objects is allocated at once. This behavior is modified by yet another version for the array of Students.

class StudentArrayV3 {  // version 3: automatic array of object pointers
public:
    StudentArrayV3( );
    ~StudentArrayV3( );
    void add( );
    void write( );

    const static int max_array_size = 1000;

private:
    Student* members[max_array_size];
    int      number_of_students = 0;
};

When memory is set aside for an object of this type, no constructor calls are made for Student objects as no such objects are part of the class definition; the private data array members does however contain pointers to such objects. Because of this, the size of the array can be made arbitrarily large (1000) and let the number of meaningful pointers in the object be maintained by the counter number_of_students.

Consider the definition for a constructor of the class. It is obligated to initialize the private data of a StudentArrayV3 object with meaningful data. Put the implementation for the constructor of the class in the file StudentArrayV3.cpp. Explain the values given to the private data (one integer and one thousand pointers) in comments in the code. Do all of the pointers in the members array need to be initialized to meet the obligation of a constructor?

In this version, we need to create an add method; this was not necessary in the previous version of the array class since constructors were called for each Student in the array as soon as memory was allocated for the array. In other words, all adding took place at the time of array construction. Arrange for add to put the address of a new Student in the next available spot in the members array; be sure to update the number of students now stored in the array. Put the add implementation in file StudentArrayV3.cpp.

A destructor is again necessary (as the logical and physical objects of the class are not the same). In this particular situation, logical minus physical may consist of up to one thousand Student objects (as pointed to by the members array pointers). How will the destructor know how many there are and where the pointers to them are stored? Put the implementation of the destructor for the class in the file StudentArrayV3.cpp.

There is a significant difference in the write method from those of the first two versions of the array. Methods StudentArrayV1::write and StudentArrayV2::write simply invoke Student::write for each element of the members array. In this version, the members array contains pointers to Student objects, not Student objects themselves. The first of these pointers is members[0]. The dereference operator * can be used to follow the arrow from this pointer to the object; *members[0] is then the first Student object. The selector operator . (the period character) is used to invoke the Student::write method for the object located by the pointer. Since the selector operator has precedence higher than that of the dereference operator, parentheses must be used; thus (*members[0]).write() will perform the desired task. This rather clumsy notation may alternatively be expressed with the -> operator as members[0]->write(); both mean exactly the same thing in C++. Put the write implementation in file StudentArrayV3.cpp.

Test this version of the array class with the following addition to function main.

    // version 3
    StudentArrayV3 cs2124;
    cout << "Enter Student data for version 3 (y/n)? ";
    char option;
    cin >> option;
    while ( option == 'y' || option == 'Y' ) {
        cs2124.add( );
        cout << "Enter more student data (y/n)? ";
        cin >> option;
    }
    cout << "contents of cs2124:\n";
    cs2124.write( );


Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate your class implementation and testing code before continuing.


Dynamic Array of Pointers to Objects

If dynamic memory can be used to allocate Student objects as needed, why not use it to allocate the array of Student pointers to be of an appropriate size as well? This would make more efficient use of storage as 1000 may be excessively large; further, an unexpected class size of more than 1000 might be encountered. This can be accomplished with the following class definition.

class StudentArrayV4 {  // version 4: dynamic array of object pointers
public:
    StudentArrayV4( );
    ~StudentArrayV4( );
    void add( );
    void write( );

private:
    Student** members            = nullptr;
    int       number_of_students = 0;
    int       physicalArraySize  = 0;
};

A constructor for this class can allocate a small array for the Student pointers and save the number as physicalArraySize; the value for number_of_students is initially zero. Write this constructor now; let the initial physical array size be three.

Students may be dynamically allocated and added to the array until the physical size limit is reached. At that point, a new pointer array of larger size is allocated, the pointers from the existing array are copied into it, and the existing array is then removed from memory. The physical size limit is increased accordingly, allowing successive additions to be made without additional work until the new physical size is reached. Write method add so that new array allocations are larger by three each time.

The destructor for this class will have to carefully free the memory we allocated. Remember that each Student in the array has been dynamically-allocated, so they must be freed. Additionally, the array itself was dynamically-allocated, so it must be freed (but only after freeing all students that it points to).

Test this new class with an addition to function main analogous to that used for the third version of the class.


Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate your class implementation and testing code before submitting your project.



Labsubmitsinglefile.png FOR IN-LAB CREDIT: Zip up these files: Submit all source code files related to this project.
Name the file oop04iL.zip and upload to CSCADE.