1. Trang chủ
  2. » Công Nghệ Thông Tin

Core C++ A Software Engineering Approach phần 9 ppsx

120 330 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Core C++ A Software Engineering Approach
Trường học Standard University
Chuyên ngành Computer Science
Thể loại Bài tập tốt nghiệp
Năm xuất bản 2002
Thành phố City Name
Định dạng
Số trang 120
Dung lượng 2,08 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Summary of Rules for Static and Dynamic BindingKind of Member Function Base Pointers Derived Pointers Base object Derived object Base object Derived object Functions defined in the Base

Trang 1

Figure 15.12 Static and dynamic binding for base and derived class pointers.

Figure 15-12 shows base class pointers (narrow ones) and derived class pointers (fat one, with two parts) that point to base class objects (the dashed part represents the missing derived part) and to derived class objects (the left part represents the base part, the right part represents the derived part)

The vertical lines inside each part represent member functions of four types Type 1 is defined in the base class and is inherited in the derived class as is Type 2 is added to the derived class without

a counterpart in the base class Type 3 is defined in the base class and redefined in the derived class with the same name (with the same or different signatures) Type 4 is defined in the base class (as virtual) and is redefined in the derived class with the same name and the same signature

The methods that can be called through the pointer are underlined In case A, functions of types 3 and 4 defined in the base class are hidden by the functions defined in the derived class In case B, only functions defined in the base class are accessible In case C, only functions defined in the base class are accessible, but the functions redefined in the derived class as virtual hide their base class counterparts and can be called dynamically In case D, only functions defined in the base class and not redefined in the derived class can be called

Table 15.1 summarizes the same rules The columns describe kinds of objects and kinds of pointers that point to these objects, the rows describe different kinds of member functions

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 2

Table 15.1 Summary of Rules for Static and Dynamic Binding

Kind of Member Function Base Pointers Derived Pointers

Base object Derived object Base object Derived object Functions defined in the Base class

Inherited in Derived class as available available available available

Redefined in Derived (non-virtual) available available not available hidden

Redefined in Derived (virtual) available hidden not available hidden

Functions defined in the Derived class

Defined in Derived class only syntax error syntax error crash available

Redefined in Derived (non-virtual) not available not available crash available

Redefined in Derived (virtual) not available dynamic binding crash available

Pure Virtual Functions

Base virtual functions may have no job to do because they have no meaning within the application Their job is just to define the interface as a standard for its derived classes This is why virtual

functions are introduced in the first place

For example, the write() method in class Person does not contain anything It has no code

Notice too that it is never called All calls to the write() method in the client code (global function

write()) are resolved either to a Faculty class or to a Student class method write().

Actually, class Person is a pure generalization with no real role There are no Person objects in the application All objects are created with the operator new in the global function read() and are either of Student or Faculty class The description of the problem at the beginning of this section simply says that there are two types of records, one for students and one for faculty members Class

Person was first introduced into the application as an abstraction that merges the characteristics of faculty objects and student objects into one generalized class (Listing 15.3) Later, it was used to define a hierarchy of derived classes (Listing 15.4) In the last version of the program (Listing

15.4), class Person was used to define the interface of the virtual function write().

In real life, class Person might be a very useful class In addition to the university id and the name,

it can contain date of birth, address, phone number, and a host of other characteristics that are

common to Faculty and Student objects In addition to data, class Person can define numerous methods such as change of name, address or phone number, retrieval of the university id, and other data common to Faculty and Student objects The derived classes can inherit all these useful functions The clients of derived classes can use these functions by sending these messages

(defined in the Person class) to objects of classes Faculty and Student. Again, I am not saying that class Person is useless I am saying that objects of class Person are useless for this

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 3

application The application needs only objects of classes derived from Person. Please keep this distinction in mind.

The Person class designer knows that the application does not create class objects and that class objects have no job to do It would be nice to pass this knowledge on to the client programmer (and

to maintainers), not in comments, but rather in code C++ allows you to define the base class in such a way that an attempt to create an object of this type would be illegal and would result in a syntax error

C++ makes it possible through the use of pure virtual functions and abstract classes I am not sure why two terms, "pure" and "abstract," are used to describe the same idea A pure virtual function is

a virtual function that should not be called (like write() in class Person) If the program tries to call this function, a syntax error results An abstract class is a class with at least one pure virtual function (more than one is okay) It is illegal to create objects of this class If the program tries to create an object of this class either dynamically or on the stack, a syntax error results

There are no C++ keywords for pure virtual functions and abstract classes Instead, a pure virtual function is recognized (by the compiler, client programmer, and maintainer) as a member function whose declaration is "initialized" to zero Here is the class Person whose write() member

function is defined as a pure virtual function

Person(const char id[], const char nm[]);

virtual void write() const = 0; // pure virtual function

~Person();

} ;

Of course, the assignment operator does not denote the assignment here This is just another

example of giving a symbol an additional meaning in yet another context This is confusing

Adding another keyword, such as pure or abstract, would probably be a better design

A pure virtual function has no implementation Actually, it is a syntax error to supply the

implementation of the pure virtual function (or to invoke the function) It is the presence of virtual functions that makes a class an abstract (or partial) class

In addition to the lack of instantiated objects, an abstract class is required to have at least one

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 4

derived class If the derived class implements this function, it becomes a regular class If the

derived class does not implement this function, it becomes an abstract class as well It is illegal to create objects of that derived class, and this class must have at least one derived class

The derived classes implement the pure virtual functions in the same way they implement regular virtual functions This means that the derived class should use the same name, the same signature, and the same return type as the pure virtual function should use The mode of derivation should be public Here is an example of class Faculty that implements the virtual function write(). It is a regular non-abstract class

Faculty(const char id[], const char nm[], const char r[]);

void write () const; // regular virtual function

Person is implemented, as long as the client code does not try to instantiate the objects of the

abstract class

For a regular class with virtual functions, the client code can create objects of this class, send

messages to these objects, and use polymorphism if needed

Again, an abstract class is a C++ class in all regards; it can have data members and regular nonpure functions, including virtual functions

If a class inherits a virtual function as a pure virtual function without defining its body, this derived class is also an abstract class: No objects of that class can be created If the client code needs

objects of that class but is not going to call that function on these objects (because this function has

no job yet), an empty body of the function can be used The class becomes a regular, non-abstract class, and an object of that class can be created

