字符函数和字符串函数
Suzhou 37 1

字符函数

C语言中对字符和字符串的处理特别频繁,但C语言本身没有字符串类型,字符串通常放在常量字符串或字符数组中。字符串以“\0”作为结束标志。

求字符串长度

strlen

字符函数和字符串函数 字符函数和字符串函数

int main()
{
    int len = strlen("abcdef");
    printf("%d\n", len);
    char arr[] = { 'a','b','c','d','e','f'};  //错误写法     
    len = strlen(arr);
    printf("%d\n", len);
    return 0;
}
6
42

::: tip strlen求字符串长度时,将字符串每一个字符和“\0”比较,如果不相等,计数器++,直到找到"\0",计数停止。要求字符串中必须有“\0”。 ::: 模拟实现strlen的三种方法

  1. 计数器法
  2. 递归法
  3. 指针-指针
    #include <assert.h>
    int my_strlen(const char* str)  //使用指针接收,函数只求长度不改字符串,
    {
     assert(str);
     int count = 0;
     while (*str != '\0')  
     //whilei循环条件也可以写成*str。如果*str不是0,条件为真,进入循环
     {
         count++;
         str++;
     }
     return count;
    }
    int main()
    {
     int len = my_strlen("abcdef");  //实际传到自定义函数的是字符“a”的地址
     printf("%d\n", len);
     return 0;
    }
    注意
    int main()
    {
     int len = strlen("abcdef");
     if (strlen("abc") - strlen("abcdef") > 0)
         printf("A");
     else
         printf("B");
     return 0;
    }
    B
    库函数strlen的返回类型是size_t,上述代码的返回类型是int。 ::: warning 字符函数和字符串函数

系统把无符号整型重命名为size_t,即size_t == unsigned int。 ::: 上述代码中无符号的3减去无符号的6,结果-3当成无符号数将补码存在内存中,是一个大于0的数字。


长度不受限制的字符串函数

字符串的长度不受限制,以“\0”作为结束标志。 存在的问题是在拷贝追加的过程中,不考虑目的字符串的大小,可能会发生上限越界。

strcpy-字符串拷贝

字符函数和字符串函数 ::: warning 源字符串不能是char arr[] = {'a','b','c'}的形式。原因是这种写法字符串内没有“\0”。 目的字符串不能是char * arr = "abc"的形式。原因是这种写法中arr指向的是常量字符串,不能被修改。 ::: strcmp对字符串进行拷贝后,希望返回目的空间的起始位置,类型是char字符函数和字符串函数 *如上图,strcmp在字符串拷贝的时候,将源字符串的全部内容包括“\0”都拷贝过去,目标字符串中原有的内容除了被替换的字符外其他内容不变。对字符串的操作是遇到“\0”停止。**

strcpy模拟实现
#include <assert.h>
char* my_strcmp(char* dest, const char* src)
{
    //先保证指针有效性
    assert(dest);
    assert(src != NULL);
    char* ret = dest;
    //保存目的空间的起始位置,后面dest会改变

//while判断的第一种写法
    while (*src)
    {
        //拷贝“world”
        *dest = *src;
        dest++;
        src++;
    }*dest = *src;  //拷贝“\0”

//while判断的第二种写法
    while (*src)
    {
        *dest++ = *src++;  //拷贝“world”
        //dest++;
        //src++;
    }*dest = *src;  //拷贝“\0”

//while判断的第三种写法
    while (*dest++ = *src++)
    {
        ;
    }

    return ret;  //返回目的空间的起始位置
}
int main()
{
    char arr1[] = "hello world";
    char arr2[] = "world";
    strcpy(arr1, arr2);
    my_strcmp(arr1, arr2);
    printf("%s\n",arr1);
    return 0;
}

::: tip 上述代码第行while ( * dest++ = * src++)代码分析: 当 * src的值“w”赋给 * dest时,表达式 * dest++ = * src++ 的结果是“w”,不等于0,进入循环, * src和 * dest后置++后将下一个字符“o”赋值给 * dest,表达式的结果变为“o”,进入循环。代码在产生赋值的同时做了判断,又执行了自增,循环直到 * src将“\0”拷贝给 * dest,替换了 * dest的“ ”,赋值导致表达式的结果变为“\0”,“\0”的ASCII值是0,表达式为假,循环停止。此时数组arr2中的所有内容都被拷贝过去。 :::


strcat-字符串追加

