Essential C++ 5_Object-Oriented Programming

    The object-based class mechanisms of Chapter 4 cannot easily model both the commonality and the differences of these fourare-a-kind-of Book classes. Why? It's because this model does not provide support for specifying relationships among classes. For this kind of support, we need theobject-oriented programming model.

5.1 Object-Oriented Programming Concepts

    The two primary characteristics of object-oriented programming are inheritance and polymorphism.Inheritance allows us to group classes into families of related types, allowing for the sharing of common operations and data.Polymorphism allows us to program these families as a unit rather than as individual classes, giving us greater flexibility in adding or removing any particular class.

    Inheritance defines a parent/child relationship. The parent defines the public interface and private implementation that are common to all its children. Eachchild adds to or overrides what it inherits to implement its own unique behavior. An AudioBook child class, for example, in addition to the title and author it inherits from its parent Book class, introduces support for a speaker and a count of the number of cassettes. In addition, it overrides the inheritedcheck_out() member function of its parent.

     In C++, the parent is called the base class and the child is called thederived class. The relationship between the parent or base class and its children is calledaninheritance hierarchy(继承体系)

    Figure 5.1 pictures a portion of a possible library lending material class hierarchy. The root of theclass hierarchy is an abstract base class, LibMat. LibMat defines all the operations that are common to all the different types of library lending materials:check_in(),check_out(),due_date(),fine(),location(), and so on.LibMat does not represent an actual lending material object; rather, it is an artifact of our design. In fact, it is the key artifact. We call it anabstract base class.


In an object-oriented program, we indirectly manipulate the class objects of our application through a pointer or reference of an abstract base class rather than directly manipulate the actual derived class objects of our application. This allows us to add or remove a derived class without the need for any modification to our existing program. For example, consider the following small function:

void loan_check_in( LibMat &mat ) 
{ 
    // mat actually refers to a derived class object 
    // such as Book, RentalBook, Magazines, and so on ... 
    mat.check_in(); 

    if ( mat.is_late() ) 
         mat.assess_fine(); 

    if ( mat.waiting_list(); 
         mat.notify_available(); 
} 

    There are no LibMat objects in our application, only Book, RentalBook, AudioCDs, and so on. How does this function actually work? What happens, for example, when thecheck_in() operation is invoked through mat? For this function to make sense,mat must somehow refer to one of the actual class objects of our application each timeloan_check_in() is executed. In addition, the check_in() member function that is invoked must somehow resolve to thecheck_in() instance of the actual class objectmat refers to. This is what happens. The question is, how does it work?

    The second unique aspect of object-oriented programming ispolymorphism多态): the ability of a base class pointer or reference to refer transparently to any of its derived class objects. In ourloan_check_in() function, for example,mat always addresses some object of one of the classes derived from LibMat. Which one? That cannot be determined until the actual execution of the program, and it is likely to vary with each invocation ofloan_check_in().

    Dynamic binding (动态绑定)is a third aspect of object-oriented programming. 

    In non-object-oriented programming, when we write

mat.check_in(); 

    the instance of check-in() to be executed is determined at compile-time based onmat's class type.Because the function to invoke is resolved before the program begins running, this is calledstatic binding(静态绑定).

    In object-oriented programming, the compiler cannot know which instance ofcheck_in() to invoke. This can be determined only during program execution based on the actual derived class objectmat addresses each timeloan_check_in() is invoked. The actual resolution of which derived class instance ofcheck_in() to invoke each time must be delayed until run-time. This is what we mean by dynamic binding.

    Inheritance allows us to define families of classes that share a common interface, such as our library lending materials. Polymorphism allows us manipulate objects of these classes in a type-independent manner. We program the common interface through a pointer or reference of an abstract base class. The actual operation to invoke is not determined until run-time based on the type of the object actually addressed.(Yes, polymorphism and dynamic binding are supported only when we are using a pointer or reference. I say more about that later.)

