第十四天(類繼承·一)

       這章,很長……不得不分兩天。下面的筆記只是這一章的一半。明天是星期天,我還有事要做,所以更新下半部分的預計是下星期。


2011-11-19(Class Inheritance I)
1. Foundation
When one class inherits from another, the original class is called a base class and the inheritance class is called derived class. To illustrate inheritance, our introduction is base on the following base class:

#ifndef TABTENN0_H_
#define TABTENN0_H_
class TableTennisPlayer
{
private:
    static const int LIM = 20;
    char firstname[LIM];
    char lastname[LIM];
    bool hasTable;
public:
    TableTennisPlayer (const char * fn = "none",
    const char * ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};
#endif
2. Deriving a class
Suppose class
RatedPlayer derives from the TableTennisPlayer class, the following code shows that relation:
class RatedPlayer : public TableTennisPlayer 
{
    //… …
};
The colon indicates that theRatedPlayer is based on the TableTennisPlayer class. Keyword public makes this derivation called public derivation. With public derivation, the public members of the base-class become public members of the derived class, The private portions of a base-class become part of the derived class, but they can be accessed only through public and protected methods of the base class.
After declaring
RatedPlayer class like that, its objects has the following special properties:
        #       A derived type object has stored the data members of base type. Precisely, the derived class inherits the base-class implementation;
        #       A derived type object can use the methods of the base-class. Precisely, the derived class inherits the base-class interface.
3. Follow-up work
As you can imagine, derived class has its own members that base-class doesn't. So derived class needs the following rights:
        #      Define its own constructors;
        #      Add additional data members and member functions as needed.
Suppose we add a data member called rating to the derived class RatedPlayer. The class declaration could look like this:
class RatedPlayer : public TableTennisPlayer
{
private:
	unsigned int rating;
public:
	RatedPlayer(unsigned int r = 0, const char * fn = "none",
		const char * ln = "none", bool ht = false);
	RatedPlayer(unsigned int r, const TableTennisPlayer& tp);
	unsigned int Rating(){return rating;}
	void ResetRating(unsigned int r){rating = r;}
};
4. Constructors Discussion
A derived class does not have direct access to the private member of the base class, it has to work through the base-class methods. So when a program constructs a derived class object, it first constructor the base-class object. C++ uses the member initializer list syntax to accomplish it:
RatedPlayer::RatedPlayer(unsigned int r,const char * fn ,
        const char * ln, bool ht):TableTennisPlayer(fn,ln,ht)
    { rating = r;}
The expression TableTennisPlayer(fn,ln,ht) is the initializer list. When constructing RatedPlayer objects, the RatedPlayer constructor passes the base-class members(fn, ln, and ht) on as actual arguments to the TableTennisPlayer constructor. After base-class object being created, the program enters the body of the RatedPlayer constructor, completes the construction of the RatedPlayer object.
        If you omit the member initializer list, the program will use the default base-class constructor. Therefore the following code:
RatedPlayer::RatedPlayer(unsigned int r,const char * fn ,
        const char * ln, bool ht)
    { rating = r;}
is the same as:
RatedPlayer::RatedPlayer(unsigned int r,const char * fn ,
        const char * ln, bool ht):TableTennisPlayer()
    { rating = r;}
If your base-class has no default constructor, it will be a compiled error.
        The second form of constructor: 
RatedPlayer::RatedPlayer(unsigned int r, const 
        TableTennisPlayer& tp): TableTennisPlayer(tp)
    { rating = r; }
Because tp is type const TableTennisPlayer&, this call invokes the base-class copy constructor. Now that RatedPlayer class has no copy constructor, the implicit copy constructor will be called. Even the implicit one does memberwise copy(or shallow copy), it is fine because the class doesn't use dynamic memory allocation.
        By the way, you can also use member initializer list syntax for members of the derived class:
RatedPlayer::RatedPlayer(unsigned int r, const 
        TableTennisPlayer& tp):TableTennisPlayer(tp), rating(r)
    {}
5. Destructor Discussion
Destroying an object occurs in the opposite order used to construct an object. That is, the body of the derived class destructor is executed first, and then the base-class destructor is called automatically.
6. Special Relationships Between Derived and Base Classes
Two important relationships are that a base-class pointer can point to a derived-class object without an explicit type cast and that a base-class reference can refer to a derived class object without an explicit type cast
:
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer& rt = rplayer;
TableTennisPlayer* pt = &rplayer;
rt.Name();   // invoke Name() with reference
pt->Name();  // invoke Name() with pointer
However, a base-class pointer or reference can invoke just base-class methods.
         On the contrary, you can't let a derived class type pointer or reference point or refer to a base class object.
         Using the first relationship, we can do something conveniently. Let's think about the following function prototype:

