一、什麼是指針
1. 地址與指針
在程序中定義了一個變量,編譯時系統會給這個變量分配存儲單元,同時根據變量的數據類型,分配一定長度的空間。內存區的每一個字節都有一個編號,這就是“地址”。由於通過地址就可以找到所需的變量單元,可以説,地址指向該變量單元。由此,將地址形象地稱為指針。
C 語言對不同的數據類型分配不同大小的存儲單元,且不同數據類型的存儲方式是不一樣的。因此,即使給了一個地址,也無法保證能正確存取所需要的信息。為了能正確存取一個數據,出路需要位置信息,還需要該數據的類型信息。C 語言的地址包括位置信息(地址編號,或稱純地址)和它所指向的數據的類型信息,或者説它是“帶類型的地址”。
2. 直接訪問與間接訪問
在程序中,一般是通過變量名來引用變量的值的,如:
scanf("%d", &n);
printf("%d\n", n);
這種直接按變量名進行的訪問稱為直接訪問。
還可以採用另一種稱為間接訪問的方式,即將一個變量 n 的地址存放在另一個變量 n_pointer 中,然後通過變量 n_pointer 來找到變量 n 的地址,從而訪問該變量。
如果我們要對變量 n 賦值,現在就有了兩種方法:
- 直接訪問:根據變量名直接向變量
n賦值,由於變量名與變量地址有一一對應關係,因此就按照此地址直接對變量n的存儲單元進行訪問。 - 間接訪問:先找到存放變量
n地址的變量n_pointer,從中得到變量n的地址,從而找到變量n的存儲單元,對它進行訪問。
如果一個變量專門用來存放另一變量的地址,則稱為指針變量。上述的 n_pointer 就是一個指針變量。指針變量就是地址變量,用來存放地址。
二、指針變量
存放地址的變量就是指針變量,它用來指向另一個對象(如變量、數組、函數等)。
1. 定義指針變量
定義指針變量的一般形式為:
類型名* 指針變量名;
定義指針是必須指定其基本類型,該類型表示此指針可以指向的變量的類型。因此,指針類型是基本數據類型派生出來的類型,不能離開基本類型而獨立存在。
定義指針的同時也可以對其進行初始化,如:
int* a;
char* b = &c;
定義指針時要注意:
- 指針前面的
*表示該變量為指針型變量,指針變量名為a而不是*a。 - 一個變量的指針的含義包括兩方面:存儲單元編號表示的地址和它指向的存儲單元的數據類型。
- 指針變量只能存放地址,不能將一個整數賦值給指針變量。
2. 引用指針變量
引用指針變量有 3 種情況:
-
給指針變量賦值:
p = &a; -
引用指針變量指向的變量:
*p = 1; printf("%d", *p); -
引用指針變量的值:
printf("%o", p); // 以八進制輸出指針變量的值(即指向的變量的地址)
要熟練掌握兩個有關運算符:
&:取地址運算符。*:指針運算符(或稱為“間接訪問運算符”)。
3. 指針變量作為函數參數
將一個變量的地址傳到一個函數中可以使用指針變量作為函數參數。
#include <stdio.h>
int main() {
void swap(int* x, int* y);
int a, b, *p1, *p2;
scanf("%d %d", &a, &b);
p1 = &a;
p2 = &b;
if (a < b)
swap(p1, p2);
printf("max = %d, min = %d\n", a, b);
return 0;
}
void swap(int* x, int* y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
}
上面的程序通過指針實現了對輸入的兩個整數按大小輸出。可以發現,在調用函數 swap() 之後,變量 a 與 b 會發生交換。通常情況下,由於“單向傳遞“的值傳遞方式,形參值的改變不能使實參的值隨之改變。為了使函數中改變了的變量的值能夠被主函數 main() 所用,應該使用指針變量作為函數參數。函數執行過程中使指針變量所指向的變量值發生變化,函數調用結束後,這些變化依然保留了下來。
如果想通過函數調用得到 n 個要改變的值,可以這樣做:
- 在主調函數中設置 n 個變量,用 n 個指針指向它們。
- 設計一個有 n 個指針形參的函數,在函數中改變轉這 n 個形參的值。
- 在主調函數中調用這個函數,將這 n 個指針變量作為實參傳遞給該函數。
- 執行函數時,通過形參指針變量改變它們所指向的 n 個變量的值。
- 主調函數中就可以使用這些改變了值的變量。
需要注意,不能企圖通過改變指針形參的值來使指針實參的值改變,因為實參變量與形參變量之間值的傳遞時單向的,不可能通過執行調用函數來改變實參指針變量的值,但是可以改變實參指針變量所指變量的值。例如,將上面例子中的 swap() 函數換為下面的函數是達不到要求的:
void swap(int* x, int* y) {
int* temp;
temp = x;
x = y;
y = temp;
}
注意:函數的調用可以且最多可以得到一個返回值,而使用指針變量做參數,可以得到多個變化了的值。要善於利用指針法。
三、通過指針引用數組
1. 數組元素的指針
一個數組包含若干元素,每個元素在內存中都有一個相應的地址,指針變量可以指向變量,也可以指向數組中的元素。數組元素的指針就是數組元素的地址。
int a[5] = {1, 2, 3, 4, 5};
int* p;
p = &a[0];
引用數組元素可以使用下標法,也可以使用指針法。指針法佔用的內存更少,運行速度快,能提高目標程序質量。
C 語言中數組名(不包括形參數組名)代表數組中首元素的地址,因此下面兩個語句等效:
p = &a[0];
p = a;
2. 引用數組元素時指針的運算
指針即地址,對指針進行賦值運算是沒問題的,但對指針進行乘和除的運算是沒有意義的。在一定條件下,允許對指針進行加和減的運算。
在指針已指向一個數組元素時,可以對指針進行以下運算:
- 加一個整數:
p + 1。 - 減一個整數:
p - 1。 - 自加運算:
p++,++p。 - 自減運算:
p--,--p。 - 兩個指針相減:
p1 - p2。
分別説明如下:
- 如果指針變量
p已經指向數組中的一個元素,則p + 1指向同一數組的下一個元素,p - 1指向同一數組的上一個元素。注意:p + 1不是簡單地將p的值加 1,而是在p的值(地址)上加上一個數組元素所佔用的字節數,從而使p指向下一個元素。 - 如果
p的初值為&a[0],則p + i和a + i都指向數組a序號為i的元素。 *(p + i)、*(a + i)、a[i]三者等價。*(p++)是先取*p的值,然後使p加 1.*(++p)是先使p加 1,再取*p的值。- 如果指針變量
p1和p2都指向同一數組中的元素,則p1 - p2的結果是兩個地址之差除以數組元素的長度(佔用的字節數)。即利用p1 - p2就可以hi到它們所指元素的相對距離。
注意:
- 兩個地址不能相加,
p1 + p2是無實際意義的。 []實際是變址運算符,a[i]表示按照a + i計算地址,然後找出此地址單元中的值。
3. 通過指針引用數組元素
綜上,引用一個數組元素可以有兩種方法:
- 下標法:
a[i]。 - 指針法:
*(a + i)或*(p + i)。
需要注意的是:
- 可以通過改變指針變量的值指向不同的元素,但要注意指針變量的當前值。
p++不能使用a++代替。因為數組名a代表數組首元素的地址,它是一個指針型常量,它的值在程序運行期間是固定不變的。- 指向數組元素的指針變量可以帶下標,如
p[i]。因為程序編譯時,對下標的處理方式是轉換為地址,對p[i]處理為*(p + i)。使用p[i]時必須先弄清楚p的當前值是什麼,否則容易和a[i]混淆。
4. 用數組名做函數參數
在07 - 函數中,介紹了使用數組名作為函數的參數。當用數組名做參數時,如果形參數組中各元素的值發生了變化,實參數組元素的值隨之變化。這是因為是實參數組名代表該數組首元素的地址,而形參是用來接收從實參傳過來的數組首元素地址的,因此形參應該是一個指針變量。實際上,C 編譯都是將形參數組名作為指針變量來處理的。
下面的兩種寫法時等效的:
fun(int arr[], int n)
fun(int* arr, int n)
注意:實參數組名代表一個固定的地址,或者説時指針常量,但形參數組名並不是固定的地址,而是按照指針常量處理。
5. 通過指針引用多維數組
指針變量也可以指向多維數組中的元素,但其概念和使用方法要比一維數組複雜。以下面的二維數組為例:
int a[2][3] = {{1, 3, 5}, {2, 4, 6}};
從二維數組的角度來看,a 代表二維數組首元素的地址,但現在的首元素是由 3 個整型元素所組成的一維數組,因此現在的 a 代表的是首行 a[0] 的起始地址,a + 1 代表下一行 a[1] 的起始地址。
a[0] 和 a[1] 都是一維數組名,數組名代表數組首元素的地址,因此 a[0] 代表一維數組 a[0] 中第 0 列元素的地址,即 &a[0][0]。同理,a[1] 的值是 a[1][0]。
由前可知,a[i] 和 *(a + i) 等價,因此a[i] + j 也和 *(a + i) + j 等價,兩者都是 &a[i][j]。
| 表示形式 | 含義 |
|---|---|
a |
二維數組名,指向一維數組 a[0] 的起始地址 |
a[i],*(a + i),*a |
i 行 0 列元素地址 |
a + 1,&a[i] |
i 行起始地址 |
a[i] + j,*(a + i) + j,&a[i][j] |
i 行 j 列元素 a[i][j] 的地址 |
*(a[i] + j),*(*(a + i) + j),a[i][j] |
i 行 j 列元素 a[i][j] 的值 |
如前所述,C 語言的地址信息中既包含位置信息,也包含它所指向的數據的類型信息。a 是二維數組名,是二維數組首行起始地址;a[0] 是一維數組名,是一維數組其實元素的地址。兩者的純地址相同,但基類型不同,前者是一維數組,後者是整型數據。
如果要用一個指針變量 p 來指向此一維數組,應該這樣定義:
int (*p)[3];
// 表示 pt 指向 4 個整型元素組成的一維數組
注意:要注意指針變量的類型,int (*p)[3] 中 p 的類型不是 int * 型,而是 int (*)[3] 型,p 被定義為指向一維數組的指針變量,一維數組有 3 個元素,因此 p 的基類型是一維數組。
四、通過指針引用字符串
1. 字符串的引用方式
C 程序中,字符串是存放在字符數組中的,要引用一個字符串,可以有以下兩種方法:
- 用字符數組存放一個字符串,可以通過數組名和下標引用字符串中的一個字符,也可以通過數組名和格式聲明
%s輸出該字符串。 - 用字符指針變量指向一個字符串常量,通過字符指針變量引用字符串常量。
#include<stdio.h>
int main() {
char* string = "Hello World!";
printf("%s\n", string);
return 0;
}
// Hello World!
上面的程序沒有定義字符數組,只定義了一個 char * 型的指針變量,並用一個字符串常量對其進行初始化。C 語言對字符串常量是按照字符數組來處理的,但這個字符數組沒有名字,因此不能使用數組名來引用,只能通過指針變量來引用。
對字符指針變量初始化,實際上是把字符串的第一個元素的地址(即存放字符串的字符數組的首元素地址)賦值給指針變量,使之指向字符串的第一個字符。
#include <stdio.h>
int main() {
char a[] = "Hello World!", b[20], *p1, *p2;
p1 = a;
p2 = b;
for (; *p1 != '\0'; p1++, p2++)
*p2 = *p1;
*p2 = '\0';
printf("String a is: %s\n", a);
printf("String b is: %s\n", b);
return 0;
}
// String a is: Hello World!
// String b is: Hello World!
2. 字符指針做函數參數
要把一個字符串從一個函數傳遞到另一個函數,可以用地址傳遞的方法,即用字符數組名做參數,也可以用字符指針變量做參數。在被調用的函數中可以改變字符串的內容,在主調函數中可以引用改變後的字符串。
#include <stdio.h>
int main() {
void copy_string(char from[], char to[]);
char a[] = "Hello";
char b[] = "World";
printf("a: %s\tb: %s\n", a, b);
copy_string(a, b);
printf("a: %s\tb: %s\n", a, b);
return 0;
}
void copy_string(char from[], char to[]) {
int i = 0;
while (from[i] != '\0') {
to[i] = from[i];
i++;
}
to[i] = '\0';
}
// a: Hello b: World
// a: Hello b: Hello
上面的程序使用字符數組名作為函數實參,若要用字符指針變量做實參,可以將第 8 行代碼改為下面兩行:
char *from = a, *to = b;
copy_string(from, to);
3. 字符指針變量和字符數組的比較
用字符數組和字符指針變量都能實現字符串的儲存和運算,但二者之間是有區別的:
- 字符數組由若干個元素組成,每個元素中放一個字符,而字符指針變量中存放的是地址(字符串第 1 個字符的地址),絕不是將字符串放到字符指針變量中。
- 可以對字符指針變量賦值,但不能對數組名賦值。
- 編譯時為字符數組分配若干存儲單元,以存放各元素的值,而對字符指針變量,只分配一個存儲單元。
- 指針變量的值是可以改變的,而字符數組名代表一個固定的值(數組首元素的地址),不能改變。
- 字符數組中各元素的值是可以改變的(可以對它們再賦值),但字符指針變量指向的字符串常量中的內容是不可以被取代的(不能對它們再賦值)。
- 用指針變量指向]一個格式字符串,可以用它代替
printf函數中的格式字符串。因此只要改變指針變量所指向的字符串,就可以改變輸入輸出發格式。這種printf函數被稱為可變格式輸出函數。
char *format;
format = "a=%d, b=%f\n";
printf(format, a, b);
// 相當於:
printf("a=%d, b=%f\n", a, b);
五、指向函數的指針
1. 函數的指針
如果在程序中定義了一個函數,在編譯時會把函數的源代碼轉換為可執行代碼並分配段存儲空間。這段內存空間有一個起始地址,也稱為函數的入口地址。每次調用函數時都從該地址入口開始執行此段函數代碼。函數名就是函數的指針,它代表函數的起始地址。調用函數時,從函數名得到函數的起始地址,並執行函數代碼。
可以定義一個指向函數的指針變量,用來存放某一函數的起始地址,這就意味着此指針變量指向該函數。例如:
int (*p)(int, int);
定義 p 是一個指向函數的指針變量,它可以指向函數類型為整型且有兩個整型參數的函數。此時,指針變量 p 的類型用 int(*)(int, int) 表示。
2. 函數指針變量的定義與使用
調用一個函數,除了使用函數名調用外,還可以通過指向函數的指針變量調用。定義指向函數的指針變量的一般形式為:
類型名 (*指針變量名)(函數參數表列);
注意:
- 定義指針變量時指定的類型名和函數參數表列必須和指向的函數一致。
- 對指向函數的指針變量進行算術運算是無意義的。
將函數賦值給函數指針變量時,只需給出函數名而不必給出參數。用函數指針變量調用函數,只需將 (*指針變量名) 代替函數名即可。
用函數名調用函數,只能調用所指定的一個函數,而使用指針變量調用函數比較靈活,可以根據不同情況先後調用不同函數。
#include <stdio.h>
int main() {
int max(int x, int y);
int (*p)(int, int);
p = max;
int a, b, c;
scanf("%d %d", &a, &b);
c = (*p)(a, b); // 等價於:c = max(a, b)
printf("max = %d\n", c);
return 0;
}
int max(int x, int y) {
if (x > y)
return x;
else
return y;
}
3. 用指向函數的指針做函數參數
指向函數的指針變量的一個重要用途就是把函數的入口地址作為參數傳遞給其他函數,這樣就能在被調用的函數中使用實參函數。
使用指向函數的指針變量做形參可以實現多個函數在不同情況下被主調函數使用,而主調函數無需做任何修改。這種方法符合結構化程序設計方法原則。
void fun(int (*x)(int), int (*y)(int, int)) {
int a, b, i=3, j=5;
a = (*x)(i);
b = (*y)(i, j);
}
六、返回指針值的函數
一個函數可以返回一個整型值、字符值等,也可以返回指針型的數據,即地址。定義返回指針值的函數的一般形式為:
類型名* 函數名(參數表列);
如定義 int *a(int x, int y);,調用後可以得到一個 int * 型指針,即整型數據的地址。
七、指針數組和多重指針
1. 指針數組
元素均為指針類型數據的數組稱為指針數組。其定義方法為:
類型名 *(數組名)[數組長度];
指針數組比較和是用來指向若干個字符串,時字符串的處理更加方便靈活。下面的程序實現了若干個字符串按字母順序由小到大輸出:
#include <stdio.h>
#include <string.h>
int main() {
void sort(char* name[], int n);
void print(char* name[], int n);
char* name[] = {"Follew me", "C-language", "408"};
int n = 3;
sort(name, n);
print(name, n);
return 0;
}
void sort(char* name[], int n) {
char* temp;
int i, j, k;
for (i = 0; i < n - 1; i++) {
k = i;
for (j = i + 1; j < n; j++) {
if (strcmp(name[k], name[j]) > 0)
k = j;
}
if (k != i) {
temp = name[i];
name[i] = name[k];
name[k] = temp;
}
}
}
void print(char* name[], int n) {
int i;
for (i = 0; i < n; i++)
printf("%s\n", name[i]);
}
// 408
// C-language
// Follew me
2. 指向指針數據的指針變量
指向指針數據的指針變量簡稱為指向指針的指針。其定義方法為:
類型名** 變量名;
* 運算符的結合順序時從右到左,因此 **p 相當於 *(*p)。
#include <stdio.h>
int main() {
char* name[] = {"Follew me", "C-language", "408"};
char** p;
int i;
for (i = 0; i < 3; i++) {
p = name + i;
printf("%s\n", *p);
}
return 0;
}
// Follew me
// C-language
// 408
3. 指針數組做 main 函數的形參
指針數組的一個重要應用是作為 main 函數的形參。以往的程序中,main 函數的第一行一般寫為 int main() 或 int main(void),表示 main 函數沒有參數。實際上,某些情況下,main 函數可以有參數,即:
int main(int argc, char* argv[])
其中,agrc 和 argv 是 main 函數的形參,它們是程序的命令行參數。argc(argument count 縮寫)意為參數個數,argv(argument vector 縮寫)意為參數向量,是一個 * char 指針數組,數組中每一個元素指向命令行中的一個字符串的首字母。
注意:如果用帶參數的 main 函數,第一個形參必須是 int 型,用來接收形參個數,第二個形參必須是字符指針數組,用來接收操作系統命令行傳來的字符串中首字符的地址。
通常 main 函數和其他函數組成一個文件模塊,對該文件進行編譯和連接得到可執行文件 .exe,執行該文件操作系統就能調用 main 函數,然後由 main 函數調用其他函數,從而完成程序的功能。main 函數是由操作系統調用的,因此其實參也只能由操作系統給出。在操作命令狀態下,實參是和執行文件的命令一起給出的。例如,在 DOS、UNIX 或 Linux 等系統的操作命令狀態下,命令行中包括了命令名和需要傳給 main 函數的參數。命令行的一般形式為:
命令名 參數1 參數2···參數n
假設可執行文件為 file.exe,現在要將兩個字符串 abc,xyz 作為傳送給 main 函數的參數,命令行為:
file abc zyx
需要注意的是,文件名也作為一個參數,即上面的例子中 argc 的值為 3,argv[0] 指向字符串 file 的首字符。
八、動態內存分配與指向它的指針變量
1. 動態內存分配
在07 - 函數中介紹過全局變量和局部變量,全局變量分配在內存中的靜態存儲區,非靜態的局部變量(包括形參)分配在內存中的動態存儲區,這個存儲區是一個稱為棧(stack)的區域。
除此之外,C 語言還允許和建立內存動態分配區域,以存放一些臨時用的數據,這些數據不必再程序的聲明部分定義,也不必等到函數結束時才釋放,而是需要時開闢,不需要時隨時釋放。這些數據臨時存放在一個特殊的自由存儲區,稱為堆(heap)區。可以根據需要,像系統申請所需大小的空間。由於未在聲明部分定義它們為變量或數組,因此不能通過變量名或數組名去引用這些數據,只能通過指針來引用。
2. 建立內存的動態分配
對內存的動態分配通過系統提供的庫函數來實現,主要有 mallo,calloc,free,realloc 這 4 個函數。以上 4 個函數的聲明在 stdib.h 頭文件中,在用到這些函數時應當用 #include <stdlib.h> 指令把 stdlib.h 頭文件包含到程序文件中。
用 mallo 函數開闢動態存儲區
其函數原型為:
void* malloc(unsigned int size);
其作用是在內存的動態存儲區中分配一個長度為 size 的連續空間。形參 size 的類型定為無符號整型(不允許為負數)。此函數是一個指針型函數,返回的指針指向該分配域的第一個字節。如:
malloc(100); // 開闢 100 字節的臨時分配域,函數值為其第 1 個字節的地址
注意指針的基類型為 void,即不指向任何類型的數據,只提供一個純地址。如果此函數未能成功地執行(例如內存空間不足),則返回空指針 NULL。
用 calloc 函數開闢動態存儲區
其函數原型為:
void* calloc(unsigned n, unsigned size);
其作用是在內存的動態存儲區中分配 n 個長度為 size 的連續空間,這個空間一般比較大,足以保存一個數組。用 calloc 函數可以為一維數組開闢動態存儲空間,n 為數組元素個數,每個元素長度為 size。這就是動態數組。函數返回指向所分配域的第一個字節的指針;如果分配不成功,返回 NULL。如:
p = calloc(50,4); // 開闢 50×4 個字節的臨時分配域,把首地址賦給指針變量 p
用 realloc 函數重新分配動態存儲區
其函數原型為:
void* realloc(void* p, unsigned int size);
如果已經通過 malloc 函數或 calloc 函數獲得了動態空間,想改變其大小,可以用 recalloc 函數重新分配。用 realloc 函數將 p 所指向的動態空間的大小改變為 size。p 的值不變。如果重分配不成功,返回 NULL。如:
realloc(p,50); // 將 p 所指向的已分配的動態空間改為 50 字節
用 free 函數釋放動態存儲區
其函數原型為:
void free(void* p);
其作用是釋放指針變量 p 所指向的動態空間,使這部分空間能重新被其他變量使用。p 應是最近一次調用 calloc 或 malloc 函數時得到的函數返回值。如:
free(p); // 釋放指針變量 p 所指向的已分配的動態空間
free 函數無返回值。
3. void 指針類型
C 99 允許使用基類型為 void 的指針類型,即 void* 型變量,它不指向任何類型的數據。在將它的值賦給另一指針變量時由系統對它進行類型轉換,使之適合於被賦值的變量的類型。例如:
注意:不要把“指向 void 類型”理解為能指向“任何的類型”的數據,而應理解為“指向空類型”或“不指向確定的類型”的數據。
由於地址必須包含基類型信息,否則無法實現對數據的存取,因此 void* 型指針所標誌的存儲單元中是不能儲存任何數據的,一般情況下只在調用動態儲存分配函數時會使用。
Reference:
譚浩強《C程序設計(第五版)》