C++继承与多态—深入掌握OOP语言最强大的机制


课程总目录



一、继承的本质和访问限定表

继承的本质:代码的复用(还有一点,在七、详解多态的应用原理中会讲会讲)

类和类之间的关系:

  • 组合(a kind of…):一部分的关系
  • 继承(a part of…):一种的关系

来看一段简单的代码:

class A
{
public:
	int ma;
protected:
	int mb;
private:
	int mc;
};

class B : public A
{
public:
	int md;
protected:
	int me;
private:
	int mf;
};

A:基类,B:派生类
A派生了B,B从A继承而来

分析:
在这里插入图片描述

派生类中可以有和基类一样的成员名,因为有作用域的限制,加一个ma,成员名不会冲突,此时派生类内存是28字节

在上面的代码class B : public A中,采用了public共有继承方式

三种继承方式publicprotectedprivate

public继承方式

基类的访问限定派生类中的访问限定外部的访问限定(main)
publicpublic可以访问
protectedprotected不能访问
private不可见的不能访问

protected继承方式

基类的访问限定派生类中的访问限定外部的访问限定(main)
publicprotected不能访问
protectedprotected不能访问
private不可见的不能访问

private继承方式

基类的访问限定派生类中的访问限定外部的访问限定(main)
publicprivate不能访问
protectedprivate不能访问
private不可见的不能访问

注意点:

  • 派生类中的访问限定不能大于继承方式
  • 外部只能访问对象的public成员,protectedprivate成员无法直接访问
  • 只有自己或友元能访问私有的成员
  • 继承可以继承下来private成员,占用派生类对象内存,但无法访问
  • protectedprivate的区别:在基类中定义的成员,想被派生类访问,但是不想被外部访问,那么在基类中,就把相关成员定义为protected;如果在派生类和外部都不打算访问,那么在基类中就把相关成员定义成private

如果我们不写继承方式的话,那么它会以什么方式继承呢?

  • 具体情况具体研究:要看派生类是用class定义的还是struct定义的
    class定义派生类(class B : A),默认继承方式是private
    struct定义派生类(struct B : A),默认继承方式是public
class A
{
public:
	int ma;
};

class B : private A { };

class C : public B { };

问题:在C中ma的访问限定是什么?

:在B中ma被继承为B的private了,则在C中ma是不可见的(能继承来,访问不了)

二、派生类的构造过程

class Base
{
public:
	Base(int data) : ma(data) { cout << "Base()构造" << endl; }
	~Base() { cout << "~Base()析构" << endl; }
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) : Base(data), mb(data) { cout << "Derive()构造" << endl; }
	~Derive() { cout << "~Derive()析构" << endl; }

private:
	int mb;
};

int main()
{
	Derive d(20);
	return 0;
}

运行结果:

Base()构造
Derive()构造
~Derive()析构
~Base()析构

基类、派生类:

  1. 派生类可以从基类继承来所有的成员(成员变量和成员方法),除构造函数和析构函数
  2. 初始化从基类继承来的成员变量:通过调用基类相应的构造函数来初始化
  3. 派生类的构造和析构函数负责初始化和清理派生类部分
  4. 派生类从基类继承来的成员的初始化和清理由基类的构造和析构函数来负责

派生类对象构造和析构过程:

  1. 派生类调用基类的构造函数,初始化从基类继承来的成员
  2. 调用派生类自己的构造函数,初始化派生类自己特有的成员
  3. 派生类对象的作用域到期,先调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存、文件)
  4. 调用基类的析构函数,释放派生类内存中从基类继承来的成员可能占用的外部资源(堆内存、文件)

三、重载、隐藏、覆盖、继承中的上下类型转换

继承结构中,名字相同的成员会产生关系,基类派生类里面相关的成员方法我们经常使用三种关系来描述它们

即:重载、隐藏、覆盖

重载:一组函数要重载,必须处在同一个作用域当中;而且函数名字相同,参数列表不同

隐藏(作用域的隐藏):在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏掉了,即作用域的隐藏

覆盖:基类和派生类方法的返回值,函数名以及参数列表都相同,而且基类的方法是虚函数。那么派生类的方法自动处理成虚函数,它们之间称为覆盖关系,在下节虚函数中会详细展开讲解

我们这节重点讲隐藏

class Base
{
public:
	Base(int data) :ma(data) {}
	void show() { cout << "Base::show()" << endl; } 		//1
	void show(int) { cout << "Base::show(int)" << endl; }	//2
	// 1和2是函数重载关系
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) {}
private:
	int mb;
};

