前言
动态开辟…灵活!
1.内存函数的模拟
1.1 memcpy
把源空间对应内存的全部数据拷贝到目标空间
实践起来就是 按字节拷贝
void* MyMemcpy(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[20] = { 0 };
int arr2[] = { 1,2,3,4,5 };
MyMemcpy(arr1, arr2, 5 * sizeof(int));
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
不妨再试试对同一块数据使用memcpy
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
MyMemcpy(arr+2, arr, 5 * sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
1 2 1 2 1 2 1 8 9 10
哟?这结果…
哦哦!原来是把待拷贝的数据都给改了:memcpy对同一块内存空间拷贝的结果是未定义的
(库函数的memcpy是能做到, 但是这样的操作仍旧是标准未定义的)
想要对同一块空间的数据进行拷贝操作,可以用memmove…
1.2 memmove
将一块数据移动到另一块
那么memmove是怎么避免待拷贝数据被修改的呢?
- dest > src:如果从 2-5 依次拷贝,就会覆盖掉待拷贝的数据
如何解决?倒着拷贝:从 5-2 依次拷贝
- dest < src:从前到后依次拷贝
- dest == src 不必多说
void* MyMemove(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
if (dest < src)
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
MyMemove(arr + 2, arr, 5 * sizeof(arr[0]));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
1 2 1 2 3 4 5 8 9 10
1.3 memcmp
逐个字节比较数据
int MyMemcmp(void* buf1, void* buf2)
{
assert(buf1 && buf2);
while (*(char*)buf1 != '\0' && *(char*)buf2 != '\0' && *(char*)buf1 == *(char*)buf2)
{
buf1 = (char*)buf1 + 1;
buf2 = (char*)buf2 + 1;
}
if (*(char*)buf1 > *(char*)buf2)
return 1;
else if (*(char*)buf1 < *(char*)buf2)
return -1;
else
return 0;
}
int main()
{
char str1[] = "abcdfe";
char str2[] = "abcdef";
printf("%d\n", MyMemcmp(str1, str2));
return 0;
}
1
1.4 memset
:按字节设置内存空间的内容
- dest - 目标空间
- c - 要设置的character
- count - character的个数(记得是字节)
使用的时候要记住:memset是按字节设置的!
2.动态内存开辟
动态内存的开辟都是在堆区上
开辟主要是三个开辟函数
释放主要是一个释放函数
2.1 malloc
开辟一块连续可用的空间,并返回这块空间的地址
拆解:
- size 是要开辟的空间的大小,字节作单位
要点:
- malloc 的返回值是void*,用什么指针维护这块内存,就要强转成什么类型的指针
- malloc 开辟成功就返回其开辟的内存空间的地址,开辟失败返回NULL
使用:
开辟成功:
int main()
{
//开辟
//1.malloc的返回值是void*,我们用int*的指针维护,就需要强转
int* p = (int*)malloc(10 * sizeof(int));
//2.由于malloc不一定开辟成功(若开辟得太大),需要检查返回值(开辟失败返回NULL)
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
:
0 1 2 3 4 5 6 7 8 9
再来看看开辟失败的情况:
int main()
{
//开辟
//1.malloc的返回值是void*,我们用int*的指针维护,就需要强转
int* p = (int*)malloc(INT_MAX);
//2.由于malloc不一定开辟成功(若开辟得太大),需要检查返回值(开辟失败返回NULL)
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
:
Not enough space
根据错误码打印了“空间不足”的信息
2.2 free
释放动态开辟的内存空间
拆解:
- memblock 是要释放的内存块的起始地址
要点:
- 只能用于释放动态开辟的内存空间
- free掉 pointer 指向的空间(这时的pointer是野指针),要将 pointer 置空…
不然还是可以用这个 pointer,太危险 - 释放时不能只释放一部分(开辟了一大块就释放一大块)
- 有动态开辟的地方就有free(开辟不释放——内存泄漏)
使用:
int main()
{
//开辟
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno);
return 1;
}
//使用...
//释放
free(p);
p = NULL;
return 0;
}
注意:
- 传过去的指针,一定是动态开辟内存的起始位置
- 传 NULL 时,free什么都不做
2.3 calloc
为 num 个 size 大小的元素开辟一块空间,并把每个字节初始化成0
好像…
calloc ~= malloc + memset?
拆解:
- num 是要开辟的空间所存放的元素个数
- size 是元素大小
- 直接这样理解:为 num 个 size 大小的元素开辟一块空间
为什么malloc的参数和calloc的不一样?
:malloc的参数只有一个size(元素大小);而calloc加了个初始化就必须要num(元素个数)和size(元素大小),calloc难道不能直接一个一个字节初始化吗?为什么要传多一个参数?
解惑
:如果说我想开辟 854个 int类型的空间
如果直接传字节数,这么写:calloc( 854*sizeof(int) )
传元素个数和大小就这么写:calloc(854, sizeof(int))
写法不同罢了
要点:
和malloc无异
使用:
int main()
{
//开辟
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
:
0 0 0 0 0 0 0 0 0 0
动态管理动态管理,讲半天咋还没看见动态的影子?!
别急别急,这不就来了嘛…
2.4 realloc
调整动态开辟的空间的大小
拆解:
- memblock 是要调整的内存块的起始地址
- size 是调整后的新大小(byte)
要点:
- 返回值的接收要用临时指针接收(以防开辟失败返回NULL,导致原指针被误置空)
- 扩容有两种可能:
(1)原内存空间后的空间充足,可以直接扩容
(2)原内存空间后的空间不足,realloc会在堆区上另找一块连续可用的空间开辟,然后返回新空间的地址
改正:在堆区上新找的另一块连续可用空间
使用:
int main()
{
//开辟10个整型的空间
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//业务处理
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
printf("\n");
//业务需要:将p指向的空间扩容至20个整型
p = (int*)realloc(p, 20 * sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//业务处理
for (i = 0; i < 20; i++)
{
p[i] = i;
}
for (i = 0; i < 20; i++)
{
printf("%d ", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
嗯… 很不错…吗?
不,我们漏了一处:对realloc的返回值疏忽了
用原指针接收realloc的返回值可以吗?
不行,如果开辟失败返回NULL,那原指针指向的内存也找不到了!
所以用一个临时指针接收并检查,确保没问题了再把临时指针赋给原指针
int* tmp = (int*)realloc(p, 20 * sizeof(int));
if (NULL == tmp)
{
printf("%s\n", strerror(errno));
return 1;
}
p = tmp;
3.常见错误
3.1 对NULL的解引用
int main()
{
//开辟
int* p = (int*)malloc(INT_MAX);
//使用
*p = 0;
//释放
free(p);
p = NULL;
return 0;
}
分析:
如果开辟失败返回了空指针,那不就是对NULL解引用了嘛…
解决办法:
对这些内存函数的返回值检查
int main()
{
//开辟
int* p = (int*)malloc(INT_MAX);
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
*p = 0;
//释放
free(p);
p = NULL;
return 0;
}
3.2 对动态开辟的空间越界访问
int main()
{
//开辟
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i <= 10; i++)
{
p[i] = 0//当i == 0,越界访问
}
//释放
free(p);
p = NULL;
return 0;
}
分析:
没有清楚自己开辟的空间大小,导致使用时越界访问
解决方案:
明了自己开辟了多大的空间,合理使用
3.3 free非动态开辟的空间
int main()
{
int arr[10];
free(arr);
return 0;
}
分析:
脑子卡了
解决办法:
没什么解决方案,一般这个情况是脑子卡了,重启一下脑子
3.4 不完全free
用free释放动态开辟的空间的一部分/指针动了
int main()
{
//开辟
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
*p = 0;
p++;
}
//释放
free(p);
p = NULL;
return 0;
}
分析:
只释放了后五个整形元素
解决办法:
对于指向动态开辟的内存空间的指针使用慎重:不乱改
3.5 对同一块空间多次释放
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
free(p);
free(p);
p = NULL;
return 0;
}
分析:
脑子卡了
解决办法:
留心
3.6 内存泄漏
来个狠的看看严重性…
int main()
{
int* p;
while (1)
{
p = (int*)malloc(1);
}
return 0;
}
好在现在的操作系统都做了很好的优化,放在以前真是要死机了
再看一个:
int main()
{
int* p = (int*)malloc(40);
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
//...
//...
//...
//...
//总算把程序写完了!
return 0;
}
总算把程序写完了…吗?
在这种情况下更容易忘记释放了
分析:
动态开辟的内存没有释放
解决办法:
哪里有动态开辟哪里就有free!!!
3.7 返回栈空间地址
将局部数据(栈空间)的地址返回会怎样?
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
:
10
好像不会怎样?
不不不,你再耐心测一下
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("bacon\n");
printf("%d\n", *p);
return 0;
}
:
bacon
6
分析:
将局部变量的地址返回
出了test函数,a就销毁,此时的p指向的是已经不属于我们的空间,一旦有任何数据把a原来的空间覆盖,那结果又随缘了
其实就是函数栈帧的事儿:
如果没有别的数据来捣乱,咱们的p还是能找到10并打印
但这种代码终究是有问题的,来找出它的病根
咱们已经知道栈区上的空间使用是从高地址向低地址使用的,所以原来a的位置上的数据就很容易被覆盖掉了
4. 典题
(1)
程序运行的结果是?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
test();
return 0;
}
;
烫烫烫烫烫烫烫烫麿
分析:
返回栈空间地址,解引用找到的空间内的数据可能已经被覆盖
(2)
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
分析:
malloc开辟的空间由局部变量p维护,出了函数就销毁,没人维护就造成内存泄漏,而str也并没有获得能够维护的空间,仍然是NULL
要拷贝字符串到NULL上,程序肯定崩溃
(3)
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
分析:
大体上没问题,但是malloc开辟的空间没有释放
(4)
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
分析:
对于动态开辟的空间,释放后没有对其指针置空,而后又在strcpy、printf中对野指针解引用,危险啊!!!
5.汲取
内存函数:
(1)malloc
开辟 size个字节
(2)calloc
开辟 num个大小为size的元素的空间
(3)realloc
将 memoryblock 处的空间调整为 size个字节
动态内存管理:
(1)开辟
- 返回值要检查和强转
- 开辟完一定要记得释放,避免内存泄漏
(2)使用
- 明确自己开辟了多大空间,避免越界访问
- 不要轻易改动指向开辟空间的指针
(3)释放
- 释放后要置空
本期分享就到这啦,不足之处望请斧正
培根的blog,和你共同进步!