C/C++ memcpy的实现

简介: memcpy作为常用函数,想必相当一部分程序员都在用的极其方便的时候却从未关注过它的具体实现,平日自然也没什么问题。但对于算法能力薄弱或者不喜欢刷题的同学来说,一旦遇见需要亲自手写的情况,突然碰见非常容易措手不及。

引言

 memcpy作为常用函数,想必相当一部分程序员都在用的极其方便的时候却从未关注过它的具体实现,平日自然也没什么问题。但对于算法能力薄弱或者不喜欢刷题的同学来说,一旦遇见需要亲自手写的情况,突然碰见非常容易措手不及。一边悔恨深平日对夯实基础的重要度认识不够,一边手忙脚乱,记录一次临场,嗯算是处理问题的过程吧。

常规思路

 最开始理解的坑:有源地址的数据类型,源地址不应该被改变,加上了const;要考虑一个字节一个字节搬移,所以要强制转化char*;参数的检查也能代表一个好习惯;通过tmp来代替dest。似乎一般情况都迎刃而解了?

void* memcpy_test(void* dest,const void* src,unsigned char length)
{
  assert(dest != NULL);
  assert(src != NULL);
  void* tmp = dest;
  while (length--)
  {
  *(char*)dest = *(char*)src;
  ((char*)dest) ++;
  ((char*)src) --;
  }
  return tmp;
}

 但很明显,这是忽略掉了所有的特殊情况,越界啊,overlap的情况也没有考虑到,在提示下画了画内存覆盖的情况,

overlap

 那么就需要增加判断条件,那么临界条件便是dest地址和src地址重合时和src+length重合的时候了

void* memcpy_test(void* dest,const void* src,size_t length)
{
  assert(dest != NULL);
  assert(src != NULL);
  void* tmp = dest;
  if ((dest <= src) || ((char*)dest >= (char*)src + length))
  {
  while (length--)
  {
    *(char*)dest = *(char*)src;
    ((char*)dest)++;
    ((char*)src)++;
  }
  }
  else
  {
  while (length > 0)
  {
    *((char*)dest + length - 1) = *((char*)src + length - 1);
    length--;
  }
  }
  return tmp;
}

 刚开始感觉等于的情况是表示dst和src区域没有重叠的情况,细想也无所谓。因为这种情况即使重叠也不会影响复制。如下图中所示,当dst复制到第4第5字节时已经覆盖了src的开始第1第2字节,但是并不影响复制。

 下图则表示的是dst > src || (char*)dst < ((char*)src + count)的情况,从后往前复制也不会影响复制结果。

2e722f05d82f4067a7dd6b3122760690.png

 复盘时参考标准库函数,想起来还有细节没有注意到,为什么最开始习惯性用u8当做长度了呢,那才多大啊

 此外,顾头不顾腚的毛病还是存在,刚开始还能想起来要强转char型,考虑新情况时编译器不警告就什么也想不起来,还是要养成好习惯啊

 同时,有常用gcc编译器的同学警告了地址自增时会报错,本身就是不可修改的左值,就算忽略编译的检查,以下图为例单说( *pb) ++等价转换也应该为a[0]++

int main()
int a[4]; int* pa; int* pb; int i;
printf("\n通过数组a来直接打印其中的元素值\n"); for(i = 0;i<4;i++)
a[i] = rand()% 100;
printf("a[%d]=%d\t",i,a[i]);
}
pa = a;
printf("\n通过int型指针pa来间接打印数组a中的元素值\n"); for (i = 0; i<4; i++)
printf("a[%d]=%d\t",i,*pa++);
}
pb = a;
printf("\n通过int型指针pa来间接打印数组a中的元素值\n"); for (i = 0; i<4;i++)
printf("a[%d]=%d\t",i,(*pb)++);
}
return
Hx--1024
通过数组a来直接打印其中的元素值 a[0]=83 a[1]=86 a[2]=77 a[3]=15
通过int型指针pa来间接打印数组a中的元素值 a[0]=83 a[1]=86 a[2]=77 a[3]=15
通过int型指针pa来间接打印数组a中的元素值 a[0]=83 a[1]=84 a[2]=85 a[3]=86 book@wwvba

 牟海军著的c语言进阶一书中5.3章节记录了这一要点

e75358691a884b67ab5b86515db21189.png

 为了使代码表现的更为清晰,同时避免不同编译器之间的优化带来的阴差阳错,就有了下面的代码

