c++模板编程-学习cpp类库的编程基础
1.函数模板
内容:
模板的实例化, 模板函数, 模板类型参数, 模板非类型参数, 模板的实参推演, 模板的特例化, 模板函数模板的特例化非模板函数的重载关系
区分 函数模板 和 模板函数的概念!!!
-
模板的意义?
对类型也可以进行参数化了// 原始的int的cmp函数 bool cmp(int a, int b) { return a>b; } cmp(10, 20); //现在的 模板函数 template<class或者typename T> //定义一个模板参数列表--尽量用template bool cmp(T a, T b) // 这是一个函数模板 { return a>b; } //使用 cmp<int>(10,20) --------- //函数调用点: 模板实例化为原始的 int 的 cmp函数 cmp<double>(10.4,20.5) cmp(10,20) // 模板的实参推演
在函数调用点, 编译器用用户指定的类型, 从原模板实例化一份函数代码出来(类型是变化的, int会出来int, double会出来double)
-
模板的实参推演?
根据用户传入的实参的类型, 推导出模板函数参数的具体类型cmp(10.4,20)// 这是错误的 此时将不能使用模板的实参推演, 需要手动确定 cmp<int>(10.5,20)// double会转化为int
-
函数模板是无法编译的, 因为不确定类型
函数的实例化是在调用点进行
模板函数才是编译器所编译的 -
字符串是不能直接比较的
const char * 用>比较, 是比较内存值大小
需要使用strcmp函数 -
如果这个模板的类型是const char *, 将需要进行模板特例化
// const char* 特例化 template<> //定义一个模板参数列表--尽量用template bool cmp<const char*>(const char* a, const char* b) // 这是一个函数模板 { return strcmp(a,b)>0; }
-
非模板函数–普通函数 以及调用关系
// 这是普通函数 bool cmp(const char* a, const char* b) // 这是一个函数模板 { return strcmp(a,b)>0; } //调用关系 对比上个的特例化, 二者存在时, cmp("aaa", "bbb")优先使用普通函数, 编译器优先处理成普通函数符号, 没有时, 才会找特例化
-
模板函数, 模板的特例化, 非模板函数的重载关系
重载和模板一定要分清楚, 有些书说, 这是重载, 重载是函数名相同, 参数不同
但要注意, 模板的函数名, 是函数名<类型>,这才是完整的函数名符号, 这个可不一样 -
函数模板的声明和定义不能跨文件?
当不在头文件时, 而是普通的两个cpp文件:
对于一般的函数模板, 是不能把 声明和定义分开放置的, 因为函数模板不参与编译, 只有实例化后的模板函数 才会编译
模板特例化是可以声明和定义分开放的, 因为编译后有确定的 函数符号(UND)定义和声明都放在头文件是可以的:
模板定义 放到头文件, 声明放在主文件, 因为include头文件 在预编译时, 直接展开即可, 所以可以看到模板定义的地方, 即 定义和声明实际在一个文件 -
那 8 的问题有办法吗?
有, 直接声明时, 指定类型-------尽量不要这么写定义在头文件 声明这么写: template bool cmp<int> (int, int); // double类似
2.理解模板函数
-
模板的非类型参数
必须是 整数类型(整型或者地址,引用都可以)c++20之后好像可以浮点数了, 只能使用,不能修改
指针和引用必须指向静态存储期的对象(如全局变量)template<typeanem T, int size> // size是非类型参数 void sort(T *arr) { 排序代码... } //使用 int arr[]={....}; const int size = sizeof(arr)/sizeof(int); sort<int, size>(arr); // size在这里定义为是一个常量, 可以使用具体数字代替
-
类模板!—重点在于 类名到底是什么?
一定要注意: 模板名称+类型参数列表=类名称
而不再是一般的 类名了, 这会导致很多错误
类名<类型> 才是现在的 类名
类外定义方法, 必须注意, 这个作用域的问题template<typename T> class SeqStark // 模板名称 +类型参数列表=类名称 { public: //构造和析构函数名不需要加<T>, 其他出现的模板地方都加上类型 SeqStark(int size=10); ~SeqStark(); SeqStark<T>(const SeqStark<T> &stark); //拷贝构造函数 SeqStark<T>& operator=(const const SeqStark<T> &stark); ..... }
#include <iostream> #include <stdexcept> template<typename T> class SeqStark { private: T* data; // 存储栈元素的数组 int capacity; // 栈的容量 int top; // 栈顶指针 public: // 构造函数 SeqStark(int size = 10) : capacity(size) , top(-1) ,data = new T[capacity]{} // 析构函数 ~SeqStark() { delete[] data; } // 拷贝构造函数 SeqStark(const SeqStark<T> &other) : capacity(other.capacity), top(other.top) { data = new T[capacity]; // 类型不确定 // 不要使用 memcpy, 如果是数组里是对象, 浅拷贝, 外部资源会出问题 for (int i = 0; i <= top; ++i) { data[i] = other.data[i]; } } // 赋值运算符重载, 加了引用才能 支持链式赋值 SeqStark<T>& operator=(const SeqStark<T> &other) { if (this != &other) { delete[] data; capacity = other.capacity; top = other.top; data = new T[capacity]; for (int i = 0; i <= top; ++i) { data[i] = other.data[i]; } } return *this; } // 压栈操作 void push(const T& value) { if (top == capacity - 1) { throw std::overflow_error("Stack is full"); } data[++top] = value; } // 弹栈操作 void pop() { if (top == -1) { throw std::underflow_error("Stack is empty"); } --top; } // 查看栈顶元素 T& peek() const { if (top == -1) { throw std::underflow_error("Stack is empty"); } return data[top]; } // 判断栈是否为空 bool isEmpty() const { return top == -1; } // 获取栈的大小 int size() const { return top + 1; } }; int main() { // 使用类模板创建一个整数栈 SeqStark<int> intStack(5); // 压栈操作 intStack.push(10); intStack.push(20); intStack.push(30); // 查看栈顶元素 std::cout << "Top element: " << intStack.peek() << std::endl; // 弹栈操作 intStack.pop(); std::cout << "Top element after pop: " << intStack.peek() << std::endl; // 判断栈是否为空 if (intStack.isEmpty()) { std::cout << "Stack is empty" << std::endl; } else { std::cout << "Stack is not empty" << std::endl; } // 获取栈的大小 std::cout << "Stack size: " << intStack.size() << std::endl; return 0; }
-
类模板是 选择性的 实例化
只有被调用的, 才会实例化, 具体看后续的代码调用, 才会实例化
类模板->实例化->模板类
3.实现cpp的vector向量容器
#include <iostream>
using namespace std;
template<typename T>
class Vector
{
private:
T* _first;//数组起始,与数组名
T* _last;//数组最后位置的下一个
T* _end;//空间的后面位置
void expand() //二倍扩容
{
int size = _last - _first;
T* ptmp = new T[2 * size];
for (int i = 0; i < size; i++)
{
ptmp[i] = _first[i];
}
delete[]_first;
_first = ptmp;
_last = _first + size;
_end = _first + 2 * size;
// 这里注意堆内存和指向堆内存的指针
//堆内存:一旦分配,会一直存在,直到显式释放
/*
如果指针是局部变量(比如在函数内部定义的),那么它的生命周期仅限于该函数的作用域。函数结束时,指针变量会被销毁,但它指向的内存不会被自动释放。
如果指针是全局变量或类的成员变量,那么它的生命周期会与程序或对象的生命周期一致。
*/
}
public:
Vector(int size = 10)
{
_first = new T[size];
_last = _first;
_end = _first + size;
}
~Vector()
{
delete[]_first;
_first = _last = _end = nullptr;
}
Vector(const Vector<T>& src)
{
int size = src._end - src._first;
_first = new T[size];
int len = src._last - src._first;
for (int i = 0; i < len; i++)
{
_first[i] = src._first[i];
}
_last = _first + len;
_end = _first + size;
}
Vector<T>& operator=(const Vector<T>& src)
{
if (this == &src)
{
return *this;
}
delete[]_first;
int size = src._end - src._first;
_first = new T[size];
int len = src._last - src._first;
for (int i = 0; i < len; i++)
{
_first[i] = src._first[i];
}
_last = _first + len;
_end = _first + size;
return *this;
}
void push_back(const T& val) //向容器末尾添加元素
{
if (full())
{
expand();
}
*_last++ = val;
}
void pop_back() //向容器末尾删除元素
{
if (empty())
{
return;
}
-- _last;
}
bool full()const
{
return _last == _end;
}
bool empty()const
{
return _last == _first;
}
T back()const // 返回末尾元素
{
return *(_last-1);// *(--_last)错误的, 本函数const方法, 不能修改成员变量, _last-1是偏移量, --会改变last值
}
};
int main()
{
Vector<int> vec;
for (int i = 0; i < 20; i++)
{
vec.push_back(rand() % 100);
}
while (!vec.empty())
{
cout << vec.back() << endl;
vec.pop_back();
}
return 0;
}
4.理解容器空间配置器allocator的重要性
上一节的vector容器, 当 类型是是下面这个:
class Test
{
public:
Test()
{
cout<< "Test"<<endl;
}
~Test()
{
cout<< "~Test"<<endl;
}
}
//执行
Vector<Test> vect; //会执行size次构造和析构, 这是不合理的
-
为什么会出现这个问题?
因为new 会做两件事, 开辟内存和构造Test对象, 导致初始化Vector对象时, 调用多次构造
析构呢, 应该是析构有限元素, 而没有元素,析构是无意义的 -
也引出了new和molloc的区别?
new: 适用于 C++ 中需要动态创建对象的场景。 支持构造函数和析构函数,适合面向对象编程。 malloc: 适用于 C 语言或需要直接操作内存的场景。 不涉及对象的构造和析构,适合底层内存管理。
-
上文实现, 在实际pop元素时, 并没有析构这个对象.
-
所以,现在需要做什么?
首先要把内存开辟和Test对象构造分开处理----否则会造成空容器构造对象
其次, 要把析构对象和释放内存分开----从容器删除元素时, 需要析构这个对象, 因为可能占用外部资源 -
容器空间配置器allocator?
就做了3里面的事template<typename T> class Allocator { T* allocate(size_t size) 开辟内存 { return (T*)malloc(sizeof(T)*size); } void deallocate(void *p) // 释放内存 { free(p); } void construct(T* p, const T &val) //对象构造 { new (p) T(val); //定位new /* 作用是在一块已经分配好的内存上构造一个对象,而不是通过 new 运算符动态分配内存。 p 是一个指针,指向一块预先分配好的内存。 T(val) 表示调用类型 T 的构造函数,并传递参数 val。 new (p) T(val) 的意思是在 p 指向的内存地址上构造一个 T 类型的对象,并调用构造函数 T(val)。 */ } void destroy(T *p) // 对象析构 { p->~T(); //~T()代表T类型的析构函数 } }
-
使用allocator—有点麻烦, 慢慢看
#include <iostream> using namespace std; template<typename T> struct Allocator { T* allocate(size_t size)// 开辟内存 { return (T*)malloc(sizeof(T) * size); } void deallocate(void* p) // 释放内存 { free(p); } void construct(T* p, const T& val) //对象构造 { new (p) T(val); //定位new /* 作用是在一块已经分配好的内存上构造一个对象,而不是通过 new 运算符动态分配内存。 p 是一个指针,指向一块预先分配好的内存。 T(val) 表示调用类型 T 的构造函数,并传递参数 val。 new (p) T(val) 的意思是在 p 指向的内存地址上构造一个 T 类型的对象,并调用构造函数 T(val)。 */ } void destroy(T* p) // 对象析构 { p->~T(); //~T()代表T类型的析构函数 } }; template<typename T, typename Alloc = Allocator<T>> //Alloc默认是Allocator<T> class Vector { private: T* _first;//数组起始,与数组名 T* _last;//数组最后位置的下一个 T* _end;//空间的后面位置 Alloc _allocator;//定义容器空间配置对象 void expand() //二倍扩容 { int size = _last - _first; //T* ptmp = new T[2 * size]; T* ptmp = _allocator.allocate(2 * size); for (int i = 0; i < size; i++) { //ptmp[i] = _first[i]; _allocator.construct(ptmp + i, _first[i]); } //delete[]_first; for (T* p = _first; p != _last; ++p) { _allocator.destroy(p); //析构_first指针指向的数组的有效元素 } _first = ptmp; _last = _first + size; _end = _first + 2 * size; } public: Vector(int size = 10) { //_first = new T[size]; // 只开辟内存 _first = _allocator.allocate(size); _last = _first; _end = _first + size; } ~Vector() { //delete[]_first; // 析构有效的元素并释放内存 for (T* p = _first; p != _last; ++p) { _allocator.destroy(p); //析构_first指针指向的数组的有效元素 } _allocator.deallocate(_first); //释放堆上的数组内存 _first = _last = _end = nullptr; } Vector(const Vector<T>& src) { int size = src._end - src._first; //_first = new T[size]; _first = _allocator.allocate(size); int len = src._last - src._first; for (int i = 0; i < len; i++) { //_first[i] = src._first[i]; _allocator.construct(_first + i, src._first[i]); } _last = _first + len; _end = _first + size; } Vector<T>& operator=(const Vector<T>& src) { if (this == &src) { return *this; } //delete[]_first; for (T* p = _first; p != _last; ++p) { _allocator.destroy(p); //析构_first指针指向的数组的有效元素 } int size = src._end - src._first; //_first = new T[size]; _first = _allocator.allocate(size); int len = src._last - src._first; for (int i = 0; i < len; i++) { //_first[i] = src._first[i]; _allocator.construct(_first + i, src._first[i]); } _last = _first + len; _end = _first + size; return *this; } void push_back(const T& val) //向容器末尾添加元素 { if (full()) { expand(); } //*_last++ = val; _allocator.construct(_last, val); _last++; } void pop_back() //向容器末尾删除元素 { if (empty()) { return; } --_last; _allocator.destroy(_last); } bool full()const { return _last == _end; } bool empty()const { return _last == _first; } T back()const // 返回末尾元素 { return *(_last - 1);// *(--_last)错误的, 本函数const方法, 不能修改成员变量, _last-1是偏移量, --会改变last值 } }; class Test { public: Test() { cout << "Test" << endl; } ~Test() { cout << "~Test" << endl; } Test(const Test&) { cout << "Test(const)" << endl; } }; int main() { Test t1,t2; cout << "----------" << endl; Vector<Test> vec; vec.push_back(t1); vec.pop_back(); cout << "----------" << endl; return 0; }