Bootstrap

【C++STL杂货铺】2万字总结学习进阶必学的两座大山:继承 && 多态(重点详解)

---------------------------------------------------------------------------------------------------------------------------------

每日鸡汤:在人生的道路上,从来没有全身而退,坐享其成,不劳而获一说。你不努力,就得出局。

---------------------------------------------------------------------------------------------------------------------------------

一:继承

1.1:继承是什么?(概念及定义)

1.1.1:继承—概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

例如,学生和教师的例子。首先,学生和教师都有着相同的属性。第一点他们都是人。即都有着各自的姓名,年龄等属性。所以如果要写关于学生和教师的属性类,那么就可以复用人这一个类。而将学生和教师不同的属性放到各个的类里面。这样就能达到对人这一个类的复用。达到继承的功能。

#include<iostream>
using namespace std;

// 描述一个人
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

private:
	string _name = "张三";	// 姓名,以“张三”为例
	int _age = 18;			// 年龄,以18为例
};
// 表述一个学生,学生也是人,所以复用”人“这一个类
class Student : public Person
{
protected:
	int _stuid;		// 学号
};
// 表述一个教师,教师也是人,所以也可复用”人“这一个类
class Teacher : public Person
{
protected:
	int _jobid;		// 职工号
};

int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
    return 0;
}

通过监视窗口查看继承情况:

继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这体现出了Student和Teacher复用了Person的成员。

在看一份继承的形式:

那么继承的定义大概就出来了。

1.1.2:继承—定义

定义格式:

我们看到Person是父类,也称作基类Student是子类,也称作派生类。

继承关系和访问限定符:

通过继承三种方式和三种访问限定符。所以继承中的权限总共有9中可能。即:

继承基类成员访问方式的变化

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected
成员
派生类的private
成员
基类的private成
在派生类中不可见在派生类中不可见在派生类中不可

对9种变化进行总结:

1、父类的私有成员在子类中都是不可见的(注意:是已经继承了但是不可见,并非private成员没有被继承下去)。即:不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

2、protected访问限定符:基类private成员无论在派生类中还是类外都是不能被访问的,那如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。即看出保护成员限定符是因继承才出现的。

3、通过上述表格发现,父类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > priveate。即:取权限最小的那个。

4、最常用的是这两个权限方式:

1.2:基类和派生类对象的赋值转换

        派生类(子类)对象可以赋值给基类(父类)的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

        基类对象不能赋值给派生类对象。

 使用一个形象的例图来分析这一赋值转换规则:

看结果:

所以,public继承,父类和子类是一个is-a的关系(“学生是人,老师也是一个人”)。

子类对象赋值给父类对象/父类指针/父类的引用,认为是天然,中间不产生临时对象,这个叫做父子类赋值兼容规侧(切剖/切片)。

1.3:继承中的作用域

命名空间中有域,而类也有着类域。那么继承也是有域的。

  1. 在继承体系中基类派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员

 看一个基类和派生类有同名成员的例子:

// 继承中的作用域
class Person
{
protected:
	string name = "小王";
	int num = 11234;	// 身份证号
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << name << endl;
		cout << "身份证号:" << num << endl;
		cout << "学号:" << num << endl;
	}

protected:
	int num = 11;	// 学号
};

int main()
{
	Student s;
	s.Print();

	return 0;
}
// 运行结果:
姓名:小王
身份证号:11
学号:11

发现结果与我们想要的结果不一样呢。即默认情况下直接访问子类的,子类同名成员隐藏了父类同名成员。

如何获得我们想要的结果呢?基类::基类成员 显示访问

继承中,同名的成员函数,函数名相同就构成隐藏,不管参数和返回值。【注意和函数重载的区别:函数重载是在同一个作用域中的,而基类和派产生类是不同作用域。】

看函数构成隐藏的用法:

#include<iostream>
using namespace std;

class A
{
public:
	void func()
	{
		cout << "void func()" << endl;
	}
};
class B : public A
{
public:
	void func()
	{
		cout << "void func(size_t i)" << endl;
	}
	void func1()
	{
		func();		// 构成隐藏
		A::func();	// 显示调用
	}

};
int main()
{
	B b;
	b.func();		// 构成隐藏
	b.A::func();	// 显示调用
	cout << endl;
	b.func1();

	return 0;
}
// 运行打印结果:
void func(size_t i)
void func()

void func(size_t i)
void func()

不过:实际过程中建议不搞用同名成员和同名函数。 

1.4:派生类的默认成员函数

