C语言-指针2

指针变量做函数
指针做函数返回值
二级指针
指针数组
函数指针
总结

函数中的参数不仅仅只有整数、浮点数,字符等数据还可以是指针变量,例如:

#include <stdio.h>
void swap(int *p1, int *p2){
    int temp;  //临时变量
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main(){
    int a = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

运用指针变量直接改变内存地址中的值,所以不需要返回值也会修改变量指向地址中的值。

用数组做函数参数

如果我们想在函数中处理数组,那么我们就必须用到指针了,我们用一个简单的程序,给大家讲解一下。取最大值:

#include <stdio.h>
int max(int *intArr, int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}
int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //读取用户输入的数据并赋值给数组元素
    for(i=0; i<len; i++){
        scanf("%d", nums+i);
    }
    printf("Max value is %d!\n", max(nums, len));
    return 0;
}

运行结果;

12 55 30 8 93 27↙
Max value is 93!

利用指针变量读取数组存在内存中的数值,将其中的值取出然后做判断。参数intArr仅仅是一个数组指针,在函数内部无法通过这个指针获取数组的长度,必须将数组长度作为实参传到函数的形参中。我们还可以将函数中*intArr也可以写成真正的数组形式:intArr[6]intArr[]这两种方式都没有问题。虽然这两种方式看着像是重新定义了两个数组,但是这两个数组并不会创建一个数组出来,编译器不会为它们分配内存,数组实际上是不存在的,他们最终还是会转换为指针,也就是说,它们也不能直接把主函数中数组的所有元素全部存入,大家还是要规规矩矩使用指针。

而我们定义的intArr[6]这种形式只能说函数期望主函数传过来6个元素并不是说数组只能有六个元素,真正传递的数组可以有少于或多于6个的元素。

强调一下不管用什么方法传递数组,在函数内部都不能求得数组的长度,因为intArr是一个指针,而不是数组,所以要增加一个参数来传递数组长度。

参数的传递本质上是一次赋值的过程,而这种赋值就是对内存进行拷贝。所以也是因为这个的原因,如果我们每次都是将所有数组中的值一起拷贝过去就将导致工作量过于大,当我们的数据成千上万时,我们要考虑我们复制这个内存空间的时间,它会严重拖慢程序的效率。所以为了防止这种情况的发生C语言没有从语法上支持数据集合的直接赋值。

指针做函数返回值

C语言是允许函数的返回值类型是指针类型的,而这种函数也被称为指针函数。

#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){
    if(strlen(str1) >= strlen(str2)){
        return str1;
    }else{
        return str2;
    }
}
int main(){
    char str1[30], str2[30], *str;
    gets(str1);
    gets(str2);
    str = strlong(str1, str2);
    printf("Longer string: %s\n", str);
    return 0;
}

运行结果:

C Language↙
tianlangz.top↙
Longer string: tianlangz.top

用指针作为函数返回值要注意一点,函数运行结束后会销毁,函数内部定义的所有局部数据,函数返回时尽量不要指向这些数据,C语言没有任何机制保证这些数据会一直有效,在后续使用中,再调用它们可能会引发运行错误。例如:

#include <stdio.h>
int *fun(){
    int n = 100;
    return &n;
}
int main()
{
    int *p = fun(),n;
    n = *p;
    printf("value = %d\n",n);
    return 0;
}

运行结果: value = 100

n 是 func() 内部的局部变量,func() 返回了指向 n 的指针,根据上面的观点,func() 运行结束后 n 将被销毁,使用 *p 应该获取不到 n 的值。但是从运行结果来看,我们的推理好像是错误的,func() 运行结束后 *p 依然可以获取局部变量 n 的值,这个上面的观点不是相悖吗?

为了进一步看清问题的本质,不妨将上面的代码稍作修改,在第9~10行之间增加一个函数调用,看看会有什么效果:

#include <stdio.h>
int *func(){
    int n = 100;
    return &n;
}
int main(){
    int *p = func(), n;
    printf("tianlangz.top\n");
    n = *p;
    printf("value = %d\n", n);
    return 0;
}

运行结果:

c.biancheng.net
value = -2

可以看到,现在 p 指向的数据已经不是原来 n 的值了,它变成了一个毫无意义的甚至有些怪异的值。与前面的代码相比,该段代码仅仅是在 *p 之前增加了一个函数调用,这一细节的不同却导致运行结果有天壤之别,究竟是为什么呢?

前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分C语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

第一个例子在调用其他函数之前使用 *p 抢先获得了 n 的值并将它保存起来,第二个例子显然没有抓住机会,有其他函数被调用后才使用 *p 获取数据,这个时候已经晚了,内存已经被后来的函数覆盖了,而覆盖它的究竟是一份什么样的数据我们无从推断(一般是一个没有意义甚至有些怪异的值)。

