c/c++学习笔记(12)
此次为练习实践记录,讲解用纯C模拟一个C++的int类型的stack。
因为在纯C中没有class关键字,也没有public、private,但是有struct,现在已最接近的方式实现,声明如下:
1 typedef struct 2 { 3 int *elements; 4 int logicalLen; 5 int allocLength; 6 } Stack;
在纯C中typedef是必须的,但是在C++中是不必要的。从技术上来讲,以上3个域都是暴露在外的,都是隐含为public的。我们可以将Stack声明为局部变量,编译器知道这种类型占12字节,但是我们不应该直接操纵这些数据,应该使用函数对它们进行操作。我们要将任何类的结构类型看作是黑盒,并通过函数来操作这12字节。因此要有一个构造函数、析构函数、pop函数(弹出栈)、push函数(压入栈),声明如下:
1 void StackNew(Stack *s); 2 3 void StackDispose(Stack *s); 4 5 void StackPush(Stack *s, int value); 6 7 int StackPop(Stack *s);
当调用一个类的构造函数的时候,函数要访问到this指针,函数会将this作为诸如第-1个参数或者作为一个隐含参数在其他参数传入之前传入。以下先给出如何调用的代码:
1 Stack s; 2 StackNew(&s); 3 for(int i = 0; i < 5; i++) 4 { 5 StackPush(&s, i); 6 } 7 StackDispose(&s);
接下来根据以上的代码,一步步往下实现。首先是StackNew(构造函数)实现如下:
1 void StackNew(Stack *s) 2 { 3 s -> logicalLen = 0; 4 s -> allocLength = 4; 5 s -> elements = malloc(4 * sizeof(int)); 6 }
malloc是一个动态分配内存的函数,是一个相当于new的比较早的申请内存的方式。在纯C中没有new、delete,有名为malloc的内存分配器。new操作符会隐含的考虑到数据类型,因为实际的表达是可能为new int[4],malloc函数用一个参数来指定需要的原始字节的大小并将申请到的空间作为数组或者你构建的结构。因此我们需要4个int大小的内存,因此就要传入4 * 每个int单元布局的大小,malloc会在堆中查找这样一个内存块,然后返回它的地址(通常情况下都会返回地址)。如果担心返回NULL,可以加入一个assert宏,代码如下:
assert( s -> elements != NULL );
因为如果由于某种原因导致malloc执行失败(实际是很少的),内存不足而失败或是你在某些不允许释放的内存上调用了free函数(该函数后面会讲到),这样就会把真个内存分配器搞得一团糟。以上的代码表明可能该地方可能会出现问题,而不是最终造成段错误或者总线错误或者不明原因的崩溃,而让你无法跟踪到错误发生的地方。
接下来是StackDispose(析构函数),实现如下:
1 void StackDispose(Stack *s) 2 { 3 free(s -> elements); 4 }
如果在C++中应该是使用delete的,free其实是通知编译器该地址指定的内存块将被重新归还给堆。这里有1点要说明:为什么不是释放s本身所占的内存。
因为除了StackNew地方以外,没有其他地方为Stack变量分配空间,假设Stack变量本身的空间已经被分配好并且它的地址作为参数被StackNew指定,因此你不知道这个变量的空间是否动态申请的。前面的Stack变量s是局部变量,它不是动态申请的。所以我们不需要释放s本身所占的内存。
StackPush(压入栈),该函数不仅仅是将一个int类型添加到数组的末尾,而是能够在当申请空间已经饱和时,适当的将申请的空间进行扩展(2n倍扩充),变得更大,然后将旧的数组复制过来,并将旧的数组清理掉,实现如下:
void StackPush(Stack *s, int value) { if(s->logicalLen == s->allocLength) { s->allocLength *= 2; s->elements = realloc(s->elemtns, s->allocLength * sizeof(int)); } s->elements[s->logicalLen++] = value; }
对于realloc在c++中没有与它等价的函数,它会尝试获取传入的指针值,然后先检查已经分配了的内存块是否可以调整大小,如果可以则记录下扩展了的内存块已经包含了更多的空间并返回同样的地址,不行的话则寻找新的内存块,并将数组复制过去,然后释放掉旧的数组内存块并返回最终的地址。第二个参数是将内存块调整到这个参数的大小。如果不将s->elements = 返回的地址,那么s->elemetns则是原始的地址,当操作后的地址发生改变,在调用s->elements的时候,引用的可能是回收给堆管理器的内存。如果realloc操作失败,会返回NULL,且原来的内存块并不会被释放。我们仍然可以在最后加入一个assert,来用简单的出错信息提示用户,扩展失败,请尝试其他的操作等。
最后StackPop(弹出栈),函数还是挺简单的,实现如下:
1 int StackPop(Stack *s) 2 { 3 arrser(s->logicalLen > 0); 4 s->elements[--s->logicalLen]; 5 }
随着不断的弹出栈,我们所需要的空间慢慢的变小了,如果想把多余的空间还给堆,根据对于realloc实现的理解,因为编译器希望能够运行得越快越好,通常编译器会忽略缩小数组的请求,只要内存大小满足需求编译器并不在乎多申请了的一点空间,你仍然可以使用想要的空间大小,但是实际的空间大小仍然为你保留着,因为这样能更快的执行。
基本上到这里已经将代码实现并且讲解完了,时间也很晚了,就到这里吧,呵呵,明天早上继续学习,加油!