19、字符串数组:命令行参数argv的实现、字符串排序、字符串查找与匹配
字符串数组,说白了就是「装着字符串的数组」。但C语言里没有真正的字符串类型,所以它实际上是个二维字符数组,或者更常见的是——一个指针数组,每个指针指向一个以'\0'结尾的字符序列。
我刚开始学的时候,总觉得这东西绕。后来在项目中写命令行解析器,才真正把它吃透了。今天咱们就把它掰开揉碎,从argv的实现讲起,再到排序、查找和匹配。
19.1 命令行参数argv的实现
你写过main函数吧?标准写法是这样的:
int main(int argc, char *argv[])
argc是参数个数,argv是字符串数组。每个元素是一个字符串指针。argv[0]通常是程序名,argv[1]是第一个参数,以此类推。最后一个元素argv[argc]一定是NULL。
我曾在项目中遇到一个bug:有人直接修改argv[0]的内容,结果程序名被改了,调试信息全乱了。嗯,这里要注意——argv指向的内存是操作系统分配的,你可以读,但最好不要写。
来看一个简单的argv遍历:
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for (i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
编译运行:
$ ./a.out hello world 123
argv[0] = ./a.out
argv[1] = hello
argv[2] = world
argv[3] = 123
你想想看,argv本质上就是一个指针数组。它的内存布局是这样的:
argv的内存结构:
- argv本身是一个指针,指向一个指针数组
- 指针数组的每个元素指向一个字符串
- 字符串存储在连续的内存区域中,以'\0'结尾
- argv[argc] == NULL,作为结束标记
19.2 字符串排序
排序字符串数组,说白了就是排序指针。你不需要移动字符串本身,只需要交换指针就行了。这样效率高得多。
我习惯用qsort来做,标准库函数,省心。但要注意比较函数的写法:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int cmp(const void *a, const void *b)
{
// a和b是指向指针的指针
return strcmp(*(char **)a, *(char **)b);
}
int main(void)
{
char *arr[] = {"banana", "apple", "cherry", "date"};
int n = sizeof(arr) / sizeof(arr[0]);
int i;
qsort(arr, n, sizeof(char *), cmp);
for (i = 0; i < n; i++) {
printf("%s\n", arr[i]);
}
return 0;
}
输出:
apple
banana
cherry
date
小技巧:比较函数里,a和b是数组元素的地址。因为数组元素是char *,所以地址是char **。先解引用拿到字符串指针,再传给strcmp。这个套路我用了十年,从来没出过问题。
如果你想按字符串长度排序,换个比较函数就行:
int cmp_len(const void *a, const void *b)
{
size_t la = strlen(*(char **)a);
size_t lb = strlen(*(char **)b);
if (la < lb) return -1;
if (la > lb) return 1;
return 0;
}
19.3 字符串查找
查找分两种:在字符串数组里找某个字符串,或者在一个字符串里找子串。
19.3.1 在字符串数组中查找
线性查找,最直接:
int find_string(char *arr[], int n, const char *target)
{
int i;
for (i = 0; i < n; i++) {
if (strcmp(arr[i], target) == 0) {
return i; // 返回索引
}
}
return -1; // 没找到
}
如果数组已经排好序,可以用二分查找,效率更高:
int binary_search(char *arr[], int n, const char *target)
{
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
int cmp = strcmp(arr[mid], target);
if (cmp == 0) return mid;
else if (cmp < 0) left = mid + 1;
else right = mid - 1;
}
return -1;
}
注意:二分查找的前提是数组已经排序。我曾经在项目里忘了排序直接调二分查找,结果查了半天查不到,debug到怀疑人生。后来养成了习惯——用二分之前先确认数组有序。
19.3.2 在字符串中查找子串
标准库提供了strstr函数:
#include <string.h>
char *strstr(const char *haystack, const char *needle);
返回needle在haystack中第一次出现的位置指针,没找到返回NULL。
char *text = "hello world";
char *p = strstr(text, "world");
if (p) {
printf("找到了:%s\n", p); // 输出 "world"
}
19.4 字符串匹配
简单的匹配用strstr就够了。但如果你需要更灵活的匹配,比如通配符或者正则,那就得自己写或者用库了。
我曾在嵌入式项目里实现过一个简单的通配符匹配,支持'*'和'?':
int wildcard_match(const char *str, const char *pattern)
{
while (*pattern) {
if (*pattern == '*') {
// '*'匹配任意字符序列
pattern++;
if (*pattern == '\0') return 1; // 末尾的'*'匹配一切
while (*str) {
if (wildcard_match(str, pattern)) return 1;
str++;
}
return 0;
} else if (*pattern == '?' || *pattern == *str) {
// '?'匹配任意单个字符,或者字符相等
pattern++;
str++;
} else {
return 0; // 不匹配
}
}
return *str == '\0'; // 模式结束,字符串也必须结束
}
用法:
if (wildcard_match("hello.c", "*.c")) {
printf("匹配C源文件\n");
}
核心要点回顾:
- argv是指针数组,argv[argc]==NULL
- 排序字符串数组就是排序指针,用qsort+自定义比较函数
- 查找用strcmp线性扫描,或strstr找子串
- 通配符匹配可以用递归实现,注意'*'的贪婪匹配
说实话,字符串处理在C语言里是最容易出bug的地方之一。我见过太多人因为忘记给字符串结尾加'\0'导致缓冲区溢出。嗯,写代码的时候多留个心眼,该检查边界就检查,该用安全函数就用安全函数。
好了,这一章的内容就到这儿。字符串数组这东西,你多写几个命令行工具自然就熟了。
公众号:蓝海资料掘金营,微信deep3321