前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >轻松带你解决c语言堆、栈、数据段、代码段、bss段的疑惑

轻松带你解决c语言堆、栈、数据段、代码段、bss段的疑惑

作者头像
用户6280468
发布2022-03-21 08:40:08
1.4K0
发布2022-03-21 08:40:08
举报
文章被收录于专栏:txp玩Linux

当各位读者看到本次文章的标题,你可能会比较熟悉堆、栈的用法,因为在你学完了c语言后,或多或少都会接触到一点数据结构(但是这里要讲的与数据结构里面的堆和栈还是有点差别的,本次分析这个是从内存分配的角度去看,不是从的数据结构特点去看,而且在笔试面试的时候,经常会遇到这种题目,让你说出他们的区别来。自己亲身体会,遇到了好几次)。后面的数据段、代码段、bss段,可能你平时没有怎么细心总结,现在你可能还真讲不出他们的区别来,不信的话,读者在看到这里可以先暂定一下,在自己以往写了那么多的代码,仔细回忆看看他们有啥区别,如果不知道也没关系,读者可以继续随着我笔步往下看,当你看完或许会发出这样的感叹,原来是这样啊。是的,确实是这样的,包括自身在写这篇文章开始之前,我也讲不出来他们的区别(这里是昨天一个网友在我自己建的一个技术交流群里。提出了一个关于数据初始化的问题,如下图,正如你所见这个可能比较简单,但是要理解这里面的知识点,还是要花点时间来总结一下的):

一、栈:

  • 这里可能我没有介绍什么是栈,但是老司机的你,应该知道问度娘,网上有好多介绍的,也考验一下你的自学能力,我主要还是从今天的角度去讲解。

 1、栈一般是存放什么数据的呢?

          一般来讲,栈主要是为局部变量(一般是定义在函数里面)、函数参数分配内存大小,但是当他们离开这个"本职岗位"范围之后,就会被操作系统强行给咔嚓掉,最终被释放了出来,归还了给操作系统。这就好比,你去饭店吃饭,你吃饭的时候非常舒服(使用内存),但是当你发现你没钱支付饭钱的时候,搞不好你会被别人强行毒打一顿,然后又给"归还了"出去。

2、栈的特点:

  • 运行时自动分配和自动回收性:栈是自动管理的,程序员不需要手工干预。方便简单。
  • 反复使用性:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。
  • 遗留性:栈内存由于反复使用,每次使用后程序不会去清理,因此在使用栈时还是上次栈中遗留下的数值。
  • 临时性:(函数不能返回栈变量的指针,因为这个空间是临时的)。
  • 溢出性:因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完。

3、例子最重要:

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

// 函数不能返回函数内部局部变量的地址,因为这个函数执行完返回后这个局部变量已经不在了

 // 这个局部变量是分配在栈上的,虽然不在了但是栈内存还在还可以访问,但是 访问时实际上这个

 // 内存地址已经和当时那个变量无关了。
  int *func(void)//指针函数
    {
       int a = 4;           // a是局部变量,分配在栈上又叫栈变量,又叫临时变量
        printf("&a = %p\n", &a);
        return &a;
     }


  int main(void)
  {     

int *p = NULL;
p = func();

  printf("*p = %d.\n", *p); // 证明栈内存完了后是脏的,也就是最开始那个值 
printf("p = %p\n", p);

printf("*p = %d.\n", *p);   


return 0;
 }

说明:

       a、这里有一个比较奇怪的地方,我在dev-c++运行时,我先打印*p的值,然后再打印p的地址,这个可以说明的栈的遗留性,但是当我我把顺序反过来时,它是0,也就是NULL那个值(它在c语言里面其实就是0,它是系统内存当中一块特殊的地址,你最好不要访问它,可能会出现段错误),不过这里我只是仅仅说明栈的遗留性,至于会出现这种情况原因,应该也是跟临时性有关,因为你已经有一次去访问它了,第二次再去访问就不是这个值了,因为它被释放掉了:

然后我在Linux环境下编译,运行后出现了段错误,这个就是我上面程序里面func函数写的注释,最好不要这样去用,说不好就会出现错误 :

      b、栈溢出:

代码语言:javascript
复制
 #include <stdio.h>
 #include <string.h>
 void stack_overflow(void)
 {
int a[10000000] = {0};
a[10000000-1] = 12;
}
int main(void)
{     

stack_overflow();

return 0;
}

说明:

       这里在函数stack_overflow()函数里面,定义的局部变量数组,它的大小超过了栈的大小,所以这段程序运行后会出现段错误:

二、堆:

 1、堆的作用:

           对于堆来讲,它是由我们程序员来自由分配内存大小的,不过你在给一个指针变量分配内存大小的时候,在主程序return  0 语句之前记得要给它释放,否则会出现不好的影响-------内存泄漏。

2、怎样分配和释放内存大小呢?

            在c语言中,我们经常使用malloc来分配内存大小(不过你分配内存的大小不要超过系统内存大小,不然会报错的,一般系统内存大小是4G,至于为啥是这个,读者可以百度一下这个原因,这里我就不讲了),而使用free函数释放之前分配的内存大小,下面是函数原型:

代码语言:javascript
复制
MALLOC(3)                                                     
Linux Programmer's Manual                                                     
MALLOC(3)

NAME
   malloc, free, calloc, realloc - allocate and free dynamic memory

 SYNOPSIS
     #include <stdlib.h>

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

说明:

  (1)void *是个指针类型,malloc返回的是一个void *类型的指针,实质上malloc返回的是堆管理器分配给我本次申请的那段内存空间的首地址(malloc返回的值其实是一个数字,这个数字表示一个内存地址)。为什么要使用void *作为类型?主要原因是malloc帮我们分配内存时只是分配了内存空间,至于这段空间将来用来存储什么类型的元素malloc是不关心的,由我们程序自己来决定。

    (2)什么是void类型。早期被翻译成空型,这个翻译非常不好,会误导人。void类型不表示没有类型,而表示万能类型。void的意思就是说这个数据的类型当前是不确定的,在需要的时候可以再去指定它的具体类型。void *类型是一个指针类型,这个指针本身占4个字节,但是指针指向的类型是不确定的,换句话说这个指针在需要的时候可以被强制转化成其他任何一种确定类型的指针,也就是说这个指针可以指向任何类型的元素。

 (3)malloc的返回值:成功申请空间后返回这个内存空间的指针,申请失败时返回NULL。所以malloc获取的内存指针使用前一定要先检验是否为NULL。

代码语言:javascript
复制
 The  malloc() function allocates size bytes and returns a pointer to the allocated memory.  The memory is not initialized.  If size is 0, then
   malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().

 (4)malloc申请的内存时用完后要free释放。free(p);会告诉堆管理器这段内存我用完了你可以回收了。堆管理器回收了这段内存后这段内存当前进程就不应该再使用了。因为释放后堆管理器就可能把这段内存再次分配给别的进程,所以你就不能再使用了(后面的演示程序里面,你会看到在free后,居然还能够使用;不过一般建议不要这么用)。

(5)再调用free归还这段内存之前,指向这段内存的指针p一定不能丢(也就是不能给p另外赋值)。因为p一旦丢失这段malloc来的内存就永远的丢失了(内存泄漏),直到当前程序结束时操作系统才会回收这段内存。

 3、示例演示:

内存分配的四个步骤:

(1)使用malloc函数申请内存大小。 (2)检验分配是否成功. (3)使用申请到的内存(不过这里使用的时候要注意上面说明里面的第五点)

(4) 使用free函数释放。             

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


    int main(void)
   {
  //    int a=8;
   //第一步:申请和绑定
int *p = (int *)malloc(20);
// 第二步:检验分配是否成功
if (NULL == p)
{
    printf("malloc error.\n");
    return -1;
}
    // 第三步:使用申请到的内存
         //    p = NULL;
       //    p = &a;         // 如果在free之前给p另外赋值,那么malloc申请的那段内存就丢失掉了
                // malloc后p和返回的内存相绑定,p是那段内存在当前进程的唯一联系人
                // 如果p没有free之前就丢了,那么这段内存就永远丢了。丢了的概念就是
                // 在操作系统的堆管理器中这段内存是当前进程拿着的,但是你也用不了

*(p+0) = 1;
*(p+1) = 2;
printf("*(p+0) = %d.\n", *(p+0));
printf("*(p+1) = %d.\n", *(p+1));   



// 第四步:释放
free(p);                // 所以你想申请新的内存来替换使用,这就叫程序“吃内存”,学名叫内存泄漏
*(p+222) = 133;
*(p+223) = 222; 
printf("*(p+222) = %d.\n", *(p+222));
printf("*(p+223) = %d.\n", *(p+223));   //这里可以验证上面说明的第四点 
return 0;
  }

输出结果:

 4、malloc的一些细节用法:

            a、malloc(0)的问题(这个没啥意义,不过还是分享一下)。

 malloc申请0字节内存本身就是一件无厘头事情,一般不会碰到这个需要。

如果真的malloc(0)返回的是NULL还是一个有效指针?答案是:实际分配了20Byte的一段内存并且返回了这段内存的地址。这个答案不是确定的,因为C语言并没有明确规定malloc(0)时的表现,由各malloc函数库的实现者来定义(这个测试了,在不同环境下,确实结果会不一样)。

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


int main(void)
{
    int *p1 = (int *)malloc(0);
  int *p2 = (int *)malloc(0);


  printf("p1 = %p.\n", p1);               // p2-p1 =  0x10 = 20Byte
printf("p2 = %p.\n", p2);
return 0;
  }

输出结果:

三、代码段、数据段、bss段:

  • 编译器在编译程序的时候,将程序中的所有的元素分成了一些组成部分,各部分构成一个段,所以说段是可执行程序的组成部分。

1、什么是代码段?

       代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的(就是函数体里面的程序那部分)。

2、什么是数据段?

     (它也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,直观理解就是C语言程序中的全局变量。(注意:全局变量才算是程序的数据,局部变量不算程序的数据(它在栈上),只能算是函数的数据)。

3、什么是bss段?

     (它又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。

注意:

      数据段(.data)和bss段的区别和联系:二者本来没有本质区别,都是用来存放C程序中的全局变量的。区别在于把显示初始化为非零的全局变量存在.data段中,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量值默认为0)的全局变量存在bss段

4、特殊一些要注意的地方:

a、有些特殊数据会被放到代码段:

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


 int main(void)
 {
     char *p ="linux";
     //最好这样写const char *p="linux";
    *(p+0)='f';//原本改为应该是finux 

        return 0;
  }

演示结果:

说明:

       C语言中使用char *p = "linux";定义字符串时,字符串"linux"实际被分配在代码段,也就是说这个"linux"字符串实际上是一个常量字符串而不是变量字符串。

       const型常量(它的用法之前有讲过,这里就不详细讲了,读者可以看之前的文章):C语言中const关键字用来定义常量,常量就是不能被改变的量。const的实现方法至少有2种:第一种就是编译将const修饰的变量放在代码段去以实现不能修改(普遍见于各种单片机的编译器);第二种就是由编译器来检查以确保const型的常量不会被修改,实际上const型的常量还是和普通变量一样放在数据段的(gcc中就是这样实现的)。

   b、显式初始化为非零的全局变量和静态局部变量放在数据段

          放在.data段的变量有2种:第一种是显式初始化为非零的全局变量。第二种是静态局部变量,也就是static修饰的局部变量。(普通局部变量分配在栈上,静态局部变量分配在.data段)。

        c、未初始化或显式初始化为0的全局变量放在bss段(这里就可以解释开头网友问的问题了)

5、小结:

     (1)相同点:三种获取内存的方法,都可以给程序提供可用内存,都可以用来定义变量给程序用。

     (2)不同点:栈内存对应C中的普通局部变量(别的变量还用不了栈,而且栈是自动的,由编译器和运行时环境共同来提供服务的,程序员无法手工控制);堆内存完全是独立于我们的程序存在和管理的,程序需要内存时可以去手工申请malloc,使用完成后必须尽快free释放。(堆内存对程序就好象公共图书馆对于人,在借书和还书,我们在借书的时候,就从图书馆里借,把借的书看完了,就要归还回图书馆里面去);数据段对于程序来说对应C程序中的全局变量和静态局部变量。

     (3)如果我需要一段内存来存储数据,我究竟应该把这个数据存储在哪里?  (或者说我要定义一个变量,我究竟应该定义为局部变量还是全局变量还是用malloc来实现)。不同的存储方式有不同的特点,简单总结如下:

* 函数内部临时使用,出了函数不会用到,就定义局部变量。堆内存和数据段几乎拥有完全相同的属性,大部分时候是可以完全替换的。但是生命周期不一。堆内存的生命周期是从malloc开始到free结束,而全局变量是从整个程序一开始执行就开始,直到整个程序结束才会消灭,伴随程序运行的一生。启示:如果你这个变量只是在程序的一个阶段有用,用完就不用了,就适合用堆内存;如果这个变量本身和程序是一生相伴的,那就适合用全局变量。(堆内存就好象租房、数据段就好象买房。堆内存就好象图书馆借书,数据段就好象自己书店买书)你以后会慢慢发现:买不如租,堆内存的使用比全局变量广泛。下面做一个示例演示:

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


int a = 5;
int b;//这里是bss段
int c = 0;//这里是bss段
int array[1000];

char str[] = "linux";        // 第二种方法:定义成全局变量,放在数据段


 int main(void)
  {
char a[] = "linux";     // 第一种方法:定义成局部变量,放在栈上

char *p = (char *)malloc(10);   
if (NULL == p)
{
    printf("malloc error.\n");
    return -1;
}
memset(p, 0, 10);       // 第三种方法: 放在malloc申请的堆内存中
strcpy(p, "linux");

printf("%s\n", a);
printf("%s\n", str);
printf("%s\n", p);
printf("%p\n", a);
printf("%p\n", str);
printf("%p\n", p);

   free(p);
return 0;
}

演示结果:

四、总结:

       这里在网上看到一个博客关于栈堆的区别,写的非常不错,分享给大家:https://www.cnblogs.com/hoip/articles/4555388.html。其实关于堆栈的区别,确实要稍微掌握,不管是面试还是工作,你必须要明白这个知识点,反正只有好处,没有坏处,哈哈。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 txp玩Linux 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档