数组基本概念与应用场景
数组(Array)是C语言中一种重要的数据结构,它是一组相同类型 的数据元素的集合。这些元素在内存中是连续存储的,通过一个统一的数组名和下标(索引)来访问。
为什么需要数组?
在程序设计中,经常需要处理大量同类型的数据。例如:
- 统计一个班级(40人)的C语言成绩
- 存储一年中每天的湿度数据
- 记录一个图像中所有像素的颜色值
如果使用普通变量,则需要定义大量变量,如:
c
int score1, score2, score3, ..., score40; // 繁琐且难以管理
而使用数组,则可以简化为:
c
int score[40]; // 一次性定义40个整型变量
数组的特点
- 类型相同:所有元素必须是同一数据类型
- 连续存储:在内存中依次存放
- 固定长度:定义时确定元素个数(静态数组)
- 随机访问:通过下标可直接访问任意元素
数组的定义与存储
数组的定义语法
c
存储类型 数据类型 数组名[元素个数];
示例:
c
int a[10]; // 10个整型元素的数组
char b[20]; // 20个字符型元素的数组
float c[5]; // 5个单精度浮点型元素的数组
double d[8]; // 8个双精度浮点型元素的数组
int *ptr[6]; // 6个整型指针的数组
存储类型说明
C语言支持四种存储类型:
- auto:自动存储类型(默认,可省略)
- register:寄存器存储类型(建议编译器将变量存储在寄存器中)
- static:静态存储类型(生命周期为整个程序运行期)
- extern:外部存储类型(声明在其他文件中定义的变量)
数组名命名规则
数组名是一个标识符,必须遵循C语言标识符的规则:
- 由字母、数字、下划线组成
- 不能以数字开头
- 不能使用C语言关键字
- 区分大小写
数组内存大小计算
使用sizeof运算符可以获取数组的总字节大小:
c
#include <stdio.h>
int main() {
int a[5];
printf("数组a的总字节数: %zu\n", sizeof(a)); // 输出 20
printf("int[5]类型的大小: %zu\n", sizeof(int[5])); // 输出 20
// 计算数组元素个数
int element_count = sizeof(a) / sizeof(a[0]);
printf("数组a的元素个数: %d\n", element_count); // 输出 5
return 0;
}
计算原理:
sizeof(a):获取整个数组的字节数sizeof(a[0]):获取单个元素的字节数- 元素个数 = 总字节数 ÷ 单个元素字节数
数组元素的表示与内存布局
数组元素的访问
数组元素通过数组名[下标]的形式访问:
c
int a[5]; // 定义包含5个元素的数组
// 合法访问
a[0] = 10; // 第一个元素
a[1] = 20; // 第二个元素
a[2] = 30; // 第三个元素
a[3] = 40; // 第四个元素
a[4] = 50; // 第五个元素
// 下标越界(危险!)
// a[5] = 60; // 错误!访问了不属于数组的内存空间
下标范围
- 有效下标:
0到元素个数-1 - C语言数组使用零基索引(zero-based indexing)
数组在内存中的布局
数组元素在内存中是连续存储的,可以通过地址验证:
c
#include <stdio.h>
int main() {
int a[5];
printf("数组元素地址:\n");
printf("&a[0] = %p\n", &a[0]);
printf("&a[1] = %p\n", &a[1]);
printf("&a[2] = %p\n", &a[2]);
printf("&a[3] = %p\n", &a[3]);
printf("&a[4] = %p\n", &a[4]);
// 计算地址差值
printf("\n地址差值:\n");
printf("&a[1] - &a[0] = %td (个int大小)\n", &a[1] - &a[0]);
printf("sizeof(int) = %zu 字节\n", sizeof(int));
return 0;
}
典型输出(64位系统):
数组元素地址:
&a[0] = 0x7ffeeb4d4a10
&a[1] = 0x7ffeeb4d4a14 // 相差4字节
&a[2] = 0x7ffeeb4d4a18 // 再相差4字节
&a[3] = 0x7ffeeb4d4a1c
&a[4] = 0x7ffeeb4d4a20
地址差值:
&a[1] - &a[0] = 1 (个int大小)
sizeof(int) = 4 字节
下标法的本质
a[i]实际上被编译器转换为:*(a + i),即:
a是数组首元素的地址a + i是第i个元素的地址*(a + i)是获取该地址的内容
数组的初始化方法
标准初始化(完全初始化)
c
int a[5] = {1, 2, 3, 4, 5}; // 完全初始化
等价于:
c
a[0] = 1;
a[1] = 2;
a[2] = 3;
a[3] = 4;
a[4] = 5;
自动推断数组大小
可以省略数组大小,编译器根据初始化列表自动推断:
c
int a[] = {1, 2, 3, 4, 5}; // 编译器自动确定a有5个元素
int b[] = {0}; // 编译器自动确定b有1个元素,值为0
部分初始化
如果初始化列表中的元素个数少于数组大小,剩余元素自动初始化为0:
c
int a[5] = {1, 2, 3}; // a[0]=1, a[1]=2, a[2]=3, a[3]=0, a[4]=0
int b[10] = {0}; // 全部元素初始化为0的简洁写法
指定下标初始化(C99标准)
可以使用指定下标的方式初始化特定元素:
c
int a[10] = {[2] = 100, [5] = 200, [9] = 300};
// 等价于:
// a[0]=0, a[1]=0, a[2]=100, a[3]=0, a[4]=0,
// a[5]=200, a[6]=0, a[7]=0, a[8]=0, a[9]=300
字符数组的特殊初始化
字符数组有多种初始化方式:
c
char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 字符列表
char str2[] = "Hello"; // 字符串字面量
char str3[10] = "Hello"; // 部分初始化,剩余为'\0'
地址与指针基础
地址的概念
计算机内存被划分为若干字节(byte),每个字节有唯一编号,称为地址(Address)。
c
int a = 10;
printf("变量a的值: %d\n", a); // 输出: 10
printf("变量a的地址: %p\n", &a); // 输出: 类似0x7ffeeb4d4a0c
指针变量
指针是存储地址的变量,声明语法:数据类型 *指针变量名
c
int a = 10;
int *p = &a; // p是指向int的指针,存储a的地址
printf("a的值: %d\n", a); // 直接访问
printf("a的值: %d\n", *p); // 通过指针间接访问
常见地址类型
c
int a; // &a 的类型是 int*
char b; // &b 的类型是 char*
float c; // &c 的类型是 float*
int arr1[5]; // &arr1 的类型是 int(*)[5](指向含5个int的数组的指针)
int arr2[3][4]; // &arr2 的类型是 int(*)[3][4](指向二维数组的指针)
int *ptr; // &ptr 的类型是 int**(指向指针的指针)
取地址&与解引用*运算符
&:取地址运算符,获取变量的内存地址*:解引用运算符,通过指针访问指向的内存
c
int a = 10;
int *p = &a;
// & 和 * 互为逆运算
printf("a = %d\n", a); // 10
printf("*&a = %d\n", *&a); // 10
printf("&*p = %p\n", &*p); // 与&a相同
printf("*p = %d\n", *p); // 10
指针的算术运算
指针可以进行加减整数运算,运算的单位是指针指向类型的大小:
c
int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[0]; // p指向arr[0]
printf("*p = %d\n", *p); // 10
printf("*(p+1) = %d\n", *(p+1));// 20
printf("*(p+2) = %d\n", *(p+2));// 30
// 指针相减得到元素个数差
int *q = &arr[4];
printf("q - p = %td\n", q - p); // 4
数组与地址运算
数组名的双重身份
数组名在大多数情况下表示数组首元素的地址:
c
int arr[5] = {1, 2, 3, 4, 5};
printf("arr = %p\n", arr); // 数组名作为地址
printf("&arr[0] = %p\n", &arr[0]); // 首元素地址
printf("arr == &arr[0]? %s\n", arr == &arr[0] ? "是" : "否"); // 是
数组名作为指针使用
c
int arr[5] = {10, 20, 30, 40, 50};
// 以下四种访问方式等价:
printf("%d\n", arr[2]); // 下标法
printf("%d\n", *(arr + 2)); // 指针法
printf("%d\n", 2[arr]); // 罕见但合法的写法
printf("%d\n", *(2 + arr)); // 指针法变体
数组指针与指针数组的区别
这是C语言中容易混淆的两个概念:
c
// 指针数组:每个元素都是指针
int *ptr_arr[5]; // 包含5个int指针的数组
// 数组指针:指向数组的指针
int (*arr_ptr)[5]; // 指向包含5个int的数组的指针
使用指针遍历数组
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p;
// 方法1:使用数组下标
printf("方法1 - 下标遍历:\n");
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// 方法2:使用指针遍历
printf("\n方法2 - 指针遍历:\n");
for (p = arr; p < arr + 5; p++) {
printf("*p = %d, 地址: %p\n", *p, p);
}
return 0;
}
数组作为函数参数
数组作为函数参数时,实际传递的是数组首元素的地址:
c
#include <stdio.h>
// 函数接收数组的指针和大小
void print_array(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]); // 等价于 *(arr + i)
}
printf("\n");
}
// 等价写法
void print_array2(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int nums[5] = {1, 2, 3, 4, 5};
print_array(nums, 5); // 传递数组名(首地址)
print_array2(nums, 5); // 等价调用
return 0;
}
地址运算示例分析
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
printf("arr = %p\n", arr); // 首元素地址
printf("arr + 1 = %p\n", arr + 1); // 下一个int的地址
printf("&arr = %p\n", &arr); // 整个数组的地址
printf("&arr + 1 = %p\n", &arr + 1);// 跳过整个数组
printf("\n地址差值:\n");
printf("(arr + 1) - arr = %td\n", (arr + 1) - arr); // 1个int
printf("(&arr + 1) - &arr = %td\n", (&arr + 1) - &arr); // 1个数组
printf("\n大小计算:\n");
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 整个数组大小
printf("sizeof(&arr) = %zu\n", sizeof(&arr)); // 指针大小
return 0;
}
常见错误与注意事项
下标越界
这是最常见的数组错误:
c
int arr[5] = {1, 2, 3, 4, 5};
// 错误示例
arr[5] = 6; // 越界!有效下标是0-4
arr[-1] = 0; // 越界!下标不能为负
// 循环中的常见越界
for (int i = 0; i <= 5; i++) { // i<=5 会导致最后一次访问arr[5]
arr[i] = i * 10;
}
未初始化的数组
c
int arr[5]; // 未初始化,值是不确定的(垃圾值)
printf("%d\n", arr[0]); // 可能输出任意值
数组名是常量指针
数组名不是变量,不能修改:
c
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
// arr = p; // 错误!数组名不能作为左值
// arr++; // 错误!数组名不能自增
// arr = arr+1; // 错误!不能修改数组名
p = arr + 1; // 正确!p是变量
p++; // 正确!p是变量
数组大小必须是编译时常量
在标准C中,数组大小必须是常量表达式:
c
#define SIZE 10 // 使用宏定义
const int N = 20; // C99之前不能用于数组大小
int arr1[SIZE]; // 正确
// int arr2[N]; // C89/C90错误,C99正确(变长数组)
int arr3[10]; // 正确
// int n = 30;
// int arr4[n]; // C99变长数组,C89不支持
多维数组的地址运算
c
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printf("arr = %p\n", arr); // 整个二维数组地址
printf("arr[0] = %p\n", arr[0]); // 第一行地址
printf("&arr[0][0] = %p\n", &arr[0][0]); // 第一个元素地址
printf("\n地址运算:\n");
printf("arr + 1 = %p\n", arr + 1); // 下一行地址
printf("arr[0] + 1 = %p\n", arr[0] + 1); // 下一个元素地址