目录

一、前言

二、继承

1、定义

2、用法

3、类模板继承

4、赋值兼容转换

5、隐藏

6、子类的默认成员函数

(1)构造函数

(2)拷贝构造

(3)赋值重载

(4)析构函数

7、final

8、继承&友元

9、static成员

10、多继承

(1)定义

(2)菱形继承

(3)菱形虚拟继承

(4)多继承内存分布

11、继承&组合

三、多态

1、定义

2、虚函数

(1)定义

(2)重写

3、条件

4、经典多态面试题

5、协变

6、析构&多态

7、override、final

8、纯虚函数&抽象类

9、虚表指针

10、虚函数表

四、结语


一、前言

面向对象是C++的一大特点,而在面向对象中,以封装、继承、多态为三大特性,封装即对一个类内部进行封装,封装将屏蔽底层的实现细节,用户可不关心底层实现的细节,直接使用其功能即可,类似于黑盒测试,对功能进行测试即可,这是封装的一大特点,类和对象、容器、迭代器实际上就是一种封装,它们都屏蔽了底层的实现细节,需要时直接使用其功能即可。除了封装之外,面向对象还有两大特性,继承、多态,本文将围绕继承、多态两大特性展开介绍,继承、多态可认为是在封装的基础上进一步扩展,进一步实现了代码复用。在继承中,子类将继承父类,父类的成员变量和函数在子类中都各有一份,子类在父类的基础上进一步扩展成员和功能,实现了父类的复用,多态与模板类似,都能实现复用,不同在于模板是通过传不同参数类型,在编译时生成对应的函数,是静态的,而多态则是同一个函数通过传不同的对象,在运行时实现相应的功能,是动态的。在面向对象编程中,继承、多态可谓是面向对象的"瑞士军刀",在面向对象编程中有着广泛的应用。

二、继承

1、定义

继承也可类比于模板,模板是在函数层次的复用,而继承则是在类层次上的复用,继承允许在原有类的基础上进一步扩展其成员和功能,原有类称为父类或者基类,扩展后的类称为子类或者派生类,子类将继承父类的成员变量和函数,并在此基础上进一步扩展成员和功能,当两个类成员变量或函数有冗余时,可以考虑对两个类使用继承。

2、用法

继承方式与类成员的访问方式一致,有3种继承方式:public(公有)、protected(保护)、private(私有)继承,3种继承方式不同在于继承下来的父类成员在子类的访问方式会有所差异。

总结一下可以看出,父类的private成员无论子类是以什么方式继承,在子类中都是不可见的,这里的不可见是指父类的private成员还是被继承到了子类中,但在语法上限制子类在类里面或类外面都不能访问它。概括地说,父类的private成员在子类中都是不可见的,父类其他类型的成员在子类的访问方式为MIN(成员在父类的访问限定符 & 继承方式),优先顺序为:public>protected>private,可以看出protected、private继承的子类成员的访问方式受限,难以扩展,因此在实际运用中,一般都采取public继承,很少使用private、protected继承,在没有指明继承方式的情况下,class默认的是private继承,struct默认的是public继承。

如上,Student对Person继承,继承格式为class Student:public Person,Student为派生类,public为继承方式,为公有继承,Person为基类。

#include<iostream>
using namespace std;
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name="张三";
	string _adress;
	string _tel;
private:
   int _age;
};
class Student :public Person
{
public:
	void study()
   {
		
   }
protected:
	int _stuid;
};
class Teacher :public Person
{
public:
	void teaching()
	{

     }
protected:
	string _title;
};

当设计Student和Teacher两个类时,不难发现Student和Teacher会有一些冗余的成员,如_name、_tel、_address、_age,那么可以考虑将这些公有的成员放到Person类中,Student和Teacher继承Person,这样Studen和Teacher就可以复用Person的成员,就不需要重复定义这些成员了,从而实现复用。

#include<iostream>
using namespace std;
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name="张三";
	string _adress;
	string _tel;
private:
   int _age ;
};
class Student :public Person
{
public:
	void study()
   {
	
   }
protected:
	int _stuid;
};
class Teacher :public Person
{
public:
	void teaching()
	{

    }
protected:
	string _title;
};
int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

构造Student、Teacher对象s、t,由于Student、Teacher都继承了Person,故都可调用person的identity成员函数,结果如(1)所示。

(1)

3、类模板继承

除了继承类之外,还可继承类模板,类模板的相应接口也都会被继承下来。

#define container std::deque
namespace Kzy
{
	template<class K>
	class Stack :public container<K>
	{
	public:
		void push(const K& x)
		{
			container<K>::push_back(x);
		}
		void pop()
		{
			container<K>::pop_back();
		}
		const K& top()
		{
			return container<K>::back();
		}
		bool empty()
		{
			return container<K>::empty();
		}
	};

例如利用deque类模板来实现Stack,Stack实现为模板类型,先宏定义std::deque为container,Stack继承container<K>类模板,即class Stack:public container<K>,下面就可以通过调用container<K>的相关接口完成入栈、出栈等操作,需要注意的是类模板为按需实例化,因此调用container<K>的相关接口时需要先实例化,再调用接口,如container<K>::push_back(x),这样就通过继承deque类模板来实现Stack的结构。

int main()
{
    Kzy::Stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

对Stack进行测试,Kzy::Stack<int> st,先构造并实例化st,再通过push将1、2、3依次入栈,通过while循环依次取出栈顶元素,结果依次为3 2 1,如(2)所示,结果正确,测试通过。

(2)

4、赋值兼容转换

父类和子类对象支持赋值兼容转换,即public继承的子类对象/指针/引用可以直接赋值给父类的对象/指针/引用,可以形象地理解为切片或者切割,将子类中父类的那部分切过去赋值给父类,如下图所示,但父类对象不能赋值给子类对象,父类的指针或引用可通过强制类型转化为子类的指针或引用,但必须是父类的指针或者引用指向子类对象才是安全的。

​
#include<iostream>
using namespace std;
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name;
	string _adress;
	string _tel;
private:
   int _age;
};
class Student :public Person
{
public:
	void study()
   {
   }
protected:
	int _stuid;
};
int main()
{
	Student st;
	Person p1 = st;
	Person* p2 = &st;
	Person& p3 = st;
	return 0;
}

​

Student继承Person类,则Person为父类,Student为Person的子类,则Student对象st可直接赋值给Person对象p1,子类对象st的指针、引用也可直接赋值给Person的指针p2、引用p3,相当于将子类对象st中父类的那一部分切出来赋值给父类对象。

5、隐藏

继承体系中父类和子类都有独立的作用域,若子类和父类中有同名成员,那么子类成员将屏蔽父类对同名成员的直接访问,称为隐藏。

#include<iostream>
using namespace std;
class Person
{
protected:
	string _name="Kzy";
	int _num=111;
};
class student :public Person
{
public:
	void print()
	{
		cout << _num << endl;
		cout << Person::_num << endl;
	}
protected:
	int _num=999;
};
int main()
{
	student s;
	s.print();
	return 0;
}

Person父类和student子类中存在同名变量_num,那么若想访问父类的同名成员变量_num,可以在子类成员函数中,使用父类::父类成员显式访问,如子类成员函数print,Person::_num即可访问父类Person的成员变量_num,若没有指明作用域,则访问子类student的成员变量_num,故s.print()结果为999 111,如(3)所示。

(3)

如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

class A
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
};
class B :public A
{
public:
	void func(int i)
	{
		cout << "func(int i)" <<i<< endl;
	}
};
int main()
{
	B b;
	b.A::func();
	b.func(1);
	return 0;
}

