C语言-内存管理深入

前言

基础篇介绍了一些关于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. 将控制转移到被调用函数入口
  • 被调用函数返回调用函数之前,系统也需要相应的完成3件事情:
    1. 在栈中保存被调用函数的计算结果(返回值)
    2. 释放在栈中为被调用函数分配的数据区
    3. 依照被调用函数保存的返回地址将控制转移到调用函数

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

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

  • 帧栈
    帧栈也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数形参、返回地址、局部变量等信息。简而言之栈帧就是一个函数执行的环境。有时候函数嵌套调用,栈中会有多个函数的信息,每个函数占用一个连续的区域。引文中提到的“存储区”就是指一个函数对应的分配空间也就是一个函数帧
  • 参数的传递和内存分配
    被调用函数的形参,在未出现函数调用时不占用内存空间,发生函数调用时,形参按照确定的类型在该函数帧中被分配指定大小的空间。并且由调用函数的实参传递给被调用函数的形参保存。
  • 函数具体调用过程:
    1. 为被调用函数在栈顶分配空间,为被调用函数的形参分配空间
    2. 将实参的值传递给形参
    3. 被调用函数利用形参(如果存在)进行运算
    4. 通过return语句将返回值带回调用函数
    5. 调用结束,释放被调用函数空间,释放其形参分配空间

举个例子说明问题

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()函数形参的内存分配)。虽然函数在开发中无处不见,但是执行过程在内存中的表现形式还是有很多值得研究的。掌握其内存分配原理有助于我们更加深入理解函数。

递归函数的调用过程

  • 递归函数
    在调用一个函数过程中又出现直接或间接调用该函数本身的情况,称为函数的递归调用。C语言的特点之一就是允许函数的递归调用。
  • 递归函数的调用过程
    一个递归函数的运行过程类似于多个函数之间的嵌套调用,只是调用函数和被调用函数是同一个函数,因此,和每次调用相关的一个重要概念是递归函数运行的“层数”。假设调用该递归函数的主函数为第 0 层,主函数调用递归函数进入第一层;从第 i 层调用本函数为进入第 i+1 层,反之,退出第 i 层递归则应返回至第 i - 1 层。例如:
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点请随手点赞;转载请注明出处,谢谢支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • C语言中内存分配 在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是...
    一生信仰阅读 1,151评论 0 2
  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,598评论 1 19
  • 前言 C语言作为一门应用途广泛、功能强大、使用灵活的面向过程式编程语言。既可用于编写应用软件,又能用于编写系统软件...
    老板娘来盘一血阅读 12,973评论 32 83
  • 题目类型 a.C++与C差异(1-18) 1.C和C++中struct有什么区别? C没有Protection行为...
    阿面a阅读 7,643评论 0 10
  • 凌晨1点半,在孩子的哭闹中醒来,迷迷糊糊爬起来给孩子冲奶粉,90ml的水,3平勺的奶粉,然后用勺子搅匀,在冷水中冲...
    我这是婴儿肥阅读 157评论 0 0