• No results found

3 Representation in C++.

N/A
N/A
Protected

Academic year: 2021

Share "3 Representation in C++."

Copied!
12
0
0

Loading.... (view fulltext now)

Full text

(1)

3

Representation in C++.

The computer language C++ is well suited for implementing a grid generator. C++ is a superset of C. The discussion below assumes knowledge of the language C.

One major difference between C++ and C is that C++ has a data type called a class. A class is a generalization of the struct in C, or the record in Pascal. In a class we can gather together a number of variables. We have also the possibility to let functions be a part of a class. Furthermore we can chose to make certain variables and functions inaccessible for all code outside the class itself. The main idea, is that a class should be something encapsulated well isolated from the rest of the code. The class can only be used through well defined interfaces. It is often difficult to design good classes, however for boundary curves it is fairly easy to get a clean representation with classes.

3.1

A first example of a class

Below we show an example of a class which implements the boundary curve

y =a0x+a1x, axb (3.14)

where x is used as curve parameter.

class bcurve

{

private: double a, b; double a0, a1; int dir; public:

bcurve( double a in, double b in )

{ a0 = 0.3033; a1 = 0.67; a = a in; b = b in; dir = 0; } double x( double p ) { return p; } double y( double p ) {

return a0*sqrt(p) + a1*p;

} void reverse() { dir = !dir; } };

A class declaration has the structure

class name

{

// declarations

(2)

The word class is a reserved word, the name of the class is an identifier chosen by the programmer.

The class bcurve contains now the variables a,b, the end points of the curve param-eter, a0, a1 describing the parameter functions, and dir which is the direction of the curve. The value 0 of dir means thatp goes froma to b, a value of 1 means from b toa.

bcurve also contains the functions bcurve, x,y, andreverse.

The reserved word private means that these variables can only be accessed from functions inside the class. Under public we declare the variables and functions which any user of the class will be able to access directly. This constitutes the interface to the class. A class should be designed such that specific details that we may want to change later is kept under private. Changes become in that case local only to the class, and the rest of the code using the class can remain unchanged.

Another reason to use private, is to prevent users of the class to change variables by mistake. For example, the variable dir can not be accessed directly, but can only be changed in a very controlled way through the function reverse.

The first function under public is a function with the same name as the class, and with no return type. This is an initialization function, called a constructor, which is called automatically each time a variable of the class type is created. The intended use of the constructor is to give values to all variables in the class, so that we do not risk, by mistake, to use uninitialized variables. The other two public functions are the parameterization (x(p), y(p)).

In a main program, the class is used as in the example below, where 10 points on the curve are generated.

// The class declaration here. int main()

{

bcurve bc(0,1.0);

double x[10], y[10]; for( int i=0; i<10; i++ )

{

x[i] = bc.x( i/9.0 ); y[i] = bc.y( i/9.0 );

}

...

};

A variable of class type, such as bc in the example above, is called an object. When the program is executed, the first declarationbcurve bc(0,1.0); will immediately cause the constructor of the bcurve class to be called with the parameter valuesa in=0, b in=1.0. Note the dot notation bc.x(0.0) to access a member function of a class. Variables are accessed in the same way, e.g., bc.a denotes the left endpoint of the curve. However, in this example, trying to access a in the main program will lead to a compilation error, because a is declared as private.

Normally functions are not defined inside the class definition as in the example above. C++ allows us to define the functions outside the class, and only declare them inside the class. The same example can in that case be written

class bcurve

{

private: double a, b; double a0, a1;

(3)

int dir; public:

bcurve( double a in, double b in ); double x(double p );

double y(double p ); void reverse();

};

bcurve::bcurve( double a in, double b in )

{ a0 = 0.3033; a1 = 0.67; a = a in; b = b in; dir = 0; }

double bcurve::x( double p )

{

return p;

}

double bcurve::y( double p )

{

return a0*sqrt(p) + a1*p;

}

void bcurve::reverse()

{

dir = !dir;

}

When the function definitions are lifted out of the class, they must be preceded by

classname::, so that the compiler knows where the functions belong. It is possible that two different classes contain functions with the same name. The function definitions can now be put anywhere in the program. Often they are written on a separate file.

Every part of the code which will use the class bcurve must include the class declaration at the top of its file. Therefore one usually puts the class declaration in a special file called in this case bcurve.h which we give below.

class bcurve

{

private: double a, b; double a0, a1; int dir; public:

bcurve( double a in, double b in ); double x(double p );

double y(double p ); void reverse();

};

The file bcurve.h

The function definitions are put on a separate file,bcurve.C, which is compiled separately once and for all. We give bcurve.Cbelow.

#include "bcurve.h"

bcurve::bcurve( double a in, double b in )

{ a0 = 0.3033; a1 = 0.67; a = a in; b = b in; dir = 0; }

(4)

double bcurve::x( double p )

{

return p;

}

double bcurve::y( double p )

{

return a0*sqrt(p) + a1*p;

}

void bcurve::reverse()

{

dir = !dir;

}

The file bcurve.C

In the main program where we use the class bcurve, we include bcurve.h as shown below

#include "bcurve.h" int main()

{

bcurve lower boundary(0,0.5); ....

}

The advantage with this, is that the functions inside the class bcurve are compiled once. We can compile the main program without recompiling the functions in bcurve. Had they been written inside the class, as in the first example in this chapter, the function definitions would have been present in the include-file, and therefore recompiled every time the main program was recompiled.

When several files include each other in a non-trivial way, it can easily happen that a file likebcurve.hbecomes included more than once. To avoid this, it is common practise to put extra preprocessor directives in bcurve.h, as follows

#ifndef BCURVE #define BCURVE class bcurve { private: double a, b; double a0, a1; int dir; public:

bcurve( double a in, double b in ); double x(double p );

double y(double p ); void reverse();

}; #endif

The file bcurve.h, with preprocessor directives

This will ensure that bcurve.h is included only once.

One way to improve execution speeds is to inline small functions which are called often. Inlining means that the compiler does not generate calls to the function, but instead it inserts the entire source code of the function at each place where it is called. In C++, we tell the compiler to inline a function, by using the keyword inline in front of the function.

inline double x( double p )

{

return p;

(5)

Furthermore, all functions which are defined inside the class declarations becomesinline

by default. Inlined functions are similar to macros, but gives less risk for unwanted side effects. Inlined functions must be defined in the same file as it is being called. Separate compiling of inline functions is not allowed in C++. Inline functions are therefore usually defined in the include file, such as shown in the class declaration bcurve.h below.

#ifndef BCURVE #define BCURVE class bcurve { private: double a, b; double a0, a1; int dir; public:

bcurve( double a in, double b in ); inline double x(double p )

{

return p;

}

inline double y(double p )

{

return a0*sqrt(p) + a1*p;

}

void reverse();

}; #endif

We next improve the class above. Assume that we want to reparameterize, using the arc length normalized to [0,1]. We would then define the class as

class bcurve

{

private: double a, b; double a0, a1; int dir; double length;

double xo( double p ); double yo( double p ); public:

bcurve( double a in, double b in ); double x( double s );

double y( double s );

};

The private functions xo,yo describes the curve in original parameterization, these are private because the user of the class should not have to worry about the exact description of the parameterization. Instead the curve should be accessed from the outside through the public functions x,y, in which the arc length is used as parameter. In this way, the curve can be implemented with any equivalent parameterization in xo,yo, the user will not note the difference in the functions x,y. Assume that we put the class declaration above on the file bcurve.h. The member functions can then be defined as below.

#include "bcurve.h"

double bcurve::xo( double p )

{

return p;

}

double bcurve::yo( double p )

{

return a0*sqrt(p) + a1*p;

}

bcurve::bcurve( double a in, double b in )

(6)

a0 = 0.3033; a1 = 0.67; a = a in; b = b in; dir = 0;

length = // numerical approximation of b

a

xo(p)2+yo(p)2dp

}

double bcurve::x( double s )

{ // solve s= 1/(length)p a xo(t)2+yo(t)2dt for p return xo(p); }

double bcurve::y( double s )

{ // solve s= 1/(length)p a xo(t)2+yo(t)2dt for p return yo(p); }

The main program given previously can still be used unchanged with this modified class. Assume that we want to use the parametert instead of x, where the curve now is defined by

x=t2

y =a0t+a1t2

This curve has the same graph as the curve (3.14), but a different parameterization (and different parameter values at the end points). We could then make this change in the functions xo,yo, but the main program (or any other function using the class) can remain unchanged.

Remark: An efficient algorithm to solve the arc length equation

s = 1 L p a x(p)2 +y(p)2dp is Newton’s method, p0 = guess pk+1 =pk− pk a x(p)2+y(p)2dp−sL x(pk)2+y(pk)2 k = 0,1,2, . . .

Where the iteration proceeds until the difference |pk+1 −pk| is less than some given

tolerance. The integral is evaluated by, e.g., Simpsons method, where the interval [a, pk]

is divided into a sufficiently large number of subintervals. It is possible to implement the integral evaluation such that only the update pk+1

pk .. dp needs to be computed in each

iteration. However, I have not investigated whether this is advantageous or not.

3.2

Dynamic allocation of objects

The declaration

bcurve bc(0,1.0);

allocates the object bc in static memory. It is common to use dynamic allocation of objects instead. In order to do that, we first declare a pointer to the object

(7)

bcurve *bc;

The memory allocation is done by the statement

bc = new bcurve(0,1.0);

The parameters (0,1.0) are passed to the constructor, which is called automatically when the object is allocated. To access a member function of the object, we can use the expression

x[i] = (*bc).x(i/9.0);

The parenthesis are necessary, because the member selection operator, ., has higher priority than the dereference operator, *. There is an alternative way to write the same expression, namely

x[i] = bc->x(i/9.0);

When an object is no longer needed, it can be deallocated by the delete operator,

delete bc;

Unused objects should be deallocated, since C++ does not have automatic garbage col-lection. Programs that break during execution due to memory leaks, are not uncommon with C and C++.

3.3

Base classes and virtual functions

Consider the domain in Fig. 2.1 below. It consists of four sides, one is a circular arc, two sides are straight lines, and one side is defined by spline points.

−2 −1.5 −1 −0.5 0 0.5 1 1.5 2 −2 −1.5 −1 −0.5 0 0.5 1 1.5 2

Fig. 2.1 Example of boundary curves.

We could think of introducing three C++ classes to describe the boundary. The classes

circle do describe the arc, line to describe the straight line sides, and spline curve

to describe the outer boundary. We could then write the class domain to describe the entire domain. It contains the four boundaries as members, and the class could have the capability to generate a grid on the domain.

class domain

{

private:

circle *side1; spline curve *side2; line *side3, *side4; double *x, *y; ...

(8)

public: ...

void generate grid( int n, int m ); ....

};

The members x, yare matrices which will contain the grid.

The problem with this approach is that it lacks generality. Tomorrow, we might want to generate a grid in a different domain. We would then have to rewrite the domain class, in order to fit in other types of boundary curves.

We would like to have a way to write a general domain class in which we can declare the boundary curves as

bcurve *sides[4];

and where the domain class does not need to care about which type of curve each side is. This can be accomplished by using inheritance, which we now describe by a simple example. Consider the class below, describing a polygon.

#include <iostream.h> #include <math.h> struct point { double x; double y; }; class polygon { private: int npts; point *vertices;

double distance( point, point ); public:

polygon( int n, point *vert );

void translate( double dx, double dy ); void draw( Widget w, GC& gc );

void rotate( double alpha ); double perimeter();

};

We have introduced a data type point, describing a point in the plane. In C++ astruct

is the same thing as class, except for the difference that all variables are public by default in a struct. In a class the default is to make a member private. For example, some of the member functions are defined as

polygon::polygon( int n, point *vert )

{

npts = n;

vertices = new point[npts]; for( int i=0 ; i<npts ; i++ )

vertices[i] = vert[i];

}

double polygon::perimeter( )

{

double p=0;

for( int i=0 ; i<npts-1 ; i++ )

p += distance( vertices[i], vertices[i+1] ); p += distance( vertices[npts-1], vertices[0] ); return p;

}

(9)

Assume that we use this class to represent a square. The square has additional prop-erties that makes it a very special type of polygon. For example, the perimeter of a square can be computed more efficiently than in the general case above, by just taking 4 times the side. We want to exploit the special properties of the square. We do that by introducing the class square as aderived classfrom the class polygon. The declaration of square is given below.

class square : public polygon

{

double side; public:

square( point vert[4] ); double perimeter();

};

square::square( point vert[4] ) : polygon( 4, vert )

{

side = distance( vert[0], vert[1] );

}

double square::perimeter()

{

return side*4;

}

The first line class square : public polygon means that the square class contains the entire polygon class. We say that square inherits polygon. The public means that all the public members in polygon are also public in square.

The constructor of square takes the four corner points as a parameter. Before the body of square::square is executed, the constructor of the base class polygon is called. This is the meaning of the notation : polygon(4,vert).

Note how we have defined a new version of the function perimeterin square. In order for everything to work as intended we have to make a few small changes in the base class. We change the base class to

class polygon

{

protected: int npts; point *vertices;

double distance( point, point ); public:

polygon( int n, point *vert );

void translate( double dx, double dy ); void draw( Widget w, GC& gc );

void rotate( double alpha ); virtual double perimeter();

};

We have changed private to protected. The derived class, square, can not access the private variables in the base class. Protected is used to allow derived classes to access the variables. In other respects the variables under protected are inaccessible from the outside.

The second change is that we have added the reserved word virtual to the function perimeter. The meaning of virtual is to tell the compiler that this is a function which can be redefined by a derived class.

In the main program we can now use the classes as usual,

point verts[5], sqverts[4]; ...

(10)

square *s = new square( sqverts );

What is more important is that we can assign squares to polygons,

point verts[5], sqverts[4]; ...

polygon *p = new polygon( 5, verts ); polygon *s = new square( sqverts ); // call to polygon::perimeter

cout << "perimeter of polygon is " << p->perimeter() << endl; // call to square::perimeter

cout << "perimeter of square is " << s->perimeter() << endl;

If we had not declared perimeter to be a virtual function, the call s->perimeter above would have been a call to polygon::perimeter().

Although *s is of type polygon, it can be assigned a square. This is called

polymor-phism, and is a powerful technique, which is often used to avoid alternative statements

like

switch( type )

{

case ’s’ : per = perimeter square(); break; case ’r’ : per = perimeter rectangle();break; case ’c’ : per = perimeter circle();break; default: per = perimeter general();

}

Codes which rely on a special type variable to choose between cases, like in the example above, are difficult to maintain and modify. With a good hierarchy of inheriting classes such switch statements can be replaced by a single call to a virtual function.

The assignment

square s; polygon p; p = s;

is thus allowed, but the opposite assignment s=pis forbidden. This is not strange, since it is unlikely that a polygon is a square. However, a square is always a polygon.

3.4

Abstract base classes

In the previous example the functionpolygon::perimeter() was replaced by a function with the same name in the derived classsquare. It is often advantageous to instead define a base class with unspecified virtual functions, a so called abstract base class. Consider the example class shape { private: points* vertices; int npts; ... public: shape( );

virtual void rotate( double alpha ) = 0;

virtual void translate( double dx, double dy ) = 0; virtual double perimeter() = 0;

...

};

By using the notation=0 after the function, we have here declared the functionsrotate, translate, and perimeter to be purely virtual functions. This means that the the

(11)

functions are not defined in shape, but that they will be specified in classes which are derived from shape.

No objects of the abstract base class can be created. The declaration of an object

shape sh;

is wrong, since the purely virtual functions are not defined in shape. An abstract class can be used only as a base for another class.

An abstract class can be used to create an interface to the class without giving any information about implementation.

3.5

Programming exercise: A base class for boundary curves

We would now like to use the techniques described above to redesign the bcurve and

domainclasses described in Section 3.3. First we define an abstract base class for boundary curves. It should contain at least the following information

class curvebase

{

protected:

double pmin, pmax; // Max and minimum values of curve parameter int rev; // pmin to pmax or vice versa

virtual double xp( double p ) = 0; // x(p) virtual double yp( double p ) = 0; // y(p) virtual double dxp( double p ) = 0; // x’(p) virtual double dyp( double p ) = 0; // y’(p)

double integrate( double a, double b ); // arc length integral ....

public:

curvebase() ; // constructor

double x( double s ); // arc length parameterization double y( double s ); // arc length parameterization ....

};

Exercise 1: Complete the class by writing the non-virtual functions. Add more variables or functions to the class, if you find it necessary.

Exercise 2: You will generate a grid on the domain in Fig. 2.2 below.

−3 −2 −1 0 1 2 3 −2 −1 0 1 2 3 4

Fig. 2.2 Computational domain.

The corners are located at (-3,1), (3,0), (3,3), and (-3,3). The lower boundary is given by the function y = 1/(e5x + 1). (The lower corners do not match perfectly, but for all

practical purposes we can assume that 1/(e15+ 1) = 0 and 1/(e15+ 1) = 1.) Derive classes that are needed to represent the boundary curves of the domain in Fig. 2.2 from the base class. Test the class by using it in a simple main program.

(12)

Exercise 3: Design a class domain as outlined in Section 3.3. The class should contain four boundary curves of type curvebase, and have capability for generating a grid on the domain. Write a main program which generates the grid. Use the algebraic grid generation formula (2.2).

Exercise 4: Add a function to the class domain to write the grid to a file. The simplest is to use cout to write an ascii file. A better way is to use the unix functions open, write, and close to output the grid in binary format. We give an example below of how they are used to write a vector x consisting of n×m doubles.

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> ...

int fd = open( "outfile.bin", O CREAT | O RDWR | O TRUNC, 0660 ); int nr = write( fd, x, n*m*sizeof(double) );

close(fd);

Use the unix commands man open, and man -s 2 write to obtain more information about these functions. The grid can be viewed in Matlab. To read a binary file, use the Matlab functions fopen and fread.

Exercise 5: This exercise is not compulsory, just do it if you have time, and would like to make a better program. The four boundary curves are somehow input to the class

domain, e.g., through a constructor. You have probably up to now used some convention of type “the first argument to the constructor should be the lower boundary” etc. Add a function order boundaries to the class domain. This function should order a random set of four boundaries such thatbcurve[0] and bcurve[1]are always facing each other, and have consistent coordinate directions, and the same for bcurve[2] and bcurve[3]. When this function is used, the boundary curves can be input to domain in any order, with any direction of the parameter.

References

Related documents

Head elements are designed to communicate information about your web page, but are not directly displayed in the browser window. These elements, which are used with the meta tag,

› With private inheritance, public and protected members of the base class become private members of the derived class. › Private members are still private to the

The acquisition by one class (called the derived class or child class or direct subclass ) of the variables and methods of another class (called the base class or parent class or

– In base-class declare draw to be virtual – Override draw in each of the derived classes – virtual declaration:.. • Keyword virtual before function prototype in base-class

class Node: base class that describes expression tree vertices:. class converting Int Node: used for implicitly int to

 Private methods of the base class are not accessible to a derived class (unless the derived class is a friend of the base class).  If the subclass is derived

 Private methods of the base class are not accessible to a derived class (unless the derived class is a friend of the base class).  If the subclass is derived

class, only the public and protected members of base class can be accessed by the member functions of derived class. This means no private member of the base class