15.1 OOP: An Overview
The key ideas in object-oriented programming are data abstraction, inheritance, and dynamic binding. Using data abstraction, we can define classes that separate interface from implementation. Through inheritance, we can define
classes that model the relationships among similar types. Through dynamic binding, we can use objects of these types while ignoring the details of how they differ.
Inheritance
A derived class must specify the class(es) from which it intends to inherit. It does so in a class derivation list, which is a colon followed by a comma-separated list of base classes each of which may have an optional access
specifier:
class Bulk_quote : public Quote { // Bulk_quote inherits from Quote
public:
double net_price(std::size_t) const override;
};
Dynamic Binding
In C++, dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class.
The derived class needs to override the definition it inherits from the base class, by providing its own definition. The base class defines as virtual those functions it expects its derived classes to override. When we call a virtual function through a pointer or reference, the call will be dynamically bound.
The virtual keyword appears only on the declaration inside the class and may not be used on a function definition that appears outside the class body. A function that is declared as virtual in the base class is implicitly virtual in the derived classes as well.
Member functions that are not declared as virtual are resolved at compile time, not run time. Thus, there is no question as to which function to run when we call isbn().
A derived class must specify from which class(es) it inherits. It does so in its class derivation list, which is a colon followed by a comma-separated list of names of previously defined classes. Each base class name may be preceded by an optional access specifier, which is one of public, protected, or private.
A derived class must declare each inherited member function it intends to override.
When the derivation is public, the public members of the base class become part of the interface of the derived class as well.
Derived classes frequently, but not always, override the virtual functions that they inherit. If a derived class does not override a virtual from its base, then, like any other member, the derived class inherits the version defined in its base class.
The new standard lets a derived class explicitly note that it intends a member function to override a virtual that it inherits. It does so by specifying override after the parameter list, or after the const or reference qualifier(s) if the member is a const or reference function.
Derived-Class Objects and the Derived-to-Base Conversion
Because a derived object contains subparts corresponding to its base class(es), we can use an object of a derived type as ifit were an object of its base type(s). In particular, we can bind a base-class reference or pointer to the base-class part of a derived object.
Quote item; // object of base type
Bulk_quote bulk; // object of derived type
Quote *p = &item; // p points to a Quote object
p = &bulk; // p points to the Quote part of bulk
Quote &r = bulk; // r bound to the Quote part of bulk
This conversion is often referred to as the derived-to-base conversion. As with any other conversion, the compiler will apply the derived-to-base conversion implicitly.
Derived-Class Constructors
The base-class part of an object is initialized, along with the data members of the derived class, during the initialization phase of the constructor.
Bulk_quote(const std::string& book, double p,std::size_t qty, double disc) :Quote(book, p), min_qty(qty), discount(disc) { }
// as before
};
Using Members of the Base Class from the Derived Class
If a base class defines a static member (§7.6, p. 300), there is only one such member defined for the entire hierarchy. Regardless of the number of classes derived from a base class, there exists a single instance of each static member.
Assuming the member is accessible, we can use a static member through either the base or derived:
void Derived::f(const Derived &derived_obj)
{
Base::statmem(); // ok: Base defines statmem
Derived::statmem(); // ok: Derived inherits statmem
// ok: derived objects can be used to access static from base
derived_obj.statmem(); // accessed through a Derived object
statmem(); // accessed through this object
}
A derived class is declared like any other class (§7.3.3, p. 278). The declaration contains the class name but does not include its derivation list:
class Bulk_quote : public Quote; // error: derivation list can‘t appear here
class Bulk_quote; // ok: right way to declare a derived class
Defination:
class Base { /* ...*/ } ;
class D1: public Base { /* ...*/ };
class D2: public D1 { /* ...*/ };
In this hierarchy, Base is a direct base to D1 and an indirect base to D2.
Under the new standard, we can prevent a class from being used as a base by following the class name withfinal:
class NoDerivedfinal { /* */ }; // NoDerived can‘t be abase class
class Base { /* */ };
// Last is final; we cannot inherit from Last
class Last final : Base { /* */ }; //Last can‘t be a base class
class Bad : NoDerived { /* */ }; // error: NoDerived is final
class Bad2 : Last { /* */ }; // error: Last is final
Classes related by inheritance are an important exception: We can bind a pointer or reference to a base-class type to an object of a type derived from that base class.
The fact that we can bind a reference (or pointer) to a base-class type to a derived object has a crucially important implication: When we use a reference (or pointer) to a base-class type, we don’t know the actual type of the object to which the pointer or reference is bound. That object can be an object of the base class or it can be an object of a derived class.
Static Type and Dynamic Type
The static type of an expression is always known at compile time—it is the type with which a variable is declared or that an expression yields. The dynamic type is the type of the object in memory that the variable or expression represents. The dynamic type may not be known until run time.
Quote base;
Bulk_quote* bulkP = &base; // error: can‘t convert base to derived
Bulk_quote& bulkRef = base; // error: can‘t convert base to derived
The compiler has no way to know (at compile time) that a specific conversion will be safe at run time. The compiler looks only at the static types of the pointer or reference to determine whether a conversion is legal.
If the base class has one or more virtual functions, we can use a dynamic_cast to request a conversion that is checked at run time. Alternatively, in those cases when we know that the conversion from base to derived is safe, we can use a static_cast to override the compiler.
The automatic derived-to-base conversion applies only for conversions to a reference or pointer type. There is no such conversion from a derived-class type to the base class type.
Nevertheless, it is often possible to convert an object of a derived class to its base-class type. However, such conversions may not behave as we might want.When we initialize or assign an object of a base type from an object of a derived type, only the base-class part of the derived object is copied, moved, or assigned. The derived part of the object is ignored.
Calls to Virtual Functions May Be Resolved at Run Time
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 of the object bound to that pointer or reference.
Key Concept: Polymorphism in C++
The key idea behind OOP is polymorphism. Polymorphism is derived from a Greek word meaning “many forms.” We speak of types related by inheritance as polymorphic types, because we can use the “many forms” of these types while ignoring the differences among them.
The fact that the static and dynamic types of references and pointers can differ is the cornerstone of how C++ supports polymorphism.
When we call a function defined in a base class through a reference or pointer to the base class, we do not know the type of the object on which that member is executed. The object can be a base-class object or an object of a derived class. If the function
is virtual, then the decision as to which function to run is delayed until run time. The version of the virtual function that is run is the one defined by the type of the object to which the reference is bound or to which the pointer points.
On the other hand, calls to nonvirtual functions are bound at compile time. Similarly, calls to any function (virtual or not) on an object are also bound at compile time. The type of an object is fixed and unvarying—there is nothing we can do to make the dynamic
type of an object differ from its static type. Therefore, calls made on an object are bound at compile time to the version defined by the type of the object.
Virtual Functions in a Derived Class
When a derived class overrides a virtual function, it may, but is not required to, repeat the virtual keyword. Once a function is declared as virtual, it remains virtual in all the derived classes.
With one exception, the return type of a virtual in the derived class also must match the return type of the function from the base class. The exception applies to virtuals that return a reference (or pointer) to types that are themselves related by inheritance.
That is, if D is derived from B, then a base class virtual can return a B* and the version in the derived can return a D*. However, such return types require that the derived-to-base conversion from D to B is accessible.
It is legal for a derived class to define a function with the same name as a virtual in its base class but with a different parameter list. The compiler considers such a function to be independent from the base-class function. In such cases, the derived version does not override the version in the base class. In practice, such declarations often are a mistake—the class author intended to override a virtual from the base class but made a mistake in specifying the parameter list.
Finding such bugs can be surprisingly hard. Under the new standard we can specify override on a virtual function in a derived class. Doing so makes our intention clear and (more importantly) enlists the compiler in finding such problems for us. The compiler will reject a program if a function marked override does not override an existing virtual function:
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B {
void f1(int) const override; // ok: f1 matches f1 in the base
void f2(int) override; // error: B has no f2(int) function
void f3() override; // error: f3 not virtual. only a virtual function can be overridden
void f4() override; // error: B doesn‘t have a function named f4
}
We can also designate a function as final(感觉在向java学习啊). Any attempt to override a function that has been defined as finalwill be flagged as an error:
struct D2 : B {
// inherits f2() and f3() from B and overrides f1(int)
void f1(int) const final; // subsequent classes can‘t override f1 (int)
};
struct D3 : D2 {
void f2(); // ok: overrides f2 inherited from the indirect base,B
void f1(int) const; // error: D2 declared f2 as final
};
final and override specifiers appear after the parameter list (including any const or reference qualifiers) and after a trailing return.
Virtual functions that have default arguments should use the same argument values in the base and derived classes.
In some cases, we want to prevent dynamic binding of a call to a virtual function; we want to force the call to use a particular version of that virtual. We can use the scope operator to do so.
Why might we wish to circumvent the virtual mechanism? The most common reason is when a derived-class virtual function calls the version from the base class. If a derived virtual function that intended to call its base-class version omits the scope operator, the call will be resolved at run time as a call to the derived version itself, resulting in an infinite recursion.
Unlike ordinary virtuals, a pure virtual function does not have to be defined. We specify that a virtual function is a pure virtual by writing = 0 in place of a function body.The = 0 may appear only on the declaration of a virtual function in the class body. Itis worth noting that we can provide a definition for a pure virtual. However, the function body must be defined outside the class. That is, we cannot provide a function body inside the class for a function that is = 0.
class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price, std::size_t qty, double disc):Quote(book, price),quantity(qty), discount(disc) { }
double net_price(std::size_t) const = 0;
protected:
std::size_t quantity = 0; // purchase size for the discount to apply
double discount = 0.0; // fractional discount to apply
};
Classes with Pure Virtuals Are Abstract Base Classes
We may not create objects of a type that is an abstract base class.
A Derived Class Constructor Initializes Its Direct Base Class Only
Redesigning programs to collect related parts into a single abstraction, replacing the original code with uses of the new abstraction. Typically, classes are refactored to move data or function members to the highest common point in the hierarchy to avoid code duplication. Refactoring involves redesigning a class hierarchy to move operations and/or data from one class to another. Refactoring is common in object-oriented applications.
protected Members
The protected specifier can be thought of as a blend of privateand public:
?Like private, protected members are inaccessible to users of the class.
?Like public, protected members are accessible to members and friends of classes derived from this class.
In addition, protectedhas another important property:
?A derived class member or friend may access the protected members of the base class only through a derived object. The derived class has no special access to the protected members of base-class objects.
class Base {
protected:
int prot_mem; // protected member
};
class Sneaky : public Base {
friend void clobber(Sneaky&); // can access Sneaky::prot_mem
friend void clobber(Base&); // can‘t access Base::prot_mem
int j; // j is private by default
};
// ok: clobber can access the private and protected members in Sneaky objects
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// error: clobber can‘t access the protected members in Base
void clobber(Base &b) { b.prot_mem = 0; }
If derived classes (and friends) could access protected members in a base-class object, then our second version of clobber(that takes a Base&)would be legal. That function is not a friend of Base, yet it would be allowed to change an object of type Base; we could circumvent the protection provided by protectedfor any class simply by defining a new class along the lines of Sneaky.
To prevent such usage, members and friends of a derived class can access the protected members only in base-class objects that are embedded inside a derived type object; they have no special access to ordinary objects of the base type.
class Base {
public:
void pub_mem(); // public member
protected:
int prot_mem; // protected member
private:
char priv_mem; // private member
};
struct Pub_Derv : public Base {
// ok: derived classes can access protected members
int f() { return prot_mem; }
// error: private members are inaccessible to derived classes
char g() { return priv_mem; }
};
struct Priv_Derv : private Base {
// private derivation doesn‘t affect access in the derived class
int f1() const { return prot_mem; }
}
Pub_Derv d1; // members inherited from Base are public
Priv_Derv d2; // members inherited from Base are private
d1.pub_mem(); // ok: pub_mem is public in the derived class
d2.pub_mem(); // error: pub_mem is private in the derived class
Both Pub_Derv and Priv_Derv inherit the pub_memfunction. When the inheritance is public, members retain their access specification. Thus, d1can call pub_mem. In Priv_Derv, the members of Baseare private; users of that class may not call pub_mem.
The derivation access specifier used by a derived class also controls access from classes that inherit from that derived class:
struct Derived_from_Public : public Pub_Derv {
// ok: Base::prot_mem remains protected in Pub_Derv
int use_base() { return prot_mem; }
};
struct Derived_from_Private : public Priv_Derv {
// error: Base::prot_mem is private in Priv_Derv
int use_base() { return prot_mem; }
};
Protected Inheritance:In protected inheritance, the protected and public members of the base class are protected members of the derived class.
Public inheritance:The public interface of the base class is part of the public interface of the derived class.
Under inheritance, there is a third kind of user, namely, derived classes. A base class makes protectedthose parts of its implementation that it is willing to let its derived classes use. The protected members remain inaccessible to ordinary user code; private members remain inaccessible to derived classes and their friends.
Like any other class, a class that is used as a base class makes its interface members public. A class that is used as a base class may divide its implementation into those members that are accessible to derived classes and those that remain accessible only to the base class and its friends. An implementation member should be protectedif it provides an operation or data that a derived class will need to use in its own implementation. Otherwise, implementation members should be private.
Friendship and Inheritance
Just as friendship is not transitive (§7.3.4, p. 279), friendship is also not inherited. Friendship is not inherited; each class controls access to its members.
Exempting Individual Members
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base { // note: private inheritance
public:
// maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
};
Because Derived uses private inheritance, the inherited members, size and n, are (by default) private members of Derived. The using declarations adjust the accessibility of these members. Users of Derived can access the size member, and classes subsequently derived from Derived can access n.
Default Inheritance Protection Levels
class Base { /* ... */ };struct D1 : Base { /* ... */ }; // public inheritance by default
class D2 : Base { /* ... */ }; // private inheritance by default
It is a common misconception to think that there are deeper differences between classes defined using the structkeyword and those defined using class. The only differences are the default access specifier for members and the default derivation access specifier. There are no other distinctions.
A privately derived class should specify private explicitly rather than rely on the default. Being explicit makes it clear that private inheritance is intended and not an oversight.
Name Collisions and Inheritance
Using the Scope Operator to Use Hidden Members
struct Derived : Base {int get_base_mem() { return Base::mem; }
// ...
};
Best Practices :Aside from overriding inherited virtual functions, a derived class usually should not reuse names defined in its base class.
As Usual, Name Lookup Happens before Type Checking
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int); // hides memfcn in the base
};
Derived d; Base b;
b.memfcn(); // calls Base::memfcn
d.memfcn(10); // calls Derived::memfcn
d.memfcn(); // error: memfcn with no arguments is hidden
d.Base::memfcn(); // ok: calls Base::memfcn
Overriding Overloaded Functions
Sometimes a class needs to override some, but not all, of the functions in an overloaded set. It would be tedious in such cases to have to override every base-class version in order to override the ones that the class needs to specialize.
Instead of overriding every base-class version that it inherits, a derived class can provide a using declaration (§15.5, p. 615) for the overloaded member. A using declaration specifies only a name; it may not specify a parameter list. Thus, a using declaration for a base-class member function adds all the overloaded instances of that function to the scope of the derived class. Having brought all the names into its scope, the derived class needs to define only those functions that truly depend on its type. It can use the inherited definitions for the others.
15.7.1 Virtual Destructors
So long as the base class destructor is virtual, when we deletea pointer to base, the correct destructor will be run:
Quote *itemP = new Quote; // same static and dynamic type
delete itemP; // destructor for Quote called
itemP = new Bulk_quote; // static and dynamic types differ
delete itemP; // destructor for Bulk_quote called
Destructors for base classes are an important exception to the rule of thumb that if a class needs a destructor, it also needs copy and assignment (§13.1.4, p. 504). A base class almost always needs a destructor, so that it can make the destructor virtual. If a base class has an empty destructor in order to make it virtual, then the fact that the class has a destructor does not indicate that the assignment operator or copy constructor is also needed.
Virtual Destructors Turn Off Synthesized Move
The synthesized copy-control members in a base or a derived class execute like any other synthesized constructor, assignment operator, or destructor: They memberwise initialize, assign, or destroy the members of the class itself. In addition, these synthesized members initialize, assign, or destroy the direct base part of an object by using the corresponding operation from the base class.
It is worth noting that it doesn’t matter whether the base-class member is itself synthesized (as is the case in our Quotehierarchy) or has a an user-provided definition. All that matters is that the corresponding member is accessible (§15.5, p. 611) and that it is not a deleted function.
The way in which a base class is defined can cause a derived-class member to be defined as deleted:
?If the default constructor, copy constructor, copy-assignment operator, or destructor in the base class is deleted or inaccessible (§15.5, p. 612), then the corresponding member in the derived class is defined as deleted, because the compiler can’t use the base-class member to construct, assign, or destroy the base-class part of the object.
?If the base class has an inaccessible or deleted destructor, then the synthesized default and copy constructors in the derived classes are defined as deleted, because there is no way to destroy the base part of the derived object.
?As usual, the compiler will not synthesize a deleted move operation. If we use = default to request a move operation, it will be a deleted function in the derived if the corresponding operation in the base is deleted or inaccessible, because the base class part cannot be moved. The move constructor will also be deleted if the base class destructor is deleted or inaccessible.
Because lack of a move operation in a base class suppresses synthesized move for its derived classes, base classes ordinarily should define the move operations if it is sensible to do so. Once it defines its move operations, it must also explicitly define the copy versions as well.
When a derived class defines a copy or move operation, that operation is responsible for copying or moving the entire object, including base-class members. Unlike the constructors and assignment operators, the destructor is responsible only for destroying the resources allocated by the derived class. Similarly, the base-class part of a derived object is destroyed automatically.
When we define a copy or move constructor (§13.1.1, p. 496, and §13.6.2, p. 534) for a derived class, we ordinarily use the corresponding base-class constructor to initialize the base part of the object.
class Base { /* ... */ } ;
class D: public Base {
public:
// by default, the base class default constructor initializes the base part of an object
// to use the copy or move constructor, we must explicitly call that
// constructor in the constructor initializer list
D(const D& d): Base(d) // copy the base members
/* initializers for members of D*/ { /* ... */ }
D(D&& d): Base(std::move(d)) // move the base members
/* initializers for members of D*/ { /* ... */ }
};
Derived-Class Assignment Operator
// Base::operator=(const Base&) is not invoked automatically
D &D::operator=(const D &rhs)
{
Base::operator=(rhs); // assigns the base part
// assign the members in the derived class, as usual,
// handling self-assignment and freeing existing resources as appropriate
return *this;
}
Calls to Virtuals in Constructors and Destructors
If a constructor or destructor calls a virtual, the version that is run is the one corresponding to the type of the constructor or destructor itself.
Under the new standard, a derived class inherits its base-class constructors by providing a using declaration that names its (direct) base class.
class Bulk_quote : public Disc_quote {
public:
using Disc_quote::Disc_quote; // inherit Disc_quote‘s constructors
double net_price(std::size_t) const;
};
Ordinarily,a using declaration only makes a name visible in the current scope. When applied to a constructor, a usingdeclaration causes the compiler to generate code. The compiler generates a derived constructor corresponding to each constructor in the base. That is, for each constructor in the base class, the compiler generates a constructor in the derived class that has the same parameter list.
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc):Disc_quote(book, price, qty, disc) { }
Unlike using declarations for ordinary members, a constructor using declaration does not change the access level of the inherited constructor(s).
Moreover, a using declaration can’t specify explicit or constexpr. If a constructor in the base is explicit or constexpr, the inherited constructor has the same property.
When we use a container to store objects from an inheritance hierarchy, we generally must store those objects indirectly. Put(Smart) Pointers, Not Objects, in Containers
vector<shared_ptr<Quote>>basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));
// calls the version defined by Quote; prints 562.5, i.e., 15 * $50 less the discount
cout << basket.back()->net_price(15) << endl;
Just as we can convert an ordinary pointer to a derived type to a pointer to an base-class type (§15.2.2, p. 597), we can also convert a smart pointer to a derived type to a smart pointer to an base-class type. Thus, make_shared<Bulk_quote>returns a shared_ptr<Bulk_quote>object, which is converted to shared_ptr<Quote> when we call push_back. As a result, despite appearances, all of the elements of baskethave the same type.
The design of inheritance hierarchies is a complicated topic in its own right and well beyond the scope of this language Primer. However, there is one important design guide that is so fundamental that every programmer should be familiar with it.
When we define a class as publicly inherited from another, the derived class should reflect an “Is A” relationship to the base class. In well-designed class hierarchies, objects of a publicly derived class can be used wherever an object of the base class is expected.
Another common relationship among types is a “Has A” relationship. Types related by a “Has A” relationship imply membership.
《C++ Primer第五版》读书笔记(13)-Object-Oriented Programming,布布扣,bubuko.com