指針和記憶體
不同記憶體中變量的作用域和聲明周期
作用域 | 生命周期 | |
全局記憶體 | 整個檔案 | 應用程式的生命周期 |
靜态記憶體 | 聲明它的函數内部 | 應用程式的生命周期 |
自動記憶體(局部記憶體) | 聲明它的函數内部 | 限制在函數執行時間内 |
動态記憶體 | 由引用該記憶體的指針決定 | 知道記憶體釋放 |
了解這些記憶體類型可以更好地了解指針。大部分指針用來操作記憶體中的資料,是以了解記憶體的分區群組織方式有助于我們弄清楚指針如何操作記憶體。
為什麼要精通指針
指針有幾種用途,包括:
- 寫出快速高效的代碼;
- 為解決很多類問題提供友善的途徑;
- 支援動态記憶體配置設定;
- 使表達式變得緊湊和簡潔;
- 提供用指針傳遞資料結構的能力而不會帶來龐大的開銷;
- 保護作為參數傳遞給函數的資料。
使用指針過程出現的問題:
- 通路數組和其他資料結構時越界;
- 自動變量消失後被引用;
- 堆上配置設定的記憶體釋放後被引用;
- 記憶體配置設定之前解引指針。
聲明指針
星号兩邊的空白符無關緊要,下列聲明時等價的:
int *pi;
int* pi;
int * pi;
int*pi;
記任這兒點:
- pi的内容最終應該指派為一個整數變量的位址;
- 這些變量沒有被初始化,是以包含的是垃圾資料;
- 指針的實作中沒有内部資訊表明自己指向的是什麼類型的資料或者内容是否合法;
- 不過,指針有類型,而且如果沒有正确使用,編譯器會頻繁抱怨。
如何閱讀聲明
const int *pci;
倒過來讀可以讓我們一點點了解這個聲明:
位址操作符
位址操作符&會傳回操作數的位址。
num = 0;
pi = #
列印指針的值
格式說明符 | 含義 |
%x | 将值顯示為十六進制數 |
%o | 将值顯示為八進制數 |
%p | 将值顯示為實作專用的格式,通常是十六進制數(大寫) |
在不同的平台上用一緻的方式顯示指針的值比較困難。一種方法是把指針轉換為void指針,然後用%p格式說明符來顯示,如下:
printf("value of pi:%p\n",(void*)pi);
用間接引用操作符解引指針
間接引用操作符(*)傳回指針變量指向的值,一般稱為解引指針。下面的例子聲明和初始化了num和pi:
int num = 5;
int *pi = #
# 傳回指針指向的值
printf("%p\n",*pi); //5
指向函數的指針
函數沒有參數也沒有傳回值,指針的名字是foo:
void (*foo)();
null的概念
null不一樣的概念:
- null概念;
- null指針常量;
- NULL宏;
- ASCII字元NULL;
- null字元串;
- null語句。
NULL被指派給指針就意味着指針不指向任何東西。null概念是指指針包含了一個特殊的值,和别的指針不一樣,它沒有指向任何記憶體區域。兩個null指針總是相等的。盡管不常見,但每一種指針類型(如字元指針和整數指針)都可以有對應的null指針類型。
null概念是通過null指針常量來支援的一種抽象。這個常量可能是也可能不是常量0,C程式員不需要關心實際的内部表示。
NULL宏是強制類型轉換為void指針的整數常量0。在很多庫中定義如下:
#define NULL ((void *)0)
這就是我們通常了解為null指針的東西。這個定義一般可以在多種頭檔案中找到,包括stddef.h、stdblib.h和 stdio.h。
如果編譯器用一個非零的位串來表示null,那麼編譯器就有責任在指針上下文中把NULL或0當做null指針,實際的null内部表示由實作定義。使用NULL或0是在語言層面表示null指針的符号。
ASCII字元NUL定義為全0的位元組。然而,這跟null指針不一樣。C的字元串表示為以О值結尾的字元序列。null字元串是空字元串,不包含任何字元。最後,null語句就是隻有一個分号的語句。
接下來我們會看到,null指針對于很多資料結構的實作來說都是很有用的特性,比如連結清單經常用null指針來表示連結清單結尾。
如果要把null值賦給pi,就像下面那樣用NULL:
pi = NULL;
我們可以給指針賦0,但是不能賦任何别的整數值。
pi = 0;
pi = NULL;
pi = 100; //文法錯誤
void指針
void指針是通用指針,用來存放任何資料類型的引用。
void *pv;
它有兩個有趣的性質:
- void指針具有與char指針相同的形式和記憶體對齊方式;
- void指針和别的指針永遠不會相等,不過,兩個指派為NULL的void指針是相等的。
任何指針都可以被賦給void指針,它可以被轉換回原來的指針類型,這樣的話指針的值和原指針的值是相等的。
int num;
int *pi = #
printf("value of pi: %p\n",pi); # 100
void* pw = pi;
pi = (int*) pw ;
printf ("value of pi: %p\n",pi); # 100
注:
void指針隻能做資料指針,而不能用做函數指針。
指針被聲明為全局或靜态,在程式啟動時被初始化為NULL。
指針的長度和類型
指針長度取決于使用的機器和編譯器。比如,在現代Windows 上,指針是32位或64位長。對于DOS和 Windows 3.1來說,指針則是16位或32位長。
記憶體模型
64位機器的出現導緻為不同資料類型配置設定的記憶體在長度上的差異變得明顯。
指針相關的預定義類型
使用指針時經常用到以下四種預定義類型:
- size_t:用于安全地表示長度。
- ptrdiff_t:用于處理指針算術運算。
- intptr_t和uintptr_t:用于存儲指針位址。
size_t
size_t類型表示C中任何對象所能達到的最大長度,它是無符号整數。
size_t用做sizeof操作符的傳回值類型,同時也是很多函數的參數類型,包括malloc和strlen。
在聲明諸如字元數或者數組索引這樣的長度變量時用size_t是好的做法。它經常用于循環計數器.數組索引,有時候還用在指針算術運算上。
intptr_t和uintptr_t
int num;
intptr_t *pi = #
uintptr_t *pu = # //出錯
uintptr_t *pu = (uintptr_t*)# //可強制轉換
指針操作符
操作符 | 名稱 | 含義 |
* | 用來聲明指針 | |
* | 解引 | 用來解引指針 |
-> | 指向 | 用來通路指針引用的結構的字段 |
+ | 加 | 用來對指針做加法 |
- | 減 | 用來對指針做減法 |
== != | 相等、不等 | 比較兩個指針 |
>、>=、<、<= | 大于、大于等于、小于、小于等于 | 比較兩個指針 |
(資料類型) | 轉換 | 改變指針的類型 |
指針算法運算
給指針加上整數
給指針加上一個整數實際上加的數是這個整數和指針資料類型對應位元組數的乘積。
void指針和加法
作為擴充,大部分編譯器都允許給void指針做算術運算,這裡我們假設void指針的長度是4。不過,試圖給void指針加1可能導緻文法錯誤。
給指針減去整數
就像整數可以和指針相加一樣,也能從指針減去整數。減去整數時,位址值會減去資料類型的長度和整數值的乘積。
指針相減
一個指針減去另一個指針會得到兩個位址的內插補點。
這個內插補點通常沒什麼用,但可以判斷數組中的元素順序。
比較指針
指針可以用标準的比較操作符來比較。通常,比較指針沒什麼用。然而,當把指針和數組元素相比時,
比較結果可以用來判斷數組元素的相對順序。
指針的常見用法
指針的用處包括:
- 多層間接引用;
- 常量指針。
多層間接引用
char *titles[] = {"A Tale of Two cities",
"wuthering Heights" , "Don Quixote" ,"odyssey" , "Moby-Dick" , "Hamlet" ."Gulliver's Travels"};
還有兩個數組分别用來維護一個"暢銷書"清單和一個英文書清單。這兩個數組儲存的是titles 數組裡書名的位址,而不是書名的副本。兩個數組都聲明為字元指針的指針。數組元素會儲存titles數組中元素的位址,這樣可以避免對每個書名重複配置設定記憶體,確定每個書名的位置唯一。如果需要修改書名,隻改一個地方就可以了。
char **bestBooks[3];
char **englishBooks[4];
bestBooks[0]= &titles[0];
bestBooks[1] = &titles[3];
bestBooks[2]= &titles [5];
englishBooks[0]= &titles[0];
englishBooks[1]= &titles[1];
englishBooks[2]= &titles[5];
englishBooks[3] = &titles[6];
printf ( %s\n" ,*englishBooks [1]); // wuthering Heights
用多層間接引用可以為代碼的編寫和使用提供更多的靈活性。雖然簡潔引用沒有層次限制,但是使用的層次過多會讓人迷惑、很難維護。
常量與指針
C語言的功能強大還表現在const關鍵字與指針的結合使用上。
指針類型 | 指針是否修改 | 指向指針的資料是否可修改 |
指向非常量的指針 | 是 | 是 |
指向常量的指針 | 是 | 否 |
指向非常量的常量指針 | 否 | 是 |
指向常量的常量指針 | 否 | 否 |
指向常量的指針
指向常量的指針:意味着不能通過指針修改它所引用的值。
int num = 5;
const int limit = 500;
int *pi; //指向整數
const int *pci; //指向整數常量
pi = #
pci = &limit;
pci = # //合法,指針可以改為引用另一個整數常量、或者普通整數
//我們可以解引pci來讀取它,但不能解引它來修改它
把pci聲明為指向整數常量的指針意味着:
- pci可以被修改為指向不同的整數常量;
- pci可以被修改為指向不同的非整數常量;
- 可以解引 pci以讀取資料;
- 不能解引 pci進而修改它指向的資料。
指向非常量的常量指針
指向非常量的常量指針:意味着指針不可變,但是它指向的資料可變。
int num;
int *const cpi = #
- cpi必須被初始化為指向非常量變量;
- cpi不能被修改;
- cpi指向的資料可以被修改。 無論cpi引用什麼,都可以解引cpi然後賦一個新值。
*cpi = limit;
*cpi = 25;
指向非常量的常量指針無法初始化為指向常量:
const int limit = 500;
int *const cpi = &limit; //警告
指向非常量的常量指針在進行初始化後無法再次進行指派:
int num;
int age;
int *const cpi = #
cpi = &age; //非法
指向常量的常量指針
指向常量的常量指針:指向常量的常量指針很少派上用場。這種指針本身不能修改,它指向的資料也不能通過它來修改。
const int * const cpci = &limit;
對于指向常量的常量指針,我們不能:
- 修改指針;
- 修改指針指向的資料。
指向"指向常量的常量指針"的指針
指向常量的指針也可以有多層間接引用。
const int * const cpci = &limit;
const int * const * pcpci;