文章目录

  • 一:前言(必看)
  • 二:校内项目一:哈夫曼树的实现
      • 项目:哈夫曼编码与译码方法
      • 场景构造内容和要求:
      • C代码:
  • 三:校内项目二:推箱子小游戏
        • (1)方法列表
        • (2)参数列表
        • (3)具体函数讲解和实现
          • 1、main函数
          • 2、initData()
          • 3、drawMap()
          • 4、moveUp()
          • 6、moveLeft()
          • 7、moveDown()
          • 8、moveRight()
  • 四:项目:linux下C++ socket网络编程——即时通信系统(博主目前是跟随视频在学习中,服务器开发和客户端开发个人挺喜欢,感兴趣的或者投简历到此的大佬可以考察一下目前我的服务器编程的能力)
    • 一:项目内容
    • 二:需求分析
    • 三:抽象与细化
    • 四:基础步骤记忆
        • TCP服务端通信常规步骤:
        • TCP客户端通信常规步骤:
    • 五:相关技术介绍
        • 1.socket 阻塞与非阻塞
        • 2. epoll
          • Epoll 用法(三步曲):
    • 六:代码结构
    • 七:代码实现
  • 五:实习项目(较长较难,个人选择,大厂选择)
    • 一、概述
      • 项目涉及的知识点
      • 内存池简介
    • 二、主函数设计
    • 三、模板链表栈
      • 总结一
      • 二、设计内存池
      • 三、实现
      • 四、与 std::vector 的性能对比
      • 总结二
  • 补充,上述项目的优化(企业内更好性能的内存池)
      • 1、封装一个类用于管理内存池的使用如下,很容易看得懂,其实就是向内存池申请size个空间并进行构造,返回是首个元素的地址。释放也是一样,不过释放多个的时候需要确保这多个元素的内存是连续的。
      • 2、内存池设计代码,下面会一个一个方法抛开说明
      • 3、申请一个空间,当回收的内存没有或内存块空间不够时,新开辟一块内存,并将新内存放在表头,返回新内存的头地址,如果内存块还有空间,那么返回首个空余的空间
      • 4、当分配超过2个元素空间时,先判断空闲块的空间够不够分配,够分配,不够新开辟一个大小跟申请元素个数一样的内存块,并将该块内存向表头置后,返回该快首地址。注意,由于分配多个元素的空间也就是分配一个数组,这个时候在下一步调用构造函数时会构造数组对象,数组对象会多一个指针空间指向该数组,所以申请n+1个元素时加上一个指针的空间,否则会泄漏。
      • 5、性能测试
  • 最后的补充

一:前言(必看)

先对大家说声抱歉,在学校玩了一周(很多好朋友,总是要聚聚的),目前在家已经调整好给大家最好的分享。
你的简历或者说你去面试最为重要的一点(除却相关知识的基础内容和算法的掌握程度),在技术面需要知道,你的简历和自我介绍必然是面试官开展的重要依据,很多公司不会顺着你的自我介绍开展询问,所以简历之中的项目经历是面试官最为常见的询问内容,任何一个项目面试官都是可以开展很深的技术的询问,还能够了解到你的解决问题的能力、学习能力和团队协作能力(团队项目),但是很多博友都是没有什么C语言相关的项目,所以这里分享两个C的小项目给大家,很多人没有实习经历所以我在为大家分享一个我在实习时学到的一个比较重要的内容,不过不能够分享源码,为大家分享的是我自己写的代码,所以多多包涵。

二:校内项目一:哈夫曼树的实现

这个不能算是一个项目,但是可以写在简历上,我身边有位李姓的大佬可以写出来代码(我叫他小灰灰,同学的话可以向他学习的哦),但我相信很多人是写不出来或者说理解不了的,我也是这几天看明白了顺便敲敲代码的,如果追求项目深度可以略过这一个项目分享。

项目:哈夫曼编码与译码方法

哈夫曼编码是一种以哈夫曼树(最优二叉树,带权路径长度最小的二叉树)为基础的基于统计学的变长编码方式。其基本思想是:将使用次数多的代码转换成长度较短的编码,而使用次数少的采用较长的编码,并且保持编码的唯一可解性。在计算机信息处理中,经常应用于数据压缩。是一种一致性编码法(又称"熵编码法"),用于数据的无损耗压缩。本项目利用贪心算法实现一个完整的哈夫曼编码与译码系统。

场景构造内容和要求:

从文件中读入任意一篇英文文本文件,分别统计英文文本文件中各字符(包括标点符号和空格)的使用频率;根据已统计的字符使用频率构造哈夫曼编码树,并给出每个字符的哈夫曼编码(字符集的哈夫曼编码表);将文本文件利用哈夫曼树进行编码,存储成压缩文件(哈夫曼编码文件);计算哈夫曼编码文件的压缩率;将哈夫曼编码文件译码为文本文件,并与原文件进行比较。

C代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define n 65//哈夫曼树节点存储结构
typedef struct{char data;int weight;int lchild;int rchild;int parent;
}Htnode;typedef Htnode HuffmanT[129];//哈夫曼编码表的存储结构
typedef struct{char ch;        //储存被编码的字符char bits[n+1]; //字符编码位串
}CodeNode;typedef CodeNode HuffmanCode[n];//0-9为数字;10-35为小写字母;36-61为大写字母;62-64为特殊字符
void InitHT(HuffmanT T)   //初始化
{char sz = '0';char xzm = 'a';char dzm = 'A';char kong = ' ';char dh = ',';char jh = '.';for(int i=0; i<n; i++)        {T[i].lchild = T[i].rchild = T[i].parent = -1;T[i].weight = 0;if(i>=0&&i<=9){T[i].data = sz;sz++;}if(i>=10&&i<=35){T[i].data = xzm;xzm++;}if(i>=36&&i<=61){T[i].data = dzm;dzm++;}if(i>=62&&i<=64){T[62].data = kong;T[63].data  = dh;T[64].data= jh;}}for(int j = n; j<2*n-1; j++){T[j].weight = 0;T[j].lchild = T[j].rchild = T[j].parent = -1;}printf("initHT over\n");
}void InputW(HuffmanT T)         //读入文件中字符并输入权值
{FILE *fp;char ch;char Filename[20];printf ("input the filename:");scanf("%s",Filename);if((fp=fopen(Filename,"r"))==NULL)    printf("faild\n");ch = fgetc(fp);while(ch != EOF){for(int i = 0; i<n; i++){if(T[i].data == ch) T[i].weight++;}ch = fgetc(fp);}for(int i =0; i<n; i++){printf("%c weight is:",T[i].data);printf("%d\n",T[i].weight);// printf("%d,%d,%d\n",T[i].parent,T[i].lchild,T[i].rchild);}fclose(fp);printf("inputW over\n");
}void SelectMin(HuffmanT T, int length, int *p1, int *p2)    //选择权值最小的两个元素,返回下标
{int min1,min2;              //min1标记最小,min2标记次小int i=0;int k,j=0;for(j; j<length; j++){if(T[j].parent == -1){min1=j;break;}}for(k=min1+1;k<length;k++){if(T[k].parent == -1){min2 = k;break;}}// for(i = 0;i<length;i++)while(i<length){if(T[i].parent == -1){if(T[i].weight<T[min1].weight){min2 = min1;min1 = i;}else if((i!=min1)&&(T[i].weight<T[min2].weight)){min2 = i;}}i++;}// printf("%d,%d:%d,%d ",min1,min2,T[min1].weight,T[min2].weight);*p1 = min1;*p2 = min2;// printf("selectmin\n");
}void CreartHT(HuffmanT T)       //构造哈夫曼编码树
{int i,p1,p2;int wei1,wei2;InitHT(T);      //初始化InputW(T);      //输入权值for(i=n; i<129; i++){SelectMin(T,i,&p1,&p2);wei1 = T[p1].weight;wei2 = T[p2].weight;T[p1].parent = i;T[p2].parent = i;T[i].lchild = p1;T[i].rchild = p2;T[i].weight = wei1 + wei2;}printf("creatHT over\n");
}void CharSetHuffmEncoding(HuffmanT T, HuffmanCode H)    //根据哈夫曼树求哈夫曼编码表H
{int c,p,i;          //c和p分别指示T中孩子和双亲位置char cd[n+1];       //临时存放编码int start;          //指示编码在cd中的位置cd[n]='\0';         //编码结束符for(i=0; i<n; i++){H[i].ch = T[i].data;start = n;c=i;while((p=T[c].parent)>=0)   //回溯到T[c]是树根位置{cd[--start] = (T[p].lchild==c) ? '0':'1';   //T[c]是T[p]的左孩子,生成代码0否则生成1c=p;}strcpy(H[i].bits,&cd[start]);}printf("creatHcode over\n");
}void PHUM(char *file,char *s);char s[30000]={3};
void PrintHUffmancode(HuffmanCode H)        //将文件中字符的哈夫曼编码打印出来并将其写入指定txt文件
{FILE *fp;char ch;char Filename[80];char file[80];printf ("output the Huffmancode of which file:");scanf("%s",Filename);if((fp=fopen(Filename,"r"))==NULL)    printf("failda\n");ch = fgetc(fp);int L =0;printf("1");while(ch != EOF){for(int i = 0; i<n; i++){if(H[i].ch == ch) {printf("%s",H[i].bits);sprintf(s+L,"%s",H[i].bits);L=strlen(s);}}ch = fgetc(fp);}printf("\n");for(int k =0;k<n;k++){printf("%c-%s\n",H[k].ch, H[k].bits);}// printf("3\n");fclose(fp);printf("stand by\n");PHUM(file,s);}void PHUM(char *file,char *s)
{FILE *fp;int i=0;printf ("save your Huffmancode to the file:");scanf("%s",file);if((fp=fopen(file,"w"))==NULL)    printf("faild\n");while(s[i]!='\0'){// fwrite(s,1,strlen(s),fp);// fprintf(fp,'%c',s[i]);fprintf(fp,"%c",s[i]);i++;}fclose(fp);printf("write over\n");}void Printftxt(HuffmanT T,char a[])  //左0右1
{int root,c;int i = 0;FILE *fp;char ch;char Filename[30];printf ("print words acroding to Huffmancode:");scanf("%s",Filename);if((fp=fopen(Filename,"r"))==NULL)    printf("faild\n");// printf("1\n");for(int j =0; j<129; j++)     //找到根节点{if(T[j].parent==-1){root = j;break;}}ch=fgetc(fp);while(ch!=EOF){c=root;while((T[c].lchild != -1) || (T[c].rchild != -1)){if(ch=='0'){c=T[c].lchild;ch = fgetc(fp);}else if(ch=='1'){c=T[c].rchild;ch = fgetc(fp);}// printf("2");}printf("%c",T[c].data);// ch = fgetc(fp);}fclose(fp);
}int main()
{HuffmanT T;HuffmanCode H;CreartHT(T);     //读入文件构造一个哈夫曼树初始化并输入权值 输出各字符权值CharSetHuffmEncoding(T,H);   //根据哈夫曼树构造哈夫曼表,并输出各字符的编码PrintHUffmancode(H);        //输出某个文件中文本的哈夫曼编码,并把它保存在指定文件中Printftxt(T,s);   //根据哈夫曼编码打印文本文件字符}

三:校内项目二:推箱子小游戏

这个项目比较常见了(贪吃蛇啊,2048游戏啊,33/44拼图游戏啊(可以导入头文件hashmap简化代码)都是可以的,我比较懒惰就写推箱子了),但是能够体现个人的代码能力和思考问题的能力,完全是可以写在简历的上边的,我个人写的比较乱,大家如果不喜欢可以百度到其他的代码,融入自己的思考,在面试官询问时能够应对深度的问题就可以,我这边是我即将好几天才完成的写一遍,大家见谅。大家主要是整理出自己实现这个项目的话的思路和可能碰到的问题,在项目中碰到了问题怎么解决的是比较受面试官关注的
在这里插入图片描述
效果就是这样,大家根据自己的爱好设置一个可实现的地图就可以了;游戏中的人物、箱子、墙壁、球都是字符构成的。通过wasd键移动,规则的话就是推箱子的规则,也就不多说了。
代码我会详细的写,不要担心,适合新手操作

(1)方法列表

//主函数
void main();//初始化一些数据
initData();//在控制台上打印地图
drawMap();//向上移动
moveUp();//向左移动
moveLeft()//向下移动
moveDown()//向右移动
moveRight();

这几个方法都顾名思义,而且用意也非常明确,就initData可能不知道具体用处,但是没有什么大问题。唯一的问题就是,上左下右的顺序可能会逼死几个强迫症患者,哈哈。

(2)参数列表

为了方便,我把include和宏定义也放到参数列表当中
//导入函数库
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>//宏定义
#define WIDTH 8
#define HEIGHT 8//定义地图数组,二维数组有两个维度,而地图也是二维的矩形
int map[HEIGHT][WIDTH] = {{0, 0, 1, 1, 1, 0, 0, 0},{0, 0, 1, 4, 1, 0, 0, 0},{0, 0, 1, 0, 1, 1, 1, 1},{1, 1, 1, 3, 0, 3, 4, 1},{1, 4, 0, 3, 2, 1, 1, 1},{1, 1, 1, 1, 3, 1, 0, 0},{0, 0, 0, 1, 4, 1, 0, 0},{0, 0, 0, 1, 1, 1, 0, 0} 
};//人的位置,在二维地图中,我们可以用坐标表示一个人的位置,就好比经纬度
int x, y;//箱子的个数,推箱子肯定要有箱子嘛。
int boxs;--------------------------------------------------------------------------------------------------------------------------------
这里参数不多,其中横为x,纵为y,另外这里再规定一下map的一些东西:/**
*	0	表示空
*	1	表示墙
*	2	表示人
*	3	表示箱子
*	4	表示目的地(球)
*	5	表示已完成的箱子
*/

(3)具体函数讲解和实现

1、main函数
int main(int argc, char *argv[]) {char direction;		//存储键盘按的方向 initData();			//初始化一些数据//开始游戏的循环,这里是个死循环,每按一次按钮循环一次while(1){//每次循环的开始清除屏幕system("cls");//绘画地图drawMap();//判断,当boxs的数量0时,!0为真,然后走break跳出循环(结束游戏) if(!boxs){break;}//键盘输入方向,这里使用getch,因为getch读取字符不会显示在屏幕上direction = getch();//用switch判断用户输入的方向switch(direction){case 'w'://按w时,调用向上移动函数moveUp();break;case 'a'://按a时,调用向左移动函数moveLeft(); break;case 's':moveDown();break;case 'd':moveRight();break; }}  //当跳出循环时,运行该语句,游戏结束printf("恭喜你完成游戏!※");return 0;
}

我大概说一下流程,循环外面没有什么特别的。initData()只是一些简单数据的初始化,不需要太在意。循环中大致流程如下:

清除屏幕
绘制地图
判断游戏是否结束
对用户按下的按钮进行反馈
进入循环体,先清除屏幕,再绘制地图,然后再判断游戏是否结束。可能大家对这个顺序不是很理解,这里我们先不考虑判断游戏结束的问题。我们把清屏和绘制地图合在一起,简称“重绘地图”,而游戏结束的判断先不考虑,那么流程就简化为“重绘地图 + 响应用户的操作”。简单来说就是,用户按一下按钮,我改变一下地图。

2、initData()
void initData(){int i, j;//加载数据时让用户等待,一般情况加载数据比较快printf("游戏加载中,请稍后........."); //遍历地图中的数据for(i = 0; i < HEIGHT; i++){for(j = 0; j < WIDTH; j++){//遍历到2(人)时,记录人的坐标。x, y是前面定义的全局变量if(map[i][j] == 2){x = j;y = i;} //遍历到3时,箱子的数目增加。boxs是前面定义的全局变量 if(map[i][j] == 3){boxs++;}}} 
}

这个方法很简单,就是遍历地图,然后初始化人的位置和箱子的个数。这里有一点要注意一下,就是到底内层循环是WIDTH还是外层循环是WIDTH。
如图,在遍历过程中。外层循环控制行数,即HEIGHT。那么内层循环应该是WIDTH。

3、drawMap()
void drawMap(){int i, j;for(i = 0; i < WIDTH; i++){for(j = 0; j < HEIGHT; j++){switch(map[i][j]){case 0:printf("  ");break;case 1:printf("■");break;case 2:printf("♀");break;case 3:printf("◆");break;case 4:printf("●");break;case 5:printf("★");break; }}printf("\n");}
}

这里也非常简单,变量map中的元素,然后通过switch判断应该输出的内容。然后内层循环每走完一次就换行。

4、moveUp()

这个函数内容有点多,想讲一下大概思路:

向上移有两种情况
1、前面为空白
这种情况有两个步骤
(1)将人当前的位置设置为空白(0),
(2)再讲人前面的位置设置为人(2)
2、前面为箱子
当前面为箱子时有三种情况
1、箱子前面为空白
移动人和箱子,这个操作有三个步骤
(1)将人当前位置设置为空(0)
(2)将箱子位置设置为人(2)
(3)将箱子前面设置为箱子(3)
2、箱子前面为墙
这种情况不需要做任何操作
3、箱子前面为终点
这种情况有四个个步骤
(1)将人的位置设置为空(0)
(2)将箱子的位置设置为人(2)
(3)将终点位置设置为★(5)
(4)箱子boxs的数量减一
3、前面为墙
这种情况最简单,不需要做任何操作
4、前面为终点
我这里没有考虑太多,这种情况不做操作。(如果更换地图的话可能需要修改代码)

具体代码如下,解析我全写在注释里面:

void moveUp(){//定义变量存放人物上方的坐标int ux, uy; //当上方没有元素时,直接return	(其实人不可能在边缘)if(y == 0){return;}//记录上方坐标,x为横,y为纵,所有ux = x, uy = y - 1;ux = x;uy = y - 1; //上方为已完成的箱子if(map[uy][ux] == 5){return;} //假设上方为墙,直接return,这个和上面的判断可以合在一起,这里为了看清楚分开写 if(map[uy][ux] == 1){return;}//假设上方为箱子if(map[uy][ux] == 3){//判断箱子上方是否为墙 if(map[uy - 1][ux] == 1){return;}//判断箱子上方是否为终点if(map[uy - 1][ux] == 4){//将箱子上面内容赋值为5★ map[uy - 1][ux] = 5;map[uy][ux] = 0;//箱子的数目减1	boxs--; }else{//移动箱子map[uy - 1][ux] = 3;}}//当上面几种return的情况都没遇到,人肯定会移动,移动操作如下map[y][x] = 0;map[uy][ux] = 2;//更新人的坐标y = uy; 
} 

这是一个方向的,其它方向要考虑的问题也和前面一样,我也就不赘述了。

6、moveLeft()

这里大致都和上面一样,就是在记录左边坐标时,应该应该是lx = x - 1。

void moveLeft(){//定义变量存放人物左边的坐标int lx, ly; //当左边没有元素时,直接return	if(x == 0){return;}//记录左边坐标lx = x - 1;ly = y; //左边为已完成方块if(map[ly][lx] == 5){return;} //假设左边为墙,直接return if(map[ly][lx] == 1){return;}//假设左边为箱子if(map[ly][lx] == 3){//判断箱子左边是否为墙 if(map[ly][lx - 1] == 1){return;}//判断箱子左边是否为球if(map[ly][lx - 1] == 4){//将箱子左边内容赋值为5★ map[ly][lx - 1] = 5;map[ly][lx] = 0;//箱子的数目减1 boxs--; }else{//移动箱子 map[ly][lx - 1] = 3; }}map[y][x] = 0;map[ly][lx] = 2;x = lx; 
}
7、moveDown()
这里在判断边界时,判断的是 y == HEIGHT - 1。void moveDown(){//定义变量存放人物下方的坐标int dx, dy; //当下方没有元素时,直接return	if(y == HEIGHT - 1){return;}//记录下方坐标dx = x;dy = y + 1; //下方为已完成方块if(map[dy][dx] == 5){return;} //假设下方为墙,直接return if(map[dy][dx] == 1){return;}//假设下方为箱子if(map[dy][dx] == 3){//判断箱子下方是否为墙 if(map[dy + 1][dx] == 1){return;}//判断箱子下方是否为球if(map[dy + 1][dx] == 4){//将箱子下面内容赋值为5★ map[dy + 1][dx] = 5;map[dy][dx] = 0;//箱子的数目减1 boxs--; }else{//移动箱子map[dy + 1][dx] = 3; }}map[y][x] = 0;map[dy][dx] = 2;y = dy; 
}
8、moveRight()

这里也没什么特别说的:

void moveRight(){//定义变量存放人物右边的坐标int rx, ry; //当右边没有元素时,直接return	if(x == WIDTH - 1){return;}//记录右边坐标rx = x + 1;ry = y; //右边为已完成方块if(map[ry][rx] == 5){return;} //假设右边为墙,直接return if(map[ry][rx] == 1){return;}//假设右边为箱子if(map[ry][rx] == 3){//判断箱子右边是否为墙 if(map[ry][rx + 1] == 1){return;}//判断箱子左边是否为球if(map[ry][rx + 1] == 4){//将箱子右边内容赋值为5★ map[ry][rx + 1] = 5;map[ry][rx] = 0;//箱子的数目减1 boxs--; }else{//移动箱子 map[ry][rx + 1] = 3; }}map[y][x] = 0;map[ry][rx] = 2;x = rx; 
}

四:项目:linux下C++ socket网络编程——即时通信系统(博主目前是跟随视频在学习中,服务器开发和客户端开发个人挺喜欢,感兴趣的或者投简历到此的大佬可以考察一下目前我的服务器编程的能力)

一:项目内容

本项目使用C++实现一个具备服务器端和客户端即时通信且具有私聊功能的聊天室。
目的是学习C++网络开发的基本概念,同时也可以熟悉下Linux下的C++程序编译和简单MakeFile编写

二:需求分析

这个聊天室主要有两个程序:
1.服务端:能够接受新的客户连接,并将每个客户端发来的信息,广播给对应的目标客户端。
2.客户端:能够连接服务器,并向服务器发送消息,同时可以接收服务器发来的消息。
即最简单的C/S模型。

三:抽象与细化

服务端类需要支持:

1.支持多个客户端接入,实现聊天室基本功能。
2.启动服务,建立监听端口等待客户端连接。
3.使用epoll机制实现并发,增加效率。
4.客户端连接时,发送欢迎消息,并存储连接记录。
5.客户端发送消息时,根据消息类型,广播给所有用户(群聊)或者指定用户(私聊)。
6.客户端请求退出时,对相应连接信息进行清理。

客户端类需要支持:

1.连接服务器。
2.支持用户输入消息,发送给服务端。
3.接受并显示服务端发来的消息。
4.退出连接。

涉及两个事情,一个写,一个读。所以客户端需要两个进程分别支持以下功能。

子进程:

1.等待用户输入信息。
2.将聊天信息写入管道(pipe),并发送给父进程。

父进程:

1.使用epoll机制接受服务端发来的消息,并显示给用户,使用户看到其他用户的信息。
2.将子进程发送的聊天信息从管道(pipe)中读取出来,并发送给客户端。

四:基础步骤记忆

TCP服务端通信常规步骤:

1.socket()创建TCP套接字
2.bind()将创建的套接字绑定到一个本地地址和端口上
3.listen(),将套接字设为监听模式,准备接受客户请求
4.accept()等用户请求到来时接受,返回一个对应此连接新套接字
5.用accept()返回的套接字和客户端进行通信,recv()/send() 接受/发送信息。
6.返回,等待另一个客户请求。
7.关闭套接字

TCP客户端通信常规步骤:

1.socket()创建TCP套接字。
2.connect()建立到达服务器的连接。
3.与客户端进行通信,recv()/send()接受/发送信息,write()/read() 子进程写入管道,父进程从管道中读取信息然后send给客户端
4. close() 关闭客户连接。

五:相关技术介绍

1.socket 阻塞与非阻塞

阻塞与非阻塞关注的是程序在等待调用结果时(消息,返回值)的状态。

阻塞调用是指在调用结果返回前,当前线程会被挂起,调用线程只有在得到调用结果之后才会返回。

非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程。

eg. 你打电话问书店老板有没有《网络编程》这本书,老板去书架上找,如果是阻塞式调用,你就会把自己一直挂起,守在电话边上,直到得到这本书有或者没有的答案。如果是非阻塞式调用,你可以干别的事情去,隔一段时间来看一下老板有没有告诉你结果。

同步异步是对书店老板而言(同步老板不会提醒你找到结果了,异步老板会打电话告诉你),阻塞和非阻塞是对你而言。

socket()函数创建套接字时,默认的套接字都是阻塞的,非阻塞设置方式代码:

//将文件描述符设置为非阻塞方式(利用fcntl函数)
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);

2. epoll

当服务端的人数越来越多,会导致资源吃紧,I/O效率越来越低,这时就应该考虑epoll,epoll是Linux内核为处理大量句柄而改进的poll,是linux特有的I/O函数。其特点如下:

1)epoll是Linux下多路复用IO接口select/poll的增强版本,其实现和使用方式与select/poll大有不同,epoll通过一组函数来完成有关任务,而不是一个函数。

2)epoll之所以高效,是因为epoll将用户关心的文件描述符放到内核里的一个事件列表中,而不是像select/poll每次调用都需要重复传入文件描述符集或事件集(大量拷贝开销),比如一个事件发生,epoll无需遍历整个被监听的描述符集,而只需要遍历哪些被内核IO事件异步唤醒而加入就绪队列的描述符集合即可。

3)epoll有两种工作方式,LT(Level triggered) 水平触发 、ET(Edge triggered)边沿触发。LT是select/poll的工作方式,比较低效,而ET是epoll具有的高速工作方式。

Epoll 用法(三步曲):

第一步:int epoll_create(int size)系统调用,创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll支持的最大句柄数。

第二步:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 事件注册函数

参数 epfd为epoll的句柄。参数op 表示动作 三个宏来表示:EPOLL_CTL_ADD注册新fd到epfd 、EPOLL_CTL_MOD 修改已经注册的fd的监听事件、EPOLL_CTL_DEL从epfd句柄中删除fd。参数fd为需要监听的标识符。参数结构体epoll_event告诉内核需要监听的事件。

第三步:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 等待事件的产生,通过调用收集在epoll监控中已经发生的事件。参数struct epoll_event 是事件队列 把就绪的事件放进去。

eg. 服务端使用epoll的时候步骤如下:

1.调用epoll_create()在linux内核中创建一个事件表。

2.然后将文件描述符(监听套接字listener)添加到事件表中

3.在主循环中,调用epoll_wait()等待返回就绪的文件描述符集合。

4.分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件。

六:代码结构

#创建所需的文件
touch Common.h Client.h Client.cpp ClientMain.cpp
touch Server.h Server.cpp ServerMain.cpp
touch Makefile

每个文件的作用:

1.Common.h:公共头文件,包括所有需要的宏以及socket网络编程头文件,以及消息结构体(用来表示消息类别等)
2.Client.h Client.cpp :客户端类的实现
3.Server.h Server.cpp : 服务端类的实现
4.ClientMain.cpp ServerMain.cpp 客户端及服务端的主函数。

七:代码实现

Common.h

定义一些共用的宏定义,包括一些共用的网络编程相关头文件。

1)定义一个函数将文件描述符fd添加到epfd表示的内核事件表中供客户端和服务端两个类使用。

