从内存分配、存储类型理解内存管理

文章详细阐述了内存管理的概念,包括内存的硬件分类、程序运行对内存的占用,以及栈和堆的区别。同时,介绍了C语言中的存储类型,如auto、static、extern、const和volatile的关键字用法,以及它们对变量的作用域、生命周期和链接属性的影响。文章还讨论了变量的内存分配,包括全局变量和局部变量的存储位置。
摘要由CSDN通过智能技术生成

1. 内存管理

1. 1 内存是什么?

从硬件的角度:内存是电脑的一个外设,即内存条。分为 SRAM 和 DRAM 两大类。SRAM(静态),掉电丢失,不需要刷新电路,一般不是行列地址复用的,集成度低,不适合做容量大的内存,用于处理器的缓存。DRAM(动态),掉电丢失数据,每隔一段时间刷新一次数据,才能保存数据,而且是行列地址复用的。

从逻辑的角度:内存可以随机访问,只需要一个地址,就可以访问对应的空间内容。

1.2 程序运行对内存的占用

运行程序就是对一些数据运算,得到某些数据结果。

  • 计算机程序 = 代码+ 数据

用函数来类比,函数的形参就是待加工的数据,函数内还需要一些临时的数据,也就是局部变量,函数本体就是代码,函数的返回值就是结果,函数执行的过程就是实现的过程。

在冯诺依曼结构中,数据和代码是放在一起的。例如在三星S5PV210上运行的Linux系统,运行应用程序时所有的应用程序的代码和数据都在DRAM上,所以这种结构就是冯诺依曼结构。而在哈佛结构中,数据和代码是分开存放的。比如在单片机中,我们把程序代码烧写到Flash中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash中,必须放在RAM中。

总结:为什么需要内存呢?内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等,在 gcc 中,其实常量也是存储在内存中的,而大部分单片机中,常量是存储在 Flash 中的,也就是在代码段,对我们写程序来说非常重要,对程序运行更是本质相关。所以内存对程序来说几乎是本质需求。越简单的程序需要越少的内存,而越庞大越复杂的程序需要更多的内存。内存管理是我们写程序时很重要的话题。数据结构是研究数据在内存中如何组织的,算法是为了用更优秀更有效的方法来加工数据,既然跟数据有关就离不开内存。

一般内存主要分为:代码区、常量区、静态区、堆区、栈区。

  • 代码区:存放程序的代码,即CPU执行的机器指令,并且是只读的。存放在最底层。
  • 常量区:存放常量(程序运行期间不能被改变的量,例如:数字常量10、字符串常量“name”、数组名等)
  • 静态区(全局区):静态变量(全局和局部)和全局变量的存储区域是在一起的,一旦静态区的内存被分配,静态区的内存直到程序结束才会被释放。
  • 堆区:程序员手动申请的空间,需要释放,如果忘记释放,会造成内存泄漏。
  • 栈区:存放函数内的非静态局部变量、形参和函数返回值。栈区的数据生命周期结束,系统会自动回收资源。
image-20230409191925171

1.2 栈 (stack)

C语言中用栈来保存局部变量,栈的特点是LIFO,后入先出,只在一端进行入栈和出栈操作。

栈管理内存的特点:管理小内存,申请释放自动化。分配和回收都不需要程序员操心,但是空间大小有限。每次使用的栈区都是同一块空间,上一次程序(不是整个进程结束,某个函数)使用完不会去清理,所以再次使用堆区时需要对变量进行初始化。栈区的空间使用完就被释放了,所以不能返回栈变量的指针。

1.3 堆(heap)

堆的内存管理特点是:使用灵活、随时申请、释放,并且申请内存块的大小随意,空间比较大,但是容易出错。申请和释放的过程需要程序员来手动完成,没有释放之前一直存在,直到程序结束。对堆区操作的API有动态申请内存函数 malloc() 和释放内存函数 free() ,或者由 new 操作符分配内存,delete释放内存,生命周期由free和delete决定。

1.4 变量的内存分配

