内存是什么样的/如何存储数据:计算机的内存一般是一个字节就对应一个地址,按字节寻址
单位介绍:
- 字节(Byte),是计算机常用的一个单位
- 位(bit),1个字节是8位,位只有0和1两种表示(二进制)
如下:
前面的是内存地址(随便举例的), 后面的一个个方块,我们就看成是一个个的内存单元,大小都是一字节。
在计算机的世界里,其实只有0和1,内存存储的数据也是由0和1组成的,比方说,我有一个int a = 259
的例子,我们声明了一个整数变量a,并将其初始化为259,那么它在内存中是怎么工作的呢?
- 分配空间:我们声明了一个整数变量,计算机会为这个变量分配内存,因为整数是4个字节,而每个块是1个字节,所以需要分配4个块,我们这里分配的是208-211这四个块
- 存放数据:然后我们将259赋值给a,因为计算机使用的是二进制,所以我们要先将259转化成二进制,即
00000000 00000000 00000001 00000011
(一个字节是8位,所以整数4个字节就是32位),然后依次放到分配的内存块中
如下:
代码片段1:
int *p = &a; // 声明了一个整型指针变量,也是4字节,假设指针分配的内存是214-217
printf("整型指针的大小: %d\n", sizeof(int *)); // sizeof可以用来计算占用的字节数
printf("指针p的地址: %d\n", &p);
printf("指针p的值: %d\n", p);
printf("变量a的地址: %d\n", &a);
printf("指针p所指向的地址的值: %d\n", *p);
基于我所假设的内存分配,运行以上代码,指针p的分配地址&p是214,指针p的值是208,&a是208,指针p所指向的地址的值是259
我们可以发现指针p的值与a的地址是一样的,这就形成了一种指向性的关系,我们就可以通过操纵指针,从而操纵内存地址
代码片段2:
char *p = &a; // 声明了一个char类型的指针变量,大小是1字节
...
在这段代码中,我们将指针从整型改成字符型,还是指向变量a,p的值是什么呢?
让我们分析一下,由于p是字符型指针,只占用1个字节,也就只能存储一个字节的数据,此时p的值是208(变量a的首地址),p的值就是3,也就是00000011
(a的首地址存储的数据)
进一步,对指针做移动操作:p=208,p+1=209,p+2=210,p+3=211
由上表这个对照关系,我们很容易就能看出,*p=00000011(2进制)=3(十进制),其他的同理
比较代码片段1和2,相信你能够更好的理解指针与内存的关系
在多补充一些关于指针偏移的细节:
- 比如
int *p = &a;
, 指针p指向的就是基址(起始地址),p+n中的这个n就是偏移量 - 对于偏移量,我们要注意到它是一个相对于指针类型的量。比如说:
char *p = &a;
,p=208,那么p+1=209
int *p = &a;
,p=208,但p+1=212
造成这种差别的原因是因为指针的类型所占用的字节数不同,char占用1个字节,所以偏移量与字节比就是1:1;int占用4个字节,所以偏移量与字节比就是1:4
所以在指针偏移操作中,一定要区分偏移量与具体偏移字节量
应用程序的内存区段图: 当我们运行c语言程序时,实际上是运行一个可执行文件(.exe文件),也可以叫做一个应用程序。计算机会为这个应用程序分配内存,也就是内存区段图,其实就是一张用来划分区域的图。
如下:
Heap:堆区或者叫动态内存区,程序中动态分配的内存来自这里
Stack:栈区,可以叫函数栈,存放的是函数调用、局部变量等
Static/Global:用来存放全局变量和静态变量,变量会一直存活直至程序结束
Code(Text):用来存放指令,其实就是我们写的代码(不过是二进制形式)
这里我们先着重于Stack的情况。
Stack存放的是函数的调用以及函数生成的局部变量等,具体看以下代码
代码片段3:
int add(int a, int b)
{
int c;
c = a + b;
return c;
}
int main()
{
int x = 1;
int y = 2;
int z;
z = add(x, y);
return 0;
}
程序执行后,我们会有一个栈区用来存储函数调用,首先我们进入入口函数main,计算机会分配200-230的内存给main函数,用来存储局部变量等,把这部分内存压入栈中,如下:
此时main函数处于运行状态,当main函数运行到z = add(x, y);
时,暂停main函数,调用add函数,计算机会分配240-280的内存给add函数,并把这部分内存压入栈中,如下:
此时add函数处于运行状态,当add函数结束后,计算机会清除分配给它的内存,并将其从stack中弹出,如下:
此时main函数恢复运行状态,顺利执行完后,计算机同样清除分配给它的内存,然后将其从stack中弹出
以上就是一个简单的函数调用过程,在函数调用过程中,stack中的每个元素(栈帧)都是分配给函数的内存,只有栈顶的函数处于运行态。
这里的main叫做主调函数(主动调用),add叫做被调函数(被调用)。
让我们分析下这个过程中的细节:
- 参数的传递过程:结合上面add和main的图,我们可以很清楚的看到,xyzabc这6个变量都是处于不同的内存地址中,所以在
z = add(x, y);
中,我们仅仅是将xy的值赋给ab,相当于拷贝一份,事实上他们是不同的变量,在add函数中改变阿ab的值并不会影响xy,因为他们身处不同的内存地址,这是显而易见的。而这也就是值传递(拷贝)。
与值传递相对应的就是引用传递了,而这就是将指针作为参数,指针操纵内存地址的本领就可以让我们实现真正的引用了,而不是拷贝,我这里就不在赘述了,大家可以自己画一画。
- 局部变量的生命周期:还是结合上面的图,我们可以看到,在stack区中,对于每一个函数中的变量来说,函数的存活时间就是局部变量的存活时间
好啦,本篇文章的内容到这里就结束了,希望对大家有所帮助!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构