C語言中文網 目錄
首頁 > C語言專題 > 指針 閱讀:2,247

指針,C語言指針完全攻略

與其他高級編程語言相比,C 語言可以更高效地對計算機硬件進行操作,而計算機硬件的操作指令,在很大程度上依賴于地址。

指針提供了對地址操作的一種方法,因此,使用指針可使得 C 語言能夠更高效地實現對計算機底層硬件的操作。另外,通過指針可以更便捷地操作數組。在一定意義上可以說,指針是 C 語言的精髓。

內存與地址

在計算機中,數據是存放在內存單元中的,一般把內存中的一個字節稱為一個內存單元。為了更方便地訪問這些內存單元,可預先給內存中的所有內存單元進行地址編號,根據地址編號,可準確找到其對應的內存單元。由于每一個地址編號均對應一個內存單元,因此可以形象地說一個地址編號就指向一個內存單元。C 語言中把地址形象地稱作指針。

C語言中的每個變量均對應內存中的一塊內存空間,而內存中每個內存單元均是有地址編號的。在 C 語言中,可以使用運算符 & 求某個變量的地址。

例如,在如下代碼中,定義了字符型變量 c 和整型變量 a,并分別賦初值 'A' 和 100。
#include <stdio.h>
int main(void)
{
    char c='A';
    int a=100;
    printf("a=%d\n",a);//輸出變量a的值
    printf("&a=%x\n",&a);//輸出變量a的地址
    printf("c=%c\n",c);
    printf("&c=%x\n",&c);
    return 0;
}
程序某次的運行結果為:
a=100
&a=12ff40
c=A
&c=12ff44

分析:
在 C 語言中,字符型變量占一個字節的內存空間,而整型變量所占字節數與系統有關。例如,在 32 位系統中,VC++6.0 開發環境中,int 型占 4 個字節。假設程序在某次運行時,變量 a 和 c 在內存中的分配情況如圖 1 所示:內存單元(每個字節)的地址編號分別為十六進制表示的 ...12ff40、12ff41、12ff42、12ff43、12ff44...,每個地址編號均為對應字節單元的起始地址。


圖 1 變量的值及內存地址

由圖 1 可知,變量 a 對應于從地址 12ff40 開始的 4 個字節(12ff40、12ff41、12ff42、12ff43)的內存空間,存儲的是整數 100 的 32 位二進制形式(為直觀表示,本例并沒有轉換成二進制形式)。字符型變量 c,對應地址為 12ff44,該地址內存儲的是字母對應 ASCII 值的 8 位二進制形式。

語句 printf("a=%d\n",a); 輸出:a=100。

語句 printf("&a=%x\n",&a); 是按十六進制形式輸出變量 a 的地址(a 在內存中的起始地址值)為 &a=12ff40。

在上例中,變量 a 和 c 的起始地址 12ff40 和 12ff44 均為指針,分別指向變量 a 和變量 c。

區分變量的地址值和變量的值。如上例中,變量 a 的地址值(指針值)為12ff40,而變量 a 的值為 100。

指針變量的定義

可以保存地址值(指針)的變量稱為指針變量,因為指針變量中保存的是地址值,故可以把指針變量形象地比喻成地址箱。

指針變量的定義形式如下。

類型 * 變量名;

例如:
int *pa;
定義了一個整型指針變量 pa,該指針變量只能指向基類型為 int 的整型變量,即只能保存整型變量的地址。

說明:
1) *號標識該變量為指針類型,當定義多個指針變量時,在每個指針變量名前面均需要加一個 *,不能省略,否則為非指針變量。例如:
int *pa,*pb;
表示定義了兩個指針變量 pa、pb。而:

int *pa,pb;

則僅有 pa 是指針變量,而 pb 是整型變量。
int *pi,a,b; //等價于inta,b,*pi;
表示定義了一個整型指針變量 pi 和兩個整型變量 a 和 b。

2) 在使用已定義好的指針變量時,在變量名前面不能加 *。例如:
int *p,a;
*p=&a; //錯誤,指針變量是p而不是*p
而如下語句是正確的。
int a,*p=&a; //正確
該語句貌似把 &a 賦給了 *p,而實際上 p 前的 * 僅是定義指針變量 p 的標識,仍然是把 &a 賦給了 p,故是正確的賦值語句。

3) 類型為該指針變量所指向的基類型,可以為 int、char、float 等基本數據類型,也可以為自定義數據類型。

該指針變量中只能保存該基類型變量的地址。

假設有如下變量定義語句:
int a,b,*pa,*pb;
char *pc,c;
則:
pa=&a;//正確。pa基類型為int,a為int型變量,類型一致
pb=&c;//錯誤。pb基類型為int,c為char型變量,類型不一致
pc=&c;//正確。pc基類型為char,c為char型變量,類型一致
*pa=&a;//錯誤。指針變量是pa而非*pa
4) 變量名是一合法標識符,為與普通變量相區分,一般指針變量名以字母 p(pointer)開頭,如 pa、pb 等。

5) 由于是變量,故指針變量的值可以改變,也即可以改變指針變量的指向。
char c1,*pc,c2;//定義了字符變量c1、c2和字符指針變量pc
則如下對指針變量的賦值語句均是正確的。
pc=&c1; //pc指向c1
pc=&c2; //pc不再指向c1,而指向c2
6) 同類型的指針變量可以相互賦值。
int a,*p1,*p2,b;//定義了兩個整型變量a,b;兩個整型指針變量為p1,p2
float *pf;
以下賦值語句均是正確的。
p1=&a; //地址箱p1中保存a的地址,即p1指向a
p2=p1; //p2也指向a,即p1和p2均指向a
上述最后一條賦值語句相當于把地址箱 p1 中的值賦給地址箱 p2,即 p2 中也保存 a 的地址,即和 p1 —樣,p2 也指向變量 a。

以下賦值語句均是錯誤的。
pf=p1; //錯誤。p1,pf雖然都是指針變量,但類型不同,不能賦值
pf=&b; //錯誤。指針變量pf的基類型為float,b類型為int,不相同
由于指針變量是專門保存地址值(指針)的變量,故本節把指針變量形象地看成“地址箱”。

設有如下定義語句:
int a=3,*pa=&a; //pa保存變量a的地址,即指向a
char c='d',*pc=&c; //pc保存變量c的地址,即指向c
把整型變量 a 的地址賦給地址箱 pa,即 pa 指向變量 a,同理 pc 指向變量 c,如圖 2 所示。


圖 2 指針指向變量

指針變量的引用

訪問內存空間,一般分為直接訪問間接訪問。

如果知道內存空間的名字,可通過名字訪問該空間,稱為直接訪問。由于變量即代表有名字的內存單元,故通。過變量名操作變量,也就是通過名字直接訪問該變量對應的內存單元。

如果知道內存空間的地址,也可以通過該地址間接訪問該空間。對內存空間的訪問操作一般指的是存、取操作,即向內存空間中存入數據和從內存空間中讀取數據。

在 C 語言中,可以使用間接訪問符(取內容訪問符)*來訪問指針所指向的空間。

例如:
int *p,a=3;//p中保存變量a對應內存單元的地址
p=&a;
在該地址 p 前面加上間接訪問符 *,即代表該地址對應的內存單元。而變量 a 也對應該內存單元,故 *p 就相當于 a。
printf("a=%d\n",a); //通過名字,直接訪問變量a空間(讀取)
printf("a=%d\n",*p); //通過地址,間接訪問變量a空間(讀?。?
*p=6;//等價于a=6;間接訪問a對應空間(存)

“野”指針

本節中,把沒有合法指向的指針稱為“野”指針。因為“野”指針隨機指向一塊空間,該空間中存儲的可能是其他程序的數據甚至是系統數據,故不能對“野”指針所指向的空間進行存取操作,否則輕者會引起程序崩潰,嚴重的可能導致整個系統崩潰。

例如:
int *pi,a; //pi未初始化,無合法指向,為“野”指針
*pi=3; //運行時錯誤!不能對”野”指針指向的空間做存入操作。該語句試圖把 3 存入“野”指針pi所指的隨機空間中,會產生運行時錯誤。
a=*pi; //運行時錯誤!不能對”野”指針指向的空間取操作。該語句試圖從“野”指針pi所指的空間中取出數據,然后賦給變量a同樣會產生運行時錯誤。
正確的使用方法:
pi=&a;//讓pi有合法的指向,pi指向a變量對應的空間
*pi=3;//把3間接存入pi所指向的變量a對應的空間

指針與數組

數組是一系列相同類型變量的集合,不管是一維數組還是多維數組其存儲結構都是順序存儲形式,即數組中的元素是按一定順序依次存放在內存中的一塊連續的內存空間中(地址連續)。

指針變量類似于一個地址箱,讓其初始化為某個數組元素的地址,以該地址值為基準,通過向前或向后改變地址箱中的地址值,即可讓該指針變量指向不同的數組元素,從而達到通過指針變量便可以方便地訪問數組中各元素的目的。

一維教組和指針

在 C 語言中,指針變量加 1 表示跳過該指針變量對應的基類型所占字節數大小的空間。指向數組元素的指針,其基類型為數組元素類型,指針加 1 表示跳過一個數組元素空間,指向下一個數組元素。

例如:
int *p,a[10];
p=a; //相當于 p=&a[0];
說明:數組名 a 相當于數組首元素 a[0] 的地址,即 a 等價于 &a[0]。

上述語句定義了整型指針變量 p 和整型數組 a,并使 p 初始指向數組首元素 a[0]。 當指針變量和數組元素建立聯系后,可通過以下三種方式訪問數組元素。

1) 直接訪問:數組名[下標]; 的形式。如 a[3]。

2) 間接訪問:*(數組名+i); 的形式。其中,i 為整數,其范圍為:0<i<N,N 為數組大小。數組名 a 為首元素的地址,是地址常量,a+i 表示跳過 i 個數據元素的存儲空間,即(a+i)表示 a[i] 元素的地址,從而 *(a+i) 表示 a[i]。

如果指針變量 p 被初始化為 a 之后,不再改變,那么也可以使用 *(p + i) 的形式訪問 a[i],不過這樣就失去了使用指針變量訪問數組元素的意義。

3) 間接訪問:*(指針變量);的形式。當執行語句 p=a; 后,可以通過改變 p 自身的值(可通過自增、自減運算),從而使得 p 中保存不同的數組元素的地址,進而通過 *p 訪問該數組中不同的元素。這是使用指針訪問數組元素較常用的形式。例如,如下代碼通過使用指針變量的移動來遍歷輸出數組中的每個元素。
for (p=a;p<a+N;p++) //用p的移動范圍控制循環次數
    printf ("%d\t",*p);
確定 p 指針移動的起止地址,即循環控制表達式的確定是使用指針訪問數組元素的關鍵。

p 初始指向 a[0],即 p=&a[0]; 或 p=a;。
p 終止指向 a[N-1],即 p=&a[N-l]; 或 p=a+N-1;。

故可得 p 的移動范圍為:p>=a && p<=a+N-1;,而 p<=a+N-1 通常寫成 p<a+N;,由此可得循環條件為:for (p=a;p<a+N;p++)。

數組名 a 和指針變量 p 的使用說明如下。有如下代碼:
int *p,a[10],i;
p=a;
1) 執行p=a; 后,*(a+i) 與 *(p+i) 等價,均表示 a[i]。

2) p[i] 與 a[i] 等價。a 為地址值,可采用 a[i] 形式訪問數組元素,而 p 也為地址值,故也可采用 p[i] 形式訪問數組元素。

3) a 為常量地址,其值不能改變,故 a++; 語法錯誤。而 p 為變量,其自身的值可以改變,故 p++; 正確。

【例 1】通過指針變量實現對數組元素的輸入和輸出操作。

實現代碼為:
#include <stdio.h>
#define N 10
int main (void)
{
    int *p,a[N],i;
    p=a; //p初始指向a[0]
    printf("Input the array:\n");
    for(i=0;i<N;i++) //用整型變量i控制循環次數
        scanf ("%d",p++); //指針P表示地址,不能寫成&P
    printf ("the array is :\n");
    for(p=a;p<a+N;p++) //用p的移動范圍控制循環次數
        printf("%d\t", *p);
    return 0;
}
補充說明:
輸入輸出循環控制方法有多種,不管采用哪種,必須準確確定起點和終點的表達式。

1) 輸入若采用p的移動范圍確定循環次數,則代碼如下。
for(p=a;p<a+N;p++)
    scanf("%d",p);
這時,for 語句之前的 p=a; 語句可以去掉。

2) 輸出若采用移動指針變量 p 控制循環的執行,因為執行完輸入操作后,p 已不再指向數組首元素,而是越界的 a[N] 初始位置,故必須重新給 p 賦值,讓其指向數組的首元素, 代碼如下。
p=a; //重新賦值,讓p指向數組首元素
for(i=0;i<N;i++)
    printf ("%d\t",*p++);
指針值加 1 與地址值加 1 的區別如下。

一般地址單元也稱內存單元,是按字節劃分的,即地址值加 1,表示跳過一個字節的內存空間。

在 C 語言中,指針變量加 1 表示跳過該指針變量對應基類型所占字節數大小的空間。

在 VC++ 6.0 中,整型占 4 個字節,故對于整型指針變量來說,指針值加 1 對應地址值加 4,即跳過 4 個字節;字符型占 1 個字節,故字符型指針變量加 1,對應地址值也加 1,即跳過 1 個字節。double 型占 8 個字節,故 double 型指針變量加 1,對應地址值加 8,即跳過 8 個字節等。

二維數組和指針

二維數組的邏輯結構為行列形式,但二維數組的存儲結構為順序形式。即二維數組中的數據元素在內存中的存儲地址是連續的,故可以使用指針變量保存各個元素的地址值,進而可以間接訪問二維數組中的各元素。

例如:
#define M 3
#define N 4
int a[M][N],*p,i,j;
上述語句定義了一個二維整型數組 a、整型指針變量 p 及整型變量 i 和 j。

訪問二維數組中的元素,目前可有如下兩種方法:
  1. 使用行列下標,直接訪問,即 a[i][j] 形式。如 a[2][3] 表示 2 行 3 列數組元素。
  2. 通過地址,間接訪問,即 *(*(a+i)+j) 形式。

M 行 N 列的二維數組 a,可以看成是含有 a[0]、a[1]、…、a[M-1] 等 M 個元素(M 行)的特殊一維數組,其每個元素 a[i](每行)又是一個含有 N 個元素(N 列)的一維數組。

由于 a[i] 可看成是“一維”數組 a的元素,而 a 可看成該“一維”數組的數組名。根據一維數組元素和一維數組名的關系可得:a[i] 等價于 *(a+i),均表示 i 行的首地址。