5.2 A Tour of Object-Oriented Programming

    Let's implement a simple three-level class hierarchy to introduce the C++ language constructs and programming idioms that support object-oriented programming. We root our class hierarchy with the abstract LibMat base class. We derive a Book class from LibMat and in turn derive an AudioBook class from Book. We limit our interface to a single function, print(), together with a constructor and destructor. I've instrumented each member function to output its presence so that we can trace the behavior of the program.

    By default, a member function is resolved statically at compile-time.To have a member function resolved dynamically during run-time, we preface its declaration with thevirtual keyword. The LibMat class declares its destructor andprint() to be virtual:

class LibMat { 
public: 
   LibMat(){ cout << "LibMat::LibMat() default constructor!\n"; } 

   virtual ~LibMat(){ cout << "LibMat::~LibMat() destructor!\n"; } 
   virtual void print() const 
         { cout << "LibMat::print() -- I am a LibMat object!\n"; } 
};

    Let's now define a nonmember print() function that takes as a single parameter aconst LibMat reference:

void print( const LibMat &mat ) 
{ 
   cout << "in global print(): about to print mat.print()\n"; 

   // this resolves to a print() member function 
   //      based on the actual object mat refers to ... 
   mat.print(); 
} 

    Within our main() program, we repeatedly invoke print(), passing it, in turn, a LibMat class object, a Book class object, and an AudioBook class object as its parameter.Based on the actual objectmat refers to within each invocation ofprint(), the appropriate LibMat, Book, or AudioBookprint() member function is invoked. Our first invocation looks like this:

cout << "\n" << "Creating a LibMat object to print()\n"; 
LibMat libmat; 
print( libmat ); 

Here is a trace of its execution:

Creating a LibMat object to print() 

// the construction of Libmat libmat 
LibMat::LibMat() default constructor! 

// the handling of print( libmat ) 
in global print(): about to print mat.print() 
LibMat::print() -- I am a LibMat object! 

// the destruction of Libmat libmat 
LibMat::~LibMat() destructor! 

    I hope there is nothing surprising in this. The definition oflibmat is followed by the default constructor invocation. Withinprint(),mat.print() resolves toLibMat::print(). This is followed by the LibMat destructor invocation. 

    Things become a bit more surprising when we pass print() a Book object:

cout << "\n" << "Creating a Book object to print()\n"; 
Book b( "The Castle", "Franz Kafka" ); 
print( b ); 

Here is an annotated trace of the execution:

Creating a Book object to print() 

// the construction of Book b 
LibMat::LibMat() default constructor! 
Book::Book( The Castle, Franz Kafka )  constructor 

// the handling of print( b ) 
in global print(): about to print mat.print() 
Book::print() -- I am a Book object! 
My title is: The Castle 
My author is: Franz Kafka 

// the destruction of Book b 
Book::~Book() destructor! 
LibMat::~LibMat() destructor! 

    A first observation is that the virtual invocation throughmat.print() actually works! The function invoked isBook::print() and notLibMat::print(). The second interesting thing is that both the base and the derived class constructors are invoked when the derived class object is defined. (When the derived class object is destroyed, both the derived and the base class destructors are invoked.)

    How do we actually implement the derived Book class? To indicate that our new class is inheriting from an existing class, we follow its name with a colon (:) followed by the public keyword[1] and the name of the base class:

    Note: A base class can be specified as public, protected, or private. Public inheritance is the only form of inheritance covered in this text. For a discussion of protected and private inheritance see Section 18.3 of [LIPPMAN98].

class Book : public LibMat { 
public: 
   Book( const string &title, const string &author ) 
      : _title( title ), _author( author ){ 
      cout << "Book::Book( " << _title 
           << ", " << _author << " )  constructor\n"; 
   } 

   virtual ~Book(){ 
      cout << "Book::~Book() destructor!\n"; 
   } 

   virtual void print() const { 
      cout << "Book::print() -- I am a Book object!\n" 
           << "My title is: "  << _title  << '\n' 
           << "My author is: " << _author << endl; 
   } 

   const string& title()  const { return _title;  } 
   const string& author() const { return _author; } 

protected: 
   string _title; 
   string _author; 
}; 

    Theprint() instance within Bookoverrides the LibMat instance. This is the function invoked bymat.print(). The two access functionstitle() and author() are nonvirtual inline member functions. We haven't seen theprotected keyword before.A member declared as protected can be directly accessed by the derived classes but cannot be directly accessed by the general program.

    Let's next derive a specialized AudioBook class from our Book class. An AudioBook, in addition to a title and author, has a narrator. Before we look at its implementation, let's first passprint() an AudioBook class object:

cout << "\n" << "Creating an AudioBook object to print()\n"; 
AudioBook ab( "Man Without Qualities", 
              "Robert Musil", "Kenneth Meyer" ); 
print( ab ); 

    What should we expect from a trace of its execution? We should expect (1) thatAudioBook::print() is invoked throughmat.print() and (2)thatab is constructed by, in turn, the LibMat, Book, and AudioBook constructors. This is what the trace shows:

Creating an AudioBook object to print() 

// the construction of AudioBook ab 
LibMat::LibMat() default constructor! 
Book::Book( Man Without Qualities, Robert Musil )  constructor 
AudioBook::AudioBook( Man Without Qualities, Robert Musil, 
                      Kenneth Meyer )  constructor 

// the resolution of print( ab ) 
in global print(): about to print mat.print() 
// oops: need to handle a Book and an AudioBook! 
AudioBook::print() -- I am a AudioBook object! 
My title is: Man Without Qualities 
My author is: Robert Musil 
My narrator is: Kenneth Meyer 

// the destruction of AudioBook ab   // The order is opposite to constructor!!
AudioBook::~AudioBook() destructor! 
Book::~Book() destructor! 
LibMat::~LibMat() destructor! 

    How do we implement the AudioBook derived class? We have only to program those aspects of an AudioBook that are different from those of its base Book class: theprint() function, of course, support for the name of the AudioBook narrator, and the class constructor and destructor. The Book class data members and member functions that support the author and title can be used directly within the AudioBook class in the same way as if they had been defined within it rather than being inherited.

class AudioBook : public Book { 
public: 
   AudioBook( const string &title, 
             const string &author, const string &narrator ) 
      : Book( title, author ), 
        _narrator( narrator ) 
   { 
      cout << "AudioBook::AudioBook( " << _title 
          << ", " << _author 
          << ", " << _narrator 
          << " )  constructor\n"; 
   } 

   ~AudioBook() 
   { 
      cout << "AudioBook::~AudioBook() destructor!\n"; 
   } 
   virtual void print() const { 
      cout << "AudioBook::print() -- I am an AudioBook object!\n" 
          // note the direct access of the inherited 
          // data members _title and _author 
          << "My title is: "    << _title << '\n' 
          << "My author is: "   << _author << '\n' 
          << "My narrator is: " << _narrator << endl; 
   } 
   const string& narrator() const { return _narrator; } 
protected: 
   string _narrator; 
}; 

Users of the derived class need not distinguish between inherited members and members actually defined within the derived class. The use of both is transparent:

int main() 
{ 
   AudioBook ab( "Mason and Dixon", 
                 "Thomas Pynchon", "Edwin Leonard" ); 

   cout << "The title is "    << ab.title()    << '\n'     // actually, the definition of title() and author() are come from Book class!
        << "The author is "   << ab.author()   << '\n' 
        << "The narrator is " << ab.narrator() << endl; 
} 

5.4 Defining an Abstract Base Class

    In this section we redesign the num_sequence class of the preceding section into an abstract base class from which we inherit each of the numeric sequence classes. How do we go about that?

    The first step in defining an abstract base class is to identify the set of operations common to its children. For example, what are the operations common to all numeric sequence classes? These operations represent the public interface of the num_sequence base class. Here is a first iteration:

