面试问题整理

一、数组与链表的区别

数组:php

数组是将元素在内存中连续存放,因为每一个元素占用内存相同,能够经过下标迅速访问数组中任何元素。可是若是要在数组中增长一个元素,须要移动大量元素,在内存中空出一个元素的空间,而后将要增长的元素放在其中。一样的道理,若是想删除一个元素,一样须要移动大量元素去填掉被移动的元素。若是应用须要快速访问数据,不多或不插入和删除元素,就应该用数组。html

链表:
链表刚好相反,链表中的元素在内存中不是顺序存储的,而是经过存在元素中的指针联系到一块儿。好比:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。若是要访问链表中一个元素,须要从第一个元素开始,一直找到须要的元素位置。可是增长和删除一个元素对于链表数据结构就很是简单了,只要修改元素中的指针就能够了。若是应用须要常常插入和删除元素你就须要用链表数据结构了。 linux


数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的状况,即在使用数组以前,就必须对数组的大小进行肯定。当数据增长时,可能超出原先定义的元素个数;当数据减小时,形成内存浪费。数组中插入、删除数据项时,须要移动其它数据项。而链表采用动态分配内存的形式实现,能够适应数据动态地增减的状况,须要时能够用new/malloc分配内存空间,不须要时用delete/free将已分配的空间释放,不会形成内存空间浪费,且能够方便地插入、删除数据项。程序员

数组中的数据在内存中是顺序存储的,而链表是随机存储的。web

数组的随机访问效率很高,能够直接定位,但插入、删除操做的效率比较低。算法

链表在插入、删除操做上相对数组有很高的效率,而若是要访问链表中的某个元素的话,那就得从表头逐个遍历,直到找到所须要的元素为止,因此,链表的随机访问效率比数组低。编程

链表不存在越界问题,数组有越界问题。数组便于查询,链表便于插入删除,数组节省空间可是长度固定,链表虽然变长可是占了更多的存储空间。windows

因此,因为数组存储效率高,存储速度快的优势,若是须要频繁访问数据,不多插入删除操做,则使用数组;反之,若是频繁插入删除,则应使用链表。两者各有用处。设计模式

数组和链表的区别整理以下: 

数组在内存中连续,长度固定;链表不连续,可动态添加。 
数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n) 
数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)数组

 

二、进程与线程的区别

进程是具备必定独立功能的程序关于某个数据集合上的一次运行活动,它是系统进行资源分配和调度的一个独立单位。例如,用户运行本身的程序,系统就建立一个进程,并为它分配资源,包括各类表格、内存空间、磁盘空间、I/O设备等,而后,该进程被放入到进程的就绪队列,进程调度程序选中它,为它分配CPU及其它相关资源,该进程就被运行起来。

线程是进程的一个实体,是CPU调度和分派的基本单位,线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源。

在没有实现线程的操做系统中,进程既是资源分配的基本单位,又是调度的基本单位,它是系统中并发执行的单元。而在实现了线程的操做系统中,进程是资源分配的基本单位,但线程是调度的基本单位,线程是系统中并发执行的单元。

具体而言,引入线程,主要有如下4个方面的优势:

1       易于调度。

2       提升并发性。经过线程能够方便有效地实现并发。

3       开销小。建立线程比建立进程要快,所须要的开销也更少。

4       有利于发挥多处理器的功能。经过建立多线程,每一个线程都在一个处理器上运行,从而实现应用程序的并行,使每一个处理器都获得充分运行。

须要注意的是,尽管线程与进程两者很类似,但也存在着很大的不一样,区别以下:

1       一个线程一定属于也只能属于一个进程;而一个进程能够拥有多个线程而且至少拥有一个线程。

2       属于一个进程的全部线程共享该进程的全部资源,包括打开的文件、建立的Socket等。不一样的进程互相独立。

3       线程又被称为轻量级进程。进程有进程控制块,线程也有线程控制块。但线程控制块比进程控制块小得多。线程间切换代价小,进程间切换代价大。

4       进程是程序的一次执行,线程能够理解为程序中一段程序片断的执行。

5       每一个进程都有独立的内存空间,而线程共享其所属进程的内存空间。

 

进程和线程的主要差异在于它们是不一样的操做系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不一样执行路径。线程有本身的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,因此多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行而且又要共享某些变量的并发操做,只能用线程,不能用进程。

三、常量指针与指针常量

常量指针

  定义:

          又叫常指针,具备只可以读取内存中数据,却不可以修改内存中数据的属性的指针,称为指向常量的指针,简称常量指针。

   关键点:

          1.常量指针指向的对象不能经过这个指针来修改,但是仍然能够经过原来的声明修改;
          2.常量指针能够被赋值为变量的地址,之因此叫常量指针,是限制了经过这个指针修改变量的值;
          3.指针还能够指向别处,由于指针自己只是个变量,能够指向任意地址;

4.指向的地址可变,指向的内容不可变

 代码形式:

          int const* p;  const int* p;

指针常量

    定义:

         本质是一个常量,而用指针修饰它。指针常量是指指针所指向的位置不能改变,即指针自己是一个常量,可是指针所指向的内容能够改变。  

   关键点:

          1.它是个常量!
          2.指针自己是常量,指向的地址不能够变化,可是指向的地址所对应的内容能够变化;

  代码形式:

         int* const p;

指向常量的常指针

  定义:

     指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。

   关键点:

        1.一个指针常量,指向的地址不能变;

        2.它指向的指针对象且是一个常量,即它指向的对象不能变化,指向的内容不能变;

   代码形式:

        const int* const p;

 

四、指针数组与数组指针

指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组自己决定。它是储存指针的数组的简称。

数组指针:首先它是一个指针,它指向一个数组。在32 位系统下永远是占4 个字节,至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。

 

int *p[2]; 首先声明了一个数组,数组的元素是int型的指针。


int (*p)[2]; 声明了一个指针, 指向了一个有两个int元素的数组。


    其实这两种写法主要是由于运算符的优先级由于[]的优先级比*。因此第一种写法,p先和[]结合,因此是一个数组,后与*结合,是指针。后一种写法同理。

 

int (*)[10] p2-----也许应该这么定义数组指针

 

