C++多态

文章目录

1.多态的概念

1.1 概念 多态的概念

2.多态的分类

2.1静态多态

2.2动态多态

3.多态的定义及实现

3.1多态构成的条件

 3.2虚函数

3.3虚函数的重写

虚函数重写的两个例外

a.协变

b.析构函数的重写

4.C++11 override 和 final

4.1.final             

4.2.override

4.3 重载、覆盖(重写)、隐藏(重定义)的对比

5.抽象类

6.多态的原理

6.1虚函数表

6.1.1虚函数表指针

6.1.2虚函数表

6.2虚函数表的继承

6.3.虚函数表的观察方法。

6.3.1虚函数的内存观察

6.3.2虚函数表地址打印

6.3.3虚函数表的位置

7.多态的底层过程

8.解决前面的问题。

8.1.虚表中函数是公用的吗?

8.2.为什么必须传入指针或引用而不能使用对象?

8.3.为什么私有虚函数也能实现多态?


1.多态的概念

1.1 概念 多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。举个例子,比如我们世界通用的货币是美元,亚洲也是如此,但是中国主要通用的货币是人民币。

2.多态的分类

2.1静态多态

函数重载,看起来调用同一个函数有不同行为。 静态:原理是编译时实现。

比如我们写的函数构造,cout这个来调用<<时。虽然都是调用的<<,但是却是调用不同被重载后的函数。

int i=1;
double d=1.1;
cout<<i<<endl;
cout<<d<<ednl;

2.2动态多态

一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理运行时实现。

这个就是我们今天所要学习的重点了。具体的例子看下面的代码。

3.多态的定义及实现

3.1多态构成的条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

那么在继承中要构成多态还有两个条件:

1. 必须通过基类的指针或者引用调用虚函数 。

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

class Asia
{
public:
	virtual void UseMoney()
	{
		cout << "美元" << endl;
	}
};

class China :public Asia
{
public:
	virtual void UseMoney()
	{
		cout << "人民币" << endl;
	}
	
};

void func(Asia& as)
{
	as.UseMoney();
}
    Asia a;
	func(a);
	China c;
	func(c);

 我们这里看到,子类的China继承了父类Asia。运行的打印结果如上图。我们看到上面的函数func()的形参是用的父类Asia的引用或者指针都行。来接受不同类型的对象,然后都调用了as.UseMoney(),最后都调用了不同类的UseMoney()。如果我们不传指针或者引用那么将不会构成多态。

 3.2虚函数

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

virtual void UseMoney()
	{
		cout << "美元" << endl;
	}

注意:1.普通的函数不能是虚函数,只有类里面的函数才能是虚函数。

2.静态成员函数不能加virtual。

3.3虚函数的重写

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

lass Asia
{
public:
//父类的虚函数
	virtual void UseMoney()
	{
		cout << "美元" << endl;
	}
};

class China :public Asia
{
public:
//子类的虚函数
	virtual void UseMoney()
	{
		cout << "人民币" << endl;
	}
	
};

虚函数重写的两个例外

a.协变

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

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

class Asia
{
public:
	//virtual Asia* UseMoney()
	virtual A* UseMoney()
	{
		cout << "美元" << endl;
		return nullptr;
	}
};

class China :public Asia
{
public:
	virtual B* UseMoney()
	//virtual China* UseMoney()
	{
		cout << "人民币" << endl;
		return nullptr;
	}
	
};

简单说就返回父子关系的指针或者引用

b.析构函数的重写

class Asia 
{
public:
	virtual void UseMoney()
		//virtual A* UseMoney()
	{
		cout << "美元" << endl;
	}

	 ~Asia()
	{
		cout << "~Asia()" << endl;
	}
};

class China :public Asia
{
public:
	virtual void UseMoney()override
    //virtual B* UseMoney()
	{
		cout << "人民币" << endl;
	}

	 ~China()
	{
		cout << "~China()" << endl;
	}

};	
    Asia a;
	China c;

 这里我们发现,这里的China会自动调用子类的析构函数,然后调用父类的析构函数,但是没有构成多态。

