Object-oriented programming (OOP) is based on three fundamental concepts: data abstraction, inheritance, and dynamic binding.
15.1 OOP: An Overview
The key idea behind OOP is polymorphism.
In C++, polymorphism applies only to reference or pointers to types related by inheritance.
Inheritance
Members defined by the base class are inherited by its derived classes.
Classes related by inheritance are often described as forming an inheritance hierarchy. There is one class, referred to as the root, from which all the other classes inherit, directly or indirectly.
In C++, a base class must indicate which of its functions it intends for its derived classes to redefine. Functions defined as virtual are ones that the base expects its derived classes to redefined. Functions that the base class intends its children to inherit are not defined as virtual.
Dynamic Binding
Dynamic binding lets us write programs that use objects of any type in an inheritance hierarchy without caring about the objects' specific types. E.G. P 559
In C++, dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class. The fact that a reference (or pointer) might refer to either a base- or a derived-class object is the key to dynamic binding. Calls to virtual functions made through a reference are resolved at run time: The function that is called is the one defined by the actual type of the object to which the reference refers.
15.2 Defining Base and Derived Classes
15.2.1 Defining a Base Class
class Item_base {
public:
Item_base (const std::string &book = "",
double sales_price = 0.0) :
isbn(book), price(sales_price) {}
std::string book() const {return isbn; }
//return total sales price for a specified number of items
//derived classes will override and apply different discount algorithms
virtual double net_price(std::size_t n) const
{ return n * price; }
virtual ~Item_base() {}
private:
std::string isbn;
protected:
double price;
};
Base-Class Member Functions
The net_price member function is preceded by keywordvirtual. The purpose of virtual is to enable dynamic binding.
By default, member functions are nonvirtual. Calls to nonvirtual functions are resolved at compile time.
Calls to virtual functions made through a reference are resolved at run time.
Any nonstatic member function, other than a constructor, may be virtual.
The virtual keyword appears only on the member-function declaration inside the class.
The virtual keyword may not be used on a function definition that appears outside the class body.
Access Control and Inheritance
A derived class has the same access as any other part of the program to the public and private members of its base class: It may access the public members and has no access to the private members.
The protected access label allows the member to be accessed by a derived object but may not be accessed by general users of the type.
15.2.2 protected Members
The protected access label has several properties:
1. Like private members, protected members are inaccessible to users of the class
2. like public members, the protected members are accessible to classes derived from this class.
3. A derived object may access the protected members of its base class only through a derived object. The derived class has no special access to the protected members of base type objects.
void Bulk_item::memfcn (const Bulk_item &d, const Item_base &b)
{
double ret = price; // ok: use this->price
ret = d.price; // ok: reference to price through an Bulk_item object
ret = b.price; // error: reference to price through an Item_base object
}
15.2.3 Derived Classes
To define a derived class, we use a class derivation list to specify the base class(es). A class derivation list names one or more base classes and has the form:
class classname: access-label base-class
Here access-label is one of public, protected, orprivate. base-class is the name of a previously defined class(es).
Access label determines the access to the inherited members. When we want to inherit the interface of a base class, then the derivation should be public.
Each derived object contains two parts: those members that is inherits from its base and those it defines itself. Typically, a derived class (re)defines only those aspects that differ from or extend the behavior of the base.
Defining a Derived Class
// discount is used when a special copies of same book is sold
class Bulk_item : public Item_base {
public:
//redefines base version so as to implement bulk purchase discount policy
double net_price(std::size_t) const;
private:
std::size_t min_qty;
double discount;
};
Each Bulk_item object contains four data elements: It inherits
isbn and
price from Item_base and defines
min_qty and
discount.
The Bulk_item class also needs to define a constructor.
Derived Classes and virtual Functions
Ordinarily, derived classes redefine the virtual functions that they inherit. If a derived class does not redefine a virtual, then the version it uses is the one defined in its base class.
A derived type must include a declaration for each inherited member it intends to redefine. The above Bulk_item will redefine net_price and use the inherited version of book.
With one exception, the declaration of a virtual function in the derived class must exactly match the way the function is defined in the base.
The exception applies to virtuals that return a reference(or pointer) to a type that is itself a base class. A virtual function in a derived class can return a reference(or pointer) to class that is publicly derived from the type returned by the base-class function.E.G. P564
Once a function is declared as virtual in a base class it remains virtual; nothing the derived classes do can change the fact that the function is virtual. When a derived class redefines a virtual, it may use the virtual keyword, but it is not required to do so.
Derived Objects Contain Their Base Classes as Subobjects
Functions in the Derived May Use Members from the Base
double Bulk_item::net_price(size_t cnt) const
{
if(cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
Because each derived object has a base-class part, classes may access the public and protected members of its base class as if those members were members of the derived class itself.
A Class Must Be Defined to Be Used as a Base Class
A class must be defined before it can be used as a base class.
class Item_base;
//error: Item_base must be defined
class Bulk_item : public Item_base {...}
This is because: Each derived class contains, and may access, the members of its base class. To use those members, the derived class must know what they are.
Using a Derived Class as a Base Class
A base class can itself be a derived class:
class Base {...};
class D1 : public Base {...};
class D2 : public D1 {...};
Each class inherits all the members of its base class. The most derived type inherits the members of its base, which in turn inherits the members of its base and so on up the inheritance chain. The most derived object contains a subobject for each of its
immediate-base and
indirect-base classes
Declaration of Derived Classes
If we need to declare (but not yet define) a derived class, the declaration contains the class name but does not include its derivation list:
// error: a forward declaration must not include the derivation list
class Bulk_item : public Item_base
// forward declaration of both derived and base class
class Bulk_item;
class Item_base;
15.2.4 virtual and Other Member Functions
To trigger dynamic binding, two conditions must be met:
1. only member functions that are specified as virtual can be dynamically bound.
2. the call must be made through a reference or a pointer to a base-class type.
Derived to Base Conversions
Because every derived object contains a base part, we can bind a base-type reference to the base-class part of a derived object. We can also use a pointer to base to point to a derived object. E.G. P 567
Treating a derived object as if it were a base is safe, because every derived object has a base subobject. Also, the derived class inherits the operations of the base class, meaning that any operation that might be performed on a base object is available through the derived object as well.
The crucial point about reference and pointers to base-class types is that the static type - the type of the reference or pointer, which is knowable at compile time -- and thedynamic type -- the type of the object to which the pointer or reference is bound, which is knowable only at run time -- may differ.
Calls to virtual Functions May Be Resolved at Run time.
The fact that the actual type of the object might differ from the static type of the reference or pointer addressing that object is the key to dynamic binding in C++.
When a virtual function is called through a reference or pointer, the compiler generates code to decide at run time which function to call. The function that is called is the one that corresponds to the dynamic type. E.G. P 568
Virtuals are resolved at run time only if the call is made through a reference or pointer. Only in these case is it possible for an object's dynamic type to be unknown until run time.
Nonvirtual Calls Are Resolved at Compile Time
Overriding the Virtual Mechanism
In some cases, we want to override the virtual mechanism and force a call to use a particular version of a virtual function. We can do so by using the scope operator:
Item_base *baseP = &derived;
// calls version from the base class regardless of the dynamic type of baseP
double d = baseP->Item_base::net_price(42);
This code forces the call to net_price to be resolved to the version defined in Item_base.The call will be resolved at compile time.
When a derived virtual calls the base-class version, it must do so explicitly using the scope operator. If the derived function neglected to do so, then the call would be resolved at run time and would be a call to itself, resulting in an infinite recursion.Virtual Functions and Default Arguments
If a call omits an argument that has a default value, then the value that is used is the one defined by the type through which the function is called, irrespective of the object's dynamic type. (default argument of a given call is determined at compile time)
When a virtual is called through a reference or pointer to base, then the default argument is the value specified in the declaration of the virtual in the base class. If a virtual is called through a pointer or reference to derived, the default argument is the one declared in the version in the derived class.
Using different default arguments in base and derived versions of the same virtual is almost guaranteed to cause trouble.
15.2.5 Public, Private, and Protected Inheritance
Access to members defined within a derived class is controlled in exactly the same way as access is handled for any other class.
Each class controls access to the members it defines. A derived class may further restrict but may not loosen the access to the members that it inherits.
If member is private in base class, then only base class and its friends can access the members
if member is public or protected, then the access label used in the derivation list determines the access level of that member in the derived class:
1. In public inheritance, the members of the base retain their access levels: the public members of the base are public members of the derived and the protected members of the base are protected in the derived.
2. In protected inheritance, the public and protected members of the base class are protected members in the derived class.
3. In private inheritance, all the members of the base class are private in the derived class.
Public: 可以被任意实体访问
Protected: 只允许子类及本类的成员函数访问
Private: 只允许本类的成员函数访问
Exempting Individual Members
When inheritance is private or protected, the access level of members of the base may be more restrictive in the derived class than it was in the base.
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base { ... };
To make size public in Derived we can add ausing declaration for it to a public section in Derived.
class Derived : private Base {
public:
//....
using Base::size;
protected:
using Base::n;
};
Now we make the
size member accessible to users and
n accessible to classes subsequently derived from Derived.
Default inheritance Protection Levels
The default inheritance access level differs depending on which keyword is used to define the derived class.
A class defined with class keyword has private inheritance. A class defined with struct keyword, has public inheritance:
// public access
struct D3 : Base {
...
}
// private access
class D4 : Base {
...
}
15.2.6 Friendship and Inheritance
A base or derived class can make other classes or functions friends.
Friendship is not inherited. Friends of the base have no special access to members of its derived classes. If a base class is granted friendship, only the base has special access. Classes derived from that base have no access to the class granting friendship.
Each class controls friendship to its own members.
E.G. P 576
15.2.7 Inheritance and Static Members
If a base class defines a static member, there is only one such member defined for the entire hierarchy. Regardless of the number of classes derived from the base class, there exists a single instance of each static member.
static members obey normal access control: If the member is private in the base class, then derived classes have no access to it. If the member is accessible, we can access the static member either through the base or derived class. As usual, we can use either the scope operator or the dot or arrow member access operators.
E.G. P 576