2)定义一个信息数据结构,用来表示传送的信息,结构体包括发送方fd, 接收方fd,用来表示消息类别的type,还有文字信息。

函数recv() send() write() read() 参数传递是字符串,所以在传送前/接受后要把结构体转换为字符串/字符串转换为结构体。

#ifndef CHATROOM_COMMON_H
#define CHATROOM_COMMON_H
#include <iostream>
#include <list>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 默认服务器端IP地址
#define SERVER_IP "127.0.0.1"
// 服务器端口号
#define SERVER_PORT 8888
// int epoll_create(int size)中的size
// 为epoll支持的最大句柄数
#define EPOLL_SIZE 5000
// 缓冲区大小65535
#define BUF_SIZE 0xFFFF
// 新用户登录后的欢迎信息
#define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d"
// 其他用户收到消息的前缀
#define SERVER_MESSAGE "ClientID %d say >> %s"
#define SERVER_PRIVATE_MESSAGE "Client %d say to you privately >> %s"
#define SERVER_PRIVATE_ERROR_MESSAGE "Client %d is not in the chat room yet~"
// 退出系统
#define EXIT "EXIT"
// 提醒你是聊天室中唯一的客户
#define CAUTION "There is only one int the char room!"
// 注册新的fd到epollfd中
// 参数enable_et表示是否启用ET模式,如果为True则启用,否则使用LT模式
static void addfd( int epollfd, int fd, bool enable_et )
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;
if( enable_et )
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
// 设置socket为非阻塞模式
// 套接字立刻返回,不管I/O是否完成,该函数所在的线程会继续运行
//eg. 在recv(fd...)时,该函数立刻返回,在返回时,内核数据还没准备好会返回WSAEWOULDBLOCK错误代码
fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0)| O_NONBLOCK);
printf("fd added to epoll!\n\n");
}
//定义信息结构,在服务端和客户端之间传送
struct Msg
{
int type;
int fromID;
int toID;
char content[BUF_SIZE];
};
#endif // CHATROOM_COMMON_H

服务端类 Server.h Server.cpp

服务端需要的接口:

1)init()初始化
2)Start()启动服务
3)Close()关闭服务
4)广播消息给所有客户端函数 SendBroadcastMessage()

服务端的主循环中每次都会检查并处理EPOLL中的就绪事件,而就绪事件列表主要是两种类型:新连接或新消息。服务器会依次从就绪事件列表里提取事件进行处理,如果是新连接则accept()然后addfd(),如果是新消息则SendBroadcastMessage()实现聊天功能。

Server.h