class Base { // abstract class

public:

virtual void member() = 0; // pure virtual function

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 5

¡K } ; // the rest of Base class

class Derived : public Base { // regular class

public:

void member() // virtual function

{ } // empty body: noop

¡K } ; // the rest of Derived class

Here, class Base is an abstract class The objects of this class cannot be created Class Derived is a regular class Its objects can be instantiated both on the stack (as named variables) and on the heap (as unnamed variables) Function member() in the Base class is a pure virtual function It cannot be called Function member() in the Derived class is a regular virtual function However, its call results in no operation

Base *b; Derived *d; // Base and Derived pointers

b = new Base; // syntax error, abstract class

d = new Derived; // OK, regular class, heap object

b = new Derived; // OK, implicit pointer conversion

d->member(); // OK, compile time binding, no op

b->member(); // OK, run time binding

d->Base::member(); // linker error: no implementation

Redefinition with a different signature makes the function non-virtual in the derived class Here, class Derived1 is a class that inherits from the abstract class Base but does not redefine the pure function member() with no parameters Instead, it defines a function member(int) with one

parameter

class Derived1 : public Base { // also an abstract class

public:

void member(int) // non-virtual function

{ } // empty body: noop

¡K } ; // the rest of Derived1 class

This means that class Derived1 is an abstract class It is a syntax error to create objects of this class Since this class is not used as a base class to derive other classes, it is quite useless, since there is no way one can use any of its functions

class Derived2 : public Derived1 { // regular class

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 6

public:

void member() // virtual function

{ } // empty body: noop

¡K } ; // the rest of Derived class

Class Derived2 inherits from class Derived1. It implements the virtual member function

member(), and hence it is legal to create objects of this class The objects of this class can respond

to the message member(), both with run-time binding and with static binding The objects of this

Derived2 *d2 = new Derived2; // OK, regular class, heap object

d2->member(); // OK, static binding

b = new Derived2; // OK for virtual functions

b->member(); // OK, dynamic binding

b->member(0); // syntax error

d2->member(0); // wrong number of parameters

Notice that the base class pointer b, when pointing to the derived class object, can invoke only

ϒΠ non-pure member (virtual or non-virtual) functions defined in the base class

ϒΠ virtual functions defined in the derived class

It cannot invoke non-virtual member functions defined in the derived class It is a myopic pointer

It takes a virtual function to extend its scope of vision to the derived part of the object it is pointing

to Otherwise, it can see only the base part of the derived object It takes the derived class pointer to access both the base part and the derived part of the derived object

Virtual Functions: Destructors

When the delete operator is invoked, the destructor is called and the object is destroyed Which destructor is called? The destructor defined in the class of the pointer pointing to the object or the destructor defined in the class to which the object pointed to by the pointer belongs?

When the pointer and the object pointed to by the pointer belong to the same class, the answer is simple: the destructor is of that class

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 7

Derived2 *d2 = new Derived2; // OK, regular class, heap object

d2->member(); // OK, static binding

b = new Derived2; // OK for virtual functions

b->member(); // OK, dynamic binding

delete d2; // class Derived2 destructor

delete b; // ??

C++ destructors are member functions They are regular non-virtual member functions When the

delete operator is used, the compiler finds the definition of the pointer operand, searches the definition of the class to which the pointer belongs, and calls the destructor that belongs to that class All this happens at compile time The compiler pays no attention to the class of object to which the pointer is pointing

When the pointer and the object are of the same class there is no problem: The dynamic memory (and other resources) allocated to the object are returned as the destructor code executes When the derived class pointer points to a base object¡Xwell, you should not do that The big and powerful derived class pointer will require the little base object to do things it cannot do

Person p; Faculty f; // base and derived pointers

p = new Person("U12345678", "Smith");

f = p; // syntax error: stay away

f = (Faculty*)p; // I insist I know what I do

delete f; // Faculty destructor

In this example, the Faculty destructor is invoked on a Person object The delete operator is called for the data member rank that is not in the object The results are undefined

When a base pointer points to a derived class object, the base class destructor is called This might

or might not be troublesome If the dynamic memory is handled in the base class but not in the derived class, there is no problem; the heap memory is returned by the base destructor If the

derived class handles the heap memory, it is not returned by the base destructor A memory leak results

Person *p; Faculty* f; // base and derived pointers

f = new Faculty("U12345689", "Black", "Assistant Professor");

Trang 8

In this example, the delete operator invokes the Person destructor, which deletes dynamic

allocated for the rank is not returned

In Listing 15.5, the client code uses a loop to go over the array of base pointers and delete each object allocated dynamically at the beginning of the execution For each object in the data structure, the Person destructor is executed

for (int j=0; j < cnt; j++)

{ delete data[j]; } // delete Person heap memory

delete operator deletes the object no matter what the type of the object The problem is not with the object memory but with the heap memory allocated to the derived class objects, the right-most part on Figure 15-10 The Person destructor deletes the heap memory allocated for name but not

pointer, only the base destructor is called, and the derived class destructor is not called

C++ offers a way out of this: to declare the base destructor virtual By convention, it makes every derived class destructor also virtual When the delete operator is applied to a base pointer, the target class destructor is called polymorphically (and then the base class destructor, if any)

Person(const char id[], const char nm[]);

virtual void write() const = 0; // pure virtual function

virtual ~Person(); // this makes the trick

Faculty(const char id[], const char nm[], const char r[]);

void write () const; // regular virtual function

~Faculty(); // now this is virtual, too

} ;

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 9

This solution is somewhat awkward After all, the first thing you should remember about virtual functions is that you use exactly the same name everywhere, in the base class and in all derived classes With destructors, this is not the case because each destructor has the same name as the class name Hence, destructors violate the rules of virtual functions This is similar to constructors, and, indeed, there are no virtual constructors in C++

However, practical considerations are more important that logical beauty Memory leaks are too dangerous to put up with This is why C++ allows this inconsistency and supports virtual

destructors

Multiple Inheritance: Several Base Classes

In C++, a derived class might have more than one base class With single inheritance, the hierarchy

of classes is a tree, with the base class on the top of the hierarchy and derived classes below the base class

With multiple inheritance, the hierarchy of classes might become a graph rather than a tree as with single inheritance This makes the relationships among classes more complex than with single inheritance The issues related to multiple inheritance are more difficult to understand than for simple inheritance

