字符串数组是 C 语言里最容易让人绕晕的知识点之一。很多初学者学到数组时觉得没问题学到指针时也勉强能跟上但一碰到“字符串数组”这个概念就开始分不清char name[3][20]和char *name[3]到底有什么区别更不用说在写用户名单、命令解析、文件读取这类程序时该怎么选。这次我们就把字符串数组彻底讲透先看它到底是什么、有哪几种实现方式再通过可运行的代码示例把它跑通。字符串数组在 C 语言中没有一个独立的“字符串”类型它本质上仍然是字符数组或者是指向字符的指针数组。核心区别在于你要存的是“一组固定长度的字符串”还是“一组长度不确定的字符串”是“可以原地修改内容”还是“只需要读取和排序”。这两种需求对应两种完全不同的写法内存布局、修改方式、传参方式都不一样。本文会围绕二维字符数组和指针数组两条主线展开配合完整的编译运行示例、常见错误分析和排查方法帮你一次性搞定字符串数组的定义、初始化、遍历、修改、传参和排序。如果你是正在学 C 语言的初学者或者准备应对期末考试、计算机等级考试、PTA 习题和洛谷题目这篇文章可以直接收藏。下面先给出一张核心能力速览表把字符串数组的两种实现方式、适用场景和关键区别一次说清。1. 字符串数组核心概念速览实现方式定义形式存储位置字符串内容可否修改适合场景典型示例二维字符数组char arr[行数][列数]栈区或全局区空间连续可以修改字符串数量固定、每条最长长度可知学生姓名表、固定选项菜单指针数组char *arr[行数]数组本身在栈区字符串常量或堆区取决于指针指向哪里字符串数量固定、长度差异大命令词表、星期名称、错误信息表指向指针的指针char **arr多用于函数传参和动态分配可以修改数量不固定的字符串集合命令行参数、动态读取文件内容三种方式里前两种是字符串数组最经典的写法第三种本质上是第二种的传参和动态扩展形式。理解前两种后面遇到char **就不会慌。从内存角度看二维字符数组是“一块完整的内存区域”每行长度固定编译器直接分配一整块空间。指针数组则是“一组指针变量”每个指针指向一个字符串的首地址字符串本身可能不在数组的连续空间里。这个差异决定了所有后续操作scanf、strcpy、strcmp、排序交换、传参都会因为选择的不同而写法不同。还有一个常见误区是混淆“字符数组”和“字符串数组”。char str[20]是一个能存放一条字符串的字符数组char strs[10][20]是能存放 10 条字符串的字符串数组。区别就是多了一维。这一维让程序可以同时管理多条字符串这也是字符串数组存在的意义。2. 适用场景与实际使用边界字符串数组在 C 语言项目里非常常用但搞清楚“什么时候该用哪种”比背语法更重要。适合用二维字符数组的场景存储固定数量的用户名、学号、商品名每条长度有上限。需要频繁修改某一行的字符内容比如循环读入数据后改写某个字段。做冒泡排序或选择排序时希望直接交换整行内容虽然可以用strcpy实现但注意行长度固定不会越界。函数内部临时定义一个小型字符串表不涉及复杂的内存生命周期管理。适合用指针数组的场景存储一组字符串常量如星期几的英文名、月份名、错误提示信息。字符串长度差异极大例如有的字符串只占几个字节有的几百字节用二维数组会浪费空间。只需要遍历、比较、排序输出不需要修改字符串内容。从外部读入未知长度的字符串配合malloc动态分配后让指针数组的元素指向堆区内存。不适合或需要小心的场景用指针数组存放字符串常量后试图通过arr[i][j] A修改内容这会导致未定义行为程序可能崩溃。二维字符数组定义过大例如char arr[10000][1024]会占用约 10MB 栈空间在部分嵌入式环境或 Windows 默认栈大小下可能直接栈溢出。这种场景应该使用malloc动态分配。函数返回局部二维字符数组名返回后数组已销毁指针悬空。此时应使用动态分配或指针数组。合规方面字符串数组本身不涉及版权和隐私问题但在处理用户输入、读取外部文件字符串时要注意缓冲区溢出风险。所有scanf、gets操作都要限制长度strcpy要能确保目标空间足够建议使用strncpy或snprintf替代。3. 环境准备与前置条件C 语言字符串数组的验证不需要特殊硬件普通 CPU 即可也没有显存、GPU 的要求。需要准备的是工具说明C 语言编译器GCC、Clang、MSVC 均可代码编辑器VS Code、Visual Studio、CLion、Dev-C 均可操作系统Windows、Linux、macOS 均可命令行终端Windows 用 CMD 或 PowerShellLinux/macOS 用 TerminalWindows 下可以使用 MinGW-w64 或 Visual Studio 的开发者命令提示符。Linux 下一般自带 GCC直接执行gcc --version检查。macOS 执行clang --version。编译运行的基本命令格式# GCC 编译并运行 gcc -stdc11 -Wall -Wextra -o string_array_demo string_array_demo.c ./string_array_demo# Windows 下如果使用 MinGW gcc -stdc11 -Wall -Wextra -o string_array_demo.exe string_array_demo.c .\string_array_demo.exe-Wall -Wextra建议一直加上字符串数组的很多问题编译器都会给出警告早发现早处理。4. 二维字符数组定义、初始化与遍历二维字符数组的定义形式是char 数组名[行数][列数]。其中行数代表能存多少条字符串列数代表每条字符串的最大字符数注意要包含结尾的\0。4.1 定义和初始化#include stdio.h int main(void) { // 方式一完全初始化编译器自动计算行数 char students[][20] { zhangsan, lisi, wangwu }; // 方式二指定行数和列数 char weeks[7][10] { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; // 方式三先定义后逐条赋值 char fruits[3][20]; snprintf(fruits[0], sizeof(fruits[0]), apple); snprintf(fruits[1], sizeof(fruits[1]), banana); snprintf(fruits[2], sizeof(fruits[2]), cherry); int row sizeof(students) / sizeof(students[0]); printf(students 数组行数: %d\n, row); printf(students 数组总大小: %zu 字节\n, sizeof(students)); printf(每一行大小: %zu 字节\n, sizeof(students[0])); for (int i 0; i row; i) { printf(%s\n, students[i]); } return 0; }这里要注意sizeof(students) / sizeof(students[0])是 C 语言中获取数组行数的通用技巧。sizeof(students)是整个二维数组占用的字节数sizeof(students[0])是每一行占用的字节数两者相除就是行数。这种方式在函数外部与数组定义在同一个作用域内有效一旦传入函数数组会退化为指针sizeof就得不到行数了这点后面会单独讲。snprintf(fruits[0], sizeof(fruits[0]), apple)是安全写法限制了拷贝长度避免strcpy造成的缓冲区溢出。4.2 遍历与输出遍历二维字符数组本质上是逐行取出字符串对字符串本身的操作和单条字符串一致。#include stdio.h #include string.h int main(void) { char names[][20] {alice, bob, carol, dave}; int count sizeof(names) / sizeof(names[0]); // 基本遍历输出 for (int i 0; i count; i) { printf(第 %d 个名字: %s\n, i, names[i]); } // 计算每条字符串的长度 for (int i 0; i count; i) { printf(%s 的长度是 %zu\n, names[i], strlen(names[i])); } // 修改某一行的内容 strcpy(names[1], robert); printf(修改后: %s\n, names[1]); return 0; }运行结果第 0 个名字: alice 第 1 个名字: bob 第 2 个名字: carol 第 3 个名字: dave alice 的长度是 5 bob 的长度是 3 carol 的长度是 5 dave 的长度是 4 修改后: robert因为二维字符数组每一行是连续内存strcpy(names[1], robert)可以直接覆盖内容前提是目标字符串长度不超过20 - 1 19个字符。使用strncpy更稳妥strncpy(names[1], robert, sizeof(names[1]) - 1); names[1][sizeof(names[1]) - 1] \0;strncpy不会自动补结尾\0如果源字符串长度等于或超过指定长度目标字符串可能没有终止符所以需要手动置\0。这是初学者最容易踩的坑。4.3 使用 scanf 读入字符串数组输入场景更接近实际编程#include stdio.h int main(void) { char students[3][20]; printf(请输入 3 个名字用空格或回车分隔:\n); for (int i 0; i 3; i) { scanf(%19s, students[i]); } printf(\n你输入的名字是:\n); for (int i 0; i 3; i) { printf(%s\n, students[i]); } return 0; }scanf(%19s, students[i])中的19是宽度限制防止输入超过 19 个字符导致缓冲区溢出。students[i]本身是char[20]类型传给scanf时自动退化为指向首元素的指针所以不需要再加。很多初学者会写成scanf(%s, students[i])这虽然可能碰巧工作但类型是错误的极不推荐。可以看到二维字符数组的读入、修改、遍历都围绕“行”这个概念展开students[i]可以当作一条普通的字符串使用。5. 指针数组定义、初始化和使用指针数组的定义形式是char *数组名[行数]。这个数组里的每个元素都是一个char *指针指向一条字符串。5.1 定义和初始化#include stdio.h int main(void) { // 指向字符串常量 char *weekdays[] { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; int count sizeof(weekdays) / sizeof(weekdays[0]); printf(weekdays 数组元素个数: %d\n, count); printf(weekdays 数组总大小: %zu 字节\n, sizeof(weekdays)); printf(每个指针元素大小: %zu 字节\n, sizeof(weekdays[0])); for (int i 0; i count; i) { printf(%s\n, weekdays[i]); } return 0; }在 64 位系统上sizeof(weekdays[0])是 8 字节因为一个指针占 8 字节。这就是指针数组和二维字符数组最大的差异二维字符数组的sizeof(students[0])是列数比如 20指针数组的sizeof(weekdays[0])是指针大小8 或 4。两者虽然都可以用数组名[下标]访问字符串但底层完全不同。5.2 指针指向字符串常量的修改限制这是最容易翻车的地方之一#include stdio.h int main(void) { char *arr[] {hello, world}; // 试图修改字符串常量 // arr[0][0] H; // 未定义行为可能会崩溃或不起作用 // 输出字符串常量没问题 printf(%s %s\n, arr[0], arr[1]); // 让指针指向另一条字符串常量这是允许的 arr[0] HELLO; printf(重新指向后: %s\n, arr[0]); return 0; }把注释解除程序可能直接崩溃也可能静默失败甚至在某些编译器上看起来正常。这属于典型的未定义行为任何结果都可能出现。原因在于hello是字符串字面量存储在只读区通过指针修改它是非法的。而arr[0] HELLO只是修改指针变量本身的值让指针指向另一块只读区这是合法的。理解“修改指针”和“修改指针指向的内容”的区别是掌握指针数组的关键。5.3 指针数组指向可修改内存如果需要在指针数组中修改字符串内容可以让指针指向栈区数组或堆区内存#include stdio.h #include string.h #include stdlib.h int main(void) { char temp[][20] {red, green, blue}; char *colors[3]; for (int i 0; i 3; i) { colors[i] temp[i]; // 指向栈区数组 } printf(访问栈区字符串: %s\n, colors[0]); // 修改栈区内容 strcpy(colors[0], yellow); printf(修改成功: %s\n, colors[0]); // 动态分配堆区内存 char *dynamic[2]; dynamic[0] (char *)malloc(20 * sizeof(char)); dynamic[1] (char *)malloc(20 * sizeof(char)); if (dynamic[0] NULL || dynamic[1] NULL) { fprintf(stderr, 内存分配失败\n); return 1; } strcpy(dynamic[0], heap string 1); strcpy(dynamic[1], heap string 2); printf(%s\n, dynamic[0]); printf(%s\n, dynamic[1]); free(dynamic[0]); free(dynamic[1]); return 0; }这里展示了两种让指针数组指向可修改内存的方式。第一种是让指针指向已有的栈区二维数组本质上是把temp[i]的首地址赋给colors[i]。第二种是动态分配堆区内存适合字符串长度在运行时才能确定的场景。动态分配时必须检查malloc的返回值返回NULL表示分配失败直接使用会空指针崩溃。使用完必须free并且只能free动态分配的内存不能free指向栈区数组的指针。5.4 用指针数组管理命令词表指针数组最常见的使用场景之一是管理固定集合的字符串表#include stdio.h #include string.h int main(void) { char *commands[] { help, version, quit, list, unknown }; char input[20]; printf(输入一个命令: ); scanf(%19s, input); int found 0; int cmd_count sizeof(commands) / sizeof(commands[0]); for (int i 0; i cmd_count; i) { if (strcmp(input, commands[i]) 0) { printf(命令匹配: %s\n, commands[i]); found 1; break; } } if (!found) { printf(未知命令\n); } return 0; }这种命令解析写法在嵌入式固件、简单的 CLI 工具里很常见。commands这个指针数组只保存字符串常量的地址不分配额外空间程序体积小、访问快。6. 字符串数组的排序、查找与函数传参6.1 字符串数组冒泡排序排序是字符串数组最常见的操作。如果使用二维字符数组交换两行要借助strcpy逐字符拷贝如果使用指针数组交换两个指针即可效率更高。先看二维字符数组版本的冒泡排序#include stdio.h #include string.h void sort_strings_2d(char arr[][20], int n) { char temp[20]; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (strcmp(arr[j], arr[j 1]) 0) { strcpy(temp, arr[j]); strcpy(arr[j], arr[j 1]); strcpy(arr[j 1], temp); } } } } int main(void) { char fruits[][20] {pear, apple, orange, banana, grape}; int n sizeof(fruits) / sizeof(fruits[0]); sort_strings_2d(fruits, n); printf(排序结果:\n); for (int i 0; i n; i) { printf(%s\n, fruits[i]); } return 0; }注意函数参数里的char arr[][20]列数必须写出来因为编译器需要知道每行的步长才能计算arr[j]的地址。如果改成char arr[][]或char arr[][ ]都会编译错误。再看指针数组版本#include stdio.h #include string.h void sort_strings_ptr(char *arr[], int n) { char *temp; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (strcmp(arr[j], arr[j 1]) 0) { temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } } int main(void) { char *fruits[] {pear, apple, orange, banana, grape}; int n sizeof(fruits) / sizeof(fruits[0]); sort_strings_ptr(fruits, n); printf(排序结果:\n); for (int i 0; i n; i) { printf(%s\n, fruits[i]); } return 0; }这个版本里排序交换的只是指针变量的值指向的字符串常量本身没有发生任何移动。如果字符串长度差异很大指针数组不知道高效到哪里去了。但注意如果原来的指针数组指向的是不可修改的字符串常量排序只是改变数组元素的指向顺序这完全合法。如果指向的是动态分配的内存交换指针时要小心原指针的所有权避免内存泄漏和重复释放。6.2 函数传参的几种写法字符串数组作为函数参数有以下常见写法#include stdio.h // 方式一二维字符数组参数列数必须写 void print_2d(char arr[][20], int rows) { for (int i 0; i rows; i) { printf(%s\n, arr[i]); } } // 方式二指针数组参数 void print_ptr(char *arr[], int rows) { for (int i 0; i rows; i) { printf(%s\n, arr[i]); } } // 方式三指向指针的指针本质同上 void print_ptr2(char **arr, int rows) { for (int i 0; i rows; i) { printf(%s\n, arr[i]); } } int main(void) { char names[][20] {a, bb, ccc}; char *week[] {mon, tue, wed}; print_2d(names, 3); print_ptr(week, 3); print_ptr2(week, 3); return 0; }方式二和方式三在函数形参上是等价的char *arr[]会退化为char **arr。很多刚学 C 语言的同学看到char **就害怕其实它就是“指向指针的指针”在字符串数组场景里就是“字符串数组参数”的另一种写法。6.3 字符串数组传参后求行数的问题在函数内部不能用sizeof(arr) / sizeof(arr[0])求行数因为数组参数已经退化为指针。#include stdio.h void bad_size(char arr[][20]) { // 错误示例arr 是指针类型不是数组 // int n sizeof(arr) / sizeof(arr[0]); // 得到的是 (8 / 20) 064 位系统上 printf(在函数内部无法通过 sizeof 求行数\n); } int main(void) { char data[][20] {x, y}; bad_size(data); return 0; }解决方案是像前面的示例一样在调用函数时把行数作为参数传入或者使用宏定义数组大小。这是 C 语言数组传参的基本规则字符串数组也不例外。7. 字符串数组接口设计思路这里说的“接口”不是网络 API而是函数接口。字符串数组在真实项目中经常要暴露给多个模块使用。7.1 结构体封装字符串表当字符串数组和它的数量需要一起传递时用结构体封装是更好的设计#include stdio.h #include stdlib.h typedef struct { char **items; int count; } StringList; void print_string_list(const StringList *list) { if (list NULL || list-items NULL) { return; } for (int i 0; i list-count; i) { printf(%s\n, list-items[i]); } } int main(void) { char *data[] {one, two, three}; StringList list; list.items data; list.count 3; print_string_list(list); return 0; }这种设计避免了“数组和长度分离”导致的问题。在更大的项目里StringList 可以继续扩展比如增加 capacity 字段支持动态追加字符串增加 sort 函数指针等。这个思路和 C 语言标准库中一些字符串数组处理场景很接近也是面试中常考的“手写动态字符串数组”的基础。7.2 批量处理字符串数组的思路批量处理场景例如从文件读取多行文本存入字符串数组再逐行处理#include stdio.h #include stdlib.h #include string.h #define MAX_LINES 100 #define MAX_LINE_LEN 256 int main(void) { FILE *fp fopen(input.txt, r); if (fp NULL) { perror(无法打开文件); return 1; } char *lines[MAX_LINES]; char buffer[MAX_LINE_LEN]; int count 0; while (count MAX_LINES fgets(buffer, sizeof(buffer), fp) ! NULL) { // 去掉换行符 buffer[strcspn(buffer, \n)] \0; lines[count] (char *)malloc((strlen(buffer) 1) * sizeof(char)); if (lines[count] NULL) { fprintf(stderr, 内存分配失败\n); // 释放已分配的内存 for (int i 0; i count; i) { free(lines[i]); } fclose(fp); return 1; } strcpy(lines[count], buffer); count; } fclose(fp); printf(读取了 %d 行:\n, count); for (int i 0; i count; i) { printf(%s\n, lines[i]); } for (int i 0; i count; i) { free(lines[i]); } return 0; }这是指针数组配合动态内存分配的典型批量处理流程按行读取、动态分配、保存指针、最后统一释放。strcspn(buffer, \n)找到换行符的位置直接替换成\0是去掉fgets自带换行符的标准写法。动态分配的内存必须一一对应释放释放后再把指针置为NULL是一个好习惯但注意这里的局部数组在函数结束时就会销毁置空是防御性写法主要意义在于防止重复释放。实际批量处理时还要考虑如果超过MAX_LINES行怎么办遍历后是否允许重复读取是否需要排序去重。可以扩展成动态扩容的二维结构但那就是“动态字符串数组”的进阶话题了。8. 常见错误与排查方法字符串数组的报错和运行问题多数集中在类型不匹配、缓冲区溢出、内存错误和sizeof误用上。下面整理成一张排查表。问题现象可能原因排查方式解决方案编译错误array type has incomplete element type二维数组函数参数缺少列数检查函数声明里的形参写完整char arr[][20]编译错误assignment to expression with array type试图直接给数组名赋值检查是否写了arr something用strcpy或snprintf逐字符拷贝运行时崩溃或卡死越界写入、栈溢出使用-fsanitizeaddress编译检查限制输入长度检查数组维度输出乱码或带换行符fgets保留了\n打印字符串长度用strcspn去掉换行符sizeof(arr)结果不对数组已退化为指针检查是否在函数内部使用sizeof把行数作为参数传入排序结果没有变化字符串内容未被真正交换检查排序时使用strcpy还是指针交换二维数组用strcpy指针数组交换指针程序提示段错误指针数组指向只读区后被修改检查是否写了arr[i][j] ...改为重新指向或指向可修改内存malloc后内存泄漏动态分配未被free使用 Valgrind 检查内存泄漏保证每个malloc都有对应的free8.1 使用 AddressSanitizer 定位越界问题Linux 或 macOS 下可以用 GCC/Clang 的 AddressSanitizer 快速定位与字符串数组相关的越界和栈问题gcc -stdc11 -Wall -Wextra -fsanitizeaddress -g -o demo demo.c ./demo如果程序有数组越界或非法访问编译器插入的检测代码会在崩溃前打印出具体位置和调用栈排查效率远超肉眼看代码。Windows 下 Visual Studio 也有类似功能但配置略繁琐建议在 Linux 环境或 WSL 里做这类检查。8.2 使用 Valgrind 检查内存错误对于指针数组和动态内存分配场景Valgrind 是另一个利器gcc -stdc11 -Wall -Wextra -g -o demo demo.c valgrind --leak-checkfull ./demoValgrind 会报告未初始化内存、非法读写、内存泄漏等。在涉及字符串数组的动态内存分配、批量读取文件、排序交换指针时用 Valgrind 验证一遍基本可以放心。8.3 指针数组和二维数组的互换使用错误一个常见错误是以为char *arr[]和char arr[][20]可以随意互换。实际上它们作为函数参数时的内存布局完全不同// 错误把二维数组传给 char ** 参数 // char data[3][20]; // print_ptr2(data, 3); // 编译警告或错误类型不兼容 // 正确 // 指针数组传给 char ** 参数 // char *data[] {a, b, c}; // print_ptr2(data, 3); // OK从严格类型角度char **的参数期望的是一个指针数组而char [3][20]的类型是“含有 3 个元素、每个元素是含有 20 个 char 的数组”的数组二者不能隐式转换。想传给二维数组必须写成char arr[][20]。如果确实需要用统一的接口处理两种情况可以借助void *强制转换但这样做的类型安全性较差不建议初学者使用。更稳妥的做法是明确两种数组类型写两套函数或者统一使用指针数组加动态分配。9. 字符串数组最佳实践与代码规范写 C 语言字符串数组时以下实践能显著减少 Bug。第一所有读入操作必须限定长度。scanf(%19s, buf)而不是scanf(%s, buf)fgets(buf, sizeof(buf), fp)而不是gets(buf)。gets在 C11 标准中已经移除绝对不要使用。这是防止缓冲区溢出最主要的措施。第二定义二维字符数组时列数留足余量。比如学生姓名最长可能 10 个字符别定义char name[10]要定义成char name[32]或char name[64]给\0和可能的扩展留空间。定义完可以用#define或enum管理维度避免魔法数字。#define MAX_NAME_LEN 32 #define MAX_STUDENTS 50 char students[MAX_STUDENTS][MAX_NAME_LEN];第三区分“修改指针”和“修改指针指向的内容”。用指针数组管理字符串常量时可以重新给指针赋值但不能通过指针修改字符串常量。如果确实需要修改内容就使用二维数组或者动态分配内存。第四动态分配必须配对释放。每个malloc都要有对应的free释放后建议把指针置为NULL。释放顺序与分配顺序相反避免悬垂指针。第五函数参数边界清晰。如果传入字符串数组的函数需要知道行数和列数就全部通过参数传入别依赖sizeof。如下所示void process_strings(char arr[][MAX_LEN], int rows) { for (int i 0; i rows; i) { // 处理 arr[i] } }第六优先使用snprintf、strncpy、strncat等限定长度的函数而不是sprintf、strcpy、strcat。虽然strncpy有补零和截断的细节要注意但总体比无界拷贝安全得多。第七涉及指针数组排序时交换的是指针而不是整个字符串。如果确实需要交换二维数组行内容使用strcpy到临时数组但要注意临时数组大小与列数一致。第八遇到程序崩溃不要慌优先怀疑字符串越界。编译时开-Wall -Wextra -fsanitizeaddress运行时用 Valgrind基本能定位 90% 的字符串数组问题。10. 练习题与验证示例为了验证是否真的理解了字符串数组下面几道题可以直接上手练习。题目一定义一个包含 5 个城市名的字符串数组把用户输入的城市名替换到指定位置再遍历输出。#include stdio.h #include string.h int main(void) { char cities[5][20] {Beijing, Shanghai, Guangzhou, Shenzhen, Chengdu}; int index; char newName[20]; printf(要替换的下标(0-4): ); scanf(%d, index); if (index 0 || index 5) { printf(下标越界\n); return 1; } printf(输入新城市名: ); scanf(%19s, newName); strncpy(cities[index], newName, sizeof(cities[index]) - 1); cities[index][sizeof(cities[index]) - 1] \0; for (int i 0; i 5; i) { printf(%s\n, cities[i]); } return 0; }题目二使用指针数组保存{C, Python, Java, Go, Rust}用选择排序按字典序输出。#include stdio.h #include string.h void selection_sort(char *arr[], int n) { for (int i 0; i n - 1; i) { int min_idx i; for (int j i 1; j n; j) { if (strcmp(arr[j], arr[min_idx]) 0) { min_idx j; } } if (min_idx ! i) { char *temp arr[i]; arr[i] arr[min_idx]; arr[min_idx] temp; } } } int main(void) { char *langs[] {C, Python, Java, Go, Rust}; int n sizeof(langs) / sizeof(langs[0]); selection_sort(langs, n); for (int i 0; i n; i) { printf(%s\n, langs[i]); } return 0; }题目三定义函数count_string(char *arr[], int n, const char *target)统计指针数组中等于target的字符串个数。这个题考察指针数组遍历和strcmp的用法能写出来说明基本掌握。题目四从键盘读入若干行字符串存入指针数组当输入end时停止然后逆序输出所有行。这个题结合动态内存、strcmp、逆序遍历是综合性较强的练习。题目五写一个函数把二维字符数组的所有字符串首字母转为大写并返回转换的字符串个数。这个题考察二维数组逐行修改和字符判断。上面的题目都能在本地完成编译和运行验证。如果你在 PTA、洛谷或学校 OJ 里遇到字符串数组相关题目比如字符串逆序、统计单词数量、按字典序排序输出思路都可以从本文的例子迁移过去。11. 总结与下一步字符串数组这块最值得记住的判断标准就是三条字符串数量是否固定、字符串长度差异大不大、是否要修改字符串内容。数量固定且长度有上限优先用二维字符数组数量固定但长度差异大或只是读取优先用指针数组数量不固定需要动态扩展就用char **配合malloc。最先应该动手验证的是二维字符数组的读入和遍历流程因为这个流程覆盖了大部分日常场景。你可以在本地新建一个.c文件把本文第 4 小节的示例粘贴进去编译运行然后改成从键盘读入 5 个字符串再输出确认scanf(%19s, arr[i])的写法没问题。最容易踩的坑是试图修改指针数组指向的字符串常量。写代码前先问自己这个指针指向的内存可写吗如果指向的是字符串字面量只能读取和重新指向不能逐字符修改。第二个坑是函数内部用sizeof求数组长度这个在数组参数退化为指针的情况下绝对会出错。下一步拓展方向有以下几条研究动态字符串数组即char **配合realloc实现自动扩容这是实现 C 语言动态列表的基础。结合qsort对字符串数组排序理解函数指针作为比较回调的用法。结合结构体构建StringList之类的字符串集合工具封装增删改查。结合文件操作实现从文本文件读入多行内容到字符串数组并统计分析。结合 C 语言库函数如strtok拆分字符串后存入数组处理命令解析和配置解析场景。字符串数组是 C 语言从“会语法”到“能写程序”之间必须跨过的一道坎。二维字符数组和指针数组这两种形态搞清楚了后面看任何 C 语言项目里的字符串处理逻辑都会顺畅很多。建议把本文中的示例代码挨个编译运行一遍再去做几道在线评测题这个知识点就能彻底稳住了。