Exercise index of this lecture   Alphabetic index   Course home   

Exercises and solutions
Abstraction Mechanisms, Part 1


3.1   Methods in class Point and class Rectangle  

In this exercise we will add methods to class Point and Rectangle from the enclosing slide.

First, however, we notice that a rectangle contains its two points. In a similar Java or C# program (programmed with classes) the rectangle would have references (in the meaning of C++ pointers) to two points.

Add a couple of methods that access the x and y coordinates of a point. This is very easy. Do you miss C#-like properties in C++?

Next, write two methods that access the two corner points of a rectangle. Do you want to return copies of the points? Pointers to the points? Or references to the points? Please play with each of these possiblities, and discuss pros and cons.

Add an area method to Rectangle. This method will most likely need to access to the x and y coordinates of the corner points of the rectangle.

Add move methods to both class Point and Rectangle. The methods should implement relative displacement of points and rectangles.

In Rectangle::move, are you able to establish (C++) references to its two corner points, such that moving the two points also will also move the rectangle?

If you get too frustrated by dealing with Point& you may consider a version that works on Point*...

Finally, supply output functions for Point and Rectangle objects, by overloading operator<< for these two classes.

Solution

Here follows my programw with the area and move methods. These program relies on methods that establish references to corner points from rectangle.

point.h:

class Point {
private: 
  double x, y;

public:
  Point();                    
  Point(double x);            
  Point(double x, double y);  

  double getx () const;
  double gety () const;
  void move(double dx, double dy) ;
};

std::ostream& operator<<(std::ostream&, const Point&);

point.cc:

#include <cmath>
#include <iostream>
#include "point.h"


Point::Point(): x(0.0), y(0.0){
}

Point::Point(double x_coord): x(x_coord), y(0.0){
}

Point::Point(double x_coord, double y_coord): x(x_coord), y(y_coord){
}

double Point::getx () const{
  return x;
}

double Point::gety () const{
  return y;
}

void Point::move(double dx, double dy){
  x += dx; y += dy;
}

std::ostream& operator<<(std::ostream& s, const Point& p){
  return s << "(" << p.getx() << "," << p.gety() << ")" ;
}

rect.h:

#include "point.h"

class Rectangle{
private:
  Point upper_left, lower_right;

public:
  Rectangle();
  Rectangle(Point p1, Point p2);
  Rectangle(double x1, double y1, double x2, double y2);
 
  void move(double dx, double dy);
  double area() const;                        
  Point& get_upper_left_point() const;   // Returns Point& in this version!
  Point& get_lower_right_point() const;
};

std::ostream& operator<<(std::ostream&, const Rectangle&);

  

rect.cc:

#include <cmath>
#include <iostream>
#include "rect.h"

Rectangle::Rectangle(): upper_left(-1,1), lower_right(1,-1){
}

Rectangle::Rectangle(Point p1, Point p2): upper_left(p1), lower_right(p2){
}

Rectangle::Rectangle(double x1, double y1, double x2, double y2):
      upper_left(x1,y1), lower_right(x2,y2){
}

void Rectangle::move(double dx, double dy){
  upper_left.move(dx, dy);
  lower_right.move(dx, dy);
}

Point& Rectangle::get_upper_left_point() const{
  return const_cast<Point&>(upper_left);            // const cast seems necessary here. Why?
}

Point& Rectangle::get_lower_right_point() const{
  return const_cast<Point&>(lower_right);           // const cast seems necessary here. Why?
}

double Rectangle::area() const{
  return (lower_right.getx() - upper_left.getx()) * (upper_left.gety()- lower_right.gety());
}

std::ostream& operator<<(std::ostream& s, const Rectangle& r){
 return s << "[" << r.get_upper_left_point() << "," << r.get_lower_right_point() << "]" ;
}


Sample client program:

#include <iostream>
#include "rect.h"

using namespace std;

int main(){
  Point p1(1,2), p2(5,-3);
  Rectangle r1,                             // Use default constructor
            r2(1,2,5,-3),                   
            r3(Point(1,2), Point(5,-3));

  cout << "r1: " << r1 << " Area: " << r1.area() << endl;
  cout << "r2: " << r2 << " Area: " << r2.area() << endl;
  cout << "r3: " << r3 << " Area: " << r3.area() << endl;


  // OK:
  cout << "r3 before move: " << r3 << endl;
  r3.move(1,2);
  cout << "r3 after move: " << r3 << endl;

  // Does not work:
  cout << "r2 before move: " << r2 << endl;
  Point c1 = r2.get_upper_left_point(),     // Copies of corners.
        c2 = r2.get_lower_right_point();

  c1.move(1,1); c2.move(1,2);  // Does not work. Move copies of points
  cout << "r2 after move: " << r2 << endl;

  // Works:
  cout << "r2 before move: " << r2 << endl; 
  r2.get_upper_left_point().move(1,1); r2.get_lower_right_point().move(1,2);    
  cout << "r2 after move: " << r2 << endl;
}


