C语言

【C进阶 三】字符串函数与内存函数

2020-03-23  本文已影响0人  不会编程的程序圆

码字不易,对你有帮助 点赞/转发/关注 支持一下作者
微信搜公众号:不会编程的程序圆
看更多干货,获取第一时间更新

思维导图


image

目录


@[toc]

正文


以上这几个函数在我的另一篇文章中我已经详细讲过了。文章链接

这篇文章中,我们先主要来简单实现一下这几个函数,然后再讨论其他函数。

序 老朋友们

myStrlen & myStrcat & myStrcpy

#include<stdio.h>
#include<assert.h>

int myStrlen(const char* str);
char* myStrcat(char* dest, const char* src);
char* myStrcpy(char* dest, const char* src);

int main(void) {

    char str1[12] = "Today ";
    char str2[6] = "sucks";
    
    //str1 和 str2 不是空指针
    if (str1 && str2) {

        printf("str1 is :%s str2 is :%s\n", str1, str2);
        printf("str1 has %d characters, str2 has %d characters\n", myStrlen(str1), myStrlen(str2));
        printf("str1 + str2 = %s\n", myStrcat(str1, str2));
        printf("Copy str2 to str1,now str1 is:%s\n", myStrcpy(str1, str2));
    
    }

    return 0;
}

int myStrlen(const char* str) {

    assert(str != NULL);

    const char* start = str;

    while (*++str);

    return str - start;
    
}

char* myStrcat(char* dest, const char* src) {
    
    assert(dest && src);

    char* ret = dest;

    //找到 dest 中 '\0' 的位置
    while (*++dest);

    while (*dest++ = *src++);

    return ret;
}

char* myStrcpy(char* dest, const char* src) {

    assert(dest && src);

    char* ret = dest;

    while (*dest++ = *src++);

    return ret;
}

MyStrcmp

#define _CRT_SECURE_NO_WARNINGS 1

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

// strcmp 返回值:
// 如果 str1 > str2, 返回的是一个比 0 大的数(不一定是 1)
// 如果 str1 < str2, 返回的是一个比 0 小的数(不一定是 -1)
// str1 > str2 在 VS 上可能是 1,在 linux gcc 中不一定是 1 
// 为了方便实现,我们就选择返回 -1 和 1

int MyStrcmp(char* str1, char* str2) {
    
    assert(str1 != NULL && str2 != NULL);
    
    while (*str1 != '\0' && *str2 != '\0') {
        
        if (*str1 > *str2) {
            return 1;
        }
        else if (*str1 < *str2) {
            return -1;
        }
        str1++;
        str2++;
    }

    // 当循环退出时,str1 或 str2 达到字符串末尾 '\0'
    // 对于 strcmp 来说,如果前面的字符都相同,那么,短的字符串就小
    // 因为 '\0' 的 ASCII 值是 0,它是最小的(达到字符串结尾的字符串用'\0'来进行最后一次比较)

    if (*str1 > * str2) {
        return 1;
    }
    else if (*str1 < *str2) {
        return -1;
    }
    else {
        return 0;
    }

}

int main(void) {
    
    char str1[] = "haha";
    char str2[] = "haha";

    int ret = MyStrcmp(str1, str2);
    
    if (ret > 0) {
        printf("str1 > str2\n");
    }
    else if (ret < 0) {
        printf("str1 < str2\n");
    }
    else {
        printf("str1 == str2\n");
    }

    return 0;
}

始 新朋友们

1. strstr

char *strstr( const char* str, const char* substr );

定义于头文件:<string.h>

查找 substr 所指的空终止字节字符串在 str 所指的空终止字节字符串中的首次出现。不比较空终止字符。

strsubstr 不是指向空终止字节字符串的指针,则行为未定义。

参数:

str - 指向要检验的空终止字节字符串的指针
substr - 指向要查找的空终止字节字符串的指针

返回值:

指向于 str 中找到的子串首字符的指针,或若找不到该子串则为 NULL 。若 substr 指向空字符串,则返回 str

想象实现 MyStrstr 是我们产品经理的需求,先来看看他的需求是什么:

  1. 查找 字符串 substr 在字符串 str 中首次出现的位置(返回找到的字串的首字符的指针)

思路

  1. 我们先在 str 中找到 substr 的第一个元素
  2. 比较 str 的下一个字符与 substr 的下一个字符是否相等(可以循环实现)
  3. 如果 substr 中有一个字符与 str 中的是不一样的,那么 substr 应该从首个元素开始重新在 str 中继续向下寻找,直到找到或者 str 结束
  4. 我忽略了一个条件:当 substr 重新从第一个元素开始在 str 中寻找时,str 应该重置为 上一次 str 所在位置的下一个字符处(比如 "cacacat" 中寻找 "cacat")
实现
#include<stdio.h>
#include<string.h>
#include<assert.h>

char* MyStrstr(const char* str, const char* substr) {
    
    const char* substr_begin = substr;
    const char* str_next = str;

    assert(str != NULL && substr != NULL);

    if (*str == '\0' || *substr == '\0') {
        return NULL;
    }

    while (*str_next != '\0') {
        
        // substr 从头开始
        substr = substr_begin;
        str = str_next;

        if (*str == *substr) {
            while (*str == *substr && *str != '\0' && *substr != '\0') {
                str++;
                substr++;
            }
            // 循环退出三种情况
            if (*substr == '\0') {
                return str_next;
            }
            if (*str == '\0') {
                return NULL;
            }
            // 剩下的一种就是 str 的字串 和 substr 不是完全匹配的,重新找 substr 的第一个元素 
        }
        
        str_next++;
    }
}

int main(void) {

    char str1[] = "Hello World";
    char str2[] = "lo";

    char* ptr = MyStrstr(str1, str2);
    
    if (MyStrstr != NULL) {
        printf("%s\n", ptr);
    }
    else {
        printf("str2 在 str1 中不存在\n");
    }
    
    return 0;
}

2. strtok

了解这个函数即可。

char *strtok( char *str, const char *delim )

定义于头文件 < string.h >

参数:

str - 指向要记号化的空终止字节字符串的指针
delim - 指向标识分隔符的空终止字节字符串的指针

返回值:

返回指向下个记号起始的指针,或若无更多记号则返回 NULL

注意:

此函数是破坏性的:它写入 '\0' 字符于字符串 str 的元素。特别是,字符串字面量不能用作 strtok 的首参数。

每次对 strtok 的调用都会修改静态对象:它不是线程安全的。

strtok 中有个 static 修饰的变量记录下来上次位置

int main(void) {

    char str[] = "World is so beautiful!Hi,Look!";
    char* pch;

    pch = strtok(str, ",. !");
    while (pch != NULL) {
        printf("%s\n", pch);
        pch = strtok(NULL, ",. !");
    }

    return 0;
}
// 输出:
World
is
so
beautiful
Hi
Look

3. memcpy

void* memcpy( void *dest, const void *src, size_t count );

定义于头文件 :<string.h>

src 所指向的对象复制 count 个字符到 dest 所指向的对象。两个对象都被转译成 unsigned char 的数组。

若访问发生在 dest 数组结尾后则行为未定义。若对象重叠(这违背 restrict 契约) (C99 起),则行为未定义。若 destsrc 为非法或空指针则行为未定义。

参数:

dest - 指向要复制的对象的指针
src - 指向复制来源对象的指针
count - 复制的字节数

返回值:

返回 dest 的副本,本质为更底层操作的临时内存地址,在实际操作中不建议直接使用此地址,操作完成以后,真正有意义的地址是dest本身。

注意:

memcpy 可用于设置分配函数所获得对象的有效类型

