内存模型

内存模型

以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); 文件打开失败返回运行环境

以上表格摘自书本,因此还有一些未去验证的疑问:

  1. 关于返回值,表格中的void*型函数,不成功时应当是NULL,0和NULL在计算机中或许等价(尚未想到验证方法);
  2. 关于分配内存,若malloc分配的内存不是该数据类型的大小的整数倍,是否报错,或引起其他错误(未想到全面验证的方法);
  3. 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 未计算 不清楚
  1. 以上,均可通过编译器验证;
  2. short是short int的缩写,同理long是long long int的缩写;
  3. 这些基本数据类型,其差别是内存大小(float、double除外);
  4. 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字节

二级及多重指针

  1. 从指针来说,无论是几级指针,都是存放地址
  2. 因为指针的星号操作,所以n级指针,存放(n-1)级指针的地址
  3. 还有步长的区别,我所知,这一点只在指向多维数组的指针中体现
//理解多级指针
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
  1. 从打印行内的元素的方式,可以看出该案例中的“行式”指针是二级指针
  2. “行式”指针,可读性相对较差,不易维护,很少使用
  3. “行式”指针,几乎与二维数组共同出现
  4. 三维数组,可以是“页式”指针

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. 指针忌指向临时变量
    1. 忌指针型返回值指向该函数内的临时变量
    2. 忌外部指针指向已调用结束的函数内的临时变量
  2. 操作不可操作的内存区
  3. 内存丢失和指针清零
  4. 指针越界
posted @ 2024-10-22 00:53  星夜之章  阅读(26)  评论(0编辑  收藏  举报