C语言动态分配内存

C语言动态分配内存

malloc

动态开辟内存的函数:

void* malloc (size_t size);

这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针
如果开辟成功,则返回⼀个指向开辟好空间的指针
如果开辟失败,则返回⼀个NULL指针,因此malloc的返回值⼀定要做检查
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者自己来决定
如果参数 size 为0, malloc的⾏为是标准是未定义的,取决于编译器

calloc

calloc 函数也⽤来动态内存分配。函数原型如下:

void *calloc(size_t num, size_t size);

函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

realloc

realloc函数的出现让动态内存管理更加灵活。
有时我们会发现过去申请的空间不够用或者用不完,那 realloc 函数就可以做到对动态开辟内存大小的调整。 函数原型如下:

void* realloc (void* ptr, size_t size);

ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:
1. 原有空间之后有⾜够⼤的空间
2. 原有空间之后没有⾜够⼤的空间

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。并且原来旧的地址就会被自动释放掉。

free

函数free,专门是⽤来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。

深究

这里我不禁产生了个疑问:既然会对已经开辟的内存进行realloc、free,那编译器是怎么知道开辟的这段内存有多大呢?

在VS2013中,我做了一个测试:

int main()
{
    int *p = (int *)malloc(sizeof(int));    
    return 0;
}

在测试中,我们申请了4个字节大小的空间。

单步调试后,跳转到malloc函数的定义处:
malloc函数的定义

继续F11加F10,可以在dbgheap.c文件中看到这样一句话:

blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize;

这里的_CrtMemBlockHeader是一个结构体。它的大小是多少呢?转到它的定义处发现:

typedef struct _CrtMemBlockHeader
{
        struct _CrtMemBlockHeader * pBlockHeaderNext;
        struct _CrtMemBlockHeader * pBlockHeaderPrev;
        char *                      szFileName;
        int                         nLine;
#ifdef _WIN64
        /* These items are reversed on Win64 to eliminate gaps in the struct
         * and ensure that sizeof(struct)%16 == 0, so 16-byte alignment is
         * maintained in the debug heap.
         */
        int                         nBlockUse;
        size_t                      nDataSize;
#else  /* _WIN64 */
        size_t                      nDataSize;
        int                         nBlockUse;
#endif  /* _WIN64 */
        long                        lRequest;
        unsigned char               gap[nNoMansLandSize];
        /* followed by:
         *  unsigned char           data[nDataSize];
         *  unsigned char           anotherGap[nNoMansLandSize];
         */
} _CrtMemBlockHeader;

可以看到,两个struct _CrtMemBlockHeader *占了8个字节,再加上char *int,就是16个字节;然后无论是不是64位系统,都要加上intsize_t的8个字节,就是24个字节;然后再加上longunsigned char的5个字节,总共有29个字节。又因为结构体的对齐规则,所以整个_CrtMemBlockHeader的大小就是32个字节。

那其他两个呢?
在dbgint.h的315行,会看到宏nNoMansLandSize的值#define nNoMansLandSize 4
监视
另一个nSize的大小刚好为4,blockSize的大小就是32+4+4=40个字节。

接着往下看,在圈起来的部分,可以看到系统用刚刚的blockSize大小到堆区去申请空间了。
开辟空间

现在意思就很明显了:在主函数中我们申请了4个字节大小的空间,而实际是在堆上多申请了32加4个字节。那这多出来的字节用来干嘛了呢?继续往下调试代码,可以看到:

系统往刚刚在堆中申请的空间做了初始化操作:往真正用户需要的四个字节中填充0XCD;往用户需要空间的首尾以外的4个字节,填充0XFD
填充

最后,返回给p的就是那被填充0XCD的四个字节:

至此,结果已经出来了:系统在动态分配内存时并不是真正用户要多少就申请多少,而是会在每一个用户申请空间时多申请32+4个字节的空间,用来标记和管理分配给用户的空间。对于用户而言,在不出意外的情况下,每次感觉“自己真正申请了n个字节”,而实际上却不是这样。

既然每次动态地在堆上申请空间时,都会多申请32+4个字节的空间。若是在做链表时每次都只申请很少的字节,那将会造成很大的空间浪费。所以用C语言的动态开辟空间时,因注意到这一点。

猜你喜欢

转载自blog.csdn.net/neverwa/article/details/80724495
今日推荐