对于面向对象,总是看了又忘,忘了又看,所以我为什么不将凌乱的书本知识总结归纳一下呢。面向对象程序设计(object-oriented programming)和核心思想是数据抽象,继承和动态绑定。在上一章节中,我们已经通过数据抽象,将类的接口和实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,我们可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
定义基类
我们现在对一个定价策略建模,我们首先定义一个名为Quote的类,并将它作为层次关系中的基类。Quote派生出另一个名为Bulk_quote的类,它表示可以打折销售的书籍。
这些类包含下面的两个成员函数:
1.isbn(),返回书籍的ISBN编号,该操作不涉及派生类的特殊性,因此只定义在Quote类中。
2.net_price(size_t),返回书籍的实际销售价格,前提是用户购买的数量达到一定的标准。这个操作明显是类型相关的,Quote和Bulk_quote都应该包含该函数。
对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明称虚函数(virtual function),基类通常都应该定义一个虚析构函数,即使这个这个函数不做任何操作也是如此。
class Quote{
public:
Quote() =default;
public string isbn() const;
virtual double net_price(size_t n) const;
virtual ~Quote()=default;
private:
string bookNo;
protected:
double price=0.0;
};
定义派生类
派生类必须通过使用派生类列表明确指出它是从哪个(哪些)基类继承而来的:
class Bulk_quote:public Quote{
public:
Bulk_quote()=default;
double net_price(size_t n) const override;
private:
size_t min_qty=0;
double discount=0.0
}
a. Bulk_quote除了在基类那里继承了isbn函数和bookNo,price**之外。此外,它还定义了net_price的新版本,同时拥有两个新增的数据成员min_qty和discount。
b.派生类经常(但不总是)铺盖它继承的虚函数。如果派生类没有覆盖其在基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,也可以不加。C++允许派生类显示地注明它将使用哪个成员函数改写基类的虚函数,具体的做法就是该在该函数的形参列表之后增加一个override关键字。
c.下面我们来看看派生类对象及派生类向基类类型的类型转换。在我们的例子中,一个派生类对象包含多个组成成分:一个含有派生类自己定义的(非静态)成员的子对象(min_qty和discount成员),以及一个与派生类继承的基类对应的子对象(bookNo,discount)。
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p=&item; //p指向Quote对象
p=&bulk; //p指向bulk的Quote部分
Quote &r=bulk; //r绑定到bulk的Quote部分
这种转换通常称为派生类到基类的类型转换。
d.让我们再来看看派生类的构造函数,派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类函数的。例如,接受四个参数的Bulk_quote构造函数:
Bulk_quote(const string& book,double p,size_t qty,double ,disc): Quote(book,p),min_qty(qty),discount(disc){};
该函数将它的前两个参数(分别表示ISBN和价格)传递给Quote的构造函数,由Quote的构造函数负责初始化Bulk_quote的基类部分(bookNo,price),然后初始化由派生类直接定义min_qty成员和discount成员。
e.这一段,看看派生类的声明与防止继承的发生。
class Bulk_quote :public Quote;//错误:在一个声明语句中,派生类列表是不能出现的
class Bulk_quote;//这才是声明派生类的正确方式
一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体,如一个类,一个函数,一个变量等等。派生列表以及与定义有关的其他细节必须与类的主体一起出现。但是一个类想作为基类,则一定要被定义,而不只是被声明。
C++11提供一种防止继承发生的办法,就是在类名后面跟一个关键字final:
Class NoDerived final { }; //NoDerived不能作为基类
class Base { };
Class Last final :Base{};//Last不能作为基类
类型转换与继承
理解类型转换和派生类之间的类型转换是理解C++语言面向对象编程的关键所在。
在这之前,我们先来回顾一下C++中关于引用的定义。当我们在定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将它和初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,因此引用必须初始化。而且值得注意的是,引用并不是对象,相反的,它只是一个已经存在的对象所起的另外一个名字。
我们必须将一个变量或其他表达式的静态变量与该表达式的动态变量区分开来。表达式的静态变量总是已知的,它是声明时的类型表达式生成的类型;动态类型则是变量或表达式表示的内存中对象的类型。
看下面的例子代码:
double ret =item.net_price(n);
假设我们知道item的静态类型是Quote&,它的动态类型则依赖于item绑定的实参,动态类型则依赖于item绑定的实参,动态类型知道运行时调用该函数时才会知道。如果我们传递一个Bulk_quote对象给print_total,则item的静态类型将它的静态类型不一致,我就是所说的动态绑定,就是说上述过程中函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定又称为“运行时绑定”。需要注意的是,如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。例如,Quote类型的变量永远是一个Quote对象,我们无论如何都不能改变该变量对应的对象。但是不存在基类向派生类的隐式转换类型:
Quote base;
Bulk_quote* bulkP=&base; //error:不能将基类转换成派生类
Bulk_quote& bulkP=base; //error:不能将基类转换成派生类
但有一点例外,即使一个基类指针或引用绑定在一个派生类对象上,我们不能也不能执行从基类向派生类的转换:
Bulk_quote bulk;
Quote *item=&bulk; //item指向bulk的Quote部分
Bulk_quote *bulk=item;//error:不能将基类转换成派生类
虚函数
a.对虚函数的调用可能在运行时才被解析。就像我们上文所说的print_total函数,该函数通过名为item的参数来进一步调用net_price
,其中item的类型是Quote&。因为item是引用而且net_price是虚函数,所以我们调用net_price的哪个版本完全依赖于运行时绑定到item的实参的实际动态类型。但值得注意的是,动态绑定只有当我们通过指针或引用调用虚函数是才会发生。
OPP的核心思想就是多态性,其含义是”多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”,而无需在意它们的差异。引用或指针的静态类型与动态类型不同这一事实真是C++语言支持多态性的根本所在。
b. final和override说明符。我们一般使用override标记某个函数,如果该函数没有覆盖已存在的虚函数,此时编译器将报错:
class B{
public:
virtual void f1(int) const;
virtual void f2();
void f3();
};
class D1: public B{
void f1(int) const override; //正确:f1与基类的f1匹配
void f2(int) override;//error:B没有形如f2(int)的函数
void f3() override; //error:f3不是虚函数
void f4() override;//error:B并没有名为f4的函数
};
class D2:public D1{
void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};
class D3 : public D2{
void f2(); //正确:这个方法其实是从间接基类B继承而来的,因为这中间没有派生类重写这个函数
void f1(int) const; //error:D2已经将f2声明成final,所以我们不能重写这个函数
}
c. 还有构造函数不能是虚函数,构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
抽象基类(接口)与纯虚函数
先让我们回顾一下前面创建的基类和派生类。Quote是一个基类,它表示的书籍按照原价去销售。Quote派生出另一个名为Bulk_quote的类,它表示可以打折销售的书籍。现在,我们要使用另一种销售策略:那就是当购买超过一定数量的时候,我们才会给所有的书籍打折扣,否则不打折。为此,我们创建一个抽象基类:Disc_quote。
class Disc_quote :public Quote{
public:
Disc_quote(const string& book, double price, size_t qty, double disc):Quote(book,price),quantity(qty),discount(disc){}
double net_price(size_t) const=0;
protected:
size_t quantity=0; //折扣适用的购买量
double discount=0.0; //表示折扣
}
在这里,我们定义了一个纯虚函数:net_price,这个函数告诉我们:它目前是没有意义的,所以在基类中不是不用定义这个纯虚函数的。书写=0就可以将一个虚函数声明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句中。
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口。下面讲一讲关于抽象基类的说明:
1.我们是不能创建抽象基类的对象的:
Disc_quote discounted; //error:不能定义Dis_quote的对象
Bulk_quote bulk; //正确:Bulk_quote中没有纯虚函数
2.Disc_quote的派生类必须给出自己的net_price定义,否则它们还是抽象基类
访问控制与继承
一个类proteced关键字是用来声明那些它希望与派生类分享但是不想被其他公共访问的成员。
先来考虑下面一个例子:
class Base{
protected:
int pro_men; //protected成员
};
class Sneaky:public Base{
friend void clobber(Sneaky&); //能访问Sneaky::prot_men
friend void clobber(Base&) //不能访问Base::prot_men
int j; //j是默认的private数据成员
};
void clobber(Sneaky& s) {s.j=s.prot_men=0;}
void clobber(Base& b) {b.prot_men=0;} //error
关于上面的例子,我们提出了一点漏洞。那就是如果我们想改变基类中受保护对象的内容,只需要定义一个形如Sneaky的新类就可以了,这是非常不科学的。
所以在c++11中,某个类对其继承而来的成员的访问权限受到两个因素的影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符:
class Base{
public:
void pub_men();
protected:
int prot_men;
private:
char priv_men;
};
struct Pub_Derv :public Base{
int f(){return prot_men;}
char g(){return priv_men;} //error,private成员对于派生类来说是不能访问的
};
struct Priv_Derv: private Base{
int f1() const {return prot_men;}
};
下面我们就派生类的派生列表中的访问说明符做一些说明:
1.这个访问说明符对派生类能不能访问基类的成员没有影响,和public一样,都是看基类中的成员访问说明符
2.和public不同的地方是:如果继承是public的,则成员将遵循原来的访问说明符;而如果继承是私有的,那么派生类继承而来所有成员都将变成私有的,继承自派生类的类是无法使用其父类的成员。
下面说说友元与继承的关系:
class Base{
friend class pal;
};
class pal{
public:
int f(Base b) {return b.prot_men;}//正确:pal是Base的友元
int f2(Sneaky s){return s.j;}错误:pal不是Sneaky的友元
int f3(Sneaky s){return s.prot_mem;}//正确:pal是Base的友元
}
如上所述,每个类负责控制自己成员的访问权限,但是f3函数正确的。pal是Base的友元,所以pal能够访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
还需要说明的是,友元关系是不能继承的:
class D2 :public Pal{
public:
int men(Base b){
return b.prot_men; //友元关系不能被继承
}
}