注册 | 登陆

Future PHP

我要走我应该走的路......
浏览模式: 标准 | 列表全部文章

[置顶] 尚学堂j2ee

这篇日志被加密了,请输入密码后查看。

位运算的应用

位运算与位运算结合能实现许多与位串运算有关的复杂计算。所有复杂的加密运算都是建立在位运算之上的。下面列举一些常用的位运算应用。

(1)不用临时变量交换两个整数。

void swap(int i , int j)
{
i = i^j;
j =j ^i;
i =i^ j;
}

(2)对一个16位数高8位清零,低8位保留。

main()
{
int x=6550;
x=x&255
printf("%d ",x);
}

(3)取出一个16位数的高8位。

main()
{
int x=6550;
x=x&(65535-255);
x=x>>8
printf("%d ",x);
}

上面举了3个和位运算有关的算法,用位运算实现了简单的功能,而复杂的位运算功能就是建立在这些简单的位运算功能基础上的。

realloc 用法

最近在写source code时需要在数组的buffer小时重新申请一块buffer,故找了一些资料,乖乖,竟然原指针还可以“漂移”。。。。。。

realloc
原型:extern void *realloc(void *mem_address, unsigned int newsize);
用法:#include <stdlib.h> 有些编译器需要#include <alloc.h>
功能:改变mem_address所指内存区域的大小为newsize长度。
说明:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
当内存不再使用时,应使用free()函数将内存块释放。
注意:这里原始内存中的数据还是保持不变的。
举例:
// realloc.c
#include <syslib.h>
#include <alloc.h>
main()
{
char *p;
clrscr(); // clear screen
p=(char *)malloc(100);
if(p)
printf("Memory Allocated at: %x",p);
else
printf("Not Enough Memory!\n");
getchar();
p=(char *)realloc(p,256);
if(p)
printf("Memory Reallocated at: %x",p);
else
printf("Not Enough Memory!\n");
free(p);
getchar();
return 0;
}
详细说明及注意要点:
1、如果有足够空间用于扩大mem_address指向的内存块,则分配额外内存,并返回mem_address
这 里说的是“扩大”,我们知道,realloc是从堆上分配内存的,当扩大一块内存空间时, realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平。也就是说,如果原先的内存大小后面还有足够的 空闲空间用来分配,加上原来的空间大小= newsize。那么就ok。得到的是一块连续的内存。
2、如果原先的内存大小后面没有足够的空闲空间用来分配,那么从堆中另外找一块newsize大小的内存。
并把原来大小内存空间中的内容复制到newsize中。返回新的mem_address指针。(数据被移动了)。
老块被放回堆上。
例如:
#include <malloc.h>
char *p,*q;
p = (char * ) malloc (10);
q=p;
p = (char * ) realloc (p,20);
…………………………
这 段程序也许在编译器中没有办法通过,因为编译器可能会为我们消除一些隐患!在这里我们只是增加了一个记录原来内存地址的指针q,然后记录了原来的内存地址 p,如果不幸的话,数据发生了移动,那么所记录的原来的内存地址q所指向的内存空间实际上已经放回到堆上了!这样一来,我们应该终于意识到问题的所在和可 怕了吧!
3、返回情况
返回的是一个void类型的指针,调用成功。(这就再你需要的时候进行强制类型转换)
返回NULL,当需要扩展的大小(第二个参数)为0并且第一个参数不为NULL,此时原内存变成了“freed(游离)”的了。
返回NULL,当没有足够的空间可供扩展的时候,此时,原内存空间的大小维持不变。
4、特殊情况
如果mem_address为null,则realloc()和malloc()类似。分配一个newsize的内存块,返回一个指向该内存块的指针。
如果newsize大小为0,那么释放mem_address指向的内存,并返回null。
如果没有足够可用的内存用来完成重新分配(扩大原来的内存块或者分配新的内存块),则返回null.而原来的内存块保持不变。

==============================================================

 

void* malloc(unsigned size); void* calloc(size_t nelem, size_t elsize); 和void* realloc(void* ptr, unsigned newsize);都在stdlib.h函数库内,是C语言的标准内存分配函数。
1. 函数malloc()和calloc()都可以用来动态分配内存空间。malloc()函数有
一个参数,即要分配的内存空间的大小,
malloc 在分配内存时会保留一定的空间用来记录分配情况,分配的次数越多,这些记录占用的空间就越多。另外,根据 malloc 实现策略的不同,malloc 每次在分配的时候,可能分配的空间比实际要求的多些,多次分配会导致更多的这种浪费。当然,这些都和 malloc 的实现有关;calloc()函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小。如果调用成功,它们都将返回所分配内存空间的首地址。
2. 函数malloc()和函数calloc()的主要区别是前者不能初始化所分配的内存
空间,而后者能。
3. realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或
是缩小,原有内存的中内容将保持不变。当然,对于缩小,则被缩小的那一
部分的内容会丢失。
4. realloc 并不保证调整后的内存空间和原来的内存空间保持同一内存地址
。相反,realloc 返回的指针很可能指向一个新的地址。所以在代码中,我
们必须将realloc返回的值,重新赋值给 p :
p = (int *) realloc (p, sizeof(int) *15);

 

==================================================================

