多态
多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。
最简单的例子就是使用同样的运算符'+',可以实现整数与整数之间,浮点数与浮点数之间的加法运算。
多态的类型
面向对象的多态性可以分为4类:重载多态﹑强制多态、包含多态和参数多态。
重载多态:
包含多态:
强制多态:
参数多态:
多态的实现
两类:编译时多态(早绑定,模板类,函数重载)与运行时多态(晚绑定,继承,虚函数)
确定操作的具体对象的过程:绑定( binding):绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程;用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。
绑定是指计算机程序自身彼此关联的过程,也就是把一-个标识符名和一个存储地址联系在一起的过程;用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。
运算符重载
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
运算符重载的实质就是函数重载。在实现过程中,首先把指定的运算表达式转化为对运算符函数的调用,将运算对象转化为运算符函数的实参,然后根据实参的类型来确定需要调用的函数,这个过程是在编译过程中完成的。
运算符重载规则:
1.只能重载C++已有的运算符,且重载之后运算符的优先级和结合性都不会改变。
2.不能改变原操作符的操作对象个数,且至少有一个操作对象是自定义类型
3.不能重载的运算符,类属关系运算符“.”,成员指针运算符“.*”,作用域标识符“::”,与三木运算符“?:”,以及sizeof
语法形式:
返回类型 operator 运算符(形参表)
{
函数体
)
返回类型指定了重载运算符的返回值类型,也就是运算结果类型, operator是定义运算符重载函数的关键字,运算符即是要重载的运算符名称,必须是C++中可重载的运算符,比如要重载加法运算符,这里就写“+”,形参表中给出重载运算符所需要的参数和类型。
注:当以非成员函数形式重载运算符时,有时需要访问运算符参数所涉及类的私有成员,这时可以把该函数声明为类的友元函数。
运算符重载为成员函数:
对于双目运算符B,如果要重载为类的成员函数,使之能够实现表达式opr1 B opr2,其中 opr1为A类的对象,则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载之后﹐表达式oprdl B oprd2就相当于函数调用oprd1.operator B(oprd2)。
对于后置运算符“++”和“- -”,如果要将它们重载为类的成员函数,用来实现表达式oprd++或oprd- -,其中oprd 为A类的对象,那么运算符就应当重载为A类的成员函数,这时函数要带有一个整型( int)形参。重载之后,表达式oprd++和 oprd- - 就相当于函数调用oprd.operator++(0)和oprd. operator- - (0)。这里的int类型参数在运算中不起任何作用,只是用于区别后置++,- -与前置++,- -。
#include <iostream>
using namespace std;
class Cormpiex {
//复数类定义
//外部接口
public:
Complex (double r=o.0,double i=0.0) : real(r), imag(i) f }
//构造函数
Complex operator+ (const Complex &c2)const;
//运算符+重载成员函数
Complex operator- (const Corplex &c2) const;
//运算符-重载成员函数
void display () const;
//输出复数
private:
//复数实部
double real;
//复数虚部
double imag;
}
complex Complex :: operator+ (const Complex &c2) const {
//重载运算符函数实现
return Complex(real+c2.real,imag+ c2.imag);//创建一个临时无名对象作为返回值
}
Complex Complex :: operator- (const Complex sc2) const {
//重载运算符函数实现
return Complex (real-c2.real,imag-c2.imag);//创建一个临时无名对象作为返回值
}
void Complex :: display () const {
cout<< "(" << real << ","<< imag <<" ) "<< endl;
)
int main () {
//主函数
complex c1(5,4),c2(2,10),c3;
//定义复数类的对象
cout<<"c1=- "; cl.display () ;
cout<< "C2="; c2.display {) ;c3=cl-c2;
//使用重载运算符完成复数减法
cout<< "c3=cl- c2="; c3.display();
c3=c1+c2;
//使用重载运算符完成复数加法
cout<< "c3=cl+c2="; c3.display() ;
return 0;
}
前置++与后置++运算符重载:
Clock operation++(); //前置++
Clock operation++(int); //后置++
虚函数
虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态(如果基类指针指向派生类对象,那么此时指向的对象可以不再被认定为基类,而是派生类)。
一般虚函数成员
virtual 函数类型 函数名(形参表);
这实际上就是在类的定义中使用virtual关键字来限定成员函数,虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
运行过程中的多态需要满足3个条件,第一是类之间满足赋值兼容规则(如果一个类型的每个数据成员都可以赋值给另一个类型的一个数据成员,那么这两个类型是赋值兼容的。),第二是要声明虚函数,第三是要由成员函数来调用或者是通过指针、引用来访问虚函数。如果是使用对象名来访问虚函数﹐则绑定在编译过程中就可以进行(静态绑定),而无须在运行过程中进行。
注:虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数处理。但将虚函数声明为内联函数也不会引起错误。
class Basel {
//基类Base1定义
public:
virtual void display() const;
//虚函数
};
void Base1 :: display () const {
cout<<"Base1 ::display()"<<endl;
}
class Base2 : public Basel {
//公有派生类Base2定义
public:
void display() const;
//覆盖基类的虚函数
};
void Base2 :: display () const {
cout<<"Base2 : :display i)"<<endl;
}
如果从名称,参数及返回值3个方面检查之后,派生类的函数满足了上述条件,就会自动确定为虚函数(与一般的重载不同,一般的重载只检查名称与参数,不检查返回值)。这时,派生类的虚函数便覆盖了基类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有其他重载形式。
注:派生类指针使用::的方式仍能够访问基类的虚函数,ptr->Base1.display()
习惯:在派生类中重载虚函数时,虽然使不使用virtual关键字它都是一个虚函数,但是为了使代码的可读性提高,一般加上virtual关键字
虚析构函数
虽然不能声明虚构造函数,但是可以声明虚析构函数
一般声明形式:
virtual ~ 类名();
析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作。
简单来说,如果有可能通过基类指针调用对象的析构函数(通过delete,相当于C语言的free),就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。
class Base {
public:
~Base ( );
};
Base :: ~ Base(){
cout<<"Base destructor"<<endl;
}
class Derived: public Base {
public:
Derived ( ) ;
~Derived () ;
private:
int * P;
};
Derived :: Derived () {
p = new int(O) ;
)
Derived :: ~ Derived () {
cout << "Derived destructor" << endl ;
delete p;
}
void fun(Base *b){
delete b;
}
int main(){
Base *b = new Derived();
fun(b);
return 0;
}
输出信息:
Base destructor
此时基类指针删除派生类对象调用的是基类的析构函数,造成了内存泄漏,此时,应该把析构函数声明为虚函数
class Base {
public:
virtual ~Base ( );
};
此时输出信息
Base destructor
Derived destructor
派生类析构函数执行顺序:基类-->派生类
派生类构造函数执行顺序:派生类-->基类
纯虚函数和抽象类
抽象类:
抽象类是一种特殊的类,抽象类是带有纯虚函数的类,它为一个类族提供统一的操作界面。可以说,建立抽象类,就是为了通过它多态地使用其中的成员函数。抽象类处于类层次的上层,一个抽象类自身无法实例化,也就是说我们无法定义一个抽象类的对象﹐只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化。
纯虚函数:
纯虚函数声明形式:virtual 函数类型 函数名(参数表)= 0;
纯虚函数与一般虚函数成员的原型在书写格式上的不同就在于后面加了“= 0”。声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。但是此时派生类必须写出纯虚函数的函数体。
注:
1.基类中仍然允许对纯虚函数给出实现,但即使给出实现,也必须由派生类覆盖,否则无法实例化。
在基类中对纯虚函数定义的函数体的调用,必须通过“基类名∵函数名(参数表)"的形式。如果将析构函数声明为纯虚函数,必须给出它的实现,因为派生类的析构函数体执行完后需要调用基类的纯虚函数。
2.纯虚函数不同于函数体为空的虚函数,纯虚函数根本就没有函数体,而空的虚函数的函数体为空;前者所在的类是抽象类,不能直接进行实例化,而后者所在的类是可以实例化的。它们共同的特点是都可以派生出新的类﹐然后在新类中给出虚函数新的实现,而且这种新的实现可以具有多态特征。
抽象类
抽象类就是带有纯虚函数的类。抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。抽象类声明了一个类族派生类的共同接口,而接口的完整实现,即纯虚函数的函数体,需要由派生类自己定义。
抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是-个抽象类。也就是说,派生类需要给出所有纯虚函数的实现,否则无法实例化
虽然抽象类不能实例化,但是可以定义一个抽象类的指针,来访问派生类对象,进而访问派生类成员
#include <iostream>
using namespace std;
class Base1 //基类Base定义
{
public:
virtual void display() const = 0; //纯虚函数
};
class Base2: public Base1 //共有派生类Base2定义
{
public:
void display() const; //覆盖基类的虚函数
};
void Base2::display() const
{
cout << "Base2::display()" << endl;
}
class Derived: public Base2 //公有派生类Derived定义
{
public:
void display() const; //覆盖基类的虚函数
}
void Derived::display() const
{
cout << "Derived::display()" << endl;
}
viod fun(Base1 *ptr) //参数为指向基类对象的指针
{
ptr->display(); //对象指针 -> 成员名
}
int main()
{
Base2 base2;
Derived derived;
fun(&base2);
fun(&derived);
return 0;
}
运行结果:
Base2::display()
Derived::display()
在fun函数中通过基类Basel的指针 ptr就可以访问到ptr指向的派生类Base2和Derived类对象的成员。这样就实现了同一类组的对象进行统一的多态处理
函数模板与类模板
模板:通过它可以实现参数化多态性。所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
函数模版
函数模板的定义形式:
template <模板参数表>
类型名 函数名(参数表)
{
函数体的定义
}
所有函数模板的定义都是用关键字template开始的,该关键字之后是使用尖括号<>括起来的“模板参数表”。模板参数表由用逗号分隔的模板参数构成,可以包括以下内容。
1.class(或typename)标识符,指明可以接收一个类型参数,这些类型参数代表的是类型,可以是内部类型或自定义类型
2.“类型说明符”标识符,指明可以接收一个由“类型说明符”所规定类型的常量作为参数。
3.template<参数表>class标识符,指明可以接收一个类模板名作为参数。
类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量。函数模板中函数体的定义方式与定义普通函数类似。
#include <iostream>
using namespace std;
template <typename T>
T abs(T t)
{
return x<0 ? -x : x;
}
int main()
{
int n = -5;
double d = -5.5;
cout << abs(n) << endl;
cout << abs(d) << endl;
return 0;
}
此时输出:
5
5.5
类模板
使用类模板使用户可以为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数、返回值或局部变量能取任意类型(包括系统预定义的和用户自定义的)。
类是对一组对象的公共性质的抽象,而类模板则是对不同类的公共性质的抽象,因此类模板是属于更高层次的抽象。由于类模板需要一种或多种类型参数,所以类模板也常常称为参数化类。
类模板声明的一般形式:
template<模板参数表>
class 类名
{
类成员声明
}
类模板外定义:
template <模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数列表)
使用模板类创建对象:
模板名<模板参数表>对象名1,...,对象名n
模板类应用:
分析:在本实例中,声明一个实现任意类型数据存取的类模板Store,然后通过具体数据类型参数对类模板进行实例化,生成类,然后类再被实例化生成对象s1,s2,s3和d.不过类模板的实例化过程(如图9-5所示)在程序中是隐藏的。
#include <iostream>
#include <cstdlib>
using namespace std;
struct Student
{
int id; //学号
float gpa; //平均分
};
template<class T>
class Store
{
public:
Store(); //无参构造函数
T &getElem(); //提取数据函数
void putElem(); //存入数据函数
private:
T item; //用于存放各种类型的数据
bool haveValue; //用于标记item是否存入内容
};
//以下实现各成员函数
template<classT> //默认构造函数的实现
Store<T>::Store():haveValue(false){}
template<class T> //提取数据函数的实现
T &Store<T>::getElem()
{
if(!haveValue)
{
cout<<"No item present!"<<endl; //如果试图提取未初始化的数据,则终止程序
exit(1); //使程序完全退出,返回到操作系统
//参数可用来表示程序终止的原因,可以被操作系统接收
}
return item; 返回item中存放的数据
}
template<class T> //存人数据函数的实现
void store<T>::putElem(const T &x)
{
haveValue=true; //将haveValue置为true,表示item中已存人数值
item=x; //将x的值存入item
}
int main()
{
Store<int>s1,s2; //定义两个Store<int>类对象,其中数据成员item为int类型
s1.putElem(3); //向对象s1中存入数据(初始化对象s1)
s2.putElem(-7); //向对象s2中存人数据(初始化对象s2)
cout<<sl.getElem()<<""<<s2.getElem()<<endl; //输出对象s1和s2的数据成员
Student g={1000,23}; //定义student类型结构体变量的同时赋予初值
Store<Student>s3; //定义 Store<student>类对象s3,其中数据成员item为Student类型
s3.putElem(g); //向对象s3中存人数据(初始化对象s3)
cout<<"The student id is "<<s3.getElem().id<<endl; //输出对象s3的数据成员
Store<double>d;//定义 store<doubie>类对象 d,其中数据成员 item为 double类型
cout<<"Retrieving obiectd...";
cout<<d.getElem()<<endl;//输出对象d的数据成员
//由于d未经初始化,在执行函数d.getElement()过程中导致程序终止
return 0;
}
运行结果:
3 -7
The student idis 1000
Retrieving object d… No item present!