一,字符指针
在 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*指针(如p1、p2),这些指针都会指向同一块内存地址; - 只读数据段的特性:程序运行时不可修改,修改会触发段错误(
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); // 内存泄漏
五、总结
- 字符指针(
char *)是指向字符 / 字符串的地址,核心用于处理字符串; - 指向字符串常量的字符指针不可修改内容,指向字符数组 / 堆内存的可修改;
- 字符指针比字符数组更灵活(可重定向、动态分配),但需注意内存管理;
- 关键禁忌:避免野指针、不修改字符串常量、不越界访问、动态内存记得释放。
二.字符数组
在 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;
}
五、总结
- 字符数组是存储可修改字符串的核心方式,初始化时建议直接用字符串(自动加
'\0'); - 字符数组的内容可修改,但数组名是常量地址,不可重定向;
- 作为函数参数时会退化为字符指针,需手动传递长度或依赖
'\0'; - 核心禁忌:避免无
'\0'、数组越界、依赖函数内sizeof获取数组长度; - 对比字符指针:字符数组适合存储固定长度、需修改的字符串;字符指针适合灵活指向(常量 / 动态内存)字符串。
三.字符指针和字符数组
字符指针(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";。
五、核心联系(二者的互通性)
虽然差异大,但字符指针和字符数组可互通使用:
六、总结(一句话记住核心)
- 字符指针可指向字符数组的首地址(最常用):
char arr[] = "hello"; char* p = arr; // p 指向 arr[0] p[1] = 'E'; // 等价于 arr[1] = 'E',合法(修改栈区内容) - 函数接收字符串时,字符数组和字符指针参数无区别:
char arr[] = "hello"; const char* p = "world"; print_str(arr); // 合法,数组退化为指针 print_str(p); // 合法,直接传指针 - 字符指针:是 “指向字符串的变量指针”,适合只读、动态、灵活指向的字符串;
- 字符数组:是 “存储字符串的固定内存块”,适合可修改、固定长度的字符串;
- 关键区别:内存是否可写 + 标识符是否可重定向。
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] 为例):
- 取
ps[0]:从指针数组ps的第 0 个位置,取出s1的首地址(如0x7ffeefbff5c0); - 计算
ps[0] + 1:在s1首地址基础上,偏移1 * sizeof(int)字节(64 位系统偏移 4 字节),得到0x7ffeefbff5c4; - 解引用
*(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}; 是指针数组的经典应用 ——用一个数组集中管理多个同类型数组的地址,其核心价值在于:
- 模拟二维数组,但比普通二维数组更灵活(支持不规则长度);
- 仅存储地址,节省内存(尤其当子数组长度差异大时);
- 可动态修改指向,适配运行时变化的场景;
掌握该代码的关键是:分清 “指针数组存储的是地址”,而非存储具体值,所有对 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,编译报错
六、总结:怎么快速区分?
- 看写法:
- 指针数组:
类型* 数组名[长度](*在数组名前,无括号); - 数组指针:
类型 (*指针名)[长度](*被括号包裹,紧跟指针名)。
- 指针数组:
- 看用途:
- 要「管理多个数组 / 指针」→ 用指针数组(如你的笔记场景);
- 要「指向一个固定长度的数组」→ 用数组指针(如遍历普通二维数组)。
- 记口诀:
- 指针数组:先数组,后指针 → 装指针的数组;
- 数组指针:先指针,后数组 → 指向数组的指针。
结合之前的代码笔记,int* ps[3] = {s1, s2, s3} 之所以用指针数组,是因为需要管理 3 个独立的数组(s1/s2/s3),而数组指针只能指向单个固定长度的数组,无法满足 “批量管理多个数组” 的需求 —— 这也是两者最核心的场景差异。