我们应如何看待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、对于复杂的表达式,善于使用嵌套的思路去分析(编译器亦或如此),注意各符号之间的优先级。