Linux 学习学习计划iOS 干货整理

C语言-内存管理深入

2017-01-14  本文已影响909人  老板娘来盘一血

前言

基础篇介绍了一些关于C语言内存管理的常见概念,包括内存编址、堆栈、内存操作函数、变量和数组存储简介等等。本文将在前文的基础上扩展以下知识:结构体变量的存储函数调用与内存分配递归函数的调用过程。如果有需要浏览上一篇文章的同学请点击C语言-内存管理基础。希望本文能给正在学习C、Objective-C、C++等语言的小伙伴们更多启发。

结构体变量的存储

结构体在C语言中是一种常见的数据结构,开发中系统提供的基本类型往往不能满足需要,所以会常常使用到“结构体”。作为由基本数据类型组合而成的结构体与普通变量存储不一样,结构体变量占用空间不是简单的结构体成员空间单纯相加,其在内存中分配空间和数组很类似,可以总结为以下几点:

struct Person {
    int age;
    int hegiht;
    int weight;
};
int main(int argc, const char * argv[]) {
    struct Person p1 = {1, 2, 3};
    int size = sizeof(p1);
    printf("%i\n",size);              //12 = 4 * 3
    printf("变量的地址:%p\n",&p1);     //0x7fff5fbff750
    printf("%p\n",&p1.age);           //0x7fff5fbff750
    printf("%p\n",&p1.hegiht);        //0x7fff5fbff754
    printf("%p\n",&p1.weight);        //0x7fff5fbff758
    return 0;
}
结构体变量内存分配示意图1
说明:在p1变量中,由于其成员都是int类型,在64位编译器中占用4个字节空间,所以系统为p1变量分配的内存空间大小应该是:4 X 3 = 12个字节。另外:结构体成员内存地址分配是从低地址到高地址的,结构体变量的地址是其首成员的地址,这点和数组是一致的。
struct Student { 
      char id;
      double score;
      int  age;    
};
int main(int argc, const char * argv[]) { 
      struct Student s1 = {'A', 10.0, 15};  
      int size = sizeof(s1);   
      printf("%i\n",size);       //24
      printf("%p\n",&s1.id);     //0x7fff5fbff788
      printf("%p\n",&s1.score);  //0x7fff5fbff790
      printf("%p\n",&s1.age);    //0x7fff5fbff798
      return 0;
}
结构体变量内存分配示意图2
说明:按照前面的说明,系统为s1分配内存时以sizeof(double)8个字节为单位,所以为s1.id分配了8个字节的空间,但是由于id定义为char类型,所以只占了8个字节中数值最小的一个内存空间;由于前面剩下的 8 - 1 = 7个字节不足以存放double类型的值,所以接着为s1.score分配8个字节并占满8个字节空间;最后为s1.age分配8个字节并占用了前4个字节空间。故s1变量在内存中占用的内存大小为 8 X 3 = 24个字节。
如果调换结构体Student成员之间的顺序如下,情况又会发生变化。
struct Student {
      double score;
      char id;
      int  age;
};
int main(int argc, const char * argv[]) {
      struct Student s1 = {'A', 10.0, 15};
      int size = sizeof(s1);
      printf("%i\n",size); //16
      printf("%p\n",&s1.score) //0x7fff5fbff790
      printf("%p\n",&s1.id); //0x7fff5fbff790
      printf("%p\n",&s1.age); //0x7fff5fbff79c
      return 0;
}
结构体变量内存分配示意图3
说明:这是由于第二次为s1.id分配内存时没有完全占满8个字节的空间,而且第三次为s1.age分配时其需要的4个字节空间也没有超出剩余的8-1 = 7个字节空间,所以s1.age的值按照内存对齐的原则就存放在了第二次分配的8个字节的后4位空间中。

总结:结构体变量所占存储空间受其不同类型的成员排列顺序及编译器内存对齐影响,开发中尽量将相同类型的成员依次定义,有助于节省内存空间。

函数调用与内存分配

