模板进阶
模板参数分类类型形参与非类型形参.
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称**。**
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
非类型模板参数
整形常量.可以传缺省值,从右往左缺省且连续。
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果
C++11的array定长数组没啥用,C语言对于越界的检查是抽查,可能查不到。但是array就是对于越界访问检查更加严格。operator[],用assert检查。
template<class T,size_t N=300>
class MyStack
{
public:
void push(const T& x)
{}
private:
T _a[N];
size_t _top;
};
int main()
{
MyStack<int, 100>st1;
MyStack<int, 200>st1;
return 0;
}
模板的特化
使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果
函数模板的特化
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出
class Date {};
template<class T>
bool ObjLess(const T& left, const T right)
{
return left < right;
}
//参数匹配
//bool ObjLess(const Date*& left, const Date*& right)
//{
// return *left < *right;
//}
//函数专用化
//template<>
//bool ObjLess<Date*>(Date* left,Date* right)
//{
// return *left < *right;
//}
int main()
{
MyStack<int, 100>st1;
MyStack<int, 200>st1;
Date* p1 = new Date();
Date* p2 = new Date();
ObjLess(p1,p2);
return 0;
}
类模板的特化
比如需要针对某些类型特殊化处理.
全特化
全特化是将模板参数列表中的所有参数都确定化.
偏特化
针对模板参数进一步尽心够条件限制设计的特化版本.
-
两种表现形式:
-
部分特化(将模板参数类表中的一部分参数特化)
-
参数更进一步的限制:针对模板参数进一步的条件限制设计出来的特化版本
-
#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;
};
// 偏特化
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
template <class T1>
class Data<T1, char>
{
public:
Data() { cout << "Data<T1, char>" << endl; }
private:
T1 _d1;
char _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); // 调用特化的引用版本
}
void TestVector()
{
Data<int, int> d1;//偏特化int
Data<int, char> d2;//全特化<int,char>
//<T1,char>
Data<char, char> d3;
Data<double, char> d4;
}
int main()
{
//TestVector();
test2();
return 0;
}
template<size_t N>
class A
{
public:
A() { cout << "A<N>" << endl; }
};
template<>
class A<10>
{
public:
A() { cout << "A<10>" << endl; }
};
int main()
{
A<100>();//A<N>
A<10>();//A<10>
return 0;
}
模板的分离编译
-
首先什么是分离编译
声明和定义分离是为了方便维护,
vector.h
结构定义和函数声明,vector.cpp
函数定义..h
文件了解框架设计基本功能.cpp
了解具体实现细节 -
编译链接步骤:
预处理:头文件展开、条件编译、宏替换、去掉注释->vector.i test.i
编译 :检查语法错误,生成汇编代码->vector.s test.s
汇编:将汇编代码转化为二进制机械码 ->vector.o test.o
链接:将多个obj文件合并在一起,并处理没有解决的地址问题链接之前,前面的文件是不会进行交互的
声明是一种承诺,告诉你有这个东西,编译时无法直接生成函数地址,在链接的时候会去根据声明的地址找他的定义.
- 模板参数不能分离(函数模板)
.cpp文件,也就是函数定义的地方,编译器不知道应该实例化为什么类型,函数模板无法实例化,类模板无法将T替换掉,不会编译生成汇编代码
,就无法实现具体的函数,函数就没有被分配地址,
在main.obj中调用的函数,编译器只有在链接的时候才会找对应的地址,但是函数因为没有实例化生成具体代码,所以是链接时报错,在链接的时候就找不到函数.
模板不能分离编译并不是语法问题,只是现在的编译器不支持,未来可能会发展支持。
模板程序被编译两次,这是不能分离编译的原因所在
如何解决?
-
将声明和定义放在.hpp中或者.h文件中,
声明和定义不分离(对于模板来说)
那么使用他的地方直接就有定义,头文件展开之后,直接就有模板定义和实例化,在编译时那么直接就可以填上函数调用地址,不需要链接的时候再去找了。 -
显示实例化(类型不同还得不同的实例化代码很麻烦)
template void F2<int>(const int& n);//将声明中未知的T实例化为int
函数模板例子:
-
类模板的例子:将模板的声明和定义放在一起就行
小结
模板的优点:复用代码节约资源,更快的迭代开发。STL因此产生
模板的缺点:模板的代码膨胀问题,实例化不同类型,编译时间变长,实例化的过程消耗的
声明和定义分离之后有声明,可以使用该类型(定义一个指针)
但是如果初始化一个对象,这里需要调用构造函数,因为没有完成实例化,链接的时候链接不上
调用函数时,如果参数类型能够直接匹配,那么就直接调用相应的类型函数,非类型模板函数
如果进行了类型的特化,就会直接调用模板函数。
IO流
流fstream函数重载以及operator>>返回值的存在实现了不同类型的连续输入输出.
scanfprintf()只能支持内置类型,而coutcin能够通过重载实现自定义类型的输入输出.
scanf输入成功返回个数,如果数据读完了就会返回EOF.
while(cin>>str);
cin返回值是istream
重载了ios::operator bool
值作为判定条件
class B
{
public:
operator bool()
{
return _a!=0;//输入0就结束调用operatorbool()转为bool值
}
int _a;
};
int main()
{
B b;
while(b)//因为重载了operatorbool(),所以可以转化为bool值作为判定条件
{
cin>>b._a;
}
return 0;
}
C语言文件操作:
struct ServerInfo
{
char _ip[32];
int port;
};
//二进制读写-写出去的东西文件看不见
void TestC_W_BIn()
{
ServerInfo info = { "127.0.0.1",80 };
//fopen/flose
//fwrite/fread-二进制读写
//fprintf/fscanf-文本读写
FILE* fout = fopen("test.bin", "w");
fwrite(&info, sizeof(info), 1, fout);
fclose(fout);
}
void TestC_R_BIn()
{
FILE* fin = fopen("test.bin", "r");
ServerInfo info;
fread(&info, sizeof(info), 1, fin);
fclose(fin);
printf("%s:%d\n",info._ip,info.port);
}
void TestC_W_Text()
{
FILE* fin = fopen("test.txt", "w");
ServerInfo info = {"127.0.0.1",80};
fprintf(fin,"%s %d", info._ip, info.port);
fclose(fin);
}
void TestC_R_Text()
{
FILE* fin = fopen("test.txt", "r");
ServerInfo info;
//字符组名字就是首元素地址
fscanf(fin,"%s %d",info._ip,&info.port);
//fscanf是以空格和换行作为分割符的,如果读的时候挨着就无法判断结束标志
printf("%s:%d\n", info._ip, info.port);
fclose(fin);
}
//int main()
//{
//
// TestC_W_Text();
// TestC_R_Text();
// return 0;
//}
C++文件操作
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<fstream>
#include<string>
#include<sstream>
using namespace std;
class Date
{
friend ifstream& operator>>(ifstream& ifs, Date& d);
friend ofstream& operator<<(ofstream& ofs, Date& d);
public:
Date(int y=1, int m=1, int d=1)
:_year(y)
, _month(m)
, _day(d)
{}
private:
int _year;
int _day;
int _month;
};
ofstream& operator<<(ofstream& ofs, Date& d)
{
ofs << d._year << " " << d._month << " " << d._day << endl;
return ofs;
}
ifstream& operator>>(ifstream& ifs, Date& d)
{
ifs >> d._year >> d._month >> d._day;
return ifs;
}
//序列化时重载使用
ostringstream& operator<<(ostringstream& ofs, Date& d);
istringstream& operator>>(istringstream& ifs, Date& d);
struct ServerInfo
{
char _ip[32];
int port;
Date _d;
};
class ConfigManager
{
public:
ConfigManager(const char* filename)
:_filename(filename)
{}
void WriteBin(ServerInfo& info)
{
ofstream ofs(_filename.c_str(), ios_base::out | ios_base::binary);
ofs.write((const char*) & info, sizeof(ServerInfo));
}
void ReadBin(ServerInfo&info)
{
ifstream ifs(_filename.c_str(), ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(ServerInfo));
}
//C++文本写支持流插入
void WriteText(ServerInfo& info)
{
ofstream ofs(_filename.c_str());
ofs << info._ip <<" " << info.port<<" ";
ofs << info._d;
//写入的时候注意分割符的添加,否则读的时候读不出来
//ofs为什么不能连续接收呢?因为返回值类型不同(见下文)
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename.c_str());
ifs >> info._ip >> info.port;
ifs >> info._d;
}
private:
string _filename;
};
class PersonInfo
{
public:
string _name;
int _age;
};
int main()
{
//组合结构化的信息-序列化
PersonInfo s = {"张三",20};
ostringstream oss;
oss << s._name <<" " << s._age;
string str = oss.str();//转成字符串
//解析字符串->结构化数据-反序列化
istringstream iss(str);
string name;
int age;
iss >> name >> age;
//ServerInfo info = { "127.0.0.1",80 };
/*ConfigManager cm("config.bin");
cm.WriteBin(info);
cm.ReadBin(info);
cout << info.port<<":"<<info._ip;*/
/*ServerInfo winfo={ "127.0.0.1",80,Date(2023,2,3)};
ConfigManager cm("config.txt");
cm.WriteText(winfo);
ServerInfo rinfo = {"",0,Date()};
cm.ReadText(rinfo);*/
return 0;
}
- ofs为什么不能连续接收呢?因为返回值类型不同
场景:
int main()
{
int x=100;
Date d1(2022,3,4);
Date d(2023,3,4);
ofstream ofs("text.txt");
ofs<<d1<<d2;
ofs<<x<<d1;//报错,返回值类型不同
//先int 返回值是ostream和Date类重载的ofstream 就匹配不上了
ostringstream ss;
ss<<d1;//也不支持,也是继承的ostream
return 0;
}
-
从左到右继承关系(父到子)
-
解决方案:
- 为了让以上两种情况都支持和利用继承关系,只需要将共同的父类ostream的的operator<<重载出来就行了,具体的那两个子类的都可以不写.因为两个子类对象都可以给给父类对象(切片).