3.2   Use of constructors with object on the free store  

Rewrite the program shown above such that all Point objects are constructed in the free store (on the heap). The dynamically allocated points should have the same (x,y) coordinates as the points in the program shown above.

This involves use of pointer and dynamic allocation, instead of automatic variables of class type and 'static allocation'.

The version of the program produced in this exercise is - in some respect - similar to a Java or C# version of the program. Discuss!

Consider how to initialize the array ap, which in this version of the program should be an array of Point pointers.

Solution

Here is my version of the program:

#include <iostream>
#include "point.h"

using namespace std;

void f(){

  Point *p = new Point(),
        *q = new Point(),
        *r = new Point(11.0, 12.0),
        *s = new Point(*p),                      
        *t = new Point(3.0),           
        *u = new Point(*t),
        *ap[10]; 

  for(int i = 0; i < 10; i++)
     ap[i] = new Point();

  *q = *r;

  cout << "Point p: " << *p << endl;   // (0,7)
  cout << "Point q: " << *q << endl;   // (11,12)
  cout << "Point r: " << *r << endl;   // (11,12)
  cout << "Point s: " << *s << endl;   // (1,9)
  cout << "Point t: " << *t << endl;   // (3,20)
  cout << "Point u: " << *u << endl;   // (4,22)

  for(int i = 0; i < 10; i++)           // (0,7) ten times
      cout << "ap[" << i << 
     "]: " << *(ap[i]) << endl;  

  delete p; delete q; delete r; delete s; delete t; delete u;
  for(int i = 0; i < 10; i++) delete ap[i];

}

int main(){
  f();
}

 


3.3   Point destruction - now with a problematic point copy constructor  

We continue our work with class Point, using a dynamically allocated point representation. It may be beneficial to review the two version of the classes (together with client programs) on the accompanying slide.

In the verison below we add a copy constructor to class Point. This constructor turns out to be problematic.

// Redoing the example - same header file as the previous version.

class Point {
private: 
  double *point_representation;

public:
  Point();                 
  Point(double, double);    
  Point(Point&);           // A new copy constructor
  ~Point();                // Destructor

  double getx () const;
  double gety () const;
  void move(double dx, double dy);
};

std::ostream& operator<<(std::ostream&, const Point&);

The class is implemented as follows:

#include <cmath>
#include <iostream>
#include "point.h"


Point::Point(){
  point_representation = new double[2];
  point_representation[0] = 0.0;
  point_representation[1] = 0.0;
}

Point::Point(double x_coord, double y_coord){
  point_representation = new double[2];
  point_representation[0] = x_coord;
  point_representation[1] = y_coord;
}

Point::Point(Point& p): point_representation(p.point_representation){   // ATTEMPTING copy construction.
}

Point::~Point(){
  std::cout << "Deleting point" << "(" << getx() << "," << gety() << ")" << std::endl;
  delete[] point_representation;
}


double Point::getx () const{
  return point_representation[0];
}

double Point::gety () const{
  return point_representation[1];
}

void Point::move(double dx, double dy){
  point_representation[0] += dx;
  point_representation[1] += dy;
}

std::ostream& operator<<(std::ostream& s, const Point& p){
  return s << "(" << p.getx() << "," << p.gety() << ")" ;
}

There are problems in the following program: (1) When point r is moved, s is also moved. This is not as intended. In addition, the program aborts on exit from the function f.

#include <iostream>
#include "point.h"

using namespace std;

int f(){

  Point p, q,                         
        r(11.0, 12.0);

  cout << "Point p: "      << p << endl;         // (0,0)
  cout << "Point q: "      << q << endl;         // (0,0)
  cout << "Point r: "      << r << endl;         // (11,12)

  Point s(r);                                    // s is intended to be a copy of r.
  cout << "Point s: "      << s << endl << endl; // (11,12)

  r.move(1.0, 2.0);                              //  Moves r.

  cout << "Point s: "      << s << endl;         // (12,14)   !! s is ALSO MOVED - why?
  cout << "Point r: "      << r << endl << endl; // (12,14)

  // PROGRAM ABORTS ON EXIT FROM F - why?
}

int main(){
  f();
}

Explain both of the problems, and make a correct version of class Point.

Solution

Here is an improved version of the implementation of class Point. Notice the improved copy constructor. In this version, it copies the point representation.

#include <cmath>
#include <iostream>
#include "point.h"


Point::Point(){
  point_representation = new double[2];
  point_representation[0] = 0.0;
  point_representation[1] = 0.0;
}

Point::Point(double x_coord, double y_coord){
  point_representation = new double[2];
  point_representation[0] = x_coord;
  point_representation[1] = y_coord;
}

