快速学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;
}

相关推荐
上海云盾第一敬业销售34 分钟前
服务器遭受攻击的应对策略及快速防护实践
运维·服务器·web安全·ddos
yyuuuzz6 小时前
独立站的技术基础与常见运维问题
大数据·运维·服务器·网络·数据库·aws
isyangli_blog8 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008118 小时前
FastAPI APIRouter
开发语言·python
Benszen8 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆8 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木8 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充9 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~9 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6169 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang