C語言高階篇 - 7.指標
1、指標使用三部曲:定義指標變數、關聯指標變數、解引用
(1)當我們int *p定義一個指標變數p時,因為p是區域性變數,所以也遵循C語言區域性變數的一般規律(定義區域性變數並且未初始化,則值是隨機的),所以此時p變數中儲存的是一個隨機的數字。
(2)此時如果我們解引用p,則相當於我們訪問了這個隨機數字為地址的記憶體空間。那這個空間到底能不能訪問不知道(也許行也許不行),所以如果直接定義指標變數未繫結有效地址就去解引用幾乎必死無疑。
(3)定義一個指標變數,不經繫結有效地址就去解引用,就好象拿一個上了鏜的槍隨意轉了幾圈然後開了一槍。
(4)指標繫結的意義就在於:讓指標指向一個可以訪問、應該訪問的地方(就好象拿著槍瞄準目標的過程一樣),指標的解引用是為了間接訪問目標變數(就好象開槍是為了打中目標一樣)
2、怎麼避免野指標?
(1)野指標的錯誤來源就是指標定義了以後沒有初始化,也沒有賦值(總之就是指標沒有明確的指向一個可用的記憶體空間),然後去解引用。
(2)知道了野指標產生的原因,避免方法就出來了:在指標的解引用之前,一定確保指標指向一個絕對可用的空間。
(3)常規的做法是:
第一點:定義指標時,同時初始化為NULL
第二點:在指標解引用之前,先去判斷這個指標是不是NULL
第三點:指標使用完之後,將其賦值為NULL
第四點:在指標使用之前,將其賦值繫結給一個可用地址空間
(4)野指標的防治方案4點絕對可行,但是略顯麻煩。很多人懶得這麼做,那實踐中怎麼處理?在中小型程式中,自己水平可以把握的情況下,不必嚴格參照這個標準;但是在大型程式,或者自己水平感覺不好把握時,建議嚴格參照這個方法。
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;因為型別不相同。
(3)所以NULL的實質其實就是0,然後我們給指標賦初值為NULL,其實就是讓指標指向0地址處。為什麼指向0地址處?2個原因。第一層原因是0地址處作為一個特殊地址(我們認為指標指向這裡就表示指標沒有被初始化,就表示是野指標);第二層原因是這個地址0地址在一般的作業系統中都是不可被訪問的,如果C語言程式設計師不按規矩(不檢查是否等於NULL就去解引用)寫程式碼直接去解引用就會觸發段錯誤,這種已經是最好的結果了。
(4)一般在判斷指標是否野指標時,都寫成
if (NULL != p)
而不是寫成 if (p != NULL)
原因是:如果NULL寫在後面,當中間是==號的時候,有時候容易忘記寫成了=,這時候其實程式已經錯誤,但是編譯器不會報錯。這個錯誤(對新手)很難檢查出來;如果習慣了把NULL寫在前面,當錯誤的把==寫成了=時,編譯器會報錯,程式設計師會發現這個錯誤。
4、const關鍵字與指標
(1)、const修飾指標的4種形式
const關鍵字,在C語言中用來修飾變數,表示這個變數是常量。
const修飾指標有4種形式,區分清楚這4種即可全部理解const和指標。
第一種:const int *p;
第二種:int const *p;
第三種:int * const p;
第四種:const int * const p;
關於指標變數的理解,主要涉及到2個變數:第一個是指標變數p本身,第二個是p指向的那個變數(*p)。一個const關鍵字只能修飾一個變數,所以弄清楚這4個表示式的關鍵就是搞清楚const放在某個位置是修飾誰的
(2)、const修飾的變數真的不能改嗎?
課堂練習說明:const修飾的變數其實是可以改的(前提是gcc環境下)。
在某些微控制器環境下,const修飾的變數是不可以改的。const修飾的變數到底能不能真的被修改,取決於具體的環境,C語言本身並沒有完全嚴格一致的要求。
在gcc中,const是通過編譯器在編譯的時候執行檢查來確保實現的(也就是說const型別的變數不能改是編譯錯誤,不是執行時錯誤。)所以我們只要想辦法騙過編譯器,就可以修改const定義的常量,而執行時不會報錯。
更深入一層的原因,是因為gcc把const型別的常量也放在了data段,其實和普通的全域性變數放在data段是一樣實現的,只是通過編譯器認定這個變數是const的,執行時並沒有標記const標誌,所以只要騙過編譯器就可以修改了。
(3)、const究竟應該怎麼用
const是在編譯器中實現的,編譯時檢查,並非不能騙過。所以在C語言中使用const,就好象是 一種道德約束而非法律約束,所以大家使用const時更多是傳遞一種資訊,就是告訴編譯器、也告訴讀程式的人,這個變數是不應該也不必被修改的。
5、陣列中幾個關鍵符號(a a[0] &a &a[0])的理解(前提是 int a[10])
(1)、這4個符號搞清楚了,陣列相關的很多問題都有答案了。理解這些符號的時候要和左值右值結合起來,也就是搞清楚每個符號分別做左值和右值時的不同含義。
(2)、a就是陣列名。a做左值時表示整個陣列的所有空間(10×4=40位元組),又因為C語言規定陣列操作時要獨立單個操作,不能整體運算元組,所以a不能做左值;a做右值表示陣列首元素(陣列的第0個元素,也就是a[0])的首地址(首地址就是起始地址,就是4個位元組中最開始第一個位元組的地址)。a做右值等同於&a[0];
(3)、a[0]表示陣列的首元素,也就是陣列的第0個元素。做左值時表示陣列第0個元素對應的記憶體空間(連續4位元組);做右值時表示陣列第0個元素的值(也就是陣列第0個元素對應的記憶體空間中儲存的那個數)
(4)、&a就是陣列名a取地址,字面意思來看就應該是陣列的地址。&a不能做左值(&a實質是一個常量,不是變數因此不能賦值,所以自然不能做左值。);&a做右值時表示整個陣列的首地址。
(5)、&a[0]字面意思就是陣列第0個元素的首地址(搞清楚[]和&的優先順序,[]的優先順序要高於&,所以a先和[]結合再取地址)。做左值時表示陣列首元素對應的記憶體空間,做右值時表示陣列首元素的值(也就是陣列首元素對應的記憶體空間中儲存的那個數值)。做右值時&a[0]等同於a。
解釋:為什麼陣列的地址是常量?因為陣列是編譯器在記憶體中自動分配的。當我們每次執行程式時,執行時都會幫我們分配一塊記憶體給這個陣列,只要完成了分配,這個陣列的地址就定好了,本次程式執行直到終止都無法再改了。那麼我們在程式中只能通過&a來獲取這個分配的地址,卻不能去用賦值運算子修改它。
(6)、總結:
*1:&a和a做右值時的區別:&a是整個陣列的首地址,而a是陣列首元素的首地址。這兩個在數字上是相等的,但是意義不相同。意義不相同會導致他們在參與運算的時候有不同的表現。
*2:a和&a[0]做右值時意義和數值完全相同,完全可以互相替代。
*3:&a是常量,不能做左值。
*4:a做左值代表整個陣列所有空間,所以a不能做左值。
6、指標的資料型別的含義
(1)、指標的本質是:變數,指標就是指標變數
(2)、一個指標涉及2個變數:一個是指標變數自己本身,一個是指標變數指向的那個變數
(3)、int *p;定義指標變數時,p(指標變數本身)是int *型別,*p(指標指向的那個變數)是int型別的。
(4)、int *型別說白了就是指標型別,只要是指標型別就都是佔4位元組,解析方式都是按照地址的方式來解析(意思是裡面存的32個二進位制加起來表示一個記憶體地址)的。結論就是:所有的指標型別(不管是int * 還是char * 還是double *)的解析方式是相同的,都是地址。
(5)、對於指標所指向的那個變數來說,指標的型別就很重要了。指標所指向的那個變數的型別(它所對應的記憶體空間的解析方法)要取決於指標型別。譬如指標是int *的,那麼指標所指向的變數就是int型別的。
7、指標、陣列與sizeof運算子
(1)、sizeof是C語言的一個運算子(主要sizeof不是函式,雖然用法很像函式),sizeof的作用是用來返回()裡面的變數或者資料型別佔用的記憶體位元組數。
(2)、sizeof存在的價值?主要是因為在不同平臺下各種資料型別所佔的記憶體位元組數不盡相同(譬如int在32位系統中為4位元組,在16位系統中為2位元組···)。所以程式中需要使用sizeof來判斷當前變數/資料型別在當前環境下佔幾個位元組。
(3)、32位系統中所有指標的長度都是4,不管是什麼型別的指標。
(4)、strlen是一個C庫函式,用來返回一個字串的長度(注意,字串的長度是不計算字串末尾的'\0'的)。一定要注意strlen接收的引數必須是一個字串(字串的特徵是以'\0'結尾)
(5)、sizeof測試一個變數本身,和sizeof測試這個變數的型別,結果是一樣的。
(6)、sizeof(陣列名)的時候,陣列名不做左值也不做右值,純粹就是陣列名的含義。那麼sizeof(陣列名)實際返回的是整個陣列所佔用記憶體空間(以位元組為單位的)。
(7)、函式形參是陣列時,實際傳遞是不是整個陣列,而是陣列的首元素首地址。也就是說函式傳參用陣列來傳,實際相當於傳遞的是指標(指標指向陣列的首元素首地址)。
8、指標與函式傳參
(1)、普通變數作為函式形參
函式傳參時,普通變數作為引數時,形參和實參名字可以相同也可以不同,實際上都是用實參來替代相對應的形參的。
在子函式內部,形參的值等於實參。原因是函式呼叫時把實參的值賦值給了形參。
這就是很多書上寫的“傳值呼叫”(相當於實參做右值,形參做左值)
(2)、陣列作為函式形參
函式名作為形參傳參時,實際傳遞是不是整個陣列,而是陣列的首元素的首地址(也就是整個陣列的首地址。因為傳參時是傳值,所以這兩個沒區別)。所以在子函式內部,傳進來的陣列名就等於是一個指向陣列首元素首地址的指標。所以sizeof得到的是4.
在子函式內傳參得到的陣列首元素首地址,和外面得到的陣列首元素首地址的值是相同的。很多人把這種特性叫做“傳址呼叫”(所謂的傳址呼叫就是呼叫子函式時傳了地址(也就是指標),此時可以通過傳進去的地址來訪問實參。)
陣列作為函式形參時,[]裡的數字是可有可無的。為什麼?因為陣列名做形參傳遞的實際只是個指標,根本沒有陣列長度這個資訊。
(3)、指標作為函式形參
只有一句話:和陣列作為函式形參是一樣的.這就好像指標方式訪問陣列元素和陣列方式訪問陣列元素的結果一樣是一樣的。
(4)、結構體變數作為函式形參
結構體變數作為函式形參的時候,實際上和普通變數(類似於int之類的)傳參時表現是一模一樣的。所以說結構體變數其實也是普通變數而已。
因為結構體一般都很大,所以如果直接用結構體變數進行傳參,那麼函式呼叫效率就會很低。(因為在函式傳參的時候需要將實參賦值給形參,所以當傳參的變數越大呼叫效率就會越低)。怎麼解決?思路只有一個那就是不要傳變量了,改傳變數的指標(地址)進去。
結構體因為自身太大,所以傳參應該用指標來傳(但是程式設計師可以自己決定,你非要傳結構體變數過去C語言也是允許的,只是效率低了);回想一下陣列,為什麼C語言設計的時候陣列傳參預設是傳的陣列首元素首地址而不是整個陣列?
(5)、傳值呼叫與傳址呼叫
#include <stdio.h>
void swap1(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
printf("in swap1, a = %d, b = %d.\n", a, b);
}
void swap2(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
printf("in swap1, *a = %d, *b = %d.\n", *a, *b);
}
傳值呼叫描述的是這樣一種現象:x和y作為實參,自己並沒有真身進入swap1函式內部,而只是拷貝了一份自己的副本(副本具有和自己一樣的值,但是是不同的變數)進入子函式swap1,然後我們在子函式swap1中交換的實際是副本而不是x、y真身。所以在swap1內部確實是交換了,但是到外部的x和y根本沒有受影響。
在swap2中x和y真的被改變了(但是x和y真身還是沒有進入swap2函式內,而是swap2函式內部跑出來把外面的x和y真身改了)。實際上實參x和y永遠無法真身進入子函式內部(進去的只能是一份拷貝),但是在swap2我們把x和y的地址傳進去給子函數了,於是乎在子函式內可以通過指標解引用方式從函式內部訪問到外部的x和y真身,從而改變x和y。
結論:這個世界上根本沒有傳值和傳址這兩種方式,C語言本身函式呼叫時一直是傳值的,只不過傳的值可以是變數名,也可以是變數的指標。
9、輸入型引數與輸出型引數
(1)、函式為什麼需要形參與返回值
函式名是一個符號,表示整個函式程式碼段的首地址,實質是一個指標常量,所以在程式中使用到函式名時都是當地址用的,用來呼叫這個函式的。
函式體是函式的關鍵,由一對{}括起來,包含很多句程式碼,函式體就是函式實際做的工作。
形參列表和返回值。形參是函式的輸入部分,返回值是函式的輸出部分。對函式最好的理解就是把函式看成是一個加工機器(程式其實就是資料加工器),形參列表就是這個機器的原材料輸入端;而返回值就是機器的成品輸出端。
其實如果沒有形參列表和返回值,函式也能對資料進行加工,用全域性變數即可。用全域性變數來傳參和用函式引數列表返回值來傳參各有特點,在實踐中都有使用。總的來說,函式引數傳參用的比較多,因為這樣可以實現模組化程式設計,而C語言中也是儘量減少使用全域性變數。
全域性變數傳參最大的好處就是省略了函式傳參的開銷,所以效率要高一些;但是實戰中用的最多的還是傳參,如果引數很多傳參開銷非常大,通常的做法是把很多引數打包成一個結構體,然後傳結構體變數指標進去。
(2)、函式傳參中使用const指標
const一般用在函式引數列表中,用法是const int *p;(意義是指標變數p本身可變的,而p所指向的變數是不可變的)。
const用來修飾指標做函式傳參,作用就在於宣告在函式內部不會改變這個指標所指向的內容,所以給該函式傳一個不可改變的指標(char *p = "linux";這種)不會觸發錯誤;而一個未宣告為const的指標的函式,你給他傳一個不可更改的指標的時候就要小心了。
(3)、函式需要向外部返回多個值時怎麼辦?
一般來說,函式的收入部分就是函式引數,輸出部分就是返回值。問題是函式的引數可以有很多個,而返回值只能有1個。這就造成我們無法讓一個函式返回多個值。
現實程式設計中,一個函式需要返回多個值是非常普遍的,因此完全依賴於返回值是不靠譜的,通常的做法是用引數來做返回(在典型的linux風格函式中,返回值是不用來返回結果的,而是用來返回0或者負數用來表示程式執行結果是對還是錯,是成功還是失敗)。
普遍做法,程式設計中函式的輸入和輸出都是靠函式引數的,返回值只是用來表示函式執行的結果是對(成功)還是錯(失敗)。如果這個引數是用來做輸入的,就叫輸入型引數;如果這個引數的目的是用來做輸出的,就叫輸出型引數。
輸出型引數就是用來讓函式內部把資料輸出到函式外部的。
(4)、總結
看到一個函式的原型後,怎麼樣一眼看出來哪個引數做輸入哪個做輸出?函式傳參如果傳的是普通變數(不是指標)那肯定是輸入型引數;如果傳指標就有2種可能性了,為了區別,經常的做法是:如果這個引數是做輸入的(通常做輸入的在函式內部只需要讀取這個引數而不會需要更改它)就在指標前面加const來修飾;如果函式形參是指標變數並且還沒加const,那麼就表示這個引數是用來做輸出型引數的。
譬如C庫函式中strcpy函式