Similar to single inheritance, multiple inheritance is not so much a technique to make the client code simpler or easier to understand; it is a technique that makes the server code easier to produce Unlike single inheritance, multiple inheritance allows the server class designer to mix the

characteristics of diverse classes in one class

Let us consider a simple example Let us assume that class B1 provides the clients with a public service f1() and class B2 provides the clients with a public service f2(). This is almost exactly what the client code needs, but in addition to these two services the client code needs a public service f3() to be available One possible technique to serve the client is to merge characteristics

of classes B1 and B2 into one class using multiple inheritance

class B1

{ public:

void f1(); // public service f1()

¡K }; // the rest of class B1

class B2

{ public:

void f2(); // public service f2()

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 10

¡K }; // the rest of class B2

By inheriting publicly from class B1 and B2, class Derived becomes able to provide its clients with the union of services provided by each of the base classes, in this case, methods f1() and f2().

This means that to provide its clients with all three services (f1(), f2(), f3(),) the designer of

class Derived : public B1, public B2 // two base classes

{ public: // f1(), f2() are inherited

void f3(); // f3() is added to services

¡K }; // the rest of class Derived

Multiple inheritance is good for customizing existing class libraries, for example, for adding or redefining members in existing classes Derived classes represent a combination of base classes rather than a refinement of a single base class Each parent class contributes properties to the

derived class; the derived class is a union of the base features

Examples of using multiple inheritance include graphic objects, NOW accounts, and iostream

classes in the C++ standard library

For a graphics package, classes Shape and Position were used as base classes to produce class

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 11

Object. The objects of class Object (sorry for the pun, it was not intended) combined properties

of Shape objects and Position objects This is an example of unreasonable use of multiple

inheritance Sure, a graphics object is a shape, but it is a stretch to state that a graphics object is a position It is more natural to say that a graphics object has a position

For NOW accounts, the classes represent savings accounts and checking accounts This is a better example of using multiple inheritance because a NOW account indeed combines the properties of savings accounts and checking accounts: It pays interest and allows check writing However, if you talk to a bank officer, you would hear that yes, basically this is correct, but there are quite a few exceptions that make a NOW account different from both a savings account and a checking

account This means that the benefits of easily merging basic characteristics are offset by the

drawbacks of suppressing features that do not fit

For the C++ iostream class library, the use of multiple inheritance to merge characteristics of input stream classes and output stream classes makes sense The resulting iostream classes support both input operations and output operations, and there is nothing to suppress in the derived classes

Notice that C++ does not put a limit on the number of base classes that can participate in forming a derived class All examples that I cited involve only two base classes In all examples of multiple inheritance that follow, I will limit myself to two base classes as well Why not three or four? The answer is simple¡Xit is hard to come up with examples of multiple inheritance with three or four base classes so that they make good sense and do not confuse the user Why is two better than three

or four? I suspect that the examples of multiple inheritance with two base classes are also hard to come by

This is why I recommend that you use multiple inheritance with caution The advantages are few and complications are many There are always ways to adequately support the client code without using multiple inheritance

Multiple Inheritance: Access Rules

With multiple inheritance, a derived class inherits all data members of all bases and all member functions of all bases The space that an object of the derived class occupies in memory is the sum

of the space that the objects of the base classes occupy in memory (with possible addition for

alignment)

Access rules for multiple inheritance are the same as for single inheritance The methods of the derived class can access public and protected members (data and methods) of all its base classes without any limitations They cannot access private members of the base classes

Inheritance links can be public, protected, or private In either case, all data members and member

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 12

functions of all base classes are inherited in the derived class, but the access rights can change depending on the mode of derivation.

Modes of derivation for multiple inheritance are the same as for single inheritance With public derivation, each base member, private, protected, and public, has the same access rights in the derived class object as in the base object This is the most natural mode of inheritance

With protected derivation, protected and public base members remain protected and public in the derived class, but public base members (data and functions) become protected in the derived class Since the derived class has full access to protected base components, protected inheritance does not affect access rights of the derived class Similar to single inheritance, it does affect access rights of the client code The client code loses the right to use public base services The derived class has to provide adequate services to the clients without making the public base services available to the client code

With private inheritance, all base members become private in the derived class Similar to single inheritance, the mode of derivation is private by default The mode of derivation should be

specified for each base class separately

Let us consider the same two base classes B1 and B2 as in the previous example

class B1

{ public:

void f1(); // public service f1()

¡K }; // the rest of class B1

class B2

{ public:

void f2(); // public service f2()

¡K }; // the rest of class B2

Let us combine their characteristics in a derived class Derived and add yet another member

function in the derived class

class Derived : public B1, B2 // two base classes

{ public: // f1(), f2() are inherited

void f3(); // f3() is added to services

¡K }; // the rest of class Derived

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 13

Then the client code can define and use the objects of the Derived class.

Derived d; // instantiate Derived object

d.f1(); // inherited from B1

d.f2(); // syntax error: f2() is private

d.f3(); // the service is added in the Derived class

access rights and used for describing the mode of derivation With access rights, the scope of the

found or until the end of the class definition With mode of derivation, the scope of the keyword

public is only one identifier

In the example above, it is only class B1 from which class Derived inherits publicly For class B2,

the default (private) mode of derivation is used This is why method f2() becomes private in the

Derived class, and as such is not available to the client code

Conversions Between Classes

Conversion rules for multiple inheritance are similar to those for single inheritance If a base class

is inherited from publicly, objects of the derived class can be implicitly converted into objects of that base class; no explicit cast operator is needed for such a conversion

The concepts behind this rule are the same as those for single inheritance An object of the derived class has all the capabilities, data, and functions of objects of base classes Conversion from a

derived object to a base object cannot result in the loss of capabilities unless, of course, the mode of derivation is not public

B1 b1; B2 b2; Derived d;

b1 = d; b2 = d; // OK: extra capabilities are discarded

d = b1; d = b2; // error: inconsistent state of object

Conversion from a base class to the derived class is not allowed A base object has only part of the data and capabilities that a derived object has, and the missing capabilities are nowhere to come from This conversion is not safe

Similar rules apply to pointers and references A pointer (reference) of a base class can safely point

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 14

to an object of the derived class The derived class object can do everything the base pointer can demand and probably more This is safe However, the base pointer can invoke only part of the capabilities of the derived object.

