C语言之字符串操作

C语言之字符串操作

C 语言提供了丰富的字符串处理相关的库函数,这些函数基本上,都声明在头文件string.h当中,所以使用它们需要包含这个头文件。这里只介绍几种最基本的和最常用的,以及手动实现它们的方式。

字符串长度strlen

strlen

函数全名:string_length

函数声明:

size_t strlen (const char *s);

函数作用:返回当前字符串的长度,也就是字符数组中空字符'\0'前面的字符数量。不包括空字符'\0'!

int len;
char str[] = "abcd";
char str2[10] = "12345";
char str3[5] = { 'a','\0','c' };
len = strlen("abc");    /* len is now 3 */
len = strlen("");       /* len is now 0 */
len = strlen(str);      /* len is now 4 */
len = strlen(str2);     /* len is now 5 */
len = strlen(str3);     /* len is now 1 */

char str4[4] = "1234";
len = strlen(str4);     // str4并不能表示一个字符串,该函数调用会引发未定义行为

手动实现my_strlen

size_t my_strlen1(const char* str) {
    size_t len = 0; 
    while (*str != '\0') {		// 遍历直到遇到空字符
        len++;
        str++;
    }
    return len;
}

// 数组下标形式
size_t my_strlen2(const char* str) {
    size_t len = 0; 
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

size_t my_strlen3(const char* str) {
    const char* p = str;	// 用一个指针记录数组首元素
    while (*str) {		// 惯用法: 遍历直到字符串的末尾,
        str++;
    }   // 循环结束时,str指针指向空字符,p指针指向数组首元素
    
    // 相当于空字符的下标减去首元素下标,结果就是字符串的长度
    return str - p;
}

字符串复制strcpy,strncpy

strcpy

函数全名:string_copy

函数声明:

char *strcpy(char *dest, const char *src);

函数作用:src 中存储的以空字符 '\0' 结束的字符串复制到 dest所指向的数组中。

也就是说,会从首字符开始逐个字符地从 src 复制字符,包括空字符在内,都会从dest首位置开始,复制到dest当中。

这个过程中,src数组是不会被修改的,所以它被const修饰。总之,该函数调用后,dest 将包含 src 的完整副本。

char src[] = "hello";
char dest[10];

// 调用函数后,src不变
// dest数组就会变成{'h','e','l','l','o','\0', ....}
// 其中前6个字符是字符串"hello"
strcpy(dest, src);

函数返回值:

该函数会返回指向目标数组 dest 的指针,一般来说,该函数的返回值没有什么意义,推荐忽略它。

但某些场景下,这个返回值允许strcpy 函数的调用,可以被嵌套使用或者用于更复杂的表达式中。比如:

// strcpy返回复制完成后指向 dest 数组的指针
char src[] = "hello";
char dest[10];
char dest2[10];

// 可以直接利用printf、puts函数打印复制后的目标数组
puts(strcpy(dest, src));

// 更加复杂的函数调用
// 将src复制到dest,再将dest复制给dest2
strcpy(dest2, strcpy(dest, src));

注意:

  1. strcpy函数是不安全的,它并不会检查dest是否真的能够包含src数组。如果dest不够大,就会因数组越界而产生未定义行为。
  2. 为了安全起见,可以考虑使用strncpy函数解决这一问题,虽然它可能效率更低一些。

strncpy

函数声明:

char *strncpy(char *dest, const char *src, size_t n);

函数作用:

该函数会将最多n个字符从src中复制到dest数组中:

  1. 如果n < strlen(src) + 1,也就是 n 小于源字符串的长度 + 1时,此时strncpy函数就无法将整个源字符串数据都复制到 dest 中,并且此时复制完成的 dest 数组也不会以空字符结尾,无法表示一个字符串。不过好在,它不会引起越界问题,是安全的操作。
  2. 如果n = strlen(src) + 1,即 n 恰好是源字符串长度 + 1时,strncpy函数会将src完整复制到dest数组中,包括空字符。
  3. 如果n > strlen(src) + 1,即 n 完全大于源字符串长度 + 1时,strncpy函数不仅会完整复制src字符串到dest中,还会将剩余 (n - strlen(src) - 1) 个字符设置为空字符。

strncpy函数一定会处理n个字符数据,如果n比较大,在复制完源数组后,它会顺道将 dest 数组中的元素置为空字符。

实际开发中,建议将n的值设定为 dest长度-1,并主动将dest的最后一个元素设置为空字符,这样总能安全地得到一个字符串。

也就是以下操作代码:

strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
char src[] = "hello";
char dest[10];
// 这个复制是没有问题的
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

char dest2[5];
// 这个复制虽然没有真的将"hello"都复制到dest2,但是一个安全的操作。最终dest2存放字符串"hell"
strncpy(dest2, src, sizeof(dest2) - 1);
dest2[sizeof(dest2) - 1] = '\0';

这样的复制行为,若dest足够装下src,会导致dest剩余部分全都被设置为空字符。

手动实现my_strcpy、my_strncpy

my_strcpy

char* my_strcpy1(char *dest, const char *src) {
    char *tmp = dest;   // dest数组的首元素指针要作为返回值,所以要临时保存一下
    while (*src) {
        *dest++ = *src++;
    }   // 这个while循环没有复制空字符,所以需要手动来复制空字符
    // while循环结束时,src指针指向空字符, dest指针也指向对应的位置
    *dest = 0;
    return tmp;
}


// 简化: 复制字符串的惯用法
char *my_strcpy2(char *dest, const char *src) {
    char *tmp = dest;   // dest数组的首元素指针要作为返回值,所以要临时保存一下
    /*
        (*dest++ = *src++)
        主要作用: 返回*src的值
        副作用: src和dest都往后挪动指针

        当主要作用返回src指针指向的元素, 且返回空字符结束这个while循环时
        =的副作用赋值并不会消失
        所以所以这种写法, 空字符也会连带被复制
        不需要考虑空字符的手动复制
    */
    while (*dest++ = *src++)
        ;
    return tmp;
}

my_strncpy

char *my_strncpy1(char *dest, const char *src, size_t n) {
    char *tmp = dest;
    /*
            复制src到dest,直到达到n个字符或遇到src的终结符
            while循环结束条件是:
            1.n足够长时(n >= strlen(src)+1时),会将src中直到空字符的数据复制到dest中,然后结束循环
            2.n不够长时(n < strlen(src)+1时),会将src中的前n个字符复制到dest中,然后结束循环
                此时dest数组不会存储空字符,它不能表示字符串,但不会有越界风险
        */
    while (n > 0 && (*dest++ = *src++)) {
        n--;
    }
    /*
            当n >= strlen(src) + 1时
            上面的while循环依靠(*dest++ = *src++)结束循环
            此时空字符被复制到dest中了
            但循环体没有执行循环就终止了, n--没有执行, 即复制了一个字符但n没有减少1
            所以下面要判断n > 1 而不是n > 0
        */
    while (n > 1) {
        *dest++ = '\0';
        n--;
    }
    return tmp;
}
// 如果不将n设置为无符号类型,而设置为int类型
char *my_strncpy2(char *dest, const char *src, int n) {
    char *tmp = dest;
    // 这个循环不用担心无符号0减1变为最大值的情况发生,可以放心使用
    while (n-- && (*dest++ = *src++))
        ;
    // 由于n--正常执行,所以下面仍然可以判断n>0
    while (n > 0) {
        *dest++ = 0;
        n--;
    }
    return tmp;
}

// 当然为了追求原汁原味, 还是应该保留n的size_t类型
// 同时为了保证n>0的正常使用,我们就需要保证n--正常执行
char *my_strncpy3(char *dest, const char *src, size_t n) {
    char *tmp = dest;
    while (n > 0 && (*dest = *src)) { 
        dest++;
        src++;
        n--;
    }// while循环结束时,src指针指向指向空字符,dest指针也指向空字符
    // 虽然空字符已经复制结束后,但由于指针仍然指向空字符,下面的while循环就可以n>0执行
    while (n > 0) {
        *dest++ = '\0';
        n--;
    }
    return tmp;
}

字符串拼接strcat,strncat

strcat

函数全名: string_concat,顾名思义,它用于将一个字符串拼接到另一个字符串的末尾。

函数声明:

char *strcat(char *dest, const char *src);

函数作用: strcat 函数会将 src(源字符串)中存储的以空字符 '\0' 结束的字符串拼接到 dest(目标字符串)的末尾。

具体来说:

会从 dest 字符串的空字符 '\0' 开始,替换这个空字符,然后将src中表示字符串的字符数据从开头到空字符,全部拼接到dest末尾。

这个过程中,src 字符串不会被修改,所以它被 const 修饰。

总之,该函数调用后,dest 将包含 dest 和 src 的字符串拼接后的副本。

简单的代码示例:

char dest[20] = "Hello, ";
char src[] = "World!";
// 调用函数后,dest数组变为 "Hello, World!\0",其余部分填充为空字符
strcat(dest, src);
// 输出拼接后的字符串
printf("%s", dest); // 输出 "Hello, World!"

函数返回值:

该函数会返回指向目标数组 dest 的指针。一般来说这个返回值可以忽略,但有些场景中可以利用该返回值,对函数调用进行嵌套处理。

例如:

char dest[20] = "Hello, ";
char src[] = "Everyone!";
char src2[] = " Have a nice day!";
// 嵌套,将src和src2都拼接到dest上
strcat(strcat(dest, src), src2);
// 输出最终的拼接字符串
printf("%s", dest); // 输出 "Hello, Everyone! Have a nice day!"

注意:

  1. src 和 dest两个参数都必须是字符串,即以空字符串结尾的字符数组,否则将引发未定义行为。
  2. strcat 函数与 strcpy 一样,不会检查 dest 数组是否能够容纳拼接后的字符串。如果 dest 不够大,就会产生未定义行为,这是潜在的安全隐患。
  3. 可以考虑使用更安全的strncat函数来实现字符串拼接,它允许指定最大拼接的字符数。

strncat

函数声明:

char *strncat(char *dest, const char *src, size_t n);

函数作用:

仍旧是找到dest字符串末尾的空字符,然后从该字符开始,将 src 的首个元素复制到 dest末尾,直到:

  • 已经复制了 n 个字符。

  • 或者复制到达了src字符串的结尾,即遇到了src的空字符串。所以该函数不会把src中的空字符复制到dest中

最后,strncat函数一定会在dest的末尾添加一个空字符,以确保dest能够表示一个字符串。由于这一步操作的存在,该函数仍然具有越界访问的风险,需要程序员自己设定合理的n取值,以规避这种情况。

为了安全的使用此函数,基于函数的特点,我们就可以得出n的计算公式:

int n = sizeof(dest) - strlen(dest) - 1;    // dest数组的长度 - 已存储字符串的长度 - 1(留给存空字符)

表达式中-1是必要的,否则会导致数组越界情况发生。

给strncat函数传入这样的一个n参数,就能够保证一定安全的得到一个拼接后的结果字符串。

当然若dest容量不够大,拼接只会截取src的一部分字符数据。但安全是更重要的事情,这样的结果是我们可以接受的。

实际开发中,建议采取以下方式来调用此函数,避免dest空间不足导致溢出:

char src[] = "world";
char dest[10] = "hello, ";
// 确保dest是一个数组类型,而不是一个指针类型才能这样操作
int n =  sizeof(dest) - strlen(dest) - 1;
strncat(dest, src, n);

dest最多再拼接(自身数组长度 - 字符串长度 - 1)个字符,最后留一个字节,strncat函数会自动拼接一个空字符表示字符串结束。

手动实现my_strcat、my_strncat

my_strcat

char* my_strcat(char* dest, const char* src) {
    char* temp = dest;
    // 找到dest的末尾,也就是让dest指针指向空字符
    while (*dest) {
        dest++;
    }

    // 将src的字符串数据从开头到空字符都拷贝到dest的末尾
    while ((*dest++ = *src++))
        ;
    return temp;  // 返回dest的起始地址
}

my_strncat


char* my_strncat1(char* dest, const char* src, int n) {
    char* temp = dest;
    // 找到dest的末尾,也就是让dest指针指向空字符
    while (*dest) {
        dest++;
    }

    // 拷贝最多n个字符从src到dest的末尾
    while (n-- && (*src)) {
        *dest++ = *src++;
    }

    // 确保dest以空字符结尾,所以一定会将末尾置为空字符。这里没有判断越界,所以strncpy函数实际仍存在越界风险
    // 这就要求我们传入一个正确合理的n以确保安全    
    *dest = '\0';
    return temp;  // 返回dest的起始地址
}

字符串比较大小strcmp

strcmp

函数全名:string_compare,用于比较两个字符串的大小。

函数声明:

int strcmp(const char *str1, const char *str2);

函数作用: strcmp函数按照字典顺序比较两个字符串的大小。当调用 strcmp函数时,它会逐个字符地比较两个字符串 str1 和 str2:

注意:

  1. 这个函数不会修改任何一个字符串,所以两个参数都被 const 修饰。
  2. 不同编译器和环境可能对strcmp函数返回值的表现有所不同。在某些环境中(比如VS),出于简化的目的,可能会将返回值归一化为 -1、0 或 1。但是标准的 C 语言库中 strcmp 的返回值是根据实际字符的ASCII值差异来确定的,而不是简单的 -1、0 或 1。
  3. 要确保传入的两个参数数组都表示字符串,都以空字符结尾,否则可能会导致比较越界。
  4. strcmp 是区分大小写的比较,如果需要进行不区分大小写的比较,可以使用 strcasecmp 函数。
  5. 由于strcmp是基于ASCII值进行比较的,它在处理非ASCII或多字节字符(如UTF-8编码的文本)时可能不会表现出预期的行为。

手动实现strcmp

int my_strcmp1(const char* str1, const char* str2) {
    while (*str1 && *str2) {
        if (*str1 != *str2)
        {
            return *str1 - *str2;
        }
        str1++;
        str2++;  
    }
    return *str1 - *str2;
}

// 这个实现可读性更差,但简洁很多
int my_strcmp2(const char* s1, const char* s2) {
    while (*s1 && (*s1 == *s2)) {
        // 移动两个指针,比较下一对字符
        s1++;
        s2++;
    }
    /*
    * while循环结束的条件是:
    * 1.s1指针指向了空字符
    * 2.或者s2指针指向了空字符
    * 3.或者s1和s2指针指向的字符不一致
    * 不管是哪一种情况,返回它们的编码值差就是结果
    */
    return *s1 - *s2;
}

字符相关的库函数

在操作字符串的过程中,经常需要对单个字符做出判断,比如:

  1. 判断字符是大写字母还是小写
  2. 判断字符是不是数字
  3. ...

C 标准函数库在头文件 <ctype.h> 中定义了大量此类功能函数。比如:

islower 和 isupper 函数是 C 标准库中声明在 <ctype.h> 头文件中的函数,它们用于检查给定的字符是否为小写字母或大写字母。

除此之外,<ctype.h> 头文件中还包括了以下类似的库函数:

  1. isalpha(int c):检查传入的字符是否为字母(包括大写和小写)。
  2. isdigit(int c):检查传入的字符是否为十进制数字(0到9)。
  3. isalnum(int c):检查传入的字符是否为字母或数字。
  4. isspace(int c):检查传入的字符是否为空白字符,比如空格、制表符、换行符等。
  5. isblank(int c):检查传入的字符是否为空格或制表符。
  6. ...
posted @   Invinc-Z  阅读(114)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示