posts - 95,comments - 0,views - 12757

内存是什么样的/如何存储数据
应用程序的内存区段图


内存是什么样的/如何存储数据:计算机的内存一般是一个字节就对应一个地址,按字节寻址

单位介绍

  • 字节(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

代码片段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

例子2

由上表这个对照关系,我们很容易就能看出,*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函数处于运行状态,当main函数运行到z = add(x, y);时,暂停main函数,调用add函数,计算机会分配240-280的内存给add函数,并把这部分内存压入栈中,如下:
add

此时add函数处于运行状态,当add函数结束后,计算机会清除分配给它的内存,并将其从stack中弹出,如下:
main

此时main函数恢复运行状态,顺利执行完后,计算机同样清除分配给它的内存,然后将其从stack中弹出

以上就是一个简单的函数调用过程,在函数调用过程中,stack中的每个元素(栈帧)都是分配给函数的内存,只有栈顶的函数处于运行态。

这里的main叫做主调函数(主动调用),add叫做被调函数(被调用)。

让我们分析下这个过程中的细节:

  • 参数的传递过程:结合上面add和main的图,我们可以很清楚的看到,xyzabc这6个变量都是处于不同的内存地址中,所以在z = add(x, y);中,我们仅仅是将xy的值赋给ab,相当于拷贝一份,事实上他们是不同的变量,在add函数中改变阿ab的值并不会影响xy,因为他们身处不同的内存地址,这是显而易见的。而这也就是值传递(拷贝)

与值传递相对应的就是引用传递了,而这就是将指针作为参数,指针操纵内存地址的本领就可以让我们实现真正的引用了,而不是拷贝,我这里就不在赘述了,大家可以自己画一画。

  • 局部变量的生命周期:还是结合上面的图,我们可以看到,在stack区中,对于每一个函数中的变量来说,函数的存活时间就是局部变量的存活时间


    好啦,本篇文章的内容到这里就结束了,希望对大家有所帮助!
posted on   Dylaris  阅读(101)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示