多态
2018-12-31 本文已影响0人
gpfworld
1.所谓多态
多态就是一种特殊的机制,执行这种机制的结果就是,调用同一个接口,在不同的情况下
可以实现不同的功能。
(1)c语言模拟的多态
讲c语言时强调过,实际上c语言也是可以模拟多态,主要时利用函数指针
进行函数的回掉实现的,这个实现过程往往会借助于结构体进行封装实现。
(2)面向对象语言的多态
在面向对象的语言中多态的实现依赖三个机制
(1)继承
(2)函数重写
(3)向上转型
(4)动态绑定
以上多态实现方式中,不管是那种实现方式,在逻辑上都实现了调用同一个函数接口时调
用的是不同派生类的重写函数,从而得到不同的执行结果。
2.基类与派生类成员函数同名
分为两种情况,一种是函数签名完全相同,另一种是同函数名但不同参数列表。
(1)函数签名完全相同(也叫函数的重写)
(1)子类访问基类同名函数时,先使用using进行声明,然后使用
“基类名::成员函数()”的方式进行调用即可
比如:
using People::show;
People::show();
(2)如果基类成员函数是public, 在外界希望对通过派生类基类同名
函数进行访问时,使用static_cast<>()将派生类向上强制转换
为基类后调用的就是基类的被同名的函数。
(3)注意:
如果基类中该函数又被重载多次,那么在子类内部活在在外部希望通过
子类访问父类的同名函数时,可以通过参数列表的进行重载的区分区分
调用,局提请看下面的例子。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
People(const string name="zhanagsan"):name(name) { }
void show() {
cout << "111基类name:"<<name<<endl;
}
//重载的show函数
void show(int a) {
cout << "222基类name:"<<name<<endl;
}
public:
string name;
};
class Student : public People {
public:
Student(const string name="wangwu"):name(name) { }
using People::show;
void show() {
People::show(10);//通过参数列表区分重载函数
cout << "派生类name:"<<name<<endl;
}
public:
string name;
};
int main(void)
{
Student stu1 = Student();
stu1.show();//通过参数列表区分重载函数
static_cast<People>(stu1).show();//通过参数列表区分重载函数
return 0;
}
(2)同名但参数列表不同
(1)这种情况下,如果希望在子类中访问基类同名但不同参数列表的
成员函数,针对这种情况时,以下方式都可以:
比如还是以show函数为例:
(1)声明:using People::show;
调用:People::show(10);
(2)声明:using People::show;
调用:show(10);//可以省略People::
(3)调用:People::show(10);//可以直接省略using声明
(2)如果希望在外部通过子类访问基类的同名但不同参数列表的成员函数
时,不需要做向基类的转换,可以通过参数列表的不同直接区分
调用的是基类的函数还是子类的函数。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
People(const string name="zhanagsan"):name(name) { }
void show(int a) {
cout << "基类name:"<<name<<endl;
}
public:
string name;
};
class Student : public People {
public:
Student(const string name="wangwu"):name(name) { }
using People::show;
void show() {
People::show(10);
cout << "派生类name:"<<name<<endl;
}
public:
string name;
};
int main(void)
{
Student stu1 = Student();
stu1.show();//通过参数区分,调用的时子类的函数
stu1.show(10);//通过参数区分,调用的时基类的函数
return 0;
}
3.成员函数重写
(1)什么是成员函数的重写
派生类中函数签名与基类函数签名完全相同的情况也被称为重写。
注意区分重写和重载,
重载:在通过一个命名空间的,比如同一个类成员函数重载,或者外部函数
重载。
重写:重写对于多态机制的实现非常的重要,c++中多态的实现靠的就是子类对
父类虚函数的重写实现的。
(2.普通函数的重写与静态绑定
(1)静态绑定的规则
调用某个对象的函数时,只会调用该对象内实现该函数,即便该函数在派生类中有被
重写,但它不会调用该函数在派生类中的重写版本。
(2)静态绑定由谁决定
静态绑定是由编译器决定的,静态绑定后,这种绑定关系不可以在程序运行时被改变。
(3)什么情况下遵守静态规则
对普通函数进行重写时,函数的调用遵守静态规则,这一点与java不一样,java对普通
函数的重写是动态绑定。
注意:实际上对普通函数进行重写并没有太大的实际意义。
(4)静态绑定的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class Box
{
protected:
int length;
int width;
int height;
public:
Box(int length, int width, int height)
:length(length),width(width),height(height) { }
int volume() {return length*width*height;}
void showvolume() {cout<<"体积:"<<volume()<<endl;}
};
class Thinbox : public Box {
public:
Thinbox(int length, int width, int height):Box(length, width, height) { }
int volume() {return length*width*height*(1-0.2);}
//void showvolume() {cout<<"体积:"<<volume()<<endl;}打开)
};
int main(void)
{
Thinbox tbox(3, 3, 3);
Box *boxp = &tbox;
tbox.showvolume();
boxp->showvolume();
return 0;
}
运行结果:
体积:27
体积:27
例子分析:
例子中,定义了一个Box类,类中实现了两个函数,一个用于计算体积的
volume函数,该函数计算体积时,没有考虑盒子壁的厚度。
另一个是用于显示体积的函数,
void showvolume() {cout<<"体积:"<<volume()<<endl;}
从Box中派生了一个Thinbox类,在这个类中并没有新增任何的成员,只是
重写了基类的volume()函数,
int volume() {return length*width*height*(1-0.2);}
考虑到盒子的厚度,因此体积*(1-0.2),进行了体积估算。
但是没有重写showvolume()函数,所以通过子类调用的showvolume()函数是
从基类继承而来的showvolume函数。
在主函数中,定义了一个派生类Thinbox的对象tbox,然后在再定义了一个
基类Box类型的指针boxp,该指针指向派生类tbox。
面函数中通过如下两条语句显示体积。
tbox.showvolume();
box->showvolume();
通过tbox调用showvolume()函数时,因为派生类中没有该函数,就进入派生类
对象中的基类对象中调用基类的该函数(继承过来的函数),基类该函数在调
用volume函数时,发现在基类中存在,根据静态绑定规则,不管这个函数有没
有重写,调用的是这个函数本身,而不是它在派生类中的重写函数。
boxp->showvolume()是通过基类指针调用基类showvolume()函数,由于boxp是
基类类型的指针,所以直接进入派生类的基类对象中调用showvolume()函数,
该函数在调用volume函数,过程同样遵循静态绑定规则。
从如下显示结果来看:
体积:27
体积:27
说明派生类的volume()函数并没有被调用,因为没有*(1-0.2)进行体积缩减。
如果我们在派生类中将派生类中将派生类的如下显示函数打开,
void showvolume() {cout<<"体积:"<<volume()<<endl;}打开)
然后再次编译运行,结果如下
体积:21
体积:27
在tbox.showvolume();调用时,首先在当前类对象查找,由于在派生类对象tbox
中有定义showvolume();函数,会根据静态规则调用该函数,以及涉及到的volume函数。
4.虚函数的重写和动态绑定(多态)
(1)动态绑定的规则
调用某对象中的函数时,如果函数在派生类中被重写了的话,那么优先调用派生类中
被重写的函数,否则才会调用自己定义的函数。
(2)如何才能实现动态绑定
对虚函数进行重写,只有虚函数才能实现动态绑定。
(3)静态绑定与动态绑定对比
(1)静态绑定先到当前类对象和基类对象中寻找
(2)动态绑定与静态绑定相反,先到当前类的派生类对象中寻找,找不到才使用当前类和基类中的函数
(3)重写普通函数时遵守的都是静态绑定规则,重写虚函数时遵守的都是动态绑定规则。
(4)虚函数的声明格式
virtual 返回值 fun(...) { }
注意:
(1)如果函数的定义和声明是分开的,那么virtual应该放在函数的声明前。
(2)派生类中对基类虚函数进行重写后,这个重写函数仍然还是一个虚函数,不管有没有显式表明virtual关键字。
(3)在派生类中,为了让语义更加清晰,应该在重写的虚函数前面加virtual关键字
(4)如果基类中的虚函数是const,那么子类中重写的函数必须也是const
(5)动态绑定举例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class Box
{
protected:
int length;
int width;
int height;
public:
Box(int length, int width, int height)
:length(length),width(width),height(height) { }
virtual int volume() {return length*width*height;}
virtual void showvolume() {cout<<"体积:"<<volume()<<endl;}
};
class Thinbox : public Box {
public:
Thinbox(int length, int width, int height):Box(length, width, height) { }
int volume() {return length*width*height*(1-0.2);}
};
int main(void)
{
Thinbox tbox(3, 3, 3);
tbox.showvolume();
return 0;
}
运行结果:
体积:21
例子分析:
在例子中,通过派生类对象tbox调用showvolume();函数时,由于该函数在派生类中并
没有被重写,所以调用的实际上时基类的showvolume(),基类的showvolume()调用
volume函数计算体积时,由于该函数时虚函数,并且该函数被派生类重写了,根据动
态绑定的规则,需要优先调用被派生类中重写的函数,因此结果是21。
5. 动态绑定和多态
(1)向上转型和向下转型
有了动态绑定,距离多态的实现也就只有一步之遥,如果需要利用动态绑定实现多态,我
们还需要加入向上转型的操作。所谓向上转型,就是使用基类的指针或者引用指向派生类
对象。利用基类指针调用派生类中重写的虚函数时,实现的就是多态的调用。
(1)向上转型举例
向上转型例子:
Thinbox tbox(3, 3, 3);
Box *boxp = &tbox;
向上转型其实就是类型的强制转换,只是向上转型都是自动进行的,不需要进行
显式的强制转换。
(3)转型涉及的静态类型和动态类型
在向上转型的过程中,=两边涉及两种类型,=左边的类型被称为静态类型,
右边的类型称为动态类型,动态的意思表示对象的类型不定,可以与左边类型
相同,也可以是左边类型的任何一个派生类型的对象。
(4)向下转型
有向上转型就有向下转型,但是向上转型时是无条件成立的,因为定义派生类对象
时一定会实例化其基类对象,但是向下转型不一定会成功,只有向上转型后的指针
或者引用才能向下转型,因为这样才能保证派生类对象相对于基类多出的空间一定存在。
向上转型时大空间向小空间转换,而向下转型时小空间向大空间转换,所有向下转型
时要格外小心。向下转型时必须显式的进行类型的强制转换。
向下转型举例:
(1)错误例子
Box box(3, 3, 3);
Box *boxp= &box;
Thinbox *tboxp = (Thinbox *)boxp;
(2)正确例子
Thinbox tbox(3, 3, 3);
Box *boxp= &box;
Thinbox *tboxp = (Thinbox *)boxp;
(2)多态举例
多态只有两种形式,一种是通过指针方式实现,另一种是通过引用实现。
(1)指针实现初始化
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
/* 普通箱子 */
class Box
{
protected:
int length;
int width;
int height;
public:
Box(int length, int width, int height)
:length(length),width(width),height(height) { }
/* 虚函数 */
virtual int volume() const {return length*width*height;}
};
/* 薄箱子 */
class Thinbox : public Box {
public:
Thinbox(int length, int width, int height):Box(length, width, height) { }
/* 由于基类的volume函数是虚汗数,就算这里不知定virtual,也是虚函数 */
virtual int volume() const {return length*width*height*(1-0.2);}
};
/* 厚箱子 */
class Thickbox : public Box {
public:
Thickbox(int length, int width, int height):Box(length, width, height) { }
virtual int volume() const {return length*width*height*(1-0.5);}
};
int main(void)
{
Thinbox thinbox(3, 3, 3);
Thickbox thickbox(3, 3, 3);
Box *boxp = NULL;
cout <<"选则显示那个箱子的体积"<<endl;
cout <<"1.薄箱子"<<endl;
cout <<"2.厚箱子"<<endl;
cout <<"选择:";
int select = 0;
cin >> select;
if(1 == select) boxp = &thinbox;
else if(2 == select) boxp = &thickbox;
cout <<"箱子体积:"<< boxp->volume() << endl;
return 0;
}
运行结果:
运行时提示,选择显示社么箱子的体积,选择1,表示显示薄箱子的体积,
如果选择2,表示选择厚箱子的体积,选择1,运行结果如下。
选则显示那个箱子的体积
1.薄箱子
2.厚箱子
选择:1
箱子体积:271
例子分析:
main函数中定义了一个Box基类的指针,根据不同选择,基类指针分别指向不同
派生类对象,通过基类调用volume函数时,根据情况的不同,实际上调用的是
不同派生类对象的volume函数,这就是多态。
(2)引用实现多态
还是上面的例子,但是将main函数改为如下形式,并增加一个子函数。
void showvolume(const Box &box) {
cout <<"箱子体积:"<< box.volume() << endl;
}
int main(void)
{
Thinbox thinbox(3, 3, 3);
Thickbox thickbox(3, 3, 3);
cout <<"选则显示那个箱子的体积"<<endl;
cout <<"1.薄箱子"<<endl;
cout <<"2.厚箱子"<<endl;
int select = 0;
cin >> select;
if(1 == select) showvolume(thinbox);
else if(2 == select) showvolume(thickbox);
return 0;
}
例子分析:
引用只能初始化,不能赋值,这一点显然与指针不同,因此通过引用实现多态,
通常都是通过函数形参实现的,多次调用该函数,可以多次对引用类型的形参
进行初始化。
(3)虚函数指针表vtable
之所以能够实现多态,是因为对象中会维护一张虚函数指针表vtable,当使用基
类被重写的接口调用虚函数时,会到表中查找需要动态绑定调用的重写函数,这
个实现机制会使实现多态的对象多消耗几个字节来存放虚函数表。但是对于多态
带来的好处来说,多消耗几个字节资源算不上什么。
因此,如果我们的多态实现有错误时,编译时往往可能会提示vtable表有问题。
6. 纯虚函数
(1)什么是纯虚函数
只有虚函数的声明,没有函数定义的就是纯虚函数。
纯虚函数的格式:virtual 返回值 函数名(参数列表) = 0;
如果函数是const类型的函数,=0需要放在const的后面,定义纯虚函数时,必写=0的标识。
(2)为什么需要纯虚函数
很多时候,基类中的虚函数只需要留下一个接口即可,不需要具体实现,因为虚函数的具
体实现应该留给派生类的重写函数来完成。
在java中普通函数就可以实现多态的,但是在c++中,只有虚函数才能实现多态,C++中的
纯虚函数就是java中的抽象函数。c++和java的实现多态时,函数重写的的对应关系是这样的。
c++中的虚函数重写 <===对应===> java中普通函数的重写
c++中的纯虚函数重写 <===对应===> java中抽象函数的重写
比如完全可以将前面多态例子中的Box基类中的volume()函数设置纯虚函数。
virtual int volume() const = 0;
程序运行的结果与之前是一样。
(3)抽象类
(1)什么是抽象类
只要有包含纯虚函数的类就是抽象类。
(2)抽象类的作用
抽象类用于给定函数接口,搭建程序框架,所有的函数的具体实现全部由派生类重写
实现。抽象类固定了接口,但是代码的实现却很灵活,都掌握在派生类手中。
(3)抽象类特点
(1)不能实例化对象,只能通过派生类实例化对象
(2)如果派生类不全部实现抽象基类的纯虚函数的话,派生类也是一个抽象类,
被实现了的纯虚函数就变为了普通虚函数。
如果在子类中继续将函数保持为纯虚函数的话,=0标志可以不用设置。
(3)抽象类虽然不能直接实例化,但是有自己构造函数,因为通过派生类间接实现
抽象类实例化时,需要在派生类的初始化列表中调用抽象类的构造函数初始
化其成员。
抽象类的构造函数不能是纯虚函数,通过派生类对象实例化抽象基类时,没
有办法调用虚构造函数初始化抽象类的成员,这会导致不确定的结果。
(4)由于抽象类不能创建对象,因此不能把抽象类作为参数类型和返回类型,因为
作为参数和返回类型时都需要开辟对象空间,但是抽象类并不允许被实例化。
(5)由于抽象类不会直接实例化,因此抽象类的构造函数通常都是由子类初始化
列表调用,外部不会调用,因此通常做法是将抽象类的构造函数设置为
protected。
7.虚析构函数
(1)通过基类指针释放派生类对像时遇到的问题
常常会使用基类指针指向动态分配的派生类对象,当我们通过基类指针释放派生类的空间
时,往往会出现问题,比如还是利用前面Box的例子来说明这个问题。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
/* 普通箱子 */
class Box
{
protected:
int length;
int width;
int height;
public:
Box(int length, int width, int height)
:length(length),width(width),height(height) { }
~Box() {
cout<<"Box' destructor"<<endl;
}
/* 虚函数 */
virtual int volume() const = 0;
virtual int volume1() const { }
};
/* 薄箱子 */
class Thinbox : public Box {
public:
Thinbox(int length, int width, int height):Box(length, width, height) { }
~Thinbox() {
cout<<"thinbox' destructor"<<endl;
}
/* 由于几类的volume函数是虚汗数,就算这里不知定virtual,也是虚函数 */
virtual int volume() const {return length*width*height*(1-0.2);}
};
/* 厚箱子 */
class Thickbox : public Box {
public:
Thickbox(int length, int width, int height):Box(length, width, height) { }
~Thickbox() {
cout<<"Thickbox' destructor"<<endl;
}
virtual int volume() const {return length*width*height*(1-0.5);}
};
int main(void)
{
Box *boxp[] = {new Thinbox(3,3,3), new Thickbox(3,3,3)};
for(int i=0; i<sizeof(boxp)/sizeof(boxp[0]); i++) {
delete(boxp[i]);
}
return 0;
}
运行结果:
Box' destructor
Box' destructor
例子分析:
例子中分别在基类和派生类中加入了各自的析构函数,主函数中定义了一个
基类指针数组,元素分别指向派生类对象,通过基类指针释放派生类对象时,
希望调用派生类各自的析构函数,然后再调用基类的析构函数。
显然从结果看,没有达到我们想要的结果,因为并没有调用派生类的析构函数。
(2)虚析构函数
通过基类指针释放派生类对象时,如果希望调用派生类析构函数的话,就应该使用虚析
构函数来解决这个问题。
使用虚析构函数时,应该将基类的析构函数设置为虚析构函数,设置派生类的虚析构函
数并不起作用。
如果在上面例子中基类的虚构函数前面添加virtual关键字即可。
virtual ~Box() {
cout<<"Box' destructor"<<endl;
}
再次编译运行,就会得到如下正确结果:
thinbox' destructor
Box' destructor
Thickbox' destructor
Box' destructor
(3)什么时候使用虚析构函数
使用继承时,并且类中至少包含一个虚函数时,我们应该总是把基类的析构函数声明
为虚析构函数,以保证析构函数被正确调用,实现资源的正确释放。
8. typeid操作符
(1)作用有两个
(1)确认类型
(2)类型统一
typeid()操作符相当于java中的instanceof()操作符。
(2)typeid操作符详解
(1)使用格式
typeid(expression)
expression可以是:
(1)类型,
(1)基本类或者基本类型相关的指针和引用类型
(2)组合类型,数组,结构体,联合体,类类型,以及相关的指针引用
比如:
typeid(int),typeid(Student)
(3)具体对象
具体数值,结构体/联合体/变量,类对象
比如:
int a;
typeid(a);
或者
type(10);
(2)操作符返回类型
使用typeid操作符,返回的是type_info类类型的引用,
(1)该类类型特点
(1)实现了虚析构函数,可以作为基类安全使用
(2)它的构造函数,副本构造函数,=重载函数都被private限制因此不
能直接使用该类型实例化对象,不能初始化,不能进行对象的赋值运算。
(3)使用typeid操作符是唯一产生type_info类型对象的方法。
(2)该类型实现了的运算符
(1)t1==t2:类型相同表达式为true,否者false
(2)t1!=t2:类型不相同表达式为true,否者false
(3)t.name():返回类型名称
(4)例子
int main(void)
{
int a = 10;
int b = 10;
long c = 120;
if(typeid(a)==typeid(b)) {
cout<<"a 与 b的类型相同\n"<<endl;
}
if(typeid(c)!=typeid(23.7)) {
cout<<"c 与 23.7的类型相同\n"<<endl;
}
if(typeid(c)==typeid(long)) {
cout<<"c是long型\n"<<endl;
}
int *p = &a;
cout <<"p的类型"<<typeid(p).name() << endl<<endl;
cout <<"*p的类型"<<typeid(*p).name() << endl<<endl;
cout <<"*p的类型"<<typeid(*p).name() << endl<<endl;
return 0;
}
以上测试方式对于结构体,数组,联合体等组合类型同样有效。
(3)静态类型确认
静态类型与sizeof同,但是sizeof不能提供==/!=等操作,静态类型的确认是由编译器
在编译时进行计算的。
(1)哪些情况是静态类型确认
(1)对于非类类型的类型确认,以及不包含虚函数的类对象的类型确认,
都是静态类型确认。
(2)如果typeid参数是类型的时候,比如
typeid(int)
或者
typeid(Box)
进行的都是静态类型确认
(3)动态类型确认RTTI(run-time Type identification)
(1)什么时候进行动态类型确认,什么是动态类型确认
对指向派生类对象的包含有虚函数的基类指针解引用和引用使用typeid进行
类型确认时,得到的是派生类的类型,这就是动态类型确认。
注意:
(1)这里强调的是基类指针解释引用(*p)和引用,如果直接对基类类型,指
针或者对象进行typeid时,是静态类型确认。
(2)引用必须初始化,但是指针可以为NULL,如果动态确认时,基类指针为
NULL,会导致typeid动态类型确认失败。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
#include <typeinfo>
using namespace std;
class Box
{
protected:
int length;
int width;
int height;
public:
Box(int length, int width, int height)
:length(length),width(width),height(height) { }
virtual ~Box() { }
/* 虚函数 */
virtual int volume() const {}
};
/* 薄箱子 */
class Thinbox : public Box {
public:
Thinbox(int length, int width, int height):Box(length, width, height) { }
/* 由于几类的volume函数是虚汗数,就算这里不知定virtual,也是虚函数 */
virtual int volume() const {return length*width*height*(1-0.2);}
};
int main(void)
{
Thinbox thinbox(1,2,3);
Box box1(1,2,3);
Box &box2 = thinbox;
Box *boxp = NULL;
cout << "对类型直接typeid是静态类型确认" <<endl;
cout << "typeid判断基类类型Box的类型为:" <<typeid(Box).name() << endl;
cout << "typeid判断派生类类型Thinbox的类型为:" <<typeid(thinbox).name() << endl;
cout << "\n对类对象或者指针typeid也是静态类型确认" <<endl;
cout << "typeid判断对象thinbox的类型为:" <<typeid(thinbox).name() << endl;
cout << "typeid判断对象box1的类型为:" <<typeid(box1).name() << endl;
cout << "typeid判断基类指针boxp的类型为:" <<typeid(boxp).name() << endl;
cout << "\n对指向派生类对象的包含有虚函数的基类";
cout <<"指针解引用和引用使用typeid时是动态类型确认" <<endl;
cout << "typeid判断基类引用box2的类型为:" <<typeid(box2).name() << endl;
cout << "typeid判断基类指针*boxp的类型为:" <<typeid(*boxp).name() << endl;
return 0;
}
(2)动态确认的好处
使用动态类型,便于在程序中根据类型的不同进行选择不同的分支运行。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
#include <typeinfo>
using namespace std;
/* 普通箱子 */
class Box
{
protected:
int length;
int width;
int height;
public:
Box(int length, int width, int height)
:length(length),width(width),height(height) { }
virtual ~Box() { }
/* 虚函数 */
virtual int volume() const {}
};
/* 薄箱子 */
class Thinbox : public Box {
public:
Thinbox(int length, int width, int height):Box(length, width, height) { }
/* 由于几类的volume函数是虚汗数,就算这里不知定virtual,也是虚函数 */
virtual int volume() const {return length*width*height*(1-0.2);}
};
/* 厚箱子 */
class Thickbox : public Box {
public:
Thickbox(int length, int width, int height):Box(length, width, height) { }
virtual int volume() const {return length*width*height*(1-0.5);}
};
//void using_box(const Box *box) {
void using_box(const Box &box) {
cout <<"箱子体积:"<<box.volume()<<endl;
//if(typeid(*box)==typeid(Thinbox)) {//使用指针时,需要解引用
if(typeid(box)==typeid(Thinbox)) {
cout<<"薄箱子,不结实,用来装衣物!"<<endl;
//} else if(typeid(*box)==typeid(Thickbox)) {
} else if(typeid(box)==typeid(Thickbox)) {
cout<<"厚箱子,很结实,用来装健身用品!"<<endl;
}
}
int main(void)
{
Thinbox thinbox(1,2,3);
Thickbox thickbox(1,2,3);
//using_box(&thinbox);
using_box(thinbox);
printf("\n");
//using_box(&thickbox);
using_box(thickbox);
return 0;
}
运行结果:
箱子体积:4
薄箱子,不结实,用来装衣物!
箱子体积:3
厚箱子,很结实,用来装健身用品!
例子分析:
在例子中的using_box函数中,
void using_box(const Box &box) {
cout <<"箱子体积:"<<box.volume()<<endl;
if(typeid(box)==typeid(Thinbox)) {
cout<<"薄箱子,不结实,用来装衣物!"<<endl;
} else if(typeid(box)==typeid(Thickbox)) {
cout<<"厚箱子,不结实,用来装健身用品!"<<endl;
}
}
对类型进行了动态确认,根据基类引用box指向的不同的派生类类型
,通过分支语句选择做不同的事情。
从这里我们看出,使用基类引用或者指针有一个好处就是可以用于
统一各种不同的派生类型对象的传参。
(3)动态类型确认带来的问题
动态类型确认有它一定的用途,但是动态类型确认也带了一个问题,那就是
不利于解耦。
我们之所以使用多态,是为了实现分层结构的程序开发,层与层之间应该尽
可能的减少耦合。
但是在上面使用动态类型确认的例子中,using_box函数涉及的的耦合性太强,
因为该函数里面使用到了具体的派生类型,如果派生类型发生改变,using_box
函数就必须相应改动,显然耦合性太强。
在实际开发中,应该尽量利用多态的特点,在上层使用基类提供的统一接口调用
下层派生类中重写的函数,降低层级间的耦合。
9. 使用多态降低耦合
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
#include <typeinfo>
using namespace std;
class Plane {
public:
Plane(const string name): name(name) {}
virtual ~Plane() {}
virtual void turn_left(int agree) const=0;
virtual void turn_right(int agree) const=0;
virtual void go_forward(int speed) const=0;
protected:
string name;
};
class Byplane: public Plane {
public:
Byplane(const string name="by747"):Plane(name) {}
void turn_left(int agree) const {
cout<<name<<" turn_left "<<agree<<"度"<<endl;
}
void turn_right(int agree) const {
cout<<name<<" turn_right "<<agree<<"度"<<endl;
}
void go_forward(int speed) const {
cout<<name<<" go forward "<<speed<<"英里"<<endl;
}
};
class Airplane: public Plane {
public:
Airplane(const string name="air380"):Plane(name) {}
virtual ~Airplane() {}
void turn_left(int agree) const {
cout<<name<<" turn_left "<<agree<<"度"<<endl;
}
void turn_right(int agree) const {
cout<<name<<" turn_right "<<agree<<"度"<<endl;
}
void go_forward(int speed) const {
cout<<name<<" go forward "<<speed<<"英里"<<endl;
}
};
void oprate_plane(const Plane &plane) {
cout << "飞机操作选择" << endl;
cout << "1.左转" << endl;
cout << "2.右转" << endl;
cout << "3.前进" << endl;
int select = 0;
cin>>select;
int tmp = 0;
if(1 == select) {
cout << "输入转向角度数" << endl;
cin>>tmp;
plane.turn_left(tmp);
} else if(2 == select) {
cout << "输入转向角度数" << endl;
cin>>tmp;
plane.turn_right(tmp);
} else if(3 == select) {
cout << "输入前进速度" << endl;
cin>>tmp;
plane.go_forward(tmp);
}
}
int main(void)
{
Byplane byplane("by747");
Airplane airplane("air380");
oprate_plane(byplane);
return 0;
}
例子分析:
在operate_plane函数中,所有的操作只与基类相关,与派生类没有任何直接关系,
就算是开发出新的派生类,只要遵守plane基类规定的接口,就可以直接使用
operate_plane函数操作,无需任何更改,完全解除了operate_plane函数与派生类
之间的耦合。
10. 类成员指针
(1)通过指针访问类对象数据成员
(1)普通指针
直接看例子:
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
class Rectangl {
public:
Rectangl(int length=1, int width=1):length(length), width(width) { }
int *get_length_ponter() { return &length; }
int length;
protected:
int width;
};
int main(void)
{
Rectangl rect1(2, 3);
int *p = NULL;
p = &rect1.length;
printf("p=%p\n", p);
printf("*p=%d\n", *p);
p = rect1.get_length_ponter();
printf("p=%p\n", p);
printf("*p=%d\n", *p);
return 0;
}
运行结果:
p=0xbff36394
*p=2
p=0xbff36394
*p=2
例子分析:
例子中,直接使用普通指针存放对象数据成员地址,或者可以使用
getter取得数据成员地址,从运行结果来看,一般指针存放对象数
据成员的地址是可行,这与使用一般指针存放结构体地址是一样的。
(2)类成员指针
(1)与普通指针的区别
访问类的数据成员,还可以使用类成员指针,类成员指针与普通指针区别在
于,普通指针存放的是绝对地址,但是类成员指针存放的确是相对于类对象
首字节地址的相对地址差,因此不能使用该地址直接访问数据成员,必须与
对象绑定后才能访问指向的成员。
(2)类指针定义和使用格式
成员类型 类名::*pointer_name;
比如:
int Student::*p = &Student::age;
Student stu1("zhangsan", 20);
Student *stu2 = new Student("wangwu", 30);
int a = stu1.*age;
int a = stu2->*age;
(3)使用举例
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
class Rectangl {
public:
Rectangl(int length=1, int width=1):length(length), width(width) { }
int length;
int width;
};
int main(void)
{
Rectangl rect1(2, 3);
Rectangl *rect2 = new Rectangl(2, 3);
int Rectangl::*p = NULL;
p = &Rectangl::width;
printf("%p\n", p);
printf("%d\n", rect1.*p);
printf("%d\n", rect2->*p);
return 0;
}
运行结果:
0x4
3
3
例子分析:
从打印出来的地址来看,并不是一个绝对地址,只是一个地址相
对值,在通过类成员指针访问类数据成员时,一定要注意与对象
绑定后才能访问成员,而且要注意访问格式。
(2)通过指针访问对象成员函数
(1)与使用指针访问数据成员对比
与前面的数据成员不同,对于成员函数,如果希望通过函数指针访问,只能通过类
成员函数指针访问,不能通过普通函数指针访问,同样,访问时也需要和对象绑定。
(2)格式
(1)定义格式
返回值 (类名::pointer_name)(形参类型列表)
样子有点复复杂,可以使用typedef关键字进行简化。
typedef 返回值 (类名::pointer_name)(形参类型列表)
这个时候pointer_name就是个别名,使用这个别名就可以用于
定义类函数成员指针了。
如果有成员函数有const的话,需要加cosnt。
(2)访问格式
(对象名.*函数指针)(实参)
或者:
(对象名->*函数指针)(实参)
注意使用时要加括号,否者函数指针将优先与参数列表先结合,显然是不对的。
(3)使用例子
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
class Rectangl {
public:
Rectangl(int length=1, int width=1):length(length), width(width) { }
int get_length() const {
return length;
}
private:
int length;
int width;
};
typedef int (Rectangl::*fun_type)() const;
int main(void)
{
Rectangl rect1(2, 3);
Rectangl *rect2 = new Rectangl(2, 3);
fun_type get_funp;
get_funp = &Rectangl::get_length;
cout << get_funp <<endl;
cout << (rect1.*get_funp)() <<endl;
cout << (rect2->*get_funp)() <<endl;
return 0;
}
运行结果:
1
2
2
例子分析:
与类的数据成员指针的使用实际上是一样的,使用时需要与对象绑定。
类的函数成员指针变量存放的任然是一个地址偏移(相对地址差值)。
(4)类成员作为传参
(1)普通函数的传参
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
class Rectangl {
public:
Rectangl(int length=1, int width=1):length(length), width(width) { }
int get_length() const {
return length;
}
int length;
int width;
};
typedef int (Rectangl::*fun_type)() const;
void show(fun_type funp, int Rectangl::*datap) {
Rectangl rect1(2, 3);
Rectangl *rect2 = &rect1;
cout << "长:" << (rect1.*funp)() << endl;
cout << "宽:" << rect2->*datap << endl;
}
int main(void)
{
fun_type get_funp;
get_funp = &Rectangl::get_length;
int Rectangl::*widthp = &Rectangl::width;
show(get_funp, widthp);
return 0;
}
(2)成员函数的传参
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
class Rectangl;//类声明
typedef int (Rectangl::*fun_type)() const;
class Rectangl {
public:
Rectangl(int length=1, int width=1):length(length), width(width) { }
int get_length() const {
return length;
}
void show(fun_type funp, int Rectangl::*datap) {
cout << "长:" << (this->*funp)() << endl;
cout << "宽:" << this->*datap << endl;
}
int length;
int width;
};
int main(void)
{
Rectangl rect1(2, 3);
fun_type get_funp;
get_funp = &Rectangl::get_length;
int Rectangl::*widthp = &Rectangl::width;
rect1.show(get_funp, widthp);
return 0;
}