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 1Figure 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 2Table 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 3application 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 4derived 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 6public:
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 7Derived2 *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 8In 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 11Object. 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 12functions 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 13Then 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 14to 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 16In 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 17a 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 18is 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 20Multiple 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 21data 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 22Multiple 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 23d.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 24arbitrary 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 25We 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 26important 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 27Operators 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 28Account 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 29Figure 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 30void 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 31cin >> 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 32when 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 34for (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 35Since 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 36const 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 37any 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 38Hence, 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 39strcpy(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 40These 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