字符函数和字符串函数 字符函数和字符串函数 上图中表示字符串拷贝时是从目的字符串的第一个“\0”处开始向后追加(覆盖后面的“\0”),源字符串的“\0”也拷贝到目的字符串中。 即目的字符串中的“\0”作用是提示源字符串追加时的起始位置,源字符串中的“\0”的作用是提示追加时停止的位置。 字符数组后面空余的空间全部填充“\0”。 ::: warning 目的字符串不能是char arr1[] = "hello "的写法。原因是不指定数组大小时,数组根据初始化来确定大小。拷贝时应确保目的数组空间足够大。 源字符串中必须有“\0”,数组拷贝时以“\0”作为拷贝结束的标志。 :::

strcat模拟实现
#include <assert.h>
char* my_strcat(char* dest, const char* src)
{
    assert(dest && src);
    char* ret = dest;
    //遍历找到目的字符串的“\0”
    while (*dest != '\0')
    {
        dest++;
    }
    //追加源字符串(相当于从目的字符串的“\0”处开始拷贝源字符串)
    while (*dest++ = *src++)
    {
        ;
    }
    return ret;
}
int main()
{
    char arr1[15] = "hello ";
    char arr2[] = "world";
    my_strcat(arr1, arr2);
    printf("%s\n", arr1);
    return 0;
}

strcmp-字符串比较

字符函数和字符串函数 当常量字符串abc作为一个表达式存在的时候,产生的值不是字符串,而是字符串首字符a的地址。所以字符串比较时不能直接使用比较运算符。 strcmp比较的是对应字符的ASCII值,字符从前向后比较,小的字符对应的字符串小于另外一个字符串,对应字符相等时比较下一个对应字符。同时遇到“\0”时比较结束,输出0。

strcmp模拟实现
#include <assert.h>
int my_strcmp(const char* str1, const char* str2)
{
    assert(str1, str2);
//解法1:
    while (*str1 == *str2)
    {
        if (*str1 == '\0')
        {
            return 0;
        }
        str1++;
        str2++;
    }
    if (*str1 > *str2)
    {
        return 1;
    }
    else
    {
        return -1;
    }
}
//解法2:
//    do
//    {
//        if (*str1 > *str2)
//        {
//            return 1;
//        }
//        else if (*str1 < *str2)
//        {
//            return -1;
//        }
//        else
//        {
//            return 0;
//        }
//        str1++;
//        str2++;
//    } while (*str1 != '\0');
//}
int main()
{
    char* p1 = "abcdef";
    char* p2 = "abcqwe";
    int ret = my_strcmp(p1, p2);
    if (ret == 0)
    {
        printf("相等");
    }
    else if (ret > 0)
    {
        printf("大于");
    }
    else
    {
        printf("小于");
    }
    return 0;
}

长度受限制的字符串函数

strncpy-拷贝n个字符

字符函数和字符串函数 strncpy严格按照n的值执行复制: 字符函数和字符串函数 strncpy拷贝时,如果n大于源字符串的长度,源字符串拷贝过去后补0到n: 字符函数和字符串函数

strncpy模拟实现
#include <string.h>
#include <assert.h>
void my_strncpy(char* str1, char* str2, int num)
{
    assert(str1, str2);
    if (num < strlen(str2))
    {
        while (num != 0)
        {
            *str1++ = *str2++;
            num--;
        }
    }
    else if (num > strlen(str2))
    {
        while (*str2 != '\0')
        {
            *str1++ = *str2++;
        }
        int i = num - strlen(str2);
        while (i != 0)
        {
            *str1++ = '\0';
            i--;
        }        
    }
}
int main()
{
    char arr1[20] = "abcdejjnmgknfukygk";
    char arr2[] = "hello";
    my_strncpy(arr1, arr2, 8);
    printf("%s\n", arr1);
    return 0;
}

