MyMSDN

MyMSDN记录开发新知道

谈void changeString(char **s),指向指针的指针

void changeString(char **t){
	*t = "world";
}

void changeString2(char *t[]){
	*t = "world2";
}

typedef char *String;
void changeString3(String *s){
	*s = "world3";
}

void Inc(int *i){
	(*i)++;
}

问题列表:
1、如何改变外部变量?
2、啥时候我们需要使用**?
前言:
先看Inc,我们知道int i是一个值类型,为了能够达到修改它的目的,我们需要将i的实际地址通过值传递的方式从调用函数传递给被调用函数,因为对相同的地址的变量进行操作,所以我们的Inc(&i)将如我们所愿,顺利地递增。
以上两个版本的changeString都是可以达到修改调用函数中的字符串的。如果按照下面的代码将得到不正确的结果。

错误代码示例:
void errorChangeString(char *t){
	t = "change!";
}

int main(void){
	char *s = "Hello";
	errorChangeString(s);

	return EXIT_SUCCESS;
}

在错误示例代码中,假设传递的s则为指向字面值"Hello"的首字母'H'所在的地址值,假设这个值为0x1000。在errorChangeString中,假设"change"字面值的首字母'c'所在的地址值为0x2000,s被拷贝给了t,t的任何改动和s没有任何关联,因此,s仍然指向0x1000,而不是指向0x2000。

我们应如何看待char **t?

若要让所谓的t的值能够跟着s变化,我们则需要指向指针的指针来帮忙。我们知道要让函数传递(C语言只有值传递)后,仍然指向相同的值(这里指s始终为0x4000(s指向0x1000,假设它自身地址为0x4000)),则我们需要将这个传递的值进行一次包装,使我们通过形参也能够控制相同的地址所在的变量(这里指0x4000),因此,我们对形参定义一个指针,形如 char* *t(等价于char **t),这样*t与s就代表了相同的值而不会因为传递而无法操纵s,因此可以在被调用函数内部使用*t来指代要操作的外部变量(通过参数传递),因此在内部*t="world"(假设"world"的首地址为0x2000),则s的值就被修改为"world"的首地址。(如下图所示)


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

posted on 2008-08-30 22:09 volnet 阅读(3650) 评论(4)  编辑 收藏 引用 所属分类: C/C++

评论

# re: 谈void changeString(char **s),指向指针的指针[未登录] 2008-09-01 10:12 raof01

第三条结论怎么得出的?

分析得不错,就是有点麻烦。TCPL里指针讲得比较好,简明扼要。  回复  更多评论   

# re: 谈void changeString(char **s),指向指针的指针 2008-09-02 11:30 volnet

@raof01
我怎么能跟TCPL比呢~~·人家是之父,我是之孙孙……呵呵  回复  更多评论   

# re: 谈void changeString(char **s),指向指针的指针[未登录] 2008-09-05 13:02 raof01

靠,别误会了,只是说这篇应当参考TCPL,写得简洁点。  回复  更多评论   

# re: 谈void changeString(char **s),指向指针的指针 2008-09-05 22:02 volnet

@raof01
被你说的一晕一晕的~  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


特殊功能