多态
面向对象程序设计的优势在于继承,还在于将派生类对象当基类对象一样处理的功能。支持这种功能的机制就是多态和动态绑定。
多态性概述
多态是指同样的消息被不同类型的对象接收时导致不同的行为。
消息指对类的成员函数的调用,不同的行为是指不同的实现,即调用了不同的函数。
如同样的加号“+”,可实现整数、浮点数、双精度浮点数之间的加法,以及几种数据类型混合的加法运算。
同样的消息——相加,被不同类型的对象——变量接收后,不同类型的变量采用不同的方式进行加法运算。其中不同类型变量相加,需转换为相同类型在进行加法运算,这是典型的多态现象。
多态的类型
面向对象的多态性可分为4类: 重载多态、强制多态、包含多态和参数多态,前两者称为专用多态,后两者为通用多态。
普通函数以类的成员函数重载、运算符重载都属于重载多态
强制多态是将一个变元的类型加以变化,以符合一个函数或者操作的要求;
包含多态是类族中定义于不同类中的同名成员函数的多态行为,主要通过虚函数实现;
参数多态与类模板相关联,在使用时必须赋予实际的类型才可以实例化;
函数重载在第3、4中介绍过,这里重点介绍运算符重载。虚函数是介绍包含多态时的关键内容
多态的实现
多态从实现角度分两类:编译时多态和运行时的多态。
编译时多态是在编译过程中明确了同名操作的具体操作对象;
运行时多态是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象的过程成绑定(binding)。
绑定指计算机程序自身彼此关联的过程,即将一个标识符和一个存储地址联系在一起的过;
在面向对象中,是把一条消息和一个对象的方法向结合的过程。
按绑定阶段分为静态板顶和动态绑定,这两种绑定过程中分别对应着多态的两种实现方式;
绑定工作在编译链接阶段完成的情况称为静态绑定,因为绑定过程中程序开始执行之前进行的,也称早期绑定或前绑定,如重载、强制和参数多态;
绑定工作在程序运行阶段完成的情况称为动态绑定,成晚期绑定或后绑定。
包含多态操作对象的确定就是通过动态绑定完成的。
运算符重载
C++中预定义的运算符的操作对象只能是基本数据类型,对于许多用户自定义类型如类,也需有类似的运算操作。如一个复数类
class Complex{
public:
Complex(double r =0.0,double i=0.0):real(r),imag(i){}
//运算符+重载成员函数
Complex operator+ (const Complex &c2) const;
//运算符-重载成员函数
Complex operator- (const Complex &c2) const;
void display() const;//输出复数
private:
double real;
double imag;
};
便可这样声明复数类的对象:
Complex c1(5,4),c2(2,10),c3;
如直接使用"a+b",编译器不知道该如何完成加法。此时,需要自己编写程序来说明"+"在作用域Complex类对象时,该实现什么样功能,这就是运算符重载。
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
运算符重载其实质是函数重载。实现过程中,把指定的运算表达式转换为对运算符函数的调用,将运算符对象转换为运算符函数的实参,然后根据实参类型来确定需要调用的函数,该过程在编译过程中完成。
运算符重载规则
C++中运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。
重载之后运算符非优先级和结合性都不会改变;
运算符重载是针对新类型数据的实际需要,对原有运算符的进行适当的改造。
C++标准中,类属关系运算符"."、成员指针运算符" * “,作用域分辨符” :: “、和三目运算符” ?: "不能重载。
运算符的重载形式有两种,即重载为类的非静态成员函数和重载为非成员函数、
运算符重载为类的成员函数一般语法形式为:
返回类型 operator 运算符(形参表)
{
函数体
}
运算符重载为非成员函数的一般形式:
返回类型 operator 运算符(形参表)
{
函数体
}
返回类型即指重载运算符的返回值类型,即运算结果类型;
operator是定义运算符重载函数的关键字;
运算符即是要重载的运算符名称;
形参表中给出重载运算符所需要的参数和类型;
注意:当以非成员函数形式重载运算符时,需要访问运算符参数所涉及类的私有成员,可把该函数声明为类的友元函数;
当运算符重载为类的成员函数时,函数参数个数比原来的操作数个数要少一个(后置"++","–“除外);
当重载为非成员函数时,参数个数与原操作数个数相同。
重载为类成员函数时,第一个操作数会被作为函数调用的目的对象,无须出现参数表中,函数体中可直接访问第一个操作数的成员;
重载为非成员函数时,运算符的所有操作数必须显示通过参数传递;
运算符重载的主要优点是可改变现有运算符的操作方式,以用于类类型,使得程序看起来更直观;
运算符重载为成员函数
其实质是函数重载,重载为成员函数,它可自由地访问本类的数据成员;通过该类的某个对象来访问重载的运算符。
对于双目运算符B,实现表达式 oprd1 B oprd2,其中oprd1为A类的对象,则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。
重载之后,表达式oprd1 B oprd2 相当于 函数调用oprd1.operator B(oprd2);
对于前置单目运算符U,若重载为类的成员函数,用来实现表达式U oprd,其中oprd为A类的对象,则U应当重载为A类的成员函数,函数没有形参。重载之后,表达式U oprd相当于函数调用oprd.operator U()。
后置运算符”++" “–”,将它们重载为类的成员函数,用来实现表达式oprd++或oprd–,其中oprd为A类的对象,那么运算符应当重载为A类的成员函数,此时函数要带一个整型(int)形参;
重载之后,表达式oprd++和oprd–就相当于函数调用oprd.operator++(0)和oprd.operator–(0)。int类型参数在运算中不起任何作用,只是用于区别后置++、–和前置++、–。
使用UML语言,重载运算符表示方法与其他成员函数类似,其形式为"operator 运算符(形参表):函数类型"
8-1 复数类加减法运算符重载为成员函数形式
重载复数加减运算是一个双目运算符重载为成员函数。
复数的实部及虚部相加减,运算符的两个操作数都是复数类的对象。
带有加减法运算符重置的复数类的UML图形表示如下
/*8_1.cpp
双目运算符重载规则
如果要重载B为类成员函数,使之能够实现表达式oprd1 B oprd2
其中oprd1为A类对象,则B应被重载为A类的成员函数,形参类应该是oprd2所属类型
经重载后,表达式oprd1 B oprd2相当于oprd1 operator B(oprd2)
重载为类成员的运算符函数定义形式:
函数类型 operator 运算符(形参)
{
....
}
参数个数=源操作数个数-1(后置++,--除外)
要求:将+、-运算符重载为复数类的成员函数
规则:实部和虚部分别相加减
操作数:两个操作数都是复数类的对象
*/
#include<iostream>
using namespace std;
class Complex{
public:
Complex(double r =0.0,double i=0.0):real(r),imag(i){}
//运算符+重载成员函数
Complex operator+ (const Complex &c2) const;
//运算符-重载成员函数
Complex operator- (const Complex &c2) const;
void display() const;//输出复数
private:
double real;
double imag;
};
Complex::operator+(const Complex &c2) const{
//创建一个临时无名对象作为返回值
return Complex(real+c2.real,imag+c2.imag);
}
Complex Complex::operator-(const Complex &c2) const{
//创建一个临时无名对象作为返回值,形式不同于上,但含义一样
//return Complex(real-c2.real,imag-c2.imag);
Complex c(real-c2.real,imag-c2.imag);
return c;
}
void Complex::display() const{
cout<<"("<<real<<","<<imag<<")"<<endl;
}
int main(){
Complex c1(5,4),c2(2,10),c3;
cout<<"c1=";c1.display();
cout<<"c2=";c2.display();
c3 = c1-c2;//使用重载运算符完成复数减法
cout<<"c3=c1-c2=";c3.display();
c3 = c1+c2;//使用重载运算符完成复数加法
cout<<"c3=c1+c2=";c3.display();
return 0;
}
8-2 将单目运算符"++"重载为成员函数形式
将单目运算符重载为类的成员函数;
其前置++和后置++的操作数时时钟类的对象,可把该运算符重载为时钟类的成员函数。
前置单目运算符 重载函数没有形参,后置单目运算符重载函数有一个int型形参
/*8_2.cpp
重载前置++ 和后置++为时钟类成员函数
前置单目运算符 重载函数没有形参
后置++运算符,重载函数需要一个int形参
操作数是时钟类的对象
实现时间增加1秒钟
*/
#include<iostream>
using namespace std;
//时钟类定义
class Clock{
public:
Clock(int hour=0,int minute=0, int second=0);
void showTime() const;
//前置单目运算符
Clock& operator++ ();
//后置单目运算符重载
Clock operator ++ (int);
private:
int hour,minute,second;
};
Clock::Clock(int hour,int minute,int second){
if(0 <= hour && hour<=24 && 0<=minute && minute<= 60
&& 0 <=second && second <=60){
this->hour = hour;
this->minute = minute;
this->second = second;
}else
cout<<"Time error"<<endl;
}
void Clock::showTime() const{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
Clock & Clock::operator++(){
second++;
if(second >=60){
second -= 60;minute++;
if(minute>=60){
minute -= 60;hour = (hour+1)%24;//?
}
}
return *this;
}
Clock Clock::operator ++(int){
// 注意形参表中整型参数
Clock old = *this;
++(*this); //调用前置 ++ 运算符
return old;
}
int main(){
Clock myClock(23,59,59);
cout<<"First time output";
myClock.showTime();
cout<<"Show myClock++";
(myClock++).showTime();
cout<<"Show ++myClock";
(++myClock).showTime();
return 0;
}
First time output23:59:59
Show myClock++23:59:59
Show ++myClock0:0:1
运算符重载为非成员函数
运算符可重载为非成员函数。运算所需要的操作数需通过函数的形参表来传递,在形参表中形参从左到右的顺序使运算符操作数的顺序。若需要访问运算符参数对象的私有成员,可将该函数声明为类的友元函数;
对于双目运算符B,若实现oprd1 B oprd2,其中oprd1和oprd2中只要一个具有自定义类型,就可以将B重载为非成员函数,函数的形参为oprd1和oprd2。重载之后,表达式oprd1 B oprd2 相当于函数调用operator B(oprd1,oprd2)。
对前置单目运算符 U,如"-"(负号)等, 如果要实现表达式U oprd,其中oprd具有自定义类型,就可将U重载为非成员函数,函数形参为oprd。重载之后,表达式U oprd 相当于函数调用operator U (oprd)
对于后置单目运算符 U,如"-"(负号)等, 若要实现表达式U oprd,其中oprd具有自定义类型,可将U重载为非成员函数,函数的形参为oprd。重载之后,表达式 U oprd相当于函数调用opertaot U(oprd).
对于后置运算符++和–,若要实现oprd++或oprd–,其中oprd为自定义类型,那么运算符可重载为非成员函数,这时函数的形参有两个,一个是oprd,另一个是int类型形参。第二个参数是用于与前置运算符函数相区别的。重载之后,表达式oprd++和oprd–相当于函数调用operator++(oprd,0)和operator–(oprd,0)。
8-3 以非成员函数形式重载Complex的加减法运算和"<<"运算符
将运算符+ - 重载为非成员函数,并将其声明为Complex类的友元函数
/*8_3.cpp
重载Complex的加法减法和"<<"运算符为非成员函数
将+、-(双目)运算符重载为非成员函数,并将其声明为复数类的友元
两个操作数都是复数类的常引用
将<<(双目)重载为非成员函数,将其声明为 复数类的友元,
它的左操作数是std::ostream引用,右操作数的常引用,
返回std::ostream引用,用以支持下面形式的输出:
cout<<a<<b;
该输出调用:
operator<<(operator<<(cout,a),b);
*/
#include<iostream>
using namespace std;
class Complex{
public:
// 构造函数,使用初始化列表初始化私有成员变量
Complex(double r =0.0,double i = 0.0):real(r),imag(i){}//构造函数
//运算符重载为类的非成员 函数,声明为友元函数来访问类的私有成员变量
friend Complex operator+(const Complex &c1,const Complex &c2);//运算符+重载
friend Complex operator-(const Complex &c1,const Complex &c2);
friend ostream & operator<<(ostream &out,const Complex &c);
private:
double real//复数实部
double imag;//复数虚部
};
Complex operator+(const Complex &c1,const Complex &c2){
return Complex(c1.real+c2.real,c1.imag+c2.imag); //??友元函数可以直接使用私有成员变量
}
Complex operator-(const Complex &c1,const Complex &c2){
return Complex(c1.real-c2.real,c1.imag-c2.imag); //??友元函数可以直接使用私有成员变量
}
ostream & operator<<(ostream &out,const Complex &c){
out<<"("<<c.real<<","<<c.imag<<")";
return out; ///返回out??
}
int main(){
Complex c1(5,4),c2(2,10),c3;
cout<<"c1="<<c1<<endl;
cout<<"c2="<<c2<<endl;
c3 = c1-c2; //使用重载运算符完成复数减法
cout<<"c3=c1-c2"<<c3<<endl;
c3 = c1+c2; //使用 重载运算符完成复数加法
cout<<"c3 = c1+c2="<<c3<<endl;
cout<<c1;//执行该语句时,会调用 operator<<(cout,c1);
return 0;
}
c1=(5,4)
c2=(2,10)
c3=c1-c2(3,-6)
c3 = c1+c2=(7,14)
暂时介绍几个简单运算符的重载,还有如"[]"、"="、类型转换等, 进行重载时有一些不同的情况,在后续结合"安全数组类模板"进行介绍。
虚函数
一个人银行账户管理的简单程序中,如何利用一个循环结构依次处理同一类族中不同类的对象。
深度搜索
组合与继承
类的继承使得已有对象成为新对象的一部分,从而达到代码复用的目的。
组合反映的是**“有一个**(has-a)的关系”。
如果类B中存在一个类A的内嵌对象,表示的是每一个B类型的对象都"有一个"A类型的对象。
抽象示例
class Engine{ //发动机类
public:
void work(); //发动机运转
...
};
class Wheel{ //轮子类
public:
void roll(); //轮子转动
...
};
class Automobile{ //汽车类
public:
void move(); //汽车移动
...
private:
Engine engine; //汽车引擎
Wheel wheels(4); //4个轮子
};
代码描述一辆汽车有一个发动机,有4个轮子
继承使用最为普遍的公有继承的是一个(is-a)关系。若类A是类B的公有基类,那么这表示每一个B类型的对象都是一个A类型的对象。
class Turck:public Automoblie{ //卡车
public:
void load(...); //装货
void dump(...); //卸货
private:
...
};
class Pumper:public Automoblie{ //消防车
public:
void water(); //碰水
private:
...
};
代码实现一个卡车,是一个汽车,一个消防车。其中卡车和消防车都是特殊的汽车,具有汽车的功能–移动,汽车也做的事情,它们也能做。除了具有移动的功能外,卡车还可以装货、卸货,消防车还可以碰水。
派生类对象的内存布局
类型兼容规则使得一个派生类的指针可被隐含转换为基类的指针;
一个类型的指针可能指向该类型的对象,也可指向它的派生类的对象,即它所指向对象的类型是不确定的;
调用一个类的成员函数时,调用的目的对象时以指针形式(this指针)作为参数传递给被调函数的,一个函数在执行中得到了this指针所指向的对象类型也是不确定的,需要一种机制来保证指针访问到正确的数据。
派生对象的内存布局需满足的要求是,一个基类指针,无论其指向基类对象,还是派生类对象,通过它来访问一个基类中定义的数据成员,都可以用相同步骤。
单继承情况
class Base {...};
class Derived:public Base {...};
Derived从Base继承的数据成员,全部放在前面,且数据成员在Base类的对象放置的顺序保持一致;
Derived类新增数据成员全部放在后面;
Derived指针到Base指针的隐含转换
Base *pha = new Base();
Derived *pd = new Derived();
Base *pbb = pd;
pha和pbb这两个Base类型的指针,虽然指向的对象具有不同的类型,但任何一个Base数据成员到该对象首地址都具有相同的偏移量,因此使用Base指针pha和pbb访问Base类中定义的数据成员时,可采用相同的方式,而无须考虑具体的对象类型。
多继承的情况
看下继承关系
class Base1{...};
class Base2{....};
class Derived:public Base1,public Base2{...};
Derivied类继承Base1类和Base2类,在Berived类的对象中,前面一次存放的是从Base1类和Base2类继承而来的数据成员,数据成员顺序类似单继承;
Base若出现从Dervied指针到Base1指针或Base2指针的隐含转换;
Base1 *pb1a = new Base1();
Base2 *ph2a = new Base2();
Derived *pd = new Dervied();
Base1 *pb1b = pd;
Base2 *pb2b = pd;
将pd赋给pb1b指针时,与单继承时情形相似,只需要把地址复制一遍即可,但将pd赋给pb2b指针时,则不能简单地执行地址复制操作,而应当在原地址的基础上加一个偏移量,使pb2b指针指向Dervied对象中Base2类的成员的首地址。
对于同为Base2类型指针的pb2a和pb2b来说,它们都指向Base2中定义,以相同方式分布的数据成员。、
注:指针转化并非都保持原先的地址不变,地址的算术运算可能在指针转换时发生;
虚继承情况
看下继承关系
class Base0{...};
class Base1:virtual public Base0{...};
class Base2:virtual public Base0{...};
class Derived:public Base1,public Base2{...};
Base1类型指针和Base2类型指针都可指向Dervied对象,且通过这两类指针都可访问Base0类中定义的数据成员,但这些数据成员在Dervied对象中只有一份。
图中(d)pb2b指向不应该是Base2类新增数据成员???
Base0 *ph0a = new Base0();
Base1 *ph1a = new Base1();
Base2 *ph2a = new Base2();
Derived *pd = new Derived();
Base1 *pb1b = pd;
Base2 *pb2b = pd;
Base0 *pb0b = pb1b ;
对于pb1a与pb1b两个Base1类型的指针,它们指向不同类的对象,而Base0类的数据成员到这两个指针具有不同的偏移量,但指向Base0成员隐含指针相对于pb1a和pb1b两个指针的位置是相同,因此能够通过相同地方方式取得这个隐含的Base0指针值。进而通过相同的步骤访问到Base0类的数据成员,而无须考虑到具体的类型。
pb2a与pb2b这两个相同类型却指向不同类型对象的指针,情况也类似。
当把pb1b指针赋给pb0b指针时,不能在按照将pd指针赋给pb2b指针时的那种将地址值加上偏移量的方式,因为pb1b指针可能指向Base1对象或Dervied对象,这两种情况下,偏移量是不同的。这里通过pb1b指针找到隐含的指向Base0类型数据成员的指针,将该指针值读出,赋给pb0b指针。这是"指针类型转换时不止是复制地址"的又一例。
基类向派生类的转换及其安全性问题
派生类指针可隐含转换为基类指针,允许这种转换隐含发送,是因为它是安全的转换。
派生类指针想要转换为基类指针,则转换一定要显式地进行。
Base *pb = new Derived();//指针隐式转换
Derived *pb = static_case<Derived* >(pb);//指针显式转换
基类指针进而指向任何派生类的对象,因此基类指针和派生类指针也具有一般和特殊的关系,如void指针和具体类型的指针一样;
C++中从特殊的指针转换到一般指针是安全,允许隐式转换,反之是不安全,所以需要显式地转换;
对于引用涞水,情况类似
Derived d;
Base &rb = d;//用Dervied对象给Base引用初始化,发生了到Base引用的隐含转换
Derived &rb = static_case<Derived &>(rb);//将Base引用隐含转换为Dervied引用
值得注意是基类对象与派生类对象之间转换关系,P301
面向对象三大概念:封装、继承、多态;
多态:从某个函数中取得“正确的”行为,而又不依赖于实际使用的到底是哪一种函数,这就是所谓的多态性。
一个带有虚函数的类型被称为一个多态类型;要在C++里取得多态性的行为,被调用的函数必须是虚函数,而对象则必须是通过指针或者引用去操作的;
为什么需要多态?
若子类定义和父类中原型相同的函数会发生什么?
#include <iostream>
using namespace std;
class Parent
{
public:
void print(){
cout<<"Parent:print() do..."<<endl;
}
};
class Child : public Parent
{
public:
void print(){
cout<<"Child:print() do..."<<endl;
}
};
int main(){
//子类和父类中函数同名,父类中同名含默认被 隐藏
Child child;
child.print();
//
Parent *p = NULL;
p = &child;
(*p).print();
//怎么通过分辨符访问父类中被隐藏的函数?
child.Parent::print();
system("pause");
return 0;
Child:print() do...
Parent:print() do...
Parent:print() do...
请按任意键继续. . .
父类中被重写的函数依然会继承给子类,默认情况下子类中重写的函数将隐藏父类中的函数,通过作用域分辨符::可访问到父类中被隐藏的函数;
C/C++是静态编译型语言,在编译时,编译器自动根据指针的类型判断指向的是一个什么的对象。
1、在编译此函数的时,编译器不可能知道指针 p 究竟指向了什么。
2、编译器没有理由报错。
3、编译器认为最安全的做法是编译到父类的print函数,因为父类和子类肯定都有相同的print函数。
面向对象新需求
编译器的默认选择不是我们所想要的;根据实际的对象类型来判断重写函数的调用;
若父类指针指向父类对象则代用父类中的函数;
若父类指针指向子类对象则调用子类中的函数;
解决方法:
使用virtual关键字对多态进行支持;
使用virtual声明的函数被重写后可展现多态特性;
多态条件
要有继承关系、存在函数重写(C虚函数)、存在父类指针(父类引用)指向子类对象;
多态理论基础
静态联编和动态联编
联编指一个程序模块、代码之间互相关联的过程;
静态联编(static binding)是程序匹配、连续在编译阶段实现,即早期匹配,如重载函数使用静态联编;
动态联编指程序联编推迟到运行时进行,即晚期联编(迟绑定),如switch语句和if语句是动态联编;
C++与C相同,是静态编译型语言;
编译时,编译器自动根据指针的类型判断指向是一个什么样的对象,所以编译器认为父类指针指向的父类对象;
程序未运行时,无法判断父类指针指向的具体对象(父类或子类);
从程序安全角度,编译器假定父类指针指向父类对象,所以编译的结果为调用父类的成员函数,该特性即为静态联编;
Question
怎么理解多态
多态是同样的调用语句有多种不同的表现形态;
实现的三个条件:有继承、有virtual重写、有父类指针(引用)指向子类对象;
C++实现:
virtual关键字告诉编译器该函数要支持多态;不是根据指针类型判断调用关系,而是根据指针指向的实际对象类型来判断调用关系;
根据实际的对象类型来判断重写函数的调用;
多态是设计模式的基础?
函数指针做函数参数是多态实现的基础?
多态原理?
对重写、重载理解
函数重载:
必须在同一个类中进行;
子类无法重载父类的函数,父类同名函数将被名称覆盖;
重载是在编译期间根据参数类型和个数决定函数调用;
函数重写
必须发生在父类与子类之间;
且父类与子类中的函数必须有完全相同的原型;
使用virtual声明后,能够产生多态(若不使用virtual,那是重定义);
多态在运行期间根据具体对象的类型决定函数调用;
C++编译器多态实现原理理解
是否可将每个成员函数够声明为虚函数,为什么?