Tải bản đầy đủ - 0 (trang)
Chapter 9. C++ Class as a Unit of Modularization

Chapter 9. C++ Class as a Unit of Modularization

Tải bản đầy đủ - 0trang

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



ϒΠ



Initialization of Object Instances



ϒΠ



Using Returned Objects in Client Code



ϒΠ



More on the const Keyword



ϒΠ



Static Class Members



ϒΠ



Summary



In the previous chapter, I formulated the basic principles of object-oriented programming using

functions as program building blocks. With the object-oriented approach to building programs,

client code calls server access functions instead of accessing and modifying data fields directly.

Server functions provide operations directed toward achieving the client code goals.

Responsibilities are allocated among functions so that client functions do not know about data

representation, and server functions do not know about client code algorithms.

This creates independent areas of concern. When changing access functions, the maintainer does

not have to introduce corresponding changes into client functions (if the server interface does not

change). When changing client functions, the maintainer does not have to consider the details of

data processing in server functions¡Xthey will not require changes. The client code is expressed in

terms of function calls to server functions, not in terms of data manipulation. Putting together what

should belong together (instead of tearing it apart) makes functions independent from each other

and further facilitates maintenance and reuse. The object diagrams I drew for the previous chapter

indicated that server functions logically belong with each other and with the data they access.

I also admitted that using functions for implementing the object-oriented approach relies on the

voluntary efforts of the programmers. Server functions can be placed in unrelated places in the

source code, and the maintainer might fail to notice that they are related to each other and to the

data representation. Client functions can fail to use encapsulation; instead, they can access the data

representation directly and create links and dependencies between different parts of the program.

Under time pressure or because of the narrow span of human attention, programmers can introduce

dependencies among functions. Functions with mutual dependencies are difficult to develop

because different programmers who work on different interdependent functions have to coordinate

their activities, and it is the coordination of human activities that breaks down so often and so

miserably.

Interdependent functions become more difficult to maintain because they require the maintainer to

