C++:模板进阶与继承

模板进阶与继承

模板进阶

1.非类型的模板参数

模板参数:类型形参和非类型形参。

类型形参 :出现在模板参数列表中,跟在class或者typename之后 的参数类型名称。

非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

cpp 复制代码
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
	T& operator[](size_t index) { return _array[index]; }
	const T& operator[](size_t index)const { return _array[index]; }

	size_t size()const { return _size; }
	bool empty()const { return 0 == _size; }

private:
	T _array[N];
	size_t _size;
};

注意:

  1. 非类型参数其实很少用,因为只能给整形,给double或一系列自定义类型都会报错
  2. 非类型模板参数必须在编译期间确定结果,所以必须给常量

2.模板的特化

2.1特化的概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如下面的情况:

cpp 复制代码
//拿日期类举例,我们比较日期类大小要比的是内容
//指针的比较是没有意义的!!!
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果不确定
	return 0;
}
2.2函数模板特化

函数模板特化的要求:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对尖括号<>,里面也可以加模板参数,放到偏特化讲。
  3. 函数名后跟一对尖括号<>,尖括号中指定需要特化的类型
  4. 函数形参表必须要和模板函数的基础参数类型完全相同,不能多加参数或者调换参数位置
cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}
int main()
{
	cout << Less(1, 2) << endl;
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl;
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 这里调用的就是特化的版本,不是指针而是内容比较
	return 0;
}
2.3类模板特化
cpp 复制代码
//类模板特化和函数模板相似,只是加<>的位置变成了类名后面加
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

template<>
class Data<int, char>  //特化版本
{
public:
	Data() { cout << "Data<int, char>" << endl; }
private:
	int _d1;
	char _d2;
};
void TestVector()
{
	Data<int, int> d1;  //这个会通过模板生成更加合适的
	Data<int, char> d2;  //这个特化版本最合适
}
2.4全特化和偏特化
2.4.1全特化

全特化即是将模板参数列表中所有的参数都确定化,我们前面所写的都是全特化。

2.4.2偏特化

偏特化有以下两种表现方式:

  • 部分特化:将模板参数类表中的一部分参数特化
cpp 复制代码
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

// 对模板参数列表的一部分特化,这个还是比较好理解的
// 将第二个参数特化为int,注意特化必须保证有一个基础的函数模板
template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};
  • 参数限制:对模板参数类型的限制
cpp 复制代码
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
};

//只要是指针,不管你指向什么类型你都匹配我
//两个参数偏特化为指针类型
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};


//只要是引用,不管你引用什么类型你都匹配我
//不过其实和指针是一回事,引用底层就是指针
template <class T1, class T2>
class Data <T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }
};

// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
};

int main()
{
	Data<double, int> d1; // 调用特化的int版本
	Data<int, double> d2; // 调用基础的模板生成合适的 
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4; // 调用特化的引用版本
	return 0;
}

3.模板的分离编译

先说结论:模板一般不建议声明定义分离 ,如果需要也尽量在同一个文件内进行分离

3.1同文件分离
cpp 复制代码
namespace My
{
	template<class T>
	class A
	{
	public:
		typedef T* ptr;  //T类型指针
		ptr operator&();  //取到T类型指针

		T get();
	private:
		T _a;
	};
}

//写简单一点,就分离两个函数好了
//分离编译的时候A<T>还没有完全实例化
//不知道ptr是类型还是静态成员,所以需要加typename指定它是类型
//另外需要用类域限定符指定该函数(类型)属于那个空间中的那个类
template<class T>
typename My::A<T>::ptr My::A<T>::operator&()  //取到T类型指针
{
	return &_a;
}

template<class T>
T My::A<T>::get()
{
	return _a;
}
3.2不同文件下分离

在C/C++程序中每个源文件在链接之前都是互不关联的,而模板要求编译之前就确定好模板参数类型 ,模板才会去实例化,而分文件最大的问题就在于分离的部分无法确定参数类型(T),也就无法实例化。

cpp 复制代码
//a.h,这里写一起是方便看,实际在不同文件
namespace My
{
	template<class T>
	class A
	{
	public:
		typedef T* ptr;  //T类型指针
		ptr operator&();  //取到T类型指针