1.如果基类的析构函数为虚函数,此时子类的析构函数无论加不加virtual,都是对父类的析构函数的重写。
2.虽然子类和父类的析构函数的函数名不同,但其实编译器对析构函数的名称进行了特殊的处理,都处理成了destructor。

class Asia 
{
public:
	virtual void UseMoney()
		//virtual A* UseMoney()
	{
		cout << "美元" << endl;
	}

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

class China :public Asia
{
public:
	virtual void UseMoney()override
    //virtual B* UseMoney()
	{
		cout << "人民币" << endl;
	}

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

};	
    Asia*a1 = new Asia;
	Asia*a2 = new China;

	delete a1;
	delete a2;

这里的结果和上图一样的。

构成多态的结果是,Asia*类型的a1和a2,接收两个不同类型的对象即Asia类型和China类型,在调用析构函数的时候可以分开调用(子类对象调用子类的析构函数,父类对象调用父类的析构函数。)

当析构父类对象时,调用父类的析构函数,当析构子类对象时,调用的是子类的析构函数和父类的析构函数。
如果我们不使用父类指针进行管理,而是使用对象来接收子类对象呢

class Asia 
{
public:
	virtual void UseMoney()
		//virtual A* UseMoney()
	{
		cout << "美元" << endl;
	}

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

class China :public Asia
{
public:
	virtual void UseMoney()override
    //virtual B* UseMoney()
	{
		cout << "人民币" << endl;
	}

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

};	
    Asia *a1 = new Asia;
	China *a2 = new China;

	delete a1;
	delete a2;

 在析构p3的时候,并没有根据按Student类的规则来进行析构。
同时,当我们将派生类的virtual去掉的时候,仍然可以构成多态,这与底层原理有关,在下面的介绍中会提及。为了统一性,不建议将virtual拿掉,C++大佬为了防止发生不必要的内存泄漏,所以设置了这一规则。这就导致所有的其实派生类的所有虚函数virtual都可以省略。这是由于其继承了基类的virtual属性,具体的还要在底层去理解,再强调一遍,尽量不要在派生类中省略virtual。

4.C++11 override 和 final

4.1.final             

final的作用是限制类不能被继承。如果我们像设计一个类不能被继承,需要把父类的构造函数设为私有的(这是因为子类需要父类的构造函数来初始化)。

final提供了另外一种方式来限制一个类不能被继承。只需要在类的构面加一个final。

class Asia final
{
public:
	virtual void UseMoney()
		//virtual A* UseMoney()
	{
		cout << "美元" << endl;
	}

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

这样子类如果继承就会发生错误。

	virtual void UseMoney()final
		//virtual A* UseMoney()
	{
		cout << "美元" << endl;
	}

如果是加在父类的虚函数后面,子类将无法重写。

4.2.override

将override放在子类的重写的虚函数后面,判断是否完成重写。

virtual void UseMoney()override
    //virtual B* UseMoney()
	{
		cout << "人民币" << endl;
	}

总结:final是在父类的位置用,override是在子类用的     

4.3 重载、覆盖(重写)、隐藏(重定义)的对比

5.抽象类

在虚函数的后面写上 =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;
	}
};
Car c;

如果用抽象类定义不同对象,那么就会报错,因为抽象类不能实例化出对象。

Car* c = nullptr;
c->Drive();

如果这样程序会奔溃。为什么呢?6.1.2虚函数表会说!!!

