C语言常用库函数实现(一)_内存拷贝
库函数的实现也是面试中的常考题,因为这是最能体现C语言功底的。
一、strcpy与strncpy
先看一下函数的原型:
strcpy函数可以按如下的方式实现:
点击(此处)折叠或打开
- char * strcpy(char *strDest, const char *strSrc)
- {
- if (strDest == NULL)
- || (strSrc == NULL)
- printf("Input parameter invalid!\n");
- char *strDestCopy = strDest;
- while ((*strDest++ = *strSrc++) != '\0');
- return strDestCopy;
- }
关于这个库函数的使用还需做如下的说明:
1. 为什么要返回char *
答:为了实现链式表达式 e.g int length = strlen(strcpy(strDest, "Hello world!"));
2. 由于strcpy一直拷贝到‘\0’才结束,所以要注意以下几点:
首先是越界的问题,看如下的例子:
char buf[10];
strcpy(buf, "Hello world!");
这个例子提醒调用strcpy时一定要确保目标内存足够大
再看下面这个例子:
char buf[10] = "123456789"
char str[4] = "hell";
strcpy(buf, str);
由于str的空间不够,故其没有以'\0'结尾,则拷贝时一定会出现越界错误,这个例子告诉我们调用strcpy时要检查源拷贝字符串是否以'\0'结尾
总之,“确保不会写越界”是调用者的责任,调用者在调用之前一定要仔细检查源字符串与目标字符串是否以'\0'结尾,如果不注意这一点,strcpy函数很有可能发生“缓冲区溢出错误(buffer overflow)”,这一错误如果被恶意用户利用,那引起的问题就会非常严重,看下面的例子:
void foo(char *str)
{
char buf[10];
strcpy(buf, str);
.......
}
如果str指向的字符串超过10个字节,而像上面的代码所示没有任何检查工作,则会导致写越界,这样的写越界错误会覆盖保存在栈帧上的返回地址,使函数返回时跳转到非法地址,出现段错误,如果这样还算好,更严重的是如果恶意用户利用了这个Bug,使函数返回时跳转到一个事先设好的地址,执行事先设好的指令,如果设计得巧妙甚至可以启动一个Shell,然后随心所欲执行任何命令,可想而知,如果一个用root
权限执行的程序存在这样的Bug,被攻陷了,后果将很不堪设想,因此写代码时这个问题一定要给予充分重视。
3. src与dest所指向的内存空间不能有重叠(凡是有指针参数的C标准库函数都有这条要求)
char buf[10] = "Hello";
strcpy(buf, buf+1);
这样的代码是有问题的
Man Page中给出了strncpy的定义:
点击(此处)折叠或打开
- char * strncpy(char *dest, const char *src, size_t n)
- {
- size_t i;
- for (i = 0; i < n && src[i] != '\0'; i++)
- dest[i] = src[i];
- for ( ; i < n ; i++)
- dest[i] = '\0';
- return dest;
- }
从函数的定义可以知道,如果src
字符串全部拷完了不足n
个字节,那么还差多少个字节就补多少个'\0'
,但是函数并不保证dest
一定以'\0'
结束,当src
字符串的长度大于n
时,不但不补多余的'\0'
,连字符串的结尾'\0'
也不拷贝。
简单点说就是strnpy并不保证dest一定以'\0'结尾,因此我们使用时一定要特别注意,推荐下面的使用方式:
strncpy(buf, str, n);
if (n > 0)
buf[n-1] = '\0';
二、memcpy与memmove
还是先看一下这两个函数的原型,这两个函数仍然都包含在string.h头文件中:
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
这两个函数实现的功能都是拷贝从src所指向的内存位置,拷贝count字节的字符到dst所指向的内存位置,唯一的区别是当内存发生部分重叠时,memmove保证拷贝结果的正确性
还要说明以下两点:
1. 这两个函数不关心src与dst所指向内存的数据类型(所以指针为void型),仅对所指内存内容进行二进制拷贝
2. 这两个函数不检测字符串结束符'\0',等,仅做count字节的拷贝
事实上为了效率,这两个函数均是用汇编语言实现的,但是研究它们的实现过程是非常有趣的,这里就分别来看一下,首先是memcpy函数:
点击(此处)折叠或打开
- void* memcpy(void *dst, const void *src, size_t count)
- {
- //安全检查
- assert( (dst != NULL) && (src != NULL) );
- unsigned char *pdst = (unsigned char *)dst;
- const unsigned char *psrc = (const unsigned char *)src;
- //防止内存重复
- assert(!(psrc<=pdst && pdst<psrc+count));
- assert(!(pdst<=psrc && psrc<pdst+count));
- while(count--)
- {
- *pdst = *psrc;
- pdst++;
- psrc++;
- }
- return dst;
- }
第10句和第11句是为了检测内存覆盖,这里把它的含义大概说明一下,如果psrc <= pdst,说明psrc在低地址,pdst在高地址,则如果
pdst < psrc + count,说明两段内存是重叠的;同样的道理,pdst <= psrc, 说明pdst在低地址,psrc在高地址, 则psrc < pdst + count说明两段内存重叠了。
上面的程序实现了memcpy函数的基本功能,但事实上memcpy是一个高效的内存拷贝函数,它的内部实现并非是一个字节一个字节的拷贝,仅仅仅在地址不对齐的情况下,memcpy才会一个字节一个字节的拷贝内存内容,当地址对齐时,memcpy会使用CPU字长(32bit或64bit)来拷贝,而且还会根据CPU类型选择一些优化的指令。再看下面的优化代码:
点击(此处)折叠或打开
- void *mymemcpy(void *dst,const void *src,size_t num)
- {
- assert((dst!=NULL)&&(src!=NULL));
- assert(!(src <= dst && dst < src + num);
- assert(!(dst <= src && src < dst + num);
- int slice = num % 4;//首先按字节拷贝
- int wordnum = num/4;//计算有多少个32位,按4字节拷贝
- const int * pintsrc = (const int *)src;
- int * pintdst = (int *)dst;
- while (slice--)*((char *)pintdst++) =*((const char *)pintsrc++);
- while(wordnum--)*pintdst++ = *pintsrc++; //后面的地址应当是对齐的
- return dst;
- }
注意:要真实模拟系统的状况,必须是先拷贝零散的字节(slice长),因为不对齐的情况是由于这些零散字节的存在。
前面说过,memmove与memcpy相比,唯一的区别就是可以处理内存局部重叠的情况,我们于是看一下内存局部重叠的两种情况:
第一种情况下,拷贝重叠的区域不会出现问题,内容均可以正确的被拷贝。
第二种情况下,问题出现在右边的两个字节,这两个字节的原来的内容首先就被覆盖了,而且没有保存。所以接下来拷贝的时候,拷贝的是已经被覆盖的内容,显然这是有问题的。
有了这个直观的认识后,可以写出如下的代码:
点击(此处)折叠或打开
- void *mymemmove(void *dst, const void *src, size_t n)
- {
- char temp[n];
- int i;
- char *d = dst;
- const char *s = src;
- for (i = 0; i < n; i++)
- temp[i] = s[i];
- for (i = 0; i < n; i++)
- d[i] = temp[i];
- return dest;
- }
这段代码的思路非常简单,既然字节覆盖是因为原先的内容被覆盖造成的,那就把原先的内容先保存下来,所以开辟一段大小为n的内存空间,先把src所指向的内容保存下来,然后再拷贝到dst中。
下面这段实现是VC的源码:
点击(此处)折叠或打开
- void * __cdecl memmove ( void * dst, const void * src, size_t count )
- {
- void * ret = dst;
- if (dst <= src || (char *)dst >= ((char *)src + count)) {
- /*
- * Non-Overlapping Buffers
- * copy from lower addresses to higher addresses
- */
- while (count--) {
- *(char *)dst = *(char *)src;
- dst = (char *)dst + 1;
- src = (char *)src + 1;
- }
- }
- else {
- /*
- * Overlapping Buffers
- * copy from higher addresses to lower addresses
- */
- dst = (char *)dst + count - 1;
- src = (char *)src + count - 1;
- while (count--) {
- *(char *)dst = *(char *)src;
- dst = (char *)dst - 1;
- src = (char *)src - 1;
- }
- }
- return(ret);
- }
如果看注释,这段代码是很容易懂的,dst <= src(表明目标地址为低地址,源地址为高地址,对应于图中第1种情况), dst >= src + count(表明没有内存局部重叠),这种情况即使用memcpy拷贝结果也是不会有问题的,所以按字节拷贝即可。else语句对应于图中的第二种情况, 则从高地址向低地址拷贝,这样就规避了内容被覆盖的问题,也能得到正确的结果。
C语言常用库函数实现(一)_内存拷贝相关推荐
- c语言常用库函数使用要点,C语言常用库函数使用要点.doc
C语言常用库函数使用要点.doc C语言常用库函数使用要点 C语言常用库函数使用要点 李素萍太原理工大学阳泉学院信息系 C语言提供了丰富的库函数,只有有效地使 用库函数,才能学好,学活C语言.为了- ...
- 二级c语言常用,二级C语言常用库函数.doc
二级C语言常用库函数 二级C语言常用库函数 二级C语言中,常用库函数可参考下面表格,具体使用方法可参考文件<C标准库函数集.pdf>,更多函数的用法可参考文件<C标准库函数集速查.c ...
- c语言库函数说明,C语言常用库函数说明
C语言常用库函数说明 序号 库类别 头文件 详细说明 1 错误处理 errno.h 2 字符处理 ctype.h 3 地区化 local.h 4 数学函数 math.h 5 信号处理 signal.h ...
- C语言常用库函数总结
1 C语言常用计算的库函数 下面的所有用例的参数赋值: int a = 2;int b = 4;int val = 0; 计算类函数声明在头文件#include<math.h>中: 1.1 ...
- c语言内存复制函数,【C语言】 字符串操作函数及内存拷贝函数归总
今天在这里把零散的一些常用的字符串操作函数和内存拷贝函数进行一下归总实现. 一 . 字符串操作函数 字符串操作函数有很多,这里我列举一些常用的函数,以及自实现的代码: 字符串拷贝函数: 函数原型:ch ...
- [C语言]常用库函数
字符函数和字符串函数 求字符串长度 size_t strlen ( const char * str ); 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字 ...
- c语言常用库函数使用方法,c语言常用库函数使用方法及用途
absread()读磁盘绝对扇区函数 原形:int absread(int drive,int num,int sectnum,void *buf) 功能:从drive指定的驱动器磁盘上,sectnu ...
- 三、C语言常用的库函数
1. 函数是什么? 维基百科中对函数的定义:子程序.子程序,是一个大型程序中的某部分代码, 由一个或多个语句块组成.它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性.一般会有输入参数并有 ...
- 计算机二级c常用函数,计算机等级考试二级C语言的常用库函数试题
计算机等级考试二级C语言的常用库函数试题 计算机等级考试二级C语言的常用库函数试题 settextstyle()显示字符的当前设置函数 功能:设置当前输出英文字符的字体.大小和方向. 调用方式: vo ...
最新文章
- 吴恩达老师深度学习视频课笔记:自然语言处理与词嵌入
- AXI总线基本知识:(基于uart_lite IP核)
- 美团今年应届生年薪 35w+?为什么互联网大厂校招的薪资一年比一年高?
- 可由一个尾指针唯一确定的链表有_L2数据结构第08课 单向链表和循环链表
- [python调试笔记] 编译运行h5py
- 配置nginx到后端服务器负载均衡
- MOS2010的界面介绍和定制方法简介【资料汇集】
- Web开发框架–第2部分:Play Framework 2.0
- vue 微信支付的坑_vue_模式下h5微信支付
- android自定义底部中间突出导航栏,Android选中突出背景效果的底部导航栏功能
- kibana报错:No default index pattern. You must select or create one to continue.
- C机顶盒开发实战常量定义方式、结构定义方式(可理解为对象Model)
- php mysql敏感词_PHP敏感词处理
- 微信公众号网页授权并获取用户信息简要流程
- c#Winform自定义控件-目录
- Python IDE开发环境
- 【LDA】动手实现LDA
- 瑞丰银行近日IPO过会,3年前曾被证监会取消审核
- 所谓的CDN动态加速技术
- 台式计算机开机不自检不起动,开机主板不自检显示器一直黑屏
热门文章
- 钉钉小程序从搭建到引入地三方库到发布---企业内小程序
- 关于上传文件的报错问题
- torch.isfinite()、torch.isinf()、torch.isnan()
- (转)如何快速学习Tableau Desktop
- win10重启电脑Java环境变量丢失问题
- 14.Java- Spring
- Echarts绘制Tree树图的涟漪效果effectScatter
- Sekiro(只狼)
- 再论VS.NET中的安装部署文件
- Perf -- Linux下的系统性能调优工具,第 2 部分