5.1 多态的引入
思考:在之前实现的英雄模型中,假如实现某个接口可以传入一个英雄,在该接口中可以对英雄的力量、敏捷和智力进行加强,请问该接口的参数该如何设计?
因为接口需要传入一个英雄对象,而每个英雄都属于不同类的实例化对象,所以如果我们对接口的参数不做特殊处理,那么有多少个英雄我们就需要设计多少个这样的函数,而每个函数中执行的逻辑是一样的只是处理的对象不一样而已,如此一来代码的重用度就高了,C++提供了这样一种机制:基类指针可以指向派生类对象,可以完美解决这个问题。我们只需要将该接口的参数设计为一个所有英雄共同的基类Hero的指针,然后传参时传递英雄对象的指针即可。
以上解决办法利用了C++中的多态,接下来让我们来了解一下C++中的多态。
多态:一个函数有多种形态。
多态的分类:静态联编 和动态联编
通常来说联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编 和动态联编 。
5.2 静态联编
5.2.1 静态联编的概念
静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。
5.2.2 静态联编的体现
1、隐藏
2、函数的的重载
3、运算符重载
4、泛型编程
5.3 运算符重载
5.3.1 运算符重载概述
1、运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。(运算符重载不能改变本来寓意,不能改变基础类型寓意)
2、运算符重载(operator overloading)只是一种 ” 语法上的方便 ” ,也就是它只是另一种函数调用的方式。
3、在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
4、可以重载的运算符
imagepng
5、不可重载的运算符:
成员访问运算符(.)
成员指针访问运算符(->)
域运算符(::)
长度运算符(sizeof)
条件运算符(: ?)
预处理符号(#)
5.3.2 运算符重载实例
#include
#include
#include
#include
using namespace std;
class Test
{
public:
int data;
char *ptr;
//默认构造函数
Test()
{
cout << "Test()" <<endl;
data = 0;
ptr = new char[10];
}
//带参数的构造函数
Test(int data, const char *src)
{
this->data = data;
if (src)
{
this->ptr = new char[strlen(src)+1];
strcpy(this->ptr, src);
}
else
ptr = new char[10];
}
//拷贝构造函数。调用时机:使用一个构造好的对象初始化一个新的对象 Test t1 = t2;
Test(const Test &t)
{
cout << "Test(const Test &t)" << endl;
//实现深拷贝
this->data = t.data;
//this->ptr = t.ptr; //浅拷贝
if (strlen(t.ptr))
{
this->ptr = new char[strlen(t.ptr)+1];
strcpy(this->ptr, t.ptr);
}
else
ptr = new char[10];
}
~Test()
{
if (ptr)
delete[] ptr;
}
//该运算符重载函数由 左操作数调用, 右操作数当做实参传递给该函数, 触发:t1 + t3 -> t1.operator +(t3)
Test operator +(const Test &t)
{
cout << "Test operator +(Test &t)" << endl;
Test val; //保存最后执行的结果
val.data = this->data + t.data; //this->data : t1.data
val.ptr = new char[strlen(this->ptr) + strlen(t.ptr) + 1];
//将分配的堆空间初始化成\0
memset(val.ptr, 0, strlen(this->ptr) + strlen(t.ptr) + 1);
strcat(val.ptr, this->ptr);
strcat(val.ptr, t.ptr);
return val;
}
bool operator > (const Test &t) // t1>t3
{
cout << "bool operator > (const Test &t)" << endl;
if (strcmp(this->ptr, t.ptr) > 0)
// if (this->data > t.data)
return true;
return false;
}
char operator [](int index) // index:下标
{
cout << "char operator [](int index)" << endl;
if (index < 0 || index >= strlen(ptr))
return '\0';
return ptr[index];
}
Test &operator =(const Test &t) //t1 = t2 t3 = t1 = t2;
{
cout << "Test &operator =(const Test &t)" << endl;
//深拷贝
delete[] this->ptr;
this->ptr = new char[strlen(t.ptr) + 1];
strcpy(this->ptr, t.ptr);
data = t.data;
return *this;
}
//前置++
Test &operator ++ () //++t1 t3 = ++t1;
{
cout << "Test &operator ++ ()" << endl;
++data;
return *this;
}
//后置++
Test operator ++ (int) // t1++ t3 = t1++;
{
cout << "Test operator ++ (int)" << endl;
//临时变量保存++之前的状态
Test tmp = *this;
data++;
return tmp;
}
//<< 重载
friend ostream &operator << (ostream &os, const Test &t) // cout << t1 : 左操作数不是一个Test对象,将左操作数和右操作数当作实参传递给该函数
//cout : ostream这个类的一个实例化对象
{
os << t.data << endl;
os << t.ptr << endl;
return os;
}
};
int main() {
#if 0
// + 运算符重载
Test t1(10, "hello");
Test t2 = t1;
cout << t2.data << endl;
cout << t2.ptr << endl;
Test t3(10, "world");
cout << "**************" <<endl;
Test t4 = t1 + t3; //20, helloworld
/*
* 思考:执行 Test t4 = t1 + t3 语句会调用哪些函数?
* 执行 t1 + t3表达式 会触发 Test operator +(Test &t)函数的调用
* 因为+运算符重载函数返回一个构造好的对象,最后执行:
* Test t4 = val; 触发拷贝构造函数的调用
* */
cout << t4.data << endl;
cout << t4.ptr << endl;
// > 运算符的重载
Test t1(10, "heLlo");
Test t2(20, "hello");
if (t1 > t2)
cout << "t1 > t2" << endl;
else
cout << "t1 <= t2" << endl;
// [] 运算符的重载
cout << t1[4] << endl;//t1.ptr[4]
// = 符号的重载
t1 = t2;
cout << t1.ptr << endl;
printf("%p, %p\n", t1.ptr, t2.ptr);
++t1; //++t1.data;
// t1++;
//前置++的重载
Test t1(10, "heLlo");
Test t2;
t2 = ++t1;
cout << t2.data << endl;
cout << t1.data << endl;
//后置++的重载
Test t3;
t3 = t1++;
cout << t3.data << endl; //11
cout << t1.data << endl; //12
cout << t1.data << endl;
cout << t1.ptr << endl;
string s("hello");
cout << s << endl;
cout << t1 << endl;
#endif
Test t1(10, "heLlo");
cout << t1 ;
return 0;
}
5.3.3 前置++和后置++
class A {
private:
int a;
public:
A& operator++() { //前置++
++a;
return *this;
}
A operator++(int) { //后置++
A a = *this;
++*this;
returnn a;
}
}
因为后置++在实现的时候构造了一个临时对象,临时对象的构造和销毁都需要消耗一定的系统资源,所以后置++的效率比前置++的效率低。
5.4.1 友元函数
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数);
友元函数的使用:
1、友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
2、一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
3、友元函数的调用与一般函数的调用方式和原理一致。
4、友元函数中没有this指针
5、两个类要共享数据的时候可以使用友元函数,比如:类A中的函数需要访问类B中的成员,那
么类A中该函数要是类B的友元函数。
6、运算符重载的某些场合需要使用友元函数,例如 << 的重载
#include
using namespace std;
class CCar; //提前声明CCar类,以便后面的CDriver类使用
class CDriver
{
public:
void ModifyCar(CCar* pCar); //改装汽车
};
class CCar
{
private:
int price;
friend int MostExpensiveCar(CCar cars[], int total); //声明友元
friend void CDriver::ModifyCar(CCar* pCar); //声明友元
};
void CDriver::ModifyCar(CCar* pCar)
{
pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar(CCar cars[], int total) //求最贵气车的价格
{
int tmpMax = -1;
for (int i = 0; i<total; ++i)
if (cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main()
{
return 0;
}
5.4.2 友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以访问另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;
其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。
class CCar
{
private:
int price;
friend class CDriver; //声明 CDriver 为友元类
};
class CDriver
{
public:
CCar myCar;
void ModifyCar() //改装汽车
{
myCar.price += 1000; //因CDriver是CCar的友元类,故此处可以访问其私有成员
}
};
int main()
{
return 0;
}
使用友元时注意:
1、友元关系不能被继承。
2、友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
3、友元关系具有非传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。
注意:友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员,不建议使用!
5.4.3 为什么输出运算符的重载需要用friend修饰呢?
如果是重载双目操作符(即为类的成员函数),就只要设置一个参数作为右侧运算量,而左侧运算量就是对象本身。而 >> 或<< 左侧运算量是 cin或cout 而不是对象本身,所以不满足后面一点,就只能申明为友元函数了。。。
如果一定要声明为成员函数,只能成为如下的形式:
ostream & operator<<(ostream &output)
{
output << this->x << endl;
return output;
}
所以在运用这个<<运算符时就变为这种形式了:data< 5.5 动态多态 2、有的工程师认为真正的多态是动态多态 3、动态多态(多态)满足的三个条件 有继承关系 using namespace std; /* 5.7 虚函数 虚函数的定义: virtual 函数类型 函数名(形参列表); 虚函数的实现原理:虚函数的实现是由两个部分组成的,虚函数指针与虚函数表。 5.7.2 虚函数指针 只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数也都会对应一个虚函数指针。 5.7.3 虚函数表 每个包含了虚函数的类都包含一个虚表。当一个类(A)继承另一个类(B)时,类 A 会继承类 B 的函数。所以如果一个基类包含了虚函数,那么其派生类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。 我们来看以下的代码。类 A 包含虚函数vfunc1,vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。 类A的虚表如下图所示。 imagepng 5.7.4 虚表指针(虚函数表指针) 为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vfptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。 虚表指针存在于每一个被实例化的对象中,前提是该对象中有虚函数,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。 imagepng 4、动态绑定 说到这里,大家一定会好奇 C++ 是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。 类 A 是基类,类 B 继承类 A,类 C 又继承类 B。类 A,类 B,类 C,其对象模型如下图所示。 imagepng 由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类 A 的虚表(A vtbl),类 B 的虚表(B vtbl),类 C 的虚表(C vtbl)。类 A,类 B,类 C 的对象都拥有一个虚表指针,*__vfptr,用来指向自己所属类的虚表。 类 A 包括两个虚函数,故 A vtbl 包含两个指针,分别指向A::vfunc1()和A::vfunc2()。 类 B 继承于类 A,故类 B 可以调用类 A 的函数,但由于类 B 重写了B::vfunc1()函数,故 B vtbl 的两个指针分别指向B::vfunc1()和A::vfunc2()。 类 C 继承于类 B,故类 C 可以调用类 B 的函数,但由于类 C 重写了C::vfunc2()函数,故 C vtbl 的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。 非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。 5.7.5 思考:设计一个类的时候什么时候考虑将某个函数定义成虚函数,什么时候又不需要定义成虚函数呢,是不是可以吧基类中所有的成员函数都定义成虚函数呢? 5.7.6 虚表指针、虚函数的访问 /* 5.8 纯虚函数和抽象基类 2、定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。 3、虚函数的语法格式如下: virtual 返回值类型 函数名 (函数参数) = 0; 4、纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。 最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。 = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址 imagepng 5.8.2 抽象基类 2、抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。 3、思考:派生类是否一定需要实现基类中的纯虚函数? 如果我们希望将派生类也当作抽象类,那不一定要实现 如果我们希望将派生类当作普通类,那必须实现 3、思考:什么时候将一个类设计成抽象类? 当某个类只需要描述某一类事物的特征时 4、抽象类实例:设计一个Shape类可以计算各种形状的面积、周长 5.9 虚析构函数
1、动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
有虚函数
有基类指针指向派生类对象或者基类的引用变量引用了派生类对象
5.6 重载、重写(覆盖)、隐藏
#include class Hero
{
public:
virtual void huicheng()
{cout << "Base::huicheng" << endl;}
void func2()
{cout << "Base::func2" << endl;}
};
class Libai : public Hero
{
public:
void func2() //隐藏了基类中的func2函数
{cout << "A::func2" << endl;}
void func2(int x) //隐藏
{}
void huicheng() //对基类中虚函数的重写(override)
{cout << "Libai::huicheng" << endl;}
void huicheng(int x){} //隐藏
};
class Caocao : public Hero
{
public:
void func2() //隐藏了基类中的func2函数
{cout << "A::func2" << endl;}
void huicheng() //对基类中虚函数的重写(override)
{cout << "Caocao::huicheng" << endl;}
};
class Houyi : public Hero
{};
void goback(Hero &h)
{
h.huicheng(); //调用h所引用的派生类对象中重写的huicheng函数,因为huicheng函数在基类中是虚函数
h.func2(); //调用基类中的func2函数 因为func2函数在基类中不是虚函数
}
int main() {
Libai libai;
Caocao caocao;
Houyi houyi;
//libai回城
goback(libai);
goback(caocao);
goback(houyi);
#if 0
a.func1();
a.func2();
Base *p; // 基类指针
p = &a; // 基类指针指向派生类对象
p->func2(); // Base::func2
p->func1(); // A::func1, 调用的是指针p所指向的派生类中的func1函数
#endif
return 0;
}
5.7.1 虚函数的基本使用
C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数。#include
虚函数指针 (virtual function pointer, vptr) 从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。
存放虚函数指针的数组我们称之为虚函数表(virtual function table, vtbl)。class A {
public:
virtual void vfunc1(){};
virtual void vfunc2(){};
void func1(){};
void func2(){};
private:
int m_data1, m_data2;
};
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
void vfunc1(); //重写了vfunc1
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2(); //重写了vfunc2
void func2();
private:
int m_data1, m_data4;
};
#include
1、如果基类的某个行为,在所有的派生类中的表现都是一样的,说明在所有的派生类中都没有必要对基类中的行为进行重新实现,那么基类中的该行为/方法/函数可以设计成普通的成员函数,那么所有的派生类对象在调用的时候都是调用基类中的函数 2、可是如果将基类中的该函数设计成虚函数,那么所有的派生类对象在调用的时候都是调用基类中的函数(因为在所有的派生类中都没有重写该函数)。如此一来,从调用的结果来看这个函数被设计成虚函数和非虚函数没有区别了呀。。。 3、其实有区别!!!如果该函数时非虚函数,采用的静态联编,程序编译完后就能够确定函数的具体调用,可是如果设计成虚函数,采用的动态联编,该函数的调用需要在程序运行的过程中才能够被确定,后者的执行效率是要低于前者的执行效率。 4、所以如果我们在设计一个基类的时候图方便或者不负责任的将基类中所有的成员函数设计成虚函数的话,我们所开发出来的程序可能运行效率不高!!#include
*
1、虚函数表指针_vfptr是对象中的第一个元素,虚函数表指针的地址就是对象的首地址
因此:&_vfptr == &b == (int*)(&b) == 0x61fe00
2、_vfptr == (&_vfptr) == (int)(&b) == 0x4098c0
虚函数表指针的值/虚函数表的地址
3、(int)((int)(&b)),(int*)((int)(&b))+2,(int*)((int)(&b))+4 : 分别代表虚函数表中的三个元素地址
0x4098c0, 0x4098c8, 0x4098d0
4、(int)((int)(&b)),((int)((int)(&b))+2),((int)((int)(&b))+4) :分别代表虚函数表中的三个元素,因为虚函数表
是保存虚函数指针也就是虚函数的地址的,所以也分别代表了三个虚函数的地址: 0x407a60, 0x407a62,0x407a64
5、typedef void(*Fun)(void);pFun = (Fun)(*((int*)(*(int*)(&b))+0));
pFun(); //调用f()
pFun = (Fun)(*((int*)(*(int*)(&b))+2));
pFun(); //调用g()
pFun = (Fun)(*((int*)(*(int*)(&b))+4));
pFun(); //调用h()
imagepng
5.8.1 纯虚函数
1、一般来说,许多时候基类并不能确定函数的实现方法,只能确定函数的功能。但是函数调用的时候必须要用到该函数。这种情况下,C++提供了一种机制,成为纯虚函数,属于虚函数的一种,体现了面向对象的多态性。#include
1、包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。#include #include
虚析构函数是为了解决基类的指针指向派生类对象,通过基类的指针删除派生类对象。#include