(技术不精代码写的自己都觉得垃圾...有大佬可以给个思路就更好了

strncat-追加n个字符

strncat严格按照n的值进行追加,如果追加后没有“\0”,strncat希望追加后的内容也是字符串,所以strncat会在追加后再添加一个“\0”: 字符函数和字符串函数 strncat追加时,如果n大于源字符串的长度,只追加源字符串内的内容,多出来的长度不处理: 字符函数和字符串函数


strncmp-比较n个字符

字符函数和字符串函数

#include <string.h>
int main()
{
    const char arr1[] = "abcdef";
    const char arr2[] = "abcqwer";
    //int ret = strcmp(arr1, arr2);  //-1
    //int ret = strncmp(arr1, arr2,3);  //0
    int ret = strncmp(arr1, arr2, 10);  //-1
    printf("%d\n", ret);
    return 0;
}

如上代码,strncmp比较到“\0”停止或者比较num个元素后停止。

字符串查找

strstr-查找字符串

文档中NULL表示空指针,NUL和Null表示“\0”。

#include <string.h>
int main()
{
    char* p1 = "abcdefgh";
    char* p2 = "def";
    char* ret = strstr(p1, p2);  
    //在str1中查找str2是否存在,如果存在,返回找到的首字符地址,如果不存在,返回0
    if (ret == NULL)
    {
        printf("不存在\n");
    }
    else
    {
        printf("%s\n", ret);  //defgh
    //ret是字符串名,存储的是找到的字符串的首字符地址,打印ret得到字符串(“\0”为止)
    }
    return 0;
}

假设目标字符串中包含多个要查找的字符串,strstr函数找到的是第一个查找到的字符串的地址。如下:是从第一个查找到的字符串的地址向后打印直到“\0”。

#include <string.h>
int main()
{
    char* p1 = "abcdefabcdef";
    char* p2 = "def";
    char* ret = strstr(p1, p2);
    printf("%s\n", ret);  //defabcdef-
    return 0;
}
strstr模拟实现

::: tip 代码思路:要查找的字符串首字符p2和目标字符串的首字符p1比较,如果不相等,p2不变,p1++,向后移动一个字符继续比较。如果相等,p1和p2同时++,比较两个字符串的下一位是否相等。 ::: ::: tip 内层while循环结束标志是:

  • *p1=='\0'(目标字符串全部查找没有找到)
  • *p2=='\0'(要查找的字符串全部查找已经找到)
  • *p!= * p2。(此时还没有找到,如abcmef中查找bcd,b和c查找到,m不等于d,没有找到) :::
  • p1和p2的值不能任意改变,原因是如果要查找的字符串是bbc,目标字符串是abbbcde,当对比到字符串c不相等时,需要被查找字符串回到起始位置b,目标字符串回到第一次 * p== * p2时p1的下一位,即第三个字符b处,继续下一轮查找(bbb匹配失败,从下一位开始匹配bbc)。需要使用一个变量cur记录目标字符串中此时的位置。*
    字符函数和字符串函数
    #include <string.h>
    #include <assert.h>
    char* my_strstr(const char* p1, const char* p2)  //返回的是空指针或有效地址
    {
       assert(p1, p2);  //保证p1和p2的有效性
       char* s1 = p1;
       char* s2 = p2;
       char* cur = p1;  //用来记录第一次*p1==*p2时的位置
       if (*p2 == '\0')
       {
           return p1;
       }
       while (*cur)  //为假则目标字符串匹配结束,匹配失败,返回空指针
       {
           s1 = cur;  //目标字符串回到第一次* p == *p2时p1的下一位
           s2 = p2;  //s2回归,重新查找
           while ((*s1 != '\0') && (*s2 != '\0') && (*s1 == *s2))  //主要的循环判断程序
           {
               s1++;  
               s2++;
           }
           if (*s2 == '\0')  //为真则要查找的字符串匹配结束,匹配成功
           {
               return cur;  //cur此时为p1内匹配成功时的位置,即字符串地址
           }
           cur++;
       }
       return NULL;
    }
    

int main() { char* p1 = "abbbcdef"; char* p2 = "bbc"; char* ret = my_strstr(p1, p2); if (ret == NULL) { printf("不存在\n"); } else { printf("%s\n", ret); } return 0; }

**上述代码中,p2可以是空字符串,p1不能是空字符串。**
::: warning
**在函数实现中,当p2是空字符串时,将空字符串的首字符的地址传进函数中。(空字符串中只有一个字符是结尾的“\0”)。返回p1的首字符地址。** 
:::
::: tip
**KMP算法是strstr算法的进阶形式。**
:::

------------------------------------
##### strtok-字符串分隔
strtok函数使用场景:某些字符串是由一个或多个字符分隔成多个段,需要拿出各个段或某个段时,使用strtok函数操作。如IP地址,邮箱等。
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/4beceeec99120cd24ce2e0995bb8a7b7.png) 
**strtok切割的字符串是原字符串的临时拷贝。**
::: tip
**strtok函数第一次调用时,str是字符串地址(首字符地址),找到str中的分隔符后,会将分隔符改为“\0”,然后返回str的地址,即返回str中第一段的内容。
再次调用strtok函数时,str置为空指针NULL,函数会从刚才“\0”的位置的后一位开始继续向后找,找到下一个分隔符时,将其改为“\0”,返回此次查找起始位置的地址。
以此向后,直到找到str末尾的“\0”,返回最后一次查找时起始的位置。函数再继续向后查找,str为空字符串,找不到分隔符,返回NULL。**
:::

