C 基础(9) - 数组和指针

数组

在 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 的数组,其有效下标范围是 0n-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++ 合法)

⚠️ 两个特殊例外

虽然 数组名 通常代表首元素地址,但在以下两种情况下,它代表的是整个数组

  1. sizeof(数组名):计算的是整个数组占用的总字节数。
  2. &数组名:取出的是整个数组的地址。

这个区别体现在指针运算上:

  • 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 的地址,并存入指针 ptr

    复制代码
      printf("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 指向常量的常量指针 ❌ 不能 ❌ 不能
野指针

野指针是指向未知或无效内存地址的指针。使用野指针会导致程序崩溃或产生不可预知的错误。

常见成因

  1. 未初始化 :声明后未赋予有效地址。

    复制代码
    int *p; // 野指针!p 的值是随机的
    *p = 10; // 危险操作!
  1. 访问已释放的内存free() 之后没有将指针置为 NULL。c

    复制代码
    int *p = (int*)malloc(sizeof(int));
    free(p);
    // p 仍然是野指针,它仍保存着已释放内存的地址
    *p = 20; // 危险操作!
  1. 返回局部变量的地址 :函数返回后,其局部变量占用的栈内存会被释放。

    复制代码
    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 个元素。

📌 总结与建议

  1. 传参首选数组指针 :如果你要写一个函数处理二维数组,参数应该写成 void func(int (*p)[列数], int rows)
  2. 区分 *p[3](*p)[3]:前者是数组,后者是指针。
  3. 不要混淆二级指针 :虽然 int **p 和二维数组看起来很像,但在内存布局上完全不同(int **p 是指针的指针,通常用于动态分配的行指针数组),直接混用会导致错误。
相关推荐
计算机安禾2 小时前
【数据结构与算法】第45篇:跳跃表(Skip List)
c语言·数据结构·算法·list·排序算法·图论·visual studio
水饺编程3 小时前
第5章,[标签 Win32] :GDI 的基本图形
c语言·c++·windows·visual studio
水饺编程3 小时前
第5章,[标签 Win32] :GDI 的其他方面的分类
c语言·c++·windows·visual studio
计算机安禾3 小时前
【数据结构与算法】第46篇:算法思想(一):递归与分治
c语言·数据结构·c++·算法·visualstudio·图论·visual studio code
Shadow(⊙o⊙)3 小时前
C中 memset enum malloc fputc fgetc fgets fread fwrite rewind指针回退
java·c语言·数据库
wengqidaifeng3 小时前
第十七届蓝桥杯C/C++软件赛C组算法题讲解
c语言·c++·蓝桥杯
Shadow(⊙o⊙)3 小时前
C学习历程的总汇
c语言·学习·jquery
艾莉丝努力练剑3 小时前
【Linux线程】Linux系统多线程(五):<线程同步与互斥>线程互斥
linux·运维·服务器·c语言·c++·学习·ubuntu
我不是懒洋洋3 小时前
【经典题目】链表OJ(轮转数组、返回倒数第k个节点、链表的回文结构)
c语言·开发语言·数据结构·算法·链表·visual studio