显然,我从前对面向对象编程的理解是有失偏颇的。我以前常常觉得,面向对象编程无非就是从整体的角度出发,定义一些对象,以及对象的操作,通过它们的协力合作完成一件事情。可是,这中间忽略了面向对象编程的一个关键思想,即——多态性。通过折腾了他最后提供的一个实例,算是对这一章节的东西有了些了解。但是在设计层面上,我觉得自己还没有办法想到那里去。
与多态性紧密相关的两个概念,一个是继承,一个是动态绑定。
一、继承
继承涉及基类和派生类,派生类拥有基类的所有成员,但是访问的级别或方式可能会有所改变。改变的方法有几种:
(1)访问标识符
对于public和private访问标识符我们并不陌生,public成员可以让其他对象访问,而private成员只能被该类访问。对于基类的派生类而言,他既不是基类,但是他与基类的关系显然又与其他的类不一样。引入了继承的概念后,就引入了一个新的访问标识符protected,派生类可以访问基类的protected成员,但其他类不能访问。必须注意的是,这种访问与类的public成员被其他类对象通过类实例的.或->访问是不一样的,派生类不能通过一个基类对象去访问基类的protected成员,然而派生类可以通过自身类型的对象去访问基类中protected的成员。用下面的例子来说可能比较好一些:
class Item_base{
public:
//public members
private:
//private memebers;
protected:
double price;
};
class Bulk_item: public Item_base{
public:
void memfcn( const Bulk_item&, const Item_base& );
private:
//private members;
};
void Bulk_item::memfcn( const Bulk_item& d, const Item_base &b ){
double ret = price; //ok: use this.price
ret = d.price; //ok: derived class can access base class's protected member
ret = b.price; //error: no access to price from a base class in the derived class context
}
从上面的例子我们也可以看到,在声明Bulk_item为Item_base的派生类时,还在Item_base前面添加了public,这是Bulk_item对Item_base的继承方式的声明。派生类对基类的继承方式不会改变派生类访问基类的级别,但是改变了派生类从基类继承来的成员在派生类中的访问标识符。同样的,总共有三种继承方式:
public继承基类成员保持自己的访问级别,基类的public成员在派生类中仍然是public的,依此类推。
protected继承,基类的public和protected成员到了派生类中都变成protected的,而private成员仍为Private成员。
private继承使基类的所有成员到了派生类中都变成了private成员。
也就是说,继承方式其实改变的是其他类(包括派生类可能有的派生类)对派生类中基类成员的访问方式。public派生类继承了基类的接口,它具有与基类相同的接口,这种方式称为接口继承;private和protected成员相当于继承了基类的操作,但并不把它们开放给其他类用户,这些派生通常被称为实现继承。
(2)友元关系与继承
像其他类一样,基类或派生类可以使其他类或函数成为友元,友元可以访问private和protected数据。友元关系不能继承。基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元,则只有基类具有特殊访问权限,该类的派生类不能访问授予基类友元关系的类。
(3)屏蔽
如果在派生类中定义了一个与基类成员同名的成员,基类的该成员在派生类中被屏蔽。这个成员包括成员函数与数据成员,派生类的成员函数也会屏蔽掉基类的数据成员。即使派生了定义了一个成员函数虽然与基类成员函数同名,但是形参列表不同,这时候如果在派生类中调用该函数而使用的是基类中定义的形参列表,也不会出现所谓的重载。因为编译器一旦在派生类的作用域内找到了同名函数,候选函数集就只会在派生类形成了。虚函数的重新定义与屏蔽不一样,声明函数为虚函数是为了实现动态绑定,这跟非虚函数由编译器确定调用哪个成员是不一样的。当然,可以通过在成员前面加上域访问符以声明使用的是基类的成员。
派生类与基类的转换
如果有一个派生类的对象,可以使用它的地址对基类类型的指针进行赋值或初始化,同样,可以使用派生类型的引用或对象初始化基类类型的引用。还可以使用派生类型的对象对基类对象进行赋值或初始化,但是这两者之间是有微妙的差别的。
把派生类对象的地址或引用赋值给基类类型的指针或引用时,引用或指针直接绑定到该对象,对象本身并未被复制,并且,转换不会在任何方面改变派生类型的对象。
另一方面,如果把一个派生类对象赋值给基类类型的对象,派生类会被阉割而成为一个实实在在的基类类型对象。用派生类对象对基类类型的对象进行赋值或初始化时,一般有两种可能,一种是基类显示定义了将派生类赋值给基类的含义,如通过一个接受派生类引用的构造函数。更大的可能是编译器用派生类中的继承自基类数据成员初始化一个基类对象。
没有从基类类型对派生类类型的自动转换,但是可以static_cast或dynamic_cast强制编译器进行转换,但是这是危险的。
基类的private成员是派生类所访问不到的,一开始我就觉得很奇怪,既然这样,派生类又如何去初始化基类的数据成员呢?原来,在派生类的构造函数中,会首先调用基类的构造函数,通过基类构造函数对基类中的数据成员进行初始化,然后再初始化属于派生类的数据成员。
继承对基类的构造函数和复制控制没有太大的影响,除了在确定提供哪些构造函数时,必须考虑一类新用户。像其他任意成员一样,构造函数可以是protected或private的。
由于初始化派生类的时候,首先必须调用基类的构造函数,这一行为对派生类的复制控制也产生了影响。派生类的复制构造函数必须首先用派生类初始化一个基类,而在派生类的赋值操作符定义中,也必须首先调用基类的赋值操作符。另外,根类的析构函数必须声明为虚函数,这一点跟动态绑定有关。派生类的构造函数和复制控制一般有如下结构:
class Base{
public:
//doesn't have default constructor
Base( double d ):price(d){}
//a base class is asked to have a virtual desctructor
virtual ~Base(){ }
private:
double price;
};
class Derived: public Base{
public:
//constructor, first call base class if it does not have a default constructor
Derived( double d, int t ):Base(d), times(t){}
//copy constructor, initialize the base part using derived part
Derived( Derived& dd ):Base(dd), times(dd.times){ }
//first call the operator=() of the base part
Derived& operator=( const Derived& rhs ){
if( this != &rhs ){
Base::operator=(rhs);
times = rhs.times;
}
return *this;
}
//it's not a base class, so destructor is not necessarilty required
private:
int times;
};
继承所主要考虑的,主要就是基类和派生类之间的关系,派生类继承了基类什么,这些继承来的成员是如何被派生类和其他派生类的类用户使用的,基类和派生类的转换,以有继承对类的复制控制的影响。
二、动态绑定
“在C++中,多态性仅用于对它继承而相关联的类型的引用或指针。”
现在我们知道,多态的一个重要方面是继承,只有通过继承,才可以互换地使用派生类型或基类型的许多形态,所谓的“许多形态”,则是通过动态绑定实现的。通过在基类声明函数为virtual的,派生类重新定义该virtual函数的实现,这样,整个继承层次就有了多态的特征。
引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态性的基石。
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型,也可能是派生类类型。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
前面我们讲到如果把一个派生类型的对象赋值给基类类型的对象,派生类对象会被阉割成基类类型的对象,而使用指针和引用则不会。这样我们就明白了,与多态性相关联的关键字包括继承、虚函数、引用或指针。
现在我们也应该能够明白,为什么根类的析构函数必须是虚函数:
删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前消除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向的派生类对象的基类类型指针。如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。通过把析构函数定义为虚函数,可以在运行时动态调用析构函数的正确版本。
虚函数也可以被重载。如果派生类中声明的函数跟基类的虚函数的形参不一样,则该函数是对应虚函数的重载版本,如果在派生类使用虚函数的形参调用该成员函数,则实际调用的是基类的成员函数。这是虚函数与非虚函数的区别:虚函数是重载,而非虚函数是屏蔽。
与虚函数相关的还有纯虚函数,通过在虚函数的形参列表后面加上=0使函数声明为纯虚函数,含有或继承一个或多个纯虚函数的类是抽象基类,除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。纯虚函数只是声明了函数,并没有实现,具体的实现要在继承该抽象基类的类中实现。
程序=设计+实现。以上部分主要都是设计实现时所需要理解的概念,并未涉及设计。书中最后还提供了两个小节,用于阐述句柄类的设计思路,以及一个简单的检索系统的多态设计与实现。虽然最终我都实现了这些,但是对于他的设计,我却有些望尘莫及。看完这本书,我希望可以进一步看有关设计方面的书吧。
分享到:
相关推荐
C++ PREMIER 中文PDF高清电子书,并有书签,使用方便。
C++premier中文完美版,C++程序开发人员必备书籍
c++ premier 是学习c++的经典著作,通过学习这本数,你可以有很大进步;本资源是书本对应的习题解答,第四版,相当详细
2017-NESC-handbook-premier-edition.zip
中文版 C++premier 第四版中文版
C++ Premier第四版中文英文源代码
c++ premier 第四版 课后习题答案+所有源代码
Premier - Crystal Reports 9 Essentials
中文版chm 电子书, 经典著作,大家分享
Premier - MS Windows Shell Script Programming for the Absolute Beginner.chm
1.1 C++的发展和主要特点 1.2 第一个C++程序以及C++程序开发过程 1.3 C++在非面向对象方面的常用新特性 1.4 程序陷阱 1.5 补充:变量的定义、数据类型、函数等
Synplify Pro and Premier ;Fast, reliable FPGA implementation and debug
总理眼物体检测程序 开始工作之前,Premier-eye需要在设备上创建数据和输出文件夹,并将图像放置在此处以进行识别工作。物体检测模块当地使用要求: Python> = 3.6 对于运行模块,您需要运行API和SPA来发送数据,...
一本非常经典的学习C++的参考书,节省时间,百看不厌!
C++ Primer Plus 课后答案,可用于C++的学习
C++Premier(中文第四版).pdf
XMind是一个非常神奇的软件,能够帮助我们组织思考,很不错的工具。C++ Premier是非常成熟的开发语言讲解书籍,非常经典
吉林大学历年C++考试试题,对于考试复习有很好的帮助!
附件的内容为使用思维导图XMind总结C++标准库的顺序容器,通过把C++ Premier顺序容器翔实的放在一张图片上,可以非常方便的梳理思路,在工作中也能提高工作效率。灵活的使用容器是C++开发人员必须具备的技能