A、B的func成员函数明显就构成了隐藏关系,先构造子类对象b,若想调用A的func函数,则需指明作用域,即b.A::func(),若直接调用,即b.func(1),由于B的func隐藏了A的func,则调用B的func函数,结果如(4)所示,在实际继承体系中最好不要定义同名的成员。

(4)

6、子类的默认成员函数

默认成员函数常见的有4个,构造、拷贝、赋值、析构,下面将探讨子类的这4个默认成员函数在继承体系下是如何生成的,这4个默认成员函数行为与父类的默认成员函数行为相关,父类Person的默认成员函数如下,接下来根据父类的默认成员函数来实现子类student的默认成员函数。

class Person
{
public:
	Person(const char* name="Kzy")
		:_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;
};

(1)构造函数

class student :public Person
{
public:
	student(const char* name,int num,const char* address)
		:Person(name)
		,_num(num)
		,_address(address)
	{ }
protected:
	int _num=1;
	string _address="China";
};

子类的构造函数必须调用父类的构造函数来初始化父类的那一部分成员,若父类没有合适的默认构造,则必须在子类构造函数的初始化列表显示调用父类的构造函数,student的初始化列表就显示调用了Person的构造函数来完成Person的构造,即Person(name)。

(2)拷贝构造

    student(const student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{ }

与构造函数类似,student的拷贝构造也必须调用父类的拷贝构造来完成父类的拷贝初始化,Person(s),这时就是通过将子类对象s切片拷贝给父类对象,从而完成父类的拷贝初始化。

(3)赋值重载

    student& operator=(const student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
			_address = s._address;
		}
		return *this;
	}

与构造类似,子类的赋值重载也必须调用父类的赋值重载来完成父类那一部分的赋值,需要注意的是子类的operator=隐藏了父类的operator=,所以调用父类的operator=,需要指明父类的作用域,即Person::operator=(s),这里也是将子类对象s切片赋值给父类对象,从而完成父类那一部分的赋值。

(4)析构函数

    ~student()
	{
		
	}

由于Person、student类没有涉及到资源的释放,所以可以不需要自己实现析构函数,需要注意的是子类构造是先构造父类的那一部分,再构造子类,因此子类的析构顺序是先析构子类,再析构父类,子类的析构函数会在析构子类后自动调用父类的析构来析构父类的那一部分,不需要在子类析构中调用父类的析构,这样才能保证子类析构先析构子类再析构父类。

7、final

class Base final 
{
public:
	void func5()
	{
		cout << "Base::func5" << endl;
	}
protected:
	int a=1;
};

final用于实现一个不能被继承的类,若想一个类不被继承,则可用final声明,如class Base final,则Base就不能被其他类继承,还可以将Base构造函数的访问限定符限定为private,则子类就不能访问Base的构造函数,也就无法完成父类Base的构造,则Base也就不能被继承。

class Base 
{
public:
	void func5()
	{
		cout << "Base::func5" << endl;
	}
protected:
	int a=1;
private:
	Base()
	{ }
};

8、继承&友元

友元关系不能被继承,即若一个函数是父类的友元函数,则该函数不一定是子类的友元函数,若为子类的友元函数,则必须在子类中声明友元关系。

class Student;
class Person
{
public:
	friend void func(const Person& p, const Student& s);
protected:
	string _name;
};
class Student :public Person
{
public:
	friend void func(const Person& p, const Student& s);
protected:
	int _stunum;
};
void func(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stunum << endl;
}

func为Person的友元函数,Student继承Person,但友元关系并不能被继承,故func并不为Student的友元函数,需在Student中声明友元关系,func才为Student的友元函数。

9、static成员

父类的普通成员变量在子类中都有,相当于子类拷贝了父类的那一部分,但若父类中有static修饰的成员,则子类与父类共享同一个static成员,它们的地址是一样的,是同一份,可以这么理解,static修饰的成员并不为某个类独享,只是受类访问限定符的限制,static成员存放在静态区,并不归属于父类和子类。

