1 引言在C语言中字符串可以通过两种方式定义cchar str1[] Hello; /* 字符数组 */ char *str2 Hello; /* 字符指针 */这两种方式看起来都能表示字符串Hello但它们有着本质的区别。下面的代码就能揭示差异c#include stdio.h int main(void) { char str1[] Hello; char *str2 Hello; str1[0] h; /* 可以修改 */ printf(str1: %s\n, str1); /* str2[0] h; */ /* 危险可能导致程序崩溃 */ printf(str1 大小: %zu\n, sizeof(str1)); /* 6包含\0 */ printf(str2 大小: %zu\n, sizeof(str2)); /* 8指针大小 */ return 0; }本章我们将深入探讨这两种方式的区别以及它们在实践中的应用。2 字符数组与字符指针的本质区别2.1 存储位置不同字符数组在栈上分配内存数组的内容可以修改。字符指针指针变量本身在栈上但它指向的字符串常量通常在只读数据区。c#include stdio.h int main(void) { char str1[] Hello; /* 栈上的数组包含副本 */ char *str2 Hello; /* 栈上的指针指向只读区 */ printf(str1 地址: %p\n, str1); /* 栈地址 */ printf(str2 地址: %p\n, str2); /* 只读区地址 */ printf(str2 : %p\n, str2); /* 指针本身的地址栈上 */ return 0; }内存布局示意图text栈区 str1: [H][e][l][l][o][\0] (可修改) str2: [0x400600] (指针变量) 只读数据区 0x400600: Hello (不可修改)2.2 可修改性不同cchar str1[] Hello; str1[0] h; /* 合法修改数组中的字符 */ str1 World; /* 非法数组名是常量不能赋值 */ char *str2 Hello; str2[0] h; /* 危险试图修改只读区可能崩溃 */ str2 World; /* 合法修改指针指向 */总结字符数组内容可改但数组名本身不能改字符指针指针本身可改可指向别处但指向的内容是否可改取决于指向哪里2.3 sizeof 的结果不同cchar str1[] Hello; char *str2 Hello; printf(%zu\n, sizeof(str1)); /* 6整个数组的大小包含\0 */ printf(%zu\n, sizeof(str2)); /* 8指针本身的大小64位系统 */2.4 取地址的结果不同cchar str1[] Hello; char *str2 Hello; printf(%p\n, str1); /* 数组首元素地址 */ printf(%p\n, str1); /* 也是数组首地址但类型是 char (*)[6] */ printf(%p\n, str2); /* 指向的字符串常量的地址 */ printf(%p\n, str2); /* 指针变量本身的地址 */3 字符串常量3.1 字符串常量的本质字符串常量是用双引号括起来的字符序列如Hello。它的本质是一个无名的字符数组存储在程序的只读数据区某些平台也可能在代码段。cconst char *p Hello; /* 指向只读区的字符串常量 */3.2 字符串常量的共享编译器通常会优化相同的字符串常量可能共享同一份存储c#include stdio.h int main(void) { char *p1 Hello; char *p2 Hello; printf(p1 %p\n, p1); printf(p2 %p\n, p2); /* 可能和 p1 相同 */ return 0; }3.3 试图修改字符串常量cchar *p Hello; p[0] h; /* 未定义行为可能导致程序崩溃 */在不同的平台上结果可能不同某些平台程序崩溃段错误某些平台修改成功如果字符串常量在可写段某些平台修改看起来成功但可能影响其他指向同一常量的指针绝对不要修改字符串常量。3.4 用 const 保护字符串常量为了明确表达意图应该用const修饰指向字符串常量的指针cconst char *p Hello; /* 表明不能通过 p 修改指向的内容 */ /* p[0] h; */ /* 编译器会报错而不是运行时崩溃 */4 字符串的初始化4.1 字符数组的初始化c/* 方式1直接用字符串初始化 */ char s1[6] Hello; /* 显式指定大小 */ char s2[] Hello; /* 编译器自动计算大小6 */ /* 方式2字符数组初始化 */ char s3[] {H, e, l, l, o, \0}; /* 手动添加\0 */ /* 注意没有\0就不是字符串 */ char s4[] {H, e, l, l, o}; /* 长度5没有\0不是字符串 */4.2 字符指针的初始化c/* 指向字符串常量 */ const char *p1 Hello; /* 推荐明确是只读 */ char *p2 Hello; /* 不推荐容易误以为可修改 */ /* 指向栈上的数组 */ char arr[] Hello; char *p3 arr; /* p3 指向可修改的数组 */ /* 动态分配内存 */ char *p4 malloc(10); strcpy(p4, Hello); /* p4 指向堆上的字符串 */4.3 区别总结特性字符数组字符指针存储位置栈或静态区指针在栈指向只读区或堆内容是否可改可修改取决于指向哪里指针本身是否可改否数组名是常量是sizeof数组大小指针大小初始化来源拷贝字符串内容存储字符串地址5 指针数组处理字符串表5.1 字符串表的概念在实际编程中经常需要处理一组字符串如菜单选项、命令列表、星期名称等。指针数组是存储这类字符串表的理想方式。c/* 定义字符串表 */ const char *weekdays[] { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; /* 访问 */ for (int i 0; i 7; i) { printf(%s\n, weekdays[i]); }5.2 指针数组 vs 二维字符数组方式1指针数组cconst char *days1[] { Monday, Tuesday, Wednesday };每个字符串长度可以不同字符串存储在只读区不可修改指针数组本身占用的空间较小每个指针8字节方式2二维字符数组cchar days2[][10] { Monday, Tuesday, Wednesday };每个字符串长度固定10字节浪费空间字符串存储在数组内可以修改占用连续空间访问可能更快内存布局对比text指针数组 days1 [ptr] → Monday [ptr] → Tuesday [ptr] → Wednesday 二维数组 days2 [Monday\0 ][Tuesday\0 ][Wednesday\0]5.3 命令行参数 argvmain函数的第二个参数argv就是一个经典的字符串表cint main(int argc, char *argv[]) { printf(程序名%s\n, argv[0]); for (int i 1; i argc; i) { printf(参数 %d%s\n, i, argv[i]); } return 0; }argv是一个指针数组每个元素指向一个命令行参数字符串。5.4 字符串表的遍历c#include stdio.h int main(void) { const char *colors[] { Red, Green, Blue, Yellow, Cyan, Magenta, NULL }; /* 用 NULL 作为结束标志 */ /* 方法1通过下标遍历 */ for (int i 0; colors[i] ! NULL; i) { printf(%s , colors[i]); } printf(\n); /* 方法2通过指针遍历 */ const char **p colors; while (*p ! NULL) { printf(%s , *p); p; } printf(\n); return 0; }6 字符串处理函数中的指针6.1 标准库函数的指针参数理解指针与字符串的关系有助于理解标准库函数的设计csize_t strlen(const char *s); /* 不修改字符串用 const */ char *strcpy(char *dest, const char *src); /* dest 可修改src 只读 */ char *strcat(char *dest, const char *src); /* 同上 */ int strcmp(const char *s1, const char *s2); /* 两者都只读 */6.2 实现简单的字符串函数c#include stdio.h /* 实现 strlen */ size_t my_strlen(const char *s) { const char *p s; while (*p ! \0) { p; } return p - s; /* 指针相减得到长度 */ } /* 实现 strcpy */ char *my_strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) { ; } return ret; } /* 实现 strcmp */ int my_strcmp(const char *s1, const char *s2) { while (*s1 *s2 *s1 *s2) { s1; s2; } return *s1 - *s2; }6.3 字符串指针作为返回值c/* 返回指向字符串常量的指针安全 */ const char* get_weekday(int n) { static const char *weekdays[] { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; if (n 1 n 7) { return weekdays[n-1]; } return Invalid; } /* 返回指向局部数组的指针危险 */ char* get_string_bad(void) { char str[] Hello; /* 局部数组 */ return str; /* 函数返回后数组被销毁 */ }7 常见错误与注意事项7.1 试图修改字符串常量cchar *p Hello; p[0] h; /* 未定义行为可能导致崩溃 */7.2 混淆字符数组和字符指针的初始化cchar str1[10]; str1 Hello; /* 错误数组名不能被赋值 */ char str2[10]; strcpy(str2, Hello); /* 正确 */ char *p; p Hello; /* 正确p 指向字符串常量 */7.3 忘记为 \0 分配空间cchar str[5] Hello; /* Hello 需要6个字符包含\0 */ /* 实际只存储了 H,e,l,l,o没有 \0不是有效字符串 */7.4 返回局部字符数组的地址cchar* get_name(void) { char name[20] Alice; return name; /* 危险name 是局部变量 */ }7.5 用 比较字符串内容cchar *p1 Hello; char *p2 Hello; if (p1 p2) { /* 比较的是地址不是内容可能为真但不能依赖 */ /* ... */ } /* 应该用 strcmp(p1, p2) 0 */7.6 字符串常量不可修改但指针本身可改cconst char *p Hello; p World; /* 合法修改指针指向 */ /* p[0] h; */ /* 非法不能修改内容 */8 本章小结本章系统介绍了指针与字符串的关系1. 字符数组 vs 字符指针特性字符数组字符指针存储栈/静态区指针在栈指向只读区或堆内容可改是取决于指向自身可改否常量是sizeof数组大小指针大小2. 字符串常量存储在只读数据区相同的字符串常量可能共享绝对不能修改应该用const char*指向3. 字符串表的两种方式指针数组每个字符串长度可不同不可修改节省空间二维数组固定长度可修改占用连续空间4. 实际应用命令行参数argv是指针数组标准库字符串函数大量使用指针实现自己的字符串函数需要理解指针操作5. 常见错误修改字符串常量忘记数组和指针的区别返回局部字符数组用比较字符串内容忘记为\0留空间
【C语言程序设计】第30篇:指针与字符串
1 引言在C语言中字符串可以通过两种方式定义cchar str1[] Hello; /* 字符数组 */ char *str2 Hello; /* 字符指针 */这两种方式看起来都能表示字符串Hello但它们有着本质的区别。下面的代码就能揭示差异c#include stdio.h int main(void) { char str1[] Hello; char *str2 Hello; str1[0] h; /* 可以修改 */ printf(str1: %s\n, str1); /* str2[0] h; */ /* 危险可能导致程序崩溃 */ printf(str1 大小: %zu\n, sizeof(str1)); /* 6包含\0 */ printf(str2 大小: %zu\n, sizeof(str2)); /* 8指针大小 */ return 0; }本章我们将深入探讨这两种方式的区别以及它们在实践中的应用。2 字符数组与字符指针的本质区别2.1 存储位置不同字符数组在栈上分配内存数组的内容可以修改。字符指针指针变量本身在栈上但它指向的字符串常量通常在只读数据区。c#include stdio.h int main(void) { char str1[] Hello; /* 栈上的数组包含副本 */ char *str2 Hello; /* 栈上的指针指向只读区 */ printf(str1 地址: %p\n, str1); /* 栈地址 */ printf(str2 地址: %p\n, str2); /* 只读区地址 */ printf(str2 : %p\n, str2); /* 指针本身的地址栈上 */ return 0; }内存布局示意图text栈区 str1: [H][e][l][l][o][\0] (可修改) str2: [0x400600] (指针变量) 只读数据区 0x400600: Hello (不可修改)2.2 可修改性不同cchar str1[] Hello; str1[0] h; /* 合法修改数组中的字符 */ str1 World; /* 非法数组名是常量不能赋值 */ char *str2 Hello; str2[0] h; /* 危险试图修改只读区可能崩溃 */ str2 World; /* 合法修改指针指向 */总结字符数组内容可改但数组名本身不能改字符指针指针本身可改可指向别处但指向的内容是否可改取决于指向哪里2.3 sizeof 的结果不同cchar str1[] Hello; char *str2 Hello; printf(%zu\n, sizeof(str1)); /* 6整个数组的大小包含\0 */ printf(%zu\n, sizeof(str2)); /* 8指针本身的大小64位系统 */2.4 取地址的结果不同cchar str1[] Hello; char *str2 Hello; printf(%p\n, str1); /* 数组首元素地址 */ printf(%p\n, str1); /* 也是数组首地址但类型是 char (*)[6] */ printf(%p\n, str2); /* 指向的字符串常量的地址 */ printf(%p\n, str2); /* 指针变量本身的地址 */3 字符串常量3.1 字符串常量的本质字符串常量是用双引号括起来的字符序列如Hello。它的本质是一个无名的字符数组存储在程序的只读数据区某些平台也可能在代码段。cconst char *p Hello; /* 指向只读区的字符串常量 */3.2 字符串常量的共享编译器通常会优化相同的字符串常量可能共享同一份存储c#include stdio.h int main(void) { char *p1 Hello; char *p2 Hello; printf(p1 %p\n, p1); printf(p2 %p\n, p2); /* 可能和 p1 相同 */ return 0; }3.3 试图修改字符串常量cchar *p Hello; p[0] h; /* 未定义行为可能导致程序崩溃 */在不同的平台上结果可能不同某些平台程序崩溃段错误某些平台修改成功如果字符串常量在可写段某些平台修改看起来成功但可能影响其他指向同一常量的指针绝对不要修改字符串常量。3.4 用 const 保护字符串常量为了明确表达意图应该用const修饰指向字符串常量的指针cconst char *p Hello; /* 表明不能通过 p 修改指向的内容 */ /* p[0] h; */ /* 编译器会报错而不是运行时崩溃 */4 字符串的初始化4.1 字符数组的初始化c/* 方式1直接用字符串初始化 */ char s1[6] Hello; /* 显式指定大小 */ char s2[] Hello; /* 编译器自动计算大小6 */ /* 方式2字符数组初始化 */ char s3[] {H, e, l, l, o, \0}; /* 手动添加\0 */ /* 注意没有\0就不是字符串 */ char s4[] {H, e, l, l, o}; /* 长度5没有\0不是字符串 */4.2 字符指针的初始化c/* 指向字符串常量 */ const char *p1 Hello; /* 推荐明确是只读 */ char *p2 Hello; /* 不推荐容易误以为可修改 */ /* 指向栈上的数组 */ char arr[] Hello; char *p3 arr; /* p3 指向可修改的数组 */ /* 动态分配内存 */ char *p4 malloc(10); strcpy(p4, Hello); /* p4 指向堆上的字符串 */4.3 区别总结特性字符数组字符指针存储位置栈或静态区指针在栈指向只读区或堆内容是否可改可修改取决于指向哪里指针本身是否可改否数组名是常量是sizeof数组大小指针大小初始化来源拷贝字符串内容存储字符串地址5 指针数组处理字符串表5.1 字符串表的概念在实际编程中经常需要处理一组字符串如菜单选项、命令列表、星期名称等。指针数组是存储这类字符串表的理想方式。c/* 定义字符串表 */ const char *weekdays[] { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; /* 访问 */ for (int i 0; i 7; i) { printf(%s\n, weekdays[i]); }5.2 指针数组 vs 二维字符数组方式1指针数组cconst char *days1[] { Monday, Tuesday, Wednesday };每个字符串长度可以不同字符串存储在只读区不可修改指针数组本身占用的空间较小每个指针8字节方式2二维字符数组cchar days2[][10] { Monday, Tuesday, Wednesday };每个字符串长度固定10字节浪费空间字符串存储在数组内可以修改占用连续空间访问可能更快内存布局对比text指针数组 days1 [ptr] → Monday [ptr] → Tuesday [ptr] → Wednesday 二维数组 days2 [Monday\0 ][Tuesday\0 ][Wednesday\0]5.3 命令行参数 argvmain函数的第二个参数argv就是一个经典的字符串表cint main(int argc, char *argv[]) { printf(程序名%s\n, argv[0]); for (int i 1; i argc; i) { printf(参数 %d%s\n, i, argv[i]); } return 0; }argv是一个指针数组每个元素指向一个命令行参数字符串。5.4 字符串表的遍历c#include stdio.h int main(void) { const char *colors[] { Red, Green, Blue, Yellow, Cyan, Magenta, NULL }; /* 用 NULL 作为结束标志 */ /* 方法1通过下标遍历 */ for (int i 0; colors[i] ! NULL; i) { printf(%s , colors[i]); } printf(\n); /* 方法2通过指针遍历 */ const char **p colors; while (*p ! NULL) { printf(%s , *p); p; } printf(\n); return 0; }6 字符串处理函数中的指针6.1 标准库函数的指针参数理解指针与字符串的关系有助于理解标准库函数的设计csize_t strlen(const char *s); /* 不修改字符串用 const */ char *strcpy(char *dest, const char *src); /* dest 可修改src 只读 */ char *strcat(char *dest, const char *src); /* 同上 */ int strcmp(const char *s1, const char *s2); /* 两者都只读 */6.2 实现简单的字符串函数c#include stdio.h /* 实现 strlen */ size_t my_strlen(const char *s) { const char *p s; while (*p ! \0) { p; } return p - s; /* 指针相减得到长度 */ } /* 实现 strcpy */ char *my_strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) { ; } return ret; } /* 实现 strcmp */ int my_strcmp(const char *s1, const char *s2) { while (*s1 *s2 *s1 *s2) { s1; s2; } return *s1 - *s2; }6.3 字符串指针作为返回值c/* 返回指向字符串常量的指针安全 */ const char* get_weekday(int n) { static const char *weekdays[] { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; if (n 1 n 7) { return weekdays[n-1]; } return Invalid; } /* 返回指向局部数组的指针危险 */ char* get_string_bad(void) { char str[] Hello; /* 局部数组 */ return str; /* 函数返回后数组被销毁 */ }7 常见错误与注意事项7.1 试图修改字符串常量cchar *p Hello; p[0] h; /* 未定义行为可能导致崩溃 */7.2 混淆字符数组和字符指针的初始化cchar str1[10]; str1 Hello; /* 错误数组名不能被赋值 */ char str2[10]; strcpy(str2, Hello); /* 正确 */ char *p; p Hello; /* 正确p 指向字符串常量 */7.3 忘记为 \0 分配空间cchar str[5] Hello; /* Hello 需要6个字符包含\0 */ /* 实际只存储了 H,e,l,l,o没有 \0不是有效字符串 */7.4 返回局部字符数组的地址cchar* get_name(void) { char name[20] Alice; return name; /* 危险name 是局部变量 */ }7.5 用 比较字符串内容cchar *p1 Hello; char *p2 Hello; if (p1 p2) { /* 比较的是地址不是内容可能为真但不能依赖 */ /* ... */ } /* 应该用 strcmp(p1, p2) 0 */7.6 字符串常量不可修改但指针本身可改cconst char *p Hello; p World; /* 合法修改指针指向 */ /* p[0] h; */ /* 非法不能修改内容 */8 本章小结本章系统介绍了指针与字符串的关系1. 字符数组 vs 字符指针特性字符数组字符指针存储栈/静态区指针在栈指向只读区或堆内容可改是取决于指向自身可改否常量是sizeof数组大小指针大小2. 字符串常量存储在只读数据区相同的字符串常量可能共享绝对不能修改应该用const char*指向3. 字符串表的两种方式指针数组每个字符串长度可不同不可修改节省空间二维数组固定长度可修改占用连续空间4. 实际应用命令行参数argv是指针数组标准库字符串函数大量使用指针实现自己的字符串函数需要理解指针操作5. 常见错误修改字符串常量忘记数组和指针的区别返回局部字符数组用比较字符串内容忘记为\0留空间