数组
在 C 语言中,数组(Array)是用来存储一组相同类型数据的集合。你可以把它想象成一排连续的储物柜,每个柜子(元素)都有一个唯一的编号(下标),并且所有柜子的大小和类型都一样。
📦 一维数组
一维数组是最基础的数组形式。
定义与初始化
定义数组的语法是:数据类型 数组名[数组长度];
int scores[5]; // 定义一个能存储5个整数的数组
你可以在定义时直接为数组赋值,这叫做初始化。
// 1. 完全初始化:为所有元素赋值
int arr1[5] = {10, 20, 30, 40, 50};
// 2. 部分初始化:未赋值的元素会自动变为0
int arr2[5] = {1, 2}; // 结果为 {1, 2, 0, 0, 0}
// 3. 省略长度:编译器根据元素个数自动确定长度
int arr3[] = {5, 4, 3, 2, 1}; // 长度自动为5
// 4. 常用技巧:将所有元素初始化为0
int arr4[10] = {0};
访问与遍历
数组元素通过下标 来访问,并且下标从 0 开始。这意味着一个长度为 n 的数组,其有效下标范围是 0 到 n-1。
int arr[5] = {10, 20, 30, 40, 50};
// 访问第一个元素
int first = arr[0]; // first 的值为 10
// 修改第三个元素
arr[2] = 100; // 数组变为 {10, 20, 100, 40, 50}
通常使用 for 循环来遍历数组,对每个元素进行操作。
计算数组长度
一个计算数组长度的通用公式是 sizeof(数组名) / sizeof(数组名[0])。
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int length = sizeof(arr) / sizeof(arr[0]); // 结果为 9
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 遍历并打印数组
for(int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
// 输出: 10 20 30 40 50
return 0;
}
📊 二维数组
二维数组可以看作是一个表格,有行和列。
定义与初始化
定义语法是:数据类型 数组名[行数][列数];
int matrix[3][4]; // 定义一个3行4列的二维数组
初始化时,可以按行分组赋值。
// 按行初始化
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
访问与遍历
访问二维数组元素需要两个下标:数组名[行下标][列下标]。通常使用双重循环来遍历。
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
// 使用双重循环遍历
for(int i = 0; i < 2; i++) { // 遍历行
for(int j = 0; j < 3; j++) { // 遍历列
printf("%d ", arr[i][j]);
}
printf("\n"); // 每打印完一行后换行
}
return 0;
}
// 输出:
// 1 2 3
// 4 5 6
⚠️ 重要注意事项
- 数组越界 :C 语言不会自动检查数组下标是否越界。访问
arr[n](n为数组长度)或更大的下标是非法的,可能导致程序崩溃或产生不可预知的错误。 - 内存连续:数组的所有元素在内存中是连续存放的。
- 类型统一:一个数组只能存储一种数据类型。
- 长度固定:数组在定义后,其长度就不能改变了。
指针和数组
在 C 语言中,指针和数组的关系极为紧密,甚至可以说数组在底层就是通过指针来实现的。理解它们之间的联系是掌握 C 语言的关键一步。
🔗 核心关系:数组名即地址
理解指针与数组关系的核心在于一句话:在绝大多数情况下,数组名代表数组首元素的地址。
例如,对于一个数组 int arr[5];,表达式 arr 的值就等同于 &arr[0],即第一个元素 arr[0] 的内存地址。
基于这一点,数组的访问方式与指针产生了深刻的等价关系。
核心等价公式
arr[i] 这种我们熟悉的下标访问方式,在编译器看来,本质上就是指针的"偏移+解引用"操作。
arr[i] <===> *(arr + i)
arr:数组首元素的地址。arr + i:从首地址开始,向后偏移i个元素的位置。*(arr + i):取出该位置上的值。
因此,以下两种写法是完全等价的:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d", arr[2]); // 输出 30
printf("%d", *(arr + 2)); // 同样输出 30
🤝 指针如何操作数组
利用上述关系,我们可以使用指针来灵活地访问和操作数组。
1. 指针遍历数组
使用指针遍历数组是一种非常高效和常见的写法。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指针 p 指向数组首元素
// 方法一:指针偏移
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
printf("\n");
// 方法二:移动指针本身
for (int i = 0; i < 5; i++) {
printf("%d ", *p);
p++; // 指针向后移动一个元素的位置
}
return 0;
}
2. 数组作为函数参数
当把一个数组传递给函数时,实际上传递的并不是整个数组的副本,而仅仅是首元素的地址。
这意味着,在函数内部,形参 int arr[] 和 int *arr 是完全等价的。
#include <stdio.h>
// 以下两种函数声明方式是等价的
void printArray1(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
void printArray2(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i));
}
}
int main() {
int myArr[] = {1, 2, 3};
// 传递数组名,即首元素地址
printArray1(myArr, 3);
printArray2(myArr, 3);
return 0;
}
重要提示 :正因为传递的是指针,所以在函数内部使用 sizeof(arr) 得到的是指针变量自身的大小(通常是 4 或 8 字节),而不是整个数组的大小。因此,通常需要额外传递一个参数来告知数组的长度。
⚖️ 指针与数组的关键区别
尽管关系紧密,但指针和数组在本质上是不同的。
| 特性 | 数组 | 指针 |
|---|---|---|
| 定义 | 同类型数据的集合 | 存储另一个变量地址的变量 |
| 内存分配 | 编译时静态分配(通常在栈上) | 可以动态分配(在堆上) |
| 大小 | 固定,定义后无法改变 | 可以指向任意大小的内存块 |
| 可修改性 | 数组名是常量地址,不能被修改(如 arr++ 非法) |
指针变量可以修改,指向其他地址(如 ptr++ 合法) |
⚠️ 两个特殊例外
虽然 数组名 通常代表首元素地址,但在以下两种情况下,它代表的是整个数组:
sizeof(数组名):计算的是整个数组占用的总字节数。&数组名:取出的是整个数组的地址。
这个区别体现在指针运算上:
arr + 1:地址值增加一个元素 的大小(例如int类型增加 4 字节)。&arr + 1:地址值增加整个数组 的大小(例如int arr[10]增加 40 字节)
🧩 易混淆概念:数组指针 vs. 指针数组
这是 C 语言中两个名字相似但含义完全不同的概念。一个简单的记忆口诀是:看最后两个字,它是什么,本质就是什么。
| 写法 | 名字 | 本质 | 记忆方法 |
|---|---|---|---|
int (*p)[5] |
数组指针 | 一个指针,指向一个包含5个int的数组 | 有括号,p先和*结合,所以是指针 |
int *p[5] |
指针数组 | 一个数组,包含5个指向int的指针 | 无括号,p先和[]结合,所以是数组 |
- 数组指针常用于处理二维数组。
- 指针数组 常用于管理多个字符串,例如
char *courses[] = {"语文", "数学", "英语"};。
指针操作
在 C 语言中,指针操作是其核心和灵魂。掌握指针操作,意味着你能够直接管理内存,写出更高效、更灵活的代码。
🎯 基础操作:取地址与解引用
这是指针操作的基石,涉及两个核心运算符:& 和 *。
-
取地址 (
&):获取一个变量在内存中的地址。 -
解引用 (
*):通过一个指针,访问它所指向地址上的值。#include <stdio.h>
int main() {
int num = 10;
int *ptr = # // 1. 使用 & 获取 num 的地址,并存入指针 ptrprintf("num 的地址: %p\n", (void*)&num); // 打印地址 printf("ptr 的值: %p\n", (void*)ptr); // ptr 的值就是 num 的地址 printf("num 的值: %d\n", num); // 直接访问 num printf("ptr 指向的值: %d\n", *ptr); // 2. 使用 * 解引用 ptr,得到 num 的值 *ptr = 20; // 3. 通过解引用修改 ptr 指向的内存,等价于 num = 20; printf("修改后 num 的值: %d\n", num); // 输出 20 return 0;}
🧮 指针运算
指针可以进行算术运算,但其行为与普通整数不同,它受到指针类型的影响。
1. 指针 ± 整数
指针加或减一个整数 n,意味着地址向前或向后移动 n 个它所指向的数据类型的大小。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向 arr[0]
p++; // p 向后移动一个 int 的大小 (通常是4字节),现在指向 arr[1]
printf("%d\n", *p); // 输出 20
p += 2; // p 再向后移动两个 int 的大小,现在指向 arr[3]
printf("%d\n", *p); // 输出 40
关键点 :char* 加 1 移动 1 字节,而 int* 加 1 移动 4 字节(在大多数系统上)。
2. 指针 - 指针
两个指向同一数组的指针可以相减,结果是它们之间相隔的元素个数。
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[1]; // 指向 2
int *p2 = &arr[4]; // 指向 5
int diff = p2 - p1; // 计算元素个数差
printf("%d\n", diff); // 输出 3
🛡️ 安全与限定:const 与野指针
编写健壮的 C 代码,必须理解如何安全地使用指针。
const 修饰指针
const 关键字用来限定指针的修改权限。一个简单口诀是:const 在 * 左边,限制内容;const 在 * 右边,限制指针本身。
| 写法 | 含义 | 能否修改指向的内容 (*p = val) |
能否修改指针本身 (p = &other) |
|---|---|---|---|
int *p |
普通指针 | ✅ 能 | ✅ 能 |
const int *p |
指向常量的指针 | ❌ 不能 | ✅ 能 |
int * const p |
常量指针 | ✅ 能 | ❌ 不能 |
const int * const p |
指向常量的常量指针 | ❌ 不能 | ❌ 不能 |
野指针
野指针是指向未知或无效内存地址的指针。使用野指针会导致程序崩溃或产生不可预知的错误。
常见成因:
-
未初始化 :声明后未赋予有效地址。
int *p; // 野指针!p 的值是随机的 *p = 10; // 危险操作!
-
访问已释放的内存 :
free()之后没有将指针置为NULL。cint *p = (int*)malloc(sizeof(int)); free(p); // p 仍然是野指针,它仍保存着已释放内存的地址 *p = 20; // 危险操作!
-
返回局部变量的地址 :函数返回后,其局部变量占用的栈内存会被释放。
int* getPtr() { int local = 10; return &local; // 返回野指针! }
🧠 进阶操作
动态内存分配
C 语言允许在程序运行时动态地申请和释放内存,这对于创建大小不固定的数据结构(如动态数组、链表)至关重要。
#include <stdio.h>
#include <stdlib.h> // malloc 和 free 的头文件
int main() {
// 1. 申请内存:分配 5 个 int 大小的空间
int *ptr = (int*)malloc(5 * sizeof(int));
if (ptr == NULL) { // 检查内存是否分配成功
printf("内存分配失败\n");
return 1;
}
// 2. 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i * 10;
}
// 3. 释放内存:使用完毕后必须释放,防止内存泄漏
free(ptr);
ptr = NULL; // 好习惯:释放后立即置为 NULL
return 0;
}
函数指针
函数指针是一个指向函数的指针。通过它,我们可以将函数作为参数传递,实现回调机制,这在事件处理和通用算法(如 qsort)中非常有用
#include <stdio.h>
// 一个简单的加法函数
int add(int a, int b) {
return a + b;
}
int main() {
// 1. 定义一个函数指针,指向参数为(int, int),返回值为int的函数
int (*func_ptr)(int, int);
// 2. 将函数 add 的地址赋给 func_ptr
func_ptr = &add; // 或者 func_ptr = add;
// 3. 通过函数指针调用函数
int result = func_ptr(3, 4); // 等价于 add(3, 4)
printf("结果: %d\n", result); // 输出 7
return 0;
}
指针和多维数组
在 C 语言中,指针与多维数组(特别是最常用的二维数组)的结合是进阶学习的一道坎。
与一维数组不同,多维数组在内存中虽然也是线性排列的,但为了保持"行"和"列"的逻辑结构,指针的操作需要更严格的类型匹配。
🧠 核心概念:行优先与内存布局
首先你需要建立一个核心认知:C 语言中的多维数组在内存中是"按行优先"连续存储的。
例如一个 int arr[2][3](2行3列)的数组:
- 逻辑上:是一个表格。
- 物理上 :内存里依次存放
arr[0][0],arr[0][1],arr[0][2], 紧接着是arr[1][0],arr[1][1],arr[1][2]。
🎯 关键角色:数组指针
这是操作二维数组最标准、最规范的指针类型。
什么是数组指针?
它是一个指针 ,指向的是一个数组(即二维数组中的一行)。
- 定义语法 :
类型 (*指针名)[列数]; - 注意 :括号
(*指针名)必不可少,且必须指定列数(除了第一维,其他维度的长度必须明确)。
为什么必须指定列数?
因为编译器需要知道"一行有多长",才能正确计算地址。
当你执行 p + 1 时,指针不是跳过 1 个字节,也不是跳过 1 个 int,而是跳过整整一行 (即 列数 * sizeof(类型) 个字节)。
代码示例
#include <stdio.h>
int main() {
// 定义一个2行3列的二维数组
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 定义一个数组指针,指向包含3个int的数组
// 这里的 [3] 必须与 arr 的列数一致
int (*p)[3] = arr;
// 访问元素
printf("%d\n", p[0][0]); // 输出 1
printf("%d\n", *(*(p + 1) + 1)); // 输出 5 (第1行第1列)
// 指针移动
p++; // p 向下移动了一整行,现在指向 arr[1]
return 0;
}
🆚 易混淆对比:指针数组 vs 数组指针
这是面试和实际开发中最容易搞混的地方。请记住那个经典的口诀:看最后两个字,它是什么,本质就是什么。
表格
| 特性 | 数组指针 | 指针数组 |
|---|---|---|
| 定义写法 | int (*p)[3]; |
int *p[3]; |
| 本质 | 指针 (指向一个数组) | 数组 (存放多个指针) |
| 内存结构 | 单个指针变量,指向一块连续内存 | 一组指针变量,每个指向不同地方 |
| 典型用途 | 操作二维数组(作为函数参数) | 存储字符串数组(如命令行参数) |
| 形象理解 | 指向"一行"数据的指针 | 一个装满了地址的"抽屉柜" |
指针数组模拟二维数组的用法:
指针数组可以用来模拟二维数组,但它允许每一行的长度不同(不规则数组),这在处理字符串时非常有用。
// 指针数组:每个元素是一个 char* (字符串)
char *courses[] = {"语文", "数学", "英语"};
📐 进阶:多维数组的地址计算
如果你想深入底层,了解 arr[i][j] 到底是怎么算出来的,公式如下:
假设数组为 arr[ROWS][COLS],基地址为 Base,元素大小为 Size:
地址 = Base + (i * COLS + j) * Size
i * COLS:跳过i个完整的行。+ j:在当前行内跳过j个元素。
📌 总结与建议
- 传参首选数组指针 :如果你要写一个函数处理二维数组,参数应该写成
void func(int (*p)[列数], int rows)。 - 区分
*p[3]和(*p)[3]:前者是数组,后者是指针。 - 不要混淆二级指针 :虽然
int **p和二维数组看起来很像,但在内存布局上完全不同(int **p是指针的指针,通常用于动态分配的行指针数组),直接混用会导致错误。