class Person
{
public:
	string _name;
	static int _count;
}; 
int Person::_count = 0;
class Student :public Person
{
protected:
	int _stunum;
};
int main()
{
	Person p;
	Student s;
	cout << &p._name << endl;
	cout << &s._name << endl;
	cout << &p._count << endl;
	cout << &s._count << endl;
	cout << Person::_count << endl;
	cout << Student::_count << endl;
	Person::_count++;
	cout << p._count << endl;
	cout << s._count << endl;
	return 0;
}

可以通过运行以上程序看出static成员在父类和子类中的关系,结果如(5)所示:

(5)

可以看出,普通成员变量_name在父类和子类的地址是不一样的,相当于在父类和子类中各有一份,而static修饰的成员_count在父类和子类的地址是一样的,即父类和子类共享同一份_count,可知_count并不为父类和子类所独享,只是受类访问限定符的限制,int Person::_count=0,将_count初始化为0,则Student的_count也会被初始化为0,Person::_count++,则Person和Student的_count都会随之++,结果都为1,可以看出Person和Student的_count成员是同一份的,二者地址是相同的。

10、多继承

(1)定义

一个子类有两个或两个以上直接父类时,这种继承关系称为多继承,多继承对象在内存中的模型是,先继承的父类在前面,后继承的父类在后面,子类成员放在最后面。

(2)菱形继承

class Person
{
public:
	Person(const char*name)
		:_name(name)
	{ }
	string _name;
	int _age;
	int _tel;
	string _gender;
	string _address;
};
class Student :public Person
{
public:
	Student(const char*name,int num=0)
		:Person(name)
		,_num(num)
	{ }
protected:
	int _num;
};
class Teacher :public Person
{
public:
	Teacher(const char*name, int id=1)
		:Person(name)
		,_id(id)
	{ }
protected:
	int _id;
};
class Assistant :public Student, public Teacher
{
public:
	Assistant(const char*name1,const char*name2,const char*name3)
		:Student(name1)
		,Teacher(name2)
		,Person(name3)
	{ }
protected:
	string _majorcourse;
};

在多继承中,菱形继承是其中的一种特殊情况,菱形继承会产生数据冗余及二义性,class Assistant:pubic Student,public Teacher,这时Assistant同时继承Student、Teacher两个类,Student、Teacher都继承了Person类,那么Person在Student、Teacher中都各有一份,则在Assistant中就有两份Person,可以看出Person在Assistant中就产生了冗余、二义性,所以此时在Assistant中就不能直接访问Person的成员,如_name

    Assistant a;
	a._name = "Kzy";

a._name会产生歧义,不能确定是Student还是Teacher的_name,因此不能直接访问_name,可以指明作用域访问:

    a.Student::_name = "Kzy";
	a.Teacher::_name = "Yzk";

如a.Student::_name="Kzy",可以解决二义性问题,但数据冗余问题还是没有得到解决,对此C++引入了虚继承的概念来解决菱形继承的数据冗余、二义性问题。

(3)菱形虚拟继承

class Person
{
public:
	Person(const char*name)
		:_name(name)
	{ }
	string _name;
	int _age;
	int _tel;
	string _gender;
	string _address;
};
class Student :virtual public Person
{
public:
	Student(const char*name,int num=0)
		:Person(name)
		,_num(num)
	{ }
protected:
	int _num;
};
class Teacher :virtual public Person
{
public:
	Teacher(const char*name, int id=1)
		:Person(name)
		,_id(id)
	{ }
protected:
	int _id;
};
class Assistant :public Student, public Teacher
{
public:
	Assistant(const char*name1,const char*name2,const char*name3)
		:Student(name1)
		,Teacher(name2)
		,Person(name3)
	{ }
protected:
	string _majorcourse;
};
int main()
{
	Assistant a;
	a._name = "Kzy";
    cout<<a._name<<endl;
	Assistant a1("Kzy","Yzk","K");
	cout << a1._name << endl;
	return 0;
}

