字符指针;字符数组;指针数组和数组指针(指针进阶)

Source

一,字符指针

在 C 语言中,字符指针(char * 是处理字符串的核心工具,它既可以指向单个字符,也可以指向字符串常量 / 字符数组的首地址。

一、字符指针的基础概念

字符指针的本质是指向 char 类型数据的指针变量,存储的是字符 / 字符串在内存中的首地址。

1. 定义与初始化
#include <stdio.h>

int main() {
    // 1. 指向单个字符
    char ch = 'A';
    char *p1 = &ch;  // p1 指向字符变量 ch 的地址

    // 2. 指向字符串常量(最常用)
    char *p2 = "Hello";  // p2 指向字符串常量 "Hello" 的首字符 'H' 的地址

    // 3. 指向字符数组(数组名本身是首地址)
    char arr[] = "World";
    char *p3 = arr;      // p3 指向字符数组 arr 的首元素 'W'

    // 打印验证
    printf("*p1 = %c\n", *p1);   // 输出:*p1 = A(解引用取单个字符)
    printf("p2 = %s\n", p2);     // 输出:p2 = Hello(字符串打印,从首地址到 '\0' 结束)
    printf("p3 = %s\n", p3);     // 输出:p3 = World

    return 0;
}

这里有个要注意的是

char *p2 = "Hello";

这里把字符串首字符H的地址,赋值给了p2,因此在打印时只需要提供一个地址就好。但是这种写法在编译器中可能会报错,如果我们在char之前加上const,那就会更加有效的保护这个字符串。

2. 核心要点
  • 字符串在 C 中以 '\0'(空字符,ASCII 码 0)作为结束标志,printf("%s") 会从指针指向的地址开始遍历,直到遇到 '\0'
  • 字符指针本身是变量(可修改指向),但它指向的内容是否可修改,取决于指向的是「字符串常量」还是「字符数组」。
3.注意
const char* p1 = "abcdef";
const char* p2 = "abcdef";

如上述,这个场景是 C 语言中字符串常量的一个核心特性 ——相同的字符串常量在只读数据段中只会存储一份,多个指向它的指针会指向同一个内存地址

C 编译器为了节省内存,会对只读数据段(.rodata)中的字符串常量做 “字符串驻留(String Interning)” 优化:

  • 所有内容相同的字符串常量(如 "abcdef")只会在只读数据段中分配一次内存;
  • 无论定义多少个指向该字符串的 const char* 指针(如 p1p2),这些指针都会指向同一块内存地址;
  • 只读数据段的特性:程序运行时不可修改,修改会触发段错误(SIGSEGV)。

在下文我会将字符指针和字符数组这样的情况进行对比说明。


二、字符指针 vs 字符数组(核心区别)

这是最易混淆的点,直接决定代码是否安全,对比表如下:

特性 字符指针(char *p = "abc" 字符数组(char arr[] = "abc"
存储位置 只读数据区(常量区) 栈 / 全局区(可写)
内容是否可修改 不可修改(修改会触发段错误) 可修改
指针是否可重定向 可以(如 p = "def" 数组名是常量地址,不可赋值(arr = "def" 报错)
内存占用 指针变量(4/8 字节)+ 常量区字符串 数组本身占用 N+1 字节(N 为字符数,含 '\0'
错误示例(修改字符串常量)
#include <stdio.h>

int main() {
    char *p = "Hello";
    p[0] = 'h';  // 错误:试图修改只读内存的字符串常量,运行时崩溃(段错误)
    return 0;
}
正确示例(修改字符数组)
#include <stdio.h>

int main() {
    char arr[] = "Hello";
    char *p = arr;
    p[0] = 'h';  // 正确:arr 在栈区,可修改
    printf("%s\n", arr);  // 输出:hello
    return 0;
}

三、字符指针的常见用法

1. 遍历字符串

通过指针偏移遍历每个字符,直到遇到 '\0'

#include <stdio.h>

int main() {
    char *str = "Hello C";
    char *p = str;  // 指向字符串首地址

    // 遍历字符串
    while (*p != '\0') {
        printf("%c ", *p);  // 输出:H e l l o   C
        p++;  // 指针后移 1 字节(char 占 1 字节)
    }

    return 0;
}
2. 函数参数传递字符串

字符指针是函数接收字符串的常用方式(比数组更灵活):

#include <stdio.h>
#include <string.h>

// 计算字符串长度(模拟 strlen)
int my_strlen(char *s) {
    int len = 0;
    while (*s != '\0') {
        len++;
        s++;
    }
    return len;
}

int main() {
    char *str = "Hello World";
    printf("长度:%d\n", my_strlen(str));  // 输出:长度:11
    return 0;
}
3. 动态分配字符串(结合 malloc

字符指针可指向堆内存中的字符串(需手动释放 free):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    // 动态分配 10 字节内存(含 '\0')
    char *str = (char *)malloc(10 * sizeof(char));
    if (str == NULL) {  // 检查分配是否成功
        perror("malloc failed");
        return 1;
    }

    strcpy(str, "Dynamic");  // 赋值字符串
    printf("str = %s\n", str);  // 输出:str = Dynamic

    free(str);  // 释放堆内存
    str = NULL; // 避免野指针

    return 0;
}

四、常见陷阱与注意事项

1. 野指针问题

未初始化的字符指针指向随机地址,解引用会导致崩溃:

char *p;
// *p = 'A';  // 错误:野指针,运行时崩溃
p = "abc";    // 正确:指向合法地址
2. 字符串常量的不可修改性

如前文所述,char *p = "abc" 中 p 指向只读区,修改会触发段错误,如需修改应使用数组:

// 正确写法(可修改)
char arr[] = "abc";
char *p = arr;
p[0] = 'A';  // 合法
3. 指针越界访问

遍历字符串时超出 '\0' 范围,会访问非法内存:

char *str = "abc";
// 错误:str 只有 3 个字符 + '\0',p 越界
char *p = str + 5;
// *p = 'x';  // 非法访问,可能导致程序崩溃
4. 忘记释放动态内存

使用 malloc/calloc 分配的字符指针,必须用 free 释放,否则内存泄漏:

char *p = (char *)malloc(10);
// 忘记 free(p);  // 内存泄漏

五、总结

  1. 字符指针(char *)是指向字符 / 字符串的地址,核心用于处理字符串;
  2. 指向字符串常量的字符指针不可修改内容,指向字符数组 / 堆内存的可修改;
  3. 字符指针比字符数组更灵活(可重定向、动态分配),但需注意内存管理;
  4. 关键禁忌:避免野指针、不修改字符串常量、不越界访问、动态内存记得释放。

二.字符数组

在 C 语言中,字符数组(char arr[] 是存储字符序列的核心结构,也是处理可修改字符串的主要方式(区别于只读的字符串常量)。

一、字符数组的基础定义与初始化

字符数组是「元素类型为 char 的数组」,专门用于存储字符(包括字符串的结束标志 '\0')。初始化方式分为两种:显式初始化单个字符 和 直接初始化字符串

1. 初始化方式对比
初始化方式 示例代码 内存布局(字节) 说明
字符串初始化(推荐) char arr[] = "hello"; h e l l o \0 自动分配 6 字节(5 个字符 + '\0'
显式指定长度 + 字符串 char arr[10] = "hello"; h e l l o \0 \0 \0 \0 \0 分配 10 字节,未填充部分补 '\0'
单个字符初始化 char arr[] = {'h','e','l','l','o'}; h e l l o 无 '\0',不能当作字符串使用(无结束标志)
单个字符 + 手动加 '\0' char arr[] = {'h','e','l','l','o','\0'}; h e l l o \0 等价于字符串初始化,可当作字符串使用
2. 核心示例
#include <stdio.h>

int main() {
    // 方式1:自动适配长度,含 '\0'(最常用)
    char arr1[] = "hello";
    printf("arr1: %s\n", arr1);  // 输出:hello
    printf("arr1 长度:%zu\n", sizeof(arr1));  // 输出:6(含 '\0')

    // 方式2:指定长度,未用空间补 '\0'
    char arr2[10] = "hello";
    printf("arr2: %s\n", arr2);  // 输出:hello
    printf("arr2 长度:%zu\n", sizeof(arr2));  // 输出:10

    // 方式3:无 '\0',不能用 %s 打印(会乱码)
    char arr3[] = {'h','e','l','l','o'};
    // printf("arr3: %s\n", arr3);  // 错误:无 '\0',越界访问乱码

    // 方式4:手动加 '\0',可正常打印
    char arr4[] = {'h','e','l','l','o','\0'};
    printf("arr4: %s\n", arr4);  // 输出:hello

    return 0;
}
char arr1[] = "abcdef";
char arr2[] = "abcdef";

har arr1[ ]、arr2[ ] 是字符数组,初始化时会将字符串常量的内容复制到栈上的独立内存空间。
arr1 和 arr2 是两个不同数组的首地址(栈上的不同内存区域),因此它们的地址不相等

二、字符数组的核心特性

1. 内存存储位置

字符数组的存储位置取决于定义位置:

  • 局部字符数组(函数内定义):存储在 栈区(可读写,函数结束后自动释放);
  • 全局 / 静态字符数组(函数外 / 加 static):存储在 全局 / 静态区(可读写,程序结束后释放);
  • 无论哪种位置,字符数组的内容都是可修改的(核心区别于字符串常量)。
2. 内容可修改(关键优势)
#include <stdio.h>

int main() {
    char arr[] = "hello";
    arr[0] = 'H';  // 修改第一个字符(合法,栈区可写)
    arr[2] = 'L';  // 修改第三个字符
    printf("arr: %s\n", arr);  // 输出:HeLlo

    return 0;
}
3. 数组名是 “常量地址”(不可重定向)

字符数组的数组名本质是「指向数组首元素的常量指针」,不能被赋值修改指向:

#include <stdio.h>

int main() {
    char arr[] = "hello";
    // arr = "world";  // 错误:数组名是常量地址,不可赋值
    char *p = arr;     // 正确:用字符指针指向数组首地址(指针可重定向)
    p = "world";       // 合法:指针 p 改为指向字符串常量

    return 0;
}
4. 长度固定(编译期确定)

字符数组的长度在定义时确定,运行时无法动态扩容 / 缩容:

char arr[5] = "hello";  // 错误:"hello" 含 '\0' 共 6 字节,超出 5 字节限制
char arr[6] = "hello";  // 正确:刚好容纳 5 个字符 + '\0'

三、字符数组的常见用法

1. 遍历字符数组(两种方式)
#include <stdio.h>

int main() {
    char arr[] = "hello";

    // 方式1:下标遍历(直观)
    for (int i = 0; arr[i] != '\0'; i++) {
        printf("%c ", arr[i]);  // 输出:h e l l o
    }
    printf("\n");

    // 方式2:指针遍历(高效)
    char *p = arr;
    while (*p != '\0') {
        printf("%c ", *p);  // 输出:h e l l o
        p++;
    }

    return 0;
}
2. 字符串操作(结合 <string.h> 库)

字符数组是 C 标准库字符串函数的主要操作对象(如 strcpy/strcat/strcmp):

#include <stdio.h>
#include <string.h>

int main() {
    char arr[20] = "hello";  // 预留足够空间,避免越界

    // 1. 字符串拷贝
    strcpy(arr, "hello world");
    printf("strcpy: %s\n", arr);  // 输出:hello world

    // 2. 字符串拼接
    strcat(arr, "!");
    printf("strcat: %s\n", arr);  // 输出:hello world!

    // 3. 字符串比较
    char arr2[] = "hello world!";
    int cmp = strcmp(arr, arr2);
    printf("strcmp: %d\n", cmp);  // 输出:0(相等)

    // 4. 获取字符串长度(不含 '\0')
    int len = strlen(arr);
    printf("strlen: %d\n", len);  // 输出:12

    return 0;
}

⚠️ 注意:使用 strcpy/strcat 时,必须保证字符数组有足够空间,否则会导致数组越界(内存污染,程序崩溃)。

3. 作为函数参数传递

字符数组作为函数参数时,会退化为字符指针char*),函数内无法通过 sizeof 获取数组真实长度:

#include <stdio.h>

// 数组参数退化为 char*,size 需手动传入
void print_arr(char arr[], int size) {
    printf("函数内 sizeof(arr):%zu\n", sizeof(arr));  // 输出:8(64位系统指针大小)
    for (int i = 0; i < size; i++) {
        printf("%c ", arr[i]);
    }
}

int main() {
    char arr[] = "hello";
    int size = sizeof(arr) / sizeof(char);  // 计算数组总长度(含 '\0')
    printf("主函数内 sizeof(arr):%zu\n", sizeof(arr));  // 输出:6
    print_arr(arr, size);  // 输出:h e l l o \0

    return 0;
}

四、常见陷阱与注意事项

1. 忘记 '\0' 导致的乱码

如果字符数组初始化时未加 '\0',使用 %s 打印或字符串函数处理时,会越界访问内存,导致乱码:

char arr[] = {'h','e','l','l','o'};
// printf("%s\n", arr);  // 错误:无 '\0',打印 hello + 随机乱码
2. 数组越界赋值 / 拷贝

字符数组长度固定,超出长度赋值会覆盖栈区其他数据,导致程序崩溃:

char arr[5] = "hello";  // 错误:"hello" 含 '\0' 共 6 字节,越界
char arr[10] = "hello";
// strcpy(arr, "hello world");  // 错误:"hello world" 共 12 字节,超出 arr 长度
3. 函数参数退化为指针

函数内无法通过 sizeof 获取字符数组的真实长度,必须手动传入长度或依赖 '\0' 结束标志:

void func(char arr[]) {
    // 错误:sizeof(arr) 是指针大小(8 字节),不是数组长度
    int len = sizeof(arr) / sizeof(char);
}
4. 避免用字符数组存储超长字符串

字符数组长度编译期固定,若需存储长度不确定的字符串(如用户输入),建议用「字符指针 + 动态内存分配(malloc/realloc)」:

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 动态分配 100 字节,可根据需要 realloc 扩容
    char *str = (char *)malloc(100 * sizeof(char));
    if (str == NULL) {
        perror("malloc failed");
        return 1;
    }
    printf("请输入字符串:");
    scanf("%99s", str);  // 限制输入长度,避免越界
    printf("你输入的是:%s\n", str);

    free(str);
    str = NULL;

    return 0;
}

五、总结

  1. 字符数组是存储可修改字符串的核心方式,初始化时建议直接用字符串(自动加 '\0');
  2. 字符数组的内容可修改,但数组名是常量地址,不可重定向;
  3. 作为函数参数时会退化为字符指针,需手动传递长度或依赖 '\0'
  4. 核心禁忌:避免无 '\0'、数组越界、依赖函数内 sizeof 获取数组长度;
  5. 对比字符指针:字符数组适合存储固定长度、需修改的字符串;字符指针适合灵活指向(常量 / 动态内存)字符串。

三.字符指针和字符数组

字符指针(char*)和字符数组(char[])是 C 语言处理字符串的两种核心方式,二者语法上看似相似,实则内存模型、可修改性、灵活性等方面差异巨大

一、核心差异速览(表格对比)

这是最直观的对比,建议先记住核心结论:

特性 字符指针(const char* p = "abc" 字符数组(char arr[] = "abc"
内存位置 指针变量在栈,指向只读数据段(.rodata) 数组整体在栈 / 全局区(可读写)
内容可修改性 不可修改(修改触发段错误) 可自由修改(栈 / 全局区可写)
标识符性质 变量指针(可重定向,如 p = "def" 常量指针(数组名不可赋值,arr = "def" 报错)
内存占用 指针变量(4/8 字节)+ 只读区字符串 数组本身占用 N+1 字节(N 为字符数,含 '\0'
相同字符串是否共享 是(编译器优化,指向同一地址) 否(每个数组独立分配内存)
长度获取 sizeof(p) 得指针大小(4/8 字节),strlen(p) 得有效字符数 sizeof(arr) 得数组总字节数(含 '\0'),strlen(arr) 得有效字符数
函数传参 直接传递指针(灵活,无拷贝) 退化为字符指针(本质传地址,非拷贝)
动态扩容 可指向堆内存(malloc/realloc),支持动态扩容 编译期固定长度,运行时无法扩容

二、内存模型拆解(关键理解点)

1. 字符指针的内存模型

以 const char* p = "abc"; 为例:

栈区(栈帧)        只读数据段(.rodata)
┌───────────┐       ┌───────────────┐
│ p (0x1000)│ ─────>│ 'a' 'b' 'c' '\0' │
└───────────┘       └───────────────┘
  • p 是栈上的指针变量,存储的是只读数据段中字符串 "abc" 的首地址;
  • 只读数据段的内容不可修改,因此 p[0] = 'A' 会触发段错误;
  • 指针 p 可重定向,如 p = "def",此时 p 指向只读数据段的另一块地址。
2. 字符数组的内存模型

以 char arr[] = "abc"; 为例:

栈区(栈帧)
┌───────────────────────┐
│ arr[0] = 'a'          │
│ arr[1] = 'b'          │
│ arr[2] = 'c'          │
│ arr[3] = '\0'         │
└───────────────────────┘
  • arr 是栈上连续的 4 字节内存(3 个字符 + '\0'),直接存储字符串内容;
  • 数组名 arr 是指向首元素 arr[0] 的常量指针,不可赋值(arr = "def" 报错);
  • 内容可自由修改,如 arr[0] = 'A',修改的是栈区内存,完全合法。

三、使用场景选择(什么时候用哪个?)

1. 优先用字符指针的场景
  • 存储只读字符串(如常量提示语、配置项):const char* msg = "success";
  • 函数参数接收字符串(灵活,无需关心原数据是数组还是常量):
  • void print_str(const char* s) {  // 加 const 避免误修改
        printf("%s\n", s);
    }
    
  • 动态字符串(结合 malloc/realloc 管理内存):
  • char* buf = (char*)malloc(100 * sizeof(char));  // 堆内存,可扩容
    strcpy(buf, "dynamic string");
    free(buf);
    
2. 优先用字符数组的场景
  • 存储需修改的字符串(如用户输入、临时拼接的字符串):
  • char input[100];
    scanf("%99s", input);  // 输入后可修改 input 的内容
    input[0] = toupper(input[0]);  // 首字母大写
    
  • 固定长度、无需动态扩容的字符串(如固定格式的缓存):
  • char cache[32] = "cache: ";
    strcat(cache, "data123");  // 拼接,修改数组内容
    

四、常见误区与避坑指南

误区 1:认为 char* p = "abc" 可修改内容

❌ 错误代码:

char* p = "abc";
p[0] = 'A';  // 段错误!指向只读数据段

✅ 正确做法:

  • 如需修改,改用字符数组:char arr[] = "abc"; arr[0] = 'A';
  • 或用字符指针指向堆内存:char* p = malloc(4); strcpy(p, "abc"); p[0] = 'A';
误区 2:函数内用 sizeof 获取字符数组长度

字符数组作为函数参数时,会退化为字符指针sizeof 只能获取指针大小,而非数组长度:❌ 错误代码:

void func(char arr[]) {
    int len = sizeof(arr);  // 64 位系统返回 8(指针大小),而非数组长度
}

✅ 正确做法:

  • 手动传递数组长度:void func(char arr[], int len)
  • 依赖 '\0' 结束标志(用 strlen):int len = strlen(arr);
误区 3:混淆 “指针比较” 和 “字符串比较”
  • 字符指针比较:p1 == p2 比较的是地址(是否指向同一块内存);
  • 字符串内容比较:必须用 strcmp(p1, p2)(返回 0 表示内容相等)。

示例:

const char* p1 = "abc";
const char* p2 = "abc";
const char* p3 = "def";
char arr[] = "abc";

printf("%d\n", p1 == p2);  // 1(地址相同,编译器优化)
printf("%d\n", p1 == arr); // 0(地址不同,arr 在栈区)
printf("%d\n", strcmp(p1, arr)); // 0(内容相同)
误区 4:字符数组初始化时越界

字符数组长度需容纳所有字符 + '\0',否则会溢出:❌ 错误代码:

char arr[3] = "abc";  // "abc" 含 '\0' 共 4 字节,超出 3 字节限制

✅ 正确做法:

  • 不指定长度(自动适配):char arr[] = "abc";(长度 4);
  • 显式指定足够长度:char arr[4] = "abc";

五、核心联系(二者的互通性)

虽然差异大,但字符指针和字符数组可互通使用:

六、总结(一句话记住核心)

  1. 字符指针可指向字符数组的首地址(最常用):
    char arr[] = "hello";
    char* p = arr;  // p 指向 arr[0]
    p[1] = 'E';     // 等价于 arr[1] = 'E',合法(修改栈区内容)
    
  2. 函数接收字符串时,字符数组和字符指针参数无区别:
    char arr[] = "hello";
    const char* p = "world";
    print_str(arr);  // 合法,数组退化为指针
    print_str(p);    // 合法,直接传指针
    
  3. 字符指针:是 “指向字符串的变量指针”,适合只读、动态、灵活指向的字符串;
  4. 字符数组:是 “存储字符串的固定内存块”,适合可修改、固定长度的字符串;
  5. 关键区别:内存是否可写 + 标识符是否可重定向

const char* p1 = "abcdef";//这种常量字符串内存中只会存储一份,存储在只读数据段
const char* p2 = "abcdef";
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (p1 == p2) {//编译器会对相同的字符串常量做 “合并优化”(即同一程序中相同的字符串常量仅占用一块内存),
 //因此 p1 和 p2 存储的是同一块内存的地址,故 p1 == p2 为真。
 printf("p1==p2\n");
}
else {
 printf("p1!=p2\n");
}
//char arr1[]、arr2[] 是字符数组,初始化时会将字符串常量的内容复制到栈上的独立内存空间。
//arr1 和 arr2 是两个不同数组的首地址(栈上的不同内存区域),因此它们的地址不相等,故 arr1 == arr2 为假。
if (arr1 == arr2) {
 printf("arr1==arr2\n");
}
else {
 printf("arr1!=arr2\n");
}

	 //指针数组
	// int* arr3[6];//存放整形指针的数组
	// char* arr4[5];//存放字符指针的数组

	 int s1[] = { 1,2,3,4,5 };
	 int s2[] = { 2,3,4,5,6 };
	 int s3[] = { 3,4,5,6,7 };
	 int* ps[3] = { s1,s2,s3 };
	 int i = 0;
	 for (i - 0; i < 3; i++) {
		 for (int j = 0; j < 5; j++) {
			 //printf("%d ", *(ps[i] + j));
			 printf("%d ", ps[i][j]);
		 }
		 printf("\n");
	 }
	 //数组指针--指针--指向数组的指针
	 //整型指针--指向整型的指针
	 //字符指针--指向字符的指针
	 //int* p1[10];//p1指针数组
	 //int (*p2)[10];//p2数组指针,p2可以指向一个数组,该数组有10个元素,每个元素是int类型
	 //数组名
	 int arrr[10] = { 0 };
	 //printf("%p\n", arrr);
	 //printf("%p\n", arrr+1);

	 //printf("%p\n", &arrr[0]);
	 //printf("%p\n", &arrr[0]+1);

	 //printf("%p\n", &arrr);
	 //printf("%p\n", &arrr+1);//跳过了整个数组
	 //int sz = sizeof(arrr);
	 //printf("%d\n", sz);//实际输出40
	 /*
	 1.数组名通常表示的都是数组首元素地址
	 2.sizeof(数组名),这里数组名表示整个数组,计算的是整个数组的大小
	 3.&数组名,这里的数组名表示的是依然是整个数组,所以&数组名取出的是整个数组的地址
	 */
	 int f[] = { 1,2,3,4,5,6,7,8,9,10 };
	 //int (*lf)[10] = &f;
	 //for (int i = 0; i < 10; i++) {//lf是指向数组的,*lf其实相当于数组名,数组名又是
		// //数组首元素的地址,所以*lf本质上是数组首元素的地址
		// printf("%d ", *(*lf + i));
	 //}
	 //但是上述的方式并不好用
	 int* q = f;
	 for (int i = 0; i < 10; i++) {
		 printf("%d ", *(q + i));
	 }
	 printf("\n");
	 printf("\n");
	 //数组指针常见的用法
	 int w[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
	 print1(w, 3, 5);
	 printf("\n");
	 print2(w, 3, 5);
	 return 0;
}
void print1(int arr[3][5], int r, int c) {
	for (int i = 0; i < r; i++) {
		for (int j = 0;j < c; j++) {
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
void print2(int (*p)[5], int r, int c) {//二维数组的首元素是他的第一行
	for (int i = 0; i < r; i++) {
		for (int j = 0; j < c; j++) {
			//printf("%d ", *((*p+i)+j));
			printf("%d ", p[i][j]);
		}
		printf("\n");
	}
}

四.指针数组

指针数组(Array of Pointers)是数组元素全部为指针类型的数组,本质是 “数组”,数组的每个元素存储的是内存地址(指针值)。它是 C/C++ 中灵活且常用的复合类型,尤其适合处理字符串、多维数组简化、动态内存管理等场景。

一、核心代码复刻与逐行解析

还原笔记中的核心代码,逐行拆解原理:

#include <stdio.h>

int main() {
    // 定义3个普通一维int数组
    int s1[] = {1,2,3,4,5};
    int s2[] = {2,3,4,5,6};
    int s3[] = {3,4,5,6,7};
    
    // 核心:定义并初始化指针数组
    int* ps[3] = {s1, s2, s3}; 
    
    // 遍历指针数组(模拟二维数组访问)
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            // 两种等价访问方式
            // printf("%d ", *(ps[i] + j)); // 指针偏移法
            printf("%d ", ps[i][j]);       // 数组下标法
        }
        printf("\n");
    }
    return 0;
}

1. 基础定义行拆解

(1)普通数组 s1/s2/s3 的本质
int s1[] = {1,2,3,4,5};
  • s1 是数组名,编译器会将其解析为「指向数组首元素的常量指针」(int* const 类型);
  • s1 本身存储的是数组首元素的内存地址(如 0x7ffeefbff5c0),不可被修改(s1 = &a 会报错);
  • s1[j] 等价于 *(s1 + j),即「首地址偏移 j 个 int 长度后解引用」。
(2)指针数组 int* ps[3] = {s1, s2, s3}; 核心拆解
  • 语法优先级[] 优先级高于 *,因此 ps 先与 [] 结合,确定是「数组」,再与 * 结合,确定数组元素是「int 类型指针」;
  • 初始化逻辑s1/s2/s3 作为数组名,自动转换为指向自身首元素的指针,因此可以直接赋值给 ps 的元素;
  • ps 的内存属性
    • ps 是数组首地址(栈内存),占用 3 * 指针长度 字节(64 位系统为 24 字节);
    • ps[0] = s1 → ps[0] 存储 s1 的首地址(如 0x7ffeefbff5c0);
    • ps[1] = s2 → ps[1] 存储 s2 的首地址(如 0x7ffeefbff5d4);
    • ps[2] = s3 → ps[2] 存储 s3 的首地址(如 0x7ffeefbff5e8)。

2. 遍历逻辑拆解

printf("%d ", ps[i][j]); // 等价于 *(ps[i] + j)
分步解析 ps[i][j] 的寻址过程(以 ps[0][1] 为例):
  1. 取 ps[0]:从指针数组 ps 的第 0 个位置,取出 s1 的首地址(如 0x7ffeefbff5c0);
  2. 计算 ps[0] + 1:在 s1 首地址基础上,偏移 1 * sizeof(int) 字节(64 位系统偏移 4 字节),得到 0x7ffeefbff5c4
  3. 解引用 *(ps[0] + 1):访问 0x7ffeefbff5c4 地址的值,即 s1[1] = 2
两种访问方式对比
方式 语法形式 适用场景 底层本质
数组下标法 ps[i][j] 可读性优先,模拟二维数组访问 编译器自动转换为指针偏移
指针偏移法 *(ps[i] + j) 理解指针本质,灵活操作地址 手动控制地址偏移

二、基于该代码的内存模型可视化

以 64 位系统为例,画出完整内存布局(地址仅为示例,实际由系统分配):

// 普通数组的内存(栈区)
地址        内容        说明
0x5c0       1           s1[0]
0x5c4       2           s1[1]
0x5c8       3           s1[2]
0x5cc       4           s1[3]
0x5d0       5           s1[4]

0x5d4       2           s2[0]
0x5d8       3           s2[1]
0x5dc       4           s2[2]
0x5e0       5           s2[3]
0x5e4       6           s2[4]

0x5e8       3           s3[0]
0x5ec       4           s3[1]
0x5f0       5           s3[2]
0x5f4       6           s3[3]
0x5f8       7           s3[4]

// 指针数组ps的内存(栈区)
0x600       0x5c0       ps[0] → 指向s1首地址
0x608       0x5d4       ps[1] → 指向s2首地址
0x610       0x5e8       ps[2] → 指向s3首地址
  • 关键结论:ps 本身不存储具体的数组值,仅存储指向 s1/s2/s3 的地址,这是指针数组 “节省内存 + 灵活” 的核心原因。

三、基于核心代码的扩展用法(贴合笔记场景)

1. 改造为 “不规则二维数组”(子数组长度不同)

笔记中的示例是等长数组,扩展为不等长数组更能体现指针数组的优势:

#include <stdio.h>

int main() {
    // 3个长度不同的普通数组
    int s1[] = {1,2,3};       // 长度3
    int s2[] = {2,3,4,5};     // 长度4
    int s3[] = {3,4,5,6,7,8}; // 长度6
    
    int* ps[3] = {s1, s2, s3};
    // 记录每个子数组的长度(核心:普通二维数组无法做到)
    int lengths[] = {3,4,6};
    
    // 遍历不规则数组
    for (int i = 0; i < 3; i++) {
        printf("第%d行(长度%d):", i, lengths[i]);
        for (int j = 0; j < lengths[i]; j++) {
            printf("%d ", ps[i][j]);
        }
        printf("\n");
    }
    return 0;
}

输出:

第0行(长度3):1 2 3 
第1行(长度4):2 3 4 5 
第2行(长度6):3 4 5 6 7 8 

2. 动态修改指针数组的指向(而非修改值)

指针数组的元素是 “可写的指针”,可以随时改变指向,这是普通数组名做不到的:

#include <stdio.h>

int main() {
    int s1[] = {1,2,3,4,5};
    int s2[] = {2,3,4,5,6};
    int s3[] = {3,4,5,6,7};
    int s4[] = {9,8,7,6,5}; // 新增数组
    
    int* ps[3] = {s1, s2, s3};
    
    // 初始遍历
    printf("初始状态:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d ", ps[i][j]);
        }
        printf("\n");
    }
    
    // 修改指针指向(核心操作)
    ps[1] = s4; // 将ps[1]从指向s2改为指向s4
    
    // 修改后遍历
    printf("\n修改后状态:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d ", ps[i][j]);
        }
        printf("\n");
    }
    return 0;
}

输出:

初始状态:
1 2 3 4 5 
2 3 4 5 6 
3 4 5 6 7 

修改后状态:
1 2 3 4 5 
9 8 7 6 5 
3 4 5 6 7 

3. 结合动态内存(malloc)实现 “堆上的指针数组”

笔记中的数组是栈内存,扩展到堆内存更贴近工程实践:(看不懂)

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 1. 动态分配指针数组(堆内存)
    int** ps = (int**)malloc(3 * sizeof(int*));
    if (ps == NULL) { // 必做:空指针检查
        perror("malloc ps failed");
        return 1;
    }
    
    // 2. 为指针数组的每个元素分配指向的数组(堆内存)
    for (int i = 0; i < 3; i++) {
        ps[i] = (int*)malloc(5 * sizeof(int));
        if (ps[i] == NULL) {
            perror("malloc ps[i] failed");
            // 回滚:释放已分配的内存
            for (int j = 0; j < i; j++) free(ps[j]);
            free(ps);
            return 1;
        }
        // 给堆数组赋值
        for (int j = 0; j < 5; j++) {
            ps[i][j] = (i+1) + j; // 模拟s1/s2/s3的赋值逻辑
        }
    }
    
    // 3. 遍历堆上的指针数组
    printf("堆上的指针数组:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d ", ps[i][j]);
        }
        printf("\n");
    }
    
    // 4. 释放内存(反向分配顺序)
    for (int i = 0; i < 3; i++) {
        free(ps[i]); // 释放每个指向的数组
        ps[i] = NULL; // 避免野指针
    }
    free(ps); // 释放指针数组本身
    ps = NULL;
    
    return 0;
}

输出:

堆上的指针数组:
1 2 3 4 5 
2 3 4 5 6 
3 4 5 6 7 

四、代码笔记中易踩的坑(避坑指南)

1. 混淆 “指针数组” 与 “数组指针”

// 你的代码:指针数组(正确)
int* ps[3]; 
// 易错点:写成数组指针(完全不同)
int (*p)[3]; 
特征 指针数组 int* ps[3] 数组指针 int (*p)[3]
本质 数组(3 个 int * 元素) 指针(指向含 3 个 int 的数组)
占用内存 3×8=24 字节(64 位) 8 字节(仅存储一个地址)
访问方式 ps[i][j] (*p)[j] 或 p[0][j]
你的场景适配 ✅ 适合管理多个数组 ❌ 仅能指向固定长度数组

2. 直接修改指针数组指向的字符串常量(若扩展到字符串场景)

// 错误示例(延伸你的代码到字符串)
char* strs[] = {"123", "456", "789"};
strs[0][0] = '9'; // 崩溃!字符串常量存储在只读区
// 正确做法:修改指针指向,而非修改常量值
char buf[] = "923";
strs[0] = buf; // 合法:指针本身可写

3. 野指针访问(未初始化的指针数组)

// 错误示例
int* ps[3]; // 未初始化,元素是随机地址
printf("%d", ps[0][0]); // 崩溃:访问野指针
// 正确做法:初始化后再使用
int s1[] = {1,2,3};
ps[0] = s1; // 先赋值,再访问

4. 栈数组越界(遍历 j 的范围错误)

代码中 j < 5 是正确的(因 s1/s2/s3 长度为 5),若改为 j <=5 则越界:

// 错误示例
for (int j = 0; j <=5; j++) { // j=5时越界
    printf("%d ", ps[i][j]);
}
// 正确做法:用长度数组控制j的范围(如扩展用法1)

五、总结(贴合你的代码笔记)

笔记中的核心代码 int* ps[3] = {s1, s2, s3}; 是指针数组的经典应用 ——用一个数组集中管理多个同类型数组的地址,其核心价值在于:

  1. 模拟二维数组,但比普通二维数组更灵活(支持不规则长度);
  2. 仅存储地址,节省内存(尤其当子数组长度差异大时);
  3. 可动态修改指向,适配运行时变化的场景;

掌握该代码的关键是:分清 “指针数组存储的是地址”,而非存储具体值,所有对 ps[i][j] 的访问,本质都是 “先找地址,再取值”。在此基础上扩展到动态内存、函数指针数组等场景,就能完全吃透指针数组的用法。

五.指针数组和数组指针有什么区别

指针数组和数组指针是 C 语言中极易混淆的两个概念,核心区别在于本质不同:指针数组是「数组」(元素为指针),数组指针是「指针」(指向数组)。

一、核心定义与语法拆解(先分清写法)

1. 指针数组
int* ps[3]; // 指针数组
  • 语法优先级[](数组)优先级 > *(指针),因此 ps 先与 [] 结合,确定是「数组」,再与 * 结合,说明数组的每个元素是 int 类型指针
  • 通俗理解:「装指针的数组」,数组里存的都是地址。
2. 数组指针
int (*p)[3]; // 数组指针
  • 语法优先级() 提升优先级,p 先与 * 结合,确定是「指针」,再与 [] 结合,说明指针指向一个包含 3 个 int 元素的一维数组
  • 通俗理解:「指向数组的指针」,指针里只存一个地址(整个数组的首地址)。

二、核心区别对比表(关键维度)

对比维度 指针数组 int* ps[3] 数组指针 int (*p)[3]
本质 数组(长度 3,元素为 int*) 指针(仅 1 个指针,指向 int [3] 数组)
内存占用 64 位系统:3×8=24 字节(存储 3 个地址) 64 位系统:8 字节(仅存储 1 个地址)
初始化方式 需赋值多个指针(如指向多个数组) 需赋值一个数组的地址(指向单个数组)
访问元素 ps[i][j](先取第 i 个指针,再偏移 j) (*p)[j] 或 p[0][j](先解引用指针,再取第 j 个元素)
可指向的数组 元素可指向不同长度的数组(灵活) 只能指向固定长度的数组([3]
你的笔记适配 ✅ 适合管理 s1/s2/s3 多个数组 ❌ 仅能指向单个长度为 3 的数组

三、结合代码笔记,实战拆解区别

1. 指针数组

核心代码:

int s1[] = {1,2,3,4,5};
int s2[] = {2,3,4,5,6};
int s3[] = {3,4,5,6,7};
int* ps[3] = {s1, s2, s3}; // 指针数组
  • 内存逻辑ps 是数组,存储 3 个地址(s1、s2、s3 的首地址),每个地址可指向不同长度的数组(如 s1 长度 5、s2 长度 5,也可改为不同长度);
  • 访问逻辑ps[1][2] → 先取 ps[1](s2 的首地址),再偏移 2 个 int,得到 s2 [2]=4;
  • 核心价值:批量管理多个独立数组,模拟 “不规则二维数组”。
2. 数组指针(对比场景)
int s1[] = {1,2,3,4,5};
int (*p)[5] = &s1; // 数组指针(指向长度为5的int数组)
// 错误尝试:p = s2; → 若s2长度不是5,编译报错
// 错误尝试:p = {s1,s2,s3}; → 数组指针只能存一个地址,无法装多个
  • 内存逻辑p 仅存储 s1 的整个数组地址(&s1),而非 s1 首元素地址(s1,但值相同,语义不同);
  • 访问逻辑(*p)[2] → 解引用 p(得到整个 s1 数组),再取第 2 个元素 = 3;p[0][2] 是等价写法(编译器语法糖);
  • 核心价值:专门指向 “固定长度的一维数组”,常用于遍历普通二维数组(如 int arr[2][3],数组指针可指向每行)。

四、典型用法对比(代码示例)

1. 指针数组的典型用法
// 管理多个不同长度的数组(指针数组的优势)
int a[] = {1,2};
int b[] = {3,4,5};
int c[] = {6,7,8,9};
int* ps[3] = {a, b, c}; // 合法,元素可指向不同长度数组
int lengths[] = {2,3,4};
// 遍历
for (int i=0; i<3; i++) {
    for (int j=0; j<lengths[i]; j++) {
        printf("%d ", ps[i][j]); // 灵活遍历不同长度
    }
    printf("\n");
}
2. 数组指针的典型用法(遍历普通二维数组)
// 普通二维数组(每行长度固定)
int arr[2][3] = {
   
     {1,2,3}, {4,5,6}};
int (*p)[3] = arr; // 数组指针指向arr的第一行(arr等价于&arr[0])
// 遍历
for (int i=0; i<2; i++) {
    for (int j=0; j<3; j++) {
        printf("%d ", (*(p+i))[j]); // p+i 指向第i行,解引用后取第j个元素
    }
    printf("\n");
}

五、易踩坑的关键细节

1. 地址值相同,但语义不同(数组指针的坑)
int arr[3] = {1,2,3};
printf("arr = %p\n", arr);     // 输出:数组首元素地址(如0x7ffeefbff5c0)
printf("&arr = %p\n", &arr);   // 输出:整个数组的地址(同0x7ffeefbff5c0)
// 但类型不同:
int* p1 = arr;    // 合法(p1指向int)
int (*p2)[3] = &arr; // 合法(p2指向int[3])
int (*p3)[3] = arr;  // 编译器警告(类型不匹配,但值可运行)
  • 指针数组无需关注此问题,数组指针必须匹配「指向的数组长度」,否则易出问题。
2. 指针数组可修改元素指向,数组指针可修改指向的数组(但长度固定)
// 指针数组:修改元素指向(灵活)
int* ps[3] = {s1, s2, s3};
ps[1] = s4; // 合法,指向新数组

// 数组指针:修改指向的数组(但长度必须匹配)
int (*p)[5] = &s1;
p = &s2; // 合法(s2长度5)
p = &s3; // 若s3长度不是5,编译报错

六、总结:怎么快速区分?

  1. 看写法
    • 指针数组:类型* 数组名[长度]* 在数组名前,无括号);
    • 数组指针:类型 (*指针名)[长度]* 被括号包裹,紧跟指针名)。
  2. 看用途
    • 要「管理多个数组 / 指针」→ 用指针数组(如你的笔记场景);
    • 要「指向一个固定长度的数组」→ 用数组指针(如遍历普通二维数组)。
  3. 记口诀
    • 指针数组:先数组,后指针 → 装指针的数组;
    • 数组指针:先指针,后数组 → 指向数组的指针。

结合之前的代码笔记,int* ps[3] = {s1, s2, s3} 之所以用指针数组,是因为需要管理 3 个独立的数组(s1/s2/s3),而数组指针只能指向单个固定长度的数组,无法满足 “批量管理多个数组” 的需求 —— 这也是两者最核心的场景差异。