数组是一种 C 构造,它创建相同类型的数据元素的有序集合,并将该集合与单个程序变量相关联。有序意味着每个元素都位于值集合中的特定位置(即,位置 0、位置 1 等都有一个元素),而不是值必然已排序。数组是 C 的主要机制之一,用于对多个数据值进行分组并通过单个名称引用它们。数组有多种形式,但基本形式是一维数组,这对于在 C 中实现类似列表的数据结构和字符串非常有用。

1.5.1. 数组简介

C 数组可以存储多个相同类型的数据值。在本章中,我们讨论静态声明的数组,这意味着总容量(数组中可以存储的最大元素数)是固定的,并在声明数组变量时定义。在下一章中,我们将讨论动态分配的数组和多维数组。

下面提供了一个程序的 Python 和 C 版本,该程序初始化并打印整数值的集合。 Python 版本使用其内置列表类型来存储值列表,而 C 版本使用 int 类型的数组来存储值的集合。

一般来说,Python 为程序员提供了一个高级列表接口,隐藏了许多低级实现细节。另一方面,C 向程序员开放了低级数组实现,并将其留给程序员来实现更高级别的功能。换句话说,数组支持低级数据存储,而没有高级列表功能,例如 len、append、insert 等。

# An example Python program using a list.

def main():
    # create an empty list
    my_lst = []

    # add 10 integers to the list
    for i in range(10):
        my_lst.append(i)

    # set value at position 3 to 100
    my_lst[3] = 100

    # print the number of list items
    print("list %d items:" % len(my_lst))

    # print each element of the list
    for i in range(10):
        print("%d" % my_lst[i])

# call the main function:
main()
/* An example C program using an array. */
#include <stdio.h>

int main(void) {
    int i, size = 0;

    // declare array of 10 ints
    int my_arr[10];

    // set the value of each array element
    for (i = 0; i < 10; i++) {
        my_arr[i] = i;
        size++;
    }

    // set value at position 3 to 100
    my_arr[3] = 100;

    // print the number of array elements
    printf("array of %d items:\n", size);

    // print each element of the array
    for (i = 0; i < 10; i++) {
        printf("%d\n", my_arr[i]);
    }

    return 0;
}

该程序的 C 和 Python 版本有几个相似之处,最值得注意的是,可以通过索引访问各个元素,并且索引值从 0 开始。也就是说两种语言都将集合中的第一个元素称为位置处0的元素。

该程序的 C 和 Python 版本的主要区别在于列表或数组的容量以及它们的大小(元素数量)的确定方式。

python 列表:

my_lst[3] = 100   # Python syntax to set the element in position 3 to 100.

my_lst[0] = 5     # Python syntax to set the first element to 5.

C 数组:

my_arr[3] = 100;  // C syntax to set the element in position 3 to 100.

my_arr[0] = 5;    // C syntax to set the first element to 5.

在Python版本中,程序员不需要提前指定列表的容量:Python会根据程序的需要自动增加列表的容量。例如,Python 的append函数会自动增加 P​​ython 列表的大小,并将传递的值添加到末尾。

相反,在 C 语言中声明数组变量时,程序员必须指定其类型(数组中存储的每个值的类型)及其总容量(存储位置的最大数量)。例如:

int  arr[10];  // declare an array of 10 ints

char str[20];  // declare an array of 20 chars

前面的声明创建了一个名为 arr 的变量(总容量为 10 的 int 值数组)和另一个名为 str 的变量(总容量为 20 的 char 值数组)。

为了计算列表的大小(大小表示列表中值的总数),Python 提供了一个 len 函数,该函数返回传递给它的任何列表的大小。在 C 中,程序员必须显式跟踪数组中的元素数量(例如示例1 中的size变量)。

通过查看该程序的 Python 和 C 版本可能不太明显的另一个区别是 Python 列表和 C 数组在内存中的存储方式。 C 规定了程序内存中的数组布局,而 Python 向程序员隐藏了列表的实现方式。在 C 语言中,各个数组元素被分配在程序内存中的连续位置。例如,第三数组位置在存储器中位于紧接着第二数组位置并且紧接着第四数组位置之前。

1.5.2. 数组访问方法

Python 提供了多种方法来访问其列表中的元素。然而,如前所述,C 仅支持索引。有效索引值的范围是从 0 到数组容量减 1。以下是一些示例:

int i, num;
int arr[10];  // declare an array of ints, with a capacity of 10