virtual为虚继承的关键字,由于在Assistant中Person的成员产生了冗余,那么可以通过Student、Teacher对Person采取虚继承,这样Person的成员就不会产生冗余,Person成员在Assistant中只有一份,这样通过虚继承就解决了菱形继承的数据冗余及二义性的问题,在Assistant中_name就可以直接访问了,如a._name="Kzy",虚继承会将父类的那部分成员从子类中分离出来,逻辑上可认为父类和子类是相独立的,如下图所示:

菱形继承

菱形虚拟继承

虚继承会将Person成员变量从Student、Teacher中独立出来,在初始化Assistant时编译器会单独调用Person构造函数初始化Person的成员变量

    Assistant(const char*name1,const char*name2,const char*name3)
		:Student(name1)
		,Teacher(name2)
		,Person(name3)
	{ }
    Assistant a1("Kzy","Yzk","K");
	cout << a1._name << endl;

Assistant a1("Kzy","Yzk","K"),则是选择name3来完成Person的初始化,即a1._name="K",结果如(6)所示:

(6)

可以看出菱形继承是较为复杂的,有了多继承就会有菱形继承,有了菱形继承就会产生数据冗余和二义性的问题,因此还需将菱形继承设计为菱形虚拟继承,显然过程就复杂了,因此在实践中也是不提倡设计出菱形继承。

(4)多继承内存分布

分布顺序为先继承的父类在前,后继承的父类在后,最后是子类的那部分成员。

class Base1
{
public:
	int _b1=1;
};
class Base2
{
public:
	int _b2=2;
};
class Derive :public Base1, public Base2
{
public:
	int _b3=3;
	int _b4 = 4;
};
int main()
{
	Derive d1;
	Base1* p1 = &d1;
	Base2* p2 = &d1;
	Derive* p3 = &d1;
    cout << p1 << endl;
	cout << p2 << endl;
	cout << p3 << endl;
	return 0;
}

Derive先继承Base1、后继承Base2,则可知p1、p2、p3在内存中的位置为:

Base1在前,Base2在后,最后是Derive的那部分成员,故可知p1==p3!=p2,如(7)所示:

(7)

11、继承&组合

public继承是一种is-a的关系,即每个子类对象都是一个父类对象,而组合是一种has-a的关系,假设B组合了A,则每个B对象中都有一个A对象,继承允许根据父类的实现来定义子类的实现,父类的内部细节对子类是可见的,继承一定程度破坏了父类的封装,父类和子类的依赖关系强,耦合度高,组合则是继承之外的另一种复用方式,组合要求被组合的对象有良好定义的接口,组合不关心被组合对象内部是如何实现的,组合类之间没有很强的依赖关系,耦合度低,继承和组合两种情况都允许实现,优先考虑组合实现,例如实现Stack,既可以通过继承vector类模板实现,也可以通过组合vector来实现,但实践中通常通过组合vector来实现Stack。