这里有个有意思的话题值得探讨一下:平时咱们定义指针不都是在数据类型后面加上指针变量名么?这个指针p2 的定义怎么不是按照这个语法来定义的呢?也许咱们应该这样来定义p2:
   int (*)[10] p2;
int (*)[10]是指针类型,p2 是指针变量。这样看起来的确不错,不过就是样子有些别扭。其实数组指针的原型确实就是这样子的,只不过为了方便与好看把指针变量p2 前移了而已。你私下彻底能够这么理解这点。虽然编译器不这么想。 

 

五、C++虚函数的实现

双击打开pdf

六、df与du的区别

1. 如何记忆这两个命令

du  Disk Usage

df  Disk Free

2.1 du的工做原理

du命令会对待统计文件逐个调用fstat这个系统调用,获取文件大小。它的数据是基于文件获取的,因此有很大的灵活性,不必定非要针对一个分区,能够跨越多个分区操做。若是针对的目录中文件不少,du速度就会很慢了。

2.2 df的工做原理

df命令使用的是statfs这个系统调用,直接读取分区的超级块信息获取分区使用状况。它的数据是基于分区元数据的,因此只能针对整个分区。因为df直接读取超级块,因此运行速度不受文件多少影响。

3 dudf不一致状况模拟

常见的dfdu不一致状况就是文件删除的问题。当一个文件被删除后,在文件系统目录中已经不可见了,因此du就不会再统计它了。然而若是此时还有运行的进程持有这个已经被删除了的文件的句柄,那么这个文件就不会真正在磁盘中被删除,分区超级块中的信息也就不会更改。这样df仍旧会统计这个被删除了的文件。

工做中须要注意的地方

(1)当出现dudf差距很大的状况时,考虑是不是有删除文件未完成形成的,方法是lsof命令,而后中止相关进程便可。

(2)能够使用清空文件的方式来代替删除文件,方式是:echo > myfile.iso

(3)对于常常发生删除问题的日志文件,以更名、清空、删除的顺序操做。

(4)除了rm外,有些命令会间接的删除文件,如gzip命令完成后会删除原来的文件,为了不删除问题,压缩前先确认没有进程打开该文件。

 

df命令经过查看文件系统磁盘块分配图得出总块数与剩余块数。
文件系统分配其中的一些磁盘块用来记录它自身的一些数据,如i节点,磁盘分布图,间接块,超级块等。这些数据对大多数用户级的程序来讲是不可见的,一般称为Meta Data
du命令是用户级的程序,它不考虑Meta Data,而df命令则查看文件系统的磁盘分配图并考虑Meta Data

 

七、堆与栈

 

主要的区别由如下几点:

      1、管理方式不一样;

      2、空间大小不一样;

      3、可否产生碎片不一样;

      4、生长方向不一样;

      5、分配方式不一样;

      6、分配效率不一样;

      管理方式:对于栈来说,是由编译器自动管理,无需咱们手工控制;对于堆来讲,释放工做由程序员控制,容易产生memory leak

      空间大小:通常来说在32位系统下,堆内存能够达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。可是对于栈来说,通常都是有必定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。固然,咱们能够修改:    

      打开工程,依次操做菜单以下:Project->Setting->Link,在Category 中选中Output,而后在Reserve中设定堆栈的最大值和commit

注意:reserve最小值为4Bytecommit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增长内存的开销和启动时间。

      碎片问题:对于堆来说,频繁的new/delete势必会形成内存空间的不连续,从而形成大量的碎片,使程序效率下降。对于栈来说,则不会存在这个问题,由于栈是先进后出的队列,他们是如此的一一对应,以致于永远都不可能有一个内存块从栈中间弹出,在他弹出以前,在他上面的后进的栈内容已经被弹出,详细的能够参考数据结构,这里咱们就再也不一一讨论了。

      生长方向:对于堆来说,生长方向是向上的,也就是向着内存地址增长的方向;对于栈来说,它的生长方向是向下的,是向着内存地址减少的方向增加。

      分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,好比局部变量的分配。动态分配由alloca函数进行分配,可是栈的动态分配和堆是不一样的,他的动态分配是由编译器进行释放,无需咱们手工实现。

      分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照必定的算法(具体的算法能够参考数据结构/操做系统)在堆内存中搜索可用的足够大小的空间,若是没有足够大小的空间(多是因为内存碎片太多),就有可能调用系统功能去增长程序数据段的内存空间,这样就有机会分到足够大小的内存,而后进行返回。显然,堆的效率比栈要低得多。

八、网络七层协议

应用层(Application Layer)

与其它计算机进行通信的一个应用,它是对应应用程序的通讯服务的。例如,一个没有通讯功能的字处理程序就不能执行通讯的代码,从事字处理工做的程序员也不关心OSI的第7层。可是,若是添加了一个传输文件的选项,那么字处理器的程序员就须要实现OSI的第7层。示例:telnetHTTP,FTP,NFS,SMTP等。

表示层(Presentation Layer)

这一层的主要功能是定义数据格式及加密。例如,FTP容许你选择以二进制或ASCII格式传输。若是选择二进制,那么发送方和接收方不改变文件的内容。若是选择ASCII格式,发送方将把文本从发送方的字符集转换成标准的ASCII后发送数据。在接收方将标准的ASCII转换成接收方计算机的字符集。示例:加密,ASCII等。

会话层(Session Layer)

它定义了如何开始、控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时能够通知应用,从而使表示层看到的数据是连续的,在某些状况下,若是表示层收到了全部的数据,则用数据表明表示层。示例:RPCSQL等。

传输层(Transport Layer)

这层的功能包括是否选择差错恢复协议仍是无差错恢复协议,及在同一主机上对不一样应用的数据流的输入进行复用,还包括对收到的顺序不对的数据包的从新排序功能。示例:TCPUDPSPX

网络层(Network Layer)

这层对端到端的包传输进行定义,它定义了可以标识全部结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。示例:IP,IPX等。

数据链路层(Data Link Layer)

它定义了在单个链路上如何传输数据。这些协议与被讨论的各类介质有关。示例:ATMFDDI等。

物理层(Physical Layer)

OSI的物理层规范是有关传输介质的特性标准,这些规范一般也参考了其余组织制定的标准。链接头、帧、帧的使用、电流、编码及光调制等都属于各类物理层规范中的内容。物理层经常使用多个规范完成对全部细节的定义。示例:Rj45802.3等。

 