而 i 行又含有 N 個元素(N 列),即 a[i][0]、a[i][1]、a[i][2]、…、a[i][j]、…、a[i][N-1]。故 a[i] 表示i行對應一維數組的數組名。由于一維數組名 a[i] 即首元素 a[i][0] 的地址,即 a[i] 等價于 &a[i][0],用 <--> 表示等價,則有以下關系:
  • i 行首元素地址:a[i] + 0 <--> *(a + i) +0 <-->&a[i][0]
  • i 行 1 列元素地址:a[i] + 1 <--> *(a + i) +1<-->&a[i][1]
  • i 行 2 列元素地址:a[i] + 2 <--> *(a + i) +2<-->&a[i][2]
  • i 行 j 列元素地址:a[i] + j <--> *(a + i) + j <-->&a[i][j]

地址即指針,通過間接訪問符 *,可以訪問指針所指空間。即可得訪問二維數組元素 a[i][j] 的幾種等價形式如下。

*(a[i] + j) <--> *(*(a + i) + j) <-->*&a[i][j]<-->a[i][j]

【例 3】分析如下程序的運行結果,理解二維數組元素 a[i][j] 及其對應地址的各種等價表示形式。
#include<stdio.h>
#define M 3
#define N 4
int main (void)
{
    int *p,a[M][N]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
    p=&a[0][0];
    printf("The address of different rows:\n");
    printf ("a + 0 = %p\n",a);
    printf ("a + 1 = %p\n",a + 1);
    printf ("a + 2 = %p\n\n",a + 2);

    printf("The same address:\n");
    printf ("a[2] + 1 = %p\n",a[2] + 1);
    printf ("*(a+2) +1 = %p\n",*(a + 2)+1);
    printf ("&a[2][1] = %p\n\n",&a[2][1]);

    printf("The same element:\n");
    printf ("*(a[1] + 3) = %d\n",*(a[1] + 3));
    printf ("*(*(a + 1) + 3) = %d\n",*(*(a + 1)+3));
    printf ("a[1][3] = %d\n", a[1][3]);
    return 0;
}
程序某次的運行結果為:
The address of different rows:
a + 0 = 0060FEDC
a + 1 = 0060FEEC
a + 2 = 0060FEFC

The same address:
a[2] + 1 = 0060FF00
*(a+2) +1 = 0060FF00
&a[2][1] = 0060FF00

The same element:
*(a[1] + 3) = 8
*(*(a + 1) + 3) = 8
a[1][3] = 8

數組指針和指針數組

數組指針

數組指針,即指向一維數組的指針。

數組指針的定義格式為:

類型 (*指針名)[N]; //N元素個數

數組指針是指向含 N 個元素的一維數組的指針。由于二維數組每一行均是一維數組,故通常使用指向一維數組的指針指向二維數組的每一行。例如:
int (*p)[5];
上述語句表示定義了一個指向一維數組的指針 p,或者簡稱為一維數組指針 p,該指針 p 只能指向含 5 個元素的整型數組。

在定義數組指針時,如果漏寫括號 (),即誤寫成如下定義形式:
int *p[5];
由于下標運算符 [] 比 * 運算符的優先級髙,p 首先與下標運算符 [] 相結合,說明 p 為數組,該數組中有 5 個元素,每個為 int * 型。即 p 為指針數組。

二維數組 a[M][N] 分解為一維數組元素 a[0]、a[1]、…、a[M-1] 之后,其每一行 a[i] 均是一個含 N 個元素的一維數組。如果使用指向一維數組的指針來指向二維數組的每一行,通過該指針可以較方便地訪問二維數組中的元素。

使用數組指針訪問二維數組中的元素。

例如:
#define M 3
#define N 4
int a[M][N],i,j;
int (*p)[N]=a; // 等價于兩條語句 int (*p)[N] ; p=a;
以上語句定義了 M 行 N 列的二維整型數組 a 及指向一維數組(大小為 N)的指針變量 p,并初始化為二維數組名 a,即初始指向二維數組的 0 行。

i 行首地址與 i 行首元素地址的區別如下。
  • i 行首元素的地址,是相對于 i 行首元素 a[i][0] 來說的,把這種具體元素的地址,稱為一級地址或一級指針,其值加 1表 示跳過一個數組元素,即變為 a[i][1] 的地址。
  • i 行首地址是相對于 i 行這一整行來說的,不是具體某個元素的地址,是二級地址,其值加 1 表示跳過 1 行元素對應的空間。
  • 對二級指針(某行的地址)做取內容操作即變成一級指針(某行首元素的地址)。

兩者的變換關系為:

*(i 行首地址)=i 行首元素地址

0 行首地址:p + 0 <--> a + 0
1 行首地址:p + 1 <--> a + 1
...
i 行首地址:p + i <--> a + i

i 行 0 列元素地址:*(p + i) +0 <—> *(a + i) +0 <—>&a[i][0]
i 行 1 列元素地址:* (p + i) +1 <--> *(a + i) +1 <—>&a[i][1]
...
i 行 j 列元素地址:* (p + i) + j <--> * (a + i) + j <--> &a[i][j]
i 行 j 列對應元素:* (* (p + i) + j) <--> * (* (a + i) + j) <--> a[i][j]

由此可見,當定義一個指向一維數組的指針 p,并初始化為二維數組名 a 時,即 p=a;, 用該指針訪問元素 a[i][j] 的兩種形式 *(*(p + i) + j) 與 *(*(a + i) + j) 非常相似,僅把 a 替換成了 p 而已。
 
由于數組指針指向的是一整行,故數組指針每加 1 表示跳過一行,而二維字符數組中每一行均代表一個串,因此在二維字符數組中運用數組指針能較便捷地對各個串進行操作。

指針數組

指針數組,即存儲指針的數組,數組中的每個元素均是同類型的指針。

指針數組的定義格式為:

類型 *數組名[數組大小];

例如,如下語句定義了一個含有 5 個整型指針變量的一維數組。
int * a[5];
數組 a 中含有 5 個元素,每個元素均是整型指針類型。

可以分別為數組的各個元素賦值,例如:
int a0,a1,a2,a3,a4;

a[0]=&a0;
a[1]=&a1;
...
a[4]=&a4;
也可以使用循環語句把另一個數組中每個元素的地址賦給指針數組的每個元素。例如:
int i,*a[5],b[]={1,2,3,4,5};
for(i=0;i<5;i++)
    a[i]=&b[i];
這樣指針數組 a 中每個元素 a[i] 中的值,均為 b 數組對應各元素的地址 &b[i],即整型指針。由于 a[i]=&b[i],兩邊同時加取內容運算符 *,即 *a[i]=*&b[i]=b[i],即通過指針數組中的每個元素 a[i] 可間接訪問 b 數組。如下程序可以輸出 b 數組中的所有元素。
for(i=0;i<5;i++)
    printf ("%d\t",*a[i]) ; //*a[i]=b[i]
指針數組最主要的用途是處理字符串。在 C 語言中,一個字符串常量代表返回該字符串首字符的地址,即指向該字符串首字符的指針常量,而指針數組的每個元素均是指針變量,故可以把若干字符串常量作為字符指針數組的每個元素。通過操作指針數組的元素間接訪問各個元素對應的字符串。例如:
char * c[4]={"if","else","for","while"};
int i;
for (i=0;i<4;i++) //需確定數組元素個數
puts (c[i]) ; //輸出c[i]所指字符串
上述方法需要知道數組元素個數即該數組中字符串的個數。更通常的做法是,在字符指針數組的最后一個元素(字符串)的后面存一個 NULL 值。NULL 不是 C 語言的關鍵字,在 C 語言中 NULL 為宏定義:
#define NULL ( (void*) 0)
NULL 在多個頭文件中均有定義,如 stdlib.h、stdio.h、string.h、stddef.h等。只要包含上述某個頭文件,均可以使用 NULL。

上述語句可以修改為:
char *c[] = {"if","else","for", "while", NULL};
int i;
for (i=0;c[i]!=NULL;i++) //NULL 代替使用數組大小
    puts (c[i]);

指針與字符串

常量字符串與指針

1) 字符串與字符指針常量

字符串常量返回的是一個字符指針常量,該字符指針常量中保存的是該字符串首字符的地址,即指向字符串中第一個字符的指針。

例如,字符串常量 "abcd" 表示一個指針,該指針指向字符 'a',表達式 "abcd"+1,是在指針 "abcd" 值的基礎上加 1,故也是一個指針,指向字符串中第二個字符的指針常量。同理,"abcd"+3 表示指向第 4 個字符 'd' 的指針常量。

由于 "abcd"+1 表示指向字符 'b' 的指針常量,即保存 'b' 的地址,故如下兩條語句均是輸出從該指針地址開始直到遇到字符串結束符 '\0' 為止的字符串 "bcd"。
puts("abcd"+1);
等價于
printf ("%s\n","abcd"+1);
既然字符串返回指針,那么通過間接訪問符 *,可以訪問該指針所指向的字符,例如: *("abcd”+1) 表示字符 'b'; *("abcd"+3) 表示字符 'd'; *("abcd"+4) 表示空字符 '\0'; *("abcd"+5) 已越界,表示的字符不確定。

所以,以下兩條語句均輸出字符 'c'。
putchar (*("abcd"+2)) ; //輸出字符
printf ("%c",*("abcd"+2) ) ;//輸出字符'c'
以下語句輸出空字符(字符串結束符)。
putchar (*("abcd"+4)) ; //輸出字符串結束符空字符
由于 "abcd"+5 表示的指針已超出字符串存儲空間,該指針指向的內容 *("abcd"+5) 不確定。
putchar (*("abcd"+5)) ; //禁止使用。其值不確定
當字符數組名用于表達式時也是作為字符指針常量的,例如:
char c[]="xyz";
數組名 c 為指針常量,即字符的地址,故 c+1 為字符 'y' 的地址,故如下語句輸出 yz。
puts(c+1); //輸出yz并換行
字符串和字符數組名均表示指針常量,其本身的值不能修改。如下語句均是錯誤的。
c++; //錯誤。字符數組名c為常量
"xyz"++; //錯誤。字符串表示指針常量,其值不能修改
*("xyz"+1)='d'; //運行時錯誤。試圖把y,變為W

2) 字符串與字符指針變量

