1. 数组基础概念与内存布局
在C语言中,数组是最基础也是最重要的数据结构之一。它本质上是一块连续的内存空间,用于存储相同类型的多个元素。这种连续存储的特性带来了两个关键特征:一是可以通过下标快速访问任意元素(时间复杂度O(1)),二是数组的大小在定义时就必须确定且不可改变。
1.1 一维数组的定义与初始化
标准的一维数组定义语法为:
c复制数据类型 数组名[常量表达式];
这里的常量表达式必须能在编译时确定其值。例如:
c复制int scores[5]; // 声明包含5个整数的数组
float temps[31]; // 声明31个浮点数的数组(如一个月的每日温度)
数组初始化有多种方式:
c复制// 方式1:完全初始化
int primes[5] = {2, 3, 5, 7, 11};
// 方式2:部分初始化(剩余元素自动初始化为0)
int evens[10] = {0, 2, 4, 6};
// 方式3:省略长度(编译器自动计算)
int days[] = {31, 28, 31, 30}; // 长度为4
重要提示:数组越界访问是C语言中最常见的错误之一。编译器不会检查数组下标是否合法,越界访问可能导致程序崩溃或数据损坏。
1.2 数组的内存布局与指针关系
理解数组在内存中的实际存储方式至关重要。假设有如下定义:
c复制int arr[3] = {10, 20, 30};
内存布局示意图:
code复制地址 | 值
---------|----
0x1000 | 10 (arr[0])
0x1004 | 20 (arr[1])
0x1008 | 30 (arr[2])
数组名arr实际上是一个指向数组首元素的常量指针。以下表达式是等价的:
c复制arr[1] == *(arr + 1)
&arr[0] == arr
这种等价性揭示了数组和指针的深层关系,也是理解后续字符串操作的基础。
2. 多维数组与矩阵运算
2.1 二维数组的定义与初始化
二维数组可以看作"数组的数组",常用于表示矩阵或表格数据。定义语法:
c复制数据类型 数组名[行数][列数];
初始化示例:
c复制// 完全初始化
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 线性初始化(按内存顺序)
int sameMatrix[2][3] = {1, 2, 3, 4, 5, 6};
// 省略第一维长度
int table[][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8}
};
2.2 多维数组的内存布局
C语言采用"行优先"存储方式。对于int arr[2][3],内存排列顺序为:
code复制arr[0][0] → arr[0][1] → arr[0][2] → arr[1][0] → arr[1][1] → arr[1][2]
这种布局意味着以下指针算术成立:
c复制*(*(arr + i) + j) == arr[i][j]
2.3 多维数组的实际应用
矩阵乘法示例:
c复制#define ROWS_A 2
#define COLS_A 3
#define COLS_B 4
void matrixMultiply(int a[ROWS_A][COLS_A], int b[COLS_A][COLS_B], int result[ROWS_A][COLS_B]) {
for (int i = 0; i < ROWS_A; i++) {
for (int j = 0; j < COLS_B; j++) {
result[i][j] = 0;
for (int k = 0; k < COLS_A; k++) {
result[i][j] += a[i][k] * b[k][j];
}
}
}
}
性能提示:由于缓存局部性原理,遍历多维数组时应尽量保持内存访问的连续性。对于行优先存储的数组,外层循环应该是行索引,内层循环是列索引。
3. 字符数组与字符串处理
3.1 字符数组的特殊性
在C语言中,字符串本质上是以空字符'\0'结尾的字符数组。这使得字符数组与其他类型数组有重要区别:
c复制char str1[] = {'H', 'e', 'l', 'l', 'o'}; // 字符数组,不是字符串
char str2[] = "Hello"; // 字符串,自动添加'\0'
两者的内存对比:
code复制str1: | 'H' | 'e' | 'l' | 'l' | 'o' |
str2: | 'H' | 'e' | 'l' | 'l' | 'o' | '\0' |
3.2 常用字符串处理函数
C标准库<string.h>提供了丰富的字符串操作函数:
- 字符串复制:
c复制char dest[20];
strcpy(dest, "Hello"); // 简单复制
strncpy(dest, src, sizeof(dest)-1); // 更安全的版本
dest[sizeof(dest)-1] = '\0'; // 确保终止
- 字符串连接:
c复制strcat(str1, str2); // 可能溢出
strncat(str1, str2, remaining_space);
- 字符串比较:
c复制if (strcmp(str1, str2) == 0) {
// 字符串相等
}
- 字符串长度:
c复制size_t len = strlen(str); // 不包含'\0'
安全警告:传统的strcpy、strcat等函数不检查目标缓冲区大小,可能导致缓冲区溢出。在生产代码中应使用更安全的替代方案,如strncpy、strncat,或考虑使用现代C的边界检查函数。
3.3 字符串与指针的关系
字符串常量在内存中的存储位置特殊:
c复制char *ptr = "Hello"; // 字符串常量存储在只读区域
char arr[] = "Hello"; // 数组存储在栈上,可修改
两者的关键区别:
| 特性 | 指针形式 | 数组形式 |
|---|---|---|
| 存储位置 | 只读数据段 | 栈内存 |
| 可修改性 | 不可修改内容 | 可修改内容 |
| sizeof | 返回指针大小 | 返回数组大小 |
| 赋值操作 | 可直接赋值 | 需要strcpy |
4. 高级数组技巧与常见问题
4.1 动态数组的实现
虽然C语言原生不支持动态数组,但可以通过指针和内存管理函数模拟:
c复制int *dynArray = NULL;
size_t size = 0;
// 分配内存
size = 10;
dynArray = malloc(size * sizeof(int));
if (dynArray == NULL) {
// 处理分配失败
}
// 调整大小
size_t newSize = 20;
int *temp = realloc(dynArray, newSize * sizeof(int));
if (temp != NULL) {
dynArray = temp;
size = newSize;
} else {
// 处理分配失败,原指针仍有效
}
// 释放内存
free(dynArray);
dynArray = NULL;
4.2 数组作为函数参数
数组作为函数参数时实际上传递的是指针,因此需要额外传递大小信息:
c复制void processArray(int arr[], size_t size) {
// 等效于 void processArray(int *arr, size_t size)
for (size_t i = 0; i < size; i++) {
arr[i] *= 2;
}
}
// 调用方式
int myArray[5] = {1, 2, 3, 4, 5};
processArray(myArray, sizeof(myArray)/sizeof(myArray[0]));
对于多维数组,必须指定除第一维外的所有维度:
c复制void printMatrix(int mat[][3], int rows) {
// ...
}
4.3 常见陷阱与最佳实践
- 数组越界访问:
c复制int arr[5];
arr[5] = 10; // 未定义行为
- 数组名不是左值:
c复制int a[5], b[5];
a = b; // 错误!不能直接赋值数组
- sizeof陷阱:
c复制void func(int arr[]) {
size_t s = sizeof(arr); // 返回指针大小,不是数组大小
}
- 建议的防御性编程技巧:
- 总是检查数组边界
- 使用宏或常量定义数组大小
- 对数组操作封装成函数
- 考虑使用结构体包装数组和大小信息
5. 现代C语言中的数组改进
5.1 C99的可变长度数组(VLA)
C99引入了运行时确定大小的数组:
c复制void func(size_t size) {
int vla[size]; // 大小在运行时确定
// ...
}
注意事项:
- VLA不能有静态存储期
- 大尺寸VLA可能导致栈溢出
- 不是所有编译器都完全支持
5.2 复合字面量
C99允许创建匿名数组:
c复制int *ptr = (int[]){1, 2, 3}; // 复合字面量
processArray((int[]){4,5,6}, 3);
5.3 数组与结构体的结合
使用结构体封装数组可以创建更安全的数据结构:
c复制typedef struct {
int *data;
size_t size;
size_t capacity;
} IntArray;
IntArray createArray(size_t initialSize) {
IntArray arr;
arr.data = malloc(initialSize * sizeof(int));
arr.size = 0;
arr.capacity = initialSize;
return arr;
}
void append(IntArray *arr, int value) {
if (arr->size >= arr->capacity) {
arr->capacity *= 2;
arr->data = realloc(arr->data, arr->capacity * sizeof(int));
}
arr->data[arr->size++] = value;
}
这种模式是现代C语言中实现动态数组的推荐方式,它结合了类型安全和内存管理的便利性。