Point::Point(Point& p){   // Better copy constructor
  point_representation = new double[2];
  point_representation[0] = p.getx();
  point_representation[1] = p.gety();
}

Point::~Point(){
  std::cout << "Deleting point" << "(" << getx() << "," << gety() << ")" << std::endl;
  delete[] point_representation;
}


double Point::getx () const{
  return point_representation[0];
}

double Point::gety () const{
  return point_representation[1];
}

void Point::move(double dx, double dy){
  point_representation[0] += dx;
  point_representation[1] += dy;
}

std::ostream& operator<<(std::ostream& s, const Point& p){
  return s << "(" << p.getx() << "," << p.gety() << ")" ;
}


3.4   Conversion via constructors  

In the program shown above, a Point can be converted to a Tripple via a constructor Tripple(Point). The other way around, we convert a Tripple to a Point with use of a conversion operator in class Tripple

In this exercise we want to provide for a more symmetric solution. We keep the constructor Tripple(Point). Your task is to program the symmetric constructor Point(Tripple). Your starting point is the header file and the cpp file

You may enconter a couple of minor challenges when you rewrite the program. First, class Tripple must now be declared (it must be known by the compiler) in order to compile class Point. The reason is that class Point now uses the name Tripple (in the new constructor).

As another challenge, the new constructor Point(Tripple) in class Point needs access to the three encapsulated, private data members in class Tripple. Consider setting up appropriate friendship, or program accessor member functions.

With this in place, rerun the program from above. Do you get the expected results (without surprises)?

Solution

Here are the revised program parts. First the header file:

// New example: Converting between Point an Tripple...

class Tripple;                 // NECESSARY FORWARD DECLARATION OF CLASS Tripple 

class Point {
private: 
  double x, y;

public:

  Point();                      // (0,7)
  Point(double d);              // constructor from double:  (d, 20)
  Point(double d, double e);    // (d, e)
  Point(Point& p);              // (p.x+1, p.y+2) 
  Point(Tripple t);             // (t.a, t.b)   NEW, INSTEAD OF CONVERSION IN Tripple.

  operator double() const;      // conversion operator to double: x+y

  double getx () const;
  double gety () const;
};

class Tripple {
private:
  int a, b, c;

public:
  friend class Point;           // GIVES POINT ACCESS TO PRIVATE a, b, and c.
                                // This is convenient in the new constructor Point::Point(Tripple) 
  Tripple();                    // (0, 0, 0)
  Tripple(int x, int y, int z); // (x, y, z)
  Tripple(Point p);             // (p.x, p.y, 0)

  friend std::ostream& operator<<(std::ostream&, const Tripple&);
};


std::ostream& operator<<(std::ostream&, const Point&);
std::ostream& operator<<(std::ostream&, const Tripple&);

The cpp file:

#include <cmath>
#include <iostream>
#include "point1.h"

// Tripple:

Tripple::Tripple(): a(0), b(0), c(0){
}

Tripple::Tripple(int x, int y, int z): a(x), b(y), c(z){
}

Tripple::Tripple(Point p): a(int(p.getx())), b(int(p.gety())), c(0){
}

std::ostream& operator<<(std::ostream& s, const Tripple& tr){
  return s << "[" << tr.a << "," << tr.b << "," << tr.c << "]";
}



// Point:

Point::Point(): x(0.0), y(7.0){
}

Point::Point(double x_coord): x(x_coord), y(20.0){
}

Point::Point(double x_coord, double y_coord): x(x_coord), y(y_coord){
}

Point::Point(Point& p): x(p.x + 1.0), y(p.y + 2.0){
}

Point::Point(Tripple t): x(t.a), y (t.b){     // NEW CONSTRUCTOR
}                                             // Can access a and b in t because Point 
                                              // has granted friendship to Tripple.

double Point::getx () const{
  return x;
}

double Point::gety () const{
  return y;
}

Point::operator double() const {
  return x + y;
}

std::ostream& operator<<(std::ostream& s, const Point& p){
  return s << "(" << p.getx() << "," << p.gety() << ")" ;
}


The client program:

#include <iostream>
#include <string>
#include "point1.h"

using namespace std;

int main(){
  Point p1, p2;      // Both (0, 7)
  Tripple t1;        // (0, 0, 0)

  cout << "t1: " << t1 << endl; 

  t1 = p2;           

  p1 = t1;                                

  cout << "t1: " << t1 << endl; 
  cout << "p1: " << p1 << endl; 
}

When I run this program I get this output:

t1: [0,0,0]
t1: [1,9,0]
p1: (1,9)

As it appears, we get natural and expected results. Thus, it turns out to be easier to control the conversions in this variation of the program


3.5   Discuss encapsulation, visibility and access  

Discusss encapsulation, visibility and access, based on the enclosing slide.


Generated: Tuesday March 26, 2013, 13:03:33