C++模板和继承

在这里插入图片描述


前言

好久不见,我又回来了,害,还以为拖几天不更就有流量嘞,都是假的。
在这里插入图片描述
别给自己拖更找理由啊㗏,十天更一篇博客你不要命了?
废话不多说,今天这篇讲的是继承。
呀!什么是继承啊,c++还有孩子呀?
猜对了!不过这个孩子是类的孩子。所以今天在讲这个继承前还要先讲一下模板。



🥇模版

1️⃣泛化特化

模板编程追求的是高效、泛化,所谓的泛化就是通用,是大家所能共同的,龙生九子各有不同,但从本源来看他们都是龙的儿子,都有龙的特点,但又与龙有许多的不同,事实上模板的泛化和特化也是这种关系。
就好比说,我现在有一个鸟类,它有一个飞函数,他有翅膀、喙,爪成员,自然界的大部分鸟都有这些属性,我可以通过鸟类来实例化它们,但是有普遍就一定有特例,鸵鸟就是一个例子,难道他不会飞也要通过鸟类来实例化它,显然是不行的。
这时候对于鸵鸟这个对象的实现,就必须我们专门给他搞个特例了。

template<class T>
class{
publicvoid(T a){
	cout<<"fly"<<endl;}
private:
	T 翅膀;
	T 喙;
	T 爪;
};

很明显吗,鸵鸟必不能飞。
于是乎我可以针对他给出他的特化。

template<>
void(鸵鸟 a){   
	cout<<"nofly"<<end;
}

这样处理就会把鸵鸟的特性也加到鸟类中了。这其实就是函数特化。

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

哎,难道只要出现一个不同的鸟,就要加一个?那你还面向个*的对象编程了,你这直接是面向补丁的编程了。
别急! 对于特殊的鸟我们也可一总结出他们的共同点来,同样加入到我们的鸟类里

template<T>
class<濒危,T>{
public:
	void safe(){
	cout<<"_k,_l"<<endl;
	}
private:
	濒危 _k;
	T _l;
};
template<>
class<飞不高,吃虫>{
public:
	void safe(){
	cout<<"_m,_n"<<endl;
	}
private:
	飞不高 _m;
	吃虫 _n;
};

我上面写了两种形式,一种是总结了一部分特点,但是仍然包含鸟类中的属性,这种形式叫做偏特化,任何针对模版参数进一步进行条件限制设计的特化版本,后一种则是全特化,全特化即是将模板参数列表中所有的参数都确定化
对于偏特化,并不仅仅是指特化部分参数,还可以针对模板参数更进一步的条件限制所设计出来的一个特化版本。

template<typename T,typename T1>
class<T*,T1*>{
publicvoid(T* a){
	cout<<"fly"<<endl;}
private:
	T 翅膀;
	T 喙;
	T 爪;
};
template<typename T>
class<T&>{
publicvoid(T& a){
	cout<<"fly"<<endl;}
private:
	T& 翅膀;
	T&;
	T&;
};

2️⃣分离编译

很突然,什么是分离编译。我来说一下。
在实际写代码的时候,常常会想把我们的实现和声明分分开来写,把声明放到头文件中,把实现放到源文件中,这样代码整体也看上去美观,但是这样一般都会遇到一个问题,编译器会报错,会告诉你链接失败,类似这种
在这里插入图片描述
这就是我要说的分离编译。
实际上,这不是程序员的错,这是编译器不举,它 不 行 !
模板分离编译(Template Separation Model)是一种将模板的定义和实现分别写进不同的文件并分别进行编译的方式。在模板使用较少且体积大的情况下,使用这种方式可以减小编译时间和可执行程序体积。

具体地讲,在模板分离编译中,我们通常将模板的声明放在头文件中,然后将模板的定义实现放在对应的源代码文件中,在编译时,对每个源代码文件(.cpp文件)单独编译,得到目标文件(.o文件)。最后,在链接阶段,将所有的目标文件链接(link)成一个可执行文件。

需要注意的是,由于模板的定义实现和模板类的使用处在不同的文件中,因此编译器在编译使用模板类的源文件时,并不知道该怎么实例化模板类,所以只会生成部分代码。当后续的目标文件链接起来时,如果没有找到相应的模板实例化,程序就无法运行。

为了解决这个问题,我们采用了显式实例化或者隐式实例化的方法。显式实例化指在模板定义的源代码文件中,使用关键字template声明具体的模板参数类型,例如 template class X; 这样,编译器就能够正确生成指定类型的模板实例化代码。而隐式实例化则是在使用模板的地方,编译器会根据参数自动推到出应该生成什么样的实例化代码。

3️⃣非类型模板参数

没错,就是没有类型的模参数
长这样

 template<class T,size_t N = 10>
 class 数组
 {
 private:
 T _array[N];
 }

在这里插入图片描述
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

好!说的好! 但是你说这一堆跟继承有半毛钱关系?
有!

🥈继承

1️⃣什么是继承

定义

对于继承完全可以理解为现实生活中的继承,想象韩剧里的财阀家的孩子,对于财产继承往往要签署一些协议,这些似乎就是一种规定,就好像是皇帝立太子一样,子类继承父类,就代表子类跟父类签署了协议,父类的全部归属于子类,不论是父类的公有属性还是私有属性。
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
想象一下,子类其实就是一个多层蛋糕,他的最上层就是一个父类,对于这块蛋糕,我们称他为子类对象,我们在吃它的时候,可以说是想吃哪一块就吃那一块,当然蛋糕里也有些部分不能吃,像是一些装饰品,实际上这就像是父类的限制,下面格式会说到。