所谓的默认,意思就是指我们我们不写,扁你一起会自动生成一个。我们已经想到了以前开始学的六个默认成员函数(构造函数,析构函数,拷贝构造,赋值重载,const对象取地址,普通对象取地址)。那么在派生类中,者留两个默认成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  5. 派生类对象初始化先调用基类构造再调派生类构造。(构造,先父后子)
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。(析构,先子后父)

 即构造,析构顺序式样图:

 看一则代码:

#include<iostream>
using namespace std;

class Person
{
public:
	Person(const char* name = "龙傲天")
		:_name(name)
	{
		cout << "基类构造函数:Person()" << endl;
	}
	
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "基类拷贝构造函数:Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p)
	{
		cout << "基类赋值重载:Person& operator=(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}

	~Person()
	{
		cout << "基类析构函数:~Person()" << endl;
	}

protected:
	string _name;	// 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int id)
		:Person(name)
		,_id(id)
	{
		cout << "派生类构造函数:Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		, _id(s._id)
	{
		cout << "派生类拷贝构造函数:Student(const Student& s)" << endl;
	}

	Student& operator=(const Student& s)
	{
		if (&s != this)
		{
			Person::operator=(s);
			_id = s._id;
		}
		cout << "派生类赋值重载:Student& operator=(const Student& s)" << endl;
		return *this;
	}

	~Student()
	{
		cout << "派生类析构函数:~Student()" << endl;
	}

protected:
	int _id;	// 学号
};

int main()
{
	Student s("龙傲天",21);
	cout << endl;

	Student s1(s);
	cout << endl;

	Student s2("张三", 40);
	s2 = s;
	cout << endl;

	return 0;
}

// 打印运行结果:
基类构造函数:Person()
派生类构造函数:Student()

基类拷贝构造函数:Person(const Person& p)
派生类拷贝构造函数:Student(const Student& s)

基类构造函数:Person()
派生类构造函数:Student()
基类赋值重载:Person& operator=(const Person& p)
派生类赋值重载:Student& operator=(const Student& s)

派生类析构函数:~Student()
基类析构函数:~Person()
派生类析构函数:~Student()
基类析构函数:~Person()
派生类析构函数:~Student()
基类析构函数:~Person()

其他的都还能理解,但是调用的析构函数是什么鬼!!没有调用父类的析构竟然也打印出来了,是编译器出问题了吗?肯定不是,先看我们自己调用出父类的析构函数:

发现多析构了一次,why? > 这是因为多态的原因,析构函数会统一被处理成destructor,父子类的析构函数构成隐藏。底层为了保证析构安全,会“先子后父”,父类析构函数不需要显式调用,子类析构函数结束时会调用父类析构

1.5:继承,友元和静态成员

1.5.1:继承和友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员.

#include<iostream>
using namespace std;

// 继承和友元
class Student;
class Person
{
public:
	friend void Print(const Person& p, const Student& s);	// 基类的友元

protected:
	string _name;
};

class Student : public Person
{
public:

protected:
	int _sid;
};

void Print(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._sid << endl;		// 不能访问子类Student的私有和保护成员
}

int main()
{
	Person p;
	Student s;
	Print(p, s);

	return 0;
}

 结果就是访问错误:

所以就只能子类再写个友元:

class Student : public Person
{
public:
	friend void Print(const Person& p, const Student& s);	// 派生类的友元
protected:
	int _sid;
};

 这样就没有问题了,不过还是要注意:友元关系是不能继承的。

1.5.2:继承和静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例

class Person
{
public:
	Person()
	{
		++_count;	
	}

protected:
	string _name;	// 姓名
public:
	static int _count;	// 统计人数
};
int Person::_count = 0;

class Student : public Person
{
protected:
	int _Sid;	// 学号
};

Student func(Student s1)
{
	Student s;
	return s;
}

int main()
{
	Person p;
	Student st;
	func(st);

	cout << Person::_count << endl;
	Student::_count = 0;
	cout << Person::_count << endl;

	return 0;
}
// 打印结果:
3
0

静态成员不属于某个对象,属于整个类。派生类继承静态成员继承的是使用权。 

1.6:菱形继承和菱形虚拟继承

1.6.1:菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

菱形继承:菱形继承是多继承的一种特殊情况。

 但是我们发现菱形继承有个缺陷问题:在式样图中,发现Assistant类中有两份Person成员信息的两份。即菱形继承有数据冗余和二义性的问题。

式样图:

#include<iostream>
using namespace std;

// 菱形继承
class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	Assistant a;
	// a._name = "龙傲天";        // 有二义性,无法确定访问的是哪一个name

	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "abcd";
	a.Teacher::_name = "刘波";

	return 0;
}

