MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言数组定义的基本规则

2021-10-284.3k 阅读

一、C 语言数组的基本概念

在 C 语言中,数组是一种非常重要的数据结构,它允许我们将多个相同类型的元素存储在连续的内存位置中。数组为我们提供了一种方便的方式来管理和操作一组相关的数据。例如,我们可以用一个数组来存储一个班级学生的成绩,或者存储一系列的坐标值等。

从本质上来说,数组是内存中一段连续的存储空间,这些空间被划分为一个个大小相等的单元,每个单元用于存储一个特定类型的元素。这些元素在内存中按照顺序依次排列,它们的类型必须是相同的,这是数组定义的一个重要前提。例如,我们不能在一个数组中同时存储整数和字符,除非使用一些特殊的技巧(如联合等,但这与普通数组的概念不同)。

二、数组定义的基本语法

(一)一维数组的定义

一维数组是最基本的数组形式,其定义语法如下:

type arrayName[arraySize];

这里,type 表示数组元素的数据类型,可以是 C 语言中的任何基本数据类型,如 int(整数类型)、float(单精度浮点类型)、char(字符类型)等,也可以是自定义的数据类型,如结构体等。arrayName 是我们给数组取的名字,它遵循 C 语言中标识符的命名规则,即只能由字母、数字和下划线组成,且不能以数字开头。arraySize 是一个常量表达式,用于指定数组中元素的个数,它必须是一个大于 0 的整数。

下面通过一些代码示例来进一步理解一维数组的定义:

// 定义一个包含 5 个整数的数组
int intArray[5];
// 定义一个包含 10 个字符的数组
char charArray[10];
// 定义一个包含 3 个单精度浮点数的数组
float floatArray[3];

在上述代码中,intArray 是一个可以存储 5 个整数的数组,charArray 可以存储 10 个字符,floatArray 可以存储 3 个单精度浮点数。数组一旦定义,其大小在程序运行期间通常是固定的,不能动态改变。

(二)多维数组的定义

多维数组是一维数组的扩展,其中最常见的是二维数组。二维数组可以看作是一个“数组的数组”,其定义语法如下:

type arrayName[numRows][numCols];

这里,numRows 表示数组的行数,numCols 表示数组的列数。同样,typearrayName 的含义与一维数组定义中的相同。例如,以下代码定义了一个 3 行 4 列的二维整数数组:

int twoDArray[3][4];

这个二维数组可以想象成一个有 3 行 4 列的表格,总共可以存储 3 * 4 = 12 个整数。

对于更高维的数组,定义方式类似,例如三维数组的定义:

type arrayName[size1][size2][size3];

这里 size1size2size3 分别表示三个维度的大小。虽然高维数组在实际应用中不像一维和二维数组那么常见,但在某些特定场景,如处理三维空间数据等情况下会很有用。

三、数组定义时的初始化

(一)一维数组的初始化

在定义数组的同时,可以对数组进行初始化,即给数组的元素赋予初始值。一维数组的初始化方式有以下几种:

  1. 逐个初始化:可以依次列出数组元素的初始值,用逗号分隔,放在花括号 {} 内。例如:
int intArray[5] = {1, 2, 3, 4, 5};

这里,intArray[0] 的值为 1,intArray[1] 的值为 2,以此类推,intArray[4] 的值为 5。

  1. 部分初始化:如果只给数组的前几个元素初始化,后面未初始化的元素会根据数组类型自动赋予默认值。对于数值类型(如 intfloat 等),默认值为 0;对于字符类型(char),默认值为空字符 '\0'。例如:
int intArray[5] = {1, 2};

在这个例子中,intArray[0] 为 1,intArray[1] 为 2,而 intArray[2]intArray[3]intArray[4] 的值都为 0。

  1. 省略数组大小初始化:在初始化数组时,可以省略数组大小,编译器会根据初始化列表中元素的个数自动确定数组的大小。例如:
int intArray[] = {1, 2, 3, 4, 5};

这里,编译器会自动将 intArray 的大小确定为 5。

(二)二维数组的初始化

二维数组的初始化也有多种方式:

  1. 按行初始化:可以将二维数组看作是由多个一维数组组成,按行进行初始化。例如:
int twoDArray[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

在这个例子中,第一行的元素 twoDArray[0][0]twoDArray[0][3] 分别为 1、2、3、4;第二行的元素 twoDArray[1][0]twoDArray[1][3] 分别为 5、6、7、8;第三行同理。

  1. 连续初始化:也可以不分行,直接连续列出所有元素的初始值。例如:
int twoDArray[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

这种方式与按行初始化的效果是一样的,只是书写形式不同。编译器会按照数组元素在内存中的存储顺序依次给元素赋值。在 C 语言中,二维数组按行优先存储,即先存储第一行的所有元素,再存储第二行,以此类推。

  1. 部分初始化:和一维数组类似,二维数组也可以进行部分初始化。例如:
int twoDArray[3][4] = {
    {1, 2},
    {5}
};

这里,twoDArray[0][0] 为 1,twoDArray[0][1] 为 2,twoDArray[1][0] 为 5,其余未初始化的元素值为 0。

对于多维数组的初始化,基本原理与二维数组类似,都是按照数组元素在内存中的存储顺序进行赋值。

四、数组定义中的常量表达式

(一)常量表达式的要求

在定义数组时,数组大小必须是一个常量表达式。常量表达式是指在编译时就能确定其值的表达式,它可以包含常量、常量运算符以及 sizeof 运算符等,但不能包含变量。例如:

const int size = 5;
int intArray[size];  // 正确,size 是常量
int n = 5;
int badArray[n];    // 错误,n 是变量

在 C99 标准之前,数组大小必须是一个整型常量。C99 引入了变长数组(VLA)的概念,允许数组大小是一个整型变量,但变长数组有一些限制,例如不能在函数体外定义,且其大小在运行时确定,这与传统的固定大小数组在内存分配和使用上有一些区别。

(二)sizeof 运算符在数组定义中的应用

sizeof 运算符可以用于获取数组的大小(以字节为单位),在某些情况下,它可以用于数组定义中的常量表达式。例如:

char charArray[] = "Hello";
int size = sizeof(charArray) / sizeof(charArray[0]);
int newArray[size];

在上述代码中,sizeof(charArray) 获取了 charArray 数组的总字节数,sizeof(charArray[0]) 获取了数组中单个元素的字节数,两者相除得到数组元素的个数。然后可以用这个计算结果来定义另一个数组的大小。

五、数组定义与内存分配

(一)一维数组的内存分配

当定义一个一维数组时,系统会在内存中为其分配一段连续的存储空间。例如,定义一个包含 5 个整数的数组 int intArray[5];,假设每个整数在系统中占用 4 个字节(在 32 位系统中通常是这样),那么系统会为 intArray 分配 5 * 4 = 20 个字节的连续空间。

数组名 intArray 实际上是数组首元素的地址,也就是这段连续存储空间的起始地址。通过数组名和下标,可以访问数组中的每个元素。例如,intArray[2] 表示访问数组中的第 3 个元素(数组下标从 0 开始),系统会根据数组首地址和元素大小,计算出该元素在内存中的实际地址并进行访问。

(二)二维数组的内存分配

二维数组在内存中同样是按顺序存储的,它按照行优先的原则分配内存。以 int twoDArray[3][4]; 为例,假设每个整数占用 4 个字节,那么总共需要分配 3 * 4 * 4 = 48 个字节的连续空间。

首先存储第一行的 4 个元素,接着存储第二行的 4 个元素,最后存储第三行的 4 个元素。在内存中,twoDArray[0][0] 是第一个存储的元素,twoDArray[0][1] 紧挨着 twoDArray[0][0] 存储,以此类推。twoDArray 同样是数组首元素(即 twoDArray[0][0])的地址。通过 twoDArray[i][j] 这种形式,可以访问二维数组中的任意元素,系统会根据数组的存储方式和下标计算出该元素的实际内存地址。

对于更高维的数组,内存分配原则类似,都是按照一定的顺序在连续的内存空间中存储元素。

六、数组定义中的常见错误及注意事项

(一)数组越界

数组越界是在使用数组时最常见的错误之一。由于 C 语言对数组下标没有自动边界检查,当访问数组元素的下标超出了数组定义的范围时,就会发生数组越界。例如:

int intArray[5] = {1, 2, 3, 4, 5};
printf("%d\n", intArray[5]);  // 越界访问,数组下标最大为 4

在上述代码中,试图访问 intArray[5],而该数组的有效下标范围是 0 到 4,这会导致未定义行为。未定义行为意味着程序的运行结果是不确定的,可能会导致程序崩溃、数据损坏或其他奇怪的现象。在编写程序时,一定要确保对数组下标的访问在有效范围内。

(二)定义数组时的语法错误

在定义数组时,可能会出现各种语法错误,例如遗漏数组大小、使用非法的数组名等。例如:

int intArray[];  // 错误,定义数组时省略大小且未初始化
int 123array[5]; // 错误,数组名不能以数字开头

第一个例子中,省略数组大小且未进行初始化是不允许的;第二个例子中,数组名 123array 不符合 C 语言标识符的命名规则。

(三)混淆数组名和指针

在 C 语言中,数组名在很多情况下会被隐式转换为指向数组首元素的指针,但数组名和指针并不完全相同。例如,不能对数组名进行自增自减操作,而指针可以。例如:

int intArray[5] = {1, 2, 3, 4, 5};
intArray++;  // 错误,数组名不是可修改的左值

在上述代码中,试图对数组名 intArray 进行自增操作是错误的,因为数组名代表的是一个固定的内存地址,不是一个可修改的左值。而指针变量是可以进行自增自减等算术运算的,例如:

int *ptr = intArray;
ptr++;

这里,指针 ptr 可以进行自增操作,使其指向数组的下一个元素。

(四)多维数组定义时的错误

在定义多维数组时,容易出现对维数理解错误的情况。例如,定义二维数组时少写了一个维度的大小:

int twoDArray[][4];  // 错误,缺少第一维的大小

在这种情况下,编译器无法确定数组的行数,所以是错误的。除非在初始化时能够让编译器推断出第一维的大小,例如:

int twoDArray[][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8}
};

这里,编译器可以根据初始化列表中的行数推断出 twoDArray 的第一维大小为 2。

另外,在初始化多维数组时,要注意元素的个数与数组大小的匹配。例如:

int twoDArray[2][3] = {1, 2, 3, 4, 5};  // 错误,初始化元素个数不足

上述代码中,初始化列表中的元素个数为 5,而数组 twoDArray 总共需要存储 2 * 3 = 6 个元素,这会导致未初始化的元素值不确定,是一种错误的初始化方式。

七、不同存储类别的数组定义

(一)自动数组

自动数组是在函数内部定义的数组,其存储类别为自动存储类别(默认情况下)。例如:

void func() {
    int autoArray[5];
    // 使用 autoArray
}

自动数组的生命周期与函数的执行周期相关。当函数被调用时,自动数组在栈上分配内存;函数结束时,自动数组占用的内存被释放。自动数组的初始化值是不确定的,除非在定义时进行了初始化。

(二)静态数组

静态数组是使用 static 关键字修饰的数组,其存储类别为静态存储类别。例如:

void func() {
    static int staticArray[5];
    // 使用 staticArray
}

静态数组在程序开始执行时就分配内存,并且在整个程序的运行期间都存在,不会随着函数的调用和结束而分配和释放内存。静态数组如果未初始化,其元素默认值为 0(对于数值类型)或空字符 '\0'(对于字符类型)。

(三)外部数组

外部数组是在所有函数外部定义的数组,其作用域是从定义处到源文件的末尾。如果需要在其他源文件中使用该数组,可以使用 extern 关键字进行声明。例如,在 file1.c 中定义:

int globalArray[5];

file2.c 中使用:

extern int globalArray[5];

外部数组的存储类别为静态存储类别,其生命周期也是整个程序的运行期间。外部数组如果未初始化,其元素默认值同样为 0(对于数值类型)或空字符 '\0'(对于字符类型)。

(四)寄存器数组

寄存器数组是使用 register 关键字修饰的数组,其存储类别为寄存器存储类别。例如:

void func() {
    register int regArray[5];
    // 使用 regArray
}

寄存器数组的目的是让编译器将数组存储在寄存器中,以提高访问速度。然而,由于寄存器数量有限,并且数组通常较大,实际上编译器可能不会将整个数组存储在寄存器中,甚至可能完全忽略 register 关键字。此外,寄存器数组不能使用 & 运算符获取其地址,因为寄存器没有内存地址。

不同存储类别的数组在内存分配、生命周期和初始化等方面都有不同的特点,在实际编程中,需要根据具体的需求选择合适的存储类别。

八、数组定义在实际编程中的应用场景

(一)数据存储与处理

数组最常见的应用场景之一是存储和处理一组相关的数据。例如,在统计学生成绩时,可以用一个数组来存储每个学生的成绩,然后通过遍历数组来计算平均分、最高分、最低分等统计信息。以下是一个简单的示例代码:

#include <stdio.h>

int main() {
    int scores[5] = {85, 90, 78, 88, 95};
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += scores[i];
    }
    float average = (float)sum / 5;
    printf("Average score: %.2f\n", average);
    return 0;
}