study these dependencies before making changes. These functions are more difficult to reuse,

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (461 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



because they cannot be moved to another application alone; they need data and other functions to

accompany them. This is why a programmer needs all the help from the programming language

that she or he can master to avoid these pitfalls. To help the programmers to create better code,

C++ offers a wonderful language construct, class, which physically binds together data

representation and operations (functions) over that data that are otherwise bound together only

conceptually, in the mind of the designer. This binding data and operations together supports the

concepts of data encapsulation and information hiding.

In this chapter, I will take a close look at C++ classes. You will see class syntax and semantics and

will learn how to define class members, both data members and member functions. I will explain

how to specify access rights to class members; how to implement classes in one-file and multifile

programs; how to define objects (class instances); and how to manipulate the objects, that is, how

to send messages, pass them as parameters, and return them from functions.

I will also discuss special member functions, constructors and destructors, which are often

misunderstood. I will further discuss the use of the const modifier discussed earlier in Chapter 7,

"Programming with C++ Functions," to help the developer to pass on his or her knowledge at the

time of design to the maintainer at the time of modification. Another special kind of data members

and member functions is static data members and functions. Static members and functions help the

designer to describe class characteristics that are common to all objects of the class.

This is an ambitious program. By the end of this chapter, you should feel comfortable using larger

units of modularization (classes), instead of smaller units of modularization (functions). But you

might also feel overwhelmed by the immense amount of technical detail you have to assimilate.

This is natural. C++ is a large and complex language, and it takes time to get used to its concepts,

practical details, and pitfalls. Anybody who promises you an easy way to learn C++ is either lying

or does not realize the complexity of this task. If you feel overwhelmed and confused, do not dig

your heels in¡Xtry the incremental approach to learning. Skip some parts of this chapter, read the

next chapters, come back to this chapter for repeated reading. Modify its coding examples,

experiment with different ways to say the same thing in C++ code, and you will see that there is a

beautiful internal logic connecting different elements of C++ programming, and it is not difficult to

use after all. But this feeling of ease will come only after extended practice.

It is important to come back to master the C++ basics of using classes. Many programmers jump

over this phase and move on to more complex issues such as inheritance and polymorphism too

quickly, without having a good foundation. They become confused even more and wind up writing

programs that are hard to understand, maintain, and reuse. After all, C++ offers you only a set of

tools. These tools might be misused (similar to guns, automobiles, or computers). Using these tools

does not automatically guarantee good results. It is up to you, the programmer, to use this set of

tools effectively. Good luck.



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (462 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Basic Class Syntax

The goal of introducing classes in C++ is to render support to the practices of object-oriented

programming and eliminate the drawbacks caused by using smaller units of modularity: functions.

The first primary goal of the class construct is to bind together data and operations into one

syntactical unit, and to indicate that these coding elements belong together. The next primary goal

is to eliminate name conflicts so that data and functions in different classes can use the same names

without clashes. The third important goal of the class construct is to allow the server designer to

control access to class elements from the outside (from the client code). The fourth goal is to

support encapsulation, information hiding, pushing responsibilities from client code down to the

server code, creating separate areas of concern and eliminating overlapping knowledge and

coordination among programmers working on different parts of the program.

These goals are a natural extension of the practice of using functions for object-oriented

programming. If you view C++ classes as yet another syntactic construct, not related to these four

goals I described, the use of classes will not improve the quality of your code. Make sure that you

pay sufficient attention to these four goals and try to achieve them every time you add a class to

your program.

The class is the center of C++ and object-oriented programming. It gives a programmer the tools to

create new data types that more closely match the behavior of real-world objects than functional

programs do. Some experts say that it is the use of inheritance and polymorphism that is central to

object-oriented programming. I disagree. There are many programs that do not benefit from the use

of inheritance and polymorphism. However, every large C++ program benefits from the use of C++

classes if these classes are used correctly and achieve the four goals just outlined. A correctly

designed C++ program is a combination of components (modules) that cooperate in performing

their common task but are independent enough to be separately maintainable.



Binding Together Data and Operations

Structures also support the concept of binding by combining data fields. They allow you to

combine different components into a composite data object. These composite objects can be

manipulated as a whole, for example, sent as a parameter to a function, or they can provide access

to their components individually.

A structure definition, however, models only a set of data but not their behavior. The server

programmer provides the tools to manipulate the data, that is, a set of access functions to access

and manipulate data on behalf of client functions. In "functional" or "procedural" programming,

data and algorithms are syntactically separated. They are related together only in the mind of the

programmer, not in code. In the examples that I discussed in Chapter08, "Object-Oriented

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (463 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Programming with Functions," I used object diagrams to indicate that functions and data logically

belong together.

When programs consist of functions, connections among different functions in the program are not

evident; every function can access every piece of data in the program. That makes development

and, especially, maintenance and reuse more difficult.

The only way to indicate that data are related to the code that works with the data is to put the data

items and the function prototypes into the same header file or in a separately compiled source file.

But a disk file is a hardware (or operating system) concept and not part of the language. This is why

C++ expands the struct facility by binding together data members that contain values and

member functions that operate on these values.

Resulting objects represent larger units of modularity. Client programmers focus on data and on

related functions, not on stand-alone functions whose connections are not evident.

In a well-designed C++ program, class data is accessed by functions that belong only to that class.

Client code is expressed in terms of operations rather than in access to data. This narrows the

horizon of client designers and maintainers.

Formally, when you put together fields in a struct definition, you actually create a C++ class.

struct Cylinder {

double radius, height;



} ;



// programmer-defined type (class)

// end of class scope



In C++, the keywords struct and class are synonymous (well, almost). For class Cylinder just

defined, you can define objects (or instances or variables) of this class. You can set the values of

object fields. You can either handle an object as a single entity (e.g., pass it as a function argument

or store on a disk file) or use its individual parts in computations.

In the example that follows, the main() function defines two Cylinder objects (variables and

instances), initializes them, and compares their volumes. If the volume of the first Cylinder object

is less than the volume of the second Cylinder object, the first Cylinder is scaled up by 20%, and

the new dimensions of the first Cylinder are printed. This is similar to the example I discussed at

the beginning of Chapter 8.

int main()

{ Cylinder c1, c2;

// program data

c1.radius = 10; c1.height = 30; c2.radius = 20; c2.height = 30;

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (464 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



cout << "\nInitial size of first cylinder\n";

cout <<"radius: " <
if (c1.height*c1.radius*c1.radius*3.141593

// compare volumes

< c2.height*c2.radius*c2.radius*3.141593)

{ c1.radius *= 1.2; c1.height *= 1.2;

// scale it up and

cout << "\nFirst cylinder changed size\n";

// print new size

cout <<"radius: " <
else

// otherwise do nothing

cout << "\nNo change in first cylinder size"<< endl;

return 0; }



In this code, the names of data fields are used explicitly. The client code accesses the field values

and does whatever is necessary (computing volumes, scaling size, printing). Changes to the

Cylinder design affect not only the Cylinder structure, but the client code as well. The

maintenance programmer has to deduce the meaning of processing (again, computing volumes,

scaling size, printing) from following each step of computations. To check whether all dimensions

of the object are initialized or scaled up or printed, one has to refer to the class Cylinder definition.

The reuse of these operations (computing volumes, scaling size, printing) for the same or for

another project is difficult because they are tied to the client code context.

These drawbacks can be eliminated by using access functions that encapsulate operations over

structure fields: setCylinder(), printCylinder(), getVolume(), and scaleCylinder().

Listing 9.1 shows this version of the client code and server code. The results of the program run are

shown in Figure 9-1.



Figure 9-1. Output for program in Listing 9.1.



Example 9.1. Example of using access function on behalf of the client code.

#include

using namespace std;

struct Cylinder {

access

double radius, height; } ;



// data structure to



void setCylinder(Cylinder& c, double r, double h)

{ c.radius = r; c.height = h; }

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (465 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



double getVolume(const Cylinder& c)

{ return c.height * c.radius * c.radius * 3.141593; }

void scaleCylinder(Cylinder &c, double factor)

{ c.radius *= factor; c.height *= factor; }



// compute volume



// scale dimensions



void printCylinder(const Cylinder &c)

// print object state

{ cout << "radius: " <
int main()

// pushing responsibility to server

functions

{ Cylinder c1, c2;

// program data

setCylinder(c1,10,30); setCylinder(c2,20,30);

// set cylinders

cout << "\nInitial size of first cylinder\n";

printCylinder(c1);

if (getVolume(c1) < getVolume(c2))

// compare volumes

{ scaleCylinder(c1,1.2);

// scale it up and

cout << "\nFirst cylinder changed size\n";

// print new size

printCylinder(c1); }

else // otherwise do nothing

cout << "\nNo change in first cylinder size" << endl;

return 0;

}



This example is similar to the one in Listing 8.7, and what I demonstrated so far does not go

beyond the capabilities of an ordinary structure. Let us make the next step: combine data fields and

functions within the same class. In the following example, the syntactic boundaries of the class

Cylinder are denoted by the opening and the closing braces and by the closing semicolon.

This class contains two fields, or data members: radius and height. In addition to data members,

the class contains four member functions. (Another term for member functions is method; it comes

from Smalltalk and artificial intelligence.) Member functions have the same syntax as global nonmember functions: They can have parameters and return values, and each function has its own

scope for its local variables. Unlike global functions, member functions are defined inside class

boundaries (braces). Now everyone can see that these functions, setCylinder(), getVolume(),

scaleCylinder,() and printCylinder(), belong together and with data fields radius and

height.



struct Cylinder {

class scope

double radius, height;

members

void setCylinder(double r, double h)

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (466 of 1187) [8/17/2002 2:57:55 PM]



// start of

// class data

// class member



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



functions

{ radius = r; height = h; }

// set field

values

double getVolume()

{ return height * radius * radius * 3.141593; } // compute volume

void scaleCylinder(double factor)

{ radius *= factor; height *= factor; }

// scale

dimensions

void printCylinder()

// print object state

{ cout << "radius: " <
} ;

// end of class

scope



Adding member functions does not change the basic property of a structure; it is a template that is

capable of defining objects of this class.

Cylinder c1, c2;



// space for two object instances is allocated



When you discuss object-oriented designs and programs, you naturally use the term "object."

Unfortunately, this term has more than one meaning. Some people use this term to denote an

abstract concept important for the application, such as customer, account, or transaction objects.

Other people use this term to denote individual objects, such as an account that belongs to a

particular customer. Yet other people do not really know what they mean when they use this term

but still somehow expect other people to figure that out. I do not want to press you to make your

choice right away, but please do not get into this third category.

In this book, I use the terms variables, instances, class instances, class objects, and object instances

as synonyms. They all denote a program entity that is allocated memory (on the stack or on the

heap) for some period during program execution; they belong to a specific storage class and are

subject to scope rules. I try to avoid using the term "object" altogether. If push comes to shove and

I do use the term, I use it in the sense of a program variable. In most cases, it will be a variable of a

programmer-defined type, but I have no qualms about using this term "object" for variables of builtin types as well. This is the programming meaning of the term.

In object-oriented analysis and design, the term "object" is often used to denote a set of potential

instances with the same properties. This usage is closer to the concept of the class (programmerdefined type) than to the concept of the object instance. I do not want to get into an argument as to

which usage is correct. But because of this ambiguity, it is a good idea not to use the term "object"

without describing the meaning of the term.

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (467 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



This might sound funny that in a book devoted to object-oriented programming, I argue against

using the term "object," but the indiscriminate use of the term does cloud its meaning. When you

use this term, always make sure to be clear what you mean¡Xa single instance in the computer

memory that exists during program execution or a generalized description of these potential

instances, a C++ class that will be used to create specific instances during program execution.

The presence of member functions expands the size of the structure definition but does not expand

the size of structure instances: It is still the sum of sizes of individual fields (with possible

additional space for alignment). In this case, each Cylinder instance is allocated the space

sufficient for holding two double values.



Elimination of Name Conflicts

Class opening and closing braces (and the closing semicolon) form the class scope pretty much in

the same way as an ordinary structure forms a separate scope for its fields. This class scope, which

is nested within the file scope, is similar to the scope of an ordinary structure. The difference is that

the class scope can nest function scopes.

Nonmember functions (e.g., access functions in Listing 9.1) are global functions, and their names

must be unique in the program (unless they are made static in the file scope, but only a small

minority of functions can be made static in the file scope; more on static functions can be found in

Chapter 6, "Memory Management. The Stack and The Heap," and later in this chapter). Normally,

only those team members that use class Cylinder should learn about these function names because

they are going to call this functions. In practice, every team member should learn about these

names to avoid accidental name conflicts. This information obstructs the channels of

communication among programmers.

When functions are implemented as class member functions (as in Listing 9.2 later), their names

are local to the class scope. As a result, you cannot call member functions (or, for that matter,

access data members) using their names, for example, radius or setCylinder(). You have to

indicate whose Cylinder instance this radius belongs to or for what Cylinder variable to call the

function setCylinder().

c1.radius = 10;

c2.setCylinder(20,30);



// radius of c1

// setCylinder() for c2



In this example, it is radius of Cylinder variable c1 that is set to 10, and it is Cylinder variable

c2 that is used to call setCylinder().

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (468 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



If the application uses another class, for example, Circle, which needs a data member radius, it

does not result in name conflicts.

struct Circle {

double radius;

. . . } ;



// it can be integer or anything



To access the radius field of class Circle, the application has to define Circle object instances

and use their names to access the radius field.

Circle cir1;



cir1.radius = 10;



// no ambiguity: Circle, not Cylinder



All class members (data members and member functions) are in the same scope within the class

braces. Hence, they can access each other by name, without qualifying references (scope operators)

to the class name or to the object name. For example, function setCylinder() sets the values of

fields (data members) radius and height.

void setCylinder(double r, double h)

{ radius = r; height = h; }



// set field values



Whose radius and height are these? They are the fields of some Cylinder object (class instance).

When a member function, for example, setCylinder(), is called in the client code, this is called

sending a message to an object. Client code (which is outside of the class braces) identifies the

target of the message (the object whose fields are used inside the member function) by explicitly

using the object name, the name of the member function, and the dot selector operator between the

two.

Cylinder c1, c2;

// potential targets of messages

c1.setCylinder(10,30); c2.setCylinder(20,30);

// messages to c1, c2



The message applies to an instance of an object of that class. When the first message executes, it is

radius and height of object c1 that are used inside setCylinder(). When the second message

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (469 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



executes, it is radius and height of object c2 that are used inside setCylinder(). This is true

not only when the values of the fields are changed during message execution, but also when they

are merely used in computations.

if (c1.getVolume() < c2.getVolume())

{ c1.scaleCylinder(1.2); . . .



// compare volumes

// scale it up



In the first message, it is the fields of variable c1 that are used in the computation of the volume

(whatever these computations are); in the second message, it is the fields of instance c2 that are

used for computing volume. In all the cases, you use the name of the object, the dot selector

operator, and the name of the message (member function). The message syntax is the same as the

syntax for accessing (or changing) the fields of the structure. For a field, you use the name of the

object, the dot selector operator, and the name of the field.

c1.radius = 40.0;



c1.height = 50.0;



// variable c1 is used



Listing 9.2 shows the version of the client code and server code that uses class Cylinder where

data fields are bound together with member functions. Since functionality of the program is the

same as in Listing 9.1 and it is the implementation only that has changed, the output of this

program is the same as that in Listing 9.1.



Example 9.2. Example of binding data and functions in a class with its own scope.

#include

using namespace std;

struct Cylinder {

double radius, height;



// start of the class scope

// data fields to access



void setCylinder(double r, double h)

{ radius = r; height = h; }



// set cylinder data



double getVolume()

{ return height * radius * radius * 3.141593; }



// compute volume



void scaleCylinder(double factor)

{ radius *= factor; height *= factor; }



// scale dimensions



void printCylinder()

// print object state

{ cout << "radius: " <
file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (470 of 1187) [8/17/2002 2:57:55 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



} ;



// end of class scope



int main()

// pushing responsibility to server functions

{ Cylinder c1, c2;

// define program data

c1.setCylinder(10,30); c2.setCylinder(20,30);

// set cylinders

cout << "\nInitial size of first cylinder\n";

c1.printCylinder();

if (c1.getVolume() < c2.getVolume())

// compare volumes

{ c1.scaleCylinder(1.2);

// scale it up and

cout << "\nFirst cylinder changed size\n";

// print new size

c1.printCylinder(); }

else

// otherwise do nothing

cout << "\nNo change in first cylinder size" << endl;

return 0;

}



Compare Listing 9.2 with Listing 9.1 and make sure you see the difference between using standalone global functions, as in Listing 9.1, and functions bound with the data, as in Listing 9.2. With

stand-alone access functions, the object variable whose data is to be used within the function is

passed as a parameter.

void setCylinder(Cylinder& c, double r, double h)

{ c.radius = r; c.height = h; }



// access function

// Cylinder is a parameter



The appropriate object instance should be used in the function call as the actual argument.

setCylinder(c1,10,30); setCylinder(c2,20,30);



Without using classes, it would be utterly incorrect to implement setCylinder() function without

a Cylinder parameter and call this function without passing the actual object to operate on.

void setCylinder(double r, double h)

{ c.radius = r; c.height = h; }

setCylinder(10,30); setCylinder(20,30);



// nonsense: what Cylinder?

// nonsense: what Cylinder?



When you design a function as a class member function, there is no need to pass the object to be

used as a parameter.

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (471 of 1187) [8/17/2002 2:57:55 PM]



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Chapter 9. C++ Class as a Unit of Modularization

Tải bản đầy đủ ngay(0 tr)

×