数组与指针

数组与指针的关系

什么是数组?什么是指针?

数组是相同类型的对象集合,可以用一个名称引用。

例如:

int scores[50];Code language: CSS (css)

它可以包含 50 场篮球赛的比分。

使用不同的索引值可以引用数组中的每个元素。scores[0] 是第一个分数,scores[49] 是最后一个分数。

如果每个月有 10 场比赛,就可以用多维数组:

scores[12][10];Code language: CSS (css)

如果一月开始比赛,则五月的第三场比赛用:

scores[4][2];Code language: CSS (css)

指针是一个变量,它的值是给定类型的另一个变量或常量的地址。

使用指针可以在不同的时间访问不同的变量,只要它们的类型相同即可。

数组和指针似乎完全不同,但它们有非常密切的关系,有时还可以互换。

下面看一个例子:

字符串是 char 类型的数组。如果用 scanf 输入一个字符:

char single = 0;
scanf("%c", &single);Code language: JavaScript (javascript)

具体代码:

#include <stdio.h>

int main(void)
{
    char single = 0;
    scanf("%c", &single);
    printf("%c\n", single);
    return 0;
}Code language: PHP (php)

在这里 scanf 需要用到取地址符 &,用于 single。因为 scanf 需要用到存储的变量的地址。

然而,如果读入的是字符串,可以这样:

#include <stdio.h>

int main(void)
{
    char multiple[10];
    scanf("%s", &multiple);
    printf("%s\n", multiple);
    return 0;
}Code language: PHP (php)

这里不需要使用 & 运算符,而使用数组名称,就像指针一样。

#include <stdio.h>

int main(void)
{
    char multiple[10];
    scanf("%s", multiple); //可以不需要取地址符 数组名本身就是地址
    printf("%s\n", multiple);
    return 0;
}Code language: PHP (php)

如果以这种方式使用数组名称,而没有带索引值,它就引用数组的第一个元素的地址。

但数组不是指针,它们有一个重要的区别:可以改变指针包含的地址,但不能改变数组名称引用的地址。

指针算术与数组类型

我们前面知道 char 类型是占一个字节。

所以当我们使用指针 p + 1 就去到下一个字节,下一个字节就读取下一个字节的字符。

#include <stdio.h>

int main(void)
{
    char multiple[10];
    scanf("%s", multiple); //可以不需要取地址符 数组名本身就是地址
    char *p = multiple; //&multiple[0]
    printf("%c\n", *p);
    printf("%c\n", *(p+1));
    return 0;
}Code language: PHP (php)

但是如果是其他类型,比如 long 类型不是占用一个字节,而是四个字节。

这时候,如果我们要读取下个元素,是否是 p+4?还是?

大家思考一下这个问题。

下面看这个例子:

#include <stdio.h>

int main(void)
{
    long multiple[] = {15L, 25L, 35L, 45L};
    long * p = multiple;

    for(int i = 0; i < sizeof(multiple)/sizeof(multiple[0]); i++)
    {
        printf("\n地址 p+%d (&multiple[%d]): %d   *(p+%d) 值: %d",
               i, i, p+i, i, *(p+i));
    }

    printf("\n    long类型占: %d bytes\n", sizeof(long));
    return 0;
}Code language: PHP (php)

上面代码,p 是 6422016,p+1 本来我们觉得应该是 6422017,但是它实际上是 6422020。

所以 p+1 比 p 是大了 4,而不是 1。

这里是编译器帮我们来计算,编译器根据指针类型 long,然后决定了是 4。

就是因为我们的指针是 long 类型指针 long * p,所以当我们 p+1 的时候,编译器知道是 long 类型,然后知道它 4 个字节,所以虽然是 p+1,但是编译器自动跳转四个字节。

多维数组与指针

前面我们讨论的是一维数组与指针的关系,二维或者多维数组是否相同?

其实某种程度上是相同的。

考虑之前的井字游戏中用到的数组:

char board[3][3] = {
    {'1','2','3'},
    {'4','5','6'},
    {'7','8','9'}
};Code language: JavaScript (javascript)

