数组是C语言中最基础也是最重要的复合数据类型之一。在实际开发中,我们几乎每天都要和各种数组打交道。但很多初学者对数组的理解停留在表面,导致在实际编码中频繁出现越界访问、内存泄漏等问题。
数组本质上是一块连续的内存空间,其中每个元素的类型相同。当我们声明int arr[5]时,编译器会在内存中分配一块足够存放5个int型数据的连续空间。这块内存的大小可以通过sizeof(arr)计算得到,在32位系统上通常是20字节(5*4字节)。
数组名在大多数情况下会退化为指向数组首元素的指针,这也是为什么我们可以用指针的方式来操作数组。但要注意sizeof(arr)和sizeof(指针)的区别,这是判断一个变量是数组还是指针的重要依据。
C语言为数组提供了多种初始化方式,合理使用可以提升代码的可读性和安全性:
c复制// 完全初始化
int arr1[5] = {1, 2, 3, 4, 5};
// 部分初始化,未指定的元素自动初始化为0
int arr2[5] = {1, 2};
// 不指定大小,由初始化列表决定数组长度
int arr3[] = {1, 2, 3, 4, 5};
// C99新增的设计指定初始化方式
int arr4[5] = {[2] = 3, [4] = 5};
在实际项目中,我强烈建议总是对数组进行显式初始化。未初始化的数组元素值是未定义的,可能导致难以排查的bug。
理解多维数组的内存布局对性能优化非常重要。C语言中的多维数组实际上是"数组的数组",在内存中是按行优先顺序连续存储的。例如:
c复制int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
这段代码在内存中的布局是:1,2,3,4,5,6,7,8,9,10,11,12。了解这一点可以帮助我们优化数据访问模式,提高缓存命中率。
提示:在遍历多维数组时,应该尽量按照内存布局的顺序访问元素,即外层循环控制行,内层循环控制列。这样可以获得更好的缓存局部性。
在C语言中,字符串常量如"hello"实际上是一个字符数组,编译器会自动在末尾添加'\0'作为结束符。这些字符串常量通常存储在程序的只读数据段(.rodata),尝试修改它们会导致未定义行为。
c复制char *str = "hello"; // 字符串常量,存储在只读区域
str[0] = 'H'; // 运行时错误!试图修改只读内存
在实际项目中,如果需要修改字符串内容,应该使用字符数组来初始化:
c复制char str[] = "hello"; // 字符数组,存储在栈上
str[0] = 'H'; // 合法操作
C语言的字符串操作是许多bug的来源,以下是一些常见问题及解决方案:
缓冲区溢出:使用strcpy等不安全的函数
c复制char dest[10];
strcpy(dest, "这个字符串太长了"); // 缓冲区溢出!
解决方案:使用strncpy或更安全的替代方案
c复制strncpy(dest, src, sizeof(dest)-1);
dest[sizeof(dest)-1] = '\0';
忘记终止符:手动构建字符串时忘记添加'\0'
c复制char str[5];
for(int i=0; i<4; i++) str[i] = 'a';
printf("%s", str); // 未定义行为,缺少终止符
字符串比较误用:使用==比较字符串内容
c复制if(str1 == str2) // 比较的是指针地址,不是内容!
正确做法:使用strcmp函数
c复制if(strcmp(str1, str2) == 0)
虽然C语言标准库提供了一系列字符串处理函数,但在现代C项目中,我们通常会采用更安全的替代方案:
使用带长度限制的函数:如snprintf代替sprintf
c复制char buf[100];
snprintf(buf, sizeof(buf), "格式化字符串", args);
使用第三方安全库:如glib的GString或类似的实现
C11新增的安全函数:如strcpy_s、strcat_s等(需编译器支持)
C语言本身不提供动态数组,但我们可以用指针和内存管理函数手动实现:
c复制// 创建动态数组
int *arr = malloc(initial_size * sizeof(int));
if(!arr) {
// 错误处理
}
// 扩容操作
int *temp = realloc(arr, new_size * sizeof(int));
if(temp) {
arr = temp;
} else {
// 错误处理
free(arr);
}
在实际项目中,通常会封装成更高级的数据结构,记录当前容量和元素个数:
c复制typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;
字符串处理是面试和实际开发中的常见任务,这里介绍几个典型算法:
字符串反转:
c复制void reverse(char *str) {
if(!str) return;
char *end = str + strlen(str) - 1;
while(str < end) {
char tmp = *str;
*str++ = *end;
*end-- = tmp;
}
}
atoi实现:
c复制int my_atoi(const char *str) {
int res = 0;
int sign = 1;
while(isspace(*str)) str++;
if(*str == '-') { sign = -1; str++; }
else if(*str == '+') { str++; }
while(isdigit(*str)) {
res = res * 10 + (*str - '0');
str++;
}
return sign * res;
}
c复制char upper_table[256];
for(int i=0; i<256; i++) upper_table[i] = toupper(i);
数组越界是C程序中最常见的错误之一,症状可能表现为:
调试技巧:
c复制assert(index >= 0 && index < array_size);
未终止的字符串:
编码问题:
数组和字符串操作常伴随内存管理问题:
c复制int *arr = malloc(100 * sizeof(int));
// 使用后忘记free(arr)
调试技巧:
在实际项目中,我通常会为数组和字符串操作编写专门的封装函数,加入健全性检查,虽然会增加少量性能开销,但能显著提高代码的健壮性。例如:
c复制// 安全的数组访问函数
int array_get(int *arr, size_t size, size_t index) {
if(index >= size) {
fprintf(stderr, "数组越界访问: size=%zu, index=%zu\n", size, index);
abort();
}
return arr[index];
}
这种防御性编程风格在大型项目中尤为重要,可以尽早发现问题,减少调试时间。