系統性學習C語言-第十二講-深入理解指針(2)
- 1. ` const ` 修飾指針
- 1.1 ` const ` 修飾變量
- 1.2 ` const ` 修飾指針變量
- 2. 野指針
- 2.1 野指針成因
- 2.2 如何規避野指針
- 2.2.1 指針初始化
- 2.2.2 小心指針越界
- 2.2.3 指針變量不再使用時,及時置 ` NULL ` ,指針使用之前檢查有效性
- 2.2.4 避免返回局部變量的地址
- 3. assert 斷言
- 4. 指針的使用和傳址調用
- 4.2 傳值調用和傳址調用
1. const
修飾指針
1.1 const
修飾變量
變量是可以修改的,如果把變量的地址交給?個指針變量,通過指針變量的也可以修改這個變量。
但是如果我們希望?個變量加上?些限制,不能被修改,怎么做呢?這就是 const
的作用。
#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的const int n = 0;n = 20;//n是不能被修改的return 0;
}
上述代碼中 n
是不能被修改的,其實 n
本質是變量,只不過被 const
修飾后,在語法上加了限制,
只要我們在代碼中對 n
進行修改,就不符合語法規則,就報錯,致使沒法直接修改 n
。
#include <stdio.h>
int main()
{const int n = 0;printf("n = %d\n", n);int* p = &n;*p = 20;printf("n = %d\n", n);return 0;
}
輸出結果:
我們可以看到變量確實被修改了,但是我們還是要思考?下,為什么 n
要被 const
修飾呢?
就是為了不能被修改,如果 p
拿到 n
的地址就能修改 n
,這樣就打破了 const
的限制,
這是不合理的,所以應該讓 p
拿到 n
的地址也不能修改 n
,那接下來怎么做呢?
1.2 const
修飾指針變量
?般來講 const
修飾指針變量,可以放在 *
的左邊,也可以放在 *
的右邊,意義是不?樣的。
int * p;//沒有const修飾?
int const * p;//const 放在*的左邊做修飾
int * const p;//const 放在*的右邊做修飾
我們看下面代碼,來分析具體分析?下:
代碼1 - 測試無 const
修飾的情況
#include <stdio.h>
//代碼1 - 測試?const修飾的情況
void test1()
{int n = 10;int m = 20;int* p = &n; *p = 20;//ok?p = &m; //ok?
}
通過觀察可以看到編譯是可以通過的,說明代碼的操作是沒有問題的。
代碼2 - 測試 const
放在 *
的左邊情況
//代碼2 - 測試const放在*的左邊情況
void test2()
{int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}
通過編譯結果我們可以得出,當 const
被放在 *
左邊時,我們無法對地址解引用進行更改,
編譯器會產生報錯。
代碼3 - 測試 const
放在 *
的右邊情況
//代碼3 - 測試const放在*的右邊情況
void test3()
{int n = 10;int m = 20;int * const p = &n;*p = 20; //ok?p = &m; //ok?
}
通過編譯結果我們可以分析出,在 const
放在 *
的右邊,我們可以通過地址的解引用來改變變量,
但是我們不能對地址進行更改。
代碼4 - 測試 *
的左右兩邊都有 const
的情況
//代碼4 - 測試*的左右兩邊都有const
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20; //ok?p = &m; //ok?
}
通過編譯的結果我們可以分析出,在 *
的左右兩邊都有 const
的情況下,
我們即無法對地址解引用來更改變量,也無法對地址的值進行改變。
結論:const修飾指針變量的時候
-
const
如果放在*
的左邊,修飾的是指針指向的內容,保證指針指向的內容不能通過指針來改變。
但是指針變量本身的內容可變。 -
const
如果放在*
的右邊,修飾的是指針變量本身,保證了指針變量的內容不能修改,但是指針指
向的內容,可以通過指針改變。
2. 野指針
概念: 野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
2.1 野指針成因
1. 指針未初始化
#include <stdio.h>
int main()
{int *p;//局部變量指針未初始化,默認為隨機值*p = 20;return 0;
}
在如圖所示的代碼中,指針 p
并未進行初始化,它的地址所指向的空間是未知的,為野指針。
2. 指針越界訪問
#include <stdio.h>
int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i = 0; i <= 11; i++){//當指針指向的范圍超出數組arr的范圍時,p就是野指針*(p++) = i;}return 0;
}
在如圖所示的代碼中,指針 p
指向的范圍超出了數組 arr
的范圍,它的地址所指向的空間是未知的,為野指針。
3. 指針指向的空間釋放
#include <stdio.h>
int* test()
{int n = 100;return &n;
}int main()
{int*p = test();printf("%d\n", *p);return 0;
}
在如圖的代碼中,函數 test
中定義的變量 n
為局部變量,在函數 test
結束后就會銷毀,但函數 test
返回的是變量 n
的地址,
在變量被銷毀后,這片地址的區域就是未知的,不再有意義,函數 test
返回的無意義的地址存儲在了變量 p
中,
此時變量 p
變為了野指針。
2.2 如何規避野指針
2.2.1 指針初始化
如果明確知道指針指向哪里就直接賦值地址,如果不知道指針應該指向哪里,可以給指針賦值 NULL
。
NULL
是 C語言 中定義的?個標識符常量,值是 0 ,0 也是地址,這個地址是無法使用的,讀寫該地址會報錯。
定義 NULL
中的文件源碼
#ifdef __cplusplus#define NULL 0
#else#define NULL ((void *)0)
#endif
初始化如下:
#include <stdio.h>int main()
{int num = 10;int*p1 = #int*p2 = NULL;return 0;
}
2.2.2 小心指針越界
?個程序向內存申請了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是越界訪問。
例如:
#include<stdio.h>
int main()
{int arr[10] = { 0 };int* p = &arr[12];
}
這里我們只給數組 arr
申請了十個空間用于存儲變量,即數組 arr
的最大下標為 9 ,但時我們的 p
指針卻超出范圍,
存儲著下標 12 處的地址,這是代碼發生了指針越界,產生了野指針, p
指針現在所指向的空間時未知的。
2.2.3 指針變量不再使用時,及時置 NULL
,指針使用之前檢查有效性
當指針變量指向一塊區域的時候,我們可以通過指針訪問該區域,后期不再使用這個指針訪問空間的時候,
我們可以把該指針置為 NULL
。因為約定俗成的?個規則就是:只要是 NULL
指針就不去訪問,
同時使用指針之前可以判斷指針是否為 NULL
。
我們可以把野指針想象成野狗,野狗放任不管是非常危險的,所以我們可以找一顆樹把野狗拴起來,就相對安全了,
給指針變量及時賦值為 NULL
,其實就類似把野狗栓起來,就是把野指針暫時管理起來。
不過野狗即使拴起來我們也要繞著走,不能去挑逗野狗,有點危險;對于指針也是,在使用之前,我們也要判斷是否為 NULL
,
看看是不是被拴起來起來的野狗,如果不是我們再去使用。
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int i = 0;for(i=0; i<10; i++){*(p++) = i;}//此時p已經越界了,可以把p置為NULLp = NULL;//下次使?的時候,判斷p不為NULL的時候再使?//...p = &arr[0];//重新讓p獲得地址if(p != NULL) //判斷{//...}return 0;}
2.2.4 避免返回局部變量的地址
如造成野指針的第 3 個例子,不要返回局部變量的地址。
3. assert 斷言
assert.h
頭文件定義了宏 assert()
,用于在運行時確保程序符合指定條件,如果不符合,就報錯終止運行。這個宏常常被稱為“斷?”。
assert(p != NULL);
上面代碼在程序運行到這一行語句時,驗證變量 p
是否等于 NULL
。如果確實不等于 NULL
,程序繼續運行,否則就會終止運?,
并且給出報錯信息提示。
assert()
宏接受?個表達式作為參數。如果該表達式為真(返回值非零),assert()
不會產?任何作用,程序繼續運行。
如果該表達式為假(返回值為零), assert()
就會報錯,在標準錯誤流 stderr
中寫入?條錯誤信息,顯示沒有通過的表達式,
以及包含這個表達式的文件名和行號。
assert()
的使用程序員是非常友好的,使用 assert()
有幾個好處:它不僅能自動標識文件和出問題的行號,
還有?種無需更改代碼就能開啟或關閉 assert()
的機制。如果已經確認程序沒有問題,不需要再做斷言,
就在 #include <assert.h>
語句的前?,定義?個宏 NDEBUG
。
#define NDEBUG
#include <assert.h>
然后,重新編譯程序,編譯器就會禁用文件中所有的 assert()
語句。
如果程序又出現問題,可以移除這條 #define NDEBUG
指令(或者把它注釋掉),再次編譯,這樣就重新啟用了 assert()
語句。
assert()
的缺點是,因為引入了額外的檢查,增加了程序的運行時間。
?般我們可以在 Debug
中使用,在 Release
版本中選擇禁用 assert
就?,在 VS 這樣的集成開發環境中,在 Release
版本中,
直接就是優化掉了。這樣在 debug
版本寫有利于程序員排查問題,在 Release
版本不影響用戶使用時程序的效率。
4. 指針的使用和傳址調用
庫函數 strlen
的功能是求字符串長度,統計的是字符串中 \0 之前的字符的個數。
函數原型如下:
size_t strlen ( const char * str );
參數 str
接收?個字符串的起始地址,然后開始統計字符串中 \0 之前的字符個數,最終返回長度。
如果要模擬實現只要從起始地址開始向后逐個字符的遍歷,只要不是 \0 字符,計數器就 + 1 ,這樣直到 \0 就停止。
參考代碼如下:
int my_strlen(const char * str)
{int count = 0;assert(str); //防止傳入的指針為空while(*str) //當 *str 不為 /0 進入循環{count++; //計數器 + 1str++; //將字符變更為下一個字符}return count; //返回計數器的數值
}int main()
{int len = my_strlen("abcdef");printf("%d\n", len);return 0;
}
4.2 傳值調用和傳址調用
學習指針的目的是使用指針解決問題,那什么問題,非指針不可呢?
例如:寫?個函數,交換兩個整型變量的值
?番思考后,我們可能寫出這樣的代碼:
#include <stdio.h>void Swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交換前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交換后:a=%d b=%d\n", a, b);return 0;
}
當我們運行代碼,結果如下:
我們發現其實沒產生交換的效果,這是為什么呢?
嘗試調試,解決問題。
我們發現在 main
函數內部,創建了 a
和 b
,a
的地址是 0x00cffdd0
,b
的地址是 0x00cffdc4
,
在調? Swap1
函數時,將 a
和 b
傳遞給了Swap1
函數,在 Swap1
函數內部創建了形參 x
和 y
接收 a
和 b
的值,
但是 x
的地址是 0x00cffcec
,y
的地址是 0x00cffcf0
,x
和 y
確實接收到了 a
和 b
的值,
不過 x
的地址和 a
的地址不?樣,y
的地址和 b
的地址不?樣,相當于 x
和 y
是獨?的空間,
那在 Swap1
函數內部交換 x
和 y
的值,自然不會影響 a
和 b
,當 Swap1
函數調?結束后回到 main
函數,
a
和 b
的沒法交換。
Swap1
函數在使用的時候,是把變量本身直接傳遞給了函數,這種調用函數的方式我們之前在函數的時候就知道了,這種叫傳值調用。
結論:實參傳遞給形參的時候,形參會單獨創建?份臨時空間來接收實參,對形參的修改不影響實參。
所以 Swap1
是失敗的了。
那怎么辦呢?
現在要解決的就是當調用 Swap
函數的時候,Swap
函數內部操作的就是 main
函數中的 a
和 b
,直接將 a
和b
的值交換了。
那么就可以使用指針了,在 main
函數中將 a
和 b
的地址傳遞給 Swap
函數,
Swap
函數里邊通過地址間接的操作 main
函數中的a和b,并達到交換的效果就好了。
#include <stdio.h>void Swap2(int*px, int*py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交換前:a=%d b=%d\n", a, b);Swap2(&a, &b);printf("交換后:a=%d b=%d\n", a, b);return 0;
}
將程序改進后,我們直接將變量 a
與 b
的地址傳入進函數中,這次通過地址對變量進行更改,就不會再出現錯誤,
通過地址操作的空間與原變量是綁定的,不再是原變量的拷貝,在改變地址所指向的變量時,我們成功對原變量進行了更改。
看輸出結果:
我們可以看到實現成 Swap2
的方式,順利完成了任務,這里調用 Swap2
函數的時候是將變量的地址傳遞給了函數,
這種函數調用方式叫:傳址調用。
傳址調用,可以讓函數和主調函數之間建立真正的聯系,在函數內部可以修改主調函數中的變量;
所以未來函數中只是需要主調函數中的變量值來實現計算,就可以采用傳值調用。
如果函數內部要修改主調函數中的變量的值,就需要傳址調用。
到此,第十二講 - 深入理解指針(2)部分的內容到此結束
如對文章有更好的意見與建議,一定要告知作者,讀者的反饋對于我十分重要,希望讀者們勤勉勵學,精益求精,
我們下篇文章再見👋。