typename和class的使用场景
什么是嵌套类型?
嵌套类型是指在类或结构体内部定义的类型 。这些类型通常是类的一部分,用于提供与类相关的特定类型定义。这些类型可以是基本类型、引用类型、指针类型、迭代器类型等。
typename的使用场景
1.模板参数的类型说明
2.定义模板参数
3.嵌套类型:
需要使用 typename 关键字来明确告诉编译器这是一个类型。
如果不加 typename,编译器会将嵌套类型误认为是一个静态变量或值,而不是一个类型(静态变量,可直接通过类域去访问)。
只要取没有实例化的类模板中的内部成员,编译器不知道是类型还是变量,需要typename来说明,等实例化后再去找
正确形式如下:
编译器会产生混淆是因为C++的语法允许在类内部定义静态成员变量,而嵌套类型的语法与静态变量的语法非常相似。
class的使用场景
1.定义类模板:
2.定义模板参数
3.定义嵌套类模板
关于嵌套类型和类模板
1.非类型模板参数
模板的作用:
1.控制数据类型:模板允许定义通用的类或函数,适用于多种数据型。
2.控制模式,适配器:模板可以实现适配器模式,使得不同的类可以适配到统一的接口中。
3.传类型,仿函数:模板可以定义仿函数,传递比较逻辑或计算逻辑,使得代码更加灵活和通用。
非类型模板参数:
就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
如果用静态栈即不用非类型模板参数,将无法同时创建超过有效索引范围的对象。
注意条件:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
了解array容器
越界检查: (对比)1.std::array 提供了 at() 方法,用于访问数组元素。at() 方法会在访问元素时进行越界检查。会抛出 std::out_of_range 异常。
2.普通数组编译器不会检查数组索引是否超出范围,因此越界访问会导致未定义行为。
鸡肋性
相比于普通数组,越界检查会增加额外的开销,性能稍逊一筹。相比vector,不能在创建对象时同时初始化,不适合动态内存管理。
所以一般情况下普通数组和vector使用更常见,只有在需要越界检查的需求下才使用array。
2.模板的特化
使用场景:通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理
函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
类模板特化
全特化
全特化即是将模板参数列表中所有的参数都确定化
偏特化
1.部分特化:将模板参数类表中的一部分参数特化。
2.参数更进一步的限制:
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
函数模板特化和类模板特化区别
注意:
1.函数模板重载 vs 特化:
函数模板特化可能不如函数重载直观,且重载优先级高于特化。建议优先使用重载。
2.类模板特化的成员函数:特化类时,可以只特化部分成员函数,但需保持接口一致。
3.函数模板和类模板不同,一般不需要显示实例化
4.模板特化不是新的类,需要依赖原类而存在,不能独立存在,特化版本会覆盖原模板在特定类型下的行为。实例化出来的是新类型。
原类的功能特化中不一定全部实现,根据需求,但一般都特化小类,功能可全部实现
3.模板的分离编译
什么是分离编译:
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
假设有以下场景:
- stack.h
cpp
#pragma once
#include<deque>
namespace ee
{
// 容器适配器
template<class T, class Container = std::deque<T>>
class stack
{
public:
//编译分离
void push(const T& x);
void pop();
T& top()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
class A
{
public:
void func1(int i);
void func2();
};
- stack.cpp
cpp
#include"stack.h"
namespace ee
{
//模板参数的默认值必须在类模板的定义中指定,
// 而不是在成员函数的定义外部
template<class T, class Container>
void stack<T, Container>::push(const T& x)
{
_con.push_back(x);
}
template<class T, class Container>
void stack<T, Container>::pop()
{
_con.pop_back();
}
void A::func1(int i)
{
}
//void A::func2(){}
// 显示实例化
template
class stack<int>;
template
class stack<double>;
}
类A中的func2在.cpp文件中没有被定义,push和pop若没有显示实例化也是没有定义,因为设计模板的只有实例化后才能生成地址。
见以下代码
- 测试
cpp
int main()
{
ee::stack<int> st;
//找不到
st.push(1);
st.pop();
//找得到
st.size();
st.top();
ee::A aa;
aa.func1(1);
aa.func2();
ee::stack<double>st1;
st1.push(2);
st1.pop();
return 0;
}
C++程序运行经历以下步骤:
预处理--编译--汇编--链接
编译阶段只看声明,声明是一种承诺,所以编译检查声明函数名参数返回可以对上,等着链接时,拿着修饰后的函数去其他文件符号表查找。
解决方法:
- 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐
- 模板定义的位置显式实例化。这种方法不实用,不推荐。
模板总结
优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
缺点:
- 模板会导致代码膨胀问题(其实正常写也难以避免,只是把我们该写的代码交给编译器去完成),也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误