从C语言到C++_21(模板进阶+array)+相关笔试题

目录

1. 非类型模板参数

1.1 array

1.2 非类型模板参数的使用场景

1.3 注意事项

2. 模板的特化

2.1 函数模板的特化

2.2 类模板的特化

2.3 全特化和偏特化(半特化)

3. 模板关于分离编译

4. 模板优缺点

5. 模板相关笔试题

本篇完。


1. 非类型模板参数

对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数。

STL 的 array 就有一个非类型模板参数。

T 是类型,而 N 这里并不是类型,而是一个常量。

类型模板参数定义的是虚拟类型,注重的是你要传什么,而非类型模板参数定义的是常量。

1.1 array

array是一个固定大小的顺序容器(静态数组),是 C++11 新增的,它有什么独特的地方吗?

很可惜,基本没有,并且 vector 可以完全碾压 array,这就是为什么只在这里简单地讲讲 array。

看段代码:

#include <iostream>
#include <array>
#include <vector>
using namespace std;

int main()
{
    vector<int> v(100, 0);
    array<int, 100> arr;

    cout << "v : " << sizeof(v) << endl;
    //这里sizeof算的是成员变量的大小,VS2022下vector应该有四个成员变量,32位平台每个指针是4个字节,因此16字节
    cout << "arr : " << sizeof(arr) << endl;

    return 0;
}

vector 是开在空间大的堆上的而 array 是开在栈上的,堆可比栈的空间大太多太多了。

array 能做的操作几乎 vector 都能做,因为 vector 的存在 array 显得有些一无是处。

所以我们拿 array 去对标 vector 是不对的,拿去和原生数组比还是可以对比的。

但是 array 也只是封装过的原生数组罢了,就是有了接口函数,

比起原生数组,array 的最大优势也只是有一个越界的检查,读和写都可以检查到是否越界。

原生数组的读检查不到,写只能检查到后面几个数,

#include <iostream>
#include <array>
#include <vector>
using namespace std;

int main()
{
    int a[10];
    array<int, 10> arr; // array也不会初始化

    int x = a[15]; // 没报错
    a[10] = 2; // 报错
    a[11] = 2; // 没报错

    int y = arr[15]; // 报错
    arr[10] = 2; // 报错
    arr[11] = 2; // 报错

    return 0;
}

在 C++11 增加完 array 后备受吐槽,从简化的角度来说完全可以不增加 array。

并且现在大多数人都习惯了用原生数组,基本没人用array。

1.2 非类型模板参数的使用场景

假设我们要定义一个静态栈: 

#define N 100

template<class T>
class Stack
{
private:
    int _arr[N];
    int _top;
};

如果定义两个容量不一样的栈,一个容量是100 另一个是 500,能做到吗?

这就像 typedef 做不到一个存 int 一个存 double,而使用模板可以做到 st1 存 int,st2 存 double。

这里你的 #define 无论是改 100 还是改 500 都没办法解决这里的问题,

对应的,这里使用非类型模板参数就可以做到 s1 存 100,s2 存 500。

#include <iostream>
using namespace std;

template<class T, size_t N>
class Stack
{
private:
    int _arr[N];
    int _top;
};

int main()
{
    Stack<int, 100> st1;  // 大小是100
    Stack<int, 500> st2;  // 大小是500

    return 0;
}

在模板这定义一个常量 N,派遣它去做数组的大小。

于是我们就可以在实例化 Stack 的时候指定其实例化对象的大小了,分别传 100 和 500。

1.3 注意事项

注意事项 ①:非类型模板参数是是常量,是不能修改的。

#include <iostream>
using namespace std;

template<class T, size_t N>
class Stack 
{
public:
    void modify()
    {
        N = 10; // 错误	C2106	“ = ”: 左操作数必须为左值	
    }
private:
    int _arr[N];
    int _top;
};

int main()
{
    Stack<int, 100> st1;
    st1.modify();

    return 0;
}

注意事项 ②:有些类型是不能作为非类型模板参数的,比如浮点数、类对象以及字符串。

