之前我们已经了解了字符串函数,并且已经会使用字符串函数对字符串进行一些操作。但是,当我们想要对其他类型的数组进行例如拷贝、比较等的操作时,字符串函数就不能实现了。
在这篇文章中就来了解一下可以对任何类型的数组进行操作的内存函数:
memcpy是内存拷贝函数,它可以实现指定内存大小的拷贝。
我们可以查询到这个 函数的声明:
库函数memcpy在头文件string.h中被声明。
它有三个参数:
第一个参数类型是void*,用来接收目的地的首地址;
第二个参数类型是const void*,用来接收源头内容的首地址(拷贝时源头的内容不会被改变,用const修饰会更安全);
第三个参数类型是size_t(无符号整型),表示从源头地址开始向后需要拷贝的字节数。
在之前使用qsort函数时我们了解到:void*可以接收任意类型的指针变量,指向1个字节的空间。由于这个库函数要能够拷贝任意类型的数据,所以这里使用了void*来接受指针变量(上一篇介绍的字符串函数的参数是char*型的,所以只能对字符进行拷贝)
结合第三个参数,就可以实现将从源头首元素地址开始的内存中num个字节内的数据拷贝到目的地首元素地址开始的num个字节的空间中。由于数据都是以二进制的形式存储在内存中的,最小存储单元就是1个字节,所以只要能够将某一内存空间的每一个字节拷贝到另一空间。这样,就可以实现任意数据类型的拷贝。
返回值是void*型的,用来返回目的地的首元素地址。
需要注意的是,memcpy函数在处理从源头地址开始的num个字节的内存空间与从目的地地址开始的num个字节的内存空间有重叠的情况时,可能会出现一些问题导致不能实现正确的拷贝。建议使用memmove函数来处理(马上就会介绍)。
例如:
#include
#include
int main()
{
int nums1[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int nums2[10] = { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
int* ret = memcpy(nums1, nums2, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", ret[i]);
}
}
在这段代码中:
数组名nums1与nums2是首元素地址。将这两个首元素地址作为第一与第二个参数,常数20作为第三个参数。表示将从nums2指向位置开始的20个字节拷贝到从nums1指向位置开始的20个字节。
假设是小端存储,数组nums1与nums2在内存中存储如图示:
将从源头指针指向的内容开始的20个字节拷贝到从目的地指针指向内容开始20的个字节后为:
再返回目的地指针即可。
再用整型指针ret接收返回的目的地指针,并用for循环打印数组。即11,12,13,14,15,6,7,8,9,10。
在了解了memcpy的使用之后,我们就可以尝试实现my_memcpy函数:
void* my_memcpy(void* e1, const void* e2,int n)
{
assert(e1&&e2);
void* ret = e1;
while (n)
{
*((char*)e1)++ = *((char*)e2)++;
n--;
}
return ret;
}
在这段代码中:
首先assert断言e1与e2的有效性。
由于后面目的地指针会自增,所以先创建一个void*性型的指针变量ret记录目的地指针,方便返回值。
然后while循环:将参数n作为判断条件,每次循环n–,共循环n次。循环内部先将void* 的指针强制类型转换为char*的指针,再后置++自增。void*类型虽然可以接收任意类型的指针变量,但是不能解引用,而强制类型转换为char*类型的指针后既能解引用,又能实现每次循环拷贝一个字节的数据。
最后返回ret,即开始记录的目的地指针。
使用my_memcpy:
#include
#include
void* my_memcpy(void* e1, const void* e2,int n)
{
assert(e1&&e2);
void* ret = e1;
while (n)
{
*((char*)e1)++ = *((char*)e2)++;
n--;
}
return ret;
}
int main()
{
int nums1[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int nums2[10] = { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
int* ret = my_memcpy(nums1, nums2, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", ret[i]);
}
}
输出结果为11、12、13、14、15、16、7、8、9、10。
前面提到的库函数memcpy可以实现内存数据的拷贝,但是在拷贝有重叠的两块内存时可能会出现一些不可预知的问题。由于vs环境对库函数memcpy进行了一些改进,所以无法展现这个问题,但是可以想象一下:
如果要将一个数组int nums[10]={1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
的前5个元素拷贝到下标为2到6的元素上。4遍循环将1拷贝到3的位置上去;再4遍循环将2拷贝到4的位置上去;再接下来的4遍循环我们应该将3拷贝到5的位置上去,但是此时原来3所在的内存中已经被替换为1;后面的4所在的内存中已经被替换为2。显然不能实现我们的目的。这时就可以使用memmove函数。
我们可以查询到这个函数的声明:
这个函数同样声明在头文件string.h中。
它的参数与返值与memcpy函数一致。
它可以实现将num个字节的值从源头地址指向的空间复制到目的地地址指向的空间,并且源空间与目的地空间可以有重叠。
memmove使用时也与memcpy函数相同:
#include
#include -
int main()
{
int nums[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ret = memmove(nums, nums+2, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", ret[i]);
}
}
我们将数组nums的首元素地址作为目的地地址;将数组第3个元素的地址作为源头地址;将常数20作为拷贝的字节数作为三个参数传给memmove,并用整形指针ret接收返回值。for循环打印,结果与我们的预期相符:3、4、5、6、7、6、7、8、9、10。
接下来我们可以尝试模拟实现memmove函数:
要实现memmove函数,首先要解决的就是如何解决源头空间与目的地空间重叠的情况。
我们当然可以创建一个缓冲数组:先从源头空间将num个字节的数据存到缓冲数组中,再从缓冲数组中将数据复制到目的地。但是这种方式不仅要再开辟一块空间,还要遍历两遍,效率不高。
这里介绍另一种方法:
我们发现,将对于上面的例子:将一个数组int nums[10]={1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
的前5个元素拷贝到下标为2到6的元素上。只要从后向前拷贝:先用4个循环将5拷贝到7的空间;4个循环将4拷贝到6的空间;4个循环将3拷贝到5的空间…这样就不会出现将要拷贝的值被替换。
同时,我们发现从后向前拷贝并不能解决所有有重叠部分的问题。比如要将下标为2到6的元素拷贝到前五个元素的位置上,从后向前拷贝时也会出现将要拷贝的值被替换的问题。这种情况下需要从前向后拷贝才能实现我们的目的。
至于没有重叠的情况,当然从后向前或是从前向后都没有影响。
总结就是:当目的地指针的位置在源头指针的前面时应使用从前向后拷贝的方式,否则采用从后向前拷贝的方式。
void* my_memmove(void* e1, const void* e2, int n)
{
assert(e1&&e2);
void* ret = e1;
if (e2 > e1)
{
while (n--)
{
*((char*)e1)++ = *((char*)e2)++;
}
}
else
{
while (n--)
{
*((char*)e1 + n) = *((char*)e2 + n);
}
}
return ret;
}
在这段代码中:
首先assert断言e1与e2的有效性。
由于后面目的地指针会自增,所以先创建一个void*性型的指针变量ret记录目的地指针,方便返回值。
接着判断,当目的地地址小于源头地址时,采用从前向后拷贝的方式;否者采用从后向前拷贝的方式:
从前向后拷贝时,while循环,判断部分用n–,表示循环n次。循环内部将void* 的指针强制类型转换为char*的指针,再后置++自增。实现了从e1与e2开始每次循环拷贝一个字节的数据的从前向后拷贝。
从后向前拷贝时,while循环,判断部分依旧用n–,表示循环n次。循环内部将void* 的指针强制类型转换为char*的指针,再对这个char*型的指针加上n,再解引用。每次n–,恰好实现了源头指针与目的地指针从加n-1到加0的从后向前的拷贝(由于后置++,在第一次循环时n就以及自减一次,加n后的指针指向的就是最后一个元素)。
最后返回ret。
使用如下:
#include
#include
void* my_memmove(void* e1, const void* e2, int n)
{
assert(e1&&e2);
void* ret = e1;
if (e2 > e1)
{
while (n--)
{
*((char*)e1)++ = *((char*)e2)++;
}
}
else
{
while (n--)
{
*((char*)e1 + n) = *((char*)e2 + n);
}
}
return ret;
}
int main()
{
int nums[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ret = memmove(nums, nums+2, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", ret[i]);
}
}
memcmp是内存比较函数。
我们可以查询到这个函数的声明:
这个库函数声明在头文件string.h中。
可以实现两个内存块中前num个字节的数据的比较。不同于strncmp,memcmp可以比较任意类型的数据。
它有3个参数:
前两个参数的类型是const void* (比较时不需要改变数据,用const修饰会更安全)。表示要进行比较的两个内存块的首字节的地址。
第三个参数的类型是size_t(无符号整型)。表示要比较两个内存块首字节地址指向的内容开始的多少个字节的数据。
返回值是int型的,当前一个内存块小于后一个时返回一个小于0的数;等于后一个时返回0;大于后一个时返回一个大于0的数(vs环境下返回-1、0、1)。
void*的指针可以接收任意类型的指针变量。这就使memcmp函数可以接收任何数据的地址进行比较。
例如:
#include
#include
int main()
{
int nums1[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int nums2[10] = { 1, 2, 3, 4, 5, 16, 17, 18, 19, 20 };
printf("%d\n", memcmp(nums1, nums2, 20));
}
在这段代码中,数组名nums1与nums2是首元素地址。
将这两个数组的首元素地址与常数20作为参数传给memcmp,表示比较从nums1与nums2指向空间开始的前20个字节(对于int型的数组来说就是5个元素)。都是1、2、3、4、5。
%d打印返回值,就是0。
在了解了库函数memcmp的使用后,我们就可以尝试实现一下my_memcpy:
int my_memcmp(const void* e1, const void* e2, int n)
{
assert(e1&&e2);
char* str1 = (char*)e1;
char* str2 = (char*)e2;
int i = 0;
while (*str1 == *str2&&i < n)
{
i++;
str1++;
str2++;
}
if (i == n)
{
return 0;
}
else
{
return *str1 - *str2;
}
}
在这段代码中:
首先assert断言e1与e2的有效性。
接着需要将void*的指针e1与e2强制类型转换为char*型。char*指向的空间大小为1个字节,强转为char*后就可以通过循环遍历两个内存块的每一个字节(前n个)。
接着while循环,变量i用于控制循环的次数,i
第二种情况下能够说明从str1指向的内存块的首字节开始的num个字节与从str2指向的内存块的首字节开始的num个字节都是相等的。于是我们在while循环后进行判断:
当满足第二种情况时返回0,其他情况返回*str1-*str2。
使用如下:
#include
#include
int my_memcmp(const void* e1, const void* e2, int n)
{
assert(e1&&e2);
char* str1 = (char*)e1;
char* str2 = (char*)e2;
int i = 0;
while (*str1 == *str2&&i < n)
{
i++;
str1++;
str2++;
}
if (i == n)
{
return 0;
}
else
{
return *str1 - *str2;
}
}
int main()
{
int nums1[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int nums2[10] = { 1, 2, 3, 4, 5, 16, 17, 18, 19, 20 };
printf("%d\n", my_memcmp(nums1, nums2, 24));//比较前24个字节
}
在比较前24个字节的内容时,nums1中第21个字节(或第24个字节,取决于大小端)存储的是06(十六进制);nums2中的第21个字节(或第24个字节)存储的是10(十六进制);这时跳出循环,返回06-10,再用有符号十进制数打印就是-10。
memset是内存设置函数。
我们可以查询到这个函数的声明:
这个函数在头文件string.h中声明。
可以实现将内存块的前num个字节设置为某个指定的值。
这个库函数有三个参数:
第一个参数的类型是void*。表示要设置的内存块的地址;
第二个参数的类型是int。表示指定的要设置的值;
第三个参数的类型是size_t(无符号整型)。表示再内存块由首地址开始中要将多少个字节设置为指定值。
返回值是void*的。表示这个内存块的地址。
虽然我们输入指定值的时候是int型的,但是当这个值被存到一个字节的空间的时候会被截断为一个无符号的char类型。所以在输入值的时候,需要在0到255之间,否则将会导致一些意料之外的结果。
并且,需要特别注意的是,在用memset设置内存块的时候,是以字节为单位的。盲目的使用memset也会造成意料之外的结果。
例如:
如果我们想将一个char类型的数组内容全部改为’1’。
#include
#include
int main()
{
char s[] = "hello word";
memset(s, '1', 10);
printf("%s\n", s);
}
数组名s是首元素地址。将字符指针s、字符’1’、常量10作为参数传给memset,表示将s指向的空间的前10个字节改为字符’1’。显然实现了我们的目的。
但是当我们想要对整型数组进行设置时,就会出现一些问题:
#include
#include
int main()
{
int nums[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
memset(nums, 1, sizeof(nums));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", nums[i]);
}
}
因为memset函数在设置内存块的单位是字节,上面的代码就相当于将这个整型数组nums的40个字节全部设置为1,就是:
这就相当于将这个整型数组每个元素的补码全部改为00000001 00000001 00000001 00000001。这个数用%d打印就是16843009。
所以在使用这个memset函数时一定要注意单位是字节!
接着,我们来实现一下my_memset:
void* my_memset(void* ptr, int v, int num)
{
assert(ptr);
char* p = (char*)ptr;
int i = 0;
for (i = 0; i < num; i++)
{
*(p + i) = v;
}
return p;
}
在这段代码中:
先assert断言ptr的有效性。
接着将ptr强制类型转换为char*,并赋值给一个字符指针p。 返回p。 使用如下: 到此,内存函数的介绍就结束了。 如果对本文有任何问题,欢迎在评论区进行讨论哦 希望与大家共同进步哦
再创建一个整型变量i用于循环判断。
for循环,每次循环i++。i#include
总结
看到这里,相信大家对于指针与内存有了更深一层的理解。