內存模型
內存模型
以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 入棧
//(具體到哪個參數先的話。。。應該是從右到左
//參考連接 http://www.rzrgm.cn/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的縮寫;
- 這些基本數據類型,其差別是內存大?。╢loat、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. 忌外部指針指向已調用結束的函數內的臨時變量 - 操作不可操作的內存區
- 內存丟失和指針清零
- 指針越界
浙公網安備 33010602011771號