int main()
{
	Derive d(20);
	d.show();	//Base::show()
	d.show(10); //Base::show(int)

	return 0;
}

接下来给派生类添加show方法

class Base
{
public:
	Base(int data) :ma(data) {}
	void show() { cout << "Base::show()" << endl; } 		//1
	void show(int) { cout << "Base::show(int)" << endl; }	//2
	// 1和2是函数重载关系
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }	//3
	//派生类的同名成员,把基类的同名成员给隐藏掉了,即3把1和2隐藏了
private:
	int mb;
};

int main()
{
	Derive d(20);
	d.show();	//Derive::show()
	d.show(10); //报错:函数不接受1个参数

	return 0;
}

派生类的同名成员,会把基类的同名成员隐藏,所以这里d.show(10);是不会调用基类中的show(int)方法的

如果想调用基类成员函数,必须指明基类的作用域来调用:

Derive d(20);
d.Base::show();		//Base::show()
d.Base::show(10);	//Base::show(int)

接下来,看继承中的上下类型转换

继承结构,也称为 从上(基类)到下(派生类) 的结构。

基类对象b ← \leftarrow 派生类对象d:类型从下到上的转换(Yes)

Base b(10);
Derive d(20);
b = d;

派生类对象d ← \leftarrow 基类对象b:类型从上到下的转换(No)

d = b;	//不行

基类指针(引用) ← \leftarrow 派生类对象:类型从下到上的转换(Yes)

Base* pb = &d;

但此时指针的类型限制了指针解引用的能力,Base类型的指针只能访问派生类中基类部分的成员

也就是:

pb->show();		//Base::show()
pb->show(10);	//Base::show(int)

此时就访问不了派生类自己的show方法了,可以通过((Derive*)pb)->show();访问,但比较危险

派生类指针(引用) ← \leftarrow 基类对象:类型从上到下的转换(No)

Derive* pd = &b; //报错:无法从 “Base*” 转换为 “Derive *” 

因为指针的类型决定了指针解引用的能力,Derive类型的指针指向的内存会比Base类型的指针多出一部分,会造成内存的非法访问

结论:在继承结构中进行上下的类型转换,默认只支持从下到上的类型的转换。除非进行强转,但强转不安全会涉及内存的非法访问

四、虚函数、静态绑定和动态绑定

虚函数:在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数

静态绑定:编译时期的函数的调用,绑定的是对普通函数的调用

动态绑定:运行时期的函数的调用,绑定的是对虚函数的调用

案例1:静态绑定

class Base
{
public:
	Base(int data) :ma(data) {}
	void show() { cout << "Base::show()" << endl; }
	void show(int) { cout << "Base::show(int)" << endl; }
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) : Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};

int main()
{
	Derive d(50);
	Base* pb = &d;
	//静态(编译时期)的绑定(函数的调用) call指令
	pb->show();
	pb->show(10);

	cout << sizeof(Base) << endl;
	cout << sizeof(Derive) << endl;

	cout << typeid(pb).name() << endl;
	cout << typeid(*pb).name() << endl;

	return 0;
}

运行结果:

Base::show()
Base::show(int)
4
8
class Base *
class Base

反汇编:

在这里插入图片描述
编译期间将高级源代码编译为汇编码,指定了call 指令,即编译期间就指定了函数的调用,这就是静态绑定

案例2:动态绑定

向基类Base中的成员方法添加virtual关键字

class Base
{
public:
	Base(int data) :ma(data) {}
	virtual void show() { cout << "Base::show()" << endl; }
	virtual void show(int) { cout << "Base::show(int)" << endl; }
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) : Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};

int main()
{
	Derive d(50);
	Base* pb = &d;
	pb->show();
	pb->show(10);

	cout << sizeof(Base) << endl;
	cout << sizeof(Derive) << endl;

	cout << typeid(pb).name() << endl;
	cout << typeid(*pb).name() << endl;

	return 0;
}

运行结果:

Derive::show()
Base::show(int)
8	# 需要多存一个虚函数指针了,所以比刚才多4
12	# 需要多存一个虚函数指针了,所以比刚才多4
class Base *
class Derive

基类虚函数表:

在这里插入图片描述
派生类虚函数表:
在这里插入图片描述
根据虚函数表,我们来分析一下上面程序的运行结果:

pb->show();,编译阶段发现pbBase类型,然后到Base作用域查看Base::show()

  • 若发现show()为普通函数,就进行静态绑定,call Base::show()
  • 若发现show()为虚函数,就进行动态绑定
    mov   eax,dword ptr [pb]	# 将指向虚函数表的vfptr放入eax寄存器
    mov   ecx,dword ptr [eax]	# 将vfptr存的地址的4字节内存&Derive::show()地址放入ecx
    call  ecx	# 调用ecx,取虚函数地址
    

只有在运行时候才知道寄存器的地址,找到哪个地址就调用哪个函数,这就是静态绑定

sizeof变化的原因:多了virtual,即会多一个指向虚函数表的vfptr指针,因此sizeof()大小也会变

typeid(pb).name()pb定义的时候是Base*,永远是Base*

typeid(*pb).name()先看*pbBase类型,再去Base看有没有虚函数

  • 如果Base没有虚函数,*pb识别的就是编译时期的类型, *pb就是Base类型
  • 如果Base有虚函数,*pb识别的就是运行时期的类型:RTTI类型,即Derive类型(vfptr去访问派生类的vftable,在vftable取得class Derive

这里经常容易判断错,判断*pb的时候一定要看有没有虚函数

vftable里的0vfptr在对象内存中的偏移量,没有虚继承的情况下,vfptr永远在对象前四个字节存放,在对象内存起始部分,大部分情况都为0。

总结:

  1. 一个类里面定义了虚函数,那么编译阶段,编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区)
  2. 一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable一个类型定义的n个对象它们的vfptr指向都是同一张虚函数表
  3. 一个类里面虚函数的个数不影响的对象内存大小,影响的是虚函数表的大小
  4. 如果派生类中的方法和基类继承来的某个方法的返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理成虚函数,即覆盖关系

覆盖:基类和派生类方法的返回值,函数名以及参数列表都相同,而且基类的方法是虚函数。那么派生类的方法自动处理成虚函数,它们之间称为覆盖关系

五、详解虚析构函数

问题一:哪些函数不能实现成虚函数呢?

  1. 要成为虚函数,函数地址就要记录在虚函数表中,即虚函数能产生函数地址,存储在vftable
  2. vfptr指针指向vftablevfptr需要依赖对象,对象必须存在,vfptr存储在对象的内存里,这样就能找到vftable,才能在vftable里找到虚函数地址

因此:

  1. 构造函数不能成为虚函数,因为构造完成后才有对象,那没对象就没有vfptr
  2. 构造函数中调用虚函数也不会发生动态绑定,构造函数中调用任何函数都是静态绑定的
  3. 派生类对象构造过程会先调用基类的构造函数,也就是调用基类的构造函数时派生类还没有构造呢,派生类都没初始化呢,也没法调用派生类的方法,因此构造函数中就算有虚函数也是静态绑定
  4. static静态成员方法不能成为虚函数:静态成员方法调用不依赖对象,虚函数表是用对象调用的,因此也不能成为虚函数

问题二:虚析构函数

析构函数调用的时候对象是存在的,因此析构函数是可以成为虚函数的

虚析构函数:在析构函数前加上virtual关键字

来看这段代码:

class Base
{
public:
	Base(int data) : ma(data) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	virtual void show() { cout << "call Base::show()" << endl; }
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) : Base(data), mb(data)
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		cout << "~Derive() " << endl;
	}
private:
	int mb;
	int* ptr;
};

int main()
{
	Base* pb = new Derive(10);
	pb->show();	//show是虚函数,动态绑定   pb : Base*   *pb : Derive
	delete pb;
	return 0;
}

运行结果:

Base()
Derive()
call Base::show()
~Base()

出现的问题:派生类的析构函数没有被调用,导致内存泄露

在这里插入图片描述
如果派生类里面有一个ptr指向外部堆内存,那不调用析构函数没法释放堆内存,这肯定是不行的

分析:先看*pb的类型是Base类型,因此delete调用析构函数先去Base中找Base::~Base(),这是普通函数,因此对于析构函数的调用就是静态绑定

解决方案: 将基类的析构函数前面加一个virtual定义为虚析构函数派生类的析构函数自动成为虚函数

virtual ~Base() { cout << "~Base()" << endl; }

运行结果:

Base()
Derive()
call Base::show()
~Derive()
~Base()

可以看到运行结果合理了

