快速学C语言——第 11 章:指针与数组

第 11 章:指针与数组

指针和数组是 C 语言中最紧密相关的核心概念,二者相辅相成。数组名在大多数场景下会被编译器解析为指向首元素的指针,而指针通过算术运算可像数组一样访问内存。这种关联性带来了极高的灵活性,让批量数据处理更高效,但也容易因概念混淆引发错误 ------ 理解二者的本质差异与共性,是掌握 C 语言的关键一步。


11.1 数组名的本质

数组名的核心特性:在大多数表达式中,数组名会自动转换为指向数组首元素的常量指针 (即&数组名[0]),但数组名并非普通指针,它是 "不可修改的指针常量"。

11.1.1 数组名作为指针常量

数组名的 "常量属性" 是与普通指针变量的核心区别:

  1. 数组名指向固定(首元素地址),不能作为左值修改指向(如arr = NULL编译错误);
  2. 普通指针变量是变量,可随时修改指向(如ptr = NULL合法);
  3. 数组名可直接赋值给指针变量(本质是传递首元素地址)。

示例代码:

c 复制代码
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};

    // 数组名与首元素地址完全等价
    printf("数组名arr: %p\n", arr);
    printf("首元素地址&arr[0]: %p\n", &arr[0]);
    printf("arr == &arr[0] ? %s\n", arr == &arr[0] ? "是" : "否");

    // 数组名是指针常量,不能修改指向(编译错误)
    // arr = NULL;  // 错误:数组名不能作为左值

    // 普通指针变量可修改指向
    int *ptr = arr;  // 正确:ptr接收arr的首元素地址
    ptr = NULL;      // 正确:指针变量可修改指向

    return 0;
}

运行结果:

bash 复制代码
数组名arr: 0061FF08
首元素地址&arr[0]: 0061FF08
arr == &arr[0] ? 是

--------------------------------
Process exited after 0.05777 seconds with return value 0
请按任意键继续. . .
11.1.2 sizeof 运算符的关键差异(数组名 vs 指针)

sizeof 是区分数组名和指针的 "试金石",核心差异源于二者的本质不同:

  • 数组名用 sizeof:计算整个数组的总字节数(元素个数 × 单个元素字节数);
  • 指针变量用 sizeof:计算指针变量本身的字节数(与指向类型无关,通常为 4 字节(32 位)或 8 字节(64 位))。

示例代码:

c 复制代码
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;  // 指针指向数组首元素

    printf("数组arr的大小: %zu 字节\n", sizeof(arr));      // 5×4=20字节(int占4字节)
    printf("指针ptr的大小: %zu 字节\n", sizeof(ptr));      // 8字节(64位系统)或4字节(32位)
    printf("int的大小: %zu 字节\n", sizeof(int));          // 4字节(单个元素大小)

    // 正确计算数组元素个数:总大小 ÷ 单个元素大小
    printf("数组元素个数: %zu\n", sizeof(arr) / sizeof(arr[0]));

    // 错误:指针sizeof无法计算数组元素个数(ptr是指针变量,不是数组)
    // printf("指针元素个数: %zu\n", sizeof(ptr) / sizeof(ptr[0]));

    return 0;
}

运行结果(64 位系统):

bash 复制代码
数组arr的大小: 20 字节
指针ptr的大小: 8 字节
int的大小: 4 字节
数组元素个数: 5

--------------------------------
Process exited after 0.0503 seconds with return value 0
请按任意键继续. . .

11.2 指针运算与数组访问

指针算术运算的核心特性:运算步长由指针指向的类型决定 (如int*步长 4 字节,char*步长 1 字节),这使得指针能像数组一样高效访问元素,甚至更灵活。

11.2.1 指针算术运算(+、-、++、--)

指针支持的算术运算及规则:

  1. 指针 + 整数:指针指向的地址向后移动 "整数 × 步长" 字节(如int* p+2移动 8 字节);
  2. 指针 - 整数:指针指向的地址向前移动 "整数 × 步长" 字节;
  3. 指针 ++/--:指针自增 / 自减 1(步长由类型决定);
  4. 同类型指针相减:结果为两个指针之间的 "元素个数"(而非字节数),用于计算数组元素间隔。

示例代码:

