1. 数组的基本概念:为什么需要数组?
在编程中,我们经常需要处理大量同类型的数据。假设你要编写一个学生成绩管理系统,需要记录100名学生的数学成绩。如果使用普通变量,你需要声明100个int变量:score1, score2, ..., score100。这不仅代码冗长,而且难以维护。
数组就是为了解决这类问题而生的数据结构。它是一组相同类型元素的集合,这些元素在内存中连续存放,通过一个统一的数组名和下标(索引)来访问特定元素。
数组的核心优势在于:
- 统一管理大量同类型数据
- 便于批量处理(如循环遍历)
- 代码简洁,逻辑清晰
举个例子,用数组存储5个学生的成绩:
c复制int scores[5] = {90, 85, 78, 92, 88};
这样只需要一个变量名scores,就能管理所有学生的成绩。
注意:C语言中数组的下标从0开始,这与一些其他编程语言(如MATLAB)不同,初学者需要特别注意。
2. 数组的声明与初始化
2.1 数组声明语法
在C语言中,声明数组的基本语法是:
c复制数据类型 数组名[数组长度];
例如:
c复制int numbers[10]; // 声明一个包含10个整数的数组
float temps[24]; // 声明一个包含24个浮点数的数组
char name[20]; // 声明一个包含20个字符的数组
2.2 数组初始化
数组可以在声明时初始化:
c复制int primes[5] = {2, 3, 5, 7, 11};
如果初始化值少于数组长度,剩余元素会自动初始化为0:
c复制int nums[5] = {1, 2, 3}; // nums[3]和nums[4]自动为0
也可以不指定长度,编译器会根据初始化值的数量自动确定:
c复制int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
提示:在C99标准中,还支持指定初始化器(designated initializers):
c复制int arr[10] = {[3] = 100, [7] = 200};这样可以直接初始化特定位置的元素,其他位置默认为0。
3. 数组元素的访问
3.1 基本访问方式
数组元素通过下标访问,语法为:
c复制数组名[下标]
例如:
c复制int scores[5] = {90, 85, 78, 92, 88};
printf("%d", scores[0]); // 输出第一个元素:90
scores[2] = 80; // 修改第三个元素
3.2 数组与指针的关系
在C语言中,数组名实际上是一个指向数组首元素的指针。因此:
c复制int arr[5];
arr == &arr[0]; // 这个表达式为真
这意味着我们可以用指针的方式来操作数组:
c复制int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d", *(p + 2)); // 输出arr[2]的值:3
注意:虽然数组名可以看作指针,但它不是左值,不能进行赋值操作。例如
arr = p;是错误的。
4. 数组的遍历
4.1 使用for循环遍历
最常见的数组遍历方式是使用for循环:
c复制int numbers[5] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
printf("%d ", numbers[i]);
}
4.2 使用指针遍历
也可以用指针来遍历数组:
c复制int numbers[5] = {10, 20, 30, 40, 50};
int *p;
for (p = numbers; p < numbers + 5; p++) {
printf("%d ", *p);
}
4.3 计算数组长度
在C语言中,数组没有内置的长度属性。通常用以下方法计算:
c复制int arr[] = {1, 2, 3, 4, 5};
int length = sizeof(arr) / sizeof(arr[0]);
警告:这种方法只适用于真正的数组,不适用于指针。如果数组作为参数传递给函数,它会退化为指针,此时sizeof返回的是指针大小而非数组大小。
5. 数组的边界检查
5.1 数组越界的危险
C语言不检查数组边界,访问越界元素会导致未定义行为:
c复制int arr[5] = {0};
arr[5] = 10; // 越界访问,可能导致程序崩溃
5.2 如何避免越界
- 始终检查数组索引是否有效
- 使用sizeof计算数组长度
- 在循环中确保索引不超过数组长度-1
例如:
c复制int arr[5];
for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) {
// 安全操作
}
经验:在大型项目中,可以考虑封装数组操作,添加边界检查功能,提高代码安全性。
6. 数组应用:数据处理基础
6.1 计算数组平均值
c复制float average(int arr[], int length) {
int sum = 0;
for (int i = 0; i < length; i++) {
sum += arr[i];
}
return (float)sum / length;
}
6.2 查找数组最大值
c复制int findMax(int arr[], int length) {
int max = arr[0];
for (int i = 1; i < length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
6.3 数组反转
c复制void reverseArray(int arr[], int length) {
for (int i = 0; i < length / 2; i++) {
int temp = arr[i];
arr[i] = arr[length - 1 - i];
arr[length - 1 - i] = temp;
}
}
7. 数组基础练习
7.1 练习1:统计成绩分布
编写程序,统计一个班级的成绩分布(假设成绩为0-100的整数):
c复制void countGrades(int scores[], int length) {
int count[11] = {0}; // 0-9, 10-19, ..., 100
for (int i = 0; i < length; i++) {
int index = scores[i] / 10;
count[index]++;
}
for (int i = 0; i < 11; i++) {
printf("%d-%d: %d\n", i*10, (i==10)?100:i*10+9, count[i]);
}
}
7.2 练习2:冒泡排序
实现经典的冒泡排序算法:
c复制void bubbleSort(int arr[], int length) {
for (int i = 0; i < length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
7.3 练习3:二分查找
在已排序数组中实现二分查找:
c复制int binarySearch(int arr[], int length, int target) {
int left = 0, right = length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到
}
8. 数组使用中的常见问题
8.1 数组作为函数参数
当数组作为函数参数传递时,实际上传递的是数组首元素的地址。因此函数内部无法通过sizeof获取数组长度,需要额外传递长度参数:
c复制void printArray(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
}
8.2 多维数组
C语言支持多维数组,例如二维数组:
c复制int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
访问方式:
c复制int value = matrix[1][2]; // 获取第二行第三列的元素:7
8.3 动态数组
C语言中可以使用malloc动态分配数组:
c复制int *arr = (int*)malloc(10 * sizeof(int)); // 分配10个整数的空间
if (arr != NULL) {
// 使用数组
free(arr); // 释放内存
}
重要:使用动态数组后必须手动释放内存,否则会导致内存泄漏。
9. 数组与字符串
在C语言中,字符串实际上是字符数组,以'\0'(空字符)结尾:
c复制char str[] = "Hello"; // 实际上是{'H','e','l','l','o','\0'}
常用字符串操作函数(需要包含string.h):
- strlen:获取字符串长度
- strcpy:字符串复制
- strcat:字符串连接
- strcmp:字符串比较
例如:
c复制char src[] = "world";
char dest[20] = "Hello ";
strcat(dest, src); // dest变为"Hello world"
10. 数组的高级应用
10.1 位数组
当需要高效存储大量布尔值时,可以使用位数组:
c复制unsigned char bitArray[10]; // 可以表示80个布尔值
// 设置第n位
void setBit(int n) {
bitArray[n/8] |= (1 << (n%8));
}
// 清除第n位
void clearBit(int n) {
bitArray[n/8] &= ~(1 << (n%8));
}
// 测试第n位
int testBit(int n) {
return bitArray[n/8] & (1 << (n%8));
}
10.2 变长数组(VLA)
C99标准引入了变长数组,长度可以在运行时确定:
c复制int n;
scanf("%d", &n);
int arr[n]; // 变长数组
注意:变长数组有一些限制,例如不能初始化,且在一些编译器中可能不支持。
10.3 柔性数组成员
在结构体末尾可以声明一个长度不确定的数组,称为柔性数组成员:
c复制struct flexArray {
int length;
int data[]; // 柔性数组成员
};
struct flexArray *arr = malloc(sizeof(struct flexArray) + 10*sizeof(int));
arr->length = 10;
这种技术常用于需要动态大小的结构体。
11. 性能优化技巧
11.1 缓存友好性
由于数组元素在内存中是连续存储的,顺序访问通常比随机访问更快,因为可以利用CPU缓存:
c复制// 好的做法:顺序访问
for (int i = 0; i < SIZE; i++) {
arr[i] = i;
}
// 不好的做法:随机访问(可能引起缓存失效)
for (int i = 0; i < SIZE; i++) {
int j = someRandomIndex();
arr[j] = i;
}
11.2 循环展开
对于性能关键的代码,可以考虑循环展开:
c复制// 普通循环
for (int i = 0; i < SIZE; i++) {
arr[i] = i * 2;
}
// 展开4次的循环
for (int i = 0; i < SIZE; i += 4) {
arr[i] = i * 2;
arr[i+1] = (i+1) * 2;
arr[i+2] = (i+2) * 2;
arr[i+3] = (i+3) * 2;
}
11.3 避免不必要的边界检查
在确保安全的情况下,可以移除一些边界检查来提高性能:
c复制// 原始代码(有边界检查)
for (int i = 0; i < SIZE; i++) {
if (i >= 0 && i < SIZE) {
arr[i] = i;
}
}
// 优化后(已知i的范围)
for (int i = 0; i < SIZE; i++) {
arr[i] = i;
}
12. 实际项目中的应用经验
在实际项目中,数组是最基础也是最常用的数据结构之一。以下是一些经验分享:
-
初始化习惯:养成初始化数组的好习惯,特别是局部数组变量。未初始化的数组可能包含随机值,导致难以调试的问题。
-
安全边界:始终考虑数组边界问题。可以使用宏或函数封装数组访问,添加边界检查:
c复制#define SAFE_ACCESS(arr, index, size) \ ((index) >= 0 && (index) < (size) ? (arr)[(index)] : 0) -
内存布局:了解数组在内存中的布局对性能优化很重要。多维数组在内存中是按行优先存储的。
-
调试技巧:当数组出现问题时,可以使用调试器查看内存内容,或者打印数组的各个元素。
-
与指针的转换:理解数组与指针的关系很重要,但也要注意它们的区别。例如
sizeof(arr)在数组和指针情况下会得到不同结果。 -
标准库函数:熟悉标准库中的数组相关函数,如memcpy、memset等,它们通常比手动实现的循环更高效。
-
动态数组管理:对于需要动态调整大小的数组,考虑使用realloc而不是频繁malloc/free。
-
平台兼容性:注意不同平台可能对栈大小有限制,大数组应该分配在堆上而非栈上。
在实际工作中,我发现很多初学者容易犯的错误包括:
- 忘记数组下标从0开始
- 数组越界访问
- 混淆数组和指针
- 在多维数组中使用错误的索引顺序
- 忘记释放动态分配的数组内存
掌握好数组的使用是C语言编程的基础,也是学习更复杂数据结构的前提。建议通过大量练习来熟悉数组的各种操作和应用场景。