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

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

思维导图


image

目录


@[toc]

正文


  • strlen & strlen_s
  • getchar() & putchar()
  • strcmp & strncmp()
  • strcpy() & strncpy()
  • strcat & strncat()

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


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

序 老朋友们

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


以上就是本次的内容。

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

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

关注我,看更多干货!

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容