下面使用该数组来探讨一下:

#include <stdio.h>

int main(void)
{
    char board[3][3] = {
        {'1','2','3'},
        {'4','5','6'},
        {'7','8','9'}
    };

    printf("address of board        : %p\n", board);
    printf("address of board[0][0]  : %p\n", &board[0][0]);
    printf("but what is in board[0] : %p\n", board[0]);
    return 0;
}Code language: PHP (php)

我们可以看到,以上三个值是相同的。

声明一维数组 x[n1] 时,它告诉编译器它是一个有 n1 个元素的数组。

声明二维数组 y[n1][n2],编译器就会创建一个大小为 n1 的数组,它的每个元素是一个大小为 n2 的数组。

声明二维数组时,就是创建一个数组的数组。

因此使用数组名+一个索引值访问这个二维数组时,例如 board[0] 就是引用一个子数组的地址。

仅使用二维数组名时,就是引用二维数组的开始地址。它也是第一个子数组的开始地址。

#include <stdio.h>

int main(void)
{
    char board[3][3] = {
        {'1','2','3'},
        {'4','5','6'},
        {'7','8','9'}
    };

    printf("value of board[0][0] : %c\n", board[0][0]);
    printf("value of *board[0]   : %c\n", *board[0]);
    printf("value of **board     : %c\n", **board);
    return 0;
}Code language: PHP (php)

使用 board 获取子数组的第一个元素需要两个间接运算符 **board。

如果只使用一个 *,只会得到子数组的第一个元素,即 board[0] 引用的地址。

也就是 *board 得到的是 board[0] 的地址。

#include <stdio.h>

int main(void)
{
    char board[3][3] =
    {
        {'1','2','3'}, //board[0] char b[] = {'1','2','3'}
        {'4','5','6'}, //board[1] char b1[] = {'4','5','6'}
        {'7','8','9'}  //board[2] char b2[] = {'7','8','9'}
    };
    //b 就是1的地址  b在这里 等同于 board[0]
    //在一维数组里面 b[0]是1 *b 也是1  *(b+1) 就是b[1]
    //把b替换为board[0]  *board[0] *(board[0]+1)
    printf("address of board        : %p\n", board);
    printf("address of board[0][0]  : %p\n", &board[0][0]);
    printf("but what is in board[0] : %p\n", board[0]);

    printf("value of board[0][0] : %c\n", board[0][0]);
    printf("value of *board[0]   : %c\n", *board[0]);
    printf("value of **board     : %c\n", **board);
    return 0;
}Code language: PHP (php)

下面例子用 for 循环进一步改进前一个例子:

#include <stdio.h>

int main(void)
{
    char board[3][3] = {
        {'1','2','3'},
        {'4','5','6'},
        {'7','8','9'}
    };

    /* List all elements of the array */
    for(int i = 0; i < 9; i++)
        printf(" board: %c\n", *(*board + i));
    return 0;
}Code language: PHP (php)

程序的输出如下:

可以看到,使用表达式 *(*board + i) 可以得到一个数组元素的值。

括号中的表达式 *board + i 会得到数组中偏移量为 i 的元素的地址。

board 其实是 char** 类型的地址值,取消对 board 的引用,也就是 *board,也就是 char*,也就是它还是一个指针,然后指针 +n 也就是获取后面的元素。比如 *board + 2 获取第三个元素的地址。

如果去掉括号,也就是:

printf("board:%c\n", **board + i);Code language: JavaScript (javascript)

则会输出下面:

虽然好像是一模一样,但是如果修改为如下:

#include <stdio.h>

int main(void)
{
    char board[3][3] = {
        {'9','8','7'},
        {'6','5','4'},
        {'3','2','1'}
    };

    /* List all elements of the array */
    for(int i = 0; i < 9; i++)
        printf(" board: %c\n", **board + i);
    return 0;
}Code language: PHP (php)

以上代码 **board 就是 '9',然后 '9' + 1 就是 ascii 表中的下一个字符。

