第九课

如果您注意到的话,从第一课开始,我们已经在编程的道路上走了很远。我们正在慢慢的完成 C 和 C++ 基础的学习。这一课里,我们将会详细讲述数组和指针。

数组:烟雾和幻像笼罩着的指针

当我们在第五课中首次接触到字符串时,我们了解到数组和指针的关系是如此的紧密,但是我们并没有仔细的探究它们究竟如何的紧密。数组是一种非常友好的使用指针的方式。让我们来了解两种不同的修改字符串的方式来加以证明。

#include <stdio.h>

int
main(void)
{
    char string[30];

    // 使用我们已知的方式为数组赋值为小写字母表:方括号

    for (int i = 0; i < 26; i++)
    {
            // 字符常量可以看作整型。它可以为我们省去检查
            // A 的小写字符所表示的整型值的必要。
            string[i] = 'a' + i;
    }
    string[26] = 0;
    printf("%s\n", string);

    // 下面是另一种方法:使用指针和一些数学表达式
    for (int i = 0; i < 26; i++)
    {
            char *string = string + (i * sizeof(char));

            // 使用大写字符 A 仅为了和上述相区别。
            *index = 'A' + i;
    }
    string[26] = 0;
    printf("%s\n", string);
}

上述两个循环完成了相同的内容,除了第二个中使用的为大写字符。第二个的代码看起来有些奇怪,那么让我们从 sizeof() 开始分开来看。sizeof() 是一个语言特性,看起来像一个函数。您可以给它一个变量,类型名,它将会返回其所包含的字节数量,即此种类型的变量在内存中占据的大小。例如,char 对象仅有 1 个字节,但是 int 型对象占据 4 个字节。传递给 sizeof() 一个数组,它将会返回整个数组在内存中占据的大小。由于 char 字符类型占据的大小为 1 ,我们原本可以跳过整个操作,但是养成这种避免猜测的习惯对程序员来说很有帮助。这样您可以少添些银发。

上述代码中另一个奇怪的地方是为指针加上数值。指针只是一个包含内存地址的变量,因此为其加上数字只是修改了内存地址。我们无法修改数组的地址,因此我们创建了一个指针以供修改。这可以允许我们为其加上数组元素的大小。因为我们通过循环来完成,指针将会存储数组中每个字符的地址。

数组索引 等价指针运算 字符值
string[0] index a
string[1] index + (size * 1) b
string[2] index + (size * 2) c
string[3] index + (size * 3) d

我们可以使用数组和指针之间的这种对应关系来脱掉它们神秘的外衣,如初始化整个 bool 数组为真。如何完成呢?memset(),它可是好久没显身手了。

int
MyFunc(void)
{
    bool bArray[100];
    memset(bArray, 1, sezeof(bool) * 100);
    return 0;
}

这个小函数只是为数组中的每个字节赋值为 1,也就是布尔逻辑中的真值。它不仅可以减少工作,它比使用循环来单独为每个元素赋值要快得多。

多维数组

我们能够创建除了字符串之外任何类型的数组,这确实不够方便。字符串存在于日常编程的任何地方,但是我们是否能够创建数组的数组呢?事实上,我们可以这么做。为了创建字符串的数组,我们必须征服 C 和 C++ 中最让人迷惑的地方之一:多维数组。您无需担心,我将尽可能讲解的简单些。

常规的数组最容易理解,它们只是存在于一列中的一系列元素而已。

int myArray[10];

myArray:

                   
0 1 2 3 4 5 6 7 8 9

数组可以有多个维度。 - 例如,二维数组可以认为是一个网格 - 行列网格。下面的数组具有两行数据,每行五个元素。

int my2DArray[2][5];

my2DArray:

         
0 1 2 3 4
5 6 7 8 9

理解多维数组最简单的方法就是将其比作空间维度,当我们添加另一维度的方括号时,仅需从右至左在添加一个维度。一个维度是一行,两个维度是长方形网格。超过三个维度时,我们可以认为其是立方体组或者立方组的组。声明和获取多维数组仅需在声明和获取时添加方括号即可。

维度 数组声明
1 my1DArray [elementCount]
2 my2DArray [numRows] [itemsInRow]
3 my3DArray [numGrids] [rowsInGrid] [itemsInRow]
4 my4DArray [numCubes] [numGrids] [numRows] [itemsInRow]
5 my5DArray [numCubeGrops] [cubesInGroup] [gridsInCube] [rowsInGrid] [elementsInRow]

既然我们已经了解了如何声明多维数组,那么让我们在下面的代码中实际的加以运用。

#include <stdio.h>
#include <string.h>

