C++专题:多态性与虚函数(详细!)

目录

多态性的概念

虚函数

动态联编(晚绑定)和静态联编(早绑定)

覆盖(重写),重载,隐藏

成员函数覆盖(重写)

成员函数重载

成员函数隐藏(重定义)

纯虚函数

抽象类

虚析构函数

限制构造函数


多态性是面向对象程序设计的重要特征之一。多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。多态性的应用可以使编程显得更简洁便利,它为程序的模块化设计又提供了一种手段。

多态性的概念

所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”,程序在运行时才决定调用哪个方法,是面向对象编程的核心概念。

我们先来看一段代码:

#include<iostream>
using namespace std;
class Base{
	public:
		void priMsg(){
			cout << __func__ << " line: " << __LINE__ << endl;//此处是13行
		}
};
class Subclass : public Base{
	public:
		void priMsg(){
			cout << __func__ << " line: " << __LINE__ << endl;
		}
};
void test(Base *p)
{
	p->priMsg();
}
int main(int argc, char *argv[])
{
	Base obj1;
	Subclass obj2;

	test(&obj1);
	test(&obj2);
    return 0;
}

可以看到我们在基类和派生类中都有同名的函数,我们在test函数形参定义一个基类的指针,接收基类对象obj1和派生类对象obj2,函数运行结果如何呢?是会分别调用基类和派生类的函数吗?

 这是因为我们定义的指针类型是基类Base,派生类向上隐式转换成为基类。

那么我们要如何实现用一个接口来调用基类和派生类里两个同名的函数呢??这就是我们接下来要讲到的虚函数。

虚函数

虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:

virtual 返回类型 函数名(形参表) {
    函数体
}

在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

注意虚函数的设置条件:

虚函数设置条件:
 *        1、非类的成员函数不能设置 为虚函数。
 *        2、类的静态成员不能定义 为虚函数。
 *        3、构造函数不能定义为 虚函数, 但是析构函数却能设置为  虚函数。
 *        4、成员函数声明时需要使用 Virtual关键字修饰,定义时不需要。
 *        5、基类成员函数设置为 虚函数, 那么派生类中同名函数(函数名、形参类型、个数、返回值类型全一样)自动成为虚函数。
 *  

看一段代码:


#include<iostream>
using namespace std;
class Base{
	public:
		virtual void priMsg(){
			cout <<“Base”<< __func__ << " line: " << __LINE__ << endl;
		}
		int priMsg(int x){}
};
class Subclass : public Base{
	private:
		void priMsg(){
			cout << "sublcass"<<__func__ << " line: " << __LINE__ << endl;
		}
		int priMsg(int x){}
};
void test(Base *p)
{
	p->priMsg();
}
int main(int argc, char *argv[])
{
	Base obj1;
	Subclass obj2;
	test(&obj1);
	test(&obj2);
    return 0;
}

当我们将基类的同名函数定义为虚函数后,结果会和我们之前有什么差别呢?

可以看到基类和派生类的同名函数都被调用了,这是因为 基类的成员函数设置为虚函数,那么派生类中同名函数(函数名,形参类型,个数,返回值类型全一样)自动成为虚函数,派生类的虚函数会覆盖调基类的同名虚函数!!!

动态联编(晚绑定)和静态联编(早绑定)

静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。也称为早绑定

运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。也称为晚绑定。(只有晚绑定才是真正的多态)

那么C++是如何在运行时做到晚绑定的呢,动态联编引入了一个虚函数表的概念!下面我们来看这张图!

 对象空间的前四个字节(注意这是在32位系统中,64位系统中为前8个字节)存放了虚函数表的首地址,接着存放了其他成员,我们可以通过虚函数表的地址去访问到对应的虚函数。看下面这段代码:

#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
class Base{
	public:
		Base(int x, int y) : x(x), y(y){}
		virtual int getValue(){
			cout << __func__ << " line: " << __LINE__ << endl;
			return x;
		}
		virtual int getData(){
			cout << __func__ << " line: " << __LINE__ << endl;
			return 0;
		}
	private:
		int x, y;
};
typedef int (*PFUNC)();
int main(int argc, char *argv[])
{
	Base obj(23, 1);
	/*
	 * &obj: 得到对象首地址 Base*
	 * (int *)&obj : 将地址 Base* 强转为 int *
	 * *(int *)&obj: 将int *地址上的前 4字节内容取出,得到int类型的 16进制数值
	 * *(long *)&obj: 将long *地址上的前 8字节内容取出,得到 long类型的 16进制数值
	 * *(char *)&obj: 将char *地址上的前 1字节内容取出,得到 char类型的 16进制数值
	 *
	 * 32位机中,含有虚函数的类对象 头4字节存储 虚函数表首地址
	 * 64位机中,含有虚函数的类对象 头8字节存储 虚函数表首地址
	 * */
	cout << sizeof(int) << " " << sizeof(int *) << endl;
	long v = *(long *)&obj;
	PFUNC p = (PFUNC)(*(long *)v);
	p(); 
	p = (PFUNC)(*((long *)v+1));
	p();
	// int *p;   p+3 = p+3*sizeof(int);
	cout << " " << *((int *)&obj+2) << endl;
	cout << " " << *((int *)&obj+3) << endl;

	//p = (PFUNC)*((int *)&obj + 3);
	//p();
	return 0;
}

运行结果:

 这里我们是在64位系统下运行的所以,在进行类型转换我们用了long型,这样才能取到前八个字节的虚函数表地址,如果用int只能取到前四个字节。

覆盖(重写),重载,隐藏

成员函数覆盖(重写)