    Car* pBenz = new Benz;
	pBenz->Drive();

只有这样实例化的Car,去调用才不会奔溃。因为子类Benz重写了父类的纯虚函数,再去调用函数才会有用。我么不能用父类来定义对象,但我们可以用父类指针来接受子类对象,同时调用函数。

总结:抽象类的存在本质上来说就是希望我们在派生类中重写父类的虚函数。抽象类中的虚函数一般只声明,不实现,因为没有意义。

6.多态的原理

6.1虚函数表

6.1.1虚函数表指针

class A
{
public:
	virtual void text()
	{
		cout << "text" << endl;
	}
    virtual void text1()
	{
		cout << "text" << endl;
	}
    void test2()
	{
		cout << "text2" << endl;
	}


private:
	int _a;
	char c;
};
    A a;
	cout << sizeof(a) << endl;

我们经过发现这里的sizeof是8字节。但是打印的结果是

 这说明带有虚函数的类所定义的对象中,除了成员变量之外还有其他东西加进去了。

 我们发现这里的对象多了一个_vfptr,这就是虚函数表指针。

6.1.2虚函数表

还是上面的图,我们发现_vfptr下面有两个地址分别是类中两个虚函数的地址,构成的虚函数表。所以虚函数表是一个指针数组,数组中的每个元素是虚函数的地址。

    A* a =nullptr;
	a->text();//程序会奔溃
	a->test2();

我们在调用普通函数时和虚函数时,他们本质是不同的。

因为a在调用text2()的过程没有发生解引用操作,非虚函数在公共代码段中,可以直接调用。而a在调用text1()时,需要虚函数表中的指针来找到text1(),但是虚函数表指针需要对a进行解引用操作,而a是nullptr的,所以程序会奔溃。

所以总结一下:成员函数是存放在公共代码段,其实 虚函数也是一样的存储在公共代码段,但是寻找虚函数时需要通过虚函数表来确定位置。普通函数是可以直接确定位置的。

6.2虚函数表的继承

class A
{
public:
	virtual void text1()
	{
		cout << "text1" << endl;
	}
	virtual void text2()
	{
		cout << "text2" << endl;
	}

	void test3()
	{
		cout << "text3" << endl;
	}
private:
	int a;
	char c;
};

class B:public A
{
public:
	virtual void text1()
	{
		cout << "text1" << endl;
	}
	virtual void test4()
	{
		cout << "test4()" << endl;
	}
private:
	int b;
};
	A a;
	B b;

我们通过调试,a和b的值。

 这里我们可以看到,a中_vfptr[0]和_vfpty[1]很显然是虚函数text1和text2。而_vfptr[0]和_vfpty[1]他是指针他里面存的是这两个函数的地址。而b中的_vfptr[1]我们可以通过地址看出来这是继承的父类的。但是_vfptr[0]却和父类的text1不一样,这是为什么呢?我们观察发现text1()是重写的父类的,在拷贝了父类的地址后又进行了覆盖。因此重写从底层来说又叫覆盖

这时又有一个问题,我们子类里面写的虚函数test4()却没有在虚函数表中。这时为什么呢?其实是写了的,但是VS的监视窗口不能看到,可以通过内存看到。

6.3.虚函数表的观察方法。

6.3.1虚函数的内存观察

我们通过发现在内存中输入虚函数表的地址,后面是两个虚函数的地址。

下面是子类的。

我们可以看到,这里不止存放了父类的虚函数的地址,还有重写的虚函数地址,还有一个就是自己子类虚函数的地址,这也说明了VS的监视窗口看不到。

6.3.2虚函数表地址打印

通过观察内存,对于单继承来说,我们只需要打印对象的首元素的地址即可找到虚表,并进行打印。

我们发现对象的前四个字节存储的就是虚表的地址。可以通过这一点来打印虚表。
 

typedef void(*vfptr)();
void Printvfptr(vfptr* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("%d:%p\n", i, table[i]);
	}
	cout << endl;
}
int main()
{

    A a;
	B b;
	Printvfptr((vfptr*)*(void**)&a);
	Printvfptr((vfptr*)*(void**)&b);
	return 0;
}

下面来解释一下如何打印的虚表,分为两部分,一部分是函数,一部分是传参:

函数

首先我们明确,虚函数指针是一个函数指针,因此为了简便我们可以将函数指针重命名为vfptr。
通过接收虚表指针,并依次打印指针数组中的内容(虚函数的地址)。

