Chap 15 Object-Oriented Programming
15.1 OOP总览
继承
inheritance hierarchy
动态绑定
在C++里,当虚函数通过基类的引用或者指针被调用时,就会发生动态绑定。(In C++, dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class.)
15.2 Defining Base and Derived Classes
15.2.1定义基类(Defining a Base Class)
基类经常定义函数为virtual,这样派生类需要对这样的函数重新定义。(A base class usually should define as virtual any function that a derived class will need to redefine.)
缺省情况下,函数都是非虚函数。非虚函数的调用是在编译时确定的。(By default, member functions are nonvirtual. Calls to nonvirtual functions are resolved at compile time.)
15.2.2protected 成员(protected Members)
protected成员
- 不能被类的使用者来访问,这像private member。
- 能够被派生类访问,这像public member。
派生类要通过派生对象来访问基类里的protected成员;对于基类类型的对象的protected成员,派生类没有特殊的访问权限。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
// attempt to use protected member
double ret = price; // ok: uses this->price
ret = d.price; // ok: uses price from a Bulk_item object
ret = b.price; // error: no access to price from an Item_base
}
|
因为Bulk_item没有对Item_base对象的特殊的访问权限。所以ret = b.price;错误。但是ret = d.price;是对的。
类设计和protected member
仅仅对一个没有派生类的类来说,只有
public:类用户可以访问。
private:类成员和友元可以访问。
有了继承,就有了protected:
protected:派生类运行访问。
15.2.3派生类(Derived Classes)
定义派生类
class classname: access-label base-class
|
l 解访问标号决定了对继承成员的访问权限(the access label determines the access to the inherited members.?)
l 虚函数返回值是基类本身的引用或者指针
派生类里的虚函数可以返回类引用或者是类指针。这个类是基类函数的返回值的派生类。(virtual function in a derived class can return a reference (or pointer) to a class that is publicly derived from the type returned by the base-class function.)
l 派生类可以访问基类的public和protected的成员。(classes may access the public and protected members of its base class)
l 类必须在它用作其它类的基类前定义。(A class must be defined before it can be used as a base class.)注意啦,这里是定义,而不是声明哦。
l 派生类的声明不必包含它的派生列表。(If we need to declare a derived class, the declaration contains the class name but does not include its derivation list.)
// forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;
|
15.2.4virtual和其他成员函数(virtual and Other Member Functions)
动态绑定有两个触发条件:
1. 成员函数必须是虚函数
2. 通过基类类型的指针或者引用产生的调用。就是说如果是通过对象来调用虚函数也不会产生动态绑定喽。
这是因为基类类型指针即可以指向基类对象也可以指向派生类对象,只有在运行期(run-time),才能够知晓指针指向的何种类型的对象。
基类类型的引用和指针可以分成:
静态类型static type:在编译期就可以确定引用和指针的类型
动态类型dynamic type:在运行期才能确定引用和指针绑定的对象类型。
这两种类型可能不同。
在运行期才能确定虚函数的调用
c++动态绑定的关键是对象的实际类型也许不同于指针或引用的静态类型。(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.)
// member binary operator: left-hand operand bound to implicit this pointer
|
非虚函数调用是在编译期确定的
下面的例子里,即使Bulk_item定义了自己的book函数,也是不会被调用的,还是要调用基类的book函数,因为book函数不是虚函数。这和Java有些不同。
// calculate and print price for given number of copies, applying any discounts
void print_total(ostream &os,
const Item_base &item, size_t n)
{
os << "ISBN: " << item.book() // calls Item_base::book
<< ""tnumber sold: " << n << ""ttotal price: "
// virtual call: which version of net_price to call is resolved at run time
<< item.net_price(n) << endl;
}
//calling
Item_base base;
Bulk_item derived;
// print_total makes a virtual call to net_price
print_total(cout, base, 10); // calls Item_base::net_price; Item_base::book
print_total(cout, derived, 10); // calls Bulk_item::net_price; Item_base::book
|
重写虚函数
似乎永远也记不住的名词:(顺手再抄一遍)L
override表示“重写”,用于继承一个基类的时候,基类当中虚拟成员的实现。
overload表示“重载”,用于同一类中同名方法但参数个数或类型不同的实现,也就是让方法有不同签名的版本。
在派生类里,虚函数一般希望可以执行基类的操作的同时执行自己特有的操作,因此需要显示调用基类的虚函数,通过使用范围操作符来指定调用基类的虚函数,就是这样:
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);
|
虚函数和缺省实参
虚函数可以有缺省实参,但是和一般的函数不同,基类和派生类可以分别有自己的缺省实参。
对象:
如果存在缺省实参,那么在编译期就确定这个虚函数调用的缺省实参的值。如果调用call忽略的实参有缺省值,那么这个缺省值由虚函数调用的对象的类型决定。
引用和指针:
当虚函数通过基类的引用或者指针来调用时,缺省的实参值就是基类里虚函数声明的值。
当虚函数通过派生类的引用或者指针来调用时,缺省的实参值就是派生类里虚函数声明的值。
Trouble Shooting:
如果把基类的引用或者指针指向派生类对象,那么通过这个引用或者指针调用虚函数时,就会把基类虚函数的缺省实参值就会传递给派生类的虚函数。L
15.2.5公用、私有和受保护的继承(Public, Private, and Protected Inheritance)
这,说的是继承方式。继承方式不同对成员的访问属性也是有影响的。
- 公用继承public inheritance
- 受保护继承protected inheritance
- 私有继承private inheritance
访问属性是针对派生类的用户来说的,而对于派生类来说,不管何种继承方式也不会影响其访问基类的成员。
但是这种说法不具有传递性,就是说派生类的派生类来说,这些继承方式,还是会起作用的。下面的例子就是这样:
class Base {
public:
void basemem(); // public member
protected:
int i; // protected member
// ...
};
struct Public_derived : public Base {
int use_base() { return i; } // ok: derived classes can access i
// ...
};
struct Private_derived : private Base {
int use_base() { return i; } // ok: derived classes can access i
};
struct Derived_from Private : public Private_derived {
// error: Base::i is private in Private_derived
int use_base() { return i; }
};
struct Derived_from_Public : public Public_derived {
// ok: Base::i remains protected in Public_derived
int use_base() { return i; }
};
|
Derived_from对于i就不能再访问了。因为Private_derived是private继承,导致i在Private_derived的访问属性就是private。
接口继承和实现继承
接口继承Interface Inheritance:public继承方式;派生类和基类有同样的接口。
实现继承Implementation Inheritance:protected或者private继承方式。
免除个别的成员Exempting Individual Members
派生类能够恢复继承的成员的访问级别,但是改变基类定义的访问级别,增加或者减少都是不可以的。(The derived class can restore the access level of an inherited member. The access level cannot be made more or less restrictive than the level originally specified within the base class.)
应用场景:以private方式继承时,又希望不改变其中某些成员的访问级别。
解决方法:使用using声明。例如:基类定义
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
|
派生类定义:
class Derived : private Base {
public:
// maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
// ...
};
|
这样,派生类虽然是以private方式继承的基类,成员size和n还是可以保持原来的访问级别。
缺省继承保护级别Default Inheritance Protection Levels
类的定义可以使用两种关键字:class和struct。二者的区别在于缺省情况下,class以private方式来继承;而struct是以public方式来继承。
class Base { /* ... */ };
struct D1 : Base { /* ... */ }; // public inheritance by default
class D2 : Base { /* ... */ }; // private inheritance by default
|
15.2.6友元和继承
首先友元是不继承的。(Friendship is not inherited.)
这句话要从两方面来理解:
1. 基类的友元不是派生类的友元。
2. 友元的派生类不是友元。
具体的sample见P575的例子。
15.2.7 继承和静态成员
对于每个静态成员只存在一个实例。(exists a single instance of each static member.)
struct Base {
static void statmem(); // public by default
};
struct Derived : Base {
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmem(); // ok: Base defines statmem
Derived::statmem(); // ok: Derived in herits statmem
// ok: derived objects can be used to access static from base
derived_obj.statmem(); // accessed through Derived object
statmem(); // accessed through this class
|
15.3转换和继承Conversions and Inheritance
15.3.1. 从派生类到基类的转换Derived-to-Base Conversions
转换成引用和转换成对象是不一样的(这指的是函数形参)
1. 当把一个对象传递给一个需要引用的函数时,对象并没有copy,引用直接绑定到对象上。
2. 当把一个对象传递给一个需要对象的函数时,派生对象的基类部分copy到形参里。
使用派生类的对象赋值或者初始化一个基类对象
基类是不大可能显式定义如何使用派生类去初始化或者赋值一个基类的对象。正如前面所阐述的,如果形参是引用,那么就可以直接把一个基类的引用绑定到派生类的对象上。因此,激烈一般都会提供形参是引用的拷贝构造函数,或者是赋值操作符。
从派生类到基类转换的访问性
这取决于派生类指定的继承方式。(Whether the conversion is accessible depends on the access label specified on the derived class' derivation.)如果基类的public成员是可访问的,那么转换是可访问的,否则,则是不可访问的。(whether a public member of the base class would be accessible. If so, the conversion is accessible; otherwise, it is not.)
15.4 Constructors and Copy Control
当构造、拷贝、赋值或撤销一个派生类对象时,也要构造、拷贝、赋值或撤销那些基类的子对象。(When we construct, copy, assign, or destroy an object of derived type, we also construct, copy, assign, or destroy those base-class subobjects.)
15.4.1 基类构造函数和拷贝控制
继承对于基类的唯一的影响就是如果希望构造函数只能北派生类访问,那么构造函数就要定义为protected。
15.4.2 派生类的构造函数
派生类的构造函数除了要完成自己的数据成员的初始化还要初始化它的基类。(Each derived constructor initializes its base class in addition to initializing its own data members.)
另外,只有直接基类才能够被初始化。(Only an Immediate Base Class May Be Initialized)
派生类的初始化顺序是先调用基类的初始化函数,再初始化自己的数据成员。
只有缺省的构造函数隐式调用基类的缺省构造函数。
class Bulk_item : public Item_base {
public:
Bulk_item(): min_qty(0), discount(0.0) { }
// as before
};
|
其它构造函数要显式定义指定的构造函数:
class Bulk_item : public Item_base {
public:
Bulk_item(const std::string& book, double sales_price,
std::size_t qty = 0, double disc_rate = 0.0):
Item_base(book, sales_price),
min_qty(qty), discount(disc_rate) { }
// as before
};
|
15.4.3 拷贝控制和继承
拷贝控制和赋值操作符都是先执行基类部分的拷贝控制和赋值,然后再执行派生类的拷贝控制和赋值。这个顺序是不能该改变的。而析构函数则相反。派生类的析构函数是先调用,然后再调用基类的析构函数。
定义派生类的拷贝构造函数:
class Base { /* ... */ };
class Derived: public Base {
public:
// Base::Base(const Base&) not invoked automatically
Derived(const Derived& d):
Base(d) /* other member initialization */ { /*... */ }
};
|
定义派生类的赋值操作符:
// Base::operator=(const Base&) not invoked automatically
Derived &Derived::operator=(const Derived &rhs)
{
if (this != &rhs) {
Base::operator=(rhs); // assigns the base part
// do whatever needed to clean up the old value in the derived part
// assign the members from the derived
}
return *this;
}
|
定义派生类的析构函数:
class Derived: public Base {
public:
// Base::~Base invoked automatically
~Derived() { /* do what it takes to clean up derived members */ }
};
|
这三种函数无论如何定义,都要显式调用基类对应的函数。
15.4.4 Virtual Destructors
先要明白为啥要把析构函数定义为virtual?
因为如果基类指针指向的是一个派生类的对象,就会导致基类的析构函数被调用,然后清除基类的成员。但是派生类的成员呢?没人管了呗L
因此提出的解决方法就是析构函数定义成虚函数,这样,通过指针调用时,调用哪一个析构函数取决于指向的对象的类型。(If the destructor is virtual, then when it is invoked through a pointer, which destructor is run will vary depending on the type of the object to which the pointer points)
继承层次关系的根类应该定义虚函数性质的析构函数,即使这个析构函数不做任何的事情。(The root class of an inheritance hierarchy should define a virtual destructor even if the destructor has no work to do.)
拷贝构造函数和赋值操作符不必是虚函数
这很好理解,因为无论是拷贝构造函数还是赋值操作符,需要的参数类型都是类本身。
15.4.5. 构造函数和析构函数中的调用虚函数
这个问题的提出源自当执行构造函数或者是析构函数时,对象是不完整的。编译器这时认为对象类型是变化的。在调用基类的构造函数或者析构函数时,派生类的对象被看作是基类类型。
因此这就引出了问题,如果这时调用虚函数,那么是调用基类的?还是调用派生类的?
结论:如果构造函数或者析构函数调用虚函数,虚函数的版本取决于构造函数或者析构函数的类型。(If a virtual is called from inside a constructor or destructor, then the version that is run is the one defined for the type of the constructor or destructor itself.)
这句话的意思就是说,情况一,在派生类的构造函数里调用基类的构造函数,此时如果基类的构造函数调用一个虚函数,那么这个虚函数一定就是基类中的那个版本。情况二,在派生类的构造函数里调用基类的构造函数,然后再调用某个虚函数,那么这个虚函数一定就是派生类里的那个版本。
15.5 继承下的类作用域Class Scope under Inheritance
(这部分最好先回去温习一下15.2.4virtual和其他成员函数)
以下这些知识点重新抄写一遍在这里:
为什么要定义为虚函数,不是虚的,有啥区别?
定义为虚函数,才能实现动态绑定。在运行期确定虚函数的调用。非虚函数的调用是在编译期确定的。
如果函数没有定义为虚函数,即使派生类中override这个函数,当通过指向派生类的基类指针调用这个函数时,也是调用的基类的函数。在编译期就确定了。
如果这个函数定义为虚函数,即使通过指向派生类的基类指针调用这个函数时,也是调用的派生类的函数。在运行期就确定了。
静态类型static type:在编译期就可以确定引用和指针的类型
动态类型dynamic type:在运行期才能确定引用和指针绑定的对象类型。
基本概念就是先在派生类中寻址,如果未找到,则在基类中寻址。(If a name is unresolved within the scope of the derived class, the enclosing base-class scope(s) are searched for a definition of that name.)
15.5.1 名字寻址发生在编译期
对象、引用或指针的静态类型决定了对象能够完成的行为。(The static type of an object, reference, or pointer determines the actions that the object can perform.)
15.5.2 名字冲突和继承
当派生类的成员和基类成员名字相同时,派生类的成员屏蔽了对基类此成员的直接访问。(A derived-class member with the same name as a member of the base class hides direct access to the base-class member.)如果还要访问这个隐藏的成员,就需要通过作用域操作符来访问。
由于在基类Base和派生类Derived中都定义了成员mem,这样基类的成员mem就会被派生类隐藏。如果还需要访问基类的这个mem,就需要使用作用域操作符。
struct Derived : Base {
int get_base_mem() { return Base::mem; }
};
|
15.5.3 作用域和成员函数
和上面的数据成员的处理方式一样。派生类成员隐藏基类成员。这里只要名称相同,即使原型不同也都会被隐藏。下面这个例子就是很好的说明。Derived里面定义的int memfcn(int);虽然形参和基类不同,也会导致基类的int memfcn();被屏蔽。
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
|
d.memfcn();为什么不对?因为编译器一旦在Derived类中找到memfcn就不再找其它的类了,而Derived类中定义的其实是int memfcn(int);,因此就翘了。
结论:
当调用派生对象的函数时,实参必须和派生类中函数定义相互匹配。基类的韩式只有在派生类中没有任何定义时才会被调用。(When the function is called through a derived object, the arguments must match a version of the function defined in the derived class. The base class functions are considered only if the derived does not define the function at all.)
Overloaded Functions
如果派生类重新定义了任何一个重载成员(overloaded members),那么只有在派生类中重新定义的成员可以通过派生类类型来访问。(If the derived class redefines any of the overloaded members, then only the one(s) redefined in the derived class are accessible through the derived type.)
这意味,如果派生类希望可以访问所有重载成员(overloaded members),要么就重新定义所有的,要么就一个都不定义。
但是大多数情况,我们都是希望重载某些函数,继承其他的。按照上面的说法,就需要在派生类中对于继承基类版本的每一个函数都要重新定义。这,这,这,这工作量。所以解决方法就是使用using声明。
15.5.4 虚函数和作用域
下面大师给的例子有助于理解为什么虚函数在基类和派生类里必须有相同的原型。
答案就是:
如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。(If the base member took different arguments than the derived-class member, there would be no way to call the derived function from a reference or pointer to the base type.)
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
// hides fcn in the base; this fcn is not virtual
int fcn(int); // parameter list differs from fcn in Base
// D1 inherits definition of Base::fcn()
};
class D2 : public D1 {
public:
int fcn(int); // nonvirtual function hides D1::fcn(int)
int fcn(); // redefines virtual fcn from Base
};
//calling…
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); // ok: virtual call, will call Base::fcnat run time
bp2->fcn(); // ok: virtual call, will call Base::fcnat run time
bp3->fcn(); // ok: virtual call, will call D2::fcnat run time
//my calling J
D1 *d1bp = &d1obj;
D1bp->fcn(); //error.
|
bp2->fcn(); 之所以OK,因为是通过基类指针调用的;如果通过派生类的指针调用D1bp->fcn(); ,就会产生Error。道理就是由于D1里定义了int fcn(int);就导致基类的fcn()被屏蔽了。
名字寻址和继承
遵循的是以下的4步:
- 开始于确定函数调用的对象,引用或指针的静态类型(Start by determining the static type of the object, reference, or pointer through which the function is called.)
- 在类中搜寻函数。如果没找到,就在直接基类中找,依此类推,沿着类的继承链向上寻找,直至函数找到或者最后一个也被搜寻过为止。如果在类或者类的基类中都没有找到这个函数,这次调用就是错误的。(Look for the function in that class. If it is not found, look in the immediate base class and continue up the chain of classes until either the function is found or the last class is searched. If the name is not found in the class or its enclosing base classes, then the call is in error.)
- 一旦找到名字,就进行类型检测,确定和找到的函数定义相比,这个调用是否合法。(Once the name is found, do normal type-checking to see if this call is legal given the definition that was found.)
- 假设调用是合法的,编译器生成代码。如果函数是虚函数,并且调用是通过引用或者指针,编译器基于动态类型生成代码确定调用哪个版本。如果不是虚函数,编译器直接生成代码调用函数。(Assuming the call is legal, the compiler generates code. If the function is virtual and the call is through a reference or pointer, then the compiler generates code to determine which version to run based on the dynamic type of the object. Otherwise, the compiler generates code to call the function directly.)
15.6 纯虚函数Pure Virtual Functions
纯虚函数引出的是抽象基类。定义:
class Disc_item : public Item_base {
public:
double net_price(std::size_t) const = 0;
};
|
类包含或者继承一个或多个纯虚函数,这样的类就是抽象基类。除了作为抽象类的派生类对象的一部分,是不能够生成抽象基类对象的。(A class containing (or inheriting) one or more pure virtual functions is an abstract base class. We may not create objects of an abstract type except as parts of objects of classes derived from the abstract base.
)
C++里面的这个纯虚函数其实就相当于Java里面的抽象函数。在Java里面,如果定义了抽象函数,那么类也就是抽象类,也是不能创建这种类的对象的。
15.7 容器和继承Containers and Inheritance
这个问题的引用场景很好理解,如果要建立一个购物车来保存客户购买的书。这个购物车可以看作是一个容器类型。这时问题就出现了,客户购买的书可能会根据不同的折扣方式计价,而不同的计价方式就对应了不同Item_base的派生类。那么这个容器的单元类型该如何定义了,定义成基类,就会丢失折扣计价方式;定义派生类,那基类的对象也不行。
如何解决这个问题呢?
那就是使用容器来保存对象指针,而不是保存对象本身。(The only viable alternative would be to use the container to hold pointers to our objects.)
15.8 句柄类和继承Handle Classes and Inheritance
开篇就是一句耐人寻味的话:C++不能用对象支持面向对象编程,必须使用指针或引用来支持面向对象编程。(One of the ironies of object-oriented programming in C++ is that we cannot use objects to support it. Instead, we must use pointers and references, not objects.)
句柄类保存和管理基类指针。(The handle class stores and manages a pointer to the base class.)用户是通过句柄来访问继承层级中的操作。(Users access the operations of the inheritance hierarchy through the handle.)
自问自答:一个指针是不是就对应一个句柄呢?
是的,一个指针指向一个对象,就是一个句柄,这个句柄才能保存到容器里。
15.8.1 指针型句柄(A Pointerlike Handle)
指针型句柄的数据成员包括两个指针:一个是指向基类的指针;另一个是用户计数。
指针型句柄指向的对象是和句柄绑定在一起的。这句话的理解就是句柄创建自己的对象,句柄中的指针是指向或则个由句柄创建的对象。
15.8.2. 复制未知类型(Cloning an Unknown Type)
保存到容器的单元类型必须实现虚函数clone。
自问自答
1. 派生类如何屏蔽的基类的函数(非虚函数)
Answer:在派生类中定义和基类同名的函数(但是形参可以不同于基类)。
2. 在继承关系中,重载函数的调用规则
Answer:动态绑定时,通过基类指针(指向的是派生类对象)访问虚函数,编译器在运行期就会确定调用派生类中的函数版本;如果派生类中没有重新定义这个虚函数,就调用基类中这个函数的版本。
花了大半天的时间把TextQuery做了,当然有一部分还是参考了下载的原书的source code
TextQuery 下载