二级指针

指针既然可以只想一份普通的数据类型,当然他也能够指向指针,而一个指针指向另一个指针我们就称它为二级指针。或者指向指针的指针。 举一个简单的例子就能明白:

#include <stdio.h>
int main(){
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
    printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
    printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
    printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
    return 0;
}

运行结果:

100, 100, 100, 100
&p2 = 0X28FF3C, p3 = 0X28FF3C
&p1 = 0X28FF40, p2 = 0X28FF40, *p3 = 0X28FF40
&a = 0X28FF44, p1 = 0X28FF44, *p2 = 0X28FF44, **p3 = 0X28FF44

每增加一级指针,就在定义指针变量前加一个*,它的定义关系是*(*(*p3))这样才能取到a的值。

指针数组

如果一个数组中的元素都是指针,则这个数组称为指针数组:

dataType *arrayName[length];

例如:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}

运行结果:

16, 932, 100
16, 932, 100

第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。

第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

我们还可以将指针数组和字符串数组连用:

#include <stdio.h>
int main(){
    char *str[3] = {
        "https://tianlangz.top",
        "tianlangz.top",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

也可以写成:

#include <stdio.h>
int main(){
    char *str0 = "https://tianlangz.top";
    char *str1 = "tinalangz.top";
    char *str2 = "C Language";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

他们两个的输出结果是相同的; 都是利用指针数组输出了字符串常量。 运行结果:

https://tianlangz.top
tianlangz.top
C Language

第一种方式将字符串常量直接赋值到数组中了,str数组存的也是字符串的首地址而不是整个字符串。第二种方式则是定义了三个字符串常量赋值到三个指针变量中,然后将指针变量存入指针数组中,而每个指针变量在不加*时表达的是这串字符串常量的首地址,所以第二个程序和第一个程序是等价的存入指针数组中的都是字符串的首地址。

二维数组指针

为了更好地了解二维数组指针我们先来定义一个数组a[4][4],我们定义一个指向a的指针变量,

int (*p)[4]=a

*p是一个指针,它指向一个数组,数组的类型时int [4]。这样就可以完整代替a这个数组了。因为[]的优先级要高于*,所以()是必须要加的,如果不加,就变成了一个指针数组。

那要如何用这个指针表示这个二维数组的每个元素:

  1. p指向数组a的开头,也即第0行;p+1前进一行,指向第1行。
  2. *(p+1)表示取地址上的数据,也就是取整个第一行的数据,是多个数据,不是一个。
  3. *(p+1)+1表示第一行第一个元素的地址
    • *(p+1)单独使用时表示的是第一行数据,放在表达式中会被转换为第一行的首地址,也就是第一行的第0个数据,因为使用整行数据没有实际含义,编译器遇到这种情况都会转换成为指向该行的第0个元素的指针;就像一维数组的名字,在定义时或者sizeof、&一起使用时才表示这个数组,出现在表达式中就会被转换为指向数组第0个元素。
  4. *(*(p+1)+1)表示第一行第一个元素,增加一个*取地址上的值。 举个例子:
#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j));
        printf("\n");
    }
    return 0;
}

运行结果:

 0   1   2   3
 4   5   6   7
 8   9  10  11

函数指针

returnType (*pointerName)(param list)

returnType函数返回值类型,pointerName指针名称,param list为函数参数列表。参数列表可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称。

函数指针第一个括号一定不能省略,省略了就成了函数原型,那它就表示返回值类型为指针类型了。

我们来用指针实现一次函数调用:

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

运行结果:

Input two numbers:10 50↙
Max value: 50

总结

定义 含义
int *p p可以指向int类型数据
int **p p为二级指针,指向int *类型数据
int *p[n] p为指针数组,[]中存放的是内存地址
int (*p)[n] p为二维数组指针
int *p() p是一个函数,它的返回值类型是int*
int (*p)() p是一个函数指针,指向原型为int型函数
  1. 指针变量可以进行加减运算,例如:p++、p+、p-=1。指针的加减法运算跟指针的类型有关。
  2. 给指针变量赋值时,应该赋值一个地址,不能直接赋整数。
  3. 使用指针变量时,一定要先给指针变量赋初值,不然不能确定指针指向了哪里,很容易导致程序崩溃,因为他要指向的内存他没有权限,对于暂时没有指向的指针,建议赋值NULL。
  4. 两个指针变量可以相减。如果两个指针同时指向一个数组中的不同元素,那么相间的结果就是两个指针之间相差的元素个数(别忘了数据类型的长度)。
  5. 数组也是有类型的,数组名的本意表示一组类型相同的数据。定义数组时,或者和sizeof 或 & 运算符一起使用时数组名才能表示整个数组,表达式中的数组名一般会表示指向数组的指针。

Posts in this Series