C语言数组详解
1. 一维数组的创建和初始化
在 C 语言中,数组是一种相同类型元素的连续存储结构,由固定数量的元素组成,这些元素在内存中依次排列。通过数组,你可以用一个统一的名称(数组名)和索引来访问各个元素。
(1)数组的创建方式
数组的创建方式:
type_t arr_name [const_n];
//type_t 是指数组的元素类型
// arr_name 是数组名
//const_n 是一个常量表达式,用来指定数组的大小
数组创建的实例:
//代码1
int arr1[10];
//代码2
int count = 10;
//代码3
char arr3[10];
float arr4[1];
double arr5[20];
int arr2[count];//数组时候可以正常创建吗?
在C99标准之前,数组大小必须是常量或常量表达式;C99之后支持变长数组(VLA),大小可以是变量。
示例:
// C99之前:必须用常量指定大小
int arr1[5]; // 正确
int n = 5;
int arr2[n]; // 错误(C99之前)
// C99之后:支持变长数组
int m = 10;
int arr3[m]; // 正确(C99及以后)
注意:变长数组不能初始化,只能先创建后赋值。
(2)数组的初始化
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
看代码:
int arr1[10] = {1,2,3};
int arr2[] = {1,2,3,4};
int arr3[5] = {1,2,3,4,5};
char arr4[3] = {'a',98, 'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";
数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
所以就有了以下两种情况:
- 完全初始化:给所有元素赋值
- 不完全初始化:只给部分元素赋值,剩余元素默认为0
示例:
// 完全初始化
int arr1[5] = {1, 2, 3, 4, 5}; // 每个元素都有值
// 不完全初始化
int arr2[5] = {1, 2}; // 等价于 {1, 2, 0, 0, 0}
// 省略大小(编译器自动计算)
int arr3[] = {1, 3, 5, 7}; // 大小为4
注意:在 C 语言中,用字符串字面量初始化字符数组和逐个字符赋值会导致数组内部结构不同,尤其是在处理字符串结束符’\0’时。
- 字符串字面量初始化(自动包含’\0’)
使用字符串字面量(如"Hello")初始化数组时,C 语言会自动在末尾添加’\0’,因此数组长度需比可见字符数多 1。
char str1[6] = "Hello"; // 包含'H', 'e', 'l', 'l', 'o', '\0'
- 数组长度:必须至少为字符串长度 + 1(否则会导致未定义行为)。
- 自动补’\0’:字符串字面量会隐式包含结束符。
- 可省略长度:若未指定数组长度,编译器会自动计算(包含’\0’):
char str2[] = "World"; // 等价于 char str2[6] = "World";
- 逐个字符赋值(需手动添加’\0’)
若逐个字符赋值,不会自动添加’\0’,需手动添加,否则数组只是普通字符数组,而非 C 风格字符串。
char str3[5] = {'H', 'e', 'l', 'l', 'o'}; // 无'\0',不是有效字符串
char str4[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动添加'\0',是有效字符串
- 非字符串风险:若未添加’\0’,使用字符串函数(如printf、strlen)会导致越界访问,直到遇到内存中的随机’\0’。
- 长度控制:数组长度需明确指定,且需预留’\0’的位置。
对比示例
#include <stdio.h>
#include <string.h>
int main() {
// 字符串字面量初始化(自动包含'\0')
char str1[6] = "Hello";
printf("str1长度(strlen): %zu\n", strlen(str1)); // 输出5(不含'\0')
printf("str1大小(sizeof): %zu\n", sizeof(str1)); // 输出6(含'\0')
// 逐个字符赋值(无'\0')
char str2[5] = {'H', 'e', 'l', 'l', 'o'};
printf("str2长度(strlen): %zu\n", strlen(str2)); // 危险!输出随机值(越界)
printf("str2大小(sizeof): %zu\n", sizeof(str2)); // 输出5(不含'\0')
// 逐个字符赋值(手动添加'\0')
char str3[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("str3长度(strlen): %zu\n", strlen(str3)); // 输出5
printf("str3大小(sizeof): %zu\n", sizeof(str3)); // 输出6
return 0;
}
关键区别总结
初始化方式 | 是否自动添加’\0’ | 数组长度要求 | 是否为有效 C 字符串 |
---|---|---|---|
char s[] = “Hello”; | ✅(自动添加) | 至少为字符串长度+1 | ✅ |
char s[5] = {‘H’,‘e’,…} | ❌(需手动添加) | 至少为字符串长度(若需字符串功能,需 + 1) | ❌(若无’\0’) |
实用建议
- 优先使用字符串字面量:简单且安全,自动处理’\0’。
- 手动赋值时勿忘’\0’:若逐个字符初始化,必须在最后添加’\0’。
- 避免越界:数组长度需足够存放所有字符和’\0’。
- 字符串函数依赖’\0’:strlen、strcpy、printf等函数均通过’\0’判断字符串结束,缺少’\0’会导致严重错误。
2. 一维数组的使用
对于数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。
我们来看代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};//数组的不完全初始化
//计算数组的元素个数
int sz = sizeof(arr)/sizeof(arr[0]);
//对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
int i = 0;//做下标
for(i=0; i<10; i++)//这里写10,好不好?
{
arr[i] = i;
}
//输出数组的内容
for(i=0; i<10; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
总结:
- 数组是使用下标来访问的,下标是从0开始。
- 数组的大小可以通过计算得到。
(1)数组的内存分配
数组在栈区开辟一块连续的内存空间,元素按顺序存放。
示例:
int arr[5] = {10, 20, 30, 40, 50};
内存布局:
地址递增 →
+----+----+----+----+----+
| 10 | 20 | 30 | 40 | 50 |
+----+----+----+----+----+
arr[0] arr[1] arr[2] arr[3] arr[4]
↑
arr (数组名表示首元素地址)
实例:
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
运行结果:
仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。
由此可以得出结论:数组在内存中是连续存放的
。
(2)数组下标
数组下标从0开始,最大下标为数组长度-1
。
示例:
int arr[5] = {1, 3, 5, 7, 9};
printf("%d\n", arr[0]); // 输出1(第1个元素)
printf("%d\n", arr[4]); // 输出9(第5个元素)
(3)计算数组大小
sizeof(数组名)
:计算整个数组的大小(字节)sizeof(数组名[0])
:计算单个元素的大小(字节)- 数组长度 =
sizeof(数组名) / sizeof(数组名[0])
示例:
int arr[5] = {1, 2, 3, 4, 5};
printf("数组总大小:%lu 字节\n", sizeof(arr)); // 输出20(假设int占4字节)
printf("单个元素大小:%lu 字节\n", sizeof(arr[0])); // 输出4
printf("数组长度:%lu\n", sizeof(arr) / sizeof(arr[0])); // 输出5
(4)数组元素的地址
每个元素的地址是其首字节的地址,相邻元素地址相差sizeof(元素类型)
。
示例:
int arr[3] = {100, 200, 300};
printf("arr[0]的地址:%p\n", &arr[0]); // 例如:0x7ffeefbff5a0
printf("arr[1]的地址:%p\n", &arr[1]); // 例如:0x7ffeefbff5a4(相差4字节)
printf("arr[2]的地址:%p\n", &arr[2]); // 例如:0x7ffeefbff5a8(相差4字节)
3. 二维数组的创建和初始化
(1)二维数组的创建
二维数组可以看作“数组的数组”,语法为类型 数组名[行数][列数]
。
示例:
// 创建一个3行3列的二维数组
int arr[3][3];
内存布局:
(2)二维数组的初始化
- 完全初始化:为每个元素赋值
- 不完全初始化:部分元素赋值,剩余补0
- 分组初始化:用大括号分隔每行
示例:
1. 完全初始化(分组)
// 完全初始化(分组)
int arr1[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
2. 完全初始化(不分组)
// 完全初始化(不分组)
int arr2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// 不完全初始化
int arr3[3][4] = {
{1, 2}, // 第1行:1,2,0,0
{5}, // 第2行:5,0,0,0
{9, 10, 11} // 第3行:9,10,11,0
};
3. 省略行数(编译器自动计算)
// 省略行数(编译器自动计算)
int arr4[][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}; // 等价于 arr4[3][4]
注意:二维数组初始化时,列数不能省略,行数可以省略。
(3)二维数组的遍历
使用嵌套循环遍历二维数组,外层循环控制行,内层循环控制列。
示例:
int arr[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("%d ", arr[i][j]);
}
printf("\n"); // 每行结束后换行
}
输出结果:
(4)二维数组在内存中的存储
二维数组在内存中是连续存储的,按行优先排列(先存第一行,再存第二行,依此类推)。
示例:
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
内存布局:
验证地址连续性:
#include <stdio.h>
int main()
{
int arr[3][4];
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
}
}
return 0;
}
运行结果:
(5)为什么行可以省略,列不能省略?
编译器需要列数来计算元素的偏移量。例如,访问arr[i][j]
时,实际地址为:
&arr [0][0] + (i * 列数 + j) * sizeof (元素类型)
示例:
int arr[][3] = { // 列数3不能省略
{1, 2, 3},
{4, 5, 6}
};
// 计算arr[1][2]的地址:
// &arr[0][0] + (1 * 3 + 2) * 4 = &arr[0][0] + 20 字节
如果列数省略,编译器无法确定每行的元素个数,导致无法正确计算地址。
4. 数组越界
数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
(1)什么是数组越界?
访问数组时,下标超过0
到数组长度-1
的范围,称为数组越界。
示例:
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[5]); // 越界访问(合法下标是0~4)
(2)越界的危害
- 读取随机值(访问了其他内存位置的数据)
- 修改其他变量的值(意外覆盖了其他内存)
- 程序崩溃(访问了不可访问的内存区域)
示例:
int a = 100;
int arr[3] = {1, 2, 3};
arr[3] = 200; // 越界写入,可能覆盖a的值(取决于内存布局)
printf("a = %d\n", a); // 可能输出200(如果a的内存被覆盖)
(3)编译器无法检测越界
C语言不检查数组下标是否合法,编译器不会报错,需程序员自己保证。
示例:
// 以下代码编译时不会报错,但运行时可能出错
int arr[5];
for (int i = 0; i <= 5; i++) { // 循环到i=5时越界
arr[i] = i;
}
可以运行(说明编译成功了)但会崩溃
(4)二维数组的越界
二维数组也会发生越界除了正常的越界外
int arr[2][3] = {
{1, 2, 3}, // arr[0][0], arr[0][1], arr[0][2]
{4, 5, 6} // arr[1][0], arr[1][1], arr[1][2]
};
for (int i = 0; i <= 2; i++) { // 应i<2,但此处i<=2导致越界
for (int j = 0; j < 3; j++) {
arr[i][j] = i + j;
}
}
还存在二维数组越界可能发生在行或列的维度上。
示例:
int arr[2][3] = { {1,2,3}, {4,5,6} };
printf("%d\n", arr[2][0]); // 越界(合法行下标0~1)
printf("%d\n", arr[0][3]); // 越界(合法列下标0~2)
注意:即使越界访问的内存地址合法(如刚好访问了其他变量),也会导致数据异常。
5. 数组作为函数参数
往往我们在写代码的时候,会将数组作为参数传个函数
(1)数组名作为参数的本质
数组名作为参数传递时,实际上传递的是首元素的地址(指针),而不是整个数组的副本。
示例:
// 函数定义(两种写法等价)
void print_array(int arr[], int size) { // 写法1:数组形式
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
void print_array(int *arr, int size) { // 写法2:指针形式
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
// 函数调用
int arr[5] = {1, 2, 3, 4, 5};
print_array(arr, 5); // 传递数组名(首元素地址)
(2)为什么不能在函数内部计算数组大小?
函数的形参是指针,sizeof(arr)
计算的是指针大小(通常8字节),不是数组大小。
错误示例:
比如:我要实现一个冒泡排序函数目标是:将一个整形数组排序。
那我们将会这样使用该函数:
//方法1:
#include <stdio.h>
void bubble_sort(int arr[])
{
int sz = sizeof(arr)/sizeof(arr[0]);//这样对吗?
int i = 0;
for(i=0; i<sz-1; i++)
{
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
bubble_sort(arr);//是否可以正常排序?
for(i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
方法1,出现问题,那我们找一下问题,调试之后可以看到 bubble_sort 函数内部的 sz ,是1。难道数组作为函数参数的时候,不是把整个数组的传递过去?
问题分析:
数组作为函数参数时会退化为指针。因此,在函数内部使用sizeof(arr)/sizeof(arr[0])无法正确计算数组长度。
冒泡排序函数的正确设计
当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。
所以即使在函数参数部分写成数组的形式: int arr[] 表示的依然是一个指针: int *arr 。那么,函数内部的 sizeof(arr) 结果是4。
如果 方法1 错了,该怎么设计?
//方法2
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
//代码同上面函数
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);//是否可以正常排序?
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
(3)二维数组作为函数参数
二维数组作为参数时,列数不能省略,因为编译器需要列数来计算元素地址。
示例:
// 正确写法(列数必须指定)
void print_2d_array(int arr[][3], int rows) { // 列数3不能省略
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int arr[2][3] = { {1,2,3}, {4,5,6} };
print_2d_array(arr, 2); // 传递二维数组
return 0;
}
6. 数组名到底是什么?
(1)数组名的本质
数组名在大多数情况下表示首元素的地址,但有两个例外:
sizeof(数组名)
:计算整个数组的大小&数组名
:取整个数组的地址
示例:
int arr[5] = {1, 2, 3, 4, 5};
// 1. 数组名作为首元素地址
printf("首元素地址:%p\n", arr); // 例如:0x7ffeefbff5a0
printf("首元素地址:%p\n", &arr[0]); // 同上
// 2. sizeof(数组名)
printf("整个数组大小:%lu\n", sizeof(arr)); // 输出20(5×4字节)
// 3. &数组名
printf("整个数组的地址:%p\n", &arr); // 数值上等于首元素地址,但类型不同
(2)地址运算的区别
虽然arr
、&arr[0]
和&arr
的值相同,但它们的类型不同,加1后的结果也不同。
示例:
int arr[5] = {1, 2, 3, 4, 5};
printf("arr + 1:%p\n", arr + 1); // 加4字节(跳过1个元素)
printf("&arr[0] + 1:%p\n", &arr[0] + 1); // 同上
printf("&arr + 1:%p\n", &arr + 1); // 加20字节(跳过整个数组)
解释:
arr和&arr[0]的类型是int*,加 1 跳过一个int(4 字节)
&arr的类型是int (*)[5](指向包含 5 个int的数组的指针),加 1 跳过整个数组(20 字节)
(3)二维数组的数组名
二维数组的数组名表示首行的地址,而不是首元素的地址。但它的类型和行为比一维数组更复杂。理解这一点对于正确使用二维数组作为函数参数尤为重要。
1. 二维数组的内存布局
二维数组在内存中是连续存储的,按行优先排列。例如:
int arr[4][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}
};
内存布局为:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|
内存布局:
- 二维数组名的本质
二维数组名arr是一个指向第一行的指针,类型为int ()[3](指向包含 3 个int的数组的指针),而非指向int的指针(int)。
int arr[2][3];
printf("%p\n", arr); // 输出第一行的地址(类型:int (*)[3])
printf("%p\n", arr + 1); // 偏移12字节(一行的大小,即3个int)
printf("%p\n", &arr[0][0]); // 输出首个元素的地址(类型:int*)
- 二维数组作为函数参数
当二维数组作为函数参数时,数组名会退化为指向第一行的指针(int (*)[列数]),因此必须显式指定列数:
// 正确:指定列数
void func(int arr[][3], int rows) {
// ...
}
// 等价写法:使用指针语法
void func(int (*arr)[3], int rows) {
// ...
}
int main() {
int arr[2][3];
func(arr, 2); // 传递数组名(退化为int (*)[3])
}
- 二维数组名与指针的转换关系
表达式 | 类型 | 含义 |
---|---|---|
arr | int (*)[3] | 指向第一行的指针 |
arr[0] | int* | 第一行首元素的地址 |
&arr[0][0] | int* | 首个元素的地址 |
arr + 1 | int (*)[3] | 偏移到下一行(+12 字节) |
arr[0] + 1 | int* | 第一行内偏移到下一个元素 |
总结
- 二维数组名是指向第一行的指针,类型为int (*)[列数]。
- 作为函数参数时,必须显式指定列数,否则会导致类型不匹配。
- 访问元素时,arr[i][j] 等价于 ((arr+i)+j)。
- 动态二维数组需使用指针的指针(int**)并手动管理内存。
7. 一句话总结
概念 | 关键点 | 示例 |
---|---|---|
一维数组创建 | 大小可以是常量(C99前)或变量(C99后) | int arr[5]; int n=10; int arr[n]; |
一维数组初始化 | 不完全初始化时剩余元素补0 | int arr[5] = {1,2}; (等价于{1,2,0,0,0} ) |
二维数组创建 | 必须指定列数,行数可省略 | int arr[][3] = {{1,2,3},{4,5,6}}; |
二维数组初始化 | 按行分组,列数不能省略 | int arr[2][3] = {{1,2},{4}}; (部分补0) |
数组越界 | 编译器不检查,可能导致数据异常或崩溃 | int arr[3]; arr[3] = 100; (越界) |
数组作为参数 | 传递首元素地址,无法在函数内计算数组大小 | void func(int arr[], int size); |
数组名本质 | 通常是首元素地址,但sizeof(数组名) 和&数组名 除外 | sizeof(arr) 计算整个数组大小 |
二维数组名 | 表示首行地址,加1跳过一行 | int arr[2][3]; arr+1 跳过3个int |