                    void show(TableTennisPlayer& player);
The parameter type is
TableTennisPlayer&, and as a base-class reference, it can refer to base-class or derived-class object. That is:
TableTennisPlayer tp(/*members info*/);
RatedPlayer rp(/*members info*/);
show(tp);
show(rp);
causes no problems. In depth, recalling the prototype of copy constructor and assignment operator:
                                            TableTennisPlayer(const TableTennisPlayer&);
            TableTennisPlayer& operator= (const TableTennisPlayer&);

Both of them have
TableTennisPlayer& type parameter, which means they can accept both TableTennisPlayer and RatedPlayer object reference. That is:
RatedPlayer rp(/*members info*/);
TableTennisPlayer tp1(rp);  //using the copy constructor
TableTennisPlayer tp2;
tp2 = rp;                   //using the assignment operator
causes no problems. But only the base-class portion of rp is copied(assigned) to tp1(tp2).
7. Inheritance: An Is-a Relationship
The special relationship between derived classed and base classes is based on an underlying model for C++ inheritance. Actually, C++ has three varieties of inheritance: public, protected and private. Public inheritance is the most common form, and it models an is-a relationship.
         When distinguishing two things, we usually compare one's features to another. If all features of one thing is equals to sectional features of another, we would describe their relationship as an is-a-kind-of relationship. Similarly, the classes world in C++, the features mentioned just now is members, including data members and methods member. Obviously, all features of base-class are the portion of derived-class. So, we describe the relationship between base and derived class as is-a-kind-of relationship, but is-a is the usual term.
8. Polymorphic Public Inheritance
The term polymorphic means the same methods(the same function name and argument list) in base and derived class have multiple behavior, depending on the context. There are two key mechanisms for implementing polymorphic public inheritance:
        #       Redefining base-class methods in a derived class;
        #      Using virtual methods.
For convenience, we declare a class called
AClass and its derived class subA as our discussion foundation:
#ifndef POLY_SAM_H_
#define POLY_SAM_H_
class AClass
{
private:
	int AMember1;
	int AMember2;
public:
	AClass(const int a1 = 5, const int a2 = 10);
	int getMember1()const{return AMember1;}
        int getMember2()const{return AMember2;}
	void setMember1(const int am){AMember1 = am;}
	void setMember2(const int am){AMember2 = am;}
	virtual void show()const;
	virtual ~AClass(){}
};

class subA : public AClass
{
private:
	int additional;
public:
	subA(const int ad = 0, const int a2 = 5, const int a3 = 10);
	subA(const int ad, AClass& ac);
	int getAdd()const{return additional;}
	int setAdd(const int ad){additional = ad;}
	virtual void show()const;
};
#endif
9. The Keyword virtual
Note that we use a new keyword
virtual as prefix of show() function, which determines which method is used if the method is invoked by a reference or a pointer. Specifically speaking, if you don't use the keyword virtual, the program choose a method based on the reference type or pointer type: 
AClass a(/*members info*/);
subA sa(/*members info*/);
AClass& aRef1 = a;
AClass& aRef2 = sa;
aRef1.show(); //call AClass::show();
aRef2.show(); //call AClass::show();
If you do use the keyword virtual, the program chooses a method based on the type of object the reference or pointer refers to. That is:
//… …, the same as the previous code
aRef1.show(); //call AClass::show();
aRef2.show(); //call subA::show();
The behavior of virtual function is very handy. Therefore, it is the common practice to declare as virtual in the base class those methods that might be redefined in a derived class. When a method is declared in a base class, it's automatically virtual in the derived class, but it is a good idea to point out which method is virtual method in the derived class.
       In the base-class, we declare and define a virtual destructor. Using keyword
virtual here isn't essential because the destructor do nothing. But once this destructor must do something, it would be vital for AClass class to have a virtual destructor:
AClass* ar[2];
ar[0] = new AClass(/*members info*/);
ar[1] = new subA(/*members info*/); 
delete ar[0], ar[1];
If the destructor is not virtual, only the AClass destructor will be called even if the ar[1] pointer pointed to subA object when executing the delete statement. Thus, using virtual destructor ensures that the correct sequence of destructors is called.
10. Static and Dynamic Binding
Interpreting a function call in the source code as executing a particular block of function code is termed binding, In C++, the compiler has to look at the function arguments as well as the function name to figure out which function to use. This kind if binding is a task a C++ compiler could perform during the compiling process. Generally, binding that takes place during compilation is called static binding(or early binding).
       Virtual functions make binding more complex and difficult. The decision of which function to use can't be made at compile time because the compiler doesn't know which kind of object the user is going to choose to make. Therefore, the compiler has to generate code that allows the correct virtual method to be selected as the program runs. This is called dynamic binding(or late binding).
      We can summarize that if a function is not virtual, the compiler will using static binding as default. Otherwise, using the dynamic binding. As you can imagine, if the default binging is dynamic, won't have to declare a polymorphic method as virtual. This confusion might disappear after answering the following questions:
       #      Why have two kinds of binding?
       #      If dynamic binding is so good, why isn't it the default?
       #      How does it work?
11. Answer The Two Questions
The title of this section is “two” not “three”, because the first two questions are have the same answers. In other word, if we explain why C++ have two kinds of binding, you would understand why the dynamic binding isn't the default.
I. Question One: Why Have Two Kinds Of Binding 
i. Efficiency.
Compare to programs that don't need dynamic binding, programs that need must do some extra processing. So static binding is more efficient than dynamic binding. One of the guiding principles of C++ is that you shouldn't have to pay in memory usage or processing time for features you don't use. For the these reasons, C++ choose static binding as default.
ii. Conceptual Model
When you design a class, it might have have member functions that you don't want redefined in derived class. By making these functions non-virtual, two things accomplished. First, you make your program more efficient. Second, you clearly announce that it is your intention that these functions not be redefined.
II. Question Two: How Virtual Functions Work
It's the compiler writer's responsibility to make virtual functions work, not programmers. But seeing how it is done may help you understand some concepts.
        The usual way compilers handle virtual functions is to add a hidden member to each object. This hidden member holds a pointer to an array of function addresses. Such an array is usually termed virtual function table(vtbl). The vtbl holds the addresses of the virtual functions. Only your class has the virtual functions, does the compiler adds the vtbl hidden member.
i. Processing of vtbl
        #      Redefine the virtual function of base-class in derived-class. Result: The vtbl would be updated. For instance, the virtual function addresses that
AClass objects' vtbl holds are:
                 ac.vtbl = {&AClass::~AClass(), &AClass::show()}
Derived class subA redefines the show() virtual function, which updates the
subA objects' vtbl:
                 sa.vtbl = {&AClass::~AClass(), &subA::show()}
        #      Define a new function an make it virtual. Result: Its address is added to the vtbl. For instance,
subA class defines a new virtual function called addMembers, then the vtbl of subA objects would become:
                 sa.vtbl = {&AClass::~AClass(), &subA::show(), &subA::addMember() }
ii. using vtbl
When call a virtual function, the program looks at the vtbl address stored in an object and goes to the corresponding table of function addresses. Then find out which functions is actually called.
12. Other Details About Virtual Functions
I. Destructor

A virtual destructor ensure you can destroy the right objects reference or pointer refer to. Even if your class doesn't require an explicit destructor, you shouldn't rely on the default destructor. Instead, you should provide a virtual destructor:
                 virtual ~Base_Class(){}
Practically, you should provide a base-class with a virtual destructor, even if the class doesn't need a destructor.
II. Friends
Friends can't be virtual functions because friends are not class members.
III. Overloading and Redefinition
What happen if we redefine a virtual function, at the same time we overload it? That is:
class AClass
{
    //… …
public:
    //… …
    virtual void show()const;
};

class subA : public AClass
{
    //… …
public:
    //… …
    virtual void show(int a)const;
};
It's seem to be make sence, but actually it causes a problem:
subA sa;
sa.show(5); //valid
sa.show();  //invalid 
Rather than resulting in two overload versions of the function, this redefinition hides the base-class version that takes a void argument. In short redefining inherited methods is not a variation of overloading.
When redefining an inherited method, you need to make sure you match the original prototype. One new exception to this rule is that if the return type is a reference or pointer to a base-class, it can be modified by a reference or pointer to derived-class. For example, the prototype of
show() function in base-class is:
                      AClass& show();
Then you can do this to overload it in derived-class:
                      subA& show();
Note that this exception applies only to return values, not to arguments.



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章