模板是泛型编程的基础
在之前我们介绍过函数重载,可以定义许多函数名相同参数列表不同的重载函数,来实现不同类型的相似操作。调用重载函数时会根据传参调用一个合适的重载函数,这方便了调用这类方法。
但是,函数重载还是有一些不足:
重载的函数还是需要自己定义的,代码复用率不高。当需要增加一种类型的实现时,就需要我们再定义一个重载函数;重载函数的可维护性不高,如果出错了可能需要逐个修改每个重载函数。
泛型编程就可以解决这个问题,可以只写一份对所有类型通用的代码,在需要使用的时候由编译器生成相应的代码,是代码复用的一种手段。模板是泛型编程的基础
函数模板代表了一类作用相同的函数,与类型无关。函数模板在使用时会由编译器自动实例化,生成一个特定类型的函数
使用
template
返回值 函数名(参数列表) {}
可以定义一个函数模板,其中typename也可以用class代替。
例如一个交换函数模板:
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
然后就可以像使用函数一样使用函数模板:
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
需要注意的是,template声明的typename只在其接下来的函数内有效
在使用函数模板时,编译器会生成一个指定类型的函数,即函数模板的实例化。
在指定生成函数的类型时,就有两种方式,即隐式与显式:
隐式实例化即编译器根据调用时的实参自动指定模板参数的类型:
例如这段代码:
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 20;
cout << Add(a, b) << endl;
return 0;
}
此时变量a
与b
均为int
,编译器自然可以通过两个均为int的实参推演出T
的类型为int
,这并不难理解。
但是当实参的类型不相同时,就无法确定T
究竟是两个参数类型中的哪一个了:
int a = 10;
double b = 20;
//cout << Add(a, b) << endl; //错误代码,调用Add时模板参数不明确
在模板中,不会擅自进行类型转换。 比如这种情况,在类型T
不明确时,并不确定要将int转化为double还是将double转换为int。
解决的方法当然也很简单:
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 10;
double b = 20;
cout << Add(a, (int)b) << endl;
return 0;
}
需要注意的是,类型转换会生成一个临时变量,临时变量具有常性。所以如果模板的形参类型不是const
修饰的话,就会发生权限放大,这种方法就是不可取的。
T
,这样编译器就会尝试进行隐式类型转换,即显式实例化:显式实例化即在调用时函数名后的<>
中显式的指定模板参数的类型: 函数名<模板参数列表>(实参列表);
例如上面的Add模板生成函数的调用:
int main()
{
int a = 10;
double b = 20;
cout << Add<int>(a, b) << endl; // Add(a, b)
return 0;
}
当指定了模板参数的类型后,编译器当然就能在传参时进行隐式类型转换了,转换失败时就会报错。
需要注意的是:
例如:
int Add(const int& a, const int& b)
{
return a + b;
}
template<typename T1, typename T2>
T1 Add(const T1& a, const T2& b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1, 2.0) << endl;
return 0;
}
在第一次调用Add函数时,实参类型均为int,所以优先调用非模板函数:
第二次调用Add函数时,第一个参数类型为int,第二个参数为double,模板生成的Add函数能更好的匹配,所以调用模板:
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换:
类似与函数,我们也可以定义一种与类型无关的类,即类模板。在使用时,再由编译器实例化为对于指定模板参数类型的类类型。
使用
template
class 类模板名 {};
可以定义一个函数模板,其中typename也可以用class代替。
例如我们可以写一个简陋的栈:
template<typename T>
class Stack
{
public:
Stack(size_t size = 0, size_t capacity = 0) //构造函数
: _size(size)
, _capacity(capacity)
, _date(nullptr)
{
_date = new T[_capacity + 1]{ 0 };
}
void push(T n) //压栈
{
if (_size == _capacity)
{
if (_capacity == 0)
{
reserve(6);
}
else
{
reserve(_capacity * 2);
}
}
_date[_size] = n;
++_size;
}
void reserve(size_t capacity)
{
if (capacity > _capacity)
{
T* newdate = new T[capacity + 1]{ 0 };
_capacity = capacity;
for (int i = 0; i < _size; ++i)
{
newdate[i] = _date[i];
}
delete[] _date;
_date = newdate;
}
}
T top()
{
return _date[_size - 1];
}
size_t size()
{
return _size;
}
~Stack() //析构函数
{
delete[] _date;
_size = 0;
_capacity = 0;
}
private:
size_t _size;
size_t _capacity;
T* _date;
};
对于int
型数据:
int main()
{
Stack<int> nums;
nums.push(1);
nums.push(2);
nums.push(3);
nums.push(4);
nums.push(5);
cout << nums.top() << endl;
cout << nums.size() << endl;
return 0;
}
int main()
{
Stack<char> str;
str.push('a');
str.push('b');
str.push('c');
str.push('d');
str.push('e');
cout << str.top() << endl;
cout << str.size() << endl;
return 0;
}
不难发现,对于不同的模板参数类型,这个简陋的栈可以实现其效果。
不同于函数模板,类模板不能通过参数来推断模板参数的类型,所以类模板的实例化必须在调用时的类模板名后的<>
中显式指出模板参数。
例如对上面的栈模板的使用:
int main()
{
//实例化类模板并实例化类对象
Stack<char> str;
Stack<int> nums;
Stack<double> dnums;
//对不同类型的栈堆栈
str.push('a');
str.push('b');
nums.push(3);
nums.push(4);
dnums.push(20.0);
dnums.push(30.0);
//打印不同类型栈的栈顶元素及元素个数
cout << str.top() << endl;
cout << str.size() << endl;
cout << nums.top() << endl;
cout << nums.size() << endl;
cout << dnums.top() << endl;
cout << dnums.size() << endl;
return 0;
}
需要注意的是:类模板名字不是真正的类,而实例化的结果才是真正的类
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架
容器:string、vector、list、deque、map、set等
算法:find、swap、reverse、sort等
(这里只做了解,接下来将会逐渐详细介绍)
到此,关于模板初阶的内容就介绍完了
包括函数模板与类模板
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