非类型模板参数基本上都是整型家族,char也是整形家族,也只有整型家族是有意义和价值的。

#include <iostream>
using namespace std;

template<class T, double N> // 错误	C2058	常量表达式不是整型
class Stack 
{

private:
    int _arr[N];
    int _top;
};

int main()
{
    Stack<int, 100> st1;

    return 0;
}

注意事项 ③:非类型的模板参数必须在编译期就能确认结果。

即非类型模板参数的实参只能是常量。

#include <iostream>
using namespace std;

template<class T, size_t N> // 错误	C2058	常量表达式不是整型
class Stack 
{

private:
    int _arr[N];
    int _top;
};

int main()
{
    size_t N;
    cin >> N;

    Stack<int, N> st1; // 错误	C2971	“Stack” : 模板参数“N”:“N”: 包含非静态存储持续时间的变量不能用作非类型参数

    return 0;
}

2. 模板的特化

通常情况下,使用模板可以实现一些与类型无关的代码

但是,对于一些特殊类型,可能我们就要对其进行一些 "特殊化的处理" 。

举例:如果不对特殊类型进行特殊处理就可能会出现一些问题,比如:

#include <iostream>
using namespace std;

class Date // 简化的日期类
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d) const
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	cout << Less(1, 2) << endl;   // 可以比较,结果正确

	Date d1(2023, 1, 1);
	Date d2(2023, 1, 2);
	cout << Less(d1, d2) << endl;  // 可以比较,结果正确

	Date* p2 = &d2;
	Date* p1 = &d1;
	cout << Less(p1, p2) << endl;  // 可以比较,结果错误

	return 0;
}

这里我们想比较的是指针指向的内容,而不是指针本身,怎么解决?

2.1 函数模板的特化

首先,必须要先有一个基础的函数模板。

其次,关键字 template 后面接上一对空的 <> 尖括号。

然后,函数名后跟一对尖括号,尖括号中指定需要特化的内容。

最后,函数形参表必须要和模板函数的基础参数类型完全相同。

template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}

template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right) {
	return *left < *right;
}

代码演示:

#include <iostream>
using namespace std;

class Date // 简化的日期类
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d) const
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}

template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right) 
{
	return *left < *right;
}

int main()
{
	cout << Less(1, 2) << endl;   // 可以比较,结果正确

	Date d1(2023, 1, 1);
	Date d2(2023, 1, 2);
	cout << Less(d1, d2) << endl;  // 可以比较,结果正确

	Date* p2 = &d2;
	Date* p1 = &d1;
	cout << Less(p1, p2) << endl;  // 可以比较,结果正确

	return 0;
}

解读:对于普通类型,它还是会调正常的模板。对于 Date* 编译器就会发现这里有个

专门为 Date* 而准备的特化版本,编译器会优先选择该特化版本。这就是函数模板的特化。

思考:现在我们加一个普通Less函数的函数重载,Date* 会走哪个版本?


bool Less(Date* left, Date* right) 
{
    return *left < *right;
}

答案:函数重载,会走直接匹配的普通函数版本,因为是现成的,不用实例化。

你可以这么理解:原模板是生肉,模板特化是半生不熟的肉,直接匹配的普通函数是熟肉。

所以:函数模板不一定非要特化,因为在参数里面就可以处理,

写一个匹配参数的普通函数也更容易理解。

2.2 类模板的特化

刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。

但是类模板我们没法实现一个具体的实际类型,就必须要特化了。

我们前面实现的仿函数(类模板)也有这样的问题:

#include <iostream>
using namespace std;

class Date // 简化的日期类
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d) const
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}

template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right) {
	return *left < *right;
}

类模板
template<class T>
struct Less2
{
	bool operator()(const T& x1, const T& x2) const
	{
		return x1 < x2;
	}
};

int main()
{
	Less2<Date> LessFunc1;
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 2);
	cout << LessFunc1(d1, d2) << endl;  // 可以比较,结果正确

	Less2<Date*> LessFunc2;
	Date* p2 = &d2;
	Date* p1 = &d1;
	cout << LessFunc2(p1, p2) << endl;  // 可以比较,结果错误

	return 0;
}