B1 *p1; B2 *p2; Derived *d;

p1 = new Derived; p2 = new Derived; // OK: safe

d = new B1; d = new B2; // syntax errors

derived class pointer To avoid run-time errors, the compiler declares this code to be a syntax error

Similarly, a base pointer (which presumably points to a base object) cannot be copied into a derived class pointer (the fourth line of code in the example) This is not safe: The derived pointer might require services that the base object cannot perform, and the compiler will not be able to catch that This is why this pointer manipulation is also considered a syntax error

What do you do if you know that the base pointer indeed points to a derived class object rather than

to a base object? The same thing you do with single inheritance¡Xyou tell the compiler that you know what you are doing by means of a cast (the last line in the example) Since the compiler

cannot check you, it accepts your assurance You had better be right

The same rules apply to parameter passing If a function expects a pointer (or a reference) to one of the base classes, it is safe to call this function, passing to it the address of a derived object instead

foo1(&d); foo2(&d); // both are OK: safe conversion

foo(b1); foo(b2); // syntax errors: unsafe conversion

foo((Derived*)b1); foo((Derived*)b2); // pass at your own risk

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 15

In the last example, functions foo1() and foo2() can accept Derived objects as actual arguments because inside these functions their parameters respond to base messages only (f1() and f2(),) and derived objects can respond to these services Function foo() cannot accept base pointers

base objects cannot do that On the other hand, pointers b1 and b2 point to Derived class objects that can do the job To tell the compiler about this, the last line of the code above explicitly casts the base pointer to the Derived pointer

With the private or protected mode of inheritance, no implicit conversion from objects of the

derived class into objects of its base classes is allowed Even in this "safe" case, an explicit cast by client code is needed (because this case is not "safe" anymore) Conversion from any base class to a derived class requires an explicit cast for any mode of multiple inheritance

Multiple Inheritance: Constructors and Destructors

A derived class is responsible for the state of its components inherited from the base classes As with single inheritance, the base class constructors are called when an object of a derived class is constructed

The mechanism for passing parameters to the base class constructors is similar to single

inheritance: The member initialization list should be used In the next example, the base class B1

constructor with three parameters so that it can pass data to its B1 and B2 components and to its own data member

class Deived: public B1, public B2 {

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 16

In the member initialization list, the Derived class constructor calls base constructors using class

(literal values are also okay)

Derived::Derived(const char *s, double d, int i) : B1(i),B2(d)

{ if ((t = new char[strlen(s)+1]) == NULL)

{ cout << "\nOut of memory\n"; exit(1); }

strcpy(t,s); }

All base class constructors are called before the derived class constructor is called They are called

in the order in which the base classes are listed in the derived class declaration, not in the order of the initialization list of the derived class constructor

Similarly to single inheritance, data members of the derived class can be initialized either in the body of the derived class constructor or in the member initialization list

When a derived class object is destroyed (dynamically or by going out of scope), the derived class destructor is called first

Then the base class destructors are called in an order that is the reverse of constructor invocations

Multiple Inheritance: Ambiguities

The use of multiple inheritance might result in name conflicts If the derived class has a data

member or a member function of the same name as one of the base classes, the base class service is hidden by the name defined in the derived class

the base class B1; in addition, the Derived class has member function f2() with the same name as

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 17

a member function in the base class B2.

void f2(); ¡K }; // hidden by Derived::f2()

