計算機的記憶體長什麼樣子?
1、計算機中的記憶體就像一疊非常厚的“便籤”,一張便籤就相當於一個位元組的記憶體,一個位元組有8個二進位制位
2、每一張“便籤”都有自然排序的一個編號,計算機是根據便籤的編號來訪問、使用"便籤"
3、CPU會有若干個金手指,每根金手指能感知高低電平,高電平轉換成1,低電平轉換成0,我們常說的32位CPU指的是CPU有32個金手指用於感知電平,並計算出“便籤”的編號
便籤的最小編號:
00000000 00000000 00000000 00000000 = 0
便籤的最大編號:
11111111 11111111 11111111 11111111 = 4294967295
所以32位CPU最多能使用4Gb的記憶體
4、便籤的編號就是記憶體的地址,是一種無符號的整數型別
什麼是指標:
1、指標(pointer)是一種特殊的資料型別,使用它可以用於定義指標變數,簡稱指標
2、指標變數中儲存的是記憶體的地址,是一種無符號的整數型別,
3、透過指標變數中記錄的記憶體地址,我們可以讀取對應的記憶體中所儲存的資料、也可以向該記憶體寫入資料
4、可以透過 %p 顯示指標變數中儲存的地址編號
如何使用指標:
定義指標變數
型別* 指標變數名;
int num;
char n;
double d;
int* nump; // 訪問4位元組
char* p; // 訪問1位元組
double* doublep; // 訪問8位元組
long* lp; // 訪問4/8位元組
1、一個指標變數衝只記錄記憶體中某一個位元組的地址,我們把它當做一塊記憶體的首地址,當使用指標變數去訪問記憶體時具體連續訪問多少個位元組,指標變數的型別來決定。
2、普通變數與指標變數的用法上有很大區別,為了避免混用,所以指標變數一般以p結尾,以示區分
3、指標變數不能連續定義,一個*只能定義一個指標變數
int n1,n2,n3; // n1 n2 n3都是int
int* p1,p2,p3; // int *p1,p2,p3 p1是int* p2 p3是int
int* p1,*p2,*p3; // p1 p2 p3都是int*
4、指標變數與普通一樣,預設值是隨機的(野指標),為了安全儘量給指標變數初始化,如果不知道該初始化為多少,可以先初始化為NULL(空指標)
int* p; // 野指標
int* p = NULL; // 空指標
給指標變數賦值:
指標變數 = 記憶體地址
所謂的給指標變數賦值,其實就是往指標變數中儲存一個記憶體地址,如果該記憶體地址是非法的,當使用該指標變數去訪問記憶體時會出現 段錯誤
// 儲存堆記憶體地址
int* p = malloc(4);
// 儲存指向num所在記憶體地址(stack\data\bss)
int num; // stack
int* p = #
注意:num變數的型別必須與p型別相同
指標變數解引用:
*指標變數名;
給指標變數賦值就是讓指標指向某一個記憶體,對指標變數解引用就是根據指標變數中儲存的記憶體編號,去訪問該記憶體,具體連續訪問多少個位元組由指標變數定義時的型別決定
int num = 100;
// 定義指標變數
int* p = NULL;
// 給指標變數賦值
p = #
// 檢視指標變數的值
printf("%p\n",p);
// 對指標變數解引用
printf("%d\n",*p + 10);
*p = 88;
printf("%d\n",num);
如果指標變數中儲存的是非法的記憶體地址,當程式執行到該指標變數解引用時,會出現段錯誤
// 定義指標變數
int* p = NULL;
*p = 100; // 非法訪問記憶體 會段錯誤
驗證指標變數中儲存的就是一個整數
#include <stdio.h>
void func(unsigned long addr) {
*(int*)addr = 88;
}
int main() {
int num = 10;
func(&num);
printf("num=%d\n",num);
}
為什麼要使用指標:
1、函式之間需要共享變數
函式之間的名稱空間是相互獨立,並且是以賦值的方式進行單向值傳遞,所以無法透過普通型別形參傳參來解決共享變數的問題
全域性變數雖然可以在函式之間共享,但是過多地使用全域性變數容易造成命名衝突和記憶體浪費
使用陣列是可以共享,但是需要額外傳遞長度
因此,雖然函式之間的名稱空間是相互獨立的,但是所使用的是同一條記憶體,也就是說記憶體空間是同一個,所以使用指標可以解決函式之間共享變數的問題
#include <stdio.h>
void func(int* p) {
printf("func p=%p,*p=%d\n",p,*p);
*p = 88;
printf("func p=%p,*p=%d\n",p,*p);
}
int main() {
int num = 66;
func(&num);
printf("main &num=%p ",&num);
printf("main:%d\n",num);
}
當函式需要返回兩個以上的資料時,光靠返回值滿足不了,可以透過指標共享一個變數,藉助該輸出型引數,返回多個資料
// put_p輸出型引數
int func(int* put_p) {
*put_p = 20;
return 10;
}
int main() {
int num = 0;
int ret1 = func(&num);
printf("ret1 = %d ret2=%d\n", ret1, num);
}
2、使用指標可以提高函式之間的傳參效率
一個指標變數佔記憶體 4 | 8 位元組
函式之間傳參是以記憶體複製的方式進行,當引數的記憶體位元組數比較大(大於4位元組時)的時候,傳參的效率就會比較低下,此時使用指標傳參可以提高傳參效率
#include <stdio.h>
void func(long double* f) {
}
int main() {
long double f = 3.14;
for (int i = 0; i < 1000000000; ++i) {
func(&f);
}
}
3、使用堆記憶體時,必須與指標變數配合
堆記憶體無法像棧、資料段、bss段那樣給記憶體取名字,透過標準庫、作業系統提供的管理堆記憶體的介面函式,來操作堆記憶體時,是直接返回堆記憶體的地址給呼叫者,因此必須使用指標變數配合才能訪問堆記憶體
malloc
realloc
calloc
學習建議:指標就是一種工具,目的是完成任務,而使用指標是有危險性,所以除了以上三種情況需要使用指標以外,不要輕易使用指標
使用指標需要注意的問題:
空指標:
指標變數中儲存的NULL,那麼它就是空指標
作業系統規定程式不能訪問NULL指向的記憶體,只要訪問必定段錯誤
當函式的返回值是指標型別時,函式執行出錯時一般返回NULL,作為函式的錯誤標誌
NULL也可以作為初始值給指標變數初始化
#include <stdio.h>
int* func(void) {
return NULL; //表示執行出錯
}
int main() {
int* p = NULL;
int num= 10;
p = #
printf("%d\n,",*p); // 必定段錯誤
}
如何避免空指標產生的段錯誤?
對來歷不明的指標進行解引用前先判斷是否是空指標
1、當自己寫的函式的引數中有指標型別時,在使用該引數時,需要先判斷是否是空指標再使用
2、當使用別人提供的函式時,它的返回值型別是指標型別時,獲取返回值後,也需要先判斷是否是空指標再使用
int* p = malloc(4);
if(NULL == p) {
printf("記憶體申請失敗\n");
} else {
*p = 100;
}
if(NULL == p) // 正確寫法
if(p == NULL) // 容易漏寫= 變成賦值 錯誤寫法
if(!p) { // 絕大多數系統中 NULL 是0,少數系統中是1
// 通用性不夠強
}
注意:必須匯入 stdio.h 後 NULL才可以使用
野指標:
指標變數中儲存的地址,無法確定是哪個地址、是否是合法地址,此時該指標就稱為野指標
對野指標解引用的後果:
1、一切正常,剛好指標變數中儲存的是空閒且合法的地址
2、段錯誤,剛好指標變數中儲存的是非法的地址
3、髒資料,儲存的是其它變數的地址
野指標比空指標的危害性更大
1、空指標可以透過if(NULL==p)判斷出來,但是野指標一旦產生,無法透過程式碼判斷,只能透過經驗人為判斷
2、野指標就算暫時不暴露問題,不代表沒有問題,後期可能隨時暴露
如何避免產生野指標:
所有的野指標都是人為造成的,因此想要避免野指標的危害,只能透過不人為製造野指標
1、定義指標變數時一定初始化
2、函式不要返回區域性變數、塊變數的地址,因為當函式執行結束後,該地址指向的記憶體就會被自動銷燬回收,如果非要接收,就接受到了一個野指標
3、與堆記憶體配合的指標,當堆記憶體手動釋放後,該指標要及時置空
int* p = malloc(4);
*p = 100;
free(p);
p = NULL;
if(NULL==p)