num = 6;      // keep track of how many elements of arr are used

// initialize first 5 elements of arr (at indices 0-4)
for (i=0; i < 5; i++) {
    arr[i] = i * 2;
}

arr[5] = 100; // assign the element at index 5 the value 100

此示例声明容量为 10 的数组(它有 10 个元素),但它只使用前 6 个元素(我们当前的值集合大小为 6,而不是 10)。使用静态声明的数组时,通常会出现数组的某些容量未使用的情况。因此,我们需要另一个程序变量来跟踪数组(本例中为 num)的实际大小(元素数量)。

当程序尝试访问无效索引时,Python 和 C 的错误处理方法有所不同。如果使用无效的索引值访问列表中的元素(例如,索引超出列表中的元素数量),Python 将引发 IndexError 异常。在 C 中,程序员需要确保他们的代码在索引数组时仅使用有效的索引值。因此,对于像下面这样访问超出分配数组范围的数组元素的代码,程序的运行时行为是未定义的:

int array[10];   // an array of size 10 has valid indices 0 through 9

array[10] = 100;  // 10 is not a valid index into the array

C 编译器很乐意编译访问超出数组范围的数组位置的代码;编译器或运行时没有边界检查。因此,运行此代码可能会导致意外的程序行为(并且每次运行的行为可能有所不同)。它可能会导致您的程序崩溃,它可能会更改另一个变量的值,或者可能对您的程序的行为没有影响。换句话说,这种情况会导致程序错误,该错误可能会或可能不会显示为意外的程序行为。因此,作为一名 C 程序员,您需要确保您的数组访问引用有效的位置!

1.5.3. 数组和函数

在 C 中将数组传递给函数的语义类似于在 Python 中将列表传递给函数的语义:函数可以更改传递的数组或列表中的元素。下面是一个带有两个参数的示例函数:一个 int 数组参数 (arr) 和一个 int 参数(size):

void print_array(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d\n", arr[i]);
    }
}

参数名后面的[]告诉编译器参数arr的类型是int数组,而不是像参数size那样int。在下一章中,我们将展示指定数组参数的替代语法。数组参数 arr 的容量未指定:arr[]表示可以使用任意容量的数组参数调用该函数。由于无法仅从数组变量获取数组的大小或容量,因此传递数组的函数几乎总是还有第二个参数来指定数组的大小(上例中的 size 参数)。

要调用带有数组参数的函数,需要将数组名称作为参数传递。下面是一个 C 代码片段,其中包含对 print_array 函数的调用示例:

int some[5], more[10], i;

for (i = 0; i < 5; i++) {  // initialize the first 5 elements of both arrays
    some[i] = i * i;
    more[i] = some[i];
}

for (i = 5; i < 10; i++) { // initialize the last 5 elements of "more" array
    more[i] = more[i-1] + more[i-2];
}

print_array(some, 5);    // prints all 5 values of "some"
print_array(more, 10);   // prints all 10 values of "more"
print_array(more, 8);    // prints just the first 8 values of "more"

在C中,数组变量的名称相当于数组的基地址(即其第0个元素的内存位置)。由于 C 的按值传递函数调用语义,当您将数组传递给函数时,数组的每个元素不会单独传递给函数。换句话说,该函数没有接收每个数组元素的副本。相反,数组参数获取数组基地址的值。此行为意味着当函数修改作为参数传递的数组的元素时,更改将在函数返回时保留。例如,考虑以下 C 程序片段:

void test(int a[], int size) {
    if (size > 3) {
        a[3] = 8;
    }
    size = 2; // changing parameter does NOT change argument
}

int main(void) {
    int arr[5], n = 5, i;

    for (i = 0; i < n; i++) {
        arr[i] = i;
    }

    printf("%d %d", arr[3], n);  // prints: 3 5

    test(arr, n);
    printf("%d %d", arr[3], n);  // prints: 8 5

    return 0;
}

main 中对test函数的调用传递了参数 arr,其值是 arr 数组在内存中的基地址。test函数中的参数 a 获取该基地址值的副本。换句话说,参数 a 引用与参数 arr 相同的数组存储位置。因此,当test函数更改 a 数组中存储的值 (a[3] = 8) 时,它会影响参数数组中的相应位置(arr[3] 现在是 8)。原因是a的值是arr的基地址,而arr的值是arr的基地址,所以a和arr都引用同一个数组(内存中相同的存储位置)!图 1 显示了测试函数返回之前的执行点的堆栈内容。