在 C 語言中,經常定義一個字符指針變量指向一個字符串,例如:
char *pc="abcd";
定義了一個字符指針變量 pc,并初始化為字符串 ”abcd”,即初始指向字符串的首字符,pc=pc+1; 表示向后移動一個字符單元,pc 保存字符 'b' 的地址,即指向字符 1。通過每次使 pc 增 1,可以遍歷字符串中的每個字符。

例如,如下代碼段通過指針變量依次遍歷輸出所指串中每個字符。

程序代碼為:
#include<stdio.h>
int main (void)
{
    //初始指向首字符
    //間接訪問所指字符 //pc依次指向后面的字符
    char *pc="hello,world!";
    while (*pc! = '\0')
    {
        putchar(*pc);
        pc++;
}
    return 0;
}
通過字符指針變量可訪問所指向的字符串常量,但僅限于“讀取”操作,也可以修改字符指針變量的指向,即讓其重新指向其他字符串;但不能進行“修改”操作,即不能通過該指針變量,企圖改變所指向字符串的內容。有些編譯器在編譯時可能不報錯,但運行時會發生錯誤。例如:
char *pc; //正確,未初始化,隨機指向
該語句定義了一個字符指針變量,并未顯式初始化,屬于“野”指針,不能對該指針所指內容進行存取操作。由于 pc 為變量,故可以修改指針的指向,即可以讓指針變量 pc 重新指向其他字符串。故如下操作是正確的。
pc="abcd"; //正確,讓 pc 指向串 "abcd"
pc="hello"; //正確,修改pc指向,讓其指向串"hello"
此時,字符指針變量 pc 已指向字符串常量 "hello”,不能通過指針修改該字符串常量。 如下操作是錯誤的。
*(pc+4) = 'p'; //運行時錯誤。試圖把'o'字符改變為'p'
更不允許企圖通過 pc 指針,覆蓋其所指字符串常量。如下企圖使用 Strcpy 把 "xyz" 串復制并覆蓋 pc 所指串 "hello”。
strcpy(pc,"xyz");//運行時錯誤。企圖把另一個串復制到pc空間

變量字符串

字符數組可以理解為若干個字符變量的集合,如果一個字符串存放在字符數組中,那么字符串中的每個字符都相當于變量,故該字符串中的每個字符均可以改變,故可把存放在字符數組中的字符串稱為變量字符串。

1) 字符數組空間分配 例如:
char str[10]="like";
定義了一個字符數組并顯式指定其大小是 10(數組空間應足夠大,一般大于等于字符串長度 +1),即占 10 個字符空間,前 5 個空間分別存放有效字符 'l','i','k','e' 及結束符 '\0',多余的空間均用 '\0' 填充。

定義時也可以不顯式指定其大小,讓編譯器根據初始化字符串長度加 1 來自動分配空間大小。例如:
char s[]="like";
編譯器為該數組分配 5 個字符空間大小,前 4 個為有效字符第 5 個為結束符 '\0'。

2) 訪問字符數組元素

使用數組下標的形式可以逐個改變數組中的每個元素,如下所示。
s[1]=*o'; //正確。
s[2] ='v';; //正確。'k'->'v'
puts (s); //輸出love并換行
可以把字符數組和字符指針聯合使用,如下所示。
char str[]='I Like C Language!";
char *ps=str;//初始指向字符串首字符"I"
*(ps+3)='o';//'i'->'o'
*(ps+4)='v';//'k'->'v'
ps=str+2;//ps指向'L'字符
puts (ps);//輸出 Love C Language!并換行

3) 字符數組訪問越界

不管采用數組名加下標形式還是使用字符指針變量訪問字符串,一定不能越界。否則可能會產生意想不到的結果,甚至程序崩潰。如下操作均是錯誤的。
char c[]="Nan Jing", *pc=c+4; //c 大?。?,pc 指向'J'
c[10] ='!'; //錯誤。沒有c[10]元素,越界存儲,編譯器不檢查數組是否越界
*(pc+6)='!'; //錯誤。越界存,pc+6 等價于 c[10]
putchar (c[9]) ; //錯誤,越界取,值不確定

4) 字符串結束符

一般把字符串存放于字符數組中時,一定要存儲字符串結束符 '\0',因為 C 庫函數中,對字符串處理的函數,幾乎都是把 '\0' 作為字符串結束標志的。如果字符數組中沒有存儲結束符,卻使用了字符串處理函數,因為這些函數會尋找結束符 '\0',可能會產生意想不到的結果,甚至程序崩潰。例如:
char s1[5]="hello"; //s1不含'\0'
char s2[] = {'w','o','r','l','d'}; //s2大小:5,不含'\0'
char s3[5]; //未初始化,5個空間全為不確定值
s3[0] = 'g';
s3[1]='o';
s3 數組的前兩個空間被賦值為 'g' 和 'o',未被顯式賦值的 s3[2]、s3[3]、s3[4] 依然為不確定值。即 s3 數組中依然不含有字符串結束符 '\0'。s3 數組各元素如下所示('?'表示不確定值)。