在这个例子中,scores 数组存储了 5 个学生的成绩,通过循环遍历数组,计算出总成绩并求出平均分。

(二)矩阵运算

二维数组在矩阵运算中有着广泛的应用。矩阵是数学中的一个重要概念,在计算机图形学、线性代数等领域经常会用到矩阵运算,如矩阵乘法、转置等。以下是一个简单的矩阵乘法示例代码:

#include <stdio.h>

void matrixMultiply(int a[][3], int b[][2], int result[][2], int rowsA, int colsA, int colsB) {
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsB; j++) {
            result[i][j] = 0;
            for (int k = 0; k < colsA; k++) {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }
}

int main() {
    int a[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };
    int b[3][2] = {
        {7, 8},
        {9, 10},
        {11, 12}
    };
    int result[2][2];
    matrixMultiply(a, b, result, 2, 3, 2);
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 2; j++) {
            printf("%d ", result[i][j]);
        }
        printf("\n");
    }
    return 0;
}

在这个示例中,定义了两个二维数组 ab 分别表示两个矩阵,通过 matrixMultiply 函数实现矩阵乘法,并将结果存储在 result 数组中。

(三)字符串处理

虽然 C 语言中没有专门的字符串类型,但可以使用字符数组来处理字符串。例如,字符串的输入、输出、比较、拼接等操作都可以通过字符数组来实现。以下是一个简单的字符串拼接示例代码:

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

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "world!";
    strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);
    printf("%s\n", str1);
    return 0;
}

