C语言:聊聊内存的管理方式1~栈、堆和数据区

1、前言

因为在校的原因,C语言学了快两年,但偶尔没敲代码,一下就忘了,特意弄个C语言的专题,记录一下C语言中那些耐人寻味的东西。大纲来自朱有鹏老师的C语言内核深度解析,并参考一些经典的C语言书籍,顺序完全是根据自己需要来排版,特此鸣谢。

2、直奔主题–内存

内存真的是一个聊不完的东西,不管是构成其的器件RAM,和CPU的关系,还是其管理方式等,是很多人的痛点。首先时刻铭记一句话:计算机中的程序都是在内存中运行的(程序包括代码和数据),内存对计算机的性能影响非常的大,只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完后CPU再将结果传出来。因此,在C语言中,所有的变量空间都必须从内存中开辟出来。

不管由软件实现的内存管理有多复杂,可以肯定的是,程序需要的内存都是来自于物理内存。但是如今,在操作系统的计算机上,为了实现内存的高效利用,操作系统对所有的物理内存进行了统一的内存管理,所有应用程序表现出来的都是虚拟内存

为了使内存管理更加合理,操作系统提供了多种机制来管理内存。这些机制各有特点,程序根据实际情况来选择某种方式获取内存(向操作系统处登记这块内存的临时使用权限)、使用内存和释放内存(向操作系统归还这块内存的使用权)。

3、管理方式–栈stack、堆heap和数据区

在C语言程序中,存放数据所能使用的内存空间大概分为四种情况:栈stack、堆heap、数据区(.data和.bss区)和常量区(.ro.data)
备注:内存的管理其实是对程序而言,而程序包括了代码和数据。

4、先聊聊栈

01 栈内存特点

(1) 空间实现自动管理:程序运行的时候,空间会自动分配,结束时会自动回收。栈是自动管理的,程序员不需要手动干预,方便简单,栈因此还称为自动管理区。

(2) 能够反复使用:栈内存在程序中用的都是同一块内存空间,程序通过自动开辟和自动释放,会反复的使用这一块空间。

(3) 使用后是脏内存:由于栈反复使用,每次使用后程序不会去清理内容,因此当下一次该空间被分配时,上一次使用的值还存在,这也是我们局部变量为什么使用的时候需要初始化的原因,因为里面的值是不确定的。

(4) 临时性:函数不能返回栈变量的指针,因为这个空间在函数运行结束之后就会被自动释放。上面这句是原话,可以理解为函数调用的时候其实是在栈中进行的,这个函数执行完,由于执行的位置其实是随机的,函数每次执行没有确定的栈变量指针。

02 看看实例代码

#include <stdio.h>
int *func(void)  // 返回的是一个int型的指针,return的应该是个地址
{
	int a = 4;  // a是局部变量,局部变量是分配在栈上的,又叫临时变量
	printf("&a = %p\n", &a);
	return &a;
}
void func2(void)
{
	int a = 33;
	printf("in func2, &a = %p\n", &a);
}
int main(void)
{
	int *p = NULL;
	p = func();  // p中存放的是函数调用后局部变量a的地址,但调用完a的内存就被释放了
	func2();  // 
	func2();
	printf("p = %p\n", p);
	printf("*p = %d.\n", *p); // 运行之后结果是*p=33,是脏、临时的内存
	return 0;
}

这是我在gcc上执行的结果
&a = 0xbfc49b6c
in func2, &a = 0xbfc49b6c
in func2, &a = 0xbfc49b6c
p = 0xbfc49b6c
*p = 33.
另外,需要加以说明的是,因为操作系统实现给定了栈的大小,如果在函数中无穷尽的分配局部变量(包括无休止的递归函数),都会使得栈内存用完,栈溢出,产生内存泄漏的事故。

5、再看看堆

01 堆内存的特点详解

(1) 灵活:是一种管理形式的内存区域,管理十分灵活。

(2) 内存量大:堆内存空间很大,进程可以按照需要手动去申请,使用完后手动释放。

(3) 程序手动申请和释放内存:手动表明需要写代码去管理,申请:malloc、释放:free。

(4) 脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此是脏的。

(5) 临时性:堆内存在malloc后和free之前这段期间内可以被访问,在malloc之前和free之后是不能被访问的,否则会有不可预计的后果。

02 实例代码

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	// 申请一个1000int类型元素的数组
	// 第一步:申请和绑定
	int *p = (int *)malloc(1000 * sizeof(int));
	// 第二步:检验申请是否成功
	if (NULL == p)
	{
		printf("malloc error.\n");
		return -1;
	}
	// 使用申请到的内容
	*(p + 0) = 1;
	*(p + 1) = 2;
	printf("*(p+0) = %d\n*(p+1) = %d\n", *(p+0), *(p+1));
	// 第四步:释放
	free(p);
	return 0;
}

这是我在gcc上执行的结果
*(p+0) = 1
*(p+1) = 2

03 备注:

(1) 这个程序中,如果最后没有将分配的堆内存空间释放出来的话,这个内存空间会被一直占用,只有整个程序终止之后才会释放。所以对于堆内存而言,使用完后用free释放空间非常的重要,否则也会造成内存泄漏(内存空间还在,但是被占用,后续无法再使用,名存实亡)。

(2) malloc返回的是 void * 类型的指针:在使用malloc的分配内存的时候,我们并不知道这段空间具体是用来存放什么类型数据的。类型是在后续使用这些空间存放具体类型数据的时候决定的,所以用空指针类型表示不确定,或者认为该地址指向的空间可以存放任何数据类型,由具体存放的数据类型决定。

(3) malloc的返回值:空间申请成功之后返回的是空间首字符的地址,申请失败则返回NULL,因此,malloc获取的内存指针使用前一定要先检验是否为NULL!

(4) 手动释放:malloc申请的内存用完之后,需要free手动释放。释放后堆管理器就可以把这段内存再次分配给别的使用者。

(5) 细节1:在调用free归还p所指向的堆内存之前,如果p的指向发生了改变,指向其他地方的话,必须通过一个中间指针变量先记住p指向的堆内存,之后free时才能通过这个中间指针变量释放之前p所指向的堆空间,否则就会造成之前p指向的堆空间无法释放,从而导致内存泄漏

(6) 细节2:
如果申请malloc(0)返回的是NULL还是一个有效指针?答案是不确定的,申请0字节空间本身就是一件无意义的事情,如果这么做了,得具体看malloc函数库的实现者对其的定义;
如果在GCC中申请malloc(4)结果是什么?因为GCC中默认最小使用的是以16B为单位进行空间分配的。如果指定的空间小于16B的话,会按照16字节进行空间分配。

6、最后看看段

01 代码段、数据段、bss段

编译器在编译程序的时候,程序会按照一定的结构,被划分为各个不同的段进行组织,这些段有代码段.text、数据段.data、.bss段。

代码段.text:存放程序的代码部分,程序中各个执行就放在这里面
数据段.data:又称数据区、静态数据区、静态区,程序中的静态变量空间就开辟于此,需要注意的是,全局变量是整个程序的公共财产,而局部变量只是函数的私有财产。
.bss段:又叫ZI(Zero Initial)段,所有未初始化或初始化为0的静态变量的空间就开辟于此,这个段会自动将这些未初始化静态空间初始化为0。

备注:数据段和.bss段本身实际上没有区别,都是用来存放程序中的静态变量,只不过.data中存放的是显式初始化为非0的静态数据,而.bss中存放的那些显示初始化为0或者为显示初始化的静态数据。

02 C语言中特殊数据会被放到代码段

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

另外,在C语言中常常会使用const这个关键字来定义常量,常量是不能被修改的量,常量实现方法至少有两种:一是在编译的时候,将const修饰的变量放在代码段中,以达到不能修改的目的,因为代码段都是只读的,在单片机开发中,这种情况非常的常见。二是让编译器来帮忙实现,如果编译器在编译时,检查到变量被const修饰,当发现程序试图去修改该变量时,编译器就会报错。本质上,const型的常量还是和普通变量一样,都被放在了数据段(GCC中其实就是这样实现的)

7、内存管理的总结1(个人)

01 内存管理内容

内存管理其实是对程序的管理,包括两部分,代码管理和数据管理,比较需要注重的是数据的管理,代码管理没有多大的变动。

02 内存管理方式

代码管理:代码段.text(存放程序中代码部分)
数据管理:栈stack、堆heap、数据段.data、.bss段、代码段(当存放常量的时候)
栈:存放局部变量、自动内存管理、未初始化时为脏内存
堆:需要在程序中自己手动申请,malloc申请,free释放
数据段:存放的是初始化了非0的静态变量
.bss段:存放的是未初始化或者初始化为0的静态变量
代码段:存放程序中的代码(指令)
备注:常量是一个需要区分对待的东西,根据不同系统的处方式而定,可以存放在代码段上,使得编译前就是不可修改的;也可以存放在数据段,一旦修改,让编译器来提示错误。

03 灵活分配内存给数据的思想

(1) 如果只是在函数内部临时使用,作用范围希望在函数内部,则定义局部变量

(2) 如果变量只是在程序的一个阶段期间有用,非常适合使用堆内存空间;如果变量需要在程序运行的整个过程中一直存在的话,适合使用全局变量(静态全局变量)。
原因:堆内存和数据段几乎具有完全相同的属性,为什么这么说呢?举个例子:如果是定义的局部变量,那么就只在定义处的函数块中有作用,每次调用这个函数的时候都会在栈中重新进行分配,如若不初始化,其实值是不确定的,是栈进行的自动管理。但是堆和数据段不一样,两者的生命周期不同,堆内存从malloc申请开始到free结束,数据段中的静态变量从程序一开始执行到程序结束,伴随程序一直存在。相应的这段期间里,我们可以自己对这段内存的数据进行操作,而不是被动的去让操作系统自动管理。

备注:其实这篇文章里面还涉及了一些其他知识,比如静态变量,全局变量,虚拟物理内存等,下回再弄。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学不懂啊阿田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值