s3[0] s3[1] s3[2] s3[3] s3[4]
g o ? ? ?

故以下操作語句嚴格來說均是錯誤的,是被禁止的操作。
puts(s2); //s2中不含'\0',輸出不確定值,甚至程序崩潰.
strcpy (s1, s3) ; //運行時錯誤。s3中找不到結束符'\0'
注意 s3 數組與如下 s4 數組的區別。
char s4[5] = {'g','o'};
s4 數組中有 5 個元素,初始化列表中顯式提供了兩個字符 'g' 和 'o',其他元素使用字符的默認值:空字符,即結束符 '\0'。s4 數組各元素如下所示。

s4[0] s4[1] s4[2] s4[3] s4[4]
g o \0 \0 \0

故對 s4 數組的如下操作語句均是正確的。
puts (s4); //輸出go并換行
strcpy (s1,s4) ; //把 s4 中的串 go 和一個'\o'復制到 s1 中。
執行上述語句后,s1 數組中各元素如下所示。

s1[0] s1[1] s1[2] s1[3] s1[4]
g o \0 1 o

此時,s1 數組中也含有字符串結束符??梢哉{用字符串處理函數(第一次遇到 '\0' 表示一個串結束),如下所示。
int len=strlen (s1) ; // 正確,len 為 2
puts (s1); //正確,輸出go并換行

5) 通過字符指針修改變量字符串

通過字符指針變量可以訪問所指字符數組中保存的串,不僅可以讀取該數組中保存的字符串,還可以修改該串的內容。原因從數組的本質上理解:數組是一系列相同類型變量的集合,故其中保存的字符串,可以理解為是由若干個字符變量組成的。每個字符變量當然可以改變。

例如:
#include<stdio.h>
#include<string.h>
int main (void)
{
    char str[30]="Learn and live."
    *p=str;
    *(p+6)='A';
    *(p+10)='L';
    puts(str);
    return 0;
}
該程序中,字符指針 p 指向數組 str 中的字符串,由于該字符串是由一系列字符變量組成的,故通過指針變量 p 可以改變該字符串中的字符。故該程序輸出:Learn And Live.。

指針與函數

指針作函教形參——傳址調用

在函數章節中,講述了函數調用的兩種形式:傳值調用傳址調用,其中,傳址調用介紹的是數組類型作函數形參,數組名作實參的形式。

現在介紹傳址調用的另外一種形式,即指針變量作函數形參,地址(或其他指針變量)作實參的形式。函數調用時,在函數體內可以通過實參地址間接地對該實參地址對應的空間進行操作,從而實現可以在函數體內改變外部變量值的功能。

傳值調用與傳址調用的區別如下:
  • 傳值調用:實參為要處理的數據,函數調用時,把要處理數據(實參)的一個副本復制到對應形參變量中,函數中對形參的所有操作均是對原實參數據副本的操作,無法影響原實參數據。且當要處理的數據量較大時,復制和傳輸實參的副本可能浪費較多的空間和時間。
  • 傳址調用:顧名思義,實參為要處理數據的地址,形參為能夠接受地址值的“地址箱”即指針變量。函數調用時,僅是把該地址傳遞給對應的形參變量,在函數體內,可通過該地址(形參變量的值)間接地訪問要處理的數據,由于并沒有復制要處理數據的副本,故此種方式可以大大節省程序執行的時間和空間。

指針作函教返回類型——指針函教

有時函數調用結束后,需要函數返回給調用者某個地址即指針類型,以便于后續操作,這種函數返回類型為指針類型的函數,通常稱為指針函數。

指針函數的定義格式為:

類型*函數名(形參列表)
{
    ... /*函數體*/
}

指針函數,在字符串處理函數中尤為常見。

例如,編程實現把一個源字符串 src 連接到目標字符串 dest 之后的函數,兩串之間用空格隔開,并返回目標串 dest 的地址。

實現代碼為:
#include<stdio.h>
char * str_cat (char * dest, char * src);
int main (void)
{
    char s1[20]="Chinese"; //目標串
    char s2[10]="Dream";
    char *p=str_cat(s1, s2); //返回地址賦給 p
    puts (p);
    return 0;
}

//str_cat的參數也可為字符數組形式
char * str_cat(char * dest, char * src)
{
    char *p1=dest,*p2=src;
    while (*p1!='\0') //尋找dest串的結尾,循環結束時,p1指向'\0T字符
        p1++;
    *p1++=' '; //加空格,等價于*p1='';p1++;
    while (*p2!='\0')
        *p1++=*p2++;
    return dest;
}
運行結果:
Chinese Dream

指向函教的指針——函教指針

在 C 語言中,整型變量在內存中占一塊內存空間,該空間的起始地址稱為整型指針,可把整型指針保存到整型指針變量中。函數像其他變量一樣,在內存中也占用一塊連續的空 間,把該空間的起始地址稱為函數指針。而函數名就是該空間的首地址,故函數名是常量指針??砂押瘮抵羔槺4娴胶瘮抵羔樧兞恐?。

1) 函數指針的定義

函數指針變量的定義格式為:

返回類型(*指針變量名)(函數參數表);

