前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C/C++ 学习笔记七(内存管理)

C/C++ 学习笔记七(内存管理)

原创
作者头像
Celebi
修改2017-08-25 10:33:58
1.9K0
修改2017-08-25 10:33:58
举报
文章被收录于专栏:Celebi的专栏Celebi的专栏

相对于其他语言,C、C++的一大利器便是可以非常灵活的控制内存。与此同时,另一方面灵活的带来的要求也是十分严格,否则会出现令人头疼的分配错误、内存越界、内存泄漏等众多内存问题。

程序内存结构

C程序的内存结构分为两种,一种是存储在磁盘时的结构,一种是程序运行时的结构。两者的区别在与运行时,系统会为其多分配堆栈空间。

图:C程序内存结构

下面通过一个例子看看具体的分配

代码语言:javascript
复制
#include <stdio.h>

int bss_var;
int data_var = 1;

int main(int argc,char** argv)
{
    int stack_var = 2;
    printf("栈 stack \n");
    printf("--------------------\n");
    printf("\t stack_var地址:%p\n",&stack_var);
    int stack_var2 = 3;
    printf("\t stack_var2地址:%p\n",&stack_var2);

    printf("堆 heap\n");
    printf("--------------------\n");
    char *b = (char *)malloc(sizeof(char));
    printf("\t b地址 :%p\n",b);

    printf("未初始化数据段 .bss \n");
    printf("--------------------\n");
    printf(" \t bss_var地址:%p \n",&bss_var);

    printf("已初始化数据段 .data \n");
    printf("--------------------\n");
    printf(" \t data_var地址:%p \n",&data_var);

    printf("代码段 text \n");
    printf("--------------------\n");
    printf("\t main函数地址:%p\n",main);
    return 1;
}

输出的结果。

代码语言:javascript
复制
栈 stack 
--------------------
     stack_var地址:0x7fff5fbff71c
     stack_var2地址:0x7fff5fbff718
堆 heap
--------------------
     b地址 :0x100203850
未初始化数据段 .bss 
--------------------
     bss_var地址:\314p 
已初始化数据段 .data 
--------------------
     data_var地址:0x100001028 
代码段 text 
--------------------
     main函数地址:0x100000cf0

堆与栈

栈是一种的“先进后出”的存储结构。

堆是一种完全二叉树。节点从左到右填满,最后一层的树叶都在最左边。(即如果一个节点没有左边儿子,那么它一定没有右边儿子),每个节点的值都小于(或者大于)其子节点的值(大顶堆、小顶堆)。它的特点是可以使用一维数组来表示。堆的操作也可通过数据元素交换的形式解决,非常适合内存空间线性的特点。

C语言中,内存分配有三种

  1. 静态区域分配:由编译器自动分配与释放,内存在编译的时候已经分配好,这块内存在整个程序的运行期间都存在直到程序结束时才被释放,如全局变量与static变量。
  2. 栈分配:由编译器在程序运行时从栈上分配,函数栈退出时自动释放。栈分配的运算在处理器的指令集中,所以它的运行效率很高,但能分配的内容是有限的。
  3. 堆分配:有程序员主动调用内存分配函数来申请内存,且使用完毕后由程序员自己释放,其使用非常灵活,但其分配方式是通过调用函数来实现,效率没栈高。malloc,alloc等

堆与栈的区别

先从汇编的角度来看堆与栈在分配空间的区别。

下面这个例子非常简单,给栈变量a赋值0xA,给堆分配的变量赋值0xB

代码语言:javascript
复制
int a = 0xA;
char *b = (char *)malloc(sizeof(char));
*b = 0xB;
int c = a+*b;

下面是汇编代码

代码语言:javascript
复制
Test`main:
    0x100000f10 <+0>:  pushq  %rbp
    0x100000f11 <+1>:  movq   %rsp, %rbp
    0x100000f14 <+4>:  subq   $0x20, %rsp
    0x100000f18 <+8>:  movl   $0x1, %eax
    0x100000f1d <+13>: movl   %eax, %ecx
    0x100000f1f <+15>: movl   $0x0, -0x4(%rbp)
    0x100000f26 <+22>: movl   %edi, -0x8(%rbp)
    0x100000f29 <+25>: movq   %rsi, -0x10(%rbp)
    0x100000f2d <+29>: movl   $0xa, -0x14(%rbp)
    0x100000f34 <+36>: movq   %rcx, %rdi
    0x100000f37 <+39>: callq  0x100000f62               ; symbol stub for: malloc
    0x100000f3c <+44>: movq   %rax, -0x20(%rbp)
    0x100000f40 <+48>: movq   -0x20(%rbp), %rax
    0x100000f44 <+52>: movb   $0xb, (%rax)
    0x100000f47 <+55>: movq   -0x20(%rbp), %rdi
    0x100000f4b <+59>: callq  0x100000f5c               ; symbol stub for: free
    0x100000f50 <+64>: movl   $0x1, %eax
    0x100000f55 <+69>: addq   $0x20, %rsp
    0x100000f59 <+73>: popq   %rbp
    0x100000f5a <+74>: retq

对于栈变量a的内存分配与赋值,这里的做法非常简单,主要的两个指令便可以完成分配与赋值操作。

1.栈顶指针寄存器向低地址移动0x30个字节空间(subq $0x30, %rsp),也可以理解为分配了0x30个字节空间给当前堆栈,而此时栈中已经包含变量a的空间。 2.将直接将立即数0xA赋值给变量a movl $0xa, -0x14(%rbp)。此时我们可以知道,相对于栈基指针寄存器向低地址偏移0x14的地址便是a的内存区域。 3.当函数执行完毕后,栈顶指针寄存器rsp与栈基地址寄存器rbp,回退到上一函数,步骤1中分配的的空间也同时被释放。

而堆变量b的内存分配与赋值,则可以看到其是通过调用callq 0x100000f68实现的(此处0x100000f68指的是malloc函数的地址)。也就是变量b内存的分配是由malloc函数内部实现,并没有像栈变量分配一样通过简单的两个指令便可以完成。

C语言中几种内存分配函数

代码语言:javascript
复制
void *malloc(size_t size)

从堆中分配内存,分配大小为size。若分配成功,返回内存首地址,如果分配失败,返回NULL。

代码语言:javascript
复制
void *calloc(size_t count, size_t size)

从堆中分配内存,分配count个相邻的内存单元,每个单元大小为size。若分配成功,返回内存首地址,如果分配失败,返回NULL。 从功能上看,该函数与malloc差不不大,不同的是calloc函数会将内存初始化为0。

有人会问既然calloc已经覆盖malloc所做的事情,而且还非常方便的将内存初始化为0,那malloc不就不太有用了吗?其实在调用方看来malloc而不需要初始化为0的情况,可能分配内存后马上赋值了有用的数据,不需要初始化为0.

代码语言:javascript
复制
void realloc(void ptr, size_t size)

用于更改已经配置的内存空间,其同样是从堆中分配内存。 当程序需要扩大空间时,函数试图从堆上当前内存段后的字节中获取更多的内存空间,如有足够的存储空间,则扩大内存后返回原地址。如果当前内存段后的字节不够,则使用内存堆上满足要求的其他内存块,并将原有的数据拷贝至新分配的区域,然后释放原有区域,返回新区域的指针。

代码语言:javascript
复制
void *alloca(size_t)

不同于malloc、calloc、realloc是从堆中分配内存,alloca是从栈中分配空间。正因其从栈中分配的内存,因此无需手动释放内存。

使用动态内存分配时的注意事项

1. 分配的内存,必须及时释放

使用malloc、calloc、realloc从堆中分配内存时,需要及时释放

2. 对内存分配函数的返回值必须进行检查

使用内存分配函数获取指针变量时,需堆分配函数的返回值进行判空处理。 因内存分配函数可能会因为其他的一些不可预知的情况导致分配失败。

代码语言:javascript
复制
char * chp = (char *)malloc(100);
if(NULL == chp){
    //处理内存分配失败
}else{
    //正常逻辑
}

3. 对内存分配函数的返回指针进行强制类型的转换

因内存分配函数返回值都为void (也称无类型),而且void 无法对该一段内存区域进行移位访问操作,所以在使用分配函数必须对其转换成其他类型,以便进行操作。

代码语言:javascript
复制
 char * chp = (char *)malloc(sizeof(char)*100);

4. 内存分配函数后必须对数据进行初始化

在使用malloc进行分配时,因该内存函数为进行初始化,若此时对内存进行访问,很可能会造成程序崩溃

代码语言:javascript
复制
char * chp = (char *)malloc(sizeof(char)*100);
if(NULL == chp){
    //处理内存分配失败
}else{
    memset(chp,sizeof(char)*100),0);
}

5. 禁止执行0长度的内存分配

C99规定,程序尝试分配长度为0的内存时,该行为是由具体编译器所决定的。可能会到时程序崩溃,可能返回一个NULL指针。所以需要避免此行为。

6. free之后必须对指针赋NULL

一块内存区域使用free释放后,需要养成将其设置为NULL的习惯,以避免在程序错误的再次访问指针时造成野指针访问错误。

代码语言:javascript
复制
    char *b = (char *)malloc(sizeof(char)*4);

    memset(b,sizeof(char)*4,0);
    strcpy(b,"abc");

    free(b);
    //此处应该加上 b = NULL;

    if (b) {
        //发生错误,非法访问野指针
        strcpy(b,"def");
    }

7.避免非法内存空间

对于不是通过内存分配函数获取的空间,禁止非法访问。

代码语言:javascript
复制
 char *b = (char *)malloc(sizeof(char)*4);
 b[5] = '1';  //错误,指针越界!

 b = b--;  //错误,不能直接操作内存
 b[0] = 'a'

8.确保指针指向一块合法的区域

C语言中,只要是一个指针变量,那就需要确保其指向是一段合法有效的值。

代码语言:javascript
复制
 struct student{
    char *name;
    int id;
};

  struct student std;
  strcpy(std.name, "Jack");  //非法赋值,std.name指针指向一个非法的值

如上例子中,需要给指针变量分配一段合法的内存

代码语言:javascript
复制
 struct student std;
 std.name = malloc(sizeof(char)*20);
 strcpy(std.name, "Jack");

9. 避免包含其他复杂成员时未及时释放导致内存泄漏

在释放c语言中的结构体时,需要确保其成员属性中的所有内存都释放,以免出现内存泄漏。 延续上面的例子

代码语言:javascript
复制
 struct student{
    char *name;
    int id;
};

struct student * std =  malloc(sizeof(struct student));
std->name = malloc(sizeof(char)*20);
strcpy(std->name, "Jack");
free(std);

仅仅释放std指针是不够的,需要释放其name成员,而且释放的顺序也需要注意,是先成员后对象。

代码语言:javascript
复制
free(std->name);
free(std);

10.避免申请过大的内存空间

各个内存分配函数中对于大小的参数都是size_t,在分配内存时需要确保避免申请过大的内存空间。

小结

C语言中对于内存使用是十分灵活与方便的,正是由于其过于灵活,我们在使用它时,需要对于分配出来的内存块的大小,初始化,生命周期,释放时机,释放方法,有个非常清楚的了解,要清楚的了解分配的每一块内存的去向,做到胸有成竹才能用好这一利器。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 程序内存结构
  • 堆与栈的区别
  • C语言中几种内存分配函数
  • 使用动态内存分配时的注意事项
    • 1. 分配的内存,必须及时释放
      • 2. 对内存分配函数的返回值必须进行检查
        • 3. 对内存分配函数的返回指针进行强制类型的转换
          • 4. 内存分配函数后必须对数据进行初始化
            • 5. 禁止执行0长度的内存分配
              • 6. free之后必须对指针赋NULL
                • 7.避免非法内存空间
                  • 8.确保指针指向一块合法的区域
                    • 9. 避免包含其他复杂成员时未及时释放导致内存泄漏
                      • 10.避免申请过大的内存空间
                      • 小结
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档