int
main(void)
{
    // 声明并初始化数组为 4 行 5 列元素 -
    // 即 4 个元素高,5 个元素宽的长方形。
    int integerArray[4][5];

    int value = 0;
    for (int y = 0; y < 4; y++)
    {
            for (int x = 0; x < 5; x++)
                    integerArray[y][x] = value++;
    }
    return 0;
}

该代码片段声明了一个数组,并使用循环对其进行了初始化。由于整个数组的内存分配为一个大的区块,我们可以使用 memset() 和 sizeof() 设置数组中的每个字节为相同的值。这也就意味着,我们可以使用一个指向同一地址的指针将二维数组视为一个长长的完整队列。

int
main(void)
{
    // 声明并初始化一个 4 行 10 列的整型数组。
    int intArray[4][10];

    for (int y = 0; y < 4; y++)
    {
            for (int x = 0; x < 10; x++)
                    intArray[y][x] = (y * 10) + x;
    }

    // 虽然其声明为一个二维数组,但是有时,可以将其
    // 视为一个具有 40 个元素的长队列。这只是一种不同
    // 的对待相似数据的方式。由于这是一个二维数组,
    // intArray 自身的类型为 int ** .. 一个指向指针的
    // 整型指针。为其添加一个星号,使其成为 int * 。
    int *pInt = *intArray;
    for (int i = 0; i < 40; i++)
            printf("%d\n", pInt[i]);
}

虽然我们可以使用 memset() 或者循环初始化所有的数组,我们还能够 - 并且有时候需要 - 使用许多自定义的值。这可以通过在大括号中使用逗号隔开的值列表来完成。每个维度都需要一个大括号。下面是使用不同于循环的方式来初始化数组的代码示例。

int
main(void)
{
    // 声明并初始化 4 行 10 列的数组。
    int intArray[4][10] = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
                            { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
                            { 20, 21, 22, 23, 24, 25, 26, 27, 28, 29};
                            { 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}}      c'
    int *pInt = *intArray;
    for (int i = 0; i < 40; i++)
            printf("%d\n", pInt[i]);
}

这样需要很多输入,但是如果我们有一列不同的值,它们不遵循一定的规律,这可能就是我们唯一的选择了。这种方法不好的一面就是我们需要为所有的元素赋值 - 我们无法挑选哪个进行赋值。同时多于两维的数组阅读起来非常不便。如果是一个普通的数组,我们只需要一对大括号以及一列数值即可,如下:

float someArray[3] = { 1.1, 2.2, 3.3 };

这也是仅有的可以在大括号之外添加分号的几个例子之一。

为了创建字符串列表,我们只需要创建一个二维 char 数组。虽然我们可以使用一系列逗号隔开的字符常量,C 和 C++ 为我们提供了许多快捷方式来省去输入和初始化字符串时计数的麻烦。

// 这是一种麻烦的方法。很麻烦!
char myShortString[15] = { 'a', 'b', 'c', 'd', 'e', '\0'};

// 声明一个可以保存包含 15 个字符的字符串的数组,包括 NULL
// 终止符。这种方法比使用字符常量和大括号要方便的多。
char myFastString[15] = "abcde";

// 将数组大小置为空,让编译器为字符串分配足够的大小。
// 它将会节省调用 strlen() 或者计算大小的过程。
char myLongString[] = "This is some really long string I don't have to count.";

// 如果我们初始化了一位数组,我们也可以将其他类型
// 的一维数组大小置为空。
int myIntArray[] = { 0, 1, 2, 3, 4, 5 };

// 下面的方式和 myLongString 的声明具有相同的结果。这也是
// 一种常用的方式。
char *anotherLongString = "This is some other really long string.";

一个小提示:不要将多维数组的大小置为空。在声明多维数组时,仅可以将最左端的维度大小置为空,将两者混为一谈将产生疑惑。请相信我。

再次的,在这么短的时间里,我们介绍了很多的内容,让我们再来复习一遍:

  • 可以从指针保存的地址中减去或者加上一个整数。
  • sizeof() 返回类型,变量或者数组的字节数目。
  • 您可以使用指针运算来获取数组中元素的值。
  • 数组的内存分配是连续的。
  • 可以声明多维数组,并通过指针将其作为长的一位数组来访问和解释。
  • char 数组(字符串)可以使用常规字符串进行初始化。
  • 非字符串数组可以使用大括号进行初始化。
  • 数组中的所有元素在初始化时必须给定一个值。
  • 如果您仅需要足够的内存来保存给定数据,初始化一维数组时可以将方括号中的大小置为空。

第八课错误查找答案

  1. 其问题在于,参数字符串为 const char * 而不是 char * 。这也就意味赋值给 ReverseString() 的字符串无法修改,因此,当我们使用循环对其进行修改时,编译器将会报错。删除 const 关键字,将会皆大欢喜。