我們應如何看待char *t[]?在我們的changeString2(char *t[])中,我們用char *t[]取代了char **t,我們知道char *t[]代表t是一個數組,數組的每一個成員都是一個char*類型的指針。我們也成為
指針數組。下面讓我們看一個調用:
void changeStrArr(char *t[]){
*t = "World";
}
int main(void){
char *sArr[] = {
"Hello"
};
printf("%s",*sArr);
changeStrArr(sArr);
printf("%s",*sArr); //printf("%s",sArr[0]);
return EXIT_SUCCESS;
}
這是教科書上比較常見的指針數組形式,甚至還會簡單不少(它們的數組通常會有多個元素并用*t++來控制移位)。sArr在這里就是這個數組,因此sArr[0]即為指向該數組第一個元素的指針(因為是指針數組,每一個元素都是一個指針),因此使用printf("%s",*sArr); 或者printf("%s",sArr[0]);都將標準輸出sArr的第一個元素所指向的字符串。
下面我們來看一下下面這段代碼:
void changeString2(char *t[]); //函數體見本文頂部
int main(void){
char *s="Hello";
printf("%s",s);
changeString2(&s);
printf("%s",s);
return EXIT_SUCCESS;
}
從這段代碼中我們主要講s換成了一個字符而不是上一段代碼中的字符指針數組sArr,從上一段代碼我們可以得知s和sArr之間的關系,*s==*sArr[0]==**sArr;(我們可以通過strcmp(q,qArr[0])或者strcmp(q,*qArr);進行判斷,我們知道strcmp(const char *_Str1, const char *_Str2);也就是我們傳遞的q和*qArr均為字符指針也就是它們的定義通常為char *q和char **qArr)。為此我們可以將其進行移項,也可以得到等價表達式(規律:==兩側同時添加相同符號等式依舊不變(在*和&的邏輯里成立),同時出現&*,兩符號起中和作用(先從一個地址中取值,再從值反求它的地址,因此最終結果還是地址本身))也就是&*s==&*sArr[0]==&**sArr <=> s==sArr[0]==*sArr,這樣,再進行一次,&s==&sArr[0]==&*sArr,也就是&s==&sArr[0]==sArr因此changeStrArr(sArr)<=>changeStrArr(&s),因此從上面的代碼段到下面代碼段的演化是成功的(changeString2和changeStrArr本質上沒有差別)。
下面的示例圖則從本質上分別分析了兩者的各自的理由(非上述推理):
用typedef char *String;改良后的程序具有更高的可讀性可以看到第三段代碼中我們在函數聲明前用typedef語句定義了typedef char *String;首先從typedef的本質來講,這種定義將導致使用它的changeString3與changeString函數具有相同的本質,但是從閱讀的習慣上來講,用String而不是用char *的方式,則顯得更加親切。首先我們從眾多起他語言中,比如C#中,C#實現了類型String/string的方式,我們知道String是一個引用類型,但我們同時也知道string類型有個顯著的特征,就是它雖然是引用類型,每次對它的操作總是像值類型一樣被復制,這時候,我們定義的任何(C#):ChangeString(string str);將不起作用,而我們需要增加ref關鍵字來告訴編譯器它是同一實例,而不進行重新申請空間重新分配等一系列復雜操作,于是ChangeString(ref string str);的語句就有類似值類型的一些地方了,同樣,在C語言中,changeString2(String *s)也達到了同樣的效果。這樣的方式也同時對我們更加了解第一種方式起到了輔助作用。(用C#來比喻可能不是太好,因為很多讀者通常都是先接觸C再有機會才接觸C#的,而且也沒有講解到本質)
void changeString3(String *s); //函數體見本文頂部
int main(void){
char *s="Hello";
printf("%s",s);
changeString3(&s);
printf("%s",s);
return EXIT_SUCCESS;
}
本質呢?因為任何一次的"Hello",其中的"Hello"是常量,而不是變量,它的存儲空間在編譯時就已經確定了,它放在了靜態常量區中,因此它的地址不會變也不能加減。因此String,也就是char *指向的是一個不可變的常量,而非變量。(例如我們一直假設char *s = "Hello",的首地址s==0x1000(s的值,不是s的地址),那么它始終是0x1000,但是s是變量,s可以拋棄0x1000指向別的字符串字面值(char literal),但是我們知道C語言中只有按值傳遞,因此我們必須用它的指針假設s的地址0x3000,那么,我們將0x3000進行傳遞,這樣內部就可以對0x3000進行操作了,這樣可以用(0x3000)->value的方式修改value指向0x2000的地址(假設這個地址是"GoodBye"的值),這樣我們的s就被修改了。因為我們的常量在編譯時就已經分配了地址,在程序加載后就長久存在,知道應用程序退出后會跟著宿主一并消失,所以我們同樣不需要free操作)。
下一個問題:
啥時候我們需要用到**?
通過以上的幾個直觀的示例,我們大體了解了一個字符串通過一個函數參數進行修改的具體情況。這是一個很發散性的問題,我也沒有一個很肯定的100%的答案。
從void **v;(//void代表任意類型,可以是int/char/struct node等)定義的本質上來觀察這個問題,我們可以推論void **v;,當我們需要獲取并改變*v的地址值得時候,我們都有這個需要(因為單從void *v的角度講,我們只能夠獲取v的地址改變v的值,但不能改變v的地址)。那我們什么需要獲取并改變*v的值呢?從上面的分析我們不難得出,我們在需要改變v的地址的時候即有這個需要。
下面是一個鏈表的例子:
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
int value;
struct node *next;
} Node;
Node *createList(int firstItem){
Node *head = (Node *)malloc(sizeof(Node));
head ->value = firstItem;
head ->next = NULL;
return head;
}
void addNode(Node *head, Node **pCurrent,int item){
Node *node = (Node *)malloc(sizeof(Node));
node ->value = item;
node ->next = NULL;
(*pCurrent)->next=node;
*pCurrent = node;
}
typedef void (*Handler)(int i);
void foreach(Node *head, Handler Ffront, Handler Flast){
if(head->next!=NULL){
Ffront(head->value);
foreach(head->next,Ffront,Flast);
}
else
{
Flast(head->value);
}
}
void printfFront(int i){
printf("%d -> ",i);
}
void printfLast(int i){
printf("%dn",i);
}
int main(void){
Node *head, *current;
current = head = createList(0);
for(int i=1;i<10;i++)
addNode(head,¤t,i);
foreach(head, printfFront, printfLast);
return EXIT_SUCCESS;
}
//函數輸出
0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9
這個程序中的關鍵部分就是當前節點值current的確定,部分老師可能會圖方便采用全局變量進行當前值的確定,這個在拋棄型的示例中當然無傷大雅,也很好地描述了鏈表的本質,這本沒什么關系,但是鏈表是一個常用的數據結構,并發怎么辦?操作多個鏈表怎么辦?總之我們要秉承“方法共享,數據不共享的原則”,這樣就不太容易出現問題了。這里我們在main函數中定義了唯一的*head用于標識鏈表的頭,并希望它始終扮演鏈表頭的角色,不然我們最后將無法找到它了。我們用一個同樣類型的節點current指向了當前節點,并始終指向當前節點(隨著鏈表的移動,它將指向最后一個節點)。由于我們的current是主函數中定義的,而它的修改是在被調函數中進行的。因為我們需要改變的*current的值,根據我們的分析,對于要修改值的,我們有使用**的必要,而類似只需要讀取值的head,則沒有任何需要了。
這個程序代表了一種使用**的典型用法,也是大部分需要使用**的用法。
總結:
不論它怎么變化,怎么復雜,我們需要把握幾點:
1、C語言中,函數傳遞永遠是值傳遞,若需要按地址傳遞,則需要用到指針(類似func(void *v){...});
2、在對于需要變化外部值的時候,直接尋址的使用*,間接尋址的使用**;
3、對于復雜的表達式,善于使用嵌套的思路去分析(編譯器亦或如此),注意各符號之間的優先級。