struct [tag]
{
members;
}[variable];
Where tag is optional and only needs to be present if no variable
is present. The members are variables declared as any C
supported data type or composed data type.
A structure is a set of values that can be referenced
collectively through a variable name. The components of a
structure are referred to as members of a structure. A structure
differs from an array in that members can be of different data
types. A structure is defined by creating a template. A
structure template does not occupy any memory space and does not
have an address, it is simply a description of a new data type.
The name of the structure is the tag. The tag is optional when a
variable is present and the variable is optional when the tag is
present. Each member-declaration has the form
data-type member-declaration.
/* define template */
struct Person
{
char ssn[12]
,last_name[20]
,first_name[16]
,street[20]
,city[20]
,state[3]
,zip_code[11]
;
int age;
float height
,weight
;
double salary;
};
In the C language, a variable of the above data type would be
declared as follows:
main()
{
/* declare variable of structure person type */
struct Person frank;
...
}
In the C++ language, the use of the keyword struct would not be
required in the declaration of a variable of the above type.
main()
{
// declare variable of structure person type
Person frank;
...
}
Each member of a structure is in a contiguous memory location in
relationship to its previous member and next member. To access
individual members of a structure, the dot operator is used to
connect a variable name with an element in the structure. For
example
frank.last_name
The composed name is treated as a variable of with the type of
the member variable.
#include
int main()
{
struct Person ralph;
printf( "\nEnter your first name:" );
gets( ralph.first_name );
.
.
.
printf( "\nEnter your age:" );
scanf( "%d", &ralph.age );
return 0;
}
or in C++
Fig. 9-6
#include
int main()
{
Person ralph;
cout << "\nEnter your first name:";
cin >> ralph.first_name;
.
.
.
cout << "\nEnter your age:";
cin >> ralph.age;
return 0;
}
Person employ = {"Ralph",
"Jones",
"123 Main",
"Nowhere",
"NV",
"12345-9878"
30,
72,
165
};
employ.age = 35;
The . operator is used to provide the connection between variable
name and member name. When referencing a particular member of a
structure, the resulting type of that expression is that of the
member being referenced.
strcpy(employ.street,"3188 Trinity Drive");
The value of one structure variable can be assigned to another
structure variable of the same type.
Person employ, employ2;
employ = employ2;
The above assignment is a valid statement. The data stored in
employ2 would be stored in employ, because objects of like type
can be assigned to one another.
Structure variables cannot be compared through a relational
operator. The following example attempts to compare two
structure variables of the same type.
int main()
{
Person employee, staff;
employee = staff;
if( employee == staff )
...
The above code is invalid, each member of the structure must be
compared in order to establish equality or non-equality.
struct Vital
{
int age
,height
,weight
;
};
struct Home
{
char f_name[12]
,l_name[20]
,street[20]
,city[20]
,state[3]
,zip_code[11]
;
};
struct Person
{
Vital emp;
Home place;
} employ;
In order to access a member of one of the nested structures, the
dot operator is used until the lowest member is reached in the
structure hierarchy.
cout << "Enter your age: ";
cin >> employ.emp.age;
cout << "Enter your last name: ";
cin >> employ.place.lname;
struct Person
{
Vital emp;
Home place;
} stats[100];
The above declaration would allow for the storage of 100 items of
type people. Any specific item could be referenced as:
stats[indx].place.f_name
stats[indx].emp.age
Notice stats is the arrayed item, and requires the array
operator, [].
main()
{
void getData( Person ); // function prototype
Person employee;
getData(employee);
.
.
.
}
void getData( Person x )
{
cout << "Enter last name: ";
cin >> x.l_name;
.
.
.
}
Just like any other argument, a structure is passed by value;
therefore, functions may not make permanent changes to the values
of members of the structure.
A structure may be used as the return-type of a function.
Person getData( Person newemp )
{
...
return( newemp );
}
The calling routine can store the structure data returned into a
like typed structure variable.
int main()
{
void getData( Person );
Person newemp, employee;
newemp = getData(employee);
...
}
If the function being called is to modify or change the data
within the structure, the address of the structure variable can
be passed and the called function can work with the structure
members through a pointer. This method is referred to as call by
reference.
#include
main()
{
void getData( Person * );
Person employ;
getData( &employ );
}
void getData( Person *ptr )
{
cout << "Enter first name:";
cin >> ptr->place.f_name;
cout << "\nEnter last name:";
cin >> ptr->place.l_name;
.
.
.
cout << "\nEnter age:";
cin >> ptr->person.age;
}
Notice that the -> operator is used when referencing a pointer to
a structure. Functions can also return pointers to structures.
union [union_tag]
{
data_type variable_1;
data_type variable_2;
data_type variable_3;
.
.
.
} {union_variable_name};
As with a structure, a union_tag is optional if a
union_variable_name is present. The compiler allocates only the
storage space of the largest data_type declared.
union Data
{
char str[6];
int y;
long z;
float x;
double t;
} var;
The union variable var is given eight(8) bytes of storage; all
the elements start at the same address in memory; the compiler
gives the amount of storage needed by the largest data type, in
this case double which is eight bytes.
var.x = 123.45;
Unions passed to functions exactly as a structure would be
passed.
struct data-rec
{
char *last_name;
char *first_name;
int id_number;
union
{
float annual_salary;
float hourly_wage;
};
};
All members of a union require only as much memory storage as the
largest member. For the structure given above, storage for only
one floating point entity is required. The union members
annual_salary and hourly_wage are stored at the same memory
address. Each instance of a data_rec can only store one of these
members.
One can access a member of an anonymous union as in the
following:
data_rec my_record;
cout << "My annual salary = "
<< my_record.annual_salary
<< endl;
bit: 0 1 2 3 4 5 6 7
+----+----+----+----+----+----+----+----+
| | | | | | | | |
+----+----+----+----+----+----+----+----+
but a software engineer would describe the area of memory as
follows:
bit: 7 6 5 4 3 2 1 0
+----+----+----+----+----+----+----+----+
| | | | | | | | |
+----+----+----+----+----+----+----+----+
As the programmer can see, a reference to bit number 2 would mean
one thing to a hardware person and an entirely different thing to
a software person. Most specifications for hardware are produced
by hardware engineers and the software to use that hardware is
written by software people, so over the years an understanding
has been reached.
As an additional complication, the Intel family of processors
used in IBM type personal computers, uses a different arrangement
for words of memory than any other processor. With most
computers a word of memory has a most significant byte (MSB) and
a least significant byte (LSB). On most processors a word of
memory would look as follows:
| Byte 1 | Byte 0 |
|15 14 13 12 11 10 9 8 |7 6 5 4 3 2 1 0 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| MSB | LSB |
But with the Intel processor, which uses something called "Big-Endian" and "Little-Endian" notation, the above word of storage will look as follows:
| Byte 0 | Byte 1 |
|7 6 5 4 3 2 1 0 |7 6 5 4 3 2 1 0 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| | | | | | | | | | | | | | | | |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| LSB | MSB |
For an illustration, there is a word in memory on all MS-DOS
machines that is used to hold the status of keyboard toggles,
such as Caps Lock, Insert, and others. The following example
shows a union that is used to determine the status of those
keyboard toggles.
#include
// address of segment and offset of
// keyboard control bits, byte address
// 0017 and 0018 on IBM PC running MS-DOS
#define BASE_ADDR 0x00400017
// layout of 16 bits of memory
union KeyBits
{
unsigned keys; // everything together
// Pg. 223, Peter Norton's
// "Inside the IBM PC".
// Must reverse order of bits in each byte
struct Bits
{
// byte 0
unsigned right_shift :1; // bit 7
unsigned left_shift :1; // bit 6
unsigned ctrl_pressed :1; // bit 5
unsigned alt_pressed :1; // bit 4
unsigned scroll_lock :1; // bit 3
unsigned num_lock :1; // bit 2
unsigned caps_lock :1; // bit 1
unsigned ins :1; // bit 0
// byte 1
unsigned :1; // bit 7
unsigned :1; // bit 6
unsigned pcjr_click :1; // bit 5
unsigned hold_state :1; // bit 4
unsigned scr_pressed :1; // bit 3
unsigned num_pressed :1; // bit 2
unsigned caps_pressed :1; // bit 1
unsigned ins_pressed :1; // bit 0
} format; // structure variable
};
void keyCtrl()
{
// use a long far pointer; 'far'
// allows a pointer to hold
// a 32-bit address, instead of
// normal or 'near' of 16-bits
// this allows you to go indirectly
// and get value at address
unsigned far *p;
KeyBits keyWord;
//
// initialize bits to off position
//
keyWord.keys = 0;
//
// establish pointer to absolute address
//
p = (unsigned far *)BASE_ADDR;
//
// retrieve bit settings
//
keyWord.keys = *p;
//
// print those toggles that are on
//
cout << (keyWord.format.ins
?"Ins ":"" )
<< (keyWord.format.caps_lock
?"Caps Lock ":"")
<< (keyWord.format.num_lock
?"Num Lock ":"" )
<< (keyWord.format.scroll_lock
?"Scroll Lock ":"")
<< (keyWord.format.alt_pressed
?"Alt Pressed":"")
<< (keyWord.format.ctrl_pressed
?"Ctrl Pressed":"")
<< (keyWord.format.left_shift
?"Left Shift Pressed":"")
<< (keyWord.format.right_shift
?"Right Shift Pressed":"")
<< (keyWord.format.ins_pressed
?"Insert":"")
<< (keyWord.format.caps_pressed
?"Caps":"")
<< (keyWord.format.num_pressed
?"NumLck":"")
<< (keyWord.format.scr_pressed
?"ScrollLck":"")
;
}// end of keyCtrl()
Notice that for bit positions that have no meaning there is no
element name, but there must be a length of the number of bits to
skip.
struct Data
{
unsigned a :1;
unsigned b :3;
unsigned x :0;
unsigned y :1;
unsigned z :2;
} bitfields;
bitfields.x causes the next bit field, bitfields.y to be
positioned at the start of the next byte in memory.
enum tag { value1, value2, ..., valueN };
where tag is an identifier that names the enumerated type, and
value1, value2, ..., valueN are identifiers, called enumerated
constants. For example, the declaration
enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday };
defines an enumerated type Days whose list of possible values are
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, and
Saturday.
Each of the values in the enumerated type has an ordinal value
associated with it. The default values start at 0 and proceed in
increments of 1 until all values in the list have an ordinal
value. In the list of values for Days, the default ordinal
values are Sunday has 0, Monday has 1, Tuesday has 2, Wednesday
has 3, Thursday has 4, Friday has 5, and Saturday has 6. An
integer value may explicitly be associated with an enumeration
constant by following it with = and a constant expression of
integral type. Subsequent enumeration constants without explicit
associations are assigned integer values one greater than the
value associated with the previous enumerated constant. For
example, the declaration
enum Days { Sunday=6, Monday, Tuesday, Wednesday=2,
Thursday, Friday, Saturday};
results in the value 6 being associated with Sunday, 7 with
Monday, 8 with Tuesday, 2 with Wednesday, 3 with Thursday, 4 with
Friday, and 5 with Saturday.
Any signed integer value may be associated with an enumeration
constant. The same integer value may be associated with two
different enumerated constants in the same type declaration.
An enumerated constant must be unique with respect to other
enumerated constants and variables within the same name scope.
Thus, in the presence of the declaration of days, the declaration
enum DaysOff { Saturday, Sunday };
is illegal because the identifiers Saturday and Sunday have
already been defined to be enumerated constant values of days.
Similarly, the variable declaration
float Monday;
following the declaration of Days, is also illegal.
Variables may be declared to be of an enumerated type in the
same declaration containing the enumerated type definition, or
in subsequent declarations of the form
enum tag variablelist;
Thus, weekday and caldays may be declared to be of type enum days
enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday } weekday, caldays;
or
enum Days weekday, caldays;
The tag may be omitted from the declaration of an enumerated
type if all the variables of this type have been declared at the
same time. Thus, the programmer may write
enum { Sunday, Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday } weekday, caldays;
but due to the omission of the enumerated tag, the programmer may
not subsequently declare another variable whose type is the same
as that of weekday or caldays.
A variable of a particular enumerated type can be assigned
enumerated constants specified in the definition of the
enumeration type. For example, the programmer may write
weekday = Tuesday;
The programmer may also compare values as in
if( weekday == caldays )
All enumerated types are treated as integer types, and the type
of enumerated constants is treated as int. However, the
programmer should differentiate between enumerated and integer
types as a matter of good programming practice and use casts if
they have to be mixed, as show in the following example:
typedef
enum { Sunday, Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday } DAYS;
DAYS nextday( DAYS this )
{
return( DAYS )( (int)this +1) % 7);
}
Listing 9-1
/*
* Example program using enumerated variables
*/
#include
#include
/*
* employee categories
*/
enum empcats {management, research, clerical, sales};
/*
* employee data
*/
struct empdata
{
char name[30];
float salary;
enum empcats category;
}
int main()
{
struct empdata employee;
/*
* establish an employee
*/
strcpy( employee.name, "Benjamin Franklin" );
employee.salary = 118.50;
employee.category = research;
/*
* show the employee data
*/
printf("\nName = %s", employee.name );
printf("\nSalary = %6.2f", employee.salary );
printf("\nCategory = %d", employee.category );
if( employee.category == clerical )
printf( "\nEmployee category is clerical.\n");
else
printf("\nEmployee category is not clerical.\n");
return 0;
}
struct SWindow
{
enum Operation {refresh, remove, clear, scroll};
void execute( Operation op);
.
.
.
};
struct SDocument
{
enum Document {brochure, pamphlet, report, book };
void setDocument( Document type );
.
.
.
};
int main()
{
SWindow win;
win.execute( SWindow::scroll );
SDocument doc;
doc.setDocument( SDocument::scroll );
.
.
.
}
Notice the use of the :: operator, the scope resolution operator. This operator associates the structure with the enumerated value defined within that structure. Although scroll is defined as a value in two enumerated types in two different structures by using the scope resolution operator there is no difficulty for the compiler in determinining which scroll is being referred in each instance.
// // stack.cpp - implementation with constructor and destructor // #includeThe member functions are written much as other functions. One difference is that they can use the data member names as they are. Thus the member functions in Stack use top and s in an unqualified manner. When invoked on a particular object of type Stack, they act on the specified member in that object. In OOP terminology an invocation is called message passing. In these terms a stack object receiving the message "pop" executes the pop method. The following example illustrates these ideas. If two Stack variablesenum BOOLEAN {FALSE, TRUE}; const int MAXSTACK = 100; struct Stack { private: char *s; int max_len; int top; enum { EMPTY = 0, FULL = MAXSTACK -1 }; public: Stack() { max_len = MAXSTACK; s = new char[max_len]; top = EMPTY; } Stack( int size ) { s = new char[size]; max_len = size; top = EMPTY; } ~Stack() { delete s; } void reset( void ) { top = EMPTY; } void push( char c ) { top++; s[top] = c; } char pop( void ) { return (s[top--]); } char top_of( void ) { return (s[top]); } BOOLEAN empty( void ) { return ((top == EMPTY) ? TRUE : FALSE ); } BOOLEAN full( void ) { return ( (top == max_len - 1) ? TRUE : FALSE ); } }; int main() { Stack z(50); Stack t; char *str = "My name is Don Knuth!"; char *str2 = "My dog has fleas and needs a bath."; int i = 0; cout << str << endl; z.reset(); // push data on the stack while( str[i] ) if( !z.full() ) z.push( str[i++] ); // pop characters from the stack and print while( !z.empty() ) cout << z.pop(); cout << endl; cout << str2 << endl; i = 0; while( str2[i] ) if( !t.full() ) t.push( str2[i++] ); while( !t.empty() ) cout << t.pop(); cout << endl; return 0; }
Stack data, operands;
are declared, then
data.reset();
operands.reset();
invoke the member function reset, which has the effect of setting
both data.top and operands.top to EMPTY. If a pointer to Stack
Stack *ptr_operands = &operands;
is declared, then
ptr_operands->push('A');
invokes the member function push, which has the effect of
incrementing operands.top and setting operands.s[top] to 'A'.
Member functions that are defined within the struct are
implicitly inline. As a rule only short, heavily used member
functions should be defined within the struct. To define a
member function outside the struct, the scope resolution operator
:: is used. To illustrate this, the definition of push will be
changed to a function prototype within struct Stack. The
implementation of the push method will be defined outside the
scope of the struct Stack which requires using the scope
resolution operator to associate push with the struct Stack.
In this case the function is not implicitly inline.
struct Stack
{
private:
char *s;
int max_len;
int top;
enum { EMPTY = 0, FULL = MAXSTACK -1 };
public:
Stack()
{
max_len = MAXSTACK;
s = new char[max_len];
top = EMPTY;
}
Stack( int size )
{
s = new char[size];
max_len = size;
top = EMPTY;
}
~Stack()
{
delete s;
}
void reset( void )
{
top = EMPTY;
}
void push( char c );
.
.
.
};
void Stack::push( char c )
{
top++;
s[top] = c;
}
The scope resolution operator allows member functions from the
different struct types to have the same names. In this case,
which member function is invoked depends on the type of object it
acts on. Member functions within the same struct can be
overloaded. Consider adding to the data type Stack a pop
operation that has an integer parameter that pops the stack that
many times before returning a value. It could be added as the
following function prototype within the struct:
struct Stack
{
...
char pop( int n );
...
};
char Stack::pop(int n )
{
while( n-- > 0 )
top--;
return( s[top--] );
}
The definition that is invoked depends on the actual arguments to
pop.
data.pop(); // invokes standard pop
data.pop(5); // invokes repeated pop
The inline specification can be used explicitly with member
functions defined at file scope. This avoids having to clutter
the struct definition with function bodies.
struct Stack
{
...
void reset();
void push( char c );
...
};
inline void Stack::reset()
{
top = EMPTY;
}
inline void Stack::push( char c )
{
s[++top} = c;
}
The grouping of operations with data emphasizes their
"objectness". Objects have a description and behavior. The data
members hold the description or attributes of an object and the
member functions implement the behavior of an object.
The concept of struct is augmented in C++ to allow functions to
have public, private, and protected members. For now the
protected keyword is a synonym for private. Its real use will be
shown when the topic of class inheritance is discussed. Inside a
struct the use of the keyword private followed by a colon
restricts the access of the members that follow this construct.
The private members can be used by only a few categories of
functions, whose privileges included access to these members.
These functions include the member functions of the struct.
In the above complete implementation of Stack, we can see that
the data members s, max_len, and top are in the private section
of the Stack. By placing data members within a private section
only member functions of the Stack can modify the data members.
For example, top can be modified by the member function reset but
cannot be accessed directly by the main() function.
The Stack has a private part that contains its data description
and a public part that contains member functions to implement
stack operations. It is useful to think of the private part as
restricted to the implementor's use and the public part as an
interface specification that clients may use. At a later time
implementor could change the private part without affecting the
correctness of a client's use of the stack type.
Hiding data is an important component of OOP. It allows for more
easily debugged and maintained code because errors and
modifications are localized. Client programs need only be aware
of the type's interface specification. As a rule of thumb, data
members should be placed in the private part of a structure and
accessed using member functions. Such an accessing discipline
ensures that a client cannot tamper with or misuse the
implemented ADT.
Stack mystack;
will cause the constructor function Stack(), which is the default
constructor, to be called. But, with a declaration such as
Stack mystack(50);
the constructor function Stack( int size ) will be called. Also,
notice that constructors do can return any values. Constructors
and destructors cannot return a value.
The programmer can also define a destructor function for a struct
or class. There can be one and only one destructor for a struct
or class. A destructor is called if there is any need to clean
up after an object is destroyed ( for example, if the programmer
wants to free memory allocated in the constructor). The C++
compiler generates code that calls the destructor function of a
structure or class whenever an instance of that struct or class
goes out of scope. The destructor has the same name as the type
except for a tilde (~) prefix. Thus, the destructor function for
the struct Stack is ~Stack(). A destructor is not called unless
an explicit or implicit return statement is executed. If no
explicit return statement is executed in a function that an
instance of a structure or class, then the implicit return
generated by encountering the closing curly brace of the function
will cause the destructor to be called. Using the exit()
function to terminate a program is discouraged because it exits
the program immediately and does not allow for the class or
structure destructors to be called.
Up until now the book has addressed syntax of the C and C++ language, because until now both languages share the same syntax, except for a few minor exceptions. Now, a major difference is to be addressed. How a program is actually assembled from a structured oriented approach and from an object oriented approach. The topic of what makes a program object oriented must be understood before an attempt is made to develop such an application.
The term object-oriented programming (OOP) is widely used, but experts cannot seem to agree on its exact definition. However, most experts agree that OOP involves defining abstract data types (ADT) that represent complex real-world or abstract objects and organizing the program around the collection of ADTs with an eye toward exploiting their common features. The term data abstraction refers to the process of defining ADTs. Object- oriented programming also involves the use of inheritance and polymorphism as the mechanisms that enable the programmer to take advantage of the common characteristics of the ADTs - the objects in the OOP.
OOP is only a method of designing and implementing software. Use of object-oriented techniques does not impart anything to a finished software product that the user can see. However, as a programmer implementing the software, you can gain significant advantages by using object-oriented methods, especially in large software projects. Because OOP enables the programmer to remain close to the conceptual, higher-level model of the real-world problem he/she is trying to solve, the programmer can manage the complexity better than with approaches that force the programmer to map the problem to fit the features of the language. The programmer can take advantage of the modularity of objects and implement the program in relatively independent units that are easier to maintain and extend. The programmer can also share code among objects through inheritance and take advantage of the commonality among objects that are inherited with polymorphism. OOP has nothing to do with any programming language, although a programming language that supports OOP makes it easier to implement the object-oriented techniques, the programmer can use objects in C programs, assembly language programs or BASIC programs. OOP is more a way of conceptualizing the problem and of implementing the code.
/***************************************************************
* File: cshapes.h
*
* Defines data types for manipulating geometric shapes in C.
*
****************************************************************/
#ifndef CSHAPES_H /* Used to avid including file twice */
#define CSHAPES_H
#define PI 3.14156
/* Define the types of shapes being supported */
typedef
enum tagSHAPE_TYPE
{
A_CIRCLE
,A_RECTANGLE
} SHAPE_TYPE;
/* Define each individual shape's data structure */
typedef
struct tagCIRCLE_SHAPE
{
SHAPE_TYPE type; /* type of shape (THE_CIRCLE) */
double x, y; /* coordinates of center */
double radius; /* radius of circle */
} CIRCLE_SHAPE;
typedef
struct tagRECTANGLE_SHAPE
{
SHAPE_TYPE type; /* type of shape (THE_RECTANGLE) */
double x1, y1; /* coordinates of the corners */
double x2, y2;
} RECTANGLE_SHAPE;
/* Now define a union of the two structures */
typedef
union SHAPE
{
SHAPE_TYPE type; /* type of shape */
CIRCLE_SHAPE circle; /* data for circle */
RECTANGLE_SHAPE rectangle; /* data for rectangle */
} SHAPE;
/* function prototypes */
double compute_area( SHAPE *p_shape );
void draw_shape( SHAPE *p_shape );
#endif /* #ifndef CSHAPES_H */
/***************************************************************
* File : cshapes.c
*
* C functions to operate on geometric shapes.
*
****************************************************************/
#include
#include
#include "cshapes.h"
/*
* compute the area of the shape and return the area
*/
double compute_area ( SHAPE *pShape )
{
double area;
/*
* handle each shape according to its type
*/
switch( pShape->type )
{
case A_CIRCLE:
area = PI * pShape->circle.radius
* pShape->circle.radius;
break;
case A_RECTANGLE:
area = fabs( (pShape->rectangle.x2
- pShape->rectangle.x1)
* (pShape->rectangle.y2
- pShape->rectangle.y1)
);
break;
default:
printf("Unknown shape\n");
}
return area;
}
/*
* draw a shape (print information about shape)
*/
void draw( SHAPE *pShape )
{
/*
* handle each shape according to its type
*/
printf( "Draw: " );
switch( pShape->type )
{
case A_CIRCLE:
printf( "Circle of radius %f at (%f, %f)\n",
pShape->circle.radius,
pShape->circle.x, pShape->circle.y);
break;
case A_RECTANGLE:
printf( "Rectangle with corners:"
"(%f, %f) at (%f, %f )\n",
pShape->rectangle.x1,
pShape->rectangle.y1,
pShape->rectangle.x2,
pShape->rectangle.y2,
break;
default:
printf("Unknown shape\n");
}
}
/*
* program to test shape-handling functions
*/
int main()
{
int i;
SHAPE s[2];
/*
* initialize the shapes
*
* A 10 x 40 rectangle with upper left corner at
* (10, 10)
*/
s[0].type = A_RECTANGLE;
s[0].rectangle.x1 = 10.0;
s[0].rectangle.y1 = 10.0;
s[0].rectangle.x2 = 20.0;
s[0].rectangle.y2 = 50.0;
/*
* A circle at (12.0, 40.0) with radius of 10.0
*/
s[1].type = A_CIRCLE;
s[1].circle.x = 40.0;
s[1].circle.y = 12.0;
s[1].circle.radius = 10.0;
/*
* compute areas
*/
for( i = 0; i < 2; ++i )
printf( "Area of shape[%d] = %f\n", i,
compute_area( &s[i] ) );
/*
* draw shapes
*/
for( i = 0; i < 2; ++i )
draw( &s[i] ) );
return 0;
}
1. Define a data structure for triangles.
typedef
enum tagSHAPE_TYPE
{
A_CIRCLE
,A_RECTANGLE
,A_TRIANGLE
} SHAPE_TYPE;
typedef struct TRIANGLE_SHAPE
{
SHAPE_TYPE type; /* type of shape (T_TRIANGLE) */
double x1, y1; /* coordinates of the corners */
double x2, y2;
double x3, y3;
} TRIANGLE_SHAPE;
2. Add a new member to the SHAPE union.
typedef union SHAPE
{
SHAPE_TYPE type; /* type of shape */
CIRCLE_SHAPE circle; /* data for circle */
RECTANGLE_SHAPE rectangle;/* data for rectangle */
TRIANGLE_SHAPE triangle; /* data for triangle */
} SHAPE;
3. In the cshapes.c file, add code in the functions
compute_area() and draw() to handle triangles.
Fig. 9-29
/*
* compute the area of the shape and return the area
*/
double compute_area ( SHAPE *pShape )
{
double area;
/*
* handle each shape according to its type
*/
switch( pShape->type )
{
case A_CIRCLE:
area = PI * pShape->circle.radius
* pShape->circle.radius;
break;
case A_RECTANGLE:
area = fabs( (pShape->rectangle.x2
- pShape->rectangle.x1)
* (pShape->rectangle.y2
- pShape->rectangle.y1)
);
break;
case A_TRIANGLE:
{
double x21, y21, x31, y31;
x21 = pShape->triangle.x2
- pShape->triangle.x1;
y21 = pShape->triangle.y2
- pShape->triangle.y1;
x31 = pShape->triangle.x3
- pShape->triangle.x1;
y31 = pShape->triangle.y3
- pShape->triangle.y1;
area = fabs ( y21 * x31 - x21 * y31 ) / 2.0;
}
break;
default:
printf("Unknown shape\n");
}
return area;
}
/*
* draw a shape (print information about shape)
*/
void draw( SHAPE *pShape )
{
/*
* handle each shape according to its type
*/
printf( "Draw: " );
switch( pShape->type )
{
case A_CIRCLE:
printf( "Circle of radius %f at (%f, %f)\n",
pShape->circle.radius,
pShape->circle.x, pShape->circle.y);
break;
case A_RECTANGLE:
printf( "Rectangle with corners:"
"(%f, %f) at (%f, %f )\n",
pShape->rectangle.x1,
pShape->rectangle.y1,
pShape->rectangle.x2,
pShape->rectangle.y2,
break;
case A_TRIANGLE:
printf( "Triangle with vertices: "
"(%f, %f) (%f, %f) (%f, %f)\n",
pShape->triangle.x1,
pShape->triangle.y1
pShape->triangle.x2,
pShape->triangle.y2
pShape->triangle.x3,
pShape->triangle.y3 );
break;
default:
printf("Unknown shape\n");
}
}
4. The programmer can now test operations on the triangle shape.
Fig. 9-30
/*
* program to test shape-handling functions
*/
int main()
{
int i;
SHAPE s[3];
/*
* initialize the shapes
*
* A 10 x 40 rectangle with upper left corner at
* (10, 10)
*/
s[0].type = A_RECTANGLE;
s[0].rectangle.x1 = 10.0;
s[0].rectangle.y1 = 10.0;
s[0].rectangle.x2 = 20.0;
s[0].rectangle.y2 = 50.0;
/*
* A circle at (12.0, 40.0) with radius of 10.0
*/
s[1].type = A_CIRCLE;
s[1].circle.x = 40.0;
s[1].circle.y = 12.0;
s[1].circle.radius = 10.0;
/*
* A triangle at (20, 5), (20, 40), and (5, 20)
*/
s[2].type = A_TRIANGLE;
s[2].triangle.x1 = 5.0
s[2].triangle.y1 = 20.0
s[2].triangle.x2 = 40.0
s[2].triangle.y2 = 20.0
s[2].triangle.x3 = 20.0
s[2].triangle.y3 = 5.0
/*
* compute areas
*/
for( i = 0; i < 3; ++i )
printf( "Area of shape[%d] = %f\n", i,
compute_area( &s[i] ) );
/*
* draw shapes
*/
for( i = 0; i < 3; ++i )
draw( &s[i] ) );
return 0;
}
The reason for going through this exercise is to point out the types of changes the programmer has to make when a new data type - a new object - has to be added to an existing program written in conventional procedure-oriented style. Notice that the programmer has to edit working code - the switch statements in the compute_area and draw functions - when he/she wants to handle triangles in addition to the rectangles and circles that the program was originally designed to accept. If this were a realistic program with many files, a change such as this would have required the programmer to edit switch statements in most of the files.
The object-oriented approach avoids this problem by keeping data structures together with functions that operate on them. This approach effectively localizes the changes that become necessary when the programmer decides to add a new object to the program.
First the terminology of object-oriented programming needs to be defined. The following terms are required knowledge when doing object-oriented programming.
Data Abstraction: The process of defining a data type, often called an abstract data type (ADT), together with the principle of data hiding. The definition of an ADT involves specifying the internal representation of the ADT's data as well as the functions to be used by others to manipulate the ADT. Data hiding ensures that the internal structure of the ADT can be altered without any fear of breaking the programs that call the functions provided for operations on the ADT.
Method: A function by any other name. A method is code bound to an object. The code provides an interface for messages requesting an operation and a means to perform the requested operation.
Object: A conceptual entity, implemented in software, which has distinct boundaries and contains both data and code. There are two types of code in an object - methods, which transfer data across the object's boundaries, and internal functions, which provide private services to the object.
Class: A class is the template from which an object is derived. It defines what data, functions and methods can be bound to the object. An object is formed by creating an instance of the class and filling in the blanks.
Inheritance: This is the ability to derive a new class from an existing one. The child class can be a sub or super set of the parent. Multiple Inheritance is the ability to derive a new class from more than one parent class.
Polymorphism: Polymorphism is when two objects bind a different method but access it by the same name. The classic example is to create two classes, car and boat. An object from either class can perform an operation "Move" but the method which carries out the operation is different. The use of the same command to generate similar effects using different code is polymorphism.
Information Hiding: Information hiding denies outside entities direct access to the object's private data and functions. Instead it forces outsiders to access the data in a controlled fashion via the object's methods. As with some government agencies, if the programmer do not need to know, then knowing will gum the works.
Messaging: The act of communicating with an object to get something done. It consists of invoking a method, supplying that method with the information it requires, and receiving a reply from the method. Clearly, messaging can be something as simple as a function call.