函数作为编程中实现功能的重要手段,深入理解函数的调用过程对提升开发能力有很大的帮助,首先了解一下两个任意函数之间进行调用的情形,与汇编程序设计中主程序和子程序之间的链接及信息交换类似,在高级语言编写的程序中(比如C语言),调用函数与被调用函数之间的链接及信息交换需要通过栈来进行。《C程序设计》一书中对于函数之间调用提出两个注意点:

  • 函数运行期间调用另外一个函数,在运行被调用函数之前,系统需要先完成3件事情:
  1. 将所有的实参、返回地址等信息传递给被调用函数保存
  2. 为被调用函数局部变量在栈上分配内存
  3. 将控制转移到被调用函数入口

当有多个函数嵌套调用时,按照 “后调用先返回” 的原则依次进行,看到这里想必大家一目了然,函数之间的调用规则和 “栈” 管理数据的方式完全相同,因此函数之间的信息传递和控制转移必须通过 “栈”来实现。
《数据结构(C语言版)》一书中对函数的调用过程在内存中的描述是这样的:

函数之间信息传递和控制转移必须通过“栈”来实现,即系统将整个程序运行所需的数据空间安排在一个栈中,每当调用一个函数就为它在栈顶分配一个存储区,每当从一个函数退出时,就释放它的存储区,当前正在运行的函数存储区必在栈顶。

举个例子说明问题

int add(int num1, int num2) {
         int tempSum = num1 + num2;
         return tempSum;
}
int main(int argc, const char * argv[]) {
          int a = 10;
          int b = 20;
          int sum = add(a, b);
          printf("%i\n",sum); //   sum = 30
          return 0;
}

上面的main()add()函数之间调用的过程及参数传递在内存的示意图如下:

函数调用参数分配示意图
(1)首先执行main()函数,系统为main()函数在栈顶分配一定大小的空间,其次为a、b局部变量分配空间;(2)调用add()函数,main()函数压入栈底,栈顶指针上移,系统为add()函数在栈顶分配一定大小的空间,其次为num1、num2局部变量分配空间;(3)执行两个整数的加法运算,在add()函数帧中新开辟一块空间存放计算后的结果tempSum;(4)最后add()函数返回,在main()函数帧中开辟一块新的空间存放add()函数的返回值sum,(5)add()函数帧调用结束出栈,系统释放其空间并且栈顶指针下移,main()函数重新回到栈顶。

注意:当前正在运行的函数存储区必在栈顶。

以上就是两个函数在调用过程中栈内存完整的工作情况(省略了main()函数形参的内存分配)。虽然函数在开发中无处不见,但是执行过程在内存中的表现形式还是有很多值得研究的。掌握其内存分配原理有助于我们更加深入理解函数。

递归函数的调用过程

int getAge(int n) {
      if (n == 1) {
          return 10;
      } else {      
          return getAge(n-1) + 2;
      }
}
int main(int argc, const char * argv[]) {
      printf("NO.5.age: %d\n",getAge(5));
      return 0;
}
递归函数调用过程

《数据结构(C语言版)》对递归函数调用从内存分配角度的解释如下:

为了保证递归函数正确执行,系统建立一个“递归工作栈”作为整个递归函数运行期间使用的数据存储区,每一层递归所需信息构成一个“工作记录”,其中包括所有的实参、局部变量以及上一层的返回地址。每进入一层递归,就产生一个新的工作记录压入栈顶。每退出一层递归,就从栈顶弹出一个工作记录。当前活动的工作记录成为“活动记录”,并称活动记录的栈顶指针为“当前环境指针”。

一个递归问题可以分为“递推”和“回溯”两个阶段,要经历若干步才能求出最后的结果。但是其原理和一般函数的调用没有本质区别。递归函数调用次数越多,在栈上为其分配的空间就越大,所以我们应该避免调用次数过多的递归函数,因为该操作很可能会使栈的容量“溢出”。
由于”递归函数“概念本身不是本文的重点,这里仅仅是介绍一下递归函数调用在内存中分配情况。对”递归函数“不甚了解的同学可以查阅一下相关资料,这里就不再赘述了。

文章最后

以上就是笔者对于C语言内存管理深入的学习心得,知识点比较少,部分描述引自书籍文档。如果文中有任何纰漏或错误欢迎在评论区留言指出,本人将在第一时间修改过来;喜欢我的文章,可以关注我以此促进交流学习; 如果觉得此文戳中了你的G点请随手点赞;转载请注明出处,谢谢支持。

上一篇下一篇

猜你喜欢

热点阅读