堆和栈
栈(stack):这个主要是存放代码执行顺序的数据,比如我们要执行一个函数,首先函数首地址入栈,其次函数参数入栈,当函数执行的时候,会按照代码执行顺序把函数内的变量值入栈,如果遇到的是局部变量,那么入栈的时候才会给他们分配内存,这里对于局部变量又要做一下区分,如果是局部普通变量则会在栈上直接分配内存,如果是new出来的对象,则会在堆中分配内存,然后将地址放到栈上,如果遇到的是全局变量或者静态变量,则直接把值存在栈上,这个在代码编译阶段就已经分配好内存了,如果遇到的是const定义的常量,则把常量区的值存放到栈上,
注意:
1:栈在对一个函数执行结束以后,只能自动释放局部普通变量
2:在这段话中我提到了一个变量值,这个变量值其实指的是一个起始地址,假设函数里有两个局部普通变量结构体a,结构体b,在函数执行的时候,这两个变量都会有寄存器为他们分配一个存储地址,寄存器分配地址这个是系统做的,这是个艺术的操作,因为系统分配出来的地址是经过巧妙计算的,不同变量之间,地址的偏差会刚刚好,这里存到栈上的变量值,其实指的是寄存器的偏移地址,函数的地址为寄存器执行的基地址,(基地址+偏移地址)就可以找到对应变量的起始地址,不同的变量会对应不同的类型,每一种类型都决定了分配策略和内存大小,如果还不懂就看一下下面我写的虚拟机分配策略
堆(heap):这个主要存放new出来的数据,对于内存的分配,我们不可能遇到一个数据就给他分配一个连续的空间,因为我们的对象大小是不确定的,如果都是连续的话,那么对于内存是一种浪费,会有很多磁盘间隙,所以这就需要一种存储模式叫做链式存储,对于同样一组数据,我们可以把它存在多个非连续的内存空间中,
常量区:const定义的变量
代码区:二进制代码
静态(全局)区:全局变量或者静态变量,代码编译阶段分配
虚拟机的寄存器分配策略
在函数执行的时候,会将函数首地址入栈,其次是函数的参数,再后面函数执行就是将函数里的变量入栈,其实这个过程就是确定所有需要参与运算的变量的地址,而这个确定的操作就是要依靠栈上的寄存器来实现,栈上的寄存器的地址分配偏移都是单位1,因为地址的大小都是固定不变的,这个由操作系统决定或者自定义,一般是32位或64位,虚拟机以这个函数的首地址作为寄存器本次执行分配的一个基地址,不断的将所有变量的地址依次写入到相邻的寄存器里,这个阶段其实是语法和词法分析阶段做的,这样,对于虚拟机而言,它很明确每个寄存器里存了那些值,在虚拟机的世界里,就会去依靠寄存器偏移地址来操作数据,又或者说我们在外界定义的变量,虚拟机经过一番解析,将他们定义为了偏移地址,形如1,2,3,4…n,
内存
C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址,下图是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址
指针
指针的大小非常固定,它和操作系统的位数有关,如果是32位的,那指针的大小就是32位的,如果是64位的,那指针大小就是64位的,对于系统自己定义的类型和我们程序员自己定义的类型去声明指针时,这个时候类型就决定了当前首地址指向的数据区占用了多少字节的内存
声明一个指针
int *p; // 声明一个 int 类型的指针 p
char *p // 声明一个 char 类型的指针 p
int *arr[10] // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向 int 类型对象的指针
int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组
int **p; // 声明一个指针 p ,该指针指向一个 int 类型的指针
上面只是声明了一个指针类型,并没有给指针分配内存
给指针分配内存
/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x; // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址
/* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10); // malloc 函数用于动态分配内存
free(p); // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用,要使用这两个函数需要头文件 stdlib.h
指针和普通变量有何不同?
普通变量在内存中的地址是有系统分配的,它存的是具体的值
指针变量在内存中的地址是可以由我们程序员自己分配的,它存的是值的地址
int a = 10;a的地址由系统给我们分配好,我们不需要关心a的地址,为啥呢?因为高级语言不过是字符串,你看到这句代码会经过语法和词法分析,最终会变成可执行的机器指令,大量的机器指令之间依靠系统给我们分配和设定的寄存器来存取和计算数据,系统知道它在那就可以了啊,我们只负责运算
寄存器 0xnnnnnnnn
------------------------------------- 值寄存器
------------------------------------------------- 0x0000000(N+3) byte
------------------------------------------------- 0x0000000(N+2) byte
------------------------------------------------- 0x0000000(N+1) byte
------------------------------------------------- 0x0000000(N) byte
int *p = & a;*将a的地址赋给p,相当于就是把寄存器存储a的地址给暴露出来给外界玩,此处的p是一个指针变量,系统也会给它分配地址,只是它存储的值是地址,p这个变量的值就是和a地址一样的值,p是取出来地址中对应的值,&p是系统为这个指针变量分配的地址
寄存器
0xnnnnnnnn
int *p = & a;这句代码给指针变量p的类型设置为int,和a的类型一致,其实都是变量,变量用来存储数据,变量的地址用来指定存储数据的首地址,类型则用来明确这段存储内存的长度
普通变量是编译器为了让程序员好理解,才抽象出来的符号,其实从汇编语言的角度,这些代码在经过词法和语法分析以后,生成机器指令,存在寄存器中,都变成了可以明确知道的地址,系统可以明确访问的,也就是说我们使用了符号,系统使用了地址,符号就是抽象出来的,地址才是实际存在的东西,他们之间不需要任何转换,是一一对应的,这个一一对应来源于强大的词法和语法分析,这两个阶段,会让系统分配好所有变量的内存和管理他们,就好像我们站在高级语言的角度去写逻辑,每个变量的声明周期和调用我们都很熟悉
指针变量,本质上说,它也是变量,系统也会给他分配内存地址,只是它存储的值是地址,
任何一个变量都有一个值和一个地址
空指针
struct Date { int month, day, year; }dat;
dat.day = 10;
dat.month = 12;
dat.year = 2020;
//此处就是指针的流弊之处
//指针的大小都是一样的,理论上他可以存储所有内存中所有数据地址
//在赋值的时候可以先不知道值的类型,在使用的时候再强制转换一下类型
void *t =&dat;
struct Date * p = (struct Date *)t;
printf("%d---%d---%d",(*p).day,p->month,p->year);