如果明白的同学可以回去复习一下 char 类型。

另外,如果表达式写成 **(board + 1) 这时候可能会导致错误。

因为 **(board + 0) 指向 board[0][0],而 **(board + 1) 指向 board[1][0],**(board + 2) 指向 board[2][0]。

为什么 board + 1 是 board[1][0] 而不是 board[0][1]?

原因是 board 是 char** 类型,它 +1 也就是一维数组 char[] 移动 1,而不是一个元素 char 移动 1。

#include <stdio.h>

int main(void)
{
    char board[3][3] = {
        {'9','8','7'},
        {'6','5','4'},
        {'3','2','1'}
    };

    /* List all elements of the array */
    for(int i = 0; i < 9; i++)
        printf(" board: %c\n", **(board + i));
    return 0;
}Code language: PHP (php)

三种写法记住:

  • *(*board + i):*board 本身就是一个 char 类型指针 char*,char 类型刚好占一个字节,+i 就自动跳到下一个元素
  • **board + i:这个很好理解,**board 就是 '9',+1 就跳到下一个 ascii 码了
  • **(board + i):board 本身是一个 char** 类型,也就是一维数组的指针,+i 就是跳过一个一维数组的跨度

*board 就是一个 char* 类型:

char *p = *board;
*(p+1)
*(p+2)
*(p+3)
*(p+4)

这里就回到前面章节的内容了。

前面用指针的表示法用数组名称引用二维数组,现在学习使用声明为指针的变量。

下面的例子使用多维数组和指针:

指针+一个数组,看这个指针是什么类型,如果是 char 就是跳过一个字节,如果是 long 就是四个字节。

#include <stdio.h>

int main(void)
{
    char board[3][3] = {
        {'1','2','3'},
        {'4','5','6'},
        {'7','8','9'}
    };

    char *pboard = *board;             /* A pointer to char */

    for(int i = 0; i < 9; i++)
        printf(" board: %c\n", *(pboard + i));  //pboard 是一个char*指针 +1 就是跳过一个字节
    return 0;
}Code language: PHP (php)

这里使用数组中第一个元素的地址初始化指针,然后用一般的指针算术运算遍历整个数组。

board 是 char** 类型,也就是指针的指针,是子数组 board[0] 的地址,board[0] 本身是一个一维数组。

所以当我们 board + i 就是跳过一个一维数组的字节长度。

**(board + i) 而不是第一个元素的地址。它必须是 char* 类型。

也可以用以下方式来初始化 pboard:

char *pboard = &board[0][0];

如果下面的写法则会报错,编译器可能会警告:

pboard = board;

因为 board 是两个 *,而 pboard 是一个 *。

可以使用下面几种方式访问二维数组的元素:

char board[3][3] = {
    {'9','8','7'},
    {'6','5','4'},
    {'3','2','1'}
};Code language: JavaScript (javascript)

board[0] 就是 {'9','8','7'},是一个一维数组。

假如 char b3[] = {'9','8','7'},这时候 board[0] 就是 b3。

以前 char* p = b3;,这里 p 就是 board[0]。

如果要访问 '8',*(b3+1)。

所以 board[0] 就是二维数组里面的第一个一维数组的第一个元素的地址。

同样 board[1] 就是第二个一维数组的第一个元素的地址了,也就 {'6','5','4'} 的 6 的地址。

然后 board[2] 就是 3 的地址。

动态内存分配

指针是一个非常灵活且强大的编程工具,有非常广泛的应用。

大多数 C 程序都在某种程度上使用了指针。

C 语言还有一个功能:动态内存分配,它依赖指针的概念,为在代码中使用指针提供了很强的刺激机制,它允许在执行程序时动态分配内存。

只有使用指针,才能动态分配内存。

大多数产品程序都使用了动态内存分配。例如电子邮件客户端在检索电子邮件时,事先并不知道有多少封电子邮件,也不知道每封邮件需要多少内存。电子邮件客户端在运行期间会得到足够的内存,来管理电子邮件的数量和大小。