 所以,关于菱形继承有很多的问题。那么该如何解决它呢?虚拟继承

1.6.2:菱形虚拟继承

        虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

代码:

#include<iostream>
using namespace std;

/// 虚拟继承
class Person
{
public:
	string _name; // 姓名
};

class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	Assistant a;

	a.Student::_name = "stu";
	a.Teacher::_name = "tea";

	a._name = "龙傲天";

	return 0;
}

调试运行,查看结果:

继续调试:

继续调试: 

继续调试:

 发现每次修改name,所对应的name都会发生改变,如何做到的呢?

接下来使用一个简化的菱形继承体系,借助内存窗口观察:

先看没有菱形虚拟继承的形态:

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	// d._a = 0;	/// 没有菱形虚拟继承该行代码无法通过

	return 0;
}

查看内存窗口:

 刚开始:

继续调试:

 继续调试:

看看发现了什么。就是含有两份的_a变量。即当没有virtual虚拟继承时存在的二义性和数据冗余。

在看含有菱形虚拟继承的形态:

#include<iostream>
using namespace std;

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	d._a = 0;	

	return 0;
}

调试查看内存窗口:

继续调试:

继续调试:

继续调试: 

 在虚拟继承中,通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

Person关系菱形继承的原理: 

1.7:总结继承

1.7.1:继承的总结反思

        大部分人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

1.7.2:继承和组合

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。(“是”的关系
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。(“有”的关系

继承允许你根据基类的实现来定义派生类的实现。
对象组合是类继承之外的另一种复用选择。

 先看代码来发现区别:

#include<iostream>
using namespace std;

class A
{
public:
	void func()
	{}
protected:
	int _a;
};

class B : public A
{
public:
	void f()
	{
		func();
		_a++;
	}
protected:
	int _b;
};

// 组合
class C
{
public:
	void func()
	{}
protected:
	int _c;
};

class D
{
public:
	void f()
	{
		_c.func();
		//_c._c++;	// C类的protected不可访问
	}
protected:
	C _c;
	int _d;
};

int main()
{
	cout << sizeof(D) << endl;
	cout << sizeof(B) << endl;

	D dd;
	//dd.func();	// “黑箱”,外部不能调用

	B bb;
	bb.func();		// “白箱”,外部可以调用

	return 0;
}
// 打印结果:
8
8

组合类的公有可以使用,保护不可使用;继承中父类的共有可以使用,保护也可以使用。即继承相对于组合,权限要更大一些的。

  • 继承:白箱复用。【白箱:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。】
  • 组合:黑箱复用。【黑箱:新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。】

总结:实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

二:多态

2.1:多态 is what ?(概念,定义及实现)

2.1.1:多态—概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

一个例子:比如买票这个行为,当普通人买票时,是全价买票学生买票时,是半价买票军人
买票时是优先买票。
不同的人买票时有着几种不同的购买方式,这就是多态。

2.1.2:多态—定义

想要构成多态,除了要有继承之外,还必须要达到两个必要条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数:被virtual修饰的类成员函数被称为虚函数。

class Person
{
public:
	virtual void BuyTicket()        // 被virtual修饰的类成员函数
	{
		cout << "全价买票" << endl;
	}
};

注意:虚函数virtual 和 virtual继承没有任何关系。virtual继承只是解决数据的冗余和二义性问题。 

虚函数的重写:

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同【三同】),称子类的虚函数重写了基类的虚函数。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价买票" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()            // 三同。构成重写
	{
		cout << "全价半票" << endl;
	}
};

2.1.3:多态—实现

现在来实现一个多态代码:

#include<iostream>
using namespace std;

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价买票" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价半票" << endl;
	}
};

// 基类的指针/引用来调用这个虚函数
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);

	return 0;
}

// 打印结果:
全价买票
全价半票

需要注意的是,关于虚函数重写的两个例外:

  1. 协变(基类与派生类虚函数返回值类型不同)
  2. 析构函数的重写(基类与派生类析构函数的名字不同)

协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A {};
class B : public A {};

class Person
{
public:
	// 基类虚函数返回基类对象的指针或者引用
	virtual A* func()
	{
		return new A;
	}
private:

};
class Student : public Person
{
public:
	// 与基类虚函数的返回值类型不同
	// 派生类虚函数返回派生类对象的指针或者引用时
	virtual B* func()
	{
		return new B;
	}
};

析构函数的重写:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

#include<iostream>
using namespace std;

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
	 ~Student()
	{
		cout << "~Student()" << endl;
	}
};


int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;

	return 0;
}
// 打印运行结果:
~Person()
~Student()
~Person()

 发现在派生类的析构函数前加不加virtual都一样。