解决后分析*pb的类型是Base类型,调用析构时去Base中找Base::~Base()发现它为虚函数,那么此时对于析构函数的调用就是动态绑定。
此时Base* pb = new Derive(10);pb指向派生类对象,访问的是派生类的虚函数表,派生类的虚函数表中有派生类的析构函数&Derive:: ~derive(),用派生类析构函数将自己部分进行析构,最后再调用基类的析构函数将基类部分析构

所以,什么时候把基类的析构函数必须实现成虚函数?

  • 基类的指针/引用,指向堆上new出来的派生类对象的时候,delete调用析构函数的时候,必须要让其发生动态绑定,否则会导致派生类的析构函数无法调用

六、再谈动态绑定

问题:虚函数的调用一定就是动态绑定吗?

:肯定不是的!!之前说过,构造函数中调用任何函数,包括虚函数,不会发生动态绑定,都是静态绑定

class Base
{
public:
	Base(int data = 0) : ma(data) {}
	virtual void show() { cout << "Base::show()" << endl; }
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data = 0) : Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};

案例一:

Base b;
Derive d;
b.show(); //Base::show()
d.show(); //Derive::show()

反汇编:

在这里插入图片描述
可以看到,使用对象本身调用虚函数,是静态绑定

案例二:

Base* pb1 = &b;
pb1->show(); //Base::show()
Base* pb2 = &d;
pb2->show(); //Derive::show()

反汇编:

在这里插入图片描述
通过一系列操作,最终调用的是寄存器,动态绑定

可以看到,不管是基类指针指向基类对象,还是基类指针指向派生类对象,发生的都是动态绑定

案例三:

Base& rb1 = b;
rb1.show();	//Base::show()
Base& rb2 = d;
rb2.show();	//Derive::show()

引用和指针本质是相同的,是动态绑定

案例四:

Derive* pd1 = &d;
pd1->show(); //Derive::show()
Derive& rd1 = d;
rd1.show();	//Derive::show()

派生类指针指向派生类对象,是动态绑定

案例五:

Derive* p21 = &b;	// 不行!!
Derive* p22 = (Derive*)&b;	// 可以,但是不安全
p22->show();	//动态绑定	Base::show()

虽然此时是动态绑定,但是p22指针指向的是b,这是Base类型的,通过bvfptr会找到Base的虚函数表,调用基类的show方法

总结:

  1. 用对象本身调用虚函数,是静态绑定。
  2. 动态绑定:虚函数前面必须是指针或引用调用才能发生动态绑定:基类指针指向基类对象、基类指针指向派生类对象、派生类指针指向派生类对象都是动态绑定。
  3. 如果不是通过指针或者引用来调用虚函数,那就是静态绑定

七、详解多态的应用原理

多态字面意思:多种多样的形态。多态分为静态的多态动态的多态

静态(编译时期)的多态:函数重载、模板(函数模板、类模板)

函数重载:

bool compare(int, int){}
bool compare(double, double){}
compare(10, 20); 		//call compare_int_int,在编译阶段就确定好调用的函数版本
compare(10.5, 20.5);	//call compare_double_double,在编译阶段就确定好调用的函数版本

模板(函数模板、类模板):

template<typename T>
bool compare(T a, T b) {}
// 模板的实例化发生在编译阶段
compare(10, 20); 	 // => int 实例化一个compare<int>
compare(10.5, 20.5); // => double 实例化一个compare<double>

动态(运行时期)的多态:在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为多态。
多态底层是通过动态绑定来实现的。 基类指针指向谁就访问谁的vfptr,进而继续访问vftable,从vftable中取出来的就是相应的派生类对象的方法

案例:

// 动物基类
class Animal
{
public:
	Animal(string name) :_name(name) {}
	virtual void bark() {}
protected:
	string _name;
};

// 动物实体类
class Cat : public Animal
{
public:
	Cat(string name) :Animal(name) {}
	void bark() { cout << _name << "bark:miao miao!" << endl; }
};

class Dog : public Animal
{
public:
	Dog(string name) :Animal(name) {}
	void bark() { cout << _name << "bark:wang wang!" << endl; }
};

class Pig : public Animal
{
public:
	Pig(string name) :Animal(name) {}
	void bark() { cout << _name << "bark:heng heng!" << endl; }
};

void bark(Cat& cat) { cat.bark(); }
void bark(Dog& dog) { dog.bark(); }
void bark(Pig& pig) { pig.bark(); }

int main()
{
	Cat cat("小猫");	//小猫bark:miao miao!
	Dog dog("小狗");	//小狗bark:wang wang!
	Pig pig("小猪");	//小猪bark:heng heng!

	bark(cat);
	bark(dog);
	bark(pig);

	return 0;
}