真正认识 realloc 的工作方式。

Posted on 2008-11-20 13:12 啊夏 阅读(142) 评论(0)  编辑 收藏 网摘 所属分类: c/c++

realloc 用过很多次了。无非就是将已经存在的一块内存扩大。

char* p = malloc(1024);
char* q = realloc(p,2048);

现在的问题是我们应该如何处理指针 p。 刚开始按照我最直观的理解,如果就是直接将 p = NULL;。 到最后只需要释放 q的空间就可以了。

因为最近在做个封装。结果在做单元测试的时候发现。有时候我在 free(q); 的时候会出错。这样我就郁闷了。

后来仔细一跟踪,发现 realloc 完以后 q 和 p 的指针地址是一样。不过有时候又不一样。

仔细查了下资料。得到如下信息:

       1.如果 当前连续内存块足够 realloc 的话,只是将p所指向的空间扩大,并返回p的指针地址。 这个时候 q 和 p 指向的地址是一样的。

       2.如果 当前连续内存块不够长度,再找一个足够长的地方,分配一块新的内存,q,并将 p指向的内容 copy到 q,返回 q。并将p所指向的内存空间删除。

这样也就是说 realloc 有时候会产生一个新的内存地址 有的时候不会。所以在分配完成后。我们需要判断下 p 是否等于 q。并做相应的处理。

这里有点要注意的是要避免 p = realloc(p,2048); 这种写法。有可能会造成 realloc 分配失败后,p原先所指向的内存地址丢失。

 

=========================================

关于realloc函数说明的补充:
函数定义:
void *realloc(void *ptr, size_t size);
上面的分析基本没有问题,但有两点要注意:
1.返回值可能与ptr的值不同,如果是不同的话,那么realloc函数完成后,ptr指向的旧内存已被free掉了。
2。如果返回NULL值,则分配不成功,而原来的ptr指向的内存还没有被free掉,要求程序显式free.

故p = (int *) realloc (p, sizeof(int) *15);语句有这么一个问题,
调用前p指向一个已分配成功的内存,而调用realloc时却失败(即返回NULL),此时,p原来指向的内存还没有free掉,而现在又找不到地址,这样就出现memory leak了。

关于这一点的确要注意,最好如下:
int *q
q = (int *) realloc (p, sizeof(int) *15);

if(!q) p =q;

 

======================================================

 

首先看一下下面的C程序片断:

 

#include <malloc.h>

char  *p;

p = (char * ) malloc (10);

p = (char * ) realloc (p,20);

…………………………

 

    这段程序的意思很简单,只有稍有点C基础的人都可以看懂。函数首先定义了一个字符型的指针p,然后为指针p分配了一个10个字节大小的内存空间,接着将这个内存块的大小增加到20个字节。

 

    这里有什么问题吗?上机运行一下,好像没有问题!

 

    是的,这样上机运行是没有问题的,但是这里存在着也许我们不太注意的隐患!隐患在那里?这就是我在本文中要详细说明的realloc()函数了。

 

    再看一下下面一段来自MSDN的话:

realloc returns a void pointer to the reallocated (and possibly moved) memory block. The return value is NULL if the size is zero and the buffer argument is not NULL, or if there is not enough available memory to expand the block to the given size. In the first case, the original block is freed. In the second, the original block is unchanged. The return value points to a storage space that is guaranteed to be suitably aligned for storage of any type of object. To get a pointer to a type other than void, use a type cast on the return value.

这段E文还不算是晦涩难懂,所以我就不翻译了,大致的意思是说关于realloc返回值的。但是这里对他的返回值分了几种情况:

1、  返回void * 指针,调用成功。

2、  返回NULL,当需要扩展的大小(第二个参数)为0并且第一个参数不为NULL,此时原内存变成了“freed(游离)”的了。

3、  返回NULL,当没有足够的空间可供扩展的时候,此时,原内存空间的大小维持不变。

 

第一种情况告诉了我们在得到需要的内存空间后需要做类型转换的工作;

第二种情况可能只有傻瓜才会去使用吧!

第三种情况,内存空间不够的时候就会维持未来的大小不变。

 

        MSDN上面说内存空间不够的时候就不会扩展原来的内存空间的大小,这话固然没有错,但是有点含糊,似乎遗漏了一种情况!我们知道,realloc是从堆上分配内存的,当扩大一块内存空间时, realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;可如果数据后面的字节不够的话,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上。这句话传递的一个重要的信息就是数据可能被移动!看到这里,也许我们已经发现一开始我给出的程序的问题了。为了更清楚地说明问题,可以将上面的程序改成下面的形式:

 

#include <malloc.h>

char  *p*q;

p = (char * ) malloc (10);

q=p;

p = (char * ) realloc (p,20);

…………………………

 

    这段程序也许在编译器中没有办法通过,因为编译器可能会为我们消除一些隐患!在这里我们只是增加了一个记录原来内存地址的指针q,然后记录了原来的内存地址p,如果不幸的话,数据发生了移动,那么所记录的原来的内存地址q所指向的内存空间实际上已经放回到堆上了!这样一来,我们应该终于意识到问题的所在和可怕了吧!

 

    这个问题似乎有点牛角尖的味道,因为我们也许从来不曾遇上过,但是我们应该明白这样的事情的始终存在,只有这样,在万一我们碰上的时候才会去有意识的去避免这种隐患,否则,一旦这样的隐患一旦发作,程序崩溃不说,恐怕查错也不是一件容易的事!

C语言面向对象编程

经常看到关于OO编程的讨论,C++, Java, C#...还有最近很流行的动态语言Python,Ruby等,但很少看到有C的份。在我看来,OO编程的核心是OO的思想,用什么语言倒是其次。但是,不可否认,那些专门为OO编程设计的语言可以比较方便和自然地表达OO思想,有些语言甚至强制使用OO特性。

C,作为最贴近底层的高级语言,拥有简洁的语法和直接内存操作能力(指针),大量运用于系统级编程,如操作系统内核,驱动程序等。而在嵌入式系统中,由于资源有限等因素,更倾向于用C编程。

C虽然在语言特性上并没有体现OO特性,但是依然可以通过各种编程技巧来体现OO的思想。由于C的高度自由的特点,在OO编程方面还能体现有别于其他语言的特殊韵味。

C的面向对象概念 Top

OO Programing in C is not only POSSIBLE but also PRACTICAL.
--------------------------------------------------------------------------------

OO思想在Unix世界中很早就有:UNIX把设备抽象成文件,这样就可以用一套相同的方法(open, read, write, close, ... )去访问不同的设备和文件——尽管设备之间的差异很大。用OO的观点来看,这些“设备”对象都实现了"文件操作接口",可以想象有一个叫"文件"的基类,定义了"文件操作接口",“设备”对象继承了“文件”对象....。在实现角度看,在内核里面,设备驱动提供了自己的read, write等实现,并用它们去填充文件操作结构体里面的函数指针....这和C++里面的虚函数运行时绑定的道理是一样的。( C++虚函数是其实是运行时静态绑定,而文件操作接口可以运行时动态绑定 :-)

Linux内核中则处处体现了OO的思想。2.6内核的Device Driver Modal是一套层次分明又错综复杂的机制,其中体现了许多OO设计理念。虽然可能设备驱动程序开发者觉察不到,但所有的设备驱动对象内部都隐藏了一个叫 KObject的对象。内核把这些KObjects互相联系在一起,并通过KObject的相互关系构造了/sys文件系统。/sys就是内核中各种设备对象的映射图,如果把/sys全部展开,我们可以清楚地看到各种对象的关系。

实践证明,C也可以很好地用于OO编程,而且可以用于构造很复杂的系统,而且C在表达OO思想的时候并不会显得蹩脚,而是可以很简单,很自然。

用struct来仿真class Top

OO Programing in C is not only POSSIBLE, but also PRACTICAL.
--------------------------------------------------------------------------------

“class“是很多OO编程语言里的关键字,它来源于OO鼻祖Smalltalk。class(类),是对一群有相同特性的对象的抽象概括,对象称为类的实例。在class里面可以存放有状态(变量),行为(函数/方法)....有关OO概念、方法的文章太多了,不再啰嗦。在C里面,唯一可以实现自定义类型的是struct,struct是C的OO编程最重要的工具。


一个最常见的技巧,就是用struct来"仿真"class: 在struct里面放入变量,函数指针,嵌入其他struct等。

以下例子摘自我最近刚开发完成的一个USB Firmware项目:

C代码 复制代码
  1. struct usb_device;   
  2. struct usb_ctl;   
  3.   
  4. struct usb_iobuf {   
  5.   int len;              /* data length in the buffer */  
  6.     unsigned char buf[USBEPFIFO_SIZE];  /* data buffer itself */  
  7. };   
  8.   
  9. struct usb_endpoint {   int type;       /* endpoint type: BULKIN, BULKOUT, CTL, ISO ... */  
  10.     int qlen;       /* queue length */  
  11.   
  12.     xQueueHandle lock;  /* semaphore lock */  
  13.     xQueueHandle q;     /* data queue (pointer of bulk_buf) */  
  14.   
  15.     int idx;        /* endpoint index */  
  16.     int epx;        /* endpoint mark bit */  
  17.     int cfg;        /* endpoint configure */  
  18.     int bank;       /* current operation bank (for ping-pong mode) */  
  19.     int txCount;        /* used for ping-pong mode */   /* endpoint data process function */        void (*ep_process) (struct usb_device *dev,          struct usb_endpoint *ep,            xISRStatus *pxMessage);   
  20. };   
  21.   
  22. struct usb_descriptor {   
  23.     int type;       /* descriptor type: device, conf, string or endpoint */  
  24.     int idx;        /* descriptor index (for string descriptor) */  
  25.     int size;       /* descriptor size */  
  26.     void * data;        /* descriptor data */  
  27.     struct list_head list;  /* link list of descriptors */  
  28. };   
  29.   
  30. struct usb_deviceOps {   
  31.     int (*init)(struct usb_device *dev);        /* called when framework init usb device, add device descriptors, init private data ... etc. */  
  32.     int (*reset)(struct usb_device *dev);       /* called when reseted by host */  
  33.     int (*switch_in)(struct usb_device *dev);   /* called when switch in */  
  34.     int (*switch_out)(struct usb_device *dev);  /* called when swithc out */    /* called when HOST request class interface data */  
  35.     void (*class_interface_req)(struct usb_device *dev, xUSB_REQUEST *pxRequest);   /* called when HOST complete the data sending stage */      int (*ctl_data_comp)(struct usb_device *dev,                     xCONTROL_MESSAGE *pxMessage);   
  36. };   
  37.   
  38. struct usb_ctlOps {   
  39.     void (*ctl_transmit_null)(struct usb_ctl *ctl);   
  40.     void (*ctl_send_stall)(struct usb_ctl *ctl);   
  41.     void (*ctl_reset_ep0)(struct usb_ctl *ctl);   
  42.     void (*ctl_detach_usb)(struct usb_ctl *ctl);   
  43.     void (*ctl_attach_usb)(struct usb_ctl *ctl);   
  44.     void (*ctl_send_data)(struct usb_ctl *ctl, unsigned char *data,   
  45.             int req_len,   
  46.             int send_len,   
  47.             int is_des);   
  48. };   
  49.   
  50.   
  51. struct usb_ctl {   
  52.     int addr;           /* address alloced by host */  
  53.     int conf;           /* configuration set by host */  
  54.     eDRIVER_STATE state;        /* current status */  
  55.     xCONTROL_MESSAGE tx;        /* control transmit message */  
  56.     xCONTROL_MESSAGE rx;        /* control receive message */  
  57.     struct ubufm *bufmn;        /* 'usb_iobuf' buffer manager, shared by all usb devices */  
  58.     int prio;           /* the main task priority */  
  59.     xTaskHandle task_handle;    /* the main task handler */  
  60.     struct usb_ctlOps *ctlOps;  /* control endpoint operations */  
  61. };   
  62.   
  63. struct usb_device {   
  64.     char name[16];          /* device name, e.g. "usbser" */  
  65.     struct usb_deviceOps *ops;  /* usb device callback functions */  
  66.   
  67.     struct usb_ctl *ctl;        /* usb control enpoint, provided by framework */  
  68.     struct list_head desc_list; /* usb descriptors */  
  69.     struct usb_endpoint *ep[MAX_ENDPOINTS]; /* endpoints */  
  70.     int active;         /* whether the device is active */  
  71.     xQueueHandle ready;     /* notify this queue when usb device ready */  
  72.     void *private;          /* device private data */  
  73.     struct list_head list;      /* link list of usb device */  
  74. };  

在这个例子,我用struct分别描述了USB设备,USB控制通道,USB端点,USB描述符和USB缓冲区对象。USB设备对象包含了若干个USB端点,一个USB控制通道指针,一个USB描述符表的表头(指向若干个USB描述符),和一个USB缓冲区管理对象。而且,USB设备对象还包含了name属性,一个由USB Framework调用的回调函数集,还有一个用于连接其他USB设备的链表节点。

值得一提的是,USB设备对象中有一个void *private 成员,可以指向任何数据。实际上在我的程序里,我实现了usb-serial和usb-mass-storage两个USB设备,对于usb-serial对象,private我弃之不用,而在usb-mass-storage对象中,private指向一个Mass storage对象,usb-mass-storage正是通过这个Mass storage对象访问外部大容量存储的(在我的程序里,Mass storage对象和一个MMC Card对象绑定,外部存储是SD/MMC卡)。由于对于每一种设备的具体实现来说,它知道private指向的是何种类型的设备,因此不会引起混乱。而外部程序根据需要在初始化USB设备对象前赋予private有意义的值——运行时动态绑定。

这一系列struct基本上如实地反映了USB DEVICE硬件逻辑和规范要求: "一个USB设备包含若干个端点,其中有一个固定的控制端点(端点0)。在枚举阶段USB设备要根据HOST的请求应答相应的描述符..."

现在回到OO的话题,这个例子中体现了"组合类":USB设备对象包含了USB端点对象,USB描述符对象...。还有动态绑定 (private成员)。从严格的OO意义上来看,好像有点"怪",不过我认为这恰恰是C的特点——简洁,直接。不信你用C++表达试试?也许会更漂亮,很OO,但是不一定会如此清爽!

P.S. : 熟悉USB Firmware开发的人可能对struct usb_endpoint中的epx,cfg,bank和txCount四个成员有异议,因为这些成员是和特定的硬件相关,并不是所有的USB硬件都支持ping-pong mode,所以bank和txCount不一定用得上,epx, cfg也可能因硬件的不同而不同。没错!更理想的设计是把与硬件相关的部分分离出来,用void *private指向各自的与硬件相关的配置——就像struct usb_device所采用方法,所以更好的版本应该是:

Java代码 复制代码
  1. struct usb_endpoint {   
  2.   int type;     /* endpoint type: BULKIN, BULKOUT, CTL, ISO ... */  
  3.   int qlen;     /* queue length */  
  4.   xQueueHandle lock;    /* semaphore lock */  
  5.   xQueueHandle q;   /* data queue (pointer of bulk_buf) */  
  6.   int idx;      /* endpoint index */  
  7.      
  8.   /* endpoint data process function */  
  9.   void (*ep_process)(struct usb_device *dev,    struct usb_endpoint *ep, xISRStatus *pxMessage);   
  10.   void *private;    /* endpoint private data (hardware relevant) */  
  11. };  


