十八、array 类

Ⅰ . 非类型模板参数

01 什么是非类型模板参数?

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

注意看,我们普通定义的 T 是类型,而 N 这里并不是类型,而是一个常量

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

 

               "非类型模板参数"
                     👇
template<class T, size_t N> class array;
             👆    
        "类型模板参数"

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

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

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

那么我们如果想定义两个容量不同的栈,可以做到嘛?

这里无论 #define 改成 100 还是 500 都无法解决这里的问题

这里只能使用非类型模板参数来解决

代码实现:

template<class T, size_t N>
class Stack {
    private:
        int _arr[N];
        int _top;
};
 
int main() 
{
    Stack<int, 100> st1;     
    Stack<double, 500> st2;  
 
    return 0;
}

这里我们在模板这定义一个常量 N,于是我们就可以在实例化时去指定其实例化对象的大小了

这个 N 就是非类型模板参数

03 非类型模板参数不能修改

非类型模板参数是常量,是不能被修改的

template<class T, size_t N> 
class Stack {
    public:
        void f() {    // 修改常量试试看
            N = 10; 
        }
    private:
        int _arr[N];
        int _top;
};
 
int main()
{
    Stack<int, 100> st1;
    st1.f();
 
    return 0;
}

运行结果如下:

test1711.cpp:10:15: error: lvalue required as left operand of assignment
             N = 10;

04 非类型模板参数类型规定

有些类型是不能作为非类型模板参数的,比如浮点数、类对象和字符串

非类型模板参数基本上都是整型

05 STL 中的 array

文档介绍:array - C++ Reference

我们现在再来看 array:

array 是 C++ 11 新增的,那么它有什么特别的地方嘛?

很可惜,基本没有

#include <iostream>
#include <array>
#include <vector>
using namespace std;
 
int main()
{
    vector<int> v1(100, 0);
    array<int, 100> a1;
    
    cout << "size of v1: " << sizeof(v1) << endl;
    cout << "size of a1: " << sizeof(a1) << endl;
 
    return 0;
}

运行结果如下:

vector 是开在堆上,而 array 是开在堆上

尴尬的是 array 能做的操作 vector 几乎都能做,array 也只是封装过的原生数组罢了

array<int, 100> a1;  // 封装过的原生数组
int a2[100];         // 原生数组

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

总结:array 相较于原生数组,有越界检查,实际中还是建议直接用 vector

Ⅱ . 模板的特化

01 给特殊类型准备特殊模板

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

但对于一些特殊类型,我们需要一些特殊的处理

代码演示:

#include <iostream>
#include"Date.h"
using namespace std;

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	cout << Less(1, 2) << endl;

	Date d1(2024, 8, 28);
	Date d2(2024, 8, 29);
	cout << Less(d1, d2) << endl;

	Date* d3 = new Date(2024, 1, 1);
	Date* d4 = new Date(2024, 1, 2);
	cout << Less(d3, d4) << endl;

	return 0;
}

运行结果如下:每次运行的结果都不一样

问题出在没传指针,传 *d3 和 *d4 就能解决

但如果不让你传指针怎么办呢?

这里就需要用到模板的特化,针对特定的类型做特殊化处理

02 模板特化的步骤

首先,需要有一个基础的函数模板

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

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

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

代码实现:

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(2024, 8, 28);
	Date d2(2024, 8, 29);
	cout << Less(d1, d2) << endl;

	Date* d3 = new Date(2024, 1, 1);
	Date* d4 = new Date(2024, 1, 2);
	cout << Less(d3, d4) << endl;

	return 0;
}

运行结果如下:

对于普通类型,它还是会调用正常的模板,对于 Date* 编译器就会发现这里有一个专门为它准备的特化版本,编译器会优先选择该特化版本,这就是模板的特化

那如果我们直接加一个普通函数,会调用哪个呢?

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

// 特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

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

函数重载,会直接调用普通函数的版本,因为是现成的,不需要实例化

03 类模板的特化

 刚才的函数模板不一定要特化,可以写一个具体的函数

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

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

int main()
{
	Date<int, int> d1;
	Date<int, double> d2;
	return 0;
}

这种情况就需要类模板的特化

代码实现:

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

// 类模板的特化
template<>
class Date<int, double>
{
public:
	Date()
	{
		cout << "Date<int, double>" << endl;
	}
};

int main()
{
	Date<int, int> d1;
	Date<int, double> d2;
	return 0;
}

运行结果如下:

 04 全特化和半特化

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

...
// 全特化
template<>
class Data<int, double> 
{
public:
	Data() {
		cout << "Data<int, double>" << endl;
	}
};

半特化(偏特化):将部分参数列表中的一部分参数特化

...
// 半特化(偏特化)
template<class T1>
class Data<T1, char> 
{
public:
	Data() 
    {
		cout << "Data<T1, char>" << endl;
	}
};
 
int main()
{
	// 只要第二个值是 char 都会匹配到半特化
	Data<int, char> d3;
	Data<char, char> d4;
 
	return 0;
}

