一维数组
数组是相同类型数据元素的有序集合,通过下标(索引)访问元素,内存中连续存储。
数组名表示首元素地址,sizeof(arr) 返回整个数组的字节大小
核心特点
- 元素类型一致:所有元素必须为同一数据类型(如
int,float)。 - 固定大小:数组长度在声明时确定,静态数组无法动态调整。
- 下标访问:下标从
0开始,最大下标为长度-1。
一维数组的定义与初始化
基本语法
数据类型 数组名[长度];// 声明未初始化的数组
数据类型 数组名[长度] = {值列表};// 声明并初始化
示例:
int numbers[5];// 声明长度为5的整型数组(元素值不确定)
float scores[3] = {8.5, 9.0, 7.8};// 完全初始化
char vowels[] = {'a', 'e', 'i', 'o', 'u'};// 自动推断长度为5
初始化规则
-
未完全初始化:剩余元素自动填充零(数值类型)或空字符(字符类型)。
int arr[5] = {1, 2};// [1, 2, 0, 0, 0] char str[10] = "Hi";// ['H','i','\0',0,...,0],\0是字符串数组的结束符(必须有) -
静态初始化(全局数组):未显式初始化时,默认全零。
static int global_arr[3];// [0, 0, 0]
数组越界初始化
在C语言中,数组的初始化分为声明时初始化和动态初始化。越界问题在这两种情况下有不同的表现。
声明时的初始化(使用初始化列表)
定义:指在声明数组的同时用大括号 {} 提供初始值列表时,提供的值的个数超过了数组声明的长度。
例子:
#include <stdio.h>
int main()
{
// 声明一个长度为5的数组,但尝试用6个值初始化它
int arr[5] = {1, 2, 3, 4, 5, 6};// 编译器会报错!
return 0;
}
- 后果:这是一种编译时错误。编译器(如GCC)会直接报错,提示类似
excess elements in array initializer(初始化列表中的元素超出了数组的范围)。程序无法通过编译,无法生成可执行文件。 - 原因:数组的大小在编译时是固定的,编译器可以明确检查出初始值的数量是否匹配。
动态初始化(运行时赋值)
定义:指在程序运行时,通过循环或其他逻辑为数组元素赋值时,循环变量或索引计算错误,导致试图对数组范围之外的内存进行赋值。
例子:
#include <stdio.h>
int main() {
int arr[5];
int i;
// 错误:循环了6次 (i从0到5),最后一次arr[5]是越界写入
for(i = 0; i <= 5; i++) {// 应该是 i < 5
arr[i] = i * 10;
}
return 0;
}
- 后果:这是一种运行时错误,属于未定义行为。编译器通常不会报错(最多可能给出警告),但程序运行时的行为是不可预测的。它可能会覆盖其他重要的数据(如其他变量、函数调用的返回地址),导致程序崩溃或产生诡异的结果。
逐个赋值
声明数组后,通过循环或直接索引为每个元素赋值:
int arr[5];// 声明未初始化的数组
// 逐个赋值
arr[0] = 10;// 第一个元素
arr[1] = 20;
arr[2] = 30;
arr[3] = 40;
arr[4] = 50;
// 或通过循环赋值
for (int i = 0; i < 5; i++) {
arr[i] = (i + 1) * 10;// 赋值 10, 20, 30, 40, 50
}
数组元素的操作
访问元素
通过下标(索引)访问,范围从 0 到 长度-1:
int data[4] = {10, 20, 30, 40};
printf("%d\n", data[0]);// 输出10
data[3] = 50;// 修改最后一个元素为50
遍历数组
使用 for 循环遍历所有元素:
for (int i = 0; i < sizeof(data)/sizeof(data[0]); i++) {
printf("data[%d] = %d\n", i, data[i]);
}
- 关键技巧:用
sizeof(data)/sizeof(data[0])计算数组长度。
数组访问越界
这是C语言中最常见且最危险的错误之一。
定义:指在使用数组时,下标索引的值不在 [0, 数组长度 - 1] 这个有效范围内。包括读越界和写越界。
例子:
#include <stdio.h>
int main()
{
int arr[5] = {10, 20, 30, 40, 50};// 有效索引: 0, 1, 2, 3, 4
// 1. 读越界 - 读取不存在的数据
printf("越界读取: %d\n", arr[5]);// 索引5无效
printf("越界读取: %d\n", arr[-1]);// 索引-1无效
// 2. 写越界 - 向非法内存地址写入数据(极其危险!)
arr[5] = 100;// 破坏未知的内存
arr[100] = 200;// 更严重的破坏
return 0;
}
字符数组 与 字符串 的对比
| 对比项 | 字符数组 | 字符串 |
|---|---|---|
| 本质 | 存储字符序列的一维数组,元素类型为 char(纯粹的字符集合)。 | 以 '\0'(空字符)结尾的特殊字符数组(本质是字符数组的子集,附加终止符约束)。 |
| 终止符 | 可无 '\0'(仅作字符集合时);若用于字符串场景需手动添加。 | 必须包含 '\0'(否则字符串函数如 strlen 会越界访问,导致未定义行为)。 |
| 初始化方式 | - 显式列表:char arr1[] = {'H', 'i'};(无终止符,长度=2)- 字符串赋值: char arr2[] = "Hi";(自动添加 '\0',长度=3) | 只能通过字符串字面量初始化(隐式带 '\0'):char str[] = "Hello";(长度=6,含终止符) |
| 内存占用 | 长度 = 初始化字符数(无终止符时)或 字符数+1(含自动添加的 '\0' 时)。 | 长度 = 可见字符数 + 1(固定多1字节存放 '\0',如 "abc" 占4字节)。 |
| 操作函数 | 无专用函数,需手动遍历(如 for 循环逐个处理字符)。 | 可直接使用标准库函数: - 长度计算: strlen()- 复制: strcpy()- 拼接: strcat() |
| 常见用途 | 存储原始字节数据(如二进制缓冲区、非文本编码数据)、固定长度字符集合。 | 文本处理(如用户输入、文件内容、字符串拼接/比较)、需用字符串函数操作的场景。 |
| 长度计算 | sizeof(arr):返回数组总字节数(含所有元素,包括可能的 '\0')。 | strlen(str):返回 '\0' 前的字符数(不含终止符,如 "abc" 返回3)。 |
| 修改权限 | 非 const 修饰时,可自由修改任意元素(如 arr[0] = 'h')。 | 字符串字面量(如 "Hello")存储在只读区(.rodata),不可修改;数组形式字符串(char str[] = "...")可修改。 |
| 输入输出 | 需逐个字符处理(如 for 循环打印),或指定长度(printf("%.2s", arr))。 | 可直接用 %s 格式化输入输出(自动识别 '\0' 终止):printf("%s", str); |
二维数组
二维数组的定义与初始化
基本语法
数据类型 数组名[行数][列数];// 声明未初始化的二维数组
数据类型 数组名[行数][列数] = {初始化值};// 声明并初始化
-
示例:
int matrix[3][4];// 声明3行4列的整型数组(元素值不确定) float grid[2][3] = {{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}};// 完全初始化 char chessboard[][3] = {{'X', 'O'}, {'O', 'X'}};// 自动推断行数为2(列数表示元素类型列数不能不声明)
初始化规则
-
逐行初始化:内层花括号对应行,可省略内部括号(按顺序填充)。
int arr[2][3] = {1, 2, 3, 4, 5, 6};// 等效于 {{1,2,3}, {4,5,6}} -
部分初始化:未初始化的元素自动填充零。
int arr[2][3] = {{1}, {4, 5}};// 第一行 [1,0,0],第二行 [4,5,0]
二维数组的内存布局
存储方式
二维数组在内存中按 行优先顺序 连续存储:
int arr[2][3] = {{1,2,3}, {4,5,6}};
内存布局(假设 int 占4字节):
地址: 0x1000 → 1 (arr[0][0])
地址: 0x1004 → 2 (arr[0][1])
地址: 0x1008 → 3 (arr[0][2])
地址: 0x100C → 4 (arr[1][0])
地址: 0x1010 → 5 (arr[1][1])
地址: 0x1014 → 6 (arr[1][2])
计算元素地址
对于 arr[row][col],其内存地址为:
基地址 + (row * 列数 + col) * sizeof(数据类型)
二维数组的常见操作
遍历数组
嵌套循环访问所有元素:
for (int i = 0; i < 行数; i++) {
for (int j = 0; j < 列数; j++) {
printf("arr[%d][%d] = %d\n", i, j, arr[i][j]);
}
}
变长数组
核心特性:一旦创建,其大小就固定了,不能再改变。 这里的“变长”指的是“长度由变量决定”,而不是“长度可以变化”。
基本语法和示例
#include <stdio.h>
int main()
{
int size;
printf("请输入数组的大小: ");
scanf("%d", &size);// 运行时才确定大小
// 声明一个变长数组 (Variable-Length Array, VLA)
// 其长度由变量size的值决定
int myArray[size];
// 像普通数组一样使用
for (int i = 0; i < size; i++) {
myArray[i] = i * 2;
}
for (int i = 0; i < size; i++) {
printf("myArray[%d] = %d\n", i, myArray[i]);
}
return 0;
}
主要特点
长度由变量决定:数组的长度可以是任何整数表达式(只要结果为正数),而不仅仅是常量。
int n = 10;
int m = 5;
float arr1[n];// 合法
float arr2[n*2 + 1];// 合法
float arr3[m*n];// 合法
通常分配在栈上:与普通自动(局部)数组一样,VLA通常在函数调用栈上分配内存。这是其最大的限制之一。
作用域和生命周期:VLA是自动存储期的对象。它的生命周期仅限于其所在的作用域(例如,函数内部或块内部)。当程序离开声明它的块时,数组占用的内存会自动被释放。
不允许在文件作用域或静态存储期使用:
// int global_vla[n]; // 错误!VLA不能是全局的
// static int static_vla[n]; // 错误!VLA不能是static的
int main()
{
// 只能在块作用域内使用
int n = 5;
int vla[n];// 正确
}
不能初始化:声明VLA时不能同时进行初始化。
int n = 5;
int vla[n] = {1, 2, 3, 4, 5};// 错误!编译不通过
零长数组
零长数组是一种在结构体中声明的、长度为零的数组。它的主要用途是作为结构体的最后一个成员,指向一段动态分配的内存空间,从而使整个结构体能够容纳可变长度的数据。
基本形式:
struct my_struct
{
int some_data;
char flexible_data[0];// 这就是零长数组
};
在C语言中,结构体的大小通常是固定的。但有时我们需要一个结构体来存储一个可变长度的数据(例如,一个可变长度的字符串、一个动态数量的数组等)。
不优雅的解决方案(过去):
在零长数组出现之前,程序员常用一种称为“struct hack”的技巧,即声明一个长度为1的数组。
struct my_struct
{
int length;
char data[1];// 长度为1的数组
};
然后动态分配内存时,分配 sizeof(struct my_struct) + (actual_length - 1) 的大小。这种方法可行,但语法上不美观,且标准未明确支持。
零长数组的解决方案:
零长数组让这个“Hack”变得更加直观和自然。它明确表示:“这个数组没有占用结构体本身的空间,它只是另一个内存区域的起点”。
使用零长数组的标准步骤是:
定义结构体:将零长数组作为最后一个成员。
动态分配内存:为结构体本身 加上 你希望零长数组拥有的实际长度分配内存。
使用数据:像使用普通数组一样使用零长数组成员。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义包含零长数组的结构体
struct line
{
int length;
char contents[0];// 零长数组
};
int main()
{
int desired_length = 20;
// 动态分配内存:结构体基础大小 + 所需的数组大小
struct line *thisline = (struct line *)malloc(sizeof(struct line)
+ desired_length * sizeof(char));
thisline->length = desired_length;
// 现在可以使用 contents 数组了,它拥有 20 个字符的空间
strncpy(thisline->contents, "Hello, World!", desired_length);
printf("Content: %s\n", thisline->contents);
printf("Length: %d\n", thisline->length);
// 释放内存free(thisline);
return 0;
}
数组与数组名
数组名(Array Name)
数组名是一个标识符,它代表整个数组。但在大多数表达式中,它会自动转换为指向其首元素的指针。
int arr[5] = {10, 20, 30, 40, 50};
arr 的含义:
- 类型:
int*(指向整型的指针) - 值:
&arr[0](数组第一个元素的地址) - 指针运算:以单个元素为单位进行偏移
示例:
printf("arr: %p\n", (void*)arr);// 输出数组首元素地址
printf("&arr[0]: %p\n", (void*)&arr[0]);// 输出数组首元素地址
// 两者输出相同
printf("*arr: %d\n", *arr);// 输出 10 (解引用得到第一个元素)
printf("*(arr + 1): %d\n", *(arr + 1));// 输出 20 (向后移动一个int的大小)
&数组名(Address of Array Name)
&arr 获取的是整个数组的地址。
&arr 的含义:
- 类型:
int (*)[5](指向长度为5的整型数组的指针) - 值:与
arr相同(因为数组的地址就是其首元素的地址) - 指针运算:以整个数组为单位进行偏移
示例:
printf("&arr: %p\n", (void*)&arr);// 输出整个数组的地址// 这个值与 arr 相同,但类型不同
// 指针运算的差异:
printf("arr + 1: %p\n", (void*)(arr + 1));// 地址增加 4 字节(一个int的大小)
printf("&arr + 1: %p\n", (void*)(&arr + 1));// 地址增加 20 字节(5个int的大小,即整个数组的大小)
对比表格
| 特性 | arr (数组名) | &arr (数组的地址) |
|---|---|---|
| 类型 | int* | int (*)[5] |
| 指向对象 | 第一个元素 arr[0] | 整个数组 arr |
| 值(地址) | 与 &arr[0] 相同 | 与 arr 相同 |
| +1 的偏移量 | sizeof(int) 字节 | sizeof(int[5]) 字节(即20字节) |
| 主要用途 | 访问数组元素 | 操作整个数组(如传递给函数) |
219

被折叠的 条评论
为什么被折叠?