tips: 用C表达的一个关键处就是要很好地应用struct来描述模型。

实现OO的继承机制 Top

OO Programing in C is not only POSSIBLE but also PRACTICAL
--------------------------------------------------------------------------------

OO的一个亮点是类的"继承",通过"继承",可以重用许多代码。而且"继承"也是现实生活中非常自然的一种关系。但是很不幸,C没有class,更没有提供"继承"的表达方式。既然能用C的struct来仿真class, 那能不能继续来仿真"继承"呢?答案是:possible。就像<<Inside the C++ Object Modal>>书中所叙述的那样——你可以用C来达到所有C++能做到的事。但这种仿真显然毫无实际应用价值。

"继承"是一种表达方式,代码重用才是目的。

为了重用代码,C++可以用"继承"的方式来巧妙的达到目的,但是也必须付出代价:你必须非常仔细地设计你的类族谱,要有前瞻性,要有可扩展性,要决定分多少个层次....这些都不是容易做到的事。

C别无选择,模块化设计,函数,宏....只能通过巧妙的设计才能达到代码可重用的目的。还是举个例子来说明C是如何做到"殊途同归"的吧。

"链表"是一个非常常用的数据结构,常用于管理无序的数据(对象)集合。链表操作,特别是双向链表操作很容易出错。重用一套通用操作链表的代码可以为我们省不少事。在C++中,我们可以用经典的STL中的list类。为了适应各种数据类型,list类用模板来实现。list类实现的很巧妙,功能很强,但是,不得不说,很少人用。其实不仅list类很少用,STL都很少人用。(希望这是我的一家之言,反正我所熟悉的C++程序员都不怎么用STL :-)当然在C++中你还有另外一个选择:实现一个List基类完成链表操作,要放入链表的类从List类继承而来,就拥有了一套操作list的方法。

Linux内核中用C提供了一套非常巧妙的方法操作链表,位于.../linux/include/linux/list.h,只用一些宏和inline函数来实现双向链表。摘抄一部分出来:

C代码 复制代码
  1. ....   
  2. struct list_head {   
  3.     struct list_head *next, *prev;   
  4. };   
  5.   
  6.   
  7. #define LIST_HEAD_INIT(name) { &(name), &(name) }   
  8.   
  9. #define LIST_HEAD(name) \   
  10.     struct list_head name = LIST_HEAD_INIT(name)   
  11.   
  12. #define INIT_LIST_HEAD(ptr) do { \   
  13.     (ptr)->next = (ptr); (ptr)->prev = (ptr); \   
  14. while (0)   
  15.   
  16. /*  
  17.  * Insert a new entry between two known consecutive entries.  
  18.  *  
  19.  * This is only for internal list manipulation where we know  
  20.  * the prev/next entries already!  
  21.  */  
  22. static inline void __list_add(struct list_head *new,   
  23.                   struct list_head *prev,   
  24.                   struct list_head *next)   
  25. {   
  26.     next->prev = new;   
  27.     new->next = next;   
  28.     new->prev = prev;   
  29.     prev->next = new;   
  30. }   
  31.   
  32. /**  
  33.  * list_add - add a new entry  
  34.  * @new: new entry to be added  
  35.  * @head: list head to add it after  
  36.  *  
  37.  * Insert a new entry after the specified head.  
  38.  * This is good for implementing stacks.  
  39.  */  
  40. static inline void list_add(struct list_head *newstruct list_head *head)   
  41. {   
  42.     __list_add(new, head, head->next);   
  43. }   
  44.   
  45. .....   
  46.   
  47. /**  
  48.  * list_entry - get the struct for this entry  
  49.  * @ptr:    the &struct list_head pointer.  
  50.  * @type:    the type of the struct this is embedded in.  
  51.  * @member:    the name of the list_struct within the struct.  
  52.  */  
  53. #define list_entry(ptr, type, member) \   
  54.     container_of(ptr, type, member)   
  55.   
  56. /**  
  57.  * list_for_each    -    iterate over a list  
  58.  * @pos:    the &struct list_head to use as a loop counter.  
  59.  * @head:    the head for your list.  
  60.  */  
  61. #define list_for_each(pos, head) \   
  62.     for (pos = (head)->next; prefetch(pos->next), pos != (head); \   
  63.             pos = pos->next)   
  64.   
  65. ......  


其中 container_of 宏如下:

C代码 复制代码
  1. /**  
  2.  * container_of - cast a member of a structure out to the containing structure  
  3.  * @ptr:    the pointer to the member.  
  4.  * @type:    the type of the container struct this is embedded in.  
  5.  * @member:    the name of the member within the struct.  
  6.  *  
  7.  */  
  8. #define container_of(ptr, type, member) ({            \   
  9.         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \   
  10.         (type *)( (char *)__mptr - offsetof(type,member) );})  


这里使用了GCC特有的 "typeof" 关键字,如果想用其他编译器也想编译通过的话,可以修改成:

C代码 复制代码
  1. #define container_of(ptr, type, member) (            \   
  2.         (type *)( (char *)ptr - offsetof(type,member) ) )  

为了便于说明,prefetch定义成:
Java代码 复制代码
  1. static inline void prefetch(const void *x) {;}  


