指针是一种保存变量地址的变量,可以让代码更高效、紧凑,同时也是比较难掌握的一项知识。特别是指针联合结构体(struct)和函数时,很不容易搞清楚那个指针到底指到哪里去了。如果程序一旦出现问题,很难解决。所以先得一步一步来,把基础打好。
1,值传递与引用传递。在讨论函数指针以前,先说点更简单的,假如有两个整数变量int a,b,要交换a,b的值该怎么做呢?
void swap(int x, int y) {
int temp;
temp = x;
x = y;
y = temp;
}
由于C语言是面向过程的语言,不像其它面向对象的语言传对象引用,C语言传的只是一个数值过来,所以即便调用这个函数,也只是交换了下参数x,y的值,并不会反映到a,b这个两个变量上去。这个时候指针就派上用场了,因为指针传递的就是引用地址,而不是值。
void swap(int *px, int *py){
int temp;
temp = *px;
*px = *py;
*py = temp;
}
由于参数是指针,所以调用的时候就要传地址过去:
swap(&a, &b);
因为参数*px,*py是指 a,b引用的地址,所以如果再交换的话,这个两个值就真的改变了。
2,对指针的初始化。
对指针有意义的初始化只能是0或者是表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址。
int *p;
*p = 5;//这种直接赋值给指针的做法是不允许的。
//正确做法
int a = 5;
int *p = &a;
当然,有时候我们使用的指针并没有指向具体的地址,比如我们从控制台输入一些字符数据,我们想向指针来引用这些数据,但是在定义指针时,我们并不知道这些数据有多长,也不知道它们的地址是什么,该怎么办呢?来看下面代码:
#define ALLOCSIZE 10000
static char allocbuf[ALLOCSIZE];
static char *allocp = allocbuf;
char *alloc(int n){
if(allocbuf + ALLOCSIZE - allocp >= n){
allocp +=n;
return allocp - n;
}else{
return 0;
}
}
/* 如果只输入一个回车符,line的长度为1 */
int getline(char s[],int lim){
int c,i;
for(i = 0; i<lim - 1 && (c = getchar())!=EOF && c != '\n';i++){
s[i] = c;
}
if( c == '\n'){
s[i] = c;
++i;
}
s[i] = '\0';
return i;
}
int readlines(char *lineptr[],int maxlines){
int len,nlines;
char *p,line[MAXLEN];
nlines = 0;
while(nlines<maxlines){
if((len = getline(line,MAXLEN))<=0){
continue;
}
if( (p = alloc(len))== NULL){
return -1;
}else{
line[len-1] = '\0';
strcpy(p,line);
lineptr[nlines++] = p;
}
}
return nlines;
}
上面的代码来自于《C程序设计语言》,这本书是C语言设计者写的,虽然比较薄,但是写得真好。标准库里面有malloc,这里用于举例子,所以用了一个简易版的alloc。alloc函数的作用就是从allocbuff分配n个字符的存储空间。getline函数是从控制台获取输入字符,并存储在其函数参数char s[]中,并返回每一行输入字符的个数。
readlines函数就要复杂一点了,它的参数是char *lineptr[],这是个什么意思呢?意思是lineptr数组中的每个元素是一个字符类型的指针,即每个元素都指向一个字符串。结合上面代码的意思,*lineptr[2]表示控制台输入的第三行文本字符串的首字符。
交换数组间的元素对排序来说,很重要,代码如下:
void swap(char *v[], int i, int j){
char *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
如果v数组中有10个字符串,我们可以通过swap函数来交换任意两个字符串在数组中的位置。
3,排序。
其实排序本身不复杂,但是采用指向函数的指针来排序,就不那么好理解了。当然,这也正是本文的重点所在。排序的算法可以采用经典的固定算法,但排序的规则我们可以自己制定,比如以数字大小的方式排序、以字符的先后顺序排序。有没有办法可以实现:我们写一个排序的算法,然后制定不同的比较规则,然后排序算法根据这些规则,实现不同的排序效果呢?答案是可以的,这其实和现在面向对象编程的中使用的“比较器设计模式”类似,只是没得那么优雅而已。
void quicksort(void *v[],int left,int right,int(*comp)(void *,void *)){
int i,last;
void swap(void *v[],int,int);
if(left >= right){
return;
}
swap(v,left,(left + right)/2);
last = left;
for(i = left + 1; i <= right; i++){
if((*comp)(v[i],v[left]) < 0){
swap(v, ++last,i);
}
}
swap(v,left,last);
quicksort(v,left,last - 1,comp);
quicksort(v,last + 1,right,comp);
}
上面的代码采用的是快速排序算法,这个算法很经典,在此不做详述。此排序算法多了一个参数:
int(*comp)(void *,void *)
这个东西不太好理解,指针的指向越来越复杂,以前指向基本的整数变量、数组变量,这次变成了函数。先说简单的,参数void *表示通用指针类型,任何其它类型指针都可以转换为void *类型,当然也可以转回去,且信息不会丢失。
而*comp代表一个函数,用在上面的quicksort的参数中,意思是调用quicksort函数时,最后一个参数是一个函数,而且这个函数有两个指针参数。比如:
int numcmp(char *s1,char *s2){
double v1,v2;
v1 = atof(s1);
v2 = atof(s2);
if(v1 < v2){
return 1;
}else if(v1 > v2){
return -1;
}else{
return 0;
}
}
怎么调用quicksort函数呢?
char *lineptr[5];
quicksort((void **)lineptr,0,nlines - 1,(int (*)(void *,void *))numcmp);
注意,lineptr的类型是char *,而quicksort的参数类型全部是void *,所以调用quicksort时,要将这些变量转型成void *类型,那为什么有两个*呢?普通的指针直接void *,而lineptr是一个指针数组,也就是指向指针的指针,所以要两个*才能将每个元素都转换成void类型。
如果还有一个字符串的比较规则,怎么根据不同规则来调用quicksort实现排序呢?标准库里面有一个strcmp函数,用于比较字符串,我们通过一个参数来判断调用何种规则。
int nlines;
int numeric;
if(argc > 1 && strcmp(argv[1],"-n") == 0){
numeric = 1;
}
nlines = readlines(lineptr,MAXLINES);
quicksort((void **)lineptr,0,nlines - 1,(int (*)(void *,void *))(numeric? numcmp : strcmp));
也就是通过程序命令行的参数来确定numeric的值,从而确定是调用numcmp还是strcmp函数,不过上面的代码有个问题,strcmp的定义如下:
int __cdecl strcmp(const char *_Str1,const char *_Str2);
这里又出现一个新概念,那就是const char *_Str1,_Str1是指向常量的指针(_Str1 is a pointer to const char)。const放在声明的变量前,表示该变量的值不可修改,即const int a、const char arr[]="abc"这种声明后,变量a和arr里面的值就不能再修改了。所以_Str1指针指向的值也不能再修改了。numcmp函数定义如下:
int numcmp(char *s1,char *s2);
很明显这两个函数的参数类型不一致,如果直接运行,会出现警告或者错误。所以要把numcmp参数也改成const类型的,即:
int numcmp(const char *s1,const char *s2);
至此,指针常用的用法都涉及到了,但是指针还有更多更复杂的用法,所以要掌握指针绝不是件容易的事情。