更加通用的栈类型
typedef struct {
void *elems;
int elemSize;
int logLength;
int allocLength;
} Stack;
void StackNew(Stack *s, int elemSize);
void StackDispose(Stack *s);
void StackPush(Stack *s, void *value);
void StackPop(Stack *s, void *value);
我们来看一个场景
const char* example[] = {"AI", "Bob", "Alice"};
Stack stringStack;
StackNew(&stringStack, sizeof(char*));
for(int i = 0; i < 3; i++) {
char *name = strdup(example[i]);
StackPush(&stringStack, &name);
}
for(int i = 0; i < 3; i++) {
char *value = NULL;
StackPop(&stringStack, &value);
free(value);
}
StackDispose(&stringStack);
目前的做法是在堆空间中进行动态分配,然后将字符串数组中的元素(即字符串首地址)拷贝至堆空间。之后,在pop操作的过程,我们将获得栈顶的元素,通过free的方式将分配的内存进行释放。最后使用StackDispose操作将栈中动态分配的空间进行释放。如下图所示:
但是如果我们进行如下操作:
for(int i = 0; i < 2; i++) {
char *value = NULL;
StackPop(&stringStack, &value);
free(value);
}
没有将栈中的所有元素弹出,这样StackDispose在执行的过程中只会释放elems指向的空间,而elems中剩余元素指向的字符串空间并没有被释放,从而导致内存泄漏。
因此,我们目前就面临一个问题,如果栈结构体中存储的是指针类型,其可能是指向动态分配的内存空间。如果用户在pop操作的过程中每将栈中所有元素完全释放,那么在StackDispose的操作中我们需要帮助用户去进行释放。我们对栈结构体进行进一步改进。
typedef struct{
void *elems;
int elemSize;
int logLength;
int allocLength;
void (*freeFn)(void *pointer);
} Stack;
void StackNew(Stack *s, int elemSize, void (*freeFn)(void *pointer));
void StackDispose(Stack *s);
void StackDispose(Stack *s) {
if(s->freeFn != NULL && s->logLength > 0) {
for(int i = 0; i < s->logLength; i++) {
freeFn((char*)s->elems + i * elemSize);
}
}
free(s->elems);
}
void freeFn(void *pointer) {
free(*(char**)pointer);
}
这里我们实现方式是依次对栈空间中的元素进行处理,释放其动态分配的空间。另一方面,我们这里并不清楚栈中每个元素具体的分配方式,因此释放函数需要由用户来进行指定。
rotate函数
void rotate(void *front, void *middle, void *end);
如图所示,该函数的作用就是将front到middle之间的元素,与middle到end之间的元素进行交换。
这里补充两个函数的知识点
void memcpy(void *dst, const void *src, size_t n);
void memmove(void *dst, const void *src, size_t n);
这两个函数的作用都是从src拷贝n个字节的数据到dst,区别在于在目标区域与源区域有重叠的情况下,memmove会进行处理,而memcpy是未定义的。
我们将1, 2, 3, 4拷贝到destination的位置处,如果从source开始拷贝,那么1就会覆盖4导致拷贝的过程中出现问题。在这种情况下,memcpy是不会进行考虑的,但是memmove会进行处理,比如从后向前进行拷贝。由于此处我们操作的是同一个数组,所以有可能会发生覆盖的情况,所以这里使用memmove,但是通常情况下推荐使用memcpy,因为其性能更好,在编写底层代码的时候,我们更考虑性能因素。
void rotate(void *front, void *middle, void *end) {
int fsize = (char*)middle - (char*)front;
int bsize = (char*)end - (char*)middle;
char buffer[fsize];
memcpy(buffer, front, fsize);
memmove(front, middle, bsize);
memcpy((char*)front+bsize, buffer, fsize);
}
程序内存布局
我们进行的各种函数操作(函数调用、声明和使用临时变量等)都是发生在stack segment中的,栈指针会不断地向下移动,只有当函数调用返回时,栈指针才会上移。而一个函数中声明的变量,只在当前函数中是可见的。栈的内存管理是由操作系统进行的,我们不需关心。而heap segment中的操作是软件进行管理的(malloc、realloc和free等操作,具体实现在标准库中)。
对于heap segment来说,我们可以将其想象成一个线性的字节数组。该处的实现与实际有一定的差距,仅仅是为了帮助后续理解。
void *a = malloc(40);
void *b = malloc(60);
在进行内存分配的过程中,首先会从头部开始进行搜索,如果有空闲位置就直接进行分配。
free(a);
释放操作表现前40字节可以进行使用,其中的内容并没有被抹去。后续一些操作,从图中我们可以很清楚的理解。
void *c = malloc(44);
void *d = malloc(20);
在真实情况下,空闲空间是由空闲空间链表进行管理的,如下图所示(实心部分表示已经被分配的区域),每一块空闲空间相当于一个节点,该节点的头部由两部分组成,一是当前空间大小,另一个是指针(指向下一个空闲区域)。
内存管理器通过链表来依次进行判断,每个节点是否满足空间需求。本节课只是稍微介绍一下,详细内容在下一节课进行介绍。