内存函数
memmove
与memcpy
类似,但是可以处理源区域与目标区域有重叠的情况。虽然memcpy
在某些编译器下也可以处理重叠情况,但不是所有的编译器都可以。理论上可以用memmove
代替memcpy
。
在处理重叠的时候,根据源地址与目标地址的相对位置,决定拷贝顺序。当目标地址是低地址的是很好,拷贝顺序由前向后。如果源地址是低地址,那么顺序就是从后向前。
看一下代码实现:
void* my_memmove(void* dst, const void* src,size_t num)
{
assert(dst && src);
void* ret = dst;
if (dst < src)
{
while (num--)
{
*(char*)dst = *(char*)src;
dst = (char*)dst + 1;
src = (char*)src + 1;
}
}
else
{
while (num--)
{
*((char*)dst + num) = *((char*)src + num);
}
}
return ret;
}
memset
这个函数功能是设置内存中num个字节的空间改成目标值。例如可以将“hello bit”
改成“xxxxx bit”
。看个例子:
int main()
{
char arr[] = "hello bit";
//xxxxx bit
//memset(arr, 'x', 5);
memset(arr+6, 'x', 3);
//hello xxx
printf("%s\n", arr);
return 0;
}
memcmp
这个函数类似strncmp
,只不过是比较对象变成了内存中的数据。同样的,看个例子:
int main()
{
//char arr1[] = "abcdef";
//char arr2[] = "abqwertyuiop";
//printf("%d\n",memcmp(arr1, arr2, 3));
int arr1[] = { 1, 0x01234503, 3, 4, 5 };
int arr2[] = { 1, 3, 5, 7, 9 };
printf("%d\n", memcmp(arr1, arr2, 5));
return 0;
}
这里打印结果是0。
数据在内存中的存储
整型存储
整型编码
整型在内存中是以补码形式存储的。如何取得补码在之前的课程里已经有详细叙述,这里仅做个简单总结:
- 正数的原码、反码、补码相同;
- 负数的补码 = 原码取反+1
大小端字节序
有了补码的形式,那么如何将二进制码放入内存呢?这里介绍2种不同的存储方式:
- 大端:二进制的低位字节存入高地址,高位字节存入低地址;
- 小端:二进制的低位字节存入低地址,高位字节存入高地址;
举个例子:
假设需要存储一个数字:0x11223344
。
存储的起始地址假设为0x12FF40
。那么内存中的存储方式如下:
地址 | 大端存储 | 小端存储 |
---|---|---|
0x12FF40 | 11 | 44 |
0x12FF41 | 22 | 33 |
0x12FF42 | 33 | 22 |
0x12FF43 | 44 | 11 |
如何判断大小端呢?除了通过编译器来查看内存之外,还可以用代码:
int check_sys()
{
int a = 1;
return *(char*)(&a);//小端retrun 1 大端return 0;
}
int main()
{
if (check_sys())
printf("小端\n");
else
printf("大端\n");
return 0;
}
这里首先取a
的地址,强制转换成char*
之后再解引用,也就是将a
地址的第一个字节取出来看值了。参考上面的表格,如果是小端存储,那么a
地址里的第一个字节就是01
,否则是00
。这样就实现了大小端的判断。
练习
这里挑一些典型的题目做分析。
Prob 1
int main()
{
char a = -128;
//1000 0000
//整型提升:
//1111 1111 1111 1111 1111 1111 1000 0000
//
printf("%u\n", a);//42亿多 FFFF FF80
printf("%d\n", a);//-128
return 0;
}
这里分析一下。-128
是整数,那么就是以补码形式存在的。
1000 0000 0000 0000 0000 0000 1000 0000 --- -128 原码
1111 1111 1111 1111 1111 1111 0111 1111 --- -128 反码
1111 1111 1111 1111 1111 1111 1000 0000 --- -128 补码
但这里又是要放到char类型的变量a里,所以发生了截断。a实际得到的是:
1000 0000
打印的时候,会发生整型提升,整型提升会将1000 0000
提升为:
1111 1111 1111 1111 1111 1111 1000 0000
这个数字按照%u
打印 就是打印0xFFFFFF80
,是一个很大的正数。如果按照%d
打印,发现和-128
补码一样,那么就是-128
。
Prob 2
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));//255
return 0;
}
分析一下。仔细看代码,可以知道a[0] = -1, a[1] = -2...a[127] = -128
, 那么a[128] = 127,a[129] = 126..
.直到a[255] = 0
。strlen
返回的是\0
之前的字符个数,\0
是在a[255]
这里,那么之前一共就是255个字符。因此打印的是255
.
Prob 3
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);//转换为int,+1之后走1个字节,再强转成int*
printf("%x,%x", ptr1[-1], *ptr2);//*(ptr1 -1),
return 0;
}
分析一下,ptr1
指向的是4
之后的位置,那么ptr[-1] == *(ptr -1)
, 因此是4
;
ptr2
的话是a
(这里是个地址)转成整型后+1
,并没有跳过4
个字节而是跳过1
个字节。根据上面的小端存储,可以知道这个数组在内存中是这样的(地址从低到高):
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
那么此时,ptr2指向的内存空间(4个字节,ptr2是int*)里存的就是:
00 00 00 02
以%x
打印,就是打印0x2000000
atoi的实现
库函数atoi会将字符形式的数字转换为整型。它是这样工作的:
- 跳过字符串中的空白字符,直到碰到正负号或者数字;
- 将数字转换,直到下一个非数字的字符出现;
- 如果字符串不能转换,返回0;
- 如果字符串代表的数字溢出,根据正负输出整型的最大值或是最小值。
思路很简单,按照上面的要求即可。下面是代码:
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <limits.h>
int my_atoi(char* str)
{
int ret = 0;
int flag = 1;
assert(str);
//跳过空格和前置的0
while (isspace(*str))
{
str++;
}
//确认是+或者-
if (*str == '+')
{
str++;
}
else if (*str == '-')
{
str++;
flag = -1;
}
else if (*str< '0' || *str>'9')
return 0;
else
;
while (isdigit(*str))
{
ret = ret * 10 + *str - '0';
str++;
if (ret < 0 && flag == 1)
{
return INT_MAX;
}
else if (ret < 0 && flag == -1)
{
return INT_MIN;
}
}
return ret*flag;
}
int main()
{
char arr1[] = "-0+102345";
char arr2[] = "00102345";
char arr3[] = "-00102345";
char arr4[] = "2147483648";
char arr5[] = "-2147483649";
printf("%d %d %d %d %d\n", my_atoi(arr1), my_atoi(arr2), my_atoi(arr3), my_atoi(arr4), my_atoi(arr5));
printf("%d %d %d %d %d\n", atoi(arr1), atoi(arr2), atoi(arr3), atoi(arr4), atoi(arr5));
return 0;
}
有几个要点分析一下:
首先是转换语句:ret = ret * 10 + *str - '0';
这里注意数字字符-‘0’之后才是整数。
其次是溢出判断部分:最好不要出现数字,直接调用库内的值,否则可读性很差。另外,如果将负溢出的部分代码改为:
else if (ret < 0 && flag == -1)
{
ret = 2147483648;
break;
}
也是可以正常工作的。但可以看到,ret此时的值是溢出了,实际存储的时候ret会按照无符号处理,存储时发生截断,最后返回正好是加回负号。这样是不会报错的。但如果改成:
else if (ret < 0 && flag == -1)
{
ret = -2147483648;
break;
}
是会报错的。依据上面的说法,ret赋值时按照无符号处理,然后强行加了负号,所以会报错。经过和老师讨论,改成了上面的实现方法。