12.1 字符串与字符串 I/O:定义字符串 + 指针操作字符串【C 语言保姆级教程】
C 语言没有像 Python、Java 那样的string专属类型 ——字符串在 C 语言中本质是 “以'\0'(空字符,ASCII 码为 0)结尾的字符数组”。忘记加'\0'导致字符串打印乱码;混淆 “字符数组” 和 “字符指针”,修改字符串时程序崩溃;分不清sizeof和字符串实际长度的区别;指针操作字符串时出现野指针、越界访问。本文会从 “底层原理→语法规则→代码实战→避坑拆解” 四个维度,
C语言学习栏目目录
原创不易,转载请注明出处 ✨ 本文是 C 语言字符串系列的核心入门篇,超详细拆解“如何定义字符串” 和 “指针操作字符串”,从内存底层原理到逐行代码解析,零基础也能彻底吃透!
前言:为什么字符串是 C 语言的 “重点 + 难点”?
12.1.1 在程序中定义字符串(4 种方式 + 文字版内存解析)
方式 1:字符数组逐个初始化(手动加'\0')
方式 2:字符数组直接赋值字符串(自动加'\0')
方式 3:省略数组大小的字符数组
方式 4:字符指针指向字符串字面量
12.1.2 指针和字符串(底层原理 + 超详细实战)
指针操作字符串的核心原理(新手必背)
实战 1:用指针遍历字符串(统计长度)
实战 2:字符数组 vs 字符指针(核心区别超详细对比)
实战 3:指针实现字符串拷贝(手写 strcpy)
指针操作字符串的避坑指南(新手必看)
超详细总结
结尾语
前言:为什么字符串是 C 语言的 “重点 + 难点”?
C 语言没有像 Python、Java 那样的string专属类型 ——字符串在 C 语言中本质是 “以'\0'(空字符,ASCII 码为 0)结尾的字符数组”。
新手学习字符串最容易踩的坑:
- 忘记加
'\0'导致字符串打印乱码; - 混淆 “字符数组” 和 “字符指针”,修改字符串时程序崩溃;
- 分不清
sizeof和字符串实际长度的区别; - 指针操作字符串时出现野指针、越界访问。
本文会从 “底层原理→语法规则→代码实战→避坑拆解” 四个维度,把字符串的定义和指针操作讲透,每个知识点都配文字版内存解析 + 逐行代码解析,确保所有平台都能正常显示,新手可直接照抄练习!
12.1.1 在程序中定义字符串(4 种方式 + 文字版内存解析)
字符串的核心要求:连续的字符序列 + 末尾必须有'\0'终止符('\0'是字符串的 “结束标志”,没有它,程序无法判断字符串在哪里结束)。
下面是 4 种定义字符串的方式,每种都包含「语法 + 代码 + 文字版内存解析 + 注意事项」,彻底替代之前的图表,确保清晰易懂。
方式 1:字符数组逐个初始化(手动加'\0')
语法规则
char 数组名[数组大小] = {'字符1', '字符2', ..., '字符n', '\0'};
数组大小必须 ≥ 实际字符数 + 1(多出来的 1 个位置留给'\0')。
完整代码示例
#include <stdio.h>
int main(void)
{
// 定义:"Hello"有5个字符,数组大小设为6(5+1),最后一个元素手动加'\0'
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 打印整个字符串(%s会从首地址读到'\0'停止)
printf("1. 完整字符串:str1 = %s\n", str1);
// 逐字符打印(验证每个元素的值)
printf("2. 逐字符解析:\n");
for (int i = 0; i < 6; i++) {
// 打印下标、字符、ASCII值('\0'的ASCII是0)
printf(" str1[%d] = '%c' (ASCII码:%d)\n", i, str1[i], str1[i]);
}
return 0;
}
输出结果
1. 完整字符串:str1 = Hello
2. 逐字符解析:
str1[0] = 'H' (ASCII码:72)
str1[1] = 'e' (ASCII码:101)
str1[2] = 'l' (ASCII码:108)
str1[3] = 'l' (ASCII码:108)
str1[4] = 'o' (ASCII码:111)
str1[5] = '' (ASCII码:0)
文字版内存解析(新手必看)
字符串str1存储在栈区(可读写区域),内存地址是连续的(示例地址仅用于理解,实际运行时会变化):
| 内存地址(示例) | 存储内容(字符 / ASCII) | 说明 |
|---|---|---|
| 0x7ffeefbff5a0 | 'H'(72) | 数组第 0 个元素(首字符) |
| 0x7ffeefbff5a1 | 'e'(101) | 数组第 1 个元素 |
| 0x7ffeefbff5a2 | 'l'(108) | 数组第 2 个元素 |
| 0x7ffeefbff5a3 | 'l'(108) | 数组第 3 个元素 |
| 0x7ffeefbff5a4 | 'o'(111) | 数组第 4 个元素 |
| 0x7ffeefbff5a5 | '\0'(0) | 字符串结束标志 |
关键:内存地址从低到高依次存储数组元素,每个字符占 1 字节,
'\0'是必须的结束标志。
新手注意事项
- 如果忘记加
'\0',比如char str1[5] = {'H','e','l','l','o'},用%s打印时会出现乱码(程序会继续读取内存中str1[5]之后的随机数据,直到遇到'\0'); - 数组大小不能小于字符数 + 1,比如
char str1[5] = {'H','e','l','l','o','\0'}会触发编译报错(数组越界)。
方式 2:字符数组直接赋值字符串(自动加'\0')
这是实际开发中最常用的方式,编译器会自动在字符串末尾添加'\0',无需手动处理。
语法规则
char 数组名[数组大小] = "字符串内容";
完整代码示例
#include <stdio.h>
int main(void)
{
// 定义:数组大小10,赋值"World"(5个字符),编译器自动加'\0'
char str2[10] = "World";
// 打印字符串
printf("1. 完整字符串:str2 = %s\n", str2);
// 关键对比:数组总大小 vs 字符串实际长度
// sizeof(str2):计算数组占用的总字节数(数组大小×单个字符字节数)
printf("2. 数组总字节数:sizeof(str2) = %zu\n", sizeof(str2));
// 手动统计字符串实际长度(从首字符到'\0'的字符数,不含'\0')
int actual_len = 0;
while (str2[actual_len] != '\0') {
actual_len++;
}
printf("3. 字符串实际长度(不含\\0):%d\n", actual_len);
// 验证未使用的数组位置的值(默认是'\0')
printf("4. 未使用位置的默认值:\n");
for (int i = 5; i < 10; i++) {
printf(" str2[%d] = '%c' (ASCII码:%d)\n", i, str2[i], str2[i]);
}
return 0;
}
输出结果
1. 完整字符串:str2 = World
2. 数组总字节数:sizeof(str2) = 10
3. 字符串实际长度(不含\0):5
4. 未使用位置的默认值:
str2[5] = '' (ASCII码:0)
str2[6] = '' (ASCII码:0)
str2[7] = '' (ASCII码:0)
str2[8] = '' (ASCII码:0)
str2[9] = '' (ASCII码:0)
文字版内存解析
str2同样存储在栈区,数组大小 10,前 6 个位置存储字符串及'\0',剩余位置自动填充'\0':
| 内存地址(示例) | 存储内容(字符 / ASCII) | 说明 |
|---|---|---|
| 0x7ffeefbff5b0 | 'W'(87) | 数组第 0 个元素 |
| 0x7ffeefbff5b1 | 'o'(111) | 数组第 1 个元素 |
| 0x7ffeefbff5b2 | 'r'(114) | 数组第 2 个元素 |
| 0x7ffeefbff5b3 | 'l'(108) | 数组第 3 个元素 |
| 0x7ffeefbff5b4 | 'd'(100) | 数组第 4 个元素 |
| 0x7ffeefbff5b5 | '\0'(0) | 编译器自动添加的结束标志 |
| 0x7ffeefbff5b6 | '\0'(0) | 未使用位置,默认填充 |
| 0x7ffeefbff5b7 | '\0'(0) | 未使用位置,默认填充 |
| 0x7ffeefbff5b8 | '\0'(0) | 未使用位置,默认填充 |
| 0x7ffeefbff5b9 | '\0'(0) | 未使用位置,默认填充 |
新手注意事项
- 数组大小建议 “留有余量”:比如存储 “手机号 13800138000”(11 位),数组大小至少设为 12(11+1),避免后续拼接字符串时越界;
sizeof(数组名)计算的是数组总字节数,不是字符串实际长度(实际长度需要手动统计或用strlen函数)。
方式 3:省略数组大小的字符数组
如果不确定字符串长度,可省略数组大小,编译器会根据字符串内容自动计算数组长度(包含'\0')。
语法规则
char 数组名[] = "字符串内容";
完整代码示例
#include <stdio.h>
int main(void)
{
// 定义:省略数组大小,编译器自动计算为6("Hello"5个字符 + '\0')
char str3[] = "Hello";
printf("1. 字符串:str3 = %s\n", str3);
// 验证数组总长度(自动计算为6)
printf("2. 数组总字节数:sizeof(str3) = %zu\n", sizeof(str3));
// 验证每个元素
printf("3. 所有元素值:\n");
for (int i = 0; i < sizeof(str3)/sizeof(char); i++) {
printf(" str3[%d] = '%c' (ASCII:%d)\n", i, str3[i], str3[i]);
}
return 0;
}
输出结果
1. 字符串:str3 = Hello
2. 数组总字节数:sizeof(str3) = 6
3. 所有元素值:
str3[0] = 'H' (ASCII:72)
str3[1] = 'e' (ASCII:101)
str3[2] = 'l' (ASCII:108)
str3[3] = 'l' (ASCII:108)
str3[4] = 'o' (ASCII:111)
str3[5] = '' (ASCII:0)
文字版内存解析
编译器自动计算数组长度为 6,存储在栈区,地址连续且刚好容纳字符串 +'\0':
| 内存地址(示例) | 存储内容(字符 / ASCII) | 说明 |
|---|---|---|
| 0x7ffeefbff5c0 | 'H'(72) | 数组第 0 个元素 |
| 0x7ffeefbff5c1 | 'e'(101) | 数组第 1 个元素 |
| 0x7ffeefbff5c2 | 'l'(108) | 数组第 2 个元素 |
| 0x7ffeefbff5c3 | 'l'(108) | 数组第 3 个元素 |
| 0x7ffeefbff5c4 | 'o'(111) | 数组第 4 个元素 |
| 0x7ffeefbff5c5 | '\0'(0) | 自动添加的结束标志 |
新手注意事项
- 优势:修改字符串内容时,无需同步修改数组大小(比如把
"Hello"改成"Hello World",编译器会自动重新计算数组长度); - 缺点:数组长度固定,初始化后无法扩展(比如想在
str3后拼接" C",会导致数组越界)。
方式 4:字符指针指向字符串字面量
用char*(字符指针)指向字符串字面量(双引号""定义的字符串),是指针操作字符串的基础。
语法规则
char *指针名 = "字符串内容";
完整代码示例
#include <stdio.h>
int main(void)
{
// 定义:字符指针str4指向字符串字面量"Hello C"的首地址
char *str4 = "Hello C";
printf("1. 字符串:str4 = %s\n", str4);
// 方式1:下标访问(和数组一样)
printf("2. 下标访问 str4[0] = %c\n", str4[0]); // 输出'H'
printf("3. 下标访问 str4[6] = %c\n", str4[6]); // 输出'C'
// 方式2:指针偏移访问(核心!)
printf("4. 指针偏移 *(str4+1) = %c\n", *(str4+1)); // 输出'e'
printf("5. 指针偏移 *(str4+5) = %c\n", *(str4+5)); // 输出空格
// 打印指针本身的值(字符串首地址)
printf("6. str4指针存储的地址:%p\n", str4);
printf("7. str4+1的地址:%p\n", str4+1); // 地址+1(char占1字节)
return 0;
}
输出结果
1. 字符串:str4 = Hello C
2. 下标访问 str4[0] = H
3. 下标访问 str4[6] = C
4. 指针偏移 *(str4+1) = e
5. 指针偏移 *(str4+5) =
6. str4指针存储的地址:0x100003f00
7. str4+1的地址:0x100003f01
文字版内存解析(关键!)
这种方式的内存分布分为两部分,指针在栈区,字符串字面量在只读区:
| 区域 | 内存地址(示例) | 存储内容 | 说明 |
|---|---|---|---|
| 栈区(可写) | 0x7ffeefbff5d0 | 0x100003f00(字符串首地址) | str4 指针本身存储的内容 |
| 只读区(不可写) | 0x100003f00 | 'H'(72) | 字符串字面量的首字符 |
| 只读区(不可写) | 0x100003f01 | 'e'(101) | 字符串第 1 个字符 |
| 只读区(不可写) | 0x100003f02 | 'l'(108) | 字符串第 2 个字符 |
| 只读区(不可写) | 0x100003f03 | 'l'(108) | 字符串第 3 个字符 |
| 只读区(不可写) | 0x100003f04 | 'o'(111) | 字符串第 4 个字符 |
| 只读区(不可写) | 0x100003f05 | ' '(32) | 字符串第 5 个字符(空格) |
| 只读区(不可写) | 0x100003f06 | 'C'(67) | 字符串第 6 个字符 |
| 只读区(不可写) | 0x100003f07 | '\0'(0) | 自动添加的结束标志 |
[!WARNING]重点:字符串字面量存储在内存的只读数据区(.rodata 段),只能读取不能修改,通过指针修改会导致程序崩溃!比如
str4[0] = 'h'看似简单,实际运行时会触发 “段错误(Segmentation fault)”。
12.1.2 指针和字符串(底层原理 + 超详细实战)
字符串的本质是 “连续的字符内存块”,指针的本质是 “存储内存地址的变量”—— 用指针操作字符串,就是通过地址直接访问 / 修改字符,效率比数组下标更高(少了下标计算步骤)。
指针操作字符串的核心原理(新手必背)
| 表达式 | 含义 |
|---|---|
char *p = str |
p 指向字符串 str 的首字符地址(str 可以是字符数组 / 字符串字面量) |
p[i] |
等价于*(p+i):通过指针偏移访问第 i 个字符(下标从 0 开始) |
*p |
取 p 指向的地址中的字符(即字符串第一个字符) |
p++ |
指针后移 1 字节(char 占 1 字节),指向下一个字符 |
*p == '\0' |
判断是否到字符串末尾(终止条件) |
实战 1:用指针遍历字符串(统计长度)
需求
不使用strlen函数,用指针手动统计字符串长度(不含'\0')。
分步解析
- 定义字符指针指向字符串首地址;
- 初始化长度变量
len=0; - 循环判断
*p != '\0':满足则长度 + 1,指针后移; - 循环结束后,
len就是字符串实际长度。
完整代码
#include <stdio.h>
int main(void)
{
char *str = "C Language"; // 目标字符串
int len = 0; // 存储长度
char *p = str; // 指针p指向字符串首地址
printf("遍历过程:\n");
// 循环遍历:直到*p为'\0'停止
while (*p != '\0') {
printf(" p当前地址:%p → 字符:'%c' → 长度暂存:%d\n", p, *p, len+1);
len++; // 长度+1
p++; // 指针后移1字节
}
// 最终结果
printf("\n字符串:%s\n", str);
printf("字符串实际长度(不含\\0):%d\n", len);
return 0;
}
输出结果
遍历过程:
p当前地址:0x100003f10 → 字符:'C' → 长度暂存:1
p当前地址:0x100003f11 → 字符:' ' → 长度暂存:2
p当前地址:0x100003f12 → 字符:'L' → 长度暂存:3
p当前地址:0x100003f13 → 字符:'a' → 长度暂存:4
p当前地址:0x100003f14 → 字符:'n' → 长度暂存:5
p当前地址:0x100003f15 → 字符:'g' → 长度暂存:6
p当前地址:0x100003f16 → 字符:'u' → 长度暂存:7
p当前地址:0x100003f17 → 字符:'a' → 长度暂存:8
p当前地址:0x100003f18 → 字符:'g' → 长度暂存:9
p当前地址:0x100003f19 → 字符:'e' → 长度暂存:10
字符串:C Language
字符串实际长度(不含\0):10
实战 2:字符数组 vs 字符指针(核心区别超详细对比)
新手最易混淆的两个概念,用表格 + 代码 + 文字版内存解析彻底讲透。
核心区别表
| 对比维度 | 字符数组(char str [] = "xxx") | 字符指针(char *str = "xxx") |
|---|---|---|
| 内存存储位置 | 栈区(可读写区域) | 指针存在栈区,指向只读数据区的字符串字面量 |
| 内容可修改 | ✅ 可以修改(比如 str [0] = 'h') | ❌ 直接修改会崩溃(只读区不可写) |
| 数组大小 | 固定(初始化时确定,可通过 sizeof 获取) | 无大小(仅存储地址,sizeof (str) 是指针长度,通常 8 字节) |
| 赋值方式 | 仅初始化时可整体赋值,后续只能逐字符赋值 | 可重新指向其他字符串(比如 str = "new str") |
| 底层本质 | 一块连续的字符内存块 | 一个存储地址的变量 |
代码验证(逐行解析)
#include <stdio.h>
int main(void)
{
// ========== 第一部分:字符数组(可修改) ==========
char str_arr[] = "Hello"; // 存储在栈区,可读写
printf("【字符数组】初始值:%s\n", str_arr);
// 修改第一个字符为小写h
str_arr[0] = 'h';
printf("【字符数组】修改后:%s\n", str_arr); // 输出hello
// 打印数组大小(6字节:5字符+'\0')
printf("【字符数组】sizeof(str_arr) = %zu\n\n", sizeof(str_arr));
// ========== 第二部分:字符指针(不可直接修改) ==========
char *str_ptr = "World"; // 指针指向只读区的字符串字面量
printf("【字符指针】初始值:%s\n", str_ptr);
// str_ptr[0] = 'w'; // ❌ 注释打开会崩溃!只读区不可写
// 打印指针大小(8字节,64位系统指针长度固定为8)
printf("【字符指针】sizeof(str_ptr) = %zu\n", sizeof(str_ptr));
// 指针可重新指向其他字符串(合法!)
str_ptr = "New World";
printf("【字符指针】重新指向后:%s\n\n", str_ptr);
// ========== 第三部分:指针指向字符数组(可修改) ==========
char temp[] = "World"; // 先定义字符数组(栈区,可写)
char *str_ptr2 = temp; // 指针指向数组首地址
printf("【指针指向数组】初始值:%s\n", str_ptr2);
str_ptr2[0] = 'w'; // ✅ 合法:修改的是栈区的数组
printf("【指针指向数组】修改后:%s\n", str_ptr2); // 输出world
return 0;
}
输出结果
【字符数组】初始值:Hello
【字符数组】修改后:hello
【字符数组】sizeof(str_arr) = 6
【字符指针】初始值:World
【字符指针】sizeof(str_ptr) = 8
【字符指针】重新指向后:New World
【指针指向数组】初始值:World
【指针指向数组】修改后:world
文字版内存解析(关键!)
(1)字符数组(str_arr)的内存分布
| 区域 | 内存地址(示例) | 存储内容(字符 / ASCII) | 说明 |
|---|---|---|---|
| 栈区 | 0x7ffeefbff5e0 | 'h'(104) | 修改后的第 0 个元素 |
| 栈区 | 0x7ffeefbff5e1 | 'e'(101) | 第 1 个元素 |
| 栈区 | 0x7ffeefbff5e2 | 'l'(108) | 第 2 个元素 |
| 栈区 | 0x7ffeefbff5e3 | 'l'(108) | 第 3 个元素 |
| 栈区 | 0x7ffeefbff5e4 | 'o'(111) | 第 4 个元素 |
| 栈区 | 0x7ffeefbff5e5 | '\0'(0) | 结束标志 |
(2)字符指针(str_ptr)的内存分布
| 区域 | 内存地址(示例) | 存储内容 | 说明 |
|---|---|---|---|
| 栈区 | 0x7ffeefbff5f0 | 0x100003f20(初始地址) | 初始指向 "World" 的首地址 |
| 栈区 | 0x7ffeefbff5f0 | 0x100003f30(修改后地址) | 重新指向 "New World" 的首地址 |
| 只读区 | 0x100003f20 | 'W'(87) | "World" 的首字符(不可修改) |
| 只读区 | 0x100003f21 | 'o'(111) | "World" 的第 1 个字符 |
| ... | ... | ... | ... |
| 只读区 | 0x100003f25 | '\0'(0) | "World" 的结束标志 |
| 只读区 | 0x100003f30 | 'N'(78) | "New World" 的首字符(不可修改) |
| ... | ... | ... | ... |
(3)指针指向数组(str_ptr2)的内存分布
| 区域 | 内存地址(示例) | 存储内容(字符 / ASCII) | 说明 |
|---|---|---|---|
| 栈区 | 0x7ffeefbff600 | 'w'(119) | 修改后的第 0 个元素 |
| 栈区 | 0x7ffeefbff601 | 'o'(111) | 第 1 个元素 |
| 栈区 | 0x7ffeefbff602 | 'r'(114) | 第 2 个元素 |
| 栈区 | 0x7ffeefbff603 | 'l'(108) | 第 3 个元素 |
| 栈区 | 0x7ffeefbff604 | 'd'(100) | 第 4 个元素 |
| 栈区 | 0x7ffeefbff605 | '\0'(0) | 结束标志 |
| 栈区 | 0x7ffeefbff610 | 0x7ffeefbff600 | str_ptr2 指针存储的数组首地址 |
实战 3:指针实现字符串拷贝(手写 strcpy)
需求
手动实现strcpy函数功能:将源字符串src拷贝到目标字符串dest,掌握指针操作字符串的核心逻辑。
核心思路
- 用两个指针分别指向
dest和src的首地址; - 循环将
*src赋值给*dest,直到*src为'\0'; - 最后给
dest末尾手动加'\0'(确保目标字符串完整); - 用
const修饰src指针,防止误修改源字符串(编程好习惯)。
完整代码(逐行注释)
#include <stdio.h>
/**
* 自定义字符串拷贝函数
* @param dest 目标字符串(字符数组,需足够大)
* @param src 源字符串(用const保护,防止修改)
*/
void my_strcpy(char *dest, const char *src)
{
// 1. 定义临时指针,避免修改原指针(保留首地址)
char *p_dest = dest;
const char *p_src = src;
// 2. 循环拷贝:直到*p_src为'\0'
printf("拷贝过程:\n");
while (*p_src != '\0') {
*p_dest = *p_src; // 将源字符赋值给目标字符
printf(" src地址:%p → '%c' → dest地址:%p\n", p_src, *p_src, p_dest);
p_dest++; // 目标指针后移
p_src++; // 源指针后移
}
// 3. 目标字符串末尾加'\0'(关键!)
*p_dest = '\0';
printf(" 最后给dest加\\0:dest地址:%p → 值:%d\n", p_dest, *p_dest);
}
int main(void)
{
char dest[20]; // 目标数组(大小20,足够存储源字符串)
char *src = "Hello C Language"; // 源字符串
// 调用自定义拷贝函数
my_strcpy(dest, src);
// 验证结果
printf("\n拷贝完成:\n");
printf(" 源字符串:%s\n", src);
printf(" 目标字符串:%s\n", dest);
return 0;
}
输出结果
拷贝过程:
src地址:0x100003f40 → 'H' → dest地址:0x7ffeefbff620
src地址:0x100003f41 → 'e' → dest地址:0x7ffeefbff621
src地址:0x100003f42 → 'l' → dest地址:0x7ffeefbff622
src地址:0x100003f43 → 'l' → dest地址:0x7ffeefbff623
src地址:0x100003f44 → 'o' → dest地址:0x7ffeefbff624
src地址:0x100003f45 → ' ' → dest地址:0x7ffeefbff625
src地址:0x100003f46 → 'C' → dest地址:0x7ffeefbff626
src地址:0x100003f47 → ' ' → dest地址:0x7ffeefbff627
src地址:0x100003f48 → 'L' → dest地址:0x7ffeefbff628
src地址:0x100003f49 → 'a' → dest地址:0x7ffeefbff629
src地址:0x100003f4a → 'n' → dest地址:0x7ffeefbff62a
src地址:0x100003f4b → 'g' → dest地址:0x7ffeefbff62b
src地址:0x100003f4c → 'u' → dest地址:0x7ffeefbff62c
src地址:0x100003f4d → 'a' → dest地址:0x7ffeefbff62d
src地址:0x100003f4e → 'g' → dest地址:0x7ffeefbff62e
src地址:0x100003f4f → 'e' → dest地址:0x7ffeefbff62f
最后给dest加\0:dest地址:0x7ffeefbff630 → 值:0
拷贝完成:
源字符串:Hello C Language
目标字符串:Hello C Language
指针操作字符串的避坑指南(新手必看)
坑 1:修改字符串字面量(最常见)
char *p = "test";
p[0] = 'T'; // ❌ 运行崩溃!字符串字面量在只读区,不可写
解决方案:先定义字符数组,再用指针指向数组:
char temp[] = "test";
char *p = temp;
p[0] = 'T'; // ✅ 合法
坑 2:野指针访问字符串
char *p; // ❌ 未初始化的野指针,指向随机地址
printf("%s\n", p); // 运行崩溃!
解决方案:指针必须先指向有效内存(字符数组 / 合法字符串):
char *p = "valid string"; // ✅ 指向合法字符串字面量
// 或
char temp[] = "valid string";
char *p = temp; // ✅ 指向字符数组
坑 3:忘记加'\0'导致乱码
char str[5] = {'a','b','c','d','e'}; // ❌ 无'\0'
printf("%s\n", str); // 输出abcde+乱码(越界读取)
解决方案:要么手动加'\0',要么直接赋值字符串(自动加'\0'):
char str[6] = {'a','b','c','d','e','\0'}; // ✅ 手动加
// 或
char str[] = "abcde"; // ✅ 自动加
坑 4:目标数组大小不足导致越界
char dest[5]; // ❌ 大小仅5,无法存储"Hello"(5字符+'\0'=6)
my_strcpy(dest, "Hello"); // 数组越界,程序可能崩溃/数据错乱
解决方案:目标数组大小必须≥源字符串长度 + 1:
char dest[6]; // ✅ 大小6,足够存储
my_strcpy(dest, "Hello");
超详细总结
- 字符串本质:以
'\0'结尾的字符数组,'\0'是结束标志,缺失会导致乱码 / 越界; - 字符串定义 4 种方式:
- 逐个初始化(手动加
'\0'):适合需要精准控制每个字符的场景; - 直接赋值字符串(自动加
'\0'):开发中最常用; - 省略数组大小:编译器自动计算长度,灵活但数组长度固定;
- 字符指针指向字面量:指针可重定向,但字面量不可修改;
- 逐个初始化(手动加
- 指针操作字符串核心:
p[i]等价于*(p+i),遍历终止条件是*p == '\0';- 字符数组存储在栈区(可写),字符串字面量存储在只读区(不可写);
- 核心避坑点:
- 不修改字符串字面量,野指针必须先初始化;
- 字符串拷贝时目标数组大小要足够,末尾必须加
'\0'。
掌握这些知识点,你已经能熟练处理 C 语言字符串的基础操作 —— 下一篇我们会讲解字符串输入,带你完成字符串的输入实战,敬请关注!
结尾语
如果本文对你有帮助,欢迎点赞 + 收藏 + 关注!学习过程中有任何疑问,评论区留言,我会第一时间解答~后续会持续更新 C 语言字符串进阶知识点(字符串 I/O、字符串函数、字符串拼接 / 分割),带你从入门到精通!
更多推荐



所有评论(0)