一、C语言基础
1、struct 的内存对齐和填充问题
其实只要记住一个概念和三个原则就可以了:
- 一个概念:
自然对齐:如果一个变量的内存起始地址正好位于它长度的整数倍,就被称做自然对齐。
如果不自然对齐,会带来CPU存取数据时的性能损失。(PS:具体应该与CPU通过总线读写内存数据的细节相关,具体没有细究) - 三个原则:
1)struct 的起始地址需要能够被其成员中最宽的基本数据类型整除;
2)struct 的size 也必须能够被其成员中最宽的基本数据类型整除;
3)struct 中每个成员地址相对于struct 的起始地址的offset,必须是自然对齐的。
填充的字节是随意填充任意字符的,除非赋值前先对结构体变量s进行memset(s, 0, sizeof(s));
进行整个内存清零处理(全0填充,也可以用其他字符全填充)。
2、struct变量能进行比较吗?
- C语言不能直接用
==、>、>=、<、<=
这些进行比较。但是C++若是对这些用于比较的关系运算符进行了重载(重载内部可以调用memcmp函数),那就可以进行比较了。 - 在对结构体变量都用memset初始化的前提下,可以用memcmp函数进行一个一个字节的比较。
int memcmp(const void *buf1, const void *buf2, unsigned int count);
相同返回0,大于返回正数,小于返回负数。(类似于strcmp函数的返回值) - 若是没有用memset统一初始化填充字节,那么任意的填充字节就会影响比较结果。
- 结构体变量之间可以直接赋值,如
struct student stu1 = stu2;
但是注意,赋值是调用memcpy一个一个字节复制过去的,而不是按照stu1.id=stu2.id;
这样一个一个成员复制过去的。因此对于用=直接赋值的两个变量,再用memcmp比较,肯定是相等的。
3、运算符优先级问题
- 优先级排序:()
>
!非(其他单目的)>
算术运算符>
关系(> < == !=)运算符>
逻辑(& ^ | && ||)运算符>
赋值运算符>
逗号
4、手写strcpy字符串拷贝函数?先问要C风格的还是C++风格的?
#include <assert.h>
#include <stdio.h>
char* strcpy(char* des, const char* src) //注意const。C++风格的(char& des, const char &src)
{
assert((des!=NULL) && (src!=NULL)); //注意输入合法性检查。
//但是对于C++可以有更好的解决方案,将输入参数指针改为引用&,就默认不能传入NULL,否则报错
char *address = des; //注意返回值是目的串首地址
while((*des++ = *src++) != '\0')
;
return address;
}
https://www.nowcoder.com/ta/review-c/review?tpId=22&tqId=21053&query=&asc=true&order=&page=4
另有strlen、strcmp、strcat函数的手写:https://blog.csdn.net/lisonglisonglisong/article/details/44278013
另外strncpy函数,复制完后des目标字符串的末尾不会有'\0'结束符,一种解决办法是自己加上一行des[n]='\0';
。更好的做法是给des字符数组初始化为全空字符串char des[100]="";
或者memset(des, 0, 100);
5、手写一个memcpy函数?
- 陷阱是要注意有重叠区的拷贝,比如
str[10]="123456789"
,要memcpy(str+2, str, 5);
,若是不考虑重叠区而简单的从前往后拷贝,会得到返回"1212189"
,然而我们想要的结果是"1234589"
void* Memcpy(void* dest, const void* src, size_t size)
{
char *pdest, *psrc;
if (NULL == dest || NULL == src)
{
return NULL;
}
//要考虑如果目标地址和源地址后面有重叠,则要防止源地址后面的原始数据被目标地址拷贝过来的数据覆盖
//这种情况就需要从后往前拷贝
if (dest>src && (char *)dest<(char *)src+size)
{
pdest = (char *)dest + size - 1;
psrc = (char *)src + size - 1;
while (size--)
{
*pdest-- = *psrc--;
}
}
//从前往后正常拷贝赋值
else
{
pdest = (char*)dest;
psrc = (char*)src;
while (size--)
{
*pdest++ = *psrc++;
}
}
return dest;
}
6、main函数的返回值作用?如何捕获返回值?
-
int main(int argc, char *argv[])
{ return 0; }
标准main函数写法,在命令行运行可执行文件时,argc表示参数个数,argv表示指针数组,数组里面每个元素都是一个字符串指针首地址。 - 返回0表示程序正常退出,返回其他数字的含义由操作系统决定。
- 在windows中再命令行执行上面最简单的代码编译承德可执行文件
test.exe
后,再执行命令echo %ERRORLEVEL%
(windows命令行大小写不敏感)可以输出main函数的返回值:0,修改为return -1;
实验后也可以看到返回值为:-1。 - 在linux中可在执行可执行文件
./test
后,再输入一行命令echo $?
打印出main函数的返回值。 - 在win的
test.exe && dir
或linux的./test && ls
,都可以根据可执行文件的main返回值来判断是否后面的系统命令是否继续执行,main返回值为0时才表示真,则后面的列出当前目录命令都可以执行。main返回其他值则后面的不继续执行。
7、进程间通信的方式有哪些?
- 1、无名管道(父子进程兄弟进程使用
int pipe(int fd[2]);
fd[0]单收fd[1]单发);
2、有名管道FIFO(任意两进程间,是一种文件类型mkfifo("fifo1", 0666);
,文件标识符fd = open("fifo1", O_WRONLY)
);管道的实质是一个内核缓冲区,管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据。
3、共享内存(key = ftok(".", 'z')
,创建并获取共享内存号shmid = shmget(key, 1024, IPC_CREAT|0666)
);采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队列等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入进程到共享内存区,另一次从共享内存到输出进程。
4、消息队列(需要内核维护这个消息队列key = ftok("/etc/passwd",'z')
,队列号msqid = msgget(key, IPC_CREAT|0777)
);消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程通过获取消息队列的唯一ID可以向消息队列添加消息,也可以向消息队列读取消息。
5、信号(如一些引发中断信号kill);几十种信号和以他们的名称命名的常量,通常程序中直接包含<signal.h>就好。信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号常用于用户进程和内核之间直接交互。内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。信号事件有两个来源:1)硬件来源,例如按下了cltr+C,通常产生中断信号sigint。2)软件来源,例如使用系统调用或者命令发出信号。最常用的发送信号的系统函数是kill,raise函数。软件来源还包括一些非法运算等操作。
6、信号量(也是需要由内核维护P获取信号量操作减一,V释放信号量操作加1,只是同步计数器不传数据key = ftok(".", 'z')
,获取信号量idsem_id = semget(key, 1, IPC_CREAT|0666)
);
7、socket(可跨网络主机进程通信)sys/socket.h 或 winsock2.h
https://www.jianshu.com/p/21fba1542026 - socket网络跨主机的进程间通信是以五元组【源IP,源端口,目的IP,目的端口,TCP或UDP协议】,来找到不同的进程,并互发数据进行通信。
- 同一主机上的进程间通信,大都是使用在本主机上唯一的key关键值来标识一个可供不同进程访问的资源,这个唯一的关键值又大都是与某一个实际存在的文件绑定,利用ftok()函数
key_t ftok( const char * fname, int id );
,可根据fname即某文件名(一般是系统上一定存在的文件)找到它的索引节点号(唯一标识一个文件的node_id)搭配提供的另一个参数id得到唯一的key值=(16进制的)id连接node_id
。因此不同的进程只要都遵守这个约定提供相同的参数给ftok()函数就都可以获取到这个唯一的key_id,然后根据key_id再获取到对应的进程间通信方式的id号,然后对其进行读写数据操作。
类似的IPC有FIFO命名管道、消息队列(前面两个自带进程同步效果),信号量(只能控制进程同步,要想互发数据需要结合共享内存)、共享内存 - 无名管道由于只在父子进程或兄弟进程间通信,可以在主进程中通过pipe(fd)随机产生出管道的文件描述符。不需要在主机上唯一,因为只在这几个相关的进程中私有。
8、线程间的通信
- 共享的全局变量,然后利用全局的锁变量或者信号量来实现同步互斥访问共享的全局变量达到通信的目的。
- 消息传递(如窗口应用的点击按钮事件传递)、事件订阅机制等
9、进程调度算法有哪些?Linux使用的什么进程调度方法?
- 【解】进程调度算法有下面几种:
先来先服务
短作业优先
时间片轮转
基于优先级
Linux系统中,进程分为实时和非实时两种:
- 实时进程(相对于普通进程优先级更高)
SCHED_FIFO —> 相同优先级时,先来先服务;不同优先级,抢占式调度。进程一旦占用cpu则一直运行,直到有更高优先级任务到达或自己放弃。
SCHED_RR —> 时间片轮转,抢占式调度。 - 普通进程
SCHED_OTHER —> priority(静态优先级)+counter(剩余时间片)之和作为动态优先级,基于动态优先级的时间片轮转。
10、虚拟内存和物理内存?
- 先说说为什么会有虚拟内存和物理内存的
区别
。正在运行的一个进程,他所需的内存是有可能大于内存条容量之和的,比如你的内存条是256M,你的程序却要创建一个2G的数据区,那么不是所有数据都能一起加载到内存(物理内存)中,势必有一部分数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再通过调度从磁盘进入物理内存。所以,虚拟内存是进程运行时所有内存空间的总和,并且可能有一部分不在物理内存中而在磁盘中,而物理内存就是我们平时所了解的内存条。有的地方呢,也叫这个虚拟内存为内存交换区。
1)每个进程都有自己的虚拟内存空间,所有进程共享物理内存空间。
2)虚拟内存大小和机器的位数有关,如32位的是4GB的地址总线(0~0xFFFFFFFF字节)其中每个进程都有自己的0~0xBFFFFFFF的3GB虚拟内存空间——用户空间
,然后所有进程共用0xCFFFFFFF~0xFFFFFFFF这1GB的虚拟内存空间——内核空间
。64位的有2^64字节的地址总线。
3)该图显示了两个 64 位进程的虚拟地址空间:Notepad.exe 和 MyApp.exe。每个进程都有其各自的虚拟地址空间,范围从 0x000'0000000 至 0x7FF'FFFFFFFF(8TB用户进程虚拟内存空间)。每个阴影框都表示虚拟内存或物理内存的一个页面(大小都为4KB)。注意,Notepad 进程使用从 0x7F7'93950000 开始的虚拟地址的三个相邻页面。但虚拟地址的这三个相邻页面会映射到物理内存中的非相邻页面
。而且还注意,两个进程都使用从 0x7F7'93950000 开始的虚拟内存页面,但这些虚拟页面都映射到物理内存的不同页面,说明个进程间的虚拟内存互不影响。
4)这种虚拟内存一个页page(4KB)和物理内存一个页帧(4KB)之间的映射关系,由操作系统的一个页表来维护,页表中给各个页都有编号(页号和页帧号对应映射)。进程中虚拟内存页》页表中的页号—MMU内存管理单元—页表中的页帧号《物理内存中的页帧
【由MMU的页表来维护映射关系】
5)但是问题来了,虚拟内存页的个数=3GB/4KB > 物理内存页帧的个数=256MB/4KB,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个缺页中断(page fault)缺页异常
功能。操作系统把暂时不访问的物理内存页帧,让他缺页失效,并把它的原内容写入磁盘转存起来——这个过程叫分页-分页是磁盘和内存间传输数据块的最小单位。
,随后把当前需要访问的页放到页帧中,并修改页表中的映射,这样就保证所有的页都有被调度的可能了。当进程又要访问原转存的内容,会先访问原物理内存页但是引发缺页中断然后从磁盘中取出页内容到物理内存页,然后进程访问到数据继续执行。这就是处理虚拟内存地址到物理内存的步骤。
https://www.cnblogs.com/curtful/archive/2012/02/16/2354496.html
11、为什么需要虚拟内存?通过虚拟地址访问内存有哪些优势?
- 虚拟内存地址可以提供只读只写的访问权限来保护实际对应的物理地址的数据。
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将(暂时不用的)物理内存页(通常大小为 4 KB)保存到磁盘文件,从而空出可用的物理内存。数据或代码页会根据需要在物理内存与磁盘之间移动。
12、死锁产生的四个条件,死锁发生后怎么检测和恢复?
- 死锁的四个必要条件:
互斥条件
:一个资源每次只能被一个进程使用。
请求与保持条件:
一个进程在申请新的资源的同时保持对原有资源的占有。
不剥夺条件:
进程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:
若干进程之间形成一种头尾相接的循环等待资源关系。 - 死锁发生后:
检测死锁:首先为每个进程和每个资源指定一个唯一的号码,然后建立资源分配表和进程等待表。
解除死锁:当发现有进程死锁后,可以直接撤消死锁的进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止。
13、不用中间变量实现两个元素交换值。
void swap1(int &a, int &b)
{ //异或操作只适用于整型数,不能用于小数
if (a != b) //如果两个数地址相同,异或操作会清零
{
a ^= b;
b ^= a;
a ^= b;
}
}
void swap2(double &a, double &b)
{//用double型可以使适用的范围更大,防止加法和越界
a = a + b;
b = a - b;
a = a - b;
}
14、一些常见的位操作
- 位操作符的运算优先级比加减更低,如
int a = 1 << 2+ 1;
得到a的值1左移3位得8,与int a = (1 << 2)+ 1;
得5不同。 - 取得a的相反数-a:~a+1; a取反再加一。
- 可以用if ((a & 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数。即只需判断a的最低位是0还是1。
- 取得绝对值
int my_abs(int a)
{
int i = a >> 31; //得到最高位符号位,a为正或0返回0,a为负数返回-1
return i == 0 ? a : (~a + 1); //正数返回本身,负数返回相反数
//return (a^i)-i; //a^0 ==a本身;a^(-1) == a取反,再减-1即加1得到相反数
}
- 给出一个16位的无符号整数。称这个二进制数的前8位为“高位”,后8位为“低位”。现在写一程序将它的高低位交换。
a = (a >> 8) | (a << 8);
15、守护、僵尸、孤儿进程的概念?
-
守护进程:
运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。如何实现守护进程?(孤儿进程)
僵尸进程:
一个进程 fork 子进程,子进程退出,而父进程没有 wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程(进程号pid为1)收养并对它们完成状态收集工作)
16、一个进程的主线程结束后,其他线程怎么办?
- 通常一个进程的主线程是指的main()函数启动后的那个线程。实际上一个进程的所有线程是平等的,没有主线程子线程之分。
- 如果让main()函数执行到最后一行(默认return 0;)或return其他值来结束主线程,然后调用glibc库函数exit,exit进行相关清理工作后调用_exit系统调用退出该进程。因为进程结束了,所以该进程所有的线程也就结束了。
- 如果main()主线程在没有运行到return退出前就由
pthread_cancel(tid);
终止了,那么就不会引发进程结束,其他的线程可以继续正常运行。而所谓的线程之间共享的全局变量是在main()函数之外也就是主线程之外的,因此主线程main函数内的局部变量销毁对其他线程也无影响。 - 如果系统调用#kill 某线程号 ,也是可以结束整个进程的。
17、内存泄漏如何避免?发生后如何检查定位?
- 内存泄漏是指用malloc/realloc/new申请的堆内存,在销毁指向这些堆内存的指针之前没有调用对应的free/delete归还内存给堆。使得这些堆内存不能再被利用。然而不管内存泄漏多么轻微,当程序长时间运行时,其破坏力是惊人的 - 从性能下降到内存耗尽,甚至会影响其他程序的正常运行。
- 内存泄漏的避免:
一是
从编程习惯来谈要求我们记得在同一个代码块成对使用malloc/free或new/delete或new[]数组/delete[]数组,最好不要在函数内申请内存然后返回指针,这样在调用函数后很容易忘记这个指针指向的内存是堆内存而忘记手动释放;而是应该在函数外申请内存,然后将指针作为参数传递给函数使用,这样在函数使用完后我们还可以从上面代码看到这个指针是指向堆内存的而轻松手动释放。二是
在C++中可以使用智能指针,使得在指向堆内存的指针在用完后会随着智能指针的析构而调用free/delete函数释放该指针指向的内存,就不需要手动显示地去释放。三是
使用内存池去集中申请堆内存,然后集中释放堆内存。四是
别忘了将基类的析构函数定义为虚函数,否则在多态使用父类指针指向子类对象时,析构的时候会无法调用子类的析构函数释放子类的资源,从而造成泄漏。 - 内存泄漏发生后的定位:
一是
先找到发生内存泄漏的程序在linux下可以用top命令列出正在运行的程序占用的内存,windows下可以查看任务管理器,从中分析出异常的程序,然后通过#ps aux| grep 程序名》得到进程号pid》然后#kill pid结束这个进程;二是
寻找程序中引发内存泄漏的代码,若是代码量不大,可以通过人工分析是否malloc/new对应的指针都得到了正确的释放,然后重点检查程序会循环执行的那些代码就是引起内存泄漏一直增长的地方。如果还是找不到,那就需要借助一些工具来定位,比如使用vs编程可以安装一个VLD(Visual Leak Detector)插件,使用的时候#include <vld.h>
就可以了调试程序看到代码检查的报错结果。在linux下可以使用 Valgrind Memcheck工具的使用方式如下:valgrind --tool=memcheck ./a.out
其中a.out为要检查的程序编译生成的可执行文件(为了使valgrind发现的错误更精确,能够定位到源代码行,建议在编译时加上-g参数即加入gdb调试信息)。
二、C++基础
1、new表达式(new operator)和std::operator new标准库函数的区别
- new operator 是先调用operator new函数来分配返回值为void*的内存,然后再调用作用类型的构造函数去初始化赋值这块内存。如
string * str = new string("hello");
就是对new表达式的常用方式。
这里我们可以这么理解,new表达式(new operator)其实可以分解为两部,即先调用new操作符(operator new)申请内存,再调用placement new来初始化对象。即上面对new表达式的使用
string * str = new string("hello");
等价于下面两句。
void *buffer = ::operator new(sizeof(string));
buffer = new(buffer) string(“hello”);
- 而operator new函数相当于是malloc,其实它的内部实现也就是调用了
void * malloc(size_t size);
函数。
void* operator new(size_t sizt)
{
return malloc(size);
}
2、new和malloc有哪些区别?
最重要的是要说到最下面一条。
详见:https://www.cnblogs.com/QG-whz/p/5140930.html
3、头文件防卫式编程,防止头文件多次嵌套导入。
比如要编写头文件mystring.h,就可以这样写。如果统一遵循这种规则,那么就不会出现mystring.h头文件内容在编译前预处理时被重复导入的情况。
#ifndef __MYSTRING__
#define __MYSTRING__
#include<string.h> //用到C语言的字符串处理函数
class mystring
{
public:
mystring();
~mystring();
private:
char * head;
int length;
};
#endif
4、const函数?——函数定义后面加const有什么作用?
- 只有类成员函数后面才能加const,普通函数没有所谓的const函数,不能在后面加const。如复数complex类的获取实部成员函数
int getReal() const { return real; }
- 作用是表示此类成员函数不能改变所有类成员变量的值。
- 当定义一个const的类对象时
const complex con1;
,此对象con1就只能调用这些const成员函数,调用非const成员函数就会报错。
5、C++内存分区?进程虚拟内存分布?共有5个分区
6、一个进程可用的内存大小、线程可用内存大小?堆内存多大?栈内存多大?
- 根据上面这张图可以分析出,一个用户进程最多可用3GB内存,进程里既有堆又有栈。而一个线程只有自己的栈空间,所以可用的最大栈内存也就是上图说的8MB或10MB(可手动修改)。因此一个最多可开辟3GB/10MB=300个线程。
- 对于4GB内存封顶的32位系统,用户空间堆内存<3GB(留1GB给内核空间),而栈内存一般只有8M(ubuntu)或10M(centos)各系统默认值不同,可修改。
7、用vs编程时debug和release版本有啥区别?
- Debug通常称为调试版本,它包含很多调试信息,并且不作任何优化,便于程序员调试程序(加断点实时查看变量值)。会检查assert断言语句。也可关闭assert断言——先
#define NDEBUG
后#include<assert.h>
,就可以将后面写的那些assert断言语句关闭检查。 - Release称为发布版本,它往往是进行了各种优化(比如调整某些变量在栈内存的地址顺序),使得程序在代码大小和运行速度上都是最优的,以便用户很好的使用。会忽略assert断言语句。
8、面向对象的四大特性讲一下?
- 抽象:将多种事物抽象出共同特征形成一个抽象父类。
- 封装:将变量和对变量的操作组合在一起形成一个类或者一个模块,就是封装。一处封装,多处调用。目的是代码复用。
- 继承:子类在原有父类代码的基础上进行扩展。目的是代码复用。
- 多态:调用同名函数,却因为上下文环境不同,而会有不同的实现。一个函数接口多种实现。目的是接口复用。
9、C++多态分几类?多态有几种实现方式?
10、C能直接用C++写的动态库吗?C++能直接用C写的动态库吗?
- 都不能直接互用。主要原因是C++有函数重载,相同函数在汇编代码中的会带上参数表示,而C不支持函数重载因此在汇编中函数名只有函数名。比如
int add(int a, int b);
,在C++编译后的汇编代码中是_add_int_int
或其他表示,而在C语言汇编代码中只是_add
。 - 因此,C++要调用用C的动态库,不用重新编译C,只需在自己的C++代码中吧要调用的C函数原型用
extern "C" int add(int a, int b);
,这样该C++代码在编译是就会将这个add函数原型用C的形式编译成_add
。然后就和原C的动态库函数名对上了。否则C++代码中编译成_add_int_int
在调用C动态库时会报错找不到该函数。 - C要调用C++的动态库时,必须修改原C++代码,将要被C调用的函数原型修改为
extern "C" int add(int a, int b);
,就可以被编译成C形式了,然后重新编译就可供C调用。编译完成后,提供给c使用的头文件里面不能包含extern “C”,可以使用宏开关解决,也可以重新写个头文件。当然还有一点就是该函数内部不能包含用C++特有的东西。 - 总结,要想互相能调用,都只需要对C++代码进行修改,函数原型前加
extern "C"
,而C代码不需要修改。
11、从源代码到可执行文件经历了那几步?
- 文本源代码》预处理(得去注释、宏替换、展开头文件等的文本源码》编译(得文本汇编代码.S)》汇编(得二进制的.o目标文件)》链接多个.o(得到二进制可执行文件)
12、C和C++的区别
- C语言更加注重的是过程,C++除了保留C语言的优点,还增加了面向对象的机制。
- 应用场景来看,由于C没有面向对象,所以在程序规模太大的时候写起来很复杂,面向对象的C++更适合大规模的程序。
- C没类class面向对象更多的是面向过程编程,struct的用法也有差异,比如定义struct变量的时候C要带上struct关键字,而C++的struct用法和class一样,只是内部数据访问权限struct默认public,calss默认private。
- C没有bool型,可用int型代替。C没有方便的string可用
- C++的泛型编程和STL提供的多种容器也是C所没有的。
- 函数重载,操作符重载,C也没有。这也导致两者代码不能直接调用。
13、C++和java的区别
- 我觉得C++与Java最大的区别是在于内存管理上,C++的内存管理是需要程序员自己控制的,自己开了需要自己去释放。然而Java提供了JVM,JVM就是用来进行内存管理的,不需要程序员自己手动开关。
- Java呢,摒弃了C++很多复杂的特性,比如指针、多继承、操作符重载等等,相对来说Java的编程学习入门比较容易。
14、static_cast 和 dynamic_cast 的区别?两个操作符
- cast转换发生的时间不同,一个是static静态编译时,一个是动态运行时;
- static_cast是相当于C的强制类型转换,用起来可能有一点危险,不提供运行时的检查来确保转换的安全性。
- dynamic_cast用于转换指针和和引用,不能用来转换对象 —— 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息。
15、说几个C++11的新特性。
1)auto类型推导:
auto vData = vectors.begin();
自动推导出vector中的数据类型
2)范围for循环:for( auto v:vectors) { cout<<v<<endl; }
这样的v是只读的,也可以用到&引用,作为可修改的for(auto &v:vectors){ v=v+1; }
3)lambda表达式:是一个匿名仿函数(函数对象),一定是[]中括号开头。[可选的捕捉到的外部变量] (可选的形参传递)mutable可选 throwSpec可选 -> retType可选 { ... }
,当中括号和大括号之间有东西时,也一定要有小括号。
4)override 和 final 关键字:两个继承控制关键字,override明确地表示一个函数是对基类中一个虚函数的重写,virtual void func(int) override;
确保在派生类中声明的重载函数跟基类的虚函数有相同的签名。final阻止类的进一步派生class TaskManager {/*..*/} final;
和虚函数的进一步重载virtual void func(int) override final;
。
5)空指针常量nullptr。C语言中NULL其实是define成0,所以两者等价,但是因为C++支持函数重载,那么在遇到函数f(int)
和f(void*)
时,f(0)和f(NULL)就会产生歧义不明确指向的是哪个,而有了f(nullptr)后就明确了是指向f(void*)。
6)智能指针(模板类)shared_ptr(附带weak_ptr)、unique_ptr。shared_ptr采用引用计数的方式管理所指向的对象。当有一个新的shared_ptr指向同一个对象时(复制shared_ptr等),引用计数加1。当shared_ptr离开作用域时,引用计数减1。当引用计数为0时,释放所管理的内存。weak_ptr一般和shared_ptr配合使用。它可以指向shared_ptr所指向的对象,但是却不增加对象的引用计数。这样就有可能出现weak_ptr所指向的对象实际上已经被释放了的情况。因此,weak_ptr有一个lock函数,尝试取回一个指向对象的shared_ptr。unique_ptr对于所指向的对象,正如其名字所示,是独占的。所以,不可以对unique_ptr进行拷贝、赋值等操作,但是可以通过release函数在unique_ptr之间转移控制权。上述对于拷贝的限制,有两个特殊情况,即unique_ptr可以作为函数的返回值和参数使用,这时虽然也有隐含的拷贝存在,但是并非不可行的。
1、所有的智能指针类都有一个explicit构造函数,以普通指针作为参数。因此不能自动将普通指针转换为智能指针对象,必须显式调用构造函数。 2、所有智能指针都是对原始指针执行浅拷贝,即所有指针指向同一块内存,而不是深拷贝创建多个备份。 3、当一个shared_ptr对象sp2是由sp1拷贝构造或者赋值构造得来的时候,实际上构造完成后sp1内部的__shared_count对象包含的指向管理对象的指针与sp2内部的__shared_count对象包含的指向管理对象的指针是相等的,也就是说当多个shared_ptr对象来管理同一个对象时,它们共同使用同一个动态分配的管理对象指针。 4、都是运用了RAII技术来实现的。原生指针被转为智能指针类使用之后,就不要再使用原生指针,否则容易出错。即不要把一个原生指针给多个shared_ptr管理。 5、引用头文件#include<memory>。智能指针是模板类而不是指针。 6、智能指针实质就是重载了->和*操作符的类,由类来实现对内存的管理,确保即使有异常产生,也可以通过智能指针类的析构函数完成内存的释放。
7)initializer_list<>初始化列表,用于给变量定义时用{}赋初值,如vector<int> v1{2,3,5};或者是直接用于函数的参数列表如max({5,2,8,1})得到8。这种对应于{}大括号的初始化列表,底层是一个array容器,利用这个array先把初值开辟内存,然后再调用对应的接受initializer_list<>形参的构造函数,然后进行数据的浅拷贝。
8)explicit关键字,多用于构造函数前,明确表示该构造函数必须被显示的调用,而不能被用于隐式转换调用(如多个参数中后面的参数有默认值时,只给出前面参数一般也会触发隐式转换构造),现在加了explicit关键字就是为了禁止这种隐式的转换。
9)decltype关键字,用于从操作的对象或表达式中得到类型,如decltype(lambda)
表示得到一个lambda对象的类型。
用途一:用于指明模板函数的返回类型。
用途二:用于传递lambda表达式对象的类型。
10)move移动语义,对应于右值引用,可以将右值(只能放在赋值等号=右边的变量,std::move(非右值)可以强制转变为右值)的资源(内存空间和值)窃取再直接赋给左值,使得左值不用再定义和开辟额外的内存,加快move拷贝的速度。常用于在构造函数的时候新增一个类似于拷贝构造(
string(const string &str){ ... }
)的move构造(string(string &&str){ ... }
)。本质上是浅拷贝的行为,即拷贝的时候只是将原指针的值(变量内存地址)赋给新的指针,然后原指针赋NULL不再使用,实际的那块内存没有动。相对的深拷贝就是对内存中每一个数据都重新拷贝到了一块新的内存。https://blog.csdn.net/FX677588/article/details/70157088
https://www.cnblogs.com/guxuanqing/p/6707824.html
16、如何在一个不安全的环境中实现安全的数据通信?
- 要实现数据的安全传输,当然就要对数据进行加密了。
- 如果使用
对称加密算法
,加解密使用同一个密钥,除了自己保存外,对方也要知道这个密钥,才能对数据进行解密。如果你把密钥也一起传过去,就存在密码泄漏的可能。所以我们使用非对称加密算法
,过程如下:
首先 数据接收方 生成一对密钥,即私钥和公钥;
然后,数据接收方 将公钥发送给 数据发送方;
数据发送方用收到的公钥对数据加密
,再发送给接收方;
接收方收到数据后,使用自己的私钥解密
。
》由于在非对称算法中,公钥加密的数据必须用对应的私钥才能解密,而私钥又只有接收方自己知道,这样就保证了数据传输的安全性。
17、面向对象思想主要如何体现?
- 面向对象的三大特性是封装、继承、多态。
- 实际中,对一个类的数据成员和方法成员进行封装,对抽象基类的继承可以很好的复用基类封装的数据和方法成员,多态的应用可以使父类指针指向子类的实例对象从而在程序运行时动态地选择最合适的方法执行。
- 类与类之间的各种关系,也是面向对象的重点,合理地利用类与类之间的关系可以设计出很多很好的设计模式。
18、类与类之间的各种关系?
【继承inheritance】A is-a B
类A继承类B,UML类图是实线加三角形由A指向B。A is-a B.
public继承:父类中的成员访问权限不变。
pretected继承:父类的访问权限中public权限降为protected,其他不变。
private继承:父类中public和protected权限的成员全变为private,权限降级,即子类只有类成员变量和友元函数能够访问这些。
【依赖dependency】A use-a B.
类A依赖类B,UML类图是虚线加箭头由A指向B。A use-a B.
一般来说,依赖是指A的某些方法功能要用到B,常表现为B作为A的成员方法的形参或局部变量或返回值,即只和类成员方法有关。
【关联association】A has-a B, B has-a A.
类A和类B双向关联,UML类图是A——B一根实线连接。A与B互相作为类的数据成员变量。
【组合composition】A has-a B 实例对象
类A组合了类B,UML类图是A实心菱形再实线和箭头指向B。类A中定义了类B作为数据成员,B在A中定义构造。A和B的对象生命周期一样。A拥有完整的B,强拥有关系。
【聚合aggregation】A has-a B 引用
类A聚合了类B,UML类图是A空心菱形再实线和箭头指向B。类A中定义了类B的指针作为数据成员,类B的实例化在其他地方。A和B的对象生命周期不一样。A拥有不完整的B,弱拥有。可以认为是 composition by reference == aggregation 或者也可以叫做委托delegation。
19、谈谈你了解的设计模式有哪些?
- 设计模式本质上是通过类与类之间的各种关系来实现的。
- 【模板方法模式】技巧总结:抽象父类+抽象方法+继承+多态
- 【单例模式】技巧总结:聚合了一个自身的静态对象指针+私有构造+公有静态getInstance()方法(懒汉模式)
- 【简单工厂模式】技巧总结:继承+依赖+多态+前后端分离
- 【工厂方法模式】技巧总结:继承+依赖+多态+简单工厂升级(增加具体的工厂子类)
- 【策略模式】技巧总结:继承+依赖+多态+聚合(简单工厂多一个聚合和多一个算法调用函数)
- 【观察者模式】技巧总结:继承+聚合+依赖+多态
20、C++的拷贝构造函数为什么要传引用?
- 如string类的拷贝构造
string( const string & str ){ ... }
参数传递了引用,使用时string str2(str1);
,内部其实是先把实参通过引用传递给形参,const string & str=str1;
没有给str定义初始化,只是使用引用而已。 - 如果进行值传递
string( const string str ){ ... }
,那么内部其实是先有const string str=str1;
等于是先给形参传递实参进行定义构造,就又得进行一次拷贝构造,同样之后会再次进行拷贝构造,导致无限递归进行拷贝构造,直至爆栈程序结束。
21、如何防止一个类被拷贝?——拷贝分为拷贝构造和拷贝赋值。
- 1是将拷贝构造函数和拷贝赋值函数设为private私有的,这样的话在使用到拷贝构造和拷贝赋值时编译就会报错“设为私有的成员函数无法被对象访问”。但是友元以及其他成员函数也还是可以使用(一般将成员函数中出现的本类对象默认当成是友元)。因此是非绝对的禁止拷贝。
- 2是使用C++11的delete关键字,在拷贝构造和拷贝赋值函数定义后面加上=delete;,如
Test(const Test&) = delete;
和Test& operator=(const Test&) =delete;
,那么就会将该类默认提供的这两个函数给delete删除了,用的时候编译会报错“无法引用已删除的函数”。
22、带指针成员变量的类编写要注意什么?
- 必须自定义拷贝构造和拷贝赋值成员函数,因为类默认提供的这两个函数只会完成对所有成员变量的数据浅拷贝,即指针也只会拷贝指针自身的值,而不会去深拷贝指针指向的内容;因此在自定义的拷贝构造和拷贝赋值成员函数中就要注意这个问题然后对指针指向的值就行深拷贝。还要注意,只要自定义了构造、拷贝构造、赋值构造、析构函数,那么默认的就自动失效,如果还想保留默认的,需要=default。
23、什么是RAII 技术?
- RAII(Resource Acquisition Is Initialization资源获取时初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、锁互斥量、智能指针等等)的简单技术。
- RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有
两大好处:
1、不需要显式地释放资源。
2、采用这种方式,对象所需的资源在其生命期内始终保持有效。 - RAII在C++中的应用非常广泛,如C++标准库中的lock_guard便是用RAII方式来控制互斥量,程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。
template <class Mutex> class lock_guard {
private:
Mutex& mutex_;
public:
lock_guard(Mutex& mutex) : mutex_(mutex) { mutex_.lock(); }
~lock_guard() { mutex_.unlock(); }
lock_guard(lock_guard const&) = delete;
lock_guard& operator=(lock_guard const&) = delete;
};
void write_to_file(const std::string & message)
{
// 创建关于文件的互斥锁
static std::mutex mutex;
// 在访问文件前进行加锁,使用了局部对象lock
std::lock_guard<std::mutex> lock(mutex);
// 尝试打开文件
std::ofstream file("example.txt");
if (!file.is_open())
throw std::runtime_error("unable to open file");
// 输出文件内容
file << message << std::endl;
// 当离开作用域时,文件句柄会被首先析构 (不管是否抛出了异常)
// 互斥锁lock对象也会被析构 (同样地,不管是否抛出了异常)
}
- 当一个函数需要通过多个局部变量来管理资源时,RAII就显得非常好用。因为只有被构造成功(构造函数没有抛出异常)的对象才会在返回时调用析构函数[4],同时析构函数的调用顺序恰好是它们构造顺序的反序[5],这样既可以保证多个资源(对象)的正确释放,又能满足多个资源之间的依赖关系。
由于RAII可以极大地简化资源管理,并有效地保证程序的正确和代码的简洁,所以通常会强烈建议在C++中使用它。
24.1、epoll如此高效的原因?
这是由于我们在调用epoll_create时,Linux内核会创建一个eventpoll结构体对象,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个【红黑树】用于存储以后epoll_ctl传来的要监听的socket事件外;还会再建立一个list链表,用于存储准备就绪的socket事件;当epoll_wait调用时,仅仅观察这个list链表里有没有数据(有事件发生的socket句柄)即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket句柄放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。(注:好好理解这句话!)
从上面这句可以看出,epoll的基础就是回调呀!
-
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数。当网卡监听到socket数据变化时,就查找该红黑树是否有监听对应的socket(红黑树查找速度很快),找到红黑树中有该socket时引发中断事件,向准备就绪链表中插入该socket句柄数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
24.2、epoll的水平触发和边沿触发?
-
水平触发(LT):
当epoll检测到其上有事件发生并通知应用程序时,应用程序可以不立即处理,这样当应用程序再次调用epoll中调用函数时,epoll会再次通知应用程序此事件,直到被处理。
边沿触发(ET):
当epoll检测到其上有事件发生并通知应用程序时,应用程序必须立即处理,并且下一次的epoll调用,不会再向应用程序通知此事件。
所以ET模式大大得降低了同一个epoll事件被重复触发的次数,因此ET模式工作效率比LT模式更高。
select、poll、epoll的默认工作模式都是水平触发(LT)模式,但是epoll是支持边沿触发(ET)模式的。如果将ET模式代码中的event.events = EPOLLIN | EPOLLET;改成event.events = EPOLLIN;,即使用LT模式。可见ET,LT模式是针对具体的某个事件来设置的
select/poll会因为监听fd的数量而导致效率低下,因为它是轮询所有fd,有数据就处理,没数据就跳过,所以fd的数量会降低效率;而epoll只处理就绪的fd,它有一个就绪设备的链表,每次只轮询该链表的数据,然后进行处理。 - LT, ET这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回这个句柄。(从上面这段,可以看出,LT还有个回放的过程,低效了)
25、Linux下的三个特殊进程
- Linux下有三个特殊的进程idle进程(PID=0),init进程(PID=1),和kthreadd(PID=2)
- idle(空闲)进程由系统自动创建,运行在内核态
idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换。 - kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间,负责所有内核进程的调度和管理。
它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程 。 - init进程由idle通过kernel_thread创建,在内核空间完成初始化后,加载init程序
在这里我们就主要讲解下init进程,init进程由0进程创建,完成系统的初始化,是系统中所有其他用户进程的祖先进程
Linux中的所有进程都是由init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成后,init将变成为守护进程监视系统其他进程。
所以说init进程是Linux系统操作中不可缺少的程序之一,如果内核找不到init进程就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。
26、平衡二叉树和红黑树的区别?
- 平衡二叉树AVL是指严格的平衡二叉树——即每个树结点的左右子树的深度差值(平衡因子)不超过1。
- 红黑树只是弱平衡二叉树,不是完全遵守平衡因子不超过1的,也就是说可能会出现某结点左右子树的深度差值大于1,因此红黑树是相对平衡的二叉树。
- 那为什么红黑树比AVL平衡二叉树使用的地方更多呢?
- 因为AVL为了保持严格平衡,每次有数据插入或删除后都需要做更多的检查调整工作,效率较低。
而红黑树不需要维持严格平衡,因此插入和删除效率更高。 - 就查找效率来说,AVL因为严格平衡树的深度大概率会比红黑树更小,因此查找效率会更高。
- 总结,AVL更适合于插入和删除不频繁而查找频繁的地方;而红黑树兼顾了插入、删除和查询的性能,所以适用范围更广。
三、STL
1、STL的作用,为什么需要STL?(常用的是SGI版本的STL,被纳入GNU C++标准库)
- 一是可复用性。STL标准模板库,提供了一套常用的标准的数据结构和算法程序以及和泛型模板编程结合,我们在写程序时,可以快速复用,而不需要重复的去造轮子,要站在巨人的肩膀上,节约时间。
- 二是高效性。STL的代码经过多位C++大师的优化,比绝大多数人自己再重写相关的数据结构和算法代码效率更高,因此我们应该尽量多用STL。
- 研究STL源码的意义在于,深入理解和学习优秀的数据结构和算法原理,然后扩展轮子,或者是深入定制自己的轮子。
- STL更注重的是模板泛型编程,而不是面向对象编程。
2、STL六大组件之间的关系?
- 六大组件:空间配置器(allocator)、容器(container)、迭代器(iterator)、算法(algorithm)、仿函数(functor)[函数对象]、适配器(adapter)[包装器]。
-
空间配置器
为容器分配内存,容器的模板参数中都有一个默认的空间配置器class Alloc = alloc
。最简单的空间配置器实现就是类内部用alllocate成员函数封装::operator new(相当于malloc)分配内存,再用construct成员函数负责对象的构造(实际上是借用定位new——placement new来给这块内存初始化的buffer = new(buffer) T(value);
其中T(value)就用到了构造函数);用destroy成员函数完成析构,再用deallocate成员函数封装::operator delete(相当于free),来实现对象的内存分配和构造初始化以及用完后析构和释放内存。而这些分步也是实现常用的new/delete表达式的步骤。STL提供的空间配置器分为第一级配置器和第二级配置器。 -
容器
分为序列式容器(vector/deque/list/forward_list)和关联式容器(set/map/unordered_set/unordered_map)。 -
迭代器
作为容器和算法的粘合剂,算法函数的传入参数一般都是容器的迭代器。指针也是一种特特殊的迭代器。迭代器有5中类型:
1)input iterator:只读入数据的迭代器,不能通过它修改指向的元素值
2)output iterator:只写入数据的迭代器
3)forward iterator:包含上面的可读可写功能,然后只有operator++向前单步移动的功能。
4)bidirectional iterator:包含上面的读写功能,可双向单步移动,支持operator++和--。
5)random access iterator:随机存取迭代器,不仅可读可写,还支持顺序存储如用数组下标的跳步移动访问功能。 算法
-
仿函数
又叫函数对象,常用类名加()代表一个临时对象,可为算法动态地提供某种策略。类中一定重载了operator()。比如常用的将算法库中的sort函数的比较策略用一个仿函数传入,当然也可传入一个函数指针或是对类型重载<号。一般函数指针可以当做仿函数对象用。 -
适配器类
的内部封装(组合)了一个其他类的对象。如容器适配器stack/queue默认就是在内部组合了一个deque<T> dq;
,该deque对象的生命周期和stack/queue对象一样,然后通过封装dq对象的某些成员函数来实现自己的功能。priority_queue也是以vector为底层容器的完全二叉树的容器适配器。前面说的三种容器适配器都不提供迭代器不能遍历。还有仿函数适配器;迭代器适配器。
3、vector、map/multimap、unordered_map/unordered_multimap的底层数据结构,以及几种map容器如何选择?
- 底层数据结构:vector基于数组,map/multimap基于红黑树,unordered_map/unordered_multimap基于哈希表。
根据应用场景进行选择:
map/unordered_map 不允许重复元素
multimap/unordered_multimap允许重复元素
map/multimap底层基于红黑树,元素自动有序,且插入、删除效率高
unordered_map/unordered_multimap底层基于哈希表,故元素无序,查找效率高。
4、type_traits<T>是什么作用?
- 这个的作用是对类型T进行一些和类型相关的询问,然后返回true或false的相关回答(即对类型T萃取出相关类型说明)。也有iterator_traits可以萃取出一个迭代器的相关类型。
比如在空间配置器alloctor的实现中,有个destroy成员函数在对数组中的所有成员析构时,会先用type_traits<T>::has_trivial_destructor
检查成员类型的析构函数是否是trivial(不重要的),若是返回__false_type
则说明这个类型T的析构函数是重要的,必须对数组中每个对象依次调用析构函数。相反,若是返回__true_type
说明确实是不重要的析构函数(甚至是只有默认隐藏的空析构函数),则不需要去遍历数组对象调用析构函数,可以大大提高效率。
5、STL的空间配置器实现原理?
- STL空间配置器的作用是维护对象的
内存分配、释放以及构造、析构
。因此我们可以从这四个作用来看实现原理,空间配置器中一定实现了allocate/deallocate/contructor/destroy
这四个成员函数。这几个成员函数如何实现又要根据使用的空间配置器是第一级还是第二级。 - STL的空间配置器分为第一级
__malloc_alloc_template
和第二级__default_alloc_template
,第一级用于处理从堆中大块内存的申请(一次128字节以上算一大块?),第二级用于处理从自定义的内存池中取得小块内存的申请。默认使用第二级,但是在第二级的allocate
会判断要申请的字节数n若是大于128则直接跳转到第一级,否则再从第二级内存池中对应的链表中获取合适的区块。 - 第一级配置器就是使用
malloc/free/realloc
这几个C语言函数从堆中分配释放内存给用户,并且自己写了处理内存不足的函数set_malloc_handle()
,大致思想就是用一个无限循环来不断尝试分配内存(可能程序在刚开始时就预先分配了大块内存备用,这时候就可以释放一部分出来解决内存分配不足问题),直到分配成功。如果不写这个处理函数,那就只能返回内存分配不足的异常或直接退出程序。 - 第二级配置器(又叫次层配置sub-allocation)是维护了一个内存池,处理频繁的小块内存的申请,相比第一级从堆中申请不仅速度更快还更有线程安全性,也能更好地处理内存碎片的问题节约内存。内存池用一个数组
free_list[16]
维护了128/8=16个链表,分别存放区块大小为{8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128}。首次从对应链表中比如16申请区块时,会调用refill(n=16)函数
先分配20个区块给区块大小n=16的链表备用。 -
void * allocate(size_t n)申请内存过程:
若是n>128那就转到第一级配置器去调用malloc从堆中申请,否则默认按照第二级配置器从内存池申请。比如你要申请n=10的一块内存,就可以向上升级到8的倍数16那个free_list[16/8-1]
的那个链表中获取一个元素(区块)。链表中的元素(区块)obj也有意思,是一个union联合体,在分配给用户使用前将obj->free_list_link赋值给对应链表的首元素my_free_list(由于该变量是obj * volatile * 类型
,直接指向了实际内存而不是存放在临时寄存器中,因此后面给它重新复制会被立即修改,而不用担心多线程问题),以便下次再从这个链表申请内存时可以立即获取一个空闲内存块。然后再分配给用户使用,从第一个char字节开始的内存。
若是从链表中获取不到内存则要从内存池中重新分配区块到链表,若是内存池中没有了足够的内存那就要从堆中分配两倍多的内存到内存池,若是堆中内存不够则可以拆分大区块链表的内存,若是都获取不到内存,那就转到第一级配置的set_malloc_handle()去处理内存分配失败。
union obj{
union obj * free_list_link; //保存着下一个空闲区块的地址
char client_data; //表示给用户申请使用后的数据存放首字节地址
};
-
void deallocate(void *p, size_t n); 释放内存过程:
若是n>128,则直接转到第一级配置器去调用free释放内存到堆,否则默认按照第二级配置器的流程归还到内存池中16个链表对应的那个。
-
内存池设计:
两根指针start_free和end_free分别代表池中自由的内存的首字节和尾字节地址;将要申请的字节数扩展到8的倍数的函数;一个指针数组free_list维护16个链表的区块的首地址;一个union obj表示一个区块,内含下一个区块的指针和首地址;allocate分配内存函数返回分配的区块的首地址若是大于128则转到第一级配置从malloc获取;refill()函数重新填充该种区块的链表,内部调用chunk_alloc()函数从内存池获取一大块内存,再转换成多个小区快(最多20块)并循环链接起来;chunk_alloc()负责提供大块的区块,管理内存池中剩余的区块,不够时从堆空间获取两倍多的需求填充内存池;deallocate头插法归还内存到对应链表的首部。
6、STL中vector内存动态扩展的倍数是多少?设为多少比较合适?为什么
- 不同的STL库实现,可能会设置为不同的倍数,大都是设为2或者1.5倍。理论上来讲设为1.5倍更好,更有利于后面扩展内存时复用前面释放的内存(重新从原开始释放处向后取得一块足够大小的连续内存)。
https://www.nowcoder.com/discuss/90907?type=2&order=0&pos=3&page=1
http://www.cnblogs.com/webary/p/4754522.html
https://blog.csdn.net/lisonglisonglisong/article/details/51327586