		T get();
	private:
		T _a;
	};
}

//a.cpp
#include "a.h"
//这里因为A<T>还没有完全实例化,可以理解为实例化过程中分离的部分和内部是隔离的
//也就是说不知道指定的内容是类型还是静态变量,需要加typename指定它是类型
template<class T>
typename My::A<T>::ptr My::A<T>::operator&()  //取到T类型指针
{
	return &_a;
}

template<class T>
T My::A<T>::get()
{
	return _a;
}

//test.cpp
#include "a.h"
int main()
{
	My::A<int> a;
	a.get();  //调了一下这个分离的函数
	return 0;
}

我们看执行结果:

这里的报错是链接错误,其实就是get这个函数压根没有实例化出来,我们通过显示实例化可以解决这个问题。

cpp 复制代码
//a.cpp
template<class T>
typename My::A<T>::ptr My::A<T>::operator&()  //取到T类型指针
{
	return &_a;
}

template<class T>
T My::A<T>::get()
{
	return _a;
}

template
My::A<int>;  //告诉了T类型,你去实例化

但是显示实例化非常局限,不同的类型都要写一次,要分离我还是建议采用第一种写法。

继承

1.继承的概念和定义

1.1继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用 的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

1.2继承的定义
1.2.1定义格式
1.2.2继承关系和访问限定符

这里说一下大多数的继承都是public(公有)继承 ,C++中protected这个访问限定符就是为继承准备的

这九种情况我们通过表格给出,但是大家不需要记忆这个表格 ,看总结1和2即可:

总结:

  1. 基类的private成员 在派生类中无论如何都不可见(不可见:①类外不能访问 ②隐身了,派生类内部也不能访问,继承了个寂寞)。
  2. 基类其它成员 在派生类中的访问方式 == Min(成员在基类的访问限定符 , 继承方式),即谁的权限小就取谁,认为 public > protected > private。
  3. 想让派生类像大多数类一样,我们可以把基类中想让别人访问的成员用public修饰,不想让别人访问的用protected修饰,最后采用pubilc方式继承即可。
  4. 继承方式不是必须写的,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式

2.基类和派生类的对象赋值转换

  • 派生类对象可以赋值给 ①基类的对象 ②基类的指针 ③基类的引用 。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切出来赋值过去
  • 基类对象不能赋值给派生类对象 (派生类有基类的部分,但基类却没有派生类的部分)。
cpp 复制代码
class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
void Test()
{
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.基类对象不能赋值给派生类对象
	sobj = pobj;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;

	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
	ps2->_No = 10;
}

3.继承中的作用域

  1. 在继承体系中基类派生类 都有独立的作用域
  2. 派生类和基类中有同名成员 ,派生类成员将屏蔽对父类同名成员的直接访问 ,这种情况叫隐藏,也叫重定义 。(在派生类成员函数中,可以使用 基类::基类成员 去显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

类成员变量同名:

cpp 复制代码
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111;  //身份证号
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl; // 类名::成员显示访问
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};

类成名函数同名:

cpp 复制代码
//这里要重点区分一下函数重载,首先重载是对同一域中才有的概念,这里肯定不构成重载
//其次隐藏的要求也和重载不同,只要和基类成员函数同名就构造隐藏
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();  //类名::成员 还是可以访问的
		cout << "func(int i)->" << i << endl;
	}
};

void Test()
{
	B b;
	b.fun(10);
};

4.派生类的默认成员函数

这里主要讲构造,=重载和析构 ,剩下的&重载意义不大。子类这几个函数处理的时候有一条原则:把父类的部分当作整体,调用父类的函数去处理,子类的部分子类处理

4.1构造和拷贝构造
  1. 因为有初始化列表的存在,在写子类构造时即使不显示调用父类构造,父类构造也会自己调用完成初始化
  2. 需要指定初始化父类内容的话可以显示调父类构造。
cpp 复制代码
class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}

protected:
	int _num; //学号
};
void Test()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
}
4.2operator = ()
  1. 遇到对象中有堆上资源存在的情况,为避免多次释放,我们可能需要自己写 operator= 。
  2. 不同于构造,如果我们在子类中显示写了operator=,父类的 operator= 是不会自动调用的,必须显示调用
cpp 复制代码
class Person
{
public:
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		Person::operator =(s);  //不会自动调用,必须显示调用
		_num = s._num;
		return *this;
	}
protected:
	int _num; //学号
};
4.3析构函数
  1. 析构比较特殊,不能主动调用,必须由编译器自动调用,且调用在子类析构函数之后。你自己调不会报错,但也不会生效。
  2. 至于为什么这样设计?①保持后定义的先析构这个顺序 ②子可以用父,如果父先析构了,子可能访问父的成员导致野指针访问。
  3. 因此设计子类析构时只要保证自己的资源正确释放即可

5.继承中的友元与静态成员

  • 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
cpp 复制代码
class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);  //声明该函数是基类的友元
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;    //你是基类的友元,访问基类对象是可以的
	cout << s._stuNum << endl;  //友元不继承你不能访问派生类对象,这里会报错,显示不可访问
}

int main()
{
	Person p;
	Student s;
	Display(p, s);
	return 0;
}
  • 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
cpp 复制代码
//无论继承多少层,大家用的始终是同一个变量
class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum; // 学号
};

class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

6.菱形继承

6.1菱形继承的概念
  • 单继承:一个子类只有一个直接父类 时称这个继承关系为单继承
  • 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
  • 菱形继承:菱形继承是多继承的一种特殊情况
6.2菱形继承的危害

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性 的问题。在assistant的对象中person成员会有两份

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	int _score; // 主修课程
};
void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";   //这个地方会报错,编译器不知道访问那个
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}
6.3菱形继承的解决方式
  1. 虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在student和teacher的继承person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
  2. 虚拟继承的关键字是virtual
cpp 复制代码
class Person
{
public:
	string _name; // 姓名
};

class Student : virtual public Person
{
protected:
	int _num = 0; //学号
};

class Teacher : virtual public Person
{
protected:
	int _id = 1; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	int _score = 60; // 主修课程
};
void Test()
{
	Assistant a;
	a._name = "peter";   //这个时候_name其实只有一个
	a.Student::_name = "xxx";  //这里的两个显示访问其实访问的都是一个变量
	a.Teacher::_name = "yyy";
}
6.4菱形虚拟继承的实现原理

PS:下面的讨论基于上面的代码,程序是32位程序(方便看而已)。

不过student多了一个0x00cf5dd8,teacher多了0x00cf5de4,这两个变量其实是指针,指向了两个表,表中记录了偏移量,通过这个偏移量从student(teacher)位置开始找到_name。这两个指针叫虚基表指针,这两个表叫虚基表

6.5菱形虚拟的总结
  1. 实际当中尽量避免写出菱形继承,对象复杂以后再引入多态,会变得相当复杂。
  2. 其次菱形继承会有时间消耗,空间消耗还好,因为所有子类对象都是共用虚基表的

7.继承和组合

继承关系:

cpp 复制代码
class Car {
protected:
	string _colour ; // 颜色
	string _num; // 车牌号
};

class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

组合关系:

cpp 复制代码
class Tire {
protected:
	string _brand = "Michelin"; //品牌
	size_t _size = 17; // 尺寸

};

class Car {
protected:
	string _colour; // 颜色
	string _num; // 车牌号
	Tire _t; // 轮胎
};
  • public继承是一种is-a(B是A)的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a(B有A)的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 实际当中优先采用组合关系而不是继承关系。
  • 继承关系是一种"白箱复用",基类的内部细节派生类是可见的,在一定程度上破坏了封装。派生类和基类间的依赖关系很强,耦合度高。
  • 组合关系是一种"黑箱复用",对象内部细节不可见,只对外提供对应接口。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  • 不过继承也有独特的优势,有的场景下继承更加符合语义,并且要实现多态必须继承
相关推荐
知识分享小能手28 分钟前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知3 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun3 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao3 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾3 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
利刃大大4 小时前
【高并发内存池】五、页缓存的设计
c++·缓存·项目·内存池
DKPT4 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa4 小时前
HTML和CSS学习
前端·css·学习·html
ST.J4 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
C语言小火车4 小时前
【C++八股文】基础知识篇
c++·tcp/ip·const·智能指针·多线程同步·static关键字·c++内存模型