注意,这只是编译器给析构函数“开了一个绿灯”,但是最好我们都加上。

C++11两个关键字:override 和 final

final:修饰虚函数,表示该虚函数不能再被重写

class A
{
public:
	virtual void Func() final
	{
		cout << "virtual void Func()--->1" << endl;
	}
};

class B : public A
{
public:
	virtual void Func()
	{
		cout << "virtual void Func()" << endl;
	}
};

加上final,该函数就不能被重写啦。 

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class A
{
public:
};

class B : public A
{
public:
	virtual void Func() override    // 检查是否重写了基类的某个虚函数
	{
		cout << "virtual void Func()" << endl;
	}
};

小小总结:重载、覆盖(重写)、隐藏(重定义)的对比

2.2:抽象类

        在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数,派生类才能实例化出对象纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

两个特点: 

  • 间接强制让派生类重写
  • 抽象类——不能实例化出对象,但是能有指针
#include<iostream>
using namespace std;

// 抽象类
class A
{
public:
	virtual void func() = 0;

};
class B : public A
{
public:
	virtual void func()		// A为抽象类,func函数必须重写
	{
		cout << "i am B, 继承的抽象类A" << endl;
	}
};
class C : public A			
{
public:
	virtual void func()		// A为抽象类,func函数必须重写
	{	
		cout << "i am C, 继承的抽象类A" << endl;
	}
};

void Func(A* ptr)
{
	ptr->func();
}

int main()
{
	// A a;	// 不能实例化出对象
	A* pa;	// 可以有A类指针

	Func(new B);
	Func(new C);

	return 0;
}
// 打印运行结果:
i am B, 继承的抽象类A
i am C, 继承的抽象类A

2.3:多态的原理

2.3.1:虚函数表

先看代码,回答问题:

#include<iostream>
using namespace std;

class A
{
public:
	virtual void func()
	{
		cout << "func()" << endl;
	}
private:
	int _a = 1;
};

int main()
{
	cout << sizeof(A) << endl;

	return 0;
}

其中sizeof(A)的大小是多少?相信很多宝子们都会以为是4,其实不然,sizeof(A) = 8。为什么?不是只有一个整形变量_a吗?怎么会是8呢。这是因为,在C++中,C++将虚函数存到一个虚函数表内,只要是计算一个类的大小,且这个类内有一个虚函数就会加4。这个“4”就是虚表的指针,指向的是一个指针数组,用来存虚函数的地址用的。

看一个简单的虚函数表:

#include<iostream>
using namespace std;

class A
{
public:
	virtual void func()
	{
		cout << "func()" << endl;
	}
private:
	int _a = 1;
};

int main()
{
	A a;
	cout << sizeof(A) << endl;

	return 0;
}

 调试发现,有一个指针_vfptr。

内存情况如下:

修改类内虚函数个数 :

class A
{
public:
	virtual void func()
	{
		cout << "func()" << endl;
	}
	virtual void func1()
	{
		cout << "func()" << endl;
	}
private:
	int _a = 1;
};

查看调试信息:

 内存情况如下:

根据代码画出式样图分析:

发现所谓的_vfptr就是一个指向虚函数表(指针数组)的指针。

看看多态形式的虚函数表:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func(){}

private:
	int a = 0;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
	int b = 1;
};

int main()
{
	Person p1;
	Person p2;
	Person p3;

	Student s1;
	Student s2;

	return 0;
}

内存调试:

 

 看看发现了什么:BuyTicket虚函数地址“变化了”,Func地址没有被覆盖

2.3.2:多态原理

看一个多态的例子:

#include<iostream>
using namespace std;

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func() {}

private:
	int a = 0;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
	int b = 1;
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person p1;
	Student s1;

	Func(&p1);
	Func(&s1);

	return 0;
}
// 运行结果:
买票-全价
买票-半价

构成多态:ptr指向父类就调用父类的虚函数;ptr指向子类就调用子类的虚函数。

调试:

注意:基类使用的是同一张虚表,派生类使用的是同一张虚表。

切片:

这一段属于了解部分,大家自行看图理解一下即可。 

2.4:单继承和多继承关系中的虚函数表

已经说了很多次虚表了,又因为“基类使用的是同一张虚表,派生类使用的是同一张虚表”,并且已经知道虚表就是一个函数指针数组。那么虚表到底存储在哪里的?虚函数存储在哪里呢?

用一则代码来验证在内存中的哪一个位置。

#include<iostream>
using namespace std;

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

void func()
{
	cout << "void func()" << endl;
}