c 复制代码
#include <stdio.h>

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int *ptr = numbers;
    int size = sizeof(numbers) / sizeof(numbers[0]);

    printf("初始指针位置: %p -> %d\n", ptr, *ptr);  // 指向首元素10

    // 指针+2:移动2个int(8字节),指向30
    ptr = ptr + 2;
    printf("+2后指针位置: %p -> %d\n", ptr, *ptr);

    // 指针-1:移动1个int(4字节),指向20
    ptr = ptr - 1;
    printf("-1后指针位置: %p -> %d\n", ptr, *ptr);

    // 指针++:自增1,指向30
    ptr++;
    printf("递增后位置: %p -> %d\n", ptr, *ptr);

    // 指针--:自减1,指向20
    ptr--;
    printf("递减后位置: %p -> %d\n", ptr, *ptr);

    // 指针相减:计算元素个数
    int *start = numbers;
    int *end = numbers + size;
    printf("指针距离: %td 个元素\n", end - start);  // 输出5(间隔5个元素)

    return 0;
}
11.2.2 数组索引与指针运算的等价性

核心结论:对于数组arr和指针ptr=arrarr[i]\*(arr+i)ptr[i]\*(ptr+i)完全等价

等价性原理:数组索引本质是 "指针算术运算 + 解引用"------arr[i]表示 "从数组首地址移动 i 个元素,再解引用访问值",底层与指针运算逻辑一致。

示例代码:

c 复制代码
#include <stdio.h>

int main() {
    int arr[5] = {100, 200, 300, 400, 500};
    int *ptr = arr;

    printf("各种访问方式的等价性:\n");
    // 数组名的等价访问(arr是常量指针)
    printf("arr[2] = %d\n", arr[2]);        // 传统索引:300
    printf("*(arr + 2) = %d\n", *(arr + 2));// 指针运算:300
    printf("*(2 + arr) = %d\n", *(2 + arr));// 加法交换律:300
    printf("2[arr] = %d\n", 2[arr]);      // 语法允许(不推荐):300

    // 指针变量的等价访问
    printf("ptr[2] = %d\n", ptr[2]);        // 指针用索引:300
    printf("*(ptr + 2) = %d\n", *(ptr + 2));// 指针运算:300

    return 0;
}

运行结果:

bash 复制代码
各种访问方式的等价性:
arr[2] = 300
*(arr + 2) = 300
*(2 + arr) = 300
2[arr] = 300
ptr[2] = 300
*(ptr + 2) = 300

--------------------------------
Process exited after 0.0512 seconds with return value 0
请按任意键继续. . .

注意:2[arr]语法合法但不推荐,可读性极差,实际开发中应使用传统索引或指针运算。


11.3 指针与多维数组

多维数组(如二维数组)的本质是 "数组的数组",其内存是连续存储的。指针访问多维数组的核心是理解 "行指针" 和 "元素指针" 的区别。

11.3.1 二维数组的内存布局

关键特性:二维数组在内存中按 "行优先" 顺序连续存储,无额外间隔。例如int matrix[3][4]的内存顺序为:matrix[0][0]→matrix[0][1]→...→matrix[0][3]→matrix[1][0]→...→matrix[2][3]

地址差异核心:

  • matrix:指向第一行的 "行指针"(类型为int (*)[4]),matrix+i表示指向第 i 行,地址跳4×4=16字节(一行 4 个 int);
  • matrix[0]:第一行的数组名,退化为指向首元素的 "元素指针"(类型为int*),matrix[0]+j表示指向第 0 行第 j 个元素,地址跳 4 字节。

示例代码:

c 复制代码
#include <stdio.h>

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    printf("二维数组的内存布局分析:\n");
    printf("整个数组大小: %zu 字节\n", sizeof(matrix));  // 3×4×4=48字节
    printf("一行大小: %zu 字节\n", sizeof(matrix[0]));  // 4×4=16字节
    printf("一个元素大小: %zu 字节\n", sizeof(matrix[0][0]));  // 4字节

    printf("\n地址分析:\n");
    printf("matrix: %p\n", matrix);           // 行指针:指向第0行
    printf("matrix[0]: %p\n", matrix[0]);     // 元素指针:指向第0行第0列
    printf("&matrix[0][0]: %p\n", &matrix[0][0]); // 元素地址:与matrix[0]相同

    printf("matrix + 1: %p\n", matrix + 1);   // 跳一行(16字节)
    printf("matrix[0] + 1: %p\n", matrix[0] + 1); // 跳一个元素(4字节)

    return 0;
}
11.3.2 遍历二维数组的 3 种方法

基于内存布局和指针特性,提供 3 种遍历方式,适用于不同场景:

c 复制代码
#include <stdio.h>

#define ROWS 3
#define COLS 4

int main() {
    int matrix[ROWS][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 方法1:传统双重循环(数组索引,最直观)
    printf("方法1: 传统双重循环\n");
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", matrix[i][j]);
        }
        printf("\n");
    }

    // 方法2:使用行指针(指向一行的指针)
    printf("\n方法2: 使用行指针\n");
    int (*row_ptr)[COLS] = matrix;  // 行指针,指向包含COLS个int的数组
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", row_ptr[i][j]);  // 等价于 (*(row_ptr+i))[j]
        }
        printf("\n");
    }

    // 方法3:使用元素指针(逐个访问所有元素,利用连续内存)
    printf("\n方法3: 使用元素指针\n");
    int *elem_ptr = &matrix[0][0];  // 元素指针,指向首元素
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", *(elem_ptr + i * COLS + j));  // 计算偏移量
        }
        printf("\n");
    }

    return 0;
}

运行结果:

bash 复制代码
方法1: 传统双重循环
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 

方法2: 使用行指针
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 

方法3: 使用元素指针
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 

--------------------------------
Process exited after 0.0523 seconds with return value 0
请按任意键继续. . .

11.4 指针数组 vs 数组指针(核心区分)

二者是 C 语言高频混淆点,核心差异在于 "优先级" 和 "本质":指针数组是 "数组",数组指针是 "指针",可通过声明语法和用途快速区分。

11.4.1 指针数组(数组里装指针)

声明语法:数据类型 *数组名[数组大小](如char *names[4]

本质:一个数组,每个元素都是指针变量(如names[0]char*类型)。

核心特点:数组大小由元素个数决定,每个元素可指向不同的对象(如不同字符串)。

示例代码(字符串指针数组):

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    // 指针数组:每个元素是char*(指向字符串的指针)
    char *names[] = {"Alice", "Bob", "Charlie", "David"};
    int count = sizeof(names) / sizeof(names[0]);  // 数组大小4(4个指针)

    printf("指针数组示例:\n");
    for (int i = 0; i < count; i++) {
        printf("names[%d] = %s (指针地址: %p, 字符串地址: %p)\n", 
               i, names[i], &names[i], names[i]);
    }

    // 修改指针数组的元素(切换指针指向,不修改字符串本身)
    char new_name[] = "Eve";
    names[1] = new_name;  // names[1]现在指向new_name的地址
    printf("\n修改后:\n");
    for (int i = 0; i < count; i++) {
        printf("names[%d] = %s\n", i, names[i]);
    }

    return 0;
}

运行结果:

bash 复制代码
指针数组示例:
names[0] = Alice (指针地址: 0061FF00, 字符串地址: 00404000)
names[1] = Bob (指针地址: 0061FF08, 字符串地址: 00404006)
names[2] = Charlie (指针地址: 0061FF10, 字符串地址: 0040400A)
names[3] = David (指针地址: 0061FF18, 字符串地址: 00404012)

修改后:
names[0] = Alice
names[1] = Eve
names[2] = Charlie
names[3] = David

--------------------------------
Process exited after 0.0534 seconds with return value 0
请按任意键继续. . .
11.4.2 数组指针(指针指向数组)

