【C语言】 数组基础与地址运算

数组基本概念与应用场景

数组(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);   // 下一个元素地址
相关推荐
im_AMBER1 小时前
Leetcode 112 两数相加 II
笔记·学习·算法·leetcode
long3161 小时前
KMP模式搜索算法
数据库·算法
wuguan_1 小时前
C#/VP联合编程之绘制图像与保存
开发语言·c#
Howrun7771 小时前
C++_错误处理
开发语言·c++
_OP_CHEN2 小时前
【算法基础篇】(五十三)隔板法指南:从 “分球入盒” 到不定方程,组合计数的万能解题模板
算法·蓝桥杯·c/c++·组合数学·隔板法·acm/icpc
近津薪荼2 小时前
优选算法——滑动窗口3(子数组)
c++·学习·算法
遨游xyz2 小时前
数据结构-栈
java·数据结构·算法
ghie90902 小时前
基于动态规划算法的混合动力汽车能量管理建模与计算
算法·汽车·动态规划
蓝海星梦2 小时前
GRPO 算法演进——裁剪机制篇
论文阅读·人工智能·深度学习·算法·自然语言处理·强化学习