1.多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
- 举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
- 再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。
2. 多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 如下图就是通过对Person或者Student对象的引用来调用虚函数
2.2虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
2.3虚函数的重写
情况1:
虚函数的重写(覆盖):
- 三同:函数名、参数、返回值相同(重写的条件)
- 也就是派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
// 基类
class Person
{
public:
// 基类的虚函数
virtual void BuyTicket()
{
cout << "Person--买票-全价" << endl; return this;
}
};
// 派生类
class Student : public Person
{
public:
// 派生类的虚函数
// 因为派生类的虚函数与基类的虚函数返回值类型、函数名字、参数列表完全相同,
// 因此可以说派生类的虚函数重写了基类的虚函数
virtual void BuyTicket()
{
cout << "Student--买票-半价" << endl; return this;
}
};
// 派生类
class Soldier: public Person
{
public:
// 派生类的虚函数
// 因为派生类的虚函数与基类的虚函数返回值类型、函数名字、参数列表完全相同,
// 因此可以说派生类的虚函数重写了基类的虚函数
virtual void BuyTicket()
{
cout << "Soldier--买票-优先" << endl; return this;
}
};
// 多态的条件:
// 1、虚函数重写
// 2、基类的指针或者引用去调用虚函数(也就是将派生类的对象传引用给基类的对象)
void Func(Person& p)
{
// 此处是基类引用p对基类对象或者派生类对象的引用
// 如果p引用的是基类对象,毫无疑问那么会调用基类对象的成员函数BuyTicket()
// 如果p引用的是派生类对象,那么派生类对象不仅有自己的成员函数BuyTicket(),还有继承的基类的成员函数BuyTicket(),这时基类和派生类各自的成员函数由于函数名、返回值、参数都相同,因此构成了重写,此时p调用的必然是派生类的BuyTicket()
p.BuyTicket();
}
int main()
{
Person pn;
Student st;
Soldier sd;
Func(pn);
Func(st);
Func(sd);
// 普通调用:跟调用对象类型有关
// 多态调用:指针/引用--指向的对象有关
// 当p指向基类则调用基类的虚函数,如果p指向派生类,则调用派生类的虚函数
// 所以打印结果为:
// Person--买票-全价
// Student--买票-半价
// Soldier--买票-优先
return 0;
}
情况2:
-
虚函数的重写/覆盖
-
三同:函数名、参数、返回值
1、派生类的虚函数可以不加virtual (建议:基类和派生类的虚函数都加上virtual )
2、协变:三同中,返回值可以不同,但是要求返回值必须是一个基类派生类关系的指针或者引用
// case1: 派生类虚函数可以不加virtual
class Person
{
public:
// 虚函数
virtual void* BuyTicket() { cout << "Person--买票-全价" << endl; return nullptr; }
};
class Student : public Person
{
public:
// 这里是派生类,加不加virtual都是可以的,效果是一样的
// virtual void* BuyTicket() { cout << "Student--买票-半价" << endl; return nullptr; }
void* BuyTicket() { cout << "Student--买票-半价" << endl; return nullptr; }
};
class Soldier : public Person
{
public:
// 这里是派生类,加不加virtual都是可以的,效果是一样的
// virtual void* BuyTicket() { cout << "Soldier--买票-优先" << endl; return nullptr; }
void* BuyTicket() { cout << "Soldier--买票-优先" << endl; return nullptr; }
};
// 多态的条件:
// 1、虚函数重写
// 2、父类的指针或者引用去调用虚函数
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pn;
Student st;
Soldier sd;
Func(pn);
Func(st);
Func(sd);
// 打印结果:
// Person--买票-全价
// Student--买票-半价
// Soldier--买票-优先
return 0;
}
- case2: 协变:三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用(任意父子类关系的指针或者引用都是可以的)
class Person
{
public:
// 虚函数
// 此处的返回值就不是void*,而是Person的指针,Person和Stuent或者Soldier都构成父子关系
virtual Person* BuyTicket() { cout << "Person--买票-全价" << endl; return this; }
};
class Student : public Person
{
public:
// 以下两种方式都是可以的
// virtual Person* BuyTicket() { cout << "Student--买票-半价" << endl; return this; }
virtual Student* BuyTicket() { cout << "Student--买票-半价" << endl; return this; }
};
class Soldier : public Person
{
public:
// 以下两种方式都是可以的
// virtual Student* BuyTicket() { cout << "Student--买票-半价" << endl; return this; }
virtual Soldier* BuyTicket() { cout << "Soldier--买票-优先" << endl; return this; }
};
// 多态的条件:
// 1、虚函数重写
// 2、父类的指针或者引用去调用虚函数
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pn;
Student st;
Soldier sd;
Func(pn);
Func(st);
Func(sd);
// 打印结果:
// Person--买票-全价
// Student--买票-半价
// Soldier--买票-优先
return 0;
}
============================================================================================================
// 返回值必须是一个父子类关系的指针或者引用(任意父子类关系的指针或者引用都是可以的)
// 只要是父子类关系的指针或者引用就行,返回值不是当前类的指针或者引用,都是可以的
class A
{};
class B : public A
{};
class Person
{
public:
// 虚函数
// A类和B类是父子类关系,A是基类,B是A的派生类
virtual A* BuyTicket() { cout << "Person--买票-全价" << endl; return this; }
};
class Student : public Person
{
public:
virtual B* BuyTicket() { cout << "Student--买票-半价" << endl; return this; }
};
class Soldier : public Person
{
public:
virtual B* BuyTicket() { cout << "Soldier--买票-优先" << endl; return this; }
};
// 多态的条件:
// 1、虚函数重写
// 2、父类的指针或者引用去调用虚函数
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pn;
Student st;
Soldier sd;
Func(pn);
Func(st);
Func(sd);
// 打印结果:
// Person--买票-全价
// Student--买票-半价
// Soldier--买票-优先
return 0;
}
情况3:析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这样就可以满足重写的三个条件了
- case1:基类的析构函数不是虚函数,会发生什么情况?
// 基类
class Person
{
public:
// 析构函数
// 因为基类的析构函数,没有用virtual修饰,所以目前基类的析构函数不是虚函数,与派生类的析构函数不构成重写
~Person()
{
cout << "Person delete:" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
// 派生类
class Student : public Person
{
public:
~Student()
{
cout << "Student delete:" << _s << endl;
delete[] _s;
}
protected:
int* _s = new int[20];
};
// 场景一:这是一个正确的使用场景,并不能够体现出析构函数不重写的弊端
void test1()
{
// 基类在出作用域之后,会调用自己的析构函数
Person p;
// 派生类在出作用域后,会先调用自己的析构函数,在派生类的析构函数调用之后,系统会默认调用基类的析构函数,来析构派生类中继承的基类的那部分资源
Student s;
// 打印结果为
// Student delete:01085260
// Person delete:010853F8
// Person delete:0108B1F0
}
// 场景二:错误的使用场景
void test2()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
// 此处的delete ptr1会有两个行为:
// 1.使用指针ptr1调用析构函数
// 2. operator delete(ptr1) 也就是c语言中free(ptr1)
// 对于普通调用:调用的函数和调用对象的类型有关
// 此时ptr1的类型为Person*,因此delete会调用Person的析构函数
// 再释放ptr1
// 但是对于ptr2,其类型也为Person*,因此delete会调用Person的析构函数
// 但是ptr2指向的对象为派生类,而派生类的资源并没有被析构,只是析构了派生类中继承的父类的那部分资源,这样就会造成内存泄漏
// 打印结果为:并没有调用派生类的析构函数
// Person delete:0146D7C0
// Person delete:01469E00
}
int main()
{
test1();
return 0;
}
- case2:基类的析构函数是虚函数,为多态调用
// 基类
class Person
{
public:
// 析构函数
// 基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写
virtual ~Person()
{
cout << "Person delete:" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
// 派生类
class Student : public Person
{
public:
// 派生类的析构函数
~Student()
{
cout << "Student delete:" << _s << endl;
delete[] _s;
}
protected:
int* _s = new int[20];
};
void test2()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
// 此时基类的析构函数是虚函数,因此析构函数也就满足了重写的条件
// 而我们也是通过基类的指针来调用的基类或者派生类对象,因此也就满足了多态的条件
// 而对于多态调用,delete ptr2,会调用派生类Stuent的析构函数(此时的析构函数的被重写了),这样就不会再造成内存泄漏了
// 注:派生类函数被调用之后,会自动调用对应的基类的析构函数
// 所以打印结果为
// Person delete:00D26618
// Student delete:00D24408
// Person delete:00D25260
}
int main()
{
test2();
return 0;
}
2.4 C++11 override 和 final
如何实现一个让一个类不能被其他类继承
- 构造私有 C++98
- 类定义时 加final C++11
// case1:构造私有
// 下面这种情况是A类是无法被B继承的,如果要创建B类型的对象,编译器会报错
class A
{
private:
// 私有的构造函数
A()
{}
};
class B : public A
{};
int main()
{
// 当我们创建B类对象bb时,我们需要调用B类对象的构造函数,但是由于A类的构造函数是自定义的,因此B类对象的构造函数需要调用A类对象的构造函数来完成B类继承A类的那一部分的初始化,但是又因为A类的构造函数是私有成员函数,虽然被B类继承了,但是在B类中是不可见的,也就是无法调用的,因此编译会报错
B bb;
return 0;
}
// case2:类定义时 加final
// 下面这种情况,基类A被final修饰,则直接编译就会报错(因为规定被final修饰的类不可以作为基类)
class A final
{
A()
{}
};
class B : public A
{};
int main()
{
return 0;
}
- 从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了
override
和final
两个关键字,可以帮助用户检测是否重写。
// 1.final:修饰虚函数,表示该虚函数不能再被重写
// 下面的代码进行编译,编译器会报错
class Car
{
public:
virtual void Drive() final // 此处使用了final修饰,因此其他类无法继承
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
Car a;
return 0;
}
// 2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
// override 是修饰派生类函数的
// 如果没有完全重写(也就是没有构成重写的条件),编译器就会报错
// 如下的代码,虚函数Drive()构成了重写的条件,因此,即使派生类的虚函数Drive()被override修饰,编译器也不会报错
class Car
{
public:
virtual void Drive(){}
};
class Benz :public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
Car a;
return 0;
}
2.5重载、覆盖(重写)、隐藏(重定义)的对比
重定义在继承这篇文章中有详细的介绍中有详细介绍
3. 抽象类
3.1 概念
在虚函数的后面加上 =0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
// 抽象类 -- 不能实例化出对象
class Car
{
public:
// 纯虚函数(在虚函数的后面写上 =0)
// 派生类必须对纯虚函数进行重写才可以实例化出对象
virtual void Drive(int val = 1) = 0;
};
class BMW : public Car
{
public:
// 派生类的虚函数与基类的纯虚函数,满足三同,完成了对基类纯虚函数的重写
virtual void Drive(int val = 0)
{
cout << val << endl
cout << "驾驶乐趣" << endl;
}
};
int main()
{
// Car c; // 抽象类不可以实例化出对象,因此编译时,编译器会报错
BMW b; // 类BMW 已经对纯虚函数进行了重写,因此此时是可以实例化出对象的
b.Drive();
return 0;
}
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
4.1虚函数表
首先我们先来看下面这道题:sizeof(Base)
是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
return 0;
}
通过观察测试我们发现b对象是8bytes
,除了_b成员,还多一个__vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析
针对上面的代码我们做出以下改造
- 我们增加一个派生类
Derive
去继承Base
Derive
中重写Func1
Base
再增加一个虚函数func1()
和一个普通函数func3()
// 基类
class Base
{
public:
// 虚函数
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
// 虚函数
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
// 普通函数
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
// 派生类
class Derive : public Base
{
public:
// 对基类中的Func1()虚函数完成了重写
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过观察和测试,我们发现了以下几点问题:
-
派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是基类继承下来的成员(包括虚表指针),另一部分是自己的成员。
-
基类b对象和派生类d对象虚表是不一样的,这里我们发现
Func1
完成了重写,所以d的虚表中存的是重写的Derive::Func1
(如下图所示),所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。 -
另外
Func2
继承下来后是虚函数,所以放进了虚表,Func3
也继承下来了,但是不是虚函数,所以不会放进虚表。 -
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个
nullptr
。 -
总结一下派生类的虚表生成:
a. 先将基类中的虚表内容拷贝一份到派生类虚表中
b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 -
这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
答:虚函数存在虚表,虚表存在对象中。这个回答是错的。但是很多初学者都是这样深以为然的。
注意:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。
验证虚函数表存放在代码段
// 基类
class Base
{
public:
// 基类的虚函数
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
// 基类的虚函数
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
// 普通函数
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
// 派生类
class Derive : public Base
{
public:
// 派生类的虚函数(且对基类的虚函数Func1()进行了重写)
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int* p1 = new int;
cout << "堆:" << p1 << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
// (void*)str 强转类型是为了将地址按%p来打印,否则cout会自动识别str的类型为char*
static int b = 0;
cout << "静态区/数据段:" << &b << endl;
// be是一个基类对象,里面有一个虚函数
Base be;
cout << "虚表:" << (void*)*((int*)&be) << endl;
// 将&be强转为int*,则对&be解引用(对int*解引用就只会拿到所指向对象的前四个字节),就只会解引用拿到&be指向的前4个字节,而这前四个字节存放的正好就是虚表的指针的地址
Base* ptr1 = &be;
int* ptr2 = (int*)ptr1;
printf("虚表:%p\n", *ptr2);
Derive de;
cout << "虚表:" << (void*)*((int*)&de) << endl;
// 通过监视窗口我们可以发现b1,b2用的是同一个虚表
Base b1;
Base b2;
return 0;
}
-
输出的结果为:
- 栈:
003AF914
- 堆:
0083A178
- 代码段/常量区:
00B2AB8C
- 静态区/数据段:
00B2D414
- 栈:
-
Base be 的虚表地址:
- 虚表:
00B2AB34
- 虚表:
00B2AB34
- 虚表:
-
Derive de 的虚表地址:
- 虚表:
00B2AB64
- 虚表:
通过对比我们发现虚表的地址与代码段的地址是十分接近的,因此我们可以判断出,虚表是存放在代码段的
4.2多态的原理
那么多态的原理到底是什么?还记得这里Func
函数传Person
调用的Person::BuyTicket
,传Student
调用的是Student::BuyTicket
// 基类
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
// 派生类
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
// 多态构成的条件
// 1.基类的指针或者引用调用虚函数
// 2.调用的必须是虚函数,并且派生类的虚函数是对基类虚函数的重写
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
-
观察下图的红色箭头我们看到,p是指向
mike
对象时,p->BuyTicket
在mike的虚表中找到虚函数是Person::BuyTicket
。 -
观察下图的蓝色箭头我们看到,p是指向
johnson
对象时,p->BuyTicket
在johson的虚表中找到虚函数是Student::BuyTicket
。 -
这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
-
反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?
通过如下的分析:满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的,因此只有使用指针才可以找到相应对象的虚函数表中的虚函数 -
再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
// 基类
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
// 派生类
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
// 以下汇编代码中去掉了无关的部分
void Func(Person* p)
{
p->BuyTicket();
/*
// 下面是虚函数表调用对应虚函数过程的汇编代码
// 如果p中存的是person类对象mike的指针,将p移动到寄存器eax中
001940DE mov eax, dword ptr[p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(头4个字节也就是虚表指针)移动到了寄存器edx中
001940E1 mov edx, dword ptr[eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节(也就是存的虚函数BuyTicket()指针,且这个虚函数就是Person::BuyTicket())移动到了eax
00B823EE mov eax, dword ptr[edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA call eax
001940EC cmp esi, esp
*/
/*
同理:
// 如果p中存的是派生类对象Jhonson的指针,将p移动到寄存器eax中
001940DE mov eax, dword ptr[p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(头4个字节也就是虚表指针)移动到了寄存器edx中
001940E1 mov edx, dword ptr[eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节(也就是存的虚函数BuyTicket()指针,且这个虚函数就是Student::BuyTicket(),因此此时的虚函数BuyTicket()已经被重写了)移动到了eax
00B823EE mov eax, dword ptr[edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA call eax
001940EC cmp esi, esp
*/
}
int main()
{
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,
// 所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
Person mike;
mike.BuyTicket();
/*
// 调用的汇编代码如下:
00195182 lea ecx, [mike]
00195185 call Person::BuyTicket(01914F6h)
...
*/
}
4.3 动态绑定与静态绑定
-
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
-
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
// 基类
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
// 派生类
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
Derive d;
// 普通调用 -- 编译时/静态 绑定
// func3()不是虚函数,因此不构成重写的条件,那么也就不构成多态的条件
// 所以调用func3()是普通调用
// 普通调用,是根据指针ptr的类型来找到对应类中的函数来调用
// 因此ptr调用都是Base类的成员函数Func3()
Base* ptr = &b;
ptr->Func3();
ptr = &d;
ptr->Func3();
// 多态调用 -- 运行时/动态 绑定
// 调用Func1(),Func1()满足多态的条件
// 条件1:Func1(),Func2()都是虚函数,满足重写的条件
// 条件2:ptr的类型为Base*,是基类指针
ptr = &b;
// 因此,此处ptr调用的Func1()是对象b的Func1()函数
ptr->Func1();
ptr = &d;
// 因此,此处ptr调用的Func1()是对象d的Func1()函数,且d对象的Func1()函数已经完成了重写,为Deriver::Func1()
ptr->Func1();
return 0;
}
// 输出的结果如下:
12
Base::Func3()
Base::Func3()
Base::Func1()
Derive::Func1()
5.单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
// 基类
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
// 派生类
class Derive :public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
/*
观察下图中的监视窗口中我们发现看不见func3。这里是编译器的监视窗口故意隐藏了这个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。
*/
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
// 将函数指针的类型 void(*)() 重定义为 VFptr,需要注意VFptr的位置
typedef void(*VFPtr)();
// vs编译器下,虚表是以nullptr来结尾的
void PrintVFTbale(VFPtr vft[])
{
for (int i = 0; vft[i] != nullptr; ++i)
{
printf("[%d]:%p->", i, vft[i]);
// vft[i] 是对应函数的地址,vft[i]()是调用这个函数
vft[i]();
}
cout << endl;
}
int main()
{
Base b;
// 以下两种方法都是可以的,我们的目的是拿到指针&b指向的前4/8个字节
// 拿到的就是虚表指针,虚表指针指向一个数组,这个数组中存放的是虚表函数
// (32位下拿到4个字节,64位下拿到8个字节,这4/8个字节是虚表的指针)
// 将&b的指针强转为int*,我们解引用就可以拿到指针指向的前4个字节
// 将&b的指针强转为void**(也就是强转为二级指针,解引用之后,就可以拿到void*类型的元素,同理int**也是一样的),我们解引用就可以拿到指针指向的前4/8个字节
// (32位下,void*是4个字节,解引用拿到4个字节,64位下void*是8个字节,解引用拿到8个字节)
// 两种方法:
// PrintVFTbale((VFPtr*)(*(int*)&b));
// PrintVFTbale((VFPtr*)(*(void**)&b));
// 因为需要将解引用(*(int*)&d)得到的地址传递给函数PrintVFTbale
// 因此需要将地址的类型强转为函数PrintVFTbale的函数类型 VFPtr*
Derive d;
PrintVFTbale((VFPtr*)(*(int*)&d));
return 0;
}
// 打印结果为:
/*
[0]:0052129E->Base::func1
[1]:00521113->Base::func2
[0]:00521235->Derive::func1
[1]:00521113->Base::func2
[2]:00521221->Derive::func3
*/
// 注:需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
5.2 多继承中的虚函数表
// 基类
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
// 基类
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
// 派生类
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
// 函数指针数组
typedef void(*VFPtr)();
void PrintVFTbale(VFPtr vft[])
{
for (int i = 0; vft[i] != nullptr; ++i)
{
printf("[%d]:%p->", i, vft[i]);
vft[i]();
}
cout << endl;
}
// 多继承中,派生类对于不同的基类会生成各自的虚表
// 上述有两个基类Base1和Base2,因此就会生成两张虚表
int main()
{
// Base1 和 Base2 都有虚函数,因此都有一个虚表
//(如果Base1/Base2没有虚函数,则不存在虚表)
Base1 b1;
Base2 b2;
PrintVFTbale((VFPtr*)(*(void**)&b1));
PrintVFTbale((VFPtr*)(*(void**)&b2));
Derive d;
// 打印第一张虚表,也就是Base1的虚表
// 其中func1满足重写的条件
PrintVFTbale((VFPtr*)(*(void**)&d));
// 打印第二张虚表 (方法一)
// 因为 *(void**)&d) 解引用拿到的地址是第一张虚表的地址
// 因此我们需要跳过 sizeof(Base1) 大小的字节,拿到第二张虚表的地址
// 又因为&d的类型为Derive*,那么指针加1,是跳过sizeof(Derive)个地址
// 所以我们需要将&d强转为char*
//PrintVFTbale((VFPtr*)(*(void**)((char*)&d+sizeof(Base1))));
// 打印第二张虚表 (方法二)
// 将指针&d进行切片赋值给ptr2(类型为Base2),
// 此时我们再进行解引用,则指针会自动跳转到第二张虚表的位置
// 此时指针指向的是第二张虚表的地址,解引用拿到虚表的地址
Base2* ptr2 = &d;
PrintVFTbale((VFPtr*)(*(void**)ptr2));
return 0;
}
// 打印结果为:
/*
// Base1的虚表函数
[0]:007D11B3->Base1::func1
[1]:007D12DF->Base1::func2
// Base2的虚表函数
[0]:007D13B6->Base2::func1
[1]:007D10B9->Base2::func2
// Derive的第一张虚表
// 我们可以通过打印结果判断出,Derive中没有被重写的虚函数会放入到第一张虚表中
[0]:007D123F->Derive::func1
[1]:007D12DF->Base1::func2
[2]:007D122B->Derive::func3
// Derive的第二张虚表
[0]:007D134D->Derive::func1
[1]:007D10B9->Base2::func2
*/
- 图2:清晰的展示了我们上述所说的内容
5.3. 菱形继承、菱形虚拟继承
实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表本文就不赘述了。
6 继承和多态常见的问题
6.1概念查考
1-7
-
下面哪种面向对象的方法可以让你变得富有(A)
A: 继承 B: 封装 C: 多态 D: 抽象 -
(D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定 -
面向对象设计中的继承和组合,下面说法错误的是?(C)
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现 -
以下关于纯虚函数的说法,正确的是(A)
A:声明纯虚函数的类不能实例化对象
B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数 -
关于虚函数的描述正确的是(B)
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数 -
关于虚表说法正确的是(D )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表 -
假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(D )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
- 参考答案
- A 2. D 3. C 4. A 5. B 6. D 7. D
8 下面代码的打印结果是什么
#include<iostream>
using namespace std;
// 基类
class A
{
public:
// 构造函数
A(const char* s)
{
cout << s << endl;
}
~A() {}
};
// 派生类
class B :virtual public A
{
public:
B(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
// A的派生类
class C :virtual public A
{
public:
C(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
// B和C的派生类
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4)
:C(s1, s3)
,B(s1, s2)
,A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
// 选项:
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D
// 正确选项为A
// 分析:
// 注:初始化列表初始化成员变量:是按照成员变量的声明顺序来进行初始化的,而不是在初始化列表中的顺序
// 注:初始化列表初始化继承的类:是按照继承的顺序来初始化对应的类对象,而不是按照初始化列表中的顺序
// 因此:
// 构造D对象,编译器会先走构造函数的初始化列表,初始化列表中的继承类进行初始化,不是按照它们在初始化列表中的顺序,而是它们继承的顺序,所以会先构造A,再依次构造B、C、D,因为是虚拟继承,所以再构造B、C时,调用的A的构造,其实已经构造过了,因为它们共享同最先构造的A
- 如果上面的题目不是菱形虚拟继承,那么其结果是什么呢?
#include<iostream>
using namespace std;
class A
{
public:
A(const char* s)
{
cout << s << endl;
}
~A() {}
};
class B : public A
{
public:
B(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class C : public A
{
public:
C(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class D :public B, public C
{
public:
// 如果不是菱形虚拟继承,那么初始化列表就不可以初始化A,因为初始化B、C时,它们各自都会调用A的构造函数
// A此时不是D的基类,也不是D的成员,是不可以走初始化列表的
D(const char* s1, const char* s2, const char* s3, const char* s4)
:C(s1, s3)
,B(s1, s2)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
// 此时输出的结果为:
/*
class A
class B
class A
class C
class D
*/
9.
多继承中指针偏移问题?下面说法正确的是(正确选项为C)
// 基类
class Base1
{
public: int _b1;
};
// 基类
class Base2
{
public: int _b2;
};
// 派生类
class Derive : public Base1, public Base2
{
public: int _d;
};
int main()
{
Derive d;
// p1的类型为Base1*, 派生类给基类赋值,会进行切片, p1指向的是d对象中Base1类的那部分
Base1* p1 = &d;
// p2的类型为Base2*, 派生类类给基类赋值,会进行切片, p2指向的是d对象中Base2类的那部分
Base2* p2 = &d;
// p3的类型为Derive* ,与d为同类型的对象,因此不会进行切片,p3指向整个d对象
Derive* p3 = &d;
return 0;
}
// d对象模型中对应的Base1和Base2的顺序是根据继承顺序来判定的(因为初始化Base1和Base2是按照继承顺序的)
// 由下图可知,p1和p3的位置相同,但是p1仅仅指向Base1,而p3是指向整个Derive对象
// A:p1 == p2 == p3
// B:p1 < p2 < p3
// C:p1 == p3 != p2
// D:p1 != p2 != p3
10.
// 以下程序输出结果是什么(正确选项为B)
// 基类
class A
{
public:
// 虚函数
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
// 虚函数
// 派生类并没有对test函数进行重写,因此Base::test(),就算呗派生类继承,test()也属于基类
virtual void test() { func(); }
};
// 派生类
class B : public A
{
public:
// 此处如果是被多态调用,会继承基类虚函数的接口,即继承virtual void func(int val = 1)
// 则派生类的虚函数变为了
// virtual void func(int val = 1) { std::cout << "B->" << val << std::endl; }
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
// 首先我们需要明白,func()是虚函数的重写(参数是一样的,只是缺省值不一样,并不影响构成重写),test()不是虚函数的重写
// 此时会调用test(),然后test()再调用func(),此时我们是通过this指针来调用func()的,也就是this->func,只不过一般this不会显示的写出来,这时this的类型为A*,也就是this的类型是基类的指针 ,且又因为func()是虚函数的重写,因此构成多态调用的条件的,虽然此时this的类型为A*,但是this指向的对象确实B类对象,因此this->func()访问的成员函数其实是B类的func(),又因为虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,所以此时val的值为1.
p->func();
//此时p的类型为B*,并不是基类的指针,因此并不构成多态,因此p调用的就是派生类B的func()函数,并不会继承基类虚函数的接口,因此打印结果为B->0
A* pt = new B;
pt->func();
// 此时A*是基类的指针,且虚函数构成重写,因此构成多态,pt指向的是B类对象的func()函数,又因为继承了A类虚函数的接口,所以打印结果为B->1
return 0;
}
// 选项
// A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
6.2 问答题
-
什么是多态?
答:参考多态博客 -
什么是重载、重写(覆盖)、重定义(隐藏)?
答:参考多态博客 -
多态的实现原理?
答:参考本节课件内容 -
inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
多态就没有inline属性了,普通调用继续可以保持inline属性
-
静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 -
构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 -
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容 -
对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 -
虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的(在构造函数列表中初始化【虚表指针】),一般情况下存在代码段(常量区)的。 -
C++菱形继承的问题?虚继承的原理?
答:参考继承课件。注意这里不要把虚函数表和虚基表搞混了。 -
什么是抽象类?抽象类的作用?
答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。