class dad{
public:
void car(){
	cout<<"car"endl; 				
}						
void get(){
	cout<<房产<<endl;
	cout<<存款<<endl;
}

private: <-------访问限定符
	int 房产;
	int 存款;
};             继承方式
class son:public dad{
public:
	void put(){}
private:
	int me;
};

格式

上述代码是一种public继承,也就是最全面的继承方式,除此之外,还有protected、private继承。
对于父类中属性的使用权限,不仅取决于父类成员的访问限定符,还取决于继承方式。
在这里插入图片描述示例代码中son以public继承了dad,也就是>=public的继承方式要改为public,而<public的继承方式则要保持不变。显然public继承是最直接的继承,所以public>protected>private。
在这里插入图片描述

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

所以说,实际上子类内部含有一个父类。只不过对于父类的private成员,不论如何都是不可见的,虽然不可见,但不代表不可用,对于私有变量,子类可以通过父类的公有方法去调用,
在这里插入图片描述
在这里插入图片描述

class dad{
};
struct son:dad{                 class son:dad{
	public继承							private继承
};										};

2️⃣对象赋值转换

在这里插入图片描述
在这里插入图片描述

子类中的父类理解为上图的蛋糕中的。父类独立于子类的上层。
子类可以给父类赋值、但是父类不可以给子类赋值
这是因为,子类给父类赋值就像是直接把上层蛋糕全部切一下切下来给了父类,而父类很知足,只要这个上层,就算给多了也不要。
而如果用父类给子类赋值的话,会导致子类没有下层,使子类出现空架子,干脆就不给他了。
这实质上是:

赋值兼容规则(向上转换)
可以用子类对象给父类对象赋值
可以用子类地址给父类指针赋值
可以用子类对象给父类的一个引用进行初始化

dad d;
son s;
dad d0=a;      
dad* d1=&s;
dad& d2=s;

为什么父类的指针和引用可以指向子类?
通俗的理解是我们的父类像是个无私切疼爱孩子的父亲,即使孩子过的很好,很有条件,但是他对于孩子的给予,仍然是很保守的,只是会去接受那一小部分他心里能接受的,他的目的是不要让儿子铺张浪费。
在这里插入图片描述
在这里插入图片描述

3️⃣作用域

依旧是前面的蛋糕作参考
子类和父类都有自己的作用域
在某个类中,有多个同名函数,他们只是参数、返回值不同的情形会构成重载
那在子类和父类中的同名函数哪?
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

class dad{
void fun(){
	cout<<"d::fun()"<<endl;
	}
void fun(int a){
cout<<"d::fun(a)"<<endl;
}
};
class son:public dad{
void fun(){
	cout<<"d::fun()"<<endl;
	}	
}

没错,父类中的fun(),全部失效(只要同名即失效)。
在这里插入图片描述

4️⃣子类默认成员函数

在这里插入图片描述

class dad {
public:
	dad() {
		cout << "d::struct" << endl;
	}
	dad(const int m) :a(m) {
		cout<<"d::struct(=)"<<endl;
	}
	~dad() {
		cout << "d::des" << endl;
	}
private:
	int a;
};
class son :public dad{
public:
	son(){
		cout << "s::struct" << endl;
	}
	son(const int n,const int m) :dad(n),b(m){
		cout<<"s::struct(=)"<<endl;
	}
	~son() {
		cout << "s::des" << endl;
	}
private:
	int b;
};
  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

  • 派生类对象初始化先调用基类构造再调派生类构造。

  • 派生类对象析构清理先调用派生类析构再调基类的析构
    在这里插入图片描述

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

  • 派生类的operator=必须要调用基类的operator=完成基类的复制。

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
    在这里插入图片描述

5️⃣静态友元

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

6️⃣多继承与菱形继承

class A {

};
class B {

};
class C {

};
class D :public A, public B, public C {
		
};

上面的代码就是一个多继承的例子。
要注意的是,假如在构造对象时,要进行拷贝构造,那必须从其父类开始进行拷贝构造,除此之外,还要根据继承的顺序进行拷贝构造。上述继承顺序是A,B,C
在这里插入图片描述

多继承一看就会,关键点在于菱形继承。
看下面代码:

class A {

};
class Bpublic A{

};
class Cpublic A{

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

B和C都继承自A,而D又继承自B和C,难道说蛋糕D的排列方式是ABCD?
实则不然,B和C各自都有一个A。在这里插入图片描述

在这里插入图片描述

显然这样好浪费资源,并且在调用A时也会很麻烦,需要限定作用域,于是乎聪明的设计者设计出了虚继承来解决菱形继承,只需要添加BC的A为virtual继承,也就是虚继承,就可以解决菱形继承的二义性和数据冗余的问题。

在这里插入图片描述
很明显他们公有一份A。
在这里插入图片描述

BC的开头都有一个地址,这个地址原本是指向各自的A,现在改为虚继承后,该地址并非指向A,而是指向了一块空间,这个空间存储了他们相对于A的偏移量。
在这里插入图片描述
28、18都是十六进制,转化成十进制就是40和24
从标出来的20和28开始,分表往下数40和24字节,刚好就是02的位置,也就是A的位置在这里插入图片描述
总结一下继承和组合:

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 优先使用对象组合,而不是类继承。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
    (white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见。
  • 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
    象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
  • 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

夜深人静代码时(好像每天都是这个点哎)

在这里插入图片描述

虚继承是多态的实现方式,下一篇咱们聊聊多态。
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值