九、迪杰斯特拉算法

 

十、New、delete和malloc、free的区别。返回值有什么不一样

 

返回值区别:malloc失败返回NULL

New失败会触发异常。

 

十一、机器为何采用补码

1)由于使用补码能够将符号位和其余位统一处理,同时,减法也能够按加法来处理,即若是是补码表示的数,无论是加减法都直接用加法运算便可实现。

2)两个用补码表示的数相加时,若是最高位(符号位)有进位,则进位被舍弃。

这样的运算有两个好处:

1)使符号位能与有效值部分一块儿参加运算,从而简化运算规则。从而能够简化运算器的结构,提升运算速度;(减法运算能够用加法运算表示出来。)

2)加法运算比减法运算更易于实现。使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计。

采用补码表示还有另一个缘由,那就是为了防止0的机器数有两个编码。原码和反码表示的0有两种形式+0-0,而咱们知道,+0-0是相同的。这样,8位的原码和反码表示的整数的范围就是-127~+12711111111~01111111),而采用补码表示的时候,00000000+0,即010000000再也不是-0,而是-128,这样,补码表示的数的范围就是-128~+127了,不但增长了一个数得表示范围,并且还保证了0编码的惟一性。

 

十二、存储器层次

Cache——主存

主存——辅存

1三、函数重载

两个重载函数必须在下列一个或两个方面有所区别:

一、函数有不一样参数。

二、函数有不一样参数类型,

注:不一样的返回值,属于错误的函数重复声明!

C++运算符重载的相关规定以下:

(1)不能改变运算符的优先级;

(2)不能改变运算符的结合型;

(3)默认参数不能和重载的运算符一块儿使用;

(4)不能改变运算符的操做数的个数;

(5)不能建立新的运算符,只有已有运算符能够被重载;

(6)运算符做用于C++内部提供的数据类型时,原来含义保持不变。

1四、动态连接库与静态连接库

http://wxxweb.blog.163.com/blog/static/1351269002010113185624129/

1、           介绍

 

本文意在讲解静态连接库与动态连接库的建立与使用,在此以前先来对两者的概念、区别及优缺点进行简要的阐述。其中大多内容参考相关网络资料,因为本人能力有限,不能确保彻底准确无误,如有误差之处请不吝指出。文中使用到的代码均在Visual Studio 2008中编译经过,若是您使用的IDE与本文不一样,可根据实际状况进行相应项目建立与操做。但愿本文内容对您有所帮助。

 

2、           概念定义

 

1.     分别编译与连接

 

大多数高级语言都支持分别编译(Compiling),程序员能够显式地把程序划分为独立的模块或文件,而后由编译器(Compiler)对每一个独立部分分别进行编译。在编译以后,由连接器(Linker)把这些独立编译单元连接(Linking)到一块儿。连接方式分为两种:

 

(1)     静态连接方式:在程序开发中,将各类目标模块(.OBJ)文件、运行时库(.LIB)文件,以及常常是已编译的资源(.RES)文件连接在一块儿,以便建立Windows.EXE文件。

(2)     动态连接方式:在程序运行时,Windows把一个模块中的函数调用连接到库模块中的实际函数上的过程。

2.     静态连接库与动态连接库

 

静态连接库(Static Library,简称LIB)与动态连接库(Dynamic Link Library,简称DLL)都是共享代码的方式。若是使用静态连接库(也称静态库),则不管你愿不肯意,.LIB文件中的指令都会被直接包含到最终生成的.EXE文件中。可是若使用.DLL文件,.DLL文件中的代码没必要被包含在最终的.EXE文件中,.EXE文件执行时能够“动态”地载入和卸载这个与.EXE文件独立的.DLL文件。

 

2.1.          动态连接方式

 

连接一个DLL有两种方式:

 

2.1.1           载入时动态连接(Load-Time Dynamic Linking

 

使用载入时动态连接,调用模块能够像调用本模块中的函数同样直接使用导出函数名调用DLL中的函数。这须要在连接时将函数所在DLL的导入库连接到可执行文件中,导入库向系统提供了载入DLL时所需的信息及用于定位DLL函数的地址符号。(至关于注册,看成API函数来使用,其实API函数就存放在系统DLL当中。)

 

2.1.2           运行时动态连接(Run-Time Dynamic Linking

 

使用运行时动态连接,运行时能够经过LoadLibraryLoadLibraryEx函数载入DLLDLL载入后,模块能够经过调用GetProcAddress获取DLL函数的入口地址,而后就能够经过返回的函数指针调用DLL中的函数了。如此便可避免导入库文件了。

 

2.2.          两者优势及不足

 

2.2.1           静态连接库的优势

 

(1)     代码装载速度快,执行速度略比动态连接库快;

(2)     只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。

2.2.2           动态连接库的优势

 

(1)     更加节省内存并减小页面交换;

(2)     DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件形成任何影响,于是极大地提升了可维护性和可扩展性;

(3)     不一样编程语言编写的程序只要按照函数调用约定就能够调用同一个DLL函数。

(4) 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不一样开发者和开发组织之间进行开发和测试。

2.2.3           不足之处

 

(1)     使用静态连接生成的可执行文件体积较大,包含相同的公共代码,形成浪费;

(2)     使用动态连接库的应用程序不是自完备的,它依赖的DLL模块也要存在,若是使用载入时动态连接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态连接,系统不会终止,但因为DLL中的导出函数不可用,程序会加载失败;

(3)     使用动态连接库可能形成DLL地狱。

2.3.          DLL 地狱

 

DLL 地狱(DLL Hell)是指由于系统文件被覆盖而让整个系统像是掉进了地狱。

 

简单地讲,DLL地狱是指当多个应用程序试图共享一个公用组件时,如某个DLL或某个组件对象模型(COM)类,所引起的一系列问题。

 

最典型的状况是,某个应用程序将要安装一个新版本的共享组件,而该组件与机器上的现有版本不向后兼容。虽然刚安装的应用程序运行正常,但原来依赖前一版本共享组件的应用程序也许已没法再工做。在某些状况下,问题的原由更加难以预料。好比,当用户浏览某些web站点时会同时下载某个Microsoft ActiveX控件。若是下载该控件,它将替换机器上原有的任何版本的控件。若是机器上的某个应用程序刚好使用该控件,则极可能也会中止工做。

 

在许多状况下,用户须要很长时间才会发现应用程序已中止工做。结果每每很难记起是什么时候的机器变化影响到了该应用程序。

 

这些问题的缘由是应用程序不一样组件的版本信息没有由系统记录或增强。并且,系统为某个应用程序所作的改变会影响机器上的全部应用程序—如今创建彻底从变化中隔离出来的应用程序并不容易。

1五、僵死进程

UNIX 系统中,一个进程结束了,可是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程。 可是若是该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 由于每一个进程结束的时候,系统都会扫描当前系统中所运行的全部进程, 看有没有哪一个进程是刚刚结束的这个进程的子进程,若是是的话,就由Init 来接管他,成为他的父进程。

一个进程在调用exit命令结束本身的生命的时候,其实它并无真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit它的做用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其彻底销毁)。在Linux进程的状态中,僵尸进程是很是特殊的一种,它已经放弃了几乎全部内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其余进程收集,除此以外,僵尸进程再也不占有任何内存空间。它须要它的父进程来为它收尸,若是他的父进程没安装SIGCHLD信号处理函数调用waitwaitpid()等待子进程结束,又没有显是忽略该信号,那么它就一直保持僵尸状态,若是这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它仍是能被清除的。可是若是父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为何系统中有时会有不少的僵尸进程。

 

 

危害:

进程长时间保持僵尸状态通常是错误的并致使资源泄漏

避免:

1、改写父进程,要求父进程为子进程收尸,调用waitpid()函数或者接受处理SIGCHLD信号

2、若是父进程不关心子进程何时结束,那么能够用signalSIGCHLD,SIG_IGN) 通知内核,本身对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并再也不给父进程发送信号。