class num_sequence { 
public: 
    // elem( pos ): return element at pos 
    // gen_elems( pos ): generate the elements up to pos 
    // what_am_i() : identify the actual sequence 
    // print( os ) : write the elements to os 
    //check_integrity( pos ) : is pos a valid value? 
    // max_elems() : returns maximum position supported 
    int         elem( int pos ); 
    void        gen_elems( int pos ); 
    const char* what_am_i() const; 
    ostream&    print( ostream &os = cout ) const; 
    bool        check_integrity( int pos ); 
    static int  max_elems(); 
    // ... 
}; 

  elem() returns the element at the user-requested position.max_elems() returns the maximum number of elements supported by our implementation.check_integrity() determines whetherpos is a valid position.print() displays the elements. gen_elems() generates the elements for the sequence.what_am_i() returns a character string identifying the sequence.

    The next step in the design of an abstract base class is to identify which operations are type-dependent� that is, which operations require separate implementations based on the derived class type. These operations become the virtual functions of the class hierarchy.For example, each numeric sequence class must provide a separate implementation ofgen_elems().check_integrity(), on the other hand, is type-invariant. It must determine whetherpos is a valid element position. Its algorithm is independent of the numeric sequence. Similarly,max_elems() is type-invariant. All the numeric sequences hold the same maximum number of elements.

    Not every function is this easy to distinguish. what_am_i() may or may not be type-dependent depending on how we choose to implement our inheritance hierarchy. The same is true ofelem() andprint(). For now, we'll presume that they are type-dependent. Later, we'll see an alternative design that turns them into type-invariant functions. A static member function cannot be declared as virtual.

    The third step in designing an abstract base class is to identify the access level of each operation. If the operation is to be available to the general program, we declare it aspublic. For example,elem(),max_elems(), andwhat_am_i() are public operations.

    If the operation is not meant to be invoked outside the base class, we declare it asprivate. A private member of the base class cannot be accessed by the classes that inherit from the base class. In this example, all the operations must be available to the inheriting classes, so we do not declare any of them asprivate.

    A third access level,protected, identifies operations that are available to the inheriting classes but not to the general program.check_integrity() and gen_elems(), for example, are operations that the inheriting classes, but not the general program, must invoke. Here is our revised num_sequence class definition:

class num_sequence { 
public: 
    virtual ~num_sequence(){}; 

    virtual int         elem( int pos ) const = 0; 
    virtual const char* what_am_i() const = 0; 
    static  int         max_elems(){ return _max_elems; } 
    virtual ostream&    print( ostream &os = cout ) const = 0; 

protected: 
     virtual void        gen_elems( int pos ) const = 0; 
     bool                check_integrity( int pos ) const; 

     const static int    _max_elems = 1024; 
};

    Each virtual function either must be defined for the class that declares it or, if there is no meaningful implementation of that function for that class (such asgen_elems()), must be declared as apure virtual function. The assignment of 0 indicates that the virtual function is pure:

virtual void gen_elems( int pos ) = 0; 

    Because its interface is incomplete, a class that declares one or more pure virtual functions cannot have independent class objects defined in the program. It can serve only as the subobject of a derived class, which, in effect, completes it by providing concrete implementations for each of its pure virtual functions.

What data, if any, should the num_sequence class declare? There is no hard and fast rule. In this class design, num_sequence does not declare any class data members. This design provides an interface for the numeric sequence hierarchy but defers the implementation to its derived classes.

What about constructors and a destructor? Because there are no nonstatic data members within the class to initialize, there is no real benefit to providing a constructor. We will, however, provide a destructor.As a general rule, a base class that defines one or more virtual functions should always define a virtual destructor. For example,

class num_sequence { 
public: 
    virtual ~num_sequence(); 
    // ... 
}; 

    For completeness, here are the implementations of the num_sequence instance of the output operator and ofcheck_integrity():

bool num_sequence:: 
check_integrity( int pos ) const 
{ 
   if ( pos <= 0 || pos > _max_elems ) 
   { 
        cerr << "!! invalid position: " << pos 
             << " Cannot honor request\n"; 
        return false; 
   } 

   return true; 
} 

ostream& operator<<( ostream &os, const num_sequence &ns ) 
      {  return ns.print( os ); } 

    Although this completes the definition of the abstract num_sequence base class, the class itself is incomplete. It provides an interface for the subsequently derived classes.IEach derived class provides the implementation that completes the num_sequence base class definition.

5.5 Defining a Derived Class

     The derived class consists of two parts:the subobject of its base class (consisting of the nonstatic base class data members, if any) and the derived class portion (consisting of the nonstatic derived class data members). (Think of a blue Lego block snapped together with a red one.) This composite nature of the derived class is reflected in its declaration syntax:

// the header file contains the base class definition 
#include "num_sequence.h" 

