熟悉C++的继承与多态 并完成自己定义的购物车
一、基类和派生类
1.1 继承
C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫作类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。
正如继承一笔财产要比自己白手起家容易一样,通过继承派生出的类通常比设计新类要容易得多。下面是可以通过继承完成的一些工作。
当一个类派生出另一个类,原始类称为基类,继承类成为派生类。
1.2 派生类
派生类对象将具有以下特征:
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
- 派生类对象可以使用基类的方法(派生类继承了基类的接口)
需要在继承属性中添加:
- 派生类需要自己的构造函数
- 派生类可以根据需要添加额外的数据成员和成员函数
1.2.1 派生类成员的访问属性
1、公用继承(public inheritance)
基类的公有成员和受保护成员在派生类中保持原有的访问属性,其私有成员仍为基类私有(基类的封装性)。
公有继承是一种is a关系,即派生类对象也是一个基类对象,可以对基类对象执行任何操作,也可以对派生类对象执行。
2、 私有继承(private inheritance)
基类的公有成员和受保护成员在派生类中成为了私有成员。其私有成员仍为基类私有。
3、受保护的继承(protected inheritance)
基类的公有成员和受保护成员在派生类中成为了受保护成员。其私有成员仍为基类私有。
受保护成员:不能被外界引用,但可以被派生类的成员引用。
1.2.2 构造函数
派生类不能直接访问基类的私有成员,必须通过基类的方法进行访问。故派生类的构造函数必须使用基类的构造函数
。
在创建派生类对象之前,程序首先创造基类对象。
如果不显式的调用基类的构造函数(使用初始化器列表来指名要使用的基类构造函数),则默认使用基类的构造函数:BandAcount()
//写法一(写一种就好)
Bulk_item(const string &strName,double dPrice,int minNum,double dDis):Item_Base(strName,dPrice),m_nMinNum(minNum),m_dDis(dDis){}
//使用成员初始化列表 只有参数名而不包含参数类型=>这些参数是实参而不是形参
//写法二(写一种就好)
Bulk_item(const string &strName,double dPrice,int minNum,double dDis):Item_Base(strName,dPrice){
//函数体内部只对派生类新增的数据成员初始化
m_nMinNum=minNum;
m_dDis=dDis;
}
//写法一(写一种就好)
Bulk_item(const Bulk_item & in){
*this=in;
}
//写法二(写一种就好)
Bulk_item(const Bulk_item & in){
m_nMinNum=in.m_nMinNum;
m_dDis=in.m_dDis;
}
一个类不仅可以派生出一个新的类,派生类还可以继续派生,形成派生的层次结构。
1.2.3 析构函数
释放对象的顺序与创建对象的顺序相反
对象过期时,程序将首先使用派生类析构函数,然后再(自动的)调用基类析构函数。
在派生时,派生类不能继承基类的析构函数,需要通过自己的析构函数去调用基类的析构函数。
1.3 多重继承
一个类可以从多个基类派生——多重继承(多继承)
一般来说,除基类外,每个类只有一个父类。
但是,一些类的表达性质上并不是很准确,只是两个类的合成或组合。则需要几个父类来共同描述其子类的性质。
例如,沙发床同时继承了沙发和床的特征。
C++标准的未来发展趋势趋向于淡化多重继承
1.3.1 声明多重继承
1.3.2多重继承的构造函数
1.3.3多重继承的二义性
(1)两个基类有同名成员
用基类限定名来限定
使用作用域解析运算符(::)来标识函数所属的类
(2)两个基类和派生类三者都有同名函数
基类的同名成员在派生类中被屏蔽(不可见),即派生类新增加的同名函数覆盖了基类中的同名成员。相当于重载。
注意:函数名与特征标相同时才为函数重载
特征标——函数的参数列表(特征标相同==参数数目和类型相同+参数的排列顺序相同(与变量名无关))
(3)如果类A和类B是从同一个基类派生的
通过类N的直接派生类名来指出要访问的时类N的哪一个派生类的基类成员。
1.3.4 多继承的构造函数
注意,是被继承的顺序
(声明的部分),不管其构造函数初始化列表的顺序。
1.4 派生类和基类之间的相互转换
(1)赋值
赋值兼容:不同类型数据之间的自动转换和赋值。
只能用派生类对象对基类对象赋值 ,不能用基类对象对派生类对象赋值 ,因为基类对象不包含派生类成员。同理,同一基类的不同派生类对象之间也不能赋值。
(2)派生类对象可以代替基类对象对基类的应用进行赋值和初始化
派生类对象可以代替基类对象对基类的应用进行赋值和初始化
如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象
(2)指针
指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。
可以使用static_cast
强制编译器进行转换。或者,可以用dynamic_cast
申请在运行时进行检查。
1.5 组合(composition)
类的组合:在一个类中以另一个类的对象作为数据成员
。
继承是”是“的关系,是纵向的,派生类是基类。
组合是”有“的关系,是横向的。
对于组合,成员对象的数据隐私是不能被直接访问的,必须通过成员对象的操作去间接访问
。也就是说,类对象和成员对象之间是彼此独立的。
1.6 虚基类(virtual base class)
C++提供虚基类方法,在继承间接共同基类时只保留一份成员
。
虚基类不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。
当派生类通过多条派生路径继承虚基类时,基类成员只保留一次(虚基类在派生类中只被继承一次)
注意:应该在该基类的所有派生类中直接声明为虚基类,否则仍会出现多次继承。
二、多态性与虚函数
多态性(polymorphism):
向不同的对象发送同一条信息(调用函数),不同的对象在接收时回产生不同的行为、方法(执行不同的函数)
例如函数的重载、运算符重载都是多态。
如果所使用的语言虽然支持继承,但不支持多态,则不能称之为面向对象编程
静态多态性(编译时的多态):在程序编译时就知道调用函数的全部信息,系统就能决定要调用哪个函数。通过函数重载
实现。
动态多态性(运行时的多态):在程序运行过程中动态地确定操作所针对的对象,通过虚函数(virtual function)
实现。
覆盖:同名同类型函数
的定义内容的重写,一般用在父子类的虚函数描述中,表示多态行为。
重载:同名异类型函数的
定义内容的重写,编译器通过分析函数调用中不停参数组合来识别不同的同名函数调用。
C++编译器 对非虚方法使用静态联编(static binding) 对虚方法使用动态联编(dynamic binding)
C++采用一种滞后捆绑(late binding)技术,通过预先设定其成员函数的虚函数性质,使得任何捆绑该成员函数的未定义类型的对象操作在编译之前,都以一个不确定的指针特殊地“引命待发”来编码;到了运行时,遇到确定类型的对象,才突然指定其真正的行为。即滞后到运行时,根据具体类型的对象来捆绑成员函数。
这样一来,辨别对象类型的工作就可以不交给用户做了。
2.1虚函数
虚函数:在基类声明函数时虚拟的,并不是实际存在的函数,然后再派生类中才正式定义此函数。
作用:允许派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问派生类中的同名函数。
若需要在派生类中重新定义基类的方法,通常应该将基类方法声明为虚的
虚函数的使用方法是:
(1)在基类中用virtual声明
成员函数为虚函数。在类外定义虚函数时,不必再加virtual。
只要标记上virtual,则该操作便具有多态性
(2)在派生类中重新定义此函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同
,根据派生类的需要重新定义函数体。
如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
连锁反应
:当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加 virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。
(3)定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
(4)通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。
如果想调用同一类族中不同类的同名函数,只要先使用基类指针指向该类对象即可。
若在基类中定义的非虚函数在派生类中被重新定义。
基类指针 =>对象中基类部分的成员函数
派生类指针 =>派生类对象中的成员函数
这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
2.1.1 虚函数虚析构函数
我们希望:通过基类的指针删除派生类对象时,正确的析构函数被调用,即程序将首先使用派生类析构函数,然后再(自动的)调用基类析构函数
。
若基类的析构函数不为虚析构函数时,用基类指针指向派生类对象的时候,只会调用基类析构函数。
当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统都会采用动态关联,调用相应类的析构函数,对该对象进行清理工作。
通常,给基类提供一个虚析构函数(即使它不需要虚构函数)
2.1.2 虚函数工作原理
2.1.3 注意
(1)构造函数
构造函数不能是虚函数(因为在执行构造函数时类对象还未完成建立过程)
(2)析构函数
析构函数应该是虚函数,除非类不用做基类。
(3)友元函数
友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数。
(可以让友元函数是使用虚成员函数)
(4)没有重新定义
如果派生类没有重新定义函数,则使用该函数的基类版本。
(5)重新定义
第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化:这种例外只适用于返回值,而不适用于参数。
第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
(6)静态成员函数不能是虚函数
静态成员函数的不受对象捆绑,即失去了多态的条件。
(7)内联函数不能是虚函数
因为内联函数不不能在运行中动态得确定其位置。即使虚函数在类的内部定义,在编译时,依然将其看做是非内联的。
2.2 抽象类
2.2.1 抽象基类(abstract base class,ABC)
目的:用它作为基类去建立派生类(抽象类的用途是被继承)。
包含纯虚函数的类都是抽象类(因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的)
抽象意味着不完整,它的实现依赖于具体!抽象类是不允许有实例对象的,由编译器进行管理(即使语法允许,创造出来的对象也不是完整的)
2.2.2 纯虚函数(pure virtual function)
2.3 类型转化
类型可以通过动态类型转换来识别,这也是表现多态的一种方式。
例如:区别共同父亲下的不同子女
C++
2.3.1 动态转型(dynamic_cast)
dynamic_cast操作专门针对有虚函数的继承结构,它将基类指针转化为想要的子类指针,已经做好了子类操作的准备。
参考博客:类型转换(动态转换)dynamic_cast
(1)不可以转换基础数据类型
(2)父子之间可以转换
父转子——不可以
子转父——可以
发生多态—都可以(如果发生了多态,那么可以让基类转成派生类,向下转换)
2.3.2 静态转型(static_cast)
相对动态类型转换,静态类型转换可以做范围更广的转换,但前提是相关类型的(编译器必须认为是可理解的)
静态类型转换不是专门针对指针的,只要是相关类型的转换,都可操作。
void * 到任何类型的指针都可以进行相融性转换
2.3.3 常量转型(const_cast)
type类型转换为const type类型——可以
在作为参数传递到函数后,具有对参数使用的写约束作用。
const type类型转换为type类型——不可以
将常量或者常量对象的地址托付 给变量或者对象的指针和引用,有很大损坏的风险,编译器不予通过。
2.3.4 隐式转化
赋值语句可以使用隐式转换
这里不详细讨论
2.3.5 转换函数
但是必须返回转换后的值(虽然没有声明返回类型)
2.4 句柄与继承
面向对象编程中涉及大量的指针和引用,所指向的对象大多是从动态内存空间中借来的。
由于动态内存空间中的对象总是要由程序员释放的,所以必须有选择地对指针做释放操作。
因为指针的操作,使用起来很别扭,总要进行“ & ”,“ * ”操作,并且还要做“->”间访操作。
由于指针的烫手操作,在抽象基类外面套一个手柄,防止烫手:D。
在面向对象编程中,由于多态,我们维护了在问题发生客观变化时的程序的有效性。但多态使我们的指针处理形式显得有点粗俗,于是利用嵌套多态
的形式,让指针不要直接与用户见面,以一种指针手柄的形式代替指针
。手柄有许多变化,它使面向对象编程更丰富多彩,许多高级编程都用到了手柄技术。
手柄类(Handle)
手柄类是专门拿来处理有多态表现的指针的,这些指针所指向的对象有一个共同点,都是通过某个创建函数来产生对象的,而且该对象的实体一般不复制,传递都是通过指针或引用。所以我们给手柄类做进一步的加工,以适应这种对象指针的性质:
(1)对象通过指针参数
的形式创建,不另外申请内存空间创建指针所指向的对象,但是要重新开辟计数
;
(2)对象通过别的对象创建时
,挂接相同对象,对象计数加1
;
(3)对象复制时
,原对象需要析构,然后挂接相同对象,对象计数加1
;
(4)析构时
,计数值减1
。只有在计数值减至0时,才释放指针指向的对象
;
显然,计数不能通过静态数据成员来做,因为传递一个指针的创建方式,手柄需要重新开辟计数,并不是任何手柄都做同一种计数的。
Sony类,基类是抽象类,里面有一些纯虚函数。
于是,对手柄类做如下进一步的设计:
修改后的手柄变得实用了,运行程序f1306.cpp,就没有内存泄漏的隐患了。
这样一来,手柄就可以随意创建和复制了。通过手柄传递的抽象基类层次中的对象,仍然可以表现其多态,再也没有指针复制的困惑。当然,代价还是有的,那就是额外增加手柄的代价。
手柄只是帮助我们更生动地表现面向对象编程的技术效用,而且,其成员就是固定的那两个必要的指针,开销不算太大,但换来的却是赏心悦目。这种技术实际上已经包含了高级编程中的智能指针(Smart Pointer)和引用计数(Reference Count)。这两项技术是使面向对象编程走向真正实用的基本技术。
实验部分(熟悉C++的继承与多态智能指针 并完成自己定义的购物车)
sy2.h
#ifndef SY
#define SY
#include <string>
using namespace std;
class Bulk_item;
//不使用折扣策略的基类 Item_Base
class Item_Base{
private:
protected:
string m_strName; //商品名
double m_dPrice; //价格
public:
Item_Base(const string &strName="",double dPrice=10.0):m_strName(strName),m_dPrice(dPrice){}
Item_Base(const Item_Base & ib){
m_strName=ib.m_strName;
m_dPrice=ib.m_dPrice;
}
//返回商品名
string book() const{
return m_strName;
}
//基类不须要折扣策略
virtual double net_price(int n) const{
return n*m_dPrice;
}
virtual Item_Base *Clone() const{
return new Item_Base(*this);
}
//析构函数
virtual ~Item_Base(){}
//赋值操作符声明
Item_Base & operator=(const Item_Base &ib){
m_strName=ib.m_strName;
m_dPrice=ib.m_dPrice;
}
friend ostream & operator<<(ostream &out,const Item_Base &s){
out<<s.m_strName<<"\t"<<s.m_dPrice;
return out;
}
};
//批量购买折扣策略:大于一定的数量才有折扣
//派生类 Bulk_item (公有继承)
class Bulk_item:public Item_Base{
protected:
int m_nMinNum; //实现折扣策略的购买量
double m_dDis; //折扣率
public:
//构造函数
Bulk_item(const string &strName,double dPrice,int minNum,double dDis):Item_Base(strName,dPrice),m_nMinNum(minNum),m_dDis(dDis){}
Bulk_item(const Bulk_item & in){
m_nMinNum=in.m_nMinNum;
m_dDis=in.m_dDis;
*this=in;
}
virtual double net_price(int n) const{
return n>=m_nMinNum?n*m_dPrice*m_dDis:n*m_dPrice;
}
virtual Bulk_item *Clone() const{
return new Bulk_item(*this);
}
//析构函数
~Bulk_item(){
delete []this;
m_nMinNum=0;
m_dDis=0;
}
friend ostream & operator<<(ostream &out,const Bulk_item &s){
out<<s.m_strName<<"\t"<<s.m_dPrice<<"\t"<<s.m_nMinNum<<"\t"<<s.m_dDis;
return out;
}
};
#endif //SY2
main.cpp
#include <iostream>
#include <stdio.h>
#include <fstream>
#include "sy2.h"
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
double total(Item_Base &ib){
cout<<"total="<<ib. net_price(10)<<endl;
}
void printT(Item_Base &bi){ // 形参是 Item_Base类对象的引用
//由于派生类对象与基类对象赋值兼容。派生类对象可以自动转换类型
cout<<"printT "<<bi.net_price(10)<<endl;
}
int main() {
cout<<"--------------------------------------------------"<<endl;
Item_Base base("《C++》",10); //定义 Item_Base类对象 base
Item_Base base1("《C++》",10); //定义 Item_Base类对象 base1
Bulk_item b1("《C++》",10,10,0.8); //定义 Bulk_item类对象 b1
cout<<base<<endl;
cout<<b1<<endl;
Item_Base base2(base); //定义 Item_Base类对象 base2
Bulk_item b2(b1); //定义 Bulk_item类对象 b2
cout<<base2<<endl;
cout<<b2<<endl;
cout<<base.book()<<"base.net_price(10):"<<base.net_price(10)<<endl;
cout<<b1.book()<<"bi.net_price(10):"<<b1.net_price(10)<<endl;
cout<<"--------------------------------------------------"<<endl;
Item_Base *pBase1=&base; //定义指向 Item_Base类对象的指针 pBase1,指向base
cout<<"pBase1->net_price(10):"<<pBase1->net_price(10)<<endl;
pBase1=&b1; //指针 pBase1,指向 Bulk_item类对象 b1
cout<<"函数重载 (不是多态) :"<<endl;
cout<<"*pBase1:"<<(*pBase1)<<endl; //运算符<<重载(不是多态
cout<<"b1:"<<b1<<endl;
cout<<"虚函数(多态): "<<endl;
cout<<"pBase1->net_price(10):"<<pBase1->net_price(10)<<endl;//net_price() 多态
cout<<"b1.net_price(10):"<<b1.net_price(10)<<endl;
Item_Base *pBase2=pBase1->Clone(); //定义指向 Item_Base类对象的指针 pBase2,指向 pBase1->Clone()
cout<<"*pBase2:"<<(*pBase2)<<endl;
Item_Base *pBase3=pBase1->Clone(); //定义指向 Item_Base类对象的指针 pBase3,指向 pBase1->Clone() 多态
cout<<"*pBase3:"<<pBase3->net_price(10)<<endl;
Item_Base *pBase4=b1.Clone(); //定义指向 Item_Base类对象的指针 pBase3,指向 b1.Clone()
cout<<"*pBase4:"<<pBase4->net_price(10)<<endl;
cout<<"--------------------------------------------------"<<endl;
//赋值转换
//b2=base2; //errorn 不能用基类对象对派生类对象赋值 (应为基类中不包含
base2=b2; //只能用派生类对象对基类对象赋值
cout<< base2.net_price(10)<<endl;
cout<< b2.net_price(10)<<endl;
//指针转换
Item_Base *pBase5=&base;
Bulk_item *pA=static_cast<Bulk_item*>(pBase5); //base
printT(*pA);
Bulk_item *pB=dynamic_cast<Bulk_item*>(pBase1);//b1
printT(*pB);
return 0;
}