說明:上述定義中,指針變量名定義括號不能省略,否則,則為返回指針類型的函數原型聲明,即指針函數的聲明。

例如:
int *pf (int,int);//該語句聲明了一個函數原型,該函數名為pf,該函數含兩個int型參數,且該函數返回類型為整型指針類型,即int*。
int (*pf) (int,int);//該語句定義了一個函數指針變量pf,該指針變量pf可以指向任意含有兩個整型參數,且返回值為整型的函數。
如下定義了一個func函數。
int func (int a, int b)
{
    //...
}
該函數含有兩個整型參數,且返回類型為整型。與 pf 要求指向的函數類型一致,可讓 pf 指向該函數,可以采用如下兩種方式。
pf=&func; //正確
pf=func; //正確。也可省略&
在給函數指針變量賦值時,函數名前面的取地址操作符 & 可以省略。因為在編譯時,C 語言編譯器會隱含完成把函數名轉換成對應指針形式的操作,故加 & 只是為了顯式說明編譯器隱含執行該轉換操作。

有如下三個函數的原型聲明:
void f1(int);
int f2(int,float);
char f3(int,int);
可能有些編譯器對類型檢查不嚴格,但嚴格意義上來說,如下對函數指針的賦值語句均認為是錯誤的。
pf=f1; //錯誤。參數個數不一致、返回類型不一致
pf=f2; //錯誤。參數2的類型不一致
pf=f3; //錯誤。返回類型不一致

2) 通過函數指針調用函數

例如,如下 f() 函數原型及函數指針變量 pf:
int f (int a);
int (*pf) (int)=&f; //正確。pf 初始指向 f()函數
當函數指針變量 pf 被初始化指向函數f()后,調用函數 f() 有如下三種形式。
int result;
result=f(2); //正確。編譯器會把函數名轉換成對應指針
result=pf(2); //正確。直接使用函數指針
result=(*pf)(2); //正確。先把函數指針轉換成對應函數名
函數調用時,編譯器把函數名轉換為對應指針形式,故前兩種調用方式含義一樣,而第三種調用方式,*pf 轉換成對應的函數名 f(),編譯時,編譯器還會把函數名轉換成對應指針形式,從這個角度來理解,第三種調用方式走了些彎路。

函數指針通常主要用于作為函數參數的情形。

假如實現一個簡易計算器,函數名為 cal(),假設該計算器有加減乘除等基本操作,每個操作均對應一個函數實現,即有 add()、sub ()、mult()、div() 等,這 4 個函數具有相同的參數及返回值類型。即:
int add (int a, int b); //加操作
int sub (int a, int b) ; //減操作
int mult (int a, int b) ; //乘操作
int div (int a, int b); //除操作
定義函數指針變量 int (*pf) (int,int);,該函數指針變量 pf 可分別指向這 4 個函數。

如果用戶調用該計算器函數 cal(),希望在不同的時刻調用其不同的功能(加減乘除),較通用的方法,是把該函數指針變量作為計算器函數 cal() 的參數。即:
//計算器函數
void cal (int (*pf) (int, int) , int op1, int op2)
{
    pf (op1,op2) ;//或者(*pf) (op1,op2);
}
假如當用戶希望調用 cal() 函數實現加操作時,只需把加操作函數名 add() 及加數和被加數作為實參傳給 cal () 函數即可;此時 pf 指針指向 add() 函數,在 cal () 函數內通過該函數指針變量 pf 調用其所執行的函數 add ()??刹捎萌缦聝煞N調用方式。
pf (op1,op2);
或者
(*pf)(op1,op2);
例如,使用函數指針,編程實現一個簡單計算器程序。實現代碼為:
#include<stdio.h>
void cal(void (*pf) (int,int),int opl,int op2);
void add (int a, int b) ; //加操作
void sub (int a, int b) ; //減操作
void mult (int a, int b) ; //乘操作
int main (void)
{
    int sel,x1,x2;
    printf ("Select the operator:");
    scanf("%d",&sel);
    printf("Input two numbers:");
    scanf ("%d%d",&x1, &x2);
    switch(sel)
    {
        case 1:
            cal(add,x1,x2);
            break;
        case 2 :
            cal(sub,x1,x2);
            break;
        case 3:
            cal(mult,x1,x2) ;
            break;
        default:
            printf ("Input error!\n");
    }
    return 0;
}
void cal (void (*pf) (int, int) , int opl, int op2)
{
    pf (opl,op2) ; //或者(*pf)(opl,op2);
}
void add (int a, int b)
{
    int result=(a + b);
    printf("%d + %d = %d\n",a,b,result);
}
void sub (int a, int b)
{
    int result= (a - b);
    printf ("%d - %d = %d\n", a,b, result);
}
void mult (int a, int b)
{
    int result= (a * b);
    printf ("%d * %d = %d\n",a,b,result);
}
運行結果為:
Select the operator:1
Input two numbers:2 5
2 + 5 = 7

精美而實用的網站,提供C語言、C++、STL、Linux、Shell、Java、Go語言等教程,以及socket、GCC、vi、Swing、設計模式、JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo