指针学习
printf("p = %p.\n", p); // %p打印指針和%x打印指針,打印出的值是一樣的
printf("p = 0x%x.\n", p);
一、指針是什么?
1、指針變量和普通變量的區別
- 指針的實質就是個變量,它跟普通變量沒有任何本質區別。指針完整的名字應該叫指針變量,簡稱為指針。
2、為什么需要指針?
- 為了實現間接訪問。在匯編中都有間接訪問,其實就是CPU的尋址方式中的間接尋址。
- 間接訪問(CPU的間接尋址)是CPU設計時決定的,這個決定了匯編語言必須能夠實現間接尋址,又決定了匯編之上的C語言也必須實現簡介尋址。
- 高級語言如Java、C#等沒有指針,那他們怎么實現間接訪問?答案是語言本身幫我們封裝了。
3、指針使用三部曲:定義指針變量、關聯指針變量、解引用
- 當我們int *p定義一個指針變量p時,因為p是局部變量,所以也遵循C語言局部變量的一般規律(定義局部變量并且未初始化,則值是隨機的),所以此時p變量中存儲的是一個隨機的數字。
- 此時如果我們解引用p,則相當于我們訪問了這個隨機數字為地址的內存空間。那這個空間到底能不能訪問不知道(也許行也許不行),所以如果直接定義指針變量未綁定有效地址就去解引用幾乎必死無疑。
- 指針綁定的意義就在于:讓指針指向一個可以訪問、應該訪問的地方(就好象拿著槍瞄準目標的過程一樣),指針的解引用是為了間接訪問目標變量。
4、符號的理解
(1)星號*
- 2種用法:第一種是指針定義時,*結合前面的類型用于表明要定義的指針的類型;第二種功能是指針解引用,解引用時*p表示p指向的變量本身
(2)取地址符&
- 直接加在一個變量的前面,然后取地址符和變量加起來構成一個新的符號,這個符號表示這個變量的地址。
(3)左值與右值
- 放在賦值運算符左邊的就叫左值,右邊的就叫右值。所以賦值操作其實就是:左值 = 右值;
- 當一個變量做左值時,編譯器認為這個變量符號的真實含義是這個變量所對應的那個內存空間;
- 當一個變量做右值時,編譯器認為這個變量符號的真實含義是這個變量的值,也就是這個變量所對應的內存空間中存儲的那個數。
二、野指針問題
1、野指針的概念及危害
(1)野指針,就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的);
(2)野指針很可能觸發運行時段錯誤(Sgmentation fault);
(3)野指針因為指向地址是不可預知的,所以有3種情況
- 第一種是指向不可訪問(操作系統不允許訪問的敏感地址,譬如內核空間)的地址,結果是觸發段錯誤,這種算是最好的情況了;
- 第二種是指向一個可用的、而且沒什么特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程序運行不會出錯,也不會對當前程序造成損害,這種情況下會掩蓋你的程序錯誤,讓你以為程序沒問題,其實是有問題的;
- 第三種情況就是指向了一個可用的空間,而且這個空間其實在程序中正在被使用(譬如說是程序的一個變量x),那么野指針的解引用就會剛好修改這個變量x的值,導致這個變量莫名其妙的被改變,程序出現離奇的錯誤。一般最終都會導致程序崩潰,或者數據被損害。這種危害是最大的。
(5)指針變量如果是局部變量,則分配在棧上,本身遵從棧的規律。
- 即反復使用,使用完不擦除,所以是臟的,本次在棧上分配到的變量的默認值是上次這個棧空間被使用時余留下來的值。
- 因此野指針的值是有一定規律不是完全隨機,但是這個值的規律對我們沒意義。因為不管落在上面野指針3種情況的哪一種,都不是我們想看到的。
2、避免野指針
在指針的解引用之前,一定確保指針指向一個絕對可用的空間。常規的做法是:
- 第一點:定義指針時,同時初始化為NULL;
- 第二點:在指針解引用之前,先去判斷這個指針是不是NULL;
- 第三點:指針使用完之后,將其賦值為NULL;
- 第四點:在指針使用之前,將其賦值綁定給一個可用地址空間。
3、NULL
(1)NULL在C/C++中的定義
#ifdef _cplusplus // 定義這個符號就表示當前是C++環境 #define NULL 0 // 在C++中NULL就是0 #else #define NULL (void *)0 // 在C中NULL是強制類型轉換為void *的0 #endif(2)在C語言中,int *p;你可以p = (int *)0;但是不可以p = 0;因為類型不相同。- NULL的實質其實就是地址0,然后給指針賦初值為NULL,其實就是讓指針指向0地址處。
- 為什么指向0地址處?0地址處作為一個特殊地址(我們認為指針指向這里就表示指針沒有被初始化,就表示是野指針),這個地址0地址在一般的操作系統中都是不可被訪問的,如果C語言程序員不按規矩(不檢查是否等于NULL就去解引用)寫代碼直接去解引用就會觸發段錯誤,這種已經是最好的結果了。
(3)一般在判斷指針是否野指針時,都寫成if (NULL != p)而不是寫成 if (p != NULL)。
三、const關鍵字與指針
1、const修飾指針的4種形式
(1)const關鍵字,在C語言中用來修飾變量,表示這個變量是常量。
(2)const修飾指針有4種形式:
- 第一種:const int *p;
- 第二種:int const *p;
- 第三種:int * const p;
- 第四種:const int * const p;
- 一個const關鍵字只能修飾一個變量,所以關鍵是搞清楚const是修飾誰的。
2、const修飾的變量其實是可以改的
- 在某些單片機環境下,const修飾的變量是不可以改的。
- const修飾的變量到底能不能真的被修改,取決于具體的環境,C語言本身并沒有完全嚴格一致的要求。
- 在gcc中,const是通過編譯器在編譯的時候執行檢查來確保實現的(也就是說const類型的變量不能改是編譯錯誤,不是運行時錯誤。)所以我們只要想辦法騙過編譯器,就可以修改const定義的常量,而運行時不會報錯。
- 更深入一層的原因,是因為gcc把const類型的常量也放在了data段,其實和普通的全局變量放在data段是一樣實現的,只是通過編譯器認定這個變量是const的,運行時并沒有標記const標志,所以只要騙過編譯器就可以修改了。
- const是在編譯器中實現的,編譯時檢查,并非不能騙過。所以在C語言中使用const,就好象是 一種道德約束而非法律約束,所以大家使用const時更多是傳遞一種信息,就是告訴編譯器、也告訴讀程序的人,這個變量是不應該也不必被修改的。
四、深入學習數組
1、從內存角度來理解數組
(1)從內存角度講,數組變量就是一次分配多個變量,而且這多個變量在內存中的存儲單元是依次相連接的。
(2)分開定義多個變量(譬如int a, b, c, d;)和一次定義一個數組(int a[4]);這兩種定義方法相同點是都定義了4個int型變量,而且這4個變量都是獨立的單個使用的;不同點是單獨定義時a、b、c、d在內存中的地址不一定相連,但是定義成數組后,數組中的4個元素地址肯定是依次相連的。
(3)數組中多個變量雖然必須單獨訪問,但是因為他們的地址彼此相連,因此很適合用指針來操作,因此數組和指針天生就叫糾結在一起。
2、從編譯器角度來理解數組
(1)從編譯器角度來講,數組變量也是變量,和普通變量和指針變量并沒有本質不同。
- 變量的本質就是一個地址,這個地址在編譯器中決定具體數值,具體數值和變量名綁定,變量類型決定這個地址的延續長度。
- 搞清楚變量、變量名、變量類型這三個概念的具體含義,很多問題都清楚了。
3、數組中幾個關鍵符號(a a[0] &a &a[0])的理解(前提是 int a[10])
(1)a是數組名
- a做左值時表示整個數組的所有空間(10×4=40字節),又因為C語言規定數組操作時要獨立單個操作,不能整體操作數組,所以a不能做左值;
- a做右值表示數組首元素(數組的第0個元素,也就是a[0])的首地址(首地址就是起始地址,就是4個字節中最開始第一個字節的地址)。
- a做右值等同于&a[0];
(2)a[0]表示數組的首元素,也就是數組的第0個元素。
- 做左值時表示數組第0個元素對應的內存空間(連續4字節);
- 做右值時表示數組第0個元素的值(也就是數組第0個元素對應的內存空間中存儲的那個數);
(3)&a就是數組名a取地址,字面意思來看就應該是數組的地址。
- &a不能做左值(&a實質是一個常量,不是變量因此不能賦值,所以自然不能做左值。);
- &a做右值時表示整個數組的首地址。
(4)&a[0]字面意思就是數組第0個元素的首地址(搞清楚[]和&的優先級,[]的優先級要高于&,所以a先和[]結合再取地址)。
(5)為什么數組的地址是常量?
- 因為數組是編譯器在內存中自動分配的。當我們每次執行程序時,運行時都會幫我們分配一塊內存給這個數組,只要完成了分配,這個數組的地址就定好了,本次程序運行直到終止都無法再改了。那么我們在程序中只能通過&a來獲取這個分配的地址,卻不能去用賦值運算符修改它。
(6)總結
- &a和a做右值時的區別:&a是整個數組的首地址,而a是數組首元素的首地址。這兩個在數字上是相等的,但是意義不相同。意義不相同會導致他們在參與運算的時候有不同的表現。
- a和&a[0]做右值時意義和數值完全相同,完全可以互相替代。
- &a是常量,不能做左值。
- a做左值代表整個數組所有空間,所以a不能做左值。
五、指針與數組
1、以指針方式來訪問數組元素
(1)數組元素使用時不能整體訪問,只能單個訪問。訪問方式有2種:數組形式和指針形式。
- 數組格式訪問數組元素是:數組名[下標]; (注意下標從0開始);
- 指針格式訪問數組元素是:*(指針+偏移量); 如果指針是數組首元素地址(a或者&a[0]),那么偏移量就是下標;指針也可以不是首元素地址而是其他哪個元素的地址,這時候偏移量就要考慮疊加了。
- 數組下標方式和指針方式均可以訪問數組元素,兩者的實質其實是一樣的。在編譯器內部都是用指針方式來訪問數組元素的,數組下標方式只是編譯器提供給編程者一種殼(語法糖)而已。所以用指針方式來訪問數組才是本質的做法。
2、從內存角度理解指針訪問數組的實質
- 數組中各個元素的地址是依次相連的,而且數組還有一個很大的特點(其實也是數組的一個限制)就是數組中各個元素的類型比較相同。類型相同就決定了每個數組元素占幾個字節是相同的(譬如int數組每個元素都占4字節,沒有例外)。這兩個特點就決定了只要知道數組中一個元素的地址,就可以很容易推算出其他元素的地址。
3、指針和數組類型的匹配問題
int *p; int a[5]; p = a; // 類型匹配 int *p; int a[5]; p = &a; // 類型不匹配。p是int *,&a是整個數組的指針int (*)[5]4、總結:指針類型決定了指針如何參與運算
- 指針變量+1,并不是真的加1,而是加1*sizeof(指針類型);
- 如果是int *指針,則+1就實際表示地址+4,如果是char *指針,則+1就表示地址+1;如果是double *指針,則+1就表示地址+8.
- 指針變量+1時實際不是加1而是加1×sizeof(指針類型),主要原因是希望指針+1后剛好指向下一個元素(而不希望錯位)。
六、指針與強制類型轉換
1、變量的數據類型的含義
(1)所有的類型的數據存儲在內存中,都是按照二進制格式存儲的。所以內存中只知道有0和1,不知道是int的、還是float的還是其他類型。
(2)int、char、short等屬于整型,存儲方式(數轉換成二進制往內存中放的方式)是相同的,只是內存格子大小不同(所以這幾種整形就彼此叫二進制兼容格式);而float和double的存儲方式彼此不同,和整形更不同。
- int a = 5;時,編譯器給a分配4字節空間,并且將5按照int類型的存儲方式轉成二進制存到a所對應的內存空間中去(a做左值的);
- 我們printf去打印a的時候(a此時做右值),printf內部的vsprintf函數會按照格式化字符串(就是printf傳參的第一個字符串參數中的%d之類的東西)所代表的類型去解析a所對應的內存空間,解析出的值用來輸出。
- 也就是說,存進去時是按照這個變量本身的數據類型來存儲的(譬如本例中a為int所以按照int格式來存儲);但是取出來時是按照printf中%d之類的格式化字符串的格式來提取的。
- 此時雖然a所代表的內存空間中的10101序列并沒有變(內存是沒被修改的)但是怎么理解(怎么把這些1010轉成數字)就不一定了。
- 譬如我們用%d來解析,那么還是按照int格式解析則值自然還是5;但是如果用%f來解析,則printf就以為a對應的內存空間中存儲的是一個float類型的數,會按照float類型來解析,值自然是很奇怪的一個數字了。
2、指針數據類型轉換實例分析1(int * -> char *)
- int和char類型都是整形,類型兼容的。所以互轉的時候有時候錯有時候對。
- int和char的不同在于char只有1個字節而int有4個字節,所以int的范圍比char大。
- 在char所表示的范圍之內int和char是可以互轉的不會出錯;但是超過了char的范圍后char轉成int不會錯。
3、指針數據類型轉換實例分析2(int * -> float *)
- int和float的解析方式是不兼容的,所以int *轉成float *再去訪問絕對會出錯。
七、指針、數組與sizeof運算符
1、sizeof
- 是C語言的一個運算符(主要sizeof不是函數,雖然用法很像函數),sizeof的作用是用來返回()里面的變量或者數據類型占用的內存字節數。
- sizeof存在的價值?主要是因為在不同平臺下各種數據類型所占的內存字節數不盡相同(譬如int在32位系統中為4字節,在16位系統中為2字節···)。所以程序中需要使用sizeof來判斷當前變量/數據類型在當前環境下占幾個字節。
2、代碼測試
char str[] = ”hello”; //sizeof(str) sizeof(str[0]) strlen(str) char *p=str; //sizeof(p) sizeof(*p) strlen(p)- 32位系統中所有指針的長度都是4,不管是什么類型的指針。
- strlen是一個C庫函數,用來返回字符串的長度(字符串的長度是不計算字符串末尾的'\0'的)。注意strlen接收的參數必須是字符串(字符串的特征是以'\0'結尾)
- sizeof測試一個變量本身,和sizeof測試這個變量的類型,結果是一樣的。
- sizeof(數組名)的時候,數組名不做左值也不做右值,純粹就是數組名的含義。那么sizeof(數組名)實際返回的是整個數組所占用內存空間(以字節為單位的)。
- 函數形參是數組時,實際傳遞是不是整個數組,而是數組的首元素首地址。也就是說函數傳參用數組來傳,實際相當于傳遞的是指針(指針指向數組的首元素首地址)。
八、指針與函數傳參
1、普通變量作為函數形參
- 在子函數內部,形參的值等于實參。原因是函數調用時把實參的值賦值給了形參。值傳遞!
2、數組作為函數形參
- 函數名作為形參傳參時,實際傳遞是不是整個數組,而是數組的首元素的首地址(也就是整個數組的首地址。因為傳參時是傳值,所以這兩個沒區別)。所以在子函數內部,傳進來的數組名就等于是一個指向數組首元素首地址的指針。所以sizeof得到的是4。
- 在子函數內傳參得到的數組首元素首地址,和外面得到的數組首元素首地址的值是相同的。傳址調用就是調用子函數時傳了地址(也就是指針),此時可以通過傳進去的地址來訪問實參。
- 數組作為函數形參時,[]里的數字是可有可無的。因為數組名做形參傳遞的實際只是個指針,根本沒有數組長度這個信息。
3、指針作為函數形參
- 和數組作為函數形參是一樣的。這就好像指針方式訪問數組元素和數組方式訪問數組元素的結果一樣是一樣的。
4、結構體變量作為函數形參
- 結構體變量作為函數形參的時候,實際上和普通變量(類似于int之類的)傳參時表現是一模一樣的。所以說結構體變量其實也是普通變量而已。
- 因為結構體一般都很大,所以如果直接用結構體變量進行傳參,那么函數調用效率就會很低。(因為在函數傳參的時候需要將實參賦值給形參,所以當傳參的變量越大調用效率就會越低)。怎么解決?思路只有一個那就是不要傳變量了,改傳變量的指針(地址)進去。
- 結構體因為自身太大,所以傳參應該用指針來傳(但是程序員可以自己決定,你非要傳結構體變量過去C語言也是允許的,只是效率低了);回想一下數組,為什么C語言設計的時候數組傳參默認是傳的數組首元素首地址而不是整個數組?
5、傳值調用與傳址調用
- C語言本身函數調用時一直是傳值的,只不過傳的值可以是變量名,也可以是變量的指針。
九、輸入型參數與輸出型參數
1、函數為什么需要形參與返回值?
(1)函數名是一個符號,表示整個函數代碼段的首地址,實質是一個指針常量,所以在程序中使用到函數名時都是當地址用的,用來調用這個函數的。
(2)函數體是函數的關鍵,由一對{}括起來,包含很多句代碼,函數體就是函數實際做的工作。
(3)形參列表和返回值
- 形參是函數的輸入部分,返回值是函數的輸出部分。對函數最好的理解就是把函數看成是一個加工機器(程序其實就是數據加工器),形參列表就是這個機器的原材料輸入端;而返回值就是機器的成品輸出端。
- 其實如果沒有形參列表和返回值,函數也能對數據進行加工,用全局變量即可。用全局變量來傳參和用函數參數列表返回值來傳參各有特點,在實踐中都有使用。總的來說,函數參數傳參用的比較多,因為這樣可以實現模塊化編程,而C語言中也是盡量減少使用全局變量。
- 全局變量傳參最大的好處就是省略了函數傳參的開銷,所以效率要高一些;但是實戰中用的最多的還是傳參,如果參數很多傳參開銷非常大,通常的做法是把很多參數打包成一個結構體,然后傳結構體變量指針進去。
2、函數傳參中使用const指針
- const一般用在函數參數列表中,用法是const int *p;(意義是指針變量p本身可變的,而p所指向的變量是不可變的)。
- const用來修飾指針做函數傳參,作用就在于聲明在函數內部不會改變這個指針所指向的內容,所以給該函數傳一個不可改變的指針(char *p = "linux";這種)不會觸發錯誤;而一個未聲明為const的指針的函數,你給他傳一個不可更改的指針的時候就要小心了。
3、函數需要向外部返回多個值時
- 現實編程中,一個函數需要返回多個值是非常普遍的,因此完全依賴于返回值是不靠譜的,通常的做法是用參數來做返回(在典型的linux風格函數中,返回值是不用來返回結果的,而是用來返回0或者負數用來表示程序執行結果是對還是錯,是成功還是失敗)。
- 普遍做法,編程中函數的輸入和輸出都是靠函數參數的,返回值只是用來表示函數執行的結果是對(成功)還是錯(失敗)。如果這個參數是用來做輸入的,就叫輸入型參數;如果這個參數的目的是用來做輸出的,就叫輸出型參數。輸出型參數就是用來讓函數內部把數據輸出到函數外部的。
4、哪個參數做輸入哪個做輸出?
- 函數傳參如果傳的是普通變量(不是指針)那肯定是輸入型參數;
- 如果傳指針就有2種可能性,為了區別,經常的做法是:如果這個參數是做輸入的(通常做輸入的在函數內部只需要讀取這個參數而不會需要更改它)就在指針前面加const來修飾;如果函數形參是指針變量并且還沒加const,那么就表示這個參數是用來做輸出型參數的。譬如C庫函數中strcpy函數。
總結
- 上一篇: 《JavaScript权威指南第7版》第
- 下一篇: 什么是1号信令、7号信令和PRI信令?