文章目錄
- 深入理解指針(二)
- 1.const修飾指針
- 1.1 const修飾變量
- 1.2 const修飾指針變量
- 2.野指針
- 2.1 野指針成因
- 1.指針未初始化
- 2. 指針越界訪問
- 3.指針指向的空間釋放
- 2.2 如何規避野指針
- 2.2.1 指針初始化
- 2.2.2 小心指針越界
- 2.2.3 指針變量不再使?時,及時置NULL,指針使?之前檢查有效性
- 2.2.4 避免返回局部變量的地址
- 3.assert斷?
- 4.指針的使用和傳址調用
- 4.1 strlen的模擬實現
- 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。
但是如果我們繞過n,使?n的地址,去修改n就能做到了,雖然這樣做是在打破語法規則。
#include <stdio.h>
int main()
{const int n = 0;//n具備了常屬性(不能被修改了),a在這還是變量,但在cpp中是常量printf("n = %d\n", n);int* p = &n;*p = 20;//*是解引用操作符(間接訪問操作符)通過p存放的地址找到p指向的對象printf("n = %d\n", n);return 0;
}
輸出結果:
我們可以看到這??個確實修改了,但是我們還是要思考?下,為什么n要被const修飾呢?就是為了 不能被修改,如果p拿到n的地址就能修改n,這樣就打破了const的限制,這是不合理的,所以應該讓 p拿到n的地址也不能修改n,那接下來怎么做呢?
1.2 const修飾指針變量
?般來講const修飾指針變量,可以放在的左邊,也可以放在的右邊,意義是不?樣的。
nt * p;//沒有const修飾?
int const * p;//const 放在*的左邊做修飾
int * const p;//const 放在*的右邊做修飾
我們看下?代碼,來分析具體分析?下:
#include <stdio.h>
//代碼1 - 測試?const修飾的情況
void test1()
{int n = 10;int m = 20;int* p = &n;*p = 20;//ok?yesp = &m; //ok?yes
}
//代碼2 - 測試const放在*的左邊情況
void test2(){int n = 10;int m = 20;const int* p = &n;//限制指針指向內容,不能通過指針來修改*p = 20;//ok?nop = &m; //ok?yes
}
//代碼3 - 測試const放在*的右邊情況
void test3()
{int n = 10;int m = 20;int * const p = &n;//此時限制指針變量p本身,原本放的是n的地址,不能改成m的地址//但是可以通過指針改變指針指向對象的內容 也就是n的值*p = 20; //ok?yesp = &m; //ok?no
}
//代碼4 - 測試*的左右兩邊都有const
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20; //ok?nop = &m; //ok?no
}
int main()
{//測試?const修飾的情況 test1();//測試const放在*的左邊情況 test2();//測試const放在*的右邊情況 test3();//測試*的左右兩邊都有const test4();return 0;
}
結論:const修飾指針變量的時候
? const如果放在的左邊,修飾的是指針指向的內容,保證指針指向的內容不能通過指針來改變。但是指針變量本?的內容可變。
? const如果放在的右邊,修飾的是指針變量本?,保證了指針變量的內容不能修改,但是指針指向的內容,可以通過指針改變。
2.野指針
概念:野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
2.1 野指針成因
1.指針未初始化
#include <stdio.h>
int main()
{ int *p;//局部變量指針未初始化,默認為隨機值 *p = 20;return 0;
}
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;
}
3.指針指向的空間釋放
#include <stdio.h>
int* test()
{int n = 100;return &n;
}
int main()
{int*p = test();printf("%d\n", *p);//n的地址放進了p,但是空間已經釋放,p非法訪問return 0;
}
2.2 如何規避野指針
2.2.1 指針初始化
如果明確知道指針指向哪?就直接賦值地址,如果不知道指針應該指向哪?,可以給指針賦值NULL. NULL 是C語?中定義的?個標識符常量,值是0,0也是地址,這個地址是?法使?的,讀寫該地址 會報錯。
#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 小心指針越界
?個程序向內存申請了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是 越界訪問。
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置為NULL p = 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 語句的前?,定義?個宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新編譯程序,編譯器就會禁??件中所有的 assert() 語句。如果程序?出現問題,可以移 除這條 #define NDEBUG 指令(或者把它注釋掉),再次編譯,這樣就重新啟?了 assert() 語 句。
assert() 的缺點是,因為引?了額外的檢查,增加了程序的運?時間。
?般我們可以在 Debug 中使?,在 Release 版本中選擇禁? assert 就?,在 VS 這樣的集成開 發環境中,在 Release 版本中,直接就是優化掉了。這樣在debug版本寫有利于程序員排查問題, 在 Release 版本不影響??使?時程序的效率。
4.指針的使用和傳址調用
4.1 strlen的模擬實現
庫函數strlen的功能是求字符串?度,統計的是字符串中 \0 之前的字符的個數。 函數原型如下:
size_t strlen ( const char * str );
參數str接收?個字符串的起始地址,然后開始統計字符串中 \0 之前的字符個數,最終返回?度。 如果要模擬實現只要從起始地址開始向后逐個字符的遍歷,只要不是 \0 字符,計數器就+1,這樣直 到 \0 就停?。 參考代碼如下:
int my_strlen(const char * str)
{int count = 0;assert(str);//assert(str!=NULL)while(*str)// while(*str !='\0') char屬于整型家族{count++;str++;}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;
}
我們可以看到實現成Swap2的?式,順利完成了任務,這?調?Swap2函數的時候是將變量的地址傳 遞給了函數,這種函數調??式叫:傳址調?。
*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;
}
[外鏈圖片轉存中...(img-Eq2WLgKI-1744880216123)].assets\image-20250417165536566.png)我們可以看到實現成Swap2的?式,順利完成了任務,這?調?Swap2函數的時候是將變量的地址傳 遞給了函數,這種函數調??式叫:**傳址調?。**傳址調?,可以讓函數和主調函數之間建?真正的聯系,在函數內部可以修改主調函數中的變量;所 以未來函數中只是需要主調函數中的變量值來實現計算,就可以采?傳值調?。如果函數內部要修改 主調函數中的變量的值,就需要傳址調?。