C 语言字符串:从字符数组、指针到核心操作实战

字符串的两种表现形式

字符数组:小心缺少的结束符

字符串有两种形式,第一种形式是字符数组:

c 复制代码
#include <stdio.h>

int main() {
    // 字符数组
    char arr[] = { 'H','e','l','l','o','!' };
    printf("%s", arr);
    return 0;
}

运行后,你会发现打印结果是:Hello!烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫

不要着急,这是因为字符串遇到 \0 空字符才会停止,而我们定义的字符数组没有这个结束符,程序自然会越界读取栈区未初始化的内存(0xCCCC),从而以字符串形式打印时,会显示为 "烫",当遇到 \0 才会停止。

我们加上 \0 即可,或是输出字符串长度的字符:

c 复制代码
#include <stdio.h>

int main() {
    // 字符数组
    char arr[] = { 'H','e','l','l','o','!','\0' }; // 添加 \0 空字符
    printf("%s\n", arr);

    // 按需打印
    char arr2[] = { 'H','e','l','l','o','!' };
    int len = sizeof(arr2) / sizeof(arr2[0]);
    for (int i = 0; i < len; i++)
    {
        printf("%c", arr2[i]);
    }
    printf("\n");

    return 0;
}

字符数组还有另一种定义的方式,这种方式会自动在末尾添加 \0

c 复制代码
#include <stdio.h>

int main() {
    char arr[] = "Hello!"; // 自动加上 \0 空字符
    printf("%s\n", arr);

    return 0;
}

字符指针:指向常量的字符串

第二种形式是字符指针,指向字符串常量的首地址。

c 复制代码
#include <stdio.h>

int main() {
    const char* str = "hello";
    return 0;
}

核心差异剖析:读写权限与内存分配的区别

这两种形式有什么区别呢?

首先第一种形式,数据存在于 栈区,我们可以修改其内容。

即使用字符串常量去初始化数组(如 char arr[] = "Hello!";),只要它在函数内部,也是在栈区。因为程序运行时,会把常量区的 "Hello!" 拷贝一份到栈区的数组中。

c 复制代码
#include <stdio.h>

int main() {
    char arr[] = { 'H','e','l','l','o','!','\0' }; // 栈区
    char arr2[] = "Hello!"; // 还是栈区

    // 修改字符
    arr[1] = 'a';
    arr2[0] = 'h';

    printf("arr: %s\n", arr); // arr: Hallo!
    printf("arr2: %s\n", arr2); // arr2: hello!

    return 0;
}

第二种形式,指针所指向的内容存在常量区 ,只可以读不能改。我们从指针的类型(const char*)也能看出,它是指向常量的字符指针。当然,指针自身的指向可以改变,因为它不是常指针(char* const)。

c 复制代码
#include <stdio.h>

int main() {
    const char* str = "hello world!";
    str[5] = '-'; // 报错:表达式必须是可修改的左值
    str = "maybe"; // 正常运行
    return 0;
}

字符串基本操作实战

下面,我们来练习一下字符串的基本操作。

获取长度

先从获取字符串长度开始。

c 复制代码
#include <stdio.h>
#include <string.h>

// 获取字符串长度,返回无符号整数
// 关键点:不断读取字符,遇到 \0 就停止
size_t my_strlen(const char* str) {
    size_t length = 0;
    const char* cur = str;
    while (*cur != '\0') {
        length++, cur++;
    }
    return length;
}

int main() {
    const char* const str = "hello world!";
    printf("str's length is %zu", my_strlen(str)); // 12
    return 0;
}

<string.h> 头文件中已经存在 strlen,用于获取字符串的长度。

字符串比较

然后是字符串比较,我们可以使用 <string.h> 中的 strcmp

  • str1 等于 str2,函数会返回 0。
  • str1 小于 str2,函数会返回负数。
  • str1 大于 str2,函数会返回正数。
c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    const char* str1 = "World";
    const char* str2 = "Word";
    int res = strcmp(str1, str2);
    if (res == 0)
    {
        printf("equal");
    }
    else if (res < 0) {
        printf("less than");
    }
    else {
        printf("greater than");
    }
    return 0;
}

这个函数是区分大小写的。如果要忽略大小写,在 Windows 环境下可以使用 strcmpi_stricmp;在 Linux 环境下,你需要使用 strcasecmp,它定义在 <strings.h> 头文件中。

子串查找

字符串查找使用的是 strstr

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    const char* str = "my name is leon,I'm happy!";
    const char* sub_str = "leon";

    // 如果找得到,返回的是子串在原串中第一次出现的位置
    // 找不到,返回的是 NULL
    const char* pos = strstr(str, sub_str);
    if (pos != NULL)
    {
        printf("exist\n");
        printf("%s\n", pos); // leon,I'm happy!
    }
    else {
        printf("not exist\n");
    }

    // 转换为索引:直接相减即可,指针相减的结果就是相隔的元素个数
    int index = pos - str;
    printf("index is %d\n", index); // 11

    return 0;
}

字符串拷贝

字符串拷贝使用的是 strcpy,它会将原字符串(第二个参数)拷贝到目标字符数组(第一个参数)中。

拷贝过程中遇到 \0 将会停止,并且拷贝结束后会自动添加上结束符 \0

注:这里使用的 strcpy_s 是 Windows 特有的安全函数版本,如果在 Linux 等其他环境下,请直接使用 strcpy

C 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "Jack";
    char dest[5];

    strcpy_s(dest, src);
    printf("After strcpy_s: %s\n", dest);

    return 0;
}

字符串拼接

字符串拼接使用的是 strcat

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str[100] = "My dream ";
    char str2[50] = "will not end. ";

    strcat_s(str, str2);
    printf("%s\n", str); // My dream will not end. 

    strcat_s(str, "even if ");
    strcat_s(str, "it is very small.");
    printf("%s\n", str); // My dream will not end. even if it is very small.
    return 0;
}

手写字符串替换函数

最后是字符串替换,这个稍微复杂点,特别是查找、替换的过程,需要处理好边界。

c 复制代码
#include <stdio.h>
#include <stdlib.h> // 需要用到 calloc 和 free
#include <string.h> // 需要用到 strlen, strstr, memcpy

// 字符串替换
char* replace(const char* src, const char* old_sub, const char* new_sub) {
    // 边界校验
    if (!src || !old_sub || !new_sub) return NULL;
    size_t old_sub_len = strlen(old_sub);
    if (old_sub_len == 0) return NULL; // 禁止空匹配串

    size_t src_len = strlen(src);
    size_t new_sub_len = strlen(new_sub);

    // 查找 old_sub 出现的次数,方便进行多次替换
    size_t count = 0;
    const char* p = src;
    while ((p = strstr(p, old_sub)) != NULL) {
        count++;
        p += old_sub_len; // 跳过已匹配的部分
    }

    // 计算替换后的新字符串长度,开辟空间 (包含末尾的 \0)
    size_t new_len = src_len + count * (new_sub_len - old_sub_len);
    char* new_str = (char*)calloc(new_len + 1, sizeof(char));
    if (new_str == NULL) {
        // 内存分配失败
        return NULL;
    }

    // 进行替换
    char* dest = new_str;
    const char* start = src; // 每次匹配时的起点
    const char* cur_pos = start;
    while ((cur_pos = strstr(cur_pos, old_sub)) != NULL) {
        // 拷贝匹配点之前的字符
        size_t pre_len = (size_t)(cur_pos - start);
        memcpy(dest, start, pre_len); // 将 src 中长度为 pre_len 的字符复制到新字符串 dest 上
        dest += pre_len;

        // 拷贝替换的字符串
        memcpy(dest, new_sub, new_sub_len);
        dest += new_sub_len;

        // 移动原字符串中的位置指针,跳过已匹配项
        cur_pos += old_sub_len;
        start = cur_pos;
    }

    // 拷贝剩余的字符串
    size_t tail_len = strlen(start);
    memcpy(dest, start, tail_len);
    dest[tail_len] = '\0';

    return new_str;
}

int main() {
    char origin[] = "Hello World! World is big, World is beautiful";
    char* res = replace(origin, "World", "C Language");
    if (res != NULL)
    {
        printf("After replace: %s\n", res);
        free(res);
        res = NULL;
    }
    return 0;
}
相关推荐
Java小白笔记2 小时前
MySQL中存储过程大表分批删除历史数据
android·mysql·adb
aidou13142 小时前
Kotlin中沉浸式状态栏显示布局
android·kotlin·windowmanager·沉浸式状态栏·insetslistener·insetscompat·systembars
阿pin3 小时前
Android随笔-pipe是什么?
android·linux·pipe
帅次3 小时前
Android 高级工程师面试:Flow 与状态流 近1年高频追问 22 题
android·面试·职场和发展
雨季余静3 小时前
RustDesk Android APK 从零编译全步骤
android
辰合软件3 小时前
通达OA配置文件详解
android·adb
辰合软件4 小时前
通达OA目录结构与核心文件解析
android·数据库·oracle
通玄4 小时前
Jetpack Compose 入门系列(八):Compose 中的副作用处理
android
流氓也是种气质 _Cookie4 小时前
如何创建MCP
android·adb