void* memcpy_test(void* dest,const void* src, size_t length)
{
  assert(dest != NULL);
  assert(src != NULL);
  char* tmpdest = (char*)dest;
  char* tmpsrc = (char*)src;
  if ((tmpdest <= tmpsrc) || (tmpdest >= tmpsrc + length))
  {
  while (length--)
  {
    *tmpdest++ = *tmpsrc++;
  }
  }
  else
  {
  tmpdest = tmpdest + length - 1;
  tmpsrc = tmpsrc + length - 1;
  while (length--)
  {
    *tmpdest-- = *tmpsrc--;
  }
  }
  return dest;
}

size_t

 提到size_t,可能许多人对这个类型不太熟悉没有用过或者根本不敢去用,看到一篇文章对这个类型讲的比较详细,便翻译过来让不熟悉的同学可以知道它产生的原因以及如何使用。  

 原文地址: http://web.archive.org/web/20101209143037/http://www.eetimes.com/discussion/programming-pointers/4026076/Why-size-t-matters?

前言:

 使用size_t可能会提高代码的可移植性、有效性或者可读性,或许同时提高这三者。  

 在标准C库中的许多函数使用的参数或者返回值都是表示的用字节表示的对象大小,比如说malloc(n) 函数的参数n指明了需要申请的空间大小,还有memcpy(s1, s2, n)的最后一个参数,表明需要复制的内存大小,strlen(s)函数的返回值表明了以’\0’结尾的字符串的长度(不包括’\0’),其返回值并不是该字符串的实际长度,因为要去掉’\0’。 

 或许你会认为这些参数或者返回值应该被申明为int类型(或者long或者unsigned),但是事实上并不是。C标准中将他们定义为size_t。标准中记载malloc的申明应该出现在,定义为:

void *malloc(size_t n); 

 memcpy和strlen的申明应该出现在中:

void *memcpy(void *s1, void const *s2, size_t n);
size_t strlen(char const *s); 

 size_t还经常出现在C++标准库中,此外,C++库中经常会使用一个相似的类型size_type,用的可能比size_t还要多。  

 据我所知,大部分的C和C++程序员害怕这些库使用size_t,因为他们不知道size_t代表什么或者为什么这些库需要使用它,归根结底,原因在于他们什么时候什么地方需要用到它。

可移植性问题

 早期的C语言(由Brian Kernighan 和 Dennis Ritchie 在The C Programming Language书中所写,Prentice-Hall, 1978)并没有提供size_t类型,C标准委员会为了解决移植性问题将size_t引入,举例如下:  

 让我们来写一个可移植的标准memcpy函数,我们将会看到一些不同的申明和它们在不同平台不同大小的地址空间上编译下的情况。 

 回忆memcpy(s1, s2, n)函数,它将s2指向地址开始的n个字节拷贝到s2指向的地址,返回s1,这个函数可以拷贝任何数据类型,所以参数和返回值的类型应该为可以指向任何类型的void*,同时,源地址不应该被改变,所以第二个参数s2类型应该为const void*,这些都不是问题。 

 真正的问题在于我们如何申明第三个参数,它代表了源对象的大小,我相信大部分程序员都会选择int:

void *memcpy(void *s1, void const *s2, int n);  

 使用int类型在大部分情况下都是可以的,但是并不是所有情况下都可以。int是有符号的,它可以表示负数,但是,大小不可能是复数。所以我们可以使用unsigned int代替它让第三个参数表示的范围更大。

 在大部分机器上,unsigned int的最大值要比int的最大值大两倍,比如说再也给16位的机器上,unsigned int的最大值为65535,int的最大值为32767。  

 尽管int类型的大小依赖于C编译器的实现,但是在给定的平台上int对象的大小和unsigned int对象的大小是一样的。因此,使用unsigned int修饰第三个参数的代价与int是相同的:

void *memcpy(void *s1, void const *s2, unsigned int n);  

 这样似乎没有问题了,unsigned int可以表示最大类型的对象大小了,这种情况只有在整形和指针类型具有相同大小的情况下,比如说在IP16中,整形和指针都占2个字节(16位),而在IP32上面,整形和指针都占4个字节(32位)。(参见下面C数据模型表示法)C数据模型表示法  

 最近,我偶然发现几篇文章,他们使用简明的标记来表述不同目标平台下c语言数据的实现。我还没有找到这个标记的来源,正式的语法,甚至连名字都没有,但他似乎很简单,即使没有正规的定义也可以很容易使用起来。这些标记的一边形式形如: 

 I nI L nL LL nLL P nP。   

 其中每个大写字母(或成对出现)代表一个C的数据类型,每一个对应的n是这个类型包含的位数。I代表int,L代表long,LL代表long long,以及P代表指针(指向数据,而不是函数)。每个字母和数字都是可选的。  

 例如,I16P32架构支持16位int和32位指针类型,没有指明是否支持long或者long long。如果两个连续的类型具有相同的大小,通常省略第一个数字。例如,你可以将I16L32P32写为I16LP32,这是一个支持16位int,32位long,和32位指针的架构。  

 标记通常把字母分类在一起,所以可以按照其对应的数字升序排列。例如,IL32LL64P32表示支持32位int,32位long,64位long long和32位指针的架构;然而,通常写作ILP32LL64。  

 不幸的是,这种memcpy的申明在I16LP32架构上(整形是16-bit 长整形和指针类型时32-bits)显得不够用了,比如说摩托罗拉第一代处理器68000,在这种情况下,处理器可能拷贝的数据大于65535个字节,但是这个函数第三个参数n不能处理这么大的数据。 

 什么?你说很容易就可以改正?只需要把memcpy的第三个参数的类型修改一下:

void *memcpy(void *s1, void const *s2, unsigned long  n); 

 你可以在I16LP32目标架构上使用这个函数了,它可以处理更大的数据。而且在IP16和IP32平台上效果也还行,说明它确实给出了memcpy的一种移植性较好的申明。但是,在IP16平台上相比于使用unsigned int,你使用unsigned long可能会使你的代码运行效率大打折扣(代码量变大而且运行变慢)。 

 在标准C中规定,长整形(无论无符号或者有符号)至少占用32位,因此在IP16平台上支持标准C的话,那么它一定是IP16L32 平台。这些平台通常使用一对16位的字来实现32位的长整形。在这种情况下,移动一个长整形需要两条机器指令,每条移动一个16位的块。事实上,这个平台上的大部分的32位操作都需要至上两条指令。 

 因此,以可移植性为名将memcpy的第三个参数申明为unsigned long而降低某些平台的性能是我们所不希望看到的。使用size_t可以有效避免这种情况。  

size_t类型是一个类型定义,通常将一些无符号的整形定义为size_t,比如说unsigned int或者unsigned long,甚至unsigned long long。每一个标准C实现应该选择足够大的无符号整形来代表该平台上最大可能出现的对象大小。使用size_t  

size_t的定义在<stddef.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>和<wchar.h>这些标准C头文件中,也出现在相应的C++头文件, 等等中,你应该在你的头文件中至少包含一个这样的头文件在使用size_t之前。   

 包含以上任何C头文件(由C或C++编译的程序)表明将size_t作为全局关键字。包含以上任何C++头文件(当你只能在C++中做某种操作时)表明将size_t作为std命名空间的成员。   

 根据定义,size_t是sizeof关键字(注:sizeof是关键字,并非运算符)运算结果的类型。所以,应当通过适当的方式声明n来完成赋值:n = sizeof(thing);  

考虑到可移植性和程序效率,n应该被申明为size_t类型。类似的,下面的foo函数的参数也应当被申明为sizeof:foo(sizeof(thing));  

 参数中带有size_t的函数通常会含有局部变量用来对数组的大小或者索引进行计算,在这种情况下,size_t是个不错的选择。   

 适当地使用size_t还会使你的代码变得如同自带文档。当你看到一个对象声明为size_t类型,你马上就知道它代表字节大小或数组索引,而不是错误代码或者是一个普通的算术值。


相关文章
|
7月前
|
编译器 C++ Windows
【C++】vector问题解决(非法的间接寻址,迭代器失效 , memcpy拷贝问题)
不使用memcpy函数不就可以了,然后我们使用简单粗暴的赋值拷贝,这样就不会发生浅拷贝问题了!!!
150 1
|
存储 Cloud Native Linux
C++ strcpy、sprintf与memcpy的区别
C++ strcpy、sprintf与memcpy的区别
|
7月前
|
编译器 C++
C++中memcpy函数的实现
C++中memcpy函数的实现
208 0
|
程序员 C++ 开发者
C/C++基础面试集锦(一)strcpy、memcpy
**一、strcpy函数实现方法** char* strcpy(char *dest, const char *src) { assert((dest != NULL) && (src != NULL)); char *DesSrc = dest; w...
1603 0
|
C++ 网络协议
C++执行内存memcpy的效率测试
在进行memcpy操作时,虽然是内存操作,但是仍然是耗一点点CPU的,今天测试了一下单线程中执行memcpy的效率,这个结果对于配置TCP epoll中的work thread 数量有指导意义。
1384 0
|
28天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
50 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
103 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
88 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
104 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4