3fork两次,杀死一级子进程,使得二级子进程成为孤儿进程,由init接受。可是这里父进程仍是要处理杀死一级进程并为他收尸。

解除僵尸进程

一、手动向(kill)父进程发送SIGCHLD信号,若是父进程仍不处理,则

二、杀死父进程,使其成为孤儿进程,由init接受,init会回收清理。

 

1六、MVC介绍

 

1七、面向对象思想

三大特性:封装,继承,多态。

六大原则:单一职责原则,依赖倒转原则,里氏准则,开放-封闭原则,接口隔离原则,迪米特法则。

一、单一职责SRP--Single-Responsibility Principle

就一个类而言,应该只专一于作一件事和仅有一个引发它变化的缘由

SRP优势:

消除耦合,减少因需求变化引发代码僵化性臭味

使用SRP注意点:

一、一个合理的类,应该仅有一个引发它变化的缘由,即单一职责; 
二、在没有变化征兆的状况下应用SRP或其余原则是不明智的; 
三、在需求实际发生变化时就应该应用SRP等原则来重构代码; 
四、使用测试驱动开发会迫使咱们在设计出现臭味以前分离不合理代码; 
五、若是测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用Facade或Proxy模式对代码重构;

二、开放封闭原则OCP--Open-Closed Principle

Software entities(classes,modules,functions,etc.) should be open for extension, but closed for modification

软件实体应当对扩展开放,对修改关闭,即软件实体应当在不修改(在.Net当中可能经过代理模式来达到这个目的)的前提下扩展。

Open for extension:当新需求出现的时候,能够经过扩展示有模型达到目的。   

Close for modification:对已有的二进制代码,如dll,jar等,则不容许作任何修改。

OCP优势:

一、下降程序各部分之间的耦合性,使程序模块互换成为可能;
二、使软件各部分便于单元测试,经过编制与接口一致的模拟类(Mock),能够很容易地实现软件各部分的单元测试;
三、利于实现软件的模块的呼唤,软件升级时能够只部署发生变化的部分,而不会影响其它部分;

 

使用OCP注意点:

1、实现OCP原则的关键是抽象;

2、两种安全的实现开闭原则的设计模式是:Strategy pattern策略模式),Template Methord(模版方法模式);

3、依据开闭原则,咱们尽可能不要修改类,只扩展类,但在有些状况下会出现一些比较怪异的情况,这时能够采用几个类进行组合来完成;

四、将可能发生变化的部分封装成一个对象,如: 状态, 消息,,算法,数据结构等等 , 封装变化是实现"开闭原则"的一个重要手段,如常常发生变化的状态值,如温度,气压,颜色,积分,排名等等,能够将这些做为独立的属性,若是参数之间有关系,有必要进行抽象。对于行为,若是是基本不变的,则能够直接做为对象的方法,不然考虑抽象或者封装这些行为;

五、在许多方面,OCP是面向对象设计的核心所在。遵循这个原则可带来面向对象技术所声称的巨大好处(灵活性、可重用性以及可维护性)。然而,对于应用程序的每一个部分都肆意地进行抽象并非一个好主意。应该仅仅对程序中呈现出频繁变化的那部分做出抽象。拒毫不成熟的抽象和抽象自己同样重要;

三、里氏准则LSP--Liskov Substitution Principle

子类型必须可以替换它的基类型。LSP又称里氏替换原则。

对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写。
LSP优势:

一、保证系统或子系统有良好的扩展性。只有子类可以彻底替换父类,才能保证系统或子系统在运行期内识别子类就能够了,于是使得系统或子系统有了良好的扩展性。
二、实现运行期内绑定,即保证了面向对象多态性的顺利进行。这节省了大量的代码重复或冗余。避免了相似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。
三、有利于实现契约式编程。契约式编程有利于系统的分析和设计,指咱们在分析和设计的时候,定义好系统的接口,而后再编码的时候实现这些接口便可。在父类里定义好子类须要实现的功能,而子类只要实现这些功能便可。

 

四使用LSP注意点:

一、此原则和OCP的做用有点相似,其实这些面向对象的基本原则就2条:1:面向接口编程,而不是面向实现;2:用组合而不主张用继承

二、LSP是保证OCP的重要原则
三、这些基本的原则在实现方法上也有个共同层次,就是使用中间接口层,以此来达到类对象的低偶合,也就是抽象偶合!

四、派生类的退化函数:派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会致使替换违规。在派生类中存在退化函数并不老是表示违反了LSP,可是当存在这种状况时,应该引发注意。 
五、从派生类抛出异常:若是在派生类的方法中添加了其基类不会抛出的异常。若是基类的使用者不指望这些异常,那么把他们添加到派生类的方法中就能够能会致使不可替换性。