传参

拿父类对象a举例,我们要找到a的前四个字节的内容,即为虚表指针,然后再传入函数中。
首先使用(void**)对a的地址进行强制类型转换,这其中发生了切割。使用(void**)的原因在于,由于不知道是使用的32位还是64位系统,但我们可以通过指针的大小来判断。首先将&a转换成一个指针,再将其转换成一个指针类型,再进行解引用就得到了a的前4或者8个字节。但同时我们需要传递的是一个vfptr类型的函数指针,所以还需要进行(vfptr*)类型的强制转换。

有了前面的解释,我们就可以理解打印虚表的原理了,我们把这段代码运行一下:

 上面的是虚函数表a的地址,下面是b的。

6.3.3虚函数表的位置

    A a;
	B b;
    int* p = (int*)malloc(4);
	printf("堆:%p\n", p);
	int a1 = 0;
	printf("栈:%p\n", &a1);
	static int b1 = 1;
	printf("数据段:%p\n", &b1);
	const char* c = "aaa";
	printf("常量区:%p\n", &c);
	printf("虚表:%p\n", *(void**)&a);
	return 0;

 我们发现虚表的位置在数据段和常量区之间。大致属于数据段。

7.多态的底层过程

class Asia
{
public:

	virtual void UseMoney()
	//virtual A* UseMoney()
	{
		cout << "美元" << endl;
	}

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

class China :public Asia
{
public:
	//virtual B* UseMoney()
	virtual void UseMoney()
	{
		cout << "人民币" << endl;
	}

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

void func(Asia& as)
{
	as.UseMoney();
}

int main()
{
	Asia a;
	func(a);
	China c;
	func(c);
}

我们还使用这一段代码来举例,首先复习一下多态:使用父类的指针或者引用去接收子类或者父类的对象,使用该指针或者引用调用虚函数,调用的是父类或子类中不同的虚函数。
下面来分析原理:
父类对象原理:
首先用父类引用p来接收父类对象per,此时p中的虚表和per中的虚表一模一样,只需要访问__vfptr中的BuyTicket()的地址即可调用该函数。
子类对象的原理:
用p来接收子类对象std,发生切片处理,会将子类中的虚表内容拷贝到父类引用p中,然后再调用其中的__vfptr中的BuyTicket地址。此时的p不是新创建了一个父类对象,而是子类对象std切片后构成的,其中就将重写之后的BuyTicket()的地址也随之切入了p。可以把p看成原std的包含__vfptr的一部分。
总结:基类的指针或者引用,指向谁就去谁的虚函数表中找到对应位置的虚函数进行调用。

8.解决前面的问题。

8.1.虚表中函数是公用的吗?

虚表中的函数和类中的普通函数一样是放在代码段的,只是虚函数还需要将地址存一份到虚表,方便实现多态。这也就说明同一类型的不同对象的虚表指针是相同的,我们还可以通过调试观察:

A a;
A aa;

相同类型的对象, 我们可以看到a和aa的虚函数表是一样的,所以说虚函数表是存在公共区域的。

8.2.为什么必须传入指针或引用而不能使用对象?

当我们使用父类对象去接收时,父类对象本身就具有一个虚表了,当子类对象传给父类对象的时候,其他内容会发生拷贝,但是虚表不会,C++这样处理的原因在于,如果虚表也会发生拷贝的话,那么该父类对象的虚表就存了子类对象的虚表,这是不合理的。
我们同样可以通过调试来进行观察:

void func(Asia as)
{
	as.UseMoney();
}

int main()
{
	Asia a;
	func(a);
	China c;
	func(c);
}

 这是c中的虚表内容,而且在调试过程中,程序是进入父类中进行调用函数的。

8.3.为什么私有虚函数也能实现多态?

这是因为编译器调用了父类的public接口,由于是父类的引用或者指针,因此编译器发现是public之后就不再进行检查了,只要在虚表中可以找到就能调用函数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值