重修c艹第三天

        emmm,今天先模一下继承和虚函数吧,第一次学的时候就没太弄明白,尽量吧~

       首先是灰常重要的继承,对于瓜同学这样的小菜鸡来说,至少就目前而言,很少用到,但毫无疑问继承很重要,继承做了一件事,它让类之间有了一个相互关联的结构(层级),我们可以先建一个基类,这个基类包含公共的基本功能,接下来我们就可以建基于基类的类了,可以在这个类里面包含新的东西,这个类我们就称之为子类,而本来的基类就是这个子类的父类,emmm,设想一下,你是一个父亲,你有孩子,你肯定希望你的孩子能继承到你的所有优点,是吧,但在此基础上,孩子可以更厉害,且每个孩子可以不同,有多的优点,此外肯定还是有些属性是不太一样的,像是你自己可能见了这个江湖的险恶,你不想让孩子看见,所以有private继承...先不说这个,后面讲可见性的时候再谈,目前瓜同学用的最多的还是直接public继承,排除可见性和继承方式这块,其实继承最最最本质的用途就是减少复制粘贴,让代码更convenient,更perfect。

       最基本的继承倒是没有什么多说的,除了后面的可见性可能对没有重新学习的瓜同学有点强度,但总的来说,就是将继承理解为子类对父类的扩展就OK,但在测试中我发现一件好玩的事情:

#include<iostream>
using namespace std;

class play
{
public:
	int a = 1;
	void an()
	{
		cout << "是父类中的an在输出" << a <<endl;
	}
};

class player :public play
{
public:
	int b = 2;
	void an()
	{
		cout << "是子类中的an在输出" << b << endl;
	}
};

int main()
{
	player p1;
	p1.an();
	return 0;
}
//最终输出:是子类中的an在输出2