四、依赖倒转DIP--Dependency Inversion Principle

一、高层模块不该该依赖于低层模块,两者都应该依赖于抽象。

二、抽象不该该依赖于细节,细节应该依赖于抽象。

DIP优势:

使用传统过程化程序设计所建立的依赖关系,策略依赖于细节,这是糟糕的,由于策略受到细节改变的影响。依赖倒置原则使细节和策略都依赖于抽象,抽象的稳定性决定了系统的稳定性。

启发式规则:

一、任何变量都不该该持有一个指向具体类的指针或者引用
二、任何类都不该该从具体类派生(始于抽象,来自具体)
三、任何方法都不该该覆写它的任何基类中的已经实现了的方法

 

五、接口分离原则ISP--Interface Segregation Principle

使用多个专门的接口比使用单一的总接口要好。

一个类对另一个类的依赖性应当是创建在最小的接口上的。

一个接口表明一个角色,不该当将不一样的角色都交给一个接口。没有关系的接口合并在一块儿,造成一个臃肿的大接口,这是对角色和接口的污染。

 

“不该该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,若是强迫用户使用它们不使用的方法,那么这些客户就会面临因为这些不使用的方法的改变所带来的改变。

实现方法:
一、使用委托分离接口
二、使用多重继承分离接口

六、迪米特法则

若是两个类没必要彼此直接通讯,那么这两个类就不该当发生直接的相互做用。若是其中的一个类须要调用另外一个类的某一个方法的话,能够经过第三者转发这个调用。

迪米特法则能够简单说成:talk only to your immediate friends。

设计模式的门面模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。

1八、c中常见的内存问题

http://blog.sciencenet.cn/blog-267716-666789.html

http://see.xidian.edu.cn/cpp/html/483.html

 

1、指针没有指向一块合法的内存

定义了指针变量,可是没有为指针分配内存,即指针没有指向一块合法的内存。浅显的例子就不举了,这里举几个比较隐蔽的例子。

一、结构体成员指针未初始化
struct student
{
   char *name;
   int score;
}stu,*pstu;
intmain()
{
   strcpy(stu.name,"Jimy");
   stu.score = 99;
   return 0;
}
不少初学者犯了这个错误还不知道是怎么回事。这里定义告终构体变量stu,可是他没想到这个结构体内部char *name 这成员在定义结构体变量stu 时,只是给name 这个指针变量自己分配了4 个字节。name 指针并无指向一个合法的地址,这时候其内部存的只是一些乱码。因此在调用strcpy 函数时,会将字符串"Jimy"往乱码所指的内存上拷贝,而这块内存name 指针根本就无权访问,致使出错。解决的办法是为name 指针malloc 一块空间。

一样,也有人犯以下错误:
intmain()
{
   pstu = (struct student*)malloc(sizeof(struct student));
   strcpy(pstu->name,"Jimy");
   pstu->score = 99;
   free(pstu);
   return 0;
}
为指针变量pstu 分配了内存,可是一样没有给name 指针分配内存。错误与上面第一种状况同样,解决的办法也同样。这里用了一个malloc 给人一种错觉,觉得也给name 指针分配了内存。

二、没有为结构体指针分配足够的内存
intmain()
{
   pstu = (struct student*)malloc(sizeof(struct student*));
   strcpy(pstu->name,"Jimy");
   pstu->score = 99;
   free(pstu);
   return 0;
}
为pstu 分配内存的时候,分配的内存大小不合适。这里把sizeof(struct student)误写为sizeof(struct student*)。固然name 指针一样没有被分配内存。解决办法同上。

三、函数的入口校验
无论何时,咱们使用指针以前必定要确保指针是有效的。

通常在函数入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用if(NULL != p)来校验。但这都有一个要求,即p 在定义的同时被初始化为NULL 了。好比上面的例子,即便用if(NULL != p)校验也起不了做用,由于name 指针并无被初始化为NULL,其内部是一个非NULL 的乱码。

assert 是一个宏,而不是函数,包含在assert.h 头文件中。若是其后面括号里的值为假,则程序终止运行,并提示出错;若是后面括号里的值为真,则继续运行后面的代码。这个宏只在Debug 版本上起做用,而在Release 版本被编译器彻底优化掉,这样就不会影响代码的性能。

有人也许会问,既然在Release 版本被编译器彻底优化掉,那Release 版本是否是就彻底没有这个参数入口校验了呢?这样的话那不就跟不使用它效果同样吗?

是的,使用assert 宏的地方在Release 版本里面确实没有了这些校验。可是咱们要知道,assert 宏只是帮助咱们调试代码用的,它的一切做用就是让咱们尽量的在调试函数的时候把错误排除掉,而不是等到Release 以后。它自己并无除错功能。再有一点就是,参数出现错误并不是本函数有问题,而是调用者传过来的实参有问题。assert 宏能够帮助咱们定位错误,而不是排除错误。

2、为指针分配的内存过小

为指针分配了内存,可是内存大小不够,致使出现越界错误。
   char *p1 = “abcdefg”;
   char *p2 = (char *)malloc(sizeof(char)*strlen(p1));
   strcpy(p2,p1);
p1 是字符串常量,其长度为7 个字符,但其所占内存大小为8 个byte。初学者每每忘了字符串常量的结束标志“\0”。这样的话将致使p1 字符串中最后一个空字符“\0”没有被拷贝到p2 中。解决的办法是加上这个字符串结束标志符:
   char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
这里须要注意的是,只有字符串常量才有结束标志符。好比下面这种写法就没有结束标志符了:
   char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
另外,不要由于char 类型大小为1 个byte 就省略sizof(char)这种写法。这样只会使你的代码可移植性降低。

3、内存分配成功,但并未初始化

犯这个错误每每是因为没有初始化的概念或者是觉得内存分配好以后其值天然为0。未初始化指针变量也许看起来不那么严重,可是它确确实实是个很是严重的问题,并且每每出现这种错误很难找到缘由。

曾经有一个学生在写一个windows 程序时,想调用字库的某个字体。而调用这个字库须要填充一个结构体。他很天然的定义了一个结构体变量,而后把他想要的字库代码赋值给了相关的变量。可是,问题就来了,无论怎么调试,他所须要的这种字体效果老是不出来。我在检查了他的代码以后,没有发现什么问题,因而单步调试。在观察这个结构体变量的内存时,发现有几个成员的值为乱码。就是其中某一个乱码惹得祸!由于系统会按照这个结构体中的某些特定成员的值去字库中寻找匹配的字体,当这些值与字库中某种字体的某些项匹配时,就调用这种字体。可是很不幸,正是由于这几个乱码,致使没有找到相匹配的字体!由于系统并没有法区分什么数据是乱码,什么数据是有效的数据。只要有数据,系统就理所固然的认为它是有效的。

也许这种严重的问题并很少见,可是也毫不能掉以轻心。因此在定义一个变量时,第一件事就是初始化。你能够把它初始化为一个有效的值,好比:
   int i = 10;
   char *p = (char *)malloc(sizeof(char));
可是每每这个时候咱们还不肯定这个变量的初值,这样的话能够初始化为0 或NULL。
   int i = 0;
   char *p = NULL;
若是定义的是数组的话,能够这样初始化:
   int a[10] = {0};
或者用memset 函数来初始化为0:
   memset(a,0,sizeof(a));
memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置的值;第三个参数是要被设置的内存大小,单位为byte。这里并不想过多的讨论memset 函数的用法,若是想了解更多,请参考相关资料。

至于指针变量若是未被初始化,会致使if 语句或assert 宏校验失败。这一点,上面已有分析。

4、内存越界

内存分配成功,且已经初始化,可是操做越过了内存的边界。这种错误常常是因为操做数组或指针时出现“多1”或“少1”。好比:
int a[10] = {0};
for (i=0; i<=10; i++)
{
   a[i] = i;
}
因此,for 循环的循环变量必定要使用半开半闭的区间,并且若是不是特殊状况,循环变量尽可能从0 开始。

5、内存泄漏

内存泄漏几乎是很难避免的,无论是老手仍是新手,都存在这个问题。甚至包括windows,Linux 这类软件,都或多或少有内存泄漏。也许对于通常的应用软件来讲,这个问题彷佛不是那么突出,重启一下也不会形成太大损失。可是若是你开发的是嵌入式系统软件呢?好比汽车制动系统,心脏起搏器等对安全要求很是高的系统。你总不能让心脏起搏器重启吧,人家阎王老爷是很是好客的。

会产生泄漏的内存就是堆上的内存(这里不讨论资源或句柄等泄漏状况),也就是说由malloc 系列函数或new 操做符分配的内存。若是用完以后没有及时free 或delete,这块内存就没法释放,直到整个程序终止。

一、告老还乡求良田
怎么去理解这个内存分配和释放过程呢?先看下面这段对话:
万岁爷:爱卿,你为朕立下了汗马功劳,想要何赏赐啊?
某功臣:万岁,黄金白银,臣视之如粪土。臣年岁已老,欲告老还乡。臣乞良田千亩以荫后世,别无他求。
万岁爷:爱卿,你劳苦功高,却仅要如此小赏,朕今天就如你所愿。户部刘侍郎,查看湖广一带是否还有千亩上等良田不曾封赏。
刘侍郎:长沙尚有五万余亩上等良田不曾封赏。
万岁爷:在长沙拨良田千亩封赏爱卿。爱卿,良田千亩,你欲何用啊?
某功臣:谢万岁。长沙一带,适合种水稻,臣想用来种水稻。种水稻须要把田分为一亩一块,方便耕种。
。。。。

二、如何使用malloc 函数
不要莫名其妙,其实上面这段小小的对话,就是malloc 的使用过程。malloc 是一个函数,专门用来从堆上分配内存。使用malloc 函数须要几个要求:
内存分配给谁?这里是把良田分配给某功臣。
分配多大内存?这里是分配一千亩。
是否还有足够内存分配?这里是还有足够良田分配。
内存的将用来存储什么格式的数据,即内存用来作什么?
这里是用来种水稻,须要把田分红一亩一块。分配好的内存在哪里?这里是在长沙。

若是这五点都肯定,那内存就能分配。下面先看malloc 函数的原型:
   (void *)malloc(int size)
malloc 函数的返回值是一个void 类型的指针,参数为int 类型数据,即申请分配的内存大小,单位是byte。内存分配成功以后,malloc 函数返回这块内存的首地址。你须要一个指针来接收这个地址。可是因为函数的返回值是void *类型的,因此必须强制转换成你所接收的类型。也就是说,这块内存将要用来存储什么类型的数据。好比:
   char *p = (char *)malloc(100);
在堆上分配了100 个字节内存,返回这块内存的首地址,把地址强制转换成char *类型后赋给char *类型的指针变量p。同时告诉咱们这块内存将用来存储char 类型的数据。也就是说你只能经过指针变量p 来操做这块内存。这块内存自己并无名字,对它的访问是匿名访问。

上面就是使用malloc 函数成功分配一块内存的过程。可是,每次你都能分配成功吗?

不必定。上面的对话,皇帝让户部侍郎查询是否还有足够的良田未被分配出去。使用malloc函数一样要注意这点:若是所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,函数返回NULL。注意这里说的“堆上剩余内存块”不是全部剩余内存块之和,由于malloc 函数申请的是连续的一块内存。

既然malloc 函数申请内存有不成功的可能,那咱们在使用指向这块内存的指针时,必须用if(NULL != p)语句来验证内存确实分配成功了。

三、用malloc 函数申请0 字节内存
另外还有一个问题:用malloc 函数申请0 字节内存会返回NULL 指针吗?

能够测试一下,也能够去查找关于malloc 函数的说明文档。申请0 字节内存,函数并不返回NULL,而是返回一个正常的内存地址。可是你却没法使用这块大小为0 的内存。这好尺子上的某个刻度,刻度自己并无长度,只有某两个刻度一块儿才能量出长度。对于这一点必定要当心,由于这时候if(NULL != p)语句校验将不起做用。

四、内存释放
既然有分配,那就必须有释放。否则的话,有限的内存总会用光,而没有释放的内存却在空闲。与malloc 对应的就是free 函数了。free 函数只有一个参数,就是所要释放的内存块的首地址。好比上例:
   free(p);
