知识前提
常见数据类型的位宽
在计算机科学中,“位”(bit)是信息的最小单位,表示二进制数字(0 或 1)。位是计算机中所有数据表示和存储的基础
以下是典型情况下常见数据类型的位宽:
数据类型 | 位宽(位) | 字节数 |
---|---|---|
char | 8 | 1 |
short | 16 | 2 |
int | 32 | 4 |
long | 32 或 64 | 4 或 8 |
long long | 64 | 8 |
float | 32 | 4 |
double | 64 | 8 |
long double | 64、80 或 128 | 8、10 或 16 |
void* (指针) | 32 或 64 | 4 或 8 |
1. 位(bit)的定义
-
位是“二进制位”(binary digit)的缩写。
-
1 位可以表示两种状态:
0
或1
。 -
位是计算机中最小的数据单位。
2. 字节(byte)的定义
-
字节是由 8 个位组成的单位。
-
1 字节 = 8 位。
-
1 字节可以表示 28=25628=256 种不同的状态(从
00000000
到11111111
)。
3. 位与字节的关系
-
1 字节 = 8 位。
-
1 位是二进制的最小单位,而字节是计算机中常用的基本存储单位。
4. 数据类型的位宽
在C语言中,数据类型的“位宽”指的是该类型占用的位数。例如:
-
char
通常是 8 位(1 字节)。 -
int
通常是 32 位(4 字节)。 -
long
可能是 32 位或 64 位,具体取决于平台。
位宽决定了数据类型的取值范围。例如:
-
8 位的
char
可以表示 28=25628=256 个不同的值(通常范围为-128
到127
或0
到255
)。 -
32 位的
int
可以表示 232=4,294,967,296232=4,294,967,296 个不同的值(通常范围为-2,147,483,648
到2,147,483,647
)。
内存分布
虚拟地址空间在32位环境下的大小为 4GB,在64位环境下的大小为 256TB。
那么这里“虚拟地址空间在32位环境下的大小为 4GB”这个结论怎么来的呢?32位系统即32个2进制数,4个2进制数组成1个十六进制数。因此32位由于8个十六进制组成,FFFF FFFF转化为十进制即为4294967295,由于地址是从零开始,因此实际大小为4294967296。
4294967296 Byte
4294967296/1024 KB
4294967296/1024/1024 MB
4294967296/1024/1024/1024 GB = 4GB
因此这里的4GB是这里来的。
程序内存在地址空间中的分布情况称为内存模型(Memory Model)。内存模型由操作系统构建,在Linux和Windows下有所差异,并且会受到编译模式的影响。
内核空间和用户空间
对于32位环境,理论上程序可以拥有 4GB 的虚拟地址空间,我们在C语言中使用到的变量、函数、字符串等都会对应内存中的一块区域。
但是,在这 4GB 的地址空间中,要拿出一部分给操作系统内核(每个进程都留部分空间给内核代码)使用,应用程序无法直接访问这一段内存(如果访问,则会发生段错误),这一部分内存地址被称为内核空间(Kernel Space)。
Windows 在默认情况下会将高地址的 2GB 空间分配给内核(也可以配置为1GB),而 Linux 默认情况下会将高地址的 1GB 空间分配给内核。也就是说,应用程序只能使用剩下的 2GB 或 3GB 的地址空间,称为用户空间(User Space)。
Linux下32位环境的用户空间内存分布情况
Linux下32位环境的一种经典内存模型:
内存分区 | 说明 |
内核空间 | 在这 4GB 的地址空间中,要拿出一部分给操作系统内核(每个进程都留部分空间给内核代码)使用,应用程序无法直接访问这一段内存(如果访问,则会发生段错误) |
程序代码区 (.test code) | 存放函数体的二进制代码。一个C语言程序由多个函数构成,C语言程序的执行就是函数之间的相互调用。 |
常量区 (constant) | 存放一般的常量、字符串常量等。这块内存只有读取权限,没有写入权限,因此它们的值在程序运行期间不能改变。 |
全局数据区 (global data) | 存放全局变量、静态变量等。这块内存有读写权限,因此它们的值在程序运行期间可以任意改变。 |
堆区 (heap) | 一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。 malloc(), calloc, free()等函数操作的就是这块内存。 注意:这里所说的堆区与数据结构中的堆不是一个概念,堆区的分配方式倒是类似于链表。 |
动态链接库 | 用于在程序运行期间加载和卸载动态链接库。 |
栈区 (stack) | 存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。 |
在这些内存分区中(暂时不讨论动态链接库),程序代码区用来保存指令(存放函数体的二进制代码),常量区、全局数据区、堆、栈都用来保存数据。对内存的研究,重点是对数据分区的研究。
程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。
常量区和全局数据区有时也被合称为静态数据区,意思是这段内存专门用来保存数据,在程序运行期间一直存在。
函数被调用时,会将参数、局部变量(在函数内{}包含的变量)、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。
局部变量、形参---栈
局部变量、形参只在当前函数中有效,函数结束它们的内存不在了。局部变量存储在栈里的, 局部变量的值未知,在使用要特别注意,建议赋值为0。
局部变量如果未初始化,值为未知的。
#include <stdio.h>
void fun(int x,int y)
{
int num = x+y;
printf("x = %p\n",&x);
printf("y = %p\n",&y);
printf("num = %p\n",&num);
printf("fun x = %d y = %d num = %d\n",x,y,num);
}
int main(void)
{
int x,y,num;
num = x+y;
printf("x = %p\n",&x);
printf("y = %p\n",&y);
printf("num = %p\n",&num);
printf("main x = %d y = %d num = %d\n",x,y,num);
fun(x,y);
return 0;
}
结果:
x = 00000000005ffecc
y = 00000000005ffec8
num = 00000000005ffec4
main x = 0 y = 6833696 num = 6833696
x = 00000000005ffea0
y = 00000000005ffea8
num = 00000000005ffe8c
fun x = 0 y = 6833696 num = 6833696
主函数的变量也是一种局部变量,它的地址分配一般都会比它调用的函数里面的局部变量分配的地址高。一般认为在主函数里面栈是向下生长的,即地址会向下去分配。其它函数分配可能向下分配也可能向下分配,这点由编译器决定。
#include <stdio.h>
//int x, int y int num属于局部变量,存在栈当中,向下增长
int add(int x, int y)
{
int num;
printf("x addr:%p\n", &x);
printf("y addr:%p\n", &y);
printf("num addr:%p\n", &num);
num = x+y;
return num;
}
int main(void)
{
//a, b, c, m也是局部变量
int a = 10;
int b = 20;
int c = 30;
int m;
printf("a addr:%p\n", &a);
printf("b addr:%p\n", &b);
printf("c addr:%p\n", &c);
printf("m addr:%p\n", &m);
m = add(a, b);
return 0;
}
输出:
b addr:0xffffcbf8
c addr:0xffffcbf4
m addr:0xffffcbf0
x addr:0xffffcbd0
y addr:0xffffcbd8
num addr:0xffffcbbc
栈区(先进后出 FILO)
存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈,栈可能是向下增长(以这个为主),也可能向上增长,这个取决系统对栈的管理, 栈上的内存由系统自动分配和释放,不能由程序员控制。
全局变量--存储在数据区
C语言 ---- 数据区(Data Segment)-优快云博客
一般作用于整个文件,甚至是其它文件,它的生命周期与进程的生命周期是一致,全局变量一般定义在所有函数前面。全局变量默认初始化的值为0
#include <stdio.h>
//记录函数调用的次数 g_fun_count全部变量
int g_fun_count = 0;
int add(int x, int y)
{
//由于g_fun_count为全部变量,可在这个函数里做加法
g_fun_count++;
return x+y;
}
int sub(int x, int y)
{
//由于g_fun_count为全部变量,可在这个函数里做加法
g_fun_count++;
return x-y;
}
int main(void)
{
int a = 1, b = 2;
while(1)
{
printf("请输入两个正整数");
scanf("%d%d", &a, &b);
printf("和为%d\n", add(a,b));
printf("差为%d\n", sub(a,b));
//由于g_fun_count为全部变量,可以这里打印
printf("调用函数的次数:%d\n", g_fun_count);
}
return 0;
}
输出:
请输入两个正整数25 47
和为72
差为-22
调用函数的次数:2
请输入两个正整数69 44
和为113
差为25
调用函数的次数:4
堆区 -- 动态内存分配
一般由程序员分配和释放,若程序员不释放(会造成内存泄漏),程序运行结束时由操作系统回收。 malloc(), calloc(), realloc(),free()等函数操作的就是这块内存。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int *func(void)
{
//p是局部变量,函数执行后,这里的变量会被系统回收
int *p1 = NULL;
//在堆内存中开辟一个4字节(int),这个空间永久存在,开辟完成后,将空间地址返回给指针p1
p1 = malloc(4);
//将开辟的空间里面设置值100
*p1 = 100;
return p1;
}
int main(void)
{
int *p = NULL;
int m ;
//调用指针函数 p获取到n地址
p = func();
m = *p;
printf("m的值:%d\n", m);
//再交通过指针访问,此时地址所指向的空间的已经是系统回收后的未知值
m = *p;
printf("m的值:%d\n", m);
//因为p里存放了指向100空间的指针,通过指针直接可以释放空间 只要p空间的值为开辟的空间的地址,都可以释放空间
free(p);
return 0;
}
常量区、全局数据区、栈上的内存由系统自动分配和释放,不能由程序员控制。程序员唯一能控制的内存区域就是堆(Heap):它是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分,在这片空间中,程序可以申请一块内存,并自由地使用(放入任何数据)。堆内存在程序主动释放之前会一直存在,不随函数的结束而失效。在函数内部产生的数据只要放到堆中,就可以在函数外部使用。
程序代码区(Text Segment 或 Code Segment)
C语言 ---- 程序代码区(Text Segment 或 Code Segment)-优快云博客
.test:用于存储用户代码(for while if ... printf)
.init:用于存储系统给每一个用户进程自动添加的初始化代码
数据段(Data Segment)
数据段存储的数据生存周期与程序运行时间相同。数据段(Data Segment) 是程序内存布局中的一个重要部分,用于存储全局变量和静态变量。数据段通常分为两个部分:.data
段和 .bss
段。
-
.data
段中的变量在编译时被初始化。 -
.bss
段中的变量在程序加载时被初始化为0
。
.bss:用于存储未初始化的静态数据(全局变量--定义在头文件下面,所有函数上面)及用static修改局部变量(包括赋值及不赋值,这个变量不会随函数结束而结束,只表明它适用于函数内),它初始化值为0
int g_flag; //全局变量,默认的值为0
void fun(void)
{
static int num_count;//static修改的局部变量,默认的值为0
static int count = 0;//static修改的局部变量,这个修饰的局部变量在函数内只定义一次,随后变量存储上一次改变的值
}
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//全局变量
int g_flag;
int *func(void)
{
// static修改的局部变量,这个修饰的局部变量在函数内只定义一次,随后变量存储上一次改变的值
static int count = 0;
//p1是局部变量,函数执行后,这里的变量会被系统回收
int *p1, g_flag = 10;
//开辟20字节--5个int
p1 = (int *)calloc(5, 4);
printf("count:%d\n", ++count);
return p1;
}
int main(void)
{
int *p = NULL;
p = func();
free(p);
p = func();
free(p);
p = func();
//因为p里存放了指向100空间的指针,通过指针直接可以释放空间 只要p空间的值为开辟的空间的地址,都可以释放空间
free(p);
return 0;
}
输出结果:
count:1
count:2
count:3
count的值分别上一次存储的值进行加1,而不是重新调用函数后,count初始化为0.
.data:用于存储已初始化的静态数据(全局变量及static修改的全局变量)
int g_flag = 0; //赋值为0
static int count = 1;//全局变量
.rodata:用于存储只读数据(用const修饰),比如修饰的字符串,字符常量,整型浮点常量等
数据只能读操作,不能写操作。
//"helloworld"字符串在常量区
char *p = "helloworld"; //这种定义的方法是不能通过*p = 某个值来修改空间的值
const int a = 100; //a在常量区
变量放左边,叫写数据
变量放右边,叫读数据
Linux下64位环境的用户空间内存分布情况
在64位环境下,虚拟地址空间大小为 256TB,Linux 将高 128TB 的空间分配给内核使用,而将低 128TB 的空间分配给用户程序使用。如下图所示:
在64位环境下,虚拟地址虽然占用64位,但只有最低48位有效。这里需要补充的一点是,任何虚拟地址的48位至63位必须与47位一致。
上图中,用户空间地址的47位是0,所以高16位也是0,换算成十六进制形式,最高的四个数都是0;内核空间地址的47位是1,所以高16位也是1,换算成十六进制形式,最高的四个数都是1。这样中间的一部分地址正好空出来,也就是图中的“未定义区域”,这部分内存无论如何也访问不到。