从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 就有一个非类型模板参数。

从C语言到C++_21(模板进阶+array)+相关笔试题_第1张图片

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

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

1.1 array

从C语言到C++_21(模板进阶+array)+相关笔试题_第2张图片

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

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

看段代码:

#include 
#include 
#include 
using namespace std;

int main()
{
    vector v(100, 0);
    array 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 也只是封装过的原生数组罢了,就是有了接口函数,

从C语言到C++_21(模板进阶+array)+相关笔试题_第3张图片

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

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

#include 
#include 
#include 
using namespace std;

int main()
{
    int a[10];
    array 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 Stack
{
private:
    int _arr[N];
    int _top;
};

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

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

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

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

#include 
using namespace std;

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

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

    return 0;
}

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

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

1.3 注意事项

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

#include 
using namespace std;

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

int main()
{
    Stack st1;
    st1.modify();

    return 0;
}

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

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

#include 
using namespace std;

template // 错误	C2058	常量表达式不是整型
class Stack 
{

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

int main()
{
    Stack st1;

    return 0;
}

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

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

#include 
using namespace std;

template // 错误	C2058	常量表达式不是整型
class Stack 
{

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

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

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

    return 0;
}

2. 模板的特化

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

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

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

#include 
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 // 函数模板 -- 参数匹配
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;
}

从C语言到C++_21(模板进阶+array)+相关笔试题_第4张图片

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

2.1 函数模板的特化

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

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

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

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

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

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

代码演示:

#include 
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 // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}

template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less(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;
}

从C语言到C++_21(模板进阶+array)+相关笔试题_第5张图片

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

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

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


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

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

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

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

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

2.2 类模板的特化

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

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

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

#include 
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 // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
	return left < right;
}

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

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

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

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

	return 0;
}

从C语言到C++_21(模板进阶+array)+相关笔试题_第6张图片

加上类模板的特化:

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

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

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

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

	return 0;
}

从C语言到C++_21(模板进阶+array)+相关笔试题_第7张图片

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#include 
using namespace std;

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

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

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

template
class Data
{
public:
	Data() { cout << "Data" << endl; }
};

template
class Data
{
public:
	Data() { cout << "Data" << endl; }
};

template
class Data
{
public:
	Data() { cout << "Data" << endl; }
};

int main()
{
	Data d0;
	Data d1;

	Data d2;

	Data d3;
	Data d4;
	Data d5;
	Data d6;

	Data d7;
	Data d8;
	Data d9;

	return 0;
}

从C语言到C++_21(模板进阶+array)+相关笔试题_第8张图片

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

3. 模板关于分离编译

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

从C语言到C++_21(模板进阶+array)+相关笔试题_第9张图片

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

从C语言到C++_21(模板进阶+array)+相关笔试题_第10张图片

 理解两个概念:

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

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

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

4. 模板优缺点

【优点】

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

② 增强了代码的灵活性。


【缺点】

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

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

5. 模板相关笔试题

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

1)template

2)template

3)template

4)template

5)template

6)template

7)template

8)

9)template

10)template>

11)template

A.3

B.4

C.5

D.6

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

template

Type Max(const Type &a, const Type &b)

{

cout<<"This is Max"<

return a > b ? a : b;

}

template<>

int Max(const int &a, const int &b)

{

cout<<"This is Max"<

return a > b ? a : b;

}

template<>

char Max(const char &a, const char &b)

{

cout<<"This is Max"<

return a > b ? a : b;

}

int Max(const int &a, const int &b)

{

cout<<"This is Max"<

return a > b ? a : b;

}

int main()

{

Max(10,20);

Max(12.34,23.45);

Max('A','B');

Max(20,30);

return 0;

}

A.This is Max This is Max This is Max This is Max

B.This is Max This is Max This is Max This is Max

C.This is Max This is Max This is Max This is Max

D.This is Max This is Max This is Max This is Max

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

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

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

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

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

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

template

class Data

{

public:

Data() { cout << "Data" << endl; }

private:

T1 _d1;

T2 _d2;

};

template

class Data

{

public:

Data() { cout << "Data" << endl; }

private:

T1 _d1;

int _d2;

};

template

class Data ,T2*>

{

public:

Data() { cout << "Data, T2*>" << endl; }

private:

T1 _d1;

T2 _d2;

};

template

class Data

{

public:

Data(const T1& d1, const T2& d2)

: _d1(d1)

, _d2(d2)

{

cout << "Data" << endl;

}

private:

const T1 & _d1;

const T2 & _d2;

};

int main()

{

Data d1;

Data d2;

Data d3;

Data d4(1, 2);

return 0;

}

A.Data Data Data Data

B.Data Data Data Data

C.Data Data Data Data

D.Data Data Data Data

答案:

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

3. A

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

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

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

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

4. C

分析:Data d1; // 调用特化的int版本

Data d2; // 调用基础的模板

Data d3; // 调用特化的指针版本

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

本章完。

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

你可能感兴趣的:(④从C语言到C++,c++,模板,知识点,STL,开发语言)