free 函数看上去挺狠的,但它到底做了什么呢?其实它就作了一件事:斩断指针变量与这块内存的关系。好比上面的例子,咱们能够说malloc 函数分配的内存块是属于p 的,由于咱们对这块内存的访问都须要经过p 来进行。free 函数就是把这块内存和p 之间的全部关系斩断。今后p 和那块内存之间再无瓜葛。至于指针变量p 自己保存的地址并无改变,可是它对这个地址处的那块内存却已经没有全部权了。那块被释放的内存里面保存的值也没有改变,只是再也没有办法使用了。

这就是free 函数的功能。按照上面的分析,若是对p 连续两次以上使用free 函数,确定会发生错误。由于第一使用free 函数时,p 所属的内存已经被释放,第二次使用时已经无内存可释放了。关于这点,我上课时让学生记住的是:必定要一夫一妻制,否则确定出错。

malloc 两次只free 一次会内存泄漏;malloc 一次free 两次确定会出错。也就是说,在程序中malloc 的使用次数必定要和free 相等,不然必有错误。这种错误主要发生在循环使用malloc 函数时,每每把malloc 和free 次数弄错了。这里留个练习:
写两个函数,一个生成链表,一个释放链表。两个函数的参数都只使用一个表头指针。

五、内存释放以后
既然使用free 函数以后指针变量p 自己保存的地址并无改变,那咱们就须要从新把p的值变为NULL:
   p = NULL;
这个NULL 就是咱们前面所说的“栓野狗的链子”。若是你不栓起来早晚会出问题的。好比:
在free(p)以后,你用if(NULL != p)这样的校验语句还能起做用吗?例如:
   char *p = (char *)malloc(100);
   strcpy(p, “hello”);
   free(p); /* p 所指的内存被释放,可是p 所指的地址仍然不变*/
   …
   if (NULL != p)
   {
      /* 没有起到防错做用*/
      strcpy(p, “world”); /* 出错*/
   }
释放完块内存以后,没有把指针置NULL,这个指针就成为了“野指针”,也有书叫“悬垂指针”。这是很危险的,并且也是常常出错的地方。因此必定要记住一条:free 完以后,必定要给指针置NULL。

同时留一个问题:对NULL 指针连续free 屡次会出错吗?为何?若是让你来设计free函数,你会怎么处理这个问题?

6、内存已经被释放了,可是继续经过指针来使用

这里通常有三种状况:
第一种:就是上面所说的,free(p)以后,继续经过p 指针来访问内存。解决的办法就是给p 置NULL。
第二种:函数返回栈内存。这是初学者最容易犯的错误。好比在函数内部定义了一个数组,却用return 语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的生命周期。
第三种:内存使用太复杂,弄不清到底哪块内存被释放,哪块没有被释放。解决的办法是从新设计程序,改善对象之间的调用关系。

1九、malloc机制

一、linux中实现

http://blog.csdn.net/hzhzh007/article/details/6424638

http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/

 

如何查看进程发生缺页中断的次数?

         ps -o majflt,minflt -C program命令查看。

          majflt表明major fault,中文名叫大错误,minflt表明minor fault,中文名叫小错误。

          这两个数值表示一个进程自启动以来所发生的缺页中断的次数。

发成缺页中断后,执行了那些操做?

当一个进程发生缺页中断的时候,进程会陷入内核态,执行如下操做: 
1检查要访问的虚拟地址是否合法 
2查找/分配一个物理页 
3填充物理页内容(读取磁盘,或者直接置0,或者啥也不干) 
4创建映射关系(虚拟地址到物理地址) 
从新执行发生缺页中断的那条指令 
若是第3步,须要读取磁盘,那么此次缺页中断就是majflt,不然就是minflt 

内存分配的原理

从操做系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brkmmap(不考虑共享内存)。

1brk是将数据段(.data)的最高地址指针_edata往高地址推;

2mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

     这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操做系统负责分配物理内存,而后创建虚拟内存和物理内存之间的映射关系。


在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brkmmapmunmap这些系统调用实现的。


下面以一个例子来讲明内存分配的原理:

状况1、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(所以没有初始化),第一次读/写数据时,引发内核缺页中断,内核才分配对应的物理内存,而后虚拟地址空间创建映射关系),以下图:

 

 

1进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。

      其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。

      _edata指针(glibc里面定义)指向数据段的最高地址。 
2进程调用A=malloc(30K)之后,内存空间如图2

      malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。

      你可能会问:只要把_edata+30K就完成内存分配了?

      事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存如今仍是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,若是用malloc分配了A这块内容,而后历来不访问它,那么,A对应的物理页是不会被分配的。 
3进程调用B=malloc(40K)之后,内存空间如图3

状况2、malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,并且初始化为0),以下图:

 

4进程调用C=malloc(200K)之后,内存空间如图4

      默认状况下,malloc函数分配内存,若是请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存

      这样子作主要是由于::

      brk分配的内存须要等到高地址内存释放之后才能释放(例如,在B释放以前,A是不可能释放的,这就是内存碎片产生的缘由,何时紧缩看下面),而mmap分配的内存能够单独释放。

      固然,还有其它的好处,也有坏处,再具体下去,有兴趣的同窗能够去看glibc里面malloc的代码了。 
5进程调用D=malloc(100K)之后,内存空间如图5
6进程调用free(C)之后,C对应的虚拟内存和物理内存一块儿释放。

 

7进程调用free(B)之后,如图7所示:

        B对应的虚拟内存和物理内存都没有释放,由于只有一个_edata指针,若是往回推,那么D这块内存怎么办呢

固然,B这块内存,是能够重用的,若是这个时候再来一个40K的请求,那么malloc极可能就把B这块内存返回回去了 
8进程调用free(D)之后,如图8所示:

        BD链接起来,变成一块140K的空闲内存。

9默认状况下:

       当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操做(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,因而内存紧缩,变成图9所示。

二、AT&T实现

http://blog.csdn.net/dog250/article/details/5302958

本质上贝尔实验室的malloc使用了一个线性的链表来表示能够分配的内存块,熟悉伙伴系统和slab的应该知道这是这么回事,可是仍然和slab和伙伴系统有所不一样,slab中分配的都是相同大小的内存块,而伙伴系统中分配的是不一样肯定大小的内存块,好比肯定的1248...的内存,贝尔试验室的malloc版本是一种随意的分配,自己遵循碎片最少化,利用率最大化的原则分配,其实想一想slab的初衷,其实就是一个池的概念,省去了分配时的开销,而伙伴系统的提出就是为了消除碎片,贝尔malloc版本一箭双雕。

typedef struct mem{

 

struct mem *next;//巧妙之处

 

Unsigned len;

 

}mem;

Fmem指针,&F并非一个真正的mem指针,而是因为mem的第一个字段为一个mem指针,而&F在内存中应该是一个mem指针的指针,可是该指针的指针不论如何也是一个指针类型,其指向的数据正好也是一个指针,后者是mem指针类型,这正好符合mem结构体的布局,mem结构体的第一个字段就是一个mem指针类型,所以咱们能够将&F理解成F的前一个元素,由于&F的第一个字段是F

 

 

20、伙伴系统和slab

http://blog.csdn.net/vanbreaker/article/details/7605367

1、伙伴关系主要用来解决外部碎片问题。

伙伴系统的宗旨就是用最小的内存块来知足内核的对于内存的请求。在最初,只有一个块,也就是整个内存,假如为1M大小,而容许的最小块为64K,那么当咱们申请一块200K大小的内存时,就要先将1M的块分裂成两等分,各为512K,这两分之间的关系就称为伙伴,而后再将第一个512K的内存块分裂成两等分,各位256K,将第一个256K的内存块分配给内存,这样就是一个分配的过程。

2slab用来解决内部碎片问题,通常直接工做在伙伴系统上。

http://oss.org.cn/kernel-book/ch06/6.3.3.htm

slabLinux操做系统的一种内存分配机制。其工做是针对一些常常分配并释放的对象,如进程描述符等,这些对象的大小通常比较小,若是直接采用伙伴系统来进行分配和释放,不只会形成大量的内碎片,并且处理速度也太慢。而slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其从新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片。slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当之后又要请求新的对象时,就能够从内存直接获取而不用重复初始化。

2一、内存池、线程池

内存池

http://blog.csdn.net/w_miracle/article/details/12321819

http://www.cppblog.com/weiym/archive/2012/05/05/173785.html

http://www.cnblogs.com/bangerlee/archive/2011/08/31/2161421.html

http://2309998.blog.51cto.com/2299998/1263164

在软件开发中,有些对象使用很是频繁,那么咱们能够预先在堆中实例化一些对象,咱们把维护这些对象的结构叫“内存池”。在须要用的时候,直接从内存池中拿,而不用重新实例化,在要销毁的时候,不是直接free/delete,而是返还给内存池。

把那些经常使用的对象存在内存池中,就不用频繁的分配/回收内存,能够相对减小内存碎片,更重要的是实例化这样的对象更快,回收也更快。当内存池中的对象不够用的时候就扩容。

 

C/C++下内存管理是让几乎每个程序员头疼的问题,分配足够的内存、追踪内存的分配、在不须要的时候释放内存——这个任务至关复杂。而直接使用系统调用malloc/freenew/delete进行内存分配和释放,有如下弊端:

1、调用malloc/new,系统须要根据“最早匹配”、“最优匹配”或其余算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能须要合并空闲内存块,这些会产生额外开销

2、频繁使用时会产生大量内存碎片,从而下降程序运行效率

3、容易形成内存泄漏

 

内存池(memory pool)是代替直接调用malloc/freenew/delete进行内存管理的经常使用方法,当咱们申请内存空间时,首先到咱们的内存池中查找合适的内存块,而不是直接向操做系统申请,优点在于:

1、比malloc/free进行内存申请/释放的方式快

2、不会产生或不多产生堆碎片

3、可避免内存泄漏

线程池

http://blog.csdn.net/liu1pan2min3/article/details/8545979

所以线程池的出现正是着眼于减小线程池自己带来的开销。线程池采用预建立的技术,在应用程序启动以后,将当即建立必定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动建立必定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。

基于这种预建立技术,线程池将线程建立和销毁自己所带来的开销分摊到了各个具体的任务上,执行次数越多,每一个任务所分担到的线程自己开销则越小,不过咱们另外可能须要考虑进去线程之间同步所带来的开销。

 

构建线程池框架 

 

通常线程池都必须具有下面几个组成部分: 

1、线程池管理器:用于建立并管理线程池 

2、工做线程线程池中实际执行的线程 

3、任务接口尽管线程池大多数状况下是用来支持网络服务器,可是咱们将线程执行的任务抽象出来,造成任务接口,从而是的线程池与具体的任务无关。 

4、任务队列:线程池的概念具体到实现则多是队列,链表之类的数据结构,其中保存执行线程

2二、函数堆栈

http://www.360doc.com/content/12/1008/19/1317564_240288359.shtml

任何一个程序一般都包括代码段和数据段,这些代码和数据自己都是静态的。程序要想运行,首先要由操做系统负责为其建立进程,并在进程的虚拟地址空间中为其代码段和数据段创建映射。光有代码段和数据段是不够的,进程在运行过程当中还要有其动态环境,其中最重要的就是堆栈。图3所示为Linux下进程的地址空间布局:

 

3 Linux下进程地址空间的布局

 

 

首先,execve(2)会负责为进程代码段和数据段创建映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将bss段清零,这就是为何未赋初值的全局变量以及static变量其初值为零的缘由。进程用户空间的最高位置是用来存放程序运行时的命令行参数及环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空洞,而做为进程动态运行环境的堆栈和堆就栖身其中,其中堆栈向下伸展,堆向上伸展。

知道了堆栈在进程地址空间中的位置,咱们再来看一看堆栈中都存放了什么。相信读者对C语言中的函数这样的概念都已经很熟悉了,实际上堆栈中存放的就是与每一个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。典型的堆栈帧结构如图4所示。

堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个堆栈帧一般都有两个指针,其中一个称为堆栈帧指针,另外一个称为栈顶指针。前者所指向的位置是固定的,然后者所指向的位置在函数的运行过程当中可变。所以,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移。对照图4可知,实参的偏移为正,局部变量的偏移为负。

 

典型的堆栈帧结构

 

http://blog.csdn.net/zsy2020314/article/details/9429707

http://www.cnblogs.com/bangerlee/archive/2012/05/22/2508772.html

相关文章
相关标签/搜索