也就是说,如果父类定义的东西在子类中被同名复现,那么父类的将被覆盖(隐藏),(其实这就是子类函数对父类同名函数的重写。(学半成归来瓜同学补充道))就连同名的重载都做不到,只能用子类中新定义的,查阅资料后发现:在C++中,子类是不能重载父类函数的,因为重载只发生在同一个类中。此外,还发现几个有趣的点:对于父类而言,子类的所有函数均是不可见的,在父类的成员中访问同名函数,访问的均为父类的同名函数。相应的,对于子类而言,父类中所有的同名函数都被隐藏了,在子类的成员中访问同名函数,访问的均为子类的同名函数。这样想嘛,孩子也有孩子的隐私,父亲看不了很正常,孩子读书也是为自己读,不能全靠爹,所以在相同的领域,孩子的成就也是自己的;孩子长大了,自己成家了,各家有本自己难念的经,所以各成员用在同名情况下都会用自己的函数,而隐藏“其他亲属”的同名函数。

       继承暂时告一段落(真的只是暂时QAQ),接下来是虚函数,之前这段没认真听(瓜同学太瓜了呜呜呜)下面开始从零补档。在重修虚函数之前,咱们先来重温一下多态,多态是指同类的对象对于相同的函数调用会产生不同的功能,这时候大家可能都会不自觉地想到函数重载,这就是编译时的多态,相同的还有运算符重载(如“&”可作取地址符,也可作引用符号)以及之前都还没接触到的模板;除了编译时的多态,还有运行时的多态,主要通过刚刚讲到的继承与马上要讲的虚函数来实现;要实现运行时多态需要满足三个条件:1、有虚函数;2、符合赋值兼容规则(在需要基类对象的任何地方都可以使用公有派生类的对象替代);3、得用指针或者引用去调用;简单有了个基础概念,咱们来讲虚函数吧!

       在讲第一个例子前,咱们需要先弄清一些东西,首先,虚函数必须存在于类的继承环境之中才有意义。下面是我们的第一个例子,我们首先定义一个父类play,再定义一个继承于此父类(父类也可称作基类,子类也可称作派生类,突然想起来好像没说这个,不然等会可能会有些迷乱,瓜同学的习惯太坏了呜呜呜的派生类,因为派生类是继承自此基类,所以可以算是一个类型,可以用‘=’来赋值,我们想要用player的an输出,所以我们这样做了(注意区别一下瓜同学在继承那块里面的例子,那个是派生类类型的定义,下面是基类类型的定义):

#include<iostream>
using namespace std;

class play
{
public:
	play(int b, int c)
	{
	}
	int a = 1;
	void an()
	{
		cout << "是父类中的an在输出" <<endl;
	}
};

class player :public play
{
public:
	int b = 2;
	player(int b, int c, int d) :play(a, b)
	{
	}
	void an()
	{
		cout << "是子类中的an在输出" << endl;
		play::an();
	}
};

int main()
{
	play p1(1,2);
	player pr(1,2,3);
	p1 = pr;
	p1.an();
	//这里我们把p2赋给了同类型(player继承自play)的p1想要
	//最后输出的是子类的an,但结果仍然是父类的an

	//相似的,还有:
	play* p2 = &pr;//父类的指针指向派生类对象
	p2->an();

	play &p3 = pr;//父类的引用等于派生类
	p3.an();

	//最后的结果都不是我们想要的子类an;
	return 0;
}

最终输出:

是父类中的an在输出
是父类中的an在输出
是父类中的an在输出

但最后咱们惊奇地发现,无论是直接赋值还是指针或者引用,最后都是调用基类的an(),太怪了。不急,咱们从原理来看,咱们想要最后用play定义的对象通过一定手段去实现用player的an,这不是多态吗,再康康我们之前提到的三个要求,噔噔咚,第一个就中枪了,我们没有定义虚函数,所以这里不能构成一个多态,相当于定义之初就已经锁死(静态绑定)了,就是父类,不能用子类的an了...既然知道了,咱们不妨先试试:

//在play类中在an前加上
//虚函数关键词virtual
class play
{
public:
	play(int b, int c)
	{
	}
	int a = 1;
	virtual void an()
	{
		cout << "是父类中的an在输出" <<endl;
	}
};
//建议在player中的an()前面也加一个virtual,更好看代码

现在的输出就是:

是父类中的an在输出 //直接赋值是不满足多态条件的哦,所以这里还是绑定了父类的an
是子类中的an在输出
是子类中的an在输出

 我们也就此利用虚函数实现了多态,emmm,刚刚在上面提到在定义之初就静态绑定力,从原理上讲,其实是静态联编,而我们的虚函数则是引入了动态联编,这是一种基于v表(虚函数表)来实现编译的东西,在虚函数里包含了基类中所有虚函数的映射,我们就可以基于此在运行时将它们映射到正确的派生类的重写函数中,此外,我们还可以在重写函数的参数表后面添加关键字override来提高代码的可读性(不这样做其实没有影响,流汗黄豆)。此外,虚函数也不是无缘无故地对咱们好,让我们多态(多么变态简称多态),实际上,它带来了两种额外开销,1是我们需要额外的内存来处理v表(包括基类中一个指向v表的指针),2是我们需要遍历v表来取得正确的映射,即派生类的重写函数,这也是为什么有的单片机用不了虚函数(但瓜同学目前接触到的板子像st的,51的都好像还没注意是不是这样)

...接下来咱们接着看虚析构函数(构造函数没有虚函数):

#include<iostream>
using namespace std;

class play
{
	int* a;
public:
	play()
	{
		a = new int;
		cout << "构造父类啦" << endl;
	}

	~play() 
	{
		delete a;
		cout << "父类析构啦" << endl;
	}
};

class player :public play
{
public:
	int *b;
	player()
	{
		 b = new int;
		 cout << "构造子类啦" << endl;
	}
	
	~player()
	{
		delete b;
		cout << "子类析构啦" << endl;
	}
};

int main()
{
	play* p2;
	p2 = new player;
	delete p2;
	return 0;
}

我们做的是定义了一个基类类型的p2指针,但是为他申请派生类的空间,最后delete掉,我们想要的应该是如下输出:

构造父类啦
构造子类啦

子类析构啦
父类析构啦

但事实上我们得到的是:

构造父类啦
构造子类啦
父类析构啦 

纳尼?少析构了空间,这是不妙的,but why? well,咱们再想想,你确实是申请了player空间,可以构造,没问题,但是你没办法去析构它啊,析构函数又不兼容,这个时候就又想到多态了,回去看多态三要素,不出所料,又是死在第一点,这个时候虚析构函数的作用就体现出来了:

//修改play类,在析构函数前加关键词virtual
class play
{
	int* a;
public:
	play()
	{
		a = new int;
		cout << "构造父类啦" << endl;
	}

	virtual ~play()
	{
		delete a;
		cout << "父类析构啦" << endl;
	}
};

是不是很好奇,为什么是在基类加virtual?that’s because 在将基类析构函数声明为虚函数时,由此基类派生出的所有子类的虚构函数都会自动成为虚析构函数(就是基类写了,派生类可以不写关键字),但记住,不能基类虚析构函数不是虚函数,而其派生类的析构函数是虚析构函数,这样(maybe)会有很不美好的事情发生,emmm,建议大家习惯去声明虚析构函数,在基类不需要析构函数时,也去这样做一下,哪怕这个显式定义的虚构函数是空的,这样能更大程度地保证delete时动态分配空间得到正确的处理。

        Then,let's talk about 瓜同学之前没玩过的纯虚函数、接口和抽象类这些看上去神秘莫测的东西,首先是纯虚函数,有了之前的虚函数基础,咱们在此基础上来看。很多时候,对某一函数,在基类中起不了作用,而是在其派生类的同名重写函数作用(多态嘛),对于该种在基类对应虚函数中没有实例的虚函数,我们可以将其改为纯虚函数,写法如下:

virtual void a() = 0 ;//取代virtual void a(){} 

也就是说,无法再调用基类的a(),(毕竟没有函数体);有纯虚函数的类就叫抽象类,抽象类不能去实例化对象(也就是说有纯虚函数的基类没办法去声明对象,只能用其对纯虚函数重写的派生类去声明),总而言是,这个纯虚函数就是为其派生类提供一个接口,以此来实现多态。但注意,抽象类派生出的子类在给出所有纯虚函数的函数实现后才可以定义自己的对象,否则这个派生类也是抽象类,不能实例化对象。

        光看文字有点抽象,咱们来康个example,有的聪明同学家里面又养狗又养猫,猫粮狗粮傻傻分不清(假装),所以狗狗和猫猫看不下去了,写了段代码来告诉你该喂什么东西,于是有了下面的这串代码:

#include<iostream>
using namespace std;

class feed
{
public:
	virtual void fed() = 0;
	int a;
};

class dog :public feed
{
public:
	void fed() override
	{
		cout << "我吃狗粮" << endl;
	}
};
class cat :public feed
{
public:
	void fed() override
	{
		cout << "我吃猫粮" << endl;
	}
};

void choose(feed* p) 
{
	p->fed();
}
int main()
{
	cat p1;
	choose(&p1);
	dog p2;
	choose(&p2);
	return 0;
}

最终输出:

我吃猫粮
我吃狗粮

首先猫猫和狗狗一起给定了接口feed()和外部实现他们提供各自诉求的choose函数,然后各分各粮,有了派生类dog和cat,这个时候, 再把他们各自的特点传给choose,锵锵!分好力,再也不用担心猫猫和狗狗打架了。刚刚咱们也提到了接口,纯虚函数这个名词念上去就有些拗口,所以瓜同学更愿意叫它接口。

emmm,写这篇blog的时候去补充了些知识,花了些时间,并且因为篇幅原因(懒惰)决定把可见性丢在后面,QAQ(要被传热学薄纱), 虽然标题是几天几天,但就当他是一个顺序把,每天学点东西就行捏~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值