Thursday, October 19, 2017

Lecture Notes 07

Object Oriented Programming: Lecture Notes 07

Author: A. El-Gadi/Faculty of Computer Engineering/Tripoli University

 

Arrays contd.


Arrays of objects can be allocated statically and used in the way familiar to C programmers. The default constructor is automatically used when creating the objects that constitute the elements of the array.

class AA{public: int d,e;
     int mul(){return d*e;}};

int main(){AA aaar[10];
    aaar[7].d=10;
    aaar[7].e=11;
    cout<<aaar[7].mul();//Output: 110
    }


Arrays can be allocated dynamically, as well. This is accomplished using the new keyword. Dynamically allocated arrays must, eventually, be explicitly deleted. The example below illustrate the syntax for accomplishing dynamic allocation. Only the default constructor can be used for the dynamic allocation of arrays.

class AA{public: int d,e;
     int mul(){return d*e;}};

int main(){AA *paa;
   
    paa=new AA[10];/*Dynamic allocation of an array of type AA of size 10*/
    paa[7].d=10;
    paa[7].e=11;
    cout<<paa[7].mul();//Output: 110
    delete [] paa;
    return 0;
    }


Since in the case of arrays, only default constructors are allowed, populating arrays is done in a later step. The code below uses a loop to populate a dynamically allocated array, and then displays the result on the console using another loop:

class AA{public: int d,e;
     int mul(){return d*e;}};

int main(){AA *paa;
   
    paa=new AA[10];
   
    int i;
    for(i=0;i<10;i++){cin>>paa[i].d; cin>>paa[i].e;}

    for(i=0;i<10;i++){cout<<paa[i].mul();}

    delete [] paa;
   
    return 0;
    }


Copy constructors


Copy constructors are used to create objects that are exact copies of other objects. A full treatment of the syntax involved in defining copy constructors can be somewhat complicated at this stage, but the basic idea is simple. A Copy constructor takes one object of same type of the class as a parameter, and copies each member variable of the passed object to the corresponding member variable in the object being created. As such, the prototype of the copy constructor for some class AAA should look like this AAA(AAA). In practice, however, the mentioned copy constructor's prototype looks like this AAA(const AAA&). The const keyword here tells the compiler that the object passed is constant and therefore should be immutable. The importance of the const keyword in this context becomes clear when we consider the role of the ampersand (&). The ampersand here is different from the ampersand used to get a pointer of an object or a variable, and is confusingly called reference in the context of C++. It is an addition to C++ that forces a function -in this case the copy constructor- to use the original object or variable in its original memory space rather than copy it to the function's memory space. This feature is especially important in OOP; because most of the time we find ourselves dealing with large objects, which are expensive to copy from one memory space to another. But allowing a function to work on the original object exposes it to modification. Therefore, the keyword const is prepended to it to prevent such a any modification from taking place. The copy constructor is supplied by the compiler and need not be overridden unless there is a plausible reason to do so. One such typical reason is having a deep class, whose pointer member(s) referent(s) need to be copied explicitly in the overridden copy constructors to avoid having a shared referent. The following code correctly uses the default copy constructor for a shallow class:

class AA{public: int d,e;};

int main(){AA aa1;
    aa1.d=33; aa1.e=99;
   
    AA aa2(aa1);

    cout<<aa2.d<<' '<<aa2.e;//Output: 33 99

    return 0;
    }



The diagram below illustrates a deep object aa1 of type AA after assigning values to members and member(s) of pb referent object. Note that the code below is defective since it does not override the copy constructor:

class BB{public:int x;}

class AA{public: int d,e;
     BB *pb;
     AA(){pb=new BB();}
     ~AA(){delete pb;}
    };

int main(){AA aa1;
    aa1.d=10; aa1.e=30;
    aa1.pd->x=70;
    return 0;
    }

aa1                    Obj1 BB@address 16000
+------------+    +--->+--------+
|            |    |    |        |
| d: 10      |    |    | x: 70  |
|            |    |    +--------+
| e: 30      |    |
|            |    |
|pb:16000+--------+
|            |
+------------+


Here is the situation we will end up with when using the default copy constructor for a deep class. It is evident that this situation leads to all kinds of problems because of the shared referent. Consider what would happen when aa1.pb->x is changed. Since we have the same referent, aa2.pb->x will be changed as well. Even worse, in the case of dynamically allocated objects, when the first object is deleted the other object will have a pointer pointing to a deallocated memory space.

class BB{public:int x;}

class AA{public: int d,e;
     BB *pb;
     AA(){pb=new BB();}
     ~AA(){delete pb;}
    };

int main(){AA aa1;
    aa1.d=10; aa1.e=30;
    aa1.pd->x=70;

    AA aa2(aa1);
    return 0;
    }

aa1                    Obj1 BB@address 16000
+------------+    +-->>+--------+
|            |    | |  |        |
| d: 10      |    | |  | x: 70  |
|            |    | |  +--------+
| e: 30      |    | |
|            |    | |
|pb:16000+--------+ |
|            |      |
+------------+      |
                    |
                    |
aa2                 |
+------------+      |
|            |      |
| d: 10      |      |
|            |      |
| e: 30      |      |
|            |      |
|pb:16000+----------+
|            |
+------------+


To solve the problem above, we need to override the copy constructor to perform a deep copy when we have deep classes. See the code and the diagram below:

class BB{public:int x;}

class AA{public: int d,e;
     BB *pb;
     AA(){pb=new BB();}
     AA(const AA& aa){d=aa.d; e=aa.e; pb=new BB(*aa.pb);}
     ~AA(){delete pb;}
    };

int main(){AA aa1;
    aa1.d=10; aa1.e=30;
    aa1.pd->x=70;

    AA aa2(aa1);
    return 0;
    }

aa1                    Obj1 BB@address 16000
+------------+    +--->+--------+
|            |    |    |        |
| d: 10      |    |    | x: 70  |
|            |    |    +--------+
| e: 30      |    |
|            |    |
|pd:16000+--------+
|            |
+------------+


aa2                    Obj2 BB@address 24000
+------------+    +--->+--------+
|            |    |    |        |
| d: 10      |    |    | x: 70  |
|            |    |    +--------+
| e: 30      |    |
|            |    |
|pd:24000+--------+
|            |
+------------+


Design consideration when a value is determined by other member variables


When deciding which member variables to include in a class, it is important to keep in mind the following guiding principle:
If some information about the object can be determined by other member variables this information should not be included as a member variable but rather it should be calculated from other member variables. For example, assume that we want to design a rectangle class, and among the information that we wish to be able to retrieve from our class is the area and the perimeter of the rectangle. In such a class, if we include the area and the perimeter as member variables our design would be flawed because the area and the perimeter can be calculated by knowledge of the side lengths of the rectangle. Here is the flawed class:

class Rectangle{public:
        double height;
        double width;
        double area;
        double perimeter;
        };


The reason that this design is flawed is because it is prone to inconsistent values of the height, length, area, and perimeter. We can have an object that has 5 for height, 10 for width, 70 for area and 40 for perimeter. Making area and perimeter functions that perform a calculation of the area and perimeter and return their values circumvents the aforementioned problem. The correct design should be as follows:

class Rectangle{public:
        double height;
        double width;
        double area(){return height*width;}
        double perimeter(){return 2*(height+width);}
        };


That is why when we designed a Triangle class and chose to determine a triangle by the lengths of the three sides, we did not include member variables for area and perimeter. Nor should we include member variables for the triangle's angles, since any angle can be calculated by knowledge of the side lengths of the triangle's sides which are included as member variables in the class.

No comments:

Post a Comment