C++第十二节课 模板初阶和string引入
一、函数模板
我们不需要写具体的函数,而是写这个函数的模板,编译器会根据模板生成对应的函数;
template<typename T>
template<class T>
两者的作用是等效的!
用模板完成的功能有时候也叫泛型编程;
如果我们要传递多种类型的参数:
template<typename T1,typename T2>
void Func(const T1& x, const T2& y)
{
cout << x << " " << y << endl;
}
int main()
{
Func(1, 2.2);
return 0;
}
可以通过定义多个模板参数来实现;
函数模板根据调用,自己退到模板参数的类型,实例化出对应的函数;
问题:模板参数可以用typename / class,那么可以用struct吗?
答案:不可以!规定不可以!
1、函数模板的实例化
模板参数的实例化分为两种:隐式实例化和显式实例化!
隐式实例化:让编译器根据实参推演模板参数的实际类型!
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
// 下面这种编译不通过!
// Add(a1, d1);
}
这里只有一个模板参数,实例化的时候不知道该参数的类型为int还是double,会产生歧义!
有两种解决方法:
- 用户自己强制转化;
- 显示实例化
用于自己强制转化:
// 方法一:强制类型转换
cout << Add(a1, (int)d1) << endl;
cout << Add((double)a1, d1) << endl;
这里是根据实参传递的类型,推导T的类型
显示实例化:
// 方法二:显示实例化
cout << Add<int>(a1,d1) << endl;
cout << Add<double>(a1, d1) << endl;
这里是指定T的类型进行实例化;(会存在隐式类型的转换 --- 产生临时变量 --- 临时变量具有常属性 --- 因此参数需要加上const进行修饰!)
但是有一种情况必须使用显式实例化:
template<class T>
T* Alloc(int a)
{
return new T[a];
}
这与这个函数,我们如果要调用,必须使用显示实例化,因为参数的类型已经被指定,返回值T*不确定,此时应该指定T的类型;
double* p1 = Alloc<double>(a1);
只能通过显示实例化来调用!
2、模板参数的匹配原则
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数;
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板;
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
二、类模板
我们分析下下面的代码:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
如果我们改变一个栈中存放的数据类型;
typedef int DataType;
我们需要将int改为其他类型;
但是如果对于下面这种情况:
int main()
{
Stack s1; // int
Stack s2; // double
return 0;
}
栈s1要存放int类型;
栈s2要存放double类型;
如果不使用模板我们只能拷贝多份相同的代码用于存放不同的类型!
这时候我们可以将上面的代码改为模板形式的:
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3)
{
/*_array = (T*)malloc(sizeof(T) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}*/
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
T* _array;
int _capacity;
int _size;
};
类模板的实例化
此时如果我们需要创建对象,因为构造函数里面不一定有参数!因此我们不能隐式实例化的方式让编译器去猜,只能自己显示实例化!
因此我们这样子实例化:
int main()
{
Stack<int> s1(); //int
Stack<double> s2(); // double
Stack<char> s3(); //char
return 0;
}
注意点:类模板中函数的声明和定义与之前的不太一样:
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3);
void Push(const T& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
T* _array;
int _capacity;
int _size;
};
Stack(size_t capacity = 3)
{
/*_array = (T*)malloc(sizeof(T) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}*/
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
如果我们直接在类外面对构造函数进行定义,此时编译器无法识别T!
template<class T>
class Stack
{
public:
Stack(size_t capacity = 3);
void Push(const T& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
T* _array;
int _capacity;
int _size;
};
template<class T>
Stack<T>::Stack(size_t capacity = 3)
{
/*_array = (T*)malloc(sizeof(T) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}*/
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
注意点:对于普通类还是,类型和类名是一样的!
例如:Date既是类型,也是类名!
但是类模板的类型和类名不相同!例如:
- Vector类名
- Vector<int>才是类型
template的作用范围可以认为是跟接下来的函数或者类的作用域(一个对应的花括号内);
每个函数都需要写对应的模板参数!
三、string类
使用string类需要包含头文件:#include<string>
且string这个类位于标准库中,因此如我们可以采用下面两种形式:
int main()
{
string s1;
std::string s2;
return 0;
}
1、string的构造函数
int main()
{
string s1; // 使用默认构造
string s2("张三");
string s3("hello");
string s4(10, 'a'); // 复制n个字符;
string s5(s2); // 调用拷贝构造
return 0;
}
这些是较为常用的重载的构造函数;
分析上面这个拷贝构造函数:
拷贝一个对象从pos位置的len长度!------ 部分拷贝(len在这里为缺省值!pos是对应字符串的下标,与数组类似)
npos是什么?
这里的npos是一个公有的静态的成员变量,它的值为-1!
但实际上,它的值不等于-1,而是整形的最大值!
因为size_t 默认参数都是正数,此时是把一个负数传递给一个无符号数,会发生整型提升,提升后,len会变成size_t的最大值,且在32 位的系统中,size_t 的最大值是 4294967295,而在 64 位的系统中则最大为 18446744073709551615。
string s7(s3, 6);
因此,如果我们没有给第三个参数,默认的第三个参数的值非常大,也就是默认拷贝之后的所有内容!
接下来我们尝试调用第三种构造函数重载的形式:
int main()
{
string s1; // 使用默认构造
string s2("张三");
string s3("hello world");
string s4(10, 'a'); // 复制n个字符;
string s5(s2); // 调用拷贝构造
string s6(s3, 6, 5);
cout << s3 << endl;
cout << s6 << endl;
return 0;
}
并且可以发现string这个类重载了流输出,我们可以直接cout输出流;
并且,因为string类中重载了运算符,如果我们需要对两个字符串进行比较,我们可以采用下面的方法:
cout << (s1 == s2) << endl;
cout << (s1 < s2) << endl;
注意,流插入和流提取运算符的优先级比较高,因此这里我们需要加上括号!
假设我们需要将下面的字符串拆分为三部分:
string ur1("https://cplusplus.com/reference/string/string/string/");
string sub1(ur1, 0, 5);
string sub2(ur1, 8, 13);
string sub3(ur1, 22, ur1.size()-22);
//string sub3(ur1, 22);
cout << sub1 << endl;
cout << sub2 << endl;
cout << sub3 << endl;
如果没有缺省值,那么我们就得采用上面的方法,但是有了缺省值就非常方便;
分析下面的拷贝构造函数:
意思为:拷贝字符数组s的前n个字符进行初始化;
2、赋值运算符重载
对于赋值运算符,我们可以采用下面三种形式:
s1 = s2;
cout << s1 << endl;
s1 = "11111";
cout << s1 << endl;
s1 = '2';
cout << s1 << endl;
3、增删查改中的增
对于一个字符串,我们我们想向其中插入值:
int main()
{
string s1("hello");
// 尾插一个字符
s1.push_back(' ');
// 尾插一个字符串
s1.append("world");
cout << s1 << endl;
return 0;
}
使用push_back可以完成工作,但是还可以使用运算符重载“+=”:
int main()
{
string s1("hello");
// 尾插一个字符
//s1.push_back(' ');
尾插一个字符串
//s1.append("world");
//使用运算符重载
s1 += " ";
s1 += "world";
cout << s1 << endl;
return 0;
}
也能达到目标!
问题:假设有一个正整数x,要求将x转为string对象,怎么实现?
int main()
{
size_t x;
cin >> x;
string xstr;
while (x)
{
size_t val = x % 10;
xstr += ('0' + val);
x /= 10;
}
cout << xstr << endl;
return 0;
}
但是这里得到的结果是反的,因此我们需要逆置,且考虑x为0的情况,具体情况我们下次再讲!