在这个例子中,str1str2 都是字符数组,通过 strncat 函数将 str2 拼接在 str1 的后面,并确保不会发生数组越界。

(四)图形处理

在计算机图形学中,多维数组可以用于表示图像、图形等数据。例如,一个二维数组可以表示一个黑白图像,数组中的每个元素表示图像中一个像素的灰度值;三维数组可以用于表示彩色图像,其中一个维度表示颜色通道(如红、绿、蓝)。以下是一个简单的用二维数组表示黑白图像并进行简单图像处理的示例代码:

#include <stdio.h>

#define WIDTH 10
#define HEIGHT 10

void invertImage(int image[HEIGHT][WIDTH]) {
    for (int i = 0; i < HEIGHT; i++) {
        for (int j = 0; j < WIDTH; j++) {
            image[i][j] = 255 - image[i][j];  // 假设灰度值范围是 0 - 255
        }
    }
}

int main() {
    int image[HEIGHT][WIDTH] = {
        // 初始化图像数据,这里省略具体值
    };
    invertImage(image);
    for (int i = 0; i < HEIGHT; i++) {
        for (int j = 0; j < WIDTH; j++) {
            printf("%3d ", image[i][j]);
        }
        printf("\n");
    }
    return 0;
}

在这个示例中,定义了一个二维数组 image 来表示黑白图像,通过 invertImage 函数对图像进行反相处理,即将每个像素的灰度值取反。

通过以上这些实际应用场景,可以看到数组在 C 语言编程中扮演着非常重要的角色,熟练掌握数组的定义和使用对于编写高效、实用的程序至关重要。