声明语法:数据类型 (*指针名)[数组大小](如int (*arr_ptr)[5]

本质:一个指针,专门指向 "包含指定个数元素的数组"(如arr_ptr指向包含 5 个 int 的数组)。

核心特点:指针运算步长为整个数组的大小(如arr_ptr+15×4=20字节)。

示例代码:

c 复制代码
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};

    // 数组指针:指向包含5个int的数组(括号改变优先级)
    int (*arr_ptr)[5] = &arr;  // 必须取数组地址(&arr),不能直接写arr(退化为元素指针)

    printf("数组指针示例:\n");
    printf("arr_ptr(指针值): %p\n", arr_ptr);
    printf("*arr_ptr(解引用→数组首元素地址): %p\n", *arr_ptr);
    printf("arr(数组名→首元素地址): %p\n", arr);

    // 通过数组指针访问元素(两种等价方式)
    printf("\n通过数组指针访问元素:\n");
    printf("(*arr_ptr)[0] = %d\n", (*arr_ptr)[0]);  // 解引用指针→数组,再索引
    printf("(*arr_ptr)[2] = %d\n", (*arr_ptr)[2]);  // 访问第3个元素30

    // 数组指针的运算(步长=数组大小)
    printf("\n指针运算(步长=5×4=20字节):\n");
    printf("arr_ptr: %p\n", arr_ptr);
    printf("arr_ptr + 1: %p\n", arr_ptr + 1);  // 地址+20字节

    return 0;
}

运行结果:

bash 复制代码
数组指针示例:
arr_ptr(指针值): 0061FF04
*arr_ptr(解引用→数组首元素地址): 0061FF04
arr(数组名→首元素地址): 0061FF04

通过数组指针访问元素:
(*arr_ptr)[0] = 10
(*arr_ptr)[2] = 30

指针运算(步长=5×4=20字节):
arr_ptr: 0061FF04
arr_ptr + 1: 0061FF18

--------------------------------
Process exited after 0.0517 seconds with return value 0
请按任意键继续. . .
核心区分表
特性 指针数组(如int *ptr_arr[5] 数组指针(如int (*arr_ptr)[5]
本质 数组(元素是指针) 指针(指向数组)
优先级 [] > *,先识别数组 () > *,先识别指针
运算步长 指针类型步长(如int*→4 字节) 数组大小步长(如 5×4=20 字节)
常见用途 存储多个字符串、多组数据地址 遍历多维数组、传递二维数组

11.5 字符串与指针

C 语言中字符串本质是 "'\0'结尾的字符数组",字符指针是处理字符串的灵活方式,比字符数组更便于切换指向。

11.5.1 字符串常量与字符数组的指针差异
c 复制代码
#include <stdio.h>

int main() {
    // 字符串常量:存储在只读内存区,指针指向常量地址(不可修改内容)
    char *str1 = "Hello, World!";
    // 字符数组:存储在栈区/全局区,可修改内容
    char str2[] = "Hello, World!";

    printf("str1: %s (指针地址: %p, 字符串地址: %p)\n", str1, &str1, str1);
    printf("str2: %s (数组地址: %p, 首元素地址: %p)\n", str2, str2, &str2[0]);

    // 字符数组可修改内容
    str2[0] = 'h';
    printf("修改后str2: %s\n", str2);  // 输出"hello, World!"

    // 字符串常量不可修改(运行时错误,只读内存区保护)
    // str1[0] = 'h';

    // 指针可切换指向不同字符串常量(合法,修改的是指针指向,不是字符串本身)
    str1 = "New string";
    printf("切换指向后str1: %s\n", str1);

    return 0;
}
11.5.2 字符串操作的底层是指针移动

标准库字符串函数(如strlenstrcpy)的底层的都是通过指针移动实现的,自定义实现可帮助理解核心逻辑:

c 复制代码
#include <stdio.h>
#include <string.h>

// 自定义strlen:统计'\0'前的字符数(指针移动计数)
size_t my_strlen(const char *str) {
    const char *ptr = str;  // 临时指针,不修改原指针
    while (*ptr != '\0') {  // 指针移动直到遇到结束符
        ptr++;
    }
    return ptr - str;  // 指针相减=元素个数(字符数)
}

// 自定义strcpy:将src复制到dest(指针移动赋值)
char* my_strcpy(char *dest, const char *src) {
    char *original_dest = dest;  // 保存dest初始地址(需返回)
    while ((*dest++ = *src++) != '\0');  // 逐字符复制,直到src的'\0'
    return original_dest;
}

int main() {
    char source[] = "Source string";
    char destination[50];

    // 测试自定义函数
    printf("自定义字符串函数测试:\n");
    printf("source长度: %zu\n", my_strlen(source));  // 输出13
    my_strcpy(destination, source);
    printf("复制结果: %s\n", destination);  // 输出"Source string"

    // 标准库函数示例
    char str1[20] = "Hello";
    char str2[20] = "World";
    strcat(str1, " ");  // 连接空格
    strcat(str1, str2); // 连接str2
    printf("\n标准库strcat结果: %s\n", str1);  // 输出"Hello World"

    return 0;
}

11.6 指针与数组作为函数参数

数组作为函数参数时,会自动退化为指向首元素的指针(即 "传地址"),因此函数内修改数组会影响原始数组;指针作为参数则直接传递地址,效果一致。

11.6.1 一维数组作为函数参数(3 种等价声明)

核心结论:一维数组作为参数时,以下 3 种声明完全等价,均退化为int*

  1. void func(int arr[])
  2. void func(int *arr)
  3. void func(int arr[5])(数组大小 5 被忽略,无意义)

注意:必须显式传递数组大小,函数内无法通过sizeof(arr)计算(arr 已退化为指针)。

示例代码:

c 复制代码
#include <stdio.h>

// 3种等价声明(任选一种即可)
void print_array1(int arr[], int size);
void print_array2(int *arr, int size);
void print_array3(int arr[5], int size);

// 实现(统一用一种即可)
void print_array1(int arr[], int size) {
    printf("数组元素: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);  // 等价于*(arr+i)
    }
    printf("\n");
}

// 修改数组(传地址,影响原始数组)
void modify_array(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;  // 每个元素×2
    }
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);

    printf("原始数组: ");
    print_array1(numbers, size);

    modify_array(numbers, size);  // 传数组名(退化为指针)

    printf("修改后数组: ");
    print_array2(numbers, size);  // 用等价声明调用

    return 0;
}
11.6.2 二维数组作为函数参数(必须指定列数)

