知識內容總結
為什么要學習指針?
我們已經學習了如何用數組存放多個相同類型的數據並進行運算,但數組的長度在定義時必須給定以后不能再改變。如果事先無法確定需要處理數據數量,應該如何處理呢?一種方法是估計一個上限,並將該上限作為數組長度,這常常會造成空間浪費;另一種方法是利用指針實現存儲空間的動態分配。
指針是C語言中一個非常重要的概念,也是C語言的特色之一。使用指針可以對復雜數據進行處理,能對計算機的內存分配進行控制,在函數調用中使用指針還可以返回多個值。同時指針也可以作為函數的參數,也可以用於數組和字符處理,實現更多功能。
思維導圖:
地址和指針
這是計算機中的兩個重要概念,在程序運行過程中,變量或者程序代碼被存儲在以字節為單位組織的存儲器中。
地址
在C語言中,如果定義了一個變量,在編譯時就會根據該變量的類型給它分配相應大小的內存單元。計算機為了對內存單元中的數據進行操作,一般是按“地址”存取的,也就是說對內存單元進行標識編號。如果把存儲器看成一個建築物,建築物內的房間就是存儲器單元,房間號就是地址。
- 要注意區分內存單元的內容和內存單元的地址。
指針
在C程序中還有一種使用變量的方法.即通過變量的地址進行操作:用指針訪問內存和操縱地址。指針是用來存放內存地址的變量,如果一個措針變量的值是另一個變量的地址,就稱該指針變量指向那個變量。
取地址運算
在前面的章節中,已經多次看到了把地址作為scanf()的輸人參數的甩法,例如,
scanf("%d",&n);
把輸入的值存儲到變量所在的內存單元里。其中&n表示變量n的內存地址或存儲位置。這里的稱為地址運算符,是一元運箅符與其他的一元運算符有同樣的優先級和從右到左的結合性。
指針變量的定義
如果在程序中聲明一個變量並使用地址作為該變量的值,那么這個變量就是指針變量。定義指針變量的一般形式為:
類型名 *指針變量名;
類型名指定指針變量所指向變量的類型,必須是有效的數據類型,如int,float,char等。指針變量名是指針變量的名稱,必須是一個合法的標識符。定義指針變量要使用指針聲明符"*"。
- 指針聲明符在定義指針變量時被使用,說明被定義的那個變量是指針。
指針變量本身就是變量,和一般變量不同的是它存放的是地址。指針變量用於存放變量的地址,由於不同類型的變瓧在內存中占用不同大小的存儲單元,所以只知道內存地址,還不能確定該地址上的對象。因此在定義指針變量時,除了指針變量名,還需要說明該指針變量所指向的內存空間上所存放數據的類型。
- 定義多個指針變量時,每一個指針變量前面都必須加上*。
注意,指針變量的類型不是指指針變量本身的類型,而是指它所指向的變量的數據類型。指針變量自身所占的內存空間大小和它所指向的變量數據類型無關,不同類型指針變量所占的內存空間大小都是相同的。指針變量被定義后,指針變量也要先賦值再使用,當然指針變量被賦的值應該是地址。
在定義指針變量時,要注意以下幾點:
- 指針變量名是一個標識符,要按照C語言標識符的命名規則對指針變量進行命名。
- 指針變量的數據類型是它所指向的變量的類型,一般情況下一旦指針變量的類型被確定后,它只能指向同種類型的變量。
- 在定義指針變量時需要使用指針聲明符“*”,但指針聲明符並不是指針的組成部分。
- 建議用其類型名的首字母作為指針名的首字符,用ptr作為名字,以使程序具有較好的可讀性。
指針的基本運算
如果指針的值是某個變量的地址,通過指針就能間接訪問那個變量,這些操作由取地址運算符"&"和間接訪問運算符“*”完成。此外,相同類型的指針還能進行賦值、比較和算術運算。
取地址運算和間接訪問運算
單目運算符&用於給出變量的地址。例如:
int p,a = 3;
p = &a;
這兩行代碼將整型變量的地址賦給整型指針p,使指針p指向變量a。在程序中,“*”除了被用於定義指針變量外,還被用於訪問指針所指向的變量,它也稱為間接訪問運算符。
- 我們在使用指針編程的時候,要正確理解指針操作的意義,帶有間接地址訪問符*的變量在不同情況下會有完全不同的意義。
賦值運算
一旦指針被定義並賦值后就可以如同其他類型的變量一樣進行賦值運算。例如
int a = 3;
int *p1,*p2;
p1 = &a;
p2 = p1;
將變量a的地址賦給指針pl,再將p1的值賦給指針p2,因此指針pl和p2都指向a變量,兩個指針訪問的是同一個存儲單元。、
指針之間的相互賦值只能在相同類型的指針之間進行,可以在定義時對指針進行賦值,也可以在程序運行過程中根據需要對指針重新賦值。
初始化
C語言中的變量在引用前必須先定義並賦值.指針變量在定義后也要先賦值再引用。在定義指針變量時,可以同時對它賦初值。例如:
int a;
int p1 = &a;
int p2 = p1;
以上對指針pl和p2的賦值都是在定義時進行的,使得指針pl和p2都指向變量a。
在進行指針初始化的時候需要注意以下幾點:
- 在指針變量定義或者初始化時變量名前面的“*”,只表示該變量是個指針變量,它既不是乘法運算符也不是間接訪問符。
- 把一個變量的地址作為初始化值賦給指針變量時,該變量必須在此之前已經定義。
- 可以用初始化了的指針變量給另一個指針變量作初始化值。
- 不能用數值作為指針變量的初值,但可以將一個指針變量初始化為空指針,例如
int *p = NULL;
5.指針變量定義時的數據類型和它所指向的目標變量的數據類型必須一致。
野指針
指針如果沒有被賦值,它的值是不確定的,即它指向一個不確定的單元,使用這樣的指針,可能會出現難以預料的結果,甚至導致系統錯誤。
算術運算
C 指針的算術運算只限於兩種形式:
指針 +/- 整數 :
假定有一個指針變量p,我們可以對指針變量p進行自增、自減、加上一個整數等操作,所得結果也是一個指針,只是指針所指向的內存地址相比於p所指的內存地址前進或者后退了i個操作數。
在上圖中,p 是一個 int 類型的指針,指向內存地址 0x10000008 處。則 p++ 將指向與 p 相鄰的下一個內存地址,由於 int 型數據占 4 個字節,因此 p++ 所指的內存地址為 1000000b。其余類推。這種運算並不會改變指針變量 p 自身的地址,只是改變了它所指向的地址。
指針 - 指針
只有當兩個指針都指向同一個數組中的元素時,才允許從一個指針減去另一個指針。兩個指針相減的結果表示它們之間相隔的數組元素數目。減法運算的值是兩個指針在內存中的距離(以數組元素的長度為單位,而不是以字節為單位),因為減法運算的結果將除以數組元素類型的長度。
要特別注意的是,在C語言中,其他的操作如指針相加、相乘和相除,或指針加上和減去一個浮點數都是非法的。
指針與函數
指針作為函數的參數
函數參數也可以是指針類型,如果將某個變量的地址作為函數的實參,相應的形參就是指針。
在C語言中實參和形參之間的數據傳遞是單向的“值傳遞”方式,調用函數不能改變實參變量的值。如果傳遞的值是指針變量,調用函數時可以改變實參指針變量所指向的變量的值。此時需要在函數定義時將指針作為函數的形參,在函數調用時把變量的地址作為實參。
要通過函數調用來改變主調函數中某個變量的值,可以把指針作為函數的參數。在之前的學習中,我們知道了函數只能通過return語句返回一個值。如果希望函數調用能將多個計算結果帶回主調函數,用return語句是無法實現的,而將指針作為函數的參數就能使函數返回多個值。
數組名作為函數的參數
數組的形參實際上是一個指針。當進行參數傳遞時,主函數傳遞數組的基地址,數組元素本身不被復制,編譯器允許在作為參數聲明的指針中使用數組方括號。例如:
int sum(int a[],int n);
當字符數組名、字符串常量或字符指針作為函數參數時,相應的形參都是字符指針,它也可以寫成數組的形式。
指針作為函數返回值
在C語言中,函數返回值也可以是指針類型,不過,我們要注意,不能在實現函數時返回在函數內部定義的局部數據對象的地址,這是因為所有的局部數據對象在函數返回時就會消亡,其值不再有效。因此,返回指針的函數一般都返回全局數據對象或主凋函數中教據對象的地址,不能返回在函數內部定義的局部數據對象的地址。
指針、數組和地址間的關系
在定義數組時,編譯器必須分配基地址和足夠的存儲空間,以存儲數組的所有元素。數組的基地址是在內存中存儲數組的起始位置,它是數組中第一個元素的地址,因此數組名本身是一個地址即指針值。在訪問內存方面,指針和數組幾乎是相同的,當然也有區別,這些區別是微妙且重要的:指針是以地址作為值的變量,而函數名的值是一個特殊的固定地址,可以把它看作是指針常量。
數組名可以使用指針形式,而指針變量也可以轉換為數組形式。
字符串和字符指針
字符串常量是用一對雙引號括起來的字符序列,字符串常量在內存中的存放位置由系統自動安排。由於字符串常量是一串字符,通常被看作一個特殊的一維字符數組,字符串常量中的所有字符在內存中連續存放。所以,系統在存儲一個字符串常量時,先給定一個起始地址,從該地址指定的存儲單元開始,連續存放該字符串中的字符。字符串常量實質上是一個指向該字符串首字符的指針常量。例如,字符串"hello"的值是一個地址,從它指定的存儲單元開始連續存放該字符串的6個字符。如果定義一個字符指針接收字符串常雖的值,該指針就指向字符串的首字符。
字符數組和字符指針都可以用來處理字符串,但是二者有重要區別,例如:
char a[] = "ThiS iS a string";
char *p = "ThiS iS a string";
字符數組a在內存中占用了一塊連續的單元,有確定的地址,每個數組元素放字符串的一個字符。字符指針p只占用一個可以存放地址的內存單元,存儲字符串首字符的地址。如果要改變數組a所代表的字符串,只能改變數組元素的內容。如果要改變指針p所代表的字符串,通常直接改變指針的值,讓它指向新的字符串。因為p是指針變量,它的值可以改變,轉而指向其他單元。例如:
strcpy(a,"hello");
p = "hello";
分別改變了sa和sp所表示的字符串。而因為數組名是常量,不能對它賦值。
- 為了盡量避免引用未賦值的指針所造成的危害,在定義指針時,可先將它的初值置為空,如
char *s = NULL;
動態內存分配
程序中需要使用各種變量來保存被處理的數據和各種狀態信息,變量在使用前必須被定義且安排好存儲空間包括內存起始地址和存儲單元大小。C語言的全局變量、靜態局部變量的存儲是在編譯時確定的,其存儲空間的實際分配在程序開始執行 前完成。對於局部自動變量,在執行進人變量定義所在的復合語句時為它們分配存儲單元,這種變量的大小也是靜態確定的。以靜態方式安排存儲的做法也有限制,例如當數據量很大時,我們要先定義一個很大的數組,以保證輸入的項數不要超過數組能容納的范圍。
一般情況下運行中的很多存儲要求在寫程序時無法確定,因此需要一種機制可以根據運行時的實際存儲需求分配適當的存儲區,用於存放那些在運行中才能確定數量的數據。在C語言中主要用兩種方法使用內存:一種是由編譯系統分配的內存區;另一種是用內存動態分配方式,留給程序動態分配的存儲區。
動態分配的存儲區在用戶的程序之外,不是由編譯系統分配的,而是由用戶在程序中通過動態分配獲取的。使用動態內存分配能有效地使用內存,同一段內存區域可以被多次使用,使用時申請,用完就釋放。
動態內存分配函數
頭文件:#include <stdlib.h>
malloc()函數
該函數用於申請一塊連續的指定大小的內存塊區域以void*類型返回分配的內存區域地址,在內存的動態存儲區中分配一個長度為size的連續空間。此函數的返回值是分配區域的起始地址,或者說,此函數是一個指針型函數,返回的指針指向該分配域的開頭位置。
函數原型
void *malloc(unsigned int size);
返回值:如果分配成功則返回指向被分配內存的指針(此存儲區中的初始值不確定),否則返回空指針NULL。
當內存不再使用時,應使用free()函數將內存塊釋放。
calloc()函數
該函數功能為在內存的動態存儲區中分配num個長度為size的連續空間。如果要求的空間無效,那么此函數返回指針。在分配了內存之后,calloc()函數會通過將所有位設置為0的方式進行初始化。比如,調用calloc()函數為n個整數的數組分配存儲空間,且保證所有整數初始化為0。
函數原型:
void* calloc(unsigned int num,unsigned int size);
calloc與malloc的區別在於,在動態分配完內存后,自動初始化該內存空間為零,而malloc不做初始化,分配到的空間中的數據是隨機數據。
realloc()函數
該函數將先判斷當前的指針是否有足夠的連續空間,如果有,擴大mem_address指向的地址,並且將mem_address返回,如果空間不夠,先按照newsize指定的大小分配空間,將原有數據從頭到尾拷貝到新分配的內存區域,而后釋放原來mem_address所指內存區域(注意:原來指針是自動釋放,不需要使用free),同時返回新分配的內存區域的首地址。
函數原型:
void *realloc(void *mem_address, unsigned int newsize);
返回值:如果重新分配成功則返回指向被分配內存的指針,否則返回空指針NULL。
free()函數
釋放之前調用 calloc、malloc 或 realloc 所分配的內存空間。如果傳遞的參數是一個空指針,則不會執行任何動作。該函數不返回任何值。
函數原型:
void free(void *ptr)
指針數組
C語言中的數組可以是任何類型,如果數組的各個元素都是指針類型,用於存放內存地址,那么這個數組就是指針數組。一維指針數組定義的一般格式為:
類型名 *數組名[數組長度];
類型名指定數組元素所指向的變量的類型。對於我們來說,關鍵是要掌握指針數組中,每個數組元素中存放的內容都是地址,通過數組元素可以訪問它所措向的單元。指針數組是由指針變量構成的數組,在操作時,既可以直接對數組元素進行賦值和引用,也可以間接訪問數組元素所指的單元內容,改變或引用該單元的內容。
指針數組操作多個字符串
指針數組與二維數組
代碼實現:
定義二維字符數組時必須指定列長度,該長度要大於最長的字符串的有效長度。由於各個字符串的長度一般並不相同,會造成內存單元的浪費。而指針數組並不存放 字符串,僅僅用數組元素指向各個字符串,就沒有類似的問題。
用指針數組操作多個字符串
偽代碼:
函數定義void sort(char *a[], int n)
定義變量i, j用於進行排序操作;
定義指針變量*temp作為交換數據的中間變量;
for i = 0; i < n; i++ do //對指針數組冒泡排序
for j = 0; j < n - i - 1; j++ do
if (strcmp(a[j + 1], a[j]) > 0) //字符串之間作比較需要用字符串比較函數strcmp
temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
end if
end for
end for
代碼實現:
動態輸入多個字符串
前面的舉例在用指針數組操作多個字符串時,都是通過初始化的方式對指針數組賦值,使指針數組的元素指向字符串。如果需要輸入多個字符串,我們可以利用動態內存分配來配合輸入。
閱讀裁判代碼:
在裁判的代碼中,根據輸入的字符串的長短,通過函數malloc動態分配相應大小的內存單元,並將此單元的首地址保存在指針數組的相應元素中,即數組的元素指向這些動態分配的內存單元。采用動態分配內存的方法處理多個字符串的輸入的優點在於, 數據的多少來申請和分配內存空間,從而提高了內存的使用率。
二級指針
定義:
類型名 **變量名;
我們很明白,任何變量都有地址,一級指針的值雖然是地址,但這個地址做為一個值亦需要空間來存放,二級指針就是為了獲取這個地址。一級指針所關聯的是地址里的數據,這個數據可以是任意類型並做任意用途,但二級指針所關聯的數據只有一個類型一個用途,就是為了提供對於內存地址的讀取或改寫。
如果存在A指向B的指向關系,則A是B的地址,“A”表示通過這個指向關系間接訪問B.如果B的值也是一個指針,它指向C,則B是C的地址,“B”表示間接訪問C,如果C是整型、實型或者結構體等類型的變量或者是存放這些類型的數據的數組元素,則B(即C的地址)是普通的指針,稱為一級指針,用於存放一級指針的變量稱為一級指針變量,指向一級指針變量的"A"則是“二級指針”。
- 二維數組名也是一個二級指針,指針數組名也是二級指針,因此用數組下標能完成的操作也能用指針完成。
例題講解
合並兩個有序數組
偽代碼
函數定義
定義變量idxa,idxb,控制指針a,b指向單元的下標變量;
定義指針*num指向一個還未開辟的數組;
定義變量idx,控制指針num指向單元的下標變量;
動態內存分配給指針num,大小為(m + n) * sizeof(int);
while idxa + idxb < m + n do //按照數字大小順序將兩個數組存放進新數組
if idxb >= n //當數組b數據存儲完畢時,只需存儲數組a
num[idx++] = a[idxa++];
else if idxa >= m //當數組a數據存儲完畢時,只需存儲數組b
num[idx++] = b[idxb++];
else if a[idxa] < b[idxb] //比較數組a,b下一個單元的大小,將較小的數據寫入新數組
num[idx++] = a[idxa++];
else
num[idx++] = b[idxb++];
end if
end if
end if
end while
for idx = 0; idx < m + n; idx++
a[idx] = num[idx];
end for
代碼實現
本題知識點
1.動態內存分配開辟新數組
num = (int*)malloc((m + n) * sizeof(int));
詳細知識點見上文
2.構造新數組:利用循環結構和判斷結構,將兩個數組的數據按順序寫入新數組
3.將數組數據拷貝到另一個數組
for idx = 0; idx < m + n; idx++ do
a[idx] = num[idx];
說反話-加強版
偽代碼
主函數
定義數組str[500001]存放輸入的句子;
輸入數組str;
調用函數SetContrary,傳入參數數組str的地址,數組str的長度;
void SetContrary(char* str, int bit);
定義變量i,j控制循環並作為下標移動指針;
定義變量Word記錄單個單詞長度;
定義變量flag判斷是否需要輸出空格;
for i = bit - 1; i >= 0; i-- do //從后往前遍歷字符串
if *(str + i) == ' ' && word != 0 //如果遇到空格且記錄的單詞長度不為零
if flag != 0 //如果不是第一個單詞,輸出空格
輸出空格;
end if
for j = i + 1; j <= i + word; j++ do //輸出單詞
輸出字符*(str + j);
end if
Word = 0; //清零變量,記錄下一個單詞長度
flag++; //記錄單詞數量
else if *(str + i) != ' ' //如果不是空格,單詞長度加一
Word++;
end if
end for
if word > 0 //單獨輸出最后一個單詞
if flag != 0
輸出空格
end if
for j = 0; j <= word - 1; j++ do
輸出字符*(str + j);
end for
end if
代碼實現
本題知識點
1.函數傳遞數組
SetContrary(str, strlen(str));
傳入數組的首地址給函數。
2.輸出單個字符串中的部分字符
利用一個變量存儲需要輸出的字符數量,用判斷結構找到需要輸出的節點,利用循環結構輸出;
for (i = bit - 1; i >= 0; i--)
{
if (*(str + i) == ' ' && word != 0)
{
for (j = i + 1; j <= i + word; j++)
{
printf("%c", *(str + j));
}
word = 0;
flag++;
}
else if (*(str + i) != ' ')
{
word++;
}
}
3.利用變量移動指針指向的單元
printf("%c", *(str + j));
刪除字符串中的子串
偽代碼
定義指針*ptr用於記錄子串查找結果;
定義字符數組mom[81],son[81]分別輸入母串和子串;
輸入母串;
輸入子串;
while (ptr = strstr(mom, son)) != NULL do //查找母串是否含有子串,若strstr函數返回值不為NULL,證明有查找到子串
*ptr = '\0'; //標記查找到子串的位置為結束符‘\0’
strcat(mom, ptr + strlen(son)); //將字符串的后半部分復制到結束符之前的部分
end while
puts(mom);
代碼實現
知識點
1.字符串函數的應用
字符串查找函數strstr()
包含文件:string.h
函數名: strstr
函數原碼:
char *strstr(const char *s1,const char *s2)
{
int len2;
if(!(len2=strlen(s2)))//此種情況下s2不能指向空,否則strlen無法測出長度,這條語句錯誤
return(char*)s1;
for(;*s1;++s1)
{
if(*s1==*s2 && strncmp(s1,s2,len2)==0)
return(char*)s1;
}
return NULL;
}
語法:
strstr(str1,str2)
str1: 被查找目標
str2: 要查找對象
返回值:若str2是str1的子串,則返回str2在str1的首次出現的地址;如果str2不是str1的子串,則返回NULL。
2.字符串長度函數strlen()
頭文件:string.h
格式:strlen
功能:計算給定字符串的長度,不包括'\0'在內,返回s的長度,不包括結束符NULL。
3.字符串連接函數
原型:
extern char *strcat(char *dest, const char *src);
頭文件:string.h
功能:把src所指向的字符串(包括“\0”)復制到dest所指向的字符串后面(刪除dest原來末尾的“\0”)。要保證dest足夠長,以容納被復制進來的src。src中原有的字符不變。返回指向dest的指針。src和dest所指內存區域不可以重疊且dest必須有足夠的空間來容納src的字符串。
2.活用字符串結束符'\0'
編譯器判斷字符串時,是通過讀取字符數組直到結束符,則判斷為一個字符串。本題代碼中利用結束符把一個字符串分割成兩個字符串(原字符串末尾還有一個結束符),然后操作指針,利用字符串拼接函數實現集成移動字符串。
延伸閱讀
zoj 1418 Lazy Math Instructor
題意概括
先輸入一個數字,接着輸入n組字符串,輸入的字符串都是一個算式,程序需要判斷一組的算式是否等價,等價就輸出"YES",不等價就輸出"NO"。
代碼分析:
- 這代碼我看不懂,但是這道題涉及有考慮優先級的算式運算,所以懷疑使用了數據結構中的棧結構,經過咨詢了學長、學姐之后確認了這段代碼使用了棧結構。雖然是還沒有學的知識,但是我還是強行去讀了代碼。棧的思想是“先進后出,后進先出”,先進入的數據被壓入棧底,最后的數據在棧頂,需要讀數據的時候從棧頂開始。用數組來類比,相當於我操作一個只能從尾部操作的數組,從數組后面加入新元素,並使得數組有限長度加1,從數組尾部刪除一個元素,則使數組長度減1。用在一個死胡同停車為例,如圖所示:
- 這段代碼我最大的疑惑是全局變量中的那個二維數組,它起到了判斷符號優先級的作用,用於確認數據是進棧、出棧還是運算;
- 解題的目的是去判斷等式是否等價,但是判斷等式是否等價不是要去整理這兩個算式,而是去計算這兩個算式,因為字母雖然在算式中表示一個變量,但是字母是可以通過一定的處理進行代數運算的,因此這道題就變成了計算兩個等式的結果並比較是否相等的問題了;
- 了解了什么是棧之后,就能大概知道這道題時怎么實現判斷算式等價了,首先先把一個等式搞成后綴表達式,然后利用棧來進行計算,遇到數字就把數據放到棧里面,然后遇見符號就出棧進行計算,再把結果入棧,這樣就可以有效去處理涉及考慮優先級的算式運算了,因此如何去處理這個表達式就顯得尤其重要;
- 這段代碼用了7個函數,其中一個函數是起到計算的作用,其他函數都是在起着數據處理的作用,我在跑代碼調試的時候我是看不懂代碼在干嘛的,只能看到不斷在變化的4個數組和n個變量的值,但是這也說明了這段代碼的函數接口做得很好,數據的輸入和返回都能夠准確無誤地完成,變量的設計也很合理,都能夠起到一定的作用;
- 運用了“#include <ctype.h>”頭文件下的字符分類函數,isdigit()函數主要用於檢查其參數是否為十進制數字字符,isalpha()函數用於判斷字符ch是否為英文字母。
參考資料
《C語言程序設計(第三版)》——何欽銘、顏輝
指針
動態內存分配函數
一級指針與二級指針
數組指針和指針數組的區別