#ifndef CHATROOM_SERVER_H
#define CHATROOM_SERVER_H
#include <string>
#include "Common.h"
using namespace std;
// 服务端类,用来处理客户端请求
class Server {
public:
// 无参数构造函数
Server();
// 初始化服务器端设置
void Init();
// 关闭服务
void Close();
// 启动服务端
void Start();
private:
// 广播消息给所有客户端
int SendBroadcastMessage(int clientfd);
// 服务器端serverAddr信息
struct sockaddr_in serverAddr;
//创建监听的socket
int listener;
// epoll_create创建后的返回值
int epfd;
// 客户端列表
list<int> clients_list;
};
//Server.cpp
#include <iostream>
#include "Server.h"
using namespace std;
// 服务端类成员函数
// 服务端类构造函数
Server::Server(){
// 初始化服务器地址和端口
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
// 初始化socket
listener = 0;
// epool fd
epfd = 0;
}
// 初始化服务端并启动监听
void Server::Init() {
cout << "Init Server..." << endl;
//创建监听socket
listener = socket(PF_INET, SOCK_STREAM, 0);
if(listener < 0) { perror("listener"); exit(-1);}
//绑定地址
if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind error");
exit(-1);
}
//监听
int ret = listen(listener, 5);
if(ret < 0) {
perror("listen error");
exit(-1);
}
cout << "Start to listen: " << SERVER_IP << endl;
//在内核中创建事件表 epfd是一个句柄
epfd = epoll_create (EPOLL_SIZE);
if(epfd < 0) {
perror("epfd error");
exit(-1);
}
//往事件表里添加监听事件
addfd(epfd, listener, true);
}
// 关闭服务,清理并关闭文件描述符
void Server::Close() {
//关闭socket
close(listener);
//关闭epoll监听
close(epfd);
}
// 发送广播消息给所有客户端
int Server::SendBroadcastMessage(int clientfd)
{
// buf[BUF_SIZE] 接收新消息
// message[BUF_SIZE] 保存格式化的消息
char recv_buf[BUF_SIZE];
char send_buf[BUF_SIZE];
Msg msg;
bzero(recv_buf, BUF_SIZE);
// 接收新消息
cout << "read from client(clientID = " << clientfd << ")" << endl;
int len = recv(clientfd, recv_buf, BUF_SIZE, 0);
//清空结构体,把接受到的字符串转换为结构体
memset(&msg,0,sizeof(msg));
memcpy(&msg,recv_buf,sizeof(msg));
//判断接受到的信息是私聊还是群聊
msg.fromID=clientfd;
if(msg.content[0]=='\\'&&isdigit(msg.content[1])){
msg.type=1;
msg.toID=msg.content[1]-'0';
memcpy(msg.content,msg.content+2,sizeof(msg.content));
}
else
msg.type=0;
// 如果客户端关闭了连接
if(len == 0)
{
close(clientfd);
// 在客户端列表中删除该客户端
clients_list.remove(clientfd);
cout << "ClientID = " << clientfd
<< " closed.\n now there are "
<< clients_list.size()
<< " client in the char room"
<< endl;
}
// 发送广播消息给所有客户端
else
{
// 判断是否聊天室还有其他客户端
if(clients_list.size() == 1){
// 发送提示消息
memcpy(&msg.content,CAUTION,sizeof(msg.content));
bzero(send_buf, BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
send(clientfd, send_buf, sizeof(send_buf), 0);
return len;
}
//存放格式化后的信息
char format_message[BUF_SIZE];
//群聊
if(msg.type==0){
// 格式化发送的消息内容 #define SERVER_MESSAGE "ClientID %d say >> %s"
sprintf(format_message, SERVER_MESSAGE, clientfd, msg.content);
memcpy(msg.content,format_message,BUF_SIZE);
// 遍历客户端列表依次发送消息,需要判断不要给来源客户端发
list<int>::iterator it;
for(it = clients_list.begin(); it != clients_list.end(); ++it) {
if(*it != clientfd){
//把发送的结构体转换为字符串
bzero(send_buf, BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
if( send(*it,send_buf, sizeof(send_buf), 0) < 0 ) {
return -1;
}
}
}
}
//私聊
if(msg.type==1){
bool private_offline=true;
sprintf(format_message, SERVER_PRIVATE_MESSAGE, clientfd, msg.content);
memcpy(msg.content,format_message,BUF_SIZE);
// 遍历客户端列表依次发送消息,需要判断不要给来源客户端发
list<int>::iterator it;
for(it = clients_list.begin(); it != clients_list.end(); ++it) {
if(*it == msg.toID){
private_offline=false;
//把发送的结构体转换为字符串
bzero(send_buf, BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
if( send(*it,send_buf, sizeof(send_buf), 0) < 0 ) {
return -1;
}
}
}
//如果私聊对象不在线
if(private_offline){
sprintf(format_message,SERVER_PRIVATE_ERROR_MESSAGE,msg.toID);
memcpy(msg.content,format_message,BUF_SIZE);
bzero(send_buf,BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
if(send(msg.fromID,send_buf,sizeof(send_buf),0)<0)
return -1;
}
}
}
return len;
}
// 启动服务端
void Server::Start() {
// epoll 事件队列
static struct epoll_event events[EPOLL_SIZE];
// 初始化服务端
Init();
//主循环
while(1)
{
//epoll_events_count表示就绪事件的数目
int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if(epoll_events_count < 0) {
perror("epoll failure");
break;
}
cout << "epoll_events_count =\n" << epoll_events_count << endl;
//处理这epoll_events_count个就绪事件
for(int i = 0; i < epoll_events_count; ++i)
{
int sockfd = events[i].data.fd;
//新用户连接
if(sockfd == listener)
{
struct sockaddr_in client_address;
socklen_t client_addrLength = sizeof(struct sockaddr_in);
int clientfd = accept( listener, ( struct sockaddr* )&client_address, &client_addrLength );
cout << "client connection from: "
<< inet_ntoa(client_address.sin_addr) << ":"
<< ntohs(client_address.sin_port) << ", clientfd = "
<< clientfd << endl;
addfd(epfd, clientfd, true);
// 服务端用list保存用户连接
clients_list.push_back(clientfd);
cout << "Add new clientfd = " << clientfd << " to epoll" << endl;
cout << "Now there are " << clients_list.size() << " clients int the chat room" << endl;
// 服务端发送欢迎信息
cout << "welcome message" << endl;
char message[BUF_SIZE];
bzero(message, BUF_SIZE);
sprintf(message, SERVER_WELCOME, clientfd);
int ret = send(clientfd, message, BUF_SIZE, 0);
if(ret < 0) {
perror("send error");
Close();
exit(-1);
}
}
//处理用户发来的消息,并广播,使其他用户收到信息
else {
int ret = SendBroadcastMessage(sockfd);
if(ret < 0) {
perror("error");
Close();
exit(-1);
}
}
}
}
// 关闭服务
Close();
}

客户端类实现

需要的接口:

1)连接服务端connect()
2)退出连接close()
3)启动客户端Start()

Client.h

#ifndef CHATROOM_CLIENT_H
#define CHATROOM_CLIENT_H
#include <string>
#include "Common.h"
using namespace std;
// 客户端类,用来连接服务器发送和接收消息
class Client {
public:
// 无参数构造函数
Client();
// 连接服务器
void Connect();
// 断开连接
void Close();
// 启动客户端
void Start();
private:
// 当前连接服务器端创建的socket
int sock;
// 当前进程ID
int pid;
// epoll_create创建后的返回值
int epfd;
// 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写
int pipe_fd[2];
// 表示客户端是否正常工作
bool isClientwork;
// 聊天信息
Msg msg;
//结构体要转换为字符串
char send_buf[BUF_SIZE];
char recv_buf[BUF_SIZE];
//用户连接的服务器 IP + port
struct sockaddr_in serverAddr;
};

Client.cpp

#include <iostream>
#include "Client.h"
using namespace std;
// 客户端类成员函数
// 客户端类构造函数
Client::Client(){
// 初始化要连接的服务器地址和端口
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
// 初始化socket
sock = 0;
// 初始化进程号
pid = 0;
// 客户端状态
isClientwork = true;
// epool fd
epfd = 0;
}
// 连接服务器
void Client::Connect() {
cout << "Connect Server: " << SERVER_IP << " : " << SERVER_PORT << endl;
// 创建socket
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock < 0) {
perror("sock error");
exit(-1);
}
// 连接服务端
if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("connect error");
exit(-1);
}
// 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写
if(pipe(pipe_fd) < 0) {
perror("pipe error");
exit(-1);
}
// 创建epoll
epfd = epoll_create(EPOLL_SIZE);
if(epfd < 0) {
perror("epfd error");
exit(-1);
}
//将sock和管道读端描述符都添加到内核事件表中
addfd(epfd, sock, true);
addfd(epfd, pipe_fd[0], true);
}
// 断开连接,清理并关闭文件描述符
void Client::Close() {
if(pid){
//关闭父进程的管道和sock
close(pipe_fd[0]);
close(sock);
}else{
//关闭子进程的管道
close(pipe_fd[1]);
}
}
// 启动客户端
void Client::Start() {
// epoll 事件队列
static struct epoll_event events[2];
// 连接服务器
Connect();
// 创建子进程
pid = fork();
// 如果创建子进程失败则退出
if(pid < 0) {
perror("fork error");
close(sock);
exit(-1);
} else if(pid == 0) {
// 进入子进程执行流程
//子进程负责写入管道,因此先关闭读端
close(pipe_fd[0]);
// 输入exit可以退出聊天室
cout << "Please input 'exit' to exit the chat room" << endl;
cout<<"\\ + ClientID to private chat "<<endl;
// 如果客户端运行正常则不断读取输入发送给服务端
while(isClientwork){
//清空结构体
memset(msg.content,0,sizeof(msg.content));
fgets(msg.content, BUF_SIZE, stdin);
// 客户输出exit,退出
if(strncasecmp(msg.content, EXIT, strlen(EXIT)) == 0){
isClientwork = 0;
}
// 子进程将信息写入管道
else {
//清空发送缓存
memset(send_buf,0,BUF_SIZE);
//结构体转换为字符串
memcpy(send_buf,&msg,sizeof(msg));
if( write(pipe_fd[1], send_buf, sizeof(send_buf)) < 0 ) {
perror("fork error");
exit(-1);
}
}
}
} else {
//pid > 0 父进程
//父进程负责读管道数据,因此先关闭写端
close(pipe_fd[1]);
// 主循环(epoll_wait)
while(isClientwork) {
int epoll_events_count = epoll_wait( epfd, events, 2, -1 );
//处理就绪事件
for(int i = 0; i < epoll_events_count ; ++i)
{
memset(recv_buf,0,sizeof(recv_buf));
//服务端发来消息
if(events[i].data.fd == sock)
{
//接受服务端广播消息
int ret = recv(sock, recv_buf, BUF_SIZE, 0);
//清空结构体
memset(&msg,0,sizeof(msg));
//将发来的消息转换为结构体
memcpy(&msg,recv_buf,sizeof(msg));
// ret= 0 服务端关闭
if(ret == 0) {
cout << "Server closed connection: " << sock << endl;
close(sock);
isClientwork = 0;
} else {
cout << msg.content << endl;
}
}
//子进程写入事件发生,父进程处理并发送服务端
else {
//父进程从管道中读取数据
int ret = read(events[i].data.fd, recv_buf, BUF_SIZE);
// ret = 0
if(ret == 0)
isClientwork = 0;
else {
// 将从管道中读取的字符串信息发送给服务端
send(sock, recv_buf, sizeof(recv_buf), 0);
}
}
}//for
}//while
}
// 退出进程
Close();
}

ClientMain.cpp

#include "Client.h"
// 客户端主函数
// 创建客户端对象后启动客户端
int main(int argc, char *argv[]) {
Client client;
client.Start();
return 0;
}

