第五章 多态
5.1 多态的引入
思考:在之前实现的英雄模型中,假如实现某个接口可以传入一个英雄,在该接口中可以对英雄的力量、敏捷和智力进行加强,请问该接口的参数该如何设计?
以上解决办法利用了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、可以重载的运算符
5、不可重载的运算符:
-
成员访问运算符(.)
-
成员指针访问运算符(->)
-
域运算符(::)
-
长度运算符(sizeof)
-
条件运算符(: ?)
-
预处理符号(#)
5.3.2 运算符重载实例
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
using namespace std;
class Test
{
public:
int data;
char *ptr;
// 默认构造函数
Test()
{
std::cout << "Test() " << std::endl;
}
// 带参数的构造函数
Test(int data, const char *src)
{
this->data = data;
if (src)
{
std::cout << "struct test(data)" << std::endl;
this->ptr = new char[std::strlen(src) + 1];
std::strcpy(this->ptr, src);
}
else
{
ptr = new char[10];
}
}
//拷贝构造函数,调用时间:使用一个构造好的对象初始化一个新的对象
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];
}
//该运算符重载函数由 左操作数调用,右操作数当做实参传递给该函数,触发:t1 + t3 -> t1.operator + (t3)
Test operator + (const Test &t) //operator 操作符
{
cout << "Test operator + (Test &t)" << endl;
Test val;
val.data = this->data + t.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)
{
cout << "bool operator > (const Test &t)" << endl;
if (strcmp(this->ptr, t.ptr) > 0)
return true;
return false;
}
char operator [](int index)
{
cout << "char operator [](int index)" << endl;
if (index < 0 || index >= strlen(ptr))
return '\0';
return ptr[index];
}
Test &operator = (const Test &t)
{
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++()
{
cout << "Test &operator ++(int)" << endl;
++data;
return *this;
}
//后置++
Test operator++(int)
{
cout << "Test operator ++(int)" << endl;
Test tmp = *this;
data++;
return tmp;
}
//<<重载
friend ostream &operator << (ostream &os, const Test &t)
{
os << t.data << endl;
os << t.ptr << endl;
return os;
}
// 析构函数
~Test()
{
std::cout << "~Test() delete test()" << std::endl;
if (ptr)
delete[] ptr;
}
};
int main() {
#if 0
// + 运算符重载
Test t1(10, "hello");
Test t2 = t1;
cout << t2.data << endl;
cout << t2.ptr << endl;
Test t3(23, "world");
cout << "********** input t3" << endl;
Test t4 = t1 + t3;
cout << t4.data << endl;
cout << t4.ptr << endl;
#endif
#if 0
// > 运算符的重载
Test t1(10, "hello");
Test t2(20, "hello");
if (t1 > t2)
cout << "t1 > t2" << endl;
else
cout << "t1 < t2" << endl;
// [] 运算符的重载
cout << "t1.ptr:" << t1.ptr << endl;
cout << "t1[4]: " << t1[4] << endl;
// = 符号的重载
t1 = t2;
cout << "-------------- t1.ptr: " << t1.ptr << endl;
printf("t1.ptr: %p, t2.ptr: %p\n", t1.ptr, t2.ptr); //地址
printf("t1.ptr: %s, t2.ptr: %s\n", t1.ptr, t2.ptr);
++t1;
#endif
#if 0
//前置++的重载
Test t1(10, "hello");
Test t2;
t2 = ++t1;
cout << "t2.data: " << t2.data << endl;
cout << "t1.data: " << t1.data << endl;
cout << "t1.ptr: " << t1.ptr << endl;
//后置++的重载
//Test t3;
//t3 = t1++;
//cout << t3.data << endl;
//cout << t1.data << endl;
string s("hello");
cout << "s: " << s << endl;
cout << "t1: " << t1 << endl;
#endif
Test t1(10, "hello");
cout << "++++++++++++++ t1: " << t1 << endl;
return 0;
}
5.3.3 前置++和后置++
class A
{
private:
int a;
public:
A& operator++()
{
++a;
return *this;
}
A operator++(int)
{
A a = *this;
++*this;
return a;
}
};
因为后置++在实现的时候构造了一个临时对象,临时对象的构造和销毁都需要消耗一定的系统资源,所以后置++的效率比前置++的效率低。
5.4 友元
5.4.1 友元函数
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数);
友元函数 的使用:
1、友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
2、一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
3、友元函数的调用与一般函数的调用方式和原理一致。
4、友元函数中没有this指针
5、两个类要共享数据的时候可以使用友元函数,比如:类A中的函数需要访问类B中的成员,那
么类A中该函数要是类B的友元函数。
6、运算符重载的某些场合需要使用友元函数,例如 << 的重载
#include <iostream>
using namespace std;
class CCar;
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是关键字,类名必须是程序中的一个已定义过的类。
#include <iostream>
using namespace std;
class CCar
{
private:
int price;
friend class CDriver;
};
class CDriver
{
public:
CCar myCar;
void ModifyCar()
{
myCar.price += 1000;
}
};
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;
}
5.5 动态多态
1、动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
2、有的工程师认为真正的多态是动态多态
3、动态多态(多态)满足的三个条件
-
有继承关系
-
有虚函数
-
有基类指针指向派生类对象或者基类的引用变量引用了派生类对象
5.6 重载、重写(覆盖)、隐藏
- 重载:overload 重载只发生在同一个作用域中,比如 一个类中的多个成员函数函数名相同,但是形参数据类型或或者个数或者顺序不相同,那么我们就称这些函数是重载。
- 重写:override 也叫做覆盖 重写发生在不同的作用域中(发生在基类和派生类中),而且派生类中的成员函数的名字和基类中的虚函数的名字相同,并且返回值相同,形参列表也相同!!!
- 隐藏:隐藏发生在不同的作用域中(发生在基类和派生类中),派生类中的成员函数的名字、返回值、形参列表与基类中的普通函数完全相同, 派生类中的成员函数的名字和基类中的成员函数的名字。
- 相同,但是形参列表不同或者返回值不同这也叫做隐藏,此时基类中的那个函数不论是普通函数还是虚函数都会被派生类中的函数隐藏。
#include <iostream>
using namespace std;
class Hero
{
public:
virtual void huicheng()
{cout << "Base::huicheng" << endl;}
void func2()
{cout << "Base::func2" << endl;}
};
class Libai : public Hero
{
public:
void func2()
{cout << "A::func2" << endl;}
void func2(int x)
{}
void huicheng()
{cout << "Libai::huicheng" << endl;}
void huicheng(int x){}
};
class Caocao : public Hero
{
public:
void func2()
{cout << "A::func2" << endl;}
void huicheng()
{cout << "Caocao::huicheng" << endl;}
};
class Houyi : public Hero
{};
void goback(Hero &h)
{
h.huicheng();
h.func2();
}
int main() {
Libai libai;
Caocao caocao;
Houyi houyi;
goback(libai);
goback(caocao);
goback(houyi);
cout << " +++++++++++++++++++ output caocao a:" << endl;
Caocao a;
a.func2();
a.huicheng();
cout << " +++++++++++++++++++ output hero *p:" << endl;
Hero *p;
p = &a;
p->func2();
p->huicheng(); //指向派生类
return 0;
}
5.7 虚函数
5.7.1 虚函数的基本使用
C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数。
虚函数的定义:
virtual 函数类型 函数名(形参列表);
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func()
{cout << "Base func: " << endl;}
virtual void func2()
{cout << "Base func2: " << endl;}
};
class Derived:public Base
{
public:
void func()
{cout << "Derived func: " << endl;}
};
int main() {
Derived d;
d.func();
return 0;
}
虚函数的实现原理:虚函数的实现是由两个部分组成的,虚函数指针与虚函数表。
5.7.2 虚函数指针
虚函数指针 (virtual function pointer, vptr) 从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。
只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数也都会对应一个虚函数指针。
5.7.3 虚函数表
存放虚函数指针的数组我们称之为 虚函数表(virtual function table, vtbl)。
每个包含了虚函数的类都包含一个虚表。当一个类(A)继承另一个类(B)时,类 A 会继承类 B 的函数。所以如果一个基类包含了虚函数,那么其派生类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
我们来看以下的代码。类 A 包含虚函数vfunc1,vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。
class A{
public:
virtual void vfunc1(){};
virtual void vfunc2(){};
void func1(){};
void func2(){};
private:
int m_data1, m_data2;
};
5.7.4 虚表指针(虚函数表指针)
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vfptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
虚表指针存在于每一个被实例化的对象中,前提是该对象中有虚函数,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。
动态绑定, C++ 是如何利用虚表和虚表指针来实现动态绑定的。
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();
void func1();
private:
int m_data3;
};
class C: public B
{
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
类 A 是基类,类 B 继承类 A,类 C 又继承类 B。类 A,类 B,类 C。
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类 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()。
非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
#include <iostream>
using namespace std;
class A{
public:
virtual void vfunc1()
{cout << "A::virtual void vfunc1()" << endl;};
virtual void vfunc2()
{cout << "A::virtual void vfunc2()" << endl;};
void func1()
{cout << "A::void func1()" << endl;};
void func2()
{cout << "A::void func2()" << endl;};
};
class B : public A
{
public:
void vfunc1()
{{cout << "B::virtual void vfunc1()" << endl;}};
void func1()
{{cout << "B::void func1()" << endl;}};
};
int main() {
B b;
b.vfunc1();
b.vfunc2();
b.func1();
b.func2();
A *p;
p = &b;
p->vfunc1();
p->vfunc2();
p->func1();
p->func2();
return 0;
}
5.7.6 虚表指针、虚函数的访问
#include <iostream>
#include <Cstdio>
using namespace std;
typedef void(*Fun)(void);
class Base {
public:
int x;
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
int main() {
Base b;
Fun pFun = NULL;
//虚函数表指针是对象中的第一个元素,虚函数表指针的地址就是对象的首地址
printf("%p\n",(int*)(&b)); // &_vfptr
//(*(int*)(&b)) : 虚函数表指针的值/虚函数表的地址
printf("%p\n", (int*)(*(int*)(&b))); //_vfptr
printf("%p\n", (int*)(*(int*)(&b))+2);
printf("%p\n", (int*)(*(int*)(&b))+4);
printf("%p\n", *(int*)(*(int*)(&b)));
printf("%p\n", *(int*)(*(int*)(&b))+2);
printf("%p\n", *(int*)(*(int*)(&b))+4);
pFun = (Fun)(*((int*)(*(int*)(&b))+4));
pFun();
}
5.8 纯虚函数和抽象基类
5.8.1 纯虚函数
1、一般来说,许多时候基类并不能确定函数的实现方法,只能确定函数的功能。但是函数调用的时候必须要用到该函数。这种情况下,C++提供了一种机制,成为纯虚函数,属于虚函数的一种,体现了面向对象的多态性。
2、定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
3、虚函数的语法格式如下:
virtual 返回值类型 函数名 (函数参数) = 0;
4、纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
= 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址
#include <iostream>
using namespace std;
//游戏中所有英雄的基类
class Hero
{
public:
virtual void huicheng() = 0;//纯虚函数 英雄的回城功能
};
class Libai:public Hero
{
public:
void huicheng() //重写
{
cout << "Libai::huicheng" << endl;
}
};
class Caocao:public Hero
{
public:
void huicheng()
{
cout << "Caocao::huicheng" << endl;
}
};
int main()
{
Libai h1;
Caocao h2;
h1.huicheng();
h2.huicheng();
Hero *p;
p = &h1;
p->huicheng();
p = &h2;
p->huicheng();
return 0;
}
5.8.2 抽象基类
1、包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
2、抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
#include <iostream>
using namespace std;
//游戏中所有英雄的基类
//抽象类:规定了某一类事物的特征
class Hero
{
public:
virtual void huicheng() = 0;//纯虚函数 英雄的回城功能
virtual void attack() = 0;
};
//假设这是对Libai类进行声明 (libai.h)
class Libai:public Hero
{
public:
void huicheng(); //对派生类中重写基类中的纯虚函数的声明
void attack();
};
//对Libai类的实现 (libai.cpp)
void Libai::huicheng() {
cout << "Libai::huicheng" << endl;
}
void Libai::attack() {
}
class Caocao:public Hero
{
public:
void huicheng()
{
cout << "Caocao::huicheng" << endl;
}
};
int main()
{
#if 0
Libai h1;
Caocao h2;
h1.huicheng();
h2.huicheng();
Hero *p;
p = &h1;
p->huicheng();
p = &h2;
p->huicheng();
//Hero h; //抽象类不能实例化对象
#endif
return 0;
}
抽象类实例:设计一个Shape类可以计算各种形状的面积、周长文章来源:https://www.toymoban.com/news/detail-618607.html
#include <iostream>
using namespace std;
typedef unsigned int u32_t;
class Shape
{
public:
virtual double getPermiter() = 0;
virtual double getArea() = 0;
};
class Trangle : public Shape
{
public:
Trangle():_a(10), _b(10), _c(10)
{}
Trangle(u32_t a, u32_t b, u32_t c):_a(a), _b(b), _c(c)
{}
double getPermiter()
{return double(_a + _b + _c);}
double getArea()
{return 10000;}
private:
u32_t _a, _b, _c;
u32_t _permiter;
double _area;
};
class Circle : public Shape
{
private:
double _r;
double _permiter;
double _area;
static double pi;
public:
Circle():_r(10)
{}
double getPermiter();
double getArea();
};
double Circle::pi = 3.14;
double Circle::getPermiter() {
return 2 * pi * _r;
}
double Circle::getArea() {
return pi * _r * _r;
}
int main() {
Trangle x;
cout << x.getPermiter() << endl;
Circle x2;
cout << x2.getPermiter() << endl;
cout << x2.getArea() << endl;
Shape *shape[2];
shape[0] = &x;
shape[1] = &x2;
for (int i = 0; i < 2; i++)
{
cout << shape[i]->getArea() << endl;
cout << shape[i]->getPermiter() << endl;
}
return 0;
}
5.9 虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,通过基类的指针删除派生类对象。文章来源地址https://www.toymoban.com/news/detail-618607.html
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func()
{
cout << "Base func" << endl;
}
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derived:public Base
{
public:
void func()
{
cout << "Derived func" << endl;
cout << "访问基类的虚函数: ";
Base::func();
}
~Derived()
{
cout << "~Derived" << endl;
}
};
int main(void)
{
Base *p = new Derived;
delete p;
return 0;
}
到了这里,关于16- C++多态-4 (C++)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!