组合实现Stack:

    template<class K, class container=vector<K>>
	class stack
	{
	public:
		void push(const K& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		const K& top() const
		{                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		container _con;
	};

继承实现Stack:

    template<class K>
	class Stack :public container<K>
	{
	public:
		void push(const K& x)
		{
			container<K>::push_back(x);
		}
		void pop()
		{
			container<K>::pop_back();
		}
		const K& top()
		{
			return container<K>::back();
		}
		bool empty()
		{
			return container<K>::empty();
		}
	};

三、多态

1、定义

多态分为编译时多态和运行时多态,编译时多态也称为静态多态,函数重载和函数模板就是编译时多态,它们通过传不同类型的参数在编译时调用不同的函数,通过参数不同达到多种形态,运行时多态就是同一个函数,通过传不同的对象就会完成不同的行为,从而达到多种形态,下面我们主要围绕运行时多态展开介绍。

2、虚函数

(1)定义

类成员函数前加virtual修饰,则这个成员函数为虚函数,非成员函数不能加virtual修饰。

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

(2)重写

虚函数的重写:派生类中有一个跟基类完全相同的虚函数,满足"三同",即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,则称派生类的虚函数重写了基类的虚函数。

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

由重写的"三同"条件可知,子类Student的虚函数Buyticket重写了Person的Buyticket。

需要注意的是在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也构成重写,因为基类的虚函数被子类继承了下来,在派生类中依旧保持虚函数属性,但实战中还是建议加上virtual。

3、条件

多态是一个继承关系下的类对象,去调用同一函数,产生了不同的行为,比如Student继承了Person,Person对象买票全价,Student对象优惠买票。

实现多态有两个重要条件:

(1)必须是基类的指针或引用调用虚函数,因为只有基类的指针或引用才能既指向基类对象又能指向派生类对象。(2)派生类必须对基类的虚函数进行重写,只有重写了,派生类才能有不同的函数,从而实现多态。

#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)
void func(Person* ptr)
{
	ptr->Buyticket();
	//p.Buyticket();
}
int main()
{
	Person p;
	Student s;
	//func(p);
	func(&p);
	//func(s);
	func(&s);
	return 0;
}

func函数参数为基类指针ptr,且Student完成了对虚函数Buyticket的重写,通过基类指针ptr调用重写的虚函数Buyticket,满足了多态条件,从而实现了多态,此时Person指针ptr调用Buyticket时,调用Person还是Student的Buyticket函数与ptr没关系,而是由ptr指向的对象决定,若ptr指向Person,则调用Person的Buyticket,若指向Student,则调用Student的Buyticket,当然func函数参数为基类Person的引用时,同样也可构成多态,结果如(8)所示。

(8)

4、经典多态面试题

class A
{
public:
	virtual void func(int val=1)
	{
		std::cout << "A->" << val << std::endl;
	}
	virtual void test()
	{
		func();
	}
};
class B :public A
{
public:
	void func(int val = 0)
	{
		std::cout << "B->" << val << std::endl;
	}
};
int main(int c, char*ch1[])
{
	B* p = new B;
	p->test();
	p->func();
	return 0;
}

分析:p->test(),test被B继承下来,但本质上test还是在基类A中,只是允许B访问test,故p->test(),是访问A的test,由于test的this指针为A*类型,故相当于将p赋值给A*指针,故此时p的指针类型为A*,test内部调用func,可以看出func满足虚函数的重写条件,通过基类指针p调用重写的虚函数,满足多态的条件,故调用p所指向的对象B的func,还需要注意的一点的是虚函数的重写本质上是重写虚函数的实现,故A的func的函数声明没有被重写,val缺省值为A的缺省值1,故结果为B->1,p->func(),就不满足多态的条件了,并没有将p的指针类型转化为A*,故p->func(),直接调用B的func,由于没涉及到多态,也就没涉及到虚函数的重写问题,故结果为B->0,如(9)所示:

(9)

5、协变

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

class A
{};
class B:public A
{};
class Person
{
public:
	virtual A* Buyticket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
class Student :public Person
{
public:
	virtual B* Buyticket()
	{
		cout << "买票-半价" << endl;
		return nullptr;
	}
};
void func(Person* ptr)
{
	ptr->Buyticket();
}
int main()
{
	Person p;
	Student s;
	func(&p);
	func(&s);
	return 0;
}

A与B为继承关系,A为基类,B为子类,基类Person中的虚函数Buyticket返回基类A的指针,子类Student中的虚函数Buyticket返回子类B的指针,这样基类虚函数返回基类A的指针,子类虚函数返回子类B的指针,虽然返回类型不同,但也实现了虚函数的重写,称为协变,结果如(10)所示。

(10)

6、析构&多态

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,这是由于编译器对析构函数的名称做了特殊处理,析构函数的名称统一处理成destructor,那么如果基类的析构函数加了virtual修饰,派生类的析构函数就构成重写。

class A
{
public:
	virtual~A()
	{
		cout << "~A()" << endl;
	}
};
class B:public A
{
public:
	virtual ~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

这里需要注意的是,基类的析构函数建议设计成虚函数,若~A()不加virtual修饰,那么就不构成重写,则delete p2时只会调用A的析构函数,没有调用B的析构函数,由于~B()中存在释放资源,那么就会导致内存泄漏问题,所以基类的析构函数建议设计为虚函数。

7、override、final

​
class car
{
public:
	virtual void drive()
	{ }
};
class Benz :public car
{
public:
	virtual void drive()override
	{
		cout << "Benz" << endl;
	}
};

​

由于C++对虚函数的重写要求比较严格,故检查起来也较为麻烦,对此C++11提供了override关键字用于检测虚函数是否构成重写,如virtual void drive() override。

class car
{
public:
	virtual void drive()final
	{ }
};

若不想让派生类重写虚函数,可以使用final修饰虚函数,这样虚函数就不会被重写,如virtual void drive() final。

8、纯虚函数&抽象类

class car
{
public:
	virtual void drive() = 0;
};

在虚函数后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现,只要声明即可,包含纯虚函数的类叫作抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,则派生类也是抽象类,纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

class car
{
public:
	virtual void drive() = 0;
};
class Benz :public car
{
public:
	virtual void drive()
	{
	  cout << "Benz" << endl;
	}
};
class BMW :public car
{
public:
	virtual void drive()
	{
		cout << "BMW" << endl;
	}
};
int main()
{
	//car c;
	Benz b;
	car* c1 = new Benz;
	car* c2 = new BMW;
	c1->drive();
	c2->drive();
	return 0;
}

car中有纯虚函数drive,则car为抽象类,因此car不能实例化出对象,Benz、BWM继承car,并重写了drive,因而可以实例化出对象,c1、c2均为基类指针,且调用重写的虚函数drive,满足多态的条件,结果如(11)所示。

(11)

9、虚表指针

先来运行下面这段代码:

class Base
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
protected:
	int _b=1;
	char _ch='x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

(12)

结果如(12)所示,通过监视窗口可以看出,除了_b和_ch成员,还多了一个_vfptr放在对象的前面,对象中的这个指针称为虚函数表指针,一个含有虚函数的类中都至少都有一个虚函数表指针,这个类所有虚函数的地址都要放到这个类对象的虚函数表中,虚函数表也简称为虚表。

类对象中的虚函数表正是实现多态的重要原因,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

Person为基类,Student为派生类,当基类指针指向Person对象时,调用的就是Person虚表的虚函数,当基类指针指向Student对象时,调用的就是Student虚表的虚函数。

10、虚函数表

虚函数表本质是一个存虚函数指针的指针数组,基类对象的虚函数表中存放基类所有虚函数的地址,派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,派生类就不会再生成虚函数表指针,但继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个。

class Base
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func3()
	{
		cout << "func3()" << endl;
	}
protected:
	int _a1 = 1;
};
class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "void func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func4()
	{
	   cout << "func4()" << endl;
	}
protected:
	int _b1 = 2;
};
int main()
{
	Base b1;
	Base b2;
	Derive d;
	return 0;
}

派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址,如派生类Derive重写了基类Base的虚函数func1、func2,通过监视窗口可以看出Derive对象d的虚表数组的func1、func2的虚函数地址进行了重写,如(13)所示:

(13)

派生类的虚函数表中包含基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址,通过(13)还可以看出同类型对象b1、b2的虚表共用,虚函数地址是一样的,不同类型对象虚表是各自独立的,如b1、d的虚表是不同的。

虚函数与普通函数一样,编译后是一段指令,故存放在常量区(代码段),只是虚函数的地址又存放到了虚表中,可以通过汇编层的角度观察多态调用与非多态调用的区别:

class Base
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func3()
	{
		cout << "func3()" << endl;
	}
protected:
	int _a1 = 1;
};
class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "void func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func4()
	{
	   cout << "func4()" << endl;
	}
protected:
	int _b1 = 2;
};
//多态
void func(Base* ptr)
{
	ptr->func1();
}
//非多态
void func6(Base* ptr)
{
	ptr->func3();
}
int main()
{
	Base b1;
	Base b2;
	Derive d;
	Base* p1 = &d;
	func(p1);//多态
	func6(p1);//非多态
	return 0;
}

