1. 指针和地址
TCPL 中给指针的定义是: A pointer is a group of cells (often two or four) that can hold an address .
int value = 10; int *pvalue = &value;
上面这个语句的内存模型是:
注意: &(取地址)操作符只对内存中存在的对象起作用,如变量和数组类型。不能对表达式,常量和寄存器变量使用取地址操作。
*(析取)操作符应用于一个指针变量时,取得这个指针变量所指向的对象。
每个指针只能指向特定类型的数据类型(void *型的指针可以指向任意类型的对象,但是却无法析取自身)。
2. 指针和函数参数:
TCPL中有这样一段描述: Since C passes arguments to functions by value, there is no direct way for the called function to alter a variable in the calling function.
C语言函数参数的传递方式是传值的,所以不能直接在被调用函数中修改调用函数中的变量。下面所谓的通过传递指针的"传址"方式,实际上也是"传值"方式,不过这里传递的是一个地址的值而已。
下面通过一个实例来验证参数为指针的函数:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <ctype.h> 4 5 #define BUFSIZE 100 6 7 char buf[BUFSIZE]; /* buffer for ungetch */ 8 int bufp = 0; /* next free position in buf */ 9 10 int getch(void) /* get a (possibly pushed back) character */ 11 { 12 return (bufp > 0) ? buf[--bufp] : getchar(); 13 } 14 15 void ungetch(int c) /* push character back on input */ 16 { 17 if(bufp >= BUFSIZE) 18 printf("ungetchar: too many characters\n"); 19 else 20 buf[bufp++] = c; 21 } 22 23 /* getint : get next integer from input into *pn */ 24 int getint(int *pn) 25 { 26 int c,sign; 27 28 while (isspace(c = getch())) /* skip white space */ 29 ; 30 if(!isdigit(c) && c != EOF && c != '+' && c!= '-') { 31 ungetch(c); /* the current character is not a number */ 32 return 0; 33 } 34 35 sign = (c == '-') ? -1 : 1; 36 if(c == '+' || c == '-') 37 c = getch(); 38 for(*pn = 0; isdigit(c); c = getch()) 39 *pn = 10 * *pn + (c - '0'); 40 *pn *= sign; 41 if(c != EOF) 42 ungetch(c); 43 return c; 44 } 45 46 int 47 main(void) 48 { 49 int result; 50 getint(&result); 51 printf("%d\n",result); 52 }
这段代码将用户输入的整数部分保存在result变量中,对result变量的修改是通过getint函数的指针参数进行的。
3. 指针和数组的关系:
TCPL 中对指针和数组之间的关系描述如下:
Any operation can be achieved by array subscripting can also be done with pointers. [任何可以由数组下标完成的操作都可以由指针来完成]
并经过一番论证之后得出这样的结论: 一个 [数组名+下表索引] 的表达式等价于一个对应的 [指针+偏移量] 表达式。
但数组名和指针之间的一个重要区别是,指针是一个变量,可以用于赋值操作,像pa = a(a是一个地址或数组名),pa++这样的表达式是合法的; 但数组
名不是变量,像前面那样的表达式是非法的。
当一个数组名被当做参数传递给一个函数的时候,这个函数可以根据自己的需要将要操作的对象看成是一个指针或一个数组。
绝对不能越界访问数组元素。
4. 地址运算[very important]:
TCPL: C is consistent and regular in its approach to address arithmetic; it's integration of pointers, arrays and address arithmetic is one of the strengths of the language. [C语言在其地址运算方面具有其一致性和正规性的特点,这种将指针,数组和地址运算结合起来的特点是C语言的优势之一];
书中用了一个初级的内存分配实例来说明地址运算,下面是内存分配动作示例图:
图中allocp指向数组中下一个空闲位置,当alloc要求分配n个单元时,首先检查数组还有足够的空间吗?如果有,那么alloc返回当前的allocp值作为新分配空间的开始位置,
然后allocp = allocp + n; 让allocp指向下一个空闲单元的位置! 如果没有足够多的空间那么alloc直接返回0. afree(p)直接将allocp设为p即可!
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define ALLOCSIZE 10000 /* size of avaiaable space */ 5 6 static char allocbuf[ALLOCSIZE]; /* storage for malloc */ 7 static char *allocp = allocbuf; /* next free position */
[理论上来说:指针变量作为一个变量可以被赋予任何值,但是在实际情况中:
要么是赋为NULL,要么是赋为某个以前已经定义好的对象的地址。这也是指针
被诟病的原因,不当的指针的使用往往会导致地址越界等情况的发生]。
8 9 char alloc(int n) /* return pointer to n characters */ 10 { 11 if(allocbuf + ALLOCSIZE - allocp >= n) /* it fits */
[allocbuf + ALLOCSIZE 是allocbuf数组最后一个元素后面的位置(最后一个元素allocbuf[ALLOCSIZE-1])]
12 { 13 allocp += n; 14 return allocp - n; /* old position */ 15 } 16 else 17 return 0;
[C语言保证任意一个有效地址都不会是0] 18 } 19 20 void afree(char *p) /* free storage pointed by p */ 21 { 22 if(p >= allocbuf && p < allocbuf + ALLOCSIZE) 23 allocp = p; 24 }
指针和整数之间是无法进行相互之间的转换的,但0除外,一个指针可以被赋值成0,表示这个指针不指向任何一个元素。一个指针也可以用来和0做比较!
上面程序中的 if(p >= allocbuf && p < allocbuf + ALLOCSIZE) 揭示了这样一个事实: 在特定的情况下,指针变量之间是可以相互比较的!
总结:
合法的指针运算:
不合法的指针运算:
5. 一个强化版的内存分配器:
上面的基于栈的内存分配器存在诸多限制,比如说空闲分区的数量是一定的。下面这个内存分配器将不存在空间的限制,并可以以任意的顺序调用malloc和free函数。
实际内存的使用情况是这样的: malloc每次都向操作系统请求新的空间。因为操作系统中还可能有其他的活动并非通过这个allocator分配内存空间,所以这个malloc函数
管理的内存区域并不是连续的。这样,空闲空间可以被组织成一个空闲块的列表,每个块包含一个大小,指向下一个块的指针和自身的空间。各个块以地址递增的顺序排序,
最后一个空闲块指向第一个空闲块。
下面是内存分配示意图:
内存分配的策略是:最快适应算法,就是扫描空闲块列表,找到第一个满足要求的空闲块; 与之相对应的是“最好适应算法”,即找出满足要求的最小的空闲块。
我们使用最快分配策略:
内存释放策略:
在释放空间在空闲链表中的位置,如果将要释放的区域的两侧区域都在空闲链表块中,那么将三个块合并形成一个新的空闲列表块。
下面是malloc返回的一块空闲块的示意图:
一个空闲块包含下面三个信息:
位于一个空闲块开始部分的信息被称为"头部";为了对齐需要,整个空闲区域的大小是头部大小的整数被,而头部要进行对齐,下面是头部的代码:
1 typedef long Align; /* for alignment to long boundary */ 2 3 union header { /* the header of a block */ 4 struct 5 { 6 union header *ptr; /* the next block if on free list */ 7 unsigned size; /* size of this block */ 8 } s; 9 Align x; /* force alignment of blocks */ [这个对齐域在程序中并没有被使用,只是为了最坏情况下的对齐需要] 10 }; 11 12 typedef union header Header;
下面是内存分配代码:
static Header base; /* empty list to get started */
static Header *freep = NULL; /* start of the free list */
1 /* malloc: general-purpose storage allocator
2 void *malloc(unsigned nbytes)
3 { 4 Header *p,*prevp; 5 Header *morecore(unsigned); /* [这是请求内核分配存储空间的一个函数] */ 6 unsigned nunits; 7 8 nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1; /* [每次分配的单元个数,以Header的大小作为一个单元] */ 9 10 /* [第一次调用malloc的时候,产生一个退化了的空闲区块列表,在这个列表中一个size为0的空闲区块,并且这个区块指向自身]
11 if((prevp = freep) == NULL) //[这里的prep是为了遍历链表的需要,初始时指向被赋予freep] 12 { 13 base.s.ptr = freep = prevp = &base; 14 base.s.size = 0; 15 } 16 17 /* 遍历整个空闲区间列表 */ 18 for (p = prevp->s.ptr; ; prevp = p , p = p->s.ptr) 19 { 20 if(p->s.size >= nunits) 21 { 22 /* big enough */ 23 if(p->s.size == nunits) 24 prevp->s.ptr = p->s.ptr; /* [这是一个常见的链表删除操作] */ 25 else 26 { 27 /* allocate tail end */ 28 p->s.size -= nunits; 29 p += p->s.size; 30 p->s.size = nunits; 31 } 32 freep = prevp; 33 return (void *)(p+1); 34 } 35 36 if(p == freep) /* [在循环链表中没有找到合适的区块,请求系统分配一个区块] */ 37 if((p = morecore(nunits)) == NULL) 38 return NULL; 39 } 40 }
我们用图示来说明内存空间的分配过程:
图1: 空闲区链表初始化状态
上图是这段代码初次运行后的结果:
if((prevp = freep) == NULL) { base.s.ptr = freep = prevp = &base; base.s.size = 0; }
我们假设malloc函数中的参数是10bytes,而我们对齐大小是4bytes,那么要分配的nunits将是4units。
执行下面的语句:
1 if(p == freep) /* [在循环链表中没有找到合适的区块,请求系统分配一个区块] */ 2 if((p = morecore(nunits)) == NULL) 3 return NULL;
这时的空闲块链表的映像是:
然后,再调用malloc(5), nunits = 3,而链表中有一个区块的大小是4,满足我们的要求:
执行这段语句:
for (p = prevp->s.ptr; ; prevp = p , p = p->s.ptr) { if(p->s.size >= nunits) { /* big enough */ if(p->s.size == nunits) prevp->s.ptr = p->s.ptr; /* [这是一个常见的链表删除操作] */ else { /* allocate tail end */ p->s.size -= nunits; p += p->s.size; p->s.size = nunits; } freep = prevp; return (void *)(p+1); }
特别是上面用红色字体标注的字段:
现在空闲区链表变成了:
好吧!关于malloc函数的执行我们就讲到这里,读者需慢慢体会!