成员函数覆盖(也称为重写):是指派生类重新定义基类的虚函数

        1.作用域不同(分别位于派生类和基类)

        2.函数名字相同

        3.参数相同

        4.返回值相同

        5.基类函数必须有virtual关键字,不能有static

        6.重写函数的权限访问限定符可以不同

成员函数重载

是指函数名相同,参数不同(数量,类型,次序)

        1.相同的范围,在同一个作用域内

        2.函数名字相同

        3.参数不同

        4.返回值可以不同

        5.virtual关键字可有可无

成员函数隐藏(重定义)

1.不在同一个作用域

2.函数名字相同

3.返回值可以不同

4.参数不同时:此时不论有无virtual关键字,基类的函数将被隐藏

5.参数相同时,但基类没有virtual关键字,此时基类的函数被隐藏

我们来看一个例子具体说明他们三个之间的差别:

#define TRACE
    cout<<typeid(this).name()<<":"<<__func__<<":"<<__LINE__<<endl;
class Base{
    public:
        virtual void func(void)  {TRACE();}
        int func(int val)  {TRACE();}
};
class Inherit:public Base{
    public:
        void func(void) {TRACE();}
        int func(int val)  {TRACE();}
};
int main()
{
    Inherit obj;
    obj.func();
    obj.func(1);
    return 0;
}

说明:

1.派生类Inherit和基类Base都各自重载了func成员函数(在同一个作用域,参数不同返回值不同)

2.派生类Inherit的成员函数void func(void)覆盖了基类的成员函数void func(void)(在不同作用域,函数参数返回值都一样,且必须有virtual关键字)

3.派生类Inherit的成员函数int func(int)隐藏了基类的成员函数int func(int)(在不同作用域,参数相同,且没有virtual关键字)

4.派生类Inherit的成员函数 void func(void)隐藏了基类的成员函数 int func(int)(在不同的作用域,参数不同无论有无关键字virtual都被隐藏)

纯虚函数

纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:

virtual 函数类型 函数名(参数表) = 0;

声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。

抽象类

如果一个类至少有一个纯虚函数,那么就称该类为抽象类

#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
class Base{
	public:
		virtual void get() = 0;			//设置纯虚函数,该虚函数没有函数体,此时该类成为抽象类	
};
class Subclass : public Base{
	public:
		void get(){
			cout << "hello world" << endl;
		}
};
/*
 * 抽象类不能有实例,必须 派生出子类来使用;
 * 抽象类的子类中,必须 重写基类的纯虚函数;否则子类定义对象都是错的。
 *
 * */
int main(int argc, char *argv[])
{
	// Base obj;    //抽象类不能定义对象,不能有具体实例
	Base *p;
	Subclass obj2;
	Base &obj = obj2;
	return 0;
}

注意:

1.由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。
2.不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。
3.抽象类不能用作函数的参数类型、函数的返回类型或是显式转换的类型。
4.可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
5.如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

 

虚析构函数

如果在主函数中用new运算符建立一个派生类的无名对象和定义一个基类的对象指针,并将无名对象的地址赋值给这个对象指针,当用delete运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。由此我们有了虚析构函数,它可以完全回收派生类和基类资源

#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class Base{
	public:
		Base(){ pri(); }
		//虚析构函数,C++ 中常将 析构函数设置 虚函数。
		virtual ~Base(){ pri(); }
};
class Subclass : public Base{
	public:
		Subclass(){ pri(); }
		//派生类的析构函数自动成为 虚析构函数,覆盖 基类虚析构函数
		~Subclass(){ pri(); }
};

int main(int argc, char *argv[])
{
	Subclass *p = new Subclass;
	delete p;
	// 设置虚析构函数的目的是:当基类指针指向 派生类对象时,释放基类指针,能完全回收派生类和基类资源。
	Base *q = new Subclass;
	delete q;
	return 0;
}

 注意:虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数

限制构造函数

如果一个类的构造函数访问权限不是public,那么该类的构造函数就是限制构造函数

如果构造函数访问权限为保护,那么只能派生出子类,定义子类对象通过接口访问

#include<iostream>
using namespace std;
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class Base{
	protected:
		//构造函数权限不是public,那么这就是限制构造函数
		Base(){ pri(); }
	public:
		virtual ~Base(){ pri(); }
		void get(){ pri(); }
};
class Subclass : public Base{
	public:
		Subclass(){ pri(); }
		~Subclass(){ pri(); }
};

int main(int argc, char *argv[])
{
	// Base obj;   //限制构造函数不能创建实例,
	// 如果构造函数权限为 protected,那么只能派生出子类,定义子类对象来访问接口函数
	Subclass obj;
	obj.get();
	return 0;
}

如果构造函数访问权限为私有,那么只能通过友元函数:

#include<iostream>
using namespace std;
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class Base{
	private:
		//构造函数权限不是public,那么这就是限制构造函数
		Base(){ pri(); }
	public:
		virtual ~Base(){ pri(); }
		void get(){ pri(); }
		friend Base *getObj();
		friend void freeObj(Base *);
};
#if 0
class Subclass : public Base{
	public:
		Subclass(){ pri(); }
		~Subclass(){ pri(); }
};
#endif
Base *getObj() //友元成员函数,打破类的封装,在函数可以访问 类的保护和私有成员
{
	return new Base; //开辟堆区空间时,系统会调用Base类默认构造函数
}
void freeObj(Base *p)
{
	delete p;
}

int main(int argc, char *argv[])
{
//	Base obj;   //限制构造函数不能创建实例,
	// 如果构造函数权限为 private,那么只能设计友元成员函数打破private的限制

//  Subclass obj; //错误,因为基类构造函数为 private权限,而派生类不能访问基类的 私有成员。
//	obj.get();
	Base *p = getObj();
	p->get();
	freeObj(p);
	return 0;
}

  • 11
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值