C++模板编程—学习C++类库的编程基础


课程总目录


文章目录

  • 一、详解函数模板
  • 二、类模板
  • 三、类模板实践:实现向量容器vector
  • 四、理解容器空间配置器allocator的重要性


一、详解函数模板

模板的意义:对类型也可以进行参数化了

// 也可以用template<class T>,但class容易和类混淆,我们都用typename
template<typename T>	// 模板参数列表
bool compare(T a, T b)	// compare是一个函数模板
{cout << "template compare" << endl;return a > b;
}/*
调用点实例化出来的模板函数
bool compare<int>(int a, int b)
{return a > b;
}bool compare<double>(double a, double b)
{return a > b;
}
*/int main()
{// 函数的调用点compare<int>(10, 20);compare<double>(10.5, 20.5);// 函数模板实参推演compare(20, 20); 	 // 还是用的刚才实例化的compare<int>// compare(30, 40.5); // 错误,推演不出来是什么类型// 解决方法一:template<typename T, typename E>,a和b用两个类型,各推各的// 解决方法二:compare<int>(30, 40.5),double强转成int
}

函数模板:不进行编译,因为类型还不知道

模板函数:在函数调用点,编译器用程序员指定的类型,从原模板实例化一份函数代码出来这就叫做模板函数,这是实例化出来真正需要进行编译的函数,因此站在编译器的角度来看,待编译的函数并没有减少,只是我们编写的代码量减少了。
同时,实例化出来的模板函数在.o文件符号表中产生相应的符号,每个函数名的符号只能出现一次

来看看字符串的情况 (模板的特例化)

// 针对compare函数模板,提供const char*类型的特例化版本
template<>	// 要写上
bool compare(const char* a, const char* b)
{cout << "compare<const char*>" << endl;return strcmp(a, b) > 0;
}
// 模板特化不需要在函数名后面加上类型参数
// 即别写成compare<const char*>int main()
{// 推演T为const char*,字符串 > 代表的是比较两个常量的地址,要用strcmp才能比较字符串的字典顺序// 对于某些类型来说,依赖编译器默认实例化的模板代码,代码处理逻辑是错误的// 这时候,就需要我们进行模板的特例化了,这不是编译器提供的,而是程序员提供的compare("aaa", "bbb");compare<const char*>("aaa", "bbb");// 这两种写法都是对的
}

当然,非模板函数(普通函数)优先被调用

//非模板函数 - 普通函数
bool compare(const char* a, const char* b)
{cout << "normal compare" << endl;return strcmp(a, b) > 0;
}int main()
{// 这时候就调用普通函数了,不调用模板函数了compare("aaa", "bbb");// 调用模板函数compare<const char*>("aaa", "bbb");
}

编译器优先把compare处理成函数名字,没有的话,才去找compare模板特例化,如果没有特例化,才进行模板的实例化

分文件编写

模板代码是不能在一个文件中定义,在另一个文件中使用的,否则链接的时候会出现错误

比如在test.cpp中存放模板代码,在main.cpp中声明,这是不可以的,因为声明产生的符号是*UND*,而在test.cpp中只有模板,模板本身是不编译的,没有模板实例化出来的compare<int>等函数,所以不可以

模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能进行正常的实例化,产生能够被编译器编译的代码。

所以,模板代码都是放在头文件.h当中的,然后在原文件当中直接进行#include包含

模板的非类型参数:

必须是整数类型(整数或者地址/引用都可以)是常量,只能使用,而不能修改

模板不仅可以接受类型参数typename T,还可以接受非类型参数。这些非类型参数可以是整型、指针、引用等。它们在编译时是常量,只能使用,不能修改

示例代码:

template <int N>
class Array {
public:int arr[N];int size() const { return N; }
};int main() {Array<5> myArray; // 创建一个包含5个整数的数组cout << "Array size: " << myArray.size() << endl;return 0;
}
// 使用模板实现冒泡排序
template <typename T, int N>
void bubbleSort(T* arr) {for (int i = N - 1; i >= 1; --i){int flag = 0;for (int j = 1; j <= i; ++j){if (arr[j - 1] > arr[j]){T temp = arr[j];arr[j] = arr[j - 1];arr[j - 1] = temp;flag = 1;}}if (flag == 0)return;}
}int main() {int arr[] = { 64, 34, 25, 12, 22, 11, 90 };const int size = sizeof(arr) / sizeof(arr[0]);// 调用冒泡排序模板函数bubbleSort<int, size>(arr);cout << "排序后的数组: ";for (int i : arr)cout << i << " ";cout << endl;return 0;
}