class Fibonacci : public num_sequence { 
public: 
    // ... 
}; 

    The derived class name is followed by a colon, the public keyword, and the name of the base class.[3] The only rule is that the base class definition must be present before a class can inherit from it (this is why the header file containing the num_sequence class definition is included).

    The Fibonacci class must provide an implementation of each of the pure virtual functions inherited from its base class. In addition, it must declare those members that are unique to the Fibonacci class. Here is the class definition:

class Fibonacci : public num_sequence { 
public: 
   Fibonacci( int len = 1, int beg_pos = 1 ) 
            : _length( len ), _beg_pos( beg_pos ){} 

   virtual int         elem( int pos ) const; 
   virtual const char* what_am_i() const { return "Fibonacci"; } 
   virtual ostream&    print( ostream &os = cout ) const; 
   int                 length()  const { return _length;  } 
   int                 beg_pos() const { return _beg_pos; } 
protected: 
   virtual void       gen_elems( int pos ) const; 
   int                _length; 
   int                _beg_pos; 
   static vector<int> _elems; 
}; 

    In this design, length and beginning position are data members of each derived class. The read access functionslength() andbeg_pos() are declared as nonvirtual because there is no base class instance to override. Because they are not part of the base class interface, they cannot be accessed when we're programming through a base class pointer or reference. For example,

num_sequence *ps = new Fibonacci( 12, 8 ); 

// ok: invokes Fibonacci::what_am_i() through virtual mechanism 
ps->what_am_i(); 

// ok: invokes inherited num_sequence::max_elems(); 
ps->max_elems(); 

// error: length() is not part of num_sequence interface 
ps->length(); 

// ok: invokes Fibonacci destructor through virtual mechanism 
delete ps; 

    If the inaccessibility of length() and beg_pos() through the base class interface turns out to be a problem for our users, we'll need to go back and modify the base class interface. One redesign is to introducelength() and beg_pos() as pure virtual functions within the num_sequence base class. This automatically turns the derived class instances ofbeg_pos() andlength() into virtual functions. This is one reason that the derived class instances of a virtual function are not required to specify thevirtual keyword. If the keyword were required, retrofitting a base class virtual function such asbeg_pos() would be difficult to get right: Every derived class instance would need to be redeclared.

    An alternative redesign might be to factor the storage of the length and beginning position from the derived classes into the base class. In this way,length() andbeg_pos() become inherited inline nonvirtual functions. (We consider the ramifications of this design inSection 5.6.)

    My point in bringing this up is that the challenge of an object-oriented design is not so much in the programming as it is in the factoring of the base and derived classes and determining the interface and members that belong to each. In general, this is an iterative process that evolves through experience and feedback from users.

    Here is the implementation of elem(). The derived class virtual function must exactly match the function prototype of the base class instance. Thevirtual keyword is not specified in a definition that occurs outside the class.

int Fibonacci:: 
elem( int pos ) const 
{ 
   if ( ! check_integrity( pos )) 
        return 0; 

   if ( pos > _elems.size() ) 
        Fibonacci::gen_elems( pos ); 

   return _elems[ pos-1 ]; 
} 

    Notice thatelem() invokes the inherited membercheck_integrity() exactly as if it were a member of its class. In general, the inherited public and protected base class members, regardless of the depth of an inheritance hierarchy, are accessed as if they are members of the derived class. The public base class members are also public in the derived class and are available to users of the derived class.The protected base class members are protected within the derived class. They are available to classes inheriting from the derived class but not to users of the class. The derived class, however, has no access privilege to the private base class members.

    Whenever a member of the derived class reuses the name of an inherited base class member, the base class member becomes lexically hidden within the derived class. That is, each use of the name within the derived class resolves to the derived class member. To access the base class member within the derived class, we must qualify its reference with theclass scope operator of the base class.

5.6 Using an Inheritance Hierarchy

    Let's presume we've defined the five other numeric sequence classes (Pell, Lucas, Square, Triangular, and Pentagonal) in the same manner as the Fibonacci class. We now have a two-level inheritance hierarchy: an abstract num_sequence base class and the six inheriting derived classes. How might we use them?

    Here is a simple display() function whose second parameter isns, a const reference to a num_sequence object.

