内存模型
内存模型
以C语言编译器的常见实现为例
内存四区:堆、栈、全局区、代码区
-
内存四区
- 堆
- 栈
- 全局区
- 全局区
- 常量区
- 代码区
- 示意图(待补充)
1、堆
由程序员使用动态分配函数分配内存,需要头文件stdlib.h。
stdlib.h中的函数主要有
函数名 | 函数原型 | 功能 | 返回值 |
---|---|---|---|
calloc | void *calloc (unsigned n,unsigned size); | 分配n个数据项的内存空间,每个数据项的大小为size个字节 | 分配内存单元首地址;不成功则返回0 |
free | void free (void *p); | 释放p所指的内存区 | 无 |
malloc | void *malloc (unsigned size); | 分配size个字节的存储空间 | 分配内存空间的地址;如不成功返回0 |
realloc | void *realloc (void *p,unsigned size); | 把p所指的内存区的大小改为size个字节 | 新分配内存空间的地址;如不成功返回0 |
rand | int rand (void); | 产生0~32767的随机数 | 返回一个随机数 |
exit | void exit (0); | 文件打开失败返回运行环境 | 无 |
以上表格摘自书本,因此还有一些未去验证的疑问:
- 关于返回值,表格中的void*型函数,不成功时应当是NULL,0和NULL在计算机中或许等价(尚未想到验证方法);
- 关于分配内存,若malloc分配的内存不是该数据类型的大小的整数倍,是否报错,或引起其他错误(未想到全面验证的方法);
- realloc函数,在改写p所指的内存区的大小后,返回新分配内存空间的地址,那么原内存空间是被覆盖还是被释放,是否会产生内存丢失(因为懒,暂未验证)。
堆区使用实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//使用malloc函数,在堆区分配20个字节
//即分配5个int型变量大小的内存
//int *p = (int*)malloc(20);
//关于内存分配
//避免出现上述提问2提及的可能出现的错误,建议按下列书写格式
int *p = (int*)malloc(5*sizeof(int));
//……对*p使用完后
free(p); //将堆释放掉
p = NULL; //清零p的指向,避免误判
return 0;
}
2、栈
存放程序的局部变量,先入后出,由编译器自动分配内存,出栈的顺序基本就是程序执行的顺序
栈的实例:
#include<stdio.h>
//假设栈开口向下
//则此时相当于在栈上分配了一个存放main函数的内存空间
int main()
{
//这两个变量,放在了栈区
int num1,num2;
printf("请输入数字1:");
scanf("%d",&num1);
printf("请输入数字2:");
scanf("%d",&num2);
//以下面这条语句为例
printf("它们的和为:%d",sum(num1,num2));
//printf函数的返回值地址先入栈(反正是函数的某地址啦)
//然后printf函数的参数
// "它们的和为:%d" 和 sum(num1,num2) 入栈
//再sum函数的返回值地址入栈
//随后,sum的参数 numOne 和 numTwo 入栈
//(具体到哪个参数先的话。。。应该是从右到左
//参考连接 https://www.cnblogs.com/xkfz007/archive/2012/03/27/2420158.html
//随着函数依次运行
//numOne 、numTwo 出栈
//sum函数在栈上的内存空间析构
//sum返回值地址出栈
//printf函数的参数出栈
//printf函数在栈上的内存空间析构
//printf函数返回值地址出栈
return 0;
}
int sum(int numOne,int numTwo)
{
return (numOne+numTwo);
}
3、全局区
这里的全局区实际是将“全局区”和"常量区“统称
若分开来看的话
全局区:存放全局变量、静态变量
常量区:存放常量、字符串
(PS:字面量,比如 int b = 123;在这句语句中,123就是字面量,在b入栈之前就存在,此时字面量应该在常量区)
全局区实例:
#include<stdio.h>
int main()
{
//变量len在栈上,123在常量区
int len = 123; //然后在执行完该语句后,123放入len
//同理,变量*str在栈上,"I an Chinese"在常量区
char *str = "I an Chinese"; //str存放"I an Chinese"的地址
return 0;
}
4、代码区:存放代码
目前还没接触过需要操作代码区的地方,不清楚有什么需要了解的特性。
内存四区的示意图先咕着,待补充
数据类型
在C语言中,数据类型,可以说是不同内存大小的别名,我所知,其数据结构所定义的算法只有四则运算。
(PS:在数据结构的内容中,有这么一个说法,数据类型是已经实现的数据结构)
类型名 | 字节 | 位 | 数值范围 | 范围说明 |
---|---|---|---|---|
char | 1 | 8 | -128~127 | -27 ~ (27-1) |
unsigned char | 1 | 8 | 0~255 | 0 ~ (28-1) |
short | 2 | 16 | -32768 ~ 32767 | -215 ~ (215-1) |
unsigned short | 2 | 16 | 0 ~ 65535 | 0 ~ (216-1) |
int | 4 | 32 | -2147483648 ~ 2147483647 | -231 ~ (231-1) |
unsigned int | 4 | 32 | 0 ~ 4294967295 | 0 ~ (232-1) |
float | 4 | 32 | -3.4x1038 ~ 3.4x1038 | 7位有效数字 |
double | 8 | 64 | -1.7x10308 ~ 1.7x10308 | 15位有效数字 |
long long | 8 | 64 | 未计算 | -263 ~ (263-1) |
unsigned long long | 8 | 64 | 未计算 | 0 ~ (264-1) |
long double | 12 | 96 | 未计算 | 不清楚 |
- 以上,均可通过编译器验证;
- short是short int的缩写,同理long是long long int的缩写;
- 这些基本数据类型,其差别是内存大小(float、double除外);
- float和double其数据存储形式与其它类型有差别(示意图待补充)。
指针:存放地址的数据类型
存放地址,通过类型,指定指针的步长
- 一级指针
- 步长
- 二级及多重指针
- 指针数组
- 指向二维数组的指针(”行式“指针)
- const型指针
- 指针与函数
- 指针作形参
- 指针作返回值
- 指向函数的指针
- 注意事项
一级指针
一级指针很好理解,就是在定义时多一个星号
//如下:
int *num; //整型指针 指针本身在栈上占4字节内存 步长4字节
char *str; //字符型指针 指针本身在栈上占4字节内存 步长1字节
double *lf; //浮点型指针 指针本身在栈上占4字节内存 步长8字节
//计算指针所占内存的大小
printf("int型指针的所占内存的大小:%d\n",sizeof(num));
printf("char型指针的所占内存的大小:%d\n",sizeof(str));
printf("double型指针的所占内存的大小:%d\n",sizeof(lf));
//计算步长
printf("int型指针的步长:%d\n",sizeof(*num));
printf("char型指针的步长:%d\n",sizeof(*str));
printf("double型指针的步长:%d\n",sizeof(*lf));
步长
步长是指针的重要概念,与指针的加减运算相关
(PS:指针的加减运算,实质的指针指向的偏移,故没有乘除运算)【YY:除非某天出现向量指针甚至张量指针(啊,真是让人头秃的假想)】
//理解步长
int *num;
char *str;
double *lf;
//以下仅为假设示例
//除非清楚地址(内存标号)所指向的内存内容,不然请勿模仿
//初始化 指向同一个地址
num = 0xaaaaa;
str = 0xaaaaa;
lf = 0xaaaaa;
//执行+1操作
num++;
str++;
lf++;
//用十六进制显示
printf("num存放的地址值:%x\n",num);
printf("str存放的地址值:%x\n",str);
printf("lf存放的地址值:%x\n",lf);
输出:
num存放的地址值:0xaaaae //比原来多4字节
str存放的地址值:0xaaaab //比原来多1字节
lf存放的地址值:0xaaab4 //比原来多8字节
二级及多重指针
- 从指针来说,无论是几级指针,都是存放地址
- 因为指针的星号操作,所以n级指针,存放(n-1)级指针的地址
- 还有步长的区别,我所知,这一点只在指向多维数组的指针中体现
//理解多级指针
char ***str_T;
char **str_O;
char *str = "I am Chinese"; //指向一个字符串
str_O = &str; //指向str
str_T = &str_O;//指向str_O
str_O = str_O+1;//偏移str_O的指向(一般,此操作无意义)
*str_O = *str_O+1; //使str存储的地址值加一个步长
**str_O = **str_O+1;//报错 常量区的内容无法更改
printf("打印字符串str:%s\n",str);
str_T = str_T+1;//偏移str_T的指向(一般,此操作无意义)
*str_T = *str_T+1;//偏移str_O的指向(一般,此操作无意义)
**str_T = **str_T+1; //使str存储的地址值加一个步长
***str_O = ***str_O+1;//报错 常量区的内容无法更改
printf("打印字符串str:%s\n",str);
输出:
打印字符串str: am Chinese
打印字符串str:am Chinese
第二个比第一个少输出一个空格
因为第一个只移一个步长,第二个共移了两个步长
指针数组
顾名思义,以数组的形式,定义多个指针
//指针数组
//定义了存放地址的数组
int *p[5]; //有5个指针元素
指向二维数组的指针(“行式”指针)
指向多维数组的指针可以是普通的指针,也可以是“行式”指针
此处只对”行式“指针进行说明
//理解”行式“指针
//定义一个3x4的二维数组
int numlen[3][4]={1,3,5,7,
9,11,13,15,
17,19,21,23};
int(*num)[4]; //定义一个”行式“指针 步长为4xsizeof(int)字节
p = a; //指向数组a
//以打印元素的方式验证
printf("num指向的元素:%d\n",*(*num));
printf("num+1指向的元素:%d\n",*(*(num+1)));
//以打印地址的方式验证
printf("num的地址:%d\n",num);
printf("num+1的地址:%d\n",num+1);
//注意 下列书写依然是打印地址
printf("仍是存放在num的地址:%d\n",*num);
printf("仍是存放在num+1的地址:%d\n",*(num+1));
//打印行内的元素
//打印元素a[0][1]
printf("打印num指向的行内元素:%d\n",*(*num+1));
//打印元素a[1][1]
printf("打印num+1指向的行内元素:%d\n",*(*(num+1)+1));
输出:
1
9
地址根据系统变化,但二者之间,地址值相差16
同上方的地址一样,二者值同样相差16
3
11
- 从打印行内的元素的方式,可以看出该案例中的“行式”指针是二级指针
- “行式”指针,可读性相对较差,不易维护,很少使用
- “行式”指针,几乎与二维数组共同出现
- 三维数组,可以是“页式”指针
const型指针
const型指针有两种
//可进行遍历的只读指针
//可以修改p的值,但不能用*p修改a的值
const int *p = &a;
//不可进行遍历的标志指针
//不能修改p的值
int * const p = &a; //与数组首地址作用相同
//存储的地址不会变化,可作为函数形参,标识地址
指针与函数
指针除了内存操作外,可以说是专服务于函数
指针作形参
指针作形参,就是作为函数的参数,其目的,大多都是为函数提供多个返回值的
(PS:指针忌指向临时变量,即在指针使用的过程中,勿指向已析构或即将析构的变量 此点将在注意事项中作示例)
函数作形参实例:
//指针作形参
#include<stdio.h>
//函数声明
void swap(int *pt1,int *pt2);
void exchange(int *p1,int *p2,int *p3);
int main()
{
int num1,num2,num3;
//对需要输入的数据进行必要的说明
printf("请输入num1:");
scanf("%d",&num1);
printf("\n");
printf("请输入num2:");
scanf("%d",&num1);
printf("\n");
printf("请输入num3:");
scanf("%d",&num1);
printf("\n");
//调用排序函数 将变量的地址传递给函数的形参指针
exchange(&num1,&num2,&num3);
printf("从大到小排序后:%d,%d,%d\n",num1,num2,num3);
return 0;
}
void exchange(int *p1,int *p2,int *p3)
{
//在判断为真后,调用换值函数 交换变量中的值
if(*p1 < *p2)swap(p1,p2);
if(*p1 < *p3)swap(p1,p3);
if(*p2 < *p3)swap(p2,p3);
}
void swap(int *pt1,int *pt2)
{
int temp;
temp = *pt1;
*pt1 = *pt2;
*pt2 = temp;
}
指针作返回值
所谓指针作返回值,就是定义函数时,使用指针类型
//如下
//定义函数的类型,其实是定义函数返回值的类型
int* twoSum(int* nums, int numsSize, int target)
{
static int a[2]={0};
for (int i = 0; i < numsSize - 1; i++)
{
for (int j = i+1; j < numsSize; j++)
{
if (nums[i] + nums[j] == target)
{
a[0] = i;
a[1] = j;
return a;
}
}
}
return 0;
}
指向函数的指针
指向函数的指针,其实就是通过指针调用函数,就我的学习经历来说,使用不多
指针调用函数实例:
#include<stdio.h>
int main()
{
//定义三个存放数据的变量
int num1,num2,NumMax;
//定义一个可以指向函数的指针
int (*p)(int,int);
//对需要输入的数据进行必要的说明
printf("请输入num1:");
scanf("%d",num1);
printf("\n");
printf("请输入num2:");
scanf("%d",num2);
printf("\n");
//指针指向函数
p = max;
//用指针调用函数
NumMax = (*p)(num1,num2);
//输出结果
printf("num1 = %d\tnum2 = %d\t NumMax = %d\n",num1,num2,NumMax);
return 0;
}
//定义一个返回两数中最大数的函数
int max(int x,int y)
{
return x > y ? x : y;
}
注意事项
想要安全地使用指针,就必须明确指针指向的内存空间信息
如:
这块内存空间的生命周期有多长
这块内存空间能否被操作
内存空间不再使用时,是否已释放
在释放内存空间后,指针是否已清零
错误的函数示例:
、//示例 错误函数
int* twoSum(int* nums, int numsSize, int target)
{
int a[2]={0};
for (int i = 0; i < numsSize - 1; i++)
{
for (int j = i+1; j < numsSize; j++)
{
if (nums[i] + nums[j] == target)
{
a[0] = i;
a[1] = j;
//返回值有误 a是该函数在栈上临时分配的内存
//在函数调用结束后,会被析构
return a;
}
}
}
return 0;
}
错误的调用:
int numlen[]= {2, 7, 11, 15};
int *result;
int size = sizeof(numlen)/sizeof(numlen[0]);
twoSum(numlen,size,9,result);
int* twoSum(int* nums, int numsSize, int target,int *out)
{
int a[2]={0};
for (int i = 0; i < numsSize - 1; i++)
{
for (int j = i+1; j < numsSize; j++)
{
if (nums[i] + nums[j] == target)
{
a[0] = i;
a[1] = j;
//值传递有误 a是本函数中定义的临时变量
//函数调用完毕后,会被析构
//无法通过指针 将内容传递出去
out = a;
}
}
}
return 0;
}
正确函数书写:
int numlen[]= {2, 7, 11, 15};
int *result;
int size = sizeof(numlen)/sizeof(numlen[0]);
twoSum(numlen,size,9,result);
int* twoSum(int* nums, int numsSize, int target,int *out)
{
//使用静态变量 其内存位置在全局区
static int a[2]={0};
for (int i = 0; i < numsSize - 1; i++)
{
for (int j = i+1; j < numsSize; j++)
{
if (nums[i] + nums[j] == target)
{
a[0] = i;
a[1] = j;
out = a;
}
}
}
return 0;
}
指针越界:
char *str;
char strlen[4] = {'I',' ','a','m'};
//指针指向字符数组
str = strlen;
//错误操作 字符数组不是字符串 缺少’\0‘
//因此会越界输出
printf("%s",str);
//其它越界
str = strlen[3];
//指向数组外的未知内存
str++;
//越界输出
printf("%s",str);
操作常量区:
char *str = "I am Chinese"; //该指针指向常量区的字符串
free(str); //错误操作 常量区无法操作
内存丢失:
char *str1 = "I am Chinese";
//在堆上分配100字节的内存
char *str2 = (char *)malloc(100);
//错误操作 str2指针指向了常量区字符串
str2 = str1;
//在堆上分配的100字节内存丢失
指针不清零:
char *str1 = "I am Chinese";
//在堆上分配100字节的内存
char *str2 = (char *)malloc(100);
//假设使用完毕 进行释放
if(str2 != NULL)
{
free(str2);
}
//计划重新使用
if(str2 !=NULL)
{
//错误操作 str2指向的内存已被释放
strcpy(str2,str1);
}
//因此 释放指针指向的内存后,指针应当复位清零
if(str2 != NULL)
{
free(str2);
str = NULL;
}
- 指针忌指向临时变量
1. 忌指针型返回值指向该函数内的临时变量
2. 忌外部指针指向已调用结束的函数内的临时变量 - 操作不可操作的内存区
- 内存丢失和指针清零
- 指针越界