斯坦福大学公开课-编程范式 笔记 1

Posted on 日 07 七月 2013 in opencourse • 2 min read

前言

最近在跟一门斯坦福大学的公开课 Programming Paradigms ,网易公开课也有其中文翻译版,翻译已经完成了:斯坦福大学公开课:编程范式

课程内容:

Advanced memory management features of C and C++; the differences between imperative and object-oriented paradigms. The functional paradigm (using LISP) and concurrent programming (using C and C++). Brief survey of other modern languages such as Python, Objective C, and C#.

首先涉及的是 C/C++ 的高级内存管理,内容包括 C 的各种数据类型的内存布局,malloc 和 free 的实现,等等。然后还有命令式和面向对象,函数式编程等等几种不同的编程范式及他们的差别。

可以说这些内容应该是属于比较高级的内容。我之前偶尔也会接触到一些,有些书也会讲到,但是在我所在的大学的课堂上,这些内容基本上是不讲授的。在上 C 课程的时候,甚至连指针都语焉不详,更别提有一门专门的课程来讲述这些高级内容。上这个公开课刚好可以完整地补全我对这方面内容的不足,毕竟一个斯坦福大学的教授给你讲解这些内容总比自己看书效果要好得多。

在这里,我要记下的是在课程中碰到的一些有趣的内容。

malloc 与 free

我们知道,malloc 函数是 C 中动态分配内存的一个函数,通过这个函数可以在堆中申请一块限定大小的内存使用。对应地,有申请内存函数就有释放内存函数,这个函数就是 free 函数。这两个函数的原型如下:

#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);

通过这两个函数的原型我们可以看到一些信息。malloc 函数的参数是一个 size_t 类型的变量 size ,而 malloc 返回的则是一个 void * 类型的指针。这不意外,因为我们知道通过 malloc 函数分配制定大小的内存成功之后,会将这块内存的首地址作为返回值返回给某个类型的指针变量。而通过 C 的自动类型转换,void * 类型的指针地址将会被转换为该指针变量类型的指针。

free 函数也是只有一个参数,类型为 void * 的指针变量 ptr ,无返回值。在这里问题出现了, free 函数如果只是接受某块内存的首地址作为参数,那它是如何得知这块内存的大小?或者说,free 函数怎么知道需要被释放的内存到底有多大?这块内存的大小是必要要知道的,因为如果不知道,free 函数是无法准确地将要释放的内存释放掉,也许会将后面接着的不允许释放的内存也释放掉,也许还遗留一部分内存没有释放掉。

所以,编译器,或者操作系统,肯定是提供了一种机制来告知 free 函数,这块在堆中的内存的大小。那这个机制是什么呢?

malloc 的机制

答案并不复杂,那就是在 malloc 函数返回的地址前面,有 4 字节或者 8 字节同样也是属于这块内存的内容,这几个字节中储存了该内存地址的大小。free 函数接受这个地址的时候,会回退一部分地址,根据这个结构的内容,得到该内存块的大小,然后将相关的释放掉。

以上是公开课中老师的简单讲述,那实际情况是怎样?下面进行一下简单的验证。首先测试的平台是 Fedora17 ,Linux 的内核版本为 3.6.11-5.fc17.i686.PAE ,使用的 C 编译器为 GCC 4.7.2 20120921 (Red Hat 4.7.2-2) 。测试代码如下:

#include <stdio.h>
#include <stdlib.h>


//Test the memory alloc by malloc.
int main(int argc, char *argv[])
{
    int *ptr1, *ptr2, num1, num2;
    char *cptr;

    ptr1 = &num1;
    ptr1 = (int *)malloc(512 * sizeof(int));
    ptr2 = &num2;
    ptr2 = (int *)malloc(1024 * sizeof(int));
    cptr = (char *)malloc(1024 * sizeof(char));


    printf("The start of num1 memory address is %x.\n", ptr1);
    printf("Before this address, the value is %d.\n", *(ptr1 - 1));
    printf("The start of num2 memory address is %x.\n", ptr2);
    printf("Before this address, the value is %d.\n", *(ptr2 - 1));
    printf("The start of char memory address is %x.\n", cptr);
    printf("Before this address, the value is %d.\n", *((int *)cptr - 1));
}

测试代码并不复杂,简单定义了两个 int 类型的指针变量和一个 char * 类型的指针变量,然后用 malloc 函数分配一定数量的内存,返回的内存块首地址赋值给这三个变量,然后输出这三个内存块首地址前一个位置的值。注意对于 char * 类型的地址,首先强制转换成 int * 类型的再进行 -1 操作。

代码的输出结果如下所示:

╰ ➤ ./a.out 
The start of num1 memory address is 9922008.
Before this address, the value is 2057.
The start of num2 memory address is 9922810.
Before this address, the value is 4105.
The start of char memory address is 9923818.
Before this address, the value is 1033.

通过代码我们可以知道,ptr1 申请的内存块大小为 512 * 4 = 2048,ptr2 申请的内存块大小为 1024 * 4 = 4096,cptr 申请的内存块大小为 1024 * 1 = 1024,以上单位均为字节。根据输出,有如下计算:

2057 - 2048 = 9
4105 - 4096 = 9
1033 - 1024 = 9

可见,如果 malloc 函数返回的地址前一个字节保存了该内存块的整体大小,那可以推测到,其中 9 个字节作为额外的结构,保存了这块内存的信息,以提供给其他函数比如 free 函数利用。当然这只是一个推测,真实的情况需要深入到 glibc 的库的 malloc 和 free 的源码中。

实际上,老师也说了,不同的编译器的实现是不同的,比如,可以参考下这篇文章:malloc/new函数及malloc()的一种简单原理性实现

参考: