JoeChenzzz

导航

c语言中几个常见的库函数strlen、strcmp、strcat、strcpy、strncpy、memset、memcpy、memmove、mmap

1.strlen()

1)计算给定字符串的长度,不包括’\0’在内

unsigned int strlen(const char *s)
{
    assert(NULL != s);//如果条件不满足,则终止程序
    unsigned int length = 0;
    while (*s++ != '\0')
        ++length;
    return length;
}

2.strcmp()

1)比较两个字符串,若str1、str2字符串相等,则返回零;若str1大于str2,则返回正数;否则,则返回负数

int strcmp(const char *str1, const char *str2)
{
    assert(NULL != str1 && NULL != str2);
    while(*str1 && *str2 && *str1 == *str2)
    {
        ++str1;
        ++str2;
    }
    return *str1 - *str2;
    //若相等,则*str1 - *str2 = '\0' - '\0' = 0;否则,*str1 - *str2 != 0;因为前面的位都相等,所以只需要比较当前位来确定返回值
}

3.strcat()

1)将src所指向的字符串添加到dest结尾处(会覆盖dest结尾处的'\0'

2)src和dest所指内存区域不可以重叠且dest必须有足够的空间(sizeof(dest)-strlen(dest)必须>=strlen(src)+1)来容纳src的字符串

char* strcat(char* dest, const char* src)
{
    assert(NULL != dest && NULL != src);//如果条件不满足,则终止程序
    char* temp = dest;
    while ('\0' != *temp)//while循环出来temp指向原字符串的NULL空字符的位置
        ++temp;
    while (*src != '\0')
        *temp++ = *src++;
    return dest;
}

4.strcpy()

1)把字符串src(包括'\0')复制到dest

2)src和dest所指内存区域不可以重叠且dest必须有足够的空间(sizeof(dest)>=strlen(src)+1)来容纳src的字符串

3)返回值char*而不是void,实现了链式表达(就是让调用strcpy的时候可以方便一些,在调用的时候可以一连串(链式)写下来)

char* strcpy(char* dest, const char *src)
{
    assert(NULL != dest && NULL != src);
    char * temp = dest;
    while (*src != '\0')
        *temp++ = *src++;
    return dest;
}

5.strncpy()

1)将以字符串src所指向的地址开始的前n个字节复制到dest中,并返回dest

2)如果src的前n个字符不含NULL结束符,则dest不会以NULL字符结束

3)如果n>strlen(src)+1,则以'\0'填充dest,直到复制完n个字节

4)src和dest所指内存区域不可以重叠且dest必须有足够的空间(sizeof(dest)>=strlen(src)+1)来容纳src的字符串

5)dest和n应该满足:sizeof(dest)>=n

char* strncpy(char* dest, const char* src, unsigned int n)
{
    assert(NULL != dest && NULL != src);
    char* temp = dest;
    while (n > 0 && *src != '\0')
    {
        *temp++ = *src++;
        --n;
    }
    if (n > 0)
    {
        *temp = '\0';
    }
    return dest;
}

6.memset()

1)将以s所指向的地址开始的前n个字节用ch替换,并返回s

2)常用于对较大的数组进行清零操作

void* memset(void* s, int ch, unsigned int n)
{
    assert(NULL != s);
    char* temp = s; //VS2017中这里会报错,gcc不会
    while (n > 0)
    {
        *temp++ = (char)ch; //将int转化为char,截去高24位,只保留低8位
        --n;
    }
    return s;
}

3)注意:不能把memset当作万能的初始化工具,应该只把memset当作清0的工具,因为memset是以字节(8bits)为单位用第二个参数ch进行赋值的

int main()
{
    int dest[2];//这里是int
    memset(dest, 1, sizeof(dest));
    for (int i = 0; i < sizeof(dest) / sizeof(int); ++i)
        printf("%d\n", dest[i]);

    return 0;
}
程序输出:
16843009
16843009

int main()
{
    int dest[2];//这里是int
    memset(dest, 0, sizeof(dest));
    for (int i = 0; i < sizeof(dest) / sizeof(int); ++i)
        printf("%d\n", dest[i]);

    return 0;
}
程序输出:
0
0

程序解读:

  1)1的二进制 : 00000000000000000000000000000001(32位),在memset源码中1将强制类型转换成char(1字节占8位)00000001(8位)

  2)memset函数传入的是sizeof(dest),也就是8(字节),所以n=8,进行8次while循环

  3)temp的类型是char*,所以会8位8位地去赋值

  4)所以while循环结束,从起始地址开始的值为:0000000100000001000000010000000100000001000000010000000100000001(64位)

  5)memset()调用结束后,dest的每个元素按照int类型读取(按32位读取),所以dest[0]=00000001000000010000000100000001(32位)=16843009,dest[1]=00000001000000010000000100000001(32位)=16843009

7.memcpy()

1)将以src所指向的地址开始的前n个字节的任意内容(不仅限于字符串)到拷贝到dest

2)src和dest所指内存区域不可以重叠,否则拷贝出来的数据将是错误的

void* memcpy(void* dest, const void* src, unsigned int n)
{
    assert(NULL != dest && NULL != src);
    void* temp = dest;
    for (int i = 0; i < n; ++i)
        *((char*)temp + i) = *((char*)src + i); //1个字节1个字节地拷贝,未知类型,不能++
    return dest;
}

8.memmove()

1)memmove也是将以src所指向的地址开始的前n个字节的任意内容(不仅限于字符串)到拷贝到dest,但是可以它可以处理src和dest所指内存区域存在重叠的情况(src<dest<src+n)

2)memmove这个函数名称中有"move"这个单词,但实际上并不是"移动"了。这个函数名称有它的历史原因:因为有了memcpy函数后,发现这个函数在使用时容易出现问题,于是又发明了一个能够处理重叠的memcpy函数,但为了保证兼容性依然保留了memcpy函数

void* memmove(void* dest, const void* src, unsigned int n)
{
    assert(NULL != dest && NULL != src);
    void* temp = dest;
    if (dest > src && dest < src + n)//内存重叠
    {
        for (int i = n - 1; i >= 0; --i)//从高地址向低地址拷贝
            *((char*)temp + i) = *((char*)src + i);
    }
    else//内存不重叠
    {
        for (int i = 0; i < n; ++i)
            *((char*)temp + i) = *((char*)src + i);
    }
    return dest;
}

9.mmap()

  转载自:https://blog.csdn.net/qq_36675830/article/details/79283113

9.1mmap()

1)mmap是一种内存映射的方法,即将一个磁盘文件映射到进程的虚拟地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应

2)内存映射的好处:实现这样的映射关系后,进程一旦读写操作这一段内存,系统会自动回写到对应的磁盘文件上,这样一来,对文件的操作就不必再调用read、write等系统调用函数,效率会很高

3)mmap()的函数原型

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
其中:
addr:映射区起始地址,通常设为NULL,由系统指定。
length:将文件的多大长度映射到内存
prot:映射区的保护方式,可以是:
    PROT_EXEC:映射区可被执行
    PROT_READ:映射区可被读取
    PROT_WRITE:映射区可被写入
    PROT_NONE:映射区不能存取
flag:映射区的特性,可以是:
    MAP_SHARD:对映射区的写入数据会复制回文件,且允许其他映射该文件的进程共享
    MAP_PRIVATE:对映射区域的写入数据会产生一个映射的复制(copy - on - write),对此区域所做的修改不会写回到原文件
    其他标识这里就不再写出来了,可以通过man mmap查看
fd:由open返回的文件描述符,表示要映射的文件
offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为0, 表示从文件头开始映射
*/

4)vm_area_struct结构:linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问

5)在用户层创建虚拟地址空间中的映射区域

  • mmap()在内存映射段寻找一段空闲的满足要求的连续的区域,为此区域分配一个vm_area_struct结构,接着对这个结构进行初始化
  • 将新建的vm_area_struct结构插入进程的虚拟地址区域链表或树中 

6)从用户层进入内核层,调用内核函数mmap()(不同于用户空间库函数)完成文件物理地址和进程虚拟地址的一一映射关系

  • 内核函数mmap(),其原型为:
int mmap(struct file* filp, struct vm_area_struct* vma);

7)注意:mmap只是创建了建立文件磁盘地址和虚拟内存区域的映射关系,没有任何文件拷贝操作

9.2常规文件的读写和内存映射段读写的区别

1)进程对常规文件的读写过程:

  • 进程调用read()函数发起读文件的请求
  • 内核通过查找进程文件符表,从而找到此文件的inode
  • 通过inode检查次文件页是否已经缓存在物理页中,如果已缓存,则直接返回物理页上的内容
  • 如果未缓存,即发生缺页,于是进行页面置换,将数据从磁盘复制到物理页上缓存
  • 由于页缓存处在内核空间不能被用户进程直接寻址,所以还需要将页缓存中数据再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取

2)进程对建立了内存映射的文件的读写过程:

  • 进程的读或写操作访问虚拟地址空间的映射地址,通过查询页表,发现这虚拟页并未被缓存,引发缺页异常。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到物理内存中,即没有被缓存,因此引发缺页异常
  • 然后内核利用已经建立好的映射关系,将磁盘文件直接拷贝至位于用户空间的内存映射段上,于是进程就可以直接读取文件内容了
  • 如果进程的写操作改变了其内容,一定延迟后系统会自动回写对应磁盘地址,也即完成了写入到文件的过程,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了

3)区别:常规文件操作需要从磁盘到内核空间的页缓存再到用户空间的两次数据拷贝;而读写进行了内存映射的文件,只需要从磁盘到用户空间的一次数据拷贝,因此内存映射段效率很高

posted on 2019-03-22 08:16  JoeChenzzz  阅读(1117)  评论(0编辑  收藏  举报