int main()
{
	Base b1;
	Base b2;

	static int a = 0;
	int b = 0;
	int* p1 = new int;
	const char* p2 = "hello world";
	printf("静态区:%p\n", &a);
	printf("栈:%p\n", &b);
	printf("堆:%p\n", p1);
	printf("代码段:%p\n", p2);
	printf("虚表:%p\n", *((int*)&b1));		// 取Base的前四个字节,再解引用
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", func);

	return 0;
}
// 运行结果:
静态区:00E6C414
栈:00B6F800
堆:0107FE38
代码段:00E69B64
虚表:00E69B44
虚函数地址:00E615AA
普通函数地址:00E615A5

发现虚表地址与代码段那里很是接近,虚函数地址与普通函数那里也很是接近。所以总结,虚函数和普通函数一样,都是存在代码段里面的同时,把虚函数的地址存了一份到虚函数表内。虚函数表地址在内存中的代码段(常量区)的位置。

如何获取虚表内的地址呢,我们知道虚表就是一个函数指针数组。那么就可以这样:

#ubclude<iostream>
using namespace std;

// 打印虚表的地址
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

typedef void(*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p\n", i, a[i]);
		VFUNC f = a[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	PrintVFT((VFUNC*)(*(int*)&b));

	return 0;
}
// 打印运行:
[0]:009E159B
Base::func1
[1]:009E158C
Base::func2

所以,虚函数的地址一定会被放进虚函数表内。

2.4.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; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

typedef void(*VFUNC)();
void PrintVFT(VFUNC a[])
{
	for (size_t i = 0; a[i] != nullptr; i++)
	{
		printf("[%d]:%p -> ", i, a[i]);
		VFUNC f = a[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	PrintVFT((VFUNC*)(*(int*)&b));
	cout << endl;
	PrintVFT((VFUNC*)(*(int*)&d));

	return 0;
}
// 运行结果:
[0]:008E12B7 -> Base::func1
[1]:008E111D -> Base::func2


[0]:008E1249 -> Derive::func1
[1]:008E111D -> Base::func2
[2]:008E1235 -> Derive::func3
[3]:008E1172 -> Derive::func4

 分析代码单继承虚表:

单继承的虚函数表一看就能懂,就不再分析啦,下面看多继承中的虚函数表。 

2.4.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(*VFUNC)();
void PrintVFT(VFUNC* a)
{
	cout << "虚表地址:" << a << endl;
	for (size_t i = 0; a[i] != nullptr; i++)
	{
		printf("[%d]:%p -> ", i, a[i]);
		VFUNC f = a[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	PrintVFT((VFUNC*)(*(int*)&d));

	PrintVFT((VFUNC*)(*(int*)((char*)&d + sizeof(Base1))));

	return 0;
}
// 运行结果:
虚表地址:007E9B94
[0]:007E124E -> Derive::func1
[1]:007E12F3 -> Base1::func2
[2]:007E123A -> Derive::func3

虚表地址:007E9BA8
[0]:007E1366 -> Derive::func1
[1]:007E10BE -> Base2::func2

发现:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

一张形象的式样图:

2.5:总结多态

面试问答形式总结:

2.5.1:什么是多态?

  • 静态多态:函数重载
  • 动态多态:1.父类的指针或者引用调用虚函数;2.虚函数完成重写

2.5.2:什么是重载、重写(覆盖)、重定义(隐藏)?

2.5.3:多态的实现原理?

“虚函数表”,“函数指针数组”,“虚函数指针”,参考上述内容。

2.5.4:inline函数可以是虚函数吗?

可以,若是普通调用的话,inline起作用;但是多态调用的话,inline就不起作用了。因为inline修饰的函数是没有地址的,而多态调用是需要地址的(虚函数表)。

2.5.5:静态成员可以是虚函数吗?

不可以,编译会报错,static成员函数没有this指针,它可以指定类域进行调用,无法构成多态,没有意义。

2.5.6:构造函数可以是虚函数吗?

不可以,编译会报错,对象中虚表指针是构造函数阶段才初始化的。虚函数多态调用,要到虚表中找,但是虚表指针还没有初始化。即不可以。

2.5.7:析构函数可以是虚函数吗?

最好是虚函数。有场景:

父类指针 = new 子类对象;

delete 父类指针;

该场景只有析构函数构成重写,delete时构成多态,才能正确调用子类析构函数。

2.5.8:对象访问普通函数快还是虚函数更快?

普通调用一样快;多态调用时要慢一些,因为要到虚表中查找。

2.5.9:虚函数表是在什么阶段生成的,存在哪的?

虚函数表在编译时就生成了,虚函数表指针构造是,才初始化给对象的。存于内存中的代码段。

;