第八章多 态 性第八章 多态性
§ 8.1 多态性的基本概念
§ 8.2 函数重载
§ 8.3 拷贝构造函数
§ 8.4 运算符重载
§ 8.5 虚函数
§ 8.6 纯虚函数与抽象类
§ 8.1 多态性的基本概念
在基类中定义的属性或操作被派生类继承之后,可以具有不同的数据类型或表现出不同的行为,从而同一个属性或操作名称在各个派生类中具有不同的含义
Manager
Wife AssistantSecretary
§ 8.1 多态性的基本概念
C++语言支持两种多态性
编译时的多态性通过重载实现(函数重载,运算符重载)
运行时的多态性通过虚函数实现(借助抽象类和动态绑定)
§ 8.1 多态性的基本概念
绑定将一个函数调用链接上相应的函数体代码的过程
静态绑定在编译时就能够确定调用哪一个函数
动态绑定在运行时才能够确定调用哪一个函数
§ 8.1 多态性的基本概念
本章重点重载,函数重载,运算符重载 静态绑定虚函数,抽象类 动态绑定
§ 8.2 函数重载
函数重载 (函数名相同,函数参数不同)
–void print(char);
–void print(int,int);
–void print(float);
–void print(char *);
–……
函数调用
–print(“Hello,overload!”);
–print(3,5);
§ 8.2 函数重载
类构造函数的重载
class A
{
public:
A( );
A(int);
A(char c);
//……
};
A aa(34);
§ 8.2 函数重载函数重载的注意事项
⑴ 参数不同,是指参数的个数,参数的类型不同
⑵ 只有返回值类型不同,不是函数重载
⑶ 不要用重载函数描述毫不相干的函数
⑷ 在重载函数中使用缺省参数时应注意二义性问题
§ 8.2 函数重载例,void print(int a,int b)
{
……
}
void print(int a,int b,int c)
{
……
}
函数说明,void print(int a,int b,int c=50);
函数调用,print(10,100); //error! 系统不知调用哪个函数
§ 8.2 函数重载例:参数类型不同
float abs(float x)
{
……
}
double abs(double x)
{
……
}
函数调用,abs(5); //error!
因为系统不知应将 5转换成 float,还是 double
§ 8.2 函数重载在函数调用时,如找不到与实参完全匹配的函数原型,C++的隐式类型转换如能将一个类型转为能与函数原型匹配,则选择调用该重载函数例:
函数原型,int func(double d);
函数调用,func(?A?);
//系统自动将‘ A?(字符型)转换成 double
§ 8.3 拷贝构造函数构造对象的两个特殊构造函数
1,默认构造函数 (复习)
2,拷贝构造函数
§ 8.3 拷贝构造函数
默认构造函数例,A aa; //当定义一个对象而不给它任何初始值时,系统就要调用默认构造函数
默认构造函数:
参数表为空或所有参数都有默认值的构造函数如,A( ); 或 A(int i=0,int j=5);
§ 8.3 拷贝构造函数遇下列情况系统自动调用默认构造函数来构造对象
⑴ 直接定义一个对象而没有给初始值例,A aa;
⑵ 用 new动态申请的对象而没有给初始值例,A *pa=new A;
或,A *pa;
pa=new A;
⑶ 定义了一个对象数组例,A array[10];
§ 8.3 拷贝构造函数
默认构造函数的作用为了让所定义的类能够被使用(有时甚至没有给对象任何初始化信息)
当所定义的类没有提供任何构造函数时,系统将为类自动生成一个默认构造函数
§ 8.3 拷贝构造函数只要定义了一个构造函数,系统将不再生成默认构造函数例,class A
{
A(int i);
……
};
如下定义类 A的对象:
A aa;
A array[10];
A *pa=new A;
一般情况下,都要定义自己的构造函数,并至少定义一个默认构造函数
§ 8.3 拷贝构造函数拷贝构造函数的作用
用一个对象去构造另一个同类的对象例:
STUDENT s1(“Jenny”); //系统调用构造函数
STUDENT s2=s1; //系统调用拷贝构造函数,用对象 s1的值去初始化对象 s2
对象作为函数参数
对象作为函数返回值
§ 8.3 拷贝构造函数
拷贝构造函数的形式 ( STUDENT类)
STUDENT(const STUDENT& obj);
{
……
}
用 obj初始化一个新对象
§ 8.3 拷贝构造函数拷贝构造函数的 说明:
⑴ 拷贝构造函数只有一个参数,用同一类的对象作参数
⑵ 参数传递方式必须按引用调用函数
⑶ 每个类都必须有一个拷贝构造函数,如果没定义,系统自动生成一个缺省的拷贝构造函数缺省拷贝构造函数的含义:
用一个已知对象的数据成员的值拷贝给正在创建的另一个同类的对象
§ 8.3 拷贝构造函数
拷贝构造函数在下列情况被系统自动调用:
⑴ 当一个对象以声明初始化的方式复制时如,STUDENT s2=s1;
⑵ 当一个对象作为参数传递给一个函数时例,void main( )
{
STUDENT ms;
func(ms);
}
void func(STUDENT fs)
{
……
}
//调用拷贝构造函数将实参
ms对象的副本拷贝给 fs
//func( )函数调用结束返回时,还要析构 fs
§ 8.3 拷贝构造函数
⑶ 当一个对象作为值从一个函数返回时例,STUDENT func( )
{
STUDENT ms(“Randy”);
return ms;
}
void main( )
{
STUDENT s;
s=func( );
}
函数 func
的栈区
main函数的栈区
ms s临时对象放入 取出
//系统调用拷贝构造函数,将 ms
拷贝到新创建的临时对象中例:分析程序执行结果(拷贝构造函数举例)
class STUDENT
{
public:
STUDENT(char *pn=“no name”,int sid=0)
{
id=sid;
strcpy(name,pn);
cout<<“constructing new student”<<name<<endl;
}
STUDENT(const STUDENT& s) //拷贝构造函数
{
cout<<“constructing copy of”<<s.name<<endl;
strcpy(name,“copy of”);
strcat(name,s.name);
id=s.id;
}
~STUDENT( )
{ cout<<“Destructing”<<name<<endl; }
protected:
char name[40];
int id;
};
void fn(STUDENT s)
{
cout<<“In function fn()\n”;
}
void main()
{
STUDENT randy(“Randy”,1234);
cout<<“calling fn()\n”;
fn(randy);
cout<<“Returned from fn()\n”;
}
程序执行结果:
constructing new student Randy //调用普通构造函数
calling fn() //main()函数中的输出
constructing copy of Randy //调用拷贝构造函数
In function fn() //fn()函数中的输出
Destructing copy of Randy //从 fn函数返回时,形参 s被析构
Returned from fn() //回到 main函数后的输出
Destructing Randy // main函数执行结束时,对象 randy被析构
§ 8.3 拷贝构造函数浅拷贝与深拷贝 (shallow copy 与 deep copy)
浅拷贝 (见下图)
pName
堆
p1
pName
pName
堆
p1
p2
浅拷贝:创建 p2时,对象 p1被复制给了 p2,但资源并未复制,p1,p2指向同一资源默认拷贝构造函数是浅拷贝 (浅拷贝是不安全的)
拷贝前 拷贝后
§ 8.3 拷贝构造函数
深拷贝 (见下图)
pName
堆
p1
pName
pName
堆
p1
p2
堆拷贝前拷贝后深拷贝:对象 p1被复制给 p2时,不但拷贝成员,也拷贝资源自己定义拷贝构造函数,在函数体中用 new动态申请内存,以实现深拷贝
§ 8.4 运算符重载
运算符重载的概念
友元的概念
成员函数与友元函数
小 结
§ 8.4 运算符重载
运算符重载的含义对 C++预定义的运算符功能的扩展例:
QUEUE q1,q2;
q1=q1+q2;
MATRIX m1,m2,m;
m=m1*m2;
COUNTER c;
c++;
对象作操作数
§ 8.4 运算符重载
运算符重载的目的增加程序的可读性,简洁,直接,易理解例:
q1.addqueue(q2);
q1=q1+q2;
m=multimatrix(m1,m2);
m=m1*m2;
IncCounter(c);
c++;
看起来简单与数学公式相似成员函数可以用成员函数来实现运算符重载
§ 8.4 运算符重载
可以重载的运算符几乎所有的 C++预定义的运算符都可以被重载
(都可以为自己定义的类重载,即用对象作为这 些运算符的操作数参与运算)
不可以重载的运算符圆点运算符,
域运算符,:
间接访问运算符 *
指针成员运算符?
条件 运算符?,
§ 8.4 运算符重载
重载运算符的定义返回类型 operator 运算符 (参数表 )
例:要重载运算符,+”
返回类型 operator +(类型 1 参数 1,类型 2 参数 2)
例:要重载运算符,++”
返回类型 operator ++(类型 参数 )
可以改变 可以改变但参数个数不能变是运算符不是函数
§ 8.4 运算符重载运算符重载时应遵守的规则
重载的运算符至少有一个参数的类型与自定义的类有关 (即重载的运算符的参数不能都是预定义的数据类型 )
重载时,运算符的优先级、结合性以及参数的个数不能改变
不能创造新的运算符
§ 8.4 运算符重载
友元的概念可以访问类的保护、私有成员的 函数 或 类
如果一个函数与某个类关系密切,可以定义其为 友元函数
如果一个类与某个类关系密切,可以定义其为 友元类
§ 8.4 运算符重载
使用友元的目的提高程序的运行效率类的数据成员大部分为私有成员,只能通过成员函数去访问,当对某些成员函数需多次调用时,会影响程序的运行效率
§ 8.4 运算符重载
友元的定义(声明)
友元是一种定义在类外部的普通函数或类,但它需要在类体内进行声明
§ 8.4 运算符重载
声明一个函数 f( )是另一个类 B的友元函数的方式
class B
{
friend 函数 f( )的原型;
//其它类成员的定义或声明;
};
声明一个类 A是另一个类 B的友元类的方式
class B
{
friend class A;
//其它类成员的定义或声明;
};
§ 8.4 运算符重载
当将一个函数 f( )或一个类 A声明为另一个类
B的友元后,f( )或类 A就可以直接访问类 B的任何成员,包括私有和保护成员
友元函数 f( )与友元类 A的定义、调用等与普通函数、普通类完全一样
§ 8.4 运算符重载友元通常应用在下列情况
一个函数需要经常且大量的访问一个类的数据成员时
运算符重载为了能直接对对象进行加减乘除等各种运算,经常需要将所定义的运算符运算函数声明为类的友元函数,以便可方便地访问类的私有成员
一个类从属于另一个类时
(经常需要将一个类定义为另一个类的友元)
§ 8.4 运算符重载使用友元的注意事项
friend是单向的如果类 A是类 B的友元,则类 A可以访问类 B的私有成员,但反之不成
友元不是类的成员友元不受类成员访问控制的限制,没有公有、保护、私有之分,它只是一个说明,可以放在类体内的任何地方
一个类的成员函数也可以成为另一个类的友元函数,
此时的友元声明需要加上类域的限定
§ 8.4 运算符重载例:将函数 func()和类 A的成员函数 memfun()同时声明为类 B的友元
int func(int,float); //一个普通函数
class A
{
public:
void memfunc(char *); //类 A的成员函数
//……
};
class B
{
friend int func(int,float);
frieng void A::memfunc(char *);
//类 B的其它成员
};
注:友元的引用会破坏类的封装性,使用时应权衡利弊
§ 8.4 运算符重载运算符重载的函数可采用两种方式:
成员函数与友元函数可以将运算符函数定义成类的成员函数也可以将运算符函数定义成友元函数
§ 8.4 运算符重载成员函数与友元函数的比较
成员函数 有一个隐含的参数 (this指针 )
如果将一个二元运算符定义为类的成员函数,则只需为这个函数说明一个参数。
如果将一个一元运算符定义为类的成员函数,则这个函数就不能再带参数。
§ 8.4 运算符重载
如果将一个重载的运算符定义为友元函数,
则友元函数的参数个数和实际参数个数相同
成员函数与友元函数的相同点:
二者都可以访问类的私有成员和保护成员
§ 8.4 运算符重载二元运算符的重载(以,+”为例)
定义
① 作为类的成员函数,具有一个参数
class COUNTER
{
public:
int operator +(COUNTER& c );
};
② 作为友元函数,应有两个参数
class COUNTER
{
public:
friend int operator +(COUNTER& c1,COUNTER& c2);
};
§ 8.4 运算符重载
函数实现
① 成员函数的实现
int COUNTER::operator +(COUNTER& c)
{
return value+c.value;
}
② 友元函数的实现
int operator +(COUNTER& c1,COUNTER& c2)
{
return c1.value+c2.value;
}
§ 8.4 运算符重载
使用(二者相同)
COUNTER c1(3),c2(4); //创建 2个对象
cout<<c1+c2; //输出 7
程序中出现的表达式,c1+c2
成员函数,编译程序将其解释为,c1.operator +(c2)
友元函数,编译程序将其解释为,operator +(c1,c2)
§ 8.4 运算符重载一元运算符的重载 (以,前缀 ++”为例)
定义 —— 用类成员函数
class COUNTER
{
public:
COUNTER& operator ++( );
COUNTER(int val) {value=val;} //构造函数
int getvalue( ) {return value;}
private:
int value;
};
定义 —— 用友元函数
class COUNTER
{
public:
friend COUNTER& operator ++(COUNTER& c );
};
类成员函数默认有一个参数,是调用该函数时所给的对象本身
§ 8.4 运算符重载函数实现
类成员函数的实现
COUNTER& COUNTER::operator ++( )
{
value++;
return *this;
}
友元函数的实现
COUNTER& operator ++(COUNTER& c)
{
c.value++; //因为是友元,所以可以访问私有成员 value
return c;
}
返回的是当前对象的引用,可以对当前对象再进行运算指针所指的对象(当前对象)
必须给出哪个对象
§ 8.4 运算符重载
使用方式
COUNTER c(0);
++c;
cout<<c.getvalue(); //输出 1
++(++c);
cout<<c.getvalue(); //输出 3
成员函数与友元函数的使用方式是相同的注:如果不是用引用返回类型,则不能有上述操作和结果成员函数,c.operator ++( )
友元函数,operator ++(c)
§ 8.4 运算符重载
赋值运算符的重载
//函数实现
COUNTER& COUNTER&::operator =(COUNTER& c)
{
value=c.value;
return *this;
}
//使用
COUNTER c1(2),c2(5);
c1=c2; //对象的赋值,调用赋值运算符重载函数注,用对象初始化对象调用拷贝构造函数,如,COUNTER c1=c2;
当前对象可作为左值
§ 8.4 运算符重载
二元运算符与一元运算符成员函数 友元函数二元 1个参数 (实际还是 2个 ) 2个参数一元 0个参数 (实际还是 1个 ) 1个参数注:类对象本身 (this)自动作为其成员函数的第一个参数
§ 8.4 运算符重载小 结
1,重载运算符,可以是类的成员函数、友元函数或普通函数 (普通函数不常用)
2,二者比较:
参数的个数不同访问成员的方式不同使用方式相同
§ 8.4 运算符重载
3.重载的运算符可以重新定义的有:
4.重载的运算符不能重新定义的有:
参数类型返回值类型参数个数优先级结合性新的运算符附:总结几个常用概念
值返回与引用返回当需要将运算结果作为左值时,用引用返回 (见下例 )
例,cout<<a<<b; //作为左值,还能再用,就须用引用
++(++a); //如果只用 ++a,可不用引用返回
参数用引用参数用引用,不用复制对象副本,可节省内存,
提高速度,还可改变参数的值例,COUNTER& operator ++(COUNTER& c)
常量引用参数不希望在函数中改变引用参数的值例,int COUNTER::operator +(const COUNTER& c)
{ return value+c.value; } //不希望改变参数 c的值附:函数的返回类型是引用举例
//例:求两个数中的最小数
#include<iostream.h>
int& min(int& i,int& j)
{
if(i<=j)
return i;
else
return j;
}
void main()
{
int a=3,b=4;
cout<<"a="<<a<<' '<<"b="<<b<<endl;
min(a,b)=5;
cout<<"a="<<a<<' '<<"b="<<b<<endl;
min(a,b)=0;
cout<<"a="<<a<<' '<<"b="<<b<<endl;
}
程序执行结果:
a=3 b=4
a=5 b=4
a=5 b=0
说明:
函数的返回值是引用类型,表示该函数的返回值是一个变量的别名,可将函数调用作为一个变量使用,
可为其赋值
§ 8.5 虚函数
虚函数的定义在类的成员函数原型前加,virtual”
( 该成员函数称为虚函数)
如,class A
{
public:
virtual void fun( ); //fun( )是虚函数
//……
};
注:虚函数必须是类的成员函数
§ 8.5 虚函数介绍两个与虚函数有关的概念
函数的重定义
指向基类对象的指针可以指向派生类对象
§ 8.5 虚函数
函数的重定义( overriding)
函数的原型相同,函数的实现不同
函数的重定义与函数重载的比较相同点:函数名相同 (对同名函数进行再定义 )
不同点,1、重定义的函数只能是类的成员函数,而重载的函数则即可对类成员函数重载,
也可对普通函数重载
2、重定义的函数原型相同,而重载的函数是函数名相同,参数不同
§ 8.5 虚函数
重定义函数的使用在基类中定义的某成员函数为虚函数,在其派生类中可以重定义该虚函数例,class A
{
public:
virtual int area(int x,int y);
//……
};
class B:public A
{
public:
virtual int area(int x,int y);
//……
};
注:函数体不同,否则使用继承就可以了
§ 8.5 虚函数
指向基类的指针可以指向派生类对象( down casting)
指向基类对象的指针指向派生类后,可以访问派生类对象的继承成员(但不能访问派生成员)
基类 派生类如,BASE *pobj;
DERIVED dobj;
pobj=&dobj; //基类对象的指针可以指向派生类
§ 8.5 虚函数
down casting带来的问题
class BASE
{
//……
int fun( );
};
class DERIVED:public BASE
{
//……
int fun( );
};
如果,pobj->fun( ); //调用哪个类的函数?
如果是静态绑定,就应调用 BASE类的 fun(),因为 pobj是指向 BASE类对象的指针,但现在 pobj实际指向 DERIVED类的对象,所以从道理上应调用 DERIVED类的 fun(),因此造成二者的矛盾
§ 8.5 虚函数
解决的办法 —— virtual函数
class BASE
{
//……
virtual int fun( );
};
class DERIVED:public BASE
{
//……
int fun( );
};
BASE *pobj;
DERIVED dobj;
pobj=&dobj;
pobj->fun( ); //根据 pobj当前所指的对象正确调用相应函数
§ 8.5 虚函数
虚函数的含义
1、是一个类的成员函数
2、可以为派生类对象使用
3、派生类可以通过编写自己的成员函数来替代基类的虚函数,这种替代是基类预见到的,默认的,甚至是赞成的
§ 8.5 虚函数
虚函数举例学生 (Student)
本科生
(UnderGrad)
研究生
(PostGrad)
硕士研究生
(MasterGrad)
博士研究生
(DoctorGrad)
§ 8.5 虚函数
class STUDENT
{
public:
virtual void SeleteCourse(); //选课
virtual int CalcuCridit(); //计算总学分
//……
protected:
char name[30];
int age;
//……
};
在定义 STUDENT类时,已经预见到不同的学生选课的方式是不一样的,计算学分的方法也不一样定义成 virtual后,就允许派生类根据自己的需要重新定义成员函数
§ 8.5 虚函数
//UnderGrad本科生类没有重新实现两个虚函数,
它是用基类的实现
class UNDERGRAD:public STUDENT
{
public:
void practice( ); //工程实践
private:
……
};
本科生使用基类的两个虚函数(基类中必须有两个虚函数的实现)
§ 8.5 虚函数
//PostGrad研究生类重新实现两个虚函数
class POSTGRAD:public STUDENT
{
public:
virtual void SeleteCourse(); //选课
virtual int CalcuCridit(); //计算总学分
private:
int SupervisorID; //导师
//……
};
§ 8.5 虚函数
//MasterGrad硕士生使用 PostGrad提供的虚函数
class MASTERGRAD:public POSTGRAD
{
public:
//……
}
§ 8.5 虚函数
//DoctorGrad博士生类重新实现两个虚函数
class DOCTORGRAD:public POSTGRAD
{
public:
void SeleteCourse(); //选课
int CalcuCridit(); //计算总学分
//……
};
§ 8.5 虚函数
虚函数的好处在基类中提供了一个接口,都用同样的调用方式。具体派生类如何实现,可由派生类去决定,既可以使用基类的实现,也可以不使用基类的实现,派生类定义自己的实现
§ 8.5 虚函数
void main()
{
STUDENT *s; //s可能指向以下各种派生类对象
//程序运行过程中动态创建学生对象,可能是以下各种语句
//s=new UNDERGRAD();
//s=new POSTGRAD();
//s=new DOCTORGRAD();
//s=new MASTERGRAD();
S->SeleteCourse(); //调用 POSTGRAD:,SeleteCourse()因为
MASTERGRAD类中没有实现函数,调用它的直接基类 POSTGRAD中的函数
//……
}
§ 8.5 虚函数
virtual的含义
virtual只在类继承时才发辉作用
virtual告诉编译器,它所修饰的函数需要动态绑定
在调用该函数时,需要根据对象的实际类型决定使用类继承层次中的哪个类的成员函数
(是哪个类的对象就调用哪个类的成员函数)
§ 8.5 虚函数
使用虚函数的例子
//students存放所有学生的信息
STUDENT *students[3000]; //指针数组
int StudNum; //学生数目
//在程序运行过程中动态创建了 StudNum个学生对象
// students[StudNum++]=new DoctorGrad( );
// students[StudNum++]=new UnderGrad( );
//……
§ 8.5 虚函数
//所有学生选课
void StudentSeleteCourse()
{
for(int i=0; i<StudNum; i++)
students[i]->SeleteCourse();
//……
} //程序非常简单学生本科生 研究生硕士生 博士生复杂的实现虚函数
§ 8.5 虚函数
如果不用虚函数,则程序会非常复杂
void StudentSeleteCourse()
{
for(int i=0; i<StudNum; i++)
switch(students[i]->type)
{
case GRAD,//本科生
students[i]->UNDERGRAD:,SeleteCourse();
break;
case POST,//研究生
students[i]->POSTGRAD:,SeleteCourse();
break;
case DOCT,//博士生
//……
}
//…… 不仅麻烦,若再加一种学生还需修改程序,如果用虚函数就不用作任何改变,只加一个派生类 ….
§ 8.5 虚函数虚函数的好处:多态性主要体现在如下方面:
程序简单
设计与实现分开,实现了一定的封装在基类中设计高层逻辑、接口,具体实现细节由派生类根据各自需要实现,基类可以不用担心实现细节
程序可重用,易扩充如:加一种学生,只需加一个派生类的虚函数实现
多态性语言的实现体现“同一接口,多种方法”的多态性概念
§ 8.5 虚函数虚函数的缺点:增加运行开销 (多占用内存空间)
每个对象增加一个指针
调用虚函数时要查表虚函数的内部实现
STUDENT::SeleteCourse
POSTGRAD::SeleteCourse
DOCTORGRAD::SeleteCourse
MASTERGRAD::SeleteCourse
……
V-table 虚拟表 (所有虚函数都在此表中 )
name
age
….
V-pointer
学生对象所有对象都有一个指针如果没有虚函数,则对象没有这个特殊成员
§ 8.5 虚函数使用虚函数的注意事项
派生类中重新定义基类中的虚成员函数,必须在函数原型上与基类中的虚成员函数保持一致
派生类对象调用虚函数时,如果该虚函数在派生类中重新定义了,则执行派生类所定义的操作,否则执行基类中所定义的操作
基类中的非虚函数不能在派生类中重新(覆盖)定义
派生类中的虚函数,virtual关键字可有可无,如果下一级派生类还需要使用时,则必须有 virtual
调用虚函数操作的是指向对象的指针或对象引用
(调用虚函数的参数只能是对象指针或对象引用)
§ 8.5 虚函数例:虚函数
#include<iostream.h>
class POINT
{
public:
POINT(int x1,int y1)
{ x=x1; y=y1; }
virtual int area()
{ return 0; }
private:
int x,y;
};
§ 8.5 虚函数
class RECT:public POINT
{
public:
RECT(int x1,int y1,int l1,int w1):POINT(x1,y1)
{
l=l1; w=w1;
}
int area() //虚函数
{
return l*w;
}
private:
int l,w;
};
§ 8.5 虚函数
void fun(POINT& p)
{
cout<<p.area()<<endl;
}
void main()
{
RECT rec(2,4,10,6);
fun(rec);
}
程序执行结果,60
说明:
1,fun函数的引用对象参数 p被动态绑定,该函数体内调用的 area()
在运行中被确定为 RECT类中的 area函数
2、该程序中如果没有 virtual关键字,则为静态绑定,输出结果为 0
§ 8.6 纯虚函数与抽象类
纯虚函数是一种特殊的虚函数
纯虚函数的定义格式在基类的虚函数原型的分号前加,=0”
例,class POINT
{
public:
virtual int area()=0;
//……
};
§ 8.6 纯虚函数与抽象类纯虚函数的特点
纯虚函数是将要被派生类实现的函数
(纯虚函数只有函数原型,没有函数实现)
具有纯虚函数的类不能实例化 (不能创建对象,因为这种类不完整)
派生类中如果还有纯虚函数,则还不能实例化,
能实例化的派生类必须实现所有纯虚函数
§ 8.6 纯虚函数与抽象类纯虚函数的作用
为抽象基类的定义提供了手段
为所有派生类设计一个标准接口,方便高层应用逻辑的设计
§ 8.6 纯虚函数与抽象类例,学生选课
class ABSTRACT_STUDENT
{
public:
virtual void SeleteCourse()=0;
virtual int CalcuCridit()=0;
//……
protected:
char name[30];
int age;
//……
};
纯虚函数没有实现,只有接口带有纯虚函数的类不能实例化
§ 8.6 纯虚函数与抽象类
// UNDERGRAD必须实现基类的纯虚函数
class UNDERGRAD:public ABSTRACT_ STUDENT
{
public:
void SeleteCourse();
int CalcuCridit();
void practice( ); //工程实践
//……
};
// UNDERGRAD 类可以实例化
§ 8.6 纯虚函数与抽象类
//POSTGRAD类可以保持两个纯虚函数的定义
class POSTGRAD_ABSTRACT,public ABSTRACT_ STUDENT
{
public:
void SeleteCourse()=0;
int CalcuCridit()=0;
//……
private:
int supervisorID; //导师
};
//该类也不能实例化
§ 8.6 纯虚函数与抽象类
// DOCTORGRAD类必须实现两个虚函数
class DOCTORGRAD:public ABSTRACT_POSTGRAD
{
public:
void SeleteCourse();
int CalcuCridit();
//……
};
// DOCTORGRAD 类可以实例化纯虚函数举例
#include<iostream.h>
#include<math.h>
class BASE
{
public:
virtual void setx(int i,int j=0) //虚函数
{
x=i; y=j;
}
virtual void disp()=0; //纯虚函数
protected:
int x,y;
};
class SQUARE:public BASE //平方
{
public:
void disp()
{
cout<<"x="<<x<<":";
cout<<"x square="<<x*x<<endl;
}
};
class CUBE:public BASE //立方
{
public:
void disp()
{
cout<<"x="<<x<<":";
cout<<"x cube="<<x*x*x<<endl;
}
};
class CHPOW:public BASE //n次方
{
public:
void disp()
{
cout<<"x="<<x<<"y="<<y<<":";
cout<<"pow(x,y)="<<pow(double(x),double(y))
<<endl;
}
};
//x的 y次方
void main()
{
BASE *ptr;
SQUARE b;
CUBE c;
CHPOW d;
ptr=&b;
ptr->setx(5);
ptr->disp(); //调用 SQUARE类中的 disp()
ptr=&c;
ptr->setx(6);
ptr->disp(); //调用 CUBE类中的 disp()
ptr=&d;
ptr->setx(3,4);
ptr->disp(); //调用 CHPOW类中的 disp()
}
§ 8.6 纯虚函数与抽象类程序执行结果:
x=5,x square=25
x=6,x cube=216
x=3 y=4,pow(x,y)=81
//该程序 中 disp()在运行时进行选择调用哪个类,
实现了动态绑定
§ 8.6 纯虚函数与抽象类
抽象类带有纯虚函数的类称为抽象类,它处于继承层次结构的较上层
抽象类的作用将有关的子类组织在一个继承层次结构中,由 抽象类 来为它们提供一个公共的根,
相关的子类从这个根派生出来
§ 8.6 纯虚函数与抽象类抽象类的使用规定:
抽象类只能用作其他类的基类,不能建立抽象类的对象
抽象类不能用作参数类型,函数返回类型
可以说明指向抽象类的指针和引用,该指针可以指向它的派生类,进而实现多态类
§ 8.1 多态性的基本概念
§ 8.2 函数重载
§ 8.3 拷贝构造函数
§ 8.4 运算符重载
§ 8.5 虚函数
§ 8.6 纯虚函数与抽象类
§ 8.1 多态性的基本概念
在基类中定义的属性或操作被派生类继承之后,可以具有不同的数据类型或表现出不同的行为,从而同一个属性或操作名称在各个派生类中具有不同的含义
Manager
Wife AssistantSecretary
§ 8.1 多态性的基本概念
C++语言支持两种多态性
编译时的多态性通过重载实现(函数重载,运算符重载)
运行时的多态性通过虚函数实现(借助抽象类和动态绑定)
§ 8.1 多态性的基本概念
绑定将一个函数调用链接上相应的函数体代码的过程
静态绑定在编译时就能够确定调用哪一个函数
动态绑定在运行时才能够确定调用哪一个函数
§ 8.1 多态性的基本概念
本章重点重载,函数重载,运算符重载 静态绑定虚函数,抽象类 动态绑定
§ 8.2 函数重载
函数重载 (函数名相同,函数参数不同)
–void print(char);
–void print(int,int);
–void print(float);
–void print(char *);
–……
函数调用
–print(“Hello,overload!”);
–print(3,5);
§ 8.2 函数重载
类构造函数的重载
class A
{
public:
A( );
A(int);
A(char c);
//……
};
A aa(34);
§ 8.2 函数重载函数重载的注意事项
⑴ 参数不同,是指参数的个数,参数的类型不同
⑵ 只有返回值类型不同,不是函数重载
⑶ 不要用重载函数描述毫不相干的函数
⑷ 在重载函数中使用缺省参数时应注意二义性问题
§ 8.2 函数重载例,void print(int a,int b)
{
……
}
void print(int a,int b,int c)
{
……
}
函数说明,void print(int a,int b,int c=50);
函数调用,print(10,100); //error! 系统不知调用哪个函数
§ 8.2 函数重载例:参数类型不同
float abs(float x)
{
……
}
double abs(double x)
{
……
}
函数调用,abs(5); //error!
因为系统不知应将 5转换成 float,还是 double
§ 8.2 函数重载在函数调用时,如找不到与实参完全匹配的函数原型,C++的隐式类型转换如能将一个类型转为能与函数原型匹配,则选择调用该重载函数例:
函数原型,int func(double d);
函数调用,func(?A?);
//系统自动将‘ A?(字符型)转换成 double
§ 8.3 拷贝构造函数构造对象的两个特殊构造函数
1,默认构造函数 (复习)
2,拷贝构造函数
§ 8.3 拷贝构造函数
默认构造函数例,A aa; //当定义一个对象而不给它任何初始值时,系统就要调用默认构造函数
默认构造函数:
参数表为空或所有参数都有默认值的构造函数如,A( ); 或 A(int i=0,int j=5);
§ 8.3 拷贝构造函数遇下列情况系统自动调用默认构造函数来构造对象
⑴ 直接定义一个对象而没有给初始值例,A aa;
⑵ 用 new动态申请的对象而没有给初始值例,A *pa=new A;
或,A *pa;
pa=new A;
⑶ 定义了一个对象数组例,A array[10];
§ 8.3 拷贝构造函数
默认构造函数的作用为了让所定义的类能够被使用(有时甚至没有给对象任何初始化信息)
当所定义的类没有提供任何构造函数时,系统将为类自动生成一个默认构造函数
§ 8.3 拷贝构造函数只要定义了一个构造函数,系统将不再生成默认构造函数例,class A
{
A(int i);
……
};
如下定义类 A的对象:
A aa;
A array[10];
A *pa=new A;
一般情况下,都要定义自己的构造函数,并至少定义一个默认构造函数
§ 8.3 拷贝构造函数拷贝构造函数的作用
用一个对象去构造另一个同类的对象例:
STUDENT s1(“Jenny”); //系统调用构造函数
STUDENT s2=s1; //系统调用拷贝构造函数,用对象 s1的值去初始化对象 s2
对象作为函数参数
对象作为函数返回值
§ 8.3 拷贝构造函数
拷贝构造函数的形式 ( STUDENT类)
STUDENT(const STUDENT& obj);
{
……
}
用 obj初始化一个新对象
§ 8.3 拷贝构造函数拷贝构造函数的 说明:
⑴ 拷贝构造函数只有一个参数,用同一类的对象作参数
⑵ 参数传递方式必须按引用调用函数
⑶ 每个类都必须有一个拷贝构造函数,如果没定义,系统自动生成一个缺省的拷贝构造函数缺省拷贝构造函数的含义:
用一个已知对象的数据成员的值拷贝给正在创建的另一个同类的对象
§ 8.3 拷贝构造函数
拷贝构造函数在下列情况被系统自动调用:
⑴ 当一个对象以声明初始化的方式复制时如,STUDENT s2=s1;
⑵ 当一个对象作为参数传递给一个函数时例,void main( )
{
STUDENT ms;
func(ms);
}
void func(STUDENT fs)
{
……
}
//调用拷贝构造函数将实参
ms对象的副本拷贝给 fs
//func( )函数调用结束返回时,还要析构 fs
§ 8.3 拷贝构造函数
⑶ 当一个对象作为值从一个函数返回时例,STUDENT func( )
{
STUDENT ms(“Randy”);
return ms;
}
void main( )
{
STUDENT s;
s=func( );
}
函数 func
的栈区
main函数的栈区
ms s临时对象放入 取出
//系统调用拷贝构造函数,将 ms
拷贝到新创建的临时对象中例:分析程序执行结果(拷贝构造函数举例)
class STUDENT
{
public:
STUDENT(char *pn=“no name”,int sid=0)
{
id=sid;
strcpy(name,pn);
cout<<“constructing new student”<<name<<endl;
}
STUDENT(const STUDENT& s) //拷贝构造函数
{
cout<<“constructing copy of”<<s.name<<endl;
strcpy(name,“copy of”);
strcat(name,s.name);
id=s.id;
}
~STUDENT( )
{ cout<<“Destructing”<<name<<endl; }
protected:
char name[40];
int id;
};
void fn(STUDENT s)
{
cout<<“In function fn()\n”;
}
void main()
{
STUDENT randy(“Randy”,1234);
cout<<“calling fn()\n”;
fn(randy);
cout<<“Returned from fn()\n”;
}
程序执行结果:
constructing new student Randy //调用普通构造函数
calling fn() //main()函数中的输出
constructing copy of Randy //调用拷贝构造函数
In function fn() //fn()函数中的输出
Destructing copy of Randy //从 fn函数返回时,形参 s被析构
Returned from fn() //回到 main函数后的输出
Destructing Randy // main函数执行结束时,对象 randy被析构
§ 8.3 拷贝构造函数浅拷贝与深拷贝 (shallow copy 与 deep copy)
浅拷贝 (见下图)
pName
堆
p1
pName
pName
堆
p1
p2
浅拷贝:创建 p2时,对象 p1被复制给了 p2,但资源并未复制,p1,p2指向同一资源默认拷贝构造函数是浅拷贝 (浅拷贝是不安全的)
拷贝前 拷贝后
§ 8.3 拷贝构造函数
深拷贝 (见下图)
pName
堆
p1
pName
pName
堆
p1
p2
堆拷贝前拷贝后深拷贝:对象 p1被复制给 p2时,不但拷贝成员,也拷贝资源自己定义拷贝构造函数,在函数体中用 new动态申请内存,以实现深拷贝
§ 8.4 运算符重载
运算符重载的概念
友元的概念
成员函数与友元函数
小 结
§ 8.4 运算符重载
运算符重载的含义对 C++预定义的运算符功能的扩展例:
QUEUE q1,q2;
q1=q1+q2;
MATRIX m1,m2,m;
m=m1*m2;
COUNTER c;
c++;
对象作操作数
§ 8.4 运算符重载
运算符重载的目的增加程序的可读性,简洁,直接,易理解例:
q1.addqueue(q2);
q1=q1+q2;
m=multimatrix(m1,m2);
m=m1*m2;
IncCounter(c);
c++;
看起来简单与数学公式相似成员函数可以用成员函数来实现运算符重载
§ 8.4 运算符重载
可以重载的运算符几乎所有的 C++预定义的运算符都可以被重载
(都可以为自己定义的类重载,即用对象作为这 些运算符的操作数参与运算)
不可以重载的运算符圆点运算符,
域运算符,:
间接访问运算符 *
指针成员运算符?
条件 运算符?,
§ 8.4 运算符重载
重载运算符的定义返回类型 operator 运算符 (参数表 )
例:要重载运算符,+”
返回类型 operator +(类型 1 参数 1,类型 2 参数 2)
例:要重载运算符,++”
返回类型 operator ++(类型 参数 )
可以改变 可以改变但参数个数不能变是运算符不是函数
§ 8.4 运算符重载运算符重载时应遵守的规则
重载的运算符至少有一个参数的类型与自定义的类有关 (即重载的运算符的参数不能都是预定义的数据类型 )
重载时,运算符的优先级、结合性以及参数的个数不能改变
不能创造新的运算符
§ 8.4 运算符重载
友元的概念可以访问类的保护、私有成员的 函数 或 类
如果一个函数与某个类关系密切,可以定义其为 友元函数
如果一个类与某个类关系密切,可以定义其为 友元类
§ 8.4 运算符重载
使用友元的目的提高程序的运行效率类的数据成员大部分为私有成员,只能通过成员函数去访问,当对某些成员函数需多次调用时,会影响程序的运行效率
§ 8.4 运算符重载
友元的定义(声明)
友元是一种定义在类外部的普通函数或类,但它需要在类体内进行声明
§ 8.4 运算符重载
声明一个函数 f( )是另一个类 B的友元函数的方式
class B
{
friend 函数 f( )的原型;
//其它类成员的定义或声明;
};
声明一个类 A是另一个类 B的友元类的方式
class B
{
friend class A;
//其它类成员的定义或声明;
};
§ 8.4 运算符重载
当将一个函数 f( )或一个类 A声明为另一个类
B的友元后,f( )或类 A就可以直接访问类 B的任何成员,包括私有和保护成员
友元函数 f( )与友元类 A的定义、调用等与普通函数、普通类完全一样
§ 8.4 运算符重载友元通常应用在下列情况
一个函数需要经常且大量的访问一个类的数据成员时
运算符重载为了能直接对对象进行加减乘除等各种运算,经常需要将所定义的运算符运算函数声明为类的友元函数,以便可方便地访问类的私有成员
一个类从属于另一个类时
(经常需要将一个类定义为另一个类的友元)
§ 8.4 运算符重载使用友元的注意事项
friend是单向的如果类 A是类 B的友元,则类 A可以访问类 B的私有成员,但反之不成
友元不是类的成员友元不受类成员访问控制的限制,没有公有、保护、私有之分,它只是一个说明,可以放在类体内的任何地方
一个类的成员函数也可以成为另一个类的友元函数,
此时的友元声明需要加上类域的限定
§ 8.4 运算符重载例:将函数 func()和类 A的成员函数 memfun()同时声明为类 B的友元
int func(int,float); //一个普通函数
class A
{
public:
void memfunc(char *); //类 A的成员函数
//……
};
class B
{
friend int func(int,float);
frieng void A::memfunc(char *);
//类 B的其它成员
};
注:友元的引用会破坏类的封装性,使用时应权衡利弊
§ 8.4 运算符重载运算符重载的函数可采用两种方式:
成员函数与友元函数可以将运算符函数定义成类的成员函数也可以将运算符函数定义成友元函数
§ 8.4 运算符重载成员函数与友元函数的比较
成员函数 有一个隐含的参数 (this指针 )
如果将一个二元运算符定义为类的成员函数,则只需为这个函数说明一个参数。
如果将一个一元运算符定义为类的成员函数,则这个函数就不能再带参数。
§ 8.4 运算符重载
如果将一个重载的运算符定义为友元函数,
则友元函数的参数个数和实际参数个数相同
成员函数与友元函数的相同点:
二者都可以访问类的私有成员和保护成员
§ 8.4 运算符重载二元运算符的重载(以,+”为例)
定义
① 作为类的成员函数,具有一个参数
class COUNTER
{
public:
int operator +(COUNTER& c );
};
② 作为友元函数,应有两个参数
class COUNTER
{
public:
friend int operator +(COUNTER& c1,COUNTER& c2);
};
§ 8.4 运算符重载
函数实现
① 成员函数的实现
int COUNTER::operator +(COUNTER& c)
{
return value+c.value;
}
② 友元函数的实现
int operator +(COUNTER& c1,COUNTER& c2)
{
return c1.value+c2.value;
}
§ 8.4 运算符重载
使用(二者相同)
COUNTER c1(3),c2(4); //创建 2个对象
cout<<c1+c2; //输出 7
程序中出现的表达式,c1+c2
成员函数,编译程序将其解释为,c1.operator +(c2)
友元函数,编译程序将其解释为,operator +(c1,c2)
§ 8.4 运算符重载一元运算符的重载 (以,前缀 ++”为例)
定义 —— 用类成员函数
class COUNTER
{
public:
COUNTER& operator ++( );
COUNTER(int val) {value=val;} //构造函数
int getvalue( ) {return value;}
private:
int value;
};
定义 —— 用友元函数
class COUNTER
{
public:
friend COUNTER& operator ++(COUNTER& c );
};
类成员函数默认有一个参数,是调用该函数时所给的对象本身
§ 8.4 运算符重载函数实现
类成员函数的实现
COUNTER& COUNTER::operator ++( )
{
value++;
return *this;
}
友元函数的实现
COUNTER& operator ++(COUNTER& c)
{
c.value++; //因为是友元,所以可以访问私有成员 value
return c;
}
返回的是当前对象的引用,可以对当前对象再进行运算指针所指的对象(当前对象)
必须给出哪个对象
§ 8.4 运算符重载
使用方式
COUNTER c(0);
++c;
cout<<c.getvalue(); //输出 1
++(++c);
cout<<c.getvalue(); //输出 3
成员函数与友元函数的使用方式是相同的注:如果不是用引用返回类型,则不能有上述操作和结果成员函数,c.operator ++( )
友元函数,operator ++(c)
§ 8.4 运算符重载
赋值运算符的重载
//函数实现
COUNTER& COUNTER&::operator =(COUNTER& c)
{
value=c.value;
return *this;
}
//使用
COUNTER c1(2),c2(5);
c1=c2; //对象的赋值,调用赋值运算符重载函数注,用对象初始化对象调用拷贝构造函数,如,COUNTER c1=c2;
当前对象可作为左值
§ 8.4 运算符重载
二元运算符与一元运算符成员函数 友元函数二元 1个参数 (实际还是 2个 ) 2个参数一元 0个参数 (实际还是 1个 ) 1个参数注:类对象本身 (this)自动作为其成员函数的第一个参数
§ 8.4 运算符重载小 结
1,重载运算符,可以是类的成员函数、友元函数或普通函数 (普通函数不常用)
2,二者比较:
参数的个数不同访问成员的方式不同使用方式相同
§ 8.4 运算符重载
3.重载的运算符可以重新定义的有:
4.重载的运算符不能重新定义的有:
参数类型返回值类型参数个数优先级结合性新的运算符附:总结几个常用概念
值返回与引用返回当需要将运算结果作为左值时,用引用返回 (见下例 )
例,cout<<a<<b; //作为左值,还能再用,就须用引用
++(++a); //如果只用 ++a,可不用引用返回
参数用引用参数用引用,不用复制对象副本,可节省内存,
提高速度,还可改变参数的值例,COUNTER& operator ++(COUNTER& c)
常量引用参数不希望在函数中改变引用参数的值例,int COUNTER::operator +(const COUNTER& c)
{ return value+c.value; } //不希望改变参数 c的值附:函数的返回类型是引用举例
//例:求两个数中的最小数
#include<iostream.h>
int& min(int& i,int& j)
{
if(i<=j)
return i;
else
return j;
}
void main()
{
int a=3,b=4;
cout<<"a="<<a<<' '<<"b="<<b<<endl;
min(a,b)=5;
cout<<"a="<<a<<' '<<"b="<<b<<endl;
min(a,b)=0;
cout<<"a="<<a<<' '<<"b="<<b<<endl;
}
程序执行结果:
a=3 b=4
a=5 b=4
a=5 b=0
说明:
函数的返回值是引用类型,表示该函数的返回值是一个变量的别名,可将函数调用作为一个变量使用,
可为其赋值
§ 8.5 虚函数
虚函数的定义在类的成员函数原型前加,virtual”
( 该成员函数称为虚函数)
如,class A
{
public:
virtual void fun( ); //fun( )是虚函数
//……
};
注:虚函数必须是类的成员函数
§ 8.5 虚函数介绍两个与虚函数有关的概念
函数的重定义
指向基类对象的指针可以指向派生类对象
§ 8.5 虚函数
函数的重定义( overriding)
函数的原型相同,函数的实现不同
函数的重定义与函数重载的比较相同点:函数名相同 (对同名函数进行再定义 )
不同点,1、重定义的函数只能是类的成员函数,而重载的函数则即可对类成员函数重载,
也可对普通函数重载
2、重定义的函数原型相同,而重载的函数是函数名相同,参数不同
§ 8.5 虚函数
重定义函数的使用在基类中定义的某成员函数为虚函数,在其派生类中可以重定义该虚函数例,class A
{
public:
virtual int area(int x,int y);
//……
};
class B:public A
{
public:
virtual int area(int x,int y);
//……
};
注:函数体不同,否则使用继承就可以了
§ 8.5 虚函数
指向基类的指针可以指向派生类对象( down casting)
指向基类对象的指针指向派生类后,可以访问派生类对象的继承成员(但不能访问派生成员)
基类 派生类如,BASE *pobj;
DERIVED dobj;
pobj=&dobj; //基类对象的指针可以指向派生类
§ 8.5 虚函数
down casting带来的问题
class BASE
{
//……
int fun( );
};
class DERIVED:public BASE
{
//……
int fun( );
};
如果,pobj->fun( ); //调用哪个类的函数?
如果是静态绑定,就应调用 BASE类的 fun(),因为 pobj是指向 BASE类对象的指针,但现在 pobj实际指向 DERIVED类的对象,所以从道理上应调用 DERIVED类的 fun(),因此造成二者的矛盾
§ 8.5 虚函数
解决的办法 —— virtual函数
class BASE
{
//……
virtual int fun( );
};
class DERIVED:public BASE
{
//……
int fun( );
};
BASE *pobj;
DERIVED dobj;
pobj=&dobj;
pobj->fun( ); //根据 pobj当前所指的对象正确调用相应函数
§ 8.5 虚函数
虚函数的含义
1、是一个类的成员函数
2、可以为派生类对象使用
3、派生类可以通过编写自己的成员函数来替代基类的虚函数,这种替代是基类预见到的,默认的,甚至是赞成的
§ 8.5 虚函数
虚函数举例学生 (Student)
本科生
(UnderGrad)
研究生
(PostGrad)
硕士研究生
(MasterGrad)
博士研究生
(DoctorGrad)
§ 8.5 虚函数
class STUDENT
{
public:
virtual void SeleteCourse(); //选课
virtual int CalcuCridit(); //计算总学分
//……
protected:
char name[30];
int age;
//……
};
在定义 STUDENT类时,已经预见到不同的学生选课的方式是不一样的,计算学分的方法也不一样定义成 virtual后,就允许派生类根据自己的需要重新定义成员函数
§ 8.5 虚函数
//UnderGrad本科生类没有重新实现两个虚函数,
它是用基类的实现
class UNDERGRAD:public STUDENT
{
public:
void practice( ); //工程实践
private:
……
};
本科生使用基类的两个虚函数(基类中必须有两个虚函数的实现)
§ 8.5 虚函数
//PostGrad研究生类重新实现两个虚函数
class POSTGRAD:public STUDENT
{
public:
virtual void SeleteCourse(); //选课
virtual int CalcuCridit(); //计算总学分
private:
int SupervisorID; //导师
//……
};
§ 8.5 虚函数
//MasterGrad硕士生使用 PostGrad提供的虚函数
class MASTERGRAD:public POSTGRAD
{
public:
//……
}
§ 8.5 虚函数
//DoctorGrad博士生类重新实现两个虚函数
class DOCTORGRAD:public POSTGRAD
{
public:
void SeleteCourse(); //选课
int CalcuCridit(); //计算总学分
//……
};
§ 8.5 虚函数
虚函数的好处在基类中提供了一个接口,都用同样的调用方式。具体派生类如何实现,可由派生类去决定,既可以使用基类的实现,也可以不使用基类的实现,派生类定义自己的实现
§ 8.5 虚函数
void main()
{
STUDENT *s; //s可能指向以下各种派生类对象
//程序运行过程中动态创建学生对象,可能是以下各种语句
//s=new UNDERGRAD();
//s=new POSTGRAD();
//s=new DOCTORGRAD();
//s=new MASTERGRAD();
S->SeleteCourse(); //调用 POSTGRAD:,SeleteCourse()因为
MASTERGRAD类中没有实现函数,调用它的直接基类 POSTGRAD中的函数
//……
}
§ 8.5 虚函数
virtual的含义
virtual只在类继承时才发辉作用
virtual告诉编译器,它所修饰的函数需要动态绑定
在调用该函数时,需要根据对象的实际类型决定使用类继承层次中的哪个类的成员函数
(是哪个类的对象就调用哪个类的成员函数)
§ 8.5 虚函数
使用虚函数的例子
//students存放所有学生的信息
STUDENT *students[3000]; //指针数组
int StudNum; //学生数目
//在程序运行过程中动态创建了 StudNum个学生对象
// students[StudNum++]=new DoctorGrad( );
// students[StudNum++]=new UnderGrad( );
//……
§ 8.5 虚函数
//所有学生选课
void StudentSeleteCourse()
{
for(int i=0; i<StudNum; i++)
students[i]->SeleteCourse();
//……
} //程序非常简单学生本科生 研究生硕士生 博士生复杂的实现虚函数
§ 8.5 虚函数
如果不用虚函数,则程序会非常复杂
void StudentSeleteCourse()
{
for(int i=0; i<StudNum; i++)
switch(students[i]->type)
{
case GRAD,//本科生
students[i]->UNDERGRAD:,SeleteCourse();
break;
case POST,//研究生
students[i]->POSTGRAD:,SeleteCourse();
break;
case DOCT,//博士生
//……
}
//…… 不仅麻烦,若再加一种学生还需修改程序,如果用虚函数就不用作任何改变,只加一个派生类 ….
§ 8.5 虚函数虚函数的好处:多态性主要体现在如下方面:
程序简单
设计与实现分开,实现了一定的封装在基类中设计高层逻辑、接口,具体实现细节由派生类根据各自需要实现,基类可以不用担心实现细节
程序可重用,易扩充如:加一种学生,只需加一个派生类的虚函数实现
多态性语言的实现体现“同一接口,多种方法”的多态性概念
§ 8.5 虚函数虚函数的缺点:增加运行开销 (多占用内存空间)
每个对象增加一个指针
调用虚函数时要查表虚函数的内部实现
STUDENT::SeleteCourse
POSTGRAD::SeleteCourse
DOCTORGRAD::SeleteCourse
MASTERGRAD::SeleteCourse
……
V-table 虚拟表 (所有虚函数都在此表中 )
name
age
….
V-pointer
学生对象所有对象都有一个指针如果没有虚函数,则对象没有这个特殊成员
§ 8.5 虚函数使用虚函数的注意事项
派生类中重新定义基类中的虚成员函数,必须在函数原型上与基类中的虚成员函数保持一致
派生类对象调用虚函数时,如果该虚函数在派生类中重新定义了,则执行派生类所定义的操作,否则执行基类中所定义的操作
基类中的非虚函数不能在派生类中重新(覆盖)定义
派生类中的虚函数,virtual关键字可有可无,如果下一级派生类还需要使用时,则必须有 virtual
调用虚函数操作的是指向对象的指针或对象引用
(调用虚函数的参数只能是对象指针或对象引用)
§ 8.5 虚函数例:虚函数
#include<iostream.h>
class POINT
{
public:
POINT(int x1,int y1)
{ x=x1; y=y1; }
virtual int area()
{ return 0; }
private:
int x,y;
};
§ 8.5 虚函数
class RECT:public POINT
{
public:
RECT(int x1,int y1,int l1,int w1):POINT(x1,y1)
{
l=l1; w=w1;
}
int area() //虚函数
{
return l*w;
}
private:
int l,w;
};
§ 8.5 虚函数
void fun(POINT& p)
{
cout<<p.area()<<endl;
}
void main()
{
RECT rec(2,4,10,6);
fun(rec);
}
程序执行结果,60
说明:
1,fun函数的引用对象参数 p被动态绑定,该函数体内调用的 area()
在运行中被确定为 RECT类中的 area函数
2、该程序中如果没有 virtual关键字,则为静态绑定,输出结果为 0
§ 8.6 纯虚函数与抽象类
纯虚函数是一种特殊的虚函数
纯虚函数的定义格式在基类的虚函数原型的分号前加,=0”
例,class POINT
{
public:
virtual int area()=0;
//……
};
§ 8.6 纯虚函数与抽象类纯虚函数的特点
纯虚函数是将要被派生类实现的函数
(纯虚函数只有函数原型,没有函数实现)
具有纯虚函数的类不能实例化 (不能创建对象,因为这种类不完整)
派生类中如果还有纯虚函数,则还不能实例化,
能实例化的派生类必须实现所有纯虚函数
§ 8.6 纯虚函数与抽象类纯虚函数的作用
为抽象基类的定义提供了手段
为所有派生类设计一个标准接口,方便高层应用逻辑的设计
§ 8.6 纯虚函数与抽象类例,学生选课
class ABSTRACT_STUDENT
{
public:
virtual void SeleteCourse()=0;
virtual int CalcuCridit()=0;
//……
protected:
char name[30];
int age;
//……
};
纯虚函数没有实现,只有接口带有纯虚函数的类不能实例化
§ 8.6 纯虚函数与抽象类
// UNDERGRAD必须实现基类的纯虚函数
class UNDERGRAD:public ABSTRACT_ STUDENT
{
public:
void SeleteCourse();
int CalcuCridit();
void practice( ); //工程实践
//……
};
// UNDERGRAD 类可以实例化
§ 8.6 纯虚函数与抽象类
//POSTGRAD类可以保持两个纯虚函数的定义
class POSTGRAD_ABSTRACT,public ABSTRACT_ STUDENT
{
public:
void SeleteCourse()=0;
int CalcuCridit()=0;
//……
private:
int supervisorID; //导师
};
//该类也不能实例化
§ 8.6 纯虚函数与抽象类
// DOCTORGRAD类必须实现两个虚函数
class DOCTORGRAD:public ABSTRACT_POSTGRAD
{
public:
void SeleteCourse();
int CalcuCridit();
//……
};
// DOCTORGRAD 类可以实例化纯虚函数举例
#include<iostream.h>
#include<math.h>
class BASE
{
public:
virtual void setx(int i,int j=0) //虚函数
{
x=i; y=j;
}
virtual void disp()=0; //纯虚函数
protected:
int x,y;
};
class SQUARE:public BASE //平方
{
public:
void disp()
{
cout<<"x="<<x<<":";
cout<<"x square="<<x*x<<endl;
}
};
class CUBE:public BASE //立方
{
public:
void disp()
{
cout<<"x="<<x<<":";
cout<<"x cube="<<x*x*x<<endl;
}
};
class CHPOW:public BASE //n次方
{
public:
void disp()
{
cout<<"x="<<x<<"y="<<y<<":";
cout<<"pow(x,y)="<<pow(double(x),double(y))
<<endl;
}
};
//x的 y次方
void main()
{
BASE *ptr;
SQUARE b;
CUBE c;
CHPOW d;
ptr=&b;
ptr->setx(5);
ptr->disp(); //调用 SQUARE类中的 disp()
ptr=&c;
ptr->setx(6);
ptr->disp(); //调用 CUBE类中的 disp()
ptr=&d;
ptr->setx(3,4);
ptr->disp(); //调用 CHPOW类中的 disp()
}
§ 8.6 纯虚函数与抽象类程序执行结果:
x=5,x square=25
x=6,x cube=216
x=3 y=4,pow(x,y)=81
//该程序 中 disp()在运行时进行选择调用哪个类,
实现了动态绑定
§ 8.6 纯虚函数与抽象类
抽象类带有纯虚函数的类称为抽象类,它处于继承层次结构的较上层
抽象类的作用将有关的子类组织在一个继承层次结构中,由 抽象类 来为它们提供一个公共的根,
相关的子类从这个根派生出来
§ 8.6 纯虚函数与抽象类抽象类的使用规定:
抽象类只能用作其他类的基类,不能建立抽象类的对象
抽象类不能用作参数类型,函数返回类型
可以说明指向抽象类的指针和引用,该指针可以指向它的派生类,进而实现多态类