首页 > 技术文章 > C语言的核心--指针补充

EXQSLoveForever 2021-08-23 17:14 原文

指针变量作为函数参数

在C语言体系中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。我们使用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。从而实现函数之间数据的传递。同时类似于数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

这里我们举一个例子来进行论证,也是书中经常使用的一个例子:

#include <stdio.h>

void swap(int a, int b)
{
    int temp;  //临时变量
    temp = a;
    a = b;
    b = temp;
}


int main()
{
    int a = 1, b = 2;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

输出结果:

a = 1,b = 2

我们发现数据并没有交换:

原因是swap() 函数内部的 a、b 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,它们除了名字一样,没有其他任何关系,swap() 交换的是它内部 a、b 的值,不会影响它外部(main() 内部) a、b 的值。

如果我们使用指针变量进行传递参数的操作:

#include <stdio.h>

void swap(int *p1, int *p2)
{
    int temp;  //临时变量
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main()
{
    int a = 1, b = 2;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

输出结果:

a = 2,b = 1

我们可以发现交换成功,原因在于调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 p1、p2 代表的就是变量 a、b 本身,交换 p1、p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”

在这里我们可以了解到用指针传递参数的奥妙,那么我们继续进一步深入,我们看一下如果我们用指针传递数组会怎?

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。

栗子:查找数组中值最大的元素

#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("%d\n", max(nums, len));
    return 0;
}


运行结果:

输入:1,2,3,4,5,6
输出:6

参数 intArr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址,nums+i就是第 i 个数组元素的地址。

这里需要注意的一点:

不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

这一点非常重要。


我们在来深入了解一下为什么不直接传递数组的所有元素,而必须传递数组指针呢?

正如参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。

当然函数还可以作为返回值,这里我们不再具体阐述。

指针数组

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。

指针数组的定义形式一般为:
dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为:
dataType *(arrayName[length]);

这里要注意和数组指针相区别。
括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的。

给出一个具体例子,供大家了解:

#include <stdio.h>

int main(){
    int a = 1, b = 2, c = 3;
    //定义一个指针数组

    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;
}

输出结果:

1,2,3
1,2,3

分析:
arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int (parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。

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

二维数组指针

二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。

例如:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

其实a数组的分布更像是一个矩阵:

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

C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。
C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

由于数组在内存中为线性排列,这就为指针的使用提供了条件。

例如,我们定义一个指针变量p:

int (*p)[4] = a;

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],是 a 所包含的每个一维数组的类型。
[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。

我们看一下如何用指针 p 来访问二维数组中的每个元素:

  1. p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。

  2. *(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素

  3. *(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢?

*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

  1. ((p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。

由此我们得到如下公式,我们称之为万能公式:

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

推荐阅读