offsetof的一个简单版本:
C代码 复制代码
  1. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  

好了,让我们看看怎么用:
Java代码 复制代码
  1. struct my_data {   
  2.     int x;   
  3.     int y;   
  4.     struct list_head list;   
  5. }   
  6.   
  7. /* 链表头 */  
  8. LIST_HEAD(my_listhead);   
  9.   
  10. void my_function()   
  11. {   
  12.     ...   
  13.     /* 节点对象 */  
  14.     struct my_data *node_1 = (struct my_data *) malloc(sizeof(struct my_data));   
  15.     struct my_data *node_2 = (struct my_data *) malloc(sizeof(struct my_data));   
  16.     ...   
  17.     /* 加入链表 */  
  18.     list_add (node_1->list, &my_listhead);   
  19.     list_add (node_2->list, &my_listhead);   
  20.     ...   
  21.     /* 遍历链表 */  
  22.     struct my_data * node;   
  23.     struct list_head *pos;   
  24.     list_for_each (pos, &my_listhead) {   
  25.        node = list_entry (pos, struct my_data, list);   
  26.        ...   
  27.     }  


其中最精彩的部分是遍历链表的表达方式:
    list_for_each (...) {
       ...
    }
这种表达方式另我想起了Ruby,C++ STL中的到处出现的iterator,和VB中的for each...in...next语句。

从内部结构角度来看,Linux的list实现方式有点类似C++中的"组合类"——在需要放入链表的对象内部放入list类(struct list_head)。但是从遍历链表的时候,可以根据list指针得到包含list节点的对象指针来看,又有点超出了"组合类"的范畴。能否把 struct my_data看成继承了struct list_head呢?从内存映像来看倒有点像(C++子类对象的内存映像是父类对象的超级)。当然,这种强行联系完全没有必要,C就是C,何必去往C+ +套呢?C自有C的表达方式

编程思想:C语言中的面向对象

C语言的多态实现
  相信很多人都看过设计模式方面的书,大家有什么体会呢?Bridge,Proxy,Factory这些设计模式都是基于抽象类的。使用抽象对象是这里的一个核心。
  
  其实我觉得框架化编程的一个核心问题是抽象,用抽象的对象构建程序的主体框架,这是面向对象编程的普遍思想。用抽象构建骨架,再加上多态就形成了一个完整的程序。由于C++语言本身实现了继承和多态,使用这样的编程理念(理念啥意思?跟个风,嘿嘿)在C++中是十分普遍的现象,可以说Virtual(多态)是VC的灵魂。
  
  但是,使用C语言的我们都快把这个多态忘光光了。我常听见前辈说,类?多态?我们用的是C,把这些忘了吧。很不幸的是,我是一个固执的人。这么好的东西,为啥不用呢。很高兴的,在最近的一些纯C代码中,我看见了C中的多态!下面且听我慢慢道来。
  
  1. VC中的Interface是什么
  Interface:中文解释是接口,其实它表示的是一个纯虚类。不过我所要说的是,在VC中的Interface其实就是struct,查找Interface的定义,你可以发现有这样的宏定义:
  #Ifndef Interface
  #define Interface struct
  #endif
  而且,实际上在VC中,如果一个类有Virtual的函数,则类里面会有vtable,它实际上是一个虚函数列表。实际上C++是从C发展而来的,它不过是在语言级别上支持了很多新功能,在C语言中,我们也可以使用这样的功能,前提是我们不得不自己实现。
  
  2.C中如何实现纯虚类(我称它为纯虚结构)
  比较前面,相信大家已经豁然开朗了。使用struct组合函数指针就可以实现纯虚类。
  例子: typedef struct {
  void (*Foo1)();
  char (*Foo2)();
  char* (*Foo3)(char* st);
  }MyVirtualInterface;
  
  这样假设我们在主体框架中要使用桥模式。(我们的主类是DoMyAct,接口具体实现类是Act1,Act2)下面我将依次介绍这些“类”。(C中的“类”在前面有说明,这里换了一个,是使用早期的数组的办法)
  
  主类DoMyAct: 主类中含有MyVirtualInterface* m_pInterface; 主类有下函数:
  DoMyAct_SetInterface(MyVirtualInterface* pInterface)
  {
  m_pInterface= pInterface;
  }
  DoMyAct_Do()
  {
  if(m_pInterface==NULL) return;
  m_pInterface->Foo1();
  c=m_pInterface->Foo2();
  }
  子类Act1:实现虚结构,含有MyVirtualInterface st[MAX]; 有以下函数:
  MyVirtualInterface* Act1_CreatInterface()
  {
  index=FindValid() //对象池或者使用Malloc !应该留在外面申请,实例化
  if(index==-1) return NULL;
  St[index].Foo1=Act1_Foo1; // Act1_Foo1要在下面具体实现
  St[index].Foo2=Act1_Foo2;
  St[index].Foo3=Act1_Foo3;
  Return &st [index];
  }
  子类Act2同上。
在main中,假设有一个对象List。List中存贮的是MyVirtualInterface指针,则有:
  if( (p= Act1_CreatInterface()) != NULL)
  List_AddObject(&List, p); //Add All
  
  While(p=List_GetObject()){
  DoMyAct_SetInterface(p);//使用Interface代替了原来大篇幅的Switch Case
  DoMyAct_Do();//不要理会具体的什么样的动作,just do it
  }
  
  FREE ALL。
  在微系统里面,比如嵌入式,通常使用对象池的技术,这个时候可以不用考虑释放的问题(对象池预先没有空间,使用Attach,在某个函数中申请一个数组并临时为对象池分配空间,这样函数结束,对象池就释放了)
  
  但是在Pc环境下,由于程序规模比较大,更重要的是一些特殊的要求,使得对象的生命周期必须延续到申请的那个函数体以外,就不得不使用malloc,实际上即使在C++中,new对象的自动释放始终是一个令人头疼的问题,新的标准引入了智能指针。但是就我个人而言,我觉得将内存释放的问题完全的交给机器是不可信任的,它只能达到准最佳。
  
  你知道设计Java的垃圾回收算法有多困难吗?现实世界是错综复杂的,在没有先验条件下,要想得到精确的结果及其困难。所以我说程序员要时刻将free记在心上,有关程序的健壮性和自我防御将在另外一篇文章中讲述。
  
  3.纯虚结构的退化
  下面我们来看看如果struct里面仅仅有一个函数是什么? 这个时候如果我们不使用struct,仅仅使用函数指针又是什么? 我们发现,这样就退化为普通的函数指针的使用了。
  
  所以说,有的时候我觉得面向对象仅仅是一种形式,而不是一种技术。是一种观点,而不是一种算法。但是,正如炭,石墨和钻石的关系一样,虽然分子式都是C,但是组成方法不一样,表现就完全不一样了!
  有的时候,我们经常被编程中琐碎的事情所烦恼,而偏离了重心,其实程序可进化的特性是很重要的。有可能,第一次是不成功的,但是只要可进化,就可以发展。
  
  4.进阶――类结构树,父类不是纯虚类的类
  前面仅仅讲的是父类是纯虚结构的情况 (面向对象建议的是所有类的基类都是从纯虚类开始的), 那么当类层次比较多的情况下,出现父类不是纯虚结构怎么办呢。嘿嘿,其实在C中的实现比C++要简单多了。因为C中各个函数是分散的。
  
  在这里使用宏定义是一个很好的办法:比如两个类Act1,ActByOther1“继承”Act1:
  MyVirtualInterface* ActByOther1_CreatInterface()
  {
  index=FindValid() //对象池或者使用Malloc
  if(index==-1) return NULL;
  St[index].Foo1= ActByOther1_Foo1; // Act1_Foo1要在下面具体实现
  St[index].Foo2= ActByOther1_Foo2;
  St[index].Foo3= ActByOther1_Foo3;
  Return &st [index];
  }
  
  #define ActByOther1_Foo1 Act1_Foo1 //这就是继承 嘿嘿
  ActByOther1_Foo2(){} // 可以修改其实现
  ActByOther1_DoByOther() {} //当然就可以添加新的实现咯

Linux C编程

Linux C编程

附件: linux_c.chm (4.51 M, 下载次数:4)

C语言内存管理内幕(一)

为什么必须管理内存

内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。 

追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。 

不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求: 

  • 确定您是否有足够的内存来处理数据。 
  • 从可用的内存中获取一部分内存。 
  • 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。 

 

实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。 

C 风格的内存分配程序

C 编程语言提供了两个函数来满足我们的三个需求: 

  • malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。 
  • free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。 

 

物理内存和虚拟内存

要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存。 

只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 

在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。) 