inline void display( ostream &os, 
                     const num_sequence &ns, int pos ) 
{ 
   os << "The element at position " 
      << pos            << " for the " 
      << ns.what_am_i() << " sequence is " 
      << ns.elem( pos ) << endl; 
} 

    Within display(), we call the two virtual functionswhat_am_i() and elem(). Which instances of these functions are invoked? We cannot say for certain. We know thatns does not refer to an actual num_sequence class object but rather to an object of a class derived from num_sequence. The two virtual function calls are resolved at run-time based on the type of the class objectns refers to. For example, in the following small program I define an object of each derived class in turn and pass it todisplay():

int main() 
{ 
   const int pos = 8; 

   Fibonacci fib; 
   display( cout, fib, pos ); 

   Pell pell; 
   display( cout, pell, pos ); 

   Lucas lucas; 
   display( cout, lucas, pos ); 

   Triangular trian; 
   display( cout, trian, pos ); 

   Square square; 
   display( cout, square, pos ); 

   Pentagonal penta; 
   display( cout, penta, pos ); 
} 

    When compiled and executed, this program generates the following output:

The element at position 8 for the Fibonacci sequence is 21 
The element at position 8 for the Pell sequence is 408 
The element at position 8 for the Lucas sequence is 47 
The element at position 8 for the Triangular sequence is 36 
The element at position 8 for the Square sequence is 64 
The element at position 8 for the Pentagonal sequence is 92 

5.8 Initialization, Destruction, and Copy

    Now that num_sequence declares actual data members, we must provide for their initialization. We could leave it to each derived class to initialize these data members, but that's potentially error-prone. A better design is to provide a base class constructor to handle the initialization of all base class members.

    Recall that num_sequence is an abstract base class. We cannot define an independent object of its class; rather, the num_sequence serves as a subobject of each derived class object. For this reason, we declare the base class constructor to be a protected rather than public member.

    The initialization of a derived class object consists of the invocation of the base class constructor followed by that of the derived class constructor. It helps to think of the derived class object as consisting of multiple subobjects: a base class subobject initialized by the base class constructor and a derived class subobject initialized by the derived class constructor. In a three-level class hierarchy, such as the AudioBook class of Section 5.1, the derived class consists of three subobjects, each one initialized by its respective constructor.

    The design requirements of a derived class constructor are twofold: Not only must it initialize the derived class data members, but it must also supply the expected values to its base class constructor. In our example, the num_sequence base class requires three values that are passed to it using the member initialization list. For example,

inline Fibonacci:: 
Fibonacci( int len, int beg_pos ) 
         : num_sequence( len, beg_pos, &_elems ) 

{} 

    If we should overlook the invocation of the num_sequence constructor, the definition of the Fibonacci constructor is flagged as an error. Why? The num_sequence base class requires our explicit invocation of its three-argument constructor. In our design, this is what we want.

    Alternatively, we could provide a default num_sequence constructor. We must change _relems to a pointer, however, and add code to verify that it is non-null before each access of the vector:

num_sequence:: 
num_sequence( int len=1, int bp=1, vector<int> *pe=0 ) 
      : _length( len ), _beg_pos( bp ), _pelems( re ){} 

    Now, if the derived class constructor does not explicitly invoke the base class constructor, the default base class constructor is invoked automatically.

What happens when we initialize one Fibonacci class object with another?

Fibonacci fib1( 12 ); 
Fibonacci fib2 = fib1; 

    If an explicit copy constructor is defined, that instance is invoked. For example, we might define a Fibonacci copy constructor as follows:

Fibonacci::Fibonacci( const Fibonacci &rhs ) 
          : num_sequence( rhs ) 
{} 

 rhs, the right-hand derived class object, is passed to the base class copy constructor using the member initialization list. What if the base class does not define an explicit copy constructor? Nothing bad happens. Default memberwise initialization is carried out. If an explicit base class copy constructor is defined, it is invoked.

    In this case, an explicit Fibonacci copy constructor is unnecessary because the default behavior accomplishes the same results: First the base class subobject is memberwise initialized, followed by the memberwise initialization of the derived class members.















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值