class Derived: public B1, public B2 {

In this example, the object of the Derived class has two data members x; the data member

inherited from B1 is hidden in Derived; the member function f2() inherited from base B2 is

Both the client code and the Derived class code can override the scope rules by using the explicit scope operator

Base class member names may also clash This is a more common occurrence and a more difficult one to deal with because base classes are often developed independently from each other and there

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 18

is little opportunity to coordinate their development to avoid name conflicts.

If two base classes have a data member or a member function with the same name, the derived class object has both copies The language provides no predefined precedence rules for access to data and functions If needed, we resolve ambiguities with the use of explicit qualification; the scope operator has to be used both by the client and by the derived class

that the client code cannot use either of them unless the client code provides an explicit indication

of which one to use

This technique for eliminating ambiguity flies in the face of principles of object-oriented

programming The problem is resolved by adding more responsibility to the client code rather than

to its server class Make sure that you spot these kinds of designs and avoid them

A better approach would be to ask the Derived class to insulate the client code from ambiguity in member function names

Trang 19

This is a much better solution It is the server code (class Derived) that goes to the trouble of

dealing with the problem The client code is insulated from the problem After all, when the client code uses class Derived as its server, the client code should not be exposed to the details of the server design¡Xwhat it inherits, from whom it inherits, what conflicts it has to deal with, and so on All that the client code should know is how to call the services f1(), f2(), and f3() to get the job done

If two (or more) base classes have a data member with the same name (of the same or different types), the derived class object has both copies, and this results in ambiguity

Conflicts between names of data members should be resolved by the derived class to avoid

ambiguities and to protect the client code The scope operator has to be used

Trang 20

Multiple Inheritance: Directed Graph

This is the most insidious form of ambiguity It happens when a base class is inherited from more than once In general, C++ is against that, and a class may explicitly appear only once in a

derivation list for a derived class

The situation with data member m is worse Each object of class Derived has two instances of this

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 21

data member; one is inherited through class B1 and the other through B2. Space required for

multiple instances of the same base data member is wasteful Also, these two data members are

part of Derived, and another serves the needs of the B2 part of Derived.

C++ comes with an interesting fix for this problem It gives the programmer an opportunity to

explicitly state that using two (or more) copies of the same data and functions is undesirable I wish this were the default case, and people who like puzzles would have the right to request that using two copies of the same data and functions is desirable

modifies declarations of the derived classes, which are later used in multiple inheritance

Now the Derived class has only one copy of data and functions inherited from class B. Notice that

to eliminate the problems in the Derived class, it is the designers of its base classes B1 and B2 that have to define these classes as virtual This flies in the face of the principle that base classes do not know about their derived classes; it is derived classes that know about their bases

keyword virtual used for virtual functions They are totally different It would be nice to have two different keywords It would probably be even nicer not to have multiple inheritance

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 22

Multiple Inheritance: Is It Useful?

I am not sure I can answer this question impartially, but I feel that the complexity of design with multiple inheritance (and I discussed here only part of what exists) outweighs the advantages of its use

So, what do you do when you feel you have to design a class that provides to its clients the union of services provided by other classes? My answer to this question is: Use composition, or use

composition with inheritance

Consider again the first example of inheritance that I discussed in the beginning of this section The goal of this design is to provide the client code with the ability to call functions f1(), f2(), and

f3(). Functions f1() and f2() are already implemented in classes B1 and B2. Function f3()

class B1

{ public:

void f1(); // public service f1()

¡K }; // the rest of class B1

class B2

{ public:

void f2(); // public service f2()

¡K }; // the rest of class B2

Derived.

class Derived : public B1, public B2 // two base classes

{ public: // f1(), f2() are inherited

void f3(); // f3() is added to services

¡K }; // the rest of class Derived

Instead, you can create class Derived inheriting function f1() from class B1. To provide clients of

Derived with the function f2(), you make a field of class B2 a member of class Derived.

class Derived : public B1 { // single inheritance

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 23

d.f2(); // passed from B2 through Derived

d.f3(); // added in the Derived class

It goes without saying that this client code should not go into the details of how its server class Derived is designed It provides the required services, and this is all that is needed¡Xwithout the complexity of multiple inheritance

Summary

In this chapter, we looked at advanced uses of inheritance They all rotate around the fact that base classes and derived classes have some capabilities in common; hence, objects of one class can be used instead of objects of another class, at least in some circumstances

You saw that it is always safe to use an object of the derived class where an object of a base class is expected This conversion is safe, but it is not very interesting This derived object will be asked to

do only things that a base object could do, and the derived class object can do much more

However, the same is true about pointers (and references), and this is much more interesting This means that you can use a pointer of the base class where a derived class pointer is expected, that is, you can point to derived class objects using base class pointers

Why would you want to do that? The most common context for doing that is processing a

collection of objects of different classes

This has always been a problem in programming languages All of our collections that are

supported by modern languages are homogeneous C++ arrays cannot contain components of

different classes C++ linked lists cannot use nodes of different types It is only the use of

inheritance that allows you to use collections of objects of different classes These classes are not totally different The heterogeneous lists discussed in this chapter cannot contain objects of

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 24

arbitrary classes But they can contain objects of classes related by inheritance.

When a heterogeneous collection of objects (related by inheritance) is processed, four types of messages are sent to the objects in the collection

ϒΠ Messages that every object in the collection can respond to: These are methods defined

in the base class of the inheritance hierarchy and are not overwritten in derived classes

ϒΠ Messages that only some kinds of objects in the collection can respond to: These are methods defined in derived classes of an inheritance hierarchy without a message with the same name in the base class

ϒΠ Messages whose names all kinds of objects in the collection can respond to but which are defined as non-virtual functions in base and derived classes (with the same or different interface)

ϒΠ Messages that each kind of object in the collection can respond to, which are defined as virtual functions using the same interface in the base and derived classes

The first type of message can be accessed using the base class pointer; no conversion is necessary when the object in the collection is accessed

The second kind of message can be sent using only derived class pointers: When the object is taken from the collection, the base pointer has to be converted to the pointer of the class to which the object belongs; only then will the message of the second kind become accessible This conversion

is not safe, and you should know what you are doing (that the object can respond to this message) because the compiler is not able to protect you

The third kind of message also requires conversion if the object should respond to a message

defined in the derived class The base class message is hidden by the derived class message

The fourth kind of message does not require conversion Even though these messages are sent using the base class pointer, they are interpreted by the run-time system according to the type of the

object the pointer points to (dynamic binding) The design that uses virtual functions encapsulates the algorithms that are performed differently for different kinds of objects into the functions with the same name

The use of virtual functions entails some space and performance penalty Each object of the class that uses virtual functions has a hidden data member that specifies the kind of object or a pointer that points to the table with addresses of available virtual functions Each time the virtual function

is called, this pointer is used to find the required object code, and this extra step of indirection adds

to the execution time This penalty is not large and for most applications is not an issue

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 25

We also took a look at multiple inheritance I think that the complexity of this topic might equal the complexity of the rest of the language Meanwhile, its practical utility is small Everything that can

be done with the use of multiple inheritance can be achieved as a combination of inheritance and composition Use multiple inheritance sparingly if at all

With virtual functions, it is a different story They are immensely popular in C++ programming They are so popular that I am afraid it is not politically correct to argue against their use

Remember, however, that the virtual function mechanism is fragile You have to use public

inheritance You have to use the same name in all classes in your inheritance hierarchy You have

to use exactly the same parameter list, return value, and even const modifiers Have a small

inconsistency, and your program will call a totally different function just because it has the same name, and you might not notice it

This being said, use virtual functions everywhere the processing of different kinds of related

objects can be reasonably described using the same function name

Chapter 16 Advanced Uses of Operator Overloading

Topics in this Chapter

ϒΠ Operator Overloading: A Brief Overview

operator overloading in C++ For some, any operator overloading is already quite eccentric For these programmers, what you will learn in this chapter might appear remote from what most C++ programmers do every day

This might be true¡Xyou do not write advanced overloaded operators often But this topic is

definitely not remote from what many C++ programmers use every day Advanced operators are an

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 26

important component of any C++ library, standard or nonstandard, and it is useful to understand how they work It is certainly not your first priority in studying C++ But these operators are

intellectually challenging and aesthetically exciting You will surely be rewarded for the time and energy spent on studying them

Operator Overloading: A Brief Overview

An overloaded operator is a programmer-defined function that has a special name (composed of the

the names of operator functions, overloaded operator function, or just operators They provide the convenience of operator syntax for manipulating objects of programmer-defined classes

The driving force behind the incorporation of overloaded operators into C++ is the desire to treat objects of programmer-defined types in the same way as variables of built-in types are treated If

If you can add three numeric values, you should be able to add three Account objects, and so on The overloaded operators enable you to do that

It is the C++ language that defines the meaning of built-in operators over built-in types For

overloaded operators, the meaning is defined, not by the language, but by the programmer This meaning should not be arbitrary: it should depend on the nature of objects being added, multiplied and so on But the programmer has significant freedom in defining the operator meaning, and this can easily lead to abuse¡Xto the design of overloaded operators whose meaning is not intuitively clear A good example of such abuse is the unary operator plus that I designed in Chapter 10 for displaying the fields of a Complex number (see Listing 10.4) Used in the client code, this operator

display the fields of the object x in a specified format

For the freedom to choose any content of overloaded operators, we pay with the lack of freedom in

followed by any legal C++ operator (two-symbol operators like == or += are allowed)

There are five exceptions to this rule: operators " . ", " .* ", " :: ", " ?: " and " sizeof "

Overloaded operators can be defined as either class members (and hence used as messages) or level global functions (usually, friends of the class whose objects are used as operands of

top-overloaded operators) If an operator is top-overloaded as a class member, it can have whatever

arguments are appropriate

The target of the message will be used as the first operand If an operator is overloaded as a global function, it has to have at least one class argument; it cannot have arguments of built-in types only

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 27

Operators overloaded in a base class are inherited in derived classes Obviously, these operators cannot access members defined in derived classes, because the derived class members are outside

of the scope of the base class and hence cannot be accessed from the base class methods The

overloaded assignment operator is an anomaly¡Xit is not inherited by the derived classes, or, rather,

it can access only the base part of the derived object, not the derived part of the derived object Hence, each class in the inheritance hierarchy has to define its own assignment operator if needed

Operator precedence for overloaded operators is the same as for their built-in counterparts For example, the multiplication operator always has a higher precedence than does the addition

operator, whatever their meaning is for the programmer-defined class The expression syntax for overloaded operators is the same as for corresponding built-in operators For example, binary

operators always occur between their two arguments, whether they are built-in or overloaded

(However, in this chapter you are going to see some exceptions to this rule.)

Arity (the number of operands) for overloaded operators is the same as for corresponding built-in operators Binary operators remain binary; they require two operands As global nonmember

functions (e.g., friends), binary overloaded operators must have two parameters As class member functions, binary overloaded operators have only one parameter, because another parameter

becomes the target object of the message

Similarly, unary built-in operators remain unary when they are overloaded If a unary overloaded operator is implemented as a global nonmember unary operator (e.g., a friend), it will have one parameter If this overloaded operator is defined as a member function (which is sent as a message

to a target object), it will have no parameters

As a simple example, consider class Account similar to one discussed in Chapter 13, "Similar

Classes: How to Treat Them." The class maintains information about the owner name and the

current balance and supports services that allow the client code to access the values of object data members and carry out deposits and withdrawals

In addition to four inline member functions, the class has a general constructor The class does not need a default constructor, since Account objects will be created on the heap only when they are needed The default constructor might be useful only if class objects were created in advance, when the name of the owner and the initial balance are not yet known

Since the class manages memory dynamically, it would be a good idea to add to it the copy

constructor and the assignment operator or, better yet, make the prototypes of these member

functions private (see Chapter 11 for more details) For the simplicity of the example, I am not doing that here because I am not passing Account objects by value, I do not initialize one Account

object from the data of another Account object, and I do not assign one Account object to another

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 28

Account object In real life, it would be important to protect Account objects from accidental

Account(const char* name, double initBalance) // general

{ owner = new char[strlen(name)+1]; // allocate heap space

if (owner == 0) { cout << "\nOut of memory\n"; exit(0); }

strcpy(owner, name); // initialize data fields balance = initBalance; }

double getBal() const // common for both

void withdraw(double amount)

{ balance -= amount; } // pop responsibility up void deposit(double amount)

{ balance += amount; } } ; // increment

unconditionally

I am going to create an array of Account pointers, create Account objects dynamically, initialize them, search for an account that belongs to a given owner, and deposit and withdraw funds Again, for simplicity of the example, I will use hardwired data rather than data loaded from an external file

Listing 16.1 shows the source code for the example Function createAccount() creates an

Account object dynamically, calls the Account constructor with two parameters, and returns the pointer to the newly allocated object Function processRequest() sets up the ios flags for

printing floating point numbers in the fixed format and with the trailing zeros, searches the

customer name within the objects and prints a message if the name is not found Otherwise, the function prompts the user for the transaction code, requests the transaction amount and performs the transaction (deposit or withdrawal)

The main() function defines an array of Account pointers and calls createAccount() to created

Account objects In a loop, it prompts the user to enter the customer name and calls

processRequest() to process the transaction An example of the program run is shown in Figure

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 29

Figure 16-1 Output for program in Listing 16.1.

Example 16.1 Example of handling class Account with programmer-named methods.

Account(const char* name, double initBalance) // general

{ owner = new char[strlen(name)+1]; // allocate heap space

if (owner == 0) { cout << "\nOut of memory\n"; exit(0); }

strcpy(owner, name); // initialize data fields

Trang 30

void withdraw(double amount)

{ balance -= amount; } // pull responsibility up

void deposit(double amount)

{ balance += amount; } // increment

unconditionally

} ;

Account* createAccount(const char* name, double bal)

{ Account* a = new Account(name,bal); // account on the heap

if (a == 0) { cout << "\nOut of memory\n"; exit(0); }

return a; }

void processRequest(Account* a[], const char name[])

{ int i; int choice; double amount;

cout.setf(ios::fixed,ios::floatfield);

cout.precision(2);

for (i=0; a[i] != 0; i++)

{ if (strcmp(a[i]->getOwner(),name)==0) // search for name

{ cout << "Account balance: " << a[i]->getBal() << endl;

cout <<"Enter 1 to deposit, 2 to withdraw, 3 to cancel: ";

cin >> choice; // transaction type

if (choice != 1 && choice != 2) break; // get out

cout << "Enter amount: ";

cin >> amount; // transaction amount switch (choice) { // select further path case 1: a[i]->deposit(amount); // unconditional

break;

case 2: if (amount <= a[i]->getBal()) // enough funds?

a[i]->withdraw(amount);

else

cout << "Insufficient funds\n";

break; } // end of switch scope cout << "New balance: "<< a[i]->getBal() << endl; // OK

break; } } // end of search loop

if (a[i] == 0)

{ cout << "Customer is not found\n"; } }

int main()

{

Account* accounts[100]; char name[80]; // program data

accounts[0] = createAccount("Jones",5000); // create objects

accounts[1] = createAccount("Smith",3000);

accounts[2] = createAccount("Green",1000);

accounts[3] = createAccount("Brown",1000);

accounts[4] = 0;

while (true) // process requests

{ cout << "Enter customer name ('exit' to exit): ";

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 31

cin >> name; // accept name

if (strcmp(name,"exit")==0) break; // test for end

processRequest(accounts, name); // next transaction }

return 0;

}

In this example, class Account relies on its client code to check whether the withdrawal transaction

is legitimate The advantage of this approach is that the Account member functions are not

involved in the user interface; they are responsible for the access to Account data members only The disadvantage of this approach is that the data is popped up to the client code for further

handling instead of pushing responsibility down to the server code The reason why I chose this design is that it lends itself better to using overloaded operators

and withdraw(). All that it takes to convert them into operator functions is to cut out their current

other changes are necessary

void operator -= (double amount)

{ balance -= amount; } // client tests feasibility

void operator += (double amount)

{ balance += amount; } // increment unconditionally

Instead of calling the deposit() and withdraw() member functions, the client function

processRequest() will be able to use the expression syntax where the operator is inserted

between the left operand (message target) and the right operand (message parameter)

switch (choice) {

case 1: *a[i] += amount; // a[i]->deposit(amount);

break;

case 2: if (amount <= a[i]->getBal())

*a[i] -= amount; // a[i]->withdraw(amount);

else

cout << "Insufficient funds\n";

break; }

Notice that the target of the message is an Account pointer, and hence it has to be dereferenced

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 32

when used in expressions This is an inconvenience but is not very serious At least not more

serious than deciding whether you should use the dot selector operator (when the message target is

an object or a reference) or the arrow selector operator (when the target is a pointer)

The real meaning of the expression syntax is, of course, a function call, a message sent to the left

Listing 16.2 shows the program that uses the overloaded operator functions instead of named methods It is similar to the program in Listing 16.1 Before starting the interacting stage of processing, the main() function calls the function printList() that goes over the list of Account

programmer-pointers and prints the contents of the objects pointed to by the programmer-pointers (see Figure 16-2) Notice the statements that format the names to be printed left justified and the account balances to be

printed right justified

Similar to processRequest(), the function printList() iterates through the list until the null pointer (this pointer plays the role of the sentinel value) is found in the array Notice the difference

in the loop headers in these two functions In printList(), the index i is local to the loop In

processRequest(), the index is global to the loop (It is local to the function scope.) The reason for the difference is that the index value is not needed after the loop in printList(): Iterations are

stopped before the end of the list is reached (if the name is found), and processRequest() needs

to know about it

Example 16.2 Example of handling class Account with overloaded operator methods.

Account(const char* name, double initBalance) // general

{ owner = new char[strlen(name)+1]; // allocate heap space

if (owner == 0) { cout << "\nOut of memory\n"; exit(0); }

strcpy(owner, name); // initialize data fields

Trang 33

{ return balance; }

const char* getOwner() const // protect data from changes

{ return owner; }

void operator -= (double amount)

{ balance -= amount; } // pull responsibility up

void operator += (double amount)

{ balance += amount; } // increment

unconditionally

} ;

Account* createAccount(const char* name, double bal)

{ Account* a = new Account(name,bal); // account on the heap

if (a == 0) { cout << "\nOut of memory\n"; exit(0); }

return a; }

void processRequest(Account* a[], const char name[])

{ int i; int choice; double amount;

cout.setf(ios::fixed,ios::floatfield);

cout.precision(2);

for (i=0; a[i] != 0; i++)

{ if (strcmp(a[i]->getOwner(),name)==0 // search for name

{ cout << "Account balance: " << a[i]->getBal() << endl;

cout <<"Enter 1 to deposit, 2 to withdraw, 3 to cancel: ";

cin >> choice; // transaction type

if (choice != 1 && choice != 2) break;

cout << "Enter amount: ";

cin >> amount; // transaction amount switch (choice) {

case 1: *a[i] += amount; //

a[i]->operator+=(amount);

break;

case 2: if (amount <= a[i]->getBal())

*a[i] -= amount; //

a[i]->operator-=(amount);

else

cout << "Insufficient funds\n";

break; } // end of switch scope cout << "New balance: "<< a[i]->getBal() << endl;

break; } } // end of search loop

if (a[i] == 0)

{ cout << "Customer is not found\n"; } }

void printList (Account* a[])

{ cout << "Customer List:\n\n";

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 34

for (int i=0; a[i] != 0; i++)

{ cout.setf(ios::left, ios::adjustfield); cout.width(30);

cout << a[i]->getOwner();

cout.setf(ios::right, ios::adjustfield); cout.width(10);

cout << a[i]->getBal() << endl; }

cin >> name; // accept name

if (strcmp(name,"exit")==0) break; // test for end processRequest(accounts, name); // next transaction }

return 0;

}

Implementing overloaded operators as global functions is simple: The target of the message becomes the first function parameter Instead of data members of the target object, the operators use data members of the first parameter Here are the two operators implemented as global

functions

void operator -= (Account &a, double amount) // global function

{ a.balance -= amount; } // pop responsibility up

void operator += (Account &a, double amount)

{ a.balance += amount; } // increment

unconditionally

Figure 16-2 Output for program in Listing 16.2.

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 35

Since these two functions access nonpublic members of class Account, they should be declared as

work As I mentioned earlier, the modern approach to programming does not view additional

writing as a shortcoming if it results in more understandable code You write additional

declarations only once, but you (and others) read them many times in the course of program

development, testing, and maintenance

In this case, adding friend function declarations to the class clearly indicates that these functions belong to this class They belong to the class physically; that is, they cannot be used without objects

of class Account. They belong to the class conceptually; that is, they are part of operations

provided by the class The syntax of friend functions is different from the syntax of member

functions, but this is a minor technical peculiarity Frequent accusations against using friend

functions, breaking of encapsulation and development of extra dependencies between parts of the program, are nonissues for overloaded operator functions

Account(const char* name, double initBalance) // general

{ owner = new char[strlen(name)+1]; // allocate heap space

if (owner == 0) { cout << "\nOut of memory\n"; exit(0); }

strcpy(owner, name); // initialize data fields

Trang 36

const char* getOwner() const // protect data from changes

{ return owner; }

friend void operator-= (Account &a, double amount); // operators

friend void operator+= (Account &a, double amount);

} ;

The expression syntax in client code does not change with the switch from member function

operators to friend operators

switch (choice) {

case 1: *a[i] += amount; // operator+=(*a[i],amount);

break;

case 2: if (amount <= a[i]->getBal())

*a[i] -= amount; // operator-=(*a[i],amount);

else

cout << "Insufficient funds\n";

break; } // end of switch scope

The meaning of this code changes The expression syntax is still a syntactic sugar for a function call, but it is a call to a global function There is no need for the target object in the function call Instead, the object that participates in the operation is passed as the actual argument to the function This is how the compiler perceives this client code

switch (choice) {

case 1: operator+=(*a[i],amount); // a.k.a *a[i]+=amount;

break;

case 2: if (amount <= a[i]->getBal())

operator-=(*a[i],amount); // a.k.a *a[i]-=amount;

else

cout << "Insufficient funds\n";

break; } // end of switch scope

Notice that the actual argument has to be dereferenced because a[i] is a pointer to an Account

object, not an object itself The reference argument has to be initialized with the value of an object, not with the value of a pointer This is why this function call needs dereferencing

The use of overloaded operators offers us a very nice way to write client code, but it does not solve

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 37

any essential software engineering issues Everything that can be done with the use of overloaded operators can be done with the use of conventional member functions, as Listing 16.1 clearly attests to.

Unary Operators

Unary operators have only one operand They include increment and decrement operators, negation operators, logical and bitwise negation, positive and negative sign operators, cast, address, and dereference operators, and operators new, delete, and sizeof. All these operators (with the

exception of sizeof) can be overloaded

Of course, not every operator can have its own special meaning for every class In Chapter 10, I overloaded the positive sign operator for class Complex as an output operator, and this design is confusing This is why overloading of unary operators is not very popular However, there are some situations where these operators can contribute to intuitive understanding of the client code In this section, I will discuss several examples of overloaded unary operators

Increment and Decrement Operators

Increment and decrement operators are very popular in C++ They are especially popular in text processing, where incrementing (or decrementing) a pointer can be combined with access to the current character for processing

void printString(const char data[]) // text does not change

{ const char *p = data; // point to start of data

while (*p != 0) // go until the end of data

{ cout << *p; // print current character

++p; } // point to next character

cout << endl; }

In this example, the character array is passed to the global function (as a constant), and each

character is displayed in turn The pointer p is first set to point to the first character of the array

data[] and then is incremented until it points to the terminating zero Even though it looks like a very low-level control construct that increments the memory address, in reality this operation is rather abstract because it does not reveal the real details of storage management¡Xby how much the address is changed and whether it is actually incremented or decremented

For example, there is no guarantee that an argument array is located in memory from lower

addresses to higher addresses On my machine, physical addresses decrease to the end of the array

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 38

Hence, there is no guarantee that the contents of the pointer are actually increased when the

increment operator is applied to the pointer What it implies is that the pointer is set to access the next array component, no matter what size the component is (it can be larger than one) or in what direction the pointer moves

Nevertheless, this "open" access to array components by the client code is error prone Accesses to locations outside of array boundaries are not flagged as syntax errors at compile time These

accesses might crash the program at run time, they can quietly produce incorrect results, or they can quietly produce correct results until some later time when the use of memory changes and disaster strikes

Combining data and operations together in a class protects data from inept access from client

programmers and gives client programmers the tools for handling objects that prevent mistakes Here is an example of class String that is similar to one discussed in Chapter 11

class String {

int size; // string size

char *str; // start of internal string

void set(const char* s); // private string allocation

String& operator = (const String& s); // assignment

int getSize() const; // current string length

char* reset() const; } ; // reset to start of string

The class data member str points to a dynamically allocated array whose size is stored in the data

assignment operator This function accepts a character array (labeled as constant) as an argument,

allocated memory, and initializes dynamically allocated memory using the text array supplied as argument

void String::set(const char* s)

{ size = strlen(s); // evaluate size

str = new char[size + 1]; // request heap memory

if (str == 0) { cout << "Out of memory\n"; exit(0); }

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 39

strcpy(str,s); } // copy client data to heap

This is a typical design for dynamic memory management This private function is convenient because it encapsulates the operations common to the constructors and the assignment operator

constructor (which doubles as a default constructor because of the default parameter value), a copy constructor, an assignment operator, and a destructor The default constructor passes to set() an

Client code can define a String object as a noninitialized variable (the default constructor is

called), as an object initialized with a character array value (the conversion constructor is used), or

as an object initialized with the data from another existing String object (the copy constructor is called)

The assignment operator is intelligent It supports the assignment of an object to itself (by checking whether the this pointer points to the location of the actual argument) It deletes the existing heap

code that uses the expression syntax for multiple chain assignments (by returning the reference to the target String object) Notice that even though the body of the assignment returns the whole

String object (dereferenced pointer this), it is only a reference to the object that is

returned¡Xthere is no copying

String& String::operator = (const String& s)

{ if (this == &s) return *this; // no work if self-assignment

delete [ ] str; // return existing memory

set(s.str); // allocate/set new memory

return *this; } // to support chain assignment

Listing 16.3 shows the complete implementation of class String (together with inline member

String object can contain, and the second function returns the pointer to the internal string, so that the client code (functions printString() and modifyString()) can initialize the external pointer that points to the internal string

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Trang 40

These two client functions use the increment operator to retrieve and replace the symbols inside their parameter String objects The loop in printString() continues until the terminating zero is found in the internal string of the String parameter (The pointer p to the internal string character has to be dereferenced.) The loop in modifyString() continues until all characters in the

The main() function creates and initializes a String object, prints its contents, modifies its

contents, and then prints again Since the loop in modifyString() does not take into account the current size of the heap memory allocated to its String parameter, this results in memory

corruption The output of the program is shown in Figure 16-3

Figure 16-3 Example of memory corruption by program in Listing 16.3.

Example 16.3 Example of using the increment operator with a pointer to internal data.

#include <iostream>

using namespace std;

class String { //string size

int size; // string size

char *str; // start of internal string

void set(const char* s); // private string

String& operator = (const String& s); // assignment

int getSize() const; // current string

length

char* reset() const; } ; // reset to start of string

void String::set(const char* s)

{ size = strlen(s); // evaluate size

str = new char[size + 1]; // request heap memory

if (str == 0) { cout << "Out of memory\n"; exit(0); }

strcpy(str,s); } // copy client data to

Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com

Ngày đăng: 12/08/2014, 11:20

TỪ KHÓA LIÊN QUAN