前言:上期我們介紹了如何理解地址,內存,以及指針的一些基礎知識和運算;這期我們來介紹一下const修飾指針,野指針,assert斷言,指針的傳址調用。
上一篇指針(1)
文章目錄
- 一,const修飾指針
- 1,const修飾變量
- 2,const修飾指針變量
- 第一種const在*左邊
- 第二種const在*右邊
- 第三種const在*左和右邊
- 二,野指針
- 1,野指針的成因
- 2,如何規避野指針
- 三,assert斷言
- 四,指針的使用和傳址調用
- 1,傳值調用
- 2,傳址調用
- 五,strlen的模擬實現
一,const修飾指針
1,const修飾變量
我們先從const修飾變量說起,在學函數的時候我們知道被const修飾后的變量在C語言中變成了常變量,它是一個變量但具有常量屬性(不變的屬性)是不能被修改的。
#include<stdio.h>
int main()
{int n=0;n=100;//沒有加上const之前n可以修改const int m=0;m=100;//加上了const之后m就不能被修改了return 0;
}
我們可以看到被const修飾后的m修改時會報錯,那 我們怎么驗證它是一個變量呢?還記得在數組篇說過:數組定義變量的大小時必須是一個常量而不能是一個變量嗎?這樣我們就可以用數組來檢驗,如果數組報錯說明是變量,不報錯說明是常量。
#include<stdio.h>
int main()
{const int n=10;int arr[n]={0};return 0;
}
我們們看到編譯器報錯就說明被const修飾的n就是一個變量。那么既然const修飾變量會導致變量不能被修改,那么const修飾指針會有什么樣的結果呢?接下來就來看看const修飾指針會有什么樣的效果。
延用上面的例子分析一下為什么m不能被修改?假如想修改該怎么做呢?
#include<stdio.h>
int main()
{int n=0;n=100;//沒有加上const之前n可以修改const int m=0;m=100;//加上了const之后m就不能被修改了return 0;
}
上述代碼中m是不能被修改的,其實m本質是變量,只不過被const修飾后,在語法上加了限制,只要我們在代碼中對m就?修改,就不符合語法規則,就報錯致使沒法直接修改n。
我們說指針的好處就是可以間接訪問內存,但如果我們繞過m取得m的地址然后再去修改m就可以做到了,雖然這是在打破語法規則但確實能夠達到我們的目的。
#include<stdio.h>
int main()
{const int m=0;m=100;//加上了const之后m就不能被修改了int *p=&m;*p=100;printf("%d\n",*p);return 0;
}
顯然m被修改了,但是思考一下我們加const的用意是什么?我們其實是要固定m的值使他不能被修改,但是我們卻有方法讓他修改,這就打破了const的限制所以這與我們的初衷是違背的。這是一個漏洞要避免這個漏洞要怎么做呢?
答案是用const來修飾指針,使指針變量p拿不到m的地址從而無法間接修改m的值。
2,const修飾指針變量
首先const修飾指針變量有三種情況:
int * p;//沒有const修飾
int const * p;//const 放在*的左邊做修飾
int * const p;//const 放在*的右邊做修飾
const int *const p;//const 放在*左右兩邊的修飾
第一種const在*左邊
我們先來看一段代碼:
#include<stdio.h>
int main()
{int n=0;int *p1=&n;*p1=100;printf("%d\n",*p1);//沒加const 打印的結果是100int m=0;const int *p2=&m;*p2=100;printf("%d\n",*p2);//加了const *p2這行代碼報錯return 0;
}
由此我們可以知道在
*號
左邊加上const是限制解引用這個操作,即限制修改指針變量p所指向的變量的內容(即*p
),但能否修改指針變量本身(p
)來改變p所儲存的地址從而再解引用改變所想要改變的值呢?我們不妨寫個代碼來驗證一下:
#include<stdio.h>
int main()
{int n = 0;const int* p = &n;int m = 100;//創建第三變量int* x = &n;//要想改變n的值只能再重新創建一個指針變量p = &m;//*p = 100;//指針變量p在*左邊加了const 所以解引用已經被禁用*x = 10;printf("*p=m=%d\n", *p);printf("*x=n=%d\n", *x);printf("n=%d\n", n);return 0;
}
從運行結果我們可以得出3個結論: 1,要想改變*p
必須借助第三變量m才能改變,但是是不會改變n的值的!2,指針變量p被const修飾后要想改變n的值只能通過再創建一個指針變量來改變!3,指針變量被const修飾后,*p
是無論如何都不能使用了,即使是改變了p也依然不能解引用!
第二種const在*右邊
來看一段代碼
還是上面的代碼我們改一下
#include<stdio.h>
int main()
{int n = 0;int* const p = &n;//在*右邊加const//p = &m;//指針變量p在*右邊加了const p所存的地址就已經固定不能更改了*p = 100;printf("*p=%d\n", *p);printf("n=%d\n", n);return 0;
}
從運行結果上來看我們可以知道在*右邊加上const 是限制了指針變量p,*p
是可以使用的。上面的代碼中由于 *p
可以使用所以改變*p
就相當于改變了n。
第三種const在*左和右邊
顯然我們知道const放在*的左右兩邊會導致*p
和p都無法使用,下面來看代碼:
#include<stdio.h>
int main()
{ int n = 10; int m = 20; int const * const p = &n; *p = 20; //ok? nop = &m; //ok? no
}
將代碼復制到vs里就會發現報錯,結果當然與我們猜想的一樣。下面給出一個圖來進行總結:
結論:const修飾指針變量的時候
? const如果放在的左邊,修飾的是指針指向的內容,保證指針指向的內容不能通過指針來改變。 但是指針變量本?的內容可變。
? const如果放在的右邊,修飾的是指針變量本?,保證了指針變量的內容不能修改,但是指針指 向的內容,可以通過指針改變。
二,野指針
我們先來了解一下它的概念:野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)。
1,野指針的成因
野指針有幾種成因分別是:
- 指針未初始化
int *p; *p=20 *p沒有指向的對象所以默認為隨機值
- 指針訪問越界
#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;
}
- 指針指向的空間被回收
#include <stdio.h>
int* test()
{ int n = 100; return &n;
}
int main()
{ int*p = test(); printf("%d\n", *p); return 0;
}
由于n是一個局部變量,在函數調用完后就已經被回收了 (還給操作系統了),所以返回n的地址是一個隨機值。
指針指向的內存空間不屬于當前程序,這個時候就是野指針。
2,如何規避野指針
了解了野指針的成因后我們自然有辦法去規避它。
1. 及時初始化
如果我們不明確指針指向的對象就及時給指針初始化:int *p=NULL
- NULL 是C語言中定義的?個標識符常量,值是0,0也是地址,這個地址是無法使用的,讀寫該地址
會報錯。
如果我們明確指針所指向的對象就要及時給指針初始化:int n=0; int *p=&n;
2.小心指針越界
以上面的代碼來舉例:
#include <stdio.h>
int main()
{ int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i = 0; i <= 10; i++) //i必須小于等于10防止指針越界{ *(p++) = i; }//程序走到這指針就越界了要即使置為空指針*p=NULL;int m=0;*p=&m;//下次使用該指針的時候再進行判斷if(*p!=NULL){}return 0;
}
當指針變量指向?塊區域的時候,我們可以通過指針訪問該區域,后期不再使?這個指針訪問空間的 時候,我們可以把該指針置為NULL。因為約定俗成的?個規則就是:只要是NULL指針就不去訪問, 同時使?指針之前可以判斷指針是否為NULL。
為了更加深入的理解我們舉一個例子:
我們可以把野指針想象成野狗,野狗放任不管是?常危險的,所以我們可以找?棵樹把野狗拴起來, 就相對安全了,給指針變量及時賦值為NULL,其實就類似把野狗栓起來,就是把野指針暫時管理起來。
不過野狗即使拴起來我們也要繞著?,不能去挑逗野狗,有點危險;對于指針也是,在使?之前,我 們也要判斷是否為NULL,看看是不是被拴起來起來的野狗,如果是不能直接使?,如果不是我們再去 使?。
3. 不要返回局部變量的地址
當局部變量的作用域與該指針的作用域不同時,給指針返回局部變量的地址就相當于沒有初始化指針變成了野指針。
三,assert斷言
assert.h 頭文件定義了宏 assert() ,用于在運?時確保程序符合指定條件,如果不符合,就報 錯終?運?。這個宏常常被稱為“斷言”。
舉個例子我們運行下面的代碼看看會有什么結果:
#include<stdio.h>
#include<assert.h>//assert.h 頭文件定義了宏 assert() 所以要包含assert.h
int main()
{int n=0;int *p=NULL;assert(p!=NULL);
}
上?代碼在程序運行到這一行語句
assert(p!=NULL)
時,驗證變量 p 是否等于 NULL 。如果確實不等于 NULL ,程序 繼續運行,否則就會終止運行,并且給出報錯信息提示。
assert() 宏接受?個表達式作為參數。如果該表達式為真(返回值?零), assert() 不會產? 任何作?,程序繼續運?。如果該表達式為假(返回值為零), assert() 就會報錯,在標準錯誤 流 stderr 中寫??條錯誤信息,顯?沒有通過的表達式,以及包含這個表達式的文件名和行號。
當然assert不僅僅能用來判斷指針,還可以判斷非指針的問題,上代碼:
#include<stdio.h>
#include<assert.h>
int main()
{int a=10;scanf("%d",&a);assert(a==10);
}
我們輸入15看看有什么結果:
我們看到編譯器直接報錯,其原因是assert括號內表達式的值為假返回0所以編譯器直接報錯,那如果我們輸入10呢?
我們可以看到輸入10編譯器就不會報錯,因為assert括號內表達式值為真返回非0所以不報錯。
assert() 的使?對程序員是?常友好的,使? assert() 有?個好處:
1. 它不僅能?動標識文件和 出問題的行號
*2.還有?種無需更改代碼就能開啟或關閉 assert() 的機制
該機制是如果已經確認程序沒有問 題,不需要再做斷,就在 #include <assert.h> 語句的前?,定義?個宏 NDEBUG 。 例如(以上面的代碼來舉例):
#define NDEBUG
#include<stdio.h>
#include<assert.h>
int main()
{int a=10;scanf("%d",&a);assert(a==10);
}
我們剛剛輸入15編譯器會報錯,現在我們在 #include<assert.h>
語句前面加了 #define NDEBUG
這句話后輸入15看看會不會報錯:
發現沒有報錯,但前提是要在 #include<assert.h>
這句話前 加上 #define NDEBUG
這句話才行!
然后,重新編譯程序,編譯器就會禁用文件中所有的 assert() 語句。如果程序?出現問題,可以移 除這條 #define NDEBUG 指令(或者把它注釋掉),再次編譯,這樣就重新啟?了 assert() 語句。
這么好用的assert當然也有缺點:
> assert() 的缺點是,因為引?了額外的檢查,增加了程序的運?時間。 ?般我們可以在 Debug 中使?,在 Release 版本中選擇禁? assert 就?,在 VS 這樣的集成開 發環境中,在 Release 版本中,直接就是優化掉了。這樣在debug版本寫有利于程序員排查問題, 在 Release 版本不影響??使?時程序的效率。
介紹完了assert我們就來看看指針的使用和傳址調用
四,指針的使用和傳址調用
我們學習指針就是為了使用指針來解決問題,但有什么問題是非指針不可得呢?
舉個例子,寫一個函數完成兩個數得交換(我們先用函數的傳值調用)看看能不能實現:
1,傳值調用
#include<stdio.h>
void swap1(int a,int b)
{int z=0;z=a;a=b;b=z;
}
int main()
{int a=10;int b=20;printf("交換前a=%d b=%d\n",a,b);swap1(a,b);printf("交換后a=%d b=%d\n",a,b);return 0;
}
我們看到并沒有交換,因為這是傳值調用形參只是實參的一份零時拷貝,形參的改變不影響實參。所以Swap1是失敗的了。
如果對傳值調用還不理解也可以看看我在函數篇講的形參和實參就知道了
傳送門:函數(上)
那怎么辦呢?
我們現在要解決的就是當調?Swap函數的時候,Swap函數內部操作的就是main函數中的a和b,直接 將a和b的值交換了。那么就可以使?指針了,在main函數中將a和b的地址傳遞給Swap函數,Swap 函數?邊通過地址間接的操作main函數中的a和b,并達到交換的效果就好了。
2,傳址調用
還是上面的代碼我們修改一下:
#include<stdio.h>
void swap2(int *pa,int *pb)
{int z=0;z=*pa;//z=a*pa=*pb;//a=b*pb=z;//b=z
}
int main()
{int a=10;int b=20;int *pa=&a;int *pb=&b;printf("交換前a=%d b=%d\n",a,b);swap2(&a,&b);printf("交換后a=%d b=%d\n",a,b);return 0;
}
我們將a和b的地址傳給形參pa和pb這樣形參和實參就共用一塊內存空間,所以形參的改變會影響實參。這就是傳址調用!
傳址調用,可以讓函數和主調函數之間建立真正的聯系,在函數內部可以修改主調函數中的變量;所 以未來函數中只是需要主調函數中的變量值來實現計算,就可以采用傳值調用。如果函數內部要修改 主調函數中的變量的值,就需要傳址調用。
五,strlen的模擬實現
學完了const和assert斷言后我們對上次模擬strlen的代碼進行修改:
1. 加上const修飾
2. 加上assert斷言
#include <assert.h>
size_t my_strlen(const char* p)//在*左邊加const防止arr內容被修改
{size_t count = 0;assert(p != NULL);//加上assert斷言避免傳入的是空指針!while (*p){count++;//計數器p++;}return count;
}int main()
{char arr[] = "abcdef";//a b c d e f \0size_t len = my_strlen(NULL);printf("%zd\n", len);return 0;
}
好了以上就是本章的全部內容啦!
感謝能夠看到這里的讀者,如果我的文章能夠幫到你那我甚是榮幸,文章有任何問題都歡迎指出!制作不易還望給一個免費的三連,你們的支持就是我最大的動力!