memcpy 是最快的内存到内存复制子程序。它通常比必须扫描其所复制数据的 strcpy ,或必须预防以处理重叠输入的 memmove 更高效。

许多 C 编译器将适合的内存复制循环变换为 memcpy 调用。

严格别名使用禁止检验同一内存为二个不同类型的值处,可用 memcpy 转换值。

注:

void* 只包含地址,没有内存空间大小这样的信息,所以 void* 不能解引用,也不能进行运算

void* 是为了兼容各种类型的指针,算是一种简单的“泛型编程”。

简单的用法:

int main(void) {

    char src[] = "Once upon a midnight dreary", dest[4];
    memcpy(dest, src, sizeof dest);
    for (size_t i = 0; i < sizeof dest; i++)
        putchar(dest[i]);
    
    int* p = malloc(3 * sizeof(int));// 分配的内存没有有效类型
    int arr[3] = { 1, 2, 3 };
    memcpy(p, arr, 3 * sizeof(int));// 分配到内存有了有效类型 int


    return 0;
}
实现
#include<stdio.h>
#include<assert.h>

void* MyMemcpy(void* dest, const void* src, size_t count) {
    
    assert(dest != NULL && src != NULL);

    void* ret = dest;

    for (size_t i = 0; i < count; i++) {
        *(char*)dest = *(char*)src;// 逐字节复制
        dest = (char*)dest + 1;
        src = (char*)src + 1;
    }

    return dest;
}

int main(void) {

    char src[] = "Once upon a midnight dreary", dest[4];
    MyMemcpy(dest, src, sizeof dest);
    for (size_t i = 0; i < sizeof dest; i++)
        putchar(dest[i]);
    
    int* p = malloc(3 * sizeof(int));// 分配的内存没有有效类型
    int arr[3] = { 1, 2, 3 };
    MyMemcpy(p, arr, 3 * sizeof(int));// 分配到内存有了有效类型 int
    for (int i = 0; i < 3; i++) {
        printf("%d ", *(p + i));
    }

    return 0;
}
思考

请看下例:

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

int main(void) {
    
    double d = 1.0;
    int64_t i = 1;
    
    i = d;
    // 输出:0.000000 5509945

    printf("%f %d\n", i, i);

    memcpy(&i, &d, sizeof d);

    printf("%f %"PRId64" ", i, i);//#define PRId64 "lld"
    //输出:1.000000 4607182418800017408

    return 0;
}

memcpy 该表了 i 作为整型的内存布局,所以 i 可以直接用 %f 输出

4. memmove

void* memmove( void* dest, const void* src, size_t count )

定义于头文件 :< string.h >

src 所指向的对象复制 count 个字节到 dest 所指向的对象。两个对象都被转译成 unsigned char 的数组。对象可以重叠:如同复制字符到临时数组,再从该数组到 dest 一般发生复制。

若出现 dest 数组末尾后的访问则行为未定义。若 destsrc 为非法或空指针则行为未定义。

参数:

dest - 指向复制目的对象的指针
src - 指向复制来源对象的指针
count - 要复制的字节数

返回值:

返回 dest 的副本,本质为更底层操作的临时内存地址,在实际操作中不建议直接使用此地址,操作完成以后,真正有意义的地址是dest本身。

注意:

memmove 可用于设置由分配函数获得的对象的有效类型

尽管说明了“如同”使用临时缓冲区,此函数的实际实现不会带来二次复制或额外内存的开销。常用方法( glibc 和 bsd libc )是若目标在源之前开始,则从缓冲区开始正向复制,否则从末尾反向复制,完全无重叠时回落到更高效的 memcpy

严格别名时用禁止检验同一内存为二个不同类型的值时,可使用 memmove 转换值。

重叠的含义
在这里插入图片描述
实现
#include<stdio.h>
#include<assert.h>