前面我们见过一个程序计算一组学生的平均分,当时它处理 10 个学生。

理想情况下,该程序应能处理任意多个学生,但事先不知道要处理多少个学生,所使用的内存也不会比指定的学生分数所需的内存多。

动态内存分配就可以实现这个功能,可以在执行时创建足够以容纳所需数据量的数组。

在程序的执行期间分配内存时,内存区域中的这个空间称为堆(heap)。

还有另一个内存区域,称为栈(stack),其中的内存分配给函数的参数和本地变量。在执行完该函数后,存储参数和本地变量的内存空间就会释放。堆中的内存是由程序员控制的。

如本章后面所述,在分配堆上的内存时,由程序员跟踪所分配的内存何时不再需要,并释放这些空间,以便可以重用它们。

前面第五章,可以利用变量指定数组的维度,在运行期间创建数组。

也可以在运行期间明确分配内存。

在运行期间分配内存的最简单的标准函数是 malloc 函数。

使用这个函数需要在程序中包含头文件 stdlib.h。malloc 函数需要指定要分配的内存字节数作为参数。

这个函数返回所分配内存的第一个字节的地址。所以需要使用指针来存储该函数的返回值。

例子:

int *pNumber = (int*)malloc(100);

这条语句请求 100 个字节的内存,并把这个内存块的地址赋予 pNumber。只要不修改它,任何时间使用这个变量 pNumber 它都会指向所分配的 100 个字节的第一个 int 的位置。这个内存块能够保持 25 个 int 值,每个 int 占 4 个字节。

上面语句 100 是假定 int 占 4 个字节。但是我们最好这样写,因为不是所有的系统 int 都是 4 个字节。

int *pNumber = (int*)malloc(25 * sizeof(int));

这样就保证可以存储 25 个 int 值了。

这里需要 int* 指针,所以需要强制类型转换。因为 malloc 可以为任何类型的数据分配内存。

这个函数本身不知道是什么类型,所以它返回一个 void 类型的指针,写成 void*。

类型 void* 可以指向任何类型的数据。然而不能去掉对 void 指针的引用,因为它指向未具体说明的对象。

可以请求任意数量的字节,字节数受制于计算机未用的内存以及 malloc 的运用场合。如果因某种原因而不能分配请求的内存,malloc 会返回一个 NULL 指针。这个指针等于 0。

所以我们最好先检查是否已分配,再使用它。

int *pNumber = (int*)malloc(25 * sizeof(int));
if(!pNumber)
{
    //其它操作
}Code language: JavaScript (javascript)

在动态分配内存时,应该总是在不需要该内存时释放它们。堆上分配的内存会在程序结束时自动释放,但最好在使用完这些内存后立即释放,甚至是在退出程序之前,也应立即释放。在比较复杂的情况下,很容易出现内存泄漏。当动态分配了一些内存时,没有保留对它们的引用,就会出现内存泄漏,此时无法释放内存。这常常发生在循环内部,由于没有释放不再需要的内存,程序会在每次循环迭代时使用越来越多的内存,最终占用所有内存。

当然,要释放动态分配的内存,必须能访问引用内存块的地址。要释放动态分配的内存,而该内存的地址存储在 pNumber 指针中,可以使用下面的语句:

free(pNumber);
pNumber = NULL;Code language: PHP (php)

free() 函数的形参是 void* 类型,所有指针类型都可以自动转换为这个类型,所以可以把任意类型的指针作为参数传送给这个函数。只要 pNumber 包含分配内存时返回的地址,就会释放所分配的整个内存块,以备以后使用。在指针指向的内存释放后,应总是把指针设置为 NULL。

警告:在释放指针指向的堆内存时,必须确保它不被另一个地址覆盖。

如果给 free() 函数传送一个空指针,该函数就什么也不做。应避免两次释放相同的内存区域,因为在这种情况下,free() 函数的操作是不确定的,因此也就无法预料。如果多个指针变量引用已分配的内存,就有可能两次释放相同的内存,所以要特别小心。

试试看:动态内存分配

