5-程序与内存关系

mac2025-06-08  47

一、程序与内存

1. 概述

内存是程序的载体,程序的全局变量和局部变量都存储在内存中,因此需要内存提供程序运行环境。 注意:程序的代码可以在rom中运行,并不一定必须在内存中,但变量则必须在内存中,因为ROM只读。内存由操作系统统一管理(裸机需自己分配),程序根据自己的特点,通过操作系统提供的多种机制,来申请获取内存的临时使用和释放;程序通过三种方式获取/使用内存:栈(stack)、堆(heap)、数据区(.data);

2. 栈的详解

栈内存就是操作系统自动分配的一块特定内存区域,用来存储程序运行时的局部变量、返回值、形参等:某个程序运行时,则为该程序的临时数据在栈中分配空间,该程序执行完则释放空间(通过栈指针上移但不清空数据)。

栈的使用和释放是由操作系统通过操作栈指针,根据后进先出的原则自动管理的,无需手动操作;

由于栈是不同程序反复使用且使用完不清空数据,即在执行完某个程序时,栈内存中依然存有上个程序的数据,因此栈内存是脏的;

由于栈中的数据是临时性的,因此函数结束时的返回值不能是存储在栈指针或通过栈指针间接访问栈变量的值;(语法上无错,但实际上无意义)

栈空间的内存有限(大约10k以内),因此若有大量临时变量时,未防止栈溢出产生段错误,需手动申请堆内存;

int* func1(void) { int a = 5; //存储在栈中 printf("&a = %p\n", &a); //打印a地址 return &a; //返回栈指针 } int* func2(void) { int a = 123; printf("&a = %p\n", &a); return &a; } int main(void) { int *p; p = func1(); //p = &a printf("&a = %p\n", p); p = func2(); //p = &a printf("&a = %p\n", p); printf("*p = %d\n", *p); } /*3个&a的值相同:执行func1和执行func2分配a的空间是恰巧同一空间 /* *p的值不确定:因为执行不同的程序会将该地址的数据污染

3. 堆与栈的对比

申请与释放 (1)栈是由操作系统根据程序的代码,自动分配内存地址以及大小和释放; (2)堆是由程序员自己通过malloc和free来申请和释放。

空间的分配 (1)栈是在当前程序运行时,操作系统给每个程序分配的空间,仅当前的程序可以用,别的程序无法使用; (2)堆是在程序运行时,操作系统根据每个程序申请的空间大小,从所有程序共用的空间中分配内存当前程序,使用完成后释放空间。

脏内存 栈和堆都是反复使用,且使用后不清除内存,因此都是脏的;

临时性 (1)对堆内存的使用,仅在当前程序的malloc和free函数之间使用,在malloc前和free后都不应该再使用; (2)而栈的使用也不应该将函数结束时的返回值返回(无语法错误,但无实际意义)。因为操作系统会随时将该块内存分配给别的程序。

4. malloc和free函数

语法:type *pc = (type *)malloc(n * sizeof(type)); free(pc);

malloc函数 malloc函数将(n * sizeof(type)大小的内存空间的首地址(void *)赋值给pc,但具体这块堆内存里可以存放什么样的数据类型,取决于(type *)的强制类型转换。 如:int* pi = (int*)malloc(100 * sizeof(int)); //分配100个int类型的空间

malloc按块(最小16byte)分配内存,即每次分配的内存都大于实际申请的内存;

malloc使用的步骤: 申请:malloc函数; 检验:若申请堆失败,malloc会返回NULL,成功则返回堆的首地址; 使用:由于堆是内存中一块连续的空间,从内存的角度看和数组无本质区别,因此可以像数组那用通过指针的方法访问堆内存 释放:free函数;

int func2(int pi) { if(pi == NULL) { printf("error!\n"); return -1; } return 0; } int main(void) { int *pi = (int *)malloc(100 *sizeof(int)); //申请 func2(pi); //检验 *(pi +5) = 10; //使用 free(pi); //释放 }

5. 段的概念

编译器在编译程序时,会将程序分解成.text段、.data段、.bss段;text段是程序中可执行部分,即函数的集合;.data段和.bss段本质无区别,存放的都是全局变量,只是.bss段存放的是显式初始化为0和未初始化的全局变量,程序中未初始化的全局变量默认是0是因为编译器将未初始化的全局变量也放在.bss段中。.data段存放的是显式初始化非0的全局变量或static修饰的静态局部变量;有些特殊的数据如字符串常量,并未存放在数据段,而是存放在代码段。 int func1(void) { char *pc = "linux" //pc指向字符串的首个字符地址 *(pc + 0= f; //修改‘l’为‘f’ printf("*pc = %s\n", pc); //发生段错误 }

二、变量与内存的关系

1. 概念概述

存储类 存储类就是存储类型的位置,即变量在内存中的哪个地方存储,如:局部变量的存储类是栈;初始化为0或未初始化的全局变量的存储类是bss段;初始化非0的全局变量的存储类是.data段;

作用域 作用域是指变量起作用的代码范围,一般的作用域是当前的代码块(当前变量定义之后{}之间的范围)作用域,因此变量的定义一般都放在当前代码块的最前面。

int a = 10; //全局变量 void func(void) { printf("%d\n", a); //此时a = 10 } int main(void) { int a = 5; //局部变量 func(); if(1) { a = 1; printf("%d\n", a); //此时a = 1 } printf("%d\n", a); //此时a = 1 return 0; }

生命周期 生命周期是指运行时从函数给这个参数分配内存空间,到该内存空间失效(无法访问或已重新分配给其他参数);

链接属性 程序在生成可执行文件过程中,需要经过编译和链接,编译后的.o文件里面包含了很多符号、代码段、数据段、bss段等分段;符号就是参数名及函数名等,通过这些符号,将不同的.o文件链接在一起;

linux C的内存映像 (1)只读段:代码段:可执行的程序;只读数据段:只读数据,const修饰的数据(平台不同也不一定); (2)读写段:初始化不为0的全局变量及static修饰的局部变量;bss段:初始化未0或未初始化的全局变量及static修饰的局部变量; (3)堆:存放的变量取决于程序员自己的malloc和free; (4)文件映射区:程序运行是在内存中运行的,因此需要先将程序从flash中读取内存映射区,然后程序在内存映射区执行,将结果保存到flash中; (5)栈:局部变量、实参、返回值,都会保存在栈中; (6)内核映射区:将操作系统的内核映射到该区域,由于采用虚拟内存技术,因此每个进程都认位独享3G内存,0xC0000000以上的区域属于内核;

裸机C程序与OS下的C程序的区别 C代码的运行需要环境,裸机中的代码运行需要自己搭建运行环境,如清.bss,设置栈,调用main函数等;而在OS的状态下操作系统已经将该环境搭建好,可以直接运行C代码。

2. 存储类详解

auto关键字 auto关键字只有一个作用,就是修饰局部变量,成为自动局部变量。平时定义局部变量时,默认定义为自动局部变量,存储在栈中,即自动将auto关键字省略;

static关键字 static关键字有两个作用: (1)修饰全局变量,成为静态全局变量。静态全局变量与普通全局变量的区别在链接属性上,见下面链接属性详解; (2)修饰局部变量,成为静态局部变量。静态局部变量与普通局部变量的区别在存储类与生命周期上:静态的存储类是.data段或.bss段,普通的是栈;静态的生命周期和全局变量一样,因此下一次执行某函数时,该函数内静态局部变量的值依然是上一次该函数执行的结果,普通的是函数结束; (3)静态局部变量与普通全局变量的对比:二者存储类和生命周期相同,但作用域与链接属性不同:静态局部变量的作用域是代码块作用域,和普通局部变量相同,而全局变量是文件作用域,和函数相同;静态局部变量的链接属性是无链接属性,而全局变量是外链接属性;

register关键字 (1) register关键字只有一个作用,就是编译器会将修饰的变量尽量分配在寄存器中,而非内存中。若变量分配在寄存器中,可以和普通变量同样使用,但可以极大地提高该变量的访问效率,一般用来修饰访问频率极高的变量。 (2)但修饰过的变量不一定真正的能分配到寄存器中,因为寄存器个数有限,可能还是会分配到内存中。

extern关键字 (1)extern关键字的作用是声明全局变量,可以跨文件引用同一个全局变量。当在a.c中定义了一个全局变量int a = 5;,在b.c文件中,就可以通过extern声明变量a,extern int a;从而使b.c文件也可以同a.c一样使用变量a; (2)由于编译时,是以源文件为单位,因此当b.c未声明变量a就直接使用的话,编译器会因为无法识别而报错;而声明后就是告知编译器将在链接时将该变量链接到相应的文件;

volatile关键字 字面意思时易变的、可变的;在C中的作用就是提醒编译器不要优化某个参数或程序的运行,否则可能导致程序会出错:

int func() { int a, b, c; //voiatile int a, b, c; a = 5; b = a; c = b; return c; } 当未加volatile关键字修饰a,b,c时,编译器为了提高程序运行效率, 会优化程序,即程序只执行一行代码,让c == 5,直接return 5, 但若在`a = 5`和`b = a`之间因为突发中断、其他线程、硬件等的原因而修改了a的值, 由于编译器的优化,返回值仍会是5;而添加volatile后,程序会逐条执行程序, 即当a的值改变了后,在执行`b = a`时,会将a的新值赋给b,避免程序运行错误; restrict关键字 restrict只用来修饰指针参数,其他类型无意义。作用是告知编译器,该指针所指向的内容,若有修改必须基于该指针,其他方式无法修改该指针指向的内容,以此来提高编译器效率; int func (int* x, int* y) //int func(int * restrict x, int * restrict y) { *x = 0; *y = 1; return *x; } 编译器为了保证正确,在`return *x`仍然会访问*x,而不会`return 0`; 而加了restrict关键字后,告知了编译器只能x才能访问*x,其他方式无法修改*x, 因此编译器可以放心大胆的直接renturn 0,提高了效率;

3. 作用域详解

局部变量作用域 局部变量的作用域是代码块(即大括号{}之间的部分)中,自己定义后的部分。由于函数中还可能包含判断if或for、while等语句也有大括号,因此代码块小于等于函数;代码块中局部变量定义之前并非其作用域。

全局变量和函数名作用域 (1)全局变量和函数名的作用域是文件,即整个.c文件中,在该全局变量或函数定义了之后,都可以访问该变量或函数名。 (2)全局变量和函数定义之前并非作用域,在函数定义之前访问可以通过声明,声明之后的范围也属于函数的作用域;

同名掩蔽原则 (1)若同名文件作用域未交叠,则相互之间无影响; (2)若同名文件作用域有交叠,则在交叠的地方,作用域小的屏蔽作用域大的。

4. 生命周期

局部变量 普通局部变量存储在栈中,生命周期是临时的,函数的每次执行,都会分配、使用、释放一次该变量的内存,每次之间无关联;

堆变量 (1)堆内存空间是客观存在的,由操作系统维护。代码只是根据需要申请一块空间使用然后释放,释放后堆内存仍然存在; (2)堆变量的生命周期是在malloc和free之间,以外的时候无法访问原来的数据;

数据段、bss段变量 全局变量的生命周期从程序开始执行到程序结束都存在。全局变量所占用的内存无法自己释放,因此若程序定义了过多全局变量,会一直占用大量的内存。

代码段、只读数据段 代码段即程序执行的代码,其实就是函数,根据平台不同,有时也会存放const修饰的变量和字符串常量。其生命周期也是持续到整个程序结束。

5. 链接属性

C工程的组织架构 (1)一个工程代码由众多.c文件与.h文件组成,程序生成就是通过编译和链接:编译将每个源文件编译生成.o文件,链接就是将多个由编译生成的.o文件链接在一起生成这个工程的可执行程序; (2)链接的标志就是函数名、变量名等符号,如a.c中调用了b.c中func1()函数,在链接时,连接器就会通过a.o中的func1函数名的符号,到b.o中找func1函数名的符号,找到后将b.o中的func1与a.o的func1链接在一起,程序运行时,b.o的func1函数名作为指针跳转到函数体中执行程序;

三种链接属性 (1)外链接:外链接就是可以跨文件链接,如普通全局变量和函数,通过extern声明或.h头文件就可以在整个工程内使用; (2)内链接:内链接就是只能在当前所在的.c文件内链接,如static修饰的全局变量和函数,只能在本文件内被使用,而无法跨文件; (3)无链接:无链接就是本身不参与链接,如所有的局部变量,宏,inline函数;

全局变量与函数的同名冲突 (1)由于全局变量与函数是外链接属性,因此同一个工程之中之间不能出现同名的全局变量/函数; (2)若两个.c文件中各自定义了一个名字相同的全局变量,并都进行了初始化,系统认位定义了两个同名的全局变量; (3)但若有一个或两个都未初始化,则系统会认位只是定义了一个全局变量,而将另一个.c文件中未初始化的全局变量作为声明,此时并不会报错误; (4)为了避免同名冲突,可以通过static修饰全局变量/函数,更改其链接属性为内链接,但所有该命名的全局变量/函数都无法进行外链接;

三、数据类型转换

1. 不同数据类型的分类

按照打印方式的兼容性可分为:%d/c:char、short、int、long;%f:float;%lf:double 三种类型;

以上三种不同的类型,互相之间在内存中的二进制存储方式和空间大小以及取出的解析方式各有不同。因此在使用强制类型转换或取出时的格式错误会导致输出乱码。

%d/c:所占的存储空间大小和解析方式不同,但在内存中二进制存储的方式相同,因此在强制类型转换或输出格式错误时,数据是否损失取决于输出强制的类型所占的字节数,大转小会造成数据损失;

2. 指针的数据类型

指针本身只是数据类型其中只有一种,区别只是其指向的地址中的数据类型。因此相互之间强制类型转换是否出错取决于上述的三种类型之间的关系;所有指针类型变量的内存大小都是4字节,同时都按照地址数据的方式解析,打印用%p。
最新回复(0)