加上类模板的特化:

#include <iostream>
using namespace std;

class Date // 简化的日期类
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d) const
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}
template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right) 
{
	return *left < *right;
}

类模板
template<class T>
struct Less2
{
	bool operator()(const T& x1, const T& x2) const
	{
		return x1 < x2;
	}
};
template<>// 类模板特化
struct Less2<Date*>
{
	bool operator()(const Date* x1, const Date* x2) const
	{
		return *x1 < *x2;
	}
};

int main()
{
	Less2<Date> LessFunc1;
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 2);
	cout << LessFunc1(d1, d2) << endl;  // 可以比较,结果正确

	Less2<Date*> LessFunc2;
	Date* p2 = &d2;
	Date* p1 = &d1;
	cout << LessFunc2(p1, p2) << endl;  // 可以比较,结果错误,加上类模板特化后结果正确

	return 0;
}

2.3 全特化和偏特化(半特化)

全特化和偏特化的概念和缺省值很像,前面我们写的都叫作模板的全特化。

全特化:全特化即是将模板参数列表中所有的参数都确定化。

偏特化(又称半特化):将部分参数类表中的一部分参数特化。

(半特化并不是特化一半,就像半缺省并不是缺省一半一样)

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

① 部分特化:将模板参数类表中的一部分参数特化。

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

② 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,

而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }

private:
	T1 _d1;
	T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1, const T2& d2)
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}

private:
	const T1& _d1;
	const T2& _d2;
};
void test2()
{
	Data<double, int> d1; // 调用特化的int版本
	Data<int, double> d2; // 调用基础的模板 
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

放一段代码体会偏特化的花哨玩法:

#include <iostream>
using namespace std;

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;*/
};

// 偏特化
template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
private:
	/*T1 _d1;
	int _d2;*/
};

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; }
};

template<class T1, class T2>
class Data<T1&, T2*>
{
public:
	Data() { cout << "Data<T1&, T2*>" << endl; }
};

int main()
{
	Data<int, int> d0;
	Data<double, int> d1;

	Data<int, char> d2;

	Data<double, double> d3;
	Data<double*, double*> d4;
	Data<int*, char*> d5;
	Data<int*, char> d6;

	Data<int&, char&> d7;
	Data<int&, double&> d8;
	Data<int&, double*> d9;

	return 0;
}

 这就对应说过的类型匹配原则,有更匹配的就去调用它,没有就逐层递减去匹配。

3. 模板关于分离编译

什么是分离编译?
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,
最后将所有目标文件链 接起来形成单一的可执行文件的过程称为分离编译模式。
先说结论: 模板是不支持分离编译的。
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

在这里插入图片描述

如上图所示,在template.cpp源文件中定义了Sub函数,并在template.h头文件中进行了声明。
但是在编译过程中,编译器是对各个源文件进行单独编译的,template.cpp源文件进行编译的过程中,没有检测到Sub函数模板的实例化,所以不会生成对应的代码,在main.cpp源文件中进行调用,链接阶段便会出错。如图:

在这里插入图片描述

 理解两个概念:

  • 导出符号表:编译完成后该源文件中地址(函数定义的位置)已经确定的函数
  • 未解决符号表:源文件中地址还没有确定的函数

这里main.cpp源文件编译完成后,没有找到Sub函数的定义,但是由于头文件中进行了声明,在预处理阶段头文件中的声明会拷贝到源文件中,所以并不会立即报错,而是将Sub函数放在未解决符号表中,链接阶段,在template.cpp文件的导出符号表中找Sub函数的入口地址,而如果Sub函数没有生成则会报错。
 

解决方法:
① 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
② 模板定义的位置显式实例化。这种方法不实用,不推荐使用。

4. 模板优缺点

【优点】

① 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。

② 增强了代码的灵活性。


【缺点】

① 模板会导致代码膨胀问题,也会导致编译时间变长。

② 出现模板编译错误时,错误信息非常凌乱,不易定位错误。