下面使用指针来计算质数,将动态内存分配的概念应用于实践。质数是只能被 1 和这个数本身整除的整数。

查找质数的过程非常简单。首先,由观察得知,2、3 和 5 是前三个质数,因为它们不能被除了 1 以外更小的数整除。其他质数必定都是奇数(否则它们可以被 2 整除),所以要找出下一个质数,可以从最后一个质数开始,给它加 2。检查完这个数后,再给它加 2,继续检查。

检查一个数是否为质数,而不只是奇数,可以用这个数除以比它小的所有奇数。其实不需要这么麻烦。如果一个数不是质数,它必定能被比它小的质数整除。我们要按顺序查找质数,所以可以把已经找到的质数作为除数,确定所检查的数是否为质数。

这个程序将使用指针和动态内存分配:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main(void)
{
    unsigned long *primes = NULL;       /* Pointer to primes storage area    */
    unsigned long trial = 0;            /* Integer to be tested              */

    bool found = false;                 /* Indicates when we find a prime    */
    size_t total = 0;                   /* Number of primes required         */
    size_t count = 0;                   /* Number of primes found            */

    printf("How many primes would you like - you'll get at least 4?  ");
    scanf("%u", &total);                /* Total is how many we need to find */
    total = total < 4U ? 4U : total;    /* Make sure it is at least 4        */

    /* Allocate sufficient memory to store the number of primes required */
    primes = (unsigned long *)malloc(total * sizeof(unsigned long));
    if(primes == NULL)
    {
        printf("\nNot enough memory. Hasta la Vista, baby.\n");
        return 1;
    }

    /* We know the first three primes     */
    /* so let's give the program a start. */
    *primes = 2UL;                      /* First prime                   */
    *(primes+1) = 3UL;                  /* Second prime                  */
    *(primes+2) = 5UL;                  /* Third prime                   */
    count = 3U;                         /* Number of primes stored       */
    trial = 5U;                         /* Set to the last prime we have */

    /* Find all the primes required */
    while(count < total)
    {
        trial += 2UL;                     /* Next value for checking       */

        /* Try dividing by each of the primes we have       */
        /* If any divide exactly - the number is not prime  */
        for(size_t i = 0; i < count; i++)
            if(!(found = (trial % *(primes+i))))
                break;                        /* Exit if no remainder          */

        if(found)                        /* we got one - if found is true */
            *(primes+count++) = trial;     /* Store it and increment count  */
    }

    /* Display primes 5-up */
    for(size_t i = 0; i < total; i++)
    {
        if(!(i % 5U))
            printf("\n");                   /* Newline after every 5         */
        printf("%12lu", *(primes+i));
    }
    printf("\n");                       /* Newline for any stragglers    */
    return 0;
}Code language: PHP (php)

在 <stdlib.h> 头文件中声明的 calloc() 函数与 malloc() 函数相比有两个优点。

第一,它把内存分配为给定大小的数组。

第二,它初始化了所分配的内存,所有的位都是 0。

函数需要两个参数:数组的元素个数和数组元素占用的字节数,这两个参数的类型都是 size_t。

该函数也不知道数组元素的类型,所以所分配区域的地址返回为 void* 类型。

下面的语句使用 calloc() 为包含 75 个 int 元素的数组分配内存:

int *pNumber = (int*)calloc(75, sizeof(int));

如果不能分配所请求的内存,返回值是 NULL,也可以检查分配内存的结果,这非常类似于 malloc(),但 calloc() 分配的内存区域都会初始化为 0。当然,可以让编译器执行类型转换:

int *pNumber = calloc(75, sizeof(int));

后面的代码省略了这个类型转换。

将上面的程序改为使用 calloc() 代替 malloc() 来分配需要的内存,只需要修改一条语句。其他代码不变:

primes = calloc((size_t)total, sizeof(unsigned long));
if (primes == NULL)
{
    printf("Not enough memory. It's the end I'm afraid.\n");
    return 1;
}Code language: PHP (php)

Previous:
Next:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注