当在程序中声明一个变量时,编译器会给变量预留内存空间,这种预留内存空间的过程就叫分配。

**全局变量:**在程序开始时分配,生命周期时整个程序运行的时间,程序结束后才会释放内存。在静态区分配。

**局部变量:**只有当函数被调用时才会分配内存,变量本身在分配给这个函数的存储空间中分配,即函数中的变量占用函数体分配的内存空间。函数的存储空间叫做函数的帧(frame)。只要函数在运行,局部变量在阵中的地址就始终不变。当函数返回时,帧以及它的所有变量都会被丢弃,以供其他函数使用。在栈区分配。

在下列分区中分别存储不同声明的变量

  • data 段:存放显式**初始化为非0(已初始化)**的全局变量、static局部变量。
  • bss 段:存放显式初始化为0或者未初始化的全局变量和未初始化的 static 局部变量。
  • 栈:存放非静态局部变量
image-20230409191802185

注:在这里的内存分配图和上面的内存分配有些区别,参考过不同的书,书上也不统一。这内存分配是在学习过程中老师讲解的,在这里都列举出来了。

2. C 存储类型

2.1 C语言存储类

变量的存储类就是存储类型(storage class),也就是描述存储C语言变量值的内存类型。在前面内存管理中可以了解到:内存有多种管理方法,比如栈、堆、数据段、bss段、.text段等,一个变量的存储类属性就是描述这个变量存储在何种内存段中。

举几个例子:比如局部变量分配在栈上,所以它的存储类型就是栈;显式初始化为非0的全局变量分配在数据段;显式初始化为0和没有显示初始化(默认为0)的全局变量分配在bss段。

2.2 作用域

作用域是描述一个变量起作用的代码范围。当变量在程序的某个部分被声明时,它只有在程序的一定区域内才能被访问,这个区域就是它的作用域。

基本来说,C语言变量的作用域规则是代码块作用域,意思就是这个变量起作用的范围是当前的代码块。代码块就是一对大括号{}括起来的范围,所以一个变量的作用域是:这个变量定义所在的{ }范围内从这个变量定义开始往后的部分,这就解释了为什么变量定义一般都在一个函数的最前面。

2.3 生命周期

生命周期声明周期是描述这个变量什么时候诞生,即运行时分配内存空间给这个变量和什么时候死亡,即运行时收回这个内存空间,此后再不能访问这个内存地址,或者访问这个内存地址已经和这个变量无关了的。

2.4 链接属性

程序从源代码到最终可执行程序经历的过程主要有:预处理、编译、汇编、链接。编译和汇编阶段就是把源代码生成.o目标文件,目标文件里面有很多符号和代码段、数据段、bss段等分段,符号就是编程中的变量名、函数名等。运行时变量名、函数名能够和相应的内存对应起来,靠符号来做链接的。.o的目标文件链接生成最终可执行程序的时候,其实就是把符号和相对应的段给链接起来。

C语言中的符号有三种链接属性:external(外部)internal(内部)和 none。one也被称为没有链接属性的标识符 。

**none属性的总会被认为是独立的个体,该标识符多次被声明会被当做多个独立不同的实体。例如,多个代码块中声明的同一名字的局部变量或静态局部变量。**具有块作用域、函数作用域、函数原型作用域的变量都是无链接变量。

**external:**指的是在不同的源文件中表示同一个实体。具有外部链接的标识符不管声明多少次,位于几个源文件中,都表示同一实体。 外部链接变量可以在多个文件程序中使用,内部链接变量只能在一个翻译单元中使用。

**internal:**指的是在同一个源文件中表示同一个个体。同一源文件名字相同,具有内部链接的标识符会被当做同一实体。但是,不同源文件名字相同,具有内部链接的标识符会被当做不同实体。

具有文件作用域的变量可以是外部链接变量或内部链接变量。关键字 extern 和static 用于在声明中修改标识符的链接属性。

2.4.1 static 修改链接属性

可以 static 关键字将 external 链接属性的标识符修改为 intrnal 链接属性。

int a; // 此时a的链接属性为external
char name; // external
// 在代码块内部的变量标识符链接属性为 none
{
    int b; //none
    shout c; //none
}
// 此时对 a 使用 static 修饰就可以修改它的链接属性
static int a;
// 也可以修饰一个函数的链接属性
static int func(int a, int b) {
    return a+b;
}

不过 static 只对缺省链接属性为 external 的标识符才有改变属性的效果,因此对b和c使用 static 修饰达不到想要的效果。

2.4.2 extern 修改链接属性

使用 extern 即可以修改 none 也可以修改 internal 的标识符链接属性。

int a;
{
    extern char c;
}
// 若标识符是第二次声明是使用的extern 则不会修改其链接属性
static int d;
{
    extern int d; // 此时不会修改a的链接属性 
}

2.5 C 存储类型关键字

2.5.1 auto (自动变量)

auto 存储类时所有局部变量默认的存储类型,用在函数内,只能用来修饰局部变量。在栈区分配,不初始化就是随机值。一般隐藏 auto 默认为自动存储类别。我们程序都变量大多是自动变量。

2.5.2 static (对外不可见)

  • static 存储类修饰局部变量时,指示编译器在程序的生命周期内保持局部变量的存在,即延长局部变量的生命周期至程序结束,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
  • static 修饰全局变量时,会使该变量限制在声明它的文件内,不能被外部文件使用。
  • 静态变量在程序中只被初始化一次,即使在循环中使用 static 声明变量,也只在第一次进行初始化。

2.5.3 extern (表明外部定义)

  • 外部的,用于定义在其他文件中声明的全局变量或函数。当使用 extern 关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。
  • 用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

例如,在 first.c 中定义了全局变量 name ,在 second.c 中要使用 name 变量,就只需在 name 前加上 extern 修饰关键字。

2.5.4 const (只读变量)

const 限定一个变量不允许被改变,产生静态作用。使用const在一定程度上可以提高程序的安全性和可靠性。

  1. const 修饰的变量属于常量类型,具有不可变性,但是可以通过指针修改指向的内容。
int main() 
{
	const int max = 100;
	max++;  //产生错误,常量不允许被赋值
		//可以通过指针修改max的内容
	int *p = (int *)&max;
	*p = 102;
	printf("a = %d",a);  // a = 102
	printf("*p = %d",*p); // *p = 102
}
  1. 可以定义全局变量,全局变量只读,不可以被修改;

    #include <stdio.h>
    // 修饰全局变量
    const int b = 30;
    int main()
    {
        // 修饰局部变量
        const int a = 10;
        int *p = (int *)&a; 
        int *q = (int *)&b;
        //a++; //常量不可以被赋值
        *p = 20;   // 局部变量可以通过指针修改
        //*q = 50;  // 会出现段错误,const修饰的全局变量不能被修改
        printf("a = %d\n",a); //20
        printf("*p = %d\n",*p);//20
        printf("b = %d\n",b); //30
        printf("*q = %d\n",*q);//30
        return 0;
    }
    
  2. const 修饰指针

    	const int *p;		// const修饰*p
    	int const *p;		// const修饰*p 前两种方式一致
    	int *const p;		// const修饰p
    	const int *const p; // 第1个const修饰*p,第2个const修饰p
    区分方法:const*之前就是修饰*p 内容不可更改,指针指向可以更改
             const*之后就是修饰p 指针指向不能变,指针的内容可以更改
        	 const **后都有,指针内容和指针指向都不允许修改
    
  3. const 修饰函数形参或者返回值

    //-修饰函数的形参
    //1.值传递不需要修饰
    //2.地址传参使用指针时防止指针被意外改动,使用const修饰
    int strcmp (const char *sstr,const char *dstr);
    //3.自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。并且对于一般的 int、double 等内置类型,我们不采用引用的传递方式(C++程序代替)
    #include<iostream>
    using namespace std;
    class Test
    {
    public:
        Test(){}
        Test(int _m):_cm(_m){}
        int get_cm()const
        {
           return _cm;
        }
    private:
        int _cm;
    };
    void Cmf(const Test& _tt)
    {
        cout<<_tt.get_cm();
    }
    int main(void)
    {
        Test t(8);
        Cmf(t);
        system("pause");
        return 0;
    }
    
    //-修饰函数返回值
    //1.当const 修饰内置类型的返回值的时候,与不用修饰一样
    const int CMd() 
    {
        return 0;
    }
    int CMo() 
    {
        return 0;
    }
    //2.const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
    
    //3.const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
    

