記憶體模型

星夜之章發表於2024-10-22

記憶體模型

以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. 指標越界

相關文章