六:字符串——字符数组与字符串处理

一、回顾与本篇目标

上一篇你学会了指针——C 语言最核心的概念。你知道变量有内存地址,指针存地址,* 解引用可以访问指针指向的数据,数组名就是指向首元素的指针。

这一篇我们要学的是字符串。在 Python 和 JavaScript 中,字符串是一种内置的高级数据类型——你可以直接拼接、切片、替换,几乎感觉不到它的底层实现。但在 C 语言中,没有专门的字符串类型。字符串就是一个\0 结尾的字符数组。操作字符串就是操作数组和指针。

这种“简陋”的设计恰恰是 C 语言哲学的体现——给你最基础的工具,让你自己构建一切。理解了 C 语言的字符串,你会对字符串的底层存储有全新的认识。

本篇的目标:

  1. 理解 C 语言中字符串的本质——以 \0 结尾的 char 数组
  2. 学会字符串的声明和初始化
  3. 掌握 printfscanf 处理字符串
  4. 学会常用字符串处理函数:strlenstrcpystrcmpstrcat
  5. 理解字符串和指针的关系

二、C 语言中字符串的本质

在 Python 或 JavaScript 中,字符串是一个高级对象:

// JavaScript
let str = "Hello";
console.log(str.length);     // 5
console.log(str.toUpperCase()); // "HELLO"

# Python
s = "Hello"
print(len(s))      # 5
print(s.upper())   # "HELLO"

在 C 语言中,没有专门的字符串类型。字符串就是一个 char 类型的数组,最后一个字符后面跟着一个特殊字符 \0(空字符,ASCII 码为 0),用来标记字符串的结束。

// C 语言中 "Hello" 在内存中的存储:
// 索引:  0    1    2    3    4    5
// 字符: 'H'  'e'  'l'  'l'  'o'  '\0'
// 地址:1000 1001 1002 1003 1004 1005

\0 是字符串的“终止符”。C 语言中所有处理字符串的函数,都是通过这个 \0 来判断字符串在哪结束的。如果没有 \0,函数会一直向后读内存,直到碰巧遇到一个 0 为止——这会导致读取越界。

三、字符串的声明和初始化

C 语言中有两种方式创建字符串:

方式一:字符数组

// 逐个字符初始化(需要手动加 \0)
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

// 用字符串字面量初始化(编译器自动加 \0)
char str2[6] = "Hello";    // 数组大小至少为 6(5 个字符 + 1 个 \0)

// 不指定大小,编译器自动推导
char str3[] = "Hello";     // 自动分配 6 个字节

// 部分初始化
char str4[10] = "Hello";   // 前 6 个字节是 Hello\0,后面 4 个字节自动填 0

方式二:字符指针

// 指针指向一个字符串常量(存储在只读内存区)
char *str5 = "Hello";

// 注意:这种方式声明的字符串通常不能修改!
// str5[0] = 'h';  // 可能导致程序崩溃(取决于编译器和操作系统)

字符数组和字符指针的区别:

char str[] = "Hello" char *str = "Hello"
存储位置 栈上(可读可写) 指向只读数据段(通常不可写)
是否可以修改内容 ✅ 可以 ❌ 通常不行(未定义行为)
sizeof 的结果 整个数组的大小(包括 \0) 指针本身的大小(8 或 4 字节)
是否可以指向其他字符串 ❌ 数组名是常量指针 ✅ 可以

如果你想创建一个可以修改的字符串,用字符数组。如果只是一个不需要修改的常量,用字符指针更省内存。

四、字符串的输入输出

用 printf 输出字符串

char name[] = "张三";

printf("你好,%s!\n", name);  // 你好,张三!

%s 是字符串的格式占位符。printf 从传入的地址开始,逐个打印字符,直到遇到 \0 停止。

用 scanf 读取字符串

char name[50];  // 预留足够的空间

printf("请输入你的名字:");
scanf("%s", name);  // 注意:name 不需要加 &,因为它本身就是地址!

printf("你好,%s!\n", name);

注意scanf("%s", name)name 前面没有 &。因为 name 是数组名,本身就是指向首元素的指针,不需要再取地址。这和我们之前学的 scanf("%d", &age) 不同——age 是普通变量,需要 & 才能得到地址。

scanf 读取字符串的三个问题:

  1. 遇到空格就停止:如果用户输入 张三 丰scanf("%s") 只会读到 张三。空格、Tab、换行都被当作分隔符。
  2. 不检查数组大小:如果用户输入了超过 49 个字符(留 1 个给 \0),就会数组越界。这是 scanf 的安全隐患。
  3. 换行符留在缓冲区:用户输入后按的回车键会留在输入缓冲区中,可能影响后续的输入。

更安全的字符串输入:fgets

fgets 是更安全的读取字符串的函数,它限制了最大读取字符数:

#include <stdio.h>

int main() {
    char name[50];

    printf("请输入你的全名:");
    fgets(name, sizeof(name), stdin);  // 最多读 49 个字符

    // fgets 会保留换行符,如果不需要可以去掉
    // name[strcspn(name, "\n")] = '\0';

    printf("你好,%s!\n", name);
    return 0;
}

fgets 参数解释:

  • name:存储读取内容的数组。
  • sizeof(name):最多读取的字符数(包括 \0,实际读取 sizeof - 1 个字符)。
  • stdin:标准输入(键盘)。

fgets 和 scanf 的区别:

  • fgets 会读取空格,可以读入完整的带空格的字符串。
  • fgets 会保留用户输入的换行符 \n(如果你不想要,需要手动去掉)。
  • fgets 限制了读取长度,不会越界

五、字符串处理函数

C 语言标准库提供了一组字符串处理函数,声明在 <string.h> 头文件中。

strlen:获取字符串长度

#include <string.h>
#include <stdio.h>

int main() {
    char str[] = "Hello World";

    int len = strlen(str);
    printf("字符串长度:%d\n", len);  // 11(不包括 \0)

    printf("sizeof:%d\n", (int)sizeof(str));  // 12(包括 \0)

    return 0;
}

strlen 和 sizeof 的区别:

  • strlen(str):返回字符串的实际长度,不包含 \0
  • sizeof(str):返回数组占用的总字节数,包含 \0

strcpy:复制字符串

#include <string.h>
#include <stdio.h>

int main() {
    char source[] = "Hello";
    char destination[20];  // 确保目标数组足够大

    strcpy(destination, source);  // 把 source 复制到 destination

    printf("destination: %s\n", destination);  // "Hello"
    return 0;
}

注意strcpy 不会检查目标数组的大小。如果 sourcedestination 长,会导致数组越界。更安全的版本是 strncpy

strncpy(destination, source, sizeof(destination) - 1);
destination[sizeof(destination) - 1] = '\0';  // 确保以 \0 结尾

strcmp:比较两个字符串

#include <string.h>
#include <stdio.h>

int main() {
    char str1[] = "apple";
    char str2[] = "banana";
    char str3[] = "apple";

    int result1 = strcmp(str1, str2);
    int result2 = strcmp(str2, str1);
    int result3 = strcmp(str1, str3);

    printf("apple vs banana: %d\n", result1);  // 负数(str1 < str2)
    printf("banana vs apple: %d\n", result2);  // 正数(str2 > str1)
    printf("apple vs apple: %d\n", result3);   // 0(相等)

    return 0;
}

strcmp 的返回值规则:

  • 0:两个字符串相等。
  • 正数:第一个字符串大于第二个(按字典序)。
  • 负数:第一个字符串小于第二个(按字典序)。

不能用 == 比较字符串!

// 错误示范
if (str1 == str2) {   // 这样比较的是地址,不是内容!
    printf("相同\n");
}

// 正确写法
if (strcmp(str1, str2) == 0) {
    printf("相同\n");
}

在 C 语言中,str1 == str2 比较的是两个数组的地址,而不是字符串的内容。这和 JavaScript/Python 不同——JS 和 Python 的 == 比较字符串内容,C 语言比较的是指针。

strcat:拼接字符串

#include <string.h>
#include <stdio.h>

int main() {
    char str1[50] = "Hello";  // 目标数组必须足够大
    char str2[] = " World";

    strcat(str1, str2);  // 把 str2 追加到 str1 末尾

    printf("%s\n", str1);  // "Hello World"
    return 0;
}

注意str1 必须有足够的空间容纳拼接后的完整字符串(包括 \0)。如果空间不足,就会越界。

更安全的版本是 strncat

strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);

六、字符串和指针的关系

字符串本质上就是 char*。这意味着你可以用指针来遍历和处理字符串。

用指针遍历字符串

#include <stdio.h>

int main() {
    char str[] = "Hello";

    // 用指针遍历字符串的每个字符
    for (char *p = str; *p != '\0'; p++) {
        printf("%c ", *p);  // H e l l o
    }
    printf("\n");

    return 0;
}

循环条件是 *p != '\0'——只要指针指向的字符不是终止符,就继续。

字符串数组:存储多个字符串

#include <stdio.h>

int main() {
    // 用二维数组存储多个字符串
    char fruits[][10] = {"apple", "banana", "orange"};

    for (int i = 0; i < 3; i++) {
        printf("%s\n", fruits[i]);  // fruits[i] 是一维数组的数组名,即指针
    }

    // 用指针数组存储多个字符串(更灵活,每个字符串长度可以不同)
    char *colors[] = {"red", "green", "blue", "yellow"};

    for (int i = 0; i < 4; i++) {
        printf("%s\n", colors[i]);
    }

    return 0;
}

二维字符数组 vs 字符指针数组:

  • char fruits[][10]:每个字符串固定占用 10 字节,即使只有 5 个字符也占 10 字节。浪费空间但可以修改内容。
  • char *colors[]:每个元素是一个指针,指向不同长度的字符串常量。节省空间但内容通常不可修改。

七、综合演示:一个简单的用户管理系统

下面这段代码综合运用了字符串的声明、输入输出、处理函数和指针遍历:

#include <stdio.h>
#include <string.h>

int main() {
    // 存储用户名和密码的数组
    char username[50];
    char password[50];

    // 预设的用户名和密码
    char correct_username[] = "admin";
    char correct_password[] = "123456";

    printf("===== 登录系统 =====\n");
    printf("用户名:");
    fgets(username, sizeof(username), stdin);
    // 去掉 fgets 读取的换行符
    username[strcspn(username, "\n")] = '\0';

    printf("密码:");
    fgets(password, sizeof(password), stdin);
    password[strcspn(password, "\n")] = '\0';

    // 验证用户名和密码
    if (strcmp(username, correct_username) == 0 &&
        strcmp(password, correct_password) == 0) {
        printf("\n登录成功!欢迎,%s!\n", username);

        // 用指针遍历用户名,统计字符数(演示指针用法)
        int char_count = 0;
        for (char *p = username; *p != '\0'; p++) {
            char_count++;
        }
        printf("你的用户名有 %d 个字符。\n", char_count);
    } else {
        printf("\n登录失败:用户名或密码错误。\n");
    }

    return 0;
}

代码解析:

  • strcspn(str, "\n"):返回 str 中第一个匹配 "\n" 中任意字符的位置。如果用户输入了换行符,它会返回换行符的索引。我们把这个位置设为 \0,就去掉了换行符。这是处理 fgets 换行符的经典写法。
  • strcmp 比较字符串内容,不能直接用 ==
  • 最后用指针遍历用户名统计字符数——虽然 strlen 也能做到,但这里是为了演示指针遍历。

八、字符串处理函数的源码实现

理解这些函数的内部实现,能帮你更深刻地理解指针和字符串。以下是 strlenstrcpy 的手动实现:

// 手动实现 strlen
int my_strlen(const char *str) {
    int count = 0;
    while (*str != '\0') {
        count++;
        str++;  // 指针后移
    }
    return count;
}

// 手动实现 strcpy
char* my_strcpy(char *dest, const char *src) {
    char *original = dest;  // 保存目标起始地址
    while (*src != '\0') {
        *dest = *src;  // 复制一个字符
        dest++;
        src++;
    }
    *dest = '\0';  // 别忘了加终止符
    return original;
}

核心逻辑:遍历源字符串,把每个字符复制到目标数组,最后加上 \0。这两个函数的核心都是用指针遍历,以 \0 为终止条件

九、本篇动手练习

练习 1:实现 strcmp

新建 practice6-1.c,手动实现 strcmp 函数。用指针同时遍历两个字符串,逐个字符比较,返回第一个不同字符的差值(或 0 表示相等)。

练习 2:字符串反转

新建 practice6-2.c,让用户输入一个字符串,用指针对这个字符串原地反转。例如输入 "hello",反转后变成 "olleh"。提示:用两个指针,一个指向头部,一个指向尾部,交换它们指向的字符后向中间移动。

练习 3:统计字符类型

新建 practice6-3.c,让用户输入一个字符串,统计其中大写字母、小写字母、数字、空格各有多少个。用指针遍历实现。

练习 4:简单加密

新建 practice6-4.c,让用户输入一个字符串,对每个字符的 ASCII 码加 1(如 'A''B''z''{'),输出加密后的字符串。再写一个解密函数把加密后的字符串还原。

十、本篇小结

这一篇你学会了 C 语言中字符串的核心知识:

  • 字符串的本质:以 \0(空字符)结尾的字符数组。没有专门的字符串类型,字符串就是 char[]char*
  • 声明和初始化char str[] = "Hello"(可修改,在栈上)vs char *str = "Hello"(通常不可修改,指向只读区)。
  • 输入输出printf("%s", str) 输出,scanf("%s", str) 输入(遇到空格停止,不安全),fgets(str, size, stdin) 更安全。
  • 常用字符串函数strlen(长度)、strcpy/strncpy(复制)、strcmp(比较,不能用 ==)、strcat/strncat(拼接)。永远注意目标数组的大小,防止越界。
  • 字符串和指针:字符串就是 char*,可以用指针遍历字符串。char *p = str; while (*p != '\0') { ... p++; } 是经典遍历模式。
  • 字符串数组:二维数组 char arr[][N] 固定长度但可修改,指针数组 char *arr[] 灵活但通常不可修改。

C 语言的字符串没有 Python/JavaScript 那样丰富的内置方法,但正因为它“简陋”,你能清楚地看到字符串在内存中是怎么存的、\0 是怎么起作用的。下一篇,我们学习 C 语言中的函数——如何定义函数、传值和传指针的区别、递归函数。

下一篇预告

下一篇——《函数——封装可复用的代码块》:函数的定义和声明、函数原型、传值调用 vs 传指针调用、递归函数、函数指针初探。同时对比 JavaScript 和 Python 中的函数,理解 C 语言函数的独特之处。

C/C++ 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容