二、类模板

  • 类模板 → \to 实例化 → \to 模板类
  • 类名称 = 模板名称 + 类型参数列表
  • 为了简化,构造和析构函数不用加<T>,其他出现模板的地方都要加上
  • 类模板可以设置默认类型参数,实例化的时候只用写SeqStack<>就行了
//template<typename T = int> // 类模板可以设置默认类型参数
template<typename T>
class SeqStack
{
public:SeqStack(int size = 10):_pstack(new T[size]), _top(0), _size(size){}~SeqStack(){delete[]_pstack;_pstack = nullptr;}SeqStack(const SeqStack<T>& stack):_top(stack._top), _size(stack._size){_pstack = new T[_size];for (int i = 0; i < _top; i++)_pstack[i] = stack._pstack[i];}SeqStack<T>& operator=(const SeqStack<T>& stack){// 防止自赋值if (this == &stack)return *this;delete[]_pstack;_top = stack._top;_size = stack._size;_pstack = new T[_size];for (int i = 0; i < _top; i++)_pstack[i] = stack._pstack[i];return *this;}void push(const T& val);void pop(){cout << "pop():" << _pstack[_top] << endl;if (empty())return;--_top;}// 之前说过,对于只需要读的方法,最好写成常方法T top() const	// 返回栈顶元素{if (empty())throw "stack is empty";//抛异常也代表函数逻辑结束return _pstack[_top - 1];}bool full() const { return _top == _size; }	// 栈满bool empty() const { return _top == 0; }	// 栈空private:T* _pstack;int _top;int _size;// 扩容void expand(){T* ptmp = new T[_size * 2];for (int i = 0; i < _top; i++)ptmp[i] = _pstack[i];delete[] _pstack;_pstack = ptmp;_size *= 2;}
};// 在类外实现成员方法
// 注意点:1.加类的作用域SeqStack<T>::  2.写template<typename T>
template<typename T>
void SeqStack<T>::push(const T& val)
{cout << "push(const T& val):" << val << endl;if (full())expand();_pstack[++_top] = val;
}

三、类模板实践:实现向量容器vector

template<typename T>
class vector
{
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>& vec){int size = vec._end - vec._first;_first = new T[size];int len = vec._last - vec._first;for (int i = 0; i < len; ++i)_first[i] = vec._first[i];_last = _first + len;_end = _first + size;}vector<T>& operator=(const vector<T>& vec){// 防止自赋值if (this == &vec)return *this;// 释放本身指向delete[] _first;// 拷贝int size = vec._end - vec._first;_first = new T[size];int len = vec._last - vec._first;for (int i = 0; i < len; ++i)_first[i] = vec._first[i];_last = _first + len;_end = _first + size;}void push_back(const T& val)	// 向容器末尾添加元素{if (full())expend();*_last++ = val;}void pop_back()		// 从容器末尾删除元素{if (empty())return;--_last;}T back() const		// 返回容器末尾的元素的值{return *(_last - 1);}bool full() const { return _last == _end; }bool empty() const { return _first == _last; }int size() const { return _last - _first; }private:T* _first;	// 指向数组起始位置T* _last;	// 指向数组中有效元素的后继位置T* _end;	// 指向数组空间的后继位置void expend()	// 容器的二倍扩容{int size = _end - _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;}
};int main()
{vector<int> vec;for (int i = 0; i < 20; ++i)vec.push_back(i);vec.pop_back();	// 弹出19while (!vec.empty()){// 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0cout << vec.back() << " ";vec.pop_back();}cout << endl;return 0;
}

四、理解容器空间配置器allocator的重要性

先来看一下目前会有哪些问题?

template<typename T>
class vector { ... };	// 同上节class Test
{
public:Test() { cout << "Test构造" << endl; }~Test() { cout << "~Test析构" << endl; }Test(const Test& t) { cout << "Test拷贝构造" << endl; }
};
int main()
{vector<Test> vec;return 0;
}

运行结果:

Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构

一个空容器,竟然构造了10个Test对象,在vector的构造函数里使用了new,这不仅会开辟空间,还会去调用构造函数去构造对象,这明显是不合理的!

也就是我们需要把内存开辟和对象构造分开处理

vector的析构函数里面使用delete,即delete[] _first;,这会把_first指针指向数组的每一个元素都析构一遍,而我们应该是析构容器中有效的元素(数组可能很长,里面可能只有几个有效元素),然后释放_first指针指向的堆内存

另外我们来看,当我们添加一些对象的时候:

int main()
{Test t1, t2, t3;cout << "--------------------" << endl;vector<Test> vec;vec.push_back(t1);vec.push_back(t2);vec.push_back(t3);cout << "--------------------" << endl;vec.pop_back();cout << "--------------------" << endl;return 0;
}

运行结果:

Test构造
Test构造
Test构造
--------------------
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
Test构造
-------------------- # 这里pop_back没析构,明显不行
--------------------
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构
~Test析构

现在的逻辑vector底层用了new,相当于容器中每一个位置已经放了一个Test()对象,我们现在push_back(t1)是用t1去给容器里的的Test()对象赋值;

正确的逻辑:容器中生成的只有内存,而没有对象,push_back的时候应该是在已有的内存上面进行拷贝构造,

同时,容器中的对象很有可能占用外部资源,而pop_back现在的逻辑只是--_last;,下一次再添加对象的时候覆盖当前内存,指向外部资源的指针就丢失了,因此在pop_back的时候应该析构当前的对象,但是注意不能使用delete,因为delete不仅会调用对象的析构函数,而且还做一个free操作,把当前的内存释放掉,在这里面我们正确的需求是只析构对象,而不释放容器内部的空间

也就是我们需要把对象的析构和内存释放分开处理

那么,我们想要解决以上的问题,这就需要容器的空间配置器allocator

容器的空间配置器allocator做的四件事:

  • 内存开辟/内存释放
  • 对象构造/对象析构

那我们现在来实现一个自己的空间配置器

// 定义容器的空间配置器,和C++标准库的allocator实现一样
template<typename T>
class Allocator
{
public: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// 在指定内存上构造一个值为val的对象,这回调用T的拷贝构造}void destruct(T* p)			// 负责对象析构{p->~T(); // ~T()代表了T类型的析构函数}
};

添加到vector中:

// 注意不要只写Allocator,这是模板名称,要是类名称才对
template<typename T, typename Alloc = Allocator<T>>
class vector
{
public:vector(int size = 10){// 需要把内存开辟和对象构造分开处理// _first = new T[size];_first = _allocator.allocate(size);_last = _first;_end = _first + size;}~vector(){// 析构容器中有效的元素,然后释放_first指针指向的堆内存// delete[] _first;for (T* p = _first; p != _last; ++p)_allocator.destruct(p);		// 析构有效元素_allocator.deallocate(_first);	// 释放堆上的数组内存_first = _last = _end = nullptr;}vector(const vector<T>& vec){int size = vec._end - vec._first;// _first = new T[size];_first = _allocator.allocate(size);int len = vec._last - vec._first;for (int i = 0; i < len; ++i){// _first[i] = vec._first[i];_allocator.construct(_first + i, vec._first[i]);}_last = _first + len;_end = _first + size;}vector<T>& operator=(const vector<T>& vec){if (this == &vec)return *this;// delete[] _first;// 和析构~vector()一样for (T* p = _first; p != _last; ++p)_allocator.destruct(p);		// 析构有效元素_allocator.deallocate(_first);	// 释放堆上的数组内存// 和拷贝构造一样int size = vec._end - vec._first;// _first = new T[size];_first = _allocator.allocate(size);int len = vec._last - vec._first;for (int i = 0; i < len; ++i){// _first[i] = vec._first[i];_allocator.construct(_first + i, vec._first[i]);}_last = _first + len;_end = _first + size;}void push_back(const T& val){if (full())expend();// *_last++ = val;// 现在要在_last指针指向的内存构造一个值为val的对象_allocator.construct(_last, val);++_last;}void pop_back(){if (empty())return;// --_last;// 不仅要--_last,还需要析构删除的元素--_last;_allocator.destruct(_last);}T back() const{return *(_last - 1);}bool full() const { return _last == _end; }bool empty() const { return _first == _last; }int size() const { return _last - _first; }private:T* _first;T* _last;T* _end;Alloc _allocator; // 定义容器的空间配置器对象void expend(){int size = _end - _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.destruct(p);		// 析构有效元素_allocator.deallocate(_first);	// 释放堆上的数组内存_first = ptmp;_last = _first + size;_end = _first + 2 * size;}
};

使用&未使用allocator的对比:
在这里插入图片描述
可以看到,使用空间配置器之后才是正确的逻辑!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/850296.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

适用于 Windows 的 8 大数据恢复软件

数据恢复软件可帮助您恢复因意外删除或由于某些技术故障&#xff08;如硬盘损坏等&#xff09;而丢失的数据。这些工具可帮助您从硬盘驱动器 (HDD) 中高效地恢复丢失的数据&#xff0c;因为这些工具不支持从 SSD 恢复数据。重要的是要了解&#xff0c;您删除的数据不会被系统永…

NodeJs实现脚本:将xlxs文件输出到json文件中

文章目录 前期工作和依赖笔记功能代码输出 最近有一个功能&#xff0c;将json文件里的内容抽取到一个xlxs中&#xff0c;然后维护xlxs文件。当要更新json文件时&#xff0c;就更新xlxs的内容并把它传回json中。这个脚本主要使用NodeJS写。 以下是完成此功能时做的一些笔记。 …

【面试八股总结】内存页面置换算法

参考资料&#xff1a;小林coding、阿秀 缺页中断 在 CPU 里访问一条 Load M 指令&#xff0c;然后 CPU 会去找 M 所对应的页表项。如果该页表项的状态位是「有效的」&#xff0c;那 CPU 就可以直接去访问物理内存了&#xff0c;如果状态位是「无效的」&#xff0c;则 CPU 则会…

stanfordcorenlp+python做中文nlp任务,得到的结果中全是空字符串,而不是中文字符串

问题描述 代码&#xff1a; from stanfordcorenlp import StanfordCoreNLP import logging#中文中的应用&#xff0c;一定记得下载中文jar包&#xff0c;并标志lang‘zh’ nlp_zh StanfordCoreNLP(rD:\stanford-corenlp-full-2016-10-31, port8094, langzh,quietFalse,logg…

GiantPandaCV | 提升分类模型acc(一):BatchSizeLARS

本文来源公众号“GiantPandaCV”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;提升分类模型acc(一)&#xff1a;BatchSize&LARS 在使用大的bs训练情况下&#xff0c;会对精度有一定程度的损失&#xff0c;本文探讨了训练的b…

Java Web学习笔记24——Vue项目开发流程

import是引入文件。 export是将对象导出为模块。 new Vue({ router, router: h > h(App) }).$mount(#app) App.vue: vue的组成文件以.vue结尾&#xff0c;每个组件由三个部分组成&#xff1a;<template>、<script>、<style>。 <template><d…

i.MX8MP平台开发分享(RDC软件配置篇)

Uboot中已经将RDC的配置写入到了OCRAM中&#xff0c;NXP在ATF中预设了SIP服务&#xff0c;SIP服务下有厂商自定义的smc命令ID。例如下面的DDR、GPC、SRC和HAB的smc回调函数。 在SRC中断处理函数中&#xff0c;对于SRC_M4_START指令&#xff0c;先读取OCRAM中的配置&#xff0c;…

第一个小爬虫_爬取 股票数据

前言 爬取 雪球网的股票数据 [环境使用]&#xff1a;python 3.12 解释器pycharm 编辑器 【模块使用】&#xff1a;import requests -->数据请求模块 要安装 命令 pip install requestsimport csv -->将数据保存到CSV表格中import pandas -->也可以将数据保…

vue3+vite插件开发

插件开发目的:由于我司使用的前端技术栈为vue3tsvite2.Xaxios,在前端代码框架设计初期,做了把axios挂载到proxy对象上的操作,具体可见我的另一篇文章vue3TS自动化封装全局api_ts 封装腾讯位置api-CSDN博客 现在可以实现vue2的类似this.$api.xxx去调用接口,但是vue2源码使用的是…

flutter日历范围选择器

1.传入日期跨度&#xff0c;选择上架日期时&#xff0c;自动显示下架日期 2.手动选择上架日期和下架日期(图中下架日期自动填了只需CalendarDateRangePicker在initState方法中使用_startDate widget.initialStartDate; _endDate widget.initialEndDate;&#xff0c;而不直接…

【python】OpenCV—Blob Detection(11)

学习来自OpenCV基础&#xff08;10&#xff09;使用OpenCV进行Blob检测 文章目录 1、cv2.SimpleBlobDetector_create 中文文档2、默认 parameters3、配置 parameters附录——cv2.drawKeypoints 1、cv2.SimpleBlobDetector_create 中文文档 cv2.SimpleBlobDetector_create 是 O…

端午搞个零花钱,轻松赚取创业的第一桶金!2024最受欢迎的创业项目,2024新的创业机会

好好的端午节&#xff0c; 净给我添堵&#xff01; 本来我打算在端午节愉快的玩耍&#xff0c; 结果一大早起床却看到舍友在给一堆设备充电&#xff0c; 然后装的整整齐齐&#xff0c; 满满一书包。 我好奇他小子这是要干嘛&#xff1f; 不会是打算今天回去给亲朋好友准备…

【动态规划-BM79 打家劫舍(二)】

题目 BM79 打家劫舍(二) 描述 你是一个经验丰富的小偷&#xff0c;准备偷沿湖的一排房间&#xff0c;每个房间都存有一定的现金&#xff0c;为了防止被发现&#xff0c;你不能偷相邻的两家&#xff0c;即&#xff0c;如果偷了第一家&#xff0c;就不能再偷第二家&#xff0c;如…

全面分析找不到msvcr120.dll,无法继续执行程序问题

在计算机使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中“找不到msvcr120.dll”就是常见的一种。那么&#xff0c;找不到msvcr120.dll是什么意思呢&#xff1f; 一&#xff0c;msvcr120.dll文件概述 msvcr120.dll 是 Microsoft Visual C Redistributable …

C++教程(003):运算符

3 运算符 作用&#xff1a;用于执行代码的运算 我们主要讲解以下运算符&#xff1a; 运算符类型作用算术运算符用于处理四则运算赋值运算符用于将表达式的值赋给变量比较运算符用于表达式的比较&#xff0c;并返回一个真值或假值逻辑运算符用于根据表达式的值返回真值或假值 …

详解 Flink 的时间语义和 watermark

一、Flink 时间语义类型 Event Time&#xff1a;是事件创建的时间。它通常由事件中的时间戳描述&#xff0c;例如采集的日志数据中&#xff0c;每一条日志都会记录自己的生成时间&#xff0c;Flink 通过时间戳分配器访问事件时间戳Ingestion Time &#xff1a;是数据进入 Flink…

el-table合计行前置在首行,自定义合计行方法

背景 el-table原生合计行是在标签内增加show-summary属性&#xff0c;在表尾实现设计合计&#xff0c;且只对表格当前页面显示的列数据进行合计。element-UI效果如下图所示。 现要求在首行显示合计行&#xff0c;并自定义合计逻辑实现如下效果。 图示表格中&#xff0c;成本…

【渗透测试】DC-1靶机实战(上)漏洞扫描获取反弹shell

目录 一、范围界定 二、信息收集 三、目标识别 1&#xff09;主机发现 2&#xff09;端口扫描 四. 服务枚举 1&#xff09;网站首页 2&#xff09;Web指纹识别 3&#xff09;nikto报告 4&#xff09;robots.txt 5&#xff09;UPGRADE.txt 五. 漏洞映射 1&#xff…

万字长文|OpenAI模型规范(全文)

本文是继《OpenAI模型规范概览》之后对OpenAI Model Spec的详细描述&#xff0c;希望能对各位从事大模型及RLHF研究的朋友有帮助。万字长文&#xff0c;建议收藏后阅读。 一、概述 在AI的世界里&#xff0c;确保技术的行为符合我们的期望至关重要。OpenAI最近发布了一份名为Mo…

今天是放假带娃的一天

端午节放假第一天 早上5点半宝宝就咔咔乱叫了&#xff0c;几乎每天都这个点醒&#xff0c;准时的很&#xff0c;估计他是个勤奋的娃吧&#xff0c;要早起锻炼婴语&#xff0c;哈哈 醒来后做饭、洗锅、洗宝宝的衣服、给他吃D3&#xff0c;喂200ml奶粉、给他洗澡、哄睡&#xff0…