二维数组退化为 "行指针"(如int (*)[COLS]),因此函数参数必须指定列数(编译器需计算行的大小),行数可省略。

3 种常见声明方式:

c 复制代码
#include <stdio.h>

#define COLS 4  // 必须指定列数

// 方式1:数组表示法(最直观)
void print_matrix1(int matrix[][COLS], int rows) {
    printf("方法1 - 数组表示法:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", matrix[i][j]);
        }
        printf("\n");
    }
}

// 方式2:行指针表示法(等价于方式1)
void print_matrix2(int (*matrix)[COLS], int rows) {
    printf("方法2 - 指针表示法:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", *(*(matrix + i) + j));  // 行指针+i→第i行,解引用→元素指针+j
        }
        printf("\n");
    }
}

// 方式3:元素指针表示法(视为一维数组,需传行列数)
void print_matrix3(int *matrix, int rows, int cols) {
    printf("方法3 - 一维数组处理:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%2d ", matrix[i * cols + j]);  // 计算全局偏移量
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    print_matrix1(matrix, 3);
    printf("\n");
    print_matrix2(matrix, 3);
    printf("\n");
    print_matrix3(&matrix[0][0], 3, COLS);  // 传首元素地址

    return 0;
}

相关推荐
handler011 小时前
速通蓝桥杯省一: 前缀和&差分(附经典例题)
c语言·c++·笔记·职场和发展·蓝桥杯
无限进步_1 小时前
【C++】lambda表达式与std::function/bind包装器
开发语言·c++
树下水月1 小时前
php artisan serve 在window上执行报错的问题
开发语言·php
梦梦代码精1 小时前
电商系统的核心难点:订单与营销系统如何设计?——LikeShop 架构深度拆解(规则计算与状态一致性)
java·开发语言·低代码·架构·开源·github
隐退山林1 小时前
JavaEE进阶:SpringBoot日志
java·开发语言
nbwenren1 小时前
C++ 资源管理 —— RAII
开发语言·c++
上海云盾第一敬业销售1 小时前
游戏开服即“炸服“?CC攻击成游戏行业隐形杀手
服务器·网络·游戏
开开心心就好1 小时前
直接减少蓝光辐射的专业护眼工具
linux·运维·服务器·智能手机·excel·java-rabbitmq·sdkman
棒棒的唐1 小时前
开发中,如何指定不同的php版本启动yii项目
开发语言·php