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,作为结束标记
argv 内存布局示意图 argv argv[0] argv[1] argv[2] argv[3] NULL "./a.out\0" "hello\0" "world\0" "123\0" 字符串1 字符串2 字符串3 字符串4 argv指针 指针数组 字符串数据

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