#include <string.h> int main() { char arr[] = "https://www.helloworld.net/80662724"; char* p = ":/."; char buf[100] = { 0 }; strcpy(buf, arr); char* ret = NULL; for (ret = strtok(arr, p); ret != NULL; ret = strtok(NULL, p)) { printf("%s\n", ret); } return 0; }

https www helloworld net 80662724

::: danger
很少用警告...大写加粗来一个

**代码运行时候能消除warning时候尽量消除,如果没有消除,代码执行异常,一定要先去把warning搞掉。**

上面这个代码我没引头文件<string.h>,vs报warning,按照惯例忽略警告,F5执行成功不输出结果,F11运行到一半报错,还考虑了是不是NULL没有定义,最后才发现是头文件的锅...**吃了经验主义的亏**。
:::
**上述代码第9行strtok可以传入字符串也可以传入NULL空指针,在后续调用时strtok可以从上次调用时结束的位置后再输出内容,可以推测strtok内有一个静态变量,第一次strtok调用完毕后不销毁。**

------------------------------------
#### 错误信息报告
strerror-错误报告函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/2870f26504ec790fc6be2e3072bbf197.png)
**strerror的作用是翻译错误信息。**

#include <string.h> int main() { char* str1 = strerror(0); printf("%s\n", str1); //No error char* str2 = strerror(1); printf("%s\n", str2); //Operation not permitted char* str3 = strerror(2); printf("%s\n", str3); //No such file or directory char* str4 = strerror(3); printf("%s\n", str4); //No such process return 0; }

上述代码中strerror中的数字是自己放进去的,真实在代码中出现的是如下情况:

#include <string.h> #include <errno.h> int main() { char* str = strerror(errno); printf("%s\n", str); //No error return 0; }

::: tip
**errno是全局的错误码变量,当C语言的库函数在执行过程中发生错误,就会把对应的错误码赋值给errno变量中。所以只需要检测errno中错误码的值,就可以输出错误信息锁定错误原因。
errno的值由库函数维护。**
:::
使用场景如下:

#include <string.h> #include <errno.h> int main() { FILE* p = fopen("测试", "r"); //表示打开“测试”文件并执行“读”操作并将地址赋给p,fopen函数调用失败会返回空指针 if (p == NULL) { printf("%s\n", strerror(errno)); } else { printf("open file success"); } return 0; }

No such file or directory

上述代码的所在路径下没有名为“测试”的文件,输出结果如上。
在代码路径下新建了名为“测试”的文件后再次执行命令,输出结果如下:

Permission denied


------------------------------------
### 字符操作
#### 字符分类函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/f36de9d902e9aed1e933d870672a4b9d.png)

::: tip
  **字符分类函数中传入的是小写字母,返回非0值,不是小写字母则返回0。**
:::

#include <ctype.h> int main() { printf("%d\n", islower('a')); //2 printf("%d\n", isspace('0')); //0 return 0; }


------------------------------------
#### 字符转换函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/b0dbe54924a2fdaf490c1c123a9b6380.png)
**小写转大写时,ASCII值-32;反之+32。**

#include <ctype.h> int main() { printf("%c\n", tolower('A')); //a printf("%c\n", toupper('a')); //A return 0; }

大写转小写:

int main() { char arr[] = "I Love Yaner"; int i = 0; while (arr[i] != '\0') { if (isupper(arr[i])) { arr[i] = tolower(arr[i]); } i++; } printf("%s\n", arr); return 0; }

i love yaner

------------------------------------

### 内存操作函数
**strcpy、strcat、strcmp、strncpy、strncat、strncmp函数的操作对象是字符串,操作时遇到“\0”停止,不能用来操作整型、浮点型、结构体等其他类型。**

#### memcpy-内存拷贝函数
**memcpy可以操作任意类型的数据,使用无类型指针void * ,size_t类型的num用来标明拷贝的字节数。**
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/b6c74fd0811dea9750bd6a2b449440b1.png)
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/2690587bb8d8f12c9e69157c35b0e8fd.png)

##### my_memcpy函数实现
**代码思路:void * 类型的数据没有类型,不能进行运算,也不能解引用。已知num的大小,即src的字节数,可以逐字节的拷贝,将dest和src强制类型转换成(char * )类型,每次拷贝一个字节,满足题目需要。**

#include <string.h> void* my_memcpy(void* dest, const void* src, size_t num) { assert(dest); assert(src); int i = 0; for (i = 0; i <= num; i++) { (char)dest = (char)src; ++(char)dest; ++(char)src; }

}

struct S { char name[4]; int age; }; int main() { int arr1[] = { 1,2,3,4,5 }; int arr2[5] = { 0 }; my_memcpy(arr2, arr1, sizeof(arr1)); struct S arr3[] = { {"张三",22} ,{"李四",20} ,{"王五",18}}; struct S arr4[2] = { 0 }; my_memcpy(arr4, arr3, sizeof(arr3)); return 0; }

::: tip
**上述代码中自定义函数my_memcpy中的for循环可以改成while(num--)。**
:::
::: warning
**上述函数实现过程中应该返回dest的地址,可以在自定义函数中先把dest暂存在void*ret中,函数最后再return ret。**
:::

当拷贝的源空间和目的空间有交集时,memcpy仍可以正确输出结果:

int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; memcpy(arr + 2, arr, 5 * 4); //将arr中的12345拷贝到34567的位置 int i = 0; for (i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }

1 2 1 2 3 4 5 8 9

**但是根据C语言标准,有内存空间重叠的拷贝,使用memmove函数。**

------------------------------------
#### memmove内存移动函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/4ec62eb012ad27672470e2b6d7dd5a02.png)

int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; memmove(arr + 2, arr, 5 * 4); //将arr中的12345拷贝到34567的位置 int i = 0; for (i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }

1 2 1 2 3 4 5 8 9

::: tip
  在同一个内存空间内进行有内存重叠的拷贝操作时,为了避免需要拷贝的数据被覆盖,需要考虑拷贝的顺序。
:::
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/cb6ad8709e6d5c53d0246c9151fba8a4.png)
下图是123拷贝到345时,3->5的逐字节拷贝示意:
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/6629b37cd4620162521f26fe6b9bea83.png)
如上,拷贝时需要找到最后一个字节,即(char*)src+num-1和(char*)dest+num-1。

::: tip
  **当dest起始位置在src起始位置的左边时,选择从前向后拷贝;当dest起始位置在src起始位置的右边,src结束位置的左边时,选择从后先前拷贝。**
**当dest的结束位置在src的左边,和dest起始位置在src结束位置的右边时,内存空间无重叠,无需考虑拷贝顺序。**
:::

#include <string.h> #include <assert.h> void* my_memmove(void* dest, const void* src, size_t num) { assert(dest); assert(src); if (dest < src) { int i = 0; for (i = 0; i <= num; i++) { (char)dest = (char)src; ++(char)dest; ++(char)src; } } else { while (num--) { ((char)dest + num) = ((char)src + num); } } }

int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; my_memmove(arr + 2, arr, 5 * 4); //将arr中的12345拷贝到34567的位置 int i = 0; for (i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }

::: warning
**上述函数实现过程中应该返回dest的地址,可以在自定义函数中先把dest暂存在void*ret中,函数最后再return ret。**
:::

------------------------------------
#### memset-内存设置
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/3ea3042f04b72e3fd2b86fb649c21f0d.png)
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/82dc904d9614e22cfff5ef172fd69fe1.png)
**memset中的参数num单位是字节,操作非char类型的内存空间时需要注意:**
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/29954fbf4368de968f6919a95636b0f2.png)
#### memcmp-内存比较
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/30b8b3902236de7e06e8459c29107ef8.png)
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/9e55c7fd509e1f004feb3637f17849ad.png)

int main() { int arr1[] = { 1,2,3,4,5,6,7,8,9 }; int arr2[] = { 1,2,6,5,4,8,7,3,9 }; int ret = memcmp(arr1, arr2, 8); printf("%d\n", ret); //0 return 0; }

上述代码比较内存中前8个字节,完全相等。当比较前9个字节时,返回值为-1,如下:
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/ebe4310435fc0ce198bf62f15ab36211.png)
评论区

索引目录