在 C++ 中,const 有一种不同的用法:当 const 存储类修饰一个变量 a 并用常量10初始化时,是指示编译器 a 是一个符号常量,即用 a 标识符指代常量10,会在编译阶段将所有 a 替换成10。之后也不能再对 a 进行赋值修改(编译器将给常量赋值认定为违法), 但可以将 a 赋值给其他变量。

g++编译器的语法要求:const 修饰的变量必须初始化。

const int a = 10;
int b = a; // 正确
a = 8;		//不允许
int *p = (int *)&a;
*p = 20;  // 此时20已经覆盖了原来a地址中的10

cout << a << endl; // 输出结果10 编译阶段直接将a替换成10输出
cout << *p << endl;// 输出结果20 可以对a的地址指向的空间赋值

// 当使用volatile修饰时 
const int volatile a = 10;
int *p = (int *)&a;
*p = 20;  // 此时20已经覆盖了原来a地址中的10

cout << a << endl; // 结果20 用volatile修饰之后会从a的原地址读值输出
cout << *p << endl;// 输出结果20 可以对a的地址指向的空间赋值

2.5.5 volatile (可变的、易变的)

volatile 与 const 对应,用它修饰变量表示该变量可以被某些编译器未知的因素更改,比如:在中断处理程序中更改了这个变量的值,或者在多线程中在别的线程更改了这个变量的值,或者是硬件自动更改了这个变量的值(一般这个变量是一个寄存器的值)。

以上说的三种情况(中断ISR中引用的变量,多线程中共用的变量,硬件会更改的变量)都是编译器在编译时无法预知的更改,此时应使用 volatile 告诉编译器这个变量属于这种可变的、易变的情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。从而可以提供对特殊地址的稳定访问。

声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

一般说来,volatile用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
  2. 多任务环境下各任务间共享的标志应该加 volatile;
  3. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

2.5.6 register (寄存器类型)

register : 寄存器类型, 在实际开发中使用并不多。
1> 定义寄存器类型的变量时,对寄存器类型的变量进行读写访问,
	使用时和普通变量没有任何的区别。
2> 在实际开发板中,尽量不要定义太多的寄存器类型的变量,
	原因:CPU内部寄存器的个数有限
3> 定义的寄存器类型的变量不可以进行取地址运算,
	原因:寄存器没有地址
4> 定义的寄存器类型的变量比普通的变量读写访问速度块,
	原因:寄存器的读写速度比内存的读写速度块。
	
	疑问: 
		1. 为什么寄存器的个数有限?
		2. 为什么寄存器类型的变量不能取地址?
		3. 什么是寄存器?
	以上三个问题在《ARM体系结构及接口技术》时进行详细讲解。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    register int a = 100; // 定义寄存器类型的变量
    int b = 200; // 定义普通的变量

    // 报错,寄存器类型的变量不可以进行取地址运算
    // printf("&a = %p, a = %d\n", &a, a);
    printf("a = %d\n", a);
    printf("&b = %p, b = %d\n", &b, b);

    return 0;
}

clude <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
register int a = 100; // 定义寄存器类型的变量
int b = 200; // 定义普通的变量

// 报错,寄存器类型的变量不可以进行取地址运算
// printf("&a = %p, a = %d\n", &a, a);
printf("a = %d\n", a);
printf("&b = %p, b = %d\n", &b, b);

return 0;

}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

赶路人_ax

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

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

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

打赏作者

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

抵扣说明:

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

余额充值