这里一个基类有三个派生类,此时发生动态绑定,但是这个bark()接口不太好,我们若添加更多的新的动物,派生类对象越多,bark()方法还需要继续增加。相应的实体类若删除,其对应接口也要删除,无法做到我们软件设计要求的“开-闭”原则(对修改关闭,对扩展开放)

修改bark()即可:

void bark(Animal& a) { a.bark(); }	//动态绑定,找虚函数表

此时再增加新的派生类型,API接口就不用修改了

那我们再来看看继承的本质(好处)是什么

  • 代码的复用
  • 在基类中提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了

八、抽象类以及实践应用

抽象类:拥有纯虚函数的类叫做抽象类,不用提供方法的实现

在上一节,我们定义Animal的初衷,并不是让Animal抽象某个实体的类型。

  1. string _name;,让所有的动物实体类通过继承Animal直接复用该属性
  2. 给所有的派生类保留统一的覆盖/重写接口
class Animal
{
public:
	Animal(string name) :_name(name) {}
	virtual void bark() = 0;	//纯虚函数,现在Animal就是抽象类了
protected:
	string _name;
};

抽象类不能再实例化对象了,即Animal a;是不可以的!!

抽象类与普通类有什么区别?

  • 抽象类一般不是用来定义某一个实体类型的,不能实例化对象,但是可以定义指针和引用变量
  • 一般把基类设计成抽象类,它所做的事情是:
    1. 让所有的派生类类通过继承基类直接复用基类属性。
    2. 给所有的派生类保留统一的覆盖/重写接口

示例:

// 汽车的基类,抽象类
class Car
{
public:
	Car(string name, double oil) : _name(name), _oil(oil) {}
	// 获取汽车剩余油量还能跑的公里数
	double getLeftMiles()
	{
		// 不同汽车1L油跑的公里不一样
		return _oil * this->getMilesPerLiter();//动态绑定
		// 不同的车跑到公里不一样
		// 基类指针指向不同派生类对象访问不同派生类重写的getMilesPerLiter()
	}
	string getName() const { return _name; }
protected:
	string _name;
	double _oil;
	// 纯虚函数,根据具体的汽车而定1L油跑的公里数
	virtual double getMilesPerLiter() = 0;
};

class Benz : public Car
{
public:
	Benz(string name, double oil) : Car(name, oil) {}
	double getMilesPerLiter() { return 20.0; }
};

class Audi : public Car
{
public:
	Audi(string name, double oil) : Car(name, oil) {}
	double getMilesPerLiter() { return 18.0; }
};

class BMW : public Car
{
public:
	BMW(string name, double oil) :Car(name, oil) {}
	double getMilesPerLiter() { return 19.0; }
};

// 给外部提供一个统一的获取汽车剩余路程数的API
void showCarleftMiles(Car& car)
{
	cout << car.getName() << "left miles:"
		<< car.getLeftMiles() << "公里" << endl;
}

int main()
{
	Benz b1("奔驰", 20.0);
	Audi a("奥迪", 20.0);
	BMW b2("宝马", 20.0);
	showCarleftMiles(b1);
	showCarleftMiles(a);
	showCarleftMiles(b2);
	return 0;
}

运行结果:

奔驰left miles:400公里
奥迪left miles:360公里
宝马left miles:380公里

九、继承与多态笔试面试题实战分析

题目一:

class Animal
{
public:
	Animal(string name) : _name(name) {}
	virtual void bark() = 0;
protected:
	string _name;
};

class Cat : public Animal
{
public:
	Cat(string name) : Animal(name) {}
	void bark() { cout << _name << "bark:miao miao!" << endl; }
};

class Dog : public Animal
{
public:
	Dog(string name) : Animal(name) {}
	void bark() { cout << _name << "bark:wang wang!" << endl; }
};

class Pig : public Animal
{
public:
	Pig(string name) : Animal(name) {}
	void bark() { cout << _name << "bark:heng heng!" << endl; }
};

int main()
{
	Animal* p1 = new Cat("加菲猫");
	Animal* p2 = new Dog("二哈");

	int* p11 = (int*)p1;
	int* p22 = (int*)p2;
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;

	p1->bark();
	p2->bark();

	delete p1;
	delete p2;

	return 0;
}

运行结果:

加菲猫bark:wang wang!
二哈bark:miao miao!

分析:

int *p11 = (int*)p1;
int *p22 = (int*)p2;
int tmp = p11[0];
p11[0] = p22[0];
p22[0] = tmp;

p11[0]访问的Cat的前4个字节,p22[0]访问的Dog的前4个字节,这里相当于交换了他俩的vfptr中存放的内容,也就是把这两个指针指向的虚函数表调换了。当前是动态绑定,因此输出结果被交换了

题目二:

class Base
{
public:
	virtual void show(int i = 10) { cout << "call Base::show i:" << i << endl; }
};

class Derive : public Base
{
public:
	virtual void show(int i = 20) { cout << "call Derive::show i:" << i << endl; }
};

int main()
{
	Base* p = new Derive();
	p->show();
	delete p;
	return 0;
}

运行结果:

call Derive::show i:10

分析:

基类有virtual,是动态绑定,会调用派生类的show。这里i是10的原因是在函数调用的时候,参数的压栈在编译时期

push 0Ah	# 函数调用,参数压栈在编译时期确定好的
mov eax, dword ptr[p]
mov ecx, dword ptr[eax]
call ecx	# 动态绑定

调用一个函数时要先压参数,若没有传入实参,会将形参默认值压栈,且参数压栈是在编译时期确定好的,编译阶段编译器只能看见基类的show,会将基类虚函数形参默认值10压栈,运行时候才发生动态绑定调用派生类的show方法。不管调用谁的show方法,只要是show方法形参压栈的值都是10

在继承结构中,基类和派生类的同名覆盖方法(虚函数)如果有默认值且默认值不同,派生类中虚函数的形参默认值是无效的,永远也不会用到

题目三:

class Base
{
public:
	virtual void show() { cout << "call Base::show" << endl; }
};

class Derive : public Base
{
private:
	virtual void show() { cout << "call Derive::show" << endl; }
};

int main()
{
	Base* p = new Derive();
	p->show();
	delete p;
	return 0;
}

运行结果:

call Derive::show

注意,当前派生类的方法是private的,但是还能正常调用,这是因为在编译阶段,编译器只能看见Basepublicshow,所以编译是通过的,也就是说,一个成员方法能不能调用,编译阶段就确定好了。而p->show();最终能够调用Derive中的show,是在运行时期才确定的。最终调用的是基类的方法还是派生类的方法,取决于形成的汇编指令是静态绑定还是动态绑定,因为现在是动态绑定,所以最后能成功的调用派生类的show

再来看,如果BaseprivateDerivepublic

class Base
{
private:
	virtual void show() { cout << "call Base::show" << endl; }
};

class Derive : public Base
{
public:
	virtual void show() { cout << "call Derive::show" << endl; }
};

由于Base* p = new Derive();p指针是Base类型,Base里的show方法是private,所以Base* p = new Derive(); p->show();编译不会通过,不能调用

改成Derive* p = new Derive();就可以调用了

题目四:

class Base
{
public:
	Base()
	{
		cout << "call Base()" << endl;
		clear();
	}
	void clear() {
		memset(this, 0, sizeof(*this));	// 全赋为0
	}
	virtual void show() { cout << "call Base::show() " << endl; }
};

class Derive : public Base
{
public:
	Derive() { cout << "call Derive() " << endl; }
	void show() { cout << "call Derive::show() " << endl; }
};

示例1:

Base* pb1 = new Base();
pb1->show(); // 动态绑定
delete pb1;

运行失败,产生异常

分析:基类的对象内存如图,vfptr指向Basevftable,当构造函数里调用clear()时,将基类的对象内存清为0,虚函数指针也变为0地址,进行动态绑定时,访问不到,调用时出错,程序崩溃

在这里插入图片描述

示例2:

Base* pb2 = new Derive();
pb2->show(); // 动态绑定
delete pb2;

运行正确

call Base()
call Derive()
call Derive::show()

在这里插入图片描述

那么,虚函数表的地址什么时候被写入到vfptr里面?

函数栈帧开辟完以后,执行函数体之前

Base()
{
	/*
	push ebp
	mov ebp,esp
	sub esp,4Ch
	rep stos esp<->ebp 0xCCCCCCCC
	vfptr <- &Base::vftable
	*/
	cout << "call Base()" << endl;
	clear();
}

Derive()
{
	/*
	push ebp
	mov ebp,esp
	sub esp,4Ch
	rep stos esp<->ebp 0xCCCCCCCC
	vfptr <- &Derive::vftable
	*/
	cout << "call Derive() " << endl;
}
  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GeniusAng丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值