08、数组介绍
在C语言中,数组是一种非常重要的数据结构,它允许我们存储和管理固定大小的相同类型元素的集合。
本文将基于数组的基本概念,深入探讨其用法、特性以及一些高级技巧。
基本概念
数组是由具有相同数据类型的元素按连续内存位置存储的数据结构。在C语言中,数组的大小在定义时必须指定,且在整个生命周期内保持不变。数组的元素可以通过索引来访问,索引从0开始。
1. 数组的声明与初始化
数组的声明需要指定数据类型和数组的大小。例如,声明一个包含5个整数的数组:
int numbers[5];
此时,数组中的元素未被初始化,它们的值是未定义的。为了初始化数组,我们可以在声明时指定元素的值:
int numbers[5] = {1, 2, 3, 4, 5};
如果初始化时提供的元素数量少于数组的大小,剩余的元素将被自动初始化为0(对于基本数据类型):
int numbers[5] = {1, 2}; // numbers数组为 {1, 2, 0, 0, 0}
如果省略数组大小,编译器将根据初始化列表中的元素数量来自动确定数组的大小:
int numbers[] = {1, 2, 3, 4, 5}; // 编译器自动确定数组大小为5
2. 访问数组元素
数组元素可以通过数组名和索引来访问。索引从0开始,因此numbers[0]
访问的是数组的第一个元素。
#include <stdio.h>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}
return 0;
}
3. 数组的遍历
遍历数组是访问数组元素的常见操作,通常使用循环来实现。上面的示例已经展示了如何使用for
循环来遍历数组,这里就不再次展示了。
数组的进阶用法
1. 多维数组
多维数组是数组的数组。最常见的是二维数组,它可以看作是一个表格,其中每个元素都是一个一维数组。
声明一个二维数组需要指定行数和列数:
int matrix[3][4]; // 3行4列的二维数组
初始化二维数组:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
访问二维数组的元素需要使用两个索引,第一个索引表示行,第二个索引表示列:
#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("matrix[%d][%d] = %d\n", i, j, matrix[i][j]);
}
}
return 0;
}
2. 数组作为函数参数
在C语言中,数组名代表数组的首地址,因此可以将数组作为函数参数传递。但是,需要注意的是,数组在作为函数参数时,会被退化为指向数组首元素的指针。
#include <stdio.h>
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
printArray(numbers, 5);
return 0;
}
对于二维数组,需要传递数组的行数和列数,或者传递指向数组的指针(在这种情况下,通常需要使用额外的参数来指定数组的维度)。
#include <stdio.h>
void printMatrix(int matrix[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printMatrix(matrix, 3);
return 0;
}
3. 数组与指针
数组名和指针之间有着密切的关系。数组名可以被解释为指向数组首元素的指针。因此,可以使用指针来遍历和操作数组。
#include <stdio.h>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int *ptr = numbers; // 指针指向数组首元素
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i)); // 使用指针遍历数组
}
printf("\n");
return 0;
}
指针算术允许我们通过指针来访问数组的元素。ptr + i
计算的是第i
个元素的地址,*(ptr + i)
则访问该地址处的值。
数组的扩展用法与技巧
1. 动态数组
数组的大小在定义时必须指定,且在整个生命周期内保持不变。然而,有时我们需要根据程序运行时的条件来动态分配数组的大小。这时,可以使用C标准库中的动态内存分配函数,如malloc
、calloc
和realloc
。
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("Enter the number of elements: ");
scanf("%d", &n);
int *arr = (int *)malloc(n * sizeof(int)); // 动态分配内存
if (arr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 初始化数组元素
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// 打印数组元素
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 释放动态分配的内存
free(arr);
return 0;
}
在这个例子中,我们使用malloc
函数动态分配了一个整数数组,并在使用完毕后使用free
函数释放了内存。
2. 字符串与字符数组
字符串通常是通过字符数组来实现的。字符串以空字符\0
结尾,因此字符数组的大小通常比字符串的实际长度多1。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!"; // 自动包含空字符'\0'
printf("String: %s\n", str);
printf("Length of string: %lu\n", strlen(str)); // 使用strlen函数获取字符串长度
return 0;
}
在这个例子中,str
是一个字符数组,它存储了字符串"Hello, World!"
以及一个空字符\0
。strlen
函数用于获取字符串的长度,不包括空字符。
3. 数组与函数指针
函数指针是指向函数的指针。我们可以将函数指针作为数组的元素,从而创建一个函数指针数组。我们可以在运行时动态选择并调用函数。
#include <stdio.h>
void func1() {
printf("Function 1\n");
}
void func2() {
printf("Function 2\n");
}
int main() {
void (*funcArray[2])() = {func1, func2}; // 函数指针数组
for (int i = 0; i < 2; i++) {
funcArray[i](); // 调用函数指针数组中的函数
}
return 0;
}
在这个例子中,我们定义了一个函数指针数组 funcArray
,其中包含了两个函数的指针:func1
和 func2
。通过遍历这个数组,我们可以动态地调用这些函数。
数组的高级应用与注意事项
1. 数组与内存布局
在C语言中,数组在内存中是连续存储的。这意味着数组的元素在内存中的地址是连续的,这有助于快速访问和处理数组数据。
例如,对于一个整型数组 int arr[5];
,其在内存中的布局可能如下(假设每个整数占用4个字节):
+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | (索引)
+---+---+---+---+---+
|.. |.. |.. |.. |.. | (内存地址,每个单元格4字节)
+---+---+---+---+---+
数组名 arr
实际上是指向数组第一个元素的指针,即 &arr[0]
。
2. 数组越界
当试图访问数组索引范围之外的元素时,就会发生数组越界。这种错误可能导致程序崩溃、数据损坏或不可预测的行为。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
// 故意访问越界元素
printf("arr[5] = %d\n", arr[5]); // 未定义行为
return 0;
}
arr[5]
是越界访问,因为数组 arr
的有效索引范围是 0 到 4。访问 arr[5]
将导致未定义行为。
为了避免数组越界,应该始终在访问数组元素时检查索引是否在有效范围内。
3. 数组与指针运算
虽然数组名可以被当作指针使用,但有一些细微的差别。数组名是一个常量指针,不能对其赋值或进行算术运算以改变其指向。然而,可以通过指针变量来遍历数组。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 指针指向数组首元素
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d, *(ptr + %d) = %d\n", i, arr[i], i, *(ptr + i));
}
// 指针运算
ptr += 2; // 指针移动到数组的第三个元素
printf("After moving pointer, *ptr = %d\n", *ptr); // 输出3
return 0;
}
在这个例子中,指针 ptr
被初始化为指向数组 arr
的首元素。通过指针运算 ptr += 2;
,我们可以将指针移动到数组的第三个元素。
4. 多维数组的内存布局
多维数组在内存中也是连续存储的。对于二维数组 int matrix[3][4];
,其在内存中的布局是行优先的(即先存储第一行的所有元素,再存储第二行的所有元素,依此类推)。
+---+---+---+---+---+---+---+---+---+---+---+---+
| 0,0| 0,1| 0,2| 0,3| 1,0| 1,1| 1,2| 1,3| 2,0| 2,1| ...
+---+---+---+---+---+---+---+---+---+---+---+---+
5. 数组与字符串处理
字符串是以空字符 \0
结尾的字符数组。处理字符串时,需要特别注意字符串的长度和空字符的存在。
#include <stdio.h>
#include <string.h>
int main() {
char str[20] = "Hello, World!";
printf("String: %s\n", str);
printf("Length: %lu\n", strlen(str)); // 不包括空字符
// 字符串拼接
strcat(str, "!!!"); // 注意目标数组必须有足够的空间
printf("After concatenation: %s\n", str);
return 0;
}
在这个例子中,我们使用 strcat
函数将字符串 "!!!"
拼接到 str
的末尾。需要注意的是,目标数组 str
必须有足够的空间来存储拼接后的字符串,否则会导致数组越界和未定义行为。
回顾
基本声明与初始化
一维数组
// 声明一个整型数组,大小为5,未初始化(这里只是为了掌握概念,我们平时写代码可不建议声明后未初始化)
int arr[5];
// 声明并初始化一个整型数组
int arr[5] = {1, 2, 3, 4, 5};
// 部分初始化,未初始化的元素将自动初始化为0
int arr[5] = {1, 2}; // arr = {1, 2, 0, 0, 0}
多维数组
// 声明一个2x3的整型二维数组,未初始化
int arr[2][3];
// 声明并初始化一个2x3的整型二维数组
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 部分初始化,未初始化的元素将自动初始化为0
int arr[2][3] = {
{1, 2}
}; // arr = {{1, 2, 0}, {0, 0, 0}}
遍历
一维数组
int arr[5] = {1, 2, 3, 4, 5};
// 使用for循环遍历
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
// 使用while循环遍历
int i = 0;
while (i < 5) {
std::cout << arr[i] << " ";
++i;
}
多维数组
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 使用嵌套的for循环遍历二维数组
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
元素访问与修改
一维数组
int arr[5] = {1, 2, 3, 4, 5};
// 访问数组元素
int value = arr[2]; // value = 3
// 修改数组元素
arr[2] = 10; // arr = {1, 2, 10, 4, 5}
// 思考并测试下,如果越界访问数据会发生什么?
多维数组
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 访问二维数组元素
int value = arr[1][2]; // value = 6
// 修改二维数组元素
arr[1][2] = 9; // arr = {{1, 2, 3}, {4, 5, 9}}
数组操作
计算数组长度
在C++中,静态数组的长度是编译时确定的,可以使用sizeof
运算符来计算数组的长度:
int arr[5];
int length = sizeof(arr) / sizeof(arr[0]); // length = 5
// 注意,一定要除以sizeof(arr[0])才能得到数组真正的长度,思考下为什么?
注意,对于动态分配的数组(如使用new
关键字分配的数组),需要手动保存其长度,因为sizeof
运算符无法直接应用于指针来获取动态数组的长度。
不支持动态改变大小和直接在数组中插入/删除元素
C++标准数组不支持动态改变大小或直接在数组中插入/删除元素。如果需要这些功能,可以使用std::vector
等容器。
排序
简单举个冒泡排序的示例,方便我们深入理解数组:
可以直接点此链接 https://godbolt.org/z/1orWW1beM 查看代码。
#include <iostream>
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n-1; ++i) {
for (int j = 0; j < n-i-1; ++j) {
if (arr[j] > arr[j+1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr)/sizeof(arr[0]);
bubbleSort(arr, n);
std::cout << "Sorted array: \n";
for (int i = 0; i < n; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0;
}
总结
- 数组是一种存储固定大小相同类型元素的集合。
- 可以使用
for
或while
循环遍历数组。 - 可以通过索引访问和修改数组元素,不能越界访问数组。
- 静态数组的长度是编译时确定的,可以使用
sizeof
运算符计算。 - C++标准数组不支持动态改变大小和直接在数组中插入/删除元素,但可以使用
std::vector
等动态容器。 - 可以使用各种排序算法对数组进行排序。
练习
题目1:数组声明与初始化
编写一个程序,声明一个整型数组,并初始化为{1, 3, 5, 7, 9}
,然后遍历并打印数组中的每个元素。
题目2:数组元素查找
编写一个程序,声明一个整型数组,并初始化为一些随机整数。编写一个函数,该函数接受数组、数组大小和要查找的元素作为参数,如果找到该元素,则返回其在数组中的索引;如果未找到,则返回-1。在main
函数中测试该函数。
题目3:数组排序
编写一个程序,声明一个整型数组,并初始化为一些未排序的整数。使用冒泡排序算法对数组进行排序,并打印排序后的数组。
题目4:二维数组操作
编写一个程序,声明并初始化一个二维整型数组(如矩阵),然后编写一个函数,该函数计算并返回矩阵中所有元素的和。在main
函数中测试该函数。
题目5:数组与字符串(额外)
虽然字符串在C++中通常通过std::string
处理,但了解C风格字符串(字符数组)的处理也是有益的。编写一个程序,声明一个字符数组作为字符串,并使用循环结构遍历字符串,打印出每个字符及其ASCII码值。