基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用: 

  • brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。 
  • mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。 

 

如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。 

实现一个简单的分配程序

如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。 

要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。 

在大部分操作系统中,内存分配由以下两个简单的函数来处理: 

  • void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。 
  • void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。 

malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量: 


清单 1. 我们的简单分配程序的全局变量



        
int has_initialized = 0;

void *managed_memory_start;

void *last_valid_address;

      

 

如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量: 


清单 2. 分配程序初始化函数



        
/* Include the sbrk function */

#include <unistd.h>

void malloc_init()

{

 /* grab the last valid address from the OS */

 last_valid_address = sbrk(0);

 /* we don't have any memory to manage yet, so
  *just set the beginning to be last_valid_address
  */

 managed_memory_start = last_valid_address;

 /* Okay, we're initialized and ready to go */

  has_initialized = 1;

}

      

 

现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构: 


清单 3. 内存控制块结构定义



        
struct mem_control_block {

 int is_available;

 int size;

};

      

 

现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。 

在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码: 


清单 4. 解除分配函数



        
void free(void *firstbyte) {

 struct mem_control_block *mcb;

 /* Backup from the given pointer to find the
  * mem_control_block
  */

 mcb = firstbyte - sizeof(struct mem_control_block);

 /* Mark the block as being available */

 mcb->is_available = 1;

 /* That's It!  We're done. */

 return;
}

      

 

如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述: 


清单 5. 主分配程序的伪代码



        

1. If our allocator has not been initialized, initialize it.

2. Add sizeof(struct mem_control_block) to the size requested.

3. start at managed_memory_start.

4. Are we at last_valid address?

5. If we are:

   A. We didn't find any existing space that was large enough
      -- ask the operating system for more and return that.

6. Otherwise:

   A. Is the current space available (check is_available from
      the mem_control_block)?

   B. If it is:

      i)   Is it large enough (check "size" from the
           mem_control_block)?

      ii)  If so:

           a. Mark it as unavailable

           b. Move past mem_control_block and return the
              pointer

      iii) Otherwise:

           a. Move forward "size" bytes

           b. Go back go step 4

   C. Otherwise:

      i)   Move forward "size" bytes

      ii)  Go back to step 4

      

 

我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码: 


清单 6. 主分配程序



        
void *malloc(long numbytes) {

 /* Holds where we are looking in memory */

 void *current_location;

 /* This is the same as current_location, but cast to a
  * memory_control_block
  */

 struct mem_control_block *current_location_mcb;

 /* This is the memory location we will return.  It will
  * be set to 0 until we find something suitable
  */

 void *memory_location;

 /* Initialize if we haven't already done so */

 if(! has_initialized)  {

  malloc_init();

 }

 /* The memory we search for has to include the memory
  * control block, but the users of malloc don't need
  * to know this, so we'll just add it in for them.
  */

 numbytes = numbytes + sizeof(struct mem_control_block);

 /* Set memory_location to 0 until we find a suitable
  * location
  */

 memory_location = 0;

 /* Begin searching at the start of managed memory */

 current_location = managed_memory_start;

 /* Keep going until we have searched all allocated space */

 while(current_location != last_valid_address)

 {

  /* current_location and current_location_mcb point
   * to the same address.  However, current_location_mcb
   * is of the correct type, so we can use it as a struct.
   * current_location is a void pointer so we can use it
   * to calculate addresses.
   */

  current_location_mcb =

   (struct mem_control_block *)current_location;

  if(current_location_mcb->is_available)

  {

   if(current_location_mcb->size >= numbytes)

   {

    /* Woohoo!  We've found an open,
     * appropriately-size location.
     */

    /* It is no longer available */

    current_location_mcb->is_available = 0;

    /* We own it */

    memory_location = current_location;

    /* Leave the loop */

    break;

   }

  }

  /* If we made it here, it's because the Current memory
   * block not suitable; move to the next one
   */

  current_location = current_location +

   current_location_mcb->size;

 }

 /* If we still don't have a valid location, we'll
  * have to ask the operating system for more memory
  */

 if(! memory_location)

 {

  /* Move the program break numbytes further */

  sbrk(numbytes);

  /* The new memory will be where the last valid
   * address left off
   */

  memory_location = last_valid_address;

  /* We'll move the last valid address forward
   * numbytes
   */

  last_valid_address = last_valid_address + numbytes;

  /* We need to initialize the mem_control_block */

  current_location_mcb = memory_location;

  current_location_mcb->is_available = 0;

  current_location_mcb->size = numbytes;

 }

 /* Now, no matter what (well, except for error conditions),
  * memory_location has the address of the memory, including
  * the mem_control_block
  */

 /* Move the pointer past the mem_control_block */

 memory_location = memory_location + sizeof(struct mem_control_block);

 /* Return the pointer */

 return memory_location;

 }

      

 

这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。 

运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc() 和 free() 才是最主要的函数): 


清单 7. 编译分配程序



        
gcc -shared -fpic malloc.c -o malloc.so

      

 

该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。 

在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下: 


清单 8. 替换您的标准的 malloc



        
LD_PRELOAD=/path/to/malloc.so

export LD_PRELOAD

      

 

LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。 

如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。 

我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括: 

  • 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。 
  • 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。 
  • 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。 
  • 它没有实现很多其他的内存函数,比如 realloc()。 
  • 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。 
  • 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。 
  • 分配程序不是线程安全的。 
  • 分配程序不能将空闲空间拼合为更大的内存块。 
  • 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。 
  • 我确信还有很多其他问题。这就是为什么它只是一个例子! 

 

其他 malloc 实现

malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括: 

  • 分配的速度。 
  • 回收的速度。 
  • 有线程的环境的行为。 
  • 内存将要被用光时的行为。 
  • 局部缓存。 
  • 簿记(Bookkeeping)内存开销。 
  • 虚拟内存环境中的行为。 
  • 小的或者大的对象。 
  • 实时保证。 

 

每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。 

还有其他许多分配程序可以使用。其中包括: 

  • Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。 
  • BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。 
  • Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。 

 

众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。 

在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。 

基于 malloc() 的内存管理的缺点

不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。 

因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。

C语言内存管理

欢迎进入内存这片雷区。伟大的Bill Gates 曾经失言: 
640K ought to be enough for everybody 
— Bill Gates 1981
程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本章的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。
7.1内存分配方式
内存分配方式有三种:
(1)       从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2)       在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)       从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
7.2常见的内存错误及其对策
       发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。
常见的内存错误及其对策如下:
u       内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

u       内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

u       内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

u       忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
u       释放了内存却继续使用它。
有三种情况:
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

l         【规则7-2-1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
l         【规则7-2-2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
l         【规则7-2-3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
l         【规则7-2-4】动态内存的申请与释放必须配对,防止内存泄漏。
l         【规则7-2-5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

Records:15612345678910»