A stack with two frames: main at the bottom and test on the top. main has two variables, an integer n (5) and an array storing values 0, 1, 2, 8, and 4.  Test also has two values, an integer size (2) and an array parameter arr that stores the base memory address of the array in main’s stack frame.

Figure 1. The stack contents for a function with an array parameter

参数 a 传递的是数组参数 arr 的基地址值,这意味着它们都引用内存中同一组数组存储位置。我们用从 a 到 arr 的箭头来指示这一点。通过test函数修改的值会突出显示。改变参数size的值不会改变其对应的参数n的值,但是改变a引用的其中一个元素的值(例如a[3] = 8)确实会影响对应位置已编好的值。

1.5.4. 字符串和 C 字符串库简介

Python 实现了字符串类型,并提供了丰富的使用字符串的接口,但 C 中没有相应的字符串类型。相反,字符串被实现为 char 值的数组。并非每个字符数组都用作 C 字符串,但每个 C 字符串都是一个字符数组。

回想一下,C 中的数组定义的大小可能比程序最终使用的大小更大。例如,我们在前面的“数组访问方法”部分中看到,我们可以声明一个大小为 10 的数组,但只使用前 6 个位置。这种行为对字符串有重要的影响:我们不能假设字符串的长度等于存储它的数组的长度。因此,C 中的字符串必须以特殊字符值(空字符 ('\0') 结尾)来指示字符串的结束。

以空字符结尾的字符串称为空终止字符串。尽管 C 中的所有字符串都应以 null 结尾,但未能正确考虑 null 字符是新手 C 程序员的常见错误来源。使用字符串时,请务必记住,您的字符数组必须声明为具有足够的容量来存储字符串中的每个字符值以及空字符(\0)。例如,要存储字符串“hi”,您需要一个至少包含三个字符的数组(一个用于存储“h”,一个用于存储“i”,一个用于存储\0)。

由于字符串很常用,C 提供了一个字符串库,其中包含用于操作字符串的函数。使用这些字符串库函数的程序需要包含 string.h 标头。

使用 printf 打印字符串值时,请在格式字符串中使用 %s占位符。 printf 函数将打印数组参数中的所有字符,直到遇到\0字符。同样,字符串库函数通常通过搜索\0字符来定位字符串的末尾,或者将\0字符添加到它们修改的任何字符串的末尾。

这是一个使用字符串和字符串库函数的示例程序:

#include <stdio.h>
#include <string.h>   // include the C string library

int main(void) {
    char str1[10];
    char str2[10];
    int len;

    str1[0] = 'h';
    str1[1] = 'i';
    str1[2] = '\0';

    len = strlen(str1);

    printf("%s %d\n", str1, len);  // prints: hi 2

    strcpy(str2, str1);     // copies the contents of str1 to str2
    printf("%s\n", str2);   // prints:  hi

    strcpy(str2, "hello");  // copy the string "hello" to str2
    len = strlen(str2);
    printf("%s has %d chars\n", str2, len);   // prints: hello has 5 chars
}

C 字符串库中的 strlen 函数返回其字符串参数中的字符数。字符串的终止空字符不计入字符串长度的一部分,因此对 strlen(str1) 的调用返回 2(字符串“hi”的长度)。 strcpy 函数一次将一个字符从源字符串(第二个参数)复制到目标字符串(第一个参数),直到到达源中的空字符。

请注意,大多数 C 字符串库函数都希望调用传入一个字符数组,该数组有足够的容量供函数执行其工作。例如,您不希望使用大小不足以包含源的目标字符串来调用 strcpy;这样做会导致程序中出现未定义的行为!

C 字符串库函数还要求传递给它们的字符串值格式正确,并以\0字符结尾。作为 C 程序员,您需要确保传入有效的字符串以供 C 库函数进行操作。因此,在前面示例中对 strcpy 的调用中,如果源字符串 (str1) 未初始化为具有终止字符\0,则 strcpy 将继续超出 str1 数组边界的末尾,从而导致未定义的行为导致其崩溃。

waring

前面的示例安全地使用了 strcpy 函数。但一般来说,strcpy 会带来安全风险,因为它假设其目标足够大以存储整个字符串,但情况可能并非总是如此(例如,如果字符串来自用户输入)。 我们选择现在显示 strcpy 是为了简化对字符串的介绍,但我们在第 2.6 节中说明了更安全的替代方案

在下一章中,我们将更详细地讨论 C 字符串和 C 字符串库。