半特化还可以用来对参数进行进一步限制

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

int main()
{
	Date<int*, char*> d3;
	Date<char*, string*> d4;
	Date<char**, void*> d5;
	return 0;
}

运行结果如下:

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

int main()
{
	Date<int&, char&> d6;
	return 0;
}

运行结果如下:

Ⅲ . 模板的优缺点

优点:

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

② 增强了代码的灵活性。

缺点:

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

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

具体分析:

优点:

泛型编程:模板允许你编写通用代码,可以适用于多种数据类型,而不仅仅是特定类型。这使得代码更具通用性和重用性。

类型安全:C++模板系统提供了强类型检查,这有助于捕捉在编译时发生的类型错误,而不是在运行时。

性能:模板生成的代码通常比使用宏或运行时多态的方式更高效。编译器可以生成针对具体类型的优化代码,从而提高性能。

容器和算法库:STL(Standard Template Library)使用了C++模板,提供了丰富的容器和算法,使开发者能够更轻松地使用和操作数据结构。

可扩展性:模板允许你创建自定义数据类型和函数,从而增强C++的可扩展性。

缺点:

编译时间:使用模板可能导致较长的编译时间,因为编译器需要为每个具体的模板实例生成代码。对于大型项目,这可能会导致显著的编译时间增加。

复杂性:模板语法相对复杂,可能对初学者不够友好。编写和维护模板代码需要一定的经验。

错误消息:当涉及到模板的错误发生时,编译器生成的错误消息通常比较晦涩难懂,这增加了调试的难度。

二进制兼容性:在C++中,由于模板的实现方式,对于不同编译器版本、不同编译选项或不同平台之间的二进制兼容性可能存在问题。

代码膨胀:每个不同的模板实例都会生成新的代码,这可能会导致代码膨胀,增加可执行文件的大小。

代码膨胀

代码膨胀(Code bloat)是指代码有着不必要的长度、缓慢或者其他浪费资源的情况。代码膨胀可能是由于编写代码的语言、编译时所用的编译器,或者编写的程序员所致。因此,虽然代码膨胀通常指源代码存在不必要的部分(由程序员导致),但也可指生成的代码或者二进制文件文件有膨胀问题。

在编程中使用了大量的模板、宏或泛型编程技术,导致生成的代码变得冗长、复杂和庞大的现象。这种情况通常出现在C++等编程语言中,其中模板和泛型编程被广泛应用。代码膨胀的主要原因是为了实现通用性和灵活性,程序员使用了大量的模板和泛型类型,以满足各种不同的数据类型和需求。代码膨胀可能会导致以下问题:

编译时间变长:生成大量的冗长代码需要更多的时间来进行编译。编译器需要处理更多的代码,这可能会导致编译时间明显增加,尤其是在大型项目中。

可维护性下降:庞大的代码库更难以维护和理解,因为其中可能包含了大量看似相似但实际上略有不同的代码片段。这会增加错误的引入和修复的难度,降低代码的可维护性。

错误信息混乱:当代码膨胀时,编译器生成的错误信息可能会变得非常混乱,不容易理解。这使得在出现编译错误时很难迅速定位和解决问题,增加了调试的复杂性。

为了避免代码膨胀,程序员可以谨慎使用模板和泛型编程技术,只在必要的情况下使用它们,而不是过度使用。此外,编译器优化和代码重构也可以帮助减少代码膨胀的问题。

  • 17
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
array的自行实现是指在编程语言中,我们可以自己编写一个来实现数组的功能。在这个array中,我们可以使用一些常见的数组操作,比如添加元素、删除元素、查找元素等。 首先,我们需要定义一个数组的数据结构,可以是一个由元素组成的列表。我们可以使用一个内部数组来存储这些元素,并且定义一个变量来记录数组的长度。 接下来,我们可以实现一些常用的数组操作方法。例如,我们可以实现一个添加元素的方法,它接受一个参数作为要添加的元素,并将其添加到数组的末尾。我们还可以实现一个删除元素的方法,它接受一个参数作为要删除的元素,并将其从数组中删除。还可以实现一个查找元素的方法,它接受一个参数作为要查找的元素,并返回其在数组中的索引。 除了这些基本的操作方法之外,我们还可以实现一些其他的功能。例如,我们可以实现一个获取数组长度的方法,用于返回数组当前的长度。我们还可以实现一个遍历数组的方法,用于逐个输出数组中的所有元素。 当实现完这些基本的功能之后,我们可以对这个array进行测试,确保它能够正确地执行各种操作。我们可以创建一个数组实例,并调用其中的方法来测试其功能是否正常。如果遇到问题,我们可以检查代码并进行调试,直到问题得到解决为止。 总结起来,自行实现一个array需要定义数组的数据结构,并实现一些常用的数组操作方法。通过这个,我们可以方便地操作数组,并进行各种操作。这对于数据处理和算法设计都是十分有帮助的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值