void* MyMemcpy(void* dest, const void* src, size_t count) {
    
    assert(dest != NULL && src != NULL);

    void* ret = dest;

    for (size_t i = 0; i < count; i++) {
        *(char*)dest = *(char*)src;// 逐字节复制
        dest = (char*)dest + 1;
        src = (char*)src + 1;
    }

    return dest;
}

void* MyMemmove(void* dest, const void* src, size_t count) {

    assert(dest != NULL && src != NULL);

    void* ret = dest;
    char* pdest = (char*)dest;
    char* psrc = (char*)src;
    
    // 先判断有没有重叠

    // 如果没有重叠,应该用更为高效的 memcpy
    if (dest >= (char*)src + count || src >= dest) {
        MyMemcpy(dest, src, count);
    }
    else {
        // 分别指向 dest 和 src 的最后一个字符
        pdest = pdest + count - 1;
        psrc = psrc + count - 1;
        
        while (count--) {
            *pdest = *psrc;
            pdest--;
            psrc--;
        }
        return dest;
    }
}


int main(void) {

    char str[] = "123456789";
    puts(str + 1);
    MyMemcpy(str + 1, str, 3);
    puts(str + 1);
    MyMemcpy(str, "123456789", sizeof(str));
    MyMemmove(str + 1, str, 3);
    puts(str + 1);
    
    return 0;
}
//输出:
23456789
11156789
12356789

5. memcmp

int memcmp( const void* lhs, const void* rhs, size_t count );

参数:

lhs, rhs - 指向要比较的对象的指针
count - 要检验的字节数

返回值:

lhs 以字典序出现前于 rhs 则为负值。

lhsrhs 比较相等,或 count 为零则为零。

lhs 以字典序出现后于 rhs 则为正值。

注意:

此函数读取对象表示,而非对象值,而且典型地只对字节数组有意义:结构体可以含有填充字节而其值不确定,存储于联合体最近存储成员后的任何字节的值是不确定的,且一个类型可以对相同值拥有二种或多种表示(对于 +0 和 -0 或 +0.0 和 –0.0 的相异编码、类型中不确定填充位)。

简单的应用:

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

int main(void) {

    int arr1[] = { 0, 1, 2, 3 };
    int arr2[] = { 0, 1, 2, 3 };
    int arr3[] = { -0, 1, 2, 3 };

    if(memcmp(arr1, arr2, sizeof arr1) == 0){
        printf("arr1 == arr2\n");
    }
    else {
        printf("arr1 != arr2\n");
    }

    if (memcmp(arr1, arr3, sizeof arr1) == 0) {
        printf("arr1 == arr3\n");
    }
    else {
        printf("arr1 != arr3\n");
    }
    

    return 0;
}

6. memset

void *memset( void *dest, int ch, size_t count );

定义于头文件 <string.h>

复制值 ch (如同以 (unsigned char)ch 转换到 unsigned char 后)到 dest 所指向对象的首 count 个字节。

若出现 dest 数组结尾后的访问则行为未定义。若 dest 为空指针则行为未定义。

参数:

dest - 指向要填充的对象的指针
ch - 填充字节
count - 要填充的字节数

返回值:

  1. dest 的副本,本质为更底层操作的临时内存地址,在实际操作中不建议直接使用此地址,操作完成以后,真正有意义的地址是dest本身。

简单应用:

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

int main(void) {

    char str[] = "Hello World";

    memset(str, '\0', sizeof str);

    puts(str);

    return 0;
}

在 Github 上看更全的目录:

https://github.com/hairrrrr/C-CrashCourse

以后的这个系列的代码都会上传上去,欢迎 star


以上就是本次的内容。

如果文章有错误欢迎指正和补充,感谢!

最后,如果你还有什么问题或者想知道到的,可以在评论区告诉我呦,我可以在后面的文章加上你们的真知灼见​​。

关注我,看更多干货!

我是程序圆,我们下次再见。

上一篇下一篇

猜你喜欢

热点阅读