由多态的条件可知,func中基类指针调用重写的虚函数func1,故func满足多态的条件,而func6中func3并不是虚函数,故func6不满足多态的条件,ptr->func1()为多态调用,通过汇编层观察ptr->func1(),如(14)所示:

(14)

从汇编层的角度可以看出多态调用是通过call指令来实现的,call eax,即调用ptr所指向对象的func1虚函数地址,同理可观察ptr->func3()非多态调用的汇编层指令,如(15)所示:

(15)

可以看出非多态调用就与ptr所指向的对象类型无关了,只与ptr的类型有关,汇编层的指令为call Base::func3,即调用Base的func3,与ptr指向的对象类型无关,只与ptr本身的类型有关。

虚函数表存放在内存的哪个区域,C++标准并没有规定,VS环境下是存放在常量区(代码段),运行以下代码可以观察虚函数表在内存中的位置:

class Base
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func3()
	{
		cout << "func3()" << endl;
	}
protected:
  int _a1 = 1;
};
class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "void func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func4()
	{
	   cout << "func4()" << endl;
	}
protected:
	int _b1 = 2;
};
int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* ch = "Kzy";
	printf("栈:%p\n", &i);
	printf("堆:%p\n", p1);
	printf("静态区:%p\n", &j);
	printf("常量区:%p\n", ch);
	Base b1;
	Base* p2 = &b1;
	Derive d1;
	Derive* p3 = &d1;
	printf("Base虚表地址:%p\n", *(int*)p2);
	printf("Derive虚表地址:%p\n", *(int*)p3);
	printf("Base虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func3);
	return 0;
}

虚表指针32位环境下为4个字节,存放在对象的前4个字节位置,观察虚表地址,可以通过将类对象指针强制类型转化为int*指针,再解引用就可访问虚表地址,*(int*)p2即访问Base的虚表地址,*(int*)p3即访问Derive的虚表地址,结果如(16)所示:

(16)

可以看出虚表地址与常量区地址比较接近,故VS环境下可以看出虚表地址存放在常量区,虚函数与普通函数一样,也存放在常量区。

四、结语

继承、多态是C++面向对象编程的两大重要特性,继承是类层次的复用,派生类将在继承原有类的基础上进行扩展,派生类很好地复用了基类的成员,不需要重复设计基类成员,但派生类与基类的依赖关系强,耦合度高,基类的改变将影响派生类的实现,此外,允许多继承是C++的一个缺陷,多继承必然会有菱形继承,就会产生数据冗余及二义性,就须将菱形继承设计为菱形虚拟继承,过程就显得比较复杂,因此实战中不提倡设计出菱形继承,多态则是同一个函数,通过传不同的对象来实现不同的功能,也称为运行时多态,实现多态有两个重要条件,第一必须是基类的指针或引用去调用虚函数,第二则是该虚函数完成了重写,虚函数的重写才能实现多种形态,虚函数调用的本质也是通过虚表指针来实现的,一个类所有虚函数的地址都会放在该类的虚表中,通过该类的虚表指针来实现虚函数的调用,这也是多态实现的原理。将继承、多态两者相结合,类似于1+1>2原理,两者正是面向对象的"瑞士军刀",是面向对象编程和建模的不二选择,为C++的高性能运行提供了强大的支撑!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