ServerMain.cpp

#include "Server.h"
// 服务端主函数
// 创建服务端对象后启动服务端
int main(int argc, char *argv[]) {
Server server;
server.Start();
return 0;
}

最后是Makefile 文件 对上面的文件进行编译

CC = g++
CFLAGS = -std=c++11
all: ClientMain.cpp ServerMain.cpp Server.o Client.o
$(CC) $(CFLAGS) ServerMain.cpp Server.o -o chatroom_server
$(CC) $(CFLAGS) ClientMain.cpp Client.o -o chatroom_client
Server.o: Server.cpp Server.h Common.h
$(CC) $(CFLAGS) -c Server.cpp
Client.o: Client.cpp Client.h Common.h
$(CC) $(CFLAGS) -c Client.cpp
clean:
rm -f *.o chatroom_server chatroom_client

五:实习项目(较长较难,个人选择,大厂选择)

首先声明,这是非线程安全的。

一、概述

在 C/C++ 中,内存管理是一个非常棘手的问题,我们在编写一个程序的时候几乎不可避免的要遇到内存的分配逻辑,这时候随之而来的有这样一些问题:是否有足够的内存可供分配? 分配失败了怎么办? 如何管理自身的内存使用情况? 等等一系列问题。在一个高可用的软件中,如果我们仅仅单纯的向操作系统去申请内存,当出现内存不足时就退出软件,是明显不合理的。正确的思路应该是在内存不足的时,考虑如何管理并优化自身已经使用的内存,这样才能使得软件变得更加可用。本次项目我们将实现一个内存池,并使用一个栈结构来测试我们的内存池提供的分配性能。最终,我们要实现的内存池在栈结构中的性能,要远高于使用 std::allocator 和 std::vector,如下图所示:
在这里插入图片描述

项目涉及的知识点

C++ 中的内存分配器 std::allocator
内存池技术
手动实现模板链式栈
链式栈和列表栈的性能比较

内存池简介

内存池是池化技术中的一种形式。通常我们在编写程序的时候回使用 new delete 这些关键字来向操作系统申请内存,而这样造成的后果就是每次申请内存和释放内存的时候,都需要和操作系统的系统调用打交道,从堆中分配所需的内存。如果这样的操作太过频繁,就会找成大量的内存碎片进而降低内存的分配性能,甚至出现内存分配失败的情况。

而内存池就是为了解决这个问题而产生的一种技术。从内存分配的概念上看,内存申请无非就是向内存分配方索要一个指针,当向操作系统申请内存时,操作系统需要进行复杂的内存管理调度之后,才能正确的分配出一个相应的指针。而这个分配的过程中,我们还面临着分配失败的风险。

所以,每一次进行内存分配,就会消耗一次分配内存的时间,设这个时间为 T,那么进行 n 次分配总共消耗的时间就是 nT;如果我们一开始就确定好我们可能需要多少内存,那么在最初的时候就分配好这样的一块内存区域,当我们需要内存的时候,直接从这块已经分配好的内存中使用即可,那么总共需要的分配时间仅仅只有 T。当 n 越大时,节约的时间就越多。

二、主函数设计

我们要设计实现一个高性能的内存池,那么自然避免不了需要对比已有的内存,而比较内存池对内存的分配性能,就需要实现一个需要对内存进行动态分配的结构(比如:链表栈),为此,可以写出如下的代码:

#include <iostream>   // std::cout, std::endl
#include <cassert>    // assert()
#include <ctime>      // clock()
#include <vector>     // std::vector#include "MemoryPool.hpp"  // MemoryPool<T>
#include "StackAlloc.hpp"  // StackAlloc<T, Alloc>// 插入元素个数
#define ELEMS 10000000
// 重复次数
#define REPS 100int main()
{clock_t start;// 使用 STL 默认分配器StackAlloc<int, std::allocator<int> > stackDefault;start = clock();for (int j = 0; j < REPS; j++) {assert(stackDefault.empty());for (int i = 0; i < ELEMS; i++)stackDefault.push(i);for (int i = 0; i < ELEMS; i++)stackDefault.pop();}std::cout << "Default Allocator Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";// 使用内存池StackAlloc<int, MemoryPool<int> > stackPool;start = clock();for (int j = 0; j < REPS; j++) {assert(stackPool.empty());for (int i = 0; i < ELEMS; i++)stackPool.push(i);for (int i = 0; i < ELEMS; i++)stackPool.pop();}std::cout << "MemoryPool Allocator Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";return 0;
}

在上面的两段代码中,StackAlloc 是一个链表栈,接受两个模板参数,第一个参数是栈中的元素类型,第二个参数就是栈使用的内存分配器。

因此,这个内存分配器的模板参数就是整个比较过程中唯一的变量,使用默认分配器的模板参数为 std::allocator,而使用内存池的模板参数为 MemoryPool。

std::allocator 是 C++标准库中提供的默认分配器,他的特点就在于我们在 使用 new 来申请内存构造新对象的时候,势必要调用类对象的默认构造函数,而使用 std::allocator 则可以将内存分配和对象的构造这两部分逻辑给分离开来,使得分配的内存是原始、未构造的。

下面我们来实现这个链表栈。

三、模板链表栈

栈的结构非常的简单,没有什么复杂的逻辑操作,其成员函数只需要考虑两个基本的操作:入栈、出栈。为了操作上的方便,我们可能还需要这样一些方法:判断栈是否空、清空栈、获得栈顶元素。

#include <memory>
template <typename T>
struct StackNode_
{T data;StackNode_* prev;
};
// T 为存储的对象类型, Alloc 为使用的分配器, 并默认使用 std::allocator 作为对象的分配器
template <typename T, typename Alloc = std::allocator<T> >
class StackAlloc
{public:// 使用 typedef 简化类型名typedef StackNode_<T> Node;typedef typename Alloc::template rebind<Node>::other allocator;// 默认构造StackAlloc() { head_ = 0; }// 默认析构~StackAlloc() { clear(); }// 当栈中元素为空时返回 truebool empty() {return (head_ == 0);}// 释放栈中元素的所有内存void clear();// 压栈void push(T element);// 出栈T pop();// 返回栈顶元素T top() { return (head_->data); }private:// allocator allocator_;// 栈顶Node* head_;
};

简单的逻辑诸如构造、析构、判断栈是否空、返回栈顶元素的逻辑都非常简单,直接在上面的定义中实现了,下面我们来实现 clear(), push() 和 pop() 这三个重要的逻辑:

// 释放栈中元素的所有内存
void clear() {Node* curr = head_;// 依次出栈while (curr != 0){Node* tmp = curr->prev;// 先析构, 再回收内存allocator_.destroy(curr);allocator_.deallocate(curr, 1);curr = tmp;}head_ = 0;
}
// 入栈
void push(T element) {// 为一个节点分配内存Node* newNode = allocator_.allocate(1);// 调用节点的构造函数allocator_.construct(newNode, Node());// 入栈操作newNode->data = element;newNode->prev = head_;head_ = newNode;
}// 出栈
T pop() {// 出栈操作 返回出栈元素T result = head_->data;Node* tmp = head_->prev;allocator_.destroy(head_);allocator_.deallocate(head_, 1);head_ = tmp;return result;
}

至此,我们完成了整个模板链表栈,现在我们可以先注释掉 main() 函数中使用内存池部分的代码来测试这个连表栈的内存分配情况,我们就能够得到这样的结果:

在这里插入图片描述

在使用 std::allocator 的默认内存分配器中,在

#define ELEMS 10000000
#define REPS 100

的条件下,总共花费了近一分钟的时间。
如果觉得花费的时间较长,不愿等待,则你尝试可以减小这两个值

总结一

本节我们实现了一个用于测试性能比较的模板链表栈,目前的代码如下。在下一节中,我们开始详细实现我们的高性能内存池。

// StackAlloc.hpp#ifndef STACK_ALLOC_H
#define STACK_ALLOC_H#include <memory>template <typename T>
struct StackNode_
{T data;StackNode_* prev;
};// T 为存储的对象类型, Alloc 为使用的分配器,
// 并默认使用 std::allocator 作为对象的分配器
template <class T, class Alloc = std::allocator<T> >
class StackAlloc
{public:// 使用 typedef 简化类型名typedef StackNode_<T> Node;typedef typename Alloc::template rebind<Node>::other allocator;// 默认构造StackAlloc() { head_ = 0; }// 默认析构~StackAlloc() { clear(); }// 当栈中元素为空时返回 truebool empty() {return (head_ == 0);}// 释放栈中元素的所有内存void clear() {Node* curr = head_;while (curr != 0){Node* tmp = curr->prev;allocator_.destroy(curr);allocator_.deallocate(curr, 1);curr = tmp;}head_ = 0;}// 入栈void push(T element) {// 为一个节点分配内存Node* newNode = allocator_.allocate(1);// 调用节点的构造函数allocator_.construct(newNode, Node());// 入栈操作newNode->data = element;newNode->prev = head_;head_ = newNode;}// 出栈T pop() {// 出栈操作 返回出栈结果T result = head_->data;Node* tmp = head_->prev;allocator_.destroy(head_);allocator_.deallocate(head_, 1);head_ = tmp;return result;}// 返回栈顶元素T top() { return (head_->data); }private:allocator allocator_;Node* head_;
};#endif // STACK_ALLOC_H
// main.cpp#include <iostream>
#include <cassert>
#include <ctime>
#include <vector>// #include "MemoryPool.hpp"
#include "StackAlloc.hpp"// 根据电脑性能调整这些值
// 插入元素个数
#define ELEMS 25000000
// 重复次数
#define REPS 50int main()
{clock_t start;// 使用默认分配器StackAlloc<int, std::allocator<int> > stackDefault;start = clock();for (int j = 0; j < REPS; j++) {assert(stackDefault.empty());for (int i = 0; i < ELEMS; i++)stackDefault.push(i);for (int i = 0; i < ELEMS; i++)stackDefault.pop();}std::cout << "Default Allocator Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";// 使用内存池// StackAlloc<int, MemoryPool<int> > stackPool;// start = clock();// for (int j = 0; j < REPS; j++) {//     assert(stackPool.empty());//     for (int i = 0; i < ELEMS; i++)//       stackPool.push(i);//     for (int i = 0; i < ELEMS; i++)//       stackPool.pop();// }// std::cout << "MemoryPool Allocator Time: ";// std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";return 0;}

二、设计内存池

我们在模板链表栈中使用了默认构造器来管理栈操作中的元素内存,一共涉及到了 rebind::other, allocate(), dealocate(), construct(), destroy()这些关键性的接口。所以为了让代码直接可用,我们同样应该在内存池中设计同样的接口:

#ifndef MEMORY_POOL_HPP
#define MEMORY_POOL_HPP#include <climits>
#include <cstddef>template <typename T, size_t BlockSize = 4096>
class MemoryPool
{public:// 使用 typedef 简化类型书写typedef T*              pointer;// 定义 rebind<U>::other 接口template <typename U> struct rebind {typedef MemoryPool<U> other;};// 默认构造, 初始化所有的槽指针// C++11 使用了 noexcept 来显式的声明此函数不会抛出异常MemoryPool() noexcept {currentBlock_ = nullptr;currentSlot_ = nullptr;lastSlot_ = nullptr;freeSlots_ = nullptr;}// 销毁一个现有的内存池~MemoryPool() noexcept;// 同一时间只能分配一个对象, n 和 hint 会被忽略pointer allocate(size_t n = 1, const T* hint = 0);// 销毁指针 p 指向的内存区块void deallocate(pointer p, size_t n = 1);// 调用构造函数template <typename U, typename... Args>void construct(U* p, Args&&... args);// 销毁内存池中的对象, 即调用对象的析构函数template <typename U>void destroy(U* p) {p->~U();}private:// 用于存储内存池中的对象槽, // 要么被实例化为一个存放对象的槽, // 要么被实例化为一个指向存放对象槽的槽指针union Slot_ {T element;Slot_* next;};// 数据指针typedef char* data_pointer_;// 对象槽typedef Slot_ slot_type_;// 对象槽指针typedef Slot_* slot_pointer_;// 指向当前内存区块slot_pointer_ currentBlock_;// 指向当前内存区块的一个对象槽slot_pointer_ currentSlot_;// 指向当前内存区块的最后一个对象槽slot_pointer_ lastSlot_;// 指向当前内存区块中的空闲对象槽slot_pointer_ freeSlots_;// 检查定义的内存池大小是否过小static_assert(BlockSize >= 2 * sizeof(slot_type_), "BlockSize too small.");
};#endif // MEMORY_POOL_HPP

在上面的类设计中可以看到,在这个内存池中,其实是使用链表来管理整个内存池的内存区块的。内存池首先会定义固定大小的基本内存区块(Block),然后在其中定义了一个可以实例化为存放对象内存槽的对象槽(Slot_)和对象槽指针的一个联合。然后在区块中,定义了四个关键性质的指针,它们的作用分别是:

currentBlock_: 指向当前内存区块的指针
currentSlot_: 指向当前内存区块中的对象槽
lastSlot_: 指向当前内存区块中的最后一个对象槽
freeSlots_: 指向当前内存区块中所有空闲的对象槽
梳理好整个内存池的设计结构之后,我们就可以开始实现关键性的逻辑了。

三、实现

MemoryPool::construct() 实现

MemoryPool::construct() 的逻辑是最简单的,我们需要实现的,仅仅只是调用信件对象的构造函数即可,因此:

// 调用构造函数, 使用 std::forward 转发变参模板

template <typename U, typename... Args>
void construct(U* p, Args&&... args) {new (p) U (std::forward<Args>(args)...);
}

MemoryPool::deallocate() 实现

MemoryPool::deallocate() 是在对象槽中的对象被析构后才会被调用的,主要目的是销毁内存槽。其逻辑也不复杂:// 销毁指针 p 指向的内存区块
void deallocate(pointer p, size_t n = 1) {if (p != nullptr) {// reinterpret_cast 是强制类型转换符// 要访问 next 必须强制将 p 转成 slot_pointer_reinterpret_cast<slot_pointer_>(p)->next = freeSlots_;freeSlots_ = reinterpret_cast<slot_pointer_>(p);}
}

MemoryPool::~MemoryPool() 实现

析构函数负责销毁整个内存池,因此我们需要逐个删除掉最初向操作系统申请的内存块:

// 销毁一个现有的内存池
~MemoryPool() noexcept {// 循环销毁内存池中分配的内存区块slot_pointer_ curr = currentBlock_;while (curr != nullptr) {slot_pointer_ prev = curr->next;operator delete(reinterpret_cast<void*>(curr));curr = prev;}
}

MemoryPool::allocate() 实现

MemoryPool::allocate() 毫无疑问是整个内存池的关键所在,但实际上理清了整个内存池的设计之后,其实现并不复杂。具体实现如下:

// 同一时间只能分配一个对象, n 和 hint 会被忽略
pointer allocate(size_t n = 1, const T* hint = 0) {// 如果有空闲的对象槽,那么直接将空闲区域交付出去if (freeSlots_ != nullptr) {pointer result = reinterpret_cast<pointer>(freeSlots_);freeSlots_ = freeSlots_->next;return result;} else {// 如果对象槽不够用了,则分配一个新的内存区块if (currentSlot_ >= lastSlot_) {// 分配一个新的内存区块,并指向前一个内存区块data_pointer_ newBlock = reinterpret_cast<data_pointer_>(operator new(BlockSize));reinterpret_cast<slot_pointer_>(newBlock)->next = currentBlock_;currentBlock_ = reinterpret_cast<slot_pointer_>(newBlock);// 填补整个区块来满足元素内存区域的对齐要求data_pointer_ body = newBlock + sizeof(slot_pointer_);uintptr_t result = reinterpret_cast<uintptr_t>(body);size_t bodyPadding = (alignof(slot_type_) - result) % alignof(slot_type_);currentSlot_ = reinterpret_cast<slot_pointer_>(body + bodyPadding);lastSlot_ = reinterpret_cast<slot_pointer_>(newBlock + BlockSize - sizeof(slot_type_) + 1);}return reinterpret_cast<pointer>(currentSlot_++);}
}

四、与 std::vector 的性能对比

我们知道,对于栈来说,链栈其实并不是最好的实现方式,因为这种结构的栈不可避免的会涉及到指针相关的操作,同时,还会消耗一定量的空间来存放节点之间的指针。事实上,我们可以使用 std::vector 中的 push_back() 和 pop_back() 这两个操作来模拟一个栈,我们不妨来对比一下这个 std::vector 与我们所实现的内存池在性能上谁高谁低,我们在 主函数中加入如下代码:

// 比较内存池和 std::vector 之间的性能std::vector<int> stackVector;start = clock();for (int j = 0; j < REPS; j++) {assert(stackVector.empty());for (int i = 0; i < ELEMS; i++)stackVector.push_back(i);for (int i = 0; i < ELEMS; i++)stackVector.pop_back();}std::cout << "Vector Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";

这时候,我们重新编译代码,就能够看出这里面的差距了:
首先是使用默认分配器的链表栈速度最慢,其次是使用 std::vector 模拟的栈结构,在链表栈的基础上大幅度削减了时间。
std::vector 的实现方式其实和内存池较为类似,在 std::vector 空间不够用时,会抛弃现在的内存区域重新申请一块更大的区域,并将现在内存区域中的数据整体拷贝一份到新区域中。
最后,对于我们实现的内存池,消耗的时间最少,即内存分配性能最佳,完成了本项目。

总结二

本节中,我们实现了我们上节未实现的内存池,完成了整个项目的目标。 这个内存池不仅精简而且高效,整个内存池的完整代码如下:

#ifndef MEMORY_POOL_HPP
#define MEMORY_POOL_HPP#include <climits>
#include <cstddef>template <typename T, size_t BlockSize = 4096>
class MemoryPool
{public:// 使用 typedef 简化类型书写typedef T*              pointer;// 定义 rebind<U>::other 接口template <typename U> struct rebind {typedef MemoryPool<U> other;};// 默认构造// C++11 使用了 noexcept 来显式的声明此函数不会抛出异常MemoryPool() noexcept {currentBlock_ = nullptr;currentSlot_ = nullptr;lastSlot_ = nullptr;freeSlots_ = nullptr;}// 销毁一个现有的内存池~MemoryPool() noexcept {// 循环销毁内存池中分配的内存区块slot_pointer_ curr = currentBlock_;while (curr != nullptr) {slot_pointer_ prev = curr->next;operator delete(reinterpret_cast<void*>(curr));curr = prev;}}// 同一时间只能分配一个对象, n 和 hint 会被忽略pointer allocate(size_t n = 1, const T* hint = 0) {if (freeSlots_ != nullptr) {pointer result = reinterpret_cast<pointer>(freeSlots_);freeSlots_ = freeSlots_->next;return result;}else {if (currentSlot_ >= lastSlot_) {// 分配一个内存区块data_pointer_ newBlock = reinterpret_cast<data_pointer_>(operator new(BlockSize));reinterpret_cast<slot_pointer_>(newBlock)->next = currentBlock_;currentBlock_ = reinterpret_cast<slot_pointer_>(newBlock);data_pointer_ body = newBlock + sizeof(slot_pointer_);uintptr_t result = reinterpret_cast<uintptr_t>(body);size_t bodyPadding = (alignof(slot_type_) - result) % alignof(slot_type_);currentSlot_ = reinterpret_cast<slot_pointer_>(body + bodyPadding);lastSlot_ = reinterpret_cast<slot_pointer_>(newBlock + BlockSize - sizeof(slot_type_) + 1);}return reinterpret_cast<pointer>(currentSlot_++);}}// 销毁指针 p 指向的内存区块void deallocate(pointer p, size_t n = 1) {if (p != nullptr) {reinterpret_cast<slot_pointer_>(p)->next = freeSlots_;freeSlots_ = reinterpret_cast<slot_pointer_>(p);}}// 调用构造函数, 使用 std::forward 转发变参模板template <typename U, typename... Args>void construct(U* p, Args&&... args) {new (p) U (std::forward<Args>(args)...);}// 销毁内存池中的对象, 即调用对象的析构函数template <typename U>void destroy(U* p) {p->~U();}private:// 用于存储内存池中的对象槽union Slot_ {T element;Slot_* next;};// 数据指针typedef char* data_pointer_;// 对象槽typedef Slot_ slot_type_;// 对象槽指针typedef Slot_* slot_pointer_;// 指向当前内存区块slot_pointer_ currentBlock_;// 指向当前内存区块的一个对象槽slot_pointer_ currentSlot_;// 指向当前内存区块的最后一个对象槽slot_pointer_ lastSlot_;// 指向当前内存区块中的空闲对象槽slot_pointer_ freeSlots_;// 检查定义的内存池大小是否过小static_assert(BlockSize >= 2 * sizeof(slot_type_), "BlockSize too small.");
};
#endif

补充,上述项目的优化(企业内更好性能的内存池)

1、封装一个类用于管理内存池的使用如下,很容易看得懂,其实就是向内存池申请size个空间并进行构造,返回是首个元素的地址。释放也是一样,不过释放多个的时候需要确保这多个元素的内存是连续的。

#pragma once
#include <set>template<typename T, typename Alloc = std::allocator<T>>
class AllocateManager
{
private:typedef typename Alloc::template rebind<T>::other other_;other_ m_allocate;//创建一个内存池管理器public://MemoryPool申请空间T * allocate(size_t size = 1){//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);T * node = m_allocate.allocate(size);m_allocate.construct(node, size);return node;}//Allocator申请空间T * allocateJJ(size_t size = 1){//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);T * node = m_allocate.allocate(size);m_allocate.construct(node);return node;}//释放并回收空间void destroy(T * node, size_t size = 1){//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);for (int i = 0; i < size; i++){m_allocate.destroy(node);m_allocate.deallocate(node,1);node++;}}//获得当前内存池的大小const size_t getMenorySize(){return m_allocate.getMenorySize();}//获得当前内存池的块数const size_t getBlockSize(){return m_allocate.getBlockSize();}
};
rebind的设计跟C++stl里的设计是同样套路,stl设计代码如下:template<class _Elem,class _Traits,class _Ax>class basic_string: public _String_val<_Elem, _Ax>{...typedef _String_val<_Elem, _Ax> _Mybase;typedef typename _Mybase::_Alty _Alloc;...template<class _Ty,class _Alloc>class _String_val: public _String_base{...typedef typename _Alloc::templaterebind<_Ty>::other _Alty;...template<class _Ty>class allocator: public _Allocator_base<_Ty>{...template<class _Other>struct rebind{	// convert an allocator<_Ty> to an allocator <_Other>typedef allocator<_Other> other;};

2、内存池设计代码,下面会一个一个方法抛开说明

#pragma once
#include <mutex>template<typename T, int BlockSize = 6, int Block = sizeof(T) * BlockSize>
class MemoryPool
{
public:template<typename F>struct rebind{typedef MemoryPool<F, BlockSize> other;};MemoryPool(){m_FreeHeadSlot = nullptr;m_headSlot = nullptr;m_currentSlot = nullptr;m_LaterSlot = nullptr;m_MenorySize = 0;m_BlockSize = 0;}~MemoryPool(){//将每一块内存deletewhile (m_headSlot){Slot_pointer pre = m_headSlot;m_headSlot = m_headSlot->next;operator delete(reinterpret_cast<void*>(pre));}}//申请空间T * allocateOne(){//空闲的位置有空间用空闲的位置if (m_FreeHeadSlot){Slot_pointer pre = m_FreeHeadSlot;m_FreeHeadSlot = m_FreeHeadSlot->next;return reinterpret_cast<T*>(pre);}//申请一块内存if (m_currentSlot >= m_LaterSlot){Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(Block + sizeof(Slot_pointer)));m_MenorySize += (Block + sizeof(Slot_pointer));m_BlockSize++;reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;//将新内存放在表头m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_)+1);//指向最后一个内存的开头位置}return reinterpret_cast<T*>(m_currentSlot++);}/*动态分配空间,注意:分配超过2个空间会在块里面创建占用4字节的空间存放数组的指针,这个空间不会被回收,所以动态分配最好分配大空间才使用动态*/T * allocate(size_t size = 1){std::unique_lock<std::mutex> lock{ this->m_lock };//申请一个空间if (size == 1)return allocateOne();Slot_pointer pReSult = nullptr;/*先计算最后申请的块空间够不够,不适用回收的空间,因为回收空间不是连续*/int canUseSize = reinterpret_cast<int>(m_LaterSlot) + sizeof(Slot_) - 1 - reinterpret_cast<int>(m_currentSlot);int applySize = sizeof(T) * size + sizeof(T*);//创建数组对象时多了个指针,所以内存要加个指针的大小if (applySize <= canUseSize) //空间足够,把剩余空间分配出去{pReSult = m_currentSlot;m_currentSlot = reinterpret_cast<Slot_pointer>(reinterpret_cast<Char_pointer>(m_currentSlot) + applySize);return reinterpret_cast<T*>(pReSult);}/*空间不够动态分配块大小,不把上一块剩余的空间使用是因为空间是需要连续,所以上一块会继续往前推供下次使用*/Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(applySize + sizeof(Slot_pointer)));m_MenorySize += (applySize + sizeof(Slot_pointer));m_BlockSize++;if (!m_headSlot)//目前没有一块内存情况{reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_) + 1);pReSult = m_currentSlot;m_currentSlot = m_LaterSlot;//第一块内存且是动态分配,所以这一块内存是满的}else{//这个申请一块动态内存就用完,直接往头后面移动Slot_pointer currentSlot = nullptr;Slot_pointer next = m_headSlot->next;currentSlot = reinterpret_cast<Slot_pointer>(blockSize);currentSlot->next = next;m_headSlot->next = currentSlot;pReSult = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存}return reinterpret_cast<T*>(pReSult);}//使用空间void construct(T * p, size_t size = 1){//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);if (size == 1)new (p)T();elsenew (p)T[size]();}//析构一个对象void destroy(T * p){p->~T();}//回收一个空间void deallocate(T * p, size_t count = 1){std::unique_lock<std::mutex> lock{ this->m_lock };reinterpret_cast<Slot_pointer>(p)->next = m_FreeHeadSlot;m_FreeHeadSlot = reinterpret_cast<Slot_pointer>(p);}const size_t getMenorySize(){return m_MenorySize;}const size_t getBlockSize(){return m_BlockSize;}
private:union Slot_{T _data;Slot_ * next;};typedef Slot_* Slot_pointer;typedef char*  Char_pointer;Slot_pointer m_FreeHeadSlot;//空闲的空间头部位置Slot_pointer m_headSlot;//指向的头位置Slot_pointer m_currentSlot;//当前所指向的位置Slot_pointer m_LaterSlot;//指向最后一个元素的开始位置size_t m_MenorySize;size_t m_BlockSize;// 同步std::mutex m_lock;static_assert(BlockSize > 0, "BlockSize can not zero");
};

3、申请一个空间,当回收的内存没有或内存块空间不够时,新开辟一块内存,并将新内存放在表头,返回新内存的头地址,如果内存块还有空间,那么返回首个空余的空间

//申请空间T * allocateOne(){//空闲的位置有空间用空闲的位置if (m_FreeHeadSlot){Slot_pointer pre = m_FreeHeadSlot;m_FreeHeadSlot = m_FreeHeadSlot->next;return reinterpret_cast<T*>(pre);}//申请一块内存if (m_currentSlot >= m_LaterSlot){Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(Block + sizeof(Slot_pointer)));m_MenorySize += (Block + sizeof(Slot_pointer));m_BlockSize++;reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;//将新内存放在表头m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_)+1);//指向最后一个内存的开头位置}return reinterpret_cast<T*>(m_currentSlot++);}

4、当分配超过2个元素空间时,先判断空闲块的空间够不够分配,够分配,不够新开辟一个大小跟申请元素个数一样的内存块,并将该块内存向表头置后,返回该快首地址。注意,由于分配多个元素的空间也就是分配一个数组,这个时候在下一步调用构造函数时会构造数组对象,数组对象会多一个指针空间指向该数组,所以申请n+1个元素时加上一个指针的空间,否则会泄漏。

这个指针的空间是没有用的,释放和回收空间是不会回收这个指针,这样它就会占用了内存块一个指针空间,就相当于磁盘分区有未分配的内存一样,分配多个元素空间时这个是无法避免的。

/*动态分配空间,注意:分配超过2个空间会在块里面创建占用4字节的空间存放数组的指针,这个空间不会被回收,所以动态分配最好分配大空间才使用动态*/T * allocate(size_t size = 1){std::unique_lock<std::mutex> lock{ this->m_lock };//申请一个空间if (size == 1)return allocateOne();Slot_pointer pReSult = nullptr;/*先计算最后申请的块空间够不够,不适用回收的空间,因为回收空间不是连续*/int canUseSize = reinterpret_cast<int>(m_LaterSlot) + sizeof(Slot_) - 1 - reinterpret_cast<int>(m_currentSlot);int applySize = sizeof(T) * size + sizeof(T*);//创建数组对象时多了个指针,所以内存要加个指针的大小if (applySize <= canUseSize) //空间足够,把剩余空间分配出去{pReSult = m_currentSlot;m_currentSlot = reinterpret_cast<Slot_pointer>(reinterpret_cast<Char_pointer>(m_currentSlot) + applySize);return reinterpret_cast<T*>(pReSult);}/*空间不够动态分配块大小,不把上一块剩余的空间使用是因为空间是需要连续,所以上一块会继续往前推供下次使用*/Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(applySize + sizeof(Slot_pointer)));m_MenorySize += (applySize + sizeof(Slot_pointer));m_BlockSize++;if (!m_headSlot)//目前没有一块内存情况{reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_) + 1);pReSult = m_currentSlot;m_currentSlot = m_LaterSlot;//第一块内存且是动态分配,所以这一块内存是满的}else{//这个申请一块动态内存就用完,直接往头后面移动Slot_pointer currentSlot = nullptr;Slot_pointer next = m_headSlot->next;currentSlot = reinterpret_cast<Slot_pointer>(blockSize);currentSlot->next = next;m_headSlot->next = currentSlot;pReSult = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存}return reinterpret_cast<T*>(pReSult);}

其他小的方法就不介绍了,代码也有注释很容易看得懂。

5、性能测试

动态分配时,num1表示分配块数,num2表示分配的每块大小。
在这里插入图片描述
逐步申请和释放一千万个空间(元素为单位),速度如下,C++是最慢的,MemoryPool快乐接近20倍,MemoryPool动态分配会更快乐些(面对疾风吧)。

测试代码:

#include <iostream>
#include <string>
#include <set>
#include <ctime>
#include <thread>
#include"MemoryPool.h"
#include"AllocateManager.h"
using namespace std;
//动态分配时,num1表示块数,num2表示每块大小
#define num1 1000
#define num2 10000  
class Test
{
public:int a;~Test(){//cout << a << " ";}
};void TestByCjj()
{clock_t start;start = clock();Test * p[num1][num2];Test * t;AllocateManager<Test, allocator<Test>> pool;start = clock();int count = 0;//向内存池申请空间并构造出对象for (int i = 0; i < num1; i++){for (int j = 0; j < num2; j++){t = pool.allocateJJ(1);t->a = count++;p[i][j] = t;}}//根据对象从内存池释放并回收该空间for (int i = 0; i < num1; i++){for (int j = 0; j < num2; j++){t = p[i][j];pool.destroy(t);}}std::cout << "C++ Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << endl;
}void TestByOne()
{clock_t start;start = clock();Test * p[num1][num2];Test * t;AllocateManager<Test, MemoryPool<Test, 1024>> memoryPool;start = clock();int count = 0;for (int i = 0; i < num1; i++){for (int j = 0; j < num2; j++){t = memoryPool.allocate(1);t->a = count++;p[i][j] = t;}}for (int i = 0; i < num1; i++){for (int j = 0; j < num2; j++){t = p[i][j];memoryPool.destroy(t);}}std::cout << "MemoryPool One Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC);std::cout << "  内存块数量:" << memoryPool.getBlockSize();std::cout << "  内存消耗(byte):" << memoryPool.getMenorySize() << std::endl;
}
void TestByBlock()
{clock_t start;start = clock();Test * p[num1][num2];Test * t;AllocateManager<Test, MemoryPool<Test, 1024>> memoryPool;start = clock();int count = 0;for (int i = 0; i < num1; i++){t = memoryPool.allocate(num2);for (int j = 0; j < num2; j++){t->a = count++;p[i][j] = t++;}}for (int i = 0; i < num1; i++){for (int j = 0; j < num2; j++){Test * t = p[i][j];memoryPool.destroy(t);}}std::cout << "MemoryPool Block Time: ";std::cout << (((double)clock() - start) / CLOCKS_PER_SEC);std::cout << "  内存块数量:" << memoryPool.getBlockSize();std::cout << "  内存消耗(byte):" << memoryPool.getMenorySize() << std::endl;
}int main()
{TestByCjj();TestByOne();TestByBlock();return 0;
}

最后的补充

哇,对不起,我高估了我的能力了,居然写了整整一周了,大家喜欢的给个关注,后续就不在乞讨大家啦,如果对后两个项目我的实现有疑问或者说有错误的大佬请联系我一下,我目前也是正在学服务器开发,所以不保证完全正确,联系方式:1311392184 ,一定要指导我一下如果大佬发现错误了,我以后很想在服务器开发上走一段时间。

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

相关文章

  1. Reactor模式

    疯狂创客圈,一个Java 高并发研习社群 【博客园 总入口 】疯狂创客圈,倾力推出:面试必备 + 面试必备 + 面试必备 的基础原理+实战 书籍 《Netty Zookeeper Redis 高并发实战》写在前面 ​ 大家好,我是 高并发的实战社群【疯狂创客圈】尼恩。Reactor模式非常重要,无论开发、…...

    2024/3/29 12:29:35
  2. OpenMP 入门

    前两天(其实是几个月以前了)看到了代码中有 #pragma omp parallel for 一段,感觉好像是 OpenMP,以前看到并行化的东西都是直接躲开,既然躲不开了,不妨研究一下:OpenMP 是 Open MultiProcessing 的缩写。OpenMP 并不是一个简单的函数库,而是一个诸多编译器支持的框架,或者…...

    2024/4/29 11:05:00
  3. 《深入理解Java虚拟机》读书笔记七

    Java内存模型与线程硬件效率与一致性MSI、MESI、MOSI、Synapse、Firefly、Dragon处理器的乱序执行与Java的指令重排Java内存模型java定义了自己的内存模型JMM,来屏蔽各种硬件与操作系统的内存访问差异主内存与工作内存:Java规定所有的变量都存储在主内存内每条线程都有自己的…...

    2024/3/29 12:29:33
  4. 树莓派远程监控 - web异地监控

    最近接了个项目,项目中有一个远程监控的功能,最好用网页就能查看摄像头画面。通过几天的查找资料,找到了一个相对简单的解决方案:借助一个名为“MJPG-Streamer”的web开源项目,可以在局域网中用web查看摄像头画面,之后再通过内网穿透的方式,将一个购买的域名映射到这个局…...

    2024/4/6 4:15:44
  5. 疯狂创客圈 JAVA 高并发 总目录

    无编程不创客,疯狂创客圈,一大波编程高手正在交流、学习中!疯狂创客圈: JAVA 高并发 研习社群, QQ群:104131248(已满) 236263776 (请加此群)疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备 机械工业出版社 《…...

    2024/3/29 7:48:19
  6. 学会了这些技术,你离BAT大厂不远了

    每一个程序员都有一个梦想,梦想着能够进入阿里、腾讯、字节跳动、百度等一线互联网公司,由于身边的环境等原因,不知道 BAT 等一线互联网公司使用哪些技术?或者该如何去学习这些技术?或者我该去哪些获取这些技术资料?没关系,平头哥一站式服务,上面统统不是问题。平头哥整…...

    2024/3/29 7:48:18
  7. 2019 全球 AI 安防市场十大事件丨年终盘点

    AI对于安防的变革在这两年的安博会上有最为明显的体现。2018年的北京安博会,几乎所有的摄像机都开始向“AI摄像机”演进。DEMO演示上,也均在显示人脸识别、车辆识别、视频结构化及大数据研判方面的效果。单画面抓取100张人脸、人脸抓取率99.5%、百亿人/秒人脸识别比对、800路…...

    2024/3/29 7:48:17
  8. 一篇文章讲清mysql

    一篇文章讲清mysql...

    2024/4/24 23:01:04
  9. 支持向量机SVM

    1...

    2024/4/8 2:09:06
  10. springMVC(七):Spring、SpringMVC和Mybatis整合

    1、创建maven项目并添加坐标2、...

    2024/4/28 8:21:48
  11. ubuntu 在终端中打开当前路径

    命令是$ nautilus ....

    2024/4/29 1:14:33
  12. 2020年1月13日,学习Python在气象行业里的应用,从今天开始

    博主当前是一名在读研究生,学校不算好,但也很珍惜通过研究生考试得来的学习机会。比不上名校学生,但也不想屈服。 由于本科毕设与图像处理相关,所以当时对机器学习在图像处理领域的应用关注较多,觉得新奇好玩,但还没实际动手实践。随后接触Python是因为一次偶然的比赛经历…...

    2024/4/28 3:17:42
  13. mysqlclient所需的一些依赖

    sudo apt-get install libssl-dev sudo apt-get install python3-dev...

    2024/5/2 5:01:28
  14. 高德地图 使用Glide加载网络图片作为 marker

    今天有人问,一个marker同时加载本地图片和网络图片。应该怎么处理。最后是这样: public void setMarker(final double wd, final double jd, final int mid, String url) {view = getLayoutInflater().inflate(R.layout.zdyview, null);final ImageView ivHead = view.findVi…...

    2024/4/29 2:42:36
  15. 有关NLP的笔记

    RNN考虑t与t-1时刻的状态相当于有记忆能力由于梯度消失,很难学到长距离的关系 LSTM有一个直通的通道保存例子,可以控制遗忘门来遗忘通过门的的机制来避免梯度消失 GRU遗忘门和输入门合成更新门 seq2seq 两个RNN Encoding和Decoder要用固定的contex向量来编码整个语义 Attenti…...

    2024/4/29 0:47:29
  16. 《MySql必知必会》章节目录

    01单表查询总结>>...

    2024/4/28 8:15:30
  17. 文本分类 20个epoch后dev acc还在上升

    很可能是因为dev集和train集的数据太像了,train集过拟合导致dev集也过拟合, 基本5-10个epoch就差不多了。...

    2024/4/28 19:26:46
  18. python算法之归并排序

    将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。 def merge_sort(li):if len(li) <= 1:return li# 二分分解num = …...

    2024/4/28 5:02:53
  19. vim 下设置行号,并删除单行或多行

    前言 linux 经常需要对文本进行编辑,有时候要看文本是第几行,有时候需要删除连续的多行文本 1.显示行号 vim test.py显示:esc 模式下输入 set number 或者 set nu2.删除单行 两种方式:vim 进入查看文件,光标移动到待删除行,连续按两次 d 即可删除整行数据 在显示行号的基…...

    2024/4/28 0:24:40
  20. Linux中的find(-atime、-ctime、-mtime)指令分析

    本篇主要对find -atime(-ctime、、mtime)指令的用法、参数、运行情况进行分析用法: find . {-atime/-ctime/-mtime/-amin/-cmin/-mmin} [-/+]num参数分析:1.第一个参数“.”,代表当前目录,如果是其他目录,可以输入绝对目录和相对目录位置;2.第二个参数分两部分,前面字…...

    2024/4/29 0:26:39

最新文章

  1. 如何配置Jupyter Lab以允许远程访问和设置密码保护

    如何配置Jupyter Lab以允许远程访问和设置密码保护 当陪你的人要下车时&#xff0c;即使不舍&#xff0c;也该心存感激&#xff0c;然后挥手道别。——宫崎骏《千与千寻》 在数据科学和机器学习工作流中&#xff0c;Jupyter Lab是一个不可或缺的工具&#xff0c;但是默认情况下…...

    2024/5/3 23:45:53
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. Linux学习-网络UDP

    网络 数据传输,数据共享 网络协议模型 OSI协议模型 应用层 实际发送的数据 表示层 发送的数据是否加密 会话层 是否建立会话连接 传输层 数据传输的方式&#xff08;数据报、流式&#…...

    2024/5/2 18:10:46
  4. 从头开发一个RISC-V的操作系统(二)RISC-V 指令集架构介绍

    文章目录 前提ISA的基本介绍ISA是什么CISC vs RISCISA的宽度 RISC-V指令集RISC-V ISA的命名规范模块化的ISA通用寄存器Hart特权级别内存管理与保护异常和中断 目标&#xff1a;通过这一个系列课程的学习&#xff0c;开发出一个简易的在RISC-V指令集架构上运行的操作系统。 前提…...

    2024/5/1 12:59:53
  5. Vue ts 如何给 props 中的变量指定特定类型,比如 Interface 类的

    Vue ts 如何给 props 中的变量指定特定类型&#xff0c;比如 Interface 类的 我有一个这样的变量值类型 一、在没用 ts 之前的 props 类型指定方式 我们都知道之前在没用 ts 之前的 props 变量值类型指定方式&#xff1a; 如下图&#xff0c;billFood 定义方式是这样的&…...

    2024/5/2 17:08:55
  6. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  7. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  8. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  9. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/3 23:10:03
  10. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  11. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  12. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  13. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  14. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  15. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  16. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  17. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  18. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  19. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  20. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  21. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  22. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  23. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  24. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  25. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57