5. 模板相关笔试题

1. 下列的模板声明中,其中几个是正确的( )

1)template

2)template<T1,T2>

3)template<class T1,T2>

4)template<class T1,class T2>

5)template<typename T1,T2>

6)template<typename T1,typename T2>

7)template<class T1,typename T2>

8)<typename T1,class T2>

9)template<typeaname T1, typename T2, size_t N>

10)template<typeaname T, size_t N=100, class _A=alloc<T>>

11)template<size_t N>

A.3

B.4

C.5

D.6

2. 以下程序运行结果正确的是( )

#include <iostream>
using namespace std;

template<typename Type>
Type Max(const Type& a, const Type& b)
{
	cout << "This is Max<Type>" << endl;
	return a > b ? a : b;
}

template<>
int Max<int>(const int& a, const int& b)
{
	cout << "This is Max<int>" << endl;
	return a > b ? a : b;
}

template<>
char Max<char>(const char& a, const char& b)
{
	cout << "This is Max<char>" << endl;
	return a > b ? a : b;
}

int Max(const int& a, const int& b)
{
	cout << "This is Max" << endl;
	return a > b ? a : b;
}

int main()
{
	Max(10, 20);
	Max(12.34, 23.45);
	Max('A', 'B');
	Max<int>(20, 30);
	return 0;
}

A.This is Max This is Max<Type> This is Max<char> This is Max<int>

B.This is Max<int> This is Max<Type> This is Max<char> This is Max<int>

C.This is Max This is Max<int> This is Max<char> This is Max<int>

D.This is Max This is Max<Type> This is Max<char> This is Max

3. 关于模板的编译说法错误的是( )

A.模板在.h文件中声明,在.cpp里面实现

B.模板程序一般直接在一个文件里面进行定义与实现

C.不久的将来,编译器有望支持export关键字,实现模板分离编译

D.模板不能分离编译,是因为模板程序在编译过程中需要经过两次编译

4. 以下程序运行结果正确的是( )

#include <iostream>
using namespace std;

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};

template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1, const T2& d2)
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}
private:
	const T1& _d1;
	const T2& _d2;
};

int main()
{
	Data<double, int> d1;
	Data<int, double> d2;
	Data<int*, int*> d3;
	Data<int&, int&> d4(1, 2);
	return 0;
}

A.Data<T1, T2> Data<T1, int> Data<T1*, T2*> Data<T1&, T2&>

B.Data<T1, int> Data<T1, T2> Data<T1&, T2&> Data<T1*, T2*>

C.Data<T1, int> Data<T1, T2> Data<T1*, T2*> Data<T1&, T2&>

D.Data<T1, T2> Data<T1, T2> Data<T1*, T2*> Data<T1&, T2&>

答案:

1. D

分析:正确的定义为:4 6 7 9 10 11,一共6个

2. A

分析:Max(10,20);    //能够直接匹配int参数,调动非模板函数

Max(12.34,23.45); //double类型参数没有最佳匹配函数,此时只能调动模板函数

Max('A','B');   //能够直接匹配char参数,调动非模板函数

Max<int>(20,30); //由于直接实例化了函数,因此要调动模板函数,但是,由于进行函数的int特化,所以会调动特化版本的模板函数

3. A

A.模板不支持分离编译,所以不能在.h声明,在.cpp实现

B.由于不支持分离编译,模板程序一般只能放在一个文件里实现

C.不支持分离编译并不是语法错误,而是暂时的编译器不支持,不久将来,或许会被支持

D.模板程序被编译两次,这是不能分离编译的原因所在

4. C

分析:Data<double, int> d1; // 调用特化的int版本

Data<int, double> d2; // 调用基础的模板

Data<int *, int*> d3; // 调用特化的指针版本

Data<int&, int&> d4(1, 2); //调用特化的引用版本

本篇完。

下一部分:C++中的继承,讲完继承讲多态。

穿越回来复习顺便贴个下